@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
JavaScript
"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