@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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Stealth Scale B.V.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,487 @@
1
+ # @stealthscale/component-media
2
+
3
+ The package renders pictures, clips, sound and embedded documents through six components: the
4
+ avatar, the carousel, the image cropper, the video, the audio element and the iframe. Each component
5
+ binds a recipe, so a theme restyles it by extending the recipe. The preset under `./theme` registers
6
+ the recipes with an application's compiler.
7
+
8
+ ## Install
9
+
10
+ ```bash
11
+ pnpm add @stealthscale/component-media
12
+ ```
13
+
14
+ The package peers on `react`, `@stealthscale/theme`, `@stealthscale/hooks` and
15
+ `@stealthscale/component-actions`, whose `Button` the carousel's triggers are. It depends on
16
+ `@zag-js/avatar`, `@zag-js/carousel` and `@zag-js/image-cropper`, the Zag machines behind the
17
+ avatar, the carousel and the image cropper. The video, the audio element and the iframe render the
18
+ browser's own elements.
19
+
20
+ ## Avatar
21
+
22
+ A box with a person's picture, or with their initials while the picture is missing, loading or
23
+ broken. The root runs the Zag avatar machine. The picture is hidden until it loads, and the initials
24
+ are hidden only once it has, so a slow or broken picture never leaves an empty box.
25
+
26
+ ```tsx
27
+ import { Avatar } from "@stealthscale/component-media";
28
+
29
+ <Avatar.Root name="Ada Okafor">
30
+ <Avatar.Fallback />
31
+ <Avatar.Image src="/people/ada.webp" />
32
+ </Avatar.Root>;
33
+ ```
34
+
35
+ | Part | Element | What it renders |
36
+ | ---------- | ------- | ---------------------------------------------------------------------------------- |
37
+ | `Root` | `span` | The box. It takes the name, the machine options and the variants |
38
+ | `Image` | `img` | The picture, which covers the box once it loads |
39
+ | `Fallback` | `span` | Its children, or the initials of the root's name |
40
+ | `Badge` | `span` | A dot, a count, an emoji or an icon pinned to a corner of the box |
41
+ | `Group` | `span` | A row of avatars that overlap. It gives its variants to every avatar that has none |
42
+
43
+ | Axis | Values | Default |
44
+ | --------- | ------------------------------------------------------------------ | -------- |
45
+ | `size` | `2xs`, `xs`, `sm`, `md`, `lg`, `xl`, `2xl` | `md` |
46
+ | `shape` | `circle`, `rounded`, `square` | `circle` |
47
+ | `variant` | `solid`, `subtle`, `surface`, `outline` | `subtle` |
48
+ | `palette` | `primary`, `secondary`, `accent`, `neutral`, and the four statuses | neutral |
49
+ | `effect` | `glow`, `pulse` | none |
50
+
51
+ The side of the box is a control height, so an avatar is as tall as a button of the same size: 32,
52
+ 36, 40, 44, 48 and 56px from `xs` to `2xl`. `2xs` is 24px, for a byline or a list row. The initials
53
+ take the label style of that size.
54
+
55
+ The initials are the first letter of the first word and of the last word of `name`. A letter is a
56
+ grapheme cluster, so an accented or composed letter is kept whole. Pass children to `Fallback` for
57
+ anything else: an icon for a service, or a count for a group.
58
+
59
+ ### Accessibility
60
+
61
+ - With `name`, the root is an image named by the name: `role="img"` with `aria-label`. Without
62
+ `name`, the root has no role. Name the picture through `alt` on `Image` instead.
63
+ - `Image` sets `alt` to an empty string by default, because the root's `name` is the avatar's
64
+ accessible name.
65
+ - Beside the same name in words, set `aria-hidden` on the root, so a screen reader reads the name
66
+ once.
67
+
68
+ ```tsx
69
+ <Text>
70
+ <Avatar.Root aria-hidden name="Ada Okafor" size="2xs">
71
+ <Avatar.Fallback />
72
+ <Avatar.Image src="/people/ada.webp" />
73
+ </Avatar.Root>{" "}
74
+ Ada Okafor commented 2 hours ago
75
+ </Text>
76
+ ```
77
+
78
+ ### Badges
79
+
80
+ `Avatar.Badge` goes inside `Avatar.Root` and is pinned to one of its corners. Without children it is
81
+ a status dot. With a count, an emoji or an icon it grows around its content.
82
+
83
+ ```tsx
84
+ <Avatar.Root name="Ada Okafor">
85
+ <Avatar.Fallback />
86
+ <Avatar.Image src="/people/ada.webp" />
87
+ <Avatar.Badge label="Online" palette="success" />
88
+ </Avatar.Root>;
89
+ <Avatar.Root name="Bram Voss">
90
+ <Avatar.Fallback />
91
+ <Avatar.Badge label="3 unread messages" palette="error" placement="top-end">
92
+ 3
93
+ </Avatar.Badge>
94
+ </Avatar.Root>;
95
+ <Avatar.Root name="Cleo Han">
96
+ <Avatar.Fallback />
97
+ <Avatar.Badge label="On holiday" variant="subtle">
98
+ 🌴
99
+ </Avatar.Badge>
100
+ </Avatar.Root>;
101
+ ```
102
+
103
+ | Axis | Values | Default |
104
+ | ----------- | ------------------------------------------------------------------ | ------------ |
105
+ | `placement` | `top-start`, `top-end`, `bottom-start`, `bottom-end` | `bottom-end` |
106
+ | `palette` | `primary`, `secondary`, `accent`, `neutral`, and the four statuses | neutral |
107
+ | `variant` | `solid`, `subtle` | `solid` |
108
+
109
+ The badge centres on the point where the avatar's diagonal crosses its rim, so half of it covers the
110
+ picture. An empty badge is a dot, 28% of the avatar's side and never under 8px. Content makes it 40%
111
+ of the side and never under 16px, and a longer count widens it into a pill. An outline in `bg.panel`
112
+ cuts it away from the picture. The badge takes its own palette and look, apart from the avatar's.
113
+
114
+ With `label`, the badge is an image named by the label, and its ID joins the avatar's
115
+ `aria-describedby`. A named avatar is an image whose children are not read, so a screen reader reads
116
+ the label as the avatar's description, after the name. Without `label` the badge is hidden from
117
+ screen readers. Leave it out only where the same state is written beside the avatar in words.
118
+
119
+ ### Groups
120
+
121
+ `Avatar.Group` overlaps each avatar with the one before it by a quarter of its side, and rings each
122
+ avatar in the panel's ground. Show the people left out as a last avatar with a count, named for
123
+ them:
124
+
125
+ ```tsx
126
+ <Avatar.Group size="sm">
127
+ {people.map((person) => (
128
+ <Avatar.Root key={person.id} name={person.name}>
129
+ <Avatar.Fallback />
130
+ <Avatar.Image src={person.picture} />
131
+ </Avatar.Root>
132
+ ))}
133
+ <Avatar.Root name="3 more">
134
+ <Avatar.Fallback>+3</Avatar.Fallback>
135
+ </Avatar.Root>
136
+ </Avatar.Group>
137
+ ```
138
+
139
+ ### Machine options
140
+
141
+ The root takes the machine's `id`, `ids` and `onStatusChange` options. `onStatusChange` reports
142
+ `loaded` when the picture loads and `error` when it fails. To load another picture, change `src` on
143
+ `Image`. The machine's `setSrc`, `setLoaded` and `setError` methods are not offered.
144
+
145
+ ### Not offered
146
+
147
+ - A default icon in the fallback. A component takes its glyphs from the caller, so a caller passes
148
+ the icon as the fallback's children.
149
+ - A color picked from the name. Set `palette` per avatar.
150
+
151
+ ## Carousel
152
+
153
+ Slides in a scroller that snaps a page at a time. Triggers move a page, dots pick one, and a
154
+ rotation control starts and stops a carousel that plays by itself. The root runs the Zag carousel
155
+ machine.
156
+
157
+ ```tsx
158
+ import { Carousel } from "@stealthscale/component-media";
159
+
160
+ <Carousel.Root aria-label="Landscapes" controls="overlay" loop slideCount={pictures.length}>
161
+ <Carousel.ItemGroup>
162
+ {pictures.map((picture, index) => (
163
+ <Carousel.Item index={index} key={picture.src}>
164
+ <img alt={picture.alt} src={picture.src} />
165
+ </Carousel.Item>
166
+ ))}
167
+ </Carousel.ItemGroup>
168
+ <Carousel.Control>
169
+ <Carousel.PrevTrigger>
170
+ <ChevronLeftIcon />
171
+ </Carousel.PrevTrigger>
172
+ <Carousel.IndicatorGroup>
173
+ <Carousel.Indicators />
174
+ </Carousel.IndicatorGroup>
175
+ <Carousel.NextTrigger>
176
+ <ChevronRightIcon />
177
+ </Carousel.NextTrigger>
178
+ </Carousel.Control>
179
+ </Carousel.Root>;
180
+ ```
181
+
182
+ | Part | Element | What it renders |
183
+ | ------------------- | -------- | ------------------------------------------------------------------------ |
184
+ | `Root` | `div` | The region the machine runs. It takes the options and the variants |
185
+ | `ItemGroup` | `div` | The scroller that snaps a page at a time |
186
+ | `Item` | `div` | The slide at `index`. A picture that is its child covers it |
187
+ | `Control` | `div` | The row of controls, beside the slides or over them |
188
+ | `PrevTrigger` | `button` | The library's square `Button`, which moves to the previous page |
189
+ | `NextTrigger` | `button` | The library's square `Button`, which moves to the next page |
190
+ | `IndicatorGroup` | `div` | The group of dots |
191
+ | `Indicator` | `button` | A dot, which moves to the page at `index` |
192
+ | `Indicators` | none | A dot per page |
193
+ | `AutoplayTrigger` | `button` | The rotation control, the library's square `Button` |
194
+ | `AutoplayIndicator` | none | The caller's `play` or `pause` glyph, whichever action the control takes |
195
+ | `ProgressText` | `span` | The current page over the number of pages, "2 / 5" |
196
+
197
+ | Axis | Values | Default |
198
+ | ---------- | ------------------------------------------------------------------ | --------- |
199
+ | `controls` | `outside`, `overlay` | `outside` |
200
+ | `palette` | `primary`, `secondary`, `accent`, `neutral`, and the four statuses | neutral |
201
+ | `radius` | `l1`, `l2`, `l3` | `l3` |
202
+ | `ratio` | the theme's seven aspect ratios | none |
203
+
204
+ - `controls="outside"` lays the controls in a row under the slides, or beside them in a vertical
205
+ carousel. `controls="overlay"` puts the triggers at the sides of the slides and the dots on a
206
+ `bg.panel` pill at the bottom, and the triggers take the `surface` look.
207
+ - A dot is a 24px target around an 8px mark in the palette's `border` role. The current page's dot
208
+ is a 20px pill in the palette's solid.
209
+ - `ratio` sets the scroller's shape. A vertical carousel needs a height, so its scroller takes the
210
+ `landscape` ratio unless `ratio` sets another.
211
+
212
+ ### Options
213
+
214
+ The root takes every option of the Zag carousel machine except `translations`: `slideCount`
215
+ (required), `page`, `defaultPage`, `onPageChange`, `orientation`, `slidesPerPage` (1),
216
+ `slidesPerMove` (`auto`, the slides of a page), `spacing`, `padding`, `loop`, `autoplay` (`true` for
217
+ every 4000ms, or `{ delay }`), `allowMouseDrag`, `snapType`, `autoSize`, `inViewThreshold` (0.6),
218
+ `onDragStatusChange`, `onAutoplayStatusChange`, `dir`, `id` and `ids`.
219
+
220
+ - `spacing` defaults to the theme's `gap.md` between slides.
221
+ - `loop` defaults to `true` while `autoplay` is set, and to `false` otherwise.
222
+ - `page` and `onPageChange` control the page from outside the root.
223
+
224
+ ### Rotation
225
+
226
+ The rotation follows the APG carousel pattern.
227
+
228
+ - `autoplay` starts the rotation, unless the reader asks the system for reduced motion.
229
+ - Keyboard focus that enters the carousel stops the rotation, and it stays stopped until the reader
230
+ starts it with the rotation control.
231
+ - A pointer over the carousel pauses the rotation until it leaves, unless the reader started the
232
+ rotation with the control.
233
+ - The rotation control's name states its action, "Stop slide rotation" or "Start slide rotation",
234
+ and follows the reader's choice rather than a pause under the pointer. Pass `startLabel` and
235
+ `stopLabel` in the reader's language, and put the control first among the carousel's controls.
236
+
237
+ ### Accessibility
238
+
239
+ - The machine makes the root a `region` with the role description "carousel". Name it with
240
+ `aria-label`.
241
+ - The machine makes a slide a `group` with the role description "slide", named "1 of 5" unless the
242
+ slide takes `aria-label` or `aria-labelledby`. A slide out of the scroller's view is `inert`, so
243
+ its controls leave the tab order.
244
+ - The scroller is a polite live region while the carousel does not rotate. It is in the tab order
245
+ while no slide holds a control. While it has focus, the arrow keys of its orientation, Home and
246
+ End move a page, swapped under `dir="rtl"`.
247
+ - While a dot has focus, the same keys move a page and focus moves to the new page's dot. The
248
+ current page's dot sets `aria-disabled`, as the APG pattern marks the picker of the slide in view.
249
+ - The triggers are named "Previous slide" and "Next slide" unless they take `label`. At an end of a
250
+ carousel that does not loop, a trigger sets `aria-disabled` and keeps focus.
251
+ - Under reduced motion, a trigger, a dot or a key moves the page at once.
252
+
253
+ ### Machine behaviour
254
+
255
+ - The machine's own keys on the scroller are switched off. They page on Left and Right only, in a
256
+ vertical carousel too, ignore `dir`, and act while focus is on a control inside a slide.
257
+ - The machine ignores a drag that starts while the scroller has focus. The carousel starts it, as it
258
+ does while the scroller has no focus.
259
+
260
+ ### Not offered
261
+
262
+ - The machine's `translations`. The parts take their words as props, with the machine's English as
263
+ the default.
264
+ - A provider that runs the machine outside the root. `page` and `onPageChange` control the page from
265
+ outside.
266
+
267
+ ## Image cropper
268
+
269
+ A picture in a viewport with a selection over the part to keep. A pointer drags the selection and
270
+ resizes it by its handles, and the arrow keys move it. The picture zooms, turns, flips and pans
271
+ under the selection. The application starts the machine with `ImageCropper.useImageCropper` and
272
+ passes the api to the root, so its own controls outside the root, such as a zoom button or a save
273
+ button, call the same api.
274
+
275
+ ```tsx
276
+ import { ImageCropper } from "@stealthscale/component-media";
277
+
278
+ function ProfileCropper() {
279
+ const cropper = ImageCropper.useImageCropper({ aspectRatio: 1, cropShape: "circle" });
280
+
281
+ return (
282
+ <ImageCropper.Root aria-label="Crop your profile picture" cropper={cropper}>
283
+ <ImageCropper.Viewport>
284
+ <ImageCropper.Image src="/people/ada.webp" />
285
+ <ImageCropper.Selection>
286
+ {ImageCropper.handles.map((position) => (
287
+ <ImageCropper.Handle key={position} position={position} />
288
+ ))}
289
+ <ImageCropper.Grid axis="horizontal" />
290
+ <ImageCropper.Grid axis="vertical" />
291
+ </ImageCropper.Selection>
292
+ </ImageCropper.Viewport>
293
+ </ImageCropper.Root>
294
+ );
295
+ }
296
+ ```
297
+
298
+ | Part | Element | What it renders |
299
+ | ----------- | ------- | ------------------------------------------------------------------------------------- |
300
+ | `Root` | `div` | The group. It takes `cropper`, the description's words and the variants |
301
+ | `Viewport` | `div` | The box that clips the picture. A press outside the selection pans the picture |
302
+ | `Image` | `img` | The picture, as wide as the viewport at its own ratio |
303
+ | `Selection` | `div` | The crop, a slider in the tab order, with the picture outside it dimmed |
304
+ | `Handle` | `div` | A handle that resizes the selection from the edge or corner `position` names |
305
+ | `Grid` | `div` | Two lines of thirds along `axis`, shown while the selection moves or the picture pans |
306
+
307
+ `ImageCropper.handles` lists the eight positions, from `nw` clockwise to `w`.
308
+
309
+ | Axis | Values | Default |
310
+ | -------- | ---------------- | ------- |
311
+ | `radius` | `l1`, `l2`, `l3` | `l2` |
312
+
313
+ The picture sets the viewport's shape. The image spans the viewport's width at its own ratio,
314
+ because the machine measures the image's box for the crop it exports. The axis leaves out `full`,
315
+ because a fully round viewport clips the selection's corners and their handles.
316
+
317
+ ### Options
318
+
319
+ `useImageCropper` takes every option of the Zag image cropper machine except `translations`.
320
+
321
+ | Option | Default | What it sets |
322
+ | ------------------------------------------------------------------ | ----------- | ------------------------------------------------------------------ |
323
+ | `aspectRatio` | none | The crop's width over its height |
324
+ | `cropShape` | `rectangle` | `circle` for a round crop in a square |
325
+ | `initialCrop` | see below | The crop in the viewport's pixels once the picture loads |
326
+ | `minWidth`, `minHeight` | 40 | The smallest crop in pixels |
327
+ | `maxWidth`, `maxHeight` | none | The largest crop in pixels |
328
+ | `fixedCropArea` | `false` | Keeps the crop in place. A drag and the arrow keys pan the picture |
329
+ | `defaultZoom`, `zoom` | 1 | The zoom, uncontrolled or controlled |
330
+ | `minZoom`, `maxZoom` | 1, 5 | The zoom's range |
331
+ | `zoomStep` | 0.1 | The zoom step of the plus and minus keys and of the wheel |
332
+ | `zoomSensitivity` | 2 | The exponent a pinch's scale is raised to |
333
+ | `defaultRotation`, `rotation` | 0 | The rotation in degrees, uncontrolled or controlled |
334
+ | `defaultFlip`, `flip` | no flip | The horizontal and the vertical flip |
335
+ | `nudgeStep`, `nudgeStepShift`, `nudgeStepCtrl` | 1, 10, 50 | The arrow keys' step in pixels: alone, with Shift and with Control |
336
+ | `onCropChange`, `onZoomChange`, `onRotationChange`, `onFlipChange` | none | Called with the new value |
337
+ | `id` | generated | The prefix of every part's ID |
338
+ | `ids`, `dir`, `getRootNode` | none | Part IDs, the direction and the root node |
339
+
340
+ Without `initialCrop`, the crop is the largest crop of the aspect ratio inside 80% of the viewport,
341
+ centred.
342
+
343
+ ### The api
344
+
345
+ - `crop`, `zoom`, `rotation`, `flip`, `offset`, `naturalSize`, `viewportRect`, `dragging` and
346
+ `panning` report the state.
347
+ - `setZoom`, `zoomBy`, `setRotation`, `rotateBy`, `setFlip`, `flipHorizontally`, `flipVertically`,
348
+ `resize(position, delta)` and `reset` change it.
349
+ - `getCropData()` returns the crop in the picture's own pixels, with the rotation and the flip.
350
+ - `getCroppedImage({ type, quality, maxSize, output })` renders the crop to a canvas. It resolves to
351
+ a `Blob`, to a data URL with `output: "dataUrl"`, or to `null` before the picture loads. A round
352
+ crop exports its square, so the caller rounds the picture it shows, as an avatar does.
353
+
354
+ ### Accessibility
355
+
356
+ - The machine makes the root a `group` named "Image cropper". Name it for the picture with
357
+ `aria-label`.
358
+ - The machine describes the zoom, the rotation and the crop in English through the root's
359
+ `aria-description`, and marks the root `aria-busy` while the picture loads. `description(details)`
360
+ returns the description in the reader's language from the crop in whole pixels, the zoom and the
361
+ rotation. `loadingDescription` replaces it while the picture loads.
362
+ - The selection is a `slider` in the tab order. The arrow keys move the crop, Alt with an arrow
363
+ moves its east or south edge, plus and minus zoom, and Shift or Control take larger steps. With
364
+ `fixedCropArea` the arrow keys pan the picture. The selection takes `aria-label`,
365
+ `aria-roledescription` and `aria-description` for its words, and `valueText(crop)` for its
366
+ `aria-valuetext`.
367
+ - The picture, the handles and the grid are hidden from assistive technology.
368
+ - Moving the crop takes a drag or the arrow keys. The api has no method that moves the crop, so an
369
+ application cannot offer a single-pointer alternative for moving it (WCAG 2.5.7). `resize` and the
370
+ zoom, rotation and flip methods each take a button.
371
+ - A handle on the viewport's edge takes a press only on its half inside the viewport, because the
372
+ viewport clips the picture and everything over it.
373
+
374
+ ### Machine behaviour
375
+
376
+ - A turn changes neither the zoom nor the crop. A quarter turn of a landscape picture in a landscape
377
+ viewport leaves empty bands beside the picture, which the crop can cover.
378
+
379
+ ### Not offered
380
+
381
+ - The machine's `translations`. The parts take their words as props, with the machine's English as
382
+ the default.
383
+
384
+ ## Video
385
+
386
+ A `video` element that keeps its shape before the clip's metadata loads, and follows three rules for
387
+ a clip that starts by itself.
388
+
389
+ ```tsx
390
+ import { Video } from "@stealthscale/component-media";
391
+
392
+ <Video controls poster="/tour.webp" preload="metadata" src="/tour.webm" />;
393
+ ```
394
+
395
+ | Axis | Values | Default |
396
+ | -------- | ------------------------------------------------------------------------- | ------- |
397
+ | `ratio` | `square`, `landscape`, `portrait`, `golden`, `video`, `wide`, `ultrawide` | `video` |
398
+ | `fit` | `cover`, `contain` | `cover` |
399
+ | `radius` | `l1`, `l2`, `l3`, `full` | `l3` |
400
+
401
+ `VideoPropsProvider` sets the variants on every video below it.
402
+
403
+ - The element keeps the ratio's shape from the first paint, 16:9 by default, so nothing below it
404
+ moves when the metadata loads. `cover` crops the clip to the shape, and `contain` shows the whole
405
+ clip between bars in `bg.emphasized`, which also shows while the poster loads. A hairline edge
406
+ bounds pale footage.
407
+ - `autoPlay` starts the clip only while the reader does not ask for reduced motion
408
+ (`prefers-reduced-motion: reduce`). The video reads the setting before the first paint and again
409
+ when it changes. A server render leaves out `autoplay`.
410
+ - A clip with `autoPlay` shows the browser's controls unless `controls={false}`. Moving content that
411
+ starts by itself and runs longer than 5 seconds needs a way to pause it (WCAG 2.2.2). A caller who
412
+ turns the controls off renders a pause button of its own, which calls the element's `pause()`
413
+ through a ref.
414
+ - A clip that starts by itself is muted and sets `playsInline`, because a browser starts only a
415
+ muted clip unasked, and a phone plays an inline clip in the page. The video keeps a caller's
416
+ `playsInline`.
417
+ - A clip with speech takes a `track` with `kind="captions"` as a child (WCAG 1.2.2).
418
+ - Not offered: a control bar of the library's own, because the browser's controls work with a
419
+ keyboard and a screen reader already. The `fill`, `none` and `scale-down` fits.
420
+
421
+ ## Audio
422
+
423
+ An `audio` element with the browser's controls, as wide as its container.
424
+
425
+ ```tsx
426
+ import { Audio } from "@stealthscale/component-media";
427
+
428
+ <Audio aria-label="Voice memo from Ada" preload="metadata" src="/memo.webm" />;
429
+ ```
430
+
431
+ The recipe has no axes. The browser draws the controls, and their colors follow the page's
432
+ `color-scheme`.
433
+
434
+ - The controls are on unless `controls={false}`. Sound that plays for more than 3 seconds needs a
435
+ way to stop it (WCAG 1.4.2), and the browser does not render an `audio` element without controls.
436
+ A caller who turns them off renders controls of its own, which call the element's `play()` and
437
+ `pause()` through a ref.
438
+ - `autoPlay` is passed as given. It starts sound, not motion, so reduced motion does not gate it,
439
+ and a browser starts sound only after the reader has interacted with the page.
440
+ - Speech needs a transcript in the page beside the player (WCAG 1.2.1), because the transcript is
441
+ the only way a reader who cannot hear the recording gets its words.
442
+ - Name the element with `aria-label` or `aria-labelledby`.
443
+ - Not offered: a control bar of the library's own, for the reason the video gives. A radius, which
444
+ clips Chromium's control panel.
445
+
446
+ ## Iframe
447
+
448
+ An `iframe` for a document the application does not control, such as a customer's email template or
449
+ a partner's widget. It is sandboxed by default and keeps one of the theme's ratios before its
450
+ document loads.
451
+
452
+ ```tsx
453
+ import { Iframe } from "@stealthscale/component-media";
454
+
455
+ <Iframe ratio="portrait" srcDoc={template} title="Preview of the March invoice email" />;
456
+ ```
457
+
458
+ | Axis | Values | Default |
459
+ | -------- | ------------------------------------------------------------------------- | ------- |
460
+ | `ratio` | `square`, `landscape`, `portrait`, `golden`, `video`, `wide`, `ultrawide` | `video` |
461
+ | `radius` | `l1`, `l2`, `l3` | `l3` |
462
+
463
+ `radius` leaves out `full`, because a framed document's content reaches the frame's corners and a
464
+ fully round corner clips it.
465
+
466
+ - `sandbox` defaults to the empty value, which denies the document scripts, forms, popups and
467
+ same-origin access. Widen it one capability at a time, such as `sandbox="allow-scripts"` for a
468
+ widget that needs scripts. A document from the page's own origin with `allow-scripts` and
469
+ `allow-same-origin` can remove its own sandbox. Never grant both to such a document.
470
+ - `referrerPolicy` defaults to `no-referrer`, so the framed origin is not told which page framed it.
471
+ - `loading` defaults to `lazy`, which lets the browser defer a frame below the fold until the reader
472
+ scrolls near it.
473
+ - The frame sets no `allow`, so it is delegated no permission the page has, such as the camera or
474
+ geolocation. Pass `allow` to delegate one.
475
+ - `title` is required, because it is the frame's accessible name. State what the frame shows.
476
+ - The box is `bg.panel` with a hairline edge until the document paints over it.
477
+
478
+ ### Focus
479
+
480
+ Firefox and Chromium give a frame's document a tab stop, and a control inside the document takes
481
+ focus as it does on a page of its own. While a frame's document has focus, both browsers match no
482
+ focus pseudo-class on the `iframe` element and fire no focus event at it. The frame draws no focus
483
+ ring. A framed document draws the rings of its own controls.
484
+
485
+ ## Licence
486
+
487
+ MIT. See [LICENSE](LICENSE).
@@ -0,0 +1 @@
1
+ {"$schema":"http://cyclonedx.org/schema/bom-1.7.schema.json","bomFormat":"CycloneDX","specVersion":"1.7","version":1,"metadata":{"lifecycles":[{"phase":"build"}],"tools":{"components":[{"type":"application","name":"rolldown","version":"1.2.11"},{"type":"application","name":"vite","version":"8.3.1"},{"type":"library","name":"core","group":"@arethetypeswrong","version":"0.18.5","bom-ref":"pkg:npm/%40arethetypeswrong/core@0.18.5","author":"Andrew Branch","description":"The API behind arethetypeswrong.github.io","hashes":[{"alg":"SHA-512","content":"f72b63cc6c318e6f54cfb23d6af7dbb79be542deae93db914444b3263aabce797a58abc8e9dc184e8faf277534d96aeafe64778aa97f3f386f1e129d2480230d"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40arethetypeswrong/core@0.18.5","externalReferences":[{"url":"https://github.com/arethetypeswrong/arethetypeswrong.github.io.git#packages/core","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=20"}]},{"type":"library","name":"component-actions","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/component-actions@0.1.0","description":"Renders what a person presses: a button, a toggle group, a swap, a color mode toggle, a clipboard and a download trigger.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/component-actions@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#components/actions","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/components/actions#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"component-layout","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/component-layout@0.1.0","description":"Arranges what is already there: the boxes, the stacks, the rules between them and the surfaces under them.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/component-layout@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#components/layout","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/components/layout#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"hooks","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/hooks@0.1.0","description":"The React hooks a component uses to read the page it draws into: how the page is read, what it measures, and what a value was on the previous render.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/hooks@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#foundations/hooks","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/foundations/hooks#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"theme","group":"@stealthscale","version":"0.3.0","bom-ref":"pkg:npm/%40stealthscale/theme@0.3.0","description":"Publishes the foundation every recipe is written against, the runtime every component binds its recipe with, and the vocabulary a theme is written in.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/theme@0.3.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#foundations/theme","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/foundations/theme#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-i18n","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-i18n@0.1.0","description":"Configures a package or an application with words: the catalogue plugin, and the words in scope for its specifications.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-i18n@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config-i18n","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config-i18n#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-react","group":"@stealthscale","version":"0.8.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-react@0.8.0","description":"Adds the JSX transform, the React lint rules, a test document and an MDX compiler to a package that renders.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-react@0.8.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config-react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config-react#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-specimen","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-specimen@0.1.0","description":"Configures an application that shows a catalogue, and the workspace root the specimens sit under.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-specimen@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config-specimen","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config-specimen#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-theme","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-theme@0.1.0","description":"Adds the runtime generator to a design-system package and the stylesheet compiler to an application.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-theme@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config-theme","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config-theme#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config-typescript","group":"@stealthscale","version":"0.3.0","bom-ref":"pkg:npm/%40stealthscale/vite-config-typescript@0.3.0","description":"Sets the TypeScript options every stealthscale package compiles with.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config-typescript@0.3.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config-typescript","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config-typescript#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"vite-config","group":"@stealthscale","version":"0.6.0","bom-ref":"pkg:npm/%40stealthscale/vite-config@0.6.0","description":"Configures a package from five Vite+ tiers and a named layer for every block.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/vite-config@0.6.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#packages/vite-config","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/packages/vite-config#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}]},{"type":"library","name":"react","group":"@testing-library","version":"16.3.3","bom-ref":"pkg:npm/%40testing-library/react@16.3.3","author":"Kent C. Dodds <me@kentcdodds.com> (https://kentcdodds.com)","description":"Simple and complete React DOM testing utilities that encourage good testing practices.","hashes":[{"alg":"SHA-512","content":"528d7ddcd8106cf333ea5aeb86d45040570c0ba45efc42cb15b6ee54bdf4583959c65a597fdfe53074c05713d12f0d6ad62bbd389991d5cd812e210d46cb5d06"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40testing-library/react@16.3.3","externalReferences":[{"url":"https://github.com/testing-library/react-testing-library/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"https://github.com/testing-library/react-testing-library","type":"vcs","comment":"as detected from PackageJson property \"repository.url\""},{"url":"https://github.com/testing-library/react-testing-library#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=18"}]},{"type":"library","name":"react-dom","group":"@types","version":"19.3.0","bom-ref":"pkg:npm/%40types/react-dom@19.3.0","description":"TypeScript definitions for react-dom","hashes":[{"alg":"SHA-512","content":"648edb538da66575ca1e7fea34b130d88adb888354ed7e7ebdf81d8b10479023696165e32a07d0fcffaec866f90a338b07d51c9d37a0debca542d5768729b8e1"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40types/react-dom@19.3.0","externalReferences":[{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped.git#types/react-dom","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react-dom","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"react","group":"@types","version":"19.3.0","bom-ref":"pkg:npm/%40types/react@19.3.0","description":"TypeScript definitions for react","hashes":[{"alg":"SHA-512","content":"374ac50ae1fd628c46f7f9bad65f4c7e924a7e62ce554d1e9bb8a9233e9346049292f45e2c1f6f2fce4607ecabf01b3995eaa9be0f7a252c05e194b5b38be242"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40types/react@19.3.0","externalReferences":[{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped.git#types/react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"happy-dom","version":"20.14.5","bom-ref":"pkg:npm/happy-dom@20.14.5","author":"David Ortner","description":"Happy DOM is a JavaScript implementation of a web browser without its graphical user interface. It includes many web standards from WHATWG DOM and HTML.","hashes":[{"alg":"SHA-512","content":"c7f47392958ee346d38c8a13df4890b6deb814b2e61ff89171408dd97fff6cbc7b1f789f91d7cf5f2aacae8fce62dab32008622cc300ee69a2391f42dcd38a8d"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/happy-dom@20.14.5","externalReferences":[{"url":"https://github.com/capricorn86/happy-dom","type":"vcs","comment":"as detected from PackageJson property \"repository\""},{"url":"https://github.com/capricorn86/happy-dom","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=20.0.0"}]},{"type":"library","name":"lucide-react","version":"1.48.0","bom-ref":"pkg:npm/lucide-react@1.48.0","author":"Eric Fennis","description":"A Lucide icon library package for React applications.","hashes":[{"alg":"SHA-512","content":"474088298fdf5e20bacbdc510400e0b0af955b6a7fa5c4c938c84b65fd53fae1bebc952fc94474da718e8262080bb30f92234ae0ec7c4039dba85f2b25858f65"}],"licenses":[{"license":{"id":"ISC"}}],"purl":"pkg:npm/lucide-react@1.48.0","externalReferences":[{"url":"https://github.com/lucide-icons/lucide/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"https://github.com/lucide-icons/lucide.git#packages/lucide-react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://lucide.dev","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"oxc-transform-react","version":"0.151.0","bom-ref":"pkg:npm/oxc-transform-react@0.151.0","author":"Boshen and oxc contributors","description":"Oxc React Compiler Node API","hashes":[{"alg":"SHA-512","content":"60ed7f62cdc5202fee060ca37b1bc6b084d05085c318d9b1272eafa906ddf1663b263ccc0f5faf095879f65cdaf7f3f4dbf4d135a517c94eceaf6c70c7f29b77"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/oxc-transform-react@0.151.0","externalReferences":[{"url":"https://github.com/oxc-project/oxc/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/oxc-project/oxc.git#napi/transform-react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://oxc.rs","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":"^20.19.0 || >=22.12.0"}]},{"type":"library","name":"react-dom","version":"19.3.0","bom-ref":"pkg:npm/react-dom@19.3.0","description":"React package for working with the DOM.","hashes":[{"alg":"SHA-512","content":"24393c76089fe753a31680c4ef4f8e4fd202c98afeebd1e59a284dc29d7e36c7db9dadede6c22209af59264b43990e3fd5bfeb9f6ea1200476b985c332be6bd1"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/react-dom@19.3.0","externalReferences":[{"url":"https://github.com/react/react/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"https://github.com/react/react.git#packages/react-dom","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://react.dev/","type":"website","comment":"as detected from PackageJson property \"homepage\""}]},{"type":"library","name":"react","version":"19.3.0","bom-ref":"pkg:npm/react@19.3.0","description":"React is a JavaScript library for building user interfaces.","hashes":[{"alg":"SHA-512","content":"13c2d471bb415addb46db9766281dfc7864305dc554df38ab42667f5c0d227897afe7ba8029729201723e3bb76c19a155fc836647b8c1db892860091d53e52a2"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/react@19.3.0","externalReferences":[{"url":"https://github.com/react/react/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"https://github.com/react/react.git#packages/react","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://react.dev/","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=0.10.0"}]},{"type":"library","name":"typescript","version":"7.0.2","bom-ref":"pkg:npm/typescript@7.0.2","author":"Microsoft Corp.","description":"TypeScript is a language for application scale JavaScript development","hashes":[{"alg":"SHA-512","content":"f0561abbdea8dcd28e85b8ca8bfa8dbc6fd6e63873c646dd9b9b23f406d9ff94f9b16aa7de12602df1b1dbbb11299593bf2cc23fc74b4414dfe6d05349155434"}],"licenses":[{"license":{"id":"Apache-2.0"}}],"purl":"pkg:npm/typescript@7.0.2","externalReferences":[{"url":"https://github.com/microsoft/TypeScript/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"https://github.com/microsoft/TypeScript.git","type":"vcs","comment":"as detected from PackageJson property \"repository.url\""},{"url":"https://www.typescriptlang.org/","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=16.20.0"}]},{"type":"library","name":"vitest","version":"5.0.1","bom-ref":"pkg:npm/vitest@5.0.1","author":"Anthony Fu <anthonyfu117@hotmail.com>","description":"Next generation testing framework powered by Vite","hashes":[{"alg":"SHA-512","content":"880f799506ca124bebb5391d0209d66d77db8a95a28967bf8439763f9b4c8ba585c03efa1b43715c01a9fccf4439c62ea5e18946beb0a6931cec2a00e354d08e"}],"licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/vitest@5.0.1","externalReferences":[{"url":"https://github.com/vitest-dev/vitest/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs.url\""},{"url":"git+https://github.com/vitest-dev/vitest.git#packages/vitest","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://vitest.dev","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":"^22.12.0 || ^24.0.0 || >=26.0.0"}]}]},"component":{"type":"library","name":"component-media","group":"@stealthscale","version":"0.1.0","bom-ref":"pkg:npm/%40stealthscale/component-media@0.1.0","description":"Renders pictures of people and things, crops pictures, shows slides and plays clips: an avatar, an image cropper, a carousel and a video.","licenses":[{"license":{"id":"MIT"}}],"purl":"pkg:npm/%40stealthscale/component-media@0.1.0","externalReferences":[{"url":"https://github.com/stealth-scale/scale/issues","type":"issue-tracker","comment":"as detected from PackageJson property \"bugs\""},{"url":"git+https://github.com/stealth-scale/scale.git#components/media","type":"vcs","comment":"as detected from PackageJson property \"repository.url\" and \"repository.directory\""},{"url":"https://github.com/stealth-scale/scale/tree/main/components/media#readme","type":"website","comment":"as detected from PackageJson property \"homepage\""}],"properties":[{"name":"cdx:npm:package:constraint:engine:node","value":">=26.0.0"}],"evidence":{"licenses":[{"license":{"name":"file: LICENSE","text":{"content":"TUlUIExpY2Vuc2UKCkNvcHlyaWdodCAoYykgMjAyNiBTdGVhbHRoIFNjYWxlIEIuVi4KClBlcm1pc3Npb24gaXMgaGVyZWJ5IGdyYW50ZWQsIGZyZWUgb2YgY2hhcmdlLCB0byBhbnkgcGVyc29uIG9idGFpbmluZyBhIGNvcHkKb2YgdGhpcyBzb2Z0d2FyZSBhbmQgYXNzb2NpYXRlZCBkb2N1bWVudGF0aW9uIGZpbGVzICh0aGUgIlNvZnR3YXJlIiksIHRvIGRlYWwKaW4gdGhlIFNvZnR3YXJlIHdpdGhvdXQgcmVzdHJpY3Rpb24sIGluY2x1ZGluZyB3aXRob3V0IGxpbWl0YXRpb24gdGhlIHJpZ2h0cwp0byB1c2UsIGNvcHksIG1vZGlmeSwgbWVyZ2UsIHB1Ymxpc2gsIGRpc3RyaWJ1dGUsIHN1YmxpY2Vuc2UsIGFuZC9vciBzZWxsCmNvcGllcyBvZiB0aGUgU29mdHdhcmUsIGFuZCB0byBwZXJtaXQgcGVyc29ucyB0byB3aG9tIHRoZSBTb2Z0d2FyZSBpcwpmdXJuaXNoZWQgdG8gZG8gc28sIHN1YmplY3QgdG8gdGhlIGZvbGxvd2luZyBjb25kaXRpb25zOgoKVGhlIGFib3ZlIGNvcHlyaWdodCBub3RpY2UgYW5kIHRoaXMgcGVybWlzc2lvbiBub3RpY2Ugc2hhbGwgYmUgaW5jbHVkZWQgaW4gYWxsCmNvcGllcyBvciBzdWJzdGFudGlhbCBwb3J0aW9ucyBvZiB0aGUgU29mdHdhcmUuCgpUSEUgU09GVFdBUkUgSVMgUFJPVklERUQgIkFTIElTIiwgV0lUSE9VVCBXQVJSQU5UWSBPRiBBTlkgS0lORCwgRVhQUkVTUyBPUgpJTVBMSUVELCBJTkNMVURJTkcgQlVUIE5PVCBMSU1JVEVEIFRPIFRIRSBXQVJSQU5USUVTIE9GIE1FUkNIQU5UQUJJTElUWSwKRklUTkVTUyBGT1IgQSBQQVJUSUNVTEFSIFBVUlBPU0UgQU5EIE5PTklORlJJTkdFTUVOVC4gSU4gTk8gRVZFTlQgU0hBTEwgVEhFCkFVVEhPUlMgT1IgQ09QWVJJR0hUIEhPTERFUlMgQkUgTElBQkxFIEZPUiBBTlkgQ0xBSU0sIERBTUFHRVMgT1IgT1RIRVIKTElBQklMSVRZLCBXSEVUSEVSIElOIEFOIEFDVElPTiBPRiBDT05UUkFDVCwgVE9SVCBPUiBPVEhFUldJU0UsIEFSSVNJTkcgRlJPTSwKT1VUIE9GIE9SIElOIENPTk5FQ1RJT04gV0lUSCBUSEUgU09GVFdBUkUgT1IgVEhFIFVTRSBPUiBPVEhFUiBERUFMSU5HUyBJTiBUSEUKU09GVFdBUkUuCg==","contentType":"text/plain","encoding":"base64"}}}]}},"supplier":{"name":"Stealth Scale B.V.","url":["https://stealthscale.io"]}},"components":[],"dependencies":[{"ref":"pkg:npm/%40stealthscale/component-media@0.1.0"}]}