diff --git a/apps/builder/app/builder/features/command-panel/command-panel.tsx b/apps/builder/app/builder/features/command-panel/command-panel.tsx index f0511171a5..57ad57af18 100644 --- a/apps/builder/app/builder/features/command-panel/command-panel.tsx +++ b/apps/builder/app/builder/features/command-panel/command-panel.tsx @@ -42,6 +42,8 @@ import { insert as insertComponent } from "~/builder/features/components/insert" import { $selectedPage, selectPage } from "~/shared/awareness"; import { mapGroupBy } from "~/shared/shim"; import { setActiveSidebarPanel } from "~/builder/shared/nano-states"; +import { $commandMetas } from "~/shared/commands-emitter"; +import { emitCommand } from "~/builder/shared/commands"; const $commandPanel = atom< | undefined @@ -129,7 +131,7 @@ const $componentOptions = computed( } ); -const ComponentGroup = ({ options }: { options: ComponentOption[] }) => { +const ComponentOptionsGroup = ({ options }: { options: ComponentOption[] }) => { return ( { return `${breakpoint.label}: ${label}`; }; -const BreakpointGroup = ({ options }: { options: BreakpointOption[] }) => { +const BreakpointOptionsGroup = ({ + options, +}: { + options: BreakpointOption[]; +}) => { return ( { +const PageOptionsGroup = ({ options }: { options: PageOption[] }) => { const action = useSelectedAction(); return ( { ); }; +type ShortcutOption = { + tokens: string[]; + type: "shortcut"; + name: string; + label: string; + keys: string[]; +}; + +const $shortcutOptions = computed([$commandMetas], (commandMetas) => { + const shortcutOptions: ShortcutOption[] = []; + for (const [name, meta] of commandMetas) { + if (meta.defaultHotkeys && !meta.hidden) { + const label = humanizeString(name); + const keys = meta.defaultHotkeys[0] + .split("+") + .map((key) => (key === "meta" ? "cmd" : key)); + shortcutOptions.push({ + tokens: ["shortcuts", "commands", label], + type: "shortcut", + name, + label, + keys, + }); + } + } + return shortcutOptions; +}); + +const ShortcutOptionsGroup = ({ options }: { options: ShortcutOption[] }) => { + return ( + Shortcuts} + actions={["execute"]} + > + {options.map(({ name, label, keys }) => ( + { + closeCommandPanel(); + emitCommand(name as never); + }} + > + {label} + + + ))} + + ); +}; + const $options = computed( - [$componentOptions, $breakpointOptions, $pageOptions], - (componentOptions, breakpointOptions, pageOptions) => [ + [$componentOptions, $breakpointOptions, $pageOptions, $shortcutOptions], + (componentOptions, breakpointOptions, pageOptions, commandOptions) => [ ...componentOptions, ...breakpointOptions, ...pageOptions, + ...commandOptions, ] ); @@ -309,10 +369,14 @@ const CommandDialogContent = () => { const [search, setSearch] = useState(""); const options = useStore($options); let matches = options; - for (const word of search.trim().split(/\s+/)) { - matches = matchSorter(matches, word, { - keys: ["tokens"], - }); + // prevent searching when value is empty + // to preserve original items order + if (search.trim().length > 0) { + for (const word of search.trim().split(/\s+/)) { + matches = matchSorter(matches, word, { + keys: ["tokens"], + }); + } } const groups = mapGroupBy(matches, (match) => match.type); return ( @@ -324,7 +388,7 @@ const CommandDialogContent = () => { {Array.from(groups).map(([group, matches]) => { if (group === "component") { return ( - @@ -332,7 +396,7 @@ const CommandDialogContent = () => { } if (group === "breakpoint") { return ( - @@ -340,7 +404,18 @@ const CommandDialogContent = () => { } if (group === "page") { return ( - + + ); + } + if (group === "shortcut") { + return ( + ); } group satisfies never; diff --git a/apps/builder/app/builder/shared/commands.ts b/apps/builder/app/builder/shared/commands.ts index a8b4283a16..ebefb4f156 100644 --- a/apps/builder/app/builder/shared/commands.ts +++ b/apps/builder/app/builder/shared/commands.ts @@ -36,6 +36,7 @@ const makeBreakpointCommand = ( number: number ): Command => ({ name, + hidden: true, defaultHotkeys: [`${number}`], disableHotkeyOnFormTags: true, disableHotkeyOnContentEditable: true, @@ -107,6 +108,7 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({ { name: "cancelCurrentDrag", + hidden: true, defaultHotkeys: ["escape"], // radix check event.defaultPrevented before invoking callbacks preventDefault: false, @@ -336,8 +338,11 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({ { name: "openCommandPanel", + hidden: true, defaultHotkeys: ["meta+k", "ctrl+k"], - handler: openCommandPanel, + handler: () => { + openCommandPanel(); + }, }, ], }); diff --git a/apps/builder/app/canvas/shared/commands.ts b/apps/builder/app/canvas/shared/commands.ts index db6cfad3c6..4cf022b652 100644 --- a/apps/builder/app/canvas/shared/commands.ts +++ b/apps/builder/app/canvas/shared/commands.ts @@ -23,6 +23,7 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({ commands: [ { name: "editInstanceText", + hidden: true, defaultHotkeys: ["enter"], // builder invokes command with custom hotkey setup disableHotkeyOutsideApp: true, @@ -56,6 +57,7 @@ export const { emitCommand, subscribeCommands } = createCommandsEmitter({ { name: "escapeSelection", + hidden: true, defaultHotkeys: ["escape"], // reset selection for canvas, but not for the builder disableHotkeyOutsideApp: true, diff --git a/apps/builder/app/shared/commands-emitter.ts b/apps/builder/app/shared/commands-emitter.ts index 5d1d86c116..68ee7c827a 100644 --- a/apps/builder/app/shared/commands-emitter.ts +++ b/apps/builder/app/shared/commands-emitter.ts @@ -22,6 +22,10 @@ type CommandMeta = { * event behavior is prevented **/ disableHotkeyOnContentEditable?: boolean; + /** + * hide the command in cmd+k panel + */ + hidden?: boolean; }; type CommandHandler = () => void; @@ -40,7 +44,7 @@ export type Command = CommandMeta & { /* * expose command metas to synchronize between builder, canvas and plugins */ -const $commandMetas = atom(new Map>()); +export const $commandMetas = atom(new Map>()); clientSyncStore.register("commandMetas", $commandMetas); // Copied from https://github.com/ai/keyux/blob/main/hotkey.js#L1C1-L2C1 diff --git a/packages/design-system/src/components/command.tsx b/packages/design-system/src/components/command.tsx index f302e661ea..410678be25 100644 --- a/packages/design-system/src/components/command.tsx +++ b/packages/design-system/src/components/command.tsx @@ -157,14 +157,26 @@ const CommandInputField = styled(CommandPrimitive.Input, { export const CommandInput = ( props: ComponentPropsWithoutRef ) => { + const inputRef = useRef(null); const action = useSelectedAction(); return ( { + // reset scroll whenever search is changed + requestAnimationFrame(() => { + inputRef.current + ?.closest("[cmdk-root]") + ?.querySelector("[data-radix-scroll-area-viewport]") + ?.scrollTo(0, 0); + }); + props.onValueChange?.(value); + }} />