@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.
- package/CLAUDE.md +54 -5
- package/THIRD_PARTY_NOTICES.md +67 -0
- package/dist/actions/scrollEdge.d.ts +55 -0
- package/dist/actions/scrollEdge.js +144 -0
- package/dist/artifact-blocks/blocks/diagram/DiagramBlock.svelte.d.ts +1 -1
- package/dist/artifact-blocks/blocks/diagram/schema.d.ts +1 -1
- package/dist/components/BottomSheet.svelte +7 -6
- package/dist/components/BottomSheet.svelte.d.ts +2 -0
- package/dist/components/BulkActionBar.svelte +32 -2
- package/dist/components/BulkActionBar.svelte.d.ts +5 -1
- package/dist/components/CloseButton.svelte +75 -0
- package/dist/components/CloseButton.svelte.d.ts +29 -0
- package/dist/components/Drawer.svelte +10 -11
- package/dist/components/Drawer.svelte.d.ts +2 -8
- package/dist/components/FloatingActionBar.svelte +102 -34
- package/dist/components/FloatingActionBar.svelte.d.ts +9 -0
- package/dist/components/IconButton.svelte +200 -0
- package/dist/components/IconButton.svelte.d.ts +46 -0
- package/dist/components/LinkButton.svelte +117 -0
- package/dist/components/LinkButton.svelte.d.ts +32 -0
- package/dist/components/MediaExplorer.svelte +249 -0
- package/dist/components/MediaExplorer.svelte.d.ts +37 -0
- package/dist/components/MediaLightbox.svelte +2 -7
- package/dist/components/Modal.svelte +2 -10
- package/dist/components/NotificationCenter.svelte +481 -0
- package/dist/components/NotificationCenter.svelte.d.ts +29 -0
- package/dist/components/PinInput.svelte +94 -31
- package/dist/components/PinInput.svelte.d.ts +4 -0
- package/dist/components/SearchDropdown.svelte +54 -8
- package/dist/components/SearchDropdown.svelte.d.ts +12 -0
- package/dist/components/TextArea.svelte +67 -5
- package/dist/components/TextArea.svelte.d.ts +4 -6
- package/dist/components/ThemeSelector.svelte +29 -9
- package/dist/components/ThemeSelector.svelte.d.ts +6 -0
- package/dist/components/ThemeToggle.svelte +28 -3
- package/dist/components/ThemeToggle.svelte.d.ts +9 -0
- package/dist/components/agent/AgentLog.svelte +217 -0
- package/dist/components/agent/AgentLog.svelte.d.ts +72 -0
- package/dist/components/agent/AgentLogRow.svelte +281 -0
- package/dist/components/agent/AgentLogRow.svelte.d.ts +21 -0
- package/dist/components/agent/AgentTaskList.svelte +607 -0
- package/dist/components/agent/AgentTaskList.svelte.d.ts +40 -0
- package/dist/components/agent/AgentThinking.svelte +280 -0
- package/dist/components/agent/AgentThinking.svelte.d.ts +22 -0
- package/dist/components/agent/ComposerGlow.svelte +265 -0
- package/dist/components/agent/ComposerGlow.svelte.d.ts +18 -0
- package/dist/components/agent/ContextMeter.svelte +277 -0
- package/dist/components/agent/ContextMeter.svelte.d.ts +21 -0
- package/dist/components/agent/Questionnaire.svelte +767 -0
- package/dist/components/agent/Questionnaire.svelte.d.ts +29 -0
- package/dist/components/agent/ShimmerText.svelte +100 -0
- package/dist/components/agent/ShimmerText.svelte.d.ts +19 -0
- package/dist/components/agent/StreamedText.svelte +135 -0
- package/dist/components/agent/StreamedText.svelte.d.ts +13 -0
- package/dist/components/agent/agentLog.d.ts +22 -0
- package/dist/components/agent/agentLog.js +26 -0
- package/dist/components/agent/agentTaskList.d.ts +90 -0
- package/dist/components/agent/agentTaskList.js +93 -0
- package/dist/components/agent/agentThinking.d.ts +46 -0
- package/dist/components/agent/agentThinking.js +78 -0
- package/dist/components/agent/contextMeter.d.ts +97 -0
- package/dist/components/agent/contextMeter.js +156 -0
- package/dist/components/agent/questionnaire.d.ts +104 -0
- package/dist/components/agent/questionnaire.js +225 -0
- package/dist/components/agent/revealCore.d.ts +74 -0
- package/dist/components/agent/revealCore.js +160 -0
- package/dist/components/agent/revealTicker.d.ts +34 -0
- package/dist/components/agent/revealTicker.js +73 -0
- package/dist/components/agent/streamedText.d.ts +28 -0
- package/dist/components/agent/streamedText.js +59 -0
- package/dist/components/basic/Breadcrumb.svelte +226 -11
- package/dist/components/basic/Breadcrumb.svelte.d.ts +27 -1
- package/dist/components/basic/breadcrumbCollapse.d.ts +19 -0
- package/dist/components/basic/breadcrumbCollapse.js +12 -0
- package/dist/components/graph/GraphCanvas.svelte.d.ts +2 -2
- package/dist/components/messaging/MessageInput.svelte +34 -4
- package/dist/components/messaging/MessageInput.svelte.d.ts +7 -0
- package/dist/components/messaging/MessageItem.svelte +2 -2
- package/dist/components/notificationCenter.d.ts +65 -0
- package/dist/components/notificationCenter.js +110 -0
- package/dist/components/progress/Steps.svelte +269 -5
- package/dist/components/progress/Steps.svelte.d.ts +16 -1
- package/dist/components/progress/stepsSummary.d.ts +14 -0
- package/dist/components/progress/stepsSummary.js +32 -0
- package/dist/index.d.ts +48 -1
- package/dist/index.js +39 -1
- package/dist/selection/clickSelection.d.ts +89 -0
- package/dist/selection/clickSelection.js +152 -0
- package/dist/selection/createClickSelection.d.ts +79 -0
- package/dist/selection/createClickSelection.js +134 -0
- package/dist/styles/tokens.css +80 -1
- package/dist/utils/pinInput.d.ts +33 -0
- package/dist/utils/pinInput.js +63 -0
- package/dist/utils/textareaRows.d.ts +23 -0
- package/dist/utils/textareaRows.js +13 -0
- package/dist/utils/themeWipe.d.ts +36 -17
- package/dist/utils/themeWipe.js +145 -35
- 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;
|