@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,318 @@
|
|
|
1
|
+
/* @stisla/style — Drawer. Ported from src/scss/components/_drawer.scss. An edge-anchored sheet: a
|
|
2
|
+
* frosted backdrop dims the page, the panel slides in from a viewport edge (default --end). Sharp
|
|
3
|
+
* flush corners are the affordance; only the inner edge carries a border. Shown via
|
|
4
|
+
* [data-state="open"] on the root. References the @theme tokens: colors var(--color-*),
|
|
5
|
+
* sizes/spacing --spacing(n), type var(--text-*) / var(--leading-*) / var(--font-weight-*), radius
|
|
6
|
+
* var(--radius-*), shadow var(--shadow-*); only no-namespace customs use --st-* (border-width,
|
|
7
|
+
* duration, z-index); z-index routes through the z-index scale (--z-index-modal); the backdrop scrim is a bespoke oklch alpha
|
|
8
|
+
* (no scrim token) and blur radii stay literal (filter geometry). State + scroll-lock hooks
|
|
9
|
+
* ([data-shaking], html[data-drawer-open]) are attributes, recast from the legacy is-* classes per
|
|
10
|
+
* the no-is-* convention. Knobs are --drawer-*. Open/close + focus-trap behavior ships with the JS
|
|
11
|
+
* layer. @layer components. Authoring rules: ../../../../PORTING.md */
|
|
12
|
+
|
|
13
|
+
@layer components {
|
|
14
|
+
.drawer {
|
|
15
|
+
position: fixed;
|
|
16
|
+
inset: 0;
|
|
17
|
+
z-index: var(--drawer-z-index, var(--z-index-modal));
|
|
18
|
+
display: none;
|
|
19
|
+
pointer-events: none;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.drawer[data-state="open"] {
|
|
23
|
+
display: block;
|
|
24
|
+
pointer-events: auto;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
.drawer__backdrop {
|
|
28
|
+
position: absolute;
|
|
29
|
+
inset: 0;
|
|
30
|
+
background-color: var(--drawer-backdrop-bg, oklch(0 0 0 / 0.55));
|
|
31
|
+
backdrop-filter: blur(var(--drawer-backdrop-blur, 12px));
|
|
32
|
+
-webkit-backdrop-filter: blur(var(--drawer-backdrop-blur, 12px));
|
|
33
|
+
opacity: 0;
|
|
34
|
+
transition: opacity var(--drawer-transition-duration, var(--transition-duration-normal)) ease;
|
|
35
|
+
cursor: pointer;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
.drawer[data-state="open"] .drawer__backdrop {
|
|
39
|
+
opacity: 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/* Backdrop opt-out — hide the backdrop element entirely; the JS skips the dismiss-on-click path so
|
|
43
|
+
the page behind stays interactive (filter panels, inspector strips). */
|
|
44
|
+
.drawer[data-stisla-drawer-backdrop="false"] .drawer__backdrop {
|
|
45
|
+
display: none;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.drawer__content {
|
|
49
|
+
position: absolute;
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: column;
|
|
52
|
+
background-color: var(--drawer-bg, var(--color-surface));
|
|
53
|
+
color: var(--drawer-color, var(--color-foreground));
|
|
54
|
+
box-shadow: var(--drawer-shadow, var(--shadow-lg));
|
|
55
|
+
overflow: hidden;
|
|
56
|
+
outline: none;
|
|
57
|
+
transition: transform var(--drawer-transition-duration, var(--transition-duration-normal)) ease;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* === Placement === each pins the panel to a viewport edge and parks the content off-screen by
|
|
61
|
+
100% at rest. Only the inner edge carries a border. Default (bare .drawer) paints as --end. */
|
|
62
|
+
.drawer .drawer__content,
|
|
63
|
+
.drawer--end .drawer__content {
|
|
64
|
+
top: 0;
|
|
65
|
+
bottom: 0;
|
|
66
|
+
inset-inline-end: 0;
|
|
67
|
+
width: var(--drawer-width, --spacing(88));
|
|
68
|
+
max-width: 100vw;
|
|
69
|
+
border-inline-start: var(--drawer-border-width, var(--st-border-width)) solid
|
|
70
|
+
var(--drawer-border-color, var(--color-border));
|
|
71
|
+
transform: translateX(100%);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
.drawer--start .drawer__content {
|
|
75
|
+
top: 0;
|
|
76
|
+
bottom: 0;
|
|
77
|
+
inset-inline-start: 0;
|
|
78
|
+
inset-inline-end: auto;
|
|
79
|
+
width: var(--drawer-width, --spacing(88));
|
|
80
|
+
max-width: 100vw;
|
|
81
|
+
border-inline-start: 0;
|
|
82
|
+
border-inline-end: var(--drawer-border-width, var(--st-border-width)) solid
|
|
83
|
+
var(--drawer-border-color, var(--color-border));
|
|
84
|
+
transform: translateX(-100%);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.drawer--top .drawer__content {
|
|
88
|
+
top: 0;
|
|
89
|
+
inset-inline: 0;
|
|
90
|
+
bottom: auto;
|
|
91
|
+
width: auto;
|
|
92
|
+
height: var(--drawer-height, --spacing(64));
|
|
93
|
+
max-height: 100dvh;
|
|
94
|
+
border: 0;
|
|
95
|
+
border-bottom: var(--drawer-border-width, var(--st-border-width)) solid
|
|
96
|
+
var(--drawer-border-color, var(--color-border));
|
|
97
|
+
transform: translateY(-100%);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.drawer--bottom .drawer__content {
|
|
101
|
+
top: auto;
|
|
102
|
+
bottom: 0;
|
|
103
|
+
inset-inline: 0;
|
|
104
|
+
width: auto;
|
|
105
|
+
height: var(--drawer-height, --spacing(64));
|
|
106
|
+
max-height: 100dvh;
|
|
107
|
+
border: 0;
|
|
108
|
+
border-top: var(--drawer-border-width, var(--st-border-width)) solid
|
|
109
|
+
var(--drawer-border-color, var(--color-border));
|
|
110
|
+
transform: translateY(100%);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/* RTL — inset-inline-* already anchors the panel on the correct physical edge, but translateX is
|
|
114
|
+
physical and can't be expressed logically, so flip the sign for the two inline-axis placements.
|
|
115
|
+
--top / --bottom use translateY (block-axis, direction-agnostic) and stay unchanged. */
|
|
116
|
+
[dir="rtl"] .drawer .drawer__content,
|
|
117
|
+
[dir="rtl"] .drawer--end .drawer__content {
|
|
118
|
+
transform: translateX(-100%);
|
|
119
|
+
}
|
|
120
|
+
[dir="rtl"] .drawer--start .drawer__content {
|
|
121
|
+
transform: translateX(100%);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/* Open — slide into place. */
|
|
125
|
+
.drawer[data-state="open"] .drawer__content {
|
|
126
|
+
transform: none;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/* === Floating === detaches the panel from the viewport: a gap on every side, all four corners
|
|
130
|
+
rounded, a full border, reading as a raised card rather than a flush sheet. Stacks onto any
|
|
131
|
+
placement (bare = end). The rest transform adds the gap on top of the 100% slide so the panel
|
|
132
|
+
still parks fully off-screen. */
|
|
133
|
+
.drawer--floating .drawer__content,
|
|
134
|
+
.drawer--floating.drawer--start .drawer__content,
|
|
135
|
+
.drawer--floating.drawer--end .drawer__content,
|
|
136
|
+
.drawer--floating.drawer--top .drawer__content,
|
|
137
|
+
.drawer--floating.drawer--bottom .drawer__content {
|
|
138
|
+
border: var(--drawer-border-width, var(--st-border-width)) solid
|
|
139
|
+
var(--drawer-border-color, var(--color-border));
|
|
140
|
+
border-radius: var(--drawer-radius, var(--radius-lg));
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
.drawer--floating .drawer__content,
|
|
144
|
+
.drawer--floating.drawer--end .drawer__content {
|
|
145
|
+
top: var(--drawer-gap, --spacing(3));
|
|
146
|
+
bottom: var(--drawer-gap, --spacing(3));
|
|
147
|
+
inset-inline-end: var(--drawer-gap, --spacing(3));
|
|
148
|
+
transform: translateX(calc(100% + var(--drawer-gap, --spacing(3))));
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.drawer--floating.drawer--start .drawer__content {
|
|
152
|
+
top: var(--drawer-gap, --spacing(3));
|
|
153
|
+
bottom: var(--drawer-gap, --spacing(3));
|
|
154
|
+
inset-inline-start: var(--drawer-gap, --spacing(3));
|
|
155
|
+
inset-inline-end: auto;
|
|
156
|
+
transform: translateX(calc(-100% - var(--drawer-gap, --spacing(3))));
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.drawer--floating.drawer--top .drawer__content {
|
|
160
|
+
top: var(--drawer-gap, --spacing(3));
|
|
161
|
+
bottom: auto;
|
|
162
|
+
inset-inline: var(--drawer-gap, --spacing(3));
|
|
163
|
+
transform: translateY(calc(-100% - var(--drawer-gap, --spacing(3))));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.drawer--floating.drawer--bottom .drawer__content {
|
|
167
|
+
top: auto;
|
|
168
|
+
bottom: var(--drawer-gap, --spacing(3));
|
|
169
|
+
inset-inline: var(--drawer-gap, --spacing(3));
|
|
170
|
+
transform: translateY(calc(100% + var(--drawer-gap, --spacing(3))));
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/* RTL — flip the inline-axis floating transforms (mirrors the base block). */
|
|
174
|
+
[dir="rtl"] .drawer--floating .drawer__content,
|
|
175
|
+
[dir="rtl"] .drawer--floating.drawer--end .drawer__content {
|
|
176
|
+
transform: translateX(calc(-100% - var(--drawer-gap, --spacing(3))));
|
|
177
|
+
}
|
|
178
|
+
[dir="rtl"] .drawer--floating.drawer--start .drawer__content {
|
|
179
|
+
transform: translateX(calc(100% + var(--drawer-gap, --spacing(3))));
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/* Open — slide into place. Last so it overrides every floating rest transform, LTR and RTL alike. */
|
|
183
|
+
.drawer--floating[data-state="open"] .drawer__content {
|
|
184
|
+
transform: none;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* === Parts === */
|
|
188
|
+
.drawer__header {
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: --spacing(2);
|
|
192
|
+
flex: 0 0 auto;
|
|
193
|
+
padding: var(--drawer-padding-block, --spacing(5)) var(--drawer-padding-inline, --spacing(5));
|
|
194
|
+
padding-block-end: 0;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
.drawer__title {
|
|
198
|
+
margin: 0;
|
|
199
|
+
font-size: var(--drawer-title-font-size, var(--text-lg));
|
|
200
|
+
font-weight: var(--drawer-title-font-weight, var(--font-weight-semibold));
|
|
201
|
+
line-height: var(--leading-tight);
|
|
202
|
+
color: var(--drawer-color, var(--color-foreground));
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
.drawer__body {
|
|
206
|
+
flex: 1 1 auto;
|
|
207
|
+
min-height: 0;
|
|
208
|
+
padding: var(--drawer-padding-block, --spacing(5)) var(--drawer-padding-inline, --spacing(5));
|
|
209
|
+
color: var(--drawer-color, var(--color-foreground));
|
|
210
|
+
overflow-y: auto;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.drawer__footer {
|
|
214
|
+
display: flex;
|
|
215
|
+
flex: 0 0 auto;
|
|
216
|
+
flex-wrap: wrap;
|
|
217
|
+
align-items: center;
|
|
218
|
+
justify-content: flex-end;
|
|
219
|
+
gap: --spacing(2);
|
|
220
|
+
padding: var(--drawer-footer-padding-block, --spacing(3.5))
|
|
221
|
+
var(--drawer-padding-inline, --spacing(5));
|
|
222
|
+
background-color: var(--drawer-footer-bg, var(--color-surface-2));
|
|
223
|
+
border-top: var(--st-border-width) solid var(--drawer-footer-border-color, var(--color-border));
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* Inline ghost dismiss chip, pushed to the trailing edge whether the header wraps the title
|
|
227
|
+
directly or in a centered column. */
|
|
228
|
+
.drawer__close {
|
|
229
|
+
margin-inline-start: auto;
|
|
230
|
+
display: inline-flex;
|
|
231
|
+
align-items: center;
|
|
232
|
+
justify-content: center;
|
|
233
|
+
flex-shrink: 0;
|
|
234
|
+
width: var(--drawer-close-size, --spacing(7));
|
|
235
|
+
height: var(--drawer-close-size, --spacing(7));
|
|
236
|
+
padding: 0;
|
|
237
|
+
color: var(--drawer-close-color, var(--color-muted-foreground));
|
|
238
|
+
background-color: transparent;
|
|
239
|
+
border: 0;
|
|
240
|
+
border-radius: 50%;
|
|
241
|
+
cursor: pointer;
|
|
242
|
+
transition:
|
|
243
|
+
background-color var(--transition-duration-fast) ease,
|
|
244
|
+
color var(--transition-duration-fast) ease;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
.drawer__close:hover {
|
|
248
|
+
color: var(--drawer-close-color-hover, var(--color-foreground));
|
|
249
|
+
background-color: var(--drawer-close-bg-hover, var(--color-accent));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.drawer__close:focus-visible {
|
|
253
|
+
outline: 2px solid var(--color-ring);
|
|
254
|
+
outline-offset: 2px;
|
|
255
|
+
color: var(--drawer-close-color-hover, var(--color-foreground));
|
|
256
|
+
background-color: var(--drawer-close-bg-hover, var(--color-accent));
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.drawer__close > svg,
|
|
260
|
+
.drawer__close > i {
|
|
261
|
+
width: --spacing(4);
|
|
262
|
+
height: --spacing(4);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/* === Static backdrop shake === the behavior layer sets [data-shaking] on the panel when a click
|
|
266
|
+
hits a static backdrop. The shake axis matches the placement axis so the nudge reads as "stuck"
|
|
267
|
+
along the slide direction. */
|
|
268
|
+
.drawer__content[data-shaking] {
|
|
269
|
+
animation: st-drawer-shake-x var(--transition-duration-normal) ease-in-out;
|
|
270
|
+
}
|
|
271
|
+
.drawer--top .drawer__content[data-shaking],
|
|
272
|
+
.drawer--bottom .drawer__content[data-shaking] {
|
|
273
|
+
animation-name: st-drawer-shake-y;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/* === Scroll lock === applied to <html> while any backdrop-bearing drawer is open; scrollbar-gutter
|
|
278
|
+
keeps the page width stable. */
|
|
279
|
+
html[data-drawer-open] {
|
|
280
|
+
overflow: hidden;
|
|
281
|
+
scrollbar-gutter: stable;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
@keyframes st-drawer-shake-x {
|
|
285
|
+
0%,
|
|
286
|
+
100% {
|
|
287
|
+
transform: none;
|
|
288
|
+
}
|
|
289
|
+
25% {
|
|
290
|
+
transform: translateX(calc(--spacing(1) * -1));
|
|
291
|
+
}
|
|
292
|
+
75% {
|
|
293
|
+
transform: translateX(--spacing(1));
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
@keyframes st-drawer-shake-y {
|
|
298
|
+
0%,
|
|
299
|
+
100% {
|
|
300
|
+
transform: none;
|
|
301
|
+
}
|
|
302
|
+
25% {
|
|
303
|
+
transform: translateY(calc(--spacing(1) * -1));
|
|
304
|
+
}
|
|
305
|
+
75% {
|
|
306
|
+
transform: translateY(--spacing(1));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@media (prefers-reduced-motion: reduce) {
|
|
311
|
+
.drawer__backdrop,
|
|
312
|
+
.drawer__content {
|
|
313
|
+
transition: none;
|
|
314
|
+
}
|
|
315
|
+
.drawer__content[data-shaking] {
|
|
316
|
+
animation: none;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/* @stisla/style — Empty state. Ported from src/scss/components/_empty-state.scss. A centred block
|
|
2
|
+
* shown in place of absent content (an empty list, no results, a fresh account, a 404, an error).
|
|
3
|
+
* Pure layout + media chrome, no JS. BEM: .empty-state + __media / __title / __text / __action; tone
|
|
4
|
+
* modifiers colour the media, --sm shrinks it for a card/table, --bordered draws a dashed region. The
|
|
5
|
+
* media slot is a tinted circle holding one glyph by default; drop in an .illustration / <img> /
|
|
6
|
+
* .icon-box / .avatar / .spinner and the :has() sheds below strip the circle so richer art isn't
|
|
7
|
+
* double-framed. References the @theme tokens: colors var(--color-*), sizes/spacing --spacing(n), type
|
|
8
|
+
* var(--text-*) / var(--leading-*) / var(--font-weight-*), radius var(--radius-*); only no-namespace
|
|
9
|
+
* customs use --st-* (border-width). Knobs are --empty-state-*. @layer components.
|
|
10
|
+
* Authoring rules: ../../../../PORTING.md */
|
|
11
|
+
|
|
12
|
+
@layer components {
|
|
13
|
+
.empty-state {
|
|
14
|
+
display: flex;
|
|
15
|
+
flex-direction: column;
|
|
16
|
+
align-items: center;
|
|
17
|
+
text-align: center;
|
|
18
|
+
/* A measure so the text wraps to a comfortable line; margin-inline auto centres horizontally,
|
|
19
|
+
vertical fill is the consumer's (wrap it in a flex/grid cell). */
|
|
20
|
+
max-width: var(--empty-state-max-width, --spacing(96));
|
|
21
|
+
margin-inline: auto;
|
|
22
|
+
padding-block: var(--empty-state-padding-block, --spacing(10));
|
|
23
|
+
padding-inline: var(--empty-state-padding-inline, --spacing(4));
|
|
24
|
+
color: var(--color-foreground);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/* === Media === default: a soft-tinted circle holding one glyph, accent from the tone knob. The
|
|
28
|
+
tint mixes in oklab toward the surface so a saturated tone stays on-hue. */
|
|
29
|
+
.empty-state__media {
|
|
30
|
+
display: inline-flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
flex-shrink: 0;
|
|
34
|
+
box-sizing: border-box;
|
|
35
|
+
width: var(--empty-state-media-size, --spacing(16));
|
|
36
|
+
height: var(--empty-state-media-size, --spacing(16));
|
|
37
|
+
margin-block-end: var(--empty-state-media-gap, --spacing(5));
|
|
38
|
+
color: var(--empty-state-tone, var(--color-muted-foreground));
|
|
39
|
+
background: color-mix(in oklab, var(--empty-state-tone, var(--color-muted-foreground)) 12%, var(--color-surface));
|
|
40
|
+
border-radius: 9999px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Bare glyph pins to a sensible size so the media reads the same across icon packs; :only-child
|
|
44
|
+
leaves a composed media (icon + badge) to size itself. */
|
|
45
|
+
.empty-state__media > :is(svg, i):only-child {
|
|
46
|
+
width: var(--empty-state-icon-size, --spacing(7));
|
|
47
|
+
height: var(--empty-state-icon-size, --spacing(7));
|
|
48
|
+
flex-shrink: 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Richer media brings its own shape, so the slot sheds the circle + tint and becomes a plain
|
|
52
|
+
centring box (illustration spot art, raw <img>, or the borrowed icon-box / avatar / spinner). */
|
|
53
|
+
.empty-state__media:is(
|
|
54
|
+
:has(> .illustration),
|
|
55
|
+
:has(> img),
|
|
56
|
+
:has(> .icon-box),
|
|
57
|
+
:has(> .avatar),
|
|
58
|
+
:has(> .spinner)
|
|
59
|
+
) {
|
|
60
|
+
width: auto;
|
|
61
|
+
height: auto;
|
|
62
|
+
background: none;
|
|
63
|
+
border-radius: 0;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/* Spot art and bitmaps fill the slot up to a cap so a tall illustration never dominates. */
|
|
67
|
+
.empty-state__media > .illustration,
|
|
68
|
+
.empty-state__media > img {
|
|
69
|
+
width: 100%;
|
|
70
|
+
max-width: var(--empty-state-art-size, --spacing(40));
|
|
71
|
+
height: auto;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/* === Parts === */
|
|
75
|
+
.empty-state__title {
|
|
76
|
+
margin: 0;
|
|
77
|
+
font-size: var(--text-base);
|
|
78
|
+
font-weight: var(--font-weight-semibold);
|
|
79
|
+
line-height: var(--leading-snug);
|
|
80
|
+
color: var(--color-foreground);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/* Tight rhythm under the title; the larger gaps are the media's bottom margin and the action's top
|
|
84
|
+
margin, so title and text always sit together. */
|
|
85
|
+
.empty-state__text {
|
|
86
|
+
margin-block: var(--empty-state-gap, --spacing(1.5)) 0;
|
|
87
|
+
font-size: var(--text-sm);
|
|
88
|
+
line-height: var(--leading-normal);
|
|
89
|
+
color: var(--color-muted-foreground);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/* Trailing controls — centred and wrapping so a primary + secondary pair stacks on a narrow region. */
|
|
93
|
+
.empty-state__action {
|
|
94
|
+
display: flex;
|
|
95
|
+
flex-wrap: wrap;
|
|
96
|
+
align-items: center;
|
|
97
|
+
justify-content: center;
|
|
98
|
+
gap: --spacing(2);
|
|
99
|
+
margin-block-start: var(--empty-state-action-gap, --spacing(5));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/* === Tone modifiers === set --empty-state-tone; the media fill, tint, and glyph colour re-resolve.
|
|
103
|
+
Title and text stay foreground / muted so the message reads as text, not colour. */
|
|
104
|
+
.empty-state--primary {
|
|
105
|
+
--empty-state-tone: var(--color-primary);
|
|
106
|
+
}
|
|
107
|
+
.empty-state--success {
|
|
108
|
+
--empty-state-tone: var(--color-success);
|
|
109
|
+
}
|
|
110
|
+
.empty-state--warning {
|
|
111
|
+
--empty-state-tone: var(--color-warning);
|
|
112
|
+
}
|
|
113
|
+
.empty-state--danger {
|
|
114
|
+
--empty-state-tone: var(--color-danger);
|
|
115
|
+
}
|
|
116
|
+
.empty-state--info {
|
|
117
|
+
--empty-state-tone: var(--color-info);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/* === Small === for an empty region inside a card, a panel, or an empty table body: smaller media
|
|
121
|
+
and tighter padding so it doesn't dominate a confined space. */
|
|
122
|
+
.empty-state--sm {
|
|
123
|
+
--empty-state-media-size: --spacing(11);
|
|
124
|
+
--empty-state-icon-size: --spacing(5);
|
|
125
|
+
--empty-state-media-gap: --spacing(3);
|
|
126
|
+
--empty-state-action-gap: --spacing(3);
|
|
127
|
+
--empty-state-padding-block: --spacing(6);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/* === Bordered === a dashed region box (a droppable / fillable empty area); the border colour rides
|
|
131
|
+
the tone so an error region can read red. */
|
|
132
|
+
.empty-state--bordered {
|
|
133
|
+
border: var(--st-border-width) dashed
|
|
134
|
+
var(--empty-state-border-color, color-mix(in oklch, var(--empty-state-tone, var(--color-border)) 60%, var(--color-border)));
|
|
135
|
+
border-radius: var(--empty-state-radius, var(--radius-lg));
|
|
136
|
+
background: var(--empty-state-bg, transparent);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/* @stisla/style — Field. Ported from src/scss/components/_field.scss. Wrapper that groups a label +
|
|
2
|
+
* control + helper text, plus an item element for inline input + label rows. Pairs with any form
|
|
3
|
+
* control (.input, .select, .textarea, .checkbox, .radio, .switch, .slider) and owns the layout
|
|
4
|
+
* contract. References the @theme tokens: colors var(--color-*), spacing --spacing(n), type
|
|
5
|
+
* var(--text-*) / var(--font-weight-*). Knobs are --field-* (fallback-default). @layer components.
|
|
6
|
+
* Authoring rules: ../../../../PORTING.md */
|
|
7
|
+
|
|
8
|
+
@layer components {
|
|
9
|
+
/* Root — vertical stack (label / control / helper). Fills its column by default: the controls
|
|
10
|
+
inside carry width: 100%, so without a width a shrink-to-fit parent would collapse them. */
|
|
11
|
+
.field {
|
|
12
|
+
display: flex;
|
|
13
|
+
flex-direction: column;
|
|
14
|
+
gap: var(--field-gap, --spacing(1.5));
|
|
15
|
+
width: 100%;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/* Wrapping flex row — for groups of .field__item on one line (radio sets, inline checkbox lists). */
|
|
19
|
+
.field--inline {
|
|
20
|
+
flex-direction: row;
|
|
21
|
+
flex-wrap: wrap;
|
|
22
|
+
align-items: center;
|
|
23
|
+
--field-gap: --spacing(4);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.field__label {
|
|
27
|
+
font-size: var(--field-label-font-size, var(--text-sm));
|
|
28
|
+
font-weight: var(--field-label-font-weight, var(--font-weight-medium));
|
|
29
|
+
color: var(--field-label-color, var(--color-foreground));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
.field__description {
|
|
33
|
+
font-size: var(--field-helper-font-size, var(--text-sm));
|
|
34
|
+
color: var(--field-helper-color, var(--color-muted-foreground));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/* Error-tone helper; pair with [aria-invalid="true"] on the control via aria-describedby. */
|
|
38
|
+
.field__error {
|
|
39
|
+
font-size: var(--field-helper-font-size, var(--text-sm));
|
|
40
|
+
color: var(--field-error-color, var(--color-danger));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Inline input + label row (checkbox, radio, settings row). Lives inside .field. */
|
|
44
|
+
.field__item {
|
|
45
|
+
display: flex;
|
|
46
|
+
align-items: center;
|
|
47
|
+
gap: var(--field-item-gap, --spacing(2));
|
|
48
|
+
padding-block: var(--field-item-padding-block, --spacing(0.5));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/* Label-first, input-last — justify-content pins the input to the row end (settings rows). */
|
|
52
|
+
.field__item--reverse {
|
|
53
|
+
flex-direction: row-reverse;
|
|
54
|
+
justify-content: flex-end;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/* Inside an item, the label flips to the row-typography contract: regular weight, clickable. */
|
|
58
|
+
.field__item > .field__label {
|
|
59
|
+
font-weight: var(--field-item-label-font-weight, var(--font-weight-normal));
|
|
60
|
+
cursor: pointer;
|
|
61
|
+
line-height: var(--leading-snug);
|
|
62
|
+
user-select: none;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* A disabled control inside an item dims its label. Covers checkbox / radio / switch. */
|
|
66
|
+
.field__item:has(> :is(.checkbox, .radio, .switch):disabled) > .field__label {
|
|
67
|
+
cursor: not-allowed;
|
|
68
|
+
opacity: var(--field-item-disabled-opacity, 0.55);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* @stisla/style — Icon box. Ported from src/scss/components/_icon-box.scss. A square tinted container
|
|
2
|
+
* for a single icon (Stisla original). Intent modifiers set --icon-box-tone; the bg (a 15% tint, same
|
|
3
|
+
* as .badge--soft) and color re-resolve from it. References the @theme tokens (colors var(--color-*),
|
|
4
|
+
* spacing/sizes --spacing(n), radius var(--radius-*)). Knobs are --icon-box-*. Sizes compact/roomy →
|
|
5
|
+
* sm/lg; pill radius literal 9999px. @layer components. Authoring rules: ../../../../PORTING.md */
|
|
6
|
+
|
|
7
|
+
@layer components {
|
|
8
|
+
.icon-box {
|
|
9
|
+
display: inline-flex;
|
|
10
|
+
align-items: center;
|
|
11
|
+
justify-content: center;
|
|
12
|
+
flex-shrink: 0;
|
|
13
|
+
width: var(--icon-box-size, --spacing(9));
|
|
14
|
+
height: var(--icon-box-size, --spacing(9));
|
|
15
|
+
color: var(--icon-box-color, var(--icon-box-tone, var(--color-muted-foreground)));
|
|
16
|
+
background: var(
|
|
17
|
+
--icon-box-bg,
|
|
18
|
+
color-mix(in oklch, var(--icon-box-tone, var(--color-muted-foreground)) 15%, transparent)
|
|
19
|
+
);
|
|
20
|
+
border-radius: var(--icon-box-radius, var(--radius-sm));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Icon stays a fixed size regardless of surrounding font-size (so not em). */
|
|
24
|
+
.icon-box :is(svg, i) {
|
|
25
|
+
width: var(--icon-box-icon-size, --spacing(4.5));
|
|
26
|
+
height: var(--icon-box-icon-size, --spacing(4.5));
|
|
27
|
+
flex-shrink: 0;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* === Intent modifiers === override --icon-box-tone; bg + color both re-resolve from the new tone. */
|
|
31
|
+
.icon-box--primary {
|
|
32
|
+
--icon-box-tone: var(--color-primary);
|
|
33
|
+
}
|
|
34
|
+
.icon-box--success {
|
|
35
|
+
--icon-box-tone: var(--color-success);
|
|
36
|
+
}
|
|
37
|
+
.icon-box--warning {
|
|
38
|
+
--icon-box-tone: var(--color-warning);
|
|
39
|
+
}
|
|
40
|
+
.icon-box--danger {
|
|
41
|
+
--icon-box-tone: var(--color-danger);
|
|
42
|
+
}
|
|
43
|
+
.icon-box--info {
|
|
44
|
+
--icon-box-tone: var(--color-info);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/* === Shape === */
|
|
48
|
+
.icon-box--round {
|
|
49
|
+
border-radius: 9999px;
|
|
50
|
+
}
|
|
51
|
+
.icon-box--square {
|
|
52
|
+
border-radius: 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* === Sizes (base = md) === */
|
|
56
|
+
.icon-box--sm {
|
|
57
|
+
--icon-box-size: --spacing(7);
|
|
58
|
+
--icon-box-icon-size: --spacing(3.5);
|
|
59
|
+
}
|
|
60
|
+
.icon-box--lg {
|
|
61
|
+
--icon-box-size: --spacing(12);
|
|
62
|
+
--icon-box-icon-size: --spacing(6);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/* @stisla/style — Illustration. Ported from src/scss/components/_illustration.scss. Soft volumetric
|
|
2
|
+
* spot art (empty states, dialogs, onboarding, success/error screens). Every object is shaded from ONE
|
|
3
|
+
* hue: the gradient stops, the backing disc, and the long shadow all derive from --illus-accent via
|
|
4
|
+
* color-mix, so recolouring never touches the SVG markup — set --illus-accent (and --illus-badge for
|
|
5
|
+
* the corner pip) and the whole piece follows. Default is neutral; intent modifiers recolour; an
|
|
6
|
+
* ancestor or inline --illus-accent overrides everything. Motion is opt-in via --animate.
|
|
7
|
+
*
|
|
8
|
+
* NOTE: the short --illus-* knobs, the .il-* SVG paint hooks, and the il-* keyframes are kept as the
|
|
9
|
+
* illustration's deliberate, cohesive vocabulary — they're referenced inside the SVG art assets, so
|
|
10
|
+
* renaming them to --illustration-* / .illustration__* would ripple through every illustration file.
|
|
11
|
+
* Only the token refs are converted: --st-* colours → var(--color-*). The white mix targets, the
|
|
12
|
+
* drop-shadow geometry, and the ambient loop durations have no token and stay literal.
|
|
13
|
+
* @layer components. Authoring rules: ../../../../PORTING.md */
|
|
14
|
+
|
|
15
|
+
@layer components {
|
|
16
|
+
.illustration {
|
|
17
|
+
/* Resolve a PRIVATE accent from the public knob with a fallback. The public --illus-accent is
|
|
18
|
+
never set on the element itself, so an ancestor (a gallery, an .empty-state) or a modifier can
|
|
19
|
+
override it — a custom property set on the element would shadow inheritance and freeze it. */
|
|
20
|
+
--_a: var(--illus-accent, var(--color-muted-foreground));
|
|
21
|
+
/* Badge follows the accent by default (monochrome); override --illus-badge for a pop colour. */
|
|
22
|
+
--_b: var(--illus-badge, var(--_a));
|
|
23
|
+
|
|
24
|
+
display: block;
|
|
25
|
+
width: 100%;
|
|
26
|
+
height: auto;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Gradient stops: light → mid → deep, all from the one accent. The light stops sit well above the
|
|
30
|
+
disc tint so faces never melt into the glow. */
|
|
31
|
+
.illustration .il-g0 {
|
|
32
|
+
stop-color: color-mix(in oklch, var(--_a) 34%, white);
|
|
33
|
+
}
|
|
34
|
+
.illustration .il-g1 {
|
|
35
|
+
stop-color: color-mix(in oklch, var(--_a) 58%, white);
|
|
36
|
+
}
|
|
37
|
+
.illustration .il-g2 {
|
|
38
|
+
stop-color: color-mix(in oklch, var(--_a) 82%, white);
|
|
39
|
+
}
|
|
40
|
+
.illustration .il-g3 {
|
|
41
|
+
stop-color: var(--_a);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/* Soft circular badge behind the object, two layers for depth. */
|
|
45
|
+
.illustration .il-disc-o {
|
|
46
|
+
fill: color-mix(in oklch, var(--_a) 6%, transparent);
|
|
47
|
+
}
|
|
48
|
+
.illustration .il-disc-i {
|
|
49
|
+
fill: color-mix(in oklch, var(--_a) 10%, transparent);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/* The object lifts off the disc; shadow falls down-right from the locked top-left light. */
|
|
53
|
+
.illustration .il-obj {
|
|
54
|
+
filter: drop-shadow(5px 9px 11px color-mix(in oklch, var(--_a) 26%, transparent));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.illustration--primary {
|
|
58
|
+
--illus-accent: var(--color-primary);
|
|
59
|
+
}
|
|
60
|
+
.illustration--success {
|
|
61
|
+
--illus-accent: var(--color-success);
|
|
62
|
+
--illus-badge: var(--color-success);
|
|
63
|
+
}
|
|
64
|
+
.illustration--danger {
|
|
65
|
+
--illus-accent: var(--color-danger);
|
|
66
|
+
--illus-badge: var(--color-danger);
|
|
67
|
+
}
|
|
68
|
+
.illustration--warning {
|
|
69
|
+
--illus-accent: var(--color-warning);
|
|
70
|
+
--illus-badge: var(--color-warning);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@media (prefers-reduced-motion: no-preference) {
|
|
74
|
+
.illustration--animate .il-obj {
|
|
75
|
+
animation: il-float 5.5s ease-in-out infinite;
|
|
76
|
+
}
|
|
77
|
+
.illustration--animate .il-badge {
|
|
78
|
+
animation: il-pop 3.4s ease-in-out infinite;
|
|
79
|
+
transform-box: fill-box;
|
|
80
|
+
transform-origin: center;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
@keyframes il-float {
|
|
86
|
+
0%,
|
|
87
|
+
100% {
|
|
88
|
+
transform: translateY(0);
|
|
89
|
+
}
|
|
90
|
+
50% {
|
|
91
|
+
transform: translateY(-5px);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@keyframes il-pop {
|
|
96
|
+
0%,
|
|
97
|
+
100% {
|
|
98
|
+
transform: scale(1);
|
|
99
|
+
}
|
|
100
|
+
50% {
|
|
101
|
+
transform: scale(1.12);
|
|
102
|
+
}
|
|
103
|
+
}
|