+ Zugänge von Since3 selbst – verschlüsselt im Supabase Vault und nur für Admins sichtbar.
+ Jedes Anzeigen oder Kopieren eines Passworts wird protokolliert.
+
+
+
+
+
+
+
+
+
+
+
+
+
+ )
+}
diff --git a/modules/shared/data/sidebar-menu-content.ts b/modules/shared/data/sidebar-menu-content.ts
index d6dcd75..f0ad7fd 100644
--- a/modules/shared/data/sidebar-menu-content.ts
+++ b/modules/shared/data/sidebar-menu-content.ts
@@ -3,6 +3,7 @@ import {
FoldersIcon,
InfoIcon,
KeyIcon,
+ KeyRoundIcon,
LayoutDashboardIcon,
LayoutTemplateIcon,
LucideIcon,
@@ -34,6 +35,11 @@ export const adminMenu: MenuItem[] = [
url: "/admin/templates",
icon: LayoutTemplateIcon
},
+ {
+ title: "Internal Accounts",
+ url: "/admin/internal-accounts",
+ icon: KeyRoundIcon
+ },
{
title: "Settings",
url: "/admin/settings",
diff --git a/modules/templates/ai/knowledge.ts b/modules/templates/ai/knowledge.ts
index ad8b21a..313e6bc 100644
--- a/modules/templates/ai/knowledge.ts
+++ b/modules/templates/ai/knowledge.ts
@@ -1,8 +1,28 @@
import { brandName } from "../lib/content"
-import type { TemplateContent, TemplateDefinition } from "../lib/types"
+import type { FieldDef, ListFieldDef, SectionData, TemplateContent, TemplateDefinition } from "../lib/types"
const clean = (value: string) => value.replace(/\*/g, "").replace(/\s*\n\s*/g, " · ").trim()
+type PageSection = { label: string; fields: (FieldDef | ListFieldDef)[]; data: SectionData | undefined; hidden: boolean }
+
+// Feste Sektionen der Vorlage und – im Baukasten – die Bausteine in ihrer Reihenfolge
+function pageSections(def: TemplateDefinition, content: TemplateContent): PageSection[] {
+ const fixed = def.sections.map((section) => ({
+ label: section.label,
+ fields: section.fields,
+ data: content.data[section.key],
+ hidden: content.hidden.includes(section.key),
+ }))
+ const library = new Map(def.builder?.library.map((block) => [block.type, block]))
+ const blocks = content.blocks.flatMap((block) => {
+ const definition = library.get(block.type)
+ if (!definition) return []
+ const menu = typeof block.data.menu === "string" ? block.data.menu.trim() : ""
+ return [{ label: menu || definition.label, fields: definition.fields, data: block.data, hidden: block.hidden }]
+ })
+ return [...fixed, ...blocks]
+}
+
export function businessName(def: TemplateDefinition, content: TemplateContent) {
return brandName(content) ?? def.name
}
@@ -11,9 +31,9 @@ export function businessName(def: TemplateDefinition, content: TemplateContent)
export function buildKnowledge(def: TemplateDefinition, content: TemplateContent) {
const parts = [`Betrieb: ${businessName(def, content)}`, `Branche: ${def.industry}`]
- for (const section of def.sections) {
- const data = content.data[section.key]
- if (!data || content.hidden.includes(section.key)) continue
+ for (const section of pageSections(def, content)) {
+ const data = section.data
+ if (!data || section.hidden) continue
const lines: string[] = []
for (const field of section.fields) {
@@ -43,8 +63,8 @@ export function buildKnowledge(def: TemplateDefinition, content: TemplateContent
// Bild-Links der Variante, z. B. für die Post-Vorschauen im Content-Generator
export function collectImages(def: TemplateDefinition, content: TemplateContent) {
const images: string[] = []
- for (const section of def.sections) {
- const data = content.data[section.key]
+ for (const section of pageSections(def, content)) {
+ const data = section.data
if (!data) continue
for (const field of section.fields) {
const value = data[field.key]
diff --git a/modules/templates/catalog/atelier/definition.ts b/modules/templates/catalog/atelier/definition.ts
new file mode 100644
index 0000000..00268d4
--- /dev/null
+++ b/modules/templates/catalog/atelier/definition.ts
@@ -0,0 +1,124 @@
+import { unsplash } from "../../lib/content"
+import { defineBuilder } from "../blocks/builder"
+
+export const atelier = defineBuilder({
+ key: "atelier",
+ name: "Atelier",
+ industry: "Universell: Manufaktur & Boutique",
+ description: "Edel wie ein Magazin: Cover-Schriftzug, Bild öffnet sich wie Jalousien, Bilder färben sich unter der Maus ein, Kollektion im Querlauf und ein Wort voller Bild. Aus Bausteinen – frei änderbar.",
+ cover: unsplash("1555041469-a586c61ea9bc", 1200),
+ samples: {
+ inquiry: "Guten Tag, wir suchen einen Esstisch aus Eiche für acht Personen, gern mit passender Bank – nach Maß für unser neues Haus. Ist so etwas möglich, und wie lange wäre die Lieferzeit? Herzliche Grüße, Familie Brandt",
+ topic: "Hinter den Kulissen: So entsteht Sessel Ove",
+ },
+ palettes: [
+ { key: "elfenbein", label: "Elfenbein", bg: "#f4f0e8", fg: "#1a1714", muted: "#756d63", surface: "#e9e2d6", accent: "#8c3b2a" },
+ { key: "salbei", label: "Salbei", bg: "#eef0ea", fg: "#1b211d", muted: "#66705f", surface: "#e0e5da", accent: "#4f6b4f" },
+ { key: "tinte", label: "Tinte", dark: true, bg: "#14110f", fg: "#efe8de", muted: "#9a8f82", surface: "#1f1b18", accent: "#c99a5b" },
+ ],
+ fonts: [
+ { key: "cormorant", label: "Cormorant & Manrope" },
+ { key: "garamond", label: "EB Garamond & Host Grotesk" },
+ ],
+ brand: {
+ name: "Lindqvist",
+ tagline: "Manufaktur für Möbel & Objekte · seit 1924",
+ email: "atelier@lindqvist.de",
+ phone: "+49 40 3861 2290",
+ address: "Bei den Mühren 4, 20457 Hamburg",
+ logo: "",
+ },
+ seed: [
+ { type: "nav-masthead", data: { edition: "Ausgabe Nº 12 · Herbst/Winter", cta: "Termin vereinbaren", href: "#besuch" } },
+ {
+ type: "hero-editorial",
+ data: {
+ eyebrow: "Manufaktur seit 1924",
+ title: "Gemacht,\num zu *bleiben.*",
+ text: "Möbel und Objekte aus massivem Holz und feinen Stoffen, gefertigt von Hand in unserer Hamburger Werkstatt – für Räume, die Geschichten erzählen.",
+ cta: "Kollektion entdecken",
+ href: "#kollektion",
+ image: unsplash("1555041469-a586c61ea9bc", 1600),
+ caption: "Sofa Mira · Samt in Moosgrün",
+ },
+ },
+ {
+ type: "text-bild",
+ data: {
+ menu: "Manufaktur",
+ eyebrow: "Die Manufaktur",
+ title: "Hundert Jahre\n*Handwerk.*",
+ text: "Was 1924 als kleine Tischlerei begann, ist heute eine Manufaktur mit 22 Meisterinnen und Meistern. Wir arbeiten mit Hölzern aus Norddeutschland, Stoffen aus europäischen Webereien und einer Geduld, die man jedem Stück ansieht.",
+ quote: "Ein gutes Möbel erkennt man nicht beim Kauf, sondern nach dreißig Jahren.",
+ signature: "Henrik Lindqvist",
+ image: unsplash("1426927308491-6380b6a9936f", 1400),
+ caption: "Hobel und Stecheisen in unserer Werkstatt in der Speicherstadt",
+ },
+ },
+ {
+ type: "projekte-querlauf",
+ data: {
+ menu: "Kollektion",
+ eyebrow: "Kollektion",
+ title: "Die *Kollektion*",
+ items: [
+ { title: "Sessel Ove", category: "Samt, Senfgelb", year: "2026", image: unsplash("1586023492125-27b2c045efd7", 1600) },
+ { title: "Sofa Mira", category: "Bouclé, Kreide", year: "2026", image: unsplash("1493663284031-b7e3aefcae8e", 1600) },
+ { title: "Stuhl Elin", category: "Eiche, Leinen", year: "2025", image: unsplash("1567538096630-e0c55bd6374c", 1600) },
+ { title: "Vasen Sora", category: "Steinzeug, handgedreht", year: "2025", image: unsplash("1520408222757-6f9f95d87d5d", 1600) },
+ { title: "Schalen Kumo", category: "Keramik, glasiert", year: "2024", image: unsplash("1610701596007-11502861dcfa", 1600) },
+ ],
+ },
+ },
+ {
+ type: "wortmaske",
+ data: {
+ eyebrow: "Was uns antreibt",
+ word: "Handwerk",
+ text: "Rund sechzig Arbeitsstunden stecken in jedem Sessel. Keine Maschine ersetzt das Gefühl in den Händen, das weiß, wann eine Fuge sitzt und eine Oberfläche fertig ist.",
+ image: unsplash("1493106641515-6b5631de4bb9", 2000),
+ },
+ },
+ {
+ type: "ablauf-spalten",
+ data: {
+ menu: "Werkstatt",
+ eyebrow: "Vom Baum zum Möbel",
+ title: "In vier *Schritten.*",
+ items: [
+ { title: "Auswahl", text: "Jedes Brett wählen wir selbst aus – aus nachhaltiger Forstwirtschaft in Norddeutschland." },
+ { title: "Trocknung", text: "Zwei Jahre lagert das Holz an der Luft, bevor es verarbeitet wird." },
+ { title: "Fertigung", text: "Verbindungen werden von Hand gezinkt, geleimt und geschliffen – ganz ohne Metall." },
+ { title: "Veredelung", text: "Natürliche Öle und Wachse schützen die Oberfläche und lassen das Holz atmen." },
+ ],
+ },
+ },
+ {
+ type: "stimmen-zitat",
+ data: {
+ eyebrow: "Stimmen",
+ items: [
+ { quote: "Der Tisch steht seit zwanzig Jahren in unserer Küche. Er wird mit jedem Jahr schöner.", author: "Familie Albers", role: "Hamburg" },
+ { quote: "Man spürt bei jedem Stück, wie viel Zeit und Liebe darin steckt. Für unsere Projekte gibt es keine bessere Adresse.", author: "Clara Nyström", role: "Innenarchitektin" },
+ { quote: "Sessel Ove ist das Möbel, das jeder Besuch zuerst ausprobieren möchte.", author: "Jonathan Weiß", role: "Kunde seit 2019" },
+ ],
+ },
+ },
+ {
+ type: "kontakt-formular",
+ data: {
+ menu: "Besuch",
+ eyebrow: "Besuch",
+ title: "Besuchen Sie\nunsere *Werkstatt.*",
+ text: "Wir zeigen Ihnen gern, wie Ihr Möbel entsteht – nach Vereinbarung auch samstags.",
+ topics: "Werkstattbesuch, Maßanfertigung, Beratung, Sonstiges",
+ button: "Termin anfragen",
+ success: "Danke! Wir melden uns, um Ihren Besuch zu planen.",
+ hours: "Di–Sa, 10–18 Uhr",
+ },
+ },
+ { type: "footer-wortmarke", data: { text: "Möbel für Generationen. Gemacht in Hamburg.", cta: "Werkstatt besuchen", href: "#besuch" } },
+ ],
+})
+
+export type AtelierData = typeof atelier.defaults
diff --git a/modules/templates/catalog/atelier/fonts.ts b/modules/templates/catalog/atelier/fonts.ts
new file mode 100644
index 0000000..03cd6c5
--- /dev/null
+++ b/modules/templates/catalog/atelier/fonts.ts
@@ -0,0 +1,4 @@
+import type { FontPair } from "../../lib/types"
+import { pairs } from "../blocks/fonts"
+
+export const fonts: Record = { cormorant: pairs.cormorant, garamond: pairs.garamond }
diff --git a/modules/templates/catalog/atelier/template.tsx b/modules/templates/catalog/atelier/template.tsx
new file mode 100644
index 0000000..7110900
--- /dev/null
+++ b/modules/templates/catalog/atelier/template.tsx
@@ -0,0 +1,10 @@
+"use client"
+
+import type { TemplateProps } from "../../lib/types"
+import { BlockPage } from "../blocks/page"
+import { atelier, type AtelierData } from "./definition"
+import { fonts } from "./fonts"
+
+export function AtelierTemplate({ content }: TemplateProps) {
+ return
+}
diff --git a/modules/templates/catalog/aurora/definition.ts b/modules/templates/catalog/aurora/definition.ts
new file mode 100644
index 0000000..30c15cf
--- /dev/null
+++ b/modules/templates/catalog/aurora/definition.ts
@@ -0,0 +1,157 @@
+import { unsplash } from "../../lib/content"
+import { defineBuilder } from "../blocks/builder"
+
+export const aurora = defineBuilder({
+ key: "aurora",
+ name: "Aurora",
+ industry: "Universell: Moderne Dienstleister",
+ description: "Leuchtend und modern: Farbwolken und Lichtkugel mit kreisenden Schlagworten, Bento-Kacheln mit Lichtkegel, Zeitstrahl, endlose Stimmen-Wand und Pakete mit Lichtrand. Aus Bausteinen – frei änderbar.",
+ cover: unsplash("1614850523459-c2f4c699c52e", 1200),
+ samples: {
+ inquiry: "Hallo Lumen-Team, wir sind ein Maschinenbauer mit 80 Mitarbeitenden und wollen unsere Angebotserstellung mit KI automatisieren. Können Sie uns dabei begleiten – und was würde ein erster Workshop kosten? Beste Grüße, Jan Petersen",
+ topic: "5 Prozesse, die jedes Unternehmen 2026 automatisieren sollte",
+ },
+ palettes: [
+ { key: "nacht", label: "Nacht", dark: true, bg: "#070913", fg: "#eef0ff", muted: "#8d93b5", surface: "#10142a", accent: "#7c5cff" },
+ { key: "perle", label: "Perle", bg: "#f7f7fb", fg: "#11121c", muted: "#5f6380", surface: "#ececf6", accent: "#5b5bf7" },
+ { key: "polar", label: "Polar", dark: true, bg: "#04120f", fg: "#e9fff8", muted: "#7fa79b", surface: "#0b1f1a", accent: "#2ee6a8" },
+ ],
+ fonts: [
+ { key: "funnel", label: "Funnel Display & Funnel Sans" },
+ { key: "park", label: "Parkinsans & Rethink Sans" },
+ ],
+ brand: {
+ name: "Lumen",
+ tagline: "Beratung für digitale Exzellenz",
+ email: "hallo@lumen-beratung.de",
+ phone: "+49 30 6098 4410",
+ address: "Torstraße 109, 10119 Berlin",
+ logo: "",
+ },
+ seed: [
+ { type: "nav-glas", data: { cta: "Gespräch buchen", href: "#kontakt" } },
+ {
+ type: "hero-aurora",
+ data: {
+ badge: "Neu: KI-Workshops für Teams",
+ title: "Wir machen Unternehmen\nbereit für *morgen.*",
+ text: "Strategie, Design und Technologie aus einer Hand – für Unternehmen, die nicht abwarten, sondern gestalten wollen.",
+ cta: "Kostenloses Erstgespräch",
+ href: "#kontakt",
+ ctaSecondary: "Leistungen ansehen",
+ hrefSecondary: "#leistungen",
+ keywords: "Strategie, Design, KI, Automatisierung, Wachstum, Daten",
+ },
+ },
+ {
+ type: "logos",
+ data: {
+ title: "Mehr als 120 Unternehmen vertrauen uns",
+ items: [
+ { name: "Nordwerk", logo: "" },
+ { name: "Velora", logo: "" },
+ { name: "Kraftbau", logo: "" },
+ { name: "Brightfield", logo: "" },
+ { name: "Okuma", logo: "" },
+ { name: "Tessin & Co", logo: "" },
+ { name: "Lumio", logo: "" },
+ ],
+ },
+ },
+ {
+ type: "leistungen-bento",
+ data: {
+ eyebrow: "Leistungen",
+ title: "Alles, was Wachstum\n*möglich* macht.",
+ text: "Wir begleiten Sie von der ersten Idee bis zum messbaren Ergebnis – mit einem festen Team, das Ihr Geschäft versteht.",
+ items: [
+ { title: "Strategie & Beratung", text: "Wir finden heraus, wo Ihr größter Hebel liegt, und machen daraus einen Fahrplan mit klaren Zielen.", image: unsplash("1531297484001-80022131f5a1", 1400) },
+ { title: "Design & Marke", text: "Auftritte, die man sich merkt – online wie offline.", image: "" },
+ { title: "Software & KI", text: "Maßgeschneiderte Tools, die Ihrem Team Arbeit abnehmen.", image: "" },
+ { title: "Automatisierung", text: "Wiederkehrende Abläufe laufen von selbst – zuverlässig und nachvollziehbar.", image: "" },
+ { title: "Daten & Analyse", text: "Dashboards, die zeigen, was wirklich zählt.", image: "" },
+ { title: "Wachstum & Marketing", text: "Kampagnen, die messbar neue Kundschaft bringen.", image: "" },
+ ],
+ },
+ },
+ {
+ type: "ablauf-linie",
+ data: {
+ eyebrow: "So arbeiten wir",
+ title: "Vom ersten Gespräch\nzum *Ergebnis.*",
+ items: [
+ { title: "Kennenlernen", text: "Im kostenlosen Erstgespräch verstehen wir Ihr Geschäft, Ihre Ziele und Ihre größten Hürden.", duration: "Woche 1" },
+ { title: "Analyse & Strategie", text: "Wir priorisieren nach Wirkung und Aufwand und legen gemeinsam einen klaren Fahrplan fest.", duration: "2–3 Wochen" },
+ { title: "Umsetzung", text: "In kurzen Sprints entstehen Ergebnisse, die Sie sofort sehen und testen können.", duration: "6–12 Wochen" },
+ { title: "Wachstum", text: "Wir messen, lernen und verbessern laufend – damit der Erfolg bleibt.", duration: "fortlaufend" },
+ ],
+ },
+ },
+ {
+ type: "zahlen",
+ data: {
+ eyebrow: "Ergebnisse",
+ title: "Zahlen, die *überzeugen.*",
+ items: [
+ { value: "120", suffix: "+", label: "erfolgreiche Projekte" },
+ { value: "38", suffix: " %", label: "mehr Effizienz im Schnitt" },
+ { value: "4,9", suffix: "/5", label: "Kundenzufriedenheit" },
+ { value: "12", suffix: "", label: "Expertinnen und Experten" },
+ ],
+ },
+ },
+ {
+ type: "stimmen-wand",
+ data: {
+ title: "Was unsere Kunden *sagen.*",
+ items: [
+ { quote: "Lumen hat in sechs Wochen geschafft, woran wir zwei Jahre gescheitert sind. Unser Angebotsprozess läuft heute fast von selbst.", author: "Mira Schulz", role: "COO, Velora", image: unsplash("1534528741775-53994a69daeb", 300) },
+ { quote: "Klar in der Sprache, schnell in der Umsetzung und immer erreichbar. So wünscht man sich einen Partner.", author: "David Moreno", role: "Gründer, Brightfield", image: unsplash("1507003211169-0a1dd7228f2d", 300) },
+ { quote: "Die Workshops waren der Wendepunkt. Unser Team arbeitet heute ganz anders – und lieber.", author: "Lena Brandt", role: "Head of Operations, Nordwerk", image: unsplash("1494790108377-be9c29b29330", 300) },
+ { quote: "Endlich Dashboards, die wir wirklich nutzen. Jede Entscheidung basiert heute auf Daten statt Bauchgefühl.", author: "Jonas Becker", role: "Geschäftsführer, Kraftbau", image: unsplash("1472099645785-5658abf4ff4e", 300) },
+ { quote: "Professionell, herzlich und mit beeindruckendem technischem Tiefgang.", author: "Amara Okafor", role: "CEO, Okuma", image: unsplash("1531123897727-8f129e1688ce", 300) },
+ { quote: "Wir haben unsere Kosten um ein Drittel gesenkt und gleichzeitig die Kundenzufriedenheit erhöht.", author: "Tobias Lehmann", role: "CFO, Tessin & Co", image: unsplash("1560250097-0b93528c311a", 300) },
+ ],
+ },
+ },
+ {
+ type: "pakete",
+ data: {
+ title: "Klare Pakete.\n*Faire* Preise.",
+ text: "Alle Pakete mit persönlicher Ansprechperson und monatlichem Bericht. Preise zzgl. MwSt.",
+ plans: [
+ { name: "Start", price: "2.900 €", period: "einmalig", text: "Für den ersten Schritt", features: "Workshop mit Ihrem Team\nAnalyse & Potenziale\nKonkreter Fahrplan", ribbon: "", cta: "Anfragen" },
+ { name: "Wachstum", price: "5.900 €", period: "pro Monat", text: "Für Teams, die skalieren", features: "Alles aus Start\nFestes Umsetzungsteam\nWöchentliche Sprints\nMonatliches Reporting", ribbon: "Beliebt", cta: "Anfragen" },
+ { name: "Partner", price: "Individuell", period: "", text: "Für langfristige Partnerschaften", features: "Alles aus Wachstum\nStrategische Begleitung\nPriorisierter Support\nQuartalsweise Workshops", ribbon: "", cta: "Gespräch vereinbaren" },
+ ],
+ },
+ },
+ {
+ type: "faq",
+ data: {
+ title: "Häufige *Fragen*",
+ text: "Ihre Frage ist nicht dabei? Schreiben Sie uns – wir antworten innerhalb eines Werktags.",
+ items: [
+ { question: "Für welche Unternehmen arbeitet Lumen?", answer: "Für den Mittelstand ebenso wie für Start-ups – überall dort, wo Teams schneller, digitaler und näher an ihren Kunden werden wollen." },
+ { question: "Wie schnell sehen wir Ergebnisse?", answer: "Erste Ergebnisse liefern wir meist nach vier bis sechs Wochen. Jedes Projekt startet mit klaren, messbaren Zielen." },
+ { question: "Arbeiten Sie remote oder vor Ort?", answer: "Beides. Workshops machen wir gern bei Ihnen, die Umsetzung läuft meist remote – mit festen wöchentlichen Terminen." },
+ { question: "Was passiert mit unseren Daten?", answer: "Wir arbeiten DSGVO-konform mit Servern in der EU und schließen mit jedem Kunden einen Vertrag zur Auftragsverarbeitung." },
+ ],
+ },
+ },
+ {
+ type: "cta-portal",
+ data: {
+ menu: "Kontakt",
+ eyebrow: "Bereit?",
+ title: "Lassen Sie uns\nGroßes *starten.*",
+ text: "Ein 30-minütiges Erstgespräch – kostenlos und unverbindlich.",
+ cta: "Gespräch buchen",
+ href: "mailto:hallo@lumen-beratung.de",
+ },
+ },
+ { type: "footer-schlicht", data: { city: "Berlin", note: "Mit Sorgfalt gemacht in Berlin." } },
+ ],
+})
+
+export type AuroraData = typeof aurora.defaults
diff --git a/modules/templates/catalog/aurora/fonts.ts b/modules/templates/catalog/aurora/fonts.ts
new file mode 100644
index 0000000..a29cb7d
--- /dev/null
+++ b/modules/templates/catalog/aurora/fonts.ts
@@ -0,0 +1,4 @@
+import type { FontPair } from "../../lib/types"
+import { pairs } from "../blocks/fonts"
+
+export const fonts: Record = { funnel: pairs.funnel, park: pairs.park }
diff --git a/modules/templates/catalog/aurora/template.tsx b/modules/templates/catalog/aurora/template.tsx
new file mode 100644
index 0000000..28f162e
--- /dev/null
+++ b/modules/templates/catalog/aurora/template.tsx
@@ -0,0 +1,10 @@
+"use client"
+
+import type { TemplateProps } from "../../lib/types"
+import { BlockPage } from "../blocks/page"
+import { aurora, type AuroraData } from "./definition"
+import { fonts } from "./fonts"
+
+export function AuroraTemplate({ content }: TemplateProps) {
+ return
+}
diff --git a/modules/templates/catalog/baukasten/definition.ts b/modules/templates/catalog/baukasten/definition.ts
new file mode 100644
index 0000000..b2e27ed
--- /dev/null
+++ b/modules/templates/catalog/baukasten/definition.ts
@@ -0,0 +1,40 @@
+import { unsplash } from "../../lib/content"
+import { defineBuilder } from "../blocks/builder"
+
+// Leer starten: nur ein Grundgerüst, alles Weitere wird im Editor aus Bausteinen zusammengeklickt
+export const baukasten = defineBuilder({
+ key: "baukasten",
+ name: "Baukasten",
+ industry: "Eigene Seite aus Bausteinen",
+ description: "Selbst zusammenklicken: über 30 Premium-Bausteine für Start, Leistungen, Projekte, Stimmen, Kontakt und mehr – frei kombinierbar, sortierbar und mit jeder Palette nutzbar.",
+ cover: unsplash("1557672172-298e090bd0f1", 1200),
+ samples: {
+ inquiry: "Guten Tag, wir sind ein Familienunternehmen und möchten unsere Website komplett erneuern – modern, hochwertig und gut auffindbar. Wie läuft so ein Projekt bei Ihnen ab und mit welchen Kosten müssen wir rechnen? Viele Grüße, Sandra Keller",
+ topic: "Neu bei uns: persönliche Beratung jetzt auch per Video-Call",
+ },
+ palettes: [
+ { key: "weiss", label: "Weiß", bg: "#fafaf8", fg: "#111111", muted: "#6b6b66", surface: "#efefea", accent: "#111111" },
+ { key: "sand", label: "Sand", bg: "#f3eee6", fg: "#1d1a16", muted: "#776f64", surface: "#e8e1d5", accent: "#b0643c" },
+ { key: "nacht", label: "Nacht", dark: true, bg: "#0c0c0d", fg: "#f0eeea", muted: "#8a8780", surface: "#18181a", accent: "#e8c27a" },
+ { key: "ozean", label: "Ozean", dark: true, bg: "#08131d", fg: "#e8f1f7", muted: "#7f97a8", surface: "#102231", accent: "#4cc3ff" },
+ { key: "wald", label: "Wald", bg: "#e9ede6", fg: "#16201a", muted: "#5f6b62", surface: "#dbe2d6", accent: "#2f6b4a" },
+ { key: "koralle", label: "Koralle", bg: "#fff6f2", fg: "#2a1410", muted: "#8a6a62", surface: "#fbe6de", accent: "#ff5a3c" },
+ ],
+ fonts: [
+ { key: "grotesk", label: "Host Grotesk" },
+ { key: "kino", label: "Noto Serif Display & Host Grotesk" },
+ { key: "funnel", label: "Funnel Display & Funnel Sans" },
+ { key: "mona", label: "Mona Sans (variabel)" },
+ ],
+ brand: {
+ name: "Nordlicht",
+ tagline: "Ihr Unternehmen in einem Satz",
+ email: "hallo@nordlicht.de",
+ phone: "+49 40 1234 5678",
+ address: "Musterstraße 1, 20095 Hamburg",
+ logo: "",
+ },
+ seed: [{ type: "nav-leiste" }, { type: "hero-vollbild" }, { type: "statement" }, { type: "kontakt-formular" }, { type: "footer-schlicht" }],
+})
+
+export type BaukastenData = typeof baukasten.defaults
diff --git a/modules/templates/catalog/baukasten/fonts.ts b/modules/templates/catalog/baukasten/fonts.ts
new file mode 100644
index 0000000..fae4473
--- /dev/null
+++ b/modules/templates/catalog/baukasten/fonts.ts
@@ -0,0 +1,4 @@
+import type { FontPair } from "../../lib/types"
+import { pairs } from "../blocks/fonts"
+
+export const fonts: Record = { grotesk: pairs.grotesk, kino: pairs.kino, funnel: pairs.funnel, mona: pairs.mona }
diff --git a/modules/templates/catalog/baukasten/template.tsx b/modules/templates/catalog/baukasten/template.tsx
new file mode 100644
index 0000000..a00bb52
--- /dev/null
+++ b/modules/templates/catalog/baukasten/template.tsx
@@ -0,0 +1,10 @@
+"use client"
+
+import type { TemplateProps } from "../../lib/types"
+import { BlockPage } from "../blocks/page"
+import { baukasten, type BaukastenData } from "./definition"
+import { fonts } from "./fonts"
+
+export function BaukastenTemplate({ content }: TemplateProps) {
+ return
+}
diff --git a/modules/templates/catalog/blocks/about.tsx b/modules/templates/catalog/blocks/about.tsx
new file mode 100644
index 0000000..6a835a2
--- /dev/null
+++ b/modules/templates/catalog/blocks/about.tsx
@@ -0,0 +1,151 @@
+"use client"
+
+import { AnimatePresence, motion, useInView } from "motion/react"
+import { useEffect, useEffectEvent, useRef, useState } from "react"
+import { cn } from "@/lib/utils"
+import { TImage } from "../shared/media"
+import { Reveal, ScrollWords, ease } from "../shared/motion"
+import type { StatementData, StoryKapitelData, TextBildData } from "./definitions"
+import type { BlockProps } from "./types"
+import { container, easeInOut, Eyebrow, Heading, InkImage } from "./ui"
+
+export function Statement({ data }: BlockProps) {
+ return (
+
+
+
+ {data.eyebrow}
+
+
+
+ {data.author.trim() && (
+
+
+
+ {data.author}
+ {data.role}
+
+
+ )}
+
+
+
+ )
+}
+
+type Chapter = StoryKapitelData["items"][number]
+
+// Geschichte in Kapiteln: links ein stehendes Bild, das sich mit dem aktiven Kapitel von unten neu aufdeckt
+export function StoryKapitel({ data }: BlockProps) {
+ const [active, setActive] = useState(0)
+ const current = Math.min(active, Math.max(0, data.items.length - 1))
+
+ return (
+
+
+
+ )
+}
+
+function TimelineStep({ item, index }: { item: Step; index: number }) {
+ const ref = useRef(null)
+ // Gleiche Höhe wie das Ende der Linie (offset "end 55%"), damit Punkt und Linie zusammen ankommen
+ const lit = useInView(ref, { margin: "0px 0px -45% 0px" })
+ const right = index % 2 === 1
+
+ return (
+
+
+ )
+}
+
+// ── Bento ────────────────────────────────────────────────────────────────────
+
+// Ein Lichtkegel folgt der Maus über alle Kacheln: Rand und Fläche leuchten in Akzentfarbe auf.
+// Die Position steht als --x/--y direkt an jeder Kachel (nicht am Raster), damit nur die Kacheln neu zeichnen.
+export function LeistungenBento({ data }: BlockProps) {
+ const cards = useRef<(HTMLDivElement | null)[]>([])
+
+ function onMove(event: PointerEvent) {
+ for (const card of cards.current) {
+ if (!card) continue
+ const rect = card.getBoundingClientRect()
+ card.style.setProperty("--x", `${event.clientX - rect.left}px`)
+ card.style.setProperty("--y", `${event.clientY - rect.top}px`)
+ }
+ }
+
+ return (
+
+
)
}
diff --git a/modules/templates/ui/components/editor/preview-frame.tsx b/modules/templates/ui/components/editor/preview-frame.tsx
index 6ea605d..b170074 100644
--- a/modules/templates/ui/components/editor/preview-frame.tsx
+++ b/modules/templates/ui/components/editor/preview-frame.tsx
@@ -3,14 +3,20 @@
import { useEffect, useEffectEvent, useRef, useState } from "react"
import { MonitorIcon, SmartphoneIcon, TabletIcon } from "lucide-react"
import { cn } from "@/lib/utils"
-import { DEVICES, PREVIEW_CONTENT, PREVIEW_READY, type Device } from "../../../lib/preview"
+import { DEVICES, PREVIEW_CONTENT, PREVIEW_FOCUS, PREVIEW_READY, type Device } from "../../../lib/preview"
import type { TemplateContent } from "../../../lib/types"
const icons = { desktop: MonitorIcon, tablet: TabletIcon, mobile: SmartphoneIcon }
// Die Vorlage läuft in echter Gerätebreite im iframe (damit Media Queries greifen) und wird
// passend herunterskaliert. Änderungen gehen per postMessage hinein, ohne Neuladen.
-export function PreviewFrame({ templateKey, variantId, content }: { templateKey: string; variantId: string; content: TemplateContent }) {
+// `focus` (Baukasten): Baustein, zu dem die Vorschau scrollen soll; `n` erlaubt erneutes Springen zum selben Baustein.
+export function PreviewFrame({ templateKey, variantId, content, focus }: {
+ templateKey: string
+ variantId: string
+ content: TemplateContent
+ focus?: { id: string; n: number } | null
+}) {
const frameRef = useRef(null)
const containerRef = useRef(null)
const [size, setSize] = useState({ width: 0, height: 0 })
@@ -24,6 +30,15 @@ export function PreviewFrame({ templateKey, variantId, content }: { templateKey:
post()
}, [content])
+ useEffect(() => {
+ if (!focus) return
+ // Kurz warten, damit ein gerade hinzugefügter Baustein in der Vorschau schon gerendert ist
+ const timer = setTimeout(() => {
+ frameRef.current?.contentWindow?.postMessage({ type: PREVIEW_FOCUS, id: focus.id }, window.location.origin)
+ }, 120)
+ return () => clearTimeout(timer)
+ }, [focus])
+
useEffect(() => {
function onMessage(event: MessageEvent) {
if (event.origin === window.location.origin && event.source === frameRef.current?.contentWindow && event.data?.type === PREVIEW_READY) {
diff --git a/modules/templates/ui/components/live-preview.tsx b/modules/templates/ui/components/live-preview.tsx
index d792636..b8171cf 100644
--- a/modules/templates/ui/components/live-preview.tsx
+++ b/modules/templates/ui/components/live-preview.tsx
@@ -3,11 +3,25 @@
import { useEffect, useState } from "react"
import type { AiAccess } from "../../ai/demos"
import { resolveContent } from "../../lib/content"
-import { PREVIEW_CONTENT, PREVIEW_READY } from "../../lib/preview"
+import { PREVIEW_CONTENT, PREVIEW_FOCUS, PREVIEW_READY } from "../../lib/preview"
import { getTemplate } from "../../lib/registry"
import type { TemplateContent } from "../../lib/types"
import { TemplateRenderer } from "./template-renderer"
+// Baukasten: zum Baustein scrollen und ihn kurz umranden, damit man sieht, was man gerade bearbeitet
+function focusBlock(id: string) {
+ const element = document.querySelector(`[data-block="${CSS.escape(id)}"]`)
+ if (!element) return
+ element.scrollIntoView({ behavior: "smooth", block: "start" })
+ element.animate(
+ [
+ { outline: "3px solid rgb(99 102 241 / 0.9)", outlineOffset: "-3px" },
+ { outline: "3px solid rgb(99 102 241 / 0)", outlineOffset: "-3px" },
+ ],
+ { duration: 1600, easing: "ease-out" }
+ )
+}
+
// Läuft im iframe des Editors und übernimmt ungespeicherte Änderungen per postMessage
export function LivePreview({ templateKey, initialContent, access }: {
templateKey: string
@@ -21,8 +35,9 @@ export function LivePreview({ templateKey, initialContent, access }: {
if (!def) return
function onMessage(event: MessageEvent) {
- if (event.origin !== window.location.origin || event.data?.type !== PREVIEW_CONTENT || !def) return
- setContent(resolveContent(def, event.data.content))
+ if (event.origin !== window.location.origin || !def) return
+ if (event.data?.type === PREVIEW_FOCUS && typeof event.data.id === "string") focusBlock(event.data.id)
+ if (event.data?.type === PREVIEW_CONTENT) setContent(resolveContent(def, event.data.content))
}
window.addEventListener("message", onMessage)
diff --git a/modules/templates/ui/components/template-grid.tsx b/modules/templates/ui/components/template-grid.tsx
index 7ae27e9..a9631b3 100644
--- a/modules/templates/ui/components/template-grid.tsx
+++ b/modules/templates/ui/components/template-grid.tsx
@@ -2,23 +2,27 @@
import Image from "next/image"
import { useState } from "react"
-import { EyeIcon, SearchIcon } from "lucide-react"
+import { BlocksIcon, EyeIcon, SearchIcon } from "lucide-react"
import { buttonVariants } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { cn } from "@/lib/utils"
import { templates } from "../../lib/registry"
+import type { TemplateDefinition } from "../../lib/types"
import { CreateVariantButton } from "./create-variant-button"
-// Seiten-Typen für bestimmte Situationen, alles andere sind Branchen-Webseiten
+// Seiten-Typen für bestimmte Situationen, Baukasten-Vorlagen sind frei aus Bausteinen, alles andere sind Branchen-Webseiten
const PAGE_TYPES = new Set(["karriere", "kampagne", "event", "shop", "linkinbio", "speisekarte"])
-const GROUPS = ["Alle", "Branchen", "Seiten-Typen"] as const
+const GROUPS = ["Alle", "Baukasten", "Branchen", "Seiten-Typen"] as const
+
+const groupOf = (template: TemplateDefinition): (typeof GROUPS)[number] =>
+ template.builder ? "Baukasten" : PAGE_TYPES.has(template.key) ? "Seiten-Typen" : "Branchen"
export function TemplateGrid() {
const [query, setQuery] = useState("")
const [group, setGroup] = useState<(typeof GROUPS)[number]>("Alle")
const term = query.trim().toLowerCase()
const visible = templates.filter((template) => {
- const inGroup = group === "Alle" || (group === "Seiten-Typen") === PAGE_TYPES.has(template.key)
+ const inGroup = group === "Alle" || groupOf(template) === group
const text = `${template.name} ${template.industry} ${template.description}`.toLowerCase()
return inGroup && (!term || text.includes(term))
})
@@ -58,6 +62,11 @@ export function TemplateGrid() {
/>
{template.industry}
+ {template.builder && (
+
+ Bausteine
+
+ )}
{template.palettes.map((palette) => (
const components: Record = {
+ baukasten: BaukastenTemplate as TemplateComponent,
+ monolith: MonolithTemplate as TemplateComponent,
+ aurora: AuroraTemplate as TemplateComponent,
+ atelier: AtelierTemplate as TemplateComponent,
+ prisma: PrismaTemplate as TemplateComponent,
restaurant: RestaurantTemplate as TemplateComponent,
handwerk: HandwerkTemplate as TemplateComponent,
beauty: BeautyTemplate as TemplateComponent,
diff --git a/modules/templates/ui/views/templates-view.tsx b/modules/templates/ui/views/templates-view.tsx
index 94be721..a992f49 100644
--- a/modules/templates/ui/views/templates-view.tsx
+++ b/modules/templates/ui/views/templates-view.tsx
@@ -22,7 +22,7 @@ const TemplatesView = async () => {
Vorlagen
- Demo-Webseiten für Pitches: Vorlage wählen, für den Kunden anpassen (Inhalte, Farben, Schriften, Reihenfolge) und als Link teilen.
+ Demo-Webseiten für Pitches: Vorlage wählen oder im Baukasten aus Bausteinen selbst zusammenstellen, für den Kunden anpassen (Inhalte, Farben, Schriften, Reihenfolge) und als Link teilen.
diff --git a/modules/templates/ui/views/variant-editor.tsx b/modules/templates/ui/views/variant-editor.tsx
index ab982b1..287b1e1 100644
--- a/modules/templates/ui/views/variant-editor.tsx
+++ b/modules/templates/ui/views/variant-editor.tsx
@@ -10,6 +10,7 @@ import type { ShareState } from "../../lib/preview"
import { getTemplate } from "../../lib/registry"
import type { ListItem, TemplateContent } from "../../lib/types"
import { AiPanel } from "../components/editor/ai-panel"
+import { BlocksPanel } from "../components/editor/blocks-panel"
import { ContentPanel } from "../components/editor/content-panel"
import { DesignPanel } from "../components/editor/design-panel"
import { PreviewFrame } from "../components/editor/preview-frame"
@@ -46,6 +47,8 @@ export function VariantEditor({ variant, clients }: { variant: EditorVariant; cl
const [content, setContent] = useState(variant.content)
const [saved, setSaved] = useState(() => snapshot(variant.name, variant.clientId, variant.content))
const [tab, setTab] = useState("content")
+ // Baukasten: Baustein, zu dem die Vorschau springt (n zählt hoch, damit erneutes Aufklappen wieder springt)
+ const [focus, setFocus] = useState<{ id: string; n: number } | null>(null)
const [error, setError] = useState(null)
const [pending, startTransition] = useTransition()
@@ -150,7 +153,17 @@ export function VariantEditor({ variant, clients }: { variant: EditorVariant; cl