import { useEffect, useMemo, useRef, useState } from 'react'; import { useT } from '../i18n'; import type { Dict } from '../i18n/types'; import type { DesignSystemSummary, ProjectKind, ProjectMetadata, ProjectTemplate, SkillSummary, } from '../types'; import { Icon } from './Icon'; import { Skeleton } from './Loading'; type TranslateFn = (key: keyof Dict, vars?: Record) => string; export type CreateTab = 'prototype' | 'deck' | 'template' | 'other'; export interface CreateInput { name: string; skillId: string | null; designSystemId: string | null; metadata: ProjectMetadata; } interface Props { skills: SkillSummary[]; designSystems: DesignSystemSummary[]; defaultDesignSystemId: string | null; templates: ProjectTemplate[]; onCreate: (input: CreateInput) => void; loading?: boolean; } const TAB_LABEL_KEYS: Record = { prototype: 'newproj.tabPrototype', deck: 'newproj.tabDeck', template: 'newproj.tabTemplate', other: 'newproj.tabOther', }; export function NewProjectPanel({ skills, designSystems, defaultDesignSystemId, templates, onCreate, loading = false, }: Props) { const t = useT(); const [tab, setTab] = useState('prototype'); const [name, setName] = useState(''); // Design-system selection is now an *array* internally so the same // component can drive both single-select and multi-select modes without // duplicating state. Single-select coerces to length 0/1. const [selectedDsIds, setSelectedDsIds] = useState([]); const [dsMulti, setDsMulti] = useState(false); // Per-tab metadata. Tracked independently so switching tabs preserves // each tab's pick rather than resetting to defaults. const [fidelity, setFidelity] = useState<'wireframe' | 'high-fidelity'>( 'high-fidelity', ); const [speakerNotes, setSpeakerNotes] = useState(false); const [animations, setAnimations] = useState(false); const [templateId, setTemplateId] = useState(null); // When entering the template tab, snap to the first user-saved template // if there is one (and we don't already have a valid pick). The template // tab no longer offers a built-in fallback — the entire point is to // start from a template *the user* created via Share. useEffect(() => { if (tab !== 'template') return; if (templates.length === 0) { setTemplateId(null); return; } if (templateId == null || !templates.some((t) => t.id === templateId)) { setTemplateId(templates[0]!.id); } }, [tab, templates, templateId]); // The skill the request still routes through — kept so prototype/deck // pick a default-rendered skill (so the agent gets the right SKILL.md // body) without requiring the user to choose one explicitly. const skillIdForTab = useMemo(() => { if (tab === 'other') return null; if (tab === 'prototype') { const list = skills.filter((s) => s.mode === 'prototype'); return list.find((s) => s.defaultFor.includes('prototype'))?.id ?? list[0]?.id ?? null; } if (tab === 'deck') { const list = skills.filter((s) => s.mode === 'deck'); return list.find((s) => s.defaultFor.includes('deck'))?.id ?? list[0]?.id ?? null; } return null; }, [tab, skills]); const canCreate = !loading && (tab !== 'template' || templateId != null); function handleCreate() { if (!canCreate) return; const primaryDs = selectedDsIds[0] ?? null; const inspirations = selectedDsIds.slice(1); const metadata = buildMetadata({ tab, fidelity, speakerNotes, animations, templateId, templates, inspirationIds: inspirations, }); onCreate({ name: name.trim() || autoName(tab, t), skillId: skillIdForTab, designSystemId: primaryDs, metadata, }); } return (
{(Object.keys(TAB_LABEL_KEYS) as CreateTab[]).map((entry) => ( ))}

{titleForTab(tab, t)}

setName(e.target.value)} /> {tab === 'prototype' ? ( ) : null} {tab === 'deck' ? ( ) : null} {tab === 'template' ? ( <> ) : null}
{t('newproj.privacyFooter')}
); } function FidelityPicker({ value, onChange, }: { value: 'wireframe' | 'high-fidelity'; onChange: (v: 'wireframe' | 'high-fidelity') => void; }) { const t = useT(); return (
onChange('wireframe')} label={t('newproj.fidelityWireframe')} variant="wireframe" /> onChange('high-fidelity')} label={t('newproj.fidelityHigh')} variant="high-fidelity" />
); } function FidelityCard({ active, onClick, label, variant, }: { active: boolean; onClick: () => void; label: string; variant: 'wireframe' | 'high-fidelity'; }) { return ( ); } function WireframeArt() { return ( ); } function HighFidelityArt() { return ( ); } function ToggleRow({ label, hint, checked, onChange, }: { label: string; hint?: string; checked: boolean; onChange: (v: boolean) => void; }) { return ( ); } function TemplatePicker({ templates, value, onChange, }: { templates: ProjectTemplate[]; value: string | null; onChange: (id: string | null) => void; }) { const t = useT(); return (
{templates.length === 0 ? (
{t('newproj.noTemplatesTitle')} {t('newproj.noTemplatesBody')}
) : (
{templates.map((tpl) => { const fallbackDesc = `${t('newproj.savedTemplate')} · ${tpl.files.length} ${ tpl.files.length === 1 ? t('newproj.fileSingular') : t('newproj.filePlural') }`; return ( onChange(tpl.id)} name={tpl.name} description={tpl.description ?? fallbackDesc} /> ); })}
)}
); } function TemplateOption({ active, onClick, name, description, }: { active: boolean; onClick: () => void; name: string; description: string; }) { return ( ); } /* ============================================================ Design system picker — custom popover (replaces native setQuery(e.target.value)} />
} title={t('newproj.dsNoneTitle')} subtitle={t('newproj.dsNoneSub')} /> {filtered.length === 0 ? (
{t('newproj.dsEmpty', { query })}
) : ( filtered.map((d) => { const active = selectedIds.includes(d.id); const order = active ? selectedIds.indexOf(d.id) : -1; return ( toggle(d.id)} avatar={} title={d.title} badge={ d.id === defaultDesignSystemId ? t('newproj.dsBadgeDefault') : undefined } subtitle={d.summary || d.category || ''} /> ); }) )}
{multi && selectedIds.length > 1 ? (
{primary?.title ?? t('newproj.dsPrimaryFallback')}{' '} {extraCount === 1 ? t('newproj.dsFootSingular') : t('newproj.dsFootPlural')}
) : null} ) : null} ); } function DsPickerItem({ active, multi, order, onClick, avatar, title, subtitle, badge, }: { active: boolean; multi: boolean; order?: number; onClick: () => void; avatar: React.ReactNode; title: string; subtitle: string; badge?: string; }) { return ( ); } function DesignSystemAvatar({ system, extraCount = 0, }: { system: DesignSystemSummary | null; extraCount?: number; }) { if (!system) return ; const swatches = system.swatches && system.swatches.length > 0 ? system.swatches.slice(0, 4) : fallbackSwatches(system.title); return ( {swatches.map((c, i) => ( ))} {extraCount > 0 ? ( +{extraCount} ) : null} ); } function NoneAvatar() { return ( ); } // Deterministic fallback swatches for design systems whose DESIGN.md doesn't // expose its tokens via the bold-and-hex format. Keeps the avatar visually // distinct per-system without extra metadata fetches. function fallbackSwatches(seed: string): string[] { let h = 0; for (let i = 0; i < seed.length; i++) { h = (h * 31 + seed.charCodeAt(i)) >>> 0; } const base = h % 360; return [ `hsl(${base}, 18%, 96%)`, `hsl(${(base + 90) % 360}, 22%, 78%)`, `hsl(${(base + 180) % 360}, 30%, 32%)`, `hsl(${(base + 30) % 360}, 70%, 52%)`, ]; } function buildMetadata(input: { tab: CreateTab; fidelity: 'wireframe' | 'high-fidelity'; speakerNotes: boolean; animations: boolean; templateId: string | null; templates: ProjectTemplate[]; inspirationIds: string[]; }): ProjectMetadata { const kind: ProjectKind = input.tab; const inspirations = input.inspirationIds.length > 0 ? { inspirationDesignSystemIds: input.inspirationIds } : {}; if (input.tab === 'prototype') { return { kind, fidelity: input.fidelity, ...inspirations }; } if (input.tab === 'deck') { return { kind, speakerNotes: input.speakerNotes, ...inspirations }; } if (input.tab === 'template') { if (input.templateId == null) { return { kind, animations: input.animations, ...inspirations }; } const tpl = input.templates.find((x) => x.id === input.templateId); // The fallback label is consumed by the agent prompt rather than the // UI, so we keep it in English to match the rest of the prompt corpus. return { kind, animations: input.animations, templateId: input.templateId, templateLabel: tpl?.name ?? 'Saved template', ...inspirations, }; } return { kind: 'other', ...inspirations }; } function titleForTab(tab: CreateTab, t: TranslateFn): string { switch (tab) { case 'prototype': return t('newproj.titlePrototype'); case 'deck': return t('newproj.titleDeck'); case 'template': return t('newproj.titleTemplate'); case 'other': return t('newproj.titleOther'); } } function autoName(tab: CreateTab, t: TranslateFn): string { const stamp = new Date().toLocaleDateString(); return `${t(TAB_LABEL_KEYS[tab])} · ${stamp}`; }