@streamscloud/kit 0.10.7-1781475571897 → 0.10.7-1781511135617
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/ui/badge/cmp.badge.svelte +12 -4
- package/dist/ui/badge/cmp.badge.svelte.d.ts +4 -1
- package/dist/ui/grid-card/cmp.grid-card-media.svelte +7 -6
- package/dist/ui/grid-card/cmp.grid-card-media.svelte.d.ts +5 -3
- package/dist/ui/grid-card/fields/cmp.grid-card-badge-field.svelte +7 -11
- package/dist/ui/grid-card/fields/cmp.grid-card-badge-field.svelte.d.ts +1 -1
- package/dist/ui/grid-card/fields/cmp.grid-card-field.svelte +13 -5
- package/dist/ui/grid-card/{cmp.video-player.svelte → video-player.svelte} +2 -1
- package/dist/ui/grid-card/{cmp.video-player.svelte.d.ts → video-player.svelte.d.ts} +5 -3
- package/dist/ui/select/select-internals.d.ts +7 -2
- package/dist/ui/select/select-internals.js +44 -28
- package/package.json +1 -1
|
@@ -1,14 +1,17 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
|
|
1
|
+
<script lang="ts">import { IconSlot } from '../icon';
|
|
2
|
+
const { variant = 'neutral', size = 'md', style = 'soft', fullWidth = false, icon, children } = $props();
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<span class="badge badge--{size} badge--{variant}-{style}" class:badge--full={fullWidth}>
|
|
6
|
+
{#if icon}
|
|
7
|
+
<span class="badge__icon" aria-hidden="true"><IconSlot icon={icon} /></span>
|
|
8
|
+
{/if}
|
|
6
9
|
<span class="badge__content">{@render children()}</span>
|
|
7
10
|
</span>
|
|
8
11
|
|
|
9
12
|
<!--
|
|
10
13
|
@component
|
|
11
|
-
Pill-shaped status indicator. Use `solid` for stronger emphasis, `soft` (default) for a calm tinted fill that pairs with body content.
|
|
14
|
+
Pill-shaped status indicator. Use `solid` for stronger emphasis, `soft` (default) for a calm tinted fill that pairs with body content. Optional leading `icon` renders in its own slot; the text content keeps its own ellipsis when it overflows.
|
|
12
15
|
|
|
13
16
|
### CSS Custom Properties
|
|
14
17
|
| Property | Description | Default |
|
|
@@ -38,9 +41,14 @@ Pill-shaped status indicator. Use `solid` for stronger emphasis, `soft` (default
|
|
|
38
41
|
background: var(--_badge--background);
|
|
39
42
|
font-size: var(--_badge--font-size);
|
|
40
43
|
font-weight: var(--_badge--font-weight);
|
|
41
|
-
line-height:
|
|
44
|
+
line-height: var(--sc-kit--leading--tight);
|
|
42
45
|
max-width: 100%;
|
|
43
46
|
}
|
|
47
|
+
.badge__icon {
|
|
48
|
+
display: inline-flex;
|
|
49
|
+
align-items: center;
|
|
50
|
+
flex-shrink: 0;
|
|
51
|
+
}
|
|
44
52
|
.badge__content {
|
|
45
53
|
min-width: 0;
|
|
46
54
|
overflow: hidden;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type IconProp } from '../icon';
|
|
1
2
|
import type { BadgeVariant } from './types';
|
|
2
3
|
import type { Snippet } from 'svelte';
|
|
3
4
|
type Props = {
|
|
@@ -9,10 +10,12 @@ type Props = {
|
|
|
9
10
|
style?: 'soft' | 'solid';
|
|
10
11
|
/** Stretch to fill the parent's inline axis, centering the content. @default false */
|
|
11
12
|
fullWidth?: boolean;
|
|
13
|
+
/** Optional leading icon. */
|
|
14
|
+
icon?: IconProp;
|
|
12
15
|
children: Snippet;
|
|
13
16
|
};
|
|
14
17
|
/**
|
|
15
|
-
* Pill-shaped status indicator. Use `solid` for stronger emphasis, `soft` (default) for a calm tinted fill that pairs with body content.
|
|
18
|
+
* Pill-shaped status indicator. Use `solid` for stronger emphasis, `soft` (default) for a calm tinted fill that pairs with body content. Optional leading `icon` renders in its own slot; the text content keeps its own ellipsis when it overflows.
|
|
16
19
|
*
|
|
17
20
|
* ### CSS Custom Properties
|
|
18
21
|
* | Property | Description | Default |
|
|
@@ -4,14 +4,13 @@ import { PlaybackManager } from '../media-playback';
|
|
|
4
4
|
import { Carousel } from '../player/carousel';
|
|
5
5
|
import { ProportionalContainer } from '../proportional-container';
|
|
6
6
|
import { SeekBar } from '../seek-bar';
|
|
7
|
-
import
|
|
7
|
+
import VideoPlayer from './video-player.svelte';
|
|
8
8
|
import IconImageOff from '@fluentui/svg-icons/icons/image_off_20_regular.svg?raw';
|
|
9
9
|
const { items, aspectRatio = 'vertical', objectFit = 'contain', showSeekBar = true, duration } = $props();
|
|
10
10
|
let currentIndex = $state(0);
|
|
11
11
|
let currentTime = $state(0);
|
|
12
12
|
let mediaDuration = $state(NaN);
|
|
13
13
|
let activePlayerId = $state.raw(null);
|
|
14
|
-
const hasPlayable = $derived(items.some((item) => item.playable));
|
|
15
14
|
const isCurrentPlayable = $derived(items[currentIndex]?.playable === true);
|
|
16
15
|
const progress = $derived(mediaDuration ? currentTime / mediaDuration : 0);
|
|
17
16
|
const onPlayerTimeUpdate = (t) => (currentTime = t);
|
|
@@ -52,6 +51,7 @@ const onCarouselIndexChanged = (index) => {
|
|
|
52
51
|
{#if items[0].playable}
|
|
53
52
|
<VideoPlayer
|
|
54
53
|
src={items[0].url}
|
|
54
|
+
poster={items[0].cover}
|
|
55
55
|
active={true}
|
|
56
56
|
on={{ timeUpdate: onPlayerTimeUpdate, durationChange: onPlayerDurationChange, activate: onPlayerActivate }} />
|
|
57
57
|
{:else}
|
|
@@ -65,6 +65,7 @@ const onCarouselIndexChanged = (index) => {
|
|
|
65
65
|
{#if item.playable}
|
|
66
66
|
<VideoPlayer
|
|
67
67
|
src={item.url}
|
|
68
|
+
poster={item.cover}
|
|
68
69
|
active={item === items[currentIndex]}
|
|
69
70
|
on={{ timeUpdate: onPlayerTimeUpdate, durationChange: onPlayerDurationChange, activate: onPlayerActivate }} />
|
|
70
71
|
{:else}
|
|
@@ -82,7 +83,7 @@ const onCarouselIndexChanged = (index) => {
|
|
|
82
83
|
</ProportionalContainer>
|
|
83
84
|
</div>
|
|
84
85
|
|
|
85
|
-
{#if showSeekBar
|
|
86
|
+
{#if showSeekBar}
|
|
86
87
|
<div class="seek-bar-section">
|
|
87
88
|
<div class="seek-bar-section__content">
|
|
88
89
|
<div class="seek-bar-section__bar" class:seek-bar-section__bar--hidden={!isCurrentPlayable}>
|
|
@@ -95,9 +96,9 @@ const onCarouselIndexChanged = (index) => {
|
|
|
95
96
|
<!--
|
|
96
97
|
@component
|
|
97
98
|
Media slot for `GridCard`. Renders an empty placeholder, a single `Image` / `VideoPlayer`, or
|
|
98
|
-
a `Carousel` of mixed image / video items. Seek-bar
|
|
99
|
-
|
|
100
|
-
|
|
99
|
+
a `Carousel` of mixed image / video items. Seek-bar renders below the media whenever `showSeekBar`
|
|
100
|
+
(default true); the bar itself is hidden via `visibility: hidden` when the current item isn't
|
|
101
|
+
playable (preserves card height). Optional `duration` overlay renders bottom-right.
|
|
101
102
|
|
|
102
103
|
### CSS Custom Properties
|
|
103
104
|
| Property | Description | Default |
|
|
@@ -2,6 +2,8 @@ import type { AspectRatio, ObjectFit } from './types';
|
|
|
2
2
|
export type MediaItem = {
|
|
3
3
|
url: string;
|
|
4
4
|
playable?: boolean;
|
|
5
|
+
/** Poster shown for a playable item before playback; falls back to the video's first frame. */
|
|
6
|
+
cover?: string;
|
|
5
7
|
};
|
|
6
8
|
type Props = {
|
|
7
9
|
items: MediaItem[];
|
|
@@ -16,9 +18,9 @@ type Props = {
|
|
|
16
18
|
};
|
|
17
19
|
/**
|
|
18
20
|
* Media slot for `GridCard`. Renders an empty placeholder, a single `Image` / `VideoPlayer`, or
|
|
19
|
-
* a `Carousel` of mixed image / video items. Seek-bar
|
|
20
|
-
*
|
|
21
|
-
*
|
|
21
|
+
* a `Carousel` of mixed image / video items. Seek-bar renders below the media whenever `showSeekBar`
|
|
22
|
+
* (default true); the bar itself is hidden via `visibility: hidden` when the current item isn't
|
|
23
|
+
* playable (preserves card height). Optional `duration` overlay renders bottom-right.
|
|
22
24
|
*
|
|
23
25
|
* ### CSS Custom Properties
|
|
24
26
|
* | Property | Description | Default |
|
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
<script lang="ts">import { Badge } from '../../badge';
|
|
2
|
-
import { IconSlot } from '../../icon';
|
|
3
2
|
import GridCardField from './cmp.grid-card-field.svelte';
|
|
4
3
|
const { label, text, variant = 'neutral', style = 'soft', icon } = $props();
|
|
5
4
|
</script>
|
|
6
5
|
|
|
7
6
|
<GridCardField label={label}>
|
|
8
|
-
<
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
</Badge>
|
|
7
|
+
<span class="grid-card-badge-field">
|
|
8
|
+
<Badge variant={variant} style={style} size="sm" icon={icon}>
|
|
9
|
+
{text}
|
|
10
|
+
</Badge>
|
|
11
|
+
</span>
|
|
14
12
|
</GridCardField>
|
|
15
13
|
|
|
16
14
|
<!--
|
|
@@ -20,9 +18,7 @@ inside a `<GridCardField>` so it aligns with text rows. Use for content status,
|
|
|
20
18
|
plan / tier indicators, etc.
|
|
21
19
|
-->
|
|
22
20
|
|
|
23
|
-
<style>.grid-card-badge-
|
|
24
|
-
display:
|
|
25
|
-
align-items: center;
|
|
21
|
+
<style>.grid-card-badge-field {
|
|
22
|
+
display: contents;
|
|
26
23
|
--sc-kit--icon--size: 0.875em;
|
|
27
|
-
--sc-kit--icon--color: currentColor;
|
|
28
24
|
}</style>
|
|
@@ -3,8 +3,8 @@ export {};
|
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
5
|
<div class="grid-card-field" class:grid-card-field--multiline={multiline} class:grid-card-field--vertical={vertical}>
|
|
6
|
-
<span class="grid-card-field__label">{label}</span>
|
|
7
|
-
<div class="grid-card-field__content">
|
|
6
|
+
<span class="grid-card-field__label" class:grid-card-field__label--vertical={vertical}>{label}</span>
|
|
7
|
+
<div class="grid-card-field__content" class:grid-card-field__content--multiline={multiline} class:grid-card-field__content--vertical={vertical}>
|
|
8
8
|
{@render children()}
|
|
9
9
|
</div>
|
|
10
10
|
</div>
|
|
@@ -43,9 +43,6 @@ content; set `vertical` to stack the label above the content (escapes the subgri
|
|
|
43
43
|
grid-row-gap: 0.25rem;
|
|
44
44
|
padding: var(--_gcf--padding-block) var(--_gcf--padding-inline);
|
|
45
45
|
}
|
|
46
|
-
.grid-card-field--vertical .grid-card-field__label, .grid-card-field--vertical .grid-card-field__content {
|
|
47
|
-
padding: 0;
|
|
48
|
-
}
|
|
49
46
|
.grid-card-field__label {
|
|
50
47
|
font-size: 0.625rem;
|
|
51
48
|
color: var(--sc-kit--color--text--secondary);
|
|
@@ -54,8 +51,19 @@ content; set `vertical` to stack the label above the content (escapes the subgri
|
|
|
54
51
|
white-space: nowrap;
|
|
55
52
|
line-height: var(--sc-kit--leading--tight);
|
|
56
53
|
}
|
|
54
|
+
.grid-card-field__label--vertical {
|
|
55
|
+
padding: 0;
|
|
56
|
+
}
|
|
57
57
|
.grid-card-field__content {
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
58
60
|
padding-block: var(--_gcf--padding-block);
|
|
59
61
|
padding-inline-end: var(--_gcf--padding-inline);
|
|
60
62
|
min-width: 0;
|
|
63
|
+
}
|
|
64
|
+
.grid-card-field__content--multiline {
|
|
65
|
+
align-items: flex-start;
|
|
66
|
+
}
|
|
67
|
+
.grid-card-field__content--vertical {
|
|
68
|
+
padding: 0;
|
|
61
69
|
}</style>
|
|
@@ -4,7 +4,7 @@ import { PlaybackManager } from '../media-playback';
|
|
|
4
4
|
import IconPause from '@fluentui/svg-icons/icons/pause_20_regular.svg?raw';
|
|
5
5
|
import IconPlay from '@fluentui/svg-icons/icons/play_20_regular.svg?raw';
|
|
6
6
|
import { untrack } from 'svelte';
|
|
7
|
-
const { src, active, on } = $props();
|
|
7
|
+
const { src, poster, active, on } = $props();
|
|
8
8
|
const id = randomNanoid();
|
|
9
9
|
let videoEl = $state(null);
|
|
10
10
|
let currentTime = $state(0);
|
|
@@ -115,6 +115,7 @@ export const seekTo = (time) => {
|
|
|
115
115
|
<video
|
|
116
116
|
bind:this={videoEl}
|
|
117
117
|
src={src}
|
|
118
|
+
poster={poster}
|
|
118
119
|
controls={false}
|
|
119
120
|
preload="auto"
|
|
120
121
|
ontimeupdate={onTimeUpdate}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
type Props = {
|
|
2
2
|
src: string;
|
|
3
|
+
/** Poster image shown before playback; falls back to the video's first frame when omitted. */
|
|
4
|
+
poster?: string;
|
|
3
5
|
/**
|
|
4
6
|
* When `true`, the player re-publishes its current `currentTime` / `duration` upward via
|
|
5
7
|
* `on.timeUpdate` / `on.durationChange` so the parent can read this slide's state on
|
|
@@ -19,8 +21,8 @@ type Props = {
|
|
|
19
21
|
* with the global `PlaybackManager` so only one player can play at a time. Exposes `seekTo(t)`
|
|
20
22
|
* via `bind:this` for parent seek-bar sync. Not exported standalone — use `GridCardMedia`.
|
|
21
23
|
*/
|
|
22
|
-
declare const
|
|
24
|
+
declare const VideoPlayer: import("svelte").Component<Props, {
|
|
23
25
|
seekTo: (time: number) => void;
|
|
24
26
|
}, "">;
|
|
25
|
-
type
|
|
26
|
-
export default
|
|
27
|
+
type VideoPlayer = ReturnType<typeof VideoPlayer>;
|
|
28
|
+
export default VideoPlayer;
|
|
@@ -25,9 +25,14 @@ export declare const buildFuseIndex: <T>(items: SelectItem<T>[]) => Fuse<FlatOpt
|
|
|
25
25
|
* shape so the core's group-header logic keeps working. Empty query returns the original
|
|
26
26
|
* items untouched.
|
|
27
27
|
*
|
|
28
|
+
* Results are ordered by relevance, not by the original list order: matches rank
|
|
29
|
+
* prefix > substring > scattered-fuzzy (the Fuse score breaks ties within a tier), groups
|
|
30
|
+
* are ordered by their best hit, and options keep that order inside each group. Scattered
|
|
31
|
+
* fuzzy hits are a typo fallback only — they are dropped whenever any prefix/substring hit
|
|
32
|
+
* exists, so a real match is never buried under noise.
|
|
33
|
+
*
|
|
28
34
|
* Group labels participate in the match: when a group's label fuzzy-matches the query, the
|
|
29
35
|
* entire group (header + every child) is included regardless of whether the children
|
|
30
|
-
* themselves matched.
|
|
31
|
-
* the tree structure stays readable.
|
|
36
|
+
* themselves matched.
|
|
32
37
|
*/
|
|
33
38
|
export declare const runFuseSearch: <T>(fuse: Fuse<FlatOption<T>>, query: string, items: SelectItem<T>[]) => SelectItem<T>[];
|
|
@@ -52,56 +52,72 @@ export const moveHighlightIndex = (indices, current, delta) => {
|
|
|
52
52
|
* locally inside their `loadOptions` shim. Async impls don't need this — the server does
|
|
53
53
|
* the filtering.
|
|
54
54
|
*/
|
|
55
|
-
export const buildFuseIndex = (items) => new Fuse(flattenItems(items), { keys: ['option.label'], threshold: 0.4, ignoreLocation: true });
|
|
55
|
+
export const buildFuseIndex = (items) => new Fuse(flattenItems(items), { keys: ['option.label'], threshold: 0.4, ignoreLocation: true, includeScore: true });
|
|
56
56
|
/**
|
|
57
57
|
* Runs a fuzzy search via the Fuse index and reconstructs the grouped `SelectItem<T>[]`
|
|
58
58
|
* shape so the core's group-header logic keeps working. Empty query returns the original
|
|
59
59
|
* items untouched.
|
|
60
60
|
*
|
|
61
|
+
* Results are ordered by relevance, not by the original list order: matches rank
|
|
62
|
+
* prefix > substring > scattered-fuzzy (the Fuse score breaks ties within a tier), groups
|
|
63
|
+
* are ordered by their best hit, and options keep that order inside each group. Scattered
|
|
64
|
+
* fuzzy hits are a typo fallback only — they are dropped whenever any prefix/substring hit
|
|
65
|
+
* exists, so a real match is never buried under noise.
|
|
66
|
+
*
|
|
61
67
|
* Group labels participate in the match: when a group's label fuzzy-matches the query, the
|
|
62
68
|
* entire group (header + every child) is included regardless of whether the children
|
|
63
|
-
* themselves matched.
|
|
64
|
-
* the tree structure stays readable.
|
|
69
|
+
* themselves matched.
|
|
65
70
|
*/
|
|
66
71
|
export const runFuseSearch = (fuse, query, items) => {
|
|
67
72
|
if (!query) {
|
|
68
73
|
return items;
|
|
69
74
|
}
|
|
70
|
-
const
|
|
75
|
+
const needle = query.toLowerCase();
|
|
76
|
+
const rankOf = (label, score) => {
|
|
77
|
+
const haystack = label.toLowerCase();
|
|
78
|
+
const tier = haystack.startsWith(needle) ? 0 : haystack.includes(needle) ? 1 : 2;
|
|
79
|
+
return tier + score;
|
|
80
|
+
};
|
|
81
|
+
const ranked = fuse
|
|
82
|
+
.search(query)
|
|
83
|
+
.map((r) => ({ flat: r.item, rank: rankOf(r.item.option.label, r.score ?? 1) }))
|
|
84
|
+
.sort((a, b) => a.rank - b.rank);
|
|
85
|
+
const hasExact = ranked.some((r) => r.rank < 2);
|
|
86
|
+
const matches = hasExact ? ranked.filter((r) => r.rank < 2) : ranked;
|
|
87
|
+
const groups = items.filter(isSelectGroup);
|
|
88
|
+
const labelMatches = new Set();
|
|
89
|
+
if (groups.length > 0) {
|
|
90
|
+
const labelFuse = new Fuse(groups.map((g) => ({ label: g.label })), { keys: ['label'], threshold: 0.4, ignoreLocation: true });
|
|
91
|
+
for (const hit of labelFuse.search(query)) {
|
|
92
|
+
labelMatches.add(hit.item.label);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
71
95
|
const groupBuckets = new Map();
|
|
96
|
+
const groupRank = new Map();
|
|
72
97
|
const standalone = [];
|
|
73
|
-
|
|
74
|
-
if (
|
|
75
|
-
const bucket = groupBuckets.get(
|
|
98
|
+
matches.forEach(({ flat }, i) => {
|
|
99
|
+
if (flat.group) {
|
|
100
|
+
const bucket = groupBuckets.get(flat.group);
|
|
76
101
|
if (bucket) {
|
|
77
|
-
bucket.push(
|
|
102
|
+
bucket.push(flat.option);
|
|
78
103
|
}
|
|
79
104
|
else {
|
|
80
|
-
groupBuckets.set(
|
|
105
|
+
groupBuckets.set(flat.group, [flat.option]);
|
|
106
|
+
groupRank.set(flat.group, i);
|
|
81
107
|
}
|
|
82
108
|
}
|
|
83
109
|
else {
|
|
84
|
-
standalone.push(
|
|
110
|
+
standalone.push(flat.option);
|
|
85
111
|
}
|
|
86
|
-
}
|
|
87
|
-
// Fuzzy-match group labels: build a tiny per-call Fuse just over group label entries.
|
|
88
|
-
// Cheap (one item per group) and reuses the same threshold so behavior matches options.
|
|
89
|
-
const groups = items.filter(isSelectGroup);
|
|
90
|
-
const labelMatches = new Set();
|
|
91
|
-
if (groups.length > 0) {
|
|
92
|
-
const labelFuse = new Fuse(groups.map((g) => ({ label: g.label })), { keys: ['label'], threshold: 0.4, ignoreLocation: true });
|
|
93
|
-
for (const hit of labelFuse.search(query)) {
|
|
94
|
-
labelMatches.add(hit.item.label);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
112
|
+
});
|
|
97
113
|
const out = [];
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
114
|
+
const matchedGroups = groups
|
|
115
|
+
.filter((g) => groupBuckets.has(g.label) || labelMatches.has(g.label))
|
|
116
|
+
.sort((a, b) => (groupRank.get(a.label) ?? Infinity) - (groupRank.get(b.label) ?? Infinity));
|
|
117
|
+
for (const g of matchedGroups) {
|
|
118
|
+
const options = labelMatches.has(g.label) ? g.options : (groupBuckets.get(g.label) ?? []);
|
|
119
|
+
if (options.length > 0) {
|
|
120
|
+
out.push({ label: g.label, value: g.value, options });
|
|
105
121
|
}
|
|
106
122
|
}
|
|
107
123
|
for (const opt of standalone) {
|