@kerfjs/ui 5.0.0-beta.4 → 5.0.0-beta.6

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 (95) hide show
  1. package/README.md +8 -8
  2. package/ai/component-catalog.json +303 -167
  3. package/ai/public-api-signatures-v1.md +333 -26
  4. package/ai/skill.md +14 -7
  5. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  6. package/dist/browser/{dialog-header.js → panel-header.js} +3 -2
  7. package/dist/{chunk-GY5WH7TO.js → chunk-24Z2XE6C.js} +3 -3
  8. package/dist/chunk-24Z2XE6C.js.map +1 -0
  9. package/dist/chunk-2RJBFNB6.js +172 -0
  10. package/dist/chunk-2RJBFNB6.js.map +1 -0
  11. package/dist/chunk-KZKSBUKC.js +19 -0
  12. package/dist/chunk-KZKSBUKC.js.map +1 -0
  13. package/dist/chunk-RBTVBGRD.js +23 -0
  14. package/dist/chunk-RBTVBGRD.js.map +1 -0
  15. package/dist/device-class.d.ts +62 -0
  16. package/dist/device-class.js +75 -0
  17. package/dist/device-class.js.map +1 -0
  18. package/dist/index.d.ts +2 -3
  19. package/dist/index.js +8 -9
  20. package/dist/nav-stack.d.ts +38 -0
  21. package/dist/nav-stack.js +4 -0
  22. package/dist/nav-stack.js.map +1 -0
  23. package/dist/panel-header.d.ts +26 -0
  24. package/dist/panel-header.js +6 -0
  25. package/dist/panel-header.js.map +1 -0
  26. package/dist/split-view.d.ts +42 -0
  27. package/dist/split-view.js +20 -0
  28. package/dist/split-view.js.map +1 -0
  29. package/dist/styles/foundation.css +21 -0
  30. package/dist/styles/menu-header.css +19 -2
  31. package/dist/styles/nav-stack.css +112 -0
  32. package/dist/styles/panel-header.css +59 -0
  33. package/dist/styles/split-view.css +35 -0
  34. package/dist/styles/styles.css +1 -2
  35. package/dist/styles/tab-scaffold.css +84 -0
  36. package/dist/styles/toolbar-control-group.css +52 -2
  37. package/dist/styles/toolbar-text.css +11 -0
  38. package/dist/styles/toolbar.css +9 -0
  39. package/dist/styles/workbench.css +94 -0
  40. package/dist/tab-scaffold.d.ts +30 -0
  41. package/dist/tab-scaffold.js +16 -0
  42. package/dist/tab-scaffold.js.map +1 -0
  43. package/dist/toolbar-text.d.ts +4 -2
  44. package/dist/toolbar-text.js +1 -1
  45. package/dist/wire-nav-stack.d.ts +15 -0
  46. package/dist/wire-nav-stack.js +88 -0
  47. package/dist/wire-nav-stack.js.map +1 -0
  48. package/dist/wire-tab-scaffold.d.ts +11 -0
  49. package/dist/wire-tab-scaffold.js +16 -0
  50. package/dist/wire-tab-scaffold.js.map +1 -0
  51. package/dist/wire-token-search-fields.d.ts +46 -4
  52. package/dist/wire-token-search-fields.js +1 -1
  53. package/dist/workbench.d.ts +33 -0
  54. package/dist/workbench.js +17 -0
  55. package/dist/workbench.js.map +1 -0
  56. package/docs/accessibility.md +10 -10
  57. package/docs/app-layouts.md +57 -0
  58. package/docs/component-contract.md +34 -9
  59. package/docs/component-selection.md +56 -7
  60. package/docs/design-philosophy.md +23 -1
  61. package/docs/device-class.md +54 -0
  62. package/docs/layout.md +33 -8
  63. package/docs/nav-stack.md +47 -0
  64. package/docs/recipes.md +17 -5
  65. package/docs/split-view.md +49 -0
  66. package/docs/tab-scaffold.md +41 -0
  67. package/docs/ux-demo.md +3 -3
  68. package/docs/workbench.md +47 -0
  69. package/llms.txt +107 -43
  70. package/package.json +37 -11
  71. package/ux-demo/recipes/app-shell.tsx +3 -3
  72. package/ux-demo/recipes/composer-form.tsx +2 -2
  73. package/ux-demo/recipes/list-workspace-states.tsx +2 -2
  74. package/ux-demo/recipes/loaders.ts +2 -0
  75. package/ux-demo/recipes/master-detail-dialog.tsx +2 -2
  76. package/ux-demo/recipes/mount-recipe.ts +3 -0
  77. package/ux-demo/recipes/navigation-stack.tsx +76 -0
  78. package/ux-demo/recipes/recipes.css +19 -8
  79. package/ux-demo/recipes/workspace-header.tsx +2 -2
  80. package/dist/browser/page-header.js +0 -3
  81. package/dist/chunk-2PES33HS.js +0 -13
  82. package/dist/chunk-2PES33HS.js.map +0 -1
  83. package/dist/chunk-GY5WH7TO.js.map +0 -1
  84. package/dist/chunk-H5AGGVU5.js +0 -75
  85. package/dist/chunk-H5AGGVU5.js.map +0 -1
  86. package/dist/chunk-K3G72I6D.js +0 -24
  87. package/dist/chunk-K3G72I6D.js.map +0 -1
  88. package/dist/dialog-header.d.ts +0 -15
  89. package/dist/dialog-header.js +0 -5
  90. package/dist/dialog-header.js.map +0 -1
  91. package/dist/page-header.d.ts +0 -9
  92. package/dist/page-header.js +0 -3
  93. package/dist/page-header.js.map +0 -1
  94. package/dist/styles/dialog-header.css +0 -87
  95. package/dist/styles/page-header.css +0 -33
@@ -0,0 +1,20 @@
1
+ import { NavStack } from './chunk-RBTVBGRD.js';
2
+
3
+ import { ResizableRegion } from './chunk-M5JRI6MN.js';
4
+ import { jsx, jsxs } from 'kerfjs/jsx-runtime';
5
+
6
+ function SplitView({ id, label, list, detail, compact = false, detailActive = false, listTitle = "", detailTitle = "", backLabel = "Back", resizable, className = "" }) {
7
+ if (compact) {
8
+ const views = detailActive ? [{ key: "list", title: listTitle, content: list }, { key: "detail", title: detailTitle, content: detail }] : [{ key: "list", title: listTitle, content: list }];
9
+ return /* @__PURE__ */ jsx("div", { class: `kui-split-view kui-split-view--compact ${className}`.trim(), id, "data-component": "split-view", "data-split-mode": "compact", children: /* @__PURE__ */ jsx(NavStack, { id: `${id}-stack`, label, views, backLabel }) });
10
+ }
11
+ const listPane = resizable ? /* @__PURE__ */ jsx(ResizableRegion, { id: `${id}-list`, label: listTitle || "List", size: resizable.size, min: resizable.min, max: resizable.max, children: /* @__PURE__ */ jsx("div", { class: "kui-split-view__list kui-pane", "data-split-list": true, children: list }) }) : /* @__PURE__ */ jsx("div", { class: "kui-split-view__list kui-pane", "data-split-list": true, children: list });
12
+ return /* @__PURE__ */ jsxs("div", { class: `kui-split-view ${className}`.trim(), id, "data-component": "split-view", "data-split-mode": "split", "aria-label": label, children: [
13
+ listPane,
14
+ /* @__PURE__ */ jsx("div", { class: "kui-split-view__detail kui-pane", "data-split-detail": true, "aria-label": detailTitle || void 0, children: detail })
15
+ ] });
16
+ }
17
+
18
+ export { SplitView };
19
+
20
+ //# sourceMappingURL=split-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/split-view.tsx"],"names":[],"mappings":";;;;;AA4CO,SAAS,UAAU,EAAE,EAAA,EAAI,OAAO,IAAA,EAAM,MAAA,EAAQ,UAAU,KAAA,EAAO,YAAA,GAAe,OAAO,SAAA,GAAY,EAAA,EAAI,cAAc,EAAA,EAAI,SAAA,GAAY,QAAQ,SAAA,EAAW,SAAA,GAAY,IAAG,EAAmB;AAC7L,EAAA,IAAI,OAAA,EAAS;AACX,IAAA,MAAM,KAAA,GAAQ,YAAA,GACV,CAAC,EAAE,GAAA,EAAK,MAAA,EAAQ,KAAA,EAAO,SAAA,EAAW,OAAA,EAAS,IAAA,EAAK,EAAG,EAAE,GAAA,EAAK,QAAA,EAAU,KAAA,EAAO,WAAA,EAAa,OAAA,EAAS,MAAA,EAAQ,CAAA,GACzG,CAAC,EAAE,GAAA,EAAK,MAAA,EAAQ,KAAA,EAAO,SAAA,EAAW,OAAA,EAAS,IAAA,EAAM,CAAA;AACrD,IAAA,uBAAO,GAAA,CAAC,SAAI,KAAA,EAAO,CAAA,uCAAA,EAA0C,SAAS,CAAA,CAAA,CAAG,IAAA,EAAK,EAAG,EAAA,EAAQ,gBAAA,EAAe,YAAA,EAAa,mBAAgB,SAAA,EACnI,QAAA,kBAAA,GAAA,CAAC,YAAS,EAAA,EAAI,CAAA,EAAG,EAAE,CAAA,MAAA,CAAA,EAAU,KAAA,EAAc,KAAA,EAAc,SAAA,EAAsB,CAAA,EACjF,CAAA;AAAA,EACF;AACA,EAAA,MAAM,QAAA,GAAW,SAAA,mBACb,GAAA,CAAC,eAAA,EAAA,EAAgB,IAAI,CAAA,EAAG,EAAE,CAAA,KAAA,CAAA,EAAS,KAAA,EAAO,aAAa,MAAA,EAAQ,IAAA,EAAM,SAAA,CAAU,IAAA,EAAM,KAAK,SAAA,CAAU,GAAA,EAAK,GAAA,EAAK,SAAA,CAAU,KACtH,QAAA,kBAAA,GAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,+BAAA,EAAgC,mBAAe,IAAA,EAAE,QAAA,EAAA,IAAA,EAAK,CAAA,EACnE,CAAA,uBACC,KAAA,EAAA,EAAI,KAAA,EAAM,+BAAA,EAAgC,iBAAA,EAAe,MAAE,QAAA,EAAA,IAAA,EAAK,CAAA;AACrE,EAAA,uBAAO,IAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAO,CAAA,eAAA,EAAkB,SAAS,CAAA,CAAA,CAAG,IAAA,EAAK,EAAG,EAAA,EAAQ,gBAAA,EAAe,YAAA,EAAa,iBAAA,EAAgB,OAAA,EAAQ,cAAY,KAAA,EAC9H,QAAA,EAAA;AAAA,IAAA,QAAA;AAAA,oBACD,GAAA,CAAC,SAAI,KAAA,EAAM,iCAAA,EAAkC,qBAAiB,IAAA,EAAC,YAAA,EAAY,WAAA,IAAe,MAAA,EAAY,QAAA,EAAA,MAAA,EAAO;AAAA,GAAA,EAC/G,CAAA;AACF","file":"split-view.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\n\nimport { NavStack } from './nav-stack.js';\nimport { ResizableRegion } from './resizable-region.js';\n\nexport interface SplitViewResizable {\n size: number;\n min: number;\n max: number;\n}\n\nexport interface SplitViewProps {\n id: string;\n label: string;\n /** The list (primary) pane. */\n list: SafeHtml;\n /** The detail (secondary) pane. */\n detail: SafeHtml;\n /**\n * Compact (\"one pane at a time\") classes — a handset or portrait tablet.\n * Derive from `deviceClass().value.compact`. When true the split collapses to\n * a `NavStack`: the list is the root and the detail is pushed over it.\n */\n compact?: boolean;\n /** In compact mode, whether the detail is currently pushed over the list. */\n detailActive?: boolean;\n /** Title/label for the list (compact NavStack root + region label). */\n listTitle?: string;\n /** Title/label for the detail (compact NavStack pushed view + region label). */\n detailTitle?: string;\n /** Back label for the compact NavStack (default \"Back\"). */\n backLabel?: string;\n /** A resizable separator on roomy classes (min/max px). Omit for a fixed split. */\n resizable?: SplitViewResizable;\n className?: string;\n}\n\n/**\n * A list-detail (master-detail) split. On roomy classes it shows both panes side\n * by side with an optional resizable separator; on compact classes it collapses\n * to a `NavStack` (list → detail). See `docs/23-app-layouts.md` §3.2. Compose the\n * resizable wiring with `wireResizableRegions` and the compact back with\n * `wireNavStack`.\n */\nexport function SplitView({ id, label, list, detail, compact = false, detailActive = false, listTitle = '', detailTitle = '', backLabel = 'Back', resizable, className = '' }: SplitViewProps) {\n if (compact) {\n const views = detailActive\n ? [{ key: 'list', title: listTitle, content: list }, { key: 'detail', title: detailTitle, content: detail }]\n : [{ key: 'list', title: listTitle, content: list }];\n return <div class={`kui-split-view kui-split-view--compact ${className}`.trim()} id={id} data-component=\"split-view\" data-split-mode=\"compact\">\n <NavStack id={`${id}-stack`} label={label} views={views} backLabel={backLabel} />\n </div>;\n }\n const listPane = resizable\n ? <ResizableRegion id={`${id}-list`} label={listTitle || 'List'} size={resizable.size} min={resizable.min} max={resizable.max}>\n <div class=\"kui-split-view__list kui-pane\" data-split-list>{list}</div>\n </ResizableRegion>\n : <div class=\"kui-split-view__list kui-pane\" data-split-list>{list}</div>;\n return <div class={`kui-split-view ${className}`.trim()} id={id} data-component=\"split-view\" data-split-mode=\"split\" aria-label={label}>\n {listPane}\n <div class=\"kui-split-view__detail kui-pane\" data-split-detail aria-label={detailTitle || undefined}>{detail}</div>\n </div>;\n}\n"]}
@@ -24,12 +24,33 @@
24
24
  --kui-font-l: var(--wa-font-size-l, 20px);
25
25
  --kui-font-2xl: var(--wa-font-size-2xl, 32px);
26
26
  --kui-font-weight-semibold: var(--wa-font-weight-semibold, 650);
27
+ /* Official spacing scale (see docs/layout.md "Spacing scale"). Five canonical
28
+ steps, each expressing one relationship — pick by how connected the elements
29
+ are, not by eye:
30
+ 0px --kui-space-none no separation (elements read as one unit)
31
+ 4px --kui-space-2xs very minor — connected, but readability/aesthetics
32
+ demand a hair of air
33
+ 8px --kui-space-xs standard — between elements within a group
34
+ 16px --kui-space-m minor — between homogeneous groups
35
+ 24px --kui-space-l major — between heterogeneous groups
36
+ --kui-space-s (12px) and --kui-space-xl (32px) are off the canonical rhythm;
37
+ reach for them only as deliberate exceptions, not as default steps. All
38
+ scalable values are remify-authored against the fixed 16px baseline. */
39
+ --kui-space-none: 0;
27
40
  --kui-space-2xs: var(--wa-space-2xs, 0.25rem);
28
41
  --kui-space-xs: var(--wa-space-xs, 0.5rem);
29
42
  --kui-space-s: var(--wa-space-s, 0.75rem);
30
43
  --kui-space-m: var(--wa-space-m, 1rem);
31
44
  --kui-space-l: var(--wa-space-l, 1.5rem);
32
45
  --kui-space-xl: var(--wa-space-xl, 2rem);
46
+ /* Device-class breakpoints, mirrored from @kerfjs/ui/device-class's
47
+ DEFAULT_BREAKPOINTS so CSS media queries and the reactive deviceClass()
48
+ signal read the same minimum widths. Authored as literal px (a media-query
49
+ boundary is a physical width, not a scalable length). */
50
+ --kui-bp-mobile: 360px;
51
+ --kui-bp-tablet: 720px;
52
+ --kui-bp-desktop: 1024px;
53
+ --kui-bp-xl-desktop: 1440px;
33
54
  --kui-layout-inline-margin: var(--kui-space-xs);
34
55
  --kui-layout-item-border-width: 1px;
35
56
  --kui-layout-item-padding: var(--kui-space-xs);
@@ -93,10 +93,23 @@
93
93
  }
94
94
 
95
95
  .kui-menu-header__action {
96
+ /* A fitted square: the icon size plus its padding and 1px border, instead of a
97
+ forced 44px box that left an oversized halo around the ~18px icon and, via the
98
+ parent's align-items: stretch, a vertically stretched oval. `align-self:
99
+ center` keeps it centered in the header's 44px row without stretching. Sizing
100
+ off the icon variable keeps it square and correct at any icon size. */
101
+ --_kui-menu-header-action-size: calc(
102
+ var(--_kui-menu-header-action-icon-size) +
103
+ (2 * var(--kui-layout-item-padding, 0.5rem)) + 2px
104
+ );
96
105
  appearance: none;
97
- width: 2.75rem;
98
- height: 2.75rem;
106
+ display: inline-flex;
107
+ width: var(--_kui-menu-header-action-size);
108
+ height: var(--_kui-menu-header-action-size);
99
109
  padding: var(--kui-layout-item-padding, 0.5rem);
110
+ align-items: center;
111
+ justify-content: center;
112
+ align-self: center;
100
113
  cursor: pointer;
101
114
  flex: none;
102
115
  }
@@ -138,6 +151,10 @@
138
151
 
139
152
  .kui-menu-header__action > svg,
140
153
  .kui-menu-header__action-layer > svg {
154
+ /* min-block-size: 0 overrides the flex item's automatic minimum, which would
155
+ otherwise resolve to the icon SVG's intrinsic 24px viewBox height and stretch
156
+ the button past its used icon size. */
141
157
  width: var(--_kui-menu-header-action-icon-size);
142
158
  height: var(--_kui-menu-header-action-icon-size);
159
+ min-block-size: 0;
143
160
  }
@@ -0,0 +1,112 @@
1
+ .kui-nav-stack {
2
+ display: flex;
3
+ min-width: 0;
4
+ min-height: 0;
5
+ height: 100%;
6
+ overflow: hidden;
7
+ flex-direction: column;
8
+ background: var(--kui-color-surface);
9
+ }
10
+
11
+ .kui-nav-stack__chrome {
12
+ display: flex;
13
+ min-width: 0;
14
+ min-height: calc(2px + 2.625rem);
15
+ padding-inline: var(--kui-space-xs);
16
+ border-block-end: 1px solid var(--kui-color-border);
17
+ align-items: center;
18
+ gap: var(--kui-space-xs);
19
+ background: var(--kui-color-surface);
20
+ }
21
+
22
+ .kui-nav-stack__lead {
23
+ display: flex;
24
+ flex: 1;
25
+ min-width: 0;
26
+ align-items: center;
27
+ gap: var(--kui-space-2xs);
28
+ }
29
+
30
+ .kui-nav-stack__back {
31
+ display: grid;
32
+ width: 2.5rem;
33
+ height: 2.5rem;
34
+ padding: 0;
35
+ border: 1px solid transparent;
36
+ border-radius: var(--kui-layout-rounded-radius, calc(1px + 0.6875rem));
37
+ color: inherit;
38
+ background: transparent;
39
+ cursor: pointer;
40
+ place-items: center;
41
+ }
42
+
43
+ .kui-nav-stack__back:hover {
44
+ border-color: var(--kui-color-border);
45
+ background: var(--kui-color-neutral-fill-normal);
46
+ }
47
+
48
+ .kui-nav-stack__back:focus-visible {
49
+ outline: var(--kui-focus-ring);
50
+ outline-offset: 1px;
51
+ }
52
+
53
+ .kui-nav-stack__back-icon {
54
+ display: block;
55
+ }
56
+
57
+ /* The title is ToolbarText (which owns its font, height, and ellipsis); it just
58
+ needs to flex to fill the lead zone beside the back control. */
59
+ .kui-nav-stack__title {
60
+ flex: 1 1 auto;
61
+ min-width: 0;
62
+ }
63
+
64
+ .kui-nav-stack__actions {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: var(--kui-space-2xs);
68
+ }
69
+
70
+ .kui-nav-stack__viewport {
71
+ position: relative;
72
+ flex: 1;
73
+ min-height: 0;
74
+ overflow: hidden;
75
+ }
76
+
77
+ .kui-nav-stack__view {
78
+ position: absolute;
79
+ overflow: auto;
80
+ background: var(--kui-color-surface);
81
+ transition: transform 200ms ease;
82
+ inset: 0;
83
+ }
84
+
85
+ /* Only the active view (and any view mid-exit) is shown; lower entries stay
86
+ mounted for state/focus but hidden behind it. */
87
+ .kui-nav-stack__view[data-nav-active="false"] {
88
+ visibility: hidden;
89
+ }
90
+
91
+ .kui-nav-stack__view[data-nav-exiting="true"] {
92
+ z-index: 1;
93
+ visibility: visible;
94
+ }
95
+
96
+ /* Push: the incoming view starts off the trailing edge and slides to rest.
97
+ Pop: the outgoing view starts at rest and slides off the trailing edge. */
98
+ .kui-nav-stack__view--entering,
99
+ .kui-nav-stack__view--exiting {
100
+ transform: translateX(100%);
101
+ }
102
+
103
+ .kui-nav-stack__bottom {
104
+ border-block-start: 1px solid var(--kui-color-border);
105
+ background: var(--kui-color-surface);
106
+ }
107
+
108
+ @media (prefers-reduced-motion: reduce) {
109
+ .kui-nav-stack__view {
110
+ transition: none;
111
+ }
112
+ }
@@ -0,0 +1,59 @@
1
+ .kui-panel-header {
2
+ display: grid;
3
+ width: 100%;
4
+ gap: 0;
5
+ }
6
+
7
+ /* The only chrome PanelHeader adds to the plain Toolbar: the icon group is a
8
+ normal (bordered) control group given a brand fill, with its border color set
9
+ to match the fill so it reads as a solid filled tile rather than a bordered
10
+ pill. Both are the group's own theming variables — no Toolbar style is
11
+ overridden. */
12
+ .kui-panel-header__icon.kui-toolbar-control-group {
13
+ --kui-toolbar-control-background: var(
14
+ --kui-panel-header-icon-background,
15
+ var(--kui-color-brand-fill-quiet)
16
+ );
17
+ --kui-toolbar-control-border: var(
18
+ --kui-panel-header-icon-border,
19
+ var(--kui-color-brand-fill-quiet)
20
+ );
21
+ color: var(
22
+ --kui-panel-header-icon-foreground,
23
+ var(--kui-color-brand-on-quiet)
24
+ );
25
+ }
26
+
27
+ .kui-panel-header__icon svg {
28
+ width: 1.375rem;
29
+ height: 1.375rem;
30
+ }
31
+
32
+ /* The subtitle sits on its own row, its text left-aligned with the title text
33
+ (past the toolbar padding, the optional icon group + leading gap, and the
34
+ title's own inline padding). */
35
+ .kui-panel-header__summary {
36
+ min-width: 0;
37
+ margin: 0;
38
+ overflow: hidden;
39
+ padding-inline-start: var(--kui-panel-header-title-inset);
40
+ color: var(--kui-color-text-quiet);
41
+ text-overflow: ellipsis;
42
+ white-space: nowrap;
43
+ }
44
+
45
+ .kui-panel-header[data-has-icon="false"] {
46
+ --kui-panel-header-title-inset: calc(
47
+ var(--kui-toolbar-gap, 0.5rem) +
48
+ var(--kui-layout-item-padding, 0.5rem)
49
+ );
50
+ }
51
+
52
+ .kui-panel-header[data-has-icon="true"] {
53
+ --kui-panel-header-title-inset: calc(
54
+ var(--kui-toolbar-gap, 0.5rem) +
55
+ var(--kui-toolbar-group-size, calc(2px + 2.625rem)) +
56
+ var(--kui-toolbar-gap, 0.5rem) +
57
+ var(--kui-layout-item-padding, 0.5rem)
58
+ );
59
+ }
@@ -0,0 +1,35 @@
1
+ .kui-split-view {
2
+ display: flex;
3
+ min-width: 0;
4
+ min-height: 0;
5
+ height: 100%;
6
+ overflow: hidden;
7
+ background: var(--kui-color-surface);
8
+ }
9
+
10
+ /* The list pane; when not resizable it takes an intrinsic column and the detail
11
+ grows to fill. A resizable list is sized by ResizableRegion instead. */
12
+ .kui-split-view__list {
13
+ min-width: 0;
14
+ min-height: 0;
15
+ flex: 0 0 auto;
16
+ width: var(--kui-split-view-list-width, 20rem);
17
+ border-inline-end: 1px solid var(--kui-color-border);
18
+ }
19
+
20
+ .kui-split-view > .kui-resizable-region .kui-split-view__list {
21
+ width: 100%;
22
+ border-inline-end: 0;
23
+ }
24
+
25
+ .kui-split-view__detail {
26
+ flex: 1;
27
+ min-width: 0;
28
+ min-height: 0;
29
+ }
30
+
31
+ /* Compact mode is a full-bleed NavStack. */
32
+ .kui-split-view--compact {
33
+ display: block;
34
+ height: 100%;
35
+ }
@@ -11,12 +11,11 @@
11
11
  @import "./resizable-region.css";
12
12
  @import "./app-tab.css";
13
13
  @import "./tab-bar.css";
14
- @import "./page-header.css";
15
14
  @import "./loading-spinner.css";
16
15
  @import "./select.css";
17
16
  @import "./segmented-control.css";
18
17
  @import "./token-search-field.css";
19
18
  @import "./state-banner.css";
20
19
  @import "./empty-state.css";
21
- @import "./dialog-header.css";
20
+ @import "./panel-header.css";
22
21
  @import "./value-table.css";
@@ -0,0 +1,84 @@
1
+ .kui-tab-scaffold {
2
+ display: flex;
3
+ min-width: 0;
4
+ min-height: 0;
5
+ height: 100%;
6
+ overflow: hidden;
7
+ flex-direction: column;
8
+ background: var(--kui-color-surface);
9
+ }
10
+
11
+ .kui-tab-scaffold__scenes {
12
+ position: relative;
13
+ flex: 1;
14
+ min-height: 0;
15
+ overflow: hidden;
16
+ }
17
+
18
+ /* Every tab stays mounted (its NavStack keeps its stack and scroll); only the
19
+ active scene is shown. */
20
+ .kui-tab-scaffold__scene {
21
+ position: absolute;
22
+ overflow: auto;
23
+ inset: 0;
24
+ }
25
+
26
+ .kui-tab-scaffold__scene[data-active="false"] {
27
+ visibility: hidden;
28
+ }
29
+
30
+ .kui-tab-scaffold__bar {
31
+ display: flex;
32
+ flex: 0 0 auto;
33
+ padding-block-start: var(--kui-space-2xs);
34
+ /* Respect the home-indicator safe area on iOS-class devices. */
35
+ padding-block-end: calc(
36
+ var(--kui-space-2xs) + env(safe-area-inset-bottom, 0px)
37
+ );
38
+ border-block-start: 1px solid var(--kui-color-border);
39
+ background: var(--kui-color-surface);
40
+ }
41
+
42
+ .kui-tab-scaffold__tab {
43
+ display: flex;
44
+ flex: 1 1 0;
45
+ min-width: 0;
46
+ min-height: 2.75rem;
47
+ padding: var(--kui-space-2xs) var(--kui-space-xs);
48
+ border: 0;
49
+ color: var(--kui-color-text-quiet);
50
+ background: transparent;
51
+ cursor: pointer;
52
+ font: inherit;
53
+ flex-direction: column;
54
+ align-items: center;
55
+ justify-content: center;
56
+ gap: 0.125rem;
57
+ }
58
+
59
+ .kui-tab-scaffold__tab[aria-selected="true"] {
60
+ color: var(--kui-color-accent-text);
61
+ }
62
+
63
+ .kui-tab-scaffold__tab:focus-visible {
64
+ outline: var(--kui-focus-ring);
65
+ outline-offset: -2px;
66
+ }
67
+
68
+ .kui-tab-scaffold__tab-icon {
69
+ display: grid;
70
+ place-items: center;
71
+ }
72
+
73
+ .kui-tab-scaffold__tab-icon svg {
74
+ width: 1.375rem;
75
+ height: 1.375rem;
76
+ }
77
+
78
+ .kui-tab-scaffold__tab-label {
79
+ max-width: 100%;
80
+ overflow: hidden;
81
+ font-size: var(--kui-font-2xs);
82
+ text-overflow: ellipsis;
83
+ white-space: nowrap;
84
+ }
@@ -43,6 +43,14 @@
43
43
  }
44
44
 
45
45
  .kui-toolbar-control-group[data-appearance="borderless"] {
46
+ /* A borderless group has no base fill to lift from, so the default
47
+ surface-colored hover/selected background is invisible against the page.
48
+ Use a visible neutral tint so buttons still show hover/press feedback
49
+ (KF-WZDQS8). */
50
+ --kui-toolbar-control-hover-background: var(--kui-color-neutral-fill-normal);
51
+ --kui-toolbar-control-selected-background: var(
52
+ --kui-color-neutral-fill-normal
53
+ );
46
54
  border-color: transparent;
47
55
  background: transparent;
48
56
  }
@@ -107,6 +115,10 @@
107
115
  }
108
116
 
109
117
  .kui-toolbar-control-group wa-button::part(base) {
118
+ /* Match min-width to the 40px height so an icon-only button's highlight is at
119
+ least a circle, not a vertical oval — the host is 40px wide but the base
120
+ part was sizing to its ~32px content. Text/caret buttons grow past it. */
121
+ min-width: 2.5rem;
110
122
  min-height: 2.5rem;
111
123
  box-sizing: border-box;
112
124
  border: 1px solid transparent;
@@ -133,14 +145,52 @@
133
145
  background: transparent;
134
146
  }
135
147
 
136
- .kui-toolbar-control-group[data-single="false"]
137
- > :is(button, wa-button)[aria-pressed="true"] {
148
+ /* A group holding exactly one control behaves like `single` even without the
149
+ flag: the whole group background changes on hover instead of highlighting an
150
+ inner layer inset from the border (KF-J9P1ZD — a PanelHeader's lone Close/
151
+ Done action). */
152
+ .kui-toolbar-control-group:not([data-single="true"]):has(
153
+ > :is(button, wa-button):only-child
154
+ ):hover {
155
+ background: var(--kui-toolbar-control-hover-background);
156
+ }
157
+
158
+ .kui-toolbar-control-group:not([data-single="true"]):has(
159
+ > :is(button, wa-button):only-child
160
+ )
161
+ > :is(button, wa-button):hover,
162
+ .kui-toolbar-control-group:not([data-single="true"]):has(
163
+ > :is(button, wa-button):only-child
164
+ )
165
+ wa-button:hover::part(base) {
166
+ border-color: transparent;
167
+ background: transparent;
168
+ }
169
+
170
+ /* A plain <button> child paints its own box for the selected state. */
171
+ .kui-toolbar-control-group[data-single="false"] > button[aria-pressed="true"] {
138
172
  border-color: var(--kui-toolbar-control-selected-border);
139
173
  color: var(--kui-toolbar-control-selected-color);
140
174
  background: var(--kui-toolbar-control-selected-background);
141
175
  box-shadow: var(--kui-toolbar-control-selected-shadow);
142
176
  }
143
177
 
178
+ /* A wa-button's visual is ::part(base), not the outer 40px host box (which has no
179
+ pill radius). Paint the selected background/border/shadow on ::part(base) —
180
+ mirroring the hover rule — so the selected state matches the pill radius/size
181
+ and stays inside the group's border instead of overflowing it as an oversized
182
+ square (KF-5BDDQ9). The selected text color rides on the host and inherits in. */
183
+ .kui-toolbar-control-group[data-single="false"]
184
+ > wa-button[aria-pressed="true"] {
185
+ color: var(--kui-toolbar-control-selected-color);
186
+ }
187
+ .kui-toolbar-control-group[data-single="false"]
188
+ > wa-button[aria-pressed="true"]::part(base) {
189
+ border-color: var(--kui-toolbar-control-selected-border);
190
+ background: var(--kui-toolbar-control-selected-background);
191
+ box-shadow: var(--kui-toolbar-control-selected-shadow);
192
+ }
193
+
144
194
  .kui-toolbar-control-group > :is(button, wa-button):focus-visible {
145
195
  outline: var(--kui-focus-ring);
146
196
  outline-offset: 1px;
@@ -14,6 +14,17 @@
14
14
  align-items: center;
15
15
  }
16
16
 
17
+ .kui-toolbar-text[data-size="xlarge"] {
18
+ /* Matches the page/panel title: the same responsive clamp so a PanelHeader
19
+ title reads at the page-title scale without a bespoke override. Height grows
20
+ to the larger text (the fixed 40px would clip its ascenders). */
21
+ height: auto;
22
+ min-height: 2.75rem;
23
+ color: var(--kui-color-text);
24
+ font-size: clamp(var(--kui-font-l), 2.3vw, var(--kui-font-2xl));
25
+ letter-spacing: -0.025em;
26
+ }
27
+
17
28
  .kui-toolbar-text[data-size="large"] {
18
29
  font-size: var(--kui-font-m);
19
30
  letter-spacing: -0.015em;
@@ -33,16 +33,25 @@
33
33
  gap: var(--kui-toolbar-gap, 0.5rem);
34
34
  }
35
35
 
36
+ /* Pin each zone to its column so a display:none (empty) leading or center zone
37
+ doesn't let the trailing zone auto-place into the first column — which left an
38
+ extra gap + empty track to the trailing zone's right (KF-5BEF2M). The trailing
39
+ zone anchors to the last column via -2/-1, so it stays flush right for both
40
+ the 3- and 2-column layouts, and still stacks under the leading zone when a
41
+ consumer collapses the toolbar to a single column. */
36
42
  .kui-toolbar__leading {
43
+ grid-column: 1;
37
44
  overflow: hidden;
38
45
  }
39
46
 
40
47
  .kui-toolbar__center {
41
48
  justify-content: center;
49
+ grid-column: 2;
42
50
  }
43
51
 
44
52
  .kui-toolbar__trailing {
45
53
  justify-content: flex-end;
54
+ grid-column: -2 / -1;
46
55
  }
47
56
 
48
57
  .kui-toolbar[data-has-center="false"] .kui-toolbar__center,
@@ -0,0 +1,94 @@
1
+ .kui-workbench {
2
+ display: flex;
3
+ min-width: 0;
4
+ min-height: 0;
5
+ height: 100%;
6
+ overflow: hidden;
7
+ background: var(--kui-color-surface);
8
+ }
9
+
10
+ /* Side rails. Collapsing snaps the flex track to zero (one reflow) while the
11
+ fixed-width content slides out via transform (composited, clipped) — never a
12
+ per-frame width animation. See docs/18 / the KF-7QKJRK catalog sidebar. */
13
+ .kui-workbench__rail {
14
+ flex: 0 0 var(--kui-workbench-rail-width, 17.5rem);
15
+ min-width: 0;
16
+ overflow: hidden;
17
+ background: var(--kui-color-surface);
18
+ }
19
+
20
+ .kui-workbench__rail--left {
21
+ border-inline-end: 1px solid var(--kui-color-border);
22
+ }
23
+
24
+ .kui-workbench__rail--right {
25
+ border-inline-start: 1px solid var(--kui-color-border);
26
+ }
27
+
28
+ .kui-workbench__rail[data-collapsed="true"] {
29
+ flex-basis: 0;
30
+ border-inline: 0;
31
+ }
32
+
33
+ .kui-workbench__center {
34
+ display: flex;
35
+ flex: 1;
36
+ min-width: 0;
37
+ min-height: 0;
38
+ overflow: hidden;
39
+ flex-direction: column;
40
+ }
41
+
42
+ .kui-workbench__main {
43
+ flex: 1;
44
+ min-height: 0;
45
+ overflow: auto;
46
+ }
47
+
48
+ /* Bottom drawer — the vertical analogue of a rail. */
49
+ .kui-workbench__drawer {
50
+ flex: 0 0 var(--kui-workbench-drawer-height, 13.75rem);
51
+ min-height: 0;
52
+ overflow: hidden;
53
+ border-block-start: 1px solid var(--kui-color-border);
54
+ background: var(--kui-color-surface);
55
+ }
56
+
57
+ .kui-workbench__drawer[data-collapsed="true"] {
58
+ flex-basis: 0;
59
+ border-block-start: 0;
60
+ }
61
+
62
+ /* The fixed-size sliding content. The rail/drawer keeps a definite size on its
63
+ content so the transform reads as a slide, not a squeeze. */
64
+ .kui-workbench__rail .kui-workbench__panel-content {
65
+ width: var(--kui-workbench-rail-width, 17.5rem);
66
+ height: 100%;
67
+ transition: transform 200ms ease;
68
+ }
69
+
70
+ .kui-workbench__drawer .kui-workbench__panel-content {
71
+ width: 100%;
72
+ height: var(--kui-workbench-drawer-height, 13.75rem);
73
+ transition: transform 200ms ease;
74
+ }
75
+
76
+ .kui-workbench__rail--left[data-collapsed="true"]
77
+ .kui-workbench__panel-content {
78
+ transform: translateX(-100%);
79
+ }
80
+
81
+ .kui-workbench__rail--right[data-collapsed="true"]
82
+ .kui-workbench__panel-content {
83
+ transform: translateX(100%);
84
+ }
85
+
86
+ .kui-workbench__drawer[data-collapsed="true"] .kui-workbench__panel-content {
87
+ transform: translateY(100%);
88
+ }
89
+
90
+ @media (prefers-reduced-motion: reduce) {
91
+ .kui-workbench__panel-content {
92
+ transition: none;
93
+ }
94
+ }
@@ -0,0 +1,30 @@
1
+ import { SafeHtml } from 'kerfjs';
2
+
3
+ interface TabScaffoldTab {
4
+ id: string;
5
+ label: string;
6
+ /** Decorative icon shown above the label in the bottom bar. */
7
+ icon?: SafeHtml;
8
+ /** The tab's content — typically a `NavStack` so each tab keeps its own stack. */
9
+ content: SafeHtml;
10
+ }
11
+ interface TabScaffoldProps {
12
+ id: string;
13
+ /** Accessible name for the tab bar. */
14
+ label: string;
15
+ tabs: TabScaffoldTab[];
16
+ /** The controlled active tab id (the app owns selection). */
17
+ active: string;
18
+ className?: string;
19
+ }
20
+ /**
21
+ * A mobile-first, iOS-like bottom tab scaffold: a bottom tab bar that switches
22
+ * between major sections, each tab keeping its own content (usually a `NavStack`)
23
+ * mounted so its stack and scroll survive a switch. Controlled — the app owns
24
+ * `active`; wire selection with `@kerfjs/ui/wire-tab-scaffold`'s `wireTabScaffold`.
25
+ * On larger classes, promote the tabs to a `Workbench` rail or sidebar instead of
26
+ * a bottom bar. See `docs/23-app-layouts.md` §3.4.
27
+ */
28
+ declare function TabScaffold({ id, label, tabs, active, className }: TabScaffoldProps): SafeHtml;
29
+
30
+ export { TabScaffold, type TabScaffoldProps, type TabScaffoldTab };
@@ -0,0 +1,16 @@
1
+ import { jsxs, jsx } from 'kerfjs/jsx-runtime';
2
+
3
+ // src/tab-scaffold.tsx
4
+ function TabScaffold({ id, label, tabs, active, className = "" }) {
5
+ return /* @__PURE__ */ jsxs("section", { class: `kui-tab-scaffold ${className}`.trim(), id, "data-component": "tab-scaffold", "data-tab-scaffold-id": id, children: [
6
+ /* @__PURE__ */ jsx("div", { class: "kui-tab-scaffold__scenes", children: tabs.map((tab) => /* @__PURE__ */ jsx("div", { class: "kui-tab-scaffold__scene", "data-tab-scaffold-scene": tab.id, "data-active": String(tab.id === active), "aria-hidden": String(tab.id !== active), children: tab.content })) }),
7
+ /* @__PURE__ */ jsx("nav", { class: "kui-tab-scaffold__bar", role: "tablist", "aria-label": label, children: tabs.map((tab) => /* @__PURE__ */ jsxs("button", { type: "button", class: "kui-tab-scaffold__tab", role: "tab", "data-tab-scaffold-tab": tab.id, "aria-selected": String(tab.id === active), tabindex: tab.id === active ? "0" : "-1", children: [
8
+ tab.icon && /* @__PURE__ */ jsx("span", { class: "kui-tab-scaffold__tab-icon", "aria-hidden": "true", children: tab.icon }),
9
+ /* @__PURE__ */ jsx("span", { class: "kui-tab-scaffold__tab-label", children: tab.label })
10
+ ] })) })
11
+ ] });
12
+ }
13
+
14
+ export { TabScaffold };
15
+
16
+ //# sourceMappingURL=tab-scaffold.js.map