From 4eca1af5c954516711d4bffda11f7abc7cb17419 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Fri, 25 Sep 2026 11:36:18 +0100 Subject: [PATCH 1/2] Share one dialog shell across main-window dialogs Setup, pairing, profile, confirmation, practice and capture dialogs now share one surface, radius, padding and shadow, with matching header and footer spacing. The setup guide sizes to its step up to the window instead of a fixed 900px, with a sentence-case label and a thicker step rail. Pairing requests are cards with the code set large for comparison. Opening a dialog is a short fade and rise that reduced motion skips, and dialogs show a focus ring only for keyboard focus. Closes #889 Co-Authored-By: Claude Opus 5.5 (1M context) --- src/ui/app.css | 98 +++++++++++++++++++++++++++++--------------------- 1 file changed, 58 insertions(+), 40 deletions(-) diff --git a/src/ui/app.css b/src/ui/app.css index 101d2967..99018edb 100644 --- a/src/ui/app.css +++ b/src/ui/app.css @@ -519,50 +519,61 @@ fieldset:disabled :is(.color-options, .scanner-colours) label:not(:has(input:che .mobile-pairing li + li { margin-top: 8px; } .mobile-pairing .teaching-disclosure { margin-bottom: 0; } -/* --- Dialogs ------------------------------------------------------------------ */ -.modal-backdrop { position: fixed; z-index: 20; inset: 0; display: grid; place-items: center; padding: 12px; background: rgba(28, 27, 31, 0.48); } -.pairing-dialog, .profile-dialog, .switch-practice-dialog { width: min(100%, 900px); max-height: calc(100dvh - 24px); overflow: auto; border: 1px solid var(--border); border-radius: 8px; padding: 20px; color: var(--text); background: var(--surface); box-shadow: var(--shadow-dialog); } -.switch-practice-dialog { border-radius: 12px; } -.confirm-dialog { width: min(100%, 480px); } -.dialog-error { margin-top: 16px; border-left: 3px solid var(--status-error); padding: 9px 11px; color: var(--status-error); background: var(--status-error-soft); } - -.setup-dialog { display: flex; flex-direction: column; width: min(100%, 920px); height: min(900px, calc(100dvh - 24px)); min-height: 0; max-height: calc(100dvh - 24px); overflow: hidden; border: 1px solid var(--border); border-radius: 10px; padding: 20px; color: var(--text); background: var(--surface); box-shadow: var(--shadow-dialog); } -.setup-dialog:focus { outline: 2px solid var(--focus); outline-offset: 2px; } -.setup-dialog > header, .setup-dialog > footer { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; } -.setup-dialog > header { justify-content: space-between; } -.setup-dialog > header span { color: var(--brand); font-size: 16px; font-weight: 700; text-transform: uppercase; } -.setup-dialog > header h2 { margin-top: 4px; font-size: 24px; line-height: 1.33; } -.setup-dialog > header strong { color: var(--muted); font-size: 16px; } -.setup-progress { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; margin-top: 18px; } -.setup-progress span { height: 4px; border-radius: 2px; background: var(--border); } +/* --- Dialogs ------------------------------------------------------------------ + Every main-window dialog shares one shell: the same surface, radius, + padding and shadow; a header with the title and any close control; a body; + and a footer whose spacer pushes the main actions to the end. Opening is + the one moment of motion, and it is skipped under reduced motion. */ +.modal-backdrop { position: fixed; z-index: 20; inset: 0; display: grid; place-items: center; padding: 12px; background: rgba(28, 27, 31, 0.5); } +.setup-dialog, .pairing-dialog, .profile-dialog, .switch-practice-dialog, .capture-dialog { max-height: calc(100dvh - 24px); overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-dialog); padding: 28px; color: var(--text); background: var(--surface); box-shadow: var(--shadow-dialog); } +.setup-dialog, .pairing-dialog, .profile-dialog, .switch-practice-dialog { animation: dialog-in 160ms ease-out; } +@keyframes dialog-in { from { opacity: 0; transform: translateY(8px) scale(0.985); } } +:is(.setup-dialog, .pairing-dialog, .profile-dialog, .switch-practice-dialog):focus { outline: none; } +:is(.setup-dialog, .pairing-dialog, .profile-dialog, .switch-practice-dialog):focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; } +.pairing-dialog { width: min(100%, 640px); } +.profile-dialog, .switch-practice-dialog { width: min(100%, 760px); } +.confirm-dialog { width: min(100%, 520px); } +:is(.setup-dialog, .pairing-dialog, .profile-dialog) > header h2, .switch-practice-dialog > h2 { font-size: 24px; line-height: 1.3; font-weight: 700; } +:is(.setup-dialog, .pairing-dialog, .profile-dialog) > header p { margin-top: 4px; color: var(--muted); } +:is(.setup-dialog, .profile-dialog) > footer { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; margin-top: 24px; border-top: 1px solid var(--border); padding-top: 20px; } +:is(.setup-dialog, .profile-dialog) > footer > span { flex: 1; } +.confirm-dialog > footer { border-top: 0; padding-top: 0; } +.dialog-error { margin-top: 16px; border-left: 4px solid var(--status-error); border-radius: 0 8px 8px 0; padding: 10px 14px; color: var(--status-error); background: var(--status-error-soft); } + +/* The setup guide sizes to its step, up to the window, and scrolls only its + middle so the step rail and the navigation stay in view. */ +.setup-dialog { display: flex; flex-direction: column; width: min(100%, 880px); min-height: min(480px, calc(100dvh - 24px)); overflow: hidden; } +.setup-dialog > header { display: flex; flex-wrap: wrap; align-items: flex-end; justify-content: space-between; gap: 8px 16px; } +.setup-dialog > header span { color: var(--muted); font-size: 16px; font-weight: 600; } +.setup-dialog > header h2 { margin-top: 2px; } +.setup-dialog > header strong { color: var(--muted); font-size: 16px; font-variant-numeric: tabular-nums; } +.setup-progress { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; margin-top: 16px; } +.setup-progress span { height: 6px; border-radius: 3px; background: var(--border); } .setup-progress span[data-active="true"] { background: var(--brand); } -.setup-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 16px 4px; } +.setup-content { flex: 1 1 auto; min-height: 0; overflow-y: auto; padding: 24px 4px 8px; } +.setup-content > div > p { max-width: 68ch; } .setup-content p { margin-top: 8px; color: var(--muted); } -.setup-choices { margin-top: 22px; } -.setup-privacy { margin-top: 18px; color: var(--brand-label); } +.setup-choices { margin-top: 24px; } +.setup-privacy { margin-top: 16px; color: var(--brand-label); } .setup-navigation-guidance { flex: 0 0 auto; margin: 0; color: var(--muted); line-height: 1.5; } -.setup-navigation-guidance:not(:empty) { padding-bottom: 16px; } -.setup-dialog > footer { gap: 12px; border-top: 1px solid var(--border); padding-top: 12px; } -.setup-dialog > footer > span { flex: 1; } +.setup-navigation-guidance:not(:empty) { padding-top: 12px; } +.setup-dialog > footer { flex: none; margin-top: 12px; } .setup-backdrop[hidden] { display: none; } -.pairing-dialog:focus { outline: 2px solid var(--focus); outline-offset: 2px; } -.pairing-dialog > header { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: 12px; } +.pairing-dialog > header { display: grid; grid-template-columns: auto minmax(0, 1fr) auto; align-items: start; gap: 16px; } .pairing-dialog > header > svg { margin-top: 2px; color: var(--brand); } -.pairing-dialog > header > span { display: grid; place-items: center; min-width: 28px; height: 28px; border-radius: 14px; color: var(--on-brand-container); background: var(--brand-container); font-weight: 700; } +.pairing-dialog > header > span { display: grid; place-items: center; min-width: 32px; height: 32px; border-radius: 16px; color: var(--on-brand-container); background: var(--brand-container); font-weight: 700; } .pairing-dialog p { margin-top: 4px; color: var(--muted); line-height: 1.4; } .pairing-dialog .pairing-connection { color: var(--text); } -.pairing-list { margin-top: 20px; border-top: 1px solid var(--border); } -.pairing-list article { display: flex; flex-wrap: wrap; align-items: center; gap: 16px; min-height: 88px; border-bottom: 1px solid var(--border); padding: 12px 0; } -.pairing-list output { color: var(--text); font: 700 25px/1 ui-monospace, monospace; letter-spacing: 0.12em; white-space: nowrap; } -.pairing-actions { display: flex; flex-wrap: wrap; gap: 12px; } - -.profile-dialog > header, .profile-dialog > footer { display: flex; flex-wrap: wrap; align-items: center; gap: 12px; } -.profile-dialog > header { justify-content: space-between; } -.profile-dialog header p { margin-top: 4px; color: var(--muted); } -.profile-dialog > footer { margin-top: 20px; } -.profile-dialog > footer > span { flex: 1; } -.binding-list { margin-top: 18px; border-top: 1px solid var(--border); } +/* Each request is a card whose code is the thing to compare, so the code is + set large and spaced like the code on the phone. */ +.pairing-list { display: grid; gap: 12px; margin-top: 24px; } +.pairing-list article { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 12px 16px; border: 1px solid var(--border); border-radius: var(--radius-card); padding: 16px 20px; background: var(--surface-muted); } +.pairing-list output { border-radius: 10px; padding: 8px 14px; color: var(--text); background: var(--surface); font: 700 28px/1 ui-monospace, Consolas, monospace; letter-spacing: 0.12em; white-space: nowrap; } +.pairing-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 12px; grid-column: 1 / -1; } + +.profile-dialog > header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; } +.binding-list { margin-top: 20px; border-top: 1px solid var(--border); } /* Switch label, action, then the action's value; both controls share the row instead of the action squeezing into a fixed narrow column. */ .profile-dialog { container-type: inline-size; } @@ -573,14 +584,20 @@ fieldset:disabled :is(.color-options, .scanner-colours) label:not(:has(input:che .binding-row > :nth-child(3):not(.binding-error) { grid-column: 2; } } -.capture-dialog { display: grid; justify-items: center; width: min(100%, 420px); border: 1px solid var(--brand-border); border-radius: 10px; padding: 32px 28px 24px; color: var(--text); background: var(--surface); box-shadow: var(--shadow-dialog); text-align: center; animation: capture-pulse 1.4s ease-in-out infinite; } +.switch-practice-dialog > p { max-width: 68ch; margin-top: 8px; color: var(--muted); } +.switch-practice-dialog > p[role="status"] { color: var(--text); font-weight: 600; } + +.capture-dialog { display: grid; justify-items: center; width: min(100%, 440px); border-color: var(--brand-border); padding: 36px 28px 28px; text-align: center; animation: capture-pulse 1.4s ease-in-out infinite; } .capture-dialog:focus { outline: none; } .capture-dialog > svg { color: var(--brand); } -.capture-dialog h2 { margin-top: 14px; } +.capture-dialog h2 { margin-top: 14px; font-size: 24px; line-height: 1.3; font-weight: 700; } .capture-dialog p { margin-top: 8px; color: var(--muted); } .capture-dialog .btn { margin-top: 20px; } @keyframes capture-pulse { 50% { border-color: var(--brand); box-shadow: var(--shadow-dialog), 0 0 0 4px var(--brand-container); } } -@media (prefers-reduced-motion: reduce) { .capture-dialog, .btn { animation: none; transition: none; } } +@media (prefers-reduced-motion: reduce) { + .setup-dialog, .pairing-dialog, .profile-dialog, .switch-practice-dialog, .capture-dialog { animation: none; } + .btn { transition: none; } +} @media (forced-colors: active) { .btn.nav-button[data-active="true"] { outline: 2px solid Highlight; outline-offset: -2px; } @@ -608,6 +625,7 @@ fieldset:disabled :is(.color-options, .scanner-colours) label:not(:has(input:che .update-banner .btn.text-button { grid-column: 2; justify-self: start; padding-left: 0; } } @media (max-width: 480px) { + .setup-dialog, .pairing-dialog, .profile-dialog, .switch-practice-dialog { padding: 20px; } .app-shell nav { grid-template-columns: repeat(2, minmax(0, 1fr)); } .view { padding: 12px; } .btn.profile-row { display: flex; flex-wrap: wrap; } @@ -617,7 +635,7 @@ fieldset:disabled :is(.color-options, .scanner-colours) label:not(:has(input:che .scanner-preferences .segmented { grid-template-columns: repeat(2, minmax(0, 1fr)); } } @media (max-height: 600px) { - .setup-dialog { overflow-y: auto; } + .setup-dialog { min-height: 0; overflow-y: auto; } .setup-dialog > * { flex-shrink: 0; } .setup-dialog .setup-content { overflow: visible; } } From 919c498ab853054cc6f12805f02e1fc1f60314f9 Mon Sep 17 00:00:00 2001 From: Owen McGirr Date: Fri, 25 Sep 2026 11:52:39 +0100 Subject: [PATCH 2/2] Wrap long pairing device names and guard the dialog animation A device name without spaces ran under the verification code; it now wraps within its column. A note on the entrance animation records why it must never leave a transform on the setup guide. Co-Authored-By: Claude Opus 5.5 (1M context) --- src/ui/app.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ui/app.css b/src/ui/app.css index 99018edb..f379afcd 100644 --- a/src/ui/app.css +++ b/src/ui/app.css @@ -527,6 +527,9 @@ fieldset:disabled :is(.color-options, .scanner-colours) label:not(:has(input:che .modal-backdrop { position: fixed; z-index: 20; inset: 0; display: grid; place-items: center; padding: 12px; background: rgba(28, 27, 31, 0.5); } .setup-dialog, .pairing-dialog, .profile-dialog, .switch-practice-dialog, .capture-dialog { max-height: calc(100dvh - 24px); overflow: auto; border: 1px solid var(--border); border-radius: var(--radius-dialog); padding: 28px; color: var(--text); background: var(--surface); box-shadow: var(--shadow-dialog); } .setup-dialog, .pairing-dialog, .profile-dialog, .switch-practice-dialog { animation: dialog-in 160ms ease-out; } +/* No fill mode, and never a lasting transform: the switch capture dialog + renders inside the setup guide with position: fixed, and a transform on the + guide would trap it inside the guide's clipped box instead of the window. */ @keyframes dialog-in { from { opacity: 0; transform: translateY(8px) scale(0.985); } } :is(.setup-dialog, .pairing-dialog, .profile-dialog, .switch-practice-dialog):focus { outline: none; } :is(.setup-dialog, .pairing-dialog, .profile-dialog, .switch-practice-dialog):focus-visible { outline: 3px solid var(--focus); outline-offset: 3px; } @@ -569,6 +572,7 @@ fieldset:disabled :is(.color-options, .scanner-colours) label:not(:has(input:che set large and spaced like the code on the phone. */ .pairing-list { display: grid; gap: 12px; margin-top: 24px; } .pairing-list article { display: grid; grid-template-columns: minmax(0, 1fr) auto; align-items: center; gap: 12px 16px; border: 1px solid var(--border); border-radius: var(--radius-card); padding: 16px 20px; background: var(--surface-muted); } +.pairing-list h3 { overflow-wrap: anywhere; } .pairing-list output { border-radius: 10px; padding: 8px 14px; color: var(--text); background: var(--surface); font: 700 28px/1 ui-monospace, Consolas, monospace; letter-spacing: 0.12em; white-space: nowrap; } .pairing-actions { display: flex; flex-wrap: wrap; justify-content: flex-end; gap: 12px; grid-column: 1 / -1; }