summaryrefslogtreecommitdiff
path: root/makima/frontend/src
diff options
context:
space:
mode:
Diffstat (limited to 'makima/frontend/src')
-rw-r--r--makima/frontend/src/components/templates/TemplateEditor.tsx31
-rw-r--r--makima/frontend/src/lib/api.ts120
-rw-r--r--makima/frontend/src/routes/contracts.tsx46
-rw-r--r--makima/frontend/src/routes/templates.tsx248
4 files changed, 328 insertions, 117 deletions
diff --git a/makima/frontend/src/components/templates/TemplateEditor.tsx b/makima/frontend/src/components/templates/TemplateEditor.tsx
index 03382f3..c8e1f98 100644
--- a/makima/frontend/src/components/templates/TemplateEditor.tsx
+++ b/makima/frontend/src/components/templates/TemplateEditor.tsx
@@ -5,9 +5,10 @@ interface Props {
template: ContractTemplate;
onSave: (template: ContractTemplate) => void;
onCancel: () => void;
+ readOnly?: boolean;
}
-export function TemplateEditor({ template, onSave, onCancel }: Props) {
+export function TemplateEditor({ template, onSave, onCancel, readOnly = false }: Props) {
const [editedTemplate, setEditedTemplate] = useState<ContractTemplate>({
...template,
phases: template.phases.map((p) => ({
@@ -106,11 +107,16 @@ export function TemplateEditor({ template, onSave, onCancel }: Props) {
{/* 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}
+ {readOnly ? "View" : "Edit"} Template: {template.name}
</h2>
<p className="text-xs font-mono text-[#75aafc] opacity-70">
{template.description}
</p>
+ {readOnly && (
+ <p className="text-xs font-mono text-amber-400 mt-2">
+ Built-in templates are read-only
+ </p>
+ )}
</div>
{/* Phases */}
@@ -127,10 +133,11 @@ export function TemplateEditor({ template, onSave, onCancel }: Props) {
</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]"
+ 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] disabled:opacity-60"
value={phase.name}
onChange={(e) => handlePhaseNameChange(phase.id, e.target.value)}
placeholder="Phase name"
+ disabled={readOnly}
/>
{!template.isBuiltIn && (
<button
@@ -233,15 +240,17 @@ export function TemplateEditor({ template, onSave, onCancel }: Props) {
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
+ {readOnly ? "Close" : "Cancel"}
</button>
+ {!readOnly && (
+ <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/lib/api.ts b/makima/frontend/src/lib/api.ts
index 8838dbd..e8b3d8a 100644
--- a/makima/frontend/src/lib/api.ts
+++ b/makima/frontend/src/lib/api.ts
@@ -1645,6 +1645,56 @@ export interface ListContractTypesResponse {
contractTypes: ContractTypeTemplate[];
}
+/** Phase definition for custom templates */
+export interface PhaseDefinition {
+ id: string;
+ name: string;
+ order: number;
+}
+
+/** Deliverable definition for custom templates */
+export interface DeliverableDefinition {
+ id: string;
+ name: string;
+ priority: "required" | "recommended" | "optional";
+}
+
+/** Request to create a custom contract type template */
+export interface CreateTemplateRequest {
+ name: string;
+ description?: string;
+ phases: PhaseDefinition[];
+ defaultPhase: string;
+ deliverables?: Record<string, DeliverableDefinition[]>;
+}
+
+/** Request to update a custom contract type template */
+export interface UpdateTemplateRequest {
+ name?: string;
+ description?: string;
+ phases?: PhaseDefinition[];
+ defaultPhase?: string;
+ deliverables?: Record<string, DeliverableDefinition[]>;
+ version?: number;
+}
+
+/** Custom template record from the API */
+export interface ContractTypeTemplateRecord {
+ id: string;
+ name: string;
+ description: string | null;
+ phases: PhaseDefinition[];
+ defaultPhase: string;
+ isBuiltin: boolean;
+ version: number;
+ createdAt: string;
+}
+
+/** Response for single template operations */
+export interface TemplateResponse {
+ template: ContractTypeTemplateRecord;
+}
+
/**
* List available contract types/templates.
* Returns built-in types (simple, specification) and any custom types.
@@ -1657,6 +1707,66 @@ export async function listContractTypes(): Promise<ListContractTypesResponse> {
return res.json();
}
+/**
+ * Create a new custom contract type template.
+ */
+export async function createContractTemplate(
+ req: CreateTemplateRequest
+): Promise<TemplateResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/contract-types`, {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(req),
+ });
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({ message: res.statusText }));
+ throw new Error(err.message || `Failed to create template: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/**
+ * Get a custom contract type template by ID.
+ */
+export async function getContractTemplate(id: string): Promise<TemplateResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/contract-types/${id}`);
+ if (!res.ok) {
+ throw new Error(`Failed to get template: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/**
+ * Update a custom contract type template.
+ */
+export async function updateContractTemplate(
+ id: string,
+ req: UpdateTemplateRequest
+): Promise<TemplateResponse> {
+ const res = await authFetch(`${API_BASE}/api/v1/contract-types/${id}`, {
+ method: "PUT",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify(req),
+ });
+ if (!res.ok) {
+ const err = await res.json().catch(() => ({ message: res.statusText }));
+ throw new Error(err.message || `Failed to update template: ${res.statusText}`);
+ }
+ return res.json();
+}
+
+/**
+ * Delete a custom contract type template.
+ */
+export async function deleteContractTemplate(id: string): Promise<void> {
+ const res = await authFetch(`${API_BASE}/api/v1/contract-types/${id}`, {
+ method: "DELETE",
+ });
+ if (!res.ok) {
+ throw new Error(`Failed to delete template: ${res.statusText}`);
+ }
+}
+
export interface ContractRepository {
id: string;
contractId: string;
@@ -1741,10 +1851,12 @@ export interface ContractListResponse {
export interface CreateContractRequest {
name: string;
description?: string;
- /** Contract type: "simple" (default) or "specification" */
- contractType?: ContractType;
- /** Initial phase to start in (defaults based on contract type) */
- initialPhase?: ContractPhase;
+ /** Contract type: "simple" (default), "specification", "execute", or custom template name */
+ contractType?: ContractType | string;
+ /** UUID of a custom template to use. If provided, takes precedence over contractType. */
+ templateId?: string;
+ /** Initial phase to start in (defaults based on contract type or template) */
+ initialPhase?: ContractPhase | string;
/** When true, tasks won't auto-push or create PRs - use patch files instead */
localOnly?: boolean;
/** When true, spawn a red team task to monitor work output */
diff --git a/makima/frontend/src/routes/contracts.tsx b/makima/frontend/src/routes/contracts.tsx
index bb66215..fc76f60 100644
--- a/makima/frontend/src/routes/contracts.tsx
+++ b/makima/frontend/src/routes/contracts.tsx
@@ -96,49 +96,19 @@ function ContractsPageContent() {
const [redTeamEnabled, setRedTeamEnabled] = useState(false);
const [redTeamPrompt, setRedTeamPrompt] = useState("");
- // Fetch contract types when modal opens - merges built-in types with user templates
+ // Fetch contract types when modal opens - API returns both built-in and custom templates
useEffect(() => {
if (isCreating) {
setContractTypesLoading(true);
- // Load user templates from localStorage
- const loadUserTemplates = (): ContractTypeTemplate[] => {
- try {
- const saved = localStorage.getItem("makima_contract_templates");
- if (saved) {
- const templates = JSON.parse(saved);
- // Convert user templates to ContractTypeTemplate format, excluding built-ins
- return templates
- .filter((t: { isBuiltIn?: boolean }) => !t.isBuiltIn)
- .map((t: { id: string; name: string; description: string; phases: { id: string; name: string }[] }) => ({
- id: t.id,
- name: t.name,
- description: t.description,
- phases: t.phases.map((p: { id: string }) => p.id) as ContractPhase[],
- phaseNames: Object.fromEntries(t.phases.map((p: { id: string; name: string }) => [p.id, p.name])),
- defaultPhase: (t.phases[0]?.id || "execute") as ContractPhase,
- isBuiltin: false,
- }));
- }
- } catch {
- // Ignore localStorage errors
- }
- return [];
- };
-
listContractTypes()
.then((res) => {
- // Merge built-in types from API with user templates from localStorage
- const userTemplates = loadUserTemplates();
- // Filter out any user templates that have the same ID as built-in types
- const builtinIds = new Set(res.contractTypes.map(t => t.id));
- const uniqueUserTemplates = userTemplates.filter(t => !builtinIds.has(t.id));
- setContractTypes([...res.contractTypes, ...uniqueUserTemplates]);
+ setContractTypes(res.contractTypes);
setContractTypesLoading(false);
})
.catch((err) => {
console.error("Failed to fetch contract types:", err);
- // Fall back to built-in types + user templates
+ // Fall back to built-in types
const builtinTypes: ContractTypeTemplate[] = [
{
id: "simple",
@@ -165,10 +135,7 @@ function ContractsPageContent() {
isBuiltin: true,
},
];
- const userTemplates = loadUserTemplates();
- const builtinIds = new Set(builtinTypes.map(t => t.id));
- const uniqueUserTemplates = userTemplates.filter(t => !builtinIds.has(t.id));
- setContractTypes([...builtinTypes, ...uniqueUserTemplates]);
+ setContractTypes(builtinTypes);
setContractTypesLoading(false);
});
}
@@ -261,11 +228,14 @@ function ContractsPageContent() {
// Get default phase from contract types or fall back to static function
const selectedType = contractTypes.find((t) => t.id === contractType);
const defaultPhaseForType = selectedType?.defaultPhase || (contractType === "simple" ? "plan" : "research");
+ const isCustomTemplate = selectedType && !selectedType.isBuiltin;
const data: CreateContractRequest = {
name: newContractName.trim(),
description: newContractDescription.trim() || undefined,
- contractType: contractType,
+ // For custom templates, send templateId instead of contractType
+ contractType: isCustomTemplate ? undefined : contractType,
+ templateId: isCustomTemplate ? contractType : undefined,
initialPhase: initialPhase !== defaultPhaseForType ? initialPhase : undefined,
localOnly: localOnly || undefined,
redTeamEnabled: redTeamEnabled || undefined,
diff --git a/makima/frontend/src/routes/templates.tsx b/makima/frontend/src/routes/templates.tsx
index 15bf95c..b2c9974 100644
--- a/makima/frontend/src/routes/templates.tsx
+++ b/makima/frontend/src/routes/templates.tsx
@@ -1,28 +1,27 @@
-import { useState, useEffect } from "react";
+import { useState, useEffect, useCallback } 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";
+import {
+ listContractTypes,
+ createContractTemplate,
+ updateContractTemplate,
+ deleteContractTemplate,
+ type PhaseDefinition,
+ type DeliverableDefinition,
+} from "../lib/api";
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 [templates, setTemplates] = useState<ContractTemplate[]>(DEFAULT_TEMPLATES);
+ const [loading, setLoading] = useState(true);
+ const [error, setError] = useState<string | null>(null);
+ const [saving, setSaving] = useState(false);
const [editingTemplate, setEditingTemplate] = useState<ContractTemplate | null>(
null
@@ -38,63 +37,161 @@ export default function TemplatesPage() {
}
}, [authLoading, isAuthConfigured, isAuthenticated, navigate]);
- const saveTemplates = (newTemplates: ContractTemplate[]) => {
- setTemplates(newTemplates);
- localStorage.setItem(STORAGE_KEY, JSON.stringify(newTemplates));
- };
+ // Fetch templates from API
+ const fetchTemplates = useCallback(async () => {
+ try {
+ setLoading(true);
+ setError(null);
+ const response = await listContractTypes();
- const handleSaveTemplate = (updatedTemplate: ContractTemplate) => {
- const newTemplates = templates.map((t) =>
- t.id === updatedTemplate.id ? updatedTemplate : t
- );
- saveTemplates(newTemplates);
- setEditingTemplate(null);
+ // Convert API response to ContractTemplate format
+ const apiTemplates: ContractTemplate[] = response.contractTypes.map((t) => ({
+ id: t.id,
+ name: t.name,
+ description: t.description,
+ isBuiltIn: t.isBuiltin,
+ phases: t.phases.map((phaseId) => ({
+ id: phaseId,
+ name: t.phaseNames?.[phaseId] || phaseId.charAt(0).toUpperCase() + phaseId.slice(1),
+ deliverables: [], // Deliverables are managed server-side
+ })),
+ }));
+
+ // Merge with DEFAULT_TEMPLATES to ensure we have full phase/deliverable info for built-ins
+ const mergedTemplates = apiTemplates.map((apiTemplate) => {
+ const defaultTemplate = DEFAULT_TEMPLATES.find((d) => d.id === apiTemplate.id);
+ if (defaultTemplate && apiTemplate.isBuiltIn) {
+ return defaultTemplate; // Use the richer default template for built-ins
+ }
+ return apiTemplate;
+ });
+
+ setTemplates(mergedTemplates);
+ } catch (err) {
+ console.error("Failed to fetch templates:", err);
+ setError(err instanceof Error ? err.message : "Failed to fetch templates");
+ // Fall back to default templates
+ setTemplates(DEFAULT_TEMPLATES);
+ } finally {
+ setLoading(false);
+ }
+ }, []);
+
+ useEffect(() => {
+ if (!authLoading && isAuthenticated) {
+ fetchTemplates();
+ } else if (!authLoading && !isAuthConfigured) {
+ // No auth configured, just show defaults
+ setTemplates(DEFAULT_TEMPLATES);
+ setLoading(false);
+ }
+ }, [authLoading, isAuthenticated, isAuthConfigured, fetchTemplates]);
+
+ const handleSaveTemplate = async (updatedTemplate: ContractTemplate) => {
+ if (updatedTemplate.isBuiltIn) {
+ // Built-in templates are read-only, just close the editor
+ setEditingTemplate(null);
+ return;
+ }
+
+ try {
+ setSaving(true);
+ setError(null);
+
+ // Convert to API format
+ const phases: PhaseDefinition[] = updatedTemplate.phases.map((p, index) => ({
+ id: p.id,
+ name: p.name,
+ order: index,
+ }));
+
+ const deliverables: Record<string, DeliverableDefinition[]> = {};
+ for (const phase of updatedTemplate.phases) {
+ if (phase.deliverables.length > 0) {
+ deliverables[phase.id] = phase.deliverables.map((d) => ({
+ id: d.id,
+ name: d.name,
+ priority: "required" as const,
+ }));
+ }
+ }
+
+ await updateContractTemplate(updatedTemplate.id, {
+ name: updatedTemplate.name,
+ description: updatedTemplate.description,
+ phases,
+ defaultPhase: phases[0]?.id || "execute",
+ deliverables: Object.keys(deliverables).length > 0 ? deliverables : undefined,
+ });
+
+ // Refresh templates from server
+ await fetchTemplates();
+ setEditingTemplate(null);
+ } catch (err) {
+ console.error("Failed to update template:", err);
+ setError(err instanceof Error ? err.message : "Failed to update template");
+ } finally {
+ setSaving(false);
+ }
};
- const handleCreateTemplate = () => {
+ const handleCreateTemplate = async () => {
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);
+ try {
+ setSaving(true);
+ setError(null);
+
+ const phases: PhaseDefinition[] = [
+ { id: "execute", name: "Execute", order: 0 },
+ ];
+
+ await createContractTemplate({
+ name: newTemplateName.trim(),
+ description: newTemplateDescription.trim() || "Custom contract template",
+ phases,
+ defaultPhase: "execute",
+ });
+
+ // Refresh templates from server
+ await fetchTemplates();
+
+ setNewTemplateName("");
+ setNewTemplateDescription("");
+ setShowNewTemplateForm(false);
+ } catch (err) {
+ console.error("Failed to create template:", err);
+ setError(err instanceof Error ? err.message : "Failed to create template");
+ } finally {
+ setSaving(false);
+ }
};
- const handleDeleteTemplate = (templateId: string) => {
+ const handleDeleteTemplate = async (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));
+ try {
+ setSaving(true);
+ setError(null);
+ await deleteContractTemplate(templateId);
+ await fetchTemplates();
+ } catch (err) {
+ console.error("Failed to delete template:", err);
+ setError(err instanceof Error ? err.message : "Failed to delete template");
+ } finally {
+ setSaving(false);
+ }
}
};
- const handleResetToDefaults = () => {
- if (
- window.confirm(
- "Reset all templates to defaults? This will remove any custom templates."
- )
- ) {
- saveTemplates(DEFAULT_TEMPLATES);
- }
+ const handleRefresh = () => {
+ fetchTemplates();
};
// Show loading state
- if (authLoading) {
+ if (authLoading || loading) {
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">
@@ -114,6 +211,7 @@ export default function TemplatesPage() {
template={editingTemplate}
onSave={handleSaveTemplate}
onCancel={() => setEditingTemplate(null)}
+ readOnly={editingTemplate.isBuiltIn}
/>
</main>
</div>
@@ -124,6 +222,20 @@ export default function TemplatesPage() {
<div className="relative z-10 min-h-screen bg-[#0a1628]">
<Masthead />
<main className="max-w-6xl mx-auto px-4 py-6">
+ {/* Error display */}
+ {error && (
+ <div className="mb-4 p-3 bg-red-400/10 border border-red-400/30 text-red-400 font-mono text-xs">
+ {error}
+ <button
+ type="button"
+ onClick={() => setError(null)}
+ className="ml-2 text-red-400/70 hover:text-red-400"
+ >
+ Dismiss
+ </button>
+ </div>
+ )}
+
{/* Header */}
<div className="flex justify-between items-start mb-6 pb-4 border-b border-[rgba(117,170,252,0.15)]">
<div>
@@ -137,15 +249,17 @@ export default function TemplatesPage() {
<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"
+ onClick={handleRefresh}
+ disabled={saving}
+ 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 disabled:opacity-50"
>
- Reset to Defaults
+ Refresh
</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"
+ disabled={saving}
+ 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 disabled:opacity-50"
>
+ New Template
</button>
@@ -162,6 +276,7 @@ export default function TemplatesPage() {
placeholder="Template name..."
value={newTemplateName}
onChange={(e) => setNewTemplateName(e.target.value)}
+ disabled={saving}
/>
<input
type="text"
@@ -169,18 +284,21 @@ export default function TemplatesPage() {
placeholder="Description (optional)..."
value={newTemplateDescription}
onChange={(e) => setNewTemplateDescription(e.target.value)}
+ disabled={saving}
/>
<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"
+ disabled={saving || !newTemplateName.trim()}
+ 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 disabled:opacity-50"
>
- Create
+ {saving ? "Creating..." : "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"
+ disabled={saving}
+ 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 disabled:opacity-50"
>
Cancel
</button>
@@ -245,15 +363,17 @@ export default function TemplatesPage() {
<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"
+ disabled={saving}
+ 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 disabled:opacity-50"
>
- Edit
+ {template.isBuiltIn ? "View" : "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"
+ disabled={saving}
+ 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 disabled:opacity-50"
>
Delete
</button>