diff --git a/apps/portfolio/template-library b/apps/portfolio/template-library index cb37b35e..507f3253 160000 --- a/apps/portfolio/template-library +++ b/apps/portfolio/template-library @@ -1 +1 @@ -Subproject commit cb37b35ee9a7a3ec2f093c0bd8d4dd65bbe90e49 +Subproject commit 507f32532e4e1600e3de127cc97175ad80b4af46 diff --git a/apps/studio/app/(main)/(dashboard)/components/OverviewHome.tsx b/apps/studio/app/(main)/(dashboard)/components/OverviewHome.tsx index dcc2212d..50bf8a9d 100644 --- a/apps/studio/app/(main)/(dashboard)/components/OverviewHome.tsx +++ b/apps/studio/app/(main)/(dashboard)/components/OverviewHome.tsx @@ -3,17 +3,16 @@ import type { LucideIcon } from "lucide-react"; import Link from "next/link"; -import { useSyncExternalStore } from "react"; import { BookOpen, FolderOpen, ArrowRight, BriefcaseBusiness } from "lucide-react"; import { Card } from "@veriworkly/ui"; -import { - getDocumentLibrarySnapshot, - subscribeToDocumentLibrary, - DOCUMENT_LIBRARY_SERVER_SNAPSHOT, -} from "@/features/documents/services/document-library"; +import DestructiveModal from "@/components/modals/DestructiveModal"; +import SyncDetailsModal from "@/components/modals/SyncDetailsModal"; +import ShareDocumentModal from "@/components/modals/ShareDocumentModal"; +import RenameDocumentModal from "@/components/modals/RenameDocumentModal"; +import { useDocumentsWorkspace } from "../documents/useDocumentsWorkspace"; import RecentCard from "./RecentCard"; import OverviewHomeHeader from "./OverviewHomeHeader"; import OverviewReferenceCard from "./OverviewReferenceCard"; @@ -32,18 +31,33 @@ function MiniLink({ href, icon: Icon, label }: { href: string; icon: LucideIcon; } const OverviewHome = () => { - const snapshot = useSyncExternalStore( - subscribeToDocumentLibrary, - () => getDocumentLibrarySnapshot(), - () => DOCUMENT_LIBRARY_SERVER_SNAPSHOT, - ); - - const totalCount = Object.values(snapshot.counts).reduce((sum, count) => sum + count, 0); - - const resumeCount = snapshot.counts.RESUME; - const coverLetterCount = snapshot.counts.COVER_LETTER; - - const recentDocs = snapshot.docs.slice(0, 6); + const { + counts, + handleSyncNow, + handleConfirmDelete, + handleKeepLocalOnly, + handleResolveUseCloud, + handleResolveUseLocal, + isDeleting, + deleteTarget, + shareTarget, + renameTarget, + syncDetailsTarget, + setDeleteTarget, + setShareTarget, + setRenameTarget, + setSyncDetailsTargetId, + syncingDocumentId, + syncTargetTelemetry, + totalCount, + visibleDocs, + bump, + } = useDocumentsWorkspace(); + + const resumeCount = counts.RESUME; + const coverLetterCount = counts.COVER_LETTER; + + const recentDocs = visibleDocs.slice(0, 6); return (
@@ -71,7 +85,18 @@ const OverviewHome = () => {
{recentDocs.length > 0 ? ( - recentDocs.map((doc) => ) + recentDocs.map((doc) => ( + + )) ) : (

No files yet

@@ -100,6 +125,45 @@ const OverviewHome = () => {
+ + setDeleteTarget(null)} + entityName={deleteTarget?.title ?? "document"} + /> + + {syncDetailsTarget ? ( + setSyncDetailsTargetId(null)} + /> + ) : null} + + {shareTarget ? ( + setShareTarget(null)} + /> + ) : null} + + {renameTarget ? ( + setRenameTarget(null)} + onSuccess={bump} + /> + ) : null}
); }; diff --git a/apps/studio/app/(main)/(dashboard)/components/RecentCard.tsx b/apps/studio/app/(main)/(dashboard)/components/RecentCard.tsx index 9598ad31..7aa6f058 100644 --- a/apps/studio/app/(main)/(dashboard)/components/RecentCard.tsx +++ b/apps/studio/app/(main)/(dashboard)/components/RecentCard.tsx @@ -9,16 +9,32 @@ import { Badge } from "@veriworkly/ui"; import { getDocumentDefinition } from "@/features/documents/core/registry"; import { getDocumentEditorPath } from "@/features/documents/core/routes"; import { type DocumentLibraryItem } from "@/features/documents/services/document-library"; +import { DocumentActionsMenu } from "../documents/components/DocumentActionsMenu"; -const RecentCard = ({ doc }: { doc: DocumentLibraryItem }) => { +interface RecentCardProps { + doc: DocumentLibraryItem; + syncing: boolean; + onDeleteAction: (doc: DocumentLibraryItem) => void; + onShareAction: (doc: DocumentLibraryItem) => void; + onRenameAction: (doc: DocumentLibraryItem) => void; + onSyncNowAction: (id: string) => void; + onSyncDetailsAction: (id: string) => void; +} + +const RecentCard = ({ + doc, + syncing, + onDeleteAction, + onShareAction, + onRenameAction, + onSyncNowAction, + onSyncDetailsAction, +}: RecentCardProps) => { const definition = getDocumentDefinition(doc.type); const editorPath = getDocumentEditorPath(doc.type, doc.id); return ( - +
{doc.previewImage ? ( {

{doc.description || doc.templateName}

- + + + + +
); }; diff --git a/apps/studio/features/resume/editor/EditorSettingsPanel.tsx b/apps/studio/features/resume/editor/EditorSettingsPanel.tsx index 6c4a02af..5adf72bc 100644 --- a/apps/studio/features/resume/editor/EditorSettingsPanel.tsx +++ b/apps/studio/features/resume/editor/EditorSettingsPanel.tsx @@ -1,16 +1,11 @@ "use client"; import { memo, useState } from "react"; - -import { - Accordion, - AccordionContent, - AccordionItem, - AccordionTrigger, - Button, -} from "@veriworkly/ui"; +import { ChevronDown, RotateCcw } from "lucide-react"; +import { Button } from "@veriworkly/ui"; import type { FontFamilyId } from "@/features/documents/constants/fonts"; +import { cn } from "@/lib/utils"; import { templateCatalogByType } from "@/features/documents/core/template-catalog"; import { @@ -24,10 +19,42 @@ import { SettingsColor, SettingsRange, SettingsSelect } from "./settings/Setting import { fontOptions } from "@/features/documents/constants/fonts"; import { useResumeStore } from "@/features/resume/store/resume-store"; import { defaultResume } from "@/features/resume/constants/default-resume"; -import { RotateCcw } from "lucide-react"; + +const SettingsSectionAccordion = ({ + children, + isOpen, + label, + onToggle, +}: { + children: React.ReactNode; + isOpen: boolean; + label: string; + onToggle: () => void; +}) => { + return ( +
+ + + {isOpen ?
{children}
: null} +
+ ); +}; const EditorSettingsPanel = memo(function EditorSettingsPanel() { const [templateModalOpen, setTemplateModalOpen] = useState(false); + const [openSection, setOpenSection] = useState<"typography" | "colors" | "visibility" | null>( + "typography", + ); const sections = useResumeStore((state) => state.resume.sections); const templateId = useResumeStore((state) => state.resume.templateId); @@ -37,11 +64,18 @@ const EditorSettingsPanel = memo(function EditorSettingsPanel() { const reorderSections = useResumeStore((state) => state.reorderSections); const updateCustomization = useResumeStore((state) => state.updateCustomization); const setSectionVisibility = useResumeStore((state) => state.setSectionVisibility); + const updateSectionColumn = useResumeStore((state) => state.updateSectionColumn); const selectedTemplate = templateCatalogByType.RESUME.find( (template) => template.id === templateId, ); + const isTwoColumnTemplate = !!selectedTemplate?.tags.includes("Two columns"); + + const handleToggle = (section: "typography" | "colors" | "visibility") => { + setOpenSection((curr) => (curr === section ? null : section)); + }; + return (
@@ -55,156 +89,165 @@ const EditorSettingsPanel = memo(function EditorSettingsPanel() { onOpen={() => setTemplateModalOpen(true)} /> -
- - - Typography & Spacing - - + handleToggle("typography")} + > +
+ + updateCustomization({ + fontFamily: event.target.value as FontFamilyId, + }) + } + > + {fontOptions.map((font) => ( + + ))} + + + + updateCustomization({ + sectionSpacing: Number(event.target.value), + }) + } + /> + + + updateCustomization({ + pagePadding: Number(event.target.value), + }) + } + /> + + + updateCustomization({ + bodyLineHeight: Number(event.target.value), + }) + } + /> +
+
+ + handleToggle("colors")} + > +
+
+ updateCustomization({ - fontFamily: event.target.value as FontFamilyId, + accentColor: event.target.value, }) } - > - {fontOptions.map((font) => ( - - ))} - - - + + updateCustomization({ borderColor: event.target.value })} + value={customization.borderColor} + /> + + updateCustomization({ textColor: event.target.value })} + value={customization.textColor} + /> + + updateCustomization({ mutedTextColor: event.target.value })} + value={customization.mutedTextColor} + /> + + - updateCustomization({ - sectionSpacing: Number(event.target.value), - }) + updateCustomization({ pageBackgroundColor: event.target.value }) } + value={customization.pageBackgroundColor} /> - updateCustomization({ - pagePadding: Number(event.target.value), + sectionBackgroundColor: event.target.value, }) } + value={customization.sectionBackgroundColor} /> - - updateCustomization({ - bodyLineHeight: Number(event.target.value), - }) + updateCustomization({ sectionHeadingColor: event.target.value }) } + value={customization.sectionHeadingColor} /> - - - - - Color Theme - -
- - updateCustomization({ - accentColor: event.target.value, - }) - } - /> - - updateCustomization({ borderColor: event.target.value })} - value={customization.borderColor} - /> - - updateCustomization({ textColor: event.target.value })} - value={customization.textColor} - /> - - updateCustomization({ mutedTextColor: event.target.value })} - value={customization.mutedTextColor} - /> - - - updateCustomization({ pageBackgroundColor: event.target.value }) - } - value={customization.pageBackgroundColor} - /> - - - updateCustomization({ - sectionBackgroundColor: event.target.value, - }) - } - value={customization.sectionBackgroundColor} - /> - - - updateCustomization({ sectionHeadingColor: event.target.value }) - } - value={customization.sectionHeadingColor} - /> -
- - -
-
- - - Section Visibility - - - - - +
+ + +
+
+ + handleToggle("visibility")} + > +
+ +
+
void; @@ -10,6 +10,8 @@ interface SectionVisibilitySettingsProps { sections: ResumeSection[]; /** When true, omit the section title (e.g. inside an accordion). */ embedded?: boolean; + isTwoColumn?: boolean; + onUpdateSectionColumn?: (sectionId: ResumeSectionId, column: "left" | "right") => void; } const SectionVisibilitySettings = ({ @@ -17,8 +19,118 @@ const SectionVisibilitySettings = ({ onToggle, sections, embedded = false, + isTwoColumn = false, + onUpdateSectionColumn, }: SectionVisibilitySettingsProps) => { const sortedSections = sections.slice().sort((left, right) => left.order - right.order); + const [draggedSectionId, setDraggedSectionId] = useState(null); + + const handleDropOnColumn = (targetColumn: "left" | "right") => { + if (!draggedSectionId || !onUpdateSectionColumn) return; + + const section = sections.find((s) => s.id === draggedSectionId); + if (!section || section.id === "basics" || section.id === "links") return; + + onUpdateSectionColumn(draggedSectionId, targetColumn); + setDraggedSectionId(null); + }; + + const handleDropOnSection = (targetSectionId: ResumeSectionId) => { + if (!draggedSectionId || draggedSectionId === targetSectionId) return; + + const targetSection = sections.find((s) => s.id === targetSectionId); + if (!targetSection) return; + + const fromIndex = sortedSections.findIndex((s) => s.id === draggedSectionId); + const toIndex = sortedSections.findIndex((s) => s.id === targetSectionId); + + if (fromIndex !== -1 && toIndex !== -1 && fromIndex >= 2 && toIndex >= 2) { + if (onUpdateSectionColumn) { + onUpdateSectionColumn(draggedSectionId, targetSection.column || "left"); + } + onMove(fromIndex, toIndex); + } + + setDraggedSectionId(null); + }; + + const renderSectionCard = (section: ResumeSection) => { + const index = sortedSections.findIndex((s) => s.id === section.id); + const isLocked = section.id === "basics" || section.id === "links"; + + return ( +
!isLocked && setDraggedSectionId(section.id)} + onDragEnd={() => setDraggedSectionId(null)} + onDragOver={(e) => { + if (!isLocked) e.preventDefault(); + }} + onDrop={(e) => { + e.stopPropagation(); + if (!isLocked) handleDropOnSection(section.id); + }} + className={cn( + "border-border bg-card/85 flex items-center justify-between gap-2 rounded-xl border p-2 text-xs transition", + draggedSectionId === section.id ? "opacity-40" : "", + !isLocked ? "hover:border-accent/40 cursor-grab active:cursor-grabbing" : "", + )} + > + + +
+ {/* Column Select Dropdown (visible in two column template) */} + {isTwoColumn && !isLocked && onUpdateSectionColumn && ( + + )} + + {/* Position Select Dropdown */} + {!isLocked && ( + + )} +
+
+ ); + }; + + const leftSections = sortedSections.filter((s) => s.column !== "right"); + const rightSections = sortedSections.filter((s) => s.column === "right"); return (
@@ -29,46 +141,59 @@ const SectionVisibilitySettings = ({
)} -
- {sortedSections.map((section, index) => ( -
-
- - -
+ {isTwoColumn ? ( +
+

+ Drag & drop cards to reorder or move between columns. Or use dropdown selections. +

-
+
+ ) : ( +
+ {sortedSections.map((section) => renderSectionCard(section))} +
+ )}
); }; diff --git a/apps/studio/features/resume/store/resume-store.ts b/apps/studio/features/resume/store/resume-store.ts index f6886185..d988b895 100644 --- a/apps/studio/features/resume/store/resume-store.ts +++ b/apps/studio/features/resume/store/resume-store.ts @@ -47,6 +47,7 @@ interface ResumeStoreState { selectSection: (section: ResumeSectionId) => void; setSectionVisibility: (section: ResumeSectionId, visible: boolean) => void; reorderSections: (fromIndex: number, toIndex: number) => void; + updateSectionColumn: (sectionId: ResumeSectionId, column: "left" | "right") => void; setTemplateId: (templateId: string) => void; updateCustomization: (values: Partial) => void; updateBasics: (values: Partial) => void; @@ -170,6 +171,9 @@ export const useResumeStore = create((set, get) => ({ reorderSections: (fromIndex, toIndex) => set((state) => { + if (fromIndex < 2 || toIndex < 2) { + return {}; + } const reorderedSections = reorderItems(state.resume.sections, fromIndex, toIndex).map( (section, index) => ({ ...section, @@ -185,6 +189,16 @@ export const useResumeStore = create((set, get) => ({ }; }), + updateSectionColumn: (sectionId, column) => + set((state) => ({ + resume: withTimestamp({ + ...state.resume, + sections: state.resume.sections.map((section) => + section.id === sectionId ? { ...section, column } : section, + ), + }), + })), + setTemplateId: (templateId) => set((state) => ({ resume: withTimestamp({ diff --git a/apps/studio/features/resume/utils/normalize-data.ts b/apps/studio/features/resume/utils/normalize-data.ts index 1e7f20c4..7edba698 100644 --- a/apps/studio/features/resume/utils/normalize-data.ts +++ b/apps/studio/features/resume/utils/normalize-data.ts @@ -88,13 +88,22 @@ function normalizeSections(value: Partial | null | undefined) { }; }); - return merged - .slice() - .sort((left, right) => left.order - right.order) - .map((section, index) => ({ + const basicsSection = merged.find((s) => s.id === "basics") || defaultSections[0]; + const linksSection = merged.find((s) => s.id === "links") || defaultSections[1]; + const otherSections = merged + .filter((s) => s.id !== "basics" && s.id !== "links") + .sort((left, right) => left.order - right.order); + + const finalSections = [ + { ...basicsSection, order: 0 }, + { ...linksSection, order: 1 }, + ...otherSections.map((section, index) => ({ ...section, - order: index, - })); + order: index + 2, + })), + ]; + + return finalSections; } function normalizeNumericDate(value: string | undefined, maxLength: number) { diff --git a/apps/studio/types/resume.ts b/apps/studio/types/resume.ts index 7fc34cb7..2d39fc98 100644 --- a/apps/studio/types/resume.ts +++ b/apps/studio/types/resume.ts @@ -24,6 +24,7 @@ export interface ResumeSection { label: string; visible: boolean; order: number; + column?: "left" | "right"; } export interface ResumeBasics {