@joewinke/jatui 0.38.6 → 0.40.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.
Files changed (98) hide show
  1. package/CLAUDE.md +54 -5
  2. package/THIRD_PARTY_NOTICES.md +67 -0
  3. package/dist/actions/scrollEdge.d.ts +55 -0
  4. package/dist/actions/scrollEdge.js +144 -0
  5. package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +1 -1
  6. package/dist/artifact-blocks/blocks/diagram/schema.d.ts +1 -1
  7. package/dist/components/BottomSheet.svelte +7 -6
  8. package/dist/components/BottomSheet.svelte.d.ts +2 -0
  9. package/dist/components/BulkActionBar.svelte +32 -2
  10. package/dist/components/BulkActionBar.svelte.d.ts +5 -1
  11. package/dist/components/CloseButton.svelte +75 -0
  12. package/dist/components/CloseButton.svelte.d.ts +29 -0
  13. package/dist/components/Drawer.svelte +10 -11
  14. package/dist/components/Drawer.svelte.d.ts +2 -8
  15. package/dist/components/FloatingActionBar.svelte +102 -34
  16. package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
  17. package/dist/components/IconButton.svelte +200 -0
  18. package/dist/components/IconButton.svelte.d.ts +46 -0
  19. package/dist/components/LinkButton.svelte +117 -0
  20. package/dist/components/LinkButton.svelte.d.ts +32 -0
  21. package/dist/components/MediaExplorer.svelte +249 -0
  22. package/dist/components/MediaExplorer.svelte.d.ts +37 -0
  23. package/dist/components/MediaLightbox.svelte +2 -7
  24. package/dist/components/Modal.svelte +2 -10
  25. package/dist/components/NotificationCenter.svelte +481 -0
  26. package/dist/components/NotificationCenter.svelte.d.ts +29 -0
  27. package/dist/components/PinInput.svelte +94 -31
  28. package/dist/components/PinInput.svelte.d.ts +4 -0
  29. package/dist/components/SearchDropdown.svelte +54 -8
  30. package/dist/components/SearchDropdown.svelte.d.ts +12 -0
  31. package/dist/components/TextArea.svelte +67 -5
  32. package/dist/components/TextArea.svelte.d.ts +4 -6
  33. package/dist/components/ThemeSelector.svelte +29 -9
  34. package/dist/components/ThemeSelector.svelte.d.ts +6 -0
  35. package/dist/components/ThemeToggle.svelte +28 -3
  36. package/dist/components/ThemeToggle.svelte.d.ts +9 -0
  37. package/dist/components/agent/AgentLog.svelte +217 -0
  38. package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
  39. package/dist/components/agent/AgentLogRow.svelte +281 -0
  40. package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
  41. package/dist/components/agent/AgentTaskList.svelte +607 -0
  42. package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
  43. package/dist/components/agent/AgentThinking.svelte +280 -0
  44. package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
  45. package/dist/components/agent/ComposerGlow.svelte +265 -0
  46. package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
  47. package/dist/components/agent/ContextMeter.svelte +277 -0
  48. package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
  49. package/dist/components/agent/Questionnaire.svelte +767 -0
  50. package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
  51. package/dist/components/agent/ShimmerText.svelte +100 -0
  52. package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
  53. package/dist/components/agent/StreamedText.svelte +135 -0
  54. package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
  55. package/dist/components/agent/agentLog.d.ts +22 -0
  56. package/dist/components/agent/agentLog.js +26 -0
  57. package/dist/components/agent/agentTaskList.d.ts +90 -0
  58. package/dist/components/agent/agentTaskList.js +93 -0
  59. package/dist/components/agent/agentThinking.d.ts +46 -0
  60. package/dist/components/agent/agentThinking.js +78 -0
  61. package/dist/components/agent/contextMeter.d.ts +97 -0
  62. package/dist/components/agent/contextMeter.js +156 -0
  63. package/dist/components/agent/questionnaire.d.ts +104 -0
  64. package/dist/components/agent/questionnaire.js +225 -0
  65. package/dist/components/agent/revealCore.d.ts +74 -0
  66. package/dist/components/agent/revealCore.js +160 -0
  67. package/dist/components/agent/revealTicker.d.ts +34 -0
  68. package/dist/components/agent/revealTicker.js +73 -0
  69. package/dist/components/agent/streamedText.d.ts +28 -0
  70. package/dist/components/agent/streamedText.js +59 -0
  71. package/dist/components/basic/Breadcrumb.svelte +226 -11
  72. package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
  73. package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
  74. package/dist/components/basic/breadcrumbCollapse.js +12 -0
  75. package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
  76. package/dist/components/messaging/MessageInput.svelte +34 -4
  77. package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
  78. package/dist/components/messaging/MessageItem.svelte +2 -2
  79. package/dist/components/notificationCenter.d.ts +65 -0
  80. package/dist/components/notificationCenter.js +110 -0
  81. package/dist/components/progress/Steps.svelte +269 -5
  82. package/dist/components/progress/Steps.svelte.d.ts +16 -1
  83. package/dist/components/progress/stepsSummary.d.ts +14 -0
  84. package/dist/components/progress/stepsSummary.js +32 -0
  85. package/dist/index.d.ts +48 -1
  86. package/dist/index.js +39 -1
  87. package/dist/selection/clickSelection.d.ts +89 -0
  88. package/dist/selection/clickSelection.js +152 -0
  89. package/dist/selection/createClickSelection.d.ts +79 -0
  90. package/dist/selection/createClickSelection.js +134 -0
  91. package/dist/styles/tokens.css +80 -1
  92. package/dist/utils/pinInput.d.ts +33 -0
  93. package/dist/utils/pinInput.js +63 -0
  94. package/dist/utils/textareaRows.d.ts +23 -0
  95. package/dist/utils/textareaRows.js +13 -0
  96. package/dist/utils/themeWipe.d.ts +36 -17
  97. package/dist/utils/themeWipe.js +145 -35
  98. package/package.json +3 -2
@@ -0,0 +1,481 @@
1
+ <!--
2
+ NotificationCenter — inbox panel: header (title, "N unread", Mark all read),
3
+ category tabs with count chips, and a date-grouped, keyboard-navigable list.
4
+
5
+ MIT License — Copyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)
6
+ Adapted from BoardUI's notification-center.tsx: the header / segmented tabs /
7
+ card layout, the item data model (category, status, avatar, actions[]),
8
+ controlled-or-uncontrolled tab, and "an action click marks the item read".
9
+ jatui additions: configurable `tabs`, Today / Yesterday / This week / Earlier
10
+ date headers (notificationCenter.ts), listNav roving focus with Enter → onOpen,
11
+ host callbacks onMarkRead / onMarkAllRead with optimistic local read state,
12
+ an announced unread marker, and the shared Empty / Avatar components.
13
+ -->
14
+ <script lang="ts">
15
+ import { untrack } from 'svelte';
16
+ import { SvelteSet } from 'svelte/reactivity';
17
+ import { createListNav } from '../actions/listNav';
18
+ import Empty from './basic/Empty.svelte';
19
+ import Avatar from './messaging/Avatar.svelte';
20
+ import { formatDateTime, formatRelativeTime } from '../utils/dateFormatters';
21
+ import {
22
+ DEFAULT_NOTIFICATION_TABS,
23
+ filterByTab,
24
+ groupNotificationsByDate,
25
+ isNotificationUnread,
26
+ tabCounts,
27
+ toTimestampMs,
28
+ unreadIds,
29
+ unreadSummary,
30
+ type NotificationAction,
31
+ type NotificationItem,
32
+ type NotificationStatus,
33
+ type NotificationTab
34
+ } from './notificationCenter';
35
+
36
+ interface Props {
37
+ items: NotificationItem[];
38
+ title?: string;
39
+ /** Tabs across the top. Default All / Mentions / System (by `category`). Pass [] to hide. */
40
+ tabs?: NotificationTab[];
41
+ /** Active tab id (bindable). Leave unbound for uncontrolled use with `defaultTab`. */
42
+ tab?: string;
43
+ defaultTab?: string;
44
+ onTabChange?: (tab: string) => void;
45
+ /** Enter on a focused item, or a click on it / its title. Also marks it read. */
46
+ onOpen?: (item: NotificationItem) => void;
47
+ /** Items became read (opened, or an action was used). */
48
+ onMarkRead?: (ids: string[]) => void;
49
+ /** "Mark all read". When omitted, `onMarkRead` receives every unread id instead. */
50
+ onMarkAllRead?: () => void;
51
+ /** Group the list under Today / Yesterday / This week / Earlier. Default true. */
52
+ groupByDate?: boolean;
53
+ /** Reference "now" for the date groups (tests, demos). Default: live clock. */
54
+ now?: Date | number;
55
+ /** Scroll-area cap — CSS length or px number. Default '32rem'. */
56
+ maxHeight?: string | number;
57
+ emptyTitle?: string;
58
+ emptyDescription?: string;
59
+ class?: string;
60
+ }
61
+
62
+ let {
63
+ items,
64
+ title = 'Notifications',
65
+ tabs = DEFAULT_NOTIFICATION_TABS,
66
+ tab = $bindable(),
67
+ defaultTab,
68
+ onTabChange,
69
+ onOpen,
70
+ onMarkRead,
71
+ onMarkAllRead,
72
+ groupByDate = true,
73
+ now,
74
+ maxHeight = '32rem',
75
+ emptyTitle = "You're all caught up",
76
+ emptyDescription = 'New activity will show up here.',
77
+ class: className = ''
78
+ }: Props = $props();
79
+
80
+ const uid = $props.id();
81
+
82
+ // ---- tabs (controlled via bind:tab / tab, or uncontrolled) -------------
83
+ // svelte-ignore state_referenced_locally
84
+ let internalTab = $state(defaultTab ?? tabs[0]?.id ?? 'all');
85
+ const activeTabId = $derived.by(() => {
86
+ const want = tab ?? internalTab;
87
+ return tabs.some((t) => t.id === want) ? want : (tabs[0]?.id ?? want);
88
+ });
89
+ const activeTab = $derived(tabs.find((t) => t.id === activeTabId));
90
+ const counts = $derived(tabCounts(items, tabs));
91
+ const tabEls: HTMLButtonElement[] = $state([]);
92
+
93
+ function selectTab(id: string, focus = false) {
94
+ internalTab = id;
95
+ tab = id;
96
+ onTabChange?.(id);
97
+ if (focus) tabEls[tabs.findIndex((t) => t.id === id)]?.focus();
98
+ }
99
+
100
+ function onTabKeydown(e: KeyboardEvent, i: number) {
101
+ const n = tabs.length;
102
+ let next = -1;
103
+ if (e.key === 'ArrowRight') next = (i + 1) % n;
104
+ else if (e.key === 'ArrowLeft') next = (i - 1 + n) % n;
105
+ else if (e.key === 'Home') next = 0;
106
+ else if (e.key === 'End') next = n - 1;
107
+ if (next < 0) return;
108
+ e.preventDefault();
109
+ selectTab(tabs[next].id, true);
110
+ }
111
+
112
+ // ---- read state (host-owned, optimistic locally) -----------------------
113
+ const localRead = new SvelteSet<string>();
114
+ const unread = $derived(unreadIds(items, localRead));
115
+
116
+ function markRead(item: NotificationItem) {
117
+ if (!isNotificationUnread(item, localRead)) return;
118
+ localRead.add(item.id);
119
+ onMarkRead?.([item.id]);
120
+ }
121
+
122
+ function markAllRead() {
123
+ const ids = unread;
124
+ if (ids.length === 0) return;
125
+ for (const id of ids) localRead.add(id);
126
+ if (onMarkAllRead) onMarkAllRead();
127
+ else onMarkRead?.(ids);
128
+ }
129
+
130
+ // ---- clock: relative times + date buckets refresh once a minute --------
131
+ let tick = $state(Date.now());
132
+ $effect(() => {
133
+ if (now !== undefined) return;
134
+ const t = setInterval(() => (tick = Date.now()), 60_000);
135
+ return () => clearInterval(t);
136
+ });
137
+ const refNow = $derived(now ?? tick);
138
+
139
+ const visible = $derived(filterByTab(items, activeTab));
140
+ const groups = $derived(
141
+ groupByDate
142
+ ? groupNotificationsByDate(visible, refNow)
143
+ : [{ key: 'all', label: '', items: visible }]
144
+ );
145
+ const byId = $derived(new Map(items.map((it) => [it.id, it])));
146
+
147
+ // ---- list keyboard nav (listNav, scoped to the list) --------------------
148
+ let listEl: HTMLElement | null = $state(null);
149
+ let focusedId = $state<string | null>(null);
150
+ const firstId = $derived(groups[0]?.items[0]?.id ?? null);
151
+
152
+ const nav = createListNav({
153
+ getItems: () =>
154
+ listEl ? Array.from(listEl.querySelectorAll<HTMLElement>('[data-nav-id]')) : [],
155
+ onSelect: (el) => {
156
+ const item = byId.get(el.dataset.navId ?? '');
157
+ if (!item) return;
158
+ const link = el.querySelector<HTMLAnchorElement>('a[data-nc-open]');
159
+ if (link) link.click();
160
+ else open(item);
161
+ },
162
+ onFocusChange: (el, _i, id) => {
163
+ focusedId = id;
164
+ // Move DOM focus with the cursor (roving focus) — but never pull it
165
+ // off a control inside the item (an action button the user tabbed to).
166
+ const active = document.activeElement;
167
+ if (el && !el.contains(active) && listEl?.contains(active)) {
168
+ el.focus({ preventScroll: true });
169
+ }
170
+ },
171
+ wraparound: false,
172
+ focusedClass: 'nc-focused'
173
+ });
174
+
175
+ function onListKeydown(e: KeyboardEvent) {
176
+ // Escape belongs to the host (closing a popover, etc.).
177
+ if (e.key === 'Escape') return;
178
+ nav.handleKeydown(e);
179
+ }
180
+
181
+ function onItemFocusIn(id: string) {
182
+ if (nav.focusedId() === id || !listEl) return;
183
+ const els = Array.from(listEl.querySelectorAll<HTMLElement>('[data-nav-id]'));
184
+ const idx = els.findIndex((el) => el.dataset.navId === id);
185
+ if (idx >= 0) nav.focus(idx);
186
+ }
187
+
188
+ $effect(() => {
189
+ // Re-paint the cursor after the list changes (tab switch, new items).
190
+ void groups;
191
+ untrack(() => nav.refresh());
192
+ });
193
+
194
+ function open(item: NotificationItem) {
195
+ markRead(item);
196
+ onOpen?.(item);
197
+ }
198
+
199
+ function onCardClick(e: MouseEvent, item: NotificationItem) {
200
+ const t = e.target as HTMLElement | null;
201
+ if (t?.closest('button, a, input, select, textarea')) return;
202
+ const link = (e.currentTarget as HTMLElement).querySelector<HTMLAnchorElement>('a[data-nc-open]');
203
+ if (link) link.click();
204
+ else open(item);
205
+ }
206
+
207
+ function runAction(action: NotificationAction, item: NotificationItem) {
208
+ markRead(item);
209
+ action.onSelect(item);
210
+ }
211
+
212
+ const actionClass: Record<NonNullable<NotificationAction['variant']>, string> = {
213
+ primary: 'btn-primary',
214
+ secondary: 'btn-soft',
215
+ ghost: 'btn-ghost',
216
+ danger: 'btn-soft btn-error'
217
+ };
218
+
219
+ const statusTone: Record<NotificationStatus | 'neutral', string> = {
220
+ info: 'bg-info/15 text-info',
221
+ success: 'bg-success/15 text-success',
222
+ warning: 'bg-warning/15 text-warning',
223
+ error: 'bg-error/15 text-error',
224
+ neutral: 'bg-base-200 text-base-content/60'
225
+ };
226
+ const statusLabel: Record<NotificationStatus, string> = {
227
+ info: 'Info',
228
+ success: 'Success',
229
+ warning: 'Warning',
230
+ error: 'Error'
231
+ };
232
+
233
+ const cssLen = (v: string | number) => (typeof v === 'number' ? `${v}px` : v);
234
+ const iso = (ts: NotificationItem['timestamp']) => {
235
+ const ms = toTimestampMs(ts);
236
+ return Number.isNaN(ms) ? undefined : new Date(ms).toISOString();
237
+ };
238
+ const asDate = (ts: NotificationItem['timestamp']) => {
239
+ const ms = toTimestampMs(ts);
240
+ return Number.isNaN(ms) ? null : new Date(ms);
241
+ };
242
+ function relTime(ts: NotificationItem['timestamp'], _tick: number) {
243
+ return formatRelativeTime(asDate(ts));
244
+ }
245
+ </script>
246
+
247
+ <section
248
+ class="nc bg-base-100 border-base-300 rounded-box flex w-full max-w-md flex-col overflow-hidden border {className}"
249
+ aria-labelledby="{uid}-title"
250
+ >
251
+ <header class="flex flex-col gap-3 p-4 pb-2">
252
+ <div class="flex items-start justify-between gap-4">
253
+ <div class="flex min-w-0 flex-col gap-0.5">
254
+ <h2 id="{uid}-title" class="text-base-content text-base font-semibold">{title}</h2>
255
+ <p class="text-base-content/60 text-sm" aria-live="polite">{unreadSummary(unread.length)}</p>
256
+ </div>
257
+ <button
258
+ type="button"
259
+ class="btn btn-ghost btn-sm"
260
+ onclick={markAllRead}
261
+ disabled={unread.length === 0}
262
+ >
263
+ Mark all read
264
+ </button>
265
+ </div>
266
+
267
+ {#if tabs.length > 1}
268
+ <div
269
+ role="tablist"
270
+ aria-label="Notification category"
271
+ class="bg-base-200 rounded-field flex w-full gap-1 p-1"
272
+ >
273
+ {#each tabs as t, i (t.id)}
274
+ {@const selected = t.id === activeTabId}
275
+ <button
276
+ bind:this={tabEls[i]}
277
+ type="button"
278
+ role="tab"
279
+ id="{uid}-tab-{t.id}"
280
+ aria-selected={selected}
281
+ aria-controls="{uid}-panel"
282
+ tabindex={selected ? 0 : -1}
283
+ class="nc-tab rounded-field flex flex-1 items-center justify-center gap-1.5 px-2 py-1 text-sm font-medium"
284
+ class:nc-tab-active={selected}
285
+ onclick={() => selectTab(t.id)}
286
+ onkeydown={(e) => onTabKeydown(e, i)}
287
+ >
288
+ {t.label}
289
+ <span
290
+ class="bg-base-content/10 text-base-content/70 rounded-selector min-w-5 px-1 text-xs tabular-nums"
291
+ >{counts[t.id] ?? 0}</span
292
+ >
293
+ </button>
294
+ {/each}
295
+ </div>
296
+ {/if}
297
+ </header>
298
+
299
+ <div class="bg-base-100 p-1.5 pt-0">
300
+ <div
301
+ id="{uid}-panel"
302
+ role={tabs.length > 1 ? 'tabpanel' : undefined}
303
+ aria-labelledby={tabs.length > 1 ? `${uid}-tab-${activeTabId}` : `${uid}-title`}
304
+ class="bg-base-200 rounded-box overflow-y-auto overscroll-contain p-2"
305
+ style:max-height={cssLen(maxHeight)}
306
+ >
307
+ {#if visible.length === 0}
308
+ <Empty title={emptyTitle} description={emptyDescription} glyph="check" size="sm" />
309
+ {:else}
310
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
311
+ <div bind:this={listEl} class="flex flex-col gap-3" onkeydown={onListKeydown}>
312
+ {#each groups as group (group.key)}
313
+ <div class="flex flex-col gap-1.5">
314
+ {#if group.label}
315
+ <h3
316
+ id="{uid}-g-{group.key}"
317
+ class="text-base-content/50 px-1 text-xs font-semibold tracking-wide uppercase"
318
+ >
319
+ {group.label}
320
+ </h3>
321
+ {/if}
322
+ <ul
323
+ class="flex flex-col gap-1.5"
324
+ aria-labelledby={group.label ? `${uid}-g-${group.key}` : `${uid}-title`}
325
+ >
326
+ {#each group.items as item (item.id)}
327
+ {@const isUnread = isNotificationUnread(item, localRead)}
328
+ {@const status = item.status ?? 'neutral'}
329
+ {@const date = asDate(item.timestamp)}
330
+ <li>
331
+ <!-- svelte-ignore a11y_no_noninteractive_tabindex, a11y_click_events_have_key_events, a11y_no_noninteractive_element_interactions -->
332
+ <article
333
+ data-nav-id={item.id}
334
+ tabindex={(focusedId ?? firstId) === item.id ? 0 : -1}
335
+ aria-labelledby="{uid}-i-{item.id}"
336
+ aria-describedby={item.description ? `${uid}-d-${item.id}` : undefined}
337
+ class="nc-item bg-base-100 rounded-box relative flex cursor-pointer gap-3 p-3"
338
+ class:nc-unread={isUnread}
339
+ onfocusin={() => onItemFocusIn(item.id)}
340
+ onclick={(e) => onCardClick(e, item)}
341
+ >
342
+ {#if item.avatar}
343
+ <Avatar
344
+ name={item.avatar.name}
345
+ avatarUrl={item.avatar.src ?? null}
346
+ size="medium"
347
+ />
348
+ {:else}
349
+ <span
350
+ class="flex size-10 shrink-0 items-center justify-center rounded-full {statusTone[status]}"
351
+ >
352
+ <svg
353
+ class="size-5"
354
+ viewBox="0 0 24 24"
355
+ fill="none"
356
+ stroke="currentColor"
357
+ stroke-width="1.8"
358
+ stroke-linecap="round"
359
+ stroke-linejoin="round"
360
+ aria-hidden="true"
361
+ >
362
+ {#if status === 'success'}
363
+ <circle cx="12" cy="12" r="9" /><path d="m8.5 12.5 2.5 2.5 4.5-5" />
364
+ {:else if status === 'error'}
365
+ <circle cx="12" cy="12" r="9" /><path d="M12 7.5v5.5M12 16.5h.01" />
366
+ {:else if status === 'warning'}
367
+ <path d="M10.3 4.1 2.6 17.5A2 2 0 0 0 4.3 20.5h15.4a2 2 0 0 0 1.7-3L13.7 4.1a2 2 0 0 0-3.4 0Z" /><path d="M12 9.5v4M12 17h.01" />
368
+ {:else if status === 'info'}
369
+ <circle cx="12" cy="12" r="9" /><path d="M12 11v5.5M12 7.5h.01" />
370
+ {:else}
371
+ <path d="M6 8a6 6 0 1 1 12 0c0 7 3 9 3 9H3s3-2 3-9" /><path d="M10.3 21a1.9 1.9 0 0 0 3.4 0" />
372
+ {/if}
373
+ </svg>
374
+ {#if item.status}<span class="sr-only">{statusLabel[item.status]}:</span>{/if}
375
+ </span>
376
+ {/if}
377
+
378
+ <div class="flex min-w-0 flex-1 flex-col gap-0.5">
379
+ <div class="flex min-w-0 items-start justify-between gap-3">
380
+ {#if item.href}
381
+ <a
382
+ id="{uid}-i-{item.id}"
383
+ data-nc-open
384
+ href={item.href}
385
+ tabindex="-1"
386
+ class="text-base-content min-w-0 text-sm font-medium hover:underline"
387
+ class:font-semibold={isUnread}
388
+ onclick={() => open(item)}>{item.title}</a
389
+ >
390
+ {:else}
391
+ <p
392
+ id="{uid}-i-{item.id}"
393
+ class="text-base-content min-w-0 text-sm font-medium"
394
+ class:font-semibold={isUnread}
395
+ >
396
+ {item.title}
397
+ </p>
398
+ {/if}
399
+ <span class="flex shrink-0 items-center gap-2">
400
+ <time
401
+ class="text-base-content/50 text-xs whitespace-nowrap tabular-nums"
402
+ datetime={iso(item.timestamp)}
403
+ title={date ? formatDateTime(date) : undefined}
404
+ >{relTime(item.timestamp, tick)}</time
405
+ >
406
+ {#if isUnread}
407
+ <span class="bg-primary size-2 rounded-full" aria-hidden="true"></span>
408
+ <span class="sr-only">Unread</span>
409
+ {/if}
410
+ </span>
411
+ </div>
412
+ {#if item.description}
413
+ <p id="{uid}-d-{item.id}" class="text-base-content/70 text-sm">
414
+ {item.description}
415
+ </p>
416
+ {/if}
417
+ {#if item.actions?.length}
418
+ <div class="mt-1.5 flex flex-wrap items-center gap-2">
419
+ {#each item.actions as action, ai (ai)}
420
+ <button
421
+ type="button"
422
+ class="btn btn-xs {actionClass[action.variant ?? 'secondary']}"
423
+ onclick={() => runAction(action, item)}
424
+ >
425
+ {action.label}
426
+ </button>
427
+ {/each}
428
+ </div>
429
+ {/if}
430
+ </div>
431
+ </article>
432
+ </li>
433
+ {/each}
434
+ </ul>
435
+ </div>
436
+ {/each}
437
+ </div>
438
+ {/if}
439
+ </div>
440
+ </div>
441
+ </section>
442
+
443
+ <style>
444
+ .nc-tab {
445
+ color: color-mix(in oklab, var(--color-base-content) 65%, transparent);
446
+ transition:
447
+ background-color var(--motion-fast, 150ms) var(--motion-glide, ease-out),
448
+ color var(--motion-fast, 150ms) var(--motion-glide, ease-out);
449
+ }
450
+ .nc-tab:hover {
451
+ color: var(--color-base-content);
452
+ }
453
+ .nc-tab-active {
454
+ background: var(--color-base-100);
455
+ color: var(--color-base-content);
456
+ }
457
+ .nc-tab:focus-visible {
458
+ outline: 2px solid var(--color-primary);
459
+ outline-offset: 1px;
460
+ }
461
+ .nc-item {
462
+ border-radius: var(--jatui-radius-box);
463
+ transition: background-color var(--motion-fast, 150ms) var(--motion-glide, ease-out);
464
+ }
465
+ .nc-item:hover {
466
+ background: color-mix(in oklab, var(--color-base-100) 92%, var(--color-base-content));
467
+ }
468
+ .nc-item.nc-unread {
469
+ background: color-mix(in oklab, var(--color-base-100) 94%, var(--color-primary));
470
+ }
471
+ .nc-item:focus-visible {
472
+ outline: 2px solid var(--color-primary);
473
+ outline-offset: -2px;
474
+ }
475
+ @media (prefers-reduced-motion: reduce) {
476
+ .nc-tab,
477
+ .nc-item {
478
+ transition: none;
479
+ }
480
+ }
481
+ </style>
@@ -0,0 +1,29 @@
1
+ import { type NotificationItem, type NotificationTab } from './notificationCenter';
2
+ interface Props {
3
+ items: NotificationItem[];
4
+ title?: string;
5
+ /** Tabs across the top. Default All / Mentions / System (by `category`). Pass [] to hide. */
6
+ tabs?: NotificationTab[];
7
+ /** Active tab id (bindable). Leave unbound for uncontrolled use with `defaultTab`. */
8
+ tab?: string;
9
+ defaultTab?: string;
10
+ onTabChange?: (tab: string) => void;
11
+ /** Enter on a focused item, or a click on it / its title. Also marks it read. */
12
+ onOpen?: (item: NotificationItem) => void;
13
+ /** Items became read (opened, or an action was used). */
14
+ onMarkRead?: (ids: string[]) => void;
15
+ /** "Mark all read". When omitted, `onMarkRead` receives every unread id instead. */
16
+ onMarkAllRead?: () => void;
17
+ /** Group the list under Today / Yesterday / This week / Earlier. Default true. */
18
+ groupByDate?: boolean;
19
+ /** Reference "now" for the date groups (tests, demos). Default: live clock. */
20
+ now?: Date | number;
21
+ /** Scroll-area cap — CSS length or px number. Default '32rem'. */
22
+ maxHeight?: string | number;
23
+ emptyTitle?: string;
24
+ emptyDescription?: string;
25
+ class?: string;
26
+ }
27
+ declare const NotificationCenter: import("svelte").Component<Props, {}, "tab">;
28
+ type NotificationCenter = ReturnType<typeof NotificationCenter>;
29
+ export default NotificationCenter;