From 5bebdd00b818ef4b3b210e673dc24a62794fe219 Mon Sep 17 00:00:00 2001 From: Copons Date: Tue, 30 Jun 2026 16:19:04 +0100 Subject: [PATCH 1/8] AI Launchpad: drop redundant write_3_posts task from the tailoring menu The model could pick both write_3_posts and first_post_published, which read as duplicates in the same list (DSGCOM-678). Remove write_3_posts from the prompt's TASK_MENU so it's no longer offered; the menu stays a subset of the catalog (AI_Launchpad_Task_Menu_Test still passes). --- .../src/features/ai-launchpad/js/lib/prompts.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/lib/prompts.ts b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/lib/prompts.ts index c5e12c2210bb..668df1b8befe 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/lib/prompts.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/lib/prompts.ts @@ -3,13 +3,11 @@ import type { WizardInput } from './types.ts'; /** * Allowed task IDs the model may pick from. Snake_case catalog IDs drawn from * launchpad-task-definitions.php (verified 2026-06-02). A PHP test - * (AI_Launchpad_Task_Menu_Test) guards this list against catalog drift; making - * the catalog the single source is tracked in DOTOBRD-472. + * (AI_Launchpad_Task_Menu_Test) guards this list against catalog drift. */ export const TASK_MENU: readonly string[] = [ 'first_post_published', 'first_post_published_newsletter', - 'write_3_posts', 'site_theme_selected', 'add_about_page', 'add_new_page', From 9609e839db29df43e749e553b352cf64fca5cb5f Mon Sep 17 00:00:00 2001 From: Copons Date: Tue, 30 Jun 2026 16:19:30 +0100 Subject: [PATCH 2/8] AI Launchpad: keep social task subtitles general, not network-specific Design feedback (DSGCOM-678): "Connect your social media accounts" read too specific (e.g. naming Instagram/Pinterest) when we do not know which platforms the user uses. Add a prompt rule so connect_social_media / drive_traffic / post_sharing_enabled subtitles stay about growing the audience and engaging visitors, without naming specific networks. --- .../jetpack-mu-wpcom/src/features/ai-launchpad/js/lib/prompts.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/lib/prompts.ts b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/lib/prompts.ts index 668df1b8befe..9ed6434ca2be 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/lib/prompts.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/lib/prompts.ts @@ -113,6 +113,7 @@ HARD RULES (do not break - the server rejects output that violates these): - The 6th and final task MUST be a launch task: one of "site_launched" (canonical), "blog_launched", "woo_launch_site", or "link_in_bio_launched". - Only include "woo_products", "woo_customize_store", "set_up_payments", "stripe_connected", or "woo_woocommerce_payments" if the goal is sell OR the user explicitly mentions selling, products, store, shop, or commerce. - Only include "add_10_email_subscribers", "subscribers_added", "newsletter_plan_created", or "import_subscribers" if the goal is newsletter OR the user explicitly mentions email subscribers or a newsletter. +- For the social tasks "connect_social_media", "drive_traffic", and "post_sharing_enabled", keep the subtitle general - about growing the site's audience and engaging visitors (e.g. "Build the audience of your blog and engage with your visitors."). Do NOT name specific social networks (Instagram, Pinterest, X, Facebook, TikTok, etc.); the user has not said which platforms they use. - Subtitles must be plain text: no URLs, no HTML, and no template syntax such as {{ }} or [[ ]]. ============ STEP 3 - first_post_draft ============ From 77c982f175569cbac746c076830c43bb48e47b4b Mon Sep 17 00:00:00 2001 From: Copons Date: Tue, 30 Jun 2026 16:19:49 +0100 Subject: [PATCH 3/8] AI Launchpad: let wizard goal-card copy span the full width on mobile Design feedback (DSGCOM-678): the Educate and Portfolio descriptions did not extend across the container on mobile. `text-wrap: balance` evens line lengths, which keeps the copy narrow on the full-width single-column mobile cards. Scope balance to the two-column desktop grid and use `text-wrap: pretty` on mobile so longer copy fills the width while still avoiding orphans. --- .../src/features/ai-launchpad/js/wizard/style.scss | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/wizard/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/wizard/style.scss index 6f180d9c691f..363f9000fe42 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/wizard/style.scss +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/wizard/style.scss @@ -111,8 +111,15 @@ color: #50575e; line-height: 1.4; // Balance the short two-line copy so a single word never orphans onto its - // own last line (e.g. "expertise.", "goods."). + // own last line (e.g. "expertise.", "goods.") in the narrow two-column grid. text-wrap: balance; + + // On mobile the cards stack full-width, where `balance` keeps the lines short + // instead of letting longer copy (Educate, Portfolio) span the container. + // `pretty` still avoids orphans but fills the available width first. + @media (max-width: 600px) { + text-wrap: pretty; + } } .ai-launchpad-wizard__footer { From 9216d5dd572e0aa54c69c778b8628dfa37657b55 Mon Sep 17 00:00:00 2001 From: Copons Date: Tue, 30 Jun 2026 16:20:22 +0100 Subject: [PATCH 4/8] AI Launchpad: use WPDS state icons in the tailored list Design feedback (DSGCOM-678): use WPDS icons for the task states. Replace the inline hand-rolled SVGs with @wordpress/icons `border` (a dashed ring) for an active/not-started task and `published` (a check-in-circle) for a done one. Icons fill from currentColor so the existing grey toning still applies. --- .../ai-launchpad/js/tailored-list/style.scss | 13 +++-- .../js/tailored-list/task-card.tsx | 48 ++++--------------- 2 files changed, 17 insertions(+), 44 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/style.scss b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/style.scss index de874c27a3e6..b7bab8da4cca 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/style.scss +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/style.scss @@ -94,17 +94,22 @@ } } - // The inline SVG task icons use `currentColor`, so tone them via `color`. + // The WPDS task icons fill from `currentColor`, so tone them via `color`. &__icon { flex-shrink: 0; + display: inline-flex; - // Incomplete tasks: a light dashed ring. + svg { + fill: currentColor; + } + + // Incomplete tasks: a light dashed ring (the `border` icon). &.is-todo { color: #c3c4c7; } - // Completed tasks: a check-in-circle, greyed to sit alongside the - // struck-through title. + // Completed tasks: a check-in-circle (the `published` icon), greyed to sit + // alongside the struck-through title. &.is-done { color: #757575; } diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/task-card.tsx b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/task-card.tsx index 86bb59931715..69a5b1379077 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/task-card.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/task-card.tsx @@ -1,45 +1,9 @@ +import { Icon } from '@wordpress/components'; import { __ } from '@wordpress/i18n'; +import { border, published } from '@wordpress/icons'; import { Button, Card, CollapsibleCard } from '@wordpress/ui'; import { ctaKind, type EnrichedTask } from './model.ts'; -// WPDS doesn't ship a "todo / dashed-circle" or "check-in-circle" icon yet (only -// `check`), so we inline both. Sized at 24px to line up with each other; -// `currentColor` lets us tone them via CSS. -const taskActiveIcon = ( - -); - -const taskDoneIcon = ( - -); - interface Props { task: EnrichedTask; isBusy: boolean; @@ -132,7 +96,9 @@ export function TaskCard( { - { taskDoneIcon } + + + { task.title } @@ -144,7 +110,9 @@ export function TaskCard( { - { taskActiveIcon } + + + { task.title } From 68f0ea9c72f30043c8533ac4931664ad8846ab94 Mon Sep 17 00:00:00 2001 From: Copons Date: Tue, 30 Jun 2026 16:22:19 +0100 Subject: [PATCH 5/8] AI Launchpad: make the tailored list an accordion (one card open at a time) Design feedback (DSGCOM-678): only one task card should be open at a time, and opening a different card should collapse the active one. Replace the cards uncontrolled defaultOpen with parent-controlled open/onOpenChange: a single openId state means opening any card closes the rest, and the user can still collapse all. The first incomplete card auto-opens once (guarded by a ref so a fully-collapsed list is never force-reopened). firstIncompleteIndex becomes nextIncompleteId (id-based, null when all complete). --- .../js/tailored-list/model.test.mts | 16 ++++----- .../ai-launchpad/js/tailored-list/model.ts | 13 +++---- .../js/tailored-list/tailored-list.tsx | 35 ++++++++++++++----- .../js/tailored-list/task-card.tsx | 20 ++++++++--- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.test.mts b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.test.mts index 1cd36928fa37..0bd14bf33e13 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.test.mts +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.test.mts @@ -6,7 +6,7 @@ import { fileURLToPath } from 'node:url'; import { validateAgainstSchema } from '../lib/schema-validator.ts'; import { ctaKind, - firstIncompleteIndex, + nextIncompleteId, isCompleteOnClickTask, isTaskActionable, launchSiteUrl, @@ -273,24 +273,24 @@ describe( 'resolveCtaUrl', () => { } ); } ); -describe( 'firstIncompleteIndex', () => { - it( 'returns the first incomplete task index', () => { +describe( 'nextIncompleteId', () => { + it( 'returns the first incomplete task id', () => { const tasks = [ task( { id: 'a', completed: true } ), task( { id: 'b', completed: false } ), task( { id: 'c', completed: false } ), ]; - assert.equal( firstIncompleteIndex( tasks ), 1 ); + assert.equal( nextIncompleteId( tasks ), 'b' ); } ); - it( 'returns 0 when nothing is complete', () => { + it( 'returns the first id when nothing is complete', () => { const tasks = [ task( { id: 'a' } ), task( { id: 'b' } ) ]; - assert.equal( firstIncompleteIndex( tasks ), 0 ); + assert.equal( nextIncompleteId( tasks ), 'a' ); } ); - it( 'returns -1 when everything is complete', () => { + it( 'returns null when everything is complete', () => { const tasks = [ task( { id: 'a', completed: true } ), task( { id: 'b', completed: true } ) ]; - assert.equal( firstIncompleteIndex( tasks ), -1 ); + assert.equal( nextIncompleteId( tasks ), null ); } ); } ); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts index 272fd30c6af0..fa2f30036c18 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts @@ -244,14 +244,15 @@ export function isTaskActionable( } /** - * The index of the first incomplete task, or -1 when every task is complete. - * Drives which card auto-expands on first render. + * The id of the first incomplete task, driving which card the accordion + * auto-expands. Returns null when every task is complete. * - * @param tasks - The enriched tasks. - * @return The index of the first incomplete task, or -1. + * @param tasks - The enriched tasks (skipped tasks already coerced to completed). + * @return The first incomplete task id, or null. */ -export function firstIncompleteIndex( tasks: EnrichedTask[] ): number { - return tasks.findIndex( task => ! task.completed ); +export function nextIncompleteId( tasks: EnrichedTask[] ): string | null { + const next = tasks.find( task => ! task.completed ); + return next ? next.id : null; } /** diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/tailored-list.tsx b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/tailored-list.tsx index dec1903aff3e..ba459e32679f 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/tailored-list.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/tailored-list.tsx @@ -1,12 +1,12 @@ import apiFetch from '@wordpress/api-fetch'; -import { useEffect, useMemo, useState } from '@wordpress/element'; +import { useEffect, useMemo, useRef, useState } from '@wordpress/element'; import { __, sprintf } from '@wordpress/i18n'; import { createFirstPostDraft } from '../lib/first-post.ts'; import { createPatternPage } from '../lib/pattern-page.ts'; import { trackTaskClicked } from '../lib/tracks.ts'; import { Layout } from './layout.tsx'; import { - firstIncompleteIndex, + nextIncompleteId, isCompleteOnClickTask, isTaskActionable, resolveCtaUrl, @@ -80,6 +80,17 @@ export function TailoredList( { pendingTailor, initialData, site }: Props = {} ) ); const [ skippedIds, setSkippedIds ] = useState< Set< string > >( () => new Set() ); const [ busyId, setBusyId ] = useState< string | null >( null ); + // The single expanded card (accordion: only one open at a time). Seeded to the + // first incomplete task for returning users (initialData present on first + // render); the wizard→list path has no tasks yet, so the load effect opens it + // once the read lands. `null` means every card is collapsed — a state the user + // can reach by toggling the open card shut, which must not auto-reopen. + const [ openId, setOpenId ] = useState< string | null >( () => + initialData?.tasks ? nextIncompleteId( initialData.tasks ) : null + ); + // Guards the one-time auto-open so the load effect doesn't fight a user who has + // collapsed every card. True already when seeded from initialData above. + const didAutoOpen = useRef( !! initialData?.tasks ); // The site's front-end URL, used to build the launch CTA and the preview // thumbnail; the title labels the preview. Seeded from the read (returning users) // or the host's `site` prop (wizard path, so the skeleton shows the preview), @@ -143,6 +154,16 @@ export function TailoredList( { pendingTailor, initialData, site }: Props = {} ) }; }, [ pendingTailor, initialData ] ); + // Open the first incomplete card once the tasks first arrive on the + // wizard→list path (returning users are seeded synchronously above). Guarded + // so it runs only once and never reopens a list the user has collapsed. + useEffect( () => { + if ( ! didAutoOpen.current && tasks && tasks.length > 0 ) { + setOpenId( nextIncompleteId( tasks ) ); + didAutoOpen.current = true; + } + }, [ tasks ] ); + const visibleTasks = useMemo( () => ( tasks ?? [] ).map( task => @@ -234,15 +255,10 @@ export function TailoredList( { pendingTailor, initialData, site }: Props = {} ) setSkippedIds( prev => new Set( prev ).add( task.id ) ); }; - // The first incomplete task opens on mount; because the cards are uncontrolled - // (defaultOpen), the user can then collapse it — or all of them — without it - // reopening. Computed from the initial render (skippedIds is empty then). - const firstOpenIndex = firstIncompleteIndex( visibleTasks ); - return (
- { visibleTasks.map( ( task, index ) => ( + { visibleTasks.map( task => ( setOpenId( open ? task.id : null ) } onGetStarted={ () => handleGetStarted( task ) } onMarkComplete={ () => handleMarkComplete( task ) } onSkip={ () => handleSkip( task ) } diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/task-card.tsx b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/task-card.tsx index 69a5b1379077..77e2ad4d848a 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/task-card.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/task-card.tsx @@ -9,7 +9,8 @@ interface Props { isBusy: boolean; canStart: boolean; canMarkComplete: boolean; - defaultOpen: boolean; + isOpen: boolean; + onOpenChange: ( open: boolean ) => void; onGetStarted: () => void; onMarkComplete: () => void; onSkip: () => void; @@ -68,14 +69,18 @@ function getCtaLabel( taskId: string ): string { * the always-visible header, which is the toggle trigger); expanding reveals the * AI subtitle and the action-specific CTA / "Skip" actions. * + * Open state is controlled by the parent so the list behaves as an accordion: + * only one card is open at a time. + * * @param props - The component props. * @param props.task - The enriched task to render. * @param props.isBusy - Whether the primary action is in flight. * @param props.canStart - Whether the task has an actionable CTA destination. * @param props.canMarkComplete - Whether the task offers a "Mark as complete" button * (a complete-on-click task with no CTA destination). - * @param props.defaultOpen - Whether the card starts expanded (uncontrolled, so - * the user can then collapse it without it reopening). + * @param props.isOpen - Whether the card is expanded (controlled by the parent). + * @param props.onOpenChange - Called with the requested open state when the header + * is toggled, so the parent can enforce single-open. * @param props.onGetStarted - Called when the primary CTA is clicked. * @param props.onMarkComplete - Called when "Mark as complete" is clicked. * @param props.onSkip - Called when "Skip" is clicked. @@ -86,7 +91,8 @@ export function TaskCard( { isBusy, canStart, canMarkComplete, - defaultOpen, + isOpen, + onOpenChange, onGetStarted, onMarkComplete, onSkip, @@ -107,7 +113,11 @@ export function TaskCard( { } return ( - + From a6b3323631ff98398e09f873dc66b4f09e051a2c Mon Sep 17 00:00:00 2001 From: Copons Date: Tue, 30 Jun 2026 16:23:54 +0100 Subject: [PATCH 6/8] AI Launchpad: auto-expand the next task when one is skipped or completed Design feedback (DSGCOM-678): skipping a task should expand the next one. handleSkip now advances the accordion to the next incomplete task (via a new nextIncompleteId afterId overload that walks past the skipped id, falling back to any remaining incomplete task). Apply the same advance to handleMarkComplete so completing the open card does not leave the whole list collapsed. --- .../js/tailored-list/model.test.mts | 24 ++++++++++++++++ .../ai-launchpad/js/tailored-list/model.ts | 28 ++++++++++++++----- .../js/tailored-list/tailored-list.tsx | 17 ++++++++++- 3 files changed, 61 insertions(+), 8 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.test.mts b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.test.mts index 0bd14bf33e13..e2377838e3d9 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.test.mts +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.test.mts @@ -292,6 +292,30 @@ describe( 'nextIncompleteId', () => { const tasks = [ task( { id: 'a', completed: true } ), task( { id: 'b', completed: true } ) ]; assert.equal( nextIncompleteId( tasks ), null ); } ); + + it( 'advances to the next incomplete task after the given id', () => { + const tasks = [ + task( { id: 'a', completed: true } ), + task( { id: 'b', completed: false } ), + task( { id: 'c', completed: false } ), + ]; + assert.equal( nextIncompleteId( tasks, 'b' ), 'c' ); + } ); + + it( 'wraps back to a remaining incomplete task when none follow the given id', () => { + const tasks = [ + task( { id: 'a', completed: false } ), + task( { id: 'b', completed: false } ), + task( { id: 'c', completed: true } ), + ]; + // Skipping the last incomplete task (b) leaves only a earlier in the list. + assert.equal( nextIncompleteId( tasks, 'b' ), 'a' ); + } ); + + it( 'returns null when the given id was the only incomplete task', () => { + const tasks = [ task( { id: 'a', completed: true } ), task( { id: 'b', completed: true } ) ]; + assert.equal( nextIncompleteId( tasks, 'b' ), null ); + } ); } ); describe( 'tasksFromFixture', () => { diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts index fa2f30036c18..425ba84c08d2 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts @@ -244,15 +244,29 @@ export function isTaskActionable( } /** - * The id of the first incomplete task, driving which card the accordion - * auto-expands. Returns null when every task is complete. + * The id of the next incomplete task to auto-expand, driving the accordion's + * single-open behavior. With no `afterId`, returns the first incomplete task + * (the card that opens on mount). With `afterId` (the task just skipped or + * completed), returns the first incomplete task *after* it so focus moves down + * the list; if nothing follows, falls back to any remaining incomplete task so + * the user is always pointed at outstanding work. Returns null when every task + * is complete. * - * @param tasks - The enriched tasks (skipped tasks already coerced to completed). - * @return The first incomplete task id, or null. + * @param tasks - The enriched tasks (skipped tasks already coerced to completed). + * @param afterId - The id to advance past, or undefined to take the first. + * @return The next incomplete task id, or null. */ -export function nextIncompleteId( tasks: EnrichedTask[] ): string | null { - const next = tasks.find( task => ! task.completed ); - return next ? next.id : null; +export function nextIncompleteId( tasks: EnrichedTask[], afterId?: string ): string | null { + const incomplete = tasks.filter( task => ! task.completed ); + if ( incomplete.length === 0 ) { + return null; + } + if ( afterId === undefined ) { + return incomplete[ 0 ].id; + } + const fromIndex = tasks.findIndex( task => task.id === afterId ); + const next = incomplete.find( task => tasks.indexOf( task ) > fromIndex ); + return ( next ?? incomplete[ 0 ] ).id; } /** diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/tailored-list.tsx b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/tailored-list.tsx index ba459e32679f..662193c927c0 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/tailored-list.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/tailored-list.tsx @@ -244,6 +244,13 @@ export function TailoredList( { pendingTailor, initialData, site }: Props = {} ) setTasks( prev => prev ? prev.map( t => ( t.id === task.id ? { ...t, completed: true } : t ) ) : prev ); + // Advance the accordion to the next task, mirroring Skip — otherwise the + // just-completed card (now a non-collapsible done card) keeps openId, so + // no remaining CollapsibleCard matches and the whole list collapses. + const afterComplete = ( tasks ?? [] ).map( t => + t.id === task.id || skippedIds.has( t.id ) ? { ...t, completed: true } : t + ); + setOpenId( nextIncompleteId( afterComplete, task.id ) ); } catch { // Leave the task incomplete on failure. } finally { @@ -251,8 +258,16 @@ export function TailoredList( { pendingTailor, initialData, site }: Props = {} ) } }; + // Skipping a task marks it complete (in memory) and expands the next + // incomplete task, as the design asks. Compute the next id from the list the + // skip produces so a just-skipped task is never re-opened. const handleSkip = ( task: EnrichedTask ) => { - setSkippedIds( prev => new Set( prev ).add( task.id ) ); + const nextSkipped = new Set( skippedIds ).add( task.id ); + setSkippedIds( nextSkipped ); + const afterSkip = ( tasks ?? [] ).map( t => + nextSkipped.has( t.id ) ? { ...t, completed: true } : t + ); + setOpenId( nextIncompleteId( afterSkip, task.id ) ); }; return ( From 588c8290768d0e04dd3dae9925115a6e8263aa76 Mon Sep 17 00:00:00 2001 From: Copons Date: Tue, 30 Jun 2026 16:26:27 +0100 Subject: [PATCH 7/8] AI Launchpad: add a site appearance-editor quick link to the site preview Design feedback (DSGCOM-678): keep the preview thumbnail as a quick link into the site editor. The REST site payload returns edit_url and SitePreview renders the thumbnail as a link with a hover "Edit site" overlay. Block themes point at the Site Editor (site-editor.php); classic themes, which cannot use it, fall back to the Customizer (customize.php) so the quick link is always available. --- .../add-ai-launchpad-dsgcom-678-feedback | 4 ++ .../ai-launchpad/class-ai-launchpad-rest.php | 10 +++- .../ai-launchpad/js/tailored-list/layout.tsx | 7 ++- .../ai-launchpad/js/tailored-list/model.ts | 3 + .../js/tailored-list/site-preview.tsx | 58 ++++++++++++++----- .../ai-launchpad/js/tailored-list/style.scss | 41 +++++++++++++ .../js/tailored-list/tailored-list.tsx | 15 ++++- 7 files changed, 119 insertions(+), 19 deletions(-) create mode 100644 projects/packages/jetpack-mu-wpcom/changelog/add-ai-launchpad-dsgcom-678-feedback diff --git a/projects/packages/jetpack-mu-wpcom/changelog/add-ai-launchpad-dsgcom-678-feedback b/projects/packages/jetpack-mu-wpcom/changelog/add-ai-launchpad-dsgcom-678-feedback new file mode 100644 index 000000000000..d3dad7f70b7c --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/add-ai-launchpad-dsgcom-678-feedback @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +AI Launchpad: address first-pass design feedback (DSGCOM-678) — a single-open accordion task list that auto-expands the next task on skip or completion, WPDS state icons, a Site Editor / Customizer quick link on the site preview, full-width goal copy on mobile, more general social-task subtitles, and removal of the redundant write-3-posts task. diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/class-ai-launchpad-rest.php b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/class-ai-launchpad-rest.php index a5999a947aed..384f8c8fc92e 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/class-ai-launchpad-rest.php +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/class-ai-launchpad-rest.php @@ -316,13 +316,17 @@ public function get_data( $request = null ) { 'is_eligible' => true, // Site context the client needs: the front-end URL drives the launch-task // CTA (its host is the launch-flow site slug) and the tailored-list - // preview thumbnail; the title labels that preview. Title and - // description also pre-fill the wizard's Name and Brief description - // fields so they reflect the site's current identity. + // preview thumbnail; the title labels that preview; the editor URL is the + // preview's quick link into the Site Editor. Title and description also + // pre-fill the wizard's Name and Brief description fields so they reflect + // the site's current identity. 'site' => array( 'url' => home_url(), 'title' => get_bloginfo( 'name' ), 'description' => get_bloginfo( 'description' ), + // Block themes open the Site Editor; classic themes (which can't use it) + // fall back to the Customizer, their equivalent appearance editor. + 'edit_url' => wp_is_block_theme() ? admin_url( 'site-editor.php' ) : admin_url( 'customize.php' ), ), ); } diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/layout.tsx b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/layout.tsx index c6b31e1e4148..e03ade35501d 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/layout.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/layout.tsx @@ -10,6 +10,8 @@ interface Props { // Site context for the preview card; omitted when unknown (dev fixtures). siteUrl: string | null; siteTitle?: string | null; + // The wp-admin Site Editor URL: the preview thumbnail's quick link. + siteEditUrl?: string | null; // The left column: the task cards, or the loading skeleton. children: ReactNode; } @@ -26,10 +28,11 @@ interface Props { * @param props.progressLabel - The status line under the heading. * @param props.siteUrl - The site's front-end URL (for the preview). * @param props.siteTitle - The site name (for the preview). + * @param props.siteEditUrl - The Site Editor URL (preview quick link). * @param props.children - The left column content. * @return The layout element. */ -export function Layout( { progressLabel, siteUrl, siteTitle, children }: Props ) { +export function Layout( { progressLabel, siteUrl, siteTitle, siteEditUrl, children }: Props ) { // Without a site URL there's no preview, so collapse to a single column — // otherwise the grid reserves an empty preview track and squeezes the tasks. const hasPreview = !! siteUrl; @@ -48,7 +51,7 @@ export function Layout( { progressLabel, siteUrl, siteTitle, children }: Props ) } ) } > { children } - +
); diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts index 425ba84c08d2..bc4395c8d285 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/model.ts @@ -22,6 +22,9 @@ export interface SiteData { title?: string; // The site tagline (blogdescription), used to pre-fill the wizard description. description?: string; + // The site's appearance-editor URL, used as the preview thumbnail's quick link: + // the Site Editor on block themes, the Customizer on classic ones. + edit_url?: string | null; } /** diff --git a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/site-preview.tsx b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/site-preview.tsx index 474aec66188d..ef7871fec1d3 100644 --- a/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/site-preview.tsx +++ b/projects/packages/jetpack-mu-wpcom/src/features/ai-launchpad/js/tailored-list/site-preview.tsx @@ -1,4 +1,7 @@ import { ExternalLink } from '@wordpress/components'; +import { __ } from '@wordpress/i18n'; +import { Button } from '@wordpress/ui'; +import type { ReactNode } from 'react'; interface Props { // The site's front-end URL, from the composite read. When absent (older @@ -6,6 +9,10 @@ interface Props { siteUrl: string | null; // The site name; falls back to the domain when absent. siteTitle?: string | null; + // The site's appearance-editor URL (Site Editor on block themes, Customizer on + // classic ones). When present the thumbnail becomes a quick link; when absent + // it renders as a plain thumbnail. + siteEditUrl?: string | null; } /** @@ -14,16 +21,20 @@ interface Props { * site. The preview is a non-interactive iframe of the front end with the * banners/overlay hidden, mirroring the wp-admin dashboard's site-management * widget — it renders immediately rather than waiting on an mShots screenshot. + * When an editor URL is known the thumbnail doubles as a quick link into the + * site's appearance editor — the Site Editor on block themes, the Customizer on + * classic ones — with a hover "Edit site" overlay, matching the PoC affordance. * Rendered in both the loading and loaded states so the layout is stable across * the wizard→tailoring→list transition. Returns nothing when the site URL is * unknown (e.g. dev fixtures), so the list still renders without it. * - * @param props - Component props. - * @param props.siteUrl - The site's front-end URL. - * @param props.siteTitle - The site name (falls back to the domain). + * @param props - Component props. + * @param props.siteUrl - The site's front-end URL. + * @param props.siteTitle - The site name (falls back to the domain). + * @param props.siteEditUrl - The Site Editor URL (makes the thumbnail a quick link). * @return The preview element, or null when there's no site URL. */ -export function SitePreview( { siteUrl, siteTitle }: Props ) { +export function SitePreview( { siteUrl, siteTitle, siteEditUrl }: Props ) { if ( ! siteUrl ) { return null; } @@ -35,17 +46,38 @@ export function SitePreview( { siteUrl, siteTitle }: Props ) { // A malformed home URL still renders: fall back to the raw string. } + const thumbnail = ( +