@spaethtech/svelte-ui 0.15.0 → 0.15.1-dev.67.fbf3875
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 +6 -9
- package/dist/components/Combobox/Combobox.svelte +31 -62
- package/dist/components/Combobox/Combobox.svelte.d.ts +1 -1
- package/dist/components/Pagination/Pagination.svelte +43 -58
- package/dist/components/Pagination/Pagination.svelte.d.ts +2 -7
- package/dist/components/Rating.svelte +30 -169
- package/dist/components/Rating.svelte.d.ts +4 -22
- package/dist/components/Slider/Slider.svelte +3 -5
- package/dist/components/TokenInput/TokenInput.svelte +6 -12
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/docs/components.md +22 -19
- package/docs/usage.md +13 -14
- package/package.json +1 -1
|
@@ -153,8 +153,7 @@ 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` (
|
|
157
|
-
with `allowHalf`, click/hover/arrow-keys) · **`Combobox`** (free-text autocomplete — arbitrary value + suggestions,
|
|
156
|
+
`Toggle` `Radio` `Rating` · **`Combobox`** (free-text autocomplete — arbitrary value + suggestions,
|
|
158
157
|
client `options` or async `suggest`; vs `Select` which is a closed choice. Composes Input + Popup.) ·
|
|
159
158
|
**`TokenInput`** (multi-value entry — Enter/comma adds a removable `Chip` token, Backspace-on-empty
|
|
160
159
|
removes; `bind:values`, `max`, `allowDuplicates`; FieldChrome.) · **`FileUpload`** (drag-drop + click
|
|
@@ -171,15 +170,13 @@ examples):
|
|
|
171
170
|
- **Specialized inputs:** `PasswordInput` `EmailInput` `SearchInput` `NumberInput` (formatting +
|
|
172
171
|
`percent`/`stepper`/`clamp`/`liveFormat`) `PhoneInput` (stores E.164; dep-free, inject
|
|
173
172
|
`parse`/`format` for per-country)
|
|
174
|
-
- **Date / time:** `DatePicker` `Calendar` `TimePicker` `TimeSpinner` `TimeRangeInput` `DateTimeInput
|
|
175
|
-
|
|
176
|
-
`Calendar mode="range"`.
|
|
173
|
+
- **Date / time:** `DatePicker` `Calendar` `TimePicker` `TimeSpinner` `TimeRangeInput` `DateTimeInput`
|
|
174
|
+
· **`DateRangePicker`** (start–end range + presets rail; `bind:start`/`bind:end` ISO; composes
|
|
175
|
+
`Calendar mode="range"` + `Input` + `Popup`. `DatePicker range` is the bare, preset-less variant.)
|
|
177
176
|
- **Data:** `DataTable` `Query` — driven by the headless layer at **`@spaethtech/svelte-ui/data`**
|
|
178
177
|
(query-language parser/AST, `createGrid`, `DataGrid<T>`, `DataSet`). · **`Pagination`** (standalone
|
|
179
|
-
pager
|
|
180
|
-
`
|
|
181
|
-
`bind:page`/`bind:perPage`, `total`, `perPageOptions`, `showEdges`/`showTotal`, `borderless`;
|
|
182
|
-
`variant` tints band+hover only; built from `Button`+`Select`+`NumberInput`).
|
|
178
|
+
pager: numbered buttons + ellipsis, prev/next/first/last, `bind:page`/`bind:perPage`, `total`,
|
|
179
|
+
`perPageOptions`, `showTotal`; built from `Button`+`Select` — the reusable DataTable footer pager).
|
|
183
180
|
- **Overlays:** `Dialog` (pure modal shell — BYO content; `width`/`height` take any CSS value;
|
|
184
181
|
`onShow(ctx)`/`onHide(ctx)` control focus — set `ctx.autoFocus` to a selector/element/`false`,
|
|
185
182
|
`ctx.restoreFocus` likewise) · `ConfirmDialog` (title + message + Confirm/Cancel, built on Dialog)
|
|
@@ -10,10 +10,9 @@
|
|
|
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";
|
|
14
13
|
import type { Variant } from "../../types/variants.js";
|
|
15
14
|
import type { Size } from "../../types/sizes.js";
|
|
16
|
-
import {
|
|
15
|
+
import type { Responsive } from "../../types/responsive.js";
|
|
17
16
|
|
|
18
17
|
let {
|
|
19
18
|
value = $bindable(""),
|
|
@@ -66,10 +65,6 @@
|
|
|
66
65
|
let fieldEl = $state<HTMLDivElement>();
|
|
67
66
|
let inputEl = $state<HTMLInputElement>();
|
|
68
67
|
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;
|
|
73
68
|
|
|
74
69
|
// Suggestions: async `suggest` (debounced) or client-side substring filter of `options`.
|
|
75
70
|
const filtered = $derived.by(() => {
|
|
@@ -83,37 +78,23 @@
|
|
|
83
78
|
return maxResults != null ? out.slice(0, maxResults) : out;
|
|
84
79
|
});
|
|
85
80
|
|
|
86
|
-
// Debounced async fetch — re-runs as `value` changes; gated by `minChars`.
|
|
87
|
-
// whole debounce→resolve window so the field shows feedback (otherwise the delay reads as "broken").
|
|
81
|
+
// Debounced async fetch — re-runs as `value` changes; gated by `minChars`.
|
|
88
82
|
$effect(() => {
|
|
89
83
|
if (!suggest) return;
|
|
90
84
|
const q = value;
|
|
91
85
|
if (q.trim().length < minChars) {
|
|
92
86
|
asyncResults = [];
|
|
93
|
-
loading = false;
|
|
94
87
|
return;
|
|
95
88
|
}
|
|
96
|
-
const seq = ++reqSeq;
|
|
97
|
-
loading = true;
|
|
98
89
|
const t = setTimeout(() => {
|
|
99
90
|
suggest(q)
|
|
100
|
-
.then((r) =>
|
|
101
|
-
|
|
102
|
-
asyncResults = r;
|
|
103
|
-
loading = false;
|
|
104
|
-
})
|
|
105
|
-
.catch(() => {
|
|
106
|
-
if (seq !== reqSeq) return;
|
|
107
|
-
asyncResults = [];
|
|
108
|
-
loading = false;
|
|
109
|
-
});
|
|
91
|
+
.then((r) => (asyncResults = r))
|
|
92
|
+
.catch(() => (asyncResults = []));
|
|
110
93
|
}, debounceMs);
|
|
111
94
|
return () => clearTimeout(t);
|
|
112
95
|
});
|
|
113
96
|
|
|
114
|
-
|
|
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" }));
|
|
97
|
+
const canOpen = $derived(open && !disabled && filtered.length > 0);
|
|
117
98
|
|
|
118
99
|
function choose(v: string) {
|
|
119
100
|
value = v;
|
|
@@ -185,20 +166,14 @@
|
|
|
185
166
|
}}
|
|
186
167
|
>
|
|
187
168
|
{#snippet actions()}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
: ''} [&_svg]:w-4 [&_svg]:h-4"
|
|
197
|
-
aria-hidden="true"
|
|
198
|
-
>
|
|
199
|
-
<IconChevronDown />
|
|
200
|
-
</span>
|
|
201
|
-
{/if}
|
|
169
|
+
<span
|
|
170
|
+
class="inline-flex items-center justify-center opacity-60 transition-transform duration-150 {canOpen
|
|
171
|
+
? 'rotate-180'
|
|
172
|
+
: ''} [&_svg]:w-4 [&_svg]:h-4"
|
|
173
|
+
aria-hidden="true"
|
|
174
|
+
>
|
|
175
|
+
<IconChevronDown />
|
|
176
|
+
</span>
|
|
202
177
|
{/snippet}
|
|
203
178
|
</Input>
|
|
204
179
|
|
|
@@ -206,31 +181,25 @@
|
|
|
206
181
|
<ul
|
|
207
182
|
id={listId}
|
|
208
183
|
role="listbox"
|
|
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
|
|
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"
|
|
210
185
|
>
|
|
211
|
-
{#
|
|
212
|
-
|
|
213
|
-
|
|
186
|
+
{#each filtered as opt, i (opt + i)}
|
|
187
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
188
|
+
<li
|
|
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}
|
|
214
202
|
</li>
|
|
215
|
-
{
|
|
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}
|
|
203
|
+
{/each}
|
|
235
204
|
</ul>
|
|
236
205
|
</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 {
|
|
4
|
+
import type { Responsive } from "../../types/responsive.js";
|
|
5
5
|
type $$ComponentProps = {
|
|
6
6
|
value?: string;
|
|
7
7
|
options?: string[];
|
|
@@ -1,18 +1,14 @@
|
|
|
1
1
|
<!--
|
|
2
2
|
/**
|
|
3
|
-
* Pagination — a standalone page navigator
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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.
|
|
3
|
+
* Pagination — a standalone page navigator (numbered buttons + ellipsis, prev/next, optional
|
|
4
|
+
* first/last, optional rows-per-page Select + total). The reusable extraction of DataTable's footer
|
|
5
|
+
* pager; built only from `Button` (ghost, active = filled variant) and `Select`. See Pagination.spec.md.
|
|
8
6
|
*/
|
|
9
7
|
-->
|
|
10
8
|
<script lang="ts">
|
|
11
9
|
import Button from "../Button.svelte";
|
|
12
10
|
import Select from "../Select.svelte";
|
|
13
|
-
import NumberInput from "../NumberInput.svelte";
|
|
14
11
|
import type { Variant } from "../../types/variants.js";
|
|
15
|
-
import { variantToken } from "../../types/variants.js";
|
|
16
12
|
import type { Size } from "../../types/sizes.js";
|
|
17
13
|
import { responsiveClasses, type Responsive } from "../../types/responsive.js";
|
|
18
14
|
import IconFirst from "~icons/mdi/page-first";
|
|
@@ -25,29 +21,25 @@
|
|
|
25
21
|
total,
|
|
26
22
|
perPage = $bindable(10),
|
|
27
23
|
perPageOptions,
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
24
|
+
siblingCount = 1,
|
|
25
|
+
boundaryCount = 1,
|
|
26
|
+
showEdges = false,
|
|
27
|
+
showTotal = false,
|
|
28
|
+
variant = "primary",
|
|
31
29
|
size = "md",
|
|
32
|
-
borderless = false,
|
|
33
30
|
disabled = false,
|
|
34
31
|
class: cls = "",
|
|
35
32
|
}: {
|
|
36
33
|
page?: number;
|
|
37
34
|
total: number;
|
|
38
35
|
perPage?: number;
|
|
39
|
-
/** When set, renders the per-page `Select` on the left. */
|
|
40
36
|
perPageOptions?: number[];
|
|
41
|
-
|
|
37
|
+
siblingCount?: number;
|
|
38
|
+
boundaryCount?: number;
|
|
42
39
|
showEdges?: boolean;
|
|
43
|
-
/** Show the "start–end of total" range text (default true). */
|
|
44
40
|
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. */
|
|
47
41
|
variant?: Variant;
|
|
48
42
|
size?: Responsive<Size>;
|
|
49
|
-
/** Drop the band border/surface, leaving just the controls. */
|
|
50
|
-
borderless?: boolean;
|
|
51
43
|
disabled?: boolean;
|
|
52
44
|
class?: string;
|
|
53
45
|
} = $props();
|
|
@@ -59,10 +51,23 @@
|
|
|
59
51
|
if (clamped !== page) page = clamped;
|
|
60
52
|
});
|
|
61
53
|
|
|
62
|
-
//
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
54
|
+
// Page-number list with ellipsis gaps: boundaryCount at each end + siblingCount around the current.
|
|
55
|
+
const items = $derived.by<(number | "ellipsis")[]>(() => {
|
|
56
|
+
const set = new Set<number>();
|
|
57
|
+
for (let i = 1; i <= boundaryCount; i++) {
|
|
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;
|
|
66
71
|
});
|
|
67
72
|
|
|
68
73
|
const rangeText = $derived(
|
|
@@ -73,38 +78,18 @@
|
|
|
73
78
|
const subtle = $derived(
|
|
74
79
|
`${responsiveClasses(size, { sm: "text-xs", md: "text-sm", lg: "text-base" })} [color:color-mix(in_srgb,var(--ui-color-text)_60%,transparent)]`,
|
|
75
80
|
);
|
|
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);
|
|
84
81
|
|
|
85
82
|
const go = (to: number) => {
|
|
86
83
|
if (disabled) return;
|
|
87
84
|
page = Math.min(Math.max(to, 1), pageCount);
|
|
88
|
-
pageInput = page;
|
|
89
85
|
};
|
|
90
86
|
const setPerPage = (n: number) => {
|
|
91
87
|
perPage = n;
|
|
92
88
|
page = 1;
|
|
93
89
|
};
|
|
94
|
-
const commitJump = () => {
|
|
95
|
-
if (pageInput == null) {
|
|
96
|
-
pageInput = page;
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
go(pageInput);
|
|
100
|
-
};
|
|
101
90
|
</script>
|
|
102
91
|
|
|
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
|
-
>
|
|
92
|
+
<nav aria-label="Pagination" class="flex flex-wrap items-center justify-between gap-3 {cls}">
|
|
108
93
|
{#if perPageOptions?.length}
|
|
109
94
|
<div class="flex items-center gap-2">
|
|
110
95
|
<Select
|
|
@@ -119,7 +104,7 @@
|
|
|
119
104
|
</div>
|
|
120
105
|
{/if}
|
|
121
106
|
|
|
122
|
-
<div class="flex items-center gap-3
|
|
107
|
+
<div class="flex items-center gap-3">
|
|
123
108
|
{#if showTotal}<span class={subtle}>{rangeText}</span>{/if}
|
|
124
109
|
<div class="inline-flex items-center gap-0.5">
|
|
125
110
|
{#snippet navBtn(Icon: typeof IconFirst, to: number, off: boolean, aria: string)}
|
|
@@ -138,22 +123,22 @@
|
|
|
138
123
|
{#if showEdges}{@render navBtn(IconFirst, 1, page <= 1, "First page")}{/if}
|
|
139
124
|
{@render navBtn(IconPrev, page - 1, page <= 1, "Previous page")}
|
|
140
125
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
<
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
clamp
|
|
126
|
+
{#each items as it, i (i)}
|
|
127
|
+
{#if it === "ellipsis"}
|
|
128
|
+
<span aria-hidden="true" class="px-1.5 {subtle}">…</span>
|
|
129
|
+
{:else}
|
|
130
|
+
<Button
|
|
131
|
+
variant={it === page ? variant : "ghost"}
|
|
148
132
|
{size}
|
|
149
133
|
{disabled}
|
|
150
|
-
|
|
151
|
-
aria-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
134
|
+
aria-label={`Page ${it}`}
|
|
135
|
+
aria-current={it === page ? "page" : undefined}
|
|
136
|
+
onclick={() => go(it)}
|
|
137
|
+
>
|
|
138
|
+
{it}
|
|
139
|
+
</Button>
|
|
140
|
+
{/if}
|
|
141
|
+
{/each}
|
|
157
142
|
|
|
158
143
|
{@render navBtn(IconNext, page + 1, page >= pageCount, "Next page")}
|
|
159
144
|
{#if showEdges}{@render navBtn(IconLast, pageCount, page >= pageCount, "Last page")}{/if}
|
|
@@ -5,18 +5,13 @@ 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. */
|
|
9
8
|
perPageOptions?: number[];
|
|
10
|
-
|
|
9
|
+
siblingCount?: number;
|
|
10
|
+
boundaryCount?: number;
|
|
11
11
|
showEdges?: boolean;
|
|
12
|
-
/** Show the "start–end of total" range text (default true). */
|
|
13
12
|
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. */
|
|
16
13
|
variant?: Variant;
|
|
17
14
|
size?: Responsive<Size>;
|
|
18
|
-
/** Drop the band border/surface, leaving just the controls. */
|
|
19
|
-
borderless?: boolean;
|
|
20
15
|
disabled?: boolean;
|
|
21
16
|
class?: string;
|
|
22
17
|
};
|
|
@@ -1,168 +1,47 @@
|
|
|
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
|
-
-->
|
|
11
1
|
<script lang="ts">
|
|
12
2
|
import { variantToken, type Variant } from "../types/variants.js";
|
|
13
3
|
import type { Size } from "../types/sizes.js";
|
|
14
4
|
|
|
15
5
|
export type RatingProps = {
|
|
16
|
-
|
|
17
|
-
|
|
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;
|
|
6
|
+
average: number;
|
|
7
|
+
count: number;
|
|
35
8
|
/** Filled-star colour. Defaults to gold; a variant tints from its token (e.g. `warning`, `primary`). */
|
|
36
9
|
variant?: Variant;
|
|
10
|
+
/** Star size (sm/md/lg). */
|
|
37
11
|
size?: Size;
|
|
38
|
-
/** Accessible label for the interactive slider. */
|
|
39
|
-
label?: string;
|
|
40
12
|
};
|
|
41
13
|
|
|
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();
|
|
14
|
+
let { average, count, variant, size = "md" }: RatingProps = $props();
|
|
57
15
|
|
|
58
16
|
const starColor = $derived(variant ? `var(${variantToken[variant]})` : "#ffd700");
|
|
59
17
|
const starSize: Record<Size, string> = { sm: "1rem", md: "1.2rem", lg: "1.5rem" };
|
|
60
18
|
|
|
61
|
-
|
|
62
|
-
const
|
|
19
|
+
// Validate and sanitize the average rating
|
|
20
|
+
const validAverage = isNaN(average) || average < 0 ? 0 : Math.min(average, 10);
|
|
63
21
|
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
const a = isNaN(average ?? NaN) || (average ?? 0) < 0 ? 0 : Math.min(average ?? 0, 10);
|
|
67
|
-
return a / 2;
|
|
68
|
-
});
|
|
22
|
+
// Convert TMDB 10-point scale to 5-star scale
|
|
23
|
+
const fiveStarRating = validAverage / 2;
|
|
69
24
|
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
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
|
-
}
|
|
25
|
+
// Calculate how many stars should be filled (ensure they're valid numbers)
|
|
26
|
+
const filledStars = Math.max(0, Math.min(5, Math.floor(fiveStarRating)));
|
|
27
|
+
const hasHalfStar = fiveStarRating % 1 >= 0.5;
|
|
28
|
+
const emptyStars = Math.max(0, 5 - filledStars - (hasHalfStar ? 1 : 0));
|
|
125
29
|
</script>
|
|
126
30
|
|
|
127
|
-
<div
|
|
128
|
-
class="
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
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>
|
|
31
|
+
<div class="rating select-none" style="--star-color: {starColor}; --star-size: {starSize[size]}">
|
|
32
|
+
<div class="stars">
|
|
33
|
+
{#each Array(filledStars) as _, i}
|
|
34
|
+
<span class="star filled">★</span>
|
|
35
|
+
{/each}
|
|
36
|
+
{#if hasHalfStar}
|
|
37
|
+
<span class="star half-filled">☆</span>
|
|
38
|
+
{/if}
|
|
39
|
+
{#each Array(emptyStars) as _, i}
|
|
40
|
+
<span class="star empty">☆</span>
|
|
158
41
|
{/each}
|
|
159
42
|
</div>
|
|
160
|
-
{
|
|
161
|
-
|
|
162
|
-
{/if}
|
|
163
|
-
{#if !interactive && count != null}
|
|
164
|
-
<span class="rating-count">({count})</span>
|
|
165
|
-
{/if}
|
|
43
|
+
<span class="rating-value">{fiveStarRating.toFixed(1)}</span>
|
|
44
|
+
<span class="rating-count">({count})</span>
|
|
166
45
|
</div>
|
|
167
46
|
|
|
168
47
|
<style>
|
|
@@ -171,46 +50,28 @@
|
|
|
171
50
|
align-items: center;
|
|
172
51
|
gap: 0.5rem;
|
|
173
52
|
}
|
|
174
|
-
.rating.disabled {
|
|
175
|
-
opacity: 0.5;
|
|
176
|
-
pointer-events: none;
|
|
177
|
-
}
|
|
178
53
|
|
|
179
54
|
.stars {
|
|
180
55
|
display: flex;
|
|
181
56
|
gap: 0.1rem;
|
|
182
57
|
}
|
|
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
|
-
}
|
|
190
58
|
|
|
191
59
|
.star {
|
|
192
60
|
font-size: var(--star-size, 1.2rem);
|
|
193
61
|
line-height: 1;
|
|
194
62
|
}
|
|
195
63
|
|
|
196
|
-
.star.
|
|
64
|
+
.star.filled {
|
|
197
65
|
color: var(--star-color, #ffd700);
|
|
198
66
|
}
|
|
199
67
|
|
|
200
|
-
.star.
|
|
201
|
-
color: var(--
|
|
68
|
+
.star.half-filled {
|
|
69
|
+
color: var(--star-color, #ffd700);
|
|
70
|
+
opacity: 0.6;
|
|
202
71
|
}
|
|
203
72
|
|
|
204
|
-
|
|
205
|
-
|
|
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;
|
|
73
|
+
.star.empty {
|
|
74
|
+
color: var(--ui-color-secondary);
|
|
214
75
|
}
|
|
215
76
|
|
|
216
77
|
.rating-value {
|
|
@@ -1,31 +1,13 @@
|
|
|
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
|
-
|
|
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;
|
|
4
|
+
average: number;
|
|
5
|
+
count: number;
|
|
23
6
|
/** Filled-star colour. Defaults to gold; a variant tints from its token (e.g. `warning`, `primary`). */
|
|
24
7
|
variant?: Variant;
|
|
8
|
+
/** Star size (sm/md/lg). */
|
|
25
9
|
size?: Size;
|
|
26
|
-
/** Accessible label for the interactive slider. */
|
|
27
|
-
label?: string;
|
|
28
10
|
};
|
|
29
|
-
declare const Rating: import("svelte").Component<RatingProps, {}, "
|
|
11
|
+
declare const Rating: import("svelte").Component<RatingProps, {}, "">;
|
|
30
12
|
type Rating = ReturnType<typeof Rating>;
|
|
31
13
|
export default Rating;
|
|
@@ -166,16 +166,14 @@
|
|
|
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">
|
|
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. -->
|
|
174
172
|
<div
|
|
175
173
|
bind:this={trackEl}
|
|
176
174
|
onpointerdown={onTrackPointerDown}
|
|
177
175
|
class="relative flex-1 select-none rounded-full {disabled
|
|
178
|
-
? 'opacity-50
|
|
176
|
+
? 'opacity-50 pointer-events-none'
|
|
179
177
|
: 'cursor-pointer'} {responsiveClasses(size, trackH)}"
|
|
180
178
|
style="background-color: color-mix(in srgb, var({token}) 16%, transparent);"
|
|
181
179
|
>
|
|
@@ -200,7 +198,7 @@
|
|
|
200
198
|
aria-describedby={describedBy}
|
|
201
199
|
onkeydown={(e) => onThumbKeydown(e, i)}
|
|
202
200
|
class="absolute top-1/2 -translate-x-1/2 -translate-y-1/2 rounded-full border-2 shadow {disabled
|
|
203
|
-
? '
|
|
201
|
+
? ''
|
|
204
202
|
: '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(
|
|
205
203
|
size,
|
|
206
204
|
thumbSz,
|
|
@@ -62,19 +62,14 @@
|
|
|
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.
|
|
68
65
|
const fieldSize: Record<Size, string> = {
|
|
69
|
-
sm: "min-h-6 px-1.5 py-0 gap-1 text-xs",
|
|
70
|
-
md: "min-h-8 px-2 py-
|
|
71
|
-
lg: "min-h-10 px-2.5 py-
|
|
66
|
+
sm: "min-h-6 px-1.5 py-0.5 gap-1 text-xs",
|
|
67
|
+
md: "min-h-8 px-2 py-1 gap-1.5 text-sm",
|
|
68
|
+
lg: "min-h-10 px-2.5 py-1.5 gap-2 text-base",
|
|
72
69
|
};
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
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" : "");
|
|
70
|
+
const chipSize = $derived<Size>(
|
|
71
|
+
(typeof size === "string" ? size : "md") === "lg" ? "md" : "sm",
|
|
72
|
+
);
|
|
78
73
|
|
|
79
74
|
let draft = $state("");
|
|
80
75
|
|
|
@@ -120,7 +115,6 @@
|
|
|
120
115
|
text={v}
|
|
121
116
|
variant={variant === "ghost" ? "secondary" : variant}
|
|
122
117
|
size={chipSize}
|
|
123
|
-
class={chipHeightClass}
|
|
124
118
|
removable
|
|
125
119
|
{disabled}
|
|
126
120
|
onremove={() => removeAt(i)}
|
package/dist/index.d.ts
CHANGED
|
@@ -43,7 +43,9 @@ 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";
|
|
46
47
|
export { FileUpload } from "./components/FileUpload/index.js";
|
|
48
|
+
export type { RangePreset } from "./components/DateRangePicker/index.js";
|
|
47
49
|
export { default as ThemeSelector } from "./components/ThemeSelector.svelte";
|
|
48
50
|
export { default as ThemeToggle } from "./components/ThemeToggle.svelte";
|
|
49
51
|
export { default as Popup } from "./components/Popup.svelte";
|
package/dist/index.js
CHANGED
|
@@ -44,6 +44,7 @@ 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";
|
|
47
48
|
export { FileUpload } from "./components/FileUpload/index.js";
|
|
48
49
|
// Theme Components
|
|
49
50
|
export { default as ThemeSelector } from "./components/ThemeSelector.svelte";
|
package/docs/components.md
CHANGED
|
@@ -122,6 +122,20 @@ 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
|
+
|
|
125
139
|
### Slider
|
|
126
140
|
|
|
127
141
|
Draggable range input — a single value or a `[min, max]` pair.
|
|
@@ -271,15 +285,10 @@ independent. Propagates `size`/`variant`/`disabled` to children.
|
|
|
271
285
|
|
|
272
286
|
### Rating
|
|
273
287
|
|
|
274
|
-
|
|
288
|
+
Read-only star-rating display (5-star, from a 10-point average).
|
|
275
289
|
|
|
276
290
|
- **Location**: `src/lib/components/Rating.svelte`
|
|
277
|
-
- **
|
|
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).
|
|
291
|
+
- **Props**: `average` (number), `count` (number)
|
|
283
292
|
|
|
284
293
|
## Specialized Input Components
|
|
285
294
|
|
|
@@ -390,19 +399,13 @@ dialog (operators, casts, dynamic `now()` dates, and a worked examples table). T
|
|
|
390
399
|
Standalone page navigator — the reusable extraction of `DataTable`'s footer pager.
|
|
391
400
|
|
|
392
401
|
- **Location**: `src/lib/components/Pagination/Pagination.svelte`
|
|
393
|
-
- **Axes**: `variant` (
|
|
394
|
-
filled button; omit for a neutral band, `ghost` for a transparent one), `size` (scales band, Select,
|
|
395
|
-
buttons, and the page field)
|
|
402
|
+
- **Axes**: `variant` (active page fill), `size`
|
|
396
403
|
- **Props**: `page` (bindable), `total`, `perPage` (bindable), `perPageOptions` (→ rows-per-page
|
|
397
|
-
`Select`
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
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>`)
|
|
404
|
+
`Select`), `siblingCount`/`boundaryCount`, `showEdges` (first/last), `showTotal`, `disabled`, `class`
|
|
405
|
+
- **Behavior**: numbered buttons with ellipsis for long ranges (active = filled, rest `ghost`);
|
|
406
|
+
prev/next/first/last disable at the ends; choosing `perPage` resets to page 1; `page` clamps to range
|
|
407
|
+
- **Accessibility**: `<nav aria-label="Pagination">`; active page `aria-current="page"`
|
|
408
|
+
- **Composition**: `Button` + `Select` only (no raw `<button>`)
|
|
406
409
|
|
|
407
410
|
## Overlays
|
|
408
411
|
|
package/docs/usage.md
CHANGED
|
@@ -311,17 +311,21 @@ 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
|
+
### DateRangePicker
|
|
315
315
|
|
|
316
|
-
|
|
316
|
+
A start–end range with a presets rail (composes `Calendar mode="range"`).
|
|
317
317
|
|
|
318
318
|
```svelte
|
|
319
319
|
<script>
|
|
320
|
-
import {
|
|
321
|
-
let
|
|
320
|
+
import { DateRangePicker } from "@spaethtech/svelte-ui";
|
|
321
|
+
let start = $state("");
|
|
322
|
+
let end = $state("");
|
|
322
323
|
</script>
|
|
323
324
|
|
|
324
|
-
<
|
|
325
|
+
<DateRangePicker bind:start bind:end label="Reporting period" />
|
|
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" }]} />
|
|
325
329
|
```
|
|
326
330
|
|
|
327
331
|
### Stepper
|
|
@@ -736,9 +740,7 @@ A thin separating rule — horizontal (optionally labelled) or vertical (toolbar
|
|
|
736
740
|
|
|
737
741
|
### Pagination
|
|
738
742
|
|
|
739
|
-
A standalone page navigator
|
|
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).
|
|
743
|
+
A standalone page navigator (numbered buttons + ellipsis, prev/next, optional per-page + total).
|
|
742
744
|
|
|
743
745
|
```svelte
|
|
744
746
|
<script>
|
|
@@ -747,13 +749,10 @@ indicator is an editable `NumberInput` (type a page + Enter to jump).
|
|
|
747
749
|
let perPage = $state(10);
|
|
748
750
|
</script>
|
|
749
751
|
|
|
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} />
|
|
752
|
+
<Pagination bind:page bind:perPage total={247} perPageOptions={[10, 25, 50, 100]} showTotal />
|
|
754
753
|
|
|
755
|
-
<!--
|
|
756
|
-
<Pagination bind:page total={200}
|
|
754
|
+
<!-- Compact: numbers only, with first/last controls -->
|
|
755
|
+
<Pagination bind:page total={200} showEdges />
|
|
757
756
|
```
|
|
758
757
|
|
|
759
758
|
### Breadcrumbs
|