Compare commits

...
Author SHA1 Message Date
gorkem-bwl 0f39be632f style: fix Prettier formatting in documentation content 2026-01-18 14:13:52 -05:00
gorkem-bwl 9559b40e67 docs: add Docker, Bulk Import, and Account Settings documentation
Add three new documentation articles to the user guide:
- Docker monitors: Setup and configuration for container monitoring
- Bulk import monitors: CSV import guide for creating multiple monitors
- Account settings: Profile, password, and account management

Updates article counts in collections accordingly.
2026-01-17 15:26:08 -05:00
gorkem-bwl 914d94497d docs: add Checks and Logs documentation collections 2026-01-17 14:29:17 -05:00
gorkem-bwl 8eb9b70c9b docs: add Checks and Logs to sidebar navigation list 2026-01-17 14:25:20 -05:00
gorkem-bwl bf55c21e79 fix: add right padding to content for sidebar tab bar 2026-01-17 14:24:05 -05:00
gorkem-bwl 9d44e11496 refactor: migrate HomeLayout from CSS to MUI sx styling 2026-01-17 14:22:07 -05:00
gorkem-bwl 478e9671d4 fix: revert padding approach, let sidebar overlay content 2026-01-17 13:39:07 -05:00
gorkem-bwl c44fd22031 fix: use different gaps for open and closed sidebar states 2026-01-17 13:10:57 -05:00
gorkem-bwl 62dc6453f1 fix: increase minimum gap between content and sidebar 2026-01-17 13:10:05 -05:00
gorkem-bwl 7663ad6c74 fix: add right padding to content wrapper for sidebar spacing 2026-01-17 13:09:54 -05:00
gorkem-bwl 2f7de5d103 fix: account for tab bar border and add minimum gap 2026-01-17 13:09:08 -05:00
gorkem-bwl 4e1f9bc431 fix: use margin instead of padding for sidebar spacing 2026-01-17 13:06:14 -05:00
gorkem-bwl 6b1d574698 fix: prevent user guide sidebar from overlaying main content 2026-01-17 13:04:30 -05:00
gorkem-bwl b196d0de78 style: format user guide files with prettier 2026-01-17 12:39:31 -05:00
gorkem-bwl c7107a5034 feat: add user guide documentation sidebar
- Add comprehensive user guide with 10 documentation collections
- Include 49 articles covering all Checkmate features
- Implement sidebar navigation with search functionality
- Support light/dark theme with proper color tokens
- Add article content renderer with multiple block types
- Include image lightbox for documentation screenshots
2026-01-17 10:49:16 -05:00
Alexander Holliday 173a4e17d2 Merge pull request #3138 from bluewave-labs/fix/password-special-chars
fix: remove <> from allowed password characters and display allowed special chars
2026-01-16 13:17:12 -08:00
Alexander Holliday 70b197675e Merge pull request #3146 from bluewave-labs/fix/pagespeed-snapshot
remove pagespeed from snapshot array, full check list required
2026-01-16 13:16:37 -08:00
Alex Holliday 11dcccc085 remove pagespeed from snapshot array, full check list required 2026-01-16 21:16:05 +00:00
Alexander Holliday e3e08da259 Merge pull request #3145 from bluewave-labs/fix/pagespeed-stats
fix: stats fixes
2026-01-16 13:06:49 -08:00
Alex Holliday 6447c315ce stats fixes 2026-01-16 21:04:41 +00:00
Gorkem Cetin (BWL) f1a6afb860 Merge pull request #3133 from bluewave-labs/fix/ui-improvements-logs-incidents
Improve UI consistency for Logs and Incidents pages
2026-01-16 14:47:21 -05:00
gorkem-bwl 5c52a208b9 Fix build errors and formatting
- Replace CheckIcon SVG import with Lucide Icon component in
  LatestIncidentsPanel/index.jsx
- Format IncidentItem.jsx and StatisticsPanel.jsx with Prettier
2026-01-16 14:44:28 -05:00
gorkem-bwl 4fdc0bde5c Merge develop into fix/ui-improvements-logs-incidents
Resolve conflicts in IncidentItem.jsx and StatisticsPanel.jsx by
keeping the UI improvements from this branch while adopting the
new Lucide Icon component from develop (replacing old SVG imports).
2026-01-16 14:42:29 -05:00
Alexander Holliday 3d4ebe9588 Merge pull request #3143 from bluewave-labs/fix/responsive-layout-overflow
Fix horizontal overflow causing white space on narrow screens
2026-01-16 11:18:02 -08:00
gorkem-bwl 62ea7b0e5b Fix horizontal overflow causing white space on narrow screens
Add overflow-x: hidden and width: 100% to .home-layout to prevent
horizontal scrollbar. Add min-width: 0 to .home-content-wrapper to
allow flexbox child to shrink below content size at narrow widths.
2026-01-16 14:09:37 -05:00
Alexander Holliday 8747c38f40 Merge pull request #3142 from bluewave-labs/fix/infra-config
infra fix
2026-01-16 10:38:57 -08:00
Alex Holliday d633fc5ccb infra fix 2026-01-16 18:37:14 +00:00
Alexander Holliday 0d7a35367f Merge pull request #3141 from bluewave-labs/feat/job-queue-ts
feat/job queue ts
2026-01-16 10:25:40 -08:00
Alex Holliday af141ec772 build fix 2026-01-16 18:25:28 +00:00
Alex Holliday 5784fec3d1 format 2026-01-16 18:17:55 +00:00
Alex Holliday 95cd33ac06 queue and buffer refactor 2026-01-16 18:11:04 +00:00
Gorkem Cetin (BWL) 2a591ee503 Merge pull request #3139 from bluewave-labs/feat/lucide-icon-migration
Migrate icons from custom SVGs and MUI to Lucide React
2026-01-16 12:50:21 -05:00
gorkem-bwl f5aa24d77e fix: rename Image import to ImageIcon to avoid conflicts 2026-01-16 07:34:03 -05:00
gorkem-bwl 7874439e4c fix: address PR review feedback for icon migration
- Use AlertCircle for error and AlertTriangle for warning icons
- Use Icon color prop instead of CSS fill for Lucide stroke-based icons
- Replace wildcard import with explicit named imports for tree-shaking
- Replace sx prop with style prop on Icon components
- Add Trash2 to Icon component imports
2026-01-16 07:31:23 -05:00
gorkem-bwl 7140d436a5 style: apply Prettier formatting 2026-01-15 23:39:26 -05:00
gorkem-bwl ba0c89fc6c feat: migrate icons from custom SVGs and MUI to Lucide React
Replace custom SVG icons and @mui/icons-material with lucide-react for consistent,
lightweight icon management across the application.

## Changes
- Add lucide-react package and remove @mui/icons-material
- Create theme-aware Icon wrapper component with color resolution
- Create iconMap.js for legacy icon name mappings
- Migrate 64 components to use new Icon component
- Remove 74 unused SVG files from assets/icons/

## Benefits
- Consistent icon API across the application
- Smaller bundle size with tree-shakeable imports
- Theme-aware color support via MUI theme paths
- Simpler maintenance with single icon library
2026-01-15 23:37:50 -05:00
gorkem-bwl d91e87f484 fix: remove <> from allowed password characters and display allowed special chars
## Changes
- Remove < and > from password regex pattern in joi.js (these chars are
  stripped by DOMPurify sanitization which caused confusing validation errors)
- Update locale strings to display the list of allowed special characters
  in password tooltip, error messages, and PasswordPanel requirements

## Benefits
- Users now see exactly which special characters are allowed when setting passwords
- Eliminates confusing "password cannot be empty" errors when using < or >
- Regex now accurately reflects the characters that actually work

Fixes #3010
2026-01-15 22:50:29 -05:00
Alexander Holliday b5caabed20 Merge pull request #3135 from bluewave-labs/fix/pagespeed-monitor-delete
Fix pagespeed monitor deletion failing with undefined ID
2026-01-15 18:29:34 -08:00
gorkem-bwl 8814596d6e Fix pagespeed monitor deletion failing with undefined ID
The delete handler was using monitor._id from state, which was undefined
because the fetched monitor object didn't have _id populated. Changed to
use monitorId directly from URL params, which is already available and
used by other handlers in the same component (e.g., handlePause).

Fixes "Failed to delete monitor" error when removing pagespeed monitors.
2026-01-15 21:27:37 -05:00
gorkem-bwl c60d07c3a0 Improve Latest Incidents panel layout
- Simplify incident rows to single line with grid layout
- Remove redundant "Monitor:" and "Status:" labels
- Align columns vertically (monitor name, status badge, duration)
- Center-align status badge column
- Match icon styling with General Statistics (18px, strokeWidth 1.5)
- Remove pointer cursor (click not implemented)
- Add dividers and minHeight to Statistics panel for alignment
2026-01-15 21:19:34 -05:00
gorkem-bwl 54ecf02d9a Fix Prettier formatting in StatisticsPanel 2026-01-15 20:48:15 -05:00
gorkem-bwl 1ab31d19f3 Improve UI consistency for Logs and Incidents pages
## Changes

### Logs page
- Convert server logs from monospace text to DataTable with pagination
- Add table columns: Timestamp, Level, Service, Method, Message
- Add colored level badges (info=green, warn=yellow, error=red, debug=blue)
- Add pagination with configurable rows per page
- Align diagnostics gauge cards with 16px gap and responsive widths

### Incidents page
- Redesign summary cards with consistent styling
- Remove shadows and use 1px border matching other sections
- Reduce icon sizes to 18-24px (sidebar-style)
- Reduce font sizes (13px titles, 18-32px values)
- Add titles to all three summary cards
- Fix "Most Affected Monitor" showing "Unknown Monitor" when no incidents
- Show "N/A" for avg resolution time when no incidents
- Remove cluttered "Resolutions" progress bar section
- Remove horizontal dividers between statistics rows
- Standardize card padding and gap to 16px

### Uptime page
- Fix oversized monitor names in table (removed h6 variant)
2026-01-15 20:44:51 -05:00
Alexander Holliday 2f2ce34373 Merge pull request #3132 from bluewave-labs/fix/theme-simplification-and-bug-fixes
Simplify theme and fix Redux state bugs
2026-01-15 16:51:05 -08:00
Alex Holliday 0f764ca084 typing 2026-01-15 23:45:40 +00:00
gorkem-bwl 8b2ee8d937 Simplify theme and fix Redux state bugs
## Changes

- Remove unused semantic color groups (successSecondary, map, chatbot)
- Remove unused properties from primary and tertiary groups
- Inline accent.darker usage in globalTheme.js
- Fix greeting component crash with persisted Redux state
- Fix MonitorGrid key prop warning

## Details

Theme simplification:
- Reduced constants.js from 355 to 291 lines
- Removed 3 unused semantic color groups
- Removed 6 unused properties (contrastBorder, contrastBorderDisabled, etc.)
- Inlined darken() call for accent hover states

Bug fixes:
- Added null-safe selectors in greeting.jsx for state.ui.greeting
- Added initialization check in setGreeting reducer for older persisted state
- Fixed key prop in MonitorGrid to handle both _id and id
- Removed unused console.log and Grid2 import
2026-01-15 18:06:40 -05:00
Alex Holliday ed80837810 related serverices to TS 2026-01-15 22:33:25 +00:00
Alexander Holliday d82877b28d Merge pull request #3121 from web-engineer/fix/3120-data-leak
3120 remove URL's from data when they are supposed to be disabled
2026-01-15 14:24:47 -08:00
Alex Holliday cf67c82f5e convert job queue and job queue helper 2026-01-15 22:06:46 +00:00
Alex Holliday 5a7b51bfec convert super simple queue to ts 2026-01-15 21:49:48 +00:00
Alexander Holliday 206dcae196 Merge pull request #3131 from bluewave-labs/fix/remove-monitor-db-module
fix: remove db module
2026-01-15 13:05:12 -08:00
Alex Holliday 56bfbe2c77 format 2026-01-15 21:05:00 +00:00
Alex Holliday 8aac3086b8 build error 2026-01-15 21:03:35 +00:00
Alex Holliday 9255500bfc remove db module 2026-01-15 20:55:01 +00:00
Alexander Holliday e99b87fc1e Merge pull request #3130 from bluewave-labs/fix/monitor-service
Fix/monitor service
2026-01-15 11:40:33 -08:00
Alex Holliday ec953ff262 format 2026-01-15 19:40:22 +00:00
Alex Holliday 68559741ba remove mongo middleware 2026-01-15 19:36:41 +00:00
Alex Holliday 74a7f0a701 remove verify team access 2026-01-15 18:33:03 +00:00
Alex Holliday 75bcf9df58 deleteById 2026-01-15 18:24:58 +00:00
Alex Holliday 6b5fd8d17f update, pause 2026-01-15 18:21:36 +00:00
Alexander Holliday a1069f487b Merge pull request #3128 from bluewave-labs/new-uptime-time-range
New uptime time range
2026-01-15 09:28:30 -08:00
Craig Lawson 420164d616 conditional simplification as reqeusted 2026-01-15 12:52:11 +00:00
Craig Lawson f0a6fc4fdf Merge upstream/develop into fix/3120-data-leak 2026-01-15 11:06:27 +00:00
karenvicent e306499bce Merge branch 'develop' into new-uptime-time-range 2026-01-14 22:50:58 -05:00
karenvicent 1472a09718 add new uptime intervals 2026-01-14 22:43:25 -05:00
Alex Holliday e7e4ce7f57 pagination fix 2026-01-14 15:57:17 -08:00
Alex Holliday 93c24d2f5c pagination and filter fix 2026-01-14 15:44:52 -08:00
Alex Holliday cdbaad5bd9 initial 2026-01-14 23:34:05 +00:00
Alexander Holliday b1b27be731 Merge pull request #3127 from bluewave-labs/feat/monitor-service-ts
Feat/monitor service ts
2026-01-14 14:49:00 -08:00
Alexander Holliday 01f005ca41 Merge branch 'develop' into feat/monitor-service-ts 2026-01-14 14:47:25 -08:00
Alex Holliday 1326a6222b format 2026-01-14 22:45:47 +00:00
Alex Holliday aea0b34533 format 2026-01-14 22:45:39 +00:00
Alex Holliday d4af86f26c pagespeed details 2026-01-14 22:44:23 +00:00
Alex Holliday 940247b7bf teamid 2026-01-14 21:47:37 +00:00
Alex Holliday 2b0c523047 unify requests 2026-01-14 20:11:05 +00:00
Alex Holliday 4ad09fad4f snapshots 2026-01-14 20:06:37 +00:00
Alex Holliday 665eb9a67f hardware details 2026-01-14 19:28:17 +00:00
Alex Holliday d1faeab042 fix params 2026-01-14 00:04:22 +00:00
Alex Holliday 76fa99f7bd getUptimeDetails 2026-01-13 23:59:09 +00:00
Alex Holliday 77764c51ee addDemo 2026-01-13 23:22:34 +00:00
Alex Holliday 8a986d2e58 add indexes 2026-01-13 22:56:51 +00:00
Alexander Holliday e57b161be9 Merge pull request #3126 from bluewave-labs/fix/checks-index
add index and cutof
2026-01-13 14:46:24 -08:00
Alex Holliday 8de06df8c4 add index and cutof 2026-01-13 14:45:54 -08:00
Alexander Holliday 8e359cf145 Merge pull request #3125 from bluewave-labs/fix/check-repository
fix: check repo fix
2026-01-13 13:46:27 -08:00
Alex Holliday 52e598a66e Merge branch 'fix/check-repository' into feat/monitor-service-ts 2026-01-13 21:46:17 +00:00
Alex Holliday 0a60cc0363 check repo fix 2026-01-13 21:44:16 +00:00
Alex Holliday 75788f7cb5 Merge branch 'develop' into feat/monitor-service-ts 2026-01-13 20:22:34 +00:00
Alexander Holliday b58a7ad053 Merge pull request #3123 from bluewave-labs/feat/time-series
Feat/time series
2026-01-13 12:21:18 -08:00
Alex Holliday f763126b38 format' 2026-01-13 20:20:50 +00:00
Alex Holliday e79bf28bdc server format 2026-01-13 20:20:29 +00:00
Alex Holliday d0b3bc60df create and update 2026-01-13 20:20:00 +00:00
Alex Holliday 117a2e00c4 status page fix 2026-01-13 19:01:56 +00:00
Alex Holliday f8e88623a8 hardware details pipeline refactor 2026-01-13 18:54:28 +00:00
Alex Holliday 705475e971 _id -> id 2026-01-13 18:37:22 +00:00
Alex Holliday 2b0a51520a infra 2026-01-13 17:50:31 +00:00
Alexander Holliday 55145a38ff Merge pull request #3105 from CodeShellDev/patch-1
bump mongo image version for arm arch to `.30`
2026-01-12 22:10:10 -08:00
Alex Holliday 25d1039376 update pagespeed 2026-01-12 23:57:39 +00:00
Alex Holliday 7a6b0d5da4 refactoring 2026-01-12 22:58:46 +00:00
Alex Holliday e090d3cfd3 -> timeseries 2026-01-12 19:32:23 +00:00
Craig Lawson 1c1ac25857 fix saveSettings is currently being called even when validation fails. 2026-01-12 08:56:24 +00:00
Craig Lawson 3ce40b1ab8 Fix handleSave function syntax errors 2026-01-11 22:48:17 +00:00
Craig Lawson 1494df9eb3 missing brace 2026-01-11 22:39:49 +00:00
Craig Lawson a70959e4a6 Nitpick fix - ref -
client/src/Pages/v1/Settings/index.jsx (1)
171-175: LGTM!

The Redux dispatch provides immediate UI feedback while the actual persistence happens on Save. This is the correct pattern for a server-persisted setting.

Minor note: The ?? value fallback is unreachable for showURL since newValue is always set to a boolean at line 96. Consider simplifying to dispatch(setShowURL(newValue)) for clarity, though the current code is functionally correct.
2026-01-11 22:36:05 +00:00
Craig Lawson b6f48a910f Revert caching changes - restore working implementation
- Remove settingsModule caching that caused inconsistent state
- Restore direct AppSettings query in statusPageModule
- Keep immediate Redux dispatch for UI feedback
- All functionality verified working"
2026-01-11 22:08:15 +00:00
Craig Lawson e4d101237e bug fix - settings handlechange 2026-01-11 21:51:59 +00:00
Craig Lawson 7556086f71 code review updates
Summary of what we fixed based on AI review feedback:

Formatting - Ran Prettier to fix code formatting
Redux state rollback - Moved Redux dispatch to after successful save in settingsHooks.js
Performance - Added caching layer in settingsModule (1-minute TTL) to prevent DB query on every status page request
Architecture - Inject settingsModule instead of raw AppSettings model for better encapsulation
2026-01-11 20:51:25 +00:00
Craig Lawson 7b619e6b31 Remove debug logging and add feature documentation
- Remove all console.log statements added during development
- Clean up code for production readiness
2026-01-11 18:56:16 +00:00
Craig Lawson 76ab2695db missing setting 2026-01-11 18:23:58 +00:00
Craig Lawson 418068e702 loggin settings 2026-01-11 18:18:38 +00:00
Craig Lawson 12b28ffb3f type issue 2026-01-10 22:30:03 +00:00
Craig Lawson 526fa24eab validation bug 2026-01-10 22:23:31 +00:00
Craig Lawson a13f6a34c4 missing value 2026-01-10 22:02:36 +00:00
Craig Lawson f659876845 select bug 2026-01-10 21:58:13 +00:00
Craig Lawson 643a743d70 more logging 2026-01-10 21:49:10 +00:00
Craig Lawson ab4cc80628 3120 persist admin value 2026-01-10 21:29:15 +00:00
Craig Lawson fb3795bd70 3120 remove URL's from data when they are supposed to be disabled 2026-01-10 12:15:29 +00:00
Alexander Holliday 5c14c68899 Merge pull request #3119 from bluewave-labs/fix/controllerUtils
js -> ts
2026-01-09 15:59:18 -08:00
Alexander Holliday 5a878e89d0 Merge pull request #3118 from bluewave-labs/fix/check-controller
js->ts
2026-01-09 15:58:45 -08:00
Alex Holliday bdb07220ed js -> ts 2026-01-09 15:58:31 -08:00
Alexander Holliday 641d5e6dda Merge pull request #3117 from bluewave-labs/fix/diagnostic-controller
js->ts
2026-01-09 15:54:02 -08:00
Alex Holliday b24339f232 js->ts 2026-01-09 15:53:53 -08:00
Alexander Holliday ff324fe6e7 Merge pull request #3116 from bluewave-labs/fix/incidentController
js->ts
2026-01-09 15:46:39 -08:00
Alex Holliday fee9b8a0de js->ts 2026-01-09 15:46:30 -08:00
Alexander Holliday ad9488f54e Merge pull request #3115 from bluewave-labs/fix/invite-controller
js -> ts
2026-01-09 15:44:23 -08:00
Alex Holliday ed77fc3a9d js->ts 2026-01-09 15:44:11 -08:00
Alexander Holliday babd8e672f Merge pull request #3114 from bluewave-labs/fix/log-controller
js->ts
2026-01-09 15:39:35 -08:00
Alex Holliday 8ceac493ef js -> ts 2026-01-09 15:39:30 -08:00
Alexander Holliday fee808789f Merge pull request #3113 from bluewave-labs/fix/mw-controller
js->ts
2026-01-09 15:35:53 -08:00
Alex Holliday dd03c554c9 js->ts 2026-01-09 15:35:48 -08:00
Alex Holliday 3ab4fcb277 js->ts 2026-01-09 15:32:53 -08:00
Alexander Holliday 11e3cac440 Merge pull request #3112 from bluewave-labs/fix/status-page-controller
js->ts
2026-01-09 15:26:37 -08:00
Alex Holliday 1d7f3b15a4 js->ts 2026-01-09 15:21:56 -08:00
Alexander Holliday b7ad3c036b Merge pull request #3111 from bluewave-labs/fix/settings-controller
js->ts
2026-01-09 15:20:30 -08:00
Alex Holliday 7b2ff593a4 js->ts 2026-01-09 15:16:17 -08:00
Alexander Holliday 916f3c0921 Merge pull request #3110 from bluewave-labs/feat/queue-controller
Feat/queue controller
2026-01-09 15:13:31 -08:00
Alex Holliday 846f1dfa6c remove extra param 2026-01-09 15:07:38 -08:00
Alex Holliday 3f631ff382 js -> ts 2026-01-09 15:07:02 -08:00
Alexander Holliday 43a370e162 Merge pull request #3109 from bluewave-labs/feat/notification-controller
js -> ts
2026-01-09 15:05:13 -08:00
Alex Holliday 64320c7aca js -> ts 2026-01-09 15:02:19 -08:00
Alexander Holliday 61cedfb2b7 Merge pull request #3108 from bluewave-labs/feat/monitor-controller
monitor controller -> ts
2026-01-09 14:51:17 -08:00
Alex Holliday a9ce980d03 monitor controller -> ts 2026-01-09 14:49:44 -08:00
Alexander Holliday 8409f08a2d Merge pull request #3107 from bluewave-labs/feat/be/ts
ts config
2026-01-09 13:38:55 -08:00
Alex Holliday ef2cccf16e ts config 2026-01-09 13:36:58 -08:00
CodeShell 0285302ecc bump mongo image version for arm arch to .30 2026-01-09 10:36:46 +00:00
Alexander Holliday 668ea7418d Merge pull request #3088 from bluewave-labs/fix-traslator-error
fix translator error and add format
2025-12-22 09:41:58 -08:00
karenvicent b12bdfcc22 fix traslator error and add format 2025-12-22 12:36:29 -05:00
Alexander Holliday 65de627870 Merge pull request #3079 from Antoplt/feat/choose-disks-monitor
feature requested #2840: improve disk device and mountpoint display with hover tooltips
2025-12-18 10:53:24 -08:00
Alexander Holliday 03d1dcaefb Merge branch 'develop' into feat/choose-disks-monitor 2025-12-18 10:53:01 -08:00
Alexander Holliday 14768bde28 Merge pull request #3086 from bluewave-labs/fix-incidents-duplication-front-end
Fix incidents duplication front end
2025-12-18 10:51:52 -08:00
karenvicent 9555ff43c7 add llama suggestions and fix genericFallback error 2025-12-18 12:47:01 -05:00
karenvicent 75acb1ade6 Merge branch 'develop' into fix-incidents-duplication-front-end 2025-12-17 18:56:25 -05:00
karenvicent fd12ca6e21 incidents feature fron-end 2025-12-17 18:51:34 -05:00
ppmar ff2f84d46b resolve disk selection persistence and validation issues 2025-12-17 23:50:40 +01:00
Alexander Holliday 1ffce112d1 Merge pull request #3077 from web-engineer/feat/helm-tls-letsencrypt-support
feat(helm): add TLS/HTTPS support with cert-manager integration
2025-12-16 15:25:05 -08:00
Alexander Holliday 382df34f06 Merge pull request #3085 from Preetham001-SR/fix-hardcoded-messages
fix: replace hardcoded response messages with localized strings
2025-12-16 15:22:19 -08:00
Preetham001-SR 124f300362 fix: replace hardcoded response messages with localized strings
- Replace hardcoded 'OK' messages in monitorController with proper localized strings
- Add new string constants to en.json for consistent messaging
- Follow existing pattern used by other controller methods
- Resolves TODO comment in getMonitorsAndSummaryByTeamId method

Fixes: Hardcoded response messages inconsistent with i18n architecture
2025-12-14 23:45:32 +05:30
Alexander Holliday f782899768 Merge pull request #3080 from bluewave-labs/feat/remove-v2
Remove business V2 logic
2025-12-05 09:05:17 -08:00
Sajanpreet Singh cdb83bb47d Fix build checks 2025-12-04 10:42:18 -06:00
Sajanpreet Singh 72bcc0d020 Remove business V2 logic 2025-12-04 10:31:45 -06:00
Craig Lawson ac901b46c3 chore(helm): default TLS secretName to <release>-* and remove duplicate prechecks file; update docs with protocol flags 2025-12-03 21:43:21 +00:00
Craig Lawson c1b3d325f6 feat(helm): enforce TLS + protocol validation and improve docs\n\n- Add prechecks.yaml to validate TLS issuer annotations and secretName\n- Require https when TLS enabled; prevent client https + server http mixed content\n- Validate client.protocol in addition to server.protocol\n- Update INSTALLATION.md to set protocol: https when enabling TLS\n- Clarify values.yaml comments for TLS usage 2025-12-03 21:30:35 +00:00
Craig Lawson 7cad2fec27 chore(helm): comment out secretName in TLS config, add usage note for cert-manager 2025-11-30 16:46:06 +00:00
Adevixxx 7edc2a71d8 Ajout de l'interface pour selectionner des disques et modifications du coté serveur pour la sauvegarde 2025-11-30 11:48:54 +01:00
Antonin 913f7a9e42 Merge branch 'develop' into feat/choose-disks-monitor 2025-11-30 11:35:34 +01:00
Craig Lawson dd98757b2a feat(helm): add TLS/HTTPS support with cert-manager integration
- Add TLS configuration to ingress templates (client & server)
- Add tls.enabled and tls.secretName to values.yaml
- Include comprehensive cert-manager documentation in INSTALLATION.md
- Add example annotations for Let's Encrypt cluster issuer
- Support both values.yaml and --set flag configuration methods
- Backward compatible (TLS disabled by default)

Enables automatic HTTPS certificate provisioning when cert-manager
is installed in the cluster with appropriate ClusterIssuer configured.
2025-11-29 17:46:29 +00:00
Alexander Holliday 1e3ee23120 Merge pull request #3071 from mospell/feat/better_infrastructure_notifications
Feat: Add link to Infrastructure Details for Notifications
2025-11-28 11:52:04 -08:00
Alexander Holliday 74d8e0a892 Merge pull request #3074 from SuryaGanesh284/develop
Add quick setup checklist to CONTRIBUTING.md
2025-11-28 11:51:06 -08:00
Antonin 50af063f64 Merge branch 'develop' into feat/choose-disks-monitor 2025-11-26 11:58:35 +01:00
Antonin 80a66d74ca Add metric/value three & four to propTypes 2025-11-26 11:56:46 +01:00
SuryaGanesh284 6c2908681a Add quick setup checklist to CONTRIBUTING.md
Added a quick setup checklist for contributors.
2025-11-25 22:12:16 +05:30
mospell 90593c0d7c added 'go-to' links to the remaining Discord notifications 2025-11-23 17:18:09 +01:00
mospell 8fb958e772 Feat: Add link to infrastructure details for Notifications 2025-11-23 16:39:09 +01:00
Alexander Holliday fc39ab3fb9 Merge pull request #3069 from bluewave-labs/feat/JSON-export
implement JSON export
2025-11-21 10:45:32 -08:00
Alex Holliday 58e7ef2527 implement JSON export 2025-11-21 10:43:29 -08:00
Alexander Holliday fec32380ae Merge pull request #3067 from bluewave-labs/fix-incidents-creation
Incident Management System - Backend Implementation
2025-11-19 09:35:15 -08:00
karenvicent 3b8d87bd17 Merge branch 'develop' into fix-incidents-creation 2025-11-19 10:06:35 -05:00
karenvicent 131ced10fc Incidents management implementation 2025-11-18 16:01:47 -05:00
antoplt 886752effd Add metricFour and valueFour props 2025-11-18 15:29:25 +01:00
antoplt a5d54a637e Fix function naming error 2025-11-18 14:44:45 +01:00
antoplt 1b4edf16fb Add mountpoint as fourth metric 2025-11-18 14:38:03 +01:00
antoplt 4042d34585 Add mountpoint field to mongoose diskSchema 2025-11-18 14:25:02 +01:00
antoplt 135c3ff454 Merge branch 'develop' into feat/choose-disks-monitor 2025-11-18 13:55:39 +01:00
antoplt f8d5791fab Add device name formatting 2025-11-18 13:13:35 +01:00
antoplt ffa4b1695b Add device field into moogose diskSchema 2025-11-18 13:12:34 +01:00
antoplt 98214ff578 Add disk device as third metric value 2025-11-18 12:06:22 +01:00
Alexander Holliday 57ceac0c24 Merge pull request #3064 from bluewave-labs/statusPage-bars-centred-fix
Status page bars centred fix
2025-11-12 11:02:32 -08:00
Sajan a526bc5a3e Revert changes to lock files 2025-11-12 12:06:34 -06:00
Alexander Holliday fac4516353 Merge pull request #3058 from yuracoff18/doccumentation-fix-and-problem-solving-contributing
Fix the location from the docker images installation and addition of …
2025-11-12 09:21:37 -08:00
Alexander Holliday 4db13fb924 Update CONTRIBUTING.md 2025-11-12 09:21:02 -08:00
Sajan c97ee22e66 Move status validation inside the block 2025-11-12 10:54:55 -06:00
Sajan 98737f92af Add validation for the status prop 2025-11-12 10:34:22 -06:00
Sajan 74637f9d2a Remove fontSize 2025-11-12 10:21:26 -06:00
Sajan 69dbc2fe7a Move up/down chips next to the monitor name 2025-11-12 09:58:38 -06:00
Sajan d157e200f6 Use flex-end instead of right 2025-11-11 10:58:30 -06:00
Sajan 594540cff5 Fix centering issue for status page bars 2025-11-11 10:46:34 -06:00
Sajan 8e47e8cd3d fix formatting issue 2025-11-11 09:26:18 -06:00
Gorkem Cetin (BWL) c5d7625a06 Update installation instructions in README
Removed 'Coolify' from the list of alternatives for Checkmate installation.
2025-11-10 06:11:50 -05:00
Yura 7763c5e288 Fix the location from the docker images installation and addition of a solution from a problem i saw while installing the front 2025-11-09 01:15:05 -05:00
Yura 957988d977 Fix the location from the docker images installation and addition of a solution from a problem i saw while installing the front 2025-11-09 01:08:56 -05:00
Yura 0c04c06d4e centred the bars in status page UI 2025-11-09 00:45:33 -05:00
Yura a7ef014b92 centred the bars in status page UI 2025-11-09 00:41:02 -05:00
Yura 3822d971cb centred the bars in status page UI 2025-11-09 00:28:14 -05:00
karenvicent eb65c41d15 Merge branch 'develop' into fix-incidents-creation 2025-11-06 17:07:24 -05:00
karenvicent 0c3fdbc539 feat: add incident model, module and service with StatusService integration
- Create Incident model with resolution tracking fields
- Implement IncidentModule for data access
- Implement IncidentService with business logic
- Integrate with StatusService to auto-create/resolve incidents on status change
2025-11-06 17:06:33 -05:00
605 changed files with 25154 additions and 16694 deletions

No files matched your search

+3 -1
View File
@@ -1,3 +1,5 @@
.idea
.vscode
.VSCodeCounter
.VSCodeCounter
*.sh
mongo
+145
View File
@@ -0,0 +1,145 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Project Overview
Checkmate is an open-source uptime and infrastructure monitoring application. It monitors server hardware, uptime, response times, and incidents with real-time alerts. The companion agent [Capture](https://github.com/bluewave-labs/capture) provides infrastructure metrics (CPU, RAM, disk, temperature).
## Development Commands
### Client (React/Vite)
```bash
cd client
npm install
npm run dev # Start dev server at http://localhost:5173
npm run build # TypeScript check + production build
npm run lint # ESLint (strict, max-warnings 0)
npm run format # Prettier formatting
npm run format-check # Check formatting
```
### Server (Node.js/Express)
```bash
cd server
npm install
npm run dev # Start with hot-reload (nodemon + tsx) at http://localhost:52345
npm run build # TypeScript compile + path alias resolution
npm run test # Run Mocha tests with c8 coverage
npm run lint # ESLint v9
npm run lint-fix # Auto-fix lint issues
npm run format # Prettier formatting
```
### Docker Development
```bash
cd docker/dev
./build_images.sh
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
```
## Environment Setup
### Server `.env` (minimum required)
```env
CLIENT_HOST="http://localhost:5173"
JWT_SECRET="my_secret_key_change_this"
DB_CONNECTION_STRING="mongodb://localhost:27017/uptime_db"
TOKEN_TTL="99d"
ORIGIN="localhost"
LOG_LEVEL="debug"
```
### Client `.env`
```env
VITE_APP_API_BASE_URL="http://localhost:52345/api/v1"
VITE_APP_LOG_LEVEL="debug"
```
## Architecture
### Monorepo Structure
- `/client` - React 18 + TypeScript + Vite + MUI frontend
- `/server` - Node.js 20+ + Express + TypeScript backend
- `/docker` - Multi-environment Docker configs (dev, staging, prod, arm, mono)
### Backend Layers
```
server/src/
├── controllers/ # Route handlers (authController, monitorController, etc.)
├── service/ # Business logic
│ ├── business/ # Core monitoring logic
│ ├── infrastructure/ # Server/system utilities
│ └── system/ # App-level settings
├── db/
│ ├── models/ # Mongoose schemas (Monitor, Check, Incident, User, etc.)
│ ├── migration/ # Database migrations (run on startup)
│ └── modules/ # Database-specific modules
├── middleware/v1/ # verifyJWT, rateLimiter, sanitization, responseHandler
├── routes/v1/ # API route definitions
├── validation/ # Joi input validation schemas
└── repositories/ # Data access layer
```
### Frontend Structure
```
client/src/
├── Components/ # Reusable UI components
├── Pages/ # Page components (Auth, Uptime, Infrastructure, Incidents, etc.)
├── Features/ # Redux slices (Auth, UI)
├── Hooks/ # Custom React hooks
├── Utils/ # Utilities (NetworkService.js is main API client)
├── Validation/ # Input validation
└── locales/ # i18n translations
```
### API
- Base URL: `/api/v1`
- Documentation: `http://localhost:52345/api-docs` (Swagger UI)
- OpenAPI spec: `/server/openapi.json`
### Key Technologies
- **State Management**: Redux Toolkit + Redux-Persist
- **Data Fetching**: SWR + Axios
- **Database**: MongoDB with Mongoose ODM
- **Queue/Cache**: Redis + BullMQ + Pulse (cron scheduling)
- **i18n**: i18next + react-i18next (translations via PoEditor)
## Code Conventions
### Internationalization
All user-facing strings must use the translation function:
```javascript
t('your.key') // Never hardcode UI strings
```
### Branching
- Always branch from `develop` (not master)
- Use descriptive names: `feat/add-alerts`, `fix/login-error`
- PRs target `develop` branch
### Formatting
- **Client**: Prettier with `printWidth: 90`, tabs, double quotes
- **Server**: Prettier with `printWidth: 150`, tabs, double quotes
- Both use ESLint with strict settings
### Testing
Server tests use Mocha + Chai + Sinon:
```bash
npm test # Run all tests with coverage
npm test -- --grep "pattern" # Run specific tests
```
Test files: `server/tests/**/*.test.js`
## Database Models
Key Mongoose models in `/server/src/db/models/`:
- **Monitor** - Monitoring configuration (website, infrastructure, port, etc.)
- **Check** - Individual monitoring check results
- **Incident** - Downtime incidents
- **User** - User accounts
- **Team** - Team/workspace management
- **StatusPage** - Public status pages
- **Notification** - Alert configuration (email, Discord, Slack, webhooks)
- **MaintenanceWindow** - Scheduled maintenance periods
- **AppSettings** - Global application settings
+29 -4
View File
@@ -10,6 +10,31 @@ PS: **We work closely with contributors on our [Discord channel](https://discord
---
## 🚀 Quick Setup Checklist
Before you dive in, make sure you have these installed:
```bash
# Check Node.js (v16+ required)
node --version
# Check npm
npm --version
# Check Docker
docker --version
# Check Git
git --version
```
**New to contributing?** Start here:
1. Pick a [`good-first-issue`](https://github.com/bluewave-labs/checkmate/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22)
2. Comment that you'd like to work on it
3. Follow the [setup guide](#set-up-checkmate-locally) below
4. Join our [Discord](https://discord.com/invite/NAb6H3UTjK) if you get stuck
## Table of contents
- [How do I...?](#how-do-i)
@@ -66,7 +91,7 @@ cd Checkmate
Navigate to the Docker dev directory:
```bash
cd server/docker/dev
cd docker/dev
```
Build the Docker images:
@@ -78,13 +103,13 @@ Build the Docker images:
Run MongoDB container:
```bash
docker run -d -p 27017:27017 -v $(pwd)/mongo/data:/data/db --name uptime_database_mongo uptime_database_mongo
docker run -d -p 27017:27017 -v uptime_mongo_data:/data/db --name uptime_database_mongo mongo:6.0
```
Navigate back to the root directory:
```bash
cd ../../..
cd ../..
```
#### Step 3: Set Up the Backend (Server)
@@ -161,7 +186,7 @@ Start the frontend:
npm run dev
```
The client will run at `http://localhost:5173`.
The client will run at `http://localhost:5173`.
#### Step 5: Access the Application
+2 -1
View File
@@ -52,7 +52,7 @@ Usage instructions can be found [here](https://docs.checkmate.so/checkmate-2.1).
See installation instructions in [Checkmate documentation portal](https://docs.checkmate.so/checkmate-2.1/users-guide/quickstart).
Alternatively, you can also use [Coolify](https://coolify.io/), [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
Alternatively, you can also use [Elestio](https://elest.io/open-source/checkmate), [K8s](./charts/helm/checkmate/INSTALLATION.md), [Sive Host](https://sive.host) (South Africa) or [Pikapods](https://www.pikapods.com/) to quickly spin off a Checkmate instance. If you would like to monitor your server infrastructure, you'll need [Capture agent](https://github.com/bluewave-labs/capture). Capture repository also contains the installation instructions.
### Using a Custom CA
@@ -91,6 +91,7 @@ Feel free to ask questions or share your ideas - we'd love to hear from you!
- Website monitoring
- Page speed monitoring
- Infrastructure monitoring (memory, disk usage, CPU performance, network etc) - requires [Capture](https://github.com/bluewave-labs/capture) agent
- Selective disk monitoring with mountpoint selection
- Docker monitoring
- Ping monitoring
- SSL monitoring
+73
View File
@@ -26,6 +26,7 @@ Edit `values.yaml` to update:
- Old: `persistence.redisSize` → New: `persistence.redis.size`
- Add: `persistence.mongo.storageClass` and `persistence.redis.storageClass` (leave empty for default)
- Secrets under the `secrets` section (`JWT_SECRET`, email credentials, API keys, etc.) — replace all change_me values
- **For TLS/HTTPS**: Configure ingress TLS settings (see section below)
### 3. Deploy the Helm chart
```bash
@@ -41,3 +42,75 @@ kubectl get svc
```
Once all pods are `Running` and `Ready`, you can access Checkmate via the configured ingress hosts.
## Enabling TLS/HTTPS with cert-manager
If you have [cert-manager](https://cert-manager.io/) installed in your cluster, you can enable automatic TLS certificate provisioning using Let's Encrypt or other certificate issuers.
### Prerequisites
- cert-manager installed in your cluster
- A ClusterIssuer or Issuer configured (e.g., `letsencrypt-prod`)
### Configuration
Edit `values.yaml` to enable TLS (and update protocols to https):
```yaml
client:
protocol: https
ingress:
enabled: true
host: checkmate.example.com
className: nginx
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: true
secretName: checkmate-client-tls
server:
protocol: https
ingress:
enabled: true
host: checkmate.example.com
className: nginx
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: true
secretName: checkmate-server-tls
```
### Alternative: Using --set flags
You can also enable TLS during installation using Helm's `--set` flags:
```bash
helm install checkmate ./charts/helm/checkmate \
--set client.protocol=https \
--set server.protocol=https \
--set client.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
--set client.ingress.tls.enabled=true \
--set client.ingress.tls.secretName=checkmate-client-tls \
--set server.ingress.annotations."cert-manager\.io/cluster-issuer"="letsencrypt-prod" \
--set server.ingress.tls.enabled=true \
--set server.ingress.tls.secretName=checkmate-server-tls
```
### Verification
After deployment, cert-manager will automatically create the TLS secrets. You can verify the certificate status:
```bash
# Check certificates
kubectl get certificates
# Check certificate details
kubectl describe certificate checkmate-client-tls
kubectl describe certificate checkmate-server-tls
# Verify the secrets were created
kubectl get secrets | grep checkmate-tls
```
The ingress will automatically use these secrets to enable HTTPS access to your Checkmate instance.
@@ -1,12 +0,0 @@
{{- if eq .Values.client.ingress.host "change_me" }}
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
{{- end }}
{{- if eq .Values.server.ingress.host "change_me" }}
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
{{- end }}
{{- $protocol := .Values.server.protocol }}
{{- if not (or (eq $protocol "http") (eq $protocol "https")) }}
{{- fail "server.protocol must be either 'http' or 'https'" }}
{{- end }}
@@ -11,6 +11,12 @@ metadata:
{{- end }}
spec:
ingressClassName: {{ .Values.client.ingress.className }}
{{- if .Values.client.ingress.tls.enabled }}
tls:
- hosts:
- {{ .Values.client.ingress.host }}
secretName: {{ default (printf "%s-client-tls" .Release.Name) .Values.client.ingress.tls.secretName }}
{{- end }}
rules:
- host: {{ .Values.client.ingress.host }}
http:
@@ -0,0 +1,48 @@
{{- if eq .Values.client.ingress.host "change_me" }}
{{- fail "client.ingress.host must be overridden and not set to 'change_me'" }}
{{- end }}
{{- if eq .Values.server.ingress.host "change_me" }}
{{- fail "server.ingress.host must be overridden and not set to 'change_me'" }}
{{- end }}
{{- $serverProtocol := .Values.server.protocol }}
{{- if not (or (eq $serverProtocol "http") (eq $serverProtocol "https")) }}
{{- fail "server.protocol must be either 'http' or 'https'" }}
{{- end }}
{{- $clientProtocol := .Values.client.protocol }}
{{- if not (or (eq $clientProtocol "http") (eq $clientProtocol "https")) }}
{{- fail "client.protocol must be either 'http' or 'https'" }}
{{- end }}
{{/* Enforce protocol when TLS is enabled to avoid mixed-content */}}
{{- if and .Values.client.ingress.tls.enabled (ne $clientProtocol "https") }}
{{- fail "client.ingress.tls.enabled is true but client.protocol is not 'https'. Set client.protocol: https to avoid mixed content." }}
{{- end }}
{{- if and .Values.server.ingress.tls.enabled (ne $serverProtocol "https") }}
{{- fail "server.ingress.tls.enabled is true but server.protocol is not 'https'. Set server.protocol: https to ensure correct API base URL." }}
{{- end }}
{{/* If client runs on https, API must also be https to avoid mixed content */}}
{{- if and (eq $clientProtocol "https") (ne $serverProtocol "https") }}
{{- fail "client.protocol is 'https' but server.protocol is not. Set server.protocol: https to prevent browser mixed-content issues." }}
{{- end }}
{{/* Fail early if TLS enabled without cert-manager annotations (cluster-issuer or issuer) */}}
{{- $cAnn := .Values.client.ingress.annotations | default dict }}
{{- $sAnn := .Values.server.ingress.annotations | default dict }}
{{- $clientHasIssuer := or (hasKey $cAnn "cert-manager.io/cluster-issuer") (hasKey $cAnn "cert-manager.io/issuer") }}
{{- $serverHasIssuer := or (hasKey $sAnn "cert-manager.io/cluster-issuer") (hasKey $sAnn "cert-manager.io/issuer") }}
{{- if and .Values.client.ingress.tls.enabled (not $clientHasIssuer) }}
{{- fail "client.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
{{- end }}
{{- if and .Values.server.ingress.tls.enabled (not $serverHasIssuer) }}
{{- fail "server.ingress.tls.enabled is true but no cert-manager issuer annotation found. Add 'cert-manager.io/cluster-issuer' or 'cert-manager.io/issuer'." }}
{{- end }}
{{/* Secret name can be omitted; we default to <release>-client|server-tls in templates */}}
@@ -18,6 +18,12 @@ metadata:
#nginx.ingress.kubernetes.io/cors-allow-credentials: "true"*/}}
spec:
ingressClassName: {{ .Values.server.ingress.className }}
{{- if .Values.server.ingress.tls.enabled }}
tls:
- hosts:
- {{ .Values.server.ingress.host }}
secretName: {{ default (printf "%s-server-tls" .Release.Name) .Values.server.ingress.tls.secretName }}
{{- end }}
rules:
- host: {{ .Values.server.ingress.host }}
http:
+18
View File
@@ -7,6 +7,15 @@ client:
host: change_me
className: nginx
annotations: {}
# Example annotations for cert-manager:
# annotations:
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: false
# secretName: {{ .Release.Name }}-client-tls # Optional; defaults to <release>-client-tls if omitted
# Note: when enabling TLS, also set client.protocol: https and add
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
server:
image: ghcr.io/bluewave-labs/checkmate-backend:v3.2.0
@@ -17,6 +26,15 @@ server:
host: change_me
className: nginx
annotations: {}
# Example annotations for cert-manager:
# annotations:
# cert-manager.io/cluster-issuer: "letsencrypt-prod"
tls:
enabled: false
# secretName: {{ .Release.Name }}-server-tls # Optional; defaults to <release>-server-tls if omitted
# Note: when enabling TLS, also set server.protocol: https and add
# a cert-manager issuer annotation (e.g. cert-manager.io/cluster-issuer: "letsencrypt-prod").
# The secret will be automatically created by cert-manager when using the cert-manager.io/cluster-issuer annotation
redis:
enabled: false
+2 -1
View File
@@ -28,4 +28,5 @@ dist-ssr
# Scripts
*.sh
!env.sh
!env.sh
.env.development
+649 -62
View File
@@ -12,7 +12,6 @@
"@emotion/styled": "^11.13.0",
"@hello-pangea/dnd": "^18.0.0",
"@hookform/resolvers": "5.2.2",
"@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@mui/x-charts": "^7.5.1",
@@ -25,6 +24,7 @@
"human-interval": "2.0.1",
"i18next": "25.4.2",
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
"pretty-ms": "9.3.0",
"react": "18.3.1",
@@ -97,6 +97,7 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz",
"integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.27.1",
@@ -408,6 +409,7 @@
"resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.14.0.tgz",
"integrity": "sha512-O000MLDBDdk/EohJPFUqvnp4qnHeYkVP5B0xEG0D/L7cOKP9kefu2DXn8dj74cQfsEzUqh+sr1RzFqiL1o+PpA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -451,6 +453,7 @@
"resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.14.0.tgz",
"integrity": "sha512-XxfOnXFffatap2IyCeJyNov3kiDQWoR08gPUQxvbL7fxKryGBKUZUkG6Hz48DZwVrJSVh9sJboyV1Ds4OW6SgA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.18.3",
"@emotion/babel-plugin": "^11.13.5",
@@ -496,6 +499,262 @@
"integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz",
"integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz",
"integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz",
"integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz",
"integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz",
"integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz",
"integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz",
"integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz",
"integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz",
"integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz",
"integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz",
"integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz",
"integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz",
"integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==",
"cpu": [
"mips64el"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz",
"integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz",
"integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz",
"integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz",
@@ -512,6 +771,134 @@
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz",
"integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz",
"integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz",
"integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz",
"integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz",
"integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz",
"integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz",
"integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.5",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz",
"integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@eslint-community/eslint-utils": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz",
@@ -802,32 +1189,6 @@
"url": "https://opencollective.com/mui-org"
}
},
"node_modules/@mui/icons-material": {
"version": "6.4.11",
"resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-6.4.11.tgz",
"integrity": "sha512-+jjJGIrB1awNbMv4ZVPPdN/p7O1UKFZ+xqRvNIQ8B1KnlID5hPMPBLM6UUbRF4bu3UDCbu79rn9Nye5LGNzmeA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.26.0"
},
"engines": {
"node": ">=14.0.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/mui-org"
},
"peerDependencies": {
"@mui/material": "^6.4.11",
"@types/react": "^17.0.0 || ^18.0.0 || ^19.0.0",
"react": "^17.0.0 || ^18.0.0 || ^19.0.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
}
}
},
"node_modules/@mui/lab": {
"version": "6.0.0-dev.240424162023-9968b4889d",
"resolved": "https://registry.npmjs.org/@mui/lab/-/lab-6.0.0-dev.240424162023-9968b4889d.tgz",
@@ -989,6 +1350,7 @@
"resolved": "https://registry.npmjs.org/@mui/material/-/material-6.4.11.tgz",
"integrity": "sha512-k2D3FLJS+/qD0qnd6ZlAjGFvaaxe1Dl10NyvpeDzIebMuYdn8VqYe6XBgGueEAtnzSJM4V03VD9kb5Fi24dnTA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.26.0",
"@mui/core-downloads-tracker": "^6.4.11",
@@ -1153,7 +1515,6 @@
"resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-7.1.0.tgz",
"integrity": "sha512-4Kck4jxhqF6YxNwJdSae1WgDfXVg0lIH6JVJ7gtuFfuKcQCgomJxPvUEOySTFRPz1IZzwz5OAcToskRdffElDA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/utils": "^7.1.0",
@@ -1181,7 +1542,6 @@
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/types": "^7.4.2",
@@ -1212,7 +1572,6 @@
"resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-7.1.0.tgz",
"integrity": "sha512-m0mJ0c6iRC+f9hMeRe0W7zZX1wme3oUX0+XTVHjPG7DJz6OdQ6K/ggEOq7ZdwilcpdsDUwwMfOmvO71qDkYd2w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.1",
"@emotion/cache": "^11.13.5",
@@ -1288,7 +1647,6 @@
"resolved": "https://registry.npmjs.org/@mui/utils/-/utils-7.1.0.tgz",
"integrity": "sha512-/OM3S8kSHHmWNOP+NH9xEtpYSG10upXeQ0wLZnfDgmgadTAk5F4MQfFLyZ5FCRJENB3eRzltMmaNl6UtDnPovw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.1",
"@mui/types": "^7.4.2",
@@ -1666,9 +2024,9 @@
}
},
"node_modules/@remix-run/router": {
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"version": "1.23.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -1696,6 +2054,123 @@
}
}
},
"node_modules/@rollup/rollup-android-arm-eabi": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.41.0.tgz",
"integrity": "sha512-KxN+zCjOYHGwCl4UCtSfZ6jrq/qi88JDUtiEFk8LELEHq2Egfc/FgW+jItZiOLRuQfb/3xJSgFuNPC9jzggX+A==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-android-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.41.0.tgz",
"integrity": "sha512-yDvqx3lWlcugozax3DItKJI5j05B0d4Kvnjx+5mwiUpWramVvmAByYigMplaoAQ3pvdprGCTCE03eduqE/8mPQ==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"android"
]
},
"node_modules/@rollup/rollup-darwin-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.41.0.tgz",
"integrity": "sha512-2KOU574vD3gzcPSjxO0eyR5iWlnxxtmW1F5CkNOHmMlueKNCQkxR6+ekgWyVnz6zaZihpUNkGxjsYrkTJKhkaw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-darwin-x64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.41.0.tgz",
"integrity": "sha512-gE5ACNSxHcEZyP2BA9TuTakfZvULEW4YAOtxl/A/YDbIir/wPKukde0BNPlnBiP88ecaN4BJI2TtAd+HKuZPQQ==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"darwin"
]
},
"node_modules/@rollup/rollup-freebsd-arm64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.41.0.tgz",
"integrity": "sha512-GSxU6r5HnWij7FoSo7cZg3l5GPg4HFLkzsFFh0N/b16q5buW1NAWuCJ+HMtIdUEi6XF0qH+hN0TEd78laRp7Dg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-freebsd-x64": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.41.0.tgz",
"integrity": "sha512-KGiGKGDg8qLRyOWmk6IeiHJzsN/OYxO6nSbT0Vj4MwjS2XQy/5emsmtoqLAabqrohbgLWJ5GV3s/ljdrIr8Qjg==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"freebsd"
]
},
"node_modules/@rollup/rollup-linux-arm-gnueabihf": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.41.0.tgz",
"integrity": "sha512-46OzWeqEVQyX3N2/QdiU/CMXYDH/lSHpgfBkuhl3igpZiaB3ZIfSjKuOnybFVBQzjsLwkus2mjaESy8H41SzvA==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm-musleabihf": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.41.0.tgz",
"integrity": "sha512-lfgW3KtQP4YauqdPpcUZHPcqQXmTmH4nYU0cplNeW583CMkAGjtImw4PKli09NFi2iQgChk4e9erkwlfYem6Lg==",
"cpu": [
"arm"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.41.0.tgz",
"integrity": "sha512-nn8mEyzMbdEJzT7cwxgObuwviMx6kPRxzYiOl6o/o+ChQq23gfdlZcUNnt89lPhhz3BYsZ72rp0rxNqBSfqlqw==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-arm64-musl": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.41.0.tgz",
@@ -1709,6 +2184,71 @@
"linux"
]
},
"node_modules/@rollup/rollup-linux-loongarch64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.41.0.tgz",
"integrity": "sha512-WbnJaxPv1gPIm6S8O/Wg+wfE/OzGSXlBMbOe4ie+zMyykMOeqmgD1BhPxZQuDqwUN+0T/xOFtL2RUWBspnZj3w==",
"cpu": [
"loong64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.41.0.tgz",
"integrity": "sha512-eRDWR5t67/b2g8Q/S8XPi0YdbKcCs4WQ8vklNnUYLaSWF+Cbv2axZsp4jni6/j7eKvMLYCYdcsv8dcU+a6QNFg==",
"cpu": [
"ppc64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.41.0.tgz",
"integrity": "sha512-TWrZb6GF5jsEKG7T1IHwlLMDRy2f3DPqYldmIhnA2DVqvvhY2Ai184vZGgahRrg8k9UBWoSlHv+suRfTN7Ua4A==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-riscv64-musl": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.41.0.tgz",
"integrity": "sha512-ieQljaZKuJpmWvd8gW87ZmSFwid6AxMDk5bhONJ57U8zT77zpZ/TPKkU9HpnnFrM4zsgr4kiGuzbIbZTGi7u9A==",
"cpu": [
"riscv64"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-s390x-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.41.0.tgz",
"integrity": "sha512-/L3pW48SxrWAlVsKCN0dGLB2bi8Nv8pr5S5ocSM+S0XCn5RCVCXqi8GVtHFsOBBCSeR+u9brV2zno5+mg3S4Aw==",
"cpu": [
"s390x"
],
"license": "MIT",
"optional": true,
"os": [
"linux"
]
},
"node_modules/@rollup/rollup-linux-x64-gnu": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.41.0.tgz",
@@ -1735,6 +2275,45 @@
"linux"
]
},
"node_modules/@rollup/rollup-win32-arm64-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.41.0.tgz",
"integrity": "sha512-4yodtcOrFHpbomJGVEqZ8fzD4kfBeCbpsUy5Pqk4RluXOdsWdjLnjhiKy2w3qzcASWd04fp52Xz7JKarVJ5BTg==",
"cpu": [
"arm64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-ia32-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.41.0.tgz",
"integrity": "sha512-tmazCrAsKzdkXssEc65zIE1oC6xPHwfy9d5Ta25SRCDOZS+I6RypVVShWALNuU9bxIfGA0aqrmzlzoM5wO5SPQ==",
"cpu": [
"ia32"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@rollup/rollup-win32-x64-msvc": {
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.41.0.tgz",
"integrity": "sha512-h1J+Yzjo/X+0EAvR2kIXJDuTuyT7drc+t2ALY0nIcGPbTatNOf0VWdhEA2Z4AAjv6X1NJV7SYo5oCTYRJhSlVA==",
"cpu": [
"x64"
],
"license": "MIT",
"optional": true,
"os": [
"win32"
]
},
"node_modules/@sideway/address": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz",
@@ -2133,6 +2712,7 @@
"integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.12.0"
}
@@ -2154,6 +2734,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.22.tgz",
"integrity": "sha512-vUhG0YmQZ7kL/tmKLrD3g5zXbXXreZXB3pmROW8bg3CnLnpjkRVwUlLne7Ufa2r9yJ8+/6B73RzhAek5TBKh2Q==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -2217,6 +2798,7 @@
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"dev": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -2523,6 +3105,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001716",
"electron-to-chromium": "^1.5.149",
@@ -2935,7 +3518,8 @@
"version": "1.11.13",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/debug": {
"version": "4.4.1",
@@ -3342,6 +3926,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.6.1",
@@ -4149,6 +4734,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.27.6"
},
@@ -4685,9 +5271,9 @@
"license": "MIT"
},
"node_modules/js-yaml": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@@ -4852,6 +5438,15 @@
"yallist": "^3.0.2"
}
},
"node_modules/lucide-react": {
"version": "0.562.0",
"resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-0.562.0.tgz",
"integrity": "sha512-82hOAu7y0dbVuFfmO4bYF1XEwYk/mEbM5E+b1jgci/udUBEE/R7LF5Ip0CCEmXe8AybRM8L+04eP+LGZeDvkiw==",
"license": "ISC",
"peerDependencies": {
"react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/math-intrinsics": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz",
@@ -5416,6 +6011,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -5429,6 +6025,7 @@
"resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.63.0.tgz",
"integrity": "sha512-ZwueDMvUeucovM2VjkCf7zIHcs1aAlDimZu2Hvel5C5907gUzMpm4xCrQXtRzCvsBqFjonB4m3x4LzCFI1ZKWA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=18.0.0"
},
@@ -5515,12 +6112,12 @@
}
},
"node_modules/react-router": {
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.0"
"@remix-run/router": "1.23.2"
},
"engines": {
"node": ">=14.0.0"
@@ -5530,13 +6127,13 @@
}
},
"node_modules/react-router-dom": {
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.0",
"react-router": "6.30.1"
"@remix-run/router": "1.23.2",
"react-router": "6.30.3"
},
"engines": {
"node": ">=14.0.0"
@@ -5632,7 +6229,8 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/redux/-/redux-5.0.1.tgz",
"integrity": "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==",
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/redux-persist": {
"version": "6.0.0",
@@ -5770,6 +6368,7 @@
"resolved": "https://registry.npmjs.org/rollup/-/rollup-4.41.0.tgz",
"integrity": "sha512-HqMFpUbWlf/tvcxBFNKnJyzc7Lk+XO3FGc3pbNBLqEbOz0gPLRgcrlS3UF4MfUrVlstOaP/q0kM6GVvi+LrLRg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/estree": "1.0.7"
},
@@ -6405,6 +7004,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -6524,6 +7124,7 @@
"resolved": "https://registry.npmjs.org/vite/-/vite-6.3.6.tgz",
"integrity": "sha512-0msEVHJEScQbhkbVTb/4iHZdJ6SXp/AvxL2sjwYQFfBqleHtnCqv1J3sa9zbWz/6kW1m9Tfzn92vW+kZ1WV6QA==",
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.25.0",
"fdir": "^6.4.4",
@@ -6744,20 +7345,6 @@
"integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==",
"license": "ISC"
},
"node_modules/yaml": {
"version": "2.8.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz",
"integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==",
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14.6"
}
},
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
+1 -1
View File
@@ -17,7 +17,6 @@
"@emotion/styled": "^11.13.0",
"@hello-pangea/dnd": "^18.0.0",
"@hookform/resolvers": "5.2.2",
"@mui/icons-material": "6.4.11",
"@mui/lab": "6.0.0-dev.240424162023-9968b4889d",
"@mui/material": "6.4.11",
"@mui/x-charts": "^7.5.1",
@@ -30,6 +29,7 @@
"human-interval": "2.0.1",
"i18next": "25.4.2",
"joi": "17.13.3",
"lucide-react": "^0.562.0",
"mui-color-input": "^6.0.0",
"pretty-ms": "9.3.0",
"react": "18.3.1",
+6 -3
View File
@@ -10,6 +10,7 @@ import { logger } from "./Utils/Logger"; // Import the logger
import { networkService } from "./main";
import { Routes } from "./Routes";
import AppLayout from "@/Components/v1/Layouts/AppLayout";
import { UserGuideSidebarProvider } from "@/Components/v1/UserGuide";
function App() {
const mode = useSelector((state) => state.ui.mode);
@@ -25,9 +26,11 @@ function App() {
return (
<ThemeProvider theme={mode === "light" ? lightTheme : darkTheme}>
<CssBaseline />
<AppLayout>
<Routes />
</AppLayout>
<UserGuideSidebarProvider>
<AppLayout>
<Routes />
</AppLayout>
</UserGuideSidebarProvider>
<ToastContainer />
</ThemeProvider>
);
+1 -1
View File
@@ -16,7 +16,7 @@ const langMap = {
const LanguageSelector = () => {
const { i18n } = useTranslation();
const theme = useTheme();
const { language } = useSelector((state) => state.ui);
const { language = "en" } = useSelector((state) => state.ui);
const dispatch = useDispatch();
const handleChange = (event) => {
const newLang = event.target.value;
+12 -7
View File
@@ -2,7 +2,7 @@
import IconButton from "@mui/material/IconButton";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Settings from "../../../assets/icons/settings-bold.svg?react";
import Icon from "../Icon";
import Dialog from "../Dialog/index.jsx";
// Utils
@@ -12,7 +12,7 @@ import { useNavigate } from "react-router-dom";
import { createToast } from "../../../Utils/toastUtils.jsx";
import PropTypes from "prop-types";
import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/v1/monitorHooks.js";
import { usePauseMonitor, useDeleteMonitor } from "../../../Hooks/monitorHooks.js";
const ActionsMenu = ({
monitor,
@@ -31,7 +31,8 @@ const ActionsMenu = ({
const handleRemove = async (event) => {
event.preventDefault();
event.stopPropagation();
let monitor = { _id: actions.id };
let monitor = { id: actions.id };
console.log(monitor);
await deleteMonitor({ monitor });
updateRowCallback();
};
@@ -39,7 +40,7 @@ const ActionsMenu = ({
const handlePause = async () => {
try {
setIsLoading(true);
await pauseMonitor({ monitorId: monitor._id });
await pauseMonitor({ monitorId: monitor.id });
pauseCallback();
} catch (error) {
createToast({ body: "Failed to pause monitor." });
@@ -72,7 +73,7 @@ const ActionsMenu = ({
aria-label="monitor actions"
onClick={(event) => {
event.stopPropagation();
openMenu(event, monitor._id, monitor.type === "ping" ? null : monitor.url);
openMenu(event, monitor.id, monitor.type === "ping" ? null : monitor.url);
}}
sx={{
"&:focus": {
@@ -83,7 +84,10 @@ const ActionsMenu = ({
},
}}
>
<Settings />
<Icon
name="Settings"
size={20}
/>
</IconButton>
<Menu
@@ -197,6 +201,7 @@ const ActionsMenu = ({
confirmationButtonLabel="Delete"
/* Do we need stop propagation? */
onConfirm={(e) => {
console.log(e);
e.stopPropagation();
handleRemove(e);
}}
@@ -210,7 +215,7 @@ const ActionsMenu = ({
ActionsMenu.propTypes = {
monitor: PropTypes.shape({
_id: PropTypes.string,
id: PropTypes.string,
url: PropTypes.string,
type: PropTypes.string,
isActive: PropTypes.bool,
+22 -11
View File
@@ -1,10 +1,7 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Box, Button, IconButton, Stack, Typography } from "@mui/material";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
import CloseIcon from "@mui/icons-material/Close";
import Icon from "../Icon";
import "./index.css";
/**
@@ -13,9 +10,24 @@ import "./index.css";
*/
const icons = {
info: <InfoOutlinedIcon />,
error: <ErrorOutlineOutlinedIcon />,
warning: <WarningAmberOutlinedIcon />,
info: (
<Icon
name="Info"
size={24}
/>
),
error: (
<Icon
name="AlertCircle"
size={24}
/>
),
warning: (
<Icon
name="AlertTriangle"
size={24}
/>
),
};
/**
@@ -97,10 +109,9 @@ const Alert = ({ variant, title, body, isToast, hasIcon = true, onClick }) => {
},
}}
>
<CloseIcon
sx={{
fontSize: "20px",
}}
<Icon
name="X"
size={20}
/>
</IconButton>
)}
+10 -9
View File
@@ -1,27 +1,28 @@
import LeftArrow from "../../../assets/icons/left-arrow.svg?react";
import LeftArrowDouble from "../../../assets/icons/left-arrow-double.svg?react";
import LeftArrowLong from "../../../assets/icons/left-arrow-long.svg?react";
import Icon from "../Icon";
import PropTypes from "prop-types";
const ArrowLeft = ({ type, color = "#667085", ...props }) => {
if (type === "double") {
return (
<LeftArrowDouble
style={{ color }}
<Icon
name="ChevronsLeft"
color={color}
{...props}
/>
);
} else if (type === "long") {
return (
<LeftArrowLong
style={{ color }}
<Icon
name="ArrowLeft"
color={color}
{...props}
/>
);
} else {
return (
<LeftArrow
style={{ color }}
<Icon
name="ChevronLeft"
color={color}
{...props}
/>
);
@@ -1,19 +1,20 @@
import RightArrow from "../../../assets/icons/right-arrow.svg?react";
import RightArrowDouble from "../../../assets/icons/right-arrow-double.svg?react";
import Icon from "../Icon";
import PropTypes from "prop-types";
const ArrowRight = ({ type, color = "#667085", ...props }) => {
if (type === "double") {
return (
<RightArrowDouble
style={{ color }}
<Icon
name="ChevronsRight"
color={color}
{...props}
/>
);
} else {
return (
<RightArrow
style={{ color }}
<Icon
name="ChevronRight"
color={color}
{...props}
/>
);
@@ -103,7 +103,7 @@ const BarChart = ({ checks = [] }) => {
</>
}
placement="top"
key={`check-${check?._id}`}
key={`check-${check?.id}`}
slotProps={{
popper: {
className: "bar-tooltip",
@@ -145,7 +145,7 @@ const BarChart = ({ checks = [] }) => {
position="relative"
width="9px"
height="100%"
backgroundColor={theme.palette.primary.lowContrast} // CAIO_REVIEW
backgroundColor={theme.palette.primary.lowContrast}
sx={{
borderRadius: theme.spacing(1.5),
/*
+10 -11
View File
@@ -1,7 +1,6 @@
import "./check.css";
import PropTypes from "prop-types";
import CheckGrey from "../../../assets/icons/check.svg?react";
import CheckOutlined from "../../../assets/icons/check-outlined.svg?react";
import Icon from "../Icon";
import { Box, Stack, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
@@ -35,16 +34,16 @@ const Check = ({ text, noHighlightText, variant = "info", outlined = false }) =>
alignItems="center"
>
{outlined ? (
<CheckOutlined alt="check" />
<Icon
name="CheckCircle"
size={20}
/>
) : (
<Box
lineHeight={0}
sx={{
"& svg > path": { fill: colors[variant] },
}}
>
<CheckGrey alt="form checks" />
</Box>
<Icon
name="Check"
size={16}
color={colors[variant]}
/>
)}
<Typography
component="span"
+11 -4
View File
@@ -8,8 +8,7 @@ import IconButton from "@mui/material/IconButton";
import Container from "@mui/material/Container";
import MenuItem from "@mui/material/MenuItem";
import Drawer from "@mui/material/Drawer";
import MenuIcon from "@mui/icons-material/Menu";
import CloseRoundedIcon from "@mui/icons-material/CloseRounded";
import Icon from "../Icon";
import ThemeSwitch from "../ThemeSwitch/index.jsx";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
@@ -153,7 +152,11 @@ const AppAppBar = () => {
aria-label="Menu button"
onClick={toggleDrawer(true)}
>
<MenuIcon sx={{ color: theme.palette.text.primary }} />
<Icon
name="Menu"
size={24}
color={theme.palette.text.primary}
/>
</IconButton>
<Drawer
anchor="top"
@@ -176,7 +179,11 @@ const AppAppBar = () => {
}}
>
<IconButton onClick={toggleDrawer(false)}>
<CloseRoundedIcon sx={{ color: theme.palette.text.primary }} />
<Icon
name="X"
size={24}
color={theme.palette.text.primary}
/>
</IconButton>
</Box>
@@ -1,7 +1,7 @@
import { Checkbox, FormControl, ListItemText, MenuItem, Select } from "@mui/material";
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import AddCircleOutlineIcon from "@mui/icons-material/AddCircleOutline";
import Icon from "../Icon";
/**
* A reusable filter header component that displays a dropdown menu with selectable options.
@@ -29,9 +29,10 @@ const FilterHeader = ({ header, options, value, onChange, multiple = true }) =>
<Select
multiple={multiple}
IconComponent={(props) => (
<AddCircleOutlineIcon
<Icon
{...props}
sx={{ fontSize: "medium" }}
name="PlusCircle"
size={18}
/>
)}
displayEmpty
@@ -40,13 +40,13 @@ const GenericFallback = ({ children }) => {
<Box
component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
Background="transparent"
alt="Loading animation"
sx={{
zIndex: 1,
border: "none",
borderRadius: theme.spacing(8),
width: "100%",
backgroundColor: "transparent",
}}
/>
<Box
+20 -3
View File
@@ -2,6 +2,7 @@ import { Stack, Typography } from "@mui/material";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import Dot from "../Dot/index.jsx";
import { StatusLabel } from "@/Components/v1/Label/index.jsx";
/**
* Host component.
* This subcomponent receives a params object and displays the host details.
@@ -14,7 +15,7 @@ import Dot from "../Dot/index.jsx";
* @param {number} params.percentage - The percentage to display.
* @returns {React.ElementType} Returns a div element with the host details.
*/
const Host = ({ url, title, percentageColor, percentage, showURL }) => {
const Host = ({ url, title, percentageColor, percentage, showURL, status }) => {
const theme = useTheme();
return (
<Stack>
@@ -24,19 +25,34 @@ const Host = ({ url, title, percentageColor, percentage, showURL }) => {
alignItems="center"
gap={theme.spacing(4)}
>
{title}
<Typography
component="span"
sx={{
fontWeight: 500,
}}
>
{title}
</Typography>
{percentageColor && percentage && (
<>
<Dot />
<Typography
component="span"
sx={{
color: percentageColor,
fontWeight: 500,
color: percentageColor,
}}
>
{percentage}%
</Typography>
{status && (
<StatusLabel
status={status}
text={status}
customStyles={{ textTransform: "capitalize" }}
/>
)}
</>
)}
</Stack>
@@ -51,6 +67,7 @@ Host.propTypes = {
percentage: PropTypes.string,
url: PropTypes.string,
showURL: PropTypes.bool,
status: PropTypes.string,
};
export default Host;
@@ -2,7 +2,7 @@ import i18n from "../../../Utils/i18n.js";
import { useSelector } from "react-redux";
import { useEffect } from "react";
const I18nLoader = () => {
const language = useSelector((state) => state.ui.language);
const language = useSelector((state) => state.ui.language ?? "en");
useEffect(() => {
if (language && i18n.language !== language) {
+120
View File
@@ -0,0 +1,120 @@
/**
* Maps legacy icon names to Lucide icon names
* Use: import { iconMap } from './iconMap'; LucideIcons[iconMap['monitors']]
*/
export const iconMap = {
// Navigation/Sidebar icons
monitors: "Globe",
incidents: "AlertTriangle",
integrations: "Link",
"page-speed": "Gauge",
settings: "Settings",
notifications: "Bell",
maintenance: "Wrench",
"status-pages": "Wifi",
docs: "FileText",
discussions: "MessageCircle",
support: "HelpCircle",
changeLog: "Code",
logs: "Database",
// User icons
user: "User",
"user-two": "Users",
"user-edit": "UserPen",
groups: "Users",
lock: "Lock",
key: "Key",
logout: "LogOut",
// Action icons
edit: "Pencil",
"trash-bin": "Trash2",
search: "Search",
"close-icon": "X",
check: "Check",
"check-icon": "CheckCircle",
"check-outlined": "CheckCircle",
// Arrow icons
"left-arrow": "ChevronLeft",
"right-arrow": "ChevronRight",
"up-arrow": "ChevronUp",
"down-arrow": "ChevronDown",
"left-arrow-double": "ChevronsLeft",
"right-arrow-double": "ChevronsRight",
"left-arrow-long": "ArrowLeft",
"top-right-arrow": "ArrowUpRight",
"open-in-new-page": "ExternalLink",
"selector-vertical": "ChevronsUpDown",
// Status/Alert icons
"alert-icon": "Bell",
"warning-icon": "AlertCircle",
"history-icon": "History",
// Data/Chart icons
dashboard: "LayoutGrid",
"monitor-graph-line": "TrendingUp",
"response-time-icon": "TrendingUp",
"uptime-icon": "Activity",
"speedometer-icon": "Gauge",
"spedometer-icon": "Gauge",
"ruler-icon": "Ruler",
"performance-report": "Layers",
"average-response-icon": "BarChart3",
"distributed-uptime": "Network",
// Misc icons
calendar: "Calendar",
"calendar-check": "CalendarCheck",
"clock-snooze": "Clock",
mail: "Mail",
email: "Mail",
folder: "Folder",
"dots-vertical": "MoreVertical",
"pause-icon": "PauseCircle",
"resume-icon": "PlayCircle",
"checkbox-filled": "CheckSquare",
"checkbox-outline": "Square",
"radio-checked": "Circle",
"cpu-chip": "Cpu",
certificate: "Award",
"interval-check": "RefreshCw",
};
/**
* MUI icon to Lucide mapping
*/
export const muiToLucideMap = {
AddCircleOutline: "PlusCircle",
ArrowDownwardRounded: "ArrowDown",
ArrowUpwardRounded: "ArrowUp",
ArrowDropDown: "ChevronDown",
ArrowOutward: "ExternalLink",
CheckCircle: "CheckCircle2",
Clear: "X",
Close: "X",
CloseRounded: "X",
CloudUpload: "Upload",
Delete: "Trash2",
DeleteOutlineRounded: "Trash2",
Email: "Mail",
ErrorOutline: "AlertTriangle",
ErrorOutlineOutlined: "AlertTriangle",
Image: "ImageIcon",
InfoOutlined: "Info",
KeyboardArrowDown: "ChevronDown",
Menu: "Menu",
PauseCircleOutline: "PauseCircle",
PauseOutlined: "Pause",
PlayArrowOutlined: "Play",
PlayCircleOutlineRounded: "PlayCircle",
RadioButtonChecked: "Circle",
ReorderRounded: "GripVertical",
SettingsOutlined: "Settings",
Share: "Share2",
Visibility: "Eye",
VisibilityOff: "EyeOff",
WarningAmberOutlined: "AlertTriangle",
};
+196
View File
@@ -0,0 +1,196 @@
import { useTheme } from "@mui/material";
import PropTypes from "prop-types";
import {
Activity,
AlertCircle,
AlertTriangle,
ArrowDown,
ArrowLeft,
ArrowUp,
ArrowUpRight,
Bell,
Calendar,
Check,
CheckCircle,
CheckSquare,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
ChevronsUpDown,
Circle,
CircleDot,
Clock,
Code,
Cpu,
Database,
ExternalLink,
Eye,
EyeOff,
FileText,
Gauge,
Globe,
GripVertical,
HelpCircle,
History,
Image as ImageIcon,
Info,
Key,
Layers,
Link,
Lock,
LogOut,
Mail,
Menu,
MessageCircle,
MoreVertical,
Pause,
PauseCircle,
Play,
PlayCircle,
PlusCircle,
RefreshCw,
Ruler,
Search,
Settings,
Square,
Trash2,
TrendingUp,
Upload,
User,
Users,
Wifi,
Wrench,
X,
} from "lucide-react";
/**
* Map of icon names to Lucide icon components.
* Only icons explicitly imported here will be included in the bundle (tree-shaking).
* To add a new icon: import it above and add it to this object.
*/
const iconComponents = {
Activity,
AlertCircle,
AlertTriangle,
ArrowDown,
ArrowLeft,
ArrowUp,
ArrowUpRight,
Bell,
Calendar,
Check,
CheckCircle,
CheckSquare,
ChevronDown,
ChevronLeft,
ChevronRight,
ChevronsLeft,
ChevronsRight,
ChevronsUpDown,
Circle,
CircleDot,
Clock,
Code,
Cpu,
Database,
ExternalLink,
Eye,
EyeOff,
FileText,
Gauge,
Globe,
GripVertical,
HelpCircle,
History,
Image: ImageIcon,
Info,
Key,
Layers,
Link,
Lock,
LogOut,
Mail,
Menu,
MessageCircle,
MoreVertical,
Pause,
PauseCircle,
Play,
PlayCircle,
PlusCircle,
RefreshCw,
Ruler,
Search,
Settings,
Square,
Trash2,
TrendingUp,
Upload,
User,
Users,
Wifi,
Wrench,
X,
};
/**
* Theme-aware icon component wrapping Lucide icons
*
* @param {string} name - Lucide icon name (e.g., "Bell", "Check", "ArrowLeft")
* @param {number} size - Icon size in pixels (default: 20)
* @param {string} color - Direct color value OR theme path like "primary.contrastText"
* @param {number} strokeWidth - Stroke width (default: 1.5)
* @param {string} fill - Fill color (default: "none")
*/
const Icon = ({ name, size = 20, color, strokeWidth = 1.5, fill = "none", ...props }) => {
const theme = useTheme();
// Resolve color from theme path or use direct value
const resolveColor = (colorValue) => {
if (!colorValue) {
return theme.palette.primary.contrastTextTertiary; // Default icon color
}
// If it's a theme path like "primary.contrastText"
if (typeof colorValue === "string" && colorValue.includes(".")) {
const parts = colorValue.split(".");
let resolved = theme.palette;
for (const part of parts) {
resolved = resolved?.[part];
}
return resolved || colorValue;
}
// Direct color value
return colorValue;
};
const LucideIcon = iconComponents[name];
if (!LucideIcon) {
console.warn(`Icon "${name}" not found in Icon component`);
return null;
}
return (
<LucideIcon
size={size}
color={resolveColor(color)}
strokeWidth={strokeWidth}
fill={fill}
{...props}
/>
);
};
Icon.propTypes = {
name: PropTypes.string.isRequired,
size: PropTypes.number,
color: PropTypes.string,
strokeWidth: PropTypes.number,
fill: PropTypes.string,
};
export default Icon;
@@ -1,8 +1,7 @@
import PropTypes from "prop-types";
import { FormControlLabel, Checkbox as MuiCheckbox } from "@mui/material";
import { useTheme } from "@emotion/react";
import CheckboxOutline from "../../../../assets/icons/checkbox-outline.svg?react";
import CheckboxFilled from "../../../../assets/icons/checkbox-filled.svg?react";
import Icon from "../../Icon";
/**
* Checkbox Component
@@ -65,8 +64,8 @@ const Checkbox = ({
name={name}
value={value}
onChange={onChange}
icon={<CheckboxOutline />}
checkedIcon={<CheckboxFilled />}
icon={<Icon name="Square" />}
checkedIcon={<Icon name="CheckSquare" />}
inputProps={{
"aria-label": "controlled checkbox",
id: id,
@@ -1,11 +1,10 @@
// Components
import { Box, Stack, Typography } from "@mui/material";
import CloudUploadIcon from "@mui/icons-material/CloudUpload";
import Icon from "../../Icon";
import Image from "../../Image/index.jsx";
import TextField from "@mui/material/TextField";
import IconButton from "@mui/material/IconButton";
import ProgressUpload from "../../ProgressBars/index.jsx";
import ImageIcon from "@mui/icons-material/Image";
// Utils
import PropTypes from "prop-types";
@@ -176,7 +175,10 @@ const ImageUpload = ({
boxShadow: theme.shape.boxShadow,
}}
>
<CloudUploadIcon />
<Icon
name="Upload"
size={24}
/>
</IconButton>
<Typography
component="h2"
@@ -203,7 +205,12 @@ const ImageUpload = ({
</Box>
{(localError || progress.isLoading || progress.value !== 0) && (
<ProgressUpload
icon={<ImageIcon />}
icon={
<Icon
name="Image"
size={20}
/>
}
label={file?.name || "Upload failed"}
size={file?.size}
progress={progress.value}
@@ -1,7 +1,7 @@
import PropTypes from "prop-types";
import { FormControlLabel, Radio as MUIRadio, Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import RadioChecked from "../../../../assets/icons/radio-checked.svg?react";
import Icon from "../../Icon";
import "./index.css";
/**
@@ -47,7 +47,12 @@ const Radio = ({
<MUIRadio
id={id}
size={size}
checkedIcon={<RadioChecked />}
checkedIcon={
<Icon
name="CircleDot"
size={16}
/>
}
sx={{
color: "transparent",
width: 16,
@@ -8,7 +8,7 @@ import {
Checkbox,
} from "@mui/material";
import { useTheme } from "@emotion/react";
import SearchIcon from "../../../../assets/icons/search.svg?react";
import Icon from "../../Icon";
import React, { useEffect } from "react";
import { useTranslation } from "react-i18next";
import FieldWrapper from "../FieldWrapper/index.jsx";
@@ -31,20 +31,13 @@ import FieldWrapper from "../FieldWrapper/index.jsx";
const SearchAdornment = () => {
const theme = useTheme();
return (
<Box
height={16}
sx={{
"& svg": {
width: 16,
height: 16,
"& path": {
stroke: theme.palette.primary.contrastTextTertiary,
strokeWidth: 1.2,
},
},
}}
>
<SearchIcon />
<Box height={16}>
<Icon
name="Search"
size={16}
color="primary.contrastTextTertiary"
strokeWidth={1.2}
/>
</Box>
);
};
@@ -85,7 +78,12 @@ const Search = ({
const enhancedOptions = React.useMemo(() => {
return multiple && isAdorned
? [
{ [filteredBy]: t("selectAll"), isSelectAll: true, _id: "select_all" },
{
[filteredBy]: t("selectAll"),
isSelectAll: true,
_id: "select_all",
id: "select_all",
},
...options,
]
: options;
@@ -93,7 +91,7 @@ const Search = ({
const isOptionSelected = (option) => {
if (!multiple && !isAdorned) return false;
if (Array.isArray(value)) {
return value.some((item) => item._id === option._id);
return value.some((item) => (item._id ?? item.id) === (option._id ?? option.id));
}
return false;
};
@@ -145,7 +143,9 @@ const Search = ({
disableClearable
options={enhancedOptions}
getOptionLabel={(option) => option[filteredBy]}
isOptionEqualToValue={(option, value) => option._id === value._id} // Compare by unique identifier
isOptionEqualToValue={(option, value) =>
(option._id ?? option.id) === (value?._id ?? value?.id)
} // Compare by unique identifier
renderInput={(params) => (
<FieldWrapper
label={label}
@@ -206,7 +206,7 @@ const Search = ({
return filtered;
}}
getOptionKey={(option) => {
return option._id;
return option._id ?? option.id;
}}
renderOption={(props, option) => {
const { key, ...optionProps } = props;
@@ -1,7 +1,7 @@
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { MenuItem, Select as MuiSelect, Stack, Typography } from "@mui/material";
import KeyboardArrowDownIcon from "@mui/icons-material/KeyboardArrowDown";
import Icon from "../../Icon";
import FieldWrapper from "../FieldWrapper/index.jsx";
import "./index.css";
@@ -62,6 +62,7 @@ const Select = ({
fieldWrapperSx = {},
}) => {
const theme = useTheme();
const getItemValue = (item) => item?._id ?? item?.id;
const itemStyles = {
fontSize: "var(--env-var-font-size-medium)",
color: theme.palette.primary.contrastTextTertiary,
@@ -97,7 +98,13 @@ const Select = ({
error={error}
name={name}
inputProps={{ id: id }}
IconComponent={KeyboardArrowDownIcon}
IconComponent={(props) => (
<Icon
name="ChevronDown"
size={20}
{...props}
/>
)}
MenuProps={{ disableScrollLock: true }}
sx={{
fontSize: 13,
@@ -123,7 +130,7 @@ const Select = ({
...sx,
}}
renderValue={(selected) => {
const selectedItem = items.find((item) => item._id === selected);
const selectedItem = items.find((item) => getItemValue(item) === selected);
const displayName = selectedItem ? selectedItem.name : placeholder;
return (
<Typography
@@ -152,17 +159,25 @@ const Select = ({
{placeholder}
</MenuItem>
)}
{items.map((item) => (
<MenuItem
value={item._id}
key={`${id}-${item._id}`}
sx={{
...itemStyles,
}}
>
{item.name}
</MenuItem>
))}
{items
.map((item) => {
const itemValue = getItemValue(item);
if (itemValue === undefined || itemValue === null) {
return null;
}
return (
<MenuItem
value={itemValue}
key={`${id}-${itemValue}`}
sx={{
...itemStyles,
}}
>
{item.name}
</MenuItem>
);
})
.filter(Boolean)}
</MuiSelect>
</FieldWrapper>
);
@@ -179,8 +194,8 @@ Select.propTypes = {
.isRequired,
items: PropTypes.arrayOf(
PropTypes.shape({
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])
.isRequired,
_id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
id: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]),
name: PropTypes.string.isRequired,
})
@@ -1,10 +1,7 @@
import { Stack, Typography, InputAdornment, IconButton } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import VisibilityOff from "@mui/icons-material/VisibilityOff";
import Visibility from "@mui/icons-material/Visibility";
import ReorderRoundedIcon from "@mui/icons-material/ReorderRounded";
import DeleteIcon from "../../../../../assets/icons/trash-bin.svg?react";
import Icon from "../../../Icon";
export const HttpAdornment = ({ https }) => {
const theme = useTheme();
@@ -56,7 +53,17 @@ export const PasswordEndAdornment = ({ fieldType, setFieldType }) => {
},
}}
>
{fieldType === "password" ? <VisibilityOff /> : <Visibility />}
{fieldType === "password" ? (
<Icon
name="EyeOff"
size={20}
/>
) : (
<Icon
name="Eye"
size={20}
/>
)}
</IconButton>
</InputAdornment>
);
+2 -4
View File
@@ -147,11 +147,9 @@ const StatusLabel = ({ status, text, customStyles }) => {
}}
>
<Box
width={7}
height={7}
bgcolor={theme.palette[themeColor].lowContrast}
borderRadius="50%"
marginRight="5px"
borderRadius="0%"
marginRight="1px"
/>
</BaseLabel>
);
@@ -1,33 +0,0 @@
.home-layout {
position: relative;
min-height: 100vh;
margin: 0 auto;
padding: 0;
}
/* TODO go for this approach for responsiveness. The aside needs to be taken care of */
/* @media (max-width: 1000px) {
.home-layout {
flex-direction: column !important;
}
} */
/* .home-layout aside {
position: sticky;
top: 0;
left: 0;
height: 100vh;
max-width: var(--env-var-side-bar-width);
} */
.home-layout > div {
min-height: calc(100vh - var(--env-var-spacing-2) * 2);
flex: 1;
}
.home-content-wrapper {
padding: var(--env-var-spacing-2);
max-width: 1400px;
margin: 0 auto;
flex: 1;
}
@@ -1,20 +1,44 @@
import Sidebar from "../../Sidebar/index.jsx";
import { Outlet } from "react-router";
import { Stack } from "@mui/material";
import "./index.css";
import { useTheme } from "@mui/material/styles";
import { UserGuideSidebar, TAB_BAR_WIDTH } from "../../UserGuide";
const HomeLayout = () => {
const theme = useTheme();
return (
<Stack
className="home-layout"
flexDirection="row"
gap={14}
sx={{
position: "relative",
minHeight: "100vh",
margin: "0 auto",
padding: 0,
overflowX: "hidden",
width: "100%",
"& > div": {
minHeight: `calc(100vh - ${theme.spacing(4)} * 2)`,
flex: 1,
},
}}
>
<Sidebar />
<Stack className="home-content-wrapper">
<Stack
sx={{
padding: theme.spacing(4),
paddingRight: `${TAB_BAR_WIDTH + 16}px`,
maxWidth: 1400,
margin: "0 auto",
flex: 1,
minWidth: 0,
overflowX: "hidden",
}}
>
<Outlet />
</Stack>
<UserGuideSidebar />
</Stack>
);
};
@@ -1,7 +1,7 @@
import * as React from "react";
import Button from "@mui/material/Button";
import ButtonGroup from "@mui/material/ButtonGroup";
import ArrowDropDownIcon from "@mui/icons-material/ArrowDropDown";
import Icon from "../Icon";
import ClickAwayListener from "@mui/material/ClickAwayListener";
import Grow from "@mui/material/Grow";
import Paper from "@mui/material/Paper";
@@ -11,7 +11,7 @@ import MenuList from "@mui/material/MenuList";
import { useNavigate } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { createToast } from "../../../Utils/toastUtils.jsx";
import { useExportMonitors } from "../../../Hooks/v1/monitorHooks.js";
import { useExportMonitors } from "../../../Hooks/monitorHooks.js";
const MonitorActions = ({ isLoading }) => {
const [open, setOpen] = React.useState(false);
@@ -70,7 +70,10 @@ const MonitorActions = ({ isLoading }) => {
aria-haspopup="menu"
onClick={handleToggle}
>
<ArrowDropDownIcon />
<Icon
name="ChevronDown"
size={20}
/>
</Button>
</ButtonGroup>
<Popper
@@ -3,19 +3,16 @@ import Status from "./status.jsx";
import Skeleton from "./skeleton.jsx";
import Button from "@mui/material/Button";
import { Tooltip } from "@mui/material";
import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined";
import PauseOutlinedIcon from "@mui/icons-material/PauseOutlined";
import PlayArrowOutlinedIcon from "@mui/icons-material/PlayArrowOutlined";
import EmailIcon from "@mui/icons-material/Email";
import Icon from "../Icon";
// Utils
import PropTypes from "prop-types";
import { useNavigate } from "react-router-dom";
import { useTheme } from "@mui/material/styles";
import { usePauseMonitor } from "../../../Hooks/v1/monitorHooks.js";
import { useSendTestEmail } from "../../../Hooks/v1/useSendTestEmail.js";
import { usePauseMonitor } from "../../../Hooks/monitorHooks.js";
import { useSendTestEmail } from "../../../Hooks/useSendTestEmail.js";
import { useTranslation } from "react-i18next";
import { useTestAllNotifications } from "../../../Hooks/v1/useNotifications.js";
import { useTestAllNotifications } from "../../../Hooks/useNotifications.js";
/**
* MonitorDetailsControlHeader component displays the control header for monitor details.
* It includes status display, pause/resume button, and a configure button for admins.
@@ -66,7 +63,7 @@ const MonitorDetailsControlHeader = ({
gap={theme.spacing(2)}
>
<Tooltip
key={monitor?._id}
key={monitor?.id}
placement="bottom"
title={tooltipTitle}
>
@@ -75,10 +72,15 @@ const MonitorDetailsControlHeader = ({
variant="contained"
color="secondary"
loading={isSending}
startIcon={<EmailIcon />}
startIcon={
<Icon
name="Mail"
size={18}
/>
}
disabled={isTestNotificationsDisabled}
onClick={() => {
testAllNotifications({ monitorId: monitor?._id });
testAllNotifications({ monitorId: monitor?.id });
}}
sx={{
whiteSpace: "nowrap",
@@ -92,7 +94,7 @@ const MonitorDetailsControlHeader = ({
variant="contained"
color="secondary"
onClick={(e) => {
navigate(`/incidents/${monitor?._id}`);
navigate(`/incidents/${monitor?.id}`);
}}
>
{t("menu.incidents")}
@@ -103,11 +105,21 @@ const MonitorDetailsControlHeader = ({
color="secondary"
loading={isPausing}
startIcon={
monitor?.isActive ? <PauseOutlinedIcon /> : <PlayArrowOutlinedIcon />
monitor?.isActive ? (
<Icon
name="Pause"
size={18}
/>
) : (
<Icon
name="Play"
size={18}
/>
)
}
onClick={() => {
pauseMonitor({
monitorId: monitor?._id,
monitorId: monitor?.id,
triggerUpdate,
});
}}
@@ -119,8 +131,13 @@ const MonitorDetailsControlHeader = ({
<Button
variant="contained"
color="secondary"
startIcon={<SettingsOutlinedIcon />}
onClick={() => navigate(`/${path}/configure/${monitor._id}`)}
startIcon={
<Icon
name="Settings"
size={18}
/>
}
onClick={() => navigate(`/${path}/configure/${monitor.id}`)}
>
Configure
</Button>
@@ -7,7 +7,7 @@ import Dot from "../Dot/index.jsx";
import { formatDurationRounded } from "../../../Utils/timeUtils.js";
import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { useMonitorUtils } from "../../../Hooks/v1/useMonitorUtils.js";
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
import { formatMonitorUrl } from "../../../Utils/utils.js";
/**
* Status component displays the status information of a monitor.
@@ -2,7 +2,7 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Divider from "@mui/material/Divider";
import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
import Icon from "../Icon";
import Search from "../Inputs/Search/index.jsx";
// Utils
@@ -102,11 +102,13 @@ const NotificationConfig = ({
>
{notification.notificationName}
</Typography>
<DeleteOutlineRoundedIcon
<Icon
name="Trash2"
size={20}
onClick={() => {
handleDelete(notification._id);
}}
sx={{ cursor: "pointer" }}
style={{ cursor: "pointer" }}
/>
{index < selectedNotifications.length - 1 && <Divider />}
</Stack>
@@ -3,7 +3,7 @@ import Fallback from "../Fallback/index.jsx";
import { Typography } from "@mui/material";
import { useTheme } from "@emotion/react";
import { useTranslation } from "react-i18next";
import { useIsAdmin } from "../../../Hooks/v1/useIsAdmin.js";
import { useIsAdmin } from "@/Hooks/useIsAdmin.js";
import PropTypes from "prop-types";
const PageStateWrapper = ({
networkError,
@@ -1,8 +1,7 @@
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
import { Box, IconButton, LinearProgress, Stack, Typography } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
import Icon from "../Icon";
import "./index.css";
/**
@@ -51,7 +50,10 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
alignItems={error ? "center" : "flex-start"}
>
{error ? (
<ErrorOutlineOutlinedIcon />
<Icon
name="AlertTriangle"
size={20}
/>
) : icon ? (
<Box
sx={{
@@ -127,10 +129,9 @@ const ProgressUpload = ({ icon, label, size, progress = 0, onClick, error }) =>
}
}
>
<CloseIcon
sx={{
fontSize: "20px",
}}
<Icon
name="X"
size={20}
/>
</IconButton>
</Stack>
@@ -1,15 +1,22 @@
import React from "react";
import { Step, Stepper, StepLabel, Typography } from "@mui/material";
import RadioButtonCheckedIcon from "@mui/icons-material/RadioButtonChecked";
import CheckCircle from "@mui/icons-material/CheckCircle";
import Icon from "../Icon";
import PropTypes from "prop-types";
const CustomStepIcon = (props) => {
const { completed, active } = props;
return completed ? (
<CheckCircle color="accent" />
<Icon
name="CheckCircle2"
size={24}
color="accent.main"
/>
) : (
<RadioButtonCheckedIcon color={active ? "accent" : "disabled"} />
<Icon
name="Circle"
size={24}
color={active ? "accent.main" : "text.disabled"}
/>
);
};
@@ -1,5 +1,5 @@
import html2canvas from "html2canvas";
import ShareIcon from "@mui/icons-material/Share";
import Icon from "../Icon";
import { Button } from "@mui/material";
import { useTheme } from "@emotion/react";
@@ -55,7 +55,13 @@ const ShareComponent = ({ elementToCapture, fileName = "screenshot" }) => {
return (
<Button
variant="outlined"
startIcon={<ShareIcon sx={{ color: theme.palette.success.main }} />}
startIcon={
<Icon
name="Share2"
size={18}
color="success.main"
/>
}
color="success"
onClick={captureAndShare}
>
@@ -7,8 +7,7 @@ import Avatar from "../../Avatar/index.jsx";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import Divider from "@mui/material/Divider";
import DotsVertical from "../../../../assets/icons/dots-vertical.svg?react";
import LogoutSvg from "../../../../assets/icons/logout.svg?react";
import Icon from "../../Icon";
import { useTheme } from "@emotion/react";
import { useSelector } from "react-redux";
@@ -81,6 +80,10 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
gap: theme.spacing(2),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
color: theme.palette.primary.contrastTextTertiary,
"& svg": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
>
{item.icon}
@@ -160,19 +163,14 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
ml: "50px",
"&:focus": { outline: "none" },
alignSelf: "center",
"& svg": {
width: "22px",
height: "22px",
},
"& svg path": {
/* Vertical three dots */
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
onClick={(event) => openPopup(event)}
>
<DotsVertical />
<Icon
name="MoreVertical"
size={22}
color="primary.contrastTextTertiary"
/>
</IconButton>
</Tooltip>
</Stack>
@@ -256,12 +254,13 @@ const AuthFooter = ({ collapsed, accountMenuItems }) => {
gap: theme.spacing(4),
borderRadius: theme.shape.borderRadius,
pl: theme.spacing(4),
"& svg path": {
stroke: theme.palette.primary.contrastTextTertiary,
},
}}
>
<LogoutSvg />
<Icon
name="LogOut"
size={20}
color="primary.contrastTextTertiary"
/>
{t("menu.logOut", "Log out")}
</MenuItem>
</Menu>
@@ -52,12 +52,11 @@ const NavItem = ({ item, collapsed, selected, onClick }) => {
<ListItemIcon
sx={{
minWidth: 0,
color: iconStroke,
"& svg": {
height: 20,
width: 20,
opacity: 0.81,
},
"& svg path": {
stroke: iconStroke,
},
}}
+21 -36
View File
@@ -8,22 +8,7 @@ import NavItem from "./components/navItem.jsx";
import AuthFooter from "./components/authFooter.jsx";
import StarPrompt from "../StarPrompt/index.jsx";
import LockSvg from "../../../assets/icons/lock.svg?react";
import UserSvg from "../../../assets/icons/user.svg?react";
import TeamSvg from "../../../assets/icons/user-two.svg?react";
import Support from "../../../assets/icons/support.svg?react";
import Maintenance from "../../../assets/icons/maintenance.svg?react";
import Monitors from "../../../assets/icons/monitors.svg?react";
import Incidents from "../../../assets/icons/incidents.svg?react";
import Integrations from "../../../assets/icons/integrations.svg?react";
import PageSpeed from "../../../assets/icons/page-speed.svg?react";
import Settings from "../../../assets/icons/settings.svg?react";
import ChangeLog from "../../../assets/icons/changeLog.svg?react";
import Docs from "../../../assets/icons/docs.svg?react";
import StatusPages from "../../../assets/icons/status-pages.svg?react";
import Discussions from "../../../assets/icons/discussions.svg?react";
import Notifications from "../../../assets/icons/notifications.svg?react";
import Logs from "../../../assets/icons/logs.svg?react";
import Icon from "../Icon";
// Utils
import { useTheme } from "@mui/material/styles";
@@ -39,43 +24,41 @@ const URL_MAP = {
};
const getMenu = (t) => [
{ name: t("menu.uptime"), path: "uptime", icon: <Monitors /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <PageSpeed /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Integrations /> },
{ name: t("menu.uptime"), path: "uptime", icon: <Icon name="Globe" /> },
{ name: t("menu.pagespeed"), path: "pagespeed", icon: <Icon name="Gauge" /> },
{ name: t("menu.infrastructure"), path: "infrastructure", icon: <Icon name="Link" /> },
{
name: t("menu.notifications"),
path: "notifications",
icon: <Notifications />,
icon: <Icon name="Bell" />,
},
{ name: t("menu.incidents"), path: "incidents", icon: <Incidents /> },
{ name: t("menu.statusPages"), path: "status", icon: <StatusPages /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Maintenance /> },
{ name: t("menu.logs"), path: "logs", icon: <Logs /> },
{ name: t("menu.checks"), path: "checks", icon: <Icon name="FileText" /> },
{ name: t("menu.incidents"), path: "incidents", icon: <Icon name="AlertTriangle" /> },
{ name: t("menu.statusPages"), path: "status", icon: <Icon name="Wifi" /> },
{ name: t("menu.maintenance"), path: "maintenance", icon: <Icon name="Wrench" /> },
{ name: t("menu.logs"), path: "logs", icon: <Icon name="Database" /> },
{
name: t("menu.settings"),
icon: <Settings />,
icon: <Icon name="Settings" />,
path: "settings",
},
];
const getOtherMenuItems = (t) => [
{ name: t("menu.support"), path: "support", icon: <Support /> },
{ name: t("menu.support"), path: "support", icon: <Icon name="HelpCircle" /> },
{
name: t("menu.discussions"),
path: "discussions",
icon: <Discussions />,
icon: <Icon name="MessageCircle" />,
},
{ name: t("menu.docs"), path: "docs", icon: <Docs /> },
{ name: t("menu.changelog"), path: "changelog", icon: <ChangeLog /> },
{ name: t("menu.docs"), path: "docs", icon: <Icon name="FileText" /> },
{ name: t("menu.changelog"), path: "changelog", icon: <Icon name="Code" /> },
];
const getAccountMenuItems = (t) => [
{ name: t("menu.profile"), path: "account/profile", icon: <UserSvg /> },
{ name: t("menu.password"), path: "account/password", icon: <LockSvg /> },
{ name: t("menu.team"), path: "account/team", icon: <TeamSvg /> },
{ name: t("menu.profile"), path: "account/profile", icon: <Icon name="User" /> },
{ name: t("menu.password"), path: "account/password", icon: <Icon name="Lock" /> },
{ name: t("menu.team"), path: "account/team", icon: <Icon name="Users" /> },
];
const Sidebar = () => {
@@ -83,7 +66,7 @@ const Sidebar = () => {
const { t } = useTranslation();
const navigate = useNavigate();
// Redux state
const collapsed = useSelector((state) => state.ui.sidebar.collapsed);
const collapsed = useSelector((state) => state.ui.sidebar?.collapsed ?? false);
const menu = getMenu(t);
const otherMenuItems = getOtherMenuItems(t);
@@ -106,6 +89,8 @@ const Sidebar = () => {
gap={theme.spacing(6)}
sx={{
transition: "width 650ms cubic-bezier(0.36, -0.01, 0, 0.77)",
backgroundColor: theme.palette.primaryBackground.main,
zIndex: 1000,
}}
>
<CollapseButton collapsed={collapsed} />
@@ -1,6 +1,6 @@
import React from "react";
import { Typography, IconButton, Stack, Box } from "@mui/material";
import CloseIcon from "@mui/icons-material/Close";
import Icon from "../Icon";
import { useTheme } from "@emotion/react";
import { useSelector, useDispatch } from "react-redux";
import { useTranslation } from "react-i18next";
@@ -67,7 +67,10 @@ const StarPrompt = ({ repoUrl = "https://github.com/bluewave-labs/checkmate" })
},
}}
>
<CloseIcon sx={{ fontSize: "1.25rem" }} />
<Icon
name="X"
size={20}
/>
</IconButton>
</Stack>
+1 -1
View File
@@ -2,7 +2,7 @@ import { Stack, Typography } from "@mui/material";
import Image from "../Image/index.jsx";
import { useTheme } from "@mui/material/styles";
import PropTypes from "prop-types";
import { useMonitorUtils } from "../../../Hooks/v1/useMonitorUtils.js";
import { useMonitorUtils } from "../../../Hooks/useMonitorUtils.js";
/**
* StatBox Component
@@ -2,7 +2,7 @@ import PropTypes from "prop-types";
import { useTheme } from "@emotion/react";
import { Stack, TablePagination, Typography } from "@mui/material";
import { TablePaginationActions } from "./Actions/index.jsx";
import SelectorVertical from "../../../../assets/icons/selector-vertical.svg?react";
import Icon from "../../Icon";
Pagination.propTypes = {
paginationLabel: PropTypes.string, // Label for the pagination.
@@ -100,7 +100,12 @@ function Pagination({
},
},
inputProps: { id: "pagination-dropdown" },
IconComponent: SelectorVertical,
IconComponent: () => (
<Icon
name="ChevronsUpDown"
size={16}
/>
),
sx: {
ml: theme.spacing(4),
mr: theme.spacing(12),
+27 -9
View File
@@ -3,19 +3,31 @@ import IconButton from "@mui/material/IconButton";
import Typography from "@mui/material/Typography";
import Button from "@mui/material/Button";
import ToastBody from "./body.jsx";
import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined";
import ErrorOutlineOutlinedIcon from "@mui/icons-material/ErrorOutlineOutlined";
import WarningAmberOutlinedIcon from "@mui/icons-material/WarningAmberOutlined";
import CloseIcon from "@mui/icons-material/Close";
import Icon from "../Icon";
// Utils
import { useTheme } from "@emotion/react";
import PropTypes from "prop-types";
const icons = {
info: <InfoOutlinedIcon />,
error: <ErrorOutlineOutlinedIcon />,
warning: <WarningAmberOutlinedIcon />,
info: (
<Icon
name="Info"
size={24}
/>
),
error: (
<Icon
name="AlertCircle"
size={24}
/>
),
warning: (
<Icon
name="AlertTriangle"
size={24}
/>
),
};
const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
@@ -50,7 +62,10 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
)}
{title && (
<IconButton onClick={onClick}>
<CloseIcon />
<Icon
name="X"
size={20}
/>
</IconButton>
)}
</Stack>
@@ -63,7 +78,10 @@ const Toast = ({ variant, title, body, onClick, hasDismiss, hasIcon }) => {
<ToastBody body={body} />
{!title && (
<IconButton onClick={onClick}>
<CloseIcon />
<Icon
name="X"
size={20}
/>
</IconButton>
)}
</Stack>
@@ -0,0 +1,157 @@
/* ArticlePage styles - hover states and nested selectors (Dark theme) */
/* Breadcrumb hover */
.user-guide-breadcrumb-link {
transition: color 150ms ease;
}
.user-guide-breadcrumb-link:hover {
color: #1570ef;
}
/* ToC link hover */
.user-guide-toc-link {
transition: all 150ms ease;
}
.user-guide-toc-link:hover {
color: #1570ef;
text-decoration: underline !important;
}
/* Navigation button hover */
.user-guide-nav-button .nav-text {
transition: color 150ms ease;
}
.user-guide-nav-button:hover .nav-text {
color: #1570ef;
}
/* Article body typography */
.user-guide-article-body h2 {
font-size: 18px;
font-weight: 600;
color: #ffffff;
margin-top: 32px;
margin-bottom: 16px;
line-height: 1.4;
}
.user-guide-article-body h3 {
font-size: 14px;
font-weight: 600;
color: #ffffff;
margin-top: 24px;
margin-bottom: 12px;
line-height: 1.4;
}
.user-guide-article-body p {
font-size: 13px;
color: #d0d5dd;
line-height: 1.5;
margin-bottom: 16px;
margin-top: 0;
}
.user-guide-article-body ul,
.user-guide-article-body ol {
padding-left: 24px;
margin-bottom: 16px;
margin-top: 0;
}
.user-guide-article-body li {
font-size: 13px;
color: #d0d5dd;
line-height: 1.5;
margin-bottom: 8px;
}
.user-guide-article-body a {
color: #1570ef;
text-decoration: none;
font-weight: 500;
}
.user-guide-article-body a:hover {
text-decoration: underline;
}
.user-guide-article-body code {
background-color: #2d2d2d;
padding: 2px 6px;
border-radius: 4px;
font-size: 10px;
font-family: "Geist Mono", "Fira Code", "Consolas", monospace;
color: #d0d5dd;
}
.user-guide-article-body pre {
background-color: #1c2130;
border-radius: 4px;
padding: 16px;
overflow: auto;
margin-bottom: 16px;
margin-top: 0;
}
.user-guide-article-body pre code {
background-color: transparent;
padding: 0;
color: #e5e7eb;
}
.user-guide-article-body blockquote {
border-left: 3px solid #1570ef;
background-color: rgba(255, 255, 255, 0.05);
padding: 12px 16px;
border-radius: 0 4px 4px 0;
margin-bottom: 16px;
margin-top: 0;
margin-left: 0;
margin-right: 0;
}
.user-guide-article-body blockquote p {
margin-bottom: 0;
color: #d0d5dd;
}
.user-guide-article-body table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
}
.user-guide-article-body th,
.user-guide-article-body td {
border: 1px solid #344054;
padding: 12px;
text-align: left;
font-size: 10px;
}
.user-guide-article-body th {
background-color: rgba(255, 255, 255, 0.05);
font-weight: 600;
color: #ffffff;
}
.user-guide-article-body td {
color: #d0d5dd;
}
.user-guide-article-body img {
max-width: 100%;
border-radius: 4px;
border: 1px solid #344054;
margin-bottom: 16px;
}
.user-guide-article-body hr {
border: none;
border-top: 1px solid #344054;
margin: 32px 0;
}
@@ -0,0 +1,333 @@
import { useState, useEffect } from "react";
import {
ChevronLeft,
ChevronRight,
Rocket,
Globe,
Gauge,
Link,
Bell,
AlertTriangle,
Settings,
FileText,
} from "lucide-react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./ArticlePage.css";
// Map icon names to actual Lucide components
const iconMap = {
Rocket,
Globe,
Gauge,
Link,
Bell,
AlertTriangle,
Settings,
FileText,
};
const ArticlePage = ({
collection,
article,
onBack,
onBackToHome,
onNextArticle,
onPrevArticle,
nextArticle,
prevArticle,
tocItems,
children,
mode = "in-app",
}) => {
const { colors, typography, spacing, border, isDark } = useUserGuideTheme();
const IconComponent = iconMap[collection.icon] || Rocket;
const isInApp = mode === "in-app";
const [activeSection, setActiveSection] = useState("");
// Track active section on scroll
useEffect(() => {
if (!tocItems || tocItems.length === 0) return;
const handleScroll = () => {
const sections = tocItems
.map((item) => {
const id = item.href.replace("#", "");
const element = document.getElementById(id);
return { id: item.href, element };
})
.filter((s) => s.element);
if (sections.length === 0) return;
let currentSection = sections[0].id;
for (const section of sections) {
if (section.element) {
const rect = section.element.getBoundingClientRect();
if (rect.top <= 150) {
currentSection = section.id;
}
}
}
setActiveSection(currentSection);
};
window.addEventListener("scroll", handleScroll, { passive: true });
document.addEventListener("scroll", handleScroll, { passive: true, capture: true });
handleScroll();
return () => {
window.removeEventListener("scroll", handleScroll);
document.removeEventListener("scroll", handleScroll, { capture: true });
};
}, [tocItems]);
return (
<div style={{ minHeight: "100%", backgroundColor: colors.background.alt }}>
{/* Main content area */}
<div
style={{
maxWidth: isInApp ? "100%" : 1100,
margin: "0 auto",
padding: isInApp ? spacing.lg : spacing["3xl"],
}}
>
{/* Article Content */}
<div style={{ maxWidth: isInApp ? "100%" : 680, minWidth: 0 }}>
{/* Title */}
<h1
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize["3xl"],
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.lg,
lineHeight: typography.lineHeight.snug,
marginTop: 0,
}}
>
{article.title}
</h1>
{/* Description */}
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.md,
color: colors.text.secondary,
marginBottom: spacing.lg,
lineHeight: typography.lineHeight.normal,
marginTop: 0,
}}
>
{article.description}
</p>
{/* Table of Contents - at top */}
{tocItems && tocItems.length > 0 && (
<div
style={{
marginBottom: spacing["2xl"],
paddingBottom: isInApp ? 0 : spacing.lg,
borderBottom: isInApp ? "none" : border.default,
}}
>
<div
style={{
backgroundColor: isInApp
? isDark
? "rgba(255, 255, 255, 0.05)"
: "rgba(0, 0, 0, 0.02)"
: "transparent",
borderRadius: isInApp ? border.radius : 0,
padding: isInApp ? spacing.md : 0,
border: isInApp ? border.default : "none",
}}
>
<span
style={{
display: "block",
fontSize: 12,
fontWeight: typography.fontWeight.semibold,
color: colors.text.secondary,
marginBottom: spacing.sm,
textTransform: "uppercase",
letterSpacing: "0.5px",
}}
>
In this article
</span>
<div
style={{
display: "flex",
flexDirection: isInApp ? "column" : "row",
flexWrap: isInApp ? "nowrap" : "wrap",
gap: isInApp ? 0 : spacing.xs,
columnGap: isInApp ? 0 : spacing.lg,
}}
>
{tocItems.map((item) => {
const isActive = activeSection === item.href;
return (
<a
key={item.href}
href={item.href}
className="user-guide-toc-link"
onClick={(e) => {
if (isInApp) {
e.preventDefault();
const id = item.href.replace("#", "");
const element = document.getElementById(id);
if (element) {
element.scrollIntoView({
behavior: "smooth",
block: "start",
});
}
}
}}
style={{
fontFamily: typography.fontFamily.sans,
fontSize: isInApp ? 13 : 12,
color: isActive ? colors.brand.primary : colors.text.secondary,
fontWeight: isActive
? typography.fontWeight.medium
: typography.fontWeight.normal,
textDecoration: "none",
padding: isInApp ? "2px 0" : `${spacing.xs} 0`,
}}
>
{item.label}
</a>
);
})}
</div>
</div>
</div>
)}
{/* Article Body */}
<div className="user-guide-article-body">
{children || (
<div
style={{
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
padding: spacing.xl,
textAlign: "center",
}}
>
<span
style={{ fontSize: typography.fontSize.sm, color: colors.text.muted }}
>
Article content coming soon.
</span>
</div>
)}
</div>
{/* Navigation */}
{(prevArticle || nextArticle) && (
<div
style={{
display: "flex",
justifyContent: "space-between",
marginTop: spacing["2xl"],
paddingTop: spacing.xl,
borderTop: border.default,
}}
>
{prevArticle ? (
<div
onClick={onPrevArticle}
className="user-guide-nav-button"
style={{
display: "flex",
alignItems: "center",
gap: spacing.sm,
cursor: "pointer",
}}
>
<ChevronLeft
size={16}
strokeWidth={1.5}
color={colors.text.muted}
/>
<div>
<span
style={{
display: "block",
fontSize: typography.fontSize.xs,
color: colors.text.muted,
}}
>
Previous
</span>
<span
className="nav-text"
style={{
display: "block",
fontSize: typography.fontSize.sm,
color: colors.text.primary,
fontWeight: typography.fontWeight.medium,
}}
>
{prevArticle.title}
</span>
</div>
</div>
) : (
<div />
)}
{nextArticle && (
<div
onClick={onNextArticle}
className="user-guide-nav-button"
style={{
display: "flex",
alignItems: "center",
gap: spacing.sm,
textAlign: "right",
cursor: "pointer",
}}
>
<div>
<span
style={{
display: "block",
fontSize: typography.fontSize.xs,
color: colors.text.muted,
}}
>
Next
</span>
<span
className="nav-text"
style={{
display: "block",
fontSize: typography.fontSize.sm,
color: colors.text.primary,
fontWeight: typography.fontWeight.medium,
}}
>
{nextArticle.title}
</span>
</div>
<ChevronRight
size={16}
strokeWidth={1.5}
color={colors.text.muted}
/>
</div>
)}
</div>
)}
</div>
</div>
</div>
);
};
export default ArticlePage;
@@ -0,0 +1,31 @@
/* CollectionPage styles - hover states */
/* Back link hover */
.user-guide-back-link {
transition: color 150ms ease;
}
.user-guide-back-link:hover {
color: #1570ef;
}
/* Article row hover */
.user-guide-article-row {
transition: all 150ms ease;
}
.user-guide-article-row:hover {
background-color: rgba(0, 0, 0, 0.04);
}
[data-mui-color-scheme="dark"] .user-guide-article-row:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-article-row .arrow-icon {
transition: all 150ms ease;
}
.user-guide-article-row:hover .arrow-icon {
color: #1570ef;
}
@@ -0,0 +1,171 @@
import {
ArrowRight,
FileText,
Rocket,
Globe,
Gauge,
Link,
Bell,
AlertTriangle,
Settings,
} from "lucide-react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./CollectionPage.css";
// Map icon names to actual Lucide components
const iconMap = {
Rocket,
Globe,
Gauge,
Link,
Bell,
AlertTriangle,
Settings,
FileText,
};
const CollectionPage = ({ collection, onBack, onArticleClick }) => {
const { colors, typography, spacing, border } = useUserGuideTheme();
const IconComponent = iconMap[collection.icon] || Rocket;
return (
<div style={{ minHeight: "100%", backgroundColor: colors.background.alt }}>
{/* Header */}
<div
style={{
backgroundColor: colors.background.white,
borderBottom: border.default,
padding: `${spacing.lg} ${spacing.lg}`,
}}
>
<div style={{ maxWidth: 900, margin: "0 auto" }}>
{/* Collection Header */}
<div style={{ display: "flex", alignItems: "flex-start", gap: spacing.md }}>
<IconComponent
size={24}
strokeWidth={1.5}
color={colors.brand.primary}
style={{ marginTop: 4 }}
/>
<div>
<h1
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize["2xl"],
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.xs,
marginTop: 0,
}}
>
{collection.title}
</h1>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
margin: 0,
}}
>
{collection.description}
</p>
<span
style={{
display: "block",
fontSize: typography.fontSize.sm,
color: colors.text.muted,
marginTop: spacing.sm,
}}
>
{collection.articleCount} articles in this collection
</span>
</div>
</div>
</div>
</div>
{/* Articles List */}
<div
style={{
maxWidth: 900,
margin: "0 auto",
padding: `${spacing.lg} ${spacing.lg}`,
}}
>
<div
style={{
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
overflow: "hidden",
}}
>
{collection.articles.map((article, index) => (
<div
key={article.id}
onClick={() => onArticleClick(article.id)}
className="user-guide-article-row"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: spacing.lg,
borderBottom:
index < collection.articles.length - 1 ? border.default : "none",
cursor: "pointer",
}}
>
<div style={{ display: "flex", alignItems: "flex-start", gap: spacing.md }}>
<FileText
size={16}
strokeWidth={1.5}
color={colors.text.muted}
style={{ marginTop: 2, flexShrink: 0 }}
/>
<div>
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
marginBottom: spacing.xs,
}}
>
{article.title}
</span>
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
}}
>
{article.description}
</span>
</div>
</div>
<ArrowRight
className="arrow-icon"
size={16}
strokeWidth={1.5}
color={colors.text.muted}
style={{
flexShrink: 0,
marginLeft: 16,
}}
/>
</div>
))}
</div>
</div>
</div>
);
};
export default CollectionPage;
@@ -0,0 +1,45 @@
/* ContentRenderer styles - hover states and list styling */
/* List styling - colors handled by inline styles for theme support */
.user-guide-content-list li {
/* Font styles handled inline for theme-aware colors */
}
/* Table row hover */
.user-guide-table-row {
transition: background-color 150ms ease;
}
.user-guide-table-row:hover {
background-color: rgba(0, 0, 0, 0.04) !important;
}
[data-mui-color-scheme="dark"] .user-guide-table-row:hover {
background-color: rgba(255, 255, 255, 0.05) !important;
}
/* Article links hover */
.user-guide-article-link-item {
transition: all 150ms ease;
}
.user-guide-article-link-item:hover {
background-color: rgba(0, 0, 0, 0.04);
}
[data-mui-color-scheme="dark"] .user-guide-article-link-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-article-link-item:hover .link-title {
color: #1570ef;
}
.user-guide-article-link-item .arrow-icon {
transition: all 150ms ease;
}
.user-guide-article-link-item:hover .arrow-icon {
transform: translateX(4px);
color: #1570ef;
}
@@ -0,0 +1,925 @@
import { useState } from "react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import ImageLightbox from "./ImageLightbox";
import {
CheckCircle,
Clock,
Rocket,
Shield,
Brain,
AlertTriangle,
Server,
Database,
Terminal,
FileText,
FolderKanban,
Gauge,
LayoutDashboard,
Menu,
Settings,
Info,
Lightbulb,
ArrowRight,
Globe,
Bell,
Link,
} from "lucide-react";
import "./ContentRenderer.css";
// Icon mapping
const iconMap = {
CheckCircle,
Clock,
Rocket,
Shield,
Brain,
AlertTriangle,
Server,
Database,
Terminal,
FileText,
FolderKanban,
Gauge,
LayoutDashboard,
Menu,
Settings,
Info,
Lightbulb,
Globe,
Bell,
Link,
};
const getIcon = (name) => iconMap[name] || Info;
const ContentRenderer = ({ content, onNavigate }) => {
const { colors, typography, spacing, border, chipStyles, imageStyles, isDark } =
useUserGuideTheme();
const [lightboxImage, setLightboxImage] = useState(null);
// Parse basic markdown in text (inside component to access onNavigate)
const parseMarkdown = (text) => {
const parts = [];
let key = 0;
// Supports: **bold**, `code`, [[link text]](collection/article)
const combinedRegex = /\*\*([^*]+)\*\*|`([^`]+)`|\[\[([^\]]+)\]\]\(([^)]+)\)/g;
let lastIndex = 0;
let match;
const getChipStyle = (codeContent) => {
const isUrl = /^https?:\/\//.test(codeContent);
return isUrl ? chipStyles.url : chipStyles.code;
};
while ((match = combinedRegex.exec(text)) !== null) {
if (match.index > lastIndex) {
parts.push(text.slice(lastIndex, match.index));
}
if (match[1]) {
// Bold: **text**
parts.push(<strong key={key++}>{match[1]}</strong>);
} else if (match[2]) {
// Code: `text`
const style = getChipStyle(match[2]);
parts.push(
<span
key={key++}
style={style}
>
{match[2]}
</span>
);
} else if (match[3] && match[4]) {
// Article link: [[text]](collection/article)
const linkText = match[3];
const linkPath = match[4];
const [collectionId, articleId] = linkPath.split("/");
parts.push(
<span
key={key++}
onClick={() => onNavigate?.(collectionId, articleId)}
style={{
color: colors.brand.primary,
cursor: "pointer",
textDecoration: "underline",
textDecorationColor: "transparent",
transition: "text-decoration-color 150ms ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.textDecorationColor = colors.brand.primary;
}}
onMouseLeave={(e) => {
e.currentTarget.style.textDecorationColor = "transparent";
}}
>
{linkText}
</span>
);
}
lastIndex = match.index + match[0].length;
}
if (lastIndex < text.length) {
parts.push(text.slice(lastIndex));
}
return parts.length > 0 ? parts : text;
};
const renderBlock = (block, index) => {
switch (block.type) {
case "heading":
return renderHeading(block, index);
case "paragraph":
return renderParagraph(block, index);
case "callout":
return renderCallout(block, index);
case "bullet-list":
return renderBulletList(block, index);
case "ordered-list":
return renderOrderedList(block, index);
case "checklist":
return renderChecklist(block, index);
case "icon-cards":
return renderIconCards(block, index);
case "grid-cards":
return renderGridCards(block, index);
case "time-estimate":
return renderTimeEstimate(block, index);
case "info-box":
return renderInfoBox(block, index);
case "code":
return renderCode(block, index);
case "requirements":
return renderRequirements(block, index);
case "table":
return renderTable(block, index);
case "image":
return renderImage(block, index);
case "article-links":
return renderArticleLinks(block, index);
default:
return null;
}
};
const renderHeading = (block, index) => {
const Tag = block.level === 2 ? "h2" : "h3";
return (
<Tag
key={index}
id={block.id}
style={{
fontFamily: typography.fontFamily.sans,
fontSize:
block.level === 2 ? typography.fontSize["2xl"] : typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.lg,
marginTop: index > 0 ? spacing["2xl"] : 0,
lineHeight: typography.lineHeight.normal,
}}
>
{block.text}
</Tag>
);
};
const renderParagraph = (block, index) => (
<p
key={index}
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.relaxed,
marginBottom: spacing.lg,
marginTop: 0,
}}
>
{parseMarkdown(block.text)}
</p>
);
const renderCallout = (block, index) => {
const calloutColors = {
info: colors.brand.primary,
tip: colors.brand.primary,
warning: colors.brand.warning,
success: colors.brand.success,
};
const accentColor = calloutColors[block.variant || "info"];
return (
<div
key={index}
style={{
border: border.default,
borderLeftWidth: "3px",
borderLeftColor: accentColor,
backgroundColor: isDark ? "rgba(255, 255, 255, 0.03)" : "rgba(0, 0, 0, 0.02)",
paddingLeft: spacing.lg,
paddingTop: spacing.md,
paddingBottom: spacing.md,
paddingRight: spacing.lg,
borderRadius: border.radius,
marginBottom: spacing["2xl"],
}}
>
{block.title && (
<span
style={{
display: "block",
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.xs,
}}
>
{block.title}
</span>
)}
<span
style={{
display: "block",
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
}}
>
{parseMarkdown(block.text)}
</span>
</div>
);
};
const renderBulletList = (block, index) => (
<ul
key={index}
className="user-guide-content-list"
style={{
paddingLeft: spacing.xl,
marginBottom: spacing["2xl"],
marginTop: 0,
}}
>
{block.items.map((item, i) => (
<li
key={i}
style={{
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
marginBottom: spacing.sm,
}}
>
{item.bold && (
<strong style={{ color: colors.text.primary }}>{item.bold}</strong>
)}
{item.bold && <span style={{ color: colors.text.secondary }}> — </span>}
<span style={{ color: colors.text.secondary }}>
{item.bold ? parseMarkdown(item.text) : parseMarkdown(item.text)}
</span>
</li>
))}
</ul>
);
const renderOrderedList = (block, index) => (
<ol
key={index}
className="user-guide-content-list"
style={{
paddingLeft: spacing.xl,
marginBottom: spacing.lg,
marginTop: 0,
}}
>
{block.items.map((item, i) => (
<li
key={i}
style={{
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
marginBottom: spacing.sm,
}}
>
{item.bold && (
<strong style={{ color: colors.text.primary }}>{item.bold}</strong>
)}
{item.bold && " "}
<span style={{ color: colors.text.secondary }}>
{item.bold ? parseMarkdown(item.text) : parseMarkdown(item.text)}
</span>
</li>
))}
</ol>
);
const renderChecklist = (block, index) => (
<div
key={index}
style={{ marginBottom: spacing["2xl"], paddingLeft: spacing.sm }}
>
{block.items.map((item, i) => (
<div
key={i}
style={{
display: "flex",
flexDirection: "row",
alignItems: "flex-start",
gap: "10px",
marginBottom: spacing.sm,
}}
>
<CheckCircle
size={16}
strokeWidth={1.5}
color={colors.brand.primary}
style={{ flexShrink: 0, marginTop: 3 }}
/>
<span
style={{
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.tight,
}}
>
{parseMarkdown(item)}
</span>
</div>
))}
</div>
);
const renderIconCards = (block, index) => (
<div
key={index}
style={{
display: "flex",
flexDirection: "column",
gap: spacing.md,
marginBottom: spacing["2xl"],
}}
>
{block.items.map((item, i) => {
const IconComponent = getIcon(item.icon);
return (
<div
key={i}
style={{
display: "flex",
gap: spacing.md,
padding: spacing.sm,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
width: "20px",
height: "22px",
}}
>
<IconComponent
size={20}
strokeWidth={1.5}
color={colors.brand.primary}
/>
</div>
<div>
<span
style={{
display: "block",
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: "22px",
marginBottom: "2px",
}}
>
{item.title}
</span>
<span
style={{
display: "block",
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
}}
>
{parseMarkdown(item.description)}
</span>
</div>
</div>
);
})}
</div>
);
const renderGridCards = (block, index) => (
<div
key={index}
style={{
display: "grid",
gridTemplateColumns: `repeat(auto-fit, minmax(${block.columns === 2 ? "250px" : "200px"}, 1fr))`,
gap: spacing.md,
marginBottom: spacing["2xl"],
}}
>
{block.items.map((item, i) => {
const IconComponent = item.icon ? getIcon(item.icon) : null;
return (
<div
key={i}
style={{
padding: "14px",
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
}}
>
<div
style={{
display: "flex",
alignItems: "flex-start",
gap: spacing.sm,
marginBottom: "6px",
}}
>
{IconComponent && (
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
width: "16px",
height: "18px",
}}
>
<IconComponent
size={16}
strokeWidth={1.5}
color={colors.brand.primary}
/>
</span>
)}
<span
style={{
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: "18px",
}}
>
{item.title}
</span>
</div>
<span
style={{
display: "block",
fontSize: typography.fontSize.xs,
color: colors.text.secondary,
lineHeight: typography.lineHeight.snug,
}}
>
{parseMarkdown(item.description)}
</span>
</div>
);
})}
</div>
);
const renderTimeEstimate = (block, index) => (
<div
key={index}
style={{
display: "inline-flex",
alignItems: "center",
gap: spacing.sm,
padding: `${spacing.md} ${spacing.lg}`,
backgroundColor: isDark ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.03)",
border: border.default,
borderRadius: border.radius,
marginBottom: spacing.xl,
}}
>
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
flexShrink: 0,
height: "20px",
}}
>
<Clock
size={16}
strokeWidth={1.5}
color={colors.brand.primary}
/>
</span>
<span
style={{
fontSize: typography.fontSize.base,
color: colors.text.primary,
lineHeight: "20px",
display: "inline-flex",
alignItems: "center",
}}
>
{parseMarkdown(block.text)}
</span>
</div>
);
const renderInfoBox = (block, index) => {
const IconComponent = block.icon ? getIcon(block.icon) : FolderKanban;
return (
<div
key={index}
style={{
display: "flex",
gap: spacing.md,
padding: spacing.lg,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
marginBottom: spacing.lg,
}}
>
<IconComponent
size={20}
strokeWidth={1.5}
color={colors.brand.primary}
style={{ flexShrink: 0, marginTop: 2 }}
/>
<div>
<span
style={{
display: "block",
fontSize: typography.fontSize.md,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.sm,
}}
>
{block.title}
</span>
<ul style={{ margin: 0, paddingLeft: "20px" }}>
{block.items.map((item, i) => (
<li
key={i}
style={{
fontSize: typography.fontSize.sm,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
marginBottom: spacing.xs,
}}
>
{parseMarkdown(item)}
</li>
))}
</ul>
</div>
</div>
);
};
const renderCode = (block, index) => (
<div
key={index}
style={{
backgroundColor: colors.background.code,
borderRadius: border.radius,
padding: "14px 16px",
overflow: "auto",
marginBottom: spacing.lg,
}}
>
<pre
style={{
fontSize: typography.fontSize.sm,
fontFamily: typography.fontFamily.mono,
color: "#E5E7EB",
margin: 0,
whiteSpace: "pre-wrap",
wordBreak: "break-all",
}}
>
{block.code}
</pre>
</div>
);
const renderRequirements = (block, index) => (
<div
key={index}
style={{
display: "flex",
flexDirection: "column",
gap: spacing.md,
marginBottom: spacing["2xl"],
}}
>
{block.items.map((req, i) => {
const IconComponent = getIcon(req.icon);
return (
<div
key={i}
style={{
padding: spacing.lg,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
}}
>
<div
style={{
display: "flex",
alignItems: "center",
gap: spacing.sm,
marginBottom: spacing.md,
}}
>
<IconComponent
size={16}
strokeWidth={1.5}
color={colors.brand.primary}
style={{ flexShrink: 0 }}
/>
<span
style={{
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
}}
>
{req.title}
</span>
</div>
<ul style={{ margin: 0, paddingLeft: "20px" }}>
{req.items.map((item, j) => (
<li
key={j}
style={{
fontSize: typography.fontSize.sm,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
marginBottom: spacing.xs,
}}
>
{item}
</li>
))}
</ul>
</div>
);
})}
</div>
);
const renderTableCell = (value) => {
if (value === "✓") {
return (
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 20,
height: 20,
borderRadius: "50%",
backgroundColor: "rgba(19, 113, 91, 0.2)",
color: colors.brand.primary,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
}}
>
✓
</span>
);
}
if (value === "✗" || value === "—") {
return (
<span
style={{
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
width: 20,
height: 20,
borderRadius: "50%",
backgroundColor: "rgba(102, 112, 133, 0.2)",
color: colors.text.muted,
fontSize: typography.fontSize.sm,
}}
>
—
</span>
);
}
return parseMarkdown(value);
};
const renderTable = (block, index) => (
<div
key={index}
style={{
border: border.default,
borderRadius: border.radius,
overflow: "hidden",
marginBottom: spacing["2xl"],
}}
>
{/* Header */}
<div
style={{
display: "grid",
gridTemplateColumns: block.columns.map((col) => col.width || "1fr").join(" "),
backgroundColor: isDark ? "rgba(255, 255, 255, 0.05)" : "rgba(0, 0, 0, 0.03)",
borderBottom: border.default,
padding: `${spacing.md} ${spacing.lg}`,
gap: spacing.md,
}}
>
{block.columns.map((col) => (
<span
key={col.key}
style={{
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: typography.lineHeight.tight,
}}
>
{col.label}
</span>
))}
</div>
{/* Rows */}
{block.rows.map((row, i) => (
<div
key={i}
className="user-guide-table-row"
style={{
display: "grid",
gridTemplateColumns: block.columns.map((col) => col.width || "1fr").join(" "),
borderBottom: i < block.rows.length - 1 ? border.default : "none",
backgroundColor:
i % 2 === 1
? isDark
? "rgba(255, 255, 255, 0.02)"
: "rgba(0, 0, 0, 0.02)"
: "transparent",
padding: `10px ${spacing.lg}`,
gap: spacing.md,
alignItems: "center",
}}
>
{block.columns.map((col, j) => (
<div
key={col.key}
style={{
fontSize: typography.fontSize.sm,
fontWeight:
j === 0 ? typography.fontWeight.medium : typography.fontWeight.normal,
color: j === 0 ? colors.text.primary : colors.text.secondary,
lineHeight: typography.lineHeight.normal,
}}
>
{renderTableCell(row[col.key])}
</div>
))}
</div>
))}
</div>
);
const renderImage = (block, index) => (
<div
key={index}
style={{
marginBottom: spacing["2xl"],
}}
>
<img
src={block.src}
alt={block.alt}
onClick={() =>
setLightboxImage({ src: block.src, alt: block.alt, caption: block.caption })
}
style={{
...imageStyles.image,
cursor: "zoom-in",
}}
/>
{block.caption && (
<p
style={{
...imageStyles.caption,
marginTop: spacing.sm,
marginBottom: 0,
}}
>
{block.caption}
</p>
)}
</div>
);
const renderArticleLinks = (block, index) => (
<div
key={index}
style={{ marginBottom: spacing["2xl"] }}
>
<div
style={{
height: "1px",
backgroundColor: colors.border.default,
marginBottom: spacing.xl,
}}
/>
{block.title && (
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.md,
}}
>
{block.title}
</span>
)}
<div style={{ display: "flex", flexDirection: "column" }}>
{block.items.map((item, i) => (
<div
key={i}
onClick={() => onNavigate?.(item.collectionId, item.articleId)}
className="user-guide-article-link-item"
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: spacing.sm,
margin: `0 -${spacing.sm}`,
borderRadius: "4px",
cursor: "pointer",
}}
>
<span
className="link-title"
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
}}
>
{item.title}
</span>
<ArrowRight
className="arrow-icon"
size={18}
strokeWidth={1.5}
color={colors.text.muted}
style={{ flexShrink: 0, marginLeft: 16 }}
/>
</div>
))}
</div>
</div>
);
return (
<>
<div>{content.blocks.map((block, index) => renderBlock(block, index))}</div>
{/* Image Lightbox */}
{lightboxImage && (
<ImageLightbox
src={lightboxImage.src}
alt={lightboxImage.alt}
caption={lightboxImage.caption}
onClose={() => setLightboxImage(null)}
/>
)}
</>
);
};
export default ContentRenderer;
@@ -0,0 +1,14 @@
/* HelpSection styles */
.user-guide-help-button {
transition: all 150ms ease;
}
.user-guide-help-button:hover {
border-color: #1570ef;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.06);
}
[data-mui-color-scheme="dark"] .user-guide-help-button:hover {
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.25);
}
@@ -0,0 +1,134 @@
import { ExternalLink } from "lucide-react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./HelpSection.css";
const HelpSection = () => {
const { colors, typography, spacing, border } = useUserGuideTheme();
return (
<div
style={{
padding: spacing.xl,
minHeight: "100%",
}}
>
{/* Contact us */}
<div style={{ marginBottom: spacing["2xl"] }}>
<h2
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.sm,
marginTop: 0,
}}
>
Contact us
</h2>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
margin: 0,
marginBottom: spacing.md,
}}
>
Can't find what you need? Our support team is here to help.
</p>
<a
href="https://discord.com/invite/NAb6H3UTjK"
target="_blank"
rel="noopener noreferrer"
className="user-guide-help-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: spacing.sm,
width: "100%",
padding: `${spacing.md} ${spacing.lg}`,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
textDecoration: "none",
cursor: "pointer",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
}}
>
Join our Discord
<ExternalLink
size={14}
strokeWidth={1.5}
color={colors.text.muted}
/>
</a>
</div>
{/* Ask the community */}
<div>
<h2
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.sm,
marginTop: 0,
}}
>
GitHub discussions
</h2>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
margin: 0,
marginBottom: spacing.md,
}}
>
Questions about features, how-tos, or use cases? Join the community discussion.
</p>
<a
href="https://github.com/bluewave-labs/checkmate/discussions"
target="_blank"
rel="noopener noreferrer"
className="user-guide-help-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
gap: spacing.sm,
width: "100%",
padding: `${spacing.md} ${spacing.lg}`,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
textDecoration: "none",
cursor: "pointer",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
}}
>
GitHub discussions
<ExternalLink
size={14}
strokeWidth={1.5}
color={colors.text.muted}
/>
</a>
</div>
</div>
);
};
export default HelpSection;
@@ -0,0 +1,125 @@
import { useEffect, useCallback } from "react";
import { X } from "lucide-react";
import { typography, spacing } from "./styles/theme";
const ImageLightbox = ({ src, alt, caption, onClose }) => {
// Close on escape key
const handleKeyDown = useCallback(
(e) => {
if (e.key === "Escape") {
onClose();
}
},
[onClose]
);
useEffect(() => {
document.addEventListener("keydown", handleKeyDown);
document.body.style.overflow = "hidden";
return () => {
document.removeEventListener("keydown", handleKeyDown);
document.body.style.overflow = "";
};
}, [handleKeyDown]);
// Close when clicking backdrop
const handleBackdropClick = (e) => {
if (e.target === e.currentTarget) {
onClose();
}
};
return (
<div
onClick={handleBackdropClick}
style={{
position: "fixed",
top: 0,
left: 0,
right: 0,
bottom: 0,
backgroundColor: "rgba(0, 0, 0, 0.9)",
display: "flex",
alignItems: "center",
justifyContent: "center",
zIndex: 2000,
padding: spacing.xl,
cursor: "zoom-out",
}}
>
{/* Close button */}
<button
onClick={onClose}
style={{
position: "absolute",
top: 16,
right: 16,
width: 40,
height: 40,
borderRadius: "50%",
backgroundColor: "rgba(255, 255, 255, 0.1)",
border: "none",
cursor: "pointer",
display: "flex",
alignItems: "center",
justifyContent: "center",
transition: "background-color 150ms ease",
}}
onMouseEnter={(e) => {
e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.2)";
}}
onMouseLeave={(e) => {
e.currentTarget.style.backgroundColor = "rgba(255, 255, 255, 0.1)";
}}
title="Close (Esc)"
>
<X
size={24}
strokeWidth={1.5}
color="#fff"
/>
</button>
{/* Image container */}
<div
style={{
maxWidth: "90vw",
maxHeight: "90vh",
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<img
src={src}
alt={alt}
style={{
maxWidth: "100%",
maxHeight: caption ? "calc(90vh - 40px)" : "90vh",
objectFit: "contain",
borderRadius: "4px",
boxShadow: "0 8px 32px rgba(0, 0, 0, 0.3)",
cursor: "default",
}}
onClick={(e) => e.stopPropagation()}
/>
{caption && (
<p
style={{
marginTop: spacing.md,
fontSize: typography.fontSize.sm,
color: "rgba(255, 255, 255, 0.8)",
textAlign: "center",
fontStyle: "italic",
}}
>
{caption}
</p>
)}
</div>
</div>
);
};
export default ImageLightbox;
@@ -0,0 +1,638 @@
# Checkmate User Guide Style Guide
This document defines the writing standards, formatting rules, and content patterns for all Checkmate user documentation.
---
## Voice & Tone
### Core Principles
1. **Second person, active voice** - Address the reader directly
- ✅ "You can configure monitors from the dashboard"
- ❌ "Monitors can be configured from the dashboard"
2. **Professional but approachable** - Clear and helpful, not stiff or overly casual
- ✅ "Select the notification channel you want to use"
- ❌ "Go ahead and pick whatever notification channel works for you!"
- ❌ "The user shall select the appropriate notification channel"
3. **Direct and instructional** - Tell users what to do, not what the system does
- ✅ "Click Save to apply your changes"
- ❌ "The Save button will apply your changes"
4. **User-focused** - Lead with what users can accomplish
- ✅ "You can track server performance with infrastructure monitors"
- ❌ "Infrastructure monitors use the Capture agent to collect metrics"
### Language Patterns
**Opening phrases:**
- "The [Feature] allows you to..."
- "[Feature] is..."
- "You can..."
- "To [action]:"
**Navigation instructions:**
- "Navigate to [location]"
- "Click [element]"
- "Select [option]"
- "From the [page], click..."
**Avoid:**
- Passive voice where active is clearer
- Jargon without explanation
- Assuming prior knowledge
- Marketing language ("powerful", "seamless", "robust")
---
## Content Structure
### Standard Article Pattern
Every article should follow this flow:
```
1. OVERVIEW (H2)
- 1-2 paragraphs explaining what and why
- Set context for the topic
2. VISUAL (Optional)
- Screenshot showing the feature
- Include after intro, not before
3. MAIN CONTENT (H2/H3 sections)
- Procedures with ordered lists
- Features with bullet lists
- Reference info in tables
4. TIPS & WARNINGS (Callouts)
- Best practices
- Common pitfalls
- Important restrictions
5. RELATED ARTICLES (Links)
- 2-4 related topics
- Logical next steps
```
### Heading Hierarchy
Use only two heading levels within articles:
- **H2** - Major sections (Overview, Configuration, Settings)
- **H3** - Subsections within H2s (Creating a monitor, Editing settings)
Never use H4 or deeper. If content requires more nesting, restructure into separate sections.
### Section Length
| Element | Target Length |
| ------------- | -------------- |
| Paragraphs | 2-4 sentences |
| H2 sections | 100-300 words |
| H3 sections | 50-150 words |
| Full articles | 800-1500 words |
---
## Formatting Rules
### Lists
**Bullet lists** - Use for features, options, characteristics (unordered items)
```javascript
{
type: 'bullet-list',
items: [
{ text: 'HTTP monitors check website availability' },
{ text: 'Ping monitors verify server reachability' },
{ text: 'Port monitors test specific service ports' }
]
}
```
**Ordered lists** - Use for step-by-step procedures (sequential actions)
```javascript
{
type: 'ordered-list',
items: [
{ text: 'Navigate to **Uptime** in the sidebar' },
{ text: 'Click **Create monitor**' },
{ text: 'Enter the URL you want to monitor' },
{ text: 'Click **Save** to create the monitor' }
]
}
```
**List guidelines:**
- Keep procedures to 4-6 steps maximum
- If longer, break into multiple procedures
- Start each item with an action verb for procedures
- Use parallel structure (all items same grammatical form)
### Callouts
Use callouts sparingly for important information that shouldn't be missed.
**Types:**
| Variant | Use For | Example |
| --------- | --------------------------------- | -------------------------------------------------------------------------- |
| `info` | Additional context, clarification | "Monitors check at the interval you specify, starting from creation time." |
| `tip` | Best practices, recommendations | "Use descriptive names to easily identify monitors in alerts." |
| `warning` | Restrictions, potential issues | "Deleting a monitor removes all associated check history." |
| `success` | Confirmations, positive outcomes | "Your monitor is now active and checking every 5 minutes." |
```javascript
{
type: 'callout',
variant: 'tip',
title: 'Best practice',
text: 'Group related monitors together for easier management.'
}
```
**Callout guidelines:**
- Maximum 2-3 callouts per article
- Keep callout text concise (1-2 sentences)
- Title is optional but helps scannability
- Don't use callouts for routine information
### Inline Formatting
**Bold** - Use for:
- UI element names: "Click **Save**"
- Key terms on first use: "**Uptime** measures availability over time"
- Emphasis in lists: "**Name** — The display name for your monitor"
**Code/monospace** - Use for:
- URLs: `https://example.com`
- File paths: `/api/v1/monitors`
- Technical values: `200`, `timeout`
- Commands: `npm run dev`
```javascript
// In paragraph text, use backticks in markdown:
{
text: "Enter the URL (e.g., `https://example.com`) in the field";
}
```
### Tables
Use tables for:
- Feature comparisons
- Configuration options
- Reference data with multiple attributes
```javascript
{
type: 'table',
columns: [
{ key: 'type', label: 'Monitor Type', width: '1fr' },
{ key: 'use', label: 'Best For', width: '2fr' }
],
rows: [
{ type: 'HTTP', use: 'Websites and APIs' },
{ type: 'Ping', use: 'Server availability' },
{ type: 'Port', use: 'Specific services (databases, mail servers)' }
]
}
```
---
## Content Types
### Block Types Reference
| Type | Purpose | When to Use |
| --------------- | ----------------- | ------------------------- |
| `heading` | Section titles | Start each major topic |
| `paragraph` | Body text | Explanations, context |
| `bullet-list` | Unordered items | Features, options |
| `ordered-list` | Sequential steps | Procedures |
| `checklist` | Checkable items | Requirements, checklists |
| `callout` | Important notices | Tips, warnings |
| `code` | Code blocks | Commands, config examples |
| `table` | Structured data | Comparisons, reference |
| `image` | Screenshots | UI demonstration |
| `icon-cards` | Feature grids | Capability overviews |
| `article-links` | Related content | End-of-article navigation |
### Icon Cards
Use for presenting 4-8 related features or concepts:
```javascript
{
type: 'icon-cards',
items: [
{
icon: 'Globe',
title: 'HTTP monitoring',
description: 'Check website availability and response times'
},
{
icon: 'Server',
title: 'Infrastructure',
description: 'Monitor CPU, memory, and disk usage'
}
]
}
```
### Article Links
Always end articles with related content:
```javascript
{
type: 'article-links',
title: 'Related articles',
items: [
{
collectionId: 'uptime-monitoring',
articleId: 'http-monitors',
title: 'Creating HTTP monitors',
description: 'Set up your first website monitor'
}
]
}
```
---
## Images & Screenshots
### When to Include Images
- After introducing a feature (not before)
- For complex UI that benefits from visual reference
- For multi-step workflows
- Skip for simple, text-describable actions
### Image Requirements
```javascript
{
type: 'image',
src: '/images/user-guide/monitor-create.png',
alt: 'Monitor creation form showing URL field, name field, and interval selector',
caption: 'The monitor creation form with required fields highlighted'
}
```
**Alt text guidelines:**
- Describe what the image shows (15-30 words)
- Include key UI elements visible
- Make it useful for screen readers
**Caption guidelines:**
- Explain the context or what to notice
- Keep to one sentence
- Optional but recommended
### Screenshot Standards
- Capture at consistent browser width (1200px recommended)
- Use light theme for screenshots
- Highlight relevant areas if needed
- Avoid personal data in screenshots
- Crop to show relevant UI only
---
## Writing Procedures
### Step-by-Step Instructions
**Format:**
```javascript
{
type: 'ordered-list',
items: [
{ text: 'Navigate to **Uptime** in the sidebar' },
{ text: 'Click **Create monitor** in the top right' },
{ text: 'Enter the URL you want to monitor' },
{ text: 'Set the check interval (default: 5 minutes)' },
{ text: 'Click **Save** to create the monitor' }
]
}
```
**Guidelines:**
- Start each step with an action verb (Click, Enter, Select, Navigate)
- One action per step
- Include the location of UI elements
- Bold all clickable elements
- Add context in parentheses when helpful
### Describing Features
**Format:**
```javascript
{
type: 'bullet-list',
items: [
{ bold: 'Name', text: 'A descriptive label for your monitor' },
{ bold: 'URL', text: 'The address to check (must include protocol)' },
{ bold: 'Interval', text: 'How often to perform checks (1-60 minutes)' }
]
}
```
---
## Terminology
### Consistent Terms
Use these terms consistently throughout documentation:
| Term | Use | Don't Use |
| ------------ | ------------------------- | --------------------- |
| Monitor | The configured check | Watcher, probe, check |
| Check | A single monitoring event | Ping, request, poll |
| Incident | A detected problem | Outage, alert, issue |
| Notification | Alert sent to user | Message, email, ping |
| Dashboard | Main overview page | Home, overview |
| Sidebar | Left navigation | Menu, nav |
### Feature Names
Always match UI exactly:
- **Uptime** (not "Uptime Monitoring")
- **PageSpeed** (not "Page Speed" or "Pagespeed")
- **Infrastructure** (not "Server Monitoring")
- **Status pages** (not "Status Pages" - lowercase 'p')
---
## Article Templates
### Getting Started Article
```javascript
{
blocks: [
{
type: "heading",
level: 2,
text: "Overview",
id: "overview",
},
{
type: "paragraph",
text: "[1-2 sentences: What this feature/concept is]",
},
{
type: "paragraph",
text: "[1-2 sentences: Why it matters to users]",
},
{
type: "heading",
level: 2,
text: "[Main topic]",
id: "main-topic",
},
// ... content blocks
{
type: "article-links",
title: "Next steps",
items: [
// 2-3 logical next articles
],
},
];
}
```
### How-To Article
```javascript
{
blocks: [
{
type: "heading",
level: 2,
text: "Overview",
id: "overview",
},
{
type: "paragraph",
text: "[Brief description of what user will accomplish]",
},
{
type: "heading",
level: 2,
text: "[Action: Creating/Configuring/Setting up...]",
id: "action",
},
{
type: "ordered-list",
items: [
// 4-6 steps
],
},
{
type: "callout",
variant: "tip",
text: "[Best practice or helpful hint]",
},
{
type: "article-links",
title: "Related articles",
items: [],
},
];
}
```
### Reference Article
```javascript
{
blocks: [
{
type: 'heading',
level: 2,
text: 'Overview',
id: 'overview'
},
{
type: 'paragraph',
text: '[What this reference covers]'
},
{
type: 'heading',
level: 2,
text: '[Category 1]',
id: 'category-1'
},
{
type: 'table',
columns: [...],
rows: [...]
},
// More categories as needed
{
type: 'article-links',
title: 'Related articles',
items: []
}
]
}
```
---
## Quality Checklist
Before publishing any article, verify:
### Content
- [ ] Overview explains what AND why
- [ ] Procedures are tested and accurate
- [ ] All UI element names match exactly
- [ ] No assumptions about user knowledge
- [ ] Related articles are linked
### Formatting
- [ ] Only H2 and H3 headings used
- [ ] All headings have unique IDs
- [ ] Lists use correct type (bullet vs ordered)
- [ ] Callouts are used sparingly (max 3)
- [ ] Bold used for UI elements
### Style
- [ ] Active voice throughout
- [ ] Second person ("you")
- [ ] Action verbs start procedure steps
- [ ] Consistent terminology
- [ ] No marketing language
### Technical
- [ ] All links work
- [ ] Images have alt text
- [ ] Code examples are correct
- [ ] No placeholder content
---
## Available Icons
For icon-cards and other icon references, use these Lucide icon names:
**Navigation:**
- `Globe` - Uptime/HTTP
- `Gauge` - PageSpeed
- `Server` - Infrastructure
- `Bell` - Notifications
- `AlertTriangle` - Incidents
- `Wifi` - Status pages
- `Wrench` - Maintenance
- `Settings` - Settings
- `Users` - Team
**Actions:**
- `CheckCircle` - Success/complete
- `Clock` - Time/scheduling
- `Info` - Information
- `Lightbulb` - Tips
- `ArrowRight` - Navigation
**Features:**
- `Database` - Data/storage
- `Terminal` - Commands
- `FileText` - Documentation
- `Shield` - Security
- `Rocket` - Getting started
---
## File Structure
Articles are stored in `/content/index.js`:
```javascript
export const articleContents = {
"collection-id": {
"article-id": {
blocks: [
// Content blocks
],
},
},
};
```
Article metadata is in `/content/userGuideConfig.js`:
```javascript
{
id: 'article-id',
title: 'Article Title',
description: 'Brief description for search and previews',
keywords: ['search', 'terms', 'for', 'this', 'article']
}
```
---
## Examples
### Good Paragraph
> "HTTP monitors check your websites and APIs at regular intervals. When a check fails—due to a timeout, error response, or connection issue—Checkmate creates an incident and sends notifications to your configured channels."
### Good Procedure
> 1. Navigate to **Uptime** in the sidebar
> 2. Click **Create monitor** in the top right
> 3. Enter the URL you want to monitor (include `https://`)
> 4. Set your preferred check interval
> 5. Click **Save** to create the monitor
### Good Callout
> **Tip:** Use descriptive monitor names like "Production API" or "Marketing Website" to quickly identify issues in notifications.
### Good Feature List
> - **Name** — A descriptive label for easy identification
> - **URL** — The address to monitor (must include protocol)
> - **Interval** — How often to check (1-60 minutes)
> - **Timeout** — How long to wait before marking as failed
---
_Last updated: January 2025_
@@ -0,0 +1,12 @@
/* SearchResults styles (Dark theme) */
.user-guide-search-result-item {
transition:
background-color 150ms ease,
border-color 150ms ease;
}
.user-guide-search-result-item:hover {
background-color: rgba(255, 255, 255, 0.05) !important;
border-color: #4a5568 !important;
}
@@ -0,0 +1,173 @@
import { FileText, ArrowRight } from "lucide-react";
import { searchArticles } from "./content/userGuideConfig";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./SearchResults.css";
const SearchResults = ({ query, onNavigate, onClearSearch }) => {
const { colors, typography, spacing, border } = useUserGuideTheme();
const results = searchArticles(query);
const handleResultClick = (result) => {
onNavigate(result.collectionId, result.articleId);
onClearSearch();
};
if (query.trim().length < 2) {
return (
<div style={{ padding: spacing.lg }}>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.muted,
textAlign: "center",
margin: 0,
}}
>
Type at least 2 characters to search...
</p>
</div>
);
}
if (results.length === 0) {
return (
<div style={{ padding: spacing.xl }}>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.primary,
textAlign: "center",
marginBottom: spacing.sm,
marginTop: 0,
}}
>
No results found for "{query}"
</p>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.muted,
textAlign: "center",
margin: 0,
}}
>
Try different keywords or browse by topic
</p>
</div>
);
}
return (
<div style={{ padding: spacing.lg }}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginBottom: spacing.md,
}}
>
<span
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.muted,
}}
>
{results.length} result{results.length !== 1 ? "s" : ""} for "{query}"
</span>
<button
onClick={onClearSearch}
style={{
background: "none",
border: "none",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.brand.primary,
cursor: "pointer",
padding: 0,
}}
>
Clear search
</button>
</div>
<div style={{ display: "flex", flexDirection: "column", gap: spacing.sm }}>
{results.map((result) => (
<div
key={`${result.collectionId}-${result.articleId}`}
onClick={() => handleResultClick(result)}
className="user-guide-search-result-item"
style={{
display: "flex",
alignItems: "flex-start",
gap: spacing.md,
padding: spacing.md,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
cursor: "pointer",
}}
>
<FileText
size={16}
strokeWidth={1.5}
color={colors.text.muted}
style={{ flexShrink: 0, marginTop: 2 }}
/>
<div style={{ flex: 1, minWidth: 0 }}>
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
marginBottom: 2,
}}
>
{result.articleTitle}
</span>
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
}}
>
{result.articleDescription}
</span>
<span
style={{
display: "block",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.xs,
color: colors.text.muted,
marginTop: spacing.xs,
}}
>
in {result.collectionTitle}
</span>
</div>
<ArrowRight
size={16}
strokeWidth={1.5}
color={colors.text.muted}
style={{ flexShrink: 0, marginTop: 2 }}
/>
</div>
))}
</div>
</div>
);
};
export default SearchResults;
@@ -0,0 +1,40 @@
/* SidebarHeader styles - Dark theme */
.user-guide-header-icon-button {
transition: all 150ms ease;
}
.user-guide-header-icon-button:hover {
background-color: rgba(255, 255, 255, 0.1) !important;
color: #1570ef !important;
}
.user-guide-header-icon-button.active {
background-color: #0d0d0d;
color: #1570ef;
}
.user-guide-breadcrumb-dropdown-trigger {
transition: background-color 150ms ease;
}
.user-guide-breadcrumb-dropdown-trigger:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-breadcrumb-item {
transition: background-color 150ms ease;
}
.user-guide-breadcrumb-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-search-input:focus {
outline: none;
border-color: #1570ef;
}
.user-guide-search-input::placeholder {
color: #667085;
}
@@ -0,0 +1,399 @@
import { useState, useRef, useEffect } from "react";
import {
Home,
ChevronDown,
ChevronLeft,
ChevronRight,
Search,
ExternalLink,
X,
} from "lucide-react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./SidebarHeader.css";
const SidebarHeader = ({
breadcrumbs,
onHomeClick,
onBack,
onForward,
canGoBack,
canGoForward,
onClose,
onOpenInNewTab,
isSearchOpen,
onSearchToggle,
searchQuery,
onSearchChange,
showOpenInNewTab = true,
showNavigation = true,
}) => {
const { colors, typography, spacing, border, isDark } = useUserGuideTheme();
const [isDropdownOpen, setIsDropdownOpen] = useState(false);
const dropdownRef = useRef(null);
const searchInputRef = useRef(null);
// Close dropdown when clicking outside
useEffect(() => {
const handleClickOutside = (event) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target)) {
setIsDropdownOpen(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// Focus search input when opened
useEffect(() => {
if (isSearchOpen && searchInputRef.current) {
searchInputRef.current.focus();
}
}, [isSearchOpen]);
// Get current breadcrumb (last item)
const currentBreadcrumb = breadcrumbs[breadcrumbs.length - 1];
return (
<div
style={{
backgroundColor: colors.background.white,
borderBottom: border.default,
padding: `${spacing.sm} ${spacing.md}`,
}}
>
{/* Main header row */}
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: spacing.sm,
}}
>
{/* Left side: Navigation + Breadcrumb dropdown */}
<div
style={{
display: "flex",
alignItems: "center",
gap: spacing.xs,
flex: 1,
minWidth: 0,
}}
>
{/* Navigation buttons - only shown when showNavigation is true */}
{showNavigation && (
<>
{/* Back button */}
<button
onClick={onBack}
disabled={!canGoBack}
className="user-guide-header-icon-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: canGoBack ? "pointer" : "default",
color: canGoBack ? colors.text.secondary : colors.border.default,
flexShrink: 0,
opacity: canGoBack ? 1 : 0.5,
}}
title="Back"
>
<ChevronLeft
size={16}
strokeWidth={1.5}
/>
</button>
{/* Forward button */}
<button
onClick={onForward}
disabled={!canGoForward}
className="user-guide-header-icon-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: canGoForward ? "pointer" : "default",
color: canGoForward ? colors.text.secondary : colors.border.default,
flexShrink: 0,
opacity: canGoForward ? 1 : 0.5,
}}
title="Forward"
>
<ChevronRight
size={16}
strokeWidth={1.5}
/>
</button>
{/* Home button */}
<button
onClick={breadcrumbs.length > 1 ? onHomeClick : undefined}
disabled={breadcrumbs.length <= 1}
className="user-guide-header-icon-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: breadcrumbs.length > 1 ? "pointer" : "default",
color:
breadcrumbs.length > 1
? colors.text.secondary
: colors.border.default,
flexShrink: 0,
opacity: breadcrumbs.length > 1 ? 1 : 0.5,
}}
title="Home"
>
<Home
size={16}
strokeWidth={1.5}
/>
</button>
</>
)}
{/* Breadcrumb dropdown */}
<div
ref={dropdownRef}
style={{ position: "relative", flex: 1, minWidth: 0 }}
>
<button
onClick={() => breadcrumbs.length > 1 && setIsDropdownOpen(!isDropdownOpen)}
className="user-guide-breadcrumb-dropdown-trigger"
style={{
display: "flex",
alignItems: "center",
gap: 4,
padding: "4px 8px",
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: breadcrumbs.length > 1 ? "pointer" : "default",
maxWidth: "100%",
}}
>
<span
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.medium,
color: colors.text.primary,
whiteSpace: "nowrap",
overflow: "hidden",
textOverflow: "ellipsis",
}}
>
{currentBreadcrumb?.label || "User guide"}
</span>
{breadcrumbs.length > 1 && (
<ChevronDown
size={14}
strokeWidth={1.5}
color={colors.text.muted}
style={{
flexShrink: 0,
transform: isDropdownOpen ? "rotate(180deg)" : "rotate(0)",
transition: "transform 150ms ease",
}}
/>
)}
</button>
{/* Dropdown menu */}
{isDropdownOpen && breadcrumbs.length > 1 && (
<div
className="user-guide-breadcrumb-dropdown"
style={{
position: "absolute",
top: "100%",
left: 0,
marginTop: 4,
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
boxShadow: isDark
? "0 4px 12px rgba(0, 0, 0, 0.3)"
: "0 4px 12px rgba(0, 0, 0, 0.1)",
minWidth: 180,
maxWidth: 280,
zIndex: 100,
overflow: "hidden",
}}
>
{breadcrumbs.slice(0, -1).map((item, index) => (
<button
key={index}
onClick={() => {
item.onClick();
setIsDropdownOpen(false);
}}
className="user-guide-breadcrumb-item"
style={{
display: "block",
width: "100%",
padding: "10px 14px",
backgroundColor: "transparent",
border: "none",
textAlign: "left",
cursor: "pointer",
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.primary,
}}
>
{item.label}
</button>
))}
</div>
)}
</div>
</div>
{/* Right side: Actions */}
<div
style={{
display: "flex",
alignItems: "center",
gap: spacing.xs,
flexShrink: 0,
}}
>
{/* Open in new tab - only shown when showOpenInNewTab is true */}
{showOpenInNewTab && (
<button
onClick={onOpenInNewTab}
className="user-guide-header-icon-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: "pointer",
color: colors.text.secondary,
}}
title="Open in new tab"
>
<ExternalLink
size={16}
strokeWidth={1.5}
/>
</button>
)}
{/* Search toggle */}
<button
onClick={onSearchToggle}
className={`user-guide-header-icon-button ${isSearchOpen ? "active" : ""}`}
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: isSearchOpen ? colors.background.alt : "transparent",
border: "none",
borderRadius: border.radius,
cursor: "pointer",
color: isSearchOpen ? colors.brand.primary : colors.text.secondary,
}}
title="Search"
>
<Search
size={16}
strokeWidth={1.5}
/>
</button>
{/* Close button */}
<button
onClick={onClose}
className="user-guide-header-icon-button"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
width: 28,
height: 28,
backgroundColor: "transparent",
border: "none",
borderRadius: border.radius,
cursor: "pointer",
color: colors.text.secondary,
}}
title="Close"
>
<X
size={16}
strokeWidth={1.5}
/>
</button>
</div>
</div>
{/* Search input (collapsible) */}
{isSearchOpen && (
<div style={{ marginTop: spacing.sm }}>
<div style={{ position: "relative" }}>
<Search
size={14}
strokeWidth={1.5}
color={colors.text.muted}
style={{
position: "absolute",
left: 10,
top: "50%",
transform: "translateY(-50%)",
pointerEvents: "none",
}}
/>
<input
ref={searchInputRef}
type="text"
placeholder="Search articles..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
className="user-guide-search-input"
style={{
width: "100%",
height: 32,
fontSize: typography.fontSize.sm,
fontFamily: typography.fontFamily.sans,
backgroundColor: colors.background.alt,
borderRadius: border.radius,
border: border.default,
paddingLeft: 32,
paddingRight: 12,
boxSizing: "border-box",
color: colors.text.primary,
}}
/>
</div>
</div>
)}
</div>
);
};
export default SidebarHeader;
@@ -0,0 +1,11 @@
/* SidebarWrapper styles (Dark theme) */
.user-guide-sidebar-container {
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.3);
}
/* Push main content when sidebar is open */
body.user-guide-sidebar-open .home-content-wrapper {
margin-right: var(--help-sidebar-padding, 440px);
transition: margin-right 300ms ease-in-out;
}
@@ -0,0 +1,485 @@
import { useState, useEffect, useCallback, useRef } from "react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import TabBar from "./TabBar";
import SidebarHeader from "./SidebarHeader";
import UserGuideLanding from "./UserGuideLanding";
import CollectionPage from "./CollectionPage";
import ArticlePage from "./ArticlePage";
import ContentRenderer from "./ContentRenderer";
import HelpSection from "./HelpSection";
import SearchResults from "./SearchResults";
import { getCollection, getArticle } from "./content/userGuideConfig";
import { getArticleContent } from "./content";
import { extractToc } from "./content/contentTypes";
import {
useUserGuideSidebarContext,
DEFAULT_CONTENT_WIDTH,
} from "./UserGuideSidebarContext";
import "./SidebarWrapper.css";
const STORAGE_KEY = "checkmate-sidebar-state";
const MIN_CONTENT_WIDTH = DEFAULT_CONTENT_WIDTH;
const MAX_CONTENT_WIDTH = DEFAULT_CONTENT_WIDTH * 2; // 100% wider
const SidebarWrapper = ({ isOpen, onClose, onOpen, initialPath, onOpenInNewTab }) => {
const { setContentWidth: setContextContentWidth } = useUserGuideSidebarContext();
const { colors, border } = useUserGuideTheme();
const [activeTab, setActiveTab] = useState("user-guide");
const [collectionId, setCollectionId] = useState(undefined);
const [articleId, setArticleId] = useState(undefined);
const [searchQuery, setSearchQuery] = useState("");
const [isSearchOpen, setIsSearchOpen] = useState(false);
const [contentWidth, setContentWidthLocal] = useState(DEFAULT_CONTENT_WIDTH);
const [isResizing, setIsResizing] = useState(false);
const [isHoveringHandle, setIsHoveringHandle] = useState(false);
const [mouseY, setMouseY] = useState(0);
const resizeRef = useRef(null);
const handleRef = useRef(null);
// Sync content width with context whenever it changes
const setContentWidth = useCallback(
(width) => {
setContentWidthLocal(width);
setContextContentWidth(width);
},
[setContextContentWidth]
);
// Initialize context with current width on mount
useEffect(() => {
setContextContentWidth(DEFAULT_CONTENT_WIDTH);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); // Only run once on mount
// Navigation history
const [history, setHistory] = useState([
{ collectionId: undefined, articleId: undefined },
]);
const [historyIndex, setHistoryIndex] = useState(0);
const isNavigatingRef = useRef(false);
// Parse initial path on mount
useEffect(() => {
if (initialPath) {
const parts = initialPath.split("/");
if (parts.length >= 1 && parts[0]) {
setCollectionId(parts[0]);
if (parts.length >= 2 && parts[1]) {
setArticleId(parts[1]);
}
}
}
}, [initialPath]);
// Persist sidebar state to localStorage
useEffect(() => {
if (isOpen) {
const state = { activeTab, collectionId, articleId };
localStorage.setItem(STORAGE_KEY, JSON.stringify(state));
}
}, [isOpen, activeTab, collectionId, articleId]);
// Restore state from localStorage on mount
useEffect(() => {
const savedState = localStorage.getItem(STORAGE_KEY);
if (savedState && !initialPath) {
try {
const {
activeTab: savedTab,
collectionId: savedCollection,
articleId: savedArticle,
} = JSON.parse(savedState);
if (savedTab) setActiveTab(savedTab);
if (savedCollection) setCollectionId(savedCollection);
if (savedArticle) setArticleId(savedArticle);
} catch (e) {
// Ignore parse errors
}
}
}, [initialPath]);
// Track navigation changes in history
useEffect(() => {
if (isNavigatingRef.current) {
isNavigatingRef.current = false;
return;
}
// Only track if this is a new navigation (not from back/forward)
const currentEntry = history[historyIndex];
if (
currentEntry?.collectionId !== collectionId ||
currentEntry?.articleId !== articleId
) {
// Truncate forward history and add new entry
const newHistory = [
...history.slice(0, historyIndex + 1),
{ collectionId, articleId },
];
setHistory(newHistory);
setHistoryIndex(newHistory.length - 1);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [collectionId, articleId]);
// Navigation handlers
const handleNavigate = useCallback((newCollectionId, newArticleId) => {
setCollectionId(newCollectionId);
setArticleId(newArticleId);
}, []);
const handleBackToHome = useCallback(() => {
setCollectionId(undefined);
setArticleId(undefined);
}, []);
// Back/Forward navigation
const canGoBack = historyIndex > 0;
const canGoForward = historyIndex < history.length - 1;
const handleBack = useCallback(() => {
if (historyIndex > 0) {
isNavigatingRef.current = true;
const prevIndex = historyIndex - 1;
const prevEntry = history[prevIndex];
setHistoryIndex(prevIndex);
setCollectionId(prevEntry.collectionId);
setArticleId(prevEntry.articleId);
}
}, [historyIndex, history]);
const handleForward = useCallback(() => {
if (historyIndex < history.length - 1) {
isNavigatingRef.current = true;
const nextIndex = historyIndex + 1;
const nextEntry = history[nextIndex];
setHistoryIndex(nextIndex);
setCollectionId(nextEntry.collectionId);
setArticleId(nextEntry.articleId);
}
}, [historyIndex, history]);
const handleBackToCollection = useCallback(() => {
setArticleId(undefined);
}, []);
const handleHomeClick = useCallback(() => {
setCollectionId(undefined);
setArticleId(undefined);
}, []);
// Get current collection and article
const collection = collectionId ? getCollection(collectionId) : undefined;
const article =
collectionId && articleId ? getArticle(collectionId, articleId) : undefined;
// Get prev/next articles for navigation
const getAdjacentArticles = () => {
if (!collection || !articleId) return { prev: undefined, next: undefined };
const currentIndex = collection.articles.findIndex((a) => a.id === articleId);
return {
prev: currentIndex > 0 ? collection.articles[currentIndex - 1] : undefined,
next:
currentIndex < collection.articles.length - 1
? collection.articles[currentIndex + 1]
: undefined,
};
};
const { prev: prevArticle, next: nextArticle } = getAdjacentArticles();
// Get article content
const articleContent =
collectionId && articleId ? getArticleContent(collectionId, articleId) : undefined;
// Auto-generate TOC from content headings
const tocItems = articleContent ? extractToc(articleContent.blocks) : undefined;
// Build breadcrumb items for dropdown
const buildBreadcrumbs = () => {
let items = [];
switch (activeTab) {
case "user-guide":
items = [{ label: "User guide", onClick: handleHomeClick }];
if (collection) {
items.push({ label: collection.title, onClick: () => setArticleId(undefined) });
}
if (article) {
items.push({ label: article.title, onClick: () => {} });
}
return items;
default:
return [{ label: "Help", onClick: () => {} }];
}
};
// Handle "Open in new tab"
const handleOpenInNewTab = () => {
let path = "https://bluewavelabs.gitbook.io/checkmate";
if (onOpenInNewTab) {
onOpenInNewTab();
} else {
window.open(path, "_blank", "noopener,noreferrer");
}
};
// Clear search and close search bar
const handleClearSearch = useCallback(() => {
setSearchQuery("");
setIsSearchOpen(false);
}, []);
// Render User Guide content
const renderUserGuideContent = () => {
// Show search results when search is active
if (isSearchOpen && searchQuery.trim().length > 0) {
return (
<SearchResults
query={searchQuery}
onNavigate={(colId, artId) => {
handleNavigate(colId, artId);
handleClearSearch();
}}
onClearSearch={handleClearSearch}
/>
);
}
if (collection && article) {
return (
<ArticlePage
collection={collection}
article={article}
onBack={handleBackToCollection}
onBackToHome={handleBackToHome}
prevArticle={prevArticle}
nextArticle={nextArticle}
onPrevArticle={
prevArticle ? () => handleNavigate(collectionId, prevArticle.id) : undefined
}
onNextArticle={
nextArticle ? () => handleNavigate(collectionId, nextArticle.id) : undefined
}
tocItems={tocItems}
mode="in-app"
>
{articleContent && (
<ContentRenderer
content={articleContent}
onNavigate={handleNavigate}
/>
)}
</ArticlePage>
);
}
if (collection) {
return (
<CollectionPage
collection={collection}
onArticleClick={(id) => handleNavigate(collectionId, id)}
/>
);
}
return (
<UserGuideLanding
onNavigate={handleNavigate}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
mode="in-app"
/>
);
};
const contentArea = (tabValue) => {
switch (tabValue) {
case "user-guide":
return renderUserGuideContent();
default:
return <HelpSection />;
}
};
// Handle tab click - open sidebar if closed, close if clicking active tab, or switch tab
const handleTabClick = (tab) => {
if (!isOpen) {
onOpen();
setActiveTab(tab);
} else if (activeTab === tab) {
// Clicking the already-active tab closes the sidebar
onClose();
} else {
// Clicking a different tab switches to it
setActiveTab(tab);
}
};
// Resize handlers
const handleResizeStart = useCallback(
(e) => {
e.preventDefault();
setIsResizing(true);
resizeRef.current = { startX: e.clientX, startWidth: contentWidth };
},
[contentWidth]
);
const handleResizeMove = useCallback(
(e) => {
if (!isResizing || !resizeRef.current) return;
const deltaX = resizeRef.current.startX - e.clientX;
const newWidth = Math.min(
MAX_CONTENT_WIDTH,
Math.max(MIN_CONTENT_WIDTH, resizeRef.current.startWidth + deltaX)
);
setContentWidth(newWidth);
},
[isResizing, setContentWidth]
);
const handleResizeEnd = useCallback(() => {
setIsResizing(false);
setIsHoveringHandle(false);
resizeRef.current = null;
}, []);
// Add/remove resize event listeners
useEffect(() => {
if (isResizing) {
document.addEventListener("mousemove", handleResizeMove);
document.addEventListener("mouseup", handleResizeEnd);
document.body.style.cursor = "ew-resize";
document.body.style.userSelect = "none";
}
return () => {
document.removeEventListener("mousemove", handleResizeMove);
document.removeEventListener("mouseup", handleResizeEnd);
document.body.style.cursor = "";
document.body.style.userSelect = "";
};
}, [isResizing, handleResizeMove, handleResizeEnd]);
return (
<div
className="user-guide-sidebar-container"
style={{
position: "fixed",
top: 0,
right: 0,
height: "100vh",
display: "flex",
zIndex: 1100,
}}
>
{/* Resize Handle - positioned absolutely on the left edge */}
<div
ref={handleRef}
onMouseDown={handleResizeStart}
onMouseEnter={() => setIsHoveringHandle(true)}
onMouseLeave={() => !isResizing && setIsHoveringHandle(false)}
onMouseMove={(e) => {
if (handleRef.current) {
const rect = handleRef.current.getBoundingClientRect();
setMouseY(e.clientY - rect.top);
}
}}
style={{
position: "absolute",
left: 0,
top: 0,
width: 8,
height: "100%",
cursor: "ew-resize",
zIndex: 20,
display: "flex",
alignItems: "center",
justifyContent: "flex-start",
}}
>
{/* Visual indicator line */}
<div
style={{
width: isResizing ? 2 : 1,
height: "100%",
backgroundColor:
isResizing || isHoveringHandle ? "#4CAF93" : colors.border.default,
transition: isResizing ? "none" : "all 150ms ease",
position: "relative",
}}
>
{/* Grab indicator follows mouse */}
{(isHoveringHandle || isResizing) && (
<div
style={{
position: "absolute",
left: -1,
top: mouseY - 10,
width: 3,
height: 20,
backgroundColor: "#555",
borderRadius: 1,
}}
/>
)}
</div>
</div>
{/* Tab Bar - Always visible */}
<TabBar
activeTab={isOpen ? activeTab : undefined}
onTabChange={handleTabClick}
/>
{/* Main Sidebar Content - Slides in/out */}
<div
style={{
width: isOpen ? contentWidth : 0,
height: "100%",
backgroundColor: colors.background.white,
borderLeft: isOpen ? border.default : "none",
display: "flex",
flexDirection: "column",
overflow: "hidden",
transition: isResizing ? "none" : "width 300ms ease-in-out",
position: "relative",
}}
>
{isOpen && (
<>
{/* Header */}
<SidebarHeader
showOpenInNewTab={activeTab === "user-guide"}
showNavigation={activeTab === "user-guide"}
breadcrumbs={buildBreadcrumbs()}
onHomeClick={handleHomeClick}
onBack={handleBack}
onForward={handleForward}
canGoBack={canGoBack}
canGoForward={canGoForward}
onClose={onClose}
onOpenInNewTab={handleOpenInNewTab}
isSearchOpen={isSearchOpen}
onSearchToggle={() => setIsSearchOpen(!isSearchOpen)}
searchQuery={searchQuery}
onSearchChange={setSearchQuery}
/>
{/* Content Area */}
<div
style={{
flex: 1,
overflowY: "auto",
backgroundColor: colors.background.alt,
}}
>
{contentArea(activeTab)}
</div>
</>
)}
</div>
</div>
);
};
export default SidebarWrapper;
@@ -0,0 +1,13 @@
/* TabBar styles */
.user-guide-tab-item {
transition: all 150ms ease;
}
.user-guide-tab-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-tab-item.active:hover {
background-color: #1a1a1a;
}
@@ -0,0 +1,96 @@
import { BookOpen, HelpCircle } from "lucide-react";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./TabBar.css";
const TabItem = ({ label, icon, isActive, onClick, colors, typography }) => {
return (
<div
onClick={onClick}
className={`user-guide-tab-item ${isActive ? "active" : ""}`}
style={{
display: "flex",
flexDirection: "column",
alignItems: "center",
justifyContent: "center",
padding: "16px 8px",
cursor: "pointer",
backgroundColor: isActive ? colors.background.white : "transparent",
borderLeft: isActive
? `2px solid ${colors.brand.primary}`
: "2px solid transparent",
borderBottom: `1px solid ${colors.border.default}`,
}}
>
<div
style={{
color: isActive ? colors.brand.primary : colors.text.muted,
marginBottom: 8,
}}
>
{icon}
</div>
<span
style={{
writingMode: "vertical-rl",
textOrientation: "mixed",
fontFamily: typography.fontFamily.sans,
fontSize: 12,
fontWeight: typography.fontWeight.normal,
color: isActive ? colors.brand.primary : colors.text.muted,
letterSpacing: "0.5px",
}}
>
{label}
</span>
</div>
);
};
const TabBar = ({ activeTab, onTabChange }) => {
const { colors, typography } = useUserGuideTheme();
return (
<div
style={{
width: 40,
height: "100%",
backgroundColor: colors.background.sidebar,
borderLeft: `1px solid ${colors.border.default}`,
display: "flex",
flexDirection: "column",
alignItems: "stretch",
}}
>
<TabItem
id="user-guide"
label="User guide"
icon={
<BookOpen
size={18}
strokeWidth={1.5}
/>
}
isActive={activeTab === "user-guide"}
onClick={() => onTabChange("user-guide")}
colors={colors}
typography={typography}
/>
<TabItem
id="help"
label="Help"
icon={
<HelpCircle
size={18}
strokeWidth={1.5}
/>
}
isActive={activeTab === "help"}
onClick={() => onTabChange("help")}
colors={colors}
typography={typography}
/>
</div>
);
};
export default TabBar;
@@ -0,0 +1,39 @@
/* UserGuideLanding styles - hover states */
/* Collection card hover */
.user-guide-collection-card {
transition: all 150ms ease;
}
.user-guide-collection-card:hover {
border-color: rgba(21, 112, 239, 0.3) !important;
background-color: rgba(21, 112, 239, 0.02) !important;
box-shadow: 0px 2px 8px -2px rgba(0, 0, 0, 0.08);
}
[data-mui-color-scheme="dark"] .user-guide-collection-card:hover {
border-color: rgba(21, 112, 239, 0.5) !important;
background-color: rgba(21, 112, 239, 0.05) !important;
box-shadow: 0px 4px 16px -4px rgba(0, 0, 0, 0.4);
}
/* Fast find item hover */
.user-guide-fast-find-item {
transition: all 150ms ease;
}
.user-guide-fast-find-item:hover {
background-color: rgba(0, 0, 0, 0.04);
}
[data-mui-color-scheme="dark"] .user-guide-fast-find-item:hover {
background-color: rgba(255, 255, 255, 0.05);
}
.user-guide-fast-find-item .arrow-icon {
transition: all 150ms ease;
}
.user-guide-fast-find-item:hover .arrow-icon {
color: #1570ef;
}
@@ -0,0 +1,214 @@
import {
ArrowRight,
Rocket,
Globe,
Gauge,
Server,
Bell,
AlertTriangle,
Settings,
Wifi,
Wrench,
Users,
} from "lucide-react";
import { collections, fastFinds, getCollection } from "./content/userGuideConfig";
import { useUserGuideTheme } from "./styles/useUserGuideTheme";
import "./UserGuideLanding.css";
// Map icon names to actual Lucide components
const iconMap = {
Rocket,
Globe,
Gauge,
Server,
Bell,
AlertTriangle,
Settings,
Wifi,
Wrench,
Users,
};
const UserGuideLanding = ({
onNavigate,
searchQuery,
onSearchChange,
mode = "in-app",
}) => {
const { colors, typography, spacing, border } = useUserGuideTheme();
const isInApp = mode === "in-app";
return (
<div style={{ minHeight: "100%", backgroundColor: colors.background.alt }}>
{/* Main Content */}
<div
style={{
maxWidth: isInApp ? "100%" : 1200,
margin: "0 auto",
padding: isInApp ? spacing.lg : `${spacing["3xl"]} ${spacing["3xl"]}`,
}}
>
{/* Collections Grid */}
<h2
style={{
fontFamily: typography.fontFamily.sans,
fontSize: isInApp ? typography.fontSize.lg : typography.fontSize.xl,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: isInApp ? spacing.md : spacing.xl,
marginTop: 0,
}}
>
Browse by topic
</h2>
<div
style={{
display: "grid",
gridTemplateColumns: isInApp
? "1fr"
: "repeat(auto-fill, minmax(340px, 1fr))",
gap: isInApp ? spacing.md : spacing.lg,
marginBottom: isInApp ? spacing.xl : spacing["4xl"],
}}
>
{collections.map((collection) => {
const IconComponent = iconMap[collection.icon] || Rocket;
return (
<div
key={collection.id}
onClick={() => onNavigate(collection.id)}
className="user-guide-collection-card"
style={{
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
padding: spacing.lg,
cursor: "pointer",
}}
>
<div
style={{ display: "flex", alignItems: "flex-start", gap: spacing.md }}
>
<IconComponent
size={20}
strokeWidth={1.5}
color={colors.brand.primary}
/>
<div style={{ flex: 1 }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
marginBottom: spacing.xs,
}}
>
<span
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
}}
>
{collection.title}
</span>
<span
style={{
fontSize: typography.fontSize.xs,
color: colors.text.secondary,
backgroundColor: colors.background.alt,
padding: "2px 6px",
borderRadius: border.radius,
}}
>
{collection.articleCount} articles
</span>
</div>
<span
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
display: "block",
}}
>
{collection.description}
</span>
</div>
</div>
</div>
);
})}
</div>
{/* About our docs - Only shown in in-app mode */}
{isInApp && (
<div
style={{
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
padding: spacing.lg,
}}
>
<h3
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.md,
marginTop: 0,
}}
>
About our docs
</h3>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
margin: 0,
marginBottom: spacing.md,
}}
>
There are a few ways to explore our docs:
</p>
<div style={{ marginBottom: spacing.md }}>
<h4
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
marginBottom: spacing.xs,
marginTop: 0,
}}
>
In the product
</h4>
<p
style={{
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.sm,
color: colors.text.secondary,
lineHeight: typography.lineHeight.normal,
margin: 0,
}}
>
Look for help icons and tooltips throughout the interface.
</p>
</div>
</div>
)}
</div>
</div>
);
};
export default UserGuideLanding;
@@ -0,0 +1,102 @@
import { createContext, useContext, useState, useCallback, useEffect } from "react";
const SIDEBAR_STATE_KEY = "checkmate-sidebar-open";
// Constants for sidebar dimensions
export const TAB_BAR_WIDTH = 41; // 40px width + 1px border
export const DEFAULT_CONTENT_WIDTH = 400;
// Gap when sidebar is closed (need more space since tab bar is narrow)
export const CLOSED_GAP = 24;
// Gap when sidebar is open (less needed since content edge is clear)
export const OPEN_GAP = 8;
const UserGuideSidebarContext = createContext(null);
export const UserGuideSidebarProvider = ({ children }) => {
const [isOpen, setIsOpen] = useState(() => {
if (typeof window === "undefined") return false;
const saved = localStorage.getItem(SIDEBAR_STATE_KEY);
return saved === "true";
});
const [currentPath, setCurrentPath] = useState(undefined);
const [contentWidth, setContentWidth] = useState(DEFAULT_CONTENT_WIDTH);
// Calculate total sidebar width and required padding
const totalSidebarWidth = isOpen ? TAB_BAR_WIDTH + contentWidth : TAB_BAR_WIDTH;
const gap = isOpen ? OPEN_GAP : CLOSED_GAP;
const requiredPaddingRight = totalSidebarWidth + gap;
// Set CSS custom properties on document root for global access
useEffect(() => {
if (typeof document === "undefined") return;
const root = document.documentElement;
root.style.setProperty("--help-sidebar-width", `${totalSidebarWidth}px`);
root.style.setProperty("--help-sidebar-padding", `${requiredPaddingRight}px`);
}, [totalSidebarWidth, requiredPaddingRight]);
// Update body class for content push effect
useEffect(() => {
if (typeof document === "undefined") return;
if (isOpen) {
document.body.classList.add("user-guide-sidebar-open");
} else {
document.body.classList.remove("user-guide-sidebar-open");
}
return () => {
document.body.classList.remove("user-guide-sidebar-open");
};
}, [isOpen]);
// Persist state to localStorage
useEffect(() => {
if (typeof window === "undefined") return;
localStorage.setItem(SIDEBAR_STATE_KEY, String(isOpen));
}, [isOpen]);
const open = useCallback((path) => {
setCurrentPath(path);
setIsOpen(true);
}, []);
const close = useCallback(() => {
setIsOpen(false);
}, []);
const toggle = useCallback(() => {
setIsOpen((prev) => !prev);
}, []);
return (
<UserGuideSidebarContext.Provider
value={{
isOpen,
open,
close,
toggle,
currentPath,
contentWidth,
setContentWidth,
totalSidebarWidth,
requiredPaddingRight,
}}
>
{children}
</UserGuideSidebarContext.Provider>
);
};
export const useUserGuideSidebarContext = () => {
const context = useContext(UserGuideSidebarContext);
if (!context) {
throw new Error(
"useUserGuideSidebarContext must be used within a UserGuideSidebarProvider"
);
}
return context;
};
export default UserGuideSidebarContext;
@@ -0,0 +1,176 @@
// Content block types for the templatic article system
/**
* @typedef {'heading' | 'paragraph' | 'callout' | 'bullet-list' | 'ordered-list' | 'checklist' | 'icon-cards' | 'grid-cards' | 'time-estimate' | 'info-box' | 'code' | 'requirements' | 'table' | 'image' | 'article-links'} ContentBlockType
*/
/**
* @typedef {Object} HeadingBlock
* @property {'heading'} type
* @property {string} id - Used for TOC anchors
* @property {2 | 3} level - h2 or h3
* @property {string} text
*/
/**
* @typedef {Object} ParagraphBlock
* @property {'paragraph'} type
* @property {string} text - Supports basic markdown: **bold**, *italic*, `code`
*/
/**
* @typedef {Object} CalloutBlock
* @property {'callout'} type
* @property {'info' | 'tip' | 'warning' | 'success'} [variant]
* @property {string} [title]
* @property {string} text
*/
/**
* @typedef {Object} ListItem
* @property {string} text
* @property {string} [bold] - Optional bold prefix
*/
/**
* @typedef {Object} BulletListBlock
* @property {'bullet-list'} type
* @property {ListItem[]} items
*/
/**
* @typedef {Object} OrderedListBlock
* @property {'ordered-list'} type
* @property {ListItem[]} items
*/
/**
* @typedef {Object} ChecklistBlock
* @property {'checklist'} type
* @property {string[]} items
*/
/**
* @typedef {Object} IconCardItem
* @property {string} icon - Icon name from lucide-react
* @property {string} title
* @property {string} description
*/
/**
* @typedef {Object} IconCardsBlock
* @property {'icon-cards'} type
* @property {IconCardItem[]} items
*/
/**
* @typedef {Object} GridCardItem
* @property {string} title
* @property {string} description
* @property {string} [icon]
*/
/**
* @typedef {Object} GridCardsBlock
* @property {'grid-cards'} type
* @property {GridCardItem[]} items
* @property {number} [columns]
*/
/**
* @typedef {Object} TimeEstimateBlock
* @property {'time-estimate'} type
* @property {string} text
*/
/**
* @typedef {Object} InfoBoxBlock
* @property {'info-box'} type
* @property {string} [icon]
* @property {string} title
* @property {string[]} items
*/
/**
* @typedef {Object} CodeBlock
* @property {'code'} type
* @property {string} code
* @property {string} [language]
*/
/**
* @typedef {Object} RequirementItem
* @property {string} icon
* @property {string} title
* @property {string[]} items
*/
/**
* @typedef {Object} RequirementsBlock
* @property {'requirements'} type
* @property {RequirementItem[]} items
*/
/**
* @typedef {Object} TableColumn
* @property {string} key
* @property {string} label
* @property {string} [width]
*/
/**
* @typedef {Object} TableBlock
* @property {'table'} type
* @property {TableColumn[]} columns
* @property {Object[]} rows
*/
/**
* @typedef {Object} ImageBlock
* @property {'image'} type
* @property {string} src
* @property {string} alt
* @property {string} [caption]
*/
/**
* @typedef {Object} ArticleLink
* @property {string} collectionId
* @property {string} articleId
* @property {string} title
* @property {string} [description]
*/
/**
* @typedef {Object} ArticleLinksBlock
* @property {'article-links'} type
* @property {string} [title] - e.g., "Next steps", "Related articles"
* @property {ArticleLink[]} items
*/
/**
* @typedef {Object} ArticleContent
* @property {ContentBlock[]} blocks
*/
/**
* @typedef {Object} TocItem
* @property {string} label
* @property {string} href
* @property {number} level
*/
/**
* Helper to extract TOC from content blocks
* @param {ContentBlock[]} blocks
* @returns {TocItem[]}
*/
export const extractToc = (blocks) => {
return blocks
.filter((block) => block.type === "heading")
.map((block) => ({
label: block.text,
href: `#${block.id}`,
level: block.level,
}));
};
File diff suppressed because it is too large. Load diff
@@ -0,0 +1,604 @@
// Icon names as strings - resolved to actual components in UserGuideLanding.jsx
// Available icons: Rocket, Globe, Gauge, Server, Bell, AlertTriangle, Settings, Wifi, Wrench, Users
/**
* @typedef {Object} Article
* @property {string} id
* @property {string} title
* @property {string} description
* @property {string[]} keywords
*/
/**
* @typedef {Object} Collection
* @property {string} id
* @property {string} title
* @property {string} description
* @property {string} icon
* @property {number} articleCount
* @property {Article[]} articles
*/
/**
* @typedef {Object} FastFind
* @property {string} id
* @property {string} title
* @property {string} collectionId
* @property {string} articleId
*/
// Collections configuration for Checkmate
export const collections = [
{
id: "getting-started",
title: "Getting started",
description: "Essential first steps for new users.",
icon: "Rocket",
articleCount: 4,
articles: [
{
id: "what-is-checkmate",
title: "What is Checkmate",
description: "Overview of monitoring capabilities and core features.",
keywords: ["introduction", "overview", "platform", "monitoring", "uptime"],
},
{
id: "quick-start",
title: "Quick start guide",
description: "Create your first monitor in 5 minutes.",
keywords: ["quick", "start", "first", "monitor", "setup", "tutorial"],
},
{
id: "dashboard",
title: "Understanding the dashboard",
description: "Navigation and key metrics explained.",
keywords: ["dashboard", "navigation", "interface", "home", "metrics"],
},
{
id: "roles-permissions",
title: "User roles and permissions",
description: "User, Admin, and Superadmin roles explained.",
keywords: ["role", "permission", "admin", "superadmin", "user", "access"],
},
],
},
{
id: "uptime-monitoring",
title: "Uptime monitoring",
description: "Monitor website and service availability.",
icon: "Globe",
articleCount: 9,
articles: [
{
id: "http-monitors",
title: "Creating HTTP monitors",
description: "URL monitoring with response validation.",
keywords: ["http", "https", "url", "website", "monitor", "create"],
},
{
id: "ping-monitors",
title: "Ping monitors",
description: "ICMP-based availability checks.",
keywords: ["ping", "icmp", "availability", "monitor"],
},
{
id: "port-monitors",
title: "Port monitors",
description: "TCP port availability monitoring.",
keywords: ["port", "tcp", "availability", "monitor", "service"],
},
{
id: "docker-monitors",
title: "Docker monitors",
description: "Monitor Docker container status and health.",
keywords: ["docker", "container", "monitoring", "health", "status"],
},
{
id: "bulk-import",
title: "Bulk import monitors",
description: "Import multiple monitors from a CSV file.",
keywords: ["bulk", "import", "csv", "multiple", "batch", "upload"],
},
{
id: "intervals-timing",
title: "Monitor intervals and timing",
description: "Configure how often to check your services.",
keywords: ["interval", "timing", "frequency", "check", "schedule"],
},
{
id: "json-path-matching",
title: "JSON path matching",
description: "Validate API response content.",
keywords: ["json", "api", "response", "validation", "match", "path"],
},
{
id: "ssl-tls-monitoring",
title: "SSL/TLS monitoring",
description: "Certificate tracking and error handling.",
keywords: ["ssl", "tls", "certificate", "https", "security", "expiry"],
},
{
id: "uptime-metrics",
title: "Understanding uptime metrics",
description: "Response time, status codes, and uptime percentage.",
keywords: [
"metrics",
"response",
"time",
"status",
"code",
"uptime",
"percentage",
],
},
],
},
{
id: "pagespeed",
title: "PageSpeed monitoring",
description: "Track website performance and Core Web Vitals.",
icon: "Gauge",
articleCount: 5,
articles: [
{
id: "creating-pagespeed",
title: "Creating PageSpeed monitors",
description: "Setup and configuration for performance monitoring.",
keywords: ["pagespeed", "create", "setup", "performance", "lighthouse"],
},
{
id: "understanding-scores",
title: "Understanding scores",
description: "Performance, Accessibility, SEO, and Best Practices explained.",
keywords: ["score", "performance", "accessibility", "seo", "best", "practices"],
},
{
id: "core-web-vitals",
title: "Core Web Vitals explained",
description: "LCP, FCP, CLS, TBT, and Speed Index.",
keywords: ["core", "web", "vitals", "lcp", "fcp", "cls", "tbt", "speed", "index"],
},
{
id: "pagespeed-api",
title: "PageSpeed API configuration",
description: "Setting up your Google PageSpeed API key.",
keywords: ["api", "key", "google", "pagespeed", "configure", "setup"],
},
{
id: "performance-trends",
title: "Performance trends",
description: "Tracking improvements over time.",
keywords: ["trend", "history", "improvement", "tracking", "chart"],
},
],
},
{
id: "infrastructure",
title: "Infrastructure monitoring",
description: "Monitor server hardware with Capture agent.",
icon: "Server",
articleCount: 6,
articles: [
{
id: "installing-capture",
title: "Installing Capture agent",
description: "Setup the monitoring agent on your servers.",
keywords: ["install", "capture", "agent", "setup", "server"],
},
{
id: "cpu-monitoring",
title: "CPU monitoring",
description: "Usage, temperature, and core metrics.",
keywords: ["cpu", "processor", "usage", "temperature", "cores"],
},
{
id: "memory-monitoring",
title: "Memory monitoring",
description: "RAM usage and availability tracking.",
keywords: ["memory", "ram", "usage", "available", "free"],
},
{
id: "disk-monitoring",
title: "Disk monitoring",
description: "Storage space and read/write speeds.",
keywords: ["disk", "storage", "space", "read", "write", "speed"],
},
{
id: "network-monitoring",
title: "Network monitoring",
description: "Bandwidth and packet statistics.",
keywords: ["network", "bandwidth", "packet", "interface", "traffic"],
},
{
id: "hardware-thresholds",
title: "Setting hardware thresholds",
description: "Alert when resources become critical.",
keywords: ["threshold", "alert", "critical", "warning", "limit"],
},
],
},
{
id: "incidents",
title: "Incidents & alerts",
description: "Respond to downtime and issues.",
icon: "AlertTriangle",
articleCount: 4,
articles: [
{
id: "understanding-incidents",
title: "Understanding incidents",
description: "How incidents are detected and created.",
keywords: ["incident", "detection", "downtime", "outage", "alert"],
},
{
id: "incident-timeline",
title: "Incident timeline",
description: "Tracking start, duration, and resolution.",
keywords: ["timeline", "duration", "start", "end", "resolution"],
},
{
id: "resolving-incidents",
title: "Resolving incidents",
description: "Manual vs automatic resolution.",
keywords: ["resolve", "manual", "automatic", "acknowledge", "fix"],
},
{
id: "incident-history",
title: "Incident history and analysis",
description: "Learn from past outages.",
keywords: ["history", "analysis", "report", "past", "outage"],
},
],
},
{
id: "checks",
title: "Checks",
description: "View monitoring check results and history.",
icon: "FileText",
articleCount: 3,
articles: [
{
id: "understanding-checks",
title: "Understanding checks",
description: "What checks are and how they work.",
keywords: ["check", "result", "monitoring", "status", "response"],
},
{
id: "check-details",
title: "Check details and timing",
description: "Response times, status codes, and timing breakdown.",
keywords: ["response", "time", "timing", "breakdown", "dns", "tls"],
},
{
id: "filtering-checks",
title: "Filtering and date ranges",
description: "Filter checks by date range and status.",
keywords: ["filter", "date", "range", "pagination", "sort"],
},
],
},
{
id: "logs",
title: "Logs & diagnostics",
description: "System logs, queue monitoring, and diagnostics.",
icon: "Database",
articleCount: 3,
articles: [
{
id: "application-logs",
title: "Application logs",
description: "View and filter system logs by level.",
keywords: ["log", "error", "warning", "info", "debug", "filter"],
},
{
id: "queue-monitoring",
title: "Queue monitoring",
description: "Monitor background jobs and queue status.",
keywords: ["queue", "job", "background", "bullmq", "worker"],
},
{
id: "system-diagnostics",
title: "System diagnostics",
description: "CPU, memory, and heap usage metrics.",
keywords: ["diagnostics", "cpu", "memory", "heap", "performance"],
},
],
},
{
id: "notifications",
title: "Notifications",
description: "Get alerted when things go wrong.",
icon: "Bell",
articleCount: 7,
articles: [
{
id: "email-notifications",
title: "Email notifications",
description: "SMTP setup and configuration.",
keywords: ["email", "smtp", "mail", "setup", "configure"],
},
{
id: "slack-integration",
title: "Slack integration",
description: "Webhook setup for Slack channels.",
keywords: ["slack", "webhook", "integration", "channel"],
},
{
id: "discord-integration",
title: "Discord integration",
description: "Channel notifications via webhooks.",
keywords: ["discord", "webhook", "integration", "channel"],
},
{
id: "pagerduty-integration",
title: "PagerDuty integration",
description: "On-call management integration.",
keywords: ["pagerduty", "oncall", "integration", "escalation"],
},
{
id: "webhook-notifications",
title: "Webhook notifications",
description: "Custom HTTP callbacks for alerts.",
keywords: ["webhook", "http", "callback", "custom", "api"],
},
{
id: "matrix-integration",
title: "Matrix integration",
description: "Decentralized messaging notifications.",
keywords: ["matrix", "messaging", "integration", "homeserver"],
},
{
id: "testing-notifications",
title: "Testing notifications",
description: "Verify your notification setup works.",
keywords: ["test", "verify", "notification", "check"],
},
],
},
{
id: "status-pages",
title: "Status pages",
description: "Share uptime with your users.",
icon: "Wifi",
articleCount: 5,
articles: [
{
id: "creating-status-page",
title: "Creating a status page",
description: "Public-facing uptime dashboard setup.",
keywords: ["status", "page", "create", "public", "dashboard"],
},
{
id: "customizing-appearance",
title: "Customizing appearance",
description: "Logo, colors, and branding options.",
keywords: ["customize", "logo", "color", "brand", "style"],
},
{
id: "adding-monitors",
title: "Adding monitors to status page",
description: "Main monitors and sub-components.",
keywords: ["add", "monitor", "component", "group"],
},
{
id: "display-options",
title: "Display options",
description: "Charts, uptime percentage, and login links.",
keywords: ["display", "chart", "uptime", "percentage", "option"],
},
{
id: "sharing",
title: "Sharing your status page",
description: "Share your status page URL with users.",
keywords: ["share", "url", "public", "link", "access"],
},
],
},
{
id: "maintenance",
title: "Maintenance windows",
description: "Schedule planned downtime.",
icon: "Wrench",
articleCount: 3,
articles: [
{
id: "creating-maintenance",
title: "Creating maintenance windows",
description: "Suppress alerts during planned maintenance.",
keywords: ["maintenance", "create", "schedule", "suppress", "alert"],
},
{
id: "recurring-maintenance",
title: "One-time vs recurring",
description: "Different scheduling options.",
keywords: ["recurring", "one-time", "schedule", "repeat", "frequency"],
},
{
id: "managing-maintenance",
title: "Managing active maintenance",
description: "Edit, extend, or cancel maintenance windows.",
keywords: ["manage", "edit", "cancel", "extend", "active"],
},
],
},
{
id: "team-management",
title: "Team management",
description: "Collaborate with your team.",
icon: "Users",
articleCount: 3,
articles: [
{
id: "inviting-members",
title: "Inviting team members",
description: "Send email invitations to your team.",
keywords: ["invite", "team", "member", "email", "add"],
},
{
id: "user-roles",
title: "User roles",
description: "Admin vs User permissions explained.",
keywords: ["role", "admin", "user", "permission", "access"],
},
{
id: "managing-access",
title: "Managing team access",
description: "Add and remove team members.",
keywords: ["manage", "access", "remove", "team", "member"],
},
],
},
{
id: "settings",
title: "Settings & configuration",
description: "Customize your Checkmate instance.",
icon: "Settings",
articleCount: 6,
articles: [
{
id: "account-settings",
title: "Account settings",
description: "Manage your profile, password, and account.",
keywords: ["account", "profile", "password", "avatar", "photo", "delete"],
},
{
id: "email-configuration",
title: "Email configuration",
description: "SMTP server setup for notifications.",
keywords: ["email", "smtp", "configure", "server", "mail"],
},
{
id: "global-thresholds",
title: "Global thresholds",
description: "Default alert levels for all monitors.",
keywords: ["threshold", "global", "default", "alert", "level"],
},
{
id: "appearance",
title: "Appearance settings",
description: "Theme and display preferences.",
keywords: ["appearance", "theme", "dark", "light", "display", "mode"],
},
{
id: "timezone-settings",
title: "Timezone settings",
description: "Display preferences for dates and times.",
keywords: ["timezone", "time", "date", "display", "preference"],
},
{
id: "export-monitors",
title: "Export monitors",
description: "Download your monitors as JSON.",
keywords: ["export", "download", "backup", "json", "monitors"],
},
],
},
];
// Fast Finds - Popular/quick access articles
export const fastFinds = [
{
id: "ff-1",
title: "Creating your first monitor",
collectionId: "getting-started",
articleId: "quick-start",
},
{
id: "ff-2",
title: "Setting up email notifications",
collectionId: "notifications",
articleId: "email-notifications",
},
{
id: "ff-3",
title: "Installing Capture agent",
collectionId: "infrastructure",
articleId: "installing-capture",
},
{
id: "ff-4",
title: "Creating a status page",
collectionId: "status-pages",
articleId: "creating-status-page",
},
{
id: "ff-5",
title: "Understanding Core Web Vitals",
collectionId: "pagespeed",
articleId: "core-web-vitals",
},
];
// Helper functions
export const getCollection = (collectionId) => {
return collections.find((c) => c.id === collectionId);
};
export const getArticle = (collectionId, articleId) => {
const collection = getCollection(collectionId);
return collection?.articles.find((a) => a.id === articleId);
};
export const getTotalArticleCount = () => {
return collections.reduce((sum, collection) => sum + collection.articleCount, 0);
};
/**
* @typedef {Object} SearchResult
* @property {string} collectionId
* @property {string} collectionTitle
* @property {string} articleId
* @property {string} articleTitle
* @property {string} articleDescription
* @property {'title' | 'description' | 'keyword'} matchType
*/
/**
* Search articles by query
* @param {string} query
* @returns {SearchResult[]}
*/
export const searchArticles = (query) => {
if (!query || query.trim().length < 2) return [];
const normalizedQuery = query.toLowerCase().trim();
const results = [];
for (const collection of collections) {
for (const article of collection.articles) {
let matchType = null;
// Check title match (highest priority)
if (article.title.toLowerCase().includes(normalizedQuery)) {
matchType = "title";
}
// Check description match
else if (article.description.toLowerCase().includes(normalizedQuery)) {
matchType = "description";
}
// Check keywords match
else if (
article.keywords.some((kw) => kw.toLowerCase().includes(normalizedQuery))
) {
matchType = "keyword";
}
if (matchType) {
results.push({
collectionId: collection.id,
collectionTitle: collection.title,
articleId: article.id,
articleTitle: article.title,
articleDescription: article.description,
matchType,
});
}
}
}
// Sort by match type priority: title > description > keyword
return results.sort((a, b) => {
const priority = { title: 0, description: 1, keyword: 2 };
return priority[a.matchType] - priority[b.matchType];
});
};
@@ -0,0 +1,30 @@
import SidebarWrapper from "./SidebarWrapper";
import {
UserGuideSidebarProvider,
useUserGuideSidebarContext,
TAB_BAR_WIDTH,
DEFAULT_CONTENT_WIDTH,
} from "./UserGuideSidebarContext";
// Main component that wraps SidebarWrapper with context consumer
const UserGuideSidebar = () => {
const { isOpen, open, close } = useUserGuideSidebarContext();
return (
<SidebarWrapper
isOpen={isOpen}
onOpen={open}
onClose={close}
/>
);
};
export {
UserGuideSidebar,
UserGuideSidebarProvider,
useUserGuideSidebarContext,
TAB_BAR_WIDTH,
DEFAULT_CONTENT_WIDTH,
};
export default UserGuideSidebar;
@@ -0,0 +1,222 @@
/**
* Design Tokens for UserGuide
* Supports both light and dark themes via MUI theme integration
*/
// Typography (theme-independent)
export const typography = {
fontFamily: {
sans: "'Inter', system-ui, -apple-system, BlinkMacSystemFont, Helvetica, Arial, sans-serif",
mono: "'Geist Mono', 'Fira Code', 'Consolas', monospace",
},
fontSize: {
xs: 12,
sm: 12,
base: 13,
md: 14,
lg: 15,
xl: 16,
"2xl": 20,
"3xl": 22,
"4xl": 26,
"5xl": 30,
},
lineHeight: {
tight: 1.2,
snug: 1.3,
normal: 1.4,
relaxed: 1.5,
},
fontWeight: {
normal: 400,
medium: 500,
semibold: 600,
bold: 700,
},
};
// Spacing (theme-independent)
export const spacing = {
xs: "4px",
sm: "8px",
md: "12px",
lg: "16px",
xl: "24px",
"2xl": "32px",
"3xl": "40px",
"4xl": "48px",
};
// Brand colors (theme-independent) - Checkmate blue
export const brandColors = {
primary: "#1570EF",
primaryDark: "#1054B0",
primaryLight: "#5BA3F5",
success: "#17B26A",
warning: "#F79009",
error: "#F04438",
};
/**
* Get theme-aware colors based on MUI theme mode
* @param {'light' | 'dark'} mode - The current theme mode
* @returns {Object} Theme-aware color tokens
*/
export const getColors = (mode) => {
const isDark = mode === "dark";
return {
text: {
primary: isDark ? "#ffffff" : "#1C2130",
secondary: isDark ? "#d0d5dd" : "#475467",
muted: isDark ? "#98a2b3" : "#667085",
white: "#ffffff",
whiteMuted: "rgba(255, 255, 255, 0.8)",
},
brand: brandColors,
border: {
default: isDark ? "#344054" : "#d0d5dd",
},
background: {
white: isDark ? "#1a1a1a" : "#ffffff",
alt: isDark ? "#0d0d0d" : "#f9fafb",
code: isDark ? "#1C2130" : "#f3f4f6",
sidebar: isDark ? "#101010" : "#ffffff",
hover: isDark ? "#2a2a2a" : "#f3f4f6",
},
};
};
/**
* Get theme-aware border styles
* @param {'light' | 'dark'} mode - The current theme mode
* @returns {Object} Border styles
*/
export const getBorder = (mode) => {
const colors = getColors(mode);
return {
radius: "4px",
default: `1px solid ${colors.border.default}`,
};
};
/**
* Get theme-aware image styles
* @param {'light' | 'dark'} mode - The current theme mode
* @returns {Object} Image styles
*/
export const getImageStyles = (mode) => {
const colors = getColors(mode);
const border = getBorder(mode);
const isDark = mode === "dark";
return {
image: {
width: "100%",
borderRadius: border.radius,
border: border.default,
display: "block",
boxShadow: isDark ? "0 2px 8px rgba(0, 0, 0, 0.3)" : "0 2px 8px rgba(0, 0, 0, 0.1)",
},
caption: {
fontSize: 10,
color: colors.text.muted,
textAlign: "center",
fontStyle: "italic",
},
};
};
/**
* Get theme-aware chip styles
* @param {'light' | 'dark'} mode - The current theme mode
* @returns {Object} Chip styles
*/
export const getChipStyles = (mode) => {
const isDark = mode === "dark";
return {
url: {
backgroundColor: isDark ? "rgba(21, 112, 239, 0.2)" : "rgba(21, 112, 239, 0.1)",
color: isDark ? "#5BA3F5" : "#1570EF",
padding: "2px 8px",
borderRadius: "4px",
fontFamily: typography.fontFamily.mono,
fontSize: "0.9em",
},
code: {
backgroundColor: isDark ? "#2d2d2d" : "#f3f4f6",
color: isDark ? "#d0d5dd" : "#1C2130",
padding: "2px 8px",
borderRadius: "4px",
fontFamily: typography.fontFamily.mono,
fontSize: "0.9em",
},
};
};
/**
* Get theme-aware component styles
* @param {'light' | 'dark'} mode - The current theme mode
* @returns {Object} Component styles
*/
export const getComponentStyles = (mode) => {
const colors = getColors(mode);
const border = getBorder(mode);
return {
card: {
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
},
heading: {
h2: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize["2xl"],
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: typography.lineHeight.normal,
},
h3: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: typography.lineHeight.normal,
},
},
paragraph: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.primary,
lineHeight: typography.lineHeight.relaxed,
},
listItem: {
fontSize: typography.fontSize.base,
color: colors.text.primary,
lineHeight: typography.lineHeight.snug,
},
};
};
// Legacy exports for backwards compatibility (dark mode defaults)
// These will be replaced by useUserGuideTheme hook usage
export const colors = getColors("dark");
export const border = getBorder("dark");
export const imageStyles = getImageStyles("dark");
export const chipStyles = getChipStyles("dark");
export const componentStyles = getComponentStyles("dark");
// Export everything as a single theme object (dark mode default)
export const theme = {
colors,
typography,
spacing,
border,
imageStyles,
chipStyles,
componentStyles,
};
export default theme;
@@ -0,0 +1,123 @@
/**
* Hook to get theme-aware styles for UserGuide components
* Uses MUI's theme system from Checkmate
*/
import { useTheme } from "@mui/material/styles";
import { typography, spacing, brandColors } from "./theme";
/**
* Returns theme-aware styles for UserGuide components
*/
export const useUserGuideTheme = () => {
const theme = useTheme();
const isDark = theme.palette.mode === "dark";
const colors = {
text: {
primary: theme.palette.primary.contrastText,
secondary: theme.palette.primary.contrastTextSecondary,
muted: theme.palette.primary.contrastTextTertiary,
white: "#ffffff",
},
brand: brandColors,
border: {
default: theme.palette.primary.lowContrast,
},
background: {
white: isDark ? "#1a1a1a" : "#ffffff",
alt: theme.palette.primaryBackground.main,
code: isDark ? "#1C2130" : "#f3f4f6",
sidebar: theme.palette.primaryBackground.main,
hover: isDark ? "#2a2a2a" : "#f3f4f6",
},
};
const border = {
radius: "4px",
default: `1px solid ${colors.border.default}`,
};
const imageStyles = {
image: {
width: "100%",
borderRadius: border.radius,
border: border.default,
display: "block",
boxShadow: isDark ? "0 2px 8px rgba(0, 0, 0, 0.3)" : "0 2px 8px rgba(0, 0, 0, 0.1)",
},
caption: {
fontSize: 10,
color: colors.text.muted,
textAlign: "center",
fontStyle: "italic",
},
};
const chipStyles = {
url: {
backgroundColor: isDark ? "rgba(21, 112, 239, 0.2)" : "rgba(21, 112, 239, 0.1)",
color: isDark ? "#5BA3F5" : "#1570EF",
padding: "2px 8px",
borderRadius: "4px",
fontFamily: typography.fontFamily.mono,
fontSize: "0.9em",
},
code: {
backgroundColor: isDark ? "#2d2d2d" : "#f3f4f6",
color: isDark ? "#d0d5dd" : "#1C2130",
padding: "2px 8px",
borderRadius: "4px",
fontFamily: typography.fontFamily.mono,
fontSize: "0.9em",
},
};
const componentStyles = {
card: {
backgroundColor: colors.background.white,
border: border.default,
borderRadius: border.radius,
},
heading: {
h2: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize["2xl"],
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: typography.lineHeight.normal,
},
h3: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.lg,
fontWeight: typography.fontWeight.semibold,
color: colors.text.primary,
lineHeight: typography.lineHeight.normal,
},
},
paragraph: {
fontFamily: typography.fontFamily.sans,
fontSize: typography.fontSize.base,
color: colors.text.primary,
lineHeight: typography.lineHeight.relaxed,
},
listItem: {
fontSize: typography.fontSize.base,
color: colors.text.primary,
lineHeight: typography.lineHeight.snug,
},
};
return {
colors,
typography,
spacing,
border,
imageStyles,
chipStyles,
componentStyles,
isDark,
theme,
};
};
export default useUserGuideTheme;
@@ -1,53 +0,0 @@
import React, { useState } from "react";
import Menu from "@mui/material/Menu";
import MenuItem from "@mui/material/MenuItem";
import IconButton from "@mui/material/IconButton";
import Settings from "@/assets/icons/settings-bold.svg?react";
export type ActionMenuItem = {
id: number | string;
label: React.ReactNode;
action: Function;
closeMenu?: boolean;
};
export const ActionsMenu = ({ items }: { items: ActionMenuItem[] }) => {
const [anchorEl, setAnchorEl] = useState<null | any>(null);
const open = Boolean(anchorEl);
const handleClick = (e: React.MouseEvent<any>) => {
e.stopPropagation();
setAnchorEl(e.currentTarget);
};
const handleClose = (e: React.MouseEvent<any>) => {
e.stopPropagation();
setAnchorEl(null);
};
return (
<div>
<IconButton onClick={handleClick}>
<Settings />
</IconButton>
<Menu
anchorEl={anchorEl}
open={open}
onClose={handleClose}
>
{items.map((item) => (
<MenuItem
key={item.id}
onClick={(e: React.MouseEvent<HTMLLIElement>) => {
e.stopPropagation();
if (item.closeMenu) handleClose(e);
item.action();
}}
>
{item.label}
</MenuItem>
))}
</Menu>
</div>
);
};
@@ -1,36 +0,0 @@
import LeftArrow from "@/assets/icons/left-arrow.svg?react";
import LeftArrowDouble from "@/assets/icons/left-arrow-double.svg?react";
import LeftArrowLong from "@/assets/icons/left-arrow-long.svg?react";
export const ArrowLeft = ({
type,
color = "#667085",
...props
}: {
type?: string;
color?: string | undefined;
[key: string]: any;
}) => {
if (type === "double") {
return (
<LeftArrowDouble
style={{ color }}
{...props}
/>
);
} else if (type === "long") {
return (
<LeftArrowLong
style={{ color }}
{...props}
/>
);
} else {
return (
<LeftArrow
style={{ color }}
{...props}
/>
);
}
};
@@ -1,28 +0,0 @@
import RightArrow from "@/assets/icons/right-arrow.svg?react";
import RightArrowDouble from "@/assets/icons/right-arrow-double.svg?react";
export const ArrowRight = ({
type,
color = "#667085",
...props
}: {
type?: string;
color?: string | undefined;
[key: string]: any;
}) => {
if (type === "double") {
return (
<RightArrowDouble
style={{ color }}
{...props}
/>
);
} else {
return (
<RightArrow
style={{ color }}
{...props}
/>
);
}
};
@@ -1,48 +0,0 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import { HeaderAuth } from "@/Components/v2/Auth";
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import type { StackProps } from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
import { Typography } from "@mui/material";
interface AuthBasePageProps extends StackProps {
title?: string;
subtitle?: string;
children: React.ReactNode;
}
export const AuthBasePage: React.FC<AuthBasePageProps> = ({
children,
title,
subtitle,
...props
}) => {
const theme = useTheme();
return (
<Stack
gap={theme.spacing(10)}
minHeight="100vh"
{...props}
>
<HeaderAuth />
<Stack
alignItems="center"
margin="auto"
width="100%"
gap={theme.spacing(4)}
>
<Box
width={{ xs: 60, sm: 70, md: 80 }}
mb={theme.spacing(10)}
>
<Logo style={{ width: "100%", height: "100%" }} />
</Box>
<Typography variant="h1">{title}</Typography>
<Typography variant="h1">{subtitle}</Typography>
{children}
</Stack>
</Stack>
);
};
@@ -1,26 +0,0 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Logo from "@/assets/icons/checkmate-icon.svg?react";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
import { LanguageSelector, ThemeSwitch } from "@/Components/v2/Inputs";
export const HeaderAuth = () => {
const theme = useTheme();
const { t } = useTranslation();
return (
<Stack
width={"100%"}
direction="row"
alignItems="center"
justifyContent="flex-end"
py={theme.spacing(4)}
px={theme.spacing(12)}
gap={theme.spacing(4)}
>
<LanguageSelector />
<ThemeSwitch color="red" />
</Stack>
);
};
-2
View File
@@ -1,2 +0,0 @@
export { HeaderAuth } from "./HeaderAuth";
export { AuthBasePage } from "./AuthBasePage";
@@ -1,23 +0,0 @@
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
import type { SxProps } from "@mui/material/styles";
type BaseBoxProps = React.PropsWithChildren<{ sx?: SxProps }>;
export const BaseBox: React.FC<BaseBoxProps> = ({ children, sx }) => {
const theme = useTheme();
return (
<Box
sx={{
backgroundColor: theme.palette.primary.main,
border: 1,
borderStyle: "solid",
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
...sx,
}}
>
{children}
</Box>
);
};
@@ -1,80 +0,0 @@
import Stack from "@mui/material/Stack";
import { ErrorFallback, EmptyFallback } from "./Fallback";
import type { StackProps } from "@mui/material/Stack";
import { useTheme } from "@mui/material/styles";
import { useTranslation } from "react-i18next";
interface BasePageProps extends StackProps {
children: React.ReactNode;
}
export const BasePage: React.FC<BasePageProps> = ({
children,
...props
}: {
children: React.ReactNode;
}) => {
const theme = useTheme();
return (
<Stack
spacing={theme.spacing(10)}
{...props}
>
{children}
</Stack>
);
};
interface BasePageWithStatesProps extends StackProps {
loading: boolean;
error: any;
items: any[];
page: string;
actionLink?: string;
children: React.ReactNode;
}
const isEmpty = (items: any[]) => {
if (!items) return true;
if (Array.isArray(items) && items.length === 0) return true;
return false;
};
export const BasePageWithStates: React.FC<BasePageWithStatesProps> = ({
loading,
error,
items,
page,
actionLink,
children,
...props
}: BasePageWithStatesProps) => {
const { t } = useTranslation();
if (loading) {
return null;
}
if (error) {
return (
<ErrorFallback
title="Something went wrong..."
subtitle="Please try again later"
/>
);
}
if (isEmpty(items)) {
return (
<EmptyFallback
page={page}
title={t(`${page}Monitor.fallback.title`)}
bullets={t(`${page}Monitor.fallback.checks`, { returnObjects: true })}
actionButtonText={t(`${page}Monitor.fallback.actionButton`)}
actionLink={actionLink || ""}
/>
);
}
return <BasePage {...props}>{children}</BasePage>;
};
@@ -1,45 +0,0 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import CheckOutlined from "@/assets/icons/check-outlined.svg?react";
import { useTheme } from "@mui/material/styles";
export const BulletPointCheck = ({
text,
variant = "info",
}: {
text: string;
noHighlightText?: string;
variant?: "success" | "error" | "info";
}) => {
const theme = useTheme();
const colors: Record<string, string | undefined> = {
success: theme.palette.success.main,
error: theme.palette.error.main,
info: theme.palette.primary.contrastTextSecondary,
};
return (
<Stack
direction="row"
className="check"
gap={theme.spacing(6)}
alignItems="center"
>
<CheckOutlined />
<Typography
component="span"
color={
variant === "info"
? theme.palette.primary.contrastTextSecondary
: colors[variant]
}
fontWeight={450}
sx={{
opacity: 0.9,
}}
>
{text}
</Typography>
</Stack>
);
};
@@ -1,23 +0,0 @@
export const Dot = ({
color = "gray",
size = "4px",
style,
}: {
color?: string;
size?: string;
style?: React.CSSProperties;
}) => {
return (
<span
style={{
content: '""',
width: size,
height: size,
borderRadius: "50%",
backgroundColor: color,
opacity: 0.8,
...style,
}}
/>
);
};
@@ -1,158 +0,0 @@
import Box from "@mui/material/Box";
import Stack from "@mui/material/Stack";
import OutputAnimation from "@/assets/Animations/output.gif";
import DarkmodeOutput from "@/assets/Animations/darkmodeOutput.gif";
import Typography from "@mui/material/Typography";
import { BulletPointCheck } from "@/Components/v2/DesignElements";
import { Button } from "@/Components/v2/Inputs";
import { useNavigate } from "react-router";
import { useMediaQuery } from "@mui/material";
import { useTheme } from "@mui/material/styles";
import { useSelector } from "react-redux";
import type { BoxProps } from "@mui/material";
interface BaseFallbackProps extends BoxProps {
children: React.ReactNode;
}
export const BaseFallback: React.FC<BaseFallbackProps> = ({ children, ...props }) => {
const theme = useTheme();
const mode = useSelector((state: any) => state.ui.mode);
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
return (
<Box
margin={isSmall ? "inherit" : "auto"}
marginTop={isSmall ? "33%" : "auto"}
width={{
sm: "90%",
md: "70%",
lg: "50%",
xl: "40%",
}}
padding={theme.spacing(16)}
bgcolor={theme.palette.primary.main}
position="relative"
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.shape.borderRadius}
overflow="hidden"
sx={{
borderStyle: "dashed",
}}
{...props}
>
<Stack
alignItems="center"
gap={theme.spacing(20)}
sx={{
width: "fit-content",
margin: "auto",
marginTop: "100px",
}}
>
<Box
component="img"
src={mode === "light" ? OutputAnimation : DarkmodeOutput}
bgcolor="transparent"
alt="Loading animation"
width="100%"
sx={{
zIndex: 1,
border: "none",
borderRadius: theme.spacing(8),
}}
/>
<Stack
gap={theme.spacing(4)}
alignItems="center"
maxWidth={"300px"}
zIndex={1}
>
{children}
</Stack>
</Stack>
</Box>
);
};
export const ErrorFallback = ({
title,
subtitle,
}: {
title: string;
subtitle: string;
}) => {
const theme = useTheme();
return (
<BaseFallback>
<Typography
variant="h1"
marginY={theme.spacing(4)}
color={theme.palette.primary.contrastTextTertiary}
>
{title}
</Typography>
<Typography>{subtitle}</Typography>
</BaseFallback>
);
};
export const EmptyFallback = ({
page,
title,
bullets,
actionButtonText,
actionLink,
}: {
page: string;
title: string;
bullets: any;
actionButtonText: string;
actionLink: string;
}) => {
const theme = useTheme();
const navigate = useNavigate();
return (
<BaseFallback>
<Stack
gap={theme.spacing(10)}
zIndex={1}
alignItems="center"
>
<Typography
component="h1"
color={theme.palette.primary.contrastText}
>
{title}
</Typography>
<Stack
sx={{
flexWrap: "wrap",
gap: theme.spacing(2),
maxWidth: { xs: "90%", md: "80%", lg: "75%" },
}}
>
{bullets?.map((bullet: string, index: number) => (
<BulletPointCheck
text={bullet}
key={`${(page + "Monitors").trim().split(" ")[0]}-${index}`}
/>
))}
</Stack>
<Stack>
<Button
variant="contained"
color="accent"
onClick={() => navigate(actionLink)}
>
{actionButtonText}
</Button>
</Stack>
</Stack>
</BaseFallback>
);
};
@@ -1,44 +0,0 @@
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import { useTheme } from "@mui/material/styles";
export const PulseDot = ({ color }: { color: string }) => {
const theme = useTheme();
return (
<Stack
width="26px"
height="24px"
alignItems="center"
justifyContent="center"
>
<Box
minWidth="18px"
minHeight="18px"
sx={{
position: "relative",
backgroundColor: color,
borderRadius: "50%",
"&::before": {
content: `""`,
position: "absolute",
width: "100%",
height: "100%",
backgroundColor: "inherit",
borderRadius: "50%",
animation: "ripple 1.8s ease-out infinite",
},
"&::after": {
content: `""`,
position: "absolute",
width: "7px",
height: "7px",
borderRadius: "50%",
backgroundColor: theme.palette.accent.contrastText,
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
},
}}
/>
</Stack>
);
};
@@ -1,68 +0,0 @@
import { Fragment } from "react";
import Stack from "@mui/material/Stack";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useMediaQuery } from "@mui/material";
export const SplitBox = ({
left,
right,
}: {
left: React.ReactNode;
right: React.ReactNode;
}) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
return (
<Stack
direction={isSmall ? "column" : "row"}
bgcolor={theme.palette.primary.main}
border={1}
borderColor={theme.palette.primary.lowContrast}
borderRadius={theme.spacing(2)}
>
<Box
padding={theme.spacing(15)}
borderRight={isSmall ? 0 : 1}
borderBottom={isSmall ? 1 : 0}
borderColor={theme.palette.primary.lowContrast}
flex={0.7}
>
{left}
</Box>
<Box
flex={1}
padding={theme.spacing(15)}
>
{right}
</Box>
</Stack>
);
};
export const ConfigBox = ({
title,
subtitle,
rightContent,
}: {
title: string;
subtitle: string;
rightContent: React.ReactNode;
}) => {
return (
<SplitBox
left={
<Fragment>
<Typography
component="h2"
variant="h2"
>
{title}
</Typography>
<Typography component="p">{subtitle}</Typography>
</Fragment>
}
right={rightContent}
/>
);
};
@@ -1,57 +0,0 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { useTheme } from "@mui/material/styles";
import { useMediaQuery } from "@mui/material";
import type { PaletteKey } from "@/Utils/Theme/v2/theme";
import { BaseBox } from "@/Components/v2/DesignElements";
type GradientBox = React.PropsWithChildren<{ palette?: PaletteKey }>;
export const GradientBox: React.FC<GradientBox> = ({ children, palette }) => {
const theme = useTheme();
const isSmall = useMediaQuery(theme.breakpoints.down("md"));
const bg = palette
? `linear-gradient(to bottom right, ${theme.palette[palette].main} 30%, ${theme.palette[palette].lowContrast} 70%)`
: `linear-gradient(340deg, ${theme.palette.tertiary.main} 10%, ${theme.palette.primary.main} 45%)`;
return (
<BaseBox
sx={{
padding: `${theme.spacing(4)} ${theme.spacing(8)}`,
width: isSmall
? `calc(50% - (1 * ${theme.spacing(8)} / 2))`
: `calc(25% - (3 * ${theme.spacing(8)} / 4))`,
background: bg,
}}
>
{children}
</BaseBox>
);
};
type StatBoxProps = React.PropsWithChildren<{
title: string;
subtitle: string;
palette?: PaletteKey;
}>;
export const StatBox: React.FC<StatBoxProps> = ({
title,
subtitle,
palette,
children,
}) => {
const theme = useTheme();
const textColor = palette ? theme.palette[palette].contrastText : "inherit";
return (
<GradientBox palette={palette}>
<Stack>
<Typography color={textColor}>{title}</Typography>
<Typography color={textColor}>{subtitle}</Typography>
{children}
</Stack>
</GradientBox>
);
};
@@ -1,100 +0,0 @@
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import Box from "@mui/material/Box";
import { BaseBox } from "@/Components/v2/DesignElements";
import Background from "@/assets/Images/background-grid.svg?react";
import { useTranslation } from "react-i18next";
import { useTheme } from "@mui/material/styles";
type StatusBoxProps = React.PropsWithChildren<{}>;
export const BGBox: React.FC<StatusBoxProps> = ({ children }) => {
const theme = useTheme();
return (
<BaseBox
sx={{
overflow: "hidden",
position: "relative",
flex: 1,
padding: theme.spacing(8),
}}
>
<Box
position="absolute"
top="-10%"
left="5%"
>
<Background />
</Box>
{children}
</BaseBox>
);
};
const StatusBox = ({
label,
n,
color,
}: {
label: string;
n: number;
color: string | undefined;
}) => {
const theme = useTheme();
return (
<BGBox>
<Stack spacing={theme.spacing(8)}>
<Typography
variant={"h2"}
textTransform="uppercase"
color={theme.palette.primary.contrastTextTertiary}
>
{label}
</Typography>
<Typography
variant="h1"
color={color}
>
{n}
</Typography>
</Stack>
</BGBox>
);
};
export const UpStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("monitorStatus.up")}
n={n}
color={theme.palette.success.lowContrast}
/>
);
};
export const DownStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("monitorStatus.down")}
n={n}
color={theme.palette.error.lowContrast}
/>
);
};
export const PausedStatusBox = ({ n }: { n: number }) => {
const theme = useTheme();
const { t } = useTranslation();
return (
<StatusBox
label={t("monitorStatus.paused")}
n={n}
color={theme.palette.warning.lowContrast}
/>
);
};
@@ -1,41 +0,0 @@
import Box from "@mui/material/Box";
import { BaseBox } from "@/Components/v2/DesignElements";
import type { MonitorStatus } from "@/Types/Monitor";
import { getStatusPalette } from "@/Utils/v2/MonitorUtils";
import { useTheme } from "@mui/material/styles";
export const StatusLabel = ({
status,
isActive,
}: {
status: MonitorStatus;
isActive?: boolean;
}) => {
const theme = useTheme();
const palette = getStatusPalette(status);
const transformedText = status.charAt(0).toUpperCase() + status.slice(1).toLowerCase();
return (
<BaseBox
sx={{
display: "inline-flex",
flexDirection: "row",
alignItems: "center",
justifyContent: "center",
padding: theme.spacing(3, 5),
color: theme.palette[palette].main,
borderColor: theme.palette[palette].lowContrast,
}}
>
<Box
width={7}
height={7}
bgcolor={theme.palette[palette].lowContrast}
borderRadius="50%"
marginRight="5px"
/>
{isActive === false ? "Paused" : transformedText}
</BaseBox>
);
};
@@ -1,215 +0,0 @@
import Paper from "@mui/material/Paper";
import Table from "@mui/material/Table";
import TableBody from "@mui/material/TableBody";
import TableCell from "@mui/material/TableCell";
import TableContainer from "@mui/material/TableContainer";
import TableHead from "@mui/material/TableHead";
import TableRow from "@mui/material/TableRow";
import IconButton from "@mui/material/IconButton";
import LastPageIcon from "@mui/icons-material/LastPage";
import FirstPageIcon from "@mui/icons-material/FirstPage";
import KeyboardArrowLeft from "@mui/icons-material/KeyboardArrowLeft";
import KeyboardArrowRight from "@mui/icons-material/KeyboardArrowRight";
import Box from "@mui/material/Box";
import TablePagination from "@mui/material/TablePagination";
import type { TablePaginationProps } from "@mui/material/TablePagination";
import { useTheme } from "@mui/material/styles";
import { useMediaQuery } from "@mui/material";
export type Header<T> = {
id: number | string;
content: React.ReactNode;
onClick?: (event: React.MouseEvent<HTMLTableCellElement | null>, row: T) => void;
render: (row: T) => React.ReactNode;
};
type DataTableProps<T extends { id?: string | number; _id?: string | number }> = {
headers: Header<T>[];
data: T[];
onRowClick?: (row: T) => void;
};
export function DataTable<
T extends {
id?: string | number;
_id?: string | number;
onRowClick?: (row: T) => void;
},
>({ headers, data, onRowClick }: DataTableProps<T>) {
const theme = useTheme();
if (data.length === 0 || headers.length === 0) return <div>No data</div>;
return (
<TableContainer component={Paper}>
<Table
stickyHeader
sx={{
"&.MuiTable-root :is(.MuiTableHead-root, .MuiTableBody-root) :is(th, td)": {
paddingLeft: theme.spacing(8),
},
"& :is(th)": {
backgroundColor: theme.palette.secondary.main,
color: theme.palette.secondary.contrastText,
fontWeight: 600,
},
"& :is(td)": {
backgroundColor: theme.palette.primary.main,
color: theme.palette.primary.contrastTextSecondary,
},
"& .MuiTableBody-root .MuiTableRow-root:last-child .MuiTableCell-root": {
borderBottom: "none",
},
}}
>
<TableHead>
<TableRow>
{headers.map((header, idx) => {
return (
<TableCell
align={idx === 0 ? "left" : "center"}
key={header.id}
>
{header.content}
</TableCell>
);
})}
</TableRow>
</TableHead>
<TableBody>
{data.map((row) => {
const key = row.id || row._id || Math.random();
return (
<TableRow
key={key}
sx={{ cursor: onRowClick ? "pointer" : "default" }}
onClick={() => (onRowClick ? onRowClick(row) : null)}
>
{headers.map((header, index) => {
return (
<TableCell
align={index === 0 ? "left" : "center"}
key={header.id}
onClick={
header.onClick ? (e) => header.onClick!(e, row) : undefined
}
>
{header.render(row)}
</TableCell>
);
})}
</TableRow>
);
})}
</TableBody>
</Table>
</TableContainer>
);
}
interface TablePaginationActionsProps {
count: number;
page: number;
rowsPerPage: number;
onPageChange: (event: React.MouseEvent<HTMLButtonElement>, newPage: number) => void;
}
function TablePaginationActions(props: TablePaginationActionsProps) {
const theme = useTheme();
const { count, page, rowsPerPage, onPageChange } = props;
const handleFirstPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, 0);
};
const handleBackButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, page - 1);
};
const handleNextButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, page + 1);
};
const handleLastPageButtonClick = (event: React.MouseEvent<HTMLButtonElement>) => {
onPageChange(event, Math.max(0, Math.ceil(count / rowsPerPage) - 1));
};
return (
<Box
sx={{ flexShrink: 0, ml: 2.5 }}
className="table-pagination-actions"
>
<IconButton
onClick={handleFirstPageButtonClick}
disabled={page === 0}
aria-label="first page"
>
{theme.direction === "rtl" ? <LastPageIcon /> : <FirstPageIcon />}
</IconButton>
<IconButton
onClick={handleBackButtonClick}
disabled={page === 0}
aria-label="previous page"
>
{theme.direction === "rtl" ? <KeyboardArrowRight /> : <KeyboardArrowLeft />}
</IconButton>
<IconButton
onClick={handleNextButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="next page"
>
{theme.direction === "rtl" ? <KeyboardArrowLeft /> : <KeyboardArrowRight />}
</IconButton>
<IconButton
onClick={handleLastPageButtonClick}
disabled={page >= Math.ceil(count / rowsPerPage) - 1}
aria-label="last page"
>
{theme.direction === "rtl" ? <FirstPageIcon /> : <LastPageIcon />}
</IconButton>
</Box>
);
}
export const Pagination: React.FC<TablePaginationProps> = ({ ...props }) => {
const isSmall = useMediaQuery((theme: any) => theme.breakpoints.down("sm"));
const theme = useTheme();
return (
<TablePagination
ActionsComponent={TablePaginationActions}
rowsPerPageOptions={[5, 10, 25]}
{...props}
sx={{
"& .MuiTablePagination-toolbar": {
display: isSmall ? "grid" : "flex",
},
"& .MuiTablePagination-selectLabel": {
gridColumn: "1",
gridRow: "1",
justifySelf: "center",
},
"& .MuiTablePagination-select": {
gridColumn: "2",
gridRow: "1",
justifySelf: "center",
},
"& .MuiTablePagination-displayedRows": {
gridColumn: "2",
gridRow: "2",
justifySelf: "center ",
},
"& .table-pagination-actions": {
gridColumn: "1",
gridRow: "2",
justifySelf: "center",
},
"& .MuiSelect-select": {
border: 1,
borderColor: theme.palette.primary.lowContrast,
borderRadius: theme.shape.borderRadius,
},
}}
/>
);
};
@@ -1,9 +0,0 @@
export { SplitBox as HorizontalSplitBox, ConfigBox } from "./SplitBox";
export { BasePage, BasePageWithStates } from "./BasePage";
export { BGBox, UpStatusBox, DownStatusBox, PausedStatusBox } from "./StatusBox";
export { DataTable as Table, Pagination } from "./Table";
export { GradientBox, StatBox } from "./StatBox";
export { BaseBox } from "./BaseBox";
export { StatusLabel } from "./StatusLabel";
export { BaseFallback, ErrorFallback, EmptyFallback } from "./Fallback";
export { BulletPointCheck } from "./BulletPointCheck";
@@ -1,48 +0,0 @@
import Autocomplete from "@mui/material/Autocomplete";
import type { AutocompleteProps } from "@mui/material/Autocomplete";
import { TextInput } from "@/Components/v2/Inputs/TextInput";
import { CheckboxInput } from "@/Components/v2/Inputs/Checkbox";
import ListItem from "@mui/material/ListItem";
import { useTheme } from "@mui/material/styles";
type AutoCompleteInputProps = Omit<
AutocompleteProps<any, boolean, boolean, boolean>,
"renderInput"
> & {
renderInput?: AutocompleteProps<any, boolean, boolean, boolean>["renderInput"];
};
export const AutoCompleteInput: React.FC<AutoCompleteInputProps> = ({ ...props }) => {
const theme = useTheme();
return (
<Autocomplete
{...props}
disableCloseOnSelect
renderInput={(params) => (
<TextInput
{...params}
placeholder="Type to search"
/>
)}
getOptionKey={(option) => option._id}
renderTags={() => null}
renderOption={(props, option, { selected }) => {
const { key, ...optionProps } = props;
return (
<ListItem
key={key}
{...optionProps}
>
<CheckboxInput checked={selected} />
{option.name}
</ListItem>
);
}}
sx={{
"&.MuiAutocomplete-root .MuiAutocomplete-input": {
padding: `0 ${theme.spacing(5)}`,
},
}}
/>
);
};
@@ -1,11 +0,0 @@
import Button from "@mui/material/Button";
import type { ButtonProps } from "@mui/material/Button";
export const ButtonInput: React.FC<ButtonProps> = ({ sx, ...props }) => {
return (
<Button
{...props}
sx={{ textTransform: "none", height: 34, fontWeight: 400, borderRadius: 2, ...sx }}
/>
);
};
@@ -1,13 +0,0 @@
import ButtonGroup from "@mui/material/ButtonGroup";
import type { ButtonGroupProps } from "@mui/material/ButtonGroup";
export const ButtonGroupInput: React.FC<ButtonGroupProps> = ({
orientation,
...props
}) => {
return (
<ButtonGroup
orientation={orientation}
{...props}
/>
);
};
Loaded 100 of 605 files, more files were not shown because too many files have changed in this diff. Show more