@eifi1/ui-kit 0.9.0 → 0.11.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 +37 -23
- package/dist/components/alert-banner.d.ts +57 -5
- package/dist/components/alert-banner.js +63 -17
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +6 -0
- package/dist/components/breadcrumbs.d.ts +60 -0
- package/dist/components/breadcrumbs.js +88 -0
- package/dist/components/breadcrumbs.js.map +1 -0
- package/dist/components/bulk-action-bar.d.ts +112 -0
- package/dist/components/bulk-action-bar.js +196 -0
- package/dist/components/bulk-action-bar.js.map +1 -0
- package/dist/components/button-group.d.ts +58 -3
- package/dist/components/button-group.js +56 -5
- package/dist/components/button-group.js.map +1 -1
- package/dist/components/calculator.d.ts +6 -0
- package/dist/components/calendar-heatmap.d.ts +124 -0
- package/dist/components/calendar-heatmap.js +295 -0
- package/dist/components/calendar-heatmap.js.map +1 -0
- package/dist/components/chip.d.ts +31 -5
- package/dist/components/chip.js +85 -5
- package/dist/components/chip.js.map +1 -1
- package/dist/components/choice-card.d.ts +34 -2
- package/dist/components/choice-card.js +49 -0
- package/dist/components/choice-card.js.map +1 -1
- package/dist/components/copy-button.d.ts +14 -4
- package/dist/components/copy-button.js +17 -4
- package/dist/components/copy-button.js.map +1 -1
- package/dist/components/data-table.js +2 -0
- package/dist/components/data-table.js.map +1 -1
- package/dist/components/date-picker.d.ts +22 -2
- package/dist/components/date-picker.js +11 -3
- package/dist/components/date-picker.js.map +1 -1
- package/dist/components/description-list.d.ts +43 -6
- package/dist/components/description-list.js +91 -11
- package/dist/components/description-list.js.map +1 -1
- package/dist/components/dialog-frame.d.ts +7 -0
- package/dist/components/dialog-frame.js.map +1 -1
- package/dist/components/disclosure.d.ts +57 -2
- package/dist/components/disclosure.js +24 -12
- package/dist/components/disclosure.js.map +1 -1
- package/dist/components/field.d.ts +43 -0
- package/dist/components/field.js +52 -0
- package/dist/components/field.js.map +1 -0
- package/dist/components/file-button.d.ts +1 -0
- package/dist/components/file-dropzone.d.ts +4 -4
- package/dist/components/file-dropzone.js +2 -1
- package/dist/components/file-dropzone.js.map +1 -1
- package/dist/components/floating-panel.d.ts +147 -4
- package/dist/components/floating-panel.js +188 -28
- package/dist/components/floating-panel.js.map +1 -1
- package/dist/components/full-bleed-dialog.d.ts +23 -4
- package/dist/components/full-bleed-dialog.js +11 -2
- package/dist/components/full-bleed-dialog.js.map +1 -1
- package/dist/components/list.d.ts +187 -0
- package/dist/components/list.js +221 -0
- package/dist/components/list.js.map +1 -0
- package/dist/components/menu-item.d.ts +109 -0
- package/dist/components/menu-item.js +88 -0
- package/dist/components/menu-item.js.map +1 -0
- package/dist/components/mini-calendar.d.ts +70 -5
- package/dist/components/mini-calendar.js +166 -62
- package/dist/components/mini-calendar.js.map +1 -1
- package/dist/components/modal.d.ts +23 -1
- package/dist/components/modal.js +36 -10
- package/dist/components/modal.js.map +1 -1
- package/dist/components/nav-pills.d.ts +66 -0
- package/dist/components/nav-pills.js +65 -0
- package/dist/components/nav-pills.js.map +1 -0
- package/dist/components/number-field.d.ts +6 -0
- package/dist/components/number-input.d.ts +6 -0
- package/dist/components/numpad-sheet.d.ts +6 -0
- package/dist/components/page-contents.js +1 -2
- package/dist/components/page-contents.js.map +1 -1
- package/dist/components/page-header.d.ts +37 -0
- package/dist/components/page-header.js +36 -0
- package/dist/components/page-header.js.map +1 -0
- package/dist/components/progress-bar.d.ts +31 -2
- package/dist/components/progress-bar.js +73 -7
- package/dist/components/progress-bar.js.map +1 -1
- package/dist/components/series-chart-labels.d.ts +3 -0
- package/dist/components/series-chart-labels.js +2 -1
- package/dist/components/series-chart-labels.js.map +1 -1
- package/dist/components/series-chart.d.ts +32 -0
- package/dist/components/series-chart.js +141 -4
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/settings-fields.d.ts +1 -0
- package/dist/components/status-dot.d.ts +53 -0
- package/dist/components/status-dot.js +57 -0
- package/dist/components/status-dot.js.map +1 -0
- package/dist/components/table.d.ts +78 -13
- package/dist/components/table.js +84 -11
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +70 -0
- package/dist/components/text.js +24 -0
- package/dist/components/text.js.map +1 -0
- package/dist/components/time-input.d.ts +1 -0
- package/dist/components/toast.d.ts +167 -0
- package/dist/components/toast.js +226 -0
- package/dist/components/toast.js.map +1 -0
- package/dist/components/toggle-group.d.ts +44 -1
- package/dist/components/toggle-group.js +73 -4
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +46 -15
- package/dist/components/tooltip.js +87 -47
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +158 -7
- package/dist/components/ui.js +191 -10
- package/dist/components/ui.js.map +1 -1
- package/dist/components/user-avatar.d.ts +25 -3
- package/dist/components/user-avatar.js +32 -3
- package/dist/components/user-avatar.js.map +1 -1
- package/dist/hooks/use-close-transition.d.ts +7 -1
- package/dist/hooks/use-close-transition.js +5 -2
- package/dist/hooks/use-close-transition.js.map +1 -1
- package/dist/hooks/use-copy-to-clipboard.js +1 -1
- package/dist/hooks/use-copy-to-clipboard.js.map +1 -1
- package/dist/hooks/use-file-drop.d.ts +1 -0
- package/dist/i18n/defaults.d.ts +6 -0
- package/dist/i18n/defaults.js +11 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +11 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +6 -0
- package/dist/i18n/locales/de-CH.d.ts +6 -0
- package/dist/i18n/locales/de-informal.d.ts +6 -0
- package/dist/i18n/locales/de.d.ts +6 -0
- package/dist/i18n/locales/de.js +30 -2
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +6 -0
- package/dist/i18n/locales/es.js +30 -2
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +6 -0
- package/dist/i18n/locales/fr.js +30 -2
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +6 -0
- package/dist/i18n/locales/hu.js +30 -2
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +6 -0
- package/dist/i18n/locales/it.js +30 -2
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +6 -0
- package/dist/i18n/locales/zh.js +30 -2
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +27 -15
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +25 -0
- package/dist/lib/clipping.js +17 -0
- package/dist/lib/clipping.js.map +1 -0
- package/dist/lib/dates.d.ts +17 -1
- package/dist/lib/dates.js +19 -0
- package/dist/lib/dates.js.map +1 -1
- package/dist/rhf/form.d.ts +1 -0
- package/dist/rhf/use-rhf-wizard-step.d.ts +22 -0
- package/dist/rhf/use-rhf-wizard-step.js +38 -0
- package/dist/rhf/use-rhf-wizard-step.js.map +1 -0
- package/dist/rhf.d.ts +2 -0
- package/dist/rhf.js +1 -0
- package/dist/rhf.js.map +1 -1
- package/dist/search/command-palette.d.ts +19 -2
- package/dist/search/command-palette.js +16 -5
- package/dist/search/command-palette.js.map +1 -1
- package/dist/search/global-search.d.ts +26 -1
- package/dist/search/global-search.js +29 -13
- package/dist/search/global-search.js.map +1 -1
- package/dist/search.d.ts +1 -1
- package/dist/shell/topbar-action-menu.d.ts +73 -3
- package/dist/shell/topbar-action-menu.js +100 -27
- package/dist/shell/topbar-action-menu.js.map +1 -1
- package/dist/shell.d.ts +2 -1
- package/dist/wizard/stepper-nav.d.ts +1 -0
- package/dist/wizard/stepper-nav.js +1 -1
- package/dist/wizard/stepper-nav.js.map +1 -1
- package/dist/wizard/types.d.ts +2 -2
- package/dist/wizard/types.js.map +1 -1
- package/dist/wizard/use-wizard.js +1 -1
- package/dist/wizard/use-wizard.js.map +1 -1
- package/dist/wizard/wizard-context.d.ts +7 -1
- package/dist/wizard/wizard-context.js +4 -0
- package/dist/wizard/wizard-context.js.map +1 -1
- package/dist/wizard/wizard-summary.js +3 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/dist/wizard.d.ts +2 -1
- package/package.json +16 -12
- package/src/components/alert-banner.tsx +149 -21
- package/src/components/breadcrumbs.tsx +168 -0
- package/src/components/bulk-action-bar.tsx +362 -0
- package/src/components/button-group.tsx +124 -4
- package/src/components/calendar-heatmap.tsx +504 -0
- package/src/components/chip.tsx +128 -7
- package/src/components/choice-card.tsx +97 -1
- package/src/components/copy-button.tsx +29 -7
- package/src/components/data-table.tsx +7 -0
- package/src/components/date-picker.tsx +49 -1
- package/src/components/description-list.tsx +168 -15
- package/src/components/dialog-frame.tsx +7 -0
- package/src/components/disclosure.tsx +93 -21
- package/src/components/field.tsx +137 -0
- package/src/components/file-dropzone.tsx +9 -10
- package/src/components/floating-panel.tsx +366 -15
- package/src/components/full-bleed-dialog.tsx +42 -5
- package/src/components/list.tsx +443 -0
- package/src/components/menu-item.tsx +235 -0
- package/src/components/mini-calendar.tsx +245 -54
- package/src/components/modal.tsx +80 -17
- package/src/components/nav-pills.tsx +145 -0
- package/src/components/page-contents.tsx +4 -4
- package/src/components/page-header.tsx +68 -0
- package/src/components/progress-bar.tsx +129 -11
- package/src/components/series-chart-labels.ts +4 -0
- package/src/components/series-chart.tsx +274 -3
- package/src/components/status-dot.tsx +107 -0
- package/src/components/table.tsx +176 -17
- package/src/components/text.tsx +97 -0
- package/src/components/toast.tsx +441 -0
- package/src/components/toggle-group.tsx +128 -4
- package/src/components/tooltip.tsx +200 -100
- package/src/components/ui.tsx +381 -14
- package/src/components/user-avatar.tsx +57 -3
- package/src/hooks/use-close-transition.ts +14 -5
- package/src/hooks/use-copy-to-clipboard.ts +1 -1
- package/src/i18n/defaults.ts +10 -0
- package/src/i18n/kit-labels.tsx +10 -0
- package/src/i18n/locales/de.ts +29 -0
- package/src/i18n/locales/es.ts +29 -0
- package/src/i18n/locales/fr.ts +29 -0
- package/src/i18n/locales/hu.ts +29 -0
- package/src/i18n/locales/it.ts +29 -0
- package/src/i18n/locales/zh.ts +29 -0
- package/src/index.ts +31 -0
- package/src/lib/clipping.ts +34 -0
- package/src/lib/dates.ts +37 -0
- package/src/rhf/use-rhf-wizard-step.ts +113 -0
- package/src/rhf.ts +2 -0
- package/src/search/command-palette.tsx +42 -5
- package/src/search/global-search.tsx +59 -18
- package/src/shell/topbar-action-menu.tsx +239 -49
- package/src/wizard/stepper-nav.tsx +2 -1
- package/src/wizard/types.ts +2 -2
- package/src/wizard/use-wizard.ts +3 -3
- package/src/wizard/wizard-context.tsx +9 -0
- package/src/wizard/wizard-summary.tsx +14 -10
- package/tokens.css +97 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bridge between a react-hook-form step and the wizard's Next gate.
|
|
3
|
+
* `@eifi1/ui-kit/rhf`.
|
|
4
|
+
*
|
|
5
|
+
* ```tsx
|
|
6
|
+
* function AddressStep({ onUpdate }: { onUpdate: (v: Address) => void }) {
|
|
7
|
+
* const form = useForm<Address>({ defaultValues });
|
|
8
|
+
* useRhfWizardStep(form, { onValid: onUpdate });
|
|
9
|
+
* return <Form {...form}>…</Form>;
|
|
10
|
+
* }
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Inside a `<StepperNav>` the hook registers the step's validator with the wizard, so
|
|
14
|
+
* Next (and Finish on a form-bearing last step) runs it: on success the values go to
|
|
15
|
+
* `onValid` and the wizard advances; on failure react-hook-form shows its own inline
|
|
16
|
+
* `<FormMessage>`s, focuses the first field in error, and the wizard stays put — and,
|
|
17
|
+
* since the result carries no per-field errors, says its "fill in the required
|
|
18
|
+
* fields" line so the button did not seem to do nothing. It also RETURNS the
|
|
19
|
+
* validator, for a nav of your own outside `<StepperNav>` (where there is no wizard
|
|
20
|
+
* context to register with, so nothing is registered).
|
|
21
|
+
*
|
|
22
|
+
* Two modes:
|
|
23
|
+
*
|
|
24
|
+
* - **The whole form** (no `fields`): `form.handleSubmit`, exactly as kastlan's
|
|
25
|
+
* `use-rhf-wizard-step.ts` — every rule and the resolver run, `onValid` receives
|
|
26
|
+
* the TRANSFORMED values (a zod `.transform()` included), and focus follows the
|
|
27
|
+
* form's own `shouldFocusError` (on by default).
|
|
28
|
+
* - **Some fields** (`fields: ["iban", "bic"]`): `form.trigger(fields)`, for a wizard
|
|
29
|
+
* that keeps one form across several steps, each of which validates only what it
|
|
30
|
+
* shows. The first of those fields in error is focused (`shouldFocus`; the control
|
|
31
|
+
* has to take `field.ref`), errors on fields of later steps are not raised, and
|
|
32
|
+
* `onValid` receives `form.getValues()` — the untransformed values, because a
|
|
33
|
+
* resolver's output is only produced for the whole form.
|
|
34
|
+
*
|
|
35
|
+
* Both callbacks are read through refs, so the validator is registered once per form
|
|
36
|
+
* however the caller writes them (an inline `(v) => onUpdate(v)` is the usual case).
|
|
37
|
+
*
|
|
38
|
+
* The positional `useRhfWizardStep(form, onValid)` of the hook kastlan kept is the
|
|
39
|
+
* whole-form mode, and still accepted.
|
|
40
|
+
*
|
|
41
|
+
* Why it lives here and not in `@eifi1/ui-kit/wizard`: it imports react-hook-form,
|
|
42
|
+
* which only this entry may (see packaging-contract).
|
|
43
|
+
*/
|
|
44
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
45
|
+
import type { FieldPath, FieldValues, UseFormReturn } from "react-hook-form";
|
|
46
|
+
import { useOptionalWizardContext } from "../wizard/wizard-context";
|
|
47
|
+
|
|
48
|
+
export type RhfWizardStepOptions<
|
|
49
|
+
TFieldValues extends FieldValues,
|
|
50
|
+
TTransformed = TFieldValues,
|
|
51
|
+
> =
|
|
52
|
+
| {
|
|
53
|
+
/** Validate only these fields — the step's share of a form that spans steps. */
|
|
54
|
+
fields: readonly FieldPath<TFieldValues>[];
|
|
55
|
+
/** Called with `form.getValues()` when the fields pass, before the wizard
|
|
56
|
+
* advances. */
|
|
57
|
+
onValid?: (values: TFieldValues) => void;
|
|
58
|
+
}
|
|
59
|
+
| {
|
|
60
|
+
/** Omitted: validate the whole form through `handleSubmit`. */
|
|
61
|
+
fields?: undefined;
|
|
62
|
+
/** Called with the form's (transformed) values when it passes, before the
|
|
63
|
+
* wizard advances. */
|
|
64
|
+
onValid?: (values: TTransformed) => void;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Connects `form` to the wizard's step validation and returns the step's validator.
|
|
69
|
+
* See the module note for the two modes.
|
|
70
|
+
*/
|
|
71
|
+
export function useRhfWizardStep<
|
|
72
|
+
TFieldValues extends FieldValues,
|
|
73
|
+
TContext = unknown,
|
|
74
|
+
TTransformed = TFieldValues,
|
|
75
|
+
>(
|
|
76
|
+
form: UseFormReturn<TFieldValues, TContext, TTransformed>,
|
|
77
|
+
options?:
|
|
78
|
+
| RhfWizardStepOptions<TFieldValues, TTransformed>
|
|
79
|
+
| ((values: TTransformed) => void),
|
|
80
|
+
): () => Promise<boolean> {
|
|
81
|
+
const opts = typeof options === "function" ? { onValid: options } : (options ?? {});
|
|
82
|
+
const optsRef = useRef(opts);
|
|
83
|
+
useEffect(() => {
|
|
84
|
+
optsRef.current = opts;
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const validate = useCallback(async (): Promise<boolean> => {
|
|
88
|
+
const current = optsRef.current;
|
|
89
|
+
if (current.fields) {
|
|
90
|
+
const ok = await form.trigger([...current.fields], { shouldFocus: true });
|
|
91
|
+
if (ok) current.onValid?.(form.getValues());
|
|
92
|
+
return ok;
|
|
93
|
+
}
|
|
94
|
+
const onValid = current.onValid as ((values: TTransformed) => void) | undefined;
|
|
95
|
+
let ok = false;
|
|
96
|
+
await form.handleSubmit(
|
|
97
|
+
(values) => {
|
|
98
|
+
ok = true;
|
|
99
|
+
onValid?.(values);
|
|
100
|
+
},
|
|
101
|
+
() => {
|
|
102
|
+
ok = false;
|
|
103
|
+
},
|
|
104
|
+
)();
|
|
105
|
+
return ok;
|
|
106
|
+
}, [form]);
|
|
107
|
+
|
|
108
|
+
const wizard = useOptionalWizardContext();
|
|
109
|
+
const register = wizard?.registerStepValidate;
|
|
110
|
+
useEffect(() => register?.(validate), [register, validate]);
|
|
111
|
+
|
|
112
|
+
return validate;
|
|
113
|
+
}
|
package/src/rhf.ts
CHANGED
|
@@ -7,3 +7,5 @@
|
|
|
7
7
|
// the barrel dropped its last react-hook-form import (see the wizard section of
|
|
8
8
|
// src/index.ts). The packaging contract test holds that line.
|
|
9
9
|
export * from "./rhf/form";
|
|
10
|
+
// The react-hook-form bridge to the wizard's Next gate (useWizard lives in /wizard).
|
|
11
|
+
export * from "./rhf/use-rhf-wizard-step";
|
|
@@ -100,6 +100,18 @@ export const DEFAULT_COMMAND_PALETTE_LABELS: CommandPaletteLabels = {
|
|
|
100
100
|
close: "Close",
|
|
101
101
|
};
|
|
102
102
|
|
|
103
|
+
/**
|
|
104
|
+
* How tall the palette's rows are. `"compact"` (the default) is the desktop list the
|
|
105
|
+
* palette always drew, 36px a row; `"comfortable"` gives every row the kit's 44px touch
|
|
106
|
+
* target (`min-h-11`) and body-size text — keksdose's phone search, where a 36px row
|
|
107
|
+
* under a thumb picks its neighbour.
|
|
108
|
+
*/
|
|
109
|
+
export type CommandPaletteDensity = "compact" | "comfortable";
|
|
110
|
+
|
|
111
|
+
/** A provider's answer is a promise — the only case with anything to wait for. */
|
|
112
|
+
const isThenable = (value: unknown): value is PromiseLike<unknown> =>
|
|
113
|
+
typeof (value as PromiseLike<unknown> | null)?.then === "function";
|
|
114
|
+
|
|
103
115
|
/** The defaults minus `dialog`, so a resolved `dialog` means someone SUPPLIED one —
|
|
104
116
|
* see the dialog's `aria-label` for why that distinction matters. */
|
|
105
117
|
const { dialog: DEFAULT_DIALOG_NAME, ...DEFAULTS_WITHOUT_DIALOG } = DEFAULT_COMMAND_PALETTE_LABELS;
|
|
@@ -112,6 +124,14 @@ interface CommandPaletteProps {
|
|
|
112
124
|
* query is allowed — return defaults like pages/recent). Sync or async; stale
|
|
113
125
|
* async responses are ignored.
|
|
114
126
|
*
|
|
127
|
+
* A SYNCHRONOUS provider (one returning an array, not a promise) never shows the
|
|
128
|
+
* "Searching…" hint: its answer is there the moment it is asked, and the hint only
|
|
129
|
+
* flashed through the debounce before results that were never late — keksdose's
|
|
130
|
+
* transaction search filters a list already in memory. The palette tells the two
|
|
131
|
+
* apart by what the provider RETURNS, call by call, so there is no prop to keep in
|
|
132
|
+
* step with the implementation: an async provider shows the hint from the first
|
|
133
|
+
* promise on (and during the debounce of every later query), a sync one never.
|
|
134
|
+
*
|
|
115
135
|
* Its IDENTITY is not a re-run signal — it is read through a ref, so the palette
|
|
116
136
|
* never re-searches merely because the caller rebuilt the callback. That is
|
|
117
137
|
* deliberate: a provider closing over a list rendered from a query allocates a new
|
|
@@ -175,6 +195,8 @@ interface CommandPaletteProps {
|
|
|
175
195
|
* up. `false` keeps the card on every viewport.
|
|
176
196
|
*/
|
|
177
197
|
fullScreenOnPhone?: boolean;
|
|
198
|
+
/** Row height — see {@link CommandPaletteDensity}. Default `"compact"`. */
|
|
199
|
+
density?: CommandPaletteDensity;
|
|
178
200
|
labels?: Partial<CommandPaletteLabels>;
|
|
179
201
|
}
|
|
180
202
|
|
|
@@ -208,6 +230,7 @@ export function CommandPalette({
|
|
|
208
230
|
searchOn = "input",
|
|
209
231
|
redactLabels = false,
|
|
210
232
|
fullScreenOnPhone = true,
|
|
233
|
+
density = "compact",
|
|
211
234
|
labels,
|
|
212
235
|
}: CommandPaletteProps) {
|
|
213
236
|
const l = useKitLabels("commandPalette", DEFAULTS_WITHOUT_DIALOG, labels);
|
|
@@ -259,6 +282,10 @@ export function CommandPalette({
|
|
|
259
282
|
const searchRef = useRef(search);
|
|
260
283
|
searchRef.current = search;
|
|
261
284
|
const reqId = useRef(0);
|
|
285
|
+
/** What the provider's last answer was: a promise (`false`), an array (`true`), or
|
|
286
|
+
* not known yet. Only a provider known to be async shows "Searching…" while the
|
|
287
|
+
* debounce runs — see `search`. */
|
|
288
|
+
const syncRef = useRef<boolean | undefined>(undefined);
|
|
262
289
|
/** The query the results on screen answer, and the row highlighted in them — so a
|
|
263
290
|
* re-run for the SAME query (a `revision` change: a source streaming in) keeps the
|
|
264
291
|
* highlight on its row instead of throwing it back to the top mid-arrow. */
|
|
@@ -295,10 +322,13 @@ export function CommandPalette({
|
|
|
295
322
|
useEffect(() => {
|
|
296
323
|
if (!open) return;
|
|
297
324
|
const id = ++reqId.current;
|
|
298
|
-
setLoading(true);
|
|
325
|
+
if (syncRef.current === false) setLoading(true);
|
|
299
326
|
const run = async () => {
|
|
300
327
|
try {
|
|
301
|
-
const
|
|
328
|
+
const answer = searchRef.current(query);
|
|
329
|
+
syncRef.current = !isThenable(answer);
|
|
330
|
+
if (!syncRef.current) setLoading(true);
|
|
331
|
+
const r = await answer;
|
|
302
332
|
if (reqId.current === id) {
|
|
303
333
|
const keep = shownQuery.current === query ? activeIdRef.current : undefined;
|
|
304
334
|
const at = keep === undefined ? -1 : groupItems(r).flat.findIndex((item) => item.id === keep);
|
|
@@ -366,11 +396,14 @@ export function CommandPalette({
|
|
|
366
396
|
// Keep the active row scrolled into view.
|
|
367
397
|
useEffect(() => {
|
|
368
398
|
const el = listRef.current?.querySelector<HTMLElement>(`[data-index="${active}"]`);
|
|
369
|
-
|
|
399
|
+
// Optional-called, as in combobox-core: jsdom has no scrollIntoView, and every
|
|
400
|
+
// consumer's tests crashed on the first highlighted row (kastlan stubbed it).
|
|
401
|
+
el?.scrollIntoView?.({ block: "nearest" });
|
|
370
402
|
}, [active]);
|
|
371
403
|
|
|
372
404
|
if (!open) return null;
|
|
373
405
|
|
|
406
|
+
const comfortable = density === "comfortable";
|
|
374
407
|
let flatIndex = -1;
|
|
375
408
|
return createPortal(
|
|
376
409
|
<div
|
|
@@ -503,7 +536,10 @@ export function CommandPalette({
|
|
|
503
536
|
<li
|
|
504
537
|
key={item.id}
|
|
505
538
|
role="presentation"
|
|
506
|
-
className=
|
|
539
|
+
className={cn(
|
|
540
|
+
"flex items-center gap-2.5 px-3 text-xs text-[var(--text-placeholder)]",
|
|
541
|
+
comfortable ? "min-h-11 py-2.5" : "py-2",
|
|
542
|
+
)}
|
|
507
543
|
>
|
|
508
544
|
{item.icon && <span className="flex size-4 shrink-0 items-center justify-center">{item.icon}</span>}
|
|
509
545
|
<span className="min-w-0 flex-1 truncate">{item.label}</span>
|
|
@@ -525,7 +561,8 @@ export function CommandPalette({
|
|
|
525
561
|
"data-index": idx,
|
|
526
562
|
onMouseMove: () => setActive(idx),
|
|
527
563
|
className: cn(
|
|
528
|
-
"flex w-full items-center gap-2.5 px-3
|
|
564
|
+
"flex w-full items-center gap-2.5 px-3 text-start",
|
|
565
|
+
comfortable ? "min-h-11 py-2.5 text-base" : "py-2 text-sm",
|
|
529
566
|
isActive
|
|
530
567
|
? "bg-[var(--bg-active)] text-[var(--text-primary)]"
|
|
531
568
|
: "text-[var(--text-secondary)]",
|
|
@@ -7,7 +7,7 @@ import { TOPBAR_TRIGGER_CLASS } from "../shell/topbar-controls";
|
|
|
7
7
|
import { cn } from "../lib/cn";
|
|
8
8
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
9
9
|
import { CommandPalette, DEFAULT_COMMAND_PALETTE_LABELS, useCommandKey } from "./command-palette";
|
|
10
|
-
import type { CommandItem, CommandPaletteLabels } from "./command-palette";
|
|
10
|
+
import type { CommandItem, CommandPaletteDensity, CommandPaletteLabels } from "./command-palette";
|
|
11
11
|
import { createSearchIndex } from "./search-index";
|
|
12
12
|
import type { SearchEntry, SearchIndexOptions } from "./search-index";
|
|
13
13
|
|
|
@@ -94,6 +94,14 @@ export interface GlobalSearchProps {
|
|
|
94
94
|
sources?: readonly GlobalSearchSource[];
|
|
95
95
|
/** What the empty query shows, under the `suggestions` heading. */
|
|
96
96
|
suggestions?: readonly GlobalSearchSuggestion[];
|
|
97
|
+
/**
|
|
98
|
+
* Show each suggested ENTRY under its own `group` instead of all of them under the
|
|
99
|
+
* one `suggestions` heading — keksdose's empty palette listed "Pages" and "Actions"
|
|
100
|
+
* apart, and one "Try" heading over both lost the difference. An entry with no
|
|
101
|
+
* `group` and a query suggestion (`{ query }`) stay under `suggestions`. `groupOrder`
|
|
102
|
+
* orders these groups as it orders the results'. Off by default.
|
|
103
|
+
*/
|
|
104
|
+
suggestionsKeepGroups?: boolean;
|
|
97
105
|
/** Groups listed here come first, in this order; the rest follow in ranking order. */
|
|
98
106
|
groupOrder?: readonly string[];
|
|
99
107
|
/** Ranking and capping — `groupLimit` (default 8) applies to every group. */
|
|
@@ -130,6 +138,23 @@ export interface GlobalSearchProps {
|
|
|
130
138
|
hideTriggerOnPhone?: boolean;
|
|
131
139
|
/** Class for the default trigger button, merged over `TOPBAR_TRIGGER_CLASS`. */
|
|
132
140
|
triggerClassName?: string;
|
|
141
|
+
/**
|
|
142
|
+
* The default trigger's magnifier, in px. Default 16 (`size-4`), as it always was;
|
|
143
|
+
* 20 matches the kit's other top-bar triggers (`TopbarIconButton` draws `size-5`) and
|
|
144
|
+
* keksdose's top bar, which reached into the button with `[&_svg]:size-5` for it.
|
|
145
|
+
*/
|
|
146
|
+
triggerIconSize?: number;
|
|
147
|
+
/**
|
|
148
|
+
* Whether the trigger's accessible name includes the shortcut: `"plain"` (default)
|
|
149
|
+
* names it "Search"; `"withShortcut"` names it by `labels.shortcut(keys)` —
|
|
150
|
+
* "Search (⌘K)" on Apple platforms, "Search (Ctrl K)" elsewhere — the same text its
|
|
151
|
+
* tooltip shows, so a speech-control user ("click Search") and a sighted keyboard user
|
|
152
|
+
* meet the shortcut in the name itself (keksdose C25). `aria-keyshortcuts` is set
|
|
153
|
+
* either way. Also reaches a custom trigger as `triggerProps.label`.
|
|
154
|
+
*/
|
|
155
|
+
triggerName?: "plain" | "withShortcut";
|
|
156
|
+
/** The palette's row height — `CommandPalette`'s `density`. Default `"compact"`. */
|
|
157
|
+
density?: CommandPaletteDensity;
|
|
133
158
|
labels?: Partial<GlobalSearchLabels>;
|
|
134
159
|
/** Passed through to the palette (`empty`, `loading`, `error`, `hint`, …). */
|
|
135
160
|
paletteLabels?: Partial<CommandPaletteLabels>;
|
|
@@ -211,6 +236,7 @@ function GlobalSearchImpl({
|
|
|
211
236
|
entries,
|
|
212
237
|
sources,
|
|
213
238
|
suggestions,
|
|
239
|
+
suggestionsKeepGroups = false,
|
|
214
240
|
groupOrder,
|
|
215
241
|
indexOptions,
|
|
216
242
|
navigate,
|
|
@@ -226,6 +252,9 @@ function GlobalSearchImpl({
|
|
|
226
252
|
trigger = "button",
|
|
227
253
|
hideTriggerOnPhone = false,
|
|
228
254
|
triggerClassName,
|
|
255
|
+
triggerIconSize,
|
|
256
|
+
triggerName = "plain",
|
|
257
|
+
density,
|
|
229
258
|
labels,
|
|
230
259
|
paletteLabels,
|
|
231
260
|
}: GlobalSearchProps & { navigate: (href: string) => void; hrefFor: (href: string) => string }) {
|
|
@@ -360,12 +389,27 @@ function GlobalSearchImpl({
|
|
|
360
389
|
// which the compiler lint cannot tell from a read during render.
|
|
361
390
|
/* eslint-disable react-hooks/refs */
|
|
362
391
|
const items = useMemo((): CommandItem[] => {
|
|
392
|
+
const ordered = (out: CommandItem[]) => {
|
|
393
|
+
if (!order.length) return out;
|
|
394
|
+
const rank = (group: string) => {
|
|
395
|
+
const i = order.indexOf(group);
|
|
396
|
+
return i === -1 ? order.length : i;
|
|
397
|
+
};
|
|
398
|
+
// Stable: within a rank, groups keep the order their best hit gave them.
|
|
399
|
+
return out
|
|
400
|
+
.map((item, i) => ({ item, i }))
|
|
401
|
+
.sort((a, b) => rank(a.item.group) - rank(b.item.group) || a.i - b.i)
|
|
402
|
+
.map(({ item }) => item);
|
|
403
|
+
};
|
|
404
|
+
|
|
363
405
|
if (!q) {
|
|
364
406
|
const out: CommandItem[] = [];
|
|
407
|
+
const groupOf = (entry: SearchEntry) =>
|
|
408
|
+
suggestionsKeepGroups ? (entry.group ?? l.suggestions) : l.suggestions;
|
|
365
409
|
suggestionList.forEach((s, i) => {
|
|
366
410
|
if (typeof s === "string") {
|
|
367
411
|
const entry = byId.get(s);
|
|
368
|
-
if (entry) out.push(toItem("try:", entry,
|
|
412
|
+
if (entry) out.push(toItem("try:", entry, groupOf(entry)));
|
|
369
413
|
} else if ("query" in s) {
|
|
370
414
|
out.push({
|
|
371
415
|
id: `try:q${i}`,
|
|
@@ -380,9 +424,10 @@ function GlobalSearchImpl({
|
|
|
380
424
|
refocusField();
|
|
381
425
|
},
|
|
382
426
|
});
|
|
383
|
-
} else out.push(toItem("try:", s,
|
|
427
|
+
} else out.push(toItem("try:", s, groupOf(s)));
|
|
384
428
|
});
|
|
385
|
-
|
|
429
|
+
// One heading has nothing to order; kept groups are ordered like results.
|
|
430
|
+
return suggestionsKeepGroups ? ordered(out) : out;
|
|
386
431
|
}
|
|
387
432
|
|
|
388
433
|
const out = index.search(q).map((hit) => toItem("s:", hit.entry, hit.entry.group ?? l.results));
|
|
@@ -410,17 +455,8 @@ function GlobalSearchImpl({
|
|
|
410
455
|
}
|
|
411
456
|
}
|
|
412
457
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
const i = order.indexOf(group);
|
|
416
|
-
return i === -1 ? order.length : i;
|
|
417
|
-
};
|
|
418
|
-
// Stable: within a rank, groups keep the order their best hit gave them.
|
|
419
|
-
return out
|
|
420
|
-
.map((item, i) => ({ item, i }))
|
|
421
|
-
.sort((a, b) => rank(a.item.group) - rank(b.item.group) || a.i - b.i)
|
|
422
|
-
.map(({ item }) => item);
|
|
423
|
-
}, [q, suggestionList, byId, index, sourceList, sourceResults, order, groupLimit, toItem, l.suggestions, l.results, pl.loading, pl.error, setOpen, setQuery, noop, redactLabels]);
|
|
458
|
+
return ordered(out);
|
|
459
|
+
}, [q, suggestionList, suggestionsKeepGroups, byId, index, sourceList, sourceResults, order, groupLimit, toItem, l.suggestions, l.results, pl.loading, pl.error, setOpen, setQuery, noop, redactLabels]);
|
|
424
460
|
/* eslint-enable react-hooks/refs */
|
|
425
461
|
|
|
426
462
|
// The palette re-runs its provider on a `revision` change, never on the provider's
|
|
@@ -434,9 +470,10 @@ function GlobalSearchImpl({
|
|
|
434
470
|
|
|
435
471
|
const apple = useMemo(() => isApplePlatform(), []);
|
|
436
472
|
const keys = apple ? "⌘K" : "Ctrl K";
|
|
473
|
+
const triggerLabel = triggerName === "withShortcut" ? l.shortcut(keys) : l.trigger;
|
|
437
474
|
const triggerProps: GlobalSearchTriggerProps = {
|
|
438
475
|
open: openSearch,
|
|
439
|
-
label:
|
|
476
|
+
label: triggerLabel,
|
|
440
477
|
tooltip: l.shortcut(keys),
|
|
441
478
|
keys,
|
|
442
479
|
ariaKeyShortcuts: "Meta+K Control+K",
|
|
@@ -456,12 +493,15 @@ function GlobalSearchImpl({
|
|
|
456
493
|
<button
|
|
457
494
|
type="button"
|
|
458
495
|
onClick={openSearch}
|
|
459
|
-
aria-label={
|
|
496
|
+
aria-label={triggerLabel}
|
|
460
497
|
aria-keyshortcuts={triggerProps.ariaKeyShortcuts}
|
|
461
498
|
aria-haspopup="dialog"
|
|
462
499
|
className={cn(TOPBAR_TRIGGER_CLASS, triggerClassName)}
|
|
463
500
|
>
|
|
464
|
-
<Search
|
|
501
|
+
<Search
|
|
502
|
+
className={triggerIconSize === undefined ? "size-4" : undefined}
|
|
503
|
+
style={triggerIconSize === undefined ? undefined : { width: triggerIconSize, height: triggerIconSize }}
|
|
504
|
+
/>
|
|
465
505
|
</button>
|
|
466
506
|
</Tooltip>
|
|
467
507
|
)}
|
|
@@ -475,6 +515,7 @@ function GlobalSearchImpl({
|
|
|
475
515
|
searchOn={searchOn}
|
|
476
516
|
redactLabels={redactLabels}
|
|
477
517
|
fullScreenOnPhone={fullScreenOnPhone}
|
|
518
|
+
density={density}
|
|
478
519
|
labels={resolvedPaletteLabels}
|
|
479
520
|
/>
|
|
480
521
|
</>
|