@kerfjs/ui 5.0.0-beta.65 → 5.0.0-beta.67

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 (66) hide show
  1. package/README.md +1 -1
  2. package/ai/component-catalog.json +26 -10
  3. package/ai/component-composition-rules.json +1 -1
  4. package/ai/component-composition.json +26 -5
  5. package/ai/components/grid.md +3 -1
  6. package/ai/components/nav-stack.md +4 -3
  7. package/ai/components/text.md +8 -6
  8. package/ai/components/toolbar-control-group.md +1 -1
  9. package/ai/components/workbench.md +5 -3
  10. package/ai/public-api-signatures-v1.md +64 -37
  11. package/ai/skill.md +6 -0
  12. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  13. package/analyzer/component-ownership.d.mts +1 -1
  14. package/analyzer/component-ownership.mjs +24 -5
  15. package/analyzer/index.mjs +3 -1
  16. package/dist/browser/catalog.js +2 -1
  17. package/dist/browser/workbench.js +4 -1
  18. package/dist/catalog-resources.d.ts +2 -0
  19. package/dist/catalog.d.ts +2 -0
  20. package/dist/catalog.js +4 -3
  21. package/dist/catalog.js.map +1 -1
  22. package/dist/{chunk-5IJ6UGNZ.js → chunk-3BCJGM36.js} +2 -2
  23. package/dist/{chunk-5IJ6UGNZ.js.map → chunk-3BCJGM36.js.map} +1 -1
  24. package/dist/{chunk-2EKHENQK.js → chunk-B2ZMR7NY.js} +9 -1
  25. package/dist/chunk-B2ZMR7NY.js.map +1 -0
  26. package/dist/{chunk-FJSZYEBM.js → chunk-C74LITXE.js} +4 -4
  27. package/dist/{chunk-FJSZYEBM.js.map → chunk-C74LITXE.js.map} +1 -1
  28. package/dist/{chunk-PWQAVNON.js → chunk-TGM424PI.js} +34 -4
  29. package/dist/chunk-TGM424PI.js.map +1 -0
  30. package/dist/chunk-VY6YRCTV.js +123 -0
  31. package/dist/chunk-VY6YRCTV.js.map +1 -0
  32. package/dist/{chunk-CQHN45XR.js → chunk-YDGNU2OD.js} +11 -2
  33. package/dist/chunk-YDGNU2OD.js.map +1 -0
  34. package/dist/collapsible-panel.js +2 -2
  35. package/dist/grid.d.ts +5 -1
  36. package/dist/grid.js +1 -1
  37. package/dist/index.d.ts +1 -1
  38. package/dist/index.js +3 -3
  39. package/dist/list-header.js +1 -1
  40. package/dist/nav-stack.d.ts +5 -1
  41. package/dist/nav-stack.js +1 -1
  42. package/dist/split-view.js +1 -1
  43. package/dist/styles/nav-stack.css +4 -0
  44. package/dist/styles/text.css +18 -0
  45. package/dist/styles/token-search-field.css +9 -2
  46. package/dist/styles/toolbar.css +5 -1
  47. package/dist/text.d.ts +8 -1
  48. package/dist/text.js +1 -1
  49. package/dist/wire-content-items.js +32 -4
  50. package/dist/wire-content-items.js.map +1 -1
  51. package/dist/workbench.d.ts +41 -33
  52. package/dist/workbench.js +4 -1
  53. package/docs/accessibility.md +9 -2
  54. package/docs/component-selection.md +17 -0
  55. package/docs/layout.md +30 -1
  56. package/docs/nav-stack.md +7 -0
  57. package/docs/ui-analyzer.md +5 -1
  58. package/docs/webawesome-theme.md +6 -0
  59. package/docs/workbench.md +27 -3
  60. package/llms.txt +7 -0
  61. package/package.json +1 -1
  62. package/dist/chunk-2EKHENQK.js.map +0 -1
  63. package/dist/chunk-CQHN45XR.js.map +0 -1
  64. package/dist/chunk-HXCMWANP.js +0 -116
  65. package/dist/chunk-HXCMWANP.js.map +0 -1
  66. package/dist/chunk-PWQAVNON.js.map +0 -1
@@ -1,6 +1,26 @@
1
1
  // src/wire-content-items.ts
2
2
  var ITEM_SELECTOR = '[data-component="content-item"][data-interactive="true"]';
3
3
  var NESTED_CONTROL_SELECTOR = 'button, a[href], input, select, textarea, [contenteditable="true"], [role="button"]';
4
+ var MULTI_GRID_SELECTOR = '[role="grid"][aria-multiselectable="true"]';
5
+ function nextTile(item, rows, key) {
6
+ const current = item.getBoundingClientRect();
7
+ const centerX = current.left + current.width / 2;
8
+ const candidates = rows.filter((row) => row !== item).map((row) => {
9
+ const rect = row.getBoundingClientRect();
10
+ return { row, x: rect.left + rect.width / 2, y: rect.top };
11
+ });
12
+ const sameLine = (y) => Math.abs(y - current.top) < 1;
13
+ const directed = candidates.filter(
14
+ ({ x, y }) => key === "ArrowLeft" ? sameLine(y) && x < centerX : key === "ArrowRight" ? sameLine(y) && x > centerX : key === "ArrowUp" ? y < current.top - 1 : y > current.top + 1
15
+ );
16
+ directed.sort((a, b) => {
17
+ if (key === "ArrowLeft") return b.x - a.x;
18
+ if (key === "ArrowRight") return a.x - b.x;
19
+ const vertical = Math.abs(a.y - current.top) - Math.abs(b.y - current.top);
20
+ return vertical || Math.abs(a.x - centerX) - Math.abs(b.x - centerX);
21
+ });
22
+ return directed[0]?.row;
23
+ }
4
24
  function actionItem(root, event) {
5
25
  const target = event.target;
6
26
  if (!(target instanceof Element)) return null;
@@ -28,17 +48,25 @@ function wireContentItems(root) {
28
48
  return;
29
49
  const item = actionItem(root, event);
30
50
  if (!item) return;
31
- if (item.dataset.selectionMode === "multiple" && ["ArrowDown", "ArrowUp", "Home", "End"].includes(event.key)) {
32
- const grid = item.closest('[role="grid"][aria-multiselectable="true"]');
51
+ if (item.dataset.selectionMode === "multiple" && [
52
+ "ArrowDown",
53
+ "ArrowUp",
54
+ "ArrowLeft",
55
+ "ArrowRight",
56
+ "Home",
57
+ "End"
58
+ ].includes(event.key)) {
59
+ const grid = item.closest(MULTI_GRID_SELECTOR);
33
60
  if (!grid) return;
34
61
  const rows = Array.from(
35
62
  grid.querySelectorAll(
36
63
  `${ITEM_SELECTOR}[data-selection-mode="multiple"]:not([data-disabled="true"])`
37
64
  )
38
- );
65
+ ).filter((row) => row.closest(MULTI_GRID_SELECTOR) === grid);
39
66
  const index = rows.indexOf(item);
40
67
  if (index < 0) return;
41
- const next = event.key === "Home" ? rows[0] : event.key === "End" ? rows[rows.length - 1] : rows[index + (event.key === "ArrowDown" ? 1 : -1)];
68
+ const isTileGrid = grid.getAttribute("data-component") === "grid";
69
+ const next = event.key === "Home" ? rows[0] : event.key === "End" ? rows[rows.length - 1] : isTileGrid ? nextTile(item, rows, event.key) : event.key === "ArrowDown" || event.key === "ArrowUp" ? rows[index + (event.key === "ArrowDown" ? 1 : -1)] : void 0;
42
70
  if (next) {
43
71
  event.preventDefault();
44
72
  next.focus();
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/wire-content-items.ts"],"names":[],"mappings":"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","file":"wire-content-items.js","sourcesContent":["const ITEM_SELECTOR =\n '[data-component=\"content-item\"][data-interactive=\"true\"]';\nconst NESTED_CONTROL_SELECTOR =\n 'button, a[href], input, select, textarea, [contenteditable=\"true\"], [role=\"button\"]';\n\nfunction actionItem(root: HTMLElement, event: Event) {\n const target = event.target;\n if (!(target instanceof Element)) return null;\n const item = target.closest<HTMLElement>(ITEM_SELECTOR);\n if (!item || !root.contains(item) || item.dataset.disabled === 'true')\n return null;\n // Composed events expose native controls inside a custom element's shadow\n // root even though event.target is retargeted to its host here.\n for (const node of event.composedPath()) {\n if (node === item) break;\n if (node instanceof Element && node.matches(NESTED_CONTROL_SELECTOR))\n return null;\n }\n return item;\n}\n\n/** True when a card action originated on the card rather than a nested control. */\nexport function isContentItemActivation(\n event: Event,\n item: HTMLElement,\n): boolean {\n return actionItem(item, event) === item;\n}\n\n/**\n * Give interactive ContentItems native-like Enter/Space activation with one\n * delegated listener pair. Space activates on release and is cancelled if\n * focus moves; the item's own `data-action` still goes through the app's\n * ordinary delegated click handler. Returns a disposer.\n */\nexport function wireContentItems(root: HTMLElement): () => void {\n let pendingSpace: HTMLElement | null = null;\n\n const clearSpace = () => {\n pendingSpace?.removeAttribute('data-kui-pressed');\n pendingSpace = null;\n };\n\n const keydown = (event: KeyboardEvent) => {\n if (\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey\n )\n return;\n const item = actionItem(root, event);\n if (!item) return;\n if (\n item.dataset.selectionMode === 'multiple' &&\n ['ArrowDown', 'ArrowUp', 'Home', 'End'].includes(event.key)\n ) {\n const grid = item.closest('[role=\"grid\"][aria-multiselectable=\"true\"]');\n if (!grid) return;\n const rows = Array.from(\n grid.querySelectorAll<HTMLElement>(\n `${ITEM_SELECTOR}[data-selection-mode=\"multiple\"]:not([data-disabled=\"true\"])`,\n ),\n );\n const index = rows.indexOf(item);\n if (index < 0) return;\n const next =\n event.key === 'Home'\n ? rows[0]\n : event.key === 'End'\n ? rows[rows.length - 1]\n : rows[index + (event.key === 'ArrowDown' ? 1 : -1)];\n if (next) {\n event.preventDefault();\n next.focus();\n }\n return;\n }\n if (event.key === 'Enter') {\n event.preventDefault();\n item.click();\n return;\n }\n if (event.key === ' ' || event.key === 'Spacebar') {\n event.preventDefault();\n if (event.repeat) return;\n clearSpace();\n pendingSpace = item;\n item.setAttribute('data-kui-pressed', 'true');\n }\n };\n\n const keyup = (event: KeyboardEvent) => {\n if (event.key !== ' ' && event.key !== 'Spacebar') return;\n const item = pendingSpace;\n if (!item) return;\n event.preventDefault();\n clearSpace();\n if (\n item === actionItem(root, event) &&\n item.ownerDocument.activeElement === item\n )\n item.click();\n };\n\n const focusout = (event: FocusEvent) => {\n if (event.target === pendingSpace) clearSpace();\n };\n\n root.addEventListener('keydown', keydown);\n root.addEventListener('keyup', keyup);\n root.addEventListener('focusout', focusout);\n return () => {\n clearSpace();\n root.removeEventListener('keydown', keydown);\n root.removeEventListener('keyup', keyup);\n root.removeEventListener('focusout', focusout);\n };\n}\n"]}
1
+ {"version":3,"sources":["../src/wire-content-items.ts"],"names":[],"mappings":"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","file":"wire-content-items.js","sourcesContent":["const ITEM_SELECTOR =\n '[data-component=\"content-item\"][data-interactive=\"true\"]';\nconst NESTED_CONTROL_SELECTOR =\n 'button, a[href], input, select, textarea, [contenteditable=\"true\"], [role=\"button\"]';\nconst MULTI_GRID_SELECTOR = '[role=\"grid\"][aria-multiselectable=\"true\"]';\n\nfunction nextTile(\n item: HTMLElement,\n rows: HTMLElement[],\n key: string,\n): HTMLElement | undefined {\n const current = item.getBoundingClientRect();\n const centerX = current.left + current.width / 2;\n const candidates = rows\n .filter((row) => row !== item)\n .map((row) => {\n const rect = row.getBoundingClientRect();\n return { row, x: rect.left + rect.width / 2, y: rect.top };\n });\n const sameLine = (y: number) => Math.abs(y - current.top) < 1;\n const directed = candidates.filter(({ x, y }) =>\n key === 'ArrowLeft'\n ? sameLine(y) && x < centerX\n : key === 'ArrowRight'\n ? sameLine(y) && x > centerX\n : key === 'ArrowUp'\n ? y < current.top - 1\n : y > current.top + 1,\n );\n directed.sort((a, b) => {\n if (key === 'ArrowLeft') return b.x - a.x;\n if (key === 'ArrowRight') return a.x - b.x;\n const vertical = Math.abs(a.y - current.top) - Math.abs(b.y - current.top);\n return vertical || Math.abs(a.x - centerX) - Math.abs(b.x - centerX);\n });\n return directed[0]?.row;\n}\n\nfunction actionItem(root: HTMLElement, event: Event) {\n const target = event.target;\n if (!(target instanceof Element)) return null;\n const item = target.closest<HTMLElement>(ITEM_SELECTOR);\n if (!item || !root.contains(item) || item.dataset.disabled === 'true')\n return null;\n // Composed events expose native controls inside a custom element's shadow\n // root even though event.target is retargeted to its host here.\n for (const node of event.composedPath()) {\n if (node === item) break;\n if (node instanceof Element && node.matches(NESTED_CONTROL_SELECTOR))\n return null;\n }\n return item;\n}\n\n/** True when a card action originated on the card rather than a nested control. */\nexport function isContentItemActivation(\n event: Event,\n item: HTMLElement,\n): boolean {\n return actionItem(item, event) === item;\n}\n\n/**\n * Give interactive ContentItems native-like Enter/Space activation with one\n * delegated listener pair. Space activates on release and is cancelled if\n * focus moves; the item's own `data-action` still goes through the app's\n * ordinary delegated click handler. Returns a disposer.\n */\nexport function wireContentItems(root: HTMLElement): () => void {\n let pendingSpace: HTMLElement | null = null;\n\n const clearSpace = () => {\n pendingSpace?.removeAttribute('data-kui-pressed');\n pendingSpace = null;\n };\n\n const keydown = (event: KeyboardEvent) => {\n if (\n event.defaultPrevented ||\n event.altKey ||\n event.ctrlKey ||\n event.metaKey\n )\n return;\n const item = actionItem(root, event);\n if (!item) return;\n if (\n item.dataset.selectionMode === 'multiple' &&\n [\n 'ArrowDown',\n 'ArrowUp',\n 'ArrowLeft',\n 'ArrowRight',\n 'Home',\n 'End',\n ].includes(event.key)\n ) {\n const grid = item.closest(MULTI_GRID_SELECTOR);\n if (!grid) return;\n const rows = Array.from(\n grid.querySelectorAll<HTMLElement>(\n `${ITEM_SELECTOR}[data-selection-mode=\"multiple\"]:not([data-disabled=\"true\"])`,\n ),\n ).filter((row) => row.closest(MULTI_GRID_SELECTOR) === grid);\n const index = rows.indexOf(item);\n if (index < 0) return;\n const isTileGrid = grid.getAttribute('data-component') === 'grid';\n const next =\n event.key === 'Home'\n ? rows[0]\n : event.key === 'End'\n ? rows[rows.length - 1]\n : isTileGrid\n ? nextTile(item, rows, event.key)\n : event.key === 'ArrowDown' || event.key === 'ArrowUp'\n ? rows[index + (event.key === 'ArrowDown' ? 1 : -1)]\n : undefined;\n if (next) {\n event.preventDefault();\n next.focus();\n }\n return;\n }\n if (event.key === 'Enter') {\n event.preventDefault();\n item.click();\n return;\n }\n if (event.key === ' ' || event.key === 'Spacebar') {\n event.preventDefault();\n if (event.repeat) return;\n clearSpace();\n pendingSpace = item;\n item.setAttribute('data-kui-pressed', 'true');\n }\n };\n\n const keyup = (event: KeyboardEvent) => {\n if (event.key !== ' ' && event.key !== 'Spacebar') return;\n const item = pendingSpace;\n if (!item) return;\n event.preventDefault();\n clearSpace();\n if (\n item === actionItem(root, event) &&\n item.ownerDocument.activeElement === item\n )\n item.click();\n };\n\n const focusout = (event: FocusEvent) => {\n if (event.target === pendingSpace) clearSpace();\n };\n\n root.addEventListener('keydown', keydown);\n root.addEventListener('keyup', keyup);\n root.addEventListener('focusout', focusout);\n return () => {\n clearSpace();\n root.removeEventListener('keydown', keydown);\n root.removeEventListener('keyup', keyup);\n root.removeEventListener('focusout', focusout);\n };\n}\n"]}
@@ -1,13 +1,15 @@
1
1
  import { SafeHtml } from 'kerfjs';
2
2
  import { ListConfig } from './list.js';
3
+ import { NavStackProps } from './nav-stack.js';
3
4
  import { PaneConfig } from './pane.js';
4
5
  import { ResizableRegionSeparator, ResizableRegionCollapseMotion, ResizableRegionContentOverflow, ResizableRegionPresentation, ResizableRegionRestorePosition } from './resizable-region.js';
5
6
  import { K as KerfUiContent } from './semantic-content-BbzjvSu9.js';
6
- import { a as PanelToolbar, b as PanelChromePlacement, c as PanelBottomToolbar, d as PanelToggle } from './panel-toolbar-DDKh4uKd.js';
7
7
  import { ToolbarConfig } from './toolbar.js';
8
+ import { a as PanelToolbar, b as PanelChromePlacement, c as PanelBottomToolbar, d as PanelToggle } from './panel-toolbar-DDKh4uKd.js';
8
9
  import './css-values.js';
9
10
  import './flex-alignment-4ms8ZbV8.js';
10
11
  import './sides-BPSWde0A.js';
12
+ import './toolbar-text.js';
11
13
  import './lucide-icon.js';
12
14
  import 'lucide';
13
15
 
@@ -80,37 +82,8 @@ interface WorkbenchPanelResizable {
80
82
  /** Largest size (default 480). */
81
83
  max?: number;
82
84
  }
83
- /** A collapsible Workbench panel — a side rail or the bottom drawer. */
84
- interface WorkbenchPanel {
85
- content: KerfUiContent;
86
- /**
87
- * The panel's top toolbar, composed by the Workbench: its marked groups
88
- * and standard `toggle` move to the work area's toolbar while the panel is
89
- * collapsed. With it, `content` renders in a `Pane` below the toolbar.
90
- */
91
- toolbar?: WorkbenchPanelToolbar;
92
- /** Fixed content below a toolbar panel's top toolbar, above its scroll region. */
93
- header?: KerfUiContent;
94
- /** List layout for `header`; omitted fields keep List defaults. */
95
- headerList?: ListConfig;
96
- /** Placement of the top toolbar and header together; default `fixed`. */
97
- headerPlacement?: WorkbenchChromePlacement;
98
- /** Fixed content above a toolbar panel's bottom toolbar. */
99
- footer?: KerfUiContent;
100
- /** List layout for `footer`; omitted fields keep List defaults. */
101
- footerList?: ListConfig;
102
- /** Fixed toolbar below `footer`, using footer semantics. */
103
- bottomToolbar?: WorkbenchPanelBottomToolbar;
104
- /** Placement of the footer and bottom toolbar together; default `fixed`. */
105
- footerPlacement?: WorkbenchChromePlacement;
106
- /**
107
- * Configuration for a `toolbar` panel's `Pane` (`contentElement`,
108
- * `contentLabel`, `separators`, `safeAreaEdges`, `chromeDividers`) — for example
109
- * `{ contentElement: 'nav', contentLabel: 'Sections' }` for a navigation
110
- * rail. Omitted or `undefined` fields keep the `Pane` defaults. Ignored
111
- * without a `toolbar`, where `content` renders as given.
112
- */
113
- pane?: PaneConfig;
85
+ /** Region sizing, collapse, and presentation shared by either panel body. */
86
+ interface WorkbenchPanelBase {
114
87
  /** Whether the panel is currently collapsed (the app owns this). */
115
88
  collapsed?: boolean;
116
89
  /**
@@ -157,6 +130,41 @@ interface WorkbenchPanel {
157
130
  restoreControl?: SafeHtml;
158
131
  restorePosition?: ResizableRegionRestorePosition;
159
132
  }
133
+ /** The original static panel body: one toolbar, optional fixed chrome, and one Pane. */
134
+ interface WorkbenchStaticPanel extends WorkbenchPanelBase {
135
+ content: KerfUiContent;
136
+ /** The composed toolbar; marked groups and its toggle relocate on collapse. */
137
+ toolbar?: WorkbenchPanelToolbar;
138
+ header?: KerfUiContent;
139
+ headerList?: ListConfig;
140
+ headerPlacement?: WorkbenchChromePlacement;
141
+ footer?: KerfUiContent;
142
+ footerList?: ListConfig;
143
+ bottomToolbar?: WorkbenchPanelBottomToolbar;
144
+ footerPlacement?: WorkbenchChromePlacement;
145
+ /** Configuration for the static toolbar panel's Pane. */
146
+ pane?: PaneConfig;
147
+ navStack?: never;
148
+ }
149
+ /** A panel whose active NavStack view supplies the toolbar, fixed header, and scroll body. */
150
+ interface WorkbenchNavigationPanel extends WorkbenchPanelBase {
151
+ navStack: Omit<NavStackProps, 'panelToggle' | 'hideToolbar' | 'className' | 'slot'>;
152
+ /** Supplies the standard toggle; active view groups precede it and may relocate on collapse. */
153
+ toolbar: Omit<WorkbenchPanelToolbar, 'title' | 'leading' | 'center' | 'trailing'> & {
154
+ toggle: NonNullable<WorkbenchPanelToolbar['toggle']>;
155
+ };
156
+ content?: never;
157
+ header?: never;
158
+ headerList?: never;
159
+ headerPlacement?: never;
160
+ footer?: never;
161
+ footerList?: never;
162
+ bottomToolbar?: never;
163
+ footerPlacement?: never;
164
+ pane?: never;
165
+ }
166
+ /** A collapsible Workbench side rail or bottom drawer. */
167
+ type WorkbenchPanel = WorkbenchStaticPanel | WorkbenchNavigationPanel;
160
168
  interface WorkbenchProps {
161
169
  /**
162
170
  * The Workbench's `id`. Each panel's `id` derives from it —
@@ -257,4 +265,4 @@ interface WorkbenchProps {
257
265
  */
258
266
  declare function Workbench({ id, label, main, leftRail, rightRail, bottomDrawer, mainToolbar, mainHeader, mainFooter, mainBottomToolbar, mainHeaderPlacement, mainFooterPlacement, mainPane, mainHeaderList, mainFooterList, mainMinSize, mainMinHeight, overlayBackdrop, className, slot, }: WorkbenchProps): SafeHtml;
259
267
 
260
- export { Workbench, type WorkbenchChromePlacement, type WorkbenchCompactOverlay, type WorkbenchMainBottomToolbar, type WorkbenchMainToolbar, type WorkbenchPanel, type WorkbenchPanelBottomToolbar, type WorkbenchPanelResizable, type WorkbenchPanelToggle, type WorkbenchPanelToolbar, type WorkbenchProps, type WorkbenchResponsiveOverlayAt };
268
+ export { Workbench, type WorkbenchChromePlacement, type WorkbenchCompactOverlay, type WorkbenchMainBottomToolbar, type WorkbenchMainToolbar, type WorkbenchNavigationPanel, type WorkbenchPanel, type WorkbenchPanelBottomToolbar, type WorkbenchPanelResizable, type WorkbenchPanelToggle, type WorkbenchPanelToolbar, type WorkbenchProps, type WorkbenchResponsiveOverlayAt, type WorkbenchStaticPanel };
package/dist/workbench.js CHANGED
@@ -1,4 +1,7 @@
1
- export { Workbench } from './chunk-PWQAVNON.js';
1
+ export { Workbench } from './chunk-TGM424PI.js';
2
+
3
+
4
+
2
5
 
3
6
 
4
7
 
@@ -15,7 +15,7 @@ These are normative contracts for package components and consuming applications.
15
15
  - `LoadingSpinner` is either labeled (`role="img"`) or hidden. Its rotation stops for reduced motion.
16
16
  - A busy `ToolbarControlGroup` makes its controls inert and exposes `aria-busy`; its `busyLabel` is announced by an adjacent status node outside the inert group. Clear `busy` when the operation finishes so the controls rejoin the focus order.
17
17
  - A bottom `Toolbar` uses `position="footer"` for footer semantics; top toolbars keep the default `header`. Workbench's main bottom toolbar sets this automatically.
18
- - Interactive `ContentItem` cards are keyboard reachable. Set `selectionMode="toggle"` for a pressed button, `selectionMode="single"` for an option inside a listbox, or `selectionMode="multiple"` for a selectable row with a gridcell that can contain native controls. Place multiple-mode rows inside a labeled `List selectionMode="multiple"` grid; the app owns the selection set and range gestures. Wire `wireContentItems` once so Enter and Space on the card activate its delegated action without activating it from a nested control. Disabled cards expose `aria-disabled` and leave the Tab order.
18
+ - Interactive `ContentItem` cards are keyboard reachable. Set `selectionMode="toggle"` for a pressed button, `selectionMode="single"` for an option inside a listbox, or `selectionMode="multiple"` for a selectable row with a gridcell that can contain native controls. Place multiple-mode rows inside a labeled `List` or `Grid` with `selectionMode="multiple"`; the app owns the selection set and range gestures. Wire `wireContentItems` once so Enter and Space on the card activate its delegated action without activating it from a nested control. Disabled cards expose `aria-disabled` and leave the Tab order.
19
19
  - An unlabeled `Skeleton` (including the value slots a component's `placeholder` prop renders) is decorative loading chrome: it is `aria-hidden`, conveys no information a user needs to act on, and is deliberately exempt from the WCAG 3:1 non-text-contrast target. Inside a solid `Badge` it stays subtle (about 2.1–4.4:1 in light, 2.4–2.8:1 in dark, depending on tone); `tests/browser/tone-contrast.spec.ts` holds those values as a regression floor rather than an accessibility threshold. Announce loading through a labeled `Skeleton`, `LoadingSpinner`, or `aria-busy` on the owning region, never through the skeleton's visibility.
20
20
  - `StateBanner` defaults to polite `role="status"`; use `urgency="alert"` only for an attention-requiring failure. Its optional `badge` is persistent inline status/count content beside the title and is announced as part of the banner; keep it terse and do not rely on color alone for its meaning.
21
21
  - A removable `Chip` has a native button with a specific `removeLabel`, such as "Remove Urgent tag". The button is absent for a plain chip and disabled for a disabled chip. Its leading `icon` is decorative; the visible label names the tag. `truncate` retains the full plain-text label as a native tooltip. Tag text remains visible because tone does not convey meaning alone.
@@ -23,10 +23,15 @@ These are normative contracts for package components and consuming applications.
23
23
  - `Select` follows the Web Awesome host's standard `input`/`change` events. Application tests verify the live `value`, focus, and events—not attributes alone. Its decorative option icons and value-dependent selected content remain present after controlled rerenders, so the visible choice does not silently lose its non-text cue.
24
24
  - `DisclosureArrow` is an 18px root-scaled decorative visual by default, not an interaction target. Its owning native control supplies a stable accessible name, pointer and keyboard interaction, and `aria-expanded`; multiple arrows keep independently controlled state. Consumers may resize it with the typed `size` prop without changing that ownership. Configured directions animate over the shortest path; a 180-degree closed-to-open tie uses counterclockwise rotation.
25
25
  - `List` is layout-only by default and adds no `list` role. With `selectionMode="multiple"` and a required `ariaLabel`, it becomes a multi-select grid for `ContentItem` rows. Give a scrollable List a bounded block size, avoid nested scroll owners, and keep focused children visible while scrolling.
26
+ - `Grid` is layout-only by default. With `selectionMode="multiple"` and a required `ariaLabel`, it becomes a multi-select grid for `ContentItem` tiles; each tile remains a row with a gridcell, including when the CSS grid wraps.
26
27
 
27
28
  In a multi-select ContentItem grid, `wireContentItems` moves focus between
28
29
  enabled rows on Up/Down and to the first/last enabled row on Home/End. Native
29
- controls inside a row retain those keys; the application changes selection
30
+ controls inside a row retain those keys. In a `Grid`, Left/Right moves between
31
+ tiles on the same rendered line and Up/Down moves to the nearest tile in the
32
+ adjacent rendered line; Home/End reaches the first/last enabled tile. The
33
+ navigation reads live geometry, so responsive column changes need no rewiring.
34
+ The application changes selection
30
35
  on card activation and handles its own Shift/Cmd range gestures.
31
36
  With delegated click actions, use `isContentItemActivation(event, card)` in the
32
37
  card's selection handler so a nested menu trigger or other control does not
@@ -413,6 +418,8 @@ searchbox references the label and hint with `aria-labelledby` and
413
418
  validates the value before submission. `wireTokenSearchFields()` also moves
414
419
  focus to the editor when the visible label is clicked. The field stands outside
415
420
  a Toolbar and fills the same inline space as a neighboring `wa-input`.
421
+ Its control radius and required-marker color and offset follow Web Awesome's
422
+ form-control tokens, so the two controls read as one form.
416
423
 
417
424
  `TokenSearchField` exposes the editable surface as a named `searchbox`. Each
418
425
  chip is atomic (`contenteditable="false"`) and contains separately named edit
@@ -132,6 +132,12 @@ an upstream component or recipe request.
132
132
 
133
133
  ## Problem-to-component matrix
134
134
 
135
+ For a read-only or click-to-edit field preview, use `FieldLabel` from
136
+ `@kerfjs/ui/text` above the preview instead of a `ListHeader`. It matches the
137
+ themed Web Awesome form-control label. Put it in `ListInsetControl sides="trl"`
138
+ beside a `ContentItem` value, and connect its id to the preview group's
139
+ `aria-labelledby`; see [Field labels](./layout.md#text).
140
+
135
141
  | Interface need | Use when | Do not use when; nearest alternative | Required wiring | Application owns | Import | Recipe |
136
142
  | ----------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
137
143
  | Icon — `LucideIcon` | A decorative or explicitly labeled Lucide-compatible icon belongs in app UI. | Do not use an icon as the only name of an unfamiliar action; add visible or accessible text. Prefer it over Web Awesome `wa-icon`. | None. | Icon choice and meaningful label. | `@kerfjs/ui/lucide-icon` | [Accessibility](./accessibility.md#shared-rules) |
@@ -334,6 +340,9 @@ In a form, use `TokenSearchField presentation="form-field"` with its `label`,
334
340
  optional `hint`, and `required` state. It renders a full-width control beside
335
341
  `wa-input` without a Toolbar or extra inline inset. The app validates required
336
342
  search content before saving.
343
+ The form-field corners and required marker use the same Web Awesome
344
+ form-control radius, marker color, and offset tokens as a neighboring
345
+ `wa-input`.
337
346
  `TokenSearchRule.suggest(input, state)` receives the unfinished value and current committed tokens, so a rule can omit values already selected. Existing one-argument suggestion callbacks continue to work.
338
347
  For a sibling date picker or other app helper, call `model.commit(value)` while a `name:` prefix is active. It parses and commits that value for the active rule even when it is absent from suggestions, replacing the unfinished prefix text. Invalid values and calls without an active prefix leave the query unchanged; `choose(value)` remains restricted to suggested tokens.
339
348
  To apply a saved search or restore a query programmatically, call `model.replace({ query, tokens })`. It publishes parsed state and rebuilds the DOM-owned editor text even when no chip changed. For a field without a model, change the field's `revision` prop when setting `query` yourself.
@@ -348,6 +357,10 @@ their compact widths when collapsed.
348
357
 
349
358
  ## Toolbar composition
350
359
 
360
+ A `FloatingToolbar` accepts `ToolbarControlGroup` directly as its children in
361
+ both floating and inline placement. Name the toolbar with `label`; group its
362
+ controls as you would in a `Toolbar` zone.
363
+
351
364
  A `Toolbar` has three zones — `leading`, `center`, and `trailing`. In almost
352
365
  every case the only things that go **directly** in a zone are `ToolbarText`
353
366
  (identity/title text) and `ToolbarControlGroup` (any control or cluster of
@@ -509,6 +522,10 @@ For a multi-select card containing its own controls, place
509
522
  `selectionMode="multiple"` ContentItems inside a labeled
510
523
  `List selectionMode="multiple"`; the app supplies each card's `selected`
511
524
  state and handles range gestures.
525
+ For wrapped document tiles, use `Grid minColumnWidth={px(160)}` with
526
+ `selectionMode="multiple"` and `ariaLabel`. The same ContentItem row/gridcell
527
+ contract applies, while `wireContentItems` follows the rendered tile layout
528
+ with all four arrow keys.
512
529
 
513
530
  Incorrect: duplicating component-like rows and compensating for nested padding
514
531
  forks the package anatomy and spacing contract.
package/docs/layout.md CHANGED
@@ -289,6 +289,9 @@ item no taller than the band is centered in it by its own size, and a taller
289
289
  item (a wrapped title, a second trailing row) starts at the band's top and
290
290
  grows down, so it never moves the other controls. A wrapped `ToolbarText`
291
291
  centers its first line in the band.
292
+ The leading zone clips a shrinking title but reserves 4px of paint overflow
293
+ for the focus outline of a control at its edge. Focus rings in all three zones
294
+ remain visible at narrow and wide widths.
292
295
  Trailing icon actions share one axis: a toolbar control centers its glyph in
293
296
  that 44px slot at the 8px inline margin, and a `ListHeader` action centers its
294
297
  fitted 36px square in a slot of the same size (a transparent hit layer extends
@@ -328,6 +331,27 @@ CSS strings.
328
331
 
329
332
  ## Text
330
333
 
334
+ `FieldLabel` from `@kerfjs/ui/text` gives a read-only or click-to-edit field
335
+ preview the same visible heading as a themed Web Awesome form control. Put it
336
+ in `ListInsetControl sides="trl"` above the preview so its text starts at the
337
+ same 17px content column as a `ContentItem` value. Unlike a native `<label>`, it
338
+ is a plain element: give it an `id` and name the preview's group with
339
+ `aria-labelledby`. Keep the app's preview/edit switching and hint copy beside
340
+ the preview; use the live control's own `label` when editing.
341
+
342
+ ```tsx
343
+ import { ContentItem } from "@kerfjs/ui/content-item";
344
+ import { ListInsetControl } from "@kerfjs/ui/list-inset-control";
345
+ import { FieldLabel } from "@kerfjs/ui/text";
346
+
347
+ <div role="group" aria-labelledby="needed-by-label">
348
+ <ListInsetControl sides="trl">
349
+ <FieldLabel id="needed-by-label">Needed by</FieldLabel>
350
+ </ListInsetControl>
351
+ <ContentItem>October 16, 2026</ContentItem>
352
+ </div>;
353
+ ```
354
+
331
355
  Use `Text` from `@kerfjs/ui/text` for ordinary semantic headings, paragraphs,
332
356
  and inline secondary text. It renders a native `p` by default; set `variant` to
333
357
  `h1`–`h6` when the document outline calls for a heading, or `span` for inline
@@ -448,7 +472,12 @@ their shared track width rather than expanding across an otherwise empty row.
448
472
  It requires `minColumnWidth`; the default `auto-fit` keeps the existing form
449
473
  behavior. When Grid is a direct child of a flex-owned `ListInsetControl`, pass
450
474
  `flex` so the Grid takes the available row width instead of shrink-wrapping.
451
- Grid is layout-only and adds no ARIA grid role. The application owns child
475
+ Grid is layout-only by default and adds no ARIA grid role. With
476
+ `selectionMode="multiple"` and a required `ariaLabel`, it supplies a labeled
477
+ multi-select grid for `ContentItem selectionMode="multiple"` tiles. Their
478
+ row/gridcell structure can contain independent controls, while
479
+ `wireContentItems` follows the rendered tile positions with arrow keys.
480
+ The application owns child
452
481
  semantics.
453
482
  Use application-owned CSS grid for intrinsic, asymmetric, spanning, or masonry
454
483
  tracks, and use `ResizableRegion` when people must adjust a boundary.
package/docs/nav-stack.md CHANGED
@@ -57,6 +57,9 @@ mounted (so their DOM state survives) but hidden. Each entry carries a
57
57
  `leading`, `center`, and trailing `toolbar` groups for the top toolbar, and an
58
58
  optional per-view `bottomToolbar`. The component-level
59
59
  `bottomToolbar` remains a persistent fallback for views that do not provide one.
60
+ Each view can also provide `header`, pinned below its top toolbar and above
61
+ that view's single scroll area. A fixed heading, notice, or section selector
62
+ can therefore change with the active screen without adding another scroller.
60
63
  The back control appears automatically once the stack has more than one entry;
61
64
  `wireNavStack`'s `onBack` is where the app pops its own signal.
62
65
 
@@ -103,6 +106,10 @@ Configure the toolbar with `toolbarConfig` instead of styling it:
103
106
  text beside it (for example, the previous view's title); visible text names
104
107
  the control, so `backLabel` applies only to the icon-only control.
105
108
 
109
+ `panelToggle` supplies a persistent final group in the trailing zone of every
110
+ view's top toolbar. `Workbench` uses it for a navigation panel's standard
111
+ collapse toggle; the active view still owns the preceding toolbar groups.
112
+
106
113
  The catalog's **Configured toolbar** example shows these together: a level-2
107
114
  heading title over a bottom divider, leading and center groups on the root
108
115
  view, a trailing group on the pushed view, and `backText` set to the previous
@@ -125,7 +125,11 @@ which element a selector styles.
125
125
  arguments) names a public class of a cataloged component (`.kui-toolbar`,
126
126
  `.kui-list-item__label`, `.acme-meter`), its `[data-component="…"]` root, or
127
127
  a Web Awesome tag the package themes (`wa-button`, `wa-select`), and the rule
128
- sets at least one ordinary property. A `::part()` subject is left to
128
+ sets at least one ordinary property. It also catches a raw descendant of
129
+ cataloged component anatomy, such as `.kui-app-tab__select > svg`, even when
130
+ the component class is not the final compound. A component root used only as
131
+ context (`.kui-toolbar > .my-widget`) and app-named descendants remain
132
+ available for app-owned content. A `::part()` subject is left to
129
133
  `KUI-L011`; a rule that only sets custom properties is configuration, judged
130
134
  by the token rules. Forcing a component's dimensions is one such restyle
131
135
  and reports here once: `.kui-pane { width: 300px }`.
@@ -364,6 +364,12 @@ smallest subtree that needs a different identity:
364
364
  }
365
365
  ```
366
366
 
367
+ `TokenSearchField` with `presentation="form-field"` uses
368
+ `--wa-form-control-border-radius` for its control corners and
369
+ `--wa-form-control-required-content-color` and
370
+ `--wa-form-control-required-content-offset` for its required marker. These
371
+ tokens keep it aligned with a neighboring `wa-input` when a form theme changes.
372
+
367
373
  The shipped theme pairs every `--wa-color-{variant}-fill-loud` with an
368
374
  `--wa-color-{variant}-on-loud` that clears WCAG AA (4.5:1) in both color
369
375
  schemes. When you override a loud fill, set its `on-loud` alongside it, in the
package/docs/workbench.md CHANGED
@@ -14,8 +14,8 @@ import { Workbench } from "@kerfjs/ui/workbench";
14
14
  In a browser bundler that honors the `browser` export condition (Vite, esbuild, and
15
15
  webpack do by default), the import above also
16
16
  loads Workbench's stylesheet and those of the components it renders internally
17
- (`Pane`, `Toolbar`, `ToolbarControlGroup`, `FloatingToolbar`, `ResizableRegion`,
18
- `List`, `LucideIcon`). Without that condition, import the manual stylesheets instead:
17
+ (`Pane`, `NavStack`, `Toolbar`, `ToolbarControlGroup`, `FloatingToolbar`,
18
+ `ResizableRegion`, `List`, `LucideIcon`). Without that condition, import the manual stylesheets instead:
19
19
  `@kerfjs/ui/workbench.css` plus those components' CSS, or `@kerfjs/ui/styles.css`.
20
20
 
21
21
  A top-level `Workbench` needs a definite containing height. Import the opt-in
@@ -71,7 +71,7 @@ const navCollapsed = signal(false);
71
71
  />;
72
72
  ```
73
73
 
74
- Each `WorkbenchPanel` takes `content`, an optional `toolbar` and `footer` (see
74
+ Each static `WorkbenchPanel` takes `content`, an optional `toolbar` and `footer` (see
75
75
  [Panel toolbars](#panel-toolbars)), an optional `collapsed`, an optional
76
76
  `size` (rail width or drawer height in px, overriding the CSS default —
77
77
  `--kui-workbench-rail-width` 280px, `--kui-workbench-drawer-height` 220px), and
@@ -165,6 +165,30 @@ into `.kui-resizable-region__content`.
165
165
 
166
166
  ## Panel toolbars
167
167
 
168
+ A panel with `navStack` hosts a controlled `NavStack` in place of the static
169
+ `content` and `Pane` slots. Give it a `toolbar` with a `toggle`; each
170
+ `navStack.views` entry then supplies its own title, leading/center/trailing
171
+ groups, fixed `header`, and scrolling `content`. The active view's toolbar is
172
+ the panel's only toolbar row, with the standard toggle always last. On
173
+ collapse, marked groups from the active view and the toggle relocate to the
174
+ work-area toolbar; the view stack stays mounted. The app owns the view array
175
+ and calls `wireNavStack` for animated push/pop and focus movement. See
176
+ [Navigation stack](nav-stack.md) and the catalog's navigation ticket rail.
177
+
178
+ ```tsx
179
+ <Workbench
180
+ id="tickets"
181
+ label="Tickets workspace"
182
+ main={<Workspace />}
183
+ rightRail={{
184
+ label: "Tickets",
185
+ collapsed: ticketsCollapsed.value,
186
+ toolbar: { toggle: { action: "toggle-tickets", name: "tickets" } },
187
+ navStack: { id: "ticket-stack", label: "Tickets", views: ticketViews.value },
188
+ }}
189
+ />
190
+ ```
191
+
168
192
  A panel's controls follow it open and closed when the Workbench composes the
169
193
  toolbars. Give a panel a `toolbar` and the work area a `mainToolbar` (and, for
170
194
  a drawer, optionally a `mainBottomToolbar`); the Workbench renders each as a
package/llms.txt CHANGED
@@ -157,6 +157,10 @@ headingLevel`, `TokenSearchField clearIcon`, `ContentItem frame="framed"` for a
157
157
  `ToolbarText size="xsmall"` suits a dense rail heading. The
158
158
  `TokenSearchField` editor shrinks in narrow groups; its minimum width can be
159
159
  set with `--kui-token-search-editor-min-width`.
160
+ - **Label read-only field previews.** Use `FieldLabel` from `@kerfjs/ui/text`
161
+ in `ListInsetControl sides="trl"` above a `ContentItem` value. Connect its
162
+ id to the preview group's `aria-labelledby`; use the live control's own
163
+ `label` while editing.
160
164
  - **Use Toolbar zone options.** `responsive="trailing-priority"` moves an
161
165
  expanded trailing control to a full second row at `responsiveAt`; the leading
162
166
  title remains above it. Public leading-min-width, trailing-gap/justify, and
@@ -203,6 +207,9 @@ headingLevel`, `TokenSearchField clearIcon`, `ContentItem frame="framed"` for a
203
207
  The app owns the outer width and child semantics; use custom CSS grid for
204
208
  asymmetric/intrinsic tracks and `ResizableRegion` for adjustable boundaries.
205
209
  `Grid.gap` and `Grid.flex` use the same typed grammars as Row.
210
+ For selectable document tiles, use `selectionMode="multiple"` and a required
211
+ `ariaLabel`, with `ContentItem selectionMode="multiple"` children. Wire
212
+ `wireContentItems` for spatial arrow navigation and Enter/Space activation.
206
213
  - **Give a layout root `fill`, not a frame `Pane`.** A Row, List, or Grid that
207
214
  is the sole layout root of a definite-height parent takes its full height
208
215
  with `fill` and carries safe `data-*` markers through `rootAttributes`.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kerfjs/ui",
3
- "version": "5.0.0-beta.65",
3
+ "version": "5.0.0-beta.67",
4
4
  "description": "Accessible, composable UI primitives for kerf applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/grid.tsx"],"names":[],"mappings":";;;;AAUA,IAAM,uBAAA,uBAA8B,GAAA,CAAI;AAAA,EACtC,gBAAA;AAAA,EACA,cAAA;AAAA,EACA,uBAAA;AAAA,EACA,gBAAA;AAAA,EACA,WAAA;AAAA,EACA;AACF,CAAC,CAAA;AAaD,IAAM,UAAA,GAAqC;AAAA,EACzC,MAAA;AAAA,EACA,KAAA;AAAA,EACA,IAAA;AAAA,EACA,GAAA;AAAA,EACA,GAAA;AAAA,EACA,GAAA;AAAA,EACA;AACF,CAAA;AAuCO,SAAS,IAAA,CAAK;AAAA,EACnB,QAAA;AAAA,EACA,OAAA;AAAA,EACA,cAAA;AAAA,EACA,QAAA,GAAW,KAAA;AAAA,EACX,GAAA,GAAM,IAAA;AAAA,EACN,IAAA,GAAO,KAAA;AAAA,EACP,IAAA,GAAO,KAAA;AAAA,EACP,SAAA,GAAY,EAAA;AAAA,EACZ,iBAAiB,EAAC;AAAA,EAClB;AACF,CAAA,EAAc;AACZ,EAAA,IAAI,cAAA,KAAmB,MAAA,IAAa,OAAA,KAAY,MAAA,EAAW;AACzD,IAAA,MAAM,IAAI,UAAA;AAAA,MACR;AAAA,KACF;AAAA,EACF;AACA,EAAA,IAAI,QAAA,IAAY,mBAAmB,MAAA,EAAW;AAC5C,IAAA,MAAM,IAAI,WAAW,uCAAuC,CAAA;AAAA,EAC9D;AACA,EAAA,IACE,cAAA,KAAmB,MAAA,KAClB,OAAA,KAAY,MAAA,IAAa,CAAC,OAAO,aAAA,CAAc,OAAO,CAAA,IAAK,OAAA,GAAU,CAAA,CAAA,EACtE;AACA,IAAA,MAAM,IAAI,WAAW,8CAA8C,CAAA;AAAA,EACrE;AAEA,EAAA,MAAM,kBAAA,GAAqB,oBAAA;AAAA,IACzB,cAAA;AAAA,IACA;AAAA,GACF;AACA,EAAA,MAAM,WAAW,UAAA,CAAW,QAAA,CAAS,GAAkB,CAAA,GACnD,KAAA,CAAM,GAAkB,CAAA,GACxB,GAAA;AACJ,EAAA,MAAM,SAAA,GAAY,IAAA,KAAS,IAAA,GAAO,UAAA,GAAa,IAAA,IAAQ,MAAA;AACvD,EAAA,MAAM,KAAA,GAAQ;AAAA,IACZ,OAAA,KAAY,MAAA,GAAY,EAAA,GAAK,CAAA,oBAAA,EAAuB,OAAO,CAAA,CAAA;AAAA,IAC3D,cAAA,KAAmB,MAAA,GACf,EAAA,GACA,CAAA,6BAAA,EAAgC,cAAc,CAAA,CAAA;AAAA,IAClD,mBAAmB,QAAQ,CAAA,CAAA;AAAA,IAC3B,SAAA,GAAY,CAAA,iBAAA,EAAoB,SAAS,CAAA,CAAA,GAAK;AAAA,GAChD,CACG,MAAA,CAAO,OAAO,CAAA,CACd,KAAK,GAAG,CAAA;AAEX,EAAA,uBACE,GAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACE,GAAG,kBAAA;AAAA,MACJ,KAAA,EAAO,CAAA,SAAA,EAAY,SAAS,CAAA,CAAA,CAAG,IAAA,EAAK;AAAA,MACpC,gBAAA,EAAe,MAAA;AAAA,MACf,cAAA,EAAc,OAAA,KAAY,MAAA,GAAY,MAAA,GAAY,OAAO,OAAO,CAAA;AAAA,MAChE,uBAAA,EAAuB,cAAA,KAAmB,MAAA,GAAY,MAAA,GAAY,MAAA;AAAA,MAClE,gBAAA,EAAgB,WAAW,MAAA,GAAS,MAAA;AAAA,MACpC,WAAA,EAAW,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAC,CAAA;AAAA,MAC/B,WAAA,EAAW,OAAO,MAAA,GAAS,MAAA;AAAA,MAC3B,KAAA;AAAA,MACA,IAAA;AAAA,MAEC;AAAA;AAAA,GACH;AAEJ","file":"chunk-2EKHENQK.js","sourcesContent":["import {\n type CssFlex,\n type CssFlexKeyword,\n type CssLength,\n space,\n type UiSpaceName,\n} from './css-values.js';\nimport { filterDataAttributes } from './extension-attributes.js';\nimport type { KerfUiContent } from './semantic-content.js';\n\nconst gridProtectedAttributes = new Set([\n 'data-component',\n 'data-columns',\n 'data-min-column-width',\n 'data-auto-fill',\n 'data-flex',\n 'data-fill',\n]);\n\ntype GridRootAttributes = Readonly<\n Record<`data-${string}`, string | undefined> & {\n 'data-component'?: never;\n 'data-columns'?: never;\n 'data-min-column-width'?: never;\n 'data-auto-fill'?: never;\n 'data-flex'?: never;\n 'data-fill'?: never;\n }\n>;\n\nconst spaceNames: readonly UiSpaceName[] = [\n 'none',\n '2xs',\n 'xs',\n 's',\n 'm',\n 'l',\n 'xl',\n];\n\ninterface GridCommonProps {\n children?: KerfUiContent;\n /** A named UI spacing token or typed CSS length. Defaults to xs. */\n gap?: UiSpaceName | CssLength;\n /** Allow this grid to grow/shrink, use a keyword, or supply a typed CSS flex shorthand. */\n flex?: boolean | CssFlexKeyword | CssFlex;\n /**\n * Fill the height of a parent with a definite height, such as an app root or\n * a fixed-height frame, when this grid is that parent's layout root. Inside a\n * flex layout use `flex` instead. Defaults to false.\n */\n fill?: boolean;\n className?: string;\n /** Safe `data-*` metadata; Grid-owned structural attributes remain protected. */\n rootAttributes?: GridRootAttributes;\n /** Native named-slot assignment when composed inside a web component. */\n slot?: string;\n}\n\nexport type GridProps = GridCommonProps &\n (\n | {\n /** Number of equal-width columns. Must be a positive safe integer. */\n columns: number;\n minColumnWidth?: never;\n autoFill?: never;\n }\n | {\n columns?: never;\n /** Fit equal columns of at least this width; collapse as the container narrows. */\n minColumnWidth: CssLength;\n /** Keep empty tracks instead of stretching a sparse row. Defaults to false. */\n autoFill?: boolean;\n }\n );\n\n/** Render equal tracks with a fixed count or a responsive minimum width. */\nexport function Grid({\n children,\n columns,\n minColumnWidth,\n autoFill = false,\n gap = 'xs',\n flex = false,\n fill = false,\n className = '',\n rootAttributes = {},\n slot,\n}: GridProps) {\n if (minColumnWidth !== undefined && columns !== undefined) {\n throw new RangeError(\n 'Grid columns and minColumnWidth are mutually exclusive',\n );\n }\n if (autoFill && minColumnWidth === undefined) {\n throw new RangeError('Grid autoFill requires minColumnWidth');\n }\n if (\n minColumnWidth === undefined &&\n (columns === undefined || !Number.isSafeInteger(columns) || columns < 1)\n ) {\n throw new RangeError('Grid columns must be a positive safe integer');\n }\n\n const safeRootAttributes = filterDataAttributes(\n rootAttributes,\n gridProtectedAttributes,\n );\n const gapValue = spaceNames.includes(gap as UiSpaceName)\n ? space(gap as UiSpaceName)\n : gap;\n const flexValue = flex === true ? '1 1 auto' : flex || undefined;\n const style = [\n columns === undefined ? '' : `--_kui-grid-columns:${columns}`,\n minColumnWidth === undefined\n ? ''\n : `--_kui-grid-min-column-width:${minColumnWidth}`,\n `--_kui-grid-gap:${gapValue}`,\n flexValue ? `--_kui-grid-flex:${flexValue}` : '',\n ]\n .filter(Boolean)\n .join(';');\n\n return (\n <div\n {...safeRootAttributes}\n class={`kui-grid ${className}`.trim()}\n data-component=\"grid\"\n data-columns={columns === undefined ? undefined : String(columns)}\n data-min-column-width={minColumnWidth === undefined ? undefined : 'true'}\n data-auto-fill={autoFill ? 'true' : undefined}\n data-flex={String(Boolean(flex))}\n data-fill={fill ? 'true' : undefined}\n style={style}\n slot={slot}\n >\n {children}\n </div>\n );\n}\n\nexport type {\n CssFlex,\n CssFlexKeyword,\n CssLength,\n UiSpaceName,\n} from './css-values.js';\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/text.tsx"],"names":[],"mappings":";;;AA2DO,SAAS,IAAA,CAAK;AAAA,EACnB,SAAS,OAAA,GAAU,GAAA;AAAA,EACnB,IAAA,GAAO,SAAA;AAAA,EACP,IAAA,GAAO,SAAA;AAAA,EACP,IAAA,GAAO,SAAA;AAAA,EACP,MAAA,GAAS,aAAA;AAAA,EACT,KAAA,GAAQ,KAAA;AAAA,EACR,UAAA,GAAa,SAAA;AAAA,EACb,IAAA,GAAO,QAAA;AAAA,EACP,QAAA;AAAA,EACA,QAAA;AAAA,EACA,OAAO,UAAA,GAAa,EAAA;AAAA,EACpB,SAAA,GAAY,EAAA;AAAA,EACZ,GAAG;AACL,CAAA,EAAc;AACZ,EAAA,IACE,QAAA,KAAa,MAAA,KACZ,CAAC,MAAA,CAAO,aAAA,CAAc,QAAQ,CAAA,IAC7B,QAAA,GAAW,CAAA,IACX,IAAA,KAAS,QAAA,IACT,IAAA,KAAS,UAAA,CAAA;AAEX,IAAA,MAAM,IAAI,UAAA;AAAA,MACR;AAAA,KACF;AACF,EAAA,MAAM,OAAA,GAAU,CAAC,UAAA,EAAY,UAAA,EAAY,SAAS,EAAE,MAAA,CAAO,OAAO,CAAA,CAAE,IAAA,CAAK,GAAG,CAAA;AAC5E,EAAA,uBACE,GAAA;AAAA,IAAC,OAAA;AAAA,IAAA;AAAA,MACE,GAAG,UAAA;AAAA,MACJ,KAAA,EAAO,OAAA;AAAA,MACP,gBAAA,EAAe,MAAA;AAAA,MACf,WAAA,EAAW,IAAA;AAAA,MACX,WAAA,EAAW,IAAA;AAAA,MACX,WAAA,EAAW,IAAA;AAAA,MACX,aAAA,EAAa,MAAA;AAAA,MACb,YAAA,EAAY,QAAQ,MAAA,GAAS,MAAA;AAAA,MAC7B,kBAAA,EAAkB,UAAA,KAAe,OAAA,GAAU,OAAA,GAAU,MAAA;AAAA,MACrD,WAAA,EAAW,IAAA,KAAS,QAAA,GAAW,MAAA,GAAY,IAAA;AAAA,MAC3C,gBAAA,EAAgB,QAAA,KAAa,MAAA,GAAY,MAAA,GAAY,OAAO,QAAQ,CAAA;AAAA,MAEnE,QAAA,EAAA,QAAA,KAAa,SACZ,QAAA,mBAEA,GAAA;AAAA,QAAC,MAAA;AAAA,QAAA;AAAA,UACC,KAAA,EAAM,iBAAA;AAAA,UACN,KAAA,EAAO,yBAAyB,QAAQ,CAAA,CAAA;AAAA,UAEvC;AAAA;AAAA;AACH;AAAA,GAEJ;AAEJ","file":"chunk-CQHN45XR.js","sourcesContent":["import type { KerfBaseAttrs } from 'kerfjs/jsx-runtime';\n\nimport type { KerfUiContent } from './semantic-content.js';\n\nexport type TextVariant =\n 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'p' | 'span';\nexport type TextTone = 'default' | 'quiet' | 'danger';\nexport type TextSize = 'compact' | 'default' | 'large' | 'xlarge';\nexport type TextFont = 'default' | 'monospace';\nexport type TextBorder = 'transparent' | 'none';\nexport type TextLineHeight = 'default' | 'tight';\nexport type TextWrap = 'normal' | 'anywhere' | 'nowrap' | 'truncate';\n\nexport type TextContent =\n KerfUiContent | string | number | readonly TextContent[];\n\ntype TextCommonProps = Omit<\n KerfBaseAttrs,\n 'children' | 'class' | 'className'\n> & {\n /** Native heading, paragraph, or inline span element to render. Defaults to `p`. */\n variant?: TextVariant;\n /** Semantic foreground treatment. Defaults to the inherited foreground. */\n tone?: TextTone;\n /** Text sizing independent of the native semantic element. */\n size?: TextSize;\n /** Font family independent of the native semantic element. */\n font?: TextFont;\n /** Transparent alignment border or no border when embedded in owner chrome. */\n border?: TextBorder;\n /** Remove the block variant's item padding and border in compact content. */\n flush?: boolean;\n /** Use compact leading for short dialog or metadata copy. */\n lineHeight?: TextLineHeight;\n children: TextContent;\n class?: string;\n className?: string;\n};\n\nexport type TextProps = TextCommonProps &\n (\n | {\n /** Wrap normally or break long unbroken strings. Defaults to normal. */\n wrap?: 'normal' | 'anywhere';\n /** Cap wrapped text to this positive number of lines. */\n maxLines?: number;\n }\n | {\n /** Keep one line, with or without an ellipsis. */\n wrap: 'nowrap' | 'truncate';\n maxLines?: never;\n }\n );\n\n/**\n * Semantic heading, paragraph, or inline text. Block variants use the standard\n * content-item padding; `span` adds no box geometry.\n * All ordinary native heading/paragraph attributes pass through to the element.\n */\nexport function Text({\n variant: Variant = 'p',\n tone = 'default',\n size = 'default',\n font = 'default',\n border = 'transparent',\n flush = false,\n lineHeight = 'default',\n wrap = 'normal',\n maxLines,\n children,\n class: classValue = '',\n className = '',\n ...attributes\n}: TextProps) {\n if (\n maxLines !== undefined &&\n (!Number.isSafeInteger(maxLines) ||\n maxLines < 1 ||\n wrap === 'nowrap' ||\n wrap === 'truncate')\n )\n throw new RangeError(\n 'Text maxLines requires a positive integer and wrapping',\n );\n const classes = ['kui-text', classValue, className].filter(Boolean).join(' ');\n return (\n <Variant\n {...attributes}\n class={classes}\n data-component=\"text\"\n data-tone={tone}\n data-size={size}\n data-font={font}\n data-border={border}\n data-flush={flush ? 'true' : undefined}\n data-line-height={lineHeight === 'tight' ? 'tight' : undefined}\n data-wrap={wrap === 'normal' ? undefined : wrap}\n data-max-lines={maxLines === undefined ? undefined : String(maxLines)}\n >\n {maxLines === undefined ? (\n children\n ) : (\n <span\n class=\"kui-text__clamp\"\n style={`--_kui-text-max-lines:${maxLines}`}\n >\n {children}\n </span>\n )}\n </Variant>\n );\n}\n"]}