summaryrefslogtreecommitdiff
path: root/frontend/src
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
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')
-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
-rw-r--r--frontend/src/pages/templates.tsx255
-rw-r--r--frontend/src/stores/index.ts9
-rw-r--r--frontend/src/styles/pc98.css663
-rw-r--r--frontend/src/types.ts20
7 files changed, 1199 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>
+ )
+}
diff --git a/frontend/src/pages/templates.tsx b/frontend/src/pages/templates.tsx
new file mode 100644
index 0000000..c847b79
--- /dev/null
+++ b/frontend/src/pages/templates.tsx
@@ -0,0 +1,255 @@
+import React, { useState } from 'react'
+import { ContractTemplate } from '../types'
+import { TemplateEditor } from '../components/templates/TemplateEditor'
+
+// Default built-in templates
+const DEFAULT_TEMPLATES: ContractTemplate[] = [
+ {
+ id: 'simple',
+ name: 'Simple',
+ description: 'A simple contract with plan and execute phases.',
+ isBuiltIn: true,
+ phases: [
+ {
+ id: 'plan',
+ name: 'Plan',
+ deliverables: [{ id: 'plan-deliverable', name: 'Plan' }]
+ },
+ {
+ id: 'execute',
+ name: 'Execute',
+ deliverables: [{ id: 'pr-deliverable', name: 'PR' }]
+ }
+ ]
+ },
+ {
+ id: 'specification',
+ name: 'Specification',
+ description: 'A comprehensive contract with research, specification, planning, execution, and review phases.',
+ isBuiltIn: true,
+ phases: [
+ {
+ id: 'research',
+ name: 'Research',
+ deliverables: [{ id: 'research-notes', name: 'Research Notes' }]
+ },
+ {
+ id: 'specify',
+ name: 'Specify',
+ deliverables: [{ id: 'requirements', name: 'Requirements' }]
+ },
+ {
+ id: 'plan',
+ name: 'Plan',
+ deliverables: [{ id: 'plan-deliverable', name: 'Plan' }]
+ },
+ {
+ id: 'execute',
+ name: 'Execute',
+ deliverables: [{ id: 'pr-deliverable', name: 'PR' }]
+ },
+ {
+ id: 'review',
+ name: 'Review',
+ deliverables: [{ id: 'release-notes', name: 'Release Notes' }]
+ }
+ ]
+ },
+ {
+ id: 'execute',
+ name: 'Execute',
+ description: 'A minimal contract with only an execute phase and no deliverables.',
+ isBuiltIn: true,
+ phases: [
+ {
+ id: 'execute',
+ name: 'Execute',
+ deliverables: []
+ }
+ ]
+ }
+]
+
+type Props = {
+ onClose: () => void
+}
+
+export const TemplatesPage: React.FC<Props> = ({ onClose }) => {
+ const [templates, setTemplates] = useState<ContractTemplate[]>(() => {
+ const saved = localStorage.getItem('contractTemplates')
+ if (saved) {
+ try {
+ return JSON.parse(saved)
+ } catch {
+ return DEFAULT_TEMPLATES
+ }
+ }
+ return DEFAULT_TEMPLATES
+ })
+
+ const [editingTemplate, setEditingTemplate] = useState<ContractTemplate | null>(null)
+ const [showNewTemplateForm, setShowNewTemplateForm] = useState(false)
+ const [newTemplateName, setNewTemplateName] = useState('')
+ const [newTemplateDescription, setNewTemplateDescription] = useState('')
+
+ const saveTemplates = (newTemplates: ContractTemplate[]) => {
+ setTemplates(newTemplates)
+ localStorage.setItem('contractTemplates', JSON.stringify(newTemplates))
+ }
+
+ const handleSaveTemplate = (updatedTemplate: ContractTemplate) => {
+ const newTemplates = templates.map(t =>
+ t.id === updatedTemplate.id ? updatedTemplate : t
+ )
+ saveTemplates(newTemplates)
+ setEditingTemplate(null)
+ }
+
+ const handleCreateTemplate = () => {
+ if (!newTemplateName.trim()) return
+
+ const newTemplate: ContractTemplate = {
+ id: `custom-${Date.now()}`,
+ name: newTemplateName.trim(),
+ description: newTemplateDescription.trim() || 'Custom contract template',
+ isBuiltIn: false,
+ phases: [
+ {
+ id: `phase-${Date.now()}`,
+ name: 'Execute',
+ deliverables: []
+ }
+ ]
+ }
+
+ saveTemplates([...templates, newTemplate])
+ setNewTemplateName('')
+ setNewTemplateDescription('')
+ setShowNewTemplateForm(false)
+ }
+
+ const handleDeleteTemplate = (templateId: string) => {
+ const template = templates.find(t => t.id === templateId)
+ if (template?.isBuiltIn) return
+
+ if (window.confirm(`Are you sure you want to delete "${template?.name}"?`)) {
+ saveTemplates(templates.filter(t => t.id !== templateId))
+ }
+ }
+
+ const handleResetToDefaults = () => {
+ if (window.confirm('Reset all templates to defaults? This will remove any custom templates.')) {
+ saveTemplates(DEFAULT_TEMPLATES)
+ }
+ }
+
+ if (editingTemplate) {
+ return (
+ <div className="templates-page">
+ <TemplateEditor
+ template={editingTemplate}
+ onSave={handleSaveTemplate}
+ onCancel={() => setEditingTemplate(null)}
+ />
+ </div>
+ )
+ }
+
+ return (
+ <div className="templates-page">
+ <div className="templates-header">
+ <div className="templates-header-left">
+ <h1 className="templates-title">Contract Templates</h1>
+ <p className="templates-subtitle">Manage contract types and their phase deliverables</p>
+ </div>
+ <div className="templates-header-right">
+ <button className="templates-btn secondary" onClick={handleResetToDefaults}>
+ Reset to Defaults
+ </button>
+ <button className="templates-btn primary" onClick={() => setShowNewTemplateForm(true)}>
+ + New Template
+ </button>
+ <button className="templates-close-btn" onClick={onClose}>
+ x
+ </button>
+ </div>
+ </div>
+
+ {showNewTemplateForm && (
+ <div className="new-template-form">
+ <div className="form-row">
+ <input
+ type="text"
+ className="form-input"
+ placeholder="Template name..."
+ value={newTemplateName}
+ onChange={(e) => setNewTemplateName(e.target.value)}
+ />
+ <input
+ type="text"
+ className="form-input"
+ placeholder="Description (optional)..."
+ value={newTemplateDescription}
+ onChange={(e) => setNewTemplateDescription(e.target.value)}
+ />
+ <button className="templates-btn primary" onClick={handleCreateTemplate}>
+ Create
+ </button>
+ <button className="templates-btn secondary" onClick={() => setShowNewTemplateForm(false)}>
+ Cancel
+ </button>
+ </div>
+ </div>
+ )}
+
+ <div className="templates-grid">
+ {templates.map(template => (
+ <div key={template.id} className={`template-card ${template.isBuiltIn ? 'built-in' : 'custom'}`}>
+ <div className="template-card-header">
+ <h3 className="template-card-title">{template.name}</h3>
+ {template.isBuiltIn && <span className="template-badge">Built-in</span>}
+ {!template.isBuiltIn && <span className="template-badge custom">Custom</span>}
+ </div>
+ <p className="template-card-description">{template.description}</p>
+
+ <div className="template-phases">
+ {template.phases.map((phase, index) => (
+ <div key={phase.id} className="template-phase">
+ <div className="phase-indicator">
+ <span className="phase-dot"></span>
+ {index < template.phases.length - 1 && <span className="phase-line"></span>}
+ </div>
+ <div className="phase-content">
+ <span className="phase-name">{phase.name}</span>
+ <span className="phase-deliverables">
+ {phase.deliverables.length === 0
+ ? '(no deliverables)'
+ : phase.deliverables.map(d => d.name).join(', ')}
+ </span>
+ </div>
+ </div>
+ ))}
+ </div>
+
+ <div className="template-card-actions">
+ <button
+ className="template-action-btn edit"
+ onClick={() => setEditingTemplate(template)}
+ >
+ Edit
+ </button>
+ {!template.isBuiltIn && (
+ <button
+ className="template-action-btn delete"
+ onClick={() => handleDeleteTemplate(template.id)}
+ >
+ Delete
+ </button>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+ )
+}
diff --git a/frontend/src/stores/index.ts b/frontend/src/stores/index.ts
index 58f461c..2fc356d 100644
--- a/frontend/src/stores/index.ts
+++ b/frontend/src/stores/index.ts
@@ -4,6 +4,10 @@ import { ChatMessage, Choice } from '../types'
// Authentication state
export const isLoggedInStore = atom<boolean>(false)
+// Navigation state
+export type NavPage = 'main' | 'templates'
+export const currentPageStore = atom<NavPage>('main')
+
// VN Interface state
export const isStandbyStore = atom<boolean>(false)
export const currentTimeStore = atom<Date>(new Date())
@@ -91,4 +95,9 @@ export const setLoadingComplete = (complete: boolean) => {
if (complete) {
loadingStore.set(false)
}
+}
+
+// Navigation actions
+export const navigateTo = (page: NavPage) => {
+ currentPageStore.set(page)
} \ No newline at end of file
diff --git a/frontend/src/styles/pc98.css b/frontend/src/styles/pc98.css
index 4dcf15e..f4747f5 100644
--- a/frontend/src/styles/pc98.css
+++ b/frontend/src/styles/pc98.css
@@ -4621,3 +4621,666 @@ button:focus-visible {
display: none;
}
}
+
+/* ======================================
+ Navigation Strip
+ ====================================== */
+.nav-strip {
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 40px;
+ background: rgba(0, 0, 0, 0.9);
+ border-bottom: 1px solid rgba(102, 204, 255, 0.3);
+ display: flex;
+ align-items: center;
+ padding: 0 16px;
+ gap: 8px;
+ z-index: 1000;
+ backdrop-filter: blur(10px);
+}
+
+.nav-strip-item {
+ appearance: none;
+ background: transparent;
+ border: 1px solid transparent;
+ color: rgba(255, 255, 255, 0.6);
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ font-weight: bold;
+ padding: 6px 16px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ gap: 6px;
+ transition: all 0.2s ease;
+ letter-spacing: 0.5px;
+}
+
+.nav-strip-item:hover {
+ color: rgba(255, 255, 255, 0.9);
+ border-color: rgba(102, 204, 255, 0.3);
+ background: rgba(102, 204, 255, 0.05);
+}
+
+.nav-strip-item.active {
+ color: #66ccff;
+ border-color: rgba(102, 204, 255, 0.5);
+ background: rgba(102, 204, 255, 0.1);
+}
+
+.nav-icon {
+ font-size: 10px;
+ opacity: 0.7;
+}
+
+.nav-label {
+ text-transform: uppercase;
+}
+
+/* Adjust VN interface content for nav strip */
+.vn-interface {
+ padding-top: 40px;
+}
+
+.floating-info-panel {
+ top: 56px;
+}
+
+/* ======================================
+ Templates Page
+ ====================================== */
+.templates-page {
+ position: fixed;
+ top: 40px;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 20, 0.95);
+ overflow-y: auto;
+ padding: 24px;
+ z-index: 100;
+}
+
+.templates-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: flex-start;
+ margin-bottom: 32px;
+ padding-bottom: 16px;
+ border-bottom: 1px solid rgba(102, 204, 255, 0.2);
+}
+
+.templates-header-left {
+ flex: 1;
+}
+
+.templates-header-right {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.templates-title {
+ font-family: 'MS Gothic', monospace;
+ font-size: 24px;
+ font-weight: bold;
+ color: #ffffff;
+ margin: 0 0 8px 0;
+ letter-spacing: 1px;
+}
+
+.templates-subtitle {
+ font-family: 'MS Gothic', monospace;
+ font-size: 13px;
+ color: rgba(255, 255, 255, 0.5);
+ margin: 0;
+}
+
+.templates-btn {
+ appearance: none;
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ font-weight: bold;
+ padding: 8px 16px;
+ cursor: pointer;
+ border: 1px solid rgba(102, 204, 255, 0.5);
+ background: rgba(102, 204, 255, 0.1);
+ color: #66ccff;
+ transition: all 0.2s ease;
+ letter-spacing: 0.5px;
+}
+
+.templates-btn:hover {
+ background: rgba(102, 204, 255, 0.2);
+ border-color: #66ccff;
+}
+
+.templates-btn.primary {
+ background: rgba(102, 204, 255, 0.2);
+ border-color: #66ccff;
+}
+
+.templates-btn.primary:hover {
+ background: rgba(102, 204, 255, 0.3);
+}
+
+.templates-btn.secondary {
+ background: rgba(255, 255, 255, 0.05);
+ border-color: rgba(255, 255, 255, 0.2);
+ color: rgba(255, 255, 255, 0.7);
+}
+
+.templates-btn.secondary:hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: rgba(255, 255, 255, 0.4);
+ color: #ffffff;
+}
+
+.templates-close-btn {
+ appearance: none;
+ background: rgba(255, 255, 255, 0.05);
+ border: 1px solid rgba(255, 255, 255, 0.2);
+ color: rgba(255, 255, 255, 0.7);
+ width: 32px;
+ height: 32px;
+ font-size: 18px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+}
+
+.templates-close-btn:hover {
+ background: rgba(255, 100, 100, 0.1);
+ border-color: rgba(255, 100, 100, 0.5);
+ color: #ff6464;
+}
+
+/* New Template Form */
+.new-template-form {
+ background: rgba(0, 0, 0, 0.5);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ padding: 16px;
+ margin-bottom: 24px;
+}
+
+.form-row {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+}
+
+.form-input {
+ flex: 1;
+ appearance: none;
+ background: rgba(0, 0, 0, 0.5);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 13px;
+ padding: 8px 12px;
+}
+
+.form-input:focus {
+ outline: none;
+ border-color: #66ccff;
+ background: rgba(102, 204, 255, 0.05);
+}
+
+.form-input::placeholder {
+ color: rgba(255, 255, 255, 0.3);
+}
+
+/* Templates Grid */
+.templates-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
+ gap: 24px;
+}
+
+.template-card {
+ background: rgba(0, 0, 0, 0.5);
+ border: 1px solid rgba(102, 204, 255, 0.2);
+ padding: 20px;
+ transition: all 0.2s ease;
+}
+
+.template-card:hover {
+ border-color: rgba(102, 204, 255, 0.4);
+ background: rgba(0, 0, 0, 0.6);
+}
+
+.template-card.custom {
+ border-color: rgba(255, 204, 102, 0.3);
+}
+
+.template-card.custom:hover {
+ border-color: rgba(255, 204, 102, 0.5);
+}
+
+.template-card-header {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.template-card-title {
+ font-family: 'MS Gothic', monospace;
+ font-size: 16px;
+ font-weight: bold;
+ color: #ffffff;
+ margin: 0;
+ letter-spacing: 0.5px;
+}
+
+.template-badge {
+ font-family: 'MS Gothic', monospace;
+ font-size: 10px;
+ font-weight: bold;
+ padding: 3px 8px;
+ background: rgba(102, 204, 255, 0.15);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ color: #66ccff;
+ letter-spacing: 0.5px;
+}
+
+.template-badge.custom {
+ background: rgba(255, 204, 102, 0.15);
+ border-color: rgba(255, 204, 102, 0.3);
+ color: #ffcc66;
+}
+
+.template-card-description {
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.5);
+ margin: 0 0 16px 0;
+ line-height: 1.5;
+}
+
+/* Template Phases */
+.template-phases {
+ margin-bottom: 16px;
+}
+
+.template-phase {
+ display: flex;
+ gap: 12px;
+ padding: 8px 0;
+}
+
+.phase-indicator {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ width: 12px;
+}
+
+.phase-dot {
+ width: 8px;
+ height: 8px;
+ background: #66ccff;
+ border-radius: 50%;
+ flex-shrink: 0;
+}
+
+.phase-line {
+ width: 2px;
+ flex: 1;
+ background: rgba(102, 204, 255, 0.3);
+ margin-top: 4px;
+}
+
+.phase-content {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ gap: 2px;
+}
+
+.phase-name {
+ font-family: 'MS Gothic', monospace;
+ font-size: 13px;
+ font-weight: bold;
+ color: #ffffff;
+}
+
+.phase-deliverables {
+ font-family: 'MS Gothic', monospace;
+ font-size: 11px;
+ color: rgba(255, 255, 255, 0.4);
+}
+
+/* Template Card Actions */
+.template-card-actions {
+ display: flex;
+ gap: 8px;
+ padding-top: 12px;
+ border-top: 1px solid rgba(102, 204, 255, 0.1);
+}
+
+.template-action-btn {
+ appearance: none;
+ font-family: 'MS Gothic', monospace;
+ font-size: 11px;
+ font-weight: bold;
+ padding: 6px 12px;
+ cursor: pointer;
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ background: transparent;
+ color: #66ccff;
+ transition: all 0.2s ease;
+}
+
+.template-action-btn:hover {
+ background: rgba(102, 204, 255, 0.1);
+ border-color: #66ccff;
+}
+
+.template-action-btn.delete {
+ border-color: rgba(255, 100, 100, 0.3);
+ color: #ff6464;
+}
+
+.template-action-btn.delete:hover {
+ background: rgba(255, 100, 100, 0.1);
+ border-color: #ff6464;
+}
+
+/* ======================================
+ Template Editor
+ ====================================== */
+.template-editor {
+ background: rgba(0, 0, 0, 0.5);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ max-width: 800px;
+ margin: 0 auto;
+}
+
+.template-editor-header {
+ padding: 20px;
+ border-bottom: 1px solid rgba(102, 204, 255, 0.2);
+}
+
+.template-editor-title {
+ font-family: 'MS Gothic', monospace;
+ font-size: 18px;
+ font-weight: bold;
+ color: #ffffff;
+ margin: 0 0 8px 0;
+}
+
+.template-editor-description {
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.5);
+ margin: 0;
+}
+
+.template-editor-content {
+ padding: 20px;
+ max-height: 60vh;
+ overflow-y: auto;
+}
+
+.phases-list {
+ display: flex;
+ flex-direction: column;
+ gap: 16px;
+}
+
+.phase-card {
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(102, 204, 255, 0.2);
+ padding: 16px;
+}
+
+.phase-header {
+ display: flex;
+ align-items: center;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.phase-number {
+ width: 24px;
+ height: 24px;
+ background: rgba(102, 204, 255, 0.2);
+ border: 1px solid rgba(102, 204, 255, 0.4);
+ color: #66ccff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ font-weight: bold;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.phase-name-input {
+ flex: 1;
+ appearance: none;
+ background: rgba(0, 0, 0, 0.5);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 14px;
+ font-weight: bold;
+ padding: 8px 12px;
+}
+
+.phase-name-input:focus {
+ outline: none;
+ border-color: #66ccff;
+}
+
+.phase-remove-btn {
+ appearance: none;
+ background: transparent;
+ border: 1px solid rgba(255, 100, 100, 0.3);
+ color: #ff6464;
+ width: 24px;
+ height: 24px;
+ font-size: 14px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+}
+
+.phase-remove-btn:hover {
+ background: rgba(255, 100, 100, 0.1);
+ border-color: #ff6464;
+}
+
+.deliverables-list {
+ display: flex;
+ flex-direction: column;
+ gap: 8px;
+ padding-left: 36px;
+}
+
+.no-deliverables {
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ color: rgba(255, 255, 255, 0.3);
+ font-style: italic;
+ padding: 8px 0;
+}
+
+.deliverable-item {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+}
+
+.deliverable-name-input {
+ flex: 1;
+ appearance: none;
+ background: rgba(0, 0, 0, 0.3);
+ border: 1px solid rgba(255, 255, 255, 0.15);
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ padding: 6px 10px;
+}
+
+.deliverable-name-input:focus {
+ outline: none;
+ border-color: rgba(102, 204, 255, 0.5);
+}
+
+.deliverable-remove-btn {
+ appearance: none;
+ background: transparent;
+ border: 1px solid rgba(255, 100, 100, 0.2);
+ color: rgba(255, 100, 100, 0.6);
+ width: 20px;
+ height: 20px;
+ font-size: 12px;
+ cursor: pointer;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ transition: all 0.2s ease;
+}
+
+.deliverable-remove-btn:hover {
+ background: rgba(255, 100, 100, 0.1);
+ border-color: #ff6464;
+ color: #ff6464;
+}
+
+.add-deliverable-row {
+ display: flex;
+ gap: 8px;
+ margin-top: 4px;
+}
+
+.add-deliverable-input {
+ flex: 1;
+ appearance: none;
+ background: rgba(0, 0, 0, 0.2);
+ border: 1px dashed rgba(102, 204, 255, 0.2);
+ color: #ffffff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 12px;
+ padding: 6px 10px;
+}
+
+.add-deliverable-input:focus {
+ outline: none;
+ border-style: solid;
+ border-color: rgba(102, 204, 255, 0.4);
+}
+
+.add-deliverable-input::placeholder {
+ color: rgba(255, 255, 255, 0.25);
+}
+
+.add-deliverable-btn {
+ appearance: none;
+ background: rgba(102, 204, 255, 0.1);
+ border: 1px solid rgba(102, 204, 255, 0.3);
+ color: #66ccff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 11px;
+ font-weight: bold;
+ padding: 6px 12px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.add-deliverable-btn:hover {
+ background: rgba(102, 204, 255, 0.2);
+ border-color: #66ccff;
+}
+
+.add-phase-btn {
+ appearance: none;
+ width: 100%;
+ background: rgba(102, 204, 255, 0.05);
+ border: 1px dashed rgba(102, 204, 255, 0.3);
+ color: #66ccff;
+ font-family: 'MS Gothic', monospace;
+ font-size: 13px;
+ font-weight: bold;
+ padding: 12px;
+ margin-top: 16px;
+ cursor: pointer;
+ transition: all 0.2s ease;
+}
+
+.add-phase-btn:hover {
+ background: rgba(102, 204, 255, 0.1);
+ border-style: solid;
+}
+
+.template-editor-footer {
+ padding: 16px 20px;
+ border-top: 1px solid rgba(102, 204, 255, 0.2);
+ display: flex;
+ gap: 12px;
+ justify-content: flex-end;
+}
+
+.template-editor-footer .modal-btn.primary {
+ background: rgba(102, 204, 255, 0.2);
+ border-color: #66ccff;
+ color: #66ccff;
+}
+
+.template-editor-footer .modal-btn.primary:hover {
+ background: rgba(102, 204, 255, 0.3);
+}
+
+/* Background overlay for templates page */
+.background-overlay {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: linear-gradient(180deg, rgba(0, 0, 30, 0.95) 0%, rgba(0, 0, 50, 0.9) 100%);
+}
+
+/* Mobile responsive adjustments for templates */
+@media (max-width: 768px) {
+ .templates-page {
+ padding: 16px;
+ }
+
+ .templates-header {
+ flex-direction: column;
+ gap: 16px;
+ }
+
+ .templates-header-right {
+ width: 100%;
+ flex-wrap: wrap;
+ }
+
+ .templates-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .form-row {
+ flex-direction: column;
+ }
+
+ .form-input {
+ width: 100%;
+ }
+
+ .template-editor-content {
+ max-height: 50vh;
+ }
+
+ .deliverables-list {
+ padding-left: 0;
+ }
+}
diff --git a/frontend/src/types.ts b/frontend/src/types.ts
index c6d1263..6bbec96 100644
--- a/frontend/src/types.ts
+++ b/frontend/src/types.ts
@@ -9,3 +9,23 @@ export type Choice = {
id: string
label: string
}
+
+// Contract Template types
+export type Deliverable = {
+ id: string
+ name: string
+}
+
+export type Phase = {
+ id: string
+ name: string
+ deliverables: Deliverable[]
+}
+
+export type ContractTemplate = {
+ id: string
+ name: string
+ description: string
+ phases: Phase[]
+ isBuiltIn: boolean
+}