rules.md §0 macht es zur Pflicht, komponenten.md ist das Nachschlagewerk mit Tokens, Komponenten, Bausteinen und Vorgehen, CLAUDE.md weist jeden Agenten darauf hin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
210 lines
14 KiB
Markdown
210 lines
14 KiB
Markdown
# 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.
|