@sveltier/ui 0.0.0-stage → 0.1.0-alpha.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 (276) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +82 -2
  3. package/dist/components/Alert/Alert.svelte +88 -0
  4. package/dist/components/Alert/Alert.svelte.d.ts +4 -0
  5. package/dist/components/Alert/alert.css +78 -0
  6. package/dist/components/Alert/index.d.ts +2 -0
  7. package/dist/components/Alert/index.js +1 -0
  8. package/dist/components/Alert/types.d.ts +67 -0
  9. package/dist/components/Alert/types.js +1 -0
  10. package/dist/components/Avatar/Avatar.svelte +47 -0
  11. package/dist/components/Avatar/Avatar.svelte.d.ts +4 -0
  12. package/dist/components/Avatar/avatar.css +34 -0
  13. package/dist/components/Avatar/index.d.ts +2 -0
  14. package/dist/components/Avatar/index.js +1 -0
  15. package/dist/components/Avatar/types.d.ts +47 -0
  16. package/dist/components/Avatar/types.js +1 -0
  17. package/dist/components/Badge/Badge.svelte +53 -0
  18. package/dist/components/Badge/Badge.svelte.d.ts +4 -0
  19. package/dist/components/Badge/badge.css +57 -0
  20. package/dist/components/Badge/index.d.ts +2 -0
  21. package/dist/components/Badge/index.js +1 -0
  22. package/dist/components/Badge/types.d.ts +43 -0
  23. package/dist/components/Badge/types.js +1 -0
  24. package/dist/components/Button/Button.svelte +85 -0
  25. package/dist/components/Button/Button.svelte.d.ts +4 -0
  26. package/dist/components/Button/button.css +132 -0
  27. package/dist/components/Button/index.d.ts +2 -0
  28. package/dist/components/Button/index.js +1 -0
  29. package/dist/components/Button/types.d.ts +76 -0
  30. package/dist/components/Button/types.js +1 -0
  31. package/dist/components/ButtonGroup/ButtonGroup.svelte +61 -0
  32. package/dist/components/ButtonGroup/ButtonGroup.svelte.d.ts +4 -0
  33. package/dist/components/ButtonGroup/button-group.css +39 -0
  34. package/dist/components/ButtonGroup/index.d.ts +2 -0
  35. package/dist/components/ButtonGroup/index.js +1 -0
  36. package/dist/components/ButtonGroup/types.d.ts +48 -0
  37. package/dist/components/ButtonGroup/types.js +1 -0
  38. package/dist/components/Card/Card.svelte +53 -0
  39. package/dist/components/Card/Card.svelte.d.ts +4 -0
  40. package/dist/components/Card/card.css +62 -0
  41. package/dist/components/Card/index.d.ts +2 -0
  42. package/dist/components/Card/index.js +1 -0
  43. package/dist/components/Card/types.d.ts +56 -0
  44. package/dist/components/Card/types.js +1 -0
  45. package/dist/components/Checkbox/Checkbox.svelte +105 -0
  46. package/dist/components/Checkbox/Checkbox.svelte.d.ts +4 -0
  47. package/dist/components/Checkbox/checkbox.css +138 -0
  48. package/dist/components/Checkbox/index.d.ts +2 -0
  49. package/dist/components/Checkbox/index.js +1 -0
  50. package/dist/components/Checkbox/types.d.ts +76 -0
  51. package/dist/components/Checkbox/types.js +1 -0
  52. package/dist/components/Chip/Chip.svelte +109 -0
  53. package/dist/components/Chip/Chip.svelte.d.ts +4 -0
  54. package/dist/components/Chip/chip.css +101 -0
  55. package/dist/components/Chip/index.d.ts +2 -0
  56. package/dist/components/Chip/index.js +1 -0
  57. package/dist/components/Chip/types.d.ts +69 -0
  58. package/dist/components/Chip/types.js +1 -0
  59. package/dist/components/Choice/Choice.svelte +60 -0
  60. package/dist/components/Choice/Choice.svelte.d.ts +4 -0
  61. package/dist/components/Choice/choice.css +75 -0
  62. package/dist/components/Choice/types.d.ts +43 -0
  63. package/dist/components/Choice/types.js +1 -0
  64. package/dist/components/Divider/Divider.svelte +19 -0
  65. package/dist/components/Divider/Divider.svelte.d.ts +4 -0
  66. package/dist/components/Divider/divider.css +23 -0
  67. package/dist/components/Divider/index.d.ts +2 -0
  68. package/dist/components/Divider/index.js +1 -0
  69. package/dist/components/Divider/types.d.ts +10 -0
  70. package/dist/components/Divider/types.js +1 -0
  71. package/dist/components/Field/Field.svelte +95 -0
  72. package/dist/components/Field/Field.svelte.d.ts +4 -0
  73. package/dist/components/Field/field.css +210 -0
  74. package/dist/components/Field/index.d.ts +3 -0
  75. package/dist/components/Field/index.js +2 -0
  76. package/dist/components/Field/types.d.ts +109 -0
  77. package/dist/components/Field/types.js +2 -0
  78. package/dist/components/Form/Form.svelte +93 -0
  79. package/dist/components/Form/Form.svelte.d.ts +8 -0
  80. package/dist/components/Form/form.css +16 -0
  81. package/dist/components/Form/index.d.ts +2 -0
  82. package/dist/components/Form/index.js +1 -0
  83. package/dist/components/Form/types.d.ts +38 -0
  84. package/dist/components/Form/types.js +1 -0
  85. package/dist/components/Icon/Icon.svelte +38 -0
  86. package/dist/components/Icon/Icon.svelte.d.ts +4 -0
  87. package/dist/components/Icon/icon.css +33 -0
  88. package/dist/components/Icon/index.d.ts +2 -0
  89. package/dist/components/Icon/index.js +1 -0
  90. package/dist/components/Icon/types.d.ts +34 -0
  91. package/dist/components/Icon/types.js +1 -0
  92. package/dist/components/List/List.svelte +39 -0
  93. package/dist/components/List/List.svelte.d.ts +4 -0
  94. package/dist/components/List/ListItem.svelte +57 -0
  95. package/dist/components/List/ListItem.svelte.d.ts +4 -0
  96. package/dist/components/List/index.d.ts +3 -0
  97. package/dist/components/List/index.js +2 -0
  98. package/dist/components/List/list.css +127 -0
  99. package/dist/components/List/types.d.ts +67 -0
  100. package/dist/components/List/types.js +1 -0
  101. package/dist/components/NativeSelect/NativeSelect.svelte +110 -0
  102. package/dist/components/NativeSelect/NativeSelect.svelte.d.ts +25 -0
  103. package/dist/components/NativeSelect/index.d.ts +2 -0
  104. package/dist/components/NativeSelect/index.js +1 -0
  105. package/dist/components/NativeSelect/native-select.css +45 -0
  106. package/dist/components/NativeSelect/types.d.ts +58 -0
  107. package/dist/components/NativeSelect/types.js +1 -0
  108. package/dist/components/NumberField/NumberField.svelte +246 -0
  109. package/dist/components/NumberField/NumberField.svelte.d.ts +4 -0
  110. package/dist/components/NumberField/index.d.ts +2 -0
  111. package/dist/components/NumberField/index.js +1 -0
  112. package/dist/components/NumberField/number-field.css +74 -0
  113. package/dist/components/NumberField/types.d.ts +73 -0
  114. package/dist/components/NumberField/types.js +1 -0
  115. package/dist/components/Progress/ProgressCircular.svelte +61 -0
  116. package/dist/components/Progress/ProgressCircular.svelte.d.ts +4 -0
  117. package/dist/components/Progress/ProgressLinear.svelte +44 -0
  118. package/dist/components/Progress/ProgressLinear.svelte.d.ts +4 -0
  119. package/dist/components/Progress/index.d.ts +3 -0
  120. package/dist/components/Progress/index.js +2 -0
  121. package/dist/components/Progress/progress-circular.css +61 -0
  122. package/dist/components/Progress/progress-linear.css +67 -0
  123. package/dist/components/Progress/types.d.ts +53 -0
  124. package/dist/components/Progress/types.js +1 -0
  125. package/dist/components/RadioGroup/RadioGroup.svelte +149 -0
  126. package/dist/components/RadioGroup/RadioGroup.svelte.d.ts +25 -0
  127. package/dist/components/RadioGroup/index.d.ts +2 -0
  128. package/dist/components/RadioGroup/index.js +1 -0
  129. package/dist/components/RadioGroup/radio-group.css +139 -0
  130. package/dist/components/RadioGroup/types.d.ts +93 -0
  131. package/dist/components/RadioGroup/types.js +1 -0
  132. package/dist/components/SegmentedControl/SegmentedControl.svelte +70 -0
  133. package/dist/components/SegmentedControl/SegmentedControl.svelte.d.ts +25 -0
  134. package/dist/components/SegmentedControl/index.d.ts +2 -0
  135. package/dist/components/SegmentedControl/index.js +1 -0
  136. package/dist/components/SegmentedControl/segmented-control.css +68 -0
  137. package/dist/components/SegmentedControl/types.d.ts +60 -0
  138. package/dist/components/SegmentedControl/types.js +1 -0
  139. package/dist/components/Skeleton/Skeleton.svelte +33 -0
  140. package/dist/components/Skeleton/Skeleton.svelte.d.ts +4 -0
  141. package/dist/components/Skeleton/index.d.ts +2 -0
  142. package/dist/components/Skeleton/index.js +1 -0
  143. package/dist/components/Skeleton/skeleton.css +70 -0
  144. package/dist/components/Skeleton/types.d.ts +25 -0
  145. package/dist/components/Skeleton/types.js +1 -0
  146. package/dist/components/Slider/Slider.svelte +153 -0
  147. package/dist/components/Slider/Slider.svelte.d.ts +4 -0
  148. package/dist/components/Slider/index.d.ts +2 -0
  149. package/dist/components/Slider/index.js +1 -0
  150. package/dist/components/Slider/slider.css +206 -0
  151. package/dist/components/Slider/types.d.ts +102 -0
  152. package/dist/components/Slider/types.js +1 -0
  153. package/dist/components/Switch/Switch.svelte +94 -0
  154. package/dist/components/Switch/Switch.svelte.d.ts +4 -0
  155. package/dist/components/Switch/index.d.ts +2 -0
  156. package/dist/components/Switch/index.js +1 -0
  157. package/dist/components/Switch/switch.css +118 -0
  158. package/dist/components/Switch/types.d.ts +67 -0
  159. package/dist/components/Switch/types.js +1 -0
  160. package/dist/components/Tabs/Tabs.svelte +93 -0
  161. package/dist/components/Tabs/Tabs.svelte.d.ts +25 -0
  162. package/dist/components/Tabs/index.d.ts +2 -0
  163. package/dist/components/Tabs/index.js +1 -0
  164. package/dist/components/Tabs/tabs.css +73 -0
  165. package/dist/components/Tabs/types.d.ts +59 -0
  166. package/dist/components/Tabs/types.js +1 -0
  167. package/dist/components/TextField/TextField.svelte +115 -0
  168. package/dist/components/TextField/TextField.svelte.d.ts +4 -0
  169. package/dist/components/TextField/index.d.ts +2 -0
  170. package/dist/components/TextField/index.js +1 -0
  171. package/dist/components/TextField/types.d.ts +57 -0
  172. package/dist/components/TextField/types.js +1 -0
  173. package/dist/components/Textarea/Textarea.svelte +116 -0
  174. package/dist/components/Textarea/Textarea.svelte.d.ts +4 -0
  175. package/dist/components/Textarea/index.d.ts +2 -0
  176. package/dist/components/Textarea/index.js +1 -0
  177. package/dist/components/Textarea/textarea.css +35 -0
  178. package/dist/components/Textarea/types.d.ts +60 -0
  179. package/dist/components/Textarea/types.js +1 -0
  180. package/dist/core/defaults/DefaultsProvider.svelte +30 -0
  181. package/dist/core/defaults/DefaultsProvider.svelte.d.ts +13 -0
  182. package/dist/core/defaults/context.d.ts +45 -0
  183. package/dist/core/defaults/context.js +38 -0
  184. package/dist/core/display/display.d.ts +39 -0
  185. package/dist/core/display/display.js +53 -0
  186. package/dist/core/errors/codes.d.ts +112 -0
  187. package/dist/core/errors/codes.js +96 -0
  188. package/dist/core/errors/errors.d.ts +8 -0
  189. package/dist/core/errors/errors.js +7 -0
  190. package/dist/core/form/constraints.d.ts +15 -0
  191. package/dist/core/form/constraints.js +42 -0
  192. package/dist/core/form/context.d.ts +19 -0
  193. package/dist/core/form/context.js +4 -0
  194. package/dist/core/form/field.svelte.d.ts +34 -0
  195. package/dist/core/form/field.svelte.js +66 -0
  196. package/dist/core/form/number.d.ts +24 -0
  197. package/dist/core/form/number.js +71 -0
  198. package/dist/core/form/types.d.ts +22 -0
  199. package/dist/core/form/types.js +1 -0
  200. package/dist/core/locale/locale.d.ts +55 -0
  201. package/dist/core/locale/locale.js +80 -0
  202. package/dist/core/props/classes.d.ts +21 -0
  203. package/dist/core/props/classes.js +44 -0
  204. package/dist/core/props/element.d.ts +18 -0
  205. package/dist/core/props/element.js +1 -0
  206. package/dist/core/props/types.d.ts +47 -0
  207. package/dist/core/props/types.js +16 -0
  208. package/dist/core/provider/UIProvider.svelte +60 -0
  209. package/dist/core/provider/UIProvider.svelte.d.ts +29 -0
  210. package/dist/core/theme/ThemeProvider.svelte +178 -0
  211. package/dist/core/theme/ThemeProvider.svelte.d.ts +25 -0
  212. package/dist/core/theme/color.d.ts +23 -0
  213. package/dist/core/theme/color.js +54 -0
  214. package/dist/core/theme/context.d.ts +25 -0
  215. package/dist/core/theme/context.js +14 -0
  216. package/dist/core/theme/css.d.ts +25 -0
  217. package/dist/core/theme/css.js +69 -0
  218. package/dist/core/theme/define.d.ts +11 -0
  219. package/dist/core/theme/define.js +21 -0
  220. package/dist/core/theme/resolve.d.ts +6 -0
  221. package/dist/core/theme/resolve.js +18 -0
  222. package/dist/core/theme/theme-provider.css +13 -0
  223. package/dist/core/theme/types.d.ts +35 -0
  224. package/dist/core/theme/types.js +61 -0
  225. package/dist/dev/off.d.ts +4 -0
  226. package/dist/dev/off.js +3 -0
  227. package/dist/dev/on.d.ts +8 -0
  228. package/dist/dev/on.js +33 -0
  229. package/dist/index.d.ts +58 -0
  230. package/dist/index.js +72 -0
  231. package/dist/primitives/anchored/anchored.d.ts +39 -0
  232. package/dist/primitives/anchored/anchored.js +104 -0
  233. package/dist/primitives/anchored/fallback.d.ts +31 -0
  234. package/dist/primitives/anchored/fallback.js +73 -0
  235. package/dist/primitives/focus/focus-trap.d.ts +25 -0
  236. package/dist/primitives/focus/focus-trap.js +86 -0
  237. package/dist/primitives/focus/roving-focus.d.ts +22 -0
  238. package/dist/primitives/focus/roving-focus.js +86 -0
  239. package/dist/primitives/focus/tabbable.d.ts +9 -0
  240. package/dist/primitives/focus/tabbable.js +75 -0
  241. package/dist/primitives/inert/inert-others.d.ts +12 -0
  242. package/dist/primitives/inert/inert-others.js +44 -0
  243. package/dist/primitives/keyboard/keymap.d.ts +17 -0
  244. package/dist/primitives/keyboard/keymap.js +39 -0
  245. package/dist/primitives/keyboard/typeahead.d.ts +18 -0
  246. package/dist/primitives/keyboard/typeahead.js +50 -0
  247. package/dist/primitives/layer/dismissable.d.ts +26 -0
  248. package/dist/primitives/layer/dismissable.js +84 -0
  249. package/dist/primitives/motion/easing.d.ts +9 -0
  250. package/dist/primitives/motion/easing.js +46 -0
  251. package/dist/primitives/motion/motion.d.ts +59 -0
  252. package/dist/primitives/motion/motion.js +80 -0
  253. package/dist/primitives/portal/Portal.svelte +37 -0
  254. package/dist/primitives/portal/Portal.svelte.d.ts +9 -0
  255. package/dist/primitives/portal/PortalContent.svelte +15 -0
  256. package/dist/primitives/portal/PortalContent.svelte.d.ts +9 -0
  257. package/dist/primitives/portal/portal.css +8 -0
  258. package/dist/primitives/scroll-lock/scroll-lock.d.ts +10 -0
  259. package/dist/primitives/scroll-lock/scroll-lock.js +33 -0
  260. package/dist/primitives/virtual/virtual-list.svelte.d.ts +65 -0
  261. package/dist/primitives/virtual/virtual-list.svelte.js +291 -0
  262. package/dist/styles/base.css +44 -0
  263. package/dist/styles/props.css +137 -0
  264. package/dist/styles/tokens.css +84 -0
  265. package/dist/styles/visually-hidden.css +16 -0
  266. package/dist/themes/aurora.d.ts +13 -0
  267. package/dist/themes/aurora.js +103 -0
  268. package/dist/themes/heritage.d.ts +13 -0
  269. package/dist/themes/heritage.js +102 -0
  270. package/dist/themes/maison.d.ts +13 -0
  271. package/dist/themes/maison.js +99 -0
  272. package/dist/themes/milano.d.ts +13 -0
  273. package/dist/themes/milano.js +101 -0
  274. package/dist/themes/monolith.d.ts +13 -0
  275. package/dist/themes/monolith.js +101 -0
  276. package/package.json +87 -5
@@ -0,0 +1,110 @@
1
+ <script lang="ts" generics="T extends string | number">
2
+ // One choice from a list, as the platform's own select in a Field. See NativeSelectProps.
3
+ import { untrack } from 'svelte';
4
+ import { DEV, warnUnnamed } from '#dev';
5
+ import { useDefaults } from '../../core/defaults/context.js';
6
+ import { createField } from '../../core/form/field.svelte.js';
7
+ import type { Rule } from '../../core/form/types.js';
8
+ import { useLocale } from '../../core/locale/locale.js';
9
+ import Field from '../Field/Field.svelte';
10
+ import './native-select.css';
11
+ import type { NativeSelectDefaults, NativeSelectProps } from './types.js';
12
+
13
+ let {
14
+ variant,
15
+ color,
16
+ size,
17
+ density,
18
+ rounded,
19
+ disabled,
20
+ options,
21
+ value = $bindable(null),
22
+ placeholder,
23
+ label,
24
+ hint,
25
+ error,
26
+ rules,
27
+ validateOn,
28
+ required = false,
29
+ prepend,
30
+ id,
31
+ 'aria-describedby': describedBy,
32
+ class: className,
33
+ style,
34
+ ref = $bindable(null),
35
+ ...rest
36
+ }: NativeSelectProps<T> = $props();
37
+
38
+ const resolve = useDefaults<NativeSelectDefaults>('NativeSelect', {
39
+ variant: 'outlined',
40
+ color: 'primary',
41
+ size: 'md',
42
+ density: 'default',
43
+ rounded: true,
44
+ disabled: false
45
+ });
46
+ const p = $derived(resolve({ variant, color, size, density, rounded, disabled }));
47
+ const locale = useLocale();
48
+
49
+ // The choice a form reset goes back to: the one the select started with.
50
+ const initial = untrack(() => value ?? null);
51
+
52
+ const field = createField<T | null>({
53
+ value: () => value ?? null,
54
+ empty: (choice) => choice === null,
55
+ check: (choice) => (choice === null && required ? locale.t('required') : undefined),
56
+ // Rules never see null: an empty choice skips them.
57
+ rules: () => rules as readonly Rule<T | null>[] | undefined,
58
+ error: () => error,
59
+ validateOn: () => validateOn,
60
+ control: () => ref
61
+ });
62
+
63
+ if (DEV) $effect(() => warnUnnamed('NativeSelect', ref));
64
+
65
+ // Leaving means focus went outside the field.
66
+ function leave(event: FocusEvent) {
67
+ const root = event.currentTarget as HTMLElement;
68
+ if (!root.contains(event.relatedTarget as Node | null)) field.left();
69
+ }
70
+ </script>
71
+
72
+ <Field
73
+ {...p}
74
+ {id}
75
+ {label}
76
+ {hint}
77
+ error={field.error}
78
+ {required}
79
+ {prepend}
80
+ class={[
81
+ 'sv-native-select',
82
+ placeholder !== undefined && value == null && 'sv-native-select--empty',
83
+ className
84
+ ]}
85
+ {style}
86
+ oninput={field.changed}
87
+ onfocusout={leave}
88
+ >
89
+ {#snippet children(attributes)}
90
+ <select
91
+ bind:this={ref}
92
+ {...rest}
93
+ {...attributes}
94
+ aria-describedby={[describedBy, attributes['aria-describedby']].filter(Boolean).join(' ') ||
95
+ undefined}
96
+ bind:value={() => value ?? '', (next: T | '') => (value = next === '' ? null : next)}
97
+ >
98
+ {#if placeholder !== undefined}
99
+ <option value="" disabled={required} selected={initial === null}>{placeholder}</option>
100
+ {/if}
101
+ {#each options as option (option.value)}
102
+ <option
103
+ value={option.value}
104
+ disabled={option.disabled}
105
+ selected={Object.is(option.value, initial)}>{option.label}</option
106
+ >
107
+ {/each}
108
+ </select>
109
+ {/snippet}
110
+ </Field>
@@ -0,0 +1,25 @@
1
+ import type { NativeSelectProps } from './types.js';
2
+ declare function $$render<T extends string | number>(): {
3
+ props: NativeSelectProps<T>;
4
+ exports: {};
5
+ bindings: "ref" | "value";
6
+ slots: {};
7
+ events: {};
8
+ };
9
+ declare class __sveltets_Render<T extends string | number> {
10
+ props(): ReturnType<typeof $$render<T>>['props'];
11
+ events(): ReturnType<typeof $$render<T>>['events'];
12
+ slots(): ReturnType<typeof $$render<T>>['slots'];
13
+ bindings(): "ref" | "value";
14
+ exports(): {};
15
+ }
16
+ interface $$IsomorphicComponent {
17
+ new <T extends string | number>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
18
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
19
+ } & ReturnType<__sveltets_Render<T>['exports']>;
20
+ <T extends string | number>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
21
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
22
+ }
23
+ declare const NativeSelect: $$IsomorphicComponent;
24
+ type NativeSelect<T extends string | number> = InstanceType<typeof NativeSelect<T>>;
25
+ export default NativeSelect;
@@ -0,0 +1,2 @@
1
+ export { default as NativeSelect } from './NativeSelect.svelte';
2
+ export type { NativeSelectDefaults, NativeSelectOption, NativeSelectProps } from './types.js';
@@ -0,0 +1 @@
1
+ export { default as NativeSelect } from './NativeSelect.svelte';
@@ -0,0 +1,45 @@
1
+ @layer sv.tokens, sv.base, sv.components, sv.utilities;
2
+
3
+ @layer sv.components {
4
+ .sv-native-select > .sv-field__box {
5
+ cursor: pointer;
6
+ }
7
+
8
+ /* Room at the end for the arrow, which the text never runs under. */
9
+ .sv-native-select .sv-field__control {
10
+ padding-inline-end: calc(1.272em + var(--sv-space-8));
11
+ text-overflow: ellipsis;
12
+ cursor: inherit;
13
+ }
14
+
15
+ /* The placeholder reads as one, as in a text field; the list keeps the text colour. */
16
+ .sv-native-select--empty .sv-field__control {
17
+ color: var(--sv-color-text-subtle);
18
+ }
19
+
20
+ .sv-native-select .sv-field__control option {
21
+ color: var(--sv-color-text);
22
+ }
23
+
24
+ /*
25
+ * The arrow: a mask filled with the muted text colour. It lets presses through to the select,
26
+ * so the whole box opens the list.
27
+ */
28
+ .sv-native-select > .sv-field__box::before {
29
+ content: '';
30
+ position: absolute;
31
+ inset-block: 0;
32
+ inset-inline-end: calc(var(--sv-size-height) * var(--sv-density) * 0.382);
33
+ inline-size: 1.272em;
34
+ block-size: 1.272em;
35
+ margin-block: auto;
36
+ background-color: var(--sv-color-text-muted);
37
+ mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.75' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M6 9l6 6 6-6'/%3E%3C/svg%3E")
38
+ center / contain no-repeat;
39
+ pointer-events: none;
40
+ }
41
+
42
+ .sv-native-select:where(.sv-variant-text, .sv-variant-plain) > .sv-field__box::before {
43
+ inset-inline-end: 0;
44
+ }
45
+ }
@@ -0,0 +1,58 @@
1
+ import type { HTMLSelectAttributes } from 'svelte/elements';
2
+ import type { Rule, ValidateOn } from '../../core/form/types.js';
3
+ import type { ElementProps } from '../../core/props/element.js';
4
+ import type { FieldContentProps, FieldDefaults } from '../Field/types.js';
5
+ /** The NativeSelect props that `DefaultsProvider` and `UIProvider` can set for a subtree. */
6
+ export type NativeSelectDefaults = FieldDefaults;
7
+ declare module '../../core/defaults/context.js' {
8
+ interface ComponentDefaults {
9
+ NativeSelect: NativeSelectDefaults;
10
+ }
11
+ }
12
+ /** One choice of a NativeSelect. */
13
+ export interface NativeSelectOption<T extends string | number = string> {
14
+ /** The value, never the empty string, which stands for no choice. */
15
+ value: T;
16
+ /** What the option says. */
17
+ label: string;
18
+ disabled?: boolean;
19
+ }
20
+ /**
21
+ * One choice from a list, as the platform's own `<select>` in a Field: the phone's picker and the
22
+ * browser's keyboard behaviour come for free. `class` and `style` apply to the field; `id`, `ref`,
23
+ * `name` and other attributes go to the `<select>`. The end of the box holds its arrow, so there
24
+ * is no `append`.
25
+ *
26
+ * @example
27
+ * <NativeSelect
28
+ * label="Country"
29
+ * name="country"
30
+ * placeholder="Choose a country"
31
+ * required
32
+ * options={[
33
+ * { value: 'gb', label: 'United Kingdom' },
34
+ * { value: 'vn', label: 'Vietnam' }
35
+ * ]}
36
+ * bind:value={country}
37
+ * />
38
+ */
39
+ export type NativeSelectProps<T extends string | number = string> = ElementProps<HTMLSelectElement, HTMLSelectAttributes, Partial<NativeSelectDefaults> & Omit<FieldContentProps, 'append'> & {
40
+ /** The choices, in order. */
41
+ options: readonly NativeSelectOption<T>[];
42
+ /** The chosen value, or `null` for none. Bindable. */
43
+ value?: T | null;
44
+ /** A first option that stands for no choice, such as "Choose a country". */
45
+ placeholder?: string;
46
+ /** Checks run in order after `required`, with the chosen value. The first message is shown. */
47
+ rules?: readonly Rule<T>[];
48
+ /** When the message shows. Defaults to the form's setting, else `'blur'`. */
49
+ validateOn?: ValidateOn;
50
+ /**
51
+ * A choice must be made. Adds an asterisk to the label, and the placeholder cannot be chosen
52
+ * again once the user has picked something.
53
+ * @default false
54
+ */
55
+ required?: boolean;
56
+ /** Inline style of the field's root element. */
57
+ style?: string | null;
58
+ }>;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,246 @@
1
+ <script lang="ts">
2
+ // A number in a Field: typed in the locale's format, stepped with the keyboard or buttons, and
3
+ // validated. See NumberFieldProps for the props.
4
+ import { untrack } from 'svelte';
5
+ import { checkValue, DEV, warnUnnamed } from '#dev';
6
+ import { useDefaults } from '../../core/defaults/context.js';
7
+ import { createField } from '../../core/form/field.svelte.js';
8
+ import { numberConstraintMessage, parseNumber } from '../../core/form/number.js';
9
+ import type { Rule } from '../../core/form/types.js';
10
+ import { useLocale } from '../../core/locale/locale.js';
11
+ import Field from '../Field/Field.svelte';
12
+ import './number-field.css';
13
+ import type { NumberFieldDefaults, NumberFieldProps } from './types.js';
14
+
15
+ let {
16
+ variant,
17
+ color,
18
+ size,
19
+ density,
20
+ rounded,
21
+ disabled,
22
+ value = $bindable(null),
23
+ min,
24
+ max,
25
+ step,
26
+ format,
27
+ controls = true,
28
+ label,
29
+ hint,
30
+ error,
31
+ rules,
32
+ validateOn,
33
+ required = false,
34
+ readonly = false,
35
+ name,
36
+ prepend,
37
+ append,
38
+ id,
39
+ 'aria-describedby': describedBy,
40
+ onkeydown,
41
+ class: className,
42
+ style,
43
+ ref = $bindable(null),
44
+ ...rest
45
+ }: NumberFieldProps = $props();
46
+
47
+ const resolve = useDefaults<NumberFieldDefaults>('NumberField', {
48
+ variant: 'outlined',
49
+ color: 'primary',
50
+ size: 'md',
51
+ density: 'default',
52
+ rounded: true,
53
+ disabled: false
54
+ });
55
+ const p = $derived(resolve({ variant, color, size, density, rounded, disabled }));
56
+ const locale = useLocale();
57
+ const uid = $props.id();
58
+ const inputId = $derived(id ?? uid);
59
+
60
+ /** The number written in the locale; empty for no number. */
61
+ const show = (number: number | null) =>
62
+ number === null || !Number.isFinite(number)
63
+ ? ''
64
+ : locale.formatNumber(number, format ?? { maximumFractionDigits: 20 });
65
+
66
+ // What the input shows, and what it means: a number, null for empty, NaN for not a number. A
67
+ // form reset goes back to the text the field started with.
68
+ const initial = untrack(() => show(value ?? null));
69
+ let text = $state(initial);
70
+ let parsed = $state<number | null>(untrack(() => value ?? null));
71
+
72
+ // A value set from outside, not typed, is written in the locale format.
73
+ $effect(() => {
74
+ const next = value ?? null;
75
+ if (
76
+ !Object.is(
77
+ next,
78
+ untrack(() => parsed)
79
+ )
80
+ ) {
81
+ parsed = next;
82
+ text = show(next);
83
+ }
84
+ });
85
+
86
+ const field = createField<number | null>({
87
+ value: () => parsed,
88
+ empty: (number) => number === null,
89
+ check: (number) => numberConstraintMessage(number, { required, min, max, step }, locale),
90
+ // Rules never see an empty or unreadable value: the checks above stop those first.
91
+ rules: () => rules as readonly Rule<number | null>[] | undefined,
92
+ error: () => error,
93
+ validateOn: () => validateOn,
94
+ control: () => ref
95
+ });
96
+
97
+ if (DEV) {
98
+ $effect(() => {
99
+ warnUnnamed('NumberField', ref);
100
+ // A currency or percent sign in the field would make the text unreadable as a number.
101
+ checkValue(
102
+ 'NumberField',
103
+ 'format.style',
104
+ format?.style,
105
+ ['decimal'],
106
+ 'Put a currency or a unit in prepend or append.'
107
+ );
108
+ });
109
+ }
110
+
111
+ const places = (number: number | undefined) => (String(number ?? '').split('.')[1] ?? '').length;
112
+ // The keys and buttons move by step, or by 1 when no step is set (and then any number is
113
+ // valid). Enough decimals for the step and its starting point, so 0.1 + 0.2 lands on 0.3.
114
+ const increment = $derived(step ?? 1);
115
+ const decimals = $derived(Math.max(places(increment), places(min)));
116
+ const inert = $derived(p.disabled || readonly);
117
+ const current = $derived(parsed !== null && Number.isFinite(parsed) ? parsed : null);
118
+
119
+ // Called for typing and for a form reset alike, so the value always follows the text.
120
+ function read(next: string) {
121
+ text = next;
122
+ const number = parseNumber(next, locale.locale);
123
+ parsed = number;
124
+ if (number === null || Number.isFinite(number)) value = number;
125
+ }
126
+
127
+ /** Sets a number the keys or buttons chose, within min and max. */
128
+ function commit(number: number) {
129
+ const bounded = Math.min(max ?? Infinity, Math.max(min ?? -Infinity, number));
130
+ const next = Number(bounded.toFixed(decimals));
131
+ parsed = next;
132
+ value = next;
133
+ text = show(next);
134
+ field.changed();
135
+ }
136
+
137
+ // Moves along the steps counted from min, as a native number input does: from a value between
138
+ // two steps, up goes to the next one and down to the previous one. Empty starts at min, or 0.
139
+ function stepBy(steps: number) {
140
+ if (inert) return;
141
+ const start = min ?? 0;
142
+ if (current === null) return commit(start);
143
+ const offset = (current - start) / increment;
144
+ const from = steps > 0 ? Math.floor(offset + 1e-9) : Math.ceil(offset - 1e-9);
145
+ commit(start + (from + steps) * increment);
146
+ }
147
+
148
+ function keydown(event: KeyboardEvent & { currentTarget: EventTarget & HTMLInputElement }) {
149
+ (onkeydown as ((event: KeyboardEvent) => void) | null | undefined)?.(event);
150
+ if (event.defaultPrevented || inert) return;
151
+ const moves: Record<string, (() => void) | undefined> = {
152
+ ArrowUp: () => stepBy(1),
153
+ ArrowDown: () => stepBy(-1),
154
+ PageUp: () => stepBy(10),
155
+ PageDown: () => stepBy(-10),
156
+ Home: min == null ? undefined : () => commit(min),
157
+ End: max == null ? undefined : () => commit(max)
158
+ };
159
+ const move = moves[event.key];
160
+ if (!move) return;
161
+ event.preventDefault();
162
+ move();
163
+ }
164
+
165
+ // Leaving means focus went outside the field, not to a button in it. The number is then
166
+ // written in the locale format.
167
+ function leave(event: FocusEvent) {
168
+ const root = event.currentTarget as HTMLElement;
169
+ if (root.contains(event.relatedTarget as Node | null)) return;
170
+ if (current !== null) text = show(current);
171
+ field.left();
172
+ }
173
+
174
+ // Only whole numbers, and no minus sign, can use the numeric keypad.
175
+ const integer = $derived(step != null && decimals === 0);
176
+ </script>
177
+
178
+ {#snippet end()}
179
+ {@render append?.()}
180
+ {#if controls}
181
+ <span class="sv-number-field__steps">
182
+ <button
183
+ type="button"
184
+ class="sv-number-field__step"
185
+ tabindex="-1"
186
+ aria-label={locale.t('decrease')}
187
+ aria-controls={inputId}
188
+ disabled={inert || (min != null && current !== null && current <= min)}
189
+ onpointerdown={(event) => event.preventDefault()}
190
+ onclick={() => stepBy(-1)}
191
+ >
192
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14" /></svg>
193
+ </button>
194
+ <button
195
+ type="button"
196
+ class="sv-number-field__step"
197
+ tabindex="-1"
198
+ aria-label={locale.t('increase')}
199
+ aria-controls={inputId}
200
+ disabled={inert || (max != null && current !== null && current >= max)}
201
+ onpointerdown={(event) => event.preventDefault()}
202
+ onclick={() => stepBy(1)}
203
+ >
204
+ <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 5v14M5 12h14" /></svg>
205
+ </button>
206
+ </span>
207
+ {/if}
208
+ {/snippet}
209
+
210
+ <Field
211
+ {...p}
212
+ id={inputId}
213
+ {label}
214
+ {hint}
215
+ error={field.error}
216
+ {required}
217
+ {readonly}
218
+ {prepend}
219
+ append={controls || append ? end : undefined}
220
+ class={['sv-number-field', className]}
221
+ {style}
222
+ oninput={field.changed}
223
+ onfocusout={leave}
224
+ >
225
+ {#snippet children(attributes)}
226
+ <input
227
+ bind:this={ref}
228
+ defaultValue={initial}
229
+ {...rest}
230
+ {...attributes}
231
+ aria-describedby={[describedBy, attributes['aria-describedby']].filter(Boolean).join(' ') ||
232
+ undefined}
233
+ type="text"
234
+ role="spinbutton"
235
+ inputmode={min != null && min >= 0 && integer ? 'numeric' : 'decimal'}
236
+ autocomplete="off"
237
+ aria-valuenow={current ?? undefined}
238
+ aria-valuemin={min}
239
+ aria-valuemax={max}
240
+ aria-valuetext={current === null ? undefined : text}
241
+ bind:value={() => text, read}
242
+ onkeydown={keydown}
243
+ />
244
+ {#if name}<input type="hidden" {name} value={current ?? ''} />{/if}
245
+ {/snippet}
246
+ </Field>
@@ -0,0 +1,4 @@
1
+ import type { NumberFieldProps } from './types.js';
2
+ declare const NumberField: import("svelte").Component<NumberFieldProps, {}, "ref" | "value">;
3
+ type NumberField = ReturnType<typeof NumberField>;
4
+ export default NumberField;
@@ -0,0 +1,2 @@
1
+ export { default as NumberField } from './NumberField.svelte';
2
+ export type { NumberFieldDefaults, NumberFieldProps } from './types.js';
@@ -0,0 +1 @@
1
+ export { default as NumberField } from './NumberField.svelte';
@@ -0,0 +1,74 @@
1
+ @layer sv.tokens, sv.base, sv.components, sv.utilities;
2
+
3
+ @layer sv.components {
4
+ /* Digits keep one width, so numbers that change or stack line up. */
5
+ .sv-number-field .sv-field__control {
6
+ font-variant-numeric: tabular-nums;
7
+ }
8
+
9
+ .sv-number-field .sv-field__append {
10
+ align-self: stretch;
11
+ }
12
+
13
+ .sv-number-field__steps {
14
+ display: flex;
15
+ align-items: center;
16
+ gap: var(--sv-space-2);
17
+ margin-inline-end: calc(var(--sv-size-height) * var(--sv-density) * -0.146);
18
+ }
19
+
20
+ /* Square, φ⁻¹ of the field's height, and never under 24px (WCAG 2.5.8). */
21
+ .sv-number-field__step {
22
+ --sv-step: max(1.5rem, calc(var(--sv-size-height) * var(--sv-density) * 0.618));
23
+ position: relative;
24
+ display: grid;
25
+ place-items: center;
26
+ inline-size: var(--sv-step);
27
+ block-size: var(--sv-step);
28
+ padding: 0;
29
+ border: 0;
30
+ border-radius: var(--sv-radius-sm);
31
+ background: none;
32
+ color: var(--sv-color-text-muted);
33
+ cursor: pointer;
34
+ }
35
+
36
+ /* The state layer, as on a Button: only its opacity changes. */
37
+ .sv-number-field__step::before {
38
+ content: '';
39
+ position: absolute;
40
+ inset: 0;
41
+ border-radius: inherit;
42
+ background-color: currentColor;
43
+ opacity: 0;
44
+ transition: opacity var(--sv-duration-fast) var(--sv-ease);
45
+ }
46
+
47
+ @media (hover: hover) {
48
+ .sv-number-field__step:enabled:hover {
49
+ color: var(--sv-color-text);
50
+ }
51
+
52
+ .sv-number-field__step:enabled:hover::before {
53
+ opacity: var(--sv-state-hover);
54
+ }
55
+ }
56
+
57
+ .sv-number-field__step:enabled:active::before {
58
+ opacity: var(--sv-state-press);
59
+ }
60
+
61
+ .sv-number-field__step:disabled {
62
+ opacity: 0.382;
63
+ cursor: not-allowed;
64
+ }
65
+
66
+ .sv-number-field__step svg {
67
+ inline-size: 1.272em;
68
+ block-size: 1.272em;
69
+ fill: none;
70
+ stroke: currentColor;
71
+ stroke-width: 1.75;
72
+ stroke-linecap: round;
73
+ }
74
+ }
@@ -0,0 +1,73 @@
1
+ import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import type { Rule, ValidateOn } from '../../core/form/types.js';
3
+ import type { ElementProps } from '../../core/props/element.js';
4
+ import type { FieldContentProps, FieldDefaults } from '../Field/types.js';
5
+ /** The NumberField props that `DefaultsProvider` and `UIProvider` can set for a subtree. */
6
+ export type NumberFieldDefaults = FieldDefaults;
7
+ declare module '../../core/defaults/context.js' {
8
+ interface ComponentDefaults {
9
+ NumberField: NumberFieldDefaults;
10
+ }
11
+ }
12
+ type NumberFieldOwnProps = Partial<NumberFieldDefaults> & FieldContentProps & {
13
+ /** The number, or `null` when the field is empty. Bindable. */
14
+ value?: number | null;
15
+ /** The smallest value. The keys and buttons stop here; a smaller typed value is an error. */
16
+ min?: number;
17
+ /** The largest value. The keys and buttons stop here; a larger typed value is an error. */
18
+ max?: number;
19
+ /**
20
+ * How far the arrow keys and the buttons move; Page Up and Page Down move ten steps. When
21
+ * set, a typed value off the steps, counted from `min`, is an error. When not set, they move
22
+ * by 1 and any number is accepted, decimals included.
23
+ */
24
+ step?: number;
25
+ /**
26
+ * How the number is written once the user leaves the field, as `Intl.NumberFormat` options
27
+ * in the app's locale: digits only, such as `{ minimumFractionDigits: 2 }`, so the user can
28
+ * edit what they see. Put a currency or a unit in `prepend` or `append`.
29
+ * @default { maximumFractionDigits: 20 }
30
+ */
31
+ format?: Intl.NumberFormatOptions;
32
+ /**
33
+ * Shows decrease and increase buttons at the end of the field.
34
+ * @default true
35
+ */
36
+ controls?: boolean;
37
+ /** Checks run in order after the HTML constraints, with the number. The first message is shown. */
38
+ rules?: readonly Rule<number>[];
39
+ /** When the message shows. Defaults to the form's setting, else `'blur'`. */
40
+ validateOn?: ValidateOn;
41
+ /**
42
+ * The field must not be empty. Adds an asterisk to the label.
43
+ * @default false
44
+ */
45
+ required?: boolean;
46
+ /**
47
+ * The number can be read and copied but not changed.
48
+ * @default false
49
+ */
50
+ readonly?: boolean;
51
+ /** Submits the plain number, such as `1234.5`, under this name in a form. */
52
+ name?: string;
53
+ /** Inline style of the field's root element. */
54
+ style?: string | null;
55
+ };
56
+ /**
57
+ * A number typed in the app's locale, stepped with the arrow keys or the buttons, and checked
58
+ * against `min`, `max` and `step`. The field shows the number formatted, for example
59
+ * `1,234.50`, and binds and submits the plain number. `class` and `style` apply to the field;
60
+ * `id`, `ref` and other attributes go to the `<input>`.
61
+ *
62
+ * @example
63
+ * <NumberField
64
+ * label="Quantity"
65
+ * name="quantity"
66
+ * min={1}
67
+ * max={99}
68
+ * required
69
+ * bind:value={quantity}
70
+ * />
71
+ */
72
+ export type NumberFieldProps = ElementProps<HTMLInputElement, HTMLInputAttributes, NumberFieldOwnProps>;
73
+ export {};
@@ -0,0 +1 @@
1
+ export {};