@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.
- package/LICENSE +9 -0
- package/README.md +16 -0
- package/dist/accordion/accordion.css +194 -0
- package/dist/alert/alert.css +138 -0
- package/dist/autocomplete/autocomplete.css +193 -0
- package/dist/avatar/avatar.css +142 -0
- package/dist/avatar-group/avatar-group.css +42 -0
- package/dist/badge/badge.css +74 -0
- package/dist/breadcrumb/breadcrumb.css +71 -0
- package/dist/button/button.css +318 -0
- package/dist/button/index.d.ts +1 -0
- package/dist/button/index.js +6 -0
- package/dist/button-group/button-group.css +108 -0
- package/dist/card/card.css +219 -0
- package/dist/carousel/carousel.css +170 -0
- package/dist/checkbox/checkbox.css +98 -0
- package/dist/chunk-K45KLI3Y.js +74 -0
- package/dist/collapsible/collapsible.css +36 -0
- package/dist/combobox/combobox.css +106 -0
- package/dist/combobox/combobox.tomselect.css +251 -0
- package/dist/config-CARtrJ7I.d.ts +61 -0
- package/dist/dialog/dialog.css +258 -0
- package/dist/drawer/drawer.css +318 -0
- package/dist/empty-state/empty-state.css +138 -0
- package/dist/field/field.css +70 -0
- package/dist/icon-box/icon-box.css +64 -0
- package/dist/illustration/illustration.css +103 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +60 -0
- package/dist/indicator/indicator.css +84 -0
- package/dist/input/input.css +220 -0
- package/dist/input-group/input-group.css +141 -0
- package/dist/kbd/kbd.css +55 -0
- package/dist/link/link.css +28 -0
- package/dist/list-group/list-group.css +261 -0
- package/dist/media/media.css +115 -0
- package/dist/menu/menu.css +237 -0
- package/dist/meter/meter.css +124 -0
- package/dist/navbar/navbar.css +170 -0
- package/dist/page/page.css +95 -0
- package/dist/pagination/pagination.css +125 -0
- package/dist/placeholders/placeholders.css +58 -0
- package/dist/popover/popover.css +251 -0
- package/dist/progress/progress.css +139 -0
- package/dist/radio/radio.css +81 -0
- package/dist/scroll-area/scroll-area.css +25 -0
- package/dist/scroll-area/scroll-area.overlayscrollbars.css +42 -0
- package/dist/select/select.css +282 -0
- package/dist/separator/separator.css +26 -0
- package/dist/sidebar/sidebar.css +493 -0
- package/dist/slider/slider.css +159 -0
- package/dist/spinner/spinner.css +65 -0
- package/dist/switch/switch.css +91 -0
- package/dist/table/table.css +284 -0
- package/dist/tabs/tabs.css +137 -0
- package/dist/textarea/textarea.css +99 -0
- package/dist/timeline/timeline.css +271 -0
- package/dist/toast/toast.css +267 -0
- package/dist/toggle/toggle.css +125 -0
- package/dist/toggle-group/toggle-group.css +87 -0
- package/dist/tooltip/tooltip.css +95 -0
- package/package.json +46 -0
- 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
|
+
}
|