diff --git a/src/components/home/ProjectHome.test.ts b/src/components/home/ProjectHome.test.ts index b3131f7b..48b2042c 100644 --- a/src/components/home/ProjectHome.test.ts +++ b/src/components/home/ProjectHome.test.ts @@ -61,6 +61,63 @@ describe('ProjectRow', () => { }) }) +function renderHomeMarkup() { + return renderToStaticMarkup(createElement( + SettingsUIProvider, + { value: { open: vi.fn() } }, + createElement( + TooltipProvider, + null, + createElement(I18nProvider, { i18n }, createElement(ProjectHome, { + activeProjectId: null, + projects: [], + loadState: 'ready', + loadError: null, + onOpenProject: vi.fn(), + onArchiveProject: vi.fn(), + onRestoreProject: vi.fn(), + onDeleteProject: vi.fn(), + onRenameProject: vi.fn(), + onPinProject: vi.fn(), + onStartWithBrief: vi.fn(), + onImportBoard: vi.fn(), + onRetryProjects: vi.fn(), + })), + ), + )) +} + +describe('ProjectHome Start lockup and Continue working', () => { + it('anchors the hero with the labelled Cutout symbol above the headline', () => { + const html = renderHomeMarkup() + const markStart = html.indexOf('data-cutout-brand="symbol"') + const headlineStart = html.indexOf('What will we design?') + + expect(markStart).toBeGreaterThan(-1) + expect(html).toContain('aria-label="Cutout"') + expect(markStart).toBeLessThan(headlineStart) + }) + + it('gives the composer the taller textarea floor', () => { + const html = renderHomeMarkup() + + expect(html).toContain('min-h-36') + expect(html).toContain('sm:min-h-32') + expect(html).not.toContain('min-h-32 w-full resize-none') + }) + + it('keeps Continue working on screen with an empty state when nothing is in progress', () => { + const html = renderHomeMarkup() + + expect(html).toContain('Continue working') + expect(html).toContain('id="home-continue-heading"') + expect(html).toContain('aria-labelledby="home-continue-heading"') + expect(html).toContain('text-xs font-medium tracking-wide text-muted-foreground uppercase') + expect(html).toContain('Nothing in progress yet') + expect(html).toContain('border-dashed') + }) +}) + describe('ProjectHome outcome-first start', () => { it('resets and refocuses the mounted Home composer when a new-task signal repeats the route',async()=>{host=document.createElement('div');document.body.append(host);root=createRoot(host);const render=(signal:number)=>act(()=>root?.render(createElement(SettingsUIProvider,{value:{open:vi.fn()}},createElement(TooltipProvider,null,createElement(I18nProvider,{i18n},createElement(ProjectHome,{resetToStartSignal:signal,activeProjectId:null,projects:[],loadState:'ready',loadError:null,onOpenProject:vi.fn(),onArchiveProject:vi.fn(),onRestoreProject:vi.fn(),onDeleteProject:vi.fn(),onRenameProject:vi.fn(),onPinProject:vi.fn(),onStartWithBrief:vi.fn(),onImportBoard:vi.fn(),onRetryProjects:vi.fn()}))))));render(0);const textarea=host.querySelector('textarea') as HTMLTextAreaElement,web=host.querySelector('button[aria-label="Web"]') as HTMLButtonElement;act(()=>web.click());expect(textarea.value).not.toBe('');render(1);await act(settleFocus);const reset=host.querySelector('textarea') as HTMLTextAreaElement;expect(reset.value).toBe('');expect(host.querySelector('button[aria-label="Web"]')?.getAttribute('aria-pressed')).toBe('false');expect(document.activeElement).toBe(reset)}) it('keeps presets selection-only and creates exactly once on submit',async()=>{const view=mountHome(),web=view.host.querySelector('button[aria-label="Web"]') as HTMLButtonElement,textarea=view.host.querySelector('textarea') as HTMLTextAreaElement;act(()=>{web.click();web.click();web.click()});await act(settleFocus);expect(document.activeElement).toBe(textarea);expect(view.onStartWithBrief).not.toHaveBeenCalled();expect(textarea.value).toBe('Design a responsive web experience for ');expect(web.getAttribute('aria-pressed')).toBe('true');act(()=>web.click());expect(textarea.value).toBe('');expect(web.getAttribute('aria-pressed')).toBe('false');act(()=>web.click());act(()=>textarea.closest('form')?.dispatchEvent(new Event('submit',{bubbles:true,cancelable:true})));expect(view.onStartWithBrief).toHaveBeenCalledTimes(1);expect(view.onStartWithBrief).toHaveBeenCalledWith('Design a responsive web experience for',[])}) diff --git a/src/components/home/ProjectHome.tsx b/src/components/home/ProjectHome.tsx index 83692629..d0776326 100644 --- a/src/components/home/ProjectHome.tsx +++ b/src/components/home/ProjectHome.tsx @@ -1,12 +1,12 @@ import { lazy, Suspense, + type RefObject, useEffect, useLayoutEffect, useMemo, useRef, useState, - type RefObject, } from "react"; import { Trans, useLingui } from "@lingui/react/macro"; import { withViewTransition } from "@/lib/view-transition"; @@ -19,31 +19,48 @@ import { ChevronLeft, ChevronRight, Clock3, + FileText, FolderOpen, Globe, Images, LayoutGrid, Library, Lightbulb, - List, Link2, - FileText, + List, Monitor, MoreHorizontal, Palette, Pencil, - Plus, Pin, PinOff, - X, + Plus, Search, - SearchX, Smartphone, Trash2, type LucideIcon, + X, } from "lucide-react"; import { Button } from "@/components/ui/button"; import { Textarea } from "@/components/ui/textarea"; +import { Skeleton } from "@/components/ui/skeleton"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; +import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; +import type { LocalProjectSummary } from "@/services/local/project-repository.local"; +import { cn } from "@/lib/utils"; +import { sortProjects } from "./project-order"; +import { ConnectorMenu } from "@/components/integrations/ConnectorMenu"; +import { CutoutBrandMark } from "@/components/brand/CutoutBrandMark"; +import { SidebarAccount } from "./SidebarAccount"; +import type { DesktopUpdateController } from "@/updater/service"; +import { Input } from "@/components/ui/input"; +import { filterProjects } from "./project-search"; import { AlertDialog, AlertDialogAction, @@ -54,8 +71,6 @@ import { AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; -import { Skeleton } from "@/components/ui/skeleton"; -import { Input } from "@/components/ui/input"; import { Dialog, DialogContent, @@ -64,25 +79,8 @@ import { DialogHeader, DialogTitle, } from "@/components/ui/dialog"; -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu"; -import { - Tooltip, - TooltipContent, - TooltipTrigger, -} from "@/components/ui/tooltip"; -import type { LocalProjectSummary } from "@/services/local/project-repository.local"; -import { cn } from "@/lib/utils"; -import { sortProjects } from "./project-order"; -import { filterProjects } from "./project-search"; -import { ConnectorMenu } from "@/components/integrations/ConnectorMenu"; -import { SidebarAccount } from "./SidebarAccount"; -import type { DesktopUpdateController } from "@/updater/service"; + +export { ProjectCard, ProjectRow }; const GlobalLibraryView = lazy(() => import("@/components/library/GlobalLibraryView").then((module) => ({ @@ -90,26 +88,9 @@ const GlobalLibraryView = lazy(() => })), ); -type HomeSection = - "start" | "library" | "drafts" | "projects" | "archived"; - function isDraftProject(project: LocalProjectSummary) { return project.status === "Draft" || project.status === "Empty"; } -type ProjectLoadState = "loading" | "ready" | "error"; -type DirectoryLayout = "grid" | "list"; - -const DIRECTORY_LAYOUT_KEY = "cutout.home.directory-layout"; - -function readDirectoryLayout(): DirectoryLayout { - try { - return localStorage.getItem(DIRECTORY_LAYOUT_KEY) === "list" - ? "list" - : "grid"; - } catch { - return "grid"; - } -} interface ProjectHomeProps { readonly initialSection?: Extract; @@ -346,12 +327,21 @@ export function ProjectHome({ function LibraryLoadingState() { return ( -
+
- -
- {Array.from({ length: 6 }, (_, index) => ( - + +
+ {Array.from({ length: 4 }, (_, index) => ( +
+ +
+ + +
+
))}
@@ -450,8 +440,8 @@ function WorkspaceHeader({ ] as const; return ( -
-
+
+
- + {sectionLabels[section]}
-
+
{presets.map((preset) => ( ))} @@ -760,7 +759,7 @@ function StartWorkspace({ id: "home.brief_placeholder", message: "Describe the result you want...", })} - className="min-h-32 w-full resize-none rounded-none border-0 bg-transparent px-5 pt-4 pb-2 text-base shadow-none outline-none focus-visible:border-0 focus-visible:ring-0 dark:bg-transparent sm:min-h-28 sm:text-lg" + className="min-h-36 w-full resize-none rounded-none border-0 bg-transparent px-5 pt-4 pb-2 text-base shadow-none outline-none focus-visible:border-0 focus-visible:ring-0 dark:bg-transparent sm:min-h-32 sm:text-lg" />
- {recentProjects.length ? ( -
-
-

- {t({ - id: "home.continue_heading", - message: "Continue working", - })} -

- - {t({ id: "home.local_projects", message: "Local projects" })} - -
-
+
+
+

+ {t({ + id: "home.continue_heading", + message: "Continue working", + })} +

+ + {t({ id: "home.local_projects", message: "Local projects" })} + +
+ {recentProjects.length ? ( +
{recentProjects.slice(0, 4).map((project) => ( ))}
-
- ) : null} + ) : ( +
+ +

+ {t({ + id: "home.continue_empty_title", + message: "Nothing in progress yet", + })} +

+

+ {t({ + id: "home.continue_empty_description", + message: + "Projects you start from the brief above collect here, ready to pick back up.", + })} +

+
+ )} +
); } +/** + * Raster types only. `image/*` also matches `image/svg+xml`, and an SVG behind + * an object URL is a document that can carry its own external references — the + * one attachment type worth refusing to preview. Browsers do not run scripts in + * SVG loaded through ``, so this is depth rather than a fix for a live + * hole, but the allowlist costs nothing and the previous `startsWith("image/")` + * was wider than the preview ever needed. + */ +const PREVIEWABLE_IMAGE_TYPES = new Set([ + "image/png", + "image/jpeg", + "image/gif", + "image/webp", + "image/avif", + "image/bmp", +]); + function AttachmentThumbnail({ file }: { readonly file: File }) { const [url, setUrl] = useState(); useEffect(() => { - if (!file.type.startsWith("image/")) return; + if (!PREVIEWABLE_IMAGE_TYPES.has(file.type)) return; const next = URL.createObjectURL(file); setUrl(next); return () => URL.revokeObjectURL(next); }, [file]); if (url) - return ; + return ; return file.type.startsWith("video/") ? ( ) : ( @@ -904,428 +946,220 @@ function AttachmentThumbnail({ file }: { readonly file: File }) { ); } -function ProjectDirectory({ - activeProjectId, - section, - projects, - loadState, - loadError, - onOpenProject, - onArchiveProject, - onRestoreProject, - onDeleteProject, - onRenameProject, - onPinProject, - onRetryProjects, - onStartNew, -}: { - readonly activeProjectId: string | null; - readonly section: Exclude; - readonly projects: readonly LocalProjectSummary[]; - readonly loadState: ProjectLoadState; - readonly loadError: string | null; - readonly onOpenProject: (id: string) => void; - readonly onArchiveProject: (id: string) => Promise; - readonly onRestoreProject: (id: string) => void; - readonly onDeleteProject: (id: string) => void; - readonly onRenameProject: ( - project: LocalProjectSummary, - name: string, - ) => void; - readonly onPinProject: ( - project: LocalProjectSummary, - pinned: boolean, - ) => void; - readonly onRetryProjects: () => void; - readonly onStartNew: () => void; -}) { - const { t, i18n } = useLingui(); - const [query, setQuery] = useState(""); - const [layout, setLayout] = useState(readDirectoryLayout); - const archived = section === "archived"; - const title = archived - ? t({ id: "home.archived_projects", message: "Archived projects" }) - : section === "drafts" - ? t({ id: "home.drafts", message: "Drafts" }) - : t({ id: "home.your_projects", message: "Your projects" }); - const matchedProjects = useMemo( - () => filterProjects(projects, query), - [projects, query], - ); - const selectLayout = (next: DirectoryLayout) => { - setLayout(next); - try { - localStorage.setItem(DIRECTORY_LAYOUT_KEY, next); - } catch { - // best-effort persistence only - } - }; - - return ( -
-
-

- {t({ id: "home.workspace", message: "Workspace" })} -

-
-

{title}

- {loadState === "ready" && projects.length ? ( -
- - setQuery(event.target.value)} - placeholder={t({ - id: "home.search_projects", - message: "Search projects...", - })} - aria-label={t({ - id: "home.search_projects", - message: "Search projects...", - })} - className="h-9 pl-8" - /> -
- ) : null} -
- - -
- {!archived ? ( - - ) : null} -
-
-
- {loadState === "loading" ? ( - - ) : loadState === "error" ? ( - - ) : !projects.length ? ( - - ) : !matchedProjects.length ? ( - setQuery("")} /> - ) : layout === "grid" ? ( -
- {matchedProjects.map((project) => ( - onOpenProject(project.id)} - onArchive={() => onArchiveProject(project.id)} - onRestore={() => onRestoreProject(project.id)} - onDelete={() => onDeleteProject(project.id)} - onRename={(name) => onRenameProject(project, name)} - onPin={(pinned) => onPinProject(project, pinned)} - /> - ))} -
- ) : ( -
- {matchedProjects.map((project) => ( - onOpenProject(project.id)} - onArchive={() => onArchiveProject(project.id)} - onRestore={() => onRestoreProject(project.id)} - onDelete={() => onDeleteProject(project.id)} - onRename={(name) => onRenameProject(project, name)} - onPin={(pinned) => onPinProject(project, pinned)} - /> - ))} -
- )} -
-
- ); -} - -export function ProjectRow({ +function ProjectListItem({ project, - locale, active, + compact = false, + card = false, onOpen, onArchive, - onRestore, - onDelete, onRename, onPin, }: { readonly project: LocalProjectSummary; - readonly locale: string; readonly active: boolean; + readonly compact?: boolean; + readonly card?: boolean; readonly onOpen: () => void; readonly onArchive: () => Promise; - readonly onRestore: () => void; - readonly onDelete: () => void; readonly onRename: (name: string) => void; readonly onPin: (pinned: boolean) => void; }) { return (
- - - {project.assetCount} - - - - {formatProjectDate(project.updatedAt, locale)} - - +
+ +
); } -export function ProjectCard({ - project, - locale, +function NavItem({ + icon: Icon, + label, + count, + badge, active, - onOpen, - onArchive, - onRestore, - onDelete, - onRename, - onPin, + compact, + onClick, }: { - readonly project: LocalProjectSummary; - readonly locale: string; + readonly icon: LucideIcon; + readonly label: string; + readonly count?: number; + readonly badge?: string; readonly active: boolean; - readonly onOpen: () => void; - readonly onArchive: () => Promise; - readonly onRestore: () => void; - readonly onDelete: () => void; - readonly onRename: (name: string) => void; - readonly onPin: (pinned: boolean) => void; + readonly compact: boolean; + readonly onClick: () => void; }) { - const { t } = useLingui(); - const pinned = Boolean(project.pinnedAt); - const archived = Boolean(project.archivedAt); - return ( -
- - {!archived ? ( -
+ + {label} + + {badge ? ( + - -
+ {badge} + ) : null} -
- - - - {project.name} - - - {t({ - id: "home.edited_date", - message: `Edited ${formatProjectDate(project.updatedAt, locale)}`, - })} - + {count !== undefined ? ( + + {count} -
- -
-
-
+ ) : null} + ); } -function CardThumbnail({ thumbnail }: { readonly thumbnail: Blob }) { - const url = useObjectUrl(thumbnail); - - return url ? ( - - ) : null; +function projectCounts(projects: readonly LocalProjectSummary[]) { + const active = projects.filter((project) => !project.archivedAt); + return { + active: active.length, + drafts: active.filter(isDraftProject).length, + archived: projects.filter((project) => Boolean(project.archivedAt)).length, + }; } -function ProjectListItem({ +export type HomeSection = + "start" | "library" | "drafts" | "projects" | "archived"; +export type ProjectLoadState = "loading" | "ready" | "error"; + +function ProjectRow({ project, + locale, active, - compact = false, - card = false, onOpen, onArchive, + onRestore, + onDelete, onRename, onPin, }: { readonly project: LocalProjectSummary; + readonly locale: string; readonly active: boolean; - readonly compact?: boolean; - readonly card?: boolean; readonly onOpen: () => void; readonly onArchive: () => Promise; + readonly onRestore: () => void; + readonly onDelete: () => void; readonly onRename: (name: string) => void; readonly onPin: (pinned: boolean) => void; }) { return (
-
- -
+ + + {project.assetCount} + + + + {formatProjectDate(project.updatedAt, locale)} + +
); } +function CardThumbnail({ thumbnail }: { readonly thumbnail: Blob }) { + const url = useObjectUrl(thumbnail); + + return url ? ( + + ) : null; +} + function ProjectActions({ project, compact = false, @@ -1401,7 +1235,7 @@ function ProjectActions({ message: `More actions for ${project.name}`, })} > - + @@ -1586,7 +1420,7 @@ function ProjectMark({ ) : ( @@ -1651,43 +1485,368 @@ function ProjectStatusDescription({ return `${statusMessages[project.status]} · ${project.assetCount} ${assetLabel}`; } -function DirectorySkeleton() { - return ( -
- {Array.from({ length: 4 }, (_, index) => ( -
- -
- - -
-
- ))} -
- ); +function formatProjectDate(timestamp: number, locale: string) { + return new Intl.DateTimeFormat(locale, { + month: "short", + day: "numeric", + }).format(new Date(timestamp)); } -function DirectoryError({ - error, - onRetry, -}: { - readonly error: string; +type DirectoryLayout = "grid" | "list"; + +const DIRECTORY_LAYOUT_KEY = "cutout.home.directory-layout"; + +function readDirectoryLayout(): DirectoryLayout { + try { + return localStorage.getItem(DIRECTORY_LAYOUT_KEY) === "list" + ? "list" + : "grid"; + } catch { + return "grid"; + } +} + +function ProjectDirectory({ + activeProjectId, + section, + projects, + loadState, + loadError, + onOpenProject, + onArchiveProject, + onRestoreProject, + onDeleteProject, + onRenameProject, + onPinProject, + onRetryProjects, + onStartNew, +}: { + readonly activeProjectId: string | null; + readonly section: Exclude; + readonly projects: readonly LocalProjectSummary[]; + readonly loadState: ProjectLoadState; + readonly loadError: string | null; + readonly onOpenProject: (id: string) => void; + readonly onArchiveProject: (id: string) => Promise; + readonly onRestoreProject: (id: string) => void; + readonly onDeleteProject: (id: string) => void; + readonly onRenameProject: ( + project: LocalProjectSummary, + name: string, + ) => void; + readonly onPinProject: ( + project: LocalProjectSummary, + pinned: boolean, + ) => void; + readonly onRetryProjects: () => void; + readonly onStartNew: () => void; +}) { + const { t, i18n } = useLingui(); + const [query, setQuery] = useState(""); + const [layout, setLayout] = useState(readDirectoryLayout); + const archived = section === "archived"; + const title = archived + ? t({ id: "home.archived_projects", message: "Archived projects" }) + : section === "drafts" + ? t({ id: "home.drafts", message: "Drafts" }) + : t({ id: "home.your_projects", message: "Your projects" }); + const description = archived + ? t({ + id: "gallery.archived_description", + message: + "Work you have set aside. Restore a project to bring it back into the workspace.", + }) + : section === "drafts" + ? t({ + id: "gallery.drafts_description", + message: + "Ideas that have not produced results yet. Pick one up where you left off.", + }) + : t({ + id: "gallery.projects_description", + message: + "Everything you have made in this workspace. Pinned work stays at the top.", + }); + const matchedProjects = useMemo( + () => filterProjects(projects, query), + [projects, query], + ); + const selectLayout = (next: DirectoryLayout) => { + setLayout(next); + try { + localStorage.setItem(DIRECTORY_LAYOUT_KEY, next); + } catch { + // best-effort persistence only + } + }; + + return ( +
+
+

+ {t({ id: "home.workspace", message: "Workspace" })} +

+
+

{title}

+
+ {!archived ? ( + + ) : null} +
+ {/* `basis-full` is what breaks the description onto its own line; + the measure cap lives on the inner span so max-width cannot clamp + the flex basis back down and cancel the wrap. */} +

+ {description} +

+
+ +
+
+ {loadState === "ready" && projects.length ? ( +
+ + setQuery(event.target.value)} + placeholder={t({ + id: "home.search_projects", + message: "Search projects...", + })} + aria-label={t({ + id: "home.search_projects", + message: "Search projects...", + })} + className="pl-8" + /> +
+ ) : null} +
+ + +
+
+
+ {loadState === "loading" ? ( + + ) : loadState === "error" ? ( + + ) : !projects.length ? ( + + ) : !matchedProjects.length ? ( + setQuery("")} /> + ) : layout === "grid" ? ( +
+ {matchedProjects.map((project) => ( + onOpenProject(project.id)} + onArchive={() => onArchiveProject(project.id)} + onRestore={() => onRestoreProject(project.id)} + onDelete={() => onDeleteProject(project.id)} + onRename={(name) => onRenameProject(project, name)} + onPin={(pinned) => onPinProject(project, pinned)} + /> + ))} +
+ ) : ( +
+ {matchedProjects.map((project) => ( + onOpenProject(project.id)} + onArchive={() => onArchiveProject(project.id)} + onRestore={() => onRestoreProject(project.id)} + onDelete={() => onDeleteProject(project.id)} + onRename={(name) => onRenameProject(project, name)} + onPin={(pinned) => onPinProject(project, pinned)} + /> + ))} +
+ )} +
+
+ ); +} + +function ProjectCard({ + project, + locale, + active, + onOpen, + onArchive, + onRestore, + onDelete, + onRename, + onPin, +}: { + readonly project: LocalProjectSummary; + readonly locale: string; + readonly active: boolean; + readonly onOpen: () => void; + readonly onArchive: () => Promise; + readonly onRestore: () => void; + readonly onDelete: () => void; + readonly onRename: (name: string) => void; + readonly onPin: (pinned: boolean) => void; +}) { + const { t } = useLingui(); + const pinned = Boolean(project.pinnedAt); + const archived = Boolean(project.archivedAt); + + return ( +
+ + {!archived ? ( +
+ +
+ ) : null} +
+ + + + {project.name} + + + {t({ + id: "home.edited_date", + message: `Edited ${formatProjectDate(project.updatedAt, locale)}`, + })} + + +
+ +
+
+
+ ); +} + +function DirectorySkeleton() { + return ( +
+ {Array.from({ length: 4 }, (_, index) => ( +
+ +
+ + +
+
+ ))} +
+ ); +} + +function DirectoryError({ + error, + onRetry, +}: { + readonly error: string; readonly onRetry: () => void; }) { const { t } = useLingui(); return ( -
+
- -
-

+ +
+

{t({ id: "home.load_failed_title", message: "Could not load projects", })}

-

{error}

+

{error}

); } - -function NavItem({ - icon: Icon, - label, - count, - badge, - active, - compact, - onClick, -}: { - readonly icon: LucideIcon; - readonly label: string; - readonly count?: number; - readonly badge?: string; - readonly active: boolean; - readonly compact: boolean; - readonly onClick: () => void; -}) { - return ( - - ); -} - -function projectCounts(projects: readonly LocalProjectSummary[]) { - const active = projects.filter((project) => !project.archivedAt); - return { - active: active.length, - drafts: active.filter(isDraftProject).length, - archived: projects.filter((project) => Boolean(project.archivedAt)).length, - }; -} - -function formatProjectDate(timestamp: number, locale: string) { - return new Intl.DateTimeFormat(locale, { - month: "short", - day: "numeric", - }).format(new Date(timestamp)); -} diff --git a/src/components/release-notes/WhatsNewDialog.test.ts b/src/components/release-notes/WhatsNewDialog.test.ts index 5cd33320..652010bb 100644 --- a/src/components/release-notes/WhatsNewDialog.test.ts +++ b/src/components/release-notes/WhatsNewDialog.test.ts @@ -20,6 +20,17 @@ describe("What's New dialog contract", () => { expect(source).toContain("restoreFocusTo.focus()"); }); + it("uses the two-column dialog shell with a brand panel that collapses on mobile", () => { + expect(source).toContain("sm:max-w-3xl"); + expect(source).toContain("sm:grid-cols-[minmax(0,17rem)_minmax(0,1fr)]"); + expect(source).toContain("