feat: the redesigned Event Clock

The new reader app replaces the old UI: Checklist with a rail (focus
pills, Next to expire, today's dailies, pinned routines), Timeline in a
new and a classic look, Routines, Archive and a Settings area, in a
Structured (default) and a Relaxed interface. It follows the approved
Round 6 B and Round 7 mockups, corrected screen by screen with the owner.

Reader data moves to a v2 store (gacha-tracker:v2:*). Existing v1 data
is migrated on first load, a recovery copy is written first, and the v1
keys are never removed, so a rollback finds them unchanged. Redeem codes
for Genshin arrive as their own feed.

Also: live's urgency ramp, editable per theme; completion and
section-complete fireworks; a returning-user tour; the design mockups,
specs, plans, reviews and handoff record.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-18 01:55:16 +02:00
co-authored by Claude Opus 5.5
parent ddd9673f86
commit 50c6e91b00
284 changed files with 45100 additions and 1628 deletions
+59
View File
@@ -0,0 +1,59 @@
import { parseReaderExport, buildReaderExport } from "./export.ts";
import { READER_KEYS, type SaveResult } from "./schema.ts";
import { readLegacy, tryWriteJson } from "./store.ts";
export function finishPendingRestore(storage: Storage): SaveResult {
try {
const raw = storage.getItem(READER_KEYS.pending);
if (raw === null) return { ok: true };
const pending = JSON.parse(raw) as { payload?: unknown };
const reader = parseReaderExport(pending.payload);
if (!reader) return { ok: false, reason: "invalid" };
const result = tryWriteJson(storage, READER_KEYS.reader, reader);
if (!result.ok) return result;
storage.removeItem(READER_KEYS.pending);
return { ok: true };
} catch {
return { ok: false, reason: "unavailable" };
}
}
export function prepareRestore(
storage: Storage,
payload: unknown,
at: string,
): SaveResult {
const reader = parseReaderExport(payload);
if (!reader) return { ok: false, reason: "invalid" };
try {
const oldRaw = storage.getItem(READER_KEYS.reader);
if (oldRaw === null) return { ok: false, reason: "invalid" };
const before = parseReaderExport({
format: "gacha-tracker-export",
version: 2,
reader: JSON.parse(oldRaw),
});
if (!before) return { ok: false, reason: "invalid" };
const recovery = tryWriteJson(
storage,
READER_KEYS.recovery,
buildReaderExport(before, readLegacy(storage), at),
);
if (!recovery.ok) return recovery;
const prepared = tryWriteJson(storage, READER_KEYS.pending, {
at,
payload: buildReaderExport(reader, {}, at),
});
if (!prepared.ok) return prepared;
return finishPendingRestore(storage);
} catch {
return { ok: false, reason: "unavailable" };
}
}
export function undoRestore(storage: Storage): SaveResult {
try {
const raw = storage.getItem(READER_KEYS.recovery);
if (raw === null) return { ok: false, reason: "invalid" };
return prepareRestore(storage, JSON.parse(raw), new Date().toISOString());
} catch {
return { ok: false, reason: "unavailable" };
}
}