@svgrid/grid 1.2.22 → 1.2.24

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 (263) hide show
  1. package/dist/GridFooter.svelte +34 -20
  2. package/dist/GridFooter.svelte.d.ts +8 -0
  3. package/dist/SvAutoComplete.svelte +89 -0
  4. package/dist/SvAutoComplete.svelte.d.ts +16 -0
  5. package/dist/SvButton.svelte +97 -0
  6. package/dist/SvButton.svelte.d.ts +23 -0
  7. package/dist/SvCalendar.svelte +409 -0
  8. package/dist/SvCalendar.svelte.d.ts +52 -0
  9. package/dist/SvCheckBox.svelte +82 -0
  10. package/dist/SvCheckBox.svelte.d.ts +19 -0
  11. package/dist/SvColorInput.svelte +160 -0
  12. package/dist/SvColorInput.svelte.d.ts +15 -0
  13. package/dist/SvComboBox.svelte +108 -0
  14. package/dist/SvComboBox.svelte.d.ts +15 -0
  15. package/dist/SvCountryInput.svelte +125 -0
  16. package/dist/SvCountryInput.svelte.d.ts +14 -0
  17. package/dist/SvDateTimePicker.svelte +265 -0
  18. package/dist/SvDateTimePicker.svelte.d.ts +38 -0
  19. package/dist/SvDropDownList.svelte +120 -0
  20. package/dist/SvDropDownList.svelte.d.ts +15 -0
  21. package/dist/SvForm.svelte +147 -0
  22. package/dist/SvForm.svelte.d.ts +29 -0
  23. package/dist/SvGauge.svelte +97 -0
  24. package/dist/SvGauge.svelte.d.ts +26 -0
  25. package/dist/SvGrid.controller.svelte.d.ts +16 -2
  26. package/dist/SvGrid.controller.svelte.js +63 -8
  27. package/dist/SvGrid.css +84 -31
  28. package/dist/SvGrid.helpers.d.ts +4 -0
  29. package/dist/SvGrid.helpers.js +40 -12
  30. package/dist/SvGrid.svelte +84 -5
  31. package/dist/SvGrid.types.d.ts +36 -0
  32. package/dist/SvGridDropdown.svelte +16 -30
  33. package/dist/SvGridDropdown.svelte.d.ts +4 -0
  34. package/dist/SvListBox.svelte +89 -0
  35. package/dist/SvListBox.svelte.d.ts +22 -0
  36. package/dist/SvMaskedInput.svelte +66 -0
  37. package/dist/SvMaskedInput.svelte.d.ts +15 -0
  38. package/dist/SvNumberInput.svelte +116 -0
  39. package/dist/SvNumberInput.svelte.d.ts +18 -0
  40. package/dist/SvPasswordInput.svelte +100 -0
  41. package/dist/SvPasswordInput.svelte.d.ts +18 -0
  42. package/dist/SvPhoneInput.svelte +87 -0
  43. package/dist/SvPhoneInput.svelte.d.ts +19 -0
  44. package/dist/SvRadioGroup.svelte +85 -0
  45. package/dist/SvRadioGroup.svelte.d.ts +20 -0
  46. package/dist/SvRating.svelte +91 -0
  47. package/dist/SvRating.svelte.d.ts +18 -0
  48. package/dist/SvRepeatButton.svelte +89 -0
  49. package/dist/SvRepeatButton.svelte.d.ts +21 -0
  50. package/dist/SvSlider.svelte +155 -0
  51. package/dist/SvSlider.svelte.d.ts +20 -0
  52. package/dist/SvSwitchButton.svelte +87 -0
  53. package/dist/SvSwitchButton.svelte.d.ts +14 -0
  54. package/dist/SvTabs.svelte +109 -0
  55. package/dist/SvTabs.svelte.d.ts +26 -0
  56. package/dist/SvTagsInput.svelte +70 -0
  57. package/dist/SvTagsInput.svelte.d.ts +14 -0
  58. package/dist/SvTimePicker.svelte +201 -0
  59. package/dist/SvTimePicker.svelte.d.ts +30 -0
  60. package/dist/SvToggleButton.svelte +57 -0
  61. package/dist/SvToggleButton.svelte.d.ts +16 -0
  62. package/dist/SvTree.svelte +131 -0
  63. package/dist/SvTree.svelte.d.ts +27 -0
  64. package/dist/build-api.js +78 -1
  65. package/dist/cdn/svgrid.js +11674 -5907
  66. package/dist/cdn/svgrid.svelte-external.js +10436 -4671
  67. package/dist/cell-render.d.ts +4 -0
  68. package/dist/cell-render.js +35 -1
  69. package/dist/clipboard.js +19 -1
  70. package/dist/collaboration.d.ts +2 -0
  71. package/dist/collaboration.js +4 -0
  72. package/dist/core.d.ts +22 -1
  73. package/dist/countries.d.ts +15 -0
  74. package/dist/countries.js +61 -0
  75. package/dist/createAutocomplete.svelte.d.ts +77 -0
  76. package/dist/createAutocomplete.svelte.js +100 -0
  77. package/dist/createCalendar.svelte.d.ts +192 -0
  78. package/dist/createCalendar.svelte.js +402 -0
  79. package/dist/createCheckbox.svelte.d.ts +45 -0
  80. package/dist/createCheckbox.svelte.js +26 -0
  81. package/dist/createColorInput.svelte.d.ts +67 -0
  82. package/dist/createColorInput.svelte.js +100 -0
  83. package/dist/createCombobox.svelte.d.ts +102 -0
  84. package/dist/createCombobox.svelte.js +171 -0
  85. package/dist/createCountryInput.svelte.d.ts +86 -0
  86. package/dist/createCountryInput.svelte.js +126 -0
  87. package/dist/createDateTimePicker.svelte.d.ts +83 -0
  88. package/dist/createDateTimePicker.svelte.js +202 -0
  89. package/dist/createDropdownList.svelte.d.ts +78 -0
  90. package/dist/createDropdownList.svelte.js +117 -0
  91. package/dist/createGauge.svelte.d.ts +56 -0
  92. package/dist/createGauge.svelte.js +49 -0
  93. package/dist/createListbox.svelte.d.ts +79 -0
  94. package/dist/createListbox.svelte.js +117 -0
  95. package/dist/createMaskedInput.svelte.d.ts +30 -0
  96. package/dist/createMaskedInput.svelte.js +62 -0
  97. package/dist/createNumberInput.svelte.d.ts +72 -0
  98. package/dist/createNumberInput.svelte.js +167 -0
  99. package/dist/createPasswordInput.svelte.d.ts +60 -0
  100. package/dist/createPasswordInput.svelte.js +72 -0
  101. package/dist/createPhoneInput.svelte.d.ts +46 -0
  102. package/dist/createPhoneInput.svelte.js +105 -0
  103. package/dist/createRadioGroup.svelte.d.ts +64 -0
  104. package/dist/createRadioGroup.svelte.js +73 -0
  105. package/dist/createRating.svelte.d.ts +70 -0
  106. package/dist/createRating.svelte.js +85 -0
  107. package/dist/createSlider.svelte.d.ts +71 -0
  108. package/dist/createSlider.svelte.js +123 -0
  109. package/dist/createSwitch.svelte.d.ts +43 -0
  110. package/dist/createSwitch.svelte.js +41 -0
  111. package/dist/createTabs.svelte.d.ts +78 -0
  112. package/dist/createTabs.svelte.js +92 -0
  113. package/dist/createTagsInput.svelte.d.ts +65 -0
  114. package/dist/createTagsInput.svelte.js +93 -0
  115. package/dist/createTimePicker.svelte.d.ts +106 -0
  116. package/dist/createTimePicker.svelte.js +248 -0
  117. package/dist/createToggle.svelte.d.ts +43 -0
  118. package/dist/createToggle.svelte.js +39 -0
  119. package/dist/createTree.svelte.d.ts +96 -0
  120. package/dist/createTree.svelte.js +194 -0
  121. package/dist/datetime/date-core.d.ts +88 -0
  122. package/dist/datetime/date-core.js +165 -0
  123. package/dist/datetime/date-format.d.ts +39 -0
  124. package/dist/datetime/date-format.js +371 -0
  125. package/dist/datetime/date-restrict.d.ts +23 -0
  126. package/dist/datetime/date-restrict.js +50 -0
  127. package/dist/datetime/date-selection.d.ts +25 -0
  128. package/dist/datetime/date-selection.js +94 -0
  129. package/dist/datetime/mask.d.ts +21 -0
  130. package/dist/datetime/mask.js +90 -0
  131. package/dist/editing.d.ts +1 -1
  132. package/dist/editing.js +7 -0
  133. package/dist/editor-contract.d.ts +48 -0
  134. package/dist/editor-contract.js +26 -0
  135. package/dist/editors/cell-editors.d.ts +2 -2
  136. package/dist/editors/cell-editors.js +3 -1
  137. package/dist/export-format.d.ts +178 -0
  138. package/dist/export-format.js +419 -0
  139. package/dist/hyperformula-adapter.d.ts +4 -0
  140. package/dist/hyperformula-adapter.js +15 -6
  141. package/dist/index.d.ts +60 -0
  142. package/dist/index.js +72 -0
  143. package/dist/list-option.d.ts +10 -0
  144. package/dist/list-option.js +7 -0
  145. package/dist/menus.js +18 -2
  146. package/dist/popover.d.ts +48 -0
  147. package/dist/popover.js +65 -0
  148. package/dist/row-resize.d.ts +4 -4
  149. package/dist/row-resize.js +8 -5
  150. package/dist/selection.d.ts +6 -0
  151. package/dist/selection.js +31 -5
  152. package/dist/server-data-source.d.ts +43 -0
  153. package/dist/server-data-source.js +100 -0
  154. package/dist/sparkline.js +8 -4
  155. package/dist/svgrid-wrapper.types.d.ts +26 -1
  156. package/package.json +6 -1
  157. package/src/GridFooter.svelte +34 -20
  158. package/src/SvAutoComplete.svelte +89 -0
  159. package/src/SvButton.svelte +97 -0
  160. package/src/SvCalendar.svelte +409 -0
  161. package/src/SvCalendar.test.ts +211 -0
  162. package/src/SvCheckBox.svelte +82 -0
  163. package/src/SvColorInput.svelte +160 -0
  164. package/src/SvComboBox.svelte +108 -0
  165. package/src/SvCountryInput.svelte +125 -0
  166. package/src/SvDateTimePicker.svelte +265 -0
  167. package/src/SvDateTimePicker.test.ts +136 -0
  168. package/src/SvDropDownList.svelte +120 -0
  169. package/src/SvForm.svelte +147 -0
  170. package/src/SvGauge.svelte +97 -0
  171. package/src/SvGrid.controller.svelte.ts +74 -8
  172. package/src/SvGrid.css +84 -31
  173. package/src/SvGrid.helpers.ts +41 -12
  174. package/src/SvGrid.svelte +84 -5
  175. package/src/SvGrid.types.ts +33 -0
  176. package/src/SvGridDropdown.svelte +16 -30
  177. package/src/SvListBox.svelte +89 -0
  178. package/src/SvMaskedInput.svelte +66 -0
  179. package/src/SvNumberInput.svelte +116 -0
  180. package/src/SvPasswordInput.svelte +100 -0
  181. package/src/SvPhoneInput.svelte +87 -0
  182. package/src/SvRadioGroup.svelte +85 -0
  183. package/src/SvRating.svelte +91 -0
  184. package/src/SvRepeatButton.svelte +89 -0
  185. package/src/SvSlider.svelte +155 -0
  186. package/src/SvSwitchButton.svelte +87 -0
  187. package/src/SvTabs.svelte +109 -0
  188. package/src/SvTagsInput.svelte +70 -0
  189. package/src/SvTimePicker.svelte +201 -0
  190. package/src/SvTimePicker.test.ts +98 -0
  191. package/src/SvToggleButton.svelte +57 -0
  192. package/src/SvTree.svelte +131 -0
  193. package/src/build-api.ts +115 -0
  194. package/src/cell-render.test.ts +38 -0
  195. package/src/cell-render.ts +43 -0
  196. package/src/clipboard.test.ts +18 -0
  197. package/src/clipboard.ts +24 -1
  198. package/src/collaboration.test.ts +30 -0
  199. package/src/collaboration.ts +6 -0
  200. package/src/core.ts +27 -3
  201. package/src/countries.ts +71 -0
  202. package/src/createAutocomplete.svelte.ts +104 -0
  203. package/src/createCalendar.svelte.ts +488 -0
  204. package/src/createCheckbox.svelte.ts +74 -0
  205. package/src/createColorInput.svelte.ts +115 -0
  206. package/src/createCombobox.svelte.ts +158 -0
  207. package/src/createCountryInput.svelte.ts +126 -0
  208. package/src/createDateTimePicker.svelte.ts +216 -0
  209. package/src/createDropdownList.svelte.ts +125 -0
  210. package/src/createGauge.svelte.ts +86 -0
  211. package/src/createListbox.svelte.ts +186 -0
  212. package/src/createMaskedInput.svelte.ts +81 -0
  213. package/src/createNumberInput.svelte.ts +193 -0
  214. package/src/createPasswordInput.svelte.ts +87 -0
  215. package/src/createPhoneInput.svelte.ts +119 -0
  216. package/src/createRadioGroup.svelte.ts +132 -0
  217. package/src/createRating.svelte.ts +145 -0
  218. package/src/createSlider.svelte.ts +160 -0
  219. package/src/createSwitch.svelte.ts +78 -0
  220. package/src/createTabs.svelte.ts +129 -0
  221. package/src/createTagsInput.svelte.ts +98 -0
  222. package/src/createTimePicker.svelte.ts +289 -0
  223. package/src/createToggle.svelte.ts +69 -0
  224. package/src/createTree.svelte.ts +216 -0
  225. package/src/datetime/date-core.test.ts +217 -0
  226. package/src/datetime/date-core.ts +204 -0
  227. package/src/datetime/date-format.test.ts +121 -0
  228. package/src/datetime/date-format.ts +317 -0
  229. package/src/datetime/date-restrict.ts +60 -0
  230. package/src/datetime/date-selection.test.ts +129 -0
  231. package/src/datetime/date-selection.ts +137 -0
  232. package/src/datetime/mask.test.ts +36 -0
  233. package/src/datetime/mask.ts +84 -0
  234. package/src/editing.test.ts +22 -0
  235. package/src/editing.ts +7 -1
  236. package/src/editor-contract.test.ts +32 -0
  237. package/src/editor-contract.ts +60 -0
  238. package/src/editors/cell-editors.ts +7 -1
  239. package/src/export-data-api.test.ts +126 -0
  240. package/src/export-format.test.ts +107 -0
  241. package/src/export-format.ts +598 -0
  242. package/src/hyperformula-adapter.test.ts +35 -1
  243. package/src/hyperformula-adapter.ts +18 -6
  244. package/src/index.ts +110 -0
  245. package/src/list-option.ts +15 -0
  246. package/src/menus.ts +21 -2
  247. package/src/popover.ts +79 -0
  248. package/src/row-resize.test.ts +25 -0
  249. package/src/row-resize.ts +8 -5
  250. package/src/selection.test.ts +36 -4
  251. package/src/selection.ts +30 -9
  252. package/src/server-data-source.test.ts +201 -0
  253. package/src/server-data-source.ts +148 -0
  254. package/src/sparkline.test.ts +9 -0
  255. package/src/sparkline.ts +9 -4
  256. package/src/svgrid-wrapper.types.ts +28 -1
  257. package/src/svgrid.behavior.test.ts +27 -0
  258. package/src/svgrid.new-features.wrapper.test.ts +27 -1
  259. package/src/ui-buttons.test.ts +105 -0
  260. package/src/ui-composite.test.ts +89 -0
  261. package/src/ui-inputs.test.ts +118 -0
  262. package/src/ui-range.test.ts +61 -0
  263. package/src/ui-selection.test.ts +119 -0
@@ -0,0 +1,202 @@
1
+ /**
2
+ * createDateTimePicker - the HEADLESS core behind <SvDateTimePicker>: a
3
+ * runes-based state machine for a masked date-time field (format/parse via the
4
+ * ./datetime token engine, min/max clamping, nullable clear, spin-button
5
+ * stepping) plus the dropdown's open/tab state and the calendar/time-picker
6
+ * wiring. The render-only concerns - the portalled popover positioning, DOM
7
+ * refs, outside-click + reposition listeners - stay in the styled component.
8
+ *
9
+ * ```svelte
10
+ * <script lang="ts">
11
+ * import { createDateTimePicker } from '@svgrid/grid'
12
+ * let value = $state<Date | null>(new Date())
13
+ * const dtp = createDateTimePicker({ value: () => value, onChange: (d) => (value = d) })
14
+ * </script>
15
+ * <input bind:value={dtp.text} {...dtp.inputProps()} />
16
+ * ```
17
+ */
18
+ import { formatDate, parseDate } from './datetime/date-format';
19
+ import { toDate, clampDate, withTime, startOfDay } from './datetime/date-core';
20
+ const defaultLocale = () => (typeof navigator !== 'undefined' ? navigator.language : 'en-US');
21
+ export function createDateTimePicker(config) {
22
+ const formatString = () => config.formatString?.() ?? 'yyyy-MM-dd HH:mm';
23
+ const nullable = () => config.nullable?.() ?? true;
24
+ const locale = () => config.locale?.() ?? defaultLocale();
25
+ const dropDownDisplayMode = () => config.dropDownDisplayMode?.() ?? 'both';
26
+ const spinButtons = () => config.spinButtons?.() ?? false;
27
+ const stepMinutes = () => config.stepMinutes?.() ?? 1;
28
+ const isDisabled = () => config.disabled?.() ?? false;
29
+ const isReadonly = () => config.readonly?.() ?? false;
30
+ const isInteractive = $derived(!isDisabled() && !isReadonly());
31
+ const minD = $derived(toDate(config.min?.() ?? null));
32
+ const maxD = $derived(toDate(config.max?.() ?? null));
33
+ let current = $state(null);
34
+ let text = $state('');
35
+ let lastKey = '￿';
36
+ $effect(() => {
37
+ const d = toDate(config.value());
38
+ const key = d ? String(d.getTime()) : 'null';
39
+ if (key !== lastKey) {
40
+ lastKey = key;
41
+ current = d;
42
+ text = d ? formatDate(d, formatString(), locale()) : '';
43
+ }
44
+ });
45
+ // --- Dropdown (open/tab) state; positioning stays in the component ----------
46
+ let open = $state(false);
47
+ let tab = $state('date');
48
+ const showDateTab = $derived(dropDownDisplayMode() !== 'time');
49
+ const showTimeTab = $derived(dropDownDisplayMode() !== 'calendar');
50
+ // --- Commit helpers --------------------------------------------------------
51
+ function commit(next) {
52
+ const clamped = next ? clampDate(next, minD, maxD) : null;
53
+ current = clamped;
54
+ lastKey = clamped ? String(clamped.getTime()) : 'null';
55
+ text = clamped ? formatDate(clamped, formatString(), locale()) : '';
56
+ config.onChange?.(clamped);
57
+ }
58
+ function commitFromText() {
59
+ const raw = text.trim();
60
+ if (!raw) {
61
+ if (nullable())
62
+ commit(null);
63
+ else
64
+ text = current ? formatDate(current, formatString(), locale()) : '';
65
+ return;
66
+ }
67
+ const parsed = parseDate(raw, formatString(), locale(), 2029, current ?? new Date());
68
+ if (parsed)
69
+ commit(parsed);
70
+ else
71
+ text = current ? formatDate(current, formatString(), locale()) : ''; // revert on bad input
72
+ }
73
+ function bump(deltaMin) {
74
+ const base = current ?? new Date();
75
+ commit(new Date(base.getTime() + deltaMin * 60000));
76
+ }
77
+ // --- Sub-picker wiring -----------------------------------------------------
78
+ function onCalendarChange(dates) {
79
+ const day = dates[0];
80
+ if (!day)
81
+ return;
82
+ commit(withTime(day, current ?? startOfDay(day)));
83
+ // Date-only picker: a single pick finalizes the value.
84
+ if (dropDownDisplayMode() === 'calendar') {
85
+ open = false;
86
+ config.onCommit?.(current);
87
+ }
88
+ else if (dropDownDisplayMode() === 'both')
89
+ tab = 'time';
90
+ }
91
+ function onTimeChange(t) {
92
+ const base = current ?? new Date();
93
+ commit(withTime(base, t));
94
+ }
95
+ // --- Dropdown control ------------------------------------------------------
96
+ function openPanel() {
97
+ if (!isInteractive || open)
98
+ return;
99
+ tab = dropDownDisplayMode() === 'time' ? 'time' : 'date';
100
+ open = true;
101
+ }
102
+ function closePanel() { open = false; }
103
+ function toggle() { (open ? closePanel : openPanel)(); }
104
+ function setTab(t) { tab = t; }
105
+ function onInputKeydown(e) {
106
+ if (e.key === 'Enter') {
107
+ e.preventDefault();
108
+ commitFromText();
109
+ closePanel();
110
+ config.onCommit?.(current);
111
+ }
112
+ else if (e.key === 'Escape') {
113
+ closePanel();
114
+ config.onCancel?.();
115
+ }
116
+ else if (e.key === 'ArrowDown' && (e.altKey || !text)) {
117
+ e.preventDefault();
118
+ openPanel();
119
+ }
120
+ else if (spinButtons() && e.key === 'ArrowUp') {
121
+ e.preventDefault();
122
+ bump(stepMinutes());
123
+ }
124
+ else if (spinButtons() && e.key === 'ArrowDown') {
125
+ e.preventDefault();
126
+ bump(-stepMinutes());
127
+ }
128
+ }
129
+ return {
130
+ // --- Reactive state -------------------------------------------------------
131
+ get current() { return current; },
132
+ get text() { return text; },
133
+ set text(v) { text = v; },
134
+ get open() { return open; },
135
+ get tab() { return tab; },
136
+ get isInteractive() { return isInteractive; },
137
+ get showDateTab() { return showDateTab; },
138
+ get showTimeTab() { return showTimeTab; },
139
+ // --- Actions --------------------------------------------------------------
140
+ commit,
141
+ commitFromText,
142
+ bump,
143
+ onCalendarChange,
144
+ onTimeChange,
145
+ openPanel,
146
+ closePanel,
147
+ toggle,
148
+ setTab,
149
+ onInputKeydown,
150
+ // --- Prop-getters ---------------------------------------------------------
151
+ /** The text field: masked value round-trips through the token engine. Spread
152
+ * alongside `bind:value={dtp.text}`. */
153
+ inputProps() {
154
+ return {
155
+ type: 'text',
156
+ disabled: isDisabled(),
157
+ readonly: isReadonly(),
158
+ onblur: commitFromText,
159
+ onkeydown: onInputKeydown,
160
+ };
161
+ },
162
+ /** The calendar-toggle button. */
163
+ toggleProps() {
164
+ return {
165
+ type: 'button',
166
+ 'aria-label': 'Open picker',
167
+ 'aria-haspopup': 'dialog',
168
+ 'aria-expanded': open,
169
+ disabled: !isInteractive,
170
+ tabindex: -1,
171
+ onclick: toggle,
172
+ };
173
+ },
174
+ /** The inline clear (x) button (nullable values). */
175
+ clearProps() {
176
+ return {
177
+ type: 'button',
178
+ 'aria-label': 'Clear',
179
+ onclick: () => commit(null),
180
+ };
181
+ },
182
+ /** A spin (increment/decrement) button that steps by `stepMinutes`. */
183
+ spinProps(dir) {
184
+ return {
185
+ type: 'button',
186
+ tabindex: -1,
187
+ 'aria-label': dir === 1 ? 'Increment' : 'Decrement',
188
+ disabled: !isInteractive,
189
+ onclick: () => bump(dir * stepMinutes()),
190
+ };
191
+ },
192
+ /** A DATE / TIME tab button. */
193
+ tabProps(which) {
194
+ return {
195
+ type: 'button',
196
+ role: 'tab',
197
+ 'aria-selected': tab === which,
198
+ onclick: () => setTab(which),
199
+ };
200
+ },
201
+ };
202
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * createDropdownList - the HEADLESS core behind <SvDropDownList>: a single-select
3
+ * dropdown state machine (trigger open/close, roving active index over enabled
4
+ * options, full keyboard) exposed as **prop-getters** you spread onto YOUR OWN
5
+ * markup. No styles, no DOM, no portal/measurement - those stay in the styled
6
+ * component.
7
+ *
8
+ * ```svelte
9
+ * <script lang="ts">
10
+ * import { createDropdownList } from '@svgrid/grid'
11
+ * let value = $state<string | null>(null)
12
+ * const dd = createDropdownList({ options: () => options, value: () => value, onChange: (v) => (value = v) })
13
+ * </script>
14
+ * <button {...dd.triggerProps()}>{dd.selected?.label ?? 'Select'}</button>
15
+ * {#if dd.open}
16
+ * <ul {...dd.listboxProps()}>
17
+ * {#each options as opt, i (opt.value)}<li {...dd.optionProps(i)}>{opt.label}</li>{/each}
18
+ * </ul>
19
+ * {/if}
20
+ * ```
21
+ */
22
+ import type { ListOption } from './list-option';
23
+ export type DropdownValue = string | number | null;
24
+ /** Reactive inputs are passed as getters so the core tracks live prop changes. */
25
+ export type DropdownListConfig = {
26
+ options: () => ReadonlyArray<ListOption>;
27
+ value: () => DropdownValue;
28
+ onChange?: (value: string | number) => void;
29
+ disabled?: () => boolean;
30
+ ariaLabel?: () => string | undefined;
31
+ /** DOM focus hook provided by the renderer; the core never touches the DOM. */
32
+ focusTrigger?: () => void;
33
+ };
34
+ export declare function createDropdownList(config: DropdownListConfig): {
35
+ /** Panel open state. */
36
+ readonly open: boolean;
37
+ /** Highlighted option index into `options`. */
38
+ readonly activeIndex: number;
39
+ /** The option matching the controlled value, if any. */
40
+ readonly selected: ListOption | null;
41
+ /** Controlled value. */
42
+ readonly value: DropdownValue;
43
+ isActive: (i: number) => boolean;
44
+ isSelected: (o: ListOption) => boolean;
45
+ setActive: (i: number) => void;
46
+ openPanel: () => void;
47
+ close: () => void;
48
+ toggle: () => void;
49
+ pick: (i: number) => void;
50
+ move: (d: number) => void;
51
+ onKeydown: (e: KeyboardEvent) => void;
52
+ /** Spread onto the trigger <button>. */
53
+ triggerProps: () => {
54
+ type: "button";
55
+ 'aria-haspopup': "listbox";
56
+ 'aria-expanded': boolean;
57
+ 'aria-label': string | undefined;
58
+ disabled: boolean;
59
+ onclick: () => void;
60
+ onkeydown: (e: KeyboardEvent) => void;
61
+ };
62
+ /** Spread onto the listbox container. */
63
+ listboxProps: () => {
64
+ role: "listbox";
65
+ tabindex: number;
66
+ };
67
+ /** Spread onto the option at `index` (index into `options`). */
68
+ optionProps: (index: number) => {
69
+ role: "option";
70
+ tabindex: number;
71
+ 'aria-selected': boolean;
72
+ 'aria-disabled': boolean | undefined;
73
+ 'data-idx': number;
74
+ onclick: () => void;
75
+ onpointermove: () => void;
76
+ };
77
+ };
78
+ export type DropdownList = ReturnType<typeof createDropdownList>;
@@ -0,0 +1,117 @@
1
+ export function createDropdownList(config) {
2
+ const opts = () => config.options();
3
+ const disabled = () => config.disabled?.() ?? false;
4
+ let open = $state(false);
5
+ let active = $state(-1);
6
+ const selected = $derived(opts().find((o) => o.value === config.value()) ?? null);
7
+ const enabledIdx = $derived(opts().map((o, i) => (o.disabled ? -1 : i)).filter((i) => i >= 0));
8
+ function openPanel() {
9
+ if (disabled() || open)
10
+ return;
11
+ open = true;
12
+ active = Math.max(0, opts().findIndex((o) => o.value === config.value()));
13
+ }
14
+ function close() { open = false; }
15
+ function toggle() { if (open) {
16
+ close();
17
+ }
18
+ else {
19
+ openPanel();
20
+ } }
21
+ function pick(i) {
22
+ const o = opts()[i];
23
+ if (!o || o.disabled)
24
+ return;
25
+ config.onChange?.(o.value);
26
+ close();
27
+ config.focusTrigger?.();
28
+ }
29
+ function setActive(i) { const o = opts()[i]; if (o && !o.disabled)
30
+ active = i; }
31
+ function move(d) {
32
+ const pos = enabledIdx.indexOf(active);
33
+ active = enabledIdx[(pos + d + enabledIdx.length) % enabledIdx.length] ?? active;
34
+ }
35
+ function onKeydown(e) {
36
+ if (disabled())
37
+ return;
38
+ if (!open && (e.key === 'ArrowDown' || e.key === 'Enter' || e.key === ' ')) {
39
+ e.preventDefault();
40
+ openPanel();
41
+ return;
42
+ }
43
+ if (!open)
44
+ return;
45
+ if (e.key === 'ArrowDown') {
46
+ e.preventDefault();
47
+ move(1);
48
+ }
49
+ else if (e.key === 'ArrowUp') {
50
+ e.preventDefault();
51
+ move(-1);
52
+ }
53
+ else if (e.key === 'Enter' || e.key === ' ') {
54
+ e.preventDefault();
55
+ pick(active);
56
+ }
57
+ else if (e.key === 'Escape') {
58
+ e.preventDefault();
59
+ close();
60
+ config.focusTrigger?.();
61
+ }
62
+ else if (e.key === 'Home') {
63
+ active = enabledIdx[0] ?? 0;
64
+ }
65
+ else if (e.key === 'End') {
66
+ active = enabledIdx.at(-1) ?? 0;
67
+ }
68
+ }
69
+ return {
70
+ /** Panel open state. */
71
+ get open() { return open; },
72
+ /** Highlighted option index into `options`. */
73
+ get activeIndex() { return active; },
74
+ /** The option matching the controlled value, if any. */
75
+ get selected() { return selected; },
76
+ /** Controlled value. */
77
+ get value() { return config.value(); },
78
+ isActive: (i) => i === active,
79
+ isSelected: (o) => o.value === config.value(),
80
+ setActive,
81
+ openPanel,
82
+ close,
83
+ toggle,
84
+ pick,
85
+ move,
86
+ onKeydown,
87
+ /** Spread onto the trigger <button>. */
88
+ triggerProps: () => ({
89
+ type: 'button',
90
+ 'aria-haspopup': 'listbox',
91
+ 'aria-expanded': open,
92
+ 'aria-label': config.ariaLabel?.(),
93
+ disabled: disabled(),
94
+ onclick: toggle,
95
+ onkeydown: onKeydown,
96
+ }),
97
+ /** Spread onto the listbox container. */
98
+ listboxProps: () => ({
99
+ role: 'listbox',
100
+ tabindex: -1,
101
+ }),
102
+ /** Spread onto the option at `index` (index into `options`). */
103
+ optionProps: (index) => {
104
+ const o = opts()[index];
105
+ return {
106
+ role: 'option',
107
+ tabindex: -1,
108
+ 'aria-selected': o ? o.value === config.value() : false,
109
+ 'aria-disabled': o?.disabled,
110
+ 'data-idx': index,
111
+ onclick: () => pick(index),
112
+ onpointermove: () => { if (o && !o.disabled)
113
+ active = index; },
114
+ };
115
+ },
116
+ };
117
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * createGauge - the HEADLESS core behind <SvGauge>. A gauge is a display control,
3
+ * so this core is light: value in, computed arc/needle geometry out, plus a
4
+ * `rootProps()` with `role="meter"` + `aria-valuenow/min/max`. The styled
5
+ * <SvGauge> (or your own SVG) reads the geometry and draws it.
6
+ *
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { createGauge } from '@svgrid/grid'
10
+ * const g = createGauge({ value: () => value })
11
+ * </script>
12
+ * <div {...g.rootProps()}>
13
+ * <svg viewBox={`0 0 ${g.cx * 2} ${g.cy * 2}`}>
14
+ * <path d={g.arcPath(0, g.clamped, g.r)} />
15
+ * </svg>
16
+ * </div>
17
+ * ```
18
+ */
19
+ export type GaugePoint = {
20
+ x: number;
21
+ y: number;
22
+ };
23
+ /** Reactive inputs are passed as getters so the core tracks live prop changes. */
24
+ export type GaugeConfig = {
25
+ value: () => number;
26
+ min?: () => number;
27
+ max?: () => number;
28
+ /** Sweep angle in degrees (default 270, a classic dashboard gauge). */
29
+ sweep?: () => number;
30
+ size?: () => number;
31
+ thickness?: () => number;
32
+ ariaLabel?: () => string | undefined;
33
+ };
34
+ export declare function createGauge(config: GaugeConfig): {
35
+ /** Value clamped into [min, max]. */
36
+ readonly clamped: number;
37
+ readonly startAngle: number;
38
+ readonly cx: number;
39
+ readonly cy: number;
40
+ readonly r: number;
41
+ /** Needle tip point (for `<line x2 y2>`). */
42
+ readonly needleEnd: GaugePoint;
43
+ frac: (v: number) => number;
44
+ angleOf: (v: number) => number;
45
+ polar: (angleDeg: number, radius: number) => GaugePoint;
46
+ arcPath: (v0: number, v1: number, radius: number) => string;
47
+ /** Spread onto the gauge root element. */
48
+ rootProps: () => {
49
+ role: "meter";
50
+ 'aria-valuenow': number;
51
+ 'aria-valuemin': number;
52
+ 'aria-valuemax': number;
53
+ 'aria-label': string;
54
+ };
55
+ };
56
+ export type Gauge = ReturnType<typeof createGauge>;
@@ -0,0 +1,49 @@
1
+ export function createGauge(config) {
2
+ const min = () => config.min?.() ?? 0;
3
+ const max = () => config.max?.() ?? 100;
4
+ const sweep = () => config.sweep?.() ?? 270;
5
+ const size = () => config.size?.() ?? 180;
6
+ const thickness = () => config.thickness?.() ?? 14;
7
+ const clamped = $derived(Math.min(max(), Math.max(min(), config.value())));
8
+ const startAngle = $derived(90 + (360 - sweep()) / 2); // symmetric around the bottom
9
+ const cx = $derived(size() / 2);
10
+ const cy = $derived(size() / 2);
11
+ const r = $derived(size() / 2 - thickness() / 2 - 2);
12
+ const frac = (v) => (v - min()) / (max() - min());
13
+ const angleOf = (v) => startAngle + frac(v) * sweep();
14
+ function polar(angleDeg, radius) {
15
+ const a = (angleDeg * Math.PI) / 180;
16
+ return { x: cx + radius * Math.cos(a), y: cy + radius * Math.sin(a) };
17
+ }
18
+ function arcPath(v0, v1, radius) {
19
+ const a0 = angleOf(v0);
20
+ const a1 = angleOf(v1);
21
+ const p0 = polar(a0, radius);
22
+ const p1 = polar(a1, radius);
23
+ const large = a1 - a0 > 180 ? 1 : 0;
24
+ return `M ${p0.x} ${p0.y} A ${radius} ${radius} 0 ${large} 1 ${p1.x} ${p1.y}`;
25
+ }
26
+ const needleEnd = $derived(polar(angleOf(clamped), r - thickness() / 2));
27
+ return {
28
+ /** Value clamped into [min, max]. */
29
+ get clamped() { return clamped; },
30
+ get startAngle() { return startAngle; },
31
+ get cx() { return cx; },
32
+ get cy() { return cy; },
33
+ get r() { return r; },
34
+ /** Needle tip point (for `<line x2 y2>`). */
35
+ get needleEnd() { return needleEnd; },
36
+ frac,
37
+ angleOf,
38
+ polar,
39
+ arcPath,
40
+ /** Spread onto the gauge root element. */
41
+ rootProps: () => ({
42
+ role: 'meter',
43
+ 'aria-valuenow': clamped,
44
+ 'aria-valuemin': min(),
45
+ 'aria-valuemax': max(),
46
+ 'aria-label': config.ariaLabel?.() ?? 'Gauge',
47
+ }),
48
+ };
49
+ }
@@ -0,0 +1,79 @@
1
+ /**
2
+ * createListbox - the HEADLESS core behind <SvListBox>, in the same spirit as
3
+ * `createSvGrid` for the grid: a runes-based state machine (roving active index,
4
+ * single/multi selection, full keyboard) with **prop-getters** you spread onto
5
+ * YOUR OWN markup. No styles, no DOM assumptions - render it however you like.
6
+ *
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { createListbox } from '@svgrid/grid'
10
+ * let value = $state<string | null>(null)
11
+ * const lb = createListbox({
12
+ * options: () => options, value: () => value,
13
+ * onChange: (v) => (value = v),
14
+ * })
15
+ * </script>
16
+ * <ul {...lb.rootProps()}>
17
+ * {#each options as opt, i}
18
+ * <li {...lb.optionProps(i)}>{opt.label}</li>
19
+ * {/each}
20
+ * </ul>
21
+ * ```
22
+ *
23
+ * The styled <SvListBox> is just one renderer over this core.
24
+ */
25
+ import type { ListOption } from './list-option';
26
+ export type ListboxValue = string | number | Array<string | number> | null;
27
+ /** Reactive inputs are passed as getters so the core tracks live prop changes
28
+ * (the same controlled pattern Svelte headless libraries use). */
29
+ export type ListboxConfig = {
30
+ options: () => ReadonlyArray<ListOption>;
31
+ value: () => ListboxValue;
32
+ onChange?: (value: string | number | Array<string | number>) => void;
33
+ multiple?: () => boolean;
34
+ disabled?: () => boolean;
35
+ ariaLabel?: () => string | undefined;
36
+ };
37
+ export type OptionProps = {
38
+ role: 'option';
39
+ id: string;
40
+ 'aria-selected': boolean;
41
+ 'aria-disabled': boolean | undefined;
42
+ 'data-idx': number;
43
+ 'data-active': '' | undefined;
44
+ 'data-selected': '' | undefined;
45
+ onclick: () => void;
46
+ onpointermove: () => void;
47
+ };
48
+ export type ListboxRootProps = {
49
+ role: 'listbox';
50
+ 'aria-multiselectable': boolean;
51
+ 'aria-label': string | undefined;
52
+ 'aria-disabled': boolean;
53
+ 'aria-activedescendant': string | undefined;
54
+ tabindex: number;
55
+ onkeydown: (e: KeyboardEvent) => void;
56
+ };
57
+ export type Listbox = {
58
+ /** Currently highlighted option index (roving focus). */
59
+ readonly activeIndex: number;
60
+ /** Selected values as an array (single mode -> 0 or 1 entry). */
61
+ readonly selectedValues: Array<string | number>;
62
+ isSelected: (value: string | number) => boolean;
63
+ isActive: (index: number) => boolean;
64
+ setActive: (index: number) => void;
65
+ /** Toggle (multi) / set (single) the option at `index`. */
66
+ pick: (index: number) => void;
67
+ /** Move the active highlight by `delta`, wrapping over enabled options. */
68
+ move: (delta: number) => void;
69
+ first: () => void;
70
+ last: () => void;
71
+ onKeydown: (e: KeyboardEvent) => void;
72
+ /** Spread onto the list container element. */
73
+ rootProps: () => ListboxRootProps;
74
+ /** Spread onto the option element at `index`. */
75
+ optionProps: (index: number) => OptionProps;
76
+ };
77
+ /** Normalize a listbox value to an array (pure). */
78
+ export declare function toSelectedArray(value: ListboxValue, multiple: boolean): Array<string | number>;
79
+ export declare function createListbox(config: ListboxConfig): Listbox;