@streamscloud/kit 0.89.0 → 0.90.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 (72) hide show
  1. package/README.md +2 -1
  2. package/dist/core/utils/html-helper.d.ts +10 -0
  3. package/dist/core/utils/html-helper.js +37 -0
  4. package/dist/core/utils/string-helper.d.ts +10 -0
  5. package/dist/core/utils/string-helper.js +14 -0
  6. package/dist/core/validation/form-validation-handler/form-validation-handler.svelte.d.ts +4 -2
  7. package/dist/core/validation/form-validation-handler/form-validation-handler.svelte.js +1 -1
  8. package/dist/ui/_internal/form-field-label-context/form-field-label-context.svelte.d.ts +15 -0
  9. package/dist/ui/_internal/form-field-label-context/form-field-label-context.svelte.js +20 -0
  10. package/dist/ui/_internal/form-field-label-context/index.d.ts +1 -0
  11. package/dist/ui/_internal/form-field-label-context/index.js +1 -0
  12. package/dist/ui/checkbox/cmp.checkbox.svelte +11 -2
  13. package/dist/ui/chip-group/cmp.chip-group.svelte +10 -3
  14. package/dist/ui/chip-group/cmp.chip-group.svelte.d.ts +4 -2
  15. package/dist/ui/color-picker/cmp.color-picker.svelte +12 -2
  16. package/dist/ui/color-picker/cmp.color-picker.svelte.d.ts +1 -3
  17. package/dist/ui/date-picker/cmp.date-picker.svelte +6 -4
  18. package/dist/ui/date-picker/cmp.date-picker.svelte.d.ts +2 -9
  19. package/dist/ui/date-picker/cmp.date-time-picker.svelte +19 -3
  20. package/dist/ui/date-picker/cmp.date-time-picker.svelte.d.ts +3 -5
  21. package/dist/ui/date-picker/cmp.time-picker.svelte +3 -1
  22. package/dist/ui/date-picker/cmp.time-picker.svelte.d.ts +1 -4
  23. package/dist/ui/document-tabs/types.d.ts +0 -1
  24. package/dist/ui/form-field/cmp.form-field-validatable.svelte +4 -4
  25. package/dist/ui/form-field/cmp.form-field-validatable.svelte.d.ts +4 -7
  26. package/dist/ui/form-field/cmp.form-field.svelte +31 -79
  27. package/dist/ui/form-field/cmp.form-field.svelte.d.ts +11 -10
  28. package/dist/ui/form-field/form-field-label.svelte.d.ts +6 -0
  29. package/dist/ui/form-field/form-field-label.svelte.js +19 -0
  30. package/dist/ui/form-field/index.d.ts +1 -1
  31. package/dist/ui/icon-picker/cmp.icon-picker.svelte +3 -1
  32. package/dist/ui/icon-picker/cmp.icon-picker.svelte.d.ts +3 -1
  33. package/dist/ui/input/cmp.input.svelte +18 -2
  34. package/dist/ui/input/cmp.input.svelte.d.ts +3 -0
  35. package/dist/ui/input/types.d.ts +0 -3
  36. package/dist/ui/numeral-input/cmp.numeral-input.svelte +6 -2
  37. package/dist/ui/numeral-input/cmp.numeral-input.svelte.d.ts +0 -4
  38. package/dist/ui/option-pill/cmp.option-pill.svelte.d.ts +0 -2
  39. package/dist/ui/page-layout/cmp.page-layout.svelte.d.ts +1 -2
  40. package/dist/ui/pin-input/cmp.pin-input.svelte +11 -3
  41. package/dist/ui/pin-input/cmp.pin-input.svelte.d.ts +1 -1
  42. package/dist/ui/popover/cmp.popover.svelte +2 -1
  43. package/dist/ui/popover/cmp.popover.svelte.d.ts +2 -6
  44. package/dist/ui/radio/cmp.radio.svelte +12 -3
  45. package/dist/ui/rich-text-input/cmp.rich-text-input.svelte +19 -7
  46. package/dist/ui/rich-text-input/cmp.rich-text-input.svelte.d.ts +1 -2
  47. package/dist/ui/segmented-control/cmp.segmented-control.svelte +5 -3
  48. package/dist/ui/segmented-control/cmp.segmented-control.svelte.d.ts +1 -2
  49. package/dist/ui/select/cmp.input-suggest.svelte +5 -1
  50. package/dist/ui/select/cmp.input-suggest.svelte.d.ts +0 -3
  51. package/dist/ui/select/cmp.multiselect-tree.svelte +2 -1
  52. package/dist/ui/select/cmp.multiselect-tree.svelte.d.ts +1 -3
  53. package/dist/ui/select/cmp.singleselect-async.svelte +5 -2
  54. package/dist/ui/select/cmp.singleselect-async.svelte.d.ts +0 -2
  55. package/dist/ui/select/cmp.singleselect.svelte +3 -1
  56. package/dist/ui/select/cmp.singleselect.svelte.d.ts +0 -2
  57. package/dist/ui/select/multiselect-base.svelte +6 -2
  58. package/dist/ui/select/select-core.svelte.js +0 -2
  59. package/dist/ui/select/select-listbox.svelte +1 -5
  60. package/dist/ui/select/types.d.ts +2 -6
  61. package/dist/ui/slider/cmp.slider.svelte +6 -2
  62. package/dist/ui/slider/cmp.slider.svelte.d.ts +1 -0
  63. package/dist/ui/tabs/types.d.ts +0 -1
  64. package/dist/ui/textarea/cmp.textarea.svelte +16 -16
  65. package/dist/ui/textarea/cmp.textarea.svelte.d.ts +4 -2
  66. package/dist/ui/toggle/cmp.toggle.svelte +4 -2
  67. package/dist/ui/validatable/cmp.validatable.svelte +1 -2
  68. package/dist/ui/validatable/cmp.validatable.svelte.d.ts +1 -2
  69. package/dist/ui/video/cmp.video.svelte.d.ts +0 -1
  70. package/dist/ui/visual-select/cmp.visual-select.svelte +11 -3
  71. package/dist/ui/visual-select/cmp.visual-select.svelte.d.ts +2 -1
  72. package/package.json +3 -2
@@ -2,10 +2,14 @@
2
2
  </script>
3
3
 
4
4
  <script lang="ts">import { HtmlHelper } from '../../core/utils';
5
+ import { claimFormFieldLabel } from '../_internal/form-field-label-context';
5
6
  import { InputClearButton } from '../_internal/input-clear-button';
6
7
  import { IconSlot } from '../icon';
7
8
  import { default as SelectListbox } from './select-listbox.svelte';
8
9
  const { value, suggestions, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, clearable = false, trim = true, icon, name, id, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
10
+ const fallbackId = $props.id();
11
+ const controlId = $derived(id ?? fallbackId);
12
+ claimFormFieldLabel(() => controlId);
9
13
  let inputEl = $state.raw(undefined);
10
14
  let rootEl = $state.raw(undefined);
11
15
  let isOpen = $state(false);
@@ -241,7 +245,7 @@ const handlePickRow = (row) => {
241
245
  aria-describedby={ariaDescribedby}
242
246
  aria-required={ariaRequired ? 'true' : undefined}
243
247
  aria-invalid={error ? 'true' : undefined}
244
- id={id}
248
+ id={controlId}
245
249
  name={name}
246
250
  autocomplete={autocomplete}
247
251
  placeholder={placeholder}
@@ -17,15 +17,12 @@ type Props = {
17
17
  size?: 'sm' | 'md' | 'lg';
18
18
  placeholder?: string;
19
19
  disabled?: boolean;
20
- /** Non-editable but selectable; the popover never opens. */
21
20
  readonly?: boolean;
22
- /** Visual display mode — non-focusable, no hover/focus animations. */
23
21
  inert?: boolean;
24
22
  /** Visual error state — also sets `aria-invalid="true"`. */
25
23
  error?: boolean;
26
24
  /** Strip border + background + focus underline. */
27
25
  borderless?: boolean;
28
- /** Show the × clear button when the value is non-empty. */
29
26
  clearable?: boolean;
30
27
  /**
31
28
  * Strip leading / trailing whitespace when the value is committed — focus leaving the control or
@@ -4,7 +4,7 @@
4
4
  <script lang="ts" generics="T">import { default as MultiselectBase } from './multiselect-base.svelte';
5
5
  import { buildFuseIndex, defaultCompare, runFuseSearch } from './select-internals';
6
6
  import { isSelectGroup } from './types';
7
- const { options, value, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = false, groupHeader = 'toggle-all', selectionMode = 'children-only', selectedDisplay = 'checkbox', createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
7
+ const { options, value, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = false, groupHeader = 'toggle-all', selectionMode = 'children-only', selectedDisplay = 'checkbox', createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
8
8
  const fuse = $derived(buildFuseIndex(options));
9
9
  const loadOptionsShim = (q) => Promise.resolve(runFuseSearch(fuse, q, options));
10
10
  // Include flat roots, not only groups — else a childless root can't be picked as a create-parent.
@@ -70,6 +70,7 @@ $effect(() => {
70
70
  name={name}
71
71
  autocomplete={autocomplete}
72
72
  aria-label={ariaLabel}
73
+ aria-labelledby={ariaLabelledby}
73
74
  aria-describedby={ariaDescribedby}
74
75
  aria-required={ariaRequired}
75
76
  on={{
@@ -21,11 +21,8 @@ declare function $$render<T>(): {
21
21
  size?: "sm" | "md" | "lg";
22
22
  /** Placeholder shown only when nothing is selected and the user is not typing. @default '' */
23
23
  placeholder?: string;
24
- /** Fully disabled — no focus, no interaction, dimmed styling. @default false */
25
24
  disabled?: boolean;
26
- /** Read-only — value visible, but selection cannot change. @default false */
27
25
  readonly?: boolean;
28
- /** Inert subtree — non-interactive but unstyled. @default false */
29
26
  inert?: boolean;
30
27
  /** Visual error state (accent border + aria-invalid). @default false */
31
28
  error?: boolean;
@@ -81,6 +78,7 @@ declare function $$render<T>(): {
81
78
  name?: string;
82
79
  autocomplete?: HTMLInputAttributes["autocomplete"];
83
80
  'aria-label'?: string;
81
+ 'aria-labelledby'?: string;
84
82
  'aria-describedby'?: string;
85
83
  'aria-required'?: boolean;
86
84
  on?: {
@@ -1,7 +1,8 @@
1
1
  <script lang="ts" module>export {};
2
2
  </script>
3
3
 
4
- <script lang="ts" generics="T">import { InputClearButton } from '../_internal/input-clear-button';
4
+ <script lang="ts" generics="T">import { getFormFieldLabel, resolveAriaLabelledby } from '../_internal/form-field-label-context';
5
+ import { InputClearButton } from '../_internal/input-clear-button';
5
6
  import { Spinner } from '../_internal/spinner';
6
7
  import { Icon, IconSlot } from '../icon';
7
8
  import { createSelectCore } from './select-core.svelte';
@@ -9,7 +10,8 @@ import { defaultCompare } from './select-internals';
9
10
  import { default as SelectListbox } from './select-listbox.svelte';
10
11
  import { SingleselectLocalization } from './singleselect-localization';
11
12
  import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_16_regular.svg?raw';
12
- const { loadOptions, debounceMs = 400, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', compare = defaultCompare, createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
13
+ const { loadOptions, debounceMs = 400, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', compare = defaultCompare, createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
14
+ const formFieldLabel = getFormFieldLabel();
13
15
  const localization = new SingleselectLocalization();
14
16
  const selectedOption = $derived(mode.value ?? null);
15
17
  const selectionLabel = $derived(selectedOption?.label ?? '');
@@ -101,6 +103,7 @@ const showClear = $derived(mode.clearable && hasValue && isInteractive && !core.
101
103
  aria-activedescendant={core.isOpen ? `${listboxId}-row-${core.highlight}` : undefined}
102
104
  aria-autocomplete="list"
103
105
  aria-label={ariaLabel ?? name}
106
+ aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}
104
107
  aria-describedby={ariaDescribedby}
105
108
  aria-required={ariaRequired}
106
109
  aria-invalid={error ? 'true' : undefined}
@@ -15,7 +15,6 @@ declare function $$render<T>(): {
15
15
  /** Internal — set by the sync `Singleselect` wrapper. Async typeahead is always searchable. @default true */
16
16
  searchable?: boolean;
17
17
  } & {
18
- /** Discriminator — `true` enables the X clear button and allows `value` to be `null`. */
19
18
  clearable: true;
20
19
  /** Currently selected option (pre-resolved — labels render before the first fetch lands). `null` when nothing is picked. */
21
20
  value: SelectOption<T> | null | undefined;
@@ -33,7 +32,6 @@ declare function $$render<T>(): {
33
32
  /** Internal — set by the sync `Singleselect` wrapper. Async typeahead is always searchable. @default true */
34
33
  searchable?: boolean;
35
34
  } & {
36
- /** Discriminator — `false` removes the X clear button; `value` must always be a non-null `SelectOption<T>`. */
37
35
  clearable: false;
38
36
  /** Currently selected option — non-null. */
39
37
  value: SelectOption<T>;
@@ -4,7 +4,7 @@
4
4
  <script lang="ts" generics="T">import { default as SingleselectAsync } from './cmp.singleselect-async.svelte';
5
5
  import { buildFuseIndex, defaultCompare, runFuseSearch } from './select-internals';
6
6
  import { isSelectGroup } from './types';
7
- const { options, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
7
+ const { options, compare = defaultCompare, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', createOptions, icon, chevronIcon, optionSnippet, selectionSnippet, listbox, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, ...mode } = $props();
8
8
  const fuse = $derived(buildFuseIndex(options));
9
9
  const loadOptionsShim = (q) => Promise.resolve(runFuseSearch(fuse, q, options));
10
10
  const resolvedValue = $derived.by(() => {
@@ -64,6 +64,7 @@ $effect(() => {
64
64
  name={name}
65
65
  autocomplete={autocomplete}
66
66
  aria-label={ariaLabel}
67
+ aria-labelledby={ariaLabelledby}
67
68
  aria-describedby={ariaDescribedby}
68
69
  aria-required={ariaRequired}
69
70
  on={{
@@ -97,6 +98,7 @@ $effect(() => {
97
98
  name={name}
98
99
  autocomplete={autocomplete}
99
100
  aria-label={ariaLabel}
101
+ aria-labelledby={ariaLabelledby}
100
102
  aria-describedby={ariaDescribedby}
101
103
  aria-required={ariaRequired}
102
104
  on={{
@@ -11,7 +11,6 @@ declare function $$render<T>(): {
11
11
  /** When true, typing filters the options; when false, it is a click/keyboard-only picker. @default true */
12
12
  searchable?: boolean;
13
13
  } & {
14
- /** Discriminator — `true` enables the X clear button and allows `value` to be `null`. */
15
14
  clearable: true;
16
15
  /** Current selection by value. `null` when nothing is picked. */
17
16
  value: T | null | undefined;
@@ -27,7 +26,6 @@ declare function $$render<T>(): {
27
26
  /** When true, typing filters the options; when false, it is a click/keyboard-only picker. @default true */
28
27
  searchable?: boolean;
29
28
  } & {
30
- /** Discriminator — `false` hides the X button; `value` must always be a non-null `T`. */
31
29
  clearable: false;
32
30
  /** Current selection by value — non-null. */
33
31
  value: T;
@@ -1,7 +1,8 @@
1
1
  <script lang="ts" module>export {};
2
2
  </script>
3
3
 
4
- <script lang="ts" generics="T">import { Spinner } from '../_internal/spinner';
4
+ <script lang="ts" generics="T">import { getFormFieldLabel, resolveAriaLabelledby, stopFormFieldLabelHere } from '../_internal/form-field-label-context';
5
+ import { Spinner } from '../_internal/spinner';
5
6
  import { Button } from '../button';
6
7
  import { Icon, IconSlot } from '../icon';
7
8
  import { default as Singleselect } from './cmp.singleselect.svelte';
@@ -13,7 +14,9 @@ import { default as SelectListbox } from './select-listbox.svelte';
13
14
  import IconCheckmark from '@fluentui/svg-icons/icons/checkmark_16_regular.svg?raw';
14
15
  import IconChevronDown from '@fluentui/svg-icons/icons/chevron_down_16_regular.svg?raw';
15
16
  import { dndzone } from 'svelte-dnd-action';
16
- const { loadOptions, debounceMs = 400, value, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', compare = defaultCompare, selectionMode = 'children-only', selectedDisplay = 'checkmark', createOptions, chipMode = 'inline', reorderable = false, maxVisible = Infinity, showFullItem = false, chipVariant, showChevron = true, hideSelected = false, icon, chevronIcon, optionSnippet, chipSnippet, selectionSnippet, parentSource, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
17
+ const { loadOptions, debounceMs = 400, value, size = 'md', placeholder = '', disabled = false, readonly = false, inert = false, error = false, borderless = false, searchable = true, groupHeader = 'static', compare = defaultCompare, selectionMode = 'children-only', selectedDisplay = 'checkmark', createOptions, chipMode = 'inline', reorderable = false, maxVisible = Infinity, showFullItem = false, chipVariant, showChevron = true, hideSelected = false, icon, chevronIcon, optionSnippet, chipSnippet, selectionSnippet, parentSource, id, name, autocomplete = 'off', 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, 'aria-describedby': ariaDescribedby, 'aria-required': ariaRequired, on } = $props();
18
+ const formFieldLabel = getFormFieldLabel();
19
+ stopFormFieldLabelHere();
17
20
  const localization = new MultiselectLocalization();
18
21
  const isPicked = (v) => value.some((existing) => compare(existing.value, v));
19
22
  const hasValue = $derived(value.length > 0);
@@ -225,6 +228,7 @@ const handleDndFinalize = (e) => {
225
228
  aria-activedescendant={core.isOpen ? `${listboxId}-row-${core.highlight}` : undefined}
226
229
  aria-autocomplete="list"
227
230
  aria-label={ariaLabel ?? name}
231
+ aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}
228
232
  aria-describedby={ariaDescribedby}
229
233
  aria-required={ariaRequired}
230
234
  aria-invalid={error ? 'true' : undefined}
@@ -286,8 +286,6 @@ export function createSelectCore(config) {
286
286
  };
287
287
  const handleFocus = () => undefined;
288
288
  const handleClick = (e) => {
289
- // Cancels FormField's <label> activation — else its synthetic click re-toggles the popover on every pick.
290
- e.preventDefault();
291
289
  if (!config.getInteractive() || isFiltering) {
292
290
  return;
293
291
  }
@@ -119,11 +119,7 @@ $effect(() => {
119
119
  role="listbox"
120
120
  id={listboxId}
121
121
  tabindex="-1"
122
- onclick={(e) => {
123
- // preventDefault cancels an ancestor <label>'s activation — stopPropagation alone doesn't. FormField guards its own; a consumer's plain <label> does not.
124
- e.preventDefault();
125
- e.stopPropagation();
126
- }}
122
+ onclick={(e) => e.stopPropagation()}
127
123
  onmousedown={(e) => {
128
124
  // Keeps the trigger root's mousedown preventDefault (select-core) off panel presses: without it a nested input in the panel never takes the caret and text selection dies.
129
125
  e.stopPropagation();
@@ -70,11 +70,8 @@ export type SingleselectCommonProps<T> = {
70
70
  size?: 'sm' | 'md' | 'lg';
71
71
  /** Placeholder shown only when nothing is selected and the user is not typing. @default '' */
72
72
  placeholder?: string;
73
- /** Fully disabled — no focus, no interaction, dimmed styling. @default false */
74
73
  disabled?: boolean;
75
- /** Read-only — focusable, value visible, but selection cannot change. @default false */
76
74
  readonly?: boolean;
77
- /** Inert subtree — non-interactive but unstyled (use for "view-only" parents). @default false */
78
75
  inert?: boolean;
79
76
  /** Visual error state (accent border + aria-invalid). @default false */
80
77
  error?: boolean;
@@ -119,6 +116,7 @@ export type SingleselectCommonProps<T> = {
119
116
  name?: string;
120
117
  autocomplete?: HTMLInputAttributes['autocomplete'];
121
118
  'aria-label'?: string;
119
+ 'aria-labelledby'?: string;
122
120
  'aria-describedby'?: string;
123
121
  'aria-required'?: boolean;
124
122
  };
@@ -134,11 +132,8 @@ export type MultiselectBaseProps<T> = {
134
132
  size?: 'sm' | 'md' | 'lg';
135
133
  /** Placeholder shown only when nothing is selected and the user is not typing. @default '' */
136
134
  placeholder?: string;
137
- /** Fully disabled — no focus, no interaction, dimmed styling. @default false */
138
135
  disabled?: boolean;
139
- /** Read-only — value visible, but selection cannot change. @default false */
140
136
  readonly?: boolean;
141
- /** Inert subtree — non-interactive but unstyled. @default false */
142
137
  inert?: boolean;
143
138
  /** Visual error state (accent border + aria-invalid). @default false */
144
139
  error?: boolean;
@@ -216,6 +211,7 @@ export type MultiselectBaseProps<T> = {
216
211
  name?: string;
217
212
  autocomplete?: HTMLInputAttributes['autocomplete'];
218
213
  'aria-label'?: string;
214
+ 'aria-labelledby'?: string;
219
215
  'aria-describedby'?: string;
220
216
  'aria-required'?: boolean;
221
217
  on?: {
@@ -1,5 +1,8 @@
1
- <script lang="ts">"use strict";
2
- let { value, min = 0, max = 100, step = 1, disabled = false, showValue = false, withInput = false, ticks = [], valueSuffix = '', on } = $props();
1
+ <script lang="ts">import { claimFormFieldLabel } from '../_internal/form-field-label-context';
2
+ let { value, min = 0, max = 100, step = 1, disabled = false, showValue = false, withInput = false, ticks = [], valueSuffix = '', id, on } = $props();
3
+ const fallbackId = $props.id();
4
+ const controlId = $derived(id ?? fallbackId);
5
+ claimFormFieldLabel(() => controlId);
3
6
  let focused = $state(false);
4
7
  const fillPct = $derived(max === min ? 0 : ((value - min) / (max - min)) * 100);
5
8
  const handleInput = (e) => {
@@ -25,6 +28,7 @@ const handleNumeric = (e) => {
25
28
  <input
26
29
  type="range"
27
30
  class="slider__input"
31
+ id={controlId}
28
32
  min={min}
29
33
  max={max}
30
34
  step={step}
@@ -15,6 +15,7 @@ type Props = {
15
15
  ticks?: string[];
16
16
  /** Suffix appended to the displayed value (e.g. ' %'). @default '' */
17
17
  valueSuffix?: string;
18
+ id?: string;
18
19
  on?: {
19
20
  change?: (value: number) => void;
20
21
  input?: (value: number) => void;
@@ -3,6 +3,5 @@ export type TabsSize = 'sm' | 'md';
3
3
  export type Tab<TValue = unknown> = {
4
4
  value: TValue;
5
5
  label: string;
6
- /** Non-interactive — click does nothing. */
7
6
  disabled?: boolean;
8
7
  };
@@ -1,9 +1,13 @@
1
- <script lang="ts">import { HtmlHelper } from '../../core/utils';
1
+ <script lang="ts">import { HtmlHelper, StringHelper } from '../../core/utils';
2
+ import { claimFormFieldLabel } from '../_internal/form-field-label-context';
2
3
  import { InputEmojiPicker } from '../_internal/input-emoji-picker';
3
4
  const { value, size = 'md', disabled = false, readonly = false, inert = false, error = false, borderless = false, emoji = false, singleLine = false, submitOnEnter = false, trim = true, placeholder = '', name = '', id = null, autofocus = false, maxLength = null, rows = 3, autoresize = true, maxRows = 12, autocomplete, spellcheck, 'aria-describedby': ariaDescribedby, 'aria-label': ariaLabel, 'aria-required': ariaRequired, on } = $props();
5
+ const fallbackId = $props.id();
6
+ const controlId = $derived(id ?? fallbackId);
7
+ claimFormFieldLabel(() => controlId);
4
8
  let textareaEl = $state.raw(undefined);
5
- const sanitizeLineBreaks = (text) => text.replace(/[\r\n]+/g, ' ');
6
- const valueParsed = $derived(singleLine && value ? sanitizeLineBreaks(value) : (value ?? ''));
9
+ const cleanText = $derived(singleLine ? StringHelper.toSingleLineText : StringHelper.toMultilineText);
10
+ const valueParsed = $derived(singleLine && value ? cleanText(value) : (value ?? ''));
7
11
  const effectiveRows = $derived(singleLine ? 1 : rows);
8
12
  const trimOnCommit = $derived(trim && !disabled && !readonly && !inert);
9
13
  /** Imperative API exposed via `bind:this` on `<Textarea>`. */
@@ -38,6 +42,7 @@ const handleInput = () => {
38
42
  if (!textareaEl) {
39
43
  return;
40
44
  }
45
+ HtmlHelper.cleanElementValue(textareaEl, cleanText);
41
46
  resize();
42
47
  on?.input?.(textareaEl.value);
43
48
  };
@@ -62,21 +67,11 @@ const handleChange = () => {
62
67
  on?.change?.(textareaEl.value);
63
68
  };
64
69
  const handleBeforeInput = (e) => {
65
- if (!singleLine || !textareaEl) {
66
- return;
67
- }
68
- if (e.inputType === 'insertLineBreak' || e.inputType === 'insertParagraph') {
69
- e.preventDefault();
70
- return;
71
- }
72
- if (e.data && /[\r\n]/.test(e.data)) {
70
+ if (singleLine && (e.inputType === 'insertLineBreak' || e.inputType === 'insertParagraph')) {
73
71
  e.preventDefault();
74
- const start = textareaEl.selectionStart;
75
- const end = textareaEl.selectionEnd;
76
- textareaEl.setRangeText(sanitizeLineBreaks(e.data), start, end, 'end');
77
- textareaEl.dispatchEvent(new Event('input', { bubbles: true }));
78
72
  }
79
73
  };
74
+ const handlePaste = (e) => HtmlHelper.pasteCleanedText(e, cleanText);
80
75
  const handleKeydown = (e) => {
81
76
  if (submitOnEnter && e.key === 'Enter' && !e.isComposing && !(e.altKey || e.ctrlKey || e.metaKey || e.shiftKey)) {
82
77
  e.preventDefault();
@@ -116,7 +111,7 @@ const focusTextarea = () => textareaEl?.focus();
116
111
  use:initTextarea
117
112
  class="textarea__el"
118
113
  name={name}
119
- id={id}
114
+ id={controlId}
120
115
  placeholder={placeholder}
121
116
  disabled={disabled}
122
117
  readonly={readonly}
@@ -134,6 +129,7 @@ const focusTextarea = () => textareaEl?.focus();
134
129
  oninput={handleInput}
135
130
  onchange={handleChange}
136
131
  onbeforeinput={handleBeforeInput}
132
+ onpaste={handlePaste}
137
133
  onkeydown={handleKeydown}
138
134
  onblur={handleBlur}
139
135
  onfocus={() => on?.focus?.()}></textarea>
@@ -157,6 +153,10 @@ point — `change`, blur, and before `on.submit` — so the value the consumer r
157
153
  what the field shows. Blank lines inside the text are left alone. Typing is untouched. Pass
158
154
  `trim={false}` to opt out.
159
155
 
156
+ Entered text is cleaned as it arrives — typed, pasted or dropped: every line break (including Word's
157
+ soft break) becomes `\n`, other control characters except tab are dropped. In `singleLine` mode line breaks and tabs
158
+ become spaces instead.
159
+
160
160
  ### CSS Custom Properties
161
161
  | Property | Description | Default |
162
162
  |---|---|---|
@@ -4,9 +4,7 @@ type Props = {
4
4
  /** @default 'md' */
5
5
  size?: 'sm' | 'md' | 'lg';
6
6
  disabled?: boolean;
7
- /** Non-editable but selectable. */
8
7
  readonly?: boolean;
9
- /** Visual display mode — non-focusable, no hover/focus animations. */
10
8
  inert?: boolean;
11
9
  /** Visual error state — also sets `aria-invalid="true"`. */
12
10
  error?: boolean;
@@ -67,6 +65,10 @@ type Props = {
67
65
  * what the field shows. Blank lines inside the text are left alone. Typing is untouched. Pass
68
66
  * `trim={false}` to opt out.
69
67
  *
68
+ * Entered text is cleaned as it arrives — typed, pasted or dropped: every line break (including Word's
69
+ * soft break) becomes `\n`, other control characters except tab are dropped. In `singleLine` mode line breaks and tabs
70
+ * become spaces instead.
71
+ *
70
72
  * ### CSS Custom Properties
71
73
  * | Property | Description | Default |
72
74
  * |---|---|---|
@@ -1,6 +1,8 @@
1
- <script lang="ts">const { checked, size = 'md', disabled = false, presentational = false, layout = 'spread', label, sublabel, title, name, on } = $props();
1
+ <script lang="ts">import { getFormFieldLabel } from '../_internal/form-field-label-context';
2
+ const { checked, size = 'md', disabled = false, presentational = false, layout = 'spread', label, sublabel, title, name, on } = $props();
2
3
  const hasLabel = $derived(label !== undefined || sublabel !== undefined);
3
4
  const ariaLabel = $derived(typeof label === 'string' ? label : name);
5
+ const formFieldLabel = getFormFieldLabel();
4
6
  const handleClick = (event) => {
5
7
  event.stopPropagation();
6
8
  if (disabled) {
@@ -8,7 +10,6 @@ const handleClick = (event) => {
8
10
  }
9
11
  on?.change?.(!checked);
10
12
  };
11
- export {};
12
13
  </script>
13
14
 
14
15
  {#snippet switchControl()}
@@ -25,6 +26,7 @@ export {};
25
26
  role="switch"
26
27
  aria-checked={checked}
27
28
  aria-label={ariaLabel}
29
+ aria-labelledby={hasLabel || ariaLabel ? undefined : formFieldLabel?.labelId}
28
30
  title={hasLabel ? undefined : title}
29
31
  disabled={disabled}
30
32
  onclick={handleClick}>
@@ -73,6 +73,5 @@ const field = $derived({
73
73
  <!--
74
74
  @component
75
75
  Binds any kit input to a `FormValidationHandler` through a render-prop snippet that receives the wired field, and renders the error message below
76
- it per `errorPlacement`. Inputs stay unaware of validation; around a `FormField`, keep `Validatable` on the outside so the error stays out of the
77
- field's name.
76
+ it per `errorPlacement`. Inputs stay unaware of validation; with a label, `FormFieldValidatable` composes it with `FormField`.
78
77
  -->
@@ -77,8 +77,7 @@ interface $$IsomorphicComponent {
77
77
  }
78
78
  /**
79
79
  * Binds any kit input to a `FormValidationHandler` through a render-prop snippet that receives the wired field, and renders the error message below
80
- * it per `errorPlacement`. Inputs stay unaware of validation; around a `FormField`, keep `Validatable` on the outside so the error stays out of the
81
- * field's name.
80
+ * it per `errorPlacement`. Inputs stay unaware of validation; with a label, `FormFieldValidatable` composes it with `FormField`.
82
81
  */
83
82
  declare const Cmp: $$IsomorphicComponent;
84
83
  type Cmp<T extends Record<string, unknown>, K extends keyof T & string> = InstanceType<typeof Cmp<T, K>>;
@@ -11,7 +11,6 @@ type Props = {
11
11
  loop?: boolean;
12
12
  /** Scroll ancestor for the IntersectionObserver */
13
13
  intersectionContainer?: HTMLElement;
14
- /** Make the video non-interactive */
15
14
  inert?: boolean;
16
15
  /** Begin loading the video source before it scrolls into view */
17
16
  allowPreloading?: boolean;
@@ -1,5 +1,7 @@
1
- <script lang="ts" generics="T">import { Icon } from '../icon';
2
- const { options, cols = 3, disabled = false, compare, ...mode } = $props();
1
+ <script lang="ts" generics="T">import { getFormFieldLabel, resolveAriaLabelledby } from '../_internal/form-field-label-context';
2
+ import { Icon } from '../icon';
3
+ const { options, cols = 3, disabled = false, compare, 'aria-label': ariaLabel, 'aria-labelledby': ariaLabelledby, ...mode } = $props();
4
+ const formFieldLabel = getFormFieldLabel();
3
5
  const eq = (a, b) => (compare ? compare(a, b) : a === b);
4
6
  const isSelected = (o) => {
5
7
  if (mode.multi) {
@@ -23,7 +25,13 @@ const toggle = (o) => {
23
25
  };
24
26
  </script>
25
27
 
26
- <div class="visual-select" class:visual-select--disabled={disabled} style:--_cols={cols} role={mode.multi ? 'group' : 'radiogroup'}>
28
+ <div
29
+ class="visual-select"
30
+ class:visual-select--disabled={disabled}
31
+ style:--_cols={cols}
32
+ role={mode.multi ? 'group' : 'radiogroup'}
33
+ aria-label={ariaLabel}
34
+ aria-labelledby={resolveAriaLabelledby(formFieldLabel, ariaLabel, ariaLabelledby)}>
27
35
  {#each options as o (o.value)}
28
36
  {@const selected = isSelected(o)}
29
37
  <button
@@ -14,10 +14,11 @@ declare function $$render<T>(): {
14
14
  }[];
15
15
  /** Tiles per row in the auto-grid. @default 3 */
16
16
  cols?: number;
17
- /** Global disabled — every tile is non-interactive. */
18
17
  disabled?: boolean;
19
18
  /** Optional equality fn — defaults to `===`. Useful when `T` is a non-primitive. */
20
19
  compare?: (a: T, b: T) => boolean;
20
+ 'aria-label'?: string;
21
+ 'aria-labelledby'?: string;
21
22
  } & ({
22
23
  multi: false;
23
24
  value: T | null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.89.0",
3
+ "version": "0.90.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",
@@ -17,7 +17,8 @@
17
17
  "pack": "npm run build && npm pack",
18
18
  "preview": "vite preview",
19
19
  "check": "svelte-check --tsconfig ./tsconfig.app.json --fail-on-warnings --output machine && tsc -p tsconfig.node.json && eslint . --format codeframe --max-warnings 0 && prettier --check --plugin prettier-plugin-svelte --cache .",
20
- "lint:fix": "eslint --fix . && prettier --write --plugin prettier-plugin-svelte ."
20
+ "check:fix": "npm run lint:fix && npm run check",
21
+ "lint:fix": "eslint --fix . && prettier --write --plugin prettier-plugin-svelte --cache ."
21
22
  },
22
23
  "files": [
23
24
  "dist"