@hearthlog/shell 0.0.0-stage → 0.1.1
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/LICENSE +235 -0
- package/package.json +51 -3
- package/src/app.template.html +184 -0
- package/src/components/ai/DecisionReviewSheet.svelte +226 -0
- package/src/components/ai/SuggestButton.svelte +23 -0
- package/src/components/ai/SuggestionChip.svelte +13 -0
- package/src/components/ai/SuggestionMark.svelte +15 -0
- package/src/components/calendar/CalendarFeedsDrawer.svelte +414 -0
- package/src/components/calendar/CalendarMonthSummary.svelte +122 -0
- package/src/components/calendar/calendar-weeks.svelte.ts +27 -0
- package/src/components/calendar/source-icons.ts +19 -0
- package/src/components/data-table/DataTableValue.svelte +14 -0
- package/src/components/data-table/EntityActionsCell.svelte +36 -0
- package/src/components/data-table/EntityRefsCell.svelte +58 -0
- package/src/components/data-table/EntityValue.svelte +42 -0
- package/src/components/data-table/entity-columns.ts +87 -0
- package/src/components/data-table/sqlite-columns.ts +42 -0
- package/src/components/entities/CalendarSourceFields.svelte +149 -0
- package/src/components/history/ChangeRecord.svelte +30 -0
- package/src/components/history/ForkComposer.svelte +174 -0
- package/src/components/history/HistoryScrubber.svelte +245 -0
- package/src/components/history/LogHistory.svelte +334 -0
- package/src/components/history/LogMap.svelte +293 -0
- package/src/components/history/LogTree.svelte +124 -0
- package/src/components/layout/AccountMenu.svelte +111 -0
- package/src/components/layout/AppearanceOptions.svelte +78 -0
- package/src/components/layout/BrandWordmark.svelte +12 -0
- package/src/components/layout/ErrorToast.svelte +34 -0
- package/src/components/layout/LogCreateDrawer.svelte +111 -0
- package/src/components/layout/LogPanel.svelte +225 -0
- package/src/components/layout/LogStatusChip.svelte +137 -0
- package/src/components/layout/MobilePageAction.svelte +44 -0
- package/src/components/layout/MovedBanner.svelte +51 -0
- package/src/components/layout/NavBar.svelte +603 -0
- package/src/components/layout/NavFlyout.svelte +132 -0
- package/src/components/layout/NavItem.svelte +36 -0
- package/src/components/layout/NavMoreDialog.svelte +179 -0
- package/src/components/layout/PageDataStatus.svelte +36 -0
- package/src/components/layout/PhoneGlance.svelte +96 -0
- package/src/components/layout/SidebarResizer.svelte +122 -0
- package/src/components/layout/SyncIndicator.svelte +24 -0
- package/src/components/layout/feature-icons.ts +24 -0
- package/src/components/layout/nav-indicator.ts +128 -0
- package/src/components/layout/sidebar-size.ts +31 -0
- package/src/components/sync/SyncIssuesDrawer.svelte +149 -0
- package/src/components/sync/SyncStateBadge.svelte +17 -0
- package/src/pages/calendar/Page.svelte +1341 -0
- package/src/pages/calendar/load.ts +77 -0
- package/src/pages/developer/DatabaseBrowser.svelte +325 -0
- package/src/pages/developer/Event.svelte +51 -0
- package/src/pages/developer/EventLogManager.svelte +268 -0
- package/src/pages/developer/IndexedDbStats.svelte +93 -0
- package/src/pages/developer/LedgerAdmin.svelte +877 -0
- package/src/pages/developer/LocalSqliteStats.svelte +56 -0
- package/src/pages/developer/Page.svelte +590 -0
- package/src/pages/developer/PendingWorkImport.svelte +49 -0
- package/src/pages/developer/PerformanceDashboard.svelte +262 -0
- package/src/pages/developer/StackInfo.svelte +117 -0
- package/src/pages/developer/SyncLog.svelte +188 -0
- package/src/pages/developer/load.ts +28 -0
- package/src/pages/entities/Page.svelte +1069 -0
- package/src/pages/entities/load.ts +18 -0
- package/src/pages/entity/Page.svelte +655 -0
- package/src/pages/entity/load.ts +79 -0
- package/src/pages/error/ErrorPage.svelte +147 -0
- package/src/pages/layout/Layout.svelte +597 -0
- package/src/pages/layout/load.ts +28 -0
- package/src/pages/redirects/history.ts +20 -0
- package/src/pages/redirects/start.ts +11 -0
- package/src/pages/redirects/today.ts +9 -0
- package/src/service-worker/index.ts +108 -0
- package/src/service-worker/offline-bundle.ts +116 -0
- package/src/service-worker/virtual.d.ts +3 -0
- package/src/styles/index.css +11 -0
- package/src/styles/shell.css +296 -0
- package/static/_headers +4 -0
- package/static/favicon.ico +0 -0
- package/static/offline.html +44 -0
- package/static/replay/sw.js +2 -0
- 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>
|