fix web design system selection persistence (#621)

This commit is contained in:
Sid 2026-05-07 02:27:00 +08:00 committed by GitHub
parent 1bd1f3a661
commit 4c82e48e4f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 115 additions and 4 deletions

View file

@ -75,6 +75,28 @@ const TAB_LABEL_KEYS: Record<CreateTab, keyof Dict> = {
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<string[]>([]);
const initialDefaultDsSelection = useMemo(
() => defaultDesignSystemSelection(defaultDesignSystemId, designSystems),
[defaultDesignSystemId, designSystems],
);
const [selectedDsIds, setSelectedDsIds] = useState<string[]>(
() => 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}

View file

@ -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(
<NewProjectPanel
skills={skills}
designSystems={designSystems}
defaultDesignSystemId="clay"
templates={[]}
promptTemplates={[]}
onCreate={vi.fn()}
/>,
);
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: [],
});
});
});