@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,148 @@
1
+ <!--
2
+ /**
3
+ * TimePicker — a themed time field: an {@link Input}-framed trigger (clock icon) that opens a compact
4
+ * {@link TimeSpinner} (iOS-style hour/minute/AM·PM stepper) in a {@link Popup} (top layer, dismisses on
5
+ * Escape/outside). The field shows a locale-formatted time; a hidden input carries the 24h `HH:MM`
6
+ * value for forms. Use this instead of a bare `<input type=time>` so the field AND its popup match.
7
+ *
8
+ * The footer mirrors DatePicker's Today/Done: "Now" sets the current time, "Done" closes.
9
+ */
10
+ -->
11
+ <script lang="ts">
12
+ import type { HTMLInputAttributes } from "svelte/elements";
13
+ import type { Size } from "../types/sizes.js";
14
+ import { responsiveClasses, type Responsive } from "../types/responsive.js";
15
+ import { resolveStep, type StepSpec } from "../types/time.js";
16
+ import { variantToken, type Variant } from "../types/variants.js";
17
+ import Input from "./Input.svelte";
18
+ import Popup from "./Popup.svelte";
19
+ import TimeSpinner from "./TimeSpinner.svelte";
20
+ import IconClock from "~icons/mdi/clock-outline";
21
+
22
+ interface Props extends Omit<HTMLInputAttributes, "value" | "size" | "min" | "max" | "step"> {
23
+ /** 24h `HH:MM` (or `HH:MM:SS` when `seconds`), or ''. Bindable. */
24
+ value?: string;
25
+ /** Column interval: a number = minute step, or `{ hour, minute, second }`. Default 1 (minute). */
26
+ step?: StepSpec;
27
+ /** Add a seconds column; value becomes `HH:MM:SS`. Default false. */
28
+ seconds?: boolean;
29
+ min?: string;
30
+ max?: string;
31
+ /** Show the AM/PM meridiem (12h). `false` = 24h. Default true; supply from the app's own config. */
32
+ meridiem?: boolean;
33
+ locale?: string;
34
+ size?: Responsive<Size>;
35
+ /** Accent for the popup (focus ring, "Now" action, current value). Default primary. */
36
+ variant?: Variant;
37
+ disabled?: boolean;
38
+ name?: string;
39
+ placeholder?: string;
40
+ class?: string;
41
+ }
42
+
43
+ let {
44
+ value = $bindable(""),
45
+ step = 1,
46
+ seconds = false,
47
+ min = "00:00",
48
+ max = "23:59",
49
+ meridiem = true,
50
+ locale = typeof navigator !== "undefined" ? navigator.language : "en-US",
51
+ size = "md",
52
+ variant = "primary",
53
+ disabled = false,
54
+ name,
55
+ placeholder = "Select a time",
56
+ class: cls = "",
57
+ ...rest
58
+ }: Props = $props();
59
+
60
+ const accent = $derived(`var(${variantToken[variant]})`);
61
+ // Popup surface padding + text scale with size, so the popup honours `size` like the field does.
62
+ const pad = $derived(responsiveClasses(size, { sm: "p-1.5", md: "p-2", lg: "p-3" }));
63
+ const footText = $derived(
64
+ responsiveClasses(size, { sm: "text-xs", md: "text-sm", lg: "text-base" }),
65
+ );
66
+
67
+ let open = $state(false);
68
+ let anchorEl = $state<HTMLDivElement>();
69
+
70
+ const display = $derived.by(() => {
71
+ if (!value) return "";
72
+ const [h, m, s] = value.split(":").map(Number);
73
+ return new Intl.DateTimeFormat(locale, {
74
+ hour: "numeric",
75
+ minute: "2-digit",
76
+ second: seconds ? "2-digit" : undefined,
77
+ hour12: meridiem,
78
+ }).format(new Date(2000, 0, 1, h, m, s || 0));
79
+ });
80
+
81
+ function toggle() {
82
+ if (!disabled) open = !open;
83
+ }
84
+ function onKey(e: KeyboardEvent) {
85
+ if (disabled) return;
86
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
87
+ e.preventDefault();
88
+ open = true;
89
+ }
90
+ }
91
+ function now() {
92
+ const d = new Date();
93
+ // Snap minutes to the nearest minute-step so "Now" lands on a real spinner value; keep real seconds.
94
+ const ms = resolveStep(step).minute;
95
+ const mins = Math.round((d.getHours() * 60 + d.getMinutes()) / ms) * ms;
96
+ const hhmm = `${String(Math.floor(mins / 60) % 24).padStart(2, "0")}:${String(mins % 60).padStart(2, "0")}`;
97
+ value = seconds ? `${hhmm}:${String(d.getSeconds()).padStart(2, "0")}` : hhmm;
98
+ open = false; // a time is a complete selection — set and close (like DatePicker's Today)
99
+ }
100
+
101
+ const subtle = "[color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]";
102
+ </script>
103
+
104
+ <div bind:this={anchorEl} class="w-full {cls}">
105
+ <Input
106
+ value={display}
107
+ readonly
108
+ {size}
109
+ {disabled}
110
+ {placeholder}
111
+ icon={clockIcon}
112
+ iconPosition="right"
113
+ iconClickable
114
+ onIconClick={toggle}
115
+ onclick={toggle}
116
+ onkeydown={onKey}
117
+ inputClass="cursor-pointer"
118
+ {...rest}
119
+ />
120
+ {#if name}<input type="hidden" {name} value={value ?? ""} />{/if}
121
+ </div>
122
+
123
+ {#snippet clockIcon()}<IconClock />{/snippet}
124
+
125
+ <Popup anchor={anchorEl} bind:open offset={6}>
126
+ <div
127
+ 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)]"
128
+ style="--accent: {accent}"
129
+ >
130
+ <div class="grid place-items-center py-1">
131
+ <TimeSpinner bind:value {step} {seconds} {min} {max} {meridiem} {locale} {size} {variant} />
132
+ </div>
133
+ <div
134
+ class="mt-2 flex items-center justify-between border-t pt-2 {footText} [border-color:var(--ui-border-color)]"
135
+ >
136
+ <button
137
+ type="button"
138
+ class="rounded px-2 py-1 [color:var(--accent)] hover:underline"
139
+ onclick={now}>Now</button
140
+ >
141
+ <button
142
+ type="button"
143
+ class="rounded px-2 py-1 {subtle} hover:underline"
144
+ onclick={() => (open = false)}>Done</button
145
+ >
146
+ </div>
147
+ </div>
148
+ </Popup>
@@ -0,0 +1,28 @@
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 StepSpec } from "../types/time.js";
5
+ import { type Variant } from "../types/variants.js";
6
+ interface Props extends Omit<HTMLInputAttributes, "value" | "size" | "min" | "max" | "step"> {
7
+ /** 24h `HH:MM` (or `HH:MM:SS` when `seconds`), or ''. Bindable. */
8
+ value?: string;
9
+ /** Column interval: a number = minute step, or `{ hour, minute, second }`. Default 1 (minute). */
10
+ step?: StepSpec;
11
+ /** Add a seconds column; value becomes `HH:MM:SS`. Default false. */
12
+ seconds?: boolean;
13
+ min?: string;
14
+ max?: string;
15
+ /** Show the AM/PM meridiem (12h). `false` = 24h. Default true; supply from the app's own config. */
16
+ meridiem?: boolean;
17
+ locale?: string;
18
+ size?: Responsive<Size>;
19
+ /** Accent for the popup (focus ring, "Now" action, current value). Default primary. */
20
+ variant?: Variant;
21
+ disabled?: boolean;
22
+ name?: string;
23
+ placeholder?: string;
24
+ class?: string;
25
+ }
26
+ declare const TimePicker: import("svelte").Component<Props, {}, "value">;
27
+ type TimePicker = ReturnType<typeof TimePicker>;
28
+ export default TimePicker;
@@ -0,0 +1,203 @@
1
+ <!--
2
+ /**
3
+ * TimeRangeInput — a themed time-interval field: an {@link Input}-framed trigger (clock icon) that
4
+ * opens two compact {@link TimeSpinner}s (Start / End) side by side in a {@link Popup}. The field
5
+ * shows "9:00 AM – 5:00 PM"; the spinners are cross-bounded so End can't precede Start. Value is
6
+ * `{ start, end }` with 24h `HH:MM` strings (mirrors DatePicker's range shape).
7
+ *
8
+ * The footer mirrors DatePicker's Today/Done: "Now" sets the start to the current time, "Done" closes.
9
+ */
10
+ -->
11
+ <script lang="ts">
12
+ import type { Size } from "../types/sizes.js";
13
+ import { responsiveClasses, type Responsive } from "../types/responsive.js";
14
+ import { resolveStep, type StepSpec } from "../types/time.js";
15
+ import { variantToken, type Variant } from "../types/variants.js";
16
+ import Input from "./Input.svelte";
17
+ import Popup from "./Popup.svelte";
18
+ import TimeSpinner from "./TimeSpinner.svelte";
19
+ import IconClock from "~icons/mdi/clock-outline";
20
+
21
+ type RangeValue = { start: string | null; end: string | null };
22
+ interface Props {
23
+ /** `{ start, end }` with 24h `HH:MM` (or `HH:MM:SS` when `seconds`), or null. Bindable. */
24
+ value?: RangeValue | null;
25
+ /** Column interval: a number = minute step, or `{ hour, minute, second }`. Default 1 (minute). */
26
+ step?: StepSpec;
27
+ /** Add a seconds column to both spinners; values become `HH:MM:SS`. Default false. */
28
+ seconds?: boolean;
29
+ min?: string;
30
+ max?: string;
31
+ /** Show the AM/PM meridiem (12h). `false` = 24h. Default true; supply from the app's own config. */
32
+ meridiem?: boolean;
33
+ locale?: string;
34
+ size?: Responsive<Size>;
35
+ variant?: Variant;
36
+ disabled?: boolean;
37
+ placeholder?: string;
38
+ class?: string;
39
+ }
40
+
41
+ let {
42
+ value = $bindable(),
43
+ step = 1,
44
+ seconds = false,
45
+ min = "00:00",
46
+ max = "23:59",
47
+ meridiem = true,
48
+ locale = typeof navigator !== "undefined" ? navigator.language : "en-US",
49
+ size = "md",
50
+ variant = "primary",
51
+ disabled = false,
52
+ placeholder = "Select a time range",
53
+ class: cls = "",
54
+ ...rest
55
+ }: Props = $props();
56
+
57
+ const accent = $derived(`var(${variantToken[variant]})`);
58
+ const pad = $derived(responsiveClasses(size, { sm: "p-1.5", md: "p-2", lg: "p-3" }));
59
+ const footText = $derived(
60
+ responsiveClasses(size, { sm: "text-xs", md: "text-sm", lg: "text-base" }),
61
+ );
62
+ const subtle = "[color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]";
63
+
64
+ const rv = $derived(value ?? { start: null, end: null });
65
+
66
+ let open = $state(false);
67
+ let anchorEl = $state<HTMLDivElement>();
68
+
69
+ const fmt = (t: string | null) => {
70
+ if (!t) return "";
71
+ const [h, m, s] = t.split(":").map(Number);
72
+ return new Intl.DateTimeFormat(locale, {
73
+ hour: "numeric",
74
+ minute: "2-digit",
75
+ second: seconds ? "2-digit" : undefined,
76
+ hour12: meridiem,
77
+ }).format(new Date(2000, 0, 1, h, m, s || 0));
78
+ };
79
+ const label = $derived(rv.start ? `${fmt(rv.start)} – ${rv.end ? fmt(rv.end) : "…"}` : "");
80
+ const hasValue = $derived(!!(rv.start || rv.end));
81
+
82
+ // Keep the interval ordered: nudging start past end drags end along, and vice-versa.
83
+ function setStart(t: string) {
84
+ value = { start: t, end: rv.end && rv.end < t ? t : rv.end };
85
+ }
86
+ function setEnd(t: string) {
87
+ value = { start: rv.start && rv.start > t ? t : rv.start, end: t };
88
+ }
89
+ function now() {
90
+ const d = new Date();
91
+ const ms = resolveStep(step).minute;
92
+ const mins = Math.round((d.getHours() * 60 + d.getMinutes()) / ms) * ms;
93
+ const hhmm = `${String(Math.floor(mins / 60) % 24).padStart(2, "0")}:${String(mins % 60).padStart(2, "0")}`;
94
+ setStart(seconds ? `${hhmm}:${String(d.getSeconds()).padStart(2, "0")}` : hhmm);
95
+ }
96
+ function clear() {
97
+ value = { start: null, end: null };
98
+ open = false;
99
+ }
100
+ function toggle() {
101
+ if (!disabled) open = !open;
102
+ }
103
+ function onKey(e: KeyboardEvent) {
104
+ if (disabled) return;
105
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") {
106
+ e.preventDefault();
107
+ open = true;
108
+ }
109
+ }
110
+ </script>
111
+
112
+ <div bind:this={anchorEl} class="w-full {cls}">
113
+ <Input
114
+ value={label}
115
+ readonly
116
+ {size}
117
+ {disabled}
118
+ {placeholder}
119
+ onclick={toggle}
120
+ onkeydown={onKey}
121
+ inputClass="cursor-pointer"
122
+ actions={trailing}
123
+ {...rest}
124
+ />
125
+ </div>
126
+
127
+ {#snippet trailing()}
128
+ {#if hasValue && !disabled}
129
+ <button
130
+ type="button"
131
+ aria-label="Clear"
132
+ tabindex="-1"
133
+ class="grid size-5 place-items-center rounded {subtle} hover:[background-color:color-mix(in_srgb,var(--ui-color-text)_10%,transparent)]"
134
+ onclick={(e) => {
135
+ e.stopPropagation();
136
+ clear();
137
+ }}>×</button
138
+ >
139
+ {/if}
140
+ <button
141
+ type="button"
142
+ tabindex="-1"
143
+ aria-label="Open time range"
144
+ {disabled}
145
+ class="inline-flex items-center [color:var(--ui-color-secondary)] disabled:opacity-50"
146
+ onclick={toggle}><IconClock /></button
147
+ >
148
+ {/snippet}
149
+
150
+ <Popup anchor={anchorEl} bind:open offset={6}>
151
+ <div
152
+ 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)]"
153
+ style="--accent: {accent}"
154
+ >
155
+ <div class="flex flex-col gap-2">
156
+ <div class="flex items-center gap-3">
157
+ <span class="w-10 shrink-0 text-xs {subtle}">Start</span>
158
+ <TimeSpinner
159
+ value={rv.start ?? ""}
160
+ {step}
161
+ {seconds}
162
+ {min}
163
+ max={rv.end ?? max}
164
+ {meridiem}
165
+ {locale}
166
+ {size}
167
+ {variant}
168
+ onchange={setStart}
169
+ />
170
+ </div>
171
+ <div class="border-t [border-color:var(--ui-border-color)]"></div>
172
+ <div class="flex items-center gap-3">
173
+ <span class="w-10 shrink-0 text-xs {subtle}">End</span>
174
+ <TimeSpinner
175
+ value={rv.end ?? ""}
176
+ {step}
177
+ {seconds}
178
+ min={rv.start ?? min}
179
+ {max}
180
+ {meridiem}
181
+ {locale}
182
+ {size}
183
+ {variant}
184
+ onchange={setEnd}
185
+ />
186
+ </div>
187
+ </div>
188
+ <div
189
+ class="mt-2 flex items-center justify-between border-t pt-2 {footText} [border-color:var(--ui-border-color)]"
190
+ >
191
+ <button
192
+ type="button"
193
+ class="rounded px-2 py-1 [color:var(--accent)] hover:underline"
194
+ onclick={now}>Now</button
195
+ >
196
+ <button
197
+ type="button"
198
+ class="rounded px-2 py-1 {subtle} hover:underline"
199
+ onclick={() => (open = false)}>Done</button
200
+ >
201
+ </div>
202
+ </div>
203
+ </Popup>
@@ -0,0 +1,29 @@
1
+ import type { Size } from "../types/sizes.js";
2
+ import { type Responsive } from "../types/responsive.js";
3
+ import { type StepSpec } from "../types/time.js";
4
+ import { type Variant } from "../types/variants.js";
5
+ type RangeValue = {
6
+ start: string | null;
7
+ end: string | null;
8
+ };
9
+ interface Props {
10
+ /** `{ start, end }` with 24h `HH:MM` (or `HH:MM:SS` when `seconds`), or null. Bindable. */
11
+ value?: RangeValue | null;
12
+ /** Column interval: a number = minute step, or `{ hour, minute, second }`. Default 1 (minute). */
13
+ step?: StepSpec;
14
+ /** Add a seconds column to both spinners; values become `HH:MM:SS`. Default false. */
15
+ seconds?: boolean;
16
+ min?: string;
17
+ max?: string;
18
+ /** Show the AM/PM meridiem (12h). `false` = 24h. Default true; supply from the app's own config. */
19
+ meridiem?: boolean;
20
+ locale?: string;
21
+ size?: Responsive<Size>;
22
+ variant?: Variant;
23
+ disabled?: boolean;
24
+ placeholder?: string;
25
+ class?: string;
26
+ }
27
+ declare const TimeRangeInput: import("svelte").Component<Props, {}, "value">;
28
+ type TimeRangeInput = ReturnType<typeof TimeRangeInput>;
29
+ export default TimeRangeInput;
@@ -0,0 +1,202 @@
1
+ <!--
2
+ /**
3
+ * TimeSpinner — a compact iOS-style time stepper: a single row of HOUR : MINUTE : (AM·PM) columns,
4
+ * each showing just the current value with an up-chevron above and a down-chevron below. Tiny footprint,
5
+ * so it tucks under a Calendar in a combined date/time picker. 24h mode drops the meridiem column.
6
+ *
7
+ * Each column steps its own unit (wrapping), driven by the chevrons, the mouse wheel, or ↑/↓ when
8
+ * focused. Values are 24h `HH:MM`; labels follow the locale. Minute steps by `step`; `min`/`max`
9
+ * disable the chevrons that would leave range. Sibling of {@link TimeList} (the flat slot list).
10
+ */
11
+ -->
12
+ <script lang="ts">
13
+ import type { Size } from "../types/sizes.js";
14
+ import { responsiveClasses, type Responsive } from "../types/responsive.js";
15
+ import { variantToken, type Variant } from "../types/variants.js";
16
+ import { resolveStep, type StepSpec } from "../types/time.js";
17
+ import IconUp from "~icons/mdi/chevron-up";
18
+ import IconDown from "~icons/mdi/chevron-down";
19
+
20
+ interface Props {
21
+ /** Selected time, 24h `HH:MM` (or `HH:MM:SS` when `seconds`), or ''. Bindable. */
22
+ value?: string;
23
+ /** Column interval: a number = minute step, or `{ hour, minute, second }`. Default 1 (minute). */
24
+ step?: StepSpec;
25
+ /** Show a seconds column; value becomes `HH:MM:SS`. Default false. */
26
+ seconds?: boolean;
27
+ min?: string;
28
+ max?: string;
29
+ /** Show the AM/PM meridiem column (12h). `false` = 24h. Default true; the app supplies this from
30
+ * its own (server-side) locale/format config rather than sniffing the browser. */
31
+ meridiem?: boolean;
32
+ locale?: string;
33
+ size?: Responsive<Size>;
34
+ /** Accent for the focus ring. Default primary. */
35
+ variant?: Variant;
36
+ class?: string;
37
+ onchange?: (value: string) => void;
38
+ }
39
+
40
+ let {
41
+ value = $bindable(""),
42
+ step = 1,
43
+ seconds = false,
44
+ min,
45
+ max,
46
+ meridiem = true,
47
+ locale = typeof navigator !== "undefined" ? navigator.language : "en-US",
48
+ size = "md",
49
+ variant = "primary",
50
+ class: cls = "",
51
+ onchange,
52
+ }: Props = $props();
53
+
54
+ const accent = $derived(`var(${variantToken[variant]})`);
55
+
56
+ const pad = (n: number) => String(n).padStart(2, "0");
57
+ // Compose a value in the active format (adds :SS only when `seconds`).
58
+ const mk = (h: number, m: number, s: number = curS) =>
59
+ seconds ? `${pad(h)}:${pad(m)}:${pad(s)}` : `${pad(h)}:${pad(m)}`;
60
+ // Bounds may be given as HH:MM even in seconds mode — normalize so the string compare lines up.
61
+ const normB = (b: string | undefined) =>
62
+ b && seconds && b.split(":").length === 2 ? `${b}:00` : b;
63
+ const lo = $derived(normB(min));
64
+ const hi = $derived(normB(max));
65
+ // Pull a candidate into range. Independent columns mean a single step can momentarily land out of
66
+ // range (e.g. AM→PM keeping the hour) — clamp instead of rejecting, so it settles on the nearest
67
+ // valid time rather than trapping the user.
68
+ const clamp = (t: string) => (lo && t < lo ? lo : hi && t > hi ? hi : t);
69
+
70
+ const is12 = $derived(meridiem);
71
+
72
+ const base = $derived(value || min || (seconds ? "12:00:00" : "12:00"));
73
+ const curH = $derived(Number(base.split(":")[0])); // 0..23
74
+ const curM = $derived(Number(base.split(":")[1]));
75
+ const curS = $derived(Number(base.split(":")[2] ?? 0));
76
+ const curMer = $derived<"AM" | "PM">(curH < 12 ? "AM" : "PM");
77
+ const dispHour = $derived(is12 ? (curH % 12 === 0 ? 12 : curH % 12) : curH);
78
+
79
+ const steps = $derived(resolveStep(step));
80
+ // A 0..59 grid on `by`, always including the current (possibly off-grid) value so it stays selectable.
81
+ const grid60 = (by: number, cur: number) => {
82
+ const out: number[] = [];
83
+ for (let n = 0; n < 60; n += by) out.push(n);
84
+ if (!out.includes(cur)) {
85
+ out.push(cur);
86
+ out.sort((a, b) => a - b);
87
+ }
88
+ return out;
89
+ };
90
+ const minutes = $derived(grid60(steps.minute, curM));
91
+ const secondsGrid = $derived(grid60(steps.second, curS));
92
+
93
+ // Candidate value after stepping one column. HOUR steps in 24h space (wrapping 0–23) so it crosses
94
+ // noon naturally — 11 AM → 12 PM → 1 PM … — with the AM/PM display flipping as a consequence, rather
95
+ // than looping within one half-day. AM/PM flips a half-day (±12h). Minute/second wrap within their grid.
96
+ const stepHour = (dir: 1 | -1) => mk((curH + dir * steps.hour + 24) % 24, curM);
97
+ const stepMinute = (dir: 1 | -1) => {
98
+ const i = minutes.indexOf(curM);
99
+ return mk(curH, minutes[(i + dir + minutes.length) % minutes.length]);
100
+ };
101
+ const stepSecond = (dir: 1 | -1) => {
102
+ const i = secondsGrid.indexOf(curS);
103
+ return mk(curH, curM, secondsGrid[(i + dir + secondsGrid.length) % secondsGrid.length]);
104
+ };
105
+ const stepMer = () => mk((curH + 12) % 24, curM);
106
+
107
+ type Col = { display: string; label: string; up: string; down: string; colon?: boolean };
108
+ const cols = $derived.by<Col[]>(() => {
109
+ const list: Col[] = [
110
+ {
111
+ display: is12 ? String(dispHour) : pad(curH),
112
+ label: "Hour",
113
+ up: stepHour(1),
114
+ down: stepHour(-1),
115
+ },
116
+ { display: pad(curM), label: "Minute", up: stepMinute(1), down: stepMinute(-1), colon: true },
117
+ ];
118
+ if (seconds)
119
+ list.push({
120
+ display: pad(curS),
121
+ label: "Second",
122
+ up: stepSecond(1),
123
+ down: stepSecond(-1),
124
+ colon: true,
125
+ });
126
+ if (is12) list.push({ display: curMer, label: "AM/PM", up: stepMer(), down: stepMer() });
127
+ return list;
128
+ });
129
+
130
+ function apply(cand: string) {
131
+ value = clamp(cand);
132
+ onchange?.(value);
133
+ }
134
+ // A chevron is dead only when stepping can't move off the current value (already at that bound).
135
+ const noStep = (cand: string) => clamp(cand) === base;
136
+
137
+ // Each field is a per-Size class map → responsiveClasses() so `size` may be scalar OR responsive.
138
+ const valMap: Record<Size, string> = {
139
+ sm: "text-base w-9 py-0.5",
140
+ md: "text-lg w-11 py-1",
141
+ lg: "text-2xl w-14 py-1.5",
142
+ };
143
+ const chevMap: Record<Size, string> = { sm: "size-5", md: "size-6", lg: "size-7" };
144
+ const colMap: Record<Size, string> = { sm: "text-xs", md: "text-sm", lg: "text-sm" };
145
+ const sizing = $derived({
146
+ val: responsiveClasses(size, valMap),
147
+ chev: responsiveClasses(size, chevMap),
148
+ col: responsiveClasses(size, colMap),
149
+ });
150
+ const chevBtn =
151
+ "grid place-items-center rounded-[var(--ui-border-radius)] [color:color-mix(in_srgb,var(--ui-color-text)_55%,transparent)] transition-colors enabled:hover:[background-color:var(--ui-color-hover)] enabled:active:[background-color:var(--ui-color-active)] enabled:hover:[color:var(--ui-color-text)] disabled:opacity-25 disabled:cursor-not-allowed";
152
+ </script>
153
+
154
+ <div
155
+ class="inline-flex items-center gap-1 [color:var(--ui-color-text)] {cls}"
156
+ style="--accent: {accent}"
157
+ >
158
+ {#each cols as c (c.label)}
159
+ {#if c.colon}<span class="{sizing.val} grid place-items-center pb-0 font-semibold opacity-60"
160
+ >:</span
161
+ >{/if}
162
+ <div class="flex flex-col items-center">
163
+ <button
164
+ type="button"
165
+ aria-label={`${c.label} up`}
166
+ class="{chevBtn} {sizing.chev} w-full"
167
+ disabled={noStep(c.up)}
168
+ onclick={() => apply(c.up)}><IconUp /></button
169
+ >
170
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
171
+ <div
172
+ role="spinbutton"
173
+ aria-label={c.label}
174
+ aria-valuetext={c.display}
175
+ tabindex="0"
176
+ class="{sizing.val} grid cursor-ns-resize place-items-center rounded-[var(--ui-border-radius)] text-center font-semibold tabular-nums select-none focus:outline-none focus-visible:[outline:2px_solid_var(--accent)]"
177
+ onwheel={(e) => {
178
+ e.preventDefault();
179
+ apply(e.deltaY > 0 ? c.down : c.up);
180
+ }}
181
+ onkeydown={(e) => {
182
+ if (e.key === "ArrowUp") {
183
+ e.preventDefault();
184
+ apply(c.up);
185
+ } else if (e.key === "ArrowDown") {
186
+ e.preventDefault();
187
+ apply(c.down);
188
+ }
189
+ }}
190
+ >
191
+ {c.display}
192
+ </div>
193
+ <button
194
+ type="button"
195
+ aria-label={`${c.label} down`}
196
+ class="{chevBtn} {sizing.chev} w-full"
197
+ disabled={noStep(c.down)}
198
+ onclick={() => apply(c.down)}><IconDown /></button
199
+ >
200
+ </div>
201
+ {/each}
202
+ </div>
@@ -0,0 +1,26 @@
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
+ interface Props {
6
+ /** Selected time, 24h `HH:MM` (or `HH:MM:SS` when `seconds`), or ''. Bindable. */
7
+ value?: string;
8
+ /** Column interval: a number = minute step, or `{ hour, minute, second }`. Default 1 (minute). */
9
+ step?: StepSpec;
10
+ /** Show a seconds column; value becomes `HH:MM:SS`. Default false. */
11
+ seconds?: boolean;
12
+ min?: string;
13
+ max?: string;
14
+ /** Show the AM/PM meridiem column (12h). `false` = 24h. Default true; the app supplies this from
15
+ * its own (server-side) locale/format config rather than sniffing the browser. */
16
+ meridiem?: boolean;
17
+ locale?: string;
18
+ size?: Responsive<Size>;
19
+ /** Accent for the focus ring. Default primary. */
20
+ variant?: Variant;
21
+ class?: string;
22
+ onchange?: (value: string) => void;
23
+ }
24
+ declare const TimeSpinner: import("svelte").Component<Props, {}, "value">;
25
+ type TimeSpinner = ReturnType<typeof TimeSpinner>;
26
+ export default TimeSpinner;