@hearthlog/shell 0.0.0-stage → 0.1.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 (80) hide show
  1. package/LICENSE +235 -0
  2. package/package.json +51 -3
  3. package/src/app.template.html +184 -0
  4. package/src/components/ai/DecisionReviewSheet.svelte +226 -0
  5. package/src/components/ai/SuggestButton.svelte +23 -0
  6. package/src/components/ai/SuggestionChip.svelte +13 -0
  7. package/src/components/ai/SuggestionMark.svelte +15 -0
  8. package/src/components/calendar/CalendarFeedsDrawer.svelte +414 -0
  9. package/src/components/calendar/CalendarMonthSummary.svelte +122 -0
  10. package/src/components/calendar/calendar-weeks.svelte.ts +27 -0
  11. package/src/components/calendar/source-icons.ts +19 -0
  12. package/src/components/data-table/DataTableValue.svelte +14 -0
  13. package/src/components/data-table/EntityActionsCell.svelte +36 -0
  14. package/src/components/data-table/EntityRefsCell.svelte +58 -0
  15. package/src/components/data-table/EntityValue.svelte +42 -0
  16. package/src/components/data-table/entity-columns.ts +87 -0
  17. package/src/components/data-table/sqlite-columns.ts +42 -0
  18. package/src/components/entities/CalendarSourceFields.svelte +149 -0
  19. package/src/components/history/ChangeRecord.svelte +30 -0
  20. package/src/components/history/ForkComposer.svelte +174 -0
  21. package/src/components/history/HistoryScrubber.svelte +245 -0
  22. package/src/components/history/LogHistory.svelte +334 -0
  23. package/src/components/history/LogMap.svelte +293 -0
  24. package/src/components/history/LogTree.svelte +124 -0
  25. package/src/components/layout/AccountMenu.svelte +111 -0
  26. package/src/components/layout/AppearanceOptions.svelte +78 -0
  27. package/src/components/layout/BrandWordmark.svelte +12 -0
  28. package/src/components/layout/ErrorToast.svelte +34 -0
  29. package/src/components/layout/LogCreateDrawer.svelte +111 -0
  30. package/src/components/layout/LogPanel.svelte +225 -0
  31. package/src/components/layout/LogStatusChip.svelte +137 -0
  32. package/src/components/layout/MobilePageAction.svelte +44 -0
  33. package/src/components/layout/MovedBanner.svelte +51 -0
  34. package/src/components/layout/NavBar.svelte +603 -0
  35. package/src/components/layout/NavFlyout.svelte +132 -0
  36. package/src/components/layout/NavItem.svelte +36 -0
  37. package/src/components/layout/NavMoreDialog.svelte +179 -0
  38. package/src/components/layout/PageDataStatus.svelte +36 -0
  39. package/src/components/layout/PhoneGlance.svelte +96 -0
  40. package/src/components/layout/SidebarResizer.svelte +122 -0
  41. package/src/components/layout/SyncIndicator.svelte +24 -0
  42. package/src/components/layout/feature-icons.ts +24 -0
  43. package/src/components/layout/nav-indicator.ts +128 -0
  44. package/src/components/layout/sidebar-size.ts +31 -0
  45. package/src/components/sync/SyncIssuesDrawer.svelte +149 -0
  46. package/src/components/sync/SyncStateBadge.svelte +17 -0
  47. package/src/pages/calendar/Page.svelte +1341 -0
  48. package/src/pages/calendar/load.ts +77 -0
  49. package/src/pages/developer/DatabaseBrowser.svelte +325 -0
  50. package/src/pages/developer/Event.svelte +51 -0
  51. package/src/pages/developer/EventLogManager.svelte +268 -0
  52. package/src/pages/developer/IndexedDbStats.svelte +93 -0
  53. package/src/pages/developer/LedgerAdmin.svelte +877 -0
  54. package/src/pages/developer/LocalSqliteStats.svelte +56 -0
  55. package/src/pages/developer/Page.svelte +590 -0
  56. package/src/pages/developer/PendingWorkImport.svelte +49 -0
  57. package/src/pages/developer/PerformanceDashboard.svelte +262 -0
  58. package/src/pages/developer/StackInfo.svelte +117 -0
  59. package/src/pages/developer/SyncLog.svelte +188 -0
  60. package/src/pages/developer/load.ts +28 -0
  61. package/src/pages/entities/Page.svelte +1069 -0
  62. package/src/pages/entities/load.ts +18 -0
  63. package/src/pages/entity/Page.svelte +655 -0
  64. package/src/pages/entity/load.ts +79 -0
  65. package/src/pages/error/ErrorPage.svelte +147 -0
  66. package/src/pages/layout/Layout.svelte +597 -0
  67. package/src/pages/layout/load.ts +28 -0
  68. package/src/pages/redirects/history.ts +20 -0
  69. package/src/pages/redirects/start.ts +11 -0
  70. package/src/pages/redirects/today.ts +9 -0
  71. package/src/service-worker/index.ts +108 -0
  72. package/src/service-worker/offline-bundle.ts +116 -0
  73. package/src/service-worker/virtual.d.ts +3 -0
  74. package/src/styles/index.css +11 -0
  75. package/src/styles/shell.css +296 -0
  76. package/static/_headers +4 -0
  77. package/static/favicon.ico +0 -0
  78. package/static/offline.html +44 -0
  79. package/static/replay/sw.js +2 -0
  80. package/README.md +0 -3
@@ -0,0 +1,77 @@
1
+ import { deferPage } from '@hearthlog/runtime/runtime/page-data.svelte.js';
2
+ import {
3
+ calendarMonthBounds,
4
+ parseCalendarMonth,
5
+ toLocalDateString,
6
+ } from '@hearthlog/runtime/calendar/logic.js';
7
+ import { getCalendarSourcePreference } from '@hearthlog/runtime/calendar/preferences.js';
8
+ import type { CalendarSourcePreference } from '@hearthlog/runtime/calendar/groups.js';
9
+ import type { CalendarSourceCounts, CalendarSourceInfo } from '@hearthlog/contracts/calendar/types';
10
+ import { eventLogFilename } from '@hearthlog/runtime/store/event-log.svelte.js';
11
+ import { queryDatabase } from '@hearthlog/runtime/db/read-repositories.js';
12
+ import { appClock } from '@hearthlog/runtime/runtime/view.js';
13
+ import { tenant } from '@hearthlog/contracts/tenant/config';
14
+ import { MODULE_REFRESH_KEYS } from '#generated/modules';
15
+ import type { LoadEvent } from '@sveltejs/kit';
16
+ type PageLoad = (event: LoadEvent) => unknown;
17
+
18
+ const loadData = async ({ url }: Parameters<PageLoad>[0]) => {
19
+ const boundaryNow = await appClock.ready();
20
+ // The endless grid reads its weeks as they scroll into view; the page
21
+ // loads what spans them and the counts for the month it opens at.
22
+ const month = parseCalendarMonth(url.searchParams.get('month'), boundaryNow);
23
+ const monthBounds = calendarMonthBounds(month);
24
+ const today = toLocalDateString(boundaryNow);
25
+ await eventLogFilename.ready();
26
+ const sourcePreference = await getCalendarSourcePreference(
27
+ eventLogFilename.value ?? '',
28
+ );
29
+
30
+ const [sources, monthCounts, pinnedEvents] = await queryDatabase(async (repositories) => {
31
+ const [sources, monthCounts, pinnedEvents] = await Promise.all([
32
+ repositories.calendar.getCalendarSources(),
33
+ repositories.calendar.getCalendarSourceCounts(
34
+ monthBounds.startDate,
35
+ monthBounds.endDateExclusive,
36
+ sourcePreference,
37
+ ),
38
+ repositories.calendar.getUpcomingPinnedCalendarEvents(today),
39
+ ]);
40
+ return [sources, monthCounts, pinnedEvents] as const;
41
+ });
42
+
43
+ return {
44
+ month,
45
+ today,
46
+ sources: sources as CalendarSourceInfo[],
47
+ monthCounts,
48
+ pinnedEvents,
49
+ sourcePreference,
50
+ effectiveNow: boundaryNow.toISOString(),
51
+ };
52
+ };
53
+
54
+ type CalendarPageData = Awaited<ReturnType<typeof loadData>>;
55
+
56
+ export const load = ((event) => {
57
+ event.depends('app:calendar');
58
+ // Modules feed the calendar through their entity sources, so any of their refreshes re-reads it.
59
+ for (const key of MODULE_REFRESH_KEYS) event.depends(key);
60
+ if (tenant.dynamicEntities) {
61
+ event.depends('app:entity-types');
62
+ event.depends('app:entities');
63
+ }
64
+ return deferPage(
65
+ event,
66
+ {
67
+ month: parseCalendarMonth(event.url.searchParams.get('month')),
68
+ today: toLocalDateString(),
69
+ sources: [] as CalendarSourceInfo[],
70
+ monthCounts: { total: 0, sources: [] } as CalendarSourceCounts,
71
+ pinnedEvents: [],
72
+ sourcePreference: null as CalendarSourcePreference | null,
73
+ effectiveNow: new Date().toISOString(),
74
+ } as CalendarPageData,
75
+ () => loadData(event),
76
+ );
77
+ }) satisfies PageLoad;
@@ -0,0 +1,325 @@
1
+ <script lang="ts">
2
+ import { refreshPageData } from "@hearthlog/runtime/runtime/refresh.js";
3
+ import { goto } from "$app/navigation";
4
+ import { page } from "$app/state";
5
+ import EmptyState from "@hearthlog/ui/components/EmptyState.svelte";
6
+ import DataTable from "@hearthlog/ui/components/data-table/DataTable.svelte";
7
+ import DataTablePagination from "@hearthlog/ui/components/data-table/DataTablePagination.svelte";
8
+ import { toSqliteTableColumns } from "@hearthlog/shell/components/data-table/sqlite-columns.js";
9
+ import {
10
+ DEFAULT_RAW_SQLITE_PAGE_SIZE,
11
+ parseRawSqlitePage,
12
+ parseRawSqlitePageSize,
13
+ parseRawSqliteSortDirection,
14
+ parseRawSqliteSortField,
15
+ RAW_SQLITE_PAGE_SIZES,
16
+ type RawSqliteTablePage,
17
+ } from "@hearthlog/runtime/db/raw-browser.js";
18
+ import IconDatabaseOutline from "~icons/mdi/database-outline";
19
+ import IconRefresh from "~icons/mdi/refresh";
20
+
21
+ let { databasePage = null, isLoading = false }: { databasePage?: RawSqliteTablePage | null; isLoading?: boolean } = $props();
22
+ // Stale-while-revalidate: keep the last loaded page on screen while the
23
+ // next table/sort/page loads, so switching tables or sorting never
24
+ // flashes a blank spinner. `viewPage` lags one load behind by design.
25
+ let viewPage = $state<RawSqliteTablePage | null>(null);
26
+ $effect(() => {
27
+ if (databasePage) viewPage = databasePage;
28
+ });
29
+ const effectivePage = $derived(viewPage ?? databasePage);
30
+ const isStale = $derived(
31
+ isLoading && viewPage !== null && viewPage !== databasePage,
32
+ );
33
+ const tableColumns = $derived(
34
+ effectivePage ? toSqliteTableColumns(effectivePage.columns) : [],
35
+ );
36
+ const tableSorting = $derived(
37
+ effectivePage?.sortField
38
+ ? {
39
+ id: effectivePage.sortField,
40
+ desc: effectivePage.sortDirection === "desc",
41
+ }
42
+ : { id: null, desc: false },
43
+ );
44
+ const tablePagination = $derived(
45
+ effectivePage
46
+ ? {
47
+ pageIndex: effectivePage.currentPage - 1,
48
+ pageSize: effectivePage.pageSize,
49
+ rowCount: effectivePage.totalRows,
50
+ pageCount: effectivePage.totalPages,
51
+ }
52
+ : { pageIndex: 0, pageSize: DEFAULT_RAW_SQLITE_PAGE_SIZE, rowCount: 0, pageCount: 1 },
53
+ );
54
+ const showingStart = $derived(
55
+ effectivePage && effectivePage.totalRows > 0
56
+ ? (effectivePage.currentPage - 1) * effectivePage.pageSize + 1
57
+ : 0,
58
+ );
59
+ const showingEnd = $derived(
60
+ effectivePage
61
+ ? Math.min(
62
+ effectivePage.currentPage * effectivePage.pageSize,
63
+ effectivePage.totalRows,
64
+ )
65
+ : 0,
66
+ );
67
+
68
+ $effect(() => {
69
+ if (!databasePage || isLoading) return;
70
+ const url = page.url.searchParams;
71
+ if (databasePage.tableName !== url.get('table') ||
72
+ databasePage.currentPage !== parseRawSqlitePage(url.get('page')) ||
73
+ databasePage.pageSize !== parseRawSqlitePageSize(url.get('pageSize')) ||
74
+ (databasePage.sortField ?? null) !== parseRawSqliteSortField(url.get('sort')) ||
75
+ databasePage.sortDirection !== parseRawSqliteSortDirection(url.get('direction'))) {
76
+ void updateDatabaseUrl({
77
+ table: databasePage.tableName,
78
+ page: databasePage.currentPage > 1 ? databasePage.currentPage : null,
79
+ pageSize: databasePage.pageSize === DEFAULT_RAW_SQLITE_PAGE_SIZE ? null : databasePage.pageSize,
80
+ sort: databasePage.sortField,
81
+ direction: databasePage.sortField && databasePage.sortDirection === 'desc' ? 'desc' : null,
82
+ }, true);
83
+ }
84
+ });
85
+
86
+ function selectTable(tableName: string): void {
87
+ void updateDatabaseUrl({
88
+ table: tableName,
89
+ page: null,
90
+ sort: null,
91
+ direction: null,
92
+ });
93
+ }
94
+
95
+ function selectPage(pageNumber: number): void {
96
+ if (!effectivePage) return;
97
+ const nextPage = Math.min(
98
+ Math.max(1, pageNumber),
99
+ effectivePage.totalPages,
100
+ );
101
+ void updateDatabaseUrl({
102
+ page: nextPage > 1 ? nextPage : null,
103
+ });
104
+ }
105
+
106
+ function selectPageSize(pageSize: number): void {
107
+ void updateDatabaseUrl({
108
+ page: null,
109
+ pageSize:
110
+ pageSize === DEFAULT_RAW_SQLITE_PAGE_SIZE ? null : pageSize,
111
+ });
112
+ }
113
+
114
+ function selectSorting(sorting: { id: string | null; desc: boolean }): void {
115
+ if (!effectivePage) return;
116
+ // TanStack already validated the id against live column defs; only
117
+ // accept it when it still exists on the visible page.
118
+ const validColumn = sorting.id !== null
119
+ && effectivePage.columns.some((column) => column.name === sorting.id);
120
+ void updateDatabaseUrl({
121
+ sort: validColumn ? sorting.id : null,
122
+ direction: validColumn && sorting.desc ? "desc" : null,
123
+ page: null,
124
+ });
125
+ }
126
+
127
+ function refresh(): void {
128
+ if (isLoading) return;
129
+ void refreshPageData("app:developer").catch(error => console.warn("[Database] Refresh failed", error));
130
+ }
131
+
132
+ async function updateDatabaseUrl(
133
+ updates: {
134
+ table?: string | null;
135
+ page?: string | number | null;
136
+ pageSize?: string | number | null;
137
+ sort?: string | null;
138
+ direction?: string | null;
139
+ },
140
+ replaceState = false,
141
+ ): Promise<void> {
142
+ const nextUrl = new URL(page.url);
143
+ nextUrl.searchParams.set("tab", "database");
144
+
145
+ for (const [key, value] of Object.entries(updates)) {
146
+ if (value === null || value === "") {
147
+ nextUrl.searchParams.delete(key);
148
+ } else if (value !== undefined) {
149
+ nextUrl.searchParams.set(key, String(value));
150
+ }
151
+ }
152
+
153
+ const nextPath = `${nextUrl.pathname}${nextUrl.search}${nextUrl.hash}`;
154
+ const currentPath = `${page.url.pathname}${page.url.search}${page.url.hash}`;
155
+ if (nextPath === currentPath) return;
156
+
157
+ await goto(nextPath, {
158
+ reset: false,
159
+ replaceState,
160
+ });
161
+ }
162
+ </script>
163
+
164
+ <section
165
+ class="mt-4 min-w-0 lg:mt-6"
166
+ aria-labelledby="raw-database-heading"
167
+ data-testid="database-browser"
168
+ >
169
+ <div class="stor-panel overflow-hidden">
170
+ <header class="flex flex-col gap-3 border-b border-[var(--stor-border)] px-[var(--stor-panel-padding)] py-4 sm:flex-row sm:items-center sm:justify-between">
171
+ <div>
172
+ <div class="flex items-center gap-2">
173
+ <IconDatabaseOutline class="h-5 w-5 text-[var(--stor-muted-text)]" />
174
+ <h2
175
+ id="raw-database-heading"
176
+ class="text-base font-semibold text-stone-900"
177
+ >
178
+ Local SQLite
179
+ </h2>
180
+ </div>
181
+ <p class="mt-1 text-sm text-[var(--stor-muted-text)]">
182
+ Read-only access to this tenant's projected tables in this browser.
183
+ </p>
184
+ </div>
185
+ <button
186
+ type="button"
187
+ class="inline-flex items-center justify-center gap-2 self-start rounded-lg bg-[var(--stor-surface-muted)] px-3 py-2 text-sm font-semibold text-stone-700 transition hover:bg-[var(--stor-surface-hover)] disabled:opacity-50 sm:self-auto"
188
+ onclick={refresh}
189
+ disabled={isLoading && !databasePage}
190
+ aria-disabled={isLoading}
191
+ >
192
+ <IconRefresh class="h-4 w-4 {isLoading ? 'animate-spin' : ''}" />
193
+ Refresh
194
+ </button>
195
+ </header>
196
+
197
+ {#if isLoading && !effectivePage}
198
+ <div class="flex min-h-64 items-center justify-center p-8">
199
+ <div class="text-center text-sm text-[var(--stor-muted-text)]">
200
+ <IconRefresh class="mx-auto h-6 w-6 animate-spin" />
201
+ <p class="mt-2">Reading local SQLite…</p>
202
+ </div>
203
+ </div>
204
+ {:else if effectivePage && effectivePage.tables.length === 0}
205
+ <div class="p-[var(--stor-panel-padding)]">
206
+ <EmptyState
207
+ title="No local tables"
208
+ description="The local SQLite database does not contain any application tables."
209
+ />
210
+ </div>
211
+ {:else if effectivePage}
212
+ <div class="border-b border-[var(--stor-border)] px-[var(--stor-panel-padding)] py-3 md:hidden">
213
+ <label
214
+ for="raw-sqlite-table-select"
215
+ class="text-xs font-semibold uppercase tracking-[0.1em] text-[var(--stor-muted-text)]"
216
+ >
217
+ Table
218
+ </label>
219
+ <select
220
+ id="raw-sqlite-table-select"
221
+ value={effectivePage.tableName ?? ""}
222
+ onchange={(event) =>
223
+ selectTable(
224
+ (event.currentTarget as HTMLSelectElement).value,
225
+ )}
226
+ class="mt-1.5 block w-full rounded-lg border border-[var(--stor-field-border)] bg-white px-3 py-2.5 font-mono text-sm text-stone-900 outline-none focus:border-[var(--stor-focus)] focus:ring-2 focus:ring-[var(--stor-selected)]"
227
+ >
228
+ {#each effectivePage.tables as table}
229
+ <option value={table.name}>{table.name}</option>
230
+ {/each}
231
+ </select>
232
+ </div>
233
+
234
+ <div class="grid min-w-0 md:grid-cols-[15rem_minmax(0,1fr)]">
235
+ <aside
236
+ class="hidden border-r border-[var(--stor-border)] bg-[var(--stor-surface-muted)]/60 p-3 md:block"
237
+ aria-label="SQLite tables"
238
+ >
239
+ <div class="px-2 pb-2 text-xs font-semibold uppercase tracking-[0.1em] text-[var(--stor-muted-text)]">
240
+ Tables
241
+ </div>
242
+ <nav class="max-h-[calc(100vh-16rem)] space-y-1 overflow-y-auto">
243
+ {#each effectivePage.tables as table}
244
+ <button
245
+ type="button"
246
+ class="block w-full truncate rounded-lg px-2 py-2 text-left font-mono text-sm transition {table.name ===
247
+ effectivePage.tableName
248
+ ? 'bg-[var(--stor-accent-soft)] font-semibold text-[var(--stor-accent-strong)]'
249
+ : 'text-stone-600 hover:bg-white hover:text-stone-950'}"
250
+ aria-current={table.name ===
251
+ effectivePage.tableName
252
+ ? "page"
253
+ : undefined}
254
+ title={table.name}
255
+ onclick={() => selectTable(table.name)}
256
+ >
257
+ {table.name}
258
+ </button>
259
+ {/each}
260
+ </nav>
261
+ </aside>
262
+
263
+ <div class="min-w-0">
264
+ <div class="flex flex-col gap-3 border-b border-[var(--stor-border)] px-[var(--stor-panel-padding)] py-3 sm:flex-row sm:items-center sm:justify-between">
265
+ <div class="min-w-0">
266
+ <h3 class="truncate font-mono text-sm font-semibold text-stone-950">
267
+ {effectivePage.tableName}
268
+ </h3>
269
+ <p class="mt-0.5 text-xs text-[var(--stor-muted-text)]">
270
+ {effectivePage.totalRows}
271
+ {effectivePage.totalRows === 1 ? " row" : " rows"}
272
+ · {effectivePage.columns.length}
273
+ {effectivePage.columns.length === 1
274
+ ? " column"
275
+ : " columns"}
276
+ </p>
277
+ </div>
278
+ {#if isLoading}
279
+ <div class="flex items-center gap-2 text-xs font-medium text-[var(--stor-muted-text)]">
280
+ <IconRefresh class="h-3.5 w-3.5 animate-spin" />
281
+ Updating
282
+ </div>
283
+ {/if}
284
+ </div>
285
+
286
+ <div class:opacity-60={isStale} class:transition-opacity={isStale} aria-busy={isStale || undefined}>
287
+ {#if effectivePage.rows.length === 0 && !isStale}
288
+ <div class="p-[var(--stor-panel-padding)]">
289
+ <EmptyState
290
+ title="Table is empty"
291
+ description={`${effectivePage.tableName} has no rows.`}
292
+ />
293
+ </div>
294
+ {:else}
295
+ <DataTable
296
+ columns={tableColumns}
297
+ data={effectivePage.rows}
298
+ pagination={tablePagination}
299
+ sorting={tableSorting}
300
+ onSortingChange={selectSorting}
301
+ isLoading={isLoading && !isStale}
302
+ emptyTitle="Table is empty"
303
+ emptyDescription={`${effectivePage.tableName} has no rows.`}
304
+ testId="raw-sqlite-table"
305
+ scrollTestId="raw-sqlite-table-scroll"
306
+ />
307
+ {/if}
308
+ </div>
309
+
310
+ <DataTablePagination
311
+ currentPage={effectivePage.currentPage}
312
+ totalPages={effectivePage.totalPages}
313
+ totalRows={effectivePage.totalRows}
314
+ pageSize={effectivePage.pageSize}
315
+ pageSizes={RAW_SQLITE_PAGE_SIZES}
316
+ showingStart={showingStart}
317
+ showingEnd={showingEnd}
318
+ onSelectPage={selectPage}
319
+ onSelectPageSize={selectPageSize}
320
+ />
321
+ </div>
322
+ </div>
323
+ {/if}
324
+ </div>
325
+ </section>
@@ -0,0 +1,51 @@
1
+ <script lang="ts">
2
+ import JsonViewer from "@hearthlog/ui/components/JsonViewer.svelte";
3
+ import type { AppEvent } from "@hearthlog/runtime/event/index.js";
4
+
5
+ let { event, expanded = $bindable() } = $props<{
6
+ event: AppEvent;
7
+ expanded?: boolean;
8
+ }>();
9
+
10
+ function toggle() {
11
+ expanded = !expanded;
12
+ }
13
+ </script>
14
+
15
+ <li class="stor-panel overflow-hidden">
16
+ <button
17
+ type="button"
18
+ class="flex w-full items-center justify-between gap-3 p-3 text-left transition hover:bg-[var(--stor-surface-muted)] focus:outline-none focus:ring-2 focus:ring-inset focus:ring-stone-300"
19
+ onclick={toggle}
20
+ aria-expanded={expanded}
21
+ >
22
+ <div class="min-w-0">
23
+ <div class="truncate text-sm font-semibold text-stone-900">
24
+ {event.type}
25
+ </div>
26
+ <div class="mt-1 text-xs text-[var(--stor-muted-text)]">
27
+ {new Date(event.timestamp).toLocaleString()}
28
+ </div>
29
+ </div>
30
+ <div class="flex shrink-0 items-center gap-2">
31
+ {#if event.id}
32
+ <span
33
+ class="hidden rounded-full bg-[var(--stor-surface-muted)] px-2 py-1 text-[10.5px] font-semibold text-[var(--stor-muted-text)] sm:inline"
34
+ >
35
+ {String(event.id).slice(0, 8)}
36
+ </span>
37
+ {/if}
38
+ <span class="rounded-full border border-[var(--stor-border)] px-2.5 py-1 text-xs font-semibold text-stone-600">
39
+ {expanded ? "Hide" : "Payload"}
40
+ </span>
41
+ </div>
42
+ </button>
43
+ {#if expanded}
44
+ <div class="border-t border-[var(--stor-border)] p-3">
45
+ <JsonViewer
46
+ value={event.payload}
47
+ label={`${event.type} event payload`}
48
+ />
49
+ </div>
50
+ {/if}
51
+ </li>