@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.
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,108 @@
1
+ /// <reference lib="webworker" />
2
+ // SvelteKit bundles this reserved entry as a module worker; the precache list
3
+ // comes from $app/manifest instead of a build-time Workbox injection.
4
+
5
+ import { version } from '$app/env';
6
+ import { assets, immutable, prerendered } from '$app/manifest';
7
+ import { self } from '$app/service-worker';
8
+ import { workerAssets } from 'virtual:worker-assets';
9
+ import { clientsClaim, type RouteHandlerCallback } from 'workbox-core';
10
+ import { ExpirationPlugin } from 'workbox-expiration';
11
+ import {
12
+ cleanupOutdatedCaches,
13
+ createHandlerBoundToURL,
14
+ precacheAndRoute,
15
+ } from 'workbox-precaching';
16
+ import { NavigationRoute, registerRoute } from 'workbox-routing';
17
+ import { CacheFirst, NetworkOnly } from 'workbox-strategies';
18
+
19
+ import { ACCESS_REAUTH_QUERY_PARAM } from '@hearthlog/runtime/access/session.js';
20
+ import { PRECACHE_EXTRA_PATHS, precacheEntries } from './offline-bundle.js';
21
+
22
+ const APP_SHELL_URL = '/index.html';
23
+
24
+ self.skipWaiting();
25
+ clientsClaim();
26
+ precacheAndRoute(precacheManifest());
27
+ cleanupOutdatedCaches();
28
+
29
+ // Hashed build output is content-addressed; static files and prerendered pages
30
+ // are revisioned with the deployment version. The SPA fallback shell is added
31
+ // explicitly for offline navigation.
32
+ function precacheManifest() {
33
+ const withRevision = (revision: string | null) => ({ path }: { path: string }) => ({ path, revision });
34
+ return precacheEntries([
35
+ ...immutable.map(withRevision(null)),
36
+ ...workerAssets.map(withRevision(null)),
37
+ ...assets.map(withRevision(version)),
38
+ ...prerendered.map(withRevision(version)),
39
+ ...PRECACHE_EXTRA_PATHS.map((path) => ({ path, revision: version })),
40
+ ], import.meta.env.VITE_STOR_TENANT);
41
+ }
42
+
43
+ const appShell = createHandlerBoundToURL(APP_SHELL_URL);
44
+
45
+ const navigationHandler: RouteHandlerCallback = async (options) => {
46
+ try {
47
+ return await fetch(options.request);
48
+ } catch {
49
+ return appShell(options);
50
+ }
51
+ };
52
+
53
+ registerRoute(new NavigationRoute(navigationHandler, {
54
+ // Cloudflare Access login/logout must always reach the edge. Serving a
55
+ // cached shell (or an offline fallback) for /cdn-cgi/* leaves the user
56
+ // on a dead session with a logout button that appears to do nothing.
57
+ // Recovery navigations are excluded for the same reason: they must reach
58
+ // the edge, and the browser has to follow Access's cross-origin login
59
+ // redirect natively instead of the route replaying a redirected response.
60
+ denylist: [
61
+ /\/api\//,
62
+ /\/logs\/.*\/events\/stream/,
63
+ /\/cdn-cgi\//,
64
+ new RegExp(`[?&]${ACCESS_REAUTH_QUERY_PARAM}=`),
65
+ ],
66
+ }));
67
+
68
+ const immutableAssetStrategy = new CacheFirst({
69
+ cacheName: 'assets-cache',
70
+ plugins: [
71
+ {
72
+ cacheWillUpdate: async ({ response }: { response: Response }) =>
73
+ response.ok && !isHtmlResponse(response) ? response : null,
74
+ },
75
+ new ExpirationPlugin({
76
+ maxEntries: 200,
77
+ maxAgeSeconds: 60 * 60 * 24 * 90,
78
+ purgeOnQuotaError: true,
79
+ }),
80
+ ],
81
+ });
82
+
83
+ registerRoute(
84
+ ({ url, sameOrigin }) => sameOrigin && url.pathname.startsWith('/_app/immutable/'),
85
+ async (options) => {
86
+ const response = await immutableAssetStrategy.handle(options);
87
+ if (!isHtmlResponse(response)) return response;
88
+ return new Response('Not found', {
89
+ status: 404,
90
+ headers: {
91
+ 'Cache-Control': 'no-store',
92
+ 'Content-Type': 'text/plain; charset=utf-8',
93
+ 'X-Content-Type-Options': 'nosniff',
94
+ },
95
+ });
96
+ },
97
+ );
98
+
99
+ registerRoute(
100
+ ({ url, sameOrigin }) => sameOrigin && url.pathname.startsWith('/api/'),
101
+ // Local SQLite plus the app shell provide offline operation. Caching a
102
+ // protected API response risks retaining an Access login page as data.
103
+ new NetworkOnly(),
104
+ );
105
+
106
+ function isHtmlResponse(response: Response): boolean {
107
+ return response.headers.get('Content-Type')?.toLowerCase().includes('text/html') ?? false;
108
+ }
@@ -0,0 +1,116 @@
1
+ export interface PrecacheManifestEntry {
2
+ url: string;
3
+ revision?: string | null;
4
+ size?: number;
5
+ integrity?: string;
6
+ }
7
+
8
+ /**
9
+ * Converts generated client URLs to their deployed, build-root-relative path.
10
+ * SvelteKit's PWA build runs before adapter-static moves `client/*` into the
11
+ * build root, so the prefix must be removed from the emitted manifest too.
12
+ */
13
+ export function normalize(url: string): string {
14
+ const withoutOrigin = url.replace(/^[a-z][a-z\d+.-]*:\/\/[^/]+/i, '');
15
+ return withoutOrigin
16
+ .split(/[?#]/, 1)[0]
17
+ .replace(/^\/+/, '')
18
+ .replace(/^client\//, '');
19
+ }
20
+
21
+ export function toPrecacheUrl(url: string): string {
22
+ return `/${normalize(url)}`;
23
+ }
24
+
25
+ export function normalizeManifestEntries<T extends PrecacheManifestEntry>(
26
+ entries: T[],
27
+ ): T[] {
28
+ const entriesByUrl = new Map<string, T>();
29
+
30
+ for (const entry of entries) {
31
+ const normalizedEntry = { ...entry, url: toPrecacheUrl(entry.url) };
32
+ const existing = entriesByUrl.get(normalizedEntry.url);
33
+ if (!existing || (existing.revision == null && normalizedEntry.revision != null)) {
34
+ entriesByUrl.set(normalizedEntry.url, normalizedEntry as T);
35
+ }
36
+ }
37
+
38
+ return [...entriesByUrl.values()];
39
+ }
40
+
41
+ export function parseRouteNodeIds(serverManifest: string, routeIds: readonly string[]): number[] {
42
+ const result = new Set<number>([0]);
43
+ for (const routeId of routeIds) {
44
+ const escaped = routeId.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
45
+ const match = serverManifest.match(new RegExp(
46
+ `id:\\s*"${escaped}"[\\s\\S]*?page:\\s*\\{\\s*layouts:\\s*\\[([^\\]]*)\\],\\s*errors:\\s*\\[([^\\]]*)\\],\\s*leaf:\\s*(\\d+)`,
47
+ ));
48
+ if (!match) continue;
49
+ for (const layout of match[1].matchAll(/\d+/g)) result.add(Number(layout[0]));
50
+ for (const error of match[2].matchAll(/\d+/g)) result.add(Number(error[0]));
51
+ result.add(Number(match[3]));
52
+ }
53
+ return [...result];
54
+ }
55
+
56
+ export function isCanonicalSqliteWasm(url: string): boolean {
57
+ return /^_app\/immutable\/workers\/assets\/sqlite3[.-][^/]+\.wasm$/.test(normalize(url));
58
+ }
59
+
60
+ export function isGenericSqliteWorker(url: string): boolean {
61
+ return /^_app\/immutable\/workers\/sqlite3-worker1-[^/]+\.js$/.test(normalize(url));
62
+ }
63
+
64
+ function activeTenantId(): string {
65
+ return process.env.VITE_STOR_TENANT?.trim() || 'st';
66
+ }
67
+
68
+ export function shouldPrecacheManifestEntry(
69
+ entry: PrecacheManifestEntry,
70
+ tenantId: string = activeTenantId(),
71
+ ): boolean {
72
+ const normalizedUrl = normalize(entry.url);
73
+
74
+ if (/^_app\/immutable\/(?:workers\/)?assets\/sqlite3[.-][^/]+\.wasm$/.test(normalizedUrl)) {
75
+ return isCanonicalSqliteWasm(normalizedUrl);
76
+ }
77
+ if (isGenericSqliteWorker(normalizedUrl)) return false;
78
+ // Every tenant's icon set ships in static/, but a build installs only its own.
79
+ const iconSet = /^icons\/([^/]+)\//.exec(normalizedUrl)?.[1];
80
+ if (iconSet !== undefined) return iconSet === (tenantId?.trim() || 'st');
81
+ // Install the complete app, including lazy route and overlay chunks. A
82
+ // fresh device can then open any local page without visiting it online.
83
+ return true;
84
+ }
85
+
86
+ /**
87
+ * Files outside `$app/manifest`: the SPA fallback shell and the manifest the
88
+ * tenant plugin emits at build time.
89
+ */
90
+ export const PRECACHE_EXTRA_PATHS = ['/index.html', '/manifest.webmanifest'] as const;
91
+
92
+ const PRECACHE_FILE = /\.(?:js|mjs|css|html|ico|png|svg|woff2?|webmanifest|wasm)$/;
93
+
94
+ /**
95
+ * The service worker's core precache list: build output, static files and
96
+ * prerendered pages from `$app/manifest`, plus the SPA shell. The worker and
97
+ * the build checks share this so they cannot disagree about what installs.
98
+ */
99
+ export function precacheEntries(
100
+ paths: readonly { path: string; revision: string | null }[],
101
+ tenantId?: string,
102
+ ): PrecacheManifestEntry[] {
103
+ return normalizeManifestEntries(
104
+ paths.map(({ path, revision }) => ({ url: path, revision })),
105
+ )
106
+ .filter((entry) => PRECACHE_FILE.test(entry.url))
107
+ .filter((entry) => shouldPrecacheManifestEntry(entry, tenantId));
108
+ }
109
+
110
+ /** Reads the `$app/manifest` paths that Kit bundles into the built worker. */
111
+ export function parseServiceWorkerManifestPaths(serviceWorker: string): string[] {
112
+ return Array.from(
113
+ serviceWorker.matchAll(/\{\s*path\s*:\s*[`"']([^`"']+)[`"']\s*\}/g),
114
+ (match) => match[1],
115
+ );
116
+ }
@@ -0,0 +1,3 @@
1
+ declare module 'virtual:worker-assets' {
2
+ export const workerAssets: Array<{ path: string }>;
3
+ }
@@ -0,0 +1,11 @@
1
+ /*
2
+ * The shell's stylesheet: navigation, the dock and mobile chrome, the calendar page, and the
3
+ * native-shell tweaks. Import it after the design system's:
4
+ *
5
+ * @import '@hearthlog/ui/styles/index.css';
6
+ * @import '@hearthlog/shell/styles/index.css';
7
+ */
8
+ @import './shell.css';
9
+
10
+ /* The shell's components use Tailwind classes; scan them wherever the host's source root is. */
11
+ @source '../';
@@ -0,0 +1,296 @@
1
+ @layer components {
2
+ .stor-native-field { width: 100%; min-height: 44px; border: 1px solid var(--stor-field-border); border-radius: 12px; background: var(--stor-surface-muted); padding: 0 12px; color: var(--stor-ink); font-size: 16px; outline: none; }
3
+ .stor-native-field:focus { border-color: var(--stor-focus); box-shadow: 0 0 0 2px var(--stor-selected); }
4
+ .stor-native-field:disabled { cursor: not-allowed; color: var(--stor-muted-text); }
5
+ @media (min-width: 640px) { .stor-native-field { min-height: 36px; font-size: 14px; } }
6
+ /* Navigation: the current destination is a raised pill, the same selection as tabs. */
7
+ :root { --stor-nav-hover: color-mix(in srgb, var(--stor-ink) 5%, transparent); }
8
+ /* The pill glides between rows (navIndicator). The selected row keeps its own look until the pill is placed. */
9
+ .stor-nav-indicator { position: absolute; z-index: 0; left: 0; top: 0; border-radius: 10px; opacity: 0; pointer-events: none; will-change: transform; }
10
+ .stor-nav-sliding .stor-nav-indicator { opacity: 1; }
11
+ .stor-nav-badge { min-width: 20px; height: 18px; padding: 0 6px; border-radius: 999px; font-size: 10.5px; font-weight: 650; line-height: 18px; text-align: center; font-variant-numeric: tabular-nums; }
12
+ .stor-nav-badge-negative { background: var(--stor-negative-soft); color: var(--stor-negative); }
13
+ .stor-nav-badge-warning { background: var(--stor-warning-soft); color: var(--stor-warning); }
14
+ .stor-nav-badge-dot { position: absolute; top: 7px; right: 8px; min-width: 0; width: 8px; height: 8px; padding: 0; box-shadow: 0 0 0 2px var(--stor-sidebar); }
15
+ .stor-nav-badge-dot.stor-nav-badge-negative { background: var(--stor-negative); }
16
+ .stor-nav-badge-dot.stor-nav-badge-warning { background: var(--stor-warning); }
17
+ /* Sub-pages open and close in place, so the rows below glide instead of jumping. */
18
+ .stor-nav-sub { display: grid; grid-template-rows: 0fr; transition: grid-template-rows 200ms ease; }
19
+ .stor-nav-sub[data-open] { grid-template-rows: 1fr; }
20
+ .stor-nav-sub-in { min-height: 0; overflow: hidden; visibility: hidden; transition: visibility 0s linear 200ms; }
21
+ .stor-nav-sub[data-open] > .stor-nav-sub-in { visibility: visible; transition-delay: 0s; }
22
+ /* Rail flyout: a section's pages beside the rail, opened by hover or keyboard focus (NavFlyout). */
23
+ .stor-nav-fly { position: fixed; z-index: 60; width: 13rem; padding: 8px; border-radius: 16px; background: var(--stor-surface-overlay); box-shadow: var(--stor-elevation-3); opacity: 0; visibility: hidden; transform: translateX(-4px); transition: opacity 140ms ease, transform 140ms ease, visibility 0s linear 140ms; }
24
+ .stor-nav-fly[data-open] { opacity: 1; visibility: visible; transform: none; transition-delay: 0s; }
25
+ /* A bridge across the gap keeps the pointer inside while it travels from the rail to the flyout. */
26
+ .stor-nav-fly::before { content: ""; position: absolute; left: -14px; top: 0; bottom: 0; width: 14px; }
27
+ .stor-nav-fly ul { display: grid; gap: 1px; margin: 0; padding: 0; list-style: none; }
28
+ .stor-nav-fly-head { display: flex; align-items: center; justify-content: space-between; gap: 8px; padding: 4px 8px 8px; font-size: 13px; font-weight: 600; color: var(--stor-ink); }
29
+ .stor-nav-fly .stor-nav-child { height: 32px; padding: 0 8px; }
30
+ .stor-nav-fly .stor-nav-child-active { padding-left: 14px; }
31
+ .stor-nav-fly .stor-nav-child-active::before { left: 0; top: 8px; bottom: 8px; }
32
+ .stor-nav-children { position: relative; display: flex; flex-direction: column; gap: 1px; margin: 2px 0 0 21px; padding-left: 12px; }
33
+ .stor-nav-children::before { content: ""; position: absolute; left: 0; top: 4px; bottom: 4px; width: 1.5px; border-radius: 2px; background: var(--stor-border); }
34
+ .stor-nav-child { position: relative; display: flex; align-items: center; gap: 8px; height: 30px; padding: 0 10px; border-radius: 8px; font-size: 12.5px; font-weight: 500; color: var(--stor-muted-text); transition: color 120ms, background-color 120ms; }
35
+ .stor-nav-child:hover { color: var(--stor-ink); background: var(--stor-nav-hover); }
36
+ .stor-nav-child:focus-visible { outline: none; box-shadow: inset 0 0 0 2px var(--stor-focus); }
37
+ .stor-nav-child .stor-nav-badge { min-width: 18px; height: 16px; padding: 0 5px; font-size: 10px; line-height: 16px; }
38
+ /* Selection is ink, so red keeps meaning late work. */
39
+ .stor-nav-child-active { color: var(--stor-ink); font-weight: 600; }
40
+ .stor-nav-child-active::before { content: ""; position: absolute; left: -13.25px; top: 7px; bottom: 7px; width: 2px; border-radius: 2px; background: var(--stor-ink); }
41
+ .stor-dock-badge { position: absolute; top: -3px; right: 2px; min-width: 16px; height: 16px; padding: 0 4px; border-radius: 999px; font-size: 9.5px; font-weight: 700; line-height: 16px; text-align: center; color: var(--stor-negative-text); box-shadow: 0 0 0 2px var(--stor-surface-raised); }
42
+ .stor-dock-badge-negative { background: var(--stor-negative); }
43
+ .stor-dock-badge-warning { background: var(--stor-warning); color: var(--stor-surface); }
44
+ /* Inset dividers start at the row text, so icons read as one clean column. */
45
+ .stor-inset-rows > * + * { position: relative; }
46
+ .stor-inset-rows > * + *::before { content: ""; position: absolute; top: 0; left: var(--stor-row-inset, 0px); right: 8px; height: 1px; background: color-mix(in srgb, var(--stor-border) 70%, transparent); pointer-events: none; }
47
+ .stor-inset-rows > *:hover::before, .stor-inset-rows > *:hover + *::before { opacity: 0; }
48
+ .stor-section-link { display: inline-flex; align-items: center; gap: 4px; min-height: 44px; font-size: 13px; font-weight: 500; color: var(--stor-accent-text); }
49
+ }
50
+
51
+ /* The shell alone owns space occupied by mobile chrome. */
52
+
53
+ [data-stor-main] {
54
+ padding-bottom: calc(var(--stor-mobile-bottom-inset) + var(--stor-mobile-nav-height) + 12px);
55
+ scroll-padding-bottom: calc(var(--stor-mobile-bottom-inset) + var(--stor-mobile-nav-height) + 12px);
56
+ }
57
+
58
+ [data-stor-main]:has(.stor-mobile-action) {
59
+ padding-bottom: calc(var(--stor-mobile-bottom-inset) + var(--stor-mobile-nav-height) + var(--stor-mobile-stack-gap) + var(--stor-mobile-action-height) + 12px);
60
+ scroll-padding-bottom: calc(var(--stor-mobile-bottom-inset) + var(--stor-mobile-nav-height) + var(--stor-mobile-stack-gap) + var(--stor-mobile-action-height) + 12px);
61
+ }
62
+
63
+ /* The page's main action is a pill at the thumb's corner, sized by its label, not a bar across the content. */
64
+
65
+ .stor-mobile-action {
66
+ position: fixed; right: 12px;
67
+ bottom: calc(var(--stor-mobile-bottom-inset) + var(--stor-mobile-nav-height) + var(--stor-mobile-stack-gap));
68
+ z-index: 40; display: flex; height: var(--stor-mobile-action-height); gap: 8px;
69
+ }
70
+
71
+ .stor-mobile-action > button { width: auto; height: 100%; min-height: 44px; padding-inline: 18px; border-radius: 999px; box-shadow: var(--stor-shadow-md); }
72
+
73
+ .stor-mobile-nav {
74
+ left: 12px; right: 12px; bottom: var(--stor-mobile-bottom-inset);
75
+ height: var(--stor-mobile-nav-height); padding: 4px;
76
+ border: 0; border-radius: 24px; background: var(--stor-surface-raised);
77
+ box-shadow: var(--stor-elevation-1);
78
+ }
79
+
80
+ .stor-mobile-header { background: var(--stor-bg); box-shadow: var(--stor-header-shadow); }
81
+
82
+ [data-testid="desktop-sidebar"] { border-radius: 20px; box-shadow: var(--stor-sidebar-shadow); }
83
+
84
+ body:has([data-stor-drawer]) .stor-mobile-action,
85
+ body:has([data-stor-drawer]) .stor-mobile-fab { visibility: hidden; }
86
+
87
+ /* A phone spends its width on content: tighter gaps and panel padding (still clear of the smaller curve). */
88
+
89
+ @media (max-width: 639px) {
90
+ :root { --stor-section-gap: 12px; --stor-panel-padding: 16px; --stor-panel-radius: 16px; }
91
+ }
92
+
93
+ @media (max-width: 1023px) {
94
+ [data-stor-main]:has([data-under-action]) {
95
+ padding-bottom: calc(var(--stor-mobile-bottom-inset) + var(--stor-mobile-nav-height));
96
+ scroll-padding-bottom: calc(var(--stor-mobile-bottom-inset) + var(--stor-mobile-nav-height));
97
+ }
98
+ [data-under-action] {
99
+ --endless-home-bottom: calc(var(--stor-mobile-stack-gap) - 8px);
100
+ --endless-home-left: 10px;
101
+ --endless-home-height: var(--stor-mobile-action-height);
102
+ --endless-home-transform: none;
103
+ }
104
+ }
105
+
106
+ @media (max-width: 767px) {
107
+ /* While its list moves, the page action shrinks to its icon; it is an overlay, so nothing resizes. */
108
+ .stor-mobile-action[data-moving] .stor-action-label { display: none; }
109
+ .stor-mobile-action[data-moving] > button { padding-inline: 12px; }
110
+ }
111
+
112
+ /* From 768px the rail replaces the phone header and dock, so their heights fall away. Pages and
113
+ fixed bars size themselves from these variables, so they follow without their own rules. */
114
+
115
+ @media (min-width: 768px) {
116
+ :root { --stor-mobile-header-height: env(safe-area-inset-top, 0px); --stor-mobile-nav-height: 0px; }
117
+ }
118
+
119
+ @media (min-width: 1024px) {
120
+ :root { --stor-section-gap: 24px; --stor-panel-padding: 24px; }
121
+ [data-stor-main], [data-stor-main]:has(.stor-mobile-action) { padding-bottom: 0; scroll-padding-bottom: 12px; }
122
+ /* Finance, Calendar and Gallery fit the screen; their endless lists scroll themselves. */
123
+ main[data-page-fit] { overflow: hidden; }
124
+ .stor-mobile-action { display: none; }
125
+ }
126
+
127
+ .stor-calendar-layout { display: grid; align-items: start; min-width: 0; gap: var(--stor-section-gap); }
128
+
129
+ .stor-calendar-agendas { display: grid; min-width: 0; gap: var(--stor-section-gap); }
130
+
131
+ /* The weeks scroll endlessly inside the panel; on smaller screens the page scrolls around it. */
132
+
133
+ .stor-calendar-panel { display: flex; height: min(34rem, 70dvh); min-height: 0; flex-direction: column; }
134
+
135
+ .stor-calendar-grid { display: flex; min-height: 0; flex: 1; flex-direction: column; }
136
+
137
+ .stor-calendar-weeks { position: relative; min-height: 0; flex: 1; }
138
+
139
+ /* A thumb over a century of weeks says nothing, and a gutter would shift the columns off the weekday row. */
140
+
141
+ .stor-calendar-scroller { scrollbar-width: none; }
142
+
143
+ .stor-calendar-scroller::-webkit-scrollbar { display: none; }
144
+
145
+ .stor-calendar-day { position: relative; min-width: 0; min-height: 64px; border-radius: 14px; padding: 0 1px 6px; transition: background-color 160ms ease; }
146
+
147
+ /* Days outside the month in view step back, so the weeks still read as a month. */
148
+
149
+ .stor-calendar-out > * { opacity: .45; }
150
+
151
+ .stor-calendar-select { min-height: 44px; justify-content: center; }
152
+
153
+ /* A phone shows about five weeks, so the selected day's agenda starts on the first screen
154
+ and there is page left to scroll by that is not the endless grid. */
155
+
156
+ @media (max-width: 639px) { .stor-calendar-panel { height: min(23rem, 56dvh); } .stor-calendar-day { min-height: 56px; } }
157
+
158
+ /* Phones: the month grid is the page and its only scroller; weeks are tall enough to name their events. */
159
+
160
+ @media (max-width: 767px) {
161
+ .stor-calendar-page { display: flex; height: 100%; flex-direction: column; }
162
+ .stor-calendar-layout { display: flex; min-height: 0; flex: 1; flex-direction: column; align-items: stretch; }
163
+ .stor-calendar-agendas { display: none; }
164
+ .stor-calendar-panel { height: auto; min-height: 0; flex: 1; }
165
+ .stor-calendar-day { min-height: 104px; height: 104px; overflow: hidden; border-radius: 10px; padding: 2px 1px 0; }
166
+ .stor-calendar-select { min-height: 30px; }
167
+ /* Names keep every letter they can: no ellipsis eating the last ones in a 50 px day. */
168
+ .stor-calendar-day .truncate { text-overflow: clip; }
169
+ /* The day card lists its agenda as plain rows. */
170
+ [data-calendar-peek] a, [data-calendar-peek] button.flex { border-color: transparent; padding-inline: 4px; }
171
+ [data-calendar-peek] a .text-xs::first-letter { text-transform: uppercase; }
172
+ }
173
+
174
+ @media (min-width: 1024px) {
175
+ .stor-calendar-layout { min-height: 0; flex: 1; grid-template-columns: minmax(0, 1fr) 320px; grid-template-rows: minmax(0, 1fr); align-items: stretch; }
176
+ .stor-calendar-agendas { min-height: 0; align-content: start; overflow-y: auto; }
177
+ .stor-calendar-panel { height: auto; }
178
+ .stor-calendar-day { min-height: 112px; padding: 4px; }
179
+ .stor-calendar-select { justify-content: flex-start; }
180
+ }
181
+
182
+ /* ---------- Native shell (apple/) ----------
183
+ Rules in this block apply only inside the Mac and iOS apps, which set
184
+ html[data-stor-shell] from the boot script. The browser version never matches them. */
185
+
186
+ .stor-lights { display: none; }
187
+
188
+ /* Mac: the window has no title bar of its own. The shell draws the window controls over the sidebar's
189
+ header row (the empty .stor-lights anchor tells it where), the web view is transparent over the window's
190
+ glass, and one canvas tint covers the content and the space around the translucent sidebar. */
191
+
192
+ html[data-stor-shell="mac"] {
193
+ --stor-shell-titlebar: 52px;
194
+ --stor-shell-lights: 62px;
195
+ --stor-shell-canvas-tint: 86%;
196
+ --stor-shell-sidebar-tint: 56%;
197
+ /* The title bar reports a top safe area; the page header already clears the window edge. */
198
+ --stor-mobile-header-height: 0px;
199
+ background: transparent;
200
+ }
201
+
202
+ html[data-stor-shell="mac"] body { background: transparent; }
203
+
204
+ html[data-stor-shell="mac"] [data-stor-root] {
205
+ background: color-mix(in srgb, var(--stor-bg) var(--stor-shell-canvas-tint), transparent);
206
+ }
207
+
208
+ html[data-stor-shell="mac"] [data-testid="desktop-sidebar"] {
209
+ background: color-mix(in srgb, var(--stor-sidebar) var(--stor-shell-sidebar-tint), transparent);
210
+ box-shadow: inset 0 0 0 0.5px color-mix(in srgb, var(--stor-ink) 9%, transparent), var(--stor-sidebar-shadow);
211
+ }
212
+
213
+ html[data-stor-shell="mac"] .stor-lights { display: block; flex: none; width: var(--stor-shell-lights); height: 14px; }
214
+
215
+ /* The menu bar already names the app; the Mac sidebar shows no brand, only the drag space. */
216
+
217
+ html[data-stor-shell="mac"] [data-stor-brand] { display: none; }
218
+
219
+ html[data-stor-shell="mac"]:not([data-sidebar-collapsed="true"]) [data-testid="desktop-sidebar"] > [data-stor-drag] {
220
+ height: var(--stor-shell-titlebar);
221
+ }
222
+
223
+ /* Full screen hides the window controls, so their space goes back to the wordmark. */
224
+
225
+ html[data-stor-shell="mac"][data-stor-fullscreen] .stor-lights { display: none; }
226
+
227
+ /* iOS: edge to edge with glass bars. The header and the dock stay web; content scrolls under them. */
228
+
229
+ html[data-stor-shell="ios"] {
230
+ -webkit-text-size-adjust: 100%;
231
+ overscroll-behavior: none;
232
+ }
233
+
234
+ html[data-stor-shell="ios"] :where(a, button, summary, label, [role="button"], [role="tab"]) { -webkit-tap-highlight-color: transparent; }
235
+
236
+ html[data-stor-shell="ios"] .stor-mobile-header {
237
+ background: color-mix(in srgb, var(--stor-bg) 62%, transparent);
238
+ -webkit-backdrop-filter: blur(22px) saturate(1.8);
239
+ backdrop-filter: blur(22px) saturate(1.8);
240
+ box-shadow: none;
241
+ }
242
+
243
+ html[data-stor-shell="ios"] .stor-mobile-nav {
244
+ left: 18px; right: 18px;
245
+ border-radius: 999px;
246
+ background: color-mix(in srgb, var(--stor-surface-raised) 58%, transparent);
247
+ -webkit-backdrop-filter: blur(24px) saturate(1.9);
248
+ backdrop-filter: blur(24px) saturate(1.9);
249
+ box-shadow:
250
+ inset 0 0.5px 0 color-mix(in srgb, white 70%, transparent),
251
+ inset 0 0 0 0.5px color-mix(in srgb, var(--stor-ink) 10%, transparent),
252
+ var(--stor-shadow-md);
253
+ }
254
+
255
+ /* iPad shows the rail from 768px: clear the status bar, which the edge-to-edge page now sits under. */
256
+
257
+ html[data-stor-shell="ios"] [data-testid="desktop-sidebar"] { top: calc(12px + env(safe-area-inset-top, 0px)); }
258
+
259
+ html[data-stor-shell="ios"] [data-stor-main] { padding-top: var(--stor-mobile-header-height); }
260
+
261
+ /* Sidebar edge: a 12px strip straddling the card edge (4px inside, 8px in the gutter). The line shows
262
+ after a short hover so it does not flash as the pointer passes. */
263
+
264
+ .stor-sidebar-handle { position: absolute; top: 0; bottom: 0; left: calc(100% - 4px); width: 12px; cursor: col-resize; touch-action: none; z-index: 1; }
265
+
266
+ .stor-sidebar-handle::after { content: ""; position: absolute; left: 3px; top: 18px; bottom: 18px; width: 2px; border-radius: 2px; background: var(--stor-focus); opacity: 0; transition: opacity 120ms ease 150ms; }
267
+
268
+ .stor-sidebar-handle:hover::after, .stor-sidebar-handle:focus-visible::after, html[data-sidebar-resizing] .stor-sidebar-handle::after { opacity: 1; }
269
+
270
+ .stor-sidebar-handle:focus-visible { outline: none; }
271
+
272
+ .stor-sidebar-handle:focus-visible::after { transition-delay: 0s; box-shadow: 0 0 0 3px color-mix(in srgb, var(--stor-focus) 30%, transparent); }
273
+
274
+ @media (pointer: coarse) { .stor-sidebar-handle { width: 24px; left: calc(100% - 8px); } .stor-sidebar-handle::after { left: 11px; } }
275
+
276
+ html[data-sidebar-resizing], html[data-sidebar-resizing] * { cursor: col-resize !important; user-select: none; }
277
+
278
+ html[data-sidebar-resizing] [data-testid="desktop-sidebar"], html[data-sidebar-resizing] [data-stor-root] { transition: none !important; }
279
+
280
+ @media (prefers-reduced-motion: reduce) { .stor-sidebar-handle::after { transition: none; } }
281
+
282
+ /* A round list-options button at the thumb's other corner, level with the page action; it floats over the list. */
283
+
284
+ .stor-mobile-fab {
285
+ position: fixed; left: 12px; z-index: 30;
286
+ bottom: calc(var(--stor-mobile-bottom-inset) + var(--stor-mobile-nav-height) + var(--stor-mobile-stack-gap));
287
+ width: var(--stor-mobile-action-height); height: var(--stor-mobile-action-height); min-height: 44px; padding: 0; justify-content: center;
288
+ border: 0; border-radius: 999px; box-shadow: var(--stor-elevation-2);
289
+ }
290
+
291
+ /* A list marked data-under-action runs on under the page action down to the dock; it scrolls its last
292
+ rows clear of the pill, and its way back (Today) sits bottom left, level with the pill. */
293
+
294
+ @media (max-width: 1023px) {
295
+ [data-under-action] [data-timeline-scroll] { padding-bottom: calc(var(--stor-mobile-action-height) + 2 * var(--stor-mobile-stack-gap)); }
296
+ }
@@ -0,0 +1,4 @@
1
+ /*
2
+ Cross-Origin-Embedder-Policy: require-corp
3
+ Cross-Origin-Opener-Policy: same-origin
4
+ X-Content-Type-Options: nosniff
Binary file
@@ -0,0 +1,44 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <meta name="theme-color" content="#FBF9FA" />
7
+ <title>App unavailable offline | stor</title>
8
+ <script>
9
+ (() => {
10
+ let preference = "system";
11
+ try {
12
+ for (let index = 0; index < localStorage.length; index++) {
13
+ const key = localStorage.key(index);
14
+ if (key?.startsWith("stor:appearance:")) {
15
+ const saved = localStorage.getItem(key);
16
+ if (saved === "light" || saved === "dark" || saved === "system") preference = saved;
17
+ break;
18
+ }
19
+ }
20
+ } catch {}
21
+ const theme = preference === "system"
22
+ ? (matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light")
23
+ : preference;
24
+ document.documentElement.dataset.appearance = preference;
25
+ document.documentElement.dataset.theme = theme;
26
+ document.querySelector('meta[name="theme-color"]')?.setAttribute("content", theme === "dark" ? "#1b181a" : "#fbf9fa");
27
+ })();
28
+ </script>
29
+ <style>
30
+ :root{color-scheme:light;font-family:Inter,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif;background:#f8f5f4;color:#251a18}body{display:grid;min-width:320px;min-height:100vh;place-items:center;margin:0;padding:24px;box-sizing:border-box;background:inherit;color:inherit}.card{width:100%;max-width:480px;border:1px solid #e8ddda;border-radius:20px;background:#fff;padding:28px;box-sizing:border-box;box-shadow:0 12px 40px rgba(60,35,30,.08)}.brand{display:flex;align-items:center;gap:10px;font-weight:750}.logo{display:grid;width:36px;height:36px;place-items:center;border-radius:12px;background:#b23a34;color:#fff;font-family:ui-monospace,monospace;font-size:18px;font-weight:800}.eyebrow{margin:32px 0 0;color:#78716c;font-size:11px;font-weight:700;letter-spacing:.12em;text-transform:uppercase}h1{margin:8px 0 0;font-size:25px;line-height:1.2}p{margin:12px 0 0;color:#57534e;font-size:14px;line-height:1.65}a{display:inline-flex;margin-top:24px;border-radius:12px;background:#251a18;padding:11px 16px;color:#fff;font-size:14px;font-weight:700;text-decoration:none}a:focus-visible{outline:3px solid #b23a34;outline-offset:3px}
31
+ :root[data-theme="dark"]{color-scheme:dark;background:#1b181a;color:#f2f0ed}:root[data-theme="dark"] .card{border-color:#514a4d;background:#272427;box-shadow:0 12px 40px #090b0dcc}:root[data-theme="dark"] .eyebrow,:root[data-theme="dark"] p{color:#b7b9bd}:root[data-theme="dark"] .logo{background:#ce817d;color:#151719}:root[data-theme="dark"] a{background:#ce817d;color:#151719}
32
+ @media(prefers-color-scheme:dark){:root:not([data-theme]){color-scheme:dark;background:#1b181a;color:#f2f0ed}:root:not([data-theme]) .card{border-color:#514a4d;background:#272427;box-shadow:0 12px 40px #090b0dcc}:root:not([data-theme]) .eyebrow,:root:not([data-theme]) p{color:#b7b9bd}:root:not([data-theme]) .logo{background:#ce817d;color:#151719}:root:not([data-theme]) a{background:#ce817d;color:#151719}}
33
+ </style>
34
+ </head>
35
+ <body>
36
+ <main class="card">
37
+ <div class="brand"><span class="logo">s</span><span>stor</span></div>
38
+ <p class="eyebrow">Offline</p>
39
+ <h1>The app is not available offline yet</h1>
40
+ <p>Connect once to finish installing the offline app, then try again.</p>
41
+ <a href="/">Go to start</a>
42
+ </main>
43
+ </body>
44
+ </html>
@@ -0,0 +1,2 @@
1
+ importScripts("https://cdn.jsdelivr.net/npm/replaywebpage@2.4.6/sw.js");
2
+
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.