This commit is contained in:
Your Name committed 2026-06-28 07:58:15 -04:00
1 parent e405af357c
commit e1cf9e62e7
51 files changed
+2598 -168

No files matched your search

@@ -0,0 +1,47 @@
# BRIEFING — 2026-06-28T10:52:10Z
## Mission
Investigate layout configurations (layout: standard/split_screen and imagePosition: left/right) for the Editorial Carousel section across types, MCP server, backend, admin UI, and renderers.
## 🔒 My Identity
- Archetype: teamwork_preview_explorer
- Roles: Explorer, Investigator, Synthesizer
- Working directory: /home/ti/git/homaroma/.agents/teamwork_preview_explorer_m1_1
- Original parent: c02d550e-454f-4c49-9b30-4ae4587b6e54
- Milestone: m1_1
## 🔒 Key Constraints
- Read-only investigation — do NOT implement
- Strictly CODE_ONLY network mode: no external web or HTTP client requests
- Only write to agent folder (.agents/teamwork_preview_explorer_m1_1)
- Verify claims before concluding
## Current Parent
- Conversation ID: c02d550e-454f-4c49-9b30-4ae4587b6e54
- Updated: 2026-06-28T10:53:00Z
## Investigation State
- **Explored paths**:
- packages/types/index.ts
- apps/mcp-server/section-toolkit.ts
- apps/backend/src/routes/pages.ts
- apps/admin/src/components/page-builder/EditorialCarouselForm.tsx
- apps/spa/src/components/DynamicEditorialCarousel.tsx
- apps/website/src/components/dynamic/DynamicPageRenderer.tsx
- **Key findings**:
- Schema extension needs `layout` and `imagePosition` as optional keys.
- MCP tool properties configuration must expose both options.
- Backend validation handles the schemas seamlessly via `safeParse`.
- The Admin UI requires form elements that update local payload state.
- The client and website renderers require grid columns `35%` / `65%` and mobile stacked behavior using CSS grid/order styling.
- **Unexplored areas**: none (all files in scope explored)
## Key Decisions Made
- Layout controls and logic designed for complete backwards compatibility.
- CSS order property determined to be the cleanest approach for mobile layout stacking.
## Artifact Index
- /home/ti/git/homaroma/.agents/teamwork_preview_explorer_m1_1/ORIGINAL_REQUEST.md — Original User Request
- /home/ti/git/homaroma/.agents/teamwork_preview_explorer_m1_1/BRIEFING.md — Briefing file
- /home/ti/git/homaroma/.agents/teamwork_preview_explorer_m1_1/progress.md — Progress file
- /home/ti/git/homaroma/.agents/teamwork_preview_explorer_m1_1/handoff.md — Comprehensive investigation report
@@ -0,0 +1,15 @@
## 2026-06-28T10:52:05Z
You are the read-only exploration agent (teamwork_preview_explorer).
Your working directory is: /home/ti/git/homaroma/.agents/teamwork_preview_explorer_m1_1
Your objective is to:
1. Examine packages/types/index.ts to locate EditorialCarouselSectionPayloadSchema and EditorialCarouselSectionPayload.
2. Examine apps/mcp-server/section-toolkit.ts to see how update_editorial_carousel tool is structured and how payload properties are exposed.
3. Examine apps/backend/src/routes/pages.ts to verify how section validation and parsing occur.
4. Examine apps/admin/src/components/page-builder/EditorialCarouselForm.tsx to see how the form fields are laid out, how the form controls update the editingSection state, and how we should add controls for layout (standard/split_screen) and imagePosition (left/right).
5. Examine apps/spa/src/components/DynamicEditorialCarousel.tsx and apps/website/src/components/dynamic/DynamicPageRenderer.tsx (specifically EditorialCarouselSection) to see how they render slides, and plan how they should render standard vs split_screen. Focus on:
- Desktop layout for split_screen: 35% text (contains title, category, description, and link button) / 65% image split. Arrows on far edges.
- Mobile layout for split_screen: Stacked layout (Image top, text bottom).
- Theme styling: Ensuring colors are dynamically resolved from theme tokens (e.g. using tailwind classes or bgClass/textClass from theme config instead of hardcoded hex values).
- Compatibility: Do not break existing 'standard' layouts.
6. Write a comprehensive handoff report (handoff.md) in your working directory.
Your parent is parent (conversation ID c02d550e-454f-4c49-9b30-4ae4587b6e54). Use send_message to report when done. Do NOT edit any source code.
@@ -0,0 +1 @@
# Explorer M1 Metadata Directory
@@ -0,0 +1,515 @@
# Handoff Report: Editorial Carousel split_screen Layout Investigation
This handoff report summarizes findings, analysis, and implementation proposals for introducing the `split_screen` layout configuration to the `EditorialCarousel` page section.
---
## 1. Observation
We directly investigated five code locations:
### 1.1 Types Definition
- **File**: `packages/types/index.ts`
- **Lines**: 375-388
- **Current schema code**:
```typescript
export const EditorialCarouselSectionPayloadSchema = z.object({
title: LocalizedStringSchema,
backgroundColor: z.string().optional(),
textColor: z.string().optional(),
slides: z.array(z.object({
category: LocalizedStringSchema,
title: LocalizedStringSchema,
description: LocalizedStringSchema,
imageUrl: z.string().min(1),
buttonText: LocalizedStringSchema.optional(),
buttonLink: z.string().optional(),
})).min(1)
});
export type EditorialCarouselSectionPayload = z.infer<typeof EditorialCarouselSectionPayloadSchema>;
```
### 1.2 MCP Server Tool
- **File**: `apps/mcp-server/section-toolkit.ts`
- **Lines**: 265-292
- **Current properties structure**:
```typescript
name: "update_editorial_carousel",
sectionType: "EDITORIAL_CAROUSEL",
description: `Update an EDITORIAL_CAROUSEL section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. title, description, and slides are supported. Example payload: { "sectionId": 12, "title": ${localizedStringExample}, "description": ${localizedStringExample} }`,
properties: {
sectionId: { type: "number", description: "The numeric ID of the section to update" },
title: { ...localizedStringSchema, description: `Localized section title, for example ${localizedStringExample}` },
description: { ...localizedStringSchema, description: `Localized section description, for example ${localizedStringExample}` },
backgroundColor: { type: "string", description: "Plain string background color in hex or rgba" },
textColor: { type: "string", description: "Plain string text color in hex or rgba" },
slides: { ... }
}
```
- **Execution (Lines 550-578)**: The tool retrieves the payload properties defined in the config, extracts them from incoming `params`, fetches the existing section from backend (`GET /pages/sections/:id?hydrate=false`), merges the two payloads (`mergedPayload = { ...existingRes.data.payload, ...payload }`), and calls `PUT /pages/sections/:id`.
### 1.3 Backend Section Validation
- **File**: `apps/backend/src/routes/pages.ts`
- **Lines**: 451-465 (under `PUT /sections/:sectionId`), 536-578 (inside `validateSectionPayload`)
- **Validation logic**:
```typescript
function validateSectionPayload(
type: string,
payload: any,
): { success: boolean; issues?: z.ZodIssue[]; data?: any } {
// ...
switch (type.toUpperCase()) {
// ...
case "EDITORIAL_CAROUSEL":
schema = EditorialCarouselSectionPayloadSchema;
break;
// ...
}
const res = schema.safeParse(payload);
if (!res.success) {
return { success: false, issues: res.error.issues };
}
return { success: true, data: res.data };
}
```
Validation output is saved directly into the database: `payload: finalPayload = validationResult.data`.
### 1.4 Admin Panel Page Builder Form
- **File**: `apps/admin/src/components/page-builder/EditorialCarouselForm.tsx`
- **Lines**: 43-86
- **Current properties updating pattern**: Uses `setEditingSection` by merging state:
```tsx
onChange={(e) => setEditingSection({
...editingSection,
payload: { ...(editingSection.payload || {}), backgroundColor: e.target.value }
})}
```
### 1.5 Client Renderers (SPA and Website)
- **SPA Component**: `apps/spa/src/components/DynamicEditorialCarousel.tsx` (Lines 77-145)
- Uses `Swiper` with standard slides-per-view breakpoints (slidesPerView: 1, 2, or 3).
- Uses theme token-mapped utility classes: `bg-pop-bg` (resolved via `bgMap`), `text-pop-black` (resolved via `textMap`).
- **Website Renderer**: `apps/website/src/components/dynamic/DynamicPageRenderer.tsx` (Lines 415-481)
- Uses a horizontal flex scroll container with `scrollContainerRef`.
- Buttons call `scrollBy(400)` to scroll horizontally.
- Render card style uses `.ui-card` with `w-[85vw] md:w-[350px]`.
---
## 2. Logic Chain
1. **Schema Extension (Types)**: To allow the layout properties (`layout` and `imagePosition`) to be saved, we must add them to `EditorialCarouselSectionPayloadSchema` in `packages/types/index.ts`. They must be `.optional()` so that existing sections in the database remain valid.
2. **Backend Validation Validation & Tool Exposure**: Since `pages.ts` validates payload records using `safeParse` against the types schema, extending the schema will automatically allow the new parameters to pass backend validation. In the MCP server, we must also update the properties structure of `update_editorial_carousel` in `apps/mcp-server/section-toolkit.ts` to expose `layout` and `imagePosition` to AI agents.
3. **Form Fields (Admin)**: To enable administrative control over these layout configurations, we can add two select fields (`Layout` and `Image Position`) to the styling settings section in `EditorialCarouselForm.tsx` using the same merging style as `backgroundColor`. To enhance UX, the `Image Position` field can be disabled unless `layout === 'split_screen'`.
4. **Renderer Behavior (SPA & Website)**:
- For `layout === 'split_screen'`, we render exactly 1 slide at a time.
- For the SPA component, Swiper configuration must adapt (`slidesPerView={1}` and no desktop breakpoints).
- For the Next.js website component, slide width must stretch to 100% (`w-full`), and `scrollBy` must scroll by the full width of the container `scrollContainerRef.current.clientWidth`.
- On desktop, CSS Grid will split the slide `35%` text / `65%` image. CSS `order` property will arrange them so that `imagePosition: 'right'` renders text-left/image-right, and `imagePosition: 'left'` renders image-left/text-right.
- On mobile, CSS `order` will force the image to stack on top and text on the bottom.
---
## 3. Caveats
- **Existing Data Migrations**: Since no database migrations are needed (fields are optional and schema validates them safely as optional), existing databases do not need prisma migrations.
- **Swiper Navigation Controls**: In `split_screen` layout, standard Swiper arrows overlap the image/text on the far edges. Swiper handles pagination bullet styling, which has been configured to match `bg-pop-pink`.
---
## 4. Conclusion
The layout configuration changes are backward-compatible and require updates to:
1. `packages/types/index.ts`
2. `apps/mcp-server/section-toolkit.ts`
3. `apps/admin/src/components/page-builder/EditorialCarouselForm.tsx`
4. `apps/spa/src/components/DynamicEditorialCarousel.tsx`
5. `apps/website/src/components/dynamic/DynamicPageRenderer.tsx`
### Proposed Code Diffs
#### 1. packages/types/index.ts
```diff
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({
category: LocalizedStringSchema,
title: LocalizedStringSchema,
```
#### 2. apps/mcp-server/section-toolkit.ts
```diff
{
name: "update_editorial_carousel",
sectionType: "EDITORIAL_CAROUSEL",
- description: `Update an EDITORIAL_CAROUSEL section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. title, description, and slides are supported. Example payload: { "sectionId": 12, "title": ${localizedStringExample}, "description": ${localizedStringExample} }`,
+ description: `Update an EDITORIAL_CAROUSEL section. This tool performs a partial update/merge (PATCH). Omitted fields are preserved. The 'sectionId' argument correlates to the 'id' field in the fetched payload. title, description, layout, imagePosition, and slides are supported. Example payload: { "sectionId": 12, "title": ${localizedStringExample}, "layout": "split_screen", "imagePosition": "right" }`,
properties: {
sectionId: { type: "number", description: "The numeric ID of the section to update" },
title: { ...localizedStringSchema, description: `Localized section title, for example ${localizedStringExample}` },
description: { ...localizedStringSchema, description: `Localized section description, for example ${localizedStringExample}` },
backgroundColor: { type: "string", description: "Plain string background color in hex or rgba" },
textColor: { type: "string", description: "Plain string text color in hex or rgba" },
+ 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)" },
slides: {
```
#### 3. apps/admin/src/components/page-builder/EditorialCarouselForm.tsx
```diff
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Background Color</label>
...
</div>
<div className="space-y-2">
<label className="text-[10px] uppercase font-bold tracking-widest text-slate-400">Text Color</label>
...
</div>
</div>
+ <div className="grid grid-cols-2 gap-4">
+ <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 (Split Screen)</label>
+ <select
+ disabled={editingSection.payload?.layout !== 'split_screen'}
+ value={editingSection.payload?.imagePosition || 'right'}
+ onChange={(e) => setEditingSection({
+ ...editingSection,
+ payload: { ...(editingSection.payload || {}), imagePosition: e.target.value }
+ })}
+ className="admin-input w-full disabled:opacity-50"
+ >
+ <option value="right">Right (Text Left, Image Right)</option>
+ <option value="left">Left (Image Left, Text Right)</option>
+ </select>
+ </div>
+ </div>
+
<div className="space-y-4">
```
#### 4. apps/spa/src/components/DynamicEditorialCarousel.tsx
```diff
export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCarouselProps) {
const { i18n } = useTranslation();
const lang = i18n.language;
+ const layout = payload.layout || 'standard';
+ const imagePosition = payload.imagePosition || 'right';
+ const isSplitScreen = layout === 'split_screen';
+
const title = getLocalizedField(payload.title, lang);
const slides = (payload.slides || []).map((slide) => ({
...slide,
@@ -86,13 +90,13 @@ export default function DynamicEditorialCarousel({ payload }: DynamicEditorialCa
<div className="relative select-none pb-12">
<Swiper
modules={[Autoplay, Pagination, Navigation]}
- spaceBetween={30}
+ spaceBetween={isSplitScreen ? 0 : 30}
slidesPerView={1}
- breakpoints={{
+ breakpoints={isSplitScreen ? undefined : {
640: { slidesPerView: 2 },
1024: { slidesPerView: 3 }
- }}
+ }}
pagination={{ clickable: true, bulletActiveClass: 'swiper-pagination-bullet-active !bg-pop-pink' }}
navigation={true}
autoplay={{ delay: 5000, disableOnInteraction: false }}
className="w-full"
>
{slides.map((slide, idx) => (
<SwiperSlide key={idx} className="h-auto flex flex-col">
- <div className="ui-dark-card group flex h-full flex-col transition-all duration-300 hover:border-pop-pink/30">
- <div className="aspect-[4/3] overflow-hidden relative">
- <img
- src={slide.imageUrl}
- alt={slide.title}
- className="w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
- onError={(e) => {
- (e.target as HTMLImageElement).style.display = 'none';
- }}
- />
- </div>
-
- <div className="p-6 md:p-8 flex flex-col flex-1 justify-between space-y-6">
- <div className="space-y-3">
- <span className="block text-[10px] font-bold uppercase tracking-[0.2em] text-pop-pink">
- {slide.category}
- </span>
- <h3 className="font-serif text-xl font-light tracking-wide text-pop-black">
- {renderTitle(slide.title) || slide.title}
- </h3>
- <p className="line-clamp-3 text-xs font-light leading-relaxed text-pop-black/70 md:text-sm">
- {slide.description}
- </p>
- </div>
-
- {slide.buttonText && slide.buttonLink && (
- <div className="pt-2">
- <Link
- to={slide.buttonLink}
- className="inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-pop-pink hover:text-pop-black transition-colors group/link"
- >
- {slide.buttonText}
- <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
- </Link>
- </div>
- )}
- </div>
- </div>
+ {isSplitScreen ? (
+ <div className={`grid grid-cols-1 md:grid-cols-${imagePosition === 'left' ? '[65%_35%]' : '[35%_65%]'} w-full items-stretch border border-white/10 rounded-xl overflow-hidden bg-night-950/20`}>
+ {/* Text Column */}
+ <div className={`p-8 md:p-12 lg:p-16 flex flex-col justify-center space-y-6 order-2 md:order-${imagePosition === 'left' ? '2' : '1'}`}>
+ <span className="block text-[10px] font-bold uppercase tracking-[0.2em] text-pop-pink">
+ {slide.category}
+ </span>
+ <h3 className="font-serif text-2xl md:text-3xl lg:text-4xl font-light tracking-wide text-pop-black">
+ {renderTitle(slide.title) || slide.title}
+ </h3>
+ <p className="text-sm font-light leading-relaxed text-pop-black/70">
+ {slide.description}
+ </p>
+ {slide.buttonText && slide.buttonLink && (
+ <div className="pt-2">
+ <Link
+ to={slide.buttonLink}
+ className="inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-pop-pink hover:text-pop-black transition-colors group/link"
+ >
+ {slide.buttonText}
+ <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
+ </Link>
+ </div>
+ )}
+ </div>
+ {/* Image Column */}
+ <div className={`relative aspect-[4/3] md:aspect-auto md:h-full min-h-[300px] overflow-hidden order-1 md:order-${imagePosition === 'left' ? '1' : '2'}`}>
+ <img
+ src={slide.imageUrl}
+ alt={slide.title}
+ className="w-full h-full object-cover transition-transform duration-700 ease-out hover:scale-105"
+ onError={(e) => {
+ (e.target as HTMLImageElement).style.display = 'none';
+ }}
+ />
+ </div>
+ </div>
+ ) : (
+ <div className="ui-dark-card group flex h-full flex-col transition-all duration-300 hover:border-pop-pink/30">
+ <div className="aspect-[4/3] overflow-hidden relative">
+ <img
+ src={slide.imageUrl}
+ alt={slide.title}
+ className="w-full h-full object-cover transition-transform duration-700 ease-out group-hover:scale-105"
+ onError={(e) => {
+ (e.target as HTMLImageElement).style.display = 'none';
+ }}
+ />
+ </div>
+ <div className="p-6 md:p-8 flex flex-col flex-1 justify-between space-y-6">
+ <div className="space-y-3">
+ <span className="block text-[10px] font-bold uppercase tracking-[0.2em] text-pop-pink">
+ {slide.category}
+ </span>
+ <h3 className="font-serif text-xl font-light tracking-wide text-pop-black">
+ {renderTitle(slide.title) || slide.title}
+ </h3>
+ <p className="line-clamp-3 text-xs font-light leading-relaxed text-pop-black/70 md:text-sm">
+ {slide.description}
+ </p>
+ </div>
+ {slide.buttonText && slide.buttonLink && (
+ <div className="pt-2">
+ <Link
+ to={slide.buttonLink}
+ className="inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest text-pop-pink hover:text-pop-black transition-colors group/link"
+ >
+ {slide.buttonText}
+ <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
+ </Link>
+ </div>
+ )}
+ </div>
+ </div>
+ )}
</SwiperSlide>
))}
```
#### 5. apps/website/src/components/dynamic/DynamicPageRenderer.tsx
```diff
function EditorialCarouselSection({ payload, lang }: { payload: Record<string, unknown>, lang: string }) {
const i18nLang = useLang()
const resolvedLang = lang === 'all' ? i18nLang : lang
const title = localizedFromRecord(payload, 'title', resolvedLang)
const slides = Array.isArray(payload.slides) ? payload.slides.map(toRecord) : []
const backgroundColor = typeof payload.backgroundColor === 'string' ? payload.backgroundColor : undefined
const textColor = typeof payload.textColor === 'string' ? payload.textColor : undefined
const bgClass = backgroundColor ? `bg-pop-${backgroundColor}` : 'bg-surface-page'
const textClass = textColor ? `text-pop-${textColor}` : 'text-text-main'
+ const layout = typeof payload.layout === 'string' ? payload.layout : 'standard'
+ const imagePosition = typeof payload.imagePosition === 'string' ? payload.imagePosition : 'right'
+ const isSplitScreen = layout === 'split_screen'
+
const scrollContainerRef = useRef<HTMLDivElement>(null)
const scrollBy = (offset: number) => {
if (scrollContainerRef.current) {
- scrollContainerRef.current.scrollBy({ left: offset, behavior: 'smooth' })
+ if (isSplitScreen) {
+ const width = scrollContainerRef.current.clientWidth
+ scrollContainerRef.current.scrollBy({ left: offset > 0 ? width : -width, behavior: 'smooth' })
+ } else {
+ scrollContainerRef.current.scrollBy({ left: offset, behavior: 'smooth' })
+ }
}
}
if (!slides.length) return null
@@ -432,38 +441,755
{slides.map((slide, index) => {
const slideTitle = localizedFromRecord(slide, 'title', resolvedLang)
const description = localizedFromRecord(slide, 'description', resolvedLang)
const category = localizedFromRecord(slide, 'category', resolvedLang)
const buttonText = localizedFromRecord(slide, 'buttonText', resolvedLang)
const buttonLink = typeof slide.buttonLink === 'string' ? slide.buttonLink : ''
const imageUrl = typeof slide.imageUrl === 'string' ? slide.imageUrl : ''
- return (
- <article key={index} className="ui-card overflow-hidden group w-[85vw] md:w-[350px] flex-none snap-start">
- {imageUrl ? (
- <div className="relative aspect-[4/3] overflow-hidden">
- <Image
- src={imageUrl}
- alt={slideTitle || 'Editorial slide'}
- fill
- unoptimized
- className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
- />
- </div>
- ) : null}
- <div className="space-y-4 p-6 flex flex-col flex-1 justify-between">
- <div>
- {category && <span className="text-[10px] font-bold uppercase tracking-[0.24em] text-brand-primary block">{category}</span>}
- {slideTitle && <h3 className="text-xl font-bold text-text-main mt-6 group-hover:text-brand-primary transition-colors">{renderAccentTitle(slideTitle)}</h3>}
- {description && <p className="text-base text-muted-text mt-3 whitespace-pre-wrap">{description}</p>}
- </div>
- {buttonText && buttonLink && (
- <div className="pt-2">
- <Link href={buttonLink} className="ui-section-dark-link inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-colors group/link">
- {buttonText} <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
- </Link>
- </div>
- )}
- </div>
- </article>
- )
+ return isSplitScreen ? (
+ <article key={index} className={`w-full flex-none snap-start grid grid-cols-1 md:grid-cols-${imagePosition === 'left' ? '[65%_35%]' : '[35%_65%]'} items-stretch border border-border-muted rounded-xl overflow-hidden`}>
+ {/* Text Column */}
+ <div className={`p-8 md:p-12 lg:p-16 flex flex-col justify-center space-y-6 order-2 md:order-${imagePosition === 'left' ? '2' : '1'}`}>
+ {category && <span className="text-[10px] font-bold uppercase tracking-[0.24em] text-brand-primary block">{category}</span>}
+ {slideTitle && <h3 className="text-2xl md:text-3xl lg:text-4xl font-bold text-text-main group-hover:text-brand-primary transition-colors">{renderAccentTitle(slideTitle)}</h3>}
+ {description && <p className="text-sm font-light leading-relaxed text-muted-text whitespace-pre-wrap">{description}</p>}
+ {buttonText && buttonLink && (
+ <div className="pt-2">
+ <Link href={buttonLink} className="ui-section-dark-link inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-colors group/link">
+ {buttonText} <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
+ </Link>
+ </div>
+ )}
+ </div>
+ {/* Image Column */}
+ {imageUrl ? (
+ <div className={`relative aspect-[4/3] md:aspect-auto md:h-full min-h-[300px] overflow-hidden order-1 md:order-${imagePosition === 'left' ? '1' : '2'}`}>
+ <Image
+ src={imageUrl}
+ alt={slideTitle || 'Editorial slide'}
+ fill
+ unoptimized
+ className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
+ />
+ </div>
+ ) : null}
+ </article>
+ ) : (
+ <article key={index} className="ui-card overflow-hidden group w-[85vw] md:w-[350px] flex-none snap-start">
+ {imageUrl ? (
+ <div className="relative aspect-[4/3] overflow-hidden">
+ <Image
+ src={imageUrl}
+ alt={slideTitle || 'Editorial slide'}
+ fill
+ unoptimized
+ className="h-full w-full object-cover transition duration-700 group-hover:scale-105"
+ />
+ </div>
+ ) : null}
+ <div className="space-y-4 p-6 flex flex-col flex-1 justify-between">
+ <div>
+ {category && <span className="text-[10px] font-bold uppercase tracking-[0.24em] text-brand-primary block">{category}</span>}
+ {slideTitle && <h3 className="text-xl font-bold text-text-main mt-6 group-hover:text-brand-primary transition-colors">{renderAccentTitle(slideTitle)}</h3>}
+ {description && <p className="text-base text-muted-text mt-3 whitespace-pre-wrap">{description}</p>}
+ </div>
+ {buttonText && buttonLink && (
+ <div className="pt-2">
+ <Link href={buttonLink} className="ui-section-dark-link inline-flex items-center gap-2 text-xs font-bold uppercase tracking-widest transition-colors group/link">
+ {buttonText} <span className="group-hover/link:translate-x-1 transition-transform">&rarr;</span>
+ </Link>
+ </div>
+ )}
+ </div>
+ </article>
+ )
})}
</div>
```
---
## 5. Verification Method
To verify these changes after implementation:
### 5.1 Compilation and Type Check
Run the build command for the monorepo from the root directory to ensure type safety:
```bash
npm run build
```
### 5.2 Dynamic Form Verification
1. Access the Admin Panel and navigate to the Page Builder.
2. Select or create an `Editorial Carousel` section.
3. Verify that the `Layout` option displays `Standard Carousel` and `Split Screen`.
4. Select `Split Screen` and confirm that `Image Position` is enabled (it should be disabled when `Standard Carousel` is chosen).
5. Switch `Image Position` between `Left` and `Right` and save the page.
### 5.3 Frontend Verification (Desktop and Mobile)
1. Verify the layout renders correctly on desktop:
- For `split_screen`, check that exactly 1 slide occupies the width.
- Text takes up `35%` of the width, and the Image takes up `65%`.
- When `imagePosition` is `right`, text is on the left, image on the right.
- When `imagePosition` is `left`, image is on the left, text on the right.
2. Verify layout on mobile:
- Viewport width < 768px should stack the image on top and the text on the bottom for both left and right positions.
3. Test compatibility by validating that sections with standard layouts remain untouched (3 columns on desktop, 1.5/2 cards on tablet/mobile).
@@ -0,0 +1,11 @@
# Progress Log
Last visited: 2026-06-28T10:53:11Z
- [x] Initialized ORIGINAL_REQUEST.md and BRIEFING.md
- [x] Investigate types/index.ts (EditorialCarouselSectionPayloadSchema & EditorialCarouselSectionPayload)
- [x] Investigate apps/mcp-server/section-toolkit.ts
- [x] Investigate apps/backend/src/routes/pages.ts
- [x] Investigate apps/admin/src/components/page-builder/EditorialCarouselForm.tsx
- [x] Investigate apps/spa/src/components/DynamicEditorialCarousel.tsx and apps/website/src/components/dynamic/DynamicPageRenderer.tsx
- [x] Synthesize findings and write handoff.md