experimental: add shortcuts group to command panel (#4434)

Ref https://github.com/webstudio-is/webstudio/issues/1696

Added most of our shortcuts to command panel.

<img width="482" alt="Screenshot 2024-11-17 at 17 28 55"
src="https://github.com/user-attachments/assets/347f877f-43e5-4ab7-9061-a1728d5c9df8">
This commit is contained in:
Bogdan Chadkin authored and GitHub committed 2024-11-29 20:20:19 +07:00
1 parent bbcdd2529c
commit 09364f654c
5 files changed
+112 -14

No files matched your search

@@ -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 (
<CommandGroup
name="component"
@@ -208,7 +210,11 @@ const getBreakpointLabel = (breakpoint: Breakpoint) => {
return `${breakpoint.label}: ${label}`;
};
const BreakpointGroup = ({ options }: { options: BreakpointOption[] }) => {
const BreakpointOptionsGroup = ({
options,
}: {
options: BreakpointOption[];
}) => {
return (
<CommandGroup
name="breakpoint"
@@ -262,7 +268,7 @@ const $pageOptions = computed(
}
);
const PageGroup = ({ options }: { options: PageOption[] }) => {
const PageOptionsGroup = ({ options }: { options: PageOption[] }) => {
const action = useSelectedAction();
return (
<CommandGroup
@@ -296,12 +302,66 @@ const PageGroup = ({ options }: { options: PageOption[] }) => {
);
};
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 (
<CommandGroup
name="shortcut"
heading={<CommandGroupHeading>Shortcuts</CommandGroupHeading>}
actions={["execute"]}
>
{options.map(({ name, label, keys }) => (
<CommandItem
key={name}
// preserve selected state when rerender
value={name}
onSelect={() => {
closeCommandPanel();
emitCommand(name as never);
}}
>
<Text variant="labelsTitleCase">{label}</Text>
<Kbd value={keys} />
</CommandItem>
))}
</CommandGroup>
);
};
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 (
<ComponentGroup
<ComponentOptionsGroup
key={group}
options={matches as ComponentOption[]}
/>
@@ -332,7 +396,7 @@ const CommandDialogContent = () => {
}
if (group === "breakpoint") {
return (
<BreakpointGroup
<BreakpointOptionsGroup
key={group}
options={matches as BreakpointOption[]}
/>
@@ -340,7 +404,18 @@ const CommandDialogContent = () => {
}
if (group === "page") {
return (
<PageGroup key={group} options={matches as PageOption[]} />
<PageOptionsGroup
key={group}
options={matches as PageOption[]}
/>
);
}
if (group === "shortcut") {
return (
<ShortcutOptionsGroup
key={group}
options={matches as ShortcutOption[]}
/>
);
}
group satisfies never;
+6 -1
View File
@@ -36,6 +36,7 @@ const makeBreakpointCommand = <CommandName extends string>(
number: number
): Command<CommandName> => ({
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();
},
},
],
});
@@ -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,
+5 -1
View File
@@ -22,6 +22,10 @@ type CommandMeta<CommandName extends string> = {
* 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<CommandName extends string> = CommandMeta<CommandName> & {
/*
* expose command metas to synchronize between builder, canvas and plugins
*/
const $commandMetas = atom(new Map<string, CommandMeta<string>>());
export const $commandMetas = atom(new Map<string, CommandMeta<string>>());
clientSyncStore.register("commandMetas", $commandMetas);
// Copied from https://github.com/ai/keyux/blob/main/hotkey.js#L1C1-L2C1
@@ -157,14 +157,26 @@ const CommandInputField = styled(CommandPrimitive.Input, {
export const CommandInput = (
props: ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
) => {
const inputRef = useRef<HTMLInputElement>(null);
const action = useSelectedAction();
return (
<CommandInputContainer>
<CommandInputIcon />
<CommandInputField
ref={inputRef}
autoFocus={true}
placeholder="Type a command or search..."
{...props}
onValueChange={(value) => {
// reset scroll whenever search is changed
requestAnimationFrame(() => {
inputRef.current
?.closest("[cmdk-root]")
?.querySelector("[data-radix-scroll-area-viewport]")
?.scrollTo(0, 0);
});
props.onValueChange?.(value);
}}
/>
<Text
variant="labelsTitleCase"