@streamscloud/kit 0.79.0 → 0.80.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.
@@ -1,8 +1,8 @@
1
1
  <script lang="ts">import { InputClearButton } from '../_internal/input-clear-button';
2
- import { Icon } from '../icon';
2
+ import { IconSlot } from '../icon';
3
3
  import { formatNumeral, NumeralThousandGroupStyles, registerCursorTracker } from 'cleave-zen';
4
4
  import { untrack } from 'svelte';
5
- const { size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, placeholder = '', title = '', name = '', id = null, autofocus = false, min = null, max = null, decimals = 2, suffix, 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, 'aria-required': ariaRequired, icon, iconPosition = 'leading', secondaryIcon, ...mode } = $props();
5
+ const { size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, placeholder = '', title = '', name = '', id = null, autofocus = false, min = null, max = null, decimals = 2, suffix, hideResolvedValue = false, 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, 'aria-required': ariaRequired, icon, iconPosition = 'leading', secondaryIcon, ...mode } = $props();
6
6
  let inputEl = $state.raw(undefined);
7
7
  let focused = $state(false);
8
8
  let typedValue = $state(undefined);
@@ -33,7 +33,7 @@ const resolveValue = (typed) => {
33
33
  return mode.clearable ? null : (min ?? mode.value);
34
34
  };
35
35
  const resolvedTypedValue = $derived(typedValue === undefined ? undefined : resolveValue(typedValue));
36
- const valueHintVisible = $derived(resolvedTypedValue !== typedValue);
36
+ const valueHintVisible = $derived(!hideResolvedValue && resolvedTypedValue !== typedValue);
37
37
  const initialText = untrack(() => displayValue);
38
38
  // Writing the prop into the field while focused would overwrite a typed "1" with min "8" before "12" can be typed.
39
39
  $effect(() => {
@@ -121,14 +121,10 @@ const initInput = (node) => {
121
121
  onkeydown={() => undefined}
122
122
  role="none">
123
123
  {#if icon && iconPosition === 'leading'}
124
- <span class="numeral-input__icon numeral-input__icon--leading" aria-hidden="true">
125
- {#if typeof icon === 'string'}<Icon src={icon} />{:else}{@render icon()}{/if}
126
- </span>
124
+ <span class="numeral-input__icon numeral-input__icon--leading" aria-hidden="true"><IconSlot icon={icon} /></span>
127
125
  {/if}
128
126
  {#if secondaryIcon && iconPosition === 'trailing'}
129
- <span class="numeral-input__icon numeral-input__icon--leading" aria-hidden="true">
130
- {#if typeof secondaryIcon === 'string'}<Icon src={secondaryIcon} />{:else}{@render secondaryIcon()}{/if}
131
- </span>
127
+ <span class="numeral-input__icon numeral-input__icon--leading" aria-hidden="true"><IconSlot icon={secondaryIcon} /></span>
132
128
  {/if}
133
129
 
134
130
  <!-- svelte-ignore a11y_autofocus -->
@@ -169,14 +165,10 @@ const initInput = (node) => {
169
165
  {/if}
170
166
 
171
167
  {#if icon && iconPosition === 'trailing'}
172
- <span class="numeral-input__icon numeral-input__icon--trailing" aria-hidden="true">
173
- {#if typeof icon === 'string'}<Icon src={icon} />{:else}{@render icon()}{/if}
174
- </span>
168
+ <span class="numeral-input__icon numeral-input__icon--trailing" aria-hidden="true"><IconSlot icon={icon} /></span>
175
169
  {/if}
176
170
  {#if secondaryIcon && iconPosition === 'leading'}
177
- <span class="numeral-input__icon numeral-input__icon--trailing" aria-hidden="true">
178
- {#if typeof secondaryIcon === 'string'}<Icon src={secondaryIcon} />{:else}{@render secondaryIcon()}{/if}
179
- </span>
171
+ <span class="numeral-input__icon numeral-input__icon--trailing" aria-hidden="true"><IconSlot icon={secondaryIcon} /></span>
180
172
  {/if}
181
173
  </div>
182
174
 
@@ -191,7 +183,7 @@ While focused the text belongs to the user: a value change from outside lands on
191
183
  emitted value is clamped to `min` / `max`, but the typed text is left alone until commit (blur,
192
184
  Enter, `change`) — so a prefix below `min` stays typeable. With `clearable: false` an empty
193
185
  field emits `min`, or keeps the held value when there is no `min`. Whenever the typed text resolves
194
- to a different value, that value shows next to the field as `→ 12`.
186
+ to a different value, that value shows next to the field as `→ 12` (off with `hideResolvedValue`).
195
187
 
196
188
  ### CSS Custom Properties
197
189
  | Property | Description | Default |
@@ -1,4 +1,4 @@
1
- import { type Snippet } from 'svelte';
1
+ import { type IconProp } from '../icon';
2
2
  type BaseProps = {
3
3
  /** @default 'md' */
4
4
  size?: 'sm' | 'md' | 'lg';
@@ -24,15 +24,17 @@ type BaseProps = {
24
24
  decimals?: number;
25
25
  /** Muted unit rendered after the value, outside the editable text (`px`, `%`, `°`). */
26
26
  suffix?: string;
27
+ /** Hides the `→ 12` hint shown while the typed text resolves to a different value — for narrow fields. */
28
+ hideResolvedValue?: boolean;
27
29
  'aria-describedby'?: string;
28
30
  'aria-label'?: string;
29
31
  'aria-required'?: boolean;
30
- /** Primary icon — SVG source string or snippet. */
31
- icon?: string | Snippet;
32
+ /** Primary icon — string SVG source, `{ src, color?, size? }` object, or custom snippet. */
33
+ icon?: IconProp;
32
34
  /** Side of the input where the primary icon sits. @default 'leading' */
33
35
  iconPosition?: 'leading' | 'trailing';
34
36
  /** Optional secondary icon — always on the opposite side from `icon`. */
35
- secondaryIcon?: string | Snippet;
37
+ secondaryIcon?: IconProp;
36
38
  };
37
39
  type ClearableProps = BaseProps & {
38
40
  /** Value can be cleared to `null`. Trigger shows an × button when set. */
@@ -73,7 +75,7 @@ type Props = ClearableProps | RequiredProps;
73
75
  * emitted value is clamped to `min` / `max`, but the typed text is left alone until commit (blur,
74
76
  * Enter, `change`) — so a prefix below `min` stays typeable. With `clearable: false` an empty
75
77
  * field emits `min`, or keeps the held value when there is no `min`. Whenever the typed text resolves
76
- * to a different value, that value shows next to the field as `→ 12`.
78
+ * to a different value, that value shows next to the field as `→ 12` (off with `hideResolvedValue`).
77
79
  *
78
80
  * ### CSS Custom Properties
79
81
  * | Property | Description | Default |
@@ -1,16 +1,59 @@
1
1
  <script lang="ts">import { Checkbox } from '../checkbox';
2
- import { ToolbarIconButton } from '../page-toolbar';
2
+ import { Icon, IconSlot } from '../icon';
3
+ import { IconText } from '../icon-text';
3
4
  import { PlayIndicator } from '../play-indicator';
4
- import { Popover } from '../popover';
5
+ import { Popover, PopoverItem } from '../popover';
5
6
  import { TileCardLocalization } from './tile-card-localization';
6
7
  import IconMore from '@fluentui/svg-icons/icons/more_vertical_20_regular.svg?raw';
7
- const { title, meta, preview, menu, menuLabel, selected = false, checkboxVisibility = 'hover', on } = $props();
8
+ const { title, meta, preview, actions, selected = false, checkboxVisibility = 'hover', on } = $props();
8
9
  const localization = new TileCardLocalization();
10
+ const actionItems = $derived(actions?.items ?? []);
11
+ const actionsPosition = $derived(preview ? (actions?.position ?? 'footer') : 'footer');
12
+ let menuOpened = $state(false);
13
+ let keyboardFocused = $state(false);
9
14
  // Safari paints no frame for preload="metadata" until a seek target is given.
10
15
  const withFirstFrame = (src) => (src.includes('#') ? src : `${src}#t=0.001`);
11
16
  </script>
12
17
 
13
- <div class="tile-card" class:tile-card--selected={selected} class:tile-card--checkbox-always={checkboxVisibility === 'always'}>
18
+ {#snippet actionsBlock()}
19
+ {#if actionItems.length}
20
+ <div class="tile-card__actions tile-card__actions--{actionsPosition}">
21
+ {#if actionItems.length === 1 && actionsPosition === 'top' && actions?.singleAsButton}
22
+ <button
23
+ type="button"
24
+ class="tile-card__action"
25
+ class:tile-card__action--overlay={actionsPosition === 'top'}
26
+ aria-label={actionItems[0].label}
27
+ title={actionItems[0].label}
28
+ onclick={() => actionItems[0].onClick()}>
29
+ <IconSlot icon={actionItems[0].icon} fallbacks={{ size: 'control-md' }} />
30
+ </button>
31
+ {:else}
32
+ <Popover
33
+ position="bottom-end"
34
+ fixedPosition
35
+ aria-label={actions?.label ?? localization.actions}
36
+ on={{ opened: () => (menuOpened = true), closed: () => (menuOpened = false) }}>
37
+ {#snippet trigger()}
38
+ <span class="tile-card__action" class:tile-card__action--overlay={actionsPosition === 'top'}><Icon src={IconMore} size="control-md" /></span>
39
+ {/snippet}
40
+ {#each actionItems as action, index (index)}
41
+ <PopoverItem on={{ click: action.onClick }}><IconText icon={action.icon} size="sm">{action.label}</IconText></PopoverItem>
42
+ {/each}
43
+ </Popover>
44
+ {/if}
45
+ </div>
46
+ {/if}
47
+ {/snippet}
48
+
49
+ <div
50
+ class="tile-card"
51
+ class:tile-card--selected={selected}
52
+ class:tile-card--checkbox-always={checkboxVisibility === 'always'}
53
+ class:tile-card--menu-opened={menuOpened}
54
+ class:tile-card--keyboard-focused={keyboardFocused}
55
+ onfocusin={(e) => (keyboardFocused = (e.target as HTMLElement).matches(':focus-visible'))}
56
+ onfocusout={() => (keyboardFocused = false)}>
14
57
  {#if on?.activate}
15
58
  <button type="button" class="tile-card__hit" aria-label={title} title={title} onclick={() => on?.activate?.()}></button>
16
59
  {/if}
@@ -33,17 +76,13 @@ const withFirstFrame = (src) => (src.includes('#') ? src : `${src}#t=0.001`);
33
76
  <span class="tile-card__meta">{meta}</span>
34
77
  {/if}
35
78
  </div>
36
- {#if menu}
37
- <div class="tile-card__menu">
38
- <Popover position="bottom-end" fixedPosition aria-label={menuLabel ?? localization.actions}>
39
- {#snippet trigger()}
40
- <ToolbarIconButton icon={IconMore} presentational />
41
- {/snippet}
42
- {@render menu()}
43
- </Popover>
44
- </div>
79
+ {#if actionsPosition === 'footer'}
80
+ {@render actionsBlock()}
45
81
  {/if}
46
82
  </div>
83
+ {#if actionsPosition === 'top'}
84
+ {@render actionsBlock()}
85
+ {/if}
47
86
  {#if on?.select}
48
87
  <div class="tile-card__checkbox">
49
88
  <Checkbox checked={selected} variant="on-accent" title={localization.select} on={{ change: (value) => on?.select?.(value) }} />
@@ -53,8 +92,9 @@ const withFirstFrame = (src) => (src.includes('#') ? src : `${src}#t=0.001`);
53
92
 
54
93
  <!--
55
94
  @component
56
- File-browser style tile: a full-bleed preview on top and a footer with title, optional meta line and a kebab menu. Selection
57
- checkbox via `on.select`, whole-card activation via `on.activate`. For a drop-target highlight wrap it in `DropZone`.
95
+ File-browser style tile: a full-bleed preview on top and a footer with title and optional meta line. `actions` sit at the end
96
+ of the footer or over the preview's top-right corner. Selection checkbox via `on.select`, whole-card activation via
97
+ `on.activate`. For a drop-target highlight wrap it in `DropZone`.
58
98
 
59
99
  ### CSS Custom Properties
60
100
  | Property | Description | Default |
@@ -79,6 +119,7 @@ checkbox via `on.select`, whole-card activation via `on.activate`. For a drop-ta
79
119
  --_tile-card--preview-aspect-ratio: var(--sc-kit--tile-card--preview--aspect-ratio, 7 / 5);
80
120
  --_tile-card--footer-min-height: var(--sc-kit--tile-card--footer--min-height, 3.25rem);
81
121
  --_tile-card--checkbox-opacity: 0;
122
+ --_tile-card--top-actions-opacity: 0;
82
123
  position: relative;
83
124
  display: flex;
84
125
  flex-direction: column;
@@ -93,8 +134,12 @@ checkbox via `on.select`, whole-card activation via `on.activate`. For a drop-ta
93
134
  .tile-card:hover {
94
135
  --_tile-card--border-color: var(--_tile-card--border-color-hover);
95
136
  }
96
- .tile-card:hover, .tile-card:focus-within {
137
+ .tile-card:hover, .tile-card--keyboard-focused {
97
138
  --_tile-card--checkbox-opacity: 1;
139
+ --_tile-card--top-actions-opacity: 1;
140
+ }
141
+ .tile-card--menu-opened {
142
+ --_tile-card--top-actions-opacity: 1;
98
143
  }
99
144
  .tile-card:has(> .tile-card__hit:focus-visible) {
100
145
  outline: 2px solid var(--sc-kit--color--border--focus);
@@ -112,6 +157,7 @@ checkbox via `on.select`, whole-card activation via `on.activate`. For a drop-ta
112
157
  @media (hover: none) {
113
158
  .tile-card {
114
159
  --_tile-card--checkbox-opacity: 1;
160
+ --_tile-card--top-actions-opacity: 1;
115
161
  }
116
162
  }
117
163
  .tile-card__hit {
@@ -182,10 +228,46 @@ checkbox via `on.select`, whole-card activation via `on.activate`. For a drop-ta
182
228
  white-space: nowrap;
183
229
  overflow: hidden;
184
230
  }
185
- .tile-card__menu {
231
+ .tile-card__actions {
186
232
  position: relative;
187
233
  display: flex;
188
234
  }
235
+ .tile-card__actions--top {
236
+ position: absolute;
237
+ inset-block-start: var(--sc-kit--space--3);
238
+ inset-inline-end: var(--sc-kit--space--3);
239
+ opacity: var(--_tile-card--top-actions-opacity);
240
+ transition: opacity var(--sc-kit--duration--base) var(--sc-kit--ease--default);
241
+ }
242
+ .tile-card__action {
243
+ --sc-kit--icon--color: var(--sc-kit--color--text--muted);
244
+ display: inline-flex;
245
+ align-items: center;
246
+ justify-content: center;
247
+ inline-size: 1.75rem;
248
+ block-size: 1.75rem;
249
+ padding: 0;
250
+ background: transparent;
251
+ border: 0;
252
+ border-radius: var(--sc-kit--field--border-radius--sm);
253
+ cursor: pointer;
254
+ transition: background-color var(--sc-kit--duration--base) var(--sc-kit--ease--default);
255
+ }
256
+ .tile-card__action:hover {
257
+ background: var(--sc-kit--color--bg--hover);
258
+ }
259
+ .tile-card__action:focus-visible {
260
+ outline: 2px solid var(--sc-kit--color--border--focus);
261
+ outline-offset: 2px;
262
+ }
263
+ .tile-card__action--overlay {
264
+ --sc-kit--icon--color: var(--sc-kit--color--text--primary);
265
+ background: var(--sc-kit--color--bg--panel);
266
+ box-shadow: var(--sc-kit--shadow--md);
267
+ }
268
+ .tile-card__action--overlay:hover {
269
+ background: var(--sc-kit--color--bg--hover);
270
+ }
189
271
  .tile-card__checkbox {
190
272
  position: absolute;
191
273
  inset-block-start: var(--sc-kit--space--3);
@@ -1,28 +1,25 @@
1
- import type { TileCardCheckboxVisibility, TileCardPreview } from './types';
2
- import type { Snippet } from 'svelte';
1
+ import type { TileCardActions, TileCardCheckboxVisibility, TileCardPreview } from './types';
3
2
  type Props = {
4
3
  title: string;
5
4
  /** Secondary line under the title. */
6
5
  meta?: string;
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`. */
6
+ /** 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 `actions` or `on.activate`. */
8
7
  preview?: TileCardPreview;
9
- /** `PopoverItem`s of the kebab menu at the end of the footer. */
10
- menu?: Snippet;
11
- /** Accessible name of the kebab menu. @default localized "Actions" */
12
- menuLabel?: string;
8
+ actions?: TileCardActions;
13
9
  selected?: boolean;
14
- /** When the checkbox shows: `'hover'` — on hover, focus-within, when selected and on touch screens; `'always'` — permanently. @default 'hover' */
10
+ /** When the checkbox shows: `'hover'` — on hover, keyboard focus, when selected and on touch screens; `'always'` — permanently. @default 'hover' */
15
11
  checkboxVisibility?: TileCardCheckboxVisibility;
16
12
  on?: {
17
13
  /** Renders the checkbox at the top-left of the preview. */
18
14
  select?: (value: boolean) => void;
19
- /** Fires on a click on the card surface or Enter / Space; the checkbox and the menu don't trigger it. */
15
+ /** Fires on a click on the card surface or Enter / Space; the checkbox and the actions don't trigger it. */
20
16
  activate?: () => void;
21
17
  };
22
18
  };
23
19
  /**
24
- * File-browser style tile: a full-bleed preview on top and a footer with title, optional meta line and a kebab menu. Selection
25
- * checkbox via `on.select`, whole-card activation via `on.activate`. For a drop-target highlight wrap it in `DropZone`.
20
+ * File-browser style tile: a full-bleed preview on top and a footer with title and optional meta line. `actions` sit at the end
21
+ * of the footer or over the preview's top-right corner. Selection checkbox via `on.select`, whole-card activation via
22
+ * `on.activate`. For a drop-target highlight wrap it in `DropZone`.
26
23
  *
27
24
  * ### CSS Custom Properties
28
25
  * | Property | Description | Default |
@@ -1,2 +1,2 @@
1
- export type { TileCardCheckboxVisibility, TileCardPreview } from './types';
1
+ export type { TileCardAction, TileCardActions, TileCardCheckboxVisibility, TileCardPreview } from './types';
2
2
  export { default as TileCard } from './cmp.tile-card.svelte';
@@ -1,3 +1,4 @@
1
+ import type { IconProp } from '../icon';
1
2
  import type { Snippet } from 'svelte';
2
3
  export type TileCardPreview = {
3
4
  image: string;
@@ -6,3 +7,22 @@ export type TileCardPreview = {
6
7
  poster?: string;
7
8
  } | Snippet;
8
9
  export type TileCardCheckboxVisibility = 'hover' | 'always';
10
+ /** One card action. `label` is the menu item text, or the accessible name and tooltip when it renders as a lone icon button. */
11
+ export type TileCardAction = {
12
+ label: string;
13
+ icon: IconProp;
14
+ onClick: () => void;
15
+ };
16
+ export type TileCardActions = {
17
+ items: TileCardAction[];
18
+ /**
19
+ * `'footer'` — at the end of the footer; `'top'` — over the top-right corner of the preview, revealed on hover, keyboard focus, while its menu is
20
+ * open and on touch screens. A card without a `preview` always uses `'footer'`.
21
+ * @default 'footer'
22
+ */
23
+ position?: 'footer' | 'top';
24
+ /** With `position: 'top'`, a single item renders as its own icon button instead of a one-item kebab menu. @default false */
25
+ singleAsButton?: boolean;
26
+ /** Accessible name of the kebab menu. @default localized "Actions" */
27
+ label?: string;
28
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.79.0",
3
+ "version": "0.80.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",