mirror of
https://github.com/Since-3/since3-portal.git
synced 2026-10-12 20:51:17 +00:00
76 lines
3.1 KiB
TypeScript
76 lines
3.1 KiB
TypeScript
"use client"
|
||
|
||
import { MotionConfig } from "motion/react"
|
||
import { Children, isValidElement, useEffect, type CSSProperties, type ReactNode } from "react"
|
||
import { cn } from "@/lib/utils"
|
||
import { themeStyle } from "../../lib/theme"
|
||
import type { FontPair, TemplateContent, TemplateDefinition, TemplateStyle } from "../../lib/types"
|
||
import { libraryFont } from "../font-library"
|
||
import { useDemo } from "./demo"
|
||
|
||
const imageStyles: Record<TemplateStyle["images"], string> = {
|
||
original: "",
|
||
mono: "[&_img]:grayscale",
|
||
warm: "[&_img]:sepia-[.3] [&_img]:saturate-[1.2]",
|
||
soft: "[&_img]:saturate-[.75] [&_img]:brightness-[1.05] [&_img]:contrast-[.95]",
|
||
contrast: "[&_img]:contrast-[1.2] [&_img]:saturate-[1.15]",
|
||
}
|
||
|
||
// Sektionen mit data-order="<schlüssel>" tauschen ihre Plätze nach content.order; alle anderen bleiben, wo sie sind
|
||
function arrange(children: ReactNode, order: string[]) {
|
||
const items = Children.toArray(children)
|
||
const keyOf = (node: ReactNode) => (isValidElement(node) ? (node.props as Record<string, unknown>)["data-order"] : undefined)
|
||
const slots = items.flatMap((node, i) => (typeof keyOf(node) === "string" ? [i] : []))
|
||
const rank = (node: ReactNode) => {
|
||
const index = order.indexOf(keyOf(node) as string)
|
||
return index < 0 ? order.length : index
|
||
}
|
||
const movable = slots.map((i) => items[i]).sort((a, b) => rank(a) - rank(b))
|
||
const arranged = [...items]
|
||
slots.forEach((slot, n) => {
|
||
arranged[slot] = movable[n]
|
||
})
|
||
return arranged
|
||
}
|
||
|
||
// Rahmen jeder Vorlage: Palette und Schriften als CSS-Variablen (bg-t-*, font-t-*), Bildstil, Bewegung,
|
||
// Sektions-Reihenfolge, sanftes Scrollen zu Ankern und – nur im Portal – Entwurfs-Hinweis und KI-Chat (shared/demo.tsx).
|
||
// Fixe Leisten am unteren Rand (z. B. mobile CTA) schieben Badge und Chat über --t-safe-bottom nach oben.
|
||
export function TemplateShell({ def, content, fonts, className, children }: {
|
||
def: TemplateDefinition
|
||
content: TemplateContent
|
||
fonts: FontPair
|
||
className?: string
|
||
children: ReactNode
|
||
}) {
|
||
const demo = useDemo()
|
||
|
||
useEffect(() => {
|
||
const root = document.documentElement
|
||
const previous = root.style.scrollBehavior
|
||
root.style.scrollBehavior = "smooth"
|
||
return () => {
|
||
root.style.scrollBehavior = previous
|
||
}
|
||
}, [])
|
||
|
||
const pair = (content.fonts.includes(":") && libraryFont(content.fonts)) || fonts
|
||
const style = { ...themeStyle(def, content), "--t-display": pair.display, "--t-body": pair.body } as CSSProperties
|
||
|
||
return (
|
||
<MotionConfig reducedMotion={content.style.motion === "subtle" ? "always" : "user"}>
|
||
<div
|
||
style={style}
|
||
className={cn(
|
||
pair.className,
|
||
"relative min-h-dvh overflow-x-clip bg-t-bg font-t-body text-t-fg antialiased selection:bg-t-accent selection:text-t-accent-fg",
|
||
imageStyles[content.style.images],
|
||
className
|
||
)}
|
||
>
|
||
{arrange(children, content.order)}
|
||
{demo?.overlay}
|
||
</div>
|
||
</MotionConfig>
|
||
)
|
||
}
|