From f7fbd2aa2708e2b0ad8bbc919c6e1b4e135ec916 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:44:14 +0800 Subject: [PATCH 01/10] feat(icons): add --spacing-icon-* scale + icon-scale guard (report mode) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce a documented icon glyph size scale in the @theme block of globals.css (size-icon-xs..xl → 12/14/16/20/24px), parallel to how --spacing-tap generates size-tap. This retires the convention-only h-4 w-4 literal and lets a step carry a responsive variant (size-icon-md sm:size-icon-lg). Adds scripts/check-icon-scale.mjs (report mode) wired into verify:cheap. It flags unambiguous drift — the retired 4.5 (18px) half-step and arbitrary 12–24px icon sizes — but deliberately does NOT ban raw h-4 w-4, which also sizes non-icons (ToggleSwitch knob, status dots). Documents the scale in design-system.md §2 (Type & icon scale). Additive only: no call sites migrated yet. Co-Authored-By: Claude Opus 4.8 --- docs/design-system.md | 20 ++++++++- package.json | 3 +- scripts/check-icon-scale.mjs | 86 ++++++++++++++++++++++++++++++++++++ src/app/globals.css | 13 ++++++ 4 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 scripts/check-icon-scale.mjs diff --git a/docs/design-system.md b/docs/design-system.md index dfa24d858..ec1c64c4a 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -49,7 +49,9 @@ When you meet a pre-token hardcode (mockups being promoted, old branches), map i | `slate-200` / `slate-500` / `slate-600` | `var(--border)` / `var(--text-soft)` / `var(--text-muted)` | | ad-hoc `rgba(...)` shadows | `var(--shadow-tight/soft/hover/elevated/inset)` or `--glow-*` | -## 2. Type scale +## 2. Type & icon scale + +### Type Named steps live in the `@theme` block of `globals.css` and are **size-only** (no baked line-height/tracking — set `leading-*`/`tracking-*` at the call site): @@ -69,6 +71,22 @@ line-height/tracking — set `leading-*`/`tracking-*` at the call site): on hero/mode-home titles, and `*-mockups` files. Don't add scale steps for one-off display sizes. +### Icon size + +Icon **glyphs** use the parallel `--spacing-icon-*` scale in `@theme`: +`size-icon-xs` 12 · `size-icon-sm` 14 · `size-icon-md` 16 (default) · `size-icon-lg` 20 · +`size-icon-xl` 24 (px). These generate `size-icon-*` / `h-icon-*` / `w-icon-*`, exactly like +`--spacing-tap` → `size-tap`. + +- Prefer `size-icon-md` over raw `h-4 w-4` for an icon glyph. `npm run check:icon-scale` counts + off-scale drift — the retired `4.5` (18px) half-step and arbitrary `≤24px` `h-[Npx]`. + **Ratchet:** report-only now; flip to `--strict` once the cluster migration lands. +- **Responsive** icons add a breakpoint variant — `size-icon-md sm:size-icon-lg`. Reserve it for + a few roles (nav, composer, hero, panel headings); most icons stay one fixed size. +- **Not** for container tiles (`iconTile` h-9, empty-state tile h-10) or non-icon boxes (the + `ToggleSwitch` knob, status dots) — those keep the integer spacing scale. Icon glyph size is + independent of the 44px tap target (§3), which stays on `--spacing-tap`. + ## 3. Spacing & tap targets - 4px grid via Tailwind spacing; safe-area env paddings on shell edges. diff --git a/package.json b/package.json index 950af5169..e6cabbfcd 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,7 @@ "test:e2e:advisory": "node scripts/run-playwright.mjs --project=chromium --grep-invert @critical", "test:e2e:chromium": "node scripts/run-playwright.mjs --project=chromium", "test:e2e:visual": "node scripts/run-playwright.mjs --config=playwright.visual.config.ts", - "verify:cheap": "npm run check:runtime && npm run check:github-actions && npm run sitemap:check && npm run check:type-scale && npm run lint && npm run typecheck && npm run test", + "verify:cheap": "npm run check:runtime && npm run check:github-actions && npm run sitemap:check && npm run check:type-scale && npm run check:icon-scale && 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:chromium", "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", @@ -70,6 +70,7 @@ "check:m13-migration": "node scripts/run-tsx.mjs scripts/check-m13-migration.ts", "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", "recover:ingestion": "node scripts/run-tsx.mjs scripts/recover-ingestion-queue.ts", "registry:seed": "node scripts/run-tsx.mjs scripts/seed-registry-records.ts", "registry:embed": "node scripts/run-tsx.mjs scripts/embed-registry-records.ts", diff --git a/scripts/check-icon-scale.mjs b/scripts/check-icon-scale.mjs new file mode 100644 index 000000000..4d2aa0d9c --- /dev/null +++ b/scripts/check-icon-scale.mjs @@ -0,0 +1,86 @@ +#!/usr/bin/env node +// Guardrail/diagnostic: flags icon-sizing utilities that bypass the design +// icon-size scale. +// +// The scale lives in the @theme block of src/app/globals.css: +// --spacing-icon-xs 12px → size-icon-xs / h-icon-xs / w-icon-xs +// --spacing-icon-sm 14px → size-icon-sm +// --spacing-icon-md 16px → size-icon-md (default) +// --spacing-icon-lg 20px → size-icon-lg +// --spacing-icon-xl 24px → size-icon-xl +// +// It tracks two kinds of drift that are unambiguously icon sizing: +// 1. The retired `4.5` half-step (h-4.5 / w-4.5 / size-4.5, 18px) — always an +// icon size, replaced by size-icon-sm/-md/-lg per site. +// 2. Arbitrary square pixel sizes in the icon range (<= 24px), e.g. +// h-[18px] w-[18px] / size-[16px] — bypass the scale. +// +// It intentionally does NOT flag raw h-4 w-4 / h-5 w-5 etc.: those integer +// spacing steps also size non-icons (the ToggleSwitch knob, status dots, +// avatars, container tiles), so a blanket ban would false-positive. Migrating +// icon glyphs onto size-icon-* is a codemod, not a lint rule — see +// docs/design-system.md §2. Only the unambiguous drift above is enforced here. +// Mockups (*mockup*) are design-scratch and out of scope. +// +// Usage: +// node scripts/check-icon-scale.mjs report only, exit 0 (default) +// node scripts/check-icon-scale.mjs --strict exit 1 if any found (promote to a +// CI gate once the backlog is cleared) + +import { readFileSync } from "node:fs"; +import { execSync } from "node:child_process"; + +const strict = process.argv.includes("--strict"); + +// Retired 18px half-step: (min-)h-4.5 / (min-)w-4.5 / size-4.5. +const HALF_STEP = /\b(?:min-)?(?:h|w|size)-4\.5\b/g; +// Arbitrary square pixel sizes: (min-)h-[Npx] / (min-)w-[Npx] / size-[Npx]. +const ARBITRARY_PX = /\b(?:min-)?(?:h|w|size)-\[(\d+)px\]/g; + +// Tracked source files under src (fast; respects .gitignore). Exclude mockups. +const files = execSync("git ls-files src", { encoding: "utf8" }) + .split("\n") + .filter((f) => /\.(tsx?|jsx?)$/.test(f)) + .filter((f) => !/mockup/i.test(f)); + +const hits = []; +for (const file of files) { + let text; + try { + text = readFileSync(file, "utf8"); + } catch { + continue; + } + text.split("\n").forEach((line, i) => { + for (const m of line.matchAll(HALF_STEP)) { + hits.push({ file, line: i + 1, match: m[0] }); + } + for (const m of line.matchAll(ARBITRARY_PX)) { + // Only the icon glyph range (12–24px). Below 12px is dividers/dots/rules + // (e.g. w-[2px]); above 24px is avatars/tiles/shells — none are icon glyphs. + const px = Number(m[1]); + if (px >= 12 && px <= 24) hits.push({ file, line: i + 1, match: m[0] }); + } + }); +} + +if (hits.length === 0) { + console.log("✓ icon-scale: no off-scale icon sizes (4.5 half-step / arbitrary ≤24px) in src."); + process.exit(0); +} + +const byMatch = new Map(); +for (const h of hits) byMatch.set(h.match, (byMatch.get(h.match) ?? 0) + 1); +const fileCount = new Set(hits.map((h) => h.file)).size; + +console.log(`icon-scale: ${hits.length} off-scale icon sizes bypass the scale (across ${fileCount} files):\n`); +for (const [size, n] of [...byMatch.entries()].sort((a, b) => b[1] - a[1])) { + console.log(` ${String(n).padStart(4)} ${size}`); +} +console.log("\nPrefer a named @theme step from src/app/globals.css (size-icon-sm, size-icon-md, size-icon-lg, …)."); + +if (strict) { + console.error("\n✗ --strict: off-scale icon sizes present."); + process.exit(1); +} +process.exit(0); diff --git a/src/app/globals.css b/src/app/globals.css index 9a4cb8040..9d32d4d16 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -39,6 +39,19 @@ deep in sheets stay on min-h-12 (48px) — do not downgrade those to the token. */ --spacing-tap: 2.75rem; + /* Icon glyph size scale. Named spacing tokens so icon sizing is a documented + scale (size-icon-md is the 16px default) instead of raw `h-4 w-4` literals, + and so a step can carry a responsive variant: `size-icon-md sm:size-icon-lg`. + Generates size-icon-*, h-icon-*, w-icon-* exactly like --spacing-tap generates + size-tap. The `4.5` (18px) half-step these retire snaps to sm/md/lg per site. + NB: container tiles (iconTile h-9, empty-state tile h-10) are NOT glyphs and + stay on the integer spacing scale — this token is for the icon glyph itself. */ + --spacing-icon-xs: 0.75rem; /* 12px — inline markers, dense metadata */ + --spacing-icon-sm: 0.875rem; /* 14px — badge / chip / metadata icons */ + --spacing-icon-md: 1rem; /* 16px — default: buttons, nav, fields */ + --spacing-icon-lg: 1.25rem; /* 20px — header / primary controls */ + --spacing-icon-xl: 1.5rem; /* 24px — hero / composer emphasis */ + /* Type scale — sub-`xs` micro steps plus the 13–22px "between" steps the dense clinical UI reaches for. These are intentionally SIZE-ONLY: unlike Tailwind's default text-* tokens they carry no `--line-height` / From 18e748639834dd13da8370ad6d918994644fed4e Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 12 Jul 2026 01:52:25 +0800 Subject: [PATCH 02/10] refactor(icons): normalize deprecated lucide aliases to canonical names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Five deprecated↔renamed alias families were used side-by-side. Collapse each onto one canonical name, verified against lucide-react@1.22.0 as the same icon module (zero-pixel): AlertTriangle → TriangleAlert AlertCircle → CircleAlert CheckCircle2 → CircleCheck XCircle → CircleX HelpCircle → CircleHelp 102 whole-word renames across 24 production files. Mockups keep their imports (out of scope). typecheck / lint / prettier green; no call site renders differently. Co-Authored-By: Claude Opus 4.8 --- src/components/ClinicalDashboard.tsx | 10 ++++----- src/components/DocumentManagementActions.tsx | 4 ++-- src/components/DocumentOrganizationBadges.tsx | 6 ++--- src/components/DocumentViewer.tsx | 8 +++---- .../account-setup-dialog.tsx | 10 ++++----- .../clinical-dashboard/answer-content.tsx | 10 ++++----- src/components/clinical-dashboard/badges.tsx | 8 +++---- .../clinical-dashboard/evidence-panels.tsx | 20 ++++++++--------- .../master-search-header.tsx | 4 ++-- .../medication-prescribing-workspace.tsx | 4 ++-- .../medication-record-page.tsx | 4 ++-- .../clinical-dashboard/mode-action-popup.tsx | 4 ++-- .../clinical-dashboard/output-panel.tsx | 4 ++-- .../clinical-dashboard/relevance.tsx | 4 ++-- .../clinical-dashboard/settings-dialog.tsx | 4 ++-- .../universal-search-command-surface.tsx | 4 ++-- .../clinical-dashboard/visual-evidence.tsx | 10 ++++----- .../differentials/diagnosis-map-panel.tsx | 4 ++-- .../differential-detail-page.tsx | 22 +++++++++---------- ...ifferential-presentation-workflow-page.tsx | 10 ++++----- .../document-search-live-opener.tsx | 8 +++---- src/components/forms/form-detail-page.tsx | 12 +++++----- src/components/route-error-boundary.tsx | 4 ++-- .../services/service-detail-page.tsx | 14 ++++++------ 24 files changed, 96 insertions(+), 96 deletions(-) diff --git a/src/components/ClinicalDashboard.tsx b/src/components/ClinicalDashboard.tsx index b0635e586..0cb24f752 100644 --- a/src/components/ClinicalDashboard.tsx +++ b/src/components/ClinicalDashboard.tsx @@ -3,7 +3,7 @@ import { useRouter, useSearchParams } from "next/navigation"; import dynamic from "next/dynamic"; import { - AlertCircle, + CircleAlert, BookOpen, ChevronDown, Clock3, @@ -573,7 +573,7 @@ function PriorAnswerTurnSurface({ data-testid="prior-answer-source-review" className="mt-2 flex items-start gap-2 rounded-lg border border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] px-3 py-2 text-xs text-[color:var(--text-muted)]" > - + Review source match. Verify cited passages before relying on this previous answer. @@ -2982,7 +2982,7 @@ export function ClinicalDashboard({ ] as const; const renderSystemNotice = (className?: string) => ( (
- + {error}
{activeModeResultKind === "answer" && lastFailedQuery && ( diff --git a/src/components/DocumentManagementActions.tsx b/src/components/DocumentManagementActions.tsx index 4b9a8ad51..8b156a84e 100644 --- a/src/components/DocumentManagementActions.tsx +++ b/src/components/DocumentManagementActions.tsx @@ -1,6 +1,6 @@ "use client"; -import { AlertTriangle, Check, Loader2, Pencil, Trash2 } from "lucide-react"; +import { TriangleAlert, Check, Loader2, Pencil, Trash2 } from "lucide-react"; import { FormEvent, useRef, useState } from "react"; import { cn, @@ -174,7 +174,7 @@ export function DocumentManagementActions({
- +

This action cannot be undone.

diff --git a/src/components/DocumentOrganizationBadges.tsx b/src/components/DocumentOrganizationBadges.tsx index 61d7485ac..0574aeb4f 100644 --- a/src/components/DocumentOrganizationBadges.tsx +++ b/src/components/DocumentOrganizationBadges.tsx @@ -1,4 +1,4 @@ -import { AlertTriangle, Building2, FileText, Tag } from "lucide-react"; +import { TriangleAlert, Building2, FileText, Tag } from "lucide-react"; import { cn, metadataPill, toneInfo, toneNeutral, toneWarning } from "@/components/ui-primitives"; import { canonicalDocumentDisplayTitle } from "@/lib/document-organization"; @@ -74,7 +74,7 @@ export function DocumentOrganizationBadges({ ) : needsReview && candidateCount > 0 ? ( - + Ambiguous site ) : null} @@ -86,7 +86,7 @@ export function DocumentOrganizationBadges({ ) : null} {needsReview ? ( - + Needs review ) : null} diff --git a/src/components/DocumentViewer.tsx b/src/components/DocumentViewer.tsx index 28a40eafd..31758db3a 100644 --- a/src/components/DocumentViewer.tsx +++ b/src/components/DocumentViewer.tsx @@ -5,7 +5,7 @@ import Link from "next/link"; import { useRouter } from "next/navigation"; import { - AlertCircle, + CircleAlert, ArrowLeft, Check, ChevronLeft, @@ -461,7 +461,7 @@ function DocumentImage({ image }: { image: ImageRow }) { {failed ? (
- + Image preview failed.
- + Saved
@@ -219,7 +219,7 @@ export function AccountSetupDialog({ open, onClose }: { open: boolean; onClose: )} aria-hidden > - {selected ? : } + {selected ? : } ); @@ -239,7 +239,7 @@ export function AccountSetupDialog({ open, onClose }: { open: boolean; onClose: - + Verified @@ -261,7 +261,7 @@ export function AccountSetupDialog({ open, onClose }: { open: boolean; onClose: {item.detail} - + ); })} diff --git a/src/components/clinical-dashboard/answer-content.tsx b/src/components/clinical-dashboard/answer-content.tsx index 980aee715..3504320d9 100644 --- a/src/components/clinical-dashboard/answer-content.tsx +++ b/src/components/clinical-dashboard/answer-content.tsx @@ -5,8 +5,8 @@ import Link from "next/link"; import { memo, useEffect, useRef, useState } from "react"; import { - AlertCircle, - CheckCircle2, + CircleAlert, + CircleCheck, ChevronDown, Copy, ExternalLink, @@ -123,7 +123,7 @@ export const SourceImage = memo(function SourceImage({ )} >
- + Image preview could not load. ) : null} @@ -538,7 +538,7 @@ function CurrentPresentation({ record }: { record: DifferentialRecord }) {
  • {hingeCallout(item.text)}
  • ) : (
  • - + {item.text}
  • ), diff --git a/src/components/differentials/differential-presentation-workflow-page.tsx b/src/components/differentials/differential-presentation-workflow-page.tsx index f84fb3dea..369dbc7b9 100644 --- a/src/components/differentials/differential-presentation-workflow-page.tsx +++ b/src/components/differentials/differential-presentation-workflow-page.tsx @@ -3,7 +3,7 @@ import { Activity, ArrowLeft, BrainCircuit, - CheckCircle2, + CircleCheck, ChevronDown, ChevronRight, CircleHelp, @@ -36,7 +36,7 @@ type CandidateView = { }; const criterionIcon: Record = { - fit: CheckCircle2, + fit: CircleCheck, warning: ShieldAlert, question: CircleHelp, action: Activity, @@ -194,7 +194,7 @@ function DesktopComparisonTable({ type="button" className="inline-flex min-h-11 items-center gap-2 rounded-lg border border-[color:var(--border-lux)] bg-[color:var(--surface)] px-4 text-sm font-bold text-[color:var(--text-heading)] shadow-[var(--shadow-inset)]" > - + {workflow.selectedCount} of {workflow.totalCount} selected @@ -338,7 +338,7 @@ function SelectedDifferentialsPanel({ href={`/differentials/diagnoses/${candidate.record.slug}`} className="inline-flex min-w-0 items-center gap-2" > - + {candidate.record.title} @@ -381,7 +381,7 @@ function ReviewPanel({ workflow }: { workflow: DifferentialPresentationWorkflow
      {workflow.reviewChecklist.map((item) => (
    • - + {item}
    • ))} diff --git a/src/components/document-search-live-opener.tsx b/src/components/document-search-live-opener.tsx index 79bc1f918..0ec70ce46 100644 --- a/src/components/document-search-live-opener.tsx +++ b/src/components/document-search-live-opener.tsx @@ -3,7 +3,7 @@ import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; import { - AlertCircle, + CircleAlert, ArrowLeft, BadgeCheck, BookOpen, @@ -94,7 +94,7 @@ const mockSources: MockSourceDocument[] = [ evidence: [ { label: "Table evidence", value: "8 rows", icon: Table2, tone: "success" }, { label: "PDF page", value: "p.12", icon: FileText, tone: "info" }, - { label: "Review note", value: "2026", icon: AlertCircle, tone: "warning" }, + { label: "Review note", value: "2026", icon: CircleAlert, tone: "warning" }, ], passage: [ "Monitoring requirements are grouped by treatment stage and missed-dose interval.", @@ -124,7 +124,7 @@ const mockSources: MockSourceDocument[] = [ evidence: [ { label: "Image evidence", value: "flowchart", icon: FileImage, tone: "info" }, { label: "PDF page", value: "p.4", icon: FileText, tone: "success" }, - { label: "Risk pathway", value: "visible", icon: AlertCircle, tone: "warning" }, + { label: "Risk pathway", value: "visible", icon: CircleAlert, tone: "warning" }, ], passage: [ "The pathway separates immediate safety steps from medication and senior review prompts.", @@ -154,7 +154,7 @@ const mockSources: MockSourceDocument[] = [ evidence: [ { label: "Checklist", value: "forms", icon: BadgeCheck, tone: "success" }, { label: "PDF page", value: "p.2", icon: FileText, tone: "info" }, - { label: "Workflow", value: "legal", icon: AlertCircle, tone: "warning" }, + { label: "Workflow", value: "legal", icon: CircleAlert, tone: "warning" }, ], passage: [ "The quick reference groups forms by use case and required documentation step.", diff --git a/src/components/forms/form-detail-page.tsx b/src/components/forms/form-detail-page.tsx index 419ba464a..88ae1a87a 100644 --- a/src/components/forms/form-detail-page.tsx +++ b/src/components/forms/form-detail-page.tsx @@ -6,7 +6,7 @@ import { Bookmark, BookmarkCheck, CalendarDays, - CheckCircle2, + CircleCheck, ChevronRight, Clipboard, ClipboardList, @@ -24,7 +24,7 @@ import { Tag, UserRound, X, - XCircle, + CircleX, type LucideIcon, } from "lucide-react"; import { useMemo, useState, type ReactNode } from "react"; @@ -314,9 +314,9 @@ function PathwayContextCard({ )} > {criterion.tone === "reject" ? ( - + ) : ( - + )} {criterion.label} @@ -686,7 +686,7 @@ export function FormDetailPage({ form }: { form: FormRecord }) { ? FileText : label.includes("pathway") ? Navigation - : CheckCircle2; + : CircleCheck; return ; })} @@ -760,7 +760,7 @@ export function FormDetailPage({ form }: { form: FormRecord }) { key={note} className="flex gap-2 text-xs font-medium leading-5 text-[color:var(--text-muted)]" > - diff --git a/src/components/route-error-boundary.tsx b/src/components/route-error-boundary.tsx index 85c3db2c7..9df72cc39 100644 --- a/src/components/route-error-boundary.tsx +++ b/src/components/route-error-boundary.tsx @@ -1,7 +1,7 @@ "use client"; import { useEffect } from "react"; -import { AlertTriangle, RefreshCw } from "lucide-react"; +import { TriangleAlert, RefreshCw } from "lucide-react"; import { cn, primaryControl } from "@/components/ui-primitives"; @@ -51,7 +51,7 @@ export function RouteErrorBoundary({ >
      - +

      {title}

      diff --git a/src/components/services/service-detail-page.tsx b/src/components/services/service-detail-page.tsx index 1ef3fadbe..df077b30a 100644 --- a/src/components/services/service-detail-page.tsx +++ b/src/components/services/service-detail-page.tsx @@ -2,12 +2,12 @@ import { useRouter } from "next/navigation"; import { - AlertTriangle, + TriangleAlert, ArrowLeft, BadgeDollarSign, Bookmark, BookmarkCheck, - CheckCircle2, + CircleCheck, ChevronRight, Clipboard, Copy, @@ -22,7 +22,7 @@ import { Tag, Users, X, - XCircle, + CircleX, type LucideIcon, } from "lucide-react"; import { useState, type ReactNode } from "react"; @@ -109,9 +109,9 @@ function renderCriterionIcon(tone: ServiceCriterion["tone"]) { ? "text-[color:var(--danger)]" : "text-[color:var(--warning)]", ); - if (tone === "meet") return ; - if (tone === "reject") return ; - return ; + if (tone === "meet") return ; + if (tone === "reject") return ; + return ; } function criterionPill(tone: ServiceCriterion["tone"]) { @@ -597,7 +597,7 @@ export function ServiceDetailPage({ service }: { service: ServiceRecord }) {
      - +

      {verified ? "Verified for local use" : "Verify locally before use"} From bb89401f743ec6b5bbd60e0971679db9320ec393 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Sun, 12 Jul 2026 02:07:04 +0800 Subject: [PATCH 03/10] refactor(icons): retire the 4.5 (18px) half-step; enforce icon-scale --strict MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve every h-4.5/w-4.5 (18px) icon size — the one off-4px-grid drift in the icon scale. Lucide glyphs snap to size-icon-lg (20px, preserving their "bigger than default" intent); non-icon 18px boxes (account-setup indicator / grid) snap to h-5; the two responsive sites tokenize as size-icon-md sm:size-icon-lg. One arbitrary h-[18px] ShieldCheck glyph also moves to size-icon-lg. Simplify check-icon-scale.mjs to enforce the 4.5 half-step only (dropped the arbitrary-px rule, which false-flagged non-icon 18px count badges) and flip it to --strict in verify:cheap so the half-step can't return. Verified: strict guard green, Tailwind emits .size-icon-* from the --spacing-icon-* tokens; typecheck / lint / prettier / 1648 vitest green. Co-Authored-By: Claude Opus 4.8 --- docs/design-system.md | 7 +++--- package.json | 2 +- scripts/check-icon-scale.mjs | 23 +++++++------------ src/components/applications-launcher-page.tsx | 10 ++++---- .../clinical-dashboard/ClinicalSidebar.tsx | 2 +- .../account-setup-dialog.tsx | 6 ++--- .../clinical-dashboard/answer-content.tsx | 2 +- .../clinical-dashboard/dashboard-nav.tsx | 2 +- .../clinical-dashboard/dashboard-shell.tsx | 2 +- .../document-search-results.tsx | 2 +- .../favourites-command-library-page.tsx | 2 +- .../master-search-header.tsx | 4 ++-- .../clinical-dashboard/settings-dialog.tsx | 2 +- src/components/forms/form-detail-page.tsx | 2 +- src/components/ui-primitives.tsx | 2 +- 15 files changed, 32 insertions(+), 38 deletions(-) diff --git a/docs/design-system.md b/docs/design-system.md index ec1c64c4a..5f56ff118 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -78,9 +78,10 @@ Icon **glyphs** use the parallel `--spacing-icon-*` scale in `@theme`: `size-icon-xl` 24 (px). These generate `size-icon-*` / `h-icon-*` / `w-icon-*`, exactly like `--spacing-tap` → `size-tap`. -- Prefer `size-icon-md` over raw `h-4 w-4` for an icon glyph. `npm run check:icon-scale` counts - off-scale drift — the retired `4.5` (18px) half-step and arbitrary `≤24px` `h-[Npx]`. - **Ratchet:** report-only now; flip to `--strict` once the cluster migration lands. +- Prefer `size-icon-md` over raw `h-4 w-4` for an icon glyph. `npm run check:icon-scale --strict` + (in `verify:cheap`) blocks the retired `4.5` (18px) half-step — icon glyphs resolve to + `size-icon-lg`, non-icon 18px boxes to `h-5`. It does **not** touch raw `h-4 w-4` (which also + sizes non-icons), so migrating the long tail onto `size-icon-*` is opportunistic, not enforced. - **Responsive** icons add a breakpoint variant — `size-icon-md sm:size-icon-lg`. Reserve it for a few roles (nav, composer, hero, panel headings); most icons stay one fixed size. - **Not** for container tiles (`iconTile` h-9, empty-state tile h-10) or non-icon boxes (the diff --git a/package.json b/package.json index e6cabbfcd..8a552eb5d 100644 --- a/package.json +++ b/package.json @@ -70,7 +70,7 @@ "check:m13-migration": "node scripts/run-tsx.mjs scripts/check-m13-migration.ts", "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", + "check:icon-scale": "node scripts/check-icon-scale.mjs --strict", "recover:ingestion": "node scripts/run-tsx.mjs scripts/recover-ingestion-queue.ts", "registry:seed": "node scripts/run-tsx.mjs scripts/seed-registry-records.ts", "registry:embed": "node scripts/run-tsx.mjs scripts/embed-registry-records.ts", diff --git a/scripts/check-icon-scale.mjs b/scripts/check-icon-scale.mjs index 4d2aa0d9c..b9d9725e8 100644 --- a/scripts/check-icon-scale.mjs +++ b/scripts/check-icon-scale.mjs @@ -9,17 +9,18 @@ // --spacing-icon-lg 20px → size-icon-lg // --spacing-icon-xl 24px → size-icon-xl // -// It tracks two kinds of drift that are unambiguously icon sizing: -// 1. The retired `4.5` half-step (h-4.5 / w-4.5 / size-4.5, 18px) — always an -// icon size, replaced by size-icon-sm/-md/-lg per site. -// 2. Arbitrary square pixel sizes in the icon range (<= 24px), e.g. -// h-[18px] w-[18px] / size-[16px] — bypass the scale. +// It enforces one unambiguous drift signal: the retired `4.5` half-step +// (h-4.5 / w-4.5 / size-4.5, 18px). 18px is off the 4px grid; it was only ever +// reached by icon glyphs and a handful of tiny boxes, and it resolves cleanly to +// size-icon-lg (glyphs) or h-5 (non-icon boxes). // // It intentionally does NOT flag raw h-4 w-4 / h-5 w-5 etc.: those integer // spacing steps also size non-icons (the ToggleSwitch knob, status dots, // avatars, container tiles), so a blanket ban would false-positive. Migrating // icon glyphs onto size-icon-* is a codemod, not a lint rule — see -// docs/design-system.md §2. Only the unambiguous drift above is enforced here. +// docs/design-system.md §2. It also does NOT flag arbitrary h-[Npx]: those are +// used by deliberate non-icon elements too (e.g. count-badge bubbles at 18px), +// so flagging them would false-positive; genuine icon glyphs simply use the scale. // Mockups (*mockup*) are design-scratch and out of scope. // // Usage: @@ -34,8 +35,6 @@ const strict = process.argv.includes("--strict"); // Retired 18px half-step: (min-)h-4.5 / (min-)w-4.5 / size-4.5. const HALF_STEP = /\b(?:min-)?(?:h|w|size)-4\.5\b/g; -// Arbitrary square pixel sizes: (min-)h-[Npx] / (min-)w-[Npx] / size-[Npx]. -const ARBITRARY_PX = /\b(?:min-)?(?:h|w|size)-\[(\d+)px\]/g; // Tracked source files under src (fast; respects .gitignore). Exclude mockups. const files = execSync("git ls-files src", { encoding: "utf8" }) @@ -55,17 +54,11 @@ for (const file of files) { for (const m of line.matchAll(HALF_STEP)) { hits.push({ file, line: i + 1, match: m[0] }); } - for (const m of line.matchAll(ARBITRARY_PX)) { - // Only the icon glyph range (12–24px). Below 12px is dividers/dots/rules - // (e.g. w-[2px]); above 24px is avatars/tiles/shells — none are icon glyphs. - const px = Number(m[1]); - if (px >= 12 && px <= 24) hits.push({ file, line: i + 1, match: m[0] }); - } }); } if (hits.length === 0) { - console.log("✓ icon-scale: no off-scale icon sizes (4.5 half-step / arbitrary ≤24px) in src."); + console.log("✓ icon-scale: no retired 4.5 (18px) half-step icon sizes in src."); process.exit(0); } diff --git a/src/components/applications-launcher-page.tsx b/src/components/applications-launcher-page.tsx index 3af9807e4..f876ffe33 100644 --- a/src/components/applications-launcher-page.tsx +++ b/src/components/applications-launcher-page.tsx @@ -176,7 +176,7 @@ function ToolIcon({ app, size = "md" }: { app: LauncherApp; size?: "sm" | "md" | size === "lg" && "h-14 w-14", )} > - + ); } @@ -226,7 +226,7 @@ function ToolSearch({ )} > - +

      - + {title}
      @@ -575,7 +575,7 @@ function MobileDetailSections({ app }: { app: LauncherApp }) { focusRing, )} > - + {label} - + )} diff --git a/src/components/clinical-dashboard/account-setup-dialog.tsx b/src/components/clinical-dashboard/account-setup-dialog.tsx index 5e429561c..7c05bc465 100644 --- a/src/components/clinical-dashboard/account-setup-dialog.tsx +++ b/src/components/clinical-dashboard/account-setup-dialog.tsx @@ -212,7 +212,7 @@ export function AccountSetupDialog({ open, onClose }: { open: boolean; onClose: {source.label}
      From c0e3ae42895778a27466dce01c8be06fa62cf604 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:07:18 +0800 Subject: [PATCH 09/10] fix(lint): use file-level no-img-element disable in next/og routes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI lints with --max-warnings 0. The inline {/* eslint-disable-next-line @next/next/no-img-element */} placed inside a JSX expression container does not work as a directive — it was reported unused while the rule still flagged the , failing the zero-warnings gate. Switch to the file-level /* eslint-disable @next/next/no-img-element */ the codebase already uses for these next/og image routes (cf. answer-content.tsx, DocumentViewer.tsx). Verified with `npm run lint -- --max-warnings 0`. Co-Authored-By: Claude Opus 4.8 --- src/app/opengraph-image.tsx | 2 +- src/lib/brand-image.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx index 6263c9500..eefa4267d 100644 --- a/src/app/opengraph-image.tsx +++ b/src/app/opengraph-image.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @next/next/no-img-element -- Satori rasterises this server-side; it is not a DOM element and next/image cannot run inside next/og. */ import { ImageResponse } from "next/og"; import { brandMarkDataUri } from "@/lib/brand-image"; @@ -21,7 +22,6 @@ export default function OpengraphImage() { color: "#ffffff", }} > - {/* eslint-disable-next-line @next/next/no-img-element -- Satori element, not DOM; next/image N/A */}
      Clinical KB
      diff --git a/src/lib/brand-image.tsx b/src/lib/brand-image.tsx index 46855839e..3ea92fee7 100644 --- a/src/lib/brand-image.tsx +++ b/src/lib/brand-image.tsx @@ -1,3 +1,4 @@ +/* eslint-disable @next/next/no-img-element -- Satori rasterises these s server-side; they are not DOM elements and next/image cannot run inside next/og. */ // Server-only helpers for rendering the brand mark inside next/og ImageResponse // routes (apple-icon, PWA maskable/any icons, opengraph-image). All derive from // the single geometry source in ./brand-mark so raster app icons never drift @@ -42,7 +43,6 @@ export function BrandIconImage({ background, }} > - {/* eslint-disable-next-line @next/next/no-img-element -- Satori element, not DOM; next/image N/A */}
      ); From 92ae093e7b2d0350b881dab49934ccf282c72850 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:12:39 +0800 Subject: [PATCH 10/10] fix(lint): disable no-img-element via config for next/og image routes The file-level /* eslint-disable @next/next/no-img-element */ was reported "unused" on CI's fresh checkout but "used" locally: the Next plugin only fires this rule when a local .next build dir is present, so the directive flipped between used/unused and tripped --max-warnings 0. Move it to a deterministic eslint.config.mjs override (rule off for src/lib/brand-image.tsx + src/app/opengraph-image.tsx). A config rule setting can't be an "unused directive", so it holds in every environment. Verified with .next removed + `npm run lint -- --max-warnings 0`. Co-Authored-By: Claude Opus 4.8 --- eslint.config.mjs | 12 ++++++++++++ src/app/opengraph-image.tsx | 1 - src/lib/brand-image.tsx | 1 - 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/eslint.config.mjs b/eslint.config.mjs index 0c05f9ceb..abdd87663 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -36,6 +36,18 @@ const eslintConfig = defineConfig([ "local/require-lucide-icon-aria": "error", }, }, + // next/og image routes render through Satori (rasterised server-side, + // not DOM); next/image cannot run there. Turn the rule off for these files via + // config rather than a per-file disable directive — the Next plugin reports + // this rule inconsistently across environments (it fires with a local .next + // present but not on a fresh CI checkout), so a disable directive flips + // between "used" and "unused" and trips `--max-warnings 0`. + { + files: ["src/lib/brand-image.tsx", "src/app/opengraph-image.tsx"], + rules: { + "@next/next/no-img-element": "off", + }, + }, // Override default ignores of eslint-config-next. globalIgnores([ // Default ignores of eslint-config-next: diff --git a/src/app/opengraph-image.tsx b/src/app/opengraph-image.tsx index eefa4267d..5bc48bd26 100644 --- a/src/app/opengraph-image.tsx +++ b/src/app/opengraph-image.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @next/next/no-img-element -- Satori rasterises this server-side; it is not a DOM element and next/image cannot run inside next/og. */ import { ImageResponse } from "next/og"; import { brandMarkDataUri } from "@/lib/brand-image"; diff --git a/src/lib/brand-image.tsx b/src/lib/brand-image.tsx index 3ea92fee7..803ffb35c 100644 --- a/src/lib/brand-image.tsx +++ b/src/lib/brand-image.tsx @@ -1,4 +1,3 @@ -/* eslint-disable @next/next/no-img-element -- Satori rasterises these s server-side; they are not DOM elements and next/image cannot run inside next/og. */ // Server-only helpers for rendering the brand mark inside next/og ImageResponse // routes (apple-icon, PWA maskable/any icons, opengraph-image). All derive from // the single geometry source in ./brand-mark so raster app icons never drift