@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
|
@@ -76,7 +76,7 @@ declare function $$render<N extends Node = Node, E extends Edge = Edge>(): {
|
|
|
76
76
|
screenToFlowPosition: (p: import("@xyflow/system").XYPosition) => import("@xyflow/system").XYPosition;
|
|
77
77
|
getViewport: () => Viewport;
|
|
78
78
|
};
|
|
79
|
-
bindings: "viewport" | "
|
|
79
|
+
bindings: "viewport" | "edges" | "nodes";
|
|
80
80
|
slots: {};
|
|
81
81
|
events: {};
|
|
82
82
|
};
|
|
@@ -84,7 +84,7 @@ declare class __sveltets_Render<N extends Node = Node, E extends Edge = Edge> {
|
|
|
84
84
|
props(): ReturnType<typeof $$render<N, E>>['props'];
|
|
85
85
|
events(): ReturnType<typeof $$render<N, E>>['events'];
|
|
86
86
|
slots(): ReturnType<typeof $$render<N, E>>['slots'];
|
|
87
|
-
bindings(): "viewport" | "
|
|
87
|
+
bindings(): "viewport" | "edges" | "nodes";
|
|
88
88
|
exports(): {
|
|
89
89
|
/**
|
|
90
90
|
* Imperative handle (`bind:this={canvas}` → `canvas.api.fitView()`). Every
|
|
@@ -4,6 +4,13 @@
|
|
|
4
4
|
Rich text input for composing and sending messages.
|
|
5
5
|
Supports mentions, attachments, and drag/drop.
|
|
6
6
|
All searches and uploads are via callbacks.
|
|
7
|
+
|
|
8
|
+
Agent composers (opt-in, jatui-lltq9): `busy` + `onStop` swap the send
|
|
9
|
+
button for a same-size Stop button (aria-label "Stop generating") while a
|
|
10
|
+
reply is generating; Escape inside the input also stops (an open mention
|
|
11
|
+
list still takes Escape first). Sending is held while busy — the draft is
|
|
12
|
+
kept. `statusTab` renders a slim strip attached to the input's top edge
|
|
13
|
+
(e.g. model · message count). Pair with ComposerGlow for the rim light.
|
|
7
14
|
-->
|
|
8
15
|
|
|
9
16
|
<script lang="ts">
|
|
@@ -18,6 +25,7 @@
|
|
|
18
25
|
} from '../../utils/mentionParser'
|
|
19
26
|
import MentionAutocomplete from './MentionAutocomplete.svelte'
|
|
20
27
|
import QuotedMessageDisplay from './QuotedMessageDisplay.svelte'
|
|
28
|
+
import type { Snippet } from 'svelte'
|
|
21
29
|
|
|
22
30
|
interface Props {
|
|
23
31
|
placeholder?: string
|
|
@@ -37,6 +45,12 @@
|
|
|
37
45
|
ontyping?: (data: { isTyping: boolean }) => void
|
|
38
46
|
onquoteremove?: () => void
|
|
39
47
|
onsearchmentions?: (query: string) => Promise<MentionItem[]>
|
|
48
|
+
/** A reply is generating: the send slot shows Stop, Escape stops, sending is held. */
|
|
49
|
+
busy?: boolean
|
|
50
|
+
/** Called by the Stop button / Escape while `busy`. */
|
|
51
|
+
onStop?: () => void
|
|
52
|
+
/** Slim strip attached to the input's top edge (status chrome). */
|
|
53
|
+
statusTab?: Snippet
|
|
40
54
|
}
|
|
41
55
|
|
|
42
56
|
let {
|
|
@@ -52,6 +66,9 @@
|
|
|
52
66
|
ontyping,
|
|
53
67
|
onquoteremove,
|
|
54
68
|
onsearchmentions,
|
|
69
|
+
busy = false,
|
|
70
|
+
onStop,
|
|
71
|
+
statusTab,
|
|
55
72
|
}: Props = $props()
|
|
56
73
|
|
|
57
74
|
let selectedPriority = $state<'low' | 'normal' | 'high' | 'urgent'>('normal')
|
|
@@ -203,6 +220,7 @@
|
|
|
203
220
|
|
|
204
221
|
function handleKeyDown(event: KeyboardEvent) {
|
|
205
222
|
if (showMentionAutocomplete && ['ArrowUp', 'ArrowDown', 'Tab', 'Enter', 'Escape'].includes(event.key)) return
|
|
223
|
+
if (event.key === 'Escape' && busy && onStop) { event.preventDefault(); event.stopPropagation(); onStop(); return }
|
|
206
224
|
if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage() }
|
|
207
225
|
}
|
|
208
226
|
|
|
@@ -255,7 +273,7 @@
|
|
|
255
273
|
|
|
256
274
|
function sendMessage() {
|
|
257
275
|
const content = inputText.trim()
|
|
258
|
-
if ((!content && attachments.length === 0) || disabled) return
|
|
276
|
+
if ((!content && attachments.length === 0) || disabled || busy) return
|
|
259
277
|
|
|
260
278
|
onsend?.({
|
|
261
279
|
content,
|
|
@@ -368,6 +386,12 @@
|
|
|
368
386
|
</div>
|
|
369
387
|
{/if}
|
|
370
388
|
|
|
389
|
+
{#if statusTab}
|
|
390
|
+
<div class="self-start -mb-2 ml-3 max-w-[calc(100%-1.5rem)] truncate px-2.5 py-0.5 text-xs text-base-content/60 bg-base-200 border border-b-0 border-base-300 rounded-t-lg">
|
|
391
|
+
{@render statusTab()}
|
|
392
|
+
</div>
|
|
393
|
+
{/if}
|
|
394
|
+
|
|
371
395
|
<div class="flex items-end gap-2 {compact ? 'p-2' : 'p-3'} border border-base-300 rounded-lg focus-within:border-primary transition-colors">
|
|
372
396
|
{#if !compact}
|
|
373
397
|
<button type="button" class="btn btn-ghost btn-sm btn-circle flex-shrink-0" onclick={handleFileUpload} title="Attach file" {disabled}>📎</button>
|
|
@@ -412,9 +436,15 @@
|
|
|
412
436
|
</select>
|
|
413
437
|
{/if}
|
|
414
438
|
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
439
|
+
{#if busy}
|
|
440
|
+
<button type="button" class="btn btn-primary {compact ? 'btn-xs' : 'btn-sm'} btn-circle flex-shrink-0" onclick={() => onStop?.()} disabled={!onStop} aria-label="Stop generating" title="Stop generating (Esc)">
|
|
441
|
+
<svg viewBox="0 0 16 16" class="w-3 h-3" fill="currentColor" aria-hidden="true"><rect x="3" y="3" width="10" height="10" rx="2" /></svg>
|
|
442
|
+
</button>
|
|
443
|
+
{:else}
|
|
444
|
+
<button type="button" class="btn btn-primary {compact ? 'btn-xs' : 'btn-sm'} btn-circle flex-shrink-0" onclick={sendMessage} disabled={disabled || (!inputText.trim() && attachments.length === 0) || isOverLimit} title="Send message (Enter)">
|
|
445
|
+
➤
|
|
446
|
+
</button>
|
|
447
|
+
{/if}
|
|
418
448
|
</div>
|
|
419
449
|
|
|
420
450
|
<div class="text-xs text-base-content/50">
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { QuotedMessage, PendingAttachment, MentionItem, AttachmentCallbacks } from '../../types/messaging';
|
|
2
|
+
import type { Snippet } from 'svelte';
|
|
2
3
|
interface Props {
|
|
3
4
|
placeholder?: string;
|
|
4
5
|
disabled?: boolean;
|
|
@@ -19,6 +20,12 @@ interface Props {
|
|
|
19
20
|
}) => void;
|
|
20
21
|
onquoteremove?: () => void;
|
|
21
22
|
onsearchmentions?: (query: string) => Promise<MentionItem[]>;
|
|
23
|
+
/** A reply is generating: the send slot shows Stop, Escape stops, sending is held. */
|
|
24
|
+
busy?: boolean;
|
|
25
|
+
/** Called by the Stop button / Escape while `busy`. */
|
|
26
|
+
onStop?: () => void;
|
|
27
|
+
/** Slim strip attached to the input's top edge (status chrome). */
|
|
28
|
+
statusTab?: Snippet;
|
|
22
29
|
}
|
|
23
30
|
declare const MessageInput: import("svelte").Component<Props, {
|
|
24
31
|
focus: () => void;
|
|
@@ -290,7 +290,7 @@
|
|
|
290
290
|
{#if !isConsecutive}
|
|
291
291
|
<Avatar name={message.senderName} avatarUrl={message.senderAvatar} size="small" />
|
|
292
292
|
{:else}
|
|
293
|
-
<div class="text-xs text-base-content/40 opacity-0 group-hover:opacity-100 transition-opacity text-center pt-0.5">
|
|
293
|
+
<div class="text-xs text-base-content/40 opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity text-center pt-0.5">
|
|
294
294
|
{formatTime(message.createdAt).split(' ')[1] || ''}
|
|
295
295
|
</div>
|
|
296
296
|
{/if}
|
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
<!-- Content -->
|
|
300
300
|
<div class="flex-1 min-w-0 relative">
|
|
301
301
|
<!-- Floating action buttons -->
|
|
302
|
-
<div class="absolute right-0 {!isConsecutive ? 'top-0' : 'top-1'} opacity-0 group-hover:opacity-100 transition-opacity bg-base-200 rounded-lg border border-base-300 p-1 z-10">
|
|
302
|
+
<div class="absolute right-0 {!isConsecutive ? 'top-0' : 'top-1'} opacity-0 group-hover:opacity-100 group-focus-within:opacity-100 transition-opacity bg-base-200 rounded-lg border border-base-300 p-1 z-10">
|
|
303
303
|
<div class="flex items-center">
|
|
304
304
|
{#if reactionCallbacks && currentUserId}
|
|
305
305
|
<div class="flex items-center gap-1 mr-1">
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export type NotificationStatus = 'info' | 'success' | 'warning' | 'error';
|
|
2
|
+
export interface NotificationAction {
|
|
3
|
+
label: string;
|
|
4
|
+
onSelect: (item: NotificationItem) => void;
|
|
5
|
+
/** Button weight. Default 'secondary' (a quiet outline button). */
|
|
6
|
+
variant?: 'primary' | 'secondary' | 'ghost' | 'danger';
|
|
7
|
+
}
|
|
8
|
+
export interface NotificationItem {
|
|
9
|
+
id: string;
|
|
10
|
+
title: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
/** When it happened — Date, ISO/DB timestamp string, or epoch ms. */
|
|
13
|
+
timestamp: Date | string | number;
|
|
14
|
+
/** Host-owned read flag. Missing / false = unread. */
|
|
15
|
+
read?: boolean;
|
|
16
|
+
/** Free-form category the tabs filter on (defaults: 'mentions', 'system'). */
|
|
17
|
+
category?: string;
|
|
18
|
+
/** Status glyph shown when there is no avatar. */
|
|
19
|
+
status?: NotificationStatus;
|
|
20
|
+
avatar?: {
|
|
21
|
+
name: string;
|
|
22
|
+
src?: string | null;
|
|
23
|
+
};
|
|
24
|
+
actions?: NotificationAction[];
|
|
25
|
+
/** Opening the item follows this link. */
|
|
26
|
+
href?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface NotificationTab {
|
|
29
|
+
id: string;
|
|
30
|
+
label: string;
|
|
31
|
+
/** Which items the tab shows. Omitted = every item. */
|
|
32
|
+
filter?: (item: NotificationItem) => boolean;
|
|
33
|
+
}
|
|
34
|
+
/** All / Mentions / System, filtered by `category` ('mention(s)' / 'system'). */
|
|
35
|
+
export declare const DEFAULT_NOTIFICATION_TABS: NotificationTab[];
|
|
36
|
+
export type DateGroupKey = 'today' | 'yesterday' | 'week' | 'earlier';
|
|
37
|
+
export declare const DATE_GROUP_LABELS: Record<DateGroupKey, string>;
|
|
38
|
+
/** Epoch ms for a timestamp, or NaN when it cannot be parsed. */
|
|
39
|
+
export declare function toTimestampMs(ts: Date | string | number | null | undefined): number;
|
|
40
|
+
/**
|
|
41
|
+
* Which date bucket a timestamp falls in, by LOCAL calendar day relative to
|
|
42
|
+
* `now`: today, yesterday, the 5 days before that ("This week" = within the
|
|
43
|
+
* last 7 calendar days), else earlier. Future and unparseable timestamps land
|
|
44
|
+
* in 'today' / 'earlier' respectively.
|
|
45
|
+
*/
|
|
46
|
+
export declare function dateGroupKey(ts: Date | string | number, now?: Date | number): DateGroupKey;
|
|
47
|
+
export interface NotificationGroup<T extends NotificationItem = NotificationItem> {
|
|
48
|
+
key: DateGroupKey;
|
|
49
|
+
label: string;
|
|
50
|
+
items: T[];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Newest-first items bucketed under Today / Yesterday / This week / Earlier.
|
|
54
|
+
* Empty buckets are dropped; ties keep input order (stable sort); items with
|
|
55
|
+
* an unparseable timestamp sort last.
|
|
56
|
+
*/
|
|
57
|
+
export declare function groupNotificationsByDate<T extends NotificationItem>(items: readonly T[], now?: Date | number): NotificationGroup<T>[];
|
|
58
|
+
/** Unread = host says not read AND not optimistically read locally. */
|
|
59
|
+
export declare function isNotificationUnread(item: NotificationItem, localRead?: ReadonlySet<string>): boolean;
|
|
60
|
+
export declare function unreadIds(items: readonly NotificationItem[], localRead?: ReadonlySet<string>): string[];
|
|
61
|
+
export declare function filterByTab<T extends NotificationItem>(items: readonly T[], tab: NotificationTab | undefined): T[];
|
|
62
|
+
/** Item count per tab id (what the tab chips show). */
|
|
63
|
+
export declare function tabCounts(items: readonly NotificationItem[], tabs: readonly NotificationTab[]): Record<string, number>;
|
|
64
|
+
/** The header sentence: "3 unread" / "No unread notifications". */
|
|
65
|
+
export declare function unreadSummary(count: number): string;
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* NotificationCenter — pure data helpers (DOM-free, unit-tested in
|
|
3
|
+
* tests/unit/notificationCenter.test.ts).
|
|
4
|
+
*
|
|
5
|
+
* The data model is adapted from BoardUI's notification-center.tsx
|
|
6
|
+
* (MIT License — Copyright (c) 2026 Mertcan Dundar Esmergul (BoardUI)):
|
|
7
|
+
* `category` / `status` / `avatar` / `actions[]` and the All / Mentions /
|
|
8
|
+
* System tab split. jatui changes: real timestamps (Date | string | epoch ms)
|
|
9
|
+
* grouped into calendar date buckets, configurable tabs, host-owned read state.
|
|
10
|
+
*/
|
|
11
|
+
import { parseTimestamp } from '../utils/dateFormatters';
|
|
12
|
+
const inCategory = (...names) => (item) => item.category != null && names.includes(item.category.toLowerCase());
|
|
13
|
+
/** All / Mentions / System, filtered by `category` ('mention(s)' / 'system'). */
|
|
14
|
+
export const DEFAULT_NOTIFICATION_TABS = [
|
|
15
|
+
{ id: 'all', label: 'All' },
|
|
16
|
+
{ id: 'mentions', label: 'Mentions', filter: inCategory('mentions', 'mention') },
|
|
17
|
+
{ id: 'system', label: 'System', filter: inCategory('system') }
|
|
18
|
+
];
|
|
19
|
+
export const DATE_GROUP_LABELS = {
|
|
20
|
+
today: 'Today',
|
|
21
|
+
yesterday: 'Yesterday',
|
|
22
|
+
week: 'This week',
|
|
23
|
+
earlier: 'Earlier'
|
|
24
|
+
};
|
|
25
|
+
const DATE_GROUP_ORDER = ['today', 'yesterday', 'week', 'earlier'];
|
|
26
|
+
/** Epoch ms for a timestamp, or NaN when it cannot be parsed. */
|
|
27
|
+
export function toTimestampMs(ts) {
|
|
28
|
+
if (ts == null || ts === '')
|
|
29
|
+
return NaN;
|
|
30
|
+
if (typeof ts === 'number')
|
|
31
|
+
return Number.isFinite(ts) ? ts : NaN;
|
|
32
|
+
const d = parseTimestamp(ts);
|
|
33
|
+
return d ? d.getTime() : NaN;
|
|
34
|
+
}
|
|
35
|
+
/** Local midnight of the day containing `ms`. */
|
|
36
|
+
function startOfDay(ms) {
|
|
37
|
+
const d = new Date(ms);
|
|
38
|
+
d.setHours(0, 0, 0, 0);
|
|
39
|
+
return d.getTime();
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Which date bucket a timestamp falls in, by LOCAL calendar day relative to
|
|
43
|
+
* `now`: today, yesterday, the 5 days before that ("This week" = within the
|
|
44
|
+
* last 7 calendar days), else earlier. Future and unparseable timestamps land
|
|
45
|
+
* in 'today' / 'earlier' respectively.
|
|
46
|
+
*/
|
|
47
|
+
export function dateGroupKey(ts, now = Date.now()) {
|
|
48
|
+
const ms = toTimestampMs(ts);
|
|
49
|
+
if (Number.isNaN(ms))
|
|
50
|
+
return 'earlier';
|
|
51
|
+
const today = startOfDay(typeof now === 'number' ? now : now.getTime());
|
|
52
|
+
const day = startOfDay(ms);
|
|
53
|
+
if (day >= today)
|
|
54
|
+
return 'today';
|
|
55
|
+
// Calendar-day difference, rounded so DST shifts (23h / 25h days) don't skew it.
|
|
56
|
+
const days = Math.round((today - day) / 86_400_000);
|
|
57
|
+
if (days === 1)
|
|
58
|
+
return 'yesterday';
|
|
59
|
+
if (days < 7)
|
|
60
|
+
return 'week';
|
|
61
|
+
return 'earlier';
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Newest-first items bucketed under Today / Yesterday / This week / Earlier.
|
|
65
|
+
* Empty buckets are dropped; ties keep input order (stable sort); items with
|
|
66
|
+
* an unparseable timestamp sort last.
|
|
67
|
+
*/
|
|
68
|
+
export function groupNotificationsByDate(items, now = Date.now()) {
|
|
69
|
+
const sorted = items
|
|
70
|
+
.map((item, i) => ({ item, i, ms: toTimestampMs(item.timestamp) }))
|
|
71
|
+
.sort((a, b) => {
|
|
72
|
+
const am = Number.isNaN(a.ms) ? -Infinity : a.ms;
|
|
73
|
+
const bm = Number.isNaN(b.ms) ? -Infinity : b.ms;
|
|
74
|
+
return bm - am || a.i - b.i;
|
|
75
|
+
});
|
|
76
|
+
const buckets = new Map();
|
|
77
|
+
for (const { item } of sorted) {
|
|
78
|
+
const key = dateGroupKey(item.timestamp, now);
|
|
79
|
+
let list = buckets.get(key);
|
|
80
|
+
if (!list)
|
|
81
|
+
buckets.set(key, (list = []));
|
|
82
|
+
list.push(item);
|
|
83
|
+
}
|
|
84
|
+
return DATE_GROUP_ORDER.filter((k) => buckets.has(k)).map((key) => ({
|
|
85
|
+
key,
|
|
86
|
+
label: DATE_GROUP_LABELS[key],
|
|
87
|
+
items: buckets.get(key)
|
|
88
|
+
}));
|
|
89
|
+
}
|
|
90
|
+
/** Unread = host says not read AND not optimistically read locally. */
|
|
91
|
+
export function isNotificationUnread(item, localRead) {
|
|
92
|
+
return item.read !== true && !(localRead?.has(item.id) ?? false);
|
|
93
|
+
}
|
|
94
|
+
export function unreadIds(items, localRead) {
|
|
95
|
+
return items.filter((it) => isNotificationUnread(it, localRead)).map((it) => it.id);
|
|
96
|
+
}
|
|
97
|
+
export function filterByTab(items, tab) {
|
|
98
|
+
return tab?.filter ? items.filter((it) => tab.filter(it)) : items.slice();
|
|
99
|
+
}
|
|
100
|
+
/** Item count per tab id (what the tab chips show). */
|
|
101
|
+
export function tabCounts(items, tabs) {
|
|
102
|
+
const out = {};
|
|
103
|
+
for (const tab of tabs)
|
|
104
|
+
out[tab.id] = tab.filter ? items.filter(tab.filter).length : items.length;
|
|
105
|
+
return out;
|
|
106
|
+
}
|
|
107
|
+
/** The header sentence: "3 unread" / "No unread notifications". */
|
|
108
|
+
export function unreadSummary(count) {
|
|
109
|
+
return count === 0 ? 'No unread notifications' : `${count} unread`;
|
|
110
|
+
}
|
|
@@ -1,4 +1,24 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
Steps — a numbered step track with an animated connector fill.
|
|
3
|
+
|
|
4
|
+
Opt-in extras (jatui-lltq9; none of them change the default output):
|
|
5
|
+
collapsible a summary header button ("3 steps left") that folds the
|
|
6
|
+
track away; the summary stays live while folded, and the
|
|
7
|
+
folded header also shows the active step's label + ring.
|
|
8
|
+
activeProgress 0–1 → a mini ProgressRing on the active step's node.
|
|
9
|
+
orientation 'vertical' stacks the steps (labels + descriptions beside
|
|
10
|
+
the nodes) — the shape for agent-progress / todo cards.
|
|
11
|
+
onFinished fires once, `completionDelay` ms after `current` passes the
|
|
12
|
+
last step (re-arms if `current` drops back), so a host can
|
|
13
|
+
hand the surface back to the agent's answer.
|
|
14
|
+
Drive `current` from real events; there is no time-simulated stepping.
|
|
15
|
+
-->
|
|
1
16
|
<script lang="ts">
|
|
17
|
+
import { untrack, type Snippet } from 'svelte';
|
|
18
|
+
import ProgressRing from './ProgressRing.svelte';
|
|
19
|
+
import { motionSlide } from '../../motion/transitions';
|
|
20
|
+
import { clampStepProgress, isStepsFinished, stepsSummary } from './stepsSummary';
|
|
21
|
+
|
|
2
22
|
interface Step {
|
|
3
23
|
label?: string;
|
|
4
24
|
description?: string;
|
|
@@ -10,6 +30,20 @@
|
|
|
10
30
|
color?: 'primary' | 'secondary' | 'accent' | 'success' | 'warning' | 'error' | 'info';
|
|
11
31
|
size?: 'sm' | 'md' | 'lg';
|
|
12
32
|
class?: string;
|
|
33
|
+
/** Render a summary header button that folds the steps away. */
|
|
34
|
+
collapsible?: boolean;
|
|
35
|
+
/** Folded state (bindable). Only meaningful with `collapsible`. */
|
|
36
|
+
collapsed?: boolean;
|
|
37
|
+
/** Header copy. Default: "N steps left" / "All steps done". */
|
|
38
|
+
summary?: string | Snippet;
|
|
39
|
+
/** Layout. Default 'horizontal'. */
|
|
40
|
+
orientation?: 'horizontal' | 'vertical';
|
|
41
|
+
/** 0–1 progress of the active step → a mini ring on its node. */
|
|
42
|
+
activeProgress?: number;
|
|
43
|
+
/** Hold (ms) after the last step completes before `onFinished`. Default 0. */
|
|
44
|
+
completionDelay?: number;
|
|
45
|
+
/** Fired once after `current` passes the last step (+ `completionDelay`). */
|
|
46
|
+
onFinished?: () => void;
|
|
13
47
|
}
|
|
14
48
|
|
|
15
49
|
let {
|
|
@@ -17,7 +51,14 @@
|
|
|
17
51
|
current = 1,
|
|
18
52
|
color = 'primary',
|
|
19
53
|
size = 'md',
|
|
20
|
-
class: className = ''
|
|
54
|
+
class: className = '',
|
|
55
|
+
collapsible = false,
|
|
56
|
+
collapsed = $bindable(false),
|
|
57
|
+
summary,
|
|
58
|
+
orientation = 'horizontal',
|
|
59
|
+
activeProgress,
|
|
60
|
+
completionDelay = 0,
|
|
61
|
+
onFinished
|
|
21
62
|
}: Props = $props();
|
|
22
63
|
|
|
23
64
|
const normalized = $derived(
|
|
@@ -53,17 +94,45 @@
|
|
|
53
94
|
const nodePx = $derived(nodeSize[size]);
|
|
54
95
|
const fontSize: Record<NonNullable<Props['size']>, string> = { sm: '0.6875rem', md: '0.8125rem', lg: '1rem' };
|
|
55
96
|
const labelSize = $derived(fontSize[size]);
|
|
97
|
+
|
|
98
|
+
// ── Opt-in extras ────────────────────────────────────────────────────
|
|
99
|
+
const uid = $props.id();
|
|
100
|
+
const bodyId = `steps-body-${uid}`;
|
|
101
|
+
const vertical = $derived(orientation === 'vertical');
|
|
102
|
+
const finished = $derived(isStepsFinished(count, current));
|
|
103
|
+
const ringValue = $derived(
|
|
104
|
+
activeProgress == null ? null : Math.round(clampStepProgress(activeProgress) * 100)
|
|
105
|
+
);
|
|
106
|
+
const summaryText = $derived(typeof summary === 'string' ? summary : stepsSummary(count, current));
|
|
107
|
+
const activeLabel = $derived(finished ? undefined : normalized[current - 1]?.label);
|
|
108
|
+
|
|
109
|
+
// onFinished: once per "run" — re-arms when current drops back to a step.
|
|
110
|
+
let finishedFired = false;
|
|
111
|
+
$effect(() => {
|
|
112
|
+
if (!finished) {
|
|
113
|
+
finishedFired = false;
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (finishedFired) return;
|
|
117
|
+
const delay = Math.max(0, completionDelay);
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
finishedFired = true;
|
|
120
|
+
untrack(() => onFinished)?.();
|
|
121
|
+
}, delay);
|
|
122
|
+
return () => clearTimeout(timer);
|
|
123
|
+
});
|
|
56
124
|
</script>
|
|
57
125
|
|
|
126
|
+
{#snippet track()}
|
|
58
127
|
<div
|
|
59
|
-
class="steps-wrap {className}"
|
|
128
|
+
class="steps-wrap {collapsible ? '' : className}{vertical ? ' steps-wrap--vertical' : ''}"
|
|
60
129
|
role="list"
|
|
61
130
|
aria-label="Progress steps"
|
|
62
131
|
style="--fill:{fillColor};--fg:{fgColor};--node:{nodePx}px"
|
|
63
132
|
>
|
|
64
133
|
<!-- Connector track -->
|
|
65
134
|
<div class="steps-track" aria-hidden="true">
|
|
66
|
-
<div class="steps-track-fill" style="transform:scaleX({trackFill})"></div>
|
|
135
|
+
<div class="steps-track-fill" style="transform:{vertical ? 'scaleY' : 'scaleX'}({trackFill})"></div>
|
|
67
136
|
</div>
|
|
68
137
|
|
|
69
138
|
<!-- Step nodes -->
|
|
@@ -78,7 +147,7 @@
|
|
|
78
147
|
role="listitem"
|
|
79
148
|
aria-current={isActive ? 'step' : undefined}
|
|
80
149
|
>
|
|
81
|
-
<div class="step-node" style="font-size:{labelSize}">
|
|
150
|
+
<div class="step-node" class:step-node--ring={isActive && ringValue != null} style="font-size:{labelSize}">
|
|
82
151
|
{#if isComplete}
|
|
83
152
|
<!-- Check mark drawn in on complete -->
|
|
84
153
|
<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="step-check" aria-hidden="true">
|
|
@@ -86,14 +155,71 @@
|
|
|
86
155
|
</svg>
|
|
87
156
|
{:else}
|
|
88
157
|
<span>{stepNum}</span>
|
|
158
|
+
{/if}{#if isActive && ringValue != null}
|
|
159
|
+
<span class="step-ring">
|
|
160
|
+
<ProgressRing
|
|
161
|
+
value={ringValue}
|
|
162
|
+
size={nodePx}
|
|
163
|
+
strokeWidth={size === 'sm' ? 2 : 3}
|
|
164
|
+
{color}
|
|
165
|
+
showValue={false}
|
|
166
|
+
label="{step.label ?? `Step ${stepNum}`} progress"
|
|
167
|
+
/>
|
|
168
|
+
</span>
|
|
89
169
|
{/if}
|
|
90
170
|
</div>
|
|
91
|
-
{#if
|
|
171
|
+
{#if vertical}
|
|
172
|
+
{#if step.label || step.description}
|
|
173
|
+
<span class="step-text">
|
|
174
|
+
{#if step.label}<span class="step-label" style="font-size:{labelSize}">{step.label}</span>{/if}
|
|
175
|
+
{#if step.description}<span class="step-desc">{step.description}</span>{/if}
|
|
176
|
+
</span>
|
|
177
|
+
{/if}
|
|
178
|
+
{:else if step.label}
|
|
92
179
|
<span class="step-label" style="font-size:{labelSize}">{step.label}</span>
|
|
93
180
|
{/if}
|
|
94
181
|
</div>
|
|
95
182
|
{/each}
|
|
96
183
|
</div>
|
|
184
|
+
{/snippet}
|
|
185
|
+
|
|
186
|
+
{#if collapsible}
|
|
187
|
+
<div class="steps-fold {className}" style="--fill:{fillColor}">
|
|
188
|
+
<button
|
|
189
|
+
type="button"
|
|
190
|
+
class="steps-fold-header"
|
|
191
|
+
aria-expanded={!collapsed}
|
|
192
|
+
aria-controls={bodyId}
|
|
193
|
+
onclick={() => (collapsed = !collapsed)}
|
|
194
|
+
>
|
|
195
|
+
<svg class="steps-fold-chevron" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
196
|
+
<polyline points="6,4 10,8 6,12" />
|
|
197
|
+
</svg>
|
|
198
|
+
<span class="steps-fold-summary">
|
|
199
|
+
{#if typeof summary === 'function'}{@render summary()}{:else}{summaryText}{/if}
|
|
200
|
+
</span>
|
|
201
|
+
{#if collapsed && activeLabel}
|
|
202
|
+
<span class="steps-fold-current">{activeLabel}</span>
|
|
203
|
+
{/if}
|
|
204
|
+
{#if collapsed && ringValue != null && !finished}
|
|
205
|
+
<span class="steps-fold-ring" aria-hidden="true">
|
|
206
|
+
<ProgressRing value={ringValue} size={16} strokeWidth={2} {color} showValue={false} />
|
|
207
|
+
</span>
|
|
208
|
+
{/if}
|
|
209
|
+
</button>
|
|
210
|
+
<!-- Live summary for assistive tech — the header text itself is not a live region. -->
|
|
211
|
+
<span class="sr-only" aria-live="polite">{summaryText}</span>
|
|
212
|
+
<div id={bodyId} class="steps-fold-body">
|
|
213
|
+
{#if !collapsed}
|
|
214
|
+
<div class="steps-fold-inner" transition:motionSlide>
|
|
215
|
+
{@render track()}
|
|
216
|
+
</div>
|
|
217
|
+
{/if}
|
|
218
|
+
</div>
|
|
219
|
+
</div>
|
|
220
|
+
{:else}
|
|
221
|
+
{@render track()}
|
|
222
|
+
{/if}
|
|
97
223
|
|
|
98
224
|
<style>
|
|
99
225
|
.steps-wrap {
|
|
@@ -136,6 +262,7 @@
|
|
|
136
262
|
}
|
|
137
263
|
|
|
138
264
|
.step-node {
|
|
265
|
+
position: relative;
|
|
139
266
|
width: var(--node);
|
|
140
267
|
height: var(--node);
|
|
141
268
|
border-radius: 9999px;
|
|
@@ -160,6 +287,18 @@
|
|
|
160
287
|
transform: scale(1.18);
|
|
161
288
|
}
|
|
162
289
|
|
|
290
|
+
/* Active node with a progress ring: the ring IS the border. */
|
|
291
|
+
.step--active .step-node--ring {
|
|
292
|
+
border-color: transparent;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
.step-ring {
|
|
296
|
+
position: absolute;
|
|
297
|
+
inset: -2px;
|
|
298
|
+
display: flex;
|
|
299
|
+
pointer-events: none;
|
|
300
|
+
}
|
|
301
|
+
|
|
163
302
|
.step--complete .step-node {
|
|
164
303
|
background: var(--fill);
|
|
165
304
|
border-color: var(--fill);
|
|
@@ -184,6 +323,127 @@
|
|
|
184
323
|
color: var(--color-base-content);
|
|
185
324
|
}
|
|
186
325
|
|
|
326
|
+
/* ── Vertical orientation ─────────────────────────────────────────── */
|
|
327
|
+
.steps-wrap--vertical {
|
|
328
|
+
flex-direction: column;
|
|
329
|
+
align-items: stretch;
|
|
330
|
+
justify-content: flex-start;
|
|
331
|
+
gap: 0.875rem;
|
|
332
|
+
padding-top: 0;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
.steps-wrap--vertical .steps-track {
|
|
336
|
+
top: calc(var(--node) / 2);
|
|
337
|
+
bottom: calc(var(--node) / 2);
|
|
338
|
+
left: calc(var(--node) / 2);
|
|
339
|
+
right: auto;
|
|
340
|
+
width: 3px;
|
|
341
|
+
height: auto;
|
|
342
|
+
transform: translateX(-50%);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
.steps-wrap--vertical .steps-track-fill {
|
|
346
|
+
width: 100%;
|
|
347
|
+
transform-origin: top;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.steps-wrap--vertical .step {
|
|
351
|
+
flex-direction: row;
|
|
352
|
+
align-items: flex-start;
|
|
353
|
+
gap: 0.75rem;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.steps-wrap--vertical .step-node {
|
|
357
|
+
flex-shrink: 0;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
.step-text {
|
|
361
|
+
display: flex;
|
|
362
|
+
flex-direction: column;
|
|
363
|
+
gap: 0.125rem;
|
|
364
|
+
min-width: 0;
|
|
365
|
+
min-height: var(--node);
|
|
366
|
+
justify-content: center;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.steps-wrap--vertical .step-label {
|
|
370
|
+
text-align: left;
|
|
371
|
+
max-width: none;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
.step-desc {
|
|
375
|
+
font-size: 0.75rem;
|
|
376
|
+
line-height: 1.3;
|
|
377
|
+
color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/* ── Collapsible header ───────────────────────────────────────────── */
|
|
381
|
+
.steps-fold {
|
|
382
|
+
width: 100%;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
.steps-fold-header {
|
|
386
|
+
display: flex;
|
|
387
|
+
align-items: center;
|
|
388
|
+
gap: 0.5rem;
|
|
389
|
+
width: 100%;
|
|
390
|
+
min-height: 2.25rem;
|
|
391
|
+
padding: 0.375rem 0.5rem;
|
|
392
|
+
border-radius: var(--jatui-radius-field, var(--radius-field, 0.25rem));
|
|
393
|
+
font-size: 0.875rem;
|
|
394
|
+
font-weight: 600;
|
|
395
|
+
text-align: left;
|
|
396
|
+
color: var(--color-base-content);
|
|
397
|
+
background: transparent;
|
|
398
|
+
cursor: pointer;
|
|
399
|
+
transition: background var(--motion-fast, 150ms) var(--motion-glide);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
.steps-fold-header:hover {
|
|
403
|
+
background: color-mix(in oklch, var(--color-base-content) 6%, transparent);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.steps-fold-header:focus-visible {
|
|
407
|
+
outline: 2px solid var(--fill);
|
|
408
|
+
outline-offset: 2px;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
.steps-fold-chevron {
|
|
412
|
+
width: 1rem;
|
|
413
|
+
height: 1rem;
|
|
414
|
+
flex-shrink: 0;
|
|
415
|
+
color: color-mix(in oklch, var(--color-base-content) 55%, transparent);
|
|
416
|
+
transform: rotate(90deg);
|
|
417
|
+
transition: transform var(--motion-fast, 150ms) var(--motion-glide);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
.steps-fold-header[aria-expanded='false'] .steps-fold-chevron {
|
|
421
|
+
transform: rotate(0deg);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
.steps-fold-summary {
|
|
425
|
+
font-variant-numeric: tabular-nums;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
.steps-fold-current {
|
|
429
|
+
min-width: 0;
|
|
430
|
+
overflow: hidden;
|
|
431
|
+
text-overflow: ellipsis;
|
|
432
|
+
white-space: nowrap;
|
|
433
|
+
font-weight: 400;
|
|
434
|
+
color: color-mix(in oklch, var(--color-base-content) 60%, transparent);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
.steps-fold-ring {
|
|
438
|
+
display: inline-flex;
|
|
439
|
+
margin-left: auto;
|
|
440
|
+
flex-shrink: 0;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.steps-fold-inner {
|
|
444
|
+
padding: 0.5rem 0.25rem 0.25rem;
|
|
445
|
+
}
|
|
446
|
+
|
|
187
447
|
@media (prefers-reduced-motion: reduce) {
|
|
188
448
|
.steps-track-fill {
|
|
189
449
|
transition: none;
|
|
@@ -191,5 +451,9 @@
|
|
|
191
451
|
.step-node {
|
|
192
452
|
transition: none;
|
|
193
453
|
}
|
|
454
|
+
.steps-fold-chevron,
|
|
455
|
+
.steps-fold-header {
|
|
456
|
+
transition: none;
|
|
457
|
+
}
|
|
194
458
|
}
|
|
195
459
|
</style>
|