import { useEffect, useMemo, useRef, useState } from 'react'; import { useT } from '../i18n'; import { providerLabel } from '../providers/presets'; import type { AgentInfo, AppConfig, DesignSystemSummary, Project, ProjectKind, ProjectMetadata, ProjectTemplate, SkillSummary, } from '../types'; import { DesignsTab } from './DesignsTab'; import { DesignSystemPreviewModal } from './DesignSystemPreviewModal'; import { DesignSystemsTab } from './DesignSystemsTab'; import { ExamplesTab } from './ExamplesTab'; import { Icon } from './Icon'; import { LanguageMenu } from './LanguageMenu'; import { CenteredLoader } from './Loading'; import { NewProjectPanel, type CreateInput } from './NewProjectPanel'; type TopTab = 'designs' | 'examples' | 'design-systems'; interface Props { skills: SkillSummary[]; designSystems: DesignSystemSummary[]; projects: Project[]; templates: ProjectTemplate[]; defaultDesignSystemId: string | null; config: AppConfig; agents: AgentInfo[]; loading?: boolean; onCreateProject: (input: CreateInput & { pendingPrompt?: string }) => void; onOpenProject: (id: string) => void; onDeleteProject: (id: string) => void; onChangeDefaultDesignSystem: (id: string) => void; onOpenSettings: () => void; } const SIDEBAR_MIN = 320; const SIDEBAR_MAX = 560; const SIDEBAR_DEFAULT = 380; const SIDEBAR_STORAGE_KEY = 'od-entry-sidebar-width'; function loadSidebarWidth(): number { try { const raw = window.localStorage.getItem(SIDEBAR_STORAGE_KEY); if (!raw) return SIDEBAR_DEFAULT; const n = parseInt(raw, 10); if (Number.isNaN(n)) return SIDEBAR_DEFAULT; return Math.max(SIDEBAR_MIN, Math.min(SIDEBAR_MAX, n)); } catch { return SIDEBAR_DEFAULT; } } export function EntryView({ skills, designSystems, projects, templates, defaultDesignSystemId, config, agents, loading = false, onCreateProject, onOpenProject, onDeleteProject, onChangeDefaultDesignSystem, onOpenSettings, }: Props) { const t = useT(); const [topTab, setTopTab] = useState('designs'); const [previewSystemId, setPreviewSystemId] = useState(null); const [sidebarWidth, setSidebarWidth] = useState(() => loadSidebarWidth()); const [resizing, setResizing] = useState(false); const currentAgent = useMemo( () => agents.find((a) => a.id === config.agentId) ?? null, [agents, config.agentId], ); const envMetaLine = useMemo(() => { if (config.mode === 'api') { const provider = providerLabel(config.provider); try { return `${provider} · ${config.model} · ${new URL(config.baseUrl).host}`; } catch { return `${provider} · ${config.model}`; } } return currentAgent ? `${currentAgent.name}${currentAgent.version ? ` · ${currentAgent.version}` : ''}` : t('settings.noAgentSelected'); }, [config.mode, config.model, config.baseUrl, config.provider, currentAgent, t]); // 'Use this prompt' on an example card is a fast path — skip the form and // create the project immediately with sane defaults derived from the skill, // seeding the chat composer with the example prompt via pendingPrompt. function usePromptFromSkill(skill: SkillSummary) { onCreateProject({ name: skill.name, skillId: skill.id, designSystemId: null, metadata: metadataForSkill(skill), pendingPrompt: skill.examplePrompt || skill.description, }); } function previewDesignSystem(id: string) { setPreviewSystemId(id); } const previewSystem = useMemo( () => (previewSystemId ? designSystems.find((d) => d.id === previewSystemId) ?? null : null), [designSystems, previewSystemId], ); function handleCreate(input: CreateInput) { onCreateProject(input); } const startWidthRef = useRef(0); const startXRef = useRef(0); useEffect(() => { if (!resizing) return; function onMove(e: MouseEvent) { const dx = e.clientX - startXRef.current; const next = Math.max( SIDEBAR_MIN, Math.min(SIDEBAR_MAX, startWidthRef.current + dx), ); setSidebarWidth(next); } function onUp() { setResizing(false); } document.body.classList.add('entry-resizing'); window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); return () => { document.body.classList.remove('entry-resizing'); window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); }; }, [resizing]); useEffect(() => { try { window.localStorage.setItem(SIDEBAR_STORAGE_KEY, String(sidebarWidth)); } catch { /* ignore */ } }, [sidebarWidth]); return (
{/* Avatar settings live next to tabs to mirror the project view. */}
{loading ? ( ) : ( <> {topTab === 'designs' ? ( ) : null} {topTab === 'examples' ? ( ) : null} {topTab === 'design-systems' ? ( ) : null} )}
{previewSystem ? ( setPreviewSystemId(null)} /> ) : null}
); } function TopTabButton({ current, value, label, onClick, }: { current: TopTab; value: TopTab; label: string; onClick: (v: TopTab) => void; }) { return ( ); } // Map a skill's declared mode to project metadata. Falls back to the same // defaults the new-project form would apply (high-fidelity prototype, no // speaker notes on decks, no template animations) so 'Use this prompt' // produces a project indistinguishable from one created via the form. Per- // skill hints in SKILL.md frontmatter (od.fidelity, od.speaker_notes, // od.animations) override the defaults so each example reproduces the // shipped example.html — e.g. wireframe-sketch declares fidelity:wireframe. function metadataForSkill(skill: SkillSummary): ProjectMetadata { const kind = kindForSkill(skill); if (kind === 'prototype') { return { kind, fidelity: skill.fidelity ?? 'high-fidelity' }; } if (kind === 'deck') { return { kind, speakerNotes: typeof skill.speakerNotes === 'boolean' ? skill.speakerNotes : false, }; } if (kind === 'template') { return { kind, animations: typeof skill.animations === 'boolean' ? skill.animations : false, }; } return { kind: 'other' }; } function kindForSkill(skill: SkillSummary): ProjectKind { if (skill.mode === 'deck') return 'deck'; if (skill.mode === 'prototype') return 'prototype'; if (skill.mode === 'template') return 'template'; return 'other'; }