@streamscloud/kit 0.11.3 → 0.12.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/dist/ui/button/cmp.button.svelte +20 -13
- package/dist/ui/button/cmp.button.svelte.d.ts +7 -2
- package/dist/ui/card-actions/cmp.card-action.svelte +24 -16
- package/dist/ui/card-actions/cmp.card-action.svelte.d.ts +2 -0
- package/dist/ui/card-actions/cmp.card-actions.svelte +5 -5
- package/dist/ui/card-actions/cmp.card-actions.svelte.d.ts +2 -2
- package/dist/ui/grid-card/cmp.grid-card-media.svelte +11 -9
- package/dist/ui/page-toolbar/cmp.toolbar-popover.svelte +1 -1
- package/dist/ui/pagination/cmp.pagination.svelte +3 -3
- package/dist/ui/popover/cmp.hover-popover.svelte +1 -1
- package/dist/ui/popover/cmp.hover-popover.svelte.d.ts +1 -1
- package/dist/ui/popover/cmp.popover-item.svelte +3 -3
- package/dist/ui/popover/cmp.popover-item.svelte.d.ts +6 -3
- package/dist/ui/popover/cmp.popover.svelte +10 -2
- package/dist/ui/popover/cmp.popover.svelte.d.ts +12 -1
- package/dist/ui/popover/validate-trigger.d.ts +8 -0
- package/dist/ui/popover/validate-trigger.js +32 -0
- package/dist/ui/table/table-cells/table-actions-cell.svelte +10 -13
- package/dist/ui/table/table-columns-manager/cmp.table-columns-manager.svelte +1 -1
- package/dist/ui/table/table-group-actions/cmp.table-group-actions.svelte +1 -1
- package/dist/ui/table/table-headers/table-header-sortable.svelte +3 -2
- package/dist/ui/video/cmp.video.svelte +14 -5
- package/dist/ui/video/cmp.video.svelte.d.ts +1 -0
- package/dist/ui/video/video-localization.d.ts +6 -0
- package/dist/ui/video/video-localization.js +33 -0
- package/package.json +1 -1
|
@@ -4,7 +4,18 @@ const { variant = 'primary', size = 'md', iconScale = 'default', disabled = fals
|
|
|
4
4
|
const inactive = $derived(disabled || loading);
|
|
5
5
|
const isIconOnly = $derived(!children && (!!icon || loading));
|
|
6
6
|
const iconFallbacks = $derived({ size: CONTROL_ICON_SIZE[size], scale: iconScale });
|
|
7
|
-
const
|
|
7
|
+
const btnClass = $derived([
|
|
8
|
+
'btn',
|
|
9
|
+
`btn--${variant}`,
|
|
10
|
+
`btn--${size}`,
|
|
11
|
+
inactive && `btn--${variant}-disabled`,
|
|
12
|
+
fullWidth && 'btn--full',
|
|
13
|
+
loading && 'btn--loading',
|
|
14
|
+
isIconOnly && 'btn--icon-only',
|
|
15
|
+
inactive && 'btn--disabled'
|
|
16
|
+
]
|
|
17
|
+
.filter(Boolean)
|
|
18
|
+
.join(' '));
|
|
8
19
|
const anchorRel = $derived.by(() => {
|
|
9
20
|
if (mode.type !== 'anchor') {
|
|
10
21
|
return undefined;
|
|
@@ -23,7 +34,7 @@ const handleClick = (e) => {
|
|
|
23
34
|
e.stopPropagation();
|
|
24
35
|
return;
|
|
25
36
|
}
|
|
26
|
-
if (mode.type !== 'anchor') {
|
|
37
|
+
if (mode.type !== 'anchor' && mode.type !== 'presentational') {
|
|
27
38
|
mode.on?.click?.(e);
|
|
28
39
|
}
|
|
29
40
|
};
|
|
@@ -45,11 +56,7 @@ const handleClick = (e) => {
|
|
|
45
56
|
|
|
46
57
|
{#if mode.type === 'anchor'}
|
|
47
58
|
<a
|
|
48
|
-
class=
|
|
49
|
-
class:btn--full={fullWidth}
|
|
50
|
-
class:btn--loading={loading}
|
|
51
|
-
class:btn--icon-only={isIconOnly}
|
|
52
|
-
class:btn--disabled={inactive}
|
|
59
|
+
class={btnClass}
|
|
53
60
|
href={inactive ? undefined : mode.href}
|
|
54
61
|
target={mode.target}
|
|
55
62
|
rel={anchorRel}
|
|
@@ -62,13 +69,13 @@ const handleClick = (e) => {
|
|
|
62
69
|
onclick={handleClick}>
|
|
63
70
|
{@render content()}
|
|
64
71
|
</a>
|
|
72
|
+
{:else if mode.type === 'presentational'}
|
|
73
|
+
<span class={btnClass}>
|
|
74
|
+
{@render content()}
|
|
75
|
+
</span>
|
|
65
76
|
{:else}
|
|
66
77
|
<button
|
|
67
|
-
class=
|
|
68
|
-
class:btn--full={fullWidth}
|
|
69
|
-
class:btn--loading={loading}
|
|
70
|
-
class:btn--icon-only={isIconOnly}
|
|
71
|
-
class:btn--disabled={inactive}
|
|
78
|
+
class={btnClass}
|
|
72
79
|
type={mode.type}
|
|
73
80
|
disabled={disabled}
|
|
74
81
|
aria-label={ariaLabel}
|
|
@@ -88,7 +95,7 @@ A button with configurable variant, size, loading state, and an anchor mode. Wid
|
|
|
88
95
|
|
|
89
96
|
The `icon` slot accepts an SVG source string or a snippet; `iconPosition` ('leading' / 'trailing', default leading) flips which side of the label the icon renders on. Icon-only mode kicks in automatically when `icon` is set without `children` — the button becomes square and requires an `aria-label`.
|
|
90
97
|
|
|
91
|
-
Pass `type="anchor"` to render as `<a>` with `href
|
|
98
|
+
Pass `type="anchor"` to render as `<a>` with `href`, or `type="presentational"` to render a non-interactive `<span>` styled as a button (for use as the visual inside another interactive element, e.g. a Popover trigger). Otherwise `type` is the native `<button type>` (`'button' | 'submit' | 'reset'`).
|
|
92
99
|
|
|
93
100
|
### CSS Custom Properties
|
|
94
101
|
| Property | Description | Default |
|
|
@@ -41,13 +41,18 @@ type AnchorModeProps = BaseProps & {
|
|
|
41
41
|
rel?: string;
|
|
42
42
|
download?: string;
|
|
43
43
|
};
|
|
44
|
-
type
|
|
44
|
+
type PresentationalModeProps = BaseProps & {
|
|
45
|
+
/** Presentational mode — renders a non-interactive `<span>` styled as a button. Use as the visual inside another interactive element (e.g. a Popover trigger) to avoid nesting interactive elements. Accepts no `href` / click callback. */
|
|
46
|
+
type: 'presentational';
|
|
47
|
+
href?: never;
|
|
48
|
+
};
|
|
49
|
+
type Props = ButtonModeProps | AnchorModeProps | PresentationalModeProps;
|
|
45
50
|
/**
|
|
46
51
|
* A button with configurable variant, size, loading state, and an anchor mode. Width is intrinsic by default — use `fullWidth` for inline-axis stretch, or override `--sc-kit--button--{min,max,}width` from CSS for fine-grained control. Overflowing labels are truncated with an ellipsis.
|
|
47
52
|
*
|
|
48
53
|
* The `icon` slot accepts an SVG source string or a snippet; `iconPosition` ('leading' / 'trailing', default leading) flips which side of the label the icon renders on. Icon-only mode kicks in automatically when `icon` is set without `children` — the button becomes square and requires an `aria-label`.
|
|
49
54
|
*
|
|
50
|
-
* Pass `type="anchor"` to render as `<a>` with `href
|
|
55
|
+
* Pass `type="anchor"` to render as `<a>` with `href`, or `type="presentational"` to render a non-interactive `<span>` styled as a button (for use as the visual inside another interactive element, e.g. a Popover trigger). Otherwise `type` is the native `<button type>` (`'button' | 'submit' | 'reset'`).
|
|
51
56
|
*
|
|
52
57
|
* ### CSS Custom Properties
|
|
53
58
|
* | Property | Description | Default |
|
|
@@ -1,22 +1,28 @@
|
|
|
1
1
|
<script lang="ts">import { IconSlot } from '../icon';
|
|
2
|
-
let { action, on } = $props();
|
|
2
|
+
let { action, presentational = false, on } = $props();
|
|
3
3
|
</script>
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
}
|
|
5
|
+
{#if presentational}
|
|
6
|
+
<span class="card-action" title={action.title ?? ''}>
|
|
7
|
+
<IconSlot icon={action.icon} fallbacks={{ color: 'text' }} />
|
|
8
|
+
</span>
|
|
9
|
+
{:else}
|
|
10
|
+
<button
|
|
11
|
+
type="button"
|
|
12
|
+
class="card-action"
|
|
13
|
+
title={action.title ?? ''}
|
|
14
|
+
disabled={action.disabled}
|
|
15
|
+
onclick={(e) => {
|
|
16
|
+
if (!action.propagateClickEvent) {
|
|
17
|
+
e.stopPropagation();
|
|
18
|
+
}
|
|
14
19
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
</button>
|
|
20
|
+
action.callback?.();
|
|
21
|
+
on?.click?.();
|
|
22
|
+
}}>
|
|
23
|
+
<IconSlot icon={action.icon} fallbacks={{ color: 'text' }} />
|
|
24
|
+
</button>
|
|
25
|
+
{/if}
|
|
20
26
|
|
|
21
27
|
<!--
|
|
22
28
|
@component
|
|
@@ -32,7 +38,7 @@ render a disabled button (no callback, no hover effect via the native `:disabled
|
|
|
32
38
|
|
|
33
39
|
<style>.card-action {
|
|
34
40
|
--_card-action--padding: var(--sc-kit--card-action--padding, 0.3125em);
|
|
35
|
-
--_card-action--hover-scale: var(--sc-kit--card-action--hover-scale, 1.
|
|
41
|
+
--_card-action--hover-scale: var(--sc-kit--card-action--hover-scale, 1.1);
|
|
36
42
|
appearance: none;
|
|
37
43
|
background: transparent;
|
|
38
44
|
border: 0;
|
|
@@ -40,6 +46,8 @@ render a disabled button (no callback, no hover effect via the native `:disabled
|
|
|
40
46
|
cursor: pointer;
|
|
41
47
|
padding: var(--_card-action--padding);
|
|
42
48
|
transition: transform var(--sc-kit--duration--slow) var(--sc-kit--ease--default);
|
|
49
|
+
transform-origin: center;
|
|
50
|
+
will-change: transform;
|
|
43
51
|
line-height: 0;
|
|
44
52
|
}
|
|
45
53
|
.card-action:hover:not(:disabled) {
|
|
@@ -2,6 +2,8 @@ import type { CardActionModel } from './types';
|
|
|
2
2
|
type Props = {
|
|
3
3
|
/** Action definition containing icon, callback, and title */
|
|
4
4
|
action: CardActionModel;
|
|
5
|
+
/** Render a non-interactive `<span>` (same look, no button) — for use as the visual inside another interactive element, e.g. a Popover trigger. */
|
|
6
|
+
presentational?: boolean;
|
|
5
7
|
on?: {
|
|
6
8
|
click?: () => void;
|
|
7
9
|
};
|
|
@@ -10,7 +10,7 @@ const closePopover = () => popover?.close();
|
|
|
10
10
|
<div class="card-actions">
|
|
11
11
|
<Popover bind:this={popover} position={popoverPosition}>
|
|
12
12
|
{#snippet trigger()}
|
|
13
|
-
<CardAction action={{ icon: IconMoreVertical
|
|
13
|
+
<CardAction action={{ icon: IconMoreVertical }} presentational />
|
|
14
14
|
{/snippet}
|
|
15
15
|
|
|
16
16
|
<div class="card-actions__dropdown-content">
|
|
@@ -32,9 +32,9 @@ this is a deliberate scope rule, not a class to slap on by accident.
|
|
|
32
32
|
### CSS Custom Properties
|
|
33
33
|
| Property | Description | Default |
|
|
34
34
|
|---|---|---|
|
|
35
|
-
| `--sc-kit--card-actions--background` | Trigger background | `--sc-kit--color--bg--
|
|
35
|
+
| `--sc-kit--card-actions--background` | Trigger background | `--sc-kit--color--bg--element` |
|
|
36
36
|
| `--sc-kit--card-actions--border-radius` | Corner rounding | `--sc-kit--radius--sm` |
|
|
37
|
-
| `--sc-kit--card-actions--popover-background` | Popover panel background |
|
|
37
|
+
| `--sc-kit--card-actions--popover-background` | Popover panel background | `--sc-kit--color--bg--panel` |
|
|
38
38
|
| `--sc-kit--card-actions--font-size` | Base font size | `1rem` |
|
|
39
39
|
| `--sc-kit--card-actions--left-offset` | Left position offset | `0.3125em` |
|
|
40
40
|
| `--sc-kit--card-actions--opacity` | Resting opacity (0 = hidden until hover) | `0` |
|
|
@@ -43,9 +43,9 @@ this is a deliberate scope rule, not a class to slap on by accident.
|
|
|
43
43
|
-->
|
|
44
44
|
|
|
45
45
|
<style>.card-actions {
|
|
46
|
-
--_card-actions--background: var(--sc-kit--card-actions--background, var(--sc-kit--color--bg--
|
|
46
|
+
--_card-actions--background: var(--sc-kit--card-actions--background, var(--sc-kit--color--bg--element));
|
|
47
47
|
--_card-actions--border-radius: var(--sc-kit--card-actions--border-radius, var(--sc-kit--radius--sm));
|
|
48
|
-
--_card-actions--popover-background: var(--sc-kit--card-actions--popover-background, var(--
|
|
48
|
+
--_card-actions--popover-background: var(--sc-kit--card-actions--popover-background, var(--sc-kit--color--bg--panel));
|
|
49
49
|
--_card-actions--font-size: var(--sc-kit--card-actions--font-size, 1rem);
|
|
50
50
|
--_card-actions--left-offset: var(--sc-kit--card-actions--left-offset, 0.3125em);
|
|
51
51
|
--_card-actions--opacity: var(--sc-kit--card-actions--opacity, 0);
|
|
@@ -15,9 +15,9 @@ type Props = {
|
|
|
15
15
|
* ### CSS Custom Properties
|
|
16
16
|
* | Property | Description | Default |
|
|
17
17
|
* |---|---|---|
|
|
18
|
-
* | `--sc-kit--card-actions--background` | Trigger background | `--sc-kit--color--bg--
|
|
18
|
+
* | `--sc-kit--card-actions--background` | Trigger background | `--sc-kit--color--bg--element` |
|
|
19
19
|
* | `--sc-kit--card-actions--border-radius` | Corner rounding | `--sc-kit--radius--sm` |
|
|
20
|
-
* | `--sc-kit--card-actions--popover-background` | Popover panel background |
|
|
20
|
+
* | `--sc-kit--card-actions--popover-background` | Popover panel background | `--sc-kit--color--bg--panel` |
|
|
21
21
|
* | `--sc-kit--card-actions--font-size` | Base font size | `1rem` |
|
|
22
22
|
* | `--sc-kit--card-actions--left-offset` | Left position offset | `0.3125em` |
|
|
23
23
|
* | `--sc-kit--card-actions--opacity` | Resting opacity (0 = hidden until hover) | `0` |
|
|
@@ -49,15 +49,17 @@ const onCarouselIndexChanged = (index) => {
|
|
|
49
49
|
<Icon src={IconImageOff} color="on-accent" />
|
|
50
50
|
</div>
|
|
51
51
|
{:else if items.length === 1}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
52
|
+
<div class="grid-card-media__slide">
|
|
53
|
+
{#if items[0].playable}
|
|
54
|
+
<VideoPlayer
|
|
55
|
+
src={items[0].url}
|
|
56
|
+
poster={items[0].cover}
|
|
57
|
+
active={true}
|
|
58
|
+
on={{ timeUpdate: onPlayerTimeUpdate, durationChange: onPlayerDurationChange, activate: onPlayerActivate }} />
|
|
59
|
+
{:else}
|
|
60
|
+
<Image src={items[0].url} showStubOnError={true} />
|
|
61
|
+
{/if}
|
|
62
|
+
</div>
|
|
61
63
|
{:else}
|
|
62
64
|
<div class="grid-card-media__carousel">
|
|
63
65
|
<Carousel items={items} mode="arrows-with-counts" initialIndex={0} on={{ indexChanged: onCarouselIndexChanged }}>
|
|
@@ -9,7 +9,7 @@ const { title, triggerLabel, icon, size = 'sm', position = 'bottom-end', childre
|
|
|
9
9
|
<div class="toolbar-popover">
|
|
10
10
|
<Popover position={position} keepOpen>
|
|
11
11
|
{#snippet trigger()}
|
|
12
|
-
<Button type="
|
|
12
|
+
<Button type="presentational" variant="secondary" size={size}>
|
|
13
13
|
<IconText icon={icon} secondaryIcon={IconChevronDown} size={size} iconScale="large">{triggerLabel}</IconText>
|
|
14
14
|
</Button>
|
|
15
15
|
{/snippet}
|
|
@@ -57,12 +57,12 @@ const setSize = (size) => {
|
|
|
57
57
|
{#if state.total > 0 || preserveSpace}
|
|
58
58
|
<div class="pagination" class:pagination--hidden={!state.total} role="navigation" aria-label={localization.label}>
|
|
59
59
|
{#if pageSizes && !compact}
|
|
60
|
-
<Popover position="top-end">
|
|
60
|
+
<Popover position="top-end" aria-label={localization.pageSize}>
|
|
61
61
|
{#snippet trigger()}
|
|
62
|
-
<
|
|
62
|
+
<span class="pagination__node pagination__node--trigger">
|
|
63
63
|
{state.pageSize}
|
|
64
64
|
<Icon src={IconChevronDown} />
|
|
65
|
-
</
|
|
65
|
+
</span>
|
|
66
66
|
{/snippet}
|
|
67
67
|
{#each pageSizes as size (size)}
|
|
68
68
|
<PopoverItem on={{ click: () => setSize(size) }}>{size}</PopoverItem>
|
|
@@ -53,7 +53,7 @@ small visual gap between trigger and content.
|
|
|
53
53
|
import { HoverPopover, PopoverItem } from '@streamscloud/kit/ui/popover';
|
|
54
54
|
|
|
55
55
|
<HoverPopover>
|
|
56
|
-
{#snippet trigger()}<Button type="
|
|
56
|
+
{#snippet trigger()}<Button type="presentational">Hover me</Button>{/snippet}
|
|
57
57
|
<PopoverItem>Action</PopoverItem>
|
|
58
58
|
</HoverPopover>
|
|
59
59
|
```
|
|
@@ -38,7 +38,7 @@ type Props = {
|
|
|
38
38
|
* import { HoverPopover, PopoverItem } from '@streamscloud/kit/ui/popover';
|
|
39
39
|
*
|
|
40
40
|
* <HoverPopover>
|
|
41
|
-
* {#snippet trigger()}<Button type="
|
|
41
|
+
* {#snippet trigger()}<Button type="presentational">Hover me</Button>{/snippet}
|
|
42
42
|
* <PopoverItem>Action</PopoverItem>
|
|
43
43
|
* </HoverPopover>
|
|
44
44
|
* ```
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<script lang="ts">import { popoverIgnore } from './popover-ignore';
|
|
2
|
-
const { disabled = false, keepOpen = false, divider = false, inset =
|
|
2
|
+
const { disabled = false, keepOpen = false, divider = false, inset = true, on, children } = $props();
|
|
3
3
|
const handleClick = () => {
|
|
4
4
|
if (disabled) {
|
|
5
5
|
return;
|
|
@@ -43,8 +43,8 @@ for toggle items inside menus).
|
|
|
43
43
|
| `--sc-kit--popover-item--divider-color` | Divider line color | `var(--sc-kit--color--border)` |
|
|
44
44
|
| `--sc-kit--popover-item--divider-spacing` | Vertical gap added by divider | `var(--sc-kit--space--1)` |
|
|
45
45
|
| `--sc-kit--popover-item--text-align` | Text alignment | `left` |
|
|
46
|
-
| `--sc-kit--popover-item--margin-inline` | Inline margin when `inset` | `var(--sc-kit--space--2)` |
|
|
47
|
-
| `--sc-kit--popover-item--border-radius` | Corner radius when `inset` | `var(--sc-kit--radius--sm)` |
|
|
46
|
+
| `--sc-kit--popover-item--margin-inline` | Inline margin when `inset` (default on) | `var(--sc-kit--space--2)` |
|
|
47
|
+
| `--sc-kit--popover-item--border-radius` | Corner radius when `inset` (default on) | `var(--sc-kit--radius--sm)` |
|
|
48
48
|
-->
|
|
49
49
|
|
|
50
50
|
<style>.popover-item {
|
|
@@ -5,7 +5,10 @@ type Props = {
|
|
|
5
5
|
keepOpen?: boolean;
|
|
6
6
|
/** Render a separator line below this item — useful for grouping menu sections. */
|
|
7
7
|
divider?: boolean;
|
|
8
|
-
/**
|
|
8
|
+
/**
|
|
9
|
+
* Inset the item from the popover edges: adds an inline margin and rounds the hover background.
|
|
10
|
+
* @default true
|
|
11
|
+
*/
|
|
9
12
|
inset?: boolean;
|
|
10
13
|
on?: {
|
|
11
14
|
click?: () => void;
|
|
@@ -31,8 +34,8 @@ type Props = {
|
|
|
31
34
|
* | `--sc-kit--popover-item--divider-color` | Divider line color | `var(--sc-kit--color--border)` |
|
|
32
35
|
* | `--sc-kit--popover-item--divider-spacing` | Vertical gap added by divider | `var(--sc-kit--space--1)` |
|
|
33
36
|
* | `--sc-kit--popover-item--text-align` | Text alignment | `left` |
|
|
34
|
-
* | `--sc-kit--popover-item--margin-inline` | Inline margin when `inset` | `var(--sc-kit--space--2)` |
|
|
35
|
-
* | `--sc-kit--popover-item--border-radius` | Corner radius when `inset` | `var(--sc-kit--radius--sm)` |
|
|
37
|
+
* | `--sc-kit--popover-item--margin-inline` | Inline margin when `inset` (default on) | `var(--sc-kit--space--2)` |
|
|
38
|
+
* | `--sc-kit--popover-item--border-radius` | Corner radius when `inset` (default on) | `var(--sc-kit--radius--sm)` |
|
|
36
39
|
*/
|
|
37
40
|
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
38
41
|
type Cmp = ReturnType<typeof Cmp>;
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
<script lang="ts">import { Icon } from '../icon';
|
|
2
2
|
import { isIgnored } from './popover-ignore';
|
|
3
|
+
import { validateTrigger } from './validate-trigger';
|
|
3
4
|
import { autoUpdate, computePosition, flip, offset as offsetMiddleware, shift, size } from '@floating-ui/dom';
|
|
4
5
|
import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_20_regular.svg?raw';
|
|
5
6
|
import { tick } from 'svelte';
|
|
6
|
-
let { position = 'bottom-start', disabled = false, fixedPosition = false, offset = 4, boundaryMargin = 8, backdrop = false, matchWidth = false, panel = true, keepOpen = false, fillContainer = false, on, children, trigger } = $props();
|
|
7
|
+
let { position = 'bottom-start', disabled = false, fixedPosition = false, offset = 4, boundaryMargin = 8, backdrop = false, matchWidth = false, panel = true, keepOpen = false, fillContainer = false, on, children, trigger, 'aria-label': ariaLabel } = $props();
|
|
7
8
|
let opened = $state(false);
|
|
8
9
|
let triggerEl = $state.raw(undefined);
|
|
9
10
|
let contentEl = $state.raw(undefined);
|
|
@@ -169,9 +170,11 @@ const handleBackdropClick = (e) => {
|
|
|
169
170
|
class="popover__trigger"
|
|
170
171
|
class:popover__trigger--fill={fillContainer}
|
|
171
172
|
disabled={disabled}
|
|
173
|
+
aria-label={ariaLabel}
|
|
172
174
|
aria-haspopup="true"
|
|
173
175
|
aria-expanded={opened}
|
|
174
|
-
onclick={handleTriggerClick}
|
|
176
|
+
onclick={handleTriggerClick}
|
|
177
|
+
use:validateTrigger>
|
|
175
178
|
{#if trigger}
|
|
176
179
|
{@render trigger()}
|
|
177
180
|
{:else}
|
|
@@ -200,6 +203,11 @@ Popover — floating content anchored to a trigger element. Click trigger to ope
|
|
|
200
203
|
click outside, Escape, or click inside content (default) to close. Use `popoverIgnore`
|
|
201
204
|
action on a sub-tree to keep the popover open when clicks land inside that sub-tree.
|
|
202
205
|
|
|
206
|
+
The trigger renders inside a `<button>`, so trigger content must be non-interactive (an icon,
|
|
207
|
+
text). For a button-looking trigger, pass `Button` with `type="presentational"` (a styled `<span>`)
|
|
208
|
+
— never an interactive element, which would nest inside the trigger `<button>`. A dev-only guard
|
|
209
|
+
warns and outlines the trigger in red if interactive content is detected.
|
|
210
|
+
|
|
203
211
|
Imperative control via `bind:this` — the component exports `open()`, `close()`, `toggle()`
|
|
204
212
|
methods. Import `PopoverInstance` type from the barrel for typing the ref.
|
|
205
213
|
|
|
@@ -25,14 +25,25 @@ type Props = {
|
|
|
25
25
|
closed?: () => void;
|
|
26
26
|
};
|
|
27
27
|
children: Snippet;
|
|
28
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Custom trigger content; defaults to a chevron-down icon. Renders inside the trigger `<button>`,
|
|
30
|
+
* so content MUST be non-interactive — for a button-looking trigger use `Button` with
|
|
31
|
+
* `type="presentational"`. Nesting an interactive element triggers a dev-only warning.
|
|
32
|
+
*/
|
|
29
33
|
trigger?: Snippet;
|
|
34
|
+
/** Accessible name for the trigger button — required when the trigger content carries no text (an icon-only trigger). */
|
|
35
|
+
'aria-label'?: string;
|
|
30
36
|
};
|
|
31
37
|
/**
|
|
32
38
|
* Popover — floating content anchored to a trigger element. Click trigger to open;
|
|
33
39
|
* click outside, Escape, or click inside content (default) to close. Use `popoverIgnore`
|
|
34
40
|
* action on a sub-tree to keep the popover open when clicks land inside that sub-tree.
|
|
35
41
|
*
|
|
42
|
+
* The trigger renders inside a `<button>`, so trigger content must be non-interactive (an icon,
|
|
43
|
+
* text). For a button-looking trigger, pass `Button` with `type="presentational"` (a styled `<span>`)
|
|
44
|
+
* — never an interactive element, which would nest inside the trigger `<button>`. A dev-only guard
|
|
45
|
+
* warns and outlines the trigger in red if interactive content is detected.
|
|
46
|
+
*
|
|
36
47
|
* Imperative control via `bind:this` — the component exports `open()`, `close()`, `toggle()`
|
|
37
48
|
* methods. Import `PopoverInstance` type from the barrel for typing the ref.
|
|
38
49
|
*
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Action } from 'svelte/action';
|
|
2
|
+
/**
|
|
3
|
+
* Dev-only guard for the Popover trigger. The trigger renders inside a `<button>`, so its content
|
|
4
|
+
* must be non-interactive (use `Button` with `type="presentational"` for a button-looking trigger).
|
|
5
|
+
* If an interactive element is found inside, it logs a console warning pointing at the offending
|
|
6
|
+
* node and outlines the trigger in red. No-op in production.
|
|
7
|
+
*/
|
|
8
|
+
export declare const validateTrigger: Action<HTMLElement>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
const INTERACTIVE_SELECTOR = 'button, a[href], input:not([type="hidden"]), select, textarea, [role="button"], [role="link"], [role="menuitem"], [tabindex]:not([tabindex="-1"])';
|
|
2
|
+
/**
|
|
3
|
+
* Dev-only guard for the Popover trigger. The trigger renders inside a `<button>`, so its content
|
|
4
|
+
* must be non-interactive (use `Button` with `type="presentational"` for a button-looking trigger).
|
|
5
|
+
* If an interactive element is found inside, it logs a console warning pointing at the offending
|
|
6
|
+
* node and outlines the trigger in red. No-op in production.
|
|
7
|
+
*/
|
|
8
|
+
export const validateTrigger = (node) => {
|
|
9
|
+
if (!import.meta.env?.DEV) {
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const check = () => {
|
|
13
|
+
const offender = node.querySelector(INTERACTIVE_SELECTOR);
|
|
14
|
+
if (offender) {
|
|
15
|
+
node.style.outline = '2px solid red';
|
|
16
|
+
node.style.outlineOffset = '1px';
|
|
17
|
+
console.warn('[Popover] Trigger content must be non-interactive — it renders inside a <button>. Use <Button type="presentational"> (or plain markup) instead of an interactive element. Offending element:', offender);
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
node.style.removeProperty('outline');
|
|
21
|
+
node.style.removeProperty('outline-offset');
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
check();
|
|
25
|
+
const observer = new MutationObserver(check);
|
|
26
|
+
observer.observe(node, { childList: true });
|
|
27
|
+
return {
|
|
28
|
+
destroy() {
|
|
29
|
+
observer.disconnect();
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -2,21 +2,18 @@
|
|
|
2
2
|
import { Popover } from '../../popover';
|
|
3
3
|
import IconMoreHorizontal from '@fluentui/svg-icons/icons/more_horizontal_20_regular.svg?raw';
|
|
4
4
|
let { popoverPosition = 'bottom-end', children } = $props();
|
|
5
|
-
let popover = $state.raw(undefined);
|
|
6
5
|
</script>
|
|
7
6
|
|
|
8
|
-
<
|
|
9
|
-
<
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
</span>
|
|
19
|
-
</button>
|
|
7
|
+
<div class="table-actions-cell__dropdown-wrapper">
|
|
8
|
+
<Popover position={popoverPosition} fillContainer>
|
|
9
|
+
{#snippet trigger()}
|
|
10
|
+
<span class="table-actions-cell__trigger">
|
|
11
|
+
<Icon src={IconMoreHorizontal} />
|
|
12
|
+
</span>
|
|
13
|
+
{/snippet}
|
|
14
|
+
{@render children()}
|
|
15
|
+
</Popover>
|
|
16
|
+
</div>
|
|
20
17
|
|
|
21
18
|
<!--
|
|
22
19
|
@component
|
|
@@ -77,7 +77,7 @@ const handleChange = (updatedItems) => {
|
|
|
77
77
|
|
|
78
78
|
<Popover position="bottom-start" keepOpen>
|
|
79
79
|
{#snippet trigger()}
|
|
80
|
-
<Button type="
|
|
80
|
+
<Button type="presentational" variant="secondary" size="sm">
|
|
81
81
|
<IconText icon={{ src: IconColumnTwo, color: 'accent' }} secondaryIcon={IconChevronDown} size="sm" iconScale="large">{localization.columns}</IconText>
|
|
82
82
|
</Button>
|
|
83
83
|
{/snippet}
|
|
@@ -17,7 +17,7 @@ const singleGroupAction = $derived(availableGroupActions[0]);
|
|
|
17
17
|
{#if availableGroupActions.length > 1}
|
|
18
18
|
<Popover position="bottom-end">
|
|
19
19
|
{#snippet trigger()}
|
|
20
|
-
<Button type="
|
|
20
|
+
<Button type="presentational" variant="secondary" size="sm">
|
|
21
21
|
<IconText icon={IconChevronDown} iconPosition="trailing">{localization.actions}</IconText>
|
|
22
22
|
</Button>
|
|
23
23
|
{/snippet}
|
|
@@ -23,12 +23,12 @@ const changeSort = (direction) => {
|
|
|
23
23
|
<span> </span>
|
|
24
24
|
<Popover position="bottom-start">
|
|
25
25
|
{#snippet trigger()}
|
|
26
|
-
<
|
|
26
|
+
<span class="th-sortable__marker">
|
|
27
27
|
<span class="th-sortable__title">
|
|
28
28
|
{column.title}
|
|
29
29
|
</span>
|
|
30
30
|
<Icon src={IconCaretDownFilled} />
|
|
31
|
-
</
|
|
31
|
+
</span>
|
|
32
32
|
{/snippet}
|
|
33
33
|
|
|
34
34
|
<div class="th-sortable__popover">
|
|
@@ -97,6 +97,7 @@ const changeSort = (direction) => {
|
|
|
97
97
|
display: flex;
|
|
98
98
|
justify-content: space-between;
|
|
99
99
|
align-items: center;
|
|
100
|
+
font-weight: 400;
|
|
100
101
|
}
|
|
101
102
|
.th-sortable__marker {
|
|
102
103
|
--sc-kit--icon--size: 0.75em;
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { Icon } from '../icon';
|
|
3
3
|
import { MediaVolumeManager, PlaybackManager } from '../media-playback';
|
|
4
4
|
import { SeekBar } from '../seek-bar';
|
|
5
|
+
import { VideoLocalization } from './video-localization';
|
|
5
6
|
import IconPause from '@fluentui/svg-icons/icons/pause_20_regular.svg?raw';
|
|
6
7
|
import IconPlay from '@fluentui/svg-icons/icons/play_20_regular.svg?raw';
|
|
7
8
|
import IconSpeaker from '@fluentui/svg-icons/icons/speaker_2_20_regular.svg?raw';
|
|
@@ -9,6 +10,7 @@ import IconSpeakerMute from '@fluentui/svg-icons/icons/speaker_mute_20_regular.s
|
|
|
9
10
|
import { untrack } from 'svelte';
|
|
10
11
|
import { fade } from 'svelte/transition';
|
|
11
12
|
let { src, poster, id = randomNanoid(), autoplay = false, loop = false, inert = false, allowPreloading = false, hideSpeaker = false, hidePlayButton = false, intersectionContainer, scrubberPosition = 'bottom', on } = $props();
|
|
13
|
+
const localization = new VideoLocalization();
|
|
12
14
|
let video = $state(null);
|
|
13
15
|
let videoContainerRef = $state(null);
|
|
14
16
|
let showControlsOnHover = $state(false);
|
|
@@ -270,17 +272,22 @@ const handleSeek = (percent) => {
|
|
|
270
272
|
onmouseleave={() => (showControlsOnHover = false)}
|
|
271
273
|
role="none">
|
|
272
274
|
{#if isVideoPaused && !hidePlayButton}
|
|
273
|
-
<button type="button" aria-label=
|
|
275
|
+
<button type="button" aria-label={localization.play} class="video__playback-button" onclick={togglePlay} onkeydown={() => ({})}>
|
|
274
276
|
<Icon src={IconPlay} color="on-accent" />
|
|
275
277
|
</button>
|
|
276
278
|
{:else if showControlsOnHover && !hidePlayButton}
|
|
277
|
-
<button
|
|
279
|
+
<button
|
|
280
|
+
type="button"
|
|
281
|
+
aria-label={localization.pause}
|
|
282
|
+
class="video__playback-button video__playback-button--pause"
|
|
283
|
+
onclick={togglePlay}
|
|
284
|
+
onkeydown={() => ({})}>
|
|
278
285
|
<Icon src={IconPause} color="on-accent" />
|
|
279
286
|
</button>
|
|
280
287
|
{/if}
|
|
281
288
|
|
|
282
|
-
{#if (showControlsOnHover || MediaVolumeManager.isMuted) && !hideSpeaker}
|
|
283
|
-
<button type="button" aria-label={MediaVolumeManager.isMuted ?
|
|
289
|
+
{#if everActivated && (showControlsOnHover || MediaVolumeManager.isMuted) && !hideSpeaker}
|
|
290
|
+
<button type="button" aria-label={MediaVolumeManager.isMuted ? localization.unmute : localization.mute} class="video__mute-button" onclick={toggleMute}>
|
|
284
291
|
{#if MediaVolumeManager.isMuted}
|
|
285
292
|
<Icon src={IconSpeakerMute} color="on-accent" />
|
|
286
293
|
{:else}
|
|
@@ -322,6 +329,7 @@ A full-featured video player with custom overlay controls, play/pause, mute, see
|
|
|
322
329
|
| `--sc-kit--video--background-color` | Player background | `black` |
|
|
323
330
|
| `--sc-kit--video--border-radius` | Corner rounding | `0` |
|
|
324
331
|
| `--sc-kit--video--media-fit` | Video object-fit mode | `contain` |
|
|
332
|
+
| `--sc-kit--video--playback-button--size` | Size of the custom Play button | `2.5rem` |
|
|
325
333
|
| `--sc-kit--video--poster--media-fit` | Poster image object-fit mode | `cover` |
|
|
326
334
|
-->
|
|
327
335
|
|
|
@@ -329,6 +337,7 @@ A full-featured video player with custom overlay controls, play/pause, mute, see
|
|
|
329
337
|
--_video--background-color: var(--sc-kit--video--background-color, #000);
|
|
330
338
|
--_video--border-radius: var(--sc-kit--video--border-radius, 0);
|
|
331
339
|
--_video--media-fit: var(--sc-kit--video--media-fit, contain);
|
|
340
|
+
--_video--playback-button--size: var(--sc-kit--video--playback-button--size, 2.5rem);
|
|
332
341
|
--_video--poster--media-fit: var(--sc-kit--video--poster--media-fit, cover);
|
|
333
342
|
height: 100%;
|
|
334
343
|
min-height: 100%;
|
|
@@ -343,7 +352,7 @@ A full-featured video player with custom overlay controls, play/pause, mute, see
|
|
|
343
352
|
background: var(--_video--background-color);
|
|
344
353
|
}
|
|
345
354
|
.video__playback-button {
|
|
346
|
-
--sc-kit--icon--size:
|
|
355
|
+
--sc-kit--icon--size: var(--_video--playback-button--size);
|
|
347
356
|
--sc-kit--icon--filter: drop-shadow(1px 1px #000);
|
|
348
357
|
position: absolute;
|
|
349
358
|
top: 50%;
|
|
@@ -47,6 +47,7 @@ type Props = {
|
|
|
47
47
|
* | `--sc-kit--video--background-color` | Player background | `black` |
|
|
48
48
|
* | `--sc-kit--video--border-radius` | Corner rounding | `0` |
|
|
49
49
|
* | `--sc-kit--video--media-fit` | Video object-fit mode | `contain` |
|
|
50
|
+
* | `--sc-kit--video--playback-button--size` | Size of the custom Play button | `2.5rem` |
|
|
50
51
|
* | `--sc-kit--video--poster--media-fit` | Poster image object-fit mode | `cover` |
|
|
51
52
|
*/
|
|
52
53
|
declare const Cmp: import("svelte").Component<Props, {}, "">;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { AppLocale } from '../../core/locale';
|
|
2
|
+
const loc = {
|
|
3
|
+
play: {
|
|
4
|
+
en: 'Play',
|
|
5
|
+
no: 'Spill av'
|
|
6
|
+
},
|
|
7
|
+
pause: {
|
|
8
|
+
en: 'Pause',
|
|
9
|
+
no: 'Pause'
|
|
10
|
+
},
|
|
11
|
+
mute: {
|
|
12
|
+
en: 'Mute',
|
|
13
|
+
no: 'Demp lyd'
|
|
14
|
+
},
|
|
15
|
+
unmute: {
|
|
16
|
+
en: 'Unmute',
|
|
17
|
+
no: 'Slå på lyd'
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
export class VideoLocalization {
|
|
21
|
+
get play() {
|
|
22
|
+
return loc.play[AppLocale.current];
|
|
23
|
+
}
|
|
24
|
+
get pause() {
|
|
25
|
+
return loc.pause[AppLocale.current];
|
|
26
|
+
}
|
|
27
|
+
get mute() {
|
|
28
|
+
return loc.mute[AppLocale.current];
|
|
29
|
+
}
|
|
30
|
+
get unmute() {
|
|
31
|
+
return loc.unmute[AppLocale.current];
|
|
32
|
+
}
|
|
33
|
+
}
|