@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
package/dist/index.js CHANGED
@@ -1,26 +1,51 @@
1
1
  // Reexport your entry components here
2
+ export { default as Alert } from "./components/Alert.svelte";
3
+ export { default as Banner } from "./components/Banner.svelte";
2
4
  export { default as Badge } from "./components/Badge/Badge.svelte";
3
- export { default as Button } from "./components/Button/Button.svelte";
4
- export { default as Dialog } from "./components/Dialog/Dialog.svelte";
5
- export { default as Input } from "./components/Input/Input.svelte";
6
- export { default as NotesEditor } from "./components/NotesEditor/NotesEditor.svelte";
7
- export { default as Rating } from "./components/Rating/Rating.svelte";
8
- export { default as Select } from "./components/Select/Select.svelte";
9
- export { default as List } from "./components/List/List.svelte";
10
- export { default as ListView } from "./components/ListView/ListView.svelte";
11
- export { default as ListItem } from "./components/ListItem/ListItem.svelte";
12
- export { default as TextArea } from "./components/TextArea/TextArea.svelte";
13
- export { default as Icon } from "./components/Icon/Icon.svelte";
14
- export { default as ScrollView } from "./components/ScrollView/ScrollView.svelte";
5
+ export { default as Button } from "./components/Button.svelte";
6
+ export { default as ButtonDropdown } from "./components/ButtonDropdown.svelte";
7
+ export { default as Card } from "./components/Card.svelte";
8
+ export { default as CardHeader } from "./components/CardHeader.svelte";
9
+ export { default as CardBody } from "./components/CardBody.svelte";
10
+ export { default as CardFooter } from "./components/CardFooter.svelte";
11
+ // Toast notifications `toast.*` pushes from anywhere; mount <Toaster /> once at the app root.
12
+ export { default as Toaster } from "./components/Toast/Toaster.svelte";
13
+ export { toast, getToasts, dismissToast, } from "./components/Toast/toast.svelte.js";
14
+ export { default as Dialog } from "./components/Dialog.svelte";
15
+ export { default as ConfirmDialog } from "./components/ConfirmDialog.svelte";
16
+ export { default as Input } from "./components/Input.svelte";
17
+ export { default as NotesEditor } from "./components/NotesEditor.svelte";
18
+ export { default as Rating } from "./components/Rating.svelte";
19
+ export { default as Select } from "./components/Select.svelte";
20
+ export { default as List } from "./components/List.svelte";
21
+ export { default as TextArea } from "./components/TextArea.svelte";
15
22
  // Specialized Input Components
16
- export { default as PasswordInput } from "./components/PasswordInput/PasswordInput.svelte";
17
- export { default as EmailInput } from "./components/EmailInput/EmailInput.svelte";
18
- export { default as SearchInput } from "./components/SearchInput/SearchInput.svelte";
19
- export { default as NumberInput } from "./components/NumberInput/NumberInput.svelte";
23
+ export { default as PasswordInput } from "./components/PasswordInput.svelte";
24
+ export { default as EmailInput } from "./components/EmailInput.svelte";
25
+ export { default as SearchInput } from "./components/SearchInput.svelte";
26
+ export { default as NumberInput } from "./components/NumberInput.svelte";
27
+ export { default as DateTimeInput } from "./components/DateTimeInput.svelte";
28
+ export { default as Calendar } from "./components/Calendar.svelte";
29
+ export { default as DatePicker } from "./components/DatePicker.svelte";
30
+ export { default as TimeSpinner } from "./components/TimeSpinner.svelte";
31
+ export { default as TimePicker } from "./components/TimePicker.svelte";
32
+ export { default as TimeRangeInput } from "./components/TimeRangeInput.svelte";
20
33
  // Theme Components
21
- export { default as ThemeSelector } from "./components/ThemeSelector/ThemeSelector.svelte";
34
+ export { default as ThemeSelector } from "./components/ThemeSelector.svelte";
22
35
  // Utility Components
23
- export { default as ElementManager } from "./components/ElementManager/ElementManager.svelte";
24
- export { default as TooltipHandler } from "./components/TooltipHandler/TooltipHandler.svelte";
25
- // NodeGraph System
26
- export { NodeGraph, Node, NodePort, Edge, NodeDecorator, Input as InputDecorator, Output as OutputDecorator, BaseNode, NodeInstance, ExpressionEvaluator, } from "./components/NodeGraph/index.js";
36
+ export { default as Popup } from "./components/Popup.svelte";
37
+ export { default as Menu } from "./components/Menu.svelte";
38
+ export { default as Checkbox } from "./components/Checkbox.svelte";
39
+ export { default as Toggle } from "./components/Toggle.svelte";
40
+ // Navigation Components
41
+ export { default as SideBarMenu } from "./components/SideBarMenu/SideBarMenu.svelte";
42
+ export { default as TabStrip } from "./components/TabStrip/TabStrip.svelte";
43
+ // Data table (pairs with the `svelte-ui/data` headless layer)
44
+ export { default as DataTable } from "./components/DataTable.svelte";
45
+ export { default as Query } from "./components/Query.svelte";
46
+ // Positioning engine (shared by Popup and tooltips)
47
+ export { anchored, computePlacement, resolveBoundary } from "./positioning/anchored.js";
48
+ // Tooltip action (use:tooltip) — themed tooltips on the `anchored` engine, no global handler.
49
+ export { tooltip } from "./positioning/tooltip.js";
50
+ export { BREAKPOINT_PX } from "./types/breakpoints.js";
51
+ export { responsiveClasses, resolveScalar } from "./types/responsive.js";
@@ -0,0 +1,61 @@
1
+ /**
2
+ * anchored — the shared placement engine.
3
+ *
4
+ * Extracted from TooltipHandler's positioning math and generalized so interactive popovers
5
+ * (Popup/Menu) and tooltips can share one engine. Pure geometry + a thin Svelte action.
6
+ *
7
+ * Boundary model (matches the old TooltipHandler): a popover is placed within a boundary that
8
+ * defaults to the **viewport**, or an explicit container (HTMLElement or CSS selector).
9
+ */
10
+ export type Side = "top" | "right" | "bottom" | "left";
11
+ export type Align = "start" | "center" | "end";
12
+ export type Boundary = "viewport" | HTMLElement | string;
13
+ export interface PlaceOptions {
14
+ /** Preferred side; flips to the opposite (then scans) if it doesn't fit. Default 'bottom'. */
15
+ side?: Side;
16
+ /** Cross-axis alignment of the floating box relative to the anchor. Default 'start'. */
17
+ align?: Align;
18
+ /** Gap between anchor and floating box, in px. Default 4. */
19
+ offset?: number;
20
+ /** Minimum gap kept from the boundary edges, in px. Default 8. */
21
+ margin?: number;
22
+ /** Resolve the boundary rect the placement must stay within. Default the viewport. */
23
+ boundary?: Boundary;
24
+ }
25
+ export interface Placement {
26
+ x: number;
27
+ y: number;
28
+ /** The side actually chosen after flip/scan. */
29
+ side: Side;
30
+ /** Max height (px) the floating box may use on the chosen side before hitting the boundary. */
31
+ maxHeight: number;
32
+ }
33
+ /** Resolve a Boundary into a rect (in viewport coordinates), accounting for scrollbars on viewport. */
34
+ export declare function resolveBoundary(boundary?: Boundary): DOMRect;
35
+ /**
36
+ * Compute where to place a floating box of `size` next to `anchor`, within `boundary`.
37
+ * Vertical sides (top/bottom) align horizontally per `align`; horizontal sides (left/right) align
38
+ * vertically per `align`. Flips to the opposite side, then scans, when the preferred side won't fit.
39
+ */
40
+ export declare function computePlacement(anchor: DOMRect, size: {
41
+ width: number;
42
+ height: number;
43
+ }, opts?: PlaceOptions): Placement;
44
+ export interface AnchoredParams extends PlaceOptions {
45
+ /** The trigger element the floating box is positioned against. */
46
+ anchor: HTMLElement | undefined;
47
+ /** When false, positioning is skipped (e.g. popup closed). Default true. */
48
+ enabled?: boolean;
49
+ /** Floating width = anchor width (selects/comboboxes). */
50
+ matchWidth?: boolean;
51
+ /** Called after each (re)position with the resolved placement. */
52
+ onplaced?: (p: Placement) => void;
53
+ }
54
+ /**
55
+ * Svelte action for the floating element: positions it (position: fixed) against `params.anchor`
56
+ * and keeps it placed on scroll/resize. Pair with the Popover API (top layer) so nothing clips it.
57
+ */
58
+ export declare function anchored(node: HTMLElement, params: AnchoredParams): {
59
+ update(next: AnchoredParams): void;
60
+ destroy(): void;
61
+ };
@@ -0,0 +1,122 @@
1
+ /**
2
+ * anchored — the shared placement engine.
3
+ *
4
+ * Extracted from TooltipHandler's positioning math and generalized so interactive popovers
5
+ * (Popup/Menu) and tooltips can share one engine. Pure geometry + a thin Svelte action.
6
+ *
7
+ * Boundary model (matches the old TooltipHandler): a popover is placed within a boundary that
8
+ * defaults to the **viewport**, or an explicit container (HTMLElement or CSS selector).
9
+ */
10
+ const opposite = { top: "bottom", bottom: "top", left: "right", right: "left" };
11
+ /** Resolve a Boundary into a rect (in viewport coordinates), accounting for scrollbars on viewport. */
12
+ export function resolveBoundary(boundary = "viewport") {
13
+ if (boundary !== "viewport") {
14
+ const el = typeof boundary === "string" ? document.querySelector(boundary) : boundary;
15
+ if (el instanceof HTMLElement)
16
+ return el.getBoundingClientRect();
17
+ }
18
+ const w = document.documentElement.clientWidth;
19
+ const h = document.documentElement.clientHeight;
20
+ return new DOMRect(0, 0, w, h);
21
+ }
22
+ /**
23
+ * Compute where to place a floating box of `size` next to `anchor`, within `boundary`.
24
+ * Vertical sides (top/bottom) align horizontally per `align`; horizontal sides (left/right) align
25
+ * vertically per `align`. Flips to the opposite side, then scans, when the preferred side won't fit.
26
+ */
27
+ export function computePlacement(anchor, size, opts = {}) {
28
+ const { side = "bottom", align = "start", offset = 4, margin = 8 } = opts;
29
+ const b = resolveBoundary(opts.boundary);
30
+ const { width: fw, height: fh } = size;
31
+ const fits = (s) => {
32
+ switch (s) {
33
+ case "top":
34
+ return anchor.top - fh - offset >= b.top + margin;
35
+ case "bottom":
36
+ return anchor.bottom + fh + offset <= b.bottom - margin;
37
+ case "left":
38
+ return anchor.left - fw - offset >= b.left + margin;
39
+ case "right":
40
+ return anchor.right + fw + offset <= b.right - margin;
41
+ }
42
+ };
43
+ // Pick a side: preferred -> opposite -> first that fits -> preferred (overflowing).
44
+ let chosen = side;
45
+ if (!fits(side)) {
46
+ if (fits(opposite[side]))
47
+ chosen = opposite[side];
48
+ else
49
+ chosen = ["bottom", "top", "right", "left"].find(fits) ?? side;
50
+ }
51
+ // Available space on the chosen side -> maxHeight (for scrollable menus).
52
+ const maxHeight = chosen === "bottom"
53
+ ? b.bottom - margin - (anchor.bottom + offset)
54
+ : chosen === "top"
55
+ ? anchor.top - offset - (b.top + margin)
56
+ : b.height - 2 * margin;
57
+ // Primary-axis position.
58
+ let x = 0;
59
+ let y = 0;
60
+ if (chosen === "top" || chosen === "bottom") {
61
+ y = chosen === "bottom" ? anchor.bottom + offset : anchor.top - fh - offset;
62
+ x =
63
+ align === "center"
64
+ ? anchor.left + anchor.width / 2 - fw / 2
65
+ : align === "end"
66
+ ? anchor.right - fw
67
+ : anchor.left;
68
+ }
69
+ else {
70
+ x = chosen === "right" ? anchor.right + offset : anchor.left - fw - offset;
71
+ y =
72
+ align === "center"
73
+ ? anchor.top + anchor.height / 2 - fh / 2
74
+ : align === "end"
75
+ ? anchor.bottom - fh
76
+ : anchor.top;
77
+ }
78
+ // Clamp into the boundary (shift), so the box never spills past the edges.
79
+ x = Math.min(Math.max(x, b.left + margin), b.right - margin - fw);
80
+ y = Math.min(Math.max(y, b.top + margin), b.bottom - margin - fh);
81
+ return { x, y, side: chosen, maxHeight };
82
+ }
83
+ /**
84
+ * Svelte action for the floating element: positions it (position: fixed) against `params.anchor`
85
+ * and keeps it placed on scroll/resize. Pair with the Popover API (top layer) so nothing clips it.
86
+ */
87
+ export function anchored(node, params) {
88
+ let current = params;
89
+ function place() {
90
+ const { anchor, enabled = true, matchWidth } = current;
91
+ if (!anchor || !enabled)
92
+ return;
93
+ const a = anchor.getBoundingClientRect();
94
+ if (matchWidth)
95
+ node.style.width = `${a.width}px`;
96
+ const size = { width: node.offsetWidth, height: node.offsetHeight };
97
+ if (size.width === 0 || size.height === 0) {
98
+ requestAnimationFrame(place);
99
+ return;
100
+ }
101
+ const p = computePlacement(a, size, current);
102
+ node.style.position = "fixed";
103
+ node.style.left = `${p.x}px`;
104
+ node.style.top = `${p.y}px`;
105
+ node.style.margin = "0";
106
+ current.onplaced?.(p);
107
+ }
108
+ const onScrollResize = () => place();
109
+ window.addEventListener("scroll", onScrollResize, { passive: true, capture: true });
110
+ window.addEventListener("resize", onScrollResize, { passive: true });
111
+ place();
112
+ return {
113
+ update(next) {
114
+ current = next;
115
+ place();
116
+ },
117
+ destroy() {
118
+ window.removeEventListener("scroll", onScrollResize, { capture: true });
119
+ window.removeEventListener("resize", onScrollResize);
120
+ },
121
+ };
122
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * tooltip — a self-contained Svelte action for themed tooltips, built on the `anchored` engine.
3
+ *
4
+ * <button use:tooltip>Delete</button> // reads (and suppresses) the native `title`
5
+ * <button use:tooltip={{ text: 'Delete' }}>…</button> // explicit text (how Button feeds it)
6
+ *
7
+ * No global handler / ElementManager needed — each use:tooltip owns its own tooltip. The floating
8
+ * box is a top-layer popover positioned by `anchored` (same flip-vs-viewport math as Popup/Menu),
9
+ * styled with inline CSS + theme vars (so it never depends on Tailwind scanning the dist).
10
+ *
11
+ * a11y: when the element has no accessible name of its own (e.g. icon-only buttons), the text is
12
+ * also mirrored to `aria-label` so screen readers still get a name.
13
+ */
14
+ import { type Side, type Align } from "./anchored.js";
15
+ import { type Variant } from "../types/variants.js";
16
+ import type { Size } from "../types/sizes.js";
17
+ export interface TooltipOptions {
18
+ /** Tooltip text. Defaults to the element's `title` attribute (which is then suppressed). */
19
+ text?: string;
20
+ /** Preferred side; flips if it doesn't fit. Default 'top'. */
21
+ side?: Side;
22
+ /** Cross-axis alignment. Default 'center'. */
23
+ align?: Align;
24
+ /** Gap between the anchor and the tooltip edge, px. Default 8. */
25
+ offset?: number;
26
+ /** Show the callout triangle pointing at the anchor. Default true. */
27
+ arrow?: boolean;
28
+ /** Hover/focus open delay, ms. Default 400. */
29
+ delay?: number;
30
+ /** Colour of the bubble (+ arrow). Accent variants fill with their colour + white text; the default
31
+ * (unset / `neutral` / `ghost`) is the `--ui-color-text` bubble with contrasting text. */
32
+ variant?: Variant;
33
+ /** Density — padding + text size (sm / md / lg). Default `md`. */
34
+ size?: Size;
35
+ /** Turn the tooltip off without removing the action. */
36
+ disabled?: boolean;
37
+ }
38
+ export declare function tooltip(node: HTMLElement, options?: TooltipOptions): {
39
+ update(next: TooltipOptions): void;
40
+ destroy(): void;
41
+ };
@@ -0,0 +1,147 @@
1
+ /**
2
+ * tooltip — a self-contained Svelte action for themed tooltips, built on the `anchored` engine.
3
+ *
4
+ * <button use:tooltip>Delete</button> // reads (and suppresses) the native `title`
5
+ * <button use:tooltip={{ text: 'Delete' }}>…</button> // explicit text (how Button feeds it)
6
+ *
7
+ * No global handler / ElementManager needed — each use:tooltip owns its own tooltip. The floating
8
+ * box is a top-layer popover positioned by `anchored` (same flip-vs-viewport math as Popup/Menu),
9
+ * styled with inline CSS + theme vars (so it never depends on Tailwind scanning the dist).
10
+ *
11
+ * a11y: when the element has no accessible name of its own (e.g. icon-only buttons), the text is
12
+ * also mirrored to `aria-label` so screen readers still get a name.
13
+ */
14
+ import { anchored } from "./anchored.js";
15
+ import { variantToken } from "../types/variants.js";
16
+ // Per-size padding + text (the bubble is deliberately compact vs a control).
17
+ const TOOLTIP_SIZE = {
18
+ sm: "padding:0.2rem 0.4rem;font-size:0.7rem;line-height:0.95rem;",
19
+ md: "padding:0.3rem 0.5rem;font-size:0.75rem;line-height:1rem;",
20
+ lg: "padding:0.45rem 0.65rem;font-size:0.875rem;line-height:1.25rem;",
21
+ };
22
+ export function tooltip(node, options = {}) {
23
+ let opts = options;
24
+ let text = "";
25
+ let tip = null;
26
+ let placement = null;
27
+ let timer;
28
+ let ariaSet = false; // whether WE set aria-label (so we only clear our own)
29
+ // Resolve text from the option or the native `title` (which we strip to kill the OS tooltip),
30
+ // and backstop the accessible name for elements that have none.
31
+ function sync() {
32
+ const fromAttr = node.getAttribute("title");
33
+ text = (opts.text ?? fromAttr ?? "").trim();
34
+ if (fromAttr !== null)
35
+ node.removeAttribute("title"); // suppress native tooltip
36
+ const hasName = !!node.getAttribute("aria-label") && !ariaSet;
37
+ const hasText = !!node.textContent?.trim();
38
+ if (text && !hasName && !hasText) {
39
+ node.setAttribute("aria-label", text);
40
+ ariaSet = true;
41
+ }
42
+ else if (ariaSet && !text) {
43
+ node.removeAttribute("aria-label");
44
+ ariaSet = false;
45
+ }
46
+ }
47
+ // Place the callout triangle on the edge opposite the (post-flip) side, pointing at the anchor
48
+ // centre and clamped off the rounded corners. Runs on every (re)placement via `onplaced`.
49
+ function positionArrow(arrowEl, box, p) {
50
+ const a = node.getBoundingClientRect();
51
+ const w = box.offsetWidth;
52
+ const h = box.offsetHeight;
53
+ const pad = 12; // keep clear of the rounded corners
54
+ arrowEl.style.top = arrowEl.style.bottom = arrowEl.style.left = arrowEl.style.right = "";
55
+ if (p.side === "top" || p.side === "bottom") {
56
+ arrowEl.style.left = `${Math.min(Math.max(a.left + a.width / 2 - p.x, pad), w - pad)}px`;
57
+ arrowEl.style[p.side === "top" ? "bottom" : "top"] = "0";
58
+ arrowEl.style.transform = `translate(-50%, ${p.side === "top" ? "50%" : "-50%"}) rotate(45deg)`;
59
+ }
60
+ else {
61
+ arrowEl.style.top = `${Math.min(Math.max(a.top + a.height / 2 - p.y, pad), h - pad)}px`;
62
+ arrowEl.style[p.side === "left" ? "right" : "left"] = "0";
63
+ arrowEl.style.transform = `translate(${p.side === "left" ? "50%" : "-50%"}, -50%) rotate(45deg)`;
64
+ }
65
+ }
66
+ function show() {
67
+ if (tip || !text || opts.disabled)
68
+ return;
69
+ tip = document.createElement("div");
70
+ tip.setAttribute("role", "tooltip");
71
+ tip.setAttribute("popover", "manual");
72
+ // variant → bubble colour. Accent variants fill with their token + white text; neutral/ghost
73
+ // (and no variant) keep the --ui-color-text bubble with contrasting (background) text.
74
+ const token = opts.variant ? variantToken[opts.variant] : "--ui-color-text";
75
+ const bg = `var(${token})`;
76
+ const fg = token === "--ui-color-text" ? "var(--ui-color-background)" : "white";
77
+ tip.style.cssText =
78
+ "position:fixed;inset:auto;margin:0;border:0;z-index:9999;width:max-content;max-width:16rem;" +
79
+ // overflow:visible so the arrow can poke past the edge — the popover UA style otherwise
80
+ // clips it (hidden triangle) and adds a scrollbar.
81
+ "overflow:visible;border-radius:0.375rem;" +
82
+ TOOLTIP_SIZE[opts.size ?? "md"] +
83
+ `background-color:${bg};color:${fg};` +
84
+ "box-shadow:0 4px 12px rgba(0,0,0,0.15);pointer-events:none;white-space:normal;";
85
+ // Callout triangle: a rotated square in the tooltip's bg, appended BEFORE the label so it
86
+ // paints behind it — its outer half pokes past the edge (the triangle), its inner half merges
87
+ // seamlessly with the background.
88
+ let arrowEl = null;
89
+ if (opts.arrow !== false) {
90
+ arrowEl = document.createElement("div");
91
+ arrowEl.style.cssText = `position:absolute;width:0.5rem;height:0.5rem;background-color:${bg};`;
92
+ tip.appendChild(arrowEl);
93
+ }
94
+ const label = document.createElement("span");
95
+ label.textContent = text;
96
+ tip.appendChild(label);
97
+ document.body.appendChild(tip);
98
+ // Show first (so it has a measurable size), then position against the trigger.
99
+ tip.showPopover?.();
100
+ const box = tip;
101
+ placement = anchored(tip, {
102
+ anchor: node,
103
+ side: opts.side ?? "top",
104
+ align: opts.align ?? "center",
105
+ offset: opts.offset ?? 8,
106
+ onplaced: arrowEl ? (p) => positionArrow(arrowEl, box, p) : undefined,
107
+ });
108
+ }
109
+ function hide() {
110
+ clearTimeout(timer);
111
+ placement?.destroy();
112
+ placement = null;
113
+ if (tip) {
114
+ tip.hidePopover?.();
115
+ tip.remove();
116
+ tip = null;
117
+ }
118
+ }
119
+ function onEnter() {
120
+ if (opts.disabled || !text)
121
+ return;
122
+ clearTimeout(timer);
123
+ timer = setTimeout(show, opts.delay ?? 400);
124
+ }
125
+ node.addEventListener("mouseenter", onEnter);
126
+ node.addEventListener("mouseleave", hide);
127
+ node.addEventListener("focusin", onEnter);
128
+ node.addEventListener("focusout", hide);
129
+ node.addEventListener("click", hide); // don't let it linger after activating the control
130
+ sync();
131
+ return {
132
+ update(next) {
133
+ opts = next ?? {};
134
+ sync();
135
+ if (tip)
136
+ hide(); // re-evaluate on next hover with fresh text/options
137
+ },
138
+ destroy() {
139
+ hide();
140
+ node.removeEventListener("mouseenter", onEnter);
141
+ node.removeEventListener("mouseleave", hide);
142
+ node.removeEventListener("focusin", onEnter);
143
+ node.removeEventListener("focusout", hide);
144
+ node.removeEventListener("click", hide);
145
+ },
146
+ };
147
+ }
package/dist/theme.css ADDED
@@ -0,0 +1,205 @@
1
+ /**
2
+ * svelte-ui theme — the single source of truth for library-wide design tokens.
3
+ *
4
+ * Every shared token lives under the `--ui-*` prefix. That's the contract for
5
+ * consumers: import this file, remap any `--ui-*` value to your palette, and
6
+ * every component follows in lockstep. No unprefixed tokens leak from svelte-ui
7
+ * into the host — so DaisyUI, Tailwind's built-ins, or any other design system
8
+ * can coexist at :root without collision.
9
+ *
10
+ * Two themes only — light (default) and dark. Dark follows the system
11
+ * preference automatically; force either with the `.theme-light` /
12
+ * `.theme-dark` classes (see <ThemeSelector />).
13
+ *
14
+ * NOTE: density spacing (padding, gap, per-component layout) is NOT a CSS
15
+ * token — components handle it via `Record<Size, string>` Tailwind class
16
+ * maps keyed off the `size` prop. Only shared design tokens (color, border,
17
+ * text-size, control-height, icon-size, radius) live here as vars.
18
+ */
19
+
20
+ @theme {
21
+ /* Responsive breakpoints. Tailwind 4 ships sm/md/lg/xl/2xl by default;
22
+ * we add narrow-mobile tiers (4xs/3xs/2xs/xs) and an ultrawide tier
23
+ * (3xl) so DataTable's column placements can target the full range. */
24
+ --breakpoint-4xs: 320px; /* iPhone SE (1st gen), absolute mobile minimum */
25
+ --breakpoint-3xs: 360px; /* narrow Android */
26
+ --breakpoint-2xs: 420px; /* standard phone */
27
+ --breakpoint-xs: 480px; /* large phone / small tablet portrait */
28
+ --breakpoint-3xl: 1920px; /* FHD / 1080p — ultrawide */
29
+
30
+ /* ── Colour palette ────────────────────────────────────────────
31
+ * Six semantic colours + background + text. Every component that
32
+ * paints anything reads through here (either directly via
33
+ * `var(--ui-color-*)` or via `variantToken[variant]`). */
34
+ --ui-color-background: #ffffff;
35
+ --ui-color-text: #111827;
36
+
37
+ --ui-color-primary: #1d4ed8;
38
+ --ui-color-secondary: #0891b2;
39
+ --ui-color-success: #059669;
40
+ --ui-color-warning: #d97706;
41
+ --ui-color-error: #dc2626;
42
+ --ui-color-info: #0ea5e9;
43
+
44
+ /* ── Density scale ─────────────────────────────────────────────
45
+ * The `size` axis (see types/sizes.ts). Components that need a
46
+ * specific control-square height (icon buttons in lead/tail
47
+ * columns, checkboxes, toggle knobs) read these directly. */
48
+ --ui-height-sm: 1.5rem; /* 24px — compact */
49
+ --ui-height: 2rem; /* 32px — md, default */
50
+ --ui-height-lg: 2.5rem; /* 40px — large */
51
+ --ui-height-touch: 2.75rem; /* 44px — explicit touch target (WCAG/Apple); NOT the lg size */
52
+
53
+ /* ── Text + icon scale ─────────────────────────────────────────
54
+ * Parallel to the height scale. Components pick the matching tier
55
+ * so density propagates uniformly (control height + label text +
56
+ * inline icon). */
57
+ --ui-text-sm: 0.75rem; /* 12px */
58
+ --ui-text: 0.875rem; /* 14px — md, default */
59
+ --ui-text-lg: 1rem; /* 16px */
60
+ --ui-icon-sm: 0.875rem; /* 14px */
61
+ --ui-icon: 1rem; /* 16px — md, default */
62
+ --ui-icon-lg: 1.25rem; /* 20px */
63
+
64
+ /* ── Border + radius ───────────────────────────────────────────
65
+ * --ui-border-radius corner radius — the ONE value every rectangular surface uses
66
+ * (Button, Input, Card, Dialog[via Card], Alert, Banner, DataTable…).
67
+ * Fixed, NOT scaled by `size`, so different-size controls share a
68
+ * corner. (Pills like Toggle/Badge use rounded-full; the tiny
69
+ * Checkbox box keeps its own small radius — not worth a token.)
70
+ * --ui-border-color soft internal divider (row separators, section splits) */
71
+ --ui-border-radius: 0.375rem;
72
+ --ui-border-color: color-mix(in srgb, var(--ui-color-secondary) 30%, transparent);
73
+ }
74
+
75
+ /* One interactive-surface language, library-wide. A subtle tint of the ACCENT colour, so it lightens on
76
+ * dark themes and darkens on light. Three steps of the SAME tint, from lightest to strongest:
77
+ * --ui-color-hover row / option / ghost-button hover ( 4% — lightest)
78
+ * --ui-color-surface header / footer / section bands ( 8% — a notch darker than hover)
79
+ * --ui-color-active pressed / held (12% — strongest)
80
+ * Every interactive + section background references these — change here, change everywhere.
81
+ *
82
+ * The accent is --ui-accent, which DEFAULTS to the neutral text colour (so out of the box these are grey
83
+ * tints, exactly as before). Point --ui-accent at a semantic colour and the tints follow — that's how a
84
+ * variant colours a whole surface (e.g. <DataTable variant="primary">). Because a bare --ui-accent
85
+ * override on a descendant will NOT re-tint a token that was computed higher up (a custom property that
86
+ * references others resolves at its DEFINING element, then inherits already-resolved — verified), any
87
+ * element that overrides --ui-accent must also carry `.ui-accent` (below) to RE-MIX the three tokens.
88
+ *
89
+ * NOT in @theme, and repeated in EVERY theme scope (not defined once at :root), for two reasons:
90
+ * 1. Tailwind v4 only emits an @theme var that backs a recognised utility; it never scans inside
91
+ * arbitrary classes like `[background-color:var(--ui-color-hover)]`, so an @theme home gets these
92
+ * tree-shaken out of the final bundle (hover worked off the SSR critical CSS during load, then died).
93
+ * 2. Same freeze rule as above: defined only at :root, a switcher putting .theme-* on <body> (or any
94
+ * subtree) would leave these frozen to :root's theme (a white hover onto a light page → invisible).
95
+ * Repeating them per theme scope makes them re-resolve wherever the theme class lands.
96
+ * The percentages are theme-INDEPENDENT (identical in light and dark); only --ui-accent's colour flips. */
97
+ :root {
98
+ /* light default — no theme class + light system */
99
+ --ui-accent: var(--ui-color-text);
100
+ --ui-color-hover: color-mix(in srgb, var(--ui-accent) 4%, transparent);
101
+ --ui-color-surface: color-mix(in srgb, var(--ui-accent) 8%, transparent);
102
+ --ui-color-active: color-mix(in srgb, var(--ui-accent) 12%, transparent);
103
+
104
+ /* Tint SCALE for a variant-tinted surface (Badge chip, Card frame, `filled` Alert…). The colour is
105
+ * always the component's own variant token; these are the shared STRENGTHS it's mixed at, so every
106
+ * tinted surface is calculated the same way — change one number here, every component follows:
107
+ * fill: color-mix(in srgb, var(<token>) var(--ui-tint-soft), transparent) soft resting fill
108
+ * border: color-mix(in srgb, var(<token>) var(--ui-tint-border), transparent) divider/frame (rest)
109
+ * hover: color-mix(in srgb, var(<token>) var(--ui-tint-border-hover), transparent) bordered field, hover/focus
110
+ * text: color-mix(in srgb, var(<token>) var(--ui-tint-strong), var(--ui-color-text)) foreground
111
+ * A primary badge is primary@soft/border/strong; a neutral one is text@… — one calculation, any
112
+ * variant. `--ui-tint-border-hover` is the STRONGER step an interactive bordered field (Input,
113
+ * TextArea, an outline Button) moves to on hover/focus. Theme-independent constants (no colour → no
114
+ * per-theme scope needed) — but a theme MAY override these to tune the whole tint language. */
115
+ --ui-tint-soft: 12%;
116
+ --ui-tint-border: 50%;
117
+ --ui-tint-border-hover: 80%;
118
+ --ui-tint-strong: 80%;
119
+ }
120
+
121
+ /* Accent scoping — opt-in. Set --ui-accent on an element with this class (components do this from a
122
+ * `variant` prop) and the interactive tints RE-MIX from it. The re-mix must happen here (not merely by
123
+ * inheriting an overridden --ui-accent) because the theme-scope tokens above already resolved against
124
+ * the neutral default; see the freeze note. Default accent stays neutral text if --ui-accent is unset. */
125
+ .ui-accent {
126
+ --ui-color-hover: color-mix(in srgb, var(--ui-accent) 4%, transparent);
127
+ --ui-color-surface: color-mix(in srgb, var(--ui-accent) 8%, transparent);
128
+ --ui-color-active: color-mix(in srgb, var(--ui-accent) 12%, transparent);
129
+ }
130
+
131
+ body {
132
+ background-color: var(--ui-color-background);
133
+ color: var(--ui-color-text);
134
+ transition:
135
+ background-color 0.2s ease,
136
+ color 0.2s ease;
137
+ }
138
+
139
+ /* Dark palette, shared by the system-preference query and the .theme-dark force class. */
140
+ @media (prefers-color-scheme: dark) {
141
+ :root {
142
+ --ui-color-background: #111827;
143
+ --ui-color-text: #f9fafb;
144
+ --ui-color-primary: #2563eb;
145
+ --ui-color-secondary: #06b6d4;
146
+ --ui-color-success: #10b981;
147
+ --ui-color-warning: #f59e0b;
148
+ --ui-color-error: #ef4444;
149
+ --ui-color-info: #06b6d4;
150
+ --ui-accent: var(--ui-color-text);
151
+ --ui-color-hover: color-mix(in srgb, var(--ui-accent) 4%, transparent);
152
+ --ui-color-surface: color-mix(in srgb, var(--ui-accent) 8%, transparent);
153
+ --ui-color-active: color-mix(in srgb, var(--ui-accent) 12%, transparent);
154
+ color-scheme: dark;
155
+ }
156
+ }
157
+
158
+ .theme-light {
159
+ --ui-color-background: #ffffff;
160
+ --ui-color-text: #111827;
161
+ --ui-color-primary: #1d4ed8;
162
+ --ui-color-secondary: #0891b2;
163
+ --ui-color-success: #059669;
164
+ --ui-color-warning: #d97706;
165
+ --ui-color-error: #dc2626;
166
+ --ui-color-info: #0ea5e9;
167
+ /* forced light must re-assert the tint (overrides the dark @media above) */
168
+ --ui-accent: var(--ui-color-text);
169
+ --ui-color-hover: color-mix(in srgb, var(--ui-accent) 4%, transparent);
170
+ --ui-color-surface: color-mix(in srgb, var(--ui-accent) 8%, transparent);
171
+ --ui-color-active: color-mix(in srgb, var(--ui-accent) 12%, transparent);
172
+ color-scheme: light;
173
+ }
174
+
175
+ .theme-dark {
176
+ --ui-color-background: #111827;
177
+ --ui-color-text: #f9fafb;
178
+ --ui-color-primary: #2563eb;
179
+ --ui-color-secondary: #06b6d4;
180
+ --ui-color-success: #10b981;
181
+ --ui-color-warning: #f59e0b;
182
+ --ui-color-error: #ef4444;
183
+ --ui-color-info: #06b6d4;
184
+ --ui-accent: var(--ui-color-text);
185
+ --ui-color-hover: color-mix(in srgb, var(--ui-accent) 4%, transparent);
186
+ --ui-color-surface: color-mix(in srgb, var(--ui-accent) 8%, transparent);
187
+ --ui-color-active: color-mix(in srgb, var(--ui-accent) 12%, transparent);
188
+ color-scheme: dark;
189
+ }
190
+
191
+ /* ── Responsive size/variant safelist ──────────────────────────────────────────────────────────
192
+ * Components accept `size`/`variant` as a scalar OR a per-breakpoint map (Responsive<T>, see
193
+ * types/responsive.ts) and build `bp:`-prefixed Tailwind classes at RUNTIME — which Tailwind can't
194
+ * discover by scanning source. Safelist every size utility family across every tier here so the
195
+ * classes exist; consuming apps inherit this through `@import 'svelte-ui/theme.css'`. The leading
196
+ * `{,4xs:,…,3xl:}` alternation emits the unprefixed class plus one per breakpoint. Keep this in sync
197
+ * with the per-Size class maps in the components (heights, widths, padding, text, inline-icon svg). */
198
+ @source inline("{,4xs:,3xs:,2xs:,xs:,sm:,md:,lg:,xl:,2xl:,3xl:}{h,w,size}-{3,3.5,4,5,6,7,8,9,10,11,14}");
199
+ @source inline("{,4xs:,3xs:,2xs:,xs:,sm:,md:,lg:,xl:,2xl:,3xl:}p-{1.5,2,3,4,6,8}");
200
+ @source inline("{,4xs:,3xs:,2xs:,xs:,sm:,md:,lg:,xl:,2xl:,3xl:}px-{1.5,2,2.5,3,4,5,6}");
201
+ @source inline("{,4xs:,3xs:,2xs:,xs:,sm:,md:,lg:,xl:,2xl:,3xl:}py-{0.5,1,1.5,2,2.5,3}");
202
+ @source inline("{,4xs:,3xs:,2xs:,xs:,sm:,md:,lg:,xl:,2xl:,3xl:}{pt,pb}-{2,3,4}");
203
+ @source inline("{,4xs:,3xs:,2xs:,xs:,sm:,md:,lg:,xl:,2xl:,3xl:}gap-{1.5,2,3}");
204
+ @source inline("{,4xs:,3xs:,2xs:,xs:,sm:,md:,lg:,xl:,2xl:,3xl:}text-{xs,sm,base,lg,2xl}");
205
+ @source inline("{,4xs:,3xs:,2xs:,xs:,sm:,md:,lg:,xl:,2xl:,3xl:}[&_svg]:{w,h}-{3.5,4,5,6}");