@broberg/app-shell 0.13.1 → 0.14.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/README.md CHANGED
@@ -53,9 +53,23 @@ the user menu reads and writes theme, palette, surfaces and backdrop through it.
53
53
  | `TenantSwitcher` | The user's own organisations, the active one marked, switching through the app's route (0.7.1). |
54
54
  | `AnalyticsPage` | The app's own visitors, page views, page views per day and most visited pages, from Upmetrics (0.6.0). |
55
55
 
56
+ ## 0.14.0 — the «…» menu from the keyboard, fixed; «Slet» confirms in the menu (F092.22)
57
+
58
+ **Upgrade from 0.13.x — a keyboard user could trigger the wrong action.** In
59
+ 0.13.x, Enter in the «…» menu picked the *highlighted* item, not the *focused*
60
+ one, and opening the menu left focus on «…» (so P/R/D did nothing). Measured by
61
+ scout on prod: Enter on «Tilføj til projekt» pinned the conversation. Now:
62
+
63
+ - Opening moves focus to the first item; ↑/↓ move real focus; Enter/Space act on
64
+ the item that has focus. Keys typed in the instant before focus arrives still
65
+ belong to the menu (two fast ↓ land two items down, measured in Lens).
66
+ - `NavAction.confirm: { label }` — the first pick shows the label («Tryk igen for
67
+ at slette») and keeps the menu open; the second runs `onItemAction`. Escape or
68
+ closing resets it.
69
+
56
70
  ## 0.13.1 — the rename form has a testid (F092.21)
57
71
 
58
- `sidebar-item-<id>-form` on the input view's form (the F086 testid scan caught it after 0.13.0 was tagged). No behaviour change.
72
+ `sidebar-item-<id>-form` on the input view's form (the F086 testid scan caught it after 0.13.0 was tagged). No behaviour change. **0.13.0 was tagged but never published** — its run was cancelled so it could not land after 0.13.1 and take `latest`. Install 0.13.1.
59
73
 
60
74
  ## 0.13.0 — row actions on a sidebar item: the «…» menu (F092.21)
61
75
 
package/css/app-shell.css CHANGED
@@ -139,6 +139,7 @@
139
139
  .bas-rowmenu__item.is-active, .bas-rowmenu__item:hover, .bas-rowmenu__back:hover { background: var(--hover); }
140
140
  .bas-rowmenu__item:active { background: var(--active); }
141
141
  .bas-rowmenu__item.is-danger { color: var(--danger); }
142
+ .bas-rowmenu__item.is-armed { background: var(--danger-soft, var(--hover)); font-weight: 500; }
142
143
  .bas-rowmenu__icon { display: inline-flex; color: var(--fg-subtle); }
143
144
  .bas-rowmenu__label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
144
145
  .bas-rowmenu__kbd { font-family: var(--font-mono, ui-monospace, monospace); font-size: 11px; color: var(--fg-subtle); }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["d"],"mappings":"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","file":"index.cjs","sourcesContent":["// @broberg/app-shell — the framework-free core of the fleet's app shell (F092).\n//\n// The shell every app in the fleet had been copying: sidebar + top bar +\n// notification bell + user menu. cardmem and trail each had one (\"ported from\n// trail\"), and helpdesk and the appkit starter would have made the third and\n// fourth. This file holds what does not depend on a framework: the navigation\n// data model, the notification contract and its sources, and the small rules\n// both the Preact and React parts must agree on.\n//\n// Christian, 4 Oct 2026: notifications are a STANDARD part of the template, not\n// an optional slot — so the contract and a default source live here, and a new\n// app gets a working notification centre by pointing one URL at its backend.\n\n// ── Navigation ─────────────────────────────────────────────────────────────\n\n/** One sidebar entry. `icon` is whatever your framework renders (an element). */\nexport interface NavItem<Icon = unknown> {\n id: string;\n label: string;\n href: string;\n icon?: Icon;\n /** A count shown on the item (99+ above 99). 0/undefined shows nothing. */\n badge?: number;\n /**\n * Sub-items, ONE level (F092.5). An item with children is a toggle that opens\n * and closes them, not a link; its own `href` is used only to decide whether\n * it is on the current path.\n */\n children?: NavItem<Icon>[];\n /** F092.20 — a status marker before the label; \"working\" = a run in progress. */\n marker?: \"working\";\n /** F092.20 — shows a pin icon after the label. */\n pinned?: boolean;\n /** F092.21 — row actions: a «…» button opens a menu with these. */\n actions?: NavAction[];\n}\n/**\n * F092.21 — one row action. `submenu` switches the menu to a sub-list (with a\n * back arrow); `input` switches it to a field + save (e.g. rename). A plain\n * action, a submenu leaf and a saved input all call `onItemAction`.\n */\nexport interface NavAction {\n id: string;\n label: string;\n icon?: unknown;\n /** A single letter that picks this action while the menu is open (e.g. \"P\"). */\n kbd?: string;\n /** Shown in the danger colour (delete). */\n danger?: boolean;\n submenu?: NavAction[];\n input?: { value: string; placeholder?: string; saveLabel?: string };\n}\nexport interface NavSection<Icon = unknown> {\n /** A sub-heading inside one group, e.g. \"I dag\" / \"I går\". */\n label: string;\n items: NavItem<Icon>[];\n}\nexport interface NavGroup<Icon = unknown> {\n label: string;\n items: NavItem<Icon>[];\n /** F092.20 — \"plain\": the group label in normal text (not uppercase/mono). */\n variant?: \"default\" | \"plain\";\n /** F092.20 — sub-headed lists inside this group, shown after `items`. */\n sections?: NavSection<Icon>[];\n}\n\n/**\n * Is this item the current page? Exact match for \"/\", otherwise the path is the\n * item or below it (\"/board/123\" is on \"/board\", \"/boardroom\" is not).\n */\nexport function isActivePath(href: string, currentPath: string): boolean {\n const path = (currentPath.split(/[?#]/)[0] || \"/\").replace(/\\/+$/, \"\") || \"/\";\n const target = (href.split(/[?#]/)[0] || \"/\").replace(/\\/+$/, \"\") || \"/\";\n if (target === \"/\") return path === \"/\";\n return path === target || path.startsWith(target + \"/\");\n}\n\n/**\n * A notification's `navigate` is SERVER DATA, and it ends in `location.assign`.\n * `javascript:` there runs code in the app, so only a same-site path or an\n * http(s) URL is followed; anything else returns null and the row just closes.\n */\nexport function safeNavigate(target: string | null | undefined): string | null {\n if (typeof target !== \"string\") return null;\n const t = target.trim();\n if (t.startsWith(\"/\") && !t.startsWith(\"//\") && !t.startsWith(\"/\\\\\")) return t;\n try {\n const u = new URL(t);\n return u.protocol === \"https:\" || u.protocol === \"http:\" ? u.href : null;\n } catch {\n return null;\n }\n}\n\n/**\n * F092.10 — is this link leaving the app? An http(s) link to another origin.\n * Paths, \"#…\", mailto:/tel: and the app's own origin are not. Owner decision\n * D-d28547: every external link opens in a new tab.\n */\nexport function isExternal(\n href: string | null | undefined,\n origin: string | undefined = typeof location !== \"undefined\" ? location.origin : undefined,\n): boolean {\n if (typeof href !== \"string\") return false;\n const h = href.trim();\n if (!origin) return /^(https?:)?\\/\\//i.test(h); // no page to compare with: absolute = external\n let u: URL;\n try {\n u = new URL(h, origin);\n } catch {\n return false;\n }\n return (u.protocol === \"http:\" || u.protocol === \"https:\") && u.origin !== origin;\n}\n\n/**\n * The label of the nav item the user is on, for the content header (F092.4).\n * Sub-items count, and the most specific match wins: on \"/models/genesis\" a\n * \"Genesis\" child beats its \"Models\" parent.\n */\nexport function activeNavLabel<Icon>(\n groups: NavGroup<Icon>[],\n footer: NavItem<Icon>[] | undefined,\n currentPath: string,\n): string | undefined {\n let best: { label: string; len: number } | undefined;\n const visit = (it: NavItem<Icon>) => {\n if (it.href && isActivePath(it.href, currentPath)) {\n const len = it.href.length;\n if (!best || len > best.len) best = { label: it.label, len };\n }\n it.children?.forEach(visit);\n };\n for (const g of groups) g.items.forEach(visit);\n footer?.forEach(visit);\n return best?.label;\n}\n\n/** \"99+\" above 99, nothing for 0 or less. */\nexport function badgeLabel(n: number | undefined): string | null {\n if (!n || n <= 0) return null;\n return n > 99 ? \"99+\" : String(n);\n}\n\n// ── Persisted UI state ─────────────────────────────────────────────────────\n\n/** Read a boolean flag from localStorage; storage errors read as `fallback`. */\nexport function readFlag(key: string, fallback: boolean): boolean {\n try {\n const v = typeof localStorage === \"undefined\" ? null : localStorage.getItem(key);\n return v === null ? fallback : v === \"1\";\n } catch {\n return fallback;\n }\n}\nexport function writeFlag(key: string, value: boolean): void {\n try {\n if (typeof localStorage !== \"undefined\") localStorage.setItem(key, value ? \"1\" : \"0\");\n } catch {\n /* private mode / quota: the UI still works, it just does not remember */\n }\n}\nexport function readSet(key: string): Set<string> {\n try {\n const raw = typeof localStorage === \"undefined\" ? null : localStorage.getItem(key);\n const v = raw ? (JSON.parse(raw) as unknown) : [];\n return new Set(Array.isArray(v) ? v.filter((x): x is string => typeof x === \"string\") : []);\n } catch {\n return new Set();\n }\n}\nexport function writeSet(key: string, value: Set<string>): void {\n try {\n if (typeof localStorage !== \"undefined\") localStorage.setItem(key, JSON.stringify([...value]));\n } catch {\n /* see writeFlag */\n }\n}\n\n// ── Avatar ─────────────────────────────────────────────────────────────────\n\n/**\n * Two initials for the avatar fallback: first letter of the first and the last\n * word, upper-cased. One word gives one letter; nothing gives \"?\".\n *\n * There is deliberately no Gravatar here (owner decision D-1dc849): the avatar\n * is the Broberg ID `picture`, and without one, initials. Gravatar would send a\n * hash of the user's email to a US service on every render.\n */\nexport function initials(name: string | undefined, email?: string): string {\n const source = (name ?? \"\").trim() || (email ?? \"\").split(\"@\")[0] || \"\";\n const words = source.split(/[\\s._-]+/).filter(Boolean);\n if (words.length === 0) return \"?\";\n const first = words[0]![0] ?? \"\";\n const last = words.length > 1 ? words[words.length - 1]![0] ?? \"\" : \"\";\n return (first + last).toUpperCase() || \"?\";\n}\n\n// ── Notifications ──────────────────────────────────────────────────────────\n//\n// REUSE, NOT A SECOND CONTRACT. The fleet already agreed the notification shape\n// in @broberg/notifications (F074): `NotificationRow` — the same field names\n// @broberg/webpush sends, end to end — and the server core that owns the ONE\n// counting rule. The shell's bell renders that row and drives its panel with\n// that package's `createBellShell`, so a server built on `createNotifications`\n// and this bell speak the same language without an adapter.\n//\n// Christian, 4 Oct 2026: notifications are a STANDARD part of the template. So\n// a source, and a default one, ship here: point one URL at your backend.\n\nimport type { NotificationRow } from \"@broberg/notifications/types\";\nexport type { NotificationRow } from \"@broberg/notifications/types\";\n\n/** Where the bell gets its data. Swap it to point at any backend. */\nexport interface NotificationSource {\n /** The rows to show (newest first). */\n loadRows(): Promise<NotificationRow[]>;\n /** THE count — your server's, which may exclude muted kinds (F074's counting rule). */\n countUnseen(): Promise<number>;\n /** Returns the ids that actually transitioned to seen. */\n markSeen(ids: readonly string[]): Promise<string[]>;\n markAllSeen(): Promise<string[]>;\n /** Called with a callback that re-counts; return an unsubscribe. Optional. */\n subscribe?(onChange: () => void): () => void;\n}\n\n/**\n * The default source: a URL on your own backend, shaped after\n * @broberg/notifications' server core so `createNotifications` sits right\n * behind it.\n *\n * GET {url} → { items: NotificationRow[], unseen: number }\n * POST {url}/seen { ids: string[] } → { clearedIds: string[], count: number }\n * POST {url}/seen-all → { clearedIds: string[], count: number }\n *\n * It re-counts when the window regains focus or the tab becomes visible, and on\n * a timer (default 60 s). A failed GET THROWS — the bell shows «could not\n * load», never an empty list: \"could not fetch\" and \"nothing new\" must not look\n * the same.\n */\nexport function createFetchNotificationSource(opts: {\n url: string;\n fetch?: typeof fetch;\n /** Re-count interval in ms. 0 turns it off. Default 60 000. */\n pollMs?: number;\n credentials?: RequestCredentials;\n}): NotificationSource {\n const base = opts.url.replace(/\\/+$/, \"\");\n const doFetch = (input: string, init?: RequestInit) =>\n (opts.fetch ?? fetch)(input, { credentials: opts.credentials ?? \"include\", ...init });\n const get = async (): Promise<{ items: NotificationRow[]; unseen: number }> => {\n const res = await doFetch(base, { headers: { accept: \"application/json\" } });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const body = (await res.json().catch(() => null)) as { items?: unknown; unseen?: unknown } | null;\n if (!body || !Array.isArray(body.items)) throw new Error(\"response has no items array\");\n const items = body.items as NotificationRow[];\n const unseen = typeof body.unseen === \"number\" ? body.unseen : items.filter((n) => n.seenAt == null).length;\n return { items, unseen };\n };\n const post = async (path: string, body?: unknown): Promise<string[]> => {\n const res = await doFetch(`${base}${path}`, {\n method: \"POST\",\n headers: body === undefined ? undefined : { \"content-type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const r = (await res.json().catch(() => ({}))) as { clearedIds?: unknown };\n return Array.isArray(r.clearedIds) ? (r.clearedIds as string[]) : [];\n };\n return {\n loadRows: async () => (await get()).items,\n countUnseen: async () => (await get()).unseen,\n markSeen: (ids) => post(\"/seen\", { ids }),\n markAllSeen: () => post(\"/seen-all\"),\n subscribe(onChange) {\n if (typeof window === \"undefined\") return () => {};\n const onVisible = () => {\n if (document.visibilityState === \"visible\") onChange();\n };\n window.addEventListener(\"focus\", onChange);\n document.addEventListener(\"visibilitychange\", onVisible);\n const ms = opts.pollMs ?? 60_000;\n const timer = ms > 0 ? setInterval(onChange, ms) : null;\n return () => {\n window.removeEventListener(\"focus\", onChange);\n document.removeEventListener(\"visibilitychange\", onVisible);\n if (timer) clearInterval(timer);\n };\n },\n };\n}\n\n/** An in-memory source for demos and tests. */\nexport function createMemoryNotificationSource(initial: NotificationRow[] = []): NotificationSource & {\n push(n: NotificationRow): void;\n rows(): NotificationRow[];\n} {\n let rows = initial.map((n) => ({ ...n }));\n const listeners = new Set<() => void>();\n const emit = () => listeners.forEach((l) => l());\n const mark = (pred: (n: NotificationRow) => boolean): string[] => {\n const now = Date.now();\n const cleared: string[] = [];\n rows = rows.map((n) => {\n if (n.seenAt == null && pred(n)) {\n cleared.push(n.id);\n return { ...n, seenAt: now };\n }\n return n;\n });\n if (cleared.length) emit();\n return cleared;\n };\n return {\n loadRows: async () => rows.map((n) => ({ ...n })),\n countUnseen: async () => rows.filter((n) => n.seenAt == null).length,\n markSeen: async (ids) => mark((n) => ids.includes(n.id)),\n markAllSeen: async () => mark(() => true),\n subscribe(onChange) {\n listeners.add(onChange);\n return () => listeners.delete(onChange);\n },\n push(n) {\n rows = [n, ...rows];\n emit();\n },\n rows: () => rows.map((n) => ({ ...n })),\n };\n}\n\n/** \"nu\", \"5 min\", \"3 t\", \"i går\", \"4 d\" (da) or the English equivalents. */\nexport function relativeTime(ts: string | number, lang: \"da\" | \"en\", now = Date.now()): string {\n const ms = typeof ts === \"number\" ? ts : Date.parse(ts);\n if (!Number.isFinite(ms)) return \"\";\n const m = Math.floor((now - ms) / 60_000);\n if (m < 1) return lang === \"da\" ? \"nu\" : \"now\";\n if (m < 60) return `${m} min`;\n const h = Math.floor(m / 60);\n if (h < 24) return lang === \"da\" ? `${h} t` : `${h} h`;\n const d = Math.floor(h / 24);\n if (d === 1) return lang === \"da\" ? \"i går\" : \"yesterday\";\n return `${d} d`;\n}\n\n// ── Account page (F095.3) ─────────────────────────────────────────────────\n//\n// The user edits their own name and picture inside the app. The app's backend\n// (@broberg/sso's `accountRoutes()`, mounted on /api/account) talks to Broberg\n// ID; this package only talks to that backend, through an adapter, and never\n// imports @broberg/sso.\n\n/** What every account call resolves to — the profile AS THE SERVER NOW HOLDS IT. */\nexport interface AccountProfile {\n sub: string;\n name: string | null;\n picture: string | null;\n email: string | null;\n /** Broberg ID's own account page (password, passkeys, sessions). */\n account_url: string;\n}\n\n/** Where the account page gets and saves the profile. Swap it to point anywhere. */\nexport interface AccountAdapter {\n load(): Promise<AccountProfile>;\n saveName(name: string): Promise<AccountProfile>;\n uploadAvatar(file: Blob): Promise<AccountProfile>;\n removeAvatar(): Promise<AccountProfile>;\n}\n\n/** A failed account call: the HTTP status and the server's `error` code, if any. */\nexport class AccountError extends Error {\n constructor(\n readonly status: number,\n readonly code?: string,\n ) {\n super(code ? `HTTP ${status} ${code}` : `HTTP ${status}`);\n this.name = \"AccountError\";\n }\n}\n\nexport const AVATAR_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\"] as const;\nexport const AVATAR_MAX_BYTES = 2 * 1024 * 1024;\n\n/** Checked BEFORE uploading, so a file the server will refuse is never sent. */\nexport function checkAvatarFile(file: { size: number; type: string }): \"tooLarge\" | \"wrongType\" | null {\n if (!(AVATAR_TYPES as readonly string[]).includes(file.type)) return \"wrongType\";\n if (file.size > AVATAR_MAX_BYTES) return \"tooLarge\";\n return null;\n}\n\n/**\n * What a failed call means for the page. `reauth` = the user's session or its\n * scope does not allow the change (401, or 403 insufficient_scope): signing in\n * again is the fix, so the page says that instead of «try again».\n */\nexport function accountErrorKind(err: unknown): \"reauth\" | \"tooLarge\" | \"wrongType\" | \"other\" {\n if (!(err instanceof AccountError)) return \"other\";\n if (err.status === 401) return \"reauth\";\n if (err.status === 403 && err.code === \"insufficient_scope\") return \"reauth\";\n if (err.status === 413) return \"tooLarge\";\n if (err.status === 415) return \"wrongType\";\n return \"other\";\n}\n\n/**\n * The default adapter: the app's own backend, shaped after @broberg/sso's\n * `accountRoutes()`.\n *\n * GET {url}/profile → AccountProfile\n * POST {url}/profile {name} → AccountProfile\n * POST {url}/profile/avatar (raw bytes, Content-Type = the image's) → AccountProfile\n * POST {url}/profile/avatar/remove → AccountProfile\n *\n * A non-2xx THROWS an AccountError — a failed save must never look like a\n * saved one.\n */\nexport function createFetchAccountAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): AccountAdapter {\n const base = (opts.url ?? \"/api/account\").replace(/\\/+$/, \"\");\n const call = async (path: string, init?: RequestInit): Promise<AccountProfile> => {\n const res = await (opts.fetch ?? fetch)(`${base}${path}`, {\n credentials: opts.credentials ?? \"same-origin\",\n ...init,\n headers: { accept: \"application/json\", ...(init?.headers as Record<string, string> | undefined) },\n });\n const body = (await res.json().catch(() => null)) as Record<string, unknown> | null;\n if (!res.ok) throw new AccountError(res.status, typeof body?.error === \"string\" ? body.error : undefined);\n if (!body || typeof body.sub !== \"string\") throw new AccountError(res.status, \"bad_response\");\n return body as unknown as AccountProfile;\n };\n return {\n load: () => call(\"/profile\"),\n saveName: (name) =>\n call(\"/profile\", { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: JSON.stringify({ name }) }),\n uploadAvatar: (file) =>\n call(\"/profile/avatar\", { method: \"POST\", headers: { \"content-type\": file.type }, body: file }),\n removeAvatar: () => call(\"/profile/avatar/remove\", { method: \"POST\" }),\n };\n}\n\n// ── Analytics page (F097) ─────────────────────────────────────────────────\n//\n// The app's OWN visitor numbers from Upmetrics. The app's backend mounts\n// `analyticsRoutes()` from `@upmetrics/sdk/hono` (the uk_ key stays on the\n// server); this package only talks to that backend, through an adapter, and has\n// no dependency on the SDK — the type below is structural, so Upmetrics'\n// `AnalyticsResponse` fits it as is.\n\nexport interface AnalyticsDay {\n /** YYYY-MM-DD on the Europe/Copenhagen calendar (Upmetrics' own day key). */\n day: string;\n visitors: number;\n pageviews: number;\n}\n\nexport interface AnalyticsData {\n totals: { visitors: number; pageviews: number };\n /** The same-length period just before this one. */\n prev: { visitors: number; pageviews: number };\n /** Only days that had visits — use fillDays() to draw the whole period. */\n series: AnalyticsDay[];\n topPages: { value: string; count: number }[];\n}\n\nexport interface AnalyticsAdapter {\n load(days: number): Promise<AnalyticsData>;\n}\n\n/** A failed analytics call: HTTP status (0 = no answer) and the server's `error` code. */\nexport class AnalyticsError extends Error {\n constructor(\n readonly status: number,\n readonly code?: string,\n ) {\n super(code ? `HTTP ${status} ${code}` : `HTTP ${status}`);\n this.name = \"AnalyticsError\";\n }\n}\n\n/** \"unconfigured\" only for the backend's explicit 503 analytics_unconfigured; anything else is \"unavailable\". */\nexport function analyticsErrorKind(err: unknown): \"unconfigured\" | \"unavailable\" {\n return err instanceof AnalyticsError && err.status === 503 && err.code === \"analytics_unconfigured\" ? \"unconfigured\" : \"unavailable\";\n}\n\n/**\n * The default adapter: `GET {url}?days=N` on the app's own backend (default\n * /api/analytics, where `@upmetrics/sdk/hono`'s analyticsRoutes() answers).\n * A non-2xx, an unreadable body or no answer at all THROWS — never zeros that\n * look like a real, quiet month.\n */\nexport function createFetchAnalyticsAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): AnalyticsAdapter {\n const url = opts.url ?? \"/api/analytics\";\n return {\n async load(days) {\n let res: Response;\n try {\n res = await (opts.fetch ?? fetch)(`${url}${url.includes(\"?\") ? \"&\" : \"?\"}days=${encodeURIComponent(String(days))}`, {\n credentials: opts.credentials ?? \"same-origin\",\n headers: { accept: \"application/json\" },\n });\n } catch {\n throw new AnalyticsError(0);\n }\n const body = (await res.json().catch(() => null)) as Record<string, unknown> | null;\n if (!res.ok) throw new AnalyticsError(res.status, typeof body?.error === \"string\" ? body.error : undefined);\n const ok =\n body !== null &&\n typeof (body.totals as Record<string, unknown> | undefined)?.visitors === \"number\" &&\n Array.isArray(body.series) &&\n Array.isArray(body.topPages);\n if (!ok) throw new AnalyticsError(res.status, \"bad_response\");\n return body as unknown as AnalyticsData;\n },\n };\n}\n\n/** Today's YYYY-MM-DD on the Copenhagen calendar — the calendar Upmetrics buckets on. */\nexport function copenhagenDay(at: Date = new Date()): string {\n return new Intl.DateTimeFormat(\"en-CA\", { timeZone: \"Europe/Copenhagen\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).format(at);\n}\n\n/**\n * Exactly `days` entries ending today (Copenhagen calendar), with Upmetrics'\n * values where it sent a day and zeros where it did not — Upmetrics only sends\n * days that had visits. Calendar arithmetic, so a DST change never skips or\n * repeats a day.\n */\nexport function fillDays(series: AnalyticsDay[], days: number, now: Date = new Date()): AnalyticsDay[] {\n const byDay = new Map(series.map((d) => [d.day, d]));\n const [y, m, d] = copenhagenDay(now).split(\"-\").map(Number) as [number, number, number];\n return Array.from({ length: days }, (_, i) => {\n const day = new Date(Date.UTC(y, m - 1, d - (days - 1 - i))).toISOString().slice(0, 10);\n return byDay.get(day) ?? { day, visitors: 0, pageviews: 0 };\n });\n}\n\n/** Whole-percent change against the previous period, or null when there is nothing to compare with. */\nexport function changePct(now: number, before: number): number | null {\n return before === 0 ? null : Math.round(((now - before) / before) * 100);\n}\n\n// ── Tenant switcher (F029.10) ──────────────────────────────────────────────\n//\n// The user's own organisations, from the app's backend (@broberg/tenant/hono's\n// tenantRoutes: GET /memberships). This package never imports @broberg/tenant;\n// the row type is structural.\n\nexport interface TenantMembershipRow {\n tenant: { id: string; slug: string; name: string; status?: \"active\" | \"suspended\" };\n role: string;\n}\n\nexport interface TenantAdapter {\n load(): Promise<TenantMembershipRow[]>;\n}\n\nexport class TenantLoadError extends Error {\n constructor(readonly status: number) {\n super(`HTTP ${status}`);\n this.name = \"TenantLoadError\";\n }\n}\n\n/** GET {url} (default /api/me/memberships) → the user's memberships. Non-2xx or a bad body THROWS. */\nexport function createFetchTenantAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): TenantAdapter {\n return {\n async load() {\n let res: Response;\n try {\n res = await (opts.fetch ?? fetch)(opts.url ?? \"/api/me/memberships\", { credentials: opts.credentials ?? \"same-origin\", headers: { accept: \"application/json\" } });\n } catch {\n throw new TenantLoadError(0);\n }\n const body = (await res.json().catch(() => null)) as unknown;\n if (!res.ok) throw new TenantLoadError(res.status);\n if (!Array.isArray(body) || !body.every((r) => r && typeof r.role === \"string\" && r.tenant && typeof r.tenant.slug === \"string\")) {\n throw new TenantLoadError(res.status);\n }\n return body as TenantMembershipRow[];\n },\n };\n}\n\n// ── Text ───────────────────────────────────────────────────────────────────\n\nexport const TEXT = {\n da: {\n openMenu: \"Åbn menu\",\n closeMenu: \"Luk menu\",\n home: \"Forside\",\n working: \"Arbejder\",\n moreFor: \"Mere for\",\n back: \"Tilbage\",\n pinned: \"Fastgjort\",\n language: \"Sprog\",\n zoom: \"Zoom\",\n zoomOut: \"Zoom ud\",\n zoomIn: \"Zoom ind\",\n couldNotMarkAll: \"Kunne ikke markere alle som læst. Prøv igen.\",\n collapse: \"Skjul sidemenu\",\n expand: \"Vis sidemenu\",\n notifications: \"Notifikationer\",\n markAllRead: \"Markér alle læst\",\n noNotifications: \"Ingen notifikationer endnu.\",\n couldNotLoad: \"Kunne ikke hente notifikationer.\",\n seeAll: \"Vis alle\",\n close: \"Luk\",\n account: \"Konto\",\n signOut: \"Log ud\",\n theme: \"Tema\",\n light: \"Lys\",\n dark: \"Mørk\",\n system: \"System\",\n palette: \"Palet\",\n surfaces: \"Flader\",\n flat: \"Flade\",\n layered: \"Lag\",\n backdrop: \"Baggrund\",\n neurons: \"Neuroner\",\n plain: \"Ren\",\n name: \"Navn\",\n email: \"E-mail\",\n save: \"Gem\",\n saving: \"Gemmer…\",\n saved: \"Gemt\",\n loadingProfile: \"Henter profil…\",\n couldNotLoadProfile: \"Kunne ikke hente din profil.\",\n couldNotSave: \"Kunne ikke gemme. Prøv igen.\",\n reauth: \"Log ind igen for at rette\",\n signInAgain: \"Log ind igen\",\n changePicture: \"Skift billede\",\n removePicture: \"Fjern billede\",\n confirmRemovePicture: \"Fjern dit billede?\",\n remove: \"Fjern\",\n cancel: \"Annuller\",\n pictureSaved: \"Billedet er gemt\",\n pictureRemoved: \"Billedet er fjernet\",\n pictureTooLarge: \"Billedet er for stort — højst 2 MB.\",\n pictureWrongType: \"Vælg et PNG-, JPEG- eller WebP-billede.\",\n bidSecurity: \"Sikkerhed i Broberg ID\",\n analyticsPeriod: \"Periode\",\n analyticsDays: \"{n} dage\",\n analyticsVisitors: \"Besøgende\",\n analyticsPageviews: \"Sidevisninger\",\n analyticsVsPrev: \"{pct} % mod forrige periode\",\n analyticsNoPrev: \"Ingen sammenligning\",\n analyticsOverTime: \"Sidevisninger pr. dag\",\n analyticsTopPages: \"Mest besøgte sider\",\n analyticsDay: \"Dag\",\n analyticsPage: \"Side\",\n analyticsEmpty: \"Ingen besøg i perioden.\",\n analyticsLoading: \"Henter tal…\",\n analyticsUnconfigured: \"Analytics er ikke sat op for denne app.\",\n analyticsUnavailable: \"Upmetrics svarer ikke lige nu. Prøv igen om lidt.\",\n tenantSwitch: \"Skift organisation\",\n tenantLoadFailed: \"Kunne ikke hente dine organisationer.\",\n tenantSwitchFailed: \"Kunne ikke skifte organisation. Du er stadig i {name}.\",\n tenantRetry: \"Prøv igen\",\n tenantSuspended: \"lukket\",\n tenantActive: \"aktiv\",\n },\n en: {\n openMenu: \"Open menu\",\n closeMenu: \"Close menu\",\n home: \"Home\",\n working: \"Working\",\n moreFor: \"More for\",\n back: \"Back\",\n pinned: \"Pinned\",\n language: \"Language\",\n zoom: \"Zoom\",\n zoomOut: \"Zoom out\",\n zoomIn: \"Zoom in\",\n couldNotMarkAll: \"Could not mark all as read. Try again.\",\n collapse: \"Hide sidebar\",\n expand: \"Show sidebar\",\n notifications: \"Notifications\",\n markAllRead: \"Mark all read\",\n noNotifications: \"No notifications yet.\",\n couldNotLoad: \"Could not load notifications.\",\n seeAll: \"See all\",\n close: \"Close\",\n account: \"Account\",\n signOut: \"Sign out\",\n theme: \"Theme\",\n light: \"Light\",\n dark: \"Dark\",\n system: \"System\",\n palette: \"Palette\",\n surfaces: \"Surfaces\",\n flat: \"Flat\",\n layered: \"Layered\",\n backdrop: \"Backdrop\",\n neurons: \"Neurons\",\n plain: \"Plain\",\n name: \"Name\",\n email: \"Email\",\n save: \"Save\",\n saving: \"Saving…\",\n saved: \"Saved\",\n loadingProfile: \"Loading profile…\",\n couldNotLoadProfile: \"Could not load your profile.\",\n couldNotSave: \"Could not save. Try again.\",\n reauth: \"Sign in again to make changes\",\n signInAgain: \"Sign in again\",\n changePicture: \"Change picture\",\n removePicture: \"Remove picture\",\n confirmRemovePicture: \"Remove your picture?\",\n remove: \"Remove\",\n cancel: \"Cancel\",\n pictureSaved: \"Picture saved\",\n pictureRemoved: \"Picture removed\",\n pictureTooLarge: \"The picture is too large — 2 MB at most.\",\n pictureWrongType: \"Choose a PNG, JPEG or WebP image.\",\n bidSecurity: \"Security in Broberg ID\",\n analyticsPeriod: \"Period\",\n analyticsDays: \"{n} days\",\n analyticsVisitors: \"Visitors\",\n analyticsPageviews: \"Page views\",\n analyticsVsPrev: \"{pct}% vs previous period\",\n analyticsNoPrev: \"No comparison\",\n analyticsOverTime: \"Page views per day\",\n analyticsTopPages: \"Most visited pages\",\n analyticsDay: \"Day\",\n analyticsPage: \"Page\",\n analyticsEmpty: \"No visits in this period.\",\n analyticsLoading: \"Loading figures…\",\n analyticsUnconfigured: \"Analytics is not set up for this app.\",\n analyticsUnavailable: \"Upmetrics is not answering right now. Try again shortly.\",\n tenantSwitch: \"Switch organisation\",\n tenantLoadFailed: \"Could not load your organisations.\",\n tenantSwitchFailed: \"Could not switch organisation. You are still in {name}.\",\n tenantRetry: \"Try again\",\n tenantSuspended: \"suspended\",\n tenantActive: \"active\",\n },\n} as const;\nexport type Lang = keyof typeof TEXT;\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["d"],"mappings":"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","file":"index.cjs","sourcesContent":["// @broberg/app-shell — the framework-free core of the fleet's app shell (F092).\n//\n// The shell every app in the fleet had been copying: sidebar + top bar +\n// notification bell + user menu. cardmem and trail each had one (\"ported from\n// trail\"), and helpdesk and the appkit starter would have made the third and\n// fourth. This file holds what does not depend on a framework: the navigation\n// data model, the notification contract and its sources, and the small rules\n// both the Preact and React parts must agree on.\n//\n// Christian, 4 Oct 2026: notifications are a STANDARD part of the template, not\n// an optional slot — so the contract and a default source live here, and a new\n// app gets a working notification centre by pointing one URL at its backend.\n\n// ── Navigation ─────────────────────────────────────────────────────────────\n\n/** One sidebar entry. `icon` is whatever your framework renders (an element). */\nexport interface NavItem<Icon = unknown> {\n id: string;\n label: string;\n href: string;\n icon?: Icon;\n /** A count shown on the item (99+ above 99). 0/undefined shows nothing. */\n badge?: number;\n /**\n * Sub-items, ONE level (F092.5). An item with children is a toggle that opens\n * and closes them, not a link; its own `href` is used only to decide whether\n * it is on the current path.\n */\n children?: NavItem<Icon>[];\n /** F092.20 — a status marker before the label; \"working\" = a run in progress. */\n marker?: \"working\";\n /** F092.20 — shows a pin icon after the label. */\n pinned?: boolean;\n /** F092.21 — row actions: a «…» button opens a menu with these. */\n actions?: NavAction[];\n}\n/**\n * F092.21 — one row action. `submenu` switches the menu to a sub-list (with a\n * back arrow); `input` switches it to a field + save (e.g. rename). A plain\n * action, a submenu leaf and a saved input all call `onItemAction`.\n */\nexport interface NavAction {\n id: string;\n label: string;\n icon?: unknown;\n /** A single letter that picks this action while the menu is open (e.g. \"P\"). */\n kbd?: string;\n /** Shown in the danger colour (delete). */\n danger?: boolean;\n submenu?: NavAction[];\n input?: { value: string; placeholder?: string; saveLabel?: string };\n /** F092.22 — ask twice: the first pick shows `label` («Tryk igen for at slette») and keeps the menu open; the second runs it. */\n confirm?: { label: string };\n}\nexport interface NavSection<Icon = unknown> {\n /** A sub-heading inside one group, e.g. \"I dag\" / \"I går\". */\n label: string;\n items: NavItem<Icon>[];\n}\nexport interface NavGroup<Icon = unknown> {\n label: string;\n items: NavItem<Icon>[];\n /** F092.20 — \"plain\": the group label in normal text (not uppercase/mono). */\n variant?: \"default\" | \"plain\";\n /** F092.20 — sub-headed lists inside this group, shown after `items`. */\n sections?: NavSection<Icon>[];\n}\n\n/**\n * Is this item the current page? Exact match for \"/\", otherwise the path is the\n * item or below it (\"/board/123\" is on \"/board\", \"/boardroom\" is not).\n */\nexport function isActivePath(href: string, currentPath: string): boolean {\n const path = (currentPath.split(/[?#]/)[0] || \"/\").replace(/\\/+$/, \"\") || \"/\";\n const target = (href.split(/[?#]/)[0] || \"/\").replace(/\\/+$/, \"\") || \"/\";\n if (target === \"/\") return path === \"/\";\n return path === target || path.startsWith(target + \"/\");\n}\n\n/**\n * A notification's `navigate` is SERVER DATA, and it ends in `location.assign`.\n * `javascript:` there runs code in the app, so only a same-site path or an\n * http(s) URL is followed; anything else returns null and the row just closes.\n */\nexport function safeNavigate(target: string | null | undefined): string | null {\n if (typeof target !== \"string\") return null;\n const t = target.trim();\n if (t.startsWith(\"/\") && !t.startsWith(\"//\") && !t.startsWith(\"/\\\\\")) return t;\n try {\n const u = new URL(t);\n return u.protocol === \"https:\" || u.protocol === \"http:\" ? u.href : null;\n } catch {\n return null;\n }\n}\n\n/**\n * F092.10 — is this link leaving the app? An http(s) link to another origin.\n * Paths, \"#…\", mailto:/tel: and the app's own origin are not. Owner decision\n * D-d28547: every external link opens in a new tab.\n */\nexport function isExternal(\n href: string | null | undefined,\n origin: string | undefined = typeof location !== \"undefined\" ? location.origin : undefined,\n): boolean {\n if (typeof href !== \"string\") return false;\n const h = href.trim();\n if (!origin) return /^(https?:)?\\/\\//i.test(h); // no page to compare with: absolute = external\n let u: URL;\n try {\n u = new URL(h, origin);\n } catch {\n return false;\n }\n return (u.protocol === \"http:\" || u.protocol === \"https:\") && u.origin !== origin;\n}\n\n/**\n * The label of the nav item the user is on, for the content header (F092.4).\n * Sub-items count, and the most specific match wins: on \"/models/genesis\" a\n * \"Genesis\" child beats its \"Models\" parent.\n */\nexport function activeNavLabel<Icon>(\n groups: NavGroup<Icon>[],\n footer: NavItem<Icon>[] | undefined,\n currentPath: string,\n): string | undefined {\n let best: { label: string; len: number } | undefined;\n const visit = (it: NavItem<Icon>) => {\n if (it.href && isActivePath(it.href, currentPath)) {\n const len = it.href.length;\n if (!best || len > best.len) best = { label: it.label, len };\n }\n it.children?.forEach(visit);\n };\n for (const g of groups) g.items.forEach(visit);\n footer?.forEach(visit);\n return best?.label;\n}\n\n/** \"99+\" above 99, nothing for 0 or less. */\nexport function badgeLabel(n: number | undefined): string | null {\n if (!n || n <= 0) return null;\n return n > 99 ? \"99+\" : String(n);\n}\n\n// ── Persisted UI state ─────────────────────────────────────────────────────\n\n/** Read a boolean flag from localStorage; storage errors read as `fallback`. */\nexport function readFlag(key: string, fallback: boolean): boolean {\n try {\n const v = typeof localStorage === \"undefined\" ? null : localStorage.getItem(key);\n return v === null ? fallback : v === \"1\";\n } catch {\n return fallback;\n }\n}\nexport function writeFlag(key: string, value: boolean): void {\n try {\n if (typeof localStorage !== \"undefined\") localStorage.setItem(key, value ? \"1\" : \"0\");\n } catch {\n /* private mode / quota: the UI still works, it just does not remember */\n }\n}\nexport function readSet(key: string): Set<string> {\n try {\n const raw = typeof localStorage === \"undefined\" ? null : localStorage.getItem(key);\n const v = raw ? (JSON.parse(raw) as unknown) : [];\n return new Set(Array.isArray(v) ? v.filter((x): x is string => typeof x === \"string\") : []);\n } catch {\n return new Set();\n }\n}\nexport function writeSet(key: string, value: Set<string>): void {\n try {\n if (typeof localStorage !== \"undefined\") localStorage.setItem(key, JSON.stringify([...value]));\n } catch {\n /* see writeFlag */\n }\n}\n\n// ── Avatar ─────────────────────────────────────────────────────────────────\n\n/**\n * Two initials for the avatar fallback: first letter of the first and the last\n * word, upper-cased. One word gives one letter; nothing gives \"?\".\n *\n * There is deliberately no Gravatar here (owner decision D-1dc849): the avatar\n * is the Broberg ID `picture`, and without one, initials. Gravatar would send a\n * hash of the user's email to a US service on every render.\n */\nexport function initials(name: string | undefined, email?: string): string {\n const source = (name ?? \"\").trim() || (email ?? \"\").split(\"@\")[0] || \"\";\n const words = source.split(/[\\s._-]+/).filter(Boolean);\n if (words.length === 0) return \"?\";\n const first = words[0]![0] ?? \"\";\n const last = words.length > 1 ? words[words.length - 1]![0] ?? \"\" : \"\";\n return (first + last).toUpperCase() || \"?\";\n}\n\n// ── Notifications ──────────────────────────────────────────────────────────\n//\n// REUSE, NOT A SECOND CONTRACT. The fleet already agreed the notification shape\n// in @broberg/notifications (F074): `NotificationRow` — the same field names\n// @broberg/webpush sends, end to end — and the server core that owns the ONE\n// counting rule. The shell's bell renders that row and drives its panel with\n// that package's `createBellShell`, so a server built on `createNotifications`\n// and this bell speak the same language without an adapter.\n//\n// Christian, 4 Oct 2026: notifications are a STANDARD part of the template. So\n// a source, and a default one, ship here: point one URL at your backend.\n\nimport type { NotificationRow } from \"@broberg/notifications/types\";\nexport type { NotificationRow } from \"@broberg/notifications/types\";\n\n/** Where the bell gets its data. Swap it to point at any backend. */\nexport interface NotificationSource {\n /** The rows to show (newest first). */\n loadRows(): Promise<NotificationRow[]>;\n /** THE count — your server's, which may exclude muted kinds (F074's counting rule). */\n countUnseen(): Promise<number>;\n /** Returns the ids that actually transitioned to seen. */\n markSeen(ids: readonly string[]): Promise<string[]>;\n markAllSeen(): Promise<string[]>;\n /** Called with a callback that re-counts; return an unsubscribe. Optional. */\n subscribe?(onChange: () => void): () => void;\n}\n\n/**\n * The default source: a URL on your own backend, shaped after\n * @broberg/notifications' server core so `createNotifications` sits right\n * behind it.\n *\n * GET {url} → { items: NotificationRow[], unseen: number }\n * POST {url}/seen { ids: string[] } → { clearedIds: string[], count: number }\n * POST {url}/seen-all → { clearedIds: string[], count: number }\n *\n * It re-counts when the window regains focus or the tab becomes visible, and on\n * a timer (default 60 s). A failed GET THROWS — the bell shows «could not\n * load», never an empty list: \"could not fetch\" and \"nothing new\" must not look\n * the same.\n */\nexport function createFetchNotificationSource(opts: {\n url: string;\n fetch?: typeof fetch;\n /** Re-count interval in ms. 0 turns it off. Default 60 000. */\n pollMs?: number;\n credentials?: RequestCredentials;\n}): NotificationSource {\n const base = opts.url.replace(/\\/+$/, \"\");\n const doFetch = (input: string, init?: RequestInit) =>\n (opts.fetch ?? fetch)(input, { credentials: opts.credentials ?? \"include\", ...init });\n const get = async (): Promise<{ items: NotificationRow[]; unseen: number }> => {\n const res = await doFetch(base, { headers: { accept: \"application/json\" } });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const body = (await res.json().catch(() => null)) as { items?: unknown; unseen?: unknown } | null;\n if (!body || !Array.isArray(body.items)) throw new Error(\"response has no items array\");\n const items = body.items as NotificationRow[];\n const unseen = typeof body.unseen === \"number\" ? body.unseen : items.filter((n) => n.seenAt == null).length;\n return { items, unseen };\n };\n const post = async (path: string, body?: unknown): Promise<string[]> => {\n const res = await doFetch(`${base}${path}`, {\n method: \"POST\",\n headers: body === undefined ? undefined : { \"content-type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const r = (await res.json().catch(() => ({}))) as { clearedIds?: unknown };\n return Array.isArray(r.clearedIds) ? (r.clearedIds as string[]) : [];\n };\n return {\n loadRows: async () => (await get()).items,\n countUnseen: async () => (await get()).unseen,\n markSeen: (ids) => post(\"/seen\", { ids }),\n markAllSeen: () => post(\"/seen-all\"),\n subscribe(onChange) {\n if (typeof window === \"undefined\") return () => {};\n const onVisible = () => {\n if (document.visibilityState === \"visible\") onChange();\n };\n window.addEventListener(\"focus\", onChange);\n document.addEventListener(\"visibilitychange\", onVisible);\n const ms = opts.pollMs ?? 60_000;\n const timer = ms > 0 ? setInterval(onChange, ms) : null;\n return () => {\n window.removeEventListener(\"focus\", onChange);\n document.removeEventListener(\"visibilitychange\", onVisible);\n if (timer) clearInterval(timer);\n };\n },\n };\n}\n\n/** An in-memory source for demos and tests. */\nexport function createMemoryNotificationSource(initial: NotificationRow[] = []): NotificationSource & {\n push(n: NotificationRow): void;\n rows(): NotificationRow[];\n} {\n let rows = initial.map((n) => ({ ...n }));\n const listeners = new Set<() => void>();\n const emit = () => listeners.forEach((l) => l());\n const mark = (pred: (n: NotificationRow) => boolean): string[] => {\n const now = Date.now();\n const cleared: string[] = [];\n rows = rows.map((n) => {\n if (n.seenAt == null && pred(n)) {\n cleared.push(n.id);\n return { ...n, seenAt: now };\n }\n return n;\n });\n if (cleared.length) emit();\n return cleared;\n };\n return {\n loadRows: async () => rows.map((n) => ({ ...n })),\n countUnseen: async () => rows.filter((n) => n.seenAt == null).length,\n markSeen: async (ids) => mark((n) => ids.includes(n.id)),\n markAllSeen: async () => mark(() => true),\n subscribe(onChange) {\n listeners.add(onChange);\n return () => listeners.delete(onChange);\n },\n push(n) {\n rows = [n, ...rows];\n emit();\n },\n rows: () => rows.map((n) => ({ ...n })),\n };\n}\n\n/** \"nu\", \"5 min\", \"3 t\", \"i går\", \"4 d\" (da) or the English equivalents. */\nexport function relativeTime(ts: string | number, lang: \"da\" | \"en\", now = Date.now()): string {\n const ms = typeof ts === \"number\" ? ts : Date.parse(ts);\n if (!Number.isFinite(ms)) return \"\";\n const m = Math.floor((now - ms) / 60_000);\n if (m < 1) return lang === \"da\" ? \"nu\" : \"now\";\n if (m < 60) return `${m} min`;\n const h = Math.floor(m / 60);\n if (h < 24) return lang === \"da\" ? `${h} t` : `${h} h`;\n const d = Math.floor(h / 24);\n if (d === 1) return lang === \"da\" ? \"i går\" : \"yesterday\";\n return `${d} d`;\n}\n\n// ── Account page (F095.3) ─────────────────────────────────────────────────\n//\n// The user edits their own name and picture inside the app. The app's backend\n// (@broberg/sso's `accountRoutes()`, mounted on /api/account) talks to Broberg\n// ID; this package only talks to that backend, through an adapter, and never\n// imports @broberg/sso.\n\n/** What every account call resolves to — the profile AS THE SERVER NOW HOLDS IT. */\nexport interface AccountProfile {\n sub: string;\n name: string | null;\n picture: string | null;\n email: string | null;\n /** Broberg ID's own account page (password, passkeys, sessions). */\n account_url: string;\n}\n\n/** Where the account page gets and saves the profile. Swap it to point anywhere. */\nexport interface AccountAdapter {\n load(): Promise<AccountProfile>;\n saveName(name: string): Promise<AccountProfile>;\n uploadAvatar(file: Blob): Promise<AccountProfile>;\n removeAvatar(): Promise<AccountProfile>;\n}\n\n/** A failed account call: the HTTP status and the server's `error` code, if any. */\nexport class AccountError extends Error {\n constructor(\n readonly status: number,\n readonly code?: string,\n ) {\n super(code ? `HTTP ${status} ${code}` : `HTTP ${status}`);\n this.name = \"AccountError\";\n }\n}\n\nexport const AVATAR_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\"] as const;\nexport const AVATAR_MAX_BYTES = 2 * 1024 * 1024;\n\n/** Checked BEFORE uploading, so a file the server will refuse is never sent. */\nexport function checkAvatarFile(file: { size: number; type: string }): \"tooLarge\" | \"wrongType\" | null {\n if (!(AVATAR_TYPES as readonly string[]).includes(file.type)) return \"wrongType\";\n if (file.size > AVATAR_MAX_BYTES) return \"tooLarge\";\n return null;\n}\n\n/**\n * What a failed call means for the page. `reauth` = the user's session or its\n * scope does not allow the change (401, or 403 insufficient_scope): signing in\n * again is the fix, so the page says that instead of «try again».\n */\nexport function accountErrorKind(err: unknown): \"reauth\" | \"tooLarge\" | \"wrongType\" | \"other\" {\n if (!(err instanceof AccountError)) return \"other\";\n if (err.status === 401) return \"reauth\";\n if (err.status === 403 && err.code === \"insufficient_scope\") return \"reauth\";\n if (err.status === 413) return \"tooLarge\";\n if (err.status === 415) return \"wrongType\";\n return \"other\";\n}\n\n/**\n * The default adapter: the app's own backend, shaped after @broberg/sso's\n * `accountRoutes()`.\n *\n * GET {url}/profile → AccountProfile\n * POST {url}/profile {name} → AccountProfile\n * POST {url}/profile/avatar (raw bytes, Content-Type = the image's) → AccountProfile\n * POST {url}/profile/avatar/remove → AccountProfile\n *\n * A non-2xx THROWS an AccountError — a failed save must never look like a\n * saved one.\n */\nexport function createFetchAccountAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): AccountAdapter {\n const base = (opts.url ?? \"/api/account\").replace(/\\/+$/, \"\");\n const call = async (path: string, init?: RequestInit): Promise<AccountProfile> => {\n const res = await (opts.fetch ?? fetch)(`${base}${path}`, {\n credentials: opts.credentials ?? \"same-origin\",\n ...init,\n headers: { accept: \"application/json\", ...(init?.headers as Record<string, string> | undefined) },\n });\n const body = (await res.json().catch(() => null)) as Record<string, unknown> | null;\n if (!res.ok) throw new AccountError(res.status, typeof body?.error === \"string\" ? body.error : undefined);\n if (!body || typeof body.sub !== \"string\") throw new AccountError(res.status, \"bad_response\");\n return body as unknown as AccountProfile;\n };\n return {\n load: () => call(\"/profile\"),\n saveName: (name) =>\n call(\"/profile\", { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: JSON.stringify({ name }) }),\n uploadAvatar: (file) =>\n call(\"/profile/avatar\", { method: \"POST\", headers: { \"content-type\": file.type }, body: file }),\n removeAvatar: () => call(\"/profile/avatar/remove\", { method: \"POST\" }),\n };\n}\n\n// ── Analytics page (F097) ─────────────────────────────────────────────────\n//\n// The app's OWN visitor numbers from Upmetrics. The app's backend mounts\n// `analyticsRoutes()` from `@upmetrics/sdk/hono` (the uk_ key stays on the\n// server); this package only talks to that backend, through an adapter, and has\n// no dependency on the SDK — the type below is structural, so Upmetrics'\n// `AnalyticsResponse` fits it as is.\n\nexport interface AnalyticsDay {\n /** YYYY-MM-DD on the Europe/Copenhagen calendar (Upmetrics' own day key). */\n day: string;\n visitors: number;\n pageviews: number;\n}\n\nexport interface AnalyticsData {\n totals: { visitors: number; pageviews: number };\n /** The same-length period just before this one. */\n prev: { visitors: number; pageviews: number };\n /** Only days that had visits — use fillDays() to draw the whole period. */\n series: AnalyticsDay[];\n topPages: { value: string; count: number }[];\n}\n\nexport interface AnalyticsAdapter {\n load(days: number): Promise<AnalyticsData>;\n}\n\n/** A failed analytics call: HTTP status (0 = no answer) and the server's `error` code. */\nexport class AnalyticsError extends Error {\n constructor(\n readonly status: number,\n readonly code?: string,\n ) {\n super(code ? `HTTP ${status} ${code}` : `HTTP ${status}`);\n this.name = \"AnalyticsError\";\n }\n}\n\n/** \"unconfigured\" only for the backend's explicit 503 analytics_unconfigured; anything else is \"unavailable\". */\nexport function analyticsErrorKind(err: unknown): \"unconfigured\" | \"unavailable\" {\n return err instanceof AnalyticsError && err.status === 503 && err.code === \"analytics_unconfigured\" ? \"unconfigured\" : \"unavailable\";\n}\n\n/**\n * The default adapter: `GET {url}?days=N` on the app's own backend (default\n * /api/analytics, where `@upmetrics/sdk/hono`'s analyticsRoutes() answers).\n * A non-2xx, an unreadable body or no answer at all THROWS — never zeros that\n * look like a real, quiet month.\n */\nexport function createFetchAnalyticsAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): AnalyticsAdapter {\n const url = opts.url ?? \"/api/analytics\";\n return {\n async load(days) {\n let res: Response;\n try {\n res = await (opts.fetch ?? fetch)(`${url}${url.includes(\"?\") ? \"&\" : \"?\"}days=${encodeURIComponent(String(days))}`, {\n credentials: opts.credentials ?? \"same-origin\",\n headers: { accept: \"application/json\" },\n });\n } catch {\n throw new AnalyticsError(0);\n }\n const body = (await res.json().catch(() => null)) as Record<string, unknown> | null;\n if (!res.ok) throw new AnalyticsError(res.status, typeof body?.error === \"string\" ? body.error : undefined);\n const ok =\n body !== null &&\n typeof (body.totals as Record<string, unknown> | undefined)?.visitors === \"number\" &&\n Array.isArray(body.series) &&\n Array.isArray(body.topPages);\n if (!ok) throw new AnalyticsError(res.status, \"bad_response\");\n return body as unknown as AnalyticsData;\n },\n };\n}\n\n/** Today's YYYY-MM-DD on the Copenhagen calendar — the calendar Upmetrics buckets on. */\nexport function copenhagenDay(at: Date = new Date()): string {\n return new Intl.DateTimeFormat(\"en-CA\", { timeZone: \"Europe/Copenhagen\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).format(at);\n}\n\n/**\n * Exactly `days` entries ending today (Copenhagen calendar), with Upmetrics'\n * values where it sent a day and zeros where it did not — Upmetrics only sends\n * days that had visits. Calendar arithmetic, so a DST change never skips or\n * repeats a day.\n */\nexport function fillDays(series: AnalyticsDay[], days: number, now: Date = new Date()): AnalyticsDay[] {\n const byDay = new Map(series.map((d) => [d.day, d]));\n const [y, m, d] = copenhagenDay(now).split(\"-\").map(Number) as [number, number, number];\n return Array.from({ length: days }, (_, i) => {\n const day = new Date(Date.UTC(y, m - 1, d - (days - 1 - i))).toISOString().slice(0, 10);\n return byDay.get(day) ?? { day, visitors: 0, pageviews: 0 };\n });\n}\n\n/** Whole-percent change against the previous period, or null when there is nothing to compare with. */\nexport function changePct(now: number, before: number): number | null {\n return before === 0 ? null : Math.round(((now - before) / before) * 100);\n}\n\n// ── Tenant switcher (F029.10) ──────────────────────────────────────────────\n//\n// The user's own organisations, from the app's backend (@broberg/tenant/hono's\n// tenantRoutes: GET /memberships). This package never imports @broberg/tenant;\n// the row type is structural.\n\nexport interface TenantMembershipRow {\n tenant: { id: string; slug: string; name: string; status?: \"active\" | \"suspended\" };\n role: string;\n}\n\nexport interface TenantAdapter {\n load(): Promise<TenantMembershipRow[]>;\n}\n\nexport class TenantLoadError extends Error {\n constructor(readonly status: number) {\n super(`HTTP ${status}`);\n this.name = \"TenantLoadError\";\n }\n}\n\n/** GET {url} (default /api/me/memberships) → the user's memberships. Non-2xx or a bad body THROWS. */\nexport function createFetchTenantAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): TenantAdapter {\n return {\n async load() {\n let res: Response;\n try {\n res = await (opts.fetch ?? fetch)(opts.url ?? \"/api/me/memberships\", { credentials: opts.credentials ?? \"same-origin\", headers: { accept: \"application/json\" } });\n } catch {\n throw new TenantLoadError(0);\n }\n const body = (await res.json().catch(() => null)) as unknown;\n if (!res.ok) throw new TenantLoadError(res.status);\n if (!Array.isArray(body) || !body.every((r) => r && typeof r.role === \"string\" && r.tenant && typeof r.tenant.slug === \"string\")) {\n throw new TenantLoadError(res.status);\n }\n return body as TenantMembershipRow[];\n },\n };\n}\n\n// ── Text ───────────────────────────────────────────────────────────────────\n\nexport const TEXT = {\n da: {\n openMenu: \"Åbn menu\",\n closeMenu: \"Luk menu\",\n home: \"Forside\",\n working: \"Arbejder\",\n moreFor: \"Mere for\",\n back: \"Tilbage\",\n pinned: \"Fastgjort\",\n language: \"Sprog\",\n zoom: \"Zoom\",\n zoomOut: \"Zoom ud\",\n zoomIn: \"Zoom ind\",\n couldNotMarkAll: \"Kunne ikke markere alle som læst. Prøv igen.\",\n collapse: \"Skjul sidemenu\",\n expand: \"Vis sidemenu\",\n notifications: \"Notifikationer\",\n markAllRead: \"Markér alle læst\",\n noNotifications: \"Ingen notifikationer endnu.\",\n couldNotLoad: \"Kunne ikke hente notifikationer.\",\n seeAll: \"Vis alle\",\n close: \"Luk\",\n account: \"Konto\",\n signOut: \"Log ud\",\n theme: \"Tema\",\n light: \"Lys\",\n dark: \"Mørk\",\n system: \"System\",\n palette: \"Palet\",\n surfaces: \"Flader\",\n flat: \"Flade\",\n layered: \"Lag\",\n backdrop: \"Baggrund\",\n neurons: \"Neuroner\",\n plain: \"Ren\",\n name: \"Navn\",\n email: \"E-mail\",\n save: \"Gem\",\n saving: \"Gemmer…\",\n saved: \"Gemt\",\n loadingProfile: \"Henter profil…\",\n couldNotLoadProfile: \"Kunne ikke hente din profil.\",\n couldNotSave: \"Kunne ikke gemme. Prøv igen.\",\n reauth: \"Log ind igen for at rette\",\n signInAgain: \"Log ind igen\",\n changePicture: \"Skift billede\",\n removePicture: \"Fjern billede\",\n confirmRemovePicture: \"Fjern dit billede?\",\n remove: \"Fjern\",\n cancel: \"Annuller\",\n pictureSaved: \"Billedet er gemt\",\n pictureRemoved: \"Billedet er fjernet\",\n pictureTooLarge: \"Billedet er for stort — højst 2 MB.\",\n pictureWrongType: \"Vælg et PNG-, JPEG- eller WebP-billede.\",\n bidSecurity: \"Sikkerhed i Broberg ID\",\n analyticsPeriod: \"Periode\",\n analyticsDays: \"{n} dage\",\n analyticsVisitors: \"Besøgende\",\n analyticsPageviews: \"Sidevisninger\",\n analyticsVsPrev: \"{pct} % mod forrige periode\",\n analyticsNoPrev: \"Ingen sammenligning\",\n analyticsOverTime: \"Sidevisninger pr. dag\",\n analyticsTopPages: \"Mest besøgte sider\",\n analyticsDay: \"Dag\",\n analyticsPage: \"Side\",\n analyticsEmpty: \"Ingen besøg i perioden.\",\n analyticsLoading: \"Henter tal…\",\n analyticsUnconfigured: \"Analytics er ikke sat op for denne app.\",\n analyticsUnavailable: \"Upmetrics svarer ikke lige nu. Prøv igen om lidt.\",\n tenantSwitch: \"Skift organisation\",\n tenantLoadFailed: \"Kunne ikke hente dine organisationer.\",\n tenantSwitchFailed: \"Kunne ikke skifte organisation. Du er stadig i {name}.\",\n tenantRetry: \"Prøv igen\",\n tenantSuspended: \"lukket\",\n tenantActive: \"aktiv\",\n },\n en: {\n openMenu: \"Open menu\",\n closeMenu: \"Close menu\",\n home: \"Home\",\n working: \"Working\",\n moreFor: \"More for\",\n back: \"Back\",\n pinned: \"Pinned\",\n language: \"Language\",\n zoom: \"Zoom\",\n zoomOut: \"Zoom out\",\n zoomIn: \"Zoom in\",\n couldNotMarkAll: \"Could not mark all as read. Try again.\",\n collapse: \"Hide sidebar\",\n expand: \"Show sidebar\",\n notifications: \"Notifications\",\n markAllRead: \"Mark all read\",\n noNotifications: \"No notifications yet.\",\n couldNotLoad: \"Could not load notifications.\",\n seeAll: \"See all\",\n close: \"Close\",\n account: \"Account\",\n signOut: \"Sign out\",\n theme: \"Theme\",\n light: \"Light\",\n dark: \"Dark\",\n system: \"System\",\n palette: \"Palette\",\n surfaces: \"Surfaces\",\n flat: \"Flat\",\n layered: \"Layered\",\n backdrop: \"Backdrop\",\n neurons: \"Neurons\",\n plain: \"Plain\",\n name: \"Name\",\n email: \"Email\",\n save: \"Save\",\n saving: \"Saving…\",\n saved: \"Saved\",\n loadingProfile: \"Loading profile…\",\n couldNotLoadProfile: \"Could not load your profile.\",\n couldNotSave: \"Could not save. Try again.\",\n reauth: \"Sign in again to make changes\",\n signInAgain: \"Sign in again\",\n changePicture: \"Change picture\",\n removePicture: \"Remove picture\",\n confirmRemovePicture: \"Remove your picture?\",\n remove: \"Remove\",\n cancel: \"Cancel\",\n pictureSaved: \"Picture saved\",\n pictureRemoved: \"Picture removed\",\n pictureTooLarge: \"The picture is too large — 2 MB at most.\",\n pictureWrongType: \"Choose a PNG, JPEG or WebP image.\",\n bidSecurity: \"Security in Broberg ID\",\n analyticsPeriod: \"Period\",\n analyticsDays: \"{n} days\",\n analyticsVisitors: \"Visitors\",\n analyticsPageviews: \"Page views\",\n analyticsVsPrev: \"{pct}% vs previous period\",\n analyticsNoPrev: \"No comparison\",\n analyticsOverTime: \"Page views per day\",\n analyticsTopPages: \"Most visited pages\",\n analyticsDay: \"Day\",\n analyticsPage: \"Page\",\n analyticsEmpty: \"No visits in this period.\",\n analyticsLoading: \"Loading figures…\",\n analyticsUnconfigured: \"Analytics is not set up for this app.\",\n analyticsUnavailable: \"Upmetrics is not answering right now. Try again shortly.\",\n tenantSwitch: \"Switch organisation\",\n tenantLoadFailed: \"Could not load your organisations.\",\n tenantSwitchFailed: \"Could not switch organisation. You are still in {name}.\",\n tenantRetry: \"Try again\",\n tenantSuspended: \"suspended\",\n tenantActive: \"active\",\n },\n} as const;\nexport type Lang = keyof typeof TEXT;\n"]}
package/dist/index.d.cts CHANGED
@@ -41,6 +41,10 @@ interface NavAction {
41
41
  placeholder?: string;
42
42
  saveLabel?: string;
43
43
  };
44
+ /** F092.22 — ask twice: the first pick shows `label` («Tryk igen for at slette») and keeps the menu open; the second runs it. */
45
+ confirm?: {
46
+ label: string;
47
+ };
44
48
  }
45
49
  interface NavSection<Icon = unknown> {
46
50
  /** A sub-heading inside one group, e.g. "I dag" / "I går". */
package/dist/index.d.ts CHANGED
@@ -41,6 +41,10 @@ interface NavAction {
41
41
  placeholder?: string;
42
42
  saveLabel?: string;
43
43
  };
44
+ /** F092.22 — ask twice: the first pick shows `label` («Tryk igen for at slette») and keeps the menu open; the second runs it. */
45
+ confirm?: {
46
+ label: string;
47
+ };
44
48
  }
45
49
  interface NavSection<Icon = unknown> {
46
50
  /** A sub-heading inside one group, e.g. "I dag" / "I går". */
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/index.ts"],"names":["d"],"mappings":"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","file":"index.js","sourcesContent":["// @broberg/app-shell — the framework-free core of the fleet's app shell (F092).\n//\n// The shell every app in the fleet had been copying: sidebar + top bar +\n// notification bell + user menu. cardmem and trail each had one (\"ported from\n// trail\"), and helpdesk and the appkit starter would have made the third and\n// fourth. This file holds what does not depend on a framework: the navigation\n// data model, the notification contract and its sources, and the small rules\n// both the Preact and React parts must agree on.\n//\n// Christian, 4 Oct 2026: notifications are a STANDARD part of the template, not\n// an optional slot — so the contract and a default source live here, and a new\n// app gets a working notification centre by pointing one URL at its backend.\n\n// ── Navigation ─────────────────────────────────────────────────────────────\n\n/** One sidebar entry. `icon` is whatever your framework renders (an element). */\nexport interface NavItem<Icon = unknown> {\n id: string;\n label: string;\n href: string;\n icon?: Icon;\n /** A count shown on the item (99+ above 99). 0/undefined shows nothing. */\n badge?: number;\n /**\n * Sub-items, ONE level (F092.5). An item with children is a toggle that opens\n * and closes them, not a link; its own `href` is used only to decide whether\n * it is on the current path.\n */\n children?: NavItem<Icon>[];\n /** F092.20 — a status marker before the label; \"working\" = a run in progress. */\n marker?: \"working\";\n /** F092.20 — shows a pin icon after the label. */\n pinned?: boolean;\n /** F092.21 — row actions: a «…» button opens a menu with these. */\n actions?: NavAction[];\n}\n/**\n * F092.21 — one row action. `submenu` switches the menu to a sub-list (with a\n * back arrow); `input` switches it to a field + save (e.g. rename). A plain\n * action, a submenu leaf and a saved input all call `onItemAction`.\n */\nexport interface NavAction {\n id: string;\n label: string;\n icon?: unknown;\n /** A single letter that picks this action while the menu is open (e.g. \"P\"). */\n kbd?: string;\n /** Shown in the danger colour (delete). */\n danger?: boolean;\n submenu?: NavAction[];\n input?: { value: string; placeholder?: string; saveLabel?: string };\n}\nexport interface NavSection<Icon = unknown> {\n /** A sub-heading inside one group, e.g. \"I dag\" / \"I går\". */\n label: string;\n items: NavItem<Icon>[];\n}\nexport interface NavGroup<Icon = unknown> {\n label: string;\n items: NavItem<Icon>[];\n /** F092.20 — \"plain\": the group label in normal text (not uppercase/mono). */\n variant?: \"default\" | \"plain\";\n /** F092.20 — sub-headed lists inside this group, shown after `items`. */\n sections?: NavSection<Icon>[];\n}\n\n/**\n * Is this item the current page? Exact match for \"/\", otherwise the path is the\n * item or below it (\"/board/123\" is on \"/board\", \"/boardroom\" is not).\n */\nexport function isActivePath(href: string, currentPath: string): boolean {\n const path = (currentPath.split(/[?#]/)[0] || \"/\").replace(/\\/+$/, \"\") || \"/\";\n const target = (href.split(/[?#]/)[0] || \"/\").replace(/\\/+$/, \"\") || \"/\";\n if (target === \"/\") return path === \"/\";\n return path === target || path.startsWith(target + \"/\");\n}\n\n/**\n * A notification's `navigate` is SERVER DATA, and it ends in `location.assign`.\n * `javascript:` there runs code in the app, so only a same-site path or an\n * http(s) URL is followed; anything else returns null and the row just closes.\n */\nexport function safeNavigate(target: string | null | undefined): string | null {\n if (typeof target !== \"string\") return null;\n const t = target.trim();\n if (t.startsWith(\"/\") && !t.startsWith(\"//\") && !t.startsWith(\"/\\\\\")) return t;\n try {\n const u = new URL(t);\n return u.protocol === \"https:\" || u.protocol === \"http:\" ? u.href : null;\n } catch {\n return null;\n }\n}\n\n/**\n * F092.10 — is this link leaving the app? An http(s) link to another origin.\n * Paths, \"#…\", mailto:/tel: and the app's own origin are not. Owner decision\n * D-d28547: every external link opens in a new tab.\n */\nexport function isExternal(\n href: string | null | undefined,\n origin: string | undefined = typeof location !== \"undefined\" ? location.origin : undefined,\n): boolean {\n if (typeof href !== \"string\") return false;\n const h = href.trim();\n if (!origin) return /^(https?:)?\\/\\//i.test(h); // no page to compare with: absolute = external\n let u: URL;\n try {\n u = new URL(h, origin);\n } catch {\n return false;\n }\n return (u.protocol === \"http:\" || u.protocol === \"https:\") && u.origin !== origin;\n}\n\n/**\n * The label of the nav item the user is on, for the content header (F092.4).\n * Sub-items count, and the most specific match wins: on \"/models/genesis\" a\n * \"Genesis\" child beats its \"Models\" parent.\n */\nexport function activeNavLabel<Icon>(\n groups: NavGroup<Icon>[],\n footer: NavItem<Icon>[] | undefined,\n currentPath: string,\n): string | undefined {\n let best: { label: string; len: number } | undefined;\n const visit = (it: NavItem<Icon>) => {\n if (it.href && isActivePath(it.href, currentPath)) {\n const len = it.href.length;\n if (!best || len > best.len) best = { label: it.label, len };\n }\n it.children?.forEach(visit);\n };\n for (const g of groups) g.items.forEach(visit);\n footer?.forEach(visit);\n return best?.label;\n}\n\n/** \"99+\" above 99, nothing for 0 or less. */\nexport function badgeLabel(n: number | undefined): string | null {\n if (!n || n <= 0) return null;\n return n > 99 ? \"99+\" : String(n);\n}\n\n// ── Persisted UI state ─────────────────────────────────────────────────────\n\n/** Read a boolean flag from localStorage; storage errors read as `fallback`. */\nexport function readFlag(key: string, fallback: boolean): boolean {\n try {\n const v = typeof localStorage === \"undefined\" ? null : localStorage.getItem(key);\n return v === null ? fallback : v === \"1\";\n } catch {\n return fallback;\n }\n}\nexport function writeFlag(key: string, value: boolean): void {\n try {\n if (typeof localStorage !== \"undefined\") localStorage.setItem(key, value ? \"1\" : \"0\");\n } catch {\n /* private mode / quota: the UI still works, it just does not remember */\n }\n}\nexport function readSet(key: string): Set<string> {\n try {\n const raw = typeof localStorage === \"undefined\" ? null : localStorage.getItem(key);\n const v = raw ? (JSON.parse(raw) as unknown) : [];\n return new Set(Array.isArray(v) ? v.filter((x): x is string => typeof x === \"string\") : []);\n } catch {\n return new Set();\n }\n}\nexport function writeSet(key: string, value: Set<string>): void {\n try {\n if (typeof localStorage !== \"undefined\") localStorage.setItem(key, JSON.stringify([...value]));\n } catch {\n /* see writeFlag */\n }\n}\n\n// ── Avatar ─────────────────────────────────────────────────────────────────\n\n/**\n * Two initials for the avatar fallback: first letter of the first and the last\n * word, upper-cased. One word gives one letter; nothing gives \"?\".\n *\n * There is deliberately no Gravatar here (owner decision D-1dc849): the avatar\n * is the Broberg ID `picture`, and without one, initials. Gravatar would send a\n * hash of the user's email to a US service on every render.\n */\nexport function initials(name: string | undefined, email?: string): string {\n const source = (name ?? \"\").trim() || (email ?? \"\").split(\"@\")[0] || \"\";\n const words = source.split(/[\\s._-]+/).filter(Boolean);\n if (words.length === 0) return \"?\";\n const first = words[0]![0] ?? \"\";\n const last = words.length > 1 ? words[words.length - 1]![0] ?? \"\" : \"\";\n return (first + last).toUpperCase() || \"?\";\n}\n\n// ── Notifications ──────────────────────────────────────────────────────────\n//\n// REUSE, NOT A SECOND CONTRACT. The fleet already agreed the notification shape\n// in @broberg/notifications (F074): `NotificationRow` — the same field names\n// @broberg/webpush sends, end to end — and the server core that owns the ONE\n// counting rule. The shell's bell renders that row and drives its panel with\n// that package's `createBellShell`, so a server built on `createNotifications`\n// and this bell speak the same language without an adapter.\n//\n// Christian, 4 Oct 2026: notifications are a STANDARD part of the template. So\n// a source, and a default one, ship here: point one URL at your backend.\n\nimport type { NotificationRow } from \"@broberg/notifications/types\";\nexport type { NotificationRow } from \"@broberg/notifications/types\";\n\n/** Where the bell gets its data. Swap it to point at any backend. */\nexport interface NotificationSource {\n /** The rows to show (newest first). */\n loadRows(): Promise<NotificationRow[]>;\n /** THE count — your server's, which may exclude muted kinds (F074's counting rule). */\n countUnseen(): Promise<number>;\n /** Returns the ids that actually transitioned to seen. */\n markSeen(ids: readonly string[]): Promise<string[]>;\n markAllSeen(): Promise<string[]>;\n /** Called with a callback that re-counts; return an unsubscribe. Optional. */\n subscribe?(onChange: () => void): () => void;\n}\n\n/**\n * The default source: a URL on your own backend, shaped after\n * @broberg/notifications' server core so `createNotifications` sits right\n * behind it.\n *\n * GET {url} → { items: NotificationRow[], unseen: number }\n * POST {url}/seen { ids: string[] } → { clearedIds: string[], count: number }\n * POST {url}/seen-all → { clearedIds: string[], count: number }\n *\n * It re-counts when the window regains focus or the tab becomes visible, and on\n * a timer (default 60 s). A failed GET THROWS — the bell shows «could not\n * load», never an empty list: \"could not fetch\" and \"nothing new\" must not look\n * the same.\n */\nexport function createFetchNotificationSource(opts: {\n url: string;\n fetch?: typeof fetch;\n /** Re-count interval in ms. 0 turns it off. Default 60 000. */\n pollMs?: number;\n credentials?: RequestCredentials;\n}): NotificationSource {\n const base = opts.url.replace(/\\/+$/, \"\");\n const doFetch = (input: string, init?: RequestInit) =>\n (opts.fetch ?? fetch)(input, { credentials: opts.credentials ?? \"include\", ...init });\n const get = async (): Promise<{ items: NotificationRow[]; unseen: number }> => {\n const res = await doFetch(base, { headers: { accept: \"application/json\" } });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const body = (await res.json().catch(() => null)) as { items?: unknown; unseen?: unknown } | null;\n if (!body || !Array.isArray(body.items)) throw new Error(\"response has no items array\");\n const items = body.items as NotificationRow[];\n const unseen = typeof body.unseen === \"number\" ? body.unseen : items.filter((n) => n.seenAt == null).length;\n return { items, unseen };\n };\n const post = async (path: string, body?: unknown): Promise<string[]> => {\n const res = await doFetch(`${base}${path}`, {\n method: \"POST\",\n headers: body === undefined ? undefined : { \"content-type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const r = (await res.json().catch(() => ({}))) as { clearedIds?: unknown };\n return Array.isArray(r.clearedIds) ? (r.clearedIds as string[]) : [];\n };\n return {\n loadRows: async () => (await get()).items,\n countUnseen: async () => (await get()).unseen,\n markSeen: (ids) => post(\"/seen\", { ids }),\n markAllSeen: () => post(\"/seen-all\"),\n subscribe(onChange) {\n if (typeof window === \"undefined\") return () => {};\n const onVisible = () => {\n if (document.visibilityState === \"visible\") onChange();\n };\n window.addEventListener(\"focus\", onChange);\n document.addEventListener(\"visibilitychange\", onVisible);\n const ms = opts.pollMs ?? 60_000;\n const timer = ms > 0 ? setInterval(onChange, ms) : null;\n return () => {\n window.removeEventListener(\"focus\", onChange);\n document.removeEventListener(\"visibilitychange\", onVisible);\n if (timer) clearInterval(timer);\n };\n },\n };\n}\n\n/** An in-memory source for demos and tests. */\nexport function createMemoryNotificationSource(initial: NotificationRow[] = []): NotificationSource & {\n push(n: NotificationRow): void;\n rows(): NotificationRow[];\n} {\n let rows = initial.map((n) => ({ ...n }));\n const listeners = new Set<() => void>();\n const emit = () => listeners.forEach((l) => l());\n const mark = (pred: (n: NotificationRow) => boolean): string[] => {\n const now = Date.now();\n const cleared: string[] = [];\n rows = rows.map((n) => {\n if (n.seenAt == null && pred(n)) {\n cleared.push(n.id);\n return { ...n, seenAt: now };\n }\n return n;\n });\n if (cleared.length) emit();\n return cleared;\n };\n return {\n loadRows: async () => rows.map((n) => ({ ...n })),\n countUnseen: async () => rows.filter((n) => n.seenAt == null).length,\n markSeen: async (ids) => mark((n) => ids.includes(n.id)),\n markAllSeen: async () => mark(() => true),\n subscribe(onChange) {\n listeners.add(onChange);\n return () => listeners.delete(onChange);\n },\n push(n) {\n rows = [n, ...rows];\n emit();\n },\n rows: () => rows.map((n) => ({ ...n })),\n };\n}\n\n/** \"nu\", \"5 min\", \"3 t\", \"i går\", \"4 d\" (da) or the English equivalents. */\nexport function relativeTime(ts: string | number, lang: \"da\" | \"en\", now = Date.now()): string {\n const ms = typeof ts === \"number\" ? ts : Date.parse(ts);\n if (!Number.isFinite(ms)) return \"\";\n const m = Math.floor((now - ms) / 60_000);\n if (m < 1) return lang === \"da\" ? \"nu\" : \"now\";\n if (m < 60) return `${m} min`;\n const h = Math.floor(m / 60);\n if (h < 24) return lang === \"da\" ? `${h} t` : `${h} h`;\n const d = Math.floor(h / 24);\n if (d === 1) return lang === \"da\" ? \"i går\" : \"yesterday\";\n return `${d} d`;\n}\n\n// ── Account page (F095.3) ─────────────────────────────────────────────────\n//\n// The user edits their own name and picture inside the app. The app's backend\n// (@broberg/sso's `accountRoutes()`, mounted on /api/account) talks to Broberg\n// ID; this package only talks to that backend, through an adapter, and never\n// imports @broberg/sso.\n\n/** What every account call resolves to — the profile AS THE SERVER NOW HOLDS IT. */\nexport interface AccountProfile {\n sub: string;\n name: string | null;\n picture: string | null;\n email: string | null;\n /** Broberg ID's own account page (password, passkeys, sessions). */\n account_url: string;\n}\n\n/** Where the account page gets and saves the profile. Swap it to point anywhere. */\nexport interface AccountAdapter {\n load(): Promise<AccountProfile>;\n saveName(name: string): Promise<AccountProfile>;\n uploadAvatar(file: Blob): Promise<AccountProfile>;\n removeAvatar(): Promise<AccountProfile>;\n}\n\n/** A failed account call: the HTTP status and the server's `error` code, if any. */\nexport class AccountError extends Error {\n constructor(\n readonly status: number,\n readonly code?: string,\n ) {\n super(code ? `HTTP ${status} ${code}` : `HTTP ${status}`);\n this.name = \"AccountError\";\n }\n}\n\nexport const AVATAR_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\"] as const;\nexport const AVATAR_MAX_BYTES = 2 * 1024 * 1024;\n\n/** Checked BEFORE uploading, so a file the server will refuse is never sent. */\nexport function checkAvatarFile(file: { size: number; type: string }): \"tooLarge\" | \"wrongType\" | null {\n if (!(AVATAR_TYPES as readonly string[]).includes(file.type)) return \"wrongType\";\n if (file.size > AVATAR_MAX_BYTES) return \"tooLarge\";\n return null;\n}\n\n/**\n * What a failed call means for the page. `reauth` = the user's session or its\n * scope does not allow the change (401, or 403 insufficient_scope): signing in\n * again is the fix, so the page says that instead of «try again».\n */\nexport function accountErrorKind(err: unknown): \"reauth\" | \"tooLarge\" | \"wrongType\" | \"other\" {\n if (!(err instanceof AccountError)) return \"other\";\n if (err.status === 401) return \"reauth\";\n if (err.status === 403 && err.code === \"insufficient_scope\") return \"reauth\";\n if (err.status === 413) return \"tooLarge\";\n if (err.status === 415) return \"wrongType\";\n return \"other\";\n}\n\n/**\n * The default adapter: the app's own backend, shaped after @broberg/sso's\n * `accountRoutes()`.\n *\n * GET {url}/profile → AccountProfile\n * POST {url}/profile {name} → AccountProfile\n * POST {url}/profile/avatar (raw bytes, Content-Type = the image's) → AccountProfile\n * POST {url}/profile/avatar/remove → AccountProfile\n *\n * A non-2xx THROWS an AccountError — a failed save must never look like a\n * saved one.\n */\nexport function createFetchAccountAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): AccountAdapter {\n const base = (opts.url ?? \"/api/account\").replace(/\\/+$/, \"\");\n const call = async (path: string, init?: RequestInit): Promise<AccountProfile> => {\n const res = await (opts.fetch ?? fetch)(`${base}${path}`, {\n credentials: opts.credentials ?? \"same-origin\",\n ...init,\n headers: { accept: \"application/json\", ...(init?.headers as Record<string, string> | undefined) },\n });\n const body = (await res.json().catch(() => null)) as Record<string, unknown> | null;\n if (!res.ok) throw new AccountError(res.status, typeof body?.error === \"string\" ? body.error : undefined);\n if (!body || typeof body.sub !== \"string\") throw new AccountError(res.status, \"bad_response\");\n return body as unknown as AccountProfile;\n };\n return {\n load: () => call(\"/profile\"),\n saveName: (name) =>\n call(\"/profile\", { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: JSON.stringify({ name }) }),\n uploadAvatar: (file) =>\n call(\"/profile/avatar\", { method: \"POST\", headers: { \"content-type\": file.type }, body: file }),\n removeAvatar: () => call(\"/profile/avatar/remove\", { method: \"POST\" }),\n };\n}\n\n// ── Analytics page (F097) ─────────────────────────────────────────────────\n//\n// The app's OWN visitor numbers from Upmetrics. The app's backend mounts\n// `analyticsRoutes()` from `@upmetrics/sdk/hono` (the uk_ key stays on the\n// server); this package only talks to that backend, through an adapter, and has\n// no dependency on the SDK — the type below is structural, so Upmetrics'\n// `AnalyticsResponse` fits it as is.\n\nexport interface AnalyticsDay {\n /** YYYY-MM-DD on the Europe/Copenhagen calendar (Upmetrics' own day key). */\n day: string;\n visitors: number;\n pageviews: number;\n}\n\nexport interface AnalyticsData {\n totals: { visitors: number; pageviews: number };\n /** The same-length period just before this one. */\n prev: { visitors: number; pageviews: number };\n /** Only days that had visits — use fillDays() to draw the whole period. */\n series: AnalyticsDay[];\n topPages: { value: string; count: number }[];\n}\n\nexport interface AnalyticsAdapter {\n load(days: number): Promise<AnalyticsData>;\n}\n\n/** A failed analytics call: HTTP status (0 = no answer) and the server's `error` code. */\nexport class AnalyticsError extends Error {\n constructor(\n readonly status: number,\n readonly code?: string,\n ) {\n super(code ? `HTTP ${status} ${code}` : `HTTP ${status}`);\n this.name = \"AnalyticsError\";\n }\n}\n\n/** \"unconfigured\" only for the backend's explicit 503 analytics_unconfigured; anything else is \"unavailable\". */\nexport function analyticsErrorKind(err: unknown): \"unconfigured\" | \"unavailable\" {\n return err instanceof AnalyticsError && err.status === 503 && err.code === \"analytics_unconfigured\" ? \"unconfigured\" : \"unavailable\";\n}\n\n/**\n * The default adapter: `GET {url}?days=N` on the app's own backend (default\n * /api/analytics, where `@upmetrics/sdk/hono`'s analyticsRoutes() answers).\n * A non-2xx, an unreadable body or no answer at all THROWS — never zeros that\n * look like a real, quiet month.\n */\nexport function createFetchAnalyticsAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): AnalyticsAdapter {\n const url = opts.url ?? \"/api/analytics\";\n return {\n async load(days) {\n let res: Response;\n try {\n res = await (opts.fetch ?? fetch)(`${url}${url.includes(\"?\") ? \"&\" : \"?\"}days=${encodeURIComponent(String(days))}`, {\n credentials: opts.credentials ?? \"same-origin\",\n headers: { accept: \"application/json\" },\n });\n } catch {\n throw new AnalyticsError(0);\n }\n const body = (await res.json().catch(() => null)) as Record<string, unknown> | null;\n if (!res.ok) throw new AnalyticsError(res.status, typeof body?.error === \"string\" ? body.error : undefined);\n const ok =\n body !== null &&\n typeof (body.totals as Record<string, unknown> | undefined)?.visitors === \"number\" &&\n Array.isArray(body.series) &&\n Array.isArray(body.topPages);\n if (!ok) throw new AnalyticsError(res.status, \"bad_response\");\n return body as unknown as AnalyticsData;\n },\n };\n}\n\n/** Today's YYYY-MM-DD on the Copenhagen calendar — the calendar Upmetrics buckets on. */\nexport function copenhagenDay(at: Date = new Date()): string {\n return new Intl.DateTimeFormat(\"en-CA\", { timeZone: \"Europe/Copenhagen\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).format(at);\n}\n\n/**\n * Exactly `days` entries ending today (Copenhagen calendar), with Upmetrics'\n * values where it sent a day and zeros where it did not — Upmetrics only sends\n * days that had visits. Calendar arithmetic, so a DST change never skips or\n * repeats a day.\n */\nexport function fillDays(series: AnalyticsDay[], days: number, now: Date = new Date()): AnalyticsDay[] {\n const byDay = new Map(series.map((d) => [d.day, d]));\n const [y, m, d] = copenhagenDay(now).split(\"-\").map(Number) as [number, number, number];\n return Array.from({ length: days }, (_, i) => {\n const day = new Date(Date.UTC(y, m - 1, d - (days - 1 - i))).toISOString().slice(0, 10);\n return byDay.get(day) ?? { day, visitors: 0, pageviews: 0 };\n });\n}\n\n/** Whole-percent change against the previous period, or null when there is nothing to compare with. */\nexport function changePct(now: number, before: number): number | null {\n return before === 0 ? null : Math.round(((now - before) / before) * 100);\n}\n\n// ── Tenant switcher (F029.10) ──────────────────────────────────────────────\n//\n// The user's own organisations, from the app's backend (@broberg/tenant/hono's\n// tenantRoutes: GET /memberships). This package never imports @broberg/tenant;\n// the row type is structural.\n\nexport interface TenantMembershipRow {\n tenant: { id: string; slug: string; name: string; status?: \"active\" | \"suspended\" };\n role: string;\n}\n\nexport interface TenantAdapter {\n load(): Promise<TenantMembershipRow[]>;\n}\n\nexport class TenantLoadError extends Error {\n constructor(readonly status: number) {\n super(`HTTP ${status}`);\n this.name = \"TenantLoadError\";\n }\n}\n\n/** GET {url} (default /api/me/memberships) → the user's memberships. Non-2xx or a bad body THROWS. */\nexport function createFetchTenantAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): TenantAdapter {\n return {\n async load() {\n let res: Response;\n try {\n res = await (opts.fetch ?? fetch)(opts.url ?? \"/api/me/memberships\", { credentials: opts.credentials ?? \"same-origin\", headers: { accept: \"application/json\" } });\n } catch {\n throw new TenantLoadError(0);\n }\n const body = (await res.json().catch(() => null)) as unknown;\n if (!res.ok) throw new TenantLoadError(res.status);\n if (!Array.isArray(body) || !body.every((r) => r && typeof r.role === \"string\" && r.tenant && typeof r.tenant.slug === \"string\")) {\n throw new TenantLoadError(res.status);\n }\n return body as TenantMembershipRow[];\n },\n };\n}\n\n// ── Text ───────────────────────────────────────────────────────────────────\n\nexport const TEXT = {\n da: {\n openMenu: \"Åbn menu\",\n closeMenu: \"Luk menu\",\n home: \"Forside\",\n working: \"Arbejder\",\n moreFor: \"Mere for\",\n back: \"Tilbage\",\n pinned: \"Fastgjort\",\n language: \"Sprog\",\n zoom: \"Zoom\",\n zoomOut: \"Zoom ud\",\n zoomIn: \"Zoom ind\",\n couldNotMarkAll: \"Kunne ikke markere alle som læst. Prøv igen.\",\n collapse: \"Skjul sidemenu\",\n expand: \"Vis sidemenu\",\n notifications: \"Notifikationer\",\n markAllRead: \"Markér alle læst\",\n noNotifications: \"Ingen notifikationer endnu.\",\n couldNotLoad: \"Kunne ikke hente notifikationer.\",\n seeAll: \"Vis alle\",\n close: \"Luk\",\n account: \"Konto\",\n signOut: \"Log ud\",\n theme: \"Tema\",\n light: \"Lys\",\n dark: \"Mørk\",\n system: \"System\",\n palette: \"Palet\",\n surfaces: \"Flader\",\n flat: \"Flade\",\n layered: \"Lag\",\n backdrop: \"Baggrund\",\n neurons: \"Neuroner\",\n plain: \"Ren\",\n name: \"Navn\",\n email: \"E-mail\",\n save: \"Gem\",\n saving: \"Gemmer…\",\n saved: \"Gemt\",\n loadingProfile: \"Henter profil…\",\n couldNotLoadProfile: \"Kunne ikke hente din profil.\",\n couldNotSave: \"Kunne ikke gemme. Prøv igen.\",\n reauth: \"Log ind igen for at rette\",\n signInAgain: \"Log ind igen\",\n changePicture: \"Skift billede\",\n removePicture: \"Fjern billede\",\n confirmRemovePicture: \"Fjern dit billede?\",\n remove: \"Fjern\",\n cancel: \"Annuller\",\n pictureSaved: \"Billedet er gemt\",\n pictureRemoved: \"Billedet er fjernet\",\n pictureTooLarge: \"Billedet er for stort — højst 2 MB.\",\n pictureWrongType: \"Vælg et PNG-, JPEG- eller WebP-billede.\",\n bidSecurity: \"Sikkerhed i Broberg ID\",\n analyticsPeriod: \"Periode\",\n analyticsDays: \"{n} dage\",\n analyticsVisitors: \"Besøgende\",\n analyticsPageviews: \"Sidevisninger\",\n analyticsVsPrev: \"{pct} % mod forrige periode\",\n analyticsNoPrev: \"Ingen sammenligning\",\n analyticsOverTime: \"Sidevisninger pr. dag\",\n analyticsTopPages: \"Mest besøgte sider\",\n analyticsDay: \"Dag\",\n analyticsPage: \"Side\",\n analyticsEmpty: \"Ingen besøg i perioden.\",\n analyticsLoading: \"Henter tal…\",\n analyticsUnconfigured: \"Analytics er ikke sat op for denne app.\",\n analyticsUnavailable: \"Upmetrics svarer ikke lige nu. Prøv igen om lidt.\",\n tenantSwitch: \"Skift organisation\",\n tenantLoadFailed: \"Kunne ikke hente dine organisationer.\",\n tenantSwitchFailed: \"Kunne ikke skifte organisation. Du er stadig i {name}.\",\n tenantRetry: \"Prøv igen\",\n tenantSuspended: \"lukket\",\n tenantActive: \"aktiv\",\n },\n en: {\n openMenu: \"Open menu\",\n closeMenu: \"Close menu\",\n home: \"Home\",\n working: \"Working\",\n moreFor: \"More for\",\n back: \"Back\",\n pinned: \"Pinned\",\n language: \"Language\",\n zoom: \"Zoom\",\n zoomOut: \"Zoom out\",\n zoomIn: \"Zoom in\",\n couldNotMarkAll: \"Could not mark all as read. Try again.\",\n collapse: \"Hide sidebar\",\n expand: \"Show sidebar\",\n notifications: \"Notifications\",\n markAllRead: \"Mark all read\",\n noNotifications: \"No notifications yet.\",\n couldNotLoad: \"Could not load notifications.\",\n seeAll: \"See all\",\n close: \"Close\",\n account: \"Account\",\n signOut: \"Sign out\",\n theme: \"Theme\",\n light: \"Light\",\n dark: \"Dark\",\n system: \"System\",\n palette: \"Palette\",\n surfaces: \"Surfaces\",\n flat: \"Flat\",\n layered: \"Layered\",\n backdrop: \"Backdrop\",\n neurons: \"Neurons\",\n plain: \"Plain\",\n name: \"Name\",\n email: \"Email\",\n save: \"Save\",\n saving: \"Saving…\",\n saved: \"Saved\",\n loadingProfile: \"Loading profile…\",\n couldNotLoadProfile: \"Could not load your profile.\",\n couldNotSave: \"Could not save. Try again.\",\n reauth: \"Sign in again to make changes\",\n signInAgain: \"Sign in again\",\n changePicture: \"Change picture\",\n removePicture: \"Remove picture\",\n confirmRemovePicture: \"Remove your picture?\",\n remove: \"Remove\",\n cancel: \"Cancel\",\n pictureSaved: \"Picture saved\",\n pictureRemoved: \"Picture removed\",\n pictureTooLarge: \"The picture is too large — 2 MB at most.\",\n pictureWrongType: \"Choose a PNG, JPEG or WebP image.\",\n bidSecurity: \"Security in Broberg ID\",\n analyticsPeriod: \"Period\",\n analyticsDays: \"{n} days\",\n analyticsVisitors: \"Visitors\",\n analyticsPageviews: \"Page views\",\n analyticsVsPrev: \"{pct}% vs previous period\",\n analyticsNoPrev: \"No comparison\",\n analyticsOverTime: \"Page views per day\",\n analyticsTopPages: \"Most visited pages\",\n analyticsDay: \"Day\",\n analyticsPage: \"Page\",\n analyticsEmpty: \"No visits in this period.\",\n analyticsLoading: \"Loading figures…\",\n analyticsUnconfigured: \"Analytics is not set up for this app.\",\n analyticsUnavailable: \"Upmetrics is not answering right now. Try again shortly.\",\n tenantSwitch: \"Switch organisation\",\n tenantLoadFailed: \"Could not load your organisations.\",\n tenantSwitchFailed: \"Could not switch organisation. You are still in {name}.\",\n tenantRetry: \"Try again\",\n tenantSuspended: \"suspended\",\n tenantActive: \"active\",\n },\n} as const;\nexport type Lang = keyof typeof TEXT;\n"]}
1
+ {"version":3,"sources":["../src/index.ts"],"names":["d"],"mappings":"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","file":"index.js","sourcesContent":["// @broberg/app-shell — the framework-free core of the fleet's app shell (F092).\n//\n// The shell every app in the fleet had been copying: sidebar + top bar +\n// notification bell + user menu. cardmem and trail each had one (\"ported from\n// trail\"), and helpdesk and the appkit starter would have made the third and\n// fourth. This file holds what does not depend on a framework: the navigation\n// data model, the notification contract and its sources, and the small rules\n// both the Preact and React parts must agree on.\n//\n// Christian, 4 Oct 2026: notifications are a STANDARD part of the template, not\n// an optional slot — so the contract and a default source live here, and a new\n// app gets a working notification centre by pointing one URL at its backend.\n\n// ── Navigation ─────────────────────────────────────────────────────────────\n\n/** One sidebar entry. `icon` is whatever your framework renders (an element). */\nexport interface NavItem<Icon = unknown> {\n id: string;\n label: string;\n href: string;\n icon?: Icon;\n /** A count shown on the item (99+ above 99). 0/undefined shows nothing. */\n badge?: number;\n /**\n * Sub-items, ONE level (F092.5). An item with children is a toggle that opens\n * and closes them, not a link; its own `href` is used only to decide whether\n * it is on the current path.\n */\n children?: NavItem<Icon>[];\n /** F092.20 — a status marker before the label; \"working\" = a run in progress. */\n marker?: \"working\";\n /** F092.20 — shows a pin icon after the label. */\n pinned?: boolean;\n /** F092.21 — row actions: a «…» button opens a menu with these. */\n actions?: NavAction[];\n}\n/**\n * F092.21 — one row action. `submenu` switches the menu to a sub-list (with a\n * back arrow); `input` switches it to a field + save (e.g. rename). A plain\n * action, a submenu leaf and a saved input all call `onItemAction`.\n */\nexport interface NavAction {\n id: string;\n label: string;\n icon?: unknown;\n /** A single letter that picks this action while the menu is open (e.g. \"P\"). */\n kbd?: string;\n /** Shown in the danger colour (delete). */\n danger?: boolean;\n submenu?: NavAction[];\n input?: { value: string; placeholder?: string; saveLabel?: string };\n /** F092.22 — ask twice: the first pick shows `label` («Tryk igen for at slette») and keeps the menu open; the second runs it. */\n confirm?: { label: string };\n}\nexport interface NavSection<Icon = unknown> {\n /** A sub-heading inside one group, e.g. \"I dag\" / \"I går\". */\n label: string;\n items: NavItem<Icon>[];\n}\nexport interface NavGroup<Icon = unknown> {\n label: string;\n items: NavItem<Icon>[];\n /** F092.20 — \"plain\": the group label in normal text (not uppercase/mono). */\n variant?: \"default\" | \"plain\";\n /** F092.20 — sub-headed lists inside this group, shown after `items`. */\n sections?: NavSection<Icon>[];\n}\n\n/**\n * Is this item the current page? Exact match for \"/\", otherwise the path is the\n * item or below it (\"/board/123\" is on \"/board\", \"/boardroom\" is not).\n */\nexport function isActivePath(href: string, currentPath: string): boolean {\n const path = (currentPath.split(/[?#]/)[0] || \"/\").replace(/\\/+$/, \"\") || \"/\";\n const target = (href.split(/[?#]/)[0] || \"/\").replace(/\\/+$/, \"\") || \"/\";\n if (target === \"/\") return path === \"/\";\n return path === target || path.startsWith(target + \"/\");\n}\n\n/**\n * A notification's `navigate` is SERVER DATA, and it ends in `location.assign`.\n * `javascript:` there runs code in the app, so only a same-site path or an\n * http(s) URL is followed; anything else returns null and the row just closes.\n */\nexport function safeNavigate(target: string | null | undefined): string | null {\n if (typeof target !== \"string\") return null;\n const t = target.trim();\n if (t.startsWith(\"/\") && !t.startsWith(\"//\") && !t.startsWith(\"/\\\\\")) return t;\n try {\n const u = new URL(t);\n return u.protocol === \"https:\" || u.protocol === \"http:\" ? u.href : null;\n } catch {\n return null;\n }\n}\n\n/**\n * F092.10 — is this link leaving the app? An http(s) link to another origin.\n * Paths, \"#…\", mailto:/tel: and the app's own origin are not. Owner decision\n * D-d28547: every external link opens in a new tab.\n */\nexport function isExternal(\n href: string | null | undefined,\n origin: string | undefined = typeof location !== \"undefined\" ? location.origin : undefined,\n): boolean {\n if (typeof href !== \"string\") return false;\n const h = href.trim();\n if (!origin) return /^(https?:)?\\/\\//i.test(h); // no page to compare with: absolute = external\n let u: URL;\n try {\n u = new URL(h, origin);\n } catch {\n return false;\n }\n return (u.protocol === \"http:\" || u.protocol === \"https:\") && u.origin !== origin;\n}\n\n/**\n * The label of the nav item the user is on, for the content header (F092.4).\n * Sub-items count, and the most specific match wins: on \"/models/genesis\" a\n * \"Genesis\" child beats its \"Models\" parent.\n */\nexport function activeNavLabel<Icon>(\n groups: NavGroup<Icon>[],\n footer: NavItem<Icon>[] | undefined,\n currentPath: string,\n): string | undefined {\n let best: { label: string; len: number } | undefined;\n const visit = (it: NavItem<Icon>) => {\n if (it.href && isActivePath(it.href, currentPath)) {\n const len = it.href.length;\n if (!best || len > best.len) best = { label: it.label, len };\n }\n it.children?.forEach(visit);\n };\n for (const g of groups) g.items.forEach(visit);\n footer?.forEach(visit);\n return best?.label;\n}\n\n/** \"99+\" above 99, nothing for 0 or less. */\nexport function badgeLabel(n: number | undefined): string | null {\n if (!n || n <= 0) return null;\n return n > 99 ? \"99+\" : String(n);\n}\n\n// ── Persisted UI state ─────────────────────────────────────────────────────\n\n/** Read a boolean flag from localStorage; storage errors read as `fallback`. */\nexport function readFlag(key: string, fallback: boolean): boolean {\n try {\n const v = typeof localStorage === \"undefined\" ? null : localStorage.getItem(key);\n return v === null ? fallback : v === \"1\";\n } catch {\n return fallback;\n }\n}\nexport function writeFlag(key: string, value: boolean): void {\n try {\n if (typeof localStorage !== \"undefined\") localStorage.setItem(key, value ? \"1\" : \"0\");\n } catch {\n /* private mode / quota: the UI still works, it just does not remember */\n }\n}\nexport function readSet(key: string): Set<string> {\n try {\n const raw = typeof localStorage === \"undefined\" ? null : localStorage.getItem(key);\n const v = raw ? (JSON.parse(raw) as unknown) : [];\n return new Set(Array.isArray(v) ? v.filter((x): x is string => typeof x === \"string\") : []);\n } catch {\n return new Set();\n }\n}\nexport function writeSet(key: string, value: Set<string>): void {\n try {\n if (typeof localStorage !== \"undefined\") localStorage.setItem(key, JSON.stringify([...value]));\n } catch {\n /* see writeFlag */\n }\n}\n\n// ── Avatar ─────────────────────────────────────────────────────────────────\n\n/**\n * Two initials for the avatar fallback: first letter of the first and the last\n * word, upper-cased. One word gives one letter; nothing gives \"?\".\n *\n * There is deliberately no Gravatar here (owner decision D-1dc849): the avatar\n * is the Broberg ID `picture`, and without one, initials. Gravatar would send a\n * hash of the user's email to a US service on every render.\n */\nexport function initials(name: string | undefined, email?: string): string {\n const source = (name ?? \"\").trim() || (email ?? \"\").split(\"@\")[0] || \"\";\n const words = source.split(/[\\s._-]+/).filter(Boolean);\n if (words.length === 0) return \"?\";\n const first = words[0]![0] ?? \"\";\n const last = words.length > 1 ? words[words.length - 1]![0] ?? \"\" : \"\";\n return (first + last).toUpperCase() || \"?\";\n}\n\n// ── Notifications ──────────────────────────────────────────────────────────\n//\n// REUSE, NOT A SECOND CONTRACT. The fleet already agreed the notification shape\n// in @broberg/notifications (F074): `NotificationRow` — the same field names\n// @broberg/webpush sends, end to end — and the server core that owns the ONE\n// counting rule. The shell's bell renders that row and drives its panel with\n// that package's `createBellShell`, so a server built on `createNotifications`\n// and this bell speak the same language without an adapter.\n//\n// Christian, 4 Oct 2026: notifications are a STANDARD part of the template. So\n// a source, and a default one, ship here: point one URL at your backend.\n\nimport type { NotificationRow } from \"@broberg/notifications/types\";\nexport type { NotificationRow } from \"@broberg/notifications/types\";\n\n/** Where the bell gets its data. Swap it to point at any backend. */\nexport interface NotificationSource {\n /** The rows to show (newest first). */\n loadRows(): Promise<NotificationRow[]>;\n /** THE count — your server's, which may exclude muted kinds (F074's counting rule). */\n countUnseen(): Promise<number>;\n /** Returns the ids that actually transitioned to seen. */\n markSeen(ids: readonly string[]): Promise<string[]>;\n markAllSeen(): Promise<string[]>;\n /** Called with a callback that re-counts; return an unsubscribe. Optional. */\n subscribe?(onChange: () => void): () => void;\n}\n\n/**\n * The default source: a URL on your own backend, shaped after\n * @broberg/notifications' server core so `createNotifications` sits right\n * behind it.\n *\n * GET {url} → { items: NotificationRow[], unseen: number }\n * POST {url}/seen { ids: string[] } → { clearedIds: string[], count: number }\n * POST {url}/seen-all → { clearedIds: string[], count: number }\n *\n * It re-counts when the window regains focus or the tab becomes visible, and on\n * a timer (default 60 s). A failed GET THROWS — the bell shows «could not\n * load», never an empty list: \"could not fetch\" and \"nothing new\" must not look\n * the same.\n */\nexport function createFetchNotificationSource(opts: {\n url: string;\n fetch?: typeof fetch;\n /** Re-count interval in ms. 0 turns it off. Default 60 000. */\n pollMs?: number;\n credentials?: RequestCredentials;\n}): NotificationSource {\n const base = opts.url.replace(/\\/+$/, \"\");\n const doFetch = (input: string, init?: RequestInit) =>\n (opts.fetch ?? fetch)(input, { credentials: opts.credentials ?? \"include\", ...init });\n const get = async (): Promise<{ items: NotificationRow[]; unseen: number }> => {\n const res = await doFetch(base, { headers: { accept: \"application/json\" } });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const body = (await res.json().catch(() => null)) as { items?: unknown; unseen?: unknown } | null;\n if (!body || !Array.isArray(body.items)) throw new Error(\"response has no items array\");\n const items = body.items as NotificationRow[];\n const unseen = typeof body.unseen === \"number\" ? body.unseen : items.filter((n) => n.seenAt == null).length;\n return { items, unseen };\n };\n const post = async (path: string, body?: unknown): Promise<string[]> => {\n const res = await doFetch(`${base}${path}`, {\n method: \"POST\",\n headers: body === undefined ? undefined : { \"content-type\": \"application/json\" },\n body: body === undefined ? undefined : JSON.stringify(body),\n });\n if (!res.ok) throw new Error(`HTTP ${res.status}`);\n const r = (await res.json().catch(() => ({}))) as { clearedIds?: unknown };\n return Array.isArray(r.clearedIds) ? (r.clearedIds as string[]) : [];\n };\n return {\n loadRows: async () => (await get()).items,\n countUnseen: async () => (await get()).unseen,\n markSeen: (ids) => post(\"/seen\", { ids }),\n markAllSeen: () => post(\"/seen-all\"),\n subscribe(onChange) {\n if (typeof window === \"undefined\") return () => {};\n const onVisible = () => {\n if (document.visibilityState === \"visible\") onChange();\n };\n window.addEventListener(\"focus\", onChange);\n document.addEventListener(\"visibilitychange\", onVisible);\n const ms = opts.pollMs ?? 60_000;\n const timer = ms > 0 ? setInterval(onChange, ms) : null;\n return () => {\n window.removeEventListener(\"focus\", onChange);\n document.removeEventListener(\"visibilitychange\", onVisible);\n if (timer) clearInterval(timer);\n };\n },\n };\n}\n\n/** An in-memory source for demos and tests. */\nexport function createMemoryNotificationSource(initial: NotificationRow[] = []): NotificationSource & {\n push(n: NotificationRow): void;\n rows(): NotificationRow[];\n} {\n let rows = initial.map((n) => ({ ...n }));\n const listeners = new Set<() => void>();\n const emit = () => listeners.forEach((l) => l());\n const mark = (pred: (n: NotificationRow) => boolean): string[] => {\n const now = Date.now();\n const cleared: string[] = [];\n rows = rows.map((n) => {\n if (n.seenAt == null && pred(n)) {\n cleared.push(n.id);\n return { ...n, seenAt: now };\n }\n return n;\n });\n if (cleared.length) emit();\n return cleared;\n };\n return {\n loadRows: async () => rows.map((n) => ({ ...n })),\n countUnseen: async () => rows.filter((n) => n.seenAt == null).length,\n markSeen: async (ids) => mark((n) => ids.includes(n.id)),\n markAllSeen: async () => mark(() => true),\n subscribe(onChange) {\n listeners.add(onChange);\n return () => listeners.delete(onChange);\n },\n push(n) {\n rows = [n, ...rows];\n emit();\n },\n rows: () => rows.map((n) => ({ ...n })),\n };\n}\n\n/** \"nu\", \"5 min\", \"3 t\", \"i går\", \"4 d\" (da) or the English equivalents. */\nexport function relativeTime(ts: string | number, lang: \"da\" | \"en\", now = Date.now()): string {\n const ms = typeof ts === \"number\" ? ts : Date.parse(ts);\n if (!Number.isFinite(ms)) return \"\";\n const m = Math.floor((now - ms) / 60_000);\n if (m < 1) return lang === \"da\" ? \"nu\" : \"now\";\n if (m < 60) return `${m} min`;\n const h = Math.floor(m / 60);\n if (h < 24) return lang === \"da\" ? `${h} t` : `${h} h`;\n const d = Math.floor(h / 24);\n if (d === 1) return lang === \"da\" ? \"i går\" : \"yesterday\";\n return `${d} d`;\n}\n\n// ── Account page (F095.3) ─────────────────────────────────────────────────\n//\n// The user edits their own name and picture inside the app. The app's backend\n// (@broberg/sso's `accountRoutes()`, mounted on /api/account) talks to Broberg\n// ID; this package only talks to that backend, through an adapter, and never\n// imports @broberg/sso.\n\n/** What every account call resolves to — the profile AS THE SERVER NOW HOLDS IT. */\nexport interface AccountProfile {\n sub: string;\n name: string | null;\n picture: string | null;\n email: string | null;\n /** Broberg ID's own account page (password, passkeys, sessions). */\n account_url: string;\n}\n\n/** Where the account page gets and saves the profile. Swap it to point anywhere. */\nexport interface AccountAdapter {\n load(): Promise<AccountProfile>;\n saveName(name: string): Promise<AccountProfile>;\n uploadAvatar(file: Blob): Promise<AccountProfile>;\n removeAvatar(): Promise<AccountProfile>;\n}\n\n/** A failed account call: the HTTP status and the server's `error` code, if any. */\nexport class AccountError extends Error {\n constructor(\n readonly status: number,\n readonly code?: string,\n ) {\n super(code ? `HTTP ${status} ${code}` : `HTTP ${status}`);\n this.name = \"AccountError\";\n }\n}\n\nexport const AVATAR_TYPES = [\"image/png\", \"image/jpeg\", \"image/webp\"] as const;\nexport const AVATAR_MAX_BYTES = 2 * 1024 * 1024;\n\n/** Checked BEFORE uploading, so a file the server will refuse is never sent. */\nexport function checkAvatarFile(file: { size: number; type: string }): \"tooLarge\" | \"wrongType\" | null {\n if (!(AVATAR_TYPES as readonly string[]).includes(file.type)) return \"wrongType\";\n if (file.size > AVATAR_MAX_BYTES) return \"tooLarge\";\n return null;\n}\n\n/**\n * What a failed call means for the page. `reauth` = the user's session or its\n * scope does not allow the change (401, or 403 insufficient_scope): signing in\n * again is the fix, so the page says that instead of «try again».\n */\nexport function accountErrorKind(err: unknown): \"reauth\" | \"tooLarge\" | \"wrongType\" | \"other\" {\n if (!(err instanceof AccountError)) return \"other\";\n if (err.status === 401) return \"reauth\";\n if (err.status === 403 && err.code === \"insufficient_scope\") return \"reauth\";\n if (err.status === 413) return \"tooLarge\";\n if (err.status === 415) return \"wrongType\";\n return \"other\";\n}\n\n/**\n * The default adapter: the app's own backend, shaped after @broberg/sso's\n * `accountRoutes()`.\n *\n * GET {url}/profile → AccountProfile\n * POST {url}/profile {name} → AccountProfile\n * POST {url}/profile/avatar (raw bytes, Content-Type = the image's) → AccountProfile\n * POST {url}/profile/avatar/remove → AccountProfile\n *\n * A non-2xx THROWS an AccountError — a failed save must never look like a\n * saved one.\n */\nexport function createFetchAccountAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): AccountAdapter {\n const base = (opts.url ?? \"/api/account\").replace(/\\/+$/, \"\");\n const call = async (path: string, init?: RequestInit): Promise<AccountProfile> => {\n const res = await (opts.fetch ?? fetch)(`${base}${path}`, {\n credentials: opts.credentials ?? \"same-origin\",\n ...init,\n headers: { accept: \"application/json\", ...(init?.headers as Record<string, string> | undefined) },\n });\n const body = (await res.json().catch(() => null)) as Record<string, unknown> | null;\n if (!res.ok) throw new AccountError(res.status, typeof body?.error === \"string\" ? body.error : undefined);\n if (!body || typeof body.sub !== \"string\") throw new AccountError(res.status, \"bad_response\");\n return body as unknown as AccountProfile;\n };\n return {\n load: () => call(\"/profile\"),\n saveName: (name) =>\n call(\"/profile\", { method: \"POST\", headers: { \"content-type\": \"application/json\" }, body: JSON.stringify({ name }) }),\n uploadAvatar: (file) =>\n call(\"/profile/avatar\", { method: \"POST\", headers: { \"content-type\": file.type }, body: file }),\n removeAvatar: () => call(\"/profile/avatar/remove\", { method: \"POST\" }),\n };\n}\n\n// ── Analytics page (F097) ─────────────────────────────────────────────────\n//\n// The app's OWN visitor numbers from Upmetrics. The app's backend mounts\n// `analyticsRoutes()` from `@upmetrics/sdk/hono` (the uk_ key stays on the\n// server); this package only talks to that backend, through an adapter, and has\n// no dependency on the SDK — the type below is structural, so Upmetrics'\n// `AnalyticsResponse` fits it as is.\n\nexport interface AnalyticsDay {\n /** YYYY-MM-DD on the Europe/Copenhagen calendar (Upmetrics' own day key). */\n day: string;\n visitors: number;\n pageviews: number;\n}\n\nexport interface AnalyticsData {\n totals: { visitors: number; pageviews: number };\n /** The same-length period just before this one. */\n prev: { visitors: number; pageviews: number };\n /** Only days that had visits — use fillDays() to draw the whole period. */\n series: AnalyticsDay[];\n topPages: { value: string; count: number }[];\n}\n\nexport interface AnalyticsAdapter {\n load(days: number): Promise<AnalyticsData>;\n}\n\n/** A failed analytics call: HTTP status (0 = no answer) and the server's `error` code. */\nexport class AnalyticsError extends Error {\n constructor(\n readonly status: number,\n readonly code?: string,\n ) {\n super(code ? `HTTP ${status} ${code}` : `HTTP ${status}`);\n this.name = \"AnalyticsError\";\n }\n}\n\n/** \"unconfigured\" only for the backend's explicit 503 analytics_unconfigured; anything else is \"unavailable\". */\nexport function analyticsErrorKind(err: unknown): \"unconfigured\" | \"unavailable\" {\n return err instanceof AnalyticsError && err.status === 503 && err.code === \"analytics_unconfigured\" ? \"unconfigured\" : \"unavailable\";\n}\n\n/**\n * The default adapter: `GET {url}?days=N` on the app's own backend (default\n * /api/analytics, where `@upmetrics/sdk/hono`'s analyticsRoutes() answers).\n * A non-2xx, an unreadable body or no answer at all THROWS — never zeros that\n * look like a real, quiet month.\n */\nexport function createFetchAnalyticsAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): AnalyticsAdapter {\n const url = opts.url ?? \"/api/analytics\";\n return {\n async load(days) {\n let res: Response;\n try {\n res = await (opts.fetch ?? fetch)(`${url}${url.includes(\"?\") ? \"&\" : \"?\"}days=${encodeURIComponent(String(days))}`, {\n credentials: opts.credentials ?? \"same-origin\",\n headers: { accept: \"application/json\" },\n });\n } catch {\n throw new AnalyticsError(0);\n }\n const body = (await res.json().catch(() => null)) as Record<string, unknown> | null;\n if (!res.ok) throw new AnalyticsError(res.status, typeof body?.error === \"string\" ? body.error : undefined);\n const ok =\n body !== null &&\n typeof (body.totals as Record<string, unknown> | undefined)?.visitors === \"number\" &&\n Array.isArray(body.series) &&\n Array.isArray(body.topPages);\n if (!ok) throw new AnalyticsError(res.status, \"bad_response\");\n return body as unknown as AnalyticsData;\n },\n };\n}\n\n/** Today's YYYY-MM-DD on the Copenhagen calendar — the calendar Upmetrics buckets on. */\nexport function copenhagenDay(at: Date = new Date()): string {\n return new Intl.DateTimeFormat(\"en-CA\", { timeZone: \"Europe/Copenhagen\", year: \"numeric\", month: \"2-digit\", day: \"2-digit\" }).format(at);\n}\n\n/**\n * Exactly `days` entries ending today (Copenhagen calendar), with Upmetrics'\n * values where it sent a day and zeros where it did not — Upmetrics only sends\n * days that had visits. Calendar arithmetic, so a DST change never skips or\n * repeats a day.\n */\nexport function fillDays(series: AnalyticsDay[], days: number, now: Date = new Date()): AnalyticsDay[] {\n const byDay = new Map(series.map((d) => [d.day, d]));\n const [y, m, d] = copenhagenDay(now).split(\"-\").map(Number) as [number, number, number];\n return Array.from({ length: days }, (_, i) => {\n const day = new Date(Date.UTC(y, m - 1, d - (days - 1 - i))).toISOString().slice(0, 10);\n return byDay.get(day) ?? { day, visitors: 0, pageviews: 0 };\n });\n}\n\n/** Whole-percent change against the previous period, or null when there is nothing to compare with. */\nexport function changePct(now: number, before: number): number | null {\n return before === 0 ? null : Math.round(((now - before) / before) * 100);\n}\n\n// ── Tenant switcher (F029.10) ──────────────────────────────────────────────\n//\n// The user's own organisations, from the app's backend (@broberg/tenant/hono's\n// tenantRoutes: GET /memberships). This package never imports @broberg/tenant;\n// the row type is structural.\n\nexport interface TenantMembershipRow {\n tenant: { id: string; slug: string; name: string; status?: \"active\" | \"suspended\" };\n role: string;\n}\n\nexport interface TenantAdapter {\n load(): Promise<TenantMembershipRow[]>;\n}\n\nexport class TenantLoadError extends Error {\n constructor(readonly status: number) {\n super(`HTTP ${status}`);\n this.name = \"TenantLoadError\";\n }\n}\n\n/** GET {url} (default /api/me/memberships) → the user's memberships. Non-2xx or a bad body THROWS. */\nexport function createFetchTenantAdapter(opts: { url?: string; fetch?: typeof fetch; credentials?: RequestCredentials } = {}): TenantAdapter {\n return {\n async load() {\n let res: Response;\n try {\n res = await (opts.fetch ?? fetch)(opts.url ?? \"/api/me/memberships\", { credentials: opts.credentials ?? \"same-origin\", headers: { accept: \"application/json\" } });\n } catch {\n throw new TenantLoadError(0);\n }\n const body = (await res.json().catch(() => null)) as unknown;\n if (!res.ok) throw new TenantLoadError(res.status);\n if (!Array.isArray(body) || !body.every((r) => r && typeof r.role === \"string\" && r.tenant && typeof r.tenant.slug === \"string\")) {\n throw new TenantLoadError(res.status);\n }\n return body as TenantMembershipRow[];\n },\n };\n}\n\n// ── Text ───────────────────────────────────────────────────────────────────\n\nexport const TEXT = {\n da: {\n openMenu: \"Åbn menu\",\n closeMenu: \"Luk menu\",\n home: \"Forside\",\n working: \"Arbejder\",\n moreFor: \"Mere for\",\n back: \"Tilbage\",\n pinned: \"Fastgjort\",\n language: \"Sprog\",\n zoom: \"Zoom\",\n zoomOut: \"Zoom ud\",\n zoomIn: \"Zoom ind\",\n couldNotMarkAll: \"Kunne ikke markere alle som læst. Prøv igen.\",\n collapse: \"Skjul sidemenu\",\n expand: \"Vis sidemenu\",\n notifications: \"Notifikationer\",\n markAllRead: \"Markér alle læst\",\n noNotifications: \"Ingen notifikationer endnu.\",\n couldNotLoad: \"Kunne ikke hente notifikationer.\",\n seeAll: \"Vis alle\",\n close: \"Luk\",\n account: \"Konto\",\n signOut: \"Log ud\",\n theme: \"Tema\",\n light: \"Lys\",\n dark: \"Mørk\",\n system: \"System\",\n palette: \"Palet\",\n surfaces: \"Flader\",\n flat: \"Flade\",\n layered: \"Lag\",\n backdrop: \"Baggrund\",\n neurons: \"Neuroner\",\n plain: \"Ren\",\n name: \"Navn\",\n email: \"E-mail\",\n save: \"Gem\",\n saving: \"Gemmer…\",\n saved: \"Gemt\",\n loadingProfile: \"Henter profil…\",\n couldNotLoadProfile: \"Kunne ikke hente din profil.\",\n couldNotSave: \"Kunne ikke gemme. Prøv igen.\",\n reauth: \"Log ind igen for at rette\",\n signInAgain: \"Log ind igen\",\n changePicture: \"Skift billede\",\n removePicture: \"Fjern billede\",\n confirmRemovePicture: \"Fjern dit billede?\",\n remove: \"Fjern\",\n cancel: \"Annuller\",\n pictureSaved: \"Billedet er gemt\",\n pictureRemoved: \"Billedet er fjernet\",\n pictureTooLarge: \"Billedet er for stort — højst 2 MB.\",\n pictureWrongType: \"Vælg et PNG-, JPEG- eller WebP-billede.\",\n bidSecurity: \"Sikkerhed i Broberg ID\",\n analyticsPeriod: \"Periode\",\n analyticsDays: \"{n} dage\",\n analyticsVisitors: \"Besøgende\",\n analyticsPageviews: \"Sidevisninger\",\n analyticsVsPrev: \"{pct} % mod forrige periode\",\n analyticsNoPrev: \"Ingen sammenligning\",\n analyticsOverTime: \"Sidevisninger pr. dag\",\n analyticsTopPages: \"Mest besøgte sider\",\n analyticsDay: \"Dag\",\n analyticsPage: \"Side\",\n analyticsEmpty: \"Ingen besøg i perioden.\",\n analyticsLoading: \"Henter tal…\",\n analyticsUnconfigured: \"Analytics er ikke sat op for denne app.\",\n analyticsUnavailable: \"Upmetrics svarer ikke lige nu. Prøv igen om lidt.\",\n tenantSwitch: \"Skift organisation\",\n tenantLoadFailed: \"Kunne ikke hente dine organisationer.\",\n tenantSwitchFailed: \"Kunne ikke skifte organisation. Du er stadig i {name}.\",\n tenantRetry: \"Prøv igen\",\n tenantSuspended: \"lukket\",\n tenantActive: \"aktiv\",\n },\n en: {\n openMenu: \"Open menu\",\n closeMenu: \"Close menu\",\n home: \"Home\",\n working: \"Working\",\n moreFor: \"More for\",\n back: \"Back\",\n pinned: \"Pinned\",\n language: \"Language\",\n zoom: \"Zoom\",\n zoomOut: \"Zoom out\",\n zoomIn: \"Zoom in\",\n couldNotMarkAll: \"Could not mark all as read. Try again.\",\n collapse: \"Hide sidebar\",\n expand: \"Show sidebar\",\n notifications: \"Notifications\",\n markAllRead: \"Mark all read\",\n noNotifications: \"No notifications yet.\",\n couldNotLoad: \"Could not load notifications.\",\n seeAll: \"See all\",\n close: \"Close\",\n account: \"Account\",\n signOut: \"Sign out\",\n theme: \"Theme\",\n light: \"Light\",\n dark: \"Dark\",\n system: \"System\",\n palette: \"Palette\",\n surfaces: \"Surfaces\",\n flat: \"Flat\",\n layered: \"Layered\",\n backdrop: \"Backdrop\",\n neurons: \"Neurons\",\n plain: \"Plain\",\n name: \"Name\",\n email: \"Email\",\n save: \"Save\",\n saving: \"Saving…\",\n saved: \"Saved\",\n loadingProfile: \"Loading profile…\",\n couldNotLoadProfile: \"Could not load your profile.\",\n couldNotSave: \"Could not save. Try again.\",\n reauth: \"Sign in again to make changes\",\n signInAgain: \"Sign in again\",\n changePicture: \"Change picture\",\n removePicture: \"Remove picture\",\n confirmRemovePicture: \"Remove your picture?\",\n remove: \"Remove\",\n cancel: \"Cancel\",\n pictureSaved: \"Picture saved\",\n pictureRemoved: \"Picture removed\",\n pictureTooLarge: \"The picture is too large — 2 MB at most.\",\n pictureWrongType: \"Choose a PNG, JPEG or WebP image.\",\n bidSecurity: \"Security in Broberg ID\",\n analyticsPeriod: \"Period\",\n analyticsDays: \"{n} days\",\n analyticsVisitors: \"Visitors\",\n analyticsPageviews: \"Page views\",\n analyticsVsPrev: \"{pct}% vs previous period\",\n analyticsNoPrev: \"No comparison\",\n analyticsOverTime: \"Page views per day\",\n analyticsTopPages: \"Most visited pages\",\n analyticsDay: \"Day\",\n analyticsPage: \"Page\",\n analyticsEmpty: \"No visits in this period.\",\n analyticsLoading: \"Loading figures…\",\n analyticsUnconfigured: \"Analytics is not set up for this app.\",\n analyticsUnavailable: \"Upmetrics is not answering right now. Try again shortly.\",\n tenantSwitch: \"Switch organisation\",\n tenantLoadFailed: \"Could not load your organisations.\",\n tenantSwitchFailed: \"Could not switch organisation. You are still in {name}.\",\n tenantRetry: \"Try again\",\n tenantSuspended: \"suspended\",\n tenantActive: \"active\",\n },\n} as const;\nexport type Lang = keyof typeof TEXT;\n"]}