@streamscloud/kit 0.11.3 → 0.13.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.
Files changed (29) hide show
  1. package/dist/core/validation/validation-schemas/number-validations.d.ts +2 -2
  2. package/dist/core/validation/validation-schemas/number-validations.js +2 -8
  3. package/dist/core/validation/validation-schemas/types.d.ts +0 -2
  4. package/dist/ui/button/cmp.button.svelte +20 -13
  5. package/dist/ui/button/cmp.button.svelte.d.ts +7 -2
  6. package/dist/ui/card-actions/cmp.card-action.svelte +24 -16
  7. package/dist/ui/card-actions/cmp.card-action.svelte.d.ts +2 -0
  8. package/dist/ui/card-actions/cmp.card-actions.svelte +5 -5
  9. package/dist/ui/card-actions/cmp.card-actions.svelte.d.ts +2 -2
  10. package/dist/ui/grid-card/cmp.grid-card-media.svelte +11 -9
  11. package/dist/ui/page-toolbar/cmp.toolbar-popover.svelte +1 -1
  12. package/dist/ui/pagination/cmp.pagination.svelte +3 -3
  13. package/dist/ui/popover/cmp.hover-popover.svelte +1 -1
  14. package/dist/ui/popover/cmp.hover-popover.svelte.d.ts +1 -1
  15. package/dist/ui/popover/cmp.popover-item.svelte +3 -3
  16. package/dist/ui/popover/cmp.popover-item.svelte.d.ts +6 -3
  17. package/dist/ui/popover/cmp.popover.svelte +10 -2
  18. package/dist/ui/popover/cmp.popover.svelte.d.ts +12 -1
  19. package/dist/ui/popover/validate-trigger.d.ts +8 -0
  20. package/dist/ui/popover/validate-trigger.js +32 -0
  21. package/dist/ui/table/table-cells/table-actions-cell.svelte +10 -13
  22. package/dist/ui/table/table-columns-manager/cmp.table-columns-manager.svelte +1 -1
  23. package/dist/ui/table/table-group-actions/cmp.table-group-actions.svelte +1 -1
  24. package/dist/ui/table/table-headers/table-header-sortable.svelte +3 -2
  25. package/dist/ui/video/cmp.video.svelte +14 -5
  26. package/dist/ui/video/cmp.video.svelte.d.ts +1 -0
  27. package/dist/ui/video/video-localization.d.ts +6 -0
  28. package/dist/ui/video/video-localization.js +33 -0
  29. package/package.json +1 -1
@@ -1,4 +1,4 @@
1
1
  import type { MinNumberValidation, NumberValidation } from './types';
2
2
  import * as yup from 'yup';
3
- export declare const numberValidationSchema: (rules: NumberValidation) => yup.NumberSchema<number | undefined, yup.AnyObject, undefined, "">;
4
- export declare const minNumberValidationSchema: (rules: MinNumberValidation) => yup.NumberSchema<number | undefined, yup.AnyObject, undefined, "">;
3
+ export declare const numberValidationSchema: (rules: NumberValidation) => yup.NumberSchema<number | null | undefined, yup.AnyObject, undefined, "">;
4
+ export declare const minNumberValidationSchema: (rules: MinNumberValidation) => yup.NumberSchema<number | null | undefined, yup.AnyObject, undefined, "">;
@@ -2,10 +2,7 @@ import { ValidationLocalization } from './validation-localization';
2
2
  import * as yup from 'yup';
3
3
  export const numberValidationSchema = (rules) => {
4
4
  const msg = new ValidationLocalization();
5
- let schema = yup.number().typeError(msg.badFormat);
6
- if (rules.isRequired) {
7
- schema = schema.required(msg.required);
8
- }
5
+ let schema = yup.number().typeError(msg.badFormat).nullable();
9
6
  if (rules.minValue !== null) {
10
7
  const minValue = rules.minExclusive ? rules.minValue + 1 : rules.minValue;
11
8
  schema = schema.min(minValue, msg.min(minValue));
@@ -18,10 +15,7 @@ export const numberValidationSchema = (rules) => {
18
15
  };
19
16
  export const minNumberValidationSchema = (rules) => {
20
17
  const msg = new ValidationLocalization();
21
- let schema = yup.number().typeError(msg.badFormat);
22
- if (rules.isRequired) {
23
- schema = schema.required(msg.required);
24
- }
18
+ let schema = yup.number().typeError(msg.badFormat).nullable();
25
19
  if (rules.minValue !== null) {
26
20
  const minValue = rules.minExclusive ? rules.minValue + 1 : rules.minValue;
27
21
  schema = schema.min(minValue, msg.min(minValue));
@@ -6,14 +6,12 @@ export type TextWithFormatValidation = TextValidation & {
6
6
  format: string;
7
7
  };
8
8
  export type NumberValidation = {
9
- isRequired: boolean;
10
9
  minValue: number | null;
11
10
  maxValue: number | null;
12
11
  minExclusive: boolean;
13
12
  maxExclusive: boolean;
14
13
  };
15
14
  export type MinNumberValidation = {
16
- isRequired: boolean;
17
15
  minValue: number | null;
18
16
  minExclusive: boolean;
19
17
  };
@@ -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 disabledVariantClass = $derived(inactive ? `btn--${variant}-disabled` : '');
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="btn btn--{variant} btn--{size} {disabledVariantClass}"
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="btn btn--{variant} btn--{size} {disabledVariantClass}"
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`. Otherwise `type` is the native `<button type>` (`'button' | 'submit' | 'reset'`).
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 Props = ButtonModeProps | AnchorModeProps;
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`. Otherwise `type` is the native `<button type>` (`'button' | 'submit' | 'reset'`).
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
- <button
6
- type="button"
7
- class="card-action"
8
- title={action.title ?? ''}
9
- disabled={action.disabled}
10
- onclick={(e) => {
11
- if (!action.propagateClickEvent) {
12
- e.stopPropagation();
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
- action.callback?.();
16
- on?.click?.();
17
- }}>
18
- <IconSlot icon={action.icon} fallbacks={{ color: 'text' }} />
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.2);
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, propagateClickEvent: true }} />
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--panel` |
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 | inherits `--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--panel));
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(--_card-actions--background));
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--panel` |
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 | inherits `--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
- {#if items[0].playable}
53
- <VideoPlayer
54
- src={items[0].url}
55
- poster={items[0].cover}
56
- active={true}
57
- on={{ timeUpdate: onPlayerTimeUpdate, durationChange: onPlayerDurationChange, activate: onPlayerActivate }} />
58
- {:else}
59
- <Image src={items[0].url} showStubOnError={true} />
60
- {/if}
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="button" variant="secondary" size={size}>
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
- <button type="button" class="pagination__node pagination__node--trigger" aria-label={localization.pageSize}>
62
+ <span class="pagination__node pagination__node--trigger">
63
63
  {state.pageSize}
64
64
  <Icon src={IconChevronDown} />
65
- </button>
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="button">Hover me</Button>{/snippet}
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="button">Hover me</Button>{/snippet}
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 = false, on, children } = $props();
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
- /** Inset the item from the popover edges: adds an inline margin and rounds the hover background. */
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
- /** Custom trigger snippet; defaults to a chevron-down icon. */
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
- <button type="button" class="table-actions-cell__dropdown-wrapper" onclick={() => popover?.toggle()}>
9
- <span>
10
- <Popover bind:this={popover} position={popoverPosition}>
11
- {#snippet trigger()}
12
- <button type="button" class="table-actions-cell__trigger">
13
- <Icon src={IconMoreHorizontal} />
14
- </button>
15
- {/snippet}
16
- {@render children()}
17
- </Popover>
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="button" variant="secondary" size="sm">
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="button" variant="secondary" size="sm">
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>&nbsp;</span>
24
24
  <Popover position="bottom-start">
25
25
  {#snippet trigger()}
26
- <button type="button" class="th-sortable__marker">
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
- </button>
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="play" class="video__playback-button" onclick={togglePlay} onkeydown={() => ({})}>
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 type="button" aria-label="pause" class="video__playback-button video__playback-button--pause" onclick={togglePlay} onkeydown={() => ({})}>
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 ? 'mute' : 'unmute'} class="video__mute-button" onclick={toggleMute}>
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: 2.5rem;
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,6 @@
1
+ export declare class VideoLocalization {
2
+ get play(): string;
3
+ get pause(): string;
4
+ get mute(): string;
5
+ get unmute(): string;
6
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.11.3",
3
+ "version": "0.13.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",