@kerfjs/ui 4.4.1 → 5.0.0-beta.10
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.
- package/README.md +240 -10
- package/ai/component-catalog.json +4766 -0
- package/ai/component-catalog.schema.json +79 -0
- package/ai/public-api-signatures-v1.md +1327 -0
- package/ai/skill.md +73 -11
- package/ai/webawesome-jsx-signatures-v1.md +106 -0
- package/dist/app-tab.d.ts +14 -2
- package/dist/app-tab.js +3 -1
- package/dist/browser/app-tab.js +3 -2
- package/dist/browser/disclosure-arrow.js +4 -0
- package/dist/browser/empty-state.js +3 -3
- package/dist/browser/list-action-row.js +4 -0
- package/dist/browser/list-header.js +6 -0
- package/dist/browser/list-item.js +4 -0
- package/dist/browser/loading-spinner.js +2 -2
- package/dist/browser/lucide-icon.js +2 -2
- package/dist/browser/panel-header.js +7 -0
- package/dist/browser/resizable-region.js +2 -2
- package/dist/browser/segmented-control.js +3 -2
- package/dist/browser/select.js +4 -3
- package/dist/browser/skeleton.js +3 -0
- package/dist/browser/state-banner.js +3 -2
- package/dist/browser/tab-bar.js +2 -2
- package/dist/browser/token-search-field.js +4 -0
- package/dist/browser/toolbar-control-group.js +2 -2
- package/dist/browser/toolbar-text.js +3 -2
- package/dist/browser/toolbar.js +2 -2
- package/dist/browser/value-table.js +3 -2
- package/dist/catalog.d.ts +119 -0
- package/dist/catalog.js +143 -0
- package/dist/catalog.js.map +1 -0
- package/dist/chunk-2Z5TWHKQ.js +34 -0
- package/dist/chunk-2Z5TWHKQ.js.map +1 -0
- package/dist/chunk-37T3VNIZ.js +19 -0
- package/dist/chunk-37T3VNIZ.js.map +1 -0
- package/dist/chunk-56RJXPHG.js +43 -0
- package/dist/chunk-56RJXPHG.js.map +1 -0
- package/dist/chunk-7BR3DO2A.js +31 -0
- package/dist/chunk-7BR3DO2A.js.map +1 -0
- package/dist/chunk-7JB7VPRI.js +34 -0
- package/dist/chunk-7JB7VPRI.js.map +1 -0
- package/dist/chunk-7LDCTU7D.js +134 -0
- package/dist/chunk-7LDCTU7D.js.map +1 -0
- package/dist/{chunk-4HHCYSGT.js → chunk-AQNKJRFH.js} +2 -2
- package/dist/{chunk-4HHCYSGT.js.map → chunk-AQNKJRFH.js.map} +1 -1
- package/dist/{chunk-MBR3TYNL.js → chunk-AVZHJN6C.js} +3 -3
- package/dist/chunk-AVZHJN6C.js.map +1 -0
- package/dist/chunk-B24C4A6T.js +22 -0
- package/dist/chunk-B24C4A6T.js.map +1 -0
- package/dist/chunk-BLK3UEEI.js +23 -0
- package/dist/chunk-BLK3UEEI.js.map +1 -0
- package/dist/chunk-CVZYNJJ5.js +265 -0
- package/dist/chunk-CVZYNJJ5.js.map +1 -0
- package/dist/chunk-I7INRJO4.js +17 -0
- package/dist/chunk-I7INRJO4.js.map +1 -0
- package/dist/chunk-L4OKFDJY.js +24 -0
- package/dist/chunk-L4OKFDJY.js.map +1 -0
- package/dist/chunk-LC3YSAZC.js +39 -0
- package/dist/chunk-LC3YSAZC.js.map +1 -0
- package/dist/{chunk-3O7ZEUW2.js → chunk-M5JRI6MN.js} +3 -3
- package/dist/chunk-M5JRI6MN.js.map +1 -0
- package/dist/chunk-RUNWEPR4.js +10 -0
- package/dist/chunk-RUNWEPR4.js.map +1 -0
- package/dist/chunk-W5EQ3HYQ.js +42 -0
- package/dist/chunk-W5EQ3HYQ.js.map +1 -0
- package/dist/{chunk-MQIZAD4A.js → chunk-XXMOARTH.js} +5 -3
- package/dist/chunk-XXMOARTH.js.map +1 -0
- package/dist/chunk-Y5MRDTLC.js +20 -0
- package/dist/chunk-Y5MRDTLC.js.map +1 -0
- package/dist/{chunk-TRNBHPNW.js → chunk-YGN7OMLT.js} +11 -8
- package/dist/chunk-YGN7OMLT.js.map +1 -0
- package/dist/collapsible-panel.d.ts +66 -0
- package/dist/collapsible-panel.js +22 -0
- package/dist/collapsible-panel.js.map +1 -0
- package/dist/device-class.d.ts +62 -0
- package/dist/device-class.js +75 -0
- package/dist/device-class.js.map +1 -0
- package/dist/disclosure-arrow.d.ts +14 -0
- package/dist/disclosure-arrow.js +4 -0
- package/dist/disclosure-arrow.js.map +1 -0
- package/dist/index.d.ts +10 -6
- package/dist/index.js +21 -16
- package/dist/list-action-row.d.ts +52 -0
- package/dist/list-action-row.js +5 -0
- package/dist/list-action-row.js.map +1 -0
- package/dist/list-header.d.ts +43 -0
- package/dist/list-header.js +7 -0
- package/dist/list-header.js.map +1 -0
- package/dist/list-item.d.ts +33 -0
- package/dist/list-item.js +5 -0
- package/dist/list-item.js.map +1 -0
- package/dist/nav-stack.d.ts +38 -0
- package/dist/nav-stack.js +5 -0
- package/dist/nav-stack.js.map +1 -0
- package/dist/panel-header.d.ts +36 -0
- package/dist/panel-header.js +7 -0
- package/dist/panel-header.js.map +1 -0
- package/dist/resizable-region.d.ts +3 -1
- package/dist/resizable-region.js +1 -1
- package/dist/segmented-control.d.ts +3 -1
- package/dist/segmented-control.js +2 -1
- package/dist/select-register.d.ts +5 -1
- package/dist/select-register.js +3 -0
- package/dist/select-register.js.map +1 -1
- package/dist/select.d.ts +5 -2
- package/dist/select.js +2 -1
- package/dist/skeleton.d.ts +24 -0
- package/dist/skeleton.js +3 -0
- package/dist/skeleton.js.map +1 -0
- package/dist/split-view.d.ts +42 -0
- package/dist/split-view.js +21 -0
- package/dist/split-view.js.map +1 -0
- package/dist/state-banner.d.ts +3 -1
- package/dist/state-banner.js +2 -1
- package/dist/styles/app-tab.css +152 -0
- package/dist/styles/catalog.css +498 -0
- package/dist/styles/collapsible-panel.css +146 -0
- package/dist/styles/disclosure-arrow.css +23 -0
- package/dist/styles/empty-state.css +47 -0
- package/dist/styles/foundation.css +347 -0
- package/dist/styles/layout.css +73 -0
- package/dist/styles/list-action-row.css +160 -0
- package/dist/styles/list-header.css +160 -0
- package/dist/styles/list-item.css +123 -0
- package/dist/styles/loading-spinner.css +21 -0
- package/dist/styles/lucide-icon.css +6 -0
- package/dist/styles/nav-stack.css +111 -0
- package/dist/styles/panel-header.css +64 -0
- package/dist/styles/resizable-region.css +111 -0
- package/{src → dist/styles}/segmented-control.css +62 -31
- package/dist/styles/select.css +138 -0
- package/dist/styles/skeleton.css +31 -0
- package/dist/styles/split-view.css +35 -0
- package/dist/styles/state-banner.css +173 -0
- package/dist/styles/styles.css +23 -0
- package/dist/styles/tab-bar.css +45 -0
- package/dist/styles/tab-scaffold.css +84 -0
- package/dist/styles/token-search-field.css +289 -0
- package/dist/styles/toolbar-control-group.css +211 -0
- package/dist/styles/toolbar-text.css +40 -0
- package/dist/styles/toolbar.css +61 -0
- package/dist/styles/value-table.css +75 -0
- package/{src → dist/styles}/webawesome.css +119 -29
- package/dist/styles/workbench.css +94 -0
- package/dist/tab-bar.d.ts +8 -1
- package/dist/tab-bar.js +1 -1
- package/dist/tab-scaffold.d.ts +30 -0
- package/dist/tab-scaffold.js +16 -0
- package/dist/tab-scaffold.js.map +1 -0
- package/dist/token-search-field.d.ts +43 -0
- package/dist/token-search-field.js +4 -0
- package/dist/token-search-field.js.map +1 -0
- package/dist/toolbar-text.d.ts +15 -3
- package/dist/toolbar-text.js +2 -1
- package/dist/value-table.d.ts +12 -2
- package/dist/value-table.js +2 -1
- package/dist/webawesome.d.ts +100 -0
- package/dist/webawesome.js +3 -0
- package/dist/webawesome.js.map +1 -0
- package/dist/wire-catalog.d.ts +26 -0
- package/dist/wire-catalog.js +48 -0
- package/dist/wire-catalog.js.map +1 -0
- package/dist/wire-nav-stack.d.ts +15 -0
- package/dist/wire-nav-stack.js +88 -0
- package/dist/wire-nav-stack.js.map +1 -0
- package/dist/wire-resizable-regions.js +1 -1
- package/dist/wire-sidebar.d.ts +43 -0
- package/dist/wire-sidebar.js +117 -0
- package/dist/wire-sidebar.js.map +1 -0
- package/dist/wire-tab-bars.d.ts +15 -2
- package/dist/wire-tab-bars.js +1 -1
- package/dist/wire-tab-scaffold.d.ts +11 -0
- package/dist/wire-tab-scaffold.js +16 -0
- package/dist/wire-tab-scaffold.js.map +1 -0
- package/dist/wire-token-search-fields.d.ts +97 -0
- package/dist/wire-token-search-fields.js +5 -0
- package/dist/wire-token-search-fields.js.map +1 -0
- package/dist/workbench.d.ts +33 -0
- package/dist/workbench.js +17 -0
- package/dist/workbench.js.map +1 -0
- package/docs/accessibility.md +101 -3
- package/docs/app-layouts.md +61 -0
- package/docs/catalog.md +155 -0
- package/docs/collapsible-panel.md +102 -0
- package/docs/component-contract.md +169 -4
- package/docs/component-selection.md +185 -0
- package/docs/design/templates/panel-header/icon-actions-dark.svg +1 -0
- package/docs/design/templates/panel-header/icon-actions.svg +1 -0
- package/docs/design/templates/panel-header/icon-summary-actions-dark.svg +1 -0
- package/docs/design/templates/panel-header/icon-summary-actions.svg +1 -0
- package/docs/design/templates/panel-header/no-icon-dark.svg +1 -0
- package/docs/design/templates/panel-header/no-icon.svg +1 -0
- package/docs/design/templates/panel-header/page-heading-dark.svg +1 -0
- package/docs/design/templates/panel-header/page-heading.svg +1 -0
- package/docs/design/templates/panel-header/title-only-dark.svg +1 -0
- package/docs/design/templates/panel-header/title-only.svg +1 -0
- package/docs/design/templates/panel-header-dark.svg +13 -0
- package/docs/design/templates/panel-header.svg +13 -0
- package/docs/design/templates/toolbar-control-group/borderless-single-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/borderless-single.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-buttons-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/icon-buttons.svg +1 -0
- package/docs/design/templates/toolbar-control-group/push-buttons-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/push-buttons.svg +1 -0
- package/docs/design/templates/toolbar-control-group/with-text-dark.svg +1 -0
- package/docs/design/templates/toolbar-control-group/with-text.svg +1 -0
- package/docs/design/templates/toolbar-control-group-dark.svg +11 -0
- package/docs/design/templates/toolbar-control-group.svg +11 -0
- package/docs/design/templates.md +83 -0
- package/docs/design-philosophy.md +61 -1
- package/docs/device-class.md +54 -0
- package/docs/examples/command-palette-adapter.tsx +70 -0
- package/docs/layout.md +131 -0
- package/docs/nav-stack.md +47 -0
- package/docs/recipes.md +167 -0
- package/docs/split-view.md +49 -0
- package/docs/tab-scaffold.md +41 -0
- package/docs/ux-demo.md +23 -9
- package/docs/webawesome-theme.md +15 -0
- package/docs/workbench.md +47 -0
- package/llms.txt +123 -14
- package/package.json +139 -44
- package/ux-demo/recipes/app-shell.tsx +32 -0
- package/ux-demo/recipes/collapsible-sidebar.tsx +100 -0
- package/ux-demo/recipes/compact-toolbar.tsx +43 -0
- package/ux-demo/recipes/composer-form.tsx +63 -0
- package/ux-demo/recipes/list-detail-dialog.tsx +45 -0
- package/ux-demo/recipes/list-workspace-states.tsx +26 -0
- package/ux-demo/recipes/loaders.ts +33 -0
- package/ux-demo/recipes/loading-inspector.tsx +81 -0
- package/ux-demo/recipes/mount-recipe.ts +43 -0
- package/ux-demo/recipes/navigation-sidebar.tsx +26 -0
- package/ux-demo/recipes/navigation-stack.tsx +76 -0
- package/ux-demo/recipes/recipes.css +448 -0
- package/ux-demo/recipes/types.ts +17 -0
- package/ux-demo/recipes/workspace-header.tsx +12 -0
- package/dist/browser/dialog-header.js +0 -3
- package/dist/browser/menu-header.js +0 -3
- package/dist/browser/menu-item.js +0 -3
- package/dist/browser/page-header.js +0 -3
- package/dist/chunk-2PES33HS.js +0 -13
- package/dist/chunk-2PES33HS.js.map +0 -1
- package/dist/chunk-3O7ZEUW2.js.map +0 -1
- package/dist/chunk-723YNJBH.js +0 -17
- package/dist/chunk-723YNJBH.js.map +0 -1
- package/dist/chunk-GY5WH7TO.js +0 -10
- package/dist/chunk-GY5WH7TO.js.map +0 -1
- package/dist/chunk-MBR3TYNL.js.map +0 -1
- package/dist/chunk-MQIZAD4A.js.map +0 -1
- package/dist/chunk-NHXX4JI5.js +0 -28
- package/dist/chunk-NHXX4JI5.js.map +0 -1
- package/dist/chunk-OPCJRCYO.js +0 -19
- package/dist/chunk-OPCJRCYO.js.map +0 -1
- package/dist/chunk-T5XNMVOI.js +0 -24
- package/dist/chunk-T5XNMVOI.js.map +0 -1
- package/dist/chunk-TRNBHPNW.js.map +0 -1
- package/dist/chunk-VHW5YNIB.js +0 -17
- package/dist/chunk-VHW5YNIB.js.map +0 -1
- package/dist/chunk-VKH2JLDZ.js +0 -10
- package/dist/chunk-VKH2JLDZ.js.map +0 -1
- package/dist/chunk-WY4CAEPG.js +0 -14
- package/dist/chunk-WY4CAEPG.js.map +0 -1
- package/dist/dialog-header.d.ts +0 -14
- package/dist/dialog-header.js +0 -3
- package/dist/dialog-header.js.map +0 -1
- package/dist/menu-header.d.ts +0 -15
- package/dist/menu-header.js +0 -3
- package/dist/menu-header.js.map +0 -1
- package/dist/menu-item.d.ts +0 -22
- package/dist/menu-item.js +0 -3
- package/dist/menu-item.js.map +0 -1
- package/dist/page-header.d.ts +0 -9
- package/dist/page-header.js +0 -3
- package/dist/page-header.js.map +0 -1
- package/src/app-tab.css +0 -20
- package/src/dialog-header.css +0 -9
- package/src/empty-state.css +0 -6
- package/src/foundation.css +0 -106
- package/src/loading-spinner.css +0 -4
- package/src/lucide-icon.css +0 -1
- package/src/menu-header.css +0 -13
- package/src/menu-item.css +0 -12
- package/src/page-header.css +0 -3
- package/src/resizable-region.css +0 -19
- package/src/select.css +0 -18
- package/src/state-banner.css +0 -31
- package/src/styles.css +0 -18
- package/src/tab-bar.css +0 -4
- package/src/toolbar-control-group.css +0 -25
- package/src/toolbar-text.css +0 -4
- package/src/toolbar.css +0 -9
- package/src/value-table.css +0 -5
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import '@kerfjs/ui/layout.css';
|
|
2
|
+
import '@kerfjs/ui/select/register';
|
|
3
|
+
import './recipes.css';
|
|
4
|
+
|
|
5
|
+
import { LucideIcon } from '@kerfjs/ui/lucide-icon';
|
|
6
|
+
import { SegmentedControl } from '@kerfjs/ui/segmented-control';
|
|
7
|
+
import { Select } from '@kerfjs/ui/select';
|
|
8
|
+
import { Toolbar } from '@kerfjs/ui/toolbar';
|
|
9
|
+
import { ToolbarControlGroup } from '@kerfjs/ui/toolbar-control-group';
|
|
10
|
+
import { ToolbarText } from '@kerfjs/ui/toolbar-text';
|
|
11
|
+
import { signal } from 'kerfjs';
|
|
12
|
+
import { Filter, MoreHorizontal, RefreshCw } from 'lucide';
|
|
13
|
+
|
|
14
|
+
import type { RecipeFactory } from './types.js';
|
|
15
|
+
|
|
16
|
+
export const createRecipe: RecipeFactory = (announce) => {
|
|
17
|
+
const view = signal('list');
|
|
18
|
+
const sort = signal('updated');
|
|
19
|
+
const filtered = signal(false);
|
|
20
|
+
const render = () => <section class="kui-recipe recipe-compact-toolbar kui-recipe__surface kui-layout" data-recipe="recipe-compact-toolbar">
|
|
21
|
+
<Toolbar
|
|
22
|
+
label="Task workspace controls"
|
|
23
|
+
leading={<ToolbarControlGroup appearance="borderless" single><ToolbarText text="Tasks" size="small" /></ToolbarControlGroup>}
|
|
24
|
+
center={<ToolbarControlGroup><SegmentedControl id="recipe-view" label="View" value={view.value} action="recipe-action" appearance="toolbar" shape="pill" size="small" choices={[{ value: 'list', label: 'List' }, { value: 'board', label: 'Board' }, { value: 'timeline', label: 'Timeline' }]} /></ToolbarControlGroup>}
|
|
25
|
+
trailing={<div class="kui-control-cluster"><Select name="recipe-sort" value={sort.value} ariaLabel="Sort tasks" fitMenu choices={[{ value: 'updated', label: 'Recently updated' }, { value: 'priority', label: 'Priority' }, { value: 'owner', label: 'Owner' }, { value: 'created', label: 'Created date' }]} /><ToolbarControlGroup label="Task actions" buttonAppearance="push"><button type="button" aria-label="Toggle filters" aria-pressed={String(filtered.value)} data-action="recipe-action" data-recipe-command="filter"><LucideIcon icon={Filter} name="filter" /></button><button type="button" aria-label="Refresh tasks" data-action="recipe-action" data-recipe-command="refresh"><LucideIcon icon={RefreshCw} name="refresh-cw" /></button></ToolbarControlGroup><button class="kui-recipe__button" type="button" aria-label="More task actions" data-action="recipe-action" data-recipe-command="more"><LucideIcon icon={MoreHorizontal} name="more-horizontal" /></button></div>}
|
|
26
|
+
/>
|
|
27
|
+
<div class="recipe-compact-toolbar__guide kui-content-item"><div><strong>ToolbarControlGroup</strong><span class="kui-recipe__muted">Related commands or pressed tools.</span></div><div><strong>SegmentedControl</strong><span class="kui-recipe__muted">A few visible exclusive choices.</span></div><div><strong>Select</strong><span class="kui-recipe__muted">A longer controlled value list.</span></div><div><strong>Ordinary button</strong><span class="kui-recipe__muted">One independent command.</span></div></div>
|
|
28
|
+
<p class="kui-recipe__ownership kui-content-item">The recipe owns control semantics and wrapping. The app owns values, actions, persistence, and responsive priority.</p>
|
|
29
|
+
</section>;
|
|
30
|
+
return {
|
|
31
|
+
render,
|
|
32
|
+
action(command, element) {
|
|
33
|
+
const value = element.dataset.segmentValue;
|
|
34
|
+
if (value) view.value = value;
|
|
35
|
+
if (command === 'filter') filtered.value = !filtered.value;
|
|
36
|
+
announce(value ? `View: ${value}` : command === 'filter' ? `Filters ${filtered.value ? 'on' : 'off'}` : `${command} requested`);
|
|
37
|
+
},
|
|
38
|
+
change(element) {
|
|
39
|
+
const value = (element as HTMLElement & { value?: string }).value;
|
|
40
|
+
if (element.getAttribute('name') === 'recipe-sort' && value) { sort.value = value; announce(`Sort: ${value}`); }
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import '@kerfjs/ui/layout.css';
|
|
2
|
+
import '@kerfjs/ui/select/register';
|
|
3
|
+
import '@kerfjs/ui/webawesome.css';
|
|
4
|
+
import '@awesome.me/webawesome/dist/components/button/button.js';
|
|
5
|
+
import '@awesome.me/webawesome/dist/components/input/input.js';
|
|
6
|
+
import '@awesome.me/webawesome/dist/components/textarea/textarea.js';
|
|
7
|
+
import './recipes.css';
|
|
8
|
+
|
|
9
|
+
import { PanelHeader } from '@kerfjs/ui/panel-header';
|
|
10
|
+
import { Select } from '@kerfjs/ui/select';
|
|
11
|
+
import { StateBanner } from '@kerfjs/ui/state-banner';
|
|
12
|
+
import { signal } from 'kerfjs';
|
|
13
|
+
|
|
14
|
+
import type { RecipeFactory } from './types.js';
|
|
15
|
+
|
|
16
|
+
type ValueField = HTMLElement & { value: string };
|
|
17
|
+
|
|
18
|
+
const syncControlledFieldValues = (action: HTMLElement, values: Record<string, string>) => {
|
|
19
|
+
const form = action.closest<HTMLElement>('[data-recipe="recipe-composer-form"]');
|
|
20
|
+
if (!form) return;
|
|
21
|
+
for (const [name, value] of Object.entries(values)) {
|
|
22
|
+
const field = form.querySelector<ValueField>(`[name="${name}"]`);
|
|
23
|
+
if (field && field.value !== value) field.value = value;
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
export const createRecipe: RecipeFactory = (announce) => {
|
|
28
|
+
const title = signal('');
|
|
29
|
+
const body = signal('The tablet layout now keeps navigation, content, and inspector focus order aligned.');
|
|
30
|
+
const audience = signal('team');
|
|
31
|
+
const status = signal<'idle' | 'error' | 'saved'>('idle');
|
|
32
|
+
const render = () => <form class="kui-recipe recipe-form kui-recipe__surface kui-content" data-recipe="recipe-composer-form" aria-labelledby="recipe-composer-title" aria-describedby="recipe-composer-summary" noValidate>
|
|
33
|
+
<PanelHeader title="Publish workspace update" titleId="recipe-composer-title" summary="Share a concise, actionable update with collaborators." summaryId="recipe-composer-summary" />
|
|
34
|
+
{status.value === 'error' && <StateBanner title="Add a title before publishing" detail="The update body and audience are preserved." tone="danger" urgency="alert" />}
|
|
35
|
+
{status.value === 'saved' && <StateBanner title="Update published" detail="The team audience can now read it." tone="success" />}
|
|
36
|
+
<div class="recipe-form__section recipe-form__fields">
|
|
37
|
+
<wa-input name="recipe-title" label="Update title" hint="Summarize the outcome in one line." required value={title.value}></wa-input>
|
|
38
|
+
<wa-textarea name="recipe-body" label="Details" hint="Include decisions, owners, and the next checkpoint." rows="5" maxlength="400" with-count value={body.value}></wa-textarea>
|
|
39
|
+
<Select name="recipe-audience" value={audience.value} label="Audience" choices={[{ value: 'team', label: 'Workspace team' }, { value: 'reviewers', label: 'Reviewers' }, { value: 'organization', label: 'Entire organization' }]} />
|
|
40
|
+
</div>
|
|
41
|
+
<footer class="recipe-form__section recipe-form__footer"><div class="recipe-form__actions kui-control-cluster"><wa-button appearance="outlined" data-action="recipe-action" data-recipe-command="reset">Reset</wa-button><wa-button variant="brand" appearance="accent" data-action="recipe-action" data-recipe-command="submit">Publish update</wa-button></div><p class="kui-recipe__ownership">The recipe owns field, message, and action rhythm. The app owns validation rules, draft persistence, permissions, and transport.</p></footer>
|
|
42
|
+
</form>;
|
|
43
|
+
return {
|
|
44
|
+
render,
|
|
45
|
+
action(command, element) {
|
|
46
|
+
if (command === 'reset') {
|
|
47
|
+
title.value = '';
|
|
48
|
+
body.value = '';
|
|
49
|
+
status.value = 'idle';
|
|
50
|
+
syncControlledFieldValues(element, { 'recipe-title': title.value, 'recipe-body': body.value });
|
|
51
|
+
announce('Draft reset');
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (command === 'submit') { status.value = title.value.trim() ? 'saved' : 'error'; announce(status.value === 'saved' ? 'Update published' : 'Title required'); }
|
|
55
|
+
},
|
|
56
|
+
change(element) {
|
|
57
|
+
const value = (element as HTMLElement & { value?: string | null }).value ?? '';
|
|
58
|
+
if (element.getAttribute('name') === 'recipe-title') title.value = value;
|
|
59
|
+
if (element.getAttribute('name') === 'recipe-body') body.value = value;
|
|
60
|
+
if (element.getAttribute('name') === 'recipe-audience') audience.value = value;
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { ListHeader } from '@kerfjs/ui/list-header';
|
|
8
|
+
import { ListItem } from '@kerfjs/ui/list-item';
|
|
9
|
+
import { LucideIcon } from '@kerfjs/ui/lucide-icon';
|
|
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-list-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/list-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"><div class="recipe-list-detail kui-pane__content"><nav class="recipe-list-detail__list kui-pane" aria-label="Projects"><ListHeader label="Recent projects" /><div class="kui-pane__content kui-content"><section>{Object.entries(records).map(([id, record]) => <ListItem action="recipe-action" itemId={id} label={record.name} selected={selected.value === id} multiline />)}</section></div></nav><section class="recipe-list-detail__detail 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-list-detail__body kui-pane__content kui-content" aria-live="polite"><h3 class="recipe-list-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-list-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></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
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import '@kerfjs/ui/layout.css';
|
|
2
|
+
import './recipes.css';
|
|
3
|
+
|
|
4
|
+
import { EmptyState } from '@kerfjs/ui/empty-state';
|
|
5
|
+
import { LoadingSpinner } from '@kerfjs/ui/loading-spinner';
|
|
6
|
+
import { PanelHeader } from '@kerfjs/ui/panel-header';
|
|
7
|
+
import { StateBanner } from '@kerfjs/ui/state-banner';
|
|
8
|
+
import { signal } from 'kerfjs';
|
|
9
|
+
|
|
10
|
+
import type { RecipeFactory } from './types.js';
|
|
11
|
+
|
|
12
|
+
type ListState = 'loading' | 'empty' | 'populated' | 'stale' | 'error';
|
|
13
|
+
const items = ['Audit keyboard focus order', 'Verify 200% zoom layout', 'Publish package migration notes'];
|
|
14
|
+
|
|
15
|
+
export const createRecipe: RecipeFactory = (announce) => {
|
|
16
|
+
const state = signal<ListState>('loading');
|
|
17
|
+
const populated = () => <ul class="recipe-list__items" aria-label="Release tasks">{items.map((item, index) => <li><strong>{item}</strong><span class="kui-recipe__muted">{index === 0 ? 'In review · Mara' : index === 1 ? 'Ready · Sam' : 'Draft · Inez'}</span></li>)}</ul>;
|
|
18
|
+
const renderBody = () => {
|
|
19
|
+
if (state.value === 'loading') return <EmptyState title="Loading release tasks" detail="The current workspace will appear when data is ready." busy action={<button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="load">Complete load</button>} />;
|
|
20
|
+
if (state.value === 'empty') return <EmptyState title="No release tasks" detail="Create the first task for this milestone." action={<button class="kui-recipe__button" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="create">Create task</button>} />;
|
|
21
|
+
if (state.value === 'error') return <StateBanner title="Release tasks could not be refreshed" detail="Existing filters are preserved. Try again when the connection recovers." tone="danger" urgency="alert" action={<button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="retry">Retry</button>} />;
|
|
22
|
+
return <>{state.value === 'stale' && <StateBanner title="Showing saved results" detail="Refreshing in the background." tone="warning" action={<span class="kui-inline-metadata"><LoadingSpinner label="Refreshing release tasks" /><button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="finish">Finish refresh</button></span>} />}{populated()}</>;
|
|
23
|
+
};
|
|
24
|
+
const render = () => <section class="kui-recipe recipe-list kui-recipe__surface kui-pane" data-recipe="recipe-list-workspace-states" data-list-state={state.value}><PanelHeader title="Release tasks" titleId="recipe-list-title" actions={<div class="kui-control-cluster"><button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="empty">Clear</button><button class="kui-recipe__button" type="button" data-action="recipe-action" data-recipe-command="fail">Simulate failure</button><button class="kui-recipe__button" data-primary="true" type="button" data-action="recipe-action" data-recipe-command="refresh">Refresh</button></div>} /><div class="recipe-list__body kui-pane__content kui-content">{renderBody()}<p class="kui-recipe__ownership kui-content-item">The recipe owns feedback placement and stable content. The app owns fetching, cache age, retry policy, and domain rows.</p></div></section>;
|
|
25
|
+
return { render, action(command) { const next: Record<string, ListState> = { load: 'populated', empty: 'empty', create: 'populated', fail: 'error', retry: 'populated', refresh: 'stale', finish: 'populated' }; if (command in next) state.value = next[command]!; announce(`List state: ${state.value}`); } };
|
|
26
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { RecipeFactory } from './types.js';
|
|
2
|
+
|
|
3
|
+
export const recipeIds = [
|
|
4
|
+
'recipe-app-shell',
|
|
5
|
+
'recipe-navigation-sidebar',
|
|
6
|
+
'recipe-workspace-header',
|
|
7
|
+
'recipe-list-detail-dialog',
|
|
8
|
+
'recipe-composer-form',
|
|
9
|
+
'recipe-list-workspace-states',
|
|
10
|
+
'recipe-compact-toolbar',
|
|
11
|
+
'recipe-navigation-stack',
|
|
12
|
+
'recipe-loading-inspector',
|
|
13
|
+
'recipe-collapsible-sidebar',
|
|
14
|
+
] as const;
|
|
15
|
+
|
|
16
|
+
export type RecipeId = typeof recipeIds[number];
|
|
17
|
+
|
|
18
|
+
export const recipeLoaders: Record<RecipeId, () => Promise<{ createRecipe: RecipeFactory }>> = {
|
|
19
|
+
'recipe-app-shell': () => import('./app-shell.js'),
|
|
20
|
+
'recipe-navigation-sidebar': () => import('./navigation-sidebar.js'),
|
|
21
|
+
'recipe-workspace-header': () => import('./workspace-header.js'),
|
|
22
|
+
'recipe-list-detail-dialog': () => import('./list-detail-dialog.js'),
|
|
23
|
+
'recipe-composer-form': () => import('./composer-form.js'),
|
|
24
|
+
'recipe-list-workspace-states': () => import('./list-workspace-states.js'),
|
|
25
|
+
'recipe-compact-toolbar': () => import('./compact-toolbar.js'),
|
|
26
|
+
'recipe-navigation-stack': () => import('./navigation-stack.js'),
|
|
27
|
+
'recipe-loading-inspector': () => import('./loading-inspector.js'),
|
|
28
|
+
'recipe-collapsible-sidebar': () => import('./collapsible-sidebar.js'),
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function isRecipeId(value: string): value is RecipeId {
|
|
32
|
+
return (recipeIds as readonly string[]).includes(value);
|
|
33
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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
|
+
<div class="recipe-inspector__controls">
|
|
50
|
+
<Select name="recipe-inspector-status" value="review" label="Status" placeholder={p} choices={[
|
|
51
|
+
{ value: 'review', label: 'In review' },
|
|
52
|
+
{ value: 'ready', label: 'Ready' },
|
|
53
|
+
{ value: 'done', label: 'Done' },
|
|
54
|
+
]} />
|
|
55
|
+
<SegmentedControl id="recipe-inspector-view" label="Inspector view" value="details" appearance="toolbar" shape="pill" size="small" placeholder={p} choices={[
|
|
56
|
+
{ value: 'details', label: 'Details', content: icon(List, 'list') },
|
|
57
|
+
{ value: 'activity', label: 'Activity', content: icon(Columns3, 'columns-3') },
|
|
58
|
+
{ value: 'files', label: 'Files', content: icon(FileText, 'file-text') },
|
|
59
|
+
]} />
|
|
60
|
+
</div>
|
|
61
|
+
<section>
|
|
62
|
+
<ListItem action="recipe-action" itemId="reassign" label="Reassign ticket" icon={icon(Inbox, 'inbox')} placeholder={p} />
|
|
63
|
+
<ListItem action="recipe-action" itemId="watch" label="Watch for changes" placeholder={p} />
|
|
64
|
+
</section>
|
|
65
|
+
{p
|
|
66
|
+
? <StateBanner tone="info" title="" detail="" icon={icon(FileText, 'file-text')} placeholder />
|
|
67
|
+
: <StateBanner tone="success" title="Up to date" detail="All checks passed on the latest revision." icon={icon(FileText, 'file-text')} />}
|
|
68
|
+
<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>
|
|
69
|
+
</div>
|
|
70
|
+
</section>;
|
|
71
|
+
};
|
|
72
|
+
return {
|
|
73
|
+
render,
|
|
74
|
+
action(command) {
|
|
75
|
+
if (command === 'toggle') {
|
|
76
|
+
loading.value = !loading.value;
|
|
77
|
+
announce(loading.value ? 'Inspector is loading' : 'Inspector loaded');
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { wireNavStack } from '@kerfjs/ui/wire-nav-stack';
|
|
2
|
+
import { wireResizableRegions } from '@kerfjs/ui/wire-resizable-regions';
|
|
3
|
+
import { delegate, mount } from 'kerfjs';
|
|
4
|
+
import { delegateActions } from 'kerfjs/actions';
|
|
5
|
+
|
|
6
|
+
import type { RecipeController } from './types.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Copyable application boundary for a recipe. The caller owns one stable root
|
|
10
|
+
* and retains the returned disposer for that root's lifetime.
|
|
11
|
+
*/
|
|
12
|
+
export function mountRecipe(root: HTMLElement, controller: RecipeController): () => void {
|
|
13
|
+
const stopMount = mount(root, controller.render);
|
|
14
|
+
const stopActions = delegateActions(root, 'click', {
|
|
15
|
+
'recipe-action': (_event, element) => {
|
|
16
|
+
const target = element as HTMLElement;
|
|
17
|
+
controller.action(target.dataset.recipeCommand ?? '', target);
|
|
18
|
+
},
|
|
19
|
+
});
|
|
20
|
+
const dispatchChange = (_event: Event, element: Element) => controller.change?.(element as HTMLElement);
|
|
21
|
+
const stopChanges = delegate(root, 'change', 'wa-select, wa-input, wa-textarea', dispatchChange);
|
|
22
|
+
const stopInputs = delegate(root, 'input', 'wa-input, wa-textarea', dispatchChange);
|
|
23
|
+
const stopDialogs = delegate(root, 'wa-after-hide', 'wa-dialog', (_event, element) => controller.afterHide?.(element as HTMLElement));
|
|
24
|
+
const stopResize = wireResizableRegions(root, {
|
|
25
|
+
onCommit: ({ id, size }) => controller.resize?.(id, size),
|
|
26
|
+
});
|
|
27
|
+
const stopNav = wireNavStack(root, { onBack: () => controller.action('nav-back', root) });
|
|
28
|
+
const stopWire = controller.wire?.(root);
|
|
29
|
+
|
|
30
|
+
let disposed = false;
|
|
31
|
+
return () => {
|
|
32
|
+
if (disposed) return;
|
|
33
|
+
disposed = true;
|
|
34
|
+
stopWire?.();
|
|
35
|
+
stopNav();
|
|
36
|
+
stopResize();
|
|
37
|
+
stopDialogs();
|
|
38
|
+
stopInputs();
|
|
39
|
+
stopChanges();
|
|
40
|
+
stopActions();
|
|
41
|
+
stopMount();
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import '@kerfjs/ui/layout.css';
|
|
2
|
+
import './recipes.css';
|
|
3
|
+
|
|
4
|
+
import { ListHeader } from '@kerfjs/ui/list-header';
|
|
5
|
+
import { ListItem } from '@kerfjs/ui/list-item';
|
|
6
|
+
import { LucideIcon } from '@kerfjs/ui/lucide-icon';
|
|
7
|
+
import { Toolbar } from '@kerfjs/ui/toolbar';
|
|
8
|
+
import { ToolbarControlGroup } from '@kerfjs/ui/toolbar-control-group';
|
|
9
|
+
import { signal } from 'kerfjs';
|
|
10
|
+
import { Folder, Inbox, Plus, Settings, Users } from 'lucide';
|
|
11
|
+
|
|
12
|
+
import type { RecipeFactory } from './types.js';
|
|
13
|
+
|
|
14
|
+
export const createRecipe: RecipeFactory = (announce) => {
|
|
15
|
+
const selected = signal('inbox');
|
|
16
|
+
const expanded = signal(true);
|
|
17
|
+
const render = () => <aside class="kui-recipe recipe-sidebar kui-recipe__surface kui-pane" data-recipe="recipe-navigation-sidebar">
|
|
18
|
+
<div class="recipe-sidebar__body kui-pane__content kui-content">
|
|
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
|
+
<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
|
+
</div>
|
|
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
|
+
</aside>;
|
|
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
|
+
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import '@kerfjs/ui/layout.css';
|
|
2
|
+
import '@kerfjs/ui/nav-stack.css';
|
|
3
|
+
import '@kerfjs/ui/toolbar-text.css';
|
|
4
|
+
import './recipes.css';
|
|
5
|
+
|
|
6
|
+
import { ListHeader } from '@kerfjs/ui/list-header';
|
|
7
|
+
import { ListItem } from '@kerfjs/ui/list-item';
|
|
8
|
+
import { LucideIcon } from '@kerfjs/ui/lucide-icon';
|
|
9
|
+
import { NavStack, type NavStackView } from '@kerfjs/ui/nav-stack';
|
|
10
|
+
import { signal } from 'kerfjs';
|
|
11
|
+
import { ChevronRight, FileText, Folder } from 'lucide';
|
|
12
|
+
|
|
13
|
+
import type { RecipeFactory } from './types.js';
|
|
14
|
+
|
|
15
|
+
interface LibraryItem {
|
|
16
|
+
id: string;
|
|
17
|
+
label: string;
|
|
18
|
+
detail: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const ITEMS: LibraryItem[] = [
|
|
22
|
+
{ id: 'tokens', label: 'Design tokens', detail: 'Semantic color, spacing, and radius scales shared across every surface.' },
|
|
23
|
+
{ id: 'controls', label: 'Toolbar controls', detail: 'Control groups and segmented controls with their hover and pressed contracts.' },
|
|
24
|
+
{ id: 'layouts', label: 'App layouts', detail: 'Navigation stacks, split views, workbench, and bottom-tab scaffolds.' },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export const createRecipe: RecipeFactory = (announce) => {
|
|
28
|
+
const listView = (): NavStackView => ({
|
|
29
|
+
key: 'library',
|
|
30
|
+
title: 'Library',
|
|
31
|
+
content: <div class="recipe-navstack__view kui-pane__content kui-content">
|
|
32
|
+
<section>
|
|
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
|
+
</section>
|
|
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
|
+
</div>,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const detailView = (item: LibraryItem): NavStackView => ({
|
|
41
|
+
key: item.id,
|
|
42
|
+
title: item.label,
|
|
43
|
+
content: <div class="recipe-navstack__view kui-pane__content kui-content">
|
|
44
|
+
<div class="kui-content-item">
|
|
45
|
+
<LucideIcon icon={FileText} name="file-text" />
|
|
46
|
+
<strong>{item.label}</strong>
|
|
47
|
+
<p class="kui-recipe__muted">{item.detail}</p>
|
|
48
|
+
</div>
|
|
49
|
+
</div>,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const views = signal<NavStackView[]>([listView()]);
|
|
53
|
+
|
|
54
|
+
const render = () => <section class="kui-recipe recipe-navstack kui-recipe__surface" data-recipe="recipe-navigation-stack">
|
|
55
|
+
<div class="recipe-navstack__stage"><NavStack id="recipe-nav" label="Component library" views={views.value} /></div>
|
|
56
|
+
</section>;
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
render,
|
|
60
|
+
action(command, element) {
|
|
61
|
+
if (command === 'nav-back') {
|
|
62
|
+
if (views.value.length > 1) {
|
|
63
|
+
views.value = views.value.slice(0, -1);
|
|
64
|
+
announce('Navigated back');
|
|
65
|
+
}
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const id = element.dataset.itemId;
|
|
69
|
+
const item = ITEMS.find((candidate) => candidate.id === id);
|
|
70
|
+
if (item) {
|
|
71
|
+
views.value = [...views.value, detailView(item)];
|
|
72
|
+
announce(`Opened ${item.label}`);
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|