diff options
| author | soryu <soryu@soryu.co> | 2026-01-24 15:50:43 +0000 |
|---|---|---|
| committer | soryu <soryu@soryu.co> | 2026-01-24 16:17:59 +0000 |
| commit | 595548db950eca303a7d73ca09f31895d291534f (patch) | |
| tree | ad6317979526e6a683fa6987bc9979ec3ac055f3 /frontend/src | |
| parent | abc5fbed331ea527ccaac0cd4120c4a0650f8bc0 (diff) | |
| download | soryu-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.tsx | 30 | ||||
| -rw-r--r-- | frontend/src/components/VNInterface.tsx | 40 | ||||
| -rw-r--r-- | frontend/src/components/templates/TemplateEditor.tsx | 185 | ||||
| -rw-r--r-- | frontend/src/pages/templates.tsx | 255 | ||||
| -rw-r--r-- | frontend/src/stores/index.ts | 9 | ||||
| -rw-r--r-- | frontend/src/styles/pc98.css | 663 | ||||
| -rw-r--r-- | frontend/src/types.ts | 20 |
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 +} |
