@sentropic/design-system-svelte 0.3.0 → 0.4.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 (37) hide show
  1. package/dist/Accordion.svelte +187 -0
  2. package/dist/Accordion.svelte.d.ts +20 -0
  3. package/dist/Accordion.svelte.d.ts.map +1 -0
  4. package/dist/Combobox.svelte +390 -0
  5. package/dist/Combobox.svelte.d.ts +28 -0
  6. package/dist/Combobox.svelte.d.ts.map +1 -0
  7. package/dist/DataTable.svelte +543 -0
  8. package/dist/DataTable.svelte.d.ts +50 -0
  9. package/dist/DataTable.svelte.d.ts.map +1 -0
  10. package/dist/MultiSelect.svelte +427 -0
  11. package/dist/MultiSelect.svelte.d.ts +28 -0
  12. package/dist/MultiSelect.svelte.d.ts.map +1 -0
  13. package/dist/NumberInput.svelte +256 -0
  14. package/dist/NumberInput.svelte.d.ts +19 -0
  15. package/dist/NumberInput.svelte.d.ts.map +1 -0
  16. package/dist/PasswordInput.svelte +205 -0
  17. package/dist/PasswordInput.svelte.d.ts +17 -0
  18. package/dist/PasswordInput.svelte.d.ts.map +1 -0
  19. package/dist/ProgressBar.svelte +176 -0
  20. package/dist/ProgressBar.svelte.d.ts +17 -0
  21. package/dist/ProgressBar.svelte.d.ts.map +1 -0
  22. package/dist/Search.svelte +218 -0
  23. package/dist/Search.svelte.d.ts +16 -0
  24. package/dist/Search.svelte.d.ts.map +1 -0
  25. package/dist/Slider.svelte +279 -0
  26. package/dist/Slider.svelte.d.ts +20 -0
  27. package/dist/Slider.svelte.d.ts.map +1 -0
  28. package/dist/Tag.svelte +140 -0
  29. package/dist/Tag.svelte.d.ts +16 -0
  30. package/dist/Tag.svelte.d.ts.map +1 -0
  31. package/dist/Toggle.svelte +135 -0
  32. package/dist/Toggle.svelte.d.ts +14 -0
  33. package/dist/Toggle.svelte.d.ts.map +1 -0
  34. package/dist/index.d.ts +15 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +11 -0
  37. package/package.json +2 -2
@@ -0,0 +1,427 @@
1
+ <script lang="ts" module>
2
+ export interface MultiSelectOption {
3
+ label: string;
4
+ value: string;
5
+ disabled?: boolean;
6
+ }
7
+ </script>
8
+
9
+ <script lang="ts">
10
+ import type { HTMLAttributes } from "svelte/elements";
11
+
12
+ type MultiSelectProps = Omit<HTMLAttributes<HTMLDivElement>, "class" | "onchange"> & {
13
+ label?: string;
14
+ helperText?: string;
15
+ errorText?: string;
16
+ invalid?: boolean;
17
+ size?: "sm" | "md" | "lg";
18
+ options: MultiSelectOption[];
19
+ selected?: string[];
20
+ placeholder?: string;
21
+ searchPlaceholder?: string;
22
+ noResultsLabel?: string;
23
+ toggleLabel?: string;
24
+ removeLabel?: string;
25
+ listLabel?: string;
26
+ disabled?: boolean;
27
+ class?: string;
28
+ onchange?: (selected: string[]) => void;
29
+ };
30
+
31
+ let {
32
+ label,
33
+ helperText,
34
+ errorText,
35
+ invalid = false,
36
+ size = "md",
37
+ options,
38
+ selected = $bindable([]),
39
+ placeholder = "Select items",
40
+ searchPlaceholder = "Filter",
41
+ noResultsLabel = "No results",
42
+ toggleLabel = "Toggle options",
43
+ removeLabel = "Remove",
44
+ listLabel,
45
+ disabled = false,
46
+ class: className,
47
+ onchange,
48
+ ...rest
49
+ }: MultiSelectProps = $props();
50
+
51
+ let expanded = $state(false);
52
+ let query = $state("");
53
+
54
+ const fieldClasses = () => ["st-field", className].filter(Boolean).join(" ");
55
+ const groupClasses = () => ["st-multiSelect", `st-multiSelect--${size}`].join(" ");
56
+ const isInvalid = () => invalid || Boolean(errorText);
57
+
58
+ const filtered = $derived.by(() => {
59
+ const q = query.trim().toLowerCase();
60
+ if (!q) return options;
61
+ return options.filter((opt) => opt.label.toLowerCase().includes(q));
62
+ });
63
+
64
+ const selectedOptions = $derived(
65
+ selected
66
+ .map((value) => options.find((opt) => opt.value === value))
67
+ .filter((opt): opt is MultiSelectOption => Boolean(opt))
68
+ );
69
+
70
+ function toggleOption(option: MultiSelectOption) {
71
+ if (option.disabled) return;
72
+ const next = selected.includes(option.value)
73
+ ? selected.filter((v) => v !== option.value)
74
+ : [...selected, option.value];
75
+ selected = next;
76
+ onchange?.(next);
77
+ }
78
+
79
+ function removeOption(value: string) {
80
+ const next = selected.filter((v) => v !== value);
81
+ selected = next;
82
+ onchange?.(next);
83
+ }
84
+
85
+ function toggleOpen() {
86
+ if (disabled) return;
87
+ expanded = !expanded;
88
+ }
89
+
90
+ function onContainerKeyDown(event: KeyboardEvent) {
91
+ if (event.key === "Escape" && expanded) {
92
+ event.preventDefault();
93
+ expanded = false;
94
+ }
95
+ }
96
+ </script>
97
+
98
+ <div
99
+ {...rest}
100
+ class={fieldClasses()}
101
+ role="group"
102
+ aria-label={label}
103
+ onkeydown={onContainerKeyDown}
104
+ >
105
+ {#if label}<span class="st-field__label">{label}</span>{/if}
106
+ {#if selectedOptions.length > 0}
107
+ <span class="st-multiSelect__tags">
108
+ {#each selectedOptions as option (option.value)}
109
+ <span class="st-multiSelect__tag">
110
+ <span class="st-multiSelect__tagLabel">{option.label}</span>
111
+ <button
112
+ type="button"
113
+ class="st-multiSelect__tagRemove"
114
+ aria-label={`${removeLabel} ${option.label}`}
115
+ {disabled}
116
+ onclick={() => removeOption(option.value)}
117
+ >
118
+ <span aria-hidden="true">×</span>
119
+ </button>
120
+ </span>
121
+ {/each}
122
+ </span>
123
+ {/if}
124
+ <span class={groupClasses()} data-invalid={isInvalid() ? "true" : undefined}>
125
+ <button
126
+ type="button"
127
+ class="st-multiSelect__trigger"
128
+ aria-haspopup="listbox"
129
+ aria-expanded={expanded ? "true" : "false"}
130
+ {disabled}
131
+ onclick={toggleOpen}
132
+ >
133
+ {#if selectedOptions.length === 0}
134
+ <span class="st-multiSelect__placeholder">{placeholder}</span>
135
+ {:else}
136
+ <span class="st-multiSelect__count">{selectedOptions.length} selected</span>
137
+ {/if}
138
+ <span class="st-multiSelect__caret" aria-hidden="true">▾</span>
139
+ <span class="st-visually-hidden">{toggleLabel}</span>
140
+ </button>
141
+ </span>
142
+ {#if expanded}
143
+ <div class="st-multiSelect__panel">
144
+ <input
145
+ type="search"
146
+ class="st-multiSelect__search"
147
+ placeholder={searchPlaceholder}
148
+ bind:value={query}
149
+ aria-label={searchPlaceholder}
150
+ />
151
+ <div class="st-multiSelect__list" role="listbox" aria-label={listLabel ?? label ?? "Options"} aria-multiselectable="true">
152
+ {#if filtered.length === 0}
153
+ <div class="st-multiSelect__empty">{noResultsLabel}</div>
154
+ {:else}
155
+ {#each filtered as option (option.value)}
156
+ {@const isSelected = selected.includes(option.value)}
157
+ <button
158
+ class="st-multiSelect__option"
159
+ type="button"
160
+ role="option"
161
+ aria-selected={isSelected ? "true" : "false"}
162
+ aria-disabled={option.disabled ? "true" : undefined}
163
+ disabled={option.disabled}
164
+ onclick={() => toggleOption(option)}
165
+ >
166
+ <span class="st-multiSelect__check" aria-hidden="true">
167
+ {#if isSelected}✓{/if}
168
+ </span>
169
+ <span>{option.label}</span>
170
+ </button>
171
+ {/each}
172
+ {/if}
173
+ </div>
174
+ </div>
175
+ {/if}
176
+ {#if errorText}
177
+ <span class="st-field__error">{errorText}</span>
178
+ {:else if helperText}
179
+ <span class="st-field__help">{helperText}</span>
180
+ {/if}
181
+ </div>
182
+
183
+ <style>
184
+ .st-field {
185
+ color: var(--st-component-field-labelText, var(--st-semantic-text-primary));
186
+ display: grid;
187
+ gap: var(--st-component-field-gap, 0.5rem);
188
+ max-width: var(--st-component-field-maxWidth, 28rem);
189
+ position: relative;
190
+ }
191
+
192
+ .st-field__label {
193
+ font-size: 0.875rem;
194
+ font-weight: 600;
195
+ }
196
+
197
+ .st-field__help,
198
+ .st-field__error {
199
+ font-size: 0.8125rem;
200
+ line-height: 1.4;
201
+ }
202
+
203
+ .st-field__help {
204
+ color: var(--st-component-field-helpText, var(--st-semantic-text-secondary));
205
+ }
206
+
207
+ .st-field__error {
208
+ color: var(--st-component-field-errorText, var(--st-semantic-feedback-error));
209
+ }
210
+
211
+ .st-visually-hidden {
212
+ border: 0;
213
+ clip: rect(0 0 0 0);
214
+ height: 1px;
215
+ margin: -1px;
216
+ overflow: hidden;
217
+ padding: 0;
218
+ position: absolute;
219
+ white-space: nowrap;
220
+ width: 1px;
221
+ }
222
+
223
+ .st-multiSelect {
224
+ display: inline-flex;
225
+ width: 100%;
226
+ }
227
+
228
+ .st-multiSelect--sm .st-multiSelect__trigger {
229
+ min-height: var(--st-component-control-smHeight, 2rem);
230
+ }
231
+
232
+ .st-multiSelect--md .st-multiSelect__trigger {
233
+ min-height: var(--st-component-control-mdHeight, 2.5rem);
234
+ }
235
+
236
+ .st-multiSelect--lg .st-multiSelect__trigger {
237
+ min-height: var(--st-component-control-lgHeight, 3rem);
238
+ }
239
+
240
+ .st-multiSelect__trigger {
241
+ align-items: center;
242
+ background: var(--st-component-control-background, var(--st-semantic-surface-default));
243
+ border: 1px solid var(--st-component-control-border, var(--st-semantic-border-subtle));
244
+ border-radius: var(--st-component-control-radius, 0.375rem);
245
+ color: var(--st-component-control-text, var(--st-semantic-text-primary));
246
+ cursor: pointer;
247
+ display: flex;
248
+ flex-wrap: wrap;
249
+ font: inherit;
250
+ gap: 0.375rem;
251
+ padding: 0.25rem 0.5rem 0.25rem 0.75rem;
252
+ transition:
253
+ border-color var(--st-motion-fast, 120ms) var(--st-motion-easing, ease),
254
+ box-shadow var(--st-motion-fast, 120ms) var(--st-motion-easing, ease);
255
+ width: 100%;
256
+ }
257
+
258
+ .st-multiSelect__trigger:hover:not(:disabled) {
259
+ border-color: var(--st-component-control-hoverBorder, var(--st-semantic-border-strong));
260
+ }
261
+
262
+ .st-multiSelect__trigger:focus-visible {
263
+ border-color: var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
264
+ box-shadow: 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
265
+ outline: none;
266
+ }
267
+
268
+ .st-multiSelect[data-invalid="true"] .st-multiSelect__trigger {
269
+ border-color: var(--st-component-control-invalidBorder, var(--st-semantic-feedback-error));
270
+ }
271
+
272
+ .st-multiSelect__trigger:disabled {
273
+ background: var(--st-component-control-disabledBackground, var(--st-semantic-surface-subtle));
274
+ color: var(--st-component-control-disabledText, var(--st-semantic-text-muted));
275
+ cursor: not-allowed;
276
+ }
277
+
278
+ .st-multiSelect__placeholder {
279
+ color: var(--st-component-control-placeholderText, var(--st-semantic-text-muted));
280
+ flex: 1 1 auto;
281
+ text-align: start;
282
+ }
283
+
284
+ .st-multiSelect__count {
285
+ color: var(--st-semantic-text-secondary);
286
+ flex: 1 1 auto;
287
+ font-size: 0.875rem;
288
+ text-align: start;
289
+ }
290
+
291
+ .st-multiSelect__tags {
292
+ display: flex;
293
+ flex-wrap: wrap;
294
+ gap: 0.25rem;
295
+ }
296
+
297
+ .st-multiSelect__tag {
298
+ align-items: center;
299
+ background: var(--st-semantic-surface-subtle);
300
+ border-radius: var(--st-radius-pill, 999px);
301
+ color: var(--st-semantic-text-primary);
302
+ display: inline-flex;
303
+ font-size: 0.75rem;
304
+ font-weight: 600;
305
+ gap: 0.25rem;
306
+ line-height: 1;
307
+ padding: 0.25rem 0.5rem;
308
+ }
309
+
310
+ .st-multiSelect__tagLabel {
311
+ line-height: 1;
312
+ }
313
+
314
+ .st-multiSelect__tagRemove {
315
+ align-items: center;
316
+ background: transparent;
317
+ border: 0;
318
+ border-radius: 50%;
319
+ color: inherit;
320
+ cursor: pointer;
321
+ display: inline-flex;
322
+ font: inherit;
323
+ font-size: 1em;
324
+ height: 1.25em;
325
+ justify-content: center;
326
+ line-height: 1;
327
+ padding: 0;
328
+ width: 1.25em;
329
+ }
330
+
331
+ .st-multiSelect__tagRemove:hover:not(:disabled) {
332
+ background: color-mix(in srgb, currentColor 18%, transparent);
333
+ }
334
+
335
+ .st-multiSelect__tagRemove:focus-visible {
336
+ outline: 2px solid currentColor;
337
+ outline-offset: 1px;
338
+ }
339
+
340
+ .st-multiSelect__caret {
341
+ align-items: center;
342
+ color: var(--st-semantic-text-secondary);
343
+ display: inline-flex;
344
+ flex: 0 0 auto;
345
+ font-size: 0.875rem;
346
+ margin-inline-start: auto;
347
+ padding-inline-start: 0.25rem;
348
+ }
349
+
350
+ .st-multiSelect__panel {
351
+ background: var(--st-component-dropdown-background, var(--st-semantic-surface-default));
352
+ border: 1px solid var(--st-component-dropdown-border, var(--st-semantic-border-subtle));
353
+ border-radius: var(--st-component-dropdown-radius, 0.375rem);
354
+ box-shadow: var(--st-component-dropdown-shadow, 0 8px 24px rgb(15 23 42 / 0.14));
355
+ display: grid;
356
+ left: 0;
357
+ margin-top: var(--st-spacing-1, 0.25rem);
358
+ position: absolute;
359
+ right: 0;
360
+ top: 100%;
361
+ z-index: var(--st-component-popover-zIndex, 80);
362
+ }
363
+
364
+ .st-multiSelect__search {
365
+ background: transparent;
366
+ border: 0;
367
+ border-bottom: 1px solid var(--st-component-control-border, var(--st-semantic-border-subtle));
368
+ color: inherit;
369
+ font: inherit;
370
+ padding: 0.5rem 0.75rem;
371
+ width: 100%;
372
+ }
373
+
374
+ .st-multiSelect__search:focus {
375
+ outline: none;
376
+ }
377
+
378
+ .st-multiSelect__list {
379
+ display: grid;
380
+ max-height: 14rem;
381
+ overflow-y: auto;
382
+ }
383
+
384
+ .st-multiSelect__empty {
385
+ color: var(--st-semantic-text-muted);
386
+ padding: var(--st-spacing-2, 0.5rem) var(--st-spacing-3, 0.75rem);
387
+ }
388
+
389
+ .st-multiSelect__option {
390
+ align-items: center;
391
+ background: transparent;
392
+ border: 0;
393
+ color: var(--st-component-dropdown-text, var(--st-semantic-text-primary));
394
+ cursor: pointer;
395
+ display: flex;
396
+ font: inherit;
397
+ gap: 0.5rem;
398
+ padding: var(--st-spacing-2, 0.5rem) var(--st-spacing-3, 0.75rem);
399
+ text-align: left;
400
+ }
401
+
402
+ .st-multiSelect__option:hover:not(:disabled),
403
+ .st-multiSelect__option:focus-visible {
404
+ background: var(
405
+ --st-component-dropdown-optionHoverBackground,
406
+ var(--st-semantic-surface-subtle)
407
+ );
408
+ outline: none;
409
+ }
410
+
411
+ .st-multiSelect__option[aria-selected="true"] {
412
+ background: color-mix(in srgb, var(--st-semantic-action-primary) 12%, transparent);
413
+ }
414
+
415
+ .st-multiSelect__option:disabled {
416
+ color: var(--st-semantic-text-muted);
417
+ cursor: not-allowed;
418
+ }
419
+
420
+ .st-multiSelect__check {
421
+ color: var(--st-semantic-action-primary);
422
+ display: inline-flex;
423
+ font-weight: 700;
424
+ justify-content: center;
425
+ width: 1rem;
426
+ }
427
+ </style>
@@ -0,0 +1,28 @@
1
+ export interface MultiSelectOption {
2
+ label: string;
3
+ value: string;
4
+ disabled?: boolean;
5
+ }
6
+ import type { HTMLAttributes } from "svelte/elements";
7
+ type MultiSelectProps = Omit<HTMLAttributes<HTMLDivElement>, "class" | "onchange"> & {
8
+ label?: string;
9
+ helperText?: string;
10
+ errorText?: string;
11
+ invalid?: boolean;
12
+ size?: "sm" | "md" | "lg";
13
+ options: MultiSelectOption[];
14
+ selected?: string[];
15
+ placeholder?: string;
16
+ searchPlaceholder?: string;
17
+ noResultsLabel?: string;
18
+ toggleLabel?: string;
19
+ removeLabel?: string;
20
+ listLabel?: string;
21
+ disabled?: boolean;
22
+ class?: string;
23
+ onchange?: (selected: string[]) => void;
24
+ };
25
+ declare const MultiSelect: import("svelte").Component<MultiSelectProps, {}, "selected">;
26
+ type MultiSelect = ReturnType<typeof MultiSelect>;
27
+ export default MultiSelect;
28
+ //# sourceMappingURL=MultiSelect.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"MultiSelect.svelte.d.ts","sourceRoot":"","sources":["../src/lib/MultiSelect.svelte.ts"],"names":[],"mappings":"AAGE,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAGH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAGpD,KAAK,gBAAgB,GAAG,IAAI,CAAC,cAAc,CAAC,cAAc,CAAC,EAAE,OAAO,GAAG,UAAU,CAAC,GAAG;IACnF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,IAAI,GAAG,IAAI,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,IAAI,CAAC;CACzC,CAAC;AAkIJ,QAAA,MAAM,WAAW,8DAAwC,CAAC;AAC1D,KAAK,WAAW,GAAG,UAAU,CAAC,OAAO,WAAW,CAAC,CAAC;AAClD,eAAe,WAAW,CAAC"}
@@ -0,0 +1,256 @@
1
+ <script lang="ts">
2
+ import type { HTMLInputAttributes } from "svelte/elements";
3
+
4
+ type NumberInputProps = Omit<HTMLInputAttributes, "class" | "size" | "type" | "value"> & {
5
+ label?: string;
6
+ helperText?: string;
7
+ errorText?: string;
8
+ invalid?: boolean;
9
+ size?: "sm" | "md" | "lg";
10
+ value?: number | null;
11
+ min?: number;
12
+ max?: number;
13
+ step?: number;
14
+ incrementLabel?: string;
15
+ decrementLabel?: string;
16
+ class?: string;
17
+ };
18
+
19
+ let {
20
+ label,
21
+ helperText,
22
+ errorText,
23
+ invalid = false,
24
+ size = "md",
25
+ value = $bindable(null),
26
+ min,
27
+ max,
28
+ step = 1,
29
+ incrementLabel = "Increment value",
30
+ decrementLabel = "Decrement value",
31
+ disabled,
32
+ class: className,
33
+ ...rest
34
+ }: NumberInputProps = $props();
35
+
36
+ const fieldClasses = () => ["st-field", className].filter(Boolean).join(" ");
37
+ const groupClasses = () => ["st-numberInput", `st-numberInput--${size}`].join(" ");
38
+ const isInvalid = () => invalid || Boolean(errorText);
39
+
40
+ const isAtMin = () => value !== null && value !== undefined && min !== undefined && value <= min;
41
+ const isAtMax = () => value !== null && value !== undefined && max !== undefined && value >= max;
42
+
43
+ const clamp = (n: number) => {
44
+ if (min !== undefined && n < min) return min;
45
+ if (max !== undefined && n > max) return max;
46
+ return n;
47
+ };
48
+
49
+ function increment() {
50
+ const base = value ?? min ?? 0;
51
+ value = clamp(base + step);
52
+ }
53
+
54
+ function decrement() {
55
+ const base = value ?? max ?? 0;
56
+ value = clamp(base - step);
57
+ }
58
+
59
+ function onInput(event: Event) {
60
+ const raw = (event.currentTarget as HTMLInputElement).value;
61
+ if (raw === "") {
62
+ value = null;
63
+ return;
64
+ }
65
+ const parsed = Number(raw);
66
+ value = Number.isFinite(parsed) ? parsed : value;
67
+ }
68
+ </script>
69
+
70
+ <div class={fieldClasses()}>
71
+ <label class="st-field__control">
72
+ {#if label}<span class="st-field__label">{label}</span>{/if}
73
+ <span class={groupClasses()}>
74
+ <input
75
+ {...rest}
76
+ type="number"
77
+ class="st-numberInput__control"
78
+ value={value ?? ""}
79
+ {min}
80
+ {max}
81
+ {step}
82
+ {disabled}
83
+ aria-invalid={isInvalid() ? "true" : undefined}
84
+ oninput={onInput}
85
+ />
86
+ <span class="st-numberInput__buttons">
87
+ <button
88
+ type="button"
89
+ class="st-numberInput__button"
90
+ aria-label={decrementLabel}
91
+ disabled={disabled || isAtMin()}
92
+ onclick={decrement}
93
+ >
94
+ <span aria-hidden="true">−</span>
95
+ </button>
96
+ <button
97
+ type="button"
98
+ class="st-numberInput__button"
99
+ aria-label={incrementLabel}
100
+ disabled={disabled || isAtMax()}
101
+ onclick={increment}
102
+ >
103
+ <span aria-hidden="true">+</span>
104
+ </button>
105
+ </span>
106
+ </span>
107
+ </label>
108
+ {#if errorText}
109
+ <span class="st-field__error">{errorText}</span>
110
+ {:else if helperText}
111
+ <span class="st-field__help">{helperText}</span>
112
+ {/if}
113
+ </div>
114
+
115
+ <style>
116
+ .st-field {
117
+ color: var(--st-component-field-labelText, var(--st-semantic-text-primary));
118
+ display: grid;
119
+ gap: var(--st-component-field-gap, 0.5rem);
120
+ max-width: var(--st-component-field-maxWidth, 28rem);
121
+ }
122
+
123
+ .st-field__control {
124
+ display: grid;
125
+ gap: var(--st-component-field-gap, 0.5rem);
126
+ }
127
+
128
+ .st-field__label {
129
+ font-size: 0.875rem;
130
+ font-weight: 600;
131
+ }
132
+
133
+ .st-field__help,
134
+ .st-field__error {
135
+ font-size: 0.8125rem;
136
+ line-height: 1.4;
137
+ }
138
+
139
+ .st-field__help {
140
+ color: var(--st-component-field-helpText, var(--st-semantic-text-secondary));
141
+ }
142
+
143
+ .st-field__error {
144
+ color: var(--st-component-field-errorText, var(--st-semantic-feedback-error));
145
+ }
146
+
147
+ .st-numberInput {
148
+ align-items: stretch;
149
+ background: var(--st-component-control-background, var(--st-semantic-surface-default));
150
+ border: 1px solid var(--st-component-control-border, var(--st-semantic-border-subtle));
151
+ border-radius: var(--st-component-control-radius, 0.375rem);
152
+ color: var(--st-component-control-text, var(--st-semantic-text-primary));
153
+ display: inline-flex;
154
+ overflow: hidden;
155
+ transition:
156
+ border-color var(--st-motion-fast, 120ms) var(--st-motion-easing, ease),
157
+ box-shadow var(--st-motion-fast, 120ms) var(--st-motion-easing, ease);
158
+ width: 100%;
159
+ }
160
+
161
+ .st-numberInput--sm {
162
+ min-height: var(--st-component-control-smHeight, 2rem);
163
+ }
164
+
165
+ .st-numberInput--md {
166
+ min-height: var(--st-component-control-mdHeight, 2.5rem);
167
+ }
168
+
169
+ .st-numberInput--lg {
170
+ min-height: var(--st-component-control-lgHeight, 3rem);
171
+ }
172
+
173
+ .st-numberInput:hover:not(:has(input:disabled)) {
174
+ border-color: var(--st-component-control-hoverBorder, var(--st-semantic-border-strong));
175
+ }
176
+
177
+ .st-numberInput:focus-within {
178
+ border-color: var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
179
+ box-shadow: 0 0 0 2px var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
180
+ }
181
+
182
+ .st-numberInput:has([aria-invalid="true"]) {
183
+ border-color: var(--st-component-control-invalidBorder, var(--st-semantic-feedback-error));
184
+ }
185
+
186
+ .st-numberInput__control {
187
+ background: transparent;
188
+ border: 0;
189
+ color: inherit;
190
+ flex: 1 1 auto;
191
+ font: inherit;
192
+ min-width: 0;
193
+ padding: 0 0.75rem;
194
+ text-align: right;
195
+ width: 100%;
196
+ }
197
+
198
+ .st-numberInput__control:focus {
199
+ outline: none;
200
+ }
201
+
202
+ .st-numberInput__control::-webkit-inner-spin-button,
203
+ .st-numberInput__control::-webkit-outer-spin-button {
204
+ appearance: none;
205
+ margin: 0;
206
+ }
207
+
208
+ .st-numberInput__control[type="number"] {
209
+ -moz-appearance: textfield;
210
+ }
211
+
212
+ .st-numberInput__control:disabled {
213
+ color: var(--st-component-control-disabledText, var(--st-semantic-text-muted));
214
+ cursor: not-allowed;
215
+ }
216
+
217
+ .st-numberInput__buttons {
218
+ border-left: 1px solid var(--st-component-control-border, var(--st-semantic-border-subtle));
219
+ display: inline-flex;
220
+ flex: 0 0 auto;
221
+ }
222
+
223
+ .st-numberInput__button {
224
+ align-items: center;
225
+ background: transparent;
226
+ border: 0;
227
+ color: inherit;
228
+ cursor: pointer;
229
+ display: inline-flex;
230
+ font: inherit;
231
+ font-size: 1.125rem;
232
+ justify-content: center;
233
+ line-height: 1;
234
+ min-width: 2.25rem;
235
+ padding: 0 0.5rem;
236
+ transition: background-color var(--st-motion-fast, 120ms) var(--st-motion-easing, ease);
237
+ }
238
+
239
+ .st-numberInput__button + .st-numberInput__button {
240
+ border-left: 1px solid var(--st-component-control-border, var(--st-semantic-border-subtle));
241
+ }
242
+
243
+ .st-numberInput__button:hover:not(:disabled) {
244
+ background: var(--st-component-control-hoverBackground, var(--st-semantic-surface-subtle));
245
+ }
246
+
247
+ .st-numberInput__button:focus-visible {
248
+ outline: 2px solid var(--st-component-control-focusRing, var(--st-semantic-border-interactive));
249
+ outline-offset: -2px;
250
+ }
251
+
252
+ .st-numberInput__button:disabled {
253
+ color: var(--st-component-control-disabledText, var(--st-semantic-text-muted));
254
+ cursor: not-allowed;
255
+ }
256
+ </style>