From f465386ed81f9c5b1b12289c0ee65edb63937e4c Mon Sep 17 00:00:00 2001 From: kubrickcode Date: Sun, 30 Nov 2025 04:50:30 +0000 Subject: [PATCH] feat(view): add icon picker component for visual icon selection Previously users had to manually type icon syntax like $(terminal) This required knowing icon names, creating poor UX for new users - Add IconPicker component with search and grid selection for 510 VS Code codicons - Separate Command Name field into IconPicker + Display Text (UI only, no schema change) - Auto-combine icon and text into $(icon) format internally - Implement unified field UX with focus-within styling - Add 18 e2e tests for icon picker functionality fix #185 --- src/view/e2e/icon-picker.spec.ts | 317 +++++++++++ src/view/src/components/command-form.tsx | 50 +- src/view/src/components/icon-picker.tsx | 160 ++++++ src/view/src/data/codicons.ts | 522 ++++++++++++++++++ src/view/src/i18n/locales/en.json | 12 + src/view/src/i18n/locales/ko.json | 12 + src/view/src/style.css | 6 +- src/view/src/utils/parse-vscode-icon-name.tsx | 2 +- 8 files changed, 1070 insertions(+), 11 deletions(-) create mode 100644 src/view/e2e/icon-picker.spec.ts create mode 100644 src/view/src/components/icon-picker.tsx create mode 100644 src/view/src/data/codicons.ts diff --git a/src/view/e2e/icon-picker.spec.ts b/src/view/e2e/icon-picker.spec.ts new file mode 100644 index 00000000..ed56f196 --- /dev/null +++ b/src/view/e2e/icon-picker.spec.ts @@ -0,0 +1,317 @@ +import { expect, test, type Page } from "@playwright/test"; +import { + clearAllCommands, + fillCommandForm, + openAddCommandDialog, + saveCommandDialog, +} from "./helpers/test-helpers"; + +const TEST_COMMAND = { + name: "Icon Picker Test", + command: "echo 'icon picker test'", +}; + +const openIconPicker = async (page: Page) => { + await page.getByRole("button", { name: /open icon picker/i }).click(); +}; + +const getIconPickerPopover = (page: Page) => { + return page.getByTestId("icon-picker-popover"); +}; + +const getIconSearchInput = (page: Page) => { + return page.getByPlaceholder(/search icons/i); +}; + +const getIconPickerButton = (page: Page) => { + return page.getByRole("button", { name: /open icon picker/i }); +}; + +const getDisplayTextInput = (page: Page) => { + return page.getByPlaceholder(/terminal.*build.*deploy/i); +}; + +const getIconGridButtons = (page: Page) => { + return page.getByTestId("icon-grid-button"); +}; + +const getIconByName = (page: Page, iconName: string) => { + return page.locator(`button[title='${iconName}']`); +}; + +const getIconsContaining = (page: Page, text: string) => { + return page.locator(`button[title*='${text}']`); +}; + +test.describe("Icon Picker Component", () => { + test.beforeEach(async ({ page }) => { + await page.goto("/"); + await clearAllCommands(page); + }); + + test("should open icon picker popover when clicking the icon button", async ({ page }) => { + // Given: Open add command dialog + await openAddCommandDialog(page); + + // When: Click the icon picker button + await openIconPicker(page); + + // Then: Icon picker popover should be visible + const popover = getIconPickerPopover(page); + await expect(popover).toBeVisible(); + }); + + test("should close icon picker popover when clicking outside", async ({ page }) => { + // Given: Open add command dialog and icon picker + await openAddCommandDialog(page); + await openIconPicker(page); + await expect(getIconPickerPopover(page)).toBeVisible(); + + // When: Click outside the popover + await page.getByLabel(/command name/i).click(); + + // Then: Popover should be hidden + await expect(getIconPickerPopover(page)).toBeHidden(); + }); + + test("should display icons in a grid", async ({ page }) => { + // Given: Open add command dialog and icon picker + await openAddCommandDialog(page); + await openIconPicker(page); + + // Then: Should have icon buttons visible + const iconButtons = getIconGridButtons(page); + await expect(iconButtons.first()).toBeVisible(); + + // And: Should have multiple icons (at least 50 initially shown) + const count = await iconButtons.count(); + expect(count).toBeGreaterThan(50); + }); + + test("should filter icons when searching", async ({ page }) => { + // Given: Open add command dialog and icon picker + await openAddCommandDialog(page); + await openIconPicker(page); + const initialIconCount = await getIconGridButtons(page).count(); + + // When: Type search query + await getIconSearchInput(page).fill("terminal"); + + // Then: Should show filtered icons containing "terminal" + const iconButtons = getIconsContaining(page, "terminal"); + await expect(iconButtons.first()).toBeVisible(); + + // And: Count should be reduced compared to initial + const count = await iconButtons.count(); + expect(count).toBeGreaterThan(0); + expect(count).toBeLessThan(initialIconCount); + }); + + test("should show 'No icons found' when search has no results", async ({ page }) => { + // Given: Open add command dialog and icon picker + await openAddCommandDialog(page); + await openIconPicker(page); + + // When: Type search query with no matches + await getIconSearchInput(page).fill("xyznonexistent"); + + // Then: Should show no icons found message + await expect(page.getByText(/no icons found/i)).toBeVisible(); + }); + + test("should select icon and show in picker button", async ({ page }) => { + // Given: Open add command dialog and icon picker + await openAddCommandDialog(page); + await openIconPicker(page); + + // When: Search for terminal and click the terminal icon + await getIconSearchInput(page).fill("terminal"); + await getIconByName(page, "terminal").click(); + + // Then: Popover should close + await expect(getIconPickerPopover(page)).toBeHidden(); + + // And: Icon picker button should show the selected icon + const iconButton = getIconPickerButton(page); + await expect(iconButton.locator(".codicon-terminal")).toBeVisible(); + }); + + test("should replace existing icon when selecting new icon", async ({ page }) => { + // Given: Open add command dialog and select an icon first + await openAddCommandDialog(page); + await openIconPicker(page); + await getIconSearchInput(page).fill("folder"); + await getIconByName(page, "folder").click(); + + // Add display text + await getDisplayTextInput(page).fill("My Command"); + + // When: Open icon picker and select a different icon + await openIconPicker(page); + await getIconSearchInput(page).fill("terminal"); + await getIconByName(page, "terminal").click(); + + // Then: Icon picker button should show the new icon + const iconButton = getIconPickerButton(page); + await expect(iconButton.locator(".codicon-terminal")).toBeVisible(); + + // And: Display text should remain unchanged + await expect(getDisplayTextInput(page)).toHaveValue("My Command"); + }); + + test("should show selected icon in the picker button", async ({ page }) => { + // Given: Open add command dialog + await openAddCommandDialog(page); + + // When: Select an icon via picker + await openIconPicker(page); + await getIconSearchInput(page).fill("terminal"); + await getIconByName(page, "terminal").click(); + + // Then: Icon picker button should show the terminal icon + const iconButton = getIconPickerButton(page); + await expect(iconButton.locator(".codicon-terminal")).toBeVisible(); + }); + + test("should show 'Icon' label when no icon is selected", async ({ page }) => { + // Given: Open add command dialog + await openAddCommandDialog(page); + + // Then: Icon picker button should show "Icon" label (making purpose clear) + const iconButton = getIconPickerButton(page); + await expect(iconButton).toContainText(/icon/i); + }); + + test("should save command with selected icon", async ({ page }) => { + // Given: Open add command dialog + await openAddCommandDialog(page); + + // When: Select an icon and fill form + await openIconPicker(page); + await getIconSearchInput(page).fill("rocket"); + await getIconByName(page, "rocket").click(); + + // Fill in display text (separate from icon) + await getDisplayTextInput(page).fill("Launch"); + + await fillCommandForm(page, { + command: TEST_COMMAND.command, + }); + await saveCommandDialog(page); + + // Then: Command should be saved with icon + const commandCard = page.locator('[data-testid="command-card"]', { + hasText: "Launch", + }); + await expect(commandCard).toBeVisible(); + + // And: Icon should be visible in the card + await expect(commandCard.locator(".codicon-rocket")).toBeVisible(); + }); + + test("should preserve icon when editing command", async ({ page }) => { + // Given: Create a command with icon + await openAddCommandDialog(page); + await openIconPicker(page); + await getIconSearchInput(page).fill("debug"); + await getIconByName(page, "debug").click(); + + // Fill display text (separate input) + await getDisplayTextInput(page).fill("Debug Test"); + await fillCommandForm(page, { + command: "echo 'debug'", + }); + await saveCommandDialog(page); + + // When: Open edit dialog + const commandCard = page.locator('[data-testid="command-card"]', { + hasText: "Debug Test", + }); + await commandCard.getByRole("button", { name: /edit/i }).click(); + + // Then: Display text input should have the text + await expect(getDisplayTextInput(page)).toHaveValue("Debug Test"); + + // And: Icon picker button should show the debug icon + await expect(getIconPickerButton(page).locator(".codicon-debug")).toBeVisible(); + }); + + test("should close popover with Escape key", async ({ page }) => { + // Given: Open add command dialog and icon picker + await openAddCommandDialog(page); + await openIconPicker(page); + await expect(getIconPickerPopover(page)).toBeVisible(); + + // When: Press Escape key + await page.keyboard.press("Escape"); + + // Then: Popover should be hidden + await expect(getIconPickerPopover(page)).toBeHidden(); + }); + + test("should show 'Show all' button when more than 100 icons available", async ({ page }) => { + // Given: Open add command dialog and icon picker + await openAddCommandDialog(page); + await openIconPicker(page); + + // Then: Should show 'Show all' button + const showAllButton = page.getByRole("button", { name: /show all.*icons/i }); + await expect(showAllButton).toBeVisible(); + }); + + test("should show all icons when clicking 'Show all' button", async ({ page }) => { + // Given: Open add command dialog and icon picker + await openAddCommandDialog(page); + await openIconPicker(page); + + // Get initial icon count + const initialCount = await getIconGridButtons(page).count(); + + // When: Click 'Show all' button + await page.getByRole("button", { name: /show all.*icons/i }).click(); + + // Then: Should show more icons + const finalCount = await getIconGridButtons(page).count(); + expect(finalCount).toBeGreaterThan(initialCount); + + // And: 'Show all' button should be hidden + await expect(page.getByRole("button", { name: /show all.*icons/i })).toBeHidden(); + }); + + test("should highlight currently selected icon in the grid", async ({ page }) => { + // Given: Open add command dialog and select an icon + await openAddCommandDialog(page); + await openIconPicker(page); + await getIconSearchInput(page).fill("terminal"); + await getIconByName(page, "terminal").click(); + + // When: Open icon picker again + await openIconPicker(page); + await getIconSearchInput(page).fill("terminal"); + + // Then: Terminal icon button should have selected styling (bg-accent class) + const terminalButton = getIconByName(page, "terminal"); + await expect(terminalButton).toHaveClass(/bg-accent/); + }); + + test("should allow creating command with only display text (no icon)", async ({ page }) => { + // Given: Open add command dialog + await openAddCommandDialog(page); + + // When: Only fill display text without selecting an icon + await getDisplayTextInput(page).fill("No Icon Button"); + await fillCommandForm(page, { + command: "echo 'test'", + }); + await saveCommandDialog(page); + + // Then: Command should be saved without icon + const commandCard = page.locator('[data-testid="command-card"]', { + hasText: "No Icon Button", + }); + await expect(commandCard).toBeVisible(); + + // And: No codicon should be visible in the card + await expect(commandCard.locator(".codicon")).toBeHidden(); + }); +}); diff --git a/src/view/src/components/command-form.tsx b/src/view/src/components/command-form.tsx index b49de37a..66decd24 100644 --- a/src/view/src/components/command-form.tsx +++ b/src/view/src/components/command-form.tsx @@ -20,6 +20,9 @@ import { } from "~/core"; import { ColorInput } from "./color-input"; +import { GroupCommandEditor } from "./group-command-editor"; +import { GroupToSingleWarningDialog } from "./group-to-single-warning-dialog"; +import { IconPicker } from "./icon-picker"; import { createCommandFormSchema } from "../schemas/command-form-schema"; import { type ButtonConfig, @@ -28,8 +31,7 @@ import { toCommandButton, toGroupButton, } from "../types"; -import { GroupCommandEditor } from "./group-command-editor"; -import { GroupToSingleWarningDialog } from "./group-to-single-warning-dialog"; +import { parseVSCodeIconName } from "../utils/parse-vscode-icon-name"; type CommandFormProps = { command?: (ButtonConfig & { index?: number }) | null; @@ -124,16 +126,48 @@ export const CommandForm = ({ command, commands, formId, onSave }: CommandFormPr setIsGroupMode(value === "group"); }; + // Parse initial name into icon and display text + const initialParsed = useMemo(() => { + const name = command?.name || ""; + return parseVSCodeIconName(name); + }, [command?.name]); + + const [selectedIcon, setSelectedIcon] = useState(initialParsed.iconName); + const [displayText, setDisplayText] = useState(initialParsed.displayText); + + const getCombinedName = (icon?: string, text?: string): string => { + if (icon) { + return `$(${icon})${text ? ` ${text}` : ""}`; + } + return text || ""; + }; + + const handleIconChange = (icon: string | undefined) => { + setSelectedIcon(icon); + setValue("name", getCombinedName(icon, displayText)); + }; + + const handleDisplayTextChange = (e: React.ChangeEvent) => { + const newText = e.target.value; + setDisplayText(newText); + setValue("name", getCombinedName(selectedIcon, newText)); + }; + return (
- {t("commandForm.commandName")} - + {t("commandForm.commandName")} +
+ + +
{errors.name &&

{errors.name.message}

}
diff --git a/src/view/src/components/icon-picker.tsx b/src/view/src/components/icon-picker.tsx new file mode 100644 index 00000000..721abd5e --- /dev/null +++ b/src/view/src/components/icon-picker.tsx @@ -0,0 +1,160 @@ +import { ChevronDown, Search, Sparkles } from "lucide-react"; +import { useMemo, useState } from "react"; +import { useTranslation } from "react-i18next"; + +import { Button, Input, Popover, PopoverContent, PopoverTrigger } from "~/core"; +import { cn } from "~/core/shadcn/utils"; + +import { CODICONS } from "../data/codicons"; +import { VSCodeIcon } from "../utils/parse-vscode-icon-name"; + +type IconPickerProps = { + className?: string; + onChange: (iconName: string | undefined) => void; + value?: string; +}; + +const ICONS_PER_PAGE = 100; +const GRID_COLUMNS = 8; + +export const IconPicker = ({ className, onChange, value }: IconPickerProps) => { + const { t } = useTranslation(); + const [isOpen, setIsOpen] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); + const [showAll, setShowAll] = useState(false); + + const filteredIcons = useMemo(() => { + if (!searchQuery) { + return CODICONS; + } + const query = searchQuery.toLowerCase(); + return CODICONS.filter((icon) => icon.toLowerCase().includes(query)); + }, [searchQuery]); + + const visibleIcons = useMemo(() => { + if (showAll || searchQuery) { + return filteredIcons; + } + return filteredIcons.slice(0, ICONS_PER_PAGE); + }, [filteredIcons, showAll, searchQuery]); + + const hasMore = !showAll && !searchQuery && filteredIcons.length > ICONS_PER_PAGE; + + const handleIconClick = (iconName: string) => { + onChange(iconName); + setIsOpen(false); + setSearchQuery(""); + setShowAll(false); + }; + + const handleShowMore = () => { + setShowAll(true); + }; + + const handleClearIcon = (e: React.MouseEvent) => { + e.stopPropagation(); + onChange(undefined); + }; + + return ( + + + + + +
+
+ + setSearchQuery(e.target.value)} + placeholder={t("iconPicker.searchPlaceholder")} + value={searchQuery} + /> +
+ +
+
+ {visibleIcons.map((iconName) => ( + + ))} +
+ + {visibleIcons.length === 0 && ( +
+ {t("iconPicker.noIconsFound")} +
+ )} + + {hasMore && ( +
+ +
+ )} +
+ + {value && ( +
+ + {t("iconPicker.selected")}: {value} + + +
+ )} +
+
+
+ ); +}; diff --git a/src/view/src/data/codicons.ts b/src/view/src/data/codicons.ts new file mode 100644 index 00000000..29fa1848 --- /dev/null +++ b/src/view/src/data/codicons.ts @@ -0,0 +1,522 @@ +/** + * Complete list of VS Code Codicons + * + * This list is extracted from @vscode/codicons package. + * These icon names can be used in button names with the syntax: $(icon-name) + * + * @see https://microsoft.github.io/vscode-codicons/dist/codicon.html + */ +export const CODICONS = [ + "account", + "activate-breakpoints", + "add", + "agent", + "alert", + "archive", + "array", + "arrow-both", + "arrow-circle-down", + "arrow-circle-left", + "arrow-circle-right", + "arrow-circle-up", + "arrow-down", + "arrow-left", + "arrow-right", + "arrow-small-down", + "arrow-small-left", + "arrow-small-right", + "arrow-small-up", + "arrow-swap", + "arrow-up", + "attach", + "azure", + "azure-devops", + "beaker", + "beaker-stop", + "bell", + "bell-dot", + "bell-slash", + "bell-slash-dot", + "bold", + "book", + "bookmark", + "bracket", + "bracket-dot", + "bracket-error", + "briefcase", + "broadcast", + "browser", + "bug", + "calendar", + "call-incoming", + "call-outgoing", + "case-sensitive", + "check", + "check-all", + "checklist", + "chevron-down", + "chevron-left", + "chevron-right", + "chevron-up", + "chip", + "circle", + "circle-filled", + "circle-large", + "circle-large-filled", + "circle-large-outline", + "circle-outline", + "circle-slash", + "circle-small", + "circle-small-filled", + "circuit-board", + "clear-all", + "clippy", + "clock", + "clone", + "close", + "close-all", + "close-dirty", + "cloud", + "cloud-download", + "cloud-upload", + "code", + "coffee", + "collapse-all", + "color-mode", + "combine", + "comment", + "comment-add", + "comment-discussion", + "comment-draft", + "comment-unresolved", + "compass", + "compass-active", + "compass-dot", + "console", + "copilot", + "copy", + "credit-card", + "dash", + "dashboard", + "database", + "debug", + "debug-all", + "debug-alt", + "debug-alt-small", + "debug-breakpoint", + "debug-breakpoint-conditional", + "debug-breakpoint-conditional-disabled", + "debug-breakpoint-conditional-unverified", + "debug-breakpoint-data", + "debug-breakpoint-data-disabled", + "debug-breakpoint-data-unverified", + "debug-breakpoint-disabled", + "debug-breakpoint-function", + "debug-breakpoint-function-disabled", + "debug-breakpoint-function-unverified", + "debug-breakpoint-log", + "debug-breakpoint-log-disabled", + "debug-breakpoint-log-unverified", + "debug-breakpoint-unsupported", + "debug-breakpoint-unverified", + "debug-console", + "debug-continue", + "debug-continue-small", + "debug-coverage", + "debug-disconnect", + "debug-hint", + "debug-line-by-line", + "debug-pause", + "debug-rerun", + "debug-restart", + "debug-restart-frame", + "debug-reverse-continue", + "debug-stackframe", + "debug-stackframe-active", + "debug-stackframe-dot", + "debug-stackframe-focused", + "debug-start", + "debug-step-back", + "debug-step-into", + "debug-step-out", + "debug-step-over", + "debug-stop", + "desktop-download", + "device-camera", + "device-camera-video", + "device-desktop", + "device-mobile", + "diff", + "diff-added", + "diff-ignored", + "diff-modified", + "diff-removed", + "diff-renamed", + "discard", + "edit", + "editor-layout", + "ellipsis", + "empty-window", + "error", + "error-small", + "exclude", + "expand-all", + "export", + "extensions", + "eye", + "eye-closed", + "eye-unwatch", + "eye-watch", + "feedback", + "file", + "file-add", + "file-binary", + "file-code", + "file-directory", + "file-directory-create", + "file-media", + "file-pdf", + "file-submodule", + "file-symlink-directory", + "file-symlink-file", + "file-zip", + "files", + "filter", + "filter-filled", + "flame", + "fold", + "fold-down", + "fold-up", + "folder", + "folder-active", + "folder-library", + "folder-opened", + "game", + "gear", + "gift", + "gist", + "gist-fork", + "gist-new", + "gist-private", + "gist-secret", + "git-commit", + "git-compare", + "git-fork-private", + "git-merge", + "git-pull-request", + "git-pull-request-abandoned", + "git-pull-request-closed", + "git-pull-request-create", + "git-pull-request-draft", + "git-pull-request-go-to-changes", + "git-pull-request-label", + "git-pull-request-new-changes", + "github", + "github-action", + "github-alt", + "github-inverted", + "globe", + "go-to-file", + "go-to-search", + "grabber", + "graph", + "graph-left", + "graph-line", + "graph-scatter", + "gripper", + "group-by-ref-type", + "heart", + "heart-filled", + "history", + "home", + "horizontal-rule", + "hubot", + "inbox", + "info", + "insert", + "inspect", + "issue-closed", + "issue-draft", + "issue-opened", + "issue-reopened", + "issues", + "italic", + "jersey", + "json", + "kebab-horizontal", + "kebab-vertical", + "key", + "keyboard", + "law", + "layers", + "layers-active", + "layers-dot", + "layout", + "layout-activitybar-left", + "layout-activitybar-right", + "layout-centered", + "layout-menubar", + "layout-panel", + "layout-panel-center", + "layout-panel-justify", + "layout-panel-left", + "layout-panel-off", + "layout-panel-right", + "layout-sidebar-left", + "layout-sidebar-left-off", + "layout-sidebar-right", + "layout-sidebar-right-off", + "layout-statusbar", + "library", + "lightbulb", + "lightbulb-autofix", + "lightbulb-sparkle", + "link", + "link-external", + "list-filter", + "list-flat", + "list-ordered", + "list-selection", + "list-tree", + "list-unordered", + "live-share", + "loading", + "location", + "lock", + "lock-small", + "log-in", + "log-out", + "magnet", + "mail", + "mail-read", + "mail-reply", + "map", + "map-filled", + "map-vertical", + "map-vertical-filled", + "markdown", + "megaphone", + "mention", + "menu", + "merge", + "microscope", + "milestone", + "mirror", + "mirror-private", + "mirror-public", + "mortar-board", + "move", + "multiple-windows", + "mute", + "new-file", + "new-folder", + "newline", + "no-newline", + "note", + "notebook", + "notebook-template", + "octoface", + "open-preview", + "organization", + "organization-filled", + "organization-outline", + "output", + "package", + "paintcan", + "pass", + "pass-filled", + "pencil", + "person", + "person-add", + "person-filled", + "person-follow", + "person-outline", + "pie-chart", + "pin", + "pinned", + "pinned-dirty", + "play", + "play-circle", + "plug", + "plus", + "preserve-case", + "preview", + "primitive-dot", + "primitive-square", + "project", + "pulse", + "question", + "quote", + "radio-tower", + "reactions", + "record", + "record-keys", + "record-small", + "redo", + "references", + "refresh", + "regex", + "remote", + "remote-explorer", + "remove", + "remove-close", + "repl", + "replace", + "replace-all", + "reply", + "repo", + "repo-clone", + "repo-create", + "repo-delete", + "repo-force-push", + "repo-forked", + "repo-pull", + "repo-push", + "repo-sync", + "report", + "request-changes", + "rocket", + "root-folder", + "root-folder-opened", + "rss", + "ruby", + "run-above", + "run-all", + "run-below", + "run-coverage", + "run-errors", + "save", + "save-all", + "save-as", + "screen-full", + "screen-normal", + "search", + "search-fuzzy", + "search-save", + "search-stop", + "selection", + "send", + "server", + "server-environment", + "server-process", + "settings", + "settings-gear", + "shield", + "sign-in", + "sign-out", + "smiley", + "sort-precedence", + "source-control", + "sparkle", + "sparkle-filled", + "split-horizontal", + "split-vertical", + "squirrel", + "star", + "star-add", + "star-delete", + "star-empty", + "star-full", + "star-half", + "stop-circle", + "symbol-array", + "symbol-boolean", + "symbol-class", + "symbol-color", + "symbol-constant", + "symbol-constructor", + "symbol-enum", + "symbol-enum-member", + "symbol-event", + "symbol-field", + "symbol-file", + "symbol-folder", + "symbol-interface", + "symbol-key", + "symbol-keyword", + "symbol-method", + "symbol-misc", + "symbol-namespace", + "symbol-null", + "symbol-number", + "symbol-numeric", + "symbol-object", + "symbol-operator", + "symbol-package", + "symbol-parameter", + "symbol-property", + "symbol-ruler", + "symbol-snippet", + "symbol-string", + "symbol-structure", + "symbol-type-parameter", + "symbol-unit", + "symbol-value", + "symbol-variable", + "sync", + "sync-ignored", + "table", + "tag", + "tag-add", + "tag-remove", + "target", + "tasklist", + "telescope", + "terminal", + "terminal-bash", + "terminal-cmd", + "terminal-debian", + "terminal-decoration-error", + "terminal-decoration-incomplete", + "terminal-decoration-mark", + "terminal-decoration-success", + "terminal-linux", + "terminal-powershell", + "terminal-tmux", + "terminal-ubuntu", + "text-size", + "three-bars", + "thumbsdown", + "thumbsup", + "tools", + "trash", + "trashcan", + "triangle-down", + "triangle-left", + "triangle-right", + "triangle-up", + "twitter", + "type-hierarchy", + "type-hierarchy-sub", + "type-hierarchy-super", + "unfold", + "ungroup-by-ref-type", + "unlock", + "unmute", + "unverified", + "variable-group", + "verified", + "verified-filled", + "versions", + "vm", + "vm-active", + "vm-connect", + "vm-outline", + "vm-running", + "wand", + "warning", + "watch", + "whitespace", + "whole-word", + "window", + "workspace-trusted", + "workspace-unknown", + "workspace-untrusted", + "wrench", + "wrench-subaction", + "x", + "zoom-in", + "zoom-out", +] as const; + +export type CodiconName = (typeof CODICONS)[number]; diff --git a/src/view/src/i18n/locales/en.json b/src/view/src/i18n/locales/en.json index a1f4009d..75927d80 100644 --- a/src/view/src/i18n/locales/en.json +++ b/src/view/src/i18n/locales/en.json @@ -29,6 +29,7 @@ "commandForm": { "commandName": "Command Name", "commandNamePlaceholder": "e.g., $(terminal) Terminal", + "displayTextPlaceholder": "e.g., Terminal, Build, Deploy", "commandType": "Command Type", "singleCommand": "Single Command", "groupCommands": "Group Commands", @@ -198,6 +199,17 @@ "dontSave": "Don't Save", "saveAndSwitch": "Save & Switch" }, + "iconPicker": { + "openPicker": "Open icon picker", + "addIconPrefix": "Add icon (optional)", + "label": "Icon", + "searchPlaceholder": "Search icons...", + "selectIcon": "Select icon {{name}}", + "noIconsFound": "No icons found", + "showAll": "Show all {{count}} icons", + "selected": "Selected", + "clear": "Clear" + }, "buttonSets": { "default": "Default", "selector": "Button Set", diff --git a/src/view/src/i18n/locales/ko.json b/src/view/src/i18n/locales/ko.json index 6cf7187c..e0d1764f 100644 --- a/src/view/src/i18n/locales/ko.json +++ b/src/view/src/i18n/locales/ko.json @@ -29,6 +29,7 @@ "commandForm": { "commandName": "명령 이름", "commandNamePlaceholder": "예: $(terminal) 터미널", + "displayTextPlaceholder": "예: 터미널, 빌드, 배포", "commandType": "명령 유형", "singleCommand": "단일 명령", "groupCommands": "그룹 명령", @@ -198,6 +199,17 @@ "dontSave": "저장 안 함", "saveAndSwitch": "저장 후 전환" }, + "iconPicker": { + "openPicker": "아이콘 선택기 열기", + "addIconPrefix": "아이콘 추가 (optional)", + "label": "아이콘", + "searchPlaceholder": "아이콘 검색...", + "selectIcon": "{{name}} 아이콘 선택", + "noIconsFound": "아이콘을 찾을 수 없습니다", + "showAll": "{{count}}개 아이콘 모두 보기", + "selected": "선택됨", + "clear": "삭제" + }, "buttonSets": { "default": "기본", "selector": "버튼 세트", diff --git a/src/view/src/style.css b/src/view/src/style.css index a83812a9..ec1d6229 100644 --- a/src/view/src/style.css +++ b/src/view/src/style.css @@ -585,7 +585,8 @@ body { background-color: var(--background); } -.input-premium:focus { +.input-premium:focus, +.input-premium:focus-within { border-color: var(--accent); outline: none; box-shadow: @@ -594,7 +595,8 @@ body { inset 0 1px 0 oklch(1 0 0 / 0.03); } -.dark .input-premium:focus { +.dark .input-premium:focus, +.dark .input-premium:focus-within { box-shadow: 0 0 0 3px var(--focus-ring), 0 2px 4px oklch(0 0 0 / 0.2), diff --git a/src/view/src/utils/parse-vscode-icon-name.tsx b/src/view/src/utils/parse-vscode-icon-name.tsx index 677668bc..439483c7 100644 --- a/src/view/src/utils/parse-vscode-icon-name.tsx +++ b/src/view/src/utils/parse-vscode-icon-name.tsx @@ -26,7 +26,7 @@ export const parseVSCodeIconName = (text: string): ParseResult => { const iconName = rawIconName.replace("~spin", ""); return { - displayText: text.slice(match[0].length).trim() || iconName, + displayText: text.slice(match[0].length).trim(), iconName, spin, };