@eifi1/ui-kit 0.9.0 → 0.10.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 +21 -11
- package/dist/components/alert-banner.d.ts +32 -5
- package/dist/components/alert-banner.js +57 -14
- package/dist/components/alert-banner.js.map +1 -1
- package/dist/components/amount-input.d.ts +4 -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 +66 -0
- package/dist/components/bulk-action-bar.js +149 -0
- package/dist/components/bulk-action-bar.js.map +1 -0
- package/dist/components/calculator.d.ts +4 -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/copy-button.d.ts +13 -4
- package/dist/components/copy-button.js +17 -4
- package/dist/components/copy-button.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/file-dropzone.d.ts +3 -4
- package/dist/components/file-dropzone.js +2 -1
- package/dist/components/file-dropzone.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 +174 -0
- package/dist/components/list.js +215 -0
- package/dist/components/list.js.map +1 -0
- package/dist/components/menu-item.d.ts +99 -0
- package/dist/components/menu-item.js +83 -0
- package/dist/components/menu-item.js.map +1 -0
- 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/number-field.d.ts +4 -0
- package/dist/components/number-input.d.ts +4 -0
- package/dist/components/numpad-sheet.d.ts +4 -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/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 +16 -0
- package/dist/components/series-chart.js +137 -3
- package/dist/components/series-chart.js.map +1 -1
- package/dist/components/status-dot.d.ts +49 -0
- package/dist/components/status-dot.js +52 -0
- package/dist/components/status-dot.js.map +1 -0
- package/dist/components/table.d.ts +39 -6
- package/dist/components/table.js +43 -4
- package/dist/components/table.js.map +1 -1
- package/dist/components/text.d.ts +63 -0
- package/dist/components/text.js +23 -0
- package/dist/components/text.js.map +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 +29 -1
- package/dist/components/toggle-group.js +52 -3
- package/dist/components/toggle-group.js.map +1 -1
- package/dist/components/tooltip.d.ts +38 -15
- package/dist/components/tooltip.js +85 -47
- package/dist/components/tooltip.js.map +1 -1
- package/dist/components/ui.d.ts +104 -4
- package/dist/components/ui.js +112 -8
- package/dist/components/ui.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/i18n/defaults.d.ts +4 -0
- package/dist/i18n/defaults.js +9 -1
- package/dist/i18n/defaults.js.map +1 -1
- package/dist/i18n/kit-labels.d.ts +8 -0
- package/dist/i18n/kit-labels.js.map +1 -1
- package/dist/i18n/locales/de-CH-informal.d.ts +4 -0
- package/dist/i18n/locales/de-CH.d.ts +4 -0
- package/dist/i18n/locales/de-informal.d.ts +4 -0
- package/dist/i18n/locales/de.d.ts +4 -0
- package/dist/i18n/locales/de.js +21 -1
- package/dist/i18n/locales/de.js.map +1 -1
- package/dist/i18n/locales/es.d.ts +4 -0
- package/dist/i18n/locales/es.js +21 -1
- package/dist/i18n/locales/es.js.map +1 -1
- package/dist/i18n/locales/fr.d.ts +4 -0
- package/dist/i18n/locales/fr.js +21 -1
- package/dist/i18n/locales/fr.js.map +1 -1
- package/dist/i18n/locales/hu.d.ts +4 -0
- package/dist/i18n/locales/hu.js +21 -1
- package/dist/i18n/locales/hu.js.map +1 -1
- package/dist/i18n/locales/it.d.ts +4 -0
- package/dist/i18n/locales/it.js +21 -1
- package/dist/i18n/locales/it.js.map +1 -1
- package/dist/i18n/locales/zh.d.ts +4 -0
- package/dist/i18n/locales/zh.js +21 -1
- package/dist/i18n/locales/zh.js.map +1 -1
- package/dist/index.d.ts +15 -7
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -1
- package/dist/lib/clipping.d.ts +9 -0
- package/dist/lib/clipping.js +14 -0
- package/dist/lib/clipping.js.map +1 -0
- 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/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-summary.js +3 -3
- package/dist/wizard/wizard-summary.js.map +1 -1
- package/package.json +16 -12
- package/src/components/alert-banner.tsx +117 -18
- package/src/components/breadcrumbs.tsx +168 -0
- package/src/components/bulk-action-bar.tsx +256 -0
- package/src/components/chip.tsx +128 -7
- package/src/components/copy-button.tsx +29 -7
- 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/file-dropzone.tsx +9 -10
- package/src/components/full-bleed-dialog.tsx +42 -5
- package/src/components/list.tsx +417 -0
- package/src/components/menu-item.tsx +214 -0
- package/src/components/modal.tsx +80 -17
- package/src/components/page-contents.tsx +4 -4
- package/src/components/page-header.tsx +68 -0
- package/src/components/series-chart-labels.ts +4 -0
- package/src/components/series-chart.tsx +253 -3
- package/src/components/status-dot.tsx +99 -0
- package/src/components/table.tsx +91 -6
- package/src/components/text.tsx +89 -0
- package/src/components/toast.tsx +441 -0
- package/src/components/toggle-group.tsx +82 -3
- package/src/components/tooltip.tsx +186 -100
- package/src/components/ui.tsx +237 -8
- package/src/hooks/use-close-transition.ts +14 -5
- package/src/hooks/use-copy-to-clipboard.ts +1 -1
- package/src/i18n/defaults.ts +8 -0
- package/src/i18n/kit-labels.tsx +8 -0
- package/src/i18n/locales/de.ts +20 -0
- package/src/i18n/locales/es.ts +20 -0
- package/src/i18n/locales/fr.ts +20 -0
- package/src/i18n/locales/hu.ts +20 -0
- package/src/i18n/locales/it.ts +20 -0
- package/src/i18n/locales/zh.ts +20 -0
- package/src/index.ts +26 -0
- package/src/lib/clipping.ts +16 -0
- package/src/search/command-palette.tsx +42 -5
- package/src/search/global-search.tsx +59 -18
- 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-summary.tsx +14 -10
- package/tokens.css +97 -0
package/src/index.ts
CHANGED
|
@@ -96,6 +96,8 @@ export * from "./components/danger-confirm";
|
|
|
96
96
|
export * from "./components/confirm-dialog";
|
|
97
97
|
// A non-modal corner panel and its round trigger (keksdose's assistant launcher).
|
|
98
98
|
export * from "./components/floating-panel";
|
|
99
|
+
// The count / clear / actions bar a selection brings up (keksdose's three copies).
|
|
100
|
+
export * from "./components/bulk-action-bar";
|
|
99
101
|
export * from "./components/swatch-picker";
|
|
100
102
|
export * from "./components/icon-picker";
|
|
101
103
|
export * from "./components/choice-card";
|
|
@@ -151,6 +153,30 @@ export * from "./components/separator";
|
|
|
151
153
|
// Named: scroll-area.tsx also holds the overflow hook and scrollbar class Table shares.
|
|
152
154
|
export { ScrollArea } from "./components/scroll-area";
|
|
153
155
|
export type { ScrollAreaProps } from "./components/scroll-area";
|
|
156
|
+
// 0.10.0 rows, menu rows, text primitives and page chrome the apps drew by hand.
|
|
157
|
+
export * from "./components/list";
|
|
158
|
+
export * from "./components/menu-item";
|
|
159
|
+
export * from "./components/text";
|
|
160
|
+
export * from "./components/status-dot";
|
|
161
|
+
export * from "./components/page-header";
|
|
162
|
+
export * from "./components/breadcrumbs";
|
|
163
|
+
// The toast layer over sonner (an OPTIONAL peer, loaded lazily): `toast` mirrors sonner's
|
|
164
|
+
// API so apps migrate by swapping the import; `<Toaster>` carries the placement, theme,
|
|
165
|
+
// tones and z-index both apps had wired by hand.
|
|
166
|
+
export { toast, Toaster, DEFAULT_TOAST_LABELS, TOAST_ACTION_DURATION } from "./components/toast";
|
|
167
|
+
export type {
|
|
168
|
+
ToastId,
|
|
169
|
+
ToastAction,
|
|
170
|
+
ToastOptions,
|
|
171
|
+
ToastPromiseOptions,
|
|
172
|
+
ToastUndoOptions,
|
|
173
|
+
ToastRedoOptions,
|
|
174
|
+
ToastLabels,
|
|
175
|
+
ToastPosition,
|
|
176
|
+
ToastSwipeDirection,
|
|
177
|
+
ToasterOffset,
|
|
178
|
+
ToasterProps,
|
|
179
|
+
} from "./components/toast";
|
|
154
180
|
|
|
155
181
|
// ── data-table suite ─────────────────────────────────────────────────────────
|
|
156
182
|
export * from "./components/data-table-labels";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Internal (not exported from the package): the Tooltip's auto-portal check. */
|
|
2
|
+
|
|
3
|
+
/** Whether anything between `el` and `<body>` clips or scrolls its content — the
|
|
4
|
+
* containers an absolutely positioned bubble would be cut off by, or would widen.
|
|
5
|
+
* `<body>` and `<html>` are not counted: their `overflow` belongs to the viewport (a
|
|
6
|
+
* dialog's scroll lock sets it), and the viewport clips a portalled bubble just the
|
|
7
|
+
* same. */
|
|
8
|
+
export function hasClippingAncestor(el: Element | null): boolean {
|
|
9
|
+
if (typeof window === "undefined" || !el) return false;
|
|
10
|
+
for (let node = el.parentElement; node; node = node.parentElement) {
|
|
11
|
+
if (node === document.body || node === document.documentElement) return false;
|
|
12
|
+
const style = window.getComputedStyle(node);
|
|
13
|
+
if (style.overflowX !== "visible" || style.overflowY !== "visible") return true;
|
|
14
|
+
}
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
@@ -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
|
</>
|
|
@@ -107,7 +107,8 @@ export function StepperNav<TData extends Record<string, unknown>>({
|
|
|
107
107
|
const pending = wizard.isValidating;
|
|
108
108
|
const stepNextLabel = wizard.currentStep.nextLabel;
|
|
109
109
|
|
|
110
|
-
|
|
110
|
+
// Every branch below assigns it; no initial value to overwrite.
|
|
111
|
+
let forwardButton: ReactNode;
|
|
111
112
|
if (wizard.isCommitStep) {
|
|
112
113
|
const finishButton = (
|
|
113
114
|
<Button
|
package/src/wizard/types.ts
CHANGED
|
@@ -109,8 +109,8 @@ export interface UseWizardOptions<TData extends Record<string, unknown>> {
|
|
|
109
109
|
*/
|
|
110
110
|
missingRequiredMessage?: string;
|
|
111
111
|
/**
|
|
112
|
-
* How to surface {@link missingRequiredMessage}. Defaults to
|
|
113
|
-
* (an optional peer,
|
|
112
|
+
* How to surface {@link missingRequiredMessage}. Defaults to the kit's `toast.error`
|
|
113
|
+
* (sonner, an optional peer, is loaded only when a toast is shown). Pass your own to
|
|
114
114
|
* route it somewhere else, or to a no-op to silence it.
|
|
115
115
|
*/
|
|
116
116
|
onValidationFailed?: (message: string) => void;
|
package/src/wizard/use-wizard.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { useCallback, useEffect, 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";
|
|
21
|
+
import { toast } from "../components/toast";
|
|
21
22
|
import type {
|
|
22
23
|
FieldErrors,
|
|
23
24
|
StepStatus,
|
|
@@ -312,9 +313,8 @@ export function useWizard<TData extends Record<string, unknown>>(
|
|
|
312
313
|
if (onValidationFailed) {
|
|
313
314
|
onValidationFailed(message);
|
|
314
315
|
} else {
|
|
315
|
-
// sonner
|
|
316
|
-
//
|
|
317
|
-
const { toast } = await import("sonner");
|
|
316
|
+
// The kit's `toast` loads sonner (an optional peer) lazily, so an app
|
|
317
|
+
// that never trips this never has to install it.
|
|
318
318
|
toast.error(message);
|
|
319
319
|
}
|
|
320
320
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Pencil } from "lucide-react";
|
|
2
2
|
import { Card, CardAction, CardContent, CardHeader, CardTitle, IconButton } from "../components/ui";
|
|
3
|
+
import { Tooltip } from "../components/tooltip";
|
|
3
4
|
import type { SummarySection, WizardLabels } from "./types";
|
|
4
5
|
import { DEFAULT_WIZARD_LABELS } from "./types";
|
|
5
6
|
import { useKitLabels } from "../i18n/kit-labels";
|
|
@@ -44,16 +45,19 @@ export function WizardSummary({
|
|
|
44
45
|
<CardTitle className="text-base">{section.label}</CardTitle>
|
|
45
46
|
{section.stepIndex !== undefined && onEditStep && (
|
|
46
47
|
<CardAction>
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
48
|
+
{/* The kit Tooltip, not a native `title`: it shows on keyboard focus too,
|
|
49
|
+
and an app with a one-tooltip rule (keksdose dev#523) sees one. */}
|
|
50
|
+
<Tooltip label={l.edit}>
|
|
51
|
+
<IconButton
|
|
52
|
+
size="sm"
|
|
53
|
+
variant="ghost"
|
|
54
|
+
aria-label={l.edit}
|
|
55
|
+
disabled={disabled}
|
|
56
|
+
onClick={() => onEditStep(section.stepIndex as number)}
|
|
57
|
+
>
|
|
58
|
+
<Pencil />
|
|
59
|
+
</IconButton>
|
|
60
|
+
</Tooltip>
|
|
57
61
|
</CardAction>
|
|
58
62
|
)}
|
|
59
63
|
</CardHeader>
|
package/tokens.css
CHANGED
|
@@ -233,6 +233,103 @@
|
|
|
233
233
|
--success-bg: rgba(6, 78, 59, 0.25);
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
/* ── Toasts ─────────────────────────────────────────────────────────────────
|
|
237
|
+
The kit `<Toaster>` (components/toast.tsx) paints sonner with the AlertBanner
|
|
238
|
+
tones. These are ALIASES, not new colours, and they exist for one reason: sonner
|
|
239
|
+
declares its own `--success-bg` / `--success-border` (and info, warning) on the
|
|
240
|
+
toaster element, so that element cannot read the kit's value under the same name
|
|
241
|
+
— `--success-bg: var(--success-bg)` is a cycle. Declared here, they are computed
|
|
242
|
+
on `<html>` and inherited down under a name sonner does not touch.
|
|
243
|
+
|
|
244
|
+
The background is the tone's wash layered on the surface, as AlertBanner's
|
|
245
|
+
`elevated` frame does: the dark washes are translucent, and a toast floats over
|
|
246
|
+
content. `--z-toast` is above every kit overlay (modal and sheets z-50, tour
|
|
247
|
+
z-60/61, command palette z-70) and above whatever an app stacks on top of those —
|
|
248
|
+
a toast confirming a dialog's action must not land under the dialog. */
|
|
249
|
+
:root,
|
|
250
|
+
.dark {
|
|
251
|
+
--z-toast: 2147483647;
|
|
252
|
+
--toast-success-bg: linear-gradient(var(--success-bg), var(--success-bg)) var(--bg-surface);
|
|
253
|
+
--toast-success-border: var(--success-border);
|
|
254
|
+
--toast-info-bg: linear-gradient(var(--info-bg), var(--info-bg)) var(--bg-surface);
|
|
255
|
+
--toast-info-border: var(--info-border);
|
|
256
|
+
--toast-warning-bg: linear-gradient(var(--warning-bg), var(--warning-bg)) var(--bg-surface);
|
|
257
|
+
--toast-warning-border: var(--warning-border);
|
|
258
|
+
--toast-danger-bg: linear-gradient(var(--danger-bg), var(--danger-bg)) var(--bg-surface);
|
|
259
|
+
--toast-danger-border: var(--danger-border);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/* The description of an untoned toast in the secondary text colour (sonner hard-codes
|
|
263
|
+
#3f3f3f, unreadable in the dark theme); a toned toast's inherits its tone. */
|
|
264
|
+
.kit-toaster [data-sonner-toast][data-styled="true"]:not([data-type="success"], [data-type="info"], [data-type="warning"], [data-type="error"]) [data-description] {
|
|
265
|
+
color: var(--text-secondary);
|
|
266
|
+
}
|
|
267
|
+
/* The action button in the tone's own colour rather than sonner's black pill. */
|
|
268
|
+
.kit-toaster [data-sonner-toast][data-styled="true"] [data-button]:not([data-cancel]) {
|
|
269
|
+
background: var(--toast-action-bg, var(--text-primary));
|
|
270
|
+
color: var(--bg-surface);
|
|
271
|
+
}
|
|
272
|
+
.kit-toaster [data-sonner-toast][data-type="success"] { --toast-action-bg: var(--success); }
|
|
273
|
+
.kit-toaster [data-sonner-toast][data-type="info"] { --toast-action-bg: var(--info); }
|
|
274
|
+
.kit-toaster [data-sonner-toast][data-type="warning"] { --toast-action-bg: var(--warning); }
|
|
275
|
+
.kit-toaster [data-sonner-toast][data-type="error"] { --toast-action-bg: var(--danger); }
|
|
276
|
+
/* The kit's focus ring on the toast's buttons — sonner's is a black shadow that
|
|
277
|
+
vanishes on the dark theme. */
|
|
278
|
+
.kit-toaster [data-sonner-toast][data-styled="true"] :is([data-button], [data-close-button]):focus-visible {
|
|
279
|
+
outline: 2px solid var(--brand);
|
|
280
|
+
outline-offset: 2px;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* ── Categorical hues ───────────────────────────────────────────────────────
|
|
284
|
+
Colours that MEAN nothing but "this is a different value": kastlan's status
|
|
285
|
+
enums (status-badge.tsx over enum-meta.ts), where ~15 enums map to 9 Tailwind
|
|
286
|
+
hues. Four of those are semantic and already have tokens — green = --success,
|
|
287
|
+
yellow = --warning, red = --danger, gray = the neutral chip — so only the five
|
|
288
|
+
below are new: blue (open / sent / rented), purple (assigned / accepted),
|
|
289
|
+
orange (terminated / high urgency), teal (invoiced) and indigo (a meeting
|
|
290
|
+
stage). The smallest set that covers every enum; deliberately not a ramp.
|
|
291
|
+
|
|
292
|
+
Same triple and same shades as the status family (700 text / 300 border /
|
|
293
|
+
100 surface in light, 300 text / translucent 700 border / 900 wash in dark),
|
|
294
|
+
so a categorical chip sits beside a semantic one at the same weight. Not in
|
|
295
|
+
`TokenSet`: like `--danger`, a hue is a specific colour, not a tint of the
|
|
296
|
+
palette preset, so it lives here and nowhere else. */
|
|
297
|
+
:root {
|
|
298
|
+
--hue-blue: #1d4ed8; /* blue-700 */
|
|
299
|
+
--hue-blue-border: #93c5fd; /* blue-300 */
|
|
300
|
+
--hue-blue-bg: #dbeafe; /* blue-100 */
|
|
301
|
+
--hue-indigo: #4338ca; /* indigo-700 */
|
|
302
|
+
--hue-indigo-border: #a5b4fc; /* indigo-300 */
|
|
303
|
+
--hue-indigo-bg: #e0e7ff; /* indigo-100 */
|
|
304
|
+
--hue-purple: #7e22ce; /* purple-700 */
|
|
305
|
+
--hue-purple-border: #d8b4fe; /* purple-300 */
|
|
306
|
+
--hue-purple-bg: #f3e8ff; /* purple-100 */
|
|
307
|
+
--hue-teal: #0f766e; /* teal-700 */
|
|
308
|
+
--hue-teal-border: #5eead4; /* teal-300 */
|
|
309
|
+
--hue-teal-bg: #ccfbf1; /* teal-100 */
|
|
310
|
+
--hue-orange: #c2410c; /* orange-700 */
|
|
311
|
+
--hue-orange-border: #fdba74; /* orange-300 */
|
|
312
|
+
--hue-orange-bg: #ffedd5; /* orange-100 */
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
.dark {
|
|
316
|
+
--hue-blue: #93c5fd; /* blue-300 */
|
|
317
|
+
--hue-blue-border: rgba(29, 78, 216, 0.6);
|
|
318
|
+
--hue-blue-bg: rgba(30, 58, 138, 0.25);
|
|
319
|
+
--hue-indigo: #a5b4fc; /* indigo-300 */
|
|
320
|
+
--hue-indigo-border: rgba(67, 56, 202, 0.6);
|
|
321
|
+
--hue-indigo-bg: rgba(49, 46, 129, 0.25);
|
|
322
|
+
--hue-purple: #d8b4fe; /* purple-300 */
|
|
323
|
+
--hue-purple-border: rgba(126, 34, 206, 0.6);
|
|
324
|
+
--hue-purple-bg: rgba(88, 28, 135, 0.25);
|
|
325
|
+
--hue-teal: #5eead4; /* teal-300 */
|
|
326
|
+
--hue-teal-border: rgba(15, 118, 110, 0.6);
|
|
327
|
+
--hue-teal-bg: rgba(19, 78, 74, 0.25);
|
|
328
|
+
--hue-orange: #fdba74; /* orange-300 */
|
|
329
|
+
--hue-orange-border: rgba(194, 65, 12, 0.6);
|
|
330
|
+
--hue-orange-bg: rgba(124, 45, 18, 0.25);
|
|
331
|
+
}
|
|
332
|
+
|
|
236
333
|
/* Semantic colour utilities backed by the tokens above. Unlayered so they win
|
|
237
334
|
over Tailwind's layered colour utilities, and theme-aware for free (the var
|
|
238
335
|
flips under .dark) — no dark: variant needed at the call site. */
|