@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.
- package/README.md +20 -20
- package/ai/component-catalog.json +130 -83
- package/ai/public-api-signatures-v1.md +200 -29
- package/ai/skill.md +12 -12
- package/ai/webawesome-jsx-signatures-v1.md +1 -1
- package/dist/browser/list-action-row.js +4 -0
- package/dist/browser/{menu-header.js → list-header.js} +2 -2
- package/dist/browser/{menu-item.js → list-item.js} +2 -2
- package/dist/catalog.d.ts +119 -0
- package/dist/catalog.js +128 -0
- package/dist/catalog.js.map +1 -0
- package/dist/{chunk-TZNCCM5H.js → chunk-2TS7ZSYX.js} +4 -4
- package/dist/chunk-2TS7ZSYX.js.map +1 -0
- package/dist/{chunk-PONXLV5U.js → chunk-37T3VNIZ.js} +1 -1
- package/dist/{chunk-PONXLV5U.js.map → chunk-37T3VNIZ.js.map} +1 -1
- package/dist/{chunk-3YD6ZNRA.js → chunk-7JB7VPRI.js} +8 -8
- package/dist/{chunk-3YD6ZNRA.js.map → chunk-7JB7VPRI.js.map} +1 -1
- package/dist/{chunk-N3SUXLLS.js → chunk-BLK3UEEI.js} +2 -2
- package/dist/{chunk-N3SUXLLS.js.map → chunk-BLK3UEEI.js.map} +1 -1
- package/dist/{chunk-YKUUZPED.js → chunk-L4OKFDJY.js} +7 -7
- package/dist/{chunk-YKUUZPED.js.map → chunk-L4OKFDJY.js.map} +1 -1
- package/dist/{chunk-KJYXOTG3.js → chunk-RUNWEPR4.js} +3 -3
- package/dist/chunk-RUNWEPR4.js.map +1 -0
- package/dist/{chunk-H6URQGJI.js → chunk-W5EQ3HYQ.js} +11 -11
- package/dist/{chunk-H6URQGJI.js.map → chunk-W5EQ3HYQ.js.map} +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +11 -11
- package/dist/{menu-action-row.d.ts → list-action-row.d.ts} +7 -7
- package/dist/list-action-row.js +5 -0
- package/dist/list-action-row.js.map +1 -0
- package/dist/{menu-header.d.ts → list-header.d.ts} +9 -9
- package/dist/list-header.js +7 -0
- package/dist/list-header.js.map +1 -0
- package/dist/{menu-item.d.ts → list-item.d.ts} +5 -5
- package/dist/list-item.js +5 -0
- package/dist/list-item.js.map +1 -0
- package/dist/nav-stack.js +1 -1
- package/dist/panel-header.d.ts +9 -1
- package/dist/panel-header.js +1 -1
- package/dist/split-view.d.ts +1 -1
- package/dist/split-view.js +1 -1
- package/dist/split-view.js.map +1 -1
- package/dist/styles/catalog.css +482 -0
- package/dist/styles/list-action-row.css +160 -0
- package/dist/styles/{menu-header.css → list-header.css} +33 -33
- package/dist/styles/list-item.css +123 -0
- package/dist/styles/styles.css +4 -3
- package/dist/toolbar-text.d.ts +10 -2
- package/dist/toolbar-text.js +1 -1
- package/dist/value-table.js +1 -1
- package/dist/wire-catalog.d.ts +26 -0
- package/dist/wire-catalog.js +50 -0
- package/dist/wire-catalog.js.map +1 -0
- package/docs/accessibility.md +15 -10
- package/docs/catalog.md +154 -0
- package/docs/component-contract.md +12 -12
- package/docs/component-selection.md +14 -13
- package/docs/design-philosophy.md +4 -4
- package/docs/layout.md +8 -8
- package/docs/recipes.md +17 -5
- package/docs/split-view.md +1 -1
- package/docs/ux-demo.md +5 -5
- package/llms.txt +12 -8
- package/package.json +25 -16
- package/ux-demo/recipes/app-shell.tsx +3 -3
- package/ux-demo/recipes/list-detail-dialog.tsx +45 -0
- package/ux-demo/recipes/loaders.ts +4 -2
- package/ux-demo/recipes/loading-inspector.tsx +79 -0
- package/ux-demo/recipes/navigation-sidebar.tsx +5 -5
- package/ux-demo/recipes/navigation-stack.tsx +4 -4
- package/ux-demo/recipes/recipes.css +13 -7
- package/dist/browser/menu-action-row.js +0 -4
- package/dist/chunk-KJYXOTG3.js.map +0 -1
- package/dist/chunk-TZNCCM5H.js.map +0 -1
- package/dist/menu-action-row.js +0 -5
- package/dist/menu-action-row.js.map +0 -1
- package/dist/menu-header.js +0 -7
- package/dist/menu-header.js.map +0 -1
- package/dist/menu-item.js +0 -5
- package/dist/menu-item.js.map +0 -1
- package/dist/styles/menu-action-row.css +0 -160
- package/dist/styles/menu-item.css +0 -123
- package/ux-demo/recipes/master-detail-dialog.tsx +0 -45
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
.kui-
|
|
2
|
-
--kui-
|
|
3
|
-
--kui-
|
|
4
|
-
--_kui-
|
|
5
|
-
--kui-
|
|
1
|
+
.kui-list-header {
|
|
2
|
+
--kui-list-header-color: var(--kui-color-neutral-on-quiet);
|
|
3
|
+
--kui-list-header-hover-background: var(--kui-color-neutral-border-quiet);
|
|
4
|
+
--_kui-list-header-action-icon-size: var(
|
|
5
|
+
--kui-list-header-action-icon-size,
|
|
6
6
|
1.125rem
|
|
7
7
|
);
|
|
8
8
|
display: flex;
|
|
@@ -12,14 +12,14 @@
|
|
|
12
12
|
margin-inline: var(--kui-layout-inline-margin, 0.5rem);
|
|
13
13
|
padding: 0;
|
|
14
14
|
box-sizing: border-box;
|
|
15
|
-
color: var(--kui-
|
|
15
|
+
color: var(--kui-list-header-color);
|
|
16
16
|
background: transparent;
|
|
17
17
|
align-items: stretch;
|
|
18
18
|
gap: var(--kui-layout-item-gap, 0.5rem);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
.kui-
|
|
22
|
-
.kui-
|
|
21
|
+
.kui-list-header__title,
|
|
22
|
+
.kui-list-header__action {
|
|
23
23
|
box-sizing: border-box;
|
|
24
24
|
border: 1px solid transparent;
|
|
25
25
|
border-radius: var(--kui-layout-rounded-radius, calc(1px + 0.6875rem));
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
background: transparent;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
.kui-
|
|
30
|
+
.kui-list-header__title {
|
|
31
31
|
display: flex;
|
|
32
32
|
min-width: 0;
|
|
33
33
|
min-height: 2.75rem;
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
text-align: start;
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
-
.kui-
|
|
42
|
+
.kui-list-header__label {
|
|
43
43
|
min-width: 0;
|
|
44
44
|
margin: 0;
|
|
45
45
|
overflow: hidden;
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
white-space: nowrap;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
-
.kui-
|
|
54
|
+
.kui-list-header__badge {
|
|
55
55
|
display: inline-flex;
|
|
56
56
|
min-height: 1.5rem;
|
|
57
57
|
padding-inline: var(--kui-layout-item-padding, 0.5rem);
|
|
@@ -66,7 +66,7 @@
|
|
|
66
66
|
line-height: 1;
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
.kui-
|
|
69
|
+
.kui-list-header__count {
|
|
70
70
|
display: inline-grid;
|
|
71
71
|
min-width: 1.35rem;
|
|
72
72
|
height: 1.35rem;
|
|
@@ -78,7 +78,7 @@
|
|
|
78
78
|
place-items: center;
|
|
79
79
|
flex: none;
|
|
80
80
|
font-size: var(
|
|
81
|
-
--kui-
|
|
81
|
+
--kui-list-header-count-font-size,
|
|
82
82
|
var(--wa-font-size-3xs, 0.625rem)
|
|
83
83
|
);
|
|
84
84
|
font-weight: 700;
|
|
@@ -86,26 +86,26 @@
|
|
|
86
86
|
line-height: 1;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
|
-
.kui-
|
|
90
|
-
.kui-
|
|
89
|
+
.kui-list-header__action,
|
|
90
|
+
.kui-list-header__action-layer {
|
|
91
91
|
display: grid;
|
|
92
92
|
place-items: center;
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
-
.kui-
|
|
95
|
+
.kui-list-header__action {
|
|
96
96
|
/* A fitted square: the icon size plus its padding and 1px border, instead of a
|
|
97
97
|
forced 44px box that left an oversized halo around the ~18px icon and, via the
|
|
98
98
|
parent's align-items: stretch, a vertically stretched oval. `align-self:
|
|
99
99
|
center` keeps it centered in the header's 44px row without stretching. Sizing
|
|
100
100
|
off the icon variable keeps it square and correct at any icon size. */
|
|
101
|
-
--_kui-
|
|
102
|
-
var(--_kui-
|
|
101
|
+
--_kui-list-header-action-size: calc(
|
|
102
|
+
var(--_kui-list-header-action-icon-size) +
|
|
103
103
|
(2 * var(--kui-layout-item-padding, 0.5rem)) + 2px
|
|
104
104
|
);
|
|
105
105
|
appearance: none;
|
|
106
106
|
display: inline-flex;
|
|
107
|
-
width: var(--_kui-
|
|
108
|
-
height: var(--_kui-
|
|
107
|
+
width: var(--_kui-list-header-action-size);
|
|
108
|
+
height: var(--_kui-list-header-action-size);
|
|
109
109
|
padding: var(--kui-layout-item-padding, 0.5rem);
|
|
110
110
|
align-items: center;
|
|
111
111
|
justify-content: center;
|
|
@@ -114,47 +114,47 @@
|
|
|
114
114
|
flex: none;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
.kui-
|
|
117
|
+
.kui-list-header__toggle {
|
|
118
118
|
appearance: none;
|
|
119
119
|
width: 100%;
|
|
120
120
|
cursor: pointer;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
-
.kui-
|
|
123
|
+
.kui-list-header__action-layer {
|
|
124
124
|
width: 1.5rem;
|
|
125
125
|
height: 1.5rem;
|
|
126
126
|
margin-inline-start: auto;
|
|
127
127
|
flex: none;
|
|
128
128
|
}
|
|
129
129
|
|
|
130
|
-
.kui-
|
|
131
|
-
.kui-
|
|
130
|
+
.kui-list-header__action:hover,
|
|
131
|
+
.kui-list-header__toggle:hover {
|
|
132
132
|
border-color: var(--kui-color-neutral-border-normal);
|
|
133
|
-
background: var(--kui-
|
|
133
|
+
background: var(--kui-list-header-hover-background);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
.kui-
|
|
137
|
-
.kui-
|
|
136
|
+
.kui-list-header__action:focus-visible,
|
|
137
|
+
.kui-list-header__toggle:focus-visible {
|
|
138
138
|
outline: var(--kui-focus-ring);
|
|
139
139
|
outline-offset: 1px;
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
.kui-
|
|
142
|
+
.kui-list-header__action:disabled {
|
|
143
143
|
cursor: not-allowed;
|
|
144
144
|
opacity: 0.48;
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
.kui-
|
|
147
|
+
.kui-list-header__action:disabled:hover {
|
|
148
148
|
border-color: transparent;
|
|
149
149
|
background: transparent;
|
|
150
150
|
}
|
|
151
151
|
|
|
152
|
-
.kui-
|
|
153
|
-
.kui-
|
|
152
|
+
.kui-list-header__action > svg,
|
|
153
|
+
.kui-list-header__action-layer > svg {
|
|
154
154
|
/* min-block-size: 0 overrides the flex item's automatic minimum, which would
|
|
155
155
|
otherwise resolve to the icon SVG's intrinsic 24px viewBox height and stretch
|
|
156
156
|
the button past its used icon size. */
|
|
157
|
-
width: var(--_kui-
|
|
158
|
-
height: var(--_kui-
|
|
157
|
+
width: var(--_kui-list-header-action-icon-size);
|
|
158
|
+
height: var(--_kui-list-header-action-icon-size);
|
|
159
159
|
min-block-size: 0;
|
|
160
160
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
.kui-list-item {
|
|
2
|
+
--kui-list-item-color: var(--kui-color-neutral-on-normal);
|
|
3
|
+
--kui-list-item-border: transparent;
|
|
4
|
+
--kui-list-item-background: transparent;
|
|
5
|
+
--kui-list-item-hover-border: var(--kui-color-neutral-border-normal);
|
|
6
|
+
--kui-list-item-hover-background: var(--kui-color-neutral-fill-quiet);
|
|
7
|
+
--kui-list-item-selected-color: var(--kui-color-brand-on-quiet);
|
|
8
|
+
--kui-list-item-selected-border: var(--kui-color-brand-border-quiet);
|
|
9
|
+
--kui-list-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-list-item-border);
|
|
17
|
+
border-radius: var(--kui-layout-rounded-radius, calc(1px + 0.6875rem));
|
|
18
|
+
color: var(--kui-list-item-color);
|
|
19
|
+
background: var(--kui-list-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-list-item[data-has-icon="true"] {
|
|
29
|
+
grid-template-columns: 1.5rem minmax(0, 1fr) auto;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.kui-list-item[data-has-icon="true"] .kui-list-item__icon {
|
|
33
|
+
grid-column: 1;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.kui-list-item[data-has-icon="true"] .kui-list-item__label {
|
|
37
|
+
grid-column: 2;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.kui-list-item[data-has-icon="true"] .kui-list-item__trailing {
|
|
41
|
+
grid-column: 3;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.kui-list-item[data-has-icon="false"] .kui-list-item__label {
|
|
45
|
+
grid-column: 1;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.kui-list-item[data-has-icon="false"] .kui-list-item__trailing {
|
|
49
|
+
grid-column: 2;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.kui-list-item:hover {
|
|
53
|
+
border-color: var(--kui-list-item-hover-border);
|
|
54
|
+
background: var(--kui-list-item-hover-background);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.kui-list-item:disabled {
|
|
58
|
+
color: var(--kui-color-neutral-on-quiet);
|
|
59
|
+
cursor: not-allowed;
|
|
60
|
+
opacity: 0.58;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
.kui-list-item[aria-current="page"],
|
|
64
|
+
.kui-list-item[aria-pressed="true"] {
|
|
65
|
+
border-color: var(--kui-list-item-selected-border);
|
|
66
|
+
color: var(--kui-list-item-selected-color);
|
|
67
|
+
background: var(--kui-list-item-selected-background);
|
|
68
|
+
font-weight: 650;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.kui-list-item:focus-visible {
|
|
72
|
+
outline: var(--kui-focus-ring);
|
|
73
|
+
outline-offset: 1px;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.kui-list-item__icon {
|
|
77
|
+
display: grid;
|
|
78
|
+
width: 1.5rem;
|
|
79
|
+
height: 1.5rem;
|
|
80
|
+
color: currentColor;
|
|
81
|
+
place-items: center;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.kui-list-item__icon svg {
|
|
85
|
+
display: block;
|
|
86
|
+
width: 1.5rem;
|
|
87
|
+
height: 1.5rem;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
.kui-list-item__label {
|
|
91
|
+
min-width: 0;
|
|
92
|
+
overflow: hidden;
|
|
93
|
+
text-overflow: ellipsis;
|
|
94
|
+
white-space: nowrap;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
.kui-list-item[data-multiline="true"] {
|
|
98
|
+
height: fit-content;
|
|
99
|
+
white-space: normal;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.kui-list-item[data-multiline="true"] .kui-list-item__label {
|
|
103
|
+
overflow: visible;
|
|
104
|
+
white-space: normal;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.kui-list-item[data-multiline="true"] .kui-list-item__icon {
|
|
108
|
+
align-self: start;
|
|
109
|
+
margin-block-start: calc((1lh - 1.5rem) / 2);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.kui-list-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-list-item__trailing > svg {
|
|
121
|
+
width: 1rem;
|
|
122
|
+
height: 1rem;
|
|
123
|
+
}
|
package/dist/styles/styles.css
CHANGED
|
@@ -5,11 +5,12 @@
|
|
|
5
5
|
@import "./toolbar.css";
|
|
6
6
|
@import "./toolbar-control-group.css";
|
|
7
7
|
@import "./toolbar-text.css";
|
|
8
|
-
@import "./
|
|
9
|
-
@import "./
|
|
10
|
-
@import "./
|
|
8
|
+
@import "./list-action-row.css";
|
|
9
|
+
@import "./list-item.css";
|
|
10
|
+
@import "./list-header.css";
|
|
11
11
|
@import "./resizable-region.css";
|
|
12
12
|
@import "./app-tab.css";
|
|
13
|
+
@import "./catalog.css";
|
|
13
14
|
@import "./tab-bar.css";
|
|
14
15
|
@import "./loading-spinner.css";
|
|
15
16
|
@import "./skeleton.css";
|
package/dist/toolbar-text.d.ts
CHANGED
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
import * as kerfjs from 'kerfjs';
|
|
2
2
|
|
|
3
3
|
type ToolbarTextSize = 'xlarge' | 'large' | 'default' | 'small';
|
|
4
|
+
/** ARIA heading level for a title exposed as a heading landmark. */
|
|
5
|
+
type HeadingLevel = 1 | 2 | 3 | 4 | 5 | 6;
|
|
4
6
|
interface ToolbarTextProps {
|
|
5
7
|
text: string;
|
|
6
8
|
size?: ToolbarTextSize;
|
|
7
9
|
className?: string;
|
|
8
10
|
/** Optional id, e.g. so a dialog can reference the title via aria-labelledby. */
|
|
9
11
|
id?: string;
|
|
12
|
+
/**
|
|
13
|
+
* Expose heading semantics (`role="heading"` + `aria-level`) so the text acts as
|
|
14
|
+
* a heading landmark — e.g. a page's primary title. Omit to keep the plain span
|
|
15
|
+
* (the default), which suits a dialog title referenced via `aria-labelledby`.
|
|
16
|
+
*/
|
|
17
|
+
headingLevel?: HeadingLevel;
|
|
10
18
|
/** Render the text as an unanimated loading skeleton instead of its value. */
|
|
11
19
|
placeholder?: boolean;
|
|
12
20
|
}
|
|
13
|
-
declare function ToolbarText({ text, size, className, id, placeholder }: ToolbarTextProps): kerfjs.SafeHtml;
|
|
21
|
+
declare function ToolbarText({ text, size, className, id, headingLevel, placeholder }: ToolbarTextProps): kerfjs.SafeHtml;
|
|
14
22
|
|
|
15
|
-
export { ToolbarText, type ToolbarTextProps, type ToolbarTextSize };
|
|
23
|
+
export { type HeadingLevel, ToolbarText, type ToolbarTextProps, type ToolbarTextSize };
|
package/dist/toolbar-text.js
CHANGED
package/dist/value-table.js
CHANGED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
interface WireCatalogOptions {
|
|
2
|
+
/** Invoked with the entry id when a sidebar item or a related-entry option is chosen. */
|
|
3
|
+
onSelect: (id: string) => void;
|
|
4
|
+
/** Invoked when the sidebar collapse/expand control is activated. */
|
|
5
|
+
onToggleSidebar?: () => void;
|
|
6
|
+
/** Invoked when the theme toggle is activated. */
|
|
7
|
+
onToggleTheme?: () => void;
|
|
8
|
+
/** Invoked when the secondary (ecosystem) group's disclosure toggle is activated. */
|
|
9
|
+
onToggleSecondary?: () => void;
|
|
10
|
+
/** When set, `?<urlParam>=<id>` is written on select via `history.replaceState`. */
|
|
11
|
+
urlParam?: string;
|
|
12
|
+
selectAction?: string;
|
|
13
|
+
toggleSidebarAction?: string;
|
|
14
|
+
toggleThemeAction?: string;
|
|
15
|
+
toggleSecondaryAction?: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Wire a {@link Catalog}'s interactions with one delegated listener set: sidebar
|
|
19
|
+
* item selection (and the related-entry selector), the sidebar collapse toggle, and
|
|
20
|
+
* the theme toggle. The app owns the `active`/`collapsed`/`theme` signals and updates
|
|
21
|
+
* them in the callbacks; optionally mirror the active id into the URL via `urlParam`.
|
|
22
|
+
* Returns a disposer.
|
|
23
|
+
*/
|
|
24
|
+
declare function wireCatalog(root: HTMLElement, { onSelect, onToggleSidebar, onToggleTheme, onToggleSecondary, urlParam, selectAction, toggleSidebarAction, toggleThemeAction, toggleSecondaryAction, }: WireCatalogOptions): () => void;
|
|
25
|
+
|
|
26
|
+
export { type WireCatalogOptions, wireCatalog };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { delegate } from 'kerfjs';
|
|
2
|
+
|
|
3
|
+
// src/wire-catalog.ts
|
|
4
|
+
function wireCatalog(root, {
|
|
5
|
+
onSelect,
|
|
6
|
+
onToggleSidebar,
|
|
7
|
+
onToggleTheme,
|
|
8
|
+
onToggleSecondary,
|
|
9
|
+
urlParam,
|
|
10
|
+
selectAction = "catalog-select",
|
|
11
|
+
toggleSidebarAction = "catalog-toggle-sidebar",
|
|
12
|
+
toggleThemeAction = "catalog-toggle-theme",
|
|
13
|
+
toggleSecondaryAction = "catalog-toggle-secondary"
|
|
14
|
+
}) {
|
|
15
|
+
const select = (id) => {
|
|
16
|
+
onSelect(id);
|
|
17
|
+
const view = root.ownerDocument.defaultView;
|
|
18
|
+
if (urlParam && view) {
|
|
19
|
+
const url = new URL(view.location.href);
|
|
20
|
+
url.searchParams.set(urlParam, id);
|
|
21
|
+
view.history.replaceState(null, "", url);
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
const disposers = [
|
|
25
|
+
delegate(root, "click", `[data-action="${selectAction}"]`, (_event, element) => {
|
|
26
|
+
const id = element.dataset.itemId;
|
|
27
|
+
if (id) select(id);
|
|
28
|
+
}),
|
|
29
|
+
delegate(root, "change", '[data-catalog-related] [data-component="select"]', (event) => {
|
|
30
|
+
const value = event.target.value;
|
|
31
|
+
if (value) select(value);
|
|
32
|
+
})
|
|
33
|
+
];
|
|
34
|
+
if (onToggleSidebar) {
|
|
35
|
+
disposers.push(delegate(root, "click", `[data-action="${toggleSidebarAction}"]`, () => onToggleSidebar()));
|
|
36
|
+
}
|
|
37
|
+
if (onToggleTheme) {
|
|
38
|
+
disposers.push(delegate(root, "click", `[data-action="${toggleThemeAction}"]`, () => onToggleTheme()));
|
|
39
|
+
}
|
|
40
|
+
if (onToggleSecondary) {
|
|
41
|
+
disposers.push(delegate(root, "click", `[data-action="${toggleSecondaryAction}"]`, () => onToggleSecondary()));
|
|
42
|
+
}
|
|
43
|
+
return () => {
|
|
44
|
+
for (const dispose of disposers.splice(0)) dispose();
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export { wireCatalog };
|
|
49
|
+
|
|
50
|
+
//# sourceMappingURL=wire-catalog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/wire-catalog.ts"],"names":[],"mappings":";;;AA0BO,SAAS,YACd,IAAA,EACA;AAAA,EACE,QAAA;AAAA,EACA,eAAA;AAAA,EACA,aAAA;AAAA,EACA,iBAAA;AAAA,EACA,QAAA;AAAA,EACA,YAAA,GAAe,gBAAA;AAAA,EACf,mBAAA,GAAsB,wBAAA;AAAA,EACtB,iBAAA,GAAoB,sBAAA;AAAA,EACpB,qBAAA,GAAwB;AAC1B,CAAA,EACY;AACZ,EAAA,MAAM,MAAA,GAAS,CAAC,EAAA,KAAe;AAC7B,IAAA,QAAA,CAAS,EAAE,CAAA;AACX,IAAA,MAAM,IAAA,GAAO,KAAK,aAAA,CAAc,WAAA;AAChC,IAAA,IAAI,YAAY,IAAA,EAAM;AACpB,MAAA,MAAM,GAAA,GAAM,IAAI,GAAA,CAAI,IAAA,CAAK,SAAS,IAAI,CAAA;AACtC,MAAA,GAAA,CAAI,YAAA,CAAa,GAAA,CAAI,QAAA,EAAU,EAAE,CAAA;AACjC,MAAA,IAAA,CAAK,OAAA,CAAQ,YAAA,CAAa,IAAA,EAAM,EAAA,EAAI,GAAG,CAAA;AAAA,IACzC;AAAA,EACF,CAAA;AAEA,EAAA,MAAM,SAAA,GAA+B;AAAA,IACnC,QAAA,CAAS,MAAM,OAAA,EAAS,CAAA,cAAA,EAAiB,YAAY,CAAA,EAAA,CAAA,EAAM,CAAC,QAAQ,OAAA,KAAY;AAC9E,MAAA,MAAM,EAAA,GAAM,QAAwB,OAAA,CAAQ,MAAA;AAC5C,MAAA,IAAI,EAAA,SAAW,EAAE,CAAA;AAAA,IACnB,CAAC,CAAA;AAAA,IACD,QAAA,CAAS,IAAA,EAAM,QAAA,EAAU,kDAAA,EAAoD,CAAC,KAAA,KAAU;AACtF,MAAA,MAAM,KAAA,GAAS,MAAM,MAAA,CAA4C,KAAA;AACjE,MAAA,IAAI,KAAA,SAAc,KAAK,CAAA;AAAA,IACzB,CAAC;AAAA,GACH;AACA,EAAA,IAAI,eAAA,EAAiB;AACnB,IAAA,SAAA,CAAU,IAAA,CAAK,QAAA,CAAS,IAAA,EAAM,OAAA,EAAS,CAAA,cAAA,EAAiB,mBAAmB,CAAA,EAAA,CAAA,EAAM,MAAM,eAAA,EAAiB,CAAC,CAAA;AAAA,EAC3G;AACA,EAAA,IAAI,aAAA,EAAe;AACjB,IAAA,SAAA,CAAU,IAAA,CAAK,QAAA,CAAS,IAAA,EAAM,OAAA,EAAS,CAAA,cAAA,EAAiB,iBAAiB,CAAA,EAAA,CAAA,EAAM,MAAM,aAAA,EAAe,CAAC,CAAA;AAAA,EACvG;AACA,EAAA,IAAI,iBAAA,EAAmB;AACrB,IAAA,SAAA,CAAU,IAAA,CAAK,QAAA,CAAS,IAAA,EAAM,OAAA,EAAS,CAAA,cAAA,EAAiB,qBAAqB,CAAA,EAAA,CAAA,EAAM,MAAM,iBAAA,EAAmB,CAAC,CAAA;AAAA,EAC/G;AACA,EAAA,OAAO,MAAM;AACX,IAAA,KAAA,MAAW,OAAA,IAAW,SAAA,CAAU,MAAA,CAAO,CAAC,GAAG,OAAA,EAAQ;AAAA,EACrD,CAAA;AACF","file":"wire-catalog.js","sourcesContent":["import { delegate } from 'kerfjs';\n\nexport interface WireCatalogOptions {\n /** Invoked with the entry id when a sidebar item or a related-entry option is chosen. */\n onSelect: (id: string) => void;\n /** Invoked when the sidebar collapse/expand control is activated. */\n onToggleSidebar?: () => void;\n /** Invoked when the theme toggle is activated. */\n onToggleTheme?: () => void;\n /** Invoked when the secondary (ecosystem) group's disclosure toggle is activated. */\n onToggleSecondary?: () => void;\n /** When set, `?<urlParam>=<id>` is written on select via `history.replaceState`. */\n urlParam?: string;\n selectAction?: string;\n toggleSidebarAction?: string;\n toggleThemeAction?: string;\n toggleSecondaryAction?: string;\n}\n\n/**\n * Wire a {@link Catalog}'s interactions with one delegated listener set: sidebar\n * item selection (and the related-entry selector), the sidebar collapse toggle, and\n * the theme toggle. The app owns the `active`/`collapsed`/`theme` signals and updates\n * them in the callbacks; optionally mirror the active id into the URL via `urlParam`.\n * Returns a disposer.\n */\nexport function wireCatalog(\n root: HTMLElement,\n {\n onSelect,\n onToggleSidebar,\n onToggleTheme,\n onToggleSecondary,\n urlParam,\n selectAction = 'catalog-select',\n toggleSidebarAction = 'catalog-toggle-sidebar',\n toggleThemeAction = 'catalog-toggle-theme',\n toggleSecondaryAction = 'catalog-toggle-secondary',\n }: WireCatalogOptions,\n): () => void {\n const select = (id: string) => {\n onSelect(id);\n const view = root.ownerDocument.defaultView;\n if (urlParam && view) {\n const url = new URL(view.location.href);\n url.searchParams.set(urlParam, id);\n view.history.replaceState(null, '', url);\n }\n };\n\n const disposers: Array<() => void> = [\n delegate(root, 'click', `[data-action=\"${selectAction}\"]`, (_event, element) => {\n const id = (element as HTMLElement).dataset.itemId;\n if (id) select(id);\n }),\n delegate(root, 'change', '[data-catalog-related] [data-component=\"select\"]', (event) => {\n const value = (event.target as HTMLElement & { value?: string }).value;\n if (value) select(value);\n }),\n ];\n if (onToggleSidebar) {\n disposers.push(delegate(root, 'click', `[data-action=\"${toggleSidebarAction}\"]`, () => onToggleSidebar()));\n }\n if (onToggleTheme) {\n disposers.push(delegate(root, 'click', `[data-action=\"${toggleThemeAction}\"]`, () => onToggleTheme()));\n }\n if (onToggleSecondary) {\n disposers.push(delegate(root, 'click', `[data-action=\"${toggleSecondaryAction}\"]`, () => onToggleSecondary()));\n }\n return () => {\n for (const dispose of disposers.splice(0)) dispose();\n };\n}\n"]}
|
package/docs/accessibility.md
CHANGED
|
@@ -6,18 +6,18 @@ These are normative contracts for package components and consuming applications.
|
|
|
6
6
|
|
|
7
7
|
- All controls have accessible names and visible `:focus-visible` treatment.
|
|
8
8
|
- Decorative `LucideIcon` output is `aria-hidden`; pass `label` only when the SVG itself conveys meaning.
|
|
9
|
-
- `
|
|
10
|
-
- `
|
|
11
|
-
- In any `.kui-pane`, menu rows, `
|
|
9
|
+
- `ListItem` uses native button Enter/Space behavior. `selected` maps to `aria-current="page"`; `pressed` maps to `aria-pressed`. Its `trailing` content is dormant and must not contain controls. Its `rootAttributes` slot accepts only application `data-*` metadata and deliberately cannot emit `role="menuitem"` in isolation—a true ARIA menu must own arrow, Home/End, Escape, and focus behavior as one widget.
|
|
10
|
+
- `ListHeader` owns action naming, disabled state, and disclosure `aria-expanded`. Toggle mode supplies one decorative 18px `DisclosureArrow` when `actionIcon` is omitted; it mirrors `expanded` while the native button's accessible name stays stable. The application must update that controlled state and reveal or hide real content. A valid `count` is visually rendered in an `aria-hidden` neutral pill while the required localized `countLabel` becomes part of the owning heading or disclosure button's accessible name; zero remains a real count. Its narrow `triggerAttributes` slot may describe a native popover or controlled-content relationship with `popoverTarget`, `popoverTargetAction`, `aria-controls`, and `aria-haspopup`; the application owns the target surface and its focus/dismissal behavior.
|
|
11
|
+
- In any `.kui-pane`, menu rows, `ListHeader` actions, and toolbar groups keep a minimum 44px target in both dimensions. A `ListHeader` fills the available inline width and keeps that action at the logical end, including in RTL. Its action visual defaults to 18px through `--kui-list-header-action-icon-size`; do not reduce the target to the visible icon.
|
|
12
12
|
- `LoadingSpinner` is either labeled (`role="img"`) or hidden. Its rotation stops for reduced motion.
|
|
13
13
|
- `StateBanner` defaults to polite `role="status"`; use `urgency="alert"` only for an attention-requiring failure.
|
|
14
14
|
- `EmptyState` reports busy state through `aria-busy` and never relies on an illustration as its label.
|
|
15
15
|
- `Select` follows the Web Awesome host's standard `input`/`change` events. Application tests verify the live `value`, focus, and events—not attributes alone. Its decorative option icons and value-dependent selected content remain present after controlled rerenders, so the visible choice does not silently lose its non-text cue.
|
|
16
16
|
- `DisclosureArrow` is an 18px root-scaled decorative visual by default, not an interaction target. Its owning native control supplies a stable accessible name, pointer and keyboard interaction, and `aria-expanded`; multiple arrows keep independently controlled state. Consumers may override `--kui-disclosure-arrow-size` without changing that ownership. Configured directions animate over the shortest path; a 180-degree closed-to-open tie uses counterclockwise rotation.
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## ListActionRow
|
|
19
19
|
|
|
20
|
-
`
|
|
20
|
+
`ListActionRow` renders a noninteractive visual root containing primary and
|
|
21
21
|
trailing native buttons as siblings. The primary comes first in DOM and Tab
|
|
22
22
|
order and alone receives `aria-current` or `aria-pressed`; the trailing button
|
|
23
23
|
has its own required accessible name and disabled state. Both controls carry
|
|
@@ -46,11 +46,16 @@ The application owns persistence and collapsed/expanded policy. Keep the last ex
|
|
|
46
46
|
`PanelHeader` is a plain `Toolbar` heading. Its leading zone holds an optional
|
|
47
47
|
icon (a normal bordered `ToolbarControlGroup`) and the title as extra-large
|
|
48
48
|
`ToolbarText`, and the app's trailing controls go straight into the trailing
|
|
49
|
-
zone.
|
|
50
|
-
`titleId` and an optional `summaryId` to the
|
|
51
|
-
`aria-labelledby` and `aria-describedby
|
|
52
|
-
|
|
53
|
-
`
|
|
49
|
+
zone. By default the title carries no native heading role, so for a **dialog or
|
|
50
|
+
panel** the application connects `titleId` and an optional `summaryId` to the
|
|
51
|
+
owning host through `aria-labelledby` and `aria-describedby`. For a **page or
|
|
52
|
+
view** title, pass `headingLevel` (usually `1`): the title then exposes
|
|
53
|
+
`role="heading"` with a matching `aria-level`, giving the view a heading landmark
|
|
54
|
+
so screen-reader heading navigation and "main heading" semantics work — the same
|
|
55
|
+
role/level pair `ToolbarText` exposes when it is given `headingLevel` directly.
|
|
56
|
+
Keep the levels meaningful and non-skipping within a view. Pass the trailing
|
|
57
|
+
controls as a labeled `ToolbarControlGroup` when that group needs an accessible
|
|
58
|
+
name.
|
|
54
59
|
|
|
55
60
|
## Tabs
|
|
56
61
|
|
package/docs/catalog.md
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
# Catalog — a reusable component-gallery shell
|
|
2
|
+
|
|
3
|
+
`@kerfjs/ui/catalog` is an opt-in, whole-screen shell for building a **component
|
|
4
|
+
catalog** — the collapsible category sidebar + titled preview stage + resources
|
|
5
|
+
footer that the kerf UI catalog itself uses. Point it at your own components and
|
|
6
|
+
you get the same shell without rebuilding it. Like the app layouts, it is a
|
|
7
|
+
subpath-only, tree-shakeable module that adds nothing to the main barrel.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @kerfjs/ui # kerfjs is a peer; @kerfjs/ui/select/register is needed only if entries use `related`
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
- `Catalog(props)` returns the shell as `SafeHtml` (a `<main class="kui-catalog">`).
|
|
14
|
+
It is **controlled and stateless**: your app owns the `active`, `collapsed`, and
|
|
15
|
+
`theme` signals and computes the preview `content` from `active` in its own
|
|
16
|
+
`mount()` render.
|
|
17
|
+
- `wireCatalog(root, options)` wires the interactions (sidebar selection, the
|
|
18
|
+
related-entry selector, and the collapse/theme toggles) with one delegated
|
|
19
|
+
listener set and returns a disposer; it can also mirror the active id into the
|
|
20
|
+
URL.
|
|
21
|
+
|
|
22
|
+
## What you supply
|
|
23
|
+
|
|
24
|
+
- **`sections`** — category-grouped entries: `{ category, entries: [{ id, name,
|
|
25
|
+
description?, resources?, related? }] }`. Each entry becomes a sidebar `ListItem`
|
|
26
|
+
under a `ListHeader` for its category.
|
|
27
|
+
- **`content`** — the rendered preview for the active entry. Keep a map of `id →
|
|
28
|
+
() => SafeHtml` in your app and call `renderers[active]()` in your render.
|
|
29
|
+
- **`brand`** — `{ title, subtitle?, logoUrl? }` for the sidebar header.
|
|
30
|
+
- **`secondarySections`** — an optional secondary "ecosystem" group shown below the
|
|
31
|
+
primary sections with a quieter treatment: `{ label, sections, collapsible?,
|
|
32
|
+
expanded? }`. When `collapsible`, the label is a disclosure toggle controlling
|
|
33
|
+
`expanded` (the app owns it; wire it with `wireCatalog`'s `onToggleSecondary`).
|
|
34
|
+
- Optional slots: `headerActions` (extra header controls), `sidebarFooter` (extra
|
|
35
|
+
sidebar content), and `status` (a footer status line).
|
|
36
|
+
|
|
37
|
+
Per-entry `resources` render as "open in new tab" links in the footer, and
|
|
38
|
+
`related` renders a "Related entries" selector (a `Select`, so register it with
|
|
39
|
+
`@kerfjs/ui/select/register` when you use it).
|
|
40
|
+
|
|
41
|
+
## Preview examples
|
|
42
|
+
|
|
43
|
+
Compose each entry's `content` from `CatalogExample` (and `CatalogExampleStack`)
|
|
44
|
+
instead of hand-rolled example markup, so labels, notes, and left-edge alignment
|
|
45
|
+
stay consistent:
|
|
46
|
+
|
|
47
|
+
```tsx
|
|
48
|
+
import { CatalogExample, CatalogExampleStack } from '@kerfjs/ui/catalog';
|
|
49
|
+
|
|
50
|
+
const buttonPreview = (
|
|
51
|
+
<CatalogExampleStack label="Button variants">
|
|
52
|
+
<CatalogExample label="Icon" note="A bare glyph." align="glyph">
|
|
53
|
+
<LucideIcon icon={Plus} name="plus" />
|
|
54
|
+
</CatalogExample>
|
|
55
|
+
<CatalogExample label="Control" align="inline-control">
|
|
56
|
+
<SegmentedControl id="view" label="View" value="list" choices={choices} />
|
|
57
|
+
</CatalogExample>
|
|
58
|
+
<CatalogExample label="In composition">
|
|
59
|
+
<ValueTable label="Metadata">{rows}</ValueTable>
|
|
60
|
+
</CatalogExample>
|
|
61
|
+
</CatalogExampleStack>
|
|
62
|
+
);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
`label` is optional — omit it for a bare specimen with no `ListHeader`. `align`
|
|
66
|
+
lines a specimen's visible left edge up with its label text: `'glyph'`
|
|
67
|
+
(16px) for a bare glyph/text specimen, `'inline-control'` (8px) for a control that
|
|
68
|
+
already carries ~8px of its own inline padding, and `'none'` (the default) for a
|
|
69
|
+
content-item/composition that already owns its geometry. The inset is published as
|
|
70
|
+
the `--kui-catalog-example-align` custom property so a debug overlay can exclude it
|
|
71
|
+
from a specimen's measured margin.
|
|
72
|
+
|
|
73
|
+
## Complete example
|
|
74
|
+
|
|
75
|
+
```tsx
|
|
76
|
+
import { mount, signal } from 'kerfjs';
|
|
77
|
+
import { Catalog, type CatalogSection } from '@kerfjs/ui/catalog';
|
|
78
|
+
import { wireCatalog } from '@kerfjs/ui/wire-catalog';
|
|
79
|
+
import '@kerfjs/ui/styles.css'; // or import each primitive's CSS + @kerfjs/ui/catalog.css
|
|
80
|
+
|
|
81
|
+
// 1. Describe your components once.
|
|
82
|
+
const sections: CatalogSection[] = [
|
|
83
|
+
{
|
|
84
|
+
category: 'Controls',
|
|
85
|
+
entries: [
|
|
86
|
+
{ id: 'button', name: 'Button', description: 'A pressable control.',
|
|
87
|
+
resources: [{ label: 'Source', href: '/src/button.tsx', detail: 'src/button.tsx' }] },
|
|
88
|
+
{ id: 'field', name: 'Field', description: 'A labeled input.',
|
|
89
|
+
related: [{ id: 'button', name: 'Button', group: 'Used with' }] },
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{ category: 'Feedback', entries: [{ id: 'toast', name: 'Toast', description: 'A transient message.' }] },
|
|
93
|
+
];
|
|
94
|
+
|
|
95
|
+
// 2. One preview render per entry id.
|
|
96
|
+
const renderers: Record<string, () => ReturnType<typeof Button>> = {
|
|
97
|
+
button: () => <Button label="Save" />,
|
|
98
|
+
field: () => <Field label="Name" />,
|
|
99
|
+
toast: () => <Toast>Saved</Toast>,
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
// 3. App-owned state (domain: which entry; transient: collapsed; global: theme).
|
|
103
|
+
const initial = new URLSearchParams(location.search).get('c') ?? sections[0].entries[0].id;
|
|
104
|
+
const active = signal(initial);
|
|
105
|
+
const collapsed = signal(false);
|
|
106
|
+
const theme = signal<'light' | 'dark'>('light');
|
|
107
|
+
|
|
108
|
+
const app = document.getElementById('app')!;
|
|
109
|
+
mount(app, () => (
|
|
110
|
+
<Catalog
|
|
111
|
+
brand={{ title: 'Acme UI', subtitle: 'Design system' }}
|
|
112
|
+
sections={sections}
|
|
113
|
+
active={active.value}
|
|
114
|
+
content={renderers[active.value]?.() ?? <></>}
|
|
115
|
+
collapsed={collapsed.value}
|
|
116
|
+
theme={theme.value}
|
|
117
|
+
/>
|
|
118
|
+
));
|
|
119
|
+
|
|
120
|
+
wireCatalog(app, {
|
|
121
|
+
onSelect: (id) => { active.value = id; },
|
|
122
|
+
onToggleSidebar: () => { collapsed.value = !collapsed.value; },
|
|
123
|
+
onToggleTheme: () => {
|
|
124
|
+
theme.value = theme.value === 'dark' ? 'light' : 'dark';
|
|
125
|
+
document.documentElement.dataset.theme = theme.value; // apply your theme however you like
|
|
126
|
+
},
|
|
127
|
+
urlParam: 'c', // mirror the active id into ?c=<id>
|
|
128
|
+
});
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Ownership boundary
|
|
132
|
+
|
|
133
|
+
`Catalog` renders the shell; the app owns everything stateful:
|
|
134
|
+
|
|
135
|
+
- **`active`** is domain state (which entry is shown) — the app's signal, updated in
|
|
136
|
+
`onSelect`, read to compute `content`.
|
|
137
|
+
- **`collapsed`** is transient UI — the app's signal, flipped in `onToggleSidebar`.
|
|
138
|
+
- **`theme`** is a global preference — the app's signal; `wireCatalog` only reports
|
|
139
|
+
the toggle, the app applies the theme (the shell reads `theme` to show the toggle's
|
|
140
|
+
opposite-state label). Omit `theme` to hide the toggle entirely.
|
|
141
|
+
|
|
142
|
+
## Custom action names
|
|
143
|
+
|
|
144
|
+
The shell emits `data-action="catalog-select"` (sidebar items),
|
|
145
|
+
`catalog-toggle-sidebar`, and `catalog-toggle-theme`. Override them with
|
|
146
|
+
`selectAction` / `toggleSidebarAction` / `toggleThemeAction` on `Catalog` (and the
|
|
147
|
+
matching options on `wireCatalog`) if they collide with your own action table.
|
|
148
|
+
|
|
149
|
+
## CSS
|
|
150
|
+
|
|
151
|
+
`Catalog` composes public primitives (`Toolbar`, `ListHeader`, `ListItem`, `Select`,
|
|
152
|
+
…). Import `@kerfjs/ui/styles.css` for the whole layer, or `@kerfjs/ui/catalog.css`
|
|
153
|
+
plus each composed primitive's CSS. The shell is theme-aware and responsive: it
|
|
154
|
+
stacks the sidebar above the detail below ~832px and hides it when collapsed.
|