UNPKG

@gongfu/prd-editor

Version:

A professional PRD (Product Requirements Document) editor SDK with AI-powered features

1,254 lines (1,229 loc) 77.5 kB
"use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; }"use client" // src/components/PrdEditor.tsx var _react = require('react'); // src/store/prd-store.ts var _zustand = require('zustand'); var _middleware = require('zustand/middleware'); // src/utils/index.ts function generateId() { return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } function createEmptyDocument(title = "Untitled PRD") { const now = /* @__PURE__ */ new Date(); return { id: generateId(), title, version: "1.0.0", status: "draft", author: { id: "system", name: "System" }, createdAt: now, updatedAt: now, sections: [], requirements: [], userStories: [], tags: [] }; } function createSectionFromTemplate(type, title, order, defaultContent) { return { id: generateId(), type, title, content: defaultContent || "", order }; } function applyTemplate(document, template) { const sections = template.sections.map( (section, index) => createSectionFromTemplate( section.type, section.title, index, section.defaultContent ) ); return { ...document, sections, metadata: { ...document.metadata, templateId: template.id, templateName: template.name } }; } function validateDocument(document) { const errors = []; const warnings = []; if (!document.title || document.title.trim().length === 0) { errors.push({ field: "title", message: "Document title is required", severity: "error" }); } if (document.sections.length === 0) { warnings.push({ field: "sections", message: "Document has no sections", severity: "warning" }); } document.sections.forEach((section, index) => { if (!section.content || section.content.trim().length === 0) { warnings.push({ field: `sections[${index}].content`, message: `Section "${section.title}" is empty`, severity: "warning" }); } }); if (document.requirements.length === 0) { warnings.push({ field: "requirements", message: "No requirements defined", severity: "warning" }); } document.requirements.forEach((req, index) => { if (!req.title || req.title.trim().length === 0) { errors.push({ field: `requirements[${index}].title`, message: "Requirement title is required", severity: "error" }); } if (!req.description || req.description.trim().length === 0) { warnings.push({ field: `requirements[${index}].description`, message: `Requirement "${req.title}" has no description`, severity: "warning" }); } }); const totalChecks = 10; const passedChecks = totalChecks - errors.length - warnings.length * 0.5; const score = Math.max(0, Math.round(passedChecks / totalChecks * 100)); return { valid: errors.length === 0, errors, warnings, score }; } function exportToMarkdown(document) { let markdown = `# ${document.title} `; markdown += `**Version:** ${document.version} `; markdown += `**Status:** ${document.status} `; markdown += `**Author:** ${document.author.name} `; markdown += `**Last Updated:** ${document.updatedAt.toISOString()} `; markdown += `## Table of Contents `; document.sections.forEach((section, index) => { markdown += `${index + 1}. [${section.title}](#${section.title.toLowerCase().replace(/\s+/g, "-")}) `; }); markdown += "\n"; document.sections.forEach((section) => { markdown += `## ${section.title} `; markdown += `${section.content} `; }); if (document.requirements.length > 0) { markdown += `## Requirements `; const grouped = groupRequirementsByType(document.requirements); Object.entries(grouped).forEach(([type, reqs]) => { markdown += `### ${capitalizeFirst(type)} Requirements `; reqs.forEach((req) => { markdown += `#### ${req.title} `; markdown += `**Priority:** ${req.priority} `; markdown += `**Status:** ${req.status || "draft"} `; markdown += `${req.description} `; if (req.acceptanceCriteria && req.acceptanceCriteria.length > 0) { markdown += `**Acceptance Criteria:** `; req.acceptanceCriteria.forEach((criteria) => { markdown += `- ${criteria} `; }); markdown += "\n"; } }); }); } if (document.userStories.length > 0) { markdown += `## User Stories `; document.userStories.forEach((story) => { markdown += `### ${story.title} `; markdown += `**As a** ${story.asA}, `; markdown += `**I want** ${story.iWant}, `; markdown += `**So that** ${story.soThat} `; if (story.acceptanceCriteria.length > 0) { markdown += `**Acceptance Criteria:** `; story.acceptanceCriteria.forEach((criteria) => { markdown += `- ${criteria} `; }); markdown += "\n"; } markdown += `**Priority:** ${story.priority} `; if (story.effort) { markdown += `**Effort:** ${story.effort} points `; } markdown += "\n"; }); } return markdown; } function groupRequirementsByType(requirements) { return requirements.reduce((acc, req) => { if (!acc[req.type]) { acc[req.type] = []; } acc[req.type].push(req); return acc; }, {}); } function groupRequirementsByPriority(requirements) { return requirements.reduce((acc, req) => { if (!acc[req.priority]) { acc[req.priority] = []; } acc[req.priority].push(req); return acc; }, {}); } function calculateProgress(document) { const requirements = document.requirements; const total = requirements.length; const completed = requirements.filter((r) => r.status === "implemented").length; const percentage = total > 0 ? Math.round(completed / total * 100) : 0; return { total, completed, percentage }; } function searchDocument(document, query) { const lowerQuery = query.toLowerCase(); const sections = document.sections.filter( (section) => section.title.toLowerCase().includes(lowerQuery) || section.content.toLowerCase().includes(lowerQuery) ); const requirements = document.requirements.filter( (req) => req.title.toLowerCase().includes(lowerQuery) || req.description.toLowerCase().includes(lowerQuery) || _optionalChain([req, 'access', _ => _.tags, 'optionalAccess', _2 => _2.some, 'call', _3 => _3((tag) => tag.toLowerCase().includes(lowerQuery))]) ); const userStories = document.userStories.filter( (story) => story.title.toLowerCase().includes(lowerQuery) || story.asA.toLowerCase().includes(lowerQuery) || story.iWant.toLowerCase().includes(lowerQuery) || story.soThat.toLowerCase().includes(lowerQuery) || _optionalChain([story, 'access', _4 => _4.tags, 'optionalAccess', _5 => _5.some, 'call', _6 => _6((tag) => tag.toLowerCase().includes(lowerQuery))]) ); return { sections, requirements, userStories }; } function generateTableOfContents(sections) { return sections.sort((a, b) => a.order - b.order).map((section) => ({ id: section.id, title: section.title, level: 1 })); } function capitalizeFirst(str) { return str.charAt(0).toUpperCase() + str.slice(1); } function formatVersion(major, minor, patch) { return `${major}.${minor}.${patch}`; } function incrementVersion(version, type = "patch") { const [major, minor, patch] = version.split(".").map(Number); switch (type) { case "major": return formatVersion(major + 1, 0, 0); case "minor": return formatVersion(major, minor + 1, 0); case "patch": default: return formatVersion(major, minor, patch + 1); } } var defaultTemplates = [ { id: "standard", name: "Standard PRD", description: "A comprehensive PRD template for most products", sections: [ { type: "overview", title: "Executive Summary", required: true }, { type: "background", title: "Background & Context", required: true }, { type: "objectives", title: "Goals & Objectives", required: true }, { type: "scope", title: "Scope & Constraints", required: true }, { type: "requirements", title: "Functional Requirements", required: true }, { type: "requirements", title: "Non-Functional Requirements", required: false }, { type: "user-stories", title: "User Stories", required: true }, { type: "acceptance-criteria", title: "Acceptance Criteria", required: true }, { type: "timeline", title: "Timeline & Milestones", required: true }, { type: "risks", title: "Risks & Mitigation", required: false } ] }, { id: "agile", name: "Agile PRD", description: "Lightweight PRD for agile teams", sections: [ { type: "overview", title: "Product Vision", required: true }, { type: "objectives", title: "Sprint Goals", required: true }, { type: "user-stories", title: "User Stories", required: true }, { type: "acceptance-criteria", title: "Definition of Done", required: true }, { type: "scope", title: "Out of Scope", required: false } ] }, { id: "technical", name: "Technical PRD", description: "PRD template for technical products and APIs", sections: [ { type: "overview", title: "Technical Overview", required: true }, { type: "background", title: "System Architecture", required: true }, { type: "requirements", title: "Technical Requirements", required: true }, { type: "requirements", title: "API Specifications", required: true }, { type: "requirements", title: "Security Requirements", required: true }, { type: "acceptance-criteria", title: "Testing Strategy", required: true }, { type: "timeline", title: "Implementation Plan", required: true }, { type: "appendix", title: "Technical Appendix", required: false } ] } ]; // src/store/prd-store.ts var usePrdStore = _zustand.create.call(void 0, )( _middleware.subscribeWithSelector.call(void 0, (set, get) => ({ // Initial state document: null, isDirty: false, isLoading: false, isSaving: false, error: null, selectedSectionId: null, selectedRequirementId: null, selectedUserStoryId: null, expandedSections: [], searchQuery: "", activeView: "editor", comments: [], versionHistory: [], aiSuggestions: [], config: { mode: "light", showToolbar: true, showOutline: true, showComments: true, showVersionHistory: true, showAiAssistant: true, autosave: true, autosaveInterval: 3e4, locale: "en" }, validation: null, canPublish: false, hasUnsavedChanges: false, // Document actions loadDocument: (document) => { set({ document, isDirty: false, error: null, validation: validateDocument(document) }); }, createDocument: (title) => { const now = /* @__PURE__ */ new Date(); const document = { id: generateId(), title: title || "Untitled PRD", version: "1.0.0", status: "draft", author: { id: "current-user", name: "Current User" }, createdAt: now, updatedAt: now, sections: [], requirements: [], userStories: [] }; set({ document, isDirty: false, error: null, validation: validateDocument(document) }); }, updateDocument: (updates) => { const { document } = get(); if (!document) return; const updated = { ...document, ...updates, updatedAt: /* @__PURE__ */ new Date() }; set({ document: updated, isDirty: true, validation: validateDocument(updated) }); }, saveDocument: async () => { const { document, config } = get(); if (!document || !config.onSave) return; set({ isSaving: true, error: null }); try { await config.onSave(document); set({ isDirty: false }); } catch (error) { set({ error: error instanceof Error ? error.message : "Save failed" }); } finally { set({ isSaving: false }); } }, publishDocument: async () => { const { document, config } = get(); if (!document || !config.onPublish) return; const validation = validateDocument(document); if (!validation.valid) { set({ error: "Document has validation errors" }); return; } set({ isSaving: true, error: null }); try { const published = { ...document, status: "published", publishedAt: /* @__PURE__ */ new Date(), version: incrementVersion(document.version, "minor") }; await config.onPublish(published); set({ document: published, isDirty: false }); } catch (error) { set({ error: error instanceof Error ? error.message : "Publish failed" }); } finally { set({ isSaving: false }); } }, // Section actions addSection: (section) => { const { document } = get(); if (!document) return; const sections = [...document.sections, section]; set({ document: { ...document, sections, updatedAt: /* @__PURE__ */ new Date() }, isDirty: true }); }, updateSection: (sectionId, updates) => { const { document } = get(); if (!document) return; const sections = document.sections.map( (section) => section.id === sectionId ? { ...section, ...updates } : section ); set({ document: { ...document, sections, updatedAt: /* @__PURE__ */ new Date() }, isDirty: true }); }, deleteSection: (sectionId) => { const { document } = get(); if (!document) return; const sections = document.sections.filter((s) => s.id !== sectionId); set({ document: { ...document, sections, updatedAt: /* @__PURE__ */ new Date() }, isDirty: true, selectedSectionId: get().selectedSectionId === sectionId ? null : get().selectedSectionId }); }, reorderSections: (sectionIds) => { const { document } = get(); if (!document) return; const sections = sectionIds.map((id, index) => { const section = document.sections.find((s) => s.id === id); return section ? { ...section, order: index } : null; }).filter(Boolean); set({ document: { ...document, sections, updatedAt: /* @__PURE__ */ new Date() }, isDirty: true }); }, // Requirement actions addRequirement: (requirement) => { const { document } = get(); if (!document) return; const requirements = [...document.requirements, requirement]; set({ document: { ...document, requirements, updatedAt: /* @__PURE__ */ new Date() }, isDirty: true }); }, updateRequirement: (requirementId, updates) => { const { document } = get(); if (!document) return; const requirements = document.requirements.map( (req) => req.id === requirementId ? { ...req, ...updates } : req ); set({ document: { ...document, requirements, updatedAt: /* @__PURE__ */ new Date() }, isDirty: true }); }, deleteRequirement: (requirementId) => { const { document } = get(); if (!document) return; const requirements = document.requirements.filter((r) => r.id !== requirementId); set({ document: { ...document, requirements, updatedAt: /* @__PURE__ */ new Date() }, isDirty: true, selectedRequirementId: get().selectedRequirementId === requirementId ? null : get().selectedRequirementId }); }, // User story actions addUserStory: (userStory) => { const { document } = get(); if (!document) return; const userStories = [...document.userStories, userStory]; set({ document: { ...document, userStories, updatedAt: /* @__PURE__ */ new Date() }, isDirty: true }); }, updateUserStory: (storyId, updates) => { const { document } = get(); if (!document) return; const userStories = document.userStories.map( (story) => story.id === storyId ? { ...story, ...updates } : story ); set({ document: { ...document, userStories, updatedAt: /* @__PURE__ */ new Date() }, isDirty: true }); }, deleteUserStory: (storyId) => { const { document } = get(); if (!document) return; const userStories = document.userStories.filter((s) => s.id !== storyId); set({ document: { ...document, userStories, updatedAt: /* @__PURE__ */ new Date() }, isDirty: true, selectedUserStoryId: get().selectedUserStoryId === storyId ? null : get().selectedUserStoryId }); }, // UI actions selectSection: (sectionId) => { set({ selectedSectionId: sectionId, selectedRequirementId: null, selectedUserStoryId: null }); }, selectRequirement: (requirementId) => { set({ selectedRequirementId: requirementId, selectedSectionId: null, selectedUserStoryId: null }); }, selectUserStory: (storyId) => { set({ selectedUserStoryId: storyId, selectedSectionId: null, selectedRequirementId: null }); }, toggleSectionExpanded: (sectionId) => { set((state) => ({ expandedSections: state.expandedSections.includes(sectionId) ? state.expandedSections.filter((id) => id !== sectionId) : [...state.expandedSections, sectionId] })); }, setSearchQuery: (query) => { set({ searchQuery: query }); }, setActiveView: (view) => { set({ activeView: view }); }, // Comment actions addComment: (targetId, targetType, content) => { const thread = get().comments.find((t) => t.targetId === targetId); const comment = { id: generateId(), content, author: { id: "current-user", name: "Current User" }, createdAt: /* @__PURE__ */ new Date() }; if (thread) { const comments = get().comments.map( (t) => t.id === thread.id ? { ...t, comments: [...t.comments, comment] } : t ); set({ comments }); } else { const newThread = { id: generateId(), targetId, targetType, resolved: false, comments: [comment] }; set({ comments: [...get().comments, newThread] }); } }, resolveThread: (threadId) => { const comments = get().comments.map( (thread) => thread.id === threadId ? { ...thread, resolved: true } : thread ); set({ comments }); }, deleteComment: (threadId, commentId) => { const comments = get().comments.map( (thread) => thread.id === threadId ? { ...thread, comments: thread.comments.filter((c) => c.id !== commentId) } : thread ).filter((thread) => thread.comments.length > 0); set({ comments }); }, // Version history actions createVersion: (message) => { const { document } = get(); if (!document) return; const version = { id: generateId(), version: document.version, author: document.author, timestamp: /* @__PURE__ */ new Date(), changes: message, snapshot: { ...document } }; set({ versionHistory: [...get().versionHistory, version], document: { ...document, version: incrementVersion(document.version) } }); }, restoreVersion: (versionId) => { const version = get().versionHistory.find((v) => v.id === versionId); if (!version) return; set({ document: { ...version.snapshot, updatedAt: /* @__PURE__ */ new Date() }, isDirty: true }); }, // AI actions requestAiSuggestion: async (type, targetId) => { const { config } = get(); if (!_optionalChain([config, 'access', _7 => _7.aiConfig, 'optionalAccess', _8 => _8.enabled])) return; const suggestion = { id: generateId(), type, targetId, content: `AI suggested content for ${type}`, confidence: 0.85, reasoning: "Based on similar documents and best practices" }; set({ aiSuggestions: [...get().aiSuggestions, suggestion] }); }, applyAiSuggestion: (suggestionId) => { const suggestion = get().aiSuggestions.find((s) => s.id === suggestionId); if (!suggestion) return; set({ aiSuggestions: get().aiSuggestions.filter((s) => s.id !== suggestionId) }); }, dismissAiSuggestion: (suggestionId) => { set({ aiSuggestions: get().aiSuggestions.filter((s) => s.id !== suggestionId) }); }, // Config actions setConfig: (config) => { set({ config: { ...get().config, ...config } }); } })) ); usePrdStore.subscribe( (state) => state.isDirty, (isDirty) => { if (isDirty) { const { config, saveDocument } = usePrdStore.getState(); if (config.autosave) { const timeout = setTimeout(() => { saveDocument(); }, config.autosaveInterval || 3e4); return () => clearTimeout(timeout); } } } ); // src/components/EditorToolbar.tsx var _jsxruntime = require('react/jsx-runtime'); var EditorToolbar = () => { const { document, isDirty, isSaving, activeView, config, saveDocument, publishDocument, setActiveView, createVersion } = usePrdStore(); if (!document) return null; const handleExport = async (format3) => { if (config.onExport) { await config.onExport(format3); } else if (format3 === "markdown") { const markdown = exportToMarkdown(document); const blob = new Blob([markdown], { type: "text/markdown" }); const url = URL.createObjectURL(blob); const a = window.document.createElement("a"); a.href = url; a.download = `${document.title}.md`; a.click(); URL.revokeObjectURL(url); } }; const handleCreateVersion = () => { const message = prompt("Enter version message:"); if (message) { createVersion(message); } }; return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "prd-toolbar border-b bg-white dark:bg-gray-800 px-4 py-2", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-4", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h1", { className: "text-lg font-semibold text-gray-900 dark:text-gray-100", children: document.title }), /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-2 text-sm text-gray-500", children: [ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { children: [ "v", document.version ] }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: "\u2022" }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: `capitalize ${document.status === "published" ? "text-green-600" : document.status === "approved" ? "text-blue-600" : "text-gray-600"}`, children: document.status }), isDirty && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, _jsxruntime.Fragment, { children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: "\u2022" }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "text-orange-600", children: "Unsaved changes" }) ] }) ] }) ] }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "flex items-center", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex bg-gray-100 dark:bg-gray-700 rounded-lg p-1", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => setActiveView("editor"), className: `px-3 py-1 rounded text-sm font-medium transition-colors ${activeView === "editor" ? "bg-white dark:bg-gray-600 text-gray-900 dark:text-gray-100 shadow-sm" : "text-gray-600 dark:text-gray-400 hover:text-gray-900"}`, children: "Editor" } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => setActiveView("split"), className: `px-3 py-1 rounded text-sm font-medium transition-colors ${activeView === "split" ? "bg-white dark:bg-gray-600 text-gray-900 dark:text-gray-100 shadow-sm" : "text-gray-600 dark:text-gray-400 hover:text-gray-900"}`, children: "Split" } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => setActiveView("preview"), className: `px-3 py-1 rounded text-sm font-medium transition-colors ${activeView === "preview" ? "bg-white dark:bg-gray-600 text-gray-900 dark:text-gray-100 shadow-sm" : "text-gray-600 dark:text-gray-400 hover:text-gray-900"}`, children: "Preview" } ) ] }) }), /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center gap-2", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: handleCreateVersion, className: "px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100", title: "Create version", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" }) }) } ), /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "relative group", children: [ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "button", { className: "px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 dark:text-gray-400 dark:hover:text-gray-100", children: [ "Export", /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { className: "w-4 h-4 inline-block ml-1", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M19 9l-7 7-7-7" }) }) ] }), /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 rounded-lg shadow-lg border dark:border-gray-700 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => handleExport("markdown"), className: "block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700", children: "Export as Markdown" } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => handleExport("pdf"), className: "block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700", children: "Export as PDF" } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => handleExport("docx"), className: "block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700", children: "Export as Word" } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => handleExport("html"), className: "block w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700", children: "Export as HTML" } ) ] }) ] }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: saveDocument, disabled: !isDirty || isSaving || !config.onSave, className: `px-4 py-1.5 text-sm font-medium rounded-lg transition-colors ${isDirty && !isSaving ? "bg-blue-500 text-white hover:bg-blue-600" : "bg-gray-300 text-gray-500 cursor-not-allowed"}`, children: isSaving ? "Saving..." : "Save" } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: publishDocument, disabled: isDirty || document.status === "published" || !config.onPublish, className: `px-4 py-1.5 text-sm font-medium rounded-lg transition-colors ${!isDirty && document.status !== "published" ? "bg-green-500 text-white hover:bg-green-600" : "bg-gray-300 text-gray-500 cursor-not-allowed"}`, children: "Publish" } ) ] }) ] }) }); }; // src/components/EditorSidebar.tsx var EditorSidebar = () => { const { document, selectedSectionId, selectedRequirementId, selectedUserStoryId, expandedSections, selectSection, selectRequirement, selectUserStory, toggleSectionExpanded, addSection } = usePrdStore(); if (!document) return null; const handleAddSection = () => { const title = prompt("Enter section title:"); if (title) { const section = { id: `section-${Date.now()}`, type: "custom", title, content: "", order: document.sections.length }; addSection(section); } }; return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "prd-sidebar w-64 border-r bg-gray-50 dark:bg-gray-900 overflow-y-auto", children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "p-4", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h2", { className: "text-sm font-semibold text-gray-600 dark:text-gray-400 uppercase tracking-wider mb-3", children: "Outline" }), /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "space-y-1", children: [ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center justify-between mb-2", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h3", { className: "text-sm font-medium text-gray-700 dark:text-gray-300", children: "Sections" }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: handleAddSection, className: "text-blue-500 hover:text-blue-600 text-sm", title: "Add section", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M12 4v16m8-8H4" }) }) } ) ] }), document.sections.sort((a, b) => a.order - b.order).map((section) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => selectSection(section.id), className: `w-full text-left px-3 py-2 rounded-lg text-sm transition-colors ${selectedSectionId === section.id ? "bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300" : "hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-700 dark:text-gray-300"}`, children: section.title }, section.id )) ] }), document.requirements.length > 0 && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mt-6 space-y-1", children: [ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "button", { onClick: () => toggleSectionExpanded("requirements"), className: "flex items-center justify-between w-full text-sm font-medium text-gray-700 dark:text-gray-300 mb-2", children: [ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { children: [ "Requirements (", document.requirements.length, ")" ] }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { className: `w-4 h-4 transition-transform ${expandedSections.includes("requirements") ? "rotate-90" : ""}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) } ) ] } ), expandedSections.includes("requirements") && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "space-y-1 pl-3", children: document.requirements.map((req) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => selectRequirement(req.id), className: `w-full text-left px-3 py-1.5 rounded-lg text-sm transition-colors ${selectedRequirementId === req.id ? "bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300" : "hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400"}`, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "truncate", children: req.title }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: `text-xs px-1.5 py-0.5 rounded-full ${req.priority === "must-have" ? "bg-red-100 text-red-600" : req.priority === "should-have" ? "bg-orange-100 text-orange-600" : req.priority === "could-have" ? "bg-yellow-100 text-yellow-600" : "bg-gray-100 text-gray-600"}`, children: req.priority.split("-")[0] }) ] }) }, req.id )) }) ] }), document.userStories.length > 0 && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mt-6 space-y-1", children: [ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "button", { onClick: () => toggleSectionExpanded("user-stories"), className: "flex items-center justify-between w-full text-sm font-medium text-gray-700 dark:text-gray-300 mb-2", children: [ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { children: [ "User Stories (", document.userStories.length, ")" ] }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { className: `w-4 h-4 transition-transform ${expandedSections.includes("user-stories") ? "rotate-90" : ""}`, fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5l7 7-7 7" }) } ) ] } ), expandedSections.includes("user-stories") && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "space-y-1 pl-3", children: document.userStories.map((story) => /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => selectUserStory(story.id), className: `w-full text-left px-3 py-1.5 rounded-lg text-sm transition-colors ${selectedUserStoryId === story.id ? "bg-blue-100 dark:bg-blue-900 text-blue-700 dark:text-blue-300" : "hover:bg-gray-100 dark:hover:bg-gray-800 text-gray-600 dark:text-gray-400"}`, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex items-center justify-between", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { className: "truncate", children: story.title }), story.effort && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "span", { className: "text-xs text-gray-500", children: [ story.effort, "pt" ] }) ] }) }, story.id )) }) ] }), /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "mt-8 p-3 bg-gray-100 dark:bg-gray-800 rounded-lg", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h3", { className: "text-xs font-semibold text-gray-600 dark:text-gray-400 uppercase mb-2", children: "Statistics" }), /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "space-y-1 text-xs text-gray-600 dark:text-gray-400", children: [ /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex justify-between", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: "Sections" }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: document.sections.length }) ] }), /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex justify-between", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: "Requirements" }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: document.requirements.length }) ] }), /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex justify-between", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: "User Stories" }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: document.userStories.length }) ] }), /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "flex justify-between", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: "Last Updated" }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "span", { children: new Date(document.updatedAt).toLocaleDateString() }) ] }) ] }) ] }) ] }) }); }; // src/components/EditorContent.tsx var _react3 = require('@tiptap/react'); var _starterkit = require('@tiptap/starter-kit'); var _starterkit2 = _interopRequireDefault(_starterkit); var _extensionplaceholder = require('@tiptap/extension-placeholder'); var _extensionplaceholder2 = _interopRequireDefault(_extensionplaceholder); var _extensiontasklist = require('@tiptap/extension-task-list'); var _extensiontasklist2 = _interopRequireDefault(_extensiontasklist); var _extensiontaskitem = require('@tiptap/extension-task-item'); var _extensiontaskitem2 = _interopRequireDefault(_extensiontaskitem); var _extensiontable = require('@tiptap/extension-table'); var _extensiontable2 = _interopRequireDefault(_extensiontable); var _extensionlink = require('@tiptap/extension-link'); var _extensionlink2 = _interopRequireDefault(_extensionlink); var EditorContent = () => { const { document, selectedSectionId, selectedRequirementId, selectedUserStoryId, updateSection, updateRequirement, updateUserStory } = usePrdStore(); const editor = _react3.useEditor.call(void 0, { extensions: [ _starterkit2.default, _extensionplaceholder2.default.configure({ placeholder: "Start writing..." }), _extensiontasklist2.default, _extensiontaskitem2.default.configure({ nested: true }), _extensiontable2.default.configure({ resizable: true }), _extensionlink2.default.configure({ openOnClick: false }) ], content: "", onUpdate: ({ editor: editor2 }) => { const content = editor2.getHTML(); if (selectedSectionId) { updateSection(selectedSectionId, { content }); } else if (selectedRequirementId) { updateRequirement(selectedRequirementId, { description: content }); } else if (selectedUserStoryId) { } } }); _react.useEffect.call(void 0, () => { if (!editor || !document) return; let content = ""; let title = ""; if (selectedSectionId) { const section = document.sections.find((s) => s.id === selectedSectionId); if (section) { content = section.content; title = section.title; } } else if (selectedRequirementId) { const requirement = document.requirements.find((r) => r.id === selectedRequirementId); if (requirement) { content = requirement.description; title = requirement.title; } } else if (selectedUserStoryId) { const story = document.userStories.find((s) => s.id === selectedUserStoryId); if (story) { content = ` <h3>${story.title}</h3> <p><strong>As a</strong> ${story.asA}</p> <p><strong>I want</strong> ${story.iWant}</p> <p><strong>So that</strong> ${story.soThat}</p> <h4>Acceptance Criteria</h4> <ul> ${story.acceptanceCriteria.map((c) => `<li>${c}</li>`).join("")} </ul> `; title = story.title; } } editor.commands.setContent(content); }, [editor, document, selectedSectionId, selectedRequirementId, selectedUserStoryId]); if (!document) return null; const getSelectedTitle = () => { if (selectedSectionId) { const section = document.sections.find((s) => s.id === selectedSectionId); return _optionalChain([section, 'optionalAccess', _9 => _9.title]) || ""; } if (selectedRequirementId) { const requirement = document.requirements.find((r) => r.id === selectedRequirementId); return _optionalChain([requirement, 'optionalAccess', _10 => _10.title]) || ""; } if (selectedUserStoryId) { const story = document.userStories.find((s) => s.id === selectedUserStoryId); return _optionalChain([story, 'optionalAccess', _11 => _11.title]) || ""; } return ""; }; return /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "prd-editor-content h-full flex flex-col bg-white dark:bg-gray-800", children: [ (selectedSectionId || selectedRequirementId || selectedUserStoryId) && /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "border-b px-6 py-4", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "h2", { className: "text-xl font-semibold text-gray-900 dark:text-gray-100", children: getSelectedTitle() }) }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "flex-1 overflow-y-auto", children: editor ? /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "prose prose-lg dark:prose-invert max-w-none p-6", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, _react3.EditorContent, { editor }) }) : /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "p-6 text-center text-gray-500", children: "Select a section, requirement, or user story to edit" }) }), editor && /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "div", { className: "border-t px-6 py-2 flex items-center gap-2", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => editor.chain().focus().toggleBold().run(), className: `p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 ${editor.isActive("bold") ? "bg-gray-200 dark:bg-gray-700" : ""}`, children: /* @__PURE__ */ _jsxruntime.jsxs.call(void 0, "svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: [ /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 4h8a4 4 0 014 4 4 4 0 01-4 4H6z" }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 12h9a4 4 0 014 4 4 4 0 01-4 4H6z" }) ] }) } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => editor.chain().focus().toggleItalic().run(), className: `p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 ${editor.isActive("italic") ? "bg-gray-200 dark:bg-gray-700" : ""}`, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M10 4h4m0 16h-4m4-16l-4 16" }) }) } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => editor.chain().focus().toggleHeading({ level: 2 }).run(), className: `p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 ${editor.isActive("heading", { level: 2 }) ? "bg-gray-200 dark:bg-gray-700" : ""}`, children: "H2" } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => editor.chain().focus().toggleHeading({ level: 3 }).run(), className: `p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 ${editor.isActive("heading", { level: 3 }) ? "bg-gray-200 dark:bg-gray-700" : ""}`, children: "H3" } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "w-px h-6 bg-gray-300 dark:bg-gray-600 mx-1" }), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => editor.chain().focus().toggleBulletList().run(), className: `p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 ${editor.isActive("bulletList") ? "bg-gray-200 dark:bg-gray-700" : ""}`, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M8 6h13M8 12h13M8 18h13M3 6h.01M3 12h.01M3 18h.01" }) }) } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => editor.chain().focus().toggleOrderedList().run(), className: `p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 ${editor.isActive("orderedList") ? "bg-gray-200 dark:bg-gray-700" : ""}`, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M7 6h13M7 12h13m-13 6h13M4 6h.01M4 12h.01M4 18h.01" }) }) } ), /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "button", { onClick: () => editor.chain().focus().toggleTaskList().run(), className: `p-2 rounded hover:bg-gray-100 dark:hover:bg-gray-700 ${editor.isActive("taskList") ? "bg-gray-200 dark:bg-gray-700" : ""}`, children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "svg", { className: "w-4 h-4", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" }) }) } ) ] }) ] }); }; // src/components/EditorPreview.tsx var _markdownit = require('markdown-it'); var _markdownit2 = _interopRequireDefault(_markdownit); var md = new (0, _markdownit2.default)({ html: true, linkify: true, typographer: true }); var EditorPreview = () => { const { document } = usePrdStore(); if (!document) return null; const markdown = exportToMarkdown(document); const html = md.render(markdown); return /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "prd-preview h-full overflow-y-auto bg-white dark:bg-gray-800", children: /* @__PURE__ */ _jsxruntime.jsx.call(void 0, "div", { className: "max-w-4xl mx-auto p-8", children: /* @__PU