import { useActionQuery, useActionMutation, } from "@agent-native/core/client/hooks"; import { useT } from "@agent-native/core/client/i18n"; import { ShareButton } from "@agent-native/core/client/sharing"; import { useSetHeaderActions, useSetPageTitle, } from "@agent-native/toolkit/app-shell"; import { VisibilityBadge } from "@agent-native/toolkit/sharing"; import { IconCheckbox, IconChecks, IconDots, IconPlus, IconPalette, IconStar, IconStarFilled, IconTrash, IconX, } from "@tabler/icons-react"; import { useQueryClient } from "@tanstack/react-query"; import { useCallback, useEffect, useMemo, useState, type ReactNode, } from "react"; import { Link, useNavigate, useSearchParams } from "react-router"; import { toast } from "sonner"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { Button } from "@/components/ui/button"; import { Checkbox } from "@/components/ui/checkbox"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, } from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from "@/components/ui/sheet"; import { Textarea } from "@/components/ui/textarea"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import { formatDesignTokenValue, getCssColorToken, } from "@/lib/design-system-preview"; import type { DesignSystemTemplateId } from "../../shared/design-system-templates"; import { ProductionDesignSystemShowcase } from "../components/design-system/ProductionDesignSystemShowcase"; import { QueryErrorState } from "../components/QueryErrorState"; interface DesignSystem { id: string; title: string; description?: string | null; data: string; assets?: string | null; customInstructions?: string | null; isDefault: boolean; visibility?: "private" | "org" | "public" | null; accessRole?: "owner" | "viewer" | "editor" | "admin"; canManage?: boolean; createdAt: string; updatedAt?: string; } interface DesignSystemData { colors?: { primary?: unknown; secondary?: unknown; accent?: unknown; background?: unknown; surface?: unknown; text?: unknown; textMuted?: unknown; }; typography?: { headingFont?: unknown; bodyFont?: unknown; headingWeight?: unknown; bodyWeight?: unknown; }; spacing?: Record; borders?: Record; logos?: Array<{ url?: string; name?: string; variant?: string }>; defaults?: Record; notes?: unknown; } export default function DesignSystems() { const t = useT(); const navigate = useNavigate(); const [searchParams] = useSearchParams(); const queryClient = useQueryClient(); const [deleteId, setDeleteId] = useState(null); const [bulkDeleteOpen, setBulkDeleteOpen] = useState(false); const [isSelectionMode, setIsSelectionMode] = useState(false); const [selectedSystemIds, setSelectedSystemIds] = useState>( () => new Set(), ); const [pendingTemplateId, setPendingTemplateId] = useState(null); const { data, isLoading, isError, isFetching, refetch } = useActionQuery<{ designSystems: DesignSystem[]; }>("list-design-systems"); const setDefaultMutation = useActionMutation("set-default-design-system"); const deleteMutation = useActionMutation("delete-design-system"); const updateMutation = useActionMutation("update-design-system"); const createMutation = useActionMutation("create-design-system"); const designSystems = data?.designSystems ?? []; const selectedDesignSystemId = searchParams.get("designSystemId"); const selectedDesignSystem = useMemo( () => selectedDesignSystemId ? (designSystems.find((ds) => ds.id === selectedDesignSystemId) ?? null) : null, [designSystems, selectedDesignSystemId], ); const manageableDesignSystems = designSystems.filter((ds) => ds.canManage); const selectedSystemCount = selectedSystemIds.size; const allSystemsSelected = manageableDesignSystems.length > 0 && manageableDesignSystems.every((ds) => selectedSystemIds.has(ds.id)); const openDesignSystemDetails = useCallback( (id: string) => { navigate(`/design-systems?designSystemId=${encodeURIComponent(id)}`); }, [navigate], ); const closeDesignSystemDetails = useCallback(() => { navigate("/design-systems", { replace: true }); }, [navigate]); const openSetupFromDesignSystem = useCallback( (id: string) => { navigate(`/design-systems/setup?source=${encodeURIComponent(id)}`); }, [navigate], ); const toggleSelectionMode = useCallback(() => { if (isSelectionMode) { setSelectedSystemIds(new Set()); } setIsSelectionMode((current) => !current); }, [isSelectionMode]); const exitSelectionMode = useCallback(() => { setIsSelectionMode(false); setSelectedSystemIds(new Set()); }, []); const toggleSystemSelection = useCallback( (id: string) => { if (!designSystems.find((ds) => ds.id === id)?.canManage) return; setSelectedSystemIds((current) => { const next = new Set(current); if (next.has(id)) { next.delete(id); } else { next.add(id); } return next; }); }, [designSystems], ); const toggleAllSystems = useCallback(() => { setSelectedSystemIds((current) => { const next = new Set(current); const shouldClear = manageableDesignSystems.length > 0 && manageableDesignSystems.every((ds) => next.has(ds.id)); manageableDesignSystems.forEach((ds) => { if (shouldClear) { next.delete(ds.id); } else { next.add(ds.id); } }); return next; }); }, [manageableDesignSystems]); const clearSelection = useCallback(() => { setSelectedSystemIds(new Set()); }, []); const handleSetDefault = useCallback( (id: string) => { // Optimistic update queryClient.setQueryData( ["action", "list-design-systems", undefined], (old: any) => { if (!old?.designSystems) return old; return { ...old, designSystems: old.designSystems.map((ds: DesignSystem) => ({ ...ds, isDefault: ds.id === id, })), }; }, ); setDefaultMutation.mutate({ id } as any, { onError: () => { queryClient.invalidateQueries({ queryKey: ["action", "list-design-systems"], }); }, }); }, [queryClient, setDefaultMutation], ); const handleAddProductionTemplate = useCallback( (templateId: DesignSystemTemplateId) => { setPendingTemplateId(templateId); createMutation.mutate({ templateId } as any, { onSuccess: (result: any) => { setPendingTemplateId(null); toast.success(t("designSystems.showcase.addSuccess")); const id = result && typeof result.id === "string" ? result.id : undefined; navigate( id ? `/design-systems?designSystemId=${encodeURIComponent(id)}` : "/design-systems", ); }, onError: (error) => { setPendingTemplateId(null); toast.error(t("designSystems.showcase.addError"), { description: error instanceof Error ? error.message : t("common.genericError"), }); }, }); }, [createMutation, navigate, t], ); const handleDelete = useCallback(() => { if (!deleteId) return; const id = deleteId; queryClient.setQueryData( ["action", "list-design-systems", undefined], (old: any) => { const systems = old?.designSystems ?? []; return { count: Math.max((old?.count ?? systems.length) - 1, 0), designSystems: systems.filter((ds: DesignSystem) => ds.id !== id), }; }, ); setDeleteId(null); deleteMutation.mutate({ id } as any, { onError: (error) => { queryClient.invalidateQueries({ queryKey: ["action", "list-design-systems"], }); toast.error(t("designSystems.deleteError"), { description: error instanceof Error ? error.message : t("common.genericError"), }); }, }); }, [deleteId, queryClient, deleteMutation, t]); const handleUpdateDetails = useCallback( ( id: string, updates: { title: string; description: string; customInstructions: string; }, ) => { const previous = queryClient.getQueryData([ "action", "list-design-systems", undefined, ]); queryClient.setQueryData( ["action", "list-design-systems", undefined], (old: any) => { const systems = old?.designSystems ?? []; return { ...old, designSystems: systems.map((ds: DesignSystem) => ds.id === id ? { ...ds, ...updates } : ds, ), }; }, ); updateMutation.mutate({ id, ...updates } as any, { onSuccess: () => { toast.success(t("designSystems.updateSuccess")); }, onError: (error) => { queryClient.setQueryData( ["action", "list-design-systems", undefined], previous, ); toast.error(t("designSystems.updateError"), { description: error instanceof Error ? error.message : t("common.genericError"), }); }, }); }, [queryClient, updateMutation, t], ); const handleBulkDelete = useCallback(() => { const ids = Array.from(selectedSystemIds); if (ids.length === 0) return; const idsToDelete = new Set(ids); queryClient.setQueryData( ["action", "list-design-systems", undefined], (old: any) => { const systems = old?.designSystems ?? []; return { ...old, count: Math.max((old?.count ?? systems.length) - ids.length, 0), designSystems: systems.filter( (ds: DesignSystem) => !idsToDelete.has(ds.id), ), }; }, ); setBulkDeleteOpen(false); exitSelectionMode(); void Promise.all(ids.map((id) => deleteMutation.mutateAsync({ id } as any))) .then(() => undefined) .catch((error) => { queryClient.invalidateQueries({ queryKey: ["action", "list-design-systems"], }); toast.error(t("designSystems.bulkDeleteError"), { description: error instanceof Error ? error.message : t("common.genericError"), }); }); }, [selectedSystemIds, queryClient, exitSelectionMode, deleteMutation, t]); const parseData = (dataStr: string): DesignSystemData | null => { try { return JSON.parse(dataStr); } catch { return null; } }; useSetPageTitle(t("navigation.designSystems")); useSetHeaderActions(
{manageableDesignSystems.length > 0 ? ( ) : null}
, ); return ( <>
{isLoading ? ( ) : isError ? ( void refetch()} retrying={isFetching} /> ) : designSystems.length === 0 ? ( <>
) : ( <> {isSelectionMode ? (
{selectedSystemCount} {" "} {t("designSystems.selectedLabel")}
{allSystemsSelected ? t("designSystems.actions.clearAll") : t("designSystems.actions.selectAll")} {t("designSystems.actions.clearSelection")}
) : null}

{t("designSystems.yoursTitle")}

{/* New design system card */}

{t("designSystems.actions.new")}

{t("designSystems.newCardDescription")}
{/* Design system cards */} {designSystems.map((ds) => { const parsed = parseData(ds.data); const colors = parsed?.colors; const primaryColor = getCssColorToken(colors?.primary); const secondaryColor = getCssColorToken(colors?.secondary); const accentColor = getCssColorToken(colors?.accent); const headingFont = formatDesignTokenValue( parsed?.typography?.headingFont, ); const isSelected = selectedSystemIds.has(ds.id); return (
{isSelectionMode && ds.canManage ? (
toggleSystemSelection(ds.id) } onClick={(event) => event.stopPropagation()} aria-label={t("designSystems.selectAria", { title: ds.title, })} className="h-5 w-5 border-white/60 bg-black/60 text-white data-[state=checked]:border-[#609FF8] data-[state=checked]:bg-[#609FF8]" /> {t("designSystems.selectAria", { title: ds.title, })}
) : ( <> {/* Star button */} {ds.accessRole === "owner" && ( {ds.isDefault ? t("designSystems.currentlyDefault") : t("designSystems.actions.setDefault")} )} {ds.canManage && (
setDeleteId(ds.id)} className="text-red-400 focus:text-red-400 cursor-pointer" > {t("designSystems.actions.delete")}
)} )}
); })}
)}
{ if (!open) { setDeleteId(null); setBulkDeleteOpen(false); } }} > {bulkDeleteOpen ? t("designSystems.deleteDialog.bulkTitle", { count: selectedSystemCount, }) : t("designSystems.deleteDialog.title")} {bulkDeleteOpen ? t("designSystems.deleteDialog.bulkDescription", { count: selectedSystemCount, }) : t("designSystems.deleteDialog.description")} {t("designSystems.actions.cancel")} {t("designSystems.actions.delete")} { if (!open) closeDesignSystemDetails(); }} onUseAsSource={openSetupFromDesignSystem} onSave={handleUpdateDetails} /> ); } function DesignSystemDetailsSheet({ designSystem, open, isSaving, onOpenChange, onUseAsSource, onSave, }: { designSystem: DesignSystem | null; open: boolean; isSaving?: boolean; onOpenChange: (open: boolean) => void; onUseAsSource: (id: string) => void; onSave: ( id: string, updates: { title: string; description: string; customInstructions: string; }, ) => void; }) { const t = useT(); const [title, setTitle] = useState(""); const [description, setDescription] = useState(""); const [customInstructions, setCustomInstructions] = useState(""); useEffect(() => { if (!designSystem) return; setTitle(designSystem.title); setDescription(designSystem.description ?? ""); setCustomInstructions(designSystem.customInstructions ?? ""); // Only rehydrate when the user opens a different design system. Query // refetches can replace this object while the user is editing the sheet. // eslint-disable-next-line react-hooks/exhaustive-deps }, [designSystem?.id]); const parsed = useMemo( () => (designSystem ? parseDesignSystemData(designSystem.data) : null), [designSystem], ); const assets = useMemo( () => parseDesignSystemAssets(designSystem?.assets), [designSystem?.assets], ); if (!designSystem) { return null; } const canEdit = designSystem.accessRole === "owner" || designSystem.accessRole === "admin" || designSystem.accessRole === "editor"; const trimmedTitle = title.trim(); const hasChanges = trimmedTitle !== designSystem.title || description.trim() !== (designSystem.description ?? "") || customInstructions.trim() !== (designSystem.customInstructions ?? ""); return ( {designSystem.title} {t("designSystems.details.description")}
setTitle(event.target.value)} readOnly={!canEdit} className="bg-accent/50" />