@kouji-ui/components 0.9.2 → 0.9.4

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 (73) hide show
  1. package/fesm2022/kouji-ui-components.mjs +49 -3
  2. package/fesm2022/kouji-ui-components.mjs.map +1 -1
  3. package/package.json +1 -1
  4. package/src/accordion/accordion.css +68 -0
  5. package/src/action-sheet/action-sheet.css +79 -0
  6. package/src/alert/alert.css +133 -0
  7. package/src/avatar/avatar.css +93 -0
  8. package/src/badge/badge.css +59 -0
  9. package/src/breadcrumb/breadcrumb.css +145 -0
  10. package/src/button/button.css +295 -0
  11. package/src/button-group/button-group.css +114 -0
  12. package/src/calendar/calendar.css +129 -0
  13. package/src/card/card.css +120 -0
  14. package/src/carousel/carousel.css +181 -0
  15. package/src/cascade-select/cascade-select.css +120 -0
  16. package/src/chart/chart.css +26 -0
  17. package/src/chat/chat-ai.css +352 -0
  18. package/src/chat/chat.css +231 -0
  19. package/src/checkbox/checkbox.css +74 -0
  20. package/src/color-picker/color-picker.css +183 -0
  21. package/src/combobox/combobox.css +101 -0
  22. package/src/command-palette/command-palette.css +234 -0
  23. package/src/confirm-popup/confirm-popup.css +47 -0
  24. package/src/date-picker/date-picker.css +61 -0
  25. package/src/date-range-presets/date-range-presets.css +49 -0
  26. package/src/datetime-picker/datetime-picker.css +84 -0
  27. package/src/dialog/dialog.css +56 -0
  28. package/src/direction-toggle/direction-toggle.css +55 -0
  29. package/src/divider/divider.css +93 -0
  30. package/src/drawer/drawer.css +130 -0
  31. package/src/dropdown-menu/dropdown-menu.css +108 -0
  32. package/src/editor/editor.css +140 -0
  33. package/src/empty-state/empty-state.css +114 -0
  34. package/src/field/field.css +148 -0
  35. package/src/file-upload/file-upload.css +207 -0
  36. package/src/form/form.css +63 -0
  37. package/src/input/input.css +70 -0
  38. package/src/input-group/input-group.css +125 -0
  39. package/src/input-mask/input-mask.css +39 -0
  40. package/src/input-otp/input-otp.css +87 -0
  41. package/src/kbd/kbd.css +67 -0
  42. package/src/link/link.css +104 -0
  43. package/src/list/list.css +166 -0
  44. package/src/menubar/menubar.css +62 -0
  45. package/src/number-input/number-input.css +115 -0
  46. package/src/overflow/overflow.css +43 -0
  47. package/src/overlay/overlay.css +45 -0
  48. package/src/overlay-badge/overlay-badge.css +118 -0
  49. package/src/pagination/pagination.css +146 -0
  50. package/src/password-input/password-input.css +155 -0
  51. package/src/popover/popover.css +71 -0
  52. package/src/progress-bar/progress-bar.css +113 -0
  53. package/src/radio/radio.css +58 -0
  54. package/src/rich-text/rich-text-editor.css +351 -0
  55. package/src/select/select.css +91 -0
  56. package/src/sheet/sheet.css +121 -0
  57. package/src/skeleton/skeleton.css +111 -0
  58. package/src/skip-link/skip-link.css +58 -0
  59. package/src/slider/slider.css +172 -0
  60. package/src/speed-dial/speed-dial.css +137 -0
  61. package/src/spinner/spinner.css +161 -0
  62. package/src/stepper/stepper.css +147 -0
  63. package/src/table/table-filters/filters.css +53 -0
  64. package/src/table/table.css +509 -0
  65. package/src/tabs/tabs.css +162 -0
  66. package/src/tag/tag.css +183 -0
  67. package/src/textarea/textarea.css +117 -0
  68. package/src/time-picker/time-picker.css +126 -0
  69. package/src/toast/toast.css +115 -0
  70. package/src/toggle/toggle.css +123 -0
  71. package/src/tooltip/tooltip.css +61 -0
  72. package/src/tree-select/tree-select.css +220 -0
  73. package/types/kouji-ui-components.d.ts +19 -0
@@ -0,0 +1,351 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ kj-rich-text-editor — component-layer tokens + structural CSS.
3
+ Content nodes are rendered by Lexical (ViewEncapsulation.None),
4
+ so every selector is namespaced under `.kj-rte`.
5
+ ──────────────────────────────────────────────────────────── */
6
+
7
+ @layer kj.component {
8
+ .kj-rte {
9
+ /* component knobs — themes/users can override any of these */
10
+ --kj-rte-bg: var(--kj-bg-field);
11
+ --kj-rte-fg: var(--kj-fg-default);
12
+ --kj-rte-muted-fg: var(--kj-fg-muted);
13
+ --kj-rte-border-color: var(--kj-border-default);
14
+ --kj-rte-radius: var(--kj-radius-field);
15
+ --kj-rte-toolbar-bg: var(--kj-bg-surface, var(--kj-bg-body));
16
+ --kj-rte-btn-size: 2.75rem; /* 44px — WCAG 2.5.5 AAA touch target */
17
+ --kj-rte-accent: var(--kj-bg-primary);
18
+ --kj-rte-accent-fg: var(--kj-fg-on-primary);
19
+ --kj-rte-min-height: 12rem;
20
+ --kj-rte-pad: var(--kj-space-md);
21
+ --kj-rte-font: var(--kj-font-sans);
22
+ --kj-rte-mono: var(--kj-font-mono, ui-monospace, monospace);
23
+
24
+ display: flex;
25
+ flex-direction: column;
26
+ /* Fluid: fill the container and never exceed it. `min-width: 0` lets the
27
+ editor shrink below its content's intrinsic width inside flex/grid
28
+ parents (the docs playground stage, app layouts) so the toolbar scrolls
29
+ instead of the whole editor overflowing its container. */
30
+ width: 100%;
31
+ max-width: 100%;
32
+ min-width: 0;
33
+ box-sizing: border-box;
34
+ background: var(--kj-rte-bg);
35
+ color: var(--kj-rte-fg);
36
+ border: var(--kj-border) solid var(--kj-rte-border-color);
37
+ border-radius: var(--kj-rte-radius);
38
+ font-family: var(--kj-rte-font);
39
+ overflow: hidden;
40
+ }
41
+
42
+ .kj-rte:focus-within {
43
+ outline: 2px solid var(--kj-rte-accent);
44
+ outline-offset: 1px;
45
+ border-color: var(--kj-border-focus, var(--kj-rte-accent));
46
+ }
47
+
48
+ /* -- toolbar ----------------------------------------------------------- */
49
+
50
+ .kj-rte__toolbar {
51
+ display: flex;
52
+ /* Single horizontal row that scrolls on overflow rather than wrapping into
53
+ a tall, chunked block in narrow containers. All buttons stay keyboard-
54
+ reachable (roving tabindex) and scroll into view on focus. */
55
+ flex-wrap: nowrap;
56
+ align-items: center;
57
+ gap: var(--kj-space-xs, 0.25rem);
58
+ padding: var(--kj-space-sm);
59
+ background: var(--kj-rte-toolbar-bg);
60
+ border-bottom: var(--kj-border) solid var(--kj-rte-border-color);
61
+ overflow-x: auto;
62
+ overflow-y: hidden;
63
+ scrollbar-width: thin;
64
+ scrollbar-color: var(--kj-rte-border-color) transparent;
65
+ }
66
+
67
+ /* Subtle, unobtrusive scrollbar for the overflowing toolbar (WebKit). */
68
+ .kj-rte__toolbar::-webkit-scrollbar {
69
+ height: 0.5rem;
70
+ }
71
+
72
+ .kj-rte__toolbar::-webkit-scrollbar-thumb {
73
+ background: var(--kj-rte-border-color);
74
+ border-radius: 0.25rem;
75
+ }
76
+
77
+ .kj-rte__toolbar::-webkit-scrollbar-track {
78
+ background: transparent;
79
+ }
80
+
81
+ .kj-rte__group {
82
+ display: flex;
83
+ align-items: center;
84
+ gap: var(--kj-space-xs, 0.25rem);
85
+ }
86
+
87
+ /* Keep buttons, groups and separators at their natural size so the row
88
+ scrolls instead of squashing them when space is tight. */
89
+ .kj-rte__group,
90
+ .kj-rte__sep,
91
+ .kj-rte__btn {
92
+ flex: 0 0 auto;
93
+ }
94
+
95
+ .kj-rte__sep {
96
+ width: 1px;
97
+ align-self: stretch;
98
+ min-height: 1.5rem;
99
+ margin: 0 var(--kj-space-xs, 0.25rem);
100
+ background: var(--kj-rte-border-color);
101
+ }
102
+
103
+ .kj-rte__btn {
104
+ display: inline-flex;
105
+ align-items: center;
106
+ justify-content: center;
107
+ min-width: var(--kj-rte-btn-size);
108
+ min-height: var(--kj-rte-btn-size);
109
+ padding: 0 var(--kj-space-sm);
110
+ color: var(--kj-rte-fg);
111
+ background: transparent;
112
+ border: var(--kj-border) solid transparent;
113
+ border-radius: var(--kj-radius-field);
114
+ cursor: pointer;
115
+ transition: var(--kj-transition, background-color 0.15s ease, color 0.15s ease);
116
+ }
117
+
118
+ .kj-rte__btn:hover:not(:disabled) {
119
+ background: var(--kj-bg-subtle, var(--kj-bg-body));
120
+ }
121
+
122
+ .kj-rte__btn:focus-visible {
123
+ outline: 2px solid var(--kj-rte-accent);
124
+ outline-offset: 2px;
125
+ }
126
+
127
+ .kj-rte__btn--active {
128
+ background: color-mix(in srgb, var(--kj-rte-accent) 16%, transparent);
129
+ border-color: color-mix(in srgb, var(--kj-rte-accent) 40%, transparent);
130
+ color: var(--kj-rte-accent);
131
+ }
132
+
133
+ .kj-rte__btn:disabled {
134
+ opacity: 0.45;
135
+ cursor: not-allowed;
136
+ }
137
+
138
+ /* -- feature overlays (link / image editors) -------------------------- */
139
+
140
+ .kj-rte__overlay {
141
+ position: relative;
142
+ z-index: 1;
143
+ padding: var(--kj-space-sm) var(--kj-rte-pad);
144
+ background: var(--kj-rte-toolbar-bg);
145
+ border-bottom: var(--kj-border) solid var(--kj-rte-border-color);
146
+ box-shadow: 0 6px 16px rgb(0 0 0 / 12%);
147
+ }
148
+
149
+ .kj-rte__overlay-form {
150
+ display: flex;
151
+ flex-wrap: wrap;
152
+ align-items: center;
153
+ gap: var(--kj-space-sm);
154
+ }
155
+
156
+ .kj-rte__overlay-label {
157
+ font-size: var(--kj-text-sm);
158
+ color: var(--kj-rte-muted-fg);
159
+ }
160
+
161
+ .kj-rte__overlay-input {
162
+ flex: 1 1 12rem;
163
+ min-height: 2.25rem;
164
+ padding: var(--kj-space-sm) var(--kj-space-md);
165
+ color: var(--kj-rte-fg);
166
+ background: var(--kj-bg-field);
167
+ border: var(--kj-border) solid var(--kj-rte-border-color);
168
+ border-radius: var(--kj-radius-field);
169
+ font: var(--kj-text-sm) / 1.4 var(--kj-rte-font);
170
+ }
171
+
172
+ .kj-rte__overlay-input:focus-visible {
173
+ outline: 2px solid var(--kj-rte-accent);
174
+ outline-offset: 1px;
175
+ }
176
+
177
+ .kj-rte__overlay-actions {
178
+ display: flex;
179
+ gap: var(--kj-space-sm);
180
+ }
181
+
182
+ .kj-rte__overlay-btn {
183
+ min-height: 2.25rem;
184
+ padding: 0 var(--kj-space-md);
185
+ color: var(--kj-rte-fg);
186
+ background: transparent;
187
+ border: var(--kj-border) solid var(--kj-rte-border-color);
188
+ border-radius: var(--kj-radius-field);
189
+ cursor: pointer;
190
+ }
191
+
192
+ .kj-rte__overlay-btn:hover:not(:disabled) {
193
+ background: var(--kj-bg-subtle, var(--kj-bg-body));
194
+ }
195
+
196
+ .kj-rte__overlay-btn:focus-visible {
197
+ outline: 2px solid var(--kj-rte-accent);
198
+ outline-offset: 2px;
199
+ }
200
+
201
+ .kj-rte__overlay-btn:disabled {
202
+ opacity: 0.45;
203
+ cursor: not-allowed;
204
+ }
205
+
206
+ /* -- editable surface -------------------------------------------------- */
207
+
208
+ .kj-rte__content {
209
+ flex: 1 1 auto;
210
+ min-width: 0;
211
+ min-height: var(--kj-rte-min-height);
212
+ padding: var(--kj-rte-pad);
213
+ outline: none;
214
+ overflow-y: auto;
215
+ overflow-wrap: anywhere;
216
+ line-height: 1.6;
217
+ color: var(--kj-rte-fg);
218
+ }
219
+
220
+ .kj-rte__content[data-empty][data-placeholder]::before {
221
+ content: attr(data-placeholder);
222
+ color: var(--kj-rte-muted-fg);
223
+ pointer-events: none;
224
+ }
225
+
226
+ /* screen-reader-only live region */
227
+ .kj-rte__sr {
228
+ position: absolute;
229
+ width: 1px;
230
+ height: 1px;
231
+ padding: 0;
232
+ margin: -1px;
233
+ overflow: hidden;
234
+ clip: rect(0, 0, 0, 0);
235
+ white-space: nowrap;
236
+ border: 0;
237
+ }
238
+
239
+ /* -- Lexical content node typography ---------------------------------- */
240
+
241
+ .kj-rte .kj-rte-paragraph {
242
+ margin: 0 0 var(--kj-space-sm);
243
+ }
244
+
245
+ .kj-rte .kj-rte-h1 {
246
+ margin: 0 0 var(--kj-space-sm);
247
+ font-size: var(--kj-text-2xl, 1.75rem);
248
+ font-weight: 700;
249
+ line-height: 1.25;
250
+ }
251
+
252
+ .kj-rte .kj-rte-h2 {
253
+ margin: 0 0 var(--kj-space-sm);
254
+ font-size: var(--kj-text-xl, 1.4rem);
255
+ font-weight: 700;
256
+ line-height: 1.3;
257
+ }
258
+
259
+ .kj-rte .kj-rte-h3 {
260
+ margin: 0 0 var(--kj-space-sm);
261
+ font-size: var(--kj-text-lg, 1.2rem);
262
+ font-weight: 600;
263
+ line-height: 1.35;
264
+ }
265
+
266
+ .kj-rte .kj-rte-ul,
267
+ .kj-rte .kj-rte-ol {
268
+ margin: 0 0 var(--kj-space-sm);
269
+ padding-left: 1.5rem;
270
+ }
271
+
272
+ .kj-rte .kj-rte-ul {
273
+ list-style: disc;
274
+ }
275
+
276
+ .kj-rte .kj-rte-ol {
277
+ list-style: decimal;
278
+ }
279
+
280
+ .kj-rte .kj-rte-li {
281
+ margin-bottom: 0.25rem;
282
+ }
283
+
284
+ .kj-rte .kj-rte-quote {
285
+ margin: 0 0 var(--kj-space-sm);
286
+ padding-left: var(--kj-space-md);
287
+ border-left: 3px solid var(--kj-rte-border-color);
288
+ color: var(--kj-rte-muted-fg);
289
+ font-style: italic;
290
+ }
291
+
292
+ .kj-rte .kj-rte-link {
293
+ color: var(--kj-rte-accent);
294
+ text-decoration: underline;
295
+ text-underline-offset: 2px;
296
+ }
297
+
298
+ .kj-rte .kj-rte-bold {
299
+ font-weight: 700;
300
+ }
301
+
302
+ .kj-rte .kj-rte-italic {
303
+ font-style: italic;
304
+ }
305
+
306
+ .kj-rte .kj-rte-underline {
307
+ text-decoration: underline;
308
+ }
309
+
310
+ .kj-rte .kj-rte-strikethrough {
311
+ text-decoration: line-through;
312
+ }
313
+
314
+ .kj-rte .kj-rte-code {
315
+ padding: 0.1em 0.35em;
316
+ background: var(--kj-bg-subtle, var(--kj-bg-body));
317
+ border-radius: 0.25rem;
318
+ font-family: var(--kj-rte-mono);
319
+ font-size: 0.9em;
320
+ }
321
+
322
+ .kj-rte .kj-rte-code-block {
323
+ display: block;
324
+ margin: 0 0 var(--kj-space-sm);
325
+ padding: var(--kj-space-md);
326
+ background: var(--kj-bg-subtle, var(--kj-bg-body));
327
+ border-radius: var(--kj-radius-field);
328
+ font-family: var(--kj-rte-mono);
329
+ font-size: 0.9em;
330
+ white-space: pre-wrap;
331
+ overflow-x: auto;
332
+ }
333
+
334
+ .kj-rte .kj-rte-image {
335
+ margin: var(--kj-space-sm) 0;
336
+ }
337
+
338
+ .kj-rte .kj-rte-image img {
339
+ max-width: 100%;
340
+ height: auto;
341
+ border-radius: var(--kj-radius-field);
342
+ }
343
+
344
+ /* Respect reduced-motion preferences. */
345
+ @media (prefers-reduced-motion: reduce) {
346
+ .kj-rte__btn,
347
+ .kj-rte {
348
+ transition: none;
349
+ }
350
+ }
351
+ }
@@ -0,0 +1,91 @@
1
+ @layer kj.component {
2
+ .kj-select { position: relative; display: inline-block; min-width: 12rem; }
3
+
4
+ .kj-select-trigger {
5
+ /* Explicit height contract (matches input.css / button.css) so the
6
+ select trigger lines up with sibling inputs in a form row. */
7
+ --kj-select-trigger-height: 2.25rem;
8
+ width: 100%;
9
+ display: inline-flex;
10
+ align-items: center;
11
+ justify-content: space-between;
12
+ gap: var(--kj-space-sm);
13
+ background: var(--kj-bg-field);
14
+ color: var(--kj-fg-default);
15
+ border: var(--kj-border) solid var(--kj-border-default);
16
+ border-radius: var(--kj-radius-field);
17
+ padding: 0 var(--kj-space-md);
18
+ height: var(--kj-select-trigger-height);
19
+ font: 0.875rem var(--kj-font-sans);
20
+ cursor: pointer;
21
+ /* A flex item defaults to min-width:auto, so a long label could push the
22
+ trigger past its column instead of clipping inside it. */
23
+ min-width: 0;
24
+ }
25
+ /* One line, always. The trigger has a fixed height contract, so a label that
26
+ wrapped overflowed the button; a long branch/path name now truncates with
27
+ an ellipsis and the caret keeps its place at the end. */
28
+ .kj-select-trigger-label {
29
+ flex: 1 1 auto;
30
+ min-width: 0;
31
+ overflow: hidden;
32
+ text-overflow: ellipsis;
33
+ white-space: nowrap;
34
+ text-align: left;
35
+ }
36
+ .kj-select[data-size="xs"] .kj-select-trigger { --kj-select-trigger-height: 1.75rem; font-size: 0.75rem; padding-inline: var(--kj-space-sm); gap: var(--kj-space-xs); }
37
+ .kj-select[data-size="sm"] .kj-select-trigger { --kj-select-trigger-height: 2rem; font-size: 0.8125rem; }
38
+ .kj-select[data-size="lg"] .kj-select-trigger { --kj-select-trigger-height: 2.75rem; font-size: 1rem; }
39
+ .kj-select[data-size="xs"] { min-width: 0; }
40
+ .kj-select-trigger-caret {
41
+ font-size: 0.875em;
42
+ color: var(--kj-fg-muted);
43
+ flex: none;
44
+ }
45
+ .kj-select-trigger:focus-visible { outline: 2px solid var(--kj-fg-primary); outline-offset: 2px; }
46
+ .kj-select[data-disabled] .kj-select-trigger { opacity: 0.45; cursor: not-allowed; pointer-events: none; }
47
+
48
+ .kj-select-content {
49
+ box-sizing: border-box;
50
+ min-width: 12rem;
51
+ width: max-content;
52
+ /* max-content alone let one long option make the whole panel as wide as
53
+ that option; the cap keeps the listbox near the trigger's width class
54
+ and inside the viewport, with rows truncating instead. */
55
+ max-width: min(28rem, calc(100vw - 2rem));
56
+ background: var(--kj-bg-elevated);
57
+ border: var(--kj-border) solid var(--kj-border-default);
58
+ border-radius: var(--kj-radius-field);
59
+ box-shadow: var(--kj-shadow-md, 0 8px 24px rgb(0 0 0 / 0.12));
60
+ padding: 4px;
61
+ max-height: 16rem;
62
+ overflow: auto;
63
+ z-index: var(--kj-overlay-z, 100);
64
+ }
65
+ .kj-select-content[hidden] { display: none; }
66
+
67
+ .kj-option {
68
+ padding: var(--kj-space-sm) var(--kj-space-md);
69
+ border-radius: var(--kj-radius-selector);
70
+ cursor: pointer;
71
+ color: var(--kj-fg-default);
72
+ font: 0.875rem var(--kj-font-sans);
73
+ overflow: hidden;
74
+ text-overflow: ellipsis;
75
+ white-space: nowrap;
76
+ }
77
+ /* Hover uses bg-field (sunken slot), not bg-surface. In themes where
78
+ elevated == surface (light/corporate, both white) a bg-surface hover would
79
+ be invisible on the bg-elevated panel. */
80
+ .kj-option:hover { background: var(--kj-bg-field); }
81
+ /* Selected option — tinted primary mirrors tree-select / cascade-select so
82
+ the listbox family looks consistent across themes. */
83
+ .kj-option[aria-selected="true"] {
84
+ background: color-mix(in oklab, var(--kj-bg-primary) 12%, transparent);
85
+ color: var(--kj-fg-primary);
86
+ font-weight: 600;
87
+ }
88
+ .kj-option[aria-selected="true"]:hover {
89
+ background: color-mix(in oklab, var(--kj-bg-primary) 18%, transparent);
90
+ }
91
+ }
@@ -0,0 +1,121 @@
1
+ @layer kj.component {
2
+ .kj-sheet {
3
+ --kj-sheet-bg: var(--kj-bg-elevated);
4
+ --kj-sheet-fg: var(--kj-fg-default);
5
+ --kj-sheet-shadow: var(--kj-shadow-lg);
6
+ --kj-sheet-radius: var(--kj-radius-box);
7
+ --kj-sheet-max-height: 92vh;
8
+ --kj-sheet-grip-color: var(--kj-border-strong);
9
+
10
+ position: fixed;
11
+ left: 0;
12
+ right: 0;
13
+ bottom: 0;
14
+ display: flex;
15
+ flex-direction: column;
16
+ box-sizing: border-box;
17
+ width: 100%;
18
+ max-height: var(--kj-sheet-max-height);
19
+ margin-inline: auto;
20
+ background: var(--kj-sheet-bg);
21
+ color: var(--kj-sheet-fg);
22
+ box-shadow: var(--kj-sheet-shadow);
23
+ border-top-left-radius: var(--kj-sheet-radius);
24
+ border-top-right-radius: var(--kj-sheet-radius);
25
+ overflow: hidden;
26
+ z-index: var(--kj-overlay-z, 1000);
27
+ /* Live drag follows the pointer; released via --kj-sheet-drag-offset. */
28
+ transform: translate3d(0, var(--kj-sheet-drag-offset, 0), 0);
29
+ /* Respect the iOS home-indicator / gesture inset. */
30
+ padding-bottom: env(safe-area-inset-bottom, 0);
31
+ }
32
+
33
+ /* Constrain width on large viewports so it reads as a sheet, not a bar. */
34
+ @media (min-width: 40rem) {
35
+ .kj-sheet {
36
+ max-width: 32rem;
37
+ border-bottom-left-radius: var(--kj-sheet-radius);
38
+ border-bottom-right-radius: var(--kj-sheet-radius);
39
+ bottom: var(--kj-space-lg);
40
+ }
41
+ }
42
+
43
+ .kj-sheet[data-kj-detent="half"] { height: 50vh; }
44
+ .kj-sheet[data-kj-detent="full"] { height: var(--kj-sheet-max-height); }
45
+
46
+ /* Grab handle — a ≥44px touch target with a small visible grip pill. */
47
+ .kj-sheet__handle {
48
+ appearance: none;
49
+ display: flex;
50
+ align-items: center;
51
+ justify-content: center;
52
+ width: 100%;
53
+ min-height: 44px;
54
+ padding: 0;
55
+ border: 0;
56
+ background: transparent;
57
+ cursor: grab;
58
+ flex-shrink: 0;
59
+ touch-action: none;
60
+ }
61
+ .kj-sheet[data-kj-dragging] .kj-sheet__handle { cursor: grabbing; }
62
+
63
+ .kj-sheet__grip {
64
+ display: block;
65
+ width: 2.25rem;
66
+ height: 0.3125rem;
67
+ border-radius: 999px;
68
+ background: var(--kj-sheet-grip-color);
69
+ }
70
+
71
+ .kj-sheet__handle:focus-visible {
72
+ outline: var(--kj-focus-ring-width, 2px) solid var(--kj-focus-ring-color, var(--kj-primary));
73
+ outline-offset: -4px;
74
+ border-radius: var(--kj-radius-sm);
75
+ }
76
+
77
+ .kj-sheet__content {
78
+ flex: 1;
79
+ min-height: 0;
80
+ overflow-y: auto;
81
+ padding: 0 var(--kj-space-xl) var(--kj-space-xl);
82
+ color: var(--kj-sheet-fg);
83
+ font: 0.9375rem / 1.5 var(--kj-font-sans);
84
+ }
85
+
86
+ @media (prefers-reduced-motion: no-preference) {
87
+ .kj-sheet:not([data-kj-dragging]) {
88
+ transition: transform 260ms cubic-bezier(0.32, 0.72, 0, 1);
89
+ }
90
+ .kj-sheet[data-state="open"] { transform: translate3d(0, var(--kj-sheet-drag-offset, 0), 0); }
91
+ .kj-sheet[data-state="closed"] { transform: translate3d(0, 100%, 0); }
92
+ }
93
+
94
+ @media (prefers-reduced-motion: reduce) {
95
+ .kj-sheet:not([data-kj-dragging]) {
96
+ transition: opacity 120ms linear;
97
+ }
98
+ .kj-sheet[data-state="open"] { opacity: 1; }
99
+ .kj-sheet[data-state="closed"] { opacity: 0; }
100
+ }
101
+
102
+ /* Header / title / footer helpers for sheet bodies (parallel to drawer). */
103
+ .kj-sheet__title {
104
+ display: block;
105
+ margin: 0 0 var(--kj-space-xs);
106
+ font: 600 1.125rem / 1.3 var(--kj-font-sans);
107
+ color: var(--kj-fg-default);
108
+ }
109
+ .kj-sheet__description {
110
+ display: block;
111
+ margin: 0 0 var(--kj-space-md);
112
+ color: var(--kj-fg-muted);
113
+ font: 0.875rem / 1.4 var(--kj-font-sans);
114
+ }
115
+ .kj-sheet__footer {
116
+ display: flex;
117
+ gap: var(--kj-space-sm);
118
+ padding-top: var(--kj-space-md);
119
+ }
120
+ .kj-sheet__footer[data-align="end"] { justify-content: flex-end; }
121
+ }
@@ -0,0 +1,111 @@
1
+ /* ─────────────────────────────────────────────────────────────
2
+ .kj-skeleton — component-layer tokens + structural CSS
3
+ The directive lands `aria-hidden="true"` and reflects
4
+ `data-shape` / `data-animation` on the host. CSS owns the
5
+ shimmer / pulse keyframes and the reduced-motion override —
6
+ per `skeleton.md`, the directive deliberately does *not*
7
+ detect `prefers-reduced-motion` itself; theme CSS does, here.
8
+ ──────────────────────────────────────────────────────────── */
9
+
10
+ @layer kj.component {
11
+ .kj-skeleton {
12
+ /* component knobs — themes/users can override any of these */
13
+ --kj-skeleton-bg: var(--kj-bg-elevated, color-mix(in oklab, var(--kj-fg-default, #000) 12%, transparent));
14
+ --kj-skeleton-highlight: var(--kj-bg-surface, color-mix(in oklab, var(--kj-fg-default, #000) 5%, transparent));
15
+ --kj-skeleton-radius: var(--kj-radius-field, 0.375rem);
16
+ --kj-skeleton-line-height: 1em;
17
+ --kj-skeleton-line-radius: 0.25rem;
18
+ --kj-skeleton-shimmer-duration: 1.4s;
19
+ --kj-skeleton-pulse-duration: 1.6s;
20
+
21
+ display: block;
22
+ background-color: var(--kj-skeleton-bg);
23
+ border-radius: var(--kj-skeleton-radius);
24
+ /* Sized by consumer-supplied width/height. Default to a sensible
25
+ rectangle so a bare <kj-skeleton/> still paints something. */
26
+ inline-size: 100%;
27
+ block-size: 1rem;
28
+ overflow: hidden;
29
+ position: relative;
30
+ }
31
+
32
+ /* ── Shape routing ──────────────────────────────────────────────────── */
33
+
34
+ .kj-skeleton[data-shape="circle"] {
35
+ border-radius: 50%;
36
+ aspect-ratio: 1 / 1;
37
+ }
38
+
39
+ .kj-skeleton[data-shape="text"],
40
+ .kj-skeleton.kj-skeleton--line {
41
+ block-size: var(--kj-skeleton-line-height);
42
+ border-radius: var(--kj-skeleton-line-radius);
43
+ }
44
+
45
+ /* The text-block host is a layout container — it doesn't paint itself,
46
+ it stacks N child .kj-skeleton--line elements. Drop the default
47
+ paint chrome so only the lines are visible. */
48
+ .kj-skeleton[data-shape="text-block"] {
49
+ display: flex;
50
+ flex-direction: column;
51
+ gap: var(--kj-space-sm, 0.5rem);
52
+ background-color: transparent;
53
+ border-radius: 0;
54
+ inline-size: 100%;
55
+ block-size: auto;
56
+ overflow: visible;
57
+ }
58
+
59
+ /* ── Animation routing ──────────────────────────────────────────────── */
60
+
61
+ /* Shimmer: a moving highlight gradient sweeps left-to-right. */
62
+ .kj-skeleton[data-animation="shimmer"]::after {
63
+ content: "";
64
+ position: absolute;
65
+ inset: 0;
66
+ background: linear-gradient(
67
+ 90deg,
68
+ transparent 0%,
69
+ var(--kj-skeleton-highlight) 50%,
70
+ transparent 100%
71
+ );
72
+ transform: translateX(-100%);
73
+ animation: kj-skeleton-shimmer var(--kj-skeleton-shimmer-duration) linear infinite;
74
+ }
75
+
76
+ /* The text-block host doesn't paint, so its ::after sweep would be a
77
+ ghost. Suppress it; each child line runs its own shimmer. */
78
+ .kj-skeleton[data-shape="text-block"][data-animation="shimmer"]::after {
79
+ content: none;
80
+ }
81
+
82
+ .kj-skeleton[data-animation="pulse"] {
83
+ animation: kj-skeleton-pulse var(--kj-skeleton-pulse-duration) ease-in-out infinite;
84
+ }
85
+
86
+ .kj-skeleton[data-animation="none"]::after {
87
+ content: none;
88
+ }
89
+
90
+ @keyframes kj-skeleton-shimmer {
91
+ 0% { transform: translateX(-100%); }
92
+ 100% { transform: translateX(100%); }
93
+ }
94
+
95
+ @keyframes kj-skeleton-pulse {
96
+ 0%, 100% { opacity: 1; }
97
+ 50% { opacity: 0.55; }
98
+ }
99
+
100
+ /* ── Reduced motion ─────────────────────────────────────────────────── */
101
+ /* Per skeleton.md: when the OS sets the preference, theme CSS pins the
102
+ animation off regardless of `kjSkeletonAnimation`. The directive
103
+ deliberately does not detect the media query — this rule is the
104
+ enforcement layer. */
105
+ @media (prefers-reduced-motion: reduce) {
106
+ .kj-skeleton,
107
+ .kj-skeleton::after {
108
+ animation: none !important;
109
+ }
110
+ }
111
+ }