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 ++++++ frontend/src/pages/templates.tsx | 255 ++++++++ frontend/src/stores/index.ts | 9 + frontend/src/styles/pc98.css | 663 +++++++++++++++++++++ frontend/src/types.ts | 20 + 7 files changed, 1199 insertions(+), 3 deletions(-) create mode 100644 frontend/src/components/NavStrip.tsx create mode 100644 frontend/src/components/templates/TemplateEditor.tsx create mode 100644 frontend/src/pages/templates.tsx (limited to 'frontend/src') 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 && ( + + )} +
+ +
+ + +
+
+ ) +} 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 = ({ onClose }) => { + const [templates, setTemplates] = useState(() => { + const saved = localStorage.getItem('contractTemplates') + if (saved) { + try { + return JSON.parse(saved) + } catch { + return DEFAULT_TEMPLATES + } + } + return DEFAULT_TEMPLATES + }) + + const [editingTemplate, setEditingTemplate] = useState(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 ( +
+ setEditingTemplate(null)} + /> +
+ ) + } + + return ( +
+
+
+

Contract Templates

+

Manage contract types and their phase deliverables

+
+
+ + + +
+
+ + {showNewTemplateForm && ( +
+
+ setNewTemplateName(e.target.value)} + /> + setNewTemplateDescription(e.target.value)} + /> + + +
+
+ )} + +
+ {templates.map(template => ( +
+
+

{template.name}

+ {template.isBuiltIn && Built-in} + {!template.isBuiltIn && Custom} +
+

{template.description}

+ +
+ {template.phases.map((phase, index) => ( +
+
+ + {index < template.phases.length - 1 && } +
+
+ {phase.name} + + {phase.deliverables.length === 0 + ? '(no deliverables)' + : phase.deliverables.map(d => d.name).join(', ')} + +
+
+ ))} +
+ +
+ + {!template.isBuiltIn && ( + + )} +
+
+ ))} +
+
+ ) +} 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(false) +// Navigation state +export type NavPage = 'main' | 'templates' +export const currentPageStore = atom('main') + // VN Interface state export const isStandbyStore = atom(false) export const currentTimeStore = atom(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 +} -- cgit v1.2.3