@stealthscale/component-media 0.1.0

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.
@@ -0,0 +1,749 @@
1
+ import { PALETTES, cornerVariants, defineRecipe, defineSlotRecipe, dense, flatVariants, onSlot, onSlots, paletteVariants, ratioVariants, sizeVariants } from "@stealthscale/theme/authoring";
2
+ //#region src/audio/recipe.ts
3
+ /**
4
+ * Styles an `audio` element as a block as wide as its container.
5
+ *
6
+ * @remarks
7
+ * The browser draws the controls, so the recipe sets the box and the focus ring alone and offers
8
+ * no axis. A radius on the element clips Chromium's control panel, and the panel's colours follow
9
+ * the page's `color-scheme`.
10
+ */
11
+ /**
12
+ * Styles an audio element that fills the inline size of its container.
13
+ */
14
+ const recipe$6 = defineRecipe({
15
+ base: {
16
+ display: "block",
17
+ focusRingColor: "colorPalette.focusRing",
18
+ focusVisibleRing: "outside",
19
+ inlineSize: "full",
20
+ maxInlineSize: "full"
21
+ },
22
+ className: "audio",
23
+ jsx: [/^Audio$/u]
24
+ });
25
+ //#endregion
26
+ //#region src/avatar/recipe.ts
27
+ /**
28
+ * Declares the avatar's slot recipe: a box with a person's picture, or with their initials while
29
+ * the picture is missing, loading or broken, and a group that overlaps several boxes.
30
+ *
31
+ * @remarks
32
+ * The box is a square on the control scale, so an avatar is as tall as a button of its size: 32,
33
+ * 36, 40, 44, 48 and 56px from `xs` to `2xl`. `2xs` is the middle tag height, 24px, for a byline
34
+ * or a list row. The root sets its side as `--avatar-size`, and a group overlaps each avatar with
35
+ * the one before it by a quarter of that side. The image covers the box and takes the root's
36
+ * corners, and the root clips nothing, so a badge can overhang its edge. The fallback centres the
37
+ * initials in the label style of the avatar's size. The looks are the flat layer styles, because
38
+ * an avatar is read and not pressed, and the look and the palette color the fallback alone. A
39
+ * group rings each avatar in `bg.panel` with an outline, which is drawn outside the box and
40
+ * leaves the fill's curved edge alone. Under forced colors the box keeps a hairline `CanvasText`
41
+ * outline, because a look without an edge loses its shape there.
42
+ */
43
+ /**
44
+ * Custom property the root sets to its side, which a group and a badge read.
45
+ */
46
+ const SIZE = "--avatar-size";
47
+ /**
48
+ * Sizes the avatar offers on the control scale.
49
+ */
50
+ const SIZES = [
51
+ "xs",
52
+ "sm",
53
+ "md",
54
+ "lg",
55
+ "xl",
56
+ "2xl"
57
+ ];
58
+ /**
59
+ * Styles an element the machine hides with the `hidden` attribute, which a `display` value in the
60
+ * recipe would override.
61
+ */
62
+ const HIDDEN = { "&[hidden]": { display: "none" } };
63
+ /**
64
+ * Styles a subtle circle at the middle size in the neutral palette.
65
+ */
66
+ const recipe$5 = defineSlotRecipe({
67
+ base: {
68
+ fallback: {
69
+ ...HIDDEN,
70
+ "& > svg": {
71
+ blockSize: "50%",
72
+ inlineSize: "50%"
73
+ },
74
+ alignItems: "center",
75
+ blockSize: "full",
76
+ display: "flex",
77
+ inlineSize: "full",
78
+ justifyContent: "center",
79
+ lineHeight: "1",
80
+ textTransform: "uppercase"
81
+ },
82
+ group: {
83
+ "& > .avatar__root": {
84
+ outlineColor: "bg.panel",
85
+ outlineStyle: "solid",
86
+ outlineWidth: "indicator"
87
+ },
88
+ "& > .avatar__root + .avatar__root": { marginInlineStart: `calc(var(${SIZE}) / -4)` },
89
+ alignItems: "center",
90
+ display: "inline-flex",
91
+ flexShrink: "0",
92
+ verticalAlign: "middle"
93
+ },
94
+ image: {
95
+ ...HIDDEN,
96
+ blockSize: "full",
97
+ borderRadius: "inherit",
98
+ display: "block",
99
+ inlineSize: "full",
100
+ objectFit: "cover"
101
+ },
102
+ root: {
103
+ _highContrast: {
104
+ outlineColor: "CanvasText",
105
+ outlineOffset: "calc({borderWidths.hairline} * -1)",
106
+ outlineStyle: "solid",
107
+ outlineWidth: "hairline"
108
+ },
109
+ alignItems: "center",
110
+ blockSize: `var(${SIZE})`,
111
+ colorPalette: "neutral",
112
+ display: "inline-flex",
113
+ flexShrink: "0",
114
+ fontWeight: "medium",
115
+ inlineSize: `var(${SIZE})`,
116
+ justifyContent: "center",
117
+ position: "relative",
118
+ userSelect: "none",
119
+ verticalAlign: "middle"
120
+ }
121
+ },
122
+ className: "avatar",
123
+ defaultVariants: {
124
+ shape: "circle",
125
+ size: "md",
126
+ variant: "subtle"
127
+ },
128
+ jsx: [/^Avatar\.\w+$/u],
129
+ slots: [
130
+ "root",
131
+ "image",
132
+ "fallback",
133
+ "group"
134
+ ],
135
+ staticCss: [{ palette: [...PALETTES] }],
136
+ variants: {
137
+ /**
138
+ * The halo around the box, in the palette's solid at half opacity.
139
+ *
140
+ * @remarks
141
+ * `pulse` animates the halo and stops under reduced motion, for a person who is speaking.
142
+ */
143
+ effect: onSlot("root", {
144
+ glow: { layerStyle: "glow.sm" },
145
+ pulse: {
146
+ animationStyle: "pulse-glow",
147
+ boxShadowColor: "colorPalette.solid/50"
148
+ }
149
+ }),
150
+ palette: onSlot("root", paletteVariants()),
151
+ /**
152
+ * The corners of the box: a circle for a person, and a rounded or square box for a company, a
153
+ * service or a bot.
154
+ */
155
+ shape: onSlot("root", {
156
+ circle: { borderRadius: "full" },
157
+ rounded: { borderRadius: "l2" },
158
+ square: { borderRadius: "none" }
159
+ }),
160
+ /**
161
+ * The side of the box and the initials in the label style of that size.
162
+ *
163
+ * @remarks
164
+ * `2xs` is the middle tag height with the smallest text style, because the control scale and
165
+ * the label role start at `xs`.
166
+ */
167
+ size: onSlots({
168
+ fallback: {
169
+ "2xs": { textStyle: "2xs" },
170
+ ...sizeVariants((size) => ({ textStyle: `label.${size}` }), SIZES)
171
+ },
172
+ root: {
173
+ "2xs": { [SIZE]: dense("{sizes.tag.md}") },
174
+ ...sizeVariants((size) => ({ [SIZE]: dense(`{sizes.control.${size}}`) }), SIZES)
175
+ }
176
+ }),
177
+ variant: onSlot("root", flatVariants([
178
+ "solid",
179
+ "subtle",
180
+ "surface",
181
+ "outline"
182
+ ]))
183
+ }
184
+ });
185
+ //#endregion
186
+ //#region src/avatar/avatar-badge.recipe.ts
187
+ /**
188
+ * Declares the avatar badge's recipe: a mark pinned to one corner of an avatar, half over the
189
+ * picture, which is a status dot when empty and grows around a count, an emoji or an icon.
190
+ *
191
+ * @remarks
192
+ * The badge centres on the point where the avatar's diagonal crosses its rim, 14.64% of the side
193
+ * in from the corner, and a rounded or square avatar takes the same point. An empty badge is 28%
194
+ * of the avatar's side and never under 8px. A badge with content is 40% of the side and never
195
+ * under 16px, its text is 60% of its own height, and a longer count widens it into a pill. The
196
+ * ring is an outline in `bg.panel`, which cuts the badge away from the picture behind it. Under
197
+ * forced colors the badge keeps its palette, and its label states the same thing in words.
198
+ */
199
+ /**
200
+ * Custom property the badge sets to its own side.
201
+ */
202
+ const BADGE = "--avatar-badge-size";
203
+ /**
204
+ * Distance from the avatar's edges to the point where its diagonal crosses its rim.
205
+ */
206
+ const RIM = `calc(var(${SIZE}) * 0.1464)`;
207
+ /**
208
+ * Styles a neutral dot in the solid look on the avatar's bottom end corner.
209
+ */
210
+ const recipe$4 = defineRecipe({
211
+ base: {
212
+ _highContrast: { forcedColorAdjust: "none" },
213
+ "&:not(:empty)": {
214
+ [BADGE]: `max({sizes.4}, calc(var(${SIZE}) * 0.4))`,
215
+ fontSize: `calc(var(${BADGE}) * 0.6)`,
216
+ paddingInline: `calc(var(${BADGE}) * 0.2)`
217
+ },
218
+ "& > svg": {
219
+ blockSize: "75%",
220
+ inlineSize: "75%"
221
+ },
222
+ alignItems: "center",
223
+ [BADGE]: `max({sizes.2}, calc(var(${SIZE}) * 0.28))`,
224
+ blockSize: `var(${BADGE})`,
225
+ borderRadius: "full",
226
+ display: "inline-flex",
227
+ fontVariantNumeric: "tabular-nums",
228
+ fontWeight: "semibold",
229
+ justifyContent: "center",
230
+ lineHeight: "1",
231
+ minInlineSize: `var(${BADGE})`,
232
+ outlineColor: "bg.panel",
233
+ outlineStyle: "solid",
234
+ outlineWidth: "control",
235
+ position: "absolute",
236
+ whiteSpace: "nowrap"
237
+ },
238
+ className: "avatar-badge",
239
+ defaultVariants: {
240
+ palette: "neutral",
241
+ placement: "bottom-end",
242
+ variant: "solid"
243
+ },
244
+ jsx: [/^Avatar\.Badge$/u],
245
+ staticCss: [{ palette: [...PALETTES] }],
246
+ variants: {
247
+ palette: paletteVariants(),
248
+ /**
249
+ * The corner the badge is pinned to, in the direction of the text.
250
+ */
251
+ placement: {
252
+ "bottom-end": {
253
+ _rtl: { translate: "-50% 50%" },
254
+ insetBlockEnd: RIM,
255
+ insetInlineEnd: RIM,
256
+ translate: "50% 50%"
257
+ },
258
+ "bottom-start": {
259
+ _rtl: { translate: "50% 50%" },
260
+ insetBlockEnd: RIM,
261
+ insetInlineStart: RIM,
262
+ translate: "-50% 50%"
263
+ },
264
+ "top-end": {
265
+ _rtl: { translate: "-50% -50%" },
266
+ insetBlockStart: RIM,
267
+ insetInlineEnd: RIM,
268
+ translate: "50% -50%"
269
+ },
270
+ "top-start": {
271
+ _rtl: { translate: "50% -50%" },
272
+ insetBlockStart: RIM,
273
+ insetInlineStart: RIM,
274
+ translate: "-50% -50%"
275
+ }
276
+ },
277
+ variant: flatVariants(["solid", "subtle"])
278
+ }
279
+ });
280
+ //#endregion
281
+ //#region src/carousel/recipe.ts
282
+ /**
283
+ * Styles a carousel: the slides in a scroller that snaps a page at a time, the triggers that move a
284
+ * page, the dots that pick one, the rotation control and the progress text.
285
+ *
286
+ * @remarks
287
+ * The machine writes the scroller's layout inline, a grid of slides a page wide with scroll
288
+ * snapping, and spaces the slides by its `spacing` option. The root passes that option a custom
289
+ * property the recipe sets to the theme's `gap.md`. A dot is a 24px target around an 8px mark in
290
+ * the palette's `border` role, which measures 3:1 against every surface. The current dot is a
291
+ * 20px pill in the palette's solid, so the page it marks differs in shape as well as in color.
292
+ * With `controls` set to `overlay`, the controls share the slides' grid cell. The triggers are at
293
+ * the sides, the dots on a `bg.panel` pill at the bottom, padded by a dot's 2px focus ring, and
294
+ * the rotation control and the progress text at the top. In a vertical carousel the machine sizes
295
+ * a slide to a share of the scroller's height, so the scroller takes the `landscape` ratio unless
296
+ * `ratio` sets another. A picture that is a slide's child covers the slide: it keeps its own
297
+ * aspect ratio where the slide is as tall as its content, and the slide crops it where the
298
+ * scroller's ratio sets the height.
299
+ */
300
+ /**
301
+ * Custom property the root passes to the machine as the gap between slides.
302
+ */
303
+ const GAP = "--carousel-gap";
304
+ /**
305
+ * Selects a part of a carousel that runs top to bottom.
306
+ */
307
+ const VERTICAL = "&[data-orientation=vertical]";
308
+ /**
309
+ * Corners the slides offer. A fully round slide would clip its picture to an ellipse.
310
+ */
311
+ const CORNERS = cornerVariants([
312
+ "l1",
313
+ "l2",
314
+ "l3"
315
+ ]);
316
+ /**
317
+ * Distance of an overlaid control from the slides' edge.
318
+ */
319
+ const EDGE = dense("{spacing.gap.sm}");
320
+ /**
321
+ * Sets a `bg.panel` pill behind an overlaid control, so it reads over any photograph.
322
+ */
323
+ const PILL = {
324
+ background: "bg.panel",
325
+ borderRadius: "full",
326
+ boxShadow: "sm",
327
+ position: "absolute"
328
+ };
329
+ /**
330
+ * Defines the carousel recipe, with the controls under the slides, neutral dots and `l3` slides by
331
+ * default.
332
+ */
333
+ const recipe$3 = defineSlotRecipe({
334
+ base: {
335
+ autoplayTrigger: { flexShrink: "0" },
336
+ control: {
337
+ alignItems: "center",
338
+ display: "flex",
339
+ gap: dense("{spacing.gap.sm}"),
340
+ justifyContent: "space-between",
341
+ [VERTICAL]: { flexDirection: "column" }
342
+ },
343
+ indicator: {
344
+ _current: {
345
+ _hover: { "&::after": { background: "colorPalette.solid.hover" } },
346
+ "&::after": {
347
+ background: "colorPalette.solid",
348
+ inlineSize: "5"
349
+ }
350
+ },
351
+ _highContrast: {
352
+ _current: { "&::after": { background: "Highlight" } },
353
+ "&::after": {
354
+ background: "CanvasText",
355
+ forcedColorAdjust: "none"
356
+ }
357
+ },
358
+ _hover: { "&::after": { background: "colorPalette.border.hover" } },
359
+ "&::after": {
360
+ background: "colorPalette.border",
361
+ blockSize: "2",
362
+ borderRadius: "full",
363
+ content: "''",
364
+ inlineSize: "2",
365
+ transitionDuration: "moderate",
366
+ transitionProperty: "inline-size, background-color"
367
+ },
368
+ alignItems: "center",
369
+ background: "transparent",
370
+ borderRadius: "full",
371
+ borderWidth: "0",
372
+ boxSize: "6",
373
+ cursor: "button",
374
+ display: "inline-flex",
375
+ flexShrink: "0",
376
+ focusRingColor: "colorPalette.focusRing",
377
+ focusVisibleRing: "inside",
378
+ justifyContent: "center",
379
+ padding: "0"
380
+ },
381
+ indicatorGroup: {
382
+ alignItems: "center",
383
+ display: "flex",
384
+ flexWrap: "wrap",
385
+ justifyContent: "center",
386
+ [VERTICAL]: { flexDirection: "column" }
387
+ },
388
+ item: {
389
+ "& > img": {
390
+ blockSize: "full",
391
+ display: "block",
392
+ inlineSize: "full",
393
+ objectFit: "cover"
394
+ },
395
+ overflow: "hidden",
396
+ position: "relative"
397
+ },
398
+ itemGroup: {
399
+ focusRingColor: "colorPalette.focusRing",
400
+ focusVisibleRing: "outside",
401
+ [VERTICAL]: {
402
+ aspectRatio: "landscape",
403
+ flex: "1",
404
+ minInlineSize: "0"
405
+ }
406
+ },
407
+ nextTrigger: { flexShrink: "0" },
408
+ prevTrigger: { flexShrink: "0" },
409
+ progressText: {
410
+ color: "fg.muted",
411
+ fontVariantNumeric: "tabular-nums",
412
+ textStyle: "label.sm",
413
+ whiteSpace: "nowrap"
414
+ },
415
+ root: {
416
+ [GAP]: dense("{spacing.gap.md}"),
417
+ gap: dense("{spacing.gap.sm}"),
418
+ minInlineSize: "0",
419
+ position: "relative"
420
+ }
421
+ },
422
+ className: "carousel",
423
+ defaultVariants: {
424
+ controls: "outside",
425
+ palette: "neutral",
426
+ radius: "l3"
427
+ },
428
+ jsx: [/^Carousel(\.\w+)?$/u],
429
+ slots: [
430
+ "root",
431
+ "itemGroup",
432
+ "item",
433
+ "control",
434
+ "nextTrigger",
435
+ "prevTrigger",
436
+ "indicatorGroup",
437
+ "indicator",
438
+ "autoplayTrigger",
439
+ "progressText"
440
+ ],
441
+ variants: {
442
+ /**
443
+ * Where the controls go: beside the slides in the root's flow, or over the slides.
444
+ *
445
+ * @remarks
446
+ * Beside the slides, the root is a flex column, a row in a vertical carousel. Over the
447
+ * slides, the root is a grid whose first cell contains both the scroller and the control, so
448
+ * the control covers the slides exactly and any other child takes the next row. The control
449
+ * passes presses through, and each control in it takes them.
450
+ */
451
+ controls: {
452
+ outside: { root: {
453
+ display: "flex",
454
+ flexDirection: "column",
455
+ [VERTICAL]: { flexDirection: "row" }
456
+ } },
457
+ overlay: {
458
+ autoplayTrigger: {
459
+ insetBlockStart: EDGE,
460
+ insetInlineStart: EDGE,
461
+ position: "absolute"
462
+ },
463
+ control: {
464
+ "& > *": { pointerEvents: "auto" },
465
+ gridArea: "1 / 1 / 2 / 2",
466
+ padding: EDGE,
467
+ pointerEvents: "none",
468
+ position: "relative"
469
+ },
470
+ indicatorGroup: {
471
+ ...PILL,
472
+ insetBlockEnd: EDGE,
473
+ left: "50%",
474
+ padding: "0.5",
475
+ translate: "-50% 0",
476
+ [VERTICAL]: {
477
+ insetBlockEnd: "auto",
478
+ insetInlineEnd: EDGE,
479
+ left: "auto",
480
+ top: "50%",
481
+ translate: "0 -50%"
482
+ }
483
+ },
484
+ itemGroup: { gridArea: "1 / 1 / 2 / 2" },
485
+ progressText: {
486
+ ...PILL,
487
+ color: "fg",
488
+ insetBlockStart: EDGE,
489
+ insetInlineEnd: EDGE,
490
+ paddingBlock: "1",
491
+ paddingInline: dense("{spacing.gap.sm}")
492
+ },
493
+ root: { display: "grid" }
494
+ }
495
+ },
496
+ /**
497
+ * Palette of the dots.
498
+ */
499
+ palette: onSlot("indicator", paletteVariants()),
500
+ /**
501
+ * Corner radius token of the slides and of the scroller's focus ring.
502
+ */
503
+ radius: onSlots({
504
+ item: CORNERS,
505
+ itemGroup: CORNERS
506
+ }),
507
+ /**
508
+ * Aspect ratio token of the scroller, whose height a vertical carousel needs. A horizontal
509
+ * carousel without it is as tall as its slides.
510
+ */
511
+ ratio: onSlot("itemGroup", ratioVariants())
512
+ }
513
+ });
514
+ //#endregion
515
+ //#region src/iframe/recipe.ts
516
+ /**
517
+ * Styles an `iframe` as a box of one of the theme's ratios, as wide as its container.
518
+ *
519
+ * @remarks
520
+ * The ratio holds the box's shape before the framed document loads, so nothing below it moves
521
+ * when it does. The box is `bg.panel` with a hairline edge, which bounds a framed page with a
522
+ * white ground on a white page. The recipe sets no focus ring. Firefox and Chromium match no
523
+ * focus pseudo-class on an `iframe` while its document has focus, as the HTML Standard asks for a
524
+ * navigable container.
525
+ */
526
+ /**
527
+ * Styles a 16:9 frame with the `l3` corners.
528
+ */
529
+ const recipe$2 = defineRecipe({
530
+ base: {
531
+ background: "bg.panel",
532
+ blockSize: "auto",
533
+ borderColor: "border",
534
+ borderStyle: "solid",
535
+ borderWidth: "hairline",
536
+ display: "block",
537
+ inlineSize: "full",
538
+ maxInlineSize: "full"
539
+ },
540
+ className: "iframe",
541
+ defaultVariants: {
542
+ radius: "l3",
543
+ ratio: "video"
544
+ },
545
+ jsx: [/^Iframe$/u],
546
+ variants: {
547
+ /**
548
+ * Corner radius token. The axis leaves out `full`, because a framed document's content reaches
549
+ * the frame's corners and a fully round corner clips it.
550
+ */
551
+ radius: cornerVariants([
552
+ "l1",
553
+ "l2",
554
+ "l3"
555
+ ]),
556
+ /**
557
+ * Aspect ratio token the frame keeps before and after its document loads.
558
+ */
559
+ ratio: ratioVariants()
560
+ }
561
+ });
562
+ //#endregion
563
+ //#region src/image-cropper/recipe.ts
564
+ /**
565
+ * Styles an image cropper: a picture in a viewport, the selection over the part it keeps with the
566
+ * rest dimmed, the handles that resize the selection, and the thirds shown while it moves.
567
+ *
568
+ * @remarks
569
+ * The picture sets the viewport's shape: the image spans the viewport's width at its own ratio,
570
+ * because the machine writes `object-fit: fill` on it and measures the image's box for the crop
571
+ * it exports. The selection's box is the crop. Its line is a `bg.panel` hairline border and a
572
+ * `border.emphasized` inset hairline, so one of them contrasts with any photograph in either
573
+ * mode. The line's middle is the selection's padding edge, where the machine centres every
574
+ * handle. A spread shadow in `bg.backdrop` dims the viewport outside the crop. The focus ring is
575
+ * one color, so a `bg.panel` spread fills the gap under it and the ring contrasts with that band
576
+ * over any photograph. Forced colors drop shadows and paint the border in `CanvasText`, and the
577
+ * handles' dots keep a transparent hairline border for the same reason. A handle is a 24px
578
+ * target, and its visible dot is a pseudo-element inside it. The viewport and the selection take
579
+ * the `drag` cursor, because a press on either moves the crop or pans the picture, and the
580
+ * selection takes `dragging` while it moves.
581
+ */
582
+ /**
583
+ * Class name of the recipe, from which the binding writes each part's class.
584
+ */
585
+ const CLASS = "image-cropper";
586
+ /**
587
+ * Paints the inner hairline of the selection's line.
588
+ */
589
+ const LINE = "inset 0 0 0 {borderWidths.hairline} {colors.border.emphasized}";
590
+ /**
591
+ * Fills the gap between the selection and its focus ring with `bg.panel`, so the ring contrasts
592
+ * with that band over any photograph.
593
+ */
594
+ const BAND = "0 0 0 {spacing.ring} {colors.bg.panel}";
595
+ /**
596
+ * Dims the viewport outside the selection.
597
+ */
598
+ const DIM = "0 0 0 100vmax {colors.bg.backdrop}";
599
+ /**
600
+ * Defines the image cropper recipe, with the `l2` corners on the viewport by default.
601
+ */
602
+ const recipe$1 = defineSlotRecipe({
603
+ base: {
604
+ grid: {
605
+ "&:is([data-dragging], [data-panning])": { opacity: "1" },
606
+ "&[data-axis=horizontal]": { borderBlockWidth: "hairline" },
607
+ "&[data-axis=vertical]": { borderInlineWidth: "hairline" },
608
+ borderColor: "bg.panel",
609
+ borderStyle: "solid",
610
+ borderWidth: "0",
611
+ opacity: "0",
612
+ transitionDuration: "moderate",
613
+ transitionProperty: "opacity"
614
+ },
615
+ handle: {
616
+ "&::after": {
617
+ background: "bg.panel",
618
+ borderColor: "transparent",
619
+ borderRadius: "full",
620
+ borderStyle: "solid",
621
+ borderWidth: "hairline",
622
+ boxShadow: "0 0 0 {borderWidths.hairline} {colors.border.emphasized}",
623
+ boxSize: "2.5",
624
+ boxSizing: "border-box",
625
+ content: "''"
626
+ },
627
+ "&[data-disabled]": { display: "none" },
628
+ alignItems: "center",
629
+ blockSize: "6",
630
+ ["&:is([data-position=ne], [data-position=nw], [data-position=se], [data-position=sw])"]: { "&::after": { boxSize: "3" } },
631
+ display: "flex",
632
+ inlineSize: "6",
633
+ justifyContent: "center",
634
+ touchAction: "none"
635
+ },
636
+ image: {
637
+ blockSize: "auto",
638
+ display: "block",
639
+ inlineSize: "full",
640
+ maxInlineSize: "none"
641
+ },
642
+ root: {
643
+ display: "block",
644
+ inlineSize: "full",
645
+ position: "relative"
646
+ },
647
+ selection: {
648
+ _focusVisible: { boxShadow: [
649
+ LINE,
650
+ BAND,
651
+ DIM
652
+ ].join(", ") },
653
+ "&:is([data-dragging], [data-panning])": { cursor: "dragging" },
654
+ "&[data-shape=circle]": { borderRadius: "full" },
655
+ borderColor: "bg.panel",
656
+ borderStyle: "solid",
657
+ borderWidth: "hairline",
658
+ boxShadow: [LINE, DIM].join(", "),
659
+ boxSizing: "border-box",
660
+ cursor: "drag",
661
+ focusRingColor: "colorPalette.focusRing",
662
+ focusVisibleRing: "outside"
663
+ },
664
+ viewport: {
665
+ background: "bg.emphasized",
666
+ cursor: "drag",
667
+ display: "block"
668
+ }
669
+ },
670
+ className: CLASS,
671
+ defaultVariants: { radius: "l2" },
672
+ jsx: [/^ImageCropper(\.\w+)?$/u],
673
+ slots: [
674
+ "root",
675
+ "viewport",
676
+ "image",
677
+ "selection",
678
+ "handle",
679
+ "grid"
680
+ ],
681
+ variants: {
682
+ /**
683
+ * Corner radius token of the viewport. The axis leaves out `full`, because a fully round
684
+ * viewport clips the selection's corners and the handles on them.
685
+ */
686
+ radius: onSlot("viewport", cornerVariants([
687
+ "l1",
688
+ "l2",
689
+ "l3"
690
+ ])) }
691
+ });
692
+ //#endregion
693
+ //#region src/video/recipe.ts
694
+ /**
695
+ * Styles a video: a block of a theme aspect ratio that keeps its shape before the clip's metadata
696
+ * loads.
697
+ *
698
+ * @remarks
699
+ * A `video` reports its size only once its metadata loads, so without a ratio it is 0 pixels tall
700
+ * and grows mid-load, which moves everything below it. The ratio defaults to `video`, 16:9. The
701
+ * background is `bg.emphasized`, which shows while the poster loads and fills the bars of the
702
+ * `contain` fit. A hairline edge bounds pale footage on a light page. The recipe has no `palette`
703
+ * axis, because the clip fills the element, and no `effect` axis, because a glow in a palette has
704
+ * no relation to the clip.
705
+ */
706
+ /**
707
+ * Defines the video recipe, which defaults to a 16:9 clip that fills its shape inside `l3` corners.
708
+ */
709
+ const recipe = defineRecipe({
710
+ base: {
711
+ background: "bg.emphasized",
712
+ blockSize: "auto",
713
+ borderColor: "border",
714
+ borderStyle: "solid",
715
+ borderWidth: "hairline",
716
+ display: "block",
717
+ focusRingColor: "colorPalette.focusRing",
718
+ focusVisibleRing: "outside",
719
+ inlineSize: "full",
720
+ maxInlineSize: "full"
721
+ },
722
+ className: "video",
723
+ defaultVariants: {
724
+ fit: "cover",
725
+ radius: "l3",
726
+ ratio: "video"
727
+ },
728
+ jsx: [/^Video$/u],
729
+ variants: {
730
+ /**
731
+ * Object fit of the clip. `cover` crops it to the shape and `contain` shows the whole clip
732
+ * between bars.
733
+ */
734
+ fit: {
735
+ contain: { objectFit: "contain" },
736
+ cover: { objectFit: "cover" }
737
+ },
738
+ /**
739
+ * Corner radius token.
740
+ */
741
+ radius: cornerVariants(),
742
+ /**
743
+ * Aspect ratio token the element keeps before and after the metadata loads.
744
+ */
745
+ ratio: ratioVariants()
746
+ }
747
+ });
748
+ //#endregion
749
+ export { recipe$3 as a, recipe$6 as c, GAP as i, recipe$1 as n, recipe$4 as o, recipe$2 as r, recipe$5 as s, recipe as t };