10 KiB
10 KiB
Handoff Report — Split-Screen Editorial Carousel Review
This handoff report summarizes the comprehensive review and stress-test of the Split-Screen Editorial Carousel implementation.
1. Observation
Direct observations of the codebase:
File 1: packages/types/index.ts
- Lines 375-380: Defines
EditorialCarouselSectionPayloadSchemawith optionallayoutandimagePositionenums:
export const EditorialCarouselSectionPayloadSchema = z.object({
title: LocalizedStringSchema,
backgroundColor: z.string().optional(),
textColor: z.string().optional(),
layout: z.enum(['standard', 'split_screen']).optional(),
imagePosition: z.enum(['left', 'right']).optional(),
slides: z.array(z.object({
...
File 2: apps/mcp-server/section-toolkit.ts
- Lines 268-276: Defines properties for the MCP update tool
update_editorial_carousel:
layout: { type: "string", enum: ["standard", "split_screen"], description: "Layout style of the carousel (standard or split_screen)" },
imagePosition: { type: "string", enum: ["left", "right"], description: "Position of the image relative to text when layout is split_screen (left or right)" },
- Line 293:
requiredonly lists["sectionId"], confirminglayoutandimagePositionare optional parameters.
File 3: apps/admin/src/components/page-builder/EditorialCarouselForm.tsx
- Lines 88-118: Implements dropdown controls for Layout and Image Position:
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Layout</label>
<select
value={editingSection.payload?.layout || 'standard'}
onChange={(e) => setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), layout: e.target.value }
})}
className="admin-input w-full"
>
<option value="standard">Standard Carousel</option>
<option value="split_screen">Split Screen</option>
</select>
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Image Position</label>
<select
value={editingSection.payload?.imagePosition || 'left'}
disabled={editingSection.payload?.layout !== 'split_screen'}
onChange={(e) => setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), imagePosition: e.target.value }
})}
className="admin-input w-full disabled:opacity-50"
>
<option value="left">Left</option>
<option value="right">Right</option>
</select>
</div>
File 4: apps/spa/src/components/DynamicEditorialCarousel.tsx
- Lines 105-125: Implements grid columns split and DOM ordering based on
imagePosition:
<div className={`group grid grid-cols-1 border border-pop-black/10 bg-night-900 rounded-3xl overflow-hidden shadow-sm transition-all duration-300 hover:border-pop-pink/30 ${
imagePosition === 'left' ? 'md:grid-cols-[65%_35%]' : 'md:grid-cols-[35%_65%]'
}`}>
<div className={`overflow-hidden relative ${
imagePosition === 'left' ? 'order-1 md:order-1' : 'order-1 md:order-2'
} aspect-[4/3] md:aspect-auto h-64 md:h-full min-h-[300px]`}>
<img
src={slide.imageUrl}
...
<div className={`p-6 md:p-8 flex flex-col justify-between space-y-6 ${
imagePosition === 'left' ? 'order-2 md:order-2' : 'order-2 md:order-1'
}`}>
- Lines 92-97: Handles Swiper settings conditionally based on layout style to ensure standard carousels are untouched:
spaceBetween={isSplitScreen ? 0 : 30}
slidesPerView={1}
breakpoints={isSplitScreen ? undefined : {
640: { slidesPerView: 2 },
1024: { slidesPerView: 3 }
}}
File 5: apps/website/src/components/dynamic/DynamicPageRenderer.tsx
- Lines 449-475: Implements the identical layout split, order classes, and responsive behavior for SSR in the Next.js app:
className={
isSplitScreen
? `group grid grid-cols-1 border border-line-primary/20 bg-surface-card rounded-2xl overflow-hidden shadow-card w-full flex-none snap-start ${
imagePosition === 'left' ? 'md:grid-cols-[65%_35%]' : 'md:grid-cols-[35%_65%]'
}`
: 'ui-card overflow-hidden group w-[85vw] md:w-[350px] flex-none snap-start'
}
...
{imageUrl ? (
<div className={`relative aspect-[4/3] md:aspect-auto overflow-hidden h-64 md:h-full min-h-[300px] ${
imagePosition === 'left' ? 'order-1 md:order-1' : 'order-1 md:order-2'
}`}>
...
<div className={`space-y-4 p-6 flex flex-col justify-between ${
imagePosition === 'left' ? 'order-2 md:order-2' : 'order-2 md:order-1'
}`}>
2. Logic Chain
-
Desktop Layout correctness:
imagePosition === 'left'appliesmd:grid-cols-[65%_35%], where the image hasmd:order-1and the text hasmd:order-2. The image appears on the left (65% width) and the text appears on the right (35% width).imagePosition === 'right'appliesmd:grid-cols-[35%_65%], where the image hasmd:order-2and the text hasmd:order-1. The text appears on the left (35% width) and the image appears on the right (65% width).- Therefore, the split is exactly 35% text and 65% image on desktop.
-
Mobile Layout correctness:
- On mobile screens (below
mdbreakpoint), both components applyorder-1to the image andorder-2to the text regardless of theimagePositionsetting. This guarantees that the layout stacks with the image at the top and the text at the bottom.
- On mobile screens (below
-
Backward Compatibility:
layoutdefaults tostandardif undefined or null.- For standard layout,
<Swiper>preserves breakpoints (showing 2 or 3 cards per view) and usesspaceBetween={30}, rendering standard slide cards (ui-dark-cardon SPA,ui-cardon website). - For split-screen layout,
<Swiper>displays exactly 1 slide per view (slidesPerView={1}) withspaceBetween={0}, turning the carousel into a structured narrative slideshow.
-
Theme Compliance:
- The React SPA uses
text-pop-pink,bg-pop-bg,text-pop-black,border-pop-black/10, andhover:border-pop-pink/30. These classes dynamically adapt to the dynamic theme variables defined intailwind.config.js(colors.pop). - The Next.js website uses
bg-pop-*andtext-pop-*as well as website theme classes (bg-surface-card,border-line-primary/20). These resolve to CSS variables declared under the@themedirective inglobals.css(Tailwind v4 standard). - No hardcoded hex values (e.g.
#FF6B6B) are present in the new layout code.
- The React SPA uses
-
Typings / Zod Schema Alignment:
- Typings in
packages/types/index.tsusez.enum([...]).optional()for layout and imagePosition. - MCP toolkit definitions in
apps/mcp-server/section-toolkit.tsdefine them as string enums and exclude them from therequiredproperties list, keeping them optional for API payload validation.
- Typings in
3. Caveats
- Since no automated test suites exist in the repository (e.g., Jest or Vitest files), validation was completed via syntax compilation and production bundle builds.
- Local server state integration: The Next.js static page generator threw a standard
DYNAMIC_SERVER_USAGEwarning for/sitemap.xmldue torevalidate: 0fetch calls when compiling statically, which is standard behavior in this setup and does not impact the editorial carousel rendering.
4. Conclusion
The implementation of the Split-Screen Editorial Carousel is fully correct, robust, backwards compatible, and compliant with the design systems in both the SPA and website apps. All enums and type definitions are perfectly aligned.
Verdict: APPROVE
5. Verification Method
To independently verify:
- Ensure the monorepo builds cleanly:
- Check types checking:
cd apps/mcp-server && bun run check - Build SPA:
cd apps/spa && bun run build - Build Website:
cd apps/website && bun run build - Build Admin:
cd apps/admin && bun run build
- Check types checking:
- Inspect the rendered HTML output or component files listed above.
Quality Review Report
Verdict: APPROVE
Verified Claims
- Desktop layout has 35% text and 65% image split → verified via code inspection of
md:grid-cols-[65%_35%]/md:grid-cols-[35%_65%]→ PASS - Mobile layout stacks image on top of text → verified via
order-1(image) andorder-2(text) on mobile styles → PASS imagePositionleft puts image on left, right puts image on right → verified via CSS grid order alignment → PASS- Backward compatibility: Standard layouts are carousels → verified Swiper breakpoint fallback for standard layouts → PASS
- Theme compliance: SPA uses pop-* classes, Website uses Tailwind v4 theme classes, no hardcoded colors → verified Tailwind definitions → PASS
- Typings/Zod schema alignment: layout & imagePosition are optional enums → verified
packages/types/index.tsandsection-toolkit.ts→ PASS
Adversarial Challenge Report
Overall risk assessment: LOW
Challenges
[Low] Challenge 1: Empty slide payloads
- Assumption challenged: Assumes backend always provides at least one slide.
- Attack scenario: If a page section is saved with 0 slides, the Swiper component could crash.
- Mitigation: The code contains guard clauses
if (slides.length === 0) return null;in both SPA and website renderers. It fails gracefully by rendering nothing.
[Low] Challenge 2: Broken Image URL
- Assumption challenged: Assumes
slide.imageUrlis always a valid image resource. - Attack scenario: A broken URL shows a broken image placeholder.
- Mitigation: SPA implements
onError={(e) => { (e.target as HTMLImageElement).style.display = 'none'; }}to hide the image element, maintaining UI aesthetics. Website handles empty image URLs gracefully by rendering null.