@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.
- package/dist/ui/dialog/cmp.dialog.svelte +20 -17
- package/dist/ui/dialog/cmp.dialog.svelte.d.ts +8 -8
- package/dist/ui/dialog/layout/cmp.stepper-dialog.svelte +4 -4
- package/dist/ui/dialog/layout/cmp.stepper-dialog.svelte.d.ts +1 -0
- package/dist/ui/numeral-input/cmp.numeral-input.svelte +107 -84
- package/dist/ui/numeral-input/cmp.numeral-input.svelte.d.ts +17 -10
- package/package.json +1 -1
|
@@ -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 |
|
|
58
|
-
| `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding |
|
|
59
|
-
| `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding |
|
|
60
|
-
| `--sc-kit--dialog--header--padding-inline` | Header-specific horizontal padding |
|
|
61
|
-
| `--sc-kit--dialog--footer--padding-block` | Footer-specific vertical padding |
|
|
62
|
-
| `--sc-kit--dialog--footer--padding-inline` | Footer-specific horizontal padding |
|
|
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--
|
|
77
|
-
--_dialog--padding-inline: var(--sc-kit--
|
|
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
|
-
--
|
|
80
|
-
--
|
|
81
|
-
--
|
|
82
|
-
--
|
|
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-
|
|
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(--
|
|
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(--
|
|
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 |
|
|
59
|
-
* | `--sc-kit--dialog--body--padding-inline` | Body-specific horizontal padding |
|
|
60
|
-
* | `--sc-kit--dialog--header--padding-block` | Header-specific vertical padding |
|
|
61
|
-
* | `--sc-kit--dialog--header--padding-inline` | Header-specific horizontal padding |
|
|
62
|
-
* | `--sc-kit--dialog--footer--padding-block` | Footer-specific vertical padding |
|
|
63
|
-
* | `--sc-kit--dialog--footer--padding-inline` | Footer-specific horizontal padding |
|
|
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(--
|
|
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(--
|
|
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(--
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
-
|
|
19
|
-
const
|
|
20
|
-
|
|
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
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
mode.on?.mounted?.({ input: node });
|
|
33
|
+
return mode.clearable ? null : (min ?? mode.value);
|
|
49
34
|
};
|
|
50
|
-
const
|
|
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
|
-
|
|
53
|
-
cb?.(parsed);
|
|
46
|
+
mode.on?.[kind]?.(value);
|
|
54
47
|
}
|
|
55
|
-
else {
|
|
56
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
90
|
-
emit(
|
|
71
|
+
typedValue = parseValue(formatted);
|
|
72
|
+
emit('input', resolveValue(typedValue));
|
|
91
73
|
};
|
|
92
|
-
const
|
|
93
|
-
if (!
|
|
94
|
-
|
|
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
|
-
|
|
105
|
-
emit(
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
113
|
-
|
|
114
|
-
|
|
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={
|
|
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={
|
|
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={
|
|
154
|
+
onchange={commit}
|
|
162
155
|
onblur={handleBlur}
|
|
163
|
-
onfocus={
|
|
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
|
|
184
|
-
|
|
185
|
-
Discriminated by `clearable`: `clearable: true` allows `null` (with × button);
|
|
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
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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?: (
|
|
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?: (
|
|
61
|
+
blur?: () => void;
|
|
62
62
|
focus?: () => void;
|
|
63
63
|
};
|
|
64
64
|
};
|
|
65
65
|
type Props = ClearableProps | RequiredProps;
|
|
66
66
|
/**
|
|
67
|
-
* Formatted number input with
|
|
68
|
-
*
|
|
69
|
-
* Discriminated by `clearable`: `clearable: true` allows `null` (with × button);
|
|
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;
|