git:20261019.f8d956f to git:20261019.a95630f
2 added, 1 removed. Audit A to A.
# DOX — packages/client-utils/src
Files in this directory. One row per source file.
| File | Purpose |
|------|---------|
- | `ActionList.tsx` | `ui:action-list` primitive. Exports `ActionList({ actions })` — horizontal row of MDI-icon + label buttons. Under non-null `ActionListMenuContext` renders vertical `role=menuitem` buttons (`data-testid=action-list-item`); disabled = `aria-disabled`, no invoke; enabled activation runs handler then `ctx.onActivate?.()`. See change: add-session-actions-flyout. `ExtendedActionItem` carries `dataAction` (pluginId/sessionId/action/payload) dispatched via `sendPluginAction`, or classical `onClick`. `IconByKey` resolves the icon key via `useMdiIconByKey` (lazy full set; renders nothing while loading or for unknown keys, no placeholder). See change: shrink-client-index-chunk. See change: harden-ios-safari-memory-and-ws-diagnostics. |
+ | `ActionList.tsx` | `ui:action-list` primitive. Exports `ActionList({ actions })` — horizontal row of MDI-icon + label buttons. Under non-null `ActionListMenuContext` renders vertical `role=menuitem` buttons (`data-testid=action-list-item`); disabled = `aria-disabled`, no invoke; enabled activation runs handler then `ctx.onActivate?.()`. See change: add-session-actions-flyout. `ExtendedActionItem` carries `dataAction` (pluginId/sessionId/action/payload) dispatched via `sendPluginAction`, or classical `onClick`. `IconByKey` resolves the icon key via `useMdiIconByKey` (lazy full set; renders nothing while loading or for unknown keys, no placeholder). See change: shrink-client-index-chunk. See change: harden-ios-safari-memory-and-ws-diagnostics. Honours `tone` (icon class via `tone.ts`, `data-action-tone`, enabled danger row classes); `ExtendedActionItem = UiActionListItem & {...}`. See change: colorize-actions-menus. |
| `ActionListMenuContext.ts` | Exports `ActionListMenuContext` (`createContext<ActionListMenuContextValue \| null>(null)`) + `ActionListMenuContextValue {onActivate?}`. Non-null value switches `ActionList` to menuitem rendering. Subpath export `./ActionListMenuContext`. See change: add-session-actions-flyout. |
| `agent-card-utils.ts` | Shared helpers for agent card rendering. Exports `formatTokens`, `formatDuration`, `AgentCardStatus` type, `StatusIconDef`, `statusIconDefs` (pending/running/complete/error/blocked/stopped/background/elided → MDI icon + color), `getStatusIcon`. Used by `FlowAgentCard` and `AgentToolRenderer`. |
| `AgentCardShell.tsx` | Exports `AgentCardShell` — reusable card container with status-colored border, header row (status icon + name + optional `headerRight`), optional `stats` line, clickable/selected states. `getStatusIcon` drives icon + border color. Consumers pass tool-specific content as children. |
| `Confirm.tsx` | Confirm preset over Dialog (size sm). Props `{open,onClose,title,message,body?,intent?,confirmLabel?,cancelLabel?,onConfirm,testId?}`. Action → onConfirm only (no auto-close); cancel/Esc/overlay → onClose. Replaces legacy ConfirmDialog. See change: unify-dialog-system. |
| `Dialog.tsx` | Unified modal shell. Props `{open,onClose,title?,icon?,size?,testId?,ariaLabel?,flush?,showClose?,children}`. Owns DialogPortal, overlay `bg-black/60`, Esc via shared escape-stack (`useEscapeDismiss(open,onClose)` — onClose fires on Escape only when topmost registered layer), click-outside, focus trap (useFocusTrap), `role="dialog"`/`aria-modal`/`aria-labelledby`, `z-dialog`, size sm/md/lg/full → max-w-sm/md/lg/[95vw], cap `max-h-[80vh]` (full: `max-h-[92vh]`). `flush` = edge-to-edge body: `overflow-hidden flex flex-col min-h-0` — a CAPPED FLEX COLUMN, no definite height, so a flush child MUST size `flex-1 min-h-0` (an `h-full` child resolves against an indefinite parent, grows to content, and its `overflow-y-auto` never becomes a scroller). Built-in ✕ (testId `<testId>-close`) renders when the dialog is NOT flush, or when `showClose` is set; a flush child is self-framed and owns its own dismissal. `showClose` restores it — REQUIRED for a flush child with no focusable of its own. Static subcomponents `Dialog.Footer`/`Dialog.Cancel`/`Dialog.Action` (intent primary / danger / neutral). Panel element also published as the `LayerHostProvider` host for `{children}` (callback ref → state; `containerRef` retained for `useFocusTrap`), so a child `LayerPortal` popover portals INTO the panel instead of `body` — at `body` it would sit at `z-popover` 40 BELOW this dialog's `z-dialog` 50 backdrop and every click on it would dismiss the dialog. `Dialog.Action` primary fill = `bg-[var(--accent-solid)]` (5.17:1 under white; `--accent-primary` is 3.68:1), app-wide. See change: unify-dialog-system, fix-flush-dialog-scroll-and-close-collision, fix-composer-popover-layering, add-pi-spec-driven-schema. Contract recipe: scrim `--bg-overlay`; panel `--bg-secondary` + `--border-secondary` + `rounded-xl`; title `text-lg font-semibold`; icon tile = info severity triple; close button 44 px (36 px from `sm`) with header `pr-12`; actions + Cancel share `ACTION_BASE` (`min-h-[44px] sm:min-h-[36px]`, semibold); danger = `--tint-red-*` (no `bg-red-600`). Props unchanged. See change: align-ui-contract-recipes. |
| `DialogPortal.tsx` | Exports `DialogPortal` — `createPortal` to `document.body`. Hides body scroll (`overflow: hidden`) on mount, restores prior value on unmount. |
| `LayerPortal.tsx` | Exports `LayerPortal` — `createPortal` to `document.body` WITHOUT locking body scroll (unlike `DialogPortal`). The overlay-layering "portal-or-perish" primitive: escapes ancestor stacking contexts (`transform`/`will-change`/`opacity`/`isolate`/`z-*`) so a portaled overlay can't UNDERLAP a sibling (e.g. a `SessionCard`, which sets `isolate`). Portaled surface positions itself `fixed` from its trigger rect + a `z-*` layer token. Use for menus/popovers/dropdowns; `DialogPortal` for modals. Also exports `LayerHostContext` (default `null`) + `LayerHostProvider` — portal target is `host ?? document.body`, so an overlay opened inside a higher-ranked layer (a `Dialog`) portals into THAT layer's own element rather than competing with it from the root stacking context. `null` = `body` (the composer/card case, and while a host element is still mounting). See spec overlay-layering. See change: add-overlay-layering-system, fix-composer-popover-layering. |
| `EmptyState.tsx` | EmptyState primitive. Props `{title, body?, icon?, action?, secondaryAction?}`. Single `action` prop enforces one-primary-CTA. Renders NN/g empty-state pattern: value-framed heading, optional body, ≤1 primary CTA, ≤1 secondary escape-hatch. data-empty-state markers. See change: extend-client-utils-state-feedback-primitives. |
| `escape-stack.ts` | Shared escape-dismiss stack. Single module-stable `document`-bubble `keydown` listener + LIFO registry; Escape invokes `onEscape` of topmost registered layer only, then `preventDefault`+`stopImmediatePropagation`; guards `e.repeat`+`e.defaultPrevented`. Attach-once/never-detach (early-returns on empty). Exports `useEscapeDismiss(active,onEscape)` (`useId`-stable id, ref-backed onEscape, remove-by-id), `registerEscapeLayer`/`unregisterEscapeLayer`, `__resetEscapeStack` (test-only, prod no-op). See change: fix-stacked-escape-closes-layers. |
| `focusRing.ts` | Exports `focusRing` = "focus-ring" className string. Points at `.focus-ring` utility in packages/client/src/index.css (:focus-visible, 2px outline, 2px offset, var(--focus-ring) color). Replaces ad-hoc focus:outline-none + 1px border. WCAG 2.2 §2.4.7/§2.4.11. See change: extend-client-utils-state-feedback-primitives. |
| `mdi-by-key.ts` | Lazy MDI icon-by-key resolver (subpath `./mdi-by-key`). Exports `loadMdiIconSet()` (memoized `import("@mdi/js/commonjs/mdi.js")` — DISTINCT module identity from the `@mdi/js` named-import entry, so Rollup can split it; reads `mod.default ?? mod`; failure → `null` + memo cleared for retry), `resolveMdiIconSync(key)` (`null` until loaded / non-`mdi` / unknown), `useMdiIconByKey(key)` (triggers load, re-renders once). Test seams `__setMdiIconSetImporterForTests`, `__resetMdiIconSetForTests`. Keeps the ~2.8 MB full set off the cold landing (#712). See change: harden-ios-safari-memory-and-ws-diagnostics. |
| `Popover.tsx` | `ui:popover` primitive. Exports `Popover`, `PopoverProps`, `computePopoverPosition` (pure). Anchors floating panel to `anchorEl`'s rect; flips above on bottom overflow, shifts left on right overflow; recomputes on resize/scroll. Dismisses on outside mousedown or Escape. Renders via `LayerPortal` (body, or dialog layer host); `zIndex: var(--z-popover)`, never numeric. Not modal/scroll-lock/tooltip. See change: add-session-actions-flyout. |
| `SearchableSelectDialog.tsx` | Exports `SearchableSelectDialog`, `SelectOption`. Filterable list inside `Dialog` (size `sm`). Arrow/Enter/Esc keyboard nav (arrows SKIP `disabled` options; Enter no-ops on them), scrolls the selected row into view via `[data-active="true"]` (group captions are separate children), optional badge. `SelectOption` gains OPTIONAL `group` (caption per group, first-appearance order), `disabled`+`disabledReason` (visible, non-selectable, `aria-disabled`+`title`), `pinned` (always rendered regardless of the filter); filter matches label, description AND `value` (the id). All additive — unset options render the original flat list. Consumers: providers Add dialog (grouped, suppressed twins, pinned Custom endpoint), OpenSpec/session pickers. See change: redesign-providers-settings-page. |
| `Skeleton.tsx` | Skeleton primitive for content-layout loads. Props `{variant=text\|card\|bubble\|row, count?}`. Honors prefers-reduced-motion via useMediaQuery (static, no animate-pulse). aria-hidden. data-skeleton / data-skeleton-item / data-static markers. See change: extend-client-utils-state-feedback-primitives. |
| `StatusPill.tsx` | `ui:status-pill` primitive. Exports `StatusPill({ state, text, icon?, tooltip? })` — props unchanged. Renders the runtime kit `SettingsStatusPill` inside a `<span data-status-pill={state} title={tooltip}>`; state→tone `running/info→info, success, warn→warning, error, muted→neutral`. Colours come from `--severity-*` (no raw maps, no theme observer). `running` = spinning `mdiLoading` (ignores `icon`); `muted` supplies `mdiMinusCircleOutline` (kit neutral has no default glyph); an `icon` key resolves lazily via `useMdiIconByKey` and the tone default shows until it resolves or when the key is unknown. See change: adopt-server-driven-intent-rendering, harden-ios-safari-memory-and-ws-diagnostics, align-ui-contract-recipes. |
| `statusPresentation.ts` | `statusPresentation(kind)` maps StatusKind (done\|current\|todo\|error) to `{glyph, tokenVar, label}`. Glyph = mandatory non-hue channel (✓ done, ▸ current, ○ todo, ✕ error). tokenVar references semantic --status-* / --text-muted token. `statusAriaLabel(name, kind)` returns "Name, label". WCAG 2.2 §1.4.1. Consumed by composer ArtifactChip + board BoardStatePill. See change: extend-client-utils-state-feedback-primitives. |
+ | `tone.ts` | `toneIconClass`, `toneRowClass`, `toneAttr` — total prototype-safe tone → class helpers (literal maps, never interpolated; only enabled danger gets a row class). Subpath `./tone`. See change: colorize-actions-menus. |
| `useFocusTrap.ts` | `useFocusTrap(ref,open)` hook. Stores prev activeElement, focuses first focusable child on open, traps Tab/Shift+Tab, restores focus on close. With NO focusable child at open it focuses the container AND arms a MutationObserver that hands focus to the first focusable that appears — once, and only while focus is still on the container (a flush Dialog no longer renders a ✕, so nothing else guarantees a focusable at mount; measured: `/settings/general` has 0 at mount, 51 later). See change: unify-dialog-system, fix-flush-dialog-scroll-and-close-collision. |
| `useMediaQuery.ts` | Reactive CSS media-query hook. Returns match boolean. Guards missing window.matchMedia (returns false). See change: extend-client-utils-state-feedback-primitives. |
| `useMobile.tsx` | Exports `MobileProvider` + `useMobile`. `MobileContext` provides boolean: viewport `max-width: 767px` OR `max-height: 599px` (catches landscape phones). Backed by `useMediaQuery`. |
| `useZoomPan.ts` | Exports `useZoomPan`, `ZoomPanState`, `UseZoomPanOptions`. State `{ scale, translateX, translateY }`. Wheel zoom centered on cursor, pointer drag pan (deferred capture past `DRAG_THRESHOLD` so clicks reach children), pinch zoom, double-click reset, `zoomIn`/`zoomOut`/`reset` controls. Clamp `minScale`/`maxScale`. Used by `MermaidBlock`, `FlowGraph`. Optional `initialScale` (clamped to band) seeds state; `reset`/double-click return to it; re-seeds on change until user input; returns `initialScale`. See change: fix-markdown-remount-storm. |
| `ZoomControls.tsx` | Exports `ZoomControls({ onZoomIn, onZoomOut, onReset, scale })` — top-right overlay buttons (zoom in/out/reset) using MDI icons, percent readout when `scale !== 1`. Shared by `MermaidBlock` and `FlowGraph`. Stops pointer propagation. Optional `initialScale` (default 1): % readout hidden at that scale. See change: fix-markdown-remount-storm. Optional `labels` prop (`ZoomControlLabels`, English defaults) → `aria-label` + `title`; icons `aria-hidden`; buttons ≥44px below `sm`, ≥32px from `sm`, `focus-ring`; readout `aria-live=polite` 11px `--text-secondary`. See change: retro-ux-chat-rendering. |