@eifi1/ui-kit 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (229) hide show
  1. package/README.md +219 -0
  2. package/dist/components/account-settings.d.ts +71 -0
  3. package/dist/components/account-settings.js +159 -0
  4. package/dist/components/account-settings.js.map +1 -0
  5. package/dist/components/alert-banner.d.ts +20 -0
  6. package/dist/components/alert-banner.js +39 -0
  7. package/dist/components/alert-banner.js.map +1 -0
  8. package/dist/components/amount-input.d.ts +117 -0
  9. package/dist/components/amount-input.js +243 -0
  10. package/dist/components/amount-input.js.map +1 -0
  11. package/dist/components/calculator.d.ts +42 -0
  12. package/dist/components/calculator.js +161 -0
  13. package/dist/components/calculator.js.map +1 -0
  14. package/dist/components/chart.d.ts +58 -0
  15. package/dist/components/chart.js +158 -0
  16. package/dist/components/chart.js.map +1 -0
  17. package/dist/components/combobox-core.d.ts +77 -0
  18. package/dist/components/combobox-core.js +263 -0
  19. package/dist/components/combobox-core.js.map +1 -0
  20. package/dist/components/combobox.d.ts +141 -0
  21. package/dist/components/combobox.js +550 -0
  22. package/dist/components/combobox.js.map +1 -0
  23. package/dist/components/currency-select.d.ts +37 -0
  24. package/dist/components/currency-select.js +131 -0
  25. package/dist/components/currency-select.js.map +1 -0
  26. package/dist/components/data-table-filter-popover.d.ts +28 -0
  27. package/dist/components/data-table-filter-popover.js +201 -0
  28. package/dist/components/data-table-filter-popover.js.map +1 -0
  29. package/dist/components/data-table-filters.d.ts +5 -0
  30. package/dist/components/data-table-filters.js +118 -0
  31. package/dist/components/data-table-filters.js.map +1 -0
  32. package/dist/components/data-table-labels.d.ts +33 -0
  33. package/dist/components/data-table-labels.js +53 -0
  34. package/dist/components/data-table-labels.js.map +1 -0
  35. package/dist/components/data-table-pagination.d.ts +18 -0
  36. package/dist/components/data-table-pagination.js +92 -0
  37. package/dist/components/data-table-pagination.js.map +1 -0
  38. package/dist/components/data-table-sort.d.ts +23 -0
  39. package/dist/components/data-table-sort.js +55 -0
  40. package/dist/components/data-table-sort.js.map +1 -0
  41. package/dist/components/data-table.d.ts +5 -0
  42. package/dist/components/data-table.js +926 -0
  43. package/dist/components/data-table.js.map +1 -0
  44. package/dist/components/date-picker.d.ts +85 -0
  45. package/dist/components/date-picker.js +253 -0
  46. package/dist/components/date-picker.js.map +1 -0
  47. package/dist/components/dropdown.d.ts +98 -0
  48. package/dist/components/dropdown.js +122 -0
  49. package/dist/components/dropdown.js.map +1 -0
  50. package/dist/components/entity-combobox.d.ts +49 -0
  51. package/dist/components/entity-combobox.js +115 -0
  52. package/dist/components/entity-combobox.js.map +1 -0
  53. package/dist/components/file-dropzone.d.ts +18 -0
  54. package/dist/components/file-dropzone.js +101 -0
  55. package/dist/components/file-dropzone.js.map +1 -0
  56. package/dist/components/full-bleed-dialog.d.ts +57 -0
  57. package/dist/components/full-bleed-dialog.js +68 -0
  58. package/dist/components/full-bleed-dialog.js.map +1 -0
  59. package/dist/components/grouped-picker.d.ts +37 -0
  60. package/dist/components/grouped-picker.js +87 -0
  61. package/dist/components/grouped-picker.js.map +1 -0
  62. package/dist/components/hover-menu.d.ts +16 -0
  63. package/dist/components/hover-menu.js +113 -0
  64. package/dist/components/hover-menu.js.map +1 -0
  65. package/dist/components/mini-calendar.d.ts +33 -0
  66. package/dist/components/mini-calendar.js +133 -0
  67. package/dist/components/mini-calendar.js.map +1 -0
  68. package/dist/components/modal.d.ts +76 -0
  69. package/dist/components/modal.js +156 -0
  70. package/dist/components/modal.js.map +1 -0
  71. package/dist/components/multi-entity-combobox.d.ts +47 -0
  72. package/dist/components/multi-entity-combobox.js +116 -0
  73. package/dist/components/multi-entity-combobox.js.map +1 -0
  74. package/dist/components/multi-select.d.ts +39 -0
  75. package/dist/components/multi-select.js +132 -0
  76. package/dist/components/multi-select.js.map +1 -0
  77. package/dist/components/number-input.d.ts +74 -0
  78. package/dist/components/number-input.js +114 -0
  79. package/dist/components/number-input.js.map +1 -0
  80. package/dist/components/numpad-sheet.d.ts +45 -0
  81. package/dist/components/numpad-sheet.js +102 -0
  82. package/dist/components/numpad-sheet.js.map +1 -0
  83. package/dist/components/picker-sheet.d.ts +44 -0
  84. package/dist/components/picker-sheet.js +78 -0
  85. package/dist/components/picker-sheet.js.map +1 -0
  86. package/dist/components/popover.d.ts +20 -0
  87. package/dist/components/popover.js +46 -0
  88. package/dist/components/popover.js.map +1 -0
  89. package/dist/components/search-field.d.ts +52 -0
  90. package/dist/components/search-field.js +64 -0
  91. package/dist/components/search-field.js.map +1 -0
  92. package/dist/components/settings-fields.d.ts +39 -0
  93. package/dist/components/settings-fields.js +17 -0
  94. package/dist/components/settings-fields.js.map +1 -0
  95. package/dist/components/swipeable-row.d.ts +60 -0
  96. package/dist/components/swipeable-row.js +115 -0
  97. package/dist/components/swipeable-row.js.map +1 -0
  98. package/dist/components/toggle-group.d.ts +34 -0
  99. package/dist/components/toggle-group.js +90 -0
  100. package/dist/components/toggle-group.js.map +1 -0
  101. package/dist/components/tooltip.d.ts +86 -0
  102. package/dist/components/tooltip.js +171 -0
  103. package/dist/components/tooltip.js.map +1 -0
  104. package/dist/components/ui.d.ts +239 -0
  105. package/dist/components/ui.js +512 -0
  106. package/dist/components/ui.js.map +1 -0
  107. package/dist/components/use-mobile-reveal.d.ts +31 -0
  108. package/dist/components/use-mobile-reveal.js +33 -0
  109. package/dist/components/use-mobile-reveal.js.map +1 -0
  110. package/dist/components/use-table-state.d.ts +54 -0
  111. package/dist/components/use-table-state.js +153 -0
  112. package/dist/components/use-table-state.js.map +1 -0
  113. package/dist/components/user-avatar.d.ts +23 -0
  114. package/dist/components/user-avatar.js +33 -0
  115. package/dist/components/user-avatar.js.map +1 -0
  116. package/dist/components/wizard-stepper.d.ts +14 -0
  117. package/dist/components/wizard-stepper.js +27 -0
  118. package/dist/components/wizard-stepper.js.map +1 -0
  119. package/dist/data-table-filters-aXyf0Xub.d.ts +261 -0
  120. package/dist/feedback/feedback-attachment.d.ts +63 -0
  121. package/dist/feedback/feedback-attachment.js +160 -0
  122. package/dist/feedback/feedback-attachment.js.map +1 -0
  123. package/dist/feedback/feedback-dialog.d.ts +72 -0
  124. package/dist/feedback/feedback-dialog.js +109 -0
  125. package/dist/feedback/feedback-dialog.js.map +1 -0
  126. package/dist/feedback/feedback-inbox.d.ts +226 -0
  127. package/dist/feedback/feedback-inbox.js +315 -0
  128. package/dist/feedback/feedback-inbox.js.map +1 -0
  129. package/dist/hooks/use-anchored-panel.d.ts +80 -0
  130. package/dist/hooks/use-anchored-panel.js +71 -0
  131. package/dist/hooks/use-anchored-panel.js.map +1 -0
  132. package/dist/hooks/use-anchored-rect.d.ts +24 -0
  133. package/dist/hooks/use-anchored-rect.js +40 -0
  134. package/dist/hooks/use-anchored-rect.js.map +1 -0
  135. package/dist/hooks/use-body-scroll-lock.d.ts +9 -0
  136. package/dist/hooks/use-body-scroll-lock.js +41 -0
  137. package/dist/hooks/use-body-scroll-lock.js.map +1 -0
  138. package/dist/hooks/use-close-transition.d.ts +41 -0
  139. package/dist/hooks/use-close-transition.js +36 -0
  140. package/dist/hooks/use-close-transition.js.map +1 -0
  141. package/dist/hooks/use-dismiss.d.ts +34 -0
  142. package/dist/hooks/use-dismiss.js +41 -0
  143. package/dist/hooks/use-dismiss.js.map +1 -0
  144. package/dist/hooks/use-media-query.d.ts +8 -0
  145. package/dist/hooks/use-media-query.js +18 -0
  146. package/dist/hooks/use-media-query.js.map +1 -0
  147. package/dist/hooks/use-overlay-history.d.ts +10 -0
  148. package/dist/hooks/use-overlay-history.js +99 -0
  149. package/dist/hooks/use-overlay-history.js.map +1 -0
  150. package/dist/hooks/use-row-swipe.d.ts +30 -0
  151. package/dist/hooks/use-row-swipe.js +128 -0
  152. package/dist/hooks/use-row-swipe.js.map +1 -0
  153. package/dist/index.d.ts +75 -0
  154. package/dist/index.js +90 -0
  155. package/dist/index.js.map +1 -0
  156. package/dist/lib/calc.d.ts +89 -0
  157. package/dist/lib/calc.js +180 -0
  158. package/dist/lib/calc.js.map +1 -0
  159. package/dist/lib/cn.d.ts +6 -0
  160. package/dist/lib/cn.js +7 -0
  161. package/dist/lib/cn.js.map +1 -0
  162. package/dist/lib/dates.d.ts +84 -0
  163. package/dist/lib/dates.js +108 -0
  164. package/dist/lib/dates.js.map +1 -0
  165. package/dist/lib/logger.d.ts +17 -0
  166. package/dist/lib/logger.js +30 -0
  167. package/dist/lib/logger.js.map +1 -0
  168. package/dist/lib/safe-storage.d.ts +23 -0
  169. package/dist/lib/safe-storage.js +20 -0
  170. package/dist/lib/safe-storage.js.map +1 -0
  171. package/dist/search/command-palette.d.ts +71 -0
  172. package/dist/search/command-palette.js +198 -0
  173. package/dist/search/command-palette.js.map +1 -0
  174. package/dist/shell/app-shell.d.ts +64 -0
  175. package/dist/shell/app-shell.js +237 -0
  176. package/dist/shell/app-shell.js.map +1 -0
  177. package/dist/shell/option-switcher-menu.d.ts +24 -0
  178. package/dist/shell/option-switcher-menu.js +44 -0
  179. package/dist/shell/option-switcher-menu.js.map +1 -0
  180. package/dist/shell/role-switcher.d.ts +19 -0
  181. package/dist/shell/role-switcher.js +28 -0
  182. package/dist/shell/role-switcher.js.map +1 -0
  183. package/dist/shell/top-bar.d.ts +15 -0
  184. package/dist/shell/top-bar.js +29 -0
  185. package/dist/shell/top-bar.js.map +1 -0
  186. package/dist/shell/topbar-action-menu.d.ts +44 -0
  187. package/dist/shell/topbar-action-menu.js +49 -0
  188. package/dist/shell/topbar-action-menu.js.map +1 -0
  189. package/dist/shell/topbar-controls.d.ts +54 -0
  190. package/dist/shell/topbar-controls.js +152 -0
  191. package/dist/shell/topbar-controls.js.map +1 -0
  192. package/dist/theme/chart-palette.d.ts +60 -0
  193. package/dist/theme/chart-palette.js +79 -0
  194. package/dist/theme/chart-palette.js.map +1 -0
  195. package/dist/theme/palette-presets.d.ts +39 -0
  196. package/dist/theme/palette-presets.js +440 -0
  197. package/dist/theme/palette-presets.js.map +1 -0
  198. package/dist/theme/palette-store.d.ts +45 -0
  199. package/dist/theme/palette-store.js +52 -0
  200. package/dist/theme/palette-store.js.map +1 -0
  201. package/dist/theme/theme-store.d.ts +57 -0
  202. package/dist/theme/theme-store.js +96 -0
  203. package/dist/theme/theme-store.js.map +1 -0
  204. package/dist/tour/tour.d.ts +104 -0
  205. package/dist/tour/tour.js +351 -0
  206. package/dist/tour/tour.js.map +1 -0
  207. package/dist/wizard/stepper-nav.d.ts +30 -0
  208. package/dist/wizard/stepper-nav.js +146 -0
  209. package/dist/wizard/stepper-nav.js.map +1 -0
  210. package/dist/wizard/types.d.ts +124 -0
  211. package/dist/wizard/types.js +24 -0
  212. package/dist/wizard/types.js.map +1 -0
  213. package/dist/wizard/use-wizard.d.ts +6 -0
  214. package/dist/wizard/use-wizard.js +222 -0
  215. package/dist/wizard/use-wizard.js.map +1 -0
  216. package/dist/wizard/validation.d.ts +23 -0
  217. package/dist/wizard/validation.js +13 -0
  218. package/dist/wizard/validation.js.map +1 -0
  219. package/dist/wizard/wizard-context.d.ts +19 -0
  220. package/dist/wizard/wizard-context.js +15 -0
  221. package/dist/wizard/wizard-context.js.map +1 -0
  222. package/dist/wizard/wizard-step.d.ts +11 -0
  223. package/dist/wizard/wizard-step.js +12 -0
  224. package/dist/wizard/wizard-step.js.map +1 -0
  225. package/dist/wizard/wizard-summary.d.ts +18 -0
  226. package/dist/wizard/wizard-summary.js +41 -0
  227. package/dist/wizard/wizard-summary.js.map +1 -0
  228. package/package.json +87 -0
  229. package/tokens.css +254 -0
package/README.md ADDED
@@ -0,0 +1,219 @@
1
+ # @eifi1/ui-kit
2
+
3
+ [![npm](https://img.shields.io/npm/v/@eifi1/ui-kit)](https://www.npmjs.com/package/@eifi1/ui-kit)
4
+
5
+ The shared, app-agnostic design system for the sibling apps. Published to npm, so
6
+ consumers install it like any other dependency — no submodule, no token, no
7
+ `file:` path pointing into a neighbouring checkout.
8
+
9
+ ```bash
10
+ npm install @eifi1/ui-kit
11
+ ```
12
+
13
+ ```ts
14
+ import { Button, DataTable } from "@eifi1/ui-kit";
15
+ import "@eifi1/ui-kit/tokens.css";
16
+ ```
17
+
18
+ > Formerly `@hb/ui` in the `hb-ui` repository — `hb` was short for Household
19
+ > Books, a name none of the current consumers share. GitHub redirects the old
20
+ > repository URL, but the old package name is not published; update imports.
21
+
22
+ Ships compiled ESM plus type declarations, built one output file per source
23
+ module so consumers can still tree-shake it (see the `sideEffects` note in
24
+ `package.json` — a bundling build would pull recharts into every consumer's
25
+ entry chunk). Tailwind classes are emitted as-is; each app still compiles them
26
+ with its own Tailwind config.
27
+
28
+ ## Install
29
+
30
+ ```bash
31
+ npm install @eifi1/ui-kit
32
+ ```
33
+
34
+ Versions are pinned in your lockfile like any dependency, so each app upgrades
35
+ when it chooses. To work on the design system and an app together, use
36
+ `npm link` or a workspace override rather than a `file:` path.
37
+
38
+ `@eifi1/ui-kit` needs these installed in the consuming app: `react`, `react-dom` (required
39
+ peers) and — only if you use the pieces that need them — `recharts` (the `chart`
40
+ kit), `sonner` (`FileDropzone`), `react-router` (`DataTable` URL-sync + `AppShell`).
41
+ It also pulls `clsx`, `tailwind-merge`, `lucide-react`, `zustand`, `flag-icons`.
42
+
43
+ ## Wiring
44
+
45
+ ### 1. Vite — dedupe shared singletons
46
+
47
+ The package is a linked source package, so its imports must resolve to the app's
48
+ single copy of these (two React/router/store instances break hooks and context):
49
+
50
+ ```ts
51
+ // vite.config.ts
52
+ resolve: {
53
+ dedupe: ["react", "react-dom", "zustand", "recharts", "sonner", "react-router"],
54
+ }
55
+ ```
56
+
57
+ ### 2. Tailwind (v4) — tokens + source scan
58
+
59
+ ```css
60
+ /* app.css */
61
+ @import "tailwindcss";
62
+ @import "@eifi1/ui-kit/tokens.css"; /* design tokens, semantic utilities, chrome */
63
+ @source "../../node_modules/@eifi1/ui-kit/src"; /* keep the class names @eifi1/ui-kit uses */
64
+ ```
65
+
66
+ ### 3. Theme + palette stores (own persistence keys)
67
+
68
+ The stores are factories so each app namespaces its own `localStorage`:
69
+
70
+ ```ts
71
+ import { createThemeStore, createPaletteStore } from "@eifi1/ui-kit";
72
+ export const { useTheme, useApplyTheme } = createThemeStore("myapp-theme");
73
+ export const { usePalette, useApplyPalette, useActiveTokenSet, useChartHex, useHeatStops } =
74
+ createPaletteStore("myapp-palette", useTheme);
75
+ ```
76
+
77
+ Call `useApplyTheme()` + `useApplyPalette()` once near the root. For a no-flash
78
+ first paint, apply the persisted theme class + token set before hydration (see
79
+ Keksdose's `main.tsx` for the pattern, using `applyTokenSet`/`presetById`).
80
+
81
+ ## Tests
82
+
83
+ ```bash
84
+ npm test # vitest run
85
+ npm run test:watch
86
+ npm run typecheck
87
+ ```
88
+
89
+ Vitest + jsdom, configured as a copy of the lead consumer's setup rather than a new
90
+ dialect — so a test can move between this package and an app unchanged. The suite pins
91
+ `TZ=Europe/Berlin`, which is load-bearing for `lib/dates.ts`: those helpers answer with
92
+ the LOCAL calendar day, and on a UTC runner a broken UTC implementation and a correct
93
+ local one agree.
94
+
95
+ Start where the return on a line of test is highest and no DOM is needed — `lib/`,
96
+ `theme/`, and the data-table's pure helpers. The house rule for a fix is that its
97
+ regression test is **run against the old code first and observed to fail there**;
98
+ a test written after the fix proves only that the fix is self-consistent.
99
+
100
+ ## What's exported
101
+
102
+ - **Primitives / fields:** `cn`, `Button`, `Input`, `Select`, `Textarea`,
103
+ `FloatingField`, `FieldLabel`, `Card`, `Spinner`, `EmptyState`, `Tabs`, plus the
104
+ field-class constants (`FIELD_BASE`, `FIELD_TRIGGER`, `FLOATING_*`, …).
105
+ - **Inputs / dropdowns:** `NumberInput`, `AmountInput`, `Combobox`, `MultiSelect`,
106
+ `CurrencySelect` (+ `CURRENCIES`, `getCurrency`, `CurrencyFlag`), dropdown
107
+ primitives (`useDropdown`, `useDropdownSearch`, `DropdownPanel`,
108
+ `DropdownSearchHeader`), `CalculatorButton`, the calc engine.
109
+ - **Overlays / misc:** `Modal` (+ `useBackdropClose`), `Popover`, `HoverMenu`,
110
+ `Tooltip`, `AlertBanner`, `ToggleGroup`, `FileDropzone`, `GroupedPicker`,
111
+ `MiniCalendar`, `WizardStepper` (a bare two-step indicator — not the wizard
112
+ engine below), the `chart` kit.
113
+ - **Data table:** `DataTable` (+ `DataTableColumn`, `FilterState`, `SortState`,
114
+ `ServerPagination`), `Pagination`, filter/sort helpers, `DataTableLabels`.
115
+ - **Theme:** `TokenSet`, `PalettePreset`, `DEFAULT_PRESET`, `ALTERNATIVE_PRESETS`,
116
+ `PALETTES`, `presetById`, `applyTokenSet`, `clearTokenSet`, `TOKEN_VARS`,
117
+ `PALETTE_HEX`, chart-colour helpers, and the store factories.
118
+ - **Shell:** `TopBar`, `AppShell` (+ `AppShellNavItem`), `ThemeToggle`,
119
+ `PaletteMenu`, `LanguageMenu`, `TOPBAR_TRIGGER_CLASS`, `TOPBAR_MENU_ITEM_CLASS`.
120
+ - **Feedback:** `FeedbackDialog` (the compose form) and the inbox parts —
121
+ `FeedbackStatus`/`FeedbackCategory` and their `*_META`/`*_ORDER` tables,
122
+ `nextFeedbackStatus`, `visibleFeedbackStatuses`, `selectableFeedbackStatuses`,
123
+ `feedbackCategoryRank`, `FeedbackStatusBadge`, `FeedbackStatusTransitions`,
124
+ `FeedbackCategoryBadge`, `FeedbackNoteEditor`, `FeedbackDetail`,
125
+ `FeedbackDetailSection`, `FeedbackProse`. The vocabulary, the transition policy
126
+ and the look; each app still wires its own API, columns, strings and
127
+ permissions — see the note at the top of `src/feedback/feedback-inbox.tsx`.
128
+ - **Wizard:** the multi-step engine (`useWizard`, `WizardContextProvider`,
129
+ `useWizardContext`, `useRhfWizardStep`, `useWizardStepValidate`,
130
+ `useWizardNextGate`, `requiredFieldsValidator`), the chrome around it
131
+ (`StepperNav`, `WizardSummary`) and the parts its steps are built from
132
+ (`WizardStep`, `WizardField`, `WizardSelectField`) — plus the types
133
+ (`WizardStepConfig`, `UseWizardOptions`, `UseWizardReturn`, `StepStatus`,
134
+ `ValidateResult`, `FieldErrors`, `SummarySection`, `SummaryItem`,
135
+ `RequiredFieldSpec`) and the label table (`WizardLabels`,
136
+ `DEFAULT_WIZARD_LABELS`, `resolveWizardLabels`). See [Wizard](#wizard) below.
137
+ - **Subpath:** date helpers at `@eifi1/ui-kit/dates`; stylesheet at `@eifi1/ui-kit/tokens.css`.
138
+
139
+ ## Forms / refs
140
+
141
+ `Input`, `Select`, `Textarea` and `AmountInput` forward their `ref` to the
142
+ underlying DOM element, so they work directly with react-hook-form:
143
+ `<Input {...field} />` (from `Controller`/`register`) attaches RHF's ref, giving
144
+ focus-and-scroll-to-error for free.
145
+
146
+ ## i18n
147
+
148
+ The package carries no translation catalog. Components with user-facing text take
149
+ **label props with English defaults** — pass your own translated strings:
150
+
151
+ ```tsx
152
+ <MultiSelect allLabel={t("all")} searchLabel={t("search")} … />
153
+ <DataTable labels={{ columns: t("table.columns"), presets: { today: t("today") }, … }} … />
154
+ ```
155
+
156
+ ## Wizard
157
+
158
+ A multi-step form engine: a step machine with per-step validation gates, collected
159
+ data, `?step=` URL sync, and skip/cancel handling — with the chrome (step
160
+ indicator, nav bar, cancel confirmation) and a summary/review step on top.
161
+
162
+ ```tsx
163
+ const wizard = useWizard<LeaseDraft>({
164
+ steps: [
165
+ { id: "unit", label: "Unit" },
166
+ { id: "tenants", label: "Tenants", validate: () => draft.tenants.length > 0 },
167
+ { id: "review", label: "Review" },
168
+ ],
169
+ onComplete: (data) => createLease(data),
170
+ onCancel: () => navigate(".."),
171
+ });
172
+
173
+ <WizardContextProvider
174
+ value={{
175
+ registerStepValidate: wizard.registerStepValidate,
176
+ setNextBlocked: wizard.setNextBlocked,
177
+ }}
178
+ >
179
+ <StepperNav wizard={wizard} title="New lease">
180
+ <WizardStep>{/* the active step's fields */}</WizardStep>
181
+ </StepperNav>
182
+ </WizardContextProvider>
183
+ ```
184
+
185
+ A step gates forward navigation three ways, and `goNext` runs all of them:
186
+ its `validate` in the config above, `useWizardStepValidate(fn)` for a step that
187
+ checks by hand, and `useRhfWizardStep(form, onValid)` for a react-hook-form step
188
+ (which also reports its own inline messages). `useWizardNextGate(blocked)`
189
+ disables Next/Skip outright.
190
+
191
+ Like the rest of the package it resolves **no strings**: a step carries a `label`
192
+ node, and the chrome takes a `labels` object (`WizardLabels`, every key optional
193
+ over an English default) — the same split `DataTable` and `TourProvider` use. An
194
+ app with i18n wraps `useWizard`/`StepperNav`/`WizardSummary` once and maps its
195
+ own keys; see Kastlan's `shared/components/wizard/app-wizard.tsx`.
196
+
197
+ **Optional peers.** `react-hook-form` is needed only by `useRhfWizardStep`, and
198
+ only as a type. `react-router` backs the `?step=` sync in `useWizard`. `sonner`
199
+ carries the "fill in the required fields" toast and is imported dynamically, on
200
+ that failure path only — pass `onValidationFailed` to route it elsewhere.
201
+
202
+ ## Shell
203
+
204
+ `TopBar`/`AppShell` are composable chrome. Feed `AppShell` your nav items and pass
205
+ a `TopBar` whose `actions` combine the shared controls (`ThemeToggle`,
206
+ `PaletteMenu`, `LanguageMenu`) with your own app-owned menus (account, etc.):
207
+
208
+ ```tsx
209
+ <AppShell nav={nav} footer={<Footer/>} topBar={
210
+ <TopBar brand={<Logo/>} actions={<>
211
+ <ThemeToggle mode={mode} onToggle={toggle} />
212
+ <PaletteMenu palettes={PALETTES} activeId={id} mode={mode} onSelect={setId} />
213
+ <LanguageMenu options={langs} current={lang} onChange={setLang} />
214
+ {/* app-owned menus */}
215
+ </>} />
216
+ }>
217
+ <Outlet />
218
+ </AppShell>
219
+ ```
@@ -0,0 +1,71 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ /**
5
+ * App-agnostic account-settings SECTIONS (feedback #333). The presentation lives
6
+ * in @hb/ui; each app injects its own auth-API handlers + translated labels and
7
+ * arranges these next to its own app-specific cards (appearance, budgets, …).
8
+ * Every section is a self-contained <Card> owning its transient form state.
9
+ */
10
+ interface ProfileSettingLabels {
11
+ title: string;
12
+ email: string;
13
+ role: string;
14
+ memberSince: string;
15
+ displayName: string;
16
+ save: string;
17
+ }
18
+ declare function ProfileSetting({ name, email, role, memberSince, value, onChange, onSave, saving, labels, }: {
19
+ name?: string | null;
20
+ email?: string | null;
21
+ role?: ReactNode;
22
+ memberSince?: ReactNode;
23
+ value: string;
24
+ onChange: (value: string) => void;
25
+ onSave: () => void;
26
+ saving?: boolean;
27
+ labels: ProfileSettingLabels;
28
+ }): react.JSX.Element;
29
+ interface PasswordSettingLabels {
30
+ title: string;
31
+ current: string;
32
+ next: string;
33
+ confirm: string;
34
+ submit: string;
35
+ tooShort: string;
36
+ mismatch: string;
37
+ }
38
+ declare function PasswordSetting({ onSubmit, pending, minLength, labels, }: {
39
+ /** Called with the validated (current, new) pair; return a promise to auto-clear on success. */
40
+ onSubmit: (currentPassword: string, newPassword: string) => void | Promise<unknown>;
41
+ pending?: boolean;
42
+ minLength?: number;
43
+ labels: PasswordSettingLabels;
44
+ }): react.JSX.Element;
45
+ interface TwoFactorSettingLabels {
46
+ status: string;
47
+ enabledText: string;
48
+ disabledText: string;
49
+ enable: string;
50
+ scanHint: string;
51
+ codeLabel: string;
52
+ verify: string;
53
+ disableSection: string;
54
+ password: string;
55
+ disable: string;
56
+ }
57
+ declare function TwoFactorSetting({ enabled, setup, onStartSetup, onEnable, onDisable, busy, labels, }: {
58
+ enabled: boolean;
59
+ /** QR + secret returned by the app's setup call; null before setup starts. */
60
+ setup: {
61
+ qrSvg: string;
62
+ secret: string;
63
+ } | null;
64
+ onStartSetup: () => void;
65
+ onEnable: (code: string) => void;
66
+ onDisable: (password: string, code: string) => void;
67
+ busy?: boolean;
68
+ labels: TwoFactorSettingLabels;
69
+ }): react.JSX.Element;
70
+
71
+ export { PasswordSetting, type PasswordSettingLabels, ProfileSetting, type ProfileSettingLabels, TwoFactorSetting, type TwoFactorSettingLabels };
@@ -0,0 +1,159 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { useState } from "react";
3
+ import { Button, Card, FIELD_WRITABLE_LOOK, Input } from "./ui.js";
4
+ import { UserAvatar } from "./user-avatar.js";
5
+ function ProfileSetting({
6
+ name,
7
+ email,
8
+ role,
9
+ memberSince,
10
+ value,
11
+ onChange,
12
+ onSave,
13
+ saving,
14
+ labels
15
+ }) {
16
+ const dirty = value.trim().length > 0 && value.trim() !== (name ?? "");
17
+ return /* @__PURE__ */ jsxs(Card, { className: "p-4 space-y-3", children: [
18
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-3", children: [
19
+ /* @__PURE__ */ jsx(UserAvatar, { name, email, size: "lg" }),
20
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
21
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: labels.title }),
22
+ /* @__PURE__ */ jsx("div", { className: "truncate font-mono text-xs text-slate-500 dark:text-slate-400", children: email ?? "\u2014" })
23
+ ] })
24
+ ] }),
25
+ /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1 text-sm", children: [
26
+ /* @__PURE__ */ jsx("span", { className: "text-slate-500", children: labels.role }),
27
+ /* @__PURE__ */ jsx("span", { children: role ?? "\u2014" }),
28
+ /* @__PURE__ */ jsx("span", { className: "text-slate-500", children: labels.memberSince }),
29
+ /* @__PURE__ */ jsx("span", { children: memberSince ?? "\u2014" })
30
+ ] }),
31
+ /* @__PURE__ */ jsx(
32
+ Input,
33
+ {
34
+ id: "display-name",
35
+ label: labels.displayName,
36
+ value,
37
+ onChange: (e) => onChange(e.target.value),
38
+ maxLength: 120
39
+ }
40
+ ),
41
+ /* @__PURE__ */ jsx(Button, { onClick: onSave, disabled: !dirty || saving, children: labels.save })
42
+ ] });
43
+ }
44
+ function PasswordSetting({
45
+ onSubmit,
46
+ pending,
47
+ minLength = 8,
48
+ labels
49
+ }) {
50
+ const [current, setCurrent] = useState("");
51
+ const [next, setNext] = useState("");
52
+ const [confirm, setConfirm] = useState("");
53
+ const [error, setError] = useState(null);
54
+ const submit = async () => {
55
+ if (next.length < minLength) {
56
+ setError(labels.tooShort);
57
+ return;
58
+ }
59
+ if (next !== confirm) {
60
+ setError(labels.mismatch);
61
+ return;
62
+ }
63
+ setError(null);
64
+ try {
65
+ await onSubmit(current, next);
66
+ setCurrent("");
67
+ setNext("");
68
+ setConfirm("");
69
+ } catch {
70
+ }
71
+ };
72
+ return /* @__PURE__ */ jsxs(Card, { className: "p-4 space-y-3", children: [
73
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: labels.title }),
74
+ /* @__PURE__ */ jsx(Input, { type: "password", autoComplete: "current-password", label: labels.current, value: current, onChange: (e) => setCurrent(e.target.value) }),
75
+ /* @__PURE__ */ jsx(Input, { type: "password", autoComplete: "new-password", label: labels.next, value: next, onChange: (e) => setNext(e.target.value) }),
76
+ /* @__PURE__ */ jsx(Input, { type: "password", autoComplete: "new-password", label: labels.confirm, value: confirm, onChange: (e) => setConfirm(e.target.value) }),
77
+ error && /* @__PURE__ */ jsx("p", { className: "text-xs text-rose-600 dark:text-rose-400", children: error }),
78
+ /* @__PURE__ */ jsx(Button, { onClick: () => void submit(), disabled: !current || !next || !confirm || pending, children: labels.submit })
79
+ ] });
80
+ }
81
+ function TwoFactorSetting({
82
+ enabled,
83
+ setup,
84
+ onStartSetup,
85
+ onEnable,
86
+ onDisable,
87
+ busy,
88
+ labels
89
+ }) {
90
+ const [code, setCode] = useState("");
91
+ const [password, setPassword] = useState("");
92
+ const [otpReadonly, setOtpReadonly] = useState(true);
93
+ const onOtpFocus = () => setOtpReadonly(false);
94
+ return /* @__PURE__ */ jsxs(Card, { className: "p-4 space-y-3", children: [
95
+ /* @__PURE__ */ jsxs("div", { className: "text-sm", children: [
96
+ labels.status,
97
+ ": ",
98
+ enabled ? labels.enabledText : labels.disabledText
99
+ ] }),
100
+ !enabled && !setup && /* @__PURE__ */ jsx(Button, { onClick: onStartSetup, disabled: busy, children: labels.enable }),
101
+ setup && /* @__PURE__ */ jsxs("div", { className: "space-y-2", children: [
102
+ /* @__PURE__ */ jsx("div", { className: "text-xs text-slate-600 dark:text-slate-400", children: labels.scanHint }),
103
+ /* @__PURE__ */ jsx("div", { className: "inline-block rounded-md bg-white p-2", children: /* @__PURE__ */ jsx("img", { alt: "QR", src: `data:image/svg+xml;base64,${setup.qrSvg}`, className: "h-48 w-48" }) }),
104
+ /* @__PURE__ */ jsx("div", { className: "break-all font-mono text-xs", children: setup.secret }),
105
+ /* @__PURE__ */ jsx(
106
+ Input,
107
+ {
108
+ inputMode: "numeric",
109
+ autoComplete: "one-time-code",
110
+ readOnly: otpReadonly,
111
+ inputClassName: FIELD_WRITABLE_LOOK,
112
+ onFocus: onOtpFocus,
113
+ maxLength: 8,
114
+ label: labels.codeLabel,
115
+ value: code,
116
+ onChange: (e) => setCode(e.target.value),
117
+ onKeyDown: (e) => {
118
+ if (e.key === "Enter" && code && !busy) {
119
+ e.preventDefault();
120
+ onEnable(code);
121
+ }
122
+ }
123
+ }
124
+ ),
125
+ /* @__PURE__ */ jsx(Button, { onClick: () => onEnable(code), disabled: !code || busy, children: labels.verify })
126
+ ] }),
127
+ enabled && /* @__PURE__ */ jsxs("div", { className: "space-y-3 border-t border-slate-200 pt-3 dark:border-slate-700", children: [
128
+ /* @__PURE__ */ jsx("div", { className: "text-sm font-medium", children: labels.disableSection }),
129
+ /* @__PURE__ */ jsx(Input, { type: "password", autoComplete: "current-password", label: labels.password, value: password, onChange: (e) => setPassword(e.target.value) }),
130
+ /* @__PURE__ */ jsx(
131
+ Input,
132
+ {
133
+ inputMode: "numeric",
134
+ autoComplete: "one-time-code",
135
+ readOnly: otpReadonly,
136
+ inputClassName: FIELD_WRITABLE_LOOK,
137
+ onFocus: onOtpFocus,
138
+ maxLength: 8,
139
+ label: labels.codeLabel,
140
+ value: code,
141
+ onChange: (e) => setCode(e.target.value),
142
+ onKeyDown: (e) => {
143
+ if (e.key === "Enter" && password && code && !busy) {
144
+ e.preventDefault();
145
+ onDisable(password, code);
146
+ }
147
+ }
148
+ }
149
+ ),
150
+ /* @__PURE__ */ jsx(Button, { variant: "danger", onClick: () => onDisable(password, code), disabled: !password || !code || busy, children: labels.disable })
151
+ ] })
152
+ ] });
153
+ }
154
+ export {
155
+ PasswordSetting,
156
+ ProfileSetting,
157
+ TwoFactorSetting
158
+ };
159
+ //# sourceMappingURL=account-settings.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/account-settings.tsx"],"sourcesContent":["import { useState } from \"react\";\nimport type { KeyboardEvent, ReactNode } from \"react\";\nimport { Button, Card, FIELD_WRITABLE_LOOK, Input } from \"./ui\";\nimport { UserAvatar } from \"./user-avatar\";\n\n/**\n * App-agnostic account-settings SECTIONS (feedback #333). The presentation lives\n * in @hb/ui; each app injects its own auth-API handlers + translated labels and\n * arranges these next to its own app-specific cards (appearance, budgets, …).\n * Every section is a self-contained <Card> owning its transient form state.\n */\n\nexport interface ProfileSettingLabels {\n title: string;\n email: string;\n role: string;\n memberSince: string;\n displayName: string;\n save: string;\n}\n\nexport function ProfileSetting({\n name,\n email,\n role,\n memberSince,\n value,\n onChange,\n onSave,\n saving,\n labels,\n}: {\n name?: string | null;\n email?: string | null;\n role?: ReactNode;\n memberSince?: ReactNode;\n value: string;\n onChange: (value: string) => void;\n onSave: () => void;\n saving?: boolean;\n labels: ProfileSettingLabels;\n}) {\n const dirty = value.trim().length > 0 && value.trim() !== (name ?? \"\");\n return (\n <Card className=\"p-4 space-y-3\">\n <div className=\"flex items-center gap-3\">\n <UserAvatar name={name} email={email} size=\"lg\" />\n <div className=\"min-w-0\">\n <div className=\"text-sm font-medium\">{labels.title}</div>\n <div className=\"truncate font-mono text-xs text-slate-500 dark:text-slate-400\">{email ?? \"—\"}</div>\n </div>\n </div>\n <div className=\"grid grid-cols-[max-content_1fr] gap-x-3 gap-y-1 text-sm\">\n <span className=\"text-slate-500\">{labels.role}</span>\n <span>{role ?? \"—\"}</span>\n <span className=\"text-slate-500\">{labels.memberSince}</span>\n <span>{memberSince ?? \"—\"}</span>\n </div>\n <Input\n id=\"display-name\"\n label={labels.displayName}\n value={value}\n onChange={(e) => onChange(e.target.value)}\n maxLength={120}\n />\n <Button onClick={onSave} disabled={!dirty || saving}>\n {labels.save}\n </Button>\n </Card>\n );\n}\n\nexport interface PasswordSettingLabels {\n title: string;\n current: string;\n next: string;\n confirm: string;\n submit: string;\n tooShort: string;\n mismatch: string;\n}\n\nexport function PasswordSetting({\n onSubmit,\n pending,\n minLength = 8,\n labels,\n}: {\n /** Called with the validated (current, new) pair; return a promise to auto-clear on success. */\n onSubmit: (currentPassword: string, newPassword: string) => void | Promise<unknown>;\n pending?: boolean;\n minLength?: number;\n labels: PasswordSettingLabels;\n}) {\n const [current, setCurrent] = useState(\"\");\n const [next, setNext] = useState(\"\");\n const [confirm, setConfirm] = useState(\"\");\n const [error, setError] = useState<string | null>(null);\n\n const submit = async () => {\n if (next.length < minLength) {\n setError(labels.tooShort);\n return;\n }\n if (next !== confirm) {\n setError(labels.mismatch);\n return;\n }\n setError(null);\n try {\n await onSubmit(current, next);\n setCurrent(\"\");\n setNext(\"\");\n setConfirm(\"\");\n } catch {\n // The app surfaces the failure (e.g. a toast); keep the fields for a retry.\n }\n };\n\n return (\n <Card className=\"p-4 space-y-3\">\n <div className=\"text-sm font-medium\">{labels.title}</div>\n <Input type=\"password\" autoComplete=\"current-password\" label={labels.current} value={current} onChange={(e) => setCurrent(e.target.value)} />\n <Input type=\"password\" autoComplete=\"new-password\" label={labels.next} value={next} onChange={(e) => setNext(e.target.value)} />\n <Input type=\"password\" autoComplete=\"new-password\" label={labels.confirm} value={confirm} onChange={(e) => setConfirm(e.target.value)} />\n {error && <p className=\"text-xs text-rose-600 dark:text-rose-400\">{error}</p>}\n <Button onClick={() => void submit()} disabled={!current || !next || !confirm || pending}>\n {labels.submit}\n </Button>\n </Card>\n );\n}\n\nexport interface TwoFactorSettingLabels {\n status: string;\n enabledText: string;\n disabledText: string;\n enable: string;\n scanHint: string;\n codeLabel: string;\n verify: string;\n disableSection: string;\n password: string;\n disable: string;\n}\n\nexport function TwoFactorSetting({\n enabled,\n setup,\n onStartSetup,\n onEnable,\n onDisable,\n busy,\n labels,\n}: {\n enabled: boolean;\n /** QR + secret returned by the app's setup call; null before setup starts. */\n setup: { qrSvg: string; secret: string } | null;\n onStartSetup: () => void;\n onEnable: (code: string) => void;\n onDisable: (password: string, code: string) => void;\n busy?: boolean;\n labels: TwoFactorSettingLabels;\n}) {\n const [code, setCode] = useState(\"\");\n const [password, setPassword] = useState(\"\");\n // Browsers autofill one-time-code fields with the saved username; keep the\n // field readOnly until focus to block that. It therefore has to keep the LOOK of\n // an editable field (FIELD_WRITABLE_LOOK): this is the one place where readOnly\n // does not mean \"you may not type here\", and without the opt-out the field would\n // sit there greyed until the moment it is focused.\n const [otpReadonly, setOtpReadonly] = useState(true);\n const onOtpFocus = () => setOtpReadonly(false);\n\n return (\n <Card className=\"p-4 space-y-3\">\n <div className=\"text-sm\">\n {labels.status}: {enabled ? labels.enabledText : labels.disabledText}\n </div>\n\n {!enabled && !setup && (\n <Button onClick={onStartSetup} disabled={busy}>\n {labels.enable}\n </Button>\n )}\n\n {setup && (\n <div className=\"space-y-2\">\n <div className=\"text-xs text-slate-600 dark:text-slate-400\">{labels.scanHint}</div>\n <div className=\"inline-block rounded-md bg-white p-2\">\n <img alt=\"QR\" src={`data:image/svg+xml;base64,${setup.qrSvg}`} className=\"h-48 w-48\" />\n </div>\n <div className=\"break-all font-mono text-xs\">{setup.secret}</div>\n <Input\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n readOnly={otpReadonly}\n inputClassName={FIELD_WRITABLE_LOOK}\n onFocus={onOtpFocus}\n maxLength={8}\n label={labels.codeLabel}\n value={code}\n onChange={(e) => setCode(e.target.value)}\n onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\" && code && !busy) {\n e.preventDefault();\n onEnable(code);\n }\n }}\n />\n <Button onClick={() => onEnable(code)} disabled={!code || busy}>\n {labels.verify}\n </Button>\n </div>\n )}\n\n {enabled && (\n <div className=\"space-y-3 border-t border-slate-200 pt-3 dark:border-slate-700\">\n <div className=\"text-sm font-medium\">{labels.disableSection}</div>\n <Input type=\"password\" autoComplete=\"current-password\" label={labels.password} value={password} onChange={(e) => setPassword(e.target.value)} />\n <Input\n inputMode=\"numeric\"\n autoComplete=\"one-time-code\"\n readOnly={otpReadonly}\n inputClassName={FIELD_WRITABLE_LOOK}\n onFocus={onOtpFocus}\n maxLength={8}\n label={labels.codeLabel}\n value={code}\n onChange={(e) => setCode(e.target.value)}\n onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {\n if (e.key === \"Enter\" && password && code && !busy) {\n e.preventDefault();\n onDisable(password, code);\n }\n }}\n />\n <Button variant=\"danger\" onClick={() => onDisable(password, code)} disabled={!password || !code || busy}>\n {labels.disable}\n </Button>\n </div>\n )}\n </Card>\n );\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,20 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+
4
+ type AlertTone = "danger" | "warning" | "neutral";
5
+ /** The tone's border + surface on their own — for a caller that already has a box
6
+ * with its own radius and padding (e.g. a Card). Remember to shave 1px off that
7
+ * padding for the colored tones, whose border is 2px wide. */
8
+ declare function toneFrameClass(tone: AlertTone): string;
9
+ /** Single source for the warning-callout frame (feedback #277): the tone's border
10
+ * and surface plus this component's own radius and padding, the latter
11
+ * compensating the 2px colored border so toggling tones never shifts layout. */
12
+ declare function alertFrameClass(tone: AlertTone): string;
13
+ /** Static warning/danger callout box with the shared frame and an icon. */
14
+ declare function AlertBanner({ tone, className, children, }: {
15
+ tone?: Exclude<AlertTone, "neutral">;
16
+ className?: string;
17
+ children: ReactNode;
18
+ }): react.JSX.Element;
19
+
20
+ export { AlertBanner, type AlertTone, alertFrameClass, toneFrameClass };
@@ -0,0 +1,39 @@
1
+ import { jsx, jsxs } from "react/jsx-runtime";
2
+ import { AlertTriangle } from "lucide-react";
3
+ import { cn } from "../lib/cn.js";
4
+ const TONE_FRAME = {
5
+ danger: "border-2 border-rose-400 bg-rose-50 dark:border-rose-600 dark:bg-rose-900/20",
6
+ warning: "border-2 border-amber-300 bg-amber-50 dark:border-amber-700/60 dark:bg-amber-900/20",
7
+ neutral: "border border-slate-200 dark:border-slate-700"
8
+ };
9
+ function toneFrameClass(tone) {
10
+ return TONE_FRAME[tone];
11
+ }
12
+ function alertFrameClass(tone) {
13
+ const box = tone === "neutral" ? "rounded-md p-3" : "rounded-md p-[11px]";
14
+ return `${box} ${TONE_FRAME[tone]}`;
15
+ }
16
+ const TONE_TEXT = {
17
+ danger: "text-rose-900 dark:text-rose-100",
18
+ warning: "text-amber-900 dark:text-amber-200"
19
+ };
20
+ const TONE_ICON = {
21
+ danger: "text-rose-600 dark:text-rose-400",
22
+ warning: "text-amber-600 dark:text-amber-400"
23
+ };
24
+ function AlertBanner({
25
+ tone = "danger",
26
+ className,
27
+ children
28
+ }) {
29
+ return /* @__PURE__ */ jsxs("div", { className: cn("flex items-start gap-2 text-sm", alertFrameClass(tone), TONE_TEXT[tone], className), children: [
30
+ /* @__PURE__ */ jsx(AlertTriangle, { className: cn("mt-0.5 size-4 shrink-0", TONE_ICON[tone]) }),
31
+ /* @__PURE__ */ jsx("span", { className: "flex-1", children })
32
+ ] });
33
+ }
34
+ export {
35
+ AlertBanner,
36
+ alertFrameClass,
37
+ toneFrameClass
38
+ };
39
+ //# sourceMappingURL=alert-banner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/components/alert-banner.tsx"],"sourcesContent":["import type { ReactNode } from \"react\";\nimport { AlertTriangle } from \"lucide-react\";\nimport { cn } from \"../lib/cn\";\n\nexport type AlertTone = \"danger\" | \"warning\" | \"neutral\";\n\n/** Border + surface per tone, WITHOUT a radius or padding, so a caller that owns\n * its own box (a Card, say) can take the tone alone. Single source for both\n * {@link alertFrameClass} and {@link toneFrameClass}.\n *\n * The colored tones are 2px, not 1px, and that is load-bearing rather than\n * decorative. A 1px border is 1.25 device pixels on a 125%-scaled display — the\n * usual Windows setting — so whether it renders as a solid line or as two\n * half-lit pixels depends on where the box happens to land in device-pixel space.\n * The same card then shows a heavy left edge and a right edge that fades out,\n * which is what feedback #264, #277, #491 and #498 all reported. A 2px border is\n * 2.5 device pixels: it always covers at least two pixels fully, on both edges,\n * at any offset. Callers compensate the extra pixel in their own padding so\n * switching tones never shifts the layout. */\nconst TONE_FRAME: Record<AlertTone, string> = {\n danger: \"border-2 border-rose-400 bg-rose-50 dark:border-rose-600 dark:bg-rose-900/20\",\n warning: \"border-2 border-amber-300 bg-amber-50 dark:border-amber-700/60 dark:bg-amber-900/20\",\n neutral: \"border border-slate-200 dark:border-slate-700\",\n};\n\n/** The tone's border + surface on their own — for a caller that already has a box\n * with its own radius and padding (e.g. a Card). Remember to shave 1px off that\n * padding for the colored tones, whose border is 2px wide. */\nexport function toneFrameClass(tone: AlertTone): string {\n return TONE_FRAME[tone];\n}\n\n/** Single source for the warning-callout frame (feedback #277): the tone's border\n * and surface plus this component's own radius and padding, the latter\n * compensating the 2px colored border so toggling tones never shifts layout. */\nexport function alertFrameClass(tone: AlertTone): string {\n const box = tone === \"neutral\" ? \"rounded-md p-3\" : \"rounded-md p-[11px]\";\n return `${box} ${TONE_FRAME[tone]}`;\n}\n\nconst TONE_TEXT: Record<Exclude<AlertTone, \"neutral\">, string> = {\n danger: \"text-rose-900 dark:text-rose-100\",\n warning: \"text-amber-900 dark:text-amber-200\",\n};\n\nconst TONE_ICON: Record<Exclude<AlertTone, \"neutral\">, string> = {\n danger: \"text-rose-600 dark:text-rose-400\",\n warning: \"text-amber-600 dark:text-amber-400\",\n};\n\n/** Static warning/danger callout box with the shared frame and an icon. */\nexport function AlertBanner({\n tone = \"danger\",\n className,\n children,\n}: {\n tone?: Exclude<AlertTone, \"neutral\">;\n className?: string;\n children: ReactNode;\n}) {\n return (\n <div className={cn(\"flex items-start gap-2 text-sm\", alertFrameClass(tone), TONE_TEXT[tone], className)}>\n <AlertTriangle className={cn(\"mt-0.5 size-4 shrink-0\", TONE_ICON[tone])} />\n <span className=\"flex-1\">{children}</span>\n </div>\n );\n}\n"],"mappings":"AA6DI,SACE,KADF;AA5DJ,SAAS,qBAAqB;AAC9B,SAAS,UAAU;AAiBnB,MAAM,aAAwC;AAAA,EAC5C,QAAQ;AAAA,EACR,SAAS;AAAA,EACT,SAAS;AACX;AAKO,SAAS,eAAe,MAAyB;AACtD,SAAO,WAAW,IAAI;AACxB;AAKO,SAAS,gBAAgB,MAAyB;AACvD,QAAM,MAAM,SAAS,YAAY,mBAAmB;AACpD,SAAO,GAAG,GAAG,IAAI,WAAW,IAAI,CAAC;AACnC;AAEA,MAAM,YAA2D;AAAA,EAC/D,QAAQ;AAAA,EACR,SAAS;AACX;AAEA,MAAM,YAA2D;AAAA,EAC/D,QAAQ;AAAA,EACR,SAAS;AACX;AAGO,SAAS,YAAY;AAAA,EAC1B,OAAO;AAAA,EACP;AAAA,EACA;AACF,GAIG;AACD,SACE,qBAAC,SAAI,WAAW,GAAG,kCAAkC,gBAAgB,IAAI,GAAG,UAAU,IAAI,GAAG,SAAS,GACpG;AAAA,wBAAC,iBAAc,WAAW,GAAG,0BAA0B,UAAU,IAAI,CAAC,GAAG;AAAA,IACzE,oBAAC,UAAK,WAAU,UAAU,UAAS;AAAA,KACrC;AAEJ;","names":[]}
@@ -0,0 +1,117 @@
1
+ import * as react from 'react';
2
+ import { ReactNode } from 'react';
3
+ import { CalculatorButtonLabels } from './calculator.js';
4
+ import { NumberPadSheetLabels } from './numpad-sheet.js';
5
+
6
+ interface AmountInputProps {
7
+ value: string;
8
+ onChange: (value: string) => void;
9
+ currency?: string;
10
+ onCurrencyChange?: (code: string) => void;
11
+ placeholder?: string;
12
+ label?: ReactNode;
13
+ disabled?: boolean;
14
+ /** Required and unanswered — {@link FIELD_INVALID}. See {@link Input}'s `invalid`. */
15
+ invalid?: boolean;
16
+ className?: string;
17
+ id?: string;
18
+ ariaLabel?: string;
19
+ /** Every user-facing string this component and the two it composes own, so a
20
+ * translating host can supply its own. The package carries no translation
21
+ * catalog (see the README) — English defaults, each key optional, so the other
22
+ * consumer apps are unaffected. `currency` names the picker chip and is
23
+ * suffixed with the selected code; the rest are pass-through. */
24
+ labels?: {
25
+ currency?: string;
26
+ currencySearch?: string;
27
+ /** Names the calculator TRIGGER this component renders. `calculator` below
28
+ * names the controls inside the popover it opens. */
29
+ calculatorTrigger?: string;
30
+ pad?: NumberPadSheetLabels;
31
+ calculator?: CalculatorButtonLabels;
32
+ };
33
+ /** Focus the field on mount — on mobile this also opens the numpad sheet, so a
34
+ * new-transaction form can jump straight to amount entry (feedback #70). */
35
+ autoFocus?: boolean;
36
+ /**
37
+ * Tint the typed figure by what it will DO (Keksdose feedback #167).
38
+ *
39
+ * The direction is chosen with a toggle beside the field rather than typed, so
40
+ * the digits alone say nothing about which way the money goes — on the one screen
41
+ * where getting that backwards is the expensive mistake. Colour says it
42
+ * continuously, and it costs no layout. It stays the MAIN indication (live #201
43
+ * rework: *"The main indication shall still be the color"*); {@link negative}
44
+ * renders the sign as a second, quieter signal in front of the figure.
45
+ *
46
+ * Never the ONLY signal: the toggle it mirrors is right there, labelled, so this
47
+ * is reinforcement rather than the sole carrier of the meaning.
48
+ */
49
+ tone?: "neutral" | "outflow" | "inflow";
50
+ /**
51
+ * The SIGN of the figure, owned by the caller's direction control (Keksdose live
52
+ * #201 rework: *"Outflow should be negative and inflow positive … the sign shall
53
+ * be displayed but not necessarily be typed in"*).
54
+ *
55
+ * `value` stays the MAGNITUDE. The minus is glued on at render time and stripped
56
+ * off again before anything is reported back, so the caller never sees its own
57
+ * prefix echoed and no code path can end up applying it twice. That is the whole
58
+ * cure: there is exactly ONE place the sign lives, and it is not the text.
59
+ *
60
+ * Because it is rendered rather than stored it cannot be deleted (the next render
61
+ * puts it back), a select-all-and-retype keeps the direction, and the caret needs
62
+ * no minding.
63
+ */
64
+ negative?: boolean;
65
+ /**
66
+ * A sign the user TYPED, or a calculation that resolved to a signed number.
67
+ *
68
+ * SET, never flip. A "-" means *"this is an outflow"*, so typing it twice says the
69
+ * same thing twice — which is exactly what the rework asks for (*"Subtracting
70
+ * values twice resulting in inflow -> outflow and back to inflow seems not
71
+ * reasonable"*). "+" says the other direction.
72
+ *
73
+ * Its presence is what ARMS the whole sign behaviour. A field with no direction
74
+ * control — an account's starting balance, a loan payment, a holding's cost —
75
+ * passes neither prop and keeps a text that is the whole truth, minus included.
76
+ */
77
+ onNegativeChange?: (negative: boolean) => void;
78
+ /**
79
+ * How the field presents itself ON A PHONE (Keksdose feedback #176).
80
+ *
81
+ * `"field"` (default) is the labelled, bordered input used everywhere else.
82
+ *
83
+ * `"display"` keeps every mechanic — the same `<input>`, the same numpad sheet,
84
+ * the same currency picker, the same commit/blur contract — and only drops the
85
+ * chrome: no border, no fill, no floating label, the figure set at display size.
86
+ * On a form whose whole point is one number, a bordered `text-sm` box draws the
87
+ * amount at the same weight as the memo beside it; this makes it the headline
88
+ * instead. It stays fully editable — the underline and the caret are what say so.
89
+ *
90
+ * Phone-only by construction: it keys off the same breakpoint as the numpad, so
91
+ * from `md` up the field is byte-for-byte the normal one and a desktop grid keeps
92
+ * its column rhythm.
93
+ */
94
+ variant?: "field" | "display";
95
+ /**
96
+ * Where the figure sits in its row, for `variant="display"` only (Keksdose
97
+ * feedback #176 rework).
98
+ *
99
+ * `"start"` (default) keeps it flush left, in the reading column the fields
100
+ * below it share — and it is what the entry forms use, because a figure that
101
+ * starts at a known edge is legible at any width: when the value outgrows the
102
+ * field, the digits that scroll out of sight are the trailing ones, not the
103
+ * magnitude (Keksdose feedback #430 rework).
104
+ *
105
+ * `"center"` makes it a centred headline. It centres in the width that REMAINS
106
+ * after the trailing currency control, not in the whole box: the control is
107
+ * pinned to the right edge, so a plain `text-center` would centre the figure
108
+ * under it and the last digits would sit behind the chip. This used to mirror
109
+ * the control's reservation onto the left instead, which centred correctly but
110
+ * spent twice the control's width on padding — fatal on a 375px screen, where
111
+ * it left a 36px figure 16px to live in.
112
+ */
113
+ align?: "start" | "center";
114
+ }
115
+ declare const AmountInput: react.ForwardRefExoticComponent<AmountInputProps & react.RefAttributes<HTMLInputElement>>;
116
+
117
+ export { AmountInput };