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
17 changes: 11 additions & 6 deletions src/settings/SwitchesSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,7 @@ export function SwitchesSection({ controller, onDraftChange, suspended = false,
const remoteRows = slots
.map((slot, index) => ({ slot, index }))
.filter(({ slot }) => slot.pressAction !== null);
const hasSwitches = settings.bindings.length > 0 || remoteRows.length > 0;
const freeSlots = slots
.map((slot, index) => (slot.pressAction === null ? index : -1))
.filter((index) => index >= 0);
Expand Down Expand Up @@ -673,18 +674,20 @@ export function SwitchesSection({ controller, onDraftChange, suspended = false,
/>
)}
<SettingGroup
title="Switches"
title="Your switches"
description=""
>
{!draft && <Button className="primary" ref={addRef} disabled={disabled || settings.bindings.length >= 128} onClick={startAdd}>Add switch</Button>}
<SwitchPractice disabled={!!state?.keyboardEntry || entryPending || capturing || !!pending || unsaved || !!remote.pending || remote.unsaved || !!draft || suspended} />
<div className="switch-toolbar">
<p className="setting-note switch-status" role="status">
{pending || remote.pending
? "Saving switches..."
: unsaved || remote.unsaved
? "Switch assignments have unsaved changes."
: "Saved automatically."}
</p>
{/* One fixed place in every state, so focus survives switches loading. */}
{!draft && <Button className="primary" ref={addRef} disabled={disabled || settings.bindings.length >= 128} onClick={startAdd}>Add switch</Button>}
</div>
{remote.error && (
<div className="dialog-error switch-error" role="alert">
<span>{remote.error}</span>
Expand Down Expand Up @@ -713,13 +716,14 @@ export function SwitchesSection({ controller, onDraftChange, suspended = false,
)}
</div>
)}
{!settings.bindings.length && !remoteRows.length && !draft ? (
{!hasSwitches && !draft ? (
<div className="empty-state switch-empty">
<Keyboard size={28} aria-hidden="true" />
<h3>No switches yet</h3>
<p>
Automatic scanning needs a Select switch. Manual scanning also
needs Next and Previous.
Choose Add switch, then press the switch you use to choose
things. Automatic scanning needs a Select switch. Manual
scanning also needs Next and Previous.
</p>
<div className="switch-list-actions">
<Button
Expand Down Expand Up @@ -935,6 +939,7 @@ export function SwitchesSection({ controller, onDraftChange, suspended = false,

</div>
)}
<SwitchPractice disabled={!!state?.keyboardEntry || entryPending || capturing || !!pending || unsaved || !!remote.pending || remote.unsaved || !!draft || suspended} />
</SettingGroup>
<MoreOptions label="More options">
<SettingGroup
Expand Down
46 changes: 28 additions & 18 deletions src/ui/app.css
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ h1, h2, h3, p { margin: 0; letter-spacing: 0; }
.field-control[type="range"]:disabled { background: transparent; }
.field { display: grid; gap: 6px; min-width: 0; margin-top: 20px; color: var(--muted); }
.field-error { color: var(--status-error); line-height: 1.35; }
.field > .btn { justify-self: start; }

/* --- Shell ------------------------------------------------------------------
The sidebar is quiet: the active place is a raised surface with a brand bar
Expand Down Expand Up @@ -271,6 +272,8 @@ main { min-width: 0; }
.setting-group:first-child, .page-header + .setting-group { border-top: 0; padding-top: 0; }
.setting-group header p { max-width: 64ch; }
.setting-controls { display: grid; align-content: start; gap: 16px; }
/* A lone action in a group keeps its own width instead of spanning it. */
.setting-controls > .btn { justify-self: start; }
.setting-note { margin-top: 8px; color: var(--muted); line-height: 1.5; }
.setting-note-block { display: grid; justify-items: start; }
.setting-note-block .disclosure { margin-top: 4px; }
Expand Down Expand Up @@ -387,21 +390,28 @@ fieldset:disabled :is(.color-options, .scanner-colours) label:not(:has(input:che
/* --- Switches ----------------------------------------------------------------
A summary row per switch, expanding to an editor. Switch-access users step
through every control, so the collapsed row exposes only Edit and Remove. */
.switch-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 12px 16px; }
.switch-status { margin-top: 0; }
.switch-error { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-top: 0; }
.switch-empty { padding: 36px 24px; }
.switch-empty { padding: 40px 24px; background: var(--surface); }
.switch-empty h3 { margin-top: 12px; color: var(--text); }
.switch-list { display: grid; gap: 16px; }
.switch-row { border: 1px solid var(--border); border-radius: var(--radius-card); background: var(--surface); }
.switch-row[data-open] { border-color: var(--brand-border); }
.switch-row-summary { display: grid; grid-template-columns: auto minmax(0, 1fr) auto auto; align-items: center; gap: 12px; min-height: 60px; padding: 16px; }
.switch-row-summary > svg { color: var(--muted); }
.switch-empty .switch-list-actions { justify-content: center; margin-top: 20px; }
.switch-list { display: grid; gap: 12px; }
.switch-row { border: 1px solid var(--border); border-radius: var(--radius-card); background: var(--surface); container-type: inline-size; }
.switch-row[data-open] { border-color: var(--brand); box-shadow: 0 0 0 1px var(--brand); }
/* The key is what a person recognises a switch by, so it leads the row.
It moves visually only; DOM and focus order are unchanged, so Edit still
comes before Remove. Every saved row has a badge, so the icon is hidden on
all of them and the row text says whether a switch is local or remote. */
.switch-row-summary { display: flex; flex-wrap: wrap; align-items: center; gap: 12px 16px; min-height: 80px; padding: 16px 20px; }
.switch-row-summary > svg { flex: none; color: var(--muted); }
.switch-row-summary:has(> .key-badge) > svg { display: none; }
.switch-row-summary > div { flex: 1 1 220px; min-width: 0; }
.switch-row-summary h3 { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.switch-row-summary p { margin-top: 3px; color: var(--muted); white-space: normal; overflow-wrap: anywhere; }
.switch-row-summary > .key-badge { grid-column: 2; justify-self: start; font-size: 16px; }
.switch-row-summary > .btn { grid-row: 1; }
.switch-row-summary > .btn:first-of-type { grid-column: 3; }
.switch-row-summary > .btn:last-of-type { grid-column: 4; }
.switch-row-summary p { margin-top: 2px; color: var(--muted); overflow-wrap: anywhere; }
.switch-row-summary > .key-badge { order: -1; justify-content: center; min-width: 88px; min-height: 52px; border-bottom-width: 3px; border-radius: 10px; padding: 0 14px; font-size: 20px; }
.switch-row-summary > .btn { flex: none; }
.switch-row-summary > .btn.secondary { margin-left: auto; }
.key-badge { display: inline-flex; align-items: center; min-height: 26px; border: 1px solid var(--border); border-bottom-width: 2px; border-radius: 5px; padding: 0 9px; background: var(--surface-muted); font: 600 16px/1 ui-monospace, Consolas, monospace; white-space: nowrap; }
.key-badge[data-unavailable] { border-color: var(--status-error); color: var(--status-error); background: var(--status-error-soft); }
.key-badge-empty { border-style: dashed; color: var(--muted); font-family: inherit; font-weight: 500; }
Expand All @@ -418,12 +428,14 @@ fieldset:disabled :is(.color-options, .scanner-colours) label:not(:has(input:che
.switch-editor-footer { display: flex; flex-wrap: wrap; gap: 12px; padding-top: 4px; }
.switch-editor-footer .danger { margin-left: auto; }
.switch-list-actions, .setup-choices { display: flex; flex-wrap: wrap; gap: 12px; }
.switch-empty .switch-list-actions { margin-top: 16px; }

.switch-practice-entry { display: grid; gap: 12px; padding: 12px 0; }
.switch-practice-entry > p:first-child { font-size: 16px; }
.switch-practice-entry label { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; }
.switch-practice-entry select { width: auto; max-width: 100%; }
/* Practice is a side trip, not the page's job, so it sits after the list as
a quiet card with its source choice and start button on one line. */
.switch-practice-entry { display: flex; flex-wrap: wrap; align-items: flex-end; gap: 12px 16px; margin-top: 8px; border: 1px solid var(--border); border-radius: var(--radius-card); padding: 20px; background: var(--surface-muted); }
.switch-practice-entry > p:first-child { flex-basis: 100%; color: var(--text); font-size: 18px; font-weight: 600; }
.switch-practice-entry label { display: grid; flex: 1 1 260px; gap: 6px; color: var(--muted); }
.switch-practice-entry > .btn { flex: none; }
.switch-practice-entry > p[role="alert"] { flex-basis: 100%; color: var(--status-error); }
.switch-practice-buttons { display: flex; justify-content: flex-end; gap: 12px; }
.switch-practice-blocks { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 12px; margin: 16px 0; padding: 0; list-style: none; font-size: 16px; }
.switch-practice-blocks li { display: flex; align-items: center; justify-content: center; min-height: 72px; border: 2px solid var(--border); border-radius: 8px; padding: 16px; background: var(--surface); text-align: center; }
Expand Down Expand Up @@ -571,8 +583,6 @@ fieldset:disabled :is(.color-options, .scanner-colours) label:not(:has(input:che
.btn.nav-button { flex-direction: column; justify-content: center; gap: 4px; padding: 8px; text-align: center; }
.btn.nav-button[data-active="true"] { box-shadow: inset 0 -4px 0 var(--brand), 0 1px 2px rgba(28, 27, 31, 0.08); }
.view { padding: 16px; }
.switch-row-summary { grid-template-columns: auto minmax(0, 1fr); }
.switch-row-summary > .btn:first-of-type, .switch-row-summary > .btn:last-of-type { width: 100%; grid-row: auto; grid-column: auto; }
.hold-list li { grid-template-columns: repeat(3, minmax(64px, 1fr)); }
.hold-list li > select { grid-column: 2 / -1; }
.mobile-download { grid-template-columns: 1fr; }
Expand Down
Loading