Puck visual-editor spike: HD-Blöcke (Hero/Text/FeatureGrid/ProductGrid/CTA), OKLCH-Branding, Edit↔Vorschau
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
# build
|
||||
FROM node:22-slim AS build
|
||||
WORKDIR /app
|
||||
COPY package*.json ./
|
||||
RUN npm ci --no-audit --no-fund
|
||||
COPY . .
|
||||
RUN npm run build
|
||||
# serve
|
||||
FROM nginx:alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
RUN printf 'server{listen 80;root /usr/share/nginx/html;location / {try_files $uri $uri/ /index.html;}}' > /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
HEALTHCHECK CMD wget -qO- http://localhost/ || exit 1
|
||||
@@ -0,0 +1,16 @@
|
||||
# HD Puck-Editor-Spike
|
||||
|
||||
Schneller Prototyp: visueller Drag-&-Drop-Editor mit **@measured/puck**, HD-gebrandete Blöcke
|
||||
(Hero, Rich-Text, Feature-Grid, **Produkt-Grid**, CTA), OKLCH-Petrol/Terrakotta, Fraunces + Public Sans.
|
||||
|
||||
Zweck: das **Editor-Gefühl** vergleichen mit dem hd-commerce-Eigenbau-Editor. Persistenz im Browser
|
||||
(localStorage), Toggle Editor ↔ Vorschau, Reset. Kein Backend — reiner Editor-Spike.
|
||||
|
||||
Nächster Schritt (Commerce): Medusa v2 als Backend ODER hd-commerce-Engine für Cart/Checkout.
|
||||
|
||||
```bash
|
||||
npm install && npm run dev # http://localhost:5173
|
||||
npm run build # dist/ (static, via nginx-Dockerfile deploybar)
|
||||
```
|
||||
|
||||
Hinweis: Puck ist nach `@puckeditor/core` umgezogen — für den Spike bewusst noch `@measured/puck@0.19`.
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>HD · Puck-Editor-Spike</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.jsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+1916
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "hd-puck-spike",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@measured/puck": "^0.19.0",
|
||||
"@fontsource-variable/fraunces": "^5.1.0",
|
||||
"@fontsource-variable/public-sans": "^5.1.0",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@vitejs/plugin-react": "^4.3.4",
|
||||
"vite": "^5.4.10"
|
||||
}
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
import React, { useState } from "react";
|
||||
import { Puck, Render } from "@measured/puck";
|
||||
import "@measured/puck/puck.css";
|
||||
import { config } from "./puck.config.jsx";
|
||||
|
||||
const KEY = "hd-puck-spike-data";
|
||||
const seed = {
|
||||
content: [
|
||||
{ type: "Hero", props: { id: "hero-1" } },
|
||||
{ type: "ProductGrid", props: { id: "pg-1" } },
|
||||
{ type: "FeatureGrid", props: { id: "fg-1" } },
|
||||
{ type: "CTA", props: { id: "cta-1" } },
|
||||
],
|
||||
root: { props: {} },
|
||||
};
|
||||
|
||||
function load() {
|
||||
try { const r = localStorage.getItem(KEY); if (r) return JSON.parse(r); } catch (e) {}
|
||||
return seed;
|
||||
}
|
||||
|
||||
export default function App() {
|
||||
const [mode, setMode] = useState("edit");
|
||||
const [data, setData] = useState(load);
|
||||
|
||||
const bar = (
|
||||
<div style={{ position: "fixed", zIndex: 1000, top: 12, right: 12, display: "flex", gap: 8 }}>
|
||||
<button onClick={() => setMode(mode === "edit" ? "view" : "edit")}
|
||||
style={{ border: 0, borderRadius: 8, padding: ".55em 1em", fontWeight: 700, cursor: "pointer",
|
||||
background: "var(--ink)", color: "var(--paper)" }}>
|
||||
{mode === "edit" ? "▶ Vorschau" : "✎ Editor"}
|
||||
</button>
|
||||
<button onClick={() => { localStorage.removeItem(KEY); setData(seed); }}
|
||||
style={{ border: "1px solid var(--line)", borderRadius: 8, padding: ".55em 1em", fontWeight: 600,
|
||||
cursor: "pointer", background: "var(--paper)", color: "var(--ink)" }}>
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
|
||||
if (mode === "view") {
|
||||
return (<>{bar}<Render config={config} data={data} /></>);
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{bar}
|
||||
<Puck config={config} data={data}
|
||||
onPublish={(d) => { setData(d); localStorage.setItem(KEY, JSON.stringify(d)); setMode("view"); }}
|
||||
onChange={(d) => { setData(d); localStorage.setItem(KEY, JSON.stringify(d)); }} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import React from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import "./tokens.css";
|
||||
import App from "./App.jsx";
|
||||
createRoot(document.getElementById("root")).render(<App />);
|
||||
@@ -0,0 +1,148 @@
|
||||
import React from "react";
|
||||
|
||||
const Section = ({ bg, children }) => (
|
||||
<section className="hd-section" style={bg ? { background: "var(--sand)" } : undefined}>
|
||||
<div className="hd-wrap">{children}</div>
|
||||
</section>
|
||||
);
|
||||
|
||||
export const config = {
|
||||
categories: {
|
||||
layout: { title: "Inhalt", components: ["Hero", "Text", "FeatureGrid", "CTA"] },
|
||||
commerce: { title: "Commerce", components: ["ProductGrid"] },
|
||||
},
|
||||
components: {
|
||||
Hero: {
|
||||
label: "Hero",
|
||||
fields: {
|
||||
kicker: { type: "text", label: "Kicker" },
|
||||
headline: { type: "text", label: "Überschrift" },
|
||||
lead: { type: "textarea", label: "Lead" },
|
||||
cta: { type: "text", label: "Button-Text" },
|
||||
},
|
||||
defaultProps: {
|
||||
kicker: "Brittas Nähkiste",
|
||||
headline: "Kurzwaren, die Freude machen.",
|
||||
lead: "Handverlesene Nähbedarf-Auswahl — schnell geliefert, ehrlich beraten.",
|
||||
cta: "Zum Shop",
|
||||
},
|
||||
render: ({ kicker, headline, lead, cta }) => (
|
||||
<Section>
|
||||
<div className="hd-kicker">{kicker}</div>
|
||||
<h1 className="hd-h1">{headline}</h1>
|
||||
<p className="hd-lead">{lead}</p>
|
||||
{cta && <a className="hd-btn" href="#">{cta}</a>}
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
Text: {
|
||||
label: "Rich-Text",
|
||||
fields: {
|
||||
heading: { type: "text", label: "Überschrift" },
|
||||
body: { type: "textarea", label: "Text" },
|
||||
},
|
||||
defaultProps: {
|
||||
heading: "Über uns",
|
||||
body: "Wir nähen seit 1998 — und beraten jeden Kauf so, als wäre es für uns selbst.",
|
||||
},
|
||||
render: ({ heading, body }) => (
|
||||
<Section>
|
||||
{heading && <h2 className="hd-h2">{heading}</h2>}
|
||||
<p className="hd-lead">{body}</p>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
FeatureGrid: {
|
||||
label: "Feature-Grid",
|
||||
fields: {
|
||||
columns: { type: "number", label: "Spalten" },
|
||||
items: {
|
||||
type: "array",
|
||||
label: "Features",
|
||||
arrayFields: {
|
||||
title: { type: "text", label: "Titel" },
|
||||
body: { type: "textarea", label: "Text" },
|
||||
},
|
||||
defaultItemProps: { title: "Feature", body: "Beschreibung" },
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
columns: 3,
|
||||
items: [
|
||||
{ title: "Schneller Versand", body: "DHL-Versand am selben Werktag bis 14 Uhr." },
|
||||
{ title: "Echte Beratung", body: "Persönlich per Mail & Telefon — kein Callcenter." },
|
||||
{ title: "DSGVO-konform", body: "Hosting in der EU, keine US-Tracker." },
|
||||
],
|
||||
},
|
||||
render: ({ columns, items }) => (
|
||||
<Section bg>
|
||||
<div className="hd-grid" style={{ gridTemplateColumns: `repeat(${columns || 3}, 1fr)` }}>
|
||||
{(items || []).map((it, i) => (
|
||||
<div className="hd-feat" key={i}>
|
||||
<h3>{it.title}</h3>
|
||||
<p>{it.body}</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
ProductGrid: {
|
||||
label: "Produkt-Grid",
|
||||
fields: {
|
||||
heading: { type: "text", label: "Überschrift" },
|
||||
items: {
|
||||
type: "array",
|
||||
label: "Produkte",
|
||||
arrayFields: {
|
||||
name: { type: "text", label: "Name" },
|
||||
price: { type: "text", label: "Preis" },
|
||||
},
|
||||
defaultItemProps: { name: "Produkt", price: "9,90 €" },
|
||||
},
|
||||
},
|
||||
defaultProps: {
|
||||
heading: "Beliebt diese Woche",
|
||||
items: [
|
||||
{ name: "Stoffschere Solingen", price: "24,90 €" },
|
||||
{ name: "Garn-Set 24 Farben", price: "12,50 €" },
|
||||
{ name: "Stecknadeln (200 Stk.)", price: "4,90 €" },
|
||||
{ name: "Schnittmuster Tasche", price: "7,90 €" },
|
||||
],
|
||||
},
|
||||
render: ({ heading, items }) => (
|
||||
<Section>
|
||||
{heading && <h2 className="hd-h2">{heading}</h2>}
|
||||
<div className="hd-grid" style={{ gridTemplateColumns: "repeat(4, 1fr)" }}>
|
||||
{(items || []).map((p, i) => (
|
||||
<div className="hd-card" key={i}>
|
||||
<div className="ph" />
|
||||
<div className="bd">
|
||||
<h4>{p.name}</h4>
|
||||
<span className="price">{p.price}</span>
|
||||
<button className="cart">In den Warenkorb</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
CTA: {
|
||||
label: "CTA-Banner",
|
||||
fields: {
|
||||
headline: { type: "text", label: "Überschrift" },
|
||||
cta: { type: "text", label: "Button-Text" },
|
||||
},
|
||||
defaultProps: { headline: "Bereit zum Stöbern?", cta: "Jetzt einkaufen" },
|
||||
render: ({ headline, cta }) => (
|
||||
<Section>
|
||||
<div className="hd-cta">
|
||||
<h2 className="hd-h2">{headline}</h2>
|
||||
<a className="hd-btn" href="#">{cta}</a>
|
||||
</div>
|
||||
</Section>
|
||||
),
|
||||
},
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,39 @@
|
||||
/* HD-Tokens (OKLCH) — hell/editorial, Petrol + Terrakotta-Akzent */
|
||||
@import "@fontsource-variable/fraunces";
|
||||
@import "@fontsource-variable/public-sans";
|
||||
:root{
|
||||
--paper: oklch(0.985 0.004 95);
|
||||
--sand: oklch(0.96 0.012 80);
|
||||
--ink: oklch(0.27 0.02 250);
|
||||
--slate: oklch(0.52 0.02 250);
|
||||
--petrol: oklch(0.52 0.07 215);
|
||||
--petrol-deep: oklch(0.40 0.07 220);
|
||||
--terra: oklch(0.62 0.13 45);
|
||||
--line: oklch(0.88 0.01 95);
|
||||
--serif: "Fraunces Variable", Georgia, serif;
|
||||
--sans: "Public Sans Variable", system-ui, sans-serif;
|
||||
}
|
||||
*{box-sizing:border-box}
|
||||
body{margin:0;font-family:var(--sans);color:var(--ink);background:var(--paper)}
|
||||
.hd-section{padding:72px 28px}
|
||||
.hd-wrap{max-width:1080px;margin:0 auto}
|
||||
.hd-kicker{font-family:var(--sans);font-weight:700;letter-spacing:.12em;text-transform:uppercase;font-size:.72rem;color:var(--terra)}
|
||||
.hd-kicker::before{content:"_ ";color:var(--terra)}
|
||||
.hd-h1{font-family:var(--serif);font-weight:560;font-size:clamp(2.2rem,5vw,3.6rem);line-height:1.04;letter-spacing:-.01em;margin:.3em 0}
|
||||
.hd-h2{font-family:var(--serif);font-weight:560;font-size:clamp(1.6rem,3.4vw,2.4rem);line-height:1.1;margin:0 0 .4em}
|
||||
.hd-lead{font-size:1.15rem;color:var(--slate);max-width:60ch;line-height:1.6}
|
||||
.hd-btn{display:inline-block;margin-top:1.4em;background:var(--petrol);color:var(--paper);text-decoration:none;font-weight:600;padding:.85em 1.5em;border-radius:8px}
|
||||
.hd-btn:hover{background:var(--petrol-deep)}
|
||||
.hd-grid{display:grid;gap:24px}
|
||||
.hd-feat{border-top:2px solid var(--terra);padding-top:16px}
|
||||
.hd-feat h3{font-family:var(--serif);font-weight:560;font-size:1.25rem;margin:.2em 0 .3em}
|
||||
.hd-feat p{color:var(--slate);margin:0;line-height:1.55}
|
||||
.hd-card{background:var(--paper);border:1px solid var(--line);border-radius:12px;overflow:hidden;display:flex;flex-direction:column}
|
||||
.hd-card .ph{aspect-ratio:4/3;background:linear-gradient(135deg,var(--sand),oklch(0.9 0.03 215))}
|
||||
.hd-card .bd{padding:14px 16px 18px}
|
||||
.hd-card h4{font-family:var(--serif);font-weight:560;margin:0 0 .25em;font-size:1.05rem}
|
||||
.hd-card .price{font-weight:700;color:var(--petrol-deep)}
|
||||
.hd-card .cart{margin-top:10px;width:100%;border:0;background:var(--ink);color:var(--paper);padding:.6em;border-radius:7px;font-weight:600;cursor:pointer}
|
||||
.hd-cta{background:var(--petrol-deep);color:var(--paper);border-radius:16px;padding:48px 36px;text-align:center}
|
||||
.hd-cta .hd-h2{color:var(--paper)}
|
||||
.hd-cta .hd-btn{background:var(--terra)}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineConfig } from 'vite';
|
||||
import react from '@vitejs/plugin-react';
|
||||
export default defineConfig({ plugins: [react()] });
|
||||
Reference in New Issue
Block a user