@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
@@ -1,13 +1,33 @@
1
- import type { IconDefinition } from "@fortawesome/free-solid-svg-icons";
2
- import type { Variant } from "../../types/variants.js";
1
+ import type { Snippet } from "svelte";
3
2
  import type { Size } from "../../types/sizes.js";
3
+ import { type Responsive } from "../../types/responsive.js";
4
+ /**
5
+ * A compact status chip — themed via the semantic colour tokens like
6
+ * Alert. Honours the shared {@link Variant} + {@link Size} axes;
7
+ * chips scale in lockstep with the surrounding text.
8
+ *
9
+ * Variant → semantic token. `default` uses `--ui-color-secondary`
10
+ * (the neutral in every theme). The chip fills with a soft tint
11
+ * of the token and borders with the token at full strength; the
12
+ * text stays high-contrast against the background.
13
+ */
14
+ type BadgeVariant = "neutral" | "default" | "primary" | "secondary" | "success" | "info" | "warning" | "danger" | "ghost";
4
15
  interface Props {
5
- icon?: IconDefinition;
6
16
  text?: string;
7
- variant?: Variant;
8
- size?: Size;
9
- iconPosition?: "left" | "right";
17
+ variant?: BadgeVariant;
18
+ size?: Responsive<Size>;
19
+ dismissible?: boolean;
10
20
  rounded?: boolean;
21
+ /** Hide the border WITHOUT changing the box: the border stays 1px (so height/width are
22
+ * identical to a bordered chip) but its colour is set to the chip's own background, so the
23
+ * visible ring disappears. Set `borderless={false}` for the classic ringed chip — the ring is
24
+ * `--ui-tint-border` (30%) over the `--ui-tint-soft` (12%) fill. Defaults `true` (fill only). */
25
+ borderless?: boolean;
26
+ /** When set, the badge renders as an `<a href>` (clickable, `cursor-pointer`) instead of a
27
+ * static `<span>`. Mirrors Button's `href`. */
28
+ href?: string;
29
+ ondismiss?: () => void;
30
+ children?: Snippet;
11
31
  }
12
32
  declare const Badge: import("svelte").Component<Props, {}, "">;
13
33
  type Badge = ReturnType<typeof Badge>;
@@ -1 +1 @@
1
- export { default } from './Badge.svelte';
1
+ export { default } from "./Badge.svelte";
@@ -1 +1 @@
1
- export { default } from './Badge.svelte';
1
+ export { default } from "./Badge.svelte";
@@ -0,0 +1,133 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { Size } from "../types/sizes.js";
4
+ import { responsiveClasses, type Responsive } from "../types/responsive.js";
5
+ import IconClose from "~icons/mdi/close";
6
+
7
+ /**
8
+ * A page/section-level notice. Like {@link Alert} but ALWAYS solid-filled (variant background +
9
+ * light text), full-width, with an optional leading icon (always left) and an optional trailing
10
+ * close (X) button (always right). Quieter a11y than Alert — `role="status"`, not assertive. For
11
+ * inline field/form status, use <Alert> instead. Honours the shared Size axis (scalar OR responsive).
12
+ */
13
+ type BannerVariant =
14
+ "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | "danger" | "ghost";
15
+ interface Props {
16
+ variant?: BannerVariant;
17
+ size?: Responsive<Size>;
18
+ text?: string;
19
+ /** Leading icon (always left) — a Snippet (icon-set-agnostic), like Button/Input. */
20
+ icon?: Snippet;
21
+ /** Show a trailing close (X) button (always right). */
22
+ dismissible?: boolean;
23
+ /** Border colour = the banner's solid fill (invisible, box unchanged) — the default. Set
24
+ * `borderless={false}` for a contrasting 30% tint-border around the bar. */
25
+ borderless?: boolean;
26
+ /** Visibility — bindable. The close button sets it false. */
27
+ open?: boolean;
28
+ /** Fired after the close button flips `open` to false. */
29
+ onClose?: () => void;
30
+ class?: string;
31
+ children?: Snippet;
32
+ }
33
+ let {
34
+ variant = "info",
35
+ size = "md",
36
+ text = "",
37
+ icon,
38
+ dismissible = false,
39
+ borderless = true,
40
+ open = $bindable(true),
41
+ onClose,
42
+ class: cls = "",
43
+ children,
44
+ }: Props = $props();
45
+
46
+ // Variant → semantic token (danger maps to --ui-color-error).
47
+ const tokenFor: Record<BannerVariant, string> = {
48
+ neutral: "--ui-color-text",
49
+ primary: "--ui-color-primary",
50
+ secondary: "--ui-color-secondary",
51
+ success: "--ui-color-success",
52
+ info: "--ui-color-info",
53
+ warning: "--ui-color-warning",
54
+ danger: "--ui-color-error",
55
+ ghost: "--ui-color-text",
56
+ };
57
+ const sizeClass: Record<Size, string> = {
58
+ sm: "px-3 py-1.5 text-xs gap-2",
59
+ md: "px-4 py-2.5 text-sm gap-3",
60
+ lg: "px-5 py-3 text-base gap-3",
61
+ };
62
+ // Icon svg size per tier (16 / 20 / 24px) — a touch larger than Alert since a banner is louder.
63
+ const iconSizeClass: Record<Size, string> = {
64
+ sm: "[&_svg]:w-4 [&_svg]:h-4",
65
+ md: "[&_svg]:w-5 [&_svg]:h-5",
66
+ lg: "[&_svg]:w-6 [&_svg]:h-6",
67
+ };
68
+ const token = $derived(tokenFor[variant]);
69
+ // Accent variants: solid fill + white text (accents are dark enough for white in both themes).
70
+ // `neutral` is solid too — a --ui-color-text bar — but takes CONTRASTING text (--ui-color-background)
71
+ // so it stays legible in both modes (black bar/white text in light, white bar/black text in dark).
72
+ // `ghost` is the quiet exception to "always solid": a faint neutral surface with normal text.
73
+ const bgColor = $derived(
74
+ variant === "neutral"
75
+ ? "var(--ui-color-text)"
76
+ : variant === "ghost"
77
+ ? "color-mix(in srgb, var(--ui-color-text) 10%, transparent)"
78
+ : `var(${token})`,
79
+ );
80
+ const textColor = $derived(
81
+ variant === "neutral"
82
+ ? "var(--ui-color-background)"
83
+ : variant === "ghost"
84
+ ? "var(--ui-color-text)"
85
+ : "white",
86
+ );
87
+ // borderless (default) → border = the solid fill (invisible); else the standard 30% tint-border.
88
+ const standardBorder = $derived(
89
+ `color-mix(in srgb, var(${token}) var(--ui-tint-border), transparent)`,
90
+ );
91
+ const style = $derived(
92
+ `background-color: ${bgColor}; color: ${textColor};` +
93
+ ` border-color: ${borderless ? bgColor : standardBorder};`,
94
+ );
95
+
96
+ function close() {
97
+ open = false;
98
+ onClose?.();
99
+ }
100
+ </script>
101
+
102
+ {#if open}
103
+ <div
104
+ role="status"
105
+ class="flex w-full items-center border [border-radius:var(--ui-border-radius)] [&_svg]:fill-current {responsiveClasses(
106
+ size,
107
+ sizeClass,
108
+ )} {cls}"
109
+ {style}
110
+ >
111
+ {#if icon}
112
+ <span class="inline-flex shrink-0 {responsiveClasses(size, iconSizeClass)}"
113
+ >{@render icon()}</span
114
+ >
115
+ {/if}
116
+ <span class="min-w-0 grow"
117
+ >{#if children}{@render children()}{:else}{text}{/if}</span
118
+ >
119
+ {#if dismissible}
120
+ <button
121
+ type="button"
122
+ aria-label="Dismiss"
123
+ onclick={close}
124
+ class="inline-flex shrink-0 cursor-pointer items-center justify-center rounded p-0.5 opacity-80 transition-opacity hover:opacity-100 focus-visible:opacity-100 {responsiveClasses(
125
+ size,
126
+ iconSizeClass,
127
+ )}"
128
+ >
129
+ <IconClose />
130
+ </button>
131
+ {/if}
132
+ </div>
133
+ {/if}
@@ -0,0 +1,31 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { Size } from "../types/sizes.js";
3
+ import { type Responsive } from "../types/responsive.js";
4
+ /**
5
+ * A page/section-level notice. Like {@link Alert} but ALWAYS solid-filled (variant background +
6
+ * light text), full-width, with an optional leading icon (always left) and an optional trailing
7
+ * close (X) button (always right). Quieter a11y than Alert — `role="status"`, not assertive. For
8
+ * inline field/form status, use <Alert> instead. Honours the shared Size axis (scalar OR responsive).
9
+ */
10
+ type BannerVariant = "neutral" | "primary" | "secondary" | "success" | "info" | "warning" | "danger" | "ghost";
11
+ interface Props {
12
+ variant?: BannerVariant;
13
+ size?: Responsive<Size>;
14
+ text?: string;
15
+ /** Leading icon (always left) — a Snippet (icon-set-agnostic), like Button/Input. */
16
+ icon?: Snippet;
17
+ /** Show a trailing close (X) button (always right). */
18
+ dismissible?: boolean;
19
+ /** Border colour = the banner's solid fill (invisible, box unchanged) — the default. Set
20
+ * `borderless={false}` for a contrasting 30% tint-border around the bar. */
21
+ borderless?: boolean;
22
+ /** Visibility — bindable. The close button sets it false. */
23
+ open?: boolean;
24
+ /** Fired after the close button flips `open` to false. */
25
+ onClose?: () => void;
26
+ class?: string;
27
+ children?: Snippet;
28
+ }
29
+ declare const Banner: import("svelte").Component<Props, {}, "open">;
30
+ type Banner = ReturnType<typeof Banner>;
31
+ export default Banner;
@@ -0,0 +1,240 @@
1
+ <script lang="ts">
2
+ import type { Snippet } from "svelte";
3
+ import type { Variant } from "../types/variants.js";
4
+ import type { Size } from "../types/sizes.js";
5
+ import { responsiveClasses, type Responsive } from "../types/responsive.js";
6
+ import type { HTMLButtonAttributes, HTMLAnchorAttributes } from "svelte/elements";
7
+ import { tooltip } from "../positioning/tooltip.js";
8
+ import { DEV } from "esm-env";
9
+
10
+ // Only lib-specific props live here; standard element attributes (class, title, aria-*, onclick,
11
+ // disabled, target, …) come from the svelte element-attribute types below.
12
+ interface BaseProps {
13
+ /** Icon slot — consumer-rendered (e.g. an MDI `~icons/mdi/*` component). */
14
+ icon?: Snippet;
15
+ text?: string;
16
+ variant?: Responsive<Variant>;
17
+ size?: Responsive<Size>;
18
+ /** Border colour matches the fill (invisible ring, box unchanged) — the default. Set
19
+ * `borderless={false}` for the standard contrasting tint-border; on the `ghost` variant that's
20
+ * the "outline button" (transparent fill + a 30% text-tint edge), no separate variant needed. */
21
+ borderless?: boolean;
22
+ children?: Snippet;
23
+ // Element-agnostic so they're assignable to both <button> and <a> (and typed for consumers).
24
+ onclick?: (event: MouseEvent) => void;
25
+ onmouseenter?: (event: MouseEvent) => void;
26
+ onmouseleave?: (event: MouseEvent) => void;
27
+ element?: HTMLButtonElement | HTMLAnchorElement;
28
+ }
29
+
30
+ interface ButtonProps extends BaseProps, Omit<HTMLButtonAttributes, keyof BaseProps> {
31
+ href?: never;
32
+ }
33
+
34
+ interface AnchorProps extends BaseProps, Omit<HTMLAnchorAttributes, keyof BaseProps | "type"> {
35
+ href: string;
36
+ type?: never;
37
+ disabled?: never;
38
+ }
39
+
40
+ export type Props = ButtonProps | AnchorProps;
41
+
42
+ let {
43
+ icon,
44
+ text,
45
+ variant = "secondary",
46
+ size = "md",
47
+ borderless = true,
48
+ children,
49
+ disabled = false,
50
+ type = "button",
51
+ href,
52
+ class: additionalClass = "",
53
+ title,
54
+ onclick,
55
+ onmouseenter,
56
+ onmouseleave,
57
+ element = $bindable(),
58
+ ...restProps
59
+ }: Props = $props();
60
+
61
+ // Development warnings for prop conflicts and empty content
62
+ if (DEV) {
63
+ if ((text || icon) && children) {
64
+ console.warn(
65
+ "Button: Props (text/icon) and children provided. Using props, ignoring children.",
66
+ );
67
+ }
68
+ if (!text && !icon && !children) {
69
+ console.warn("Button: No content provided (text, icon, or children). Button will be empty.");
70
+ }
71
+ }
72
+
73
+ // Determine content mode and styling
74
+ const hasProps = !!(text || icon);
75
+ const isIconOnly = icon && !text;
76
+
77
+ // Base classes - height is always h-8 (32px), width is h-8 for icon-only
78
+ const baseClasses =
79
+ "inline-flex items-center justify-center [border-radius:var(--ui-border-radius)] font-medium transition-colors no-underline cursor-pointer border focus-visible:[outline:2px_solid_color-mix(in_srgb,var(--ui-color-text)_70%,transparent)] focus-visible:[outline-offset:0px]";
80
+
81
+ // Additional classes for disabled state (only applies to buttons)
82
+ const disabledClasses = href ? "" : "disabled:opacity-50 disabled:cursor-not-allowed";
83
+
84
+ // Size axis (see types/sizes.ts): height + (icon-only square | labeled padding) + text + icon.
85
+ // Each map is per-Size; responsiveClasses() turns a scalar OR a { base, xs, … } map into the
86
+ // right (breakpoint-prefixed) Tailwind classes. Icons scale 14 / 16 / 20px (sm / md / lg).
87
+ const boxMap: Record<Size, string> = { sm: "h-6 w-6", md: "h-8 w-8", lg: "h-10 w-10" };
88
+ const padMap: Record<Size, string> = { sm: "h-6 px-2.5", md: "h-8 px-4", lg: "h-10 px-6" };
89
+ const textMap: Record<Size, string> = { sm: "text-xs", md: "text-sm", lg: "text-base" };
90
+ const iconMap: Record<Size, string> = {
91
+ sm: "[&_svg]:w-3.5 [&_svg]:h-3.5",
92
+ md: "[&_svg]:w-4 [&_svg]:h-4",
93
+ lg: "[&_svg]:w-5 [&_svg]:h-5",
94
+ };
95
+ const sizeClasses = $derived(
96
+ `${responsiveClasses(size, isIconOnly ? boxMap : padMap)} ${responsiveClasses(size, textMap)}`,
97
+ );
98
+ const iconSizeClass = $derived(responsiveClasses(size, iconMap));
99
+
100
+ // FILL = background + text + hover-background (no border here — the border is a separate axis so
101
+ // `borderless` can swap it independently while the 1px box stays constant).
102
+ const fillClasses = {
103
+ primary:
104
+ "[background-color:var(--ui-color-primary)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-primary)_90%,black)] [&_svg]:fill-current",
105
+ secondary:
106
+ "[background-color:var(--ui-color-secondary)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-secondary)_90%,black)] [&_svg]:fill-current",
107
+ danger:
108
+ "[background-color:var(--ui-color-error)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-error)_90%,black)] [&_svg]:fill-current",
109
+ info: "[background-color:var(--ui-color-info)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-info)_90%,black)] [&_svg]:fill-current",
110
+ success:
111
+ "[background-color:var(--ui-color-success)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-success)_90%,black)] [&_svg]:fill-current",
112
+ warning:
113
+ "[background-color:var(--ui-color-warning)] text-white hover:[background-color:color-mix(in_srgb,var(--ui-color-warning)_90%,black)] [&_svg]:fill-current",
114
+ // ghost: transparent fill, hover-tint only — the minimal button (table chrome, pagination).
115
+ ghost:
116
+ "bg-transparent [color:var(--ui-color-text)] hover:[background-color:var(--ui-color-hover)] [&_svg]:fill-current",
117
+ // neutral: SOLID, like the accent variants but coloured by --ui-color-text — solid text-colour fill
118
+ // with contrasting (background-colour) text. Black-on-white / white-on-black. Hover lifts to bg.
119
+ neutral:
120
+ "[background-color:var(--ui-color-text)] [color:var(--ui-color-background)] hover:[background-color:color-mix(in_srgb,var(--ui-color-text)_85%,var(--ui-color-background))] [&_svg]:fill-current",
121
+ };
122
+
123
+ // borderless=true (default): border tracks the fill (incl. hover) → an invisible, space-preserving ring.
124
+ const borderMatchClasses = {
125
+ primary:
126
+ "[border-color:var(--ui-color-primary)] hover:[border-color:color-mix(in_srgb,var(--ui-color-primary)_90%,black)]",
127
+ secondary:
128
+ "[border-color:var(--ui-color-secondary)] hover:[border-color:color-mix(in_srgb,var(--ui-color-secondary)_90%,black)]",
129
+ danger:
130
+ "[border-color:var(--ui-color-error)] hover:[border-color:color-mix(in_srgb,var(--ui-color-error)_90%,black)]",
131
+ info: "[border-color:var(--ui-color-info)] hover:[border-color:color-mix(in_srgb,var(--ui-color-info)_90%,black)]",
132
+ success:
133
+ "[border-color:var(--ui-color-success)] hover:[border-color:color-mix(in_srgb,var(--ui-color-success)_90%,black)]",
134
+ warning:
135
+ "[border-color:var(--ui-color-warning)] hover:[border-color:color-mix(in_srgb,var(--ui-color-warning)_90%,black)]",
136
+ ghost: "border-transparent hover:[border-color:var(--ui-color-hover)]",
137
+ neutral:
138
+ "[border-color:var(--ui-color-text)] hover:[border-color:color-mix(in_srgb,var(--ui-color-text)_85%,var(--ui-color-background))]",
139
+ };
140
+
141
+ // borderless=false: the standard contrasting tint-border — the SAME calc as Input/Card (token @
142
+ // --ui-tint-border at rest, @ --ui-tint-border-hover on hover). On ghost/neutral (token =
143
+ // --ui-color-text) this is the outline button; on solids, an accent rim that strengthens on hover.
144
+ const borderTintClasses = {
145
+ primary:
146
+ "[border-color:color-mix(in_srgb,var(--ui-color-primary)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-primary)_var(--ui-tint-border-hover),transparent)]",
147
+ secondary:
148
+ "[border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border-hover),transparent)]",
149
+ danger:
150
+ "[border-color:color-mix(in_srgb,var(--ui-color-error)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-error)_var(--ui-tint-border-hover),transparent)]",
151
+ info: "[border-color:color-mix(in_srgb,var(--ui-color-info)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-info)_var(--ui-tint-border-hover),transparent)]",
152
+ success:
153
+ "[border-color:color-mix(in_srgb,var(--ui-color-success)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-success)_var(--ui-tint-border-hover),transparent)]",
154
+ warning:
155
+ "[border-color:color-mix(in_srgb,var(--ui-color-warning)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-warning)_var(--ui-tint-border-hover),transparent)]",
156
+ ghost:
157
+ "[border-color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border-hover),transparent)]",
158
+ neutral:
159
+ "[border-color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border),transparent)] hover:[border-color:color-mix(in_srgb,var(--ui-color-text)_var(--ui-tint-border-hover),transparent)]",
160
+ };
161
+
162
+ // Build final classes - reactive to variant changes
163
+ const buttonClasses = $derived(
164
+ [
165
+ baseClasses,
166
+ sizeClasses,
167
+ responsiveClasses(variant, fillClasses),
168
+ responsiveClasses(variant, borderless ? borderMatchClasses : borderTintClasses),
169
+ disabledClasses,
170
+ additionalClass,
171
+ ]
172
+ .filter(Boolean)
173
+ .join(" "),
174
+ );
175
+ </script>
176
+
177
+ <!-- Icon slot, sized by the lib; color comes from the variant's [&_svg]:fill-current. -->
178
+ {#snippet iconSlot()}
179
+ <span class="inline-flex {iconSizeClass}">{@render icon?.()}</span>
180
+ {/snippet}
181
+
182
+ {#if href}
183
+ <a
184
+ bind:this={element}
185
+ {href}
186
+ use:tooltip={{ text: title ?? undefined }}
187
+ {onclick}
188
+ {onmouseenter}
189
+ {onmouseleave}
190
+ class={buttonClasses}
191
+ {...restProps as HTMLAnchorAttributes}
192
+ >
193
+ <div class="{text || children ? '-translate-y-px' : ''} flex items-center gap-2">
194
+ {#if hasProps}
195
+ {#if isIconOnly}
196
+ {@render iconSlot()}
197
+ {:else}
198
+ {#if icon}
199
+ {@render iconSlot()}
200
+ {/if}
201
+ {#if text}
202
+ <span>{text}</span>
203
+ {/if}
204
+ {/if}
205
+ {:else if children}
206
+ {@render children()}
207
+ {/if}
208
+ </div>
209
+ </a>
210
+ {:else}
211
+ <button
212
+ bind:this={element}
213
+ {type}
214
+ {disabled}
215
+ aria-disabled={disabled}
216
+ use:tooltip={{ text: title ?? undefined }}
217
+ {onclick}
218
+ {onmouseenter}
219
+ {onmouseleave}
220
+ class={buttonClasses}
221
+ {...restProps as HTMLButtonAttributes}
222
+ >
223
+ <div class="{text || children ? '-translate-y-px' : ''} flex items-center gap-2">
224
+ {#if hasProps}
225
+ {#if isIconOnly}
226
+ {@render iconSlot()}
227
+ {:else}
228
+ {#if icon}
229
+ {@render iconSlot()}
230
+ {/if}
231
+ {#if text}
232
+ <span>{text}</span>
233
+ {/if}
234
+ {/if}
235
+ {:else if children}
236
+ {@render children()}
237
+ {/if}
238
+ </div>
239
+ </button>
240
+ {/if}
@@ -0,0 +1,33 @@
1
+ import type { Snippet } from "svelte";
2
+ import type { Variant } from "../types/variants.js";
3
+ import type { Size } from "../types/sizes.js";
4
+ import { type Responsive } from "../types/responsive.js";
5
+ import type { HTMLButtonAttributes, HTMLAnchorAttributes } from "svelte/elements";
6
+ interface BaseProps {
7
+ /** Icon slot — consumer-rendered (e.g. an MDI `~icons/mdi/*` component). */
8
+ icon?: Snippet;
9
+ text?: string;
10
+ variant?: Responsive<Variant>;
11
+ size?: Responsive<Size>;
12
+ /** Border colour matches the fill (invisible ring, box unchanged) — the default. Set
13
+ * `borderless={false}` for the standard contrasting tint-border; on the `ghost` variant that's
14
+ * the "outline button" (transparent fill + a 30% text-tint edge), no separate variant needed. */
15
+ borderless?: boolean;
16
+ children?: Snippet;
17
+ onclick?: (event: MouseEvent) => void;
18
+ onmouseenter?: (event: MouseEvent) => void;
19
+ onmouseleave?: (event: MouseEvent) => void;
20
+ element?: HTMLButtonElement | HTMLAnchorElement;
21
+ }
22
+ interface ButtonProps extends BaseProps, Omit<HTMLButtonAttributes, keyof BaseProps> {
23
+ href?: never;
24
+ }
25
+ interface AnchorProps extends BaseProps, Omit<HTMLAnchorAttributes, keyof BaseProps | "type"> {
26
+ href: string;
27
+ type?: never;
28
+ disabled?: never;
29
+ }
30
+ export type Props = ButtonProps | AnchorProps;
31
+ declare const Button: import("svelte").Component<Props, {}, "element">;
32
+ type Button = ReturnType<typeof Button>;
33
+ export default Button;
@@ -0,0 +1,145 @@
1
+ <!--
2
+ /**
3
+ * ButtonDropdown — a split-button compound built from Button + Menu.
4
+ *
5
+ * Same prop surface as Button (variant, size, icon, text, children,
6
+ * disabled, onclick, …); when `items` is empty/absent it renders
7
+ * identically to a single Button. With items, it splits into a primary
8
+ * half (the consumer's button) and a secondary chevron half that opens
9
+ * a Menu anchored to the chevron.
10
+ *
11
+ * Compound rule: nothing in this file builds primitives — it composes
12
+ * Button + Menu. The split visual is enforced by overriding the two
13
+ * halves' rounded corners + adjacent border via the {class} prop both
14
+ * Buttons already accept; the outer wrapper carries the shape so a
15
+ * consumer-applied shadow (e.g. `class="shadow-md"`) lands on the
16
+ * combined silhouette rather than splitting at the seam between the
17
+ * two halves.
18
+ */
19
+ -->
20
+ <script lang="ts">
21
+ import ChevronDown from "~icons/mdi/chevron-down";
22
+ import Button from "./Button.svelte";
23
+ import Menu from "./Menu.svelte";
24
+ import type { Variant } from "../types/variants.js";
25
+ import type { Size } from "../types/sizes.js";
26
+ import type { Responsive } from "../types/responsive.js";
27
+ import type { Snippet } from "svelte";
28
+ import type { MenuItem } from "../data/table/types.js";
29
+
30
+ // Mirrors Button's button-flavour Props (the anchor-flavour `href` is
31
+ // intentionally not on offer here — a split-button's two halves can't
32
+ // both be a single navigation target). Adds `items`.
33
+ interface Props {
34
+ /** Icon slot for the primary half — same contract as Button.icon. */
35
+ icon?: Snippet;
36
+ text?: string;
37
+ variant?: Variant;
38
+ size?: Responsive<Size>;
39
+ children?: Snippet;
40
+ disabled?: boolean;
41
+ type?: "button" | "submit" | "reset";
42
+ title?: string;
43
+ /** Append-extra classes — applied to the OUTER wrapper so consumer
44
+ * styling (margin, shadow, alignment) targets the combined shape
45
+ * rather than just one half. */
46
+ class?: string;
47
+ onclick?: (event: MouseEvent) => void;
48
+ /** Menu items shown when the chevron is clicked. Empty / absent →
49
+ * the component degrades gracefully to a plain Button. */
50
+ items?: MenuItem[];
51
+ /** Menu placement passed through to `<Menu>`. Defaults match the
52
+ * ActionBar split-button convention: opens below, right-aligned. */
53
+ menuSide?: "top" | "bottom" | "left" | "right";
54
+ menuAlign?: "start" | "center" | "end";
55
+ }
56
+
57
+ let {
58
+ icon,
59
+ text,
60
+ variant = "secondary",
61
+ size = "md",
62
+ children,
63
+ disabled = false,
64
+ type = "button",
65
+ title,
66
+ class: additionalClass = "",
67
+ onclick,
68
+ items,
69
+ menuSide = "bottom",
70
+ menuAlign = "end",
71
+ }: Props = $props();
72
+
73
+ // No items → degrade to a plain Button with the exact same prop
74
+ // surface. Keeps `<ButtonDropdown {variant} {size} text="…" />`
75
+ // usable as a drop-in for sites that may or may not declare items
76
+ // dynamically.
77
+ const hasItems = $derived(!!items && items.length > 0);
78
+
79
+ // Chevron-half state. `chevronEl` is what Menu anchors against; the
80
+ // element gets bound through Button's existing `bind:element` prop so
81
+ // we never poke at the DOM ourselves.
82
+ let menuOpen = $state(false);
83
+ let chevronEl = $state<HTMLButtonElement | HTMLAnchorElement | undefined>(undefined);
84
+
85
+ // Class overrides applied via Button's {class} pass-through. The
86
+ // outer wrapper is `inline-flex` and the two halves abut; we strip
87
+ // the inner corners + the primary's right border so the seam reads
88
+ // as a single divider rather than two stacked borders.
89
+ const primaryHalfClass = "rounded-r-none border-r-0";
90
+ const secondaryHalfClass = "rounded-l-none px-2";
91
+ </script>
92
+
93
+ {#if hasItems}
94
+ <span class="inline-flex {additionalClass}">
95
+ <Button
96
+ {icon}
97
+ {text}
98
+ {children}
99
+ {variant}
100
+ {size}
101
+ {disabled}
102
+ {type}
103
+ {title}
104
+ {onclick}
105
+ class={primaryHalfClass}
106
+ />
107
+ <Button
108
+ {variant}
109
+ {size}
110
+ {disabled}
111
+ type="button"
112
+ title="More actions"
113
+ aria-haspopup="menu"
114
+ aria-expanded={menuOpen}
115
+ onclick={() => (menuOpen = !menuOpen)}
116
+ bind:element={chevronEl}
117
+ class={secondaryHalfClass}
118
+ icon={chevronIcon}
119
+ />
120
+ </span>
121
+ <Menu
122
+ anchor={chevronEl as HTMLElement | undefined}
123
+ bind:open={menuOpen}
124
+ items={items!}
125
+ side={menuSide}
126
+ align={menuAlign}
127
+ />
128
+ {:else}
129
+ <Button
130
+ {icon}
131
+ {text}
132
+ {children}
133
+ {variant}
134
+ {size}
135
+ {disabled}
136
+ {type}
137
+ {title}
138
+ {onclick}
139
+ class={additionalClass}
140
+ />
141
+ {/if}
142
+
143
+ {#snippet chevronIcon()}
144
+ <ChevronDown />
145
+ {/snippet}
@@ -0,0 +1,31 @@
1
+ import type { Variant } from "../types/variants.js";
2
+ import type { Size } from "../types/sizes.js";
3
+ import type { Responsive } from "../types/responsive.js";
4
+ import type { Snippet } from "svelte";
5
+ import type { MenuItem } from "../data/table/types.js";
6
+ interface Props {
7
+ /** Icon slot for the primary half — same contract as Button.icon. */
8
+ icon?: Snippet;
9
+ text?: string;
10
+ variant?: Variant;
11
+ size?: Responsive<Size>;
12
+ children?: Snippet;
13
+ disabled?: boolean;
14
+ type?: "button" | "submit" | "reset";
15
+ title?: string;
16
+ /** Append-extra classes — applied to the OUTER wrapper so consumer
17
+ * styling (margin, shadow, alignment) targets the combined shape
18
+ * rather than just one half. */
19
+ class?: string;
20
+ onclick?: (event: MouseEvent) => void;
21
+ /** Menu items shown when the chevron is clicked. Empty / absent →
22
+ * the component degrades gracefully to a plain Button. */
23
+ items?: MenuItem[];
24
+ /** Menu placement passed through to `<Menu>`. Defaults match the
25
+ * ActionBar split-button convention: opens below, right-aligned. */
26
+ menuSide?: "top" | "bottom" | "left" | "right";
27
+ menuAlign?: "start" | "center" | "end";
28
+ }
29
+ declare const ButtonDropdown: import("svelte").Component<Props, {}, "">;
30
+ type ButtonDropdown = ReturnType<typeof ButtonDropdown>;
31
+ export default ButtonDropdown;