@spaethtech/svelte-ui 0.1.10 → 0.3.0
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.
- package/README.md +166 -42
- package/dist/components/Alert.svelte +119 -0
- package/dist/components/Alert.svelte.d.ts +29 -0
- package/dist/components/Badge/Badge.svelte +142 -69
- package/dist/components/Badge/Badge.svelte.d.ts +26 -6
- package/dist/components/Badge/index.d.ts +1 -1
- package/dist/components/Badge/index.js +1 -1
- package/dist/components/Banner.svelte +133 -0
- package/dist/components/Banner.svelte.d.ts +31 -0
- package/dist/components/Button.svelte +240 -0
- package/dist/components/Button.svelte.d.ts +33 -0
- package/dist/components/ButtonDropdown.svelte +145 -0
- package/dist/components/ButtonDropdown.svelte.d.ts +31 -0
- package/dist/components/Calendar.svelte +259 -0
- package/dist/components/Calendar.svelte.d.ts +27 -0
- package/dist/components/Card.svelte +78 -0
- package/dist/components/Card.svelte.d.ts +17 -0
- package/dist/components/CardBody.svelte +41 -0
- package/dist/components/CardBody.svelte.d.ts +17 -0
- package/dist/components/CardFooter.svelte +49 -0
- package/dist/components/CardFooter.svelte.d.ts +16 -0
- package/dist/components/CardHeader.svelte +49 -0
- package/dist/components/CardHeader.svelte.d.ts +16 -0
- package/dist/components/Checkbox.svelte +131 -0
- package/dist/components/Checkbox.svelte.d.ts +16 -0
- package/dist/components/DataTable.svelte +518 -0
- package/dist/components/DataTable.svelte.d.ts +74 -0
- package/dist/components/DatePicker.svelte +312 -0
- package/dist/components/DatePicker.svelte.d.ts +32 -0
- package/dist/components/DateTimeInput.svelte +93 -0
- package/dist/components/DateTimeInput.svelte.d.ts +20 -0
- package/dist/components/Dialog.svelte +132 -0
- package/dist/components/Dialog.svelte.d.ts +24 -0
- package/dist/components/{EmailInput/EmailInput.svelte → EmailInput.svelte} +17 -8
- package/dist/components/EmailInput.svelte.d.ts +21 -0
- package/dist/components/Input.svelte +369 -0
- package/dist/components/Input.svelte.d.ts +35 -0
- package/dist/components/{List/List.svelte → List.svelte} +194 -133
- package/dist/components/{List/List.svelte.d.ts → List.svelte.d.ts} +9 -1
- package/dist/components/Menu.svelte +117 -0
- package/dist/components/Menu.svelte.d.ts +20 -0
- package/dist/components/NotesEditor.svelte +127 -0
- package/dist/components/NotesEditor.svelte.d.ts +17 -0
- package/dist/components/{NumberInput/NumberInput.svelte → NumberInput.svelte} +146 -105
- package/dist/components/{NumberInput/NumberInput.svelte.d.ts → NumberInput.svelte.d.ts} +11 -4
- package/dist/components/PasswordInput.svelte +52 -0
- package/dist/components/PasswordInput.svelte.d.ts +20 -0
- package/dist/components/Popup.svelte +140 -0
- package/dist/components/Popup.svelte.d.ts +31 -0
- package/dist/components/Query.svelte +284 -0
- package/dist/components/Query.svelte.d.ts +15 -0
- package/dist/components/{Rating/Rating.svelte → Rating.svelte} +19 -10
- package/dist/components/Rating.svelte.d.ts +13 -0
- package/dist/components/{SearchInput/SearchInput.svelte → SearchInput.svelte} +19 -8
- package/dist/components/{SearchInput/SearchInput.svelte.d.ts → SearchInput.svelte.d.ts} +9 -2
- package/dist/components/{Select/Select.svelte → Select.svelte} +48 -76
- package/dist/components/{Select/Select.svelte.d.ts → Select.svelte.d.ts} +11 -1
- package/dist/components/SideBarMenu/SideBarMenu.svelte +724 -0
- package/dist/components/SideBarMenu/SideBarMenu.svelte.d.ts +109 -0
- package/dist/components/SideBarMenu/index.d.ts +2 -0
- package/dist/components/SideBarMenu/index.js +1 -0
- package/dist/components/TabStrip/TabStrip.svelte +384 -0
- package/dist/components/TabStrip/TabStrip.svelte.d.ts +50 -0
- package/dist/components/TabStrip/index.d.ts +3 -0
- package/dist/components/TabStrip/index.js +2 -0
- package/dist/components/{TextArea/TextArea.svelte → TextArea.svelte} +143 -112
- package/dist/components/TextArea.svelte.d.ts +38 -0
- package/dist/components/ThemeSelector.svelte +81 -0
- package/dist/components/ThemeSelector.svelte.d.ts +10 -0
- package/dist/components/TimePicker.svelte +148 -0
- package/dist/components/TimePicker.svelte.d.ts +28 -0
- package/dist/components/TimeRangeInput.svelte +203 -0
- package/dist/components/TimeRangeInput.svelte.d.ts +29 -0
- package/dist/components/TimeSpinner.svelte +202 -0
- package/dist/components/TimeSpinner.svelte.d.ts +26 -0
- package/dist/components/Toast/Toaster.svelte +51 -0
- package/dist/components/Toast/Toaster.svelte.d.ts +12 -0
- package/dist/components/Toast/toast.svelte.d.ts +29 -0
- package/dist/components/Toast/toast.svelte.js +27 -0
- package/dist/components/Toggle.svelte +93 -0
- package/dist/components/Toggle.svelte.d.ts +15 -0
- package/dist/data/dataset.d.ts +42 -0
- package/dist/data/dataset.js +3 -0
- package/dist/data/index.d.ts +3 -0
- package/dist/data/index.js +3 -0
- package/dist/data/query/ast.d.ts +62 -0
- package/dist/data/query/ast.js +12 -0
- package/dist/data/query/index.d.ts +3 -0
- package/dist/data/query/index.js +3 -0
- package/dist/data/query/lexer.d.ts +33 -0
- package/dist/data/query/lexer.js +225 -0
- package/dist/data/query/parser.d.ts +11 -0
- package/dist/data/query/parser.js +252 -0
- package/dist/data/table/grid.svelte.d.ts +57 -0
- package/dist/data/table/grid.svelte.js +139 -0
- package/dist/data/table/index.d.ts +2 -0
- package/dist/data/table/index.js +2 -0
- package/dist/data/table/types.d.ts +79 -0
- package/dist/index.d.ts +48 -22
- package/dist/index.js +45 -21
- package/dist/positioning/anchored.d.ts +61 -0
- package/dist/positioning/anchored.js +122 -0
- package/dist/positioning/tooltip.d.ts +41 -0
- package/dist/positioning/tooltip.js +147 -0
- package/dist/theme.css +205 -0
- package/dist/types/breakpoints.d.ts +24 -0
- package/dist/types/breakpoints.js +13 -0
- package/dist/types/responsive.d.ts +32 -0
- package/dist/types/responsive.js +54 -0
- package/dist/types/sizes.d.ts +10 -3
- package/dist/types/time.d.ts +19 -0
- package/dist/types/time.js +10 -0
- package/dist/types/variants.d.ts +8 -1
- package/dist/types/variants.js +16 -1
- package/package.json +93 -63
- package/dist/components/Button/Button.svelte +0 -172
- package/dist/components/Button/Button.svelte.d.ts +0 -32
- package/dist/components/Button/index.d.ts +0 -1
- package/dist/components/Button/index.js +0 -1
- package/dist/components/Dialog/Dialog.svelte +0 -101
- package/dist/components/Dialog/Dialog.svelte.d.ts +0 -18
- package/dist/components/Dialog/index.d.ts +0 -1
- package/dist/components/Dialog/index.js +0 -1
- package/dist/components/ElementManager/ElementManager.svelte +0 -397
- package/dist/components/ElementManager/ElementManager.svelte.d.ts +0 -43
- package/dist/components/ElementManager/index.d.ts +0 -1
- package/dist/components/ElementManager/index.js +0 -1
- package/dist/components/EmailInput/EmailInput.svelte.d.ts +0 -14
- package/dist/components/EmailInput/index.d.ts +0 -1
- package/dist/components/EmailInput/index.js +0 -1
- package/dist/components/Icon/Icon.svelte +0 -74
- package/dist/components/Icon/Icon.svelte.d.ts +0 -13
- package/dist/components/Icon/index.d.ts +0 -1
- package/dist/components/Icon/index.js +0 -1
- package/dist/components/Input/Input.svelte +0 -268
- package/dist/components/Input/Input.svelte.d.ts +0 -18
- package/dist/components/Input/index.d.ts +0 -1
- package/dist/components/Input/index.js +0 -1
- package/dist/components/List/index.d.ts +0 -1
- package/dist/components/List/index.js +0 -1
- package/dist/components/ListItem/ListItem.svelte +0 -175
- package/dist/components/ListItem/ListItem.svelte.d.ts +0 -24
- package/dist/components/ListItem/index.d.ts +0 -2
- package/dist/components/ListItem/index.js +0 -2
- package/dist/components/ListView/ListView.svelte +0 -463
- package/dist/components/ListView/ListView.svelte.d.ts +0 -37
- package/dist/components/ListView/index.d.ts +0 -2
- package/dist/components/ListView/index.js +0 -2
- package/dist/components/NodeGraph/BaseNode.d.ts +0 -57
- package/dist/components/NodeGraph/BaseNode.js +0 -30
- package/dist/components/NodeGraph/Edge.svelte +0 -60
- package/dist/components/NodeGraph/Edge.svelte.d.ts +0 -16
- package/dist/components/NodeGraph/ExpressionEvaluator.d.ts +0 -82
- package/dist/components/NodeGraph/ExpressionEvaluator.js +0 -152
- package/dist/components/NodeGraph/Node.svelte +0 -100
- package/dist/components/NodeGraph/Node.svelte.d.ts +0 -10
- package/dist/components/NodeGraph/NodeGraph.svelte +0 -52
- package/dist/components/NodeGraph/NodeGraph.svelte.d.ts +0 -14
- package/dist/components/NodeGraph/NodeInstance.svelte.d.ts +0 -80
- package/dist/components/NodeGraph/NodeInstance.svelte.js +0 -241
- package/dist/components/NodeGraph/NodePort.svelte +0 -75
- package/dist/components/NodeGraph/NodePort.svelte.d.ts +0 -11
- package/dist/components/NodeGraph/decorators.d.ts +0 -81
- package/dist/components/NodeGraph/decorators.js +0 -148
- package/dist/components/NodeGraph/index.d.ts +0 -9
- package/dist/components/NodeGraph/index.js +0 -9
- package/dist/components/NodeGraph/types.d.ts +0 -94
- package/dist/components/NodeGraph/types.js +0 -33
- package/dist/components/NotesEditor/NotesEditor.svelte +0 -203
- package/dist/components/NotesEditor/NotesEditor.svelte.d.ts +0 -9
- package/dist/components/NotesEditor/index.d.ts +0 -1
- package/dist/components/NotesEditor/index.js +0 -1
- package/dist/components/NumberInput/index.d.ts +0 -1
- package/dist/components/NumberInput/index.js +0 -1
- package/dist/components/PasswordInput/PasswordInput.svelte +0 -41
- package/dist/components/PasswordInput/PasswordInput.svelte.d.ts +0 -13
- package/dist/components/PasswordInput/index.d.ts +0 -1
- package/dist/components/PasswordInput/index.js +0 -1
- package/dist/components/Popup/index.d.ts +0 -1
- package/dist/components/Popup/index.js +0 -1
- package/dist/components/Rating/Rating.svelte.d.ts +0 -7
- package/dist/components/Rating/index.d.ts +0 -1
- package/dist/components/Rating/index.js +0 -1
- package/dist/components/ScrollView/ScrollView.svelte +0 -285
- package/dist/components/ScrollView/ScrollView.svelte.d.ts +0 -19
- package/dist/components/ScrollView/index.d.ts +0 -1
- package/dist/components/ScrollView/index.js +0 -1
- package/dist/components/SearchInput/index.d.ts +0 -1
- package/dist/components/SearchInput/index.js +0 -1
- package/dist/components/Select/index.d.ts +0 -1
- package/dist/components/Select/index.js +0 -1
- package/dist/components/TextArea/TextArea.svelte.d.ts +0 -19
- package/dist/components/TextArea/index.d.ts +0 -1
- package/dist/components/TextArea/index.js +0 -1
- package/dist/components/ThemeSelector/ThemeSelector.svelte +0 -64
- package/dist/components/ThemeSelector/ThemeSelector.svelte.d.ts +0 -3
- package/dist/components/ThemeSelector/index.d.ts +0 -1
- package/dist/components/ThemeSelector/index.js +0 -1
- package/dist/components/TooltipHandler/TooltipHandler.svelte +0 -593
- package/dist/components/TooltipHandler/TooltipHandler.svelte.d.ts +0 -10
- package/dist/components/TooltipHandler/index.d.ts +0 -1
- package/dist/components/TooltipHandler/index.js +0 -1
- package/dist/styles/sizes.d.ts +0 -78
- package/dist/styles/sizes.js +0 -120
- package/dist/styles/variants.d.ts +0 -106
- package/dist/styles/variants.js +0 -194
- package/dist/types/ManagerTypes.d.ts +0 -42
- /package/dist/{types/ManagerTypes.js → data/table/types.js} +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from
|
|
1
|
+
export { default } from "./Badge.svelte";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { default } from
|
|
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;
|