@twoux/ui 0.0.0-stage → 0.1.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/LICENSE.md +61 -0
- package/LICENSES/Apache-2.0.txt +202 -0
- package/NOTICE.md +64 -0
- package/SHADCN-DIFF.md +228 -0
- package/agent/AGENTS.md +358 -0
- package/agent/docs-md/accessibility.md +49 -0
- package/agent/docs-md/action-catalogue.md +81 -0
- package/agent/docs-md/actions.md +74 -0
- package/agent/docs-md/agent-activity-log.md +70 -0
- package/agent/docs-md/agent-permissions.md +82 -0
- package/agent/docs-md/agent-run.md +90 -0
- package/agent/docs-md/agents-on-behalf.md +74 -0
- package/agent/docs-md/ai-column.md +85 -0
- package/agent/docs-md/ai-origin-label.md +70 -0
- package/agent/docs-md/announcing-features.md +73 -0
- package/agent/docs-md/answer-actions.md +81 -0
- package/agent/docs-md/app-shell.md +114 -0
- package/agent/docs-md/artifact-card.md +89 -0
- package/agent/docs-md/ask-on-screen.md +72 -0
- package/agent/docs-md/assistant-page.md +100 -0
- package/agent/docs-md/assistant-panel.md +142 -0
- package/agent/docs-md/avatar.md +82 -0
- package/agent/docs-md/banner.md +106 -0
- package/agent/docs-md/board.md +163 -0
- package/agent/docs-md/breadcrumb.md +89 -0
- package/agent/docs-md/breakdown-table.md +97 -0
- package/agent/docs-md/bulk-action-bar.md +112 -0
- package/agent/docs-md/button.md +293 -0
- package/agent/docs-md/card.md +115 -0
- package/agent/docs-md/change-list.md +99 -0
- package/agent/docs-md/change-request.md +166 -0
- package/agent/docs-md/chart-area.md +87 -0
- package/agent/docs-md/chart-bar.md +100 -0
- package/agent/docs-md/chart-box-plot.md +88 -0
- package/agent/docs-md/chart-bullet.md +89 -0
- package/agent/docs-md/chart-candlestick.md +112 -0
- package/agent/docs-md/chart-card-line.md +120 -0
- package/agent/docs-md/chart-donut.md +90 -0
- package/agent/docs-md/chart-drill-down.md +72 -0
- package/agent/docs-md/chart-funnel.md +71 -0
- package/agent/docs-md/chart-heatmap.md +76 -0
- package/agent/docs-md/chart-histogram.md +81 -0
- package/agent/docs-md/chart-map.md +165 -0
- package/agent/docs-md/chart-mixed.md +88 -0
- package/agent/docs-md/chart-price.md +92 -0
- package/agent/docs-md/chart-radial-bars.md +109 -0
- package/agent/docs-md/chart-radial-lines.md +111 -0
- package/agent/docs-md/chart-sankey.md +104 -0
- package/agent/docs-md/chart-scatter.md +87 -0
- package/agent/docs-md/chart-share-bar.md +89 -0
- package/agent/docs-md/chart-treemap.md +98 -0
- package/agent/docs-md/chart-waterfall.md +93 -0
- package/agent/docs-md/checkbox.md +131 -0
- package/agent/docs-md/choosing-selection-control.md +71 -0
- package/agent/docs-md/citations-sources.md +79 -0
- package/agent/docs-md/citing-sources.md +63 -0
- package/agent/docs-md/clarify-before-acting.md +69 -0
- package/agent/docs-md/clarifying-question.md +92 -0
- package/agent/docs-md/code-block.md +174 -0
- package/agent/docs-md/colours.md +87 -0
- package/agent/docs-md/columns-menu.md +140 -0
- package/agent/docs-md/combobox.md +161 -0
- package/agent/docs-md/command-palette.md +126 -0
- package/agent/docs-md/confidence-note.md +87 -0
- package/agent/docs-md/configurable-dashboard.md +76 -0
- package/agent/docs-md/confirmation-dialog.md +93 -0
- package/agent/docs-md/connected-tools.md +80 -0
- package/agent/docs-md/connector-picker.md +127 -0
- package/agent/docs-md/consistency.md +53 -0
- package/agent/docs-md/content-density.md +61 -0
- package/agent/docs-md/conversation-history.md +78 -0
- package/agent/docs-md/create-record.md +80 -0
- package/agent/docs-md/csv-import.md +88 -0
- package/agent/docs-md/dashboard-items.md +78 -0
- package/agent/docs-md/data-export.md +61 -0
- package/agent/docs-md/data-grid.md +137 -0
- package/agent/docs-md/data-vis-colours.md +76 -0
- package/agent/docs-md/data-visualisation.md +76 -0
- package/agent/docs-md/date-picker.md +178 -0
- package/agent/docs-md/delete-record.md +75 -0
- package/agent/docs-md/dense-table.md +495 -0
- package/agent/docs-md/design-tokens.md +53 -0
- package/agent/docs-md/disabled-read-only.md +75 -0
- package/agent/docs-md/drag-and-drop.md +74 -0
- package/agent/docs-md/dropdown-menu.md +137 -0
- package/agent/docs-md/edit-record.md +73 -0
- package/agent/docs-md/empty-loading-error.md +78 -0
- package/agent/docs-md/empty-state.md +128 -0
- package/agent/docs-md/error-messages.md +75 -0
- package/agent/docs-md/error-state.md +123 -0
- package/agent/docs-md/export-menu.md +86 -0
- package/agent/docs-md/feedback-prompt.md +88 -0
- package/agent/docs-md/field-mapping.md +78 -0
- package/agent/docs-md/file-drop.md +110 -0
- package/agent/docs-md/filter-bar.md +138 -0
- package/agent/docs-md/filter-panel.md +103 -0
- package/agent/docs-md/filtering-search.md +79 -0
- package/agent/docs-md/follow-ups-starters.md +79 -0
- package/agent/docs-md/form-validation.md +95 -0
- package/agent/docs-md/generated-columns.md +65 -0
- package/agent/docs-md/generated-drafts-files.md +71 -0
- package/agent/docs-md/grouped-list.md +89 -0
- package/agent/docs-md/guide-cue.md +170 -0
- package/agent/docs-md/help-panel.md +103 -0
- package/agent/docs-md/help-system.md +74 -0
- package/agent/docs-md/iconography.md +49 -0
- package/agent/docs-md/inbox.md +108 -0
- package/agent/docs-md/inline-editing-validation.md +80 -0
- package/agent/docs-md/inline-input-in-answer.md +70 -0
- package/agent/docs-md/items-palette.md +116 -0
- package/agent/docs-md/keyboard-model.md +47 -0
- package/agent/docs-md/keyboard-navigation.md +68 -0
- package/agent/docs-md/keyboard-shortcut-hint.md +87 -0
- package/agent/docs-md/keyboard-shortcut-sheet.md +90 -0
- package/agent/docs-md/labelling-ai-output.md +66 -0
- package/agent/docs-md/layout.md +47 -0
- package/agent/docs-md/list-table.md +254 -0
- package/agent/docs-md/loading-skeleton.md +91 -0
- package/agent/docs-md/managing-agents.md +71 -0
- package/agent/docs-md/message-composer.md +105 -0
- package/agent/docs-md/migrating.md +49 -0
- package/agent/docs-md/motion.md +93 -0
- package/agent/docs-md/navigation.md +76 -0
- package/agent/docs-md/notification-channels.md +65 -0
- package/agent/docs-md/notifications-decision.md +100 -0
- package/agent/docs-md/onboarding-step.md +114 -0
- package/agent/docs-md/order-book.md +108 -0
- package/agent/docs-md/page-header.md +96 -0
- package/agent/docs-md/pagination.md +88 -0
- package/agent/docs-md/pane-layout.md +105 -0
- package/agent/docs-md/period-comparison.md +66 -0
- package/agent/docs-md/plan-card.md +120 -0
- package/agent/docs-md/popover-rules.md +88 -0
- package/agent/docs-md/prompt-editor.md +90 -0
- package/agent/docs-md/propose-apply.md +65 -0
- package/agent/docs-md/provider-logo.md +97 -0
- package/agent/docs-md/radio.md +83 -0
- package/agent/docs-md/ranked-list.md +102 -0
- package/agent/docs-md/reasoning-progress.md +72 -0
- package/agent/docs-md/record-detail.md +71 -0
- package/agent/docs-md/regenerating-answers.md +70 -0
- package/agent/docs-md/run-status-bar.md +94 -0
- package/agent/docs-md/saved-tasks-schedules.md +63 -0
- package/agent/docs-md/saved-views.md +149 -0
- package/agent/docs-md/saving-views.md +68 -0
- package/agent/docs-md/scheduler.md +258 -0
- package/agent/docs-md/secondary-panels.md +72 -0
- package/agent/docs-md/segmented-control.md +88 -0
- package/agent/docs-md/selection-bulk-actions.md +70 -0
- package/agent/docs-md/separator.md +75 -0
- package/agent/docs-md/setup-checklist.md +92 -0
- package/agent/docs-md/shape-elevation.md +55 -0
- package/agent/docs-md/sheet-grid.md +211 -0
- package/agent/docs-md/side-panel.md +134 -0
- package/agent/docs-md/sign-in.md +114 -0
- package/agent/docs-md/spacing.md +55 -0
- package/agent/docs-md/split-button.md +100 -0
- package/agent/docs-md/stat-compare.md +82 -0
- package/agent/docs-md/static-dashboard.md +75 -0
- package/agent/docs-md/status-badge.md +140 -0
- package/agent/docs-md/step-card.md +75 -0
- package/agent/docs-md/stepper.md +92 -0
- package/agent/docs-md/suggested-changes.md +91 -0
- package/agent/docs-md/switch.md +78 -0
- package/agent/docs-md/table-settings.md +81 -0
- package/agent/docs-md/tabs.md +85 -0
- package/agent/docs-md/tag-input.md +108 -0
- package/agent/docs-md/tag.md +79 -0
- package/agent/docs-md/task-builder.md +87 -0
- package/agent/docs-md/team-instructions.md +72 -0
- package/agent/docs-md/templates-gallery.md +85 -0
- package/agent/docs-md/text-input.md +160 -0
- package/agent/docs-md/thinking-steps.md +89 -0
- package/agent/docs-md/timeline.md +189 -0
- package/agent/docs-md/timestamp.md +84 -0
- package/agent/docs-md/timestamps.md +70 -0
- package/agent/docs-md/toast.md +117 -0
- package/agent/docs-md/tooltip.md +90 -0
- package/agent/docs-md/typography.md +62 -0
- package/agent/docs-md/undo-destructive.md +66 -0
- package/agent/docs-md/unsaved-changes.md +71 -0
- package/agent/docs-md/usage-meter.md +80 -0
- package/agent/docs-md/user-feedback.md +71 -0
- package/agent/docs-md/voice-and-tone.md +60 -0
- package/agent/docs-md/why-choice.md +93 -0
- package/agent/docs-md/workspace-onboarding.md +74 -0
- package/agent/docs-md/writing.md +48 -0
- package/agent/llms-full.txt +18369 -0
- package/agent/llms.txt +198 -0
- package/agent/skills/twoux-audit/SKILL.md +49 -0
- package/agent/skills/twoux-build/SKILL.md +39 -0
- package/agent/skills/twoux-design/SKILL.md +54 -0
- package/agent/skills/twoux-write/SKILL.md +32 -0
- package/bin/check.mjs +123 -0
- package/bin/docs.mjs +24 -0
- package/bin/guard.mjs +27 -0
- package/bin/init.mjs +113 -0
- package/bin/knowledge.mjs +87 -0
- package/bin/mcp.mjs +116 -0
- package/bin/theme.mjs +168 -0
- package/bin/twoux.mjs +25 -0
- package/package.json +80 -3
- package/registry/jobs.json +1028 -0
- package/registry/palette.json +770 -0
- package/registry/rules.json +913 -0
- package/src/components/agent-review.tsx +364 -0
- package/src/components/ai-basics.tsx +212 -0
- package/src/components/ai-work.tsx +304 -0
- package/src/components/ai.tsx +227 -0
- package/src/components/alert-dialog.tsx +203 -0
- package/src/components/alert.tsx +86 -0
- package/src/components/avatar.tsx +114 -0
- package/src/components/badge.tsx +59 -0
- package/src/components/board.tsx +397 -0
- package/src/components/breadcrumb.tsx +114 -0
- package/src/components/button-group.tsx +90 -0
- package/src/components/button.tsx +109 -0
- package/src/components/calendar.tsx +247 -0
- package/src/components/card.tsx +102 -0
- package/src/components/chart-map.tsx +336 -0
- package/src/components/charts-more.tsx +1331 -0
- package/src/components/charts-radial.tsx +328 -0
- package/src/components/charts.tsx +390 -0
- package/src/components/checkbox.tsx +40 -0
- package/src/components/code.tsx +139 -0
- package/src/components/collapsible.tsx +37 -0
- package/src/components/combobox.tsx +339 -0
- package/src/components/command-extras.tsx +11 -0
- package/src/components/command.tsx +196 -0
- package/src/components/data-grid.tsx +213 -0
- package/src/components/data-table.tsx +334 -0
- package/src/components/data-views.tsx +740 -0
- package/src/components/date-picker.tsx +686 -0
- package/src/components/dense-table.tsx +207 -0
- package/src/components/dialog.tsx +162 -0
- package/src/components/dropdown-menu.tsx +264 -0
- package/src/components/empty.tsx +114 -0
- package/src/components/feedback-prompt.tsx +80 -0
- package/src/components/field.tsx +254 -0
- package/src/components/fields.tsx +112 -0
- package/src/components/flow.tsx +840 -0
- package/src/components/guide-cue.tsx +273 -0
- package/src/components/inbox.tsx +113 -0
- package/src/components/input-group.tsx +176 -0
- package/src/components/input.tsx +26 -0
- package/src/components/kbd.tsx +34 -0
- package/src/components/label.tsx +27 -0
- package/src/components/labels.tsx +176 -0
- package/src/components/motion.tsx +136 -0
- package/src/components/onboarding.tsx +454 -0
- package/src/components/order-book.tsx +185 -0
- package/src/components/pane-layout.tsx +417 -0
- package/src/components/popover.tsx +96 -0
- package/src/components/provider-logos.data.ts +49 -0
- package/src/components/radio-group.tsx +52 -0
- package/src/components/scheduler.tsx +997 -0
- package/src/components/segmented-control.tsx +28 -0
- package/src/components/select.tsx +195 -0
- package/src/components/separator.tsx +31 -0
- package/src/components/sheet.tsx +148 -0
- package/src/components/shell.tsx +426 -0
- package/src/components/shortcut-sheet.tsx +83 -0
- package/src/components/skeleton.tsx +18 -0
- package/src/components/sonner.tsx +107 -0
- package/src/components/spinner.tsx +13 -0
- package/src/components/split-button.tsx +58 -0
- package/src/components/stepper.tsx +78 -0
- package/src/components/switch.tsx +43 -0
- package/src/components/table.tsx +126 -0
- package/src/components/tabs.tsx +118 -0
- package/src/components/tag-input.tsx +253 -0
- package/src/components/textarea.tsx +21 -0
- package/src/components/theme.tsx +80 -0
- package/src/components/timeline.tsx +408 -0
- package/src/components/toggle-group.tsx +87 -0
- package/src/components/toggle.tsx +54 -0
- package/src/components/tooltip.tsx +65 -0
- package/src/components/workspace-actions.tsx +294 -0
- package/src/components/workspace-import.tsx +465 -0
- package/src/components/workspace.tsx +1884 -0
- package/src/index.ts +80 -0
- package/src/lib/active-indicator.tsx +53 -0
- package/src/lib/chart-kit.tsx +72 -0
- package/src/lib/cn.ts +6 -0
- package/src/lib/shortcuts.tsx +61 -0
- package/src/lib/workspace.tsx +10 -0
- package/src/shadcn/alert-dialog.tsx +196 -0
- package/src/shadcn/alert.tsx +65 -0
- package/src/shadcn/avatar.tsx +108 -0
- package/src/shadcn/badge.tsx +47 -0
- package/src/shadcn/breadcrumb.tsx +108 -0
- package/src/shadcn/button-group.tsx +83 -0
- package/src/shadcn/button.tsx +63 -0
- package/src/shadcn/calendar.tsx +220 -0
- package/src/shadcn/card.tsx +91 -0
- package/src/shadcn/checkbox.tsx +31 -0
- package/src/shadcn/collapsible.tsx +33 -0
- package/src/shadcn/command.tsx +184 -0
- package/src/shadcn/dialog.tsx +158 -0
- package/src/shadcn/dropdown-menu.tsx +256 -0
- package/src/shadcn/empty.tsx +103 -0
- package/src/shadcn/field.tsx +248 -0
- package/src/shadcn/input-group.tsx +170 -0
- package/src/shadcn/input.tsx +20 -0
- package/src/shadcn/kbd.tsx +28 -0
- package/src/shadcn/label.tsx +23 -0
- package/src/shadcn/popover.tsx +88 -0
- package/src/shadcn/radio-group.tsx +44 -0
- package/src/shadcn/select.tsx +189 -0
- package/src/shadcn/separator.tsx +27 -0
- package/src/shadcn/sheet.tsx +142 -0
- package/src/shadcn/skeleton.tsx +13 -0
- package/src/shadcn/sonner.tsx +40 -0
- package/src/shadcn/switch.tsx +34 -0
- package/src/shadcn/table.tsx +115 -0
- package/src/shadcn/tabs.tsx +90 -0
- package/src/shadcn/textarea.tsx +17 -0
- package/src/shadcn/toggle-group.tsx +83 -0
- package/src/shadcn/toggle.tsx +46 -0
- package/src/shadcn/tooltip.tsx +56 -0
- package/src/styles/palette.css +127 -0
- package/src/styles/theme.css +35 -0
- package/src/styles/twoux.css +236 -0
- package/theme.json +14 -0
- package/README.md +0 -3
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Combobox: pick one or several values from a long list by typing, and add a missing one without leaving the form.
|
|
3
|
+
* TwoUX composition on the generated shadcn Popover and Command (cmdk), following shadcn's combobox pattern.
|
|
4
|
+
* Proposal: proposals/combobox.md.
|
|
5
|
+
*/
|
|
6
|
+
"use client"
|
|
7
|
+
|
|
8
|
+
import { AlertCircle, Check, ChevronDown, Plus, X } from "lucide-react"
|
|
9
|
+
import * as React from "react"
|
|
10
|
+
import { cn } from "../lib/cn"
|
|
11
|
+
import { Button } from "./button"
|
|
12
|
+
import { Command, CommandGroup, CommandInput, CommandItem, CommandList } from "./command"
|
|
13
|
+
import { Field, FieldDescription, FieldLabel } from "./field"
|
|
14
|
+
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "./popover"
|
|
15
|
+
|
|
16
|
+
export type ComboboxOption = {
|
|
17
|
+
value: string
|
|
18
|
+
label: string
|
|
19
|
+
/** Secondary text, matched as well as the label: a region, a team, a code. */
|
|
20
|
+
description?: string
|
|
21
|
+
/** Options with the same group are listed under that heading, in the order they first appear. */
|
|
22
|
+
group?: string
|
|
23
|
+
disabled?: boolean
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
type Common = {
|
|
27
|
+
label: string
|
|
28
|
+
options: ComboboxOption[]
|
|
29
|
+
/** The thing listed, used in messages: { one: "location", many: "locations" }. Defaults to the label in lower case. */
|
|
30
|
+
noun?: { one: string; many: string }
|
|
31
|
+
placeholder?: string
|
|
32
|
+
hint?: string
|
|
33
|
+
error?: string
|
|
34
|
+
required?: boolean
|
|
35
|
+
optional?: boolean
|
|
36
|
+
disabled?: boolean
|
|
37
|
+
/** Shows the value as text, without the list. */
|
|
38
|
+
readOnly?: boolean
|
|
39
|
+
id?: string
|
|
40
|
+
/** Submitted with the form: one value, or one value per chosen option. */
|
|
41
|
+
name?: string
|
|
42
|
+
className?: string
|
|
43
|
+
/** Called as people type, after 250ms without typing. Set filter to null when the app filters (server search). */
|
|
44
|
+
onSearch?: (text: string) => void
|
|
45
|
+
/** null: the app has already filtered options for the text. A function: custom matching. Default: label and description, ignoring case and accents. */
|
|
46
|
+
filter?: null | ((option: ComboboxOption, text: string) => boolean)
|
|
47
|
+
/** The app is fetching options. "Searching…" appears only if this lasts more than a second. */
|
|
48
|
+
loading?: boolean
|
|
49
|
+
/** Options failed to load: what failed and what to check, shown in the list with Try again. */
|
|
50
|
+
loadError?: string
|
|
51
|
+
onRetry?: () => void
|
|
52
|
+
/**
|
|
53
|
+
* Offers Create "<text>" when nothing matches exactly. Return the new option to select it at once (name-only
|
|
54
|
+
* records), or open an overlay side panel and select the record when it is saved.
|
|
55
|
+
*/
|
|
56
|
+
onCreate?: (text: string) => ComboboxOption | void | Promise<ComboboxOption | void>
|
|
57
|
+
}
|
|
58
|
+
type Single = Common & { multiple?: false; value: string | null; onValueChange: (value: string | null) => void }
|
|
59
|
+
type Multiple = Common & {
|
|
60
|
+
multiple: true
|
|
61
|
+
value: string[]
|
|
62
|
+
onValueChange: (value: string[]) => void
|
|
63
|
+
/** Most options people can choose. The footer says so. */
|
|
64
|
+
max?: number
|
|
65
|
+
}
|
|
66
|
+
export type ComboboxProps = Single | Multiple
|
|
67
|
+
|
|
68
|
+
const collator = new Intl.Collator("en-GB", { sensitivity: "base", usage: "search" })
|
|
69
|
+
const fold = (s: string) => s.normalize("NFD").replace(/\p{Diacritic}/gu, "").toLowerCase()
|
|
70
|
+
/** Case- and accent-insensitive "contains", on the label and the description. */
|
|
71
|
+
export const matchOption = (o: ComboboxOption, text: string) => {
|
|
72
|
+
const t = fold(text.trim())
|
|
73
|
+
return !t || fold(o.label).includes(t) || (!!o.description && fold(o.description).includes(t))
|
|
74
|
+
}
|
|
75
|
+
const MORE = 3
|
|
76
|
+
|
|
77
|
+
function useDelayed(on: boolean | undefined, ms: number) {
|
|
78
|
+
const [late, setLate] = React.useState(false)
|
|
79
|
+
React.useEffect(() => {
|
|
80
|
+
if (!on) return setLate(false)
|
|
81
|
+
const t = setTimeout(() => setLate(true), ms)
|
|
82
|
+
return () => clearTimeout(t)
|
|
83
|
+
}, [on, ms])
|
|
84
|
+
return late
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Searchable select for long lists: one value, or several with multiple. Values always come from the list or from Create. */
|
|
88
|
+
export function Combobox(props: ComboboxProps) {
|
|
89
|
+
const { label, options, noun, placeholder, hint, error, required, optional, disabled, readOnly, id, name, className, onSearch, filter, loading, loadError, onRetry, onCreate } = props
|
|
90
|
+
const auto = React.useId()
|
|
91
|
+
const fid = id ?? auto
|
|
92
|
+
const many = noun?.many ?? label.toLowerCase()
|
|
93
|
+
const multiple = props.multiple === true
|
|
94
|
+
const chosen: string[] = multiple ? props.value : props.value ? [props.value] : []
|
|
95
|
+
const max = multiple ? props.max : undefined
|
|
96
|
+
const known = React.useRef(new Map<string, ComboboxOption>())
|
|
97
|
+
for (const o of options) known.current.set(o.value, o)
|
|
98
|
+
const labelOf = (v: string) => known.current.get(v)?.label ?? v
|
|
99
|
+
|
|
100
|
+
const [open, setOpen] = React.useState(false)
|
|
101
|
+
const [text, setText] = React.useState("")
|
|
102
|
+
const [order, setOrder] = React.useState<string[]>([])
|
|
103
|
+
const [creating, setCreating] = React.useState(false)
|
|
104
|
+
const triggerRef = React.useRef<HTMLButtonElement>(null)
|
|
105
|
+
const tagRefs = React.useRef<(HTMLButtonElement | null)[]>([])
|
|
106
|
+
const searching = useDelayed(loading, 1000)
|
|
107
|
+
|
|
108
|
+
React.useEffect(() => {
|
|
109
|
+
if (!onSearch || !open) return
|
|
110
|
+
const t = setTimeout(() => onSearch(text), 250)
|
|
111
|
+
return () => clearTimeout(t)
|
|
112
|
+
}, [text, open, onSearch])
|
|
113
|
+
|
|
114
|
+
const setValue = (next: string[]) => (multiple ? (props as Multiple).onValueChange(next) : (props as Single).onValueChange(next[0] ?? null))
|
|
115
|
+
const openWith = (initial: string) => {
|
|
116
|
+
// Chosen options are listed first, in the order they had when the list opened, so rows don't jump while ticking.
|
|
117
|
+
setOrder(multiple ? [...chosen] : [])
|
|
118
|
+
setText(initial)
|
|
119
|
+
setOpen(true)
|
|
120
|
+
}
|
|
121
|
+
const pick = (v: string) => {
|
|
122
|
+
if (!multiple) {
|
|
123
|
+
setValue([v])
|
|
124
|
+
setOpen(false)
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
if (chosen.includes(v)) setValue(chosen.filter((x) => x !== v))
|
|
128
|
+
else if (!max || chosen.length < max) setValue([...chosen, v])
|
|
129
|
+
}
|
|
130
|
+
const create = async () => {
|
|
131
|
+
if (!onCreate) return
|
|
132
|
+
setCreating(true)
|
|
133
|
+
try {
|
|
134
|
+
const made = await onCreate(text.trim())
|
|
135
|
+
if (made) {
|
|
136
|
+
known.current.set(made.value, made)
|
|
137
|
+
pick(made.value)
|
|
138
|
+
}
|
|
139
|
+
setText("")
|
|
140
|
+
} finally {
|
|
141
|
+
setCreating(false)
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const remove = (i: number) => {
|
|
145
|
+
setValue(chosen.filter((_, j) => j !== i))
|
|
146
|
+
requestAnimationFrame(() => (tagRefs.current[i - 1] ?? triggerRef.current)?.focus())
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const visible = filter === null ? options : options.filter((o) => (filter ?? matchOption)(o, text))
|
|
150
|
+
const sorted = order.length ? [...visible.filter((o) => order.includes(o.value)), ...visible.filter((o) => !order.includes(o.value))] : visible
|
|
151
|
+
const groups: [string, ComboboxOption[]][] = []
|
|
152
|
+
for (const o of sorted) {
|
|
153
|
+
const g = o.group ?? ""
|
|
154
|
+
const at = groups.find(([k]) => k === g)
|
|
155
|
+
if (at) at[1].push(o)
|
|
156
|
+
else groups.push([g, [o]])
|
|
157
|
+
}
|
|
158
|
+
const exact = options.some((o) => collator.compare(o.label, text.trim()) === 0)
|
|
159
|
+
const canCreate = !!onCreate && !!text.trim() && !exact && !loading
|
|
160
|
+
const full = !!max && chosen.length >= max
|
|
161
|
+
const shown = chosen.slice(0, multiple ? MORE : 1)
|
|
162
|
+
const rest = chosen.length - shown.length
|
|
163
|
+
const msgId = `${fid}-msg`
|
|
164
|
+
const labelId = `${fid}-label`
|
|
165
|
+
const valueId = `${fid}-value`
|
|
166
|
+
const status = searching ? `Searching ${many}…` : loadError ? "" : `${visible.length} ${visible.length === 1 ? "result" : "results"}`
|
|
167
|
+
const summary = multiple ? (chosen.length ? `${chosen.length} selected` : "None selected") : chosen[0] ? labelOf(chosen[0]) : "None selected"
|
|
168
|
+
|
|
169
|
+
const message = error ? (
|
|
170
|
+
<p id={msgId} className="flex items-start gap-control text-sm text-bad">
|
|
171
|
+
<AlertCircle className="mt-0.5 size-3.5 shrink-0" aria-hidden="true" />
|
|
172
|
+
{error}
|
|
173
|
+
</p>
|
|
174
|
+
) : hint ? (
|
|
175
|
+
<FieldDescription id={msgId}>{hint}</FieldDescription>
|
|
176
|
+
) : null
|
|
177
|
+
|
|
178
|
+
const fieldLabel = (
|
|
179
|
+
<FieldLabel id={labelId} htmlFor={fid}>
|
|
180
|
+
{label}
|
|
181
|
+
{required && <span aria-hidden="true" className="text-bad">*</span>}
|
|
182
|
+
{optional && <span className="font-normal text-ink-3">(optional)</span>}
|
|
183
|
+
</FieldLabel>
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
if (readOnly)
|
|
187
|
+
return (
|
|
188
|
+
<Field className={cn("gap-2", className)}>
|
|
189
|
+
{fieldLabel}
|
|
190
|
+
<p id={fid} className="flex min-h-8 flex-wrap items-center gap-1 text-base text-ink">
|
|
191
|
+
{chosen.length ? chosen.map((v) => (multiple ? <span key={v} className="inline-flex h-5.5 items-center rounded-xs border border-line-strong bg-surface px-control text-sm">{labelOf(v)}</span> : <span key={v}>{labelOf(v)}</span>)) : <span className="text-ink-3">None</span>}
|
|
192
|
+
</p>
|
|
193
|
+
</Field>
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
return (
|
|
197
|
+
<Field data-invalid={!!error || undefined} className={cn("gap-2", className)}>
|
|
198
|
+
{fieldLabel}
|
|
199
|
+
<Popover
|
|
200
|
+
open={open}
|
|
201
|
+
onOpenChange={(o) => {
|
|
202
|
+
if (o) openWith("")
|
|
203
|
+
else setOpen(false)
|
|
204
|
+
}}
|
|
205
|
+
>
|
|
206
|
+
<PopoverAnchor asChild>
|
|
207
|
+
<div
|
|
208
|
+
data-slot="combobox"
|
|
209
|
+
className={cn(
|
|
210
|
+
"flex min-h-8 w-full min-w-0 flex-wrap items-center gap-1 rounded-sm border border-input bg-background shadow-xs transition-[color,background-color,border-color,box-shadow] duration-(--duration-fast) ease-(--ease-out) hover:border-input-hover",
|
|
211
|
+
"has-[[data-slot=combobox-trigger]:focus-visible]:border-ring has-[[data-slot=combobox-trigger]:focus-visible]:ring-3 has-[[data-slot=combobox-trigger]:focus-visible]:ring-ring/50",
|
|
212
|
+
error && "border-destructive-edge",
|
|
213
|
+
multiple && chosen.length > 0 && "py-1 pl-1",
|
|
214
|
+
disabled && "pointer-events-none opacity-50",
|
|
215
|
+
)}
|
|
216
|
+
>
|
|
217
|
+
{multiple &&
|
|
218
|
+
shown.map((v, i) => (
|
|
219
|
+
<span key={v} className="inline-flex h-5.5 max-w-48 items-center gap-control rounded-xs border border-line-strong bg-surface px-control text-sm text-ink">
|
|
220
|
+
<span className="truncate">{labelOf(v)}</span>
|
|
221
|
+
<button
|
|
222
|
+
type="button"
|
|
223
|
+
ref={(el) => {
|
|
224
|
+
tagRefs.current[i] = el
|
|
225
|
+
}}
|
|
226
|
+
aria-label={`Remove ${labelOf(v)}`}
|
|
227
|
+
disabled={disabled}
|
|
228
|
+
onClick={() => remove(i)}
|
|
229
|
+
onKeyDown={(e) => {
|
|
230
|
+
if (e.key === "Backspace" || e.key === "Delete") {
|
|
231
|
+
e.preventDefault()
|
|
232
|
+
remove(i)
|
|
233
|
+
}
|
|
234
|
+
if (e.key === "ArrowLeft") tagRefs.current[i - 1]?.focus()
|
|
235
|
+
if (e.key === "ArrowRight") (tagRefs.current[i + 1] ?? triggerRef.current)?.focus()
|
|
236
|
+
}}
|
|
237
|
+
className="relative -mr-0.5 grid size-4 shrink-0 place-items-center rounded-xs text-ink-3 after:absolute after:-inset-1 hover:bg-hover hover:text-ink focus-visible:outline-2 focus-visible:outline-ring"
|
|
238
|
+
>
|
|
239
|
+
<X className="size-3" aria-hidden="true" />
|
|
240
|
+
</button>
|
|
241
|
+
</span>
|
|
242
|
+
))}
|
|
243
|
+
{multiple && rest > 0 && <span className="tabular px-0.5 text-sm text-ink-3">+{rest} more</span>}
|
|
244
|
+
<PopoverTrigger asChild>
|
|
245
|
+
<button
|
|
246
|
+
ref={triggerRef}
|
|
247
|
+
id={fid}
|
|
248
|
+
type="button"
|
|
249
|
+
data-slot="combobox-trigger"
|
|
250
|
+
role="combobox"
|
|
251
|
+
aria-expanded={open}
|
|
252
|
+
aria-haspopup="listbox"
|
|
253
|
+
aria-labelledby={`${labelId} ${valueId}`}
|
|
254
|
+
aria-describedby={message ? msgId : undefined}
|
|
255
|
+
aria-invalid={error ? true : undefined}
|
|
256
|
+
aria-required={required || undefined}
|
|
257
|
+
disabled={disabled}
|
|
258
|
+
onKeyDown={(e) => {
|
|
259
|
+
if (e.key.length === 1 && !e.metaKey && !e.ctrlKey && !e.altKey && e.key !== " ") {
|
|
260
|
+
e.preventDefault()
|
|
261
|
+
openWith(e.key)
|
|
262
|
+
}
|
|
263
|
+
if (e.key === "ArrowDown") {
|
|
264
|
+
e.preventDefault()
|
|
265
|
+
openWith("")
|
|
266
|
+
}
|
|
267
|
+
if (e.key === "Backspace" && multiple && shown.length) {
|
|
268
|
+
e.preventDefault()
|
|
269
|
+
tagRefs.current[shown.length - 1]?.focus()
|
|
270
|
+
}
|
|
271
|
+
}}
|
|
272
|
+
// With tags chosen the button only holds the chevron, so it needs just enough room to stay on the tags' line.
|
|
273
|
+
className={cn("flex h-7.5 flex-1 items-center justify-between gap-2 rounded-sm px-2 text-left text-base outline-none", multiple && chosen.length ? "min-w-7" : "min-w-16")}
|
|
274
|
+
>
|
|
275
|
+
<span id={valueId} className={cn("truncate", (multiple || !chosen[0]) && "text-muted-foreground")}>
|
|
276
|
+
{multiple ? (chosen.length ? <span className="sr-only">{summary}</span> : (placeholder ?? `Choose ${many}`)) : chosen[0] ? <span className="text-ink">{labelOf(chosen[0])}</span> : (placeholder ?? `Choose ${noun?.one ?? many}`)}
|
|
277
|
+
</span>
|
|
278
|
+
<ChevronDown className="size-3.5 shrink-0 text-ink-3" aria-hidden="true" />
|
|
279
|
+
</button>
|
|
280
|
+
</PopoverTrigger>
|
|
281
|
+
</div>
|
|
282
|
+
</PopoverAnchor>
|
|
283
|
+
<PopoverContent align="start" className="w-(--radix-popover-trigger-width) min-w-64 p-0" onOpenAutoFocus={(e) => e.preventDefault()}>
|
|
284
|
+
<Command shouldFilter={false} loop={false} label={label}>
|
|
285
|
+
<CommandInput autoFocus value={text} onValueChange={setText} placeholder={`Search ${many}`} aria-label={`Search ${many}`} />
|
|
286
|
+
<CommandList aria-multiselectable={multiple || undefined} aria-label={label}>
|
|
287
|
+
{loadError ? (
|
|
288
|
+
<div role="alert" className="grid justify-items-start gap-2 px-3 py-3 text-base text-ink-2">
|
|
289
|
+
<span className="flex items-start gap-control"><AlertCircle className="mt-0.5 size-3.5 shrink-0 text-bad" aria-hidden="true" />{loadError}</span>
|
|
290
|
+
{onRetry && <Button variant="outline" size="xs" onClick={onRetry}>Try again</Button>}
|
|
291
|
+
</div>
|
|
292
|
+
) : searching && !visible.length ? (
|
|
293
|
+
<p className="px-3 py-3 text-base text-ink-2">Searching {many}…</p>
|
|
294
|
+
) : !visible.length && !canCreate ? (
|
|
295
|
+
<p className="px-3 py-3 text-base text-ink-2">{text.trim() ? `No ${many} match "${text.trim()}".` : `No ${many} yet.`}</p>
|
|
296
|
+
) : null}
|
|
297
|
+
{!loadError &&
|
|
298
|
+
groups.map(([g, items]) => (
|
|
299
|
+
<CommandGroup key={g || "_"} heading={g || undefined}>
|
|
300
|
+
{items.map((o) => {
|
|
301
|
+
const on = chosen.includes(o.value)
|
|
302
|
+
return (
|
|
303
|
+
<CommandItem key={o.value} value={o.value} disabled={o.disabled || (full && !on)} data-checked={on || undefined} onSelect={() => pick(o.value)} className="pr-8">
|
|
304
|
+
<span className="min-w-0 flex-1 truncate">{o.label}</span>
|
|
305
|
+
{o.description && <span className="shrink-0 text-sm text-ink-3">{o.description}</span>}
|
|
306
|
+
<Check className={cn("absolute right-2 size-3.5 text-ink-2", !on && "invisible")} aria-hidden="true" />
|
|
307
|
+
<span className="sr-only">{on ? ", selected" : ""}</span>
|
|
308
|
+
</CommandItem>
|
|
309
|
+
)
|
|
310
|
+
})}
|
|
311
|
+
</CommandGroup>
|
|
312
|
+
))}
|
|
313
|
+
{canCreate && !loadError && (
|
|
314
|
+
<CommandGroup>
|
|
315
|
+
<CommandItem value={`__create__${text}`} disabled={creating} onSelect={create}>
|
|
316
|
+
<Plus aria-hidden="true" />
|
|
317
|
+
<span className="truncate">Create "{text.trim()}"</span>
|
|
318
|
+
</CommandItem>
|
|
319
|
+
</CommandGroup>
|
|
320
|
+
)}
|
|
321
|
+
</CommandList>
|
|
322
|
+
</Command>
|
|
323
|
+
{multiple && (
|
|
324
|
+
<div className="flex items-center gap-2 border-t border-line px-3 py-2 text-sm text-ink-2">
|
|
325
|
+
<span className="tabular flex-1">
|
|
326
|
+
{chosen.length} selected
|
|
327
|
+
{max ? ` · up to ${max} ${many}` : ""}
|
|
328
|
+
</span>
|
|
329
|
+
{chosen.length > 0 && <Button variant="ghost" size="xs" onClick={() => setValue([])}>Clear</Button>}
|
|
330
|
+
</div>
|
|
331
|
+
)}
|
|
332
|
+
<span className="sr-only" role="status" aria-live="polite">{open ? status : ""}</span>
|
|
333
|
+
</PopoverContent>
|
|
334
|
+
</Popover>
|
|
335
|
+
{name && chosen.map((v) => <input key={v} type="hidden" name={name} value={v} />)}
|
|
336
|
+
{message}
|
|
337
|
+
</Field>
|
|
338
|
+
)
|
|
339
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/** TwoUX additions to shadcn Command: one-line secondary text and a key-hint footer. */
|
|
2
|
+
import type * as React from "react"
|
|
3
|
+
import { cn } from "../lib/cn"
|
|
4
|
+
|
|
5
|
+
export function CommandItemMeta({ className, ...props }: React.ComponentProps<"span">) {
|
|
6
|
+
return <span className={cn("min-w-0 truncate text-ink-3", className)} {...props} />
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function CommandFooter({ className, ...props }: React.ComponentProps<"div">) {
|
|
10
|
+
return <div className={cn("flex h-9 items-center gap-4 border-t border-line px-3 text-sm text-ink-3", className)} {...props} />
|
|
11
|
+
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generated from shadcn/ui new-york-v4 "command" by scripts/tailor-shadcn.mjs. Do not edit by hand.
|
|
3
|
+
* TwoUX changes:
|
|
4
|
+
* - Icons stay smaller than the label and muted
|
|
5
|
+
* - Group labels in sentence case, quiet
|
|
6
|
+
* - Dialog keeps compact rows and 14px icons; shadcn enlarges them to 20px with 12px padding
|
|
7
|
+
* - Shortcut slot holds words as well as keys: no letter-spacing, icons align
|
|
8
|
+
* - Adds commandProps (shouldFilter, loop…) passed to the inner Command
|
|
9
|
+
* - Opens near the top like a search, with the floating shadow; Esc closes it so no close button
|
|
10
|
+
*/
|
|
11
|
+
"use client"
|
|
12
|
+
|
|
13
|
+
import * as React from "react"
|
|
14
|
+
import { Command as CommandPrimitive } from "cmdk"
|
|
15
|
+
import { cn } from "../lib/cn"
|
|
16
|
+
import { SearchIcon } from "lucide-react"
|
|
17
|
+
|
|
18
|
+
import {
|
|
19
|
+
Dialog,
|
|
20
|
+
DialogContent,
|
|
21
|
+
DialogDescription,
|
|
22
|
+
DialogHeader,
|
|
23
|
+
DialogTitle,
|
|
24
|
+
} from "./dialog"
|
|
25
|
+
|
|
26
|
+
function Command({
|
|
27
|
+
className,
|
|
28
|
+
...props
|
|
29
|
+
}: React.ComponentProps<typeof CommandPrimitive>) {
|
|
30
|
+
return (
|
|
31
|
+
<CommandPrimitive
|
|
32
|
+
data-slot="command"
|
|
33
|
+
className={cn(
|
|
34
|
+
"flex h-full w-full flex-col overflow-hidden rounded-md bg-popover text-popover-foreground",
|
|
35
|
+
className
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function CommandDialog({
|
|
43
|
+
title = "Command Palette",
|
|
44
|
+
description = "Search for a command to run...",
|
|
45
|
+
children,
|
|
46
|
+
className,
|
|
47
|
+
showCloseButton = false,
|
|
48
|
+
commandProps,
|
|
49
|
+
...props
|
|
50
|
+
}: React.ComponentProps<typeof Dialog> & {
|
|
51
|
+
commandProps?: React.ComponentProps<typeof CommandPrimitive>
|
|
52
|
+
title?: string
|
|
53
|
+
description?: string
|
|
54
|
+
className?: string
|
|
55
|
+
showCloseButton?: boolean
|
|
56
|
+
}) {
|
|
57
|
+
return (
|
|
58
|
+
<Dialog {...props}>
|
|
59
|
+
<DialogHeader className="sr-only">
|
|
60
|
+
<DialogTitle>{title}</DialogTitle>
|
|
61
|
+
<DialogDescription>{description}</DialogDescription>
|
|
62
|
+
</DialogHeader>
|
|
63
|
+
<DialogContent
|
|
64
|
+
className={cn("top-24 translate-y-0 overflow-hidden border-line p-0 shadow-pop sm:max-w-xl", className)}
|
|
65
|
+
showCloseButton={showCloseButton}
|
|
66
|
+
>
|
|
67
|
+
<Command {...commandProps} className="**:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group]]:px-1.5 [&_[cmdk-input]]:h-12 [&_[cmdk-input]]:text-md">
|
|
68
|
+
{children}
|
|
69
|
+
</Command>
|
|
70
|
+
</DialogContent>
|
|
71
|
+
</Dialog>
|
|
72
|
+
)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function CommandInput({
|
|
76
|
+
className,
|
|
77
|
+
...props
|
|
78
|
+
}: React.ComponentProps<typeof CommandPrimitive.Input>) {
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
data-slot="command-input-wrapper"
|
|
82
|
+
className="flex h-9 items-center gap-2 border-b px-3"
|
|
83
|
+
>
|
|
84
|
+
<SearchIcon className="size-3.5 shrink-0 text-muted-foreground" />
|
|
85
|
+
<CommandPrimitive.Input
|
|
86
|
+
data-slot="command-input"
|
|
87
|
+
className={cn(
|
|
88
|
+
"flex h-10 w-full rounded-sm bg-transparent py-3 text-base outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
|
|
89
|
+
className
|
|
90
|
+
)}
|
|
91
|
+
{...props}
|
|
92
|
+
/>
|
|
93
|
+
</div>
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function CommandList({
|
|
98
|
+
className,
|
|
99
|
+
...props
|
|
100
|
+
}: React.ComponentProps<typeof CommandPrimitive.List>) {
|
|
101
|
+
return (
|
|
102
|
+
<CommandPrimitive.List
|
|
103
|
+
data-slot="command-list"
|
|
104
|
+
className={cn(
|
|
105
|
+
"max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto",
|
|
106
|
+
className
|
|
107
|
+
)}
|
|
108
|
+
{...props}
|
|
109
|
+
/>
|
|
110
|
+
)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function CommandEmpty({
|
|
114
|
+
...props
|
|
115
|
+
}: React.ComponentProps<typeof CommandPrimitive.Empty>) {
|
|
116
|
+
return (
|
|
117
|
+
<CommandPrimitive.Empty
|
|
118
|
+
data-slot="command-empty"
|
|
119
|
+
className="py-6 text-center text-base"
|
|
120
|
+
{...props}
|
|
121
|
+
/>
|
|
122
|
+
)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function CommandGroup({
|
|
126
|
+
className,
|
|
127
|
+
...props
|
|
128
|
+
}: React.ComponentProps<typeof CommandPrimitive.Group>) {
|
|
129
|
+
return (
|
|
130
|
+
<CommandPrimitive.Group
|
|
131
|
+
data-slot="command-group"
|
|
132
|
+
className={cn(
|
|
133
|
+
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-sm [&_[cmdk-group-heading]]:text-ink-3 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
|
|
134
|
+
className
|
|
135
|
+
)}
|
|
136
|
+
{...props}
|
|
137
|
+
/>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function CommandSeparator({
|
|
142
|
+
className,
|
|
143
|
+
...props
|
|
144
|
+
}: React.ComponentProps<typeof CommandPrimitive.Separator>) {
|
|
145
|
+
return (
|
|
146
|
+
<CommandPrimitive.Separator
|
|
147
|
+
data-slot="command-separator"
|
|
148
|
+
className={cn("-mx-1 h-px bg-border", className)}
|
|
149
|
+
{...props}
|
|
150
|
+
/>
|
|
151
|
+
)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function CommandItem({
|
|
155
|
+
className,
|
|
156
|
+
...props
|
|
157
|
+
}: React.ComponentProps<typeof CommandPrimitive.Item>) {
|
|
158
|
+
return (
|
|
159
|
+
<CommandPrimitive.Item
|
|
160
|
+
data-slot="command-item"
|
|
161
|
+
className={cn(
|
|
162
|
+
"relative flex cursor-default items-center gap-2 rounded-xs px-2 py-1.5 text-base outline-hidden select-none transition-colors duration-(--duration-fast) ease-(--ease-out) data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 data-[selected=true]:bg-accent data-[selected=true]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-muted-foreground",
|
|
163
|
+
className
|
|
164
|
+
)}
|
|
165
|
+
{...props}
|
|
166
|
+
/>
|
|
167
|
+
)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function CommandShortcut({
|
|
171
|
+
className,
|
|
172
|
+
...props
|
|
173
|
+
}: React.ComponentProps<"span">) {
|
|
174
|
+
return (
|
|
175
|
+
<span
|
|
176
|
+
data-slot="command-shortcut"
|
|
177
|
+
className={cn(
|
|
178
|
+
"ml-auto flex items-center gap-1 text-sm text-muted-foreground",
|
|
179
|
+
className
|
|
180
|
+
)}
|
|
181
|
+
{...props}
|
|
182
|
+
/>
|
|
183
|
+
)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export {
|
|
187
|
+
Command,
|
|
188
|
+
CommandDialog,
|
|
189
|
+
CommandInput,
|
|
190
|
+
CommandList,
|
|
191
|
+
CommandEmpty,
|
|
192
|
+
CommandGroup,
|
|
193
|
+
CommandItem,
|
|
194
|
+
CommandShortcut,
|
|
195
|
+
CommandSeparator,
|
|
196
|
+
}
|