@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,51 @@
1
+ <script lang="ts">
2
+ /**
3
+ * Renders the toast store's items in a fixed, top-layer stack. Mount ONCE at the app root; push with
4
+ * `toast.*` from anywhere. Each toast is an Alert (filled) honouring its variant + size.
5
+ */
6
+ import Alert from "../Alert.svelte";
7
+ import IconClose from "~icons/mdi/close";
8
+ import { getToasts, dismissToast } from "./toast.svelte.js";
9
+ import type { Size } from "../../types/sizes.js";
10
+ import type { Responsive } from "../../types/responsive.js";
11
+ import type { Variant } from "../../types/variants.js";
12
+
13
+ let {
14
+ position = "bottom-right",
15
+ variant,
16
+ size = "md",
17
+ }: {
18
+ position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
19
+ /** Default variant/size for toasts that don't set their own (each toast can still override). */
20
+ variant?: Variant;
21
+ size?: Responsive<Size>;
22
+ } = $props();
23
+ const items = getToasts();
24
+
25
+ const posClass = {
26
+ "bottom-right": "bottom-4 right-4 items-end",
27
+ "bottom-left": "bottom-4 left-4 items-start",
28
+ "top-right": "top-4 right-4 items-end",
29
+ "top-left": "top-4 left-4 items-start",
30
+ }[position];
31
+ </script>
32
+
33
+ {#if items.length}
34
+ <div class="pointer-events-none fixed z-[100] flex max-w-[90vw] flex-col gap-2 {posClass}">
35
+ {#each items as t (t.id)}
36
+ <div
37
+ class="pointer-events-auto flex min-w-56 items-start gap-2 rounded shadow-lg [background-color:var(--ui-color-background)]"
38
+ >
39
+ <Alert variant={t.variant ?? variant} size={t.size ?? size} filled class="grow"
40
+ >{t.message}</Alert
41
+ >
42
+ <button
43
+ type="button"
44
+ aria-label="Dismiss"
45
+ class="shrink-0 self-center pr-2 opacity-60 hover:opacity-100"
46
+ onclick={() => dismissToast(t.id)}><IconClose class="size-4" /></button
47
+ >
48
+ </div>
49
+ {/each}
50
+ </div>
51
+ {/if}
@@ -0,0 +1,12 @@
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
+ type $$ComponentProps = {
5
+ position?: "bottom-right" | "bottom-left" | "top-right" | "top-left";
6
+ /** Default variant/size for toasts that don't set their own (each toast can still override). */
7
+ variant?: Variant;
8
+ size?: Responsive<Size>;
9
+ };
10
+ declare const Toaster: import("svelte").Component<$$ComponentProps, {}, "">;
11
+ type Toaster = ReturnType<typeof Toaster>;
12
+ export default Toaster;
@@ -0,0 +1,29 @@
1
+ import type { Size } from "../../types/sizes.js";
2
+ /**
3
+ * Toast store — module-level runes state so toasts live OUTSIDE any page. Push from anywhere with
4
+ * `toast.danger('…')` etc.; a single <Toaster /> mounted at the app root renders them. Honours the
5
+ * shared Variant (status subset) + Size axes. Auto-dismisses after `timeout` ms (0 = sticky).
6
+ */
7
+ export type ToastVariant = "danger" | "success" | "warning" | "info" | "primary";
8
+ export type ToastItem = {
9
+ id: number;
10
+ variant: ToastVariant;
11
+ message: string;
12
+ size: Size;
13
+ timeout: number;
14
+ };
15
+ export type ToastOptions = {
16
+ timeout?: number;
17
+ size?: Size;
18
+ };
19
+ /** The live toast list (read in <Toaster>; mutations are reactive). */
20
+ export declare function getToasts(): ToastItem[];
21
+ export declare function dismissToast(id: number): void;
22
+ export declare const toast: {
23
+ show: (variant: ToastVariant, message: string, opts?: ToastOptions) => number;
24
+ danger: (message: string, opts?: ToastOptions) => number;
25
+ success: (message: string, opts?: ToastOptions) => number;
26
+ warning: (message: string, opts?: ToastOptions) => number;
27
+ info: (message: string, opts?: ToastOptions) => number;
28
+ dismiss: typeof dismissToast;
29
+ };
@@ -0,0 +1,27 @@
1
+ const items = $state([]);
2
+ let seq = 0;
3
+ /** The live toast list (read in <Toaster>; mutations are reactive). */
4
+ export function getToasts() {
5
+ return items;
6
+ }
7
+ export function dismissToast(id) {
8
+ const i = items.findIndex((t) => t.id === id);
9
+ if (i >= 0)
10
+ items.splice(i, 1);
11
+ }
12
+ function push(variant, message, opts = {}) {
13
+ const id = ++seq;
14
+ const timeout = opts.timeout ?? 4000;
15
+ items.push({ id, variant, message, size: opts.size ?? "md", timeout });
16
+ if (timeout > 0 && typeof window !== "undefined")
17
+ window.setTimeout(() => dismissToast(id), timeout);
18
+ return id;
19
+ }
20
+ export const toast = {
21
+ show: (variant, message, opts) => push(variant, message, opts),
22
+ danger: (message, opts) => push("danger", message, opts),
23
+ success: (message, opts) => push("success", message, opts),
24
+ warning: (message, opts) => push("warning", message, opts),
25
+ info: (message, opts) => push("info", message, opts),
26
+ dismiss: dismissToast,
27
+ };
@@ -0,0 +1,93 @@
1
+ <!--
2
+ /**
3
+ * Toggle — a sliding on/off switch (à la a physical switch). Use this, NOT a Checkbox, for binary
4
+ * state: enabled/disabled, active/inactive, on/off. A real `<input type=checkbox>` drives it — laid
5
+ * INVISIBLY over the whole control, so clicking anywhere toggles it (naturally clickable +
6
+ * testable + accessible + form-friendly; `bind:checked` works, and `onchange` fires a real event).
7
+ * Honours the shared {@link Variant} (the "on" colour) + {@link Size} axes. Self-contained: no external
8
+ * label needed — pass an `aria-label` when there's no adjacent text.
9
+ *
10
+ * ALIGNMENT: the visible switch is deliberately shorter than a text control, but the Toggle occupies a
11
+ * wrapper sized to the standard control HEIGHT for its `size` (sm 24 / md 32 / lg 40px — same as Input),
12
+ * with the switch centred inside. So a Toggle dropped next to `size`-matched Inputs/Selects lines up on
13
+ * the same baseline (and the full-height wrapper is the click target), while the switch itself stays
14
+ * switch-sized rather than ballooning to the input height.
15
+ */
16
+ -->
17
+ <script lang="ts">
18
+ import type { HTMLInputAttributes } from "svelte/elements";
19
+ import type { Size } from "../types/sizes.js";
20
+ import { responsiveClasses, resolveScalar, type Responsive } from "../types/responsive.js";
21
+ import { variantToken, type Variant } from "../types/variants.js";
22
+
23
+ let {
24
+ checked = $bindable(false),
25
+ disabled = false,
26
+ size = "md",
27
+ variant = "primary",
28
+ class: cls = "",
29
+ ...rest
30
+ }: {
31
+ checked?: boolean;
32
+ disabled?: boolean;
33
+ size?: Responsive<Size>;
34
+ /** The colour of the "on" state (default primary). */
35
+ variant?: Variant;
36
+ class?: string;
37
+ } & Omit<HTMLInputAttributes, "size" | "type" | "class"> = $props();
38
+
39
+ // Per size: track w/h + knob size are CLASSES (responsive); knob travel is an inline-style transform
40
+ // (JS value → keys off the base tier). Knob sits 2px in, travels W − K − 4px.
41
+ const trackMap: Record<Size, string> = { sm: "h-4 w-7", md: "h-5 w-9", lg: "h-6 w-11" };
42
+ const knobMap: Record<Size, string> = { sm: "size-3", md: "size-4", lg: "size-5" };
43
+ const onMap: Record<Size, string> = {
44
+ sm: "translateX(12px)",
45
+ md: "translateX(16px)",
46
+ lg: "translateX(20px)",
47
+ };
48
+ const heightMap: Record<Size, string> = { sm: "h-6", md: "h-8", lg: "h-10" };
49
+
50
+ const trackCls = $derived(responsiveClasses(size, trackMap));
51
+ const knobCls = $derived(responsiveClasses(size, knobMap));
52
+ // Wrapper height = the standard control height for this size (matches Input's h-6/h-8/h-10), so the
53
+ // switch centres on the same baseline as sibling inputs without growing the switch itself.
54
+ const heightClass = $derived(responsiveClasses(size, heightMap));
55
+ const trackStyle = $derived(
56
+ `background-color: ${checked ? `var(${variantToken[variant]})` : "color-mix(in srgb, var(--ui-color-text) 22%, transparent)"}`,
57
+ );
58
+ // Knob colour = the INVERTED text colour (`--ui-color-background`) with a hint of the variant mixed
59
+ // in — ONE rule for every variant, both themes. Light: background is white → a near-white knob (as
60
+ // before). Dark: background is dark → the knob flips dark, and since the dark theme's accent fills
61
+ // are brighter, a dark knob still reads. The 20% variant tint keeps the knob on-theme; it never
62
+ // approaches the track colour (80% background), so it stays contrasting even on a neutral track.
63
+ const knobColor = $derived(
64
+ `color-mix(in srgb, var(--ui-color-background) 80%, var(${variantToken[variant]}))`,
65
+ );
66
+ const knobStyle = $derived(
67
+ `transform: ${checked ? onMap[resolveScalar(size, "md")] : "translateX(2px)"}; background-color: ${knobColor}`,
68
+ );
69
+ </script>
70
+
71
+ <label
72
+ class="relative inline-flex shrink-0 items-center {heightClass} {disabled
73
+ ? 'cursor-not-allowed opacity-50'
74
+ : 'cursor-pointer'} {cls}"
75
+ >
76
+ <!-- Real checkbox, invisible but covering the whole control so a click anywhere toggles it. -->
77
+ <input
78
+ type="checkbox"
79
+ bind:checked
80
+ {disabled}
81
+ class="peer absolute inset-0 z-10 m-0 cursor-pointer opacity-0 disabled:cursor-not-allowed"
82
+ {...rest}
83
+ />
84
+ <span
85
+ class="relative inline-flex items-center rounded-full transition-colors duration-150 peer-focus-visible:outline peer-focus-visible:outline-2 peer-focus-visible:outline-offset-2 peer-focus-visible:[outline-color:var(--ui-color-primary)] {trackCls}"
86
+ style={trackStyle}
87
+ >
88
+ <span
89
+ class="inline-block rounded-full shadow transition-transform duration-150 {knobCls}"
90
+ style={knobStyle}
91
+ ></span>
92
+ </span>
93
+ </label>
@@ -0,0 +1,15 @@
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 $$ComponentProps = {
6
+ checked?: boolean;
7
+ disabled?: boolean;
8
+ size?: Responsive<Size>;
9
+ /** The colour of the "on" state (default primary). */
10
+ variant?: Variant;
11
+ class?: string;
12
+ } & Omit<HTMLInputAttributes, "size" | "type" | "class">;
13
+ declare const Toggle: import("svelte").Component<$$ComponentProps, {}, "checked">;
14
+ type Toggle = ReturnType<typeof Toggle>;
15
+ export default Toggle;
@@ -0,0 +1,42 @@
1
+ export type FilterDef = {
2
+ kind: "text";
3
+ } | {
4
+ kind: "number";
5
+ } | {
6
+ kind: "date";
7
+ } | {
8
+ kind: "boolean";
9
+ } | {
10
+ kind: "enum";
11
+ options: {
12
+ value: string;
13
+ label?: string;
14
+ }[];
15
+ };
16
+ /** A column the query language can filter on (drives the `$` popup + enum-value autocomplete). */
17
+ export type QueryColumn = {
18
+ id: string;
19
+ label?: string;
20
+ filter?: FilterDef;
21
+ };
22
+ /**
23
+ * The minimal surface <Query> needs to drive *any* dataset: the filterable columns + the
24
+ * draft/applied query state. A DataGrid implements this and adds rows/sort/paging/selection.
25
+ */
26
+ export interface DataSet {
27
+ readonly queryColumns: QueryColumn[];
28
+ /** Draft query text (mid-edit). */
29
+ query: string;
30
+ /** Committed/applied query text. */
31
+ readonly applied: string;
32
+ /** Live parse error for the draft, or null. */
33
+ readonly error: string | null;
34
+ /** Draft differs from applied. */
35
+ readonly dirty: boolean;
36
+ /** Type -> validate, but don't apply/fetch. */
37
+ setDraft(raw: string): void;
38
+ /** Apply the current draft (Enter). */
39
+ commit(): void;
40
+ /** Set + apply immediately (clear / URL seed). */
41
+ setQuery(raw: string): void;
42
+ }
@@ -0,0 +1,3 @@
1
+ // The shared contract that <Query> manipulates and a data source (e.g. DataGrid) implements.
2
+ // Framework-agnostic types — no Svelte. See .claude/skills/ui-design.
3
+ export {};
@@ -0,0 +1,3 @@
1
+ export * from "./dataset";
2
+ export * from "./query";
3
+ export * from "./table";
@@ -0,0 +1,3 @@
1
+ export * from "./dataset";
2
+ export * from "./query";
3
+ export * from "./table";
@@ -0,0 +1,62 @@
1
+ export type CompareOp = "eq" | "ne" | "gt" | "gte" | "lt" | "lte" | "contains" | "ncontains" | "starts" | "ends";
2
+ export type Scalar = string | number | boolean;
3
+ /** Explicit value/column cast on a comparison, e.g. `$ip |inet >= 10.0.0.0 |inet`. Storage-agnostic:
4
+ * the query layer only carries the marker; the consumer (`toConditions`) decides the SQL (e.g.
5
+ * Postgres `::inet`). `number`/`string`/`bool` also retype the literal at parse time. */
6
+ export type Cast = "number" | "string" | "bool" | "date" | "inet";
7
+ /** Membership mode: `|=` matches ANY of the operand, `&=` matches ALL (e.g. array contains-all). */
8
+ export type MemberMode = "any" | "all";
9
+ /** One end of an interval. `inclusive` reflects the bracket: `[`/`]` inclusive, `(`/`)` exclusive. */
10
+ export type Bound = {
11
+ value: Scalar;
12
+ inclusive: boolean;
13
+ };
14
+ /** `null` bound = open/unbounded on that side (e.g. `[20..)` -> upper is null). */
15
+ export type Interval = {
16
+ lower: Bound | null;
17
+ upper: Bound | null;
18
+ };
19
+ export type QueryNode = {
20
+ kind: "and";
21
+ children: QueryNode[];
22
+ } | {
23
+ kind: "or";
24
+ children: QueryNode[];
25
+ } | {
26
+ kind: "not";
27
+ child: QueryNode;
28
+ } | {
29
+ kind: "cmp";
30
+ col: string;
31
+ op: CompareOp;
32
+ value: Scalar;
33
+ cast?: Cast;
34
+ } | {
35
+ kind: "match";
36
+ col: string;
37
+ negated: boolean;
38
+ source: string;
39
+ flags: string;
40
+ } | {
41
+ kind: "in";
42
+ col: string;
43
+ mode: MemberMode;
44
+ values: Scalar[];
45
+ cast?: Cast;
46
+ } | {
47
+ kind: "range";
48
+ col: string;
49
+ mode: MemberMode;
50
+ interval: Interval;
51
+ cast?: Cast;
52
+ } | {
53
+ kind: "search";
54
+ text: string;
55
+ };
56
+ export declare class QueryError extends Error {
57
+ /** 0-based character offset into the source where the problem is. */
58
+ position: number;
59
+ constructor(message: string,
60
+ /** 0-based character offset into the source where the problem is. */
61
+ position: number);
62
+ }
@@ -0,0 +1,12 @@
1
+ // AST for the data-table query language. Framework-agnostic — no Svelte/Drizzle/SvelteKit deps,
2
+ // so it runs in the browser (Query input), on the server (toConditions), and in unit tests.
3
+ export class QueryError extends Error {
4
+ position;
5
+ constructor(message,
6
+ /** 0-based character offset into the source where the problem is. */
7
+ position) {
8
+ super(message);
9
+ this.position = position;
10
+ this.name = "QueryError";
11
+ }
12
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./ast";
2
+ export { parseQuery, tryParseQuery, type ParseResult } from "./parser";
3
+ export { lex, type Token } from "./lexer";
@@ -0,0 +1,3 @@
1
+ export * from "./ast";
2
+ export { parseQuery, tryParseQuery } from "./parser";
3
+ export { lex } from "./lexer";
@@ -0,0 +1,33 @@
1
+ import { type CompareOp, type MemberMode } from "./ast";
2
+ export type Token = {
3
+ type: "dollar" | "and" | "or" | "not" | "lparen" | "rparen" | "lbracket" | "rbracket" | "comma" | "dotdot" | "eof";
4
+ start: number;
5
+ end: number;
6
+ } | {
7
+ type: "op";
8
+ op: CompareOp;
9
+ start: number;
10
+ end: number;
11
+ } | {
12
+ type: "member";
13
+ mode: MemberMode;
14
+ start: number;
15
+ end: number;
16
+ } | {
17
+ type: "cast";
18
+ name: string;
19
+ start: number;
20
+ end: number;
21
+ } | {
22
+ type: "word" | "string";
23
+ value: string;
24
+ start: number;
25
+ end: number;
26
+ } | {
27
+ type: "regex";
28
+ source: string;
29
+ flags: string;
30
+ start: number;
31
+ end: number;
32
+ };
33
+ export declare function lex(input: string): Token[];
@@ -0,0 +1,225 @@
1
+ import { QueryError } from "./ast";
2
+ const isSpace = (c) => c === " " || c === "\t" || c === "\n" || c === "\r" || c === "\f" || c === "\v";
3
+ // Structural characters that break a bare token. Everything else (letters, digits, and ordinary
4
+ // punctuation like @ . / : % # + - _ * ?) is allowed unquoted. `/` is allowed *inside* a word
5
+ // (cat/sub) but starts a regex when it leads a token; a word also breaks at `..`.
6
+ const BREAK = new Set(['"', "(", ")", "[", "]", ",", "$", "^", "~", "!", "=", ">", "<", "&", "|"]);
7
+ const isWordChar = (c) => !isSpace(c) && !BREAK.has(c);
8
+ export function lex(input) {
9
+ const tokens = [];
10
+ const n = input.length;
11
+ let i = 0;
12
+ while (i < n) {
13
+ const c = input[i];
14
+ if (isSpace(c)) {
15
+ i++;
16
+ continue;
17
+ }
18
+ const start = i;
19
+ const next = input[i + 1];
20
+ switch (c) {
21
+ case "(":
22
+ i++;
23
+ tokens.push({ type: "lparen", start, end: i });
24
+ continue;
25
+ case ")":
26
+ i++;
27
+ tokens.push({ type: "rparen", start, end: i });
28
+ continue;
29
+ case "[":
30
+ i++;
31
+ tokens.push({ type: "lbracket", start, end: i });
32
+ continue;
33
+ case "]":
34
+ i++;
35
+ tokens.push({ type: "rbracket", start, end: i });
36
+ continue;
37
+ case ",":
38
+ i++;
39
+ tokens.push({ type: "comma", start, end: i });
40
+ continue;
41
+ case '"': {
42
+ i++;
43
+ let s = "";
44
+ while (i < n && input[i] !== '"') {
45
+ if (input[i] === "\\" && i + 1 < n) {
46
+ i++;
47
+ s += input[i];
48
+ i++;
49
+ }
50
+ else {
51
+ s += input[i];
52
+ i++;
53
+ }
54
+ }
55
+ if (i >= n)
56
+ throw new QueryError("Unterminated string", start);
57
+ i++;
58
+ tokens.push({ type: "string", value: s, start, end: i });
59
+ continue;
60
+ }
61
+ case "/": {
62
+ // regex literal: /source/flags (escapes preserved in source)
63
+ i++;
64
+ let src = "";
65
+ while (i < n && input[i] !== "/") {
66
+ if (input[i] === "\\" && i + 1 < n) {
67
+ src += input[i] + input[i + 1];
68
+ i += 2;
69
+ }
70
+ else {
71
+ src += input[i];
72
+ i++;
73
+ }
74
+ }
75
+ if (i >= n)
76
+ throw new QueryError("Unterminated regex", start);
77
+ i++; // closing /
78
+ let flags = "";
79
+ while (i < n && /[a-zA-Z]/.test(input[i])) {
80
+ flags += input[i];
81
+ i++;
82
+ }
83
+ tokens.push({ type: "regex", source: src, flags, start, end: i });
84
+ continue;
85
+ }
86
+ }
87
+ if (c === "&") {
88
+ if (next === "&") {
89
+ i += 2;
90
+ tokens.push({ type: "and", start, end: i });
91
+ }
92
+ else if (next === "=") {
93
+ i += 2;
94
+ tokens.push({ type: "member", mode: "all", start, end: i });
95
+ }
96
+ else
97
+ throw new QueryError("Expected '&&' or '&='", i);
98
+ continue;
99
+ }
100
+ if (c === "|") {
101
+ if (next === "|") {
102
+ i += 2;
103
+ tokens.push({ type: "or", start, end: i });
104
+ }
105
+ else if (next === "=") {
106
+ i += 2;
107
+ tokens.push({ type: "member", mode: "any", start, end: i });
108
+ }
109
+ else if (next && /[a-zA-Z]/.test(next)) {
110
+ // `|word` is a cast/transformer (|inet, |number, …); validated in the parser.
111
+ i++;
112
+ let name = "";
113
+ while (i < n && /[a-zA-Z]/.test(input[i])) {
114
+ name += input[i];
115
+ i++;
116
+ }
117
+ tokens.push({ type: "cast", name, start, end: i });
118
+ }
119
+ else
120
+ throw new QueryError("Expected '||', '|=', or a cast (e.g. |inet)", i);
121
+ continue;
122
+ }
123
+ if (c === "!") {
124
+ if (next === "=") {
125
+ i += 2;
126
+ tokens.push({ type: "op", op: "ne", start, end: i });
127
+ }
128
+ else if (next === "~") {
129
+ i += 2;
130
+ tokens.push({ type: "op", op: "ncontains", start, end: i });
131
+ }
132
+ else {
133
+ i++;
134
+ tokens.push({ type: "not", start, end: i });
135
+ }
136
+ continue;
137
+ }
138
+ if (c === "=") {
139
+ if (next === "=") {
140
+ i += 2;
141
+ tokens.push({ type: "op", op: "eq", start, end: i });
142
+ }
143
+ else
144
+ throw new QueryError("Expected '=='", i);
145
+ continue;
146
+ }
147
+ if (c === ">") {
148
+ if (next === "=") {
149
+ i += 2;
150
+ tokens.push({ type: "op", op: "gte", start, end: i });
151
+ }
152
+ else {
153
+ i++;
154
+ tokens.push({ type: "op", op: "gt", start, end: i });
155
+ }
156
+ continue;
157
+ }
158
+ if (c === "<") {
159
+ if (next === "=") {
160
+ i += 2;
161
+ tokens.push({ type: "op", op: "lte", start, end: i });
162
+ }
163
+ else {
164
+ i++;
165
+ tokens.push({ type: "op", op: "lt", start, end: i });
166
+ }
167
+ continue;
168
+ }
169
+ if (c === "~") {
170
+ if (next === "=") {
171
+ i += 2;
172
+ tokens.push({ type: "op", op: "contains", start, end: i });
173
+ }
174
+ else
175
+ throw new QueryError("Expected '~='", i);
176
+ continue;
177
+ }
178
+ if (c === "^") {
179
+ if (next === "=") {
180
+ i += 2;
181
+ tokens.push({ type: "op", op: "starts", start, end: i });
182
+ }
183
+ else
184
+ throw new QueryError("Expected '^='", i);
185
+ continue;
186
+ }
187
+ if (c === "$") {
188
+ // `$=` is the ends-with operator; otherwise `$` is the clause sigil.
189
+ if (next === "=") {
190
+ i += 2;
191
+ tokens.push({ type: "op", op: "ends", start, end: i });
192
+ }
193
+ else {
194
+ i++;
195
+ tokens.push({ type: "dollar", start, end: i });
196
+ }
197
+ continue;
198
+ }
199
+ if (c === "." && next === ".") {
200
+ i += 2;
201
+ tokens.push({ type: "dotdot", start, end: i });
202
+ continue;
203
+ }
204
+ // a single '.' is an ordinary word char (.com, 99.5) — falls through to the word branch.
205
+ if (isWordChar(c)) {
206
+ let w = "";
207
+ while (i < n) {
208
+ const ch = input[i];
209
+ if (ch === "." && input[i + 1] === ".")
210
+ break; // '..' is the range operator
211
+ if (isWordChar(ch)) {
212
+ w += ch;
213
+ i++;
214
+ }
215
+ else
216
+ break;
217
+ }
218
+ tokens.push({ type: "word", value: w, start, end: i });
219
+ continue;
220
+ }
221
+ throw new QueryError(`Unexpected character '${c}'`, i);
222
+ }
223
+ tokens.push({ type: "eof", start: i, end: i });
224
+ return tokens;
225
+ }
@@ -0,0 +1,11 @@
1
+ import { type QueryNode } from "./ast";
2
+ export declare function parseQuery(input: string): QueryNode | null;
3
+ export type ParseResult = {
4
+ ok: true;
5
+ ast: QueryNode | null;
6
+ } | {
7
+ ok: false;
8
+ error: string;
9
+ position: number;
10
+ };
11
+ export declare function tryParseQuery(input: string): ParseResult;