@spaethtech/svelte-ui 0.1.10 → 0.3.1-dev.3.e16df24

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 +180 -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,140 @@
1
+ <!--
2
+ /**
3
+ * Popup — an interactive, click/programmatically-opened popover anchored to a trigger.
4
+ *
5
+ * Uses the Popover API (browser top layer, so it's never clipped by overflow/stacking contexts)
6
+ * for show/hide, and the shared `anchored` engine for placement (flip up/down vs the boundary —
7
+ * viewport by default, or a container). Dismisses on Escape and outside pointerdown.
8
+ *
9
+ * @example
10
+ * ```svelte
11
+ * <button bind:this={btn} onclick={() => open = !open}>Menu</button>
12
+ * <Popup {anchor} bind:open>
13
+ * <div class="...your content...">…</div>
14
+ * </Popup>
15
+ * ```
16
+ */
17
+ -->
18
+ <script lang="ts">
19
+ import type { Snippet } from "svelte";
20
+ import {
21
+ anchored,
22
+ type Side,
23
+ type Align,
24
+ type Boundary,
25
+ type Placement,
26
+ } from "../positioning/anchored.js";
27
+ import type { Size } from "../types/sizes.js";
28
+ import { responsiveClasses, type Responsive } from "../types/responsive.js";
29
+ import { variantToken, type Variant } from "../types/variants.js";
30
+
31
+ let {
32
+ anchor,
33
+ open = $bindable(false),
34
+ side = "bottom",
35
+ align = "start",
36
+ boundary = "viewport",
37
+ offset = 4,
38
+ matchWidth = false,
39
+ surface = false,
40
+ variant,
41
+ size = "md",
42
+ borderless = false,
43
+ onclose,
44
+ children,
45
+ }: {
46
+ /** The trigger element to position against. */
47
+ anchor: HTMLElement | undefined;
48
+ open?: boolean;
49
+ side?: Side;
50
+ align?: Align;
51
+ /** Default the viewport; pass a container element or CSS selector to bound flipping to it. */
52
+ boundary?: Boundary;
53
+ offset?: number;
54
+ matchWidth?: boolean;
55
+ /** Render a themed floating PANEL around the children (background, border, padding, shadow) instead
56
+ * of the default bare positioner. `variant`/`size`/`borderless` configure that panel. Consumers
57
+ * that supply their own surface (Menu, date/time pickers) leave this `false`. */
58
+ surface?: boolean;
59
+ /** Panel border colour (when `surface`) — tinted by the variant token, like Card. */
60
+ variant?: Variant;
61
+ /** Panel padding tier (when `surface`). */
62
+ size?: Responsive<Size>;
63
+ /** Drop the panel's visible border (border = its own background). */
64
+ borderless?: boolean;
65
+ onclose?: () => void;
66
+ children: Snippet;
67
+ } = $props();
68
+
69
+ // Themed-panel styling (only used when `surface`). Border tints from the variant token; `borderless`
70
+ // paints it the panel's background. Padding scales with `size`.
71
+ const padMap: Record<Size, string> = { sm: "p-1", md: "p-1.5", lg: "p-2" };
72
+ const accentToken = $derived(variant ? variantToken[variant] : "--ui-color-secondary");
73
+ const panelBorder = $derived(
74
+ borderless
75
+ ? "var(--ui-color-background)"
76
+ : `color-mix(in srgb, var(${accentToken}) var(--ui-tint-border), transparent)`,
77
+ );
78
+ const surfaceClass = $derived(
79
+ surface
80
+ ? `rounded-md border shadow-lg ${responsiveClasses(size, padMap)} [background-color:var(--ui-color-background)] [color:var(--ui-color-text)]`
81
+ : "bg-transparent p-0 border-0",
82
+ );
83
+
84
+ let el = $state<HTMLElement>();
85
+ let placed = $state<Placement | null>(null);
86
+
87
+ function close() {
88
+ open = false;
89
+ onclose?.();
90
+ }
91
+
92
+ // Drive the Popover API (top layer) from `open`.
93
+ $effect(() => {
94
+ if (!el) return;
95
+ const isOpen = el.matches(":popover-open");
96
+ if (open && !isOpen) el.showPopover();
97
+ else if (!open && isOpen) el.hidePopover();
98
+ });
99
+
100
+ // Dismiss on Escape / outside pointerdown while open.
101
+ $effect(() => {
102
+ if (!open) return;
103
+ const onKey = (e: KeyboardEvent) => {
104
+ if (e.key === "Escape") {
105
+ e.preventDefault();
106
+ close();
107
+ }
108
+ };
109
+ const onPointer = (e: PointerEvent) => {
110
+ const t = e.target as Node;
111
+ if (el?.contains(t) || anchor?.contains(t)) return;
112
+ close();
113
+ };
114
+ document.addEventListener("keydown", onKey, true);
115
+ document.addEventListener("pointerdown", onPointer, true);
116
+ return () => {
117
+ document.removeEventListener("keydown", onKey, true);
118
+ document.removeEventListener("pointerdown", onPointer, true);
119
+ };
120
+ });
121
+ </script>
122
+
123
+ <div
124
+ bind:this={el}
125
+ popover="manual"
126
+ class="m-0 overflow-visible {surfaceClass}"
127
+ style={surface ? `border-color: ${panelBorder};` : ""}
128
+ use:anchored={{
129
+ anchor,
130
+ enabled: open,
131
+ side,
132
+ align,
133
+ boundary,
134
+ offset,
135
+ matchWidth,
136
+ onplaced: (p) => (placed = p),
137
+ }}
138
+ >
139
+ {@render children()}
140
+ </div>
@@ -0,0 +1,31 @@
1
+ import type { Snippet } from "svelte";
2
+ import { type Side, type Align, type Boundary } from "../positioning/anchored.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
+ type $$ComponentProps = {
7
+ /** The trigger element to position against. */
8
+ anchor: HTMLElement | undefined;
9
+ open?: boolean;
10
+ side?: Side;
11
+ align?: Align;
12
+ /** Default the viewport; pass a container element or CSS selector to bound flipping to it. */
13
+ boundary?: Boundary;
14
+ offset?: number;
15
+ matchWidth?: boolean;
16
+ /** Render a themed floating PANEL around the children (background, border, padding, shadow) instead
17
+ * of the default bare positioner. `variant`/`size`/`borderless` configure that panel. Consumers
18
+ * that supply their own surface (Menu, date/time pickers) leave this `false`. */
19
+ surface?: boolean;
20
+ /** Panel border colour (when `surface`) — tinted by the variant token, like Card. */
21
+ variant?: Variant;
22
+ /** Panel padding tier (when `surface`). */
23
+ size?: Responsive<Size>;
24
+ /** Drop the panel's visible border (border = its own background). */
25
+ borderless?: boolean;
26
+ onclose?: () => void;
27
+ children: Snippet;
28
+ };
29
+ declare const Popup: import("svelte").Component<$$ComponentProps, {}, "open">;
30
+ type Popup = ReturnType<typeof Popup>;
31
+ export default Popup;
@@ -0,0 +1,284 @@
1
+ <!--
2
+ /**
3
+ * Query — a filter bar over a DataSet (svelte-ui/data): a monospace input with `$` column +
4
+ * enum-value autocomplete (in a Popup, so it flips vs the viewport), Enter-to-apply, and a help
5
+ * dialog. URL sync is intentionally NOT built in — observe `dataset.applied` and sync it yourself
6
+ * (it's framework-specific; e.g. SvelteKit's replaceState).
7
+ */
8
+ -->
9
+ <script lang="ts">
10
+ import { tick } from "svelte";
11
+ import IconSearch from "~icons/mdi/magnify";
12
+ import IconClose from "~icons/mdi/close";
13
+ import IconHelp from "~icons/mdi/help-circle-outline";
14
+ import Input from "./Input.svelte";
15
+ import Button from "./Button.svelte";
16
+ import Popup from "./Popup.svelte";
17
+ import Dialog from "./Dialog.svelte";
18
+ import type { DataSet } from "../data/dataset.js";
19
+ import type { Size } from "../types/sizes.js";
20
+ import type { Responsive } from "../types/responsive.js";
21
+ import type { Variant } from "../types/variants.js";
22
+
23
+ let {
24
+ dataset,
25
+ placeholder = "Filter… e.g. $status == active && $price |= [20..50)",
26
+ size = "md",
27
+ variant = "secondary",
28
+ borderless = false,
29
+ }: {
30
+ dataset: DataSet;
31
+ placeholder?: string;
32
+ /** Shared axes — forwarded to the underlying filter Input. */
33
+ size?: Responsive<Size>;
34
+ variant?: Variant;
35
+ borderless?: boolean;
36
+ } = $props();
37
+
38
+ let inputEl = $state<HTMLInputElement>();
39
+ let wrapperEl = $state<HTMLElement>();
40
+ let helpOpen = $state(false);
41
+ let caret = $state(0);
42
+ let focused = $state(false);
43
+ let open = $state(false);
44
+ let active = $state(0);
45
+
46
+ type Suggestion = { value: string; label: string };
47
+
48
+ const filterCols = $derived(dataset.queryColumns.filter((c) => c.filter));
49
+ const TOKEN = /[A-Za-z0-9_.$:-]/;
50
+ const VALUE_OP =
51
+ /\$([A-Za-z0-9_]+)\s*(?:==|!=|>=|<=|>|<|~=|!~|\^=|\$=|\|=|&=)\s*\[?\s*(?:[^[\],]+,\s*)*$/;
52
+
53
+ function detect(text: string, pos: number) {
54
+ let i = pos;
55
+ while (i > 0 && TOKEN.test(text[i - 1])) i--;
56
+ const token = text.slice(i, pos);
57
+ if (token.startsWith("$"))
58
+ return { type: "column" as const, start: i, partial: token.slice(1) };
59
+ const m = text.slice(0, i).match(VALUE_OP);
60
+ if (m) return { type: "value" as const, start: i, partial: token, col: m[1] };
61
+ return { type: "none" as const, start: pos, partial: "" };
62
+ }
63
+
64
+ const ctx = $derived(detect(dataset.query, caret));
65
+ const suggestions = $derived.by<Suggestion[]>(() => {
66
+ if (ctx.type === "column") {
67
+ const p = ctx.partial.toLowerCase();
68
+ return filterCols
69
+ .filter((c) => c.id.toLowerCase().startsWith(p))
70
+ .map((c) => ({ value: c.id, label: c.label ?? c.id }));
71
+ }
72
+ if (ctx.type === "value") {
73
+ const col = dataset.queryColumns.find((c) => c.id === ctx.col);
74
+ if (col?.filter?.kind === "enum") {
75
+ const p = ctx.partial.toLowerCase();
76
+ return col.filter.options
77
+ .filter((o) => o.value.toLowerCase().startsWith(p))
78
+ .map((o) => ({ value: o.value, label: o.label ?? o.value }));
79
+ }
80
+ }
81
+ return [];
82
+ });
83
+ const showPopup = $derived(open && focused && suggestions.length > 0);
84
+
85
+ const syncCaret = () => (caret = inputEl?.selectionStart ?? dataset.query.length);
86
+
87
+ function oninput(e: Event & { currentTarget: HTMLInputElement }) {
88
+ dataset.setDraft(e.currentTarget.value);
89
+ caret = e.currentTarget.selectionStart ?? 0;
90
+ active = 0;
91
+ open = true;
92
+ }
93
+
94
+ async function accept(s: Suggestion) {
95
+ const insert = ctx.type === "column" ? `$${s.value} ` : `${s.value} `;
96
+ const next = dataset.query.slice(0, ctx.start) + insert + dataset.query.slice(caret);
97
+ const pos = ctx.start + insert.length;
98
+ dataset.setDraft(next);
99
+ open = false;
100
+ await tick();
101
+ if (inputEl) {
102
+ inputEl.focus();
103
+ inputEl.selectionStart = inputEl.selectionEnd = pos;
104
+ caret = pos;
105
+ }
106
+ }
107
+
108
+ function onkeydown(e: KeyboardEvent) {
109
+ if (showPopup) {
110
+ if (e.key === "ArrowDown")
111
+ return (e.preventDefault(), void (active = (active + 1) % suggestions.length));
112
+ if (e.key === "ArrowUp")
113
+ return (
114
+ e.preventDefault(),
115
+ void (active = (active - 1 + suggestions.length) % suggestions.length)
116
+ );
117
+ if (e.key === "Enter" || e.key === "Tab")
118
+ return (e.preventDefault(), accept(suggestions[active]));
119
+ if (e.key === "Escape") return (e.preventDefault(), void (open = false));
120
+ } else if (e.key === "Enter") {
121
+ e.preventDefault();
122
+ dataset.commit();
123
+ }
124
+ }
125
+
126
+ function clear() {
127
+ dataset.setQuery("");
128
+ inputEl?.focus();
129
+ caret = 0;
130
+ }
131
+
132
+ const bc = "border-[color-mix(in_srgb,var(--ui-color-secondary)_30%,transparent)]";
133
+ </script>
134
+
135
+ <div class="w-full [color:var(--ui-color-text)]">
136
+ <!-- Built on the core <Input> so the frame/border/hover/focus glow + themed autofill are shared,
137
+ not re-rolled. Search is the left icon; clear + help live in the `actions` slot; the parse
138
+ error reuses Input's own validation (danger) styling. -->
139
+ <div bind:this={wrapperEl} class="w-full">
140
+ <Input
141
+ bind:element={inputEl}
142
+ value={dataset.query}
143
+ {size}
144
+ {variant}
145
+ {borderless}
146
+ iconPosition="left"
147
+ inputClass="font-mono"
148
+ spellcheck={false}
149
+ autocapitalize="none"
150
+ autocomplete="off"
151
+ {placeholder}
152
+ validate={() => dataset.error || true}
153
+ touched={!!dataset.error}
154
+ {oninput}
155
+ {onkeydown}
156
+ onkeyup={syncCaret}
157
+ onclick={syncCaret}
158
+ onfocus={() => {
159
+ focused = true;
160
+ open = true;
161
+ }}
162
+ onblur={() => {
163
+ focused = false;
164
+ setTimeout(() => (open = false), 120);
165
+ }}
166
+ >
167
+ {#snippet icon()}<IconSearch class="opacity-60" />{/snippet}
168
+ {#snippet actions()}
169
+ {#if dataset.query}
170
+ <Button variant="ghost" size="sm" aria-label="Clear" title="Clear" onclick={clear}
171
+ >{#snippet icon()}<IconClose />{/snippet}</Button
172
+ >
173
+ {/if}
174
+ <Button
175
+ variant="ghost"
176
+ size="sm"
177
+ aria-label="Query help"
178
+ title="Query help"
179
+ onclick={() => (helpOpen = true)}>{#snippet icon()}<IconHelp />{/snippet}</Button
180
+ >
181
+ {/snippet}
182
+ </Input>
183
+ </div>
184
+
185
+ {#if dataset.error}
186
+ <p class="mt-1 text-xs [color:var(--ui-color-error)]">{dataset.error}</p>
187
+ {:else if dataset.dirty}
188
+ <p class="mt-1 text-xs [color:color-mix(in_srgb,var(--ui-color-text)_50%,transparent)]">
189
+ Press <kbd class="rounded border px-1 {bc}">Enter</kbd> to apply
190
+ </p>
191
+ {/if}
192
+ </div>
193
+
194
+ <Popup
195
+ anchor={wrapperEl}
196
+ open={showPopup}
197
+ onclose={() => (open = false)}
198
+ side="bottom"
199
+ align="start"
200
+ matchWidth
201
+ >
202
+ <ul
203
+ class="mt-1 max-h-72 overflow-y-auto rounded-md border p-1 shadow-lg [background-color:var(--ui-color-background)] {bc}"
204
+ >
205
+ {#each suggestions as s, i (s.value)}
206
+ <li>
207
+ <button
208
+ type="button"
209
+ class="flex w-full items-center justify-between gap-3 rounded px-2 py-1.5 text-left text-sm [color:var(--ui-color-text)] {i ===
210
+ active
211
+ ? '[background-color:color-mix(in_srgb,var(--ui-color-text)_10%,transparent)]'
212
+ : ''}"
213
+ onmouseenter={() => (active = i)}
214
+ onmousedown={(e) => {
215
+ e.preventDefault();
216
+ accept(s);
217
+ }}
218
+ >
219
+ <span class="font-mono">{ctx.type === "column" ? "$" : ""}{s.value}</span>
220
+ {#if s.label !== s.value}<span
221
+ class="text-xs [color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]"
222
+ >{s.label}</span
223
+ >{/if}
224
+ </button>
225
+ </li>
226
+ {/each}
227
+ </ul>
228
+ </Popup>
229
+
230
+ <Dialog bind:isOpen={helpOpen} title="Query syntax" showConfirm={false} showCancel={false}>
231
+ <div class="max-h-[70vh] space-y-4 overflow-y-auto text-sm [color:var(--ui-color-text)]">
232
+ <p>
233
+ A filter is one or more conditions, <code class="font-mono">$column operator value</code>.
234
+ Whitespace is insignificant; combine with <code>&&</code>, <code>||</code>, <code>!</code>,
235
+ <code>()</code>.
236
+ </p>
237
+ <div>
238
+ <h4 class="mb-1 font-semibold">Operators</h4>
239
+ <table class="w-full text-left">
240
+ <tbody class="[&_td]:py-0.5 [&_td:first-child]:pr-4 [&_td:first-child]:font-mono">
241
+ <tr><td>== !=</td><td>equals / not equals</td></tr>
242
+ <tr><td>{"> >= < <="}</td><td>greater / less than (numbers &amp; dates)</td></tr>
243
+ <tr><td>~= !~</td><td>contains / not — or regex if value is <code>/…/</code></td></tr>
244
+ <tr><td>^= $=</td><td>starts / ends with</td></tr>
245
+ <tr><td>|=</td><td>any of a set <code>[a, b]</code> or range <code>[1..9)</code></td></tr>
246
+ <tr><td>&=</td><td>all of a set (multi-value columns)</td></tr>
247
+ </tbody>
248
+ </table>
249
+ </div>
250
+ <div>
251
+ <h4 class="mb-1 font-semibold">Casts</h4>
252
+ <p class="mb-1 text-xs [color:color-mix(in_srgb,var(--ui-color-text)_70%,transparent)]">
253
+ A <code class="font-mono">|type</code> after a column or value forces its type for strict comparison
254
+ — one cast per clause; put it on the column and/or the literals.
255
+ </p>
256
+ <table class="w-full text-left">
257
+ <tbody class="[&_td]:py-0.5 [&_td:first-child]:pr-4 [&_td:first-child]:font-mono">
258
+ <tr
259
+ ><td>|number |string |bool</td><td
260
+ >force the literal's type (e.g. <code>007|string</code>)</td
261
+ ></tr
262
+ >
263
+ <tr><td>|date</td><td>compare as a date</td></tr>
264
+ <tr><td>|inet</td><td>compare as an IP address (IPv4 / IPv6)</td></tr>
265
+ </tbody>
266
+ </table>
267
+ </div>
268
+ <div>
269
+ <h4 class="mb-1 font-semibold">Examples</h4>
270
+ <pre
271
+ class="overflow-x-auto rounded p-2 font-mono text-xs [background-color:color-mix(in_srgb,var(--ui-color-text)_6%,transparent)]"><code
272
+ >$status == active
273
+ $email ~= /@gmail\.com$/i
274
+ $price |= [20..50) && $role |= [admin, ops]
275
+ $ip|inet |= [10.0.0.0|inet..10.255.255.255|inet]
276
+ "lemon balm" || $featured == true</code
277
+ ></pre>
278
+ </div>
279
+ <p class="text-xs [color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]">
280
+ Tip: type <kbd class="rounded border px-1 {bc}">$</kbd> for columns; enum columns suggest
281
+ values. Applies on <kbd class="rounded border px-1 {bc}">Enter</kbd>.
282
+ </p>
283
+ </div>
284
+ </Dialog>
@@ -0,0 +1,15 @@
1
+ import type { DataSet } from "../data/dataset.js";
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
+ dataset: DataSet;
7
+ placeholder?: string;
8
+ /** Shared axes — forwarded to the underlying filter Input. */
9
+ size?: Responsive<Size>;
10
+ variant?: Variant;
11
+ borderless?: boolean;
12
+ };
13
+ declare const Query: import("svelte").Component<$$ComponentProps, {}, "">;
14
+ type Query = ReturnType<typeof Query>;
15
+ export default Query;
@@ -1,14 +1,23 @@
1
1
  <script lang="ts">
2
+ import { variantToken, type Variant } from "../types/variants.js";
3
+ import type { Size } from "../types/sizes.js";
4
+
2
5
  export type RatingProps = {
3
6
  average: number;
4
7
  count: number;
8
+ /** Filled-star colour. Defaults to gold; a variant tints from its token (e.g. `warning`, `primary`). */
9
+ variant?: Variant;
10
+ /** Star size (sm/md/lg). */
11
+ size?: Size;
5
12
  };
6
13
 
7
- let { average, count }: RatingProps = $props();
14
+ let { average, count, variant, size = "md" }: RatingProps = $props();
15
+
16
+ const starColor = $derived(variant ? `var(${variantToken[variant]})` : "#ffd700");
17
+ const starSize: Record<Size, string> = { sm: "1rem", md: "1.2rem", lg: "1.5rem" };
8
18
 
9
19
  // Validate and sanitize the average rating
10
- const validAverage =
11
- isNaN(average) || average < 0 ? 0 : Math.min(average, 10);
20
+ const validAverage = isNaN(average) || average < 0 ? 0 : Math.min(average, 10);
12
21
 
13
22
  // Convert TMDB 10-point scale to 5-star scale
14
23
  const fiveStarRating = validAverage / 2;
@@ -19,7 +28,7 @@
19
28
  const emptyStars = Math.max(0, 5 - filledStars - (hasHalfStar ? 1 : 0));
20
29
  </script>
21
30
 
22
- <div class="rating select-none">
31
+ <div class="rating select-none" style="--star-color: {starColor}; --star-size: {starSize[size]}">
23
32
  <div class="stars">
24
33
  {#each Array(filledStars) as _, i}
25
34
  <span class="star filled">★</span>
@@ -48,30 +57,30 @@
48
57
  }
49
58
 
50
59
  .star {
51
- font-size: 1.2rem;
60
+ font-size: var(--star-size, 1.2rem);
52
61
  line-height: 1;
53
62
  }
54
63
 
55
64
  .star.filled {
56
- color: #ffd700;
65
+ color: var(--star-color, #ffd700);
57
66
  }
58
67
 
59
68
  .star.half-filled {
60
- color: #ffd700;
69
+ color: var(--star-color, #ffd700);
61
70
  opacity: 0.6;
62
71
  }
63
72
 
64
73
  .star.empty {
65
- color: var(--color-secondary);
74
+ color: var(--ui-color-secondary);
66
75
  }
67
76
 
68
77
  .rating-value {
69
78
  font-weight: 600;
70
- color: var(--color-text);
79
+ color: var(--ui-color-text);
71
80
  }
72
81
 
73
82
  .rating-count {
74
83
  font-size: 0.9rem;
75
- color: var(--color-secondary);
84
+ color: var(--ui-color-secondary);
76
85
  }
77
86
  </style>
@@ -0,0 +1,13 @@
1
+ import { type Variant } from "../types/variants.js";
2
+ import type { Size } from "../types/sizes.js";
3
+ export type RatingProps = {
4
+ average: number;
5
+ count: number;
6
+ /** Filled-star colour. Defaults to gold; a variant tints from its token (e.g. `warning`, `primary`). */
7
+ variant?: Variant;
8
+ /** Star size (sm/md/lg). */
9
+ size?: Size;
10
+ };
11
+ declare const Rating: import("svelte").Component<RatingProps, {}, "">;
12
+ type Rating = ReturnType<typeof Rating>;
13
+ export default Rating;
@@ -1,9 +1,13 @@
1
1
  <script lang="ts">
2
- import Input from '../Input/Input.svelte';
3
- import { faSearch, faTimes } from '@fortawesome/free-solid-svg-icons';
4
- import type { HTMLInputAttributes } from 'svelte/elements';
2
+ import Input from "./Input.svelte";
3
+ import Magnify from "~icons/mdi/magnify";
4
+ import Close from "~icons/mdi/close";
5
+ import type { HTMLInputAttributes } from "svelte/elements";
6
+ import type { Size } from "../types/sizes.js";
7
+ import type { Responsive } from "../types/responsive.js";
8
+ import type { Variant } from "../types/variants.js";
5
9
 
6
- interface Props extends Omit<HTMLInputAttributes, 'type'> {
10
+ interface Props extends Omit<HTMLInputAttributes, "type" | "size"> {
7
11
  value: string;
8
12
  class?: string;
9
13
  inputClass?: string;
@@ -11,6 +15,10 @@
11
15
  valid?: boolean;
12
16
  touched?: boolean;
13
17
  element?: HTMLInputElement;
18
+ size?: Responsive<Size>;
19
+ /** Shared axes — forwarded to the underlying Input. */
20
+ variant?: Variant;
21
+ borderless?: boolean;
14
22
  onClear?: () => void;
15
23
  clearable?: boolean;
16
24
  }
@@ -22,12 +30,12 @@
22
30
  element = $bindable(),
23
31
  onClear,
24
32
  clearable = true,
25
- placeholder = 'Search...',
33
+ placeholder = "Search...",
26
34
  ...restProps
27
35
  }: Props = $props();
28
36
 
29
37
  function handleClear() {
30
- value = '';
38
+ value = "";
31
39
  onClear?.();
32
40
  }
33
41
 
@@ -42,9 +50,12 @@
42
50
  bind:element
43
51
  type="search"
44
52
  inputClass="[&::-webkit-search-cancel-button]:appearance-none [&::-webkit-search-decoration]:appearance-none"
45
- icon={showClearIcon ? faTimes : faSearch}
46
53
  iconClickable={showClearIcon}
47
54
  onIconClick={showClearIcon ? handleClear : undefined}
48
55
  {placeholder}
49
56
  {...restProps}
50
- />
57
+ >
58
+ {#snippet icon()}
59
+ {#if showClearIcon}<Close />{:else}<Magnify />{/if}
60
+ {/snippet}
61
+ </Input>
@@ -1,5 +1,8 @@
1
- import type { HTMLInputAttributes } from 'svelte/elements';
2
- interface Props extends Omit<HTMLInputAttributes, 'type'> {
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
+ interface Props extends Omit<HTMLInputAttributes, "type" | "size"> {
3
6
  value: string;
4
7
  class?: string;
5
8
  inputClass?: string;
@@ -7,6 +10,10 @@ interface Props extends Omit<HTMLInputAttributes, 'type'> {
7
10
  valid?: boolean;
8
11
  touched?: boolean;
9
12
  element?: HTMLInputElement;
13
+ size?: Responsive<Size>;
14
+ /** Shared axes — forwarded to the underlying Input. */
15
+ variant?: Variant;
16
+ borderless?: boolean;
10
17
  onClear?: () => void;
11
18
  clearable?: boolean;
12
19
  }