@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.
- 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,226 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import { untrack } from "svelte";
|
|
3
|
+
import CommandDrawer from "@hearthlog/ui/components/commands/CommandDrawer.svelte";
|
|
4
|
+
import DataPlaceholder from "@hearthlog/ui/components/data/DataPlaceholder.svelte";
|
|
5
|
+
import Button from "@hearthlog/ui/components/ui/Button.svelte";
|
|
6
|
+
import Checkbox from "@hearthlog/ui/components/ui/Checkbox.svelte";
|
|
7
|
+
import Select, { type SelectOption } from "@hearthlog/ui/components/ui/Select.svelte";
|
|
8
|
+
import SuggestionChip from "./SuggestionChip.svelte";
|
|
9
|
+
import SuggestionMark from "./SuggestionMark.svelte";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Reviews a backlog of rows that still lack one value (a category, an
|
|
13
|
+
* expense type). Each row shows a suggestion: a history match or a
|
|
14
|
+
* confident model pick is filled and ticked, a less sure pick is a chip,
|
|
15
|
+
* and the rest says Choose. Apply hands the ticked rows to the page as one
|
|
16
|
+
* action; nothing is written here.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface ReviewRow {
|
|
20
|
+
id: string;
|
|
21
|
+
title: string;
|
|
22
|
+
/** One support line under the title. */
|
|
23
|
+
meta: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ReviewSuggestion {
|
|
27
|
+
value: string;
|
|
28
|
+
/** The value's label, for the chip. */
|
|
29
|
+
label: string;
|
|
30
|
+
kind: "ai" | "history";
|
|
31
|
+
/** A plain reason for a history match; empty for a model's pick. */
|
|
32
|
+
reason: string;
|
|
33
|
+
band: "apply" | "offer";
|
|
34
|
+
/** What the page stores as provenance when the value is kept. */
|
|
35
|
+
source: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface ReviewItem {
|
|
39
|
+
row: ReviewRow;
|
|
40
|
+
previous: { value: string; source: string };
|
|
41
|
+
suggestion: ReviewSuggestion | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ReviewChange {
|
|
45
|
+
id: string;
|
|
46
|
+
value: string;
|
|
47
|
+
source: string;
|
|
48
|
+
previous: { value: string; source: string };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
interface Entry extends ReviewRow {
|
|
52
|
+
previous: { value: string; source: string };
|
|
53
|
+
choice: string;
|
|
54
|
+
source: string;
|
|
55
|
+
ticked: boolean;
|
|
56
|
+
applied: ReviewSuggestion | null;
|
|
57
|
+
offered: ReviewSuggestion | null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
let {
|
|
61
|
+
show,
|
|
62
|
+
title,
|
|
63
|
+
description = "",
|
|
64
|
+
fieldLabel,
|
|
65
|
+
options,
|
|
66
|
+
unsetValue,
|
|
67
|
+
emptyText,
|
|
68
|
+
loadingLabel,
|
|
69
|
+
applyTestId = "apply-suggestions",
|
|
70
|
+
loadKey,
|
|
71
|
+
load,
|
|
72
|
+
onClose,
|
|
73
|
+
onApply,
|
|
74
|
+
}: {
|
|
75
|
+
show: boolean;
|
|
76
|
+
title: string;
|
|
77
|
+
description?: string;
|
|
78
|
+
/** What is being chosen, for accessible names ("Category"). */
|
|
79
|
+
fieldLabel: string;
|
|
80
|
+
/** Includes the unset option, labelled Choose. */
|
|
81
|
+
options: SelectOption[];
|
|
82
|
+
/** The option value that means nothing is chosen yet. */
|
|
83
|
+
unsetValue: string;
|
|
84
|
+
emptyText: string;
|
|
85
|
+
loadingLabel: string;
|
|
86
|
+
applyTestId?: string;
|
|
87
|
+
/** Reloads the rows while the sheet is open when this changes. */
|
|
88
|
+
loadKey: string;
|
|
89
|
+
load: () => Promise<ReviewItem[]>;
|
|
90
|
+
onClose: () => void;
|
|
91
|
+
/** Writes every change in one action. Throws when saving fails. */
|
|
92
|
+
onApply: (changes: ReviewChange[]) => Promise<void>;
|
|
93
|
+
} = $props();
|
|
94
|
+
|
|
95
|
+
let entries = $state<Entry[]>([]);
|
|
96
|
+
let loading = $state(false);
|
|
97
|
+
let busy = $state(false);
|
|
98
|
+
let errorMessage = $state("");
|
|
99
|
+
let run = 0;
|
|
100
|
+
|
|
101
|
+
const ticked = $derived(entries.filter((entry) => entry.ticked));
|
|
102
|
+
|
|
103
|
+
$effect(() => {
|
|
104
|
+
void loadKey;
|
|
105
|
+
if (!show) {
|
|
106
|
+
run += 1;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
void untrack(() => reload());
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
async function reload(): Promise<void> {
|
|
113
|
+
const current = ++run;
|
|
114
|
+
loading = true;
|
|
115
|
+
errorMessage = "";
|
|
116
|
+
entries = [];
|
|
117
|
+
try {
|
|
118
|
+
const items = await load();
|
|
119
|
+
// The sheet may have closed or moved on while the model was thinking.
|
|
120
|
+
if (current !== run) return;
|
|
121
|
+
entries = items.map(({ row, previous, suggestion }) => {
|
|
122
|
+
const apply = suggestion?.band === "apply";
|
|
123
|
+
return {
|
|
124
|
+
...row,
|
|
125
|
+
previous,
|
|
126
|
+
choice: apply ? suggestion!.value : unsetValue,
|
|
127
|
+
source: apply ? suggestion!.source : "",
|
|
128
|
+
ticked: apply,
|
|
129
|
+
applied: apply ? suggestion! : null,
|
|
130
|
+
offered: suggestion && !apply ? suggestion : null,
|
|
131
|
+
};
|
|
132
|
+
});
|
|
133
|
+
} catch (error) {
|
|
134
|
+
if (current !== run) return;
|
|
135
|
+
console.error(`[AI] Could not load rows for "${title}":`, error);
|
|
136
|
+
errorMessage = "The rows could not be loaded.";
|
|
137
|
+
} finally {
|
|
138
|
+
if (current === run) loading = false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function choose(entry: Entry, value: string): void {
|
|
143
|
+
entry.choice = value;
|
|
144
|
+
entry.source = value === unsetValue ? "" : "manual";
|
|
145
|
+
entry.ticked = value !== unsetValue;
|
|
146
|
+
entry.applied = null;
|
|
147
|
+
entry.offered = null;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function takeOffer(entry: Entry): void {
|
|
151
|
+
const offer = entry.offered;
|
|
152
|
+
if (!offer) return;
|
|
153
|
+
entry.choice = offer.value;
|
|
154
|
+
entry.source = offer.source;
|
|
155
|
+
entry.applied = offer;
|
|
156
|
+
entry.offered = null;
|
|
157
|
+
entry.ticked = true;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async function apply(): Promise<void> {
|
|
161
|
+
if (busy || ticked.length === 0) return;
|
|
162
|
+
busy = true;
|
|
163
|
+
errorMessage = "";
|
|
164
|
+
try {
|
|
165
|
+
await onApply(ticked.map((entry) => ({ id: entry.id, value: entry.choice, source: entry.source, previous: entry.previous })));
|
|
166
|
+
} catch {
|
|
167
|
+
// The ticks stay, so the same Apply can be retried.
|
|
168
|
+
errorMessage = "The changes could not be saved. Your choices are still here.";
|
|
169
|
+
} finally {
|
|
170
|
+
busy = false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
</script>
|
|
174
|
+
|
|
175
|
+
<CommandDrawer {show} {title} {description} size="lg" onClose={() => { if (!busy) onClose(); }}>
|
|
176
|
+
{#if loading}
|
|
177
|
+
<DataPlaceholder rows={6} label={loadingLabel} />
|
|
178
|
+
{:else if entries.length === 0}
|
|
179
|
+
<p class="text-sm text-[var(--stor-muted-text)]">{errorMessage || emptyText}</p>
|
|
180
|
+
{:else}
|
|
181
|
+
<ul class="divide-y divide-[var(--stor-border)]" aria-label={title}>
|
|
182
|
+
{#each entries as entry (entry.id)}
|
|
183
|
+
<li class="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-x-3 gap-y-1.5 py-2.5 sm:grid-cols-[auto_minmax(0,1fr)_12rem]">
|
|
184
|
+
<Checkbox
|
|
185
|
+
checked={entry.ticked}
|
|
186
|
+
disabled={busy || entry.choice === unsetValue}
|
|
187
|
+
onCheckedChange={(value) => (entry.ticked = value === true)}
|
|
188
|
+
aria-label={`Apply ${fieldLabel.toLowerCase()} to ${entry.title}`}
|
|
189
|
+
/>
|
|
190
|
+
<div class="min-w-0">
|
|
191
|
+
<div class="truncate text-sm font-medium text-[var(--stor-ink)]">{entry.title}</div>
|
|
192
|
+
<div class="truncate text-xs text-[var(--stor-muted-text)] tabular-nums">{entry.meta}</div>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="col-start-2 min-w-0 sm:col-start-3">
|
|
195
|
+
<Select
|
|
196
|
+
value={entry.choice}
|
|
197
|
+
{options}
|
|
198
|
+
disabled={busy}
|
|
199
|
+
ariaLabel={`${fieldLabel} for ${entry.title}`}
|
|
200
|
+
onValueChange={(value) => choose(entry, value)}
|
|
201
|
+
/>
|
|
202
|
+
</div>
|
|
203
|
+
{#if entry.applied || entry.offered}
|
|
204
|
+
<div class="col-start-2 flex min-h-5 items-center sm:col-span-3 sm:col-start-2" aria-live="polite">
|
|
205
|
+
{#if entry.applied}
|
|
206
|
+
<SuggestionMark kind={entry.applied.kind} reason={entry.applied.reason} />
|
|
207
|
+
{:else if entry.offered}
|
|
208
|
+
<SuggestionChip label={entry.offered.label} disabled={busy} onclick={() => takeOffer(entry)} />
|
|
209
|
+
{/if}
|
|
210
|
+
</div>
|
|
211
|
+
{/if}
|
|
212
|
+
</li>
|
|
213
|
+
{/each}
|
|
214
|
+
</ul>
|
|
215
|
+
{#if errorMessage}
|
|
216
|
+
<div class="mt-3 rounded-xl border border-red-200 bg-red-50 px-3 py-2 text-sm text-red-700" role="alert">{errorMessage}</div>
|
|
217
|
+
{/if}
|
|
218
|
+
{/if}
|
|
219
|
+
|
|
220
|
+
{#snippet footer()}
|
|
221
|
+
<Button variant="secondary" size="sm" onclick={onClose} disabled={busy}>Cancel</Button>
|
|
222
|
+
<Button data-testid={applyTestId} disabled={busy || ticked.length === 0} onclick={() => void apply()}>
|
|
223
|
+
{busy ? "Saving..." : ticked.length === 0 ? "Apply" : `Apply ${ticked.length}`}
|
|
224
|
+
</Button>
|
|
225
|
+
{/snippet}
|
|
226
|
+
</CommandDrawer>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import IconCreation from "~icons/mdi/creation";
|
|
3
|
+
|
|
4
|
+
/** Asks for a suggestion on demand, so the person sees what happened instead of waiting for one. */
|
|
5
|
+
let {
|
|
6
|
+
label,
|
|
7
|
+
busy = false,
|
|
8
|
+
disabled = false,
|
|
9
|
+
onclick,
|
|
10
|
+
}: { label: string; busy?: boolean; disabled?: boolean; onclick: () => void } = $props();
|
|
11
|
+
</script>
|
|
12
|
+
|
|
13
|
+
<button
|
|
14
|
+
type="button"
|
|
15
|
+
class="inline-flex h-8 items-center gap-1.5 rounded-full px-2.5 text-xs font-semibold text-[var(--stor-info)] transition hover:bg-[var(--stor-info-soft)] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--stor-focus)] disabled:pointer-events-none disabled:opacity-50"
|
|
16
|
+
aria-label={label}
|
|
17
|
+
title={label}
|
|
18
|
+
disabled={disabled || busy}
|
|
19
|
+
{onclick}
|
|
20
|
+
>
|
|
21
|
+
<IconCreation class="h-4 w-4 {busy ? 'motion-safe:animate-pulse' : ''}" />
|
|
22
|
+
{busy ? "Thinking…" : "Suggest"}
|
|
23
|
+
</button>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/** A suggestion the model is not sure enough to apply: one tap fills it. */
|
|
3
|
+
let { label, onclick, disabled = false }: { label: string; onclick: () => void; disabled?: boolean } = $props();
|
|
4
|
+
</script>
|
|
5
|
+
|
|
6
|
+
<button
|
|
7
|
+
type="button"
|
|
8
|
+
class="inline-flex min-h-7 items-center rounded-full border border-[var(--stor-info-border)] bg-[var(--stor-info-soft)] px-2.5 text-[11px] font-semibold text-[var(--stor-info)] transition hover:brightness-95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--stor-focus)] disabled:opacity-50"
|
|
9
|
+
{disabled}
|
|
10
|
+
{onclick}
|
|
11
|
+
>
|
|
12
|
+
Suggested: {label}
|
|
13
|
+
</button>
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import Badge from "@hearthlog/ui/components/ui/Badge.svelte";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Where a filled value came from. A model's pick wears the blue Suggested
|
|
6
|
+
* tag; the history rule shows only its plain reason, with no AI mark.
|
|
7
|
+
*/
|
|
8
|
+
let { kind, reason = "" }: { kind: "ai" | "history"; reason?: string } = $props();
|
|
9
|
+
</script>
|
|
10
|
+
|
|
11
|
+
{#if kind === "ai"}
|
|
12
|
+
<Badge variant="info" title="Picked by AI. Saving it is your choice.">Suggested</Badge>
|
|
13
|
+
{:else if reason}
|
|
14
|
+
<span class="text-[11px] leading-4 text-[var(--stor-muted-text)]">{reason}</span>
|
|
15
|
+
{/if}
|