Files
posts/komponenten.md
2026-10-20 18:26:21 +02:00

251 lines
17 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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/<modul>/`, `apps/web/app/modules/<modul>/`, 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:<pfad>`
- 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 <scratchpad>/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 <key>`.
---
## 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/<name>.svg` holen und das Innere
der `<svg>` 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 `<head>`
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`.