diff --git a/apps/web/src/components/NewProjectPanel.tsx b/apps/web/src/components/NewProjectPanel.tsx index e1f9f387f..f00bfac2c 100644 --- a/apps/web/src/components/NewProjectPanel.tsx +++ b/apps/web/src/components/NewProjectPanel.tsx @@ -75,6 +75,28 @@ const TAB_LABEL_KEYS: Record = { other: 'newproj.tabOther', }; +export function defaultDesignSystemSelection( + defaultDesignSystemId: string | null, + designSystems: DesignSystemSummary[], +): string[] { + if (!defaultDesignSystemId) return []; + return designSystems.some((d) => d.id === defaultDesignSystemId) + ? [defaultDesignSystemId] + : []; +} + +export function buildDesignSystemCreateSelection( + showDesignSystemPicker: boolean, + selectedIds: string[], +): { primary: string | null; inspirations: string[] } { + return showDesignSystemPicker + ? { + primary: selectedIds[0] ?? null, + inspirations: selectedIds.slice(1), + } + : { primary: null, inspirations: [] }; +} + export function NewProjectPanel({ skills, designSystems, @@ -99,7 +121,14 @@ export function NewProjectPanel({ // 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 initialDefaultDsSelection = useMemo( + () => defaultDesignSystemSelection(defaultDesignSystemId, designSystems), + [defaultDesignSystemId, designSystems], + ); + const [selectedDsIds, setSelectedDsIds] = useState( + () => initialDefaultDsSelection, + ); + const [dsSelectionTouched, setDsSelectionTouched] = useState(false); const [dsMulti, setDsMulti] = useState(false); // Per-tab metadata. Tracked independently so switching tabs preserves @@ -166,6 +195,11 @@ export function NewProjectPanel({ const showDesignSystemPicker = tabSupportsDesignSystem && !tabDefaultSkillForcesNoDs; + useEffect(() => { + if (dsSelectionTouched) return; + setSelectedDsIds(initialDefaultDsSelection); + }, [dsSelectionTouched, initialDefaultDsSelection]); + // 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 @@ -242,6 +276,11 @@ export function NewProjectPanel({ }); } + function handleDesignSystemChange(ids: string[]) { + setDsSelectionTouched(true); + setSelectedDsIds(ids); + } + useEffect(() => { const el = tabsRef.current; if (!el) return; @@ -269,8 +308,8 @@ export function NewProjectPanel({ // and inspiration ids to empty there so the New Project panel can't // accidentally bind a stale DS that the user can no longer see in the // form (the picker is hidden for image/video/audio). - const primaryDs = showDesignSystemPicker ? selectedDsIds[0] ?? null : null; - const inspirations = showDesignSystemPicker ? selectedDsIds.slice(1) : []; + const { primary: primaryDs, inspirations } = + buildDesignSystemCreateSelection(showDesignSystemPicker, selectedDsIds); const promptTemplatePick = tab === 'image' ? imagePromptTemplate @@ -379,7 +418,7 @@ export function NewProjectPanel({ selectedIds={selectedDsIds} multi={dsMulti} onChangeMulti={setDsMulti} - onChange={setSelectedDsIds} + onChange={handleDesignSystemChange} loading={loading} /> ) : null} diff --git a/apps/web/tests/components/NewProjectPanel.test.tsx b/apps/web/tests/components/NewProjectPanel.test.tsx new file mode 100644 index 000000000..50122a67f --- /dev/null +++ b/apps/web/tests/components/NewProjectPanel.test.tsx @@ -0,0 +1,72 @@ +import { renderToStaticMarkup } from 'react-dom/server'; +import { describe, expect, it, vi } from 'vitest'; + +import { + buildDesignSystemCreateSelection, + defaultDesignSystemSelection, + NewProjectPanel, +} from '../../src/components/NewProjectPanel'; +import type { DesignSystemSummary, SkillSummary } from '../../src/types'; + +const skills: SkillSummary[] = [ + { + id: 'prototype-skill', + name: 'Prototype', + description: 'Build prototypes', + mode: 'prototype', + surface: 'web', + previewType: 'html', + designSystemRequired: false, + defaultFor: ['prototype'], + triggers: [], + upstream: null, + hasBody: true, + examplePrompt: 'Build a prototype.', + }, +]; + +const designSystems: DesignSystemSummary[] = [ + { + id: 'clay', + title: 'Clay', + summary: 'Friendly tactile product UI.', + category: 'Product', + swatches: ['#f4efe7', '#25211d'], + }, +]; + +describe('NewProjectPanel design system defaults', () => { + it('uses the configured default design system when it exists in the catalog', () => { + expect(defaultDesignSystemSelection('clay', designSystems)).toEqual(['clay']); + expect(defaultDesignSystemSelection('missing', designSystems)).toEqual([]); + expect(defaultDesignSystemSelection(null, designSystems)).toEqual([]); + }); + + it('shows the configured default design system as the active project selection', () => { + const markup = renderToStaticMarkup( + , + ); + + expect(markup).toContain('Clay'); + expect(markup).toContain('Default'); + expect(markup).not.toContain('Freeform'); + }); + + it('keeps media project creation from inheriting a hidden design system pick', () => { + expect(buildDesignSystemCreateSelection(true, ['clay', 'bmw'])).toEqual({ + primary: 'clay', + inspirations: ['bmw'], + }); + expect(buildDesignSystemCreateSelection(false, ['clay', 'bmw'])).toEqual({ + primary: null, + inspirations: [], + }); + }); +});