Skip to content

Commit 82f2a9b

Browse files
authored
restructure Home creation hierarchy (#6914)
* restructure Home creation hierarchy * test(home): align Prototype expectations
1 parent 833e1a2 commit 82f2a9b

34 files changed

Lines changed: 484 additions & 175 deletions

apps/web/src/components/HomeHero.tsx

Lines changed: 51 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ import { ScenarioArt } from './home-hero/ScenarioArt';
5959
import { useEdgeAutoScroll, EdgeScrollZones } from './home-hero/EdgeAutoScroll';
6060
import {
6161
isSubChipParent,
62+
prototypeSubChipForSlug,
6263
subChipsForChip,
6364
type HomeHeroSubChip,
6465
} from './home-hero/sub-chips';
@@ -165,6 +166,9 @@ interface Props {
165166
activePluginIsExplicit?: boolean;
166167
activePluginRecord?: InstalledPluginRecord | null;
167168
activeChipId: string | null;
169+
// Prototype's selected second-level scene is owned by HomeView so action
170+
// metadata and persistence stay aligned with the visible filter selection.
171+
activePrototypeSubtypeId?: string | null;
168172
onClearActivePlugin: () => void;
169173
onClearActiveChip?: () => void;
170174
activeSkillId?: string | null;
@@ -229,6 +233,7 @@ interface Props {
229233
onPickMcp?: (server: McpServerConfig, nextPrompt: string) => void;
230234
onPickConnector?: (connector: ConnectorDetail, nextPrompt: string) => void;
231235
onPickChip: (chip: HomeHeroChip) => void;
236+
onPickPrototypeSubtype?: (sub: HomeHeroSubChip | null) => void;
232237
contextItemCount: number;
233238
error: string | null;
234239
showActivePluginChip?: boolean;
@@ -357,6 +362,8 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
357362
onPickMcp = () => undefined,
358363
onPickConnector = () => undefined,
359364
onPickChip,
365+
activePrototypeSubtypeId,
366+
onPickPrototypeSubtype,
360367
contextItemCount,
361368
error,
362369
showActivePluginChip = true,
@@ -392,9 +399,14 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
392399
const [guidePulseChipId, setGuidePulseChipId] = useState<string | null>(null);
393400
const [guidePulseFirstPreset, setGuidePulseFirstPreset] = useState(false);
394401
// Selected second-level sub-category slug (Prototype / Slide deck rail).
395-
// Local-only: it filters the example-prompt cards below the rail. It never
396-
// binds a plugin or stamps an active badge.
397-
const [selectedSubcategory, setSelectedSubcategory] = useState<string | null>(null);
402+
// Deck remains a local example filter. Prototype is controlled by HomeView:
403+
// every scene binds the Prototype action, while Mobile/Wireframe additionally
404+
// retain the project metadata from their former top-level actions.
405+
const [localSelectedSubcategory, setLocalSelectedSubcategory] = useState<string | null>(null);
406+
const selectedSubcategory =
407+
activeChipId === 'prototype' && activePrototypeSubtypeId !== undefined
408+
? activePrototypeSubtypeId
409+
: localSelectedSubcategory;
398410
// Footer Template pill preview: the create-rail card the pointer is over,
399411
// so hovering a card below previews it in the pill (cleared on rail-leave).
400412
const [previewTemplateId, setPreviewTemplateId] = useState<string | null>(null);
@@ -439,13 +451,17 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
439451
// with nothing bound we cycle the full set. Memoised by chip + locale so the
440452
// reference only changes on a real switch, which restarts the carousel.
441453
const carouselScenarios = useMemo<PlaceholderScenario[]>(() => {
454+
const nestedActionChipId =
455+
activeChipId === 'prototype'
456+
? prototypeSubChipForSlug(activePrototypeSubtypeId ?? null)?.actionChipId ?? null
457+
: null;
442458
return buildPlaceholderScenarios({
443-
activeChipId,
459+
activeChipId: nestedActionChipId ?? activeChipId,
444460
resolveTextKey: (key) => t(key),
445461
examplesForChip: (chipId) => homeHeroChipPromptExamples(chipId, locale),
446462
fallbackForChip: (chipId) => fallbackPlaceholderScenarioText(chipId, locale, t),
447463
});
448-
}, [activeChipId, locale, t]);
464+
}, [activeChipId, activePrototypeSubtypeId, locale, t]);
449465
// The placeholder carousel runs while the composer is empty and nothing
450466
// OTHER than a create-template chip is bound. A selected template keeps it
451467
// alive (showing that template's scenarios); only an explicit plugin/skill
@@ -738,9 +754,19 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
738754
// deck/image plugin that merely carries a "brand" tag is not pulled in.
739755
const filteredExamplePlugins = useMemo(() => {
740756
if (!selectedSubcategory || !isSubChipParent(activeChipId)) return activeExamplePlugins;
757+
// Mobile shares the existing Apps facet. Wireframe is a generation
758+
// constraint rather than a plugin taxonomy, so keep the Prototype starter
759+
// pool visible while the selected action carries its lo-fi metadata.
760+
const facetSubcategory =
761+
activeChipId === 'prototype' && selectedSubcategory === 'mobile'
762+
? 'app-prototypes'
763+
: activeChipId === 'prototype' && selectedSubcategory === 'wireframe'
764+
? null
765+
: selectedSubcategory;
766+
if (!facetSubcategory) return activeExamplePlugins;
741767
const pool = pluginOptions.filter((plugin) => plugin.manifest?.od?.kind !== 'atom');
742768
return sortByVisualAppeal(
743-
applyFacetSelection(pool, { category: activeChipId, subcategory: selectedSubcategory }),
769+
applyFacetSelection(pool, { category: activeChipId, subcategory: facetSubcategory }),
744770
);
745771
}, [activeExamplePlugins, activeChipId, selectedSubcategory, pluginOptions]);
746772

@@ -853,7 +879,7 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
853879

854880
useEffect(() => {
855881
setSelectedPromptExample(null);
856-
setSelectedSubcategory(null);
882+
setLocalSelectedSubcategory(null);
857883
}, [activeChipId]);
858884

859885
useEffect(() => {
@@ -1270,7 +1296,7 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
12701296
<PixelScanLogo className="home-hero__logo home-hero__logo--tiles" />
12711297
</span>
12721298

1273-
{/* Capsule type row: the 12 create-scenario types as pill chips above
1299+
{/* Capsule type row: the 10 top-level create-scenario types as pill chips above
12741300
the composer (per product — replaces the fanned card carousel); the
12751301
selected pill carries the accent tint, click switches. */}
12761302
<TypePillRow
@@ -2103,7 +2129,9 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
21032129
chip_id: activeChipId ?? undefined,
21042130
subcategory: sub.slug,
21052131
});
2106-
setSelectedSubcategory((current) => (current === sub.slug ? null : sub.slug));
2132+
const next = selectedSubcategory === sub.slug ? null : sub;
2133+
setLocalSelectedSubcategory(next?.slug ?? null);
2134+
if (activeChipId === 'prototype') onPickPrototypeSubtype?.(next);
21072135
}}
21082136
onSelectAll={() => {
21092137
trackHomeChatComposerClick(analytics.track, {
@@ -2113,7 +2141,8 @@ export const HomeHero = forwardRef<HomeHeroHandle, Props>(function HomeHero(
21132141
chip_id: activeChipId ?? undefined,
21142142
subcategory: 'all',
21152143
});
2116-
setSelectedSubcategory(null);
2144+
setLocalSelectedSubcategory(null);
2145+
if (activeChipId === 'prototype') onPickPrototypeSubtype?.(null);
21172146
}}
21182147
/>
21192148
) : null}
@@ -3409,7 +3438,7 @@ function SubTypeChip({
34093438
>
34103439
<Icon name={sub.icon} size={13} className="home-hero__subtype-chip-icon" />
34113440
<span className="home-hero__subtype-chip-label">
3412-
{pluginSubfacetLabel(sub.slug, sub.label, t)}
3441+
{homeHeroSubChipLabel(sub, t)}
34133442
</span>
34143443
</button>
34153444
);
@@ -3570,7 +3599,7 @@ function SubTypeRow({
35703599
>
35713600
<Icon name={sub.icon} size={13} className="home-hero__subtype-chip-icon" />
35723601
<span className="home-hero__subtype-chip-label">
3573-
{pluginSubfacetLabel(sub.slug, sub.label, t)}
3602+
{homeHeroSubChipLabel(sub, t)}
35743603
</span>
35753604
{isActive ? <Icon name="check" size={13} /> : null}
35763605
</button>
@@ -3590,7 +3619,7 @@ function SubTypeRow({
35903619
<span key={sub.slug} className="home-hero__subtype-chip" data-measure="chip">
35913620
<Icon name={sub.icon} size={13} className="home-hero__subtype-chip-icon" />
35923621
<span className="home-hero__subtype-chip-label">
3593-
{pluginSubfacetLabel(sub.slug, sub.label, t)}
3622+
{homeHeroSubChipLabel(sub, t)}
35943623
</span>
35953624
</span>
35963625
))}
@@ -3603,6 +3632,15 @@ function SubTypeRow({
36033632
);
36043633
}
36053634

3635+
function homeHeroSubChipLabel(
3636+
sub: HomeHeroSubChip,
3637+
t: ReturnType<typeof useT>,
3638+
): string {
3639+
if (sub.slug === 'mobile') return t('homeHero.chip.mobile');
3640+
if (sub.slug === 'wireframe') return t('homeHero.chip.wireframe');
3641+
return pluginSubfacetLabel(sub.slug, sub.label, t);
3642+
}
3643+
36063644
interface ShortcutsMenuProps {
36073645
activeChipId: string | null;
36083646
pendingChipId: string | null;

0 commit comments

Comments
 (0)