Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion build/check-bundle-size.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@ const BUDGET_BYTES: Record<string, number> = {
// one block of component-level overrides — no UI framework was added.
// Raised from 72 kB for the Mouse Check page CSS bundled alongside the
// themes in the aggregate main + check stylesheet.
".css": 78_000,
// Raised from 78 kB for the accessible theme-card grid and responsive live
// workspace preview. The existing theme tokens drive it; no dependency was added.
".css": 79_000,
// Raised from 280 kB for the production Logitech onboard-profile codec,
// guarded flash editor, verification exporter, upstream Finalmouse driver,
// the dedicated Viper Mini protocol driver, Viper V3 sleep/low-power plus
Expand Down
8 changes: 2 additions & 6 deletions src/control-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ export interface ControlEventHandlers {
selectAuthorizedDevice(index: number): Promise<void>;
openInterfaceSettings(): void;
closeInterfaceSettings(): void;
setInterfaceDensity(value: string): void;
setInterfaceTheme(value: string): void;
setReducedMotion(enabled: boolean): void;
setExpandSections(enabled: boolean): void;
Expand Down Expand Up @@ -107,11 +106,8 @@ export function bindControlEvents(handlers: ControlEventHandlers): void {
onClick("#interface-settings-button", handlers.openInterfaceSettings);
onClick("#close-interface-settings", handlers.closeInterfaceSettings);

document.querySelector<HTMLSelectElement>("#interface-density")?.addEventListener("change", (event) => {
handlers.setInterfaceDensity((event.target as HTMLSelectElement).value);
});
document.querySelector<HTMLSelectElement>("#interface-theme")?.addEventListener("change", (event) => {
handlers.setInterfaceTheme((event.target as HTMLSelectElement).value);
document.querySelectorAll<HTMLInputElement>('input[name="interface-theme"]').forEach((input) => {
input.addEventListener("change", () => handlers.setInterfaceTheme(input.value));
});
document.querySelector<HTMLInputElement>("#interface-reduced-motion")?.addEventListener("change", (event) => {
handlers.setReducedMotion((event.target as HTMLInputElement).checked);
Expand Down
12 changes: 10 additions & 2 deletions src/control-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@
const TOGGLE_ROW = "switch-row";
const FIELD_LABEL = "field-label";
const CARD_HEADING = "setting-heading compact";
const THEME_CHOICES = [
["Emerald", "#69d28d"], ["Violet", "#a78bfa"], ["Ice", "#67d8ff"],
["Ember", "#ff9b62"], ["Mono", "#f1f1f3"], ["Miku", "#39c5bb"],
["Catppuccin Mocha", "#cba6f7"], ["Catppuccin Macchiato", "#c6a0f6"], ["Catppuccin Frappé", "#ca9ee6"],
] as const;

function themeChoices(): string {
return THEME_CHOICES.map(([name, swatch]) => `<label class="theme-choice" style="--theme-swatch:${swatch}"><input type="radio" name="interface-theme" value="${name}" /><i aria-hidden="true"></i><span>${name}</span></label>`).join("");
}

function superstrikeSteps(id: string, min: number, max: number): string {
return `<div class="superstrike-steps" role="group" aria-label="${id.replace("logitech-", "").replaceAll("-", " ")}"><input id="${id}" type="hidden" /><div>${Array.from({ length: max - min + 1 }, (_, index) => {
Expand Down Expand Up @@ -134,8 +143,7 @@ export function controlTemplate(buildLabel: string): string {
<section id="interface-settings-page" class="interface-settings-page" aria-labelledby="interface-settings-title">
<header class="interface-settings-header"><div><p class="overline">OPENMOUSE</p><h2 id="interface-settings-title">Interface settings</h2></div><button id="close-interface-settings" class="interface-settings-back" type="button">Back to device</button></header>
<div class="interface-settings-grid">
<article class="interface-setting-card"><span>LAYOUT</span><h3>Interface density</h3><p>Choose tighter controls or add more breathing room throughout the panel.</p><select id="interface-density"><option>Compact</option><option>Comfortable</option></select></article>
<article class="interface-setting-card"><span>APPEARANCE</span><h3>Accent theme</h3><p>Customize active controls, status lights, switches, and focus highlights.</p><select id="interface-theme"><option>Emerald</option><option>Violet</option><option>Ice</option><option>Ember</option><option>Mono</option><option>Miku</option><option>Catppuccin Mocha</option><option>Catppuccin Macchiato</option><option>Catppuccin Frappé</option></select><div class="theme-preview" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></div></article>
<article class="interface-setting-card interface-theme-card"><span>APPEARANCE</span><h3>Accent theme</h3><p>Choose a theme and preview it across a miniature OpenMouse workspace.</p><div class="theme-studio"><fieldset id="interface-theme" class="theme-choices" aria-label="Accent theme">${themeChoices()}</fieldset><div class="theme-demo"><div class="theme-demo-window" aria-hidden="true"><header>OPENMOUSE</header><div class="theme-demo-body"><nav><i></i><i></i><i></i></nav><main><small>PROFILE 1</small><strong>Performance</strong><div><span>Polling rate</span><b>1000 Hz</b></div><em>Apply changes</em></main><img id="miku-theme-preview-mascot" data-src="/miku-mascot.gif" alt="" /></div></div><p id="theme-preview-name" aria-live="polite">Mono preview</p></div></div></article>
<article class="interface-setting-card"><span>MOTION</span><h3>Animation</h3><p>Disable interface transitions and animated state changes.</p><label class="interface-switch-row"><span>Reduce motion</span><input id="interface-reduced-motion" type="checkbox" /></label></article>
<article class="interface-setting-card"><span>WRITES</span><h3>Instant flash</h3><p>Write each change to the mouse as soon as you make it, instead of staging it for the flash bar.</p><label class="interface-switch-row"><span>Flash immediately</span><input id="interface-instant-flash" type="checkbox" /></label></article>
<article class="interface-setting-card"><span>SECTIONS</span><h3>Advanced editors</h3><p>Choose whether CPI, button mapping, and experimental sections begin expanded.</p><label class="interface-switch-row"><span>Expand by default</span><input id="interface-expand-sections" type="checkbox" /></label></article>
Expand Down
46 changes: 25 additions & 21 deletions src/control.css
Original file line number Diff line number Diff line change
Expand Up @@ -206,10 +206,9 @@ button:focus-visible, a:focus-visible, input:focus-visible { outline: 2px solid
.control-shell[data-interface-theme="miku"] .pending-flash:hover:not(:disabled) { background: #159a96; border-color: #159a96; filter: none; }
/* The pending-flash bar follows the accent in Miku rather than the mono white. */
.control-shell[data-interface-theme="miku"] .pending-flash { border-color: var(--ui-accent); background: var(--ui-accent); color: var(--ui-accent-ink); }
/* Selected tab underline and the accent-preview tip read as soft cyan/pink. */
/* Selected tab underline reads as soft cyan. */
.control-shell[data-interface-theme="miku"] .workspace-tabs button[aria-selected="true"]::after { background: var(--miku-soft-cyan); }
.control-shell[data-interface-theme="miku"] .theme-preview i:last-child { background: var(--miku-pink); }
.control-shell[data-interface-theme="miku"] .theme-preview i:nth-child(4) { background: var(--miku-soft-cyan); }
.control-shell[data-interface-theme="miku"] #miku-theme-preview-mascot { display: block; }

/* ============================================================
Catppuccin theme family — Mocha, Macchiato and Frappé: three
Expand Down Expand Up @@ -406,12 +405,6 @@ button:focus-visible, a:focus-visible, input:focus-visible { outline: 2px solid
background: color-mix(in srgb, var(--destructive) 30%, var(--ctp-surface0));
color: var(--ctp-text);
}
/* Theme preview: a surface ramp closing on blue and mauve. */
.control-shell[data-interface-theme^="catppuccin-"] .theme-preview i:nth-child(1) { background: var(--ctp-surface0); opacity: 1; }
.control-shell[data-interface-theme^="catppuccin-"] .theme-preview i:nth-child(2) { background: var(--ctp-surface1); opacity: 1; }
.control-shell[data-interface-theme^="catppuccin-"] .theme-preview i:nth-child(3) { background: var(--ctp-surface2); opacity: 1; }
.control-shell[data-interface-theme^="catppuccin-"] .theme-preview i:nth-child(4) { background: var(--ctp-blue); opacity: 1; }
.control-shell[data-interface-theme^="catppuccin-"] .theme-preview i:nth-child(5) { background: var(--ctp-mauve); opacity: 1; }
.control-shell button:focus-visible, .control-shell select:focus-visible, .control-shell input:focus-visible { outline: 2px solid var(--ui-accent); outline-offset: 2px; }

.control-shell { display: grid; grid-template-columns: 260px minmax(0, 1fr); min-height: 100vh; }
Expand Down Expand Up @@ -623,12 +616,28 @@ nav { display: grid; gap: .3rem; margin-top: 1.4rem; }
.interface-setting-card h3 { margin: .35rem 0 .3rem; font-size: 1rem; font-weight: 550; }
.interface-setting-card p { margin: 0 0 .8rem; color: var(--muted); font-size: .68rem; line-height: 1.45; }
.interface-setting-card select { width: 100%; padding: .52rem; border: 1px solid var(--border); border-radius: 7px; background: var(--input); color: var(--text-input); color-scheme: dark; }
.theme-preview { display: flex; gap: .32rem; margin-top: .65rem; }
.theme-preview i { width: 1.25rem; height: .28rem; border-radius: 999px; background: var(--ui-accent); opacity: .2; }
.theme-preview i:nth-child(2) { opacity: .35; }
.theme-preview i:nth-child(3) { opacity: .55; }
.theme-preview i:nth-child(4) { opacity: .75; }
.theme-preview i:nth-child(5) { opacity: 1; }
.interface-theme-card { grid-column: 1 / -1; }
.theme-studio { display: grid; grid-template-columns: minmax(0, .9fr) minmax(280px, 1.1fr); gap: 1rem; }
.theme-choices { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: .4rem; margin: 0; padding: 0; border: 0; }
.theme-choice { position: relative; display: flex; min-width: 0; align-items: center; gap: .5rem; padding: .52rem .6rem; border: 1px solid var(--border); border-radius: 7px; background: var(--surface-control); color: var(--text-strong); font-size: .68rem; cursor: pointer; }
.theme-choice input { position: absolute; opacity: 0; }
.theme-choice i { width: .62rem; height: .62rem; flex: 0 0 auto; border-radius: 50%; background: var(--theme-swatch); }
.theme-choice:has(input:checked) { border-color: var(--ui-accent); background: var(--ui-accent-soft); color: var(--text-hover); }
.theme-choice:has(input:focus-visible) { outline: 2px solid var(--ui-accent); outline-offset: 2px; }
.theme-demo-window { overflow: hidden; border: 1px solid var(--border-hover); border-radius: 8px; background: var(--surface-bg); }
.theme-demo-window > header { padding: .45rem .6rem; border-bottom: 1px solid var(--border); background: var(--surface-sidebar); color: var(--text-faint); font-family: var(--font-mono); font-size: .48rem; letter-spacing: .08em; }
.theme-demo-body { position: relative; display: grid; min-height: 132px; grid-template-columns: 28% 1fr; }
.theme-demo-body > nav { display: grid; align-content: start; gap: .45rem; margin: 0; padding: .8rem .6rem; border-right: 1px solid var(--border); background: var(--surface-sidebar); }
.theme-demo-body > nav i { height: .28rem; border-radius: 9px; background: var(--surface-raised); }
.theme-demo-body > nav i:first-child { background: var(--ui-accent); }
.theme-demo-body > main { display: flex; min-width: 0; flex-direction: column; padding: .75rem; }
.theme-demo-body small { color: var(--text-faint); font-family: var(--font-mono); font-size: .45rem; }
.theme-demo-body strong { margin: .15rem 0 .55rem; color: var(--text-hover); font-size: .8rem; }
.theme-demo-body main div { display: flex; justify-content: space-between; gap: .5rem; padding: .45rem; border: 1px solid var(--border); border-radius: 5px; background: var(--surface-panel); color: var(--text-soft); font-size: .52rem; }
.theme-demo-body main b { color: var(--text-strong); }
.theme-demo-body main em { align-self: flex-end; margin-top: auto; padding: .3rem .45rem; border-radius: 4px; background: var(--ui-accent); color: var(--ui-accent-ink); font-size: .48rem; font-style: normal; font-weight: 700; }
#miku-theme-preview-mascot { position: absolute; right: 5.3rem; bottom: .2rem; display: none; width: 2.8rem; pointer-events: none; }
.interface-setting-card .theme-demo > p { margin: .35rem 0 0; color: var(--text-faint); font-family: var(--font-mono); font-size: .5rem; text-align: right; }
.interface-switch-row { display: flex; align-items: center; justify-content: space-between; gap: .8rem; margin-top: .7rem; color: var(--text-strong); font-size: .72rem; }
.interface-switch-row input {
appearance: none;
Expand All @@ -648,12 +657,6 @@ nav { display: grid; gap: .3rem; margin-top: 1.4rem; }
.interface-switch-row input:focus-visible { box-shadow: 0 0 0 3px var(--ui-accent-soft); }
.interface-reset { margin-top: .75rem; padding: .55rem .75rem; border: 1px solid #4a3436; border-radius: 7px; background: #1c1315; color: #e7a7aa; font-size: .68rem; font-weight: 650; }

.density-comfortable #pulsar-advanced.egg-advanced-layout { gap: .8rem !important; }
.density-comfortable #pulsar-advanced.egg-advanced-layout > .setting-card { padding: 1rem !important; }
.density-comfortable .settings-grid { gap: .9rem !important; }
.density-comfortable .settings-grid .setting-card { padding: 1.1rem !important; }
.density-comfortable .device-overview .summary-stat { padding: 1rem 1.2rem !important; }
.density-comfortable .egg-collapsible summary, .density-comfortable .egg-experimental summary { padding: .95rem 1rem; }
.reduce-interface-motion *, .reduce-interface-motion *::before, .reduce-interface-motion *::after { scroll-behavior: auto !important; transition: none !important; animation: none !important; }

.small-screen-blocker { display: flex; min-height: 100vh; align-items: flex-start; justify-content: center; flex-direction: column; padding: clamp(2rem, 8vw, 7rem); }
Expand Down Expand Up @@ -728,6 +731,7 @@ nav { display: grid; gap: .3rem; margin-top: 1.4rem; }

@media (max-width: 720px) {
.interface-settings-grid { grid-template-columns: 1fr; }
.theme-studio { grid-template-columns: 1fr; }
}

@media (prefers-reduced-motion: reduce) {
Expand Down
21 changes: 8 additions & 13 deletions src/control.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ import {
loadInterfacePreferences,
saveInterfacePreferences as persistInterfacePreferences,
interfaceThemeSlug,
type InterfaceDensity,
type InterfaceTheme,
} from "./interface-preferences";
import {
Expand Down Expand Up @@ -346,17 +345,19 @@ function applyInterfacePreferences(): void {
setPendingBarSuppressed(interfacePreferences.instantFlash);
const shell = document.querySelector<HTMLElement>(".control-shell");
if (!shell) return;
shell.classList.toggle("density-comfortable", interfacePreferences.density === "Comfortable");
shell.classList.toggle("reduce-interface-motion", interfacePreferences.reducedMotion);
shell.classList.toggle("sidebar-hidden", sidebarHidden);
const menuToggle = document.querySelector<HTMLButtonElement>("#sidebar-menu-toggle");
if (menuToggle) menuToggle.setAttribute("aria-pressed", String(!sidebarHidden));
shell.dataset.interfaceTheme = interfaceThemeSlug(interfacePreferences.theme);
const mascot = document.querySelector<HTMLImageElement>("#miku-mascot");
const mascotSource = mascot?.dataset.src;
if (interfacePreferences.theme === "Miku" && mascot && mascotSource && !mascot.getAttribute("src")) {
mascot.src = mascotSource;
}
document.querySelectorAll<HTMLInputElement>('input[name="interface-theme"]').forEach((input) => {
input.checked = input.value === interfacePreferences.theme;
});
setText("#theme-preview-name", `${interfacePreferences.theme} preview`);
if (interfacePreferences.theme === "Miku") document.querySelectorAll<HTMLImageElement>("#miku-mascot, #miku-theme-preview-mascot").forEach((mascot) => {
const source = mascot.dataset.src;
if (source && !mascot.getAttribute("src")) mascot.src = source;
});
document.querySelectorAll<HTMLDetailsElement>(".egg-collapsible, .egg-experimental").forEach((details) => {
details.open = interfacePreferences.expandSections;
});
Expand Down Expand Up @@ -482,10 +483,6 @@ function renderControl(): void {
selectAuthorizedDevice,
openInterfaceSettings,
closeInterfaceSettings,
setInterfaceDensity: (value) => {
interfacePreferences.density = value as InterfaceDensity;
saveInterfacePreferences();
},
setInterfaceTheme: (value) => {
interfacePreferences.theme = value as InterfaceTheme;
saveInterfacePreferences();
Expand Down Expand Up @@ -779,8 +776,6 @@ async function populatePreviewLauncher(): Promise<void> {

function populateInterfaceSettings(): void {
void populatePreviewLauncher();
setControlValue("#interface-density", interfacePreferences.density);
setControlValue("#interface-theme", interfacePreferences.theme);
const reducedMotion = document.querySelector<HTMLInputElement>("#interface-reduced-motion");
const expandSections = document.querySelector<HTMLInputElement>("#interface-expand-sections");
const instantFlash = document.querySelector<HTMLInputElement>("#interface-instant-flash");
Expand Down
2 changes: 0 additions & 2 deletions src/interface-preferences.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ class MemoryStorage implements Storage {
test("interface preferences restore only supported values", () => {
const storage = new MemoryStorage();
saveInterfacePreferences(storage, {
density: "Comfortable",
theme: "Violet",
reducedMotion: true,
expandSections: true,
Expand All @@ -31,7 +30,6 @@ test("interface preferences restore only supported values", () => {
});

assert.deepEqual(loadInterfacePreferences(storage), {
density: "Comfortable",
theme: "Violet",
reducedMotion: true,
expandSections: true,
Expand Down
4 changes: 0 additions & 4 deletions src/interface-preferences.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
export type InterfaceDensity = "Compact" | "Comfortable";
export type InterfaceTheme =
| "Emerald"
| "Violet"
Expand All @@ -11,7 +10,6 @@ export type InterfaceTheme =
| "Catppuccin Frappé";

export interface InterfacePreferences {
density: InterfaceDensity;
theme: InterfaceTheme;
reducedMotion: boolean;
expandSections: boolean;
Expand All @@ -33,7 +31,6 @@ const THEMES: readonly InterfaceTheme[] = [
];

export const DEFAULT_INTERFACE_PREFERENCES: InterfacePreferences = {
density: "Compact",
theme: "Mono",
reducedMotion: false,
expandSections: false,
Expand All @@ -45,7 +42,6 @@ export function loadInterfacePreferences(storage: Storage): InterfacePreferences
try {
const saved = JSON.parse(storage.getItem(STORAGE_KEY) ?? "{}") as Partial<InterfacePreferences>;
return {
density: saved.density === "Comfortable" ? "Comfortable" : "Compact",
theme: THEMES.includes(saved.theme as InterfaceTheme) ? saved.theme as InterfaceTheme : "Mono",
reducedMotion: saved.reducedMotion === true,
expandSections: saved.expandSections === true,
Expand Down