@seekalfred/primitives 0.7.3 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +315 -4
- package/dist/alert/alert.d.ts +2 -2
- package/dist/alert/alert.js +1 -1
- package/dist/alert/alert.js.map +1 -1
- package/dist/alert-dialog/alert-dialog.d.ts +14 -0
- package/dist/alert-dialog/alert-dialog.js +52 -27
- package/dist/alert-dialog/alert-dialog.js.map +1 -1
- package/dist/announcement-bar/announcement-bar.d.ts +15 -4
- package/dist/announcement-bar/announcement-bar.js +31 -6
- package/dist/announcement-bar/announcement-bar.js.map +1 -1
- package/dist/avatar/avatar.d.ts +7 -0
- package/dist/avatar/avatar.js +33 -12
- package/dist/avatar/avatar.js.map +1 -1
- package/dist/badge/badge.d.ts +16 -1
- package/dist/badge/badge.js +12 -5
- package/dist/badge/badge.js.map +1 -1
- package/dist/bar-chart/bar-chart-series.d.ts +75 -0
- package/dist/bar-chart/bar-chart-series.js +401 -0
- package/dist/bar-chart/bar-chart-series.js.map +1 -0
- package/dist/bar-chart/bar-chart.d.ts +25 -1
- package/dist/bar-chart/bar-chart.js +26 -1
- package/dist/bar-chart/bar-chart.js.map +1 -1
- package/dist/bar-chart/index.d.ts +2 -1
- package/dist/button/button.d.ts +24 -2
- package/dist/button/button.js +62 -13
- package/dist/button/button.js.map +1 -1
- package/dist/card/card.js +1 -1
- package/dist/card/card.js.map +1 -1
- package/dist/checkbox/checkbox.d.ts +25 -1
- package/dist/checkbox/checkbox.js +103 -28
- package/dist/checkbox/checkbox.js.map +1 -1
- package/dist/checkbox/index.d.ts +2 -2
- package/dist/checkbox/index.js +2 -2
- package/dist/chip-group/chip-group.d.ts +11 -0
- package/dist/chip-group/chip-group.js +50 -34
- package/dist/chip-group/chip-group.js.map +1 -1
- package/dist/chip-group/index.d.ts +2 -2
- package/dist/code-block/code-block.d.ts +45 -0
- package/dist/code-block/code-block.js +74 -0
- package/dist/code-block/code-block.js.map +1 -0
- package/dist/code-block/index.d.ts +2 -0
- package/dist/code-block/index.js +2 -0
- package/dist/command-palette/command-palette.d.ts +128 -0
- package/dist/command-palette/command-palette.js +371 -0
- package/dist/command-palette/command-palette.js.map +1 -0
- package/dist/command-palette/index.d.ts +2 -0
- package/dist/command-palette/index.js +2 -0
- package/dist/copy-button/copy-button.d.ts +19 -3
- package/dist/copy-button/copy-button.js +7 -4
- package/dist/copy-button/copy-button.js.map +1 -1
- package/dist/dialog/dialog.js +1 -1
- package/dist/dialog/dialog.js.map +1 -1
- package/dist/empty-state/empty-state.d.ts +17 -0
- package/dist/empty-state/empty-state.js +9 -7
- package/dist/empty-state/empty-state.js.map +1 -1
- package/dist/file-drop-zone/file-drop-zone.d.ts +29 -3
- package/dist/file-drop-zone/file-drop-zone.js +26 -8
- package/dist/file-drop-zone/file-drop-zone.js.map +1 -1
- package/dist/gauge/gauge.d.ts +18 -1
- package/dist/gauge/gauge.js +35 -21
- package/dist/gauge/gauge.js.map +1 -1
- package/dist/gauge/index.d.ts +2 -2
- package/dist/index.d.ts +14 -9
- package/dist/index.js +14 -10
- package/dist/info-tooltip/index.d.ts +2 -0
- package/dist/info-tooltip/index.js +2 -0
- package/dist/info-tooltip/info-tooltip.d.ts +35 -0
- package/dist/info-tooltip/info-tooltip.js +49 -0
- package/dist/info-tooltip/info-tooltip.js.map +1 -0
- package/dist/input/input.d.ts +7 -0
- package/dist/input/input.js +4 -2
- package/dist/input/input.js.map +1 -1
- package/dist/lib/field-shell.js +2 -6
- package/dist/lib/field-shell.js.map +1 -1
- package/dist/lib/required-marker.js +20 -0
- package/dist/lib/required-marker.js.map +1 -0
- package/dist/lib/selection-control.js +32 -1
- package/dist/lib/selection-control.js.map +1 -1
- package/dist/line-chart/index.d.ts +2 -2
- package/dist/line-chart/line-chart.d.ts +106 -3
- package/dist/line-chart/line-chart.js +185 -46
- package/dist/line-chart/line-chart.js.map +1 -1
- package/dist/mention-input/mention-input.js +1 -1
- package/dist/money-input/index.d.ts +2 -2
- package/dist/money-input/money-input.d.ts +14 -0
- package/dist/money-input/money-input.js +17 -7
- package/dist/money-input/money-input.js.map +1 -1
- package/dist/morph-select/morph-select.d.ts +8 -1
- package/dist/morph-select/morph-select.js +8 -5
- package/dist/morph-select/morph-select.js.map +1 -1
- package/dist/multi-select/multi-select.d.ts +31 -0
- package/dist/multi-select/multi-select.js +123 -6
- package/dist/multi-select/multi-select.js.map +1 -1
- package/dist/number-field/number-field.js +1 -1
- package/dist/password-field/password-field.js +1 -1
- package/dist/password-strength/password-strength.js +1 -1
- package/dist/phone-input/phone-input.js +1 -1
- package/dist/popover/popover.js +1 -1
- package/dist/popover/popover.js.map +1 -1
- package/dist/progress/index.d.ts +2 -2
- package/dist/progress/index.js +2 -2
- package/dist/progress/progress.d.ts +53 -2
- package/dist/progress/progress.js +146 -36
- package/dist/progress/progress.js.map +1 -1
- package/dist/radio-group/radio-group.d.ts +12 -3
- package/dist/radio-group/radio-group.js +62 -52
- package/dist/radio-group/radio-group.js.map +1 -1
- package/dist/rich-text-editor/rich-text-editor.js +1 -1
- package/dist/scroll-area/scroll-area.d.ts +1 -1
- package/dist/scrub-input/scrub-input.d.ts +11 -1
- package/dist/scrub-input/scrub-input.js +22 -10
- package/dist/scrub-input/scrub-input.js.map +1 -1
- package/dist/search-field/search-field.js +1 -1
- package/dist/select/select.d.ts +10 -1
- package/dist/select/select.js +4 -3
- package/dist/select/select.js.map +1 -1
- package/dist/separator/separator.d.ts +1 -1
- package/dist/shortcut-recorder/shortcut-recorder.js +1 -1
- package/dist/sparkline/sparkline.d.ts +27 -2
- package/dist/sparkline/sparkline.js +115 -18
- package/dist/sparkline/sparkline.js.map +1 -1
- package/dist/stat-card/index.d.ts +2 -2
- package/dist/stat-card/index.js +2 -2
- package/dist/stat-card/stat-card.d.ts +32 -1
- package/dist/stat-card/stat-card.js +52 -9
- package/dist/stat-card/stat-card.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/table/index.d.ts +2 -2
- package/dist/table/index.js +2 -2
- package/dist/table/table.d.ts +211 -3
- package/dist/table/table.js +473 -93
- package/dist/table/table.js.map +1 -1
- package/dist/tabs/tabs.d.ts +4 -2
- package/dist/tabs/tabs.js +7 -5
- package/dist/tabs/tabs.js.map +1 -1
- package/dist/tag-input/tag-input.js +1 -1
- package/dist/text-shimmer/text-shimmer.d.ts +18 -2
- package/dist/text-shimmer/text-shimmer.js +5 -2
- package/dist/text-shimmer/text-shimmer.js.map +1 -1
- package/dist/textarea/textarea.d.ts +7 -0
- package/dist/textarea/textarea.js +4 -2
- package/dist/textarea/textarea.js.map +1 -1
- package/dist/toast/toast.d.ts +2 -2
- package/dist/toggle-button/index.d.ts +2 -0
- package/dist/toggle-button/index.js +2 -0
- package/dist/toggle-button/toggle-button.d.ts +93 -0
- package/dist/toggle-button/toggle-button.js +199 -0
- package/dist/toggle-button/toggle-button.js.map +1 -0
- package/package.json +19 -3
|
@@ -4,11 +4,26 @@ import { mergeRefs } from "../lib/merge-refs.js";
|
|
|
4
4
|
import { SwapText } from "../lib/swap-text.js";
|
|
5
5
|
import { useHeightFollow } from "../lib/height-follow.js";
|
|
6
6
|
import { useFlip } from "../lib/flip.js";
|
|
7
|
-
import { forwardRef, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { createElement, forwardRef, useMemo, useRef, useState } from "react";
|
|
8
8
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
9
9
|
//#region src/chip-group/chip-group.tsx
|
|
10
10
|
/** Roving-focus key of the overflow chip; the NUL keeps it from colliding with any option value. */
|
|
11
11
|
const MORE = "\0more";
|
|
12
|
+
/** Attributes the group owns; a chip's `attrs` can never set them. */
|
|
13
|
+
const OWN_ATTRS = /* @__PURE__ */ new Set([
|
|
14
|
+
"data-chip",
|
|
15
|
+
"data-slot",
|
|
16
|
+
"data-flip",
|
|
17
|
+
"data-more",
|
|
18
|
+
"data-selected"
|
|
19
|
+
]);
|
|
20
|
+
/** Keeps only `data-*` keys the group does not own, so a chip's attrs cannot break its styling or roving focus. */
|
|
21
|
+
function chipAttrs(attrs) {
|
|
22
|
+
if (!attrs) return void 0;
|
|
23
|
+
const kept = {};
|
|
24
|
+
for (const [key, value] of Object.entries(attrs)) if (key.startsWith("data-") && !OWN_ATTRS.has(key)) kept[key] = value;
|
|
25
|
+
return kept;
|
|
26
|
+
}
|
|
12
27
|
/** Stagger between chips revealed by the overflow (ms), at most 300 in all. */
|
|
13
28
|
const STAGGER_MS = 35;
|
|
14
29
|
const ChipGroup = forwardRef(function ChipGroup({ options, value, onValueChange, label, multiple = true, maxVisible = Infinity, moreLabel = (hidden) => `+${hidden} more`, lessLabel = "Show less", className }, ref) {
|
|
@@ -76,7 +91,9 @@ const ChipGroup = forwardRef(function ChipGroup({ options, value, onValueChange,
|
|
|
76
91
|
children: [visible.map((option) => {
|
|
77
92
|
const selected = value.includes(option.value);
|
|
78
93
|
const delay = revealedAt.get(option.value);
|
|
79
|
-
return /* @__PURE__ */
|
|
94
|
+
return /* @__PURE__ */ createElement("button", {
|
|
95
|
+
...chipAttrs(option.attrs),
|
|
96
|
+
key: option.value,
|
|
80
97
|
type: "button",
|
|
81
98
|
"data-flip": "",
|
|
82
99
|
"data-chip": option.value,
|
|
@@ -86,40 +103,39 @@ const ChipGroup = forwardRef(function ChipGroup({ options, value, onValueChange,
|
|
|
86
103
|
onClick: () => toggle(option.value),
|
|
87
104
|
onFocus: () => setActive(option.value),
|
|
88
105
|
style: delay === void 0 ? void 0 : { animationDelay: `${delay}ms` },
|
|
89
|
-
className: cn("chip", delay === void 0 ? "" : "motion-safe:animate-chip-reveal")
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
})
|
|
106
|
+
className: cn("chip", delay === void 0 ? "" : "motion-safe:animate-chip-reveal")
|
|
107
|
+
}, /* @__PURE__ */ jsxs("span", {
|
|
108
|
+
"data-selected": selected,
|
|
109
|
+
className: "chip-body",
|
|
110
|
+
children: [
|
|
111
|
+
/* @__PURE__ */ jsx("span", {
|
|
112
|
+
"aria-hidden": "true",
|
|
113
|
+
className: "chip-check",
|
|
114
|
+
children: /* @__PURE__ */ jsx("svg", {
|
|
115
|
+
width: 14,
|
|
116
|
+
height: 14,
|
|
117
|
+
viewBox: "0 0 24 24",
|
|
118
|
+
fill: "none",
|
|
119
|
+
stroke: "currentColor",
|
|
120
|
+
strokeWidth: 2.5,
|
|
121
|
+
strokeLinecap: "round",
|
|
122
|
+
strokeLinejoin: "round",
|
|
123
|
+
children: /* @__PURE__ */ jsx("path", {
|
|
124
|
+
d: "M4 12.5 9.5 18 20 6.5",
|
|
125
|
+
pathLength: 1
|
|
110
126
|
})
|
|
111
|
-
}),
|
|
112
|
-
/* @__PURE__ */ jsx("span", {
|
|
113
|
-
"aria-hidden": "true",
|
|
114
|
-
className: "chip-slot"
|
|
115
|
-
}),
|
|
116
|
-
/* @__PURE__ */ jsx("span", {
|
|
117
|
-
className: "truncate",
|
|
118
|
-
children: option.label
|
|
119
127
|
})
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
128
|
+
}),
|
|
129
|
+
/* @__PURE__ */ jsx("span", {
|
|
130
|
+
"aria-hidden": "true",
|
|
131
|
+
className: "chip-slot"
|
|
132
|
+
}),
|
|
133
|
+
/* @__PURE__ */ jsx("span", {
|
|
134
|
+
className: "truncate",
|
|
135
|
+
children: option.label
|
|
136
|
+
})
|
|
137
|
+
]
|
|
138
|
+
}));
|
|
123
139
|
}), showMore ? /* @__PURE__ */ jsx("button", {
|
|
124
140
|
type: "button",
|
|
125
141
|
"data-flip": "",
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"chip-group.js","names":[],"sources":["../../src/chip-group/chip-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useMemo, useRef, useState, type KeyboardEvent } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useFlip } from \"../lib/flip.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * ChipGroup: selectable filter chips for narrowing a list by facets people\n * toggle often, such as topics, statuses or tags.\n *\n * Selecting a chip grows a check in at its start (it draws itself as it\n * scales up from a soft blur), the label slides over, the chip widens on the\n * morph spring, and its neighbours glide to their new places, even across\n * lines. The row follows its own height. Long sets fold behind a \"+N more\"\n * chip; opening it grows the hidden chips in one after another. Arrow keys move\n * between chips, Space or Enter toggles, and each chip is a pressed or\n * unpressed button. Selected reads as a tint plus the check, never colour alone.\n */\nexport interface ChipOption {\n value: string;\n label: string;\n}\n\nexport interface ChipGroupProps {\n options: ChipOption[];\n value: string[];\n onValueChange: (value: string[]) => void;\n /** Accessible name of the group, such as \"Topics\". */\n label: string;\n /** Allow several chips at once. In single mode the selected chip can still be cleared. @default true */\n multiple?: boolean;\n /** Chips shown before the rest fold behind a \"+N more\" chip. Chips selected when it folds stay in view. */\n maxVisible?: number;\n /** Text of the overflow chip. @default (hidden) => `+${hidden} more` */\n moreLabel?: (hidden: number) => string;\n /** @default \"Show less\" */\n lessLabel?: string;\n /** Extra classes for the group. Merged last. */\n className?: string;\n}\n\n/** Roving-focus key of the overflow chip; the NUL keeps it from colliding with any option value. */\nconst MORE = \"\\u0000more\";\n/** Stagger between chips revealed by the overflow (ms), at most 300 in all. */\nconst STAGGER_MS = 35;\n\nexport const ChipGroup = forwardRef<HTMLDivElement, ChipGroupProps>(function ChipGroup(\n {\n options,\n value,\n onValueChange,\n label,\n multiple = true,\n maxVisible = Infinity,\n moreLabel = (hidden) => `+${hidden} more`,\n lessLabel = \"Show less\",\n className,\n },\n ref,\n) {\n const [expanded, setExpanded] = useState(false);\n // Chips selected when the overflow folds stay in view, so a selection is never hidden.\n const [pinned, setPinned] = useState<string[]>(value);\n const [active, setActive] = useState<string | null>(null);\n const foldable = options.length > maxVisible;\n const visible =\n !foldable || expanded\n ? options\n : options.filter(\n (option, index) => index < maxVisible || pinned.includes(option.value) || value.includes(option.value),\n );\n const hidden = options.length - visible.length;\n const showMore = foldable && (expanded || hidden > 0);\n const keys = [...visible.map((option) => option.value), ...(showMore ? [MORE] : [])];\n // One chip holds the tab stop: the last one focused, else the first selected, else the first.\n const tabStop =\n active !== null && keys.includes(active)\n ? active\n : (visible.find((option) => value.includes(option.value))?.value ?? keys[0]);\n\n const group = useRef<HTMLDivElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, group), [ref]);\n const frame = useRef<HTMLDivElement>(null);\n const morphKey = `${expanded}|${value.join(\",\")}`;\n // Neighbours glide to their new places when a chip widens, even across lines; the row follows its height.\n useFlip(group, morphKey, \"var(--duration-morph)\", \"var(--ease-morph)\");\n useHeightFollow(frame, group, morphKey);\n // Chips that appear after the first render grow in; the ones there from the start never animate.\n const seen = useRef(new Set(options.map((option) => option.value)));\n const revealedAt = new Map<string, number>();\n let rank = 0;\n for (const option of visible) {\n if (!seen.current.has(option.value)) revealedAt.set(option.value, Math.min(rank++ * STAGGER_MS, 300));\n }\n\n function toggle(next: string) {\n const on = value.includes(next);\n if (!multiple) {\n onValueChange(on ? [] : [next]);\n return;\n }\n onValueChange(\n options.filter((option) => (option.value === next ? !on : value.includes(option.value))).map((option) => option.value),\n );\n }\n\n function toggleMore() {\n if (expanded) setPinned(value);\n // Only the chips this opening reveals grow in; the ones already on screen stay put.\n else seen.current = new Set(visible.map((option) => option.value));\n setExpanded(!expanded);\n }\n\n function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n const buttons = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>(\"button[data-chip]\"));\n const index = buttons.indexOf(document.activeElement as HTMLButtonElement);\n if (index < 0) return;\n const last = buttons.length - 1;\n const moves: Record<string, number> = {\n ArrowRight: index === last ? 0 : index + 1,\n ArrowDown: index === last ? 0 : index + 1,\n ArrowLeft: index === 0 ? last : index - 1,\n ArrowUp: index === 0 ? last : index - 1,\n Home: 0,\n End: last,\n };\n if (!(event.key in moves)) return;\n event.preventDefault();\n buttons[moves[event.key]!]!.focus();\n }\n\n const moreText = expanded ? lessLabel : moreLabel(hidden);\n\n return (\n <div ref={frame} data-slot=\"chip-group-frame\" className=\"height-follow min-w-0\">\n <div\n ref={refs}\n role=\"group\"\n aria-label={label}\n data-slot=\"chip-group\"\n onKeyDown={onKeyDown}\n className={cn(\"relative isolate flex flex-wrap gap-2 box-border font-sans\", className)}\n >\n {visible.map((option) => {\n const selected = value.includes(option.value);\n const delay = revealedAt.get(option.value);\n return (\n <button\n key={option.value}\n type=\"button\"\n data-flip=\"\"\n data-chip={option.value}\n data-slot=\"chip\"\n aria-pressed={selected}\n tabIndex={tabStop === option.value ? 0 : -1}\n onClick={() => toggle(option.value)}\n onFocus={() => setActive(option.value)}\n style={delay === undefined ? undefined : ({ animationDelay: `${delay}ms` })}\n className={cn(\"chip\", delay === undefined ? \"\" : \"motion-safe:animate-chip-reveal\")}\n >\n <span data-selected={selected} className=\"chip-body\">\n <span aria-hidden=\"true\" className=\"chip-check\">\n <svg width={14} height={14} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M4 12.5 9.5 18 20 6.5\" pathLength={1} />\n </svg>\n </span>\n <span aria-hidden=\"true\" className=\"chip-slot\" />\n <span className=\"truncate\">{option.label}</span>\n </span>\n </button>\n );\n })}\n {showMore ? (\n <button\n key={MORE}\n type=\"button\"\n data-flip=\"\"\n data-chip={MORE}\n data-more=\"\"\n aria-expanded={expanded}\n tabIndex={tabStop === MORE ? 0 : -1}\n onClick={toggleMore}\n onFocus={() => setActive(MORE)}\n className=\"chip chip-more\"\n >\n <span className=\"chip-body tabular-nums\">\n <SwapText swapKey={moreText} motion=\"rise\">\n {moreText}\n </SwapText>\n </span>\n </button>\n ) : null}\n </div>\n </div>\n );\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"chip-group.js","names":[],"sources":["../../src/chip-group/chip-group.tsx"],"sourcesContent":["\"use client\";\n\nimport { forwardRef, useMemo, useRef, useState, type KeyboardEvent } from \"react\";\n\nimport { cn } from \"../lib/cn.js\";\nimport { useFlip } from \"../lib/flip.js\";\nimport { useHeightFollow } from \"../lib/height-follow.js\";\nimport { mergeRefs } from \"../lib/merge-refs.js\";\nimport { SwapText } from \"../lib/swap-text.js\";\n\n/**\n * ChipGroup: selectable filter chips for narrowing a list by facets people\n * toggle often, such as topics, statuses or tags.\n *\n * Selecting a chip grows a check in at its start (it draws itself as it\n * scales up from a soft blur), the label slides over, the chip widens on the\n * morph spring, and its neighbours glide to their new places, even across\n * lines. The row follows its own height. Long sets fold behind a \"+N more\"\n * chip; opening it grows the hidden chips in one after another. Arrow keys move\n * between chips, Space or Enter toggles, and each chip is a pressed or\n * unpressed button. Selected reads as a tint plus the check, never colour alone.\n */\nexport interface ChipOption {\n value: string;\n label: string;\n /**\n * Extra `data-*` attributes for this chip's button, such as\n * `{ \"data-testid\": \"goal-market-IN\" }`, so tests and analytics can address\n * one chip. They never override the group's own `data-chip`, `data-slot`,\n * `data-flip`, `data-more` or `data-selected`; those keys are dropped.\n */\n attrs?: ChipAttrs;\n}\n\n/** `data-*` attributes passed through to one chip's button. */\nexport type ChipAttrs = { [key: `data-${string}`]: string | undefined };\n\nexport interface ChipGroupProps {\n options: ChipOption[];\n value: string[];\n onValueChange: (value: string[]) => void;\n /** Accessible name of the group, such as \"Topics\". */\n label: string;\n /** Allow several chips at once. In single mode the selected chip can still be cleared. @default true */\n multiple?: boolean;\n /** Chips shown before the rest fold behind a \"+N more\" chip. Chips selected when it folds stay in view. */\n maxVisible?: number;\n /** Text of the overflow chip. @default (hidden) => `+${hidden} more` */\n moreLabel?: (hidden: number) => string;\n /** @default \"Show less\" */\n lessLabel?: string;\n /** Extra classes for the group. Merged last. */\n className?: string;\n}\n\n/** Roving-focus key of the overflow chip; the NUL keeps it from colliding with any option value. */\nconst MORE = \"\\u0000more\";\n/** Attributes the group owns; a chip's `attrs` can never set them. */\nconst OWN_ATTRS = new Set([\"data-chip\", \"data-slot\", \"data-flip\", \"data-more\", \"data-selected\"]);\n\n/** Keeps only `data-*` keys the group does not own, so a chip's attrs cannot break its styling or roving focus. */\nfunction chipAttrs(attrs: ChipAttrs | undefined): ChipAttrs | undefined {\n if (!attrs) return undefined;\n const kept: ChipAttrs = {};\n for (const [key, value] of Object.entries(attrs)) {\n if (key.startsWith(\"data-\") && !OWN_ATTRS.has(key)) kept[key as `data-${string}`] = value;\n }\n return kept;\n}\n\n/** Stagger between chips revealed by the overflow (ms), at most 300 in all. */\nconst STAGGER_MS = 35;\n\nexport const ChipGroup = forwardRef<HTMLDivElement, ChipGroupProps>(function ChipGroup(\n {\n options,\n value,\n onValueChange,\n label,\n multiple = true,\n maxVisible = Infinity,\n moreLabel = (hidden) => `+${hidden} more`,\n lessLabel = \"Show less\",\n className,\n },\n ref,\n) {\n const [expanded, setExpanded] = useState(false);\n // Chips selected when the overflow folds stay in view, so a selection is never hidden.\n const [pinned, setPinned] = useState<string[]>(value);\n const [active, setActive] = useState<string | null>(null);\n const foldable = options.length > maxVisible;\n const visible =\n !foldable || expanded\n ? options\n : options.filter(\n (option, index) => index < maxVisible || pinned.includes(option.value) || value.includes(option.value),\n );\n const hidden = options.length - visible.length;\n const showMore = foldable && (expanded || hidden > 0);\n const keys = [...visible.map((option) => option.value), ...(showMore ? [MORE] : [])];\n // One chip holds the tab stop: the last one focused, else the first selected, else the first.\n const tabStop =\n active !== null && keys.includes(active)\n ? active\n : (visible.find((option) => value.includes(option.value))?.value ?? keys[0]);\n\n const group = useRef<HTMLDivElement | null>(null);\n const refs = useMemo(() => mergeRefs(ref, group), [ref]);\n const frame = useRef<HTMLDivElement>(null);\n const morphKey = `${expanded}|${value.join(\",\")}`;\n // Neighbours glide to their new places when a chip widens, even across lines; the row follows its height.\n useFlip(group, morphKey, \"var(--duration-morph)\", \"var(--ease-morph)\");\n useHeightFollow(frame, group, morphKey);\n // Chips that appear after the first render grow in; the ones there from the start never animate.\n const seen = useRef(new Set(options.map((option) => option.value)));\n const revealedAt = new Map<string, number>();\n let rank = 0;\n for (const option of visible) {\n if (!seen.current.has(option.value)) revealedAt.set(option.value, Math.min(rank++ * STAGGER_MS, 300));\n }\n\n function toggle(next: string) {\n const on = value.includes(next);\n if (!multiple) {\n onValueChange(on ? [] : [next]);\n return;\n }\n onValueChange(\n options.filter((option) => (option.value === next ? !on : value.includes(option.value))).map((option) => option.value),\n );\n }\n\n function toggleMore() {\n if (expanded) setPinned(value);\n // Only the chips this opening reveals grow in; the ones already on screen stay put.\n else seen.current = new Set(visible.map((option) => option.value));\n setExpanded(!expanded);\n }\n\n function onKeyDown(event: KeyboardEvent<HTMLDivElement>) {\n const buttons = Array.from(event.currentTarget.querySelectorAll<HTMLButtonElement>(\"button[data-chip]\"));\n const index = buttons.indexOf(document.activeElement as HTMLButtonElement);\n if (index < 0) return;\n const last = buttons.length - 1;\n const moves: Record<string, number> = {\n ArrowRight: index === last ? 0 : index + 1,\n ArrowDown: index === last ? 0 : index + 1,\n ArrowLeft: index === 0 ? last : index - 1,\n ArrowUp: index === 0 ? last : index - 1,\n Home: 0,\n End: last,\n };\n if (!(event.key in moves)) return;\n event.preventDefault();\n buttons[moves[event.key]!]!.focus();\n }\n\n const moreText = expanded ? lessLabel : moreLabel(hidden);\n\n return (\n <div ref={frame} data-slot=\"chip-group-frame\" className=\"height-follow min-w-0\">\n <div\n ref={refs}\n role=\"group\"\n aria-label={label}\n data-slot=\"chip-group\"\n onKeyDown={onKeyDown}\n className={cn(\"relative isolate flex flex-wrap gap-2 box-border font-sans\", className)}\n >\n {visible.map((option) => {\n const selected = value.includes(option.value);\n const delay = revealedAt.get(option.value);\n return (\n <button\n {...chipAttrs(option.attrs)}\n key={option.value}\n type=\"button\"\n data-flip=\"\"\n data-chip={option.value}\n data-slot=\"chip\"\n aria-pressed={selected}\n tabIndex={tabStop === option.value ? 0 : -1}\n onClick={() => toggle(option.value)}\n onFocus={() => setActive(option.value)}\n style={delay === undefined ? undefined : ({ animationDelay: `${delay}ms` })}\n className={cn(\"chip\", delay === undefined ? \"\" : \"motion-safe:animate-chip-reveal\")}\n >\n <span data-selected={selected} className=\"chip-body\">\n <span aria-hidden=\"true\" className=\"chip-check\">\n <svg width={14} height={14} viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" strokeWidth={2.5} strokeLinecap=\"round\" strokeLinejoin=\"round\">\n <path d=\"M4 12.5 9.5 18 20 6.5\" pathLength={1} />\n </svg>\n </span>\n <span aria-hidden=\"true\" className=\"chip-slot\" />\n <span className=\"truncate\">{option.label}</span>\n </span>\n </button>\n );\n })}\n {showMore ? (\n <button\n key={MORE}\n type=\"button\"\n data-flip=\"\"\n data-chip={MORE}\n data-more=\"\"\n aria-expanded={expanded}\n tabIndex={tabStop === MORE ? 0 : -1}\n onClick={toggleMore}\n onFocus={() => setActive(MORE)}\n className=\"chip chip-more\"\n >\n <span className=\"chip-body tabular-nums\">\n <SwapText swapKey={moreText} motion=\"rise\">\n {moreText}\n </SwapText>\n </span>\n </button>\n ) : null}\n </div>\n </div>\n );\n});\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { ChipGroup, ChipGroupProps, ChipOption } from "./chip-group.js";
|
|
2
|
-
export { ChipGroup, type ChipGroupProps, type ChipOption };
|
|
1
|
+
import { ChipAttrs, ChipGroup, ChipGroupProps, ChipOption } from "./chip-group.js";
|
|
2
|
+
export { type ChipAttrs, ChipGroup, type ChipGroupProps, type ChipOption };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { ComponentPropsWithoutRef, ReactNode } from "react";
|
|
2
|
+
//#region src/code-block/code-block.d.ts
|
|
3
|
+
export declare const codeBlockVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
4
|
+
export interface CodeBlockProps extends Omit<ComponentPropsWithoutRef<"div">, "children"> {
|
|
5
|
+
/** The code. It is what is shown (unless `children` is given) and what goes on the clipboard. */
|
|
6
|
+
value: string;
|
|
7
|
+
/** Show this instead of `value` (for highlighted markup). `value` is still what is copied. */
|
|
8
|
+
children?: ReactNode;
|
|
9
|
+
/**
|
|
10
|
+
* Break long lines instead of scrolling sideways. Off by default: a line of code
|
|
11
|
+
* scrolls, and the scroll area takes keyboard focus (only while it actually
|
|
12
|
+
* overflows) so it can be read.
|
|
13
|
+
*
|
|
14
|
+
* @default false
|
|
15
|
+
*/
|
|
16
|
+
wrap?: boolean;
|
|
17
|
+
/** The visible label of the copy button. @default "Copy" */
|
|
18
|
+
copyLabel?: string;
|
|
19
|
+
/** The copy button's accessible name; keep `copyLabel` inside it. @default "Copy code" */
|
|
20
|
+
copyAriaLabel?: string;
|
|
21
|
+
/** Only the icon on the copy button. @default false */
|
|
22
|
+
iconOnly?: boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Names the code group for assistive technology. It is a `group`, not a landmark,
|
|
25
|
+
* so several blocks on one page do not each add a region to the landmark list.
|
|
26
|
+
* Give each a distinct label when it helps ("Install command").
|
|
27
|
+
*
|
|
28
|
+
* @default "Code"
|
|
29
|
+
*/
|
|
30
|
+
codeLabel?: string;
|
|
31
|
+
/** Called once the code has reached the clipboard. */
|
|
32
|
+
onCopied?: () => void;
|
|
33
|
+
/** Called when the clipboard refused the text. */
|
|
34
|
+
onCopyError?: (error: unknown) => void;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* A snippet of code with a copy button, built on CopyButton (which owns the
|
|
38
|
+
* copy, its feedback and its live-region announcement).
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* <CodeBlock value="npm install @seekalfred/primitives" onCopied={() => toast("Copied")} />
|
|
42
|
+
*/
|
|
43
|
+
export declare const CodeBlock: import("react").ForwardRefExoticComponent<CodeBlockProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
44
|
+
//#endregion
|
|
45
|
+
//# sourceMappingURL=code-block.d.ts.map
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn, cnAll } from "../lib/cn.js";
|
|
3
|
+
import { CopyButton } from "../copy-button/copy-button.js";
|
|
4
|
+
import { cva } from "class-variance-authority";
|
|
5
|
+
import { forwardRef, useEffect, useRef, useState } from "react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/code-block/code-block.tsx
|
|
8
|
+
const codeBlockVariants = cva([
|
|
9
|
+
"flex items-start justify-between gap-2",
|
|
10
|
+
"rounded-control bg-muted px-3 py-2 box-border",
|
|
11
|
+
"font-sans text-foreground"
|
|
12
|
+
]);
|
|
13
|
+
/**
|
|
14
|
+
* A snippet of code with a copy button, built on CopyButton (which owns the
|
|
15
|
+
* copy, its feedback and its live-region announcement).
|
|
16
|
+
*
|
|
17
|
+
* @example
|
|
18
|
+
* <CodeBlock value="npm install @seekalfred/primitives" onCopied={() => toast("Copied")} />
|
|
19
|
+
*/
|
|
20
|
+
const CodeBlock = forwardRef(function CodeBlock({ value, children, wrap = false, copyLabel = "Copy", copyAriaLabel = "Copy code", iconOnly = false, codeLabel = "Code", onCopied, onCopyError, className, ...props }, ref) {
|
|
21
|
+
const preRef = useRef(null);
|
|
22
|
+
const [scrolls, setScrolls] = useState(false);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const pre = preRef.current;
|
|
25
|
+
if (wrap || !pre) {
|
|
26
|
+
setScrolls(false);
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
const measure = () => setScrolls(pre.scrollWidth > pre.clientWidth);
|
|
30
|
+
measure();
|
|
31
|
+
if (typeof ResizeObserver === "undefined") return;
|
|
32
|
+
const observer = new ResizeObserver(measure);
|
|
33
|
+
observer.observe(pre);
|
|
34
|
+
if (pre.firstElementChild) observer.observe(pre.firstElementChild);
|
|
35
|
+
return () => observer.disconnect();
|
|
36
|
+
}, [
|
|
37
|
+
wrap,
|
|
38
|
+
value,
|
|
39
|
+
children
|
|
40
|
+
]);
|
|
41
|
+
return /* @__PURE__ */ jsxs("div", {
|
|
42
|
+
ref,
|
|
43
|
+
...props,
|
|
44
|
+
"data-slot": "code-block",
|
|
45
|
+
"data-wrap": wrap || void 0,
|
|
46
|
+
className: cn(codeBlockVariants(), className),
|
|
47
|
+
children: [/* @__PURE__ */ jsx("pre", {
|
|
48
|
+
ref: preRef,
|
|
49
|
+
"data-slot": "code-block-pre",
|
|
50
|
+
...wrap ? {} : {
|
|
51
|
+
role: "group",
|
|
52
|
+
"aria-label": codeLabel,
|
|
53
|
+
tabIndex: scrolls ? 0 : void 0
|
|
54
|
+
},
|
|
55
|
+
className: cnAll("m-0 min-w-0 flex-1 font-mono text-sm leading-(--leading-normal)", wrap ? "whitespace-pre-wrap break-all" : "overflow-x-auto outline-none focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-(--color-ring)"),
|
|
56
|
+
children: /* @__PURE__ */ jsx("code", {
|
|
57
|
+
"data-slot": "code-block-code",
|
|
58
|
+
children: children ?? value
|
|
59
|
+
})
|
|
60
|
+
}), /* @__PURE__ */ jsx(CopyButton, {
|
|
61
|
+
value,
|
|
62
|
+
variant: "plain",
|
|
63
|
+
iconOnly,
|
|
64
|
+
"aria-label": copyAriaLabel,
|
|
65
|
+
onCopied,
|
|
66
|
+
onError: onCopyError,
|
|
67
|
+
children: copyLabel
|
|
68
|
+
})]
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
//#endregion
|
|
72
|
+
export { CodeBlock, codeBlockVariants };
|
|
73
|
+
|
|
74
|
+
//# sourceMappingURL=code-block.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"code-block.js","names":[],"sources":["../../src/code-block/code-block.tsx"],"sourcesContent":["\"use client\";\n\nimport { cva } from \"class-variance-authority\";\nimport { forwardRef, useEffect, useRef, useState, type ComponentPropsWithoutRef, type ReactNode } from \"react\";\n\nimport { CopyButton } from \"../copy-button/copy-button.js\";\nimport { cn, cnAll } from \"../lib/cn.js\";\n\nexport const codeBlockVariants = cva([\n \"flex items-start justify-between gap-2\",\n \"rounded-control bg-muted px-3 py-2 box-border\",\n \"font-sans text-foreground\",\n]);\n\nexport interface CodeBlockProps extends Omit<ComponentPropsWithoutRef<\"div\">, \"children\"> {\n /** The code. It is what is shown (unless `children` is given) and what goes on the clipboard. */\n value: string;\n /** Show this instead of `value` (for highlighted markup). `value` is still what is copied. */\n children?: ReactNode;\n /**\n * Break long lines instead of scrolling sideways. Off by default: a line of code\n * scrolls, and the scroll area takes keyboard focus (only while it actually\n * overflows) so it can be read.\n *\n * @default false\n */\n wrap?: boolean;\n /** The visible label of the copy button. @default \"Copy\" */\n copyLabel?: string;\n /** The copy button's accessible name; keep `copyLabel` inside it. @default \"Copy code\" */\n copyAriaLabel?: string;\n /** Only the icon on the copy button. @default false */\n iconOnly?: boolean;\n /**\n * Names the code group for assistive technology. It is a `group`, not a landmark,\n * so several blocks on one page do not each add a region to the landmark list.\n * Give each a distinct label when it helps (\"Install command\").\n *\n * @default \"Code\"\n */\n codeLabel?: string;\n /** Called once the code has reached the clipboard. */\n onCopied?: () => void;\n /** Called when the clipboard refused the text. */\n onCopyError?: (error: unknown) => void;\n}\n\n/**\n * A snippet of code with a copy button, built on CopyButton (which owns the\n * copy, its feedback and its live-region announcement).\n *\n * @example\n * <CodeBlock value=\"npm install @seekalfred/primitives\" onCopied={() => toast(\"Copied\")} />\n */\nexport const CodeBlock = forwardRef<HTMLDivElement, CodeBlockProps>(function CodeBlock(\n {\n value,\n children,\n wrap = false,\n copyLabel = \"Copy\",\n copyAriaLabel = \"Copy code\",\n iconOnly = false,\n codeLabel = \"Code\",\n onCopied,\n onCopyError,\n className,\n ...props\n },\n ref,\n) {\n const preRef = useRef<HTMLPreElement>(null);\n const [scrolls, setScrolls] = useState(false);\n\n // A scroll area must be a tab stop (axe: scrollable-region-focusable) but a snippet\n // that fits is not one, so a page of short snippets adds no empty tab stops.\n useEffect(() => {\n const pre = preRef.current;\n if (wrap || !pre) {\n setScrolls(false);\n return;\n }\n const measure = () => setScrolls(pre.scrollWidth > pre.clientWidth);\n measure();\n if (typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver(measure);\n // The pre's own box changes with its container; the code's with its content.\n observer.observe(pre);\n if (pre.firstElementChild) observer.observe(pre.firstElementChild);\n return () => observer.disconnect();\n }, [wrap, value, children]);\n\n return (\n <div\n ref={ref}\n {...props}\n data-slot=\"code-block\"\n data-wrap={wrap || undefined}\n className={cn(codeBlockVariants(), className)}\n >\n <pre\n ref={preRef}\n data-slot=\"code-block-pre\"\n // Scrolling code must be reachable by keyboard; code that fits or wraps never needs to be.\n {...(wrap ? {} : { role: \"group\", \"aria-label\": codeLabel, tabIndex: scrolls ? 0 : undefined })}\n className={cnAll(\n \"m-0 min-w-0 flex-1 font-mono text-sm leading-(--leading-normal)\",\n wrap\n ? \"whitespace-pre-wrap break-all\"\n : \"overflow-x-auto outline-none focus-visible:outline-solid focus-visible:outline-(length:--focus-ring-width) focus-visible:outline-(--color-ring)\",\n )}\n >\n <code data-slot=\"code-block-code\">{children ?? value}</code>\n </pre>\n <CopyButton\n value={value}\n variant=\"plain\"\n iconOnly={iconOnly}\n aria-label={copyAriaLabel}\n onCopied={onCopied}\n onError={onCopyError}\n >\n {copyLabel}\n </CopyButton>\n </div>\n );\n});\n"],"mappings":";;;;;;;AAQA,MAAa,oBAAoB,IAAI;CACnC;CACA;CACA;AACF,CAAC;;;;;;;;AA0CD,MAAa,YAAY,WAA2C,SAAS,UAC3E,EACE,OACA,UACA,OAAO,OACP,YAAY,QACZ,gBAAgB,aAChB,WAAW,OACX,YAAY,QACZ,UACA,aACA,WACA,GAAG,SAEL,KACA;CACA,MAAM,SAAS,OAAuB,IAAI;CAC1C,MAAM,CAAC,SAAS,cAAc,SAAS,KAAK;CAI5C,gBAAgB;EACd,MAAM,MAAM,OAAO;EACnB,IAAI,QAAQ,CAAC,KAAK;GAChB,WAAW,KAAK;GAChB;EACF;EACA,MAAM,gBAAgB,WAAW,IAAI,cAAc,IAAI,WAAW;EAClE,QAAQ;EACR,IAAI,OAAO,mBAAmB,aAAa;EAC3C,MAAM,WAAW,IAAI,eAAe,OAAO;EAE3C,SAAS,QAAQ,GAAG;EACpB,IAAI,IAAI,mBAAmB,SAAS,QAAQ,IAAI,iBAAiB;EACjE,aAAa,SAAS,WAAW;CACnC,GAAG;EAAC;EAAM;EAAO;CAAQ,CAAC;CAE1B,OACE,qBAAC,OAAD;EACO;EACL,GAAI;EACJ,aAAU;EACV,aAAW,QAAQ,KAAA;EACnB,WAAW,GAAG,kBAAkB,GAAG,SAAS;EAL9C,UAAA,CAOE,oBAAC,OAAD;GACE,KAAK;GACL,aAAU;GAEV,GAAK,OAAO,CAAC,IAAI;IAAE,MAAM;IAAS,cAAc;IAAW,UAAU,UAAU,IAAI,KAAA;GAAU;GAC7F,WAAW,MACT,mEACA,OACI,kCACA,iJACN;GAEA,UAAA,oBAAC,QAAD;IAAM,aAAU;IAAmB,UAAA,YAAY;GAAY,CAAA;EACxD,CAAA,GACL,oBAAC,YAAD;GACS;GACP,SAAQ;GACE;GACV,cAAY;GACF;GACV,SAAS;GAER,UAAA;EACS,CAAA,CACT;;AAET,CAAC"}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/command-palette/command-palette.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* CommandPalette: a modal search-and-run list opened with Cmd+K or Ctrl+K.
|
|
5
|
+
*
|
|
6
|
+
* Built on the existing Dialog (React Aria's modal: focus trap, focus return, Escape, scrim,
|
|
7
|
+
* entrance motion that already honours prefers-reduced-motion), Input and ShortcutKeys, so it adds
|
|
8
|
+
* only the combobox + listbox wiring.
|
|
9
|
+
*
|
|
10
|
+
* The pattern is the WAI-ARIA "combobox with listbox popup": DOM focus never leaves the input,
|
|
11
|
+
* and the highlighted option is named by `aria-activedescendant`. Options are not tabbable.
|
|
12
|
+
*
|
|
13
|
+
* Every string in the cva blocks is a STATIC literal (Tailwind scans source text).
|
|
14
|
+
*/
|
|
15
|
+
/** One entry in the palette. */
|
|
16
|
+
export interface CommandPaletteItem {
|
|
17
|
+
/**
|
|
18
|
+
* Stable identity. Unique within `items`. A `recent` entry is a copy of an item, so it uses
|
|
19
|
+
* that item's `id` and `recent` need not be unique against `items`; only each list on its own.
|
|
20
|
+
*/
|
|
21
|
+
id: string;
|
|
22
|
+
/** The text shown, and the main thing the query is matched against. */
|
|
23
|
+
label: string;
|
|
24
|
+
/** The heading it is listed under. Items without one share an unlabelled group. */
|
|
25
|
+
group?: string;
|
|
26
|
+
/** A decorative glyph before the label. Mark it `aria-hidden`; it is sized by the palette. */
|
|
27
|
+
icon?: ReactNode;
|
|
28
|
+
/** A key combination shown as key caps, such as `"mod+shift+p"`. A hint only: it binds nothing. */
|
|
29
|
+
shortcut?: string;
|
|
30
|
+
/** Plain text at the trailing edge, such as "Go to" or "Run". Shown when there is no `shortcut`. */
|
|
31
|
+
hint?: string;
|
|
32
|
+
/** Extra words that also match the query but are not shown. */
|
|
33
|
+
keywords?: readonly string[];
|
|
34
|
+
/** Listed but skipped by the arrow keys and ignored when pressed. @default false */
|
|
35
|
+
isDisabled?: boolean;
|
|
36
|
+
}
|
|
37
|
+
export declare const commandPaletteListVariants: (props?: import("class-variance-authority/types").ClassProp | undefined) => string;
|
|
38
|
+
export declare const commandPaletteOptionVariants: (props?: ({} & import("class-variance-authority/types").ClassProp) | undefined) => string;
|
|
39
|
+
export interface CommandPaletteShortcutOptions {
|
|
40
|
+
/**
|
|
41
|
+
* The chord. `mod` matches Command or Control on any platform, so the default opens on both
|
|
42
|
+
* Cmd+K and Ctrl+K. Other modifiers: `ctrl`, `meta`, `alt`, `shift`. Modifiers you do not name
|
|
43
|
+
* must not be held.
|
|
44
|
+
*
|
|
45
|
+
* The last part is the key. Supported: a letter (`k`), a digit (`1`, also with `shift`), `space`,
|
|
46
|
+
* and any other name equal to `KeyboardEvent.key` such as `escape`, `enter`, `/`. A letter is
|
|
47
|
+
* read from what the key types, so it follows Dvorak and similar layouts. On a non-Latin layout
|
|
48
|
+
* it falls back to the physical key.
|
|
49
|
+
* @default "mod+k"
|
|
50
|
+
*/
|
|
51
|
+
shortcut?: string;
|
|
52
|
+
/** Turn the listener off. @default true */
|
|
53
|
+
enabled?: boolean;
|
|
54
|
+
}
|
|
55
|
+
type KeyLike = Pick<KeyboardEvent, "key" | "code" | "ctrlKey" | "metaKey" | "altKey" | "shiftKey">;
|
|
56
|
+
/** True when a keydown is this chord. `mod` is either Command or Control (not both required). */
|
|
57
|
+
export declare function matchesCommandPaletteShortcut(event: KeyLike, shortcut: string): boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Calls `onTrigger` when the chord is pressed anywhere in the document, and prevents the
|
|
60
|
+
* browser's own use of it (Ctrl+K focuses the address bar in some browsers).
|
|
61
|
+
*
|
|
62
|
+
* `CommandPalette` installs this itself, so use the hook only to open a palette from a
|
|
63
|
+
* different chord than the one the palette listens for, or to drive one you render conditionally.
|
|
64
|
+
*/
|
|
65
|
+
export declare function useCommandPaletteShortcut(onTrigger: () => void, { shortcut, enabled }?: CommandPaletteShortcutOptions): void;
|
|
66
|
+
export interface CommandPaletteProps {
|
|
67
|
+
/** Everything that can be run or opened. */
|
|
68
|
+
items: readonly CommandPaletteItem[];
|
|
69
|
+
/**
|
|
70
|
+
* A group of recent or suggested items, listed first while the search box is empty. It
|
|
71
|
+
* disappears once the user types, when the search runs over `items` alone.
|
|
72
|
+
*/
|
|
73
|
+
recent?: readonly CommandPaletteItem[];
|
|
74
|
+
/** Heading of the `recent` group. @default "Recent" */
|
|
75
|
+
recentLabel?: string;
|
|
76
|
+
/** Called with the chosen item, after the palette has asked to close. */
|
|
77
|
+
onSelect?: (item: CommandPaletteItem) => void;
|
|
78
|
+
/** Whether it is open. Pass with `onOpenChange` to control it. */
|
|
79
|
+
isOpen?: boolean;
|
|
80
|
+
/** Whether it starts open when uncontrolled. @default false */
|
|
81
|
+
defaultOpen?: boolean;
|
|
82
|
+
/** Called when it wants to open or close: the shortcut, Escape, the scrim, a selection. */
|
|
83
|
+
onOpenChange?: (isOpen: boolean) => void;
|
|
84
|
+
/**
|
|
85
|
+
* The chord that toggles it, installed on the document while the component is mounted.
|
|
86
|
+
* `false` removes the listener, for an app that binds it elsewhere (see
|
|
87
|
+
* `useCommandPaletteShortcut`). `mod` is Command or Control.
|
|
88
|
+
* @default "mod+k"
|
|
89
|
+
*/
|
|
90
|
+
shortcut?: string | false;
|
|
91
|
+
/** The dialog's accessible name. A prop so it can be translated. @default "Search and commands" */
|
|
92
|
+
label?: string;
|
|
93
|
+
/** Accessible name of the search box. @default "Search" */
|
|
94
|
+
searchLabel?: string;
|
|
95
|
+
/** Placeholder of the search box. @default "Search pages and actions" */
|
|
96
|
+
placeholder?: string;
|
|
97
|
+
/** Replaces the default filter. Receives the item and the trimmed query; the query is never empty. */
|
|
98
|
+
filter?: (item: CommandPaletteItem, query: string) => boolean;
|
|
99
|
+
/** What to show when nothing matches. @default ``No matches for “query”.`` */
|
|
100
|
+
emptyMessage?: ReactNode | ((query: string) => ReactNode);
|
|
101
|
+
/** The polite result-count announcement. @default "1 result" / "n results" / "No results" */
|
|
102
|
+
resultsLabel?: (count: number) => string;
|
|
103
|
+
/**
|
|
104
|
+
* The strip of key hints under the list. `undefined` shows the default ("↑ ↓ to move",
|
|
105
|
+
* "Enter to open"); `null` removes it; anything else replaces it, which is how to translate it.
|
|
106
|
+
*/
|
|
107
|
+
footer?: ReactNode;
|
|
108
|
+
/** Extra classes for the panel. Merged last. */
|
|
109
|
+
className?: string;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* A command palette.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* const [open, setOpen] = useState(false);
|
|
116
|
+
* <CommandPalette
|
|
117
|
+
* items={[
|
|
118
|
+
* { id: "home", group: "Pages", label: "Home", hint: "Go to" },
|
|
119
|
+
* { id: "theme", group: "Actions", label: "Switch theme", keywords: ["dark"], shortcut: "mod+shift+l" },
|
|
120
|
+
* ]}
|
|
121
|
+
* isOpen={open}
|
|
122
|
+
* onOpenChange={setOpen}
|
|
123
|
+
* onSelect={(item) => run(item.id)}
|
|
124
|
+
* />
|
|
125
|
+
*/
|
|
126
|
+
export declare function CommandPalette({ items, recent, recentLabel, onSelect, isOpen: isOpenProp, defaultOpen, onOpenChange, shortcut, label, searchLabel, placeholder, filter, emptyMessage, resultsLabel, footer, className }: CommandPaletteProps): import("react").JSX.Element;
|
|
127
|
+
//#endregion
|
|
128
|
+
//# sourceMappingURL=command-palette.d.ts.map
|