From 60698fc08f863ad4a68648957d8cd3c20b785cab Mon Sep 17 00:00:00 2001 From: Thomas Joise Date: Fri, 17 Jul 2026 15:57:57 +0530 Subject: [PATCH] =?UTF-8?q?fix(iwa):=20final-review=20fixes=20=E2=80=94=20?= =?UTF-8?q?dialog=20layering/scrim/scroll-lock,=20a11y=20labels+disabled+a?= =?UTF-8?q?lerts,=20settings=20guards+errors,=20edit-mode=20locks,=20stric?= =?UTF-8?q?t=20offsets,=20titles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 27 adversarially-confirmed findings from the multi-agent whole-branch review (2 deferred). Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/hq-web/src/Layout.tsx | 20 ++++++-- apps/hq-web/src/Login.tsx | 8 +++- apps/hq-web/src/app.css | 17 +++++-- .../src/components/ClientFormDialog.tsx | 2 +- apps/hq-web/src/components/SendDialog.tsx | 18 +++++--- apps/hq-web/src/pages/Employees.tsx | 2 +- apps/hq-web/src/pages/Modules.tsx | 12 +++-- apps/hq-web/src/pages/Settings.tsx | 19 +++++--- apps/hq-web/src/pages/client-forms.tsx | 27 +++++++---- apps/hq/src/api.ts | 12 +++-- apps/hq/src/repos-reminders.ts | 20 ++++++-- apps/hq/test/settings-reminders.test.ts | 19 ++++++++ packages/ui/src/components.tsx | 37 ++++++++++++--- packages/ui/src/dialog.tsx | 46 ++++++++++++++++++- packages/ui/src/toast.tsx | 39 +++++++++++++--- 15 files changed, 239 insertions(+), 59 deletions(-) diff --git a/apps/hq-web/src/Layout.tsx b/apps/hq-web/src/Layout.tsx index bf7a036..b47d4d6 100644 --- a/apps/hq-web/src/Layout.tsx +++ b/apps/hq-web/src/Layout.tsx @@ -80,11 +80,13 @@ export function Layout() { }, []) // Tab title tracks the active nav section (spec §3); record routes fall back to - // their section title since they aren't separate nav items. + // their section title since they aren't separate nav items. Exact match wins + // before prefix match so /documents/new titles as 'New Document', not 'Documents'. useEffect(() => { - const item = NAV_GROUPS.flatMap((g) => g.items).find((i) => - i.to === '/' ? location.pathname === '/' : location.pathname.startsWith(i.to)) - document.title = item !== undefined && item.to !== '/' ? `${item.label} · SiMS HQ` : 'SiMS HQ' + const items = NAV_GROUPS.flatMap((g) => g.items) + const item = items.find((i) => location.pathname === i.to) + ?? items.find((i) => i.to !== '/' && location.pathname.startsWith(i.to)) + document.title = item !== undefined ? `${item.label} · SiMS HQ` : 'SiMS HQ' }, [location.pathname]) const staticItems = useMemo(() => { @@ -200,7 +202,15 @@ export function Layout() { )} {/* D18 WS-E: the user chip opens a small menu — Profile / Logout. */} - + { + if (!e.currentTarget.contains(e.relatedTarget as Node | null)) setUserOpen(false) + }} + > + {error !== undefined && {error}} diff --git a/apps/hq-web/src/app.css b/apps/hq-web/src/app.css index 1eea423..428fde0 100644 --- a/apps/hq-web/src/app.css +++ b/apps/hq-web/src/app.css @@ -92,7 +92,9 @@ } .hq-user:hover, .hq-user[aria-expanded='true'] { background: var(--bg-hover); border-color: var(--border); } .hq-user-menu { - position: absolute; top: calc(100% + 6px); right: 0; z-index: 60; + /* Below the dialog scrim (50) so an opened modal covers the dropdown; above the + mobile drawer (40) and page chrome. */ + position: absolute; top: calc(100% + 6px); right: 0; z-index: 45; min-width: 150px; padding: 4px; background: var(--bg-raised); border: 1px solid var(--border); border-radius: var(--radius); box-shadow: var(--shadow-lg); @@ -125,6 +127,7 @@ flex: 1; background: #1a1a17; color: #eae8e2; display: flex; flex-direction: column; justify-content: center; padding: 48px 56px; } +.login-brand .login-mark { margin-bottom: 16px; } .login-brand h1 { font-size: 34px; margin: 0 0 8px; letter-spacing: -0.02em; } .login-brand p { color: #a8a69c; max-width: 44ch; } .login-form { flex: 0 0 440px; display: flex; align-items: center; justify-content: center; padding: 24px; } @@ -204,10 +207,18 @@ table.wf { display: block; overflow-x: auto; } /* ================= Settings hub (rail + panels) ================= */ .set-wrap { display: flex; gap: 24px; } .set-rail { flex: 0 0 190px; display: flex; flex-direction: column; gap: 2px; } -.set-rail button { text-align: left; border: none; background: none; padding: 8px 12px; border-radius: 8px; font: 500 13.5px var(--font); color: var(--text-dim); cursor: pointer; } +.set-rail button { text-align: left; border: none; background: none; padding: 8px 12px; border-radius: var(--radius); font: 500 13.5px var(--font); color: var(--text-dim); cursor: pointer; } .set-rail button:hover { background: var(--bg-hover); color: var(--text); } .set-rail button.active { background: var(--accent-soft); color: var(--accent-strong); font-weight: 600; } .set-panel { flex: 1; min-width: 0; max-width: 860px; } +/* Panel section headers sit under the 22px page h1 — pin them to the app's type + scale instead of the ~21px UA default that nearly ties with the page title. */ +.set-panel h2 { font-size: 17px; margin: 0 0 4px; } .set-card { border: 1px solid var(--border); border-radius: var(--radius); padding: 14px 16px; } .set-card h3 { margin: 0 0 8px; font-size: 14px; } -@media (max-width: 900px) { .set-wrap { flex-direction: column; } .set-rail { flex-direction: row; flex-wrap: wrap; } } +@media (max-width: 900px) { + .set-wrap { flex-direction: column; } + /* flex-basis: auto — the desktop 190px basis is a width in row layout, but once + .set-wrap flips to column it would pin the rail's HEIGHT to 190px. */ + .set-rail { flex-direction: row; flex-wrap: wrap; flex-basis: auto; } +} diff --git a/apps/hq-web/src/components/ClientFormDialog.tsx b/apps/hq-web/src/components/ClientFormDialog.tsx index f85b1bf..26976b9 100644 --- a/apps/hq-web/src/components/ClientFormDialog.tsx +++ b/apps/hq-web/src/components/ClientFormDialog.tsx @@ -92,7 +92,7 @@ export function ClientFormDialog(props: { footer={ <> - + } > diff --git a/apps/hq-web/src/components/SendDialog.tsx b/apps/hq-web/src/components/SendDialog.tsx index b8b4b1b..f240ddc 100644 --- a/apps/hq-web/src/components/SendDialog.tsx +++ b/apps/hq-web/src/components/SendDialog.tsx @@ -1,4 +1,4 @@ -import { useEffect, useMemo, useState } from 'react' +import { useEffect, useMemo, useRef, useState } from 'react' import { Button, Dialog, Field, Notice } from '@sims/ui' import { getEmailStatus, type Client, type Doc, type EmailStatus } from '../api' @@ -20,13 +20,19 @@ export function SendDialog(props: { const [other, setOther] = useState('') const [subject, setSubject] = useState('') const [gmail, setGmail] = useState() + const wasOpen = useRef(false) + // Prefill ONLY on the false→true open transition — emails/doc are read fresh at + // that moment, but a late client fetch (or doc identity change) while the dialog + // is open must not wipe a half-typed recipient or an edited subject. useEffect(() => { - if (!props.open) return - setPick(emails[0] ?? OTHER) - setOther('') - setSubject(`${props.doc.docType} ${props.doc.docNo ?? ''}`.trim()) - getEmailStatus().then(setGmail).catch(() => setGmail(undefined)) + if (props.open && !wasOpen.current) { + setPick(emails[0] ?? OTHER) + setOther('') + setSubject(`${props.doc.docType} ${props.doc.docNo ?? ''}`.trim()) + getEmailStatus().then(setGmail).catch(() => setGmail(undefined)) + } + wasOpen.current = props.open }, [props.open, emails, props.doc]) const to = pick === OTHER ? other.trim() : pick diff --git a/apps/hq-web/src/pages/Employees.tsx b/apps/hq-web/src/pages/Employees.tsx index 027bfad..cb40586 100644 --- a/apps/hq-web/src/pages/Employees.tsx +++ b/apps/hq-web/src/pages/Employees.tsx @@ -178,7 +178,7 @@ function EmployeeDialog(props: { state: DialogState | undefined; onClose: () => footer={ <> - + } > diff --git a/apps/hq-web/src/pages/Modules.tsx b/apps/hq-web/src/pages/Modules.tsx index 51a0e4b..e0c5696 100644 --- a/apps/hq-web/src/pages/Modules.tsx +++ b/apps/hq-web/src/pages/Modules.tsx @@ -156,7 +156,9 @@ function ModuleClients(props: { module: Module }) { {assigning && ( -
+ // wf-card, not the dead 'wf-form' class (no rules exist for it anywhere) — + // the assign panel gets the same contained-card chrome as every other inline panel. +
searchAssign(e.target.value)} /> @@ -182,7 +184,7 @@ function ModuleClients(props: { module: Module }) {
)} {notifying && managerial && ( -
+
setSubject(e.target.value)} />