mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-11-09 05:26:24 +09:00
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:
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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user