@streamscloud/kit 0.78.1 → 0.79.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.
@@ -51,15 +51,15 @@ A structured dialog panel with header (title + close button), scrollable body, a
51
51
  | `--sc-kit--dialog--height` | Dialog height | `auto` |
52
52
  | `--sc-kit--dialog--max-height` | Maximum height | `none` |
53
53
  | `--sc-kit--dialog--min-height` | Minimum height | `auto` |
54
- | `--sc-kit--dialog--padding-block` | Vertical padding for header/body/footer | `--sc-kit--space--5` |
55
- | `--sc-kit--dialog--padding-inline` | Horizontal padding for header/body/footer | `--sc-kit--space--6` |
54
+ | `--sc-kit--dialog--padding-block` | Vertical padding for header/body/footer | `--sc-kit--space--5`; `14px` when the dialog is under 768px wide inside `DialogHost` |
55
+ | `--sc-kit--dialog--padding-inline` | Horizontal padding for header/body/footer | `--sc-kit--space--6`; `--sc-kit--space--4` when the dialog is under 768px wide inside `DialogHost` |
56
56
  | `--sc-kit--dialog--body--overflow-y` | Body vertical overflow | `visible` |
57
- | `--sc-kit--dialog--body--padding-block` | Body-specific vertical padding | inherits `--padding-block` |
58
- | `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding | inherits `--padding-inline` |
59
- | `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding | inherits `--padding-block` |
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` |
57
+ | `--sc-kit--dialog--body--padding-block` | Body-specific vertical padding | `--sc-kit--dialog--padding-block` |
58
+ | `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
59
+ | `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding | `--sc-kit--dialog--padding-block` |
60
+ | `--sc-kit--dialog--header--padding-inline` | Header-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
61
+ | `--sc-kit--dialog--footer--padding-block` | Footer-specific vertical padding | `--sc-kit--dialog--padding-block` |
62
+ | `--sc-kit--dialog--footer--padding-inline` | Footer-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
63
63
  | `--sc-kit--dialog--title--font-size` | Title font size | `1.125rem` (18px) |
64
64
  | `--sc-kit--dialog--title--line-height` | Title line height | `1.4` |
65
65
  -->
@@ -73,13 +73,15 @@ A structured dialog panel with header (title + close button), scrollable body, a
73
73
  --_dialog--height: var(--sc-kit--dialog--height, auto);
74
74
  --_dialog--max-height: var(--sc-kit--dialog--max-height, none);
75
75
  --_dialog--min-height: var(--sc-kit--dialog--min-height, auto);
76
- --_dialog--padding-block: var(--sc-kit--dialog--padding-block, var(--sc-kit--space--5));
77
- --_dialog--padding-inline: var(--sc-kit--dialog--padding-inline, var(--sc-kit--space--6));
76
+ --_dialog--width-padding-block: var(--sc-kit--space--5);
77
+ --_dialog--width-padding-inline: var(--sc-kit--space--6);
78
+ --_dialog--padding-block: var(--sc-kit--dialog--padding-block, var(--_dialog--width-padding-block));
79
+ --_dialog--padding-inline: var(--sc-kit--dialog--padding-inline, var(--_dialog--width-padding-inline));
78
80
  --_dialog--body--overflow-y: var(--sc-kit--dialog--body--overflow-y, visible);
79
- --_dialog--body--padding-block: var(--sc-kit--dialog--body--padding-block, var(--_dialog--padding-block));
80
- --_dialog--body--padding-inline: var(--sc-kit--dialog--body--padding-inline, var(--_dialog--padding-inline));
81
- --_dialog--header--padding-block: var(--sc-kit--dialog--header--padding-block, var(--_dialog--padding-block));
82
- --_dialog--header--padding-inline: var(--sc-kit--dialog--header--padding-inline, var(--_dialog--padding-inline));
81
+ --_--dialog--body--padding-block: var(--sc-kit--dialog--body--padding-block, var(--_dialog--padding-block));
82
+ --_--dialog--body--padding-inline: var(--sc-kit--dialog--body--padding-inline, var(--_dialog--padding-inline));
83
+ --_--dialog--header--padding-block: var(--sc-kit--dialog--header--padding-block, var(--_dialog--padding-block));
84
+ --_--dialog--header--padding-inline: var(--sc-kit--dialog--header--padding-inline, var(--_dialog--padding-inline));
83
85
  --_dialog--footer--padding-block: var(--sc-kit--dialog--footer--padding-block, var(--_dialog--padding-block));
84
86
  --_dialog--footer--padding-inline: var(--sc-kit--dialog--footer--padding-inline, var(--_dialog--padding-inline));
85
87
  --_dialog--title-font-size: var(--sc-kit--dialog--title--font-size, 1.125rem);
@@ -98,7 +100,8 @@ A structured dialog panel with header (title + close button), scrollable body, a
98
100
  }
99
101
  @container (width < 768px) {
100
102
  .dialog {
101
- --_dialog--padding-inline: var(--sc-kit--dialog--padding-inline, var(--sc-kit--space--4));
103
+ --_dialog--width-padding-block: 0.875rem;
104
+ --_dialog--width-padding-inline: var(--sc-kit--space--4);
102
105
  }
103
106
  }
104
107
  .dialog__header {
@@ -107,7 +110,7 @@ A structured dialog panel with header (title + close button), scrollable body, a
107
110
  align-items: center;
108
111
  justify-content: space-between;
109
112
  gap: var(--sc-kit--space--4);
110
- padding: var(--_dialog--header--padding-block) var(--_dialog--header--padding-inline);
113
+ padding: var(--_--dialog--header--padding-block) var(--_--dialog--header--padding-inline);
111
114
  border-bottom: 1px solid var(--_dialog--border-color);
112
115
  -webkit-user-drag: none;
113
116
  user-select: none;
@@ -130,7 +133,7 @@ A structured dialog panel with header (title + close button), scrollable body, a
130
133
  flex: 1;
131
134
  min-height: 0;
132
135
  overflow-y: var(--_dialog--body--overflow-y);
133
- padding: var(--_dialog--body--padding-block) var(--_dialog--body--padding-inline);
136
+ padding: var(--_--dialog--body--padding-block) var(--_--dialog--body--padding-inline);
134
137
  --_cross-browser-scrollbar--thumb-color: var(--scrollbar--thumb-color, light-dark(#d1d5db, #4b5563));
135
138
  --_cross-browser-scrollbar--track-color: var(--scrollbar--track-color, transparent);
136
139
  }
@@ -52,15 +52,15 @@ interface $$IsomorphicComponent {
52
52
  * | `--sc-kit--dialog--height` | Dialog height | `auto` |
53
53
  * | `--sc-kit--dialog--max-height` | Maximum height | `none` |
54
54
  * | `--sc-kit--dialog--min-height` | Minimum height | `auto` |
55
- * | `--sc-kit--dialog--padding-block` | Vertical padding for header/body/footer | `--sc-kit--space--5` |
56
- * | `--sc-kit--dialog--padding-inline` | Horizontal padding for header/body/footer | `--sc-kit--space--6` |
55
+ * | `--sc-kit--dialog--padding-block` | Vertical padding for header/body/footer | `--sc-kit--space--5`; `14px` when the dialog is under 768px wide inside `DialogHost` |
56
+ * | `--sc-kit--dialog--padding-inline` | Horizontal padding for header/body/footer | `--sc-kit--space--6`; `--sc-kit--space--4` when the dialog is under 768px wide inside `DialogHost` |
57
57
  * | `--sc-kit--dialog--body--overflow-y` | Body vertical overflow | `visible` |
58
- * | `--sc-kit--dialog--body--padding-block` | Body-specific vertical padding | inherits `--padding-block` |
59
- * | `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding | inherits `--padding-inline` |
60
- * | `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding | inherits `--padding-block` |
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` |
58
+ * | `--sc-kit--dialog--body--padding-block` | Body-specific vertical padding | `--sc-kit--dialog--padding-block` |
59
+ * | `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
60
+ * | `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding | `--sc-kit--dialog--padding-block` |
61
+ * | `--sc-kit--dialog--header--padding-inline` | Header-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
62
+ * | `--sc-kit--dialog--footer--padding-block` | Footer-specific vertical padding | `--sc-kit--dialog--padding-block` |
63
+ * | `--sc-kit--dialog--footer--padding-inline` | Footer-specific horizontal padding | `--sc-kit--dialog--padding-inline` |
64
64
  * | `--sc-kit--dialog--title--font-size` | Title font size | `1.125rem` (18px) |
65
65
  * | `--sc-kit--dialog--title--line-height` | Title line height | `1.4` |
66
66
  */
@@ -103,6 +103,7 @@ subtitle / close), vertical Stepper sidebar, scrollable body, and a Cancel / Bac
103
103
  footer. Render it inside your own dialog view opened via `Dialogs.open()` — the layout owns step
104
104
  navigation and the submitting state; your `on.finish` owns closing (`controller.ok(...)`).
105
105
  Completed sidebar steps are clickable to go back; `canAdvance(step)` gates Next / Finish.
106
+ Header, body and sidebar block padding follow `Dialog`'s header / body padding vars.
106
107
 
107
108
  ### CSS Custom Properties
108
109
  | Property | Description | Default |
@@ -116,14 +117,13 @@ Completed sidebar steps are clickable to go back; `canAdvance(step)` gates Next
116
117
  --_sd--sidebar-width: var(--sc-kit--stepper-dialog--sidebar--width, 20rem);
117
118
  --_sd--sidebar-background: var(--sc-kit--stepper-dialog--sidebar--background, var(--sc-kit--color--bg--field-alt));
118
119
  --sc-kit--dialog--height: var(--sc-kit--stepper-dialog--height, auto);
119
- --sc-kit--dialog--body--overflow-y: auto;
120
120
  display: contents;
121
121
  }
122
122
  .stepper-dialog__header {
123
123
  display: flex;
124
124
  align-items: flex-start;
125
125
  gap: var(--sc-kit--space--3);
126
- padding: var(--sc-kit--space--5) var(--sc-kit--space--6);
126
+ padding: var(--_--dialog--header--padding-block) var(--_--dialog--header--padding-inline);
127
127
  border-block-end: 1px solid var(--sc-kit--color--border);
128
128
  }
129
129
  .stepper-dialog__icon {
@@ -169,7 +169,7 @@ Completed sidebar steps are clickable to go back; `canAdvance(step)` gates Next
169
169
  }
170
170
  .stepper-dialog__sidebar {
171
171
  flex: 0 0 var(--_sd--sidebar-width);
172
- padding: var(--sc-kit--space--5) var(--sc-kit--space--4);
172
+ padding: var(--_--dialog--body--padding-block) var(--sc-kit--space--4);
173
173
  background: var(--_sd--sidebar-background);
174
174
  border-inline-end: 1px solid var(--sc-kit--color--border);
175
175
  overflow-y: auto;
@@ -193,5 +193,5 @@ Completed sidebar steps are clickable to go back; `canAdvance(step)` gates Next
193
193
  flex: 1;
194
194
  min-block-size: 0;
195
195
  overflow-y: auto;
196
- padding: var(--sc-kit--space--5) var(--sc-kit--space--6);
196
+ padding: var(--_--dialog--body--padding-block) var(--_--dialog--body--padding-inline);
197
197
  }</style>
@@ -53,6 +53,7 @@ interface $$IsomorphicComponent {
53
53
  * footer. Render it inside your own dialog view opened via `Dialogs.open()` — the layout owns step
54
54
  * navigation and the submitting state; your `on.finish` owns closing (`controller.ok(...)`).
55
55
  * Completed sidebar steps are clickable to go back; `canAdvance(step)` gates Next / Finish.
56
+ * Header, body and sidebar block padding follow `Dialog`'s header / body padding vars.
56
57
  *
57
58
  * ### CSS Custom Properties
58
59
  * | Property | Description | Default |
@@ -1,120 +1,113 @@
1
1
  <script lang="ts">import { InputClearButton } from '../_internal/input-clear-button';
2
2
  import { Icon } from '../icon';
3
3
  import { formatNumeral, NumeralThousandGroupStyles, registerCursorTracker } from 'cleave-zen';
4
- 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();
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
6
  let inputEl = $state.raw(undefined);
7
+ let focused = $state(false);
8
+ let typedValue = $state(undefined);
6
9
  const formatOptions = $derived({
7
10
  delimiter: ' ',
8
11
  numeralDecimalScale: decimals,
9
12
  numeralThousandsGroupStyle: NumeralThousandGroupStyles.THOUSAND,
10
13
  numeralIntegerScale: 14
11
14
  });
12
- // Display string — formatted value + suffix, or empty when null.
13
- const displayValue = $derived(mode.value === null || mode.value === undefined ? '' : formatNumeral(String(mode.value), formatOptions) + (suffix && mode.value !== null ? suffix : ''));
15
+ const toRoundedPlainString = (value) => Number(value.toFixed(decimals))
16
+ .toFixed(decimals)
17
+ .replace(/(\.\d*?)0+$/, '$1')
18
+ .replace(/\.$/, '');
19
+ const formatValue = (value) => value === null || value === undefined ? '' : formatNumeral(toRoundedPlainString(value), formatOptions);
20
+ const displayValue = $derived(formatValue(mode.value));
14
21
  const clearButtonVisible = $derived(mode.clearable === true && !disabled && !readonly && !inert && mode.value !== null && mode.value !== undefined);
15
22
  /** Imperative API exposed via `bind:this`. */
16
23
  export const focus = () => inputEl?.focus();
17
24
  export const select = () => inputEl?.select();
18
- /** Parses the raw input string back to a number (or null when empty). Clamps to min/max. */
19
- const parseValue = (raw) => {
20
- if (!raw.trim()) {
21
- return null;
22
- }
23
- // Strip suffix + non-numeric chars except `.` `,` `-`, then normalize to dot-decimal.
24
- let cleaned = suffix ? raw.replace(suffix, '') : raw;
25
- cleaned = cleaned
26
- .replace(/\s/g, '')
27
- .replace(',', '.')
28
- .replace(/[^0-9.-]/g, '');
29
- const n = parseFloat(cleaned);
30
- if (Number.isNaN(n)) {
31
- return null;
32
- }
33
- let clamped = n;
34
- if (min !== null && clamped < min) {
35
- clamped = min;
36
- }
37
- if (max !== null && clamped > max) {
38
- clamped = max;
39
- }
40
- return Number(clamped.toFixed(decimals));
25
+ const parseValue = (formatted) => {
26
+ const n = parseFloat(formatted.replace(/\s/g, ''));
27
+ return Number.isNaN(n) ? null : n;
41
28
  };
42
- const initInput = (node) => {
43
- inputEl = node;
44
- if (autofocus) {
45
- node.focus();
29
+ const resolveValue = (typed) => {
30
+ if (typed !== null) {
31
+ return Math.min(Math.max(typed, min ?? -Infinity), max ?? Infinity);
46
32
  }
47
- registerCursorTracker({ input: node, delimiter: formatOptions.delimiter, delimiters: [] });
48
- mode.on?.mounted?.({ input: node });
33
+ return mode.clearable ? null : (min ?? mode.value);
49
34
  };
50
- const emit = (parsed, kind) => {
35
+ const resolvedTypedValue = $derived(typedValue === undefined ? undefined : resolveValue(typedValue));
36
+ const valueHintVisible = $derived(resolvedTypedValue !== typedValue);
37
+ const initialText = untrack(() => displayValue);
38
+ // Writing the prop into the field while focused would overwrite a typed "1" with min "8" before "12" can be typed.
39
+ $effect(() => {
40
+ if (inputEl && !focused) {
41
+ inputEl.value = displayValue;
42
+ }
43
+ });
44
+ const emit = (kind, value) => {
51
45
  if (mode.clearable) {
52
- const cb = mode.on?.[kind];
53
- cb?.(parsed);
46
+ mode.on?.[kind]?.(value);
54
47
  }
55
- else {
56
- // RequiredProps callbacks take `number` (never null). Coerce null → 0 (the legacy contract).
57
- if (parsed !== null) {
58
- mode.on?.[kind]?.(parsed);
59
- }
48
+ else if (value !== null) {
49
+ mode.on?.[kind]?.(value);
60
50
  }
61
51
  };
52
+ // Safari fires no native change once script has rewritten the value during input, so Enter and blur commit too.
53
+ const commit = () => {
54
+ if (!inputEl || typedValue === undefined) {
55
+ return;
56
+ }
57
+ const value = resolveValue(typedValue);
58
+ typedValue = undefined;
59
+ inputEl.value = formatValue(value);
60
+ emit('change', value);
61
+ };
62
62
  const handleInput = () => {
63
63
  if (!inputEl) {
64
64
  return;
65
65
  }
66
- // Reformat through cleave-zen so invalid characters are stripped and the decimal scale
67
- // is capped to `decimals` while the user is typing. Strip suffix first so it doesn't
68
- // get mangled, re-add nothing here (suffix re-appears on blur via displayValue resync).
69
- const raw = suffix ? inputEl.value.replace(suffix, '') : inputEl.value;
70
- let formatted = formatNumeral(raw, formatOptions);
71
- // Clamp on the fly when typing pushes the value past min / max — re-format the clamped
72
- // result so the visible input matches the emitted value.
73
- const parsedRaw = parseFloat(formatted.replace(/\s/g, '').replace(',', '.'));
74
- if (!Number.isNaN(parsedRaw)) {
75
- let clamped = parsedRaw;
76
- if (min !== null && clamped < min) {
77
- clamped = min;
78
- }
79
- if (max !== null && clamped > max) {
80
- clamped = max;
81
- }
82
- if (clamped !== parsedRaw) {
83
- formatted = formatNumeral(String(Number(clamped.toFixed(decimals))), formatOptions);
84
- }
85
- }
66
+ const raw = inputEl.value.includes('.') ? inputEl.value : inputEl.value.replace(',', '.');
67
+ const formatted = formatNumeral(raw, formatOptions);
86
68
  if (inputEl.value !== formatted) {
87
69
  inputEl.value = formatted;
88
70
  }
89
- const parsed = parseValue(inputEl.value);
90
- emit(parsed, 'input');
71
+ typedValue = parseValue(formatted);
72
+ emit('input', resolveValue(typedValue));
91
73
  };
92
- const handleChange = () => {
93
- if (!inputEl) {
94
- return;
74
+ const handleKeydown = (event) => {
75
+ if (event.key === 'Enter' && !event.isComposing) {
76
+ commit();
95
77
  }
96
- const parsed = parseValue(inputEl.value);
97
- emit(parsed, 'change');
98
78
  };
99
79
  const handleClear = () => {
100
80
  if (!inputEl) {
101
81
  return;
102
82
  }
103
83
  inputEl.value = '';
104
- emit(null, 'input');
105
- emit(null, 'change');
84
+ typedValue = undefined;
85
+ emit('input', null);
86
+ emit('change', null);
106
87
  inputEl.focus();
107
88
  };
89
+ const handleFocus = () => {
90
+ focused = true;
91
+ mode.on?.focus?.();
92
+ };
108
93
  const handleBlur = () => {
109
- if (!inputEl) {
110
- return;
94
+ commit();
95
+ focused = false;
96
+ mode.on?.blur?.();
97
+ };
98
+ const initInput = (node) => {
99
+ inputEl = node;
100
+ // A field focused before hydration never gets a focus event, so the sync effect would overwrite what is typed next.
101
+ if (node.matches(':focus')) {
102
+ handleFocus();
103
+ }
104
+ if (autofocus) {
105
+ node.focus();
111
106
  }
112
- const parsed = parseValue(inputEl.value);
113
- // Re-sync to the formatted display value so the input reflects clamped / canonical form.
114
- inputEl.value = displayValue;
115
- emit(parsed, 'blur');
107
+ const unregisterCursorTracker = registerCursorTracker({ input: node, delimiter: formatOptions.delimiter, delimiters: [] });
108
+ mode.on?.mounted?.({ input: node });
109
+ return { destroy: unregisterCursorTracker };
116
110
  };
117
- const focusInput = () => inputEl?.focus();
118
111
  </script>
119
112
 
120
113
  <div
@@ -124,7 +117,7 @@ const focusInput = () => inputEl?.focus();
124
117
  class:numeral-input--inert={inert}
125
118
  class:numeral-input--error={error}
126
119
  class:numeral-input--borderless={borderless}
127
- onclick={focusInput}
120
+ onclick={focus}
128
121
  onkeydown={() => undefined}
129
122
  role="none">
130
123
  {#if icon && iconPosition === 'leading'}
@@ -152,15 +145,24 @@ const focusInput = () => inputEl?.focus();
152
145
  inert={inert}
153
146
  autofocus={autofocus}
154
147
  title={title}
155
- value={displayValue}
148
+ value={initialText}
156
149
  aria-invalid={error ? 'true' : undefined}
157
150
  aria-describedby={ariaDescribedby}
158
151
  aria-label={ariaLabel}
159
152
  aria-required={ariaRequired ? 'true' : undefined}
160
153
  oninput={handleInput}
161
- onchange={handleChange}
154
+ onchange={commit}
162
155
  onblur={handleBlur}
163
- onfocus={() => mode.on?.focus?.()} />
156
+ onfocus={handleFocus}
157
+ onkeydown={handleKeydown} />
158
+
159
+ {#if valueHintVisible}
160
+ <span class="numeral-input__value-hint"><span aria-hidden="true">→</span> {formatValue(resolvedTypedValue)}</span>
161
+ {/if}
162
+
163
+ {#if suffix}
164
+ <span class="numeral-input__suffix">{suffix}</span>
165
+ {/if}
164
166
 
165
167
  {#if clearButtonVisible}
166
168
  <InputClearButton size={size} on={{ clear: handleClear }} />
@@ -180,10 +182,16 @@ const focusInput = () => inputEl?.focus();
180
182
 
181
183
  <!--
182
184
  @component
183
- Formatted number input with locale-aware thousand separators, decimal scale, min/max clamping,
184
- optional suffix (` %`, ` kg`), and smooth cursor tracking while typing (via cleave-zen).
185
- Discriminated by `clearable`: `clearable: true` allows `null` (with × button); `clearable: false`
186
- requires a non-null value. CSS API mirrors `Input` for visual-language compatibility.
185
+ Formatted number input with space-grouped thousands, a dot or comma decimal mark, decimal scale, min/max clamping,
186
+ a muted trailing `suffix` unit, and smooth cursor tracking while typing
187
+ (via cleave-zen). Discriminated by `clearable`: `clearable: true` allows `null` (with × button);
188
+ `clearable: false` requires a non-null value. CSS API mirrors `Input` for visual-language compatibility.
189
+
190
+ While focused the text belongs to the user: a value change from outside lands on blur. Every
191
+ emitted value is clamped to `min` / `max`, but the typed text is left alone until commit (blur,
192
+ Enter, `change`) — so a prefix below `min` stays typeable. With `clearable: false` an empty
193
+ 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`.
187
195
 
188
196
  ### CSS Custom Properties
189
197
  | Property | Description | Default |
@@ -204,6 +212,7 @@ requires a non-null value. CSS API mirrors `Input` for visual-language compatibi
204
212
  | `--sc-kit--numeral-input--placeholder--color` | Placeholder text color | `var(--sc-kit--color--text--placeholder)` |
205
213
  | `--sc-kit--numeral-input--icon--color` | Leading / trailing icon color (does NOT affect inner affordances) | `var(--sc-kit--color--text--muted)` |
206
214
  | `--sc-kit--numeral-input--icon--size` | Leading / trailing icon size (does NOT affect inner affordances) | per size |
215
+ | `--sc-kit--numeral-input--suffix--color` | Suffix and value hint color | `var(--sc-kit--color--text--muted)` |
207
216
  -->
208
217
 
209
218
  <style>.numeral-input {
@@ -224,6 +233,7 @@ requires a non-null value. CSS API mirrors `Input` for visual-language compatibi
224
233
  --_ni--placeholder-color: var(--sc-kit--numeral-input--placeholder--color, var(--sc-kit--color--text--placeholder));
225
234
  --_ni--icon-color: var(--sc-kit--numeral-input--icon--color, var(--sc-kit--color--text--muted));
226
235
  --_ni--icon-size: var(--sc-kit--numeral-input--icon--size, var(--_ni--size-icon-size, var(--sc-kit--field--icon--size--md)));
236
+ --_ni--suffix-color: var(--sc-kit--numeral-input--suffix--color, var(--sc-kit--color--text--muted));
227
237
  --_--input-affordance--opacity: 0;
228
238
  --_--input-affordance--scale: 0;
229
239
  }
@@ -321,6 +331,19 @@ requires a non-null value. CSS API mirrors `Input` for visual-language compatibi
321
331
  .numeral-input__el:disabled {
322
332
  cursor: default;
323
333
  }
334
+ .numeral-input__value-hint {
335
+ flex-shrink: 0;
336
+ margin-inline-start: var(--_ni--gap);
337
+ color: var(--_ni--suffix-color);
338
+ font-variant-numeric: tabular-nums;
339
+ white-space: nowrap;
340
+ }
341
+ .numeral-input__suffix {
342
+ flex-shrink: 0;
343
+ margin-inline-start: var(--_ni--gap);
344
+ color: var(--_ni--suffix-color);
345
+ white-space: nowrap;
346
+ }
324
347
  .numeral-input__icon {
325
348
  --sc-kit--icon--color: var(--_ni--icon-color);
326
349
  --sc-kit--icon--size: var(--_ni--icon-size);
@@ -1,4 +1,4 @@
1
- import type { Snippet } from 'svelte';
1
+ import { type Snippet } from 'svelte';
2
2
  type BaseProps = {
3
3
  /** @default 'md' */
4
4
  size?: 'sm' | 'md' | 'lg';
@@ -16,13 +16,13 @@ type BaseProps = {
16
16
  name?: string;
17
17
  id?: string | null;
18
18
  autofocus?: boolean;
19
- /** Minimum allowed value (clamped on input). */
19
+ /** Minimum value emitted to the consumer, also emitted for an empty `clearable: false` field; the typed text is left alone until commit. */
20
20
  min?: number | null;
21
- /** Maximum allowed value (clamped on input). */
21
+ /** Maximum value emitted to the consumer; the typed text is left alone until commit. */
22
22
  max?: number | null;
23
23
  /** Number of decimal places shown. @default 2 */
24
24
  decimals?: number;
25
- /** Suffix appended to the formatted value at rest (e.g. ` %`, ` kg`). */
25
+ /** Muted unit rendered after the value, outside the editable text (`px`, `%`, `°`). */
26
26
  suffix?: string;
27
27
  'aria-describedby'?: string;
28
28
  'aria-label'?: string;
@@ -44,7 +44,7 @@ type ClearableProps = BaseProps & {
44
44
  mounted?: (data: {
45
45
  input: HTMLInputElement;
46
46
  }) => void;
47
- blur?: (value: number | null) => void;
47
+ blur?: () => void;
48
48
  focus?: () => void;
49
49
  };
50
50
  };
@@ -58,16 +58,22 @@ type RequiredProps = BaseProps & {
58
58
  mounted?: (data: {
59
59
  input: HTMLInputElement;
60
60
  }) => void;
61
- blur?: (value: number) => void;
61
+ blur?: () => void;
62
62
  focus?: () => void;
63
63
  };
64
64
  };
65
65
  type Props = ClearableProps | RequiredProps;
66
66
  /**
67
- * Formatted number input with locale-aware thousand separators, decimal scale, min/max clamping,
68
- * optional suffix (` %`, ` kg`), and smooth cursor tracking while typing (via cleave-zen).
69
- * Discriminated by `clearable`: `clearable: true` allows `null` (with × button); `clearable: false`
70
- * requires a non-null value. CSS API mirrors `Input` for visual-language compatibility.
67
+ * Formatted number input with space-grouped thousands, a dot or comma decimal mark, decimal scale, min/max clamping,
68
+ * a muted trailing `suffix` unit, and smooth cursor tracking while typing
69
+ * (via cleave-zen). Discriminated by `clearable`: `clearable: true` allows `null` (with × button);
70
+ * `clearable: false` requires a non-null value. CSS API mirrors `Input` for visual-language compatibility.
71
+ *
72
+ * While focused the text belongs to the user: a value change from outside lands on blur. Every
73
+ * emitted value is clamped to `min` / `max`, but the typed text is left alone until commit (blur,
74
+ * Enter, `change`) — so a prefix below `min` stays typeable. With `clearable: false` an empty
75
+ * 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`.
71
77
  *
72
78
  * ### CSS Custom Properties
73
79
  * | Property | Description | Default |
@@ -88,6 +94,7 @@ type Props = ClearableProps | RequiredProps;
88
94
  * | `--sc-kit--numeral-input--placeholder--color` | Placeholder text color | `var(--sc-kit--color--text--placeholder)` |
89
95
  * | `--sc-kit--numeral-input--icon--color` | Leading / trailing icon color (does NOT affect inner affordances) | `var(--sc-kit--color--text--muted)` |
90
96
  * | `--sc-kit--numeral-input--icon--size` | Leading / trailing icon size (does NOT affect inner affordances) | per size |
97
+ * | `--sc-kit--numeral-input--suffix--color` | Suffix and value hint color | `var(--sc-kit--color--text--muted)` |
91
98
  */
92
99
  declare const Cmp: import("svelte").Component<Props, {
93
100
  /** Imperative API exposed via `bind:this`. */ focus: () => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@streamscloud/kit",
3
- "version": "0.78.1",
3
+ "version": "0.79.0",
4
4
  "author": "StreamsCloud",
5
5
  "repository": {
6
6
  "type": "git",