@stealthscale/component-disclosure 0.1.0 → 0.2.3
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 +759 -284
- package/dist/cyclonedx/bom.json +1 -1
- package/dist/index.d.ts +1701 -513
- package/dist/index.js +4781 -1429
- package/dist/recipe-95yCB2V3.js +2130 -0
- package/dist/theme.js +39 -21
- package/package.json +42 -33
- package/dist/recipe-BdO9ZFWU.js +0 -708
|
@@ -0,0 +1,2130 @@
|
|
|
1
|
+
import { PALETTES, below, controlSizes, defineRecipe, defineSlotRecipe, dense, divider, highlightVariants, iconSizes, insetSizes, interactive, justifyVariants, link, motion, onSlot, onSlots, paletteVariants, row, sizeVariants, surface, textSizes, truncate } from "@stealthscale/theme/authoring";
|
|
2
|
+
//#region src/accordion/recipe.ts
|
|
3
|
+
/**
|
|
4
|
+
* Recipe for the accordion: a column of items, each a heading whose trigger shows and hides the
|
|
5
|
+
* content under it.
|
|
6
|
+
*
|
|
7
|
+
* @remarks
|
|
8
|
+
* The trigger fills its heading, sets its title in the label style and puts the indicator at its
|
|
9
|
+
* end, so an icon or an avatar before the title leaves the indicators in one column. The trigger
|
|
10
|
+
* is at least a control's height and grows with a title that wraps. A heading with a second child
|
|
11
|
+
* after the trigger, such as an action button, places that child at the end of the row, outside
|
|
12
|
+
* the button. The content animates to the height the collapsible machine measures, and the body
|
|
13
|
+
* inside it has the padding, so the animation runs down to zero height. The subtle, surface
|
|
14
|
+
* and outline looks inset the trigger and the body from the box's edges. The flushed and plain
|
|
15
|
+
* looks align both with the text around the accordion. The recipe has no `effect` axis, because
|
|
16
|
+
* the box contains content as well as the triggers, and a glow marks a control.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Sizes the recipe offers.
|
|
20
|
+
*/
|
|
21
|
+
const SIZES$4 = [
|
|
22
|
+
"sm",
|
|
23
|
+
"md",
|
|
24
|
+
"lg"
|
|
25
|
+
];
|
|
26
|
+
/**
|
|
27
|
+
* Selects a heading that contains a second child after its trigger.
|
|
28
|
+
*/
|
|
29
|
+
const FOLLOWED = "&:has(> :nth-child(2))";
|
|
30
|
+
/**
|
|
31
|
+
* Places the focus ring one ring width inside the trigger's edge, for the looks whose root clips
|
|
32
|
+
* what falls outside it.
|
|
33
|
+
*/
|
|
34
|
+
const INSIDE = { _focusVisible: { outlineOffset: "calc({borderWidths.ring} * -1)" } };
|
|
35
|
+
/**
|
|
36
|
+
* Removes the inline inset of the trigger and the body, for the looks that align their text with
|
|
37
|
+
* the text around the accordion.
|
|
38
|
+
*/
|
|
39
|
+
const FLUSH = { paddingInline: "0" };
|
|
40
|
+
/**
|
|
41
|
+
* Removes the end inset a heading gives the child after its trigger, for the same looks.
|
|
42
|
+
*/
|
|
43
|
+
const FLUSH_END = { [FOLLOWED]: { paddingInlineEnd: "0" } };
|
|
44
|
+
/**
|
|
45
|
+
* Rules a hairline under every item but the last.
|
|
46
|
+
*/
|
|
47
|
+
const DIVIDED = {
|
|
48
|
+
_last: { borderBlockEndWidth: "0" },
|
|
49
|
+
borderBlockEndWidth: "hairline"
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Defines the accordion recipe: flushed items at size `md` in the neutral palette, sliding open,
|
|
53
|
+
* by default.
|
|
54
|
+
*/
|
|
55
|
+
const recipe$11 = defineSlotRecipe({
|
|
56
|
+
base: {
|
|
57
|
+
item: { overflowAnchor: "none" },
|
|
58
|
+
itemContent: { overflow: "hidden" },
|
|
59
|
+
itemHeading: {
|
|
60
|
+
alignItems: "center",
|
|
61
|
+
display: "flex"
|
|
62
|
+
},
|
|
63
|
+
itemIndicator: {
|
|
64
|
+
_motionReduce: { transitionDuration: "none" },
|
|
65
|
+
_open: { rotate: "180deg" },
|
|
66
|
+
alignItems: "center",
|
|
67
|
+
color: "fg.muted",
|
|
68
|
+
display: "inline-flex",
|
|
69
|
+
flexShrink: "0",
|
|
70
|
+
justifyContent: "center",
|
|
71
|
+
marginInlineStart: "auto",
|
|
72
|
+
transitionDuration: "press",
|
|
73
|
+
transitionProperty: "rotate, color",
|
|
74
|
+
transitionTimingFunction: "press"
|
|
75
|
+
},
|
|
76
|
+
itemTrigger: {
|
|
77
|
+
...interactive(),
|
|
78
|
+
_hover: { "& .accordion__itemIndicator": { color: "fg" } },
|
|
79
|
+
"& > svg": { flexShrink: "0" },
|
|
80
|
+
alignItems: "center",
|
|
81
|
+
display: "flex",
|
|
82
|
+
flex: "1",
|
|
83
|
+
minInlineSize: "0",
|
|
84
|
+
textAlign: "start"
|
|
85
|
+
},
|
|
86
|
+
root: { inlineSize: "full" }
|
|
87
|
+
},
|
|
88
|
+
className: "accordion",
|
|
89
|
+
defaultVariants: {
|
|
90
|
+
motion: "slide",
|
|
91
|
+
palette: "neutral",
|
|
92
|
+
size: "md",
|
|
93
|
+
variant: "flushed"
|
|
94
|
+
},
|
|
95
|
+
jsx: [/^Accordion(\.\w+)?$/u],
|
|
96
|
+
slots: [
|
|
97
|
+
"root",
|
|
98
|
+
"item",
|
|
99
|
+
"itemHeading",
|
|
100
|
+
"itemTrigger",
|
|
101
|
+
"itemIndicator",
|
|
102
|
+
"itemContent",
|
|
103
|
+
"itemBody"
|
|
104
|
+
],
|
|
105
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
106
|
+
variants: {
|
|
107
|
+
/**
|
|
108
|
+
* Animation of the content as it opens and closes.
|
|
109
|
+
*/
|
|
110
|
+
motion: {
|
|
111
|
+
fade: { itemContent: {
|
|
112
|
+
_closed: { animationStyle: "fade.out" },
|
|
113
|
+
_open: { animationStyle: "fade.in" }
|
|
114
|
+
} },
|
|
115
|
+
none: { itemContent: { animation: "none" } },
|
|
116
|
+
slide: { itemContent: {
|
|
117
|
+
_closed: { animationStyle: "collapse.out" },
|
|
118
|
+
_open: { animationStyle: "collapse.in" }
|
|
119
|
+
} }
|
|
120
|
+
},
|
|
121
|
+
/**
|
|
122
|
+
* Palette the looks read, set on the root.
|
|
123
|
+
*/
|
|
124
|
+
palette: onSlot("root", paletteVariants()),
|
|
125
|
+
/**
|
|
126
|
+
* Size of the trigger on the control scale, the indicator on the icon scale, and the body's
|
|
127
|
+
* padding and text.
|
|
128
|
+
*
|
|
129
|
+
* @remarks
|
|
130
|
+
* A trigger is at least a control's height. Its block padding is the gap one step below, so a
|
|
131
|
+
* one-line title centres in that height and a title that wraps adds its lines. A leading icon
|
|
132
|
+
* is on the icon scale one step below the indicator. The body starts one gap below the
|
|
133
|
+
* trigger and ends one inset above the next item.
|
|
134
|
+
*/
|
|
135
|
+
size: onSlots({
|
|
136
|
+
itemBody: sizeVariants((size) => ({
|
|
137
|
+
textStyle: `body.${size}`,
|
|
138
|
+
paddingBlockEnd: dense(`{spacing.inset.${size}}`),
|
|
139
|
+
paddingBlockStart: dense(`{spacing.gap.${below(size)}}`),
|
|
140
|
+
paddingInline: dense(`{spacing.inset.${size}}`)
|
|
141
|
+
}), SIZES$4),
|
|
142
|
+
itemHeading: sizeVariants((size) => ({
|
|
143
|
+
[FOLLOWED]: { paddingInlineEnd: dense(`{spacing.inset.${below(size)}}`) },
|
|
144
|
+
gap: dense(`{spacing.gap.${size}}`)
|
|
145
|
+
}), SIZES$4),
|
|
146
|
+
itemIndicator: iconSizes(SIZES$4),
|
|
147
|
+
itemTrigger: sizeVariants((size) => ({
|
|
148
|
+
textStyle: `label.${size}`,
|
|
149
|
+
"&:not(:last-child)": { paddingInlineEnd: "0" },
|
|
150
|
+
"& > svg": { boxSize: dense(`{sizes.icon.${below(size)}}`) },
|
|
151
|
+
gap: dense(`{spacing.gap.${size}}`),
|
|
152
|
+
minBlockSize: dense(`{sizes.control.${size}}`),
|
|
153
|
+
paddingBlock: dense(`{spacing.gap.${below(size)}}`),
|
|
154
|
+
paddingInline: dense(`{spacing.inset.${size}}`)
|
|
155
|
+
}), SIZES$4)
|
|
156
|
+
}),
|
|
157
|
+
/**
|
|
158
|
+
* Look of the items and the box around them.
|
|
159
|
+
*
|
|
160
|
+
* @remarks
|
|
161
|
+
* `subtle` fills the open item from the palette and fills a trigger under the pointer.
|
|
162
|
+
* `surface` is the theme's panel, with its shadow, and `outline` a hairline box, both with a
|
|
163
|
+
* hairline between items. `flushed` rules a hairline under every item and renders no box.
|
|
164
|
+
* `plain` renders no line. The boxed looks clip their content to the box's corners and place
|
|
165
|
+
* the focus ring inside the trigger. The rules and the outline read the palette's muted role,
|
|
166
|
+
* and the surface's rules read the panel's edge color.
|
|
167
|
+
*/
|
|
168
|
+
variant: {
|
|
169
|
+
subtle: {
|
|
170
|
+
item: {
|
|
171
|
+
_open: { background: "colorPalette.subtle" },
|
|
172
|
+
borderRadius: "l2"
|
|
173
|
+
},
|
|
174
|
+
itemTrigger: {
|
|
175
|
+
...INSIDE,
|
|
176
|
+
_hover: { background: "colorPalette.subtle" },
|
|
177
|
+
_open: {
|
|
178
|
+
_hover: { background: "colorPalette.muted" },
|
|
179
|
+
borderEndEndRadius: "0",
|
|
180
|
+
borderEndStartRadius: "0"
|
|
181
|
+
},
|
|
182
|
+
borderRadius: "l2"
|
|
183
|
+
}
|
|
184
|
+
},
|
|
185
|
+
surface: {
|
|
186
|
+
item: {
|
|
187
|
+
...DIVIDED,
|
|
188
|
+
borderBlockEndColor: "border"
|
|
189
|
+
},
|
|
190
|
+
itemTrigger: {
|
|
191
|
+
...INSIDE,
|
|
192
|
+
_hover: { background: "colorPalette.subtle" }
|
|
193
|
+
},
|
|
194
|
+
root: {
|
|
195
|
+
...surface(),
|
|
196
|
+
overflow: "clip"
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
outline: {
|
|
200
|
+
item: {
|
|
201
|
+
...DIVIDED,
|
|
202
|
+
borderBlockEndColor: "colorPalette.muted"
|
|
203
|
+
},
|
|
204
|
+
itemTrigger: {
|
|
205
|
+
...INSIDE,
|
|
206
|
+
_hover: { background: "colorPalette.subtle" }
|
|
207
|
+
},
|
|
208
|
+
root: {
|
|
209
|
+
borderColor: "colorPalette.muted",
|
|
210
|
+
borderRadius: "l2",
|
|
211
|
+
borderWidth: "hairline",
|
|
212
|
+
overflow: "clip"
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
flushed: {
|
|
216
|
+
item: {
|
|
217
|
+
borderBlockEndColor: "colorPalette.muted",
|
|
218
|
+
borderBlockEndWidth: "hairline"
|
|
219
|
+
},
|
|
220
|
+
itemBody: FLUSH,
|
|
221
|
+
itemHeading: FLUSH_END,
|
|
222
|
+
itemTrigger: FLUSH
|
|
223
|
+
},
|
|
224
|
+
plain: {
|
|
225
|
+
itemBody: FLUSH,
|
|
226
|
+
itemHeading: FLUSH_END,
|
|
227
|
+
itemTrigger: FLUSH
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
//#endregion
|
|
233
|
+
//#region src/collapsible/recipe.ts
|
|
234
|
+
/**
|
|
235
|
+
* Recipe for the collapsible: a trigger that shows and hides the content beneath it, and an
|
|
236
|
+
* indicator that turns as it does.
|
|
237
|
+
*
|
|
238
|
+
* @remarks
|
|
239
|
+
* The trigger takes the full width and places the indicator at its end, and reads the control
|
|
240
|
+
* scale, so its height matches a button of the same size. The content animates to the height the
|
|
241
|
+
* machine measures, with the theme's animation styles, so the theme settles reduced motion. The
|
|
242
|
+
* palette axis offers the eight palettes. The recipe has no `effect` axis, because the box holds
|
|
243
|
+
* content as well as the trigger, and a glow marks a control.
|
|
244
|
+
*/
|
|
245
|
+
/**
|
|
246
|
+
* Defines the collapsible recipe: a plain collapsible at size `md` in the neutral palette, sliding
|
|
247
|
+
* open, by default.
|
|
248
|
+
*/
|
|
249
|
+
const recipe$10 = defineSlotRecipe({
|
|
250
|
+
base: {
|
|
251
|
+
content: { overflow: "hidden" },
|
|
252
|
+
indicator: {
|
|
253
|
+
_motionReduce: { transitionDuration: "none" },
|
|
254
|
+
_open: { rotate: "180deg" },
|
|
255
|
+
alignItems: "center",
|
|
256
|
+
display: "inline-flex",
|
|
257
|
+
flexShrink: "0",
|
|
258
|
+
justifyContent: "center",
|
|
259
|
+
transitionDuration: "press",
|
|
260
|
+
transitionProperty: "rotate",
|
|
261
|
+
transitionTimingFunction: "press"
|
|
262
|
+
},
|
|
263
|
+
root: { width: "full" },
|
|
264
|
+
trigger: {
|
|
265
|
+
...interactive(),
|
|
266
|
+
alignItems: "center",
|
|
267
|
+
display: "flex",
|
|
268
|
+
justifyContent: "space-between",
|
|
269
|
+
textAlign: "start",
|
|
270
|
+
width: "full"
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
className: "collapsible",
|
|
274
|
+
defaultVariants: {
|
|
275
|
+
motion: "slide",
|
|
276
|
+
palette: "neutral",
|
|
277
|
+
size: "md",
|
|
278
|
+
variant: "plain"
|
|
279
|
+
},
|
|
280
|
+
jsx: [/^Collapsible(\.\w+)?$/u],
|
|
281
|
+
slots: [
|
|
282
|
+
"root",
|
|
283
|
+
"trigger",
|
|
284
|
+
"content",
|
|
285
|
+
"indicator"
|
|
286
|
+
],
|
|
287
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
288
|
+
variants: {
|
|
289
|
+
/**
|
|
290
|
+
* Animation of the content as it opens and closes.
|
|
291
|
+
*/
|
|
292
|
+
motion: {
|
|
293
|
+
fade: { content: {
|
|
294
|
+
_closed: { animationStyle: "fade.out" },
|
|
295
|
+
_open: { animationStyle: "fade.in" }
|
|
296
|
+
} },
|
|
297
|
+
none: { content: { animation: "none" } },
|
|
298
|
+
slide: { content: {
|
|
299
|
+
_closed: { animationStyle: "collapse.out" },
|
|
300
|
+
_open: { animationStyle: "collapse.in" }
|
|
301
|
+
} }
|
|
302
|
+
},
|
|
303
|
+
/**
|
|
304
|
+
* Palette the looks read, set on the root.
|
|
305
|
+
*/
|
|
306
|
+
palette: onSlot("root", paletteVariants()),
|
|
307
|
+
/**
|
|
308
|
+
* Size of the trigger on the control scale, the indicator on the icon scale and the content's
|
|
309
|
+
* padding on the inset scale.
|
|
310
|
+
*/
|
|
311
|
+
size: onSlots({
|
|
312
|
+
content: insetSizes(),
|
|
313
|
+
indicator: iconSizes(),
|
|
314
|
+
trigger: controlSizes()
|
|
315
|
+
}),
|
|
316
|
+
/**
|
|
317
|
+
* Look of the box around the trigger and the content.
|
|
318
|
+
*
|
|
319
|
+
* @remarks
|
|
320
|
+
* `surface` uses the theme's `surface()` fragment, with its shadow. Every look reads the
|
|
321
|
+
* palette, so the palette tints the fill and the edge. The subtle trigger takes the root's
|
|
322
|
+
* radius, so its hover fill follows the root's corners, and its end corners are square while
|
|
323
|
+
* the content is open under it. Under forced colors the subtle box, which the browser's fill
|
|
324
|
+
* replaces, takes a `CanvasText` hairline outline.
|
|
325
|
+
*/
|
|
326
|
+
variant: {
|
|
327
|
+
subtle: {
|
|
328
|
+
root: {
|
|
329
|
+
_highContrast: {
|
|
330
|
+
outlineColor: "CanvasText",
|
|
331
|
+
outlineOffset: "calc({borderWidths.hairline} * -1)",
|
|
332
|
+
outlineStyle: "solid",
|
|
333
|
+
outlineWidth: "hairline"
|
|
334
|
+
},
|
|
335
|
+
background: "colorPalette.subtle",
|
|
336
|
+
borderRadius: "l2"
|
|
337
|
+
},
|
|
338
|
+
trigger: {
|
|
339
|
+
_hover: { background: "colorPalette.muted" },
|
|
340
|
+
_open: {
|
|
341
|
+
borderEndEndRadius: "0",
|
|
342
|
+
borderEndStartRadius: "0"
|
|
343
|
+
},
|
|
344
|
+
borderRadius: "l2"
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
surface: {
|
|
348
|
+
root: surface(),
|
|
349
|
+
trigger: { _open: {
|
|
350
|
+
borderBlockEndColor: "colorPalette.muted",
|
|
351
|
+
borderBlockEndWidth: "hairline"
|
|
352
|
+
} }
|
|
353
|
+
},
|
|
354
|
+
outline: {
|
|
355
|
+
root: {
|
|
356
|
+
borderColor: "colorPalette.muted",
|
|
357
|
+
borderRadius: "l2",
|
|
358
|
+
borderWidth: "hairline"
|
|
359
|
+
},
|
|
360
|
+
trigger: { _open: {
|
|
361
|
+
borderBlockEndColor: "colorPalette.muted",
|
|
362
|
+
borderBlockEndWidth: "hairline"
|
|
363
|
+
} }
|
|
364
|
+
},
|
|
365
|
+
plain: { root: { borderWidth: "0" } }
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
//#endregion
|
|
370
|
+
//#region src/details/recipe.ts
|
|
371
|
+
/**
|
|
372
|
+
* Recipe for the details: a native `details` element whose summary shows and hides the content
|
|
373
|
+
* under it, and an indicator that turns a quarter as it opens.
|
|
374
|
+
*
|
|
375
|
+
* @remarks
|
|
376
|
+
* The browser opens and closes the element, so every open style keys on the root's `open`
|
|
377
|
+
* attribute. The summary drops the browser's marker and is a control's height for one line, and
|
|
378
|
+
* a summary that wraps grows. The indicator goes before the words and turns a quarter
|
|
379
|
+
* clockwise, and a quarter the other way under `dir="rtl"`, for a chevron that points to the
|
|
380
|
+
* end. The looks and the palette are the collapsible's, so a details beside a collapsible looks
|
|
381
|
+
* the same.
|
|
382
|
+
*/
|
|
383
|
+
/**
|
|
384
|
+
* Selects the summary of an open details.
|
|
385
|
+
*/
|
|
386
|
+
const OPENED = ".details__root[open] > &";
|
|
387
|
+
/**
|
|
388
|
+
* Defines the details recipe: an outlined details at size `md` in the neutral palette by default.
|
|
389
|
+
*/
|
|
390
|
+
const recipe$9 = defineSlotRecipe({
|
|
391
|
+
base: {
|
|
392
|
+
indicator: {
|
|
393
|
+
_motionReduce: { transitionDuration: "none" },
|
|
394
|
+
"& svg": { boxSize: "full" },
|
|
395
|
+
alignItems: "center",
|
|
396
|
+
display: "inline-flex",
|
|
397
|
+
flexShrink: "0",
|
|
398
|
+
justifyContent: "center",
|
|
399
|
+
[".details__root[open] > .details__summary > &"]: {
|
|
400
|
+
_rtl: { rotate: "-90deg" },
|
|
401
|
+
rotate: "90deg"
|
|
402
|
+
},
|
|
403
|
+
transitionDuration: "press",
|
|
404
|
+
transitionProperty: "rotate",
|
|
405
|
+
transitionTimingFunction: "press"
|
|
406
|
+
},
|
|
407
|
+
root: { width: "full" },
|
|
408
|
+
summary: {
|
|
409
|
+
...interactive(),
|
|
410
|
+
"&::-webkit-details-marker": { display: "none" },
|
|
411
|
+
alignItems: "center",
|
|
412
|
+
display: "flex",
|
|
413
|
+
fontWeight: "medium",
|
|
414
|
+
listStyle: "none",
|
|
415
|
+
textAlign: "start"
|
|
416
|
+
}
|
|
417
|
+
},
|
|
418
|
+
className: "details",
|
|
419
|
+
defaultVariants: {
|
|
420
|
+
palette: "neutral",
|
|
421
|
+
size: "md",
|
|
422
|
+
variant: "outline"
|
|
423
|
+
},
|
|
424
|
+
jsx: [/^Details(\.\w+)?$/u],
|
|
425
|
+
slots: [
|
|
426
|
+
"root",
|
|
427
|
+
"summary",
|
|
428
|
+
"indicator",
|
|
429
|
+
"content"
|
|
430
|
+
],
|
|
431
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
432
|
+
variants: {
|
|
433
|
+
/**
|
|
434
|
+
* Palette the looks read, set on the root.
|
|
435
|
+
*/
|
|
436
|
+
palette: onSlot("root", paletteVariants()),
|
|
437
|
+
/**
|
|
438
|
+
* Size of the summary, the indicator on the icon scale and the content's padding on the inset
|
|
439
|
+
* scale.
|
|
440
|
+
*
|
|
441
|
+
* @remarks
|
|
442
|
+
* The summary pads its ends by half of a control's height less its tallest line, one line of
|
|
443
|
+
* text or the indicator, and is at least a control's height. A one-line summary is then a
|
|
444
|
+
* collapsible trigger's height at the same size, and a longer one grows by its lines. It pads
|
|
445
|
+
* its sides on the inset scale.
|
|
446
|
+
*/
|
|
447
|
+
size: onSlots({
|
|
448
|
+
content: insetSizes(),
|
|
449
|
+
indicator: iconSizes(),
|
|
450
|
+
summary: sizeVariants((size) => ({
|
|
451
|
+
textStyle: `label.${size}`,
|
|
452
|
+
gap: dense(`{spacing.gap.${size}}`),
|
|
453
|
+
minBlockSize: dense(`{sizes.control.${size}}`),
|
|
454
|
+
paddingBlock: `calc((${dense(`{sizes.control.${size}}`)} - max(1lh, ${dense(`{sizes.icon.${size}}`)})) / 2)`,
|
|
455
|
+
paddingInline: dense(`{spacing.inset.${size}}`)
|
|
456
|
+
}))
|
|
457
|
+
}),
|
|
458
|
+
/**
|
|
459
|
+
* Look of the box around the summary and the content.
|
|
460
|
+
*
|
|
461
|
+
* @remarks
|
|
462
|
+
* The looks are the collapsible's. The subtle summary takes the root's radius, so its hover
|
|
463
|
+
* fill follows the root's corners, and its end corners are square while the content is open
|
|
464
|
+
* under it. Under forced colors the subtle box, which the browser's fill replaces, takes a
|
|
465
|
+
* `CanvasText` hairline outline. The plain look draws no box and pads no side, so its summary
|
|
466
|
+
* and content line up with the text around them.
|
|
467
|
+
*/
|
|
468
|
+
variant: {
|
|
469
|
+
subtle: {
|
|
470
|
+
root: {
|
|
471
|
+
_highContrast: {
|
|
472
|
+
outlineColor: "CanvasText",
|
|
473
|
+
outlineOffset: "calc({borderWidths.hairline} * -1)",
|
|
474
|
+
outlineStyle: "solid",
|
|
475
|
+
outlineWidth: "hairline"
|
|
476
|
+
},
|
|
477
|
+
background: "colorPalette.subtle",
|
|
478
|
+
borderRadius: "l2"
|
|
479
|
+
},
|
|
480
|
+
summary: {
|
|
481
|
+
_hover: { background: "colorPalette.muted" },
|
|
482
|
+
borderRadius: "l2",
|
|
483
|
+
[OPENED]: {
|
|
484
|
+
borderEndEndRadius: "0",
|
|
485
|
+
borderEndStartRadius: "0"
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
},
|
|
489
|
+
surface: {
|
|
490
|
+
root: surface(),
|
|
491
|
+
summary: { [OPENED]: {
|
|
492
|
+
borderBlockEndColor: "colorPalette.muted",
|
|
493
|
+
borderBlockEndWidth: "hairline"
|
|
494
|
+
} }
|
|
495
|
+
},
|
|
496
|
+
outline: {
|
|
497
|
+
root: {
|
|
498
|
+
borderColor: "colorPalette.muted",
|
|
499
|
+
borderRadius: "l2",
|
|
500
|
+
borderWidth: "hairline"
|
|
501
|
+
},
|
|
502
|
+
summary: { [OPENED]: {
|
|
503
|
+
borderBlockEndColor: "colorPalette.muted",
|
|
504
|
+
borderBlockEndWidth: "hairline"
|
|
505
|
+
} }
|
|
506
|
+
},
|
|
507
|
+
plain: {
|
|
508
|
+
content: { paddingInline: "0" },
|
|
509
|
+
root: { borderWidth: "0" },
|
|
510
|
+
summary: { paddingInline: "0" }
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
});
|
|
515
|
+
//#endregion
|
|
516
|
+
//#region src/hover-card/recipe.ts
|
|
517
|
+
/**
|
|
518
|
+
* Recipe for the hover card: a panel beside a link that previews what the link leads to, with an
|
|
519
|
+
* arrow.
|
|
520
|
+
*
|
|
521
|
+
* @remarks
|
|
522
|
+
* The machine has no root part. The recipe adds a root with `display: contents`, because the
|
|
523
|
+
* trigger and the positioner are siblings and a slot recipe passes its variants from an element
|
|
524
|
+
* above both. The machine writes the positioner's position inline, so the recipe sets no
|
|
525
|
+
* position. The trigger reads `link()` and is underlined at rest, because the link ink alone
|
|
526
|
+
* measures 1.64:1 against body text in light mode and 1.38:1 in dark mode on the ink theme, under
|
|
527
|
+
* the 3:1 that WCAG technique G183 asks for. The panel slides in from the trigger's side, which
|
|
528
|
+
* it reads from `data-placement`. It is at most `sizes.xs` wide, or as wide as the room the
|
|
529
|
+
* machine measures on its side of the trigger, `--available-width`, where that is less. The
|
|
530
|
+
* recipe has no `palette` axis, because every look uses a neutral surface, and no `effect` axis,
|
|
531
|
+
* because a panel is not a control.
|
|
532
|
+
*/
|
|
533
|
+
/**
|
|
534
|
+
* Steps of the size axis.
|
|
535
|
+
*/
|
|
536
|
+
const SIZES$3 = [
|
|
537
|
+
"xs",
|
|
538
|
+
"sm",
|
|
539
|
+
"md",
|
|
540
|
+
"lg"
|
|
541
|
+
];
|
|
542
|
+
/**
|
|
543
|
+
* Defines the hover card recipe: the surface look at size `md` by default.
|
|
544
|
+
*/
|
|
545
|
+
const recipe$8 = defineSlotRecipe({
|
|
546
|
+
base: {
|
|
547
|
+
arrow: {
|
|
548
|
+
"--arrow-background": "var(--hover-card-surface)",
|
|
549
|
+
"--arrow-size": "sizes.icon.sm"
|
|
550
|
+
},
|
|
551
|
+
arrowTip: {
|
|
552
|
+
borderInlineStartWidth: "hairline",
|
|
553
|
+
borderTopWidth: "hairline"
|
|
554
|
+
},
|
|
555
|
+
content: {
|
|
556
|
+
...motion("slide-fade.in", "slide-fade.out"),
|
|
557
|
+
display: "flex",
|
|
558
|
+
flexDirection: "column",
|
|
559
|
+
maxInlineSize: "min({sizes.xs}, var(--available-width, {sizes.xs}))",
|
|
560
|
+
position: "relative",
|
|
561
|
+
zIndex: "popover"
|
|
562
|
+
},
|
|
563
|
+
positioner: { position: "relative" },
|
|
564
|
+
root: { display: "contents" },
|
|
565
|
+
trigger: {
|
|
566
|
+
...link(),
|
|
567
|
+
borderRadius: "l1",
|
|
568
|
+
textDecoration: "underline"
|
|
569
|
+
}
|
|
570
|
+
},
|
|
571
|
+
className: "hover-card",
|
|
572
|
+
defaultVariants: {
|
|
573
|
+
size: "md",
|
|
574
|
+
variant: "surface"
|
|
575
|
+
},
|
|
576
|
+
jsx: [/^HoverCard(\.\w+)?$/u],
|
|
577
|
+
slots: [
|
|
578
|
+
"root",
|
|
579
|
+
"trigger",
|
|
580
|
+
"positioner",
|
|
581
|
+
"content",
|
|
582
|
+
"arrow",
|
|
583
|
+
"arrowTip"
|
|
584
|
+
],
|
|
585
|
+
variants: {
|
|
586
|
+
/**
|
|
587
|
+
* Padding of the panel on the inset scale and its text on the body role.
|
|
588
|
+
*/
|
|
589
|
+
size: onSlot("content", sizeVariants((size) => ({
|
|
590
|
+
textStyle: `body.${size}`,
|
|
591
|
+
padding: dense(`{spacing.inset.${size}}`)
|
|
592
|
+
}), SIZES$3)),
|
|
593
|
+
/**
|
|
594
|
+
* Surface of the panel: the popover surface inside a hairline edge, the panel surface with a
|
|
595
|
+
* large shadow, or the glass layer style.
|
|
596
|
+
*
|
|
597
|
+
* @remarks
|
|
598
|
+
* The elevated panel has a transparent hairline edge, which forced colors paint in
|
|
599
|
+
* `CanvasText`, so the panel keeps its outline where the shadow is not drawn.
|
|
600
|
+
*/
|
|
601
|
+
variant: {
|
|
602
|
+
elevated: {
|
|
603
|
+
arrowTip: { borderColor: "var(--hover-card-surface)" },
|
|
604
|
+
content: {
|
|
605
|
+
"--hover-card-surface": "colors.bg.panel",
|
|
606
|
+
background: "var(--hover-card-surface)",
|
|
607
|
+
borderColor: "transparent",
|
|
608
|
+
borderRadius: "l3",
|
|
609
|
+
borderStyle: "solid",
|
|
610
|
+
borderWidth: "hairline",
|
|
611
|
+
boxShadow: "xl"
|
|
612
|
+
}
|
|
613
|
+
},
|
|
614
|
+
glass: {
|
|
615
|
+
arrowTip: { borderColor: "border" },
|
|
616
|
+
content: {
|
|
617
|
+
layerStyle: "glass",
|
|
618
|
+
"--hover-card-surface": "colors.bg.popover",
|
|
619
|
+
borderRadius: "l3"
|
|
620
|
+
}
|
|
621
|
+
},
|
|
622
|
+
surface: {
|
|
623
|
+
arrowTip: { borderColor: "border" },
|
|
624
|
+
content: {
|
|
625
|
+
"--hover-card-surface": "colors.bg.popover",
|
|
626
|
+
background: "var(--hover-card-surface)",
|
|
627
|
+
borderColor: "border",
|
|
628
|
+
borderRadius: "l3",
|
|
629
|
+
borderWidth: "hairline",
|
|
630
|
+
boxShadow: "lg"
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
});
|
|
636
|
+
//#endregion
|
|
637
|
+
//#region src/menu/recipe.ts
|
|
638
|
+
/**
|
|
639
|
+
* Recipe for the menu: a trigger, a panel of rows and groups, and submenus opened from a row.
|
|
640
|
+
*
|
|
641
|
+
* @remarks
|
|
642
|
+
* The machine has no root part. The recipe adds a root with `display: contents`, because the
|
|
643
|
+
* trigger and the positioner are siblings and a slot recipe passes its variants from an element
|
|
644
|
+
* above both. A submenu's root is a child of the parent menu's panel. The machine writes the
|
|
645
|
+
* positioner's position inline and `--available-height`, which caps the panel, so a menu near the
|
|
646
|
+
* window's edge scrolls its rows in the primitives package's scroll area inside the panel. The
|
|
647
|
+
* `viewport` and `rows` slots are that scroll area's viewport, the element with the menu role,
|
|
648
|
+
* and its content: the rows take the panel's padding, and the viewport's scroll padding keeps a
|
|
649
|
+
* revealed row that padding from the edge. The panel is at least `sizes.44` wide and grows to its
|
|
650
|
+
* widest row. The panel takes the `popover` z-index plus its depth in the nest, because the
|
|
651
|
+
* machine writes `z-index: var(--z-index)` inline on the positioner from the panel's value. The
|
|
652
|
+
* `popover` level is above a page's sticky bands and the app shell's bars, so a menu opened from
|
|
653
|
+
* a bar is above the bands under it. Neither the panel nor its scroll area renders a focus ring,
|
|
654
|
+
* because the machine focuses the menu on opening and the highlight marks the row. A row reads
|
|
655
|
+
* the body role one size smaller, truncates, and sizes a leading icon to the icon size one size
|
|
656
|
+
* smaller. A row's mark renders its initial or glyph without a fill, in the muted ink. `inset`
|
|
657
|
+
* pads a row without a leading icon or mark by the gutter a leading icon takes, so its text
|
|
658
|
+
* starts where an icon row's text starts. The indicator mirrors in a right-to-left menu, so a
|
|
659
|
+
* submenu row's chevron points to the side the submenu opens on. The panel sets the palette, and
|
|
660
|
+
* the rows and the highlight inherit it. A critical row reads `error`. The recipe has no `effect`
|
|
661
|
+
* axis, because the highlight moves with the pointer and the arrow keys, and a glow would move
|
|
662
|
+
* with it.
|
|
663
|
+
*/
|
|
664
|
+
/**
|
|
665
|
+
* Sizes the menu offers.
|
|
666
|
+
*/
|
|
667
|
+
const SIZES$2 = [
|
|
668
|
+
"sm",
|
|
669
|
+
"md",
|
|
670
|
+
"lg"
|
|
671
|
+
];
|
|
672
|
+
/**
|
|
673
|
+
* The recipe's class name, which the selectors across parts are built from.
|
|
674
|
+
*
|
|
675
|
+
* @remarks
|
|
676
|
+
* The binding writes one class per part and no part attribute. A selector names a part by the
|
|
677
|
+
* compiler's class, such as `menu__itemMark`, and the naming step renames it to the class the
|
|
678
|
+
* element carries, `menu__item-mark`.
|
|
679
|
+
*/
|
|
680
|
+
const CLASS$2 = "menu";
|
|
681
|
+
/**
|
|
682
|
+
* Custom property a panel sets to its depth in the nest, which the panel adds to its z-index.
|
|
683
|
+
*
|
|
684
|
+
* @remarks
|
|
685
|
+
* When a caller portals a menu and its submenu to the document, neither is an ancestor of the
|
|
686
|
+
* other. Without the depth, mount order decides the stacking, and a submenu renders under its
|
|
687
|
+
* parent.
|
|
688
|
+
*/
|
|
689
|
+
const MENU_DEPTH = "--menu-depth";
|
|
690
|
+
/**
|
|
691
|
+
* Attribute a panel sets when it opened from a row of another menu.
|
|
692
|
+
*
|
|
693
|
+
* @remarks
|
|
694
|
+
* A selector cannot compare a custom property's number, so the attribute marks a nested panel.
|
|
695
|
+
*/
|
|
696
|
+
const NESTED = "data-nested";
|
|
697
|
+
/**
|
|
698
|
+
* Custom property the size axis sets to the gutter a leading icon takes, which `inset` reads.
|
|
699
|
+
*/
|
|
700
|
+
const GUTTER$1 = "--menu-gutter";
|
|
701
|
+
/**
|
|
702
|
+
* Custom property the scroll area's root reads for the style of its focus ring.
|
|
703
|
+
*/
|
|
704
|
+
const RING_STYLE = "--scroll-area-ring-style";
|
|
705
|
+
/**
|
|
706
|
+
* Selects a row whose first element is neither an icon nor a mark.
|
|
707
|
+
*
|
|
708
|
+
* @remarks
|
|
709
|
+
* The selector matches element children only, so the text before an icon does not count.
|
|
710
|
+
*/
|
|
711
|
+
const UNMARKED = `&:not(:has(> svg:first-child, > .${CLASS$2}__itemMark:first-child))`;
|
|
712
|
+
/**
|
|
713
|
+
* Custom property each look sets to the panel's fill, which the arrow reads.
|
|
714
|
+
*/
|
|
715
|
+
const SURFACE = "var(--menu-surface)";
|
|
716
|
+
/**
|
|
717
|
+
* Returns the side of a row's leading icon at a size: the icon size one size smaller.
|
|
718
|
+
*/
|
|
719
|
+
function icon(size) {
|
|
720
|
+
return dense(`{sizes.icon.${below(size)}}`);
|
|
721
|
+
}
|
|
722
|
+
/**
|
|
723
|
+
* Returns the gutter a leading icon takes at a size: the row's inline padding, the icon and the
|
|
724
|
+
* gap, each multiplied by the density as the row multiplies them.
|
|
725
|
+
*/
|
|
726
|
+
function gutter(size) {
|
|
727
|
+
return `calc(${dense(`{spacing.inset.${below(size)}}`)} + ${icon(size)} + ${dense(`{spacing.gap.${size}}`)})`;
|
|
728
|
+
}
|
|
729
|
+
/**
|
|
730
|
+
* Returns the margin that moves a submenu off its parent's panel, on the side it opened towards.
|
|
731
|
+
*
|
|
732
|
+
* @remarks
|
|
733
|
+
* The machine places a submenu against the row that opened it, and the row stops one padding
|
|
734
|
+
* short of the panel's edge, so the margin moves the submenu off its parent by that padding. The
|
|
735
|
+
* machine reports the side as left or right, and the margin is logical, so each side sets both
|
|
736
|
+
* inline margins and swaps them under `_rtl`.
|
|
737
|
+
* @param room - The rows' padding at the size.
|
|
738
|
+
*/
|
|
739
|
+
function cleared(room) {
|
|
740
|
+
return {
|
|
741
|
+
[`&[${NESTED}][data-placement^=left]`]: {
|
|
742
|
+
_rtl: {
|
|
743
|
+
marginInlineEnd: "0",
|
|
744
|
+
marginInlineStart: room
|
|
745
|
+
},
|
|
746
|
+
marginInlineEnd: room,
|
|
747
|
+
marginInlineStart: "0"
|
|
748
|
+
},
|
|
749
|
+
[`&[${NESTED}][data-placement^=right]`]: {
|
|
750
|
+
_rtl: {
|
|
751
|
+
marginInlineEnd: room,
|
|
752
|
+
marginInlineStart: "0"
|
|
753
|
+
},
|
|
754
|
+
marginInlineEnd: "0",
|
|
755
|
+
marginInlineStart: room
|
|
756
|
+
}
|
|
757
|
+
};
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Returns a row's styles at a size: its padding, the leading icon, the gap after a mark, the gutter
|
|
761
|
+
* and the body role one size smaller.
|
|
762
|
+
*/
|
|
763
|
+
function rowOf(size) {
|
|
764
|
+
return {
|
|
765
|
+
textStyle: `body.${below(size)}`,
|
|
766
|
+
"& > svg": {
|
|
767
|
+
boxSize: icon(size),
|
|
768
|
+
flexShrink: "0"
|
|
769
|
+
},
|
|
770
|
+
gap: dense(`{spacing.gap.${size}}`),
|
|
771
|
+
[GUTTER$1]: gutter(size),
|
|
772
|
+
paddingBlock: dense(`{spacing.gap.${below(size)}}`),
|
|
773
|
+
paddingInline: dense(`{spacing.inset.${below(size)}}`)
|
|
774
|
+
};
|
|
775
|
+
}
|
|
776
|
+
/**
|
|
777
|
+
* Defines the menu recipe: the surface look at size `md` in the neutral palette, with the tint
|
|
778
|
+
* highlight, by default.
|
|
779
|
+
*/
|
|
780
|
+
const recipe$7 = defineSlotRecipe({
|
|
781
|
+
base: {
|
|
782
|
+
arrow: {
|
|
783
|
+
"--arrow-background": SURFACE,
|
|
784
|
+
"--arrow-size": "sizes.icon.sm"
|
|
785
|
+
},
|
|
786
|
+
arrowTip: {
|
|
787
|
+
borderInlineStartWidth: "hairline",
|
|
788
|
+
borderTopWidth: "hairline"
|
|
789
|
+
},
|
|
790
|
+
content: {
|
|
791
|
+
...motion("slide-fade.in", "slide-fade.out"),
|
|
792
|
+
display: "flex",
|
|
793
|
+
flexDirection: "column",
|
|
794
|
+
maxBlockSize: "var(--available-height)",
|
|
795
|
+
minInlineSize: "44",
|
|
796
|
+
outline: "0",
|
|
797
|
+
[RING_STYLE]: "none",
|
|
798
|
+
zIndex: `calc({zIndex.popover} + var(${MENU_DEPTH}, 0))`
|
|
799
|
+
},
|
|
800
|
+
contextTrigger: { cursor: "menuitem" },
|
|
801
|
+
indicator: {
|
|
802
|
+
_rtl: { scale: "-1 1" },
|
|
803
|
+
"& > svg": { boxSize: "100%" },
|
|
804
|
+
alignItems: "center",
|
|
805
|
+
display: "inline-flex",
|
|
806
|
+
flexShrink: "0",
|
|
807
|
+
justifyContent: "center",
|
|
808
|
+
marginInlineStart: "auto"
|
|
809
|
+
},
|
|
810
|
+
item: {
|
|
811
|
+
...row(),
|
|
812
|
+
"&[data-tone=critical]": {
|
|
813
|
+
color: "colorPalette.fg",
|
|
814
|
+
colorPalette: "error"
|
|
815
|
+
},
|
|
816
|
+
borderRadius: "l1"
|
|
817
|
+
},
|
|
818
|
+
itemCommand: {
|
|
819
|
+
color: "fg.muted",
|
|
820
|
+
flexShrink: "0",
|
|
821
|
+
fontFamily: "inherit",
|
|
822
|
+
letterSpacing: "wide",
|
|
823
|
+
marginInlineStart: "auto"
|
|
824
|
+
},
|
|
825
|
+
itemDescription: {
|
|
826
|
+
...truncate(),
|
|
827
|
+
textStyle: "caption",
|
|
828
|
+
color: "fg.subtle",
|
|
829
|
+
display: "block"
|
|
830
|
+
},
|
|
831
|
+
itemGroup: {
|
|
832
|
+
display: "flex",
|
|
833
|
+
flexDirection: "column"
|
|
834
|
+
},
|
|
835
|
+
itemGroupLabel: {
|
|
836
|
+
color: "fg.subtle",
|
|
837
|
+
fontWeight: "medium"
|
|
838
|
+
},
|
|
839
|
+
itemIndicator: {
|
|
840
|
+
"&[data-state=checked]": { visibility: "visible" },
|
|
841
|
+
alignItems: "center",
|
|
842
|
+
boxSizing: "content-box",
|
|
843
|
+
display: "inline-flex",
|
|
844
|
+
flexShrink: "0",
|
|
845
|
+
justifyContent: "center",
|
|
846
|
+
marginInlineStart: "auto",
|
|
847
|
+
order: "1",
|
|
848
|
+
visibility: "hidden"
|
|
849
|
+
},
|
|
850
|
+
itemLines: {
|
|
851
|
+
display: "flex",
|
|
852
|
+
flex: "1",
|
|
853
|
+
flexDirection: "column",
|
|
854
|
+
minInlineSize: "0"
|
|
855
|
+
},
|
|
856
|
+
itemMark: {
|
|
857
|
+
alignItems: "center",
|
|
858
|
+
color: "fg.muted",
|
|
859
|
+
display: "inline-flex",
|
|
860
|
+
flexShrink: "0",
|
|
861
|
+
fontWeight: "semibold",
|
|
862
|
+
justifyContent: "center",
|
|
863
|
+
lineHeight: "tight",
|
|
864
|
+
overflow: "clip"
|
|
865
|
+
},
|
|
866
|
+
itemText: {
|
|
867
|
+
...truncate(),
|
|
868
|
+
flex: "1"
|
|
869
|
+
},
|
|
870
|
+
positioner: { position: "relative" },
|
|
871
|
+
root: { display: "contents" },
|
|
872
|
+
rows: {
|
|
873
|
+
display: "flex",
|
|
874
|
+
flexDirection: "column"
|
|
875
|
+
},
|
|
876
|
+
separator: {
|
|
877
|
+
...divider(),
|
|
878
|
+
borderColor: "border.muted",
|
|
879
|
+
inlineSize: "auto"
|
|
880
|
+
},
|
|
881
|
+
trigger: { ...interactive() },
|
|
882
|
+
triggerItem: {
|
|
883
|
+
...row(),
|
|
884
|
+
borderRadius: "l1"
|
|
885
|
+
},
|
|
886
|
+
viewport: { overscrollBehavior: "contain" }
|
|
887
|
+
},
|
|
888
|
+
className: CLASS$2,
|
|
889
|
+
defaultVariants: {
|
|
890
|
+
highlight: "tint",
|
|
891
|
+
palette: "neutral",
|
|
892
|
+
size: "md",
|
|
893
|
+
variant: "surface"
|
|
894
|
+
},
|
|
895
|
+
jsx: [/^Menu(\.\w+)?$/u],
|
|
896
|
+
slots: [
|
|
897
|
+
"root",
|
|
898
|
+
"trigger",
|
|
899
|
+
"contextTrigger",
|
|
900
|
+
"triggerItem",
|
|
901
|
+
"indicator",
|
|
902
|
+
"positioner",
|
|
903
|
+
"content",
|
|
904
|
+
"viewport",
|
|
905
|
+
"rows",
|
|
906
|
+
"itemGroup",
|
|
907
|
+
"itemGroupLabel",
|
|
908
|
+
"item",
|
|
909
|
+
"itemMark",
|
|
910
|
+
"itemLines",
|
|
911
|
+
"itemText",
|
|
912
|
+
"itemDescription",
|
|
913
|
+
"itemIndicator",
|
|
914
|
+
"itemCommand",
|
|
915
|
+
"separator",
|
|
916
|
+
"arrow",
|
|
917
|
+
"arrowTip"
|
|
918
|
+
],
|
|
919
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
920
|
+
variants: {
|
|
921
|
+
/**
|
|
922
|
+
* Look of the highlighted row.
|
|
923
|
+
*/
|
|
924
|
+
highlight: onSlots({
|
|
925
|
+
item: highlightVariants(),
|
|
926
|
+
triggerItem: highlightVariants()
|
|
927
|
+
}),
|
|
928
|
+
/**
|
|
929
|
+
* Whether a row without a leading icon or mark starts its text after the gutter an icon takes,
|
|
930
|
+
* for a menu where some rows lead with an icon.
|
|
931
|
+
*/
|
|
932
|
+
inset: { true: {
|
|
933
|
+
item: { [UNMARKED]: { paddingInlineStart: `var(${GUTTER$1})` } },
|
|
934
|
+
triggerItem: { [UNMARKED]: { paddingInlineStart: `var(${GUTTER$1})` } }
|
|
935
|
+
} },
|
|
936
|
+
/**
|
|
937
|
+
* Palette of the rows and the highlight, set on the panel, which a portalled panel keeps.
|
|
938
|
+
*/
|
|
939
|
+
palette: onSlot("content", paletteVariants()),
|
|
940
|
+
/**
|
|
941
|
+
* Size of the panel's padding, the rows, the marks and the keys.
|
|
942
|
+
*/
|
|
943
|
+
size: onSlots({
|
|
944
|
+
/**
|
|
945
|
+
* The margin that moves a submenu off its parent's panel.
|
|
946
|
+
*/
|
|
947
|
+
content: sizeVariants((size) => cleared(dense(`{spacing.gap.${below(size)}}`)), SIZES$2),
|
|
948
|
+
/**
|
|
949
|
+
* The indicator at the icon size one size smaller, the same square as a checked row's mark.
|
|
950
|
+
*/
|
|
951
|
+
indicator: sizeVariants((size) => ({ boxSize: dense(`{sizes.icon.${below(size)}}`) }), SIZES$2),
|
|
952
|
+
item: sizeVariants((size) => rowOf(size), SIZES$2),
|
|
953
|
+
itemCommand: sizeVariants((size) => ({
|
|
954
|
+
textStyle: `body.${below(below(size))}`,
|
|
955
|
+
paddingInlineStart: dense(`{spacing.inset.${size}}`)
|
|
956
|
+
}), SIZES$2),
|
|
957
|
+
itemGroupLabel: sizeVariants((size) => ({
|
|
958
|
+
textStyle: `body.${below(below(size))}`,
|
|
959
|
+
paddingBlock: dense(`{spacing.gap.${below(below(size))}}`),
|
|
960
|
+
paddingInline: dense(`{spacing.inset.${below(size)}}`)
|
|
961
|
+
}), SIZES$2),
|
|
962
|
+
itemIndicator: sizeVariants((size) => ({
|
|
963
|
+
boxSize: dense(`{sizes.icon.${below(size)}}`),
|
|
964
|
+
paddingInlineStart: dense(`{spacing.gap.${size}}`)
|
|
965
|
+
}), SIZES$2),
|
|
966
|
+
itemMark: sizeVariants((size) => ({
|
|
967
|
+
boxSize: dense(`{sizes.tag.${size}}`),
|
|
968
|
+
fontSize: below(below(size))
|
|
969
|
+
}), SIZES$2),
|
|
970
|
+
/**
|
|
971
|
+
* The rows' padding inside the scroll area, which the separators' negative margins cross.
|
|
972
|
+
*/
|
|
973
|
+
rows: sizeVariants((size) => ({ padding: dense(`{spacing.gap.${below(size)}}`) }), SIZES$2),
|
|
974
|
+
separator: sizeVariants((size) => ({
|
|
975
|
+
marginBlock: dense(`{spacing.gap.${below(size)}}`),
|
|
976
|
+
marginInline: `calc(-1 * ${dense(`{spacing.gap.${below(size)}}`)})`
|
|
977
|
+
}), SIZES$2),
|
|
978
|
+
triggerItem: sizeVariants((size) => rowOf(size), SIZES$2),
|
|
979
|
+
/**
|
|
980
|
+
* The scroll padding that keeps a revealed row the rows' padding from the edge.
|
|
981
|
+
*/
|
|
982
|
+
viewport: sizeVariants((size) => ({ scrollPadding: dense(`{spacing.gap.${below(size)}}`) }), SIZES$2)
|
|
983
|
+
}),
|
|
984
|
+
/**
|
|
985
|
+
* Surface of the panel: the popover surface inside a hairline edge, the panel surface with a
|
|
986
|
+
* large shadow, or the glass layer style.
|
|
987
|
+
*
|
|
988
|
+
* @remarks
|
|
989
|
+
* The elevated panel has a transparent hairline edge, which forced colors paint in
|
|
990
|
+
* `CanvasText`, so the panel keeps its outline where the shadow is not drawn.
|
|
991
|
+
*/
|
|
992
|
+
variant: {
|
|
993
|
+
elevated: {
|
|
994
|
+
arrowTip: { borderColor: SURFACE },
|
|
995
|
+
content: {
|
|
996
|
+
"--menu-surface": "colors.bg.panel",
|
|
997
|
+
background: SURFACE,
|
|
998
|
+
borderColor: "transparent",
|
|
999
|
+
borderRadius: "l2",
|
|
1000
|
+
borderStyle: "solid",
|
|
1001
|
+
borderWidth: "hairline",
|
|
1002
|
+
boxShadow: "lg"
|
|
1003
|
+
}
|
|
1004
|
+
},
|
|
1005
|
+
glass: {
|
|
1006
|
+
arrowTip: { borderColor: "border" },
|
|
1007
|
+
content: {
|
|
1008
|
+
layerStyle: "glass",
|
|
1009
|
+
"--menu-surface": "colors.bg.popover",
|
|
1010
|
+
borderRadius: "l2"
|
|
1011
|
+
}
|
|
1012
|
+
},
|
|
1013
|
+
surface: {
|
|
1014
|
+
arrowTip: { borderColor: "border" },
|
|
1015
|
+
content: {
|
|
1016
|
+
"--menu-surface": "colors.bg.popover",
|
|
1017
|
+
background: SURFACE,
|
|
1018
|
+
borderColor: "border",
|
|
1019
|
+
borderRadius: "l2",
|
|
1020
|
+
borderWidth: "hairline",
|
|
1021
|
+
boxShadow: "md"
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
//#endregion
|
|
1028
|
+
//#region src/menubar/recipe.ts
|
|
1029
|
+
/**
|
|
1030
|
+
* Styles the menubar's root, which measures its row, its bar of names, a name, and the trigger a
|
|
1031
|
+
* crowded bar folds into.
|
|
1032
|
+
*
|
|
1033
|
+
* @remarks
|
|
1034
|
+
* A name is drawn as a menu's row: no fill at rest, the palette's subtle role under the pointer,
|
|
1035
|
+
* and its muted role while its menu is open, the fill a highlighted row takes, with a row's
|
|
1036
|
+
* padding and text at the bar's size, so the names read as the rows they open. The root sets the
|
|
1037
|
+
* palette the names read, which the root passes to its menus as well. While the root marks its
|
|
1038
|
+
* row `data-crowded`, the bar is hidden and the fold trigger shows in its place. The panels are
|
|
1039
|
+
* the menu recipe's, so the bar states no panel. Under forced colors an open name fills with
|
|
1040
|
+
* `Highlight`, under the pointer too.
|
|
1041
|
+
*/
|
|
1042
|
+
/**
|
|
1043
|
+
* Class name of the recipe, from which the binding writes each part's class.
|
|
1044
|
+
*/
|
|
1045
|
+
const CLASS$1 = "menubar";
|
|
1046
|
+
/**
|
|
1047
|
+
* Sizes the bar offers, which its menus take too.
|
|
1048
|
+
*/
|
|
1049
|
+
const SIZES$1 = [
|
|
1050
|
+
"sm",
|
|
1051
|
+
"md",
|
|
1052
|
+
"lg"
|
|
1053
|
+
];
|
|
1054
|
+
/**
|
|
1055
|
+
* Selects the bar of a root that marks its row crowded.
|
|
1056
|
+
*/
|
|
1057
|
+
const CROWDED = "[data-crowded] > &";
|
|
1058
|
+
/**
|
|
1059
|
+
* Selects the fold trigger of a root that marks its row crowded.
|
|
1060
|
+
*
|
|
1061
|
+
* @remarks
|
|
1062
|
+
* The fold trigger is a child of its menu's root, a `div` with `display: contents`, so the
|
|
1063
|
+
* crowded root is its grandparent.
|
|
1064
|
+
*/
|
|
1065
|
+
const FOLDED = "[data-crowded] > * > &";
|
|
1066
|
+
/**
|
|
1067
|
+
* Forced colors of an open name: the system highlight, which the browser keeps.
|
|
1068
|
+
*/
|
|
1069
|
+
const FORCED = { _highContrast: {
|
|
1070
|
+
background: "Highlight",
|
|
1071
|
+
color: "HighlightText",
|
|
1072
|
+
forcedColorAdjust: "none"
|
|
1073
|
+
} };
|
|
1074
|
+
/**
|
|
1075
|
+
* Returns the styles a name shares with the fold trigger, which draw it as a menu's row that fills
|
|
1076
|
+
* under the pointer and while its menu is open.
|
|
1077
|
+
*
|
|
1078
|
+
* @remarks
|
|
1079
|
+
* The compiler emits the hover rule after the forced-colors rule, so the open state's forced
|
|
1080
|
+
* colors are restated inside the hover rule, where the open selector raises their specificity.
|
|
1081
|
+
*/
|
|
1082
|
+
function named() {
|
|
1083
|
+
return {
|
|
1084
|
+
...interactive(),
|
|
1085
|
+
_hover: {
|
|
1086
|
+
_open: FORCED,
|
|
1087
|
+
background: "colorPalette.subtle"
|
|
1088
|
+
},
|
|
1089
|
+
_open: {
|
|
1090
|
+
...FORCED,
|
|
1091
|
+
background: "colorPalette.muted"
|
|
1092
|
+
},
|
|
1093
|
+
alignItems: "center",
|
|
1094
|
+
appearance: "none",
|
|
1095
|
+
background: "transparent",
|
|
1096
|
+
borderRadius: "l1",
|
|
1097
|
+
borderWidth: "0",
|
|
1098
|
+
color: "fg",
|
|
1099
|
+
display: "inline-flex",
|
|
1100
|
+
flexShrink: "0",
|
|
1101
|
+
fontWeight: "medium",
|
|
1102
|
+
whiteSpace: "nowrap"
|
|
1103
|
+
};
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Returns the padding, gap and text of a menu's row at a size, and a leading icon one size smaller
|
|
1107
|
+
* on the icon scale, which a name and the fold trigger take.
|
|
1108
|
+
*
|
|
1109
|
+
* @param size - The size of the bar.
|
|
1110
|
+
*/
|
|
1111
|
+
function sized(size) {
|
|
1112
|
+
return {
|
|
1113
|
+
textStyle: `body.${below(size)}`,
|
|
1114
|
+
"& > svg": {
|
|
1115
|
+
boxSize: dense(`{sizes.icon.${below(size)}}`),
|
|
1116
|
+
flexShrink: "0"
|
|
1117
|
+
},
|
|
1118
|
+
gap: dense(`{spacing.gap.${size}}`),
|
|
1119
|
+
paddingBlock: dense(`{spacing.gap.${below(size)}}`),
|
|
1120
|
+
paddingInline: dense(`{spacing.inset.${below(size)}}`)
|
|
1121
|
+
};
|
|
1122
|
+
}
|
|
1123
|
+
/**
|
|
1124
|
+
* Defines the menubar recipe, in the neutral palette at size `md` by default.
|
|
1125
|
+
*/
|
|
1126
|
+
const recipe$6 = defineSlotRecipe({
|
|
1127
|
+
base: {
|
|
1128
|
+
bar: {
|
|
1129
|
+
alignItems: "center",
|
|
1130
|
+
[CROWDED]: { display: "none" }
|
|
1131
|
+
},
|
|
1132
|
+
fold: {
|
|
1133
|
+
...named(),
|
|
1134
|
+
display: "none",
|
|
1135
|
+
[FOLDED]: { display: "inline-flex" }
|
|
1136
|
+
},
|
|
1137
|
+
root: {
|
|
1138
|
+
display: "flex",
|
|
1139
|
+
maxInlineSize: "full",
|
|
1140
|
+
minInlineSize: "0"
|
|
1141
|
+
},
|
|
1142
|
+
trigger: named()
|
|
1143
|
+
},
|
|
1144
|
+
className: CLASS$1,
|
|
1145
|
+
defaultVariants: {
|
|
1146
|
+
palette: "neutral",
|
|
1147
|
+
size: "md"
|
|
1148
|
+
},
|
|
1149
|
+
jsx: [/^Menubar(\.\w+)?$/u],
|
|
1150
|
+
slots: [
|
|
1151
|
+
"root",
|
|
1152
|
+
"bar",
|
|
1153
|
+
"trigger",
|
|
1154
|
+
"fold"
|
|
1155
|
+
],
|
|
1156
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
1157
|
+
variants: {
|
|
1158
|
+
/**
|
|
1159
|
+
* Palette the names fill in under the pointer and while their menu is open, set on the root.
|
|
1160
|
+
*/
|
|
1161
|
+
palette: onSlot("root", paletteVariants()),
|
|
1162
|
+
/**
|
|
1163
|
+
* Padding and text of the names and the fold trigger, and the gap between the names.
|
|
1164
|
+
*/
|
|
1165
|
+
size: onSlots({
|
|
1166
|
+
bar: sizeVariants((size) => ({ gap: dense(`{spacing.gap.${below(size)}}`) }), SIZES$1),
|
|
1167
|
+
fold: sizeVariants(sized, SIZES$1),
|
|
1168
|
+
trigger: sizeVariants(sized, SIZES$1)
|
|
1169
|
+
})
|
|
1170
|
+
}
|
|
1171
|
+
});
|
|
1172
|
+
//#endregion
|
|
1173
|
+
//#region src/popover/recipe.ts
|
|
1174
|
+
/**
|
|
1175
|
+
* Recipe for the popover: a panel beside its trigger, with a heading, a description, a close
|
|
1176
|
+
* button and an arrow.
|
|
1177
|
+
*
|
|
1178
|
+
* @remarks
|
|
1179
|
+
* The machine has no root part. The recipe adds a root with `display: contents`, because the
|
|
1180
|
+
* trigger and the positioner are siblings and a slot recipe passes its variants from an element
|
|
1181
|
+
* above both. The machine writes the positioner's position inline, so the recipe sets no
|
|
1182
|
+
* position. The panel scales from the machine's `--transform-origin` and is at least as wide as
|
|
1183
|
+
* the trigger, `--reference-width`. The title and the description read the body role at the
|
|
1184
|
+
* panel's size, the title in the semibold weight, because a panel beside a control does not
|
|
1185
|
+
* start a section of the page. The weight is a compound over every size, because the size's
|
|
1186
|
+
* text style states the role's weight in the variants layer, which applies over the base.
|
|
1187
|
+
* The trigger takes a control's cursor, focus ring and disabled look, and no fill, edge or
|
|
1188
|
+
* padding, so a caller passes a button through `as`. The recipe has no `palette` axis, because
|
|
1189
|
+
* every look uses a neutral surface, and no `effect` axis, because a panel is not a control.
|
|
1190
|
+
*/
|
|
1191
|
+
/**
|
|
1192
|
+
* Custom property the size axis sets to the room the title leaves for the close trigger.
|
|
1193
|
+
*
|
|
1194
|
+
* @remarks
|
|
1195
|
+
* The close trigger is positioned absolutely, so a long title ran under it. The size axis sets
|
|
1196
|
+
* the room once, and the title reads it as its inline-end padding.
|
|
1197
|
+
*/
|
|
1198
|
+
const CLOSED = "--popover-closed";
|
|
1199
|
+
/**
|
|
1200
|
+
* Defines the popover recipe: the surface look at size `md` by default.
|
|
1201
|
+
*/
|
|
1202
|
+
const recipe$5 = defineSlotRecipe({
|
|
1203
|
+
base: {
|
|
1204
|
+
arrow: {
|
|
1205
|
+
"--arrow-background": "var(--popover-surface)",
|
|
1206
|
+
"--arrow-size": "sizes.icon.sm"
|
|
1207
|
+
},
|
|
1208
|
+
arrowTip: {
|
|
1209
|
+
borderInlineStartWidth: "hairline",
|
|
1210
|
+
borderTopWidth: "hairline"
|
|
1211
|
+
},
|
|
1212
|
+
closeTrigger: {
|
|
1213
|
+
...interactive(),
|
|
1214
|
+
_hover: { color: "fg" },
|
|
1215
|
+
alignItems: "center",
|
|
1216
|
+
borderRadius: "l1",
|
|
1217
|
+
color: "fg.muted",
|
|
1218
|
+
display: "inline-flex",
|
|
1219
|
+
insetBlockStart: "0",
|
|
1220
|
+
insetInlineEnd: "0",
|
|
1221
|
+
justifyContent: "center",
|
|
1222
|
+
position: "absolute"
|
|
1223
|
+
},
|
|
1224
|
+
content: {
|
|
1225
|
+
...motion("scale-fade.in", "scale-fade.out"),
|
|
1226
|
+
_focusVisible: { focusVisibleRing: "outside" },
|
|
1227
|
+
display: "flex",
|
|
1228
|
+
flexDirection: "column",
|
|
1229
|
+
gap: dense("{spacing.gap.sm}"),
|
|
1230
|
+
minInlineSize: "var(--reference-width)",
|
|
1231
|
+
position: "relative",
|
|
1232
|
+
transformOrigin: "var(--transform-origin)",
|
|
1233
|
+
zIndex: "popover"
|
|
1234
|
+
},
|
|
1235
|
+
description: { color: "fg.muted" },
|
|
1236
|
+
indicator: {
|
|
1237
|
+
_motionReduce: { transitionDuration: "0s" },
|
|
1238
|
+
_open: { rotate: "180deg" },
|
|
1239
|
+
transitionDuration: "press"
|
|
1240
|
+
},
|
|
1241
|
+
positioner: { position: "relative" },
|
|
1242
|
+
root: { display: "contents" },
|
|
1243
|
+
title: { paddingInlineEnd: `var(${CLOSED})` },
|
|
1244
|
+
trigger: {
|
|
1245
|
+
...interactive(),
|
|
1246
|
+
alignItems: "center",
|
|
1247
|
+
display: "inline-flex",
|
|
1248
|
+
gap: dense("{spacing.gap.xs}")
|
|
1249
|
+
}
|
|
1250
|
+
},
|
|
1251
|
+
className: "popover",
|
|
1252
|
+
compoundVariants: [{
|
|
1253
|
+
css: { title: { fontWeight: "semibold" } },
|
|
1254
|
+
name: "titled",
|
|
1255
|
+
size: [
|
|
1256
|
+
"xs",
|
|
1257
|
+
"sm",
|
|
1258
|
+
"md",
|
|
1259
|
+
"lg",
|
|
1260
|
+
"xl"
|
|
1261
|
+
]
|
|
1262
|
+
}],
|
|
1263
|
+
defaultVariants: {
|
|
1264
|
+
size: "md",
|
|
1265
|
+
variant: "surface"
|
|
1266
|
+
},
|
|
1267
|
+
jsx: [/^Popover(\.\w+)?$/u],
|
|
1268
|
+
slots: [
|
|
1269
|
+
"root",
|
|
1270
|
+
"anchor",
|
|
1271
|
+
"trigger",
|
|
1272
|
+
"indicator",
|
|
1273
|
+
"positioner",
|
|
1274
|
+
"content",
|
|
1275
|
+
"title",
|
|
1276
|
+
"description",
|
|
1277
|
+
"closeTrigger",
|
|
1278
|
+
"arrow",
|
|
1279
|
+
"arrowTip"
|
|
1280
|
+
],
|
|
1281
|
+
variants: {
|
|
1282
|
+
/**
|
|
1283
|
+
* Padding of the panel, text of the title and the description, and size of the close trigger.
|
|
1284
|
+
*/
|
|
1285
|
+
size: onSlots({
|
|
1286
|
+
/**
|
|
1287
|
+
* Close trigger: a square two sizes smaller on the control scale, with a mark one size
|
|
1288
|
+
* smaller on the icon scale, inset from the panel's corner by the panel's padding.
|
|
1289
|
+
*/
|
|
1290
|
+
closeTrigger: sizeVariants((size) => ({
|
|
1291
|
+
"& > svg": { boxSize: dense(`{sizes.icon.${below(size)}}`) },
|
|
1292
|
+
boxSize: dense(`{sizes.control.${below(below(size))}}`),
|
|
1293
|
+
insetBlockStart: dense(`{spacing.inset.${size}}`),
|
|
1294
|
+
insetInlineEnd: dense(`{spacing.inset.${size}}`)
|
|
1295
|
+
})),
|
|
1296
|
+
content: sizeVariants((size) => ({
|
|
1297
|
+
[CLOSED]: `calc(${dense(`{sizes.control.${below(below(size))}}`)} + ${dense(`{spacing.gap.${size}}`)})`,
|
|
1298
|
+
padding: dense(`{spacing.inset.${size}}`)
|
|
1299
|
+
})),
|
|
1300
|
+
description: textSizes("body"),
|
|
1301
|
+
title: textSizes("body")
|
|
1302
|
+
}),
|
|
1303
|
+
/**
|
|
1304
|
+
* Surface of the panel: the popover surface inside a hairline edge, the panel surface with a
|
|
1305
|
+
* large shadow, or the glass layer style.
|
|
1306
|
+
*
|
|
1307
|
+
* @remarks
|
|
1308
|
+
* The elevated panel has a transparent hairline edge, which forced colors paint in
|
|
1309
|
+
* `CanvasText`, so the panel keeps its outline where the shadow is not drawn.
|
|
1310
|
+
*/
|
|
1311
|
+
variant: {
|
|
1312
|
+
elevated: {
|
|
1313
|
+
arrowTip: { borderColor: "var(--popover-surface)" },
|
|
1314
|
+
content: {
|
|
1315
|
+
"--popover-surface": "colors.bg.panel",
|
|
1316
|
+
background: "var(--popover-surface)",
|
|
1317
|
+
borderColor: "transparent",
|
|
1318
|
+
borderRadius: "l3",
|
|
1319
|
+
borderStyle: "solid",
|
|
1320
|
+
borderWidth: "hairline",
|
|
1321
|
+
boxShadow: "xl"
|
|
1322
|
+
}
|
|
1323
|
+
},
|
|
1324
|
+
glass: {
|
|
1325
|
+
arrowTip: { borderColor: "border" },
|
|
1326
|
+
content: {
|
|
1327
|
+
layerStyle: "glass",
|
|
1328
|
+
"--popover-surface": "colors.bg.popover",
|
|
1329
|
+
borderRadius: "l3"
|
|
1330
|
+
}
|
|
1331
|
+
},
|
|
1332
|
+
surface: {
|
|
1333
|
+
arrowTip: { borderColor: "border" },
|
|
1334
|
+
content: {
|
|
1335
|
+
"--popover-surface": "colors.bg.popover",
|
|
1336
|
+
background: "var(--popover-surface)",
|
|
1337
|
+
borderColor: "border",
|
|
1338
|
+
borderRadius: "l3",
|
|
1339
|
+
borderWidth: "hairline",
|
|
1340
|
+
boxShadow: "lg"
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
});
|
|
1346
|
+
//#endregion
|
|
1347
|
+
//#region src/steps/recipe.ts
|
|
1348
|
+
/**
|
|
1349
|
+
* Recipe for the steps: a list of numbered discs with a title, the rules between them, the content
|
|
1350
|
+
* of the current step and the buttons that move between steps.
|
|
1351
|
+
*
|
|
1352
|
+
* @remarks
|
|
1353
|
+
* The machine writes `data-orientation` on the root, the list, the items, the triggers and the
|
|
1354
|
+
* rules, so a vertical flow is a column of steps beside the content and a horizontal flow is a
|
|
1355
|
+
* row above it. The machine marks a disc and a rule with `data-complete`, `data-current` or
|
|
1356
|
+
* `data-incomplete`, and the looks read those. The size writes the disc's side and the gap around
|
|
1357
|
+
* it as custom properties on the root, which the rules read to meet the discs. A rule is a
|
|
1358
|
+
* border, so forced colors paint it in `CanvasText`. The recipe has no `effect` axis, because
|
|
1359
|
+
* the discs change on every step and an effect would move with them.
|
|
1360
|
+
*/
|
|
1361
|
+
/**
|
|
1362
|
+
* Sizes the recipe offers.
|
|
1363
|
+
*/
|
|
1364
|
+
const SIZES = [
|
|
1365
|
+
"sm",
|
|
1366
|
+
"md",
|
|
1367
|
+
"lg"
|
|
1368
|
+
];
|
|
1369
|
+
/**
|
|
1370
|
+
* Side of a disc, which the root sets per size.
|
|
1371
|
+
*/
|
|
1372
|
+
const DISC = "var(--steps-disc)";
|
|
1373
|
+
/**
|
|
1374
|
+
* Room between a disc and the rule beside it, which the root sets per size.
|
|
1375
|
+
*/
|
|
1376
|
+
const GUTTER = "var(--steps-gutter)";
|
|
1377
|
+
/**
|
|
1378
|
+
* Thickness of a rule and of an outlined disc's edge.
|
|
1379
|
+
*/
|
|
1380
|
+
const THICKNESS = "{borderWidths.indicator}";
|
|
1381
|
+
/**
|
|
1382
|
+
* Selects an item of a list that is too narrow for its steps with the titles beside the discs.
|
|
1383
|
+
*/
|
|
1384
|
+
const CROWDED_ITEM = "[data-crowded] > &";
|
|
1385
|
+
/**
|
|
1386
|
+
* Selects a trigger or a rule inside such a list.
|
|
1387
|
+
*/
|
|
1388
|
+
const CROWDED_PART = "[data-crowded] &";
|
|
1389
|
+
/**
|
|
1390
|
+
* Selects an item while the list measures its steps at their natural width.
|
|
1391
|
+
*/
|
|
1392
|
+
const MEASURED_ITEM = "[data-measuring] > &";
|
|
1393
|
+
/**
|
|
1394
|
+
* Lays an item out as a centred column of its disc over its words, for titles below the discs.
|
|
1395
|
+
*/
|
|
1396
|
+
const BELOW_ITEM = {
|
|
1397
|
+
_last: { flex: "1 0 0" },
|
|
1398
|
+
flexDirection: "column",
|
|
1399
|
+
textAlign: "center"
|
|
1400
|
+
};
|
|
1401
|
+
/**
|
|
1402
|
+
* Runs a rule from the centre line of one disc to the next, one gutter clear of both, for titles
|
|
1403
|
+
* below the discs.
|
|
1404
|
+
*/
|
|
1405
|
+
const BELOW_SEPARATOR = {
|
|
1406
|
+
insetBlockStart: `calc(${DISC} / 2 - ${THICKNESS} / 2)`,
|
|
1407
|
+
insetInlineEnd: `calc(-50% + ${DISC} / 2 + ${GUTTER})`,
|
|
1408
|
+
insetInlineStart: `calc(50% + ${DISC} / 2 + ${GUTTER})`,
|
|
1409
|
+
marginInlineEnd: "0",
|
|
1410
|
+
position: "absolute"
|
|
1411
|
+
};
|
|
1412
|
+
/**
|
|
1413
|
+
* Stacks a trigger's disc over its words, for titles below the discs.
|
|
1414
|
+
*/
|
|
1415
|
+
const BELOW_TRIGGER = {
|
|
1416
|
+
flexDirection: "column",
|
|
1417
|
+
textAlign: "center"
|
|
1418
|
+
};
|
|
1419
|
+
/**
|
|
1420
|
+
* Fills a completed disc with `Highlight` under forced colors, so it is distinct from the outlined
|
|
1421
|
+
* discs around it.
|
|
1422
|
+
*/
|
|
1423
|
+
const FORCED_COMPLETE = { _highContrast: {
|
|
1424
|
+
background: "Highlight",
|
|
1425
|
+
borderColor: "Highlight",
|
|
1426
|
+
color: "HighlightText",
|
|
1427
|
+
forcedColorAdjust: "none"
|
|
1428
|
+
} };
|
|
1429
|
+
/**
|
|
1430
|
+
* Rings the current disc with `Highlight` on `Canvas` under forced colors, so it is distinct from
|
|
1431
|
+
* the outlined discs of the later steps.
|
|
1432
|
+
*/
|
|
1433
|
+
const FORCED_CURRENT = { _highContrast: {
|
|
1434
|
+
background: "Canvas",
|
|
1435
|
+
borderColor: "Highlight",
|
|
1436
|
+
borderStyle: "solid",
|
|
1437
|
+
borderWidth: THICKNESS,
|
|
1438
|
+
color: "CanvasText",
|
|
1439
|
+
forcedColorAdjust: "none"
|
|
1440
|
+
} };
|
|
1441
|
+
/**
|
|
1442
|
+
* Defines the steps recipe: solid discs at size `md` in the neutral palette with the titles beside
|
|
1443
|
+
* them, by default.
|
|
1444
|
+
*/
|
|
1445
|
+
const recipe$4 = defineSlotRecipe({
|
|
1446
|
+
base: {
|
|
1447
|
+
content: {
|
|
1448
|
+
_vertical: { flex: "1" },
|
|
1449
|
+
minInlineSize: "0"
|
|
1450
|
+
},
|
|
1451
|
+
description: { color: "fg.muted" },
|
|
1452
|
+
indicator: {
|
|
1453
|
+
_highContrast: {
|
|
1454
|
+
borderColor: "CanvasText",
|
|
1455
|
+
borderStyle: "solid",
|
|
1456
|
+
borderWidth: "hairline"
|
|
1457
|
+
},
|
|
1458
|
+
alignItems: "center",
|
|
1459
|
+
blockSize: DISC,
|
|
1460
|
+
borderRadius: "full",
|
|
1461
|
+
display: "inline-flex",
|
|
1462
|
+
flexShrink: "0",
|
|
1463
|
+
fontVariantNumeric: "tabular-nums",
|
|
1464
|
+
fontWeight: "medium",
|
|
1465
|
+
inlineSize: DISC,
|
|
1466
|
+
justifyContent: "center",
|
|
1467
|
+
transitionDuration: "press",
|
|
1468
|
+
transitionProperty: "common",
|
|
1469
|
+
transitionTimingFunction: "press"
|
|
1470
|
+
},
|
|
1471
|
+
item: {
|
|
1472
|
+
_horizontal: {
|
|
1473
|
+
_last: { flex: "0 0 auto" },
|
|
1474
|
+
alignItems: "center",
|
|
1475
|
+
flex: "1 0 0",
|
|
1476
|
+
[MEASURED_ITEM]: { flex: "0 0 auto" }
|
|
1477
|
+
},
|
|
1478
|
+
_vertical: { alignItems: "flex-start" },
|
|
1479
|
+
display: "flex",
|
|
1480
|
+
gap: GUTTER,
|
|
1481
|
+
minInlineSize: "0",
|
|
1482
|
+
position: "relative"
|
|
1483
|
+
},
|
|
1484
|
+
list: {
|
|
1485
|
+
_horizontal: {
|
|
1486
|
+
alignItems: "center",
|
|
1487
|
+
flexDirection: "row"
|
|
1488
|
+
},
|
|
1489
|
+
_vertical: { flexDirection: "column" },
|
|
1490
|
+
display: "flex",
|
|
1491
|
+
listStyle: "none",
|
|
1492
|
+
margin: "0",
|
|
1493
|
+
padding: "0"
|
|
1494
|
+
},
|
|
1495
|
+
root: {
|
|
1496
|
+
_horizontal: { flexDirection: "column" },
|
|
1497
|
+
_vertical: { flexDirection: "row" },
|
|
1498
|
+
display: "flex",
|
|
1499
|
+
inlineSize: "full"
|
|
1500
|
+
},
|
|
1501
|
+
separator: {
|
|
1502
|
+
_horizontal: {
|
|
1503
|
+
borderBlockStartWidth: THICKNESS,
|
|
1504
|
+
flex: "1",
|
|
1505
|
+
marginInlineEnd: GUTTER,
|
|
1506
|
+
minInlineSize: GUTTER
|
|
1507
|
+
},
|
|
1508
|
+
_vertical: {
|
|
1509
|
+
borderInlineStartWidth: THICKNESS,
|
|
1510
|
+
insetBlockEnd: GUTTER,
|
|
1511
|
+
insetBlockStart: `calc(${DISC} + ${GUTTER})`,
|
|
1512
|
+
insetInlineStart: `calc(${DISC} / 2 - ${THICKNESS} / 2)`,
|
|
1513
|
+
position: "absolute"
|
|
1514
|
+
},
|
|
1515
|
+
borderColor: "border",
|
|
1516
|
+
borderRadius: "full"
|
|
1517
|
+
},
|
|
1518
|
+
status: { srOnly: true },
|
|
1519
|
+
title: {
|
|
1520
|
+
"&[data-incomplete]": { color: "fg.muted" },
|
|
1521
|
+
color: "fg",
|
|
1522
|
+
fontWeight: "medium"
|
|
1523
|
+
},
|
|
1524
|
+
trigger: {
|
|
1525
|
+
focusVisibleRing: "outside",
|
|
1526
|
+
_disabled: { cursor: "disabled" },
|
|
1527
|
+
alignItems: "center",
|
|
1528
|
+
borderRadius: "l2",
|
|
1529
|
+
cursor: "button",
|
|
1530
|
+
display: "flex",
|
|
1531
|
+
focusRingColor: "colorPalette.focusRing",
|
|
1532
|
+
gap: GUTTER,
|
|
1533
|
+
minInlineSize: "0",
|
|
1534
|
+
textAlign: "start",
|
|
1535
|
+
userSelect: "none"
|
|
1536
|
+
}
|
|
1537
|
+
},
|
|
1538
|
+
className: "steps",
|
|
1539
|
+
defaultVariants: {
|
|
1540
|
+
labelPlacement: "beside",
|
|
1541
|
+
palette: "neutral",
|
|
1542
|
+
size: "md",
|
|
1543
|
+
variant: "solid"
|
|
1544
|
+
},
|
|
1545
|
+
jsx: [/^Steps(\.\w+)?$/u],
|
|
1546
|
+
slots: [
|
|
1547
|
+
"root",
|
|
1548
|
+
"list",
|
|
1549
|
+
"item",
|
|
1550
|
+
"trigger",
|
|
1551
|
+
"indicator",
|
|
1552
|
+
"title",
|
|
1553
|
+
"status",
|
|
1554
|
+
"description",
|
|
1555
|
+
"separator",
|
|
1556
|
+
"content",
|
|
1557
|
+
"nextTrigger",
|
|
1558
|
+
"prevTrigger"
|
|
1559
|
+
],
|
|
1560
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
1561
|
+
variants: {
|
|
1562
|
+
/**
|
|
1563
|
+
* Place of the titles in a horizontal flow: beside each disc, or centred below it.
|
|
1564
|
+
*
|
|
1565
|
+
* @remarks
|
|
1566
|
+
* Below, every item takes an equal share of the row and stacks its disc over its words in a
|
|
1567
|
+
* centred column, with a trigger or without one, and each rule runs from one disc to the next
|
|
1568
|
+
* across the gap between the items, one gutter clear of both. Beside falls back to below
|
|
1569
|
+
* while the list is too narrow for its steps at their natural width, which the list measures
|
|
1570
|
+
* and marks with `data-crowded`. A vertical flow keeps the titles beside the discs either
|
|
1571
|
+
* way.
|
|
1572
|
+
*/
|
|
1573
|
+
labelPlacement: {
|
|
1574
|
+
below: {
|
|
1575
|
+
item: { _horizontal: BELOW_ITEM },
|
|
1576
|
+
separator: { _horizontal: BELOW_SEPARATOR },
|
|
1577
|
+
trigger: { _horizontal: BELOW_TRIGGER }
|
|
1578
|
+
},
|
|
1579
|
+
beside: {
|
|
1580
|
+
item: { _horizontal: { [CROWDED_ITEM]: BELOW_ITEM } },
|
|
1581
|
+
separator: { _horizontal: { [CROWDED_PART]: BELOW_SEPARATOR } },
|
|
1582
|
+
trigger: { _horizontal: { [CROWDED_PART]: BELOW_TRIGGER } }
|
|
1583
|
+
}
|
|
1584
|
+
},
|
|
1585
|
+
/**
|
|
1586
|
+
* Palette the discs and the completed rules read, set on the root.
|
|
1587
|
+
*/
|
|
1588
|
+
palette: onSlot("root", paletteVariants()),
|
|
1589
|
+
/**
|
|
1590
|
+
* Size of the discs on the control scale, the titles, the descriptions and the room around
|
|
1591
|
+
* them.
|
|
1592
|
+
*
|
|
1593
|
+
* @remarks
|
|
1594
|
+
* The discs measure 36, 40 and 44px. An icon in a disc is on the icon scale one step below
|
|
1595
|
+
* the size. In a vertical flow every item but the last leaves two insets below its trigger,
|
|
1596
|
+
* so the rule under a disc with a title and a description is 21px long at `md`.
|
|
1597
|
+
*/
|
|
1598
|
+
size: onSlots({
|
|
1599
|
+
description: sizeVariants((size) => ({ textStyle: `body.${below(size)}` }), SIZES),
|
|
1600
|
+
indicator: sizeVariants((size) => ({
|
|
1601
|
+
textStyle: `label.${size}`,
|
|
1602
|
+
"& > svg": { boxSize: dense(`{sizes.icon.${below(size)}}`) }
|
|
1603
|
+
}), SIZES),
|
|
1604
|
+
item: sizeVariants((size) => ({ _vertical: {
|
|
1605
|
+
_last: { paddingBlockEnd: "0" },
|
|
1606
|
+
paddingBlockEnd: dense(`calc({spacing.inset.${size}} * 2)`)
|
|
1607
|
+
} }), SIZES),
|
|
1608
|
+
root: sizeVariants((size) => ({
|
|
1609
|
+
"--steps-disc": dense(`{sizes.control.${size}}`),
|
|
1610
|
+
"--steps-gutter": dense(`{spacing.gap.${size}}`),
|
|
1611
|
+
gap: dense(`{spacing.inset.${size}}`)
|
|
1612
|
+
}), SIZES),
|
|
1613
|
+
title: sizeVariants((size) => ({ textStyle: `label.${size}` }), SIZES)
|
|
1614
|
+
}),
|
|
1615
|
+
/**
|
|
1616
|
+
* Look of the discs and of the rules between completed steps.
|
|
1617
|
+
*
|
|
1618
|
+
* @remarks
|
|
1619
|
+
* `solid` outlines a later step's disc, rings the current disc and fills a completed one with
|
|
1620
|
+
* the palette's solid color. `subtle` fills every disc from the palette: its subtle role for
|
|
1621
|
+
* a later step, its muted role for a completed one and its emphasized role for the current
|
|
1622
|
+
* one. The three roles step away from the page in both color modes, so the current disc is
|
|
1623
|
+
* the strongest by day and after dark.
|
|
1624
|
+
*/
|
|
1625
|
+
variant: {
|
|
1626
|
+
solid: {
|
|
1627
|
+
indicator: {
|
|
1628
|
+
"&[data-complete]": {
|
|
1629
|
+
...FORCED_COMPLETE,
|
|
1630
|
+
background: "colorPalette.solid",
|
|
1631
|
+
borderColor: "colorPalette.solid",
|
|
1632
|
+
borderStyle: "solid",
|
|
1633
|
+
borderWidth: THICKNESS,
|
|
1634
|
+
color: "colorPalette.contrast"
|
|
1635
|
+
},
|
|
1636
|
+
"&[data-current]": {
|
|
1637
|
+
...FORCED_CURRENT,
|
|
1638
|
+
background: "colorPalette.subtle",
|
|
1639
|
+
borderColor: "colorPalette.solid",
|
|
1640
|
+
borderStyle: "solid",
|
|
1641
|
+
borderWidth: THICKNESS,
|
|
1642
|
+
color: "colorPalette.fg"
|
|
1643
|
+
},
|
|
1644
|
+
"&[data-incomplete]": {
|
|
1645
|
+
borderColor: "border",
|
|
1646
|
+
borderStyle: "solid",
|
|
1647
|
+
borderWidth: THICKNESS,
|
|
1648
|
+
color: "fg.muted"
|
|
1649
|
+
}
|
|
1650
|
+
},
|
|
1651
|
+
separator: { "&[data-complete]": {
|
|
1652
|
+
_highContrast: {
|
|
1653
|
+
borderColor: "Highlight",
|
|
1654
|
+
forcedColorAdjust: "none"
|
|
1655
|
+
},
|
|
1656
|
+
borderColor: "colorPalette.solid"
|
|
1657
|
+
} }
|
|
1658
|
+
},
|
|
1659
|
+
subtle: {
|
|
1660
|
+
indicator: {
|
|
1661
|
+
"&[data-complete]": {
|
|
1662
|
+
...FORCED_COMPLETE,
|
|
1663
|
+
background: "colorPalette.muted",
|
|
1664
|
+
color: "colorPalette.fg"
|
|
1665
|
+
},
|
|
1666
|
+
"&[data-current]": {
|
|
1667
|
+
...FORCED_CURRENT,
|
|
1668
|
+
background: "colorPalette.emphasized",
|
|
1669
|
+
color: "colorPalette.fg"
|
|
1670
|
+
},
|
|
1671
|
+
"&[data-incomplete]": {
|
|
1672
|
+
background: "colorPalette.subtle",
|
|
1673
|
+
color: "fg.muted"
|
|
1674
|
+
}
|
|
1675
|
+
},
|
|
1676
|
+
separator: { "&[data-complete]": {
|
|
1677
|
+
_highContrast: {
|
|
1678
|
+
borderColor: "Highlight",
|
|
1679
|
+
forcedColorAdjust: "none"
|
|
1680
|
+
},
|
|
1681
|
+
borderColor: "colorPalette.emphasized"
|
|
1682
|
+
} }
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
});
|
|
1687
|
+
//#endregion
|
|
1688
|
+
//#region src/tabs/recipe.ts
|
|
1689
|
+
/**
|
|
1690
|
+
* Recipe for the tabs: a list of tabs, the panel of each, and the indicator under the selected
|
|
1691
|
+
* tab.
|
|
1692
|
+
*
|
|
1693
|
+
* @remarks
|
|
1694
|
+
* The orientation is the machine's option, not an axis. The machine sets `data-orientation` on
|
|
1695
|
+
* every part, so the list turns into a column and the line indicator moves to the inline edge
|
|
1696
|
+
* without a second prop. The machine measures the selected tab into `--width` and `--height` and
|
|
1697
|
+
* positions the indicator, so the recipe sets its size and fill and never its position. Every
|
|
1698
|
+
* tab sits over the indicator, so a filled indicator does not cover the tab's text. The palette
|
|
1699
|
+
* is set on the root, and the line and subtle looks read it. The recipe has no `effect` axis,
|
|
1700
|
+
* because the indicator moves on every selection, and a glow would move with it.
|
|
1701
|
+
*/
|
|
1702
|
+
/**
|
|
1703
|
+
* Sizes an indicator that fills the selected tab to the tab's measured width and height.
|
|
1704
|
+
*
|
|
1705
|
+
* @remarks
|
|
1706
|
+
* A line indicator takes one of the two and its own stroke for the other. A filled indicator
|
|
1707
|
+
* takes both. Without them it collapsed to nothing and left the selected tab unmarked.
|
|
1708
|
+
*/
|
|
1709
|
+
const FILLED = {
|
|
1710
|
+
height: "var(--height)",
|
|
1711
|
+
width: "var(--width)"
|
|
1712
|
+
};
|
|
1713
|
+
/**
|
|
1714
|
+
* Places a line indicator under the selected tab, or at its inline start in a vertical list.
|
|
1715
|
+
*/
|
|
1716
|
+
const BAR = {
|
|
1717
|
+
_horizontal: {
|
|
1718
|
+
bottom: "0",
|
|
1719
|
+
height: "{borderWidths.indicator}",
|
|
1720
|
+
width: "var(--width)"
|
|
1721
|
+
},
|
|
1722
|
+
_vertical: {
|
|
1723
|
+
height: "var(--height)",
|
|
1724
|
+
insetInlineStart: "0",
|
|
1725
|
+
width: "{borderWidths.indicator}"
|
|
1726
|
+
}
|
|
1727
|
+
};
|
|
1728
|
+
/**
|
|
1729
|
+
* Paints a bar in the system's selection colour under forced colors.
|
|
1730
|
+
*
|
|
1731
|
+
* @remarks
|
|
1732
|
+
* Forced colors replace every author background with `Canvas`, so a filled bar vanishes and the
|
|
1733
|
+
* selected tab keeps no mark. A system colour with `forcedColorAdjust: none` keeps the bar.
|
|
1734
|
+
*/
|
|
1735
|
+
const FORCED_BAR = {
|
|
1736
|
+
background: "Highlight",
|
|
1737
|
+
forcedColorAdjust: "none"
|
|
1738
|
+
};
|
|
1739
|
+
/**
|
|
1740
|
+
* Defines the tabs recipe: a line of tabs at size `md` by default.
|
|
1741
|
+
*/
|
|
1742
|
+
const recipe$3 = defineSlotRecipe({
|
|
1743
|
+
base: {
|
|
1744
|
+
closeTrigger: {
|
|
1745
|
+
_hover: {
|
|
1746
|
+
background: "colorPalette.muted",
|
|
1747
|
+
color: "fg"
|
|
1748
|
+
},
|
|
1749
|
+
"& > svg": {
|
|
1750
|
+
blockSize: "1em",
|
|
1751
|
+
inlineSize: "1em"
|
|
1752
|
+
},
|
|
1753
|
+
alignItems: "center",
|
|
1754
|
+
borderRadius: "l1",
|
|
1755
|
+
color: "fg.muted",
|
|
1756
|
+
display: "inline-flex",
|
|
1757
|
+
flexShrink: "0",
|
|
1758
|
+
justifyContent: "center",
|
|
1759
|
+
marginInline: "calc((1em - {sizes.6}) / 2)",
|
|
1760
|
+
minBlockSize: "6",
|
|
1761
|
+
minInlineSize: "6"
|
|
1762
|
+
},
|
|
1763
|
+
content: {
|
|
1764
|
+
_focusVisible: { focusVisibleRing: "outside" },
|
|
1765
|
+
outline: "none"
|
|
1766
|
+
},
|
|
1767
|
+
indicator: {
|
|
1768
|
+
borderRadius: "l1",
|
|
1769
|
+
pointerEvents: "none",
|
|
1770
|
+
zIndex: "0"
|
|
1771
|
+
},
|
|
1772
|
+
list: {
|
|
1773
|
+
_horizontal: { flexDirection: "row" },
|
|
1774
|
+
_vertical: { flexDirection: "column" },
|
|
1775
|
+
display: "flex",
|
|
1776
|
+
position: "relative"
|
|
1777
|
+
},
|
|
1778
|
+
root: {
|
|
1779
|
+
_horizontal: { flexDirection: "column" },
|
|
1780
|
+
_vertical: { flexDirection: "row" },
|
|
1781
|
+
display: "flex",
|
|
1782
|
+
width: "full"
|
|
1783
|
+
},
|
|
1784
|
+
trigger: {
|
|
1785
|
+
...interactive(),
|
|
1786
|
+
_disabled: { cursor: "disabled" },
|
|
1787
|
+
_vertical: { justifyContent: "flex-start" },
|
|
1788
|
+
"& > svg": {
|
|
1789
|
+
blockSize: "1em",
|
|
1790
|
+
flexShrink: "0",
|
|
1791
|
+
inlineSize: "1em"
|
|
1792
|
+
},
|
|
1793
|
+
alignItems: "center",
|
|
1794
|
+
display: "inline-flex",
|
|
1795
|
+
justifyContent: "center",
|
|
1796
|
+
position: "relative",
|
|
1797
|
+
whiteSpace: "nowrap",
|
|
1798
|
+
zIndex: "1"
|
|
1799
|
+
}
|
|
1800
|
+
},
|
|
1801
|
+
className: "tabs",
|
|
1802
|
+
defaultVariants: {
|
|
1803
|
+
size: "md",
|
|
1804
|
+
variant: "line"
|
|
1805
|
+
},
|
|
1806
|
+
jsx: [/^Tabs(\.\w+)?$/u],
|
|
1807
|
+
slots: [
|
|
1808
|
+
"root",
|
|
1809
|
+
"list",
|
|
1810
|
+
"trigger",
|
|
1811
|
+
"closeTrigger",
|
|
1812
|
+
"content",
|
|
1813
|
+
"indicator"
|
|
1814
|
+
],
|
|
1815
|
+
staticCss: [{ palette: [...PALETTES] }],
|
|
1816
|
+
variants: {
|
|
1817
|
+
/**
|
|
1818
|
+
* Whether the tabs share the list's width equally.
|
|
1819
|
+
*/
|
|
1820
|
+
fitted: { true: { trigger: { flex: "1" } } },
|
|
1821
|
+
/**
|
|
1822
|
+
* Distribution of the tabs in a list wider than they are.
|
|
1823
|
+
*/
|
|
1824
|
+
justify: onSlot("list", justifyVariants()),
|
|
1825
|
+
/**
|
|
1826
|
+
* Palette of the line indicator, the subtle indicator and the selected tab's text, set on the
|
|
1827
|
+
* root.
|
|
1828
|
+
*/
|
|
1829
|
+
palette: onSlot("root", paletteVariants()),
|
|
1830
|
+
/**
|
|
1831
|
+
* Size of the tabs on the control scale and the panel's padding on the inset scale.
|
|
1832
|
+
*/
|
|
1833
|
+
size: onSlots({
|
|
1834
|
+
content: insetSizes(),
|
|
1835
|
+
trigger: controlSizes()
|
|
1836
|
+
}),
|
|
1837
|
+
/**
|
|
1838
|
+
* Look of the list and the indicator of the selected tab.
|
|
1839
|
+
*/
|
|
1840
|
+
variant: {
|
|
1841
|
+
enclosed: {
|
|
1842
|
+
indicator: {
|
|
1843
|
+
...FILLED,
|
|
1844
|
+
background: "bg.panel",
|
|
1845
|
+
borderColor: "border",
|
|
1846
|
+
borderWidth: "hairline"
|
|
1847
|
+
},
|
|
1848
|
+
list: {
|
|
1849
|
+
background: "bg.muted",
|
|
1850
|
+
borderRadius: "l2",
|
|
1851
|
+
padding: dense("{spacing.inset.xs}")
|
|
1852
|
+
},
|
|
1853
|
+
trigger: {
|
|
1854
|
+
_selected: { color: "fg" },
|
|
1855
|
+
borderRadius: "l1",
|
|
1856
|
+
color: "fg.muted"
|
|
1857
|
+
}
|
|
1858
|
+
},
|
|
1859
|
+
line: {
|
|
1860
|
+
indicator: {
|
|
1861
|
+
...BAR,
|
|
1862
|
+
_highContrast: FORCED_BAR,
|
|
1863
|
+
background: "colorPalette.solid"
|
|
1864
|
+
},
|
|
1865
|
+
list: {
|
|
1866
|
+
_horizontal: {
|
|
1867
|
+
borderBlockEndColor: "border",
|
|
1868
|
+
borderBlockEndWidth: "hairline"
|
|
1869
|
+
},
|
|
1870
|
+
_vertical: {
|
|
1871
|
+
borderInlineEndColor: "border",
|
|
1872
|
+
borderInlineEndWidth: "hairline"
|
|
1873
|
+
}
|
|
1874
|
+
},
|
|
1875
|
+
trigger: {
|
|
1876
|
+
_hover: { color: "fg" },
|
|
1877
|
+
_selected: { color: "colorPalette.fg" },
|
|
1878
|
+
color: "fg.muted"
|
|
1879
|
+
}
|
|
1880
|
+
},
|
|
1881
|
+
plain: {
|
|
1882
|
+
indicator: {
|
|
1883
|
+
...BAR,
|
|
1884
|
+
_highContrast: {
|
|
1885
|
+
...FORCED_BAR,
|
|
1886
|
+
display: "block"
|
|
1887
|
+
},
|
|
1888
|
+
display: "none"
|
|
1889
|
+
},
|
|
1890
|
+
trigger: {
|
|
1891
|
+
_selected: { color: "fg" },
|
|
1892
|
+
color: "fg.muted"
|
|
1893
|
+
}
|
|
1894
|
+
},
|
|
1895
|
+
subtle: {
|
|
1896
|
+
indicator: {
|
|
1897
|
+
...FILLED,
|
|
1898
|
+
_highContrast: {
|
|
1899
|
+
outlineColor: "Highlight",
|
|
1900
|
+
outlineOffset: "calc({borderWidths.indicator} * -1)",
|
|
1901
|
+
outlineStyle: "solid",
|
|
1902
|
+
outlineWidth: "{borderWidths.indicator}"
|
|
1903
|
+
},
|
|
1904
|
+
background: "colorPalette.subtle",
|
|
1905
|
+
borderRadius: "l1"
|
|
1906
|
+
},
|
|
1907
|
+
trigger: {
|
|
1908
|
+
_selected: { color: "colorPalette.fg" },
|
|
1909
|
+
color: "fg.muted"
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
});
|
|
1915
|
+
//#endregion
|
|
1916
|
+
//#region src/toggle-tip/recipe.ts
|
|
1917
|
+
/**
|
|
1918
|
+
* Recipe for the toggle tip: a short note that a press opens beside its trigger, with an arrow.
|
|
1919
|
+
*
|
|
1920
|
+
* @remarks
|
|
1921
|
+
* The toggle tip looks like the tooltip and opens like the popover, so the recipe repeats the
|
|
1922
|
+
* tooltip's looks and sizes and gives the trigger a control's cursor, focus ring and disabled
|
|
1923
|
+
* look. The machine has no root part. The recipe adds a root with `display: contents`, because
|
|
1924
|
+
* the trigger and the positioner are siblings and a slot recipe passes its variants from an
|
|
1925
|
+
* element above both. The machine writes the positioner's position inline, so the recipe sets no
|
|
1926
|
+
* position. The content takes the `tooltip` z-index, so a tip opened inside a popover renders
|
|
1927
|
+
* above it, and reads the label role. It wraps at 20rem, or at the room the machine measures on
|
|
1928
|
+
* its side of the trigger, `--available-width`, where that is less. A link inside the content
|
|
1929
|
+
* takes the content's ink and an underline, because the link ink measures 1.71:1 in light mode
|
|
1930
|
+
* and 1.38:1 in dark mode on the inverted surface of the ink theme. Each look sets
|
|
1931
|
+
* `--toggle-tip-surface` once, and the arrow tip reads it. The inverted look has a transparent
|
|
1932
|
+
* hairline edge, which forced colors paint in `CanvasText`. The recipe has no `palette` axis,
|
|
1933
|
+
* because both looks use neutral surfaces, and no `effect` axis, because a note is not a
|
|
1934
|
+
* control.
|
|
1935
|
+
*/
|
|
1936
|
+
/**
|
|
1937
|
+
* Defines the toggle tip recipe: an inverted tip at size `md` by default.
|
|
1938
|
+
*/
|
|
1939
|
+
const recipe$2 = defineSlotRecipe({
|
|
1940
|
+
base: {
|
|
1941
|
+
arrow: {
|
|
1942
|
+
"--arrow-background": "var(--toggle-tip-surface)",
|
|
1943
|
+
"--arrow-size": "sizes.icon.xs"
|
|
1944
|
+
},
|
|
1945
|
+
arrowTip: {
|
|
1946
|
+
borderInlineStartWidth: "hairline",
|
|
1947
|
+
borderTopWidth: "hairline"
|
|
1948
|
+
},
|
|
1949
|
+
content: {
|
|
1950
|
+
...motion("scale-fade.in", "scale-fade.out"),
|
|
1951
|
+
"& a": {
|
|
1952
|
+
color: "inherit",
|
|
1953
|
+
textDecoration: "underline"
|
|
1954
|
+
},
|
|
1955
|
+
background: "var(--toggle-tip-surface)",
|
|
1956
|
+
borderRadius: "l2",
|
|
1957
|
+
boxShadow: "md",
|
|
1958
|
+
fontWeight: "medium",
|
|
1959
|
+
maxInlineSize: "min({sizes.xs}, var(--available-width, {sizes.xs}))",
|
|
1960
|
+
textWrap: "pretty",
|
|
1961
|
+
transformOrigin: "var(--transform-origin)",
|
|
1962
|
+
zIndex: "tooltip"
|
|
1963
|
+
},
|
|
1964
|
+
positioner: { position: "relative" },
|
|
1965
|
+
root: { display: "contents" },
|
|
1966
|
+
trigger: {
|
|
1967
|
+
...interactive(),
|
|
1968
|
+
alignItems: "center",
|
|
1969
|
+
display: "inline-flex"
|
|
1970
|
+
}
|
|
1971
|
+
},
|
|
1972
|
+
className: "toggle-tip",
|
|
1973
|
+
defaultVariants: {
|
|
1974
|
+
size: "md",
|
|
1975
|
+
variant: "inverted"
|
|
1976
|
+
},
|
|
1977
|
+
jsx: [/^ToggleTip(\.\w+)?$/u],
|
|
1978
|
+
slots: [
|
|
1979
|
+
"root",
|
|
1980
|
+
"trigger",
|
|
1981
|
+
"positioner",
|
|
1982
|
+
"content",
|
|
1983
|
+
"arrow",
|
|
1984
|
+
"arrowTip"
|
|
1985
|
+
],
|
|
1986
|
+
variants: {
|
|
1987
|
+
/**
|
|
1988
|
+
* Padding of the content on the inset scale and its text on the label role.
|
|
1989
|
+
*/
|
|
1990
|
+
size: onSlot("content", sizeVariants((size) => ({
|
|
1991
|
+
textStyle: `label.${size}`,
|
|
1992
|
+
padding: dense(`{spacing.inset.${size}}`)
|
|
1993
|
+
}))),
|
|
1994
|
+
/**
|
|
1995
|
+
* Surface of the content: the inverted surface, or the popover surface inside a hairline edge.
|
|
1996
|
+
*/
|
|
1997
|
+
variant: {
|
|
1998
|
+
inverted: {
|
|
1999
|
+
arrowTip: { borderColor: "var(--toggle-tip-surface)" },
|
|
2000
|
+
content: {
|
|
2001
|
+
"--toggle-tip-surface": "colors.bg.inverted",
|
|
2002
|
+
borderColor: "transparent",
|
|
2003
|
+
borderStyle: "solid",
|
|
2004
|
+
borderWidth: "hairline",
|
|
2005
|
+
color: "fg.inverted"
|
|
2006
|
+
}
|
|
2007
|
+
},
|
|
2008
|
+
surface: {
|
|
2009
|
+
arrowTip: { borderColor: "border" },
|
|
2010
|
+
content: {
|
|
2011
|
+
"--toggle-tip-surface": "colors.bg.popover",
|
|
2012
|
+
borderColor: "border",
|
|
2013
|
+
borderWidth: "hairline",
|
|
2014
|
+
color: "fg"
|
|
2015
|
+
}
|
|
2016
|
+
}
|
|
2017
|
+
}
|
|
2018
|
+
}
|
|
2019
|
+
});
|
|
2020
|
+
//#endregion
|
|
2021
|
+
//#region src/tooltip/recipe.ts
|
|
2022
|
+
/**
|
|
2023
|
+
* Recipe for the tooltip: a short label beside its trigger, with an arrow.
|
|
2024
|
+
*
|
|
2025
|
+
* @remarks
|
|
2026
|
+
* The machine has no root part. The recipe adds a root with `display: contents`, because the
|
|
2027
|
+
* trigger and the positioner are siblings and a slot recipe passes its variants from an element
|
|
2028
|
+
* above both. The machine measures the trigger and writes the positioner's position inline, so
|
|
2029
|
+
* the recipe sets no position. The content scales from the `--transform-origin` the machine sets.
|
|
2030
|
+
* The content takes the `tooltip` z-index. The machine writes `z-index: var(--z-index)` inline on
|
|
2031
|
+
* the positioner from the content's computed value, so a z-index on the positioner has no effect.
|
|
2032
|
+
* Each look sets `--tooltip-surface` once, and the arrow tip reads it. The content reads the
|
|
2033
|
+
* label role and wraps at `maxWidth: xs`. The trigger has no styles, because the caller passes
|
|
2034
|
+
* the control through `as`. The recipe has no `palette` axis, because both looks use neutral
|
|
2035
|
+
* surfaces, and no `effect` axis, because a tooltip is not a control.
|
|
2036
|
+
*/
|
|
2037
|
+
/**
|
|
2038
|
+
* Defines the tooltip recipe: an inverted tooltip at size `md` by default.
|
|
2039
|
+
*/
|
|
2040
|
+
const recipe$1 = defineSlotRecipe({
|
|
2041
|
+
base: {
|
|
2042
|
+
arrow: {
|
|
2043
|
+
"--arrow-background": "var(--tooltip-surface)",
|
|
2044
|
+
"--arrow-size": "sizes.icon.xs"
|
|
2045
|
+
},
|
|
2046
|
+
arrowTip: {
|
|
2047
|
+
borderInlineStartWidth: "hairline",
|
|
2048
|
+
borderTopWidth: "hairline"
|
|
2049
|
+
},
|
|
2050
|
+
content: {
|
|
2051
|
+
...motion("scale-fade.in", "scale-fade.out"),
|
|
2052
|
+
background: "var(--tooltip-surface)",
|
|
2053
|
+
borderRadius: "l2",
|
|
2054
|
+
boxShadow: "md",
|
|
2055
|
+
fontWeight: "medium",
|
|
2056
|
+
maxWidth: "xs",
|
|
2057
|
+
textWrap: "pretty",
|
|
2058
|
+
transformOrigin: "var(--transform-origin)",
|
|
2059
|
+
zIndex: "tooltip"
|
|
2060
|
+
},
|
|
2061
|
+
positioner: { position: "relative" },
|
|
2062
|
+
root: { display: "contents" }
|
|
2063
|
+
},
|
|
2064
|
+
className: "tooltip",
|
|
2065
|
+
defaultVariants: {
|
|
2066
|
+
size: "md",
|
|
2067
|
+
variant: "inverted"
|
|
2068
|
+
},
|
|
2069
|
+
jsx: [/^Tooltip(\.\w+)?$/u],
|
|
2070
|
+
slots: [
|
|
2071
|
+
"root",
|
|
2072
|
+
"trigger",
|
|
2073
|
+
"positioner",
|
|
2074
|
+
"content",
|
|
2075
|
+
"arrow",
|
|
2076
|
+
"arrowTip"
|
|
2077
|
+
],
|
|
2078
|
+
variants: {
|
|
2079
|
+
/**
|
|
2080
|
+
* Padding of the content on the inset scale and its text on the label role.
|
|
2081
|
+
*/
|
|
2082
|
+
size: onSlot("content", sizeVariants((size) => ({
|
|
2083
|
+
textStyle: `label.${size}`,
|
|
2084
|
+
padding: dense(`{spacing.inset.${size}}`)
|
|
2085
|
+
}))),
|
|
2086
|
+
/**
|
|
2087
|
+
* Surface of the content: the inverted surface, or the popover surface inside a hairline edge.
|
|
2088
|
+
*
|
|
2089
|
+
* @remarks
|
|
2090
|
+
* The inverted content has a transparent hairline edge, which forced colors paint in
|
|
2091
|
+
* `CanvasText`, so the content keeps its outline where its fill turns into the page's.
|
|
2092
|
+
*/
|
|
2093
|
+
variant: {
|
|
2094
|
+
inverted: {
|
|
2095
|
+
arrowTip: { borderColor: "var(--tooltip-surface)" },
|
|
2096
|
+
content: {
|
|
2097
|
+
"--tooltip-surface": "colors.bg.inverted",
|
|
2098
|
+
borderColor: "transparent",
|
|
2099
|
+
borderStyle: "solid",
|
|
2100
|
+
borderWidth: "hairline",
|
|
2101
|
+
color: "fg.inverted"
|
|
2102
|
+
}
|
|
2103
|
+
},
|
|
2104
|
+
surface: {
|
|
2105
|
+
arrowTip: { borderColor: "border" },
|
|
2106
|
+
content: {
|
|
2107
|
+
"--tooltip-surface": "colors.bg.popover",
|
|
2108
|
+
borderColor: "border",
|
|
2109
|
+
borderWidth: "hairline",
|
|
2110
|
+
color: "fg"
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
}
|
|
2115
|
+
});
|
|
2116
|
+
/**
|
|
2117
|
+
* Defines the truncate recipe.
|
|
2118
|
+
*/
|
|
2119
|
+
const recipe = defineRecipe({
|
|
2120
|
+
base: {
|
|
2121
|
+
focusVisibleRing: "outside",
|
|
2122
|
+
lineClamp: "var(--truncate-lines, 1)",
|
|
2123
|
+
minInlineSize: "0",
|
|
2124
|
+
overflowWrap: "anywhere"
|
|
2125
|
+
},
|
|
2126
|
+
className: "truncate",
|
|
2127
|
+
jsx: ["Truncate"]
|
|
2128
|
+
});
|
|
2129
|
+
//#endregion
|
|
2130
|
+
export { recipe$4 as a, MENU_DEPTH as c, recipe$8 as d, recipe$9 as f, recipe$3 as i, NESTED as l, recipe$11 as m, recipe$1 as n, recipe$5 as o, recipe$10 as p, recipe$2 as r, recipe$6 as s, recipe as t, recipe$7 as u };
|