diff --git a/control-app.html b/control-app.html index 89d69126..1a80ff3a 100644 --- a/control-app.html +++ b/control-app.html @@ -3,11 +3,11 @@ - + - + OpenMouse Control diff --git a/control.html b/control.html index a6e2a286..17de87a7 100644 --- a/control.html +++ b/control.html @@ -3,11 +3,11 @@ - + - + OpenMouse License diff --git a/demo.html b/demo.html index 221a6cae..0c9770f5 100644 --- a/demo.html +++ b/demo.html @@ -3,13 +3,13 @@ - + - + diff --git a/index.html b/index.html index e80fd1a4..7e8f9712 100644 --- a/index.html +++ b/index.html @@ -3,10 +3,10 @@ - + - + diff --git a/package.json b/package.json index 0bdac19c..f5e178f0 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "dev": "vite", "build": "tsc --noEmit && vite build", "test": "node --test src/egg-we-protocol.test.ts src/egg-op1-protocol.test.ts", - "preview": "vite preview" + "preview": "vite preview", + "test:landing": "node --test tests/landing-page.test.mjs" }, "devDependencies": { "typescript": "^5.8.3", diff --git a/src/control-gate.ts b/src/control-gate.ts index 994b7bc1..3f5bce49 100644 --- a/src/control-gate.ts +++ b/src/control-gate.ts @@ -16,25 +16,24 @@ function escapeMarkup(value: string): string { function renderGate(message = ""): void { appRoot.innerHTML = ` - -
-
- OpenMouse - ${__BUILD_CHANNEL__.toUpperCase()} · v${__APP_VERSION__} -
-

PRIVATE CONTROL PANEL

-

Enter your license.

-

Use your OpenMouse license code to unlock device control.

-
- - - - ${escapeMarkup(message)} -
+
+
+ OpenMouse + ${__BUILD_CHANNEL__.toUpperCase()} · v${__APP_VERSION__} +
+
+ +

Private control panel

+

Enter your license.

+

Use your OpenMouse license code to unlock verified device controls in this browser.

+
+ + + + ${escapeMarkup(message)} +
+
+

Your code is used only to validate access to the private control panel.

`; document.querySelector("#access-form")?.addEventListener("submit", async (event) => { diff --git a/src/control.css b/src/control.css index cf86b813..1aeed2e8 100644 --- a/src/control.css +++ b/src/control.css @@ -1,24 +1,21 @@ @import "./demo.css"; -.access-gate { - display: flex; - min-height: 100vh; - align-items: flex-start; - justify-content: center; - flex-direction: column; - width: min(100% - 3rem, 520px); - margin: 0 auto; -} - -.access-gate .demo-wordmark { margin-bottom: 5rem; } -.access-gate h1 { margin: 0; font-size: clamp(2.8rem, 8vw, 4.6rem); font-weight: 500; letter-spacing: -.04em; line-height: .92; } -.access-gate > p:not(.overline) { max-width: 420px; color: #96969b; line-height: 1.6; } -.access-form { display: grid; width: min(100%, 360px); gap: .6rem; margin-top: 1.4rem; } -.access-form label { color: #96969b; font-size: .72rem; } -.access-form input { padding: .8rem .9rem; border: 1px solid #36363a; border-radius: 8px; outline: none; background: #161618; color: #f7f7f7; } -.access-form input:focus { border-color: #f7f7f7; } -.access-form button { padding: .8rem .9rem; border: 0; border-radius: 8px; background: #f7f7f7; color: #09090b; font-weight: 600; } -.access-form output { min-height: 1.2rem; color: #ee8c85; font-size: .72rem; } +.access-shell { display: grid; width: min(calc(100% - 2rem), 760px); min-height: 100vh; margin: 0 auto; padding: 1.5rem 0; grid-template-rows: auto 1fr auto; } +.access-header { display: flex; align-items: center; justify-content: space-between; min-height: 48px; } +.access-header .demo-wordmark { margin: 0; } +.build-meta { color: var(--muted); font-family: inherit; font-size: .58rem; font-weight: 600; letter-spacing: -0.15px; line-height: 1; text-transform: uppercase; white-space: nowrap; } +.access-gate { align-self: center; width: min(100%, 540px); padding: clamp(2rem, 7vw, 4rem); border: 1px solid var(--border); border-radius: var(--radius-lg); background: radial-gradient(circle at 0 0, rgb(255 255 255 / 7%), transparent 38%), var(--surface-deep); box-shadow: 0 30px 80px rgb(0 0 0 / 32%); } +.access-mark { display: grid; width: 42px; height: 42px; margin-bottom: 3.5rem; place-items: center; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); font-size: .68rem; font-weight: 600; } +.access-gate h1 { margin: 0; font-size: clamp(2.8rem, 8vw, 4.6rem); font-weight: 500; letter-spacing: -0.15px; line-height: .94; } +.access-gate > p:not(.overline) { max-width: 420px; margin: 1.25rem 0 0; color: var(--muted); line-height: 1.65; } +.access-form { display: grid; width: 100%; gap: .65rem; margin-top: 2rem; } +.access-form label { color: var(--muted); font-size: .7rem; } +.access-form input { min-height: 46px; padding: 0 .9rem; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); outline: none; background: var(--surface); color: var(--text); } +.access-form input:focus { border-color: var(--text); box-shadow: 0 0 0 3px rgb(255 255 255 / 7%); } +.access-form button { min-height: 46px; padding: 0 .9rem; border: 0; border-radius: var(--radius-sm); background: var(--text); color: var(--background); font-weight: 600; } +.access-form button:disabled { cursor: wait; opacity: .65; } +.access-form output { min-height: 1.2rem; color: var(--negative); font-size: .7rem; line-height: 1.5; } +.access-footnote { align-self: end; margin: 1rem 0 0; color: var(--muted); font-size: .64rem; text-align: center; } .control-shell { display: grid; grid-template-columns: 260px minmax(0, 1fr); min-height: 100vh; } .device-select { cursor: default; } .setting-card button:disabled { cursor: default; opacity: 1; } @@ -93,3 +90,96 @@ .control-shell .sidebar-footer { display: none; } .control-shell .control-panel { width: min(100% - 2rem, 1180px); } } + +/* Neutral precision control surface */ +.control-shell { background: var(--background); color: var(--text); } +.control-shell .sidebar { border-color: var(--border); background: var(--surface-deep); } +.control-shell .device-select, +.control-shell .preview-banner, +.control-shell .setting-card, +.control-shell .panel-footer, +.control-shell .egg-collapsible, +.control-shell .egg-experimental, +.control-shell .interface-setting-card { border-color: var(--border); background: var(--surface-deep); } +.control-shell .device-overview { border-color: var(--border); border-radius: var(--radius-lg); background: var(--surface-deep); } +.control-shell .summary-stat, +.control-shell .quick-stats, +.control-shell .toggle-row, +.control-shell .interface-settings-header, +.control-shell .egg-collapsible-body, +.control-shell .egg-experimental-body { border-color: var(--border) !important; } +.control-shell .setting-card, +.control-shell .egg-collapsible, +.control-shell .egg-experimental, +.control-shell .interface-setting-card { border-radius: var(--radius-md) !important; } +.control-shell .preview-banner, +.control-shell .panel-footer, +.control-shell .sidebar-action, +.control-shell .empty-connect-action, +.control-shell .setting-action button, +.control-shell .dpi-header-actions button, +.control-shell .dpi-header-actions input, +.control-shell .segmented button, +.control-shell .egg-action-button, +.control-shell [data-apply-cpi], +.control-shell .interface-settings-back, +.control-shell .interface-reset, +.control-shell input:not([type="checkbox"]):not([type="range"]), +.control-shell select { border-radius: var(--radius-sm) !important; } +.control-shell .sidebar-action, +.control-shell .dpi-header-actions button, +.control-shell .dpi-header-actions input, +.control-shell .segmented button, +.control-shell .egg-action-button, +.control-shell [data-apply-cpi], +.control-shell .interface-settings-back, +.control-shell input:not([type="checkbox"]):not([type="range"]), +.control-shell select { border-color: var(--border-strong) !important; background: var(--surface) !important; color: var(--text) !important; } +.control-shell .sidebar-action:hover, +.control-shell .segmented button:hover, +.control-shell .egg-action-button:hover, +.control-shell [data-apply-cpi]:hover { background: var(--surface-hover) !important; } +.control-shell .segmented button.selected, +.control-shell .setting-action button:not(:disabled), +.control-shell .dpi-header-actions button:not(:disabled) { border-color: var(--ui-accent) !important; background: var(--ui-accent) !important; color: var(--ui-accent-ink) !important; } +.control-shell .device-label, +.control-shell .overline, +.control-shell .summary-stat > span, +.control-shell .setting-heading p, +.control-shell .setting-note, +.control-shell .range-labels, +.control-shell .sidebar-footer, +.control-shell .panel-footer, +.control-shell .interface-setting-card > span, +.control-shell .interface-setting-card p { color: var(--muted); font-family: inherit; letter-spacing: -0.15px; } +.control-shell .panel-header h1, +.control-shell .empty-state h2, +.control-shell .interface-settings-header h2 { letter-spacing: -0.15px; } +.control-shell .interface-settings-page { background: var(--background); } +.control-shell.is-empty .empty-state { border-color: var(--border); border-radius: var(--radius-lg); background: radial-gradient(circle at 76% 48%, var(--ui-accent-soft), transparent 23%), linear-gradient(135deg, var(--surface), var(--surface-deep) 68%); } +.control-shell .device-dot.is-idle, +.control-shell .status-dot.is-idle { background: var(--muted); } +.control-shell .panel-footer #read-status, +.control-shell .device-status, +.control-shell .empty-state > p:not(.overline), +.control-shell .empty-state small { color: var(--muted); } +.control-scroll-region { position: relative; overflow-y: auto; } +.compact-action { align-self: flex-start; min-height: 28px; margin-top: .45rem; padding: 0 .55rem; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface); color: var(--text); font-size: .61rem; font-weight: 600; } +.device-detail-item, +.device-field-card { min-width: 0; padding: .6rem; border: 1px solid var(--border); border-radius: var(--radius-sm); background: var(--surface); } +.device-detail-item small { display: block; margin-bottom: .25rem; color: var(--muted); font-size: .52rem; } +.device-detail-item span { color: var(--text); font-size: .67rem; font-weight: 600; overflow-wrap: anywhere; } +.device-field-card > strong { font-size: .7rem; } +.device-field-card label { display: block; margin-top: .4rem; color: var(--muted); font-size: .58rem; } +.device-field-card input:not([type="checkbox"]), +.device-field-card select { width: 100%; min-height: 34px; box-sizing: border-box; margin-top: .18rem; padding: 0 .45rem; border: 1px solid var(--border-strong); border-radius: var(--radius-sm); background: var(--surface-deep); color: var(--text); } +.device-field-card .check-field { display: flex; align-items: center; gap: .35rem; font-size: .62rem; } +.paired-fields { display: grid; grid-template-columns: 1fr 1fr; gap: .35rem; } +.device-field-card [data-apply-cpi] { min-height: 30px; margin-top: .45rem; padding: 0 .55rem; } + +@media (max-width: 560px) { + .access-shell { padding-top: 1rem; } + .access-header { align-items: flex-start; gap: .75rem; } + .access-gate { padding: 1.5rem; } + .access-mark { margin-bottom: 2.5rem; } +} diff --git a/src/control.ts b/src/control.ts index 9f94816b..48991a32 100644 --- a/src/control.ts +++ b/src/control.ts @@ -142,7 +142,7 @@ function renderControl(): void { .control-shell { --ui-accent:#69d28d;--ui-accent-ink:#07120b;--ui-accent-soft:rgb(105 210 141 / 16%) } .build-identity { display:flex;align-items:center;gap:.65rem;margin-bottom:4rem } .build-identity .demo-wordmark { margin-bottom:0 } - .build-badge { display:inline-flex;align-items:center;min-height:1.35rem;padding:.2rem .45rem;border:1px solid color-mix(in srgb,var(--ui-accent) 35%,transparent);border-radius:999px;background:var(--ui-accent-soft);color:var(--ui-accent);font-family:"JetBrains Mono",ui-monospace,monospace;font-size:.52rem;font-weight:700;letter-spacing:.07em;line-height:1;white-space:nowrap } + .build-meta { color:var(--muted);font-family:inherit;font-size:.52rem;font-weight:700;letter-spacing:-.15px;line-height:1;text-transform:uppercase;white-space:nowrap } .control-shell[data-interface-theme="violet"] { --ui-accent:#a78bfa;--ui-accent-ink:#130b25;--ui-accent-soft:rgb(167 139 250 / 17%) } .control-shell[data-interface-theme="ice"] { --ui-accent:#67d8ff;--ui-accent-ink:#06161d;--ui-accent-soft:rgb(103 216 255 / 16%) } .control-shell[data-interface-theme="ember"] { --ui-accent:#ff9b62;--ui-accent-ink:#211006;--ui-accent-soft:rgb(255 155 98 / 17%) } @@ -213,7 +213,7 @@ function renderControl(): void { #egg-cpi-stage-list, #egg-button-list { grid-template-columns:1fr !important } } .interface-settings-button::before { content:"⚙";font-size:.72rem } - .interface-settings-page { position:absolute;z-index:20;inset:0;display:none;padding:1rem 0 2rem;overflow:auto;background:#0b0b0d;scrollbar-width:none } + .interface-settings-page { position:absolute;z-index:20;inset:0;display:none;padding:1rem 0 2rem;overflow:auto;background:var(--background);scrollbar-width:none } .interface-settings-page::-webkit-scrollbar { display:none } .interface-settings-page.is-open { display:block } .interface-settings-header { display:flex;align-items:flex-start;justify-content:space-between;gap:1rem;padding:1rem 0 1.2rem;border-bottom:1px solid #29292d } @@ -226,7 +226,7 @@ function renderControl(): void { .interface-setting-card p { margin:0 0 .8rem;color:#85858a;font-size:.68rem;line-height:1.45 } .interface-setting-card select { width:100%;padding:.52rem;border:1px solid #39393e;border-radius:7px;background:#18181b;color:#eee;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 { width:1.25rem;height:2px;border-radius:0;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-switch-row { display:flex;align-items:center;justify-content:space-between;gap:.8rem;margin-top:.7rem;color:#c5c5c9;font-size:.72rem } .interface-switch-row input { @@ -236,7 +236,7 @@ function renderControl(): void { flex:0 0 auto; margin:0; border:1px solid #414147; - border-radius:999px; + border-radius:var(--radius-sm); outline:none; background-color:#242428; background-image:radial-gradient(circle at .56rem 50%,#a0a0a5 0 .34rem,transparent .36rem); @@ -249,6 +249,11 @@ function renderControl(): void { background-image:radial-gradient(circle at calc(100% - .56rem) 50%,#07120b 0 .34rem,transparent .36rem); } .interface-switch-row input:focus-visible { box-shadow:0 0 0 3px var(--ui-accent-soft) } + .binary-setting-row { display:flex;align-items:center;justify-content:space-between;gap:.5rem;padding:.2rem 0;color:#b3b3b7;font-size:.7rem } + .binary-switch { min-width:42px;padding:.28rem .5rem;border:1px solid var(--border-strong);border-radius:var(--radius-sm);background:var(--surface);color:var(--muted);font-size:.58rem } + .binary-switch[aria-checked="true"] { border-color:var(--ui-accent);background:var(--ui-accent);color:var(--ui-accent-ink) } + .binary-switch:disabled { border-color:var(--border);background:var(--surface-deep);color:#66666b } + .binary-switch:focus-visible { outline:2px solid var(--ui-accent);outline-offset:2px } .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 } @@ -269,7 +274,7 @@ function renderControl(): void { -
+
WEBHID

Connect a supported device to view and change its settings.

DEVICE CONTROL

Connect a mouse

@@ -299,7 +304,7 @@ function renderControl(): void {
BATTERYRead after connection
FIRMWARERead after connection
-
CONNECTION2.4 GHz receiver
+
CONNECTION2.4 GHz receiver

DPI

Sensitivity

Choose a DPI value
@@ -312,16 +317,31 @@ function renderControl(): void {
LIVE STATUSAdd a supported device from the sidebar to read its current status.
@@ -916,7 +936,7 @@ function renderLogitechDetails(status: MouseStatus): void { ["DPI axes", status.supportsSeparateDpiAxes ? `X ${status.dpi} · Y ${status.dpiY ?? status.dpi}` : "Linked X/Y"], ]; list.innerHTML = items.map(([label, value]) => - `
${label.toUpperCase()}${value}
`).join(""); + `
${label.toUpperCase()}${value}
`).join(""); } function setControlValue(selector: string, value: number | string | null | undefined): void { @@ -932,17 +952,12 @@ function setToggleValue(selector: string, value: boolean | null | undefined): vo if (!control) return; control.disabled = value === null || value === undefined; if (control.disabled) { + control.removeAttribute("aria-checked"); control.textContent = "N/A"; - control.style.background = "#202023"; - control.style.borderColor = "#3a3a3f"; - control.style.color = "#66666b"; return; } control.setAttribute("aria-checked", String(value)); control.textContent = value ? "On" : "Off"; - control.style.background = value ? "var(--ui-accent)" : "#202023"; - control.style.borderColor = value ? "var(--ui-accent)" : "#3a3a3f"; - control.style.color = value ? "var(--ui-accent-ink)" : "#8b8b90"; } function formatHex(value: number, width = 2): string { @@ -1605,14 +1620,14 @@ function renderEggCpiStages(status: EggOp1Status): void { container.innerHTML = stages.slice(0, levels).map((stage, index) => { const split = stage.x !== stage.y; const step = stage.x <= 10_000 ? status.eggCpiStepLow : status.eggCpiStepHigh; - return `
- - -
- - + return `
+ + +
+ +
- +
`; }).join(""); container.querySelectorAll("[data-active-cpi]").forEach((radio) => { @@ -1657,8 +1672,8 @@ function renderEggButtons(status: EggOp1Status): void { const fixedPrimary = (index === 0 && !status.eggLeftHanded) || (index === 1 && status.eggLeftHanded); const action = actions[index]; const unsupported = action.key === "raw" ? `` : ""; - const multiclick = filters[index] === null || filters[index] === undefined ? "" : `