diff --git a/apps/web/src/components/config-panel.tsx b/apps/web/src/components/config-panel.tsx index bd8207e..6ee0b7e 100644 --- a/apps/web/src/components/config-panel.tsx +++ b/apps/web/src/components/config-panel.tsx @@ -5,7 +5,6 @@ import { CardHeader, CardTitle, } from "@tiny-svg/ui/components/card"; -import { Input } from "@tiny-svg/ui/components/input"; import { Label } from "@tiny-svg/ui/components/label"; import { Switch } from "@tiny-svg/ui/components/switch"; import { useCallback, useEffect } from "react"; @@ -20,6 +19,9 @@ import { import { useSvgStore } from "@/store/svg-store"; import { type ExportScale, useUiStore } from "@/store/ui-store"; import { ExportPanel } from "./export-panel"; +import { DeletePresetDialog } from "./presets/delete-preset-dialog"; +import { PresetEditorDialog } from "./presets/preset-editor-dialog"; +import { PresetList } from "./presets/preset-list"; type ConfigPanelProps = { isCollapsed: boolean; @@ -33,11 +35,8 @@ export function ConfigPanel({ className, }: ConfigPanelProps) { const { - plugins, globalSettings, - togglePlugin, updateGlobalSettings, - resetPlugins, compressedSvg, fileName, originalSvg, @@ -52,23 +51,15 @@ export function ConfigPanel({ setExportDimensions, } = useUiStore(); const { settings } = useIntlayer("optimize"); - const pluginLabels = useIntlayer("plugins"); // 提供默认值,防止服务器端渲染错误 const safeSettings = settings || { title: "Settings", global: { - title: "Global Settings", + title: "Display Settings", showOriginal: "Show original", compareGzipped: "Compare gzipped", prettifyMarkup: "Prettify markup", - multipass: "Multipass", - numberPrecision: "Number precision", - transformPrecision: "Transform precision", - }, - features: { - title: "Features", - resetAll: "Reset all", }, export: { title: "Export", @@ -213,7 +204,14 @@ export function ConfigPanel({
- {/* Global Settings */} + {/* Presets */} + + + + + + + {/* Display Settings */} @@ -264,91 +262,6 @@ export function ConfigPanel({ } />
-
- - - updateGlobalSettings({ multipass: checked }) - } - /> -
-
- - - updateGlobalSettings({ - floatPrecision: Number.parseInt(e.target.value, 10), - }) - } - type="number" - value={globalSettings.floatPrecision} - /> -
-
- - - updateGlobalSettings({ - transformPrecision: Number.parseInt(e.target.value, 10), - }) - } - type="number" - value={globalSettings.transformPrecision} - /> -
- - - - {/* Features */} - - -
- - {safeSettings.features.title} - - -
-
- - {plugins.map((plugin) => ( -
- - togglePlugin(plugin.name)} - /> -
- ))}
@@ -364,6 +277,10 @@ export function ConfigPanel({ onWidthChange={handleWidthChange} /> + + {/* Preset Dialogs */} + + ); } diff --git a/apps/web/src/components/presets/delete-preset-dialog.tsx b/apps/web/src/components/presets/delete-preset-dialog.tsx new file mode 100644 index 0000000..dd141a1 --- /dev/null +++ b/apps/web/src/components/presets/delete-preset-dialog.tsx @@ -0,0 +1,69 @@ +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "@tiny-svg/ui/components/alert-dialog"; +import { useIntlayer } from "react-intlayer"; +import { usePresetsStore } from "@/store/presets-store"; + +export function DeletePresetDialog() { + const { presets: t } = useIntlayer("presets"); + const { + deleteDialogPresetId, + closeDeleteDialog, + deletePreset, + getPresetById, + } = usePresetsStore(); + + const preset = deleteDialogPresetId + ? getPresetById(deleteDialogPresetId) + : null; + + const handleOpenChange = (open: boolean) => { + if (!open) { + closeDeleteDialog(); + } + }; + + const handleConfirm = () => { + if (deleteDialogPresetId) { + deletePreset(deleteDialogPresetId); + } + }; + + return ( + + + + {t.deleteDialog.title} + + {String(t.deleteDialog.description).replace( + "{name}", + preset?.name || "" + )} + + + + + {t.deleteDialog.cancel} + + + {t.deleteDialog.confirm} + + + + + ); +} diff --git a/apps/web/src/components/presets/preset-card.tsx b/apps/web/src/components/presets/preset-card.tsx new file mode 100644 index 0000000..bdfdd49 --- /dev/null +++ b/apps/web/src/components/presets/preset-card.tsx @@ -0,0 +1,157 @@ +import { Badge } from "@tiny-svg/ui/components/badge"; +import { Button } from "@tiny-svg/ui/components/button"; +import { + Card, + CardContent, + CardDescription, + CardHeader, + CardTitle, +} from "@tiny-svg/ui/components/card"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@tiny-svg/ui/components/tooltip"; +import { cn } from "@tiny-svg/ui/lib/utils"; +import { useIntlayer } from "react-intlayer"; +import type { Preset } from "@/types/preset"; + +type PresetCardProps = { + preset: Preset; + isActive: boolean; + onApply: () => void; + onEdit: () => void; + onDuplicate: () => void; + onDelete: () => void; + onPin: () => void; +}; + +export function PresetCard({ + preset, + isActive, + onApply, + onEdit, + onDuplicate, + onDelete, + onPin, +}: PresetCardProps) { + const { presets: t } = useIntlayer("presets"); + + return ( + + +
+
+ {preset.icon && {preset.icon}} + {preset.name} + {preset.isDefault && ( + + {t.systemBadge} + + )} + {preset.pinned && ( + + )} +
+
+ {preset.description && ( + + {preset.description} + + )} +
+ +
+ + +
+ + + + + + + {preset.pinned ? t.unpin : t.pin} + + + + + + + + {t.edit} + + + + + + + {t.duplicate} + + + {!preset.isDefault && ( + + + + + {t.delete} + + )} + +
+
+
+
+ ); +} diff --git a/apps/web/src/components/presets/preset-editor-dialog.tsx b/apps/web/src/components/presets/preset-editor-dialog.tsx new file mode 100644 index 0000000..6999d1f --- /dev/null +++ b/apps/web/src/components/presets/preset-editor-dialog.tsx @@ -0,0 +1,292 @@ +import { Button } from "@tiny-svg/ui/components/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@tiny-svg/ui/components/dialog"; +import { Input } from "@tiny-svg/ui/components/input"; +import { Label } from "@tiny-svg/ui/components/label"; +import { ScrollArea } from "@tiny-svg/ui/components/scroll-area"; +import { Separator } from "@tiny-svg/ui/components/separator"; +import { Switch } from "@tiny-svg/ui/components/switch"; +import { Textarea } from "@tiny-svg/ui/components/textarea"; +import { useEffect, useState } from "react"; +import { useIntlayer } from "react-intlayer"; +import { + defaultPresetGlobalSettings, + validatePreset, +} from "@/lib/preset-utils"; +import { allSvgoPlugins, type SvgoPluginConfig } from "@/lib/svgo-plugins"; +import { usePresetsStore } from "@/store/presets-store"; +import type { PresetGlobalSettings } from "@/types/preset"; + +export function PresetEditorDialog() { + const { presets: t } = useIntlayer("presets"); + const pluginLabels = useIntlayer("plugins"); + + const { + isEditorOpen, + editorMode, + editingPresetId, + sourcePresetId, + presets, + closeEditor, + addPreset, + updatePreset, + getPresetById, + } = usePresetsStore(); + + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [plugins, setPlugins] = useState(allSvgoPlugins); + const [globalSettings, setGlobalSettings] = useState( + defaultPresetGlobalSettings + ); + const [errors, setErrors] = useState([]); + + // Load preset data when dialog opens + useEffect(() => { + if (!isEditorOpen) { + return; + } + + if (editorMode === "edit" && editingPresetId) { + const preset = getPresetById(editingPresetId); + if (preset) { + setName(preset.name); + setDescription(preset.description); + setPlugins(preset.plugins); + setGlobalSettings(preset.globalSettings); + } + } else if (sourcePresetId) { + // Duplicating + const source = getPresetById(sourcePresetId); + if (source) { + setName(`${source.name} (Copy)`); + setDescription(source.description); + setPlugins(source.plugins); + setGlobalSettings(source.globalSettings); + } + } else { + // New preset with defaults + setName(""); + setDescription(""); + setPlugins(allSvgoPlugins); + setGlobalSettings(defaultPresetGlobalSettings); + } + setErrors([]); + }, [ + isEditorOpen, + editorMode, + editingPresetId, + sourcePresetId, + getPresetById, + ]); + + const handlePluginToggle = (pluginName: string) => { + setPlugins((prev) => + prev.map((p) => + p.name === pluginName ? { ...p, enabled: !p.enabled } : p + ) + ); + }; + + const handleResetPlugins = () => { + setPlugins(allSvgoPlugins); + }; + + const handleSave = () => { + const validation = validatePreset( + { id: editingPresetId || undefined, name }, + presets + ); + + if (!validation.isValid) { + setErrors(validation.errors); + return; + } + + if (editorMode === "edit" && editingPresetId) { + updatePreset(editingPresetId, { + name, + description, + plugins, + globalSettings, + }); + } else { + addPreset({ + name, + description, + plugins, + globalSettings, + }); + } + + closeEditor(); + }; + + const handleOpenChange = (open: boolean) => { + if (!open) { + closeEditor(); + } + }; + + return ( + + + + + {editorMode === "edit" ? t.editor.titleEdit : t.editor.titleCreate} + + {t.editor.description} + + + +
+ {/* Basic Info */} +
+
+ + setName(e.target.value)} + placeholder={String(t.editor.namePlaceholder)} + value={name} + /> +
+
+ +