CMDPalette: Migrate to new ui for modals

This commit is contained in:
thororen1234 2026-05-19 22:59:44 -04:00
parent 64ad8627e1
commit 7f65b1b395
No known key found for this signature in database
4 changed files with 210 additions and 189 deletions

View file

@ -4,11 +4,12 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
import { Heading } from "@components/Heading";
import { CogWheel } from "@components/Icons";
import { classNameFactory } from "@utils/css";
import { classes } from "@utils/misc";
import { RenderModalProps } from "@vencord/discord-types";
import { Modal,openModal, TextInput, useCallback, useEffect, useMemo, useRef, useState } from "@webpack/common";
import { Modal, openModal, TextInput, useCallback, useEffect, useMemo, useRef, useState } from "@webpack/common";
import {
type CommandEntry, getCategoryGroupLabel, getCategoryWeight, getCommandSearchText, getRecentRank, getRegistryVersion, listCommands, subscribeRegistry
@ -64,6 +65,7 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
const [pickedIds, setPickedIds] = useState<string[]>(() => normalizedInitialSelected);
const itemRefs = useRef<(HTMLButtonElement | null)[]>([]);
const pickedIdsRef = useRef<string[]>([]);
const listRef = useRef<any>(null);
useEffect(() => {
if (providedCommands) return;
@ -142,7 +144,12 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
for (const entry of availableCommands) {
if (!includeHiddenInSearch && entry.hiddenInSearch) continue;
const searchText = getCommandSearchText(entry.id);
const searchText = getCommandSearchText(entry.id) || [
entry.label,
entry.description ?? "",
...entry.keywords ?? [],
...entry.tags ?? []
].filter(Boolean).join(" ").toLowerCase();
if (!searchText.includes(lowerQuery)) continue;
const score = scoreCommand(entry);
@ -203,8 +210,22 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
useEffect(() => {
if (selectedIndex < 0) return;
const rawContainer = listRef.current as any;
const container = rawContainer?.getScrollerNode ? rawContainer.getScrollerNode() : rawContainer;
const node = itemRefs.current[selectedIndex];
node?.scrollIntoView({ block: "nearest" });
if (!container || !node) return;
const containerRect = container.getBoundingClientRect();
const nodeRect = node.getBoundingClientRect();
if (nodeRect.bottom > containerRect.bottom) {
container.scrollTop += nodeRect.bottom - containerRect.bottom;
return;
}
if (nodeRect.top < containerRect.top) {
container.scrollTop -= containerRect.top - nodeRect.top;
}
}, [selectedIndex, items]);
itemRefs.current = itemRefs.current.slice(0, items.length);
@ -321,17 +342,26 @@ function CommandPickerModal({ modalProps, commands: providedCommands, allowMulti
};
return (
<Modal {...modalProps} size="sm" title="Command Picker">
<div className={classes(cl("shell"), cl("picker-shell"), cn("vc-command-palette"), cl("picker"))} onKeyDown={handleKeyDown}>
<div className={cl("input")}>
<TextInput
autoFocus
value={query}
onChange={setQuery}
placeholder="Search commands"
className={cl("main-input")}
/>
<Modal
{...modalProps}
size="lg"
scrollerRef={listRef}
title={
<div className={classes(cl("shell"), cl("picker-shell"), cn("vc-command-palette"), cl("picker"))} onKeyDown={handleKeyDown}>
<Heading tag="h1" className={cl("title")}>Command Palette</Heading>
<div className={cl("input")}>
<TextInput
autoFocus
value={query}
onChange={setQuery}
placeholder="Search commands"
/>
</div>
</div>
}
>
<div className={classes(cl("shell"), cl("picker-shell"), cn("vc-command-palette"), cl("picker"))} onKeyDown={handleKeyDown}>
<div className={cl("list")}>
{!hasCommands && <div className={cl("empty")}>{emptyStateText}</div>}
{items.map((item, index) => {

View file

@ -20,7 +20,11 @@ export interface RegistryStore {
getTreeCommandCache(): Map<string, CommandEntry[]>;
}
let instance: RegistryStore | null = null;
export function createRegistryStore(): RegistryStore {
if (instance) return instance;
let registryVersion = 0;
let cachedSortedCommands: CommandEntry[] | null = null;
const categoryCommandCache = new Map<string, CommandEntry[]>();
@ -38,7 +42,7 @@ export function createRegistryStore(): RegistryStore {
for (const listener of registryListeners) listener(registryVersion);
};
return {
instance = {
getRegistryVersion: () => registryVersion,
bumpRegistryVersion: () => {
registryVersion += 1;
@ -77,4 +81,6 @@ export function createRegistryStore(): RegistryStore {
getCategoryCommandCache: () => categoryCommandCache,
getTreeCommandCache: () => treeCommandCache
};
return instance;
}

View file

@ -6,9 +6,8 @@
/* Command Palette Modal Container */
.vc-command-palette {
--cp-bg: #0f0f10;
--cp-bg-secondary: #18181a;
--cp-bg-tertiary: #1f1f21;
--cp-bg-secondary: transparent;
--cp-bg-tertiary: transparent;
--cp-bg-dropdown: #0b0b0d;
--cp-bg-hover: #252528;
--cp-bg-selected: #2a2a2e;
@ -26,10 +25,6 @@
--cp-radius-sm: 8px;
--cp-shadow: 0 16px 44px rgb(0 0 0 / 38%), 0 6px 18px rgb(0 0 0 / 28%);
background: var(--cp-bg) !important;
border-radius: var(--cp-radius) !important;
box-shadow: var(--cp-shadow) !important;
border: 1px solid var(--cp-border) !important;
overflow: visible !important;
box-sizing: border-box;
will-change: opacity, transform;
@ -42,19 +37,19 @@
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background: var(--cp-bg);
border-radius: var(--cp-radius);
box-sizing: border-box;
position: relative;
overflow: hidden;
}
/* Input Area */
.vc-command-palette-input {
padding: 12px 22px 10px;
border-bottom: 1px solid var(--cp-border);
position: relative;
border-bottom: none;
position: sticky;
top: 0;
z-index: 10;
width: 105%;
box-sizing: border-box;
background: var(--bg-overlay-3, var(--background-secondary));
}
.vc-command-palette-main-input input {
@ -299,21 +294,8 @@
.vc-command-palette-list {
flex: 1 1 auto;
min-height: 0;
overflow-y: auto;
padding: 0;
}
.vc-command-palette-list::-webkit-scrollbar {
width: 6px;
}
.vc-command-palette-list::-webkit-scrollbar-track {
background: transparent;
}
.vc-command-palette-list::-webkit-scrollbar-thumb {
background: var(--cp-bg-tertiary);
border-radius: 3px;
width: 100%;
}
/* Section Labels */
@ -431,24 +413,18 @@
align-items: center;
justify-content: space-between;
padding: 10px 20px;
background: var(--cp-bg);
border-top: 1px solid var(--cp-border);
gap: 16px;
min-height: 48px;
}
.vc-command-palette-picker .vc-command-palette-shell {
max-height: 62vh;
position: sticky;
bottom: 0;
z-index: 10;
background: var(--bg-overlay-3, var(--background-secondary));
}
.vc-command-palette-picker .vc-command-palette-input {
padding: 12px 16px 10px;
}
.vc-command-palette-picker .vc-command-palette-list {
max-height: 54vh;
}
.vc-command-palette-action-bar-action {
display: flex;
align-items: center;
@ -516,11 +492,13 @@
.vc-command-palette-calculator {
padding: 12px 16px;
background: transparent;
border-bottom: 1px solid var(--cp-border);
border-bottom: none;
}
.vc-command-palette-calculator-title {
font-size: 13px;
font-size: 14px;
font-weight: 600;
color: var(--cp-text-secondary);
margin: 0 0 10px;
@ -1023,4 +1001,24 @@ input.vc-command-palette-main-search-input:read-only,
input.vc-command-palette-main-search-input:not(:read-only),
.vc-command-palette-main-search-input input:not(:read-only) {
color: var(--cp-text-primary) !important;
}
.vc-command-palette-header {
display: flex;
flex-direction: column;
gap: 12px;
width: 100%;
}
.vc-command-palette-header-title {
color: var(--header-primary);
font-size: 20px;
font-weight: 600;
}
.vc-command-palette-header .vc-command-palette-input {
position: static;
border-bottom: none;
background: transparent;
padding: 0;
}

View file

@ -6,6 +6,7 @@
import "../style.css";
import { Heading } from "@components/Heading";
import { IS_MAC } from "@utils/constants";
import { classNameFactory, classNameToSelector } from "@utils/css";
import { copyWithToast } from "@utils/discord";
@ -347,24 +348,9 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: R
useEffect(() => {
return () => {
if (closeReasonRef.current === "explicit-root") {
clearPersistedNavigation();
return;
}
if (closeReasonRef.current === "programmatic") {
clearPersistedNavigation();
return;
}
if (navigationLevel.type === "category") {
persistedCategoryId = navigationLevel.categoryId;
return;
}
clearPersistedNavigation();
};
}, [navigationLevel]);
}, []);
useEffect(() => {
setIsActionsMenuOpen(false);
@ -630,7 +616,8 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: R
useEffect(() => {
if (selectedIndex < 0) return;
if (selectionSource !== "keyboard") return;
const container = listRef.current;
const rawContainer = listRef.current as any;
const container = rawContainer?.getScrollerNode ? rawContainer.getScrollerNode() : rawContainer;
const node = itemRefs.current[selectedIndex];
if (!container || !node) return;
@ -658,8 +645,10 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: R
setSelectedKey(null);
setKeyboardSelectedKey(null);
if (listRef.current) {
listRef.current.scrollTop = 0;
const rawContainer = listRef.current as any;
const container = rawContainer?.getScrollerNode ? rawContainer.getScrollerNode() : rawContainer;
if (container) {
container.scrollTop = 0;
}
}, [isPageOpen, trimmedQuery]);
@ -758,7 +747,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: R
setSelectedKey(null);
setKeyboardSelectedKey(null);
setQuery("");
setExpanded(false);
};
const setActivePageValue = (fieldKey: string, value: string) => {
@ -873,7 +861,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: R
setSelectedKey(null);
setKeyboardSelectedKey(null);
setQuery("");
setExpanded(false);
return true;
};
@ -972,7 +959,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: R
setSelectedKey(null);
setKeyboardSelectedKey(null);
setQuery("");
setExpanded(false);
};
const updateSelection = (index: number, item: PaletteCandidate) => {
@ -1045,7 +1031,6 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: R
clearPromptState();
setSelectedKey(null);
setQuery("");
setExpanded(false);
};
const onKeyDown = async (event: ReactKeyboardEvent<HTMLDivElement>) => {
@ -1439,121 +1424,123 @@ export function CommandPaletteModal({ modalProps, instanceKey }: { modalProps: R
<Modal
{...modalProps}
size="lg"
title="Command Palette"
scrollerRef={listRef}
title={
<div className={cn("vc-command-palette", cl("shell"))} onKeyDown={onKeyDown}>
<Heading tag="h1" className={cl("title")}>Command Palette</Heading>
{!isPageOpen && (
<CommandPaletteInput
inputRef={mainInputRef}
autoFocus={!activePromptCommand}
value={query}
onChange={value => {
if (activePromptCommand && value.length > 0) {
clearPromptState();
}
const next = value.trim();
setQuery(value);
if (next.length === 0) {
if (navigationLevel.type !== "root") {
goBack();
return;
}
}
}}
placeholder={mainInputPlaceholder}
readOnly={false}
>
{shouldRenderPromptPreview && promptCommand && (
<div
ref={promptContainerRef}
className={classes(cl("query-preview"), !shouldShowPromptCommandLabel && cl("query-preview-without-label"))}
>
{shouldShowPromptCommandLabel && (
<span className={cl("query-preview-command-label")}>{promptCommand.label}</span>
)}
<div className={cl("query-preview-prompt")} style={queryPreviewPromptStyle}>
{activePromptCommand ? (
<div
className={classes(cl("query-prompt-capsule"), cl("query-prompt-capsule-active"))}
style={promptChipStyle}
onMouseDown={() => setShowPromptDropdown(true)}
>
<TextInput
ref={promptInputRef}
className={cl("prompt-active-input")}
value={promptInputValue}
onChange={value => {
setPromptInputValue(value);
setShowPromptDropdown(true);
if (selectedPromptCandidateId) {
setSelectedPromptCandidateId(null);
}
}}
placeholder={selectedPromptCandidate ? "" : (promptCommand.queryPlaceholder ?? "Action")}
onFocus={() => setShowPromptDropdown(true)}
onBlur={() => {
window.setTimeout(() => {
const active = document.activeElement;
if (promptContainerRef.current?.contains(active)) return;
setShowPromptDropdown(false);
if (selectedPromptCandidateId || promptInputValue.trim().length > 0) return;
clearPromptState();
}, 0);
}}
/>
</div>
) : (
<button
type="button"
className={cl("query-prompt-capsule")}
style={promptChipStyle}
onMouseDown={event => event.preventDefault()}
onClick={() => activatePromptCommand(promptCommand, true)}
>
<span className={cl("query-prompt-capsule-label")}>{promptCommand.queryPlaceholder ?? "Action"}</span>
</button>
)}
{shouldShowPromptDropdown && (
<PaletteDropdown
className={cl("query-preview-dropdown")}
suggestions={promptDropdownSuggestions}
highlightedIndex={promptSelectedSuggestionIndex}
onHover={index => {
const candidate = promptDropdownSuggestions[index];
if (!candidate) return;
setSelectedPromptCandidateId(candidate.id);
}}
onPick={candidate => {
setSelectedPromptCandidateId(candidate.id);
if (activePromptIsSingleSelect) {
setPromptInputValue(candidate.label);
} else {
setPromptInputValue("");
}
if (activePromptIsSingleSelect) {
setShowPromptDropdown(false);
}
setFocusPromptInput(true);
}}
/>
)}
</div>
</div>
)}
</CommandPaletteInput>
)}
{!isPageOpen && calculatorResult && (
<CommandPaletteCalculatorCards result={calculatorResult} mode={calculatorViewMode} />
)}
</div>
}
>
<div className={cn("vc-command-palette", cl("shell"))} onKeyDown={onKeyDown}>
{!isPageOpen && (
<CommandPaletteInput
inputRef={mainInputRef}
autoFocus={!activePromptCommand}
value={query}
onChange={value => {
if (activePromptCommand && value.length > 0) {
clearPromptState();
}
const next = value.trim();
setQuery(value);
if (next.length > 0) {
setExpanded(true);
} else {
if (navigationLevel.type !== "root") {
goBack();
return;
}
setExpanded(false);
}
}}
placeholder={mainInputPlaceholder}
readOnly={false}
>
{shouldRenderPromptPreview && promptCommand && (
<div
ref={promptContainerRef}
className={classes(cl("query-preview"), !shouldShowPromptCommandLabel && cl("query-preview-without-label"))}
>
{shouldShowPromptCommandLabel && (
<span className={cl("query-preview-command-label")}>{promptCommand.label}</span>
)}
<div className={cl("query-preview-prompt")} style={queryPreviewPromptStyle}>
{activePromptCommand ? (
<div
className={classes(cl("query-prompt-capsule"), cl("query-prompt-capsule-active"))}
style={promptChipStyle}
onMouseDown={() => setShowPromptDropdown(true)}
>
<TextInput
ref={promptInputRef}
className={cl("prompt-active-input")}
value={promptInputValue}
onChange={value => {
setPromptInputValue(value);
setShowPromptDropdown(true);
if (selectedPromptCandidateId) {
setSelectedPromptCandidateId(null);
}
}}
placeholder={selectedPromptCandidate ? "" : (promptCommand.queryPlaceholder ?? "Action")}
onFocus={() => setShowPromptDropdown(true)}
onBlur={() => {
window.setTimeout(() => {
const active = document.activeElement;
if (promptContainerRef.current?.contains(active)) return;
setShowPromptDropdown(false);
if (selectedPromptCandidateId || promptInputValue.trim().length > 0) return;
clearPromptState();
}, 0);
}}
/>
</div>
) : (
<button
type="button"
className={cl("query-prompt-capsule")}
style={promptChipStyle}
onMouseDown={event => event.preventDefault()}
onClick={() => activatePromptCommand(promptCommand, true)}
>
<span className={cl("query-prompt-capsule-label")}>{promptCommand.queryPlaceholder ?? "Action"}</span>
</button>
)}
{shouldShowPromptDropdown && (
<PaletteDropdown
className={cl("query-preview-dropdown")}
suggestions={promptDropdownSuggestions}
highlightedIndex={promptSelectedSuggestionIndex}
onHover={index => {
const candidate = promptDropdownSuggestions[index];
if (!candidate) return;
setSelectedPromptCandidateId(candidate.id);
}}
onPick={candidate => {
setSelectedPromptCandidateId(candidate.id);
if (activePromptIsSingleSelect) {
setPromptInputValue(candidate.label);
} else {
setPromptInputValue("");
}
if (activePromptIsSingleSelect) {
setShowPromptDropdown(false);
}
setFocusPromptInput(true);
}}
/>
)}
</div>
</div>
)}
</CommandPaletteInput>
)}
{!isPageOpen && calculatorResult && (
<CommandPaletteCalculatorCards result={calculatorResult} mode={calculatorViewMode} />
)}
{!isPageOpen && (
<div ref={listRef} className={cl("list")}>
<div className={cl("list")}>
{!hasCommandItems && <div className={cl("empty")}>{emptyStateText}</div>}
{items.map((item, index) => {
if (item.type === "section") {