@kerfjs/ui 5.0.0-beta.53 → 5.0.0-beta.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +1 -0
  2. package/ai/application-ui-diagnostic-ids-v1.json +1 -0
  3. package/ai/component-catalog-v2.json +82 -6
  4. package/ai/component-catalog-v2.schema.json +2 -1
  5. package/ai/component-catalog.json +105 -10
  6. package/ai/public-api-signatures-v1.md +204 -18
  7. package/ai/skill.md +23 -17
  8. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  9. package/analyzer/index.mjs +48 -0
  10. package/analyzer/loud-pairs.d.mts +49 -0
  11. package/analyzer/loud-pairs.mjs +357 -0
  12. package/dist/app-tab.d.ts +7 -5
  13. package/dist/app-tab.js +1 -1
  14. package/dist/{chunk-EKYZCRCX.js → chunk-3CGQ7ZNO.js} +54 -22
  15. package/dist/chunk-3CGQ7ZNO.js.map +1 -0
  16. package/dist/{chunk-UKKSMZYT.js → chunk-46263QLK.js} +51 -1
  17. package/dist/chunk-46263QLK.js.map +1 -0
  18. package/dist/{chunk-NDUGJ4TU.js → chunk-5NDQABWK.js} +1 -1
  19. package/dist/{chunk-NDUGJ4TU.js.map → chunk-5NDQABWK.js.map} +1 -1
  20. package/dist/{chunk-CCMKJWLX.js → chunk-NE5JAOQS.js} +7 -7
  21. package/dist/chunk-NE5JAOQS.js.map +1 -0
  22. package/dist/chunk-PSHUSWQH.js +18 -0
  23. package/dist/chunk-PSHUSWQH.js.map +1 -0
  24. package/dist/{chunk-VOCPXGQN.js → chunk-VHADGP3B.js} +9 -8
  25. package/dist/chunk-VHADGP3B.js.map +1 -0
  26. package/dist/chunk-XQV2B4IL.js +14 -0
  27. package/dist/chunk-XQV2B4IL.js.map +1 -0
  28. package/dist/collapsible-panel.d.ts +6 -2
  29. package/dist/collapsible-panel.js +1 -0
  30. package/dist/collapsible-panel.js.map +1 -1
  31. package/dist/floating-toolbar.d.ts +5 -2
  32. package/dist/floating-toolbar.js +1 -1
  33. package/dist/index.js +6 -5
  34. package/dist/resizable-region.d.ts +4 -1
  35. package/dist/resizable-region.js +1 -1
  36. package/dist/split-view.js +1 -1
  37. package/dist/styles/collapsible-panel.css +88 -10
  38. package/dist/styles/floating-toolbar.css +11 -6
  39. package/dist/styles/resizable-region.css +81 -13
  40. package/dist/styles/workbench.css +472 -10
  41. package/dist/wire-resizable-regions.d.ts +2 -1
  42. package/dist/wire-resizable-regions.js +2 -1
  43. package/dist/wire-tab-bars.js +1 -1
  44. package/dist/wire-workbench.d.ts +109 -0
  45. package/dist/wire-workbench.js +355 -0
  46. package/dist/wire-workbench.js.map +1 -0
  47. package/dist/workbench.d.ts +65 -6
  48. package/dist/workbench.js +180 -76
  49. package/dist/workbench.js.map +1 -1
  50. package/docs/accessibility.md +54 -9
  51. package/docs/app-layouts.md +12 -1
  52. package/docs/collapsible-panel.md +59 -1
  53. package/docs/component-selection.md +16 -6
  54. package/docs/document-baseline.md +4 -0
  55. package/docs/layout.md +5 -1
  56. package/docs/recipes.md +5 -3
  57. package/docs/type-contracts.md +2 -1
  58. package/docs/ui-analyzer.md +94 -19
  59. package/docs/ui-doctor.md +6 -0
  60. package/docs/ux-demo.md +1 -1
  61. package/docs/webawesome-theme.md +11 -2
  62. package/docs/workbench.md +302 -7
  63. package/llms.txt +7 -3
  64. package/package.json +5 -1
  65. package/ux-demo/recipes/collapsible-sidebar.tsx +8 -7
  66. package/dist/chunk-CCMKJWLX.js.map +0 -1
  67. package/dist/chunk-EKYZCRCX.js.map +0 -1
  68. package/dist/chunk-UKKSMZYT.js.map +0 -1
  69. package/dist/chunk-VOCPXGQN.js.map +0 -1
@@ -1,6 +1,6 @@
1
1
  # Public API signatures for the UI authoring corpus
2
2
 
3
- Generated from emitted declarations for `@kerfjs/ui@5.0.0-beta.53` and `kerfjs@5.0.0-beta.53`. This bounded reference covers only APIs used by the seven-task corpus. It is interface evidence, not an implementation or runtime guarantee.
3
+ Generated from emitted declarations for `@kerfjs/ui@5.0.0-beta.54` and `kerfjs@5.0.0-beta.54`. This bounded reference covers only APIs used by the seven-task corpus. It is interface evidence, not an implementation or runtime guarantee.
4
4
 
5
5
  ## `@kerfjs/ui/css-values`
6
6
 
@@ -340,8 +340,11 @@ interface FloatingToolbarProps {
340
340
  * restore button) that sits over the content but NOT over dialogs or overlays
341
341
  * (it is not in the top layer). It is inset from the container edges by
342
342
  * `--kui-floating-toolbar-inset` (default `--kui-space-m`, i.e. 8px more than a
343
- * top toolbar's own inset); override that token to move it. The app owns the
344
- * controls and their behavior — wire them with `delegate()` as usual.
343
+ * top toolbar's own inset); override that token, on the toolbar or an
344
+ * ancestor, to move it. Inside a `Workbench`, `CollapsiblePanel`, or
345
+ * `ResizableRegion` restore corner the corner owns the inset, so the toolbar
346
+ * floats from the corner's own position. The app owns the controls and their
347
+ * behavior — wire them with `delegate()` as usual.
345
348
  */
346
349
  declare function FloatingToolbar({ children, label, position, className, slot, }: FloatingToolbarProps): kerfjs.SafeHtml;
347
350
 
@@ -751,11 +754,13 @@ interface AppTabProps {
751
754
  /** Render as an unanimated loading skeleton, disabling select/close and dragging. */
752
755
  placeholder?: boolean;
753
756
  /**
754
- * Dormant but named: the tab is known and still opening. Keeps `name`
755
- * visible in the quiet text color and as the tab's accessible name, shows
756
- * `trailing` (for example a `LoadingSpinner`), and disables select, close,
757
- * and dragging with `aria-busy`, like `placeholder`. Same pill geometry as
758
- * the live tab, so swapping it in place does not shift the bar.
757
+ * Named and still opening: the tab is known but its content is loading.
758
+ * Keeps `name` visible in the quiet text color and as the tab's accessible
759
+ * name, shows `trailing` (for example a `LoadingSpinner`), and marks the
760
+ * tab `aria-busy`. Unlike `placeholder` it stays selectable — the
761
+ * application shows placeholder content in its panel until loading
762
+ * completes — while close and dragging stay disabled. Same pill geometry
763
+ * as the live tab, so swapping it in place does not shift the bar.
759
764
  * `placeholder` wins when both are set.
760
765
  */
761
766
  pending?: boolean;
@@ -1051,24 +1056,64 @@ import { SafeHtml } from 'kerfjs';
1051
1056
  import { ResizableRegionSeparator, ResizableRegionCollapseMotion, ResizableRegionContentOverflow, ResizableRegionPresentation, ResizableRegionRestorePosition } from './resizable-region.js';
1052
1057
  import { K as KerfUiContent } from './semantic-content-BbzjvSu9.js';
1053
1058
 
1059
+ /**
1060
+ * The Workbench container breakpoint below which a panel presents as an
1061
+ * overlay: `narrow` (704px or less) or `compact` (448px or less) — the same
1062
+ * breakpoints as `ResizableRegion`'s `responsiveFillAt`.
1063
+ */
1064
+ type WorkbenchResponsiveOverlayAt = 'compact' | 'narrow';
1065
+ /** Drag-resize limits for a resizable Workbench panel, in px. */
1066
+ interface WorkbenchPanelResizable {
1067
+ /** Smallest size (default 180 for a rail, 120 for the drawer). */
1068
+ min?: number;
1069
+ /** Largest size (default 480). */
1070
+ max?: number;
1071
+ }
1054
1072
  /** A collapsible Workbench panel — a side rail or the bottom drawer. */
1055
1073
  interface WorkbenchPanel {
1056
1074
  content: KerfUiContent;
1057
1075
  /** Whether the panel is currently collapsed (the app owns this). */
1058
1076
  collapsed?: boolean;
1059
- /** Rail width, or drawer height, in px. Overrides the CSS default. */
1077
+ /**
1078
+ * Rail width, or drawer height, in px. Overrides the CSS default. For a
1079
+ * resizable panel it is the current size (default 280 for a rail, 220 for
1080
+ * the drawer), clamped to the limits; the app owns it and `wireWorkbench`
1081
+ * reports each resize.
1082
+ */
1060
1083
  size?: number;
1084
+ /**
1085
+ * Opt in to drag and keyboard resizing on the panel's inner separator:
1086
+ * `true` for the default limits, or `{ min, max }`. Off by default. Pair it
1087
+ * with `wireWorkbench` from `@kerfjs/ui/wire-workbench`.
1088
+ */
1089
+ resizable?: boolean | WorkbenchPanelResizable;
1061
1090
  /** Accessible name for the panel region. */
1062
1091
  label?: string;
1063
1092
  separator?: ResizableRegionSeparator;
1064
1093
  collapseMotion?: ResizableRegionCollapseMotion;
1065
1094
  contentOverflow?: ResizableRegionContentOverflow;
1066
1095
  presentation?: ResizableRegionPresentation;
1067
- /** Control shown in a safe-area-aware viewport corner while collapsed. */
1096
+ /**
1097
+ * Present the panel as an overlay, without a separator, below a Workbench
1098
+ * container breakpoint, and inline above it — the CSS decides, so the app
1099
+ * needs no device-class check. A rail overlays from its side at full
1100
+ * height; the bottom drawer overlays the bottom of the work-area column.
1101
+ */
1102
+ responsiveOverlayAt?: WorkbenchResponsiveOverlayAt;
1103
+ /**
1104
+ * Control shown while collapsed, in a safe-area-aware corner of the
1105
+ * Workbench (not the viewport); the bottom drawer's sits in the work-area
1106
+ * column.
1107
+ */
1068
1108
  restoreControl?: SafeHtml;
1069
1109
  restorePosition?: ResizableRegionRestorePosition;
1070
1110
  }
1071
1111
  interface WorkbenchProps {
1112
+ /**
1113
+ * The Workbench's `id`. Each panel's `id` derives from it —
1114
+ * `<id>-left-rail`, `<id>-right-rail`, `<id>-bottom-drawer` — so a toggle
1115
+ * can name the panel it shows and hides with `aria-controls`.
1116
+ */
1072
1117
  id: string;
1073
1118
  label: string;
1074
1119
  /** The central work area. */
@@ -1076,6 +1121,22 @@ interface WorkbenchProps {
1076
1121
  leftRail?: WorkbenchPanel;
1077
1122
  rightRail?: WorkbenchPanel;
1078
1123
  bottomDrawer?: WorkbenchPanel;
1124
+ /**
1125
+ * Minimum width, in px, the work area keeps beside the inline rails, fixed
1126
+ * or `resizable` (default 320; `0` turns it off). Resizing a rail stops
1127
+ * where the work area would drop below it, and inline rails shrink
1128
+ * proportionally when the Workbench gets narrower. Workbenches without a
1129
+ * rail ignore it.
1130
+ */
1131
+ mainMinSize?: number;
1132
+ /**
1133
+ * Minimum height, in px, the work area keeps above an inline bottom drawer,
1134
+ * fixed or `resizable` (default 120; `0` turns it off) — the drawer's
1135
+ * counterpart of `mainMinSize`. Resizing the drawer stops where the work
1136
+ * area would drop below it, and the drawer shrinks when the Workbench gets
1137
+ * shorter. Workbenches without a drawer ignore it.
1138
+ */
1139
+ mainMinHeight?: number;
1079
1140
  className?: string;
1080
1141
  /** Native named-slot assignment when composed inside a web component. */
1081
1142
  slot?: string;
@@ -1087,12 +1148,129 @@ interface WorkbenchProps {
1087
1148
  * a composited transform — the instant-width / sliding-content technique, so the
1088
1149
  * work area relayouts once, not per frame. Bottom-drawer content stays anchored
1089
1150
  * to the shell's stable bottom edge throughout that transition. The app owns
1090
- * each `collapsed` flag; the collapse is pure CSS (no wire). See
1091
- * `docs/23-app-layouts.md` §3.3.
1151
+ * each `collapsed` flag; the collapse is pure CSS. A collapsed panel renders
1152
+ * `inert` and `aria-hidden`, so neither Tab nor assistive technology reaches
1153
+ * controls that have slid out of view or an empty landmark (its restore control
1154
+ * lives outside it and stays reachable). A panel may opt in to drag and keyboard resizing with
1155
+ * `resizable`, which `wireWorkbench` drives. See `docs/23-app-layouts.md` §3.3.
1156
+ */
1157
+ declare function Workbench({ id, label, main, leftRail, rightRail, bottomDrawer, mainMinSize, mainMinHeight, className, slot, }: WorkbenchProps): SafeHtml;
1158
+
1159
+ export { Workbench, type WorkbenchPanel, type WorkbenchPanelResizable, type WorkbenchProps, type WorkbenchResponsiveOverlayAt };
1160
+ ```
1161
+
1162
+ ## `@kerfjs/ui/wire-workbench`
1163
+
1164
+ ```ts
1165
+ import { Signal, ReadonlySignal } from 'kerfjs';
1166
+ import { DeviceClass } from './device-class.js';
1167
+
1168
+ /** A Workbench panel slot, named after its `WorkbenchProps` prop. */
1169
+ type WorkbenchPanelKey = 'leftRail' | 'rightRail' | 'bottomDrawer';
1170
+
1171
+ /** Minimal `localStorage`-shaped store, so the persistence hook is testable. */
1172
+ interface WorkbenchStorage {
1173
+ getItem(key: string): string | null;
1174
+ setItem(key: string, value: string): void;
1175
+ }
1176
+ /** One Workbench panel's app-owned state. */
1177
+ interface WireWorkbenchPanel {
1178
+ /**
1179
+ * The app-owned size signal a `resizable` panel renders as its `size`.
1180
+ * `wireWorkbench` writes each committed resize here; collapsing never
1181
+ * touches it, so an expanded panel returns at the size it had. Omit it for
1182
+ * a panel that is not resizable.
1183
+ */
1184
+ size?: Signal<number>;
1185
+ /**
1186
+ * When set with `size`, the size is loaded from and saved to `storage`
1187
+ * under this key, so the panel remembers the user's size.
1188
+ */
1189
+ storageKey?: string;
1190
+ /**
1191
+ * The app-owned signal the panel renders as its `collapsed`. With it (and
1192
+ * `dismissOverlays`, on by default) `wireWorkbench` treats the panel as a
1193
+ * transient overlay while it presents as one: it collapses when its
1194
+ * `responsiveOverlayAt` breakpoint begins to apply and gets its inline state
1195
+ * back when the breakpoint stops applying, an open overlay takes focus and
1196
+ * keeps Tab inside it, and it closes on Escape or a press outside it.
1197
+ */
1198
+ collapsed?: Signal<boolean>;
1199
+ }
1200
+ /** A resize the user made, after `wireWorkbench` wrote it to the size signal. */
1201
+ interface WorkbenchResize {
1202
+ panel: WorkbenchPanelKey;
1203
+ size: number;
1204
+ source: 'keyboard' | 'pointer';
1205
+ }
1206
+ interface WireWorkbenchOptions {
1207
+ /** The `id` the `Workbench` was rendered with. */
1208
+ id: string;
1209
+ /**
1210
+ * The wired panels, keyed like the `Workbench` props. Render a panel given
1211
+ * a `size` with `resizable` and `size={panel.size.value}`, and one given a
1212
+ * `collapsed` signal with `collapsed={panel.collapsed.value}`.
1213
+ */
1214
+ panels: Partial<Record<WorkbenchPanelKey, WireWorkbenchPanel>>;
1215
+ /**
1216
+ * When provided, resizing is suspended while `deviceClass.compact` is true —
1217
+ * the classes where rails become overlay drawers or are replaced.
1218
+ */
1219
+ deviceClass?: ReadonlySignal<DeviceClass>;
1220
+ /** Persistence store (default `globalThis.localStorage`, if present). */
1221
+ storage?: WorkbenchStorage;
1222
+ /** Keyboard step in px (default 16). */
1223
+ step?: number;
1224
+ /** Shift+arrow step in px (default 64). */
1225
+ largeStep?: number;
1226
+ /** Called after each committed resize. */
1227
+ onResize?: (change: WorkbenchResize) => void;
1228
+ /**
1229
+ * Treat the panels given a `collapsed` signal as transient overlays while
1230
+ * they present as overlays (default `true`), like `wireSidebar`'s compact
1231
+ * overlay: a panel whose `responsiveOverlayAt` breakpoint begins to apply
1232
+ * starts collapsed, with no collapse motion, and gets its inline collapsed
1233
+ * state back when the breakpoint stops applying (and on disposal); an open
1234
+ * overlay panel, responsive or `presentation: "overlay"`, closes on Escape
1235
+ * or a press that starts and ends outside it. A panel that opens as an
1236
+ * overlay takes focus on its first focusable control, and while it is open
1237
+ * Tab and Shift+Tab cycle through its controls instead of reaching the
1238
+ * work area it covers (the ARIA dialog pattern of `wireSidebar`'s compact
1239
+ * overlay); inline panels never move focus. Focus stranded in a closing
1240
+ * panel — however it closed, the app's own control inside it included —
1241
+ * returns to the control that had it when the panel opened, else to the
1242
+ * panel's restore control, else to a control outside the panel whose
1243
+ * `aria-controls` names it (each panel's `id` is its region id, e.g.
1244
+ * `studio-left-rail`) — the fallback for a panel already open at wire-up.
1245
+ * An opener inside a panel that has closed since is skipped. `false` leaves
1246
+ * every `collapsed` write and all focus handling to the app.
1247
+ */
1248
+ dismissOverlays?: boolean;
1249
+ /**
1250
+ * Keep overlays exclusive (default `true`), like `wireSidebar`'s
1251
+ * `exclusiveCompact`: when a panel opens while it presents as an overlay,
1252
+ * every other open overlay panel — rails and the bottom drawer alike —
1253
+ * closes, so one overlay never covers another's controls. Panels presenting
1254
+ * inline are never closed by it. Applies only with `dismissOverlays`.
1255
+ */
1256
+ exclusiveOverlays?: boolean;
1257
+ }
1258
+ /**
1259
+ * Wire a `Workbench`'s panels. For `resizable` panels given a `size` signal:
1260
+ * pointer drags and arrow / Shift+arrow / Home / End on each panel's
1261
+ * separator, clamped to the panel's limits and to the room that leaves the
1262
+ * work area its minimum width and height, committed to the app-owned size signals.
1263
+ * Optional persistence loads and saves each size; optional `deviceClass`
1264
+ * suspends resizing on compact classes. Collapse stays the app's `collapsed`
1265
+ * flag and never changes a size. For panels given a `collapsed` signal,
1266
+ * overlays are transient (`dismissOverlays`): a responsive overlay starts
1267
+ * collapsed, an open overlay takes focus and keeps Tab inside it, it closes
1268
+ * on Escape or an outside press, and
1269
+ * opening one overlay closes the others (`exclusiveOverlays`). Returns a disposer. See `docs/23-app-layouts.md` §3.3.
1092
1270
  */
1093
- declare function Workbench({ id, label, main, leftRail, rightRail, bottomDrawer, className, slot, }: WorkbenchProps): SafeHtml;
1271
+ declare function wireWorkbench(root: HTMLElement, { id, panels, deviceClass, storage, step, largeStep, onResize, dismissOverlays, exclusiveOverlays, }: WireWorkbenchOptions): () => void;
1094
1272
 
1095
- export { Workbench, type WorkbenchPanel, type WorkbenchProps };
1273
+ export { type WireWorkbenchOptions, type WireWorkbenchPanel, type WorkbenchPanelKey, type WorkbenchResize, type WorkbenchStorage, wireWorkbench };
1096
1274
  ```
1097
1275
 
1098
1276
  ## `@kerfjs/ui/collapsible-panel`
@@ -1157,7 +1335,7 @@ interface CollapsiblePanelProps {
1157
1335
  collapseMotion?: ResizableRegionCollapseMotion;
1158
1336
  contentOverflow?: ResizableRegionContentOverflow;
1159
1337
  presentation?: ResizableRegionPresentation;
1160
- /** Control shown in a safe-area-aware viewport corner while collapsed. */
1338
+ /** Control shown while collapsed, in a safe-area-aware corner of the panel's container (not the viewport). */
1161
1339
  restoreControl?: SafeHtml;
1162
1340
  restorePosition?: ResizableRegionRestorePosition;
1163
1341
  className?: string;
@@ -1169,7 +1347,11 @@ interface CollapsiblePanelProps {
1169
1347
  * slides out via `transform` (one reflow, composited — the same technique
1170
1348
  * `Workbench` and the catalog sidebar use). Bottom-drawer content stays anchored
1171
1349
  * to the panel's fixed bottom edge, so the track cannot move its layout origin
1172
- * underneath the transform transition. The app owns the `collapsed` signal;
1350
+ * underneath the transform transition. A collapsed panel renders `inert` (with
1351
+ * `aria-hidden`), so neither Tab, pointer, nor assistive technology reaches
1352
+ * content that has slid out of view; its `restoreControl` renders outside the
1353
+ * panel and stays reachable.
1354
+ * The app owns the `collapsed` signal;
1173
1355
  * pair it with `wireSidebar` for the toggle, focus, compact-overlay, keyboard,
1174
1356
  * and persistence semantics, and with `CollapsiblePanelToggle` for the standard
1175
1357
  * affordance. See `ui/docs/collapsible-panel.md` and `docs/23-app-layouts.md`.
@@ -1323,6 +1505,9 @@ interface ResizableRegionProps {
1323
1505
  max: number;
1324
1506
  axis?: ResizableRegionAxis;
1325
1507
  edge?: ResizableRegionEdge;
1508
+ /** Snap the track to zero. The region renders `inert` and `aria-hidden`
1509
+ * while collapsed, so neither Tab nor assistive technology reaches it or
1510
+ * lands on an empty landmark; `restoreControl` renders outside the region. */
1326
1511
  collapsed?: boolean;
1327
1512
  transitioning?: boolean;
1328
1513
  /** Whether the separator line is painted. The resize hit target remains available. */
@@ -1335,7 +1520,7 @@ interface ResizableRegionProps {
1335
1520
  presentation?: ResizableRegionPresentation;
1336
1521
  /** Always-available control rendered while collapsed, outside the clipped region. */
1337
1522
  restoreControl?: SafeHtml;
1338
- /** Safe-area-aware viewport corner for `restoreControl`. */
1523
+ /** Safe-area-aware corner of the region's container (not the viewport) for `restoreControl`. */
1339
1524
  restorePosition?: ResizableRegionRestorePosition;
1340
1525
  /** Fill the available inline track and hide the separator below a container breakpoint. */
1341
1526
  responsiveFillAt?: ResizableRegionResponsiveFillAt;
@@ -1364,8 +1549,9 @@ interface WireResizableRegionsOptions {
1364
1549
  onPreview?: (change: ResizeCommit) => void;
1365
1550
  onCommit: (change: ResizeCommit) => void;
1366
1551
  }
1552
+
1367
1553
  /** Wire pointer and separator-keyboard behavior for every ResizableRegion below root. */
1368
- declare function wireResizableRegions(root: HTMLElement, { step, largeStep, onPreview, onCommit, }: WireResizableRegionsOptions): () => void;
1554
+ declare function wireResizableRegions(root: HTMLElement, options: WireResizableRegionsOptions): () => void;
1369
1555
 
1370
1556
  export { type ResizeCommit, type WireResizableRegionsOptions, wireResizableRegions };
1371
1557
  ```
package/ai/skill.md CHANGED
@@ -19,7 +19,12 @@ documented dynamic-data widening. Do not extend those diagnostics to child
19
19
  markup, live DOM relationships, disposer invocation, or other conditions that
20
20
  TypeScript cannot prove from one call.
21
21
  Read `../docs/webawesome-theme.md` when using Web Awesome components or changing
22
- shared theme tokens.
22
+ shared theme tokens. When overriding a loud fill
23
+ (`--wa-color-{tone}-fill-loud`), set the matching `--wa-color-{tone}-on-loud`
24
+ in the same selector and at-rule scope with a foreground that clears 4.5:1;
25
+ the analyzer reports an override with no governing `on-loud` (same or ancestor
26
+ scope, in any stylesheet the entry loads), or a literal pair below 4.5:1, as
27
+ review finding `KUI-L018`.
23
28
 
24
29
  Before choosing or aligning components, look for a consumer-owned catalog that
25
30
  conforms to `./component-catalog-extension.schema.json`. Treat its entries as a
@@ -83,21 +88,21 @@ Choose from the need, not from visual resemblance:
83
88
 
84
89
  Quick routing:
85
90
 
86
- | Need | Choose | Nearest alternatives / boundary |
87
- | ------------------------------------- | ---------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
88
- | Navigation row | `ListItem` | Use an ordinary link or button when sidebar/menu anatomy and state do not apply. Its rounded root clips overflowing descendants; set `multiline` when the primary label should wrap. Multiline leading icons stay aligned with the first text line. |
89
- | Horizontal flex layout | `Row` | Use `hAlign`/`vAlign`, a typed `gap`, and opt-in `wrap`; use `List` when the main axis is vertical. |
90
- | Equal-width columns | `Grid` | Use a positive fixed `columns` count plus typed `gap`/`flex`. The app owns responsive count changes; use CSS grid for asymmetric/intrinsic tracks and `ResizableRegion` for adjustable boundaries. |
91
- | Navigation row with a trailing action | `ListActionRow` | Use `ListItem` when the trailing region is dormant; never put controls inside either component's SafeHtml slots. Multiline leading icons stay aligned with the first text line. |
92
- | Page, panel, or dialog heading | `Toolbar` + `ToolbarText` | Use a direct extra-large `ToolbarText`, optional grouped icon, grouped trailing controls, and app-owned supporting copy below. |
93
- | Exclusive choice | `TabBar`, `SegmentedControl`, or `Select` | Tabs switch tabpanels; segments expose a few choices; Select handles a longer value list. |
94
- | Status or absent content | `StateBanner` or `EmptyState` | `StateBanner` accepts an optional terse, tone-tinted `badge` beside its title for a count or compact status; do not rely on color alone. Web Awesome callouts suit ecosystem content; toasts are transient and never the only record of important state. |
95
- | Compact metadata | `Badge` | Configure status, count, category, or metadata presentation with tone, appearance, shape, and size props. Do not build badges with application CSS or use them as interactive controls. |
96
- | Loading placeholder | a component's `placeholder` prop, or `Skeleton` | Set `placeholder` on a value-bearing component (`Select`, `ListHeader`, `ListItem`, `ValueTableRow`, `SegmentedControl`, `StateBanner`, `AppTab`, `ToolbarText`, `ListActionRow`) to render its real chrome with value slots as subtle **unanimated** skeletons and interactivity disabled; only unknown values change, so a placeholder is never dimmed or styled as disabled (a control whose own `disabled` / `trailingActionDisabled` / choice `disabled` / `actionDisabled` prop is set keeps its live disabled tone via the reserved `data-kui-disabled` marker) — do not hand-rebuild an empty state. Component-drawn chrome stays live too: a `ListItem` or `ListActionRow` placeholder keeps its `busy` spinner while its `status` text (and a `ListItem`'s `trailing` content, always a value) becomes a skeleton. `Skeleton` is the standalone block for a custom slot. A known `AppTab` that is still opening takes `pending` instead: it keeps its name (quiet, and as its accessible name), stays dormant with the same geometry, and shows your `trailing` spinner. Use `LoadingSpinner` for known busy activity, not a skeleton; never animate a skeleton; prefer it over `wa-skeleton`. |
97
- | Adjustable or asymmetric columns | `ResizableRegion` or CSS grid | Use `ResizableRegion` only for a user-operable controlled separator, CSS grid for asymmetric/intrinsic tracks, and `Grid` for static equal-width tracks. |
98
- | Structured or ordinary search | `TokenSearchField` or an input | Use token search only when text and ordered filter chips form one editor. |
99
- | Key/value facts | `ValueTable` with `ValueTableRow` | Use rows with `icon` only when the optional 24px leading visual adds context. |
100
- | Command search | Application-local semantics using shared layout vocabulary | No runtime component or production recipe is exported; the app owns behavior, policy, and copy. |
91
+ | Need | Choose | Nearest alternatives / boundary |
92
+ | ------------------------------------- | ---------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
93
+ | Navigation row | `ListItem` | Use an ordinary link or button when sidebar/menu anatomy and state do not apply. Its rounded root clips overflowing descendants; set `multiline` when the primary label should wrap. Multiline leading icons stay aligned with the first text line. |
94
+ | Horizontal flex layout | `Row` | Use `hAlign`/`vAlign`, a typed `gap`, and opt-in `wrap`; use `List` when the main axis is vertical. |
95
+ | Equal-width columns | `Grid` | Use a positive fixed `columns` count plus typed `gap`/`flex`. The app owns responsive count changes; use CSS grid for asymmetric/intrinsic tracks and `ResizableRegion` for adjustable boundaries. |
96
+ | Navigation row with a trailing action | `ListActionRow` | Use `ListItem` when the trailing region is dormant; never put controls inside either component's SafeHtml slots. Multiline leading icons stay aligned with the first text line. |
97
+ | Page, panel, or dialog heading | `Toolbar` + `ToolbarText` | Use a direct extra-large `ToolbarText`, optional grouped icon, grouped trailing controls, and app-owned supporting copy below. |
98
+ | Exclusive choice | `TabBar`, `SegmentedControl`, or `Select` | Tabs switch tabpanels; segments expose a few choices; Select handles a longer value list. |
99
+ | Status or absent content | `StateBanner` or `EmptyState` | `StateBanner` accepts an optional terse, tone-tinted `badge` beside its title for a count or compact status; do not rely on color alone. Web Awesome callouts suit ecosystem content; toasts are transient and never the only record of important state. |
100
+ | Compact metadata | `Badge` | Configure status, count, category, or metadata presentation with tone, appearance, shape, and size props. Do not build badges with application CSS or use them as interactive controls. |
101
+ | Loading placeholder | a component's `placeholder` prop, or `Skeleton` | Set `placeholder` on a value-bearing component (`Select`, `ListHeader`, `ListItem`, `ValueTableRow`, `SegmentedControl`, `StateBanner`, `AppTab`, `ToolbarText`, `ListActionRow`) to render its real chrome with value slots as subtle **unanimated** skeletons and interactivity disabled; only unknown values change, so a placeholder is never dimmed or styled as disabled (a control whose own `disabled` / `trailingActionDisabled` / choice `disabled` / `actionDisabled` prop is set keeps its live disabled tone via the reserved `data-kui-disabled` marker) — do not hand-rebuild an empty state. Component-drawn chrome stays live too: a `ListItem` or `ListActionRow` placeholder keeps its `busy` spinner while its `status` text (and a `ListItem`'s `trailing` content, always a value) becomes a skeleton. `Skeleton` is the standalone block for a custom slot. A known `AppTab` that is still opening takes `pending` instead: it keeps its name (quiet, and as its accessible name) and the same geometry, shows your `trailing` spinner, and stays selectable (close and drag disabled) so your panel can show placeholders until it loads. Use `LoadingSpinner` for known busy activity, not a skeleton; never animate a skeleton; prefer it over `wa-skeleton`. |
102
+ | Adjustable or asymmetric columns | `ResizableRegion` or CSS grid | Use `ResizableRegion` only for a user-operable controlled separator, CSS grid for asymmetric/intrinsic tracks, and `Grid` for static equal-width tracks. |
103
+ | Structured or ordinary search | `TokenSearchField` or an input | Use token search only when text and ordered filter chips form one editor. |
104
+ | Key/value facts | `ValueTable` with `ValueTableRow` | Use rows with `icon` only when the optional 24px leading visual adds context. |
105
+ | Command search | Application-local semantics using shared layout vocabulary | No runtime component or production recipe is exported; the app owns behavior, policy, and copy. |
101
106
 
102
107
  For a complete shell, sidebar, workspace header, list-detail dialog, composer
103
108
  form, list-state lifecycle, or mixed-control toolbar, start from the runnable
@@ -153,7 +158,7 @@ Hard rules:
153
158
  19. Compose `ValueTable` from typed `ValueTableRow` entries instead of handwritten `dt`/`dd` wrappers. Pass `icon` for the optional 24px leading visual; the row owns 8px of root-scaled top and bottom padding, the 8px iconless or 40px icon-bearing separator start, and the common 8px right inset.
154
159
  20. Use the standard direct `Toolbar` composition for a panel, dialog, or page heading, not a private wrapper or custom heading row. Put an optional icon in a `ToolbarControlGroup`, the title directly in the leading zone as extra-large `ToolbarText`, and actions in a trailing group. Set `headingLevel` for page/section landmarks. Supporting copy stays below the toolbar as app-owned content. The app owns modal behavior, focus, dismissal, command policy, ids, and action handling.
155
160
  21. Space with the official five-step scale, picked by how connected two elements are — not by eye. `0` `--kui-space-none` = no separation (one unit); `4px` `--kui-space-2xs` = very minor air on a connected cluster; `8px` `--kui-space-xs` = standard, between elements within a group; `16px` `--kui-space-m` = minor, between homogeneous groups; `24px` `--kui-space-l` = major, between heterogeneous groups (the `.kui-content` rhythm). The 8px-vs-24px distinction is inside-a-group vs between-major-differing-regions. `--kui-space-s` (12px) and `--kui-space-xl` (32px) are off-scale exceptions. For `Row.gap`, `Grid.gap`, or `List.gap`, pass a finite shorthand such as `gap="xs"` or `gap="m"`; use `Spacer` for one intentional typed empty width/height or a flexible main-axis gap. When one token is insufficient, import typed builders from the CSS-free `@kerfjs/ui/css-values` subpath. Preserve property grammar: lengths for gap/Spacer/Skeleton geometry, `flex()` for Row/Grid/List flex, and a foreground color for choice icons: `uiColor()` with a `*-on-*` or text token (`success-on-quiet`, `text-quiet`) or `foregroundColorVar()`. The bare `success`/`warning`/`danger`/`pop`/`accent` tokens are quiet fill aliases, not foregrounds, and `SelectChoice.color` rejects them. Never cast between brands or emit raw row `style` declarations. `plus` is not standalone and must be wrapped in `calc`; `remify()` remains source-CSS syntax and is never a runtime prop value. See `docs/layout.md` "Spacing scale".
156
- 22. Pick a whole-screen layout from the opt-in, tree-shakeable subpaths by data + interaction + device, and derive responsiveness from `@kerfjs/ui/device-class`'s `deviceClass()` (`compact` = handset or portrait tablet = one pane at a time). Simple/flat → `NavStack` with one entry (single pane), plus `TabScaffold` (`@kerfjs/ui/tab-scaffold`, iOS bottom tabs, each tab its own `NavStack`) for 2–5 co-equal sections on `compact`. Drill-down → `NavStack` (`@kerfjs/ui/nav-stack`), upgrading to `SplitView` (`@kerfjs/ui/split-view`, list-detail) once both panes fit (`atLeast('tablet')` landscape); `SplitView` collapses to a `NavStack` on `compact`. Keep each stack's ordered views controlled; put view-specific bottom chrome on `NavStackView.bottomToolbar` (or use the component-level persistent fallback), then call `wireNavStack` so content slides while the active view's top and bottom chrome cross-fade and focus moves into the new top view/restores on pop. Mark a preferred initial heading or control with `data-nav-focus` when DOM order is not sufficient. Complex tool with peripheral panels → `Workbench` (`@kerfjs/ui/workbench`, collapsible rails + drawer) `atLeast('desktop')`, degrading to `NavStack`/overlays below. These are declarative (the app owns the stack/selection/collapsed/active state as signals) with disposer-returning `wire…` helpers; each ships a companion CSS import and stays out of the barrel. For a standalone collapsible rail or bottom drawer outside a full shell, use `CollapsiblePanel` + `CollapsiblePanelToggle` (`@kerfjs/ui/collapsible-panel`) with `wireSidebar` (`@kerfjs/ui/wire-sidebar`): the standard collapse animation and per-side icon convention (`PanelLeft*`/`PanelRight*`/`PanelBottom*`) plus toggle, focus, compact overlay, and persistence semantics. A compact overlay opens only on a user action: wire-up on a compact device and a wide-to-compact crossing collapse every panel, and a wide crossing restores the inline state. Seed the signal with `signal(device.value.compact)` and pass the panel's `inlineCollapsed` default so the first compact render has no overlay. Dialogs pick the same inner layout, then present per device class (full-screen modal on `compact`, inline on desktop). A top-level percentage-height shell needs a definite containing height: opt into `@kerfjs/ui/document.css` and put `.kui-app-root` on the one direct mount container, or provide equivalent application-owned geometry. The global baseline is deliberately absent from `styles.css` and component imports. See `docs/app-layouts.md`, `docs/document-baseline.md`, and `docs/collapsible-panel.md`.
161
+ 22. Pick a whole-screen layout from the opt-in, tree-shakeable subpaths by data + interaction + device, and derive responsiveness from `@kerfjs/ui/device-class`'s `deviceClass()` (`compact` = handset or portrait tablet = one pane at a time). Simple/flat → `NavStack` with one entry (single pane), plus `TabScaffold` (`@kerfjs/ui/tab-scaffold`, iOS bottom tabs, each tab its own `NavStack`) for 2–5 co-equal sections on `compact`. Drill-down → `NavStack` (`@kerfjs/ui/nav-stack`), upgrading to `SplitView` (`@kerfjs/ui/split-view`, list-detail) once both panes fit (`atLeast('tablet')` landscape); `SplitView` collapses to a `NavStack` on `compact`. Keep each stack's ordered views controlled; put view-specific bottom chrome on `NavStackView.bottomToolbar` (or use the component-level persistent fallback), then call `wireNavStack` so content slides while the active view's top and bottom chrome cross-fade and focus moves into the new top view/restores on pop. Mark a preferred initial heading or control with `data-nav-focus` when DOM order is not sufficient. Complex tool with peripheral panels → `Workbench` (`@kerfjs/ui/workbench`, collapsible rails + drawer) `atLeast('desktop')`, degrading to `NavStack`/overlays below. Workbench panels are fixed-size unless a panel opts in with `resizable` (`true` or `{ min, max }`); then render `size={sizeSignal.value}` and call `wireWorkbench(root, { id, panels: { leftRail: { size: sizeSignal, storageKey? } }, deviceClass })` from `@kerfjs/ui/wire-workbench` for pointer drags, arrow/Shift+arrow/Home/End on the separator, persistence, and no resizing on `compact` classes. Inline rails, fixed or resizable, leave the work area `mainMinSize` px of width (a `Workbench` prop, default 320, `0` off), and an inline bottom drawer leaves it `mainMinHeight` px of height (default 120, `0` off): resizable panels stop growing there, rails shrink in proportion when the Workbench narrows and the drawer when it gets shorter, so do not add your own main-area `min-width` / `min-height`. Give a rail or the bottom drawer `responsiveOverlayAt: "narrow"` (or `"compact"`) to present it as an overlay below that Workbench container breakpoint (704px / 448px, as `ResizableRegion`'s `responsiveFillAt`) instead of deriving `presentation` from `deviceClass`; overlays never resize. Pass each such panel's `collapsed` signal to `wireWorkbench` too (`panels: { leftRail: { collapsed: navCollapsed } }`, `size` optional) so the overlay is transient like `wireSidebar`'s compact overlay: it collapses when the breakpoint begins to apply (the inline state returns when it ends), takes focus on its first control and keeps Tab inside it while open, and closes on Escape or an outside press with focus returned to its toggle (give each toggle `aria-controls` naming its panel's derived id, `<workbench id>-left-rail` / `-right-rail` / `-bottom-drawer`, so a panel open at wire-up has a focus target too); opening one overlay closes the other open overlays, so they never cover each other's controls; do not hand-roll those listeners (`dismissOverlays: false` opts out of all of it, `exclusiveOverlays: false` of exclusivity only). Collapse stays the app's `collapsed` flag and keeps the size, so an expanded panel returns at its last size. A collapsed panel renders `inert` and `aria-hidden` by itself, on the rail/drawer and its content (its `restoreControl` stays outside and reachable), so do not add your own `tabindex`, `aria-hidden`, or `inert` to hide it. These are declarative (the app owns the stack/selection/collapsed/active state as signals) with disposer-returning `wire…` helpers; each ships a companion CSS import and stays out of the barrel. For a standalone collapsible rail or bottom drawer outside a full shell, use `CollapsiblePanel` + `CollapsiblePanelToggle` (`@kerfjs/ui/collapsible-panel`) with `wireSidebar` (`@kerfjs/ui/wire-sidebar`): the standard collapse animation and per-side icon convention (`PanelLeft*`/`PanelRight*`/`PanelBottom*`) plus toggle, focus, compact overlay, and persistence semantics. A collapsed `CollapsiblePanel`, `ResizableRegion`, or Workbench panel renders `inert` and `aria-hidden` by itself, so do not hide collapsed content yourself either. A compact overlay opens only on a user action: wire-up on a compact device and a wide-to-compact crossing collapse every panel, and a wide crossing restores the inline state. Seed the signal with `signal(device.value.compact)` and pass the panel's `inlineCollapsed` default so the first compact render has no overlay. Dialogs pick the same inner layout, then present per device class (full-screen modal on `compact`, inline on desktop). A top-level percentage-height shell needs a definite containing height: opt into `@kerfjs/ui/document.css` and put `.kui-app-root` on the one direct mount container, or provide equivalent application-owned geometry. The global baseline is deliberately absent from `styles.css` and component imports. See `docs/app-layouts.md`, `docs/document-baseline.md`, and `docs/collapsible-panel.md`.
157
162
  23. Layouts and `Pane` are safe-area aware without configuration: backgrounds, separators, and dividers paint through a device's unsafe areas (a collapsible rail or drawer grows by the inset it docks to), content is padded only on the sides a region actually reaches, and block insets are scroll padding inside the scroller so first/last content can always be scrolled clear. Interior edges get nothing, and a collapsed rail hands its edge back to the center. Never add your own `env(safe-area-inset-*)` padding around a kerf layout or pane — that double-insets. In an app-owned arrangement of panes, pass `Pane.safeAreaEdges` (the sides that pane reaches; `[]` opts out); give an app bar or bottom bar that sits at a screen edge outside a Pane header or footer `Toolbar.safeAreaEdges` for the sides it reaches (for example `['block-start', 'inline-start', 'inline-end']`); or set `--kui-edge-inset-*: 0px` on a region for each edge it does not reach. The device insets are `--kui-safe-area-*` (defaulting to `env()`), overridable on `:root`. See `docs/app-layouts.md` "Safe areas".
158
163
 
159
164
  Common mistakes:
@@ -180,6 +185,7 @@ Common mistakes:
180
185
  | Invent an `@kerfjs/ui` command-palette export | Keep the semantics application-local, reuse public layout vocabulary, and propose recurring behavior upstream |
181
186
  | Handwrite `ValueTable` row wrappers or compensate their padding or separators | Compose `ValueTableRow`; its root-scaled block padding, optional icon hook, and separator geometry are public contract |
182
187
  | Rebuild a panel/dialog heading with bespoke CSS or a private wrapper | Compose a plain `Toolbar` with a direct xl `ToolbarText`, optional grouped icon, grouped trailing controls, and app-owned supporting copy below |
188
+ | Put an xl `ToolbarText` in a narrow rail or drawer header | Use the default size there as pane identity; xl truncates at the minimum size ("Ins…" in a 160px rail) |
183
189
  | Force a `width`/`height`/`padding` on a component to size or space it | Let it size to its content and tokens; a forced box leaves a halo or a stretched oval — adjust an icon-size or spacing token, not the box |
184
190
  | Wrap a component or region in a card, border, backdrop, or outline to "contain" it | Let it sit on the surface; add a `.kui-content-item` only for a real distinction — hierarchy comes from alignment, spacing, and type first |
185
191
  | Add another container's padding on top of a content-item's own margin | Pick one owner of the inset; a pane has no padding and its `.kui-content` children own the 8/1/8 geometry — stacking them double-insets |
@@ -1,6 +1,6 @@
1
1
  # Web Awesome JSX signatures for the UI authoring corpus
2
2
 
3
- Generated from the emitted `@kerfjs/ui@5.0.0-beta.53` declaration boundary. Import `@kerfjs/ui/webawesome` for type effects when authoring direct `wa-*` JSX. The module emits no runtime behavior and does not register custom elements.
3
+ Generated from the emitted `@kerfjs/ui@5.0.0-beta.54` declaration boundary. Import `@kerfjs/ui/webawesome` for type effects when authoring direct `wa-*` JSX. The module emits no runtime behavior and does not register custom elements.
4
4
 
5
5
  ```ts
6
6
  import { KerfCustomElement } from 'kerfjs/jsx-runtime';
@@ -14,6 +14,7 @@ import ts from 'typescript';
14
14
 
15
15
  import { loadApplicationUiProfile } from '../ai/application-ui-profile.mjs';
16
16
  import { isUiTraversalExcluded } from '../traversal-exclusions.mjs';
17
+ import { collectLoudDeclarations, inspectLoudPairs } from './loud-pairs.mjs';
17
18
 
18
19
  export const UI_ANALYSIS_SCHEMA_VERSION = 1;
19
20
 
@@ -41,6 +42,10 @@ export const UI_ANALYSIS_RULES = Object.freeze({
41
42
  'KUI-L015': { severity: 'error', title: 'Non-standalone CSS expression' },
42
43
  'KUI-L016': { severity: 'error', title: 'Forbidden declaration-list escape' },
43
44
  'KUI-L017': { severity: 'review', title: 'Exceptional spacing shorthand' },
45
+ 'KUI-L018': {
46
+ severity: 'review',
47
+ title: 'Loud fill override without its on-loud pair',
48
+ },
44
49
  });
45
50
 
46
51
  const adoptionRules = new Set([
@@ -344,6 +349,7 @@ async function inspectCss(
344
349
  diagnostics,
345
350
  cssFacts,
346
351
  adoption = false,
352
+ siblingOnLoud = [],
347
353
  ) {
348
354
  let root;
349
355
  try {
@@ -358,6 +364,15 @@ async function inspectCss(
358
364
  );
359
365
  return;
360
366
  }
367
+ inspectLoudPairs(
368
+ file,
369
+ root,
370
+ (decl, message, evidence) =>
371
+ diagnostics.push(
372
+ diagnostic('KUI-L018', location(file, decl), message, evidence),
373
+ ),
374
+ siblingOnLoud,
375
+ );
361
376
  root.walkRules((rule) => {
362
377
  const classes = [
363
378
  ...rule.selector.matchAll(/\.([_a-zA-Z]+[_a-zA-Z0-9-]*)/g),
@@ -962,6 +977,38 @@ export async function analyzeUiProject({
962
977
  const styleConsumers = new Map(
963
978
  [...styleFacts.keys()].map((file) => [file, new Set()]),
964
979
  );
980
+ // KUI-L018 accepts an on-loud from another stylesheet the same entry loads:
981
+ // every stylesheet reachable from one file (a source module's CSS imports,
982
+ // or a stylesheet with its @imports) is loaded together.
983
+ const onLoudByStyle = new Map();
984
+ for (const style of styleFacts.keys()) {
985
+ let parsed;
986
+ try {
987
+ parsed = postcss.parse(contents.get(style), { from: style });
988
+ } catch {
989
+ continue;
990
+ }
991
+ onLoudByStyle.set(
992
+ style,
993
+ collectLoudDeclarations(style, parsed).filter(
994
+ ({ kind }) => kind === 'on-loud',
995
+ ),
996
+ );
997
+ }
998
+ const coLoadedStyles = new Map(
999
+ [...styleFacts.keys()].map((style) => [style, new Set()]),
1000
+ );
1001
+ for (const file of files) {
1002
+ const loaded = new Set(reachableStyleFiles(file, imports, styleFacts));
1003
+ if (styleFacts.has(file)) loaded.add(file);
1004
+ for (const style of loaded)
1005
+ for (const sibling of loaded)
1006
+ if (sibling !== style) coLoadedStyles.get(style).add(sibling);
1007
+ }
1008
+ const siblingOnLoud = (style) =>
1009
+ [...coLoadedStyles.get(style)]
1010
+ .sort()
1011
+ .flatMap((sibling) => onLoudByStyle.get(sibling) ?? []);
965
1012
  for (const file of files.filter((item) =>
966
1013
  sourceExtensions.has(extname(item)),
967
1014
  )) {
@@ -991,6 +1038,7 @@ export async function analyzeUiProject({
991
1038
  fileDiagnostics,
992
1039
  new Map(),
993
1040
  adoption,
1041
+ siblingOnLoud(file),
994
1042
  );
995
1043
  recordDiagnostics(fileDiagnostics, context);
996
1044
  }
@@ -0,0 +1,49 @@
1
+ import type { Declaration, Root } from 'postcss';
2
+
3
+ export type Rgb = [number, number, number];
4
+
5
+ export interface LoudScope {
6
+ key: string;
7
+ atScope: string;
8
+ bare: string;
9
+ selector: string;
10
+ }
11
+
12
+ export interface LoudDeclaration {
13
+ decl: Declaration;
14
+ file: string;
15
+ tone: string;
16
+ kind: 'fill-loud' | 'on-loud';
17
+ value: string;
18
+ scope: LoudScope;
19
+ }
20
+
21
+ export declare const LOUD_PAIR_MIN_CONTRAST: number;
22
+ export declare const DEFAULT_ON_LOUD: Readonly<Record<string, string>>;
23
+
24
+ export declare function literalSchemeColors(
25
+ value: string,
26
+ ): { light: Rgb; dark: Rgb } | undefined;
27
+
28
+ export declare function contrastRatio(first: Rgb, second: Rgb): number;
29
+
30
+ export declare function loudPairContrast(
31
+ fillValue: string,
32
+ onLoudValue: string,
33
+ ): { scheme: 'light' | 'dark'; ratio: number } | undefined;
34
+
35
+ export declare function collectLoudDeclarations(
36
+ file: string,
37
+ root: Root,
38
+ ): LoudDeclaration[];
39
+
40
+ export declare function inspectLoudPairs(
41
+ file: string,
42
+ root: Root,
43
+ report: (
44
+ decl: Declaration,
45
+ message: string,
46
+ evidence: Record<string, unknown>,
47
+ ) => void,
48
+ siblingOnLoud?: readonly LoudDeclaration[],
49
+ ): void;