@streamscloud/kit 0.75.0 → 0.76.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,3 +1,4 @@
1
+ import { DEV } from 'esm-env';
1
2
  /**
2
3
  * Picks the plural form matching `count` for `locale`.
3
4
  *
@@ -17,7 +18,7 @@
17
18
  */
18
19
  export const resolvePluralForm = ({ count, candidates, locale }) => {
19
20
  const { index, categoriesLength } = indexerFor(locale);
20
- if (import.meta.env?.DEV && candidates.length !== categoriesLength) {
21
+ if (DEV && candidates.length !== categoriesLength) {
21
22
  throw new Error(`resolvePluralForm(): locale "${locale}" needs ${categoriesLength} candidate(s), got ${JSON.stringify(candidates)}.`);
22
23
  }
23
24
  const template = candidates[index(count)] ?? candidates[candidates.length - 1];
@@ -2,7 +2,8 @@ export declare class DomHelper {
2
2
  private static readonly _interactiveSelector;
3
3
  /**
4
4
  * First interactive descendant of `root`, or `null`. Use to reject interactive content from a
5
- * container that must stay non-interactive (a `<button>` trigger, a `<label>`).
5
+ * container that must stay non-interactive (a `<button>` trigger, a `<label>`). Anything inside an
6
+ * `inert` subtree is skipped — it takes neither focus nor clicks.
6
7
  */
7
8
  static findInteractiveDescendant(root: Element): HTMLElement | null;
8
9
  /**
@@ -2,10 +2,12 @@ export class DomHelper {
2
2
  static _interactiveSelector = 'button, a[href], input:not([type="hidden"]), select, textarea, [role="button"], [role="link"], [role="menuitem"], [tabindex]:not([tabindex="-1"])';
3
3
  /**
4
4
  * First interactive descendant of `root`, or `null`. Use to reject interactive content from a
5
- * container that must stay non-interactive (a `<button>` trigger, a `<label>`).
5
+ * container that must stay non-interactive (a `<button>` trigger, a `<label>`). Anything inside an
6
+ * `inert` subtree is skipped — it takes neither focus nor clicks.
6
7
  */
7
8
  static findInteractiveDescendant(root) {
8
- return root.querySelector(DomHelper._interactiveSelector);
9
+ const candidates = root.querySelectorAll(DomHelper._interactiveSelector);
10
+ return Array.from(candidates).find((candidate) => !candidate.closest('[inert]')) ?? null;
9
11
  }
10
12
  /**
11
13
  * True when the target is, or sits inside, interactive content — i.e. something that owns its own
@@ -35,12 +35,9 @@ const previewBackgroundColor = $derived.by(() => {
35
35
  });
36
36
  const alphaPercent = $derived(displayColor ? `${Math.round(colord(displayColor).alpha() * 100)}%` : '');
37
37
  const triggerPlaceholder = $derived(placeholder ?? loc.notSet);
38
- const resetValue = (e) => {
39
- e.preventDefault();
40
- e.stopPropagation();
41
- if (clearable) {
42
- on?.change?.('');
43
- }
38
+ const clearButtonVisible = $derived(clearable && !disabled && !children && variant === 'fill' && !!value);
39
+ const resetValue = () => {
40
+ on?.change?.('');
44
41
  };
45
42
  const onColorPickerInput = ({ hex }) => {
46
43
  on?.change?.(ColorHelper.normalizeHex(hex));
@@ -88,10 +85,8 @@ const onColorPickerInput = ({ hex }) => {
88
85
  <span class="color-picker__chevron" aria-hidden="true">
89
86
  <Icon src={IconChevronDown} color={displayColor ? undefined : 'muted'} />
90
87
  </span>
91
- {:else if clearable}
92
- <button type="button" class="color-picker__clear" aria-label={loc.clear} onclick={resetValue}>
93
- <Icon src={IconDismiss} />
94
- </button>
88
+ {:else if clearButtonVisible}
89
+ <span class="color-picker__clear-glyph"><Icon src={IconDismiss} /></span>
95
90
  {:else}
96
91
  <span class="color-picker__chevron" aria-hidden="true">
97
92
  <Icon src={IconChevronDown} />
@@ -111,6 +106,9 @@ const onColorPickerInput = ({ hex }) => {
111
106
  textInputModes={['hex']} />
112
107
  </div>
113
108
  </Popover>
109
+ {#if clearButtonVisible}
110
+ <button type="button" class="color-picker__clear" class:color-picker__clear--sm={size === 'sm'} aria-label={loc.clear} onclick={resetValue}></button>
111
+ {/if}
114
112
  </div>
115
113
 
116
114
  <!--
@@ -123,19 +121,6 @@ same height.
123
121
  When `value` is set, the trigger's background fills with the color and the swatch text auto-flips
124
122
  between black and white for contrast.
125
123
 
126
- ### Props
127
- | Prop | Type | Default |
128
- |---|---|---|
129
- | `value` | `string \| null \| undefined` | required |
130
- | `variant` | `'fill' \| 'field'` | `'fill'` |
131
- | `label` | `string` (field variant row label) | — |
132
- | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` |
133
- | `clearable` | `boolean` | `false` |
134
- | `disabled` | `boolean` | `false` |
135
- | `defaultColor` | `string \| undefined` | `undefined` |
136
- | `placeholder` | `string \| undefined` | localized "not set" |
137
- | `previewIcon` | `string \| Snippet` | color-droplet glyph |
138
-
139
124
  ### CSS Custom Properties
140
125
  | Property | Description | Default |
141
126
  |---|---|---|
@@ -206,19 +191,29 @@ between black and white for contrast.
206
191
  color: var(--sc-kit--color--text--secondary);
207
192
  font-size: var(--sc-kit--font-size--sm);
208
193
  }
209
- .color-picker__chevron, .color-picker__clear {
194
+ .color-picker__chevron, .color-picker__clear-glyph {
210
195
  display: inline-flex;
211
196
  align-items: center;
212
197
  justify-content: center;
198
+ line-height: 0;
199
+ }
200
+ .color-picker__clear {
201
+ position: absolute;
202
+ inset-block: 0;
203
+ inset-inline-end: 0;
204
+ aspect-ratio: 1;
205
+ padding: 0;
213
206
  background: transparent;
214
207
  border: 0;
215
- padding: 0;
216
- line-height: 0;
217
- color: inherit;
208
+ border-radius: var(--sc-kit--field--border-radius--md);
218
209
  cursor: pointer;
219
210
  }
220
- .color-picker__chevron {
221
- cursor: inherit;
211
+ .color-picker__clear:focus-visible {
212
+ outline: 2px solid var(--sc-kit--color--border--focus);
213
+ outline-offset: -2px;
214
+ }
215
+ .color-picker__clear--sm {
216
+ border-radius: var(--sc-kit--field--border-radius--sm);
222
217
  }
223
218
  .color-picker__panel {
224
219
  --focus-color: none;
@@ -13,7 +13,7 @@ type Props = {
13
13
  clearable?: boolean;
14
14
  /** Visual disabled state — popover won't open. */
15
15
  disabled?: boolean;
16
- /** Placeholder shown on the swatch trigger when no value is selected. */
16
+ /** Placeholder shown on the swatch trigger when no value is selected. @default localized "Not set" */
17
17
  placeholder?: string;
18
18
  on?: {
19
19
  change?: (value: string) => void;
@@ -35,19 +35,6 @@ type Props = {
35
35
  * When `value` is set, the trigger's background fills with the color and the swatch text auto-flips
36
36
  * between black and white for contrast.
37
37
  *
38
- * ### Props
39
- * | Prop | Type | Default |
40
- * |---|---|---|
41
- * | `value` | `string \| null \| undefined` | required |
42
- * | `variant` | `'fill' \| 'field'` | `'fill'` |
43
- * | `label` | `string` (field variant row label) | — |
44
- * | `size` | `'sm' \| 'md' \| 'lg'` | `'md'` |
45
- * | `clearable` | `boolean` | `false` |
46
- * | `disabled` | `boolean` | `false` |
47
- * | `defaultColor` | `string \| undefined` | `undefined` |
48
- * | `placeholder` | `string \| undefined` | localized "not set" |
49
- * | `previewIcon` | `string \| Snippet` | color-droplet glyph |
50
- *
51
38
  * ### CSS Custom Properties
52
39
  * | Property | Description | Default |
53
40
  * |---|---|---|
@@ -54,8 +54,8 @@ $effect(() => untrack(() => {
54
54
  {/snippet}
55
55
  {#snippet footer()}
56
56
  <div class="image-editor-dialog__footer">
57
- <DialogCancelButton on={{ click: cancel }}>{loc.cancel}</DialogCancelButton>
58
- <DialogButton on={{ click: save }}>{loc.save}</DialogButton>
57
+ <DialogCancelButton size="sm" on={{ click: cancel }}>{loc.cancel}</DialogCancelButton>
58
+ <DialogButton size="sm" on={{ click: save }}>{loc.save}</DialogButton>
59
59
  </div>
60
60
  {/snippet}
61
61
  </Dialog>
@@ -63,6 +63,14 @@ $effect(() => untrack(() => {
63
63
 
64
64
  <style>.image-editor-dialog {
65
65
  --sc-kit--dialog--height: calc(100vh - 2.5rem);
66
+ --sc-kit--dialog--footer--padding-block: var(--sc-kit--space--2);
67
+ --sc-kit--dialog--footer--padding-inline: var(--sc-kit--space--4);
68
+ --sc-kit--dialog--header--padding-block: var(--sc-kit--space--1);
69
+ --sc-kit--dialog--header--padding-inline: var(--sc-kit--space--3);
70
+ --sc-kit--dialog--title--font-size: var(--sc-kit--font-size--sm);
71
+ --sc-kit--dialog--title--line-height: var(--sc-kit--line-height--sm);
72
+ --sc-kit--dialog-close-button--size: 1.5rem;
73
+ --sc-kit--dialog-close-button--icon--size: 1rem;
66
74
  display: contents;
67
75
  }
68
76
  .image-editor-dialog__workspace {
@@ -70,7 +78,9 @@ $effect(() => untrack(() => {
70
78
  min-block-size: 0;
71
79
  }
72
80
  .image-editor-dialog__footer {
81
+ --sc-kit--dialog-cancel-button--min-width: 4.5rem;
82
+ --sc-kit--dialog-button--min-width: 7rem;
73
83
  display: flex;
74
84
  justify-content: flex-end;
75
- gap: 1.5rem;
85
+ gap: var(--sc-kit--space--4);
76
86
  }</style>
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">import { default as Button } from '../button/cmp.button.svelte';
2
- let { type = 'button', disabled = false, loading = false, variant = 'primary', icon, on, children } = $props();
2
+ let { type = 'button', disabled = false, loading = false, variant = 'primary', size = 'md', icon, on, children } = $props();
3
3
  </script>
4
4
 
5
5
  <div class="dialog-button">
6
- <Button type={type} disabled={disabled} loading={loading} variant={variant} icon={icon} on={on}>
6
+ <Button type={type} disabled={disabled} loading={loading} variant={variant} size={size} icon={icon} on={on}>
7
7
  {@render children()}
8
8
  </Button>
9
9
  </div>
@@ -1,4 +1,4 @@
1
- import type { ButtonVariant } from '../button/types';
1
+ import type { ButtonSize, ButtonVariant } from '../button/types';
2
2
  import type { IconProp } from '../icon';
3
3
  import type { Snippet } from 'svelte';
4
4
  type Props = {
@@ -9,6 +9,8 @@ type Props = {
9
9
  loading?: boolean;
10
10
  /** Button color variant @default 'primary' */
11
11
  variant?: ButtonVariant;
12
+ /** @default 'md' */
13
+ size?: ButtonSize;
12
14
  /** Leading icon — passed through to Button. */
13
15
  icon?: IconProp;
14
16
  on?: {
@@ -1,9 +1,9 @@
1
1
  <script lang="ts">import { default as Button } from '../button/cmp.button.svelte';
2
- let { disabled = false, on, children } = $props();
2
+ let { disabled = false, size = 'md', on, children } = $props();
3
3
  </script>
4
4
 
5
5
  <div class="dialog-cancel-button">
6
- <Button type="button" disabled={disabled} variant="secondary" on={on}>
6
+ <Button type="button" disabled={disabled} variant="secondary" size={size} on={on}>
7
7
  {@render children()}
8
8
  </Button>
9
9
  </div>
@@ -1,6 +1,9 @@
1
+ import type { ButtonSize } from '../button/types';
1
2
  import type { Snippet } from 'svelte';
2
3
  type Props = {
3
4
  disabled?: boolean;
5
+ /** @default 'md' */
6
+ size?: ButtonSize;
4
7
  on?: {
5
8
  click?: (e: MouseEvent) => void;
6
9
  };
@@ -58,6 +58,8 @@ A structured dialog panel with header (title + close button), scrollable body, a
58
58
  | `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding | inherits `--padding-inline` |
59
59
  | `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding | inherits `--padding-block` |
60
60
  | `--sc-kit--dialog--header--padding-inline` | Header-specific horizontal padding | inherits `--padding-inline` |
61
+ | `--sc-kit--dialog--footer--padding-block` | Footer-specific vertical padding | inherits `--padding-block` |
62
+ | `--sc-kit--dialog--footer--padding-inline` | Footer-specific horizontal padding | inherits `--padding-inline` |
61
63
  | `--sc-kit--dialog--title--font-size` | Title font size | `1.125rem` (18px) |
62
64
  | `--sc-kit--dialog--title--line-height` | Title line height | `1.4` |
63
65
  -->
@@ -78,6 +80,8 @@ A structured dialog panel with header (title + close button), scrollable body, a
78
80
  --_dialog--body--padding-inline: var(--sc-kit--dialog--body--padding-inline, var(--_dialog--padding-inline));
79
81
  --_dialog--header--padding-block: var(--sc-kit--dialog--header--padding-block, var(--_dialog--padding-block));
80
82
  --_dialog--header--padding-inline: var(--sc-kit--dialog--header--padding-inline, var(--_dialog--padding-inline));
83
+ --_dialog--footer--padding-block: var(--sc-kit--dialog--footer--padding-block, var(--_dialog--padding-block));
84
+ --_dialog--footer--padding-inline: var(--sc-kit--dialog--footer--padding-inline, var(--_dialog--padding-inline));
81
85
  --_dialog--title-font-size: var(--sc-kit--dialog--title--font-size, 1.125rem);
82
86
  --_dialog--title-line-height: var(--sc-kit--dialog--title--line-height, 1.4);
83
87
  display: flex;
@@ -155,7 +159,7 @@ A structured dialog panel with header (title + close button), scrollable body, a
155
159
  align-items: center;
156
160
  justify-content: flex-end;
157
161
  gap: var(--sc-kit--space--3);
158
- padding: var(--_dialog--padding-block) var(--_dialog--padding-inline);
162
+ padding: var(--_dialog--footer--padding-block) var(--_dialog--footer--padding-inline);
159
163
  border-top: 1px solid var(--_dialog--border-color);
160
164
  -webkit-user-drag: none;
161
165
  user-select: none;
@@ -59,6 +59,8 @@ interface $$IsomorphicComponent {
59
59
  * | `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding | inherits `--padding-inline` |
60
60
  * | `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding | inherits `--padding-block` |
61
61
  * | `--sc-kit--dialog--header--padding-inline` | Header-specific horizontal padding | inherits `--padding-inline` |
62
+ * | `--sc-kit--dialog--footer--padding-block` | Footer-specific vertical padding | inherits `--padding-block` |
63
+ * | `--sc-kit--dialog--footer--padding-inline` | Footer-specific horizontal padding | inherits `--padding-inline` |
62
64
  * | `--sc-kit--dialog--title--font-size` | Title font size | `1.125rem` (18px) |
63
65
  * | `--sc-kit--dialog--title--line-height` | Title line height | `1.4` |
64
66
  */
@@ -1,4 +1,5 @@
1
1
  import { DomHelper } from '../../core/utils';
2
+ import { DEV } from 'esm-env';
2
3
  /**
3
4
  * Dev-only guard for the Popover trigger. The trigger renders inside a `<button>`, so its content
4
5
  * must be non-interactive (use `Button` with `type="presentational"` for a button-looking trigger).
@@ -6,7 +7,7 @@ import { DomHelper } from '../../core/utils';
6
7
  * node and outlines the trigger in red. No-op in production.
7
8
  */
8
9
  export const validateTrigger = (node) => {
9
- if (!import.meta.env?.DEV) {
10
+ if (!DEV) {
10
11
  return;
11
12
  }
12
13
  const check = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.75.0",
3
+ "version": "0.76.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",
@@ -495,6 +495,7 @@
495
495
  "colord": "^2.9.3",
496
496
  "dequal": "^2.0.3",
497
497
  "dompurify": "^3.4.5",
498
+ "esm-env": "^1.2.2",
498
499
  "fuse.js": "^7.3.0",
499
500
  "hugerte": "^1.0.10",
500
501
  "mime": "^4.1.0",
@@ -534,6 +535,7 @@
534
535
  "eslint-plugin-promise": "^7.3.0",
535
536
  "eslint-plugin-svelte": "^3.23.0",
536
537
  "eslint-plugin-unused-imports": "^4.4.1",
538
+ "esm-env": "^1.2.2",
537
539
  "fuse.js": "^7.3.0",
538
540
  "globals": "^17.12.0",
539
541
  "hugerte": "^1.0.10",