@spaethtech/svelte-ui 0.15.1-dev.69.15831f2 → 0.15.1-dev.71.e357f8b
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/.claude/skills/svelte-ui/SKILL.md +8 -6
- package/dist/components/ButtonGroup/ButtonGroup.svelte +47 -2
- package/dist/components/ButtonGroup/ButtonGroup.svelte.d.ts +11 -1
- package/dist/components/Menu.svelte +35 -27
- package/dist/data/table/types.d.ts +9 -3
- package/dist/index.d.ts +1 -2
- package/dist/index.js +0 -1
- package/docs/components.md +12 -4
- package/docs/usage.md +13 -0
- package/package.json +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.svelte +0 -157
- package/dist/components/DateRangePicker/DateRangePicker.svelte.d.ts +0 -31
- package/dist/components/DateRangePicker/index.d.ts +0 -2
- package/dist/components/DateRangePicker/index.js +0 -1
|
@@ -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` `
|
|
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`,`
|
|
167
|
-
or a presentational default slot; `select`
|
|
168
|
-
selected = variant-swap to filled,
|
|
169
|
-
(`MenuItem[]`) makes a split
|
|
170
|
-
for
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
42
|
-
const
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
92
|
-
{
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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
|
|
68
|
-
*
|
|
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
|
|
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";
|
package/docs/components.md
CHANGED
|
@@ -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?,
|
|
214
|
-
|
|
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[]`
|
|
469
|
-
`
|
|
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,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 +0,0 @@
|
|
|
1
|
-
export { default as DateRangePicker } from "./DateRangePicker.svelte";
|