feat(layout-system basics): Added component-map, component-props, configloader and renderer.

Added vt-template for nav and poc example footer.
Added ste.medusa-starter.design.json.
Probable starting point (main)/layout.tsx refactored to use dynamic layout renderer instead of hardcoded template.
This commit is contained in:
2025-11-19 18:19:25 +01:00
parent b7c67b5834
commit 51d6ee2051
8 changed files with 367 additions and 21 deletions
+76
View File
@@ -0,0 +1,76 @@
import CartMismatchBanner from "@modules/layout/components/cart-mismatch-banner"
import FreeShippingPriceNudge from "@modules/shipping/components/free-shipping-price-nudge"
import LocalizedClientLink from "@modules/common/components/localized-client-link"
import CartButton from "@modules/layout/components/cart-button"
import { DynamicLayoutRenderer } from "./renderer"
import React from "react"
import VtNav from "@modules/layout/templates/vt-nav"
import VtFooter from "@modules/layout/templates/vt-footer"
export interface LayoutComponentDefinition {
props?: Record<string, any>
children?: LayoutComponentNode[]
}
//maps key = componentName to value = props + children
export type LayoutComponentNode = Record<string, LayoutComponentDefinition>
export interface LayoutContext {
customer: any;
cart: any;
shippingOptions: any[];
contentChildren: React.ReactNode;
}
export type ComponentRenderer = {
render: (entry: LayoutComponentDefinition, ctx: LayoutContext) => React.ReactNode
}
// Utility, wenn eine Komponente nur props hat und keine children
const simple = (Component: React.ComponentType<any>): ComponentRenderer => ({
render: (entry) => <Component {...entry.props} />
})
// Helper für Kinder-Rendering
const renderChildren = (entry: LayoutComponentDefinition, ctx: LayoutContext) =>
entry.children ? <DynamicLayoutRenderer nodes={entry.children} context={ctx} /> : null
// Component Map
export const componentMap: Record<string, ComponentRenderer> = {
Nav: {
render: (entry: any, ctx: LayoutContext) => ( <VtNav nodes={entry.children} context={ctx} /> ),
},
Div: {
render: (entry: any, ctx: LayoutContext) => (
<div {...entry.props}>
{entry.children ? <DynamicLayoutRenderer nodes={entry.children} context={ctx} /> : null}
</div>
)
},
LocalizedClientLink: {
render: (entry: any) => (
<LocalizedClientLink {...entry.props}>{entry.props.label}</LocalizedClientLink>
)
},
CartButton: simple(CartButton),
Suspense: {
render: (entry: any, ctx: LayoutContext) => (
<React.Suspense
fallback={
entry.props?.fallback ? (
<DynamicLayoutRenderer nodes={entry.props.fallback} context={ctx} />
) : null
}
>
{entry.children ? <DynamicLayoutRenderer nodes={entry.children} context={ctx} /> : null}
</React.Suspense>
),
},
CartMismatchBanner: simple(CartMismatchBanner),
FreeShippingPriceNudge: simple(FreeShippingPriceNudge),
PropsChildren: {
render: (_props, ctx) => ctx.contentChildren, // PageLayout's props.children
},
Footer: simple(VtFooter),
}
+1
View File
@@ -0,0 +1 @@
export interface FooterProps { copyrightText?: string }
+10
View File
@@ -0,0 +1,10 @@
import fs from "fs"
import path from "path"
const fileName = "ste.medusa-starter.design.json";
export async function loadDesignConfig() {
const filePath = path.join(process.cwd(), "config", fileName)
const fileData = await fs.promises.readFile(filePath, "utf-8")
return JSON.parse(fileData)
}
+16
View File
@@ -0,0 +1,16 @@
import React from "react"
import { LayoutComponentNode, LayoutContext, componentMap } from "./component-map"
export interface DynamicLayoutRendererProps {
nodes: LayoutComponentNode[]
context: LayoutContext
}
export function DynamicLayoutRenderer({ nodes, context } : DynamicLayoutRendererProps) {
return nodes.map((entry, index) => {
const [key, value] = Object.entries(entry)[0] as [string, any]
const component = componentMap[key]
if (!component) return null
return <React.Fragment key={index}>{component.render(value, context)}</React.Fragment>
})
}