@spaethtech/svelte-ui 0.15.1-dev.67.fbf3875 → 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.
@@ -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
- average: number;
7
- count: number;
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 { average, count, variant, size = "md" }: RatingProps = $props();
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
- // Validate and sanitize the average rating
20
- const validAverage = isNaN(average) || average < 0 ? 0 : Math.min(average, 10);
61
+ const active = $derived(interactive && !readonly && !disabled);
62
+ const step = $derived(allowHalf ? 0.5 : 1);
21
63
 
22
- // Convert TMDB 10-point scale to 5-star scale
23
- const fiveStarRating = validAverage / 2;
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
- // 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));
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 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>
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
- <span class="rating-value">{fiveStarRating.toFixed(1)}</span>
44
- <span class="rating-count">({count})</span>
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.filled {
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
- average: number;
5
- count: number;
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 pointer-events-none'
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.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",
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 chipSize = $derived<Size>(
71
- (typeof size === "string" ? size : "md") === "lg" ? "md" : "sm",
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)}
@@ -64,10 +64,10 @@ export interface QueryResult<T> {
64
64
  export type FetchFn<T> = (state: GridState) => QueryResult<FetchResult<T>>;
65
65
  export type ParsedQuery = QueryNode | null;
66
66
  /**
67
- * A row/bulk kebab menu entry. Convention: always render ALL items and `disabled` the invalid
68
- * ones. Icons are optional — when ANY item has an icon, icon-less items reserve the icon column.
67
+ * A menu action. Convention: always render ALL items and `disabled` the invalid ones. Icons are
68
+ * optional — when ANY item has an icon, icon-less items reserve the icon column so labels align.
69
69
  */
70
- export type MenuItem = {
70
+ export type MenuAction = {
71
71
  label: string;
72
72
  icon?: Component<{
73
73
  class?: string;
@@ -77,3 +77,9 @@ export type MenuItem = {
77
77
  disabled?: boolean;
78
78
  danger?: boolean;
79
79
  };
80
+ /** A non-interactive separator line between groups of actions. */
81
+ export type MenuSeparator = {
82
+ divider: true;
83
+ };
84
+ /** A menu entry — either an action or a separator (`{ divider: true }`). */
85
+ export type MenuItem = MenuAction | MenuSeparator;
package/dist/index.d.ts CHANGED
@@ -7,7 +7,6 @@ export { EmptyState } from "./components/EmptyState/index.js";
7
7
  export { Avatar, AvatarGroup } from "./components/Avatar/index.js";
8
8
  export type { AvatarShape, AvatarStatus, AvatarItem } from "./components/Avatar/index.js";
9
9
  export { default as Button } from "./components/Button.svelte";
10
- export { default as ButtonDropdown } from "./components/ButtonDropdown.svelte";
11
10
  export { default as Card } from "./components/Card.svelte";
12
11
  export { default as CardHeader } from "./components/CardHeader.svelte";
13
12
  export { default as CardBody } from "./components/CardBody.svelte";
@@ -43,9 +42,7 @@ export { default as DatePicker } from "./components/DatePicker.svelte";
43
42
  export { default as TimeSpinner } from "./components/TimeSpinner.svelte";
44
43
  export { default as TimePicker } from "./components/TimePicker.svelte";
45
44
  export { default as TimeRangeInput } from "./components/TimeRangeInput.svelte";
46
- export { DateRangePicker } from "./components/DateRangePicker/index.js";
47
45
  export { FileUpload } from "./components/FileUpload/index.js";
48
- export type { RangePreset } from "./components/DateRangePicker/index.js";
49
46
  export { default as ThemeSelector } from "./components/ThemeSelector.svelte";
50
47
  export { default as ThemeToggle } from "./components/ThemeToggle.svelte";
51
48
  export { default as Popup } from "./components/Popup.svelte";
@@ -55,7 +52,7 @@ export { CommandPalette } from "./components/CommandPalette/index.js";
55
52
  export type { Command } from "./components/CommandPalette/index.js";
56
53
  export { Tree } from "./components/Tree/index.js";
57
54
  export type { TreeNode } from "./components/Tree/index.js";
58
- export type { MenuItem } from "./data/table/types.js";
55
+ export type { MenuItem, MenuAction, MenuSeparator } from "./data/table/types.js";
59
56
  export { ButtonGroup } from "./components/ButtonGroup/index.js";
60
57
  export type { ButtonGroupItem, ButtonGroupSelect } from "./components/ButtonGroup/index.js";
61
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";
@@ -44,7 +43,6 @@ export { default as DatePicker } from "./components/DatePicker.svelte";
44
43
  export { default as TimeSpinner } from "./components/TimeSpinner.svelte";
45
44
  export { default as TimePicker } from "./components/TimePicker.svelte";
46
45
  export { default as TimeRangeInput } from "./components/TimeRangeInput.svelte";
47
- export { DateRangePicker } from "./components/DateRangePicker/index.js";
48
46
  export { FileUpload } from "./components/FileUpload/index.js";
49
47
  // Theme Components
50
48
  export { default as ThemeSelector } from "./components/ThemeSelector.svelte";
@@ -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.
@@ -224,8 +210,14 @@ the selected one fills with the group `variant`. Not a form field (controlled vi
224
210
  `string[]`, multi), `orientation` (`'row' | 'col'`, default `'row'`), `disabled` (whole group),
225
211
  `class`, `children` (a **presentational** default slot — hand-authored `<Button>`s get the frame but
226
212
  you own their behaviour; mutually exclusive with `items`, which wins + DEV-warns).
227
- - **`ButtonGroupItem`**: `{ value?, text?, icon? (Snippet), disabled?, variant?, menu? (MenuItem[] —
228
- renders a split-button caret opening a `Menu`), onclick? }`.
213
+ - **`ButtonGroupItem`**: `{ value?, text?, icon? (Snippet), disabled?, loading?, variant?, href?,
214
+ target?, menu? (MenuItem[]), menuOnly?, onclick? }`. `loading`/`href`/`target` forward to `Button`
215
+ (a disabled `href` item falls back to a plain disabled button — `<a>` has no `disabled`).
216
+ - **Split buttons & menu triggers**: an item with `menu` renders a **split button** (its action + a
217
+ trailing caret that opens the `Menu`). Add **`menuOnly`** and the whole item becomes a caret-only
218
+ **menu-trigger** (its click opens the menu; a bare one defaults to a chevron). Compose the two for a
219
+ standalone `[ action ][ ▾ ]` dropdown: a normal item + a `menuOnly` item — this **replaces the
220
+ removed `ButtonDropdown`**.
229
221
  - **Selection / a11y**: `'single'` → `role="radiogroup"` + `aria-checked`, arrow-key roving;
230
222
  `'multi'` → `aria-pressed`; `'none'` → `role="toolbar"`, `item.onclick` fires (no swap).
231
223
 
@@ -285,10 +277,15 @@ independent. Propagates `size`/`variant`/`disabled` to children.
285
277
 
286
278
  ### Rating
287
279
 
288
- Read-only star-rating display (5-star, from a 10-point average).
280
+ Star rating in two modes: a read-only display, or an interactive picker.
289
281
 
290
282
  - **Location**: `src/lib/components/Rating.svelte`
291
- - **Props**: `average` (number), `count` (number)
283
+ - **Display mode** (default): `average` (0–10 score, shown as 5 stars = average/2), `count` (votes).
284
+ - **Interactive mode**: `interactive` + `value` (bindable, 0..`max`). Click a star, hover to preview,
285
+ or focus and use ←/→ · ↑/↓ · Home/End. `allowHalf` enables half-star selection; `onchange`,
286
+ `readonly`, `disabled`. Rendered as a `role="slider"` (same a11y pattern as `Slider`).
287
+ - **Shared**: `variant` (filled-star tint; default gold), `size`, `showValue` (numeric readout —
288
+ defaults on in display mode, off interactive), `label` (slider a11y label).
292
289
 
293
290
  ## Specialized Input Components
294
291
 
@@ -399,13 +396,19 @@ dialog (operators, casts, dynamic `now()` dates, and a worked examples table). T
399
396
  Standalone page navigator — the reusable extraction of `DataTable`'s footer pager.
400
397
 
401
398
  - **Location**: `src/lib/components/Pagination/Pagination.svelte`
402
- - **Axes**: `variant` (active page fill), `size`
399
+ - **Axes**: `variant` (tints the band **surface + button hover**, like DataTable's footer — NOT a
400
+ filled button; omit for a neutral band, `ghost` for a transparent one), `size` (scales band, Select,
401
+ buttons, and the page field)
403
402
  - **Props**: `page` (bindable), `total`, `perPage` (bindable), `perPageOptions` (→ rows-per-page
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>`)
403
+ `Select` on the left), `showEdges` (first/last, default **true**), `showTotal` (range text, default
404
+ **true**), `borderless` (drop the band border/surface), `disabled`, `class`
405
+ - **Layout**: a tinted, bordered band (matches `DataTable`'s footer) per-page `Select` left; range
406
+ text + a compact `First · Prev · [page]/N · Next · Last` stepper right. The page indicator is an
407
+ **editable `NumberInput`** (min 1, max N, clamped) — type a page + Enter to jump; ↑/↓ step it.
408
+ - **Behavior**: prev/next/first/last disable at the ends; choosing `perPage` resets to page 1; `page`
409
+ clamps to range; jump commits on change (Enter/blur)
410
+ - **Accessibility**: `<nav aria-label="Pagination">`; the jump field is `aria-label="Page number"`
411
+ - **Composition**: `Button` + `Select` + `NumberInput` only (no raw `<button>`/`<input>`)
409
412
 
410
413
  ## Overlays
411
414
 
@@ -468,8 +471,10 @@ never clipped by overflow/stacking contexts. Placement via the shared `anchored`
468
471
  Keyboard-navigable action menu rendered inside a `Popup`.
469
472
 
470
473
  - **Location**: `src/lib/components/Menu.svelte`
471
- - **Props**: `anchor`, `open` (bindable), `items` (`MenuItem[]` from `@spaethtech/svelte-ui/data`), `side`,
472
- `align`, `boundary`
474
+ - **Props**: `anchor`, `open` (bindable), `items` (`MenuItem[]`), `side`, `align`, `boundary`, `size`,
475
+ `variant`
476
+ - **`MenuItem`**: an **action** `{ label, icon?, onclick?, href?, disabled?, danger? }` **or** a
477
+ **separator** `{ divider: true }` (a non-interactive rule between groups; skipped by keyboard nav).
473
478
  - **Features**: renders all items always and disables the invalid ones; aligns labels when any
474
479
  item has an icon
475
480
 
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
- ### DateRangePicker
314
+ ### Date range
315
315
 
316
- A start–end range with a presets rail (composes `Calendar mode="range"`).
316
+ Use `DatePicker` with the `range` prop — its value is a `{ start, end }` object.
317
317
 
318
318
  ```svelte
319
319
  <script>
320
- import { DateRangePicker } from "@spaethtech/svelte-ui";
321
- let start = $state("");
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
- <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" }]} />
324
+ <DatePicker range bind:value={range} label="Reporting period" />
329
325
  ```
330
326
 
331
327
  ### Stepper
@@ -468,6 +464,19 @@ A joined row/column of buttons — an action toolbar or a controlled segmented s
468
464
  <!-- Action toolbar with a split-button (Export has a `menu`) -->
469
465
  <ButtonGroup items={actions} variant="secondary" />
470
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
+
471
480
  <!-- Vertical -->
472
481
  <ButtonGroup items={alignItems} select="single" bind:value={align} orientation="col" />
473
482
 
@@ -740,7 +749,9 @@ A thin separating rule — horizontal (optionally labelled) or vertical (toolbar
740
749
 
741
750
  ### Pagination
742
751
 
743
- A standalone page navigator (numbered buttons + ellipsis, prev/next, optional per-page + total).
752
+ A standalone page navigator laid out like `DataTable`'s footer: a tinted band with an optional
753
+ per-page `Select`, a range total, and a `First · Prev · [page]/N · Next · Last` stepper whose page
754
+ indicator is an editable `NumberInput` (type a page + Enter to jump).
744
755
 
745
756
  ```svelte
746
757
  <script>
@@ -749,10 +760,13 @@ A standalone page navigator (numbered buttons + ellipsis, prev/next, optional pe
749
760
  let perPage = $state(10);
750
761
  </script>
751
762
 
752
- <Pagination bind:page bind:perPage total={247} perPageOptions={[10, 25, 50, 100]} showTotal />
763
+ <Pagination bind:page bind:perPage total={247} perPageOptions={[10, 25, 50, 100]} />
764
+
765
+ <!-- Compact: just the stepper (no per-page Select) -->
766
+ <Pagination bind:page total={200} showTotal={false} />
753
767
 
754
- <!-- Compact: numbers only, with first/last controls -->
755
- <Pagination bind:page total={200} showEdges />
768
+ <!-- variant tints the band surface + button hover (not the buttons); ghost = transparent band -->
769
+ <Pagination bind:page total={200} variant="primary" />
756
770
  ```
757
771
 
758
772
  ### Breadcrumbs
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spaethtech/svelte-ui",
3
- "version": "0.15.1-dev.67.fbf3875",
3
+ "version": "0.15.1-dev.71.e357f8b",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/spaethtech/svelte-ui.git"