From 6d693e7cce4a76d66582595bc14433e044b53577 Mon Sep 17 00:00:00 2001 From: BigSimmo <87357024+BigSimmo@users.noreply.github.com> Date: Thu, 27 Aug 2026 18:16:14 +0800 Subject: [PATCH 1/2] feat(design-system): compact-meta density policy Metadata and disclosure controls were borrowing tap-floor or ad-hoc min-heights. Add --spacing-compact-meta (40px) and adopt it on those surfaces so primary CTAs stay at 48px. Co-authored-by: Cursor --- docs/design-system/COMPONENTS.md | 10 ++- docs/design-system/TOKENS.md | 86 +++++++++++-------- src/app/globals.css | 3 + src/components/DocumentTagCloud.tsx | 11 ++- src/components/calculators/bedside-sheet.tsx | 8 +- src/components/calculators/directory-grid.tsx | 3 +- .../clinical-dashboard/answer-content.tsx | 1 + .../clinical-output-helpers.tsx | 2 +- .../clinical-dashboard/evidence-panels.tsx | 6 +- .../favourites-command-library-page.tsx | 4 +- .../clinical-dashboard/favourites-hub.tsx | 10 +-- .../result-filter-control.tsx | 30 +++---- .../search-results-header-band.tsx | 35 ++++---- .../document-viewer/source-panels.tsx | 19 ++-- src/components/dsm/dsm-search-page.tsx | 2 +- src/components/mode-home-template.tsx | 5 +- src/components/show-all-chip.tsx | 8 +- .../specifiers/specifiers-home-page.tsx | 2 +- src/components/ui-primitives.tsx | 6 +- 19 files changed, 144 insertions(+), 107 deletions(-) diff --git a/docs/design-system/COMPONENTS.md b/docs/design-system/COMPONENTS.md index 1f29f11b69..f20c35a093 100644 --- a/docs/design-system/COMPONENTS.md +++ b/docs/design-system/COMPONENTS.md @@ -656,6 +656,7 @@ tab order, never removes its accessible name); disabled via `controlBase` encodi `--focus` outline only. **Rules.** Verb-first specific labels, never "OK"/bare "Confirm" · hover/active from semantic tokens, never `brightness-*` filters · one filled command per surface. **Landed.** Danger contrast and hover/active tokens plus the 48px tap-floor comment. +**Tap vs compact-meta (DS-P1-09 / DS-P2-24).** Primary `Button` / `primaryControl` / filled command actions stay `min-h-tap` at **all** breakpoints — no `sm:min-h-10`, `sm:min-h-9`, `lg:min-h-9`, or `min-h-11`. Metadata, disclosure, filter chips, and table micro-actions may use `min-h-compact-meta` (40px) or a documented prefixed compact (`sm:min-h-compact-meta`). `--row-compact` / `min-h-9` (36px) is row height, not a tap target. Recipes: `interactiveCompact` and `tableMicroActionRow` are the named compact-meta exceptions in `ui-primitives.tsx`; `controlBase` stays tap-sized. Full table: TOKENS §2 “Compact-meta vs tap”. **Open defects → PR.** ref forwarding and the needless client boundary → follow-on. ### 9.2 `IconButton` @@ -686,7 +687,9 @@ component; broader Gate 9 motion sweep still tracks other surfaces. ### 9.5 `Chip` **Purpose.** Compact label for tones, filters, categories. Static chips are text, not -targets (tap-exempt under the inline exception). **Modes (union, PR 4).** `static` (no +targets (tap-exempt under the inline exception). Interactive filter chips that are **not** +primary CTAs may use `min-h-compact-meta` (or `sm:min-h-compact-meta`) per TOKENS density +policy; do not copy that step-down onto a filled command action. **Modes (union, PR 4).** `static` (no removal props representable) · `removable` (`onRemove` + `removeLabel` both required). **Rules.** Remove control keeps a small visible glyph inside an overlapping hit target that does not inflate the chip · truncated labels need a full-value path · category tones @@ -954,7 +957,10 @@ sanitiser's tiny grammar renders as plain text. `disabled:opacity` uses migrate in PR 3 · the module splits in PR 12 (`styles/recipes.ts`, actions, feedback, forms, clinical-source, source-metadata contract) so generic primitives stop importing clinical application modules · recipes -never restate a token value. The 2026-07-31 icon regression (lucide imports replaced +never restate a token value. `interactiveCompact` and `tableMicroActionRow` use +`min-h-tap` on phones and `sm:min-h-compact-meta` on pointer layouts (metadata / +disclosure, not primaries). Do not copy that step-down onto `controlBase` or +`primaryControl`. The 2026-07-31 icon regression (lucide imports replaced with glyph spans by an unverified merge, repaired in `0b0f393c7`) is the cautionary case: this file is load-bearing for the icon vocabulary; changes to it require the focused DOM tests to run. diff --git a/docs/design-system/TOKENS.md b/docs/design-system/TOKENS.md index c6e63579c8..5bd09f295d 100644 --- a/docs/design-system/TOKENS.md +++ b/docs/design-system/TOKENS.md @@ -41,27 +41,45 @@ This is the two-sources-of-truth failure C1 exists to kill. One winner per role: Declared in the `.ckb-v2` structural block **[verified: full read at `ef13a072a`]**. -| Group | Roles | Notes | -| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| Space scale | `--space-0` … `--space-11` | 4px base. Markup never references the raw scale — semantic tokens only. | -| Semantic gaps | `--gap-tight` · `--gap-inline` · `--gap-stack` · `--gap-block` · `--gap-section` | The only inter-element spacing markup may use. | -| Semantic padding | `--pad-chip-x` · `--pad-control-x` · `--pad-cta-x` · `--pad-card` · `--pad-panel` · `--pad-strip` | Heading-inset convention rides on these (SPEC §4.6). | -| Page | `--page-gutter` · `--page-max` · `--measure` · `--header-h` | `--measure` is the prose measure — never wider. | -| Type steps | `--text-{xs,sm,body,md,lg,xl}` · `--text-hero` · `--text-hero--line-height` · `--text-hero-tr` | Seven steps. Shared `--leading-prose`; hero keeps `--text-hero--line-height` / `--text-hero-tr` (gated). Unused xs–xl `-lh`/`-tr` companions are orphans — do not re-require them. ⚠️ `--text-hero--line-height` is the one double-dash name — keep as-is; renaming is churn without a defect. | -| Chip sizes | `Chip` `compact` → `--text-2xs` (legacy compat step); `standard` (default) → `--text-xs` | Canonical (#220): dense surfaces opt into `size="compact"`; default adoption keeps `--text-xs`. `--text-2xs` stays a compatibility alias for Chip compact / legacy call sites — not a new v2 type step to invent against. Do not densify every Chip to chase the globals “small chips” note. | -| Type companions | `--leading-prose` · `--tracking-eyebrow` · `--nums` | `--nums` = numeric variant for all data. | -| Weights | `--font-weight-{body,label,heading,value}` | One job each; display uses heading weight (SPEC §4.6). | -| Radius | `--radius-{sm,md,lg,xl,2xl}` | One step per surface role. v2 and live `--radius-md` agreed in PR 5c and are now pinned to each other by the live token contract. | -| Icons | `--icon-{xs,sm,md,lg}` | Paired to adjacent type steps. | -| Density | `--tap-min` (alias of `--spacing-tap`, §1) · `--chip-height` · `--row-comfortable` · `--row-compact` · `--cell-pad-{comfortable,compact}` | Tap target is not row height (gated on resolved values, not strings, now that `--tap-min` is an alias). | -| Accent rules | `--rule-w` · `--rule-accent` · `--rule-warning` | ⚠️ Reference `var(--clinical-accent)` / `var(--warning)` **without declaring them** — v2 depends on the live layer for both (§5). | -| Evidence gutter | `--gutter-col` · `--gutter-dot` · `--gutter-line-w` | One gutter column owns line and dot (SPEC §7). | -| Evidence spine | `--spine-w` · `--spine-current` · `--spine-stale` | Authored `59e4c3dfc` (§1); derived from rule/accent/warning roles. | -| Status mark | `--status-mark-size` · `--status-mark-stroke` | Authored `59e4c3dfc` (§1); codifies `StatusMark`'s inline geometry. | -| Quantity | `--quantity-unit-tracking` · `--quantity-unit-gap` | Winners per §1. | -| Dashed edge | `--border-dashed` | Drop targets / "nothing here yet" — distinct from `--border-strong`, which means emphasis. | -| Stacking | `--z-{base,raised,chrome,overlay,popover,modal,toast}` | Each rung names its `--eN` partner; toast above modal deliberately. | -| Motion | `--duration-{fast,base,slow}` · `--ease-standard` · `--ease-physical` | Durations zero under `prefers-reduced-motion` (gated). | +| Group | Roles | Notes | +| ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Space scale | `--space-0` … `--space-11` | 4px base. Markup never references the raw scale — semantic tokens only. | +| Semantic gaps | `--gap-tight` · `--gap-inline` · `--gap-stack` · `--gap-block` · `--gap-section` | The only inter-element spacing markup may use. | +| Semantic padding | `--pad-chip-x` · `--pad-control-x` · `--pad-cta-x` · `--pad-card` · `--pad-panel` · `--pad-strip` | Heading-inset convention rides on these (SPEC §4.6). | +| Page | `--page-gutter` · `--page-max` · `--measure` · `--header-h` | `--measure` is the prose measure — never wider. | +| Type steps | `--text-{xs,sm,body,md,lg,xl}` · `--text-hero` · `--text-hero--line-height` · `--text-hero-tr` | Seven steps. Shared `--leading-prose`; hero keeps `--text-hero--line-height` / `--text-hero-tr` (gated). Unused xs–xl `-lh`/`-tr` companions are orphans — do not re-require them. ⚠️ `--text-hero--line-height` is the one double-dash name — keep as-is; renaming is churn without a defect. | +| Chip sizes | `Chip` `compact` → `--text-2xs` (legacy compat step); `standard` (default) → `--text-xs` | Canonical (#220): dense surfaces opt into `size="compact"`; default adoption keeps `--text-xs`. `--text-2xs` stays a compatibility alias for Chip compact / legacy call sites — not a new v2 type step to invent against. Do not densify every Chip to chase the globals “small chips” note. | +| Type companions | `--leading-prose` · `--tracking-eyebrow` · `--nums` | `--nums` = numeric variant for all data. | +| Weights | `--font-weight-{body,label,heading,value}` | One job each; display uses heading weight (SPEC §4.6). | +| Radius | `--radius-{sm,md,lg,xl,2xl}` | One step per surface role. v2 and live `--radius-md` agreed in PR 5c and are now pinned to each other by the live token contract. | +| Icons | `--icon-{xs,sm,md,lg}` | Paired to adjacent type steps. | +| Density | `--tap-min` (alias of `--spacing-tap`, §1) · `--spacing-compact-meta` · `--chip-height` · `--row-comfortable` · `--row-compact` · `--cell-pad-{comfortable,compact}` | Tap target is not row height (gated on resolved values, not strings, now that `--tap-min` is an alias). Compact-meta is 40px metadata/disclosure only; `--row-compact` is 36px row height, never a tap substitute. | +| Accent rules | `--rule-w` · `--rule-accent` · `--rule-warning` | ⚠️ Reference `var(--clinical-accent)` / `var(--warning)` **without declaring them** — v2 depends on the live layer for both (§5). | +| Evidence gutter | `--gutter-col` · `--gutter-dot` · `--gutter-line-w` | One gutter column owns line and dot (SPEC §7). | +| Evidence spine | `--spine-w` · `--spine-current` · `--spine-stale` | Authored `59e4c3dfc` (§1); derived from rule/accent/warning roles. | +| Status mark | `--status-mark-size` · `--status-mark-stroke` | Authored `59e4c3dfc` (§1); codifies `StatusMark`'s inline geometry. | +| Quantity | `--quantity-unit-tracking` · `--quantity-unit-gap` | Winners per §1. | +| Dashed edge | `--border-dashed` | Drop targets / "nothing here yet" — distinct from `--border-strong`, which means emphasis. | +| Stacking | `--z-{base,raised,chrome,overlay,popover,modal,toast}` | Each rung names its `--eN` partner; toast above modal deliberately. | +| Motion | `--duration-{fast,base,slow}` · `--ease-standard` · `--ease-physical` | Durations zero under `prefers-reduced-motion` (gated). | + +### Compact-meta vs tap (DS-P1-09 / DS-P2-24) + +`--spacing-compact-meta` (`2.5rem` / 40px) lives next to `--spacing-tap` in `@theme` and generates `min-h-compact-meta` / `min-w-compact-meta`. It is **not** a second tap floor. + +| Role | Token / utility | Height | Use on | +| ------------ | ----------------------------------------------- | ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Tap | `--spacing-tap` → `min-h-tap` | 48px | **Every primary CTA, at every breakpoint.** Apply/Done, Compare, Show more, empty-state command, command/save, View PDF, and any other filled command action. No `sm:`/`lg:` step-down. | +| Compact-meta | `--spacing-compact-meta` → `min-h-compact-meta` | 40px | Metadata, disclosure, filter chips, table micro-actions, source `` rows, search-results header-band chrome, catalogue “Show all” chips, answer view-mode segments. Comment each call site. Prefixed `sm:min-h-compact-meta` / `lg:min-h-compact-meta` is the named desktop step-down. | +| Row compact | `--row-compact` / `min-h-9` | 36px | Table/list **row height** only. Never a tap target and never a compact-meta stand-in. | + +Hard bans: + +- **Never `min-h-11`.** 44px reintroduces the `ui-smoke` sub-pixel rounding flake. The phone composer 44px-below-431px exception is search chrome, not this token. +- **Never reuse `--row-compact` (36px) as tap.** `sm:min-h-9` / `lg:min-h-9` on an interactive control is the defect this rule exists to close: either keep `min-h-tap` (primary) or migrate to `min-h-compact-meta` (documented metadata/disclosure). +- **Never step a primary down at `sm`/`lg`.** Phone already has `min-h-tap`; a prefixed compact class is an override, not “desktop polish”. + +Named compact-meta recipes in `ui-primitives.tsx`: `interactiveCompact`, `tableMicroActionRow`. `controlBase` / `primaryControl` stay tap-sized. ## 3 · v2 shell inventory — light and dark blocks @@ -121,19 +139,19 @@ The v2 layer _references_ or _depends on_ these; their values stay in `live` / ` ## 7 · Usage rules — allowed and forbidden, per group -| Group | Allowed | Forbidden | -| ----------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------- | -| Ink (`--text*`, `--decoration-soft`, `--disabled`) | Per the SPEC §4.3 role table | `--decoration-soft`/`--text-soft` on any text node; darkening the decoration tier to "pass" | -| Clinical state (`--danger*`, `--warning`, `--success`) | Source state and sanctioned urgency only | Decoration, numerals, charts, identity, category colour | -| Category (`--tone-*`) | Within-surface category chips/pills | Mode identity; new hues; delete-and-alias | -| Identity (`--kind-*`) | Surface-kind identity per SPEC §3 | Varying by clinical state | -| Elevation (`--e0…--e4`, `--ring-hairline`, `--shadow-inset`, `--shadow-well`) | One edge owner; ladder per SPEC §4.7 | 1px spread terms; child heavier than parent; v2 redeclaring the bevel | -| Stacking (`--z-*`) | Via `OverlayRoot`/named rungs only | Any raw `z-` value; new rungs without an `--eN` partner | -| Motion (`--duration-*`, `--ease-*`) | All transitions/animations | Hardcoded durations; animating layout properties | -| Density (`--spacing-tap`, `--tap-min`, rows, cells) | Utilities from `@theme`; `--tap-min` as alias | Setting the pair independently; reducing any 48px target | -| Space/type/radius | Semantic tokens in markup | Raw scale values or literals in components; `--measure` on non-prose | -| Quantity/spine/status-mark | Their named components only | Reuse as generic decoration | -| Ward-scoped (`--ward-*`, `--net-*`, `--co-*`) | Inside `src/components/ward-management/**` | Any use outside that directory; adding a name without a §9 row | +| Group | Allowed | Forbidden | +| ----------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| Ink (`--text*`, `--decoration-soft`, `--disabled`) | Per the SPEC §4.3 role table | `--decoration-soft`/`--text-soft` on any text node; darkening the decoration tier to "pass" | +| Clinical state (`--danger*`, `--warning`, `--success`) | Source state and sanctioned urgency only | Decoration, numerals, charts, identity, category colour | +| Category (`--tone-*`) | Within-surface category chips/pills | Mode identity; new hues; delete-and-alias | +| Identity (`--kind-*`) | Surface-kind identity per SPEC §3 | Varying by clinical state | +| Elevation (`--e0…--e4`, `--ring-hairline`, `--shadow-inset`, `--shadow-well`) | One edge owner; ladder per SPEC §4.7 | 1px spread terms; child heavier than parent; v2 redeclaring the bevel | +| Stacking (`--z-*`) | Via `OverlayRoot`/named rungs only | Any raw `z-` value; new rungs without an `--eN` partner | +| Motion (`--duration-*`, `--ease-*`) | All transitions/animations | Hardcoded durations; animating layout properties | +| Density (`--spacing-tap`, `--tap-min`, `--spacing-compact-meta`, rows, cells) | Utilities from `@theme`; `--tap-min` as alias; compact-meta only on metadata/disclosure | Setting the pair independently; reducing any 48px **primary**; `--row-compact`/`min-h-9` as tap; `min-h-11` | +| Space/type/radius | Semantic tokens in markup | Raw scale values or literals in components; `--measure` on non-prose | +| Quantity/spine/status-mark | Their named components only | Reuse as generic decoration | +| Ward-scoped (`--ward-*`, `--net-*`, `--co-*`) | Inside `src/components/ward-management/**` | Any use outside that directory; adding a name without a §9 row | ## 8 · Naming rules going forward diff --git a/src/app/globals.css b/src/app/globals.css index 8a05fe9424..63154227eb 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -117,6 +117,9 @@ the edge-to-edge dock keeps its height (see the max-width: 430px block, which covers the composer input as well as the icon/send buttons). */ --spacing-tap: 3rem; + /* Metadata/disclosure only, never primary CTAs, not a tap-floor substitute. + --row-compact is 36px row height and must not be reused as tap. */ + --spacing-compact-meta: 2.5rem; --spacing-phone-frame: 52.75rem; /* Ward Flow's fixed phone navigation bar and the matching content reserve. */ --spacing-ward-phone-bar: 3.5rem; diff --git a/src/components/DocumentTagCloud.tsx b/src/components/DocumentTagCloud.tsx index 5556c2089d..dac823fed4 100644 --- a/src/components/DocumentTagCloud.tsx +++ b/src/components/DocumentTagCloud.tsx @@ -74,8 +74,11 @@ function DocumentTagChip({ onTagClick?: (tag: SmartDocumentTag) => void; }) { const Icon = groupIcon[tag.group]; + // Compact-meta visual density (filter chips, not primary CTAs). ::before + // expands the hit toward the 48px tap floor without painting a 48px chip. + // Ring/border stay for Agent 4. const tagClassName = cn( - "relative inline-flex max-w-full items-center gap-1 rounded-md border font-semibold shadow-[var(--shadow-inset)] before:absolute before:-inset-y-2 before:-inset-x-1 before:content-['']", + "relative inline-flex max-w-full items-center gap-1 rounded-md border font-semibold shadow-[var(--shadow-inset)] before:absolute before:-inset-y-3 before:-inset-x-1 before:content-['']", compact ? "min-h-6 px-2 text-2xs" : "min-h-7 px-2 text-2xs", groupToneClass(tag.group), tag.queryMatched && "ring-2 ring-[color:var(--focus)]/25", @@ -160,7 +163,8 @@ export function DocumentTagCloud({ type="button" onClick={() => setExpanded(true)} className={cn( - "w-fit rounded-md border border-[color:var(--border-lux)] bg-[color:var(--surface-raised)] font-semibold text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)]", + // Compact-meta show-more, not a primary CTA. relative+::before expands hit. + "relative w-fit rounded-md border border-[color:var(--border-lux)] bg-[color:var(--surface-raised)] font-semibold text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)] before:absolute before:-inset-y-3 before:-inset-x-1 before:content-['']", compact ? "min-h-6 px-2 text-2xs" : "min-h-7 px-2 text-2xs", )} aria-label={`Show ${hiddenCount} more document tags`} @@ -188,7 +192,8 @@ export function DocumentTagCloud({ type="button" onClick={() => setExpanded(true)} className={cn( - "inline-flex items-center rounded-md border border-[color:var(--border-lux)] bg-[color:var(--surface-raised)] font-semibold text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)]", + // Compact-meta show-more, not a primary CTA. relative+::before expands hit. + "relative inline-flex items-center rounded-md border border-[color:var(--border-lux)] bg-[color:var(--surface-raised)] font-semibold text-[color:var(--text-muted)] shadow-[var(--shadow-inset)] transition hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)] before:absolute before:-inset-y-3 before:-inset-x-1 before:content-['']", compact ? "min-h-6 px-2 text-2xs" : "min-h-7 px-2 text-2xs", )} aria-label={`Show ${hiddenCount} more document tags`} diff --git a/src/components/calculators/bedside-sheet.tsx b/src/components/calculators/bedside-sheet.tsx index 9b388b964f..f765323f7f 100644 --- a/src/components/calculators/bedside-sheet.tsx +++ b/src/components/calculators/bedside-sheet.tsx @@ -177,7 +177,8 @@ export function CalculatorsBedsideSheetMockup() { key={calc.id} href={`#sheet-${calc.id}`} className={cn( - "inline-flex min-h-9 shrink-0 items-center gap-1.5 rounded-md border px-2.5 text-2xs font-bold", + // Compact-meta jump chips (in-sheet nav), not primary CTAs. + "inline-flex min-h-compact-meta shrink-0 items-center gap-1.5 rounded-md border px-2.5 text-2xs font-bold", derived.started ? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]" : "border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)]", @@ -219,7 +220,7 @@ export function CalculatorsBedsideSheetMockup() { onClick={copySession} disabled={!startedCalcs.length} className={cn( - "inline-flex min-h-9 items-center gap-1.5 rounded-md border border-[color:var(--border)] bg-[color:var(--surface)] px-2.5 text-2xs font-bold text-[color:var(--text-muted)] transition hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)] disabled:pointer-events-none disabled:opacity-40", + "inline-flex min-h-tap items-center gap-1.5 rounded-md border border-[color:var(--border)] bg-[color:var(--surface)] px-2.5 text-2xs font-bold text-[color:var(--text-muted)] transition hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)] disabled:pointer-events-none disabled:opacity-40", focusRing, )} > @@ -238,7 +239,8 @@ export function CalculatorsBedsideSheetMockup() { key={calc.id} href={`#sheet-${calc.id}`} className={cn( - "inline-flex min-h-9 shrink-0 items-center gap-2 rounded-md border px-2.5 text-2xs font-bold text-[color:var(--text-heading)]", + // Compact-meta jump chips (session dock), not primary CTAs. + "inline-flex min-h-compact-meta shrink-0 items-center gap-2 rounded-md border px-2.5 text-2xs font-bold text-[color:var(--text-heading)]", derived.flags.length > 0 ? "border-[color:var(--danger-border)] bg-[color:var(--danger-soft)]" : "border-[color:var(--border)] bg-[color:var(--surface)]", diff --git a/src/components/calculators/directory-grid.tsx b/src/components/calculators/directory-grid.tsx index 4167c61adc..776ba845f9 100644 --- a/src/components/calculators/directory-grid.tsx +++ b/src/components/calculators/directory-grid.tsx @@ -237,7 +237,8 @@ export function CalculatorsDirectoryGridMockup() { aria-pressed={active} onClick={() => setDomain(chip.id)} className={cn( - "inline-flex min-h-10 shrink-0 items-center rounded-md border px-3 text-sm-minus font-bold", + // Compact-meta domain filters, not primary CTAs. + "inline-flex min-h-compact-meta shrink-0 items-center rounded-md border px-3 text-sm-minus font-bold", active ? "border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]" : "border-[color:var(--border)] bg-[color:var(--surface)] text-[color:var(--text-muted)] hover:text-[color:var(--text)]", diff --git a/src/components/clinical-dashboard/answer-content.tsx b/src/components/clinical-dashboard/answer-content.tsx index 33634fbaa7..6ffcee6554 100644 --- a/src/components/clinical-dashboard/answer-content.tsx +++ b/src/components/clinical-dashboard/answer-content.tsx @@ -513,6 +513,7 @@ export function NaturalLanguageAnswer({ diff --git a/src/components/clinical-dashboard/result-filter-control.tsx b/src/components/clinical-dashboard/result-filter-control.tsx index 31c61ed1a4..63bc63ec7f 100644 --- a/src/components/clinical-dashboard/result-filter-control.tsx +++ b/src/components/clinical-dashboard/result-filter-control.tsx @@ -239,7 +239,7 @@ export function ResultFilterTrigger({ // wrong here: a filled pill reads flush to its own edge while a stroked // funnel reads inset from its box, so equal values put the badge visibly // closer to the border than the glyph is. - "search-band-ghost inline-flex min-h-tap min-w-tap shrink-0 items-center justify-center gap-1.5 rounded-lg border pl-2.5 pr-[0.6875rem] transition-colors motion-reduce:transition-none sm:min-h-10 sm:min-w-10", + "search-band-ghost inline-flex min-h-tap min-w-tap shrink-0 items-center justify-center gap-1.5 rounded-lg border pl-2.5 pr-[0.6875rem] transition-colors motion-reduce:transition-none sm:min-h-compact-meta sm:min-w-compact-meta", "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]", // Mutually exclusive branches rather than a base plus an override. `cn` // merges now, so a later utility would win deterministically — but @@ -356,8 +356,8 @@ function FilterRadioGroup({ group, panelId }: { group: ResultFilterLensGroup; pa return (
-

- {/* The id is on the label text alone, not the heading — same fix as +

+ {/* DS-P2-24: compact-meta heading row, not a tap target. The id is on the label text alone, not the heading — same fix as ResultFilterFacetChips' own badge: with the id on the h3 itself, a sibling `note` would concatenate into the group's accessible name ("Source type" -> "Source type one only"). */} @@ -414,9 +414,9 @@ function FilterRadioGroup({ group, panelId }: { group: ResultFilterLensGroup; pa group.onChange(option.value); }} className={cn( - // Phone targets use the 48px tap floor; pointer layouts stay - // compact without dropping below the 40px filter-control floor. - "inline-flex min-h-tap max-w-full items-center gap-1.5 rounded-md border px-2.5 text-2xs font-semibold shadow-[var(--shadow-inset)] transition motion-reduce:transition-none sm:min-h-10 sm:gap-1 sm:px-2", + // DS-P2-24: filter chips are compact-meta (40px) from `sm`, not a + // primary CTA. Phone stays min-h-tap. Never min-h-11 / min-h-9. + "inline-flex min-h-tap max-w-full items-center gap-1.5 rounded-md border px-2.5 text-2xs font-semibold shadow-[var(--shadow-inset)] transition motion-reduce:transition-none sm:min-h-compact-meta sm:gap-1 sm:px-2", "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]", selected ? "border-[color:var(--clinical-accent)]/35 bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]" @@ -564,8 +564,8 @@ export function ResultFilterFacetChips({ return (
-

- {/* The id is on the label text alone, not the heading. With the badge +

+ {/* DS-P2-24: compact-meta heading row. The id is on the label text alone, not the heading. With the badge inside the labelled element the group's accessible name became "Domain 1" — the selection count leaking into the dimension's name, and changing it on every toggle. Caught by the DOM test. */} @@ -576,7 +576,7 @@ export function ResultFilterFacetChips({ aria-controls={disclosure.contentId} onClick={disclosure.onToggle} className={cn( - "flex min-h-tap w-full items-center gap-1.5 text-left text-2xs font-semibold uppercase tracking-eyebrow text-[color:var(--text-muted)] sm:min-h-10", + "flex min-h-tap w-full items-center gap-1.5 text-left text-2xs font-semibold uppercase tracking-eyebrow text-[color:var(--text-muted)] sm:min-h-compact-meta", "focus-visible:outline focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-[color:var(--focus)]", )} > @@ -717,7 +717,7 @@ export function ResultFilterScopeSelector({ /> @@ -985,7 +985,7 @@ export function ResultFilterSheet({ onClick={onApply ?? onClose} data-testid={`${testId}-done`} className={cn( - "inline-flex min-h-tap shrink-0 items-center justify-center rounded-lg border border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent)] px-4 text-xs font-semibold text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-inset)] transition hover:bg-[color:var(--clinical-accent-hover)] sm:min-h-10", + "inline-flex min-h-tap shrink-0 items-center justify-center rounded-lg border border-[color:var(--clinical-accent)] bg-[color:var(--clinical-accent)] px-4 text-xs font-semibold text-[color:var(--clinical-accent-contrast)] shadow-[var(--shadow-inset)] transition hover:bg-[color:var(--clinical-accent-hover)]", "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]", )} > @@ -1002,7 +1002,7 @@ export function ResultFilterSheet({ type="button" onClick={secondaryAction.onClick} className={cn( - "flex min-h-tap w-full items-center justify-between gap-3 rounded-lg border-t border-[color:var(--border)] px-1 pt-2.5 text-left text-xs font-semibold text-[color:var(--clinical-accent)] hover:text-[color:var(--clinical-accent-hover)] sm:min-h-10", + "flex min-h-tap w-full items-center justify-between gap-3 rounded-lg border-t border-[color:var(--border)] px-1 pt-2.5 text-left text-xs font-semibold text-[color:var(--clinical-accent)] hover:text-[color:var(--clinical-accent-hover)] sm:min-h-compact-meta", "focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)]", )} > @@ -1071,14 +1071,14 @@ export function ResultFilterSheet({ onChange={(event) => setNeedle(event.target.value)} placeholder="Find a filter…" data-testid={`${testId}-find`} - className="search-shell-input min-h-tap min-w-0 flex-1 bg-transparent text-xs font-semibold text-[color:var(--text)] outline-none placeholder:font-medium placeholder:text-[color:var(--text-placeholder)] sm:min-h-10" + className="search-shell-input min-h-tap min-w-0 flex-1 bg-transparent text-xs font-semibold text-[color:var(--text)] outline-none placeholder:font-medium placeholder:text-[color:var(--text-placeholder)] sm:min-h-compact-meta" /> {needle ? ( diff --git a/src/components/clinical-dashboard/search-results-header-band.tsx b/src/components/clinical-dashboard/search-results-header-band.tsx index 653233041e..be6778cc69 100644 --- a/src/components/clinical-dashboard/search-results-header-band.tsx +++ b/src/components/clinical-dashboard/search-results-header-band.tsx @@ -527,7 +527,7 @@ export function SearchResultsHeaderBand({ {utilityControls} {onViewChange ? (
@@ -536,7 +536,7 @@ export function SearchResultsHeaderBand({ aria-pressed={view === "table"} onClick={() => onViewChange("table")} className={cn( - "grid min-h-tap min-w-tap place-items-center sm:min-h-10 sm:min-w-10", + "grid min-h-tap min-w-tap place-items-center sm:min-h-compact-meta sm:min-w-compact-meta", focusRing, view === "table" ? "bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]" @@ -551,7 +551,7 @@ export function SearchResultsHeaderBand({ aria-pressed={view === "list"} onClick={() => onViewChange("list")} className={cn( - "grid min-h-tap min-w-tap place-items-center border-l border-[color:var(--border)] sm:min-h-10 sm:min-w-10", + "grid min-h-tap min-w-tap place-items-center border-l border-[color:var(--border)] sm:min-h-compact-meta sm:min-w-compact-meta", focusRing, view === "list" ? "bg-[color:var(--clinical-accent-soft)] text-[color:var(--clinical-accent)]" @@ -568,7 +568,7 @@ export function SearchResultsHeaderBand({ type="button" onClick={onSaveSearch} className={cn( - "inline-flex min-h-tap shrink-0 items-center gap-1.5 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-2.5 text-[color:var(--text-muted)] search-band-ghost hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)] sm:min-h-10", + "inline-flex min-h-tap shrink-0 items-center gap-1.5 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-2.5 text-[color:var(--text-muted)] search-band-ghost hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)] sm:min-h-compact-meta", focusRing, )} > @@ -586,7 +586,7 @@ export function SearchResultsHeaderBand({ busyLabel="Retrying…" onClick={retry} className={cn( - "inline-flex min-h-tap shrink-0 items-center justify-center rounded-lg border border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] px-3 text-[color:var(--warning)] search-band-ghost hover:border-[color:var(--warning)] sm:min-h-10", + "inline-flex min-h-tap shrink-0 items-center justify-center rounded-lg border border-[color:var(--warning-border)] bg-[color:var(--warning-soft)] px-3 text-[color:var(--warning)] search-band-ghost hover:border-[color:var(--warning)] sm:min-h-compact-meta", focusRing, )} > @@ -686,7 +686,7 @@ export function SearchResultsHeaderBand({ // Hover deepens the chip's own accent rather than swapping to the // neutral border the surface controls use — an accent-soft chip // going grey on hover reads as losing its active state. - "inline-flex min-h-tap shrink-0 max-w-[12rem] items-center gap-1 rounded-[7px] border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-3 text-[color:var(--clinical-accent)] search-band-chip hover:border-[color:var(--clinical-accent)] hover:text-[color:var(--clinical-accent-hover)] sm:min-h-10", + "inline-flex min-h-tap shrink-0 max-w-[12rem] items-center gap-1 rounded-[7px] border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-3 text-[color:var(--clinical-accent)] search-band-chip hover:border-[color:var(--clinical-accent)] hover:text-[color:var(--clinical-accent-hover)] sm:min-h-compact-meta", focusRing, )} > @@ -713,7 +713,7 @@ export function SearchResultsHeaderBand({ // and an underline instead. 48px, not the 44px a generic tap rule // would suggest: `min-h-11` reintroduces a fixed `ui-smoke` // sub-pixel flake, and `--spacing-tap` is this repo's floor. - "search-band-ghost inline-flex min-h-tap shrink-0 items-center rounded-md px-2 text-[color:var(--text-muted)] underline decoration-[color:var(--border-strong)] underline-offset-2 hover:text-[color:var(--text)] hover:decoration-current sm:min-h-10", + "search-band-ghost inline-flex min-h-tap shrink-0 items-center rounded-md px-2 text-[color:var(--text-muted)] underline decoration-[color:var(--border-strong)] underline-offset-2 hover:text-[color:var(--text)] hover:decoration-current sm:min-h-compact-meta", focusRing, )} > @@ -747,7 +747,7 @@ export function SearchResultsHeaderBand({ busyLabel="Retrying…" onClick={retry} className={cn( - "inline-flex min-h-tap shrink-0 items-center justify-center gap-1.5 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-3 text-[color:var(--text-muted)] search-band-ghost hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)] sm:min-h-10", + "inline-flex min-h-tap shrink-0 items-center justify-center gap-1.5 rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] px-3 text-[color:var(--text-muted)] search-band-ghost hover:border-[color:var(--border-strong)] hover:text-[color:var(--text)] sm:min-h-compact-meta", focusRing, )} > @@ -793,7 +793,7 @@ export function ResultSortControl({ role="group" aria-label="Sort results" className={cn( - "hidden min-h-tap shrink-0 overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] shadow-[var(--shadow-inset)] sm:inline-flex sm:min-h-10", + "hidden min-h-tap shrink-0 overflow-hidden rounded-lg border border-[color:var(--border)] bg-[color:var(--surface)] shadow-[var(--shadow-inset)] sm:inline-flex sm:min-h-compact-meta", className, )} > @@ -812,7 +812,7 @@ export function ResultSortControl({ // overflow and the mask faded out its own last option. The 8px this // returns is what clears 393/402/430/440. Height is untouched — // `min-h-tap` is the tap floor, and it is the floor that matters. - "search-band-sort-option min-h-tap whitespace-nowrap px-2.5 sm:min-h-10 sm:px-3", + "search-band-sort-option min-h-tap whitespace-nowrap px-2.5 sm:min-h-compact-meta sm:px-3", index > 0 && "border-l border-[color:var(--border)]", focusRing, selected @@ -837,11 +837,11 @@ export function ResultSortControl({ * escape, and on therapy-compass `Clear search` is the only one for a query-only * zero result. Shipping the escape hatch below the floor the same change raised * the facets, the find field and the disclosure headings to would contradict the - * rule this component's own redesign argues for. It keeps the 40px pointer - * floor from `sm`, exactly like the filter controls. + * rule this component's own redesign argues for. It keeps `min-h-compact-meta` + * (40px) from `sm`, exactly like the filter controls. Not a primary CTA. */ const emptyStateAction = - "inline-flex min-h-tap items-center gap-1.5 rounded-lg border border-[color:var(--border)] px-3 text-xs font-semibold text-[color:var(--text-muted)] hover:text-[color:var(--text)] sm:min-h-10"; + "inline-flex min-h-tap items-center gap-1.5 rounded-lg border border-[color:var(--border)] px-3 text-xs font-semibold text-[color:var(--text-muted)] hover:text-[color:var(--text)] sm:min-h-compact-meta"; export function SearchResultsEmptyState({ modeId, @@ -1081,11 +1081,10 @@ export function SearchResultsEmptyState({ onClick={lastFilter.onRemove} data-testid="search-results-empty-remove-filter" className={cn( - // `sm:min-h-10`, not `sm:min-h-9`: the desktop floor in this file - // is 40px — `emptyStateAction` above and every shelf control use - // it — and this is the control the filtered path leads with. The - // phone floor was already correct. - "inline-flex min-h-tap items-center gap-1.5 rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-3 text-xs font-semibold text-[color:var(--clinical-accent)] hover:border-[color:var(--clinical-accent)] sm:min-h-10", + // `sm:min-h-compact-meta`, not `sm:min-h-9`: the desktop floor in + // this file is 40px compact-meta — `emptyStateAction` and every + // shelf control use it. Never min-h-11. The phone floor stays tap. + "inline-flex min-h-tap items-center gap-1.5 rounded-lg border border-[color:var(--clinical-accent-border)] bg-[color:var(--clinical-accent-soft)] px-3 text-xs font-semibold text-[color:var(--clinical-accent)] hover:border-[color:var(--clinical-accent)] sm:min-h-compact-meta", focusRing, )} > diff --git a/src/components/document-viewer/source-panels.tsx b/src/components/document-viewer/source-panels.tsx index 61d534c5fc..12ef72bad1 100644 --- a/src/components/document-viewer/source-panels.tsx +++ b/src/components/document-viewer/source-panels.tsx @@ -182,7 +182,7 @@ export function FormattedHighYieldSummary({ diff --git a/src/components/ui-primitives.tsx b/src/components/ui-primitives.tsx index 1f5137aef1..20875a3384 100644 --- a/src/components/ui-primitives.tsx +++ b/src/components/ui-primitives.tsx @@ -125,7 +125,8 @@ export const metadataPillDensity = { comfortable: `${metadataPillBase} min-h-8 px-2.5 text-xs`, roomy: `${metadataPillBase} min-h-8 px-3 text-xs`, tap: `${metadataPillBase} min-h-tap px-3 text-xs`, - interactiveCompact: `${metadataPillBase} min-h-tap px-2.5 text-2xs sm:min-h-9 sm:px-3`, + // DS-P2-24: metadata/disclosure only — 48px phone, 40px compact-meta from `sm`. Not a primary CTA. + interactiveCompact: `${metadataPillBase} min-h-tap px-2.5 text-2xs sm:min-h-compact-meta sm:px-3`, } as const; /** Standard metadata density. Use `metadataPillDensity` when a different named density is intentional. */ export const metadataPill = metadataPillDensity.standard; @@ -173,8 +174,9 @@ export const tableCard = "overflow-hidden rounded-lg border border-[color:var(--border)]/80 bg-[color:var(--surface)] shadow-[var(--e1)]"; export const tableCardHeader = "border-b border-[color:var(--border)] bg-[color:var(--clinical-chat-table-header)] px-3 py-2.5 text-sm font-semibold text-[color:var(--text-heading)]"; +// DS-P2-24: table micro-actions — compact-meta from `sm`, never `--row-compact` (36px) as tap. export const tableMicroActionRow = - "flex min-h-tap flex-wrap items-center gap-1 border-t border-[color:var(--border)] px-2 py-1.5 text-xs font-semibold text-[color:var(--text-muted)] sm:min-h-9"; + "flex min-h-tap flex-wrap items-center gap-1 border-t border-[color:var(--border)] px-2 py-1.5 text-xs font-semibold text-[color:var(--text-muted)] sm:min-h-compact-meta"; export const sidebarItem = `flex min-h-tap min-w-0 w-full items-center gap-2 overflow-hidden rounded-lg px-2.5 text-sm font-semibold text-[color:var(--text-muted)] transition hover:bg-[color:var(--surface-subtle)] hover:text-[color:var(--text)] focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-[color:var(--focus)] ${controlDisabled}`; const statusMarkerBase = "inline-block h-2 w-2 shrink-0"; export const statusDotReady = `${statusMarkerBase} rounded-full border-2 border-[color:var(--text-heading)] bg-transparent`; From 74b830888158d7b52061edc4f411d9af7b39c774 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 27 Aug 2026 12:46:42 +0000 Subject: [PATCH 2/2] fix(design-system): track compact-meta in tailwind-merge spacing config --spacing-compact-meta was added to globals.css for the density policy but not added to CLINICAL_TWMERGE_THEME.spacing, failing the config tracking test. --- src/lib/tailwind-merge.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/tailwind-merge.ts b/src/lib/tailwind-merge.ts index 6d3bfb06e7..1d7418c99a 100644 --- a/src/lib/tailwind-merge.ts +++ b/src/lib/tailwind-merge.ts @@ -93,6 +93,7 @@ export const CLINICAL_TWMERGE_THEME = { // `tap` would reduce one. Re-measure before introducing a same-variant pair: // order decides the outcome, and tap-then-numeric is the forbidden direction. spacing: [ + "compact-meta", "hero-medallion", "icon-xs", "icon-sm",