@spaethtech/svelte-ui 0.1.10 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +166 -42
  2. package/dist/components/Alert.svelte +119 -0
  3. package/dist/components/Alert.svelte.d.ts +29 -0
  4. package/dist/components/Badge/Badge.svelte +142 -69
  5. package/dist/components/Badge/Badge.svelte.d.ts +26 -6
  6. package/dist/components/Badge/index.d.ts +1 -1
  7. package/dist/components/Badge/index.js +1 -1
  8. package/dist/components/Banner.svelte +133 -0
  9. package/dist/components/Banner.svelte.d.ts +31 -0
  10. package/dist/components/Button.svelte +240 -0
  11. package/dist/components/Button.svelte.d.ts +33 -0
  12. package/dist/components/ButtonDropdown.svelte +145 -0
  13. package/dist/components/ButtonDropdown.svelte.d.ts +31 -0
  14. package/dist/components/Calendar.svelte +259 -0
  15. package/dist/components/Calendar.svelte.d.ts +27 -0
  16. package/dist/components/Card.svelte +78 -0
  17. package/dist/components/Card.svelte.d.ts +17 -0
  18. package/dist/components/CardBody.svelte +41 -0
  19. package/dist/components/CardBody.svelte.d.ts +17 -0
  20. package/dist/components/CardFooter.svelte +49 -0
  21. package/dist/components/CardFooter.svelte.d.ts +16 -0
  22. package/dist/components/CardHeader.svelte +49 -0
  23. package/dist/components/CardHeader.svelte.d.ts +16 -0
  24. package/dist/components/Checkbox.svelte +131 -0
  25. package/dist/components/Checkbox.svelte.d.ts +16 -0
  26. package/dist/components/DataTable.svelte +518 -0
  27. package/dist/components/DataTable.svelte.d.ts +74 -0
  28. package/dist/components/DatePicker.svelte +312 -0
  29. package/dist/components/DatePicker.svelte.d.ts +32 -0
  30. package/dist/components/DateTimeInput.svelte +93 -0
  31. package/dist/components/DateTimeInput.svelte.d.ts +20 -0
  32. package/dist/components/Dialog.svelte +132 -0
  33. package/dist/components/Dialog.svelte.d.ts +24 -0
  34. package/dist/components/{EmailInput/EmailInput.svelte → EmailInput.svelte} +17 -8
  35. package/dist/components/EmailInput.svelte.d.ts +21 -0
  36. package/dist/components/Input.svelte +369 -0
  37. package/dist/components/Input.svelte.d.ts +35 -0
  38. package/dist/components/{List/List.svelte → List.svelte} +194 -133
  39. package/dist/components/{List/List.svelte.d.ts → List.svelte.d.ts} +9 -1
  40. package/dist/components/Menu.svelte +117 -0
  41. package/dist/components/Menu.svelte.d.ts +20 -0
  42. package/dist/components/NotesEditor.svelte +127 -0
  43. package/dist/components/NotesEditor.svelte.d.ts +17 -0
  44. package/dist/components/{NumberInput/NumberInput.svelte → NumberInput.svelte} +146 -105
  45. package/dist/components/{NumberInput/NumberInput.svelte.d.ts → NumberInput.svelte.d.ts} +11 -4
  46. package/dist/components/PasswordInput.svelte +52 -0
  47. package/dist/components/PasswordInput.svelte.d.ts +20 -0
  48. package/dist/components/Popup.svelte +140 -0
  49. package/dist/components/Popup.svelte.d.ts +31 -0
  50. package/dist/components/Query.svelte +284 -0
  51. package/dist/components/Query.svelte.d.ts +15 -0
  52. package/dist/components/{Rating/Rating.svelte → Rating.svelte} +19 -10
  53. package/dist/components/Rating.svelte.d.ts +13 -0
  54. package/dist/components/{SearchInput/SearchInput.svelte → SearchInput.svelte} +19 -8
  55. package/dist/components/{SearchInput/SearchInput.svelte.d.ts → SearchInput.svelte.d.ts} +9 -2
  56. package/dist/components/{Select/Select.svelte → Select.svelte} +48 -76
  57. package/dist/components/{Select/Select.svelte.d.ts → Select.svelte.d.ts} +11 -1
  58. package/dist/components/SideBarMenu/SideBarMenu.svelte +724 -0
  59. package/dist/components/SideBarMenu/SideBarMenu.svelte.d.ts +109 -0
  60. package/dist/components/SideBarMenu/index.d.ts +2 -0
  61. package/dist/components/SideBarMenu/index.js +1 -0
  62. package/dist/components/TabStrip/TabStrip.svelte +384 -0
  63. package/dist/components/TabStrip/TabStrip.svelte.d.ts +50 -0
  64. package/dist/components/TabStrip/index.d.ts +3 -0
  65. package/dist/components/TabStrip/index.js +2 -0
  66. package/dist/components/{TextArea/TextArea.svelte → TextArea.svelte} +143 -112
  67. package/dist/components/TextArea.svelte.d.ts +38 -0
  68. package/dist/components/ThemeSelector.svelte +81 -0
  69. package/dist/components/ThemeSelector.svelte.d.ts +10 -0
  70. package/dist/components/TimePicker.svelte +148 -0
  71. package/dist/components/TimePicker.svelte.d.ts +28 -0
  72. package/dist/components/TimeRangeInput.svelte +203 -0
  73. package/dist/components/TimeRangeInput.svelte.d.ts +29 -0
  74. package/dist/components/TimeSpinner.svelte +202 -0
  75. package/dist/components/TimeSpinner.svelte.d.ts +26 -0
  76. package/dist/components/Toast/Toaster.svelte +51 -0
  77. package/dist/components/Toast/Toaster.svelte.d.ts +12 -0
  78. package/dist/components/Toast/toast.svelte.d.ts +29 -0
  79. package/dist/components/Toast/toast.svelte.js +27 -0
  80. package/dist/components/Toggle.svelte +93 -0
  81. package/dist/components/Toggle.svelte.d.ts +15 -0
  82. package/dist/data/dataset.d.ts +42 -0
  83. package/dist/data/dataset.js +3 -0
  84. package/dist/data/index.d.ts +3 -0
  85. package/dist/data/index.js +3 -0
  86. package/dist/data/query/ast.d.ts +62 -0
  87. package/dist/data/query/ast.js +12 -0
  88. package/dist/data/query/index.d.ts +3 -0
  89. package/dist/data/query/index.js +3 -0
  90. package/dist/data/query/lexer.d.ts +33 -0
  91. package/dist/data/query/lexer.js +225 -0
  92. package/dist/data/query/parser.d.ts +11 -0
  93. package/dist/data/query/parser.js +252 -0
  94. package/dist/data/table/grid.svelte.d.ts +57 -0
  95. package/dist/data/table/grid.svelte.js +139 -0
  96. package/dist/data/table/index.d.ts +2 -0
  97. package/dist/data/table/index.js +2 -0
  98. package/dist/data/table/types.d.ts +79 -0
  99. package/dist/index.d.ts +48 -22
  100. package/dist/index.js +45 -21
  101. package/dist/positioning/anchored.d.ts +61 -0
  102. package/dist/positioning/anchored.js +122 -0
  103. package/dist/positioning/tooltip.d.ts +41 -0
  104. package/dist/positioning/tooltip.js +147 -0
  105. package/dist/theme.css +205 -0
  106. package/dist/types/breakpoints.d.ts +24 -0
  107. package/dist/types/breakpoints.js +13 -0
  108. package/dist/types/responsive.d.ts +32 -0
  109. package/dist/types/responsive.js +54 -0
  110. package/dist/types/sizes.d.ts +10 -3
  111. package/dist/types/time.d.ts +19 -0
  112. package/dist/types/time.js +10 -0
  113. package/dist/types/variants.d.ts +8 -1
  114. package/dist/types/variants.js +16 -1
  115. package/package.json +93 -63
  116. package/dist/components/Button/Button.svelte +0 -172
  117. package/dist/components/Button/Button.svelte.d.ts +0 -32
  118. package/dist/components/Button/index.d.ts +0 -1
  119. package/dist/components/Button/index.js +0 -1
  120. package/dist/components/Dialog/Dialog.svelte +0 -101
  121. package/dist/components/Dialog/Dialog.svelte.d.ts +0 -18
  122. package/dist/components/Dialog/index.d.ts +0 -1
  123. package/dist/components/Dialog/index.js +0 -1
  124. package/dist/components/ElementManager/ElementManager.svelte +0 -397
  125. package/dist/components/ElementManager/ElementManager.svelte.d.ts +0 -43
  126. package/dist/components/ElementManager/index.d.ts +0 -1
  127. package/dist/components/ElementManager/index.js +0 -1
  128. package/dist/components/EmailInput/EmailInput.svelte.d.ts +0 -14
  129. package/dist/components/EmailInput/index.d.ts +0 -1
  130. package/dist/components/EmailInput/index.js +0 -1
  131. package/dist/components/Icon/Icon.svelte +0 -74
  132. package/dist/components/Icon/Icon.svelte.d.ts +0 -13
  133. package/dist/components/Icon/index.d.ts +0 -1
  134. package/dist/components/Icon/index.js +0 -1
  135. package/dist/components/Input/Input.svelte +0 -268
  136. package/dist/components/Input/Input.svelte.d.ts +0 -18
  137. package/dist/components/Input/index.d.ts +0 -1
  138. package/dist/components/Input/index.js +0 -1
  139. package/dist/components/List/index.d.ts +0 -1
  140. package/dist/components/List/index.js +0 -1
  141. package/dist/components/ListItem/ListItem.svelte +0 -175
  142. package/dist/components/ListItem/ListItem.svelte.d.ts +0 -24
  143. package/dist/components/ListItem/index.d.ts +0 -2
  144. package/dist/components/ListItem/index.js +0 -2
  145. package/dist/components/ListView/ListView.svelte +0 -463
  146. package/dist/components/ListView/ListView.svelte.d.ts +0 -37
  147. package/dist/components/ListView/index.d.ts +0 -2
  148. package/dist/components/ListView/index.js +0 -2
  149. package/dist/components/NodeGraph/BaseNode.d.ts +0 -57
  150. package/dist/components/NodeGraph/BaseNode.js +0 -30
  151. package/dist/components/NodeGraph/Edge.svelte +0 -60
  152. package/dist/components/NodeGraph/Edge.svelte.d.ts +0 -16
  153. package/dist/components/NodeGraph/ExpressionEvaluator.d.ts +0 -82
  154. package/dist/components/NodeGraph/ExpressionEvaluator.js +0 -152
  155. package/dist/components/NodeGraph/Node.svelte +0 -100
  156. package/dist/components/NodeGraph/Node.svelte.d.ts +0 -10
  157. package/dist/components/NodeGraph/NodeGraph.svelte +0 -52
  158. package/dist/components/NodeGraph/NodeGraph.svelte.d.ts +0 -14
  159. package/dist/components/NodeGraph/NodeInstance.svelte.d.ts +0 -80
  160. package/dist/components/NodeGraph/NodeInstance.svelte.js +0 -241
  161. package/dist/components/NodeGraph/NodePort.svelte +0 -75
  162. package/dist/components/NodeGraph/NodePort.svelte.d.ts +0 -11
  163. package/dist/components/NodeGraph/decorators.d.ts +0 -81
  164. package/dist/components/NodeGraph/decorators.js +0 -148
  165. package/dist/components/NodeGraph/index.d.ts +0 -9
  166. package/dist/components/NodeGraph/index.js +0 -9
  167. package/dist/components/NodeGraph/types.d.ts +0 -94
  168. package/dist/components/NodeGraph/types.js +0 -33
  169. package/dist/components/NotesEditor/NotesEditor.svelte +0 -203
  170. package/dist/components/NotesEditor/NotesEditor.svelte.d.ts +0 -9
  171. package/dist/components/NotesEditor/index.d.ts +0 -1
  172. package/dist/components/NotesEditor/index.js +0 -1
  173. package/dist/components/NumberInput/index.d.ts +0 -1
  174. package/dist/components/NumberInput/index.js +0 -1
  175. package/dist/components/PasswordInput/PasswordInput.svelte +0 -41
  176. package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +0 -13
  177. package/dist/components/PasswordInput/index.d.ts +0 -1
  178. package/dist/components/PasswordInput/index.js +0 -1
  179. package/dist/components/Popup/index.d.ts +0 -1
  180. package/dist/components/Popup/index.js +0 -1
  181. package/dist/components/Rating/Rating.svelte.d.ts +0 -7
  182. package/dist/components/Rating/index.d.ts +0 -1
  183. package/dist/components/Rating/index.js +0 -1
  184. package/dist/components/ScrollView/ScrollView.svelte +0 -285
  185. package/dist/components/ScrollView/ScrollView.svelte.d.ts +0 -19
  186. package/dist/components/ScrollView/index.d.ts +0 -1
  187. package/dist/components/ScrollView/index.js +0 -1
  188. package/dist/components/SearchInput/index.d.ts +0 -1
  189. package/dist/components/SearchInput/index.js +0 -1
  190. package/dist/components/Select/index.d.ts +0 -1
  191. package/dist/components/Select/index.js +0 -1
  192. package/dist/components/TextArea/TextArea.svelte.d.ts +0 -19
  193. package/dist/components/TextArea/index.d.ts +0 -1
  194. package/dist/components/TextArea/index.js +0 -1
  195. package/dist/components/ThemeSelector/ThemeSelector.svelte +0 -64
  196. package/dist/components/ThemeSelector/ThemeSelector.svelte.d.ts +0 -3
  197. package/dist/components/ThemeSelector/index.d.ts +0 -1
  198. package/dist/components/ThemeSelector/index.js +0 -1
  199. package/dist/components/TooltipHandler/TooltipHandler.svelte +0 -593
  200. package/dist/components/TooltipHandler/TooltipHandler.svelte.d.ts +0 -10
  201. package/dist/components/TooltipHandler/index.d.ts +0 -1
  202. package/dist/components/TooltipHandler/index.js +0 -1
  203. package/dist/styles/sizes.d.ts +0 -78
  204. package/dist/styles/sizes.js +0 -120
  205. package/dist/styles/variants.d.ts +0 -106
  206. package/dist/styles/variants.js +0 -194
  207. package/dist/types/ManagerTypes.d.ts +0 -42
  208. /package/dist/{types/ManagerTypes.js → data/table/types.js} +0 -0
@@ -0,0 +1,74 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { MenuItem, DataGrid } from "../data/table/index.js";
3
+ import type { Size } from "../types/sizes.js";
4
+ import { type Responsive } from "../types/responsive.js";
5
+ import { type Variant } from "../types/variants.js";
6
+ declare function $$render<T>(): {
7
+ props: {
8
+ grid: DataGrid<T>;
9
+ selectable?: boolean;
10
+ expandable?: boolean;
11
+ /** When true, each row gets up/down chevron buttons in the lead
12
+ * column and fires `onReorder(row, 'up'|'down')` when clicked.
13
+ * The consumer owns the actual swap (typically updating the
14
+ * `sortOrder` field of two rows). Buttons auto-disable at the
15
+ * ends of the visible page — with server pagination, this
16
+ * reorders WITHIN the visible page only. */
17
+ reorderable?: boolean;
18
+ expanded?: Snippet<[T]>;
19
+ rowActions?: (row: T) => MenuItem[];
20
+ bulkActions?: (selected: ReadonlySet<string>) => MenuItem[];
21
+ onReorder?: (row: T, direction: "up" | "down") => void;
22
+ /** Overall density tier. Scales the whole table uniformly:
23
+ * font-size (--ui-text-*), row/footer padding, inter-column
24
+ * gap, lead/tail control squares (--ui-height-*), and
25
+ * propagates to every inner Button / Checkbox / Select.
26
+ * Header text inherits the outer font-size — no separate
27
+ * axis. Default `'md'` matches the rest of svelte-ui. */
28
+ size?: Responsive<Size>;
29
+ /** Every header + body cell wrapper gets Tailwind's `truncate`
30
+ * (white-space:nowrap, overflow:hidden, text-overflow:ellipsis).
31
+ * Simple text cells render with ellipsis automatically. Compound
32
+ * cells (inline-flex wrappers, badges, buttons) clip at the
33
+ * right edge — to also get ellipsis on the inner text, the
34
+ * snippet's text span needs its own `truncate` class.
35
+ *
36
+ * Defaults to `true` — the "wrong" default is columns visually
37
+ * bleeding into their neighbours when a value doesn't fit its
38
+ * configured span. Pass `truncate={false}` if the consumer
39
+ * wants wrap-on-overflow (rare — usually you'd fix the span
40
+ * allocation instead). */
41
+ truncate?: boolean;
42
+ /** Drop the visible frame while keeping each section's 1px box: every section's border colour
43
+ * becomes its own background, so it vanishes per-section on a matching surface. Defaults `false`. */
44
+ borderless?: boolean;
45
+ /** Tints the whole table's interactive/section surfaces (row hover,
46
+ * header/footer bands) with a semantic colour instead of the neutral
47
+ * default. Points --ui-accent at the variant's token and re-mixes the
48
+ * --ui-color-hover/surface/active tints via the `.ui-accent` class.
49
+ * `ghost` additionally drops ALL section fills (bands + body) for a bare,
50
+ * see-through table. Omit for the default grey tint. */
51
+ variant?: Variant;
52
+ };
53
+ exports: {};
54
+ bindings: "";
55
+ slots: {};
56
+ events: {};
57
+ };
58
+ declare class __sveltets_Render<T> {
59
+ props(): ReturnType<typeof $$render<T>>['props'];
60
+ events(): ReturnType<typeof $$render<T>>['events'];
61
+ slots(): ReturnType<typeof $$render<T>>['slots'];
62
+ bindings(): "";
63
+ exports(): {};
64
+ }
65
+ interface $$IsomorphicComponent {
66
+ new <T>(options: import('svelte').ComponentConstructorOptions<ReturnType<__sveltets_Render<T>['props']>>): import('svelte').SvelteComponent<ReturnType<__sveltets_Render<T>['props']>, ReturnType<__sveltets_Render<T>['events']>, ReturnType<__sveltets_Render<T>['slots']>> & {
67
+ $$bindings?: ReturnType<__sveltets_Render<T>['bindings']>;
68
+ } & ReturnType<__sveltets_Render<T>['exports']>;
69
+ <T>(internal: unknown, props: ReturnType<__sveltets_Render<T>['props']> & {}): ReturnType<__sveltets_Render<T>['exports']>;
70
+ z_$$bindings?: ReturnType<__sveltets_Render<any>['bindings']>;
71
+ }
72
+ declare const DataTable: $$IsomorphicComponent;
73
+ type DataTable<T> = InstanceType<typeof DataTable<T>>;
74
+ export default DataTable;
@@ -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,132 @@
1
+ <script lang="ts">
2
+ import Button from "./Button.svelte";
3
+ import Card from "./Card.svelte";
4
+ import CardHeader from "./CardHeader.svelte";
5
+ import CardFooter from "./CardFooter.svelte";
6
+ import type { Snippet } from "svelte";
7
+ import type { Size } from "../types/sizes.js";
8
+ import type { Responsive } from "../types/responsive.js";
9
+
10
+ interface Props {
11
+ isOpen: boolean;
12
+ title?: string;
13
+ message?: string;
14
+ confirmText?: string;
15
+ cancelText?: string;
16
+ onConfirm?: () => void;
17
+ onCancel?: () => void;
18
+ variant?: "danger" | "normal";
19
+ /** Density tier — forwarded to the underlying Card / CardHeader / CardFooter. */
20
+ size?: Responsive<Size>;
21
+ showConfirm?: boolean;
22
+ showCancel?: boolean;
23
+ /** Allow dismissing by clicking the backdrop OR pressing Escape (default true). Set `false` for a
24
+ * must-decide dialog (e.g. a destructive confirm) so only the buttons close it. */
25
+ dismissible?: boolean;
26
+ children?: Snippet;
27
+ }
28
+
29
+ let {
30
+ isOpen = $bindable(),
31
+ title = "Dialog",
32
+ message,
33
+ confirmText = "Confirm",
34
+ cancelText = "Cancel",
35
+ onConfirm,
36
+ onCancel,
37
+ variant = "normal",
38
+ size = "md",
39
+ showConfirm = true,
40
+ showCancel = true,
41
+ dismissible = true,
42
+ children,
43
+ }: Props = $props();
44
+
45
+ // Move the overlay to <body> so it's a real top-level layer — never a sibling of the trigger (which
46
+ // would perturb surrounding layout, e.g. flip a space-y `:last-child`) and never clipped by an
47
+ // ancestor's overflow/stacking context. This is what makes it an actual modal, not an inline box.
48
+ function portal(node: HTMLElement) {
49
+ document.body.appendChild(node);
50
+ return { destroy: () => node.remove() };
51
+ }
52
+
53
+ // Lock background scroll while open; pad by the scrollbar width so hiding it doesn't shift the page
54
+ // sideways. Restore exactly what was there on close/destroy.
55
+ $effect(() => {
56
+ if (!isOpen) return;
57
+ const sbw = window.innerWidth - document.documentElement.clientWidth;
58
+ const prevOverflow = document.body.style.overflow;
59
+ const prevPad = document.body.style.paddingRight;
60
+ document.body.style.overflow = "hidden";
61
+ if (sbw > 0) document.body.style.paddingRight = `${sbw}px`;
62
+ return () => {
63
+ document.body.style.overflow = prevOverflow;
64
+ document.body.style.paddingRight = prevPad;
65
+ };
66
+ });
67
+
68
+ function handleConfirm() {
69
+ onConfirm?.();
70
+ isOpen = false;
71
+ }
72
+ function handleCancel() {
73
+ onCancel?.();
74
+ isOpen = false;
75
+ }
76
+ // Backdrop click / Escape only dismiss when allowed.
77
+ function dismiss() {
78
+ if (dismissible) handleCancel();
79
+ }
80
+ function handleBackdropClick(event: MouseEvent) {
81
+ if (event.target === event.currentTarget) dismiss();
82
+ }
83
+ function handleKeydown(event: KeyboardEvent) {
84
+ if (isOpen && event.key === "Escape") dismiss();
85
+ }
86
+ </script>
87
+
88
+ <svelte:window on:keydown={handleKeydown} />
89
+
90
+ {#if isOpen}
91
+ <div
92
+ use:portal
93
+ class="fixed inset-0 bg-black/50 flex items-center justify-center z-[9999]"
94
+ onclick={handleBackdropClick}
95
+ onkeydown={handleKeydown}
96
+ role="dialog"
97
+ aria-modal="true"
98
+ tabindex="-1"
99
+ >
100
+ <!-- The panel IS a Card — border + --ui-border-radius + padding + background come from Card, so a
101
+ Dialog stays consistent with every other surface; CardHeader/CardFooter give the title + action
102
+ rows their (inset) separators and spacing. `size` flows to all three. -->
103
+ <Card {size} class="max-w-md w-full mx-4 shadow-xl z-[10000]">
104
+ <CardHeader {size}>
105
+ <h3 class="text-lg font-medium [color:var(--ui-color-text)]">{title}</h3>
106
+ </CardHeader>
107
+
108
+ {#if children}
109
+ {@render children()}
110
+ {:else if message}
111
+ <p class="text-sm [color:color-mix(in_srgb,var(--ui-color-text)_70%,transparent)]">
112
+ {message}
113
+ </p>
114
+ {/if}
115
+
116
+ {#if showConfirm || showCancel}
117
+ <CardFooter {size}>
118
+ {#if showCancel}
119
+ <Button text={cancelText} onclick={handleCancel} variant="secondary" />
120
+ {/if}
121
+ {#if showConfirm}
122
+ <Button
123
+ text={confirmText}
124
+ onclick={handleConfirm}
125
+ variant={variant === "danger" ? "danger" : "primary"}
126
+ />
127
+ {/if}
128
+ </CardFooter>
129
+ {/if}
130
+ </Card>
131
+ </div>
132
+ {/if}
@@ -0,0 +1,24 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { Size } from "../types/sizes.js";
3
+ import type { Responsive } from "../types/responsive.js";
4
+ interface Props {
5
+ isOpen: boolean;
6
+ title?: string;
7
+ message?: string;
8
+ confirmText?: string;
9
+ cancelText?: string;
10
+ onConfirm?: () => void;
11
+ onCancel?: () => void;
12
+ variant?: "danger" | "normal";
13
+ /** Density tier — forwarded to the underlying Card / CardHeader / CardFooter. */
14
+ size?: Responsive<Size>;
15
+ showConfirm?: boolean;
16
+ showCancel?: boolean;
17
+ /** Allow dismissing by clicking the backdrop OR pressing Escape (default true). Set `false` for a
18
+ * must-decide dialog (e.g. a destructive confirm) so only the buttons close it. */
19
+ dismissible?: boolean;
20
+ children?: Snippet;
21
+ }
22
+ declare const Dialog: import("svelte").Component<Props, {}, "isOpen">;
23
+ type Dialog = ReturnType<typeof Dialog>;
24
+ export default Dialog;