diff options
| author | 2026-02-25 01:32:51 +0800 | |
|---|---|---|
| committer | 2026-02-25 01:32:51 +0800 | |
| commit | 66668d85d603c5841d755a6023aa1925559fc6d4 (patch) | |
| tree | 485464148c76b0021efb55b7d2afd1c3004ceee0 /packages/ui-new/src/components | |
| parent | a6773bd092db654360c599ca6b0108ea0e456e8c (diff) | |
| download | DropOut-66668d85d603c5841d755a6023aa1925559fc6d4.tar.gz DropOut-66668d85d603c5841d755a6023aa1925559fc6d4.zip | |
chore(workspace): replace legacy codes
Diffstat (limited to 'packages/ui-new/src/components')
28 files changed, 0 insertions, 3759 deletions
diff --git a/packages/ui-new/src/components/bottom-bar.tsx b/packages/ui-new/src/components/bottom-bar.tsx deleted file mode 100644 index 32eb852..0000000 --- a/packages/ui-new/src/components/bottom-bar.tsx +++ /dev/null @@ -1,231 +0,0 @@ -import { listen, type UnlistenFn } from "@tauri-apps/api/event"; -import { Play, User } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { toast } from "sonner"; -import { listInstalledVersions, startGame } from "@/client"; -import { cn } from "@/lib/utils"; -import { useAuthStore } from "@/models/auth"; -import { useInstancesStore } from "@/models/instances"; -import { useGameStore } from "@/stores/game-store"; -import { LoginModal } from "./login-modal"; -import { Button } from "./ui/button"; -import { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectTrigger, - SelectValue, -} from "./ui/select"; - -interface InstalledVersion { - id: string; - type: string; -} - -export function BottomBar() { - const authStore = useAuthStore(); - const gameStore = useGameStore(); - const instancesStore = useInstancesStore(); - - const [selectedVersion, setSelectedVersion] = useState<string | null>(null); - const [installedVersions, setInstalledVersions] = useState< - InstalledVersion[] - >([]); - const [isLoadingVersions, setIsLoadingVersions] = useState(true); - const [showLoginModal, setShowLoginModal] = useState(false); - - const loadInstalledVersions = useCallback(async () => { - if (!instancesStore.activeInstance) { - setInstalledVersions([]); - setIsLoadingVersions(false); - return; - } - - setIsLoadingVersions(true); - try { - const versions = await listInstalledVersions( - instancesStore.activeInstance.id, - ); - - const installed = versions || []; - setInstalledVersions(installed); - - // If no version is selected but we have installed versions, select the first one - if (!gameStore.selectedVersion && installed.length > 0) { - gameStore.setSelectedVersion(installed[0].id); - } - } catch (error) { - console.error("Failed to load installed versions:", error); - } finally { - setIsLoadingVersions(false); - } - }, [ - instancesStore.activeInstance, - gameStore.selectedVersion, - gameStore.setSelectedVersion, - ]); - - useEffect(() => { - loadInstalledVersions(); - - // Listen for backend events that should refresh installed versions. - let unlistenDownload: UnlistenFn | null = null; - let unlistenVersionDeleted: UnlistenFn | null = null; - - (async () => { - try { - unlistenDownload = await listen("download-complete", () => { - loadInstalledVersions(); - }); - } catch (err) { - // best-effort: do not break UI if listening fails - // eslint-disable-next-line no-console - console.warn("Failed to attach download-complete listener:", err); - } - - try { - unlistenVersionDeleted = await listen("version-deleted", () => { - loadInstalledVersions(); - }); - } catch (err) { - // eslint-disable-next-line no-console - console.warn("Failed to attach version-deleted listener:", err); - } - })(); - - return () => { - try { - if (unlistenDownload) unlistenDownload(); - } catch { - // ignore - } - try { - if (unlistenVersionDeleted) unlistenVersionDeleted(); - } catch { - // ignore - } - }; - }, [loadInstalledVersions]); - - const handleStartGame = async () => { - if (!selectedVersion) { - toast.info("Please select a version!"); - return; - } - - if (!instancesStore.activeInstance) { - toast.info("Please select an instance first!"); - return; - } - // await gameStore.startGame( - // authStore.currentAccount, - // authStore.openLoginModal, - // instancesStore.activeInstanceId, - // uiStore.setView, - // ); - await startGame(instancesStore.activeInstance?.id, selectedVersion); - }; - - const getVersionTypeColor = (type: string) => { - switch (type) { - case "release": - return "bg-emerald-500"; - case "snapshot": - return "bg-amber-500"; - case "old_beta": - return "bg-rose-500"; - case "old_alpha": - return "bg-violet-500"; - default: - return "bg-gray-500"; - } - }; - - const versionOptions = useMemo( - () => - installedVersions.map((v) => ({ - label: `${v.id}${v.type !== "release" ? ` (${v.type})` : ""}`, - value: v.id, - type: v.type, - })), - [installedVersions], - ); - - return ( - <div className="absolute bottom-0 left-0 right-0 bg-linear-to-t from-black/30 via-transparent to-transparent p-4 z-10"> - <div className="max-w-7xl mx-auto"> - <div className="flex items-center justify-between bg-white/5 dark:bg-black/20 backdrop-blur-xl border border-white/10 dark:border-white/5 p-3 shadow-lg"> - <div className="flex items-center gap-4"> - <div className="flex flex-col"> - <span className="text-xs font-mono text-zinc-400 uppercase tracking-wider"> - Active Instance - </span> - <span className="text-sm font-medium text-white"> - {instancesStore.activeInstance?.name || "No instance selected"} - </span> - </div> - - <Select - items={versionOptions} - onValueChange={setSelectedVersion} - disabled={isLoadingVersions} - > - <SelectTrigger className="max-w-48"> - <SelectValue - placeholder={ - isLoadingVersions - ? "Loading versions..." - : "Please select a version" - } - /> - </SelectTrigger> - <SelectContent> - <SelectGroup> - {versionOptions.map((item) => ( - <SelectItem - key={item.value} - value={item.value} - className={getVersionTypeColor(item.type)} - > - {item.label} - </SelectItem> - ))} - </SelectGroup> - </SelectContent> - </Select> - </div> - - <div className="flex items-center gap-3"> - {authStore.account ? ( - <Button - className={cn( - "px-4 py-2 shadow-xl", - "bg-emerald-600! hover:bg-emerald-500!", - )} - size="lg" - onClick={handleStartGame} - > - <Play /> - Start - </Button> - ) : ( - <Button - className="px-4 py-2" - size="lg" - onClick={() => setShowLoginModal(true)} - > - <User /> Login - </Button> - )} - </div> - </div> - </div> - - <LoginModal - open={showLoginModal} - onOpenChange={() => setShowLoginModal(false)} - /> - </div> - ); -} diff --git a/packages/ui-new/src/components/config-editor.tsx b/packages/ui-new/src/components/config-editor.tsx deleted file mode 100644 index 129b8f7..0000000 --- a/packages/ui-new/src/components/config-editor.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import type React from "react"; -import { useEffect, useState } from "react"; -import { type ZodType, z } from "zod"; -import { useSettingsStore } from "@/models/settings"; -import type { LauncherConfig } from "@/types"; -import { Button } from "./ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "./ui/dialog"; -import { FieldError } from "./ui/field"; -import { Spinner } from "./ui/spinner"; -import { Textarea } from "./ui/textarea"; - -const launcherConfigSchema: ZodType<LauncherConfig> = z.object({ - minMemory: z.number(), - maxMemory: z.number(), - javaPath: z.string(), - width: z.number(), - height: z.number(), - downloadThreads: z.number(), - customBackgroundPath: z.string().nullable(), - enableGpuAcceleration: z.boolean(), - enableVisualEffects: z.boolean(), - activeEffect: z.string(), - theme: z.string(), - logUploadService: z.string(), - pastebinApiKey: z.string().nullable(), - assistant: z.any(), // TODO: AssistantConfig schema - useSharedCaches: z.boolean(), - keepLegacyPerInstanceStorage: z.boolean(), - featureFlags: z.any(), // TODO: FeatureFlags schema -}); - -export interface ConfigEditorProps - extends Omit<React.ComponentPropsWithoutRef<typeof Dialog>, "onOpenChange"> { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function ConfigEditor({ onOpenChange, ...props }: ConfigEditorProps) { - const settings = useSettingsStore(); - - const [errorMessage, setErrorMessage] = useState<string | null>(null); - const [rawConfigContent, setRawConfigContent] = useState( - JSON.stringify(settings.config, null, 2), - ); - const [isSaving, setIsSaving] = useState(false); - - useEffect(() => { - setRawConfigContent(JSON.stringify(settings.config, null, 2)); - }, [settings.config]); - - const handleSave = async () => { - setIsSaving(true); - setErrorMessage(null); - try { - const validatedConfig = launcherConfigSchema.parse( - JSON.parse(rawConfigContent), - ); - settings.config = validatedConfig; - await settings.save(); - onOpenChange?.(false); - } catch (error) { - setErrorMessage(error instanceof Error ? error.message : String(error)); - } finally { - setIsSaving(false); - } - }; - - return ( - <Dialog onOpenChange={onOpenChange} {...props}> - <DialogContent className="max-w-4xl max-h-[80vh] overflow-hidden"> - <DialogHeader> - <DialogTitle>Edit Configuration</DialogTitle> - <DialogDescription> - Edit the raw JSON configuration file. - </DialogDescription> - </DialogHeader> - - <Textarea - value={rawConfigContent} - onChange={(e) => setRawConfigContent(e.target.value)} - className="font-mono text-sm h-100 resize-none" - spellCheck={false} - aria-invalid={!!errorMessage} - /> - - {errorMessage && <FieldError errors={[{ message: errorMessage }]} />} - - <DialogFooter> - <Button - variant="outline" - onClick={() => onOpenChange?.(false)} - disabled={isSaving} - > - Cancel - </Button> - <Button onClick={handleSave} disabled={isSaving}> - {isSaving && <Spinner />} - Save Changes - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} diff --git a/packages/ui-new/src/components/download-monitor.tsx b/packages/ui-new/src/components/download-monitor.tsx deleted file mode 100644 index f3902d9..0000000 --- a/packages/ui-new/src/components/download-monitor.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import { X } from "lucide-react"; -import { useState } from "react"; - -export function DownloadMonitor() { - const [isVisible, setIsVisible] = useState(true); - - if (!isVisible) return null; - - return ( - <div className="bg-zinc-900/80 backdrop-blur-md border border-zinc-700 rounded-lg shadow-2xl overflow-hidden"> - {/* Header */} - <div className="flex items-center justify-between px-4 py-3 bg-zinc-800/50 border-b border-zinc-700"> - <div className="flex items-center gap-2"> - <div className="w-2 h-2 bg-emerald-500 rounded-full animate-pulse"></div> - <span className="text-sm font-medium text-white">Downloads</span> - </div> - <button - type="button" - onClick={() => setIsVisible(false)} - className="text-zinc-400 hover:text-white transition-colors p-1" - > - <X size={16} /> - </button> - </div> - - {/* Content */} - <div className="p-4"> - <div className="space-y-3"> - {/* Download Item */} - <div className="space-y-1"> - <div className="flex justify-between text-xs"> - <span className="text-zinc-300">Minecraft 1.20.4</span> - <span className="text-zinc-400">65%</span> - </div> - <div className="h-1.5 bg-zinc-800 rounded-full overflow-hidden"> - <div - className="h-full bg-emerald-500 rounded-full transition-all duration-300" - style={{ width: "65%" }} - ></div> - </div> - <div className="flex justify-between text-[10px] text-zinc-500"> - <span>142 MB / 218 MB</span> - <span>2.1 MB/s • 36s remaining</span> - </div> - </div> - - {/* Download Item */} - <div className="space-y-1"> - <div className="flex justify-between text-xs"> - <span className="text-zinc-300">Java 17</span> - <span className="text-zinc-400">100%</span> - </div> - <div className="h-1.5 bg-zinc-800 rounded-full overflow-hidden"> - <div className="h-full bg-emerald-500 rounded-full"></div> - </div> - <div className="text-[10px] text-emerald-400">Completed</div> - </div> - </div> - </div> - </div> - ); -} diff --git a/packages/ui-new/src/components/game-console.tsx b/packages/ui-new/src/components/game-console.tsx deleted file mode 100644 index 6980c8c..0000000 --- a/packages/ui-new/src/components/game-console.tsx +++ /dev/null @@ -1,290 +0,0 @@ -import { Copy, Download, Filter, Search, Trash2, X } from "lucide-react"; -import { useEffect, useRef, useState } from "react"; -import { useLogsStore } from "@/stores/logs-store"; -import { useUIStore } from "@/stores/ui-store"; - -export function GameConsole() { - const uiStore = useUIStore(); - const logsStore = useLogsStore(); - - const [searchTerm, setSearchTerm] = useState(""); - const [selectedLevels, setSelectedLevels] = useState<Set<string>>( - new Set(["info", "warn", "error", "debug", "fatal"]), - ); - const [autoScroll, setAutoScroll] = useState(true); - const consoleEndRef = useRef<HTMLDivElement>(null); - const logsContainerRef = useRef<HTMLDivElement>(null); - - const levelColors: Record<string, string> = { - info: "text-blue-400", - warn: "text-amber-400", - error: "text-red-400", - debug: "text-purple-400", - fatal: "text-rose-400", - }; - - const levelBgColors: Record<string, string> = { - info: "bg-blue-400/10", - warn: "bg-amber-400/10", - error: "bg-red-400/10", - debug: "bg-purple-400/10", - fatal: "bg-rose-400/10", - }; - - // Filter logs based on search term and selected levels - const filteredLogs = logsStore.logs.filter((log) => { - const matchesSearch = - searchTerm === "" || - log.message.toLowerCase().includes(searchTerm.toLowerCase()) || - log.source.toLowerCase().includes(searchTerm.toLowerCase()); - - const matchesLevel = selectedLevels.has(log.level); - - return matchesSearch && matchesLevel; - }); - - // Auto-scroll to bottom when new logs arrive or autoScroll is enabled - useEffect(() => { - if (autoScroll && consoleEndRef.current && filteredLogs.length > 0) { - consoleEndRef.current.scrollIntoView({ behavior: "smooth" }); - } - }, [filteredLogs, autoScroll]); - - // Handle keyboard shortcuts - useEffect(() => { - const handleKeyDown = (e: KeyboardEvent) => { - // Ctrl/Cmd + K to focus search - if ((e.ctrlKey || e.metaKey) && e.key === "k") { - e.preventDefault(); - // Focus search input - const searchInput = document.querySelector( - 'input[type="text"]', - ) as HTMLInputElement; - if (searchInput) searchInput.focus(); - } - // Escape to close console - if (e.key === "Escape") { - uiStore.toggleConsole(); - } - }; - - window.addEventListener("keydown", handleKeyDown); - return () => window.removeEventListener("keydown", handleKeyDown); - }, [uiStore.toggleConsole]); - - const toggleLevel = (level: string) => { - const newLevels = new Set(selectedLevels); - if (newLevels.has(level)) { - newLevels.delete(level); - } else { - newLevels.add(level); - } - setSelectedLevels(newLevels); - }; - - const handleCopyAll = () => { - const logsText = logsStore.exportLogs(filteredLogs); - navigator.clipboard.writeText(logsText); - }; - - const handleExport = () => { - const logsText = logsStore.exportLogs(filteredLogs); - const blob = new Blob([logsText], { type: "text/plain" }); - const url = URL.createObjectURL(blob); - const a = document.createElement("a"); - a.href = url; - a.download = `dropout_logs_${new Date().toISOString().replace(/[:.]/g, "-")}.txt`; - document.body.appendChild(a); - a.click(); - document.body.removeChild(a); - URL.revokeObjectURL(url); - }; - - const handleClear = () => { - logsStore.clear(); - }; - - return ( - <> - {/* Header */} - <div className="flex items-center justify-between p-4 border-b border-zinc-700 bg-[#252526]"> - <div className="flex items-center gap-3"> - <h2 className="text-lg font-bold text-white">Game Console</h2> - <div className="flex items-center gap-1"> - <span className="text-xs text-zinc-400">Logs:</span> - <span className="text-xs font-medium text-emerald-400"> - {filteredLogs.length} - </span> - <span className="text-xs text-zinc-400">/</span> - <span className="text-xs text-zinc-400"> - {logsStore.logs.length} - </span> - </div> - </div> - <button - type="button" - onClick={() => uiStore.toggleConsole()} - className="p-2 text-zinc-400 hover:text-white transition-colors" - > - <X size={20} /> - </button> - </div> - - {/* Toolbar */} - <div className="flex items-center gap-3 p-3 border-b border-zinc-700 bg-[#2D2D30]"> - {/* Search */} - <div className="relative flex-1"> - <Search - className="absolute left-3 top-1/2 transform -translate-y-1/2 text-zinc-500" - size={16} - /> - <input - type="text" - value={searchTerm} - onChange={(e) => setSearchTerm(e.target.value)} - placeholder="Search logs..." - className="w-full pl-10 pr-4 py-2 bg-[#3E3E42] border border-zinc-600 rounded text-sm text-white placeholder:text-zinc-500 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500" - /> - {searchTerm && ( - <button - type="button" - onClick={() => setSearchTerm("")} - className="absolute right-3 top-1/2 transform -translate-y-1/2 text-zinc-400 hover:text-white" - > - × - </button> - )} - </div> - - {/* Level Filters */} - <div className="flex items-center gap-1"> - {Object.entries(levelColors).map(([level, colorClass]) => ( - <button - type="button" - key={level} - onClick={() => toggleLevel(level)} - className={`px-3 py-1.5 text-xs font-medium rounded transition-colors ${ - selectedLevels.has(level) - ? `${levelBgColors[level]} ${colorClass}` - : "bg-[#3E3E42] text-zinc-400 hover:text-white" - }`} - > - {level.toUpperCase()} - </button> - ))} - </div> - - {/* Actions */} - <div className="flex items-center gap-1"> - <button - type="button" - onClick={handleCopyAll} - className="p-2 text-zinc-400 hover:text-white transition-colors" - title="Copy all logs" - > - <Copy size={16} /> - </button> - <button - type="button" - onClick={handleExport} - className="p-2 text-zinc-400 hover:text-white transition-colors" - title="Export logs" - > - <Download size={16} /> - </button> - <button - type="button" - onClick={handleClear} - className="p-2 text-zinc-400 hover:text-white transition-colors" - title="Clear logs" - > - <Trash2 size={16} /> - </button> - </div> - - {/* Auto-scroll Toggle */} - <div className="flex items-center gap-2 pl-2 border-l border-zinc-700"> - <label className="inline-flex items-center cursor-pointer"> - <input - type="checkbox" - checked={autoScroll} - onChange={(e) => setAutoScroll(e.target.checked)} - className="sr-only peer" - /> - <div className="relative w-9 h-5 bg-zinc-700 peer-focus:outline-none peer-focus:ring-blue-800 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-0.5 after:left-0.5 after:bg-white after:border-gray-300 after:border after:rounded-full after:h-4 after:w-4 after:transition-all peer-checked:bg-blue-600"></div> - <span className="ml-2 text-xs text-zinc-400">Auto-scroll</span> - </label> - </div> - </div> - - {/* Logs Container */} - <div - ref={logsContainerRef} - className="flex-1 overflow-y-auto font-mono text-sm bg-[#1E1E1E]" - style={{ fontFamily: "'Cascadia Code', 'Consolas', monospace" }} - > - {filteredLogs.length === 0 ? ( - <div className="flex items-center justify-center h-full"> - <div className="text-center text-zinc-500"> - <Filter className="mx-auto mb-2" size={24} /> - <p>No logs match the current filters</p> - </div> - </div> - ) : ( - <div className="p-4 space-y-1"> - {filteredLogs.map((log) => ( - <div - key={log.id} - className="group hover:bg-white/5 p-2 rounded transition-colors" - > - <div className="flex items-start gap-3"> - <div - className={`px-2 py-0.5 rounded text-xs font-medium ${levelBgColors[log.level]} ${levelColors[log.level]}`} - > - {log.level.toUpperCase()} - </div> - <div className="text-zinc-400 text-xs shrink-0"> - {log.timestamp} - </div> - <div className="text-amber-300 text-xs shrink-0"> - [{log.source}] - </div> - <div className="text-gray-300 flex-1">{log.message}</div> - </div> - </div> - ))} - <div ref={consoleEndRef} /> - </div> - )} - </div> - - {/* Footer */} - <div className="flex items-center justify-between p-3 border-t border-zinc-700 bg-[#252526] text-xs text-zinc-400"> - <div className="flex items-center gap-4"> - <div> - <span>Total: </span> - <span className="text-white">{logsStore.logs.length}</span> - <span> | Filtered: </span> - <span className="text-emerald-400">{filteredLogs.length}</span> - </div> - <div className="flex items-center gap-2"> - <kbd className="px-1.5 py-0.5 bg-[#3E3E42] rounded text-xs"> - Ctrl+K - </kbd> - <span>to search</span> - </div> - </div> - <div> - <span>Updated: </span> - <span> - {new Date().toLocaleTimeString([], { - hour: "2-digit", - minute: "2-digit", - second: "2-digit", - })} - </span> - </div> - </div> - </> - ); -} diff --git a/packages/ui-new/src/components/instance-creation-modal.tsx b/packages/ui-new/src/components/instance-creation-modal.tsx deleted file mode 100644 index 8a2b1b4..0000000 --- a/packages/ui-new/src/components/instance-creation-modal.tsx +++ /dev/null @@ -1,552 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { Loader2, Search } from "lucide-react"; -import { useCallback, useEffect, useMemo, useState } from "react"; -import { toast } from "sonner"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { ScrollArea } from "@/components/ui/scroll-area"; -import { useInstancesStore } from "@/models/instances"; -import { useGameStore } from "@/stores/game-store"; -import type { Version } from "@/types/bindings/manifest"; -import type { FabricLoaderEntry } from "../types/bindings/fabric"; -import type { ForgeVersion as ForgeVersionEntry } from "../types/bindings/forge"; -import type { Instance } from "../types/bindings/instance"; - -interface Props { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function InstanceCreationModal({ open, onOpenChange }: Props) { - const gameStore = useGameStore(); - const instancesStore = useInstancesStore(); - - // Steps: 1 = name, 2 = version, 3 = mod loader - const [step, setStep] = useState<number>(1); - - // Step 1 - const [instanceName, setInstanceName] = useState<string>(""); - - // Step 2 - const [versionSearch, setVersionSearch] = useState<string>(""); - const [versionFilter, setVersionFilter] = useState< - "all" | "release" | "snapshot" - >("release"); - const [selectedVersionUI, setSelectedVersionUI] = useState<Version | null>( - null, - ); - - // Step 3 - const [modLoaderType, setModLoaderType] = useState< - "vanilla" | "fabric" | "forge" - >("vanilla"); - const [fabricLoaders, setFabricLoaders] = useState<FabricLoaderEntry[]>([]); - const [forgeVersions, setForgeVersions] = useState<ForgeVersionEntry[]>([]); - const [selectedFabricLoader, setSelectedFabricLoader] = useState<string>(""); - const [selectedForgeLoader, setSelectedForgeLoader] = useState<string>(""); - const [loadingLoaders, setLoadingLoaders] = useState(false); - - const loadModLoaders = useCallback(async () => { - if (!selectedVersionUI) return; - setLoadingLoaders(true); - setFabricLoaders([]); - setForgeVersions([]); - try { - if (modLoaderType === "fabric") { - const loaders = await invoke<FabricLoaderEntry[]>( - "get_fabric_loaders_for_version", - { - gameVersion: selectedVersionUI.id, - }, - ); - setFabricLoaders(loaders || []); - if (loaders && loaders.length > 0) { - setSelectedFabricLoader(loaders[0].loader.version); - } else { - setSelectedFabricLoader(""); - } - } else if (modLoaderType === "forge") { - const versions = await invoke<ForgeVersionEntry[]>( - "get_forge_versions_for_game", - { - gameVersion: selectedVersionUI.id, - }, - ); - setForgeVersions(versions || []); - if (versions && versions.length > 0) { - // Binding `ForgeVersion` uses `version` (not `id`) — use `.version` here. - setSelectedForgeLoader(versions[0].version); - } else { - setSelectedForgeLoader(""); - } - } - } catch (e) { - console.error("Failed to load mod loaders:", e); - toast.error("Failed to fetch mod loader versions"); - } finally { - setLoadingLoaders(false); - } - }, [modLoaderType, selectedVersionUI]); - - // When entering step 3 and a base version exists, fetch loaders if needed - useEffect(() => { - if (step === 3 && modLoaderType !== "vanilla" && selectedVersionUI) { - loadModLoaders(); - } - }, [step, modLoaderType, selectedVersionUI, loadModLoaders]); - - // Creating state - const [creating, setCreating] = useState(false); - const [errorMessage, setErrorMessage] = useState<string>(""); - - // Derived filtered versions - const filteredVersions = useMemo(() => { - const all = gameStore.versions || []; - let list = all.slice(); - if (versionFilter !== "all") { - list = list.filter((v) => v.type === versionFilter); - } - if (versionSearch.trim()) { - const q = versionSearch.trim().toLowerCase().replace(/。/g, "."); - list = list.filter((v) => v.id.toLowerCase().includes(q)); - } - return list; - }, [gameStore.versions, versionFilter, versionSearch]); - - // Reset when opened/closed - useEffect(() => { - if (open) { - // ensure versions are loaded - gameStore.loadVersions(); - setStep(1); - setInstanceName(""); - setVersionSearch(""); - setVersionFilter("release"); - setSelectedVersionUI(null); - setModLoaderType("vanilla"); - setFabricLoaders([]); - setForgeVersions([]); - setSelectedFabricLoader(""); - setSelectedForgeLoader(""); - setErrorMessage(""); - setCreating(false); - } - }, [open, gameStore.loadVersions]); - - function validateStep1(): boolean { - if (!instanceName.trim()) { - setErrorMessage("Please enter an instance name"); - return false; - } - setErrorMessage(""); - return true; - } - - function validateStep2(): boolean { - if (!selectedVersionUI) { - setErrorMessage("Please select a Minecraft version"); - return false; - } - setErrorMessage(""); - return true; - } - - async function handleNext() { - setErrorMessage(""); - if (step === 1) { - if (!validateStep1()) return; - setStep(2); - } else if (step === 2) { - if (!validateStep2()) return; - setStep(3); - } - } - - function handleBack() { - setErrorMessage(""); - setStep((s) => Math.max(1, s - 1)); - } - - async function handleCreate() { - if (!validateStep1() || !validateStep2()) return; - setCreating(true); - setErrorMessage(""); - - try { - // Step 1: create instance - const instance = await invoke<Instance>("create_instance", { - name: instanceName.trim(), - }); - - // If selectedVersion provided, install it - if (selectedVersionUI) { - try { - await invoke("install_version", { - instanceId: instance.id, - versionId: selectedVersionUI.id, - }); - } catch (err) { - console.error("Failed to install base version:", err); - // continue - instance created but version install failed - toast.error( - `Failed to install version ${selectedVersionUI.id}: ${String(err)}`, - ); - } - } - - // If mod loader selected, install it - if (modLoaderType === "fabric" && selectedFabricLoader) { - try { - await invoke("install_fabric", { - instanceId: instance.id, - gameVersion: selectedVersionUI?.id ?? "", - loaderVersion: selectedFabricLoader, - }); - } catch (err) { - console.error("Failed to install Fabric:", err); - toast.error(`Failed to install Fabric: ${String(err)}`); - } - } else if (modLoaderType === "forge" && selectedForgeLoader) { - try { - await invoke("install_forge", { - instanceId: instance.id, - gameVersion: selectedVersionUI?.id ?? "", - installerVersion: selectedForgeLoader, - }); - } catch (err) { - console.error("Failed to install Forge:", err); - toast.error(`Failed to install Forge: ${String(err)}`); - } - } - - // Refresh instances list - await instancesStore.refresh(); - - toast.success("Instance created successfully"); - onOpenChange(false); - } catch (e) { - console.error("Failed to create instance:", e); - setErrorMessage(String(e)); - toast.error(`Failed to create instance: ${e}`); - } finally { - setCreating(false); - } - } - - // UI pieces - const StepIndicator = () => ( - <div className="flex gap-2 w-full"> - <div - className={`flex-1 h-1 rounded ${step >= 1 ? "bg-indigo-500" : "bg-zinc-700"}`} - /> - <div - className={`flex-1 h-1 rounded ${step >= 2 ? "bg-indigo-500" : "bg-zinc-700"}`} - /> - <div - className={`flex-1 h-1 rounded ${step >= 3 ? "bg-indigo-500" : "bg-zinc-700"}`} - /> - </div> - ); - - return ( - <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="w-full max-w-3xl max-h-[90vh] overflow-hidden"> - <DialogHeader> - <DialogTitle>Create New Instance</DialogTitle> - <DialogDescription> - Multi-step wizard — create an instance and optionally install a - version or mod loader. - </DialogDescription> - </DialogHeader> - - <div className="px-6"> - <div className="pt-4 pb-6"> - <StepIndicator /> - </div> - - {/* Step 1 - Name */} - {step === 1 && ( - <div className="space-y-4"> - <div> - <label - htmlFor="instance-name" - className="block text-sm font-medium mb-2" - > - Instance Name - </label> - <Input - id="instance-name" - placeholder="My Minecraft Instance" - value={instanceName} - onChange={(e) => setInstanceName(e.target.value)} - disabled={creating} - /> - </div> - <p className="text-xs text-muted-foreground"> - Give your instance a memorable name. - </p> - </div> - )} - - {/* Step 2 - Version selection */} - {step === 2 && ( - <div className="space-y-4"> - <div className="flex gap-3"> - <div className="relative flex-1"> - <Search className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" /> - <Input - value={versionSearch} - onChange={(e) => setVersionSearch(e.target.value)} - placeholder="Search versions..." - className="pl-9" - /> - </div> - - <div className="flex gap-2"> - <Button - type="button" - variant={versionFilter === "all" ? "default" : "outline"} - onClick={() => setVersionFilter("all")} - > - All - </Button> - <Button - type="button" - variant={ - versionFilter === "release" ? "default" : "outline" - } - onClick={() => setVersionFilter("release")} - > - Release - </Button> - <Button - type="button" - variant={ - versionFilter === "snapshot" ? "default" : "outline" - } - onClick={() => setVersionFilter("snapshot")} - > - Snapshot - </Button> - </div> - </div> - - <ScrollArea className="max-h-[36vh]"> - <div className="space-y-2 py-2"> - {gameStore.versions.length === 0 ? ( - <div className="flex items-center justify-center py-8 text-muted-foreground"> - <Loader2 className="animate-spin mr-2" /> - Loading versions... - </div> - ) : filteredVersions.length === 0 ? ( - <div className="text-center py-8 text-muted-foreground"> - No matching versions found - </div> - ) : ( - filteredVersions.map((v) => { - const isSelected = selectedVersionUI?.id === v.id; - return ( - <button - key={v.id} - type="button" - onClick={() => setSelectedVersionUI(v)} - className={`w-full text-left p-3 rounded-lg border transition-colors ${ - isSelected - ? "bg-indigo-50 dark:bg-indigo-600/20 border-indigo-200" - : "bg-white/40 dark:bg-white/5 border-black/5 dark:border-white/5 hover:bg-white/60" - }`} - > - <div className="flex items-center justify-between"> - <div> - <div className="font-mono font-bold">{v.id}</div> - <div className="text-xs text-muted-foreground mt-1"> - {v.type}{" "} - {v.releaseTime - ? ` • ${new Date(v.releaseTime).toLocaleDateString()}` - : ""} - </div> - </div> - {v.javaVersion && ( - <div className="text-sm"> - Java {v.javaVersion} - </div> - )} - </div> - </button> - ); - }) - )} - </div> - </ScrollArea> - </div> - )} - - {/* Step 3 - Mod loader */} - {step === 3 && ( - <div className="space-y-4"> - <div> - <div className="text-sm font-medium mb-2">Mod Loader Type</div> - <div className="flex gap-3"> - <Button - type="button" - variant={ - modLoaderType === "vanilla" ? "default" : "outline" - } - onClick={() => setModLoaderType("vanilla")} - > - Vanilla - </Button> - <Button - type="button" - variant={modLoaderType === "fabric" ? "default" : "outline"} - onClick={() => setModLoaderType("fabric")} - > - Fabric - </Button> - <Button - type="button" - variant={modLoaderType === "forge" ? "default" : "outline"} - onClick={() => setModLoaderType("forge")} - > - Forge - </Button> - </div> - </div> - - {modLoaderType === "fabric" && ( - <div> - {loadingLoaders ? ( - <div className="flex items-center gap-2"> - <Loader2 className="animate-spin" /> - Loading Fabric versions... - </div> - ) : fabricLoaders.length > 0 ? ( - <div className="space-y-2"> - <select - value={selectedFabricLoader} - onChange={(e) => - setSelectedFabricLoader(e.target.value) - } - className="w-full px-3 py-2 rounded border bg-transparent" - > - {fabricLoaders.map((f) => ( - <option - key={f.loader.version} - value={f.loader.version} - > - {f.loader.version}{" "} - {f.loader.stable ? "(Stable)" : "(Beta)"} - </option> - ))} - </select> - </div> - ) : ( - <p className="text-sm text-muted-foreground"> - No Fabric loaders available for this version - </p> - )} - </div> - )} - - {modLoaderType === "forge" && ( - <div> - {loadingLoaders ? ( - <div className="flex items-center gap-2"> - <Loader2 className="animate-spin" /> - Loading Forge versions... - </div> - ) : forgeVersions.length > 0 ? ( - <div className="space-y-2"> - <select - value={selectedForgeLoader} - onChange={(e) => setSelectedForgeLoader(e.target.value)} - className="w-full px-3 py-2 rounded border bg-transparent" - > - {forgeVersions.map((f) => ( - // binding ForgeVersion uses `version` as the identifier - <option key={f.version} value={f.version}> - {f.version} - </option> - ))} - </select> - </div> - ) : ( - <p className="text-sm text-muted-foreground"> - No Forge versions available for this version - </p> - )} - </div> - )} - </div> - )} - - {errorMessage && ( - <div className="text-sm text-red-400 mt-3">{errorMessage}</div> - )} - </div> - - <DialogFooter> - <div className="w-full flex justify-between items-center"> - <div> - <Button - type="button" - variant="ghost" - onClick={() => { - // cancel - onOpenChange(false); - }} - disabled={creating} - > - Cancel - </Button> - </div> - - <div className="flex gap-2"> - {step > 1 && ( - <Button - type="button" - variant="outline" - onClick={handleBack} - disabled={creating} - > - Back - </Button> - )} - - {step < 3 ? ( - <Button type="button" onClick={handleNext} disabled={creating}> - Next - </Button> - ) : ( - <Button - type="button" - onClick={handleCreate} - disabled={creating} - > - {creating ? ( - <> - <Loader2 className="mr-2 h-4 w-4 animate-spin" /> - Creating... - </> - ) : ( - "Create" - )} - </Button> - )} - </div> - </div> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} - -export default InstanceCreationModal; diff --git a/packages/ui-new/src/components/instance-editor-modal.tsx b/packages/ui-new/src/components/instance-editor-modal.tsx deleted file mode 100644 index f880c20..0000000 --- a/packages/ui-new/src/components/instance-editor-modal.tsx +++ /dev/null @@ -1,548 +0,0 @@ -import { invoke } from "@tauri-apps/api/core"; -import { Folder, Loader2, Save, Trash2, X } from "lucide-react"; -import { useCallback, useEffect, useState } from "react"; -import { toast } from "sonner"; - -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "@/components/ui/dialog"; -import { Input } from "@/components/ui/input"; -import { Textarea } from "@/components/ui/textarea"; - -import { toNumber } from "@/lib/tsrs-utils"; -import { useInstancesStore } from "@/models/instances"; -import { useSettingsStore } from "@/models/settings"; -import type { FileInfo } from "../types/bindings/core"; -import type { Instance } from "../types/bindings/instance"; - -type Props = { - open: boolean; - instance: Instance | null; - onOpenChange: (open: boolean) => void; -}; - -export function InstanceEditorModal({ open, instance, onOpenChange }: Props) { - const instancesStore = useInstancesStore(); - const { config } = useSettingsStore(); - - const [activeTab, setActiveTab] = useState< - "info" | "version" | "files" | "settings" - >("info"); - const [saving, setSaving] = useState(false); - const [errorMessage, setErrorMessage] = useState(""); - - // Info tab fields - const [editName, setEditName] = useState(""); - const [editNotes, setEditNotes] = useState(""); - - // Files tab state - const [selectedFileFolder, setSelectedFileFolder] = useState< - "mods" | "resourcepacks" | "shaderpacks" | "saves" | "screenshots" - >("mods"); - const [fileList, setFileList] = useState<FileInfo[]>([]); - const [loadingFiles, setLoadingFiles] = useState(false); - const [deletingPath, setDeletingPath] = useState<string | null>(null); - - // Version tab state (placeholder - the Svelte implementation used a ModLoaderSelector component) - // React versions-view/instance-creation handle mod loader installs; here we show basic current info. - - // Settings tab fields - const [editMemoryMin, setEditMemoryMin] = useState<number>(0); - const [editMemoryMax, setEditMemoryMax] = useState<number>(0); - const [editJavaArgs, setEditJavaArgs] = useState<string>(""); - - // initialize when open & instance changes - useEffect(() => { - if (open && instance) { - setActiveTab("info"); - setSaving(false); - setErrorMessage(""); - setEditName(instance.name || ""); - setEditNotes(instance.notes ?? ""); - setEditMemoryMin( - (instance.memoryOverride && toNumber(instance.memoryOverride.min)) ?? - config?.minMemory ?? - 512, - ); - setEditMemoryMax( - (instance.memoryOverride && toNumber(instance.memoryOverride.max)) ?? - config?.maxMemory ?? - 2048, - ); - setEditJavaArgs(instance.jvmArgsOverride ?? ""); - setFileList([]); - setSelectedFileFolder("mods"); - } - }, [open, instance, config?.minMemory, config?.maxMemory]); - - // load files when switching to files tab - const loadFileList = useCallback( - async ( - folder: - | "mods" - | "resourcepacks" - | "shaderpacks" - | "saves" - | "screenshots", - ) => { - if (!instance) return; - setLoadingFiles(true); - try { - const files = await invoke<FileInfo[]>("list_instance_directory", { - instanceId: instance.id, - folder, - }); - setFileList(files || []); - } catch (err) { - console.error("Failed to load files:", err); - toast.error("Failed to load files: " + String(err)); - setFileList([]); - } finally { - setLoadingFiles(false); - } - }, - [instance], - ); - - useEffect(() => { - if (open && instance && activeTab === "files") { - // explicitly pass the selected folder so loadFileList doesn't rely on stale closures - loadFileList(selectedFileFolder); - } - }, [activeTab, open, instance, selectedFileFolder, loadFileList]); - - async function changeFolder( - folder: "mods" | "resourcepacks" | "shaderpacks" | "saves" | "screenshots", - ) { - setSelectedFileFolder(folder); - // reload the list for the newly selected folder - if (open && instance) await loadFileList(folder); - } - - async function deleteFile(filePath: string) { - if ( - !confirm( - `Are you sure you want to delete "${filePath.split("/").pop()}"?`, - ) - ) { - return; - } - setDeletingPath(filePath); - try { - await invoke("delete_instance_file", { path: filePath }); - // refresh the currently selected folder - await loadFileList(selectedFileFolder); - toast.success("Deleted"); - } catch (err) { - console.error("Failed to delete file:", err); - toast.error("Failed to delete file: " + String(err)); - } finally { - setDeletingPath(null); - } - } - - async function openInExplorer(filePath: string) { - try { - await invoke("open_file_explorer", { path: filePath }); - } catch (err) { - console.error("Failed to open in explorer:", err); - toast.error("Failed to open file explorer: " + String(err)); - } - } - - async function saveChanges() { - if (!instance) return; - if (!editName.trim()) { - setErrorMessage("Instance name cannot be empty"); - return; - } - setSaving(true); - setErrorMessage(""); - try { - // Build updated instance shape compatible with backend - const updatedInstance: Instance = { - ...instance, - name: editName.trim(), - // some bindings may use camelCase; set optional string fields to null when empty - notes: editNotes.trim() ? editNotes.trim() : null, - memoryOverride: { - min: editMemoryMin, - max: editMemoryMax, - }, - jvmArgsOverride: editJavaArgs.trim() ? editJavaArgs.trim() : null, - }; - - await instancesStore.update(updatedInstance as Instance); - toast.success("Instance saved"); - onOpenChange(false); - } catch (err) { - console.error("Failed to save instance:", err); - setErrorMessage(String(err)); - toast.error("Failed to save instance: " + String(err)); - } finally { - setSaving(false); - } - } - - function formatFileSize(bytesBig: FileInfo["size"]): string { - const bytes = Number(bytesBig ?? 0); - if (bytes === 0) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB", "TB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${Math.round((bytes / k ** i) * 100) / 100} ${sizes[i]}`; - } - - function formatDate( - tsBig?: - | FileInfo["modified"] - | Instance["createdAt"] - | Instance["lastPlayed"], - ) { - if (tsBig === undefined || tsBig === null) return ""; - const n = toNumber(tsBig); - // tsrs bindings often use seconds for createdAt/lastPlayed; if value looks like seconds use *1000 - const maybeMs = n > 1e12 ? n : n * 1000; - return new Date(maybeMs).toLocaleDateString(); - } - - return ( - <Dialog open={open} onOpenChange={onOpenChange}> - <DialogContent className="w-full max-w-4xl max-h-[90vh] overflow-hidden"> - <DialogHeader> - <div className="flex items-center justify-between gap-4"> - <div> - <DialogTitle>Edit Instance</DialogTitle> - <DialogDescription>{instance?.name ?? ""}</DialogDescription> - </div> - <div className="flex items-center gap-2"> - <button - type="button" - onClick={() => onOpenChange(false)} - disabled={saving} - className="p-2 rounded hover:bg-zinc-800 text-zinc-400" - aria-label="Close" - > - <X /> - </button> - </div> - </div> - </DialogHeader> - - {/* Tab Navigation */} - <div className="flex gap-1 px-6 pt-2 border-b border-zinc-700"> - {[ - { id: "info", label: "Info" }, - { id: "version", label: "Version" }, - { id: "files", label: "Files" }, - { id: "settings", label: "Settings" }, - ].map((tab) => ( - <button - type="button" - key={tab.id} - onClick={() => - setActiveTab( - tab.id as "info" | "version" | "files" | "settings", - ) - } - className={`px-4 py-2 text-sm font-medium transition-colors rounded-t-lg ${ - activeTab === tab.id - ? "bg-indigo-600 text-white" - : "bg-zinc-800 text-zinc-400 hover:text-white" - }`} - > - {tab.label} - </button> - ))} - </div> - - {/* Content */} - <div className="p-6 overflow-y-auto max-h-[60vh]"> - {activeTab === "info" && ( - <div className="space-y-4"> - <div> - <label - htmlFor="instance-name-edit" - className="block text-sm font-medium mb-2" - > - Instance Name - </label> - <Input - id="instance-name-edit" - value={editName} - onChange={(e) => setEditName(e.target.value)} - disabled={saving} - /> - </div> - - <div> - <label - htmlFor="instance-notes-edit" - className="block text-sm font-medium mb-2" - > - Notes - </label> - <Textarea - id="instance-notes-edit" - value={editNotes} - onChange={(e) => setEditNotes(e.target.value)} - rows={4} - disabled={saving} - /> - </div> - - <div className="grid grid-cols-2 gap-4 text-sm"> - <div className="p-3 bg-zinc-800 rounded-lg"> - <p className="text-zinc-400">Created</p> - <p className="text-white font-medium"> - {instance?.createdAt ? formatDate(instance.createdAt) : "-"} - </p> - </div> - <div className="p-3 bg-zinc-800 rounded-lg"> - <p className="text-zinc-400">Last Played</p> - <p className="text-white font-medium"> - {instance?.lastPlayed - ? formatDate(instance.lastPlayed) - : "Never"} - </p> - </div> - <div className="p-3 bg-zinc-800 rounded-lg"> - <p className="text-zinc-400">Game Directory</p> - <p - className="text-white font-medium text-xs truncate" - title={instance?.gameDir ?? ""} - > - {instance?.gameDir - ? String(instance.gameDir).split("/").pop() - : ""} - </p> - </div> - <div className="p-3 bg-zinc-800 rounded-lg"> - <p className="text-zinc-400">Current Version</p> - <p className="text-white font-medium"> - {instance?.versionId ?? "None"} - </p> - </div> - </div> - </div> - )} - - {activeTab === "version" && ( - <div className="space-y-4"> - {instance?.versionId ? ( - <div className="p-4 bg-indigo-500/10 border border-indigo-500/30 rounded-lg"> - <p className="text-sm text-indigo-400"> - Currently playing:{" "} - <span className="font-medium">{instance.versionId}</span> - {instance.modLoader && ( - <> - {" "} - with{" "} - <span className="capitalize">{instance.modLoader}</span> - {instance.modLoaderVersion - ? ` ${instance.modLoaderVersion}` - : ""} - </> - )} - </p> - </div> - ) : ( - <div className="text-sm text-zinc-400"> - No version selected for this instance - </div> - )} - - <div> - <p className="text-sm font-medium mb-2"> - Change Version / Mod Loader - </p> - <p className="text-xs text-zinc-400"> - Use the Versions page to install new game versions or mod - loaders, then set them here. - </p> - </div> - </div> - )} - - {activeTab === "files" && ( - <div className="space-y-4"> - <div className="flex gap-2 flex-wrap"> - {( - [ - "mods", - "resourcepacks", - "shaderpacks", - "saves", - "screenshots", - ] as const - ).map((folder) => ( - <button - type="button" - key={folder} - onClick={() => changeFolder(folder)} - className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${ - selectedFileFolder === folder - ? "bg-indigo-600 text-white" - : "bg-zinc-800 text-zinc-400 hover:text-white" - }`} - > - {folder} - </button> - ))} - </div> - - {loadingFiles ? ( - <div className="flex items-center gap-2 text-zinc-400 py-8 justify-center"> - <Loader2 className="animate-spin" /> - Loading files... - </div> - ) : fileList.length === 0 ? ( - <div className="text-center py-8 text-zinc-500"> - No files in this folder - </div> - ) : ( - <div className="space-y-2"> - {fileList.map((file) => ( - <div - key={file.path} - className="flex items-center justify-between p-3 bg-zinc-800 rounded-lg hover:bg-zinc-700 transition-colors" - > - <div className="flex-1 min-w-0"> - <p className="font-medium text-white truncate"> - {file.name} - </p> - <p className="text-xs text-zinc-400"> - {file.isDirectory - ? "Folder" - : formatFileSize(file.size)}{" "} - • {formatDate(file.modified)} - </p> - </div> - <div className="flex gap-2 ml-4"> - <button - type="button" - onClick={() => openInExplorer(file.path)} - title="Open in explorer" - className="p-2 rounded-lg hover:bg-zinc-600 text-zinc-400 hover:text-white transition-colors" - > - <Folder /> - </button> - <button - type="button" - onClick={() => deleteFile(file.path)} - disabled={deletingPath === file.path} - title="Delete" - className="p-2 rounded-lg hover:bg-red-600/20 text-red-400 hover:text-red-300 transition-colors disabled:opacity-50" - > - {deletingPath === file.path ? ( - <Loader2 className="animate-spin" /> - ) : ( - <Trash2 /> - )} - </button> - </div> - </div> - ))} - </div> - )} - </div> - )} - - {activeTab === "settings" && ( - <div className="space-y-4"> - <div> - <label - htmlFor="min-memory-edit" - className="block text-sm font-medium mb-2" - > - Minimum Memory (MB) - </label> - <Input - id="min-memory-edit" - type="number" - value={String(editMemoryMin)} - onChange={(e) => setEditMemoryMin(Number(e.target.value))} - disabled={saving} - /> - <p className="text-xs text-zinc-400 mt-1"> - Default: {config?.minMemory} MB - </p> - </div> - - <div> - <label - htmlFor="max-memory-edit" - className="block text-sm font-medium mb-2" - > - Maximum Memory (MB) - </label> - <Input - id="max-memory-edit" - type="number" - value={String(editMemoryMax)} - onChange={(e) => setEditMemoryMax(Number(e.target.value))} - disabled={saving} - /> - <p className="text-xs text-zinc-400 mt-1"> - Default: {config?.maxMemory} MB - </p> - </div> - - <div> - <label - htmlFor="jvm-args-edit" - className="block text-sm font-medium mb-2" - > - JVM Arguments (Advanced) - </label> - <Textarea - id="jvm-args-edit" - value={editJavaArgs} - onChange={(e) => setEditJavaArgs(e.target.value)} - rows={4} - disabled={saving} - /> - </div> - </div> - )} - </div> - - {errorMessage && ( - <div className="px-6 text-sm text-red-400">{errorMessage}</div> - )} - - <DialogFooter> - <div className="flex items-center justify-between w-full"> - <div /> - <div className="flex gap-2"> - <Button - variant="outline" - onClick={() => { - onOpenChange(false); - }} - > - Cancel - </Button> - <Button onClick={saveChanges} disabled={saving}> - {saving ? ( - <Loader2 className="animate-spin mr-2" /> - ) : ( - <Save className="mr-2" /> - )} - Save - </Button> - </div> - </div> - </DialogFooter> - </DialogContent> - </Dialog> - ); -} - -export default InstanceEditorModal; diff --git a/packages/ui-new/src/components/login-modal.tsx b/packages/ui-new/src/components/login-modal.tsx deleted file mode 100644 index 49596da..0000000 --- a/packages/ui-new/src/components/login-modal.tsx +++ /dev/null @@ -1,188 +0,0 @@ -import { Mail, User } from "lucide-react"; -import { useCallback, useState } from "react"; -import { toast } from "sonner"; -import { useAuthStore } from "@/models/auth"; -import { Button } from "./ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, -} from "./ui/dialog"; -import { - Field, - FieldDescription, - FieldError, - FieldGroup, - FieldLabel, -} from "./ui/field"; -import { Input } from "./ui/input"; - -export interface LoginModalProps - extends Omit<React.ComponentPropsWithoutRef<typeof Dialog>, "onOpenChange"> { - open: boolean; - onOpenChange: (open: boolean) => void; -} - -export function LoginModal({ onOpenChange, ...props }: LoginModalProps) { - const authStore = useAuthStore(); - - const [offlineUsername, setOfflineUsername] = useState<string>(""); - const [errorMessage, setErrorMessage] = useState<string>(""); - const [isLoggingIn, setIsLoggingIn] = useState<boolean>(false); - - const handleMicrosoftLogin = useCallback(async () => { - setIsLoggingIn(true); - authStore.setLoginMode("microsoft"); - try { - await authStore.loginOnline(() => onOpenChange?.(false)); - } catch (error) { - const err = error as Error; - console.error("Failed to login with Microsoft:", err); - setErrorMessage(err.message); - } finally { - setIsLoggingIn(false); - } - }, [authStore.loginOnline, authStore.setLoginMode, onOpenChange]); - - const handleOfflineLogin = useCallback(async () => { - setIsLoggingIn(true); - try { - await authStore.loginOffline(offlineUsername); - toast.success("Logged in offline successfully"); - onOpenChange?.(false); - } catch (error) { - const err = error as Error; - console.error("Failed to login offline:", err); - setErrorMessage(err.message); - } finally { - setIsLoggingIn(false); - } - }, [authStore, offlineUsername, onOpenChange]); - - return ( - <Dialog onOpenChange={onOpenChange} {...props}> - <DialogContent className="md:max-w-md"> - <DialogHeader> - <DialogTitle>Login</DialogTitle> - <DialogDescription> - Login to your Minecraft account or play offline - </DialogDescription> - </DialogHeader> - <div className="p-4 w-full overflow-hidden"> - {!authStore.loginMode && ( - <div className="flex flex-col space-y-4"> - <Button size="lg" onClick={handleMicrosoftLogin}> - <Mail /> - Login with Microsoft - </Button> - <Button - variant="secondary" - onClick={() => authStore.setLoginMode("offline")} - size="lg" - > - <User /> - Login Offline - </Button> - </div> - )} - {authStore.loginMode === "microsoft" && ( - <div className="flex flex-col space-y-4"> - <button - type="button" - className="text-4xl font-bold text-center bg-accent p-4 cursor-pointer" - onClick={() => { - if (authStore.deviceCode?.userCode) { - navigator.clipboard?.writeText( - authStore.deviceCode?.userCode, - ); - toast.success("Copied to clipboard"); - } - }} - > - {authStore.deviceCode?.userCode} - </button> - <span className="text-muted-foreground w-full overflow-hidden text-ellipsis"> - To sign in, use a web browser to open the page{" "} - <a href={authStore.deviceCode?.verificationUri}> - {authStore.deviceCode?.verificationUri} - </a>{" "} - and enter the code{" "} - <code - className="font-semibold cursor-pointer" - onClick={() => { - if (authStore.deviceCode?.userCode) { - navigator.clipboard?.writeText( - authStore.deviceCode?.userCode, - ); - } - }} - onKeyDown={() => { - if (authStore.deviceCode?.userCode) { - navigator.clipboard?.writeText( - authStore.deviceCode?.userCode, - ); - } - }} - > - {authStore.deviceCode?.userCode} - </code>{" "} - to authenticate, this code will be expired in{" "} - {authStore.deviceCode?.expiresIn} seconds. - </span> - <FieldError>{errorMessage}</FieldError> - </div> - )} - {authStore.loginMode === "offline" && ( - <FieldGroup> - <Field> - <FieldLabel>Username</FieldLabel> - <FieldDescription> - Enter a username to play offline - </FieldDescription> - <Input - value={offlineUsername} - onChange={(e) => { - setOfflineUsername(e.target.value); - setErrorMessage(""); - }} - aria-invalid={!!errorMessage} - /> - <FieldError>{errorMessage}</FieldError> - </Field> - </FieldGroup> - )} - </div> - <DialogFooter> - <div className="flex flex-col justify-center items-center"> - <span className="text-xs text-muted-foreground "> - {authStore.statusMessage} - </span> - </div> - <Button - variant="outline" - onClick={() => { - if (authStore.loginMode) { - if (authStore.loginMode === "microsoft") { - authStore.cancelLoginOnline(); - } - authStore.setLoginMode(null); - } else { - onOpenChange?.(false); - } - }} - > - Cancel - </Button> - {authStore.loginMode === "offline" && ( - <Button onClick={handleOfflineLogin} disabled={isLoggingIn}> - Login - </Button> - )} - </DialogFooter> - </DialogContent> - </Dialog> - ); -} diff --git a/packages/ui-new/src/components/particle-background.tsx b/packages/ui-new/src/components/particle-background.tsx deleted file mode 100644 index 2e0b15a..0000000 --- a/packages/ui-new/src/components/particle-background.tsx +++ /dev/null @@ -1,63 +0,0 @@ -import { useEffect, useRef } from "react"; -import { SaturnEffect } from "../lib/effects/SaturnEffect"; - -export function ParticleBackground() { - const canvasRef = useRef<HTMLCanvasElement | null>(null); - const effectRef = useRef<SaturnEffect | null>(null); - - useEffect(() => { - const canvas = canvasRef.current; - if (!canvas) return; - - // Instantiate SaturnEffect and attach to canvas - let effect: SaturnEffect | null = null; - try { - effect = new SaturnEffect(canvas); - effectRef.current = effect; - } catch (err) { - // If effect fails, silently degrade (keep background blank) - // eslint-disable-next-line no-console - console.warn("SaturnEffect initialization failed:", err); - } - - const resizeHandler = () => { - if (effectRef.current) { - try { - effectRef.current.resize(window.innerWidth, window.innerHeight); - } catch { - // ignore - } - } - }; - - window.addEventListener("resize", resizeHandler); - - // Expose getter for HomeView interactions (getSaturnEffect) - // HomeView will call window.getSaturnEffect()?.handleMouseDown/Move/Up - ( - window as unknown as { getSaturnEffect?: () => SaturnEffect | null } - ).getSaturnEffect = () => effectRef.current; - - return () => { - window.removeEventListener("resize", resizeHandler); - if (effectRef.current) { - try { - effectRef.current.destroy(); - } catch { - // ignore - } - } - effectRef.current = null; - ( - window as unknown as { getSaturnEffect?: () => SaturnEffect | null } - ).getSaturnEffect = undefined; - }; - }, []); - - return ( - <canvas - ref={canvasRef} - className="absolute inset-0 z-0 pointer-events-none" - /> - ); -} diff --git a/packages/ui-new/src/components/sidebar.tsx b/packages/ui-new/src/components/sidebar.tsx deleted file mode 100644 index 0147b0a..0000000 --- a/packages/ui-new/src/components/sidebar.tsx +++ /dev/null @@ -1,185 +0,0 @@ -import { Folder, Home, LogOutIcon, Settings } from "lucide-react"; -import { useLocation, useNavigate } from "react-router"; -import { cn } from "@/lib/utils"; -import { useAuthStore } from "@/models/auth"; -import { Button } from "./ui/button"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuTrigger, -} from "./ui/dropdown-menu"; -import { UserAvatar } from "./user-avatar"; - -interface NavItemProps { - Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>; - label: string; - to: string; -} - -function NavItem({ Icon, label, to }: NavItemProps) { - const navigate = useNavigate(); - const location = useLocation(); - const isActive = location.pathname === to; - - const handleClick = () => { - navigate(to); - }; - - return ( - <Button - variant="ghost" - className={cn( - "w-fit lg:w-full justify-center lg:justify-start", - isActive && "relative bg-accent", - )} - size="lg" - onClick={handleClick} - > - <Icon className="size-5" strokeWidth={isActive ? 2.5 : 2} /> - <span className="hidden lg:block text-sm relative z-10">{label}</span> - {isActive && ( - <div className="absolute left-0 top-1/2 -translate-y-1/2 w-0.5 h-4 bg-black dark:bg-white rounded-r-full hidden lg:block"></div> - )} - </Button> - ); -} - -export function Sidebar() { - const authStore = useAuthStore(); - - return ( - <aside - className={cn( - "flex flex-col items-center lg:items-start", - "bg-sidebar transition-all duration-300", - "w-20 lg:w-64 shrink-0 py-6 h-full", - )} - > - {/* Logo Area */} - <div className="h-16 w-full flex items-center justify-center lg:justify-start lg:px-6 mb-6"> - {/* Icon Logo (Small) */} - <div className="lg:hidden text-black dark:text-white"> - <svg - width="32" - height="32" - viewBox="0 0 100 100" - fill="none" - xmlns="http://www.w3.org/2000/svg" - > - <title>Logo</title> - <path - d="M25 25 L50 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - <path - d="M25 75 L50 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - <path - d="M50 50 L75 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - <circle cx="25" cy="25" r="8" fill="currentColor" stroke="none" /> - <circle cx="25" cy="50" r="8" fill="currentColor" stroke="none" /> - <circle cx="25" cy="75" r="8" fill="currentColor" stroke="none" /> - <circle cx="50" cy="50" r="10" fill="currentColor" stroke="none" /> - <circle cx="75" cy="50" r="8" fill="currentColor" stroke="none" /> - </svg> - </div> - {/* Full Logo (Large) */} - <div className="hidden lg:flex items-center gap-3 font-bold text-xl tracking-tighter dark:text-white text-black"> - <svg - width="42" - height="42" - viewBox="0 0 100 100" - fill="none" - xmlns="http://www.w3.org/2000/svg" - className="shrink-0" - > - <title>Logo</title> - <path - d="M25 25 L50 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - <path - d="M25 75 L50 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - <path - d="M50 50 L75 50" - stroke="currentColor" - strokeWidth="4" - strokeLinecap="round" - /> - - <circle cx="25" cy="25" r="8" fill="currentColor" stroke="none" /> - <circle cx="25" cy="50" r="8" fill="currentColor" stroke="none" /> - <circle cx="25" cy="75" r="8" fill="currentColor" stroke="none" /> - - <circle - cx="50" - cy="25" - r="7" - stroke="currentColor" - strokeWidth="2" - strokeDasharray="4 2" - fill="none" - className="opacity-30" - /> - <circle - cx="50" - cy="75" - r="7" - stroke="currentColor" - strokeWidth="2" - strokeDasharray="4 2" - fill="none" - className="opacity-30" - /> - <circle cx="50" cy="50" r="10" fill="currentColor" stroke="none" /> - <circle cx="75" cy="50" r="8" fill="currentColor" stroke="none" /> - </svg> - - <span>DROPOUT</span> - </div> - </div> - - <nav className="w-full flex flex-col space-y-1 px-3 items-center"> - <NavItem Icon={Home} label="Overview" to="/" /> - <NavItem Icon={Folder} label="Instances" to="/instances" /> - <NavItem Icon={Settings} label="Settings" to="/settings" /> - </nav> - - <div className="flex-1 flex flex-col justify-end"> - <DropdownMenu> - <DropdownMenuTrigger render={<UserAvatar />}> - Open - </DropdownMenuTrigger> - <DropdownMenuContent align="end" side="right" sideOffset={20}> - <DropdownMenuGroup> - <DropdownMenuItem - variant="destructive" - onClick={authStore.logout} - > - <LogOutIcon /> - Logout - </DropdownMenuItem> - </DropdownMenuGroup> - </DropdownMenuContent> - </DropdownMenu> - </div> - </aside> - ); -} diff --git a/packages/ui-new/src/components/ui/avatar.tsx b/packages/ui-new/src/components/ui/avatar.tsx deleted file mode 100644 index 9fd72a2..0000000 --- a/packages/ui-new/src/components/ui/avatar.tsx +++ /dev/null @@ -1,107 +0,0 @@ -import { Avatar as AvatarPrimitive } from "@base-ui/react/avatar"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Avatar({ - className, - size = "default", - ...props -}: AvatarPrimitive.Root.Props & { - size?: "default" | "sm" | "lg"; -}) { - return ( - <AvatarPrimitive.Root - data-slot="avatar" - data-size={size} - className={cn( - "size-8 rounded-full after:rounded-full data-[size=lg]:size-10 data-[size=sm]:size-6 after:border-border group/avatar relative flex shrink-0 select-none after:absolute after:inset-0 after:border after:mix-blend-darken dark:after:mix-blend-lighten", - className, - )} - {...props} - /> - ); -} - -function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) { - return ( - <AvatarPrimitive.Image - data-slot="avatar-image" - className={cn( - "rounded-full aspect-square size-full object-cover", - className, - )} - {...props} - /> - ); -} - -function AvatarFallback({ - className, - ...props -}: AvatarPrimitive.Fallback.Props) { - return ( - <AvatarPrimitive.Fallback - data-slot="avatar-fallback" - className={cn( - "bg-muted text-muted-foreground rounded-full flex size-full items-center justify-center text-sm group-data-[size=sm]/avatar:text-xs", - className, - )} - {...props} - /> - ); -} - -function AvatarBadge({ className, ...props }: React.ComponentProps<"span">) { - return ( - <span - data-slot="avatar-badge" - className={cn( - "bg-primary text-primary-foreground ring-background absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-blend-color ring-2 select-none", - "group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden", - "group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2", - "group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2", - className, - )} - {...props} - /> - ); -} - -function AvatarGroup({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="avatar-group" - className={cn( - "*:data-[slot=avatar]:ring-background group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2", - className, - )} - {...props} - /> - ); -} - -function AvatarGroupCount({ - className, - ...props -}: React.ComponentProps<"div">) { - return ( - <div - data-slot="avatar-group-count" - className={cn( - "bg-muted text-muted-foreground size-8 rounded-full text-xs group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3 ring-background relative flex shrink-0 items-center justify-center ring-2", - className, - )} - {...props} - /> - ); -} - -export { - Avatar, - AvatarImage, - AvatarFallback, - AvatarGroup, - AvatarGroupCount, - AvatarBadge, -}; diff --git a/packages/ui-new/src/components/ui/badge.tsx b/packages/ui-new/src/components/ui/badge.tsx deleted file mode 100644 index 425ab9e..0000000 --- a/packages/ui-new/src/components/ui/badge.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { mergeProps } from "@base-ui/react/merge-props"; -import { useRender } from "@base-ui/react/use-render"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const badgeVariants = cva( - "h-5 gap-1 rounded-none border border-transparent px-2 py-0.5 text-xs font-medium transition-all has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&>svg]:size-3! inline-flex items-center justify-center w-fit whitespace-nowrap shrink-0 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive overflow-hidden group/badge", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", - secondary: - "bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80", - destructive: - "bg-destructive/10 [a]:hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 text-destructive dark:bg-destructive/20", - outline: - "border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground", - ghost: - "hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50", - link: "text-primary underline-offset-4 hover:underline", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -function Badge({ - className, - variant = "default", - render, - ...props -}: useRender.ComponentProps<"span"> & VariantProps<typeof badgeVariants>) { - return useRender({ - defaultTagName: "span", - props: mergeProps<"span">( - { - className: cn(badgeVariants({ variant }), className), - }, - props, - ), - render, - state: { - slot: "badge", - variant, - }, - }); -} - -export { Badge, badgeVariants }; diff --git a/packages/ui-new/src/components/ui/button.tsx b/packages/ui-new/src/components/ui/button.tsx deleted file mode 100644 index 7dee494..0000000 --- a/packages/ui-new/src/components/ui/button.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { Button as ButtonPrimitive } from "@base-ui/react/button"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -const buttonVariants = cva( - "focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 rounded-none border border-transparent bg-clip-padding text-xs font-medium focus-visible:ring-1 aria-invalid:ring-1 [&_svg:not([class*='size-'])]:size-4 inline-flex items-center justify-center whitespace-nowrap transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none shrink-0 [&_svg]:shrink-0 outline-none group/button select-none", - { - variants: { - variant: { - default: "bg-primary text-primary-foreground [a]:hover:bg-primary/80", - outline: - "border-border bg-background hover:bg-muted hover:text-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50 aria-expanded:bg-muted aria-expanded:text-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", - ghost: - "hover:bg-muted hover:text-foreground dark:hover:bg-muted/50 aria-expanded:bg-muted aria-expanded:text-foreground", - destructive: - "bg-destructive/10 hover:bg-destructive/20 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/20 text-destructive focus-visible:border-destructive/40 dark:hover:bg-destructive/30", - link: "text-primary underline-offset-4 hover:underline", - }, - size: { - default: - "h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", - xs: "h-6 gap-1 rounded-none px-2 text-xs has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3", - sm: "h-7 gap-1 rounded-none px-2.5 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5", - lg: "h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", - icon: "size-8", - "icon-xs": "size-6 rounded-none [&_svg:not([class*='size-'])]:size-3", - "icon-sm": "size-7 rounded-none", - "icon-lg": "size-9", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -); - -function Button({ - className, - variant = "default", - size = "default", - ...props -}: ButtonPrimitive.Props & VariantProps<typeof buttonVariants>) { - return ( - <ButtonPrimitive - data-slot="button" - className={cn(buttonVariants({ variant, size, className }))} - {...props} - /> - ); -} - -export { Button, buttonVariants }; diff --git a/packages/ui-new/src/components/ui/card.tsx b/packages/ui-new/src/components/ui/card.tsx deleted file mode 100644 index b7084a0..0000000 --- a/packages/ui-new/src/components/ui/card.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Card({ - className, - size = "default", - ...props -}: React.ComponentProps<"div"> & { size?: "default" | "sm" }) { - return ( - <div - data-slot="card" - data-size={size} - className={cn( - "ring-foreground/10 bg-card text-card-foreground gap-4 overflow-hidden rounded-none py-4 text-xs/relaxed ring-1 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-2 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-none *:[img:last-child]:rounded-none group/card flex flex-col", - className, - )} - {...props} - /> - ); -} - -function CardHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-header" - className={cn( - "gap-1 rounded-none px-4 group-data-[size=sm]/card:px-3 [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3 group/card-header @container/card-header grid auto-rows-min items-start has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto]", - className, - )} - {...props} - /> - ); -} - -function CardTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-title" - className={cn( - "text-sm font-medium group-data-[size=sm]/card:text-sm", - className, - )} - {...props} - /> - ); -} - -function CardDescription({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-description" - className={cn("text-muted-foreground text-xs/relaxed", className)} - {...props} - /> - ); -} - -function CardAction({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-action" - className={cn( - "col-start-2 row-span-2 row-start-1 self-start justify-self-end", - className, - )} - {...props} - /> - ); -} - -function CardContent({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-content" - className={cn("px-4 group-data-[size=sm]/card:px-3", className)} - {...props} - /> - ); -} - -function CardFooter({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="card-footer" - className={cn( - "rounded-none border-t p-4 group-data-[size=sm]/card:p-3 flex items-center", - className, - )} - {...props} - /> - ); -} - -export { - Card, - CardHeader, - CardFooter, - CardTitle, - CardAction, - CardDescription, - CardContent, -}; diff --git a/packages/ui-new/src/components/ui/checkbox.tsx b/packages/ui-new/src/components/ui/checkbox.tsx deleted file mode 100644 index 9f22cea..0000000 --- a/packages/ui-new/src/components/ui/checkbox.tsx +++ /dev/null @@ -1,27 +0,0 @@ -"use client"; - -import { Checkbox as CheckboxPrimitive } from "@base-ui/react/checkbox"; -import { CheckIcon } from "lucide-react"; -import { cn } from "@/lib/utils"; - -function Checkbox({ className, ...props }: CheckboxPrimitive.Root.Props) { - return ( - <CheckboxPrimitive.Root - data-slot="checkbox" - className={cn( - "border-input dark:bg-input/30 data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary data-checked:border-primary aria-invalid:aria-checked:border-primary aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 flex size-4 items-center justify-center rounded-none border transition-colors group-has-disabled/field:opacity-50 focus-visible:ring-1 aria-invalid:ring-1 peer relative shrink-0 outline-none after:absolute after:-inset-x-3 after:-inset-y-2 disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - {...props} - > - <CheckboxPrimitive.Indicator - data-slot="checkbox-indicator" - className="[&>svg]:size-3.5 grid place-content-center text-current transition-none" - > - <CheckIcon /> - </CheckboxPrimitive.Indicator> - </CheckboxPrimitive.Root> - ); -} - -export { Checkbox }; diff --git a/packages/ui-new/src/components/ui/dialog.tsx b/packages/ui-new/src/components/ui/dialog.tsx deleted file mode 100644 index 033b47c..0000000 --- a/packages/ui-new/src/components/ui/dialog.tsx +++ /dev/null @@ -1,155 +0,0 @@ -"use client"; - -import { Dialog as DialogPrimitive } from "@base-ui/react/dialog"; -import { XIcon } from "lucide-react"; -import type * as React from "react"; -import { Button } from "@/components/ui/button"; -import { cn } from "@/lib/utils"; - -function Dialog({ ...props }: DialogPrimitive.Root.Props) { - return <DialogPrimitive.Root data-slot="dialog" {...props} />; -} - -function DialogTrigger({ ...props }: DialogPrimitive.Trigger.Props) { - return <DialogPrimitive.Trigger data-slot="dialog-trigger" {...props} />; -} - -function DialogPortal({ ...props }: DialogPrimitive.Portal.Props) { - return <DialogPrimitive.Portal data-slot="dialog-portal" {...props} />; -} - -function DialogClose({ ...props }: DialogPrimitive.Close.Props) { - return <DialogPrimitive.Close data-slot="dialog-close" {...props} />; -} - -function DialogOverlay({ - className, - ...props -}: DialogPrimitive.Backdrop.Props) { - return ( - <DialogPrimitive.Backdrop - data-slot="dialog-overlay" - className={cn( - "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50", - className, - )} - {...props} - /> - ); -} - -function DialogContent({ - className, - children, - showCloseButton = true, - ...props -}: DialogPrimitive.Popup.Props & { - showCloseButton?: boolean; -}) { - return ( - <DialogPortal> - <DialogOverlay /> - <DialogPrimitive.Popup - data-slot="dialog-content" - className={cn( - "bg-background data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-none p-4 text-xs/relaxed ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none", - className, - )} - {...props} - > - {children} - {showCloseButton && ( - <DialogPrimitive.Close - data-slot="dialog-close" - render={ - <Button - variant="ghost" - className="absolute top-2 right-2" - size="icon-sm" - /> - } - > - <XIcon /> - <span className="sr-only">Close</span> - </DialogPrimitive.Close> - )} - </DialogPrimitive.Popup> - </DialogPortal> - ); -} - -function DialogHeader({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="dialog-header" - className={cn("gap-1 text-left flex flex-col", className)} - {...props} - /> - ); -} - -function DialogFooter({ - className, - showCloseButton = false, - children, - ...props -}: React.ComponentProps<"div"> & { - showCloseButton?: boolean; -}) { - return ( - <div - data-slot="dialog-footer" - className={cn( - "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end", - className, - )} - {...props} - > - {children} - {showCloseButton && ( - <DialogPrimitive.Close render={<Button variant="outline" />}> - Close - </DialogPrimitive.Close> - )} - </div> - ); -} - -function DialogTitle({ className, ...props }: DialogPrimitive.Title.Props) { - return ( - <DialogPrimitive.Title - data-slot="dialog-title" - className={cn("text-sm font-medium", className)} - {...props} - /> - ); -} - -function DialogDescription({ - className, - ...props -}: DialogPrimitive.Description.Props) { - return ( - <DialogPrimitive.Description - data-slot="dialog-description" - className={cn( - "text-muted-foreground *:[a]:hover:text-foreground text-xs/relaxed *:[a]:underline *:[a]:underline-offset-3", - className, - )} - {...props} - /> - ); -} - -export { - Dialog, - DialogClose, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogOverlay, - DialogPortal, - DialogTitle, - DialogTrigger, -}; diff --git a/packages/ui-new/src/components/ui/dropdown-menu.tsx b/packages/ui-new/src/components/ui/dropdown-menu.tsx deleted file mode 100644 index ee97374..0000000 --- a/packages/ui-new/src/components/ui/dropdown-menu.tsx +++ /dev/null @@ -1,269 +0,0 @@ -import { Menu as MenuPrimitive } from "@base-ui/react/menu"; -import { CheckIcon, ChevronRightIcon } from "lucide-react"; -import type * as React from "react"; -import { cn } from "@/lib/utils"; - -function DropdownMenu({ ...props }: MenuPrimitive.Root.Props) { - return <MenuPrimitive.Root data-slot="dropdown-menu" {...props} />; -} - -function DropdownMenuPortal({ ...props }: MenuPrimitive.Portal.Props) { - return <MenuPrimitive.Portal data-slot="dropdown-menu-portal" {...props} />; -} - -function DropdownMenuTrigger({ ...props }: MenuPrimitive.Trigger.Props) { - return <MenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />; -} - -function DropdownMenuContent({ - align = "start", - alignOffset = 0, - side = "bottom", - sideOffset = 4, - className, - ...props -}: MenuPrimitive.Popup.Props & - Pick< - MenuPrimitive.Positioner.Props, - "align" | "alignOffset" | "side" | "sideOffset" - >) { - return ( - <MenuPrimitive.Portal> - <MenuPrimitive.Positioner - className="isolate z-50 outline-none" - align={align} - alignOffset={alignOffset} - side={side} - sideOffset={sideOffset} - > - <MenuPrimitive.Popup - data-slot="dropdown-menu-content" - className={cn( - "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-32 rounded-none shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto outline-none data-closed:overflow-hidden", - className, - )} - {...props} - /> - </MenuPrimitive.Positioner> - </MenuPrimitive.Portal> - ); -} - -function DropdownMenuGroup({ ...props }: MenuPrimitive.Group.Props) { - return <MenuPrimitive.Group data-slot="dropdown-menu-group" {...props} />; -} - -function DropdownMenuLabel({ - className, - inset, - ...props -}: MenuPrimitive.GroupLabel.Props & { - inset?: boolean; -}) { - return ( - <MenuPrimitive.GroupLabel - data-slot="dropdown-menu-label" - data-inset={inset} - className={cn( - "text-muted-foreground px-2 py-2 text-xs data-inset:pl-7", - className, - )} - {...props} - /> - ); -} - -function DropdownMenuItem({ - className, - inset, - variant = "default", - ...props -}: MenuPrimitive.Item.Props & { - inset?: boolean; - variant?: "default" | "destructive"; -}) { - return ( - <MenuPrimitive.Item - data-slot="dropdown-menu-item" - data-inset={inset} - data-variant={variant} - className={cn( - "focus:bg-accent focus:text-accent-foreground data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 dark:data-[variant=destructive]:focus:bg-destructive/20 data-[variant=destructive]:focus:text-destructive data-[variant=destructive]:*:[svg]:text-destructive not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-none px-2 py-2 text-xs data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 group/dropdown-menu-item relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - {...props} - /> - ); -} - -function DropdownMenuSub({ ...props }: MenuPrimitive.SubmenuRoot.Props) { - return <MenuPrimitive.SubmenuRoot data-slot="dropdown-menu-sub" {...props} />; -} - -function DropdownMenuSubTrigger({ - className, - inset, - children, - ...props -}: MenuPrimitive.SubmenuTrigger.Props & { - inset?: boolean; -}) { - return ( - <MenuPrimitive.SubmenuTrigger - data-slot="dropdown-menu-sub-trigger" - data-inset={inset} - className={cn( - "focus:bg-accent focus:text-accent-foreground data-open:bg-accent data-open:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-none px-2 py-2 text-xs data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 data-popup-open:bg-accent data-popup-open:text-accent-foreground flex cursor-default items-center outline-hidden select-none [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - {...props} - > - {children} - <ChevronRightIcon className="ml-auto" /> - </MenuPrimitive.SubmenuTrigger> - ); -} - -function DropdownMenuSubContent({ - align = "start", - alignOffset = -3, - side = "right", - sideOffset = 0, - className, - ...props -}: React.ComponentProps<typeof DropdownMenuContent>) { - return ( - <DropdownMenuContent - data-slot="dropdown-menu-sub-content" - className={cn( - "data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 bg-popover text-popover-foreground min-w-24 rounded-none shadow-lg ring-1 duration-100 w-auto", - className, - )} - align={align} - alignOffset={alignOffset} - side={side} - sideOffset={sideOffset} - {...props} - /> - ); -} - -function DropdownMenuCheckboxItem({ - className, - children, - checked, - inset, - ...props -}: MenuPrimitive.CheckboxItem.Props & { - inset?: boolean; -}) { - return ( - <MenuPrimitive.CheckboxItem - data-slot="dropdown-menu-checkbox-item" - data-inset={inset} - className={cn( - "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-none py-2 pr-8 pl-2 text-xs data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - checked={checked} - {...props} - > - <span - className="absolute right-2 flex items-center justify-center pointer-events-none" - data-slot="dropdown-menu-checkbox-item-indicator" - > - <MenuPrimitive.CheckboxItemIndicator> - <CheckIcon /> - </MenuPrimitive.CheckboxItemIndicator> - </span> - {children} - </MenuPrimitive.CheckboxItem> - ); -} - -function DropdownMenuRadioGroup({ ...props }: MenuPrimitive.RadioGroup.Props) { - return ( - <MenuPrimitive.RadioGroup - data-slot="dropdown-menu-radio-group" - {...props} - /> - ); -} - -function DropdownMenuRadioItem({ - className, - children, - inset, - ...props -}: MenuPrimitive.RadioItem.Props & { - inset?: boolean; -}) { - return ( - <MenuPrimitive.RadioItem - data-slot="dropdown-menu-radio-item" - data-inset={inset} - className={cn( - "focus:bg-accent focus:text-accent-foreground focus:**:text-accent-foreground gap-2 rounded-none py-2 pr-8 pl-2 text-xs data-inset:pl-7 [&_svg:not([class*='size-'])]:size-4 relative flex cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - {...props} - > - <span - className="absolute right-2 flex items-center justify-center pointer-events-none" - data-slot="dropdown-menu-radio-item-indicator" - > - <MenuPrimitive.RadioItemIndicator> - <CheckIcon /> - </MenuPrimitive.RadioItemIndicator> - </span> - {children} - </MenuPrimitive.RadioItem> - ); -} - -function DropdownMenuSeparator({ - className, - ...props -}: MenuPrimitive.Separator.Props) { - return ( - <MenuPrimitive.Separator - data-slot="dropdown-menu-separator" - className={cn("bg-border -mx-1 h-px", className)} - {...props} - /> - ); -} - -function DropdownMenuShortcut({ - className, - ...props -}: React.ComponentProps<"span">) { - return ( - <span - data-slot="dropdown-menu-shortcut" - className={cn( - "text-muted-foreground group-focus/dropdown-menu-item:text-accent-foreground ml-auto text-xs tracking-widest", - className, - )} - {...props} - /> - ); -} - -export { - DropdownMenu, - DropdownMenuPortal, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuLabel, - DropdownMenuItem, - DropdownMenuCheckboxItem, - DropdownMenuRadioGroup, - DropdownMenuRadioItem, - DropdownMenuSeparator, - DropdownMenuShortcut, - DropdownMenuSub, - DropdownMenuSubTrigger, - DropdownMenuSubContent, -}; diff --git a/packages/ui-new/src/components/ui/field.tsx b/packages/ui-new/src/components/ui/field.tsx deleted file mode 100644 index ab9fb71..0000000 --- a/packages/ui-new/src/components/ui/field.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import { cva, type VariantProps } from "class-variance-authority"; -import { useMemo } from "react"; -import { Label } from "@/components/ui/label"; -import { Separator } from "@/components/ui/separator"; -import { cn } from "@/lib/utils"; - -function FieldSet({ className, ...props }: React.ComponentProps<"fieldset">) { - return ( - <fieldset - data-slot="field-set" - className={cn( - "gap-4 has-[>[data-slot=checkbox-group]]:gap-3 has-[>[data-slot=radio-group]]:gap-3 flex flex-col", - className, - )} - {...props} - /> - ); -} - -function FieldLegend({ - className, - variant = "legend", - ...props -}: React.ComponentProps<"legend"> & { variant?: "legend" | "label" }) { - return ( - <legend - data-slot="field-legend" - data-variant={variant} - className={cn( - "mb-2.5 font-medium data-[variant=label]:text-xs data-[variant=legend]:text-sm", - className, - )} - {...props} - /> - ); -} - -function FieldGroup({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="field-group" - className={cn( - "gap-5 data-[slot=checkbox-group]:gap-3 *:data-[slot=field-group]:gap-4 group/field-group @container/field-group flex w-full flex-col", - className, - )} - {...props} - /> - ); -} - -const fieldVariants = cva( - "data-[invalid=true]:text-destructive gap-2 group/field flex w-full", - { - variants: { - orientation: { - vertical: "flex-col *:w-full [&>.sr-only]:w-auto", - horizontal: - "flex-row items-center *:data-[slot=field-label]:flex-auto has-[>[data-slot=field-content]]:items-start has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", - responsive: - "flex-col *:w-full [&>.sr-only]:w-auto @md/field-group:flex-row @md/field-group:items-center @md/field-group:*:w-auto @md/field-group:*:data-[slot=field-label]:flex-auto @md/field-group:has-[>[data-slot=field-content]]:items-start @md/field-group:has-[>[data-slot=field-content]]:[&>[role=checkbox],[role=radio]]:mt-px", - }, - }, - defaultVariants: { - orientation: "vertical", - }, - }, -); - -function Field({ - className, - orientation = "vertical", - ...props -}: React.ComponentProps<"div"> & VariantProps<typeof fieldVariants>) { - return ( - <div - data-slot="field" - data-orientation={orientation} - className={cn(fieldVariants({ orientation }), className)} - {...props} - /> - ); -} - -function FieldContent({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="field-content" - className={cn( - "gap-0.5 group/field-content flex flex-1 flex-col leading-snug", - className, - )} - {...props} - /> - ); -} - -function FieldLabel({ - className, - ...props -}: React.ComponentProps<typeof Label>) { - return ( - <Label - data-slot="field-label" - className={cn( - "has-data-checked:bg-primary/5 has-data-checked:border-primary/30 dark:has-data-checked:border-primary/20 dark:has-data-checked:bg-primary/10 gap-2 group-data-[disabled=true]/field:opacity-50 has-[>[data-slot=field]]:rounded-none has-[>[data-slot=field]]:border *:data-[slot=field]:p-2 group/field-label peer/field-label flex w-fit leading-snug", - "has-[>[data-slot=field]]:w-full has-[>[data-slot=field]]:flex-col", - className, - )} - {...props} - /> - ); -} - -function FieldTitle({ className, ...props }: React.ComponentProps<"div">) { - return ( - <div - data-slot="field-label" - className={cn( - "gap-2 text-xs/relaxed group-data-[disabled=true]/field:opacity-50 flex w-fit items-center leading-snug", - className, - )} - {...props} - /> - ); -} - -function FieldDescription({ className, ...props }: React.ComponentProps<"p">) { - return ( - <p - data-slot="field-description" - className={cn( - "text-muted-foreground text-left text-xs/relaxed [[data-variant=legend]+&]:-mt-1.5 leading-normal font-normal group-has-data-horizontal/field:text-balance", - "last:mt-0 nth-last-2:-mt-1", - "[&>a:hover]:text-primary [&>a]:underline [&>a]:underline-offset-4", - className, - )} - {...props} - /> - ); -} - -function FieldSeparator({ - children, - className, - ...props -}: React.ComponentProps<"div"> & { - children?: React.ReactNode; -}) { - return ( - <div - data-slot="field-separator" - data-content={!!children} - className={cn( - "-my-2 h-5 text-xs group-data-[variant=outline]/field-group:-mb-2 relative", - className, - )} - {...props} - > - <Separator className="absolute inset-0 top-1/2" /> - {children && ( - <span - className="text-muted-foreground px-2 bg-background relative mx-auto block w-fit" - data-slot="field-separator-content" - > - {children} - </span> - )} - </div> - ); -} - -function FieldError({ - className, - children, - errors, - ...props -}: React.ComponentProps<"div"> & { - errors?: Array<{ message?: string } | undefined>; -}) { - const content = useMemo(() => { - if (children) { - return children; - } - - if (!errors?.length) { - return null; - } - - const uniqueErrors = [ - ...new Map(errors.map((error) => [error?.message, error])).values(), - ]; - - if (uniqueErrors?.length === 1) { - return uniqueErrors[0]?.message; - } - - return ( - <ul className="ml-4 flex list-disc flex-col gap-1"> - {uniqueErrors.map( - (error, index) => - error?.message && ( - <li key={`${error.message.slice(6)}-${index}`}> - {error.message} - </li> - ), - )} - </ul> - ); - }, [children, errors]); - - if (!content) { - return null; - } - - return ( - <div - role="alert" - data-slot="field-error" - className={cn("text-destructive text-xs font-normal", className)} - {...props} - > - {content} - </div> - ); -} - -export { - Field, - FieldLabel, - FieldDescription, - FieldError, - FieldGroup, - FieldLegend, - FieldSeparator, - FieldSet, - FieldContent, - FieldTitle, -}; diff --git a/packages/ui-new/src/components/ui/input.tsx b/packages/ui-new/src/components/ui/input.tsx deleted file mode 100644 index bb0390a..0000000 --- a/packages/ui-new/src/components/ui/input.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import { Input as InputPrimitive } from "@base-ui/react/input"; -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Input({ className, type, ...props }: React.ComponentProps<"input">) { - return ( - <InputPrimitive - type={type} - data-slot="input" - className={cn( - "dark:bg-input/30 border-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 h-8 rounded-none border bg-transparent px-2.5 py-1 text-xs transition-colors file:h-6 file:text-xs file:font-medium focus-visible:ring-1 aria-invalid:ring-1 md:text-xs file:text-foreground placeholder:text-muted-foreground w-full min-w-0 outline-none file:inline-flex file:border-0 file:bg-transparent disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - {...props} - /> - ); -} - -export { Input }; diff --git a/packages/ui-new/src/components/ui/label.tsx b/packages/ui-new/src/components/ui/label.tsx deleted file mode 100644 index 9a998c7..0000000 --- a/packages/ui-new/src/components/ui/label.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Label({ className, ...props }: React.ComponentProps<"label">) { - return ( - // biome-ignore lint/a11y/noLabelWithoutControl: shadcn component - <label - data-slot="label" - className={cn( - "gap-2 text-xs leading-none group-data-[disabled=true]:opacity-50 peer-disabled:opacity-50 flex items-center select-none group-data-[disabled=true]:pointer-events-none peer-disabled:cursor-not-allowed", - className, - )} - {...props} - /> - ); -} - -export { Label }; diff --git a/packages/ui-new/src/components/ui/scroll-area.tsx b/packages/ui-new/src/components/ui/scroll-area.tsx deleted file mode 100644 index 4a68eb2..0000000 --- a/packages/ui-new/src/components/ui/scroll-area.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client"; - -import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area"; -import { cn } from "@/lib/utils"; - -function ScrollArea({ - className, - children, - ...props -}: ScrollAreaPrimitive.Root.Props) { - return ( - <ScrollAreaPrimitive.Root - data-slot="scroll-area" - className={cn("relative", className)} - {...props} - > - <ScrollAreaPrimitive.Viewport - data-slot="scroll-area-viewport" - className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1" - > - {children} - </ScrollAreaPrimitive.Viewport> - <ScrollBar /> - <ScrollAreaPrimitive.Corner /> - </ScrollAreaPrimitive.Root> - ); -} - -function ScrollBar({ - className, - orientation = "vertical", - ...props -}: ScrollAreaPrimitive.Scrollbar.Props) { - return ( - <ScrollAreaPrimitive.Scrollbar - data-slot="scroll-area-scrollbar" - data-orientation={orientation} - orientation={orientation} - className={cn( - "data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent flex touch-none p-px transition-colors select-none", - className, - )} - {...props} - > - <ScrollAreaPrimitive.Thumb - data-slot="scroll-area-thumb" - className="rounded-none bg-border relative flex-1" - /> - </ScrollAreaPrimitive.Scrollbar> - ); -} - -export { ScrollArea, ScrollBar }; diff --git a/packages/ui-new/src/components/ui/select.tsx b/packages/ui-new/src/components/ui/select.tsx deleted file mode 100644 index 210adba..0000000 --- a/packages/ui-new/src/components/ui/select.tsx +++ /dev/null @@ -1,199 +0,0 @@ -import { Select as SelectPrimitive } from "@base-ui/react/select"; -import { CheckIcon, ChevronDownIcon, ChevronUpIcon } from "lucide-react"; -import type * as React from "react"; -import { cn } from "@/lib/utils"; - -const Select = SelectPrimitive.Root; - -function SelectGroup({ className, ...props }: SelectPrimitive.Group.Props) { - return ( - <SelectPrimitive.Group - data-slot="select-group" - className={cn("scroll-my-1", className)} - {...props} - /> - ); -} - -function SelectValue({ className, ...props }: SelectPrimitive.Value.Props) { - return ( - <SelectPrimitive.Value - data-slot="select-value" - className={cn("flex flex-1 text-left", className)} - {...props} - /> - ); -} - -function SelectTrigger({ - className, - size = "default", - children, - ...props -}: SelectPrimitive.Trigger.Props & { - size?: "sm" | "default"; -}) { - return ( - <SelectPrimitive.Trigger - data-slot="select-trigger" - data-size={size} - className={cn( - "border-input data-placeholder:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 gap-1.5 rounded-none border bg-transparent py-2 pr-2 pl-2.5 text-xs transition-colors select-none focus-visible:ring-1 aria-invalid:ring-1 data-[size=default]:h-8 data-[size=sm]:h-7 data-[size=sm]:rounded-none *:data-[slot=select-value]:gap-1.5 [&_svg:not([class*='size-'])]:size-4 flex w-fit items-center justify-between whitespace-nowrap outline-none disabled:cursor-not-allowed disabled:opacity-50 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - {...props} - > - {children} - <SelectPrimitive.Icon - render={ - <ChevronDownIcon className="text-muted-foreground size-4 pointer-events-none" /> - } - /> - </SelectPrimitive.Trigger> - ); -} - -function SelectContent({ - className, - children, - side = "bottom", - sideOffset = 4, - align = "center", - alignOffset = 0, - alignItemWithTrigger = true, - ...props -}: SelectPrimitive.Popup.Props & - Pick< - SelectPrimitive.Positioner.Props, - "align" | "alignOffset" | "side" | "sideOffset" | "alignItemWithTrigger" - >) { - return ( - <SelectPrimitive.Portal> - <SelectPrimitive.Positioner - side={side} - sideOffset={sideOffset} - align={align} - alignOffset={alignOffset} - alignItemWithTrigger={alignItemWithTrigger} - className="isolate z-50" - > - <SelectPrimitive.Popup - data-slot="select-content" - data-align-trigger={alignItemWithTrigger} - className={cn( - "bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 ring-foreground/10 min-w-36 rounded-none shadow-md ring-1 duration-100 data-[side=inline-start]:slide-in-from-right-2 data-[side=inline-end]:slide-in-from-left-2 relative isolate z-50 max-h-(--available-height) w-(--anchor-width) origin-(--transform-origin) overflow-x-hidden overflow-y-auto data-[align-trigger=true]:animate-none", - className, - )} - {...props} - > - <SelectScrollUpButton /> - <SelectPrimitive.List>{children}</SelectPrimitive.List> - <SelectScrollDownButton /> - </SelectPrimitive.Popup> - </SelectPrimitive.Positioner> - </SelectPrimitive.Portal> - ); -} - -function SelectLabel({ - className, - ...props -}: SelectPrimitive.GroupLabel.Props) { - return ( - <SelectPrimitive.GroupLabel - data-slot="select-label" - className={cn("text-muted-foreground px-2 py-2 text-xs", className)} - {...props} - /> - ); -} - -function SelectItem({ - className, - children, - ...props -}: SelectPrimitive.Item.Props) { - return ( - <SelectPrimitive.Item - data-slot="select-item" - className={cn( - "focus:bg-accent focus:text-accent-foreground not-data-[variant=destructive]:focus:**:text-accent-foreground gap-2 rounded-none py-2 pr-8 pl-2 text-xs [&_svg:not([class*='size-'])]:size-4 *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2 relative flex w-full cursor-default items-center outline-hidden select-none data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", - className, - )} - {...props} - > - <SelectPrimitive.ItemText className="flex flex-1 gap-2 shrink-0 whitespace-nowrap"> - {children} - </SelectPrimitive.ItemText> - <SelectPrimitive.ItemIndicator - render={ - <span className="pointer-events-none absolute right-2 flex size-4 items-center justify-center" /> - } - > - <CheckIcon className="pointer-events-none" /> - </SelectPrimitive.ItemIndicator> - </SelectPrimitive.Item> - ); -} - -function SelectSeparator({ - className, - ...props -}: SelectPrimitive.Separator.Props) { - return ( - <SelectPrimitive.Separator - data-slot="select-separator" - className={cn("bg-border -mx-1 h-px pointer-events-none", className)} - {...props} - /> - ); -} - -function SelectScrollUpButton({ - className, - ...props -}: React.ComponentProps<typeof SelectPrimitive.ScrollUpArrow>) { - return ( - <SelectPrimitive.ScrollUpArrow - data-slot="select-scroll-up-button" - className={cn( - "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 top-0 w-full", - className, - )} - {...props} - > - <ChevronUpIcon /> - </SelectPrimitive.ScrollUpArrow> - ); -} - -function SelectScrollDownButton({ - className, - ...props -}: React.ComponentProps<typeof SelectPrimitive.ScrollDownArrow>) { - return ( - <SelectPrimitive.ScrollDownArrow - data-slot="select-scroll-down-button" - className={cn( - "bg-popover z-10 flex cursor-default items-center justify-center py-1 [&_svg:not([class*='size-'])]:size-4 bottom-0 w-full", - className, - )} - {...props} - > - <ChevronDownIcon /> - </SelectPrimitive.ScrollDownArrow> - ); -} - -export { - Select, - SelectContent, - SelectGroup, - SelectItem, - SelectLabel, - SelectScrollDownButton, - SelectScrollUpButton, - SelectSeparator, - SelectTrigger, - SelectValue, -}; diff --git a/packages/ui-new/src/components/ui/separator.tsx b/packages/ui-new/src/components/ui/separator.tsx deleted file mode 100644 index e91a862..0000000 --- a/packages/ui-new/src/components/ui/separator.tsx +++ /dev/null @@ -1,25 +0,0 @@ -"use client"; - -import { Separator as SeparatorPrimitive } from "@base-ui/react/separator"; - -import { cn } from "@/lib/utils"; - -function Separator({ - className, - orientation = "horizontal", - ...props -}: SeparatorPrimitive.Props) { - return ( - <SeparatorPrimitive - data-slot="separator" - orientation={orientation} - className={cn( - "bg-border shrink-0 data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch", - className, - )} - {...props} - /> - ); -} - -export { Separator }; diff --git a/packages/ui-new/src/components/ui/sonner.tsx b/packages/ui-new/src/components/ui/sonner.tsx deleted file mode 100644 index d6e293d..0000000 --- a/packages/ui-new/src/components/ui/sonner.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { - CircleCheckIcon, - InfoIcon, - Loader2Icon, - OctagonXIcon, - TriangleAlertIcon, -} from "lucide-react"; -import { useTheme } from "next-themes"; -import { Toaster as Sonner, type ToasterProps } from "sonner"; - -const Toaster = ({ ...props }: ToasterProps) => { - const { theme = "system" } = useTheme(); - - return ( - <Sonner - theme={theme as ToasterProps["theme"]} - className="toaster group" - icons={{ - success: <CircleCheckIcon className="size-4" />, - info: <InfoIcon className="size-4" />, - warning: <TriangleAlertIcon className="size-4" />, - error: <OctagonXIcon className="size-4" />, - loading: <Loader2Icon className="size-4 animate-spin" />, - }} - style={ - { - "--normal-bg": "var(--popover)", - "--normal-text": "var(--popover-foreground)", - "--normal-border": "var(--border)", - "--border-radius": "var(--radius)", - } as React.CSSProperties - } - toastOptions={{ - classNames: { - toast: "cn-toast", - }, - }} - {...props} - /> - ); -}; - -export { Toaster }; diff --git a/packages/ui-new/src/components/ui/spinner.tsx b/packages/ui-new/src/components/ui/spinner.tsx deleted file mode 100644 index 91f6a63..0000000 --- a/packages/ui-new/src/components/ui/spinner.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { cn } from "@/lib/utils" -import { Loader2Icon } from "lucide-react" - -function Spinner({ className, ...props }: React.ComponentProps<"svg">) { - return ( - <Loader2Icon role="status" aria-label="Loading" className={cn("size-4 animate-spin", className)} {...props} /> - ) -} - -export { Spinner } diff --git a/packages/ui-new/src/components/ui/switch.tsx b/packages/ui-new/src/components/ui/switch.tsx deleted file mode 100644 index fef14e3..0000000 --- a/packages/ui-new/src/components/ui/switch.tsx +++ /dev/null @@ -1,32 +0,0 @@ -"use client"; - -import { Switch as SwitchPrimitive } from "@base-ui/react/switch"; - -import { cn } from "@/lib/utils"; - -function Switch({ - className, - size = "default", - ...props -}: SwitchPrimitive.Root.Props & { - size?: "sm" | "default"; -}) { - return ( - <SwitchPrimitive.Root - data-slot="switch" - data-size={size} - className={cn( - "data-checked:bg-primary data-unchecked:bg-input focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 dark:data-unchecked:bg-input/80 shrink-0 rounded-full border border-transparent focus-visible:ring-1 aria-invalid:ring-1 data-[size=default]:h-[18.4px] data-[size=default]:w-[32px] data-[size=sm]:h-[14px] data-[size=sm]:w-[24px] peer group/switch relative inline-flex items-center transition-all outline-none after:absolute after:-inset-x-3 after:-inset-y-2 data-disabled:cursor-not-allowed data-disabled:opacity-50", - className, - )} - {...props} - > - <SwitchPrimitive.Thumb - data-slot="switch-thumb" - className="bg-background dark:data-unchecked:bg-foreground dark:data-checked:bg-primary-foreground rounded-full group-data-[size=default]/switch:size-4 group-data-[size=sm]/switch:size-3 group-data-[size=default]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=sm]/switch:data-checked:translate-x-[calc(100%-2px)] group-data-[size=default]/switch:data-unchecked:translate-x-0 group-data-[size=sm]/switch:data-unchecked:translate-x-0 pointer-events-none block ring-0 transition-transform" - /> - </SwitchPrimitive.Root> - ); -} - -export { Switch }; diff --git a/packages/ui-new/src/components/ui/tabs.tsx b/packages/ui-new/src/components/ui/tabs.tsx deleted file mode 100644 index c66893f..0000000 --- a/packages/ui-new/src/components/ui/tabs.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"; -import { cva, type VariantProps } from "class-variance-authority"; - -import { cn } from "@/lib/utils"; - -function Tabs({ - className, - orientation = "horizontal", - ...props -}: TabsPrimitive.Root.Props) { - return ( - <TabsPrimitive.Root - data-slot="tabs" - data-orientation={orientation} - className={cn( - "gap-2 group/tabs flex data-horizontal:flex-col", - className, - )} - {...props} - /> - ); -} - -const tabsListVariants = cva( - "rounded-none p-0.75 group-data-horizontal/tabs:h-8 data-[variant=line]:rounded-none group/tabs-list text-muted-foreground inline-flex w-fit items-center justify-center group-data-vertical/tabs:h-fit group-data-vertical/tabs:flex-col", - { - variants: { - variant: { - default: "bg-muted", - line: "gap-1 bg-transparent", - }, - }, - defaultVariants: { - variant: "default", - }, - }, -); - -function TabsList({ - className, - variant = "default", - ...props -}: TabsPrimitive.List.Props & VariantProps<typeof tabsListVariants>) { - return ( - <TabsPrimitive.List - data-slot="tabs-list" - data-variant={variant} - className={cn(tabsListVariants({ variant }), className)} - {...props} - /> - ); -} - -function TabsTrigger({ className, ...props }: TabsPrimitive.Tab.Props) { - return ( - <TabsPrimitive.Tab - data-slot="tabs-trigger" - className={cn( - "gap-1.5 rounded-none border border-transparent px-1.5 py-0.5 text-xs font-medium group-data-vertical/tabs:py-[calc(--spacing(1.25))] [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-vertical/tabs:w-full group-data-vertical/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 aria-disabled:pointer-events-none aria-disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0", - "group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent", - "data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground", - "after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-horizontal/tabs:after:inset-x-0 group-data-horizontal/tabs:after:-bottom-1.25 group-data-horizontal/tabs:after:h-0.5 group-data-vertical/tabs:after:inset-y-0 group-data-vertical/tabs:after:-right-1 group-data-vertical/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100", - className, - )} - {...props} - /> - ); -} - -function TabsContent({ className, ...props }: TabsPrimitive.Panel.Props) { - return ( - <TabsPrimitive.Panel - data-slot="tabs-content" - className={cn("text-xs/relaxed flex-1 outline-none", className)} - {...props} - /> - ); -} - -export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants }; diff --git a/packages/ui-new/src/components/ui/textarea.tsx b/packages/ui-new/src/components/ui/textarea.tsx deleted file mode 100644 index 3c3e5d0..0000000 --- a/packages/ui-new/src/components/ui/textarea.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import type * as React from "react"; - -import { cn } from "@/lib/utils"; - -function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { - return ( - <textarea - data-slot="textarea" - className={cn( - "border-input dark:bg-input/30 focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:aria-invalid:border-destructive/50 disabled:bg-input/50 dark:disabled:bg-input/80 rounded-none border bg-transparent px-2.5 py-2 text-xs transition-colors focus-visible:ring-1 aria-invalid:ring-1 md:text-xs placeholder:text-muted-foreground flex field-sizing-content min-h-16 w-full outline-none disabled:cursor-not-allowed disabled:opacity-50", - className, - )} - {...props} - /> - ); -} - -export { Textarea }; diff --git a/packages/ui-new/src/components/user-avatar.tsx b/packages/ui-new/src/components/user-avatar.tsx deleted file mode 100644 index bbdb84c..0000000 --- a/packages/ui-new/src/components/user-avatar.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import { useAuthStore } from "@/models/auth"; -import { Avatar, AvatarBadge, AvatarFallback, AvatarImage } from "./ui/avatar"; - -export function UserAvatar({ - className, - ...props -}: React.ComponentProps<typeof Avatar>) { - const authStore = useAuthStore(); - - if (!authStore.account) { - return null; - } - - return ( - <Avatar {...props}> - <AvatarImage - src={`https://minotar.net/helm/${authStore.account.username}/100.png`} - /> - <AvatarFallback>{authStore.account.username.slice(0, 2)}</AvatarFallback> - <AvatarBadge /> - </Avatar> - ); -} |