# Produkt-Komponenten für Posts **Stand:** 20.10.2026. Quelle: `hivegrid-cloud`, Stand `origin/main` vom 19.10.2026. **Referenz-Umsetzung:** `post8.html`, Abschnitt „Post 8 · E-Mail-Marketing“. Diese Datei ist das Nachschlagewerk für jede Zeichnung, die in einem Post die Software zeigt. Die Regel dazu steht in `rules.md` §0 und gilt ohne Ausnahme. --- ## 1. Die Regel **Jede Oberfläche in einem Post wird aus den echten Komponenten von hivegrid-cloud nachgebaut, mit ihren Maßen, Farben, Schriften, Icons und Beschriftungen. Keine eigenen, ausgedachten Grafiken.** - Jede Zeichnung bildet einen Screen nach, den es im Produkt gibt. Im Kommentar steht die Datei, aus der er stammt. - Beschriftungen stehen wörtlich in `apps/web/public/locales/de/translations.json`. - Was im Produkt nicht existiert, wird nicht gezeichnet. Auch nicht, wenn es gut aussähe. - Was nicht auf die Slide passt, wird weggelassen, nicht umgebaut. Im Kommentar steht, was fehlt und warum. - Erfunden werden darf nur Demo-Inhalt: Namen, Betreffzeilen, Texte, Daten, Zahlen. Die Zahlen müssen aufgehen. **Warum:** Die erste Fassung von Post 8 hatte ein eigenes Design (Pillen mit Rand, Häkchen-Chips, Reiter und Kacheln, die es im Produkt nicht gibt). Dejan: „sieht total vibecoded aus“. Nach dem Umbau auf die Komponenten: „sieht jetzt genau so aus wie die Software“ (20.10.2026). --- ## 2. So gehst du vor 1. **Screens finden.** Für das Thema des Posts das Modul in hivegrid-cloud suchen: `apps/web/app/components/screens//`, `apps/web/app/modules//`, die Routen unter `apps/web/app/routes/app.$tenant/`, die Texte in `translations.json`. 2. **Immer `origin/main` lesen, nie die Arbeitskopie.** Die lokale Kopie von hivegrid-cloud ist veraltet und wird nicht angefasst (kein pull, kein checkout ohne Dejan). - Einzelne Datei: `git -C ../hivegrid-cloud show origin/main:` - Für viele Dateien ein Schnappschuss ins Scratchpad: `git -C ../hivegrid-cloud archive origin/main apps/web/app apps/web/public/locales/de | tar -x -C /hg-main` - Vorher `git -C ../hivegrid-cloud fetch` (holt nur, ändert nichts). 3. **Für jede Slide einen echten Screen wählen.** Aufbau von oben nach unten aus dem Code lesen: Kopf, Knöpfe, Tabs, Felder, Tabellenspalten, welche Badge für welchen Status. 4. **Mit den Bausteinen zeichnen** (Abschnitt 3). Sie stehen in `post8.html` und werden in den neuen Post kopiert, weil jeder Post eine einzelne Datei ist. 5. **Prüfen:** - Streifen-Ansicht `?nur=streifen`: Die Karte darf nicht in den Text der Slide ragen. - PNG-Export: Die Oberfläche muss in Instrument Sans stehen, nicht in einer Ersatzschrift (siehe Fallen). - MP4 mit `node animation.mjs postN.html `. --- ## 3. Bausteine in `post8.html` Alle Werte sind Produkt-Pixel und werden mit `PZ = 1.15` vergrößert, wie ein Browserzoom. Koordinaten sind Pixel der Zeichnung. Nur Schriften, Icons und Komponentenhöhen laufen über `pz()`. | Baustein | Bildet nach | Aufruf | |---|---|---| | `P`, `P_TON`, `P_KNOPF`, `P_KARTE` | Rollen aus `ui/theme.css` | Farbkonstanten | | `pText(x, y, t, gr, gew, farbe, o)` | Text in Instrument Sans | `o.anker` für Ausrichtung | | `pMess(t, gr, gew)` | Textbreite in Instrument Sans | | | `pMitte(ym, gr)` | Grundlinie für Text mittig in einem Kasten | | | `pIkon(name, cx, cy, gr, farbe)` | Feather/Lucide-Icon, Strich 2 auf 24 | Namen aus `LUCIDE` | | `pKarte(x, y, b, h, art, radius)` | `Card`, `InlineMessage` | `art`: std, weiss, neutral, info, warnung | | `pBadge(x, ym, t, ton, klein)` | `BadgeFeedback` subtle | `ton`: positiv, warnung, info, neutral | | `pKnopf(x, y, t, art, o)` | `Button` | `art`: primaer, sekundaer, tertiaer, geist, aus; `o.ikon`, `o.ikonRechts`, `o.klein` | | `pFeld(x, y, b, o)` | `TextInput`, `Select`, `TextArea` | `o.label`, `o.wert`, `o.platzhalter`, `o.auswahl`, `o.hoch`, `o.hilfe` | | `pTabs(x, y, b, reiter, aktiv, innen)` | `Tabs` | | | `pUmschalter(xRechts, y, eintraege, aktiv)` | `ToggleGroup` klein | Einträge `[icon, text]` | | `pTabelle(x, y, spalten, zeilen, zh, zelle)` | `Table` | Spalten `[titel, icon, breite]` | | `pRahmen(h, inhalt, kopf)` | Seite mit `PageHeader` | `kopf.ikon`, `kopf.titel`, `kopf.badge`, `kopf.ikonKasten`, `kopf.rechts` | | `pKopfKnopf(t, art, o)` | Knopf rechts im Kopf | | | `emKpi(...)` | `WidgetRahmen` mit `KPICard` | | **Icons:** `LUCIDE` enthält die Formen aus `lucide-static@0.460.0`. Subframes `Feather*`-Icons haben dieselben Formen. Fehlt ein Icon, die SVG von `https://cdn.jsdelivr.net/npm/lucide-static@0.460.0/icons/.svg` holen und das Innere der `` in die Tabelle eintragen. Phosphor (`ikon()`) gehört nur noch in ältere Posts, nicht in Oberflächen. **Schrift:** Die Dateien liegen in `assets/fonts/instrument-sans-*.woff2`, dieselben wie in `hivegrid-cloud/apps/web/public/fonts/`. Der Post braucht dafür drei Dinge: 1. den `@font-face`-Block im `` 2. beim Start `document.fonts.load(...)` für 400, 500 und 600, bevor zum ersten Mal gemessen wird 3. `instrumentSansCss()`, angehängt an `fontEmbedCss`, sonst fehlt die Schrift im PNG Alles drei steht in `post8.html`. --- ## 4. Tokens, aufgelöst ### Schrift Instrument Sans, variabel 400 bis 700. Laufweite überall 0. | Token | px / Zeilenhöhe / Gewicht | Wofür | |---|---|---| | `subtitle` | 32/40/600 | Seitentitel im `PageHeader`, Wert in `StatsCard` | | `heading-` | 24/32/500 | Dialogtitel | | `body-large` | 20/24 (‑0: 26)/500 | Abschnitte wie „E-Mail gestalten“, EmptyState-Titel | | `body-default-regular` | 16/24 (‑0: 22)/400 | **Knopfbeschriftung**, Eingaben, Fließtext | | `body-default-medium` | 16/24/500 | Feldbeschriftungen | | `body-default-semibold` | 16/24/600 | Kartentitel | | `body-small-regular` | 14/21 (‑0: 18)/400 | Hilfetexte, Tabs, Seitenleiste | | `body-small-medium` | 14/21/500 | Badges, Menüeinträge, Checkbox-Beschriftung | | `body-extra-small` | 12/18 (‑0: 16)/400 bis 600 | Tabellenkopf (500), Tooltip | `text-heading-2` und `text-heading-3` werden im Code benutzt, sind aber nirgends definiert. Sie rendern als 16/24/400. ### Farben **Neutral:** 0 `#FAFAFA` · 50 `#F5F4F4` · 100 `#F0F0F0` · 200 `#E5E5E5` · 300 `#CECECF` · 400 `#B5B5B7` · 500 `#9D9DA0` · 600 `#858589` · 700 `#6D6D71` · 800 `#505057` · 850 `#37373E` · 900 `#24242B` · 950 `#111114` | Rolle | Wert | |---|---| | Seite (`surface-neutral-primary`) | `#F5F4F4` | | Karte, Kopfzeile, Tabellenzeile (`secondary`) | `#FAFAFA` | | Tabellenkopf, Hover (`tertiary`) | `#F0F0F0` | | Rand, Rand stark | `#E5E5E5`, `#B5B5B7` (Eingabefelder) | | Text primär, sekundär, tertiär | `#24242B`, `#505057`, `#6D6D71` | | Platzhalter, deaktiviert | `#CECECF` | | Link | `#08739C` | | Icons primär, sekundär, tertiär | `#24242B`, `#505057`, `#858589` | | Akzent für aktiv und ausgewählt | `#E1F4FD` (information subtle) | | Sekundärknopf | `#E3F1FF` | | Ton | 100 | 200 | 500 | 600 | 700 | |---|---|---|---|---|---| | Erfolg (grün) | `#DCFCE4` | `#BAF8CB` | `#20C54F` | `#15A43E` | `#148134` | | Warnung (gelb, nicht Marke) | `#FCF6C5` | `#FAEB8E` | `#E1AB11` | `#C2840C` | `#9B5E0D` | | Fehler (rot) | `#FEE2E2` | `#FDCBCB` | `#ED4646` | `#DA2828` | `#C52020` | | Information (ocean) | `#E1F4FD` | `#BCEAFB` | `#15B1E2` | `#0890C1` | `#08739C` | **Kein Markengelb in der Oberfläche.** Die Rolle „brand“ ist im Produkt auf Grau gemappt, Primärknöpfe sind `#24242B`. Gelb gehört auf der Slide nur zur Überschrift und zu Marken-Elementen außerhalb der Oberfläche. ### Radien und Schatten - `rounded-sm` 4 px (Eingabefelder) · `rounded-md` **6 px** (Knöpfe, Karten, Tabs) · `rounded-lg` 8 px (Widgets) · `rounded-xl` 12 px. In `theme.css` steht md = 8, `globals.css` überschreibt das danach mit 6. - Schatten im Produkt: sm `0 1px 2px rgb(0 0 0 / .08)`, md und lg größer. In den Zeichnungen lassen wir sie weg (rules.md §5). Der Stapel um die Karte hat seinen eigenen Schatten und bleibt. --- ## 5. Komponenten | Komponente | Maße und Farben | |---|---| | **Button** | klein h32, px12, Abstand 8 · mittel h36, px16, Abstand 12 · groß h48. Radius md. Beschriftung 16/400, Icon 16. primär `#24242B`/`#FAFAFA` · sekundär `#E3F1FF`/`#24242B` · tertiär `#F5F4F4` mit Rand `#E5E5E5` · ghost transparent · information `#0890C1` · destructive `#DA2828` · deaktiviert `#F0F0F0`/`#CECECF` | | **IconButton** | rund, quadratisch mit `square` (md). 24/32/36/48 Kasten, Icon 16/20/20/24 | | **BadgeFeedback** (alle Status) | Pille **ohne Rand**, h24 (klein 20), px10, Abstand 8, Punkt 4 px, Schrift 14/500. positiv `#DCFCE4`, Text und Punkt `#148134` · Warnung `#FCF6C5`, Text `#24242B`, Punkt `#9B5E0D` · information `#E1F4FD`, Text `#24242B`, Punkt `#08739C` · neutral `#F0F0F0`, Text und Punkt `#24242B` · negativ `#FEE2E2`, `#C52020` | | **Badge** (Farbtöne) | Pille h24 mit 1-px-Rand, sonst wie oben. Standard `#F0F0F0`, Rand `#E5E5E5` | | **Tabs** | Reiter px16, py10 (rund 40 hoch), oben rund, `#FAFAFA`, unten 2 px `#E5E5E5`, Schrift 14/400 `#505057`. **Aktiv:** Fläche `#E1F4FD`, unten 2 px `#24242B`, Schrift 14/500 `#24242B` | | **TextInput / Select** | h36, px12, Radius 4, Rand `#B5B5B7`, Fläche `#F5F4F4`. Beschriftung 16/500 `#505057`, 6 px darüber. Wert 16 `#505057`, Platzhalter `#CECECF`, Hilfe 14 `#6D6D71`. Pfeil 16 `#858589` | | **TextArea** | wie oben, mindestens 96 hoch, Innenabstand 12×8 | | **Checkbox** | 16, Radius 4, Rand 2 px `#B5B5B7`. An: `#24242B` mit Haken `#FAFAFA`. Beschriftung 14/500 | | **Switch** | 36×22, rund. Aus: `#F5F4F4` mit dunklem Knopf. An: `#24242B` mit hellem Knopf | | **Table** | Außenrand `#E5E5E5`. Kopf h32, `#F0F0F0`, Schrift 12/500 `#505057`, Icon 12 `#858589`. Zeilen `#FAFAFA` mit Trennlinien `#E5E5E5`, Höhe aus dem Inhalt (rund 31) | | **Avatar** | rund, 24/32/40/48/64. Ohne Bild `#F0F0F0`, Kürzel 16/500 `#505057` | | **IconWithBackground** | Radius 4. Kasten 22/24/28/32/40, Icon 14/16/20/24/32. information `#E1F4FD` mit `#08739C` | | **Card** | Radius md, Rand `#E5E5E5`, Fläche `#FAFAFA`, Innenabstand 16 (lg 24). Varianten information, warning, negative, positive, neutral | | **InlineMessage** | Card in Ton-Variante, px16, py12, Text 16 `#24242B`. Standard information | | **Alert** | Radius md, Rand, px16, py20, Icon 24. Titel 16/500 im Ton, Text 16 `#505057` | | **Widget** (`WidgetRahmen`) | Radius lg, Rand, weiß. Kopf h44 mit Titel 13/600. KPI: Wert halbfett, Fußzeile 11 `#6D6D71` über einer Linie | | **ToggleGroup** | h36 (klein 32), Innenabstand 2, Rand, `#F0F0F0`. Aktiv: `#24242B` mit Schrift `#FAFAFA` | | **DropdownMenu** | `#FAFAFA`, Rand, Radius md, Innenabstand 4. Einträge 14/500 `#6D6D71`, Hover `#F0F0F0` | | **Tooltip** | `#505057`, Schrift 12/500 `#FAFAFA`, Radius 4 | | **Progress** | 6 px, rund, Spur `#F0F0F0`, Füllung `#24242B` | | **EmptyState** | Titel 20/500, Text 16 `#505057`. Umrandet: Rand und `#F5F4F4` | | **Dialog** | weiß, Rand, Radius md, Overlay `rgb(0 0 0 / .4)`. DecisionDialog 420 oder 560 breit, Titel 24/500 | ### App-Rahmen - **Seite:** Hintergrund `#F5F4F4`. Kopfleiste h64, `#FAFAFA`, Linie unten. - **PageHeader:** `#FAFAFA`, px24, py20, Linie unten. Icon 24 in `#858589`, Abstand 8, Titel 32/600 `#24242B`. Knöpfe rechts mit Abstand 8. In den Zeichnungen Titel 24, weil die Karte schmaler ist als ein Bildschirm. - **Filterleiste:** `#F5F4F4`, px24, py10. Suchfeld `#FAFAFA`, Facetten-Chips h24 mit Rand `#B5B5B7`. Aktiv `#BCEAFB`. - **Seitenleiste:** 240 breit, `#FAFAFA`. Einträge h40, Icon 20 mit Strich 1.5, Schrift 14. Gewählt: `#E1F4FD`. --- ## 6. Fallen 1. **Schrift im PNG.** html-to-image bettet nur die Schrift ein, die im HTML vorkommt, also Manrope. Instrument Sans steckt nur im SVG und fehlte im Export. Lösung: `instrumentSansCss()` an `fontEmbedCss` hängen. Prüfen, indem man Export und Vorschau übereinanderlegt. 2. **Messen vor dem Laden.** `pMess()` speichert Breiten zwischen. Wird vor dem Laden der Schrift gemessen, bleibt die Breite der Ersatzschrift hängen. Darum `document.fonts.load` am Anfang von `boot()`. 3. **Was in den Übersetzungen steht, ist nicht automatisch zu sehen.** Beispiel: „Pre-Send-Prüfung“ und die sechs Prüfpunkte stehen in `translations.json`, angezeigt werden sie nirgends. Immer im Screen-Code nachsehen, ob ein Text wirklich gerendert wird. 4. **Website und Produkt unterscheiden.** Die Website zeigt manches, was es im Produkt nicht gibt (in Post 8 die Produktkacheln im Mail-Baukasten). Maßgeblich ist das Produkt. 5. **Höhe der Karte.** Zu hohe Karten ragen in den Text der Slide. Lieber Schritte oder Hilfetexte weglassen als die Komponenten zu verkleinern. 6. **Feste Höhe im Bewegtbild.** Die Höhe der Zeichnung darf nicht vom Zeitpunkt abhängen, sonst ändert sich der viewBox und alles springt. --- ## 7. Beispiel: E-Mail-Marketing (Post 8) | Slide | Screen | Datei in `apps/web/app/components/screens/email-marketing/` | |---|---|---| | Bewegtbild | Kampagnenbericht | `EmailMarketingDetailScreens.tsx` | | Zielgruppe | Dialog „Zielgruppe bearbeiten“ | `SegmentEditorDialog.tsx`, `SegmentFilterBuilder.tsx` | | Vorlagen | Vorlagenseite | `EmailMarketingTemplateScreens.tsx` | | Rechtssicher | „Empfänger & Versand“ mit „Bereit zum Versand?“ | `EmailMarketingCampaignEditorPage.tsx` | | Baukasten | Gestaltung | `CampaignContentBuilder.tsx`, `CampaignBuilderSidebar.tsx`, `campaign-mail-look.ts` | Status-Badges dort: „Gesendet“ positiv · „Geplant“, „Pausiert“ Warnung · „Wird gesendet“ information · „Entwurf“, „Archiviert“ neutral. Empfänger: „Zugestellt“, „Geöffnet“, „Geklickt“ positiv · „Ausstehend“ Warnung · „Gebounced“ negativ. Vorlagen: „Standard“ information · „Eigen“ neutral. Versandbereitschaft: „Erledigt“ positiv · „Offen“ neutral. --- ## 8. Beispiel: Form Builder (Post 7) Seit 20.10.2026 nach dem echten Editor gebaut. Vorher kam Post 7 aus dem Klickdummy des Pitch-Pakets (`S02EinfacherFragenEditor.tsx`) und zeigte Dinge, die es nicht gibt. | Slide | Screen | Datei in `apps/web/app/components/screens/forms/` | |---|---|---| | Bewegtbild | Reiter „Fragen“, neue Frage klappt auf | `editor/questions-tab/QuestionsTabView.tsx`, `SortableQuestionCard.tsx` | | Die Fragen | zugeklappte Fragekarten, Werkzeugleiste | `SortableQuestionCard.tsx:121`, `QuestionsTabView.tsx` | | Bedingt sichtbar | offene Frage mit „Nur anzeigen, wenn“ | `editor/questions-tab/QuestionEditors.tsx:123` | | Lead | Ansicht „Felder zuweisen“ | `editor/LeadBanner.tsx`, `editor/LeadMappingView.tsx` | - **Kopf** (`FormEditorPage.tsx:411`): kein PageHeader, eine eigene Zeile mit `clipboard-list`, Name 20/500, Badge „Entwurf“ (neutral), „Veröffentlicht“ (positiv) oder „Änderungen nicht live“ (Warnung). Rechts klein: „Einbetten & QR“ (sekundär, nur extern), „Vorschau“ (tertiär), „Veröffentlichen“ (primär), ⋯. Autosave „Automatisch gespeichert“ steht dort und blendet nach 3 s aus. - **Reiter:** „Fragen“ (`list-checks`), „Design“ (`palette`), „Antworten“ (`inbox`), gleich breit. - **Fragekarte zu:** Radius 8, px20 py16, Griff, Typ-Icon, Titel 16/500 mit rotem `*` bei Pflicht, darunter der Typ und bei optionalen „ · optional“. Kopieren grau, Löschen rot `#C52020`. Bedingte Fragen: Badge „Bedingt sichtbar“ klein neutral. - **Fragekarte offen:** kein farbiger Rand, sondern 6-px-Streifen links in `#0791F0`. Optionen nur hier. - **Feldtypen:** Text, Mehrzeiliger Text, Auswahl, Mehrfachauswahl, Checkbox, E-Mail, Telefon, Zahl, Datum, Uhrzeit, Datum und Uhrzeit, Datei (`paperclip`), Foto (Kamera, nur intern), Unterschrift. **Nicht:** „Kurzantwort“, „Absatz“, „Multiple-Choice-Frage“. - **„Frage hinzufügen“:** gestrichelte Fläche, 2 px `#B5B5B7`, Icon 24, Schrift 20/500. Neue Frage: Typ „Text“, Name „Unbenannte Frage“, neue Optionen „Auswahl A“, „Auswahl B“. - **Lead:** Banner „Lead-Verknüpfung“ mit „Aktiv“, Text „Neue Antworten werden als Lead gespeichert.“, Knöpfe „Felder zuweisen“ (primär) und „Deaktivieren“ (tertiär). **Eine Pipeline gibt es im Form Builder nicht.** - **Gibt es nicht:** eine Fußzeile, „Zuletzt gespeichert vor wenigen Sekunden“, „Bereit zum Veröffentlichen“. Bausteine dazu in `post7.html`: `fbRahmen`, `fbFrage`, `fbLeiste`, `fbNeu`, `fbFeld`, `fbOffenKopf`, `fbOffenKarte`, `fbOption`, `fbOptionNeu`, `fbSchalter`, `pKurz` (Text mit „…“ kürzen). ## 9. PostGrid-Eigenheit: gleiche Höhe im Carousel Alle Grafiken eines Posts bekommen dieselbe SVG-Höhe, damit die Karten auf allen Slides oben auf einer Linie stehen. Eine kürzere Karte lässt deshalb unten Platz frei. Das ist gewollt. Zu hoch darf keine werden: Sie ragt sonst in den Text. Das Bewegtbild ohne Fließtext hat ein eigenes `visBottom`.