summaryrefslogtreecommitdiff
path: root/makima/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend')
-rw-r--r--makima/frontend/src/components/NavStrip.tsx1
-rw-r--r--makima/frontend/src/components/templates/TemplateEditor.tsx248
-rw-r--r--makima/frontend/src/index.css2
-rw-r--r--makima/frontend/src/main.tsx9
-rw-r--r--makima/frontend/src/routes/templates.tsx268
-rw-r--r--makima/frontend/src/types/templates.ts89
6 files changed, 616 insertions, 1 deletions
diff --git a/makima/frontend/src/components/NavStrip.tsx b/makima/frontend/src/components/NavStrip.tsx
index fb95c7f..f44799b 100644
--- a/makima/frontend/src/components/NavStrip.tsx
+++ b/makima/frontend/src/components/NavStrip.tsx
@@ -14,6 +14,7 @@ const NAV_LINKS: NavLink[] = [
{ label: "Board", href: "/workflow", requiresAuth: true },
{ label: "Mesh", href: "/mesh", requiresAuth: true },
{ label: "History", href: "/history", requiresAuth: true },
+ { label: "Templates", href: "/templates", requiresAuth: true },
];
export function NavStrip() {
diff --git a/makima/frontend/src/components/templates/TemplateEditor.tsx b/makima/frontend/src/components/templates/TemplateEditor.tsx
new file mode 100644
index 0000000..03382f3
--- /dev/null
+++ b/makima/frontend/src/components/templates/TemplateEditor.tsx
@@ -0,0 +1,248 @@
+import { useState } from "react";
+import type { ContractTemplate, Phase, Deliverable } from "../../types/templates";
+
+interface Props {
+ template: ContractTemplate;
+ onSave: (template: ContractTemplate) => void;
+ onCancel: () => void;
+}
+
+export function TemplateEditor({ template, onSave, onCancel }: Props) {
+ 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="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-6">
+ {/* Header */}
+ <div className="mb-6 pb-4 border-b border-[rgba(117,170,252,0.15)]">
+ <h2 className="text-sm font-mono uppercase tracking-wide text-[#9bc3ff] mb-1">
+ Edit Template: {template.name}
+ </h2>
+ <p className="text-xs font-mono text-[#75aafc] opacity-70">
+ {template.description}
+ </p>
+ </div>
+
+ {/* Phases */}
+ <div className="space-y-4 mb-6">
+ {editedTemplate.phases.map((phase, phaseIndex) => (
+ <div
+ key={phase.id}
+ className="bg-[rgba(0,0,0,0.3)] border border-[rgba(117,170,252,0.2)] p-4"
+ >
+ {/* Phase Header */}
+ <div className="flex items-center gap-3 mb-3">
+ <span className="w-6 h-6 flex items-center justify-center bg-[rgba(117,170,252,0.2)] text-[#9bc3ff] text-xs font-mono">
+ {phaseIndex + 1}
+ </span>
+ <input
+ type="text"
+ className="flex-1 px-3 py-1.5 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]"
+ value={phase.name}
+ onChange={(e) => handlePhaseNameChange(phase.id, e.target.value)}
+ placeholder="Phase name"
+ />
+ {!template.isBuiltIn && (
+ <button
+ type="button"
+ onClick={() => handleRemovePhase(phase.id)}
+ className="w-7 h-7 flex items-center justify-center border border-[rgba(255,100,100,0.3)] text-[#ff6464] hover:bg-[rgba(255,100,100,0.1)] transition-colors text-sm"
+ title="Remove phase"
+ >
+ x
+ </button>
+ )}
+ </div>
+
+ {/* Deliverables */}
+ <div className="ml-9 space-y-2">
+ {phase.deliverables.length === 0 ? (
+ <div className="text-xs font-mono text-[#556677] italic">
+ No deliverables
+ </div>
+ ) : (
+ phase.deliverables.map((deliverable) => (
+ <div
+ key={deliverable.id}
+ className="flex items-center gap-2"
+ >
+ <span className="text-[#75aafc] text-xs">-</span>
+ <input
+ type="text"
+ className="flex-1 px-2 py-1 bg-transparent border border-[rgba(117,170,252,0.15)] text-[#dbe7ff] font-mono text-xs focus:outline-none focus:border-[#3f6fb3]"
+ value={deliverable.name}
+ onChange={(e) =>
+ handleDeliverableNameChange(
+ phase.id,
+ deliverable.id,
+ e.target.value
+ )
+ }
+ />
+ <button
+ type="button"
+ onClick={() =>
+ handleRemoveDeliverable(phase.id, deliverable.id)
+ }
+ className="w-5 h-5 flex items-center justify-center text-[#ff6464] hover:bg-[rgba(255,100,100,0.1)] transition-colors text-xs"
+ title="Remove deliverable"
+ >
+ x
+ </button>
+ </div>
+ ))
+ )}
+
+ {/* Add Deliverable */}
+ <div className="flex items-center gap-2 pt-2">
+ <input
+ type="text"
+ className="flex-1 px-2 py-1 bg-transparent border border-[rgba(117,170,252,0.15)] text-[#dbe7ff] font-mono text-xs placeholder-[#445566] focus:outline-none focus:border-[#3f6fb3]"
+ 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
+ type="button"
+ onClick={() => handleAddDeliverable(phase.id)}
+ className="px-2 py-1 border border-[rgba(117,170,252,0.3)] text-[#9bc3ff] font-mono text-xs hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.1)] transition-colors"
+ >
+ + Add
+ </button>
+ </div>
+ </div>
+ </div>
+ ))}
+ </div>
+
+ {/* Add Phase (only for custom templates) */}
+ {!template.isBuiltIn && (
+ <button
+ type="button"
+ onClick={handleAddPhase}
+ className="w-full mb-6 px-4 py-2 border border-dashed border-[rgba(117,170,252,0.3)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors"
+ >
+ + Add Phase
+ </button>
+ )}
+
+ {/* Footer Actions */}
+ <div className="flex gap-3 justify-end pt-4 border-t border-[rgba(117,170,252,0.15)]">
+ <button
+ type="button"
+ onClick={onCancel}
+ className="px-4 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors"
+ >
+ Cancel
+ </button>
+ <button
+ type="button"
+ onClick={() => onSave(editedTemplate)}
+ className="px-4 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors"
+ >
+ Save Changes
+ </button>
+ </div>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/index.css b/makima/frontend/src/index.css
index 722c8a3..5c08006 100644
--- a/makima/frontend/src/index.css
+++ b/makima/frontend/src/index.css
@@ -69,7 +69,7 @@ body {
position: fixed;
inset: 0;
pointer-events: none;
- z-index: 1;
+ z-index: 0;
}
.grid-overlay::before {
diff --git a/makima/frontend/src/main.tsx b/makima/frontend/src/main.tsx
index 9a6e65e..383b732 100644
--- a/makima/frontend/src/main.tsx
+++ b/makima/frontend/src/main.tsx
@@ -18,6 +18,7 @@ import HistoryPage from "./routes/history";
import LoginPage from "./routes/login";
import SettingsPage from "./routes/settings";
import ContractFilePage from "./routes/contract-file";
+import TemplatesPage from "./routes/templates";
createRoot(document.getElementById("root")!).render(
<StrictMode>
@@ -126,6 +127,14 @@ createRoot(document.getElementById("root")!).render(
</ProtectedRoute>
}
/>
+ <Route
+ path="/templates"
+ element={
+ <ProtectedRoute>
+ <TemplatesPage />
+ </ProtectedRoute>
+ }
+ />
</Routes>
</BrowserRouter>
</SupervisorQuestionsProvider>
diff --git a/makima/frontend/src/routes/templates.tsx b/makima/frontend/src/routes/templates.tsx
new file mode 100644
index 0000000..15bf95c
--- /dev/null
+++ b/makima/frontend/src/routes/templates.tsx
@@ -0,0 +1,268 @@
+import { useState, useEffect } from "react";
+import { useNavigate } from "react-router";
+import { Masthead } from "../components/Masthead";
+import { TemplateEditor } from "../components/templates/TemplateEditor";
+import { useAuth } from "../contexts/AuthContext";
+import type { ContractTemplate } from "../types/templates";
+import { DEFAULT_TEMPLATES } from "../types/templates";
+
+const STORAGE_KEY = "makima_contract_templates";
+
+export default function TemplatesPage() {
+ const navigate = useNavigate();
+ const { isAuthenticated, isAuthConfigured, isLoading: authLoading } = useAuth();
+
+ const [templates, setTemplates] = useState<ContractTemplate[]>(() => {
+ const saved = localStorage.getItem(STORAGE_KEY);
+ 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("");
+
+ // Redirect to login if not authenticated
+ useEffect(() => {
+ if (!authLoading && isAuthConfigured && !isAuthenticated) {
+ navigate("/login");
+ }
+ }, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
+
+ const saveTemplates = (newTemplates: ContractTemplate[]) => {
+ setTemplates(newTemplates);
+ localStorage.setItem(STORAGE_KEY, 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);
+ }
+ };
+
+ // Show loading state
+ if (authLoading) {
+ return (
+ <div className="relative z-10 min-h-screen flex items-center justify-center bg-[#0a1628]">
+ <div className="text-[#75aafc] font-mono text-sm animate-pulse">
+ Loading...
+ </div>
+ </div>
+ );
+ }
+
+ // Editor view
+ if (editingTemplate) {
+ return (
+ <div className="relative z-10 min-h-screen bg-[#0a1628]">
+ <Masthead />
+ <main className="max-w-4xl mx-auto px-4 py-6">
+ <TemplateEditor
+ template={editingTemplate}
+ onSave={handleSaveTemplate}
+ onCancel={() => setEditingTemplate(null)}
+ />
+ </main>
+ </div>
+ );
+ }
+
+ return (
+ <div className="relative z-10 min-h-screen bg-[#0a1628]">
+ <Masthead />
+ <main className="max-w-6xl mx-auto px-4 py-6">
+ {/* Header */}
+ <div className="flex justify-between items-start mb-6 pb-4 border-b border-[rgba(117,170,252,0.15)]">
+ <div>
+ <h1 className="text-lg font-mono uppercase tracking-wide text-[#9bc3ff] mb-1">
+ Contract Templates
+ </h1>
+ <p className="text-xs font-mono text-[#75aafc] opacity-70">
+ Manage contract types and their phase deliverables
+ </p>
+ </div>
+ <div className="flex gap-3">
+ <button
+ type="button"
+ onClick={handleResetToDefaults}
+ className="px-3 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors"
+ >
+ Reset to Defaults
+ </button>
+ <button
+ type="button"
+ onClick={() => setShowNewTemplateForm(true)}
+ className="px-3 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors"
+ >
+ + New Template
+ </button>
+ </div>
+ </div>
+
+ {/* New Template Form */}
+ {showNewTemplateForm && (
+ <div className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.35)] p-4 mb-6">
+ <div className="flex gap-3 items-center">
+ <input
+ type="text"
+ className="flex-1 px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]"
+ placeholder="Template name..."
+ value={newTemplateName}
+ onChange={(e) => setNewTemplateName(e.target.value)}
+ />
+ <input
+ type="text"
+ className="flex-1 px-3 py-2 bg-transparent border border-[rgba(117,170,252,0.25)] text-white font-mono text-sm placeholder-[#556677] focus:outline-none focus:border-[#3f6fb3]"
+ placeholder="Description (optional)..."
+ value={newTemplateDescription}
+ onChange={(e) => setNewTemplateDescription(e.target.value)}
+ />
+ <button
+ type="button"
+ onClick={handleCreateTemplate}
+ className="px-4 py-2 border border-[#3f6fb3] bg-[rgba(117,170,252,0.15)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:bg-[rgba(117,170,252,0.25)] transition-colors"
+ >
+ Create
+ </button>
+ <button
+ type="button"
+ onClick={() => setShowNewTemplateForm(false)}
+ className="px-4 py-2 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] transition-colors"
+ >
+ Cancel
+ </button>
+ </div>
+ </div>
+ )}
+
+ {/* Templates Grid */}
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
+ {templates.map((template) => (
+ <div
+ key={template.id}
+ className="bg-[#0d1b2d] border border-[rgba(117,170,252,0.25)] hover:border-[rgba(117,170,252,0.45)] transition-colors"
+ >
+ {/* Card Header */}
+ <div className="px-4 py-3 border-b border-[rgba(117,170,252,0.15)] flex items-center justify-between">
+ <h3 className="font-mono text-sm text-white">{template.name}</h3>
+ {template.isBuiltIn ? (
+ <span className="px-2 py-0.5 bg-[rgba(117,170,252,0.15)] text-[#75aafc] font-mono text-[10px] uppercase tracking-wide">
+ Built-in
+ </span>
+ ) : (
+ <span className="px-2 py-0.5 bg-[rgba(100,200,100,0.15)] text-[#7bc97b] font-mono text-[10px] uppercase tracking-wide">
+ Custom
+ </span>
+ )}
+ </div>
+
+ {/* Card Body */}
+ <div className="px-4 py-3">
+ <p className="text-xs font-mono text-[#75aafc] opacity-70 mb-4 min-h-[2.5rem]">
+ {template.description}
+ </p>
+
+ {/* Phases */}
+ <div className="space-y-2 mb-4">
+ {template.phases.map((phase, index) => (
+ <div key={phase.id} className="flex items-start gap-2">
+ <div className="flex flex-col items-center">
+ <span className="w-2 h-2 rounded-full bg-[#3f6fb3]" />
+ {index < template.phases.length - 1 && (
+ <span className="w-px h-4 bg-[rgba(117,170,252,0.25)]" />
+ )}
+ </div>
+ <div className="flex-1 min-w-0">
+ <span className="text-xs font-mono text-[#9bc3ff]">
+ {phase.name}
+ </span>
+ <span className="text-[10px] font-mono text-[#556677] ml-2">
+ {phase.deliverables.length === 0
+ ? "(no deliverables)"
+ : phase.deliverables.map((d) => d.name).join(", ")}
+ </span>
+ </div>
+ </div>
+ ))}
+ </div>
+ </div>
+
+ {/* Card Footer */}
+ <div className="px-4 py-3 border-t border-[rgba(117,170,252,0.15)] flex gap-2">
+ <button
+ type="button"
+ onClick={() => setEditingTemplate(template)}
+ className="flex-1 px-3 py-1.5 border border-[rgba(117,170,252,0.25)] text-[#9bc3ff] font-mono text-xs uppercase tracking-wide hover:border-[#3f6fb3] hover:bg-[rgba(117,170,252,0.05)] transition-colors"
+ >
+ Edit
+ </button>
+ {!template.isBuiltIn && (
+ <button
+ type="button"
+ onClick={() => handleDeleteTemplate(template.id)}
+ className="px-3 py-1.5 border border-[rgba(255,100,100,0.25)] text-[#ff6464] font-mono text-xs uppercase tracking-wide hover:border-[rgba(255,100,100,0.5)] hover:bg-[rgba(255,100,100,0.05)] transition-colors"
+ >
+ Delete
+ </button>
+ )}
+ </div>
+ </div>
+ ))}
+ </div>
+ </main>
+ </div>
+ );
+}
diff --git a/makima/frontend/src/types/templates.ts b/makima/frontend/src/types/templates.ts
new file mode 100644
index 0000000..77ba89e
--- /dev/null
+++ b/makima/frontend/src/types/templates.ts
@@ -0,0 +1,89 @@
+// Contract Template types
+export interface Deliverable {
+ id: string;
+ name: string;
+}
+
+export interface Phase {
+ id: string;
+ name: string;
+ deliverables: Deliverable[];
+}
+
+export interface ContractTemplate {
+ id: string;
+ name: string;
+ description: string;
+ phases: Phase[];
+ isBuiltIn: boolean;
+}
+
+// Default built-in templates
+export 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 Document" }],
+ },
+ {
+ 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: [],
+ },
+ ],
+ },
+];