@stisla/style 3.0.0-beta.8

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 (63) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +16 -0
  3. package/dist/accordion/accordion.css +194 -0
  4. package/dist/alert/alert.css +138 -0
  5. package/dist/autocomplete/autocomplete.css +193 -0
  6. package/dist/avatar/avatar.css +142 -0
  7. package/dist/avatar-group/avatar-group.css +42 -0
  8. package/dist/badge/badge.css +74 -0
  9. package/dist/breadcrumb/breadcrumb.css +71 -0
  10. package/dist/button/button.css +318 -0
  11. package/dist/button/index.d.ts +1 -0
  12. package/dist/button/index.js +6 -0
  13. package/dist/button-group/button-group.css +108 -0
  14. package/dist/card/card.css +219 -0
  15. package/dist/carousel/carousel.css +170 -0
  16. package/dist/checkbox/checkbox.css +98 -0
  17. package/dist/chunk-K45KLI3Y.js +74 -0
  18. package/dist/collapsible/collapsible.css +36 -0
  19. package/dist/combobox/combobox.css +106 -0
  20. package/dist/combobox/combobox.tomselect.css +251 -0
  21. package/dist/config-CARtrJ7I.d.ts +61 -0
  22. package/dist/dialog/dialog.css +258 -0
  23. package/dist/drawer/drawer.css +318 -0
  24. package/dist/empty-state/empty-state.css +138 -0
  25. package/dist/field/field.css +70 -0
  26. package/dist/icon-box/icon-box.css +64 -0
  27. package/dist/illustration/illustration.css +103 -0
  28. package/dist/index.d.ts +14 -0
  29. package/dist/index.js +60 -0
  30. package/dist/indicator/indicator.css +84 -0
  31. package/dist/input/input.css +220 -0
  32. package/dist/input-group/input-group.css +141 -0
  33. package/dist/kbd/kbd.css +55 -0
  34. package/dist/link/link.css +28 -0
  35. package/dist/list-group/list-group.css +261 -0
  36. package/dist/media/media.css +115 -0
  37. package/dist/menu/menu.css +237 -0
  38. package/dist/meter/meter.css +124 -0
  39. package/dist/navbar/navbar.css +170 -0
  40. package/dist/page/page.css +95 -0
  41. package/dist/pagination/pagination.css +125 -0
  42. package/dist/placeholders/placeholders.css +58 -0
  43. package/dist/popover/popover.css +251 -0
  44. package/dist/progress/progress.css +139 -0
  45. package/dist/radio/radio.css +81 -0
  46. package/dist/scroll-area/scroll-area.css +25 -0
  47. package/dist/scroll-area/scroll-area.overlayscrollbars.css +42 -0
  48. package/dist/select/select.css +282 -0
  49. package/dist/separator/separator.css +26 -0
  50. package/dist/sidebar/sidebar.css +493 -0
  51. package/dist/slider/slider.css +159 -0
  52. package/dist/spinner/spinner.css +65 -0
  53. package/dist/switch/switch.css +91 -0
  54. package/dist/table/table.css +284 -0
  55. package/dist/tabs/tabs.css +137 -0
  56. package/dist/textarea/textarea.css +99 -0
  57. package/dist/timeline/timeline.css +271 -0
  58. package/dist/toast/toast.css +267 -0
  59. package/dist/toggle/toggle.css +125 -0
  60. package/dist/toggle-group/toggle-group.css +87 -0
  61. package/dist/tooltip/tooltip.css +95 -0
  62. package/package.json +46 -0
  63. package/src/theme.css +151 -0
@@ -0,0 +1,493 @@
1
+ /* @stisla/style — Sidebar. Ported from src/scss/components/_sidebar.scss. Stisla-original
2
+ * app-shell navigation panel. References the @theme tokens: colors var(--color-*), spacing
3
+ * --spacing(n), type var(--text-*) / var(--font-weight-*), radius var(--radius-*). Only
4
+ * no-namespace customs use --st-* (border-width). Knobs are --sidebar-* (fallback-default).
5
+ * @layer components. Authoring rules: ../../../../PORTING.md
6
+ *
7
+ * State hooks (attributes, never is-*):
8
+ * .sidebar[data-collapsed] rail / mini mode (root)
9
+ * .sidebar__button[aria-current="page"] current page (nav links)
10
+ * .sidebar__button[data-state="active"] current state (non-link, Radix-aligned)
11
+ * .sidebar__item[data-state="open|closed"] submenu visibility
12
+ * .sidebar__button[aria-expanded="true|false"] submenu trigger
13
+ *
14
+ * Animation timings are kept as literal durations: they are hand-tuned choreography (e.g. the
15
+ * 0.075s opacity vs 0.25s transform on the item-action fade), not design-scale values, and our
16
+ * duration customs don't match them. The one tunable timing is --sidebar-transition-duration. */
17
+
18
+ @layer components {
19
+ /* Fallback-default knobs: nothing on the base, so a scope / inline retunes any of them; every
20
+ child reads var(--sidebar-x, <default>) with the default repeated at each site. Width vars are
21
+ opt-in (default 16rem; set --sidebar-width / -width-collapsed to let the panel own + animate
22
+ its width through [data-collapsed]). Padding lives on the child slots, not the root, so
23
+ .sidebar__content stays flush with the panel edge and focus rings aren't clipped. */
24
+ .sidebar {
25
+ display: flex;
26
+ flex-direction: column;
27
+ gap: var(--sidebar-gap, --spacing(4));
28
+ width: var(--sidebar-width, 16rem);
29
+ background-color: var(--sidebar-bg, transparent);
30
+ color: var(--sidebar-color, var(--color-foreground));
31
+ transition: width var(--sidebar-transition-duration, 0.3s) ease;
32
+ }
33
+
34
+ /* === Header / footer === */
35
+ .sidebar__header {
36
+ display: flex;
37
+ align-items: center;
38
+ /* Outer inset = panel gutter + button inner padding, so brand content sits on the same x-grid
39
+ as menu icons below. Re-derived in rail mode to center the brand icon on the icon column. */
40
+ padding-block-start: var(--sidebar-padding-block, --spacing(4));
41
+ padding-inline: calc(
42
+ var(--sidebar-padding-inline, --spacing(4)) +
43
+ var(--sidebar-button-padding-inline, --spacing(2.5))
44
+ );
45
+ transition: padding var(--sidebar-transition-duration, 0.3s) ease;
46
+ }
47
+
48
+ .sidebar__footer {
49
+ margin-block-start: auto;
50
+ padding-block-end: var(--sidebar-padding-block, --spacing(4));
51
+ padding-inline: var(--sidebar-padding-inline, --spacing(4));
52
+ transition: padding var(--sidebar-transition-duration, 0.3s) ease;
53
+ }
54
+
55
+ /* === Content (scroll slot) === */
56
+ .sidebar__content {
57
+ flex: 1 1 auto;
58
+ /* Canonical flexbox-overflow combo: without min-height: 0 the child can't shrink below content
59
+ height in Chrome/Firefox. */
60
+ min-height: 0;
61
+ padding-inline: var(--sidebar-padding-inline, --spacing(4));
62
+ transition: padding var(--sidebar-transition-duration, 0.3s) ease;
63
+ /* Explicit x: hidden + y: auto — plain overflow-y: auto pops a horizontal scrollbar mid-collapse
64
+ when a label's intrinsic min-width briefly exceeds the rail interior. */
65
+ overflow: hidden auto;
66
+ }
67
+
68
+ /* Header-less / footer-less panels: content absorbs the missing edge's padding so the first/last
69
+ row doesn't butt against the panel edge. */
70
+ .sidebar:not(:has(> .sidebar__header)) > .sidebar__content {
71
+ padding-block-start: var(--sidebar-padding-block, --spacing(4));
72
+ }
73
+ .sidebar:not(:has(> .sidebar__footer)) > .sidebar__content {
74
+ padding-block-end: var(--sidebar-padding-block, --spacing(4));
75
+ }
76
+
77
+ /* === Brand === */
78
+ .sidebar__brand {
79
+ display: inline-flex;
80
+ align-items: center;
81
+ gap: var(--sidebar-brand-gap, --spacing(2.5));
82
+ color: var(--sidebar-brand-color, var(--sidebar-color, var(--color-foreground)));
83
+ font-weight: var(--font-weight-semibold);
84
+ text-decoration: none;
85
+ white-space: nowrap;
86
+ user-select: none;
87
+ }
88
+
89
+ .sidebar__brand :is(svg, i, img) {
90
+ flex-shrink: 0;
91
+ width: var(--sidebar-brand-icon-size, --spacing(6));
92
+ height: var(--sidebar-brand-icon-size, --spacing(6));
93
+ }
94
+
95
+ /* === Menu === */
96
+ .sidebar__menu {
97
+ display: flex;
98
+ flex-direction: column;
99
+ gap: var(--sidebar-group-gap, --spacing(4));
100
+ }
101
+
102
+ /* === Group ===
103
+ * Default: title + list stacked. With a group-action present, switch to a 2-col grid
104
+ * (title 1fr | action auto) and span the list across both columns below. */
105
+ .sidebar__group {
106
+ display: flex;
107
+ flex-direction: column;
108
+ gap: --spacing(1);
109
+ }
110
+
111
+ .sidebar__group:has(> .sidebar__group-action) {
112
+ display: grid;
113
+ grid-template-columns: 1fr auto;
114
+ align-items: center;
115
+ column-gap: --spacing(2);
116
+ }
117
+
118
+ .sidebar__group:has(> .sidebar__group-action) > .sidebar__list {
119
+ grid-column: 1 / -1;
120
+ }
121
+
122
+ .sidebar__group-title {
123
+ display: inline-flex;
124
+ align-items: center;
125
+ gap: --spacing(1.5);
126
+ padding: 0 var(--sidebar-button-padding-inline, --spacing(2.5));
127
+ color: var(--sidebar-group-title-color, var(--color-muted-foreground));
128
+ font-size: var(--sidebar-group-title-font-size, var(--text-xs));
129
+ font-weight: var(--sidebar-group-title-font-weight, var(--font-weight-normal));
130
+ }
131
+
132
+ .sidebar__group-action {
133
+ display: inline-flex;
134
+ align-items: center;
135
+ gap: --spacing(1);
136
+ padding-inline-end: calc(
137
+ var(--sidebar-button-padding-inline, --spacing(2.5)) - --spacing(1)
138
+ );
139
+ color: var(--sidebar-group-title-color, var(--color-muted-foreground));
140
+ }
141
+
142
+ /* === List & item === */
143
+ .sidebar__list {
144
+ /* Preflight already zeroes ul margin/padding + list-style; only the flex layout is set here. */
145
+ display: flex;
146
+ flex-direction: column;
147
+ gap: --spacing(0.5);
148
+ }
149
+
150
+ .sidebar__item {
151
+ position: relative;
152
+ display: flex;
153
+ width: 100%;
154
+ }
155
+
156
+ /* Items with a submenu stack their button + submenu vertically. */
157
+ .sidebar__item:has(> .sidebar__submenu) {
158
+ flex-direction: column;
159
+ align-items: stretch;
160
+ }
161
+
162
+ /* === Button (item) ===
163
+ * Hard-height contract: height: var(--height), padding-block: 0, line-height: 1. Single-line;
164
+ * truncates rather than wraps if forced narrow. */
165
+ .sidebar__button {
166
+ display: inline-flex;
167
+ align-items: center;
168
+ gap: var(--sidebar-button-gap, --spacing(4));
169
+ width: 100%;
170
+ height: var(--sidebar-button-height, --spacing(9));
171
+ padding-block: var(--sidebar-button-padding-block, 0);
172
+ padding-inline: var(--sidebar-button-padding-inline, --spacing(2.5));
173
+ border: 0;
174
+ border-radius: var(--sidebar-button-radius, var(--radius-sm));
175
+ background-color: transparent;
176
+ color: var(--sidebar-button-color, var(--sidebar-color, var(--color-foreground)));
177
+ font-weight: var(--sidebar-button-font-weight, var(--font-weight-normal));
178
+ line-height: var(--leading-none);
179
+ white-space: nowrap;
180
+ text-align: start;
181
+ text-decoration: none;
182
+ cursor: pointer;
183
+ transition:
184
+ width 0.2s ease,
185
+ background-color 0.1s ease,
186
+ color 0.1s ease;
187
+
188
+ /* Excluding active + disabled from hover keeps the active row reading steady (no transient
189
+ hover repaint over an already-tinted chip). */
190
+ &:hover:not(:disabled):not([aria-disabled="true"]):not([aria-current="page"]):not(
191
+ [data-state="active"]
192
+ ) {
193
+ background-color: var(--sidebar-button-bg-hover, var(--color-accent));
194
+ color: var(--sidebar-button-color-hover, var(--color-accent-foreground));
195
+ }
196
+
197
+ &:focus-visible {
198
+ outline: 2px solid var(--color-ring);
199
+ outline-offset: 2px;
200
+ }
201
+
202
+ &:disabled,
203
+ &[aria-disabled="true"] {
204
+ opacity: 0.55;
205
+ cursor: not-allowed;
206
+ }
207
+ }
208
+
209
+ .sidebar__button :is(svg, i) {
210
+ flex-shrink: 0;
211
+ width: var(--sidebar-button-icon-size, --spacing(4));
212
+ height: var(--sidebar-button-icon-size, --spacing(4));
213
+ color: var(--sidebar-button-icon-color, var(--color-muted-foreground));
214
+ }
215
+
216
+ /* Active state — aria-current="page" for nav links, data-state="active" for non-link rows
217
+ (Radix-aligned). The icon flips with the label so the active row reads as one block; hover
218
+ stays icon-quiet by design. */
219
+ .sidebar__button[aria-current="page"],
220
+ .sidebar__button[data-state="active"] {
221
+ background-color: var(--sidebar-button-bg-active, var(--color-highlight));
222
+ color: var(--sidebar-button-color-active, var(--color-highlight-foreground));
223
+ }
224
+
225
+ .sidebar__button[aria-current="page"] :is(svg, i),
226
+ .sidebar__button[data-state="active"] :is(svg, i) {
227
+ color: var(--sidebar-button-color-active, var(--color-highlight-foreground));
228
+ }
229
+
230
+ /* === Item action ===
231
+ * Absolute-positioned right-edge slot. The button stays the large hit target; the action overlays
232
+ * its right edge, with a padding-right carve-out triggered only when an action is present. */
233
+ .sidebar__item:has(> .sidebar__item-action) > .sidebar__button {
234
+ padding-inline-end: calc(var(--sidebar-item-action-size, --spacing(9)) + --spacing(1));
235
+ }
236
+
237
+ .sidebar__item-action {
238
+ position: absolute;
239
+ top: 50%;
240
+ inset-inline-end: var(--sidebar-button-padding-inline, --spacing(2.5));
241
+ transform: translateY(-50%);
242
+ display: inline-flex;
243
+ align-items: center;
244
+ justify-content: center;
245
+ height: var(--sidebar-item-action-size, --spacing(9));
246
+ z-index: 1;
247
+ transition:
248
+ opacity 0.075s ease,
249
+ transform 0.25s ease;
250
+ }
251
+
252
+ .sidebar__item-action :is(svg, i) {
253
+ width: var(--sidebar-button-icon-size, --spacing(4));
254
+ height: var(--sidebar-button-icon-size, --spacing(4));
255
+ }
256
+
257
+ /* Submenu-toggle variant — when a link-parent row needs a disclosure, the action slot becomes the
258
+ toggle <button> beside the navigable <a>: a self-contained square with the caret centered. */
259
+ button.sidebar__item-action {
260
+ width: --spacing(7);
261
+ height: --spacing(7);
262
+ inset-inline-end: --spacing(1.5);
263
+ border: 0;
264
+ border-radius: var(--sidebar-button-radius, var(--radius-sm));
265
+ background-color: transparent;
266
+ color: inherit;
267
+ cursor: pointer;
268
+ transition: background-color 0.1s ease;
269
+ }
270
+
271
+ button.sidebar__item-action:hover {
272
+ background-color: var(--sidebar-button-bg-hover, var(--color-accent));
273
+ }
274
+
275
+ button.sidebar__item-action:focus-visible {
276
+ outline: 2px solid var(--color-ring);
277
+ outline-offset: 2px;
278
+ }
279
+
280
+ /* Center the caret in the square — drop the base inline-start auto margin it carries when inline. */
281
+ button.sidebar__item-action > .sidebar__caret {
282
+ margin-inline-start: 0;
283
+ }
284
+
285
+ /* On a submenu parent the item is a flex column (button + submenu), so the action's default 50%
286
+ offset would track the whole open block. Pin it to the button row's center instead. */
287
+ .sidebar__item:has(> .sidebar__submenu) > .sidebar__item-action {
288
+ top: calc(var(--sidebar-button-height, --spacing(9)) / 2);
289
+ }
290
+
291
+ /* Reveal-on-hover modifier — action fades in on row hover / keyboard focus. */
292
+ .sidebar__item-action--reveal {
293
+ opacity: 0;
294
+ transition: opacity 0.1s ease;
295
+ }
296
+ .sidebar__item:hover .sidebar__item-action--reveal,
297
+ .sidebar__item:focus-within .sidebar__item-action--reveal {
298
+ opacity: 1;
299
+ }
300
+
301
+ /* Buttons inside an action slot collapse to a compact square regardless of the modifiers the
302
+ consumer passes. The descendant selector's specificity (not source order) lands the override;
303
+ the consumer just picks a tone via .button--ghost.button--neutral. */
304
+ .sidebar__item-action .button,
305
+ .sidebar__group-action .button {
306
+ --button-height: --spacing(6);
307
+ --button-padding-inline: 0;
308
+ --button-radius: var(--radius-sm);
309
+ width: --spacing(6);
310
+ gap: 0;
311
+ }
312
+
313
+ /* === Submenu ===
314
+ * CSS-only collapse — closed submenus disappear via display: none. A guide line (left border)
315
+ * anchored at the brand-icon column so child rows visually descend from the parent's icon. */
316
+ .sidebar__submenu {
317
+ margin-inline-start: var(--sidebar-submenu-margin-inline-start, --spacing(4.5));
318
+ padding-inline-start: var(--sidebar-submenu-padding-inline-start, --spacing(3.25));
319
+ border-inline-start: var(--st-border-width) solid
320
+ var(--sidebar-submenu-border-color, var(--color-border));
321
+ }
322
+
323
+ .sidebar__item[data-state="closed"] > .sidebar__submenu {
324
+ display: none;
325
+ }
326
+
327
+ /* Breathing room between a parent toggle button and its submenu. Lives on the button (outside the
328
+ submenu) so a future height-measuring animation isn't thrown off by inner padding. */
329
+ .sidebar__item:has(> .sidebar__submenu) > .sidebar__button {
330
+ margin-block-end: --spacing(0.5);
331
+ }
332
+
333
+ /* Submenu caret — opt-in <span class="sidebar__caret"></span> inside a toggle. CSS draws the
334
+ chevron via a mask so it follows --sidebar-button-icon-color and stays crisp at any size.
335
+ Defaults to the inline-end edge via margin-inline-start: auto; rotates 180° when the parent
336
+ reports aria-expanded="true". */
337
+ .sidebar__caret {
338
+ display: inline-block;
339
+ flex-shrink: 0;
340
+ width: var(--sidebar-button-icon-size, --spacing(4));
341
+ height: var(--sidebar-button-icon-size, --spacing(4));
342
+ margin-inline-start: auto;
343
+ background-color: var(--sidebar-button-icon-color, var(--color-muted-foreground));
344
+ -webkit-mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E")
345
+ no-repeat center / contain;
346
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E")
347
+ no-repeat center / contain;
348
+ transition: transform 0.15s ease;
349
+ }
350
+
351
+ .sidebar__button[aria-expanded="true"] > .sidebar__caret,
352
+ .sidebar__item-action[aria-expanded="true"] > .sidebar__caret {
353
+ transform: rotate(180deg);
354
+ }
355
+
356
+ /* === Size modifiers (base = md) ===
357
+ * Override button-level vars + group gap only. Outer panel padding stays at the default scale so
358
+ * header / content / footer have visually identical gutters at any size. */
359
+ .sidebar--sm {
360
+ --sidebar-button-height: --spacing(8);
361
+ --sidebar-button-padding-inline: --spacing(2);
362
+ --sidebar-group-gap: --spacing(3.5);
363
+ }
364
+
365
+ .sidebar--lg {
366
+ --sidebar-button-height: --spacing(10);
367
+ --sidebar-button-padding-inline: --spacing(3);
368
+ --sidebar-group-gap: --spacing(4.5);
369
+ }
370
+
371
+ /* === Rail / mini-collapsed ([data-collapsed]) ===
372
+ * Labels, group titles, chevrons, and item actions fade out instead of snapping, so the width
373
+ * animation reads as a single coordinated motion. Rail-width recipe (square icon cell, centered
374
+ * icon): width = button-height + 2 × panel-padding-inline. Collapsed mode trims
375
+ * --sidebar-padding-inline so the rail hugs the square cell. */
376
+
377
+ /* Soft-hide targets — always primed for an opacity + translate fade so the rail toggle doesn't
378
+ blink. Lives in the base rule (not under [data-collapsed]) so transitions run both directions. */
379
+ .sidebar__button > span,
380
+ .sidebar__brand > :not(svg):not(i):not(img) {
381
+ transition:
382
+ opacity var(--sidebar-transition-duration, 0.3s) ease,
383
+ transform var(--sidebar-transition-duration, 0.3s) ease;
384
+ }
385
+
386
+ /* Group title + action soft-collapse — height transitions need a numeric target (auto → 0 doesn't
387
+ interpolate), so max-height anchors expanded at a value tall enough to never clip the title's
388
+ natural box but small enough to transition cleanly to 0. */
389
+ .sidebar__group-title,
390
+ .sidebar__group-action {
391
+ max-height: 3rem;
392
+ overflow: hidden;
393
+ transition:
394
+ max-height var(--sidebar-transition-duration, 0.3s) ease,
395
+ margin var(--sidebar-transition-duration, 0.3s) ease,
396
+ padding var(--sidebar-transition-duration, 0.3s) ease,
397
+ opacity 0.15s ease,
398
+ transform var(--sidebar-transition-duration, 0.3s) ease;
399
+ }
400
+
401
+ .sidebar[data-collapsed] {
402
+ /* Panel-level clip — catches anything wider than the rail without per-leaf display: none. */
403
+ overflow: hidden;
404
+
405
+ /* Collapsed width derives from the content: the square icon cell plus its symmetric padding.
406
+ A real length (not fit-content) so the collapse animates against the expanded width out of
407
+ the box. The +border-width keeps the cell square when the panel carries a border. */
408
+ width: var(
409
+ --sidebar-width-collapsed,
410
+ calc(
411
+ var(--sidebar-button-height, --spacing(9)) + 2 *
412
+ var(--sidebar-padding-inline, --spacing(2.5)) + var(--st-border-width)
413
+ )
414
+ );
415
+
416
+ /* Trim the panel gutter so the rail hugs the square icon cell. The header re-center calc and
417
+ content padding both read this var, and the content's padding transition animates the trim. */
418
+ --sidebar-padding-inline: --spacing(2.5);
419
+
420
+ /* Brand-column re-center: trim the header's inline padding by half the delta between brand-icon
421
+ and button-icon so the larger brand icon shares the menu-icon column's center axis. */
422
+ .sidebar__header {
423
+ padding-inline: calc(
424
+ var(--sidebar-padding-inline, --spacing(4)) +
425
+ var(--sidebar-button-padding-inline, --spacing(2.5)) -
426
+ (
427
+ var(--sidebar-brand-icon-size, --spacing(6)) -
428
+ var(--sidebar-button-icon-size, --spacing(4))
429
+ ) / 2
430
+ );
431
+ overflow: hidden;
432
+ }
433
+
434
+ /* Hard clip each button — even a bare-text-node label stays clipped to the rail. flex-shrink: 0
435
+ so the rail button holds its square width against a sub-pixel border trim. */
436
+ .sidebar__button {
437
+ overflow: hidden;
438
+ flex-shrink: 0;
439
+ width: var(--sidebar-button-height, --spacing(9));
440
+ }
441
+
442
+ /* Soft-hide: fade label spans, brand wordmark, chevron suffix, and trailing item-action.
443
+ pointer-events: none keeps a faded action from intercepting clicks while invisible. */
444
+ .sidebar__button > span,
445
+ .sidebar__brand > :not(svg):not(i):not(img) {
446
+ opacity: 0;
447
+ pointer-events: none;
448
+ }
449
+ .sidebar__item-action {
450
+ opacity: 0;
451
+ pointer-events: none;
452
+ }
453
+
454
+ .sidebar__item:has(> .sidebar__item-action) > .sidebar__button {
455
+ padding: var(--sidebar-button-padding-inline, --spacing(2.5));
456
+ }
457
+
458
+ /* Group title + action collapse smoothly: max-height + margin + padding to 0, fade + slide-out. */
459
+ .sidebar__group-title,
460
+ .sidebar__group-action {
461
+ max-height: 0;
462
+ margin-block: 0;
463
+ padding-block: 0;
464
+ opacity: 0;
465
+ transform: translateY(-100%);
466
+ pointer-events: none;
467
+ }
468
+
469
+ /* Submenu stays hidden in rail mode — a vertical strip of labels reads broken at rail width.
470
+ data-state on the parent still tracks open/closed for when the rail expands. The
471
+ :not([data-collapsing]) carve-out lets a mid-close height animation run to completion
472
+ alongside the width transition (the JS behavior layer sets [data-collapsing]); it clears at
473
+ transitionend, by which point display: none snaps in invisibly. */
474
+ .sidebar__submenu:not([data-collapsing]) {
475
+ display: none;
476
+ }
477
+ }
478
+
479
+ /* === Reduced motion === */
480
+ @media (prefers-reduced-motion: reduce) {
481
+ .sidebar__header,
482
+ .sidebar__content,
483
+ .sidebar__footer,
484
+ .sidebar__brand > :not(svg):not(i):not(img),
485
+ .sidebar__button > span,
486
+ .sidebar__caret,
487
+ .sidebar__item-action,
488
+ .sidebar__group-title,
489
+ .sidebar__group-action {
490
+ transition-duration: 0.01ms;
491
+ }
492
+ }
493
+ }
@@ -0,0 +1,159 @@
1
+ /* @stisla/style — Slider. Ported from src/scss/components/_slider.scss. A JS-driven range input built
2
+ * from real DOM (.slider__track > .slider__range + .slider__thumb + hidden .slider__input) so every part
3
+ * is plain-CSS stylable — no pseudo-element gymnastics. The behavior layer (Stisla.Slider) owns pointer +
4
+ * keyboard + ARIA and writes --slider-fraction (0..1) on the host; this file owns paint and positions the
5
+ * thumb + range from that fraction. References the @theme tokens (colors var(--color-*), sizes/spacing
6
+ * --spacing(n)); only no-namespace customs use --st-* (border-width, duration). Pill radius is the literal
7
+ * 9999px (carries meaning, not a style choice — override --slider-radius to flatten). Sizes compact/roomy
8
+ * → sm/lg. Knobs are --slider-*. State is data-attribute (data-disabled / aria-invalid), no is-*.
9
+ * @layer components. Authoring rules: ../../../../PORTING.md */
10
+
11
+ @layer components {
12
+ /* Fallback-default knobs: nothing on the base, so a scope / inline retunes any of them; track / range /
13
+ thumb read them with the same defaults (thumb-w / -gap / radius / fraction repeat in the position
14
+ calcs). --slider-fraction is JS-written (0..1) inline; the 0 fallback holds before JS runs. */
15
+ .slider {
16
+ position: relative;
17
+ display: block;
18
+ width: 100%;
19
+ height: var(--slider-height, --spacing(9));
20
+ border-radius: var(--slider-radius, 9999px);
21
+ user-select: none;
22
+ cursor: pointer;
23
+ touch-action: none; /* own pointer; don't scroll on drag */
24
+
25
+ &[data-disabled="true"] {
26
+ opacity: 0.55;
27
+ cursor: not-allowed;
28
+ }
29
+ }
30
+
31
+ /* === Track === fills the shell. overflow:hidden clips the range's slight overflow at fraction=1 (range
32
+ width is 100% + thumb-gap so the fill stays visible to the thumb's right at every value). */
33
+ .slider__track {
34
+ position: absolute;
35
+ inset: 0;
36
+ background: var(--slider-track-bg, var(--color-neutral));
37
+ border-radius: var(--slider-radius, 9999px);
38
+ overflow: hidden;
39
+ transition: border-color var(--transition-duration-fast) ease;
40
+ }
41
+
42
+ /* === Range === filled segment. Width math (with edge inset = thumb-gap on each side):
43
+ thumb-right = thumb-w/2 + thumb-gap + fraction * (100% - thumb-w - 2*thumb-gap) + thumb-w/2
44
+ = thumb-w + thumb-gap + fraction * (100% - thumb-w - 2*thumb-gap)
45
+ width = thumb-right + thumb-gap
46
+ At fraction=0 the fill extends past the thumb on both sides by thumb-gap, so the thumb visually floats
47
+ in a small pool of fill before the unfilled segment begins. At fraction=1 the fill reaches 100% exactly.
48
+ No border-radius — the track's overflow:hidden + outer radius does the pill clipping; the range's right
49
+ edge stays straight (no rounded blob to the left of the thumb at low values). */
50
+ .slider__range {
51
+ position: absolute;
52
+ inset-block: 0;
53
+ inset-inline-start: 0;
54
+ width: calc(
55
+ var(--slider-thumb-width, --spacing(2)) + 2 * var(--slider-thumb-gap, 0.2rem) +
56
+ var(--slider-fraction, 0) *
57
+ (100% - var(--slider-thumb-width, --spacing(2)) - 2 * var(--slider-thumb-gap, 0.2rem))
58
+ );
59
+ background: var(--slider-fill, var(--color-primary));
60
+ }
61
+
62
+ /* === Thumb === vertical pill, centered on its position. Travel range is inset by (thumb-w/2 + thumb-gap)
63
+ on each side so the thumb stays fully visible at extremes AND keeps a thumb-gap of breathing room from
64
+ the track edge — no half-thumbs, no kissing the pill curve. */
65
+ .slider__thumb {
66
+ position: absolute;
67
+ top: 50%;
68
+ left: calc(
69
+ var(--slider-thumb-width, --spacing(2)) / 2 + var(--slider-thumb-gap, 0.2rem) +
70
+ var(--slider-fraction, 0) *
71
+ (100% - var(--slider-thumb-width, --spacing(2)) - 2 * var(--slider-thumb-gap, 0.2rem))
72
+ );
73
+ transform: translate(-50%, -50%);
74
+ width: var(--slider-thumb-width, --spacing(2));
75
+ height: var(--slider-thumb-height, --spacing(4));
76
+ background: var(--slider-thumb-bg, #fff);
77
+ border: 0;
78
+ border-radius: var(--slider-radius, 9999px);
79
+ box-shadow: var(--slider-thumb-shadow, 0 1px 3px oklch(0 0 0 / 0.25));
80
+ cursor: pointer;
81
+ touch-action: none;
82
+ transition:
83
+ transform var(--transition-duration-fast) ease,
84
+ box-shadow var(--transition-duration-fast) ease;
85
+
86
+ &:focus {
87
+ outline: none;
88
+ }
89
+
90
+ /* Focus halo as a second box-shadow layer so it composes with the drop shadow. focus-visible only
91
+ paints when keyboard-focused. */
92
+ &:focus-visible {
93
+ box-shadow:
94
+ var(--slider-thumb-shadow, 0 1px 3px oklch(0 0 0 / 0.25)),
95
+ 0 0 0 3px color-mix(in oklch, var(--slider-ring, var(--color-ring)) 25%, transparent);
96
+ }
97
+
98
+ /* Pressed feedback — preserve the centering translate. */
99
+ &:active {
100
+ transform: translate(-50%, -50%) scaleY(0.92);
101
+ }
102
+ }
103
+
104
+ /* === Hidden input === form participation only; the visible UI is the divs above. JS writes .value on
105
+ every change so the form payload reflects the current value. */
106
+ .slider__input {
107
+ position: absolute;
108
+ width: 1px;
109
+ height: 1px;
110
+ padding: 0;
111
+ margin: -1px;
112
+ overflow: hidden;
113
+ clip: rect(0, 0, 0, 0);
114
+ border: 0;
115
+ opacity: 0;
116
+ pointer-events: none;
117
+ }
118
+
119
+ /* === Validation === aria-invalid is the canonical hook (server / JS sets it explicitly). Native
120
+ :user-invalid no longer applies — the visible control is divs, not the hidden input — so we route
121
+ validity through aria-invalid only. */
122
+ .slider[aria-invalid="true"] .slider__track {
123
+ border: var(--st-border-width) solid var(--color-danger);
124
+ }
125
+ .slider[aria-invalid="true"] .slider__thumb:focus-visible {
126
+ box-shadow:
127
+ var(--slider-thumb-shadow, 0 1px 3px oklch(0 0 0 / 0.25)),
128
+ 0 0 0 3px color-mix(in oklch, var(--color-danger) 25%, transparent);
129
+ }
130
+
131
+ /* === Sizes (base = md) === track height matches .input--sm / --lg so sliders sit cleanly next to inputs
132
+ in the same row. Dims are --spacing() multiples. */
133
+ .slider--sm {
134
+ --slider-height: --spacing(7);
135
+ --slider-thumb-width: --spacing(1.5);
136
+ --slider-thumb-height: --spacing(4);
137
+ }
138
+ .slider--lg {
139
+ --slider-height: --spacing(11);
140
+ --slider-thumb-width: --spacing(2.5);
141
+ --slider-thumb-height: --spacing(6);
142
+ }
143
+
144
+ /* === Coarse pointer === touchscreens get a slightly larger thumb so it's easier to spot and grab.
145
+ Desktop (pointer: fine) keeps the compact default. */
146
+ @media (pointer: coarse) {
147
+ .slider {
148
+ --slider-thumb-width: --spacing(3);
149
+ --slider-thumb-height: --spacing(5);
150
+ }
151
+ }
152
+
153
+ @media (prefers-reduced-motion: reduce) {
154
+ .slider__thumb,
155
+ .slider__track {
156
+ transition: none;
157
+ }
158
+ }
159
+ }