feat(ui): add picker modal components for archetype and template selection
- ArchetypePicker: Full-screen modal with backdrop blur, 2-column grid, Select/Edit/Create actions, 800px width for readability - TemplatePicker: Same pattern with team size indicator and built-in badge - Both pickers support backdrop click-to-close and keyboard navigation
This commit is contained in:
parent
7b27f673fe
commit
ebd3ffcbbe
2 changed files with 284 additions and 0 deletions
131
src/components/swarm/archetype-picker.tsx
Normal file
131
src/components/swarm/archetype-picker.tsx
Normal file
|
|
@ -0,0 +1,131 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { X, Blocks, Check, Pencil, Plus } from 'lucide-react';
|
||||
import type { AgentArchetype } from '../../lib/types-swarm';
|
||||
import { getArchetypeDisplayChar } from '../../lib/utils';
|
||||
|
||||
interface ArchetypePickerProps {
|
||||
archetypes: AgentArchetype[];
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (archetype: AgentArchetype) => void;
|
||||
onEdit: (archetypeId: string) => void;
|
||||
onCreateNew: () => void;
|
||||
}
|
||||
|
||||
export function ArchetypePicker({
|
||||
archetypes,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSelect,
|
||||
onEdit,
|
||||
onCreateNew
|
||||
}: ArchetypePickerProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (archetype: AgentArchetype) => {
|
||||
onSelect(archetype);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="flex flex-col w-full max-w-[800px] max-h-[85vh] overflow-hidden rounded-xl border border-[var(--ui-border-soft)] bg-[#0f1824] shadow-2xl animate-in zoom-in-95 duration-200">
|
||||
<div className="flex items-center justify-between border-b border-[var(--ui-border-soft)] px-5 py-4 bg-[#14202e]">
|
||||
<div className="flex items-center gap-3">
|
||||
<Blocks className="w-5 h-5 text-[var(--ui-text-secondary)]" />
|
||||
<h2 className="text-lg font-bold text-[var(--ui-text-primary)]">
|
||||
Select Archetype
|
||||
</h2>
|
||||
</div>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-full p-2 text-[var(--ui-text-muted)] hover:bg-white/5 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto px-4 py-4">
|
||||
{archetypes.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-12 text-[var(--ui-text-muted)]">
|
||||
<Blocks className="w-12 h-12 mb-3 opacity-50" />
|
||||
<p className="text-sm">No archetypes available</p>
|
||||
<p className="text-xs mt-1">Create one to get started</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{archetypes.map((archetype) => {
|
||||
const displayChar = getArchetypeDisplayChar(archetype);
|
||||
return (
|
||||
<div
|
||||
key={archetype.id}
|
||||
className="group relative flex flex-col p-4 rounded-xl bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] hover:border-[var(--ui-border)] hover:bg-[#111f2b] transition-all duration-200"
|
||||
>
|
||||
<div className="flex items-start gap-3 mb-2">
|
||||
<div
|
||||
className="h-10 w-10 rounded-xl flex items-center justify-center text-lg font-bold border-2 flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: `${archetype.color}20`,
|
||||
color: archetype.color,
|
||||
borderColor: `${archetype.color}50`
|
||||
}}
|
||||
>
|
||||
{displayChar}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-[var(--ui-text-primary)] text-sm truncate">
|
||||
{archetype.name}
|
||||
</h3>
|
||||
<p className="text-xs text-[var(--ui-text-muted)] line-clamp-4 mt-0.5">
|
||||
{archetype.description || 'No description'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2 mt-auto pt-2 border-t border-[var(--ui-border-soft)]">
|
||||
<button
|
||||
onClick={() => handleSelect(archetype)}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 rounded-lg bg-blue-600 text-white text-xs font-medium hover:bg-blue-500 transition-colors"
|
||||
>
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
Select
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onEdit(archetype.id)}
|
||||
className="p-1.5 rounded-lg text-[var(--ui-text-muted)] hover:text-[var(--ui-text-primary)] hover:bg-white/5 transition-colors"
|
||||
title="Edit archetype"
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[var(--ui-border-soft)] px-5 py-4 bg-[#14202e]">
|
||||
<button
|
||||
onClick={onCreateNew}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 rounded-lg bg-[var(--ui-bg-soft)] border border-[var(--ui-border-soft)] text-[var(--ui-text-secondary)] hover:bg-[#111f2b] hover:text-[var(--ui-text-primary)] hover:border-[var(--ui-border)] transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Create New Archetype
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
153
src/components/swarm/template-picker.tsx
Normal file
153
src/components/swarm/template-picker.tsx
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
"use client";
|
||||
|
||||
import React from 'react';
|
||||
import { X, Check, Pencil, Plus, Users } from 'lucide-react';
|
||||
import type { SwarmTemplate } from '../../lib/types-swarm';
|
||||
import { getTemplateDisplayChar, getTemplateColor } from '../../lib/utils';
|
||||
|
||||
interface TemplatePickerProps {
|
||||
templates: SwarmTemplate[];
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSelect: (template: SwarmTemplate) => void;
|
||||
onEdit: (templateId: string) => void;
|
||||
onCreateNew: () => void;
|
||||
}
|
||||
|
||||
export function TemplatePicker({
|
||||
templates,
|
||||
isOpen,
|
||||
onClose,
|
||||
onSelect,
|
||||
onEdit,
|
||||
onCreateNew
|
||||
}: TemplatePickerProps) {
|
||||
if (!isOpen) return null;
|
||||
|
||||
const handleBackdropClick = (e: React.MouseEvent) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelect = (template: SwarmTemplate) => {
|
||||
onSelect(template);
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleEdit = (e: React.MouseEvent, templateId: string) => {
|
||||
e.stopPropagation();
|
||||
onEdit(templateId);
|
||||
};
|
||||
|
||||
const getTotalTeamSize = (template: SwarmTemplate): number => {
|
||||
return template.team.reduce((acc, member) => acc + member.count, 0);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 backdrop-blur-sm p-4 animate-in fade-in duration-200"
|
||||
onClick={handleBackdropClick}
|
||||
>
|
||||
<div className="flex flex-col w-full max-w-[800px] max-h-[85vh] overflow-hidden rounded-xl border border-[var(--ui-border-soft)] bg-[#0f1824] shadow-2xl animate-in zoom-in-95 duration-200">
|
||||
|
||||
<div className="flex items-center justify-between border-b border-[var(--ui-border-soft)] px-5 py-4 bg-[#14202e] flex-shrink-0">
|
||||
<h2 className="text-lg font-bold text-[var(--ui-text-primary)]">
|
||||
Select Template
|
||||
</h2>
|
||||
<button
|
||||
onClick={onClose}
|
||||
className="rounded-full p-2 text-[var(--ui-text-muted)] hover:bg-white/5 hover:text-white transition-colors"
|
||||
>
|
||||
<X className="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-y-auto p-4 custom-scrollbar">
|
||||
{templates.length === 0 ? (
|
||||
<div className="text-center py-8 text-[var(--ui-text-muted)]">
|
||||
<p className="text-sm">No templates available</p>
|
||||
<p className="text-xs mt-1">Create a template to get started</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
{templates.map((template) => {
|
||||
const displayChar = getTemplateDisplayChar(template);
|
||||
const color = getTemplateColor(template);
|
||||
const teamSize = getTotalTeamSize(template);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={template.id}
|
||||
className="group relative p-4 rounded-xl border border-[var(--ui-border-soft)] bg-[#111f2b] hover:bg-[#152836] hover:border-[var(--ui-border-hover)] transition-all cursor-pointer"
|
||||
onClick={() => handleSelect(template)}
|
||||
>
|
||||
<div className="flex items-start gap-3 mb-3">
|
||||
<div
|
||||
className="h-10 w-10 rounded-xl flex items-center justify-center text-lg font-bold border-2 flex-shrink-0"
|
||||
style={{
|
||||
backgroundColor: `${color}20`,
|
||||
color: color,
|
||||
borderColor: `${color}50`
|
||||
}}
|
||||
>
|
||||
{displayChar}
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<h3 className="font-semibold text-[var(--ui-text-primary)] text-sm truncate">
|
||||
{template.name}
|
||||
</h3>
|
||||
{template.isBuiltIn && (
|
||||
<span className="inline-block px-1.5 py-0.5 rounded-full bg-white/10 text-[9px] uppercase font-bold text-[var(--ui-text-muted)] border border-white/10 mt-1">
|
||||
Built-in
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-xs text-[var(--ui-text-muted)] mb-3 line-clamp-4">
|
||||
{template.description}
|
||||
</p>
|
||||
|
||||
<div className="flex items-center gap-2 text-xs text-[var(--ui-text-muted)] mb-3">
|
||||
<Users className="w-3.5 h-3.5" />
|
||||
<span>{teamSize} agent{teamSize !== 1 ? 's' : ''}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<button
|
||||
onClick={() => handleSelect(template)}
|
||||
className="flex-1 flex items-center justify-center gap-1.5 px-3 py-1.5 bg-[var(--ui-accent-info)] hover:bg-[var(--ui-accent-info)]/90 text-white rounded-md text-xs font-medium transition-colors"
|
||||
>
|
||||
<Check className="w-3.5 h-3.5" />
|
||||
Select
|
||||
</button>
|
||||
<button
|
||||
onClick={(e) => handleEdit(e, template.id)}
|
||||
className="p-1.5 rounded-md text-[var(--ui-text-muted)] hover:text-white hover:bg-white/10 transition-colors"
|
||||
title="Edit template"
|
||||
>
|
||||
<Pencil className="w-3.5 h-3.5" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-[var(--ui-border-soft)] bg-[#0A111A] p-4 flex-shrink-0 rounded-b-xl">
|
||||
<button
|
||||
onClick={onCreateNew}
|
||||
className="w-full flex items-center justify-center gap-2 px-4 py-2.5 bg-[var(--ui-border-soft)] hover:bg-[var(--ui-border-hover)] text-white rounded-md text-sm font-medium transition-colors"
|
||||
>
|
||||
<Plus className="w-4 h-4" />
|
||||
Create New Template
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue