@eifi1/ui-kit 0.30.0 → 0.31.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (269) hide show
  1. package/README.md +42 -5
  2. package/dist/account/data-export-setting.js +3 -2
  3. package/dist/account/data-export-setting.js.map +1 -1
  4. package/dist/account/delete-account-setting.d.ts +5 -1
  5. package/dist/account/delete-account-setting.js +3 -2
  6. package/dist/account/delete-account-setting.js.map +1 -1
  7. package/dist/account/email-change-setting.js +4 -3
  8. package/dist/account/email-change-setting.js.map +1 -1
  9. package/dist/account/sessions-setting.js +3 -2
  10. package/dist/account/sessions-setting.js.map +1 -1
  11. package/dist/admin/admin-action-confirm.d.ts +5 -1
  12. package/dist/admin/admin-parts.d.ts +8 -1
  13. package/dist/admin/admin-parts.js +1 -1
  14. package/dist/admin/admin-parts.js.map +1 -1
  15. package/dist/admin/invitations-panel.d.ts +5 -1
  16. package/dist/admin/invitations-panel.js +3 -2
  17. package/dist/admin/invitations-panel.js.map +1 -1
  18. package/dist/admin/reviewer-scope-editor.d.ts +5 -1
  19. package/dist/admin/role-select.d.ts +5 -1
  20. package/dist/admin/transfer-ownership-dialog.d.ts +5 -1
  21. package/dist/admin/user-roster.d.ts +5 -1
  22. package/dist/auth/auth-errors.d.ts +24 -6
  23. package/dist/auth/auth-errors.js +7 -1
  24. package/dist/auth/auth-errors.js.map +1 -1
  25. package/dist/auth/register-form.d.ts +5 -1
  26. package/dist/auth/reset-password-form.d.ts +1 -1
  27. package/dist/auth/reset-password-form.js +2 -1
  28. package/dist/auth/reset-password-form.js.map +1 -1
  29. package/dist/auth/sign-in-form.d.ts +15 -1
  30. package/dist/auth/sign-in-form.js +12 -3
  31. package/dist/auth/sign-in-form.js.map +1 -1
  32. package/dist/auth/verify-email.d.ts +4 -0
  33. package/dist/auth/verify-email.js +3 -2
  34. package/dist/auth/verify-email.js.map +1 -1
  35. package/dist/chart.d.ts +5 -1
  36. package/dist/components/account-settings-labels.d.ts +12 -0
  37. package/dist/components/account-settings-labels.js +2 -1
  38. package/dist/components/account-settings-labels.js.map +1 -1
  39. package/dist/components/account-settings.d.ts +8 -2
  40. package/dist/components/account-settings.js +19 -11
  41. package/dist/components/account-settings.js.map +1 -1
  42. package/dist/components/amount-input.d.ts +5 -1
  43. package/dist/components/button-group.d.ts +5 -1
  44. package/dist/components/calculator.d.ts +5 -1
  45. package/dist/components/confirm-dialog.d.ts +5 -1
  46. package/dist/components/confirm-dialog.js +7 -1
  47. package/dist/components/confirm-dialog.js.map +1 -1
  48. package/dist/components/copy-button.d.ts +5 -1
  49. package/dist/components/danger-confirm.d.ts +5 -1
  50. package/dist/components/data-table.js +8 -1
  51. package/dist/components/data-table.js.map +1 -1
  52. package/dist/components/date-picker.js +9 -4
  53. package/dist/components/date-picker.js.map +1 -1
  54. package/dist/components/facing-pair.d.ts +5 -1
  55. package/dist/components/file-button.d.ts +5 -1
  56. package/dist/components/file-dropzone.d.ts +5 -1
  57. package/dist/components/form-actions.d.ts +5 -1
  58. package/dist/components/iban-input.d.ts +5 -1
  59. package/dist/components/language-select.d.ts +5 -1
  60. package/dist/components/legal-page.d.ts +5 -1
  61. package/dist/components/legal.d.ts +5 -1
  62. package/dist/components/money-field.d.ts +5 -1
  63. package/dist/components/number-field.d.ts +5 -1
  64. package/dist/components/number-input.d.ts +5 -1
  65. package/dist/components/numpad-sheet.d.ts +5 -1
  66. package/dist/components/passkeys-setting.js +2 -1
  67. package/dist/components/passkeys-setting.js.map +1 -1
  68. package/dist/components/phone-input.d.ts +5 -1
  69. package/dist/components/series-chart.d.ts +5 -1
  70. package/dist/components/settings-fields.d.ts +40 -3
  71. package/dist/components/settings-fields.js +20 -1
  72. package/dist/components/settings-fields.js.map +1 -1
  73. package/dist/components/share-card.d.ts +5 -1
  74. package/dist/components/text-link.d.ts +5 -1
  75. package/dist/components/time-input.d.ts +5 -1
  76. package/dist/components/ui.d.ts +5 -1
  77. package/dist/{delete-account-setting-CGVMckMw.d.ts → delete-account-setting-CpIhzxxZ.d.ts} +60 -23
  78. package/dist/demo/demo-banner.d.ts +51 -0
  79. package/dist/demo/demo-banner.js +97 -0
  80. package/dist/demo/demo-banner.js.map +1 -0
  81. package/dist/demo/demo-countdown.d.ts +58 -0
  82. package/dist/demo/demo-countdown.js +79 -0
  83. package/dist/demo/demo-countdown.js.map +1 -0
  84. package/dist/demo/demo-ended.d.ts +45 -0
  85. package/dist/demo/demo-ended.js +42 -0
  86. package/dist/demo/demo-ended.js.map +1 -0
  87. package/dist/demo/demo-labels.d.ts +53 -0
  88. package/dist/demo/demo-labels.js +29 -0
  89. package/dist/demo/demo-labels.js.map +1 -0
  90. package/dist/demo/demo-session.d.ts +30 -0
  91. package/dist/demo/demo-session.js +8 -0
  92. package/dist/demo/demo-session.js.map +1 -0
  93. package/dist/demo/demo-start.d.ts +54 -0
  94. package/dist/demo/demo-start.js +68 -0
  95. package/dist/demo/demo-start.js.map +1 -0
  96. package/dist/feedback/feedback-attachment.d.ts +5 -1
  97. package/dist/feedback/feedback-context.d.ts +5 -1
  98. package/dist/feedback/feedback-crash.d.ts +5 -1
  99. package/dist/feedback/feedback-dialog.d.ts +5 -1
  100. package/dist/feedback/feedback-inbox.d.ts +5 -1
  101. package/dist/feedback/feedback-inbox.js +12 -2
  102. package/dist/feedback/feedback-inbox.js.map +1 -1
  103. package/dist/feedback/feedback-labels.d.ts +5 -1
  104. package/dist/feedback/feedback-labels.js +1 -0
  105. package/dist/feedback/feedback-labels.js.map +1 -1
  106. package/dist/feedback/feedback-menu.d.ts +5 -1
  107. package/dist/feedback/feedback-record.d.ts +5 -1
  108. package/dist/feedback/feedback-record.js +2 -0
  109. package/dist/feedback/feedback-record.js.map +1 -1
  110. package/dist/feedback/feedback-row-detail.d.ts +5 -1
  111. package/dist/feedback/feedback-row-detail.js +7 -0
  112. package/dist/feedback/feedback-row-detail.js.map +1 -1
  113. package/dist/feedback/feedback-status-undo.d.ts +5 -1
  114. package/dist/feedback/feedback-submit.d.ts +5 -1
  115. package/dist/feedback/feedback-swipe.d.ts +6 -2
  116. package/dist/feedback/feedback-swipe.js.map +1 -1
  117. package/dist/feedback/feedback-table.d.ts +5 -1
  118. package/dist/feedback/feedback-thread.d.ts +5 -1
  119. package/dist/feedback.d.ts +5 -1
  120. package/dist/hooks/use-file-drop.d.ts +5 -1
  121. package/dist/i18n/defaults.d.ts +5 -1
  122. package/dist/i18n/defaults.js +6 -0
  123. package/dist/i18n/defaults.js.map +1 -1
  124. package/dist/i18n/german.d.ts +5 -1
  125. package/dist/i18n/german.js +85 -2
  126. package/dist/i18n/german.js.map +1 -1
  127. package/dist/i18n/kit-labels.d.ts +5 -1
  128. package/dist/i18n/kit-labels.js.map +1 -1
  129. package/dist/i18n/languages.d.ts +5 -1
  130. package/dist/i18n/locales/de-CH.d.ts +5 -1
  131. package/dist/i18n/locales/en.d.ts +5 -1
  132. package/dist/i18n/locales/es.d.ts +5 -1
  133. package/dist/i18n/locales/es.js +82 -2
  134. package/dist/i18n/locales/es.js.map +1 -1
  135. package/dist/i18n/locales/fr.d.ts +5 -1
  136. package/dist/i18n/locales/fr.js +83 -2
  137. package/dist/i18n/locales/fr.js.map +1 -1
  138. package/dist/i18n/locales/hu.d.ts +5 -1
  139. package/dist/i18n/locales/hu.js +83 -2
  140. package/dist/i18n/locales/hu.js.map +1 -1
  141. package/dist/i18n/locales/it.d.ts +5 -1
  142. package/dist/i18n/locales/it.js +82 -2
  143. package/dist/i18n/locales/it.js.map +1 -1
  144. package/dist/i18n/locales/zh.d.ts +5 -1
  145. package/dist/i18n/locales/zh.js +81 -2
  146. package/dist/i18n/locales/zh.js.map +1 -1
  147. package/dist/i18n/review.d.ts +5 -1
  148. package/dist/i18n/review.js +15 -1
  149. package/dist/i18n/review.js.map +1 -1
  150. package/dist/index.d.ts +25 -3
  151. package/dist/index.js +96 -0
  152. package/dist/index.js.map +1 -1
  153. package/dist/landing/access.d.ts +48 -0
  154. package/dist/landing/access.js +38 -0
  155. package/dist/landing/access.js.map +1 -0
  156. package/dist/landing/landing-actions.d.ts +140 -0
  157. package/dist/landing/landing-actions.js +39 -0
  158. package/dist/landing/landing-actions.js.map +1 -0
  159. package/dist/landing/landing-labels.d.ts +43 -0
  160. package/dist/landing/landing-labels.js +23 -0
  161. package/dist/landing/landing-labels.js.map +1 -0
  162. package/dist/landing/landing-sections.d.ts +218 -0
  163. package/dist/landing/landing-sections.js +167 -0
  164. package/dist/landing/landing-sections.js.map +1 -0
  165. package/dist/landing/page-seo.d.ts +99 -0
  166. package/dist/landing/page-seo.js +121 -0
  167. package/dist/landing/page-seo.js.map +1 -0
  168. package/dist/landing/public-header.d.ts +148 -0
  169. package/dist/landing/public-header.js +49 -0
  170. package/dist/landing/public-header.js.map +1 -0
  171. package/dist/landing/routing.d.ts +231 -0
  172. package/dist/landing/routing.js +84 -0
  173. package/dist/landing/routing.js.map +1 -0
  174. package/dist/rhf/fields.d.ts +5 -1
  175. package/dist/rhf/form.d.ts +5 -1
  176. package/dist/rhf.d.ts +5 -1
  177. package/dist/settings/settings-catalogue.d.ts +84 -0
  178. package/dist/settings/settings-catalogue.js +47 -0
  179. package/dist/settings/settings-catalogue.js.map +1 -0
  180. package/dist/settings/settings-context.d.ts +31 -0
  181. package/dist/settings/settings-context.js +11 -0
  182. package/dist/settings/settings-context.js.map +1 -0
  183. package/dist/settings/settings-heading.d.ts +50 -0
  184. package/dist/settings/settings-heading.js +27 -0
  185. package/dist/settings/settings-heading.js.map +1 -0
  186. package/dist/settings/settings-labels.d.ts +48 -0
  187. package/dist/settings/settings-labels.js +43 -0
  188. package/dist/settings/settings-labels.js.map +1 -0
  189. package/dist/settings/settings-layout.d.ts +77 -0
  190. package/dist/settings/settings-layout.js +242 -0
  191. package/dist/settings/settings-layout.js.map +1 -0
  192. package/dist/settings/settings-section.d.ts +133 -0
  193. package/dist/settings/settings-section.js +63 -0
  194. package/dist/settings/settings-section.js.map +1 -0
  195. package/dist/settings/use-account-language.d.ts +83 -0
  196. package/dist/settings/use-account-language.js +62 -0
  197. package/dist/settings/use-account-language.js.map +1 -0
  198. package/dist/settings/use-settings-focus.d.ts +34 -0
  199. package/dist/settings/use-settings-focus.js +77 -0
  200. package/dist/settings/use-settings-focus.js.map +1 -0
  201. package/dist/settings/use-settings-route.d.ts +118 -0
  202. package/dist/settings/use-settings-route.js +120 -0
  203. package/dist/settings/use-settings-route.js.map +1 -0
  204. package/dist/shell/app-shell.d.ts +11 -1
  205. package/dist/shell/app-shell.js +3 -2
  206. package/dist/shell/app-shell.js.map +1 -1
  207. package/dist/shell/top-bar-brand.d.ts +5 -1
  208. package/dist/shell.d.ts +15 -1
  209. package/dist/shell.js +56 -0
  210. package/dist/shell.js.map +1 -1
  211. package/dist/wizard/stepper-nav.d.ts +5 -1
  212. package/dist/wizard.d.ts +5 -1
  213. package/package.json +2 -1
  214. package/src/account/data-export-setting.tsx +3 -2
  215. package/src/account/delete-account-setting.tsx +3 -2
  216. package/src/account/email-change-setting.tsx +4 -7
  217. package/src/account/sessions-setting.tsx +3 -2
  218. package/src/admin/admin-parts.ts +9 -2
  219. package/src/admin/invitations-panel.tsx +11 -4
  220. package/src/auth/auth-errors.ts +36 -5
  221. package/src/auth/reset-password-form.tsx +6 -2
  222. package/src/auth/sign-in-form.tsx +34 -3
  223. package/src/auth/verify-email.tsx +14 -2
  224. package/src/components/account-settings-labels.ts +13 -0
  225. package/src/components/account-settings.tsx +33 -12
  226. package/src/components/confirm-dialog.tsx +20 -1
  227. package/src/components/data-table.tsx +8 -0
  228. package/src/components/date-picker.tsx +9 -4
  229. package/src/components/passkeys-setting.tsx +2 -1
  230. package/src/components/settings-fields.tsx +50 -1
  231. package/src/demo/demo-banner.tsx +159 -0
  232. package/src/demo/demo-countdown.ts +141 -0
  233. package/src/demo/demo-ended.tsx +89 -0
  234. package/src/demo/demo-labels.ts +78 -0
  235. package/src/demo/demo-session.ts +32 -0
  236. package/src/demo/demo-start.tsx +150 -0
  237. package/src/feedback/feedback-inbox.tsx +21 -8
  238. package/src/feedback/feedback-labels.ts +2 -1
  239. package/src/feedback/feedback-record.ts +10 -7
  240. package/src/feedback/feedback-row-detail.tsx +28 -4
  241. package/src/feedback/feedback-swipe.tsx +1 -1
  242. package/src/i18n/defaults.ts +6 -0
  243. package/src/i18n/german.ts +87 -0
  244. package/src/i18n/kit-labels.tsx +9 -0
  245. package/src/i18n/locales/es.ts +84 -0
  246. package/src/i18n/locales/fr.ts +85 -0
  247. package/src/i18n/locales/hu.ts +85 -0
  248. package/src/i18n/locales/it.ts +84 -0
  249. package/src/i18n/locales/zh.ts +82 -0
  250. package/src/i18n/review.ts +14 -0
  251. package/src/index.ts +98 -1
  252. package/src/landing/access.ts +97 -0
  253. package/src/landing/landing-actions.tsx +96 -0
  254. package/src/landing/landing-labels.ts +59 -0
  255. package/src/landing/landing-sections.tsx +337 -0
  256. package/src/landing/page-seo.ts +251 -0
  257. package/src/landing/public-header.tsx +120 -0
  258. package/src/landing/routing.tsx +226 -0
  259. package/src/settings/settings-catalogue.ts +132 -0
  260. package/src/settings/settings-context.ts +33 -0
  261. package/src/settings/settings-heading.tsx +65 -0
  262. package/src/settings/settings-labels.ts +87 -0
  263. package/src/settings/settings-layout.tsx +379 -0
  264. package/src/settings/settings-section.tsx +108 -0
  265. package/src/settings/use-account-language.ts +146 -0
  266. package/src/settings/use-settings-focus.ts +120 -0
  267. package/src/settings/use-settings-route.ts +258 -0
  268. package/src/shell/app-shell.tsx +10 -2
  269. package/src/shell.ts +51 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/landing/page-seo.ts"],"sourcesContent":["import { useEffect } from \"react\";\n\n/**\n * Search-engine helpers for the public landing page (docs/landing-demo-harmonization.md\n * §4.3): the hook that declares a page's canonical URL, title, description and JSON-LD,\n * and the two pure helpers each app's SEO test is built from.\n */\n\n/**\n * Declares the current page's canonical URL — and optionally its title and meta\n * description — while the calling component is mounted (feedback #129).\n *\n * Client-side because there is nowhere else to put it: the SPA is served by\n * Caddy's `try_files … /index.html`, so every route shares one static head.\n * Googlebot renders JS and reads the canonical from the RENDERED DOM, which is\n * what makes this work; non-rendering scrapers (WhatsApp, Slack) only ever see\n * index.html's static tags, which is why those stay in place as the fallback.\n *\n * The href is resolved against `window.location.origin` rather than hardcoded, so\n * a preview deployment or the VPS host stays self-referential instead of pointing\n * every copy of the app at production.\n *\n * The cleanup matters for the same reason as in {@link useNoIndex}: an SPA\n * navigation from the landing page into the app must not leave the landing's\n * canonical (or title) behind on an app page.\n *\n * 0.31 (§4.3): keksdose's `usePageSeo` (shared/hooks/use-page-seo.ts), verbatim. The\n * landing calls it with `canonicalPath: \"/\"` on both `/` and `/welcome`, so the two URLs\n * of one page are one result; there are no per-language URLs and no hreflang.\n */\nexport function usePageSeo({\n canonicalPath,\n title,\n description,\n jsonLd,\n}: {\n canonicalPath: string;\n title?: string;\n description?: string;\n /** Structured data for this page, as the object (not a string). Injected as an\n * `application/ld+json` DATA BLOCK — a script element the browser never executes,\n * which is also why it can be added here rather than in `index.html`: the CSP\n * carries a HASH of the one inline script that file is allowed to have, and\n * `csp-inline-script.test.ts` fails the build on a second one (live #250). */\n jsonLd?: Record<string, unknown>;\n}): void {\n useEffect(() => {\n const link = document.createElement(\"link\");\n link.rel = \"canonical\";\n link.href = new URL(canonicalPath, window.location.origin).toString();\n document.head.appendChild(link);\n\n const previousTitle = document.title;\n if (title) document.title = title;\n\n // The description tag ships in index.html, so upsert rather than append —\n // two `meta[name=description]` tags would be a coin flip for the scraper.\n const meta = description\n ? document.head.querySelector<HTMLMetaElement>('meta[name=\"description\"]')\n : null;\n const previousDescription = meta?.content;\n if (meta && description) meta.content = description;\n\n let ld: HTMLScriptElement | null = null;\n if (jsonLd) {\n ld = document.createElement(\"script\");\n ld.type = \"application/ld+json\";\n ld.textContent = JSON.stringify(jsonLd);\n document.head.appendChild(ld);\n }\n\n return () => {\n link.remove();\n ld?.remove();\n if (title) document.title = previousTitle;\n if (meta && previousDescription !== undefined) meta.content = previousDescription;\n };\n // `jsonLd` is built inline by the caller, so it is a new object every render and\n // cannot be a dependency — it is derived from `title`/`description`, which are.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [canonicalPath, title, description]);\n}\n\n/* ── The SEO test's helpers ──────────────────────────────────────────────── */\n\n/** What a search result shows of the landing, in one language. */\nexport interface SeoCopy {\n /** The page title (`landing.seo.title`): \"Keksdose — envelope budgeting\". */\n title: string;\n /** The meta description. */\n description: string;\n /** The product name the title carries, before or after its tagline. */\n brand: string;\n}\n\n/** The width budgets, as characters. */\nexport interface SeoCopyLimits {\n /** Default 60: a result's title is cut around 580px. */\n maxTitle?: number;\n /** Default 155: a snippet is cut around 920px. */\n maxDescription?: number;\n}\n\nexport type SeoCopyProblemCode =\n | \"title-empty\"\n | \"description-empty\"\n | \"title-too-long\"\n | \"description-too-long\"\n | \"title-without-brand\"\n | \"description-repeats-title\";\n\n/** One thing wrong with a language's copy. `message` is for the test's failure output. */\nexport interface SeoCopyProblem {\n code: SeoCopyProblemCode;\n message: string;\n}\n\n/** Characters as a reader counts them: code points, so an emoji or a rare CJK character\n * is one, not two UTF-16 units. */\nconst length = (text: string) => Array.from(text).length;\n\nconst escapeRegExp = (text: string) => text.replace(/[.*+?^${}()|[\\]\\\\]/g, \"\\\\$&\");\n\n/** The separators a title puts between the brand and its tagline. */\nconst SEPARATOR = \"\\\\s*[—–\\\\-|:·•]\\\\s*\";\n\n/** The title without the brand and its separator, or `null` when the brand is in\n * neither place. */\nfunction taglineOf(title: string, brand: string): string | null {\n const name = escapeRegExp(brand.trim());\n const leading = new RegExp(`^${name}${SEPARATOR}`, \"i\");\n if (leading.test(title)) return title.replace(leading, \"\").trim();\n const trailing = new RegExp(`${SEPARATOR}${name}$`, \"i\");\n if (trailing.test(title)) return title.replace(trailing, \"\").trim();\n return null;\n}\n\n/**\n * What is wrong with one language's search-result copy (§4.3) — an empty list when\n * nothing is. keksdose's landing-seo.test.ts (live #305), made reusable:\n *\n * - the title fits `maxTitle` characters (60) and the description `maxDescription`\n * (155) — conservative stand-ins for Google's pixel budgets; German is the language\n * most likely to blow through them, and what gets cut is always the end;\n * - the title carries the brand, before or after its tagline (\"Keksdose — …\");\n * - the description does not open with the title's tagline again — a snippet that\n * repeats the line above it spends its first forty characters saying nothing new.\n *\n * ```ts\n * for (const [locale, bundle] of Object.entries(LOCALES)) {\n * it(`fits ${locale} into a search result`, () => {\n * expect(seoCopyProblems({ ...bundle.landing.seo, brand: \"Keksdose\" })).toEqual([]);\n * });\n * }\n * ```\n */\nexport function seoCopyProblems(copy: SeoCopy, limits: SeoCopyLimits = {}): SeoCopyProblem[] {\n const { maxTitle = 60, maxDescription = 155 } = limits;\n const title = copy.title.replace(/\\s+/g, \" \").trim();\n const description = copy.description.replace(/\\s+/g, \" \").trim();\n const problems: SeoCopyProblem[] = [];\n if (!title) problems.push({ code: \"title-empty\", message: \"the title is empty\" });\n if (!description) problems.push({ code: \"description-empty\", message: \"the description is empty\" });\n if (length(title) > maxTitle) {\n problems.push({\n code: \"title-too-long\",\n message: `the title is ${length(title)} characters, over ${maxTitle}: \"${title}\"`,\n });\n }\n if (length(description) > maxDescription) {\n problems.push({\n code: \"description-too-long\",\n message: `the description is ${length(description)} characters, over ${maxDescription}: \"${description}\"`,\n });\n }\n if (!title) return problems;\n const tagline = taglineOf(title, copy.brand);\n if (tagline === null) {\n problems.push({\n code: \"title-without-brand\",\n message: `the title does not start or end with \"${copy.brand}\" and a separator: \"${title}\"`,\n });\n }\n const repeated = (tagline ?? title).toLowerCase();\n if (repeated && description.toLowerCase().startsWith(repeated)) {\n problems.push({\n code: \"description-repeats-title\",\n message: `the description opens with the title's tagline \"${tagline ?? title}\"`,\n });\n }\n return problems;\n}\n\n/** The five named references and the numeric ones — enough for a meta tag's text. */\nfunction decodeEntities(text: string): string {\n const named: Record<string, string> = { amp: \"&\", lt: \"<\", gt: \">\", quot: '\"', apos: \"'\", nbsp: \" \" };\n return text.replace(/&(#x[\\da-f]+|#\\d+|[a-z]+);/gi, (whole, ref: string) => {\n if (ref[0] === \"#\") {\n const code = ref[1] === \"x\" || ref[1] === \"X\" ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);\n return Number.isFinite(code) ? String.fromCodePoint(code) : whole;\n }\n return named[ref.toLowerCase()] ?? whole;\n });\n}\n\n/** A tag's attributes, names lower-cased; values double-, single- or un-quoted. */\nfunction attributesOf(tag: string): Map<string, string> {\n const found = new Map<string, string>();\n for (const m of tag.matchAll(/([^\\s=/<>\"']+)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+))/g)) {\n found.set(m[1]!.toLowerCase(), m[2] ?? m[3] ?? m[4] ?? \"\");\n }\n return found;\n}\n\n/**\n * The `content` of the first `<meta>` in `html` that `selector` picks, whitespace\n * collapsed and entities decoded — or `undefined` when there is none. For each app's\n * parity test (§4.3): `index.html`'s static description and `og:` tags must equal the\n * default language's strings, because a scraper that runs no script (WhatsApp, Slack)\n * sees only those.\n *\n * `selector` names attributes, as in CSS — `meta[name=\"description\"]`,\n * `[property=\"og:title\"]` or just `property=\"og:description\"` — and every one named must\n * match. A `RegExp` is matched against the tag's source instead (keksdose's form).\n * String parsing, not a DOM: the test runs under `@vitest-environment node`.\n *\n * ```ts\n * expect(metaContent(html, 'meta[name=\"description\"]')).toBe(de.landing.seo.description);\n * ```\n */\nexport function metaContent(html: string, selector: string | RegExp): string | undefined {\n const wanted =\n typeof selector === \"string\"\n ? Array.from(selector.matchAll(/([\\w:.-]+)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)')/g), (m) => [\n m[1]!.toLowerCase(),\n m[2] ?? m[3] ?? \"\",\n ])\n : null;\n if (wanted !== null && wanted.length === 0) {\n throw new Error(`metaContent: the selector names no attribute: ${selector as string}`);\n }\n const pattern = selector instanceof RegExp ? new RegExp(selector.source, selector.flags.replace(\"g\", \"\")) : null;\n for (const tag of html.match(/<meta\\b[^>]*>/gi) ?? []) {\n const attrs = attributesOf(tag);\n const matches = pattern ? pattern.test(tag) : wanted!.every(([name, value]) => attrs.get(name!) === value);\n if (!matches) continue;\n const content = attrs.get(\"content\");\n return content === undefined ? undefined : decodeEntities(content).replace(/\\s+/g, \" \").trim();\n }\n return undefined;\n}\n"],"mappings":";AAAA,SAAS,iBAAiB;AA8BnB,SAAS,WAAW;AAAA,EACzB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,GAUS;AACP,YAAU,MAAM;AACd,UAAM,OAAO,SAAS,cAAc,MAAM;AAC1C,SAAK,MAAM;AACX,SAAK,OAAO,IAAI,IAAI,eAAe,OAAO,SAAS,MAAM,EAAE,SAAS;AACpE,aAAS,KAAK,YAAY,IAAI;AAE9B,UAAM,gBAAgB,SAAS;AAC/B,QAAI,MAAO,UAAS,QAAQ;AAI5B,UAAM,OAAO,cACT,SAAS,KAAK,cAA+B,0BAA0B,IACvE;AACJ,UAAM,sBAAsB,MAAM;AAClC,QAAI,QAAQ,YAAa,MAAK,UAAU;AAExC,QAAI,KAA+B;AACnC,QAAI,QAAQ;AACV,WAAK,SAAS,cAAc,QAAQ;AACpC,SAAG,OAAO;AACV,SAAG,cAAc,KAAK,UAAU,MAAM;AACtC,eAAS,KAAK,YAAY,EAAE;AAAA,IAC9B;AAEA,WAAO,MAAM;AACX,WAAK,OAAO;AACZ,UAAI,OAAO;AACX,UAAI,MAAO,UAAS,QAAQ;AAC5B,UAAI,QAAQ,wBAAwB,OAAW,MAAK,UAAU;AAAA,IAChE;AAAA,EAIF,GAAG,CAAC,eAAe,OAAO,WAAW,CAAC;AACxC;AAsCA,MAAM,SAAS,CAAC,SAAiB,MAAM,KAAK,IAAI,EAAE;AAElD,MAAM,eAAe,CAAC,SAAiB,KAAK,QAAQ,uBAAuB,MAAM;AAGjF,MAAM,YAAY;AAIlB,SAAS,UAAU,OAAe,OAA8B;AAC9D,QAAM,OAAO,aAAa,MAAM,KAAK,CAAC;AACtC,QAAM,UAAU,IAAI,OAAO,IAAI,IAAI,GAAG,SAAS,IAAI,GAAG;AACtD,MAAI,QAAQ,KAAK,KAAK,EAAG,QAAO,MAAM,QAAQ,SAAS,EAAE,EAAE,KAAK;AAChE,QAAM,WAAW,IAAI,OAAO,GAAG,SAAS,GAAG,IAAI,KAAK,GAAG;AACvD,MAAI,SAAS,KAAK,KAAK,EAAG,QAAO,MAAM,QAAQ,UAAU,EAAE,EAAE,KAAK;AAClE,SAAO;AACT;AAqBO,SAAS,gBAAgB,MAAe,SAAwB,CAAC,GAAqB;AAC3F,QAAM,EAAE,WAAW,IAAI,iBAAiB,IAAI,IAAI;AAChD,QAAM,QAAQ,KAAK,MAAM,QAAQ,QAAQ,GAAG,EAAE,KAAK;AACnD,QAAM,cAAc,KAAK,YAAY,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAC/D,QAAM,WAA6B,CAAC;AACpC,MAAI,CAAC,MAAO,UAAS,KAAK,EAAE,MAAM,eAAe,SAAS,qBAAqB,CAAC;AAChF,MAAI,CAAC,YAAa,UAAS,KAAK,EAAE,MAAM,qBAAqB,SAAS,2BAA2B,CAAC;AAClG,MAAI,OAAO,KAAK,IAAI,UAAU;AAC5B,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,SAAS,gBAAgB,OAAO,KAAK,CAAC,qBAAqB,QAAQ,MAAM,KAAK;AAAA,IAChF,CAAC;AAAA,EACH;AACA,MAAI,OAAO,WAAW,IAAI,gBAAgB;AACxC,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,SAAS,sBAAsB,OAAO,WAAW,CAAC,qBAAqB,cAAc,MAAM,WAAW;AAAA,IACxG,CAAC;AAAA,EACH;AACA,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,UAAU,UAAU,OAAO,KAAK,KAAK;AAC3C,MAAI,YAAY,MAAM;AACpB,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,SAAS,yCAAyC,KAAK,KAAK,uBAAuB,KAAK;AAAA,IAC1F,CAAC;AAAA,EACH;AACA,QAAM,YAAY,WAAW,OAAO,YAAY;AAChD,MAAI,YAAY,YAAY,YAAY,EAAE,WAAW,QAAQ,GAAG;AAC9D,aAAS,KAAK;AAAA,MACZ,MAAM;AAAA,MACN,SAAS,mDAAmD,WAAW,KAAK;AAAA,IAC9E,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAGA,SAAS,eAAe,MAAsB;AAC5C,QAAM,QAAgC,EAAE,KAAK,KAAK,IAAI,KAAK,IAAI,KAAK,MAAM,KAAK,MAAM,KAAK,MAAM,OAAI;AACpG,SAAO,KAAK,QAAQ,gCAAgC,CAAC,OAAO,QAAgB;AAC1E,QAAI,IAAI,CAAC,MAAM,KAAK;AAClB,YAAM,OAAO,IAAI,CAAC,MAAM,OAAO,IAAI,CAAC,MAAM,MAAM,SAAS,IAAI,MAAM,CAAC,GAAG,EAAE,IAAI,SAAS,IAAI,MAAM,CAAC,GAAG,EAAE;AACtG,aAAO,OAAO,SAAS,IAAI,IAAI,OAAO,cAAc,IAAI,IAAI;AAAA,IAC9D;AACA,WAAO,MAAM,IAAI,YAAY,CAAC,KAAK;AAAA,EACrC,CAAC;AACH;AAGA,SAAS,aAAa,KAAkC;AACtD,QAAM,QAAQ,oBAAI,IAAoB;AACtC,aAAW,KAAK,IAAI,SAAS,8DAA8D,GAAG;AAC5F,UAAM,IAAI,EAAE,CAAC,EAAG,YAAY,GAAG,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK,EAAE;AAAA,EAC3D;AACA,SAAO;AACT;AAkBO,SAAS,YAAY,MAAc,UAA+C;AACvF,QAAM,SACJ,OAAO,aAAa,WAChB,MAAM,KAAK,SAAS,SAAS,2CAA2C,GAAG,CAAC,MAAM;AAAA,IAChF,EAAE,CAAC,EAAG,YAAY;AAAA,IAClB,EAAE,CAAC,KAAK,EAAE,CAAC,KAAK;AAAA,EAClB,CAAC,IACD;AACN,MAAI,WAAW,QAAQ,OAAO,WAAW,GAAG;AAC1C,UAAM,IAAI,MAAM,iDAAiD,QAAkB,EAAE;AAAA,EACvF;AACA,QAAM,UAAU,oBAAoB,SAAS,IAAI,OAAO,SAAS,QAAQ,SAAS,MAAM,QAAQ,KAAK,EAAE,CAAC,IAAI;AAC5G,aAAW,OAAO,KAAK,MAAM,iBAAiB,KAAK,CAAC,GAAG;AACrD,UAAM,QAAQ,aAAa,GAAG;AAC9B,UAAM,UAAU,UAAU,QAAQ,KAAK,GAAG,IAAI,OAAQ,MAAM,CAAC,CAAC,MAAM,KAAK,MAAM,MAAM,IAAI,IAAK,MAAM,KAAK;AACzG,QAAI,CAAC,QAAS;AACd,UAAM,UAAU,MAAM,IAAI,SAAS;AACnC,WAAO,YAAY,SAAY,SAAY,eAAe,OAAO,EAAE,QAAQ,QAAQ,GAAG,EAAE,KAAK;AAAA,EAC/F;AACA,SAAO;AACT;","names":[]}
@@ -0,0 +1,148 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { TopBarProps } from '../shell/top-bar.js';
4
+ import { AccessChoice } from './access.js';
5
+ import { LandingLabels } from './landing-labels.js';
6
+ import { LandingSession } from './landing-actions.js';
7
+ import '../delete-account-setting-CpIhzxxZ.js';
8
+ import '../components/tooltip.js';
9
+ import '../hooks/use-anchored-rect.js';
10
+ import '../lib/clipping.js';
11
+ import '../data-table-labels-DUELeXpa.js';
12
+ import '../components/data-table-sort.js';
13
+ import '../components/swipeable-row.js';
14
+ import '../components/mini-calendar.js';
15
+ import '../components/calendar-heatmap.js';
16
+ import '../components/popover.js';
17
+ import '../components/chip.js';
18
+ import 'lucide-react';
19
+ import '../components/field-sync.js';
20
+ import '../wizard/types.js';
21
+ import '../tour/tour.js';
22
+ import '../search/command-palette.js';
23
+ import '../search/global-search.js';
24
+ import '../search/search-index.js';
25
+ import '../components/month-picker.js';
26
+ import '../components/page-contents.js';
27
+ import '../components/series-chart-labels.js';
28
+ import '../components/pie-chart-labels.js';
29
+ import '../components/sparkline.js';
30
+ import '../components/stat-tile.js';
31
+ import '../components/signature-pad.js';
32
+ import '../components/password-strength.js';
33
+ import '../components/swatch-picker.js';
34
+ import '../components/tile-radio.js';
35
+ import '../components/icon-picker.js';
36
+ import '../components/dialog-frame.js';
37
+ import '../components/modal.js';
38
+ import '../hooks/use-search-param-state.js';
39
+ import '../components/measured-grid.js';
40
+ import '../components/user-avatar.js';
41
+ import '../lib/person-name.js';
42
+ import '../components/status-dot.js';
43
+ import '../components/progress-bar.js';
44
+ import '../components/signed-amount.js';
45
+ import '../shell/topbar-action-menu.js';
46
+ import '../components/menu-item.js';
47
+ import '../components/floating-panel.js';
48
+ import '../hooks/use-authed-src.js';
49
+ import '../components/account-settings-labels.js';
50
+ import '../components/bulk-action-bar.js';
51
+ import '../components/list.js';
52
+ import '../components/breadcrumbs.js';
53
+ import '../components/toast.js';
54
+ import '../components/description-list.js';
55
+ import '../components/line-items.js';
56
+ import '../components/error-boundary.js';
57
+ import '../components/authed-image.js';
58
+ import '../components/image-grid.js';
59
+ import '../components/lightbox.js';
60
+ import '../components/write-lock.js';
61
+ import '../auth/sign-in-form.js';
62
+ import '../shell/topbar-controls.js';
63
+ import '../theme/palette-presets.js';
64
+ import '../theme/chart-palette.js';
65
+ import '../theme/theme-store.js';
66
+ import 'zustand/middleware';
67
+ import 'zustand';
68
+ import '../auth/complete-name-dialog.js';
69
+ import '../auth/forgot-password-form.js';
70
+ import '../auth/status-parts.js';
71
+ import '../auth/reset-password-form.js';
72
+ import '../auth/verify-email.js';
73
+ import '../auth/not-found-page.js';
74
+ import '../auth/accept-invitation.js';
75
+ import '../shell/company-switcher.js';
76
+ import '../account/email-change-setting.js';
77
+ import '../account/sessions-setting.js';
78
+ import '../lib/format.js';
79
+ import '../account/data-export-setting.js';
80
+ import '../components/account-chips.js';
81
+ import '../admin/admin-parts.js';
82
+ import '../admin/admin-action-log.js';
83
+ import '../settings/settings-labels.js';
84
+ import '../demo/demo-labels.js';
85
+ import '../components/reauth-dialog.js';
86
+ import '../components/server-wake.js';
87
+ import '../lib/server-wake.js';
88
+ import '../components/translation-review-labels.js';
89
+ import '../lib/translation-review.js';
90
+ import '../components/country-select.js';
91
+ import '../components/inline-edit-field.js';
92
+ import '../lib/iban.js';
93
+ import '../lib/phone.js';
94
+ import '../components/sign-chip.js';
95
+ import '../components/column-mapper.js';
96
+ import '../lib/column-mapping.js';
97
+ import '../lib/table-text.js';
98
+ import '../components/field-parts.js';
99
+
100
+ /** The header's brand: what `TopBarBrand` draws. */
101
+ interface PublicHeaderBrand {
102
+ /** The brand mark — an SVG or an `<img alt="">`. */
103
+ logo: ReactNode;
104
+ /** The product name beside it, and the link's accessible name. */
105
+ name: ReactNode;
106
+ /** See `TopBarBrand`. Default `sm`: the logo alone on a phone. */
107
+ collapseBelow?: "sm" | "md" | false;
108
+ }
109
+ interface PublicHeaderProps extends Omit<TopBarProps, "brand" | "actions" | "children"> {
110
+ brand: PublicHeaderBrand;
111
+ /** Where the brand links. Default `/` without a session and `/welcome` with one —
112
+ * `/` would resume into the app, `/welcome` is always the landing (§3.1). */
113
+ homeHref?: string;
114
+ session: LandingSession;
115
+ access: AccessChoice;
116
+ /** "Sign in". Default `/login`. */
117
+ signInHref?: string;
118
+ /** "Open app" for a real session — the resume target. Default `/`, which resumes
119
+ * through `RootEntry`. */
120
+ openAppHref?: string;
121
+ /** "Continue the demo" for a demo session. Default `/demo`, which continues a live
122
+ * demo (§3.1). */
123
+ demoHref?: string;
124
+ /** The app's `LanguageMenu`, before the actions. */
125
+ languageMenu?: ReactNode;
126
+ /** The app's `ThemeToggle`, before the language menu. */
127
+ themeToggle?: ReactNode;
128
+ labels?: Partial<LandingLabels>;
129
+ }
130
+ /**
131
+ * The header of every public page (docs/landing-demo-harmonization.md §4.1): the
132
+ * landing, the auth pages, the legal pages and the 404 — keksdose's `LandingHeader`, on
133
+ * the kit's `TopBar` and `TopBarBrand`, with its hard-coded teal turned into `--brand`.
134
+ *
135
+ * | session | actions |
136
+ * |---|---|
137
+ * | none | Sign in (secondary, hidden below `sm`), **Request access** (primary) |
138
+ * | demo | Continue the demo (secondary), **Request access** (primary) |
139
+ * | user | **Open app** (primary) |
140
+ *
141
+ * A demo session is not "signed in" here: it is still offered access, which is the point
142
+ * of the demo (§5.6). On a phone (`PHONE_QUERY`) the actions take the kit's `sm` size, so
143
+ * the brand mark, the two controls and two actions fit 390px; "Sign in" gives way below
144
+ * `sm` as in keksdose — the sign-in page is one tap away from "Request access" anyway.
145
+ */
146
+ declare function PublicHeader({ brand, homeHref, session, access, signInHref, openAppHref, demoHref, languageMenu, themeToggle, labels: labelsProp, className, ...rest }: PublicHeaderProps): react.JSX.Element;
147
+
148
+ export { PublicHeader, type PublicHeaderBrand, type PublicHeaderProps };
@@ -0,0 +1,49 @@
1
+ "use client";
2
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
3
+ import { Button, PHONE_QUERY } from "../components/ui.js";
4
+ import { useMediaQuery } from "../hooks/use-media-query.js";
5
+ import { TopBar } from "../shell/top-bar.js";
6
+ import { TopBarBrand } from "../shell/top-bar-brand.js";
7
+ import { useAccessAction } from "./access.js";
8
+ import { useLandingLabels } from "./landing-labels.js";
9
+ function PublicHeader({
10
+ brand,
11
+ homeHref,
12
+ session,
13
+ access,
14
+ signInHref = "/login",
15
+ openAppHref = "/",
16
+ demoHref = "/demo",
17
+ languageMenu,
18
+ themeToggle,
19
+ labels: labelsProp,
20
+ className,
21
+ ...rest
22
+ }) {
23
+ const labels = useLandingLabels(labelsProp);
24
+ const accessLink = useAccessAction(access, labelsProp);
25
+ const phone = useMediaQuery(PHONE_QUERY, false);
26
+ const size = phone ? "sm" : "md";
27
+ const home = homeHref ?? (session === "none" ? "/" : "/welcome");
28
+ const actions = session === "user" ? /* @__PURE__ */ jsx(Button, { href: openAppHref, variant: "brand", size, children: labels.openApp }) : /* @__PURE__ */ jsxs(Fragment, { children: [
29
+ session === "demo" ? /* @__PURE__ */ jsx(Button, { href: demoHref, variant: "secondary", size, children: labels.continueDemo }) : /* @__PURE__ */ jsx(Button, { href: signInHref, variant: "secondary", size, className: "max-sm:hidden", children: labels.signIn }),
30
+ /* @__PURE__ */ jsx(Button, { href: accessLink.href, variant: "brand", size, children: accessLink.label })
31
+ ] });
32
+ return /* @__PURE__ */ jsx(
33
+ TopBar,
34
+ {
35
+ ...rest,
36
+ className,
37
+ brand: /* @__PURE__ */ jsx(TopBarBrand, { logo: brand.logo, name: brand.name, to: home, collapseBelow: brand.collapseBelow }),
38
+ actions: /* @__PURE__ */ jsxs(Fragment, { children: [
39
+ themeToggle,
40
+ languageMenu,
41
+ /* @__PURE__ */ jsx("span", { "data-slot": "public-header-actions", className: "ms-1 flex items-center gap-1.5 sm:gap-2", children: actions })
42
+ ] })
43
+ }
44
+ );
45
+ }
46
+ export {
47
+ PublicHeader
48
+ };
49
+ //# sourceMappingURL=public-header.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/landing/public-header.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\n\nimport { Button, PHONE_QUERY } from \"../components/ui\";\nimport { useMediaQuery } from \"../hooks/use-media-query\";\nimport { TopBar } from \"../shell/top-bar\";\nimport type { TopBarProps } from \"../shell/top-bar\";\nimport { TopBarBrand } from \"../shell/top-bar-brand\";\nimport { useAccessAction } from \"./access\";\nimport type { AccessChoice } from \"./access\";\nimport { useLandingLabels } from \"./landing-labels\";\nimport type { LandingLabels } from \"./landing-labels\";\nimport type { LandingSession } from \"./landing-actions\";\n\n/** The header's brand: what `TopBarBrand` draws. */\nexport interface PublicHeaderBrand {\n /** The brand mark — an SVG or an `<img alt=\"\">`. */\n logo: ReactNode;\n /** The product name beside it, and the link's accessible name. */\n name: ReactNode;\n /** See `TopBarBrand`. Default `sm`: the logo alone on a phone. */\n collapseBelow?: \"sm\" | \"md\" | false;\n}\n\nexport interface PublicHeaderProps extends Omit<TopBarProps, \"brand\" | \"actions\" | \"children\"> {\n brand: PublicHeaderBrand;\n /** Where the brand links. Default `/` without a session and `/welcome` with one —\n * `/` would resume into the app, `/welcome` is always the landing (§3.1). */\n homeHref?: string;\n session: LandingSession;\n access: AccessChoice;\n /** \"Sign in\". Default `/login`. */\n signInHref?: string;\n /** \"Open app\" for a real session — the resume target. Default `/`, which resumes\n * through `RootEntry`. */\n openAppHref?: string;\n /** \"Continue the demo\" for a demo session. Default `/demo`, which continues a live\n * demo (§3.1). */\n demoHref?: string;\n /** The app's `LanguageMenu`, before the actions. */\n languageMenu?: ReactNode;\n /** The app's `ThemeToggle`, before the language menu. */\n themeToggle?: ReactNode;\n labels?: Partial<LandingLabels>;\n}\n\n/**\n * The header of every public page (docs/landing-demo-harmonization.md §4.1): the\n * landing, the auth pages, the legal pages and the 404 — keksdose's `LandingHeader`, on\n * the kit's `TopBar` and `TopBarBrand`, with its hard-coded teal turned into `--brand`.\n *\n * | session | actions |\n * |---|---|\n * | none | Sign in (secondary, hidden below `sm`), **Request access** (primary) |\n * | demo | Continue the demo (secondary), **Request access** (primary) |\n * | user | **Open app** (primary) |\n *\n * A demo session is not \"signed in\" here: it is still offered access, which is the point\n * of the demo (§5.6). On a phone (`PHONE_QUERY`) the actions take the kit's `sm` size, so\n * the brand mark, the two controls and two actions fit 390px; \"Sign in\" gives way below\n * `sm` as in keksdose — the sign-in page is one tap away from \"Request access\" anyway.\n */\nexport function PublicHeader({\n brand,\n homeHref,\n session,\n access,\n signInHref = \"/login\",\n openAppHref = \"/\",\n demoHref = \"/demo\",\n languageMenu,\n themeToggle,\n labels: labelsProp,\n className,\n ...rest\n}: PublicHeaderProps) {\n const labels = useLandingLabels(labelsProp);\n const accessLink = useAccessAction(access, labelsProp);\n const phone = useMediaQuery(PHONE_QUERY, false);\n const size = phone ? \"sm\" : \"md\";\n const home = homeHref ?? (session === \"none\" ? \"/\" : \"/welcome\");\n\n const actions =\n session === \"user\" ? (\n <Button href={openAppHref} variant=\"brand\" size={size}>\n {labels.openApp}\n </Button>\n ) : (\n <>\n {session === \"demo\" ? (\n <Button href={demoHref} variant=\"secondary\" size={size}>\n {labels.continueDemo}\n </Button>\n ) : (\n <Button href={signInHref} variant=\"secondary\" size={size} className=\"max-sm:hidden\">\n {labels.signIn}\n </Button>\n )}\n <Button href={accessLink.href} variant=\"brand\" size={size}>\n {accessLink.label}\n </Button>\n </>\n );\n\n return (\n <TopBar\n {...rest}\n className={className}\n brand={<TopBarBrand logo={brand.logo} name={brand.name} to={home} collapseBelow={brand.collapseBelow} />}\n actions={\n <>\n {themeToggle}\n {languageMenu}\n <span data-slot=\"public-header-actions\" className=\"ms-1 flex items-center gap-1.5 sm:gap-2\">\n {actions}\n </span>\n </>\n }\n />\n );\n}\n"],"mappings":";AAmFM,SAIA,UAJA,KAIA,YAJA;AAjFN,SAAS,QAAQ,mBAAmB;AACpC,SAAS,qBAAqB;AAC9B,SAAS,cAAc;AAEvB,SAAS,mBAAmB;AAC5B,SAAS,uBAAuB;AAEhC,SAAS,wBAAwB;AAoD1B,SAAS,aAAa;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,cAAc;AAAA,EACd,WAAW;AAAA,EACX;AAAA,EACA;AAAA,EACA,QAAQ;AAAA,EACR;AAAA,EACA,GAAG;AACL,GAAsB;AACpB,QAAM,SAAS,iBAAiB,UAAU;AAC1C,QAAM,aAAa,gBAAgB,QAAQ,UAAU;AACrD,QAAM,QAAQ,cAAc,aAAa,KAAK;AAC9C,QAAM,OAAO,QAAQ,OAAO;AAC5B,QAAM,OAAO,aAAa,YAAY,SAAS,MAAM;AAErD,QAAM,UACJ,YAAY,SACV,oBAAC,UAAO,MAAM,aAAa,SAAQ,SAAQ,MACxC,iBAAO,SACV,IAEA,iCACG;AAAA,gBAAY,SACX,oBAAC,UAAO,MAAM,UAAU,SAAQ,aAAY,MACzC,iBAAO,cACV,IAEA,oBAAC,UAAO,MAAM,YAAY,SAAQ,aAAY,MAAY,WAAU,iBACjE,iBAAO,QACV;AAAA,IAEF,oBAAC,UAAO,MAAM,WAAW,MAAM,SAAQ,SAAQ,MAC5C,qBAAW,OACd;AAAA,KACF;AAGJ,SACE;AAAA,IAAC;AAAA;AAAA,MACE,GAAG;AAAA,MACJ;AAAA,MACA,OAAO,oBAAC,eAAY,MAAM,MAAM,MAAM,MAAM,MAAM,MAAM,IAAI,MAAM,eAAe,MAAM,eAAe;AAAA,MACtG,SACE,iCACG;AAAA;AAAA,QACA;AAAA,QACD,oBAAC,UAAK,aAAU,yBAAwB,WAAU,2CAC/C,mBACH;AAAA,SACF;AAAA;AAAA,EAEJ;AAEJ;","names":[]}
@@ -0,0 +1,231 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { LandingSession } from './landing-actions.js';
4
+ import '../delete-account-setting-CpIhzxxZ.js';
5
+ import '../components/tooltip.js';
6
+ import '../hooks/use-anchored-rect.js';
7
+ import '../lib/clipping.js';
8
+ import '../data-table-labels-DUELeXpa.js';
9
+ import '../components/data-table-sort.js';
10
+ import '../components/swipeable-row.js';
11
+ import '../components/mini-calendar.js';
12
+ import '../components/calendar-heatmap.js';
13
+ import '../components/popover.js';
14
+ import '../components/chip.js';
15
+ import 'lucide-react';
16
+ import '../components/field-sync.js';
17
+ import '../wizard/types.js';
18
+ import '../tour/tour.js';
19
+ import '../search/command-palette.js';
20
+ import '../search/global-search.js';
21
+ import '../search/search-index.js';
22
+ import '../components/month-picker.js';
23
+ import '../components/page-contents.js';
24
+ import '../components/series-chart-labels.js';
25
+ import '../components/pie-chart-labels.js';
26
+ import '../components/sparkline.js';
27
+ import '../components/stat-tile.js';
28
+ import '../components/signature-pad.js';
29
+ import '../components/password-strength.js';
30
+ import '../components/swatch-picker.js';
31
+ import '../components/tile-radio.js';
32
+ import '../components/icon-picker.js';
33
+ import '../components/dialog-frame.js';
34
+ import '../components/modal.js';
35
+ import '../hooks/use-search-param-state.js';
36
+ import '../components/measured-grid.js';
37
+ import '../components/user-avatar.js';
38
+ import '../lib/person-name.js';
39
+ import '../components/status-dot.js';
40
+ import '../components/progress-bar.js';
41
+ import '../components/signed-amount.js';
42
+ import '../shell/topbar-action-menu.js';
43
+ import '../components/menu-item.js';
44
+ import '../components/floating-panel.js';
45
+ import '../hooks/use-authed-src.js';
46
+ import '../components/account-settings-labels.js';
47
+ import '../components/bulk-action-bar.js';
48
+ import '../components/list.js';
49
+ import '../components/breadcrumbs.js';
50
+ import '../components/toast.js';
51
+ import '../components/description-list.js';
52
+ import '../components/line-items.js';
53
+ import '../components/error-boundary.js';
54
+ import '../components/authed-image.js';
55
+ import '../components/image-grid.js';
56
+ import '../components/lightbox.js';
57
+ import '../components/write-lock.js';
58
+ import '../auth/sign-in-form.js';
59
+ import './access.js';
60
+ import './landing-labels.js';
61
+ import '../shell/topbar-controls.js';
62
+ import '../theme/palette-presets.js';
63
+ import '../theme/chart-palette.js';
64
+ import '../theme/theme-store.js';
65
+ import 'zustand/middleware';
66
+ import 'zustand';
67
+ import '../auth/complete-name-dialog.js';
68
+ import '../auth/forgot-password-form.js';
69
+ import '../auth/status-parts.js';
70
+ import '../auth/reset-password-form.js';
71
+ import '../auth/verify-email.js';
72
+ import '../auth/not-found-page.js';
73
+ import '../auth/accept-invitation.js';
74
+ import '../shell/company-switcher.js';
75
+ import '../account/email-change-setting.js';
76
+ import '../account/sessions-setting.js';
77
+ import '../lib/format.js';
78
+ import '../account/data-export-setting.js';
79
+ import '../components/account-chips.js';
80
+ import '../admin/admin-parts.js';
81
+ import '../admin/admin-action-log.js';
82
+ import '../settings/settings-labels.js';
83
+ import '../demo/demo-labels.js';
84
+ import '../components/reauth-dialog.js';
85
+ import '../components/server-wake.js';
86
+ import '../lib/server-wake.js';
87
+ import '../components/translation-review-labels.js';
88
+ import '../lib/translation-review.js';
89
+ import '../components/country-select.js';
90
+ import '../components/inline-edit-field.js';
91
+ import '../lib/iban.js';
92
+ import '../lib/phone.js';
93
+ import '../components/sign-chip.js';
94
+ import '../components/column-mapper.js';
95
+ import '../lib/column-mapping.js';
96
+ import '../lib/table-text.js';
97
+ import '../components/field-parts.js';
98
+
99
+ /**
100
+ * 0.31.1: a session that is still being restored — an app whose access token lives in
101
+ * memory reads it back from its refresh token first (kastlan's 0.31 adoption). Until
102
+ * then `/` neither shows the landing nor resumes, and a public page neither shows nor
103
+ * redirects: both render `loading` instead.
104
+ */
105
+ type RoutingSession = LandingSession | "loading";
106
+ /**
107
+ * The public routes' logic (docs/landing-demo-harmonization.md §3): what `/` shows,
108
+ * which pages a session skips, and the page a session resumes on — keksdose's
109
+ * `RootEntry`, `RedirectIfAuthed` and use-last-visited-page.ts, for every app.
110
+ * Router-aware (react-router), so they belong to the `./shell` entry.
111
+ */
112
+ /** The query parameter that carries "where I was actually trying to go" through the
113
+ * sign-in bounce (keksdose's `NEXT_PARAM`). */
114
+ declare const NEXT_PARAM = "next";
115
+ /**
116
+ * A `?next=` value that is safe to navigate to, or null.
117
+ *
118
+ * Same-origin PATHS only, decided by the URL PARSER rather than by string prefix —
119
+ * this is the open-redirect check, and it is the reason this lives in one place
120
+ * instead of at each call site.
121
+ *
122
+ * The prefix version ("starts with `/`, does not start with `//`") let two shapes
123
+ * through that the browser resolves cross-origin, because the WHATWG parser does not
124
+ * read them as text: a backslash after the leading slash counts as a second slash for
125
+ * a special scheme, so `/\evil.example` resolves to `https://evil.example/`; and tab,
126
+ * LF and CR are STRIPPED anywhere in the input before parsing, so `/<TAB>/evil.example`
127
+ * becomes `//evil.example`. Asking the same parser the browser will use closes every
128
+ * such shape at once, including ones nobody has thought of yet, and it normalises what
129
+ * comes back instead of echoing the caller's spelling.
130
+ *
131
+ * keksdose's (shared/hooks/use-last-visited-page.ts), verbatim but for the fallback
132
+ * origin.
133
+ */
134
+ declare function safeNextPath(raw: string | null | undefined): string | null;
135
+ /** A path to leave out: a string is the path and everything under it (`/demo` is
136
+ * `/demo` and `/demo/ended`; `/` is only `/`), a RegExp is tested on the pathname. */
137
+ type PathPattern = string | RegExp;
138
+ /**
139
+ * The pages never recorded as a resume target (§3.2): the landing (`/`, `/welcome`), the
140
+ * auth pages, the demo's own (`/demo*`) and the legal pages. The 404 has no path of its
141
+ * own — call {@link useLastVisitedPage} inside the signed-in layout, as keksdose does,
142
+ * and it never sees one. The app adds its own one-shot routes (keksdose:
143
+ * `/banks/callback`, `/join`, `/share-target`) through `exclude`.
144
+ */
145
+ declare const DEFAULT_LAST_VISITED_EXCLUDES: readonly PathPattern[];
146
+ interface LastVisitedPageOptions {
147
+ /** The device key: `<app>.lastVisitedPage` (§3.2), one per app on the origin. */
148
+ key: string;
149
+ /** The app's own paths to leave out, ON TOP of {@link DEFAULT_LAST_VISITED_EXCLUDES}. */
150
+ exclude?: readonly PathPattern[];
151
+ /** `false` records nothing — while the session is still being restored, say. */
152
+ enabled?: boolean;
153
+ }
154
+ /**
155
+ * Remember the page a session is on (path and query), on every navigation, so `/` — a
156
+ * revisit, a fresh sign-in, the installed PWA, whose `start_url` is `/` — resumes there
157
+ * (§3.2, keksdose feedback #59). Device-local, in `localStorage` under `key`; a blocked
158
+ * storage records nothing.
159
+ *
160
+ * Call it once in the signed-in layout. The excluded paths are never written, so a
161
+ * one-shot page (a bank callback, an invitation link) never becomes the place the next
162
+ * launch opens on.
163
+ */
164
+ declare function useLastVisitedPage({ key, exclude, enabled }: LastVisitedPageOptions): void;
165
+ /**
166
+ * The page {@link useLastVisitedPage} remembered under `key` — checked by
167
+ * {@link safeNextPath}, so a value someone planted in storage cannot leave the origin —
168
+ * or `fallback` (default `null`) when there is none or storage is blocked. A string
169
+ * fallback narrows the answer to a string, for react-router's `to`.
170
+ */
171
+ declare function readLastVisitedPage(key: string, fallback: string): string;
172
+ declare function readLastVisitedPage(key: string, fallback?: null): string | null;
173
+ /** Forget the remembered page — on an explicit sign-out, so the next account does not
174
+ * resume on the previous one's page. */
175
+ declare function clearLastVisitedPage(key: string): void;
176
+ interface RootEntryProps {
177
+ /** Without a session `/` is the landing; with one (real or demo) it resumes;
178
+ * `"loading"` while the app restores one. */
179
+ session: RoutingSession;
180
+ /** What shows while `session` is `"loading"`. Default: the kit's `LoadingState`. */
181
+ loading?: ReactNode;
182
+ /** The remembered page — `readLastVisitedPage(key)`. */
183
+ resumePath?: string | null;
184
+ /** The landing page element. */
185
+ landing: ReactNode;
186
+ /** The app's home: where a session goes with nothing to resume (keksdose `/budget`,
187
+ * kastlan and Kurvenschmiede `/dashboard`). */
188
+ home: string;
189
+ }
190
+ /**
191
+ * The `/` route (§3.1): the landing for a visitor, and for a session — real or demo —
192
+ * the last visited page, else the app's home. Indexed and canonical `/` for the visitor;
193
+ * a crawler never has a session, so for it `/` is stable.
194
+ *
195
+ * keksdose showed a session with nothing remembered the landing; the contract sends it
196
+ * home instead, because a session at `/` came to use the app. The landing stays one tap
197
+ * away at `/welcome`, where the signed-in brand link points.
198
+ */
199
+ declare function RootEntry({ session, resumePath, landing, home, loading }: RootEntryProps): react.JSX.Element;
200
+ interface RedirectIfAuthedProps {
201
+ /** `"loading"` while the app restores a session: neither the page nor a redirect. */
202
+ session: RoutingSession;
203
+ /** What shows while `session` is `"loading"`. Default: the kit's `LoadingState`. */
204
+ loading?: ReactNode;
205
+ /**
206
+ * Let a DEMO session through (§5.6): on `/login`, `/register` and the app's invitation
207
+ * pages, so a demo visitor can sign in or accept an invitation — a successful one
208
+ * replaces the demo session. Default `false`: on `/demo` a live demo continues instead
209
+ * of starting a second one (§5.2), and on `/forgot-password` a demo has no password.
210
+ */
211
+ allowDemo?: boolean;
212
+ /** Where a redirected session goes: the resume target —
213
+ * `readLastVisitedPage(key, home)`. A safe `?next=` goes first. */
214
+ fallback: string;
215
+ /** The page. Left out, an `<Outlet />`: the component is a layout route around the
216
+ * public pages, as in keksdose. */
217
+ children?: ReactNode;
218
+ }
219
+ /**
220
+ * Public pages a session skips (§3.1): a signed-in visitor at `/login` goes back into
221
+ * the app, to a safe `?next=` when the URL carries one (a share invitation or a push
222
+ * deep link opened with a live session in another tab — keksdose) and else to
223
+ * `fallback`.
224
+ *
225
+ * A demo session passes where `allowDemo` says so (§5.6). Mount one wrapper with
226
+ * `allowDemo` around `/login`, `/register` and the invitation pages, and one without
227
+ * around `/demo` (and `/forgot-password`).
228
+ */
229
+ declare function RedirectIfAuthed({ session, allowDemo, fallback, children, loading }: RedirectIfAuthedProps): react.JSX.Element;
230
+
231
+ export { DEFAULT_LAST_VISITED_EXCLUDES, type LastVisitedPageOptions, NEXT_PARAM, type PathPattern, RedirectIfAuthed, type RedirectIfAuthedProps, RootEntry, type RootEntryProps, type RoutingSession, clearLastVisitedPage, readLastVisitedPage, safeNextPath, useLastVisitedPage };
@@ -0,0 +1,84 @@
1
+ "use client";
2
+ import { Fragment, jsx } from "react/jsx-runtime";
3
+ import { useEffect } from "react";
4
+ import { Navigate, Outlet, useLocation } from "react-router";
5
+ import { readStored, writeStored } from "../lib/safe-storage.js";
6
+ import { LoadingState } from "../components/loading-state.js";
7
+ const NEXT_PARAM = "next";
8
+ function safeNextPath(raw) {
9
+ if (!raw) return null;
10
+ const base = typeof window !== "undefined" ? window.location.origin : "https://kit.invalid";
11
+ try {
12
+ const url = new URL(raw, base);
13
+ if (url.origin !== base) return null;
14
+ return `${url.pathname}${url.search}${url.hash}`;
15
+ } catch {
16
+ return null;
17
+ }
18
+ }
19
+ const DEFAULT_LAST_VISITED_EXCLUDES = Object.freeze([
20
+ "/",
21
+ "/welcome",
22
+ "/login",
23
+ "/register",
24
+ "/forgot-password",
25
+ "/reset-password",
26
+ "/verify-email",
27
+ "/demo",
28
+ "/impressum",
29
+ "/privacy",
30
+ "/terms"
31
+ ]);
32
+ function matchesPath(pathname, pattern) {
33
+ if (typeof pattern !== "string") return new RegExp(pattern.source, pattern.flags.replace("g", "")).test(pathname);
34
+ if (pattern === "/") return pathname === "/";
35
+ const base = pattern.endsWith("/") ? pattern.slice(0, -1) : pattern;
36
+ return pathname === base || pathname.startsWith(`${base}/`);
37
+ }
38
+ function isExcluded(pathname, exclude) {
39
+ return [...DEFAULT_LAST_VISITED_EXCLUDES, ...exclude].some((pattern) => matchesPath(pathname, pattern));
40
+ }
41
+ function useLastVisitedPage({ key, exclude = [], enabled = true }) {
42
+ const { pathname, search } = useLocation();
43
+ const skip = !enabled || isExcluded(pathname, exclude);
44
+ useEffect(() => {
45
+ if (skip) return;
46
+ writeStored(key, pathname + search);
47
+ }, [key, pathname, search, skip]);
48
+ }
49
+ function readLastVisitedPage(key, fallback = null) {
50
+ return safeNextPath(readStored(key)) ?? fallback;
51
+ }
52
+ function clearLastVisitedPage(key) {
53
+ if (typeof window === "undefined") return;
54
+ try {
55
+ window.localStorage.removeItem(key);
56
+ } catch {
57
+ }
58
+ }
59
+ function RootEntry({ session, resumePath, landing, home, loading }) {
60
+ if (session === "loading") return /* @__PURE__ */ jsx(Fragment, { children: loading ?? /* @__PURE__ */ jsx(LoadingState, {}) });
61
+ if (session === "none") return /* @__PURE__ */ jsx(Fragment, { children: landing });
62
+ const resume = safeNextPath(resumePath);
63
+ return /* @__PURE__ */ jsx(Navigate, { to: resume && resume !== "/" ? resume : home, replace: true });
64
+ }
65
+ function RedirectIfAuthed({ session, allowDemo = false, fallback, children, loading }) {
66
+ const { search } = useLocation();
67
+ if (session === "loading") return /* @__PURE__ */ jsx(Fragment, { children: loading ?? /* @__PURE__ */ jsx(LoadingState, {}) });
68
+ if (session === "none" || session === "demo" && allowDemo) {
69
+ return children === void 0 ? /* @__PURE__ */ jsx(Outlet, {}) : /* @__PURE__ */ jsx(Fragment, { children });
70
+ }
71
+ const next = safeNextPath(new URLSearchParams(search).get(NEXT_PARAM));
72
+ return /* @__PURE__ */ jsx(Navigate, { to: next ?? fallback, replace: true });
73
+ }
74
+ export {
75
+ DEFAULT_LAST_VISITED_EXCLUDES,
76
+ NEXT_PARAM,
77
+ RedirectIfAuthed,
78
+ RootEntry,
79
+ clearLastVisitedPage,
80
+ readLastVisitedPage,
81
+ safeNextPath,
82
+ useLastVisitedPage
83
+ };
84
+ //# sourceMappingURL=routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/landing/routing.tsx"],"sourcesContent":["import { useEffect } from \"react\";\nimport type { ReactNode } from \"react\";\nimport { Navigate, Outlet, useLocation } from \"react-router\";\n\nimport { readStored, writeStored } from \"../lib/safe-storage\";\nimport type { LandingSession } from \"./landing-actions\";\nimport { LoadingState } from \"../components/loading-state\";\n\n/**\n * 0.31.1: a session that is still being restored — an app whose access token lives in\n * memory reads it back from its refresh token first (kastlan's 0.31 adoption). Until\n * then `/` neither shows the landing nor resumes, and a public page neither shows nor\n * redirects: both render `loading` instead.\n */\nexport type RoutingSession = LandingSession | \"loading\";\n\n/**\n * The public routes' logic (docs/landing-demo-harmonization.md §3): what `/` shows,\n * which pages a session skips, and the page a session resumes on — keksdose's\n * `RootEntry`, `RedirectIfAuthed` and use-last-visited-page.ts, for every app.\n * Router-aware (react-router), so they belong to the `./shell` entry.\n */\n\n/* ── ?next= ──────────────────────────────────────────────────────────────── */\n\n/** The query parameter that carries \"where I was actually trying to go\" through the\n * sign-in bounce (keksdose's `NEXT_PARAM`). */\nexport const NEXT_PARAM = \"next\";\n\n/**\n * A `?next=` value that is safe to navigate to, or null.\n *\n * Same-origin PATHS only, decided by the URL PARSER rather than by string prefix —\n * this is the open-redirect check, and it is the reason this lives in one place\n * instead of at each call site.\n *\n * The prefix version (\"starts with `/`, does not start with `//`\") let two shapes\n * through that the browser resolves cross-origin, because the WHATWG parser does not\n * read them as text: a backslash after the leading slash counts as a second slash for\n * a special scheme, so `/\\evil.example` resolves to `https://evil.example/`; and tab,\n * LF and CR are STRIPPED anywhere in the input before parsing, so `/<TAB>/evil.example`\n * becomes `//evil.example`. Asking the same parser the browser will use closes every\n * such shape at once, including ones nobody has thought of yet, and it normalises what\n * comes back instead of echoing the caller's spelling.\n *\n * keksdose's (shared/hooks/use-last-visited-page.ts), verbatim but for the fallback\n * origin.\n */\nexport function safeNextPath(raw: string | null | undefined): string | null {\n if (!raw) return null;\n const base = typeof window !== \"undefined\" ? window.location.origin : \"https://kit.invalid\";\n try {\n const url = new URL(raw, base);\n if (url.origin !== base) return null;\n return `${url.pathname}${url.search}${url.hash}`;\n } catch {\n return null;\n }\n}\n\n/* ── The last visited page (§3.2) ────────────────────────────────────────── */\n\n/** A path to leave out: a string is the path and everything under it (`/demo` is\n * `/demo` and `/demo/ended`; `/` is only `/`), a RegExp is tested on the pathname. */\nexport type PathPattern = string | RegExp;\n\n/**\n * The pages never recorded as a resume target (§3.2): the landing (`/`, `/welcome`), the\n * auth pages, the demo's own (`/demo*`) and the legal pages. The 404 has no path of its\n * own — call {@link useLastVisitedPage} inside the signed-in layout, as keksdose does,\n * and it never sees one. The app adds its own one-shot routes (keksdose:\n * `/banks/callback`, `/join`, `/share-target`) through `exclude`.\n */\nexport const DEFAULT_LAST_VISITED_EXCLUDES: readonly PathPattern[] = Object.freeze([\n \"/\",\n \"/welcome\",\n \"/login\",\n \"/register\",\n \"/forgot-password\",\n \"/reset-password\",\n \"/verify-email\",\n \"/demo\",\n \"/impressum\",\n \"/privacy\",\n \"/terms\",\n]);\n\nfunction matchesPath(pathname: string, pattern: PathPattern): boolean {\n if (typeof pattern !== \"string\") return new RegExp(pattern.source, pattern.flags.replace(\"g\", \"\")).test(pathname);\n if (pattern === \"/\") return pathname === \"/\";\n const base = pattern.endsWith(\"/\") ? pattern.slice(0, -1) : pattern;\n return pathname === base || pathname.startsWith(`${base}/`);\n}\n\n/** Whether `pathname` is one of the defaults or the app's `exclude`. */\nfunction isExcluded(pathname: string, exclude: readonly PathPattern[]): boolean {\n return [...DEFAULT_LAST_VISITED_EXCLUDES, ...exclude].some((pattern) => matchesPath(pathname, pattern));\n}\n\nexport interface LastVisitedPageOptions {\n /** The device key: `<app>.lastVisitedPage` (§3.2), one per app on the origin. */\n key: string;\n /** The app's own paths to leave out, ON TOP of {@link DEFAULT_LAST_VISITED_EXCLUDES}. */\n exclude?: readonly PathPattern[];\n /** `false` records nothing — while the session is still being restored, say. */\n enabled?: boolean;\n}\n\n/**\n * Remember the page a session is on (path and query), on every navigation, so `/` — a\n * revisit, a fresh sign-in, the installed PWA, whose `start_url` is `/` — resumes there\n * (§3.2, keksdose feedback #59). Device-local, in `localStorage` under `key`; a blocked\n * storage records nothing.\n *\n * Call it once in the signed-in layout. The excluded paths are never written, so a\n * one-shot page (a bank callback, an invitation link) never becomes the place the next\n * launch opens on.\n */\nexport function useLastVisitedPage({ key, exclude = [], enabled = true }: LastVisitedPageOptions): void {\n const { pathname, search } = useLocation();\n // Decided here, so the effect depends on the answer and not on an `exclude` array the\n // caller likely builds inline.\n const skip = !enabled || isExcluded(pathname, exclude);\n useEffect(() => {\n if (skip) return;\n writeStored(key, pathname + search);\n }, [key, pathname, search, skip]);\n}\n\n/**\n * The page {@link useLastVisitedPage} remembered under `key` — checked by\n * {@link safeNextPath}, so a value someone planted in storage cannot leave the origin —\n * or `fallback` (default `null`) when there is none or storage is blocked. A string\n * fallback narrows the answer to a string, for react-router's `to`.\n */\nexport function readLastVisitedPage(key: string, fallback: string): string;\nexport function readLastVisitedPage(key: string, fallback?: null): string | null;\nexport function readLastVisitedPage(key: string, fallback: string | null = null): string | null {\n return safeNextPath(readStored(key)) ?? fallback;\n}\n\n/** Forget the remembered page — on an explicit sign-out, so the next account does not\n * resume on the previous one's page. */\nexport function clearLastVisitedPage(key: string): void {\n if (typeof window === \"undefined\") return;\n try {\n window.localStorage.removeItem(key);\n } catch {\n // storage blocked: there is nothing to forget\n }\n}\n\n/* ── RootEntry ───────────────────────────────────────────────────────────── */\n\nexport interface RootEntryProps {\n /** Without a session `/` is the landing; with one (real or demo) it resumes;\n * `\"loading\"` while the app restores one. */\n session: RoutingSession;\n /** What shows while `session` is `\"loading\"`. Default: the kit's `LoadingState`. */\n loading?: ReactNode;\n /** The remembered page — `readLastVisitedPage(key)`. */\n resumePath?: string | null;\n /** The landing page element. */\n landing: ReactNode;\n /** The app's home: where a session goes with nothing to resume (keksdose `/budget`,\n * kastlan and Kurvenschmiede `/dashboard`). */\n home: string;\n}\n\n/**\n * The `/` route (§3.1): the landing for a visitor, and for a session — real or demo —\n * the last visited page, else the app's home. Indexed and canonical `/` for the visitor;\n * a crawler never has a session, so for it `/` is stable.\n *\n * keksdose showed a session with nothing remembered the landing; the contract sends it\n * home instead, because a session at `/` came to use the app. The landing stays one tap\n * away at `/welcome`, where the signed-in brand link points.\n */\nexport function RootEntry({ session, resumePath, landing, home, loading }: RootEntryProps) {\n if (session === \"loading\") return <>{loading ?? <LoadingState />}</>;\n if (session === \"none\") return <>{landing}</>;\n const resume = safeNextPath(resumePath);\n return <Navigate to={resume && resume !== \"/\" ? resume : home} replace />;\n}\n\n/* ── RedirectIfAuthed ────────────────────────────────────────────────────── */\n\nexport interface RedirectIfAuthedProps {\n /** `\"loading\"` while the app restores a session: neither the page nor a redirect. */\n session: RoutingSession;\n /** What shows while `session` is `\"loading\"`. Default: the kit's `LoadingState`. */\n loading?: ReactNode;\n /**\n * Let a DEMO session through (§5.6): on `/login`, `/register` and the app's invitation\n * pages, so a demo visitor can sign in or accept an invitation — a successful one\n * replaces the demo session. Default `false`: on `/demo` a live demo continues instead\n * of starting a second one (§5.2), and on `/forgot-password` a demo has no password.\n */\n allowDemo?: boolean;\n /** Where a redirected session goes: the resume target —\n * `readLastVisitedPage(key, home)`. A safe `?next=` goes first. */\n fallback: string;\n /** The page. Left out, an `<Outlet />`: the component is a layout route around the\n * public pages, as in keksdose. */\n children?: ReactNode;\n}\n\n/**\n * Public pages a session skips (§3.1): a signed-in visitor at `/login` goes back into\n * the app, to a safe `?next=` when the URL carries one (a share invitation or a push\n * deep link opened with a live session in another tab — keksdose) and else to\n * `fallback`.\n *\n * A demo session passes where `allowDemo` says so (§5.6). Mount one wrapper with\n * `allowDemo` around `/login`, `/register` and the invitation pages, and one without\n * around `/demo` (and `/forgot-password`).\n */\nexport function RedirectIfAuthed({ session, allowDemo = false, fallback, children, loading }: RedirectIfAuthedProps) {\n const { search } = useLocation();\n if (session === \"loading\") return <>{loading ?? <LoadingState />}</>;\n if (session === \"none\" || (session === \"demo\" && allowDemo)) {\n return children === undefined ? <Outlet /> : <>{children}</>;\n }\n const next = safeNextPath(new URLSearchParams(search).get(NEXT_PARAM));\n return <Navigate to={next ?? fallback} replace />;\n}\n"],"mappings":";AAmLoC,mBAAc,WAAd;AAnLpC,SAAS,iBAAiB;AAE1B,SAAS,UAAU,QAAQ,mBAAmB;AAE9C,SAAS,YAAY,mBAAmB;AAExC,SAAS,oBAAoB;AAqBtB,MAAM,aAAa;AAqBnB,SAAS,aAAa,KAA+C;AAC1E,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,OAAO,OAAO,WAAW,cAAc,OAAO,SAAS,SAAS;AACtE,MAAI;AACF,UAAM,MAAM,IAAI,IAAI,KAAK,IAAI;AAC7B,QAAI,IAAI,WAAW,KAAM,QAAO;AAChC,WAAO,GAAG,IAAI,QAAQ,GAAG,IAAI,MAAM,GAAG,IAAI,IAAI;AAAA,EAChD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAeO,MAAM,gCAAwD,OAAO,OAAO;AAAA,EACjF;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,SAAS,YAAY,UAAkB,SAA+B;AACpE,MAAI,OAAO,YAAY,SAAU,QAAO,IAAI,OAAO,QAAQ,QAAQ,QAAQ,MAAM,QAAQ,KAAK,EAAE,CAAC,EAAE,KAAK,QAAQ;AAChH,MAAI,YAAY,IAAK,QAAO,aAAa;AACzC,QAAM,OAAO,QAAQ,SAAS,GAAG,IAAI,QAAQ,MAAM,GAAG,EAAE,IAAI;AAC5D,SAAO,aAAa,QAAQ,SAAS,WAAW,GAAG,IAAI,GAAG;AAC5D;AAGA,SAAS,WAAW,UAAkB,SAA0C;AAC9E,SAAO,CAAC,GAAG,+BAA+B,GAAG,OAAO,EAAE,KAAK,CAAC,YAAY,YAAY,UAAU,OAAO,CAAC;AACxG;AAqBO,SAAS,mBAAmB,EAAE,KAAK,UAAU,CAAC,GAAG,UAAU,KAAK,GAAiC;AACtG,QAAM,EAAE,UAAU,OAAO,IAAI,YAAY;AAGzC,QAAM,OAAO,CAAC,WAAW,WAAW,UAAU,OAAO;AACrD,YAAU,MAAM;AACd,QAAI,KAAM;AACV,gBAAY,KAAK,WAAW,MAAM;AAAA,EACpC,GAAG,CAAC,KAAK,UAAU,QAAQ,IAAI,CAAC;AAClC;AAUO,SAAS,oBAAoB,KAAa,WAA0B,MAAqB;AAC9F,SAAO,aAAa,WAAW,GAAG,CAAC,KAAK;AAC1C;AAIO,SAAS,qBAAqB,KAAmB;AACtD,MAAI,OAAO,WAAW,YAAa;AACnC,MAAI;AACF,WAAO,aAAa,WAAW,GAAG;AAAA,EACpC,QAAQ;AAAA,EAER;AACF;AA4BO,SAAS,UAAU,EAAE,SAAS,YAAY,SAAS,MAAM,QAAQ,GAAmB;AACzF,MAAI,YAAY,UAAW,QAAO,gCAAG,qBAAW,oBAAC,gBAAa,GAAG;AACjE,MAAI,YAAY,OAAQ,QAAO,gCAAG,mBAAQ;AAC1C,QAAM,SAAS,aAAa,UAAU;AACtC,SAAO,oBAAC,YAAS,IAAI,UAAU,WAAW,MAAM,SAAS,MAAM,SAAO,MAAC;AACzE;AAkCO,SAAS,iBAAiB,EAAE,SAAS,YAAY,OAAO,UAAU,UAAU,QAAQ,GAA0B;AACnH,QAAM,EAAE,OAAO,IAAI,YAAY;AAC/B,MAAI,YAAY,UAAW,QAAO,gCAAG,qBAAW,oBAAC,gBAAa,GAAG;AACjE,MAAI,YAAY,UAAW,YAAY,UAAU,WAAY;AAC3D,WAAO,aAAa,SAAY,oBAAC,UAAO,IAAK,gCAAG,UAAS;AAAA,EAC3D;AACA,QAAM,OAAO,aAAa,IAAI,gBAAgB,MAAM,EAAE,IAAI,UAAU,CAAC;AACrE,SAAO,oBAAC,YAAS,IAAI,QAAQ,UAAU,SAAO,MAAC;AACjD;","names":[]}
@@ -1,7 +1,7 @@
1
1
  import * as react from 'react';
2
2
  import { ReactNode, ReactElement } from 'react';
3
3
  import { FieldValues, FieldPath, ControllerProps, ControllerRenderProps, ControllerFieldState } from 'react-hook-form';
4
- import { dt as IbanInputProps, eg as PhoneInputProps, eD as SelectProps, dD as InputProps, eY as TextareaProps } from '../delete-account-setting-CGVMckMw.js';
4
+ import { dt as IbanInputProps, eg as PhoneInputProps, eD as SelectProps, dD as InputProps, eY as TextareaProps } from '../delete-account-setting-CpIhzxxZ.js';
5
5
  import { NumberFieldProps } from '../components/number-field.js';
6
6
  import { DatePickerProps, DateRangePickerProps } from '../components/date-picker.js';
7
7
  import { TimeInputProps } from '../components/time-input.js';
@@ -65,6 +65,8 @@ import '../components/image-grid.js';
65
65
  import '../components/lightbox.js';
66
66
  import '../components/write-lock.js';
67
67
  import '../auth/sign-in-form.js';
68
+ import '../landing/access.js';
69
+ import '../landing/landing-labels.js';
68
70
  import '../shell/topbar-controls.js';
69
71
  import '../theme/palette-presets.js';
70
72
  import '../theme/chart-palette.js';
@@ -86,6 +88,8 @@ import '../account/data-export-setting.js';
86
88
  import '../components/account-chips.js';
87
89
  import '../admin/admin-parts.js';
88
90
  import '../admin/admin-action-log.js';
91
+ import '../settings/settings-labels.js';
92
+ import '../demo/demo-labels.js';
89
93
  import '../components/reauth-dialog.js';
90
94
  import '../components/server-wake.js';
91
95
  import '../lib/server-wake.js';