@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.
- package/README.md +219 -0
- package/dist/components/account-settings.d.ts +71 -0
- package/dist/components/account-settings.js +159 -0
- package/dist/components/account-settings.js.map +1 -0
- package/dist/components/alert-banner.d.ts +20 -0
- package/dist/components/alert-banner.js +39 -0
- package/dist/components/alert-banner.js.map +1 -0
- package/dist/components/amount-input.d.ts +117 -0
- package/dist/components/amount-input.js +243 -0
- package/dist/components/amount-input.js.map +1 -0
- package/dist/components/calculator.d.ts +42 -0
- package/dist/components/calculator.js +161 -0
- package/dist/components/calculator.js.map +1 -0
- package/dist/components/chart.d.ts +58 -0
- package/dist/components/chart.js +158 -0
- package/dist/components/chart.js.map +1 -0
- package/dist/components/combobox-core.d.ts +77 -0
- package/dist/components/combobox-core.js +263 -0
- package/dist/components/combobox-core.js.map +1 -0
- package/dist/components/combobox.d.ts +141 -0
- package/dist/components/combobox.js +550 -0
- package/dist/components/combobox.js.map +1 -0
- package/dist/components/currency-select.d.ts +37 -0
- package/dist/components/currency-select.js +131 -0
- package/dist/components/currency-select.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +28 -0
- package/dist/components/data-table-filter-popover.js +201 -0
- package/dist/components/data-table-filter-popover.js.map +1 -0
- package/dist/components/data-table-filters.d.ts +5 -0
- package/dist/components/data-table-filters.js +118 -0
- package/dist/components/data-table-filters.js.map +1 -0
- package/dist/components/data-table-labels.d.ts +33 -0
- package/dist/components/data-table-labels.js +53 -0
- package/dist/components/data-table-labels.js.map +1 -0
- package/dist/components/data-table-pagination.d.ts +18 -0
- package/dist/components/data-table-pagination.js +92 -0
- package/dist/components/data-table-pagination.js.map +1 -0
- package/dist/components/data-table-sort.d.ts +23 -0
- package/dist/components/data-table-sort.js +55 -0
- package/dist/components/data-table-sort.js.map +1 -0
- package/dist/components/data-table.d.ts +5 -0
- package/dist/components/data-table.js +926 -0
- package/dist/components/data-table.js.map +1 -0
- package/dist/components/date-picker.d.ts +85 -0
- package/dist/components/date-picker.js +253 -0
- package/dist/components/date-picker.js.map +1 -0
- package/dist/components/dropdown.d.ts +98 -0
- package/dist/components/dropdown.js +122 -0
- package/dist/components/dropdown.js.map +1 -0
- package/dist/components/entity-combobox.d.ts +49 -0
- package/dist/components/entity-combobox.js +115 -0
- package/dist/components/entity-combobox.js.map +1 -0
- package/dist/components/file-dropzone.d.ts +18 -0
- package/dist/components/file-dropzone.js +101 -0
- package/dist/components/file-dropzone.js.map +1 -0
- package/dist/components/full-bleed-dialog.d.ts +57 -0
- package/dist/components/full-bleed-dialog.js +68 -0
- package/dist/components/full-bleed-dialog.js.map +1 -0
- package/dist/components/grouped-picker.d.ts +37 -0
- package/dist/components/grouped-picker.js +87 -0
- package/dist/components/grouped-picker.js.map +1 -0
- package/dist/components/hover-menu.d.ts +16 -0
- package/dist/components/hover-menu.js +113 -0
- package/dist/components/hover-menu.js.map +1 -0
- package/dist/components/mini-calendar.d.ts +33 -0
- package/dist/components/mini-calendar.js +133 -0
- package/dist/components/mini-calendar.js.map +1 -0
- package/dist/components/modal.d.ts +76 -0
- package/dist/components/modal.js +156 -0
- package/dist/components/modal.js.map +1 -0
- package/dist/components/multi-entity-combobox.d.ts +47 -0
- package/dist/components/multi-entity-combobox.js +116 -0
- package/dist/components/multi-entity-combobox.js.map +1 -0
- package/dist/components/multi-select.d.ts +39 -0
- package/dist/components/multi-select.js +132 -0
- package/dist/components/multi-select.js.map +1 -0
- package/dist/components/number-input.d.ts +74 -0
- package/dist/components/number-input.js +114 -0
- package/dist/components/number-input.js.map +1 -0
- package/dist/components/numpad-sheet.d.ts +45 -0
- package/dist/components/numpad-sheet.js +102 -0
- package/dist/components/numpad-sheet.js.map +1 -0
- package/dist/components/picker-sheet.d.ts +44 -0
- package/dist/components/picker-sheet.js +78 -0
- package/dist/components/picker-sheet.js.map +1 -0
- package/dist/components/popover.d.ts +20 -0
- package/dist/components/popover.js +46 -0
- package/dist/components/popover.js.map +1 -0
- package/dist/components/search-field.d.ts +52 -0
- package/dist/components/search-field.js +64 -0
- package/dist/components/search-field.js.map +1 -0
- package/dist/components/settings-fields.d.ts +39 -0
- package/dist/components/settings-fields.js +17 -0
- package/dist/components/settings-fields.js.map +1 -0
- package/dist/components/swipeable-row.d.ts +60 -0
- package/dist/components/swipeable-row.js +115 -0
- package/dist/components/swipeable-row.js.map +1 -0
- package/dist/components/toggle-group.d.ts +34 -0
- package/dist/components/toggle-group.js +90 -0
- package/dist/components/toggle-group.js.map +1 -0
- package/dist/components/tooltip.d.ts +86 -0
- package/dist/components/tooltip.js +171 -0
- package/dist/components/tooltip.js.map +1 -0
- package/dist/components/ui.d.ts +239 -0
- package/dist/components/ui.js +512 -0
- package/dist/components/ui.js.map +1 -0
- package/dist/components/use-mobile-reveal.d.ts +31 -0
- package/dist/components/use-mobile-reveal.js +33 -0
- package/dist/components/use-mobile-reveal.js.map +1 -0
- package/dist/components/use-table-state.d.ts +54 -0
- package/dist/components/use-table-state.js +153 -0
- package/dist/components/use-table-state.js.map +1 -0
- package/dist/components/user-avatar.d.ts +23 -0
- package/dist/components/user-avatar.js +33 -0
- package/dist/components/user-avatar.js.map +1 -0
- package/dist/components/wizard-stepper.d.ts +14 -0
- package/dist/components/wizard-stepper.js +27 -0
- package/dist/components/wizard-stepper.js.map +1 -0
- package/dist/data-table-filters-aXyf0Xub.d.ts +261 -0
- package/dist/feedback/feedback-attachment.d.ts +63 -0
- package/dist/feedback/feedback-attachment.js +160 -0
- package/dist/feedback/feedback-attachment.js.map +1 -0
- package/dist/feedback/feedback-dialog.d.ts +72 -0
- package/dist/feedback/feedback-dialog.js +109 -0
- package/dist/feedback/feedback-dialog.js.map +1 -0
- package/dist/feedback/feedback-inbox.d.ts +226 -0
- package/dist/feedback/feedback-inbox.js +315 -0
- package/dist/feedback/feedback-inbox.js.map +1 -0
- package/dist/hooks/use-anchored-panel.d.ts +80 -0
- package/dist/hooks/use-anchored-panel.js +71 -0
- package/dist/hooks/use-anchored-panel.js.map +1 -0
- package/dist/hooks/use-anchored-rect.d.ts +24 -0
- package/dist/hooks/use-anchored-rect.js +40 -0
- package/dist/hooks/use-anchored-rect.js.map +1 -0
- package/dist/hooks/use-body-scroll-lock.d.ts +9 -0
- package/dist/hooks/use-body-scroll-lock.js +41 -0
- package/dist/hooks/use-body-scroll-lock.js.map +1 -0
- package/dist/hooks/use-close-transition.d.ts +41 -0
- package/dist/hooks/use-close-transition.js +36 -0
- package/dist/hooks/use-close-transition.js.map +1 -0
- package/dist/hooks/use-dismiss.d.ts +34 -0
- package/dist/hooks/use-dismiss.js +41 -0
- package/dist/hooks/use-dismiss.js.map +1 -0
- package/dist/hooks/use-media-query.d.ts +8 -0
- package/dist/hooks/use-media-query.js +18 -0
- package/dist/hooks/use-media-query.js.map +1 -0
- package/dist/hooks/use-overlay-history.d.ts +10 -0
- package/dist/hooks/use-overlay-history.js +99 -0
- package/dist/hooks/use-overlay-history.js.map +1 -0
- package/dist/hooks/use-row-swipe.d.ts +30 -0
- package/dist/hooks/use-row-swipe.js +128 -0
- package/dist/hooks/use-row-swipe.js.map +1 -0
- package/dist/index.d.ts +75 -0
- package/dist/index.js +90 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/calc.d.ts +89 -0
- package/dist/lib/calc.js +180 -0
- package/dist/lib/calc.js.map +1 -0
- package/dist/lib/cn.d.ts +6 -0
- package/dist/lib/cn.js +7 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/dates.d.ts +84 -0
- package/dist/lib/dates.js +108 -0
- package/dist/lib/dates.js.map +1 -0
- package/dist/lib/logger.d.ts +17 -0
- package/dist/lib/logger.js +30 -0
- package/dist/lib/logger.js.map +1 -0
- package/dist/lib/safe-storage.d.ts +23 -0
- package/dist/lib/safe-storage.js +20 -0
- package/dist/lib/safe-storage.js.map +1 -0
- package/dist/search/command-palette.d.ts +71 -0
- package/dist/search/command-palette.js +198 -0
- package/dist/search/command-palette.js.map +1 -0
- package/dist/shell/app-shell.d.ts +64 -0
- package/dist/shell/app-shell.js +237 -0
- package/dist/shell/app-shell.js.map +1 -0
- package/dist/shell/option-switcher-menu.d.ts +24 -0
- package/dist/shell/option-switcher-menu.js +44 -0
- package/dist/shell/option-switcher-menu.js.map +1 -0
- package/dist/shell/role-switcher.d.ts +19 -0
- package/dist/shell/role-switcher.js +28 -0
- package/dist/shell/role-switcher.js.map +1 -0
- package/dist/shell/top-bar.d.ts +15 -0
- package/dist/shell/top-bar.js +29 -0
- package/dist/shell/top-bar.js.map +1 -0
- package/dist/shell/topbar-action-menu.d.ts +44 -0
- package/dist/shell/topbar-action-menu.js +49 -0
- package/dist/shell/topbar-action-menu.js.map +1 -0
- package/dist/shell/topbar-controls.d.ts +54 -0
- package/dist/shell/topbar-controls.js +152 -0
- package/dist/shell/topbar-controls.js.map +1 -0
- package/dist/theme/chart-palette.d.ts +60 -0
- package/dist/theme/chart-palette.js +79 -0
- package/dist/theme/chart-palette.js.map +1 -0
- package/dist/theme/palette-presets.d.ts +39 -0
- package/dist/theme/palette-presets.js +440 -0
- package/dist/theme/palette-presets.js.map +1 -0
- package/dist/theme/palette-store.d.ts +45 -0
- package/dist/theme/palette-store.js +52 -0
- package/dist/theme/palette-store.js.map +1 -0
- package/dist/theme/theme-store.d.ts +57 -0
- package/dist/theme/theme-store.js +96 -0
- package/dist/theme/theme-store.js.map +1 -0
- package/dist/tour/tour.d.ts +104 -0
- package/dist/tour/tour.js +351 -0
- package/dist/tour/tour.js.map +1 -0
- package/dist/wizard/stepper-nav.d.ts +30 -0
- package/dist/wizard/stepper-nav.js +146 -0
- package/dist/wizard/stepper-nav.js.map +1 -0
- package/dist/wizard/types.d.ts +124 -0
- package/dist/wizard/types.js +24 -0
- package/dist/wizard/types.js.map +1 -0
- package/dist/wizard/use-wizard.d.ts +6 -0
- package/dist/wizard/use-wizard.js +222 -0
- package/dist/wizard/use-wizard.js.map +1 -0
- package/dist/wizard/validation.d.ts +23 -0
- package/dist/wizard/validation.js +13 -0
- package/dist/wizard/validation.js.map +1 -0
- package/dist/wizard/wizard-context.d.ts +19 -0
- package/dist/wizard/wizard-context.js +15 -0
- package/dist/wizard/wizard-context.js.map +1 -0
- package/dist/wizard/wizard-step.d.ts +11 -0
- package/dist/wizard/wizard-step.js +12 -0
- package/dist/wizard/wizard-step.js.map +1 -0
- package/dist/wizard/wizard-summary.d.ts +18 -0
- package/dist/wizard/wizard-summary.js +41 -0
- package/dist/wizard/wizard-summary.js.map +1 -0
- package/package.json +87 -0
- package/tokens.css +254 -0
package/README.md
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
# @eifi1/ui-kit
|
|
2
|
+
|
|
3
|
+
[](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 };
|