From 4ac155c8ccada4c03c71f34112c7936cfe31f8d6 Mon Sep 17 00:00:00 2001 From: mkelvers Date: Sat, 13 Jun 2026 22:29:16 +0200 Subject: [PATCH] feat: add search/state.ts --- static/search/state.ts | 91 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) create mode 100644 static/search/state.ts diff --git a/static/search/state.ts b/static/search/state.ts new file mode 100644 index 0000000..33c3c9a --- /dev/null +++ b/static/search/state.ts @@ -0,0 +1,91 @@ +export interface CommandPaletteItem { + id: string; + type: string; + label: string; + subtitle: string; + href: string; + image?: string; + icon?: string; +} + +export interface CommandPaletteResponse { + items: CommandPaletteItem[]; + hasNextPage: boolean; + nextPage?: number; +} + +export const commandPaletteInitializedKey = Symbol("commandPaletteInitialized"); +export const globalWindow = window as Window & { [commandPaletteInitializedKey]?: boolean }; + +export const searchInput = document.getElementById( + "command-palette-input", +) as HTMLInputElement | null; +export const searchResults = document.querySelector( + "[data-command-palette-results]", +) as HTMLElement | null; +export const searchDialog = document.querySelector( + "[data-command-palette-dialog]", +) as HTMLElement | null; +export const searchRoot = document.querySelector( + "[data-command-palette-root]", +) as HTMLElement | null; +export const searchPage = document.querySelector( + "[data-command-palette-page]", +) as HTMLElement | null; +export const searchOpenButtons = document.querySelectorAll("[data-command-palette-open]"); +export const searchCloseButtons = document.querySelectorAll("[data-command-palette-close]"); +export const searchClearButtons = document.querySelectorAll("[data-command-palette-clear]"); +export const shortcutHints = document.querySelectorAll("[data-command-palette-shortcut]"); + +export const state = { + resultItems: [] as CommandPaletteItem[], + selectedIndex: 0, + fetchTimeout: undefined as number | undefined, + lastQuery: "", + activeRequestController: undefined as AbortController | undefined, + nextSearchPage: undefined as number | undefined, + hasNextSearchPage: false, + isFetchingNextPage: false, + lastFocusedSearchOpener: null as HTMLElement | null, +}; + +export const responseCache = new Map(); + +export const iconPaths: Record = { + bookmark: "M19 21l-7-4-7 4V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v16z", + compass: + "M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z M16.24 7.76l-2.12 6.36-6.36 2.12 2.12-6.36 6.36-2.12z", + play: "M8 5v14l11-7z", + search: "M11 19a8 8 0 1 1 5.65-2.35L21 21 M16.65 16.65 21 21", + trending: "M3 17l6-6 4 4 8-8 M14 7h7v7", +}; + +export const typeLabels: Record = { + anime: "Top results", +}; + +export const groupOrder = ["anime"]; +export const maxPosterImageRetries = 2; + +export const isMac = (): boolean => /Mac|iPhone|iPad|iPod/.test(navigator.platform); + +export const isTypingTarget = (target: EventTarget | null): boolean => + target instanceof HTMLInputElement || + target instanceof HTMLTextAreaElement || + target instanceof HTMLSelectElement || + (target instanceof HTMLElement && target.isContentEditable); + +export const isSearchOpen = (): boolean => searchDialog?.classList.contains("flex") ?? false; + +export const isSafeImageUrl = (rawUrl?: string): boolean => { + if (!rawUrl) { + return false; + } + + try { + const parsed = new URL(rawUrl, window.location.origin); + return parsed.protocol === "https:" || parsed.protocol === "http:"; + } catch { + return false; + } +};