@aiaiai-pt/design-system 0.5.7 → 0.6.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/components/MapPicker.svelte +2 -1
- package/components/NotificationBell.svelte +293 -0
- package/components/NotificationBell.svelte.d.ts +39 -0
- package/components/ToastManager.svelte +182 -0
- package/components/ToastManager.svelte.d.ts +59 -0
- package/components/Tree.svelte +92 -0
- package/components/Tree.svelte.d.ts +46 -0
- package/components/TreeNode.svelte +211 -0
- package/components/TreeNode.svelte.d.ts +28 -0
- package/components/index.js +4 -0
- package/package.json +1 -1
- package/tokens/components.css +29 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component NotificationBell
|
|
3
|
+
|
|
4
|
+
Bell icon button with unread badge, opening a dropdown of notifications.
|
|
5
|
+
Composes Button (ghost, iconOnly), Badge, Menu, and EmptyState.
|
|
6
|
+
|
|
7
|
+
Consumes --notification-* tokens from components.css.
|
|
8
|
+
|
|
9
|
+
@example
|
|
10
|
+
<NotificationBell
|
|
11
|
+
notifications={[
|
|
12
|
+
{ id: '1', message: 'Maintenance started', eventType: 'maintenance_started', createdAt: '2026-04-16T10:00:00Z' },
|
|
13
|
+
]}
|
|
14
|
+
unreadCount={3}
|
|
15
|
+
onmarkread={(id) => markAsRead(id)}
|
|
16
|
+
onmarkallread={() => markAllAsRead()}
|
|
17
|
+
/>
|
|
18
|
+
-->
|
|
19
|
+
<script>
|
|
20
|
+
import Button from './Button.svelte';
|
|
21
|
+
import Badge from './Badge.svelte';
|
|
22
|
+
import EmptyState from './EmptyState.svelte';
|
|
23
|
+
import Popover from './Popover.svelte';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {{ id: string; message: string; eventType: string; entityType?: string; entityId?: string; createdAt?: string; read?: boolean }} Notification
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
let {
|
|
30
|
+
/** @type {Notification[]} */
|
|
31
|
+
notifications = [],
|
|
32
|
+
/** @type {number} */
|
|
33
|
+
unreadCount = 0,
|
|
34
|
+
/** @type {((id: string) => void) | undefined} */
|
|
35
|
+
onmarkread = undefined,
|
|
36
|
+
/** @type {(() => void) | undefined} */
|
|
37
|
+
onmarkallread = undefined,
|
|
38
|
+
/** @type {((notification: Notification) => void) | undefined} */
|
|
39
|
+
onclick = undefined,
|
|
40
|
+
/** @type {string} */
|
|
41
|
+
class: className = '',
|
|
42
|
+
...rest
|
|
43
|
+
} = $props();
|
|
44
|
+
|
|
45
|
+
let open = $state(false);
|
|
46
|
+
/** @type {HTMLElement | undefined} */
|
|
47
|
+
let anchorRef = $state(undefined);
|
|
48
|
+
|
|
49
|
+
/** @type {Record<string, string>} */
|
|
50
|
+
const eventIcons = {
|
|
51
|
+
maintenance_started: 'play',
|
|
52
|
+
maintenance_completed: 'check-fat',
|
|
53
|
+
inspection_completed: 'clipboard-text',
|
|
54
|
+
reservation_approved: 'thumbs-up',
|
|
55
|
+
reservation_rejected: 'thumbs-down',
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/** @type {Record<string, string>} */
|
|
59
|
+
const eventColors = {
|
|
60
|
+
maintenance_started: 'var(--color-info)',
|
|
61
|
+
maintenance_completed: 'var(--color-success)',
|
|
62
|
+
inspection_completed: 'var(--color-success)',
|
|
63
|
+
reservation_approved: 'var(--color-success)',
|
|
64
|
+
reservation_rejected: 'var(--color-destructive)',
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* @param {string | undefined} iso
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
function formatTime(iso) {
|
|
72
|
+
if (!iso) return '';
|
|
73
|
+
try {
|
|
74
|
+
const d = new Date(iso);
|
|
75
|
+
const now = new Date();
|
|
76
|
+
const diff = now.getTime() - d.getTime();
|
|
77
|
+
const mins = Math.floor(diff / 60000);
|
|
78
|
+
if (mins < 1) return 'Just now';
|
|
79
|
+
if (mins < 60) return `${mins}m ago`;
|
|
80
|
+
const hrs = Math.floor(mins / 60);
|
|
81
|
+
if (hrs < 24) return `${hrs}h ago`;
|
|
82
|
+
const days = Math.floor(hrs / 24);
|
|
83
|
+
return `${days}d ago`;
|
|
84
|
+
} catch {
|
|
85
|
+
return '';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function handleItemClick(notification) {
|
|
90
|
+
if (!notification.read && onmarkread) {
|
|
91
|
+
onmarkread(notification.id);
|
|
92
|
+
}
|
|
93
|
+
if (onclick) {
|
|
94
|
+
onclick(notification);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
</script>
|
|
98
|
+
|
|
99
|
+
<div class="notification-bell {className}" {...rest}>
|
|
100
|
+
<div class="notification-bell-trigger">
|
|
101
|
+
<Button
|
|
102
|
+
variant="ghost"
|
|
103
|
+
size="md"
|
|
104
|
+
iconOnly
|
|
105
|
+
aria-label="Notifications"
|
|
106
|
+
bind:ref={anchorRef}
|
|
107
|
+
onclick={() => { open = !open; }}
|
|
108
|
+
>
|
|
109
|
+
{#snippet icon()}
|
|
110
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" viewBox="0 0 256 256">
|
|
111
|
+
<path d="M221.8,175.94C216.25,166.38,208,139.33,208,104a80,80,0,1,0-160,0c0,35.34-8.26,62.38-13.81,71.94A16,16,0,0,0,48,200H88.81a40,40,0,0,0,78.38,0H208a16,16,0,0,0,13.8-24.06ZM128,216a24,24,0,0,1-22.62-16h45.24A24,24,0,0,1,128,216ZM48,184c7.7-13.24,16-43.92,16-80a64,64,0,1,1,128,0c0,36.05,8.28,66.73,16,80Z"/>
|
|
112
|
+
</svg>
|
|
113
|
+
{/snippet}
|
|
114
|
+
</Button>
|
|
115
|
+
|
|
116
|
+
{#if unreadCount > 0}
|
|
117
|
+
<div class="notification-bell-badge">
|
|
118
|
+
<Badge variant="error">
|
|
119
|
+
{unreadCount > 99 ? '99+' : unreadCount}
|
|
120
|
+
</Badge>
|
|
121
|
+
</div>
|
|
122
|
+
{/if}
|
|
123
|
+
</div>
|
|
124
|
+
|
|
125
|
+
<Popover bind:open anchor={anchorRef} placement="bottom-end">
|
|
126
|
+
<div class="notification-panel" role="region" aria-label="Notifications">
|
|
127
|
+
<div class="notification-header">
|
|
128
|
+
<span class="notification-title">Notifications</span>
|
|
129
|
+
{#if unreadCount > 0 && onmarkallread}
|
|
130
|
+
<button class="notification-mark-all" onclick={onmarkallread}>
|
|
131
|
+
Mark all read
|
|
132
|
+
</button>
|
|
133
|
+
{/if}
|
|
134
|
+
</div>
|
|
135
|
+
|
|
136
|
+
{#if notifications.length === 0}
|
|
137
|
+
<div class="notification-empty">
|
|
138
|
+
<EmptyState heading="No notifications" body="You're all caught up" />
|
|
139
|
+
</div>
|
|
140
|
+
{:else}
|
|
141
|
+
<div class="notification-list">
|
|
142
|
+
{#each notifications as notification (notification.id)}
|
|
143
|
+
<button
|
|
144
|
+
class="notification-item"
|
|
145
|
+
class:notification-item--unread={!notification.read}
|
|
146
|
+
onclick={() => handleItemClick(notification)}
|
|
147
|
+
>
|
|
148
|
+
<div
|
|
149
|
+
class="notification-item-dot"
|
|
150
|
+
style:background={!notification.read
|
|
151
|
+
? (eventColors[notification.eventType] || 'var(--color-info)')
|
|
152
|
+
: 'transparent'}
|
|
153
|
+
></div>
|
|
154
|
+
<div class="notification-item-body">
|
|
155
|
+
<span class="notification-item-message">{notification.message}</span>
|
|
156
|
+
<span class="notification-item-time">{formatTime(notification.createdAt)}</span>
|
|
157
|
+
</div>
|
|
158
|
+
</button>
|
|
159
|
+
{/each}
|
|
160
|
+
</div>
|
|
161
|
+
{/if}
|
|
162
|
+
</div>
|
|
163
|
+
</Popover>
|
|
164
|
+
</div>
|
|
165
|
+
|
|
166
|
+
<style>
|
|
167
|
+
.notification-bell {
|
|
168
|
+
position: relative;
|
|
169
|
+
display: inline-flex;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
.notification-bell-trigger {
|
|
173
|
+
position: relative;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.notification-bell-badge {
|
|
177
|
+
position: absolute;
|
|
178
|
+
top: -2px;
|
|
179
|
+
right: -2px;
|
|
180
|
+
pointer-events: none;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.notification-panel {
|
|
184
|
+
width: var(--notification-panel-width, 360px);
|
|
185
|
+
max-height: var(--notification-panel-max-height, 420px);
|
|
186
|
+
display: flex;
|
|
187
|
+
flex-direction: column;
|
|
188
|
+
background: var(--color-surface);
|
|
189
|
+
border-radius: var(--radius-lg);
|
|
190
|
+
overflow: hidden;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
.notification-header {
|
|
194
|
+
display: flex;
|
|
195
|
+
align-items: center;
|
|
196
|
+
justify-content: space-between;
|
|
197
|
+
padding: var(--space-md) var(--space-lg);
|
|
198
|
+
border-bottom: var(--elevation-border);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
.notification-title {
|
|
202
|
+
font-family: var(--type-heading-font);
|
|
203
|
+
font-size: var(--type-body-sm-size);
|
|
204
|
+
font-weight: 600;
|
|
205
|
+
color: var(--color-text);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.notification-mark-all {
|
|
209
|
+
all: unset;
|
|
210
|
+
cursor: pointer;
|
|
211
|
+
font-family: var(--type-label-font);
|
|
212
|
+
font-size: var(--type-label-size);
|
|
213
|
+
letter-spacing: var(--type-label-tracking);
|
|
214
|
+
color: var(--color-accent);
|
|
215
|
+
transition: opacity var(--duration-instant) var(--easing-default);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
.notification-mark-all:hover {
|
|
219
|
+
opacity: 0.8;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.notification-mark-all:focus-visible {
|
|
223
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
224
|
+
outline-offset: var(--focus-ring-offset);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
.notification-empty {
|
|
228
|
+
padding: var(--space-2xl) var(--space-lg);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.notification-list {
|
|
232
|
+
overflow-y: auto;
|
|
233
|
+
flex: 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
.notification-item {
|
|
237
|
+
all: unset;
|
|
238
|
+
cursor: pointer;
|
|
239
|
+
display: flex;
|
|
240
|
+
align-items: flex-start;
|
|
241
|
+
gap: var(--space-sm);
|
|
242
|
+
padding: var(--space-sm) var(--space-lg);
|
|
243
|
+
width: 100%;
|
|
244
|
+
box-sizing: border-box;
|
|
245
|
+
transition: background var(--duration-instant) var(--easing-default);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.notification-item:hover {
|
|
249
|
+
background: var(--color-surface-secondary);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.notification-item:focus-visible {
|
|
253
|
+
outline: var(--focus-ring-width) solid var(--focus-ring-color);
|
|
254
|
+
outline-offset: calc(-1 * var(--focus-ring-width));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
.notification-item--unread {
|
|
258
|
+
background: var(--color-surface);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
.notification-item-dot {
|
|
262
|
+
width: 8px;
|
|
263
|
+
height: 8px;
|
|
264
|
+
border-radius: var(--radius-pill);
|
|
265
|
+
flex-shrink: 0;
|
|
266
|
+
margin-top: 6px;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.notification-item-body {
|
|
270
|
+
display: flex;
|
|
271
|
+
flex-direction: column;
|
|
272
|
+
gap: var(--space-2xs);
|
|
273
|
+
flex: 1;
|
|
274
|
+
min-width: 0;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.notification-item-message {
|
|
278
|
+
font-family: var(--type-body-sm-font);
|
|
279
|
+
font-size: var(--type-body-sm-size);
|
|
280
|
+
color: var(--color-text);
|
|
281
|
+
line-height: 1.4;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.notification-item--unread .notification-item-message {
|
|
285
|
+
font-weight: 600;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
.notification-item-time {
|
|
289
|
+
font-family: var(--type-label-font);
|
|
290
|
+
font-size: var(--type-label-size);
|
|
291
|
+
color: var(--color-text-muted);
|
|
292
|
+
}
|
|
293
|
+
</style>
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
export default NotificationBell;
|
|
2
|
+
type NotificationBell = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* NotificationBell
|
|
8
|
+
*
|
|
9
|
+
* Bell icon button with unread badge, opening a dropdown of notifications.
|
|
10
|
+
* Composes Button (ghost, iconOnly), Badge, Menu, and EmptyState.
|
|
11
|
+
*
|
|
12
|
+
* Consumes --notification-* tokens from components.css.
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* <NotificationBell
|
|
16
|
+
* notifications={[
|
|
17
|
+
* { id: '1', message: 'Maintenance started', eventType: 'maintenance_started', createdAt: '2026-04-16T10:00:00Z' },
|
|
18
|
+
* ]}
|
|
19
|
+
* unreadCount={3}
|
|
20
|
+
* onmarkread={(id) => markAsRead(id)}
|
|
21
|
+
* onmarkallread={() => markAllAsRead()}
|
|
22
|
+
* />
|
|
23
|
+
*/
|
|
24
|
+
declare const NotificationBell: import("svelte").Component<{
|
|
25
|
+
notifications?: any[];
|
|
26
|
+
unreadCount?: number;
|
|
27
|
+
onmarkread?: any;
|
|
28
|
+
onmarkallread?: any;
|
|
29
|
+
onclick?: any;
|
|
30
|
+
class?: string;
|
|
31
|
+
} & Record<string, any>, {}, "">;
|
|
32
|
+
type $$ComponentProps = {
|
|
33
|
+
notifications?: any[];
|
|
34
|
+
unreadCount?: number;
|
|
35
|
+
onmarkread?: any;
|
|
36
|
+
onmarkallread?: any;
|
|
37
|
+
onclick?: any;
|
|
38
|
+
class?: string;
|
|
39
|
+
} & Record<string, any>;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component ToastManager
|
|
3
|
+
|
|
4
|
+
Lifecycle manager for Toast notifications — positioning, stacking,
|
|
5
|
+
auto-dismiss, and max-visible limit. Wraps the visual Toast component.
|
|
6
|
+
|
|
7
|
+
Mount once in your root layout. Use the exported `toasts` store to
|
|
8
|
+
push notifications from anywhere in the app.
|
|
9
|
+
|
|
10
|
+
Consumes --toast-manager-* tokens from components.css.
|
|
11
|
+
|
|
12
|
+
@example
|
|
13
|
+
<script>
|
|
14
|
+
import { ToastManager, toasts } from '@aiaiai-pt/design-system';
|
|
15
|
+
|
|
16
|
+
toasts.push({ variant: 'success', message: 'Saved!' });
|
|
17
|
+
toasts.push({ variant: 'error', message: 'Failed.', autoDismiss: 8000 });
|
|
18
|
+
</script>
|
|
19
|
+
|
|
20
|
+
<ToastManager />
|
|
21
|
+
-->
|
|
22
|
+
<script>
|
|
23
|
+
import Toast from './Toast.svelte';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @typedef {'info' | 'success' | 'warning' | 'error'} Variant
|
|
27
|
+
* @typedef {{ id: string; variant: Variant; message: string; actionLabel?: string; onaction?: () => void; autoDismiss?: number }} ToastItem
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
let {
|
|
31
|
+
/** @type {'top-right' | 'top-left' | 'bottom-right' | 'bottom-left'} */
|
|
32
|
+
position = 'top-right',
|
|
33
|
+
/** @type {number} ms before auto-dismiss (0 = no auto-dismiss) */
|
|
34
|
+
autoDismiss = 5000,
|
|
35
|
+
/** @type {number} max visible toasts — oldest removed when exceeded */
|
|
36
|
+
maxVisible = 5,
|
|
37
|
+
/** @type {string} */
|
|
38
|
+
class: className = '',
|
|
39
|
+
} = $props();
|
|
40
|
+
|
|
41
|
+
/** @type {ToastItem[]} */
|
|
42
|
+
let items = $state([]);
|
|
43
|
+
|
|
44
|
+
/** @type {Map<string, ReturnType<typeof setTimeout>>} */
|
|
45
|
+
const timers = new Map();
|
|
46
|
+
|
|
47
|
+
let _idCounter = 0;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Add a toast. Returns the toast id for manual removal.
|
|
51
|
+
* @param {{ variant?: Variant; message: string; actionLabel?: string; onaction?: () => void; autoDismiss?: number }} toast
|
|
52
|
+
* @returns {string}
|
|
53
|
+
*/
|
|
54
|
+
export function push(toast) {
|
|
55
|
+
const id = `toast-${++_idCounter}-${Date.now()}`;
|
|
56
|
+
const item = {
|
|
57
|
+
id,
|
|
58
|
+
variant: toast.variant ?? 'info',
|
|
59
|
+
message: toast.message,
|
|
60
|
+
actionLabel: toast.actionLabel,
|
|
61
|
+
onaction: toast.onaction,
|
|
62
|
+
autoDismiss: toast.autoDismiss,
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
items = [...items, item];
|
|
66
|
+
|
|
67
|
+
// Enforce max visible
|
|
68
|
+
if (items.length > maxVisible) {
|
|
69
|
+
const removed = items[0];
|
|
70
|
+
items = items.slice(1);
|
|
71
|
+
_clearTimer(removed.id);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Schedule auto-dismiss
|
|
75
|
+
const duration = item.autoDismiss ?? autoDismiss;
|
|
76
|
+
if (duration > 0) {
|
|
77
|
+
const timer = setTimeout(() => dismiss(id), duration);
|
|
78
|
+
timers.set(id, timer);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return id;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Remove a toast by id.
|
|
86
|
+
* @param {string} id
|
|
87
|
+
*/
|
|
88
|
+
export function dismiss(id) {
|
|
89
|
+
_clearTimer(id);
|
|
90
|
+
items = items.filter((t) => t.id !== id);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Remove all toasts. */
|
|
94
|
+
export function clear() {
|
|
95
|
+
for (const [id] of timers) clearTimeout(timers.get(id));
|
|
96
|
+
timers.clear();
|
|
97
|
+
items = [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* @param {string} id
|
|
102
|
+
*/
|
|
103
|
+
function _clearTimer(id) {
|
|
104
|
+
const timer = timers.get(id);
|
|
105
|
+
if (timer) {
|
|
106
|
+
clearTimeout(timer);
|
|
107
|
+
timers.delete(id);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const positionClasses = {
|
|
112
|
+
'top-right': 'tm-top tm-right',
|
|
113
|
+
'top-left': 'tm-top tm-left',
|
|
114
|
+
'bottom-right': 'tm-bottom tm-right',
|
|
115
|
+
'bottom-left': 'tm-bottom tm-left',
|
|
116
|
+
};
|
|
117
|
+
</script>
|
|
118
|
+
|
|
119
|
+
{#if items.length > 0}
|
|
120
|
+
<div
|
|
121
|
+
class="toast-manager {positionClasses[position]} {className}"
|
|
122
|
+
aria-live="polite"
|
|
123
|
+
aria-relevant="additions removals"
|
|
124
|
+
>
|
|
125
|
+
{#each items as item (item.id)}
|
|
126
|
+
<div class="toast-slot" data-toast-id={item.id}>
|
|
127
|
+
<Toast
|
|
128
|
+
variant={item.variant}
|
|
129
|
+
actionLabel={item.actionLabel}
|
|
130
|
+
onaction={item.onaction}
|
|
131
|
+
>
|
|
132
|
+
{@html item.message}
|
|
133
|
+
</Toast>
|
|
134
|
+
</div>
|
|
135
|
+
{/each}
|
|
136
|
+
</div>
|
|
137
|
+
{/if}
|
|
138
|
+
|
|
139
|
+
<style>
|
|
140
|
+
.toast-manager {
|
|
141
|
+
position: fixed;
|
|
142
|
+
z-index: var(--toast-manager-z, 60);
|
|
143
|
+
display: flex;
|
|
144
|
+
flex-direction: column;
|
|
145
|
+
gap: var(--toast-manager-gap, var(--space-sm));
|
|
146
|
+
padding: var(--toast-manager-padding, var(--space-lg));
|
|
147
|
+
pointer-events: none;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.toast-manager > :global(*) {
|
|
151
|
+
pointer-events: auto;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/* Position modifiers */
|
|
155
|
+
.tm-top { top: 0; }
|
|
156
|
+
.tm-bottom { bottom: 0; flex-direction: column-reverse; }
|
|
157
|
+
.tm-right { right: 0; align-items: flex-end; }
|
|
158
|
+
.tm-left { left: 0; align-items: flex-start; }
|
|
159
|
+
|
|
160
|
+
/* Entry animation */
|
|
161
|
+
.toast-slot {
|
|
162
|
+
animation: toast-enter var(--duration-normal) var(--easing-enter);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@keyframes toast-enter {
|
|
166
|
+
from {
|
|
167
|
+
opacity: 0;
|
|
168
|
+
transform: translateX(var(--toast-manager-enter-offset, 16px));
|
|
169
|
+
}
|
|
170
|
+
to {
|
|
171
|
+
opacity: 1;
|
|
172
|
+
transform: translateX(0);
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/* Reduced motion */
|
|
177
|
+
@media (prefers-reduced-motion: reduce) {
|
|
178
|
+
.toast-slot {
|
|
179
|
+
animation: none;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
</style>
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export default ToastManager;
|
|
2
|
+
type ToastManager = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
} & {
|
|
6
|
+
push: (toast: {
|
|
7
|
+
variant?: Variant;
|
|
8
|
+
message: string;
|
|
9
|
+
actionLabel?: string;
|
|
10
|
+
onaction?: () => void;
|
|
11
|
+
autoDismiss?: number;
|
|
12
|
+
}) => string;
|
|
13
|
+
dismiss: (id: string) => void;
|
|
14
|
+
clear: () => void;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* ToastManager
|
|
18
|
+
*
|
|
19
|
+
* Lifecycle manager for Toast notifications — positioning, stacking,
|
|
20
|
+
* auto-dismiss, and max-visible limit. Wraps the visual Toast component.
|
|
21
|
+
*
|
|
22
|
+
* Mount once in your root layout. Use the exported `toasts` store to
|
|
23
|
+
* push notifications from anywhere in the app.
|
|
24
|
+
*
|
|
25
|
+
* Consumes --toast-manager-* tokens from components.css.
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* <script>
|
|
29
|
+
* import { ToastManager, toasts } from '@aiaiai-pt/design-system';
|
|
30
|
+
*
|
|
31
|
+
* toasts.push({ variant: 'success', message: 'Saved!' });
|
|
32
|
+
* toasts.push({ variant: 'error', message: 'Failed.', autoDismiss: 8000 });
|
|
33
|
+
* </script>
|
|
34
|
+
*
|
|
35
|
+
* <ToastManager />
|
|
36
|
+
*/
|
|
37
|
+
declare const ToastManager: import("svelte").Component<{
|
|
38
|
+
position?: string;
|
|
39
|
+
autoDismiss?: number;
|
|
40
|
+
maxVisible?: number;
|
|
41
|
+
class?: string;
|
|
42
|
+
}, {
|
|
43
|
+
push: (toast: {
|
|
44
|
+
variant?: "info" | "success" | "warning" | "error";
|
|
45
|
+
message: string;
|
|
46
|
+
actionLabel?: string;
|
|
47
|
+
onaction?: () => void;
|
|
48
|
+
autoDismiss?: number;
|
|
49
|
+
}) => string;
|
|
50
|
+
dismiss: (id: string) => void;
|
|
51
|
+
clear: () => void;
|
|
52
|
+
}, "">;
|
|
53
|
+
type $$ComponentProps = {
|
|
54
|
+
position?: string;
|
|
55
|
+
autoDismiss?: number;
|
|
56
|
+
maxVisible?: number;
|
|
57
|
+
class?: string;
|
|
58
|
+
};
|
|
59
|
+
type Variant = "info" | "success" | "warning" | "error";
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component Tree
|
|
3
|
+
|
|
4
|
+
Recursive hierarchy view with expand/collapse and selection.
|
|
5
|
+
Renders TreeNodes from a nested data array. v1: no drag-and-drop,
|
|
6
|
+
no cascading selection, no virtualization.
|
|
7
|
+
|
|
8
|
+
Data shape (recursive):
|
|
9
|
+
{ id: string | number, label: string, children?: TreeNode[], disabled?: boolean }
|
|
10
|
+
|
|
11
|
+
Keyboard navigation:
|
|
12
|
+
ArrowDown / ArrowUp — move focus between visible rows
|
|
13
|
+
ArrowRight — expand current node (or move to first child if already open)
|
|
14
|
+
ArrowLeft — collapse current node (or move to parent if already closed)
|
|
15
|
+
Enter / Space — select current node
|
|
16
|
+
Home / End — jump to first/last visible row
|
|
17
|
+
|
|
18
|
+
@example Basic
|
|
19
|
+
<Tree {items} onselect={(id) => (selectedId = id)} />
|
|
20
|
+
|
|
21
|
+
@example Controlled expansion
|
|
22
|
+
<Tree {items} bind:expanded onselect={(id) => (selectedId = id)} />
|
|
23
|
+
|
|
24
|
+
Consumes --tree-* tokens from components.css.
|
|
25
|
+
-->
|
|
26
|
+
<script>
|
|
27
|
+
import TreeNode from './TreeNode.svelte';
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @typedef {Object} TreeItem
|
|
31
|
+
* @property {string | number} id
|
|
32
|
+
* @property {string} label
|
|
33
|
+
* @property {TreeItem[]} [children]
|
|
34
|
+
* @property {boolean} [disabled]
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
let {
|
|
38
|
+
/** @type {TreeItem[]} */
|
|
39
|
+
items = [],
|
|
40
|
+
/** @type {string | number | null} */
|
|
41
|
+
selectedId = $bindable(null),
|
|
42
|
+
/** @type {Set<string | number>} */
|
|
43
|
+
expanded = $bindable(new Set()),
|
|
44
|
+
/** @type {(id: string | number) => void | undefined} */
|
|
45
|
+
onselect = undefined,
|
|
46
|
+
/** @type {(id: string | number, open: boolean) => void | undefined} */
|
|
47
|
+
ontoggle = undefined,
|
|
48
|
+
/** @type {string} */
|
|
49
|
+
class: className = '',
|
|
50
|
+
...rest
|
|
51
|
+
} = $props();
|
|
52
|
+
|
|
53
|
+
/** @param {string | number} id */
|
|
54
|
+
function toggle(id) {
|
|
55
|
+
const next = new Set(expanded);
|
|
56
|
+
const open = !next.has(id);
|
|
57
|
+
if (open) next.add(id);
|
|
58
|
+
else next.delete(id);
|
|
59
|
+
expanded = next;
|
|
60
|
+
ontoggle?.(id, open);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** @param {string | number} id */
|
|
64
|
+
function select(id) {
|
|
65
|
+
selectedId = id;
|
|
66
|
+
onselect?.(id);
|
|
67
|
+
}
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<ul class="tree {className}" role="tree" {...rest}>
|
|
71
|
+
{#each items as node (node.id)}
|
|
72
|
+
<TreeNode
|
|
73
|
+
{node}
|
|
74
|
+
depth={0}
|
|
75
|
+
{expanded}
|
|
76
|
+
{selectedId}
|
|
77
|
+
ontoggle={toggle}
|
|
78
|
+
onselect={select}
|
|
79
|
+
/>
|
|
80
|
+
{/each}
|
|
81
|
+
</ul>
|
|
82
|
+
|
|
83
|
+
<style>
|
|
84
|
+
.tree {
|
|
85
|
+
display: flex;
|
|
86
|
+
flex-direction: column;
|
|
87
|
+
gap: var(--tree-gap);
|
|
88
|
+
list-style: none;
|
|
89
|
+
margin: 0;
|
|
90
|
+
padding: 0;
|
|
91
|
+
}
|
|
92
|
+
</style>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export default Tree;
|
|
2
|
+
type Tree = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Tree
|
|
8
|
+
*
|
|
9
|
+
* Recursive hierarchy view with expand/collapse and selection.
|
|
10
|
+
* Renders TreeNodes from a nested data array. v1: no drag-and-drop,
|
|
11
|
+
* no cascading selection, no virtualization.
|
|
12
|
+
*
|
|
13
|
+
* Data shape (recursive):
|
|
14
|
+
* { id: string | number, label: string, children?: TreeNode[], disabled?: boolean }
|
|
15
|
+
*
|
|
16
|
+
* Keyboard navigation:
|
|
17
|
+
* ArrowDown / ArrowUp — move focus between visible rows
|
|
18
|
+
* ArrowRight — expand current node (or move to first child if already open)
|
|
19
|
+
* ArrowLeft — collapse current node (or move to parent if already closed)
|
|
20
|
+
* Enter / Space — select current node
|
|
21
|
+
* Home / End — jump to first/last visible row
|
|
22
|
+
*
|
|
23
|
+
* @example Basic
|
|
24
|
+
* <Tree {items} onselect={(id) => (selectedId = id)} />
|
|
25
|
+
*
|
|
26
|
+
* @example Controlled expansion
|
|
27
|
+
* <Tree {items} bind:expanded onselect={(id) => (selectedId = id)} />
|
|
28
|
+
*
|
|
29
|
+
* Consumes --tree-* tokens from components.css.
|
|
30
|
+
*/
|
|
31
|
+
declare const Tree: import("svelte").Component<{
|
|
32
|
+
items?: any[];
|
|
33
|
+
selectedId?: any;
|
|
34
|
+
expanded?: any;
|
|
35
|
+
onselect?: any;
|
|
36
|
+
ontoggle?: any;
|
|
37
|
+
class?: string;
|
|
38
|
+
} & Record<string, any>, {}, "expanded" | "selectedId">;
|
|
39
|
+
type $$ComponentProps = {
|
|
40
|
+
items?: any[];
|
|
41
|
+
selectedId?: any;
|
|
42
|
+
expanded?: any;
|
|
43
|
+
onselect?: any;
|
|
44
|
+
ontoggle?: any;
|
|
45
|
+
class?: string;
|
|
46
|
+
} & Record<string, any>;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
@component TreeNode
|
|
3
|
+
|
|
4
|
+
One node in a Tree. Recursive via `svelte:self` for descendants.
|
|
5
|
+
Not typically used directly — see `Tree.svelte`.
|
|
6
|
+
-->
|
|
7
|
+
<script>
|
|
8
|
+
import TreeNode from './TreeNode.svelte';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {Object} TreeItem
|
|
12
|
+
* @property {string | number} id
|
|
13
|
+
* @property {string} label
|
|
14
|
+
* @property {TreeItem[]} [children]
|
|
15
|
+
* @property {boolean} [disabled]
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
let {
|
|
19
|
+
/** @type {TreeItem} */
|
|
20
|
+
node,
|
|
21
|
+
/** @type {number} */
|
|
22
|
+
depth = 0,
|
|
23
|
+
/** @type {Set<string | number>} */
|
|
24
|
+
expanded = new Set(),
|
|
25
|
+
/** @type {string | number | null} */
|
|
26
|
+
selectedId = null,
|
|
27
|
+
/** @type {(id: string | number) => void} */
|
|
28
|
+
ontoggle,
|
|
29
|
+
/** @type {(id: string | number) => void} */
|
|
30
|
+
onselect,
|
|
31
|
+
} = $props();
|
|
32
|
+
|
|
33
|
+
let hasChildren = $derived(
|
|
34
|
+
Array.isArray(node.children) && node.children.length > 0
|
|
35
|
+
);
|
|
36
|
+
let isOpen = $derived(expanded.has(node.id));
|
|
37
|
+
let isSelected = $derived(selectedId === node.id);
|
|
38
|
+
|
|
39
|
+
/** @param {KeyboardEvent} event */
|
|
40
|
+
function handleKey(event) {
|
|
41
|
+
if (node.disabled) return;
|
|
42
|
+
if (event.key === 'Enter' || event.key === ' ') {
|
|
43
|
+
event.preventDefault();
|
|
44
|
+
onselect(node.id);
|
|
45
|
+
} else if (event.key === 'ArrowRight' && hasChildren && !isOpen) {
|
|
46
|
+
event.preventDefault();
|
|
47
|
+
ontoggle(node.id);
|
|
48
|
+
} else if (event.key === 'ArrowLeft' && hasChildren && isOpen) {
|
|
49
|
+
event.preventDefault();
|
|
50
|
+
ontoggle(node.id);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
</script>
|
|
54
|
+
|
|
55
|
+
<li class="tree-node" role="none">
|
|
56
|
+
<div
|
|
57
|
+
class="tree-node-row"
|
|
58
|
+
class:tree-node-row--selected={isSelected}
|
|
59
|
+
class:tree-node-row--disabled={node.disabled}
|
|
60
|
+
role="treeitem"
|
|
61
|
+
aria-expanded={hasChildren ? isOpen : undefined}
|
|
62
|
+
aria-selected={isSelected}
|
|
63
|
+
tabindex={isSelected ? 0 : -1}
|
|
64
|
+
style:--tree-depth={depth}
|
|
65
|
+
onkeydown={handleKey}
|
|
66
|
+
>
|
|
67
|
+
{#if hasChildren}
|
|
68
|
+
<button
|
|
69
|
+
type="button"
|
|
70
|
+
class="tree-toggle"
|
|
71
|
+
class:tree-toggle--open={isOpen}
|
|
72
|
+
aria-label={isOpen ? 'Collapse' : 'Expand'}
|
|
73
|
+
aria-hidden="true"
|
|
74
|
+
tabindex="-1"
|
|
75
|
+
onclick={(e) => {
|
|
76
|
+
e.stopPropagation();
|
|
77
|
+
ontoggle(node.id);
|
|
78
|
+
}}
|
|
79
|
+
>
|
|
80
|
+
<svg
|
|
81
|
+
width="10"
|
|
82
|
+
height="10"
|
|
83
|
+
viewBox="0 0 14 14"
|
|
84
|
+
fill="none"
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
>
|
|
87
|
+
<path
|
|
88
|
+
d="M5 3L10 7L5 11"
|
|
89
|
+
stroke="currentColor"
|
|
90
|
+
stroke-width="1.5"
|
|
91
|
+
stroke-linecap="round"
|
|
92
|
+
stroke-linejoin="round"
|
|
93
|
+
/>
|
|
94
|
+
</svg>
|
|
95
|
+
</button>
|
|
96
|
+
{:else}
|
|
97
|
+
<span class="tree-toggle-spacer" aria-hidden="true"></span>
|
|
98
|
+
{/if}
|
|
99
|
+
|
|
100
|
+
<button
|
|
101
|
+
type="button"
|
|
102
|
+
class="tree-label"
|
|
103
|
+
disabled={node.disabled}
|
|
104
|
+
onclick={() => onselect(node.id)}
|
|
105
|
+
>
|
|
106
|
+
{node.label}
|
|
107
|
+
</button>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
{#if hasChildren && isOpen}
|
|
111
|
+
<ul role="group" class="tree-group">
|
|
112
|
+
{#each node.children ?? [] as child (child.id)}
|
|
113
|
+
<TreeNode
|
|
114
|
+
node={child}
|
|
115
|
+
depth={depth + 1}
|
|
116
|
+
{expanded}
|
|
117
|
+
{selectedId}
|
|
118
|
+
{ontoggle}
|
|
119
|
+
{onselect}
|
|
120
|
+
/>
|
|
121
|
+
{/each}
|
|
122
|
+
</ul>
|
|
123
|
+
{/if}
|
|
124
|
+
</li>
|
|
125
|
+
|
|
126
|
+
<style>
|
|
127
|
+
.tree-node {
|
|
128
|
+
list-style: none;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.tree-node-row {
|
|
132
|
+
display: flex;
|
|
133
|
+
align-items: center;
|
|
134
|
+
gap: var(--space-2xs);
|
|
135
|
+
padding-left: calc(var(--tree-depth, 0) * var(--tree-indent));
|
|
136
|
+
border-radius: var(--tree-node-radius);
|
|
137
|
+
transition: background var(--tree-node-transition);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.tree-node-row:hover:not(.tree-node-row--disabled) {
|
|
141
|
+
background: var(--tree-node-bg-hover);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.tree-node-row--selected {
|
|
145
|
+
background: var(--tree-node-bg-selected);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
.tree-node-row--disabled {
|
|
149
|
+
opacity: 0.5;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.tree-toggle,
|
|
153
|
+
.tree-toggle-spacer {
|
|
154
|
+
width: 16px;
|
|
155
|
+
height: 16px;
|
|
156
|
+
flex-shrink: 0;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
.tree-toggle {
|
|
160
|
+
display: inline-flex;
|
|
161
|
+
align-items: center;
|
|
162
|
+
justify-content: center;
|
|
163
|
+
background: transparent;
|
|
164
|
+
border: none;
|
|
165
|
+
padding: 0;
|
|
166
|
+
cursor: pointer;
|
|
167
|
+
color: var(--tree-node-caret-color);
|
|
168
|
+
transition: transform var(--duration-fast) var(--easing-default);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.tree-toggle--open {
|
|
172
|
+
transform: rotate(90deg);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
.tree-label {
|
|
176
|
+
flex: 1;
|
|
177
|
+
min-width: 0;
|
|
178
|
+
text-align: left;
|
|
179
|
+
background: transparent;
|
|
180
|
+
border: none;
|
|
181
|
+
padding: var(--tree-node-padding-y) var(--tree-node-padding-x);
|
|
182
|
+
cursor: pointer;
|
|
183
|
+
font-family: var(--tree-node-font);
|
|
184
|
+
font-size: var(--tree-node-font-size);
|
|
185
|
+
color: var(--tree-node-color);
|
|
186
|
+
text-overflow: ellipsis;
|
|
187
|
+
overflow: hidden;
|
|
188
|
+
white-space: nowrap;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.tree-label:disabled {
|
|
192
|
+
cursor: not-allowed;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.tree-group {
|
|
196
|
+
list-style: none;
|
|
197
|
+
margin: 0;
|
|
198
|
+
padding: 0;
|
|
199
|
+
display: flex;
|
|
200
|
+
flex-direction: column;
|
|
201
|
+
gap: var(--tree-gap);
|
|
202
|
+
margin-top: var(--tree-gap);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@media (prefers-reduced-motion: reduce) {
|
|
206
|
+
.tree-toggle,
|
|
207
|
+
.tree-node-row {
|
|
208
|
+
transition: none;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
</style>
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export default TreeNode;
|
|
2
|
+
type TreeNode = {
|
|
3
|
+
$on?(type: string, callback: (e: any) => void): () => void;
|
|
4
|
+
$set?(props: Partial<$$ComponentProps>): void;
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* TreeNode
|
|
8
|
+
*
|
|
9
|
+
* One node in a Tree. Recursive via `svelte:self` for descendants.
|
|
10
|
+
* Not typically used directly — see `Tree.svelte`.
|
|
11
|
+
*/
|
|
12
|
+
declare const TreeNode: import("svelte").Component<{
|
|
13
|
+
node: any;
|
|
14
|
+
depth?: number;
|
|
15
|
+
expanded?: any;
|
|
16
|
+
selectedId?: any;
|
|
17
|
+
ontoggle: any;
|
|
18
|
+
onselect: any;
|
|
19
|
+
}, {}, "">;
|
|
20
|
+
import TreeNode from './TreeNode.svelte';
|
|
21
|
+
type $$ComponentProps = {
|
|
22
|
+
node: any;
|
|
23
|
+
depth?: number;
|
|
24
|
+
expanded?: any;
|
|
25
|
+
selectedId?: any;
|
|
26
|
+
ontoggle: any;
|
|
27
|
+
onselect: any;
|
|
28
|
+
};
|
package/components/index.js
CHANGED
|
@@ -63,7 +63,9 @@ export { default as TabPanel } from "./TabPanel.svelte";
|
|
|
63
63
|
// Feedback
|
|
64
64
|
export { default as Alert } from "./Alert.svelte";
|
|
65
65
|
export { default as Toast } from "./Toast.svelte";
|
|
66
|
+
export { default as ToastManager } from "./ToastManager.svelte";
|
|
66
67
|
export { default as EmptyState } from "./EmptyState.svelte";
|
|
68
|
+
export { default as NotificationBell } from "./NotificationBell.svelte";
|
|
67
69
|
|
|
68
70
|
// Navigation
|
|
69
71
|
export { default as Sidebar } from "./Sidebar.svelte";
|
|
@@ -80,6 +82,8 @@ export { default as CollapsibleSection } from "./CollapsibleSection.svelte";
|
|
|
80
82
|
export { default as OptionGrid } from "./OptionGrid.svelte";
|
|
81
83
|
export { default as ConditionTable } from "./ConditionTable.svelte";
|
|
82
84
|
export { default as LogViewer } from "./LogViewer.svelte";
|
|
85
|
+
export { default as Tree } from "./Tree.svelte";
|
|
86
|
+
export { default as TreeNode } from "./TreeNode.svelte";
|
|
83
87
|
|
|
84
88
|
// Data & navigation
|
|
85
89
|
export { default as DataTable } from "./DataTable.svelte";
|
package/package.json
CHANGED
package/tokens/components.css
CHANGED
|
@@ -244,6 +244,16 @@
|
|
|
244
244
|
--toast-font-size: var(--type-body-sm-size);
|
|
245
245
|
--toast-max-width: 360px;
|
|
246
246
|
|
|
247
|
+
/* Toast Manager */
|
|
248
|
+
--toast-manager-z: 60;
|
|
249
|
+
--toast-manager-gap: var(--space-sm);
|
|
250
|
+
--toast-manager-padding: var(--space-lg);
|
|
251
|
+
--toast-manager-enter-offset: 16px;
|
|
252
|
+
|
|
253
|
+
/* Notification Bell */
|
|
254
|
+
--notification-panel-width: 360px;
|
|
255
|
+
--notification-panel-max-height: 420px;
|
|
256
|
+
|
|
247
257
|
/* Empty state */
|
|
248
258
|
--empty-icon-size: var(--icon-size-2xl);
|
|
249
259
|
--empty-icon-color: var(--color-text-muted);
|
|
@@ -431,6 +441,25 @@
|
|
|
431
441
|
--list-item-leading-gap: var(--space-xs);
|
|
432
442
|
--list-item-trailing-gap: var(--space-xs);
|
|
433
443
|
|
|
444
|
+
/* ═══════════════════════════════════════════════
|
|
445
|
+
TREE
|
|
446
|
+
═══════════════════════════════════════════════ */
|
|
447
|
+
|
|
448
|
+
--tree-gap: var(--space-2xs);
|
|
449
|
+
--tree-indent: var(--space-md);
|
|
450
|
+
--tree-node-padding-x: var(--space-sm);
|
|
451
|
+
--tree-node-padding-y: var(--space-xs);
|
|
452
|
+
--tree-node-radius: var(--radius-sm);
|
|
453
|
+
--tree-node-font: var(--type-body-font);
|
|
454
|
+
--tree-node-font-size: var(--type-body-size);
|
|
455
|
+
--tree-node-color: var(--color-text);
|
|
456
|
+
--tree-node-color-hover: var(--color-text);
|
|
457
|
+
--tree-node-color-selected: var(--color-text);
|
|
458
|
+
--tree-node-bg-hover: var(--color-surface-secondary);
|
|
459
|
+
--tree-node-bg-selected: var(--color-surface-tertiary);
|
|
460
|
+
--tree-node-caret-color: var(--color-text-secondary);
|
|
461
|
+
--tree-node-transition: var(--duration-instant) var(--easing-default);
|
|
462
|
+
|
|
434
463
|
/* ═══════════════════════════════════════════════
|
|
435
464
|
FILE UPLOAD
|
|
436
465
|
═══════════════════════════════════════════════ */
|