@stealthscale/component-feedback 0.1.0 → 0.2.2
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/README.md +514 -37
- package/dist/cyclonedx/bom.json +1 -1
- package/dist/index.d.ts +716 -60
- package/dist/index.js +1741 -92
- package/dist/recipe-CX1gSsN7.js +1290 -0
- package/dist/theme.d.ts +3 -4
- package/dist/theme.js +37 -16
- package/package.json +36 -23
- package/dist/recipe-0Xo90j3-.js +0 -162
|
@@ -0,0 +1,1290 @@
|
|
|
1
|
+
import { PALETTES, SERIES, axis, below, cornerVariants, defineRecipe, defineSlotRecipe, dense, flatVariants, iconSizes, interactive, motionVariants, onSlot, onSlots, paletteVariants, sizeVariants, statusEmitted, statusVariants, touchTarget } from "@stealthscale/theme/authoring";
|
|
2
|
+
//#region src/alert/recipe.ts
|
|
3
|
+
/**
|
|
4
|
+
* Declares the alert slot recipe for a notice with an icon, a title, a description, trailing
|
|
5
|
+
* controls and a close trigger.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* `status` sets `colorPalette` and nothing else, so it combines freely with `variant` and `size`
|
|
9
|
+
* and a theme retints every alert through the palettes. The title states the severity in words,
|
|
10
|
+
* because WCAG 1.4.1 rejects a status told by color alone. `variant` reads the `flat.*` layer
|
|
11
|
+
* styles, whose fill and ink are the pairs the contrast gate measures. The indicator and the
|
|
12
|
+
* close trigger inherit the root's ink, so a solid alert draws both in the contrast ink. The
|
|
13
|
+
* recipe has no `effect` axis, because `motion` already animates the root.
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* Sizes of the icon box and the root's gap, inset and text style.
|
|
17
|
+
*/
|
|
18
|
+
const SIZES$3 = [
|
|
19
|
+
"sm",
|
|
20
|
+
"md",
|
|
21
|
+
"lg"
|
|
22
|
+
];
|
|
23
|
+
/**
|
|
24
|
+
* Maps each edge to the border that draws it, in the palette's solid at the indicator width.
|
|
25
|
+
*/
|
|
26
|
+
const EDGES = {
|
|
27
|
+
bottom: {
|
|
28
|
+
borderBlockEndColor: "colorPalette.solid",
|
|
29
|
+
borderBlockEndWidth: "indicator"
|
|
30
|
+
},
|
|
31
|
+
end: {
|
|
32
|
+
borderInlineEndColor: "colorPalette.solid",
|
|
33
|
+
borderInlineEndWidth: "indicator"
|
|
34
|
+
},
|
|
35
|
+
top: {
|
|
36
|
+
borderBlockStartColor: "colorPalette.solid",
|
|
37
|
+
borderBlockStartWidth: "indicator"
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Looks whose layer style sets the `border-width` and `border-color` shorthands.
|
|
42
|
+
*/
|
|
43
|
+
const BORDERED = ["outline", "surface"];
|
|
44
|
+
/**
|
|
45
|
+
* Alert slot recipe, a subtle info notice at the md size by default.
|
|
46
|
+
*
|
|
47
|
+
* @remarks
|
|
48
|
+
* The indicator, the content and the trailing controls are centred on the notice's full height,
|
|
49
|
+
* so the icon aligns with the middle of a text block of any length.
|
|
50
|
+
*/
|
|
51
|
+
const recipe$8 = defineSlotRecipe({
|
|
52
|
+
base: {
|
|
53
|
+
aside: {
|
|
54
|
+
alignItems: "center",
|
|
55
|
+
display: "flex",
|
|
56
|
+
flex: "0 0 auto"
|
|
57
|
+
},
|
|
58
|
+
/**
|
|
59
|
+
* The close trigger is a square of 1.5em, at least 24px, with a 1em glyph. A negative end
|
|
60
|
+
* margin of half the box less the glyph puts the glyph on the padding edge, level with the
|
|
61
|
+
* indicator at the start: 12, 16 and 20px from the edge at `sm`, `md` and `lg`. Its focus ring
|
|
62
|
+
* is drawn inside, because a ring outside it falls on the alert's fill.
|
|
63
|
+
*/
|
|
64
|
+
closeTrigger: {
|
|
65
|
+
...interactive(),
|
|
66
|
+
...touchTarget(),
|
|
67
|
+
focusVisibleRing: "inside",
|
|
68
|
+
_hover: { background: "colorPalette.emphasized" },
|
|
69
|
+
"& > svg": { boxSize: "1em" },
|
|
70
|
+
alignItems: "center",
|
|
71
|
+
appearance: "none",
|
|
72
|
+
background: "transparent",
|
|
73
|
+
borderRadius: "l1",
|
|
74
|
+
borderStyle: "none",
|
|
75
|
+
boxSize: "max({sizes.6}, 1.5em)",
|
|
76
|
+
color: "currentcolor",
|
|
77
|
+
display: "inline-flex",
|
|
78
|
+
flex: "0 0 auto",
|
|
79
|
+
justifyContent: "center",
|
|
80
|
+
marginInlineEnd: "calc((1em - max({sizes.6}, 1.5em)) / 2)",
|
|
81
|
+
padding: "0"
|
|
82
|
+
},
|
|
83
|
+
content: {
|
|
84
|
+
display: "flex",
|
|
85
|
+
flex: "1",
|
|
86
|
+
minInlineSize: "0"
|
|
87
|
+
},
|
|
88
|
+
description: { color: "inherit" },
|
|
89
|
+
indicator: {
|
|
90
|
+
"& > svg": { boxSize: "100%" },
|
|
91
|
+
alignItems: "center",
|
|
92
|
+
display: "inline-flex",
|
|
93
|
+
flex: "0 0 auto"
|
|
94
|
+
},
|
|
95
|
+
/**
|
|
96
|
+
* The root draws a hairline outline in forced colors, where the browser replaces the fill and
|
|
97
|
+
* the solid, subtle and plain looks have no border to mark the box.
|
|
98
|
+
*/
|
|
99
|
+
root: {
|
|
100
|
+
_highContrast: {
|
|
101
|
+
outlineColor: "CanvasText",
|
|
102
|
+
outlineOffset: "calc({borderWidths.hairline} * -1)",
|
|
103
|
+
outlineStyle: "solid",
|
|
104
|
+
outlineWidth: "hairline"
|
|
105
|
+
},
|
|
106
|
+
alignItems: "center",
|
|
107
|
+
display: "flex",
|
|
108
|
+
inlineSize: "full"
|
|
109
|
+
},
|
|
110
|
+
title: { fontWeight: "medium" }
|
|
111
|
+
},
|
|
112
|
+
className: "alert",
|
|
113
|
+
compoundVariants: [
|
|
114
|
+
(
|
|
115
|
+
/**
|
|
116
|
+
* The close trigger on a solid alert hovers to a tint of the contrast ink and draws its focus
|
|
117
|
+
* ring in the contrast ink. The `emphasized` role is lighter than the solid fill, and the
|
|
118
|
+
* `focusRing` role is as dark as it.
|
|
119
|
+
*/
|
|
120
|
+
{
|
|
121
|
+
css: { closeTrigger: {
|
|
122
|
+
_hover: { background: "colorPalette.contrast/20" },
|
|
123
|
+
focusRingColor: "colorPalette.contrast"
|
|
124
|
+
} },
|
|
125
|
+
name: "contrasted",
|
|
126
|
+
variant: "solid"
|
|
127
|
+
}),
|
|
128
|
+
(
|
|
129
|
+
/**
|
|
130
|
+
* The outline and surface looks restate the edge. Their layer style sets the border shorthands
|
|
131
|
+
* in the `variant` axis, which the compiler emits after `edge` and which reset the edge's
|
|
132
|
+
* width and color.
|
|
133
|
+
*/
|
|
134
|
+
{
|
|
135
|
+
css: { root: EDGES.top },
|
|
136
|
+
edge: "top",
|
|
137
|
+
name: "edged-top",
|
|
138
|
+
variant: BORDERED
|
|
139
|
+
}),
|
|
140
|
+
{
|
|
141
|
+
css: { root: EDGES.bottom },
|
|
142
|
+
edge: "bottom",
|
|
143
|
+
name: "edged-bottom",
|
|
144
|
+
variant: BORDERED
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
css: { root: EDGES.end },
|
|
148
|
+
edge: "end",
|
|
149
|
+
name: "edged-end",
|
|
150
|
+
variant: BORDERED
|
|
151
|
+
}
|
|
152
|
+
],
|
|
153
|
+
defaultVariants: {
|
|
154
|
+
layout: "stacked",
|
|
155
|
+
radius: "l3",
|
|
156
|
+
size: "md",
|
|
157
|
+
status: "info",
|
|
158
|
+
variant: "subtle"
|
|
159
|
+
},
|
|
160
|
+
jsx: [/^Alert(\.\w+)?$/u],
|
|
161
|
+
slots: [
|
|
162
|
+
"root",
|
|
163
|
+
"indicator",
|
|
164
|
+
"content",
|
|
165
|
+
"title",
|
|
166
|
+
"description",
|
|
167
|
+
"aside",
|
|
168
|
+
"closeTrigger"
|
|
169
|
+
],
|
|
170
|
+
staticCss: [statusEmitted(), { status: ["neutral"] }],
|
|
171
|
+
variants: {
|
|
172
|
+
/**
|
|
173
|
+
* The edge with a rule in the palette's solid color. Unset by default.
|
|
174
|
+
*
|
|
175
|
+
* @remarks
|
|
176
|
+
* The rule is a border on the root, so it follows the root's rounded corners, tapers into
|
|
177
|
+
* the side border and shows under forced colors. The theme's `indicator.*` layer styles are
|
|
178
|
+
* not used, because their bar is a 2px pseudo-element whose corner radius the browser scales
|
|
179
|
+
* down to fit. `end` is logical: the right edge in a left-to-right document and the left edge
|
|
180
|
+
* in a right-to-left one.
|
|
181
|
+
*/
|
|
182
|
+
edge: {
|
|
183
|
+
top: { root: EDGES.top },
|
|
184
|
+
bottom: { root: EDGES.bottom },
|
|
185
|
+
end: { root: EDGES.end }
|
|
186
|
+
},
|
|
187
|
+
/**
|
|
188
|
+
* Whether the title is above the description or on the same line with it.
|
|
189
|
+
*/
|
|
190
|
+
layout: {
|
|
191
|
+
inline: { content: {
|
|
192
|
+
alignItems: "baseline",
|
|
193
|
+
columnGap: dense("{spacing.gap.xs}"),
|
|
194
|
+
flexWrap: "wrap"
|
|
195
|
+
} },
|
|
196
|
+
stacked: { content: {
|
|
197
|
+
flexDirection: "column",
|
|
198
|
+
rowGap: dense("{spacing.gap.xs}")
|
|
199
|
+
} }
|
|
200
|
+
},
|
|
201
|
+
motion: onSlot("root", motionVariants([
|
|
202
|
+
"fade",
|
|
203
|
+
"rise",
|
|
204
|
+
"reveal"
|
|
205
|
+
])),
|
|
206
|
+
radius: onSlot("root", cornerVariants()),
|
|
207
|
+
size: onSlots({
|
|
208
|
+
indicator: iconSizes(SIZES$3),
|
|
209
|
+
root: sizeVariants((size) => ({
|
|
210
|
+
textStyle: `body.${size}`,
|
|
211
|
+
gap: dense(`{spacing.gap.${size}}`),
|
|
212
|
+
padding: dense(`{spacing.inset.${size}}`)
|
|
213
|
+
}), SIZES$3)
|
|
214
|
+
}),
|
|
215
|
+
/**
|
|
216
|
+
* The severity of the alert. It sets `colorPalette` and nothing else.
|
|
217
|
+
*/
|
|
218
|
+
status: {
|
|
219
|
+
...onSlot("root", statusVariants()),
|
|
220
|
+
neutral: { root: { colorPalette: "neutral" } }
|
|
221
|
+
},
|
|
222
|
+
/**
|
|
223
|
+
* The fill and edge of the alert, from the `flat.*` layer styles.
|
|
224
|
+
*/
|
|
225
|
+
variant: onSlot("root", flatVariants())
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/empty-state/recipe.ts
|
|
230
|
+
/**
|
|
231
|
+
* Declares the empty state slot recipe for the panel a surface renders when it has no content.
|
|
232
|
+
*
|
|
233
|
+
* @remarks
|
|
234
|
+
* `size` sets the panel's inset, the column's gap, the mark's box and the title's text style
|
|
235
|
+
* together. The mark is 32, 40 and 50px at `sm`, `md` and `lg`, above a 16, 18 and 20px title, so
|
|
236
|
+
* the mark is always larger than the title's line. The mark has twice the column's gap below it,
|
|
237
|
+
* so the title and the description read as one group. The description stays at `body.sm` at every
|
|
238
|
+
* size. The recipe has no `palette` axis, because the panel is muted, and no `effect` axis,
|
|
239
|
+
* because it has no fill or border.
|
|
240
|
+
*/
|
|
241
|
+
/**
|
|
242
|
+
* Sizes the recipe offers.
|
|
243
|
+
*/
|
|
244
|
+
const SIZES$2 = [
|
|
245
|
+
"sm",
|
|
246
|
+
"md",
|
|
247
|
+
"lg"
|
|
248
|
+
];
|
|
249
|
+
/**
|
|
250
|
+
* Maps each size to the gap step one size larger: 8, 12 and 16px.
|
|
251
|
+
*/
|
|
252
|
+
const GAPS = {
|
|
253
|
+
lg: "xl",
|
|
254
|
+
md: "lg",
|
|
255
|
+
sm: "md"
|
|
256
|
+
};
|
|
257
|
+
/**
|
|
258
|
+
* Maps each size to the icon and inset step of the mark and the panel: 32, 40 and 50px marks in
|
|
259
|
+
* 24, 32 and 40px of inset.
|
|
260
|
+
*/
|
|
261
|
+
const MARKS = {
|
|
262
|
+
lg: "3xl",
|
|
263
|
+
md: "2xl",
|
|
264
|
+
sm: "xl"
|
|
265
|
+
};
|
|
266
|
+
/**
|
|
267
|
+
* Empty state slot recipe, at the md size by default.
|
|
268
|
+
*/
|
|
269
|
+
const recipe$7 = defineSlotRecipe({
|
|
270
|
+
base: {
|
|
271
|
+
content: {
|
|
272
|
+
alignItems: "center",
|
|
273
|
+
display: "flex",
|
|
274
|
+
flexDirection: "column",
|
|
275
|
+
justifyContent: "center",
|
|
276
|
+
textAlign: "center"
|
|
277
|
+
},
|
|
278
|
+
description: {
|
|
279
|
+
textStyle: "body.sm",
|
|
280
|
+
color: "fg.muted"
|
|
281
|
+
},
|
|
282
|
+
indicator: {
|
|
283
|
+
"& svg": { boxSize: "100%" },
|
|
284
|
+
alignItems: "center",
|
|
285
|
+
color: "fg.muted",
|
|
286
|
+
display: "flex",
|
|
287
|
+
justifyContent: "center"
|
|
288
|
+
},
|
|
289
|
+
root: {
|
|
290
|
+
alignItems: "center",
|
|
291
|
+
display: "flex",
|
|
292
|
+
justifyContent: "center",
|
|
293
|
+
width: "full"
|
|
294
|
+
},
|
|
295
|
+
title: { fontWeight: "semibold" }
|
|
296
|
+
},
|
|
297
|
+
className: "empty-state",
|
|
298
|
+
defaultVariants: { size: "md" },
|
|
299
|
+
jsx: [/^EmptyState(\.\w+)?$/u],
|
|
300
|
+
slots: [
|
|
301
|
+
"root",
|
|
302
|
+
"content",
|
|
303
|
+
"indicator",
|
|
304
|
+
"title",
|
|
305
|
+
"description"
|
|
306
|
+
],
|
|
307
|
+
variants: {
|
|
308
|
+
/**
|
|
309
|
+
* The inset of the panel, the gap in the column, the box of the mark and the title's text
|
|
310
|
+
* style.
|
|
311
|
+
*/
|
|
312
|
+
size: onSlots({
|
|
313
|
+
content: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${GAPS[size]}}`) }), SIZES$2),
|
|
314
|
+
indicator: sizeVariants((size) => ({
|
|
315
|
+
boxSize: dense(`{sizes.icon.${MARKS[size]}}`),
|
|
316
|
+
marginBlockEnd: dense(`{spacing.gap.${GAPS[size]}}`)
|
|
317
|
+
}), SIZES$2),
|
|
318
|
+
root: sizeVariants((size) => ({ padding: dense(`{spacing.inset.${MARKS[size]}}`) }), SIZES$2),
|
|
319
|
+
title: sizeVariants((size) => ({ textStyle: `heading.${below(size)}` }), SIZES$2)
|
|
320
|
+
}) }
|
|
321
|
+
});
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region src/loader/recipe.ts
|
|
324
|
+
/**
|
|
325
|
+
* Declares the loader's slot recipe: a spinner beside its words, a spinner over content it hides,
|
|
326
|
+
* and the overlay that covers a positioned container while it loads.
|
|
327
|
+
*
|
|
328
|
+
* @remarks
|
|
329
|
+
* With words, the root is an inline flex row, so the spinner and the words render on one line in
|
|
330
|
+
* any container. Over content, the root is an inline grid, and the hidden content and the
|
|
331
|
+
* spinner share its single cell. The spinner is centred on the content and the box has the
|
|
332
|
+
* content's size. The hidden content leaves the accessibility tree, so a visually hidden label
|
|
333
|
+
* replaces it. The loader has no box of its own and offers no `effect` axis. A caller who needs
|
|
334
|
+
* one passes a spinner with its own `effect`.
|
|
335
|
+
*/
|
|
336
|
+
/**
|
|
337
|
+
* The recipe's class name, used to build the selector from the root to the content.
|
|
338
|
+
*/
|
|
339
|
+
const CLASS = "loader";
|
|
340
|
+
/**
|
|
341
|
+
* Writes the `palette` axis of the indicator: the palette's `solid` as the spinner's ink.
|
|
342
|
+
*
|
|
343
|
+
* @remarks
|
|
344
|
+
* The spinner inside is at `current`, so it draws in the indicator's ink. Without a palette the
|
|
345
|
+
* indicator inherits the surrounding ink, which keeps the spinner readable on a solid button.
|
|
346
|
+
*/
|
|
347
|
+
const tinted = axis(PALETTES, (palette) => ({
|
|
348
|
+
color: "colorPalette.solid",
|
|
349
|
+
colorPalette: palette
|
|
350
|
+
}));
|
|
351
|
+
/**
|
|
352
|
+
* Styles the loader and its overlay, with a veiled overlay by default.
|
|
353
|
+
*/
|
|
354
|
+
const recipe$6 = defineSlotRecipe({
|
|
355
|
+
base: {
|
|
356
|
+
content: {
|
|
357
|
+
gridArea: "1 / 1",
|
|
358
|
+
visibility: "hidden"
|
|
359
|
+
},
|
|
360
|
+
indicator: {
|
|
361
|
+
alignItems: "center",
|
|
362
|
+
display: "inline-flex",
|
|
363
|
+
gridArea: "1 / 1"
|
|
364
|
+
},
|
|
365
|
+
label: { srOnly: true },
|
|
366
|
+
overlay: {
|
|
367
|
+
alignItems: "center",
|
|
368
|
+
borderRadius: "inherit",
|
|
369
|
+
display: "flex",
|
|
370
|
+
gap: "gap.sm",
|
|
371
|
+
inset: "0",
|
|
372
|
+
justifyContent: "center",
|
|
373
|
+
position: "absolute"
|
|
374
|
+
},
|
|
375
|
+
root: {
|
|
376
|
+
[`&:has(> .${CLASS}__content)`]: {
|
|
377
|
+
display: "inline-grid",
|
|
378
|
+
placeItems: "center"
|
|
379
|
+
},
|
|
380
|
+
alignItems: "center",
|
|
381
|
+
display: "inline-flex",
|
|
382
|
+
gap: "0.5em",
|
|
383
|
+
verticalAlign: "middle"
|
|
384
|
+
}
|
|
385
|
+
},
|
|
386
|
+
className: CLASS,
|
|
387
|
+
defaultVariants: { scrim: "veil" },
|
|
388
|
+
jsx: [/^Loader(Overlay)?$/u],
|
|
389
|
+
slots: [
|
|
390
|
+
"root",
|
|
391
|
+
"indicator",
|
|
392
|
+
"content",
|
|
393
|
+
"label",
|
|
394
|
+
"overlay"
|
|
395
|
+
],
|
|
396
|
+
variants: {
|
|
397
|
+
/**
|
|
398
|
+
* The spinner's ink, as a palette's `solid` role.
|
|
399
|
+
*/
|
|
400
|
+
palette: onSlot("indicator", tinted()),
|
|
401
|
+
/**
|
|
402
|
+
* The fill of the overlay over the content it covers.
|
|
403
|
+
*
|
|
404
|
+
* @remarks
|
|
405
|
+
* `veil` fills the overlay with the panel color at 80% opacity. The covered content shows
|
|
406
|
+
* through it faintly, and the spinner and its words have contrast against it. `glass` blurs
|
|
407
|
+
* the content behind a 70% panel fill. `none` sets no fill.
|
|
408
|
+
*/
|
|
409
|
+
scrim: onSlot("overlay", {
|
|
410
|
+
glass: { layerStyle: "glass" },
|
|
411
|
+
none: { background: "transparent" },
|
|
412
|
+
veil: { background: "bg.panel/80" }
|
|
413
|
+
})
|
|
414
|
+
}
|
|
415
|
+
});
|
|
416
|
+
//#endregion
|
|
417
|
+
//#region src/spinner/recipe.ts
|
|
418
|
+
/**
|
|
419
|
+
* Declares the spinner recipe, a ring whose arc turns while work of unknown length runs.
|
|
420
|
+
*
|
|
421
|
+
* @remarks
|
|
422
|
+
* The block start and inline end borders draw the arc and the other two draw the track, so the
|
|
423
|
+
* stroke is a border width and keeps its weight at every size. The turn is the foundation's
|
|
424
|
+
* `spin` animation style, which a theme retimes and which stops under reduced motion. Each border
|
|
425
|
+
* side is a longhand, so the compiled rule contains no shorthand that overrides a side. In forced
|
|
426
|
+
* colors the browser paints all four sides in `CanvasText`, transparent ones included, so
|
|
427
|
+
* `_highContrast` sets `forcedColorAdjust: none` and draws the arc in `CanvasText` itself.
|
|
428
|
+
*/
|
|
429
|
+
/**
|
|
430
|
+
* Styles a spinner, defaulting to the medium icon size and the indicator stroke in the current
|
|
431
|
+
* ink.
|
|
432
|
+
*/
|
|
433
|
+
const recipe$5 = defineRecipe({
|
|
434
|
+
base: {
|
|
435
|
+
animationStyle: "spin",
|
|
436
|
+
_highContrast: {
|
|
437
|
+
borderBlockEndColor: "transparent",
|
|
438
|
+
borderBlockStartColor: "CanvasText",
|
|
439
|
+
borderInlineEndColor: "CanvasText",
|
|
440
|
+
borderInlineStartColor: "transparent",
|
|
441
|
+
forcedColorAdjust: "none"
|
|
442
|
+
},
|
|
443
|
+
borderBlockEndColor: "transparent",
|
|
444
|
+
borderBlockStartColor: "currentcolor",
|
|
445
|
+
borderInlineEndColor: "currentcolor",
|
|
446
|
+
borderInlineStartColor: "transparent",
|
|
447
|
+
borderRadius: "full",
|
|
448
|
+
borderStyle: "solid",
|
|
449
|
+
color: "colorPalette.solid",
|
|
450
|
+
display: "inline-block",
|
|
451
|
+
flexShrink: "0",
|
|
452
|
+
position: "relative",
|
|
453
|
+
verticalAlign: "middle"
|
|
454
|
+
},
|
|
455
|
+
className: "spinner",
|
|
456
|
+
defaultVariants: {
|
|
457
|
+
palette: "current",
|
|
458
|
+
size: "md",
|
|
459
|
+
stroke: "indicator"
|
|
460
|
+
},
|
|
461
|
+
jsx: [/Spinner$/u],
|
|
462
|
+
variants: {
|
|
463
|
+
/**
|
|
464
|
+
* The halo drawn around the ring, in the palette's solid at half opacity.
|
|
465
|
+
*
|
|
466
|
+
* @remarks
|
|
467
|
+
* `pulse` runs on `::after`, because its keyframe animates `box-shadow` through the
|
|
468
|
+
* `animation` property, which the element already uses for the spin. Both effects stop
|
|
469
|
+
* animating when the reader prefers reduced motion.
|
|
470
|
+
*/
|
|
471
|
+
effect: {
|
|
472
|
+
glow: { layerStyle: "glow.md" },
|
|
473
|
+
pulse: { _after: {
|
|
474
|
+
animationStyle: "pulse-glow",
|
|
475
|
+
borderRadius: "inherit",
|
|
476
|
+
boxShadowColor: "colorPalette.solid/50",
|
|
477
|
+
content: "\"\"",
|
|
478
|
+
inset: "0",
|
|
479
|
+
position: "absolute"
|
|
480
|
+
} }
|
|
481
|
+
},
|
|
482
|
+
/**
|
|
483
|
+
* The palette the arc is drawn in, as its `solid` role.
|
|
484
|
+
*
|
|
485
|
+
* @remarks
|
|
486
|
+
* `current` draws the arc in the surrounding ink instead, so a spinner inside a solid button
|
|
487
|
+
* takes the button's label ink.
|
|
488
|
+
*/
|
|
489
|
+
palette: {
|
|
490
|
+
...paletteVariants(),
|
|
491
|
+
current: { color: "currentcolor" }
|
|
492
|
+
},
|
|
493
|
+
size: {
|
|
494
|
+
...iconSizes(),
|
|
495
|
+
inherit: { boxSize: "1em" }
|
|
496
|
+
},
|
|
497
|
+
/**
|
|
498
|
+
* Border width of the ring. `hairline`, `control` and `indicator` read the theme's stroke
|
|
499
|
+
* tokens, and `heavy` reads the 4px `lg` width.
|
|
500
|
+
*/
|
|
501
|
+
stroke: {
|
|
502
|
+
control: { borderWidth: "control" },
|
|
503
|
+
hairline: { borderWidth: "hairline" },
|
|
504
|
+
heavy: { borderWidth: "lg" },
|
|
505
|
+
indicator: { borderWidth: "indicator" }
|
|
506
|
+
},
|
|
507
|
+
/**
|
|
508
|
+
* Whether the two track sides draw the ring behind the arc, in the palette's `muted` role.
|
|
509
|
+
*/
|
|
510
|
+
track: { true: {
|
|
511
|
+
_highContrast: {
|
|
512
|
+
borderBlockEndColor: "GrayText",
|
|
513
|
+
borderInlineStartColor: "GrayText"
|
|
514
|
+
},
|
|
515
|
+
borderBlockEndColor: "colorPalette.muted",
|
|
516
|
+
borderInlineStartColor: "colorPalette.muted"
|
|
517
|
+
} }
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
//#endregion
|
|
521
|
+
//#region src/bar.ts
|
|
522
|
+
/**
|
|
523
|
+
* Styles a bar that fills a track to a value, which the progress recipe renders for the progress
|
|
524
|
+
* bar and the meter.
|
|
525
|
+
*
|
|
526
|
+
* @remarks
|
|
527
|
+
* The root is a grid. The label starts the first row and the value ends it, and the track spans
|
|
528
|
+
* the row below, so a bar without words is the track alone. The `inline` layout puts the three on
|
|
529
|
+
* one row, with the track taking the room the words leave. The size is the track's thickness on
|
|
530
|
+
* the gap scale, 4, 6, 8, 12 and 16px from `xs` to `xl`, and the words keep the label style at
|
|
531
|
+
* every size. The range fills in the palette's solid. Under forced colors the range fills with
|
|
532
|
+
* `Highlight` and the track keeps a hairline `CanvasText` outline. Segments fill the track in the
|
|
533
|
+
* order they render, each in the theme's series color at its place unless it states a series
|
|
534
|
+
* color or a palette, and keep their colors under forced colors. A marker crosses the track at a
|
|
535
|
+
* value, 150% of its thickness, in the ink with a panel-colored edge.
|
|
536
|
+
*/
|
|
537
|
+
/**
|
|
538
|
+
* Sizes the track offers.
|
|
539
|
+
*/
|
|
540
|
+
const SIZES$1 = [
|
|
541
|
+
"xs",
|
|
542
|
+
"sm",
|
|
543
|
+
"md",
|
|
544
|
+
"lg",
|
|
545
|
+
"xl"
|
|
546
|
+
];
|
|
547
|
+
/**
|
|
548
|
+
* Custom property of a marker's place along the track, a percentage from the track's start.
|
|
549
|
+
*/
|
|
550
|
+
const MARKER = "--progress-marker";
|
|
551
|
+
/**
|
|
552
|
+
* Custom property of a segment's share of the track, a percentage.
|
|
553
|
+
*/
|
|
554
|
+
const SHARE = "--progress-share";
|
|
555
|
+
/**
|
|
556
|
+
* Attribute a segment writes the color it states to.
|
|
557
|
+
*/
|
|
558
|
+
const TINT = "data-color";
|
|
559
|
+
/**
|
|
560
|
+
* Width of a marker: the width of a line that indicates a state.
|
|
561
|
+
*/
|
|
562
|
+
const TICK = "{borderWidths.indicator}";
|
|
563
|
+
/**
|
|
564
|
+
* Styles each segment in the series color at its place: the first in `series.1`, the ninth in
|
|
565
|
+
* `series.1` again.
|
|
566
|
+
*
|
|
567
|
+
* @remarks
|
|
568
|
+
* The rules are inside `:where()`, which counts nothing towards specificity, so a stated color
|
|
569
|
+
* applies over them whatever order the rules are written in.
|
|
570
|
+
*/
|
|
571
|
+
const PLACES = Object.fromEntries(SERIES.map((step) => [`&:where(:nth-of-type(${String(SERIES.length)}n + ${step}))`, { backgroundColor: `series.${step}` }]));
|
|
572
|
+
/**
|
|
573
|
+
* Styles a segment in the series color or the palette's solid it states.
|
|
574
|
+
*/
|
|
575
|
+
const STATED = Object.fromEntries([...SERIES.map((step) => [`&[${TINT}="series.${step}"]`, { backgroundColor: `series.${step}` }]), ...PALETTES.map((palette) => [`&[${TINT}=${palette}]`, {
|
|
576
|
+
backgroundColor: "colorPalette.solid",
|
|
577
|
+
colorPalette: palette
|
|
578
|
+
}])]);
|
|
579
|
+
/**
|
|
580
|
+
* Styles the track in each look: the palette's muted role, or a muted ground with an inset line.
|
|
581
|
+
*/
|
|
582
|
+
const TRACKS$1 = {
|
|
583
|
+
outline: {
|
|
584
|
+
backgroundColor: "bg.muted",
|
|
585
|
+
boxShadow: "inset"
|
|
586
|
+
},
|
|
587
|
+
subtle: { backgroundColor: "colorPalette.muted" }
|
|
588
|
+
};
|
|
589
|
+
/**
|
|
590
|
+
* Lists the looks in the order of the shared set of looks, loudest first.
|
|
591
|
+
*/
|
|
592
|
+
const LOOKS$1 = axis(["subtle", "outline"], (look) => TRACKS$1[look]);
|
|
593
|
+
/**
|
|
594
|
+
* Styles the root, the label, the value, the track, the range, the segments and the marker of a
|
|
595
|
+
* bar.
|
|
596
|
+
*/
|
|
597
|
+
const BASE = {
|
|
598
|
+
label: {
|
|
599
|
+
textStyle: "label.sm",
|
|
600
|
+
color: "fg",
|
|
601
|
+
gridColumn: "1",
|
|
602
|
+
minInlineSize: "0",
|
|
603
|
+
overflowX: "clip",
|
|
604
|
+
overflowY: "visible",
|
|
605
|
+
textOverflow: "ellipsis",
|
|
606
|
+
whiteSpace: "nowrap"
|
|
607
|
+
},
|
|
608
|
+
marker: {
|
|
609
|
+
_highContrast: {
|
|
610
|
+
backgroundColor: "CanvasText",
|
|
611
|
+
boxShadow: "0 0 0 {borderWidths.hairline} Canvas",
|
|
612
|
+
forcedColorAdjust: "none"
|
|
613
|
+
},
|
|
614
|
+
backgroundColor: "fg",
|
|
615
|
+
boxShadow: "0 0 0 {borderWidths.hairline} {colors.bg.panel}",
|
|
616
|
+
inlineSize: TICK,
|
|
617
|
+
insetBlock: "-25%",
|
|
618
|
+
insetInlineStart: `clamp(0%, calc(var(${MARKER}) - ${TICK} / 2), calc(100% - ${TICK}))`,
|
|
619
|
+
pointerEvents: "none",
|
|
620
|
+
position: "absolute"
|
|
621
|
+
},
|
|
622
|
+
range: {
|
|
623
|
+
_highContrast: {
|
|
624
|
+
backgroundColor: "Highlight",
|
|
625
|
+
color: "HighlightText",
|
|
626
|
+
forcedColorAdjust: "none"
|
|
627
|
+
},
|
|
628
|
+
_motionReduce: { transitionDuration: "0s" },
|
|
629
|
+
backgroundColor: "colorPalette.solid",
|
|
630
|
+
blockSize: "full",
|
|
631
|
+
borderRadius: "inherit",
|
|
632
|
+
position: "relative",
|
|
633
|
+
transitionDuration: "move",
|
|
634
|
+
transitionProperty: "width",
|
|
635
|
+
transitionTimingFunction: "move"
|
|
636
|
+
},
|
|
637
|
+
root: {
|
|
638
|
+
alignItems: "center",
|
|
639
|
+
columnGap: dense("{spacing.gap.sm}"),
|
|
640
|
+
display: "grid",
|
|
641
|
+
gridAutoFlow: "dense",
|
|
642
|
+
rowGap: dense("{spacing.gap.xs}")
|
|
643
|
+
},
|
|
644
|
+
segment: {
|
|
645
|
+
...PLACES,
|
|
646
|
+
...STATED,
|
|
647
|
+
_highContrast: { forcedColorAdjust: "none" },
|
|
648
|
+
_motionReduce: { transitionDuration: "0s" },
|
|
649
|
+
"&:first-of-type": {
|
|
650
|
+
borderEndStartRadius: "inherit",
|
|
651
|
+
borderStartStartRadius: "inherit"
|
|
652
|
+
},
|
|
653
|
+
"&:last-of-type": {
|
|
654
|
+
borderEndEndRadius: "inherit",
|
|
655
|
+
borderStartEndRadius: "inherit"
|
|
656
|
+
},
|
|
657
|
+
"& + &": { _highContrast: {
|
|
658
|
+
borderInlineStartColor: "Canvas",
|
|
659
|
+
borderInlineStartStyle: "solid",
|
|
660
|
+
borderInlineStartWidth: "hairline"
|
|
661
|
+
} },
|
|
662
|
+
blockSize: "full",
|
|
663
|
+
inlineSize: `var(${SHARE})`,
|
|
664
|
+
minInlineSize: "0",
|
|
665
|
+
transitionDuration: "move",
|
|
666
|
+
transitionProperty: "inline-size",
|
|
667
|
+
transitionTimingFunction: "move"
|
|
668
|
+
},
|
|
669
|
+
track: {
|
|
670
|
+
_highContrast: {
|
|
671
|
+
outlineColor: "CanvasText",
|
|
672
|
+
outlineOffset: "calc({borderWidths.hairline} * -1)",
|
|
673
|
+
outlineStyle: "solid",
|
|
674
|
+
outlineWidth: "hairline"
|
|
675
|
+
},
|
|
676
|
+
display: "flex",
|
|
677
|
+
minInlineSize: "0",
|
|
678
|
+
position: "relative"
|
|
679
|
+
},
|
|
680
|
+
valueText: {
|
|
681
|
+
textStyle: "label.sm",
|
|
682
|
+
color: "fg.muted",
|
|
683
|
+
fontVariantNumeric: "tabular-nums",
|
|
684
|
+
gridColumn: "-2 / -1",
|
|
685
|
+
justifySelf: "end",
|
|
686
|
+
whiteSpace: "nowrap"
|
|
687
|
+
}
|
|
688
|
+
};
|
|
689
|
+
/**
|
|
690
|
+
* Offers the axes every bar takes: where the words go, the palette, the ends, the thickness and
|
|
691
|
+
* the look of the track.
|
|
692
|
+
*/
|
|
693
|
+
const VARIANTS = {
|
|
694
|
+
/**
|
|
695
|
+
* Where the words go: above the track, or on its row at either end.
|
|
696
|
+
*/
|
|
697
|
+
layout: {
|
|
698
|
+
inline: {
|
|
699
|
+
root: { gridTemplateColumns: "auto minmax(0, 1fr) auto" },
|
|
700
|
+
track: { gridColumn: "2" }
|
|
701
|
+
},
|
|
702
|
+
stacked: {
|
|
703
|
+
root: { gridTemplateColumns: "minmax(0, 1fr) auto" },
|
|
704
|
+
track: { gridColumn: "1 / -1" }
|
|
705
|
+
}
|
|
706
|
+
},
|
|
707
|
+
palette: onSlot("root", paletteVariants()),
|
|
708
|
+
/**
|
|
709
|
+
* The ends of the track, which the range takes too.
|
|
710
|
+
*/
|
|
711
|
+
shape: onSlot("track", {
|
|
712
|
+
full: { borderRadius: "full" },
|
|
713
|
+
rounded: { borderRadius: "l1" },
|
|
714
|
+
square: { borderRadius: "none" }
|
|
715
|
+
}),
|
|
716
|
+
/**
|
|
717
|
+
* The thickness of the track.
|
|
718
|
+
*/
|
|
719
|
+
size: onSlot("track", sizeVariants((size) => ({ blockSize: dense(`{spacing.gap.${size}}`) }), SIZES$1)),
|
|
720
|
+
/**
|
|
721
|
+
* Fills the track with the palette's muted role, or with a muted ground and an inset line.
|
|
722
|
+
*/
|
|
723
|
+
variant: onSlot("track", LOOKS$1())
|
|
724
|
+
};
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/progress/recipe.ts
|
|
727
|
+
/**
|
|
728
|
+
* Styles a track, a range that fills it to the value, a label and the value in words, for the
|
|
729
|
+
* progress bar and the meter.
|
|
730
|
+
*
|
|
731
|
+
* @remarks
|
|
732
|
+
* The bar's grid, its words, its track and its range come from `bar.ts`. The meter renders the
|
|
733
|
+
* same parts, so it takes the same classes and offers every axis but the stripes, their movement
|
|
734
|
+
* and the effect. Stripes are a band in the palette's contrast ink at a quarter strength over the
|
|
735
|
+
* range, and move at 40px a second. For a value the machine does not know, a segment of the range
|
|
736
|
+
* crosses the track once per 1.2s. For a reader who asked for less motion both stop, and the
|
|
737
|
+
* segment spreads over the whole track, so the bar never reads as a value it does not have.
|
|
738
|
+
*/
|
|
739
|
+
/**
|
|
740
|
+
* Styles the stripes: a band at 45 degrees in the range's ink, repeated on a square tile.
|
|
741
|
+
*/
|
|
742
|
+
const STRIPES = {
|
|
743
|
+
backgroundImage: "linear-gradient(45deg, currentColor 25%, transparent 25%, transparent 50%, currentColor 50%, currentColor 75%, transparent 75%, transparent)",
|
|
744
|
+
backgroundSize: "{sizes.4} {sizes.4}",
|
|
745
|
+
color: "colorPalette.contrast/25"
|
|
746
|
+
};
|
|
747
|
+
/**
|
|
748
|
+
* Moves the stripes three tiles towards the start per 1.2s loop, 40px a second, and the loop has
|
|
749
|
+
* no seam because it moves whole tiles.
|
|
750
|
+
*
|
|
751
|
+
* @remarks
|
|
752
|
+
* The `shimmer` motion moves a background and stops under reduced motion. Its pace is the slow
|
|
753
|
+
* ambient one, 5s. The stripes state the ambient pace where motion is allowed, because a pace
|
|
754
|
+
* stated beside the motion does not apply over it.
|
|
755
|
+
*/
|
|
756
|
+
const MOVING = {
|
|
757
|
+
animationStyle: "shimmer",
|
|
758
|
+
_motionSafe: { animationDuration: "ambient" },
|
|
759
|
+
"--animate-from": "calc({sizes.4} * 3)",
|
|
760
|
+
"--animate-to": "0"
|
|
761
|
+
};
|
|
762
|
+
/**
|
|
763
|
+
* Moves a segment half the track's width from beyond its start to beyond its end once per 1.2s.
|
|
764
|
+
*
|
|
765
|
+
* @remarks
|
|
766
|
+
* The segment is a gradient in the palette's solid that fades out at both ends. Its position runs
|
|
767
|
+
* from -100% to 200% of the room it leaves, which puts it wholly outside the track at both ends
|
|
768
|
+
* of the loop.
|
|
769
|
+
*/
|
|
770
|
+
const SWEEP = {
|
|
771
|
+
animationStyle: "shimmer",
|
|
772
|
+
_highContrast: { color: "Highlight" },
|
|
773
|
+
_motionReduce: { backgroundSize: "100% 100%" },
|
|
774
|
+
_motionSafe: {
|
|
775
|
+
animationDuration: "ambient",
|
|
776
|
+
animationTimingFunction: "in-out"
|
|
777
|
+
},
|
|
778
|
+
_rtl: {
|
|
779
|
+
"--animate-from": "200%",
|
|
780
|
+
"--animate-to": "-100%"
|
|
781
|
+
},
|
|
782
|
+
"--animate-from": "-100%",
|
|
783
|
+
"--animate-to": "200%",
|
|
784
|
+
backgroundColor: "transparent",
|
|
785
|
+
backgroundImage: "linear-gradient(to right, transparent, currentColor, transparent)",
|
|
786
|
+
backgroundRepeat: "no-repeat",
|
|
787
|
+
backgroundSize: "50% 100%",
|
|
788
|
+
color: "colorPalette.solid",
|
|
789
|
+
inlineSize: "full"
|
|
790
|
+
};
|
|
791
|
+
/**
|
|
792
|
+
* Styles a bar in the primary palette, in the outline look, at the middle size, with round ends.
|
|
793
|
+
*/
|
|
794
|
+
const recipe$4 = defineSlotRecipe({
|
|
795
|
+
base: {
|
|
796
|
+
...BASE,
|
|
797
|
+
range: {
|
|
798
|
+
...BASE.range,
|
|
799
|
+
"&[data-state=indeterminate]": SWEEP
|
|
800
|
+
}
|
|
801
|
+
},
|
|
802
|
+
className: "progress",
|
|
803
|
+
defaultVariants: {
|
|
804
|
+
layout: "stacked",
|
|
805
|
+
palette: "primary",
|
|
806
|
+
shape: "full",
|
|
807
|
+
size: "md",
|
|
808
|
+
variant: "outline"
|
|
809
|
+
},
|
|
810
|
+
jsx: [/^Progress\.\w+$/u, /^Meter\.\w+$/u],
|
|
811
|
+
slots: [
|
|
812
|
+
"root",
|
|
813
|
+
"label",
|
|
814
|
+
"valueText",
|
|
815
|
+
"track",
|
|
816
|
+
"range",
|
|
817
|
+
"segment",
|
|
818
|
+
"marker"
|
|
819
|
+
],
|
|
820
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
821
|
+
variants: {
|
|
822
|
+
/**
|
|
823
|
+
* Moves the stripes, adding them where `striped` is not set.
|
|
824
|
+
*/
|
|
825
|
+
animated: { true: { range: {
|
|
826
|
+
...STRIPES,
|
|
827
|
+
...MOVING
|
|
828
|
+
} } },
|
|
829
|
+
/**
|
|
830
|
+
* The halo around the range, in the palette's solid.
|
|
831
|
+
*
|
|
832
|
+
* @remarks
|
|
833
|
+
* `pulse` animates the halo on a layer over the range, because the range's own animation
|
|
834
|
+
* moves its stripes. Both stop under reduced motion.
|
|
835
|
+
*/
|
|
836
|
+
effect: onSlot("range", {
|
|
837
|
+
glow: { layerStyle: "glow.sm" },
|
|
838
|
+
pulse: { _after: {
|
|
839
|
+
animationStyle: "pulse-glow",
|
|
840
|
+
borderRadius: "inherit",
|
|
841
|
+
boxShadowColor: "colorPalette.solid/50",
|
|
842
|
+
content: "\"\"",
|
|
843
|
+
inset: "0",
|
|
844
|
+
position: "absolute"
|
|
845
|
+
} }
|
|
846
|
+
}),
|
|
847
|
+
layout: VARIANTS.layout,
|
|
848
|
+
palette: VARIANTS.palette,
|
|
849
|
+
shape: VARIANTS.shape,
|
|
850
|
+
size: VARIANTS.size,
|
|
851
|
+
striped: { true: { range: STRIPES } },
|
|
852
|
+
variant: VARIANTS.variant
|
|
853
|
+
}
|
|
854
|
+
});
|
|
855
|
+
//#endregion
|
|
856
|
+
//#region src/progress-circle/recipe.ts
|
|
857
|
+
/**
|
|
858
|
+
* Styles a ring whose range fills to the value, with the value in its middle and a label.
|
|
859
|
+
*
|
|
860
|
+
* @remarks
|
|
861
|
+
* The root is a grid as wide as its content, so the ring's column is as wide as the ring in any
|
|
862
|
+
* parent. The ring and the value text share one cell. The label takes the row below it or,
|
|
863
|
+
* inline, the column beside it. The size sets the `--size` and `--thickness` properties the
|
|
864
|
+
* machine's circles read: 24, 32, 40, 48 and 64px wide, and 4, 4, 4, 6 and 8px thick. From `md`
|
|
865
|
+
* up the value text takes the largest text style whose "100%" fits inside the ring. The recipe
|
|
866
|
+
* hides it at `xs` and `sm`. For a value the machine does not know, a quarter arc turns once per
|
|
867
|
+
* 1.2s. Under reduced motion the ring stops and the range is a dashed line around the whole ring.
|
|
868
|
+
* Under forced colors the range strokes in `Highlight` and the track is a hairline `CanvasText`
|
|
869
|
+
* circle at the ring's outer edge.
|
|
870
|
+
*/
|
|
871
|
+
/**
|
|
872
|
+
* Sizes the ring offers.
|
|
873
|
+
*/
|
|
874
|
+
const SIZES = [
|
|
875
|
+
"xs",
|
|
876
|
+
"sm",
|
|
877
|
+
"md",
|
|
878
|
+
"lg",
|
|
879
|
+
"xl"
|
|
880
|
+
];
|
|
881
|
+
/**
|
|
882
|
+
* Maps each size to the sizes token the ring is as wide as: 24, 32, 40, 48 and 64px.
|
|
883
|
+
*/
|
|
884
|
+
const WIDTHS = {
|
|
885
|
+
lg: "12",
|
|
886
|
+
md: "10",
|
|
887
|
+
sm: "8",
|
|
888
|
+
xl: "16",
|
|
889
|
+
xs: "6"
|
|
890
|
+
};
|
|
891
|
+
/**
|
|
892
|
+
* Maps each size to the gap token the ring is as thick as: 4, 4, 4, 6 and 8px.
|
|
893
|
+
*/
|
|
894
|
+
const THICKNESSES = {
|
|
895
|
+
lg: "sm",
|
|
896
|
+
md: "xs",
|
|
897
|
+
sm: "xs",
|
|
898
|
+
xl: "md",
|
|
899
|
+
xs: "xs"
|
|
900
|
+
};
|
|
901
|
+
/**
|
|
902
|
+
* Maps each size to the value text's styles. `xs` and `sm` hide it. From `md` up it takes the
|
|
903
|
+
* largest text style whose "100%" fits inside the hole: 32, 36 and 48px.
|
|
904
|
+
*/
|
|
905
|
+
const FIGURES = {
|
|
906
|
+
lg: { textStyle: "label.xs" },
|
|
907
|
+
md: { textStyle: "2xs" },
|
|
908
|
+
sm: { display: "none" },
|
|
909
|
+
xl: { textStyle: "label.md" },
|
|
910
|
+
xs: { display: "none" }
|
|
911
|
+
};
|
|
912
|
+
/**
|
|
913
|
+
* Styles the track under forced colors as a hairline `CanvasText` circle at the ring's outer edge.
|
|
914
|
+
*
|
|
915
|
+
* @remarks
|
|
916
|
+
* The range is a thick `Highlight` arc, so the two differ in weight whatever the system colors
|
|
917
|
+
* are. The track's own `--thickness` sets both its stroke and its radius. Chromium's `Highlight`
|
|
918
|
+
* is translucent, so a track as thick as the arc would show through it.
|
|
919
|
+
*/
|
|
920
|
+
const FORCED = {
|
|
921
|
+
"--thickness": "{borderWidths.hairline}",
|
|
922
|
+
stroke: "CanvasText"
|
|
923
|
+
};
|
|
924
|
+
/**
|
|
925
|
+
* Strokes the track in each look: the palette's muted role, or the muted ground. Each look states
|
|
926
|
+
* the forced track, because a look applies over the base.
|
|
927
|
+
*/
|
|
928
|
+
const TRACKS = {
|
|
929
|
+
outline: {
|
|
930
|
+
_highContrast: FORCED,
|
|
931
|
+
stroke: "bg.muted"
|
|
932
|
+
},
|
|
933
|
+
subtle: {
|
|
934
|
+
_highContrast: FORCED,
|
|
935
|
+
stroke: "colorPalette.muted"
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
/**
|
|
939
|
+
* Lists the looks in the order of the shared set of looks, loudest first.
|
|
940
|
+
*/
|
|
941
|
+
const LOOKS = axis(["subtle", "outline"], (look) => TRACKS[look]);
|
|
942
|
+
/**
|
|
943
|
+
* Defines the progress circle recipe, which defaults to the outline look at the middle size in the
|
|
944
|
+
* primary palette, with round ends.
|
|
945
|
+
*/
|
|
946
|
+
const recipe$3 = defineSlotRecipe({
|
|
947
|
+
base: {
|
|
948
|
+
circle: {
|
|
949
|
+
_highContrast: { forcedColorAdjust: "none" },
|
|
950
|
+
"&[data-state=indeterminate]": { animationStyle: "spin" },
|
|
951
|
+
display: "block",
|
|
952
|
+
gridArea: "ring"
|
|
953
|
+
},
|
|
954
|
+
label: {
|
|
955
|
+
textStyle: "label.sm",
|
|
956
|
+
color: "fg",
|
|
957
|
+
gridArea: "label"
|
|
958
|
+
},
|
|
959
|
+
range: {
|
|
960
|
+
_highContrast: { stroke: "Highlight" },
|
|
961
|
+
_motionReduce: { transitionDuration: "0s" },
|
|
962
|
+
"&[data-state=indeterminate]": {
|
|
963
|
+
_motionReduce: { strokeDasharray: "var(--thickness) calc(var(--thickness) * 2)" },
|
|
964
|
+
strokeDasharray: "calc(var(--circumference) / 4) var(--circumference)"
|
|
965
|
+
},
|
|
966
|
+
stroke: "colorPalette.solid",
|
|
967
|
+
transitionDuration: "move",
|
|
968
|
+
transitionProperty: "stroke-dashoffset",
|
|
969
|
+
transitionTimingFunction: "move"
|
|
970
|
+
},
|
|
971
|
+
root: {
|
|
972
|
+
alignItems: "center",
|
|
973
|
+
columnGap: dense("{spacing.gap.sm}"),
|
|
974
|
+
display: "inline-grid",
|
|
975
|
+
inlineSize: "fit",
|
|
976
|
+
rowGap: dense("{spacing.gap.xs}")
|
|
977
|
+
},
|
|
978
|
+
track: { stroke: "bg.muted" },
|
|
979
|
+
valueText: {
|
|
980
|
+
alignSelf: "center",
|
|
981
|
+
color: "fg",
|
|
982
|
+
fontVariantNumeric: "tabular-nums",
|
|
983
|
+
fontWeight: "medium",
|
|
984
|
+
gridArea: "ring",
|
|
985
|
+
justifySelf: "center",
|
|
986
|
+
whiteSpace: "nowrap"
|
|
987
|
+
}
|
|
988
|
+
},
|
|
989
|
+
className: "progress-circle",
|
|
990
|
+
defaultVariants: {
|
|
991
|
+
layout: "stacked",
|
|
992
|
+
palette: "primary",
|
|
993
|
+
shape: "full",
|
|
994
|
+
size: "md",
|
|
995
|
+
variant: "outline"
|
|
996
|
+
},
|
|
997
|
+
jsx: [/^ProgressCircle\.\w+$/u],
|
|
998
|
+
slots: [
|
|
999
|
+
"root",
|
|
1000
|
+
"label",
|
|
1001
|
+
"circle",
|
|
1002
|
+
"track",
|
|
1003
|
+
"range",
|
|
1004
|
+
"valueText"
|
|
1005
|
+
],
|
|
1006
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
1007
|
+
variants: {
|
|
1008
|
+
/**
|
|
1009
|
+
* Where the label goes: under the ring, or beside it.
|
|
1010
|
+
*/
|
|
1011
|
+
layout: {
|
|
1012
|
+
inline: { root: {
|
|
1013
|
+
gridTemplateAreas: "\"ring label\"",
|
|
1014
|
+
justifyItems: "start"
|
|
1015
|
+
} },
|
|
1016
|
+
stacked: { root: {
|
|
1017
|
+
gridTemplateAreas: "\"ring\" \"label\"",
|
|
1018
|
+
justifyItems: "center"
|
|
1019
|
+
} }
|
|
1020
|
+
},
|
|
1021
|
+
palette: onSlot("root", paletteVariants()),
|
|
1022
|
+
/**
|
|
1023
|
+
* The ends of the range.
|
|
1024
|
+
*/
|
|
1025
|
+
shape: onSlot("range", {
|
|
1026
|
+
full: { strokeLinecap: "round" },
|
|
1027
|
+
square: { strokeLinecap: "butt" }
|
|
1028
|
+
}),
|
|
1029
|
+
/**
|
|
1030
|
+
* The ring's width and thickness, and the value text's style.
|
|
1031
|
+
*/
|
|
1032
|
+
size: onSlots({
|
|
1033
|
+
root: sizeVariants((size) => ({
|
|
1034
|
+
"--size": dense(`{sizes.${WIDTHS[size]}}`),
|
|
1035
|
+
"--thickness": dense(`{spacing.gap.${THICKNESSES[size]}}`)
|
|
1036
|
+
}), SIZES),
|
|
1037
|
+
valueText: sizeVariants((size) => FIGURES[size], SIZES)
|
|
1038
|
+
}),
|
|
1039
|
+
/**
|
|
1040
|
+
* Strokes the track in the palette's muted role, or in the muted ground.
|
|
1041
|
+
*/
|
|
1042
|
+
variant: onSlot("track", LOOKS())
|
|
1043
|
+
}
|
|
1044
|
+
});
|
|
1045
|
+
//#endregion
|
|
1046
|
+
//#region src/skeleton-text/recipe.ts
|
|
1047
|
+
/**
|
|
1048
|
+
* Declares the recipe of the skeleton text column: one bar per line, spaced by half a line.
|
|
1049
|
+
*
|
|
1050
|
+
* @remarks
|
|
1051
|
+
* Every length is in `lh`, the line height of the text the bars replace, so the placeholder
|
|
1052
|
+
* takes the size of the text around it without an absolute length. Each bar is `1lh` tall and
|
|
1053
|
+
* the column's gap is `0.5lh`. The gap is on the column and not a clipped inset on the bar,
|
|
1054
|
+
* because the skeleton's `loading` variant sets `background` and `background-clip: padding-box`
|
|
1055
|
+
* in the variants layer, which overrides any clip this base sets on a bar. With two or more bars
|
|
1056
|
+
* the last is capped at 80% width, so the stack reads as a paragraph and not as a table.
|
|
1057
|
+
*/
|
|
1058
|
+
/**
|
|
1059
|
+
* Skeleton text recipe: a full-width flex column of line-height bars.
|
|
1060
|
+
*/
|
|
1061
|
+
const recipe$2 = defineRecipe({
|
|
1062
|
+
base: {
|
|
1063
|
+
"& > *": { blockSize: "1lh" },
|
|
1064
|
+
"& > *:last-child:not(:only-child)": { maxWidth: "80%" },
|
|
1065
|
+
display: "flex",
|
|
1066
|
+
flexDirection: "column",
|
|
1067
|
+
gap: "0.5lh",
|
|
1068
|
+
width: "full"
|
|
1069
|
+
},
|
|
1070
|
+
className: "skeleton-text",
|
|
1071
|
+
jsx: [/^SkeletonText$/u]
|
|
1072
|
+
});
|
|
1073
|
+
//#endregion
|
|
1074
|
+
//#region src/skeleton/recipe.ts
|
|
1075
|
+
/**
|
|
1076
|
+
* Declares the skeleton recipe, which paints a placeholder over content that is still loading.
|
|
1077
|
+
*
|
|
1078
|
+
* @remarks
|
|
1079
|
+
* A skeleton wraps the content it covers, so a caller writes one tree and toggles `loading`.
|
|
1080
|
+
* While loading, the element keeps the box of its content and hides everything inside it, so the
|
|
1081
|
+
* placeholder has the content's size without a declared width. The fade-in is in the base,
|
|
1082
|
+
* because the compiler emits no class for the `false` value of a boolean axis. Each motion is
|
|
1083
|
+
* nested under the loading class, so a finished skeleton runs no animation. Every motion is a
|
|
1084
|
+
* theme animation style, so the theme applies the reduced-motion preference once. The fill is the
|
|
1085
|
+
* neutral palette's `muted` role, which is lighter than a dark page. Under forced colors the
|
|
1086
|
+
* browser replaces the fill with Canvas, so a loading skeleton draws a `GrayText` hairline
|
|
1087
|
+
* outline inside its box. An outline leaves the layout unchanged. The recipe has no `palette`
|
|
1088
|
+
* axis, because a placeholder signals loading and not a category. It has no `effect` axis,
|
|
1089
|
+
* because `motion` already animates it.
|
|
1090
|
+
*/
|
|
1091
|
+
/**
|
|
1092
|
+
* Selector for a skeleton that is loading, under which each motion nests its rules.
|
|
1093
|
+
*
|
|
1094
|
+
* @remarks
|
|
1095
|
+
* The selector uses the compiler's variant class name, the axis and the value joined by `_`, so
|
|
1096
|
+
* the naming plugin rewrites it together with the class.
|
|
1097
|
+
*/
|
|
1098
|
+
const WHILE_LOADING = "&.skeleton--loading_true";
|
|
1099
|
+
/**
|
|
1100
|
+
* Skeleton recipe, loading with the pulse motion at the l2 radius by default.
|
|
1101
|
+
*/
|
|
1102
|
+
const recipe$1 = defineRecipe({
|
|
1103
|
+
base: { animationStyle: "fade.in" },
|
|
1104
|
+
className: "skeleton",
|
|
1105
|
+
defaultVariants: {
|
|
1106
|
+
loading: true,
|
|
1107
|
+
motion: "pulse",
|
|
1108
|
+
radius: "l2"
|
|
1109
|
+
},
|
|
1110
|
+
jsx: [/^Skeleton$/u],
|
|
1111
|
+
variants: {
|
|
1112
|
+
/**
|
|
1113
|
+
* Whether the content is still loading. `true` paints the placeholder and hides the content.
|
|
1114
|
+
*/
|
|
1115
|
+
loading: { true: {
|
|
1116
|
+
_highContrast: {
|
|
1117
|
+
outlineColor: "GrayText",
|
|
1118
|
+
outlineOffset: "calc(-1 * {borderWidths.hairline})",
|
|
1119
|
+
outlineStyle: "solid",
|
|
1120
|
+
outlineWidth: "hairline"
|
|
1121
|
+
},
|
|
1122
|
+
"&::before, &::after, *": { visibility: "hidden" },
|
|
1123
|
+
background: "colorPalette.muted",
|
|
1124
|
+
backgroundClip: "padding-box",
|
|
1125
|
+
boxShadow: "none",
|
|
1126
|
+
color: "transparent",
|
|
1127
|
+
colorPalette: "neutral",
|
|
1128
|
+
flexShrink: "0",
|
|
1129
|
+
pointerEvents: "none",
|
|
1130
|
+
userSelect: "none"
|
|
1131
|
+
} },
|
|
1132
|
+
/**
|
|
1133
|
+
* Animation of the placeholder while loading.
|
|
1134
|
+
*/
|
|
1135
|
+
motion: {
|
|
1136
|
+
none: { [WHILE_LOADING]: { animation: "none" } },
|
|
1137
|
+
pulse: { [WHILE_LOADING]: { animationStyle: "pulse" } },
|
|
1138
|
+
shimmer: { [WHILE_LOADING]: {
|
|
1139
|
+
animationStyle: "shimmer",
|
|
1140
|
+
backgroundImage: "linear-gradient(270deg, var(--colors-color-palette-muted), var(--colors-color-palette-emphasized))",
|
|
1141
|
+
backgroundSize: "400% 100%"
|
|
1142
|
+
} }
|
|
1143
|
+
},
|
|
1144
|
+
radius: cornerVariants()
|
|
1145
|
+
}
|
|
1146
|
+
});
|
|
1147
|
+
//#endregion
|
|
1148
|
+
//#region src/toast/recipe.ts
|
|
1149
|
+
/**
|
|
1150
|
+
* Recipe for the toast: a card raised into a corner of the window, with a mark, a title, a
|
|
1151
|
+
* description, an action and a close button.
|
|
1152
|
+
*
|
|
1153
|
+
* @remarks
|
|
1154
|
+
* The machine places each toast in its region and writes the toast's motion as custom
|
|
1155
|
+
* properties: `--x`, `--y`, `--scale`, `--opacity`, `--height` and `--z-index`. The root reads
|
|
1156
|
+
* them and moves between them on the theme's `move` pace, and on the `leave` pace once it is
|
|
1157
|
+
* dismissed. The toast's type sets `colorPalette`, `neutral` for `loading`, and the indicator
|
|
1158
|
+
* renders the caller's glyph in the palette's solid. A focused toast shows the theme's focus
|
|
1159
|
+
* ring outside its edge, in the palette's `focusRing` color. The root is at most `sizes.sm` wide
|
|
1160
|
+
* and never wider than the window less the region's offsets. The recipe has no axes, because a
|
|
1161
|
+
* toast has one look. A theme restyles it by extending the recipe.
|
|
1162
|
+
*/
|
|
1163
|
+
/**
|
|
1164
|
+
* Maps each type the machine writes on a toast to the palette the toast reads.
|
|
1165
|
+
*/
|
|
1166
|
+
const TYPES = {
|
|
1167
|
+
error: "error",
|
|
1168
|
+
info: "info",
|
|
1169
|
+
loading: "neutral",
|
|
1170
|
+
success: "success",
|
|
1171
|
+
warning: "warning"
|
|
1172
|
+
};
|
|
1173
|
+
/**
|
|
1174
|
+
* Size of the close trigger's box: 1.5em, and never less than the 24px a target needs.
|
|
1175
|
+
*/
|
|
1176
|
+
const CLOSE = "max({sizes.6}, 1.5em)";
|
|
1177
|
+
/**
|
|
1178
|
+
* Defines the toast recipe.
|
|
1179
|
+
*/
|
|
1180
|
+
const recipe = defineSlotRecipe({
|
|
1181
|
+
base: {
|
|
1182
|
+
actionTrigger: {
|
|
1183
|
+
...interactive(),
|
|
1184
|
+
textStyle: "label.sm",
|
|
1185
|
+
_hover: { background: "bg.muted" },
|
|
1186
|
+
alignItems: "center",
|
|
1187
|
+
alignSelf: "center",
|
|
1188
|
+
background: "transparent",
|
|
1189
|
+
borderColor: "border",
|
|
1190
|
+
borderRadius: "l2",
|
|
1191
|
+
borderStyle: "solid",
|
|
1192
|
+
borderWidth: "hairline",
|
|
1193
|
+
color: "fg",
|
|
1194
|
+
display: "inline-flex",
|
|
1195
|
+
flexShrink: "0",
|
|
1196
|
+
fontWeight: "medium",
|
|
1197
|
+
height: dense("{sizes.control.xs}"),
|
|
1198
|
+
paddingInline: dense("{spacing.inset.sm}")
|
|
1199
|
+
},
|
|
1200
|
+
/**
|
|
1201
|
+
* The close trigger centres on the first line of the title, and a negative end margin puts
|
|
1202
|
+
* its glyph on the root's padding edge.
|
|
1203
|
+
*/
|
|
1204
|
+
closeTrigger: {
|
|
1205
|
+
...interactive(),
|
|
1206
|
+
...touchTarget(),
|
|
1207
|
+
_hover: {
|
|
1208
|
+
background: "bg.muted",
|
|
1209
|
+
color: "fg"
|
|
1210
|
+
},
|
|
1211
|
+
"& > svg": { boxSize: "1em" },
|
|
1212
|
+
alignItems: "center",
|
|
1213
|
+
background: "transparent",
|
|
1214
|
+
borderRadius: "l1",
|
|
1215
|
+
borderStyle: "none",
|
|
1216
|
+
boxSize: CLOSE,
|
|
1217
|
+
color: "fg.muted",
|
|
1218
|
+
display: "inline-flex",
|
|
1219
|
+
flexShrink: "0",
|
|
1220
|
+
justifyContent: "center",
|
|
1221
|
+
marginBlock: `calc((1lh - ${CLOSE}) / 2)`,
|
|
1222
|
+
marginInlineEnd: `calc((1em - ${CLOSE}) / 2)`,
|
|
1223
|
+
padding: "0"
|
|
1224
|
+
},
|
|
1225
|
+
content: {
|
|
1226
|
+
display: "flex",
|
|
1227
|
+
flex: "1",
|
|
1228
|
+
flexDirection: "column",
|
|
1229
|
+
gap: dense("{spacing.gap.xs}"),
|
|
1230
|
+
minInlineSize: "0"
|
|
1231
|
+
},
|
|
1232
|
+
description: { color: "fg.muted" },
|
|
1233
|
+
indicator: {
|
|
1234
|
+
"& > svg": { boxSize: "{sizes.icon.sm}" },
|
|
1235
|
+
alignItems: "center",
|
|
1236
|
+
blockSize: "1lh",
|
|
1237
|
+
color: "colorPalette.solid",
|
|
1238
|
+
display: "inline-flex",
|
|
1239
|
+
flexShrink: "0"
|
|
1240
|
+
},
|
|
1241
|
+
region: {},
|
|
1242
|
+
root: {
|
|
1243
|
+
...Object.fromEntries(Object.entries(TYPES).map(([type, palette]) => [`&[data-type=${type}]`, { colorPalette: palette }])),
|
|
1244
|
+
focusVisibleRing: "outside",
|
|
1245
|
+
textStyle: "body.sm",
|
|
1246
|
+
_closed: {
|
|
1247
|
+
transitionDuration: "leave",
|
|
1248
|
+
transitionTimingFunction: "leave"
|
|
1249
|
+
},
|
|
1250
|
+
_motionReduce: { transitionDuration: "0s" },
|
|
1251
|
+
alignItems: "flex-start",
|
|
1252
|
+
background: "bg.panel",
|
|
1253
|
+
borderColor: "border",
|
|
1254
|
+
borderRadius: "l3",
|
|
1255
|
+
borderStyle: "solid",
|
|
1256
|
+
borderWidth: "hairline",
|
|
1257
|
+
boxShadow: "lg",
|
|
1258
|
+
color: "fg",
|
|
1259
|
+
display: "flex",
|
|
1260
|
+
focusRingColor: "colorPalette.focusRing",
|
|
1261
|
+
gap: dense("{spacing.gap.md}"),
|
|
1262
|
+
height: "var(--height)",
|
|
1263
|
+
inlineSize: "min({sizes.sm}, calc(100vw - var(--viewport-offset-left) - var(--viewport-offset-right)))",
|
|
1264
|
+
opacity: "var(--opacity)",
|
|
1265
|
+
padding: dense("{spacing.inset.md}"),
|
|
1266
|
+
scale: "var(--scale)",
|
|
1267
|
+
transitionDuration: "move",
|
|
1268
|
+
transitionProperty: "translate, scale, opacity, height, box-shadow",
|
|
1269
|
+
transitionTimingFunction: "move",
|
|
1270
|
+
translate: "var(--x) var(--y)",
|
|
1271
|
+
willChange: "translate, opacity, scale",
|
|
1272
|
+
zIndex: "var(--z-index)"
|
|
1273
|
+
},
|
|
1274
|
+
title: { fontWeight: "medium" }
|
|
1275
|
+
},
|
|
1276
|
+
className: "toast",
|
|
1277
|
+
jsx: [/^Toast(\.\w+)?$/u],
|
|
1278
|
+
slots: [
|
|
1279
|
+
"region",
|
|
1280
|
+
"root",
|
|
1281
|
+
"indicator",
|
|
1282
|
+
"content",
|
|
1283
|
+
"title",
|
|
1284
|
+
"description",
|
|
1285
|
+
"actionTrigger",
|
|
1286
|
+
"closeTrigger"
|
|
1287
|
+
]
|
|
1288
|
+
});
|
|
1289
|
+
//#endregion
|
|
1290
|
+
export { recipe$4 as a, TINT as c, recipe$7 as d, recipe$8 as f, recipe$3 as i, recipe$5 as l, recipe$1 as n, MARKER as o, recipe$2 as r, SHARE as s, recipe as t, recipe$6 as u };
|