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
8 changes: 7 additions & 1 deletion build/check-bundle-size.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,13 @@ const BUDGET_BYTES: Record<string, number> = {
// 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.
".js": 800_000,
// 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.
".js": 895_000,
};

const ASSETS = join("dist", "assets");
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

131 changes: 74 additions & 57 deletions src/admin.tsx

Large diffs are not rendered by default.

64 changes: 40 additions & 24 deletions src/app/App.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { useEffect, useRef, useState, type KeyboardEvent, type ReactNode } from "react";
import * as control from "../device/controller";
import { WORKSPACE_TAB_ORDER, type ControlSnapshot, type WorkspaceTab } from "../device/types";
import { t, connectLabelText, connectionText, ensureLocale, type I18nKey } from "../i18n";
import { interfaceThemeSlug } from "../interface-preferences";
import { CaptureDialog } from "./CaptureDialog";
import { Diagnostics, LogitechDetails } from "./Diagnostics";
Expand Down Expand Up @@ -50,6 +51,7 @@ function on(tab: WorkspaceTab, tabs: readonly WorkspaceTab[]): boolean {
function DeviceOverview({ snapshot }: { snapshot: ControlSnapshot }): ReactNode {
const status = snapshot.status;
if (!status) return null;
const locale = snapshot.preferences.locale;
const isWired = status.connectionType === "Wired";
const showBattery = !snapshot.traits.eggControls
&& (status.ui?.forceShowBattery || !isWired || status.batteryPercent !== null);
Expand All @@ -69,37 +71,39 @@ function DeviceOverview({ snapshot }: { snapshot: ControlSnapshot }): ReactNode
>
{showBattery ? (
<article id="battery-summary" className="summary-stat" style={{ display: "flex" }}>
<span>BATTERY</span>
<span>{t(locale, "ov.battery")}</span>
<strong className="battery-readout">
<span id="battery-icon-slot">
<BatteryIcon percent={status.batteryPercent} state={status.batteryState} />
</span>
<span id="battery-value">{status.batteryPercent === null ? "—" : `${status.batteryPercent}%`}</span>
</strong>
<small id="battery-detail">{control.batteryDetail(status)}</small>
<small id="battery-detail">{control.batteryDetail(status, locale)}</small>
</article>
) : null}
<article className="summary-stat">
<span>FIRMWARE</span>
<span>{t(locale, "ov.firmware")}</span>
<strong id="firmware-value">{status.firmware[0] ?? "—"}</strong>
<small id="firmware-detail">
{status.firmware.length > 1
? status.firmware.slice(1).join(" · ")
: status.firmware.length === 1
? "Firmware reported by mouse"
: "Not reported"}
? t(locale, "ov.fwSingle")
: t(locale, "ov.fwNone")}
</small>
</article>
<article className="summary-stat" data-pending-key="dongle-led">
<span>CONNECTION</span>
<strong id="connection-value">{status.connectionType ?? "Wireless"}</strong>
<span>{t(locale, "ov.connection")}</span>
<strong id="connection-value">{connectionText(locale, status.connectionType)}</strong>
<small id="connection-detail">
{status.connectionDetail
?? (status.activeProfile ? `2.4 GHz · Profile ${status.activeProfile}` : "2.4 GHz receiver")}
?? (status.activeProfile
? `${t(locale, "ov.conn24")} · ${t(locale, "ov.profile")} ${status.activeProfile}`
: t(locale, "ov.connReceiver"))}
</small>
{supportsDongleLed ? (
<button id="dongle-led-toggle" className="dongle-led-button" type="button" onClick={control.toggleDongleLed}>
Receiver LED: {status.dongleLedEnabled ? "On" : "Off"}
{t(locale, "ov.led")}: {status.dongleLedEnabled ? t(locale, "ov.on") : t(locale, "ov.off")}
</button>
) : null}
</article>
Expand All @@ -117,6 +121,7 @@ function Workspace({
const status = snapshot.status;
const tab = snapshot.workspaceTab;
if (!status) return null;
const locale = snapshot.preferences.locale;
const has = cardAvailability(snapshot);
const show = (available: boolean, tabs: readonly WorkspaceTab[]): boolean => available && on(tab, tabs);

Expand Down Expand Up @@ -256,11 +261,10 @@ function Workspace({
) : null}

{on(tab, ["advanced"]) ? (
<aside className="testing-note" aria-label="Development testing guidance">
<strong>Development testing</strong>
<aside className="testing-note" aria-label={t(locale, "misc.devTesting")}>
<strong>{t(locale, "misc.devTesting")}</strong>
<span>
Record the device identifier, protocol version, and any failing setting in the issue or pull
request. Do not use factory reset during initial testing.
{t(locale, "misc.devTestingBody")}
</span>
</aside>
) : null}
Expand All @@ -276,6 +280,7 @@ export function App(): ReactNode {
const [captureOpen, setCaptureOpen] = useState(false);
const [shareProfileOpen, setShareProfileOpen] = useState(false);
const { preferences, status } = snapshot;
const locale = preferences.locale;

useEffect(() => {
const element = panel.current?.closest<HTMLElement>(".control-shell");
Expand All @@ -295,6 +300,17 @@ export function App(): ReactNode {
panel.current?.scrollTo({ top: 0, behavior: preferences.reducedMotion ? "auto" : "smooth" });
}, [snapshot.workspaceTab]);

useEffect(() => {
try {
document.documentElement.lang = locale === "pt" ? "pt-BR" : "en";
} 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.
if (locale !== "en") void ensureLocale(locale).then(() => control.refreshInterface());
}, [locale]);

const filterTabs = (status:MouseStatus|null):readonly WorkspaceTab[] => {
var tempTabs = WORKSPACE_TAB_ORDER;
const has = cardAvailability(snapshot);
Expand Down Expand Up @@ -346,8 +362,8 @@ export function App(): ReactNode {
<header className="panel-header">
<div className="panel-title">
<div>
<p className="overline">DEVICE CONTROL</p>
<h1 id="device-title">{status?.name ?? "Connect a mouse"}</h1>
<p className="overline">{t(locale, "panel.overline")}</p>
<h1 id="device-title">{status?.name ?? t(locale, "panel.connectMouse")}</h1>
</div>
</div>
<div className="device-status">
Expand All @@ -364,7 +380,7 @@ export function App(): ReactNode {
className="live-status-share"
onClick={() => setShareProfileOpen(true)}
>
Share profile
{t(locale, "panel.shareProfile")}
</button>
) : null}
</p>
Expand All @@ -373,12 +389,12 @@ export function App(): ReactNode {
{status ? null : (
<section className="empty-state" aria-labelledby="empty-state-title">
<h2 id="empty-state-title">
{snapshot.previewListMessage ? "Driver previews" : "Connect a mouse."}
{snapshot.previewListMessage ? t(locale, "empty.previewsTitle") : t(locale, "empty.title")}
</h2>
<p>
{snapshot.previewListMessage ? (
<>
Render any supported driver without its hardware:{" "}
{t(locale, "empty.previewsBody")}{" "}
{snapshot.previewEntries.map(([key], index) => (
<span key={key}>
{index > 0 ? " · " : null}
Expand All @@ -387,7 +403,7 @@ export function App(): ReactNode {
))}
</>
) : (
"Pick your mouse in the browser prompt to adjust its onboard settings."
t(locale, "empty.body")
)}
</p>
<button
Expand All @@ -400,13 +416,13 @@ export function App(): ReactNode {
void control.connect();
}}
>
{snapshot.connectLabel}
{connectLabelText(locale, snapshot.connectLabel)}
</button>
</section>
)}

{status ? (
<nav className="workspace-tabs device-data" role="tablist" aria-label="Device sections">
<nav className="workspace-tabs device-data" role="tablist" aria-label={t(locale, "panel.deviceSections")}>
<i className="lg-glass__refract" aria-hidden="true" />
<i className="lg-glass__tint" aria-hidden="true" />
<i className="lg-glass__specular" aria-hidden="true" />
Expand All @@ -421,7 +437,7 @@ export function App(): ReactNode {
onClick={() => control.setWorkspaceTab(tab)}
onKeyDown={(event) => onTabKey(event, tab)}
>
{tab[0].toUpperCase() + tab.slice(1)}
{t(locale, `tab.${tab}` as I18nKey)}
</button>
))}
</nav>
Expand All @@ -432,9 +448,9 @@ export function App(): ReactNode {
</main>

<PendingBar snapshot={snapshot} />
<CaptureDialog open={captureOpen} onClose={() => setCaptureOpen(false)} />
<CaptureDialog open={captureOpen} onClose={() => setCaptureOpen(false)} locale={locale} />
<ShareProfileDialog open={shareProfileOpen} onClose={() => setShareProfileOpen(false)} snapshot={snapshot} />
<ToastHost toasts={snapshot.toasts} />
<ToastHost toasts={snapshot.toasts} locale={locale} />
</div>
);
}
24 changes: 13 additions & 11 deletions src/app/CaptureDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import {
type SectorDiff,
} from "../capture-format";
import { captureContext } from "../capture-context";
import { t } from "../i18n";
import type { InterfaceLocale } from "../interface-preferences";

export function CaptureDialog({ open, onClose }: { open: boolean; onClose: () => void }): ReactNode {
export function CaptureDialog({ open, onClose, locale = "en" }: { open: boolean; onClose: () => void; locale?: InterfaceLocale }): ReactNode {
const dialog = useRef<HTMLDialogElement>(null);
const [snapshot, setSnapshot] = useState<Map<number, Uint8Array> | null>(null);
const [diffs, setDiffs] = useState<SectorDiff[]>([]);
Expand Down Expand Up @@ -122,9 +124,9 @@ export function CaptureDialog({ open, onClose }: { open: boolean; onClose: () =>
<div style={{ display: "flex", alignItems: "flex-start", justifyContent: "space-between", gap: "1rem" }}>
<div>
<p style={{ margin: 0, color: "#77777c", fontSize: ".6rem", letterSpacing: ".05em" }}>DEVELOPMENT</p>
<h2 style={{ margin: ".1rem 0 0", fontSize: "1rem", color: "#ececef" }}>HID++ capture</h2>
<h2 style={{ margin: ".1rem 0 0", fontSize: "1rem", color: "#ececef" }}>{t(locale, "cap.title")}</h2>
</div>
<button id="capture-close" type="button" aria-label="Close capture" onClick={onClose}>Close</button>
<button id="capture-close" type="button" aria-label={t(locale, "cap.closeCapture")} onClick={onClose}>{t(locale, "common.close")}</button>
</div>

<small style={{ color: "#77777c", fontSize: ".64rem" }}>
Expand Down Expand Up @@ -159,7 +161,7 @@ export function CaptureDialog({ open, onClose }: { open: boolean; onClose: () =>
}).finally(() => setBusy(false));
}}
>
Copy verification data
{t(locale, "cap.copyVerification")}
</button>

{probe ? (
Expand Down Expand Up @@ -193,12 +195,12 @@ export function CaptureDialog({ open, onClose }: { open: boolean; onClose: () =>
}).finally(() => setBusy(false));
}}
>
Verify profile writes
{t(locale, "cap.verifyWrites")}
</button>
) : null}

<button id="capture-snapshot" type="button" onClick={() => void takeSnapshot()}>Snapshot profiles</button>
<button id="capture-compare" type="button" onClick={() => void compareSnapshot()}>Compare</button>
<button id="capture-snapshot" type="button" onClick={() => void takeSnapshot()}>{t(locale, "cap.snapshot")}</button>
<button id="capture-compare" type="button" onClick={() => void compareSnapshot()}>{t(locale, "cap.compare")}</button>
<button
id="capture-reset"
type="button"
Expand All @@ -210,7 +212,7 @@ export function CaptureDialog({ open, onClose }: { open: boolean; onClose: () =>
setMessage("Cleared.");
}}
>
Clear
{t(locale, "cap.clear")}
</button>
<button
id="capture-copy"
Expand All @@ -230,7 +232,7 @@ export function CaptureDialog({ open, onClose }: { open: boolean; onClose: () =>
);
}}
>
Copy comparison
{t(locale, "cap.copyComparison")}
</button>
<span id="capture-status" role="status" aria-live="polite" style={{ color: "#77777c", fontSize: ".62rem" }}>
{message}
Expand Down Expand Up @@ -288,7 +290,7 @@ export function CaptureDialog({ open, onClose }: { open: boolean; onClose: () =>
</div>

<div>
<p style={{ margin: "0 0 .25rem", color: "#77777c", fontSize: ".62rem" }}>What did you change?</p>
<p style={{ margin: "0 0 .25rem", color: "#77777c", fontSize: ".62rem" }}>{t(locale, "cap.whatChanged")}</p>
<div id="capture-action-list" style={{ display: "flex", flexWrap: "wrap", gap: ".3rem" }}>
{CAPTURE_ACTIONS.map((action) => {
const active = selectedActions.has(action.id);
Expand Down Expand Up @@ -324,7 +326,7 @@ export function CaptureDialog({ open, onClose }: { open: boolean; onClose: () =>
<textarea
id="capture-notes"
rows={2}
placeholder="Optional detail, e.g. wireless polling 8000 Hz to 1000 Hz"
placeholder={t(locale, "cap.notesPlaceholder")}
value={notes}
onChange={(event) => setNotes(event.currentTarget.value)}
style={{
Expand Down
Loading