@eifi1/ui-kit 0.7.1 → 0.8.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 +32 -17
- package/dist/chart.d.ts +3 -2
- package/dist/components/alert-banner.d.ts +59 -10
- package/dist/components/alert-banner.js +114 -8
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +3 -0
- package/dist/components/button-group.d.ts +26 -0
- package/dist/components/button-group.js +49 -0
- package/dist/components/button-group.js.map +1 -0
- package/dist/components/calculator.d.ts +3 -0
- package/dist/components/chart-zoom.d.ts +63 -14
- package/dist/components/chart-zoom.js +82 -16
- package/dist/components/chart-zoom.js.map +1 -1
- package/dist/components/chip.d.ts +67 -2
- package/dist/components/chip.js +73 -8
- package/dist/components/chip.js.map +1 -1
- package/dist/components/confirm-dialog.d.ts +120 -0
- package/dist/components/confirm-dialog.js +96 -0
- package/dist/components/confirm-dialog.js.map +1 -0
- package/dist/components/copy-button.d.ts +62 -0
- package/dist/components/copy-button.js +95 -0
- package/dist/components/copy-button.js.map +1 -0
- package/dist/components/data-table-filter-popover.d.ts +1 -1
- package/dist/components/data-table-filters.d.ts +1 -1
- package/dist/components/data-table-pagination.d.ts +5 -1
- package/dist/components/data-table-pagination.js +71 -58
- package/dist/components/data-table-pagination.js.map +1 -1
- package/dist/components/data-table-sort.d.ts +28 -5
- package/dist/components/data-table-sort.js +11 -7
- package/dist/components/data-table-sort.js.map +1 -1
- package/dist/components/data-table.d.ts +1 -1
- package/dist/components/data-table.js +544 -423
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +51 -5
- package/dist/components/date-picker.js +152 -40
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +60 -0
- package/dist/components/description-list.js +112 -0
- package/dist/components/description-list.js.map +1 -0
- package/dist/components/disclosure.d.ts +37 -2
- package/dist/components/disclosure.js +14 -5
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/facing-pair.d.ts +2 -0
- package/dist/components/file-button.d.ts +17 -1
- package/dist/components/file-button.js +8 -1
- package/dist/components/file-button.js.map +1 -1
- package/dist/components/file-dropzone.d.ts +54 -9
- package/dist/components/file-dropzone.js +100 -70
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.d.ts +107 -0
- package/dist/components/floating-panel.js +206 -0
- package/dist/components/floating-panel.js.map +1 -0
- package/dist/components/full-bleed-dialog.d.ts +12 -1
- package/dist/components/full-bleed-dialog.js +26 -5
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/modal.d.ts +6 -2
- package/dist/components/modal.js +2 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/number-field.d.ts +3 -0
- package/dist/components/number-input.d.ts +3 -0
- package/dist/components/numpad-sheet.d.ts +3 -0
- package/dist/components/progress-bar.d.ts +54 -0
- package/dist/components/progress-bar.js +94 -0
- package/dist/components/progress-bar.js.map +1 -0
- package/dist/components/scroll-area.d.ts +53 -0
- package/dist/components/scroll-area.js +86 -0
- package/dist/components/scroll-area.js.map +1 -0
- package/dist/components/separator.d.ts +23 -0
- package/dist/components/separator.js +24 -0
- package/dist/components/separator.js.map +1 -0
- package/dist/components/series-chart-ticks.d.ts +34 -1
- package/dist/components/series-chart-ticks.js +83 -1
- package/dist/components/series-chart-ticks.js.map +1 -1
- package/dist/components/series-chart.d.ts +314 -19
- package/dist/components/series-chart.js +510 -120
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/skeleton.d.ts +36 -0
- package/dist/components/skeleton.js +35 -0
- package/dist/components/skeleton.js.map +1 -0
- package/dist/components/stat-tile.d.ts +60 -6
- package/dist/components/stat-tile.js +60 -26
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/table.d.ts +83 -0
- package/dist/components/table.js +166 -0
- package/dist/components/table.js.map +1 -0
- package/dist/components/toggle-legend.d.ts +3 -2
- package/dist/components/toggle-legend.js +1 -1
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tree-view.d.ts +129 -0
- package/dist/components/tree-view.js +376 -0
- package/dist/components/tree-view.js.map +1 -0
- package/dist/components/treemap.d.ts +9 -2
- package/dist/components/treemap.js +2 -1
- package/dist/components/treemap.js.map +1 -1
- package/dist/components/ui.d.ts +87 -13
- package/dist/components/ui.js +111 -29
- package/dist/components/ui.js.map +1 -1
- package/dist/components/use-table-state.d.ts +1 -1
- package/dist/{data-table-filters-noy0Abvi.d.ts → data-table-filters-CF1PXqjQ.d.ts} +73 -3
- package/dist/data-table.d.ts +2 -2
- package/dist/data-table.js.map +1 -1
- package/dist/hooks/use-copy-to-clipboard.d.ts +28 -0
- package/dist/hooks/use-copy-to-clipboard.js +81 -0
- package/dist/hooks/use-copy-to-clipboard.js.map +1 -0
- package/dist/hooks/use-debounce.d.ts +43 -0
- package/dist/hooks/use-debounce.js +56 -0
- package/dist/hooks/use-debounce.js.map +1 -0
- package/dist/hooks/use-file-drop.d.ts +72 -0
- package/dist/hooks/use-file-drop.js +57 -0
- package/dist/hooks/use-file-drop.js.map +1 -0
- package/dist/i18n/defaults.d.ts +3 -0
- package/dist/i18n/defaults.js +7 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +15 -0
- package/dist/i18n/kit-labels.js +6 -2
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +43 -0
- package/dist/i18n/locales/de-CH-informal.js +8 -0
- package/dist/i18n/locales/de-CH-informal.js.map +1 -0
- package/dist/i18n/locales/de-CH.d.ts +3 -0
- package/dist/i18n/locales/de-CH.js +1 -14
- package/dist/i18n/locales/de-CH.js.map +1 -1
- package/dist/i18n/locales/de-informal.d.ts +64 -0
- package/dist/i18n/locales/de-informal.js +37 -0
- package/dist/i18n/locales/de-informal.js.map +1 -0
- package/dist/i18n/locales/de.d.ts +3 -0
- package/dist/i18n/locales/de.js +25 -0
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +3 -0
- package/dist/i18n/locales/es.js +25 -0
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +3 -0
- package/dist/i18n/locales/fr.js +25 -0
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +3 -0
- package/dist/i18n/locales/hu.js +25 -0
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +3 -0
- package/dist/i18n/locales/it.js +25 -0
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +3 -0
- package/dist/i18n/locales/zh.js +25 -0
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/i18n/swiss.d.ts +13 -0
- package/dist/i18n/swiss.js +19 -0
- package/dist/i18n/swiss.js.map +1 -0
- package/dist/index.d.ts +25 -10
- package/dist/index.js +22 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/dates.d.ts +37 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/search/command-palette.d.ts +15 -1
- package/dist/search/command-palette.js +10 -2
- package/dist/search/command-palette.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +35 -2
- package/dist/wizard/stepper-nav.js +54 -22
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +107 -5
- package/dist/wizard/types.js +2 -1
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +141 -21
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-summary.d.ts +10 -2
- package/dist/wizard/wizard-summary.js +3 -1
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +4 -1
- package/dist/wizard.js.map +1 -1
- package/package.json +1 -1
- package/src/components/alert-banner.tsx +211 -20
- package/src/components/button-group.tsx +75 -0
- package/src/components/chart-zoom.tsx +158 -20
- package/src/components/chip.tsx +160 -9
- package/src/components/confirm-dialog.tsx +242 -0
- package/src/components/copy-button.tsx +158 -0
- package/src/components/data-table-pagination.tsx +19 -6
- package/src/components/data-table-sort.ts +49 -10
- package/src/components/data-table.tsx +293 -24
- package/src/components/date-picker.tsx +262 -57
- package/src/components/description-list.tsx +174 -0
- package/src/components/disclosure.tsx +58 -5
- package/src/components/file-button.tsx +22 -2
- package/src/components/file-dropzone.tsx +198 -100
- package/src/components/floating-panel.tsx +341 -0
- package/src/components/full-bleed-dialog.tsx +48 -5
- package/src/components/modal.tsx +9 -2
- package/src/components/progress-bar.tsx +163 -0
- package/src/components/scroll-area.tsx +129 -0
- package/src/components/separator.tsx +37 -0
- package/src/components/series-chart-ticks.ts +135 -0
- package/src/components/series-chart.tsx +934 -67
- package/src/components/skeleton.tsx +65 -0
- package/src/components/stat-tile.tsx +160 -27
- package/src/components/table.tsx +263 -0
- package/src/components/toggle-legend.tsx +4 -3
- package/src/components/tree-view.tsx +589 -0
- package/src/components/treemap.tsx +10 -2
- package/src/components/ui.tsx +263 -39
- package/src/data-table.ts +2 -0
- package/src/hooks/use-copy-to-clipboard.ts +122 -0
- package/src/hooks/use-debounce.ts +105 -0
- package/src/hooks/use-file-drop.ts +123 -0
- package/src/i18n/defaults.ts +6 -0
- package/src/i18n/kit-labels.tsx +19 -0
- package/src/i18n/locales/de-CH-informal.ts +10 -0
- package/src/i18n/locales/de-CH.ts +1 -16
- package/src/i18n/locales/de-informal.ts +61 -0
- package/src/i18n/locales/de.ts +25 -0
- package/src/i18n/locales/es.ts +25 -0
- package/src/i18n/locales/fr.ts +25 -0
- package/src/i18n/locales/hu.ts +25 -0
- package/src/i18n/locales/it.ts +25 -0
- package/src/i18n/locales/zh.ts +25 -0
- package/src/i18n/swiss.ts +25 -0
- package/src/index.ts +29 -0
- package/src/lib/dates.ts +48 -0
- package/src/search/command-palette.tsx +29 -3
- package/src/wizard/stepper-nav.tsx +102 -28
- package/src/wizard/types.ts +106 -4
- package/src/wizard/use-wizard.ts +211 -31
- package/src/wizard/wizard-summary.tsx +24 -12
- package/src/wizard.ts +3 -2
package/src/wizard/use-wizard.ts
CHANGED
|
@@ -3,10 +3,16 @@
|
|
|
3
3
|
* per-step async validators, collected data, field errors, submit and the
|
|
4
4
|
* cancel-confirm gate.
|
|
5
5
|
*
|
|
6
|
-
* Domain-free,
|
|
7
|
-
* it must be mounted inside a react-router context — the same
|
|
8
|
-
* `DataTable` already carries.
|
|
9
|
-
*
|
|
6
|
+
* Domain-free, and router-free on request. By default it mirrors the active step
|
|
7
|
+
* to `?step=N`, so it must be mounted inside a react-router context — the same
|
|
8
|
+
* requirement `DataTable` already carries. `urlSync: false` keeps the step in
|
|
9
|
+
* memory and needs no router; `urlSync: { param }` renames the param. It only ever
|
|
10
|
+
* WRITES that param (see the note on `initialStep` below for why it deliberately
|
|
11
|
+
* does not read it back), and removes it when the wizard is left through the kit
|
|
12
|
+
* (see `UseWizardOptions.urlSync`).
|
|
13
|
+
*
|
|
14
|
+
* The committing step is the last one unless a step says `commits: true`; the
|
|
15
|
+
* steps after that one are post-commit steps (see `WizardStepConfig.commits`).
|
|
10
16
|
*/
|
|
11
17
|
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
12
18
|
import { useSearchParams } from "react-router";
|
|
@@ -27,9 +33,26 @@ interface WizardState<TData> {
|
|
|
27
33
|
skippedSteps: Set<number>;
|
|
28
34
|
data: TData;
|
|
29
35
|
isSubmitting: boolean;
|
|
36
|
+
isValidating: boolean;
|
|
30
37
|
error: string | null;
|
|
31
38
|
fieldErrors: FieldErrors;
|
|
32
39
|
showCancelDialog: boolean;
|
|
40
|
+
/** The step whose `onComplete` resolved, or null before any commit. */
|
|
41
|
+
committedStepIndex: number | null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Past the commit: the active step comes after the step that committed. Moves to
|
|
45
|
+
* that step or any before it are refused from here. */
|
|
46
|
+
function isAfterCommit(state: { currentStepIndex: number; committedStepIndex: number | null }) {
|
|
47
|
+
return state.committedStepIndex !== null && state.currentStepIndex > state.committedStepIndex;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Whether `index` is on the far side of a commit the wizard has already moved past. */
|
|
51
|
+
function crossesCommit(
|
|
52
|
+
state: { currentStepIndex: number; committedStepIndex: number | null },
|
|
53
|
+
index: number,
|
|
54
|
+
) {
|
|
55
|
+
return isAfterCommit(state) && index <= (state.committedStepIndex as number);
|
|
33
56
|
}
|
|
34
57
|
|
|
35
58
|
type WizardAction<TData> =
|
|
@@ -41,7 +64,9 @@ type WizardAction<TData> =
|
|
|
41
64
|
| { type: "GO_TO_STEP"; index: number }
|
|
42
65
|
| { type: "SKIP"; from: number }
|
|
43
66
|
| { type: "UPDATE_DATA"; partial: Partial<TData> }
|
|
67
|
+
| { type: "COMMITTED"; from: number }
|
|
44
68
|
| { type: "SET_SUBMITTING"; value: boolean }
|
|
69
|
+
| { type: "SET_VALIDATING"; value: boolean }
|
|
45
70
|
| { type: "SET_ERROR"; error: string | null }
|
|
46
71
|
| { type: "SET_FIELD_ERRORS"; errors: FieldErrors }
|
|
47
72
|
| { type: "SHOW_CANCEL_DIALOG"; show: boolean };
|
|
@@ -61,10 +86,12 @@ function createReducer<TData>(stepCount: number) {
|
|
|
61
86
|
}
|
|
62
87
|
case "GO_BACK": {
|
|
63
88
|
const prevIndex = Math.max(state.currentStepIndex - 1, 0);
|
|
89
|
+
if (crossesCommit(state, prevIndex)) return state;
|
|
64
90
|
return { ...state, currentStepIndex: prevIndex, fieldErrors: {} };
|
|
65
91
|
}
|
|
66
92
|
case "GO_TO_STEP": {
|
|
67
93
|
if (action.index < 0 || action.index >= stepCount) return state;
|
|
94
|
+
if (crossesCommit(state, action.index)) return state;
|
|
68
95
|
if (!state.completedSteps.has(action.index) && action.index > state.currentStepIndex) {
|
|
69
96
|
return state;
|
|
70
97
|
}
|
|
@@ -86,8 +113,27 @@ function createReducer<TData>(stepCount: number) {
|
|
|
86
113
|
}
|
|
87
114
|
return { ...state, data: { ...state.data, ...action.partial }, fieldErrors: nextErrors };
|
|
88
115
|
}
|
|
116
|
+
case "COMMITTED": {
|
|
117
|
+
// Recorded whatever step committed, so `wizard.committed` reads true after any
|
|
118
|
+
// successful Finish. Only a commit with steps after it MOVES: the default
|
|
119
|
+
// last-step commit stays where it is and looks exactly as it did before 0.8.
|
|
120
|
+
const committed = { ...state, committedStepIndex: action.from };
|
|
121
|
+
if (action.from !== state.currentStepIndex || action.from >= stepCount - 1) {
|
|
122
|
+
return committed;
|
|
123
|
+
}
|
|
124
|
+
const completed = new Set(state.completedSteps);
|
|
125
|
+
completed.add(action.from);
|
|
126
|
+
return {
|
|
127
|
+
...committed,
|
|
128
|
+
currentStepIndex: action.from + 1,
|
|
129
|
+
completedSteps: completed,
|
|
130
|
+
fieldErrors: {},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
89
133
|
case "SET_SUBMITTING":
|
|
90
134
|
return { ...state, isSubmitting: action.value };
|
|
135
|
+
case "SET_VALIDATING":
|
|
136
|
+
return { ...state, isValidating: action.value };
|
|
91
137
|
case "SET_ERROR":
|
|
92
138
|
return { ...state, error: action.error };
|
|
93
139
|
case "SET_FIELD_ERRORS":
|
|
@@ -103,8 +149,19 @@ function createReducer<TData>(stepCount: number) {
|
|
|
103
149
|
export function useWizard<TData extends Record<string, unknown>>(
|
|
104
150
|
options: UseWizardOptions<TData>,
|
|
105
151
|
): UseWizardReturn<TData> {
|
|
106
|
-
const {
|
|
107
|
-
|
|
152
|
+
const {
|
|
153
|
+
steps,
|
|
154
|
+
initialData,
|
|
155
|
+
onComplete,
|
|
156
|
+
onCancel,
|
|
157
|
+
onExit,
|
|
158
|
+
onDone,
|
|
159
|
+
cancellable = true,
|
|
160
|
+
confirmCancel: askBeforeCancel = true,
|
|
161
|
+
urlSync = true,
|
|
162
|
+
missingRequiredMessage,
|
|
163
|
+
onValidationFailed,
|
|
164
|
+
} = options;
|
|
108
165
|
const stepsRef = useRef<WizardStepConfig[]>(steps);
|
|
109
166
|
stepsRef.current = steps;
|
|
110
167
|
// A hook, not a component, but it produces two sentences of its own — the
|
|
@@ -138,7 +195,13 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
138
195
|
[],
|
|
139
196
|
);
|
|
140
197
|
|
|
141
|
-
|
|
198
|
+
// Chosen once, at mount: a hook may not be called conditionally, but it may be
|
|
199
|
+
// chosen once and then called unconditionally on every render — which is what
|
|
200
|
+
// makes `urlSync: false` work with no router above the wizard at all.
|
|
201
|
+
const [useStepUrl] = useState(() => (urlSync === false ? useNoStepUrl : useSearchParamStepUrl));
|
|
202
|
+
const [urlParam] = useState(() =>
|
|
203
|
+
typeof urlSync === "object" && urlSync.param ? urlSync.param : "step",
|
|
204
|
+
);
|
|
142
205
|
// The wizard's entered `data` and `completedSteps` are NOT persisted across a
|
|
143
206
|
// reload / deep-link (the reducer re-inits to empty defaults on every mount).
|
|
144
207
|
// Restoring a later step index from `?step=N` would therefore leave the wizard
|
|
@@ -154,9 +217,11 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
154
217
|
skippedSteps: new Set<number>(),
|
|
155
218
|
data: (initialData ?? {}) as TData,
|
|
156
219
|
isSubmitting: false,
|
|
220
|
+
isValidating: false,
|
|
157
221
|
error: null,
|
|
158
222
|
fieldErrors: {},
|
|
159
223
|
showCancelDialog: false,
|
|
224
|
+
committedStepIndex: null,
|
|
160
225
|
});
|
|
161
226
|
|
|
162
227
|
// A mounted step can DISABLE forward navigation (Next/Skip) while its own
|
|
@@ -168,26 +233,36 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
168
233
|
[],
|
|
169
234
|
);
|
|
170
235
|
|
|
171
|
-
// Sync step index to URL
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
return prev;
|
|
176
|
-
}, { replace: true });
|
|
177
|
-
}, [state.currentStepIndex, setUrlParams]);
|
|
236
|
+
// Sync step index to URL (a no-op under `urlSync: false`). `clearStepUrl` drops
|
|
237
|
+
// the param when the wizard is left through the kit, and stops the sync writing
|
|
238
|
+
// it back.
|
|
239
|
+
const clearStepUrl = useStepUrl(urlParam, state.currentStepIndex);
|
|
178
240
|
|
|
179
|
-
|
|
241
|
+
// `steps`, not `stepsRef.current`: during render the two are the same array, and
|
|
242
|
+
// reading a ref in render is what the hooks linter (rightly) objects to.
|
|
243
|
+
const currentStep = steps[state.currentStepIndex];
|
|
180
244
|
const isFirstStep = state.currentStepIndex === 0;
|
|
181
|
-
const isLastStep = state.currentStepIndex ===
|
|
182
|
-
//
|
|
183
|
-
//
|
|
245
|
+
const isLastStep = state.currentStepIndex === steps.length - 1;
|
|
246
|
+
// The step whose forward button commits: the first one flagged `commits`, else the
|
|
247
|
+
// last — which is every wizard written before the flag existed.
|
|
248
|
+
const flagged = steps.findIndex((s) => s.commits);
|
|
249
|
+
const commitStepIndex = flagged === -1 ? steps.length - 1 : flagged;
|
|
250
|
+
const isCommitStep = state.currentStepIndex === commitStepIndex;
|
|
251
|
+
const afterCommit = isAfterCommit(state);
|
|
252
|
+
// Finish is only valid once every step before the committing one has actually
|
|
253
|
+
// been walked through (completed or skipped). Guards against a restored/last-step
|
|
184
254
|
// index submitting a payload built from empty defaults.
|
|
185
|
-
const canFinish =
|
|
255
|
+
const canFinish = steps.every(
|
|
186
256
|
(_, index) =>
|
|
187
|
-
index
|
|
257
|
+
index >= commitStepIndex ||
|
|
188
258
|
state.completedSteps.has(index) ||
|
|
189
259
|
state.skippedSteps.has(index),
|
|
190
260
|
);
|
|
261
|
+
const canGoBack =
|
|
262
|
+
(!isFirstStep || onExit !== undefined) &&
|
|
263
|
+
!crossesCommit(state, state.currentStepIndex - 1);
|
|
264
|
+
const canCancel = cancellable && !afterCommit;
|
|
265
|
+
const canDone = isLastStep && afterCommit && onDone !== undefined;
|
|
191
266
|
|
|
192
267
|
// Runs the active step's config `validate` plus every validator its mounted
|
|
193
268
|
// components registered (RHF steps via useRhfWizardStep), AND-combining the
|
|
@@ -266,23 +341,50 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
266
341
|
if (advancingRef.current) return;
|
|
267
342
|
advancingRef.current = true;
|
|
268
343
|
const from = state.currentStepIndex;
|
|
344
|
+
// Pending state for the chrome (spinner, aria-busy). The ref above is still what
|
|
345
|
+
// refuses the second click: state would only be seen after a re-render.
|
|
346
|
+
dispatch({ type: "SET_VALIDATING", value: true });
|
|
269
347
|
try {
|
|
270
348
|
if (await runStepValidators()) {
|
|
271
349
|
dispatch({ type: "GO_NEXT", from });
|
|
272
350
|
}
|
|
273
351
|
} finally {
|
|
352
|
+
dispatch({ type: "SET_VALIDATING", value: false });
|
|
274
353
|
advancingRef.current = false;
|
|
275
354
|
}
|
|
276
355
|
}, [runStepValidators, state.currentStepIndex]);
|
|
277
356
|
|
|
357
|
+
// Read by `goBack`, which must not move while `onComplete` runs: the commit was
|
|
358
|
+
// decided on the data of the step the user is on, and Back mid-commit would show a
|
|
359
|
+
// step whose edits can no longer reach it. The chrome disables the button too; this
|
|
360
|
+
// covers an app calling `goBack` itself.
|
|
361
|
+
const submittingRef = useRef(false);
|
|
362
|
+
|
|
278
363
|
const goBack = useCallback(() => {
|
|
364
|
+
if (submittingRef.current) return;
|
|
365
|
+
if (state.currentStepIndex === 0 && onExit) {
|
|
366
|
+
clearStepUrl();
|
|
367
|
+
onExit();
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
// On step 0 with no `onExit` this stays the no-op move (clearing field errors) it
|
|
371
|
+
// always was.
|
|
279
372
|
dispatch({ type: "GO_BACK" });
|
|
280
|
-
}, []);
|
|
373
|
+
}, [state.currentStepIndex, onExit, clearStepUrl]);
|
|
281
374
|
|
|
282
375
|
const goToStep = useCallback((index: number) => {
|
|
283
376
|
dispatch({ type: "GO_TO_STEP", index });
|
|
284
377
|
}, []);
|
|
285
378
|
|
|
379
|
+
const canGoToStep = useCallback(
|
|
380
|
+
(index: number) =>
|
|
381
|
+
index >= 0 &&
|
|
382
|
+
index < steps.length &&
|
|
383
|
+
(index <= state.currentStepIndex || state.completedSteps.has(index)) &&
|
|
384
|
+
!crossesCommit(state, index),
|
|
385
|
+
[steps.length, state],
|
|
386
|
+
);
|
|
387
|
+
|
|
286
388
|
const skip = useCallback(() => {
|
|
287
389
|
// Not while Next is validating: the skip would land first and the pending GO_NEXT
|
|
288
390
|
// would then be stale anyway — but the user asked for one move, not a race.
|
|
@@ -290,14 +392,24 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
290
392
|
dispatch({ type: "SKIP", from: state.currentStepIndex });
|
|
291
393
|
}, [state.currentStepIndex]);
|
|
292
394
|
|
|
293
|
-
const cancel = useCallback(() => {
|
|
294
|
-
dispatch({ type: "SHOW_CANCEL_DIALOG", show: true });
|
|
295
|
-
}, []);
|
|
296
|
-
|
|
297
395
|
const confirmCancel = useCallback(() => {
|
|
298
396
|
dispatch({ type: "SHOW_CANCEL_DIALOG", show: false });
|
|
397
|
+
clearStepUrl();
|
|
299
398
|
onCancel?.();
|
|
300
|
-
}, [onCancel]);
|
|
399
|
+
}, [onCancel, clearStepUrl]);
|
|
400
|
+
|
|
401
|
+
const cancel = useCallback(() => {
|
|
402
|
+
if (!askBeforeCancel) {
|
|
403
|
+
confirmCancel();
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
dispatch({ type: "SHOW_CANCEL_DIALOG", show: true });
|
|
407
|
+
}, [askBeforeCancel, confirmCancel]);
|
|
408
|
+
|
|
409
|
+
const done = useCallback(() => {
|
|
410
|
+
clearStepUrl();
|
|
411
|
+
onDone?.();
|
|
412
|
+
}, [onDone, clearStepUrl]);
|
|
301
413
|
|
|
302
414
|
const dismissCancel = useCallback(() => {
|
|
303
415
|
dispatch({ type: "SHOW_CANCEL_DIALOG", show: false });
|
|
@@ -317,29 +429,55 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
317
429
|
|
|
318
430
|
const finish = useCallback(async () => {
|
|
319
431
|
if (!onComplete || !canFinish) return;
|
|
432
|
+
// A commit with steps after it happens once: those steps exist because the
|
|
433
|
+
// data has been written. (A last-step commit keeps its old behaviour — the app
|
|
434
|
+
// may retry Finish after `onComplete` resolved, as it always could.)
|
|
435
|
+
if (commitStepIndex < steps.length - 1 && state.committedStepIndex !== null) return;
|
|
320
436
|
// The same re-entry guard as `goNext`: a double-click on Finish would otherwise
|
|
321
437
|
// validate twice and call `onComplete` twice — `isSubmitting` is only set once
|
|
322
438
|
// validation has passed, too late to stop the second click.
|
|
323
439
|
if (advancingRef.current) return;
|
|
324
440
|
advancingRef.current = true;
|
|
441
|
+
const from = state.currentStepIndex;
|
|
325
442
|
try {
|
|
326
|
-
// Validate the (possibly form-bearing)
|
|
327
|
-
// the Next gate — review-only steps register no validators and pass
|
|
328
|
-
|
|
443
|
+
// Validate the (possibly form-bearing) committing step before submitting,
|
|
444
|
+
// matching the Next gate — review-only steps register no validators and pass
|
|
445
|
+
// through.
|
|
446
|
+
dispatch({ type: "SET_VALIDATING", value: true });
|
|
447
|
+
let valid: boolean;
|
|
448
|
+
try {
|
|
449
|
+
valid = await runStepValidators();
|
|
450
|
+
} finally {
|
|
451
|
+
dispatch({ type: "SET_VALIDATING", value: false });
|
|
452
|
+
}
|
|
453
|
+
if (!valid) return;
|
|
454
|
+
submittingRef.current = true;
|
|
329
455
|
dispatch({ type: "SET_SUBMITTING", value: true });
|
|
330
456
|
dispatch({ type: "SET_ERROR", error: null });
|
|
331
457
|
try {
|
|
332
458
|
await onComplete(state.data);
|
|
459
|
+
dispatch({ type: "COMMITTED", from });
|
|
333
460
|
} catch (err) {
|
|
334
461
|
const message = err instanceof Error && err.message ? err.message : genericError;
|
|
335
462
|
dispatch({ type: "SET_ERROR", error: message });
|
|
336
463
|
} finally {
|
|
464
|
+
submittingRef.current = false;
|
|
337
465
|
dispatch({ type: "SET_SUBMITTING", value: false });
|
|
338
466
|
}
|
|
339
467
|
} finally {
|
|
340
468
|
advancingRef.current = false;
|
|
341
469
|
}
|
|
342
|
-
}, [
|
|
470
|
+
}, [
|
|
471
|
+
onComplete,
|
|
472
|
+
canFinish,
|
|
473
|
+
commitStepIndex,
|
|
474
|
+
steps.length,
|
|
475
|
+
state.committedStepIndex,
|
|
476
|
+
state.currentStepIndex,
|
|
477
|
+
state.data,
|
|
478
|
+
runStepValidators,
|
|
479
|
+
genericError,
|
|
480
|
+
]);
|
|
343
481
|
|
|
344
482
|
const stepStatus = useCallback(
|
|
345
483
|
(index: number): StepStatus => {
|
|
@@ -354,16 +492,24 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
354
492
|
return {
|
|
355
493
|
currentStepIndex: state.currentStepIndex,
|
|
356
494
|
currentStep,
|
|
357
|
-
steps
|
|
495
|
+
steps,
|
|
358
496
|
isFirstStep,
|
|
359
497
|
isLastStep,
|
|
360
498
|
canFinish,
|
|
499
|
+
commitStepIndex,
|
|
500
|
+
isCommitStep,
|
|
501
|
+
committed: state.committedStepIndex !== null,
|
|
502
|
+
canGoBack,
|
|
503
|
+
canCancel,
|
|
504
|
+
canDone,
|
|
505
|
+
canGoToStep,
|
|
361
506
|
goNext,
|
|
362
507
|
goBack,
|
|
363
508
|
goToStep,
|
|
364
509
|
skip,
|
|
365
510
|
cancel,
|
|
366
511
|
finish,
|
|
512
|
+
done,
|
|
367
513
|
registerStepValidate,
|
|
368
514
|
nextBlocked,
|
|
369
515
|
setNextBlocked,
|
|
@@ -372,6 +518,7 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
372
518
|
data: state.data,
|
|
373
519
|
updateData,
|
|
374
520
|
isSubmitting: state.isSubmitting,
|
|
521
|
+
isValidating: state.isValidating,
|
|
375
522
|
error: state.error,
|
|
376
523
|
clearError,
|
|
377
524
|
fieldErrors: state.fieldErrors,
|
|
@@ -381,3 +528,36 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
381
528
|
dismissCancel,
|
|
382
529
|
};
|
|
383
530
|
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* `urlSync: true` / `{ param }`: mirror the active step to `?<param>=N`, and hand back
|
|
534
|
+
* the function that removes it again. Once removed it stays removed — the sync effect
|
|
535
|
+
* re-runs whenever react-router hands out a new setter (every search change), and
|
|
536
|
+
* without the latch it would write the param straight back after the exit cleared it.
|
|
537
|
+
*/
|
|
538
|
+
function useSearchParamStepUrl(param: string, index: number): () => void {
|
|
539
|
+
const [, setUrlParams] = useSearchParams();
|
|
540
|
+
const leftRef = useRef(false);
|
|
541
|
+
useEffect(() => {
|
|
542
|
+
if (leftRef.current) return;
|
|
543
|
+
setUrlParams((prev) => {
|
|
544
|
+
prev.set(param, String(index));
|
|
545
|
+
return prev;
|
|
546
|
+
}, { replace: true });
|
|
547
|
+
}, [param, index, setUrlParams]);
|
|
548
|
+
return useCallback(() => {
|
|
549
|
+
if (leftRef.current) return;
|
|
550
|
+
leftRef.current = true;
|
|
551
|
+
setUrlParams((prev) => {
|
|
552
|
+
prev.delete(param);
|
|
553
|
+
return prev;
|
|
554
|
+
}, { replace: true });
|
|
555
|
+
}, [param, setUrlParams]);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/** `urlSync: false`: the step lives in the reducer only. Touches no router context. */
|
|
559
|
+
function useNoStepUrl(_param: string, _index: number): () => void {
|
|
560
|
+
return noop;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function noop() {}
|
|
@@ -11,14 +11,23 @@ import { useKitLabels } from "../i18n/kit-labels";
|
|
|
11
11
|
* Sections are data, not children, because "jump back to step N" is the whole
|
|
12
12
|
* point of the component and a caller assembling its own cards invariably drops
|
|
13
13
|
* it. Labels and values are `ReactNode`, so a value can be a badge or a link.
|
|
14
|
+
*
|
|
15
|
+
* A section with no `stepIndex` (file-level counts, a total) is a card without an
|
|
16
|
+
* edit button. `disabled` greys every edit button at once — while the commit runs,
|
|
17
|
+
* when jumping back would show a step whose edits can no longer reach the request.
|
|
14
18
|
*/
|
|
15
19
|
export function WizardSummary({
|
|
16
20
|
sections,
|
|
17
21
|
onEditStep,
|
|
22
|
+
disabled = false,
|
|
18
23
|
labels,
|
|
19
24
|
}: {
|
|
20
25
|
sections: SummarySection[];
|
|
21
|
-
|
|
26
|
+
/** Called with the section's `stepIndex`. Optional only for a summary whose
|
|
27
|
+
* sections name no step at all. */
|
|
28
|
+
onEditStep?: (stepIndex: number) => void;
|
|
29
|
+
/** Disable every edit button (e.g. `wizard.isSubmitting`). */
|
|
30
|
+
disabled?: boolean;
|
|
22
31
|
labels?: Partial<WizardLabels>;
|
|
23
32
|
}) {
|
|
24
33
|
const l = useKitLabels("wizard", DEFAULT_WIZARD_LABELS, labels);
|
|
@@ -33,17 +42,20 @@ export function WizardSummary({
|
|
|
33
42
|
the title instead of sitting beside it. */}
|
|
34
43
|
<CardHeader className="pb-2">
|
|
35
44
|
<CardTitle className="text-base">{section.label}</CardTitle>
|
|
36
|
-
|
|
37
|
-
<
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
{section.stepIndex !== undefined && onEditStep && (
|
|
46
|
+
<CardAction>
|
|
47
|
+
<IconButton
|
|
48
|
+
size="sm"
|
|
49
|
+
variant="ghost"
|
|
50
|
+
aria-label={l.edit}
|
|
51
|
+
title={l.edit}
|
|
52
|
+
disabled={disabled}
|
|
53
|
+
onClick={() => onEditStep(section.stepIndex as number)}
|
|
54
|
+
>
|
|
55
|
+
<Pencil />
|
|
56
|
+
</IconButton>
|
|
57
|
+
</CardAction>
|
|
58
|
+
)}
|
|
47
59
|
</CardHeader>
|
|
48
60
|
<CardContent>
|
|
49
61
|
<dl className="grid gap-2 text-sm">
|
package/src/wizard.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
// `@eifi1/ui-kit/wizard` — the multi-step engine, its chrome and its review step.
|
|
2
2
|
//
|
|
3
|
-
// A re-slicing of the main barrel, not a new API.
|
|
4
|
-
//
|
|
3
|
+
// A re-slicing of the main barrel, not a new API. By default useWizard syncs the
|
|
4
|
+
// active step to `?step=` via react-router's useSearchParams, so it needs a router;
|
|
5
|
+
// `urlSync: false` keeps the step in memory and needs none.
|
|
5
6
|
//
|
|
6
7
|
// NOT the same thing as `WizardStepper` in components/wizard-stepper, which is a bare
|
|
7
8
|
// two-step indicator with no engine and stays on the main barrel.
|