@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,219 @@
|
|
|
1
|
+
/* @stisla/style — Card. Ported from src/scss/components/_card.scss. References the @theme
|
|
2
|
+
* tokens: colors var(--color-*), spacing --spacing(n), type var(--text-*) / var(--leading-*) /
|
|
3
|
+
* var(--font-weight-*), radius var(--radius-*), shadow var(--shadow-*). Only no-namespace customs
|
|
4
|
+
* use --st-* (border-width). Knobs are --card-* (fallback-default). @layer components.
|
|
5
|
+
* Authoring rules: ../../../../PORTING.md */
|
|
6
|
+
|
|
7
|
+
@layer components {
|
|
8
|
+
/* Fallback-default knobs: nothing set on the base, so a scope / inline retunes any of them.
|
|
9
|
+
--card-radius + --card-padding-inline are read by the parts (header / body / footer /
|
|
10
|
+
image / overlay), so their defaults repeat at those read sites. */
|
|
11
|
+
.card {
|
|
12
|
+
position: relative;
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
min-width: 0;
|
|
16
|
+
color: var(--card-color, var(--color-foreground));
|
|
17
|
+
background: var(--card-bg, var(--color-surface));
|
|
18
|
+
border: var(--card-border-width, var(--st-border-width)) solid
|
|
19
|
+
var(--card-border-color, var(--color-border));
|
|
20
|
+
border-radius: var(--card-radius, var(--radius-lg));
|
|
21
|
+
box-shadow: var(--card-shadow, var(--shadow-md));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/* Drop the shadow so the card reads as a frame rather than a raised surface. The border
|
|
25
|
+
stays on by default. */
|
|
26
|
+
.card--flat {
|
|
27
|
+
--card-shadow: none;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* === Parts ============================================================
|
|
31
|
+
* Body is flex column + gap so .card__title / __subtitle / __text don't need
|
|
32
|
+
* component-level margins. Parent owns the rhythm; children stay margin-zero (same model
|
|
33
|
+
* as .alert). */
|
|
34
|
+
.card__body {
|
|
35
|
+
display: flex;
|
|
36
|
+
flex-direction: column;
|
|
37
|
+
gap: --spacing(2);
|
|
38
|
+
padding: var(--card-padding-block, --spacing(5))
|
|
39
|
+
var(--card-padding-inline, --spacing(5));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.card__header {
|
|
43
|
+
min-height: var(--card-header-height, --spacing(15));
|
|
44
|
+
padding: 0 var(--card-padding-inline, --spacing(5));
|
|
45
|
+
border-bottom: var(--st-border-width) solid var(--color-border);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
.card__header,
|
|
49
|
+
.card__heading {
|
|
50
|
+
display: flex;
|
|
51
|
+
align-items: center;
|
|
52
|
+
gap: --spacing(2);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* Trailing action slot in the header (holds a link, a button, a menu trigger). The auto
|
|
56
|
+
margin is on the slot itself, so leading content packs to the start and only the action
|
|
57
|
+
moves to the end. Matches .alert__action / .page__action. */
|
|
58
|
+
.card__action {
|
|
59
|
+
display: inline-flex;
|
|
60
|
+
align-items: center;
|
|
61
|
+
gap: --spacing(2);
|
|
62
|
+
margin-inline-start: auto;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/* Round the top corners when the header is the card's first child, so a filled (--alt)
|
|
66
|
+
header fills the card's rounded top instead of showing square corners (the card carries
|
|
67
|
+
no overflow: hidden). Inner radius (outer − border) keeps it concentric with the border,
|
|
68
|
+
matching the image + footer. */
|
|
69
|
+
.card__header:first-child {
|
|
70
|
+
border-start-start-radius: calc(
|
|
71
|
+
var(--card-radius, var(--radius-lg)) -
|
|
72
|
+
var(--card-border-width, var(--st-border-width))
|
|
73
|
+
);
|
|
74
|
+
border-start-end-radius: calc(
|
|
75
|
+
var(--card-radius, var(--radius-lg)) -
|
|
76
|
+
var(--card-border-width, var(--st-border-width))
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* Opt the header onto the alt surface to mirror the footer's contrast. Composes with
|
|
81
|
+
.card__header: <div class="card__header card__header--alt">. */
|
|
82
|
+
.card__header--alt {
|
|
83
|
+
background: var(--color-surface-2);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
.card__header--compact {
|
|
87
|
+
--card-header-height: --spacing(11.5);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/* Flex breaks the inline strut leak: inline children get a fresh formatting context as flex
|
|
91
|
+
items, so their own line-height takes effect instead of the body's dictating the line-box.
|
|
92
|
+
Aligns gap behavior with .card__header too. */
|
|
93
|
+
.card__footer {
|
|
94
|
+
display: flex;
|
|
95
|
+
align-items: center;
|
|
96
|
+
gap: --spacing(2);
|
|
97
|
+
padding: --spacing(3) var(--card-padding-inline, --spacing(5));
|
|
98
|
+
background: var(--color-surface-2);
|
|
99
|
+
border-top: var(--st-border-width) solid var(--color-border);
|
|
100
|
+
border-radius: 0 0
|
|
101
|
+
calc(
|
|
102
|
+
var(--card-radius, var(--radius-lg)) -
|
|
103
|
+
var(--card-border-width, var(--st-border-width))
|
|
104
|
+
)
|
|
105
|
+
calc(
|
|
106
|
+
var(--card-radius, var(--radius-lg)) -
|
|
107
|
+
var(--card-border-width, var(--st-border-width))
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
.card__title {
|
|
112
|
+
font-size: var(--text-base);
|
|
113
|
+
font-weight: var(--font-weight-medium);
|
|
114
|
+
line-height: var(--leading-tight);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.card__subtitle {
|
|
118
|
+
font-size: var(--text-sm);
|
|
119
|
+
color: var(--color-muted-foreground);
|
|
120
|
+
font-weight: var(--font-weight-medium);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/* .card__text — no per-element rules. Sibling rhythm comes from .card__body's gap;
|
|
124
|
+
.card__overlay's color: inherit (below) still applies. */
|
|
125
|
+
|
|
126
|
+
.card__link {
|
|
127
|
+
color: var(--color-primary);
|
|
128
|
+
text-decoration: none;
|
|
129
|
+
|
|
130
|
+
& + .card__link {
|
|
131
|
+
margin-inline-start: --spacing(4);
|
|
132
|
+
}
|
|
133
|
+
&:hover {
|
|
134
|
+
text-decoration: underline;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* Image position-aware. .card carries no overflow: hidden (focus rings, sticky headers, and
|
|
139
|
+
overflowing menus must escape), so the image rounds its own corners at whichever edge it
|
|
140
|
+
meets the card. It rounds to the card's INNER radius (outer − border width) so the curve
|
|
141
|
+
sits concentric with the border, matching table-in-card / toggle-group / tabs. */
|
|
142
|
+
.card__image {
|
|
143
|
+
display: block;
|
|
144
|
+
width: 100%;
|
|
145
|
+
height: auto;
|
|
146
|
+
|
|
147
|
+
&:first-child {
|
|
148
|
+
border-radius: calc(
|
|
149
|
+
var(--card-radius, var(--radius-lg)) -
|
|
150
|
+
var(--card-border-width, var(--st-border-width))
|
|
151
|
+
)
|
|
152
|
+
calc(
|
|
153
|
+
var(--card-radius, var(--radius-lg)) -
|
|
154
|
+
var(--card-border-width, var(--st-border-width))
|
|
155
|
+
)
|
|
156
|
+
0 0;
|
|
157
|
+
}
|
|
158
|
+
&:last-child {
|
|
159
|
+
border-radius: 0 0
|
|
160
|
+
calc(
|
|
161
|
+
var(--card-radius, var(--radius-lg)) -
|
|
162
|
+
var(--card-border-width, var(--st-border-width))
|
|
163
|
+
)
|
|
164
|
+
calc(
|
|
165
|
+
var(--card-radius, var(--radius-lg)) -
|
|
166
|
+
var(--card-border-width, var(--st-border-width))
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
&:only-child {
|
|
170
|
+
border-radius: calc(
|
|
171
|
+
var(--card-radius, var(--radius-lg)) -
|
|
172
|
+
var(--card-border-width, var(--st-border-width))
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/* Overlay card: the image is the full-bleed background under .card__overlay, not an
|
|
178
|
+
image-above-body, so it rounds ALL four corners (overriding the top-only :first-child rule
|
|
179
|
+
above) to fill the card's whole rounded frame. */
|
|
180
|
+
.card:has(> .card__overlay) > .card__image {
|
|
181
|
+
border-radius: calc(
|
|
182
|
+
var(--card-radius, var(--radius-lg)) -
|
|
183
|
+
var(--card-border-width, var(--st-border-width))
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/* Overlay sits over a .card__image (or any background sibling). Positions absolute over the
|
|
188
|
+
image; default text color is the theme-independent overlay foreground because overlays
|
|
189
|
+
land on photos. */
|
|
190
|
+
.card__overlay {
|
|
191
|
+
position: absolute;
|
|
192
|
+
inset: 0;
|
|
193
|
+
display: flex;
|
|
194
|
+
flex-direction: column;
|
|
195
|
+
padding: var(--card-padding-block, --spacing(5))
|
|
196
|
+
var(--card-padding-inline, --spacing(5));
|
|
197
|
+
color: var(--color-overlay-foreground);
|
|
198
|
+
|
|
199
|
+
.card__title,
|
|
200
|
+
.card__subtitle,
|
|
201
|
+
.card__text {
|
|
202
|
+
color: inherit;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/* Block — fills its container width per SPEC §9 escape ramp. */
|
|
207
|
+
.card--block {
|
|
208
|
+
width: 100%;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/* A flushed .media row inside a card reads as a continuous row of the card's surface: pick up the
|
|
212
|
+
card gutter so the row's inline edges align with the header/footer, and drop the radius (the card
|
|
213
|
+
already owns rounded corners). */
|
|
214
|
+
.card > .media--flush {
|
|
215
|
+
--media-padding-inline: var(--card-padding-inline, --spacing(5));
|
|
216
|
+
--media-padding-block: var(--card-padding-block, --spacing(5));
|
|
217
|
+
border-radius: 0;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/* @stisla/style — Carousel. Ported from src/scss/components/_carousel.scss. A slide region whose
|
|
2
|
+
* motion is driven by Embla (the JS layer writes transform on .carousel__track); this CSS owns the
|
|
3
|
+
* chrome only — rounded viewport, control chips, indicator dots, caption gradient. Embla reads our
|
|
4
|
+
* own .carousel__* elements (it imposes no class vocabulary), so this is NOT lib-coupled — one file.
|
|
5
|
+
* Overlay chrome reads the theme-independent --color-overlay / --color-overlay-foreground pair so it
|
|
6
|
+
* stays consistent over arbitrary slide imagery in light + dark. References the @theme tokens: colors
|
|
7
|
+
* var(--color-*), sizes/spacing --spacing(n), radius var(--radius-*); only no-namespace customs use
|
|
8
|
+
* --st-* (duration). Knobs are --carousel-*. State via [data-state] / [aria-disabled], never is-*.
|
|
9
|
+
* Slide motion ships with the JS layer. @layer components. Authoring rules: ../../../../PORTING.md */
|
|
10
|
+
|
|
11
|
+
@layer components {
|
|
12
|
+
.carousel {
|
|
13
|
+
position: relative;
|
|
14
|
+
width: 100%;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.carousel:focus-visible {
|
|
18
|
+
outline: 2px solid var(--color-ring);
|
|
19
|
+
outline-offset: 4px;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/* === Viewport + track + slide === */
|
|
23
|
+
.carousel__viewport {
|
|
24
|
+
overflow: hidden;
|
|
25
|
+
border-radius: var(--carousel-radius, var(--radius-lg));
|
|
26
|
+
aspect-ratio: var(--carousel-aspect-ratio, 16 / 9);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/* Embla writes the translate transform; this just hosts the slides. */
|
|
30
|
+
.carousel__track {
|
|
31
|
+
display: flex;
|
|
32
|
+
height: 100%;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.carousel__slide {
|
|
36
|
+
flex: 0 0 100%;
|
|
37
|
+
min-width: 0;
|
|
38
|
+
position: relative;
|
|
39
|
+
padding-inline-start: var(--carousel-slide-gap, 0);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.carousel__slide > img {
|
|
43
|
+
display: block;
|
|
44
|
+
width: 100%;
|
|
45
|
+
height: 100%;
|
|
46
|
+
object-fit: cover;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/* .carousel--no-aspect lets the viewport size to slide content (e.g. text cards) instead of locking
|
|
50
|
+
to 16:9; the slide itself sets the height. */
|
|
51
|
+
.carousel--no-aspect .carousel__viewport {
|
|
52
|
+
aspect-ratio: auto;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/* === Controls (prev/next) === */
|
|
56
|
+
.carousel__control {
|
|
57
|
+
position: absolute;
|
|
58
|
+
inset-block-start: 50%;
|
|
59
|
+
z-index: 2;
|
|
60
|
+
display: inline-flex;
|
|
61
|
+
align-items: center;
|
|
62
|
+
justify-content: center;
|
|
63
|
+
width: var(--carousel-control-size, --spacing(9));
|
|
64
|
+
height: var(--carousel-control-size, --spacing(9));
|
|
65
|
+
padding: 0;
|
|
66
|
+
border: 0;
|
|
67
|
+
border-radius: 50%;
|
|
68
|
+
background: var(--carousel-control-bg, color-mix(in oklch, var(--color-overlay) 70%, transparent));
|
|
69
|
+
color: var(--carousel-control-color, var(--color-overlay-foreground));
|
|
70
|
+
box-shadow: var(--carousel-control-shadow, 0 1px 2px color-mix(in oklch, var(--color-overlay) 30%, transparent));
|
|
71
|
+
cursor: pointer;
|
|
72
|
+
transform: translateY(-50%);
|
|
73
|
+
transition: background-color var(--carousel-transition-duration, var(--transition-duration-normal)) ease;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.carousel__control:hover {
|
|
77
|
+
background: var(--carousel-control-bg-hover, var(--color-overlay));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.carousel__control:focus-visible {
|
|
81
|
+
outline: 2px solid var(--color-ring);
|
|
82
|
+
outline-offset: 2px;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
.carousel__control[aria-disabled="true"],
|
|
86
|
+
.carousel__control:disabled {
|
|
87
|
+
opacity: 0.4;
|
|
88
|
+
cursor: not-allowed;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.carousel__control > svg,
|
|
92
|
+
.carousel__control > i,
|
|
93
|
+
.carousel__control > [data-lucide] {
|
|
94
|
+
width: --spacing(4.5);
|
|
95
|
+
height: --spacing(4.5);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.carousel__control--prev {
|
|
99
|
+
inset-inline-start: var(--carousel-control-inset, --spacing(3));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
.carousel__control--next {
|
|
103
|
+
inset-inline-end: var(--carousel-control-inset, --spacing(3));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* === Indicators === */
|
|
107
|
+
.carousel__indicators {
|
|
108
|
+
position: absolute;
|
|
109
|
+
inset-inline: 0;
|
|
110
|
+
inset-block-end: var(--carousel-indicators-inset, --spacing(3.5));
|
|
111
|
+
z-index: 2;
|
|
112
|
+
display: flex;
|
|
113
|
+
justify-content: center;
|
|
114
|
+
gap: var(--carousel-indicator-gap, --spacing(1.5));
|
|
115
|
+
padding: 0;
|
|
116
|
+
margin: 0;
|
|
117
|
+
list-style: none;
|
|
118
|
+
pointer-events: none;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.carousel__indicator {
|
|
122
|
+
width: var(--carousel-indicator-size, --spacing(1.5));
|
|
123
|
+
height: var(--carousel-indicator-size, --spacing(1.5));
|
|
124
|
+
padding: 0;
|
|
125
|
+
border: 0;
|
|
126
|
+
border-radius: 9999px;
|
|
127
|
+
background: var(--carousel-indicator-bg, color-mix(in oklch, var(--color-overlay-foreground) 50%, transparent));
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
pointer-events: auto;
|
|
130
|
+
transition:
|
|
131
|
+
width var(--carousel-transition-duration, var(--transition-duration-normal)) ease,
|
|
132
|
+
background-color var(--carousel-transition-duration, var(--transition-duration-normal)) ease;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.carousel__indicator:focus-visible {
|
|
136
|
+
outline: 2px solid var(--color-ring);
|
|
137
|
+
outline-offset: 2px;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.carousel__indicator[data-state="active"] {
|
|
141
|
+
width: var(--carousel-indicator-width-active, --spacing(5));
|
|
142
|
+
background: var(--carousel-indicator-bg-active, var(--color-overlay-foreground));
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* === Caption === the slide owns the rounded clip, so the caption can run edge-to-edge. */
|
|
146
|
+
.carousel__caption {
|
|
147
|
+
position: absolute;
|
|
148
|
+
inset-block: auto 0;
|
|
149
|
+
inset-inline: 0;
|
|
150
|
+
padding: var(--carousel-caption-padding-block, --spacing(5))
|
|
151
|
+
var(--carousel-caption-padding-inline, --spacing(5));
|
|
152
|
+
background: var(
|
|
153
|
+
--carousel-caption-bg,
|
|
154
|
+
linear-gradient(to bottom, transparent, color-mix(in oklch, var(--color-overlay) 70%, transparent))
|
|
155
|
+
);
|
|
156
|
+
color: var(--carousel-caption-color, var(--color-overlay-foreground));
|
|
157
|
+
pointer-events: none;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.carousel__caption > * {
|
|
161
|
+
pointer-events: auto;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
@media (prefers-reduced-motion: reduce) {
|
|
165
|
+
.carousel__control,
|
|
166
|
+
.carousel__indicator {
|
|
167
|
+
transition: none;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/* @stisla/style — Checkbox. Ported from src/scss/components/_checkbox.scss + the form-check-base
|
|
2
|
+
* mixin. Native <input type="checkbox"> styled as a small square box. References the @theme tokens
|
|
3
|
+
* (colors var(--color-*), spacing --spacing(n)); only no-namespace customs use --st-* (border-width).
|
|
4
|
+
* Knobs are --checkbox-* (fallback-default). @layer components.
|
|
5
|
+
*
|
|
6
|
+
* The shared check base (size, border, focus halo, validation, disabled) is DUPLICATED here with the
|
|
7
|
+
* --checkbox-* namespace — plain CSS has no parameterized mixin. Keep in sync with radio.css.
|
|
8
|
+
* NOTE: the box radius default is --spacing(1) (4px) — the radius scale is sm/md/lg only (no xs tier),
|
|
9
|
+
* and the box is intentionally tighter than --radius-sm. Override via --checkbox-radius. State is native
|
|
10
|
+
* (:checked / :indeterminate / :disabled / :focus-visible / [aria-invalid] / :user-invalid), no is-*.
|
|
11
|
+
* Authoring rules: ../../../../PORTING.md */
|
|
12
|
+
|
|
13
|
+
@layer components {
|
|
14
|
+
.checkbox {
|
|
15
|
+
flex-shrink: 0;
|
|
16
|
+
width: var(--checkbox-size, --spacing(4));
|
|
17
|
+
height: var(--checkbox-size, --spacing(4));
|
|
18
|
+
margin: 0;
|
|
19
|
+
vertical-align: middle;
|
|
20
|
+
background-color: var(--checkbox-bg, var(--color-surface));
|
|
21
|
+
background-repeat: no-repeat;
|
|
22
|
+
background-position: center;
|
|
23
|
+
background-size: 100% 100%;
|
|
24
|
+
border: var(--checkbox-border-width, var(--st-border-width)) solid
|
|
25
|
+
var(--checkbox-border-color, var(--color-border-strong));
|
|
26
|
+
border-radius: var(--checkbox-radius, --spacing(1));
|
|
27
|
+
appearance: none;
|
|
28
|
+
cursor: pointer;
|
|
29
|
+
transition:
|
|
30
|
+
background-color var(--transition-duration-fast) ease,
|
|
31
|
+
background-position 0.15s ease,
|
|
32
|
+
border-color var(--transition-duration-fast) ease,
|
|
33
|
+
box-shadow var(--transition-duration-fast) ease;
|
|
34
|
+
print-color-adjust: exact;
|
|
35
|
+
|
|
36
|
+
&:focus-visible {
|
|
37
|
+
outline: none;
|
|
38
|
+
border-color: var(--color-primary);
|
|
39
|
+
box-shadow: 0 0 0 3px
|
|
40
|
+
color-mix(in oklch, var(--color-ring) 25%, transparent);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/* Hover — only when idle. Skip focus / invalid / checked / indeterminate / disabled (each owns
|
|
44
|
+
its own border color, and hovering a checked box shouldn't dim its primary border to gray). */
|
|
45
|
+
&:hover:not(
|
|
46
|
+
:disabled,
|
|
47
|
+
:focus-visible,
|
|
48
|
+
:checked,
|
|
49
|
+
:indeterminate,
|
|
50
|
+
[aria-invalid="true"],
|
|
51
|
+
:user-invalid
|
|
52
|
+
) {
|
|
53
|
+
border-color: color-mix(
|
|
54
|
+
in oklch,
|
|
55
|
+
var(--color-border-strong) 80%,
|
|
56
|
+
var(--color-foreground)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/* Validation — touches the border only; the fill (transparent unchecked, primary checked) stays
|
|
61
|
+
independent so the "selected" signal stays readable. */
|
|
62
|
+
&[aria-invalid="true"],
|
|
63
|
+
&:user-invalid {
|
|
64
|
+
border-color: var(--color-danger);
|
|
65
|
+
}
|
|
66
|
+
&[aria-invalid="true"]:focus-visible,
|
|
67
|
+
&:user-invalid:focus-visible {
|
|
68
|
+
border-color: var(--color-danger);
|
|
69
|
+
box-shadow: 0 0 0 3px
|
|
70
|
+
color-mix(in oklch, var(--color-danger) 25%, transparent);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
&:disabled {
|
|
74
|
+
opacity: 0.55;
|
|
75
|
+
cursor: not-allowed;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* Checked / indeterminate — solid primary fill, indicator painted via background-image. The SVG
|
|
79
|
+
glyph is literal white because data: URLs can't read CSS vars; white = --color-primary-foreground
|
|
80
|
+
in both default themes. Override --checkbox-indicator with a custom SVG to recolor. */
|
|
81
|
+
&:checked,
|
|
82
|
+
&:indeterminate {
|
|
83
|
+
background-color: var(--checkbox-bg-checked, var(--color-primary));
|
|
84
|
+
border-color: var(--checkbox-bg-checked, var(--color-primary));
|
|
85
|
+
background-image: var(--checkbox-indicator);
|
|
86
|
+
}
|
|
87
|
+
&:checked {
|
|
88
|
+
--checkbox-indicator: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpolyline points='3 8.5 6.5 12 13 5' stroke='white' stroke-width='2.25' fill='none' stroke-linecap='round' stroke-linejoin='round'/%3E%3C/svg%3E");
|
|
89
|
+
}
|
|
90
|
+
&:indeterminate {
|
|
91
|
+
--checkbox-indicator: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cline x1='3' y1='8' x2='13' y2='8' stroke='white' stroke-width='2.5' stroke-linecap='round'/%3E%3C/svg%3E");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@media (prefers-reduced-motion: reduce) {
|
|
95
|
+
transition: none;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// src/composer.ts
|
|
2
|
+
var camelToKebab = (s) => s.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
|
|
3
|
+
var resolveTuneValue = (v) => v.startsWith("--") ? `var(${v})` : v;
|
|
4
|
+
function composer(config) {
|
|
5
|
+
const { base, variants = {}, defaultVariants = {}, knobPrefix } = config;
|
|
6
|
+
return function compose(props = {}) {
|
|
7
|
+
const classes = [base];
|
|
8
|
+
for (const name in variants) {
|
|
9
|
+
const raw = props[name] ?? defaultVariants[name];
|
|
10
|
+
if (raw == null || raw === false) continue;
|
|
11
|
+
const key = raw === true ? "true" : String(raw);
|
|
12
|
+
const cls = variants[name]?.[key];
|
|
13
|
+
if (cls) classes.push(cls);
|
|
14
|
+
}
|
|
15
|
+
const style = {};
|
|
16
|
+
if (props.tune && knobPrefix) {
|
|
17
|
+
for (const k in props.tune) {
|
|
18
|
+
const val = props.tune[k];
|
|
19
|
+
if (val == null) continue;
|
|
20
|
+
style[`--${knobPrefix}-${camelToKebab(k)}`] = resolveTuneValue(val);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
if (props.className) classes.push(props.className);
|
|
24
|
+
return { className: classes.join(" "), style };
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/button/config.ts
|
|
29
|
+
var button = composer({
|
|
30
|
+
base: "button",
|
|
31
|
+
variants: {
|
|
32
|
+
tone: {
|
|
33
|
+
primary: "button--primary",
|
|
34
|
+
danger: "button--danger",
|
|
35
|
+
neutral: "button--neutral",
|
|
36
|
+
tertiary: "button--tertiary"
|
|
37
|
+
},
|
|
38
|
+
shape: {
|
|
39
|
+
outline: "button--outline",
|
|
40
|
+
ghost: "button--ghost",
|
|
41
|
+
soft: "button--soft"
|
|
42
|
+
},
|
|
43
|
+
size: { sm: "button--sm", md: "", lg: "button--lg", xl: "button--xl" },
|
|
44
|
+
iconOnly: { true: "button--icon-only" },
|
|
45
|
+
iconRound: { true: "button--icon-round" },
|
|
46
|
+
block: { true: "button--block" },
|
|
47
|
+
wrap: { true: "button--wrap" }
|
|
48
|
+
},
|
|
49
|
+
defaultVariants: { size: "md" },
|
|
50
|
+
knobPrefix: "button",
|
|
51
|
+
knobs: [
|
|
52
|
+
"gap",
|
|
53
|
+
"height",
|
|
54
|
+
"paddingInline",
|
|
55
|
+
"fontSize",
|
|
56
|
+
"fontWeight",
|
|
57
|
+
"color",
|
|
58
|
+
"bg",
|
|
59
|
+
"tone",
|
|
60
|
+
"bgHover",
|
|
61
|
+
"bgActive",
|
|
62
|
+
"borderWidth",
|
|
63
|
+
"borderColor",
|
|
64
|
+
"rimMix",
|
|
65
|
+
"radius",
|
|
66
|
+
"bevel",
|
|
67
|
+
"iconSize"
|
|
68
|
+
]
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
export {
|
|
72
|
+
composer,
|
|
73
|
+
button
|
|
74
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/* @stisla/style — Collapsible. Ported from src/scss/components/_collapsible.scss. The CSS half of the
|
|
2
|
+
* height-animation primitive: a closed region drops out of flow, and while the JS behavior layer is
|
|
3
|
+
* mid-transition it keeps the region displayed so the height tween is visible. State lives in
|
|
4
|
+
* [data-state="open|closed"]; the transient mid-animation flag is [data-collapsing] (recast from
|
|
5
|
+
* the legacy transient class to an attribute per the no-is-* convention). The [data-collapsing]
|
|
6
|
+
* transition rule is GLOBAL (not scoped to .collapsible) so the JS layer can set it on ANY animated
|
|
7
|
+
* region — a standalone .collapsible, but also a borrowed body like .accordion__body / a sidebar
|
|
8
|
+
* submenu. Composers own their own closed-hide rule; the .collapsible closed rule below is for
|
|
9
|
+
* standalone use. Only no-namespace customs use --st-* (duration). Knob: --collapsible-transition-duration.
|
|
10
|
+
* @layer components. Authoring rules: ../../../../PORTING.md */
|
|
11
|
+
|
|
12
|
+
@layer components {
|
|
13
|
+
/* Closed and not mid-animation → fully removed from flow. The :not([data-collapsing]) guard keeps
|
|
14
|
+
the region visible while the behavior layer runs the close transition; display:none only takes
|
|
15
|
+
over once the flag clears. */
|
|
16
|
+
.collapsible[data-state="closed"]:not([data-collapsing]) {
|
|
17
|
+
display: none;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/* The behavior layer adds [data-collapsing] for the duration of an open/close, sets an explicit
|
|
21
|
+
height before and after a reflow, and the transition spec lives here (not inline) so a same-task
|
|
22
|
+
property + transition change can't fall through to the previous (none) computation. GLOBAL on
|
|
23
|
+
purpose — applies to whatever element the JS animates (composers borrow it for their own body). */
|
|
24
|
+
[data-collapsing] {
|
|
25
|
+
transition-property: height;
|
|
26
|
+
transition-duration: var(--collapsible-transition-duration, var(--transition-duration-normal));
|
|
27
|
+
transition-timing-function: ease;
|
|
28
|
+
overflow: hidden;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
@media (prefers-reduced-motion: reduce) {
|
|
32
|
+
[data-collapsing] {
|
|
33
|
+
transition-duration: 0s;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|