From 595548db950eca303a7d73ca09f31895d291534f Mon Sep 17 00:00:00 2001 From: soryu Date: Sat, 24 Jan 2026 15:50:43 +0000 Subject: feat(frontend): Add Templates page for managing contract templates - Add NavStrip component for top navigation between pages - Create Templates page with template card grid layout - Create TemplateEditor component for editing phase deliverables - Add navigation state management in stores - Implement three built-in templates: - Simple: Plan phase (Plan deliverable), Execute phase (PR deliverable) - Specification: Research, Specify, Plan, Execute, Review phases with deliverables - Execute: Single execute phase with no deliverables - Support for creating custom templates with configurable phases/deliverables - Templates are persisted to localStorage - Add comprehensive CSS styling matching existing design patterns Co-Authored-By: Claude Opus 4.5 [WIP] Heartbeat checkpoint - 2026-01-24 16:13:00 UTC [WIP] Heartbeat checkpoint - 2026-01-24 16:14:29 UTC --- frontend/src/components/NavStrip.tsx | 30 ++++ frontend/src/components/VNInterface.tsx | 40 ++++- .../src/components/templates/TemplateEditor.tsx | 185 +++++++++++++++++++++ 3 files changed, 252 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/NavStrip.tsx create mode 100644 frontend/src/components/templates/TemplateEditor.tsx (limited to 'frontend/src/components') diff --git a/frontend/src/components/NavStrip.tsx b/frontend/src/components/NavStrip.tsx new file mode 100644 index 0000000..c0fc000 --- /dev/null +++ b/frontend/src/components/NavStrip.tsx @@ -0,0 +1,30 @@ +import React from 'react' + +type NavItem = { + id: string + label: string + icon?: string +} + +type Props = { + items: NavItem[] + activeId: string + onSelect: (id: string) => void +} + +export const NavStrip: React.FC = ({ items, activeId, onSelect }) => { + return ( + + ) +} diff --git a/frontend/src/components/VNInterface.tsx b/frontend/src/components/VNInterface.tsx index be71d27..bdc7db0 100644 --- a/frontend/src/components/VNInterface.tsx +++ b/frontend/src/components/VNInterface.tsx @@ -8,15 +8,24 @@ import { showSettingsModalStore, isVisibleStore, yenBalanceStore, + currentPageStore, toggleStandby, toggleShowChoices, - updateTime + updateTime, + navigateTo } from '../stores' +import { NavStrip } from './NavStrip' +import { TemplatesPage } from '../pages/templates' interface VNInterfaceProps { onLogout: () => void } +const NAV_ITEMS = [ + { id: 'main', label: 'Main', icon: '>' }, + { id: 'templates', label: 'Templates', icon: '>' } +] + export function VNInterface({ onLogout }: VNInterfaceProps) { const isStandby = useStore(isStandbyStore) const currentTime = useStore(currentTimeStore) @@ -25,6 +34,7 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { const showSettingsModal = useStore(showSettingsModalStore) const isVisible = useStore(isVisibleStore) const yenBalance = useStore(yenBalanceStore) + const currentPage = useStore(currentPageStore) // Fade in effect on mount useEffect(() => { @@ -45,12 +55,36 @@ export function VNInterface({ onLogout }: VNInterfaceProps) { return () => clearInterval(timer) }, []) + // Show Templates page if on templates route + if (currentPage === 'templates') { + return ( +
+
+
+
+ navigateTo(id as 'main' | 'templates')} + /> + navigateTo('main')} /> +
+ ) + } + return (
+ {/* Navigation Strip */} + navigateTo(id as 'main' | 'templates')} + /> + {/* Background */}
- Background image diff --git a/frontend/src/components/templates/TemplateEditor.tsx b/frontend/src/components/templates/TemplateEditor.tsx new file mode 100644 index 0000000..612cac0 --- /dev/null +++ b/frontend/src/components/templates/TemplateEditor.tsx @@ -0,0 +1,185 @@ +import React, { useState } from 'react' +import { ContractTemplate, Phase, Deliverable } from '../../types' + +type Props = { + template: ContractTemplate + onSave: (template: ContractTemplate) => void + onCancel: () => void +} + +export const TemplateEditor: React.FC = ({ template, onSave, onCancel }) => { + const [editedTemplate, setEditedTemplate] = useState({ ...template, phases: template.phases.map(p => ({ ...p, deliverables: [...p.deliverables] })) }) + const [newDeliverableName, setNewDeliverableName] = useState<{ [phaseId: string]: string }>({}) + + const handlePhaseNameChange = (phaseId: string, newName: string) => { + setEditedTemplate(prev => ({ + ...prev, + phases: prev.phases.map(p => + p.id === phaseId ? { ...p, name: newName } : p + ) + })) + } + + const handleDeliverableNameChange = (phaseId: string, deliverableId: string, newName: string) => { + setEditedTemplate(prev => ({ + ...prev, + phases: prev.phases.map(p => + p.id === phaseId + ? { + ...p, + deliverables: p.deliverables.map(d => + d.id === deliverableId ? { ...d, name: newName } : d + ) + } + : p + ) + })) + } + + const handleAddDeliverable = (phaseId: string) => { + const name = newDeliverableName[phaseId]?.trim() + if (!name) return + + const newDeliverable: Deliverable = { + id: `deliverable-${Date.now()}`, + name + } + + setEditedTemplate(prev => ({ + ...prev, + phases: prev.phases.map(p => + p.id === phaseId + ? { ...p, deliverables: [...p.deliverables, newDeliverable] } + : p + ) + })) + setNewDeliverableName(prev => ({ ...prev, [phaseId]: '' })) + } + + const handleRemoveDeliverable = (phaseId: string, deliverableId: string) => { + setEditedTemplate(prev => ({ + ...prev, + phases: prev.phases.map(p => + p.id === phaseId + ? { ...p, deliverables: p.deliverables.filter(d => d.id !== deliverableId) } + : p + ) + })) + } + + const handleAddPhase = () => { + const newPhase: Phase = { + id: `phase-${Date.now()}`, + name: 'New Phase', + deliverables: [] + } + setEditedTemplate(prev => ({ + ...prev, + phases: [...prev.phases, newPhase] + })) + } + + const handleRemovePhase = (phaseId: string) => { + setEditedTemplate(prev => ({ + ...prev, + phases: prev.phases.filter(p => p.id !== phaseId) + })) + } + + return ( +
+
+

Edit Template: {template.name}

+

{template.description}

+
+ +
+
+ {editedTemplate.phases.map((phase, phaseIndex) => ( +
+
+ {phaseIndex + 1} + handlePhaseNameChange(phase.id, e.target.value)} + placeholder="Phase name" + /> + {!template.isBuiltIn && ( + + )} +
+ +
+ {phase.deliverables.length === 0 ? ( +
No deliverables
+ ) : ( + phase.deliverables.map(deliverable => ( +
+ handleDeliverableNameChange(phase.id, deliverable.id, e.target.value)} + /> + +
+ )) + )} + +
+ setNewDeliverableName(prev => ({ ...prev, [phase.id]: e.target.value }))} + onKeyPress={(e) => { + if (e.key === 'Enter') { + handleAddDeliverable(phase.id) + } + }} + /> + +
+
+
+ ))} +
+ + {!template.isBuiltIn && ( + + )} +
+ +
+ + +
+
+ ) +} -- cgit v1.2.3