@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,402 @@
1
+ /**
2
+ * createCalendar - the HEADLESS core behind <SvCalendar>, in the same spirit as
3
+ * `createListbox` / `createSvGrid`: a runes-based state machine (view page +
4
+ * drill level, roving focus, every Smart `selectionMode`, range hover preview,
5
+ * full keyboard) with **prop-getters** you spread onto YOUR OWN markup. No
6
+ * styles, no DOM assumptions. It reuses the framework-free date engine in
7
+ * ./datetime for all the actual date math / selection / restriction, so this
8
+ * file only owns the reactive component-level state + interaction wiring.
9
+ *
10
+ * ```svelte
11
+ * <script lang="ts">
12
+ * import { createCalendar } from '@svgrid/grid'
13
+ * let value = $state<Date | null>(new Date())
14
+ * const cal = createCalendar({ value: () => value, onChange: (d) => (value = d[0] ?? null) })
15
+ * </script>
16
+ * <div {...cal.gridProps()}>
17
+ * {#each cal.panels[0].matrix as week}
18
+ * {#each week as cell}
19
+ * <button {...cal.dayProps(cell, cal.panels[0].month)}>{cell.date.getDate()}</button>
20
+ * {/each}
21
+ * {/each}
22
+ * </div>
23
+ * ```
24
+ *
25
+ * The styled <SvCalendar> is one renderer over this core; it keeps the render-only
26
+ * concerns (WAAPI view-change animation, mouse-wheel navigation, DOM refs) itself.
27
+ */
28
+ import { addDays, addMonths, addYears, monthMatrix, weekdayOrder, startOfDay, isSameDay, isSameMonth, toDate, decadeRange, } from './datetime/date-core';
29
+ import { selectDate, isSelected, rangeDays, emptySelection, } from './datetime/date-selection';
30
+ import { isDisabledDay, isImportant } from './datetime/date-restrict';
31
+ const defaultLocale = () => (typeof navigator !== 'undefined' ? navigator.language : 'en-US');
32
+ /** Normalize a calendar value to an ordered day-midnight list (pure). */
33
+ export function normalizeCalendarValue(v) {
34
+ if (v == null)
35
+ return [];
36
+ const list = Array.isArray(v) ? v : [v];
37
+ const out = [];
38
+ for (const item of list) {
39
+ const d = toDate(item);
40
+ if (d)
41
+ out.push(startOfDay(d));
42
+ }
43
+ return out;
44
+ }
45
+ export function createCalendar(config) {
46
+ // --- Config accessors (with the same defaults as the styled component) ------
47
+ const selectionMode = () => config.selectionMode?.() ?? 'one';
48
+ const firstDayOfWeek = () => config.firstDayOfWeek?.() ?? 0;
49
+ const weeksCount = () => config.weeks?.() ?? 6;
50
+ const monthsCount = () => Math.max(1, config.months?.() ?? 1);
51
+ const displayMode = () => config.displayMode?.() ?? 'month';
52
+ const isDisabled = () => config.disabled?.() ?? false;
53
+ const isReadonly = () => config.readonly?.() ?? false;
54
+ const locale = () => config.locale?.() ?? defaultLocale();
55
+ const dayNameFormat = () => config.dayNameFormat?.() ?? 'short';
56
+ const monthNameFormat = () => config.monthNameFormat?.() ?? 'long';
57
+ // --- Internal reactive state ------------------------------------------------
58
+ // Seeded from `value`; range mode also tracks a pending rangeStart between the
59
+ // two clicks, which `value` alone cannot hold.
60
+ let sel = $state(emptySelection());
61
+ // The month currently shown (first panel) + the drill level.
62
+ let view = $state(startOfDay(new Date()));
63
+ let mode = $state('month');
64
+ // Roving focus + range hover preview.
65
+ let focusDate = $state(startOfDay(new Date()));
66
+ let hoverDate = $state(null);
67
+ // View-change animation is a render concern that lives in the styled
68
+ // component; the core only records why the view changed and bumps a token so
69
+ // the component can replay a keyframe on the fresh DOM.
70
+ let navDir = 'fade';
71
+ let navToken = $state(0);
72
+ const restrict = $derived({
73
+ min: config.min?.() ?? null,
74
+ max: config.max?.() ?? null,
75
+ restrictedDates: config.restrictedDates?.() ?? null,
76
+ });
77
+ const isInteractive = $derived(!isDisabled() && !isReadonly());
78
+ // --- Value sync + one-time seed of the visible page ------------------------
79
+ let lastSyncedKey = '￿'; // sentinel so the first sync always fires (incl. null value)
80
+ let seeded = false;
81
+ $effect(() => {
82
+ const dates = normalizeCalendarValue(config.value());
83
+ const key = dates.map((d) => d.getTime()).join(',');
84
+ if (key !== lastSyncedKey) {
85
+ lastSyncedKey = key;
86
+ sel = { dates, anchor: dates[dates.length - 1] ?? null, rangeStart: null };
87
+ if (!seeded) {
88
+ seeded = true;
89
+ const first = dates[0] ?? startOfDay(new Date());
90
+ view = startOfDay(first);
91
+ focusDate = startOfDay(first);
92
+ mode = displayMode();
93
+ }
94
+ }
95
+ });
96
+ // --- Intl-driven labels (cached by the browser) ----------------------------
97
+ const weekdayFmt = $derived(new Intl.DateTimeFormat(locale(), { weekday: dayNameFormat() }));
98
+ const monthTitleFmt = $derived(new Intl.DateTimeFormat(locale(), { month: monthNameFormat(), year: 'numeric' }));
99
+ const monthShortFmt = $derived(new Intl.DateTimeFormat(locale(), { month: 'short' }));
100
+ const fullDateFmt = $derived(new Intl.DateTimeFormat(locale(), { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric' }));
101
+ const weekdayHeaders = $derived(weekdayOrder(firstDayOfWeek()).map((wd) => {
102
+ // 2021-08-01 is a Sunday; offset to the requested weekday.
103
+ const d = new Date(2021, 7, 1 + wd);
104
+ return { label: weekdayFmt.format(d), weekday: wd };
105
+ }));
106
+ /** The month grids to render (1..months panels), each with its own title. */
107
+ const panels = $derived(Array.from({ length: monthsCount() }, (_, i) => {
108
+ const panelMonth = addMonths(view, i);
109
+ return { month: panelMonth, title: monthTitleFmt.format(panelMonth), matrix: monthMatrix(panelMonth, firstDayOfWeek(), weeksCount()) };
110
+ }));
111
+ // Range preview: while a range is pending (or being hovered), show the band.
112
+ const previewRange = $derived.by(() => {
113
+ if (selectionMode() !== 'range')
114
+ return null;
115
+ const start = sel.rangeStart;
116
+ if (!start || !hoverDate)
117
+ return null;
118
+ return rangeDays(start, hoverDate);
119
+ });
120
+ const inPreview = (d) => !!previewRange && previewRange.some((p) => isSameDay(p, d));
121
+ function dayState(d, panelMonth) {
122
+ return {
123
+ disabled: isDisabledDay(d, restrict),
124
+ selected: isSelected(sel.dates, d),
125
+ important: isImportant(d, config.importantDates?.() ?? null),
126
+ today: isSameDay(d, new Date()),
127
+ outside: !isSameMonth(d, panelMonth),
128
+ focused: isSameDay(d, focusDate),
129
+ preview: inPreview(d),
130
+ };
131
+ }
132
+ const title = $derived(mode === 'month'
133
+ ? monthTitleFmt.format(view)
134
+ : mode === 'year'
135
+ ? String(view.getFullYear())
136
+ : (() => {
137
+ const { start, end } = decadeRange(view.getFullYear());
138
+ return `${start} - ${end}`;
139
+ })());
140
+ // Year-mode cells (12 months) and decade-mode cells (10 years + 2 padding).
141
+ const yearCells = $derived(Array.from({ length: 12 }, (_, m) => ({
142
+ index: m,
143
+ label: monthShortFmt.format(new Date(view.getFullYear(), m, 1)),
144
+ current: m === new Date().getMonth() && view.getFullYear() === new Date().getFullYear(),
145
+ selected: sel.dates.some((d) => d.getMonth() === m && d.getFullYear() === view.getFullYear()),
146
+ })));
147
+ const decadeCells = $derived.by(() => {
148
+ const { start } = decadeRange(view.getFullYear());
149
+ return Array.from({ length: 12 }, (_, i) => {
150
+ const year = start - 1 + i;
151
+ return {
152
+ year,
153
+ outside: i === 0 || i === 11,
154
+ current: year === new Date().getFullYear(),
155
+ selected: sel.dates.some((d) => d.getFullYear() === year),
156
+ };
157
+ });
158
+ });
159
+ const canClear = $derived(selectionMode() === 'zeroOrOne' || selectionMode() === 'zeroOrMany' || selectionMode() === 'none');
160
+ // --- Actions ---------------------------------------------------------------
161
+ function commit(next) {
162
+ sel = next;
163
+ lastSyncedKey = next.dates.map((x) => x.getTime()).join(',');
164
+ config.onChange?.(next.dates.map((x) => new Date(x.getTime())));
165
+ }
166
+ function pickDay(d, e) {
167
+ if (!isInteractive || isDisabledDay(d, restrict))
168
+ return;
169
+ focusDate = startOfDay(d);
170
+ commit(selectDate(sel, d, selectionMode(), { ctrl: e?.ctrlKey || e?.metaKey, shift: e?.shiftKey }, firstDayOfWeek()));
171
+ }
172
+ function setView(next, nextMode = mode) {
173
+ view = startOfDay(next);
174
+ mode = nextMode;
175
+ navToken++; // signal the styled component to replay the view-change animation
176
+ config.onNavigate?.(view, mode);
177
+ }
178
+ function drillUp() {
179
+ navDir = 'drillUp';
180
+ if (mode === 'month')
181
+ setView(view, 'year');
182
+ else if (mode === 'year')
183
+ setView(view, 'decade');
184
+ }
185
+ function step(dir) {
186
+ navDir = dir === 1 ? 'next' : 'prev';
187
+ if (mode === 'month')
188
+ setView(addMonths(view, dir));
189
+ else if (mode === 'year')
190
+ setView(addYears(view, dir));
191
+ else
192
+ setView(addYears(view, dir * 10));
193
+ }
194
+ function pickMonth(monthIndex) {
195
+ navDir = 'drillDown';
196
+ setView(new Date(view.getFullYear(), monthIndex, 1), 'month');
197
+ focusDate = new Date(view.getFullYear(), monthIndex, Math.min(focusDate.getDate(), 28));
198
+ }
199
+ function pickYear(year) {
200
+ navDir = 'drillDown';
201
+ setView(new Date(year, view.getMonth(), 1), 'year');
202
+ }
203
+ function goToday() {
204
+ navDir = 'fade';
205
+ const t = new Date();
206
+ setView(t, 'month');
207
+ focusDate = startOfDay(t);
208
+ if (isInteractive)
209
+ pickDay(t);
210
+ }
211
+ const startOfMonthSafe = (d) => new Date(d.getFullYear(), d.getMonth(), 1);
212
+ function applyPreset(p) {
213
+ if (!isInteractive)
214
+ return;
215
+ const resolved = typeof p.value === 'function' ? p.value() : p.value;
216
+ const isRange = Array.isArray(resolved);
217
+ const pair = (isRange ? resolved : [resolved, resolved]);
218
+ const start = toDate(pair[0]);
219
+ const end = toDate(pair[1]);
220
+ if (!start)
221
+ return;
222
+ focusDate = startOfDay(start);
223
+ navDir = 'fade';
224
+ view = startOfMonthSafe(start);
225
+ navToken++;
226
+ if (isRange && end) {
227
+ commit({ dates: rangeDays(start, end), anchor: startOfDay(end), rangeStart: null });
228
+ }
229
+ else {
230
+ commit({ dates: [startOfDay(start)], anchor: startOfDay(start), rangeStart: null });
231
+ }
232
+ }
233
+ function clearSelection() {
234
+ if (!isInteractive)
235
+ return;
236
+ commit(emptySelection());
237
+ }
238
+ const setHover = (d) => (hoverDate = d);
239
+ // --- Keyboard --------------------------------------------------------------
240
+ function onNonMonthKeydown(e) {
241
+ if (e.key === 'Enter' || e.key === ' ') {
242
+ e.preventDefault();
243
+ if (mode === 'year')
244
+ pickMonth(focusDate.getMonth());
245
+ else
246
+ pickYear(focusDate.getFullYear());
247
+ }
248
+ }
249
+ function onKeydown(e) {
250
+ if (mode !== 'month')
251
+ return onNonMonthKeydown(e);
252
+ let next = null;
253
+ switch (e.key) {
254
+ case 'ArrowLeft':
255
+ next = addDays(focusDate, -1);
256
+ break;
257
+ case 'ArrowRight':
258
+ next = addDays(focusDate, 1);
259
+ break;
260
+ case 'ArrowUp':
261
+ next = addDays(focusDate, -7);
262
+ break;
263
+ case 'ArrowDown':
264
+ next = addDays(focusDate, 7);
265
+ break;
266
+ case 'Home':
267
+ next = addDays(focusDate, -((focusDate.getDay() - firstDayOfWeek() + 7) % 7));
268
+ break;
269
+ case 'End':
270
+ next = addDays(focusDate, 6 - ((focusDate.getDay() - firstDayOfWeek() + 7) % 7));
271
+ break;
272
+ case 'PageUp':
273
+ next = e.shiftKey ? addYears(focusDate, -1) : addMonths(focusDate, -1);
274
+ break;
275
+ case 'PageDown':
276
+ next = e.shiftKey ? addYears(focusDate, 1) : addMonths(focusDate, 1);
277
+ break;
278
+ case 'Enter':
279
+ case ' ':
280
+ e.preventDefault();
281
+ pickDay(focusDate, e);
282
+ return;
283
+ default:
284
+ return;
285
+ }
286
+ if (next) {
287
+ e.preventDefault();
288
+ focusDate = startOfDay(next);
289
+ if (!isSameMonth(focusDate, view))
290
+ setView(focusDate, 'month');
291
+ }
292
+ }
293
+ return {
294
+ // --- Reactive state -------------------------------------------------------
295
+ get view() { return view; },
296
+ get mode() { return mode; },
297
+ get title() { return title; },
298
+ get panels() { return panels; },
299
+ get weekdayHeaders() { return weekdayHeaders; },
300
+ get yearCells() { return yearCells; },
301
+ get decadeCells() { return decadeCells; },
302
+ get selectedDates() { return sel.dates; },
303
+ get previewRange() { return previewRange; },
304
+ get focusDate() { return focusDate; },
305
+ get hoverDate() { return hoverDate; },
306
+ get canClear() { return canClear; },
307
+ get isInteractive() { return isInteractive; },
308
+ /** Increments on every view/drill change; the styled component watches it to
309
+ * replay its WAAPI animation on the updated DOM. */
310
+ get navToken() { return navToken; },
311
+ /** Why the view last changed (read untracked from the animation effect). */
312
+ get navDir() { return navDir; },
313
+ // --- Derived helpers ------------------------------------------------------
314
+ dayState,
315
+ inPreview,
316
+ formatFullDate: (d) => fullDateFmt.format(d),
317
+ formatMonthTitle: (d) => monthTitleFmt.format(d),
318
+ // --- Actions --------------------------------------------------------------
319
+ pickDay,
320
+ step,
321
+ drillUp,
322
+ pickMonth,
323
+ pickYear,
324
+ goToday,
325
+ applyPreset,
326
+ clearSelection,
327
+ setHover,
328
+ onKeydown,
329
+ // --- Prop-getters (spread onto YOUR markup) -------------------------------
330
+ /** Previous / Next navigation button. */
331
+ navProps(dir) {
332
+ return {
333
+ type: 'button',
334
+ disabled: isDisabled(),
335
+ 'aria-label': dir === 1 ? 'Next' : 'Previous',
336
+ onclick: () => step(dir),
337
+ };
338
+ },
339
+ /** The centered title button that drills up (month -> year -> decade). */
340
+ titleProps() {
341
+ return {
342
+ type: 'button',
343
+ disabled: isDisabled() || mode === 'decade',
344
+ 'aria-live': 'polite',
345
+ onclick: drillUp,
346
+ };
347
+ },
348
+ /** The grid container: keyboard + ARIA. Works for month, year and decade
349
+ * modes (the handler dispatches on the current mode). */
350
+ gridProps() {
351
+ return {
352
+ role: 'grid',
353
+ 'aria-label': title,
354
+ tabindex: -1,
355
+ onkeydown: onKeydown,
356
+ };
357
+ },
358
+ /** A single day button in the month grid. Combine with `dayState` for the
359
+ * visual class flags. */
360
+ dayProps(cell, panelMonth) {
361
+ const st = dayState(cell.date, panelMonth);
362
+ return {
363
+ type: 'button',
364
+ role: 'gridcell',
365
+ 'aria-selected': st.selected,
366
+ 'aria-disabled': st.disabled,
367
+ 'aria-current': st.today ? 'date' : undefined,
368
+ 'aria-label': fullDateFmt.format(cell.date),
369
+ title: config.dateTooltip?.()?.(cell.date) ?? undefined,
370
+ tabindex: st.focused ? 0 : -1,
371
+ disabled: st.disabled || isDisabled(),
372
+ onclick: (e) => pickDay(cell.date, e),
373
+ onpointerenter: () => setHover(cell.date),
374
+ onpointerleave: () => setHover(null),
375
+ };
376
+ },
377
+ /** A month cell in year mode. */
378
+ monthCellProps(monthIndex) {
379
+ return {
380
+ type: 'button',
381
+ disabled: isDisabled(),
382
+ onclick: () => pickMonth(monthIndex),
383
+ };
384
+ },
385
+ /** A year cell in decade mode. */
386
+ yearCellProps(year) {
387
+ return {
388
+ type: 'button',
389
+ disabled: isDisabled(),
390
+ onclick: () => pickYear(year),
391
+ };
392
+ },
393
+ /** A preset shortcut button. */
394
+ presetProps(preset) {
395
+ return {
396
+ type: 'button',
397
+ disabled: !isInteractive,
398
+ onclick: () => applyPreset(preset),
399
+ };
400
+ },
401
+ };
402
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * createCheckbox - the HEADLESS core behind <SvCheckBox>: a controlled tri-state
3
+ * checkbox (checked / unchecked / indeterminate, ARIA `checkbox` with
4
+ * `aria-checked="mixed"`), exposed as **prop-getters** you spread onto YOUR OWN
5
+ * markup. A native <button> supplies Space/Enter activation.
6
+ *
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { createCheckbox } from '@svgrid/grid'
10
+ * let checked = $state(false)
11
+ * const cb = createCheckbox({ checked: () => checked, onChange: (v) => (checked = v) })
12
+ * </script>
13
+ * <button {...cb.boxProps()}>{cb.checked ? '✓' : ''}</button>
14
+ * ```
15
+ *
16
+ * The styled <SvCheckBox> is just one renderer over this core.
17
+ */
18
+ export type CheckboxConfig = {
19
+ checked: () => boolean;
20
+ indeterminate?: () => boolean;
21
+ onChange?: (checked: boolean) => void;
22
+ disabled?: () => boolean;
23
+ ariaLabel?: () => string | undefined;
24
+ };
25
+ export type CheckboxBoxProps = {
26
+ type: 'button';
27
+ role: 'checkbox';
28
+ 'aria-checked': boolean | 'mixed';
29
+ 'aria-label': string | undefined;
30
+ disabled: boolean;
31
+ 'data-checked': '' | undefined;
32
+ 'data-indeterminate': '' | undefined;
33
+ onclick: () => void;
34
+ };
35
+ export type Checkbox = {
36
+ /** True only when fully checked (never in the indeterminate state). */
37
+ readonly checked: boolean;
38
+ readonly indeterminate: boolean;
39
+ readonly disabled: boolean;
40
+ /** Indeterminate resolves to checked; otherwise flips checked. */
41
+ toggle: () => void;
42
+ /** Spread onto the checkbox element. */
43
+ boxProps: () => CheckboxBoxProps;
44
+ };
45
+ export declare function createCheckbox(config: CheckboxConfig): Checkbox;
@@ -0,0 +1,26 @@
1
+ export function createCheckbox(config) {
2
+ const checked = () => config.checked();
3
+ const indeterminate = () => config.indeterminate?.() ?? false;
4
+ const disabled = () => config.disabled?.() ?? false;
5
+ function toggle() {
6
+ if (disabled())
7
+ return;
8
+ config.onChange?.(indeterminate() ? true : !checked());
9
+ }
10
+ return {
11
+ get checked() { return checked(); },
12
+ get indeterminate() { return indeterminate(); },
13
+ get disabled() { return disabled(); },
14
+ toggle,
15
+ boxProps: () => ({
16
+ type: 'button',
17
+ role: 'checkbox',
18
+ 'aria-checked': indeterminate() ? 'mixed' : checked(),
19
+ 'aria-label': config.ariaLabel?.(),
20
+ disabled: disabled(),
21
+ 'data-checked': checked() && !indeterminate() ? '' : undefined,
22
+ 'data-indeterminate': indeterminate() ? '' : undefined,
23
+ onclick: toggle,
24
+ }),
25
+ };
26
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * createColorInput - the HEADLESS core behind <SvColorInput>: a runes-based
3
+ * state machine that owns hex normalization, the palette, the draft hex field
4
+ * and the open/close popover state, exposed as **prop-getters** you spread onto
5
+ * YOUR OWN markup. DOM measurement / portalling stays in the renderer, NOT here.
6
+ *
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { createColorInput } from '@svgrid/grid'
10
+ * let value = $state('#3b82f6')
11
+ * const col = createColorInput({ value: () => value, onChange: (v) => (value = v) })
12
+ * </script>
13
+ * <button {...col.swatchProps()}>{col.normalized}</button>
14
+ * {#if col.popover.open}<div>...palette...</div>{/if}
15
+ * ```
16
+ *
17
+ * The styled <SvColorInput> is just one renderer over this core (it adds the
18
+ * portal + anchored positioning).
19
+ */
20
+ export declare const DEFAULT_PALETTE: string[];
21
+ /** Normalize a hex string (#abc / #aabbcc / abc) to lower-case #aabbcc (pure). */
22
+ export declare function normalizeHex(h: string): string | null;
23
+ /** Reactive inputs are passed as getters so the core tracks live prop changes. */
24
+ export type ColorInputConfig = {
25
+ value: () => string;
26
+ onChange?: (hex: string) => void;
27
+ disabled?: () => boolean;
28
+ readonly?: () => boolean;
29
+ palette?: () => string[];
30
+ ariaLabel?: () => string | undefined;
31
+ };
32
+ export declare function createColorInput(config: ColorInputConfig): {
33
+ /** The normalized current color (#aabbcc). */
34
+ readonly normalized: string;
35
+ /** Preset swatches. */
36
+ readonly palette: string[];
37
+ /** Draft hex text for the panel field (settable for `bind:value`). */
38
+ hexDraft: string;
39
+ /** Whether interaction is allowed. */
40
+ readonly isInteractive: boolean;
41
+ /** Popover open state + open/close/toggle controls. */
42
+ popover: {
43
+ readonly open: boolean;
44
+ show: () => void;
45
+ close: () => void;
46
+ toggle: () => void;
47
+ };
48
+ pick: (hex: string) => void;
49
+ commitHex: () => void;
50
+ isActive: (c: string) => boolean;
51
+ /** Spread onto the swatch trigger button. */
52
+ swatchProps: () => {
53
+ type: "button";
54
+ 'aria-haspopup': "dialog";
55
+ 'aria-expanded': boolean;
56
+ 'aria-label': string;
57
+ disabled: boolean;
58
+ onclick: () => void;
59
+ };
60
+ /** Spread onto a palette chip button for color `c`. */
61
+ chipProps: (c: string) => {
62
+ type: "button";
63
+ 'aria-label': string;
64
+ onclick: () => void;
65
+ };
66
+ };
67
+ export type ColorInputCore = ReturnType<typeof createColorInput>;
@@ -0,0 +1,100 @@
1
+ /**
2
+ * createColorInput - the HEADLESS core behind <SvColorInput>: a runes-based
3
+ * state machine that owns hex normalization, the palette, the draft hex field
4
+ * and the open/close popover state, exposed as **prop-getters** you spread onto
5
+ * YOUR OWN markup. DOM measurement / portalling stays in the renderer, NOT here.
6
+ *
7
+ * ```svelte
8
+ * <script lang="ts">
9
+ * import { createColorInput } from '@svgrid/grid'
10
+ * let value = $state('#3b82f6')
11
+ * const col = createColorInput({ value: () => value, onChange: (v) => (value = v) })
12
+ * </script>
13
+ * <button {...col.swatchProps()}>{col.normalized}</button>
14
+ * {#if col.popover.open}<div>...palette...</div>{/if}
15
+ * ```
16
+ *
17
+ * The styled <SvColorInput> is just one renderer over this core (it adds the
18
+ * portal + anchored positioning).
19
+ */
20
+ export const DEFAULT_PALETTE = [
21
+ '#ef4444', '#f97316', '#f59e0b', '#eab308', '#84cc16', '#22c55e', '#10b981', '#14b8a6',
22
+ '#06b6d4', '#0ea5e9', '#3b82f6', '#6366f1', '#8b5cf6', '#a855f7', '#d946ef', '#ec4899',
23
+ '#f43f5e', '#64748b', '#0f172a', '#ffffff',
24
+ ];
25
+ /** Normalize a hex string (#abc / #aabbcc / abc) to lower-case #aabbcc (pure). */
26
+ export function normalizeHex(h) {
27
+ let s = h.trim();
28
+ if (!s.startsWith('#'))
29
+ s = '#' + s;
30
+ if (/^#[0-9a-fA-F]{3}$/.test(s))
31
+ s = '#' + s.slice(1).split('').map((c) => c + c).join('');
32
+ return /^#[0-9a-fA-F]{6}$/.test(s) ? s.toLowerCase() : null;
33
+ }
34
+ export function createColorInput(config) {
35
+ const disabled = () => config.disabled?.() ?? false;
36
+ const readonly = () => config.readonly?.() ?? false;
37
+ const isInteractive = $derived(!disabled() && !readonly());
38
+ let open = $state(false);
39
+ let hexDraft = $state('');
40
+ const normalized = $derived(normalizeHex(config.value()) ?? '#000000');
41
+ function openPanel() {
42
+ if (!isInteractive || open)
43
+ return;
44
+ hexDraft = normalized;
45
+ open = true;
46
+ }
47
+ function close() { open = false; }
48
+ function toggle() { open ? (open = false) : openPanel(); }
49
+ function pick(hex) {
50
+ const n = normalizeHex(hex);
51
+ if (n)
52
+ config.onChange?.(n);
53
+ }
54
+ function commitHex() {
55
+ const n = normalizeHex(hexDraft);
56
+ if (n)
57
+ config.onChange?.(n);
58
+ else
59
+ hexDraft = normalized;
60
+ }
61
+ const isActive = (c) => normalizeHex(c) === normalized;
62
+ // Popover control surface (open state + show/close/toggle), no DOM measurement.
63
+ const popover = {
64
+ get open() { return open; },
65
+ show: openPanel,
66
+ close,
67
+ toggle,
68
+ };
69
+ return {
70
+ /** The normalized current color (#aabbcc). */
71
+ get normalized() { return normalized; },
72
+ /** Preset swatches. */
73
+ get palette() { return config.palette?.() ?? DEFAULT_PALETTE; },
74
+ /** Draft hex text for the panel field (settable for `bind:value`). */
75
+ get hexDraft() { return hexDraft; },
76
+ set hexDraft(v) { hexDraft = v; },
77
+ /** Whether interaction is allowed. */
78
+ get isInteractive() { return isInteractive; },
79
+ /** Popover open state + open/close/toggle controls. */
80
+ popover,
81
+ pick,
82
+ commitHex,
83
+ isActive,
84
+ /** Spread onto the swatch trigger button. */
85
+ swatchProps: () => ({
86
+ type: 'button',
87
+ 'aria-haspopup': 'dialog',
88
+ 'aria-expanded': open,
89
+ 'aria-label': config.ariaLabel?.() ?? `Color ${normalized}`,
90
+ disabled: !isInteractive,
91
+ onclick: toggle,
92
+ }),
93
+ /** Spread onto a palette chip button for color `c`. */
94
+ chipProps: (c) => ({
95
+ type: 'button',
96
+ 'aria-label': c,
97
+ onclick: () => { pick(c); open = false; },
98
+ }),
99
+ };
100
+ }