Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/web/src/components/CommunityTemplatePreview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ export type TemplateDemo = {
prompt: string;
};

export const TEMPLATE_TYPE_ORDER: TemplateType[] = ['Slides', 'Prototype', 'Live Artifact', 'Image', 'Video', 'HyperFrames', 'Audio'];
export const TEMPLATE_TYPE_ORDER: TemplateType[] = ['Prototype', 'Slides', 'Live Artifact', 'Image', 'Video', 'HyperFrames', 'Audio'];

/** The Community grid is the plugin catalogue seen through the artifact a user
* wants to make. Membership comes from the shared facet derivation in
Expand Down
7 changes: 5 additions & 2 deletions apps/web/src/components/CommunityView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ export function CommunityView({ onRemixTemplate, onUsePrompt, onUsePlugin }: Com
// chip (飞书 recvqxDuYM6Uxk). Keep the raw record here: the modal renders
// from `InstalledPluginRecord`, not from the card view-model.
const [detailsRecord, setDetailsRecord] = useState<InstalledPluginRecord | null>(null);
const [activeType, setActiveType] = useState<TemplateType>('Slides');
const [selectedType, setSelectedType] = useState<TemplateType | null>(null);
const [activeSubtype, setActiveSubtype] = useState('All');
// Remix hands off to a fire-and-forget parent callback
// (`onRemixTemplate` returns void) that kicks off a real POST /api/projects
Expand Down Expand Up @@ -153,6 +153,9 @@ export function CommunityView({ onRemixTemplate, onUsePrompt, onUsePlugin }: Com
const typeOptions = TEMPLATE_TYPE_ORDER.filter((type) =>
templates.some((template) => template.type === type),
);
const activeType = selectedType && typeOptions.includes(selectedType)
? selectedType
: typeOptions[0];
const subtypeOptions = Array.from(new Set(
templates
.filter((template) => template.type === activeType && template.subtype)
Expand Down Expand Up @@ -280,7 +283,7 @@ export function CommunityView({ onRemixTemplate, onUsePrompt, onUsePlugin }: Com
filter_value: type,
...workspaceDimensions,
});
setActiveType(type);
setSelectedType(type);
setActiveSubtype('All');
}}
>
Expand Down
48 changes: 34 additions & 14 deletions apps/web/tests/community-view.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,20 +201,34 @@ describe('CommunityView catalogue source', () => {

expect(fetchMock).toHaveBeenCalledWith('/api/plugins', undefined);

// Slides leads, and it carries exactly the two deck plugins the daemon
// served — not a bundled demo array.
// Prototype leads, followed by Slides; both come from the daemon-served
// plugin catalogue rather than a bundled demo array.
const facets = readFacets();
expect(facets.map((facet) => facet.label)).toEqual(['Slides', 'Prototype', 'Image']);
expect(facets.map((facet) => facet.label)).toEqual(['Prototype', 'Slides', 'Image']);

// The card footer reads "<type> · <sub-facet>", both resolved from the
// shared plugins-home taxonomy. Asserted before the tab walk below, which
// leaves a different facet active.
expect(renderedCards().map((card) => card.querySelector('.community-template-card__foot span')?.textContent))
.toEqual(['Slides · Fundraising pitch', 'Slides · B2B sales']);
.toEqual(['Prototype · Landing / marketing']);

// Slides leads and carries exactly the two deck plugins the daemon served
// — not a bundled demo array.
expect(readFacetCardCounts()).toEqual([2, 1, 1]);
expect(readFacetCardCounts()).toEqual([1, 2, 1]);
});

it('falls back to the first available type when the catalogue has no Prototype templates', async () => {
fetchMock.mockImplementation(async () => new Response(JSON.stringify({
plugins: [PITCH_DECK, SALES_DECK],
}), {
status: 200,
headers: { 'content-type': 'application/json' },
}));

await renderCommunity();

const facets = readFacets();
expect(facets.map((facet) => facet.label)).toEqual(['Slides']);
expect(facets[0]!.tab.classList.contains('is-active')).toBe(true);
expect(renderedCards()).toHaveLength(2);
});

it('leaves hidden and design-system plugins out of the gallery', async () => {
Expand All @@ -234,8 +248,9 @@ describe('CommunityView catalogue source', () => {
const pills = Array.from(
document.querySelectorAll('.community-template-view__subtabs button'),
).map((button) => button.textContent?.trim());
expect(pills).toEqual(['All', 'Fundraising pitch', 'B2B sales']);
expect(pills).toEqual(['All', 'Landing / marketing']);

fireEvent.click(readFacets().find((facet) => facet.label === 'Slides')!.tab);
fireEvent.click(screen.getByRole('button', { name: 'B2B sales' }));
expect(renderedCards()).toHaveLength(1);
});
Expand All @@ -244,13 +259,15 @@ describe('CommunityView catalogue source', () => {
describe('CommunityView previews', () => {
it('centres deck media in the 16:9 preview crop while legacy bakes are being replaced', async () => {
await renderCommunity();
fireEvent.click(readFacets().find((facet) => facet.label === 'Slides')!.tab);

expect(renderedCards()[0]!.querySelector('.community-template-card__preview.is-deck'))
.not.toBeNull();
});

it('shows the plugin\'s own poster on the card and its live page in the full details modal', async () => {
await renderCommunity();
fireEvent.click(readFacets().find((facet) => facet.label === 'Slides')!.tab);

// Card thumbnail: the daemon-baked poster for that plugin.
const thumb = renderedCards()[0]!.querySelector('img.plugins-home__media-img');
Expand All @@ -276,6 +293,7 @@ describe('CommunityView previews', () => {
// floor even though the daemon still attached it and the classifier still
// resolved it.
await renderCommunity();
fireEvent.click(readFacets().find((facet) => facet.label === 'Slides')!.tab);

const card = renderedCards()[0]!;
expect(card.querySelector('img.community-template-thumb__image')).toBeNull();
Expand Down Expand Up @@ -306,6 +324,7 @@ describe('CommunityView previews', () => {
// The B2B deck ships an html preview and no bake, so there is no media spec
// to mount — that card must still fall back to the stylized paper tile.
await renderCommunity();
fireEvent.click(readFacets().find((facet) => facet.label === 'Slides')!.tab);

const card = renderedCards()[1]!;
expect(card.querySelector('.community-template-thumb__paper')).not.toBeNull();
Expand Down Expand Up @@ -374,8 +393,8 @@ describe('CommunityView remix', () => {

expect(onRemix).toHaveBeenCalledTimes(1);
expect(onRemix.mock.calls[0]![0]).toEqual({
templateId: 'example-fundraising-deck',
prompt: 'A decision-grade seed round narrative.',
templateId: 'example-landing-prototype',
prompt: 'A conversion-focused SaaS landing page.',
});
});

Expand Down Expand Up @@ -420,6 +439,7 @@ describe('CommunityView remix', () => {
// handler invocation really runs (see the rapid-click note above).
const onRemix = vi.fn();
await renderCommunity({ onRemixTemplate: onRemix });
fireEvent.click(readFacets().find((facet) => facet.label === 'Slides')!.tab);

fireEvent.click(renderedCards()[0]!);
await waitFor(() => {
Expand Down Expand Up @@ -459,10 +479,10 @@ describe('CommunityView use handoff', () => {
fireEvent.click(screen.getAllByRole('button', { name: 'Use' })[0]!);

expect(onUsePrompt).toHaveBeenCalledWith({
templateId: 'example-fundraising-deck',
prompt: 'A decision-grade seed round narrative.',
chipId: 'deck',
projectKind: 'deck',
templateId: 'example-landing-prototype',
prompt: 'A conversion-focused SaaS landing page.',
chipId: 'prototype',
projectKind: 'prototype',
});
});

Expand Down
Loading