@spaethtech/svelte-ui 0.15.1-dev.67.fbf3875 → 0.15.1-dev.69.15831f2
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 +9 -6
- package/dist/components/Combobox/Combobox.svelte +62 -31
- package/dist/components/Combobox/Combobox.svelte.d.ts +1 -1
- package/dist/components/Pagination/Pagination.svelte +58 -43
- package/dist/components/Pagination/Pagination.svelte.d.ts +7 -2
- package/dist/components/Rating.svelte +169 -30
- package/dist/components/Rating.svelte.d.ts +22 -4
- package/dist/components/Slider/Slider.svelte +5 -3
- package/dist/components/TokenInput/TokenInput.svelte +12 -6
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -1
- package/docs/components.md +19 -22
- package/docs/usage.md +14 -13
- package/package.json +1 -1
|
@@ -153,7 +153,8 @@ All from `@spaethtech/svelte-ui` (see the shipped `docs/components.md` + `docs/u
|
|
|
153
153
|
examples):
|
|
154
154
|
|
|
155
155
|
- **Form:** `Button` `ButtonDropdown` `Input` `Select` `Combobox` `Slider` `List` `TextArea` `Checkbox`
|
|
156
|
-
`Toggle` `Radio` `Rating`
|
|
156
|
+
`Toggle` `Radio` `Rating` (display `average`/`count`, OR `interactive` + `bind:value` star picker
|
|
157
|
+
with `allowHalf`, click/hover/arrow-keys) · **`Combobox`** (free-text autocomplete — arbitrary value + suggestions,
|
|
157
158
|
client `options` or async `suggest`; vs `Select` which is a closed choice. Composes Input + Popup.) ·
|
|
158
159
|
**`TokenInput`** (multi-value entry — Enter/comma adds a removable `Chip` token, Backspace-on-empty
|
|
159
160
|
removes; `bind:values`, `max`, `allowDuplicates`; FieldChrome.) · **`FileUpload`** (drag-drop + click
|
|
@@ -170,13 +171,15 @@ examples):
|
|
|
170
171
|
- **Specialized inputs:** `PasswordInput` `EmailInput` `SearchInput` `NumberInput` (formatting +
|
|
171
172
|
`percent`/`stepper`/`clamp`/`liveFormat`) `PhoneInput` (stores E.164; dep-free, inject
|
|
172
173
|
`parse`/`format` for per-country)
|
|
173
|
-
- **Date / time:** `DatePicker` `Calendar` `TimePicker` `TimeSpinner` `TimeRangeInput` `DateTimeInput
|
|
174
|
-
|
|
175
|
-
`Calendar mode="range"
|
|
174
|
+
- **Date / time:** `DatePicker` `Calendar` `TimePicker` `TimeSpinner` `TimeRangeInput` `DateTimeInput`.
|
|
175
|
+
For a start–end range use **`DatePicker range`** (`bind:value` = `{ start, end }`) — it composes
|
|
176
|
+
`Calendar mode="range"`.
|
|
176
177
|
- **Data:** `DataTable` `Query` — driven by the headless layer at **`@spaethtech/svelte-ui/data`**
|
|
177
178
|
(query-language parser/AST, `createGrid`, `DataGrid<T>`, `DataSet`). · **`Pagination`** (standalone
|
|
178
|
-
pager
|
|
179
|
-
`
|
|
179
|
+
pager styled like the DataTable footer: tinted band, per-page `Select`, range total, and a
|
|
180
|
+
`First·Prev·[page]/N·Next·Last` stepper whose indicator is an editable `NumberInput` quick-jump;
|
|
181
|
+
`bind:page`/`bind:perPage`, `total`, `perPageOptions`, `showEdges`/`showTotal`, `borderless`;
|
|
182
|
+
`variant` tints band+hover only; built from `Button`+`Select`+`NumberInput`).
|
|
180
183
|
- **Overlays:** `Dialog` (pure modal shell — BYO content; `width`/`height` take any CSS value;
|
|
181
184
|
`onShow(ctx)`/`onHide(ctx)` control focus — set `ctx.autoFocus` to a selector/element/`false`,
|
|
182
185
|
`ctx.restoreFocus` likewise) · `ConfirmDialog` (title + message + Confirm/Cancel, built on Dialog)
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
import Input from "../Input.svelte";
|
|
11
11
|
import Popup from "../Popup.svelte";
|
|
12
12
|
import IconChevronDown from "~icons/mdi/chevron-down";
|
|
13
|
+
import Spinner from "../Spinner/Spinner.svelte";
|
|
13
14
|
import type { Variant } from "../../types/variants.js";
|
|
14
15
|
import type { Size } from "../../types/sizes.js";
|
|
15
|
-
import type
|
|
16
|
+
import { responsiveClasses, type Responsive } from "../../types/responsive.js";
|
|
16
17
|
|
|
17
18
|
let {
|
|
18
19
|
value = $bindable(""),
|
|
@@ -65,6 +66,10 @@
|
|
|
65
66
|
let fieldEl = $state<HTMLDivElement>();
|
|
66
67
|
let inputEl = $state<HTMLInputElement>();
|
|
67
68
|
let asyncResults = $state<string[]>([]);
|
|
69
|
+
let loading = $state(false);
|
|
70
|
+
// Monotonic request id — a later query invalidates the results of any earlier in-flight one, so a
|
|
71
|
+
// slow response can't clobber a newer, faster one (out-of-order race).
|
|
72
|
+
let reqSeq = 0;
|
|
68
73
|
|
|
69
74
|
// Suggestions: async `suggest` (debounced) or client-side substring filter of `options`.
|
|
70
75
|
const filtered = $derived.by(() => {
|
|
@@ -78,23 +83,37 @@
|
|
|
78
83
|
return maxResults != null ? out.slice(0, maxResults) : out;
|
|
79
84
|
});
|
|
80
85
|
|
|
81
|
-
// Debounced async fetch — re-runs as `value` changes; gated by `minChars`.
|
|
86
|
+
// Debounced async fetch — re-runs as `value` changes; gated by `minChars`. Sets `loading` for the
|
|
87
|
+
// whole debounce→resolve window so the field shows feedback (otherwise the delay reads as "broken").
|
|
82
88
|
$effect(() => {
|
|
83
89
|
if (!suggest) return;
|
|
84
90
|
const q = value;
|
|
85
91
|
if (q.trim().length < minChars) {
|
|
86
92
|
asyncResults = [];
|
|
93
|
+
loading = false;
|
|
87
94
|
return;
|
|
88
95
|
}
|
|
96
|
+
const seq = ++reqSeq;
|
|
97
|
+
loading = true;
|
|
89
98
|
const t = setTimeout(() => {
|
|
90
99
|
suggest(q)
|
|
91
|
-
.then((r) =>
|
|
92
|
-
|
|
100
|
+
.then((r) => {
|
|
101
|
+
if (seq !== reqSeq) return; // a newer query superseded this one
|
|
102
|
+
asyncResults = r;
|
|
103
|
+
loading = false;
|
|
104
|
+
})
|
|
105
|
+
.catch(() => {
|
|
106
|
+
if (seq !== reqSeq) return;
|
|
107
|
+
asyncResults = [];
|
|
108
|
+
loading = false;
|
|
109
|
+
});
|
|
93
110
|
}, debounceMs);
|
|
94
111
|
return () => clearTimeout(t);
|
|
95
112
|
});
|
|
96
113
|
|
|
97
|
-
|
|
114
|
+
// Open the popup while loading too, so the "Loading…" row can show during the async fetch.
|
|
115
|
+
const canOpen = $derived(open && !disabled && (filtered.length > 0 || loading));
|
|
116
|
+
const optionText = $derived(responsiveClasses(size, { sm: "text-xs", md: "text-sm", lg: "text-base" }));
|
|
98
117
|
|
|
99
118
|
function choose(v: string) {
|
|
100
119
|
value = v;
|
|
@@ -166,14 +185,20 @@
|
|
|
166
185
|
}}
|
|
167
186
|
>
|
|
168
187
|
{#snippet actions()}
|
|
169
|
-
|
|
170
|
-
class="inline-flex items-center justify-center opacity-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
188
|
+
{#if loading}
|
|
189
|
+
<span class="inline-flex items-center justify-center opacity-70" aria-hidden="true">
|
|
190
|
+
<Spinner size="sm" />
|
|
191
|
+
</span>
|
|
192
|
+
{:else}
|
|
193
|
+
<span
|
|
194
|
+
class="inline-flex items-center justify-center opacity-60 transition-transform duration-150 {canOpen
|
|
195
|
+
? 'rotate-180'
|
|
196
|
+
: ''} [&_svg]:w-4 [&_svg]:h-4"
|
|
197
|
+
aria-hidden="true"
|
|
198
|
+
>
|
|
199
|
+
<IconChevronDown />
|
|
200
|
+
</span>
|
|
201
|
+
{/if}
|
|
177
202
|
{/snippet}
|
|
178
203
|
</Input>
|
|
179
204
|
|
|
@@ -181,25 +206,31 @@
|
|
|
181
206
|
<ul
|
|
182
207
|
id={listId}
|
|
183
208
|
role="listbox"
|
|
184
|
-
class="max-h-64 overflow-y-auto rounded-md border shadow-lg [background-color:var(--ui-color-background)] [color:var(--ui-color-text)] [border-color:var(--ui-border-color)] py-1"
|
|
209
|
+
class="max-h-64 overflow-y-auto rounded-md border shadow-lg [background-color:var(--ui-color-background)] [color:var(--ui-color-text)] [border-color:var(--ui-border-color)] py-1 {optionText}"
|
|
185
210
|
>
|
|
186
|
-
{#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
id={optId(i)}
|
|
190
|
-
role="option"
|
|
191
|
-
aria-selected={i === highlight}
|
|
192
|
-
class="cursor-pointer px-3 py-1.5 text-sm {i === highlight
|
|
193
|
-
? '[background-color:var(--ui-color-hover)]'
|
|
194
|
-
: ''} hover:[background-color:var(--ui-color-hover)]"
|
|
195
|
-
onpointerdown={(e) => {
|
|
196
|
-
e.preventDefault(); // keep input focused so the click lands before blur closes the list
|
|
197
|
-
choose(opt);
|
|
198
|
-
}}
|
|
199
|
-
onpointerenter={() => (highlight = i)}
|
|
200
|
-
>
|
|
201
|
-
{opt}
|
|
211
|
+
{#if filtered.length === 0 && loading}
|
|
212
|
+
<li class="flex items-center gap-2 px-3 py-1.5 [color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]" aria-hidden="true">
|
|
213
|
+
<Spinner size="sm" /> Loading…
|
|
202
214
|
</li>
|
|
203
|
-
{
|
|
215
|
+
{:else}
|
|
216
|
+
{#each filtered as opt, i (opt + i)}
|
|
217
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
218
|
+
<li
|
|
219
|
+
id={optId(i)}
|
|
220
|
+
role="option"
|
|
221
|
+
aria-selected={i === highlight}
|
|
222
|
+
class="cursor-pointer px-3 py-1.5 {i === highlight
|
|
223
|
+
? '[background-color:var(--ui-color-hover)]'
|
|
224
|
+
: ''} hover:[background-color:var(--ui-color-hover)]"
|
|
225
|
+
onpointerdown={(e) => {
|
|
226
|
+
e.preventDefault(); // keep input focused so the click lands before blur closes the list
|
|
227
|
+
choose(opt);
|
|
228
|
+
}}
|
|
229
|
+
onpointerenter={() => (highlight = i)}
|
|
230
|
+
>
|
|
231
|
+
{opt}
|
|
232
|
+
</li>
|
|
233
|
+
{/each}
|
|
234
|
+
{/if}
|
|
204
235
|
</ul>
|
|
205
236
|
</Popup>
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { Snippet } from "svelte";
|
|
2
2
|
import type { Variant } from "../../types/variants.js";
|
|
3
3
|
import type { Size } from "../../types/sizes.js";
|
|
4
|
-
import type
|
|
4
|
+
import { type Responsive } from "../../types/responsive.js";
|
|
5
5
|
type $$ComponentProps = {
|
|
6
6
|
value?: string;
|
|
7
7
|
options?: string[];
|
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
/**
|
|
3
|
-
* Pagination — a standalone page navigator
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Pagination — a standalone page navigator laid out like DataTable's footer: a tinted, bordered band
|
|
4
|
+
* with an optional per-page `Select` on the left and a compact `First · Prev · [page]/N · Next · Last`
|
|
5
|
+
* stepper on the right. The page indicator is an editable `NumberInput` for quick-jump. `variant` tints
|
|
6
|
+
* the band surface + button hover (via the shared `.ui-accent` override) — it does NOT fill buttons.
|
|
7
|
+
* Built only from `Button`, `Select`, and `NumberInput`. See Pagination.spec.md.
|
|
6
8
|
*/
|
|
7
9
|
-->
|
|
8
10
|
<script lang="ts">
|
|
9
11
|
import Button from "../Button.svelte";
|
|
10
12
|
import Select from "../Select.svelte";
|
|
13
|
+
import NumberInput from "../NumberInput.svelte";
|
|
11
14
|
import type { Variant } from "../../types/variants.js";
|
|
15
|
+
import { variantToken } from "../../types/variants.js";
|
|
12
16
|
import type { Size } from "../../types/sizes.js";
|
|
13
17
|
import { responsiveClasses, type Responsive } from "../../types/responsive.js";
|
|
14
18
|
import IconFirst from "~icons/mdi/page-first";
|
|
@@ -21,25 +25,29 @@
|
|
|
21
25
|
total,
|
|
22
26
|
perPage = $bindable(10),
|
|
23
27
|
perPageOptions,
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
showTotal = false,
|
|
28
|
-
variant = "primary",
|
|
28
|
+
showEdges = true,
|
|
29
|
+
showTotal = true,
|
|
30
|
+
variant,
|
|
29
31
|
size = "md",
|
|
32
|
+
borderless = false,
|
|
30
33
|
disabled = false,
|
|
31
34
|
class: cls = "",
|
|
32
35
|
}: {
|
|
33
36
|
page?: number;
|
|
34
37
|
total: number;
|
|
35
38
|
perPage?: number;
|
|
39
|
+
/** When set, renders the per-page `Select` on the left. */
|
|
36
40
|
perPageOptions?: number[];
|
|
37
|
-
|
|
38
|
-
boundaryCount?: number;
|
|
41
|
+
/** Show the First/Last edge buttons (default true, matching DataTable's footer). */
|
|
39
42
|
showEdges?: boolean;
|
|
43
|
+
/** Show the "start–end of total" range text (default true). */
|
|
40
44
|
showTotal?: boolean;
|
|
45
|
+
/** Color axis — tints the band surface + button hover (like DataTable), NOT the buttons. Omit for
|
|
46
|
+
* a neutral surface band; `ghost` makes the band transparent. */
|
|
41
47
|
variant?: Variant;
|
|
42
48
|
size?: Responsive<Size>;
|
|
49
|
+
/** Drop the band border/surface, leaving just the controls. */
|
|
50
|
+
borderless?: boolean;
|
|
43
51
|
disabled?: boolean;
|
|
44
52
|
class?: string;
|
|
45
53
|
} = $props();
|
|
@@ -51,23 +59,10 @@
|
|
|
51
59
|
if (clamped !== page) page = clamped;
|
|
52
60
|
});
|
|
53
61
|
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
set.add(i);
|
|
59
|
-
set.add(pageCount - i + 1);
|
|
60
|
-
}
|
|
61
|
-
for (let i = page - siblingCount; i <= page + siblingCount; i++) set.add(i);
|
|
62
|
-
const sorted = [...set].filter((n) => n >= 1 && n <= pageCount).sort((a, b) => a - b);
|
|
63
|
-
const out: (number | "ellipsis")[] = [];
|
|
64
|
-
let prev = 0;
|
|
65
|
-
for (const n of sorted) {
|
|
66
|
-
if (n - prev > 1) out.push("ellipsis");
|
|
67
|
-
out.push(n);
|
|
68
|
-
prev = n;
|
|
69
|
-
}
|
|
70
|
-
return out;
|
|
62
|
+
// The quick-jump field mirrors `page`; committed on change (Enter/blur), not on every keystroke.
|
|
63
|
+
let pageInput = $state(page);
|
|
64
|
+
$effect(() => {
|
|
65
|
+
pageInput = page;
|
|
71
66
|
});
|
|
72
67
|
|
|
73
68
|
const rangeText = $derived(
|
|
@@ -78,18 +73,38 @@
|
|
|
78
73
|
const subtle = $derived(
|
|
79
74
|
`${responsiveClasses(size, { sm: "text-xs", md: "text-sm", lg: "text-base" })} [color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]`,
|
|
80
75
|
);
|
|
76
|
+
const bandPad = $derived(responsiveClasses(size, { sm: "px-2 py-1.5", md: "px-3 py-2", lg: "px-3.5 py-2.5" }));
|
|
77
|
+
|
|
78
|
+
// Band surface + border: variant re-points --ui-accent so --ui-color-surface/hover re-mix from it
|
|
79
|
+
// (the shared .ui-accent mechanism DataTable's footer uses). `ghost` ⇒ transparent band.
|
|
80
|
+
const accentVar = $derived(variant ? `--ui-accent: var(${variantToken[variant]});` : "");
|
|
81
|
+
const sectionBorder = "color-mix(in srgb, var(--ui-color-secondary) var(--ui-tint-border), transparent)";
|
|
82
|
+
const bandBg = $derived(variant === "ghost" ? "transparent" : "var(--ui-color-surface)");
|
|
83
|
+
const bandBorder = $derived(borderless ? "transparent" : sectionBorder);
|
|
81
84
|
|
|
82
85
|
const go = (to: number) => {
|
|
83
86
|
if (disabled) return;
|
|
84
87
|
page = Math.min(Math.max(to, 1), pageCount);
|
|
88
|
+
pageInput = page;
|
|
85
89
|
};
|
|
86
90
|
const setPerPage = (n: number) => {
|
|
87
91
|
perPage = n;
|
|
88
92
|
page = 1;
|
|
89
93
|
};
|
|
94
|
+
const commitJump = () => {
|
|
95
|
+
if (pageInput == null) {
|
|
96
|
+
pageInput = page;
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
go(pageInput);
|
|
100
|
+
};
|
|
90
101
|
</script>
|
|
91
102
|
|
|
92
|
-
<nav
|
|
103
|
+
<nav
|
|
104
|
+
aria-label="Pagination"
|
|
105
|
+
class="ui-accent flex flex-wrap items-center justify-between gap-3 border rounded-[var(--ui-border-radius)] {bandPad} {cls}"
|
|
106
|
+
style="border-color: {bandBorder}; background-color: {bandBg}; {accentVar}"
|
|
107
|
+
>
|
|
93
108
|
{#if perPageOptions?.length}
|
|
94
109
|
<div class="flex items-center gap-2">
|
|
95
110
|
<Select
|
|
@@ -104,7 +119,7 @@
|
|
|
104
119
|
</div>
|
|
105
120
|
{/if}
|
|
106
121
|
|
|
107
|
-
<div class="flex items-center gap-3">
|
|
122
|
+
<div class="flex items-center gap-3 {perPageOptions?.length ? '' : 'ml-auto'}">
|
|
108
123
|
{#if showTotal}<span class={subtle}>{rangeText}</span>{/if}
|
|
109
124
|
<div class="inline-flex items-center gap-0.5">
|
|
110
125
|
{#snippet navBtn(Icon: typeof IconFirst, to: number, off: boolean, aria: string)}
|
|
@@ -123,22 +138,22 @@
|
|
|
123
138
|
{#if showEdges}{@render navBtn(IconFirst, 1, page <= 1, "First page")}{/if}
|
|
124
139
|
{@render navBtn(IconPrev, page - 1, page <= 1, "Previous page")}
|
|
125
140
|
|
|
126
|
-
|
|
127
|
-
{
|
|
128
|
-
<
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
141
|
+
<div class="flex items-center gap-1.5 px-1 {subtle}">
|
|
142
|
+
<span class="inline-block {responsiveClasses(size, { sm: 'w-11', md: 'w-12', lg: 'w-14' })}">
|
|
143
|
+
<NumberInput
|
|
144
|
+
bind:value={pageInput}
|
|
145
|
+
min={1}
|
|
146
|
+
max={pageCount}
|
|
147
|
+
clamp
|
|
132
148
|
{size}
|
|
133
149
|
{disabled}
|
|
134
|
-
|
|
135
|
-
aria-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
{/each}
|
|
150
|
+
inputClass="text-center px-1"
|
|
151
|
+
aria-label="Page number"
|
|
152
|
+
onchange={commitJump}
|
|
153
|
+
/>
|
|
154
|
+
</span>
|
|
155
|
+
<span aria-hidden="true">/ {pageCount}</span>
|
|
156
|
+
</div>
|
|
142
157
|
|
|
143
158
|
{@render navBtn(IconNext, page + 1, page >= pageCount, "Next page")}
|
|
144
159
|
{#if showEdges}{@render navBtn(IconLast, pageCount, page >= pageCount, "Last page")}{/if}
|
|
@@ -5,13 +5,18 @@ type $$ComponentProps = {
|
|
|
5
5
|
page?: number;
|
|
6
6
|
total: number;
|
|
7
7
|
perPage?: number;
|
|
8
|
+
/** When set, renders the per-page `Select` on the left. */
|
|
8
9
|
perPageOptions?: number[];
|
|
9
|
-
|
|
10
|
-
boundaryCount?: number;
|
|
10
|
+
/** Show the First/Last edge buttons (default true, matching DataTable's footer). */
|
|
11
11
|
showEdges?: boolean;
|
|
12
|
+
/** Show the "start–end of total" range text (default true). */
|
|
12
13
|
showTotal?: boolean;
|
|
14
|
+
/** Color axis — tints the band surface + button hover (like DataTable), NOT the buttons. Omit for
|
|
15
|
+
* a neutral surface band; `ghost` makes the band transparent. */
|
|
13
16
|
variant?: Variant;
|
|
14
17
|
size?: Responsive<Size>;
|
|
18
|
+
/** Drop the band border/surface, leaving just the controls. */
|
|
19
|
+
borderless?: boolean;
|
|
15
20
|
disabled?: boolean;
|
|
16
21
|
class?: string;
|
|
17
22
|
};
|
|
@@ -1,47 +1,168 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
/**
|
|
3
|
+
* Rating — star rating in two modes:
|
|
4
|
+
* • Display (default): a read-only average. `average` is a 0–10 score (TMDB-style), shown as 5 stars
|
|
5
|
+
* with halves, plus an optional `count`.
|
|
6
|
+
* • Interactive (`interactive`): a form control — click / hover / arrow-key a `bind:value` (0..max
|
|
7
|
+
* stars), optional half-steps. `role="slider"` (same a11y pattern as our Slider).
|
|
8
|
+
* See usage docs.
|
|
9
|
+
*/
|
|
10
|
+
-->
|
|
1
11
|
<script lang="ts">
|
|
2
12
|
import { variantToken, type Variant } from "../types/variants.js";
|
|
3
13
|
import type { Size } from "../types/sizes.js";
|
|
4
14
|
|
|
5
15
|
export type RatingProps = {
|
|
6
|
-
|
|
7
|
-
|
|
16
|
+
/** Display mode: a 0–10 average score (TMDB-style), rendered as 5 stars (average / 2). */
|
|
17
|
+
average?: number;
|
|
18
|
+
/** Display mode: number of ratings, shown in parentheses. */
|
|
19
|
+
count?: number;
|
|
20
|
+
/** Interactive mode: the selected rating in stars (0..max), bindable. */
|
|
21
|
+
value?: number;
|
|
22
|
+
/** Enable selection (click / hover / keyboard). Off = read-only display. */
|
|
23
|
+
interactive?: boolean;
|
|
24
|
+
/** Interactive but not editable (shows `value`, no input). */
|
|
25
|
+
readonly?: boolean;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
/** Number of stars in interactive mode (default 5). */
|
|
28
|
+
max?: number;
|
|
29
|
+
/** Allow half-star selection/display (interactive). */
|
|
30
|
+
allowHalf?: boolean;
|
|
31
|
+
/** Show the numeric value beside the stars (default: true in display mode, false interactive). */
|
|
32
|
+
showValue?: boolean;
|
|
33
|
+
/** Fired on change (interactive). */
|
|
34
|
+
onchange?: (value: number) => void;
|
|
8
35
|
/** Filled-star colour. Defaults to gold; a variant tints from its token (e.g. `warning`, `primary`). */
|
|
9
36
|
variant?: Variant;
|
|
10
|
-
/** Star size (sm/md/lg). */
|
|
11
37
|
size?: Size;
|
|
38
|
+
/** Accessible label for the interactive slider. */
|
|
39
|
+
label?: string;
|
|
12
40
|
};
|
|
13
41
|
|
|
14
|
-
let {
|
|
42
|
+
let {
|
|
43
|
+
average,
|
|
44
|
+
count,
|
|
45
|
+
value = $bindable(0),
|
|
46
|
+
interactive = false,
|
|
47
|
+
readonly = false,
|
|
48
|
+
disabled = false,
|
|
49
|
+
max = 5,
|
|
50
|
+
allowHalf = false,
|
|
51
|
+
showValue,
|
|
52
|
+
onchange,
|
|
53
|
+
variant,
|
|
54
|
+
size = "md",
|
|
55
|
+
label = "Rating",
|
|
56
|
+
}: RatingProps = $props();
|
|
15
57
|
|
|
16
58
|
const starColor = $derived(variant ? `var(${variantToken[variant]})` : "#ffd700");
|
|
17
59
|
const starSize: Record<Size, string> = { sm: "1rem", md: "1.2rem", lg: "1.5rem" };
|
|
18
60
|
|
|
19
|
-
|
|
20
|
-
const
|
|
61
|
+
const active = $derived(interactive && !readonly && !disabled);
|
|
62
|
+
const step = $derived(allowHalf ? 0.5 : 1);
|
|
21
63
|
|
|
22
|
-
//
|
|
23
|
-
const
|
|
64
|
+
// Display mode: sanitize the 0–10 average and convert to a 5-point scale.
|
|
65
|
+
const displayFive = $derived.by(() => {
|
|
66
|
+
const a = isNaN(average ?? NaN) || (average ?? 0) < 0 ? 0 : Math.min(average ?? 0, 10);
|
|
67
|
+
return a / 2;
|
|
68
|
+
});
|
|
24
69
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
const
|
|
70
|
+
// Hover preview (interactive) overrides `value` while pointing.
|
|
71
|
+
let hover = $state<number | null>(null);
|
|
72
|
+
|
|
73
|
+
const starCount = $derived(interactive ? max : 5);
|
|
74
|
+
// The numeric value the stars are painted from.
|
|
75
|
+
const paint = $derived(interactive ? (hover ?? value) : displayFive);
|
|
76
|
+
const readout = $derived(interactive ? value : displayFive);
|
|
77
|
+
const wantValue = $derived(showValue ?? !interactive);
|
|
78
|
+
|
|
79
|
+
// Fill state for the i-th star (1-based).
|
|
80
|
+
function fillOf(i: number): "full" | "half" | "empty" {
|
|
81
|
+
if (paint >= i) return "full";
|
|
82
|
+
if (paint >= i - 0.5) return "half";
|
|
83
|
+
return "empty";
|
|
84
|
+
}
|
|
85
|
+
// Half-step from the pointer's position within a star (left half = i-0.5).
|
|
86
|
+
function valueAt(i: number, e: MouseEvent): number {
|
|
87
|
+
if (!allowHalf) return i;
|
|
88
|
+
const r = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
|
89
|
+
return e.clientX - r.left < r.width / 2 ? i - 0.5 : i;
|
|
90
|
+
}
|
|
91
|
+
function pick(i: number, e: MouseEvent) {
|
|
92
|
+
if (!active) return;
|
|
93
|
+
value = valueAt(i, e);
|
|
94
|
+
onchange?.(value);
|
|
95
|
+
}
|
|
96
|
+
function preview(i: number, e: MouseEvent) {
|
|
97
|
+
if (!active) return;
|
|
98
|
+
hover = valueAt(i, e);
|
|
99
|
+
}
|
|
100
|
+
function onkeydown(e: KeyboardEvent) {
|
|
101
|
+
if (!active) return;
|
|
102
|
+
let v = value;
|
|
103
|
+
switch (e.key) {
|
|
104
|
+
case "ArrowRight":
|
|
105
|
+
case "ArrowUp":
|
|
106
|
+
v = Math.min(max, value + step);
|
|
107
|
+
break;
|
|
108
|
+
case "ArrowLeft":
|
|
109
|
+
case "ArrowDown":
|
|
110
|
+
v = Math.max(0, value - step);
|
|
111
|
+
break;
|
|
112
|
+
case "Home":
|
|
113
|
+
v = 0;
|
|
114
|
+
break;
|
|
115
|
+
case "End":
|
|
116
|
+
v = max;
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
e.preventDefault();
|
|
122
|
+
value = v;
|
|
123
|
+
onchange?.(v);
|
|
124
|
+
}
|
|
29
125
|
</script>
|
|
30
126
|
|
|
31
|
-
<div
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
{
|
|
40
|
-
|
|
127
|
+
<div
|
|
128
|
+
class="rating select-none"
|
|
129
|
+
class:disabled
|
|
130
|
+
style="--star-color: {starColor}; --star-size: {starSize[size]}"
|
|
131
|
+
>
|
|
132
|
+
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
133
|
+
<div
|
|
134
|
+
class="stars"
|
|
135
|
+
role={active ? "slider" : undefined}
|
|
136
|
+
tabindex={active ? 0 : undefined}
|
|
137
|
+
aria-label={active ? label : undefined}
|
|
138
|
+
aria-valuemin={active ? 0 : undefined}
|
|
139
|
+
aria-valuemax={active ? max : undefined}
|
|
140
|
+
aria-valuenow={active ? value : undefined}
|
|
141
|
+
aria-valuetext={active ? `${value} of ${max} stars` : undefined}
|
|
142
|
+
aria-readonly={interactive && readonly ? true : undefined}
|
|
143
|
+
aria-disabled={disabled || undefined}
|
|
144
|
+
onkeydown={active ? onkeydown : undefined}
|
|
145
|
+
onpointerleave={active ? () => (hover = null) : undefined}
|
|
146
|
+
>
|
|
147
|
+
{#each Array(starCount) as _, idx (idx)}
|
|
148
|
+
{@const i = idx + 1}
|
|
149
|
+
{@const f = fillOf(i)}
|
|
150
|
+
<!-- svelte-ignore a11y_click_events_have_key_events a11y_no_static_element_interactions -->
|
|
151
|
+
<span
|
|
152
|
+
class="star {f} {active ? 'cursor-pointer' : ''}"
|
|
153
|
+
onpointermove={active ? (e) => preview(i, e) : undefined}
|
|
154
|
+
onclick={active ? (e) => pick(i, e) : undefined}
|
|
155
|
+
>
|
|
156
|
+
{f === "empty" ? "☆" : "★"}
|
|
157
|
+
</span>
|
|
41
158
|
{/each}
|
|
42
159
|
</div>
|
|
43
|
-
|
|
44
|
-
|
|
160
|
+
{#if wantValue}
|
|
161
|
+
<span class="rating-value">{readout.toFixed(1)}</span>
|
|
162
|
+
{/if}
|
|
163
|
+
{#if !interactive && count != null}
|
|
164
|
+
<span class="rating-count">({count})</span>
|
|
165
|
+
{/if}
|
|
45
166
|
</div>
|
|
46
167
|
|
|
47
168
|
<style>
|
|
@@ -50,30 +171,48 @@
|
|
|
50
171
|
align-items: center;
|
|
51
172
|
gap: 0.5rem;
|
|
52
173
|
}
|
|
174
|
+
.rating.disabled {
|
|
175
|
+
opacity: 0.5;
|
|
176
|
+
pointer-events: none;
|
|
177
|
+
}
|
|
53
178
|
|
|
54
179
|
.stars {
|
|
55
180
|
display: flex;
|
|
56
181
|
gap: 0.1rem;
|
|
57
182
|
}
|
|
183
|
+
.stars[role="slider"] {
|
|
184
|
+
border-radius: 4px;
|
|
185
|
+
}
|
|
186
|
+
.stars[role="slider"]:focus-visible {
|
|
187
|
+
outline: 2px solid color-mix(in srgb, var(--ui-color-text) 70%, transparent);
|
|
188
|
+
outline-offset: 2px;
|
|
189
|
+
}
|
|
58
190
|
|
|
59
191
|
.star {
|
|
60
192
|
font-size: var(--star-size, 1.2rem);
|
|
61
193
|
line-height: 1;
|
|
62
194
|
}
|
|
63
195
|
|
|
64
|
-
.star.
|
|
65
|
-
color: var(--star-color, #ffd700);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
.star.half-filled {
|
|
196
|
+
.star.full {
|
|
69
197
|
color: var(--star-color, #ffd700);
|
|
70
|
-
opacity: 0.6;
|
|
71
198
|
}
|
|
72
199
|
|
|
73
200
|
.star.empty {
|
|
74
201
|
color: var(--ui-color-secondary);
|
|
75
202
|
}
|
|
76
203
|
|
|
204
|
+
/* Half star: a full ★ glyph clipped to a left-gold / right-muted gradient. */
|
|
205
|
+
.star.half {
|
|
206
|
+
background: linear-gradient(
|
|
207
|
+
90deg,
|
|
208
|
+
var(--star-color, #ffd700) 50%,
|
|
209
|
+
var(--ui-color-secondary) 50%
|
|
210
|
+
);
|
|
211
|
+
-webkit-background-clip: text;
|
|
212
|
+
background-clip: text;
|
|
213
|
+
color: transparent;
|
|
214
|
+
}
|
|
215
|
+
|
|
77
216
|
.rating-value {
|
|
78
217
|
font-weight: 600;
|
|
79
218
|
color: var(--ui-color-text);
|
|
@@ -1,13 +1,31 @@
|
|
|
1
1
|
import { type Variant } from "../types/variants.js";
|
|
2
2
|
import type { Size } from "../types/sizes.js";
|
|
3
3
|
export type RatingProps = {
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
/** Display mode: a 0–10 average score (TMDB-style), rendered as 5 stars (average / 2). */
|
|
5
|
+
average?: number;
|
|
6
|
+
/** Display mode: number of ratings, shown in parentheses. */
|
|
7
|
+
count?: number;
|
|
8
|
+
/** Interactive mode: the selected rating in stars (0..max), bindable. */
|
|
9
|
+
value?: number;
|
|
10
|
+
/** Enable selection (click / hover / keyboard). Off = read-only display. */
|
|
11
|
+
interactive?: boolean;
|
|
12
|
+
/** Interactive but not editable (shows `value`, no input). */
|
|
13
|
+
readonly?: boolean;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
/** Number of stars in interactive mode (default 5). */
|
|
16
|
+
max?: number;
|
|
17
|
+
/** Allow half-star selection/display (interactive). */
|
|
18
|
+
allowHalf?: boolean;
|
|
19
|
+
/** Show the numeric value beside the stars (default: true in display mode, false interactive). */
|
|
20
|
+
showValue?: boolean;
|
|
21
|
+
/** Fired on change (interactive). */
|
|
22
|
+
onchange?: (value: number) => void;
|
|
6
23
|
/** Filled-star colour. Defaults to gold; a variant tints from its token (e.g. `warning`, `primary`). */
|
|
7
24
|
variant?: Variant;
|
|
8
|
-
/** Star size (sm/md/lg). */
|
|
9
25
|
size?: Size;
|
|
26
|
+
/** Accessible label for the interactive slider. */
|
|
27
|
+
label?: string;
|
|
10
28
|
};
|
|
11
|
-
declare const Rating: import("svelte").Component<RatingProps, {}, "">;
|
|
29
|
+
declare const Rating: import("svelte").Component<RatingProps, {}, "value">;
|
|
12
30
|
type Rating = ReturnType<typeof Rating>;
|
|
13
31
|
export default Rating;
|
|
@@ -166,14 +166,16 @@
|
|
|
166
166
|
|
|
167
167
|
<FieldChrome {label} {aside} {error} {description} {required} {controlId} {size} class={cls}>
|
|
168
168
|
{#snippet control({ describedBy })}
|
|
169
|
-
<div class="flex items-center gap-3">
|
|
169
|
+
<div class="flex items-center gap-3 select-none">
|
|
170
170
|
<!-- The track is the click-to-position surface; the thumbs below carry role="slider". -->
|
|
171
171
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
172
|
+
<!-- Disabled keeps pointer events (so the not-allowed cursor shows) but every handler no-ops on
|
|
173
|
+
`disabled`, so it can't be dragged/keyed. -->
|
|
172
174
|
<div
|
|
173
175
|
bind:this={trackEl}
|
|
174
176
|
onpointerdown={onTrackPointerDown}
|
|
175
177
|
class="relative flex-1 select-none rounded-full {disabled
|
|
176
|
-
? 'opacity-50
|
|
178
|
+
? 'opacity-50 cursor-not-allowed'
|
|
177
179
|
: 'cursor-pointer'} {responsiveClasses(size, trackH)}"
|
|
178
180
|
style="background-color: color-mix(in srgb, var({token}) 16%, transparent);"
|
|
179
181
|
>
|
|
@@ -198,7 +200,7 @@
|
|
|
198
200
|
aria-describedby={describedBy}
|
|
199
201
|
onkeydown={(e) => onThumbKeydown(e, i)}
|
|
200
202
|
class="absolute top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 shadow {disabled
|
|
201
|
-
? ''
|
|
203
|
+
? 'cursor-not-allowed'
|
|
202
204
|
: 'cursor-grab active:cursor-grabbing'} focus-visible:[outline:2px_solid_color-mix(in_srgb,var(--ui-color-text)_70%,transparent)] focus-visible:[outline-offset:2px] {responsiveClasses(
|
|
203
205
|
size,
|
|
204
206
|
thumbSz,
|
|
@@ -62,14 +62,19 @@
|
|
|
62
62
|
: `color-mix(in srgb, var(${token}) var(--ui-tint-border-hover), transparent)`,
|
|
63
63
|
);
|
|
64
64
|
|
|
65
|
+
// Vertical padding is kept small enough that a single token row (chip + 1px border box + padding)
|
|
66
|
+
// never exceeds the tier's `min-h`, so the field height is CONSTANT whether empty or holding tokens.
|
|
67
|
+
// Multi-row wrapping still grows the field; row spacing comes from the flex `gap`, not padding.
|
|
65
68
|
const fieldSize: Record<Size, string> = {
|
|
66
|
-
sm: "min-h-6 px-1.5 py-0
|
|
67
|
-
md: "min-h-8 px-2 py-
|
|
68
|
-
lg: "min-h-10 px-2.5 py-
|
|
69
|
+
sm: "min-h-6 px-1.5 py-0 gap-1 text-xs",
|
|
70
|
+
md: "min-h-8 px-2 py-[2px] gap-1.5 text-sm",
|
|
71
|
+
lg: "min-h-10 px-2.5 py-[2px] gap-2 text-base",
|
|
69
72
|
};
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
+
const baseSize = $derived<Size>(typeof size === "string" ? size : "md");
|
|
74
|
+
const chipSize = $derived<Size>(baseSize === "lg" ? "md" : "sm");
|
|
75
|
+
// A default `sm` chip is 24px — exactly the sm FIELD height, so it can't fit inside the border box.
|
|
76
|
+
// Only in the sm field, cap the chip to 20px (still fits the min-h with room to center).
|
|
77
|
+
const chipHeightClass = $derived(baseSize === "sm" ? "!h-5" : "");
|
|
73
78
|
|
|
74
79
|
let draft = $state("");
|
|
75
80
|
|
|
@@ -115,6 +120,7 @@
|
|
|
115
120
|
text={v}
|
|
116
121
|
variant={variant === "ghost" ? "secondary" : variant}
|
|
117
122
|
size={chipSize}
|
|
123
|
+
class={chipHeightClass}
|
|
118
124
|
removable
|
|
119
125
|
{disabled}
|
|
120
126
|
onremove={() => removeAt(i)}
|
package/dist/index.d.ts
CHANGED
|
@@ -43,9 +43,7 @@ export { default as DatePicker } from "./components/DatePicker.svelte";
|
|
|
43
43
|
export { default as TimeSpinner } from "./components/TimeSpinner.svelte";
|
|
44
44
|
export { default as TimePicker } from "./components/TimePicker.svelte";
|
|
45
45
|
export { default as TimeRangeInput } from "./components/TimeRangeInput.svelte";
|
|
46
|
-
export { DateRangePicker } from "./components/DateRangePicker/index.js";
|
|
47
46
|
export { FileUpload } from "./components/FileUpload/index.js";
|
|
48
|
-
export type { RangePreset } from "./components/DateRangePicker/index.js";
|
|
49
47
|
export { default as ThemeSelector } from "./components/ThemeSelector.svelte";
|
|
50
48
|
export { default as ThemeToggle } from "./components/ThemeToggle.svelte";
|
|
51
49
|
export { default as Popup } from "./components/Popup.svelte";
|
package/dist/index.js
CHANGED
|
@@ -44,7 +44,6 @@ export { default as DatePicker } from "./components/DatePicker.svelte";
|
|
|
44
44
|
export { default as TimeSpinner } from "./components/TimeSpinner.svelte";
|
|
45
45
|
export { default as TimePicker } from "./components/TimePicker.svelte";
|
|
46
46
|
export { default as TimeRangeInput } from "./components/TimeRangeInput.svelte";
|
|
47
|
-
export { DateRangePicker } from "./components/DateRangePicker/index.js";
|
|
48
47
|
export { FileUpload } from "./components/FileUpload/index.js";
|
|
49
48
|
// Theme Components
|
|
50
49
|
export { default as ThemeSelector } from "./components/ThemeSelector.svelte";
|
package/docs/components.md
CHANGED
|
@@ -122,20 +122,6 @@ Drag-and-drop + click file field with thumbnails, remove, and optional progress.
|
|
|
122
122
|
`<input type=file>`; labelled remove ×s
|
|
123
123
|
- **Composition**: `Button` + `Progress`; FieldChrome for label/error
|
|
124
124
|
|
|
125
|
-
### DateRangePicker
|
|
126
|
-
|
|
127
|
-
Start–end date range with a presets rail; composes `Calendar mode="range"` + `Input` + `Popup`.
|
|
128
|
-
|
|
129
|
-
- **Location**: `src/lib/components/DateRangePicker/DateRangePicker.svelte`
|
|
130
|
-
- **Axes**: `variant`, `size`
|
|
131
|
-
- **Props**: `start`/`end` (bindable ISO), `min`/`max`, `presets` (`RangePreset[]`; default set, `[]`
|
|
132
|
-
hides the rail), `format`, `placeholder`, plus FieldChrome (`label`/`aside`/`error`/`description`/
|
|
133
|
-
`id`/`required`), `class`
|
|
134
|
-
- **Behavior**: readonly `Input` trigger shows the formatted range; the popup pairs a presets column
|
|
135
|
-
(`Button` ghost) with the range calendar; a preset or the second calendar pick closes it
|
|
136
|
-
- **Note**: `DatePicker range` covers a bare range (object value, no presets); this is the
|
|
137
|
-
presets-first, `bind:start`/`bind:end` variant
|
|
138
|
-
|
|
139
125
|
### Slider
|
|
140
126
|
|
|
141
127
|
Draggable range input — a single value or a `[min, max]` pair.
|
|
@@ -285,10 +271,15 @@ independent. Propagates `size`/`variant`/`disabled` to children.
|
|
|
285
271
|
|
|
286
272
|
### Rating
|
|
287
273
|
|
|
288
|
-
|
|
274
|
+
Star rating in two modes: a read-only display, or an interactive picker.
|
|
289
275
|
|
|
290
276
|
- **Location**: `src/lib/components/Rating.svelte`
|
|
291
|
-
- **
|
|
277
|
+
- **Display mode** (default): `average` (0–10 score, shown as 5 stars = average/2), `count` (votes).
|
|
278
|
+
- **Interactive mode**: `interactive` + `value` (bindable, 0..`max`). Click a star, hover to preview,
|
|
279
|
+
or focus and use ←/→ · ↑/↓ · Home/End. `allowHalf` enables half-star selection; `onchange`,
|
|
280
|
+
`readonly`, `disabled`. Rendered as a `role="slider"` (same a11y pattern as `Slider`).
|
|
281
|
+
- **Shared**: `variant` (filled-star tint; default gold), `size`, `showValue` (numeric readout —
|
|
282
|
+
defaults on in display mode, off interactive), `label` (slider a11y label).
|
|
292
283
|
|
|
293
284
|
## Specialized Input Components
|
|
294
285
|
|
|
@@ -399,13 +390,19 @@ dialog (operators, casts, dynamic `now()` dates, and a worked examples table). T
|
|
|
399
390
|
Standalone page navigator — the reusable extraction of `DataTable`'s footer pager.
|
|
400
391
|
|
|
401
392
|
- **Location**: `src/lib/components/Pagination/Pagination.svelte`
|
|
402
|
-
- **Axes**: `variant` (
|
|
393
|
+
- **Axes**: `variant` (tints the band **surface + button hover**, like DataTable's footer — NOT a
|
|
394
|
+
filled button; omit for a neutral band, `ghost` for a transparent one), `size` (scales band, Select,
|
|
395
|
+
buttons, and the page field)
|
|
403
396
|
- **Props**: `page` (bindable), `total`, `perPage` (bindable), `perPageOptions` (→ rows-per-page
|
|
404
|
-
`Select`), `
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
397
|
+
`Select` on the left), `showEdges` (first/last, default **true**), `showTotal` (range text, default
|
|
398
|
+
**true**), `borderless` (drop the band border/surface), `disabled`, `class`
|
|
399
|
+
- **Layout**: a tinted, bordered band (matches `DataTable`'s footer) — per-page `Select` left; range
|
|
400
|
+
text + a compact `First · Prev · [page]/N · Next · Last` stepper right. The page indicator is an
|
|
401
|
+
**editable `NumberInput`** (min 1, max N, clamped) — type a page + Enter to jump; ↑/↓ step it.
|
|
402
|
+
- **Behavior**: prev/next/first/last disable at the ends; choosing `perPage` resets to page 1; `page`
|
|
403
|
+
clamps to range; jump commits on change (Enter/blur)
|
|
404
|
+
- **Accessibility**: `<nav aria-label="Pagination">`; the jump field is `aria-label="Page number"`
|
|
405
|
+
- **Composition**: `Button` + `Select` + `NumberInput` only (no raw `<button>`/`<input>`)
|
|
409
406
|
|
|
410
407
|
## Overlays
|
|
411
408
|
|
package/docs/usage.md
CHANGED
|
@@ -311,21 +311,17 @@ A drag-and-drop + click file field (thumbnails, remove, optional progress).
|
|
|
311
311
|
label="Attachments" onfiles={(f) => console.log(f)} />
|
|
312
312
|
```
|
|
313
313
|
|
|
314
|
-
###
|
|
314
|
+
### Date range
|
|
315
315
|
|
|
316
|
-
|
|
316
|
+
Use `DatePicker` with the `range` prop — its value is a `{ start, end }` object.
|
|
317
317
|
|
|
318
318
|
```svelte
|
|
319
319
|
<script>
|
|
320
|
-
import {
|
|
321
|
-
let
|
|
322
|
-
let end = $state("");
|
|
320
|
+
import { DatePicker } from "@spaethtech/svelte-ui";
|
|
321
|
+
let range = $state({ start: null, end: null });
|
|
323
322
|
</script>
|
|
324
323
|
|
|
325
|
-
<
|
|
326
|
-
|
|
327
|
-
<!-- custom presets, or presets={[]} for calendar-only -->
|
|
328
|
-
<DateRangePicker bind:start bind:end presets={[{ label: "Q1", start: "2026-01-01", end: "2026-03-31" }]} />
|
|
324
|
+
<DatePicker range bind:value={range} label="Reporting period" />
|
|
329
325
|
```
|
|
330
326
|
|
|
331
327
|
### Stepper
|
|
@@ -740,7 +736,9 @@ A thin separating rule — horizontal (optionally labelled) or vertical (toolbar
|
|
|
740
736
|
|
|
741
737
|
### Pagination
|
|
742
738
|
|
|
743
|
-
A standalone page navigator
|
|
739
|
+
A standalone page navigator laid out like `DataTable`'s footer: a tinted band with an optional
|
|
740
|
+
per-page `Select`, a range total, and a `First · Prev · [page]/N · Next · Last` stepper whose page
|
|
741
|
+
indicator is an editable `NumberInput` (type a page + Enter to jump).
|
|
744
742
|
|
|
745
743
|
```svelte
|
|
746
744
|
<script>
|
|
@@ -749,10 +747,13 @@ A standalone page navigator (numbered buttons + ellipsis, prev/next, optional pe
|
|
|
749
747
|
let perPage = $state(10);
|
|
750
748
|
</script>
|
|
751
749
|
|
|
752
|
-
<Pagination bind:page bind:perPage total={247} perPageOptions={[10, 25, 50, 100]}
|
|
750
|
+
<Pagination bind:page bind:perPage total={247} perPageOptions={[10, 25, 50, 100]} />
|
|
751
|
+
|
|
752
|
+
<!-- Compact: just the stepper (no per-page Select) -->
|
|
753
|
+
<Pagination bind:page total={200} showTotal={false} />
|
|
753
754
|
|
|
754
|
-
<!--
|
|
755
|
-
<Pagination bind:page total={200}
|
|
755
|
+
<!-- variant tints the band surface + button hover (not the buttons); ghost = transparent band -->
|
|
756
|
+
<Pagination bind:page total={200} variant="primary" />
|
|
756
757
|
```
|
|
757
758
|
|
|
758
759
|
### Breadcrumbs
|