summaryrefslogtreecommitdiff
path: root/frontend/src/components
diff options
context:
space:
mode:
authorsoryu <soryu@soryu.co>2026-01-24 15:50:43 +0000
committersoryu <soryu@soryu.co>2026-01-24 16:17:59 +0000
commit595548db950eca303a7d73ca09f31895d291534f (patch)
treead6317979526e6a683fa6987bc9979ec3ac055f3 /frontend/src/components
parentabc5fbed331ea527ccaac0cd4120c4a0650f8bc0 (diff)
downloadsoryu-595548db950eca303a7d73ca09f31895d291534f.tar.gz
soryu-595548db950eca303a7d73ca09f31895d291534f.zip
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 <noreply@anthropic.com> [WIP] Heartbeat checkpoint - 2026-01-24 16:13:00 UTC [WIP] Heartbeat checkpoint - 2026-01-24 16:14:29 UTC
Diffstat (limited to 'frontend/src/components')
-rw-r--r--frontend/src/components/NavStrip.tsx30
-rw-r--r--frontend/src/components/VNInterface.tsx40
-rw-r--r--frontend/src/components/templates/TemplateEditor.tsx185
3 files changed, 252 insertions, 3 deletions
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<Props> = ({ items, activeId, onSelect }) => {
+ return (
+ <nav className="nav-strip">
+ {items.map((item) => (
+ <button
+ key={item.id}
+ className={`nav-strip-item ${activeId === item.id ? 'active' : ''}`}
+ onClick={() => onSelect(item.id)}
+ >
+ {item.icon && <span className="nav-icon">{item.icon}</span>}
+ <span className="nav-label">{item.label}</span>
+ </button>
+ ))}
+ </nav>
+ )
+}
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 (
+ <div className={`vn-interface ${isVisible ? 'fade-in' : 'fade-out'}`}>
+ <div className="vn-background">
+ <div className="background-overlay"></div>
+ </div>
+ <NavStrip
+ items={NAV_ITEMS}
+ activeId={currentPage}
+ onSelect={(id) => navigateTo(id as 'main' | 'templates')}
+ />
+ <TemplatesPage onClose={() => navigateTo('main')} />
+ </div>
+ )
+ }
+
return (
<div className={`vn-interface ${isVisible ? 'fade-in' : 'fade-out'}`}>
+ {/* Navigation Strip */}
+ <NavStrip
+ items={NAV_ITEMS}
+ activeId={currentPage}
+ onSelect={(id) => navigateTo(id as 'main' | 'templates')}
+ />
+
{/* Background */}
<div className="vn-background">
- <img
- src="/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png"
+ <img
+ src="/__gaogao__56242cbde8f18ac64522e410bad04e68_waifu2x_art_noise2.png"
alt="Background image"
className="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<Props> = ({ template, onSave, onCancel }) => {
+ const [editedTemplate, setEditedTemplate] = useState<ContractTemplate>({ ...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 (
+ <div className="template-editor">
+ <div className="template-editor-header">
+ <h2 className="template-editor-title">Edit Template: {template.name}</h2>
+ <p className="template-editor-description">{template.description}</p>
+ </div>
+
+ <div className="template-editor-content">
+ <div className="phases-list">
+ {editedTemplate.phases.map((phase, phaseIndex) => (
+ <div key={phase.id} className="phase-card">
+ <div className="phase-header">
+ <span className="phase-number">{phaseIndex + 1}</span>
+ <input
+ type="text"
+ className="phase-name-input"
+ value={phase.name}
+ onChange={(e) => handlePhaseNameChange(phase.id, e.target.value)}
+ placeholder="Phase name"
+ />
+ {!template.isBuiltIn && (
+ <button
+ className="phase-remove-btn"
+ onClick={() => handleRemovePhase(phase.id)}
+ title="Remove phase"
+ >
+ x
+ </button>
+ )}
+ </div>
+
+ <div className="deliverables-list">
+ {phase.deliverables.length === 0 ? (
+ <div className="no-deliverables">No deliverables</div>
+ ) : (
+ phase.deliverables.map(deliverable => (
+ <div key={deliverable.id} className="deliverable-item">
+ <input
+ type="text"
+ className="deliverable-name-input"
+ value={deliverable.name}
+ onChange={(e) => handleDeliverableNameChange(phase.id, deliverable.id, e.target.value)}
+ />
+ <button
+ className="deliverable-remove-btn"
+ onClick={() => handleRemoveDeliverable(phase.id, deliverable.id)}
+ title="Remove deliverable"
+ >
+ x
+ </button>
+ </div>
+ ))
+ )}
+
+ <div className="add-deliverable-row">
+ <input
+ type="text"
+ className="add-deliverable-input"
+ placeholder="New deliverable name..."
+ value={newDeliverableName[phase.id] || ''}
+ onChange={(e) => setNewDeliverableName(prev => ({ ...prev, [phase.id]: e.target.value }))}
+ onKeyPress={(e) => {
+ if (e.key === 'Enter') {
+ handleAddDeliverable(phase.id)
+ }
+ }}
+ />
+ <button
+ className="add-deliverable-btn"
+ onClick={() => handleAddDeliverable(phase.id)}
+ >
+ + Add
+ </button>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+
+ {!template.isBuiltIn && (
+ <button className="add-phase-btn" onClick={handleAddPhase}>
+ + Add Phase
+ </button>
+ )}
+ </div>
+
+ <div className="template-editor-footer">
+ <button className="modal-btn secondary" onClick={onCancel}>
+ Cancel
+ </button>
+ <button className="modal-btn primary" onClick={() => onSave(editedTemplate)}>
+ Save Changes
+ </button>
+ </div>
+ </div>
+ )
+}