From f9b16cac8bcebff0e72e49790f11172f13dd24cd Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 19 Jul 2026 01:43:23 +0800 Subject: [PATCH 1/5] fix(ui): complete component library remediation --- docs/design-system.md | 17 +- package.json | 3 +- scripts/check-design-system-contract.mjs | 301 ++ scripts/design-system-contract-baseline.json | 5 + src/app/globals.css | 194 +- src/app/layout.tsx | 4 +- src/app/therapy-compass/layout.tsx | 1 + src/components/AccessibleTable.tsx | 2 +- src/components/ClinicalDashboard.tsx | 2 +- src/components/DocumentManagementActions.tsx | 33 +- src/components/DocumentViewer.tsx | 26 +- src/components/applications-launcher-page.tsx | 4 +- .../clinical-dashboard/ClinicalSidebar.tsx | 8 +- .../account-setup-dialog.tsx | 29 +- .../clinical-dashboard/auth-panel.tsx | 20 +- .../clinical-dashboard/cross-mode-links.tsx | 8 +- .../clinical-dashboard/differentials-home.tsx | 6 +- .../clinical-dashboard/document-admin.tsx | 4 +- .../clinical-dashboard/document-results.tsx | 4 +- .../document-search-results.tsx | 24 +- .../clinical-dashboard/evidence-panels.tsx | 8 +- .../favourites-command-library-page.tsx | 2 +- .../clinical-dashboard/favourites-hub.tsx | 22 +- .../favourites-library-nav.tsx | 8 +- .../clinical-dashboard/image-lightbox.tsx | 4 +- .../master-search-header.tsx | 26 +- .../clinical-dashboard/mode-action-popup.tsx | 5 +- .../clinical-dashboard/settings-dialog.tsx | 14 +- .../clinical-dashboard/signed-image.tsx | 2 +- .../universal-search-command-surface.tsx | 2 +- .../clinical-dashboard/visual-evidence.tsx | 8 +- .../differentials/diagnosis-map-panel.tsx | 12 +- .../differential-detail-page.tsx | 18 +- .../differential-presentation-actions.tsx | 2 +- ...ifferential-presentation-workflow-page.tsx | 18 +- .../differential-stream-page.tsx | 4 +- .../non-pdf-source-preview.tsx | 8 +- .../document-viewer/pdf-canvas-viewer.tsx | 8 +- src/components/dsm/dsm-comparison-page.tsx | 2 +- src/components/dsm/dsm-diagnosis-page.tsx | 2 +- .../dsm-differential-considerations-page.tsx | 4 +- src/components/dsm/dsm-search-page.tsx | 2 +- .../factsheets/factsheet-detail-page.tsx | 8 +- src/components/forms/form-detail-page.tsx | 10 +- src/components/pwa-lifecycle.tsx | 4 +- .../services/service-detail-page.tsx | 6 +- .../services/services-navigator-page.tsx | 2 +- src/components/therapy-compass/bindings.tsx | 109 +- src/components/therapy-compass/controls.ts | 36 +- src/components/therapy-compass/nav.tsx | 147 +- .../therapy-compass/screens/brief-screen.tsx | 274 +- .../screens/compare-screen.tsx | 221 +- .../therapy-compass/screens/detail-screen.tsx | 243 +- .../therapy-compass/screens/other-screen.tsx | 70 +- .../screens/pathways-screen.tsx | 173 +- .../screens/recommend-screen.tsx | 193 +- .../therapy-compass/screens/search-screen.tsx | 69 +- .../therapy-compass/screens/sheets-screen.tsx | 327 +- src/components/therapy-compass/style-utils.ts | 29 - src/components/therapy-compass/styles.tsx | 83 - .../therapy-compass/therapy-card.tsx | 97 +- .../therapy-compass/therapy-compass.css | 3433 +++++++++++++++++ src/components/therapy-compass/ui.tsx | 113 +- src/components/therapy-compass/workspace.tsx | 57 +- src/components/ui-primitives.tsx | 66 +- src/lib/theme.ts | 6 + tests/theme.test.ts | 32 +- ...herapy-compass-responsive-contract.test.ts | 41 +- tests/ui-accessibility.spec.ts | 66 + tests/ui-overlay-css-contract.test.ts | 4 +- tests/ui-primitives.dom.test.tsx | 28 +- 71 files changed, 4944 insertions(+), 1879 deletions(-) create mode 100644 scripts/check-design-system-contract.mjs create mode 100644 scripts/design-system-contract-baseline.json delete mode 100644 src/components/therapy-compass/style-utils.ts delete mode 100644 src/components/therapy-compass/styles.tsx create mode 100644 src/components/therapy-compass/therapy-compass.css diff --git a/docs/design-system.md b/docs/design-system.md index d77766b42..b8c0cbee4 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -18,7 +18,10 @@ contract and the code — not reinvention. If a change genuinely needs a new dir `src/app/globals.css` (`:root` + `.dark`). No raw Tailwind palette classes (`red-50`, `slate-200`, `bg-white`) and no hex values in components. **If you typed a hex or a Tailwind colour name in a component, you broke dark mode** — those values have no `.dark` override. - The only sanctioned exception: third-party brand marks (Microsoft/Google OAuth tiles). + Sanctioned raw-colour exceptions are explicit and narrow: brand artwork, diagnostic-only + visualizations, generated OpenGraph artwork, emergency error fallbacks, and the scoped + fixed-white Therapy patient sheet. `scripts/check-design-system-contract.mjs` owns the + path allowlist; adding a category requires documenting why semantic app tokens are wrong. - **Semantic vs categorical vs brand.** Three token families, never interchangeable: - Semantic triads (`--info/-soft/-border`, `--success-*`, `--warning-*`, `--danger-*`) mean something happened or matters clinically. Green is success-only; red is safety/danger-only. @@ -167,12 +170,14 @@ image"}` — never a possibly-empty variable alone. 1. `npm run verify:cheap` — lint, typecheck, unit tests, runtime + sitemap checks (offline-safe). 2. `npm run ensure` then `npm run verify:ui` — Chromium Playwright (smoke, stress, accessibility, tools, overlap). Required for any UI/styling/routing change. -3. `node scripts/check-type-scale.mjs` — the count must not exceed the recorded baseline. -4. Manual dark-mode pass on every screen you touched (theme toggle in the sidebar). -5. Reduced-motion + forced-colors spot check on touched surfaces +3. `npm run check:design-system-contract` — production-only raw colours, literal shadows, + Therapy inline-parser/style debt, and tap-token drift must not exceed the recorded baseline. +4. `node scripts/check-type-scale.mjs` — the count must not exceed the recorded baseline. +5. Manual dark-mode pass on every screen you touched (theme toggle in the sidebar). +6. Reduced-motion + forced-colors spot check on touched surfaces (`ui-accessibility.spec.ts` covers the automated slice; emulate in devtools for the rest). -6. `npm run format:check`. -7. Fill the PR template; the clinical-governance preflight applies only if you touched +7. `npm run format:check`. +8. Fill the PR template; the clinical-governance preflight applies only if you touched ingestion/answer/search/source-access surfaces — pure UI work states that explicitly. ## 10. File conventions diff --git a/package.json b/package.json index a2f67ddb8..68251c959 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,7 @@ "test:e2e:visual": "node scripts/run-playwright.mjs --config=playwright.visual.config.ts", "test:cross-tenant:staging": "node scripts/run-tsx.mjs scripts/test-cross-tenant-staging.ts", "verify:cheap": "node scripts/run-heavy.mjs --npm-script verify:cheap:internal", - "verify:cheap:internal": "npm run check:runtime && npm run check:github-actions && npm run check:ci-scope && npm run check:ci-triage && npm run check:pr-policy && npm run sitemap:check && npm run brand:check && npm run check:type-scale && npm run check:icon-scale && npm run check:function-grants && npm run check:owner-scope && npm run lint && npm run typecheck && npm run test", + "verify:cheap:internal": "npm run check:runtime && npm run check:github-actions && npm run check:ci-scope && npm run check:ci-triage && npm run check:pr-policy && npm run sitemap:check && npm run brand:check && npm run check:type-scale && npm run check:icon-scale && npm run check:design-system-contract && npm run check:function-grants && npm run check:owner-scope && npm run lint && npm run typecheck && npm run test", "verify:pr-local": "node scripts/verify-pr-local.mjs", "verify:ui": "npm run check:runtime && npm run test:e2e:pr", "verify:release": "npm run check:runtime && npm run lint && npm run typecheck && npm run test && npm run build && npm run test:e2e && npm run check:production-readiness && npm run governance:release && npm run eval:quality:release", @@ -103,6 +103,7 @@ "check:july8-live-batch": "node scripts/run-tsx.mjs scripts/check-july8-live-batch.ts", "check:type-scale": "node scripts/check-type-scale.mjs --strict", "check:icon-scale": "node scripts/check-icon-scale.mjs --strict", + "check:design-system-contract": "node scripts/check-design-system-contract.mjs", "check:function-grants": "node scripts/check-function-grants.mjs", "check:owner-scope": "node scripts/check-owner-scope-api.mjs", "recover:ingestion": "node scripts/run-tsx.mjs scripts/recover-ingestion-queue.ts", diff --git a/scripts/check-design-system-contract.mjs b/scripts/check-design-system-contract.mjs new file mode 100644 index 000000000..97fc6c98b --- /dev/null +++ b/scripts/check-design-system-contract.mjs @@ -0,0 +1,301 @@ +import fs from "node:fs"; +import path from "node:path"; +import process from "node:process"; +import ts from "typescript"; + +const ROOT = process.cwd(); +const SRC_ROOT = path.join(ROOT, "src"); +const BASELINE_PATH = path.join(ROOT, "scripts", "design-system-contract-baseline.json"); +const PRINT_METRICS = process.argv.includes("--print-metrics"); +const SOURCE_EXTENSIONS = new Set([".css", ".ts", ".tsx"]); +const LEGACY_TAP_CLASS = /(?:^|\s)(?:h|w|min-h|min-w|size)-11(?=\s|$)/g; +const LEGACY_TAP_CLASS_TEST = /(?:^|\s)(?:h|w|min-h|min-w|size)-11(?=\s|$)/; +const RAW_COLOR = /#[0-9a-f]{3,8}\b|\b(?:rgb|rgba|hsl|hsla|oklch)\(/gi; +const LITERAL_SHADOW_CLASS = /shadow-\[(?!var\()[^\]]+\]/g; +const CUSTOM_CONTROL_CLASS_PROP = + /(?:closeButtonClassName|sheetCloseButtonClassName|buttonClassName|triggerClassName)\s*=\s*(?:"[^"]*|`[^`]*)\b(?:h|w|min-h|min-w|size)-11\b/g; + +// Raw literals are permitted only at the source-of-truth or where a fixed +// external/rendering contract makes semantic app tokens inappropriate. +const RAW_COLOR_EXEMPTIONS = [ + { category: "global theme tokens", pattern: /^src\/app\/globals\.css$/ }, + { + category: "brand artwork", + pattern: + /^src\/(?:lib\/brand-(?:mark\.ts|image\.tsx)|components\/clinical-dashboard\/(?:brand|provider-brand-icons)\.tsx)$/, + }, + { + category: "diagnostic visualizations", + pattern: /^src\/components\/(?:web-vitals-reporter|clinical-dashboard\/visual-evidence)\.tsx$/, + }, + { category: "OpenGraph artwork", pattern: /^src\/app\/opengraph-image\.tsx$/ }, + { category: "error fallbacks", pattern: /^src\/(?:app\/global-error|components\/route-error-boundary)\.tsx$/ }, + { category: "printable Therapy paper", pattern: /^src\/components\/therapy-compass\/therapy-compass\.css$/ }, + { + category: "printable factsheet paper", + pattern: /^src\/components\/factsheets\/factsheet-detail-page\.tsx$/, + }, +]; + +const toPosix = (value) => value.split(path.sep).join("/"); + +function isPrototype(relativePath) { + return ( + relativePath.includes("/mockups/") || + relativePath.includes("-mockup") || + relativePath.includes("/favourites-page-mockups/") || + relativePath.includes("/calculator-mockups/") + ); +} + +function walk(directory) { + return fs.readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { + const absolutePath = path.join(directory, entry.name); + if (entry.isDirectory()) return walk(absolutePath); + if (!SOURCE_EXTENSIONS.has(path.extname(entry.name))) return []; + const relativePath = toPosix(path.relative(ROOT, absolutePath)); + return isPrototype(relativePath) ? [] : [{ absolutePath, relativePath }]; + }); +} + +function countMatches(text, pattern) { + return [...text.matchAll(pattern)].length; +} + +function withoutComments(text) { + return text.replace(/\/\*[\s\S]*?\*\//g, "").replace(/(^|[^:])\/\/.*$/gm, "$1"); +} + +function rawColorIsExempt(relativePath) { + return RAW_COLOR_EXEMPTIONS.some(({ pattern }) => pattern.test(relativePath)); +} + +function jsxClassText(attribute) { + const initializer = attribute.initializer; + if (!initializer) return ""; + if (ts.isStringLiteral(initializer)) return initializer.text; + if (!ts.isJsxExpression(initializer) || !initializer.expression) return ""; + if (ts.isStringLiteralLike(initializer.expression) || ts.isNoSubstitutionTemplateLiteral(initializer.expression)) { + return initializer.expression.text; + } + if (ts.isTemplateExpression(initializer.expression)) { + return [ + initializer.expression.head.text, + ...initializer.expression.templateSpans.map((span) => span.literal.text), + ].join(" "); + } + return initializer.expression.getText(); +} + +function findInteractiveTapLiterals(file) { + if (!file.relativePath.endsWith(".tsx")) return []; + const sourceText = fs.readFileSync(file.absolutePath, "utf8"); + const source = ts.createSourceFile(file.relativePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); + const findings = []; + const interactiveTags = new Set(["a", "button", "input", "select", "summary", "textarea"]); + + function inspectOpeningElement(node) { + const tag = node.tagName.getText(source); + if (!interactiveTags.has(tag)) return; + const classAttribute = node.attributes.properties.find( + (attribute) => ts.isJsxAttribute(attribute) && attribute.name.getText(source) === "className", + ); + if (!classAttribute || !ts.isJsxAttribute(classAttribute)) return; + const classText = jsxClassText(classAttribute); + if (!LEGACY_TAP_CLASS_TEST.test(classText)) return; + const line = source.getLineAndCharacterOfPosition(classAttribute.getStart(source)).line + 1; + findings.push(`${file.relativePath}:${line}`); + } + + function visit(node) { + if (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) inspectOpeningElement(node); + ts.forEachChild(node, visit); + } + + visit(source); + return findings; +} + +function findTherapyButtonsWithoutBaseClass(file) { + if (!file.relativePath.startsWith("src/components/therapy-compass/") || !file.relativePath.endsWith(".tsx")) + return []; + const sourceText = fs.readFileSync(file.absolutePath, "utf8"); + const source = ts.createSourceFile(file.relativePath, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TSX); + const findings = []; + + function visit(node) { + if ( + (ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node)) && + node.tagName.getText(source) === "button" + ) { + const classAttribute = node.attributes.properties.find( + (attribute) => ts.isJsxAttribute(attribute) && attribute.name.getText(source) === "className", + ); + const classText = classAttribute && ts.isJsxAttribute(classAttribute) ? jsxClassText(classAttribute) : ""; + if (!classText.includes("tc-btn")) { + const line = source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1; + findings.push(`${file.relativePath}:${line}`); + } + } + ts.forEachChild(node, visit); + } + + visit(source); + return findings; +} + +const files = walk(SRC_ROOT); +const contents = new Map(files.map((file) => [file.relativePath, fs.readFileSync(file.absolutePath, "utf8")])); +const textAt = (relativePath) => contents.get(relativePath) ?? ""; +const failures = []; +const assert = (condition, message) => { + if (!condition) failures.push(message); +}; + +const metrics = { + rawColorLiterals: 0, + literalShadowClasses: 0, + legacyTapClasses: 0, +}; + +for (const file of files) { + const source = textAt(file.relativePath); + const contractSource = withoutComments(source); + if (!rawColorIsExempt(file.relativePath)) metrics.rawColorLiterals += countMatches(contractSource, RAW_COLOR); + metrics.literalShadowClasses += countMatches(contractSource, LITERAL_SHADOW_CLASS); + metrics.legacyTapClasses += countMatches(contractSource, LEGACY_TAP_CLASS); + assert( + !CUSTOM_CONTROL_CLASS_PROP.test(source), + `${file.relativePath} contains a legacy 44px class in a control class prop`, + ); + CUSTOM_CONTROL_CLASS_PROP.lastIndex = 0; +} + +const interactiveTapFindings = files.flatMap(findInteractiveTapLiterals); +assert( + interactiveTapFindings.length === 0, + `interactive elements still use literal *-11 tap classes: ${interactiveTapFindings.join(", ")}`, +); + +const therapyFiles = files.filter(({ relativePath }) => relativePath.startsWith("src/components/therapy-compass/")); +const therapyButtonsWithoutBaseClass = therapyFiles.flatMap(findTherapyButtonsWithoutBaseClass); +const therapySource = therapyFiles.map(({ relativePath }) => textAt(relativePath)).join("\n"); +const therapyInlineStyleFindings = therapyFiles.flatMap(({ relativePath }) => { + const source = textAt(relativePath); + return source + .split(/\r?\n/) + .map((line, index) => ({ line, index: index + 1 })) + .filter(({ line }) => /style=\{/.test(line)) + .filter(({ line }) => { + if (relativePath.endsWith("/icons.tsx")) return !/style=\{style\}/.test(line); + if (relativePath.endsWith("/ui.tsx")) return !/--tc-meter-width/.test(line); + if (relativePath.endsWith("/screens/compare-screen.tsx")) return !/--tc-compare-columns/.test(line); + return true; + }) + .map(({ index }) => `${relativePath}:${index}`); +}); + +assert(!therapySource.includes("style={s("), "Therapy Compass still invokes the runtime style parser"); +assert(!therapySource.includes("style-utils"), "Therapy Compass still imports the runtime style parser"); +assert( + therapyButtonsWithoutBaseClass.length === 0, + `Therapy buttons bypass the shared interaction states: ${therapyButtonsWithoutBaseClass.join(", ")}`, +); +assert( + !fs.existsSync(path.join(ROOT, "src/components/therapy-compass/style-utils.ts")), + "style-utils.ts must stay retired", +); +assert( + !fs.existsSync(path.join(ROOT, "src/components/therapy-compass/styles.tsx")), + "the runtime Therapy style island must stay retired", +); +assert( + therapyInlineStyleFindings.length === 0, + `unscoped Therapy inline styles found: ${therapyInlineStyleFindings.join(", ")}`, +); +assert(!/outline\s*:\s*none/i.test(therapySource), "Therapy Compass suppresses a focus outline"); +assert(!therapySource.toLowerCase().includes("#8a94a3"), "the low-contrast patient-sheet gray returned"); + +const therapyCss = textAt("src/components/therapy-compass/therapy-compass.css"); +assert(!/(?:^|[^0-9])44px/.test(therapyCss), "Therapy Compass CSS contains a literal 44px tap target"); +assert(therapyCss.includes("--tc-paper-muted: #5b6472"), "the fixed paper palette must keep its accessible muted ink"); +assert( + therapyCss.includes('.tc-paper [contenteditable="true"]:focus-visible'), + "patient-sheet editing needs a visible focus state", +); +assert(therapyCss.includes(".tc-btn:hover:not(:disabled)"), "Therapy buttons need a hover state"); +assert(therapyCss.includes(".tc-btn:disabled"), "Therapy buttons need a disabled state"); + +const paperRules = therapyCss.slice( + therapyCss.indexOf(".tc-root .tc-screens-sheets-screen-023"), + therapyCss.indexOf(".tc-root .tc-screens-sheets-screen-050"), +); +assert( + !/var\(--(?:background|surface|border|text|clinical|command|focus)/.test(paperRules), + "patient-sheet paper rules leaked theme-reactive application tokens", +); + +const semanticSources = [ + "src/components/therapy-compass/nav.tsx", + "src/components/therapy-compass/therapy-card.tsx", + "src/components/therapy-compass/screens/brief-screen.tsx", + "src/components/therapy-compass/screens/compare-screen.tsx", + "src/components/therapy-compass/screens/search-screen.tsx", + "src/components/therapy-compass/screens/sheets-screen.tsx", +] + .map(textAt) + .join("\n"); +assert(semanticSources.includes("aria-current"), "Therapy navigation needs aria-current"); +assert(semanticSources.includes("aria-pressed"), "Therapy toggles need aria-pressed"); +assert(semanticSources.includes("aria-selected"), "Therapy tabs need aria-selected"); +assert( + /disabled=\{items\.length === 0\}/.test(textAt("src/components/therapy-compass/screens/compare-screen.tsx")), + "empty compare Clear must be disabled", +); + +const globals = textAt("src/app/globals.css"); +assert(!/^\s*--space-\d+\s*:/m.test(globals), "unused --space-* tokens returned"); +const primitives = textAt("src/components/ui-primitives.tsx"); +assert( + primitives.includes('export const chatComposerInput = "chat-composer-input"'), + "composer input chrome must have one CSS owner", +); +assert(primitives.includes("aria-[invalid=true]"), "shared fields need an invalid state"); +assert(primitives.includes("read-only:"), "shared fields need a read-only state"); +assert(primitives.includes("export function AsyncButton"), "shared async button semantics are missing"); + +for (const target of [ + "src/components/DocumentViewer.tsx", + "src/components/clinical-dashboard/favourites-hub.tsx", + "src/components/clinical-dashboard/master-search-header.tsx", + "src/components/clinical-dashboard/mode-action-popup.tsx", + "src/components/clinical-dashboard/settings-dialog.tsx", +]) { + assert(!/\bring-white\b|\bbg-white\b/.test(textAt(target)), `${target} bypasses the shared glass/toggle recipes`); +} + +if (!PRINT_METRICS) { + assert(fs.existsSync(BASELINE_PATH), "design-system contract baseline is missing"); + if (fs.existsSync(BASELINE_PATH)) { + const baseline = JSON.parse(fs.readFileSync(BASELINE_PATH, "utf8")); + for (const [metric, value] of Object.entries(metrics)) { + assert(value <= baseline[metric], `${metric} increased from ${baseline[metric]} to ${value}`); + } + } +} + +if (PRINT_METRICS) { + console.log(JSON.stringify(metrics, null, 2)); + process.exit(failures.length === 0 ? 0 : 1); +} + +if (failures.length > 0) { + console.error("Design-system contract failed:\n"); + for (const failure of failures) console.error(`- ${failure}`); + process.exit(1); +} + +console.log( + `Design-system contract passed (${files.length} production files; raw colors ${metrics.rawColorLiterals}; literal shadows ${metrics.literalShadowClasses}; legacy tap classes ${metrics.legacyTapClasses}).`, +); +console.log(`Raw-color exemptions: ${RAW_COLOR_EXEMPTIONS.map(({ category }) => category).join(", ")}.`); diff --git a/scripts/design-system-contract-baseline.json b/scripts/design-system-contract-baseline.json new file mode 100644 index 000000000..3ac12017b --- /dev/null +++ b/scripts/design-system-contract-baseline.json @@ -0,0 +1,5 @@ +{ + "rawColorLiterals": 9, + "literalShadowClasses": 6, + "legacyTapClasses": 22 +} diff --git a/src/app/globals.css b/src/app/globals.css index 48dc21e36..3a1e28002 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -263,14 +263,6 @@ --shadow-lux: inset 0 1px 0 rgb(255 255 255 / 64%), 0 12px 34px rgb(8 16 24 / 7%); --shadow-inset: inset 0 1px 0 rgb(255 255 255 / 58%); --shadow-card: var(--shadow-tight); - --space-1: 0.25rem; - --space-2: 0.5rem; - --space-3: 0.75rem; - --space-4: 1rem; - --space-6: 1.5rem; - --space-8: 2rem; - --space-12: 3rem; - --space-16: 4rem; --safe-area-top: env(safe-area-inset-top, 0px); --safe-area-right: env(safe-area-inset-right, 0px); --safe-area-bottom: env(safe-area-inset-bottom, 0px); @@ -553,14 +545,9 @@ summary::-webkit-details-marker { * scripts/capture-chrome-parity.ts). Responsive padding overrides are nested * here so the whole class stays in one layer. * - * The COMPOSER PILL surface below (answer-footer-search-pill, a
) is now - * ALSO layered via a chatComposerShell base/delta split (see its comment). The - * pill-interior controls (answer-footer-search-input/-send/-action) and the - * composer POSITIONING chrome (*-composer-edge, *-search-edge/-dock/-backdrop, - * -pill-open, document-mobile-search-*) stay INTENTIONALLY UNLAYERED — the - * controls are / - + ) : ( @@ -245,14 +244,16 @@ export function DocumentManagementActions({ - + )} diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 05f21b300..b50f41bd6 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -54,6 +54,7 @@ import { fieldControl, fieldLabel, floatingControl, + glassOverlaySurface, InlineNotice, LoadingPanel, panel, @@ -1262,7 +1263,7 @@ function DocumentManualTagEditor({ @@ -2834,7 +2835,7 @@ export function DocumentViewer({ ? "Standard viewer with built-in fit and zoom controls." : "Sharper zoom — uses your browser's PDF engine to keep heavy-zoom pages crisp." } - className={cn(secondaryButton, "min-h-11 w-full justify-center px-3 text-xs sm:w-auto")} + className={cn(secondaryButton, "min-h-tap w-full justify-center px-3 text-xs sm:w-auto")} > {useNativePdfViewer ? "Standard view" : "Sharper zoom"} @@ -3101,12 +3102,15 @@ export function DocumentViewer({ onBlurCapture={(event) => { if (!event.currentTarget.contains(event.relatedTarget as Node | null)) setComposerChromeFocused(false); }} - className="document-viewer-composer floating-composer-edge dashboard-composer-edge fixed z-40 mx-auto flex min-h-[56px] max-w-3xl items-center gap-2 rounded-full border border-[color:var(--border-lux)] bg-[color:var(--surface-lux)] px-2 shadow-[var(--shadow-lux)] ring-1 ring-white/35 dark:ring-white/10 backdrop-blur-xl max-sm:transition-transform max-sm:duration-200 max-sm:ease-out motion-reduce:transition-none" + className={cn( + glassOverlaySurface, + "document-viewer-composer floating-composer-edge dashboard-composer-edge fixed z-40 mx-auto flex min-h-[56px] max-w-3xl items-center gap-2 rounded-full bg-[color:var(--surface-lux)] px-2 shadow-[var(--shadow-lux)] max-sm:transition-transform max-sm:duration-200 max-sm:ease-out motion-reduce:transition-none", + )} > +
@@ -249,7 +260,7 @@ function ProviderButton({ provider, onClick }: { provider: SsoProvider; onClick: +
@@ -253,7 +255,7 @@ function ProviderButton({ provider, onClick }: { provider: "Apple" | "Google" | @@ -92,7 +92,7 @@ function CrossModeLinkChip({ link, Icon, query, onModeSearch }: CrossModeLinkCar logCrossModeLinkOpen(query, link)} - className="inline-flex min-h-11 min-w-0 items-center gap-2 px-2.5 transition hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[color:var(--focus)] md:min-h-9" + className="inline-flex min-h-tap min-w-0 items-center gap-2 px-2.5 transition hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[color:var(--focus)] md:min-h-9" > @@ -112,7 +112,7 @@ function CrossModeLinkChip({ link, Icon, query, onModeSearch }: CrossModeLinkCar // click would corrupt retrieval-quality/click telemetry. onClick={() => onModeSearch(link.modeId, link.modeSearchQuery)} aria-label={`Search ${link.title} in ${link.modeLabel}`} - className="grid min-h-11 w-11 shrink-0 place-items-center border-l border-[color:var(--border)] text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[color:var(--focus)] md:min-h-9 md:w-9" + className="grid min-h-tap w-tap shrink-0 place-items-center border-l border-[color:var(--border)] text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--clinical-accent)] focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[color:var(--focus)] md:min-h-9 md:w-9" > diff --git a/src/components/clinical-dashboard/differentials-home.tsx b/src/components/clinical-dashboard/differentials-home.tsx index e4ecdede7..40c54affd 100644 --- a/src/components/clinical-dashboard/differentials-home.tsx +++ b/src/components/clinical-dashboard/differentials-home.tsx @@ -284,7 +284,7 @@ function ResultTypeTabs({ aria-label={`${tab.label} (${tab.count})`} onClick={() => onFilterChange(tab.id)} className={cn( - "inline-flex min-h-11 shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border px-2.5 text-xs font-bold min-[390px]:text-sm", + "inline-flex min-h-tap shrink-0 items-center gap-1.5 whitespace-nowrap rounded-md border px-2.5 text-xs font-bold min-[390px]:text-sm", resultTypeTabFocusRing, active ? "border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)]" @@ -340,7 +340,7 @@ function SelectionToggle({ selected, onClick, label }: { selected: boolean; onCl aria-label={`${selected ? "Remove" : "Add"} ${label} ${selected ? "from" : "to"} comparison`} onClick={onClick} className={cn( - "grid h-11 w-11 shrink-0 place-items-center rounded-md border text-sm transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]", + "grid h-tap w-tap shrink-0 place-items-center rounded-md border text-sm transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]", selected ? "border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)]" : "border-[color:var(--border-strong)] bg-[color:var(--surface)] text-transparent hover:text-[color:var(--text-soft)]", @@ -1092,7 +1092,7 @@ function SearchResultsView({ View all catalogue matches ({results.length}) diff --git a/src/components/clinical-dashboard/document-admin.tsx b/src/components/clinical-dashboard/document-admin.tsx index 8a242779b..e5d8b8c4f 100644 --- a/src/components/clinical-dashboard/document-admin.tsx +++ b/src/components/clinical-dashboard/document-admin.tsx @@ -1038,7 +1038,7 @@ export function DocumentDrawer({
{documentDisplayTitle(document)}
- + ); } @@ -545,7 +545,7 @@ function SelectedDocumentEvidencePanel({
Open exact evidence @@ -554,7 +554,7 @@ function SelectedDocumentEvidencePanel({ onScopeDocument(document.document_id)} icon={Filter} - className="min-h-11 rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-raised)] px-2 text-xs" + className="min-h-tap rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface-raised)] px-2 text-xs" aria-label={`Scope search to ${document.title}`} > Scope @@ -562,7 +562,7 @@ function SelectedDocumentEvidencePanel({ onAnswerFromDocument(document.document_id)} icon={Sparkles} - className="min-h-11 rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-2 text-xs text-[color:var(--clinical-accent)]" + className="min-h-tap rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-2 text-xs text-[color:var(--clinical-accent)]" aria-label={`Answer from ${document.title}`} > Answer @@ -667,7 +667,7 @@ function SearchRecordResults({

{service.title} @@ -679,7 +679,7 @@ function SearchRecordResults({ href={recordRoute(service.slug)} className={cn( floatingControl, - "inline-flex min-h-11 w-full justify-center rounded-lg px-3 text-sm text-[color:var(--clinical-accent)] sm:w-auto", + "inline-flex min-h-tap w-full justify-center rounded-lg px-3 text-sm text-[color:var(--clinical-accent)] sm:w-auto", )} aria-label={`Open ${service.title}`} > @@ -1041,7 +1041,7 @@ function DocumentSearchResultsPanelImpl({

{documentDisplayTitle(document)} @@ -1091,7 +1091,7 @@ function DocumentSearchResultsPanelImpl({ onClick={() => setSelectedDocumentId(document.document_id)} icon={Sparkles} className={cn( - "min-h-11 rounded-lg px-2.5 text-xs", + "min-h-tap rounded-lg px-2.5 text-xs", selected ? "bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]" : "text-[color:var(--text)]", @@ -1102,7 +1102,7 @@ function DocumentSearchResultsPanelImpl({ {contextualOpenLabel(document)} @@ -1110,7 +1110,7 @@ function DocumentSearchResultsPanelImpl({ onScopeDocument(document.document_id)} icon={Filter} - className="min-h-11 rounded-lg px-2.5 text-xs text-[color:var(--text)]" + className="min-h-tap rounded-lg px-2.5 text-xs text-[color:var(--text)]" aria-label={`Scope search to ${document.title}`} > Scope @@ -1118,7 +1118,7 @@ function DocumentSearchResultsPanelImpl({ onAnswerFromDocument(document.document_id)} icon={Sparkles} - className="ml-auto min-h-11 rounded-lg px-2.5 text-xs text-[color:var(--clinical-accent)] hover:bg-[color:var(--clinical-accent-soft)]" + className="ml-auto min-h-tap rounded-lg px-2.5 text-xs text-[color:var(--clinical-accent)] hover:bg-[color:var(--clinical-accent-soft)]" aria-label={`Answer from ${document.title}`} > Answer diff --git a/src/components/clinical-dashboard/evidence-panels.tsx b/src/components/clinical-dashboard/evidence-panels.tsx index 47d3c984a..00db8fd13 100644 --- a/src/components/clinical-dashboard/evidence-panels.tsx +++ b/src/components/clinical-dashboard/evidence-panels.tsx @@ -695,7 +695,7 @@ export function ClinicalNotesChecklistPanel({ aria-label={`${tab.label} (${tab.count})`} onClick={() => setRequestedTab(tab.id)} className={cn( - "inline-flex min-h-11 min-w-0 items-center justify-center gap-1.5 rounded-md px-2 text-xs font-semibold leading-none transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]", + "inline-flex min-h-tap min-w-0 items-center justify-center gap-1.5 rounded-md px-2 text-xs font-semibold leading-none transition focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]", selected ? "bg-[color:var(--clinical-accent)] text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-tight)]" : "text-[color:var(--text-muted)] hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)]", @@ -824,7 +824,7 @@ export function ClinicalNotesChecklistPanel({ {bestSource ? (