From e794e58709156381b04370dedfa06986d6a39079 Mon Sep 17 00:00:00 2001 From: nyedle <232406712+nyedle@users.noreply.github.com> Date: Sat, 17 Oct 2026 19:01:49 +0300 Subject: [PATCH] refactor codebase & new UI --- .github/labeler.yml | 6 +- AGENTS.md | 98 +- DESIGN.md | 224 +- README.md | 27 +- build/check-bundle-size.ts | 237 +- build/dev-feedback-plugin.ts | 40 - build/pwa-vite-plugin.ts | 1 - build/sites-vite-plugin.ts | 1 - docs/UI_REDESIGN.md | 248 + functions/_lib/admin.js | 1 - functions/_lib/discord-card.js | 8 - functions/_lib/security-headers.js | 2 +- index.html | 2 +- package.json | 2 +- public/news.json | 2 +- public/sounds/click.wav | Bin 32694 -> 0 bytes public/theme-init.js | 1 - scripts/generate-firmware-manifest.mjs | 1 - scripts/sync-supported-devices.mjs | 10 +- src/app/AiOverlay.tsx | 148 - src/app/App.tsx | 104 +- src/app/AppSidebar.tsx | 310 +- src/app/GamesPage.tsx | 139 - src/app/HelpPanel.tsx | 102 + src/app/InterfaceSettings.tsx | 286 - src/app/LaunchCountdown.tsx | 144 - src/app/PendingBar.tsx | 10 +- src/app/Toasts.tsx | 8 +- src/app/UnsupportedNotice.tsx | 7 +- src/app/cards/teevolution/profile-card.css | 14 - src/app/page-locale.tsx | 81 - src/click-sound.ts | 9 - src/control-devices.css | 196 - src/control-profiles.css | 309 - src/control.css | 5394 ----------------- src/control.tsx | 82 - src/device/controller.ts | 107 +- src/{ => device}/device-clients.test.ts | 1 - src/{ => device}/device-clients.ts | 9 +- src/{ => device}/dpi-presets.test.ts | 6 - src/{ => device}/dpi-presets.ts | 2 - src/device/finalmouse-slx.ts | 7 - src/device/firmware-updates.ts | 16 - src/device/game-profile-snapshot.ts | 5 - src/{ => device}/hid-diagnostics.ts | 0 src/device/hid-filters.ts | 1 - src/device/keyboard-client.ts | 1 - src/device/keyboard-workspace.ts | 1 - src/device/kochi-keyboard.test.ts | 4 +- src/device/kochi-keyboard.ts | 7 - src/device/lunafury-labels.test.ts | 2 +- src/device/lunafury-labels.ts | 4 +- src/device/lunafury-profile.test.ts | 2 +- src/device/lunafury-profile.ts | 3 - src/device/m2nex-profiles.ts | 2 - src/device/options.test.ts | 1 - src/device/options.ts | 5 +- src/{ => device}/pending-changes.test.ts | 0 src/{ => device}/pending-changes.ts | 10 - .../preview}/preview-fixtures.test.ts | 9 +- src/{ => device/preview}/preview-fixtures.ts | 7 - .../preview}/preview-modes.test.ts | 0 src/{ => device/preview}/preview-modes.ts | 2 - src/device/profile-key.ts | 1 - src/device/traits.ts | 3 - src/device/types.ts | 23 +- src/device/wuque-keyboard.ts | 3 - src/features/bridge/BridgeStatus.tsx | 130 + src/{ => features/bridge}/bridge-hid.test.ts | 0 src/{ => features/bridge}/bridge-hid.ts | 5 +- .../bridge}/bridge-status-store.ts | 1 - src/{ => features/bridge}/bridge.test.ts | 0 src/{ => features/bridge}/bridge.ts | 2 +- src/{ => features/bridge}/updates.test.ts | 0 src/{ => features/bridge}/updates.ts | 0 .../bridge}/useBridgeBatteryReporter.ts | 6 +- .../bridge}/useBridgeProfileApplier.ts | 13 +- src/{app => features/buttons}/ButtonMap.tsx | 35 +- .../buttons}/SetupCodeDialog.tsx | 20 +- .../buttons}/command-search.test.ts | 2 +- .../buttons}/command-search.ts | 5 +- .../buttons}/logitech-button-map.test.ts | 0 .../buttons}/logitech-button-map.ts | 5 - .../buttons}/logitech-commands.test.ts | 2 +- .../buttons}/logitech-commands.ts | 2 - src/{ => features/buttons}/setup-code.test.ts | 0 src/{ => features/buttons}/setup-code.ts | 10 +- .../capture}/CaptureDialog.tsx | 112 +- src/{ => features/capture}/capture-context.ts | 0 .../capture}/capture-format.test.ts | 1 - src/{ => features/capture}/capture-format.ts | 7 - src/features/devices/DeviceHero.tsx | 257 + src/features/devices/DeviceStats.tsx | 68 + src/features/devices/DevicesHome.tsx | 372 ++ src/{app => features/devices}/Diagnostics.tsx | 17 +- .../devices}/KeychronNapeLayers.tsx | 19 +- .../devices}/MagneticButtons.tsx | 15 +- .../devices}/OverviewPage.tsx | 466 +- .../devices}/battery-history.test.ts | 0 src/{ => features/devices}/battery-history.ts | 4 - .../devices}/cards/AdvancedCards.tsx | 138 +- .../devices}/cards/AtkCards.tsx | 32 +- .../devices}/cards/DpiCard.tsx | 29 +- .../devices}/cards/FirmwareCard.tsx | 33 +- .../devices}/cards/KeyboardPanel.tsx | 18 +- .../devices}/cards/KeyboardWorkspaceCards.tsx | 54 +- .../devices}/cards/KochiKeyboardPanel.tsx | 15 +- .../devices}/cards/LightingCard.tsx | 17 +- .../devices}/cards/LunaFuryCards.tsx | 12 +- .../devices}/cards/MxMasterCards.tsx | 41 +- .../devices}/cards/PerformanceCards.tsx | 99 +- .../devices}/cards/atk-capabilities.test.ts | 0 .../devices}/cards/atk-capabilities.ts | 11 - .../devices}/cards/availability.test.ts | 7 +- .../devices}/cards/availability.ts | 14 +- .../cards/teevolution/ProfileCard.tsx | 26 +- .../devices}/workspace-tabs.test.ts | 1 - .../devices}/workspace-tabs.ts | 5 +- .../games}/GameProfilePanel.tsx | 76 +- .../games}/GameRequestDialog.tsx | 48 +- src/features/games/GamesPage.tsx | 165 + src/{ => features/games}/games-catalog.ts | 0 .../hardware-test}/HardwareTestPage.tsx | 112 +- .../hardware-brand-checks.test.ts | 0 .../hardware-test}/hardware-brand-checks.ts | 8 - .../hardware-test-report.test.ts | 4 +- .../hardware-test}/hardware-test-report.ts | 41 +- .../hardware-test}/polling-stats.test.ts | 0 .../hardware-test}/polling-stats.ts | 0 .../supported-devices-crosscheck.test.ts | 3 - .../supported-devices-crosscheck.ts | 22 - .../hardware-test}/supported-devices-data.ts | 7 +- src/{app => features/news}/NewsBanner.tsx | 11 +- src/{app => features/news}/WhatsNewDialog.tsx | 41 +- src/{ => features/news}/news.test.ts | 4 +- src/{ => features/news}/news.ts | 6 - src/{app => features/profiles}/Profiles.tsx | 26 +- .../profiles}/ShareProfileDialog.tsx | 11 +- src/{app => features/profiles}/hid-keys.ts | 0 src/features/settings/InterfaceSettings.tsx | 244 + .../settings}/interface-preferences.test.ts | 1 - .../settings}/interface-preferences.ts | 4 - .../superstrike}/DeleteHitsPresetDialog.tsx | 21 +- .../superstrike}/HitsTestDialog.tsx | 21 +- .../superstrike}/SaveHitsPresetDialog.tsx | 21 +- .../superstrike}/Superstrike.tsx | 20 +- .../superstrike}/hits-presets.test.ts | 0 .../superstrike}/hits-presets.ts | 11 - .../superstrike}/hits-test.test.ts | 0 src/{ => features/superstrike}/hits-test.ts | 5 - src/{ => i18n}/i18n.test.ts | 6 +- src/{i18n.ts => i18n/index.ts} | 114 +- src/{ => i18n}/locale-loading.test.ts | 2 +- src/{i18n-ar.ts => i18n/locales/ar.ts} | 69 +- src/{i18n-de.ts => i18n/locales/de.ts} | 69 +- src/{i18n-es.ts => i18n/locales/es.ts} | 69 +- src/{i18n-fr.ts => i18n/locales/fr.ts} | 69 +- src/{i18n-ja.ts => i18n/locales/ja.ts} | 69 +- src/{i18n-ko.ts => i18n/locales/ko.ts} | 69 +- src/{i18n-pt.ts => i18n/locales/pt.ts} | 69 +- src/{i18n-ru.ts => i18n/locales/ru.ts} | 69 +- .../locales/tr-extra.ts} | 0 src/{i18n-tr.ts => i18n/locales/tr.ts} | 67 +- src/{i18n-vi.ts => i18n/locales/vi.ts} | 69 +- src/{i18n-zh.ts => i18n/locales/zh.ts} | 69 +- .../tr-coverage.test.ts} | 6 +- src/launch.css | 176 - src/launch.ts | 32 - src/main.tsx | 67 + src/offline-banner.css | 33 - src/{ => platform}/browser-support.ts | 0 src/platform/offline-banner.css | 33 + src/{ => platform}/offline-banner.ts | 6 +- src/{ => platform}/register-sw.ts | 5 - src/{ => platform}/self-xss-guard.ts | 1 - src/{ => platform}/sound-manager.ts | 61 +- src/{ => platform}/trusted-types.ts | 0 src/shared/CardSlot.tsx | 74 + src/{app => shared}/ColorPicker.tsx | 4 +- src/{ui => shared}/battery-icon.ts | 2 +- src/{ui => shared}/color-math.ts | 0 src/{ui => shared}/device-images.test.ts | 17 - src/{ui => shared}/device-images.ts | 25 - src/{app => shared}/icons.tsx | 10 +- src/{ui => shared}/polling-rate.test.ts | 0 src/{ui => shared}/polling-rate.ts | 1 - src/{ui => shared}/profile-label.test.ts | 2 +- src/{ui => shared}/profile-label.ts | 4 +- src/{app => shared}/social-links.tsx | 5 - src/{app => shared}/ui-locale.ts | 2 +- src/{app => shared}/ui.tsx | 67 +- src/{ => styles}/admin.css | 15 - src/styles/areas/advanced.css | 231 + src/styles/areas/buttons.css | 220 + src/styles/areas/library.css | 227 + src/styles/areas/pages.css | 236 + src/styles/areas/performance.css | 370 ++ src/styles/base.css | 62 + src/styles/controls.css | 469 ++ src/styles/device.css | 135 + src/styles/home.css | 154 + src/styles/index.css | 15 + src/styles/instrument.css | 172 + src/styles/settings.css | 69 + src/styles/shell.css | 288 + src/styles/tokens.css | 201 + src/{ => types}/vite-env.d.ts | 0 src/{ => types}/webhid.d.ts | 0 {src => test}/admin-unban.test.ts | 0 {src => test}/discord-card.test.ts | 0 {src => test}/pwa-precache.test.ts | 0 {src => test}/security-headers.test.ts | 2 +- {src => test}/security-middleware.test.ts | 6 +- vite.config.ts | 3 - 214 files changed, 6643 insertions(+), 9303 deletions(-) create mode 100644 docs/UI_REDESIGN.md delete mode 100644 public/sounds/click.wav delete mode 100644 src/app/AiOverlay.tsx delete mode 100644 src/app/GamesPage.tsx create mode 100644 src/app/HelpPanel.tsx delete mode 100644 src/app/InterfaceSettings.tsx delete mode 100644 src/app/LaunchCountdown.tsx delete mode 100644 src/app/cards/teevolution/profile-card.css delete mode 100644 src/app/page-locale.tsx delete mode 100644 src/click-sound.ts delete mode 100644 src/control-devices.css delete mode 100644 src/control-profiles.css delete mode 100644 src/control.css delete mode 100644 src/control.tsx rename src/{ => device}/device-clients.test.ts (97%) rename src/{ => device}/device-clients.ts (92%) rename src/{ => device}/dpi-presets.test.ts (83%) rename src/{ => device}/dpi-presets.ts (90%) rename src/{ => device}/hid-diagnostics.ts (100%) rename src/{ => device}/pending-changes.test.ts (100%) rename src/{ => device}/pending-changes.ts (88%) rename src/{ => device/preview}/preview-fixtures.test.ts (94%) rename src/{ => device/preview}/preview-fixtures.ts (98%) rename src/{ => device/preview}/preview-modes.test.ts (100%) rename src/{ => device/preview}/preview-modes.ts (95%) create mode 100644 src/features/bridge/BridgeStatus.tsx rename src/{ => features/bridge}/bridge-hid.test.ts (100%) rename src/{ => features/bridge}/bridge-hid.ts (99%) rename src/{ => features/bridge}/bridge-status-store.ts (95%) rename src/{ => features/bridge}/bridge.test.ts (100%) rename src/{ => features/bridge}/bridge.ts (98%) rename src/{ => features/bridge}/updates.test.ts (100%) rename src/{ => features/bridge}/updates.ts (100%) rename src/{app => features/bridge}/useBridgeBatteryReporter.ts (92%) rename src/{app => features/bridge}/useBridgeProfileApplier.ts (91%) rename src/{app => features/buttons}/ButtonMap.tsx (90%) rename src/{app => features/buttons}/SetupCodeDialog.tsx (90%) rename src/{ui => features/buttons}/command-search.test.ts (94%) rename src/{ui => features/buttons}/command-search.ts (70%) rename src/{ => features/buttons}/logitech-button-map.test.ts (100%) rename src/{ => features/buttons}/logitech-button-map.ts (88%) rename src/{ => features/buttons}/logitech-commands.test.ts (98%) rename src/{ => features/buttons}/logitech-commands.ts (98%) rename src/{ => features/buttons}/setup-code.test.ts (100%) rename src/{ => features/buttons}/setup-code.ts (91%) rename src/{app => features/capture}/CaptureDialog.tsx (70%) rename src/{ => features/capture}/capture-context.ts (100%) rename src/{ => features/capture}/capture-format.test.ts (98%) rename src/{ => features/capture}/capture-format.ts (95%) create mode 100644 src/features/devices/DeviceHero.tsx create mode 100644 src/features/devices/DeviceStats.tsx create mode 100644 src/features/devices/DevicesHome.tsx rename src/{app => features/devices}/Diagnostics.tsx (88%) rename src/{app => features/devices}/KeychronNapeLayers.tsx (95%) rename src/{app => features/devices}/MagneticButtons.tsx (96%) rename src/{app => features/devices}/OverviewPage.tsx (57%) rename src/{ => features/devices}/battery-history.test.ts (100%) rename src/{ => features/devices}/battery-history.ts (96%) rename src/{app => features/devices}/cards/AdvancedCards.tsx (95%) rename src/{app => features/devices}/cards/AtkCards.tsx (84%) rename src/{app => features/devices}/cards/DpiCard.tsx (96%) rename src/{app => features/devices}/cards/FirmwareCard.tsx (87%) rename src/{app => features/devices}/cards/KeyboardPanel.tsx (96%) rename src/{app => features/devices}/cards/KeyboardWorkspaceCards.tsx (84%) rename src/{app => features/devices}/cards/KochiKeyboardPanel.tsx (67%) rename src/{app => features/devices}/cards/LightingCard.tsx (94%) rename src/{app => features/devices}/cards/LunaFuryCards.tsx (89%) rename src/{app => features/devices}/cards/MxMasterCards.tsx (77%) rename src/{app => features/devices}/cards/PerformanceCards.tsx (88%) rename src/{app => features/devices}/cards/atk-capabilities.test.ts (100%) rename src/{app => features/devices}/cards/atk-capabilities.ts (60%) rename src/{app => features/devices}/cards/availability.test.ts (99%) rename src/{app => features/devices}/cards/availability.ts (94%) rename src/{app => features/devices}/cards/teevolution/ProfileCard.tsx (89%) rename src/{app => features/devices}/workspace-tabs.test.ts (98%) rename src/{app => features/devices}/workspace-tabs.ts (82%) rename src/{app => features/games}/GameProfilePanel.tsx (85%) rename src/{app => features/games}/GameRequestDialog.tsx (89%) create mode 100644 src/features/games/GamesPage.tsx rename src/{ => features/games}/games-catalog.ts (100%) rename src/{app => features/hardware-test}/HardwareTestPage.tsx (91%) rename src/{ => features/hardware-test}/hardware-brand-checks.test.ts (100%) rename src/{ => features/hardware-test}/hardware-brand-checks.ts (90%) rename src/{ => features/hardware-test}/hardware-test-report.test.ts (98%) rename src/{ => features/hardware-test}/hardware-test-report.ts (88%) rename src/{ui => features/hardware-test}/polling-stats.test.ts (100%) rename src/{ui => features/hardware-test}/polling-stats.ts (100%) rename src/{ => features/hardware-test}/supported-devices-crosscheck.test.ts (97%) rename src/{ => features/hardware-test}/supported-devices-crosscheck.ts (73%) rename src/{ => features/hardware-test}/supported-devices-data.ts (99%) rename src/{app => features/news}/NewsBanner.tsx (87%) rename src/{app => features/news}/WhatsNewDialog.tsx (50%) rename src/{ => features/news}/news.test.ts (95%) rename src/{ => features/news}/news.ts (82%) rename src/{app => features/profiles}/Profiles.tsx (95%) rename src/{app => features/profiles}/ShareProfileDialog.tsx (77%) rename src/{app => features/profiles}/hid-keys.ts (100%) create mode 100644 src/features/settings/InterfaceSettings.tsx rename src/{ => features/settings}/interface-preferences.test.ts (98%) rename src/{ => features/settings}/interface-preferences.ts (95%) rename src/{app => features/superstrike}/DeleteHitsPresetDialog.tsx (64%) rename src/{app => features/superstrike}/HitsTestDialog.tsx (93%) rename src/{app => features/superstrike}/SaveHitsPresetDialog.tsx (79%) rename src/{app => features/superstrike}/Superstrike.tsx (96%) rename src/{ => features/superstrike}/hits-presets.test.ts (100%) rename src/{ => features/superstrike}/hits-presets.ts (90%) rename src/{ => features/superstrike}/hits-test.test.ts (100%) rename src/{ => features/superstrike}/hits-test.ts (89%) rename src/{ => i18n}/i18n.test.ts (92%) rename src/{i18n.ts => i18n/index.ts} (94%) rename src/{ => i18n}/locale-loading.test.ts (90%) rename src/{i18n-ar.ts => i18n/locales/ar.ts} (95%) rename src/{i18n-de.ts => i18n/locales/de.ts} (95%) rename src/{i18n-es.ts => i18n/locales/es.ts} (95%) rename src/{i18n-fr.ts => i18n/locales/fr.ts} (95%) rename src/{i18n-ja.ts => i18n/locales/ja.ts} (95%) rename src/{i18n-ko.ts => i18n/locales/ko.ts} (95%) rename src/{i18n-pt.ts => i18n/locales/pt.ts} (94%) rename src/{i18n-ru.ts => i18n/locales/ru.ts} (94%) rename src/{i18n-tr-extra.ts => i18n/locales/tr-extra.ts} (100%) rename src/{i18n-tr.ts => i18n/locales/tr.ts} (95%) rename src/{i18n-vi.ts => i18n/locales/vi.ts} (95%) rename src/{i18n-zh.ts => i18n/locales/zh.ts} (95%) rename src/{i18n-tr-coverage.test.ts => i18n/tr-coverage.test.ts} (96%) delete mode 100644 src/launch.css delete mode 100644 src/launch.ts create mode 100644 src/main.tsx delete mode 100644 src/offline-banner.css rename src/{ => platform}/browser-support.ts (100%) create mode 100644 src/platform/offline-banner.css rename src/{ => platform}/offline-banner.ts (82%) rename src/{ => platform}/register-sw.ts (51%) rename src/{ => platform}/self-xss-guard.ts (95%) rename src/{ => platform}/sound-manager.ts (55%) rename src/{ => platform}/trusted-types.ts (100%) create mode 100644 src/shared/CardSlot.tsx rename src/{app => shared}/ColorPicker.tsx (99%) rename src/{ui => shared}/battery-icon.ts (98%) rename src/{ui => shared}/color-math.ts (100%) rename src/{ui => shared}/device-images.test.ts (94%) rename src/{ui => shared}/device-images.ts (92%) rename src/{app => shared}/icons.tsx (70%) rename src/{ui => shared}/polling-rate.test.ts (100%) rename src/{ui => shared}/polling-rate.ts (88%) rename src/{ui => shared}/profile-label.test.ts (91%) rename src/{ui => shared}/profile-label.ts (69%) rename src/{app => shared}/social-links.tsx (92%) rename src/{app => shared}/ui-locale.ts (90%) rename src/{app => shared}/ui.tsx (89%) rename src/{ => styles}/admin.css (93%) create mode 100644 src/styles/areas/advanced.css create mode 100644 src/styles/areas/buttons.css create mode 100644 src/styles/areas/library.css create mode 100644 src/styles/areas/pages.css create mode 100644 src/styles/areas/performance.css create mode 100644 src/styles/base.css create mode 100644 src/styles/controls.css create mode 100644 src/styles/device.css create mode 100644 src/styles/home.css create mode 100644 src/styles/index.css create mode 100644 src/styles/instrument.css create mode 100644 src/styles/settings.css create mode 100644 src/styles/shell.css create mode 100644 src/styles/tokens.css rename src/{ => types}/vite-env.d.ts (100%) rename src/{ => types}/webhid.d.ts (100%) rename {src => test}/admin-unban.test.ts (100%) rename {src => test}/discord-card.test.ts (100%) rename {src => test}/pwa-precache.test.ts (100%) rename {src => test}/security-headers.test.ts (98%) rename {src => test}/security-middleware.test.ts (97%) diff --git a/.github/labeler.yml b/.github/labeler.yml index 1914176c..c5e25ddc 100644 --- a/.github/labeler.yml +++ b/.github/labeler.yml @@ -1,12 +1,12 @@ "area: ui": - changed-files: - - any-glob-to-any-file: ['src/app/**', 'src/**/*.css', 'index.html'] + - any-glob-to-any-file: ['src/app/**', 'src/features/**/*.tsx', 'src/shared/**', 'src/styles/**', 'index.html'] "area: devices": - changed-files: - - any-glob-to-any-file: ['src/device/**', 'src/*hid*', 'src/*device*', 'src/logitech-*'] + - any-glob-to-any-file: ['src/device/**', 'src/features/devices/**', 'src/features/buttons/**', 'src/features/bridge/**'] "area: translations": - changed-files: - - any-glob-to-any-file: ['src/i18n*.ts'] + - any-glob-to-any-file: ['src/i18n/**'] "area: artwork": - changed-files: - any-glob-to-any-file: ['public/devices/**/*.{png,jpg,jpeg,webp,svg,avif}'] diff --git a/AGENTS.md b/AGENTS.md index a38a6be5..d16f18b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,38 +4,90 @@ Guidance for human contributors and AI agents working in this repository. ## Design -Read [`DESIGN.md`](./DESIGN.md) before touching any UI. The project follows -Material Design 3 (M3): colored filled icon glyphs, floating rounded surfaces, -elevation instead of borders, and theme-token-only colors. All the specific -conventions — sidebar layout, device cards, artwork handling, and the icon -accent palette — are defined there. +Read [`DESIGN.md`](./DESIGN.md) before touching any UI. It is the source of truth for how +OpenMouse looks: a calm, dark, instrument-like control panel where the device is the hero and +settings read as numbers. -Key rules that must never be broken: +Rules that must never be broken: -- Use design tokens from `src/control.css`; never hardcode colors in TSX. +- Colours come from the tokens in `src/styles/tokens.css`. No hex, `rgb()` literals or theme-name + checks in TSX or area CSS; alpha mixes use the `-rgb` triples (`rgb(var(--accent-rgb) / 0.12)`). +- Every theme (12 of them, Emerald default) is a full token set. A change must look right in + Emerald, Light and NieR, and mirror correctly in Arabic (RTL). +- Sentence case everywhere. No uppercase micro labels, no category overlines, no em dashes in copy. +- Real data only: every value comes from the device or the browser; anything unknown is left out + or shown as "–". Never ship placeholder numbers. - Device artwork is always `object-fit: contain`, never cropped. -- Connected-device cards are square (1:1), not 9:16. -- Never invert or work around a styling bug with `overflow: hidden`; pin the - element box instead (see `DESIGN.md` "Commandments"). +- Never hide a layout bug with `overflow: hidden`; pin the element box instead. +- Never remove a setting, control, link or string as a side effect of a UI change. Restyle, don't + delete. If something has to go, say so in the PR. + +## Strings and translations + +- Every user-facing string is a key in `src/i18n/index.ts` (English, the source of truth) and is + rendered with `t(locale, key)` or `tp(locale, key, vars)`. +- `src/i18n/tr-coverage.test.ts` fails on static English text in JSX under `src/app`, + `src/features` and `src/shared`. Units and brand names go through code: `{"DPI"}`. +- Add the key to every file in `src/i18n/locales/` in the same change. `src/i18n/i18n.test.ts` + checks that every locale has every key. +- Locales load lazily; switch through `ensureLocale()` so a switch never flashes English. ## Commands ```bash -npm run dev # start dev server +npm run dev # dev server npm run build # tsc --noEmit && vite build -npm test # node --test -npx tsc --noEmit # typecheck only -npm run check # full local check (dev readiness) +npm test # node --test (src, test, build) +npm run size # bundle budget, after a build +npm run check # build + tests +node scripts/sync-supported-devices.mjs # refresh the supported-devices mirror ``` -Run `npm run build` (or `npx tsc --noEmit`) after any change before finishing. +Run `npm run build`, `npm test` and `npm run size` before finishing; CI runs all three plus +`npm audit`. If the size check fails, find what grew; raise `BUDGET_BYTES` in +`build/check-bundle-size.ts` only with a note that says what was added and the measured size. + +Previews: `?preview=list` opens the device shelf with fixtures, `?preview=` opens one +device (keys in `src/device/preview/preview-modes.ts`, e.g. `egg-op1-v2`, `steelseries-gen2`). +The app needs a window of at least 900 × 600. + +## Repo layout + +Organized by feature; tests sit next to the code they cover. + +| Path | What lives there | +| --- | --- | +| `src/main.tsx` | Entry: browser guards, service worker, mounts `App` | +| `src/app/` | Shell: `App.tsx`, `AppSidebar.tsx`, pending bar, toasts, help panel | +| `src/features/devices/` | Devices home, device page (`OverviewPage.tsx`, `DeviceHero.tsx`), setting `cards/` | +| `src/features/buttons/` | Button mapping, setup codes, Logitech button commands | +| `src/features/profiles/` | Onboard profiles and profile sharing | +| `src/features/superstrike/` | Superstrike hit test and presets | +| `src/features/bridge/` | OpenMouse Bridge client, status store, sidebar status card, update checks | +| `src/features/games/` | Games page and per-game profiles (profiles need Bridge) | +| `src/features/hardware-test/` | Hardware test page, report, supported-devices mirror and cross-check | +| `src/features/capture/` | Report capture dialog and export formats | +| `src/features/news/` | News banner and "What's new" | +| `src/features/settings/` | Interface settings page and stored preferences | +| `src/device/` | Device controller (`controller.ts`), snapshot types, per-vendor glue, pending changes, `preview/` fixtures | +| `src/shared/` | UI primitives (`ui.tsx`: `SwitchRow`, `Segmented`, `OptionMenu`, `StepperSlider`, `RateSlider`, `BatteryIcon`), `CardSlot`, icons, colour picker, formatting helpers | +| `src/i18n/` | `index.ts` (English + API), `locales/.ts` | +| `src/platform/` | Service worker, offline banner, Trusted Types, XSS guard, browser support, sounds | +| `src/styles/` | `tokens.css`, base, controls, shell, page styles, `instrument.css`, `areas/` per tab | +| `src/types/` | Global declarations (Vite, WebHID) | +| `test/` | Tests for `functions/`, security headers and the PWA build | +| `functions/` | Cloudflare Pages Functions (`/api/*`, security middleware) | +| `build/` | Vite plugins and the bundle-size check | + +Where new code goes: a new screen or capability gets its own folder under `src/features/`; a new +setting card for a device goes in `src/features/devices/cards/`; anything two features share goes +in `src/shared/`. Don't add files to the top of `src/`. + +## Devices and protocols -## Repo orientation +Wire protocols and HID drivers live in the external `@openmouse/protocol` and +`@openmouse/keyboard-protocol` packages; don't add driver code here. This repo maps a driver's +capabilities onto the UI (`src/device/controller.ts`, `src/device/traits.ts`) and renders them. -- `src/control.css` — all theme tokens and UI styles (single stylesheet). -- `src/app/` — UI: `AppSidebar.tsx` (nav rail), `InterfaceSettings.tsx` - (settings page), `OverviewPage.tsx` (connect + device list pages). -- `src/device/` — controller, types (`ControlSnapshot`), and protocol glue. -- `src/i18n.ts` — all user-facing strings, organized by key. -- Actual wire protocol/drivers live in the external `@openmouse/protocol` - library; do not expect to add HID driver code here. \ No newline at end of file +`src/features/hardware-test/supported-devices-data.ts` is generated from the website's table +(`openmouse-landing-page/src/data/supported-mice.ts`). Don't edit it by hand; run the sync script. diff --git a/DESIGN.md b/DESIGN.md index 923b4fdb..f52cf2e9 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -1,97 +1,127 @@ -# OpenMouse — Design Style Guide - -This project follows **Material Design 3 (M3)** conventions. Read this guide -before touching any UI. It is the single source of truth for visual style. - -All design tokens live in `src/control.css` (CSS custom properties on the -`.control-shell` / `:root` theme blocks). **Never hardcode colors in -components** — use the theme tokens. Art/SVG/glyph fills may only use -`currentColor` or `color-mix()` against tokens; never hardcode hue values -except for the per-icon accent palette defined below. - -## Core principles - -- **Filled, colored glyphs**: navigation and inline icons are Material - Symbol-style filled paths (`fill="currentColor"`) rendered at 20–24px. - Thin stroke-only icons are out of style for navigation. -- **Floating rounded surfaces**: elevated panels are inset from the page, - rounded (`border-radius: 16px`), with layered `box-shadow` depth — not flat, - border-only rectangles. -- **Elevation over borders**: distinguish surfaces with elevation (gradients, - shadows, `--surface-*` tokens) before reaching for borders. When a divider - is needed it must be clearly visible (use `--line-strong`, never - `--border-rule`/`--line` — those are too faint). -- **State layers**: hovers are subtle overlays (`--hover`, `--surface-hover`) - — a change of background/color, never a sudden border or outline. -- **Theme-awareness**: every style must hold up in **all themes** (dark, - light, ocean, Nier, catppuccin). Check contrast between adjacent surfaces in - both dark and light before committing. -- **Bold labels**: navigation labels use high weight (`font-weight: 600–700`) - and high-contrast text tokens so they read as interactive targets. - -## Layout - -### Sidebar (`.app-sidebar`) - -The left rail is the app's navigation drawer: - -- Floating rounded panel: inset from the shell edge (`margin: 0.55rem 0 - 0.55rem 0.55rem`), `border-radius: 16px`, full outline in `--edge`, - gradient background `linear-gradient(180deg, var(--surface-strong), - var(--surface-dialog))`, and layered drop shadows for lift. -- Nav items (`.app-sidebar-nav-item`): 42px tall, rounded 8px, flex row of - `[icon] [label] [>]`. - - `>` chevron (`.app-sidebar-nav-arrow`) sits at the right via - `margin-left: auto`; it nudges right on hover. - - Adjacent items are separated by a Material divider - (`border-top: 1px solid var(--line-strong)`). -- Per-item icon accent palette (set via inline `style={{ color }}` on the - icon svg — the label itself keeps theme text tokens): - - Home / Connect: green `#5dde89` - - Dashboard (mouse config): blue `#67d8ff` — **use the mouse glyph** - (`M13 1.07V9h7c0-4.08-3.05-7.44-7-7.93zM4 15c0 4.42 3.58 8 8 8s8-3.58 - 8-8v-4H4v4zm7-13.93C7.05 1.56 4 4.92 4 9h7V1.07z`) - - Docs: amber `#e8b267` - - Supported: violet `#a78bfa` - - What's New: gold `#fbbf24` - - Help & Feedback: blurple `#5865f2` — **brand mark** (`DiscordIcon`), sized - by height (`16px`) rather than the square 21px nav box, since the glyph is - wider than it is tall - - Settings: slate `#94a3b8` -- Section order: top nav = Home, Dashboard, Docs. Bottom nav = What's New, - Help & Feedback, Settings. Supported devices lives **inside the Settings - page**, not the sidebar. -- **Help & Feedback is the one featured nav row** - (`.app-sidebar-nav-item-featured`): taller (`min-height: 52px`) with a second - line (`.app-sidebar-nav-sub`) under the label, because the community Discord - is where support and feedback now live. It links straight out to - `DISCORD_URL` rather than opening an in-app dialog, and collapses back to a - plain 42px icon row with the rest when the sidebar is collapsed. - -### Device cards - -- Connected-device cards and the add-device cards are **square (1:1)**, equal - size, on a centered grid. -- Card background is `--surface-strong` so cards separate clearly from the - page background in every theme. -- No border at rest — border + subtle shadow appear only on hover. -- Device artwork: - - Rendered through `.device-tile-image` as `position: absolute; inset: 0; - object-fit: contain;` (+ `transform: scale(1.1)`), never cropped. - - The card clips with `overflow: hidden`. -- The mouse add card and keyboard add card are identical size and shape. The - keyboard add card in the device list must not carry the flex `max-width` - that the empty-state add grid uses (`max-width: none` override). -- State: per-card "Connecting…" text from local state, not global labels. -- Cog/gear buttons are borderless icons (no square outline), `overflow: - visible` on the icon svg so strokes are never clipped. - -## Commandments - -1. Never crop a product rendering; always `object-fit: contain`. -2. Never reintroduce the old 9:16 card aspect ratio for the connected list. -3. Never use `overflow: hidden` to "fix" artwork — pin the image box with - absolute positioning instead. -4. Never use `--line` / `--border-rule` for dividers you want visible. -5. Never introduce a new UI concept (page, card, dialog) without reading this - file and matching the M3 conventions above. \ No newline at end of file +# OpenMouse design + +The single source of truth for how OpenMouse looks. Read it before touching any UI. +Values live in code: `src/styles/tokens.css`. When this file and the code disagree, the code +wins; fix this file. + +Reading: a hardware control panel for people who tune their mouse by the number. Calm, dark, +precise, instrument-like. Dial ENERGY 1 / RHYTHM 2 / MOTION 1. + +## Principles + +- **The device is the hero.** Product renders sit on a lit stage (a dark well with one soft + accent light behind the mouse). Everything else is quiet chrome. +- **Numbers first.** Settings read as figures: label above, value, unit after, meta under. All + numbers are tabular. +- **Colour means something.** The accent (from the user's theme) marks the active place, the + primary action, focus, slider fills and the active stage. `--ok` / `--bad` / `--warn` mark real + states only. Everything else is ink. +- **Real data only.** Every value comes from the device or this browser. A value the device does + not report is left out or shown as "–". Empty states say why and what fills them. +- **One idea per block.** Short titles, sentence case, no category overlines, no uppercase micro + labels, no decorative dots, stripes or glows. + +## Structure + +| Layer | Recipe | +|---|---| +| Ground | `--ground`: page ink plus one soft accent light from the top right. Painted once on the shell. | +| Chrome | Sidebar on `--ink-0` with a hairline edge. | +| Panel | `.om-panel` (or the legacy `.setting-card`): `--sheen` over `--surface-1`, `--lit` edge, `--r-md`. Featured panel: `.om-panel.is-lit` (one or two per page). | +| Row | Inside a panel: hairline `--line-1` separators. Don't nest panels. | +| Well | Inset areas (segmented controls, inputs, render stages): `--well` + `--inset-shadow`. | +| Raised | Buttons, the selected segment key, switch thumb: `--btn` / `--key`. | +| Floating | Menus, dialogs, toasts, the pending bar, help: opaque `--solid-2` + `--shadow-float`. | + +## App layout + +- **Sidebar** (`AppSidebar.tsx`, 248px, collapsible to a 68px icon rail, remembered): + brand + build; Devices, Hardware test, Games (without Bridge it lists the supported games under a download-Bridge panel); "Your devices" switcher + (thumbnail, name, battery icon + percent or "Not connected"); footer: Docs ↗, What's new, + Help & feedback (Discord), Settings; Bridge status line, which opens a small card with Bridge's + status, update notice and download link (`features/bridge/BridgeStatus.tsx`). +- **Main column**: news strip on top, then the scrolling page (`.om-content` > `.om-page`, max + 1160px). The pending-changes bar floats at the bottom of the main column. A help button floats + bottom right and opens a small panel of common questions with instant answers, plus links to + the site FAQ and Discord (`app/HelpPanel.tsx`). +- **Devices home** (`features/devices/DevicesHome.tsx`): a swipeable shelf of device blocks, each + one surface (render on a stage, name, connection badge, live stats with icons, Configure/Open, + and a ⋯ menu to remove the device) ending in an Add device block with the supported mice + crossfading. Drag, swipe, arrow buttons, dots and arrow keys all move it. The shelf sits in the + middle of the page and centres when it doesn't fill the width. No devices: the same shelf with + only the Add device block, larger, in the middle of the page; its button becomes the primary + action. +- **Device page** (`features/devices/DeviceHero.tsx`): hero (render stage, breadcrumb, name, connection line, stats + with icons) with the section tabs bolted under it as one unit. Overview: Current setup figures + + DPI stage chips, Battery over time chart (recorded samples), Firmware. +- **Setting cards** flow in a masonry column layout (`CardSlot`, `styles/instrument.css`) so there + are no holes; every card heading carries a glyph, and feature switches are tiles + (`SwitchRow` in `shared/ui.tsx`). +- **Settings** (`features/settings/InterfaceSettings.tsx`): grouped panels of setting rows (title + description start, control end): Appearance + (theme gallery, each tile painted in its own theme; animations), Language (picker with native + names), Behaviour, Profile key, Driver previews (dev builds), About. + +## Tokens + +Use tokens only; no hex in components or area CSS. Alpha mixes use the `-rgb` triples: +`rgb(var(--accent-rgb) / 0.12)`. + +| Token | Use | +|---|---| +| `--ink-0`, `--ink-1`, `--ground` | Chrome, page | +| `--surface-1..4` | Panel, hover/selected, pressed/strong, rare | +| `--solid-1`, `--solid-2` | Opaque surfaces (menus, dialogs, sticky heads) | +| `--well`, `--glint` | Inset areas, hover wash | +| `--line-1..3` | Hairlines: divider, input edge, emphasis | +| `--text-1..4` | Values/titles, body, labels/meta, disabled/placeholder only | +| `--accent`, `--accent-rgb`, `--on-accent`, `--accent-soft`, `--accent-line` | Active place, primary action, focus, fills | +| `--ok`, `--bad`, `--warn`, `--info` | Real states only | +| `--stage-light` | The light behind a device render | + +Themes: all 12 (Emerald default, Matt, Violet, Ice, Ember, Mono, Miku, Catppuccin Mocha / +Macchiato / Frappé, NieR: Automata, Light) are full token sets in `tokens.css`, selected by +`data-interface-theme` on the shell (and mirrored on ``). Derived tokens are declared on every +themed element, so any element carrying `data-interface-theme` paints itself in that theme. +No component may fork on a theme name. + +## Type + +- DM Sans for everything; JetBrains Mono only for raw identifiers, hex and HID logs. +- Scale: page title 22, panel title 15, body 13, meta 12, small 11. Figures 28–30 at 650. +- Sentence case everywhere. No uppercase tracked labels. Weights 400–650. + +## Shape and space + +- Radii: 3 (chips), 6 (controls, rows), 10 (panels, wells), 14 (hero, device blocks, dialogs). +- Spacing on a 4px grid. Panel padding 16–18px; gaps between panels 16px; rows ≥ 40px. +- Device renders: always `object-fit: contain`, absolutely positioned inside their stage, never + cropped. + +## Controls + +`.om-btn` (default raised), `.is-primary` (one per view), `.is-ghost`, `.is-danger`, `.is-sm`, +`.is-lg`, `.is-icon`; `.om-icon-btn`; `.om-badge` (`.is-ok/.is-bad/.is-warn/.is-accent`); +`.segmented` and `.rate-slider-buttons` (well + raised key); `SwitchButton`; `OptionMenu`; +`StepperSlider`; range inputs with an accent fill. Focus is the accent gap ring (`--focus`), never +removed. + +## Icons + +lucide-react at stroke 1.8, `currentColor`, `--text-3` at rest, accent when active. Icons sit next +to text that says the same thing (battery icon + percent, connection icon + "Wireless"). The battery +is the custom `BatteryIcon` (fill tracks the level; low and charging change its colour). + +## Motion + +120–260ms washes and fades. The device page body fades in; panels and bars rise 8px once. No +loops except the add-device crossfade, which stops with reduced motion. The Animations preference +and `prefers-reduced-motion` both stop motion. + +## Checklist before shipping a page + +- [ ] Tokens only; no new radii, shadows or hex. +- [ ] Sentence case, no overlines, no em dashes in copy. +- [ ] One primary button per view. +- [ ] Every number tabular; every value real or "–". +- [ ] Works in Emerald, Light and NieR; RTL (Arabic) mirrors correctly. +- [ ] Keyboard: every control reachable, visible focus, Escape closes dialogs. diff --git a/README.md b/README.md index b7412982..052910fb 100644 --- a/README.md +++ b/README.md @@ -103,10 +103,29 @@ device inventory changes are written to the browser console with the downloadable diagnostics from a connected device's **Advanced** panel; HID report payloads are not written to the console. -The control panel is organized by responsibility: `control.ts` coordinates the -application, while the template, events, DOM helpers, persisted preferences, -battery history, device selection, and rendering live in focused modules under -`src/`. +The control panel is organized by feature. Tests sit next to the code they cover. + +| Folder | What lives there | +| --- | --- | +| `src/main.tsx` | Entry point: guards, service worker, mounts the app | +| `src/app/` | The shell: `App`, sidebar, pending-changes bar, toasts, AI overlay | +| `src/features/devices/` | Device home, device page, hero, stats, and the setting `cards/` | +| `src/features/buttons/` | Button mapping, setup codes, Logitech button commands | +| `src/features/profiles/` | Onboard profiles and profile sharing | +| `src/features/superstrike/` | Superstrike hit test and presets | +| `src/features/bridge/` | OpenMouse Bridge client, status line, update checks | +| `src/features/games/` | Games page and per-game profiles (Bridge only) | +| `src/features/hardware-test/` | Hardware test page, report builder, supported-devices cross-check | +| `src/features/capture/` | Report capture dialog and export formats | +| `src/features/news/` | News banner and "What's new" | +| `src/features/settings/` | Interface settings and stored preferences | +| `src/device/` | Device controller, per-vendor glue, pending changes, preview fixtures | +| `src/shared/` | UI primitives, icons, colour picker, formatting helpers | +| `src/i18n/` | Strings: `index.ts` is English and the API, `locales/` has one file per language | +| `src/platform/` | Service worker, offline banner, Trusted Types, browser support, sounds | +| `src/styles/` | Design tokens, base styles, and one stylesheet per area | +| `src/types/` | Global type declarations (Vite, WebHID) | +| `test/` | Tests for the Cloudflare functions, security headers and the PWA build | Packet codecs and WebHID drivers live in the standalone [`@openmouse/protocol`](https://github.com/OpenMouse-Project/mouse-protocol) diff --git a/build/check-bundle-size.ts b/build/check-bundle-size.ts index 135241ee..ff234cf3 100644 --- a/build/check-bundle-size.ts +++ b/build/check-bundle-size.ts @@ -2,241 +2,8 @@ import { readdirSync, statSync } from "node:fs"; import { join } from "node:path"; const BUDGET_BYTES: Record = { - // Raised from 103 kB for the interface themes: NieR: Automata and Liquid - // Glass each ship their own token block, and the liquid-glass material - // layer (SVG displacement filters plus their component rules) added the - // largest share. Liquid Glass was removed in the theme sync (main CSS is - // ~104 kB again). The measured bundle is 153.7 kB; 175 kB adds headroom for - // the Developer Hall of Fame page (~15 kB of animated card and hero - // styles that load only on /donate.html). Raised to 180 kB in the - // same pass as the 175 kB target, then to 195 kB for the Minecraft Hall of - // Fame overhaul: the blocky token block (plank textures, bevels, item-frame - // avatars), the animated day/night scene (sun/moon/star/cloud/bonfire - // keyframes) and the credits-style quote widget push the measured CSS - // aggregate to 183.7 kB. - // Raised to 210 kB after the protocol 0.26.0 device batch left the measured - // CSS aggregate at 194.2 kB — 843 bytes under the old 195 kB — so the next - // card or theme change would have tripped the check for no reviewable reason. - // Raised to 225 kB for the Logitech Superstrike Buttons tab: the button map - // card (callouts, command list, scrollbar), the HITS presets/test/share dialogs - // and the Bunny Hop card add ~15 kB of scoped styles; the measured CSS is - // 210.6 kB, which was 631 bytes over the old 210 kB budget. - ".css": 225_000, - // Raised from 510 kB for Bridge discovery, profile editing, automatic - // reconnection, and recent device support, which have since grown further - // with the supported-device page and MX Master remap controls. Preview - // fixtures retain their separate allowance below; the measured aggregate - // is 573.4 kB with them, plus the ~11 kB Hall of Fame chunk. Raised again - // from 590 kB for the Razer button-mapping card and its codec: the measured - // aggregate is 588.2 kB, which left under 2 kB of headroom. Raised again to - // 610 kB for the Pulsar XS-1 feature-report driver and 4K receiver support - // (mouse-protocol 3c3a445): the X3 family codec plus the 4K DPI/polling work - // adds ~1.3 kB to the measured aggregate. Raised to 632 kB for the Attack - // Shark GearHub (0x25a7) protocol routed to 0x1d57 VID devices (+1.6 kB). - // Raised to 700 kB for four new drivers landing together: Keychron M6, - // Keychron Nape Pro (layer/keymap/orientation controls), Glorious Model O - // 2/I 2 lighting, and SteelSeries Rival 3 Gen 1. Measured aggregate is - // 689.0 kB, which leaves about 11 kB of headroom. Raised to 730 kB for the - // device artwork pass: ~20 new product models mapped to transparent top-view - // renders in device-images.ts (PID keys plus name fallbacks) add ~22 kB of - // mapping code to the measured aggregate (720.5 kB). Raised to 765 kB for - // the SteelSeries/device-support and Cloudflare R2 artwork-serve work merged - // on dev: those land with the measured aggregate already at ~750 kB. The - // donate page rebuild (Hall of Fame -> Support) does not drive this; its - // rebuilt donate chunk is lighter than the old Minecraft-themed hof chunk it - // replaced. 765 kB leaves ~15 kB of headroom over the measured aggregate. - // Raised to 790 kB for the MCHOSE A7 V2 mouse and MagDock driver support: - // the measured aggregate is 779.1 kB, leaving ~11 kB of headroom. - // Raised to 800 kB for the ATK ZERO driver (AtkCards.tsx, device/atk.ts) - // and the mouse-reported lift-off range plumbing: the measured aggregate is - // 790.6 kB, leaving ~9 kB of headroom. - // Raised to 895 kB for the Portuguese (pt) localization: the full - // en+pt UI dictionary adds ~85 kB of strings to the measured aggregate - // (883.2 kB, on top of the 800 kB budget's own ~790.8 kB baseline). The pt - // table ships as its own lazy chunk (i18n-pt-*.js, loaded only when a - // non-English locale is selected), so the initial load is unaffected — the - // aggregate counts it because the check sums every emitted chunk. - // Raised to 1,065 kB for Spanish, French, German, and Simplified Chinese: - // each ships as its own lazy chunk (i18n-es/fr/de/zh-*.js) at ~40-55 kB, - // loaded only when that locale is selected — none of them touch the - // initial (English) bundle. Measured aggregate is 1,061.8 kB. - // Raised to 1,165 kB for Japanese and Korean, same lazy-chunk pattern. - // Measured aggregate is 1,160.9 kB. - // Raised to 1,185 kB for Attack Shark R2 support (GearHub-V5 driver, - // traits.ts entry, device artwork name-fallback): the measured aggregate - // is 1,171.4 kB, leaving ~14 kB of headroom. - // Raised to 1,250 kB for the Russian (ru) interface locale: the new - // translation strings push the measured aggregate to 1,232.4 kB. - // Raised to 1,290 kB for the control-app support/UI work that landed - // together: the OpenMouse AI chat overlay (AiOverlay.tsx with its topic - // flow and Discord fallback), the Discord-embed FeedbackDialog, the - // What's New Desktop-app dialog, the desktop-style AppSidebar / - // OverviewPage shell, and the ai.* + wn.* keys added across all nine - // locale tables. Those add ~21 kB of strings and UI to the measured - // aggregate (1,271.9 kB), leaving ~18 kB of headroom. - // Raised to 1,300 kB for swapping hand-rolled icons for lucide-react, - // tree-shaken per icon: the profile-row icons in icons.tsx - // (enable/disable, link/unlink, rename, running/activate, refresh, trash), - // the AppSidebar nav icons (home, mouse config, docs, what's new, - // feedback, settings, chevron), the AiOverlay launcher/close/header - // icons, and the OverviewPage icons (workspace tabs, device-tile stats, - // back/share/plus controls, and the add-device mouse/keyboard glyphs, - // which replace their old hand-drawn illustrations). The measured - // aggregate is 1,290.4 kB, leaving ~10 kB of headroom. - // Raised to 1,310 kB for the crowd-sourced device artwork system: - // ArtworkUploadDialog.tsx (drag-drop upload UI with verification - // states), artwork-verification.ts (canvas-based heuristic analysis), - // artwork-storage.ts (R2 API fetch + localStorage cache), Cloudflare - // Functions for R2 list/upload endpoints, and artwork.* i18n keys - // across all nine locale tables. Measured aggregate is ~1,305.5 kB. - // That system was fully removed later (unmoderated anonymous uploads - // were abused to overwrite device art); the budget stays, and the - // aggregate only shrank. - // Raised to 1,345 kB for the in-app Mouse Test page: MouseTestPage.tsx - // (live polling-rate sampler, rolling chart canvas, DPI/battery device - // card, button tester), its Activity nav icon, and the test.* i18n keys - // across all nine locale tables. Measured aggregate is ~1,329.5 kB. - // Raised to 1,370 kB for the HyperX and Incott drivers: both register in - // registry.ts/vendors.ts, and the Incott one brings a full vendor codec - // plus applyPulsarValue/traits/device-images wiring. Measured aggregate - // is 1,351.0 kB, leaving ~19 kB of headroom. - // Raised to 1,410 kB for the @openmouse/protocol lockfile bump that picks - // up the VXE R1 Pro Max driver on the protocol side: bridge-hid.ts pulls - // in every registered vendor's HID filters via SUPPORTED_HID_FILTERS, so a - // new protocol-side driver grows this bundle even before any app code - // references it directly. Measured aggregate is 1,386.8 kB, leaving ~23 kB - // of headroom for the VXE app-side integration PR. - // Raised to 1,465 kB for the Vietnamese (vi) interface locale landing on - // top of that: same lazy-chunk pattern as the other non-English tables - // (i18n-vi-*.js, loaded only when selected), adding ~47 kB. - // Raised to 1,500 kB for the MCHOSE A5 Pro Max Gen 1 driver: it registers - // in controller.ts/traits.ts and pulls in its own protocol-side codec via - // bridge-hid's SUPPORTED_HID_FILTERS, same growth pattern as prior drivers. - // Measured aggregate is 1,482.3 kB, leaving ~18 kB of headroom. - // Raised to 1,530 kB for Incott's receiver-LED and rapid-fire card: a new - // AdvancedCards section plus its availability/control wiring. Measured - // aggregate is 1,508.0 kB, leaving ~22 kB of headroom. - // Raised to 1,570 kB for the @openmouse/protocol lockfile bump to ec90f61: - // attackshark X11 polling/DPI/battery, MCHOSE A7 V3 settings writes + - // product-string naming, and Windows read-restriction fixes all route - // through bridge-hid's SUPPORTED_HID_FILTERS, so this grows the bundle - // even before the A7 V3 traits work (PR #267) references the new codec - // directly. Measured aggregate is 1,545.7 kB, leaving ~24 kB of headroom. - // Raised to 1,600 kB for the strict artwork-submission flow: the expanded - // artreq.* dictionary (new rejection reasons, background/photo guidance, the - // failure lockout and Discord-ticket strings) ships in all ten locale - // tables, and ArtworkRequestDialog.tsx gained the per-IP failure-lockout - // logic. Measured aggregate is 1,578.2 kB, leaving ~22 kB of headroom. - // Raised to 1,650 kB for the Bridge game-profile feature landing across - // three commits: BridgeCard.tsx (Home connection/games card), - // GameProfilePanel.tsx (per-game target-device/DPI/auto-apply editor), - // games-catalog.ts (CDN games.json fetch for cover art), bridge-status- - // store.ts (shared status poll), and useBridgeProfileApplier.ts (in-tab - // fallback apply + prior-settings restore for brands Bridge has no native - // driver for), plus the bridge.* i18n keys across all ten locale tables. - // Measured aggregate is 1,614.2 kB, leaving ~36 kB of headroom. - // Raised to 1,700 kB for the Games-page request flow shipping on top - // ("Let people request a game or app from the Games page"): the - // request-form dialog, game availability filtering, and submit wiring - // push the measured aggregate to 1,673.2 kB, already past the 1,650 kB - // budget. The read-only DPI stage editor (DpiCard disabled stage - // controls, DeviceCapabilities stage-write flags, and the RAZER preview - // fixture stage table) then adds ~1.7 kB more. Measured aggregate is - // 1,674.9 kB, leaving ~25 kB of headroom. - // Raised to 1,780 kB for the in-app Hardware Test certification suite: - // HardwareTestPage.tsx (verification runner with live polling-rate - // sampling, terminal, device card, and the share flow), the report model - // (automatic checks, verdict, Discord embed), hardware-brand-checks.ts - // (brand-depth checks), the supported-devices crosscheck, and the - // 238-row mirrored supported-devices table (src/supported-devices-data.ts, - // generated from the landing page's supported-mice.ts via - // scripts/sync-supported-devices.mjs), plus the hw.* and - // nav.hardwareTest strings in all ten locale tables. Measured aggregate - // is 1,748.7 kB, leaving ~31 kB of headroom. - // Raised to 1,845 kB for the Arabic (ar) interface locale: i18n-ar.ts is a - // full native translation of all ~1,050 keys and ships as its own lazy - // chunk (i18n-ar-*.js, ~69 kB, loaded only when Arabic is selected) — the - // initial (English) bundle is untouched. This is the same lazy-chunk pattern - // as the pt/es/fr/de/zh/ja/ko/ru/vi locales above, and the aggregate counts - // it only because the check sums every emitted chunk. Measured aggregate is - // 1,817.7 kB, leaving ~27 kB of headroom. - // The in-app Mouse Test page (MouseTestPage.tsx + MouseTestMouse.tsx, the - // test.* keys, and its chart/button-tester CSS) and the strict artwork- - // submission flow (ArtworkRequestDialog.tsx, /api/artwork + images.js, the - // artreq.* dictionary) were both removed; artwork requests now open the - // documented GitHub issue form instead. The budget stays — like the earlier - // crowd-sourced removal, the aggregate only shrank. - // Raised to 1,880 kB for a batch of feature PRs merged together: the unified - // mouse-option widgets (OptionMenu, StepperSlider, and the M3 controls in - // #288), the EGG 4K v2 sensor-glass-mode / polling-divider card (#352), the - // WLmouse Beast X 4K driver wiring (#287, which also pulls its protocol codec - // in through SUPPORTED_HID_FILTERS), and the Razer Bluetooth-mode notice - // (#357). Measured aggregate is 1,850.7 kB, leaving ~29 kB of headroom. - // Raised to 2,085 kB for protocol 0.22.0 and the four device PRs that land - // with it: the Noir Gear M2-NEX integration (#402, local profile slots and - // the macro editor), the Keychron G3 Air / Launcher 8k-1k panel wiring - // (#408, #426), and the Rapoo VT9 Pro client (#422). Their protocol codecs - // (Keychron Launcher mouse catalog, Rapoo register codec, K-snake macro - // store) are pulled in through SUPPORTED_HID_FILTERS and the controllers, - // and the shared UI gained the macro-card and profile-overview CSS. Measured - // aggregate is 1,995.2 kB, leaving ~90 kB of headroom. - // Raised to 2,150 kB for the protocol 0.26.0 device batch: AJAZZ AJ179 PRO - // (#169), Redragon M690 PRO (#163, wired into the panel by #617), Lamzu Paro - // Aurora (#172), plus the X11-on-Bridge (#174), PRO X Wireless format 4 - // (#175) and Logitech onboard-profile (#166/#589) work. Their protocol codecs - // arrive through bridge-hid's SUPPORTED_HID_FILTERS as well as the - // controllers. The measured aggregate is 2,084.2 kB, which left only 801 - // bytes of headroom under the old 2,085 kB. - // Raised to 2.2 MB for the Superstrike Buttons tab (button map, HITS presets, - // speed test, setup codes, bounce check) merged with protocol 0.28.0: the - // measured aggregate is 2,150.4 kB, 350 bytes over the old 2.15 MB budget. - // Raised to 2,350 kB for protocol 0.31.0 and the work that landed on 0.29-0.31: - // the Starlight X (Finalmouse) panel, the Wallhack M-001 scanning, DPI curve - // and macro cards, and the ATK per-model capability cards. The measured - // aggregate is 2,288.4 kB, 88 kB over the old 2.2 MB budget, leaving ~60 kB - // of headroom. - // Raised to 2,450 kB for keyboard protocol 1.0.0 and its control panel: - // Wuque codecs/client wiring, shared workspace cards, per-key trigger UI, - // and keyboard strings in all ten locale chunks. The measured aggregate - // is 2,419.9 kB, leaving ~30 kB of headroom. - // Raised to 2,470 kB for the @openmouse/protocol 0.38.0 bump: the AJAZZ - // NJ07/NJ08 key-map codec, the 49-entry vendor button-action catalog, and - // the marker/default client handling arrive through bridge-hid's - // SUPPORTED_HID_FILTERS. The measured aggregate is 2,454.4 kB, 4.4 kB over - // the old 2,450 kB budget, leaving ~15 kB of headroom. - // Raised to 2,472 kB for the @openmouse/protocol 0.40.0 bump: the G-Wolves - // XVI-new driver (profile-addressed sensor commands plus clicks-only - // remap), 16 more cataloged G-Wolves PIDs and their picker filters, and - // the Bridge empty-enumeration diagnostic. The measured aggregate is - // 2,470.9 kB, 0.9 kB over the old 2,470 kB budget, leaving ~1 kB of - // headroom — the next driver will need a real diet or a bigger bump. - // Raised to 2,474 kB for the @openmouse/protocol 0.40.1 bump: XVI-new - // button-slot fix plus the real DPI stage table and per-stage/active - // setters. The measured aggregate is 2,472.0 kB, under a kilobyte over - // the old 2,472 kB budget. - // Raised to 2,478 kB for the @openmouse/protocol 0.42.1 bump: LunaFury - // onboard, remapping, and power controls, Wallhack debounce, Attack Shark - // X11 SE battery, Superlight 2 Lightspeed polling above 1000 Hz, and the - // DeathAdder V3 polling commit. The measured aggregate is 2,476.0 kB, - // 2.0 kB over the old 2,474 kB budget, leaving ~2.0 kB of headroom. - // Raised to 2,630 kB for Turkish, including legacy driver labels, dynamic - // messages and dialog copy. The two Turkish dictionaries remain lazy - // chunks. The measured aggregate is about 2,620 kB, including render-boundary - // localization; no additional CSS framework or runtime dependency is added. - // Raised to 2,640 kB for the Endgame Gear OP1-8K control panel: the section - // the missing family tag had kept dark adds the sensor-controls, handedness - // and lift-off cards plus two strings across twelve locales, taking the - // measured aggregate from 2,629.5 kB to 2,632.2 kB — over the old budget by - // 2.2 kB, which itself had only 545 bytes of headroom, so any new localized - // string would have tripped it. - // Raised to 2,720 kB for the @openmouse/protocol 0.43.0 bump: eleven - // SteelSeries drivers arrive through the shared registry (the Aerox 3 - // Wireless Gen 2, Rival 3 Gen 2, and Rival 3 Wireless Gen 2 preset families - // plus the Rival 100/110/300/5/600, Sensei 310/Raw/Rival Pro, and Prime Mini - // codecs). The per-stage X/Y column adds ~750 bytes of JS and ~460 bytes of - // CSS of its own. The measured aggregate is 2,715.0 kB, 75.0 kB over the old - // 2,640 kB budget, leaving ~5.0 kB of headroom. - ".js": 2_720_000, + ".css": 165_000, + ".js": 2_800_000, }; const ASSETS = join("dist", "assets"); diff --git a/build/dev-feedback-plugin.ts b/build/dev-feedback-plugin.ts index 8f2e2285..1bd9e18c 100644 --- a/build/dev-feedback-plugin.ts +++ b/build/dev-feedback-plugin.ts @@ -1,30 +1,9 @@ -// Dev-only relay for `/api/feedback`. -// -// The production endpoint is a Cloudflare Pages Function -// (`functions/api/feedback.js`) that relays the POST body to the Discord -// webhook, so the bare vite dev server 404s the exact request the Feedback -// dialog and the hardware-test "Share report" button issue. -// -// This middleware serves the same JSON contract in local development and is -// honest about delivery: -// -// - If `DISCORD_FEEDBACK_WEBHOOK` is present (put it in the gitignored -// `.env.local`, e.g. `DISCORD_FEEDBACK_WEBHOOK=https://discord.com/api/ -// webhooks//`), the payload is forwarded to Discord exactly like -// the production function and `{ ok: true }` is only returned when Discord -// accepts it. -// - Without a configured webhook the request is refused with 503 and an -// actionable message — it never pretends a report was delivered. -// -// The embeds are always printed to the terminal so they can be inspected, with -// or without a webhook. import type { Plugin } from "vite"; import { loadEnv } from "vite"; import type { IncomingMessage, ServerResponse } from "node:http"; export interface FeedbackValidation { valid: boolean; - /** Status to answer with when invalid; 200 on the success path. */ status: number; body: string; embeds: unknown[] | null; @@ -36,12 +15,6 @@ export interface DiscordForwardResult { status: number; } -/** - * Boot-time hint printed by the dev server so the state of the relay is - * visible before anyone clicks Share: configured and delivering, or about to - * refuse every share with 503 until `DISCORD_FEEDBACK_WEBHOOK` exists in - * `.env.local` (and the server is restarted, since the env is read at boot). - */ export function feedbackRelayHint(webhook: string | undefined): string { if (!webhook || !webhook.startsWith("https://discord")) { return "[dev-feedback] relay up, but DISCORD_FEEDBACK_WEBHOOK is not set in .env.local — Share report will answer 503 until it is; add it and restart the dev server"; @@ -51,11 +24,6 @@ export function feedbackRelayHint(webhook: string | undefined): string { const MAX_DEV_BODY_BYTES = 1024 * 1024; -/** - * Validates a single `/api/feedback` request exactly like the production relay - * contract: POST only, a JSON body carrying an `embeds` array. Pure so it can - * be unit-tested; delivery is a separate step (`sendToDiscord`). - */ export function validateFeedbackRequest(method: string | undefined, rawBody: string): FeedbackValidation { if (method !== "POST") { return { valid: false, status: 405, body: JSON.stringify({ message: "Method not allowed." }), embeds: null, rawBody }; @@ -76,11 +44,6 @@ export function validateFeedbackRequest(method: string | undefined, rawBody: str return { valid: true, status: 200, body: JSON.stringify({ ok: true }), embeds, rawBody }; } -/** - * Forwards a validated payload to the Discord webhook, mirroring the - * production relay. Returns 502 for network failures; any other non-2xx - * status is the one Discord answered with. - */ export async function sendToDiscord( webhook: string, rawBody: string, @@ -105,14 +68,11 @@ function endJson(res: ServerResponse, status: number, body: string): void { res.end(body); } -/** Vite plugin that serves a local `/api/feedback` in dev. */ export function devFeedback(): Plugin { return { name: "openmouse:dev-feedback", apply: "serve", configureServer(server) { - // Optional local webhook so dev shares can really land in the channel: - // loadEnv reads .env / .env.local etc. (both are gitignored). const env = loadEnv(server.config.mode, server.config.root, ""); const webhook = env.DISCORD_FEEDBACK_WEBHOOK; server.config.logger.info(feedbackRelayHint(webhook)); diff --git a/build/pwa-vite-plugin.ts b/build/pwa-vite-plugin.ts index c4f3737c..654207b2 100644 --- a/build/pwa-vite-plugin.ts +++ b/build/pwa-vite-plugin.ts @@ -21,7 +21,6 @@ export const PRECACHE_PAGES = ["index.html"]; export const ROOT_PAGE = "index.html"; -/** The root page is served from "/", every other page from its own filename. */ export function pageUrl(file: string): string { return file === ROOT_PAGE ? "/" : `/${file}`; } diff --git a/build/sites-vite-plugin.ts b/build/sites-vite-plugin.ts index 96e14a2d..93c18598 100644 --- a/build/sites-vite-plugin.ts +++ b/build/sites-vite-plugin.ts @@ -2,7 +2,6 @@ import { mkdir, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; import type { Plugin } from "vite"; -/** Adds the Cloudflare Worker entry point required by the Sites host. */ export function sites(): Plugin { let root = process.cwd(); let outputDirectory = "dist"; diff --git a/docs/UI_REDESIGN.md b/docs/UI_REDESIGN.md new file mode 100644 index 00000000..e7b9b5c0 --- /dev/null +++ b/docs/UI_REDESIGN.md @@ -0,0 +1,248 @@ +# OpenMouse UI redesign plan + +Status: **implemented on branch `ui-redesign`.** `DESIGN.md` is now the live contract; this file +keeps the original audit and plan for reference. Changes from the plan after owner feedback: + +- Navigation stays a **left sidebar** (collapsible to an icon rail, remembered), not a top bar. + It also carries a "Your devices" switcher with battery icons. +- Devices home is a **swipeable shelf of device blocks** (drag, swipe, arrows, dots, arrow keys), + ending in an Add device block; first run shows a hero with the connect action. +- The device page uses a **hero header with the section tabs bolted under it** instead of a left + device column, so settings get the full width. +- Icons with every live value: connection, battery (custom fill icon), polling, DPI, profile. +- "OpenMouse AI" became **Quick help**, a plain FAQ panel opened from the sidebar. +- Language moved into Appearance; support links moved to Settings > About. +- The dead launch countdown was removed. + +- Mock: `../ui-research/openmouse-redesign/mock.html` (serve the folder, open `mock.html`; bottom-left bar switches screens and themes). Renders in `../ui-research/openmouse-redesign/shots/`. +- Current-app captures for comparison: `../ui-research/openmouse-current/`. +- Reference: `../ui-research/` (invade.lol captures, `invade-ui/` design system clone at `261eba8`). + +Decisions already made by the owner: + +| Question | Answer | +|---|---| +| antislop | During (rules apply while planning and building) | +| Themes | Keep all 12, each rebuilt as a full token override | +| Shell | Top bar first; later changed by the owner to keep the left sidebar | +| Accent | The user's selected theme supplies the accent; no new brand colour | + +Reading this as: a hardware control panel for mouse enthusiasts who tune numbers, in a calm dark instrument style, dial ENERGY 1 / RHYTHM 2 / MOTION 1. + +--- + +## 1. Every screen in OpenMouse today + +The app is a single page with state-based navigation (`App.tsx` `resolvedPage`), no URL routes. Everything below was traced from source. + +### 1.1 Gates (render instead of the app) + +| Screen | Source | Trigger | +|---|---|---| +| Launch countdown | `LaunchCountdown.tsx`, `launch.ts`, `launch.css` | Prod build before 2026-09-04 17:00 UTC. That date has passed, so this is dead code. | +| Unsupported notice, 4 variants | `UnsupportedNotice.tsx`, `platform/browser-support.ts` | Handheld, no WebHID and no Bridge, insecure context, old browser | +| Window too small | `main.tsx` + `useViewportTooSmall.ts` | Viewport under 900 × 600 | +| Offline banner | `offline-banner.ts/.css` | Mounted outside React; PWA offline | + +### 1.2 Pages + +| Page | Source | Notes | +|---|---|---| +| **Home: empty** | `OverviewPage.tsx` `OverviewEmpty` | "Connect your device" hero, animated aurora, one square add card | +| **Home: device list** | `DeviceListView` | Square 1:1 tiles of authorized devices, connected one moved to the middle, add tile last | +| **Device page** (mouse) | `OverviewPage` + `Workspace` | Pill tab bar + Back. Tabs filtered per device by `availableWorkspaceTabs()` | +| ↳ Overview | `OverviewContent` | Giant centred render, status pill (DPI, battery, Hz, profile), Firmware card, optional power cards, M2-NEX profile overview | +| ↳ Performance | `performance` array | DPI, Polling, Sensor, Ninjutso, Egg filter/polling/CPI, Processing, Power mode, Wallhack scanning/curves, ATK sensor; side column with the render | +| ↳ Buttons | same section | Superstrike (HITS), ButtonMap, Magnetic buttons, Lightforce, TMR, Debounce, SPDT, Razer/Egg/MX/ATK/LunaFury mappings, Ksnake scroll | +| ↳ Macro | `macro` array | Ksnake macros, Wallhack macros | +| ↳ Lighting | `lighting` array | LightingCard (zones, ColorPicker), DPI indicator lighting | +| ↳ Profiles | `Profiles.tsx`, `KeychronNapeLayers`, Teevolution card, ATK/onboard/Pulsar Pro cards | Rendered as the last tab in its own nav slot | +| ↳ Advanced | `advanced` array + `Diagnostics` + Logitech details + MX Master cards | Signal, sleep, low power, dongle LED, receivers, Finalmouse, Incott; dev-testing note; Diagnostics with capture | +| ↳ Empty tab | `workspace-tab-empty` | "This device has no X settings" | +| **Device page** (keyboard) | `KeyboardPanel`, `KochiKeyboardPanel`, `KeyboardWorkspaceCards` | Same frame; Kochi only gets Overview | +| **Hardware test** | `HardwareTestPage.tsx` | Render column, stage runner with verdict, device facts column, long footer paragraph | +| **Games** (Bridge only) | `GamesPage.tsx`, `GameProfilePanel.tsx` | Game tile grid; per-game profile reuses `Workspace` with `gameProfile` | +| **Settings** | `InterfaceSettings.tsx` | Profile key, theme (12), language (12), sound, animations, instant flash, advanced editors, experimental, dev previews, support links, reset | +| Docs, Help & Feedback | `AppSidebar.tsx` | External links (docs.openmouse.app, Discord) | + +### 1.3 Overlays + +`WhatsNewDialog`, `CaptureDialog`, `ShareProfileDialog`, `SetupCodeDialog`, `BounceCheckDialog`, `HitsTestDialog`, `SaveHitsPresetDialog`, `DeleteHitsPresetDialog`, `GameRequestDialog`, the dialogs inside `Profiles.tsx`, `ColorPicker`, `OptionMenu`, `ToastHost`, `PendingBar`, `NewsBanner`, `HelpPanel`. + +### 1.4 Not in scope + +- `src/styles/admin.css` has no consumer in `src/` (only `functions/api/admin/unban.js` exists). Leave it. +- `dist/check.html` and `dist/supported.html` are stale build output with no source entry. +- The marketing site and openmouse.app/supported live in other repos. + +### 1.5 Size of the job + +51 card components, ~6,100 lines of CSS across 6 files, 1,326 top-level selectors, 86 `box-shadow`, 44 `font-mono`, 15 `uppercase`, 45 hex literals in TSX (mostly the nav icon palette and stage colours). DOM coupling from logic is minimal: only `#workspace-tab-*` focus and `.battery-fill`. No test asserts class names. That makes a full restyle safe to do without touching `device/controller.ts`. + +--- + +## 2. What we take from the reference, and what we don't + +### Take (ideas) + +| invade.lol idea | OpenMouse use | +|---|---| +| Slim top bar, one content column (~1200px), footer | App shell for every page | +| Profile card on the left with section nav under it | **Device card**: render, name, connection, actions; section nav under it; "Other devices" list below (the "Played with" idea) | +| Ranked/Performance panel: big figures, label above, unit after, meta under | **Current setup** panel: DPI, polling, battery, profile, plus the DPI stage strip | +| Activity/LP chart: one line, soft fade, hairline guides | **Battery over time** from `features/devices/battery-history.ts` (real samples, 5-min checkpoints, 30-day window) | +| Match rows: one row per game, result tint, expand inline | Hardware-test stages, game list, diagnostics entries | +| Champion table: name pinned, figures right-aligned, tabular | Button mapping, DPI stages, device facts, a supported-devices table if we add one | +| Segmented control as an inset well with a raised key | Polling rate, DPI mode, language, every enum setting | +| Translucent sheets lit from the top, one soft light on the ground | Surface system for all 12 themes | +| "Last answer stays until the next arrives" | Device reads: keep the old value dimmed while a read is in flight, no skeleton flashes | +| Empty states that say why and what happens next ("Checked every 30 seconds") | Every empty state (no device, no history, tab unavailable, Bridge absent) | +| ⌘K search | Settings search over the cards the connected device actually has (only if built; see 5.2) | + +### Do not copy + +- Invade's violet, its token values, Archivo, rank/tier colours, PerfDial glow, LoL components, copy or icons. +- `invade-ui` is **PolyForm Noncommercial**: no vendoring of CSS, components or fonts. OpenMouse is AGPL and takes donations. We reimplement the ideas in our own tokens. The mock's token file is our own sketch; re-derive values during implementation rather than diffing against `invade-ui/src/styles/tokens.css`. +- The reference's coloured 3px result edge. We mark pass/fail with icon + word + a faint tint instead (no decorative stripes). + +--- + +## 3. What changes, aspect by aspect + +| Aspect | Today | New | +|---|---|---| +| Navigation | Floating rounded left rail, rainbow icon per item, collapses on device pages | 52px top bar: brand + build, Devices, Hardware test, Games (Bridge only), Docs ↗; right side: Bridge state, search, What's new, Help, Settings | +| Device page frame | Back button + centred pill tabs with glowing active pill; giant centred render | 300px left column (device card + section nav, sticky) and a main column of panels | +| Home | Square tiles centred on a page, aurora animation | Device rows (render 64px, name, connection, polling, battery, state) + an OpenMouse Bridge panel with real status | +| Cards | 51 bespoke chromes; mono uppercase micro labels ("STAGES IN USE", "POLLING RATE") | One `Panel` (title, meta, end slot); rows split by hairlines; sentence-case labels | +| Settings rows | Mixed per card | One `SettingRow`: label + description left, control right, 44px min | +| Controls | Several button styles, filled green toggles and pills | Button (default, ghost, one primary per view), Segmented, Switch, Select, Slider, NumberField, Badge | +| Colour | Per-item icon hues, green glows, green Back button | Neutral chrome; accent only for primary action, active place, focus, slider fill, active stage. `ok/bad/warn` only for real states | +| Type | DM Sans + JetBrains Mono | DM Sans only, tabular figures everywhere; mono only for raw hex IDs and HID logs in Diagnostics. Scale: page 20, panel 14, body 13, meta 12 | +| Radii | 16px floating panels, pills | 3 / 5 / 8 / 12 only; no pills | +| Shadows | 86 shadows, glows | Lit top edge on sheets; real shadow only on menus, dialogs, the pending bar | +| Unsaved changes | `PendingBar` at the bottom of the shell | Sticky bar at the bottom of the main column: "N unsaved changes", the diff in words, Discard, **Save to mouse** | +| Motion | Aurora, glows, scroll smoothing | 120–180ms washes; values swap in place; existing "Animations" preference and `prefers-reduced-motion` both honoured | +| Help bot | Floating "OpenMouse AI" bubble with a bot icon that plays canned answers | It is a scripted FAQ, so it must not be called AI. Rename to "Quick help" and move it into the Help menu, or drop it (owner decides) | +| Launch countdown | Live code, date passed | Delete with `launch.ts`/`launch.css` instead of restyling | +| Docs | `DESIGN.md` mandates M3, rainbow icons, 16px floating surfaces, square cards | Replace `DESIGN.md` and the design rules in `AGENTS.md` before code lands | + +--- + +## 4. Page by page + +Widths: content 1200px; device page = 300px side + 24px gap + main. Below 1100px the device column becomes a horizontal header (small render, name, actions) with section nav as a row of tabs. The 900 × 600 gate stays. + +### 4.1 Shell (`App.tsx`, new `TopBar.tsx`, `Footer.tsx`; delete `AppSidebar.tsx`) + +- Top bar on `--ink-0` with a 1px hairline. Active item = filled `--surface-2`, no underline glow. +- News banner becomes a 34px strip under the bar, same column width, dismiss on the right (existing `features/news/news.ts` logic). +- Footer: brand, Supported devices, GitHub, Discord, X, Donate, AGPL + not-affiliated line. Moves the support links out of Settings. +- Live region and toasts stay where they are in the tree. + +### 4.2 Devices (home) + +- Page head: "Devices", meta "N authorized in this browser", primary **Connect a device** (the only primary on the page). +- Rows: 64px render (contain, never cropped), name + brand · connection, polling, battery, state badge ("Connected" / "Plug in to open"), chevron. Whole row opens the device; gear button removed (it did the same thing). +- Empty: no aurora. Text: what OpenMouse needs (Chromium or Bridge, the device plugged in) and the Connect button. +- Bridge panel (lit sheet): what Bridge does, real status from `features/bridge/bridge-status-store.ts`, latest release from `features/bridge/updates.ts`, Download link. + +### 4.3 Device page + +**Left column** +- Device card (`--r-lg`): render in an inset well with a soft accent light behind it; name 20px; "Connected · Brand"; connection detail; actions: Share profile, Setup code, overflow (Disconnect, Copy profile key, Diagnostics). +- Section nav: Overview, Performance, Buttons, Macros, Lighting, Profiles, Advanced, filtered by the existing `availableWorkspaceTabs()`. Arrow-key behaviour from today's tablist is kept. +- Other devices (overview only): other authorized devices as 44px rows. + +**Overview** +- Current setup (lit sheet): DPI, polling, battery, onboard profile as figures; DPI stage strip with the active stage marked. Every value from `status`; missing values show "–". +- Battery over time: chart from `features/devices/battery-history.ts` with the time-left estimate it already computes; empty state explains the 10-minute minimum. +- Firmware: installed / latest / connection rows + Check for update (`firmware-updates.ts`). +- Device-specific overview blocks (M2-NEX profiles, power overview) slot in as panels. + +**Performance**: Sensitivity panel (stage rows: enable, index, number field, slider, "Active"), Polling rate (segmented well, current value as a figure), Sensor/LOD, then vendor panels. One column; the render no longer repeats here because it sits in the left card. + +**Buttons**: ButtonMap gets the main column with the mouse diagram; mapping tables become two-column rows (button name | select). HITS keeps its dial-like controls but inside a standard panel; its presets become a Select + Save/Delete in the panel header. + +**Macros / Lighting / Profiles**: standard panels. Lighting zone list on the left of the panel, ColorPicker on the right. Profiles as rows (slot, name, active badge, actions) with Share/Import in the header. + +**Advanced**: settings rows grouped into panels (Wireless, Power, Receiver, Vendor). The dev-testing note becomes one quiet line at the top. Diagnostics becomes a panel with an inline expandable log (mono allowed here) and Capture. + +**Unsaved changes bar**: described in section 3. + +### 4.4 Keyboard page + +Same frame. Device card shows the keyboard render; sections from `availableKeyboardWorkspaceTabs()`. + +### 4.5 Hardware test + +- Page head: "Hardware test", one-line meta, actions: Start test (primary), Download log. +- Left: device card (shared component). Right: stage list, one 44px row per stage: state icon + label + detail + right-aligned result; running stage shows its progress bar and live Hz. Verdict summary panel on top when finished: Passed / Failed / Stopped, counts, duration. +- Device facts panel (key/value rows) below the stages instead of a third column. +- The 3-line footer paragraph shrinks to one sentence with the Discord link. + +### 4.6 Games (Bridge only) + +- Page head with Bridge state. Grid of game tiles (art 16:9, name, "Auto-apply" badge when set) plus a "Request a game" tile that opens `GameRequestDialog`. +- Game profile: the device page frame with the game's art in place of the render and only settings sections (as `gameProfile` already filters). + +### 4.7 Settings + +- Two columns: anchor list on the left (Appearance, Language, Behaviour, Profile key, Developer previews when enabled), panels on the right. +- Theme picker: 12 swatches that each render a tiny preview in that theme's own tokens (sheet, text, accent), not just a dot. +- Language: Select with native names (12 options is too many for a segmented control). +- Behaviour: sound, animations, instant flash, advanced editors, experimental as SettingRows with switches. +- Reset as a ghost button at the bottom. Support links move to the footer. + +### 4.8 Gates and notices + +One `Notice` layout for Unsupported (4 variants) and Window too small: brand mark, headline, one paragraph, one action where an action exists (Download Bridge, Open over HTTPS). Offline banner reuses the news strip style. + +### 4.9 Dialogs, menus, toasts + +- `Dialog` primitive: opaque `--solid-2`, `--r-lg`, menu shadow, title + close, footer actions with one primary, Escape and focus trap. All 11 dialogs move onto it. +- Menus/popovers (`OptionMenu`, ColorPicker): `--solid-2`, `--r-sm`. +- Toasts: bottom-right, solid, icon + text, no glow. + +--- + +## 5. Implementation + +### 5.1 Structure + +- `src/styles/tokens.css`: shape/type/motion + **12 theme blocks**, each defining the full colour set (`--ink-0/1`, `--surface-*`, `--solid-*`, `--line-*`, `--text-1..4`, `--accent`, `--accent-rgb`, `--on-accent`, `--ok/--bad/--warn`, lighting and shadow recipes). Default stays whatever `DEFAULT_INTERFACE_PREFERENCES.theme` says (currently **Emerald**; the decision text said Matt, so confirm). +- `src/styles/base.css`, `primitives.css`, `shell.css`, then one file per area (`device.css`, `performance.css`, `buttons.css`, `lighting.css`, `profiles.css`, `advanced.css`, `hardware-test.css`, `games.css`, `settings.css`, `dialogs.css`). Replaces `control.css`, `control-devices.css`, `control-profiles.css`, `profile-card.css`. +- Primitives in `src/app/ui/`: `Panel`, `SettingRow`, `Figure`, `KeyValue`, `Button`, `Badge`, `EmptyState`, `Dialog`, `DeviceCard`, `SectionNav`. Existing `ui.tsx` controls (`Segmented`, `SwitchButton`, `OptionMenu`, `StepperSlider`, `RateSlider`, `Collapsible`, `BatteryIcon`) keep their props and get restyled, so the 51 cards mostly change only their outer chrome. +- Icons: keep `lucide-react` for now, one stroke width, `currentColor`, no per-icon hues. Swap the set later if the owner wants a different look. +- All new strings through `t()`; add keys to all 12 locales (the `i18n-tr-coverage` test fails otherwise). Logical CSS properties throughout for Arabic RTL. + +### 5.2 Order + +| # | Step | Done when | +|---|---|---| +| 0 | Rewrite `DESIGN.md`, update `AGENTS.md` rules, delete launch gate | Docs merged | +| 1 | Tokens for all 12 themes + base + primitives | Primitive gallery renders in 12 themes, AA contrast measured for `--text-1..3` on `--ink-1` and `--surface-1` per theme | +| 2 | Shell: TopBar, news strip, footer, layout; remove sidebar | Every existing page reachable; Games appears only with Bridge | +| 3 | Devices home + empty + Bridge panel + notices | Preview `list` and a no-device run render clean | +| 4 | Device page frame: DeviceCard, SectionNav, Overview, pending bar | `?preview=razer` overview matches the mock | +| 5 | Section areas in parallel (one worker each): Performance; Buttons/HITS/Magnetic/ButtonMap; Macros + Lighting + Profiles; Advanced + Diagnostics + Firmware + keyboard | Every preview key × every tab renders without overflow | +| 6 | Hardware test, Games, Settings | Click-through recorded | +| 7 | Dialogs, menus, toasts on the shared primitives | Each dialog opens, closes with Escape, traps focus | +| 8 | Optional: URL routes (`/`, `/device/:i/:section`, `/test`, `/games`, `/settings`) with an allowlisted parser like `device/preview/preview-modes.ts` | Back button works; PWA precache test still passes | +| 9 | Optional: settings search (Ctrl K) indexing only cards from `cardAvailability()` | Results jump to and focus the card; not rendered until built | + +### 5.3 Verification + +- Harness: the 36 `?preview=` fixtures already cover most vendors. A Node Playwright script shoots every preview key × every available tab at 1920 × 1080 and 1100 × 800 in Emerald, Light and NieR, plus Arabic once, and logs page overflow and console errors. +- `npm run check` (tsc + vite build + node tests) and `npm run size` (CSS budget should drop well below 225 kB). +- antislop Delivery Gate report with the click-through before anything is called done. + +--- + +## 6. Open questions + +1. Default theme: code default is **Emerald**; the decision text said Matt. Which one? +2. "OpenMouse AI": rename to Quick help in the Help menu, or remove? +3. Add an in-app **Supported devices** table page (data is already in `features/hardware-test/supported-devices-data.ts`), or keep linking to openmouse.app/supported? +4. URL routing (step 8) now, or later? +5. Keep the per-mouse DPI stage colours (M2-NEX `--stage-color-*`, these mirror the mouse's LED colours) as the only multi-hue element? diff --git a/functions/_lib/admin.js b/functions/_lib/admin.js index 3e4db455..7eb8a315 100644 --- a/functions/_lib/admin.js +++ b/functions/_lib/admin.js @@ -22,7 +22,6 @@ export function tokenFrom(request) { return request.headers.get("x-admin-token") ?? ""; } -/** True only when ADMIN_TOKEN is configured and the request presents it. */ export function isAdminRequest(request, env) { const expected = env.ADMIN_TOKEN; if (!expected) return false; diff --git a/functions/_lib/discord-card.js b/functions/_lib/discord-card.js index 48d5c4f7..f94588b1 100644 --- a/functions/_lib/discord-card.js +++ b/functions/_lib/discord-card.js @@ -1,11 +1,3 @@ -// Renders the README's "OpenMouse on Discord" card as an SVG, with live member -// and online counts when Discord answers. -// -// Counts come from Discord's public invite lookup (no bot or token needed): -// `approximate_member_count` and `approximate_presence_count`. When the lookup -// fails, the card falls back to the static tagline so the README never shows a -// broken image. Layout and colors mirror `.github/assets/discord-card.svg`. - export const DISCORD_INVITE_CODE = "yxC9jzMdw6"; const INVITE_API = `https://discord.com/api/v10/invites/${DISCORD_INVITE_CODE}?with_counts=true`; diff --git a/functions/_lib/security-headers.js b/functions/_lib/security-headers.js index 67cc4496..2e9a6af1 100644 --- a/functions/_lib/security-headers.js +++ b/functions/_lib/security-headers.js @@ -3,7 +3,7 @@ // `public/_headers` declares the exact same set for the static assets Pages // serves directly; this module applies it to the Pages Functions responses // (the IP-ban screen, blocked/rate-limited replies, and all /api/* payloads), -// which `_headers` does not cover. `src/security-headers.test.ts` keeps the two +// which `_headers` does not cover. `test/security-headers.test.ts` keeps the two // copies in lockstep so they cannot drift. // // `upgrade-insecure-requests` is deliberately absent. It would rewrite the diff --git a/index.html b/index.html index 94b621e8..9445cabb 100644 --- a/index.html +++ b/index.html @@ -27,6 +27,6 @@
- + diff --git a/package.json b/package.json index 14969cfb..f3a43fc8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "scripts": { "dev": "vite", "build": "tsc --noEmit && vite build", - "test": "node --test \"src/**/*.test.ts\" \"build/**/*.test.ts\"", + "test": "node --test \"src/**/*.test.ts\" \"test/**/*.test.ts\" \"build/**/*.test.ts\"", "size": "node build/check-bundle-size.ts", "check": "npm run build && npm test", "preview": "vite preview" diff --git a/public/news.json b/public/news.json index 0947b060..e5edd02b 100644 --- a/public/news.json +++ b/public/news.json @@ -2,5 +2,5 @@ "id": "2026-09-openmouse-bridge", "level": "info", "message": "OpenMouse Bridge is here: Razer mice work on Windows again, plus game profiles, low-battery alerts and Firefox support. Read the announcement.", - "url": "https://openmouse.app/blog-openmouse-bridge" + "url": "https://openmouse.app/blog/openmouse-bridge" } diff --git a/public/sounds/click.wav b/public/sounds/click.wav deleted file mode 100644 index 23e25b5557a56c1723f65b3d56412ce25525cb5d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32694 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 { const out = `// Supported-devices table, mirrored from the landing page. // // AUTO-GENERATED — do not edit by hand. Source of truth: -// openmouse-landing-page/src/supported-mice.ts +// openmouse-landing-page/src/data/supported-mice.ts // (the table behind openmouse.app/supported, re-checked against the // @openmouse/protocol driver registry every time the landing page builds). // @@ -61,5 +61,5 @@ ${rows.join("\n")} ]; `; -writeFileSync(join(root, "src", "supported-devices-data.ts"), out); -console.log(`Wrote src/supported-devices-data.ts with ${MICE.length} entries from ${source}`); \ No newline at end of file +writeFileSync(join(root, "src", "features", "hardware-test", "supported-devices-data.ts"), out); +console.log(`Wrote src/features/hardware-test/supported-devices-data.ts with ${MICE.length} entries from ${source}`); \ No newline at end of file diff --git a/src/app/AiOverlay.tsx b/src/app/AiOverlay.tsx deleted file mode 100644 index f1d112d4..00000000 --- a/src/app/AiOverlay.tsx +++ /dev/null @@ -1,148 +0,0 @@ -import { displayText } from "../i18n"; -import { Bot, BotMessageSquare, X } from "lucide-react"; -import { useEffect, useRef, useState, type ReactNode } from "react"; -import { t } from "../i18n"; -import type { InterfaceLocale } from "../interface-preferences"; -import { DISCORD_URL } from "./social-links"; - -type TopicId = "connect" | "dpi" | "browser" | "saving" | "other"; - -const TOPIC_ORDER: TopicId[] = ["connect", "dpi", "browser", "saving", "other"]; - -interface Message { - from: "bot" | "user"; - text: string; -} - -export function AiOverlay({ locale = "en" }: { locale?: InterfaceLocale }): ReactNode { - const [open, setOpen] = useState(false); - const [messages, setMessages] = useState([]); - const [typed, setTyped] = useState(true); - const [greeted, setGreeted] = useState(false); - const scrollRef = useRef(null); - const timerRef = useRef(null); - - useEffect(() => { - return () => { - if (timerRef.current !== null) window.clearTimeout(timerRef.current); - }; - }, []); - - useEffect(() => { - if (!open) return; - if (!greeted) { - setGreeted(true); - scheduleAnswer(t(locale, "ai.greeting"), 700); - } - }, [open, greeted, locale]); - - useEffect(() => { - const node = scrollRef.current; - if (node) node.scrollTop = node.scrollHeight; - }, [messages, typed, open]); - - function scheduleAnswer(answer: string, delay: number): void { - setTyped(false); - if (timerRef.current !== null) window.clearTimeout(timerRef.current); - timerRef.current = window.setTimeout(() => { - timerRef.current = null; - setMessages((current) => [...current, { from: "bot", text: answer }]); - setTyped(true); - }, delay); - } - - const topicLabel = (id: TopicId): string => { - switch (id) { - case "connect": return t(locale, "ai.topic1"); - case "dpi": return t(locale, "ai.topic2"); - case "browser": return t(locale, "ai.topic3"); - case "saving": return t(locale, "ai.topic4"); - case "other": return t(locale, "ai.other"); - } - }; - - const topicAnswer = (id: TopicId): string => { - switch (id) { - case "connect": return t(locale, "ai.topic1Ans"); - case "dpi": return t(locale, "ai.topic2Ans"); - case "browser": return t(locale, "ai.topic3Ans"); - case "saving": return t(locale, "ai.topic4Ans"); - case "other": return t(locale, "ai.otherAns"); - } - }; - - function pick(id: TopicId): void { - if (!typed) return; - setMessages((current) => [...current, { from: "user", text: topicLabel(id) }]); - const answer = topicAnswer(id); - scheduleAnswer(answer, 450 + Math.min(answer.length * 6, 2200)); - } - - return ( - <> - - - {open ? ( -
-
-
- - -
- {t(locale, "ai.title")} - {t(locale, "ai.subtitle")} -
-
- -
- -
- {messages.map((message, index) => ( -
- {displayText(locale, message.text)} -
- ))} - {!typed ? ( - - ) : null} -
- -
- {TOPIC_ORDER.map((id) => ( - - ))} -
- - -
- ) : null} - - ); -} \ No newline at end of file diff --git a/src/app/App.tsx b/src/app/App.tsx index bb3ec894..abfacc4d 100644 --- a/src/app/App.tsx +++ b/src/app/App.tsx @@ -1,25 +1,35 @@ -import { displayText } from "../i18n"; +import { displayText } from "../i18n/index"; import { useEffect, useRef, useState, type ReactNode } from "react"; import * as control from "../device/controller"; -import { ensureDisplayLocale } from "../i18n"; -import { interfaceThemeSlug } from "../interface-preferences"; +import { ensureDisplayLocale } from "../i18n/index"; +import { interfaceThemeSlug } from "../features/settings/interface-preferences"; import { AppSidebar, type DesktopPage } from "./AppSidebar"; -import { OverviewPage } from "./OverviewPage"; -import { CaptureDialog } from "./CaptureDialog"; -import { GamesPage, useBridgeActive } from "./GamesPage"; -import { HardwareTestPage } from "./HardwareTestPage"; -import { InterfaceSettings } from "./InterfaceSettings"; -import { NewsBanner } from "./NewsBanner"; +import { OverviewPage } from "../features/devices/OverviewPage"; +import { CaptureDialog } from "../features/capture/CaptureDialog"; +import { GamesPage } from "../features/games/GamesPage"; +import { HardwareTestPage } from "../features/hardware-test/HardwareTestPage"; +import { InterfaceSettings } from "../features/settings/InterfaceSettings"; +import { NewsBanner } from "../features/news/NewsBanner"; import { PendingBar } from "./PendingBar"; -import { ShareProfileDialog } from "./ShareProfileDialog"; -import { WhatsNewDialog } from "./WhatsNewDialog"; -import { AiOverlay } from "./AiOverlay"; +import { HelpPanel } from "./HelpPanel"; +import { ShareProfileDialog } from "../features/profiles/ShareProfileDialog"; +import { WhatsNewDialog } from "../features/news/WhatsNewDialog"; import { ToastHost } from "./Toasts"; -import { useBridgeBatteryReporter } from "./useBridgeBatteryReporter"; -import { useBridgeProfileApplier } from "./useBridgeProfileApplier"; +import { useBridgeBatteryReporter } from "../features/bridge/useBridgeBatteryReporter"; +import { useBridgeProfileApplier } from "../features/bridge/useBridgeProfileApplier"; import { useControl } from "./useControl"; -import { setSoundsEnabled } from "../sound-manager"; -import { UiLocaleContext } from "./ui-locale"; +import { setSoundsEnabled } from "../platform/sound-manager"; +import { UiLocaleContext } from "../shared/ui-locale"; + +const SIDEBAR_KEY = "openmouse-sidebar-collapsed-v1"; + +function initialSidebarCollapsed(): boolean { + try { + return localStorage.getItem(SIDEBAR_KEY) === "1"; + } catch { + return false; + } +} export function App(): ReactNode { const snapshot = useControl(); @@ -30,12 +40,10 @@ export function App(): ReactNode { const [shareProfileOpen, setShareProfileOpen] = useState(false); const [whatsNewOpen, setWhatsNewOpen] = useState(false); const [page, setPage] = useState("home"); - const [sidebarCollapsed, setSidebarCollapsed] = useState(false); + const [sidebarCollapsed, setSidebarCollapsed] = useState(initialSidebarCollapsed); const { preferences, status } = snapshot; const locale = preferences.locale; - const bridgeActive = useBridgeActive(); - // Games only exists while Bridge does — if it goes away, fall back to Home. - const showingGames = page === "games" && bridgeActive; + const showingGames = page === "games"; const showingSettings = page === "settings" || snapshot.interfaceSettingsOpen; @@ -43,9 +51,9 @@ export function App(): ReactNode { ? "settings" : page === "hardware-test" ? "hardware-test" - : showingGames - ? "games" - : (status !== null || snapshot.keyboard !== null) && snapshot.deviceView === "device" + : showingGames + ? "games" + : (status !== null || snapshot.keyboard !== null) && snapshot.deviceView === "device" ? "dashboard" : "home"; @@ -69,13 +77,11 @@ export function App(): ReactNode { if (document.documentElement.lang !== nextLang) { document.documentElement.lang = nextLang; } - // Arabic reads right-to-left; every other locale is left-to-right. const nextDir = locale === "ar" ? "rtl" : "ltr"; if (document.documentElement.dir !== nextDir) { document.documentElement.dir = nextDir; } } catch { - /* non-DOM environment (tests) */ } // A stored non-English locale resolves its table after first paint; // re-render once it arrives instead of sticking on fallback strings. @@ -85,19 +91,32 @@ export function App(): ReactNode { }); }, [locale]); + useEffect(() => { + try { + document.documentElement.dataset.interfaceTheme = interfaceThemeSlug(preferences.theme); + } catch { + } + }, [preferences.theme]); useEffect(() => { panel.current?.scrollTo({ top: 0, behavior: preferences.reducedMotion ? "auto" : "smooth" }); }, [page, snapshot.workspaceTab, preferences.reducedMotion]); - useEffect(() => { - setSidebarCollapsed(resolvedPage === "dashboard" || resolvedPage === "hardware-test"); - }, [resolvedPage]); - useEffect(() => { setSoundsEnabled(preferences.enabledSounds); }, [preferences.enabledSounds]); + const toggleSidebar = (): void => { + setSidebarCollapsed((current) => { + const next = !current; + try { + localStorage.setItem(SIDEBAR_KEY, next ? "1" : "0"); + } catch { + } + return next; + }); + }; + function navigate(next: DesktopPage): void { if (next === "settings") { control.openInterfaceSettings(); @@ -120,12 +139,19 @@ export function App(): ReactNode { } } + const openDevice = (index: number): void => { + control.closeInterfaceSettings(); + setPage("home"); + void control.openDeviceOverview(index); + }; + + return (
- - setSidebarCollapsed(!sidebarCollapsed)} onNavigate={navigate} onOpenWhatsNew={() => setWhatsNewOpen(true)} /> + setWhatsNewOpen(true)} + onOpenDevice={openDevice} + /> -
-
+
+ +
{showingSettings ? ( ) : page === "hardware-test" ? ( @@ -161,7 +195,7 @@ export function App(): ReactNode { setCaptureOpen(false)} locale={locale} /> setWhatsNewOpen(false)} locale={locale} /> setShareProfileOpen(false)} snapshot={snapshot} /> - +
diff --git a/src/app/AppSidebar.tsx b/src/app/AppSidebar.tsx index e345a24f..186b1d57 100644 --- a/src/app/AppSidebar.tsx +++ b/src/app/AppSidebar.tsx @@ -1,21 +1,120 @@ -import { displayText } from "../i18n"; -import { ChevronLeft, ChevronRight, FileText, FlaskConical, Gamepad2, House, Mouse, Settings as SettingsIcon, Star, type LucideIcon } from "lucide-react"; +import { DISCORD_URL, DiscordIcon } from "../shared/social-links"; +import { displayText, t } from "../i18n/index"; +import { + Activity, + BookOpen, + ChevronLeft, + ChevronRight, + Gamepad2, + LayoutGrid, + Megaphone, + Settings as SettingsIcon, + type LucideIcon, +} from "lucide-react"; import { type ReactNode } from "react"; import type { ControlSnapshot } from "../device/types"; -import { t } from "../i18n"; -import { useBridgeActive } from "./GamesPage"; -import { DISCORD_URL, DiscordIcon } from "./social-links"; +import { deviceImage } from "../shared/device-images"; +import { BridgeStatusButton } from "../features/bridge/BridgeStatus"; +import { BatteryIcon } from "../shared/ui"; export const OPENMOUSE_URL = "https://openmouse.app/"; export type DesktopPage = "home" | "dashboard" | "hardware-test" | "games" | "settings"; -function NavIcon({ icon: Icon, color }: { icon: LucideIcon; color: string }): ReactNode { - return