@spaethtech/svelte-ui 0.15.1-dev.69.15831f2 → 0.15.1-dev.72.768dcbb

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.
@@ -152,7 +152,7 @@ Mount `<Toaster />` once at the app root, then push from anywhere with `toast.su
152
152
  All from `@spaethtech/svelte-ui` (see the shipped `docs/components.md` + `docs/usage.md` for props +
153
153
  examples):
154
154
 
155
- - **Form:** `Button` `ButtonDropdown` `Input` `Select` `Combobox` `Slider` `List` `TextArea` `Checkbox`
155
+ - **Form:** `Button` `ButtonGroup` `Input` `Select` `Combobox` `Slider` `List` `TextArea` `Checkbox`
156
156
  `Toggle` `Radio` `Rating` (display `average`/`count`, OR `interactive` + `bind:value` star picker
157
157
  with `allowHalf`, click/hover/arrow-keys) · **`Combobox`** (free-text autocomplete — arbitrary value + suggestions,
158
158
  client `options` or async `suggest`; vs `Select` which is a closed choice. Composes Input + Popup.) ·
@@ -163,11 +163,13 @@ examples):
163
163
  `showValue`/`format`; FieldChrome + `variant`/`size`; keyboard + `role="slider"`) · **`FieldGroup`**
164
164
  (fieldset wrapper for radio/checkbox/toggle sets) · **`ButtonGroup`**
165
165
  (joined row/`col` of `<Button>`s as one unit — an action toolbar or a **controlled** segmented
166
- selector; `items` of `ButtonGroupItem` {`value`,`text`,`icon`,`variant`,`disabled`,`menu`,`onclick`}
167
- or a presentational default slot; `select` `'none'|'single'|'multi'` with `bind:value`/`bind:values`;
168
- selected = variant-swap to filled, unselected = `ghost`; the group owns the frame; an item `menu`
169
- (`MenuItem[]`) makes a split-button. Not a form field — for legend/`name`/submit use `FieldGroup`;
170
- for view switching use `TabStrip`.)
166
+ selector; `items` of `ButtonGroupItem` {`value`,`text`,`icon`,`variant`,`disabled`,`loading`,`href`/
167
+ `target`,`menu`,`menuOnly`,`onclick`} or a presentational default slot; `select`
168
+ `'none'|'single'|'multi'` with `bind:value`/`bind:values`; selected = variant-swap to filled,
169
+ unselected = `ghost`; the group owns the frame. An item `menu` (`MenuItem[]`) makes a **split
170
+ button** (action + caret); add `menuOnly` for a caret-only **menu-trigger** cell (e.g. an
171
+ `[action][▾]` pair = a plain item + a `menuOnly` item). This replaces the old `ButtonDropdown`. Not
172
+ a form field — for legend/`name`/submit use `FieldGroup`; for view switching use `TabStrip`.)
171
173
  - **Specialized inputs:** `PasswordInput` `EmailInput` `SearchInput` `NumberInput` (formatting +
172
174
  `percent`/`stepper`/`clamp`/`liveFormat`) `PhoneInput` (stores E.164; dep-free, inject
173
175
  `parse`/`format` for per-country)
@@ -28,10 +28,20 @@
28
28
  /** Icon slot — a consumer-rendered component (e.g. `~icons/mdi/*`), like `Button`'s `icon`. */
29
29
  icon?: Snippet;
30
30
  disabled?: boolean;
31
+ /** Show a spinner in place of the icon (forwarded to `Button`). */
32
+ loading?: boolean;
31
33
  /** Per-item colour override; otherwise the group `variant`. */
32
34
  variant?: Variant;
33
- /** Split-button dropdown: a trailing caret opens a `Menu` with these items. */
35
+ /** Anchor mode the item renders as a link (forwarded to `Button`). */
36
+ href?: string;
37
+ target?: string;
38
+ /** A `Menu` opened from this item. By default the item becomes a **split button** (its own action
39
+ * + a trailing caret that opens the menu). With `menuOnly`, the whole item is the menu trigger
40
+ * (no split, no caret) — the click opens the menu. Use a `menuOnly` item as the chevron half of a
41
+ * hand-built [action][▾] pair. */
34
42
  menu?: MenuItem[];
43
+ /** Make `menu` open on the item's own click, with no split caret (menu-trigger mode). */
44
+ menuOnly?: boolean;
35
45
  /** Action-mode click. Fires in `select="none"`; in selection modes it runs after the select. */
36
46
  onclick?: (e: MouseEvent) => void;
37
47
  };
@@ -179,7 +189,22 @@
179
189
  <div role={groupRole} class={frameClass} onkeydown={onKeydown}>
180
190
  {#if items}
181
191
  {#each items as item, i (item.value ?? item.text ?? i)}
182
- {#if item.menu}
192
+ {#if item.menu && item.menuOnly}
193
+ <!-- Menu-trigger cell: the whole button opens the Menu on click (no split, no extra caret).
194
+ A bare item (no icon/text) defaults to a chevron so an [action][▾] pair needs no boilerplate. -->
195
+ <Button
196
+ icon={item.icon ?? (item.text ? undefined : caretIcon)}
197
+ text={item.text}
198
+ variant={variantFor(item)}
199
+ {size}
200
+ disabled={disabled || item.disabled}
201
+ loading={item.loading}
202
+ aria-haspopup="menu"
203
+ aria-expanded={!!menuOpen[i]}
204
+ bind:element={caretRefs[i]}
205
+ onclick={() => (menuOpen[i] = !menuOpen[i])}
206
+ />
207
+ {:else if item.menu}
183
208
  <!-- Split button: main (action/select) + caret (opens the Menu). One cell in the group. -->
184
209
  <span class="inline-flex {isRow ? '[&>button+button]:border-l' : '[&>button+button]:border-t'} [&>button+button]:[border-color:var(--ui-border-color)]">
185
210
  <Button
@@ -188,6 +213,7 @@
188
213
  variant={variantFor(item)}
189
214
  {size}
190
215
  disabled={disabled || item.disabled}
216
+ loading={item.loading}
191
217
  bind:element={mainRefs[i]}
192
218
  tabindex={tabIdxFor(i, item)}
193
219
  {...selectAttrs(item)}
@@ -206,6 +232,22 @@
206
232
  {#snippet icon()}<IconChevronDown />{/snippet}
207
233
  </Button>
208
234
  </span>
235
+ {:else if item.href && !disabled && !item.disabled}
236
+ <!-- Anchor-mode item (renders as a link). A disabled link falls through to the plain button
237
+ branch below — an <a> has no native `disabled`. -->
238
+ <Button
239
+ icon={item.icon}
240
+ text={item.text}
241
+ variant={variantFor(item)}
242
+ {size}
243
+ loading={item.loading}
244
+ href={item.href}
245
+ target={item.target}
246
+ bind:element={mainRefs[i]}
247
+ tabindex={tabIdxFor(i, item)}
248
+ {...selectAttrs(item)}
249
+ onclick={(e) => activate(item, e)}
250
+ />
209
251
  {:else}
210
252
  <Button
211
253
  icon={item.icon}
@@ -213,6 +255,7 @@
213
255
  variant={variantFor(item)}
214
256
  {size}
215
257
  disabled={disabled || item.disabled}
258
+ loading={item.loading}
216
259
  bind:element={mainRefs[i]}
217
260
  tabindex={tabIdxFor(i, item)}
218
261
  {...selectAttrs(item)}
@@ -234,3 +277,5 @@
234
277
  {/if}
235
278
  {/each}
236
279
  {/if}
280
+
281
+ {#snippet caretIcon()}<IconChevronDown />{/snippet}
@@ -10,10 +10,20 @@ export type ButtonGroupItem = {
10
10
  /** Icon slot — a consumer-rendered component (e.g. `~icons/mdi/*`), like `Button`'s `icon`. */
11
11
  icon?: Snippet;
12
12
  disabled?: boolean;
13
+ /** Show a spinner in place of the icon (forwarded to `Button`). */
14
+ loading?: boolean;
13
15
  /** Per-item colour override; otherwise the group `variant`. */
14
16
  variant?: Variant;
15
- /** Split-button dropdown: a trailing caret opens a `Menu` with these items. */
17
+ /** Anchor mode the item renders as a link (forwarded to `Button`). */
18
+ href?: string;
19
+ target?: string;
20
+ /** A `Menu` opened from this item. By default the item becomes a **split button** (its own action
21
+ * + a trailing caret that opens the menu). With `menuOnly`, the whole item is the menu trigger
22
+ * (no split, no caret) — the click opens the menu. Use a `menuOnly` item as the chevron half of a
23
+ * hand-built [action][▾] pair. */
16
24
  menu?: MenuItem[];
25
+ /** Make `menu` open on the item's own click, with no split caret (menu-trigger mode). */
26
+ menuOnly?: boolean;
17
27
  /** Action-mode click. Fires in `select="none"`; in selection modes it runs after the select. */
18
28
  onclick?: (e: MouseEvent) => void;
19
29
  };
@@ -10,7 +10,7 @@
10
10
  <script lang="ts">
11
11
  import Popup from "./Popup.svelte";
12
12
  import type { Side, Align, Boundary } from "../positioning/anchored.js";
13
- import type { MenuItem } from "../data/table/types.js";
13
+ import type { MenuItem, MenuAction, MenuSeparator } from "../data/table/types.js";
14
14
  import type { Size } from "../types/sizes.js";
15
15
  import { responsiveClasses, type Responsive } from "../types/responsive.js";
16
16
  import { variantToken, type Variant } from "../types/variants.js";
@@ -38,8 +38,12 @@
38
38
  } = $props();
39
39
 
40
40
  let active = $state(-1);
41
- const hasIcons = $derived(items.some((i) => i.icon));
42
- const enabledIdx = $derived(items.map((i, idx) => (i.disabled ? -1 : idx)).filter((i) => i >= 0));
41
+ const isSeparator = (i: MenuItem): i is MenuSeparator => "divider" in i;
42
+ const hasIcons = $derived(items.some((i) => !isSeparator(i) && i.icon));
43
+ // Separators (and disabled items) are never focusable / selectable.
44
+ const enabledIdx = $derived(
45
+ items.map((i, idx) => (isSeparator(i) || i.disabled ? -1 : idx)).filter((i) => i >= 0),
46
+ );
43
47
 
44
48
  // Size axis → item padding / text / icon box. Variant → --ui-accent (re-mixes the item's hover tint).
45
49
  const padMap: Record<Size, string> = { sm: "px-1.5 py-1", md: "px-2 py-1.5", lg: "px-2.5 py-2" };
@@ -50,7 +54,7 @@
50
54
  const iconClass = $derived(responsiveClasses(size, iconMap));
51
55
  const accentVar = $derived(variant ? `--ui-accent: var(${variantToken[variant]});` : "");
52
56
 
53
- function choose(item: MenuItem) {
57
+ function choose(item: MenuAction) {
54
58
  if (item.disabled) return;
55
59
  item.onclick?.();
56
60
  if (item.href) window.location.href = item.href;
@@ -70,7 +74,8 @@
70
74
  enabledIdx[enabledIdx.length - 1];
71
75
  } else if (e.key === "Enter" && active >= 0) {
72
76
  e.preventDefault();
73
- choose(items[active]);
77
+ const it = items[active];
78
+ if (!isSeparator(it)) choose(it);
74
79
  }
75
80
  }
76
81
 
@@ -88,30 +93,33 @@
88
93
  class="ui-accent min-w-48 rounded-md border p-1 shadow-lg outline-none [background-color:var(--ui-color-background)] [border-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border),transparent)]"
89
94
  style={accentVar}
90
95
  >
91
- {#each items as item, i (item.label)}
92
- {@const danger = item.danger}
93
- <li role="none">
94
- <button
95
- type="button"
96
- role="menuitem"
97
- disabled={item.disabled}
98
- onmouseenter={() => (active = item.disabled ? active : i)}
99
- onclick={() => choose(item)}
100
- class="flex w-full items-center gap-2 rounded {padClass} {textClass} text-left transition-colors
96
+ {#each items as item, i (i)}
97
+ {#if isSeparator(item)}
98
+ <li role="separator" class="my-1 h-px [background-color:color-mix(in_srgb,var(--ui-color-secondary)_var(--ui-tint-border),transparent)]"></li>
99
+ {:else}
100
+ <li role="none">
101
+ <button
102
+ type="button"
103
+ role="menuitem"
104
+ disabled={item.disabled}
105
+ onmouseenter={() => (active = item.disabled ? active : i)}
106
+ onclick={() => choose(item)}
107
+ class="flex w-full items-center gap-2 rounded {padClass} {textClass} text-left transition-colors
101
108
  {item.disabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'}
102
109
  {i === active && !item.disabled ? '[background-color:var(--ui-color-surface)]' : ''}
103
- {danger ? '[color:var(--ui-color-error)]' : '[color:var(--ui-color-text)]'}"
104
- >
105
- {#if hasIcons}
106
- <span
107
- class="inline-flex {iconClass} shrink-0 items-center justify-center [&_svg]:w-full [&_svg]:h-full"
108
- >
109
- {#if item.icon}{@const Icon = item.icon}<Icon />{/if}
110
- </span>
111
- {/if}
112
- <span class="truncate">{item.label}</span>
113
- </button>
114
- </li>
110
+ {item.danger ? '[color:var(--ui-color-error)]' : '[color:var(--ui-color-text)]'}"
111
+ >
112
+ {#if hasIcons}
113
+ <span
114
+ class="inline-flex {iconClass} shrink-0 items-center justify-center [&_svg]:w-full [&_svg]:h-full"
115
+ >
116
+ {#if item.icon}{@const Icon = item.icon}<Icon />{/if}
117
+ </span>
118
+ {/if}
119
+ <span class="truncate">{item.label}</span>
120
+ </button>
121
+ </li>
122
+ {/if}
115
123
  {/each}
116
124
  </ul>
117
125
  </Popup>
@@ -64,10 +64,10 @@ export interface QueryResult<T> {
64
64
  export type FetchFn<T> = (state: GridState) => QueryResult<FetchResult<T>>;
65
65
  export type ParsedQuery = QueryNode | null;
66
66
  /**
67
- * A row/bulk kebab menu entry. Convention: always render ALL items and `disabled` the invalid
68
- * ones. Icons are optional — when ANY item has an icon, icon-less items reserve the icon column.
67
+ * A menu action. Convention: always render ALL items and `disabled` the invalid ones. Icons are
68
+ * optional — when ANY item has an icon, icon-less items reserve the icon column so labels align.
69
69
  */
70
- export type MenuItem = {
70
+ export type MenuAction = {
71
71
  label: string;
72
72
  icon?: Component<{
73
73
  class?: string;
@@ -77,3 +77,9 @@ export type MenuItem = {
77
77
  disabled?: boolean;
78
78
  danger?: boolean;
79
79
  };
80
+ /** A non-interactive separator line between groups of actions. */
81
+ export type MenuSeparator = {
82
+ divider: true;
83
+ };
84
+ /** A menu entry — either an action or a separator (`{ divider: true }`). */
85
+ export type MenuItem = MenuAction | MenuSeparator;
package/dist/index.d.ts CHANGED
@@ -7,7 +7,6 @@ export { EmptyState } from "./components/EmptyState/index.js";
7
7
  export { Avatar, AvatarGroup } from "./components/Avatar/index.js";
8
8
  export type { AvatarShape, AvatarStatus, AvatarItem } from "./components/Avatar/index.js";
9
9
  export { default as Button } from "./components/Button.svelte";
10
- export { default as ButtonDropdown } from "./components/ButtonDropdown.svelte";
11
10
  export { default as Card } from "./components/Card.svelte";
12
11
  export { default as CardHeader } from "./components/CardHeader.svelte";
13
12
  export { default as CardBody } from "./components/CardBody.svelte";
@@ -53,7 +52,7 @@ export { CommandPalette } from "./components/CommandPalette/index.js";
53
52
  export type { Command } from "./components/CommandPalette/index.js";
54
53
  export { Tree } from "./components/Tree/index.js";
55
54
  export type { TreeNode } from "./components/Tree/index.js";
56
- export type { MenuItem } from "./data/table/types.js";
55
+ export type { MenuItem, MenuAction, MenuSeparator } from "./data/table/types.js";
57
56
  export { ButtonGroup } from "./components/ButtonGroup/index.js";
58
57
  export type { ButtonGroupItem, ButtonGroupSelect } from "./components/ButtonGroup/index.js";
59
58
  export { default as Checkbox } from "./components/Checkbox.svelte";
package/dist/index.js CHANGED
@@ -7,7 +7,6 @@ export { Kbd } from "./components/Kbd/index.js";
7
7
  export { EmptyState } from "./components/EmptyState/index.js";
8
8
  export { Avatar, AvatarGroup } from "./components/Avatar/index.js";
9
9
  export { default as Button } from "./components/Button.svelte";
10
- export { default as ButtonDropdown } from "./components/ButtonDropdown.svelte";
11
10
  export { default as Card } from "./components/Card.svelte";
12
11
  export { default as CardHeader } from "./components/CardHeader.svelte";
13
12
  export { default as CardBody } from "./components/CardBody.svelte";
@@ -210,8 +210,14 @@ the selected one fills with the group `variant`. Not a form field (controlled vi
210
210
  `string[]`, multi), `orientation` (`'row' | 'col'`, default `'row'`), `disabled` (whole group),
211
211
  `class`, `children` (a **presentational** default slot — hand-authored `<Button>`s get the frame but
212
212
  you own their behaviour; mutually exclusive with `items`, which wins + DEV-warns).
213
- - **`ButtonGroupItem`**: `{ value?, text?, icon? (Snippet), disabled?, variant?, menu? (MenuItem[] —
214
- renders a split-button caret opening a `Menu`), onclick? }`.
213
+ - **`ButtonGroupItem`**: `{ value?, text?, icon? (Snippet), disabled?, loading?, variant?, href?,
214
+ target?, menu? (MenuItem[]), menuOnly?, onclick? }`. `loading`/`href`/`target` forward to `Button`
215
+ (a disabled `href` item falls back to a plain disabled button — `<a>` has no `disabled`).
216
+ - **Split buttons & menu triggers**: an item with `menu` renders a **split button** (its action + a
217
+ trailing caret that opens the `Menu`). Add **`menuOnly`** and the whole item becomes a caret-only
218
+ **menu-trigger** (its click opens the menu; a bare one defaults to a chevron). Compose the two for a
219
+ standalone `[ action ][ ▾ ]` dropdown: a normal item + a `menuOnly` item — this **replaces the
220
+ removed `ButtonDropdown`**.
215
221
  - **Selection / a11y**: `'single'` → `role="radiogroup"` + `aria-checked`, arrow-key roving;
216
222
  `'multi'` → `aria-pressed`; `'none'` → `role="toolbar"`, `item.onclick` fires (no swap).
217
223
 
@@ -465,8 +471,10 @@ never clipped by overflow/stacking contexts. Placement via the shared `anchored`
465
471
  Keyboard-navigable action menu rendered inside a `Popup`.
466
472
 
467
473
  - **Location**: `src/lib/components/Menu.svelte`
468
- - **Props**: `anchor`, `open` (bindable), `items` (`MenuItem[]` from `@spaethtech/svelte-ui/data`), `side`,
469
- `align`, `boundary`
474
+ - **Props**: `anchor`, `open` (bindable), `items` (`MenuItem[]`), `side`, `align`, `boundary`, `size`,
475
+ `variant`
476
+ - **`MenuItem`**: an **action** `{ label, icon?, onclick?, href?, disabled?, danger? }` **or** a
477
+ **separator** `{ divider: true }` (a non-interactive rule between groups; skipped by keyboard nav).
470
478
  - **Features**: renders all items always and disables the invalid ones; aligns labels when any
471
479
  item has an icon
472
480
 
package/docs/usage.md CHANGED
@@ -464,6 +464,19 @@ A joined row/column of buttons — an action toolbar or a controlled segmented s
464
464
  <!-- Action toolbar with a split-button (Export has a `menu`) -->
465
465
  <ButtonGroup items={actions} variant="secondary" />
466
466
 
467
+ <!-- Split-button dropdown [ action ][ ▾ ] — replaces the old ButtonDropdown.
468
+ A normal action item + a `menuOnly` caret item that opens a Menu (with a divider). -->
469
+ <ButtonGroup
470
+ variant="primary"
471
+ items={[
472
+ { text: "Save", icon: saveIcon, loading: saving, onclick: doSave },
473
+ { menuOnly: true, menu: [
474
+ { label: "Save and close", onclick: saveAndClose },
475
+ { divider: true },
476
+ { label: "Discard", danger: true, onclick: discard },
477
+ ] },
478
+ ]} />
479
+
467
480
  <!-- Vertical -->
468
481
  <ButtonGroup items={alignItems} select="single" bind:value={align} orientation="col" />
469
482
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.15.1-dev.69.15831f2",
3
+ "version": "0.15.1-dev.72.768dcbb",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"
@@ -1,145 +0,0 @@
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}
@@ -1,31 +0,0 @@
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;
@@ -1,157 +0,0 @@
1
- <!--
2
- /**
3
- * DateRangePicker — a start–end range picker with a presets rail. Composes the existing `Calendar`
4
- * (mode="range") inside an `Input`-framed trigger + `Popup`, exposing ergonomic bind:start / bind:end
5
- * (ISO). See DateRangePicker.spec.md.
6
- */
7
- -->
8
- <script lang="ts" module>
9
- export type RangePreset = { label: string; start: string; end: string };
10
- </script>
11
-
12
- <script lang="ts">
13
- import type { Snippet } from "svelte";
14
- import Input from "../Input.svelte";
15
- import Popup from "../Popup.svelte";
16
- import Button from "../Button.svelte";
17
- import Calendar from "../Calendar.svelte";
18
- import IconCalendar from "~icons/mdi/calendar-outline";
19
- import type { Variant } from "../../types/variants.js";
20
- import type { Size } from "../../types/sizes.js";
21
- import type { Responsive } from "../../types/responsive.js";
22
-
23
- // Local ISO (avoids the UTC shift of toISOString near midnight).
24
- const iso = (d: Date) =>
25
- `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
26
- function defaultPresets(): RangePreset[] {
27
- const t = new Date();
28
- t.setHours(0, 0, 0, 0);
29
- const shift = (n: number) => {
30
- const d = new Date(t);
31
- d.setDate(d.getDate() + n);
32
- return iso(d);
33
- };
34
- const today = iso(t);
35
- const monthStart = iso(new Date(t.getFullYear(), t.getMonth(), 1));
36
- return [
37
- { label: "Today", start: today, end: today },
38
- { label: "Yesterday", start: shift(-1), end: shift(-1) },
39
- { label: "Last 7 days", start: shift(-6), end: today },
40
- { label: "Last 30 days", start: shift(-29), end: today },
41
- { label: "This month", start: monthStart, end: today },
42
- ];
43
- }
44
-
45
- let {
46
- start = $bindable(""),
47
- end = $bindable(""),
48
- min,
49
- max,
50
- presets,
51
- variant = "secondary",
52
- size = "md",
53
- disabled = false,
54
- placeholder = "Select dates",
55
- format,
56
- label = null,
57
- aside,
58
- error = null,
59
- description = null,
60
- id,
61
- required = false,
62
- class: cls = "",
63
- }: {
64
- start?: string;
65
- end?: string;
66
- min?: string;
67
- max?: string;
68
- presets?: RangePreset[];
69
- variant?: Variant;
70
- size?: Responsive<Size>;
71
- disabled?: boolean;
72
- placeholder?: string;
73
- format?: (iso: string) => string;
74
- label?: string | null;
75
- aside?: Snippet;
76
- error?: string | Snippet | null;
77
- description?: string | Snippet | null;
78
- id?: string;
79
- required?: boolean;
80
- class?: string;
81
- } = $props();
82
-
83
- const rail = $derived(presets ?? defaultPresets());
84
- let open = $state(false);
85
- let fieldEl = $state<HTMLDivElement>();
86
-
87
- const fmt = (i: string): string => {
88
- if (format) return format(i);
89
- const [y, m, d] = i.split("-").map(Number);
90
- return new Date(y, m - 1, d).toLocaleDateString(undefined, {
91
- month: "short",
92
- day: "numeric",
93
- year: "numeric",
94
- });
95
- };
96
- const display = $derived(
97
- start && end
98
- ? `${fmt(start)} – ${fmt(end)}`
99
- : start
100
- ? `${fmt(start)} – …`
101
- : "",
102
- );
103
-
104
- function applyPreset(p: RangePreset) {
105
- start = p.start;
106
- end = p.end;
107
- open = false;
108
- }
109
- </script>
110
-
111
- <Input
112
- value={display}
113
- readonly
114
- {variant}
115
- {size}
116
- {disabled}
117
- {placeholder}
118
- {label}
119
- {aside}
120
- {error}
121
- {description}
122
- {id}
123
- {required}
124
- class={cls}
125
- icon={calIcon}
126
- bind:fieldElement={fieldEl}
127
- onclick={() => (disabled ? null : (open = !open))}
128
- />
129
-
130
- {#snippet calIcon()}<IconCalendar />{/snippet}
131
-
132
- <Popup anchor={fieldEl} bind:open side="bottom" align="start" lightDismiss>
133
- <div
134
- class="flex overflow-hidden rounded-md border shadow-lg [background-color:var(--ui-color-background)] [border-color:var(--ui-border-color)]"
135
- >
136
- {#if rail.length}
137
- <div class="flex flex-col gap-0.5 border-r p-2 [border-color:var(--ui-border-color)]">
138
- {#each rail as p (p.label)}
139
- <Button text={p.label} variant="ghost" size="sm" onclick={() => applyPreset(p)} />
140
- {/each}
141
- </div>
142
- {/if}
143
- <div class="p-2">
144
- <Calendar
145
- mode="range"
146
- bind:start
147
- bind:end
148
- {min}
149
- {max}
150
- {variant}
151
- onrange={(r) => {
152
- if (r.start && r.end) open = false;
153
- }}
154
- />
155
- </div>
156
- </div>
157
- </Popup>
@@ -1,31 +0,0 @@
1
- export type RangePreset = {
2
- label: string;
3
- start: string;
4
- end: string;
5
- };
6
- import type { Snippet } from "svelte";
7
- import type { Variant } from "../../types/variants.js";
8
- import type { Size } from "../../types/sizes.js";
9
- import type { Responsive } from "../../types/responsive.js";
10
- type $$ComponentProps = {
11
- start?: string;
12
- end?: string;
13
- min?: string;
14
- max?: string;
15
- presets?: RangePreset[];
16
- variant?: Variant;
17
- size?: Responsive<Size>;
18
- disabled?: boolean;
19
- placeholder?: string;
20
- format?: (iso: string) => string;
21
- label?: string | null;
22
- aside?: Snippet;
23
- error?: string | Snippet | null;
24
- description?: string | Snippet | null;
25
- id?: string;
26
- required?: boolean;
27
- class?: string;
28
- };
29
- declare const DateRangePicker: import("svelte").Component<$$ComponentProps, {}, "start" | "end">;
30
- type DateRangePicker = ReturnType<typeof DateRangePicker>;
31
- export default DateRangePicker;
@@ -1,2 +0,0 @@
1
- export { default as DateRangePicker } from "./DateRangePicker.svelte";
2
- export type { RangePreset } from "./DateRangePicker.svelte";
@@ -1 +0,0 @@
1
- export { default as DateRangePicker } from "./DateRangePicker.svelte";