@spaethtech/svelte-ui 0.1.10 → 0.3.1-dev.10.ff03a8d

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 (218) hide show
  1. package/.claude/skills/svelte-ui/SKILL.md +118 -0
  2. package/.claude/skills/themes/SKILL.md +303 -0
  3. package/CLAUDE.md +248 -0
  4. package/README.md +180 -42
  5. package/dist/components/Alert.svelte +119 -0
  6. package/dist/components/Alert.svelte.d.ts +29 -0
  7. package/dist/components/Badge/Badge.svelte +142 -69
  8. package/dist/components/Badge/Badge.svelte.d.ts +26 -6
  9. package/dist/components/Badge/index.d.ts +1 -1
  10. package/dist/components/Badge/index.js +1 -1
  11. package/dist/components/Banner.svelte +133 -0
  12. package/dist/components/Banner.svelte.d.ts +31 -0
  13. package/dist/components/Button.svelte +240 -0
  14. package/dist/components/Button.svelte.d.ts +33 -0
  15. package/dist/components/ButtonDropdown.svelte +145 -0
  16. package/dist/components/ButtonDropdown.svelte.d.ts +31 -0
  17. package/dist/components/Calendar.svelte +259 -0
  18. package/dist/components/Calendar.svelte.d.ts +27 -0
  19. package/dist/components/Card.svelte +79 -0
  20. package/dist/components/Card.svelte.d.ts +17 -0
  21. package/dist/components/CardBody.svelte +41 -0
  22. package/dist/components/CardBody.svelte.d.ts +17 -0
  23. package/dist/components/CardFooter.svelte +51 -0
  24. package/dist/components/CardFooter.svelte.d.ts +16 -0
  25. package/dist/components/CardHeader.svelte +51 -0
  26. package/dist/components/CardHeader.svelte.d.ts +16 -0
  27. package/dist/components/Checkbox.svelte +131 -0
  28. package/dist/components/Checkbox.svelte.d.ts +16 -0
  29. package/dist/components/ConfirmDialog.svelte +103 -0
  30. package/dist/components/ConfirmDialog.svelte.d.ts +33 -0
  31. package/dist/components/DataTable.svelte +518 -0
  32. package/dist/components/DataTable.svelte.d.ts +74 -0
  33. package/dist/components/DatePicker.svelte +312 -0
  34. package/dist/components/DatePicker.svelte.d.ts +32 -0
  35. package/dist/components/DateTimeInput.svelte +93 -0
  36. package/dist/components/DateTimeInput.svelte.d.ts +20 -0
  37. package/dist/components/Dialog.svelte +151 -0
  38. package/dist/components/Dialog.svelte.d.ts +30 -0
  39. package/dist/components/{EmailInput/EmailInput.svelte → EmailInput.svelte} +17 -8
  40. package/dist/components/EmailInput.svelte.d.ts +21 -0
  41. package/dist/components/Input.svelte +369 -0
  42. package/dist/components/Input.svelte.d.ts +35 -0
  43. package/dist/components/{List/List.svelte → List.svelte} +194 -133
  44. package/dist/components/{List/List.svelte.d.ts → List.svelte.d.ts} +9 -1
  45. package/dist/components/Menu.svelte +117 -0
  46. package/dist/components/Menu.svelte.d.ts +20 -0
  47. package/dist/components/NotesEditor.svelte +127 -0
  48. package/dist/components/NotesEditor.svelte.d.ts +17 -0
  49. package/dist/components/{NumberInput/NumberInput.svelte → NumberInput.svelte} +146 -105
  50. package/dist/components/{NumberInput/NumberInput.svelte.d.ts → NumberInput.svelte.d.ts} +11 -4
  51. package/dist/components/PasswordInput.svelte +52 -0
  52. package/dist/components/PasswordInput.svelte.d.ts +20 -0
  53. package/dist/components/Popup.svelte +140 -0
  54. package/dist/components/Popup.svelte.d.ts +31 -0
  55. package/dist/components/Query.svelte +284 -0
  56. package/dist/components/Query.svelte.d.ts +15 -0
  57. package/dist/components/{Rating/Rating.svelte → Rating.svelte} +19 -10
  58. package/dist/components/Rating.svelte.d.ts +13 -0
  59. package/dist/components/{SearchInput/SearchInput.svelte → SearchInput.svelte} +19 -8
  60. package/dist/components/{SearchInput/SearchInput.svelte.d.ts → SearchInput.svelte.d.ts} +9 -2
  61. package/dist/components/{Select/Select.svelte → Select.svelte} +48 -76
  62. package/dist/components/{Select/Select.svelte.d.ts → Select.svelte.d.ts} +11 -1
  63. package/dist/components/SideBarMenu/SideBarMenu.svelte +724 -0
  64. package/dist/components/SideBarMenu/SideBarMenu.svelte.d.ts +109 -0
  65. package/dist/components/SideBarMenu/index.d.ts +2 -0
  66. package/dist/components/SideBarMenu/index.js +1 -0
  67. package/dist/components/TabStrip/TabStrip.svelte +384 -0
  68. package/dist/components/TabStrip/TabStrip.svelte.d.ts +50 -0
  69. package/dist/components/TabStrip/index.d.ts +3 -0
  70. package/dist/components/TabStrip/index.js +2 -0
  71. package/dist/components/{TextArea/TextArea.svelte → TextArea.svelte} +143 -112
  72. package/dist/components/TextArea.svelte.d.ts +38 -0
  73. package/dist/components/ThemeSelector.svelte +81 -0
  74. package/dist/components/ThemeSelector.svelte.d.ts +10 -0
  75. package/dist/components/TimePicker.svelte +148 -0
  76. package/dist/components/TimePicker.svelte.d.ts +28 -0
  77. package/dist/components/TimeRangeInput.svelte +203 -0
  78. package/dist/components/TimeRangeInput.svelte.d.ts +29 -0
  79. package/dist/components/TimeSpinner.svelte +202 -0
  80. package/dist/components/TimeSpinner.svelte.d.ts +26 -0
  81. package/dist/components/Toast/Toaster.svelte +51 -0
  82. package/dist/components/Toast/Toaster.svelte.d.ts +12 -0
  83. package/dist/components/Toast/toast.svelte.d.ts +29 -0
  84. package/dist/components/Toast/toast.svelte.js +27 -0
  85. package/dist/components/Toggle.svelte +93 -0
  86. package/dist/components/Toggle.svelte.d.ts +15 -0
  87. package/dist/data/dataset.d.ts +42 -0
  88. package/dist/data/dataset.js +3 -0
  89. package/dist/data/index.d.ts +3 -0
  90. package/dist/data/index.js +3 -0
  91. package/dist/data/query/ast.d.ts +62 -0
  92. package/dist/data/query/ast.js +12 -0
  93. package/dist/data/query/index.d.ts +3 -0
  94. package/dist/data/query/index.js +3 -0
  95. package/dist/data/query/lexer.d.ts +33 -0
  96. package/dist/data/query/lexer.js +225 -0
  97. package/dist/data/query/parser.d.ts +11 -0
  98. package/dist/data/query/parser.js +252 -0
  99. package/dist/data/table/grid.svelte.d.ts +57 -0
  100. package/dist/data/table/grid.svelte.js +139 -0
  101. package/dist/data/table/index.d.ts +2 -0
  102. package/dist/data/table/index.js +2 -0
  103. package/dist/data/table/types.d.ts +79 -0
  104. package/dist/index.d.ts +49 -22
  105. package/dist/index.js +46 -21
  106. package/dist/positioning/anchored.d.ts +61 -0
  107. package/dist/positioning/anchored.js +122 -0
  108. package/dist/positioning/tooltip.d.ts +41 -0
  109. package/dist/positioning/tooltip.js +147 -0
  110. package/dist/theme.css +205 -0
  111. package/dist/types/breakpoints.d.ts +24 -0
  112. package/dist/types/breakpoints.js +13 -0
  113. package/dist/types/responsive.d.ts +32 -0
  114. package/dist/types/responsive.js +54 -0
  115. package/dist/types/sizes.d.ts +10 -3
  116. package/dist/types/time.d.ts +19 -0
  117. package/dist/types/time.js +10 -0
  118. package/dist/types/variants.d.ts +8 -1
  119. package/dist/types/variants.js +16 -1
  120. package/docs/components.md +255 -0
  121. package/docs/examples.md +323 -0
  122. package/docs/paradigm.md +240 -0
  123. package/docs/themes.md +170 -0
  124. package/docs/usage.md +450 -0
  125. package/package.json +97 -63
  126. package/dist/components/Button/Button.svelte +0 -172
  127. package/dist/components/Button/Button.svelte.d.ts +0 -32
  128. package/dist/components/Button/index.d.ts +0 -1
  129. package/dist/components/Button/index.js +0 -1
  130. package/dist/components/Dialog/Dialog.svelte +0 -101
  131. package/dist/components/Dialog/Dialog.svelte.d.ts +0 -18
  132. package/dist/components/Dialog/index.d.ts +0 -1
  133. package/dist/components/Dialog/index.js +0 -1
  134. package/dist/components/ElementManager/ElementManager.svelte +0 -397
  135. package/dist/components/ElementManager/ElementManager.svelte.d.ts +0 -43
  136. package/dist/components/ElementManager/index.d.ts +0 -1
  137. package/dist/components/ElementManager/index.js +0 -1
  138. package/dist/components/EmailInput/EmailInput.svelte.d.ts +0 -14
  139. package/dist/components/EmailInput/index.d.ts +0 -1
  140. package/dist/components/EmailInput/index.js +0 -1
  141. package/dist/components/Icon/Icon.svelte +0 -74
  142. package/dist/components/Icon/Icon.svelte.d.ts +0 -13
  143. package/dist/components/Icon/index.d.ts +0 -1
  144. package/dist/components/Icon/index.js +0 -1
  145. package/dist/components/Input/Input.svelte +0 -268
  146. package/dist/components/Input/Input.svelte.d.ts +0 -18
  147. package/dist/components/Input/index.d.ts +0 -1
  148. package/dist/components/Input/index.js +0 -1
  149. package/dist/components/List/index.d.ts +0 -1
  150. package/dist/components/List/index.js +0 -1
  151. package/dist/components/ListItem/ListItem.svelte +0 -175
  152. package/dist/components/ListItem/ListItem.svelte.d.ts +0 -24
  153. package/dist/components/ListItem/index.d.ts +0 -2
  154. package/dist/components/ListItem/index.js +0 -2
  155. package/dist/components/ListView/ListView.svelte +0 -463
  156. package/dist/components/ListView/ListView.svelte.d.ts +0 -37
  157. package/dist/components/ListView/index.d.ts +0 -2
  158. package/dist/components/ListView/index.js +0 -2
  159. package/dist/components/NodeGraph/BaseNode.d.ts +0 -57
  160. package/dist/components/NodeGraph/BaseNode.js +0 -30
  161. package/dist/components/NodeGraph/Edge.svelte +0 -60
  162. package/dist/components/NodeGraph/Edge.svelte.d.ts +0 -16
  163. package/dist/components/NodeGraph/ExpressionEvaluator.d.ts +0 -82
  164. package/dist/components/NodeGraph/ExpressionEvaluator.js +0 -152
  165. package/dist/components/NodeGraph/Node.svelte +0 -100
  166. package/dist/components/NodeGraph/Node.svelte.d.ts +0 -10
  167. package/dist/components/NodeGraph/NodeGraph.svelte +0 -52
  168. package/dist/components/NodeGraph/NodeGraph.svelte.d.ts +0 -14
  169. package/dist/components/NodeGraph/NodeInstance.svelte.d.ts +0 -80
  170. package/dist/components/NodeGraph/NodeInstance.svelte.js +0 -241
  171. package/dist/components/NodeGraph/NodePort.svelte +0 -75
  172. package/dist/components/NodeGraph/NodePort.svelte.d.ts +0 -11
  173. package/dist/components/NodeGraph/decorators.d.ts +0 -81
  174. package/dist/components/NodeGraph/decorators.js +0 -148
  175. package/dist/components/NodeGraph/index.d.ts +0 -9
  176. package/dist/components/NodeGraph/index.js +0 -9
  177. package/dist/components/NodeGraph/types.d.ts +0 -94
  178. package/dist/components/NodeGraph/types.js +0 -33
  179. package/dist/components/NotesEditor/NotesEditor.svelte +0 -203
  180. package/dist/components/NotesEditor/NotesEditor.svelte.d.ts +0 -9
  181. package/dist/components/NotesEditor/index.d.ts +0 -1
  182. package/dist/components/NotesEditor/index.js +0 -1
  183. package/dist/components/NumberInput/index.d.ts +0 -1
  184. package/dist/components/NumberInput/index.js +0 -1
  185. package/dist/components/PasswordInput/PasswordInput.svelte +0 -41
  186. package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +0 -13
  187. package/dist/components/PasswordInput/index.d.ts +0 -1
  188. package/dist/components/PasswordInput/index.js +0 -1
  189. package/dist/components/Popup/index.d.ts +0 -1
  190. package/dist/components/Popup/index.js +0 -1
  191. package/dist/components/Rating/Rating.svelte.d.ts +0 -7
  192. package/dist/components/Rating/index.d.ts +0 -1
  193. package/dist/components/Rating/index.js +0 -1
  194. package/dist/components/ScrollView/ScrollView.svelte +0 -285
  195. package/dist/components/ScrollView/ScrollView.svelte.d.ts +0 -19
  196. package/dist/components/ScrollView/index.d.ts +0 -1
  197. package/dist/components/ScrollView/index.js +0 -1
  198. package/dist/components/SearchInput/index.d.ts +0 -1
  199. package/dist/components/SearchInput/index.js +0 -1
  200. package/dist/components/Select/index.d.ts +0 -1
  201. package/dist/components/Select/index.js +0 -1
  202. package/dist/components/TextArea/TextArea.svelte.d.ts +0 -19
  203. package/dist/components/TextArea/index.d.ts +0 -1
  204. package/dist/components/TextArea/index.js +0 -1
  205. package/dist/components/ThemeSelector/ThemeSelector.svelte +0 -64
  206. package/dist/components/ThemeSelector/ThemeSelector.svelte.d.ts +0 -3
  207. package/dist/components/ThemeSelector/index.d.ts +0 -1
  208. package/dist/components/ThemeSelector/index.js +0 -1
  209. package/dist/components/TooltipHandler/TooltipHandler.svelte +0 -593
  210. package/dist/components/TooltipHandler/TooltipHandler.svelte.d.ts +0 -10
  211. package/dist/components/TooltipHandler/index.d.ts +0 -1
  212. package/dist/components/TooltipHandler/index.js +0 -1
  213. package/dist/styles/sizes.d.ts +0 -78
  214. package/dist/styles/sizes.js +0 -120
  215. package/dist/styles/variants.d.ts +0 -106
  216. package/dist/styles/variants.js +0 -194
  217. package/dist/types/ManagerTypes.d.ts +0 -42
  218. /package/dist/{types/ManagerTypes.js → data/table/types.js} +0 -0
@@ -0,0 +1,312 @@
1
+ <!--
2
+ /**
3
+ * DatePicker — a themed date field (single date, datetime, or date RANGE) that follows the library
4
+ * look end to end: an {@link Input}-framed trigger + a {@link Popup} (top layer) holding a
5
+ * {@link Calendar}, with a compact {@link TimeSpinner} tucked underneath in `time` mode. A hidden
6
+ * input carries the ISO value for forms (single mode).
7
+ *
8
+ * <DatePicker bind:value={dateStr} /> single value: 'YYYY-MM-DD'
9
+ * <DatePicker time bind:value={dtStr} /> datetime value: 'YYYY-MM-DDTHH:MM'
10
+ * <DatePicker range bind:value={{start,end}} /> range value: { start, end }
11
+ *
12
+ * `variant` accents the selection/actions; `size` scales the field, calendar, spinner, and popup.
13
+ */
14
+ -->
15
+ <script lang="ts">
16
+ import type { Size } from "../types/sizes.js";
17
+ import { responsiveClasses, type Responsive } from "../types/responsive.js";
18
+ import { variantToken, type Variant } from "../types/variants.js";
19
+ import { resolveStep, type StepSpec } from "../types/time.js";
20
+ import Input from "./Input.svelte";
21
+ import Popup from "./Popup.svelte";
22
+ import Calendar from "./Calendar.svelte";
23
+ import TimeSpinner from "./TimeSpinner.svelte";
24
+ import IconCalendar from "~icons/mdi/calendar-outline";
25
+ import IconClose from "~icons/mdi/close";
26
+
27
+ type RangeValue = { start: string | null; end: string | null };
28
+ interface Props {
29
+ value?: string | RangeValue | null;
30
+ range?: boolean;
31
+ time?: boolean;
32
+ /** `time` mode: column interval — a number = minute step, or `{ hour, minute, second }`. Default 1. */
33
+ step?: StepSpec;
34
+ /** `time` mode: add a seconds column; the time part becomes `HH:MM:SS`. Default false. */
35
+ seconds?: boolean;
36
+ /** `time` mode: show the AM/PM meridiem (12h). `false` = 24h. Default true; supply from app config. */
37
+ meridiem?: boolean;
38
+ min?: string;
39
+ max?: string;
40
+ weekStart?: 0 | 1;
41
+ locale?: string;
42
+ placeholder?: string;
43
+ disabled?: boolean;
44
+ size?: Responsive<Size>;
45
+ variant?: Variant;
46
+ class?: string;
47
+ name?: string;
48
+ }
49
+
50
+ let {
51
+ value = $bindable(),
52
+ range = false,
53
+ time = false,
54
+ step = 1,
55
+ seconds = false,
56
+ meridiem = true,
57
+ min,
58
+ max,
59
+ weekStart,
60
+ locale = typeof navigator !== "undefined" ? navigator.language : "en-US",
61
+ placeholder = range ? "Select dates" : time ? "Select date & time" : "Select date",
62
+ disabled = false,
63
+ size = "md",
64
+ variant = "primary",
65
+ class: cls = "",
66
+ name,
67
+ }: Props = $props();
68
+
69
+ const accent = $derived(`var(${variantToken[variant]})`);
70
+ const pad = $derived(responsiveClasses(size, { sm: "p-1.5", md: "p-2", lg: "p-3" }));
71
+ const footText = $derived(
72
+ responsiveClasses(size, { sm: "text-xs", md: "text-sm", lg: "text-base" }),
73
+ );
74
+ const subtle = "[color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]";
75
+
76
+ const dateOf = (s: string | null | undefined) => (s ? s.split("T")[0] : "");
77
+ const timeOf = (s: string | null | undefined) =>
78
+ s && s.includes("T") ? s.split("T")[1].slice(0, seconds ? 8 : 5) : "";
79
+ const midnight = $derived(seconds ? "00:00:00" : "00:00");
80
+ const compose = (d: string, t: string) => (time ? `${d}T${t || midnight}` : d);
81
+ const todayDate = () => {
82
+ const d = new Date();
83
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
84
+ };
85
+
86
+ const rv = $derived(
87
+ range ? ((value as RangeValue) ?? { start: null, end: null }) : { start: null, end: null },
88
+ );
89
+ const singleStr = $derived(range ? "" : ((value as string | null) ?? ""));
90
+
91
+ let open = $state(false);
92
+ let anchorEl = $state<HTMLDivElement>();
93
+
94
+ // ── single ──
95
+ function pickSingleDate(iso: string) {
96
+ value = compose(iso, timeOf(singleStr));
97
+ if (!time) open = false;
98
+ }
99
+ function setSingleTime(t: string) {
100
+ value = compose(dateOf(singleStr) || todayDate(), t);
101
+ }
102
+
103
+ // ── range ──
104
+ function onRange(r: { start: string; end: string }) {
105
+ const s = r.start ? compose(r.start, timeOf(rv.start) || (time ? midnight : "")) : null;
106
+ const e = r.end
107
+ ? compose(r.end, timeOf(rv.end) || (time ? (seconds ? "23:59:59" : "23:59") : ""))
108
+ : null;
109
+ value = { start: s, end: e };
110
+ if (!time && e) open = false;
111
+ }
112
+ const setStartTime = (t: string) =>
113
+ rv.start && (value = { ...rv, start: `${dateOf(rv.start)}T${t}` });
114
+ const setEndTime = (t: string) => rv.end && (value = { ...rv, end: `${dateOf(rv.end)}T${t}` });
115
+
116
+ function goToday() {
117
+ const t = todayDate();
118
+ if (range) value = { start: compose(t, time ? midnight : ""), end: null };
119
+ else pickSingleDate(t);
120
+ }
121
+ // time mode: jump to today's date AND the current time (minutes snapped to `step`; real seconds).
122
+ function goNow() {
123
+ const d = new Date();
124
+ const ms = resolveStep(step).minute;
125
+ const mins = Math.round((d.getHours() * 60 + d.getMinutes()) / ms) * ms;
126
+ const hhmm = `${String(Math.floor(mins / 60) % 24).padStart(2, "0")}:${String(mins % 60).padStart(2, "0")}`;
127
+ const t = seconds ? `${hhmm}:${String(d.getSeconds()).padStart(2, "0")}` : hhmm;
128
+ const day = todayDate();
129
+ if (range) value = { start: `${day}T${t}`, end: null };
130
+ else value = compose(day, t);
131
+ if (!range) open = false; // a full date+time is a complete single selection
132
+ }
133
+ function clear() {
134
+ value = range ? { start: null, end: null } : null;
135
+ open = false;
136
+ }
137
+
138
+ // ── trigger label ──
139
+ const fmtDate = (s: string) => {
140
+ if (!s) return "";
141
+ const [y, m, d] = dateOf(s).split("-").map(Number);
142
+ return new Intl.DateTimeFormat(locale, {
143
+ month: "short",
144
+ day: "numeric",
145
+ year: "numeric",
146
+ }).format(new Date(y, m - 1, d));
147
+ };
148
+ const fmtTime = (s: string) => {
149
+ const t = timeOf(s);
150
+ if (!t) return "";
151
+ const [h, m, sec] = t.split(":").map(Number);
152
+ return new Intl.DateTimeFormat(locale, {
153
+ hour: "numeric",
154
+ minute: "2-digit",
155
+ second: seconds ? "2-digit" : undefined,
156
+ hour12: meridiem,
157
+ }).format(new Date(2000, 0, 1, h, m, sec || 0));
158
+ };
159
+ const fmt = (s: string) => (s ? (time ? `${fmtDate(s)}, ${fmtTime(s)}` : fmtDate(s)) : "");
160
+ const label = $derived.by(() => {
161
+ if (range)
162
+ return rv.start ? `${fmt(rv.start ?? "")}${rv.end ? ` – ${fmt(rv.end ?? "")}` : " – …"}` : "";
163
+ return fmt(singleStr);
164
+ });
165
+ const hasValue = $derived(range ? !!(rv.start || rv.end) : !!singleStr);
166
+
167
+ function toggle() {
168
+ if (!disabled) open = !open;
169
+ }
170
+ function onKey(e: KeyboardEvent) {
171
+ if (disabled) return;
172
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
173
+ e.preventDefault();
174
+ open = true;
175
+ }
176
+ }
177
+ </script>
178
+
179
+ <div bind:this={anchorEl} class="w-full {cls}">
180
+ <Input
181
+ value={label}
182
+ readonly
183
+ {size}
184
+ {disabled}
185
+ {placeholder}
186
+ onclick={toggle}
187
+ onkeydown={onKey}
188
+ inputClass="cursor-pointer"
189
+ actions={trailing}
190
+ />
191
+ {#if name && !range}<input type="hidden" {name} value={singleStr} />{/if}
192
+ </div>
193
+
194
+ {#snippet trailing()}
195
+ {#if hasValue && !disabled}
196
+ <button
197
+ type="button"
198
+ aria-label="Clear"
199
+ tabindex="-1"
200
+ class="grid size-5 place-items-center rounded {subtle} hover:[background-color:color-mix(in_srgb,var(--ui-color-text)_10%,transparent)]"
201
+ onclick={(e) => {
202
+ e.stopPropagation();
203
+ clear();
204
+ }}><IconClose class="size-3.5" /></button
205
+ >
206
+ {/if}
207
+ <button
208
+ type="button"
209
+ tabindex="-1"
210
+ aria-label="Open calendar"
211
+ {disabled}
212
+ class="inline-flex items-center [color:var(--ui-color-secondary)] disabled:opacity-50"
213
+ onclick={toggle}><IconCalendar /></button
214
+ >
215
+ {/snippet}
216
+
217
+ <Popup anchor={anchorEl} bind:open offset={6}>
218
+ <div
219
+ class="rounded-[var(--ui-border-radius)] border {pad} shadow-lg [border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border),transparent)] [background-color:var(--ui-color-background)] [color:var(--ui-color-text)]"
220
+ style="--accent: {accent}"
221
+ >
222
+ {#if range}
223
+ <Calendar
224
+ mode="range"
225
+ start={dateOf(rv.start)}
226
+ end={dateOf(rv.end)}
227
+ {min}
228
+ {max}
229
+ {weekStart}
230
+ {locale}
231
+ {size}
232
+ {variant}
233
+ onrange={onRange}
234
+ />
235
+ {#if time}
236
+ <div class="mt-2 flex flex-col gap-2 border-t pt-2 [border-color:var(--ui-border-color)]">
237
+ <div class="flex items-center gap-3">
238
+ <span class="w-10 shrink-0 text-xs {subtle}">Start</span><TimeSpinner
239
+ value={timeOf(rv.start)}
240
+ {step}
241
+ {seconds}
242
+ {meridiem}
243
+ {size}
244
+ {variant}
245
+ {locale}
246
+ onchange={setStartTime}
247
+ />
248
+ </div>
249
+ <div class="flex items-center gap-3">
250
+ <span class="w-10 shrink-0 text-xs {subtle}">End</span><TimeSpinner
251
+ value={timeOf(rv.end)}
252
+ {step}
253
+ {seconds}
254
+ {meridiem}
255
+ {size}
256
+ {variant}
257
+ {locale}
258
+ onchange={setEndTime}
259
+ />
260
+ </div>
261
+ </div>
262
+ {/if}
263
+ {:else}
264
+ <Calendar
265
+ mode="single"
266
+ value={dateOf(singleStr)}
267
+ {min}
268
+ {max}
269
+ {weekStart}
270
+ {locale}
271
+ {size}
272
+ {variant}
273
+ onselect={pickSingleDate}
274
+ />
275
+ {#if time}
276
+ <div class="mt-2 flex justify-center border-t pt-2 [border-color:var(--ui-border-color)]">
277
+ <TimeSpinner
278
+ value={timeOf(singleStr)}
279
+ {step}
280
+ {seconds}
281
+ {meridiem}
282
+ {size}
283
+ {variant}
284
+ {locale}
285
+ onchange={setSingleTime}
286
+ />
287
+ </div>
288
+ {/if}
289
+ {/if}
290
+ <div
291
+ class="mt-2 flex items-center justify-between border-t pt-2 {footText} [border-color:var(--ui-border-color)]"
292
+ >
293
+ <div class="flex items-center gap-1">
294
+ <button
295
+ type="button"
296
+ class="rounded px-2 py-1 [color:var(--accent)] hover:underline"
297
+ onclick={goToday}>Today</button
298
+ >
299
+ {#if time}<span class={subtle}>/</span><button
300
+ type="button"
301
+ class="rounded px-2 py-1 [color:var(--accent)] hover:underline"
302
+ onclick={goNow}>Now</button
303
+ >{/if}
304
+ </div>
305
+ <button
306
+ type="button"
307
+ class="rounded px-2 py-1 {subtle} hover:underline"
308
+ onclick={() => (open = false)}>Done</button
309
+ >
310
+ </div>
311
+ </div>
312
+ </Popup>
@@ -0,0 +1,32 @@
1
+ import type { Size } from "../types/sizes.js";
2
+ import { type Responsive } from "../types/responsive.js";
3
+ import { type Variant } from "../types/variants.js";
4
+ import { type StepSpec } from "../types/time.js";
5
+ type RangeValue = {
6
+ start: string | null;
7
+ end: string | null;
8
+ };
9
+ interface Props {
10
+ value?: string | RangeValue | null;
11
+ range?: boolean;
12
+ time?: boolean;
13
+ /** `time` mode: column interval — a number = minute step, or `{ hour, minute, second }`. Default 1. */
14
+ step?: StepSpec;
15
+ /** `time` mode: add a seconds column; the time part becomes `HH:MM:SS`. Default false. */
16
+ seconds?: boolean;
17
+ /** `time` mode: show the AM/PM meridiem (12h). `false` = 24h. Default true; supply from app config. */
18
+ meridiem?: boolean;
19
+ min?: string;
20
+ max?: string;
21
+ weekStart?: 0 | 1;
22
+ locale?: string;
23
+ placeholder?: string;
24
+ disabled?: boolean;
25
+ size?: Responsive<Size>;
26
+ variant?: Variant;
27
+ class?: string;
28
+ name?: string;
29
+ }
30
+ declare const DatePicker: import("svelte").Component<Props, {}, "value">;
31
+ type DatePicker = ReturnType<typeof DatePicker>;
32
+ export default DatePicker;
@@ -0,0 +1,93 @@
1
+ <!--
2
+ /**
3
+ * DateTimeInput — the themed native date/time picker. Wraps {@link Input} so it inherits the exact
4
+ * frame, `size` height (sm 24 / md 32 / lg 40px), border, hover/focus glow and disabled styling as
5
+ * every other control — a raw `<input type="date|datetime-local">` never matches those, which is why
6
+ * bare date inputs were the one sanctioned exception. Use this instead.
7
+ *
8
+ * `type` picks the native picker: `datetime-local` (default), `date`, `time`, `month`, `week`. The
9
+ * value is the native string the browser emits (e.g. `2026-07-01T13:00` / `2026-07-01`), so it drops
10
+ * straight into a form field. We render our own trailing calendar/clock icon (in the library accent,
11
+ * matching Input's icons) and hide the UA indicator; clicking it opens the native picker via
12
+ * `showPicker()`. The picker popup + edit fields follow light/dark automatically through the theme's
13
+ * `color-scheme`.
14
+ */
15
+ -->
16
+ <script lang="ts">
17
+ import Input from "./Input.svelte";
18
+ import IconCalendar from "~icons/mdi/calendar-outline";
19
+ import IconClock from "~icons/mdi/clock-outline";
20
+ import type { HTMLInputAttributes } from "svelte/elements";
21
+ import type { Size } from "../types/sizes.js";
22
+ import type { Responsive } from "../types/responsive.js";
23
+ import { variantToken, type Variant } from "../types/variants.js";
24
+
25
+ type DateType = "datetime-local" | "date" | "time" | "month" | "week";
26
+
27
+ interface Props extends Omit<HTMLInputAttributes, "type" | "value" | "size"> {
28
+ value?: string;
29
+ /** Which native picker to render. Default `datetime-local`. */
30
+ type?: DateType;
31
+ size?: Responsive<Size>;
32
+ /** Accent for the native picker highlights (via CSS `accent-color`). Default primary. */
33
+ variant?: Variant;
34
+ class?: string;
35
+ inputClass?: string;
36
+ element?: HTMLInputElement;
37
+ disabled?: boolean;
38
+ }
39
+
40
+ let {
41
+ value = $bindable(""),
42
+ type = "datetime-local",
43
+ size = "md",
44
+ variant = "primary",
45
+ class: cls = "",
46
+ inputClass = "",
47
+ element = $bindable(),
48
+ disabled = false,
49
+ ...rest
50
+ }: Props = $props();
51
+
52
+ // `accent-color` themes the native picker's own highlights (selected day, spin buttons) in
53
+ // supporting browsers — the one lever we have on the browser-owned popup.
54
+ const accentClass = $derived(`[accent-color:var(${variantToken[variant]})]`);
55
+
56
+ // Our trailing icon opens the native picker (a real user gesture). Guard for disabled + browsers
57
+ // without showPicker (fall back to focusing, which still lets the user type / use the UA indicator).
58
+ function openPicker() {
59
+ if (disabled) return;
60
+ try {
61
+ element?.showPicker?.();
62
+ } catch {
63
+ element?.focus();
64
+ }
65
+ }
66
+ </script>
67
+
68
+ <Input
69
+ bind:value
70
+ bind:element
71
+ {type}
72
+ {size}
73
+ {disabled}
74
+ class={cls}
75
+ inputClass="ui-datetime {accentClass} {inputClass}"
76
+ icon={pickerIcon}
77
+ iconPosition="right"
78
+ iconClickable
79
+ onIconClick={openPicker}
80
+ {...rest}
81
+ />
82
+
83
+ {#snippet pickerIcon()}
84
+ {#if type === "time"}<IconClock />{:else}<IconCalendar />{/if}
85
+ {/snippet}
86
+
87
+ <style>
88
+ /* We draw our own calendar/clock icon (opens the picker), so hide the browser's native indicator to
89
+ avoid a double glyph. Value text + edit fields still theme via Input's --ui-color-text + color-scheme. */
90
+ :global(.ui-datetime::-webkit-calendar-picker-indicator) {
91
+ display: none;
92
+ }
93
+ </style>
@@ -0,0 +1,20 @@
1
+ import type { HTMLInputAttributes } from "svelte/elements";
2
+ import type { Size } from "../types/sizes.js";
3
+ import type { Responsive } from "../types/responsive.js";
4
+ import { type Variant } from "../types/variants.js";
5
+ type DateType = "datetime-local" | "date" | "time" | "month" | "week";
6
+ interface Props extends Omit<HTMLInputAttributes, "type" | "value" | "size"> {
7
+ value?: string;
8
+ /** Which native picker to render. Default `datetime-local`. */
9
+ type?: DateType;
10
+ size?: Responsive<Size>;
11
+ /** Accent for the native picker highlights (via CSS `accent-color`). Default primary. */
12
+ variant?: Variant;
13
+ class?: string;
14
+ inputClass?: string;
15
+ element?: HTMLInputElement;
16
+ disabled?: boolean;
17
+ }
18
+ declare const DateTimeInput: import("svelte").Component<Props, {}, "element" | "value">;
19
+ type DateTimeInput = ReturnType<typeof DateTimeInput>;
20
+ export default DateTimeInput;
@@ -0,0 +1,151 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { HTMLAttributes } from "svelte/elements";
4
+
5
+ /**
6
+ * Dialog — a PURE modal shell (layout container). It owns only the modal mechanics: portals itself
7
+ * to <body>, locks background scroll, renders a dismissable backdrop, traps focus, and exposes the
8
+ * ARIA dialog role. It imposes NO content structure — put a `Card` (or anything) inside via
9
+ * `children`. For the common title + message + Confirm/Cancel pattern, use `ConfirmDialog`, which is
10
+ * built on this.
11
+ *
12
+ * `width` / `height` accept any CSS length: `"80%"`, `"80vw"`, `"60vh"`, `"400px"`, … (both default
13
+ * to content size, capped to the viewport minus a 1rem inset so the panel never runs off-screen).
14
+ */
15
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "class"> {
16
+ /** Whether the dialog is shown (bindable). */
17
+ isOpen: boolean;
18
+ /** Panel width — any CSS value (`80%`, `80vw`, `400px`). Default: auto, capped at `min(32rem, …)`. */
19
+ width?: string;
20
+ /** Panel height — any CSS value (`60%`, `60vh`, `300px`). Default: auto (content height). */
21
+ height?: string;
22
+ /** Allow dismissing via backdrop click / Escape (default true). Set false for a must-decide modal. */
23
+ dismissible?: boolean;
24
+ /** Called when dismissed via backdrop / Escape (isOpen is also set false). */
25
+ onClose?: () => void;
26
+ /** Extra classes on the panel wrapper. */
27
+ class?: string;
28
+ children?: Snippet;
29
+ }
30
+
31
+ let {
32
+ isOpen = $bindable(),
33
+ width,
34
+ height,
35
+ dismissible = true,
36
+ onClose,
37
+ class: cls = "",
38
+ children,
39
+ ...rest
40
+ }: Props = $props();
41
+
42
+ const FOCUSABLE =
43
+ 'a[href], button:not([disabled]), textarea:not([disabled]), input:not([disabled]), select:not([disabled]), [tabindex]:not([tabindex="-1"])';
44
+
45
+ let panelEl: HTMLElement | undefined = $state();
46
+ let previouslyFocused: HTMLElement | null = null;
47
+
48
+ // Move the overlay to <body> so it's a real top-level layer — never a sibling of the trigger (which
49
+ // would perturb surrounding layout) and never clipped by an ancestor's overflow/stacking context.
50
+ function portal(node: HTMLElement) {
51
+ document.body.appendChild(node);
52
+ return { destroy: () => node.remove() };
53
+ }
54
+
55
+ // Lock background scroll while open; pad by the scrollbar width so hiding it doesn't shift the page.
56
+ $effect(() => {
57
+ if (!isOpen) return;
58
+ const sbw = window.innerWidth - document.documentElement.clientWidth;
59
+ const prevOverflow = document.body.style.overflow;
60
+ const prevPad = document.body.style.paddingRight;
61
+ document.body.style.overflow = "hidden";
62
+ if (sbw > 0) document.body.style.paddingRight = `${sbw}px`;
63
+ return () => {
64
+ document.body.style.overflow = prevOverflow;
65
+ document.body.style.paddingRight = prevPad;
66
+ };
67
+ });
68
+
69
+ // Focus management: on open, remember what was focused and move focus into the panel; on close,
70
+ // restore it. (Tab-wrapping is handled in onKeydown.)
71
+ $effect(() => {
72
+ if (!isOpen) return;
73
+ previouslyFocused = document.activeElement as HTMLElement | null;
74
+ queueMicrotask(() => {
75
+ const first = panelEl?.querySelector<HTMLElement>(FOCUSABLE);
76
+ (first ?? panelEl)?.focus();
77
+ });
78
+ return () => previouslyFocused?.focus?.();
79
+ });
80
+
81
+ const panelStyle = $derived(
82
+ [
83
+ width ? `width:${width}` : "width:auto",
84
+ `max-width:${width ? "calc(100vw - 2rem)" : "min(32rem, calc(100vw - 2rem))"}`,
85
+ height ? `height:${height}` : null,
86
+ "max-height:calc(100vh - 2rem)",
87
+ ]
88
+ .filter(Boolean)
89
+ .join(";"),
90
+ );
91
+
92
+ function close() {
93
+ if (!dismissible) return;
94
+ onClose?.();
95
+ isOpen = false;
96
+ }
97
+ function onBackdrop(event: MouseEvent) {
98
+ if (event.target === event.currentTarget) close();
99
+ }
100
+ function onKeydown(event: KeyboardEvent) {
101
+ if (!isOpen) return;
102
+ if (event.key === "Escape") {
103
+ close();
104
+ return;
105
+ }
106
+ if (event.key === "Tab" && panelEl) {
107
+ const items = [...panelEl.querySelectorAll<HTMLElement>(FOCUSABLE)].filter(
108
+ (el) => el.offsetParent !== null,
109
+ );
110
+ if (items.length === 0) {
111
+ event.preventDefault();
112
+ panelEl.focus();
113
+ return;
114
+ }
115
+ const first = items[0];
116
+ const last = items[items.length - 1];
117
+ const active = document.activeElement;
118
+ if (event.shiftKey && (active === first || active === panelEl)) {
119
+ event.preventDefault();
120
+ last.focus();
121
+ } else if (!event.shiftKey && active === last) {
122
+ event.preventDefault();
123
+ first.focus();
124
+ }
125
+ }
126
+ }
127
+ </script>
128
+
129
+ <svelte:window on:keydown={onKeydown} />
130
+
131
+ {#if isOpen}
132
+ <!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
133
+ <div
134
+ use:portal
135
+ class="fixed inset-0 bg-black/50 flex items-center justify-center z-[9999] p-4"
136
+ onclick={onBackdrop}
137
+ role="presentation"
138
+ >
139
+ <div
140
+ bind:this={panelEl}
141
+ class="overflow-auto z-[10000] {cls}"
142
+ style={panelStyle}
143
+ role="dialog"
144
+ aria-modal="true"
145
+ tabindex="-1"
146
+ {...rest}
147
+ >
148
+ {@render children?.()}
149
+ </div>
150
+ </div>
151
+ {/if}
@@ -0,0 +1,30 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { HTMLAttributes } from "svelte/elements";
3
+ /**
4
+ * Dialog — a PURE modal shell (layout container). It owns only the modal mechanics: portals itself
5
+ * to <body>, locks background scroll, renders a dismissable backdrop, traps focus, and exposes the
6
+ * ARIA dialog role. It imposes NO content structure — put a `Card` (or anything) inside via
7
+ * `children`. For the common title + message + Confirm/Cancel pattern, use `ConfirmDialog`, which is
8
+ * built on this.
9
+ *
10
+ * `width` / `height` accept any CSS length: `"80%"`, `"80vw"`, `"60vh"`, `"400px"`, … (both default
11
+ * to content size, capped to the viewport minus a 1rem inset so the panel never runs off-screen).
12
+ */
13
+ interface Props extends Omit<HTMLAttributes<HTMLDivElement>, "class"> {
14
+ /** Whether the dialog is shown (bindable). */
15
+ isOpen: boolean;
16
+ /** Panel width — any CSS value (`80%`, `80vw`, `400px`). Default: auto, capped at `min(32rem, …)`. */
17
+ width?: string;
18
+ /** Panel height — any CSS value (`60%`, `60vh`, `300px`). Default: auto (content height). */
19
+ height?: string;
20
+ /** Allow dismissing via backdrop click / Escape (default true). Set false for a must-decide modal. */
21
+ dismissible?: boolean;
22
+ /** Called when dismissed via backdrop / Escape (isOpen is also set false). */
23
+ onClose?: () => void;
24
+ /** Extra classes on the panel wrapper. */
25
+ class?: string;
26
+ children?: Snippet;
27
+ }
28
+ declare const Dialog: import("svelte").Component<Props, {}, "isOpen">;
29
+ type Dialog = ReturnType<typeof Dialog>;
30
+ export default Dialog;
@@ -1,9 +1,12 @@
1
1
  <script lang="ts">
2
- import Input from '../Input/Input.svelte';
3
- import { faEnvelope } from '@fortawesome/free-solid-svg-icons';
4
- import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import Input from "./Input.svelte";
3
+ import Email from "~icons/mdi/email-outline";
4
+ import type { HTMLInputAttributes } from "svelte/elements";
5
+ import type { Size } from "../types/sizes.js";
6
+ import type { Responsive } from "../types/responsive.js";
7
+ import type { Variant } from "../types/variants.js";
5
8
 
6
- interface Props extends Omit<HTMLInputAttributes, 'type'> {
9
+ interface Props extends Omit<HTMLInputAttributes, "type" | "size"> {
7
10
  value: string;
8
11
  class?: string;
9
12
  inputClass?: string;
@@ -11,6 +14,10 @@
11
14
  valid?: boolean;
12
15
  touched?: boolean;
13
16
  element?: HTMLInputElement;
17
+ size?: Responsive<Size>;
18
+ /** Shared axes — forwarded to the underlying Input. */
19
+ variant?: Variant;
20
+ borderless?: boolean;
14
21
  required?: boolean;
15
22
  }
16
23
 
@@ -26,8 +33,9 @@
26
33
 
27
34
  // Default email validation
28
35
  const defaultEmailValidator = (value: string) => {
29
- if (required && !value) return 'Email is required';
30
- if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Please enter a valid email address';
36
+ if (required && !value) return "Email is required";
37
+ if (value && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
38
+ return "Please enter a valid email address";
31
39
  return true;
32
40
  };
33
41
 
@@ -41,7 +49,8 @@
41
49
  bind:touched
42
50
  bind:element
43
51
  type="email"
44
- icon={faEnvelope}
45
52
  validate={emailValidator}
46
53
  {...restProps}
47
- />
54
+ >
55
+ {#snippet icon()}<Email />{/snippet}
56
+ </Input>