@delightstack/components 0.1.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 (195) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +136 -0
  3. package/SKILL.md +149 -0
  4. package/bin/agents.js +63 -0
  5. package/dist/actions/Alert.svelte +202 -0
  6. package/dist/actions/Alert.svelte.d.ts +36 -0
  7. package/dist/actions/Alert.svelte.d.ts.map +1 -0
  8. package/dist/actions/Button.svelte +1450 -0
  9. package/dist/actions/Button.svelte.d.ts +56 -0
  10. package/dist/actions/Button.svelte.d.ts.map +1 -0
  11. package/dist/actions/ButtonGroup.svelte +111 -0
  12. package/dist/actions/ButtonGroup.svelte.d.ts +41 -0
  13. package/dist/actions/ButtonGroup.svelte.d.ts.map +1 -0
  14. package/dist/actions/CommandPalette.svelte +939 -0
  15. package/dist/actions/CommandPalette.svelte.d.ts +37 -0
  16. package/dist/actions/CommandPalette.svelte.d.ts.map +1 -0
  17. package/dist/actions/ContextMenu.svelte +138 -0
  18. package/dist/actions/ContextMenu.svelte.d.ts +54 -0
  19. package/dist/actions/ContextMenu.svelte.d.ts.map +1 -0
  20. package/dist/actions/Modal.svelte +474 -0
  21. package/dist/actions/Modal.svelte.d.ts +28 -0
  22. package/dist/actions/Modal.svelte.d.ts.map +1 -0
  23. package/dist/actions/Popover.svelte +1214 -0
  24. package/dist/actions/Popover.svelte.d.ts +31 -0
  25. package/dist/actions/Popover.svelte.d.ts.map +1 -0
  26. package/dist/actions/Portal.svelte +80 -0
  27. package/dist/actions/Portal.svelte.d.ts +17 -0
  28. package/dist/actions/Portal.svelte.d.ts.map +1 -0
  29. package/dist/actions/ThemeToggle.svelte +345 -0
  30. package/dist/actions/ThemeToggle.svelte.d.ts +15 -0
  31. package/dist/actions/ThemeToggle.svelte.d.ts.map +1 -0
  32. package/dist/actions/index.d.ts +13 -0
  33. package/dist/actions/index.d.ts.map +1 -0
  34. package/dist/actions/index.js +10 -0
  35. package/dist/actions/scrollbar.d.ts +48 -0
  36. package/dist/actions/scrollbar.d.ts.map +1 -0
  37. package/dist/actions/scrollbar.js +404 -0
  38. package/dist/display/Accordion.svelte +586 -0
  39. package/dist/display/Accordion.svelte.d.ts +41 -0
  40. package/dist/display/Accordion.svelte.d.ts.map +1 -0
  41. package/dist/display/Avatar.svelte +527 -0
  42. package/dist/display/Avatar.svelte.d.ts +22 -0
  43. package/dist/display/Avatar.svelte.d.ts.map +1 -0
  44. package/dist/display/AvatarGroup.svelte +298 -0
  45. package/dist/display/AvatarGroup.svelte.d.ts +31 -0
  46. package/dist/display/AvatarGroup.svelte.d.ts.map +1 -0
  47. package/dist/display/Calendar.svelte +1366 -0
  48. package/dist/display/Calendar.svelte.d.ts +58 -0
  49. package/dist/display/Calendar.svelte.d.ts.map +1 -0
  50. package/dist/display/Chart.svelte +1426 -0
  51. package/dist/display/Chart.svelte.d.ts +35 -0
  52. package/dist/display/Chart.svelte.d.ts.map +1 -0
  53. package/dist/display/Code.svelte +780 -0
  54. package/dist/display/Code.svelte.d.ts +19 -0
  55. package/dist/display/Code.svelte.d.ts.map +1 -0
  56. package/dist/display/Comparison.svelte +686 -0
  57. package/dist/display/Comparison.svelte.d.ts +22 -0
  58. package/dist/display/Comparison.svelte.d.ts.map +1 -0
  59. package/dist/display/Counter.svelte +285 -0
  60. package/dist/display/Counter.svelte.d.ts +21 -0
  61. package/dist/display/Counter.svelte.d.ts.map +1 -0
  62. package/dist/display/Expand.svelte +48 -0
  63. package/dist/display/Expand.svelte.d.ts +9 -0
  64. package/dist/display/Expand.svelte.d.ts.map +1 -0
  65. package/dist/display/List.svelte +294 -0
  66. package/dist/display/List.svelte.d.ts +40 -0
  67. package/dist/display/List.svelte.d.ts.map +1 -0
  68. package/dist/display/ListContextReset.svelte +19 -0
  69. package/dist/display/ListContextReset.svelte.d.ts +7 -0
  70. package/dist/display/ListContextReset.svelte.d.ts.map +1 -0
  71. package/dist/display/ListItem.svelte +834 -0
  72. package/dist/display/ListItem.svelte.d.ts +22 -0
  73. package/dist/display/ListItem.svelte.d.ts.map +1 -0
  74. package/dist/display/QR.svelte +1193 -0
  75. package/dist/display/QR.svelte.d.ts +23 -0
  76. package/dist/display/QR.svelte.d.ts.map +1 -0
  77. package/dist/display/SplitPane.svelte +744 -0
  78. package/dist/display/SplitPane.svelte.d.ts +25 -0
  79. package/dist/display/SplitPane.svelte.d.ts.map +1 -0
  80. package/dist/display/Stat.svelte +439 -0
  81. package/dist/display/Stat.svelte.d.ts +24 -0
  82. package/dist/display/Stat.svelte.d.ts.map +1 -0
  83. package/dist/display/Table.svelte +4654 -0
  84. package/dist/display/Table.svelte.d.ts +249 -0
  85. package/dist/display/Table.svelte.d.ts.map +1 -0
  86. package/dist/display/TableCellEditor.svelte +935 -0
  87. package/dist/display/TableCellEditor.svelte.d.ts +58 -0
  88. package/dist/display/TableCellEditor.svelte.d.ts.map +1 -0
  89. package/dist/display/Timeline.svelte +1258 -0
  90. package/dist/display/Timeline.svelte.d.ts +43 -0
  91. package/dist/display/Timeline.svelte.d.ts.map +1 -0
  92. package/dist/display/Tree.svelte +1740 -0
  93. package/dist/display/Tree.svelte.d.ts +74 -0
  94. package/dist/display/Tree.svelte.d.ts.map +1 -0
  95. package/dist/display/Typewriter.svelte +338 -0
  96. package/dist/display/Typewriter.svelte.d.ts +22 -0
  97. package/dist/display/Typewriter.svelte.d.ts.map +1 -0
  98. package/dist/display/index.d.ts +24 -0
  99. package/dist/display/index.d.ts.map +1 -0
  100. package/dist/display/index.js +18 -0
  101. package/dist/feedback/Callout.svelte +529 -0
  102. package/dist/feedback/Callout.svelte.d.ts +24 -0
  103. package/dist/feedback/Callout.svelte.d.ts.map +1 -0
  104. package/dist/feedback/Confetti.svelte +631 -0
  105. package/dist/feedback/Confetti.svelte.d.ts +90 -0
  106. package/dist/feedback/Confetti.svelte.d.ts.map +1 -0
  107. package/dist/feedback/Progress.svelte +382 -0
  108. package/dist/feedback/Progress.svelte.d.ts +25 -0
  109. package/dist/feedback/Progress.svelte.d.ts.map +1 -0
  110. package/dist/feedback/Toast.svelte +967 -0
  111. package/dist/feedback/Toast.svelte.d.ts +54 -0
  112. package/dist/feedback/Toast.svelte.d.ts.map +1 -0
  113. package/dist/feedback/index.d.ts +7 -0
  114. package/dist/feedback/index.d.ts.map +1 -0
  115. package/dist/feedback/index.js +4 -0
  116. package/dist/form/Checkbox.svelte +449 -0
  117. package/dist/form/Checkbox.svelte.d.ts +27 -0
  118. package/dist/form/Checkbox.svelte.d.ts.map +1 -0
  119. package/dist/form/Fieldset.svelte +410 -0
  120. package/dist/form/Fieldset.svelte.d.ts +22 -0
  121. package/dist/form/Fieldset.svelte.d.ts.map +1 -0
  122. package/dist/form/FileUpload.svelte +934 -0
  123. package/dist/form/FileUpload.svelte.d.ts +41 -0
  124. package/dist/form/FileUpload.svelte.d.ts.map +1 -0
  125. package/dist/form/Form.svelte +530 -0
  126. package/dist/form/Form.svelte.d.ts +120 -0
  127. package/dist/form/Form.svelte.d.ts.map +1 -0
  128. package/dist/form/Input.svelte +2858 -0
  129. package/dist/form/Input.svelte.d.ts +66 -0
  130. package/dist/form/Input.svelte.d.ts.map +1 -0
  131. package/dist/form/Radio.svelte +507 -0
  132. package/dist/form/Radio.svelte.d.ts +39 -0
  133. package/dist/form/Radio.svelte.d.ts.map +1 -0
  134. package/dist/form/Range.svelte +912 -0
  135. package/dist/form/Range.svelte.d.ts +33 -0
  136. package/dist/form/Range.svelte.d.ts.map +1 -0
  137. package/dist/form/Rating.svelte +429 -0
  138. package/dist/form/Rating.svelte.d.ts +28 -0
  139. package/dist/form/Rating.svelte.d.ts.map +1 -0
  140. package/dist/form/Select.svelte +1933 -0
  141. package/dist/form/Select.svelte.d.ts +54 -0
  142. package/dist/form/Select.svelte.d.ts.map +1 -0
  143. package/dist/form/Toggle.svelte +645 -0
  144. package/dist/form/Toggle.svelte.d.ts +50 -0
  145. package/dist/form/Toggle.svelte.d.ts.map +1 -0
  146. package/dist/form/index.d.ts +15 -0
  147. package/dist/form/index.d.ts.map +1 -0
  148. package/dist/form/index.js +10 -0
  149. package/dist/index.d.ts +7 -0
  150. package/dist/index.d.ts.map +1 -0
  151. package/dist/index.js +6 -0
  152. package/dist/layout/README.md +172 -0
  153. package/dist/media/Carousel.svelte +2424 -0
  154. package/dist/media/Carousel.svelte.d.ts +47 -0
  155. package/dist/media/Carousel.svelte.d.ts.map +1 -0
  156. package/dist/media/Gallery.svelte +2881 -0
  157. package/dist/media/Gallery.svelte.d.ts +82 -0
  158. package/dist/media/Gallery.svelte.d.ts.map +1 -0
  159. package/dist/media/Image.svelte +389 -0
  160. package/dist/media/Image.svelte.d.ts +33 -0
  161. package/dist/media/Image.svelte.d.ts.map +1 -0
  162. package/dist/media/PDF.svelte +1793 -0
  163. package/dist/media/PDF.svelte.d.ts +44 -0
  164. package/dist/media/PDF.svelte.d.ts.map +1 -0
  165. package/dist/media/Panorama.svelte +1391 -0
  166. package/dist/media/Panorama.svelte.d.ts +47 -0
  167. package/dist/media/Panorama.svelte.d.ts.map +1 -0
  168. package/dist/media/Video.svelte +2501 -0
  169. package/dist/media/Video.svelte.d.ts +58 -0
  170. package/dist/media/Video.svelte.d.ts.map +1 -0
  171. package/dist/media/carousel.d.ts +211 -0
  172. package/dist/media/carousel.d.ts.map +1 -0
  173. package/dist/media/carousel.js +408 -0
  174. package/dist/media/index.d.ts +11 -0
  175. package/dist/media/index.d.ts.map +1 -0
  176. package/dist/media/index.js +5 -0
  177. package/dist/navigation/BottomSheet.svelte +636 -0
  178. package/dist/navigation/BottomSheet.svelte.d.ts +27 -0
  179. package/dist/navigation/BottomSheet.svelte.d.ts.map +1 -0
  180. package/dist/navigation/Breadcrumbs.svelte +611 -0
  181. package/dist/navigation/Breadcrumbs.svelte.d.ts +28 -0
  182. package/dist/navigation/Breadcrumbs.svelte.d.ts.map +1 -0
  183. package/dist/navigation/Pagination.svelte +641 -0
  184. package/dist/navigation/Pagination.svelte.d.ts +27 -0
  185. package/dist/navigation/Pagination.svelte.d.ts.map +1 -0
  186. package/dist/navigation/Steps.svelte +965 -0
  187. package/dist/navigation/Steps.svelte.d.ts +43 -0
  188. package/dist/navigation/Steps.svelte.d.ts.map +1 -0
  189. package/dist/navigation/Tabs.svelte +698 -0
  190. package/dist/navigation/Tabs.svelte.d.ts +41 -0
  191. package/dist/navigation/Tabs.svelte.d.ts.map +1 -0
  192. package/dist/navigation/index.d.ts +8 -0
  193. package/dist/navigation/index.d.ts.map +1 -0
  194. package/dist/navigation/index.js +5 -0
  195. package/package.json +139 -0
@@ -0,0 +1,66 @@
1
+ export type InputType = 'text' | 'email' | 'password' | 'url' | 'tel' | 'search' | 'number' | 'textarea' | 'date' | 'time' | 'datetime-local' | 'color' | 'file';
2
+ export interface InputOption {
3
+ /** The value inserted into the input when this option is chosen */
4
+ value: string;
5
+ /** Display text for the option */
6
+ label: string;
7
+ /** Whether this option cannot be selected */
8
+ disabled?: boolean;
9
+ /** Secondary descriptive text shown under the label */
10
+ description?: string;
11
+ }
12
+ import { type Component, type Snippet } from 'svelte';
13
+ declare const Input: Component<{
14
+ type?: InputType;
15
+ value?: string | number | boolean | string[] | File | File[] | null | undefined;
16
+ label?: string | undefined;
17
+ placeholder?: string | undefined;
18
+ label_display?: "float" | "pinned";
19
+ disabled?: boolean;
20
+ readonly?: boolean;
21
+ required?: boolean;
22
+ name?: string | undefined;
23
+ skeleton?: boolean;
24
+ tooltip?: string | undefined;
25
+ error?: string | boolean | undefined;
26
+ parse?: ((value: unknown) => unknown) | undefined;
27
+ pattern?: string | undefined;
28
+ minlength?: number | undefined;
29
+ maxlength?: number | undefined;
30
+ min?: number | string | undefined;
31
+ max?: number | string | undefined;
32
+ step?: number | undefined;
33
+ size?: "0" | "1" | "2" | "3";
34
+ prefix?: string | undefined;
35
+ suffix?: string | undefined;
36
+ icon?: Component | undefined;
37
+ clearable?: boolean;
38
+ show_counter?: boolean;
39
+ description?: string | undefined;
40
+ dense?: boolean;
41
+ comfortable?: boolean;
42
+ filled?: boolean;
43
+ id?: string;
44
+ class?: string;
45
+ options?: InputOption[] | undefined;
46
+ onfilter?: ((query: string) => Promise<InputOption[]>) | undefined;
47
+ multiple?: boolean;
48
+ rows?: number;
49
+ auto_resize?: boolean;
50
+ show_toggle?: boolean;
51
+ strength_indicator?: boolean;
52
+ mask?: string | undefined;
53
+ accept?: string | undefined;
54
+ option?: Snippet<[InputOption]> | undefined;
55
+ oninput?: ((detail: {
56
+ value: string | number | boolean | string[] | File | File[] | null | undefined;
57
+ }) => void) | undefined;
58
+ onchange?: ((detail: {
59
+ value: string | number | boolean | string[] | File | File[] | null | undefined;
60
+ }) => void) | undefined;
61
+ onfocus?: (() => void) | undefined;
62
+ onblur?: (() => void) | undefined;
63
+ }, {}, "value">;
64
+ type Input = ReturnType<typeof Input>;
65
+ export default Input;
66
+ //# sourceMappingURL=Input.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Input.svelte.d.ts","sourceRoot":"","sources":["../../src/form/Input.svelte.ts"],"names":[],"mappings":"AAGC,MAAM,MAAM,SAAS,GAClB,MAAM,GACN,OAAO,GACP,UAAU,GACV,KAAK,GACL,KAAK,GACL,QAAQ,GACR,QAAQ,GACR,UAAU,GACV,MAAM,GACN,MAAM,GACN,gBAAgB,GAChB,OAAO,GACP,MAAM,CAAC;AAEV,MAAM,WAAW,WAAW;IAC3B,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,kCAAkC;IAClC,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,uDAAuD;IACvD,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAIF,OAAO,EAAc,KAAK,SAAS,EAAE,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AAg1ClE,QAAA,MAAM,KAAK;WAhzCqE,SAAS;;YAA8B,MAAM,GAAG,SAAS;kBAAgB,MAAM,GAAG,SAAS;oBAAkB,OAAO,GAAG,QAAQ;eAAa,OAAO;eAAa,OAAO;eAAa,OAAO;WAAS,MAAM,GAAG,SAAS;eAAa,OAAO;cAAY,MAAM,GAAG,SAAS;YAAU,MAAM,GAAG,OAAO,GAAG,SAAS;YAAU,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,SAAS;cAAY,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;gBAAc,MAAM,GAAG,SAAS;UAAQ,MAAM,GAAG,MAAM,GAAG,SAAS;UAAQ,MAAM,GAAG,MAAM,GAAG,SAAS;WAAS,MAAM,GAAG,SAAS;WAAS,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG;aAAW,MAAM,GAAG,SAAS;aAAW,MAAM,GAAG,SAAS;WAAS,SAAS,GAAG,SAAS;gBAAc,OAAO;mBAAiB,OAAO;kBAAgB,MAAM,GAAG,SAAS;YAAU,OAAO;kBAAgB,OAAO;aAAW,OAAO;;YAA8B,MAAM;cAAY,WAAW,EAAE,GAAG,SAAS;eAAa,CAAC,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,WAAW,EAAE,CAAC,CAAC,GAAG,SAAS;eAAa,OAAO;WAAS,MAAM;kBAAgB,OAAO;kBAAgB,OAAO;yBAAuB,OAAO;WAAS,MAAM,GAAG,SAAS;aAAW,MAAM,GAAG,SAAS;aAAW,OAAO,CAAC,CAAC,WAAW,CAAC,CAAC,GAAG,SAAS;cAAY,CAAC,CAAC,MAAM,EAAE;QAAE,KAAK,0EAAY;KAAE,KAAK,IAAI,CAAC,GAAG,SAAS;eAAa,CAAC,CAAC,MAAM,EAAE;QAAE,KAAK,0EAAY;KAAE,KAAK,IAAI,CAAC,GAAG,SAAS;cAAY,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS;aAAW,CAAC,MAAM,IAAI,CAAC,GAAG,SAAS;eAgzCv2C,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}
@@ -0,0 +1,507 @@
1
+ <script lang="ts" module>
2
+ export { default as RadioGroup } from './Radio.svelte';
3
+
4
+ export interface RadioGroupContext {
5
+ /** The shared `name` attribute for all radios in the group */
6
+ name: string;
7
+ /** The currently selected value */
8
+ value: string;
9
+ /** Whether the whole group is disabled */
10
+ disabled: boolean;
11
+ /** The size applied to all radios in the group */
12
+ size: '0' | '1' | '2' | '3';
13
+ /** Selects the radio with the given value */
14
+ select: (value: string) => void;
15
+ }
16
+ </script>
17
+
18
+ <script lang="ts">
19
+ import { ripple, tooltip } from '@delightstack/utilities';
20
+ import { getContext, setContext, type Snippet } from 'svelte';
21
+ import type { FormContext } from './Form.svelte';
22
+
23
+ const propId = $props.id();
24
+
25
+ /* ------------------------------------------------------------------ */
26
+ /* Determine whether this instance is a RadioGroup or a Radio */
27
+ /* ------------------------------------------------------------------ */
28
+ let {
29
+ /* --- Radio props --- */
30
+ /** The value of this radio option (required for Radio) */
31
+ value = $bindable(''),
32
+
33
+ /** Whether this individual radio is checked (bindable, for standalone use) */
34
+ checked = $bindable(false),
35
+
36
+ /** Whether the radio is disabled */
37
+ disabled = false,
38
+
39
+ /** The size of the radio. 0=16px, 1=20px, 2=24px, 3=28px */
40
+ size = '1' as '0' | '1' | '2' | '3',
41
+
42
+ /** The label text for the radio */
43
+ label = '',
44
+
45
+ /** A description shown below the label */
46
+ description = '',
47
+
48
+ /** The tooltip message shown on hover */
49
+ tooltip: tooltip_message = '',
50
+
51
+ /** Whether to display in a condensed view */
52
+ dense = false,
53
+
54
+ /** Whether to display in an expanded view */
55
+ comfortable = false,
56
+
57
+ /** The ID of the radio element */
58
+ id = propId,
59
+
60
+ /** The name attribute for the radio group / hidden input */
61
+ name = '',
62
+
63
+ /** Specifies a custom class name */
64
+ class: class_name = '',
65
+
66
+ /* --- RadioGroup-only props --- */
67
+ /** Whether the radios should lay out horizontally */
68
+ horizontal = false,
69
+
70
+ /** An error message (RadioGroup only) */
71
+ error = '',
72
+
73
+ /** Whether the field is required (RadioGroup only) */
74
+ required = false,
75
+
76
+ /** Parses & validates the value (e.g. a database table form field's
77
+ * `parse`). Inside a Form it is registered with the form, which runs it
78
+ * on the form's validation timing. RadioGroup only. */
79
+ parse = undefined as ((value: unknown) => unknown) | undefined,
80
+
81
+ /** Child snippet (RadioGroup renders children; Radio does not) */
82
+ children = undefined as undefined | Snippet,
83
+
84
+ /** Called when the selected value changes */
85
+ onchange = undefined as ((detail: { value: string }) => void) | undefined,
86
+ } = $props();
87
+
88
+ /* ------------------------------------------------------------------ */
89
+ /* RadioGroup behaviour (when children are provided) */
90
+ /* ------------------------------------------------------------------ */
91
+ // svelte-ignore state_referenced_locally
92
+ const isGroup = !!children;
93
+
94
+ const sizes: Record<string, number> = { '0': 16, '1': 20, '2': 24, '3': 28 };
95
+
96
+ /* ------------------------------------------------------------------ */
97
+ /* Form context integration (RadioGroup only) */
98
+ /* ------------------------------------------------------------------ */
99
+
100
+ const form_ctx = getContext<FormContext | undefined>('form');
101
+ let group_element = $state<HTMLElement | undefined>(undefined);
102
+
103
+ /** Disabled merges the parent form's disabled/submitting state */
104
+ const effectively_disabled = $derived(disabled || (form_ctx?.disabled ?? false));
105
+
106
+ /** Group error from the local prop or the parent form context */
107
+ const resolved_error = $derived.by(() => {
108
+ if (error) return error;
109
+ if (form_ctx && name && form_ctx.errors[name]) return form_ctx.errors[name];
110
+ return '';
111
+ });
112
+
113
+ // Group context
114
+ if (isGroup) {
115
+ const ctx = $state<RadioGroupContext>({
116
+ name: name || id,
117
+ value,
118
+ disabled,
119
+ size,
120
+ select(val: string) {
121
+ value = val;
122
+ if (form_ctx && name) {
123
+ form_ctx.setValue(name, val);
124
+ form_ctx.setTouched(name);
125
+ }
126
+ onchange?.({ value: val });
127
+ },
128
+ });
129
+ setContext<RadioGroupContext>('radio-group', ctx);
130
+
131
+ // Keep context in sync with props
132
+ $effect(() => {
133
+ ctx.name = name || id;
134
+ ctx.value = value;
135
+ ctx.disabled = effectively_disabled;
136
+ ctx.size = size;
137
+ });
138
+
139
+ // Register the group with a parent Form (focus-on-error + field validator)
140
+ $effect(() => {
141
+ if (!form_ctx || !name) return;
142
+ if (group_element) form_ctx.register(name, group_element, parse);
143
+ return () => form_ctx.unregister(name);
144
+ });
145
+
146
+ // Context-driven: drive the selected value from the form data when the
147
+ // group lives inside a Form and has a name — no bind:value needed.
148
+ $effect(() => {
149
+ if (!form_ctx || !name) return;
150
+ const ctx_value = form_ctx.getValue(name);
151
+ const next = ctx_value == null ? '' : String(ctx_value);
152
+ if (next !== value) value = next;
153
+ });
154
+ }
155
+
156
+ /* ------------------------------------------------------------------ */
157
+ /* Radio (leaf) behaviour */
158
+ /* ------------------------------------------------------------------ */
159
+ const group = getContext<RadioGroupContext | undefined>('radio-group');
160
+
161
+ const isSelected = $derived(group ? group.value === value : checked);
162
+ const isDisabled = $derived(group ? group.disabled || disabled : disabled);
163
+ const effectiveSize = $derived(group ? group.size : size);
164
+ const effectiveName = $derived(group ? group.name : name);
165
+ const px = $derived(sizes[effectiveSize] ?? 20);
166
+
167
+ let animating = $state(false);
168
+
169
+ function select() {
170
+ if (isDisabled) return;
171
+ const wasSelected = isSelected;
172
+ if (group) {
173
+ group.select(value);
174
+ } else {
175
+ checked = true;
176
+ onchange?.({ value });
177
+ }
178
+ if (!wasSelected) {
179
+ animating = true;
180
+ setTimeout(() => (animating = false), 350);
181
+ }
182
+ }
183
+
184
+ function onKeyDown(e: KeyboardEvent) {
185
+ if (e.key === ' ') {
186
+ e.preventDefault();
187
+ select();
188
+ return;
189
+ }
190
+ // Arrow key navigation within a group
191
+ if (group && (e.key === 'ArrowDown' || e.key === 'ArrowRight')) {
192
+ e.preventDefault();
193
+ focusSibling(e.currentTarget as HTMLElement, 1);
194
+ }
195
+ if (group && (e.key === 'ArrowUp' || e.key === 'ArrowLeft')) {
196
+ e.preventDefault();
197
+ focusSibling(e.currentTarget as HTMLElement, -1);
198
+ }
199
+ }
200
+
201
+ function focusSibling(current: HTMLElement, direction: number) {
202
+ const groupEl = current.closest('.radio-group');
203
+ if (!groupEl) return;
204
+ const radios = Array.from(
205
+ groupEl.querySelectorAll<HTMLElement>('[role="radio"]:not([aria-disabled="true"])'),
206
+ );
207
+ const idx = radios.indexOf(current);
208
+ if (idx === -1) return;
209
+ const next = radios[(idx + direction + radios.length) % radios.length];
210
+ if (next) {
211
+ next.focus();
212
+ next.click();
213
+ }
214
+ }
215
+ </script>
216
+
217
+ {#if isGroup}
218
+ <!-- RadioGroup wrapper -->
219
+ <div
220
+ bind:this={group_element}
221
+ class={['radio-group', class_name].filter(Boolean).join(' ')}
222
+ class:horizontal
223
+ class:dense
224
+ class:comfortable
225
+ class:disabled={effectively_disabled}
226
+ class:has-error={!!resolved_error}
227
+ role="radiogroup"
228
+ aria-labelledby={label ? `${id}-group-label` : undefined}
229
+ aria-required={required || undefined}
230
+ {id}>
231
+ {#if label}
232
+ <span id="{id}-group-label" class="group-label">{label}</span>
233
+ {/if}
234
+ <div class="group-items" class:horizontal>
235
+ {@render children?.()}
236
+ </div>
237
+ {#if resolved_error}
238
+ <span class="error-text">{resolved_error}</span>
239
+ {/if}
240
+ </div>
241
+ {:else}
242
+ <!-- Individual Radio -->
243
+ <!-- The whole container is the click target so the hit area matches the
244
+ hover/press feedback (which keys off `.radio`). Keyboard activation and
245
+ arrow-key navigation stay on the focusable role="radio" indicator. -->
246
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
247
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
248
+ <div
249
+ class={['radio', class_name].filter(Boolean).join(' ')}
250
+ class:dense
251
+ class:comfortable
252
+ class:disabled={isDisabled}
253
+ {@attach tooltip(tooltip_message)}
254
+ style:--size="{px}px"
255
+ style:font-size={`var(--control-font-${effectiveSize})`}
256
+ onclick={select}>
257
+ <!-- Hidden native input for form submission -->
258
+ <input
259
+ type="radio"
260
+ id="{id}-input"
261
+ name={effectiveName}
262
+ {value}
263
+ checked={isSelected}
264
+ disabled={isDisabled}
265
+ {required}
266
+ tabindex={-1}
267
+ aria-hidden="true" />
268
+
269
+ <div
270
+ class="indicator-wrapper"
271
+ class:selected={isSelected}
272
+ role="radio"
273
+ tabindex={isDisabled ? -1 : 0}
274
+ aria-checked={isSelected}
275
+ aria-disabled={isDisabled}
276
+ aria-labelledby={label ? `${id}-label` : undefined}
277
+ {@attach ripple({ enabled: !isDisabled, centered: true, opacity: 0.15 })}
278
+ onkeydown={onKeyDown}>
279
+ <svg
280
+ class="indicator"
281
+ class:selected={isSelected}
282
+ class:animating
283
+ viewBox="0 0 24 24"
284
+ width={px}
285
+ height={px}
286
+ fill="none">
287
+ <circle class="ring" cx="12" cy="12" r="10" stroke-width="2" />
288
+ <circle class="dot" cx="12" cy="12" r="5" />
289
+ </svg>
290
+ </div>
291
+
292
+ {#if label || description}
293
+ <div class="content">
294
+ {#if label}
295
+ <span id="{id}-label" class="label">{label}</span>
296
+ {/if}
297
+ {#if description}
298
+ <span class="description">{description}</span>
299
+ {/if}
300
+ </div>
301
+ {/if}
302
+ </div>
303
+ {/if}
304
+
305
+ <style>
306
+ /* ========== RadioGroup ========== */
307
+ .radio-group {
308
+ display: flex;
309
+ flex-direction: column;
310
+ gap: 0.25em;
311
+
312
+ &.disabled {
313
+ opacity: 0.5;
314
+ pointer-events: none;
315
+ }
316
+ &.has-error {
317
+ .error-text {
318
+ display: block;
319
+ }
320
+ }
321
+ }
322
+
323
+ .group-label {
324
+ font-size: 0.85em;
325
+ color: var(--color-text-disabled, #888);
326
+ margin-bottom: 0.25em;
327
+ }
328
+
329
+ .group-items {
330
+ display: flex;
331
+ flex-direction: column;
332
+ gap: 0.25em;
333
+
334
+ &.horizontal {
335
+ flex-direction: row;
336
+ flex-wrap: wrap;
337
+ gap: 1em;
338
+ }
339
+ }
340
+
341
+ .error-text {
342
+ display: none;
343
+ font-size: 0.8em;
344
+ color: var(--color-error, #d32f2f);
345
+ margin-top: 0.25em;
346
+ }
347
+
348
+ /* ========== Radio ========== */
349
+ .radio {
350
+ display: flex;
351
+ align-items: flex-start;
352
+ gap: 0.5em;
353
+ /* The whole container is clickable, so the pointer cursor and the
354
+ hover/press feedback now line up with the actual hit area. Bound it
355
+ to its content so it can't stretch the hit area across the full group
356
+ width (align-items: stretch in .group-items would otherwise do so). */
357
+ width: fit-content;
358
+ cursor: pointer;
359
+ user-select: none;
360
+ position: relative;
361
+ perspective: 100px;
362
+ transition: translate 200ms ease;
363
+
364
+ &:not(.disabled):active {
365
+ translate: 0px 3px clamp(-10px, calc(0.2em - 12px), -2px);
366
+ }
367
+
368
+ &.dense {
369
+ gap: 0.25em;
370
+ }
371
+ &.comfortable {
372
+ gap: 0.75em;
373
+ }
374
+ &.disabled {
375
+ opacity: 0.5;
376
+ pointer-events: none;
377
+ }
378
+
379
+ /* Hover: circular background tint on indicator (triggers from label hover too) */
380
+ &:not(.disabled):hover > .indicator-wrapper {
381
+ background: var(--hover-tint);
382
+ transition: none;
383
+ }
384
+
385
+ /* Active: press scale on indicator (triggers from label click too) */
386
+ &:not(.disabled):active > .indicator-wrapper {
387
+ transform: scale(0.9);
388
+ transition:
389
+ transform 80ms ease,
390
+ background 200ms ease;
391
+ }
392
+ }
393
+
394
+ /* Hidden native radio, kept for form submission */
395
+ input {
396
+ position: absolute;
397
+ width: 1px;
398
+ height: 1px;
399
+ overflow: hidden;
400
+ clip: rect(0, 0, 0, 0);
401
+ white-space: nowrap;
402
+ border: 0;
403
+ padding: 0;
404
+ margin: -1px;
405
+ }
406
+
407
+ .indicator-wrapper {
408
+ position: relative;
409
+ display: flex;
410
+ align-items: center;
411
+ justify-content: center;
412
+ width: calc(var(--size) + 20px);
413
+ height: calc(var(--size) + 20px);
414
+ border-radius: 50%;
415
+ cursor: pointer;
416
+ flex-shrink: 0;
417
+ overflow: hidden;
418
+ outline: none;
419
+ -webkit-tap-highlight-color: transparent;
420
+ --hover-tint: color-mix(in srgb, var(--color-text, currentColor) 12%, transparent);
421
+ transition:
422
+ background 200ms ease,
423
+ transform 150ms ease;
424
+
425
+ /* Accent-tinted hover when selected */
426
+ &.selected {
427
+ --hover-tint: color-mix(in srgb, var(--color-action, #1976d2) 16%, transparent);
428
+ }
429
+
430
+ &:focus-visible {
431
+ box-shadow: 0 0 0 2px var(--color-text, currentColor);
432
+ }
433
+ }
434
+
435
+ .indicator {
436
+ flex-shrink: 0;
437
+
438
+ .ring {
439
+ stroke: var(--color-text-disabled, #999);
440
+ fill: transparent;
441
+ transition: stroke 150ms ease;
442
+ }
443
+
444
+ .dot {
445
+ fill: transparent;
446
+ transform-origin: center;
447
+ transform: scale(0);
448
+ transition:
449
+ fill 150ms ease,
450
+ transform 200ms cubic-bezier(0.4, 0, 0.2, 1);
451
+ }
452
+
453
+ &.selected {
454
+ .ring {
455
+ stroke: var(--color-action, #1976d2);
456
+ }
457
+ .dot {
458
+ fill: var(--color-action, #1976d2);
459
+ transform: scale(1);
460
+ }
461
+ }
462
+
463
+ /* Select animation: elastic dot scale + ring pulse */
464
+ &.animating {
465
+ animation: ring-pulse 350ms cubic-bezier(0.34, 1.56, 0.64, 1);
466
+
467
+ .dot {
468
+ transition:
469
+ fill 150ms ease,
470
+ transform 300ms cubic-bezier(0.34, 1.56, 0.64, 1);
471
+ }
472
+ }
473
+ }
474
+
475
+ @keyframes ring-pulse {
476
+ 0% {
477
+ transform: scale(1);
478
+ }
479
+ 40% {
480
+ transform: scale(1.1);
481
+ }
482
+ 100% {
483
+ transform: scale(1);
484
+ }
485
+ }
486
+
487
+ .content {
488
+ display: flex;
489
+ flex-direction: column;
490
+ gap: 0.1em;
491
+ padding-top: calc((var(--size) + 20px) / 2 - 0.7em);
492
+ margin-left: -8px;
493
+ }
494
+
495
+ .label {
496
+ cursor: pointer;
497
+ user-select: none;
498
+ line-height: 1.4;
499
+ color: var(--color-text, inherit);
500
+ }
501
+
502
+ .description {
503
+ font-size: 0.85em;
504
+ color: var(--color-text-disabled, #888);
505
+ line-height: 1.3;
506
+ }
507
+ </style>
@@ -0,0 +1,39 @@
1
+ export { default as RadioGroup } from './Radio.svelte';
2
+ export interface RadioGroupContext {
3
+ /** The shared `name` attribute for all radios in the group */
4
+ name: string;
5
+ /** The currently selected value */
6
+ value: string;
7
+ /** Whether the whole group is disabled */
8
+ disabled: boolean;
9
+ /** The size applied to all radios in the group */
10
+ size: '0' | '1' | '2' | '3';
11
+ /** Selects the radio with the given value */
12
+ select: (value: string) => void;
13
+ }
14
+ import { type Snippet } from 'svelte';
15
+ declare const Radio: import("svelte").Component<{
16
+ value?: string;
17
+ checked?: boolean;
18
+ disabled?: boolean;
19
+ size?: "0" | "1" | "2" | "3";
20
+ label?: string;
21
+ description?: string;
22
+ tooltip?: string;
23
+ dense?: boolean;
24
+ comfortable?: boolean;
25
+ id?: string;
26
+ name?: string;
27
+ class?: string;
28
+ horizontal?: boolean;
29
+ error?: string;
30
+ required?: boolean;
31
+ parse?: ((value: unknown) => unknown) | undefined;
32
+ children?: undefined | Snippet;
33
+ onchange?: ((detail: {
34
+ value: string;
35
+ }) => void) | undefined;
36
+ }, {}, "value" | "checked">;
37
+ type Radio = ReturnType<typeof Radio>;
38
+ export default Radio;
39
+ //# sourceMappingURL=Radio.svelte.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Radio.svelte.d.ts","sourceRoot":"","sources":["../../src/form/Radio.svelte.ts"],"names":[],"mappings":"AAGC,OAAO,EAAE,OAAO,IAAI,UAAU,EAAE,MAAM,gBAAgB,CAAC;AAEvD,MAAM,WAAW,iBAAiB;IACjC,8DAA8D;IAC9D,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,0CAA0C;IAC1C,QAAQ,EAAE,OAAO,CAAC;IAClB,kDAAkD;IAClD,IAAI,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAC5B,6CAA6C;IAC7C,MAAM,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAIF,OAAO,EAA0B,KAAK,OAAO,EAAE,MAAM,QAAQ,CAAC;AA0P9D,QAAA,MAAM,KAAK;YAlPsE,MAAM;cAAY,OAAO;eAAa,OAAO;WAAS,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG;YAAU,MAAM;kBAAgB,MAAM;cAAY,MAAM;YAAU,OAAO;kBAAgB,OAAO;;WAA6B,MAAM;YAAU,MAAM;iBAAe,OAAO;YAAU,MAAM;eAAa,OAAO;YAAU,CAAC,CAAC,KAAK,EAAE,OAAO,KAAK,OAAO,CAAC,GAAG,SAAS;eAAa,SAAS,GAAG,OAAO;eAAa,CAAC,CAAC,MAAM,EAAE;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC,GAAG,SAAS;2BAkPvc,CAAC;AACpD,KAAK,KAAK,GAAG,UAAU,CAAC,OAAO,KAAK,CAAC,CAAC;AACtC,eAAe,KAAK,CAAC"}