@urbicon-ui/blocks 6.23.0 → 6.24.0
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/dist/components/Sparkline/Sparkline.svelte +10 -4
- package/dist/components/Sparkline/index.d.ts +9 -1
- package/dist/primitives/Badge/Badge.svelte +8 -2
- package/dist/primitives/Badge/index.d.ts +67 -32
- package/dist/primitives/Checkbox/Checkbox.svelte +1 -1
- package/dist/primitives/ConfirmDialog/index.d.ts +2 -1
- package/dist/primitives/Pagination/Pagination.svelte +28 -15
- package/dist/primitives/RadioGroup/RadioGroup.svelte +6 -2
- package/dist/primitives/RadioGroup/RadioItem.svelte +1 -1
- package/dist/primitives/Toast/Toaster.svelte +81 -7
- package/dist/primitives/Toggle/Toggle.svelte +6 -2
- package/package.json +3 -3
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
data,
|
|
8
8
|
width = 96,
|
|
9
9
|
height = 24,
|
|
10
|
+
fluid = false,
|
|
10
11
|
area = false,
|
|
11
12
|
showEndPoint = false,
|
|
12
13
|
color = 'var(--color-chart-1)',
|
|
@@ -40,15 +41,19 @@
|
|
|
40
41
|
|
|
41
42
|
<span
|
|
42
43
|
{...rest}
|
|
43
|
-
class={[
|
|
44
|
+
class={[
|
|
45
|
+
!unstyled && (fluid ? 'block w-full' : 'inline-block align-middle'),
|
|
46
|
+
slot('root'),
|
|
47
|
+
className
|
|
48
|
+
]}
|
|
44
49
|
role={ariaLabel ? 'img' : undefined}
|
|
45
50
|
aria-label={ariaLabel}
|
|
46
51
|
aria-hidden={ariaLabel ? undefined : 'true'}
|
|
47
52
|
>
|
|
48
53
|
<svg
|
|
49
|
-
class={slot('svg')}
|
|
50
|
-
{width}
|
|
51
|
-
{height}
|
|
54
|
+
class={[fluid && 'w-full h-auto', slot('svg')]}
|
|
55
|
+
width={fluid ? undefined : width}
|
|
56
|
+
height={fluid ? undefined : height}
|
|
52
57
|
viewBox="0 0 {width} {height}"
|
|
53
58
|
preserveAspectRatio="none"
|
|
54
59
|
>
|
|
@@ -70,6 +75,7 @@
|
|
|
70
75
|
stroke-width={strokeWidth}
|
|
71
76
|
stroke-linejoin="round"
|
|
72
77
|
stroke-linecap="round"
|
|
78
|
+
vector-effect={fluid ? 'non-scaling-stroke' : undefined}
|
|
73
79
|
/>
|
|
74
80
|
{/if}
|
|
75
81
|
{#if showEndPoint && geometry.last}
|
|
@@ -4,7 +4,8 @@ export type SparklineSlotClasses = Partial<Record<'root' | 'svg' | 'line' | 'are
|
|
|
4
4
|
/**
|
|
5
5
|
* @description Tiny inline trend line — no axes, no labels — sized to flow in
|
|
6
6
|
* table cells, cards, or running text. Zero-dependency SVG, optional area fill
|
|
7
|
-
* and end-point dot.
|
|
7
|
+
* and end-point dot. Fixed `width`/`height` by default, or set `fluid` to fill
|
|
8
|
+
* the container width. Aria-hidden by default with an optional `ariaLabel`.
|
|
8
9
|
*
|
|
9
10
|
* @tag display
|
|
10
11
|
* @tag data
|
|
@@ -23,6 +24,13 @@ export interface SparklineProps extends Omit<HTMLAttributes<HTMLElement>, 'child
|
|
|
23
24
|
width?: number;
|
|
24
25
|
/** Height in px. @default 24 */
|
|
25
26
|
height?: number;
|
|
27
|
+
/**
|
|
28
|
+
* Fill the container width instead of a fixed pixel size. Drops the svg's
|
|
29
|
+
* `width`/`height` attributes (they still set the `viewBox` aspect ratio) so
|
|
30
|
+
* it scales to its container; strokes stay crisp via
|
|
31
|
+
* `vector-effect="non-scaling-stroke"`. @default false
|
|
32
|
+
*/
|
|
33
|
+
fluid?: boolean;
|
|
26
34
|
/** Fill the area under the line. @default false */
|
|
27
35
|
area?: boolean;
|
|
28
36
|
/** Mark the last point with a dot. @default false */
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
onRemove,
|
|
35
35
|
onclick,
|
|
36
36
|
onHover,
|
|
37
|
-
role
|
|
37
|
+
role,
|
|
38
38
|
...restProps
|
|
39
39
|
}: BadgeProps = $props();
|
|
40
40
|
|
|
@@ -56,6 +56,12 @@
|
|
|
56
56
|
const isInteractive = $derived(purpose === 'chip' || interactive || !!onclick);
|
|
57
57
|
const isRemovable = $derived(removable && !isDot);
|
|
58
58
|
|
|
59
|
+
// An interactive badge (clickable chip / onclick) carries button semantics:
|
|
60
|
+
// it is focusable and Enter/Space-activatable, so screen readers must hear a
|
|
61
|
+
// button, not a `status` region. An explicit `role` always wins; a disabled
|
|
62
|
+
// badge is inert (pointer-events-none, guarded handlers) so it stays `status`.
|
|
63
|
+
const effRole = $derived(role ?? (isInteractive && !disabled ? 'button' : 'status'));
|
|
64
|
+
|
|
59
65
|
const variantProps: BadgeVariants = $derived({
|
|
60
66
|
tier: effectiveTier,
|
|
61
67
|
intent,
|
|
@@ -124,7 +130,7 @@
|
|
|
124
130
|
? [slotClasses?.base, className].filter(Boolean).join(' ')
|
|
125
131
|
: styles.base({ class: [slotClasses?.base, className] })
|
|
126
132
|
]}
|
|
127
|
-
{
|
|
133
|
+
role={effRole}
|
|
128
134
|
data-purpose={purpose}
|
|
129
135
|
tabindex={isInteractive && !disabled ? 0 : undefined}
|
|
130
136
|
onmouseenter={handleMouseEnter}
|
|
@@ -3,24 +3,13 @@ import type { HTMLAttributes } from 'svelte/elements';
|
|
|
3
3
|
import type { MintProp } from '../../mint/index.js';
|
|
4
4
|
import type { BadgePlacement, BadgeSlots, BadgeVariants } from './badge.variants.js';
|
|
5
5
|
/**
|
|
6
|
-
* Shared fields that apply to every Badge variant
|
|
7
|
-
* label-
|
|
6
|
+
* Shared fields that apply to every Badge arm. `variant`, `purpose`, and the
|
|
7
|
+
* label-only props (`children` / `counter` / `removable` / `interactive` /
|
|
8
|
+
* `onRemove`) are declared per-arm instead — that split is what lets *both*
|
|
9
|
+
* dot spellings (`variant="dot"` and the canonical `purpose="dot"`) forbid the
|
|
10
|
+
* label-only props at the type level.
|
|
8
11
|
*/
|
|
9
|
-
interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
10
|
-
/**
|
|
11
|
-
* The badge's semantic purpose — the canonical axis that resolves what the
|
|
12
|
-
* badge *is*, since a bare Badge served five overlapping roles. Orchestrates
|
|
13
|
-
* the low-level visual props so you rarely set them directly:
|
|
14
|
-
* - `status` — a state marker (Active, Failed); pairs with `intent`.
|
|
15
|
-
* - `tag` — a neutral inline label (category, version).
|
|
16
|
-
* - `counter` — a compact numeric pill (replaces the `counter` boolean).
|
|
17
|
-
* - `dot` — a pure indicator, content hidden (replaces `variant="dot"`).
|
|
18
|
-
* - `chip` — a removable, interactive filter chip; pair with `removable`.
|
|
19
|
-
*
|
|
20
|
-
* Leave unset to drive the badge purely by the low-level props (back-compat).
|
|
21
|
-
* When set, `purpose` wins over `variant="dot"` / the `counter` boolean.
|
|
22
|
-
*/
|
|
23
|
-
purpose?: 'status' | 'tag' | 'counter' | 'dot' | 'chip';
|
|
12
|
+
interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter' | 'removable' | 'interactive'>, Omit<HTMLAttributes<HTMLElement>, 'children'> {
|
|
24
13
|
/** Add a pulsing animation to draw attention (e.g. for live indicators). */
|
|
25
14
|
pulse?: boolean;
|
|
26
15
|
/** Visually disable the badge (reduced opacity, no pointer events). */
|
|
@@ -29,7 +18,7 @@ interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omi
|
|
|
29
18
|
border?: boolean;
|
|
30
19
|
/** Anchor the badge absolutely within a `position: relative` parent. */
|
|
31
20
|
placement?: BadgePlacement;
|
|
32
|
-
/** Click handler. Automatically enables interactive styles. */
|
|
21
|
+
/** Click handler. Automatically enables interactive styles (and `role="button"`). */
|
|
33
22
|
onclick?: (event: MouseEvent) => void;
|
|
34
23
|
/** Called when the hover state changes. */
|
|
35
24
|
onHover?: (hovered: boolean) => void;
|
|
@@ -46,30 +35,73 @@ interface BadgeBaseProps extends Omit<BadgeVariants, 'variant' | 'counter'>, Omi
|
|
|
46
35
|
* and make the custom look reusable across the project.
|
|
47
36
|
*/
|
|
48
37
|
preset?: string;
|
|
49
|
-
/**
|
|
50
|
-
|
|
38
|
+
/**
|
|
39
|
+
* ARIA role. A static badge is announced as `"status"`; an interactive badge
|
|
40
|
+
* (`onclick` or `purpose="chip"`, when not `disabled`) defaults to `"button"`
|
|
41
|
+
* so assistive tech announces its activation semantics. Set explicitly to
|
|
42
|
+
* override — e.g. `"alert"` for time-sensitive notifications. An explicit
|
|
43
|
+
* value always wins over the derived default.
|
|
44
|
+
*/
|
|
45
|
+
role?: 'status' | 'alert' | 'badge' | 'button';
|
|
51
46
|
/** Micro-interaction preset. Only applies when the badge is interactive. */
|
|
52
47
|
mint?: MintProp;
|
|
53
48
|
}
|
|
54
49
|
/**
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* hover-scale on a 2.5 × 2.5 px dot is never the intended UI.
|
|
50
|
+
* The label-only props a pure-indicator dot forbids. An invisible remove
|
|
51
|
+
* button, counter shape, or hover-scale on a 2.5 × 2.5 px dot is never the
|
|
52
|
+
* intended UI, so every dot arm excludes them at the type level.
|
|
59
53
|
*/
|
|
60
|
-
interface
|
|
61
|
-
/** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. */
|
|
62
|
-
variant: 'dot';
|
|
54
|
+
interface BadgeDotForbiddenProps {
|
|
63
55
|
children?: never;
|
|
64
56
|
counter?: never;
|
|
65
57
|
removable?: never;
|
|
66
58
|
interactive?: never;
|
|
67
59
|
onRemove?: never;
|
|
68
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Dot badge selected by the canonical `purpose="dot"` — a pure indicator whose
|
|
63
|
+
* content is hidden. `variant` is inert here (the dot look always wins), and
|
|
64
|
+
* the label-only props are excluded by {@link BadgeDotForbiddenProps}.
|
|
65
|
+
*/
|
|
66
|
+
interface BadgeDotByPurposeProps extends BadgeBaseProps, BadgeDotForbiddenProps {
|
|
67
|
+
/**
|
|
68
|
+
* The canonical dot spelling — forces the pure-indicator look regardless of
|
|
69
|
+
* `variant`. For the label roles use `status` / `tag` / `counter` / `chip`
|
|
70
|
+
* (their own arm).
|
|
71
|
+
*/
|
|
72
|
+
purpose: 'dot';
|
|
73
|
+
/** Inert under `purpose="dot"`; accepted only so a leftover `variant` compiles. */
|
|
74
|
+
variant?: 'filled' | 'outlined' | 'soft' | 'dot';
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Dot badge selected by the deprecated `variant="dot"`. Prefer `purpose="dot"`.
|
|
78
|
+
* Same exclusions as {@link BadgeDotByPurposeProps}.
|
|
79
|
+
*/
|
|
80
|
+
interface BadgeDotProps extends BadgeBaseProps, BadgeDotForbiddenProps {
|
|
81
|
+
/** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. */
|
|
82
|
+
variant: 'dot';
|
|
83
|
+
/** Only the dot purpose is non-contradictory with `variant="dot"`. */
|
|
84
|
+
purpose?: 'dot';
|
|
85
|
+
}
|
|
69
86
|
/**
|
|
70
87
|
* Label-style badge — accepts content, counter shape, remove button, etc.
|
|
71
88
|
*/
|
|
72
89
|
interface BadgeStandardProps extends BadgeBaseProps {
|
|
90
|
+
/**
|
|
91
|
+
* The badge's semantic purpose — the canonical axis that resolves what the
|
|
92
|
+
* badge *is*, since a bare Badge served overlapping roles. Orchestrates the
|
|
93
|
+
* low-level visual props so you rarely set them directly:
|
|
94
|
+
* - `status` — a state marker (Active, Failed); pairs with `intent`.
|
|
95
|
+
* - `tag` — a neutral inline label (category, version).
|
|
96
|
+
* - `counter` — a compact numeric pill (replaces the `counter` boolean).
|
|
97
|
+
* - `chip` — a removable, interactive filter chip; pair with `removable`.
|
|
98
|
+
*
|
|
99
|
+
* For a pure indicator use `purpose="dot"` (its own arm — it forbids
|
|
100
|
+
* content / counter / remove). Leave unset to drive the badge purely by the
|
|
101
|
+
* low-level props (back-compat); when set, `purpose` wins over the `counter`
|
|
102
|
+
* boolean.
|
|
103
|
+
*/
|
|
104
|
+
purpose?: 'status' | 'tag' | 'counter' | 'chip';
|
|
73
105
|
/** Visual variant. `dot` renders a pure indicator (content hidden); the label variants accept the full surface. @default 'filled' */
|
|
74
106
|
variant?: 'filled' | 'outlined' | 'soft';
|
|
75
107
|
/** Badge content (text, icons, numbers). */
|
|
@@ -93,10 +125,13 @@ interface BadgeStandardProps extends BadgeBaseProps {
|
|
|
93
125
|
* @related Alert
|
|
94
126
|
* @related Toast
|
|
95
127
|
*
|
|
96
|
-
* Badge props are a discriminated union
|
|
97
|
-
*
|
|
98
|
-
* `
|
|
99
|
-
*
|
|
128
|
+
* Badge props are a discriminated union. The pure-indicator dot — spelled
|
|
129
|
+
* canonically as `purpose="dot"` or via the deprecated `variant="dot"` —
|
|
130
|
+
* forbids `children` / `counter` / `removable` / `interactive` / `onRemove`
|
|
131
|
+
* at the type level, while the label arms (`filled` / `outlined` / `soft`;
|
|
132
|
+
* `purpose` `status` / `tag` / `counter` / `chip`) accept the full surface.
|
|
133
|
+
* An interactive badge (`onclick` or `purpose="chip"`) is announced as a
|
|
134
|
+
* `button`, a static one as `status` (override via `role`).
|
|
100
135
|
*
|
|
101
136
|
* @example Purpose-driven (canonical) — the intent reads from `purpose`
|
|
102
137
|
* ```svelte
|
|
@@ -114,6 +149,6 @@ interface BadgeStandardProps extends BadgeBaseProps {
|
|
|
114
149
|
* </div>
|
|
115
150
|
* ```
|
|
116
151
|
*/
|
|
117
|
-
export type BadgeProps = BadgeDotProps | BadgeStandardProps;
|
|
152
|
+
export type BadgeProps = BadgeDotByPurposeProps | BadgeDotProps | BadgeStandardProps;
|
|
118
153
|
export { default as Badge } from './Badge.svelte';
|
|
119
154
|
export { type BadgePlacement, type BadgeVariants, badgeVariants, PLACEMENT_VALUES } from './badge.variants.js';
|
|
@@ -126,6 +126,7 @@
|
|
|
126
126
|
for={id}
|
|
127
127
|
>
|
|
128
128
|
<input
|
|
129
|
+
{...restProps}
|
|
129
130
|
bind:this={inputRef}
|
|
130
131
|
{id}
|
|
131
132
|
type="checkbox"
|
|
@@ -139,7 +140,6 @@
|
|
|
139
140
|
aria-invalid={ff.invalid ? 'true' : undefined}
|
|
140
141
|
aria-describedby={describedBy}
|
|
141
142
|
onchange={handleChange}
|
|
142
|
-
{...restProps}
|
|
143
143
|
/>
|
|
144
144
|
|
|
145
145
|
<span
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Snippet } from 'svelte';
|
|
2
|
+
import type { HTMLDialogAttributes } from 'svelte/elements';
|
|
2
3
|
import type { DialogIntent } from '../Dialog/index.js';
|
|
3
4
|
import type { DialogSlots } from '../Dialog/dialog.variants.js';
|
|
4
5
|
/**
|
|
@@ -36,7 +37,7 @@ export type ConfirmIntent = Exclude<DialogIntent, 'neutral'>;
|
|
|
36
37
|
* />
|
|
37
38
|
* ```
|
|
38
39
|
*/
|
|
39
|
-
export interface ConfirmDialogProps {
|
|
40
|
+
export interface ConfirmDialogProps extends Omit<HTMLDialogAttributes, 'children' | 'open' | 'title' | 'draggable'> {
|
|
40
41
|
/** Controls visibility. Supports bind:open. */
|
|
41
42
|
open?: boolean;
|
|
42
43
|
/** Heading shown in the dialog header. */
|
|
@@ -139,9 +139,10 @@
|
|
|
139
139
|
return () => handlePageChange(page);
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
-
// Computed state helpers
|
|
143
|
-
|
|
144
|
-
|
|
142
|
+
// Computed state helpers. Edge policy across all layouts is disabled-but-visible:
|
|
143
|
+
// Previous/Next stay mounted and go `disabled` at page 1 / N (never unmounted),
|
|
144
|
+
// so the arrow can't vanish from under the pointer (no layout shift, no focus
|
|
145
|
+
// loss). First/Last are a separate, redundancy-driven concern — see the markup.
|
|
145
146
|
const hasPreviousPage = $derived(currentPage > 1);
|
|
146
147
|
const hasNextPage = $derived(currentPage < totalPages);
|
|
147
148
|
|
|
@@ -220,19 +221,25 @@
|
|
|
220
221
|
{/if}
|
|
221
222
|
</div>
|
|
222
223
|
{:else if layout === 'navigation'}
|
|
223
|
-
<!-- Navigation Layout: Only Previous/Next
|
|
224
|
+
<!-- Navigation Layout: Only Previous/Next. Unified edge policy (matches the
|
|
225
|
+
table layout): both arrows stay mounted and go `disabled` at the boundary
|
|
226
|
+
(page 1 / N) rather than unmounting. This row is laid out `justify-between`,
|
|
227
|
+
so unmounting one arm would teleport the survivor across the whole bar
|
|
228
|
+
(start ↔ end) and drop focus — disabled-but-visible pins Previous left and
|
|
229
|
+
Next right. Prev/Next-only pagers therefore grey out the dead end here;
|
|
230
|
+
they do not disappear it. -->
|
|
224
231
|
<div
|
|
225
232
|
class={unstyled
|
|
226
233
|
? (slotClasses?.controls ?? '')
|
|
227
234
|
: styles.controls({ class: slotClasses?.controls })}
|
|
228
235
|
>
|
|
229
|
-
{#if showPreviousNext
|
|
236
|
+
{#if showPreviousNext}
|
|
230
237
|
<PaginationItem
|
|
231
238
|
{size}
|
|
232
239
|
{variant}
|
|
233
240
|
{intent}
|
|
234
241
|
{tier}
|
|
235
|
-
disabled={disabled || loading}
|
|
242
|
+
disabled={disabled || loading || !hasPreviousPage}
|
|
236
243
|
onPageClick={goToPrevious}
|
|
237
244
|
{mint}
|
|
238
245
|
>
|
|
@@ -242,15 +249,13 @@
|
|
|
242
249
|
{previousLabel}
|
|
243
250
|
{/if}
|
|
244
251
|
</PaginationItem>
|
|
245
|
-
{/if}
|
|
246
252
|
|
|
247
|
-
{#if showPreviousNext && hasNextPage}
|
|
248
253
|
<PaginationItem
|
|
249
254
|
{size}
|
|
250
255
|
{variant}
|
|
251
256
|
{intent}
|
|
252
257
|
{tier}
|
|
253
|
-
disabled={disabled || loading}
|
|
258
|
+
disabled={disabled || loading || !hasNextPage}
|
|
254
259
|
onPageClick={goToNext}
|
|
255
260
|
{mint}
|
|
256
261
|
>
|
|
@@ -276,7 +281,14 @@
|
|
|
276
281
|
? (slotClasses?.controls ?? '')
|
|
277
282
|
: styles.controls({ class: slotClasses?.controls })}
|
|
278
283
|
>
|
|
279
|
-
|
|
284
|
+
<!-- First / Last are gated by the ellipsis — by REDUNDANCY, not by the page
|
|
285
|
+
edge. `showStartEllipsis` is true only when page 1 sits OUTSIDE the visible
|
|
286
|
+
window; the moment it re-enters, page 1 is a directly-clickable number and
|
|
287
|
+
a "First" jump button would merely duplicate it. That predicate already
|
|
288
|
+
implies currentPage > 1, so (unlike Previous/Next) First/Last are never a
|
|
289
|
+
dead-end control needing disabled-but-visible: hiding one drops a duplicate,
|
|
290
|
+
not an edge stepper — no layout-shift/focus trap. -->
|
|
291
|
+
{#if showFirstLast && showStartEllipsis}
|
|
280
292
|
<PaginationItem
|
|
281
293
|
{size}
|
|
282
294
|
{variant}
|
|
@@ -294,13 +306,13 @@
|
|
|
294
306
|
</PaginationItem>
|
|
295
307
|
{/if}
|
|
296
308
|
|
|
297
|
-
{#if showPreviousNext
|
|
309
|
+
{#if showPreviousNext}
|
|
298
310
|
<PaginationItem
|
|
299
311
|
{size}
|
|
300
312
|
{variant}
|
|
301
313
|
{intent}
|
|
302
314
|
{tier}
|
|
303
|
-
disabled={disabled || loading}
|
|
315
|
+
disabled={disabled || loading || !hasPreviousPage}
|
|
304
316
|
onPageClick={goToPrevious}
|
|
305
317
|
{mint}
|
|
306
318
|
>
|
|
@@ -364,13 +376,13 @@
|
|
|
364
376
|
</div>
|
|
365
377
|
{/if}
|
|
366
378
|
|
|
367
|
-
{#if showPreviousNext
|
|
379
|
+
{#if showPreviousNext}
|
|
368
380
|
<PaginationItem
|
|
369
381
|
{size}
|
|
370
382
|
{variant}
|
|
371
383
|
{intent}
|
|
372
384
|
{tier}
|
|
373
|
-
disabled={disabled || loading}
|
|
385
|
+
disabled={disabled || loading || !hasNextPage}
|
|
374
386
|
onPageClick={goToNext}
|
|
375
387
|
{mint}
|
|
376
388
|
>
|
|
@@ -382,7 +394,8 @@
|
|
|
382
394
|
</PaginationItem>
|
|
383
395
|
{/if}
|
|
384
396
|
|
|
385
|
-
|
|
397
|
+
<!-- See the First-button note above: ellipsis-gated (redundancy), not edge-gated. -->
|
|
398
|
+
{#if showFirstLast && showEndEllipsis}
|
|
386
399
|
<PaginationItem
|
|
387
400
|
{size}
|
|
388
401
|
{variant}
|
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
preset,
|
|
28
28
|
id,
|
|
29
29
|
'aria-describedby': ariaDescribedby,
|
|
30
|
+
'aria-labelledby': ariaLabelledby,
|
|
30
31
|
...restProps
|
|
31
32
|
}: RadioGroupProps = $props();
|
|
32
33
|
|
|
@@ -43,6 +44,9 @@
|
|
|
43
44
|
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
44
45
|
|
|
45
46
|
const groupId = $derived(id || `radiogroup-${propsId}`);
|
|
47
|
+
// When a `label` is rendered it owns the group's `aria-labelledby`; otherwise a
|
|
48
|
+
// consumer-supplied `aria-labelledby` (an external heading) is used as the
|
|
49
|
+
// fallback so external labelling survives (see the group element below).
|
|
46
50
|
const labelId = $derived(label ? `${groupId}-label` : undefined);
|
|
47
51
|
// ARIA wiring is shared with every form primitive — see XC-2.
|
|
48
52
|
const ff = useFormField(() => ({
|
|
@@ -159,16 +163,16 @@
|
|
|
159
163
|
{/if}
|
|
160
164
|
|
|
161
165
|
<div
|
|
166
|
+
{...restProps}
|
|
162
167
|
bind:this={groupElement}
|
|
163
168
|
role="radiogroup"
|
|
164
169
|
id={groupId}
|
|
165
170
|
class={unstyled ? (slotClasses?.group ?? '') : styles.group({ class: slotClasses?.group })}
|
|
166
|
-
aria-labelledby={labelId}
|
|
171
|
+
aria-labelledby={labelId ?? ariaLabelledby}
|
|
167
172
|
aria-describedby={describedBy}
|
|
168
173
|
aria-required={required || undefined}
|
|
169
174
|
aria-invalid={ff.invalid ? 'true' : undefined}
|
|
170
175
|
onkeydown={handleKeydown}
|
|
171
|
-
{...restProps}
|
|
172
176
|
>
|
|
173
177
|
{@render children()}
|
|
174
178
|
</div>
|
|
@@ -71,6 +71,7 @@
|
|
|
71
71
|
for={id}
|
|
72
72
|
>
|
|
73
73
|
<input
|
|
74
|
+
{...restProps}
|
|
74
75
|
{id}
|
|
75
76
|
type="radio"
|
|
76
77
|
name={ctx.name}
|
|
@@ -80,7 +81,6 @@
|
|
|
80
81
|
class="peer sr-only"
|
|
81
82
|
tabindex={isChecked || (!ctx.value && !isDisabled) ? 0 : -1}
|
|
82
83
|
onchange={handleChange}
|
|
83
|
-
{...restProps}
|
|
84
84
|
/>
|
|
85
85
|
|
|
86
86
|
<span
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
2
|
import { useBlocksI18n } from '../..';
|
|
3
|
+
import { untrack } from 'svelte';
|
|
3
4
|
import { fly } from 'svelte/transition';
|
|
4
5
|
import { getOverlayMotion } from '../../utils';
|
|
5
6
|
import { toastVariants, type ToastVariants } from './toast.variants';
|
|
@@ -88,6 +89,42 @@
|
|
|
88
89
|
|
|
89
90
|
const visibleToasts = $derived(toaster.toasts.slice(-max));
|
|
90
91
|
|
|
92
|
+
// Stranded-pause guard. A toast removed from under the cursor (e.g. its own
|
|
93
|
+
// close button) does not reliably fire `pointerout`, so `pointerInside` can
|
|
94
|
+
// stay stuck `true` and freeze every remaining timer — worst case a fresh
|
|
95
|
+
// toast is then born paused and never counts down. Rather than trust the flag,
|
|
96
|
+
// when the rendered stack SHRINKS we re-derive containment from the real cursor
|
|
97
|
+
// position. The cursor is tracked from the region's own bubbling pointer events
|
|
98
|
+
// (the container is `pointer-events-none`, so these fire only over a toast
|
|
99
|
+
// child, which is exactly when the coords matter).
|
|
100
|
+
let containerEl: HTMLElement | undefined;
|
|
101
|
+
let pointerX = 0;
|
|
102
|
+
let pointerY = 0;
|
|
103
|
+
let prevVisibleCount = 0;
|
|
104
|
+
|
|
105
|
+
function reconcilePointerInside() {
|
|
106
|
+
// Only a stranded `true` needs rescuing; SSR-safe — effects/handlers never
|
|
107
|
+
// run on the server, and `document` is guarded regardless.
|
|
108
|
+
if (typeof document === 'undefined' || !pointerInside || !containerEl) return;
|
|
109
|
+
const hit = document.elementFromPoint(pointerX, pointerY);
|
|
110
|
+
const toastEl = (hit?.closest('[data-toast-id]') ?? null) as HTMLElement | null;
|
|
111
|
+
// Inside only if the cursor sits over a toast that is (a) in THIS region and
|
|
112
|
+
// (b) still live — not the just-removed one, whose node lingers through its
|
|
113
|
+
// fly-out outro.
|
|
114
|
+
pointerInside =
|
|
115
|
+
!!toastEl &&
|
|
116
|
+
containerEl.contains(toastEl) &&
|
|
117
|
+
visibleToasts.some((t) => t.id === toastEl.dataset.toastId);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
$effect(() => {
|
|
121
|
+
const count = visibleToasts.length;
|
|
122
|
+
untrack(() => {
|
|
123
|
+
if (count < prevVisibleCount) reconcilePointerInside();
|
|
124
|
+
prevVisibleCount = count;
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
91
128
|
const styles = $derived(
|
|
92
129
|
unstyled
|
|
93
130
|
? {
|
|
@@ -151,10 +188,19 @@
|
|
|
151
188
|
</script>
|
|
152
189
|
|
|
153
190
|
<div
|
|
191
|
+
bind:this={containerEl}
|
|
154
192
|
class={[slot('container'), className].filter(Boolean).join(' ')}
|
|
155
193
|
aria-live="polite"
|
|
156
194
|
aria-relevant="additions removals"
|
|
157
|
-
onpointerover={() =>
|
|
195
|
+
onpointerover={(e) => {
|
|
196
|
+
pointerX = e.clientX;
|
|
197
|
+
pointerY = e.clientY;
|
|
198
|
+
pointerInside = true;
|
|
199
|
+
}}
|
|
200
|
+
onpointermove={(e) => {
|
|
201
|
+
pointerX = e.clientX;
|
|
202
|
+
pointerY = e.clientY;
|
|
203
|
+
}}
|
|
158
204
|
onpointerout={(e) => {
|
|
159
205
|
if (leftRegion(e)) pointerInside = false;
|
|
160
206
|
}}
|
|
@@ -166,7 +212,13 @@
|
|
|
166
212
|
>
|
|
167
213
|
{#each visibleToasts as toast (toast.id)}
|
|
168
214
|
{@const IntentIcon = INTENT_ICON_MAP[toast.intent] ?? INTENT_ICON_MAP.neutral}
|
|
169
|
-
<div
|
|
215
|
+
<div
|
|
216
|
+
class={slot('toast', toast.intent)}
|
|
217
|
+
role="alert"
|
|
218
|
+
aria-atomic="true"
|
|
219
|
+
data-toast-id={toast.id}
|
|
220
|
+
transition:fly={flyParams()}
|
|
221
|
+
>
|
|
170
222
|
{#if toast.loading}
|
|
171
223
|
<span class={slot('icon', toast.intent)}><Spinner size="sm" /></span>
|
|
172
224
|
{:else}
|
|
@@ -216,13 +268,17 @@
|
|
|
216
268
|
{/if}
|
|
217
269
|
|
|
218
270
|
{#if toast.showProgress && Number.isFinite(toast.duration) && toast.duration > 0}
|
|
219
|
-
<!--
|
|
220
|
-
|
|
221
|
-
|
|
271
|
+
<!-- The countdown animation lives in the `<style>` rule below (not inline)
|
|
272
|
+
so `@media (prefers-reduced-motion: reduce)` can reach it — an inline
|
|
273
|
+
`animation` shorthand is invisible to media queries. Only the real
|
|
274
|
+
per-toast duration is passed inline, as a custom property.
|
|
275
|
+
`animation-play-state` (also inline, bound to the store's paused flag)
|
|
276
|
+
freezes the bar with the timer on hover/focus and resumes it in place;
|
|
277
|
+
inline wins over the rule's implicit `running`, and toggling only the
|
|
222
278
|
play-state never restarts the animation. -->
|
|
223
279
|
<div
|
|
224
|
-
class={slot('progress', toast.intent)}
|
|
225
|
-
style="
|
|
280
|
+
class={[slot('progress', toast.intent), 'blocks-toast-progress-bar']}
|
|
281
|
+
style="--toast-progress-duration: {toast.duration}ms; animation-play-state: {toaster.paused
|
|
226
282
|
? 'paused'
|
|
227
283
|
: 'running'};"
|
|
228
284
|
></div>
|
|
@@ -240,4 +296,22 @@
|
|
|
240
296
|
width: 0%;
|
|
241
297
|
}
|
|
242
298
|
}
|
|
299
|
+
|
|
300
|
+
/* The countdown bar animates via this class (not an inline `animation`) so the
|
|
301
|
+
reduced-motion query below can reach it; the per-toast duration arrives as
|
|
302
|
+
the `--toast-progress-duration` custom property. `:global` keeps it off
|
|
303
|
+
Svelte's scoping, consistent with the global keyframe above. */
|
|
304
|
+
:global(.blocks-toast-progress-bar) {
|
|
305
|
+
animation: blocks-toast-progress var(--toast-progress-duration) linear forwards;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/* A countdown bar carries no information once it can't animate, so under
|
|
309
|
+
reduced motion we hide it outright rather than leave it frozen. This mirrors
|
|
310
|
+
interaction.css collapsing the motion tokens; the auto-dismiss timing itself
|
|
311
|
+
is unaffected. */
|
|
312
|
+
@media (prefers-reduced-motion: reduce) {
|
|
313
|
+
:global(.blocks-toast-progress-bar) {
|
|
314
|
+
display: none;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
243
317
|
</style>
|
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
preset,
|
|
30
30
|
withBorder = false,
|
|
31
31
|
'aria-describedby': ariaDescribedby,
|
|
32
|
+
'aria-label': ariaLabel,
|
|
32
33
|
...restProps
|
|
33
34
|
}: ToggleProps = $props();
|
|
34
35
|
|
|
@@ -110,7 +111,11 @@
|
|
|
110
111
|
: styles.control({ class: slotClasses?.control })}
|
|
111
112
|
for={ff.fieldId}
|
|
112
113
|
>
|
|
114
|
+
<!-- aria-label: a visible `label` wins (stays undefined); with no label a
|
|
115
|
+
consumer-supplied aria-label is preferred over the generic i18n fallback
|
|
116
|
+
so external labelling survives the restProps-first spread. -->
|
|
113
117
|
<input
|
|
118
|
+
{...restProps}
|
|
114
119
|
id={ff.fieldId}
|
|
115
120
|
type="checkbox"
|
|
116
121
|
role="switch"
|
|
@@ -121,11 +126,10 @@
|
|
|
121
126
|
{required}
|
|
122
127
|
class="peer sr-only"
|
|
123
128
|
aria-checked={checked}
|
|
124
|
-
aria-label={label ? undefined : bt('accessibility.toggle') || 'Toggle'}
|
|
129
|
+
aria-label={label ? undefined : (ariaLabel ?? (bt('accessibility.toggle') || 'Toggle'))}
|
|
125
130
|
aria-describedby={describedBy}
|
|
126
131
|
aria-invalid={ff.invalid ? 'true' : undefined}
|
|
127
132
|
onchange={handleChange}
|
|
128
|
-
{...restProps}
|
|
129
133
|
/>
|
|
130
134
|
|
|
131
135
|
<span
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/blocks",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.24.0",
|
|
4
4
|
"description": "Svelte 5 UI component library with Tailwind CSS 4, OKLCH design tokens and zero runtime dependencies",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -91,8 +91,8 @@
|
|
|
91
91
|
"@sveltejs/package": "^2.5.8",
|
|
92
92
|
"@sveltejs/vite-plugin-svelte": "^7.0.0",
|
|
93
93
|
"@tailwindcss/vite": "^4.3.1",
|
|
94
|
-
"@urbicon-ui/i18n": "6.
|
|
95
|
-
"@urbicon-ui/shared-types": "6.
|
|
94
|
+
"@urbicon-ui/i18n": "6.24.0",
|
|
95
|
+
"@urbicon-ui/shared-types": "6.24.0",
|
|
96
96
|
"prettier": "^3.8.4",
|
|
97
97
|
"prettier-plugin-svelte": "^4.1.1",
|
|
98
98
|
"prettier-plugin-tailwindcss": "^0.8.0",
|