@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,218 @@
1
+ import * as react from 'react';
2
+ import { ComponentPropsWithoutRef, ReactNode } from 'react';
3
+ import { dY as LegalFooterProps } from '../delete-account-setting-CpIhzxxZ.js';
4
+ import { LandingActionsProps } from './landing-actions.js';
5
+ import { LandingLabels } from './landing-labels.js';
6
+ import '../components/tooltip.js';
7
+ import '../hooks/use-anchored-rect.js';
8
+ import '../lib/clipping.js';
9
+ import '../data-table-labels-DUELeXpa.js';
10
+ import '../components/data-table-sort.js';
11
+ import '../components/swipeable-row.js';
12
+ import '../components/mini-calendar.js';
13
+ import '../components/calendar-heatmap.js';
14
+ import '../components/popover.js';
15
+ import '../components/chip.js';
16
+ import 'lucide-react';
17
+ import '../components/field-sync.js';
18
+ import '../wizard/types.js';
19
+ import '../tour/tour.js';
20
+ import '../search/command-palette.js';
21
+ import '../search/global-search.js';
22
+ import '../search/search-index.js';
23
+ import '../components/month-picker.js';
24
+ import '../components/page-contents.js';
25
+ import '../components/series-chart-labels.js';
26
+ import '../components/pie-chart-labels.js';
27
+ import '../components/sparkline.js';
28
+ import '../components/stat-tile.js';
29
+ import '../components/signature-pad.js';
30
+ import '../components/password-strength.js';
31
+ import '../components/swatch-picker.js';
32
+ import '../components/tile-radio.js';
33
+ import '../components/icon-picker.js';
34
+ import '../components/dialog-frame.js';
35
+ import '../components/modal.js';
36
+ import '../hooks/use-search-param-state.js';
37
+ import '../components/measured-grid.js';
38
+ import '../components/user-avatar.js';
39
+ import '../lib/person-name.js';
40
+ import '../components/status-dot.js';
41
+ import '../components/progress-bar.js';
42
+ import '../components/signed-amount.js';
43
+ import '../shell/topbar-action-menu.js';
44
+ import '../components/menu-item.js';
45
+ import '../components/floating-panel.js';
46
+ import '../hooks/use-authed-src.js';
47
+ import '../components/account-settings-labels.js';
48
+ import '../components/bulk-action-bar.js';
49
+ import '../components/list.js';
50
+ import '../components/breadcrumbs.js';
51
+ import '../components/toast.js';
52
+ import '../components/description-list.js';
53
+ import '../components/line-items.js';
54
+ import '../components/error-boundary.js';
55
+ import '../components/authed-image.js';
56
+ import '../components/image-grid.js';
57
+ import '../components/lightbox.js';
58
+ import '../components/write-lock.js';
59
+ import '../auth/sign-in-form.js';
60
+ import './access.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
+ * The sections of the public landing page (docs/landing-demo-harmonization.md §4.1),
101
+ * top to bottom: `Hero`, `FeatureRows` of `FeatureRow`, `TrustStrip`, `CtaBand`,
102
+ * `PublicFooter` — keksdose's landing-page.tsx, taken apart.
103
+ *
104
+ * The kit draws the frame; every word of marketing copy is the app's. Colours are the
105
+ * kit's tokens (`--brand`, `--text-*`, `--bg-surface`, `--border`), so keksdose's ten
106
+ * hard-coded teal sites follow the palette now (§2.11).
107
+ *
108
+ * **Visuals are decorative** (§4.1): the `visual` slots are `aria-hidden` and `inert` —
109
+ * mock UI drawn with kit tokens, never screenshots with real data, and never anything a
110
+ * keyboard could land on. The copy beside each one carries the information. A mock's
111
+ * amounts use the demo data's currency, so the landing shows what "Try the demo" opens.
112
+ */
113
+ type HeadingLevel = "h1" | "h2" | "h3" | "h4" | "h5" | "h6";
114
+ /** The action pair's props, as `Hero` and `CtaBand` pass them on. */
115
+ type ActionProps = Pick<LandingActionsProps, "session" | "access" | "demoHref" | "openAppHref">;
116
+ interface HeroProps extends Omit<ComponentPropsWithoutRef<"section">, "title" | "children">, ActionProps {
117
+ /** The page's `h1`: what the app does, in a line. */
118
+ title: ReactNode;
119
+ /** Under it: for whom, and why. */
120
+ subtitle?: ReactNode;
121
+ /** The kit's "Beta" chip above the title. */
122
+ beta?: boolean;
123
+ /** A badge of the app's own in that place; wins over `beta`. */
124
+ badge?: ReactNode;
125
+ /** Under the actions, quiet: "Free while in beta. Your data stays yours." */
126
+ trust?: ReactNode;
127
+ /** The product glimpse beside the copy (below it on a phone). Decorative. */
128
+ visual?: ReactNode;
129
+ /** Replaces the action pair ({@link LandingActions}). */
130
+ actions?: ReactNode;
131
+ /** Default `h1`; lower where the hero is embedded in another page. */
132
+ headingAs?: HeadingLevel;
133
+ labels?: Partial<LandingLabels>;
134
+ }
135
+ /**
136
+ * The top of the landing (§4.1.2): an optional "Beta" badge, the `h1`, a subtitle, the
137
+ * action pair for the visitor's state ({@link LandingActions}: "Request access" and "Try
138
+ * the demo", or "Open app" for a session), a trust line, and a visual. Copy left and
139
+ * visual right from `lg` up, stacked below.
140
+ */
141
+ declare function Hero({ title, subtitle, beta, badge, trust, visual, actions, headingAs: Heading, session, access, demoHref, openAppHref, labels: labelsProp, className, ...rest }: HeroProps): react.JSX.Element;
142
+ type FeatureRowsProps = ComponentPropsWithoutRef<"section">;
143
+ /**
144
+ * The band that holds the feature rows (§4.1.3): two to four {@link FeatureRow}s on the
145
+ * surface colour, between hairlines. The rows alternate sides by themselves — the copy
146
+ * of every second row moves to the end from `lg` up — so the app lists them in order
147
+ * and nothing else.
148
+ */
149
+ declare function FeatureRows({ className, children, ...rest }: FeatureRowsProps): react.JSX.Element;
150
+ interface FeatureRowProps extends Omit<ComponentPropsWithoutRef<"div">, "title" | "children"> {
151
+ /** A lucide icon (or any SVG) before the eyebrow. Decorative. */
152
+ icon?: ReactNode;
153
+ /** The small uppercase line above the heading: "Capture", "Plan". */
154
+ eyebrow?: ReactNode;
155
+ /** The row's `h2`. */
156
+ title: ReactNode;
157
+ description?: ReactNode;
158
+ /** Up to three short points; the first three are shown. */
159
+ bullets?: readonly ReactNode[];
160
+ /** The mock beside the copy. Decorative. */
161
+ visual?: ReactNode;
162
+ /** Default `h2`. */
163
+ headingAs?: HeadingLevel;
164
+ }
165
+ /**
166
+ * One job of the app (§4.1.3), keksdose's "capture → budget → understand": an icon
167
+ * eyebrow, the `h2`, a description, up to three bullets, and a visual on the other side.
168
+ * Inside {@link FeatureRows} every second row puts its copy at the end (`group-even`),
169
+ * which is keksdose's alternation without an index to pass.
170
+ */
171
+ declare function FeatureRow({ icon, eyebrow, title, description, bullets, visual, headingAs: Heading, className, ...rest }: FeatureRowProps): react.JSX.Element;
172
+ interface TrustItem {
173
+ /** A lucide icon. Decorative. */
174
+ icon?: ReactNode;
175
+ /** The item's `h3`: "Hosted in Switzerland". */
176
+ title: ReactNode;
177
+ /** One sentence. */
178
+ description: ReactNode;
179
+ }
180
+ interface TrustStripProps extends Omit<ComponentPropsWithoutRef<"section">, "children"> {
181
+ /** Three or four (§4.1.4): hosting, security, data ownership, sharing. */
182
+ items: readonly TrustItem[];
183
+ /** The items' heading level. Default `h3`. */
184
+ headingAs?: HeadingLevel;
185
+ }
186
+ /**
187
+ * Quiet, start-aligned facts after the feature rows (§4.1.4) — an icon, an `h3` and one
188
+ * sentence each, on one row from `lg` up (three or four columns), two on a tablet, one
189
+ * on a phone. No marketing volume: keksdose's "trust strip".
190
+ */
191
+ declare function TrustStrip({ items, headingAs: Heading, className, ...rest }: TrustStripProps): react.JSX.Element;
192
+ interface CtaBandProps extends Omit<ComponentPropsWithoutRef<"section">, "title" | "children">, ActionProps {
193
+ /** The band's `h2`: the last ask. */
194
+ title: ReactNode;
195
+ subtitle?: ReactNode;
196
+ /** Replaces the action pair ({@link LandingActions}). */
197
+ actions?: ReactNode;
198
+ /** Default `h2`. */
199
+ headingAs?: HeadingLevel;
200
+ labels?: Partial<LandingLabels>;
201
+ }
202
+ /**
203
+ * The closing band before the footer (§4.1.5): a centred `h2`, a subtitle, and the same
204
+ * action pair as the hero.
205
+ */
206
+ declare function CtaBand({ title, subtitle, actions, headingAs: Heading, session, access, demoHref, openAppHref, labels, className, ...rest }: CtaBandProps): react.JSX.Element;
207
+ interface PublicFooterProps extends Omit<LegalFooterProps, "children"> {
208
+ /** The app's line under the legal links: "Self-hosted household budgeting." */
209
+ tagline?: ReactNode;
210
+ }
211
+ /**
212
+ * The public pages' footer (§4.1.6): the kit's {@link LegalFooter} — the three legal
213
+ * links — with the app's tagline under them. keksdose's `SiteFooter`, which was already
214
+ * this.
215
+ */
216
+ declare function PublicFooter({ tagline, ...rest }: PublicFooterProps): react.JSX.Element;
217
+
218
+ export { CtaBand, type CtaBandProps, FeatureRow, type FeatureRowProps, FeatureRows, type FeatureRowsProps, Hero, type HeroProps, PublicFooter, type PublicFooterProps, type TrustItem, TrustStrip, type TrustStripProps };
@@ -0,0 +1,167 @@
1
+ "use client";
2
+ import { jsx, jsxs } from "react/jsx-runtime";
3
+ import { cn } from "../lib/cn.js";
4
+ import { Chip } from "../components/chip.js";
5
+ import { LegalFooter } from "../components/legal.js";
6
+ import { LandingActions } from "./landing-actions.js";
7
+ import { useLandingLabels } from "./landing-labels.js";
8
+ function Visual({ children, className }) {
9
+ return /* @__PURE__ */ jsx("div", { "aria-hidden": true, inert: true, "data-slot": "landing-visual", className: cn("mx-auto w-full select-none", className), children });
10
+ }
11
+ function Hero({
12
+ title,
13
+ subtitle,
14
+ beta = false,
15
+ badge,
16
+ trust,
17
+ visual,
18
+ actions,
19
+ headingAs: Heading = "h1",
20
+ session,
21
+ access,
22
+ demoHref,
23
+ openAppHref,
24
+ labels: labelsProp,
25
+ className,
26
+ ...rest
27
+ }) {
28
+ const labels = useLandingLabels(labelsProp);
29
+ const shownBadge = badge ?? (beta ? (
30
+ // keksdose's chip, padded out to the hero's scale.
31
+ /* @__PURE__ */ jsx(Chip, { size: "sm", tone: "warning", className: "px-3 py-1 font-medium", children: labels.beta })
32
+ ) : null);
33
+ return /* @__PURE__ */ jsxs(
34
+ "section",
35
+ {
36
+ ...rest,
37
+ "data-slot": "landing-hero",
38
+ className: cn(
39
+ "mx-auto grid max-w-6xl items-center gap-12 px-4 py-16 sm:py-24",
40
+ visual != null && "lg:grid-cols-2",
41
+ className
42
+ ),
43
+ children: [
44
+ /* @__PURE__ */ jsxs("div", { children: [
45
+ shownBadge != null && /* @__PURE__ */ jsx("div", { className: "mb-4", children: shownBadge }),
46
+ /* @__PURE__ */ jsx(Heading, { className: "text-4xl font-bold tracking-tight text-balance text-[var(--text-primary)] sm:text-5xl", children: title }),
47
+ subtitle != null && /* @__PURE__ */ jsx("p", { className: "mt-4 max-w-xl text-lg text-[var(--text-secondary)]", children: subtitle }),
48
+ /* @__PURE__ */ jsx("div", { className: "mt-8", children: actions ?? /* @__PURE__ */ jsx(
49
+ LandingActions,
50
+ {
51
+ session,
52
+ access,
53
+ demoHref,
54
+ openAppHref,
55
+ labels: labelsProp
56
+ }
57
+ ) }),
58
+ trust != null && /* @__PURE__ */ jsx("p", { className: "mt-4 text-sm text-[var(--text-muted)]", children: trust })
59
+ ] }),
60
+ visual != null && /* @__PURE__ */ jsx(Visual, { className: "max-w-sm", children: visual })
61
+ ]
62
+ }
63
+ );
64
+ }
65
+ function FeatureRows({ className, children, ...rest }) {
66
+ return /* @__PURE__ */ jsx(
67
+ "section",
68
+ {
69
+ ...rest,
70
+ "data-slot": "landing-feature-rows",
71
+ className: cn("border-t border-[var(--border)] bg-[var(--bg-surface)]", className),
72
+ children: /* @__PURE__ */ jsx("div", { className: "mx-auto max-w-6xl px-4 py-8 sm:py-12", children })
73
+ }
74
+ );
75
+ }
76
+ function FeatureRow({
77
+ icon,
78
+ eyebrow,
79
+ title,
80
+ description,
81
+ bullets,
82
+ visual,
83
+ headingAs: Heading = "h2",
84
+ className,
85
+ ...rest
86
+ }) {
87
+ const shown = bullets?.slice(0, 3) ?? [];
88
+ return /* @__PURE__ */ jsxs(
89
+ "div",
90
+ {
91
+ ...rest,
92
+ "data-slot": "landing-feature-row",
93
+ className: cn("group/feature grid items-center gap-8 py-10", visual != null && "lg:grid-cols-2 lg:gap-16", className),
94
+ children: [
95
+ /* @__PURE__ */ jsxs("div", { className: "lg:group-even/feature:order-last", children: [
96
+ (icon != null || eyebrow != null) && /* @__PURE__ */ jsxs("p", { className: "flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-[var(--brand)]", children: [
97
+ icon != null && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex shrink-0 [&_svg]:size-4", children: icon }),
98
+ eyebrow
99
+ ] }),
100
+ /* @__PURE__ */ jsx(Heading, { className: "mt-2 text-2xl font-semibold tracking-tight text-balance text-[var(--text-primary)] sm:text-3xl", children: title }),
101
+ description != null && /* @__PURE__ */ jsx("p", { className: "mt-3 max-w-xl text-[var(--text-secondary)]", children: description }),
102
+ shown.length > 0 && /* @__PURE__ */ jsx("ul", { className: "mt-4 space-y-2", children: shown.map((bullet, i) => /* @__PURE__ */ jsxs("li", { className: "flex gap-2.5 text-sm text-[var(--text-secondary)]", children: [
103
+ /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "mt-1.5 size-1.5 shrink-0 rounded-full bg-[var(--brand)]" }),
104
+ /* @__PURE__ */ jsx("span", { className: "min-w-0", children: bullet })
105
+ ] }, i)) })
106
+ ] }),
107
+ visual != null && /* @__PURE__ */ jsx(Visual, { className: "max-w-md", children: visual })
108
+ ]
109
+ }
110
+ );
111
+ }
112
+ function TrustStrip({ items, headingAs: Heading = "h3", className, ...rest }) {
113
+ return /* @__PURE__ */ jsx("section", { ...rest, "data-slot": "landing-trust", className: cn("mx-auto max-w-6xl px-4 py-12 sm:py-16", className), children: /* @__PURE__ */ jsx("ul", { className: cn("grid gap-8 sm:grid-cols-2", items.length === 3 ? "lg:grid-cols-3" : "lg:grid-cols-4"), children: items.map((item, i) => /* @__PURE__ */ jsxs("li", { children: [
114
+ item.icon != null && /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex text-[var(--brand)] [&_svg]:size-5", children: item.icon }),
115
+ /* @__PURE__ */ jsx(Heading, { className: "mt-2 text-sm font-semibold text-[var(--text-primary)]", children: item.title }),
116
+ /* @__PURE__ */ jsx("p", { className: "mt-1 text-sm text-[var(--text-secondary)]", children: item.description })
117
+ ] }, i)) }) });
118
+ }
119
+ function CtaBand({
120
+ title,
121
+ subtitle,
122
+ actions,
123
+ headingAs: Heading = "h2",
124
+ session,
125
+ access,
126
+ demoHref,
127
+ openAppHref,
128
+ labels,
129
+ className,
130
+ ...rest
131
+ }) {
132
+ return /* @__PURE__ */ jsx(
133
+ "section",
134
+ {
135
+ ...rest,
136
+ "data-slot": "landing-cta",
137
+ className: cn("border-t border-[var(--border)] bg-[var(--bg-surface)]", className),
138
+ children: /* @__PURE__ */ jsxs("div", { className: "mx-auto max-w-5xl px-4 py-16 text-center", children: [
139
+ /* @__PURE__ */ jsx(Heading, { className: "text-2xl font-semibold tracking-tight text-balance text-[var(--text-primary)] sm:text-3xl", children: title }),
140
+ subtitle != null && /* @__PURE__ */ jsx("p", { className: "mx-auto mt-3 max-w-xl text-[var(--text-secondary)]", children: subtitle }),
141
+ /* @__PURE__ */ jsx("div", { className: "mt-8", children: actions ?? /* @__PURE__ */ jsx(
142
+ LandingActions,
143
+ {
144
+ session,
145
+ access,
146
+ demoHref,
147
+ openAppHref,
148
+ align: "center",
149
+ labels
150
+ }
151
+ ) })
152
+ ] })
153
+ }
154
+ );
155
+ }
156
+ function PublicFooter({ tagline, ...rest }) {
157
+ return /* @__PURE__ */ jsx(LegalFooter, { ...rest, children: tagline });
158
+ }
159
+ export {
160
+ CtaBand,
161
+ FeatureRow,
162
+ FeatureRows,
163
+ Hero,
164
+ PublicFooter,
165
+ TrustStrip
166
+ };
167
+ //# sourceMappingURL=landing-sections.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/landing/landing-sections.tsx"],"sourcesContent":["import type { ComponentPropsWithoutRef, ReactNode } from \"react\";\n\nimport { cn } from \"../lib/cn\";\nimport { Chip } from \"../components/chip\";\nimport { LegalFooter } from \"../components/legal\";\nimport type { LegalFooterProps } from \"../components/legal\";\nimport { LandingActions } from \"./landing-actions\";\nimport type { LandingActionsProps } from \"./landing-actions\";\nimport { useLandingLabels } from \"./landing-labels\";\nimport type { LandingLabels } from \"./landing-labels\";\n\n/**\n * The sections of the public landing page (docs/landing-demo-harmonization.md §4.1),\n * top to bottom: `Hero`, `FeatureRows` of `FeatureRow`, `TrustStrip`, `CtaBand`,\n * `PublicFooter` — keksdose's landing-page.tsx, taken apart.\n *\n * The kit draws the frame; every word of marketing copy is the app's. Colours are the\n * kit's tokens (`--brand`, `--text-*`, `--bg-surface`, `--border`), so keksdose's ten\n * hard-coded teal sites follow the palette now (§2.11).\n *\n * **Visuals are decorative** (§4.1): the `visual` slots are `aria-hidden` and `inert` —\n * mock UI drawn with kit tokens, never screenshots with real data, and never anything a\n * keyboard could land on. The copy beside each one carries the information. A mock's\n * amounts use the demo data's currency, so the landing shows what \"Try the demo\" opens.\n */\n\ntype HeadingLevel = \"h1\" | \"h2\" | \"h3\" | \"h4\" | \"h5\" | \"h6\";\n\n/** The decorative slot: hidden from assistive tech and out of the tab order. */\nfunction Visual({ children, className }: { children: ReactNode; className?: string }) {\n return (\n <div aria-hidden inert data-slot=\"landing-visual\" className={cn(\"mx-auto w-full select-none\", className)}>\n {children}\n </div>\n );\n}\n\n/** The action pair's props, as `Hero` and `CtaBand` pass them on. */\ntype ActionProps = Pick<LandingActionsProps, \"session\" | \"access\" | \"demoHref\" | \"openAppHref\">;\n\n/* ── Hero ────────────────────────────────────────────────────────────────── */\n\nexport interface HeroProps extends Omit<ComponentPropsWithoutRef<\"section\">, \"title\" | \"children\">, ActionProps {\n /** The page's `h1`: what the app does, in a line. */\n title: ReactNode;\n /** Under it: for whom, and why. */\n subtitle?: ReactNode;\n /** The kit's \"Beta\" chip above the title. */\n beta?: boolean;\n /** A badge of the app's own in that place; wins over `beta`. */\n badge?: ReactNode;\n /** Under the actions, quiet: \"Free while in beta. Your data stays yours.\" */\n trust?: ReactNode;\n /** The product glimpse beside the copy (below it on a phone). Decorative. */\n visual?: ReactNode;\n /** Replaces the action pair ({@link LandingActions}). */\n actions?: ReactNode;\n /** Default `h1`; lower where the hero is embedded in another page. */\n headingAs?: HeadingLevel;\n labels?: Partial<LandingLabels>;\n}\n\n/**\n * The top of the landing (§4.1.2): an optional \"Beta\" badge, the `h1`, a subtitle, the\n * action pair for the visitor's state ({@link LandingActions}: \"Request access\" and \"Try\n * the demo\", or \"Open app\" for a session), a trust line, and a visual. Copy left and\n * visual right from `lg` up, stacked below.\n */\nexport function Hero({\n title,\n subtitle,\n beta = false,\n badge,\n trust,\n visual,\n actions,\n headingAs: Heading = \"h1\",\n session,\n access,\n demoHref,\n openAppHref,\n labels: labelsProp,\n className,\n ...rest\n}: HeroProps) {\n const labels = useLandingLabels(labelsProp);\n const shownBadge =\n badge ??\n (beta ? (\n // keksdose's chip, padded out to the hero's scale.\n <Chip size=\"sm\" tone=\"warning\" className=\"px-3 py-1 font-medium\">\n {labels.beta}\n </Chip>\n ) : null);\n return (\n <section\n {...rest}\n data-slot=\"landing-hero\"\n className={cn(\n \"mx-auto grid max-w-6xl items-center gap-12 px-4 py-16 sm:py-24\",\n visual != null && \"lg:grid-cols-2\",\n className,\n )}\n >\n <div>\n {shownBadge != null && <div className=\"mb-4\">{shownBadge}</div>}\n <Heading className=\"text-4xl font-bold tracking-tight text-balance text-[var(--text-primary)] sm:text-5xl\">\n {title}\n </Heading>\n {subtitle != null && <p className=\"mt-4 max-w-xl text-lg text-[var(--text-secondary)]\">{subtitle}</p>}\n <div className=\"mt-8\">\n {actions ?? (\n <LandingActions\n session={session}\n access={access}\n demoHref={demoHref}\n openAppHref={openAppHref}\n labels={labelsProp}\n />\n )}\n </div>\n {trust != null && <p className=\"mt-4 text-sm text-[var(--text-muted)]\">{trust}</p>}\n </div>\n {visual != null && <Visual className=\"max-w-sm\">{visual}</Visual>}\n </section>\n );\n}\n\n/* ── Feature rows ────────────────────────────────────────────────────────── */\n\nexport type FeatureRowsProps = ComponentPropsWithoutRef<\"section\">;\n\n/**\n * The band that holds the feature rows (§4.1.3): two to four {@link FeatureRow}s on the\n * surface colour, between hairlines. The rows alternate sides by themselves — the copy\n * of every second row moves to the end from `lg` up — so the app lists them in order\n * and nothing else.\n */\nexport function FeatureRows({ className, children, ...rest }: FeatureRowsProps) {\n return (\n <section\n {...rest}\n data-slot=\"landing-feature-rows\"\n className={cn(\"border-t border-[var(--border)] bg-[var(--bg-surface)]\", className)}\n >\n <div className=\"mx-auto max-w-6xl px-4 py-8 sm:py-12\">{children}</div>\n </section>\n );\n}\n\nexport interface FeatureRowProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"title\" | \"children\"> {\n /** A lucide icon (or any SVG) before the eyebrow. Decorative. */\n icon?: ReactNode;\n /** The small uppercase line above the heading: \"Capture\", \"Plan\". */\n eyebrow?: ReactNode;\n /** The row's `h2`. */\n title: ReactNode;\n description?: ReactNode;\n /** Up to three short points; the first three are shown. */\n bullets?: readonly ReactNode[];\n /** The mock beside the copy. Decorative. */\n visual?: ReactNode;\n /** Default `h2`. */\n headingAs?: HeadingLevel;\n}\n\n/**\n * One job of the app (§4.1.3), keksdose's \"capture → budget → understand\": an icon\n * eyebrow, the `h2`, a description, up to three bullets, and a visual on the other side.\n * Inside {@link FeatureRows} every second row puts its copy at the end (`group-even`),\n * which is keksdose's alternation without an index to pass.\n */\nexport function FeatureRow({\n icon,\n eyebrow,\n title,\n description,\n bullets,\n visual,\n headingAs: Heading = \"h2\",\n className,\n ...rest\n}: FeatureRowProps) {\n const shown = bullets?.slice(0, 3) ?? [];\n return (\n <div\n {...rest}\n data-slot=\"landing-feature-row\"\n className={cn(\"group/feature grid items-center gap-8 py-10\", visual != null && \"lg:grid-cols-2 lg:gap-16\", className)}\n >\n <div className=\"lg:group-even/feature:order-last\">\n {(icon != null || eyebrow != null) && (\n <p className=\"flex items-center gap-2 text-xs font-semibold uppercase tracking-wider text-[var(--brand)]\">\n {icon != null && (\n <span aria-hidden className=\"flex shrink-0 [&_svg]:size-4\">\n {icon}\n </span>\n )}\n {eyebrow}\n </p>\n )}\n <Heading className=\"mt-2 text-2xl font-semibold tracking-tight text-balance text-[var(--text-primary)] sm:text-3xl\">\n {title}\n </Heading>\n {description != null && <p className=\"mt-3 max-w-xl text-[var(--text-secondary)]\">{description}</p>}\n {shown.length > 0 && (\n <ul className=\"mt-4 space-y-2\">\n {shown.map((bullet, i) => (\n <li key={i} className=\"flex gap-2.5 text-sm text-[var(--text-secondary)]\">\n <span aria-hidden className=\"mt-1.5 size-1.5 shrink-0 rounded-full bg-[var(--brand)]\" />\n <span className=\"min-w-0\">{bullet}</span>\n </li>\n ))}\n </ul>\n )}\n </div>\n {visual != null && <Visual className=\"max-w-md\">{visual}</Visual>}\n </div>\n );\n}\n\n/* ── Trust strip ─────────────────────────────────────────────────────────── */\n\nexport interface TrustItem {\n /** A lucide icon. Decorative. */\n icon?: ReactNode;\n /** The item's `h3`: \"Hosted in Switzerland\". */\n title: ReactNode;\n /** One sentence. */\n description: ReactNode;\n}\n\nexport interface TrustStripProps extends Omit<ComponentPropsWithoutRef<\"section\">, \"children\"> {\n /** Three or four (§4.1.4): hosting, security, data ownership, sharing. */\n items: readonly TrustItem[];\n /** The items' heading level. Default `h3`. */\n headingAs?: HeadingLevel;\n}\n\n/**\n * Quiet, start-aligned facts after the feature rows (§4.1.4) — an icon, an `h3` and one\n * sentence each, on one row from `lg` up (three or four columns), two on a tablet, one\n * on a phone. No marketing volume: keksdose's \"trust strip\".\n */\nexport function TrustStrip({ items, headingAs: Heading = \"h3\", className, ...rest }: TrustStripProps) {\n return (\n <section {...rest} data-slot=\"landing-trust\" className={cn(\"mx-auto max-w-6xl px-4 py-12 sm:py-16\", className)}>\n <ul className={cn(\"grid gap-8 sm:grid-cols-2\", items.length === 3 ? \"lg:grid-cols-3\" : \"lg:grid-cols-4\")}>\n {items.map((item, i) => (\n <li key={i}>\n {item.icon != null && (\n <span aria-hidden className=\"flex text-[var(--brand)] [&_svg]:size-5\">\n {item.icon}\n </span>\n )}\n <Heading className=\"mt-2 text-sm font-semibold text-[var(--text-primary)]\">{item.title}</Heading>\n <p className=\"mt-1 text-sm text-[var(--text-secondary)]\">{item.description}</p>\n </li>\n ))}\n </ul>\n </section>\n );\n}\n\n/* ── CTA band ────────────────────────────────────────────────────────────── */\n\nexport interface CtaBandProps extends Omit<ComponentPropsWithoutRef<\"section\">, \"title\" | \"children\">, ActionProps {\n /** The band's `h2`: the last ask. */\n title: ReactNode;\n subtitle?: ReactNode;\n /** Replaces the action pair ({@link LandingActions}). */\n actions?: ReactNode;\n /** Default `h2`. */\n headingAs?: HeadingLevel;\n labels?: Partial<LandingLabels>;\n}\n\n/**\n * The closing band before the footer (§4.1.5): a centred `h2`, a subtitle, and the same\n * action pair as the hero.\n */\nexport function CtaBand({\n title,\n subtitle,\n actions,\n headingAs: Heading = \"h2\",\n session,\n access,\n demoHref,\n openAppHref,\n labels,\n className,\n ...rest\n}: CtaBandProps) {\n return (\n <section\n {...rest}\n data-slot=\"landing-cta\"\n className={cn(\"border-t border-[var(--border)] bg-[var(--bg-surface)]\", className)}\n >\n <div className=\"mx-auto max-w-5xl px-4 py-16 text-center\">\n <Heading className=\"text-2xl font-semibold tracking-tight text-balance text-[var(--text-primary)] sm:text-3xl\">\n {title}\n </Heading>\n {subtitle != null && <p className=\"mx-auto mt-3 max-w-xl text-[var(--text-secondary)]\">{subtitle}</p>}\n <div className=\"mt-8\">\n {actions ?? (\n <LandingActions\n session={session}\n access={access}\n demoHref={demoHref}\n openAppHref={openAppHref}\n align=\"center\"\n labels={labels}\n />\n )}\n </div>\n </div>\n </section>\n );\n}\n\n/* ── Footer ──────────────────────────────────────────────────────────────── */\n\nexport interface PublicFooterProps extends Omit<LegalFooterProps, \"children\"> {\n /** The app's line under the legal links: \"Self-hosted household budgeting.\" */\n tagline?: ReactNode;\n}\n\n/**\n * The public pages' footer (§4.1.6): the kit's {@link LegalFooter} — the three legal\n * links — with the app's tagline under them. keksdose's `SiteFooter`, which was already\n * this.\n */\nexport function PublicFooter({ tagline, ...rest }: PublicFooterProps) {\n return <LegalFooter {...rest}>{tagline}</LegalFooter>;\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Search-engine helpers for the public landing page (docs/landing-demo-harmonization.md
3
+ * §4.3): the hook that declares a page's canonical URL, title, description and JSON-LD,
4
+ * and the two pure helpers each app's SEO test is built from.
5
+ */
6
+ /**
7
+ * Declares the current page's canonical URL — and optionally its title and meta
8
+ * description — while the calling component is mounted (feedback #129).
9
+ *
10
+ * Client-side because there is nowhere else to put it: the SPA is served by
11
+ * Caddy's `try_files … /index.html`, so every route shares one static head.
12
+ * Googlebot renders JS and reads the canonical from the RENDERED DOM, which is
13
+ * what makes this work; non-rendering scrapers (WhatsApp, Slack) only ever see
14
+ * index.html's static tags, which is why those stay in place as the fallback.
15
+ *
16
+ * The href is resolved against `window.location.origin` rather than hardcoded, so
17
+ * a preview deployment or the VPS host stays self-referential instead of pointing
18
+ * every copy of the app at production.
19
+ *
20
+ * The cleanup matters for the same reason as in {@link useNoIndex}: an SPA
21
+ * navigation from the landing page into the app must not leave the landing's
22
+ * canonical (or title) behind on an app page.
23
+ *
24
+ * 0.31 (§4.3): keksdose's `usePageSeo` (shared/hooks/use-page-seo.ts), verbatim. The
25
+ * landing calls it with `canonicalPath: "/"` on both `/` and `/welcome`, so the two URLs
26
+ * of one page are one result; there are no per-language URLs and no hreflang.
27
+ */
28
+ declare function usePageSeo({ canonicalPath, title, description, jsonLd, }: {
29
+ canonicalPath: string;
30
+ title?: string;
31
+ description?: string;
32
+ /** Structured data for this page, as the object (not a string). Injected as an
33
+ * `application/ld+json` DATA BLOCK — a script element the browser never executes,
34
+ * which is also why it can be added here rather than in `index.html`: the CSP
35
+ * carries a HASH of the one inline script that file is allowed to have, and
36
+ * `csp-inline-script.test.ts` fails the build on a second one (live #250). */
37
+ jsonLd?: Record<string, unknown>;
38
+ }): void;
39
+ /** What a search result shows of the landing, in one language. */
40
+ interface SeoCopy {
41
+ /** The page title (`landing.seo.title`): "Keksdose — envelope budgeting". */
42
+ title: string;
43
+ /** The meta description. */
44
+ description: string;
45
+ /** The product name the title carries, before or after its tagline. */
46
+ brand: string;
47
+ }
48
+ /** The width budgets, as characters. */
49
+ interface SeoCopyLimits {
50
+ /** Default 60: a result's title is cut around 580px. */
51
+ maxTitle?: number;
52
+ /** Default 155: a snippet is cut around 920px. */
53
+ maxDescription?: number;
54
+ }
55
+ type SeoCopyProblemCode = "title-empty" | "description-empty" | "title-too-long" | "description-too-long" | "title-without-brand" | "description-repeats-title";
56
+ /** One thing wrong with a language's copy. `message` is for the test's failure output. */
57
+ interface SeoCopyProblem {
58
+ code: SeoCopyProblemCode;
59
+ message: string;
60
+ }
61
+ /**
62
+ * What is wrong with one language's search-result copy (§4.3) — an empty list when
63
+ * nothing is. keksdose's landing-seo.test.ts (live #305), made reusable:
64
+ *
65
+ * - the title fits `maxTitle` characters (60) and the description `maxDescription`
66
+ * (155) — conservative stand-ins for Google's pixel budgets; German is the language
67
+ * most likely to blow through them, and what gets cut is always the end;
68
+ * - the title carries the brand, before or after its tagline ("Keksdose — …");
69
+ * - the description does not open with the title's tagline again — a snippet that
70
+ * repeats the line above it spends its first forty characters saying nothing new.
71
+ *
72
+ * ```ts
73
+ * for (const [locale, bundle] of Object.entries(LOCALES)) {
74
+ * it(`fits ${locale} into a search result`, () => {
75
+ * expect(seoCopyProblems({ ...bundle.landing.seo, brand: "Keksdose" })).toEqual([]);
76
+ * });
77
+ * }
78
+ * ```
79
+ */
80
+ declare function seoCopyProblems(copy: SeoCopy, limits?: SeoCopyLimits): SeoCopyProblem[];
81
+ /**
82
+ * The `content` of the first `<meta>` in `html` that `selector` picks, whitespace
83
+ * collapsed and entities decoded — or `undefined` when there is none. For each app's
84
+ * parity test (§4.3): `index.html`'s static description and `og:` tags must equal the
85
+ * default language's strings, because a scraper that runs no script (WhatsApp, Slack)
86
+ * sees only those.
87
+ *
88
+ * `selector` names attributes, as in CSS — `meta[name="description"]`,
89
+ * `[property="og:title"]` or just `property="og:description"` — and every one named must
90
+ * match. A `RegExp` is matched against the tag's source instead (keksdose's form).
91
+ * String parsing, not a DOM: the test runs under `@vitest-environment node`.
92
+ *
93
+ * ```ts
94
+ * expect(metaContent(html, 'meta[name="description"]')).toBe(de.landing.seo.description);
95
+ * ```
96
+ */
97
+ declare function metaContent(html: string, selector: string | RegExp): string | undefined;
98
+
99
+ export { type SeoCopy, type SeoCopyLimits, type SeoCopyProblem, type SeoCopyProblemCode, metaContent, seoCopyProblems, usePageSeo };
@@ -0,0 +1,121 @@
1
+ "use client";
2
+ import { useEffect } from "react";
3
+ function usePageSeo({
4
+ canonicalPath,
5
+ title,
6
+ description,
7
+ jsonLd
8
+ }) {
9
+ useEffect(() => {
10
+ const link = document.createElement("link");
11
+ link.rel = "canonical";
12
+ link.href = new URL(canonicalPath, window.location.origin).toString();
13
+ document.head.appendChild(link);
14
+ const previousTitle = document.title;
15
+ if (title) document.title = title;
16
+ const meta = description ? document.head.querySelector('meta[name="description"]') : null;
17
+ const previousDescription = meta?.content;
18
+ if (meta && description) meta.content = description;
19
+ let ld = null;
20
+ if (jsonLd) {
21
+ ld = document.createElement("script");
22
+ ld.type = "application/ld+json";
23
+ ld.textContent = JSON.stringify(jsonLd);
24
+ document.head.appendChild(ld);
25
+ }
26
+ return () => {
27
+ link.remove();
28
+ ld?.remove();
29
+ if (title) document.title = previousTitle;
30
+ if (meta && previousDescription !== void 0) meta.content = previousDescription;
31
+ };
32
+ }, [canonicalPath, title, description]);
33
+ }
34
+ const length = (text) => Array.from(text).length;
35
+ const escapeRegExp = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
36
+ const SEPARATOR = "\\s*[\u2014\u2013\\-|:\xB7\u2022]\\s*";
37
+ function taglineOf(title, brand) {
38
+ const name = escapeRegExp(brand.trim());
39
+ const leading = new RegExp(`^${name}${SEPARATOR}`, "i");
40
+ if (leading.test(title)) return title.replace(leading, "").trim();
41
+ const trailing = new RegExp(`${SEPARATOR}${name}$`, "i");
42
+ if (trailing.test(title)) return title.replace(trailing, "").trim();
43
+ return null;
44
+ }
45
+ function seoCopyProblems(copy, limits = {}) {
46
+ const { maxTitle = 60, maxDescription = 155 } = limits;
47
+ const title = copy.title.replace(/\s+/g, " ").trim();
48
+ const description = copy.description.replace(/\s+/g, " ").trim();
49
+ const problems = [];
50
+ if (!title) problems.push({ code: "title-empty", message: "the title is empty" });
51
+ if (!description) problems.push({ code: "description-empty", message: "the description is empty" });
52
+ if (length(title) > maxTitle) {
53
+ problems.push({
54
+ code: "title-too-long",
55
+ message: `the title is ${length(title)} characters, over ${maxTitle}: "${title}"`
56
+ });
57
+ }
58
+ if (length(description) > maxDescription) {
59
+ problems.push({
60
+ code: "description-too-long",
61
+ message: `the description is ${length(description)} characters, over ${maxDescription}: "${description}"`
62
+ });
63
+ }
64
+ if (!title) return problems;
65
+ const tagline = taglineOf(title, copy.brand);
66
+ if (tagline === null) {
67
+ problems.push({
68
+ code: "title-without-brand",
69
+ message: `the title does not start or end with "${copy.brand}" and a separator: "${title}"`
70
+ });
71
+ }
72
+ const repeated = (tagline ?? title).toLowerCase();
73
+ if (repeated && description.toLowerCase().startsWith(repeated)) {
74
+ problems.push({
75
+ code: "description-repeats-title",
76
+ message: `the description opens with the title's tagline "${tagline ?? title}"`
77
+ });
78
+ }
79
+ return problems;
80
+ }
81
+ function decodeEntities(text) {
82
+ const named = { amp: "&", lt: "<", gt: ">", quot: '"', apos: "'", nbsp: "\xA0" };
83
+ return text.replace(/&(#x[\da-f]+|#\d+|[a-z]+);/gi, (whole, ref) => {
84
+ if (ref[0] === "#") {
85
+ const code = ref[1] === "x" || ref[1] === "X" ? parseInt(ref.slice(2), 16) : parseInt(ref.slice(1), 10);
86
+ return Number.isFinite(code) ? String.fromCodePoint(code) : whole;
87
+ }
88
+ return named[ref.toLowerCase()] ?? whole;
89
+ });
90
+ }
91
+ function attributesOf(tag) {
92
+ const found = /* @__PURE__ */ new Map();
93
+ for (const m of tag.matchAll(/([^\s=/<>"']+)\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+))/g)) {
94
+ found.set(m[1].toLowerCase(), m[2] ?? m[3] ?? m[4] ?? "");
95
+ }
96
+ return found;
97
+ }
98
+ function metaContent(html, selector) {
99
+ const wanted = typeof selector === "string" ? Array.from(selector.matchAll(/([\w:.-]+)\s*=\s*(?:"([^"]*)"|'([^']*)')/g), (m) => [
100
+ m[1].toLowerCase(),
101
+ m[2] ?? m[3] ?? ""
102
+ ]) : null;
103
+ if (wanted !== null && wanted.length === 0) {
104
+ throw new Error(`metaContent: the selector names no attribute: ${selector}`);
105
+ }
106
+ const pattern = selector instanceof RegExp ? new RegExp(selector.source, selector.flags.replace("g", "")) : null;
107
+ for (const tag of html.match(/<meta\b[^>]*>/gi) ?? []) {
108
+ const attrs = attributesOf(tag);
109
+ const matches = pattern ? pattern.test(tag) : wanted.every(([name, value]) => attrs.get(name) === value);
110
+ if (!matches) continue;
111
+ const content = attrs.get("content");
112
+ return content === void 0 ? void 0 : decodeEntities(content).replace(/\s+/g, " ").trim();
113
+ }
114
+ return void 0;
115
+ }
116
+ export {
117
+ metaContent,
118
+ seoCopyProblems,
119
+ usePageSeo
120
+ };
121
+ //# sourceMappingURL=page-seo.js.map