@kerfjs/ui 5.0.0-beta.7 → 5.0.0-beta.9

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 (83) hide show
  1. package/README.md +20 -20
  2. package/ai/component-catalog.json +130 -83
  3. package/ai/public-api-signatures-v1.md +200 -29
  4. package/ai/skill.md +12 -12
  5. package/ai/webawesome-jsx-signatures-v1.md +1 -1
  6. package/dist/browser/list-action-row.js +4 -0
  7. package/dist/browser/{menu-header.js → list-header.js} +2 -2
  8. package/dist/browser/{menu-item.js → list-item.js} +2 -2
  9. package/dist/catalog.d.ts +119 -0
  10. package/dist/catalog.js +128 -0
  11. package/dist/catalog.js.map +1 -0
  12. package/dist/{chunk-TZNCCM5H.js → chunk-2TS7ZSYX.js} +4 -4
  13. package/dist/chunk-2TS7ZSYX.js.map +1 -0
  14. package/dist/{chunk-PONXLV5U.js → chunk-37T3VNIZ.js} +1 -1
  15. package/dist/{chunk-PONXLV5U.js.map → chunk-37T3VNIZ.js.map} +1 -1
  16. package/dist/{chunk-3YD6ZNRA.js → chunk-7JB7VPRI.js} +8 -8
  17. package/dist/{chunk-3YD6ZNRA.js.map → chunk-7JB7VPRI.js.map} +1 -1
  18. package/dist/{chunk-N3SUXLLS.js → chunk-BLK3UEEI.js} +2 -2
  19. package/dist/{chunk-N3SUXLLS.js.map → chunk-BLK3UEEI.js.map} +1 -1
  20. package/dist/{chunk-YKUUZPED.js → chunk-L4OKFDJY.js} +7 -7
  21. package/dist/{chunk-YKUUZPED.js.map → chunk-L4OKFDJY.js.map} +1 -1
  22. package/dist/{chunk-KJYXOTG3.js → chunk-RUNWEPR4.js} +3 -3
  23. package/dist/chunk-RUNWEPR4.js.map +1 -0
  24. package/dist/{chunk-H6URQGJI.js → chunk-W5EQ3HYQ.js} +11 -11
  25. package/dist/{chunk-H6URQGJI.js.map → chunk-W5EQ3HYQ.js.map} +1 -1
  26. package/dist/index.d.ts +4 -4
  27. package/dist/index.js +11 -11
  28. package/dist/{menu-action-row.d.ts → list-action-row.d.ts} +7 -7
  29. package/dist/list-action-row.js +5 -0
  30. package/dist/list-action-row.js.map +1 -0
  31. package/dist/{menu-header.d.ts → list-header.d.ts} +9 -9
  32. package/dist/list-header.js +7 -0
  33. package/dist/list-header.js.map +1 -0
  34. package/dist/{menu-item.d.ts → list-item.d.ts} +5 -5
  35. package/dist/list-item.js +5 -0
  36. package/dist/list-item.js.map +1 -0
  37. package/dist/nav-stack.js +1 -1
  38. package/dist/panel-header.d.ts +9 -1
  39. package/dist/panel-header.js +1 -1
  40. package/dist/split-view.d.ts +1 -1
  41. package/dist/split-view.js +1 -1
  42. package/dist/split-view.js.map +1 -1
  43. package/dist/styles/catalog.css +482 -0
  44. package/dist/styles/list-action-row.css +160 -0
  45. package/dist/styles/{menu-header.css → list-header.css} +33 -33
  46. package/dist/styles/list-item.css +123 -0
  47. package/dist/styles/styles.css +4 -3
  48. package/dist/toolbar-text.d.ts +10 -2
  49. package/dist/toolbar-text.js +1 -1
  50. package/dist/value-table.js +1 -1
  51. package/dist/wire-catalog.d.ts +26 -0
  52. package/dist/wire-catalog.js +50 -0
  53. package/dist/wire-catalog.js.map +1 -0
  54. package/docs/accessibility.md +15 -10
  55. package/docs/catalog.md +154 -0
  56. package/docs/component-contract.md +12 -12
  57. package/docs/component-selection.md +14 -13
  58. package/docs/design-philosophy.md +4 -4
  59. package/docs/layout.md +8 -8
  60. package/docs/recipes.md +17 -5
  61. package/docs/split-view.md +1 -1
  62. package/docs/ux-demo.md +5 -5
  63. package/llms.txt +12 -8
  64. package/package.json +25 -16
  65. package/ux-demo/recipes/app-shell.tsx +3 -3
  66. package/ux-demo/recipes/list-detail-dialog.tsx +45 -0
  67. package/ux-demo/recipes/loaders.ts +4 -2
  68. package/ux-demo/recipes/loading-inspector.tsx +79 -0
  69. package/ux-demo/recipes/navigation-sidebar.tsx +5 -5
  70. package/ux-demo/recipes/navigation-stack.tsx +4 -4
  71. package/ux-demo/recipes/recipes.css +13 -7
  72. package/dist/browser/menu-action-row.js +0 -4
  73. package/dist/chunk-KJYXOTG3.js.map +0 -1
  74. package/dist/chunk-TZNCCM5H.js.map +0 -1
  75. package/dist/menu-action-row.js +0 -5
  76. package/dist/menu-action-row.js.map +0 -1
  77. package/dist/menu-header.js +0 -7
  78. package/dist/menu-header.js.map +0 -1
  79. package/dist/menu-item.js +0 -5
  80. package/dist/menu-item.js.map +0 -1
  81. package/dist/styles/menu-action-row.css +0 -160
  82. package/dist/styles/menu-item.css +0 -123
  83. package/ux-demo/recipes/master-detail-dialog.tsx +0 -45
@@ -0,0 +1,79 @@
1
+ import '@kerfjs/ui/layout.css';
2
+ import '@kerfjs/ui/select/register';
3
+ import './recipes.css';
4
+
5
+ import { ListItem } from '@kerfjs/ui/list-item';
6
+ import { LucideIcon } from '@kerfjs/ui/lucide-icon';
7
+ import { PanelHeader } from '@kerfjs/ui/panel-header';
8
+ import { SegmentedControl } from '@kerfjs/ui/segmented-control';
9
+ import { Select } from '@kerfjs/ui/select';
10
+ import { StateBanner } from '@kerfjs/ui/state-banner';
11
+ import { ValueTable, ValueTableRow } from '@kerfjs/ui/value-table';
12
+ import { signal } from 'kerfjs';
13
+ import { Columns3, FileText, Inbox, List } from 'lucide';
14
+
15
+ import type { RecipeFactory } from './types.js';
16
+
17
+ const icon = (node: Parameters<typeof LucideIcon>[0]['icon'], name: string) => <LucideIcon icon={node} name={name} />;
18
+
19
+ /**
20
+ * A record inspector whose per-record values load asynchronously. Every
21
+ * value-bearing component takes `placeholder` from one loading flag, so the same
22
+ * chrome renders a faithful loading state and then the populated record — no
23
+ * separate skeleton markup. The composition is the point; `Skeleton` is the
24
+ * primitive it is built on (see the Skeleton component demo).
25
+ */
26
+ export const createRecipe: RecipeFactory = (announce) => {
27
+ const loading = signal(true);
28
+ const render = () => {
29
+ const p = loading.value;
30
+ return <section class="kui-recipe recipe-inspector kui-recipe__surface kui-pane" data-recipe="recipe-loading-inspector" data-inspector-loading={String(p)}>
31
+ <PanelHeader
32
+ title="Ticket · KF-2048"
33
+ titleId="recipe-inspector-title"
34
+ summary="Restore keyboard focus after a dialog closes"
35
+ summaryId="recipe-inspector-summary"
36
+ icon={icon(FileText, 'file-text')}
37
+ placeholder={p}
38
+ actions={<button class="kui-recipe__button" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="toggle">{p ? 'Show loaded' : 'Show loading'}</button>}
39
+ />
40
+ <div class="recipe-inspector__body kui-pane__content kui-content">
41
+ <section>
42
+ <ValueTable label="Ticket details">
43
+ <ValueTableRow label="Status" value="In review" icon={icon(Inbox, 'inbox')} placeholder={p} />
44
+ <ValueTableRow label="Owner" value="Mara Lopez" placeholder={p} />
45
+ <ValueTableRow label="Priority" value="High" placeholder={p} />
46
+ <ValueTableRow label="Updated" value="2 hours ago" placeholder={p} />
47
+ </ValueTable>
48
+ </section>
49
+ <Select name="recipe-inspector-status" value="review" label="Status" placeholder={p} choices={[
50
+ { value: 'review', label: 'In review' },
51
+ { value: 'ready', label: 'Ready' },
52
+ { value: 'done', label: 'Done' },
53
+ ]} />
54
+ <SegmentedControl id="recipe-inspector-view" label="Inspector view" value="details" appearance="toolbar" shape="pill" size="small" placeholder={p} choices={[
55
+ { value: 'details', label: 'Details', content: icon(List, 'list') },
56
+ { value: 'activity', label: 'Activity', content: icon(Columns3, 'columns-3') },
57
+ { value: 'files', label: 'Files', content: icon(FileText, 'file-text') },
58
+ ]} />
59
+ <section>
60
+ <ListItem action="recipe-action" itemId="reassign" label="Reassign ticket" icon={icon(Inbox, 'inbox')} placeholder={p} />
61
+ <ListItem action="recipe-action" itemId="watch" label="Watch for changes" placeholder={p} />
62
+ </section>
63
+ {p
64
+ ? <StateBanner tone="info" title="" detail="" icon={icon(FileText, 'file-text')} placeholder />
65
+ : <StateBanner tone="success" title="Up to date" detail="All checks passed on the latest revision." icon={icon(FileText, 'file-text')} />}
66
+ <p class="kui-recipe__ownership kui-content-item">Each value-bearing component's <code>placeholder</code> prop renders skeletons in its value slots while loading, so the recipe composes a faithful loading inspector from real chrome. The app owns the loading lifecycle and which values are still unknown.</p>
67
+ </div>
68
+ </section>;
69
+ };
70
+ return {
71
+ render,
72
+ action(command) {
73
+ if (command === 'toggle') {
74
+ loading.value = !loading.value;
75
+ announce(loading.value ? 'Inspector is loading' : 'Inspector loaded');
76
+ }
77
+ },
78
+ };
79
+ };
@@ -1,9 +1,9 @@
1
1
  import '@kerfjs/ui/layout.css';
2
2
  import './recipes.css';
3
3
 
4
+ import { ListHeader } from '@kerfjs/ui/list-header';
5
+ import { ListItem } from '@kerfjs/ui/list-item';
4
6
  import { LucideIcon } from '@kerfjs/ui/lucide-icon';
5
- import { MenuHeader } from '@kerfjs/ui/menu-header';
6
- import { MenuItem } from '@kerfjs/ui/menu-item';
7
7
  import { Toolbar } from '@kerfjs/ui/toolbar';
8
8
  import { ToolbarControlGroup } from '@kerfjs/ui/toolbar-control-group';
9
9
  import { signal } from 'kerfjs';
@@ -16,11 +16,11 @@ export const createRecipe: RecipeFactory = (announce) => {
16
16
  const expanded = signal(true);
17
17
  const render = () => <aside class="kui-recipe recipe-sidebar kui-recipe__surface kui-pane" data-recipe="recipe-navigation-sidebar">
18
18
  <div class="recipe-sidebar__body kui-pane__content kui-content">
19
- <section><MenuHeader label="Workspace" count={3} countLabel="3 workspaces" action="recipe-action" actionLabel="Add workspace item" actionIcon={<LucideIcon icon={Plus} name="plus" />} /><MenuItem action="recipe-action" itemId="inbox" label="Inbox" icon={<LucideIcon icon={Inbox} name="inbox" />} trailing={<span>12</span>} selected={selected.value === 'inbox'} /><MenuItem action="recipe-action" itemId="shared" label="Shared with me" icon={<LucideIcon icon={Users} name="users" />} selected={selected.value === 'shared'} /><MenuItem action="recipe-action" itemId="drafts" label="Drafts without an icon" selected={selected.value === 'drafts'} /></section>
20
- <section><MenuHeader label="Projects" toggle action="recipe-action" expanded={expanded.value} />{expanded.value && <><MenuItem action="recipe-action" itemId="design" label="Design system rollout across desktop and tablet" icon={<LucideIcon icon={Folder} name="folder" />} selected={selected.value === 'design'} multiline /><MenuItem action="recipe-action" itemId="archive" label="Archived" disabled title="Available to administrators" /><div class="kui-content-item"><strong>Quarterly goal</strong><p class="kui-recipe__muted">Ship accessible navigation patterns to every workspace.</p></div></>}</section>
19
+ <section><ListHeader label="Workspace" count={3} countLabel="3 workspaces" action="recipe-action" actionLabel="Add workspace item" actionIcon={<LucideIcon icon={Plus} name="plus" />} /><ListItem action="recipe-action" itemId="inbox" label="Inbox" icon={<LucideIcon icon={Inbox} name="inbox" />} trailing={<span>12</span>} selected={selected.value === 'inbox'} /><ListItem action="recipe-action" itemId="shared" label="Shared with me" icon={<LucideIcon icon={Users} name="users" />} selected={selected.value === 'shared'} /><ListItem action="recipe-action" itemId="drafts" label="Drafts without an icon" selected={selected.value === 'drafts'} /></section>
20
+ <section><ListHeader label="Projects" toggle action="recipe-action" expanded={expanded.value} />{expanded.value && <><ListItem action="recipe-action" itemId="design" label="Design system rollout across desktop and tablet" icon={<LucideIcon icon={Folder} name="folder" />} selected={selected.value === 'design'} multiline /><ListItem action="recipe-action" itemId="archive" label="Archived" disabled title="Available to administrators" /><div class="kui-content-item"><strong>Quarterly goal</strong><p class="kui-recipe__muted">Ship accessible navigation patterns to every workspace.</p></div></>}</section>
21
21
  <p class="kui-recipe__ownership kui-content-item">The recipe owns pane, content-item, and toolbar geometry. The app owns routes, permissions, labels, and disclosure state.</p>
22
22
  </div>
23
23
  <footer class="recipe-sidebar__footer kui-pane__footer"><Toolbar label="Sidebar actions" divider={false} leading={<ToolbarControlGroup appearance="borderless" single><button type="button" aria-label="Add project" data-action="recipe-action" data-recipe-command="add"><LucideIcon icon={Plus} name="plus" /></button></ToolbarControlGroup>} trailing={<ToolbarControlGroup appearance="borderless" single><button type="button" aria-label="Workspace settings" data-action="recipe-action" data-recipe-command="settings"><LucideIcon icon={Settings} name="settings" /></button></ToolbarControlGroup>} /></footer>
24
24
  </aside>;
25
- return { render, action(command, element) { if (element.matches('.kui-menu-header__toggle')) { expanded.value = !expanded.value; announce(expanded.value ? 'Projects expanded' : 'Projects collapsed'); return; } const id = element.dataset.itemId; if (id) selected.value = id; announce(id ? `Selected ${id}` : `${command} requested`); } };
25
+ return { render, action(command, element) { if (element.matches('.kui-list-header__toggle')) { expanded.value = !expanded.value; announce(expanded.value ? 'Projects expanded' : 'Projects collapsed'); return; } const id = element.dataset.itemId; if (id) selected.value = id; announce(id ? `Selected ${id}` : `${command} requested`); } };
26
26
  };
@@ -3,9 +3,9 @@ import '@kerfjs/ui/nav-stack.css';
3
3
  import '@kerfjs/ui/toolbar-text.css';
4
4
  import './recipes.css';
5
5
 
6
+ import { ListHeader } from '@kerfjs/ui/list-header';
7
+ import { ListItem } from '@kerfjs/ui/list-item';
6
8
  import { LucideIcon } from '@kerfjs/ui/lucide-icon';
7
- import { MenuHeader } from '@kerfjs/ui/menu-header';
8
- import { MenuItem } from '@kerfjs/ui/menu-item';
9
9
  import { NavStack, type NavStackView } from '@kerfjs/ui/nav-stack';
10
10
  import { signal } from 'kerfjs';
11
11
  import { ChevronRight, FileText, Folder } from 'lucide';
@@ -30,8 +30,8 @@ export const createRecipe: RecipeFactory = (announce) => {
30
30
  title: 'Library',
31
31
  content: <div class="recipe-navstack__view kui-pane__content kui-content">
32
32
  <section>
33
- <MenuHeader label="Components" />
34
- {ITEMS.map((item) => <MenuItem action="recipe-action" itemId={item.id} label={item.label} icon={<LucideIcon icon={Folder} name="folder" />} trailing={<LucideIcon icon={ChevronRight} name="chevron-right" />} />)}
33
+ <ListHeader label="Components" />
34
+ {ITEMS.map((item) => <ListItem action="recipe-action" itemId={item.id} label={item.label} icon={<LucideIcon icon={Folder} name="folder" />} trailing={<LucideIcon icon={ChevronRight} name="chevron-right" />} />)}
35
35
  </section>
36
36
  <p class="kui-recipe__ownership kui-content-item">The recipe owns the stack as a signal of views and pushes/pops it; `NavStack` renders the stack and `wireNavStack` slides the content and settles the chrome. The app owns selection, data, and routing.</p>
37
37
  </div>,
@@ -114,23 +114,25 @@
114
114
  margin: 0;
115
115
  }
116
116
 
117
- .recipe-master-detail {
117
+ .recipe-list-detail {
118
118
  display: grid;
119
119
  width: 100%;
120
120
  min-height: 25rem;
121
121
  grid-template-columns: minmax(13rem, 0.8fr) minmax(18rem, 1.4fr);
122
122
  }
123
- .recipe-master-detail__master {
123
+ .recipe-list-detail__list {
124
124
  border-inline-end: 1px solid var(--kui-color-border);
125
125
  }
126
- .recipe-master-detail__detail {
126
+ .recipe-list-detail__detail {
127
127
  min-width: 0;
128
+ }
129
+ .recipe-list-detail__body {
128
130
  padding-block: var(--kui-layout-item-gap);
129
131
  }
130
- .recipe-master-detail__title {
132
+ .recipe-list-detail__title {
131
133
  margin-inline: var(--kui-layout-inline-margin, remify(8px));
132
134
  }
133
- .recipe-master-detail__actions {
135
+ .recipe-list-detail__actions {
134
136
  margin-inline: var(--kui-layout-inline-margin, remify(8px));
135
137
  }
136
138
  .recipe-dialog {
@@ -256,10 +258,10 @@
256
258
  .recipe-compact-toolbar__guide {
257
259
  grid-template-columns: 1fr 1fr;
258
260
  }
259
- .recipe-master-detail {
261
+ .recipe-list-detail {
260
262
  grid-template-columns: 1fr;
261
263
  }
262
- .recipe-master-detail__master {
264
+ .recipe-list-detail__list {
263
265
  border-inline-end: 0;
264
266
  border-block-end: 1px solid var(--kui-color-border);
265
267
  }
@@ -353,3 +355,7 @@
353
355
  little block breathing room, not an inline pad that would double-inset them. */
354
356
  padding-block: var(--kui-space-2xs);
355
357
  }
358
+
359
+ .recipe-inspector {
360
+ width: min(remify(560px), 100%);
361
+ }
@@ -1,4 +0,0 @@
1
- import '../styles/foundation.css';
2
- import '../styles/skeleton.css';
3
- import '../styles/menu-action-row.css';
4
- export * from '../menu-action-row.js';
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/toolbar-text.tsx"],"names":[],"mappings":";;;AAcO,SAAS,WAAA,CAAY,EAAE,IAAA,EAAM,IAAA,GAAO,SAAA,EAAW,YAAY,EAAA,EAAI,EAAA,EAAI,WAAA,GAAc,KAAA,EAAM,EAAqB;AACjH,EAAA,uBAAO,GAAA,CAAC,MAAA,EAAA,EAAK,KAAA,EAAO,CAAA,iBAAA,EAAoB,SAAS,CAAA,CAAA,CAAG,IAAA,EAAK,EAAG,gBAAA,EAAe,cAAA,EAAe,WAAA,EAAW,IAAA,EAAM,kBAAA,EAAkB,WAAA,GAAc,MAAA,GAAS,MAAA,EAAW,EAAA,EAAQ,WAAA,EAAW,WAAA,GAAc,MAAA,GAAS,MAAA,EAAY,QAAA,EAAA,WAAA,mBAAc,GAAA,CAAC,QAAA,EAAA,EAAS,KAAA,EAAM,KAAA,EAAM,CAAA,GAAK,IAAA,EAAK,CAAA;AACrQ","file":"chunk-KJYXOTG3.js","sourcesContent":["import { Skeleton } from './skeleton.js';\n\nexport type ToolbarTextSize = 'xlarge' | 'large' | 'default' | 'small';\n\nexport interface ToolbarTextProps {\n text: string;\n size?: ToolbarTextSize;\n className?: string;\n /** Optional id, e.g. so a dialog can reference the title via aria-labelledby. */\n id?: string;\n /** Render the text as an unanimated loading skeleton instead of its value. */\n placeholder?: boolean;\n}\n\nexport function ToolbarText({ text, size = 'default', className = '', id, placeholder = false }: ToolbarTextProps) {\n return <span class={`kui-toolbar-text ${className}`.trim()} data-component=\"toolbar-text\" data-size={size} data-placeholder={placeholder ? 'true' : undefined} id={id} aria-busy={placeholder ? 'true' : undefined}>{placeholder ? <Skeleton width=\"8em\" /> : text}</span>;\n}\n"]}
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/panel-header.tsx"],"names":[],"mappings":";;;;;;AA+BO,SAAS,WAAA,CAAY,EAAE,KAAA,EAAO,OAAA,EAAS,OAAA,EAAS,SAAA,EAAW,IAAA,EAAM,aAAA,GAAgB,EAAA,EAAI,OAAA,EAAS,WAAA,GAAc,KAAA,EAAM,EAAqB;AAC5I,EAAA,MAAM,2BAAW,IAAA,CAAA,QAAA,EAAA,EACd,QAAA,EAAA;AAAA,IAAA,IAAA,oBAAQ,GAAA,CAAC,mBAAA,EAAA,EAAoB,MAAA,EAAM,IAAA,EAAC,SAAA,EAAW,0BAA0B,aAAa,CAAA,CAAA,CAAG,IAAA,EAAK,EAAI,QAAA,EAAA,IAAA,EAAK,CAAA;AAAA,oBACxG,GAAA,CAAC,WAAA,EAAA,EAAY,IAAA,EAAM,KAAA,EAAO,IAAA,EAAK,UAAS,EAAA,EAAI,OAAA,EAAS,SAAA,EAAU,yBAAA,EAA0B,WAAA,EAA0B;AAAA,GAAA,EACrH,CAAA;AAEA,EAAA,uBAAO,IAAA,CAAC,KAAA,EAAA,EAAI,KAAA,EAAM,kBAAA,EAAmB,gBAAA,EAAe,cAAA,EAAe,eAAA,EAAe,MAAA,CAAO,OAAA,CAAQ,IAAI,CAAC,CAAA,EAAG,kBAAA,EAAkB,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAC,CAAA,EAAG,kBAAA,EAAkB,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAC,CAAA,EAAG,kBAAA,EAAkB,WAAA,GAAc,MAAA,GAAS,MAAA,EACxO,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,WAAQ,OAAA,EAAS,QAAA,EAAU,QAAA,EAAU,OAAA,EAAS,SAAS,KAAA,EAAO,CAAA;AAAA,IAC9D,OAAA,oBAAW,GAAA,CAAC,GAAA,EAAA,EAAE,KAAA,EAAM,2BAAA,EAA4B,EAAA,EAAI,SAAA,EAAY,QAAA,EAAA,WAAA,mBAAc,GAAA,CAAC,QAAA,EAAA,EAAS,KAAA,EAAM,MAAA,EAAO,IAAK,OAAA,EAAQ;AAAA,GAAA,EACrH,CAAA;AACF","file":"chunk-TZNCCM5H.js","sourcesContent":["import type { SafeHtml } from 'kerfjs';\n\nimport { Skeleton } from './skeleton.js';\nimport { Toolbar } from './toolbar.js';\nimport { ToolbarControlGroup } from './toolbar-control-group.js';\nimport { ToolbarText } from './toolbar-text.js';\n\nexport interface PanelHeaderProps {\n title: string;\n titleId: string;\n summary?: string;\n summaryId?: string;\n icon?: SafeHtml;\n iconClassName?: string;\n actions?: SafeHtml;\n /** Render the title and summary as unanimated loading skeletons, keeping the icon and actions. */\n placeholder?: boolean;\n}\n\n/**\n * The heading of a panel, dialog, or page: a plain `Toolbar` whose leading zone\n * holds an optional icon control group and the title as extra-large `ToolbarText`,\n * whose trailing zone holds the app's action controls, and with an optional\n * subtitle on its own row, left-aligned with the title.\n *\n * PanelHeader overrides no Toolbar styles — it is just a Toolbar with an xl title.\n * The only styling it adds is the icon group's fill/border color and the subtitle.\n * When no icon is provided, the icon group is omitted entirely. The `actions` slot\n * is passed straight into the toolbar's trailing zone; the app supplies whatever\n * trailing controls it needs (typically a `ToolbarControlGroup`).\n */\nexport function PanelHeader({ title, titleId, summary, summaryId, icon, iconClassName = '', actions, placeholder = false }: PanelHeaderProps) {\n const identity = <>\n {icon && <ToolbarControlGroup single className={`kui-panel-header__icon ${iconClassName}`.trim()}>{icon}</ToolbarControlGroup>}\n <ToolbarText text={title} size=\"xlarge\" id={titleId} className=\"kui-panel-header__title\" placeholder={placeholder} />\n </>;\n\n return <div class=\"kui-panel-header\" data-component=\"panel-header\" data-has-icon={String(Boolean(icon))} data-has-actions={String(Boolean(actions))} data-has-summary={String(Boolean(summary))} data-placeholder={placeholder ? 'true' : undefined}>\n <Toolbar leading={identity} trailing={actions} divider={false} />\n {summary && <p class=\"kui-panel-header__summary\" id={summaryId}>{placeholder ? <Skeleton width=\"18em\" /> : summary}</p>}\n </div>;\n}\n"]}
@@ -1,5 +0,0 @@
1
- export { MenuActionRow } from './chunk-3YD6ZNRA.js';
2
-
3
-
4
-
5
- //# sourceMappingURL=menu-action-row.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"menu-action-row.js"}
@@ -1,7 +0,0 @@
1
- export { MenuHeader } from './chunk-H6URQGJI.js';
2
-
3
-
4
-
5
-
6
-
7
- //# sourceMappingURL=menu-header.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"menu-header.js"}
package/dist/menu-item.js DELETED
@@ -1,5 +0,0 @@
1
- export { MenuItem } from './chunk-YKUUZPED.js';
2
-
3
-
4
-
5
- //# sourceMappingURL=menu-item.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":[],"names":[],"mappings":"","file":"menu-item.js"}
@@ -1,160 +0,0 @@
1
- .kui-menu-action-row {
2
- --kui-menu-action-row-color: var(--kui-color-neutral-on-normal);
3
- --kui-menu-action-row-border: transparent;
4
- --kui-menu-action-row-background: transparent;
5
- --kui-menu-action-row-hover-border: var(--kui-color-neutral-border-normal);
6
- --kui-menu-action-row-hover-background: var(--kui-color-neutral-fill-quiet);
7
- --kui-menu-action-row-selected-color: var(--kui-color-brand-on-quiet);
8
- --kui-menu-action-row-selected-border: var(--kui-color-brand-border-quiet);
9
- --kui-menu-action-row-selected-background: var(--kui-color-brand-fill-normal);
10
- --kui-menu-action-row-trailing-hover-background: var(
11
- --kui-color-neutral-fill-normal
12
- );
13
- display: flex;
14
- width: calc(100% - (2 * var(--kui-layout-inline-margin, 0.5rem)));
15
- min-height: 2.75rem;
16
- margin-inline: var(--kui-layout-inline-margin, 0.5rem);
17
- border-radius: var(--kui-layout-rounded-radius, calc(1px + 0.6875rem));
18
- color: var(--kui-menu-action-row-color);
19
- background: var(--kui-menu-action-row-background);
20
- box-shadow: inset 0 0 0 1px var(--kui-menu-action-row-border);
21
- align-items: stretch;
22
- }
23
-
24
- .kui-menu-action-row:has(.kui-menu-action-row__primary:hover) {
25
- --kui-menu-action-row-border: var(--kui-menu-action-row-hover-border);
26
- --kui-menu-action-row-background: var(--kui-menu-action-row-hover-background);
27
- }
28
-
29
- .kui-menu-action-row[data-selected="true"],
30
- .kui-menu-action-row[data-pressed="true"] {
31
- --kui-menu-action-row-border: var(--kui-menu-action-row-selected-border);
32
- --kui-menu-action-row-background: var(
33
- --kui-menu-action-row-selected-background
34
- );
35
- color: var(--kui-menu-action-row-selected-color);
36
- font-weight: 650;
37
- }
38
-
39
- .kui-menu-action-row:has(.kui-menu-action-row__primary:focus-visible) {
40
- outline: var(--kui-focus-ring);
41
- outline-offset: 1px;
42
- }
43
-
44
- .kui-menu-action-row__primary,
45
- .kui-menu-action-row__trailing-action {
46
- height: auto !important;
47
- min-height: 2.75rem;
48
- padding: 0;
49
- border: 0;
50
- appearance: none;
51
- color: inherit;
52
- background: transparent;
53
- cursor: pointer;
54
- font: inherit;
55
- }
56
-
57
- .kui-menu-action-row__primary {
58
- position: relative;
59
- display: block;
60
- flex: 1 1 auto;
61
- min-width: 2.75rem;
62
- padding: var(--kui-layout-item-padding, 0.5rem);
63
- padding-inline-start: calc(1px + var(--kui-layout-item-padding, 0.5rem));
64
- text-align: start;
65
- }
66
-
67
- .kui-menu-action-row__primary:focus-visible {
68
- outline: none;
69
- }
70
-
71
- .kui-menu-action-row__primary:disabled,
72
- .kui-menu-action-row__trailing-action:disabled {
73
- cursor: not-allowed;
74
- opacity: 0.58;
75
- }
76
-
77
- .kui-menu-action-row__icon {
78
- position: absolute;
79
- inset-block-start: 50%;
80
- inset-inline-start: calc(1px + var(--kui-layout-item-padding, 0.5rem));
81
- display: grid;
82
- width: 1.5rem;
83
- height: 1.5rem;
84
- color: currentColor;
85
- transform: translateY(-50%);
86
- place-items: center;
87
- }
88
-
89
- .kui-menu-action-row__icon svg {
90
- display: block;
91
- width: 1.5rem;
92
- height: 1.5rem;
93
- }
94
-
95
- .kui-menu-action-row__label {
96
- display: grid;
97
- min-width: 0;
98
- min-height: calc(
99
- 2.75rem - (2 * var(--kui-layout-item-padding, 0.5rem))
100
- );
101
- overflow: hidden;
102
- text-overflow: ellipsis;
103
- white-space: nowrap;
104
- align-items: center;
105
- }
106
-
107
- .kui-menu-action-row[data-has-icon="true"] .kui-menu-action-row__label {
108
- margin-inline-start: calc(
109
- 1.5rem + var(--kui-layout-item-gap, 0.5rem)
110
- );
111
- }
112
-
113
- .kui-menu-action-row[data-multiline="true"] .kui-menu-action-row__label {
114
- display: block;
115
- overflow: visible;
116
- overflow-wrap: anywhere;
117
- white-space: normal;
118
- }
119
-
120
- .kui-menu-action-row[data-multiline="true"] .kui-menu-action-row__icon {
121
- inset-block-start: calc(
122
- var(--kui-layout-item-padding, 0.5rem) + (1lh / 2)
123
- );
124
- }
125
-
126
- .kui-menu-action-row__trailing-action {
127
- display: grid;
128
- width: 2.75rem;
129
- min-width: 2.75rem;
130
- border-radius: var(--kui-radius-pill);
131
- place-items: center;
132
- }
133
-
134
- .kui-menu-action-row__trailing-action:hover:not(:disabled) {
135
- background: var(--kui-menu-action-row-trailing-hover-background);
136
- }
137
-
138
- .kui-menu-action-row__trailing-action:focus-visible {
139
- outline: var(--kui-focus-ring);
140
- outline-offset: -2px;
141
- }
142
-
143
- .kui-menu-action-row__trailing-action > svg {
144
- width: 1rem;
145
- height: 1rem;
146
- }
147
-
148
- @media (forced-colors: active) {
149
- .kui-menu-action-row {
150
- --kui-menu-action-row-border: ButtonText;
151
- outline: 1px solid ButtonText;
152
- outline-offset: -1px;
153
- }
154
-
155
- .kui-menu-action-row[data-selected="true"],
156
- .kui-menu-action-row[data-pressed="true"] {
157
- --kui-menu-action-row-selected-color: HighlightText;
158
- --kui-menu-action-row-selected-background: Highlight;
159
- }
160
- }
@@ -1,123 +0,0 @@
1
- .kui-menu-item {
2
- --kui-menu-item-color: var(--kui-color-neutral-on-normal);
3
- --kui-menu-item-border: transparent;
4
- --kui-menu-item-background: transparent;
5
- --kui-menu-item-hover-border: var(--kui-color-neutral-border-normal);
6
- --kui-menu-item-hover-background: var(--kui-color-neutral-fill-quiet);
7
- --kui-menu-item-selected-color: var(--kui-color-brand-on-quiet);
8
- --kui-menu-item-selected-border: var(--kui-color-brand-border-quiet);
9
- --kui-menu-item-selected-background: var(--kui-color-brand-fill-normal);
10
- display: grid;
11
- width: calc(100% - (2 * var(--kui-layout-inline-margin, 0.5rem)));
12
- min-height: 2.75rem;
13
- margin-inline: var(--kui-layout-inline-margin, 0.5rem);
14
- padding: var(--kui-layout-item-padding, 0.5rem);
15
- box-sizing: border-box;
16
- border: 1px solid var(--kui-menu-item-border);
17
- border-radius: var(--kui-layout-rounded-radius, calc(1px + 0.6875rem));
18
- color: var(--kui-menu-item-color);
19
- background: var(--kui-menu-item-background);
20
- cursor: pointer;
21
- grid-template-columns: minmax(0, 1fr) auto;
22
- align-items: center;
23
- column-gap: var(--kui-layout-item-gap, 0.5rem);
24
- font: inherit;
25
- text-align: start;
26
- }
27
-
28
- .kui-menu-item[data-has-icon="true"] {
29
- grid-template-columns: 1.5rem minmax(0, 1fr) auto;
30
- }
31
-
32
- .kui-menu-item[data-has-icon="true"] .kui-menu-item__icon {
33
- grid-column: 1;
34
- }
35
-
36
- .kui-menu-item[data-has-icon="true"] .kui-menu-item__label {
37
- grid-column: 2;
38
- }
39
-
40
- .kui-menu-item[data-has-icon="true"] .kui-menu-item__trailing {
41
- grid-column: 3;
42
- }
43
-
44
- .kui-menu-item[data-has-icon="false"] .kui-menu-item__label {
45
- grid-column: 1;
46
- }
47
-
48
- .kui-menu-item[data-has-icon="false"] .kui-menu-item__trailing {
49
- grid-column: 2;
50
- }
51
-
52
- .kui-menu-item:hover {
53
- border-color: var(--kui-menu-item-hover-border);
54
- background: var(--kui-menu-item-hover-background);
55
- }
56
-
57
- .kui-menu-item:disabled {
58
- color: var(--kui-color-neutral-on-quiet);
59
- cursor: not-allowed;
60
- opacity: 0.58;
61
- }
62
-
63
- .kui-menu-item[aria-current="page"],
64
- .kui-menu-item[aria-pressed="true"] {
65
- border-color: var(--kui-menu-item-selected-border);
66
- color: var(--kui-menu-item-selected-color);
67
- background: var(--kui-menu-item-selected-background);
68
- font-weight: 650;
69
- }
70
-
71
- .kui-menu-item:focus-visible {
72
- outline: var(--kui-focus-ring);
73
- outline-offset: 1px;
74
- }
75
-
76
- .kui-menu-item__icon {
77
- display: grid;
78
- width: 1.5rem;
79
- height: 1.5rem;
80
- color: currentColor;
81
- place-items: center;
82
- }
83
-
84
- .kui-menu-item__icon svg {
85
- display: block;
86
- width: 1.5rem;
87
- height: 1.5rem;
88
- }
89
-
90
- .kui-menu-item__label {
91
- min-width: 0;
92
- overflow: hidden;
93
- text-overflow: ellipsis;
94
- white-space: nowrap;
95
- }
96
-
97
- .kui-menu-item[data-multiline="true"] {
98
- height: fit-content;
99
- white-space: normal;
100
- }
101
-
102
- .kui-menu-item[data-multiline="true"] .kui-menu-item__label {
103
- overflow: visible;
104
- white-space: normal;
105
- }
106
-
107
- .kui-menu-item[data-multiline="true"] .kui-menu-item__icon {
108
- align-self: start;
109
- margin-block-start: calc((1lh - 1.5rem) / 2);
110
- }
111
-
112
- .kui-menu-item__trailing {
113
- display: inline-flex;
114
- min-width: 0;
115
- align-items: center;
116
- justify-content: flex-end;
117
- gap: var(--kui-layout-item-gap, 0.5rem);
118
- }
119
-
120
- .kui-menu-item__trailing > svg {
121
- width: 1rem;
122
- height: 1rem;
123
- }
@@ -1,45 +0,0 @@
1
- import '@kerfjs/ui/layout.css';
2
- import '@kerfjs/ui/webawesome.css';
3
- import '@awesome.me/webawesome/dist/components/button/button.js';
4
- import '@awesome.me/webawesome/dist/components/dialog/dialog.js';
5
- import './recipes.css';
6
-
7
- import { LucideIcon } from '@kerfjs/ui/lucide-icon';
8
- import { MenuHeader } from '@kerfjs/ui/menu-header';
9
- import { MenuItem } from '@kerfjs/ui/menu-item';
10
- import { PanelHeader } from '@kerfjs/ui/panel-header';
11
- import { ValueTable, ValueTableRow } from '@kerfjs/ui/value-table';
12
- import { signal } from 'kerfjs';
13
- import { FileText } from 'lucide';
14
-
15
- import type { RecipeFactory } from './types.js';
16
-
17
- const records = {
18
- alpha: { name: 'Northstar migration', owner: 'Mara Chen', state: 'In review', updated: 'Today, 09:42' },
19
- beta: { name: 'Tablet navigation', owner: 'Sam Rivera', state: 'Ready', updated: 'Yesterday, 16:20' },
20
- gamma: { name: 'Contrast audit', owner: 'Inez Okafor', state: 'Blocked', updated: 'Monday, 11:05' },
21
- } as const;
22
-
23
- interface RecipeDialogElement extends HTMLElement {
24
- open: boolean;
25
- }
26
-
27
- export const createRecipe: RecipeFactory = (announce) => {
28
- const selected = signal<keyof typeof records>('alpha');
29
- const open = { value: false };
30
- const render = () => <section class="kui-recipe kui-content" data-recipe="recipe-master-detail-dialog"><button class="kui-recipe__button kui-content-item" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="open">Open project details</button><p class="kui-recipe__ownership kui-content-item">Web Awesome owns modal focus and dismissal. The recipe owns header/body/master-detail anatomy; the app owns open state, selection, and policy.</p><wa-dialog class="recipe-dialog" label="Project details" without-header open={open.value}><div class="recipe-dialog__pane kui-pane"><PanelHeader title="Project details" titleId="recipe-dialog-title" summary="Compare delivery state without leaving the workspace." summaryId="recipe-dialog-summary" icon={<LucideIcon icon={FileText} name="file-text" />} actions={<wa-button appearance="plain" data-action="recipe-action" data-recipe-command="close">Close</wa-button>} /><div class="recipe-master-detail kui-pane__content"><nav class="recipe-master-detail__master kui-pane" aria-label="Projects"><MenuHeader label="Recent projects" /><div class="kui-pane__content kui-content"><section>{Object.entries(records).map(([id, record]) => <MenuItem action="recipe-action" itemId={id} label={record.name} selected={selected.value === id} multiline />)}</section></div></nav><section class="recipe-master-detail__detail kui-content" aria-live="polite"><h3 class="recipe-master-detail__title kui-recipe__pane-title kui-content-item">{records[selected.value].name}</h3><ValueTable label="Project details"><ValueTableRow label="Owner" value={records[selected.value].owner} /><ValueTableRow label="Status" value={records[selected.value].state} /><ValueTableRow label="Updated" value={records[selected.value].updated} /></ValueTable><div class="recipe-master-detail__actions kui-control-cluster"><wa-button appearance="outlined" data-action="recipe-action" data-recipe-command="archive">Archive</wa-button><wa-button variant="brand" appearance="accent" data-action="recipe-action" data-recipe-command="open-record">Open project</wa-button></div></section></div></div></wa-dialog></section>;
31
- return {
32
- render,
33
- action(command, element) {
34
- const id = element.dataset.itemId;
35
- if (id && id in records) { selected.value = id as keyof typeof records; announce(`Selected ${records[selected.value].name}`); return; }
36
- const dialog = element.closest('[data-recipe]')?.querySelector<RecipeDialogElement>('wa-dialog');
37
- if (command === 'open' && dialog) { element.focus(); open.value = true; dialog.open = true; return; }
38
- if (command === 'close' && dialog) { dialog.open = false; return; }
39
- announce(`${command} requested`);
40
- },
41
- afterHide() {
42
- open.value = false;
43
- },
44
- };
45
- };