diff --git a/components/cmdk.tsx b/components/cmdk.tsx
new file mode 100644
index 00000000..b92ca33e
--- /dev/null
+++ b/components/cmdk.tsx
@@ -0,0 +1,254 @@
+"use client";
+
+import * as React from "react";
+import { Check, CornerDownLeft, Search } from "lucide-react";
+import { useRouter } from "next/navigation";
+import { useCallback, useEffect, useMemo, useState } from "react";
+import { Input } from "@/components/ui/input";
+import { Kbd } from "./ui/kbd";
+import { Button } from "./ui/button";
+import { useEditorStore } from "@/store/editor-store";
+import { useThemePresetStore } from "@/store/theme-preset-store";
+import { authClient } from "@/lib/auth-client";
+import { Badge } from "./ui/badge";
+import { isThemeNew } from "@/utils/search/is-theme-new";
+import { ThemeColors } from "./search/theme-colors";
+import { NAVIGATION_ITEMS } from "@/utils/search/constants/navigation";
+import { filterPresets } from "@/utils/search/filter-presets";
+import { sortThemes } from "@/utils/search/sort-themes";
+import {
+ CommandDialog,
+ CommandEmpty,
+ CommandGroup,
+ CommandItem,
+ CommandList,
+ CommandSeparator,
+} from "@/components/ui/command";
+import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip";
+
+export function CmdK() {
+ const [open, setOpen] = useState(false);
+ const [search, setSearch] = useState("");
+ const router = useRouter();
+
+ const themeState = useEditorStore((store) => store.themeState);
+ const applyThemePreset = useEditorStore((store) => store.applyThemePreset);
+ const currentPreset = themeState.preset;
+ const mode = themeState.currentMode;
+
+ const presets = useThemePresetStore((store) => store.getAllPresets());
+ const loadSavedPresets = useThemePresetStore((store) => store.loadSavedPresets);
+ const unloadSavedPresets = useThemePresetStore((store) => store.unloadSavedPresets);
+
+ const { data: session } = authClient.useSession();
+
+ useEffect(() => {
+ if (session?.user) {
+ loadSavedPresets();
+ } else {
+ unloadSavedPresets();
+ }
+ }, [loadSavedPresets, unloadSavedPresets, session?.user]);
+
+ const isSavedTheme = useCallback(
+ (presetId: string) => {
+ return presets[presetId]?.source === "SAVED";
+ },
+ [presets]
+ );
+
+ const presetNames = useMemo(() => Array.from(new Set(["default", ...Object.keys(presets)])), [presets]);
+
+ const filteredPresets = useMemo(() => {
+ const filteredList = filterPresets(presetNames, presets, search);
+
+ // Separate saved and default themes
+ const savedThemesList = filteredList.filter((name) => name !== "default" && isSavedTheme(name));
+ const defaultThemesList = filteredList.filter((name) => !savedThemesList.includes(name));
+
+ return [...sortThemes(savedThemesList, presets), ...sortThemes(defaultThemesList, presets)];
+ }, [presetNames, search, presets, isSavedTheme]);
+
+ const filteredSavedThemes = useMemo(() => {
+ return filteredPresets.filter((name) => name !== "default" && isSavedTheme(name));
+ }, [filteredPresets, isSavedTheme]);
+
+ const filteredDefaultThemes = useMemo(() => {
+ return filteredPresets.filter((name) => name === "default" || !isSavedTheme(name));
+ }, [filteredPresets, isSavedTheme]);
+
+ const filteredNavigation = useMemo(() => {
+ if (search.trim() === "") {
+ return NAVIGATION_ITEMS;
+ }
+ const searchLower = search.toLowerCase();
+ return NAVIGATION_ITEMS.filter((item) => {
+ const matchesLabel = item.label.toLowerCase().includes(searchLower);
+ const matchesKeywords = item.keywords?.some((keyword) =>
+ keyword.toLowerCase().includes(searchLower)
+ );
+ return matchesLabel || matchesKeywords;
+ });
+ }, [search]);
+
+ useEffect(() => {
+ const down = (e: KeyboardEvent) => {
+ if (e.key === "k" && (e.metaKey || e.ctrlKey)) {
+ e.preventDefault();
+ setOpen((open) => !open);
+ }
+ };
+
+ document.addEventListener("keydown", down);
+ return () => document.removeEventListener("keydown", down);
+ }, []);
+
+ const onThemeSelect = (presetName: string) => {
+ applyThemePreset(presetName);
+ setOpen(false);
+ setSearch("");
+ };
+
+ const onNavigationSelect = (href: string) => {
+ router.push(href);
+ setOpen(false);
+ setSearch("");
+ };
+
+ return (
+ <>
+
+
+
+
+
+ Search (⌘K)
+
+
+
+
+
+
+ setSearch(e.target.value)}
+ />
+
+
+
+ No results found.
+
+
+ {/* Navigation Group */}
+ {filteredNavigation.length > 0 && (
+ <>
+
+ {filteredNavigation.map((item) => {
+ const Icon = item.icon;
+ return (
+ onNavigationSelect(item.href)}
+ className="flex items-center gap-2"
+ >
+
+ {item.label}
+
+ );
+ })}
+
+ {(filteredSavedThemes.length > 0 || filteredDefaultThemes.length > 0) && (
+
+ )}
+ >
+ )}
+
+ {/* Saved Themes Group */}
+ {filteredSavedThemes.length > 0 && (
+ <>
+
+ {filteredSavedThemes.map((presetName, index) => (
+ onThemeSelect(presetName)}
+ className="flex items-center gap-2"
+ >
+
+
+
+
+ {presets[presetName]?.label || presetName}
+
+ {presets[presetName] && isThemeNew(presets[presetName]) && (
+
+ New
+
+ )}
+
+ {presetName === currentPreset && (
+
+ )}
+
+ ))}
+
+
+ >
+ )}
+
+ {/* Built-in Themes Group */}
+ {filteredDefaultThemes.length > 0 && (
+
+ {filteredDefaultThemes.map((presetName, index) => (
+ onThemeSelect(presetName)}
+ className="flex items-center gap-2"
+ >
+
+
+
+ {presets[presetName]?.label || presetName}
+
+ {presets[presetName] && isThemeNew(presets[presetName]) && (
+
+ New
+
+ )}
+
+ {presetName === currentPreset && (
+
+ )}
+
+ ))}
+
+ )}
+
+
+
+
+ >
+ );
+}
+
+const CommandFooter = () => {
+ return (
+
+ );
+};
diff --git a/components/editor/theme-preset-select.tsx b/components/editor/theme-preset-select.tsx
index 375d8a6c..e3ddbe0c 100644
--- a/components/editor/theme-preset-select.tsx
+++ b/components/editor/theme-preset-select.tsx
@@ -10,8 +10,14 @@ import { authClient } from "@/lib/auth-client";
import { cn } from "@/lib/utils";
import { useEditorStore } from "@/store/editor-store";
import { useThemePresetStore } from "@/store/theme-preset-store";
-import { ThemePreset } from "@/types/theme";
-import { getPresetThemeStyles } from "@/utils/theme-preset-helper";
+import Link from "next/link";
+import React, { useCallback, useEffect, useMemo, useState } from "react";
+import { ThemeToggle } from "../theme-toggle";
+import { TooltipWrapper } from "../tooltip-wrapper";
+import { ThemeColors } from "../search/theme-colors";
+import { filterPresets } from "@/utils/search/filter-presets";
+import { sortThemes } from "@/utils/search/sort-themes";
+import { isThemeNew } from "@/utils/search/is-theme-new";
import {
ArrowLeft,
ArrowRight,
@@ -22,48 +28,11 @@ import {
Settings,
Shuffle,
} from "lucide-react";
-import Link from "next/link";
-import React, { useCallback, useEffect, useMemo, useState } from "react";
-import { ThemeToggle } from "../theme-toggle";
-import { TooltipWrapper } from "../tooltip-wrapper";
interface ThemePresetSelectProps extends React.ComponentProps {
withCycleThemes?: boolean;
}
-interface ColorBoxProps {
- color: string;
-}
-
-const ColorBox: React.FC = ({ color }) => (
-
-);
-
-interface ThemeColorsProps {
- presetName: string;
- mode: "light" | "dark";
-}
-
-const ThemeColors: React.FC = ({ presetName, mode }) => {
- const styles = getPresetThemeStyles(presetName)[mode];
- return (
-
-
-
-
-
-
- );
-};
-
-const isThemeNew = (preset: ThemePreset) => {
- if (!preset.createdAt) return false;
- const createdAt = new Date(preset.createdAt);
- const timePeriod = new Date();
- timePeriod.setDate(timePeriod.getDate() - 5);
- return createdAt > timePeriod;
-};
-
const ThemeControls = () => {
const applyThemePreset = useEditorStore((store) => store.applyThemePreset);
const presets = useThemePresetStore((store) => store.getAllPresets());
@@ -211,35 +180,13 @@ const ThemePresetSelect: React.FC = ({
const currentPresetName = presetNames?.find((name) => name === currentPreset);
const filteredPresets = useMemo(() => {
- const filteredList =
- search.trim() === ""
- ? presetNames
- : presetNames.filter((name) => {
- if (name === "default") {
- return "default".toLowerCase().includes(search.toLowerCase());
- }
- return presets[name]?.label?.toLowerCase().includes(search.toLowerCase());
- });
+ const filteredList = filterPresets(presetNames, presets, search);
// Separate saved and default themes
const savedThemesList = filteredList.filter((name) => name !== "default" && isSavedTheme(name));
const defaultThemesList = filteredList.filter((name) => !savedThemesList.includes(name));
- // Sort each list, with "default" at the top for default themes
- const sortThemes = (list: string[]) => {
- const defaultTheme = list.filter((name) => name === "default");
- const otherThemes = list
- .filter((name) => name !== "default")
- .sort((a, b) => {
- const labelA = presets[a]?.label || a;
- const labelB = presets[b]?.label || b;
- return labelA.localeCompare(labelB);
- });
- return [...defaultTheme, ...otherThemes];
- };
-
- // Combine saved themes first, then default themes
- return [...sortThemes(savedThemesList), ...sortThemes(defaultThemesList)];
+ return [...sortThemes(savedThemesList, presets), ...sortThemes(defaultThemesList, presets)];
}, [presetNames, search, presets, isSavedTheme]);
const filteredSavedThemes = useMemo(() => {
@@ -260,12 +207,7 @@ const ThemePresetSelect: React.FC = ({
{...props}
>
-
-
-
-
-
-
+
{currentPresetName !== "default" &&
currentPresetName &&
isSavedTheme(currentPresetName) &&
diff --git a/components/get-pro-cta.tsx b/components/get-pro-cta.tsx
index aa0eda8b..761eb412 100644
--- a/components/get-pro-cta.tsx
+++ b/components/get-pro-cta.tsx
@@ -28,7 +28,7 @@ export function GetProCTA({ className, ...props }: GetProCTAProps) {
>
- Get Pro
+
Get Pro
);
diff --git a/components/header.tsx b/components/header.tsx
index 0435f99d..7d261194 100644
--- a/components/header.tsx
+++ b/components/header.tsx
@@ -15,6 +15,7 @@ import { formatCompactNumber } from "@/utils/format";
import Link from "next/link";
import { useState } from "react";
import { GetProCTA } from "./get-pro-cta";
+import { CmdK } from "./cmdk";
export function Header() {
const { stargazersCount } = useGithubStars("jnsahaj", "tweakcn");
@@ -26,21 +27,23 @@ export function Header() {
- tweakcn
+ tweakcn
+
+
{stargazersCount > 0 && formatCompactNumber(stargazersCount)}
-
-
+
+
@@ -50,14 +53,15 @@ export function Header() {
-
+
diff --git a/components/search/color-box.tsx b/components/search/color-box.tsx
new file mode 100644
index 00000000..8f56d74e
--- /dev/null
+++ b/components/search/color-box.tsx
@@ -0,0 +1,7 @@
+interface ColorBoxProps {
+ color: string;
+}
+
+export const ColorBox: React.FC
= ({ color }) => (
+
+);
diff --git a/components/search/theme-colors.tsx b/components/search/theme-colors.tsx
new file mode 100644
index 00000000..aef8d43d
--- /dev/null
+++ b/components/search/theme-colors.tsx
@@ -0,0 +1,20 @@
+import { getPresetThemeStyles } from "@/utils/theme-preset-helper";
+import { ColorBox } from "./color-box";
+import { useMemo } from "react";
+
+interface ThemeColorsProps {
+ presetName: string;
+ mode: "light" | "dark";
+}
+
+export const ThemeColors: React.FC = ({ presetName, mode }) => {
+ const styles = useMemo(() => getPresetThemeStyles(presetName)[mode], [presetName, mode]);
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/components/ui/command.tsx b/components/ui/command.tsx
index 035363fe..738ec945 100644
--- a/components/ui/command.tsx
+++ b/components/ui/command.tsx
@@ -21,12 +21,14 @@ const Command = React.forwardRef<
));
Command.displayName = CommandPrimitive.displayName;
-interface CommandDialogProps extends DialogProps {}
+interface CommandDialogProps extends DialogProps {
+ title?: string;
+}
-const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
+const CommandDialog = ({ children, title, ...props }: CommandDialogProps) => {
return (