@eifi1/ui-kit 0.12.0 → 0.13.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 +10 -10
- package/dist/components/amount-input.d.ts +17 -1
- package/dist/components/amount-input.js +46 -11
- package/dist/components/amount-input.js.map +1 -1
- package/dist/components/bulk-action-bar.d.ts +10 -2
- package/dist/components/bulk-action-bar.js.map +1 -1
- package/dist/components/button-group.d.ts +1 -1
- package/dist/components/calculator.d.ts +1 -1
- package/dist/components/chart.js +14 -8
- package/dist/components/chart.js.map +1 -1
- package/dist/components/chip.js +20 -15
- package/dist/components/chip.js.map +1 -1
- package/dist/components/combobox-core.d.ts +3 -1
- package/dist/components/combobox-core.js +16 -5
- package/dist/components/combobox-core.js.map +1 -1
- package/dist/components/combobox.js.map +1 -1
- package/dist/components/copy-button.d.ts +1 -1
- package/dist/components/data-table-filter-popover.js +8 -1
- package/dist/components/data-table-filter-popover.js.map +1 -1
- package/dist/components/data-table.js +28 -20
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/entity-combobox.js +22 -17
- package/dist/components/entity-combobox.js.map +1 -1
- package/dist/components/error-boundary.d.ts +202 -13
- package/dist/components/error-boundary.js +211 -20
- package/dist/components/error-boundary.js.map +1 -1
- package/dist/components/field-sync.js +90 -83
- package/dist/components/field-sync.js.map +1 -1
- package/dist/components/file-button.d.ts +1 -1
- package/dist/components/file-dropzone.d.ts +1 -1
- package/dist/components/form-actions.d.ts +1 -1
- package/dist/components/full-bleed-dialog.js +4 -0
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/hover-menu.js +64 -56
- package/dist/components/hover-menu.js.map +1 -1
- package/dist/components/list.d.ts +14 -2
- package/dist/components/list.js +3 -1
- package/dist/components/list.js.map +1 -1
- package/dist/components/modal.js.map +1 -1
- package/dist/components/multi-entity-combobox.js +23 -17
- package/dist/components/multi-entity-combobox.js.map +1 -1
- package/dist/components/number-field.d.ts +1 -1
- package/dist/components/number-input.d.ts +1 -1
- package/dist/components/number-input.js.map +1 -1
- package/dist/components/numpad-sheet.d.ts +6 -2
- package/dist/components/numpad-sheet.js +3 -2
- package/dist/components/numpad-sheet.js.map +1 -1
- package/dist/components/page-header.d.ts +35 -6
- package/dist/components/page-header.js +7 -1
- package/dist/components/page-header.js.map +1 -1
- package/dist/components/picker-sheet.js +3 -0
- package/dist/components/picker-sheet.js.map +1 -1
- package/dist/components/popover.js +3 -0
- package/dist/components/popover.js.map +1 -1
- package/dist/components/settings-fields.d.ts +1 -1
- package/dist/components/signed-amount.d.ts +2 -1
- package/dist/components/signed-amount.js +1 -1
- package/dist/components/signed-amount.js.map +1 -1
- package/dist/components/stat-tile.js +1 -1
- package/dist/components/stat-tile.js.map +1 -1
- package/dist/components/swipeable-row.js +4 -3
- package/dist/components/swipeable-row.js.map +1 -1
- package/dist/components/text-link.d.ts +73 -11
- package/dist/components/text-link.js +45 -10
- package/dist/components/text-link.js.map +1 -1
- package/dist/components/text.d.ts +26 -2
- package/dist/components/text.js +16 -2
- package/dist/components/text.js.map +1 -1
- package/dist/components/time-input.d.ts +1 -1
- package/dist/components/toggle-group.d.ts +22 -0
- package/dist/components/toggle-group.js +8 -8
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/toggle-legend.d.ts +17 -0
- package/dist/components/toggle-legend.js +6 -2
- package/dist/components/toggle-legend.js.map +1 -1
- package/dist/components/tooltip.d.ts +51 -1
- package/dist/components/tooltip.js +59 -35
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +1 -1
- package/dist/components/ui.js +166 -28
- package/dist/components/ui.js.map +1 -1
- package/dist/feedback/feedback-attachment.js +4 -6
- package/dist/feedback/feedback-attachment.js.map +1 -1
- package/dist/feedback/feedback-dialog.js +5 -3
- package/dist/feedback/feedback-dialog.js.map +1 -1
- package/dist/feedback/feedback-inbox.js +86 -75
- package/dist/feedback/feedback-inbox.js.map +1 -1
- package/dist/feedback/feedback-thread.d.ts +43 -3
- package/dist/feedback/feedback-thread.js +53 -26
- package/dist/feedback/feedback-thread.js.map +1 -1
- package/dist/feedback.d.ts +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -1
- package/dist/i18n/defaults.d.ts +1 -1
- package/dist/i18n/kit-labels.d.ts +1 -1
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +1 -1
- package/dist/i18n/locales/de-CH.d.ts +1 -1
- package/dist/i18n/locales/de-informal.d.ts +1 -1
- package/dist/i18n/locales/de.d.ts +1 -1
- package/dist/i18n/locales/de.js +9 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +1 -1
- package/dist/i18n/locales/es.js +9 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +1 -1
- package/dist/i18n/locales/fr.js +9 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +1 -1
- package/dist/i18n/locales/hu.js +9 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +1 -1
- package/dist/i18n/locales/it.js +9 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +1 -1
- package/dist/i18n/locales/zh.js +9 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +6 -6
- package/dist/{kit-labels-v3biUF1L.d.ts → kit-labels-Mz8zZHIF.d.ts} +102 -3
- package/dist/lib/format.d.ts +23 -5
- package/dist/lib/format.js +3 -1
- package/dist/lib/format.js.map +1 -1
- package/dist/rhf/fields.d.ts +57 -15
- package/dist/rhf/fields.js +41 -11
- package/dist/rhf/fields.js.map +1 -1
- package/dist/rhf/form.d.ts +1 -1
- package/dist/rhf.d.ts +2 -2
- package/dist/rhf.js.map +1 -1
- package/dist/search/command-palette.js +20 -14
- package/dist/search/command-palette.js.map +1 -1
- package/dist/shell/app-shell.d.ts +1 -1
- package/dist/shell/app-shell.js +62 -54
- package/dist/shell/app-shell.js.map +1 -1
- package/dist/shell/top-bar-brand.d.ts +1 -1
- package/dist/shell/topbar-action-menu.d.ts +9 -3
- package/dist/shell/topbar-action-menu.js +2 -1
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +1 -1
- package/dist/tour/tour.js +10 -4
- package/dist/tour/tour.js.map +1 -1
- package/dist/wizard/stepper-nav.d.ts +1 -1
- package/dist/wizard/use-wizard.js +4 -2
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-step.d.ts +11 -1
- package/dist/wizard/wizard-step.js +24 -6
- package/dist/wizard/wizard-step.js.map +1 -1
- package/dist/wizard.d.ts +1 -1
- package/package.json +6 -6
- package/src/components/amount-input.tsx +97 -10
- package/src/components/bulk-action-bar.tsx +16 -3
- package/src/components/chart.tsx +19 -8
- package/src/components/chip.tsx +6 -0
- package/src/components/combobox-core.tsx +28 -9
- package/src/components/combobox.tsx +2 -0
- package/src/components/data-table-filter-popover.tsx +13 -5
- package/src/components/data-table.tsx +6 -0
- package/src/components/entity-combobox.tsx +7 -4
- package/src/components/error-boundary.tsx +453 -29
- package/src/components/field-sync.tsx +48 -40
- package/src/components/full-bleed-dialog.tsx +4 -0
- package/src/components/hover-menu.tsx +26 -17
- package/src/components/list.tsx +17 -3
- package/src/components/modal.tsx +4 -0
- package/src/components/multi-entity-combobox.tsx +8 -4
- package/src/components/number-input.tsx +1 -0
- package/src/components/numpad-sheet.tsx +6 -1
- package/src/components/page-header.tsx +42 -5
- package/src/components/picker-sheet.tsx +3 -0
- package/src/components/popover.tsx +3 -0
- package/src/components/signed-amount.tsx +12 -3
- package/src/components/stat-tile.tsx +3 -1
- package/src/components/swipeable-row.tsx +7 -7
- package/src/components/text-link.tsx +126 -18
- package/src/components/text.tsx +46 -2
- package/src/components/toggle-group.tsx +35 -10
- package/src/components/toggle-legend.tsx +22 -1
- package/src/components/tooltip.tsx +79 -8
- package/src/components/ui.tsx +307 -39
- package/src/feedback/feedback-attachment.tsx +10 -7
- package/src/feedback/feedback-dialog.tsx +7 -4
- package/src/feedback/feedback-inbox.tsx +7 -0
- package/src/feedback/feedback-thread.tsx +109 -33
- package/src/i18n/kit-labels.tsx +16 -0
- package/src/i18n/locales/de.ts +8 -0
- package/src/i18n/locales/es.ts +8 -0
- package/src/i18n/locales/fr.ts +8 -0
- package/src/i18n/locales/hu.ts +8 -0
- package/src/i18n/locales/it.ts +8 -0
- package/src/i18n/locales/zh.ts +8 -0
- package/src/lib/format.ts +29 -6
- package/src/rhf/fields.tsx +97 -16
- package/src/rhf.ts +1 -1
- package/src/search/command-palette.tsx +29 -19
- package/src/shell/app-shell.tsx +6 -1
- package/src/shell/topbar-action-menu.tsx +11 -4
- package/src/tour/tour.tsx +16 -5
- package/src/wizard/use-wizard.ts +8 -4
- package/src/wizard/wizard-step.tsx +38 -6
package/src/rhf/fields.tsx
CHANGED
|
@@ -34,7 +34,9 @@
|
|
|
34
34
|
* omitted, none — then give the field an `aria-label`), `hint` (standing advice under
|
|
35
35
|
* the field, attached to it with `aria-describedby`), `required` (the label's mark,
|
|
36
36
|
* and `aria-required` where the control takes it — validation is `rules`' or the
|
|
37
|
-
* resolver's), `rules`, `disabled
|
|
37
|
+
* resolver's), `rules`, `disabled` (the control only — the value is still submitted;
|
|
38
|
+
* `excludeWhenDisabled` for react-hook-form's drop-the-value semantics), and
|
|
39
|
+
* `className` for the item's box.
|
|
38
40
|
*
|
|
39
41
|
* {@link RhfField} is the shell they are all built on, for a control the kit does not
|
|
40
42
|
* ship (an address autocomplete): it takes a `render` and wires the rest.
|
|
@@ -80,9 +82,17 @@ export interface RhfFieldBaseProps<
|
|
|
80
82
|
/** Draws the label's required mark and sets `aria-required` where the control
|
|
81
83
|
* takes it. Whether the field IS required is `rules`' or the resolver's to say. */
|
|
82
84
|
required?: boolean;
|
|
83
|
-
/** Disables the
|
|
84
|
-
*
|
|
85
|
+
/** Disables the control and KEEPS the value: a field locked to a preset (a lease's
|
|
86
|
+
* preselected unit, a fixed period) still submits it, and `rules` still run.
|
|
87
|
+
* react-hook-form's own Controller `disabled` drops the value from `handleSubmit`'s
|
|
88
|
+
* data instead, which a locked preset must never do (kastlan, 0.12). To get that,
|
|
89
|
+
* add `excludeWhenDisabled`. A form-wide `useForm({ disabled })` still disables
|
|
90
|
+
* every field the RHF way. */
|
|
85
91
|
disabled?: boolean;
|
|
92
|
+
/** With `disabled`: disable through react-hook-form, so the value is left out of
|
|
93
|
+
* the submitted data and validation skips it, like a disabled native input in a
|
|
94
|
+
* plain form post. Default `false`. */
|
|
95
|
+
excludeWhenDisabled?: boolean;
|
|
86
96
|
/** Classes for the item's box (a `grid gap-1`). */
|
|
87
97
|
className?: string;
|
|
88
98
|
}
|
|
@@ -187,6 +197,7 @@ export function RhfField<
|
|
|
187
197
|
hint,
|
|
188
198
|
required,
|
|
189
199
|
disabled,
|
|
200
|
+
excludeWhenDisabled,
|
|
190
201
|
className,
|
|
191
202
|
render,
|
|
192
203
|
asControl = true,
|
|
@@ -198,11 +209,16 @@ export function RhfField<
|
|
|
198
209
|
control={control}
|
|
199
210
|
name={name}
|
|
200
211
|
rules={rules}
|
|
201
|
-
disabled={disabled}
|
|
212
|
+
disabled={excludeWhenDisabled ? disabled : undefined}
|
|
202
213
|
render={({ field, fieldState }) => (
|
|
203
214
|
<FormItem className={className}>
|
|
204
215
|
{showLabel && hasContent(label) && <RhfLabel required={required}>{label}</RhfLabel>}
|
|
205
|
-
<ControlSlot
|
|
216
|
+
<ControlSlot
|
|
217
|
+
field={disabled && !field.disabled ? { ...field, disabled: true } : field}
|
|
218
|
+
fieldState={fieldState}
|
|
219
|
+
render={render}
|
|
220
|
+
asControl={asControl}
|
|
221
|
+
/>
|
|
206
222
|
{hasContent(hint) && <FormDescription>{hint}</FormDescription>}
|
|
207
223
|
{message && <FormMessage />}
|
|
208
224
|
</FormItem>
|
|
@@ -230,8 +246,17 @@ function useFocusHandle(ref: ControllerRenderProps["ref"], find: () => HTMLEleme
|
|
|
230
246
|
|
|
231
247
|
type OwnInputProps = Omit<
|
|
232
248
|
InputProps,
|
|
233
|
-
"name" | "value" | "defaultValue" | "onChange" | "onBlur" | "ref" | "label" | "error" | "invalid" | "className" | "disabled" | "required"
|
|
234
|
-
|
|
249
|
+
"name" | "value" | "defaultValue" | "onChange" | "onBlur" | "ref" | "label" | "error" | "invalid" | "className" | "disabled" | "required" | "inputClassName"
|
|
250
|
+
> & {
|
|
251
|
+
/**
|
|
252
|
+
* Classes for the `<input>` — `className` is the item's box (kastlan 42: a
|
|
253
|
+
* `font-mono` IBAN, a `tabular-nums` reference number). {@link RhfTextarea} and
|
|
254
|
+
* {@link RhfNumberField} had it; this one only reached `Input`'s prop of the same
|
|
255
|
+
* name by accident of the rest-spread, undocumented and one refactor from being
|
|
256
|
+
* dropped. Declared, it is part of the contract the three share.
|
|
257
|
+
*/
|
|
258
|
+
inputClassName?: string;
|
|
259
|
+
};
|
|
235
260
|
|
|
236
261
|
export type RhfTextFieldProps<
|
|
237
262
|
TFieldValues extends FieldValues = FieldValues,
|
|
@@ -252,12 +277,13 @@ export function RhfTextField<
|
|
|
252
277
|
hint,
|
|
253
278
|
required,
|
|
254
279
|
disabled,
|
|
280
|
+
excludeWhenDisabled,
|
|
255
281
|
className,
|
|
256
282
|
...inputProps
|
|
257
283
|
}: RhfTextFieldProps<TFieldValues, TName, TTransformed>) {
|
|
258
284
|
return (
|
|
259
285
|
<RhfField
|
|
260
|
-
{...{ name, control, rules, label, hint, required, disabled, className }}
|
|
286
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
261
287
|
render={({ field, invalid }) => (
|
|
262
288
|
<Input
|
|
263
289
|
aria-required={required || undefined}
|
|
@@ -302,13 +328,14 @@ export function RhfTextarea<
|
|
|
302
328
|
hint,
|
|
303
329
|
required,
|
|
304
330
|
disabled,
|
|
331
|
+
excludeWhenDisabled,
|
|
305
332
|
className,
|
|
306
333
|
inputClassName,
|
|
307
334
|
...areaProps
|
|
308
335
|
}: RhfTextareaProps<TFieldValues, TName, TTransformed>) {
|
|
309
336
|
return (
|
|
310
337
|
<RhfField
|
|
311
|
-
{...{ name, control, rules, label, hint, required, disabled, className }}
|
|
338
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
312
339
|
render={({ field, invalid }) => (
|
|
313
340
|
<Textarea
|
|
314
341
|
aria-required={required || undefined}
|
|
@@ -382,6 +409,7 @@ export function RhfNumberField<
|
|
|
382
409
|
hint,
|
|
383
410
|
required,
|
|
384
411
|
disabled,
|
|
412
|
+
excludeWhenDisabled,
|
|
385
413
|
className,
|
|
386
414
|
emptyValue = null,
|
|
387
415
|
nullable = true,
|
|
@@ -389,7 +417,7 @@ export function RhfNumberField<
|
|
|
389
417
|
}: RhfNumberFieldProps<TFieldValues, TName, TTransformed>) {
|
|
390
418
|
return (
|
|
391
419
|
<RhfField
|
|
392
|
-
{...{ name, control, rules, label, hint, required, disabled, className }}
|
|
420
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
393
421
|
render={({ field, invalid, id }) => (
|
|
394
422
|
// FormControl clones this element with `id` / `aria-describedby` /
|
|
395
423
|
// `aria-invalid`, which NumberControl hands on to the field.
|
|
@@ -449,6 +477,46 @@ function NumberControl({
|
|
|
449
477
|
);
|
|
450
478
|
}
|
|
451
479
|
|
|
480
|
+
export type RhfIntegerFieldProps<
|
|
481
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
482
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
483
|
+
TTransformed = TFieldValues,
|
|
484
|
+
> = RhfNumberFieldProps<TFieldValues, TName, TTransformed>;
|
|
485
|
+
|
|
486
|
+
/**
|
|
487
|
+
* {@link RhfNumberField} for a whole number — a floor, a room count, a year, a
|
|
488
|
+
* notice period in months. Every such call site in kastlan (item 41) spelled out the
|
|
489
|
+
* same three props, `digits={0} calculator={false} emptyValue=""`, and a preset
|
|
490
|
+
* written thirty times is one that is eventually written wrong once:
|
|
491
|
+
*
|
|
492
|
+
* - `digits={0}`: rounds on commit, so "2.5" rooms settles to 3 instead of reaching
|
|
493
|
+
* an `Integer` column and failing server-side;
|
|
494
|
+
* - `calculator={false}`: a count is typed, not worked out — the trigger is noise
|
|
495
|
+
* beside a two-digit field;
|
|
496
|
+
* - `emptyValue=""`: kastlan's integer schemas are `z.coerce.number()` over `""`,
|
|
497
|
+
* written against the native number inputs these fields replaced.
|
|
498
|
+
*
|
|
499
|
+
* Each is only a default: pass the prop to override it (`emptyValue={null}` for a
|
|
500
|
+
* nullable column). Everything else is {@link RhfNumberField}'s.
|
|
501
|
+
*/
|
|
502
|
+
export function RhfIntegerField<
|
|
503
|
+
TFieldValues extends FieldValues = FieldValues,
|
|
504
|
+
TName extends FieldPath<TFieldValues> = FieldPath<TFieldValues>,
|
|
505
|
+
TTransformed = TFieldValues,
|
|
506
|
+
>(props: RhfIntegerFieldProps<TFieldValues, TName, TTransformed>) {
|
|
507
|
+
// `??` / `=== undefined` rather than a spread over the defaults, so a caller
|
|
508
|
+
// forwarding its own optional prop (`digits={props.digits}`) still gets the preset,
|
|
509
|
+
// while an explicit `emptyValue={null}` keeps its null.
|
|
510
|
+
return (
|
|
511
|
+
<RhfNumberField
|
|
512
|
+
{...props}
|
|
513
|
+
digits={props.digits ?? 0}
|
|
514
|
+
calculator={props.calculator ?? false}
|
|
515
|
+
emptyValue={props.emptyValue === undefined ? "" : props.emptyValue}
|
|
516
|
+
/>
|
|
517
|
+
);
|
|
518
|
+
}
|
|
519
|
+
|
|
452
520
|
// ── money ────────────────────────────────────────────────────────────────────
|
|
453
521
|
|
|
454
522
|
export type RhfMoneyFieldProps<
|
|
@@ -476,6 +544,13 @@ export type RhfMoneyFieldProps<
|
|
|
476
544
|
valueAs?: "number" | "string";
|
|
477
545
|
/** What an emptied field stores when `valueAs` is `"number"`. Default `null`. */
|
|
478
546
|
emptyValue?: null | "";
|
|
547
|
+
/** Decimals the amount settles to on blur, Enter or a calculator result. Default:
|
|
548
|
+
* the currency's minor unit (CHF 2, JPY 0). See {@link AmountInput}'s `digits`. */
|
|
549
|
+
digits?: number;
|
|
550
|
+
/** Clamp the settled amount (a release capped at the deposit's balance). Validate
|
|
551
|
+
* with `rules` as well if a clamp needs explaining. */
|
|
552
|
+
min?: number;
|
|
553
|
+
max?: number;
|
|
479
554
|
};
|
|
480
555
|
|
|
481
556
|
/** The amount a text reads as, or `undefined` for a draft that is not one yet. */
|
|
@@ -573,12 +648,13 @@ export function RhfMoneyField<
|
|
|
573
648
|
hint,
|
|
574
649
|
required,
|
|
575
650
|
disabled,
|
|
651
|
+
excludeWhenDisabled,
|
|
576
652
|
className,
|
|
577
653
|
...amountProps
|
|
578
654
|
}: RhfMoneyFieldProps<TFieldValues, TName, TTransformed>) {
|
|
579
655
|
return (
|
|
580
656
|
<RhfField
|
|
581
|
-
{...{ name, control, rules, label, hint, required, disabled, className }}
|
|
657
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
582
658
|
asControl={false}
|
|
583
659
|
render={({ field, invalid, id, describedBy }) => (
|
|
584
660
|
<MoneyControl
|
|
@@ -662,6 +738,7 @@ export function RhfDateField<
|
|
|
662
738
|
hint,
|
|
663
739
|
required,
|
|
664
740
|
disabled,
|
|
741
|
+
excludeWhenDisabled,
|
|
665
742
|
className,
|
|
666
743
|
inputClassName,
|
|
667
744
|
emptyValue = "",
|
|
@@ -669,7 +746,7 @@ export function RhfDateField<
|
|
|
669
746
|
}: RhfDateFieldProps<TFieldValues, TName, TTransformed>) {
|
|
670
747
|
return (
|
|
671
748
|
<RhfField
|
|
672
|
-
{...{ name, control, rules, label, hint, required, disabled, className }}
|
|
749
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
673
750
|
render={({ field, invalid, id }) => (
|
|
674
751
|
// FormControl clones this element with `aria-describedby` / `aria-invalid`,
|
|
675
752
|
// which DateControl hands on to the picker's trigger.
|
|
@@ -726,6 +803,7 @@ export function RhfSelect<
|
|
|
726
803
|
hint,
|
|
727
804
|
required,
|
|
728
805
|
disabled,
|
|
806
|
+
excludeWhenDisabled,
|
|
729
807
|
className,
|
|
730
808
|
options,
|
|
731
809
|
children,
|
|
@@ -736,7 +814,7 @@ export function RhfSelect<
|
|
|
736
814
|
}: RhfSelectProps<TFieldValues, TName, TTransformed>) {
|
|
737
815
|
return (
|
|
738
816
|
<RhfField
|
|
739
|
-
{...{ name, control, rules, label, hint, required, disabled, className }}
|
|
817
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
740
818
|
render={({ field, invalid }) => (
|
|
741
819
|
<Select
|
|
742
820
|
aria-required={required || undefined}
|
|
@@ -793,12 +871,13 @@ export function RhfCheckbox<
|
|
|
793
871
|
hint,
|
|
794
872
|
required,
|
|
795
873
|
disabled,
|
|
874
|
+
excludeWhenDisabled,
|
|
796
875
|
className,
|
|
797
876
|
...boxProps
|
|
798
877
|
}: RhfCheckboxProps<TFieldValues, TName, TTransformed>) {
|
|
799
878
|
return (
|
|
800
879
|
<RhfField
|
|
801
|
-
{...{ name, control, rules, required, disabled, className }}
|
|
880
|
+
{...{ name, control, rules, required, disabled, excludeWhenDisabled, className }}
|
|
802
881
|
showLabel={false}
|
|
803
882
|
render={({ field, invalid }) => (
|
|
804
883
|
<Checkbox
|
|
@@ -923,6 +1002,7 @@ export function RhfCombobox<
|
|
|
923
1002
|
hint,
|
|
924
1003
|
required,
|
|
925
1004
|
disabled,
|
|
1005
|
+
excludeWhenDisabled,
|
|
926
1006
|
className,
|
|
927
1007
|
clearValue = null,
|
|
928
1008
|
comboClassName,
|
|
@@ -930,7 +1010,7 @@ export function RhfCombobox<
|
|
|
930
1010
|
}: RhfComboboxProps<TFieldValues, TName, V, TTransformed>) {
|
|
931
1011
|
return (
|
|
932
1012
|
<RhfField
|
|
933
|
-
{...{ name, control, rules, label, hint, required, disabled, className }}
|
|
1013
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
934
1014
|
asControl={false}
|
|
935
1015
|
message={false}
|
|
936
1016
|
render={({ field, invalid, error, id, describedBy }) => (
|
|
@@ -987,13 +1067,14 @@ export function RhfTextCombobox<
|
|
|
987
1067
|
hint,
|
|
988
1068
|
required,
|
|
989
1069
|
disabled,
|
|
1070
|
+
excludeWhenDisabled,
|
|
990
1071
|
className,
|
|
991
1072
|
comboClassName,
|
|
992
1073
|
...comboProps
|
|
993
1074
|
}: RhfTextComboboxProps<TFieldValues, TName, TTransformed>) {
|
|
994
1075
|
return (
|
|
995
1076
|
<RhfField
|
|
996
|
-
{...{ name, control, rules, label, hint, required, disabled, className }}
|
|
1077
|
+
{...{ name, control, rules, label, hint, required, disabled, excludeWhenDisabled, className }}
|
|
997
1078
|
asControl={false}
|
|
998
1079
|
message={false}
|
|
999
1080
|
render={({ field, invalid, error, id, describedBy }) => (
|
package/src/rhf.ts
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
export * from "./rhf/form";
|
|
10
10
|
// The react-hook-form bridge to the wizard's Next gate (useWizard lives in /wizard).
|
|
11
11
|
export * from "./rhf/use-rhf-wizard-step";
|
|
12
|
-
// Bound fields — one line per field (RhfTextField, RhfNumberField, RhfMoneyField,
|
|
12
|
+
// Bound fields — one line per field (RhfTextField, RhfNumberField, RhfIntegerField, RhfMoneyField,
|
|
13
13
|
// RhfDateField, RhfTextarea, RhfSelect, RhfCheckbox, RhfCombobox, RhfTextCombobox)
|
|
14
14
|
// and RhfField, the shell they are built on.
|
|
15
15
|
export * from "./rhf/fields";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect, useId, useMemo, useRef, useState } from "react";
|
|
1
|
+
import { useEffect, useEffectEvent, useId, useMemo, useRef, useState } from "react";
|
|
2
2
|
import type { ReactNode } from "react";
|
|
3
3
|
import { createPortal } from "react-dom";
|
|
4
4
|
import { CornerDownLeft, Search, X } from "lucide-react";
|
|
@@ -54,9 +54,12 @@ function groupItems(items: CommandItem[]) {
|
|
|
54
54
|
}
|
|
55
55
|
byGroup.get(item.group)!.push(item);
|
|
56
56
|
}
|
|
57
|
+
const flat = order.flatMap((g) => byGroup.get(g)!).filter((item) => item.kind !== "status");
|
|
57
58
|
return {
|
|
58
59
|
groups: order.map((g) => ({ group: g, items: byGroup.get(g)! })),
|
|
59
|
-
flat
|
|
60
|
+
flat,
|
|
61
|
+
/** Each selectable item's position in `flat` — its `data-index` and keyboard slot. */
|
|
62
|
+
flatIndex: new Map(flat.map((item, index) => [item, index])),
|
|
60
63
|
};
|
|
61
64
|
}
|
|
62
65
|
|
|
@@ -279,8 +282,9 @@ export function CommandPalette({
|
|
|
279
282
|
const dialogRef = useRef<HTMLDivElement>(null);
|
|
280
283
|
const inputRef = useRef<HTMLInputElement>(null);
|
|
281
284
|
const listRef = useRef<HTMLUListElement>(null);
|
|
282
|
-
|
|
283
|
-
|
|
285
|
+
// The latest `search`, callable from the debounced effect without being one of its
|
|
286
|
+
// dependencies — `revision` is what re-runs it (see the prop's docstring).
|
|
287
|
+
const runSearch = useEffectEvent((q: string) => search(q));
|
|
284
288
|
const reqId = useRef(0);
|
|
285
289
|
/** What the provider's last answer was: a promise (`false`), an array (`true`), or
|
|
286
290
|
* not known yet. Only a provider known to be async shows "Searching…" while the
|
|
@@ -305,17 +309,24 @@ export function CommandPalette({
|
|
|
305
309
|
useBodyScrollLock(open);
|
|
306
310
|
|
|
307
311
|
// Reset when opened — the palette's OWN text only; a controlled query belongs to
|
|
308
|
-
// its owner (see the prop).
|
|
312
|
+
// its owner (see the prop). Adjusted during render on the closed-to-open transition
|
|
313
|
+
// (starting from "closed", so a palette mounted open resets too). `controlled` is
|
|
314
|
+
// read at the open, not watched: a caller switching modes while the palette is open
|
|
315
|
+
// is not a reason to wipe what is typed.
|
|
316
|
+
const [wasOpen, setWasOpen] = useState(false);
|
|
317
|
+
if (open !== wasOpen) {
|
|
318
|
+
setWasOpen(open);
|
|
319
|
+
if (open) {
|
|
320
|
+
if (!controlled) setOwnQuery("");
|
|
321
|
+
// An uncommitted draft does not outlive the open it was typed in.
|
|
322
|
+
setDraft(controlled ? query : "");
|
|
323
|
+
setActive(0);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
// The ref half of that reset: results from a previous open do not carry a
|
|
327
|
+
// highlight into this one.
|
|
309
328
|
useEffect(() => {
|
|
310
|
-
if (
|
|
311
|
-
if (!controlled) setOwnQuery("");
|
|
312
|
-
// An uncommitted draft does not outlive the open it was typed in.
|
|
313
|
-
setDraft(controlled ? query : "");
|
|
314
|
-
setActive(0);
|
|
315
|
-
shownQuery.current = null;
|
|
316
|
-
// `controlled` is read at the open, not watched: a caller switching modes while
|
|
317
|
-
// the palette is open is not a reason to wipe what is typed.
|
|
318
|
-
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
329
|
+
if (open) shownQuery.current = null;
|
|
319
330
|
}, [open]);
|
|
320
331
|
|
|
321
332
|
// Debounced, race-safe search.
|
|
@@ -325,7 +336,7 @@ export function CommandPalette({
|
|
|
325
336
|
if (syncRef.current === false) setLoading(true);
|
|
326
337
|
const run = async () => {
|
|
327
338
|
try {
|
|
328
|
-
const answer =
|
|
339
|
+
const answer = runSearch(query);
|
|
329
340
|
syncRef.current = !isThenable(answer);
|
|
330
341
|
if (!syncRef.current) setLoading(true);
|
|
331
342
|
const r = await answer;
|
|
@@ -359,7 +370,7 @@ export function CommandPalette({
|
|
|
359
370
|
}, [query, open, revision]);
|
|
360
371
|
|
|
361
372
|
// Group results, preserving first-seen group order; keep a flat list for nav.
|
|
362
|
-
const { groups, flat } = useMemo(() => groupItems(results), [results]);
|
|
373
|
+
const { groups, flat, flatIndex } = useMemo(() => groupItems(results), [results]);
|
|
363
374
|
useEffect(() => {
|
|
364
375
|
activeIdRef.current = flat[active]?.id;
|
|
365
376
|
});
|
|
@@ -404,8 +415,8 @@ export function CommandPalette({
|
|
|
404
415
|
if (!open) return null;
|
|
405
416
|
|
|
406
417
|
const comfortable = density === "comfortable";
|
|
407
|
-
let flatIndex = -1;
|
|
408
418
|
return createPortal(
|
|
419
|
+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions -- pointer-only backdrop dismiss; Escape is the keyboard path
|
|
409
420
|
<div
|
|
410
421
|
className={cn(
|
|
411
422
|
"fixed inset-0 z-[70] flex items-start justify-center bg-black/40",
|
|
@@ -546,8 +557,7 @@ export function CommandPalette({
|
|
|
546
557
|
</li>
|
|
547
558
|
);
|
|
548
559
|
}
|
|
549
|
-
flatIndex
|
|
550
|
-
const idx = flatIndex;
|
|
560
|
+
const idx = flatIndex.get(item) ?? -1;
|
|
551
561
|
const isActive = idx === active;
|
|
552
562
|
const redact = item.redact ?? redactLabels;
|
|
553
563
|
// Written once and worn by either tag below, so the anchor and the
|
package/src/shell/app-shell.tsx
CHANGED
|
@@ -765,7 +765,6 @@ function SidebarGroup({
|
|
|
765
765
|
// group UNFOLDS into view on load rather than appearing already open.
|
|
766
766
|
const here = subActive || onOwnPage;
|
|
767
767
|
useEffect(() => {
|
|
768
|
-
// eslint-disable-next-line react-hooks/set-state-in-effect -- follows the route, see above
|
|
769
768
|
if (here) setOpenGroup(item.to);
|
|
770
769
|
}, [here, item.to, setOpenGroup]);
|
|
771
770
|
const expanded = inline && open;
|
|
@@ -931,6 +930,9 @@ function SidebarFlyout({ item, children }: { item: AppShellNavItem; children: Re
|
|
|
931
930
|
);
|
|
932
931
|
|
|
933
932
|
return (
|
|
933
|
+
// Not a control: hover and focus-within on the nav link inside. Focus (the
|
|
934
|
+
// keyboard path) opens it exactly as hover does, Escape closes it.
|
|
935
|
+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions -- hover/focus-within wrapper around a real link
|
|
934
936
|
<div
|
|
935
937
|
ref={wrapperRef}
|
|
936
938
|
className="relative"
|
|
@@ -943,6 +945,9 @@ function SidebarFlyout({ item, children }: { item: AppShellNavItem; children: Re
|
|
|
943
945
|
{open &&
|
|
944
946
|
pos &&
|
|
945
947
|
createPortal(
|
|
948
|
+
// Keeps the flyout open while the pointer crosses into it; keyboard users
|
|
949
|
+
// reach it by focus, which the wrapper above already handles.
|
|
950
|
+
// eslint-disable-next-line jsx-a11y/no-static-element-interactions -- pointer-only hover bridge
|
|
946
951
|
<div
|
|
947
952
|
dir={dir}
|
|
948
953
|
style={{ position: "fixed", ...pos }}
|
|
@@ -58,9 +58,15 @@ export interface TopBarMenuHeader {
|
|
|
58
58
|
title: ReactNode;
|
|
59
59
|
/** The muted second line — an email, a role. */
|
|
60
60
|
subtitle?: ReactNode;
|
|
61
|
-
/**
|
|
62
|
-
*
|
|
63
|
-
|
|
61
|
+
/**
|
|
62
|
+
* Anything below — a role chip. A link or button here stays out of the arrow keys
|
|
63
|
+
* and in the Tab order, like the footer's.
|
|
64
|
+
*
|
|
65
|
+
* Pass a function to get `close`, as `footer` does — keksdose F7: its admin role
|
|
66
|
+
* chip links to the admin area, and a router link does not unmount the menu, so
|
|
67
|
+
* without `close` the panel stayed open over the page it had just navigated to.
|
|
68
|
+
*/
|
|
69
|
+
extra?: ReactNode | ((close: () => void) => ReactNode);
|
|
64
70
|
}
|
|
65
71
|
|
|
66
72
|
/** What a control is, as far as `HoverMenu` is concerned, when it lives in the header
|
|
@@ -225,6 +231,7 @@ export function TopBarActionMenu({
|
|
|
225
231
|
>
|
|
226
232
|
{(close) => {
|
|
227
233
|
const footerBody = typeof footer === "function" ? footer(close) : footer;
|
|
234
|
+
const headerExtra = typeof header?.extra === "function" ? header.extra(close) : header?.extra;
|
|
228
235
|
const footerClass =
|
|
229
236
|
"flex flex-wrap gap-x-3 gap-y-1 text-xs text-[var(--text-muted)] [&_a:hover]:text-[var(--text-primary)] [&_a]:rounded-sm [&_a:focus-visible]:outline-none [&_a:focus-visible]:ring-2 [&_a:focus-visible]:ring-[var(--brand)]";
|
|
230
237
|
return (
|
|
@@ -236,7 +243,7 @@ export function TopBarActionMenu({
|
|
|
236
243
|
{header.subtitle != null && (
|
|
237
244
|
<div className="truncate text-xs text-[var(--text-muted)]">{header.subtitle}</div>
|
|
238
245
|
)}
|
|
239
|
-
{
|
|
246
|
+
{headerExtra != null && headerExtra !== false && <div className="mt-1">{headerExtra}</div>}
|
|
240
247
|
</MenuSection>
|
|
241
248
|
)}
|
|
242
249
|
{heading && (
|
package/src/tour/tour.tsx
CHANGED
|
@@ -157,11 +157,15 @@ export function TourProvider({
|
|
|
157
157
|
const [activeId, setActiveId] = useState<string | null>(null);
|
|
158
158
|
const optsRef = useRef<StartOptions>({});
|
|
159
159
|
// Live mirrors so the step-control callbacks can stay stable (identity-safe for
|
|
160
|
-
// consumers) while still reading the current steps/index.
|
|
160
|
+
// consumers) while still reading the current steps/index. Synced in a layout
|
|
161
|
+
// effect — after the commit, before paint and before any passive effect or event
|
|
162
|
+
// can call them — rather than written during render.
|
|
161
163
|
const stepsRef = useRef<TourStep[] | null>(null);
|
|
162
|
-
stepsRef.current = steps;
|
|
163
164
|
const indexRef = useRef(0);
|
|
164
|
-
|
|
165
|
+
useLayoutEffect(() => {
|
|
166
|
+
stepsRef.current = steps;
|
|
167
|
+
indexRef.current = index;
|
|
168
|
+
}, [steps, index]);
|
|
165
169
|
const active = !!steps && steps.length > 0;
|
|
166
170
|
const total = steps?.length ?? 0;
|
|
167
171
|
|
|
@@ -277,6 +281,15 @@ function TourOverlay({
|
|
|
277
281
|
const isLast = index === steps.length - 1;
|
|
278
282
|
const [rect, setRect] = useState<Rect | null>(null);
|
|
279
283
|
const [ready, setReady] = useState(false);
|
|
284
|
+
// A new step starts unlocated: reset during render when the step changes, so the
|
|
285
|
+
// previous step's spotlight never paints over the new one. The effect below then
|
|
286
|
+
// locates it.
|
|
287
|
+
const [locating, setLocating] = useState({ index, step });
|
|
288
|
+
if (locating.index !== index || locating.step !== step) {
|
|
289
|
+
setLocating({ index, step });
|
|
290
|
+
setReady(false);
|
|
291
|
+
setRect(null);
|
|
292
|
+
}
|
|
280
293
|
// The overlay is portalled to <body>, out of whatever `dir` subtree the app set, so it
|
|
281
294
|
// takes the direction of the element it spotlights (the document's for a centered
|
|
282
295
|
// step). Arrow keys and start/end placement read it.
|
|
@@ -287,8 +300,6 @@ function TourOverlay({
|
|
|
287
300
|
useEffect(() => {
|
|
288
301
|
let cancelled = false;
|
|
289
302
|
let raf = 0;
|
|
290
|
-
setReady(false);
|
|
291
|
-
setRect(null);
|
|
292
303
|
void (async () => {
|
|
293
304
|
await step.beforeStep?.();
|
|
294
305
|
if (cancelled) return;
|
package/src/wizard/use-wizard.ts
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* The committing step is the last one unless a step says `commits: true`; the
|
|
15
15
|
* steps after that one are post-commit steps (see `WizardStepConfig.commits`).
|
|
16
16
|
*/
|
|
17
|
-
import { useCallback, useEffect, useReducer, useRef, useState } from "react";
|
|
17
|
+
import { useCallback, useEffect, useLayoutEffect, useReducer, useRef, useState } from "react";
|
|
18
18
|
import { useSearchParams } from "react-router";
|
|
19
19
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
20
20
|
import { DEFAULT_WIZARD_LABELS } from "./types";
|
|
@@ -163,8 +163,12 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
163
163
|
missingRequiredMessage,
|
|
164
164
|
onValidationFailed,
|
|
165
165
|
} = options;
|
|
166
|
+
// The latest `steps`, for the stable callbacks below. Synced after commit (before
|
|
167
|
+
// paint, and before any handler can run) rather than written during render.
|
|
166
168
|
const stepsRef = useRef<WizardStepConfig[]>(steps);
|
|
167
|
-
|
|
169
|
+
useLayoutEffect(() => {
|
|
170
|
+
stepsRef.current = steps;
|
|
171
|
+
}, [steps]);
|
|
168
172
|
// A hook, not a component, but it produces two sentences of its own — the
|
|
169
173
|
// "fill in the required fields" toast and the fallback submit error — and both
|
|
170
174
|
// reach the user. So it reads the provider like any component does; only the two
|
|
@@ -239,8 +243,8 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
239
243
|
// it back.
|
|
240
244
|
const clearStepUrl = useStepUrl(urlParam, state.currentStepIndex);
|
|
241
245
|
|
|
242
|
-
// `steps`, not `stepsRef.current`:
|
|
243
|
-
//
|
|
246
|
+
// `steps`, not `stepsRef.current`: reading a ref in render is what the hooks
|
|
247
|
+
// linter (rightly) objects to, and the ref only catches up after the commit.
|
|
244
248
|
const currentStep = steps[state.currentStepIndex];
|
|
245
249
|
const isFirstStep = state.currentStepIndex === 0;
|
|
246
250
|
const isLastStep = state.currentStepIndex === steps.length - 1;
|
|
@@ -14,6 +14,16 @@ export interface WizardStepProps {
|
|
|
14
14
|
/** The line under the title: what the step asks for, or why. Rendered only with a
|
|
15
15
|
* `title` or on its own when there is none. */
|
|
16
16
|
description?: ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Controls for the step as a whole, at the end of the header row beside the title —
|
|
19
|
+
* kastlan 43: the buildings step's "Add building" had nowhere to go but UNDER the
|
|
20
|
+
* list it adds to, where it falls below the fold once there are three buildings.
|
|
21
|
+
* A header action is where every other kit surface (`PageHeader`, `Card`) puts it.
|
|
22
|
+
*
|
|
23
|
+
* Wraps under the title on a narrow screen rather than squeezing it. Renders the
|
|
24
|
+
* header on its own if there is no `title` either.
|
|
25
|
+
*/
|
|
26
|
+
actions?: ReactNode;
|
|
17
27
|
/** The heading's level. Default 3: the page has its `h1` and the wizard's own
|
|
18
28
|
* chrome sits at `h2` (the review step, the cancel dialog), so a step is under it. */
|
|
19
29
|
headingLevel?: 2 | 3 | 4;
|
|
@@ -30,20 +40,42 @@ export function WizardStep({
|
|
|
30
40
|
title,
|
|
31
41
|
description,
|
|
32
42
|
headingLevel = 3,
|
|
43
|
+
actions,
|
|
33
44
|
}: WizardStepProps) {
|
|
34
45
|
const hasTitle = title !== undefined && title !== null && title !== false && title !== "";
|
|
35
46
|
const hasDescription =
|
|
36
47
|
description !== undefined && description !== null && description !== false && description !== "";
|
|
48
|
+
const hasActions = actions !== undefined && actions !== null && actions !== false && actions !== "";
|
|
37
49
|
const Heading = HEADING[headingLevel];
|
|
50
|
+
const heading = (
|
|
51
|
+
<>
|
|
52
|
+
{hasTitle && <Heading className="text-lg font-semibold text-[var(--text-primary)]">{title}</Heading>}
|
|
53
|
+
{hasDescription && <p className="text-sm text-[var(--text-muted)]">{description}</p>}
|
|
54
|
+
</>
|
|
55
|
+
);
|
|
38
56
|
return (
|
|
39
57
|
<div className={cn("space-y-6", className)}>
|
|
40
|
-
{
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
58
|
+
{hasActions ? (
|
|
59
|
+
// One row: the text takes the room, the actions keep their width at the end
|
|
60
|
+
// and wrap below it when there is not enough — `basis-64` is the width the
|
|
61
|
+
// title asks for before that happens (a zero basis would never wrap and
|
|
62
|
+
// squeeze the title to a word a line instead). `items-start` so a two-line
|
|
63
|
+
// description does not pull the button down to its middle; `ms-auto` keeps
|
|
64
|
+
// the actions at the end with no title, and when they wrap.
|
|
65
|
+
<div data-slot="wizard-step-header" className="flex flex-wrap items-start justify-between gap-x-4 gap-y-2">
|
|
66
|
+
{(hasTitle || hasDescription) && <div className="min-w-0 grow basis-64 space-y-1">{heading}</div>}
|
|
67
|
+
<div data-slot="wizard-step-actions" className="ms-auto flex shrink-0 flex-wrap items-center gap-2">
|
|
68
|
+
{actions}
|
|
69
|
+
</div>
|
|
46
70
|
</div>
|
|
71
|
+
) : (
|
|
72
|
+
// Without actions the header is exactly what it was, so a step that passes
|
|
73
|
+
// none renders as before.
|
|
74
|
+
(hasTitle || hasDescription) && (
|
|
75
|
+
<div data-slot="wizard-step-header" className="space-y-1">
|
|
76
|
+
{heading}
|
|
77
|
+
</div>
|
|
78
|
+
)
|
|
47
79
|
)}
|
|
48
80
|
{children}
|
|
49
81
|
</div>
|