@streamscloud/kit 0.74.0 → 0.75.1
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/dist/core/actions/index.d.ts +0 -1
- package/dist/core/actions/index.js +0 -1
- package/dist/core/i18n/resolve-plural-form.js +2 -1
- package/dist/core/utils/dom-helper.d.ts +2 -1
- package/dist/core/utils/dom-helper.js +4 -2
- package/dist/ui/color-picker/cmp.color-picker.svelte +24 -29
- package/dist/ui/color-picker/cmp.color-picker.svelte.d.ts +1 -14
- package/dist/ui/grid-card/cmp.grid-card-media.svelte +3 -3
- package/dist/ui/grid-card/cmp.grid-card-media.svelte.d.ts +0 -2
- package/dist/ui/image/cmp.image.svelte +2 -4
- package/dist/ui/image/cmp.image.svelte.d.ts +0 -2
- package/dist/ui/popover/validate-trigger.js +2 -1
- package/dist/ui/tile-card/cmp.tile-card.svelte +6 -5
- package/dist/ui/tile-card/cmp.tile-card.svelte.d.ts +0 -2
- package/package.json +3 -1
- package/dist/core/actions/predecode-on-load.d.ts +0 -3
- package/dist/core/actions/predecode-on-load.js +0 -31
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { DEV } from 'esm-env';
|
|
1
2
|
/**
|
|
2
3
|
* Picks the plural form matching `count` for `locale`.
|
|
3
4
|
*
|
|
@@ -17,7 +18,7 @@
|
|
|
17
18
|
*/
|
|
18
19
|
export const resolvePluralForm = ({ count, candidates, locale }) => {
|
|
19
20
|
const { index, categoriesLength } = indexerFor(locale);
|
|
20
|
-
if (
|
|
21
|
+
if (DEV && candidates.length !== categoriesLength) {
|
|
21
22
|
throw new Error(`resolvePluralForm(): locale "${locale}" needs ${categoriesLength} candidate(s), got ${JSON.stringify(candidates)}.`);
|
|
22
23
|
}
|
|
23
24
|
const template = candidates[index(count)] ?? candidates[candidates.length - 1];
|
|
@@ -2,7 +2,8 @@ export declare class DomHelper {
|
|
|
2
2
|
private static readonly _interactiveSelector;
|
|
3
3
|
/**
|
|
4
4
|
* First interactive descendant of `root`, or `null`. Use to reject interactive content from a
|
|
5
|
-
* container that must stay non-interactive (a `<button>` trigger, a `<label>`).
|
|
5
|
+
* container that must stay non-interactive (a `<button>` trigger, a `<label>`). Anything inside an
|
|
6
|
+
* `inert` subtree is skipped — it takes neither focus nor clicks.
|
|
6
7
|
*/
|
|
7
8
|
static findInteractiveDescendant(root: Element): HTMLElement | null;
|
|
8
9
|
/**
|
|
@@ -2,10 +2,12 @@ export class DomHelper {
|
|
|
2
2
|
static _interactiveSelector = 'button, a[href], input:not([type="hidden"]), select, textarea, [role="button"], [role="link"], [role="menuitem"], [tabindex]:not([tabindex="-1"])';
|
|
3
3
|
/**
|
|
4
4
|
* First interactive descendant of `root`, or `null`. Use to reject interactive content from a
|
|
5
|
-
* container that must stay non-interactive (a `<button>` trigger, a `<label>`).
|
|
5
|
+
* container that must stay non-interactive (a `<button>` trigger, a `<label>`). Anything inside an
|
|
6
|
+
* `inert` subtree is skipped — it takes neither focus nor clicks.
|
|
6
7
|
*/
|
|
7
8
|
static findInteractiveDescendant(root) {
|
|
8
|
-
|
|
9
|
+
const candidates = root.querySelectorAll(DomHelper._interactiveSelector);
|
|
10
|
+
return Array.from(candidates).find((candidate) => !candidate.closest('[inert]')) ?? null;
|
|
9
11
|
}
|
|
10
12
|
/**
|
|
11
13
|
* True when the target is, or sits inside, interactive content — i.e. something that owns its own
|
|
@@ -35,12 +35,9 @@ const previewBackgroundColor = $derived.by(() => {
|
|
|
35
35
|
});
|
|
36
36
|
const alphaPercent = $derived(displayColor ? `${Math.round(colord(displayColor).alpha() * 100)}%` : '');
|
|
37
37
|
const triggerPlaceholder = $derived(placeholder ?? loc.notSet);
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
if (clearable) {
|
|
42
|
-
on?.change?.('');
|
|
43
|
-
}
|
|
38
|
+
const clearButtonVisible = $derived(clearable && !disabled && !children && variant === 'fill' && !!value);
|
|
39
|
+
const resetValue = () => {
|
|
40
|
+
on?.change?.('');
|
|
44
41
|
};
|
|
45
42
|
const onColorPickerInput = ({ hex }) => {
|
|
46
43
|
on?.change?.(ColorHelper.normalizeHex(hex));
|
|
@@ -88,10 +85,8 @@ const onColorPickerInput = ({ hex }) => {
|
|
|
88
85
|
<span class="color-picker__chevron" aria-hidden="true">
|
|
89
86
|
<Icon src={IconChevronDown} color={displayColor ? undefined : 'muted'} />
|
|
90
87
|
</span>
|
|
91
|
-
{:else if
|
|
92
|
-
<
|
|
93
|
-
<Icon src={IconDismiss} />
|
|
94
|
-
</button>
|
|
88
|
+
{:else if clearButtonVisible}
|
|
89
|
+
<span class="color-picker__clear-glyph"><Icon src={IconDismiss} /></span>
|
|
95
90
|
{:else}
|
|
96
91
|
<span class="color-picker__chevron" aria-hidden="true">
|
|
97
92
|
<Icon src={IconChevronDown} />
|
|
@@ -111,6 +106,9 @@ const onColorPickerInput = ({ hex }) => {
|
|
|
111
106
|
textInputModes={['hex']} />
|
|
112
107
|
</div>
|
|
113
108
|
</Popover>
|
|
109
|
+
{#if clearButtonVisible}
|
|
110
|
+
<button type="button" class="color-picker__clear" class:color-picker__clear--sm={size === 'sm'} aria-label={loc.clear} onclick={resetValue}></button>
|
|
111
|
+
{/if}
|
|
114
112
|
</div>
|
|
115
113
|
|
|
116
114
|
<!--
|
|
@@ -123,19 +121,6 @@ same height.
|
|
|
123
121
|
When `value` is set, the trigger's background fills with the color and the swatch text auto-flips
|
|
124
122
|
between black and white for contrast.
|
|
125
123
|
|
|
126
|
-
### Props
|
|
127
|
-
| Prop | Type | Default |
|
|
128
|
-
|---|---|---|
|
|
129
|
-
| `value` | `string \| null \| undefined` | required |
|
|
130
|
-
| `variant` | `'fill' \| 'field'` | `'fill'` |
|
|
131
|
-
| `label` | `string` (field variant row label) | — |
|
|
132
|
-
| `size` | `'sm' \| 'md' \| 'lg'` | `'md'` |
|
|
133
|
-
| `clearable` | `boolean` | `false` |
|
|
134
|
-
| `disabled` | `boolean` | `false` |
|
|
135
|
-
| `defaultColor` | `string \| undefined` | `undefined` |
|
|
136
|
-
| `placeholder` | `string \| undefined` | localized "not set" |
|
|
137
|
-
| `previewIcon` | `string \| Snippet` | color-droplet glyph |
|
|
138
|
-
|
|
139
124
|
### CSS Custom Properties
|
|
140
125
|
| Property | Description | Default |
|
|
141
126
|
|---|---|---|
|
|
@@ -206,19 +191,29 @@ between black and white for contrast.
|
|
|
206
191
|
color: var(--sc-kit--color--text--secondary);
|
|
207
192
|
font-size: var(--sc-kit--font-size--sm);
|
|
208
193
|
}
|
|
209
|
-
.color-picker__chevron, .color-picker__clear {
|
|
194
|
+
.color-picker__chevron, .color-picker__clear-glyph {
|
|
210
195
|
display: inline-flex;
|
|
211
196
|
align-items: center;
|
|
212
197
|
justify-content: center;
|
|
198
|
+
line-height: 0;
|
|
199
|
+
}
|
|
200
|
+
.color-picker__clear {
|
|
201
|
+
position: absolute;
|
|
202
|
+
inset-block: 0;
|
|
203
|
+
inset-inline-end: 0;
|
|
204
|
+
aspect-ratio: 1;
|
|
205
|
+
padding: 0;
|
|
213
206
|
background: transparent;
|
|
214
207
|
border: 0;
|
|
215
|
-
|
|
216
|
-
line-height: 0;
|
|
217
|
-
color: inherit;
|
|
208
|
+
border-radius: var(--sc-kit--field--border-radius--md);
|
|
218
209
|
cursor: pointer;
|
|
219
210
|
}
|
|
220
|
-
.color-
|
|
221
|
-
|
|
211
|
+
.color-picker__clear:focus-visible {
|
|
212
|
+
outline: 2px solid var(--sc-kit--color--border--focus);
|
|
213
|
+
outline-offset: -2px;
|
|
214
|
+
}
|
|
215
|
+
.color-picker__clear--sm {
|
|
216
|
+
border-radius: var(--sc-kit--field--border-radius--sm);
|
|
222
217
|
}
|
|
223
218
|
.color-picker__panel {
|
|
224
219
|
--focus-color: none;
|
|
@@ -13,7 +13,7 @@ type Props = {
|
|
|
13
13
|
clearable?: boolean;
|
|
14
14
|
/** Visual disabled state — popover won't open. */
|
|
15
15
|
disabled?: boolean;
|
|
16
|
-
/** Placeholder shown on the swatch trigger when no value is selected. */
|
|
16
|
+
/** Placeholder shown on the swatch trigger when no value is selected. @default localized "Not set" */
|
|
17
17
|
placeholder?: string;
|
|
18
18
|
on?: {
|
|
19
19
|
change?: (value: string) => void;
|
|
@@ -35,19 +35,6 @@ type Props = {
|
|
|
35
35
|
* When `value` is set, the trigger's background fills with the color and the swatch text auto-flips
|
|
36
36
|
* between black and white for contrast.
|
|
37
37
|
*
|
|
38
|
-
* ### Props
|
|
39
|
-
* | Prop | Type | Default |
|
|
40
|
-
* |---|---|---|
|
|
41
|
-
* | `value` | `string \| null \| undefined` | required |
|
|
42
|
-
* | `variant` | `'fill' \| 'field'` | `'fill'` |
|
|
43
|
-
* | `label` | `string` (field variant row label) | — |
|
|
44
|
-
* | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` |
|
|
45
|
-
* | `clearable` | `boolean` | `false` |
|
|
46
|
-
* | `disabled` | `boolean` | `false` |
|
|
47
|
-
* | `defaultColor` | `string \| undefined` | `undefined` |
|
|
48
|
-
* | `placeholder` | `string \| undefined` | localized "not set" |
|
|
49
|
-
* | `previewIcon` | `string \| Snippet` | color-droplet glyph |
|
|
50
|
-
*
|
|
51
38
|
* ### CSS Custom Properties
|
|
52
39
|
* | Property | Description | Default |
|
|
53
40
|
* |---|---|---|
|
|
@@ -8,7 +8,7 @@ import { ProportionalContainer } from '../proportional-container';
|
|
|
8
8
|
import { SeekBar } from '../seek-bar';
|
|
9
9
|
import VideoPlayer from './video-player.svelte';
|
|
10
10
|
import IconImageOff from '@fluentui/svg-icons/icons/image_off_20_regular.svg?raw';
|
|
11
|
-
const { items, aspectRatio = 'vertical', objectFit = 'contain', showSeekBar = true, duration
|
|
11
|
+
const { items, aspectRatio = 'vertical', objectFit = 'contain', showSeekBar = true, duration } = $props();
|
|
12
12
|
let currentIndex = $state(0);
|
|
13
13
|
let currentTime = $state(0);
|
|
14
14
|
let mediaDuration = $state(NaN);
|
|
@@ -63,7 +63,7 @@ const onCarouselIndexChanged = (index) => {
|
|
|
63
63
|
active={true}
|
|
64
64
|
on={{ timeUpdate: onPlayerTimeUpdate, durationChange: onPlayerDurationChange, activate: onPlayerActivate }} />
|
|
65
65
|
{:else}
|
|
66
|
-
<Image src={items[0].url} showStubOnError={true}
|
|
66
|
+
<Image src={items[0].url} showStubOnError={true} />
|
|
67
67
|
{/if}
|
|
68
68
|
</div>
|
|
69
69
|
{:else}
|
|
@@ -78,7 +78,7 @@ const onCarouselIndexChanged = (index) => {
|
|
|
78
78
|
active={item === items[currentIndex]}
|
|
79
79
|
on={{ timeUpdate: onPlayerTimeUpdate, durationChange: onPlayerDurationChange, activate: onPlayerActivate }} />
|
|
80
80
|
{:else}
|
|
81
|
-
<Image src={item.url} showStubOnError={true}
|
|
81
|
+
<Image src={item.url} showStubOnError={true} />
|
|
82
82
|
{/if}
|
|
83
83
|
</div>
|
|
84
84
|
{/snippet}
|
|
@@ -15,8 +15,6 @@ type Props = {
|
|
|
15
15
|
showSeekBar?: boolean;
|
|
16
16
|
/** Duration in seconds; rendered as a badge overlay at the bottom-right of the media. `≤ 0` / `undefined` renders nothing. */
|
|
17
17
|
duration?: number;
|
|
18
|
-
/** Forwarded to each `Image`. Set it when the card sits in a `DragSource`, or the first drag ghost comes out without the picture. @default false */
|
|
19
|
-
predecode?: boolean;
|
|
20
18
|
};
|
|
21
19
|
/**
|
|
22
20
|
* Media slot for `GridCard`. Renders an empty placeholder, a single `Image` / `VideoPlayer`, or
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
2
|
-
import { Icon } from '../icon';
|
|
1
|
+
<script lang="ts">import { Icon } from '../icon';
|
|
3
2
|
import { calculateImageState } from './image-state';
|
|
4
3
|
import IconImageOff from '@fluentui/svg-icons/icons/image_off_20_regular.svg?raw';
|
|
5
|
-
let { src, alt = '', showStubOnError = false,
|
|
4
|
+
let { src, alt = '', showStubOnError = false, on, stub } = $props();
|
|
6
5
|
let hasError = $state(false);
|
|
7
6
|
const imageState = $derived(calculateImageState(src, hasError, showStubOnError));
|
|
8
7
|
// A 0.01x density makes the intrinsic size 100× the natural one, so `contain` sizing always scales the picture
|
|
@@ -27,7 +26,6 @@ const onLoadError = () => {
|
|
|
27
26
|
srcset={srcset}
|
|
28
27
|
alt={alt || ''}
|
|
29
28
|
draggable="false"
|
|
30
|
-
use:predecodeOnLoad={predecode}
|
|
31
29
|
onload={onLoad}
|
|
32
30
|
onerror={onLoadError} />
|
|
33
31
|
{/if}
|
|
@@ -5,8 +5,6 @@ type Props = {
|
|
|
5
5
|
alt?: string;
|
|
6
6
|
/** Show a placeholder stub instead of an error icon when image fails to load */
|
|
7
7
|
showStubOnError?: boolean;
|
|
8
|
-
/** Decodes the picture once right after it loads. Set it when the element can be dragged, or the first drag ghost comes out empty. @default false */
|
|
9
|
-
predecode?: boolean;
|
|
10
8
|
on?: {
|
|
11
9
|
/** Fires after load attempt; `true` if successful, `false` on error */
|
|
12
10
|
load?: (loadedSuccessfully: boolean) => void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { DomHelper } from '../../core/utils';
|
|
2
|
+
import { DEV } from 'esm-env';
|
|
2
3
|
/**
|
|
3
4
|
* Dev-only guard for the Popover trigger. The trigger renders inside a `<button>`, so its content
|
|
4
5
|
* must be non-interactive (use `Button` with `type="presentational"` for a button-looking trigger).
|
|
@@ -6,7 +7,7 @@ import { DomHelper } from '../../core/utils';
|
|
|
6
7
|
* node and outlines the trigger in red. No-op in production.
|
|
7
8
|
*/
|
|
8
9
|
export const validateTrigger = (node) => {
|
|
9
|
-
if (!
|
|
10
|
+
if (!DEV) {
|
|
10
11
|
return;
|
|
11
12
|
}
|
|
12
13
|
const check = () => {
|
|
@@ -1,11 +1,10 @@
|
|
|
1
|
-
<script lang="ts">import {
|
|
2
|
-
import { Checkbox } from '../checkbox';
|
|
1
|
+
<script lang="ts">import { Checkbox } from '../checkbox';
|
|
3
2
|
import { ToolbarIconButton } from '../page-toolbar';
|
|
4
3
|
import { PlayIndicator } from '../play-indicator';
|
|
5
4
|
import { Popover } from '../popover';
|
|
6
5
|
import { TileCardLocalization } from './tile-card-localization';
|
|
7
6
|
import IconMore from '@fluentui/svg-icons/icons/more_vertical_20_regular.svg?raw';
|
|
8
|
-
const { title, meta, preview,
|
|
7
|
+
const { title, meta, preview, menu, menuLabel, selected = false, checkboxVisibility = 'hover', on } = $props();
|
|
9
8
|
const localization = new TileCardLocalization();
|
|
10
9
|
// Safari paints no frame for preload="metadata" until a seek target is given.
|
|
11
10
|
const withFirstFrame = (src) => (src.includes('#') ? src : `${src}#t=0.001`);
|
|
@@ -20,7 +19,7 @@ const withFirstFrame = (src) => (src.includes('#') ? src : `${src}#t=0.001`);
|
|
|
20
19
|
{#if typeof preview === 'function'}
|
|
21
20
|
{@render preview()}
|
|
22
21
|
{:else if 'image' in preview}
|
|
23
|
-
<img class="tile-card__picture" src={preview.image} alt=""
|
|
22
|
+
<img class="tile-card__picture" src={preview.image} alt="" />
|
|
24
23
|
{:else}
|
|
25
24
|
<video class="tile-card__picture" src={withFirstFrame(preview.video)} poster={preview.poster} preload="metadata" muted playsinline></video>
|
|
26
25
|
<span class="tile-card__play"><PlayIndicator /></span>
|
|
@@ -137,10 +136,12 @@ checkbox via `on.select`, whole-card activation via `on.activate`. For a drop-ta
|
|
|
137
136
|
pointer-events: none;
|
|
138
137
|
}
|
|
139
138
|
.tile-card__picture {
|
|
139
|
+
position: absolute;
|
|
140
|
+
inset: 0;
|
|
140
141
|
display: block;
|
|
141
142
|
max-inline-size: 100%;
|
|
142
143
|
max-block-size: 100%;
|
|
143
|
-
|
|
144
|
+
margin: auto;
|
|
144
145
|
}
|
|
145
146
|
.tile-card__play {
|
|
146
147
|
position: absolute;
|
|
@@ -6,8 +6,6 @@ type Props = {
|
|
|
6
6
|
meta?: string;
|
|
7
7
|
/** Top area: `image` / `video` render a drag-safe picture or frame (contain, never upscaled; a video gets a play glyph); a snippet renders custom content. The area is `pointer-events: none` — interactive content belongs in `menu` or `on.activate`. */
|
|
8
8
|
preview?: TileCardPreview;
|
|
9
|
-
/** Decodes the `image` preview once right after it loads. Set it when the tile sits in a `DragSource`, or the first drag ghost comes out without the picture — an `<img>` in a preview snippet needs `use:predecodeOnLoad` from its owner. @default false */
|
|
10
|
-
predecode?: boolean;
|
|
11
9
|
/** `PopoverItem`s of the kebab menu at the end of the footer. */
|
|
12
10
|
menu?: Snippet;
|
|
13
11
|
/** Accessible name of the kebab menu. @default localized "Actions" */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@streamscloud/kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.75.1",
|
|
4
4
|
"author": "StreamsCloud",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -495,6 +495,7 @@
|
|
|
495
495
|
"colord": "^2.9.3",
|
|
496
496
|
"dequal": "^2.0.3",
|
|
497
497
|
"dompurify": "^3.4.5",
|
|
498
|
+
"esm-env": "^1.2.2",
|
|
498
499
|
"fuse.js": "^7.3.0",
|
|
499
500
|
"hugerte": "^1.0.10",
|
|
500
501
|
"mime": "^4.1.0",
|
|
@@ -534,6 +535,7 @@
|
|
|
534
535
|
"eslint-plugin-promise": "^7.3.0",
|
|
535
536
|
"eslint-plugin-svelte": "^3.23.0",
|
|
536
537
|
"eslint-plugin-unused-imports": "^4.4.1",
|
|
538
|
+
"esm-env": "^1.2.2",
|
|
537
539
|
"fuse.js": "^7.3.0",
|
|
538
540
|
"globals": "^17.12.0",
|
|
539
541
|
"hugerte": "^1.0.10",
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import type { Action } from 'svelte/action';
|
|
2
|
-
/** Decodes the `<img>` once after every load, so a drag ghost snapshotted right after includes the picture. A broken image is ignored. `false` turns it off. */
|
|
3
|
-
export declare const predecodeOnLoad: Action<HTMLImageElement, boolean | undefined>;
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
/** Decodes the `<img>` once after every load, so a drag ghost snapshotted right after includes the picture. A broken image is ignored. `false` turns it off. */
|
|
2
|
-
export const predecodeOnLoad = (node, enabled = true) => {
|
|
3
|
-
let active = enabled;
|
|
4
|
-
const decode = async () => {
|
|
5
|
-
try {
|
|
6
|
-
await node.decode();
|
|
7
|
-
}
|
|
8
|
-
catch {
|
|
9
|
-
// a broken image has nothing to decode
|
|
10
|
-
}
|
|
11
|
-
};
|
|
12
|
-
const decodeLoaded = () => {
|
|
13
|
-
if (active && node.complete && node.naturalWidth > 0) {
|
|
14
|
-
void decode();
|
|
15
|
-
}
|
|
16
|
-
};
|
|
17
|
-
node.addEventListener('load', decodeLoaded);
|
|
18
|
-
decodeLoaded();
|
|
19
|
-
return {
|
|
20
|
-
update(nextEnabled = true) {
|
|
21
|
-
const turnedOn = nextEnabled && !active;
|
|
22
|
-
active = nextEnabled;
|
|
23
|
-
if (turnedOn) {
|
|
24
|
-
decodeLoaded();
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
destroy() {
|
|
28
|
-
node.removeEventListener('load', decodeLoaded);
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
};
|