@urbicon-ui/blocks 6.19.3 → 6.21.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/Calendar/Calendar.svelte +3 -0
- package/dist/icons/IconProvider.svelte +3 -0
- package/dist/mint/styles.css +1 -1
- package/dist/primitives/Accordion/Accordion.svelte +3 -0
- package/dist/primitives/Avatar/Avatar.svelte +74 -48
- package/dist/primitives/Avatar/avatar.variants.d.ts +32 -6
- package/dist/primitives/Avatar/avatar.variants.js +43 -18
- package/dist/primitives/Avatar/index.d.ts +6 -1
- package/dist/primitives/Collapsible/Collapsible.svelte +3 -0
- package/dist/primitives/ConfirmDialog/index.d.ts +12 -0
- package/dist/primitives/Dialog/Dialog.svelte +2 -0
- package/dist/primitives/Drawer/Drawer.svelte +3 -1
- package/dist/primitives/Menu/MenuSubmenu.svelte +1 -0
- package/dist/primitives/Select/Select.svelte +1 -1
- package/dist/primitives/Tab/Tab.svelte +3 -0
- package/dist/style/interaction.css +1 -1
- package/dist/system/attachments/ContextIsolation.svelte +2 -0
- package/package.json +3 -3
|
@@ -161,6 +161,9 @@
|
|
|
161
161
|
if (month == null && year == null) return today;
|
|
162
162
|
return new Date(year ?? today.getFullYear(), month ?? today.getMonth(), 1);
|
|
163
163
|
}
|
|
164
|
+
// Uncontrolled seed: capture only the initial default*; later changes to the
|
|
165
|
+
// default* props must not move the user's navigated month.
|
|
166
|
+
// svelte-ignore state_referenced_locally
|
|
164
167
|
let referenceDate = $state(
|
|
165
168
|
resolveInitialReference(value, defaultDate, defaultMonth, defaultYear)
|
|
166
169
|
);
|
|
@@ -4,6 +4,9 @@
|
|
|
4
4
|
|
|
5
5
|
let { icons, children }: { icons: Partial<IconSet>; children: Snippet } = $props();
|
|
6
6
|
|
|
7
|
+
// Provider config is applied once at init — icon overrides are not meant to
|
|
8
|
+
// swap reactively after mount.
|
|
9
|
+
// svelte-ignore state_referenced_locally
|
|
7
10
|
setIcons(icons);
|
|
8
11
|
</script>
|
|
9
12
|
|
package/dist/mint/styles.css
CHANGED
|
@@ -36,6 +36,9 @@
|
|
|
36
36
|
return Array.isArray(v) ? v : [v];
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Uncontrolled seed: capture only the initial `defaultValue`; later changes
|
|
40
|
+
// must not clobber user interaction.
|
|
41
|
+
// svelte-ignore state_referenced_locally
|
|
39
42
|
let internalValue = $state<string[]>(normalise(defaultValue));
|
|
40
43
|
|
|
41
44
|
const openItems = $derived(value !== undefined ? normalise(value) : internalValue);
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
intent = 'neutral',
|
|
18
18
|
status,
|
|
19
19
|
statusPosition = 'bottom-right',
|
|
20
|
+
pulse = false,
|
|
20
21
|
ring = false,
|
|
21
22
|
ringIntent = 'primary',
|
|
22
23
|
ringColor,
|
|
@@ -38,7 +39,6 @@
|
|
|
38
39
|
|
|
39
40
|
let avatarElement = $state<HTMLElement>();
|
|
40
41
|
let imageError = $state(false);
|
|
41
|
-
let isHovered = $state(false);
|
|
42
42
|
|
|
43
43
|
const isInteractive = $derived(clickable || interactive || !!onclick);
|
|
44
44
|
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
intent: randomColor ? undefined : intent,
|
|
92
92
|
status,
|
|
93
93
|
statusPosition,
|
|
94
|
+
pulse,
|
|
94
95
|
ring,
|
|
95
96
|
ringIntent: ringColor ? undefined : ringIntent,
|
|
96
97
|
interactive: isInteractive
|
|
@@ -102,31 +103,20 @@
|
|
|
102
103
|
resolveSlotClasses(blocksConfig, 'Avatar', preset, variantProps, slotClassesProp)
|
|
103
104
|
);
|
|
104
105
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
// ignored it and the avatar fell back to surface-interactive + white
|
|
112
|
-
// text (1.15:1 contrast).
|
|
113
|
-
styles['background-color'] = randomBg;
|
|
114
|
-
styles.color = 'white';
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
if (ringColor) {
|
|
118
|
-
styles['border-color'] = ringColor;
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
return styles;
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
const styleString = $derived(
|
|
125
|
-
Object.entries(dynamicStyles)
|
|
126
|
-
.map(([key, value]) => `${key}: ${value}`)
|
|
127
|
-
.join('; ')
|
|
106
|
+
// `randomColor` paints the visible fill, so it targets the `frame` slot.
|
|
107
|
+
// Inline property names stay kebab-case — a previous camelCase
|
|
108
|
+
// `backgroundColor` was silently ignored by browsers, dropping the avatar back
|
|
109
|
+
// to surface-interactive + white text.
|
|
110
|
+
const frameStyle = $derived(
|
|
111
|
+
randomColor && randomBg ? `background-color: ${randomBg}; color: white` : undefined
|
|
128
112
|
);
|
|
129
113
|
|
|
114
|
+
// `ringColor` recolours the ring on the outer `base`. The ring is a
|
|
115
|
+
// Tailwind `ring-2` box-shadow, so it reads `--tw-ring-color` — setting
|
|
116
|
+
// `border-color` (the previous approach) was a no-op, as the avatar has no
|
|
117
|
+
// border. Only meaningful together with `ring`.
|
|
118
|
+
const baseStyle = $derived(ringColor ? `--tw-ring-color: ${ringColor}` : undefined);
|
|
119
|
+
|
|
130
120
|
$effect(() => {
|
|
131
121
|
if (avatarElement && mint && mint !== 'none' && isInteractive) {
|
|
132
122
|
return mintRegistry.apply(avatarElement, mint);
|
|
@@ -134,12 +124,10 @@
|
|
|
134
124
|
});
|
|
135
125
|
|
|
136
126
|
function handleMouseEnter() {
|
|
137
|
-
isHovered = true;
|
|
138
127
|
onHover?.(true);
|
|
139
128
|
}
|
|
140
129
|
|
|
141
130
|
function handleMouseLeave() {
|
|
142
|
-
isHovered = false;
|
|
143
131
|
onHover?.(false);
|
|
144
132
|
}
|
|
145
133
|
|
|
@@ -165,12 +153,13 @@
|
|
|
165
153
|
<div
|
|
166
154
|
bind:this={avatarElement}
|
|
167
155
|
class={[
|
|
156
|
+
'blocks-avatar',
|
|
168
157
|
`blocks-intent-${intent}`,
|
|
169
158
|
unstyled
|
|
170
159
|
? [slotClasses?.base, className].filter(Boolean).join(' ')
|
|
171
160
|
: styles.base({ class: [slotClasses?.base, className] })
|
|
172
161
|
]}
|
|
173
|
-
style={
|
|
162
|
+
style={baseStyle}
|
|
174
163
|
role={isInteractive ? 'button' : undefined}
|
|
175
164
|
tabindex={isInteractive ? 0 : undefined}
|
|
176
165
|
aria-label={isInteractive ? alt || name || bt('accessibility.avatar') : undefined}
|
|
@@ -180,24 +169,31 @@
|
|
|
180
169
|
onkeydown={handleKeydown}
|
|
181
170
|
{...restProps}
|
|
182
171
|
>
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
172
|
+
<!-- `frame` clips the image/initials to the avatar shape; the status dot
|
|
173
|
+
below is a sibling so it renders outside the clip and stays whole. -->
|
|
174
|
+
<div
|
|
175
|
+
class={unstyled ? (slotClasses?.frame ?? '') : styles.frame({ class: slotClasses?.frame })}
|
|
176
|
+
style={frameStyle}
|
|
177
|
+
>
|
|
178
|
+
{#if src && !imageError}
|
|
179
|
+
<img
|
|
180
|
+
{src}
|
|
181
|
+
alt={alt || name || bt('accessibility.avatar')}
|
|
182
|
+
class={unstyled ? (slotClasses?.image ?? '') : styles.image({ class: slotClasses?.image })}
|
|
183
|
+
onerror={handleImageError}
|
|
184
|
+
/>
|
|
185
|
+
{:else if children}
|
|
186
|
+
{@render children()}
|
|
187
|
+
{:else}
|
|
188
|
+
<span
|
|
189
|
+
class={unstyled
|
|
190
|
+
? (slotClasses?.fallback ?? '')
|
|
191
|
+
: styles.fallback({ class: slotClasses?.fallback })}
|
|
192
|
+
>
|
|
193
|
+
{getInitials(name)}
|
|
194
|
+
</span>
|
|
195
|
+
{/if}
|
|
196
|
+
</div>
|
|
201
197
|
|
|
202
198
|
{#if status}
|
|
203
199
|
<span
|
|
@@ -229,14 +225,44 @@
|
|
|
229
225
|
}
|
|
230
226
|
}
|
|
231
227
|
|
|
228
|
+
/* AVT-3: opt-in "live" pulse on the status dot. A radar ring radiates via an
|
|
229
|
+
animated box-shadow — no extra DOM — coloured to match the status through
|
|
230
|
+
the per-status `--blocks-avatar-pulse-color` set in avatar.variants.ts.
|
|
231
|
+
The dot now sits outside the frame's overflow-hidden clip, so the ring is
|
|
232
|
+
never cut off. */
|
|
233
|
+
:global(.blocks-avatar-status-pulse) {
|
|
234
|
+
animation: avatar-status-pulse 1.6s var(--blocks-ease-confident) infinite;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/* The ring must reach its brightest while it is already clear of the dot —
|
|
238
|
+
a shadow at spread 0 hides behind the opaque dot, so a plain from/to fade
|
|
239
|
+
only ever shows the faint, expanded tail. The mid stop lights it up at a
|
|
240
|
+
visible radius; it then keeps growing and fades to nothing. */
|
|
241
|
+
@keyframes avatar-status-pulse {
|
|
242
|
+
0% {
|
|
243
|
+
box-shadow: 0 0 0 0 color-mix(in oklab, var(--blocks-avatar-pulse-color) 0%, transparent);
|
|
244
|
+
}
|
|
245
|
+
35% {
|
|
246
|
+
box-shadow: 0 0 0 0.22rem
|
|
247
|
+
color-mix(in oklab, var(--blocks-avatar-pulse-color) 60%, transparent);
|
|
248
|
+
}
|
|
249
|
+
100% {
|
|
250
|
+
box-shadow: 0 0 0 0.6rem color-mix(in oklab, var(--blocks-avatar-pulse-color) 0%, transparent);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
232
254
|
@media (prefers-reduced-motion: reduce) {
|
|
233
|
-
:global(.blocks-mint-pulse)
|
|
255
|
+
:global(.blocks-mint-pulse),
|
|
256
|
+
:global(.blocks-avatar-status-pulse) {
|
|
234
257
|
animation: none;
|
|
235
258
|
}
|
|
236
259
|
}
|
|
237
260
|
|
|
238
|
-
|
|
239
|
-
|
|
261
|
+
/* High-contrast delineation for the avatar disc. Targets the stable
|
|
262
|
+
`blocks-avatar` root marker exactly — not `[class*='blocks-avatar']`, which
|
|
263
|
+
would also catch the `blocks-avatar-status-pulse` dot and ring it. */
|
|
264
|
+
@media (prefers-contrast: more) {
|
|
265
|
+
:global(.blocks-avatar) {
|
|
240
266
|
outline: 2px solid currentColor;
|
|
241
267
|
outline-offset: -2px;
|
|
242
268
|
}
|
|
@@ -29,35 +29,38 @@ export declare const avatarVariants: (props?: import("../../utils/variants.js").
|
|
|
29
29
|
variant: {
|
|
30
30
|
circle: {
|
|
31
31
|
base: string;
|
|
32
|
+
frame: string;
|
|
32
33
|
image: string;
|
|
33
34
|
};
|
|
34
35
|
rounded: {
|
|
35
36
|
base: string;
|
|
37
|
+
frame: string;
|
|
36
38
|
image: string;
|
|
37
39
|
};
|
|
38
40
|
square: {
|
|
39
41
|
base: string;
|
|
42
|
+
frame: string;
|
|
40
43
|
image: string;
|
|
41
44
|
};
|
|
42
45
|
};
|
|
43
46
|
intent: {
|
|
44
47
|
primary: {
|
|
45
|
-
|
|
48
|
+
frame: string;
|
|
46
49
|
};
|
|
47
50
|
secondary: {
|
|
48
|
-
|
|
51
|
+
frame: string;
|
|
49
52
|
};
|
|
50
53
|
success: {
|
|
51
|
-
|
|
54
|
+
frame: string;
|
|
52
55
|
};
|
|
53
56
|
warning: {
|
|
54
|
-
|
|
57
|
+
frame: string;
|
|
55
58
|
};
|
|
56
59
|
danger: {
|
|
57
|
-
|
|
60
|
+
frame: string;
|
|
58
61
|
};
|
|
59
62
|
neutral: {
|
|
60
|
-
|
|
63
|
+
frame: string;
|
|
61
64
|
};
|
|
62
65
|
};
|
|
63
66
|
status: {
|
|
@@ -88,6 +91,11 @@ export declare const avatarVariants: (props?: import("../../utils/variants.js").
|
|
|
88
91
|
status: string;
|
|
89
92
|
};
|
|
90
93
|
};
|
|
94
|
+
pulse: {
|
|
95
|
+
true: {
|
|
96
|
+
status: string;
|
|
97
|
+
};
|
|
98
|
+
};
|
|
91
99
|
ring: {
|
|
92
100
|
true: {
|
|
93
101
|
base: string;
|
|
@@ -125,6 +133,21 @@ export declare const avatarVariants: (props?: import("../../utils/variants.js").
|
|
|
125
133
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
126
134
|
status?: "offline" | "online" | "away" | "busy" | undefined;
|
|
127
135
|
statusPosition?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | undefined;
|
|
136
|
+
pulse?: boolean | undefined;
|
|
137
|
+
ring?: boolean | undefined;
|
|
138
|
+
ringIntent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
139
|
+
interactive?: boolean | undefined;
|
|
140
|
+
} & {
|
|
141
|
+
class?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
142
|
+
className?: string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | (string | false | /*elided*/ any | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined)[] | null | undefined;
|
|
143
|
+
}) | undefined) => string;
|
|
144
|
+
frame: (props?: ({
|
|
145
|
+
size?: "sm" | "md" | "lg" | "xl" | "xs" | "2xl" | undefined;
|
|
146
|
+
variant?: "rounded" | "circle" | "square" | undefined;
|
|
147
|
+
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
148
|
+
status?: "offline" | "online" | "away" | "busy" | undefined;
|
|
149
|
+
statusPosition?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | undefined;
|
|
150
|
+
pulse?: boolean | undefined;
|
|
128
151
|
ring?: boolean | undefined;
|
|
129
152
|
ringIntent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
130
153
|
interactive?: boolean | undefined;
|
|
@@ -138,6 +161,7 @@ export declare const avatarVariants: (props?: import("../../utils/variants.js").
|
|
|
138
161
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
139
162
|
status?: "offline" | "online" | "away" | "busy" | undefined;
|
|
140
163
|
statusPosition?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | undefined;
|
|
164
|
+
pulse?: boolean | undefined;
|
|
141
165
|
ring?: boolean | undefined;
|
|
142
166
|
ringIntent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
143
167
|
interactive?: boolean | undefined;
|
|
@@ -151,6 +175,7 @@ export declare const avatarVariants: (props?: import("../../utils/variants.js").
|
|
|
151
175
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
152
176
|
status?: "offline" | "online" | "away" | "busy" | undefined;
|
|
153
177
|
statusPosition?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | undefined;
|
|
178
|
+
pulse?: boolean | undefined;
|
|
154
179
|
ring?: boolean | undefined;
|
|
155
180
|
ringIntent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
156
181
|
interactive?: boolean | undefined;
|
|
@@ -164,6 +189,7 @@ export declare const avatarVariants: (props?: import("../../utils/variants.js").
|
|
|
164
189
|
intent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
165
190
|
status?: "offline" | "online" | "away" | "busy" | undefined;
|
|
166
191
|
statusPosition?: "bottom-right" | "top-right" | "bottom-left" | "top-left" | undefined;
|
|
192
|
+
pulse?: boolean | undefined;
|
|
167
193
|
ring?: boolean | undefined;
|
|
168
194
|
ringIntent?: "primary" | "secondary" | "success" | "warning" | "danger" | "neutral" | undefined;
|
|
169
195
|
interactive?: boolean | undefined;
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
import { tv } from '../../utils/variants.js';
|
|
2
2
|
export const avatarVariants = tv({
|
|
3
3
|
slots: {
|
|
4
|
+
// Outer positioning context. Carries sizing, the ring, interactive/mint
|
|
5
|
+
// effects — but NOT `overflow-hidden`, so the status dot (a sibling of
|
|
6
|
+
// `frame`) is never clipped (AVT-1). The radius is mirrored here purely so
|
|
7
|
+
// the ring follows the avatar's shape.
|
|
4
8
|
base: [
|
|
5
|
-
'relative inline-flex
|
|
6
|
-
'
|
|
7
|
-
|
|
9
|
+
'relative inline-flex shrink-0',
|
|
10
|
+
'transition-[color,background-color,border-color,box-shadow,opacity] duration-[var(--blocks-duration-fast)] ease-out'
|
|
11
|
+
],
|
|
12
|
+
// The visible disc. This is the element that clips the image/initials to
|
|
13
|
+
// the avatar shape; the status dot lives outside it so it stays whole.
|
|
14
|
+
frame: [
|
|
15
|
+
'flex items-center justify-center w-full h-full overflow-hidden',
|
|
16
|
+
'font-semibold text-center select-none',
|
|
8
17
|
'bg-surface-interactive text-text-secondary'
|
|
9
18
|
],
|
|
10
19
|
image: ['w-full h-full object-cover'],
|
|
@@ -42,61 +51,68 @@ export const avatarVariants = tv({
|
|
|
42
51
|
// people and stand apart from the commit/modify/contain semantic system —
|
|
43
52
|
// a brand that flattens commit-tier surfaces (e.g. squared pill-buttons)
|
|
44
53
|
// should not also lose circular avatars. The radii below are physical
|
|
45
|
-
// shape tokens, not tier tokens.
|
|
54
|
+
// shape tokens, not tier tokens. `frame` clips the content to the shape;
|
|
55
|
+
// `base` mirrors it so the ring tracks the same silhouette.
|
|
46
56
|
variant: {
|
|
47
57
|
circle: {
|
|
48
58
|
base: 'rounded-full',
|
|
59
|
+
frame: 'rounded-full',
|
|
49
60
|
image: 'rounded-full'
|
|
50
61
|
},
|
|
51
62
|
rounded: {
|
|
52
63
|
// Avatar-specific soft-square. Maps to --radius-xl (foundation scale);
|
|
53
64
|
// brands tune via the foundation token, not via a tier override.
|
|
54
65
|
base: 'rounded-xl',
|
|
66
|
+
frame: 'rounded-xl',
|
|
55
67
|
image: 'rounded-xl'
|
|
56
68
|
},
|
|
57
69
|
square: {
|
|
58
70
|
base: 'rounded-none',
|
|
71
|
+
frame: 'rounded-none',
|
|
59
72
|
image: 'rounded-none'
|
|
60
73
|
}
|
|
61
74
|
},
|
|
62
75
|
intent: {
|
|
63
76
|
primary: {
|
|
64
|
-
|
|
77
|
+
frame: 'bg-primary-subtle text-primary-emphasis'
|
|
65
78
|
},
|
|
66
79
|
secondary: {
|
|
67
|
-
|
|
80
|
+
frame: 'bg-secondary-subtle text-secondary-emphasis'
|
|
68
81
|
},
|
|
69
82
|
success: {
|
|
70
|
-
|
|
83
|
+
frame: 'bg-success-subtle text-success-emphasis'
|
|
71
84
|
},
|
|
72
85
|
warning: {
|
|
73
|
-
|
|
86
|
+
frame: 'bg-warning-subtle text-warning-emphasis'
|
|
74
87
|
},
|
|
75
88
|
danger: {
|
|
76
|
-
|
|
89
|
+
frame: 'bg-danger-subtle text-danger-emphasis'
|
|
77
90
|
},
|
|
78
91
|
neutral: {
|
|
79
|
-
|
|
92
|
+
frame: 'bg-surface-interactive text-text-secondary'
|
|
80
93
|
}
|
|
81
94
|
},
|
|
95
|
+
// Each status also publishes its colour as `--blocks-avatar-pulse-color`
|
|
96
|
+
// so the opt-in `pulse` ring (see Avatar.svelte) radiates in the matching
|
|
97
|
+
// hue without a second colour source.
|
|
82
98
|
status: {
|
|
83
99
|
online: {
|
|
84
|
-
status: 'bg-success'
|
|
100
|
+
status: 'bg-success [--blocks-avatar-pulse-color:var(--color-success)]'
|
|
85
101
|
},
|
|
86
102
|
offline: {
|
|
87
|
-
status: 'bg-text-quaternary'
|
|
103
|
+
status: 'bg-text-quaternary [--blocks-avatar-pulse-color:var(--color-text-quaternary)]'
|
|
88
104
|
},
|
|
89
105
|
away: {
|
|
90
|
-
status: 'bg-warning'
|
|
106
|
+
status: 'bg-warning [--blocks-avatar-pulse-color:var(--color-warning)]'
|
|
91
107
|
},
|
|
92
108
|
busy: {
|
|
93
|
-
status: 'bg-danger'
|
|
109
|
+
status: 'bg-danger [--blocks-avatar-pulse-color:var(--color-danger)]'
|
|
94
110
|
}
|
|
95
111
|
},
|
|
96
|
-
// Status dot is translated out of the avatar's
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
112
|
+
// Status dot is translated out of the avatar's edge so it half-overlaps the
|
|
113
|
+
// corner (AVT-1). Translation magnitude is intentionally fractional — the
|
|
114
|
+
// dot sits half-overlapping the edge, which matches the conventional badge
|
|
115
|
+
// look (Slack, Discord, Linear).
|
|
100
116
|
statusPosition: {
|
|
101
117
|
'bottom-right': {
|
|
102
118
|
status: 'bottom-0 right-0 translate-x-1/4 translate-y-1/4'
|
|
@@ -111,6 +127,14 @@ export const avatarVariants = tv({
|
|
|
111
127
|
status: 'top-0 left-0 -translate-x-1/4 -translate-y-1/4'
|
|
112
128
|
}
|
|
113
129
|
},
|
|
130
|
+
// Opt-in "live" pulse on the status dot — a radar ring that draws the eye to
|
|
131
|
+
// presence changes. Only has a visible effect together with `status`; the
|
|
132
|
+
// animation itself + reduced-motion handling live in Avatar.svelte.
|
|
133
|
+
pulse: {
|
|
134
|
+
true: {
|
|
135
|
+
status: 'blocks-avatar-status-pulse'
|
|
136
|
+
}
|
|
137
|
+
},
|
|
114
138
|
ring: {
|
|
115
139
|
true: {
|
|
116
140
|
base: 'ring-2 ring-offset-2 ring-offset-surface-base'
|
|
@@ -184,6 +208,7 @@ export const avatarVariants = tv({
|
|
|
184
208
|
variant: 'circle',
|
|
185
209
|
intent: 'neutral',
|
|
186
210
|
statusPosition: 'bottom-right',
|
|
211
|
+
pulse: false,
|
|
187
212
|
ring: false,
|
|
188
213
|
ringIntent: 'primary',
|
|
189
214
|
interactive: false
|
|
@@ -17,6 +17,11 @@ import type { AvatarSlots, AvatarVariants } from './avatar.variants.js';
|
|
|
17
17
|
* ```svelte
|
|
18
18
|
* <Avatar name="Jane Smith" randomColor clickable onclick={() => showProfile()} />
|
|
19
19
|
* ```
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* ```svelte
|
|
23
|
+
* <Avatar name="Ada Lovelace" status="online" pulse />
|
|
24
|
+
* ```
|
|
20
25
|
*/
|
|
21
26
|
export interface AvatarProps extends AvatarVariants, Omit<HTMLAttributes<HTMLDivElement>, 'children'> {
|
|
22
27
|
/** Image URL. Falls back to initials or `children` when empty or on load error. */
|
|
@@ -43,7 +48,7 @@ export interface AvatarProps extends AvatarVariants, Omit<HTMLAttributes<HTMLDiv
|
|
|
43
48
|
class?: string;
|
|
44
49
|
/** Remove all default tv classes. */
|
|
45
50
|
unstyled?: boolean;
|
|
46
|
-
/** Per-slot class overrides merged with tv styles. Slots: base | image | fallback | status */
|
|
51
|
+
/** Per-slot class overrides merged with tv styles. Slots: base | frame | image | fallback | status */
|
|
47
52
|
slotClasses?: Partial<Record<AvatarSlots, string>>;
|
|
48
53
|
/**
|
|
49
54
|
* Apply a named preset registered via `<BlocksProvider presets={{ Avatar: {...} }}>`.
|
|
@@ -28,6 +28,9 @@
|
|
|
28
28
|
const blocksConfig = getBlocksConfig();
|
|
29
29
|
const unstyled = $derived(unstyledProp || blocksConfig?.unstyled || false);
|
|
30
30
|
|
|
31
|
+
// Uncontrolled seed: capture only the initial `defaultOpen`; later changes
|
|
32
|
+
// must not clobber user interaction.
|
|
33
|
+
// svelte-ignore state_referenced_locally
|
|
31
34
|
let internalOpen = $state(defaultOpen ?? false);
|
|
32
35
|
const isOpen = $derived(open !== undefined ? open : internalOpen);
|
|
33
36
|
|
|
@@ -74,6 +74,18 @@ export interface ConfirmDialogProps {
|
|
|
74
74
|
closeOnBackdropClick?: boolean;
|
|
75
75
|
/** Whether Escape cancels. @default true */
|
|
76
76
|
closeOnEscape?: boolean;
|
|
77
|
+
/**
|
|
78
|
+
* Override the enter/exit animation duration in milliseconds, forwarded to
|
|
79
|
+
* the underlying {@link Dialog}. Defaults to the overlay token
|
|
80
|
+
* `--blocks-overlay-enter-duration` / `--blocks-overlay-exit-duration`
|
|
81
|
+
* (200ms / 180ms). Respects `prefers-reduced-motion`.
|
|
82
|
+
*/
|
|
83
|
+
transitionDuration?: number;
|
|
84
|
+
/**
|
|
85
|
+
* Override the enter/exit easing function, forwarded to the underlying
|
|
86
|
+
* {@link Dialog}. Defaults to the overlay token easing (`quintOut`).
|
|
87
|
+
*/
|
|
88
|
+
transitionEasing?: (t: number) => number;
|
|
77
89
|
/** Optional richer markup rendered below `description`. */
|
|
78
90
|
children?: Snippet;
|
|
79
91
|
}
|
|
@@ -41,6 +41,8 @@
|
|
|
41
41
|
...restProps
|
|
42
42
|
}: DrawerProps = $props();
|
|
43
43
|
|
|
44
|
+
// One-shot DEV sanity check on the initial close-path configuration.
|
|
45
|
+
// svelte-ignore state_referenced_locally
|
|
44
46
|
if (
|
|
45
47
|
import.meta.env?.DEV &&
|
|
46
48
|
hideCloseButton &&
|
|
@@ -61,7 +63,7 @@
|
|
|
61
63
|
let previouslyFocused: HTMLElement | null = null;
|
|
62
64
|
|
|
63
65
|
const uid = $props.id();
|
|
64
|
-
const titleId = title ? `drawer-title-${uid}` : undefined;
|
|
66
|
+
const titleId = $derived(title ? `drawer-title-${uid}` : undefined);
|
|
65
67
|
const bodyId = `drawer-body-${uid}`;
|
|
66
68
|
const overlayId = `drawer-${uid}`;
|
|
67
69
|
|
|
@@ -63,7 +63,7 @@
|
|
|
63
63
|
// `<Select bind:value={x: T | null}>` narrow correctly. Internally we
|
|
64
64
|
// dispatch through a loose alias so the component code can hand either
|
|
65
65
|
// shape into `onValueChange` without per-branch casts at every call site.
|
|
66
|
-
const dispatchValueChange = onValueChange as ((v: any) => void) | undefined;
|
|
66
|
+
const dispatchValueChange = $derived(onValueChange as ((v: any) => void) | undefined);
|
|
67
67
|
|
|
68
68
|
/** Resolved null option config — `null` when prop is unset or `groups` is in use. */
|
|
69
69
|
const nullOptionConfig = $derived.by(() => {
|
|
@@ -38,6 +38,9 @@
|
|
|
38
38
|
let tabListElement = $state<HTMLDivElement>();
|
|
39
39
|
let indicatorStyle = $state('');
|
|
40
40
|
|
|
41
|
+
// Uncontrolled seed: capture only the initial `defaultValue`; later changes
|
|
42
|
+
// must not clobber user interaction.
|
|
43
|
+
// svelte-ignore state_referenced_locally
|
|
41
44
|
let internalValue = $state(defaultValue || '');
|
|
42
45
|
|
|
43
46
|
const activeValue = $derived(value !== undefined ? value : internalValue);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@urbicon-ui/blocks",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.21.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.21.0",
|
|
95
|
+
"@urbicon-ui/shared-types": "6.21.0",
|
|
96
96
|
"prettier": "^3.8.4",
|
|
97
97
|
"prettier-plugin-svelte": "^4.1.1",
|
|
98
98
|
"prettier-plugin-tailwindcss": "^0.8.0",
|