diff --git a/i18n/english.js b/i18n/english.js index 64a7784c..d4cdfdf8 100644 --- a/i18n/english.js +++ b/i18n/english.js @@ -273,6 +273,9 @@ const ui = { placeholder: "Search packages...", placeholder_filter_hint: "or use", placeholder_refine: "Add another filter...", + section_actions: "Actions", + action_toggle_theme_to_dark: "Switch to dark theme", + action_toggle_theme_to_light: "Switch to light theme", section_presets: "Quick filters", preset_has_vulnerabilities: "Has vulnerabilities", preset_has_scripts: "Has install scripts", diff --git a/i18n/french.js b/i18n/french.js index 6e3acc1b..020f0977 100644 --- a/i18n/french.js +++ b/i18n/french.js @@ -273,6 +273,9 @@ const ui = { placeholder: "Rechercher des packages...", placeholder_filter_hint: "ou utiliser", placeholder_refine: "Ajouter un autre filtre...", + section_actions: "Actions", + action_toggle_theme_to_dark: "Passer en thème sombre", + action_toggle_theme_to_light: "Passer en thème clair", section_presets: "Filtres rapides", preset_has_vulnerabilities: "Contient des vulnérabilités", preset_has_scripts: "Scripts d'installation", diff --git a/public/components/search-command/search-command-panels.js b/public/components/command-palette/command-palette-panels.js similarity index 88% rename from public/components/search-command/search-command-panels.js rename to public/components/command-palette/command-palette-panels.js index 918e3650..465079bf 100644 --- a/public/components/search-command/search-command-panels.js +++ b/public/components/command-palette/command-palette-panels.js @@ -159,6 +159,32 @@ export function renderPresets({ presets, onApply }) { `; } +/** + * @param {{ actions: Array<{ id: string, label: string, kbd: string|null }>, onExecute: Function }} props + */ +export function renderActions({ actions, onExecute }) { + const i18n = window.i18n[currentLang()].search_command; + + return html` +
+
${i18n.section_actions}
+
+
+ ${actions.map((action) => html` + + `)} +
+
+
+ `; +} + /** * @param {{ results: Array, selectedIndex: number, helperCount: number, onFocus: Function }} props */ diff --git a/public/components/search-command/search-command-styles.js b/public/components/command-palette/command-palette-styles.js similarity index 98% rename from public/components/search-command/search-command-styles.js rename to public/components/command-palette/command-palette-styles.js index efeb8d80..32156717 100644 --- a/public/components/search-command/search-command-styles.js +++ b/public/components/command-palette/command-palette-styles.js @@ -4,7 +4,7 @@ import { css } from "lit"; // Import Internal Dependencies import { scrollbarStyle } from "../../common/scrollbar-style.js"; -export const searchCommandStyles = [ +export const commandPaletteStyles = [ scrollbarStyle, css` :host { @@ -380,5 +380,11 @@ kbd { font-size: 11px; color: var(--sc-kbd-text); } + +.action-kbd { + margin-left: 6px; + opacity: 0.7; + border: 1px solid var(--sc-border); +} ` ]; diff --git a/public/components/search-command/search-command.js b/public/components/command-palette/command-palette.js similarity index 88% rename from public/components/search-command/search-command.js rename to public/components/command-palette/command-palette.js index 3dc3d7bc..d99b6472 100644 --- a/public/components/search-command/search-command.js +++ b/public/components/command-palette/command-palette.js @@ -13,23 +13,39 @@ import { computeMatches, getHelperValues } from "./filters.js"; -import { searchCommandStyles } from "./search-command-styles.js"; +import { commandPaletteStyles } from "./command-palette-styles.js"; import { renderFlagPanel, renderRangePanel, renderListPanel, renderFilterList, renderPresets, + renderActions, renderResults -} from "./search-command-panels.js"; +} from "./command-palette-panels.js"; import "./search-chip.js"; -class SearchCommand extends LitElement { +// CONSTANTS +const kActions = [ + { id: "toggle_theme", shortcut: "t" } +]; + +function resolveKbd(shortcut) { + if (!shortcut) { + return null; + } + + const key = shortcut.toUpperCase(); + + return navigator.userAgent.includes("Mac") ? `⌥${key}` : `Alt+${key}`; +} + +class CommandPalette extends LitElement { #linker = null; #network = null; #packages = []; - static styles = searchCommandStyles; + static styles = commandPaletteStyles; static properties = { open: { type: Boolean }, @@ -55,6 +71,16 @@ class SearchCommand extends LitElement { if (event.key === "Escape" && this.open) { this.#close(); + + return; + } + + if (this.open && event.altKey) { + const action = kActions.find((a) => a.shortcut && `Key${a.shortcut.toUpperCase()}` === event.code); + if (action) { + event.preventDefault(); + this.#executeAction(action); + } } }; @@ -84,12 +110,12 @@ class SearchCommand extends LitElement { connectedCallback() { super.connectedCallback(); document.addEventListener("keydown", this.#handleKeydown); - window.addEventListener(EVENTS.SEARCH_COMMAND_INIT, this.#init); + window.addEventListener(EVENTS.COMMAND_PALETTE_INIT, this.#init); } disconnectedCallback() { document.removeEventListener("keydown", this.#handleKeydown); - window.removeEventListener(EVENTS.SEARCH_COMMAND_INIT, this.#init); + window.removeEventListener(EVENTS.COMMAND_PALETTE_INIT, this.#init); super.disconnectedCallback(); } @@ -323,6 +349,17 @@ class SearchCommand extends LitElement { this.#close(); } + #executeAction(action) { + if (action.id === "toggle_theme") { + const nextTheme = window.settings.config.theme === "dark" ? "light" : "dark"; + window.dispatchEvent(new CustomEvent(EVENTS.SETTINGS_SAVED, { + detail: { ...window.settings.config, theme: nextTheme } + })); + } + + this.#close(); + } + #getEmptyQueryMessage() { const i18n = window.i18n[currentLang()].search_command; if (this.queries.length === 1) { @@ -407,6 +444,20 @@ class SearchCommand extends LitElement { } } + #resolveActions() { + const i18n = window.i18n[currentLang()].search_command; + const currentTheme = window.settings?.config?.theme ?? "light"; + const targetTheme = currentTheme === "dark" ? "light" : "dark"; + + return kActions.map((action) => { + return { + ...action, + label: i18n[`action_${action.id}_to_${targetTheme}`], + kbd: resolveKbd(action.shortcut) + }; + }); + } + render() { if (!this.open) { return nothing; @@ -483,6 +534,10 @@ class SearchCommand extends LitElement { presets: PRESETS, onApply: (preset) => this.#addQuery(preset.filter, preset.value) }) : nothing} + ${showRichPlaceholder ? renderActions({ + actions: this.#resolveActions(), + onExecute: (action) => this.#executeAction(action) + }) : nothing} ${renderResults({ results: this.results, selectedIndex: this.selectedIndex, @@ -505,4 +560,4 @@ class SearchCommand extends LitElement { } } -customElements.define("search-command", SearchCommand); +customElements.define("command-palette", CommandPalette); diff --git a/public/components/search-command/filters.js b/public/components/command-palette/filters.js similarity index 100% rename from public/components/search-command/filters.js rename to public/components/command-palette/filters.js diff --git a/public/components/search-command/search-chip.js b/public/components/command-palette/search-chip.js similarity index 100% rename from public/components/search-command/search-chip.js rename to public/components/command-palette/search-chip.js diff --git a/public/components/navigation/navigation.js b/public/components/navigation/navigation.js index 99cd89db..845c4d4a 100644 --- a/public/components/navigation/navigation.js +++ b/public/components/navigation/navigation.js @@ -37,7 +37,7 @@ export class ViewNavigation { const isTargetPopup = event.target.id === "popup--background"; const isPopupOpened = document.querySelector("#popup--background.show"); const isTargetInput = event.target.tagName === "INPUT"; - const isSearchCommandOpen = Boolean(document.querySelector("search-command")?.open); + const isSearchCommandOpen = Boolean(document.querySelector("command-palette")?.open); if (isTargetPopup || isWikiOpen || isTargetInput || isPopupOpened || isSearchCommandOpen) { return; } diff --git a/public/components/wiki/wiki.js b/public/components/wiki/wiki.js index efc14d8f..7b1aec97 100644 --- a/public/components/wiki/wiki.js +++ b/public/components/wiki/wiki.js @@ -58,7 +58,7 @@ export class Wiki { #keydownHotkeys(event) { const isTargetInput = event.target.tagName === "INPUT"; const isTargetPopup = event.target.id === "popup--background"; - const isSearchCommandOpen = Boolean(document.querySelector("search-command")?.open); + const isSearchCommandOpen = Boolean(document.querySelector("command-palette")?.open); if (isTargetInput || isTargetPopup || isSearchCommandOpen) { return; } diff --git a/public/core/events.js b/public/core/events.js index 3602f101..a7733455 100644 --- a/public/core/events.js +++ b/public/core/events.js @@ -10,7 +10,7 @@ export const EVENTS = { MODAL_OPENED: "modal-opened", NETWORK_VIEW_HID: "network-view-hid", NETWORK_VIEW_SHOWED: "network-view-showed", - SEARCH_COMMAND_INIT: "search-command-init", + COMMAND_PALETTE_INIT: "command-palette-init", DRILL_RESET: "drill-reset", DRILL_BACK: "drill-back", DRILL_SWITCH: "drill-switch", diff --git a/public/core/network-navigation.js b/public/core/network-navigation.js index 978b0b08..9e4bd11d 100644 --- a/public/core/network-navigation.js +++ b/public/core/network-navigation.js @@ -131,7 +131,7 @@ export class NetworkNavigation { const isWikiOpen = document.getElementById("documentation-root-element").classList.contains("slide-in"); const isTargetPopup = event.target.id === "popup--background"; const isTargetInput = event.target.tagName === "INPUT"; - const isSearchCommandOpen = Boolean(document.querySelector("search-command")?.open); + const isSearchCommandOpen = Boolean(document.querySelector("command-palette")?.open); if (isNetworkViewHidden || isWikiOpen || isTargetPopup || isTargetInput || isSearchCommandOpen) { return; } diff --git a/public/main.js b/public/main.js index 950a8c80..88392420 100644 --- a/public/main.js +++ b/public/main.js @@ -9,7 +9,7 @@ import "./components/popup/popup.js"; import "./components/locker/locker.js"; import "./components/legend/legend.js"; import "./components/locked-navigation/locked-navigation.js"; -import "./components/search-command/search-command.js"; +import "./components/command-palette/command-palette.js"; import "./components/views/settings/settings.js"; import { HomeView } from "./components/views/home/home.js"; import "./components/views/search/search.js"; @@ -194,7 +194,7 @@ async function onSocketPayload(event) { window.navigation.setNavByName(targetView); } - dispatchSearchCommandInit(); + dispatchCommandPaletteInit(); } async function onSocketInitOrReload(event) { @@ -231,15 +231,15 @@ async function onSocketInitOrReload(event) { window.navigation.setNavByName("search--view"); } - dispatchSearchCommandInit(); + dispatchCommandPaletteInit(); } -function dispatchSearchCommandInit() { +function dispatchCommandPaletteInit() { if (!nsn || !secureDataSet) { return; } - const event = new CustomEvent(EVENTS.SEARCH_COMMAND_INIT, { + const event = new CustomEvent(EVENTS.COMMAND_PALETTE_INIT, { detail: { network: nsn, linker: secureDataSet.linker, diff --git a/test/e2e/search-command.spec.js b/test/e2e/command-palette.spec.js similarity index 59% rename from test/e2e/search-command.spec.js rename to test/e2e/command-palette.spec.js index 173246f6..9d0c5c45 100644 --- a/test/e2e/search-command.spec.js +++ b/test/e2e/command-palette.spec.js @@ -1,7 +1,7 @@ // Import Third-party Dependencies import { test, expect } from "@playwright/test"; -test.describe("[search-command] presets", () => { +test.describe("[command-palette] presets and actions", () => { let i18n; test.beforeEach(async({ page }) => { @@ -26,7 +26,13 @@ test.describe("[search-command] presets", () => { }); test("renders all five preset buttons", async({ page }) => { - await expect(page.locator(".range-preset")).toHaveCount(5); + const presetsSection = page.locator(".section").filter({ hasText: i18n.section_presets }); + await expect(presetsSection.locator(".range-preset")).toHaveCount(5); + }); + + test("renders the theme toggle action button", async({ page }) => { + const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions }); + await expect(actionsSection.locator(".range-preset")).toHaveCount(1); }); test("clicking a preset adds a chip and hides the presets section", async({ page }) => { @@ -55,6 +61,29 @@ test.describe("[search-command] presets", () => { await expect(page.locator(".empty-state")).toHaveText(i18n.empty_after_filter); }); + test("clicking the theme action closes the palette and toggles the theme", async({ page }) => { + const initialTheme = await page.evaluate(() => window.settings.config.theme); + const expectedTheme = initialTheme === "dark" ? "light" : "dark"; + + const actionsSection = page.locator(".section").filter({ hasText: i18n.section_actions }); + await actionsSection.locator(".range-preset").click(); + + await expect(page.locator(".backdrop")).not.toBeVisible(); + const newTheme = await page.evaluate(() => window.settings.config.theme); + expect(newTheme).toBe(expectedTheme); + }); + + test("Alt+T triggers the theme toggle and closes the palette", async({ page }) => { + const initialTheme = await page.evaluate(() => window.settings.config.theme); + const expectedTheme = initialTheme === "dark" ? "light" : "dark"; + + await page.keyboard.press("Alt+t"); + + await expect(page.locator(".backdrop")).not.toBeVisible(); + const newTheme = await page.evaluate(() => window.settings.config.theme); + expect(newTheme).toBe(expectedTheme); + }); + test("pressing Escape closes the palette", async({ page }) => { await page.keyboard.press("Escape"); diff --git a/test/ui/search-command-filters.test.js b/test/ui/command-palette-filters.test.js similarity index 99% rename from test/ui/search-command-filters.test.js rename to test/ui/command-palette-filters.test.js index 71b79387..d8c623ed 100644 --- a/test/ui/search-command-filters.test.js +++ b/test/ui/command-palette-filters.test.js @@ -8,7 +8,7 @@ import { getFlagCounts, getFilterValueCounts, getHelperValues -} from "../../public/components/search-command/filters.js"; +} from "../../public/components/command-palette/filters.js"; const kLinker = new Map([ [0, { diff --git a/views/index.html b/views/index.html index 778105fa..6c7efe71 100644 --- a/views/index.html +++ b/views/index.html @@ -105,5 +105,5 @@
- +