| ... | ... | @@ -48,9 +48,12 @@ import { |
| 48 | 48 | FolderPlusIcon, |
| 49 | 49 | LinkIcon, |
| 50 | 50 | MessageSquareIcon, |
| 51 | MonitorIcon, |
| 52 | MoonIcon, |
| 51 | 53 | PaletteIcon, |
| 52 | 54 | SettingsIcon, |
| 53 | 55 | SquarePenIcon, |
| 56 | SunIcon, |
| 54 | 57 | TextSearchIcon, |
| 55 | 58 | } from "lucide-react"; |
| 56 | 59 | import { |
| ... | ... | @@ -74,6 +77,15 @@ import { useOpenPanelPullRequestUrl } from "../hooks/useOpenPanelPullRequestUrl" |
| 74 | 77 | import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; |
| 75 | 78 | import { useClientSettings } from "../hooks/useSettings"; |
| 76 | 79 | import { useTheme } from "../hooks/useTheme"; |
| 80 | import { useCustomThemes } from "../hooks/useCustomThemes"; |
| 81 | import { useEnvironmentThemeDefinitions } from "../hooks/useEnvironmentTheme"; |
| 82 | import { BUILT_IN_THEMES } from "@t3tools/shared/themePalettes"; |
| 83 | import { getThemeDefinition } from "../themePalette"; |
| 84 | import { |
| 85 | STANDARD_THEME_CARDS, |
| 86 | getThemeCardDefinition, |
| 87 | ThemePreviewCircle, |
| 88 | } from "./settings/ThemePreviewCircles"; |
| 77 | 89 | import { readLocalApi } from "../localApi"; |
| 78 | 90 | import { desktopLocalBackendId } from "../connection/desktopLocal"; |
| 79 | 91 | import { filesystemEnvironment } from "../state/filesystem"; |
| ... | ... | @@ -184,6 +196,22 @@ import { PullRequestGlyph } from "~/components/pullRequest/pullRequestIcons"; |
| 184 | 196 | |
| 185 | 197 | const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; |
| 186 | 198 | |
| 199 | const APPEARANCE_OPTIONS = [ |
| 200 | { mode: "system", label: "System", icon: MonitorIcon }, |
| 201 | { mode: "light", label: "Light", icon: SunIcon }, |
| 202 | { mode: "dark", label: "Dark", icon: MoonIcon }, |
| 203 | ] as const; |
| 204 | |
| 205 | function notifyThemeSaveFailure(): void { |
| 206 | toastManager.add( |
| 207 | stackedThreadToast({ |
| 208 | type: "error", |
| 209 | title: "Couldn't save theme selection", |
| 210 | description: "Try again.", |
| 211 | }), |
| 212 | ); |
| 213 | } |
| 214 | |
| 187 | 215 | function projectFavicon(project: Project) { |
| 188 | 216 | return <ProjectFavicon project={project} className="size-4" />; |
| 189 | 217 | } |
| ... | ... | @@ -453,7 +481,7 @@ export function CommandPalette({ children }: { children: ReactNode }) { |
| 453 | 481 | const openNewThreadIn = useCallback(() => dispatch({ _tag: "OpenNewThreadIn" }), []); |
| 454 | 482 | const clearOpenIntent = useCallback(() => dispatch({ _tag: "ClearOpenIntent" }), []); |
| 455 | 483 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); |
| 456 | | const { theme, themeHalves, resolvedTheme } = useTheme(); |
| 484 | const { theme, themeHalves, resolvedTheme, appearanceMode, setAppearanceMode } = useTheme(); |
| 457 | 485 | const composerHandleRef = useRef<ChatComposerHandle | null>(null); |
| 458 | 486 | const routeTarget = useParams({ |
| 459 | 487 | strict: false, |
| ... | ... | @@ -494,8 +522,33 @@ export function CommandPalette({ children }: { children: ReactNode }) { |
| 494 | 522 | terminalOpen, |
| 495 | 523 | previewFocus: isPreviewFocused(), |
| 496 | 524 | previewOpen, |
| 525 | modelPickerOpen: composerHandleRef.current?.isModelPickerOpen() ?? false, |
| 497 | 526 | }, |
| 498 | 527 | }); |
| 528 | if (command === "appearance.cycle") { |
| 529 | event.preventDefault(); |
| 530 | event.stopPropagation(); |
| 531 | if (event.repeat) return; |
| 532 | const nextMode = |
| 533 | appearanceMode === "system" ? "light" : appearanceMode === "light" ? "dark" : "system"; |
| 534 | if (!setAppearanceMode(nextMode)) { |
| 535 | notifyThemeSaveFailure(); |
| 536 | } else { |
| 537 | toastManager.add({ |
| 538 | id: "appearance-cycle", |
| 539 | title: `Appearance: ${APPEARANCE_OPTIONS.find((option) => option.mode === nextMode)?.label}`, |
| 540 | timeout: 1500, |
| 541 | }); |
| 542 | } |
| 543 | return; |
| 544 | } |
| 545 | if (command === "theme.select") { |
| 546 | event.preventDefault(); |
| 547 | event.stopPropagation(); |
| 548 | if (event.repeat) return; |
| 549 | dispatch({ _tag: "OpenChangeTheme" }); |
| 550 | return; |
| 551 | } |
| 499 | 552 | if (command === "themeEditor.toggle") { |
| 500 | 553 | event.preventDefault(); |
| 501 | 554 | event.stopPropagation(); |
| ... | ... | @@ -516,7 +569,17 @@ export function CommandPalette({ children }: { children: ReactNode }) { |
| 516 | 569 | }; |
| 517 | 570 | window.addEventListener("keydown", onKeyDown); |
| 518 | 571 | return () => window.removeEventListener("keydown", onKeyDown); |
| 519 | | }, [keybindings, previewOpen, resolvedTheme, terminalOpen, theme, themeHalves, toggleMode]); |
| 572 | }, [ |
| 573 | appearanceMode, |
| 574 | keybindings, |
| 575 | previewOpen, |
| 576 | resolvedTheme, |
| 577 | setAppearanceMode, |
| 578 | terminalOpen, |
| 579 | theme, |
| 580 | themeHalves, |
| 581 | toggleMode, |
| 582 | ]); |
| 520 | 583 | |
| 521 | 584 | useEffect( |
| 522 | 585 | () => |
| ... | ... | @@ -702,7 +765,30 @@ function OpenCommandPaletteDialog(props: { |
| 702 | 765 | const projectOrder = useUiStateStore((store) => store.projectOrder); |
| 703 | 766 | const threads = useThreadShells(); |
| 704 | 767 | const keybindings = useAtomValue(primaryServerKeybindingsAtom); |
| 705 | | const { theme, themeHalves, resolvedTheme } = useTheme(); |
| 768 | const { |
| 769 | theme, |
| 770 | themeHalves, |
| 771 | resolvedTheme, |
| 772 | appearanceMode, |
| 773 | setAppearanceMode, |
| 774 | setTheme, |
| 775 | setThemeHalf, |
| 776 | } = useTheme(); |
| 777 | const customThemes = useCustomThemes(); |
| 778 | const environmentThemes = useEnvironmentThemeDefinitions(); |
| 779 | const themeCards = useMemo(() => { |
| 780 | const seen = new Set<string>(); |
| 781 | return [ |
| 782 | ...STANDARD_THEME_CARDS.map((card) => ({ ...card, id: null })), |
| 783 | ...[...BUILT_IN_THEMES, ...customThemes, ...environmentThemes] |
| 784 | .filter((definition) => { |
| 785 | if (seen.has(definition.id)) return false; |
| 786 | seen.add(definition.id); |
| 787 | return true; |
| 788 | }) |
| 789 | .map(getThemeCardDefinition), |
| 790 | ]; |
| 791 | }, [customThemes, environmentThemes]); |
| 706 | 792 | const providers = useAtomValue(primaryServerProvidersAtom); |
| 707 | 793 | const providerEntryByEnvironmentAndInstanceId = useMemo(() => { |
| 708 | 794 | const map = new Map<string, ProviderInstanceEntry>(); |
| ... | ... | @@ -1803,6 +1889,100 @@ function OpenCommandPaletteDialog(props: { |
| 1803 | 1889 | }); |
| 1804 | 1890 | } |
| 1805 | 1891 | |
| 1892 | const changeThemeItem: CommandPaletteSubmenuItem = { |
| 1893 | kind: "submenu", |
| 1894 | value: "action:change-theme", |
| 1895 | searchTerms: ["change theme", "appearance", "colors", "palette"], |
| 1896 | title: "Change theme", |
| 1897 | icon: <PaletteIcon className={ITEM_ICON_CLASS} />, |
| 1898 | addonIcon: <PaletteIcon className={ADDON_ICON_CLASS} />, |
| 1899 | shortcutCommand: "theme.select", |
| 1900 | groups: [ |
| 1901 | { |
| 1902 | value: "themes", |
| 1903 | label: "Change theme", |
| 1904 | items: themeCards.map(({ id, label, previews }) => ({ |
| 1905 | kind: "action", |
| 1906 | value: id === null ? "theme:standard" : `theme:palette:${id}`, |
| 1907 | title: label, |
| 1908 | description: previews.length === 1 ? `For ${previews[0]!.mode} mode` : undefined, |
| 1909 | searchTerms: [label, "theme", "appearance"], |
| 1910 | icon: <PaletteIcon className={ITEM_ICON_CLASS} />, |
| 1911 | titleTrailingContent: ( |
| 1912 | <span className="flex shrink-0 items-center gap-2"> |
| 1913 | {(themeHalves?.[resolvedTheme] ?? getThemeDefinition(theme)?.id ?? null) === id ? ( |
| 1914 | <span className="text-xs text-muted-foreground/70">Current</span> |
| 1915 | ) : null} |
| 1916 | <span className="flex items-center gap-1" aria-hidden> |
| 1917 | {previews.map((preview) => ( |
| 1918 | <ThemePreviewCircle |
| 1919 | key={preview.mode} |
| 1920 | colors={preview.colors} |
| 1921 | mode={preview.mode} |
| 1922 | className="size-3 border-0" |
| 1923 | /> |
| 1924 | ))} |
| 1925 | </span> |
| 1926 | </span> |
| 1927 | ), |
| 1928 | run: async () => { |
| 1929 | const saved = |
| 1930 | previews.length === 1 && id !== null |
| 1931 | ? setThemeHalf(previews[0]!.mode, id) |
| 1932 | : setTheme(id ?? appearanceMode); |
| 1933 | if (!saved) notifyThemeSaveFailure(); |
| 1934 | }, |
| 1935 | })), |
| 1936 | }, |
| 1937 | ], |
| 1938 | }; |
| 1939 | actionItems.push(changeThemeItem); |
| 1940 | |
| 1941 | const changeAppearanceItem: CommandPaletteSubmenuItem = { |
| 1942 | kind: "submenu", |
| 1943 | value: "action:change-appearance", |
| 1944 | searchTerms: ["change appearance", "light", "dark", "system", "mode", "toggle"], |
| 1945 | title: "Change appearance", |
| 1946 | icon: <MonitorIcon className={ITEM_ICON_CLASS} />, |
| 1947 | addonIcon: <MonitorIcon className={ADDON_ICON_CLASS} />, |
| 1948 | shortcutCommand: "appearance.cycle", |
| 1949 | groups: [ |
| 1950 | { |
| 1951 | value: "appearance", |
| 1952 | label: "Change appearance", |
| 1953 | items: APPEARANCE_OPTIONS.map(({ mode, label, icon: Icon }) => ({ |
| 1954 | kind: "action", |
| 1955 | value: `appearance:${mode}`, |
| 1956 | title: label, |
| 1957 | searchTerms: [label, "appearance", "mode"], |
| 1958 | icon: <Icon className={ITEM_ICON_CLASS} />, |
| 1959 | titleTrailingContent: |
| 1960 | appearanceMode === mode ? ( |
| 1961 | <span className="text-xs text-muted-foreground/70">Current</span> |
| 1962 | ) : undefined, |
| 1963 | run: async () => { |
| 1964 | if (!setAppearanceMode(mode)) notifyThemeSaveFailure(); |
| 1965 | }, |
| 1966 | })), |
| 1967 | }, |
| 1968 | ], |
| 1969 | }; |
| 1970 | actionItems.push(changeAppearanceItem); |
| 1971 | |
| 1972 | useLayoutEffect(() => { |
| 1973 | if (openIntent?.kind !== "change-theme") return; |
| 1974 | clearOpenIntent(); |
| 1975 | browseNavigation.invalidate(); |
| 1976 | cloneLookupGeneration.current += 1; |
| 1977 | setIsRemoteProjectLookingUp(false); |
| 1978 | setAddProjectCloneFlow(null); |
| 1979 | setViewStack([]); |
| 1980 | pushPaletteView({ |
| 1981 | addonIcon: <PaletteIcon className={ADDON_ICON_CLASS} />, |
| 1982 | groups: [{ value: "themes", label: "Change theme", items: [] }], |
| 1983 | }); |
| 1984 | }, [browseNavigation, clearOpenIntent, openIntent, pushPaletteView]); |
| 1985 | |
| 1806 | 1986 | actionItems.push({ |
| 1807 | 1987 | kind: "action", |
| 1808 | 1988 | value: "action:theme-editor", |
| ... | ... | @@ -1898,7 +2078,11 @@ function OpenCommandPaletteDialog(props: { |
| 1898 | 2078 | addProjectEnvironmentId, |
| 1899 | 2079 | buildAddProjectRemoteSourceReadiness(sourceControlDiscovery.data), |
| 1900 | 2080 | ) |
| 1901 | | : (currentView?.groups ?? rootGroups); |
| 2081 | : currentView?.groups[0]?.value === "themes" |
| 2082 | ? changeThemeItem.groups |
| 2083 | : currentView?.groups[0]?.value === "appearance" |
| 2084 | ? changeAppearanceItem.groups |
| 2085 | : (currentView?.groups ?? rootGroups); |
| 1902 | 2086 | |
| 1903 | 2087 | const filteredGroups = filterCommandPaletteGroups({ |
| 1904 | 2088 | activeGroups, |