Compare commits

1 Commits

3 changed files with 505 additions and 597 deletions

View File

@@ -7,23 +7,11 @@ Setze das Design aus diesem Ordner in unserem Stack um.
Zum Ansehen im Browser öffnen — `support.js` muss im selben Ordner liegen. Zum Ansehen im Browser öffnen — `support.js` muss im selben Ordner liegen.
Diese Dateien sind Design-Referenz, kein Produktionscode: nicht kopieren, sondern Diese Dateien sind Design-Referenz, kein Produktionscode: nicht kopieren, sondern
in unserer bestehenden Umgebung mit unseren Patterns und Komponenten nachbauen. in unserer bestehenden Umgebung mit unseren Patterns und Komponenten nachbauen.
3. Fidelity ist high — pixelgenau umsetzen: Farben, Radien, Übergänge, Hover-/Focus-States 3. Fidelity ist high — pixelgenau umsetzen: Farben, Radien, Übergänge, Hover-/Focus-States,
und Animationsdauern wie im README angegeben. Drag-Schwellen und Animationsdauern wie im README angegeben.
Die visuelle Sprache ist Pinterest Desktop: weißer Hintergrund, 80px-Icon-Rail ohne 4. Umfang: App-Shell (Sidebar + Kopfzeile mit Tabs) und die vier Screens
Beschriftung, Suchfeld als Pille, rote Primäraktion `#e60023`, randlose 16px-Kacheln, Feed, Profil, Erstellen, Einstellungen.
Masonry-Raster. 5. Icons als Lucide-Icons umsetzen (im Prototyp stehen Unicode-Glyphen als Platzhalter).
4. Umfang: App-Shell (Icon-Rail + Kopfzeile + Tab-Leiste) und die vier Screens
Feed, Profil, Erstellen, Einstellungen — inklusive Detailansicht als Overlay
und Auswahlmodus mit Sammelaktion.
5. **Kein Swipen.** Der Feed ist ein Raster, kein Kartenstapel: keine Drag-Geste, keine
Pfeiltasten, kein Undo. „Gemerkt" ist ein Zustand, den ein zweiter Klick zurücknimmt.
6. Karten werden über ihre `id` referenziert (Merken, Ausblenden, Auswahl, Detailansicht),
nicht über einen Index.
7. Hover-Elemente (Merken-Pille, ⋯, Scrim) hängen am Hover der Kachel — `:focus-within`
mitnehmen, sonst sind sie per Tastatur nicht erreichbar.
8. Icons als Lucide-Icons umsetzen (im Prototyp stehen Unicode-Glyphen als Platzhalter).
Medien-Platzhalter (CSS-Verläufe mit Streifen) durch echte Bilder/Videos ersetzen. Medien-Platzhalter (CSS-Verläufe mit Streifen) durch echte Bilder/Videos ersetzen.
Frag nach, bevor du Struktur oder Verhalten abweichend vom README änderst. Frag nach, bevor du Struktur oder Verhalten abweichend vom README änderst.
Die offenen Punkte am Ende des README (Suche, dauerhaftes Ausblenden, Nachladen)
vor der Umsetzung klären.

View File

@@ -1,26 +1,16 @@
# Handoff: Videogen — Web-App (Desktop-Adaption der Mobile-App) # Handoff: Videogen — Web-App (Desktop-Adaption der Mobile-App)
## Überblick ## Überblick
Die bestehende Mobile-App („videogen") als Desktop-Web-App. Statt einer schmalen Handy-Spalte im Die bestehende Mobile-App („videogen", Swipe-Feed für Creator-/Produkt-Posts) als Desktop-Web-App.
Browser: Icon-Rail links, breite Kopfzeile mit Suche und ein **Masonry-Raster** als Feed. Statt einer schmalen Handy-Spalte im Browser: dreispaltiges App-Layout mit permanenter Navigation,
zentralem Kartenstapel und Detailspalte. Swipen wird durch Maus-Drag, Buttons und Tastatur ersetzt.
**Das Swipen entfällt.** In der Mobile-App wird eine Karte nach der anderen durchgeschoben; auf dem
Desktop liegen stattdessen alle Karten nebeneinander und werden einzeln gemerkt, ausgeblendet oder
geöffnet. Damit fallen Kartenstapel, Maus-Drag, Pfeiltasten, die Exit-Animation und das Undo weg —
„gemerkt" ist jetzt ein Zustand, den ein zweiter Klick zurücknimmt, keine Entscheidung in einer
History.
**Visuelle Sprache: Pinterest Desktop.** Heller Hintergrund, schmale Icon-Leiste ohne Beschriftung,
Suchfeld als breite Pille, rote Primäraktion („Merken"), randlose Karten mit 16px-Radius,
gestaffeltes Raster. Referenz ist die Pinterest-Web-App (`de.pinterest.com`); der Screenshot dafür
liegt bisher nicht im Repo, siehe [Offene Punkte](#offene-punkte).
## Zu den Design-Dateien ## Zu den Design-Dateien
Die Dateien in diesem Bundle sind **Design-Referenzen in HTML** — Prototypen, die Aussehen und Die Dateien in diesem Bundle sind **Design-Referenzen in HTML** — Prototypen, die Aussehen und
Verhalten zeigen, **kein Produktionscode zum Kopieren**. Aufgabe ist, diese Designs in der bestehenden Verhalten zeigen, **kein Produktionscode zum Kopieren**. Aufgabe ist, diese Designs in der bestehenden
Umgebung des Ziel-Codebases nachzubauen (React, Vue, Svelte, Next.js …) mit dessen etablierten Patterns Umgebung des Ziel-Codebases nachzubauen (React, Vue, Svelte, Next.js …) mit dessen etablierten Patterns
und Bibliotheken. Existiert noch keine Umgebung, das passendste Framework wählen (Empfehlung: React + und Bibliotheken. Existiert noch keine Umgebung, das passendste Framework wählen (Empfehlung: React +
TypeScript) und dort umsetzen. TypeScript, Motion/Framer Motion für die Kartenanimation) und dort umsetzen.
Die HTML-Datei ist ein „Design Component"-Format: `Videogen Web.dc.html` enthält Template + Logikklasse, Die HTML-Datei ist ein „Design Component"-Format: `Videogen Web.dc.html` enthält Template + Logikklasse,
`support.js` ist die Laufzeit dazu. Zum Ansehen: beide Dateien im selben Ordner belassen und die `support.js` ist die Laufzeit dazu. Zum Ansehen: beide Dateien im selben Ordner belassen und die
@@ -37,188 +27,177 @@ und sollen pixelgenau nachgebaut werden — mit den Komponenten/Utilities des Zi
- Wurzel: `display:flex`, `min-height:100vh`, **`min-width:1180px`** (darunter horizontales Scrollen, - Wurzel: `display:flex`, `min-height:100vh`, **`min-width:1180px`** (darunter horizontales Scrollen,
die App ist Desktop-only gedacht). die App ist Desktop-only gedacht).
- Hintergrund durchgehend `#ffffff`, Grundschrift `#111111`. Keine Verläufe, kein `backdrop-filter`. - Hintergrund (fix, nicht mitscrollend):
`radial-gradient(120% 80% at 50% 0%, #0d4c3d 0%, #0a3a30 34%, #24121a 68%, #40161b 100%)`,
`background-attachment: fixed`. Oben Tannengrün, unten Weinrot — direkt aus der App übernommen.
- Grundschrift `#f2efe9` auf diesem Verlauf.
### 1. Icon-Rail (links) ### 1. Sidebar (links)
- Breite `80px`, `flex:none`, `position:sticky; top:0; height:100vh`, `background:#ffffff`, - Breite `236px`, `flex:none`, `position:sticky; top:0; height:100vh`.
rechte Kante `1px solid #efefef`. Padding `12px 0 16px`, zentrierte Flex-Spalte, `gap:8px`. - `background: rgba(8,26,22,.42)`, `backdrop-filter: blur(14px)`, rechte Kante `1px solid rgba(255,255,255,.08)`.
- **Brand**: 32×32 Kreis `#e60023` in einem 48px-Slot, weißes „V" 17px/700. - Padding `26px 20px`, Inhalt als Flex-Spalte, `gap:28px`.
- **Nav-Buttons**: 48×48, `border-radius:50%`, Icon 20px, **ohne Textlabel** — der Name steht im - **Brand**: 30×30 Kachel, `border-radius:9px`, `linear-gradient(150deg,#2fa87f,#0e5442)`, Buchstabe „V" 15px/700;
`title`-Attribut und wird als Tooltip gezeigt. daneben Wortmarke „videogen" 16px/600, `letter-spacing:-.01em`.
- Inaktiv: transparent, Icon `#111111`. Hover: `background:#e9e9e9`. - **Nav** (Flex-Spalte, `gap:4px`), Einträge: Feed, Profil, Erstellen, Einstellungen.
- Aktiv: `background:#111111`, Icon `#ffffff`. Aktiv + Hover: `background:#000000`. - Item: `display:flex; gap:11px; padding:11px 13px; border-radius:11px; font:500 14px Manrope; text-align:left; cursor:pointer`.
- Transition `.12s ease`. - Inaktiv: transparent, Text `rgba(242,239,233,.7)`, Border transparent.
- **Umsetzungshinweis:** Die Zustände gehören in Klassen (`.rail`, `.rail.active`), nicht in - Aktiv: `background: rgba(126,226,184,.12)`, `border:1px solid rgba(126,226,184,.4)`, Text `#d6fbec`.
Inline-Styles. Im Prototyp setzt die `.dc`-Laufzeit `style-hover` mit `!important`; ein inline - Hover: `background: rgba(255,255,255,.09)`, Text `#ffffff`. Transition `.18s ease`.
gesetztes Aktiv-Schwarz würde beim Hover überschrieben und der aktive Button hellte auf. - Icon-Slot 20px breit (im Prototyp Glyphen ◧ ◍ ⚙ — **in der Implementierung durch Lucide-Icons ersetzen**:
- Reihenfolge oben: **Feed, Profil, Erstellen**. Unten (`margin-top:auto`): **Einstellungen**. `layout-grid`, `user`, `plus`, `settings`).
- Icons (im Prototyp Unicode-Glyphen als Platzhalter) — **Lucide**: `home`, `user`, `plus`, `settings`. - **Fußbereich** (`margin-top:auto`):
- Statistik-Karte: `1px solid rgba(255,255,255,.12)`, `radius:14px`, `padding:14px`, `background: rgba(255,255,255,.03)`.
Kicker „HEUTE" 11px, `letter-spacing:.14em`, uppercase, `rgba(242,239,233,.5)`; Zahl 22px JetBrains Mono,
`font-variant-numeric: tabular-nums`, zweistellig mit führender Null; Label „Karten gesichtet" 12px `rgba(242,239,233,.55)`.
- Konto-Zeile: 32px Kreis `linear-gradient(140deg,#b2323c,#5d1620)`, Name 13px/600, Rolle 11px `rgba(242,239,233,.5)`.
### 2. Kopfzeile ### 2. Kopfzeile (über dem Inhalt)
- `padding:12px 16px 4px`, Flex-Zeile, `gap:8px`. - Höhe `72px`, `padding: 0 34px`, Unterkante `1px solid rgba(255,255,255,.07)`, Inhalt `space-between`.
- **Suchfeld**: `flex:1`, Höhe `48px`, `border-radius:24px`, `background:#efefef`, `padding:0 16px`, - **Tabs** links (`gap:6px`): „Videos", „Folge ich", „Posts". Jeder Tab wechselt den Kartenstapel und
Platzhaltertext „Suchen" 16px `#767676`; links Lupe, rechts Objektiv-Symbol springt zurück auf den Feed-Screen (Index/History zurücksetzen).
(Lucide `search` und `camera`). - `padding:9px 18px; border-radius:999px; font:500 16px Manrope`.
- **Konto**: 40px Kreis `linear-gradient(140deg,#b2323c,#5d1620)` mit weißer Initiale 15px/700, - Aktiv: `background: rgba(255,255,255,.08)`, `border:1px solid rgba(255,255,255,.22)`, Text `#fff`, 600.
daneben 28px Chevron-Button (`▾`, Hover `#e9e9e9`). - Inaktiv: transparent, Text `rgba(242,239,233,.58)`; Hover `#fff`.
- **Rechts**: Zähler „3 / 5" (12px JetBrains Mono, tabular-nums, `rgba(242,239,233,.5)`) und runder
### 3. Tab-Leiste Info-Button 36px (`border:1px solid rgba(255,255,255,.18)`, `background: rgba(255,255,255,.04)`;
- `padding:0 24px`, Unterkante `1px solid #efefef`, Tabs mit `gap:24px`, an der Grundlinie ausgerichtet. Hover `rgba(255,255,255,.12)` + `translateY(-1px)`), toggelt das Kürzel-Panel.
- **Tabs**: „Videos", „Folge ich", „Posts" — drei Raster. Ein Wechsel springt auf den Feed-Screen
und verwirft eine laufende Auswahl.
- `padding:0 2px 12px`, Unterkante `3px solid` — aktiv `#111111`, inaktiv `transparent`.
- Aktiv: `#111111`, 700. Inaktiv: `#767676`, 500; Hover → `#111111`. Schriftgröße 16px.
- **Rechts**: Zähler „**9 Karten · 4 gemerkt**" (13px `#767676`) und der
**Auswählen-Umschalter**: Pille `padding:10px 18px`, `radius:22px`, `font:700 15px`;
aus → `#efefef`/`#111111` mit Beschriftung „Auswählen", an → `#111111`/`#ffffff` mit „Fertig".
--- ---
## Screens ## Screens
### A. Feed (Startansicht) ### A. Feed (Startansicht)
**Zweck:** Material sichten, Gutes merken, Uninteressantes ausblenden. **Zweck:** Karten sichten, merken oder überspringen.
- Bereich: `flex:1; min-height:0; overflow:auto; padding:24px 24px 96px` **Layout:** `display:grid; grid-template-columns: minmax(0,1fr) 372px; flex:1; min-height:0; overflow:auto`.
(unten Platz für die Auswahlleiste).
- **Masonry-Raster**: `columns:5; column-gap:16px` — Spaltenzahl ist als Prop einstellbar (36).
Kachel-Wrapper: `break-inside:avoid; margin-bottom:16px; position:relative`.
#### A1 Kachel #### A1 Kartenbühne (linke Spalte)
- Medienfläche: `position:relative`, **variable Höhe** (Zyklus 320 / 400 / 260 / 340 / 280 / 440 / - Zentriert, `padding:26px 20px 30px`, `gap:22px`, `position:relative`.
300 / 360 / 240 / 380 px), `border-radius:16px`, `overflow:hidden`, **kein Rahmen**, - Bühne: `position:relative; width:min(404px,100%); height:560px; flex:none`.
`cursor:zoom-in` (im Auswahlmodus `pointer`). - Es werden **bis zu 3 Karten** gestapelt gerendert (konfigurierbar 15).
- Inhalt von unten nach oben: - Karte k (0 = oben): `transform: translate3d(0, -12px·k, 0) scale(1 - 0.045·k)`, `z-index: 10-k`,
1. Grundfläche `linear-gradient(155deg, <hue0> 0%, <hue1> 100%)`. `pointer-events` nur auf der obersten.
2. Streifen-Overlay `repeating-linear-gradient(<40+9·i>deg, rgba(255,255,255,.08) 0 2px, transparent 2px 16px)`, - Karte: `position:absolute; inset:0; border-radius:26px; overflow:hidden;
`opacity:.45`. (Platzhalter für das Medium — im Produkt liegt hier Bild oder Video.) border:1px solid rgba(255,255,255,.14); box-shadow: 0 26px 60px rgba(0,0,0,.45);
3. **Hover-Scrim** `rgba(0,0,0,.24)`, `opacity` 0 → 1 in `.12s`. touch-action:none; user-select:none; cursor:grab` (beim Ziehen `grabbing`).
4. Fußzeile auf `linear-gradient(0deg, rgba(0,0,0,.62), transparent)`, `padding:14px`: - Transition: `transform .34s cubic-bezier(.22,.9,.3,1), opacity .3s ease` — während des aktiven Drags `none`.
Caption 14px/700 weiß, darunter „Name · Art" 12px `rgba(255,255,255,.82)`. - **Karteninhalt (von unten nach oben gestapelt):**
- **Hover-Steuerung** (beide `opacity` 0 → 1 in `.12s`, gebunden an den Hover der *Kachel*): 1. Grundfläche: `linear-gradient(155deg, <hue0> 0%, <hue1> 100%)` (pro Karte, siehe Daten unten).
- **Merken-Pille** oben rechts (`right:12px; top:12px`): `padding:11px 16px; radius:22px; 2. Streifen-Overlay: `repeating-linear-gradient(<40+9·i>deg, rgba(255,255,255,.08) 0 2px, transparent 2px 16px)`, `opacity:.5`.
font:700 15px`, ungemerkt `#e60023` mit weißem Text, **gemerkt `#111111` mit „Gemerkt"**. (Im Prototyp Platzhalter für das Video-/Bild-Asset — im Produkt liegt hier das Medium.)
Klick schaltet um — ein zweiter Klick nimmt zurück. 3. Lesbarkeits-Verlauf: `linear-gradient(180deg, rgba(0,0,0,.32) 0%, transparent 26%, transparent 52%, rgba(0,0,0,.66) 100%)`.
- **⋯-Button** unten rechts (`right:12px; bottom:12px`): 40px Kreis `#ffffff`, 4. **Pills** oben links (`padding:18px`, `gap:9px`): Art („Produkt"/„Video") und Assets („3 Bilder").
`box-shadow: 0 1px 8px rgba(0,0,0,.2)`, Hover `#e9e9e9`. `padding:7px 15px; border-radius:999px; border:1px solid rgba(255,255,255,.34);
- **⋯-Menü**: öffnet über dem Button (`right:12px; bottom:56px`), 210px, `radius:16px`, background: rgba(0,0,0,.22); backdrop-filter: blur(6px); font:600 13px`.
`background:#ffffff`, `box-shadow: 0 1px 20px rgba(0,0,0,.22)`, `padding:8px`, 5. **Caption** mittig, 118px über der Unterkante: JetBrains Mono 15px, `padding:9px 16px`,
Einblendung `riseIn .14s`. Einträge `padding:12px 14px`, `radius:12px`, 15px/600, `border-radius:9px`, `background: rgba(0,0,0,.5)`, `backdrop-filter: blur(4px)`.
Hover `#efefef`: **„Nicht mehr zeigen"** (blendet die Kachel aus) und **„Details öffnen"**. 6. **Creator-Zeile** unten (`padding:20px`, `gap:13px`): 46px Avatar-Kreis (Farbverlauf, `border:2px solid rgba(255,255,255,.3)`),
Ein zweiter Klick auf ⋯ schließt das Menü, `Escape` ebenfalls. Name 19px/700 `letter-spacing:-.01em`, Untertitel 14px `rgba(255,255,255,.72)`.
- **Leerzustand** (alles ausgeblendet): zentriert, Titel „Nichts mehr in dieser Ansicht" 20px/700, 7. **Entscheidungs-Badge** (nur oberste Karte, erscheint ab 40px Drag oder beim Exit):
Text 15px `#5f5f5f`, roter Pill-Button „Ausgeblendete wieder anzeigen". `top:86px`, links bei „GEMERKT" / rechts bei „SKIP", `padding:8px 16px; border-radius:10px;
font:800 15px; letter-spacing:.12em`, `border:2px solid` und Text in
Grün `rgba(126,226,184,.9)` / `#a9f0d0` bzw. Rot `rgba(232,120,120,.9)` / `#f0a8a8`,
`rotate(-10deg)` bzw. `rotate(10deg)`, Opacity `min(1, |dx|/110)`, Transition `.16s`.
- **Leerzustand** (Stapel durch): gestrichelter Rahmen `1px dashed rgba(255,255,255,.2)`, `radius:26px`,
Titel „Alles gesichtet" 20px/600, Text „Für heute sind keine neuen Karten mehr in dieser Ansicht.",
Outline-Button „Stapel neu laden" (`border:1px solid rgba(126,226,184,.6)`, Text `#8fe3c1`,
Hover `background: rgba(126,226,184,.14)`), Einblendung `fadeIn .3s`.
- **Aktions-Buttons** unter der Bühne (`gap:18px`), alle `border-radius:50%`,
`background: rgba(14,10,12,.72)`, `border:1px solid rgba(255,255,255,.16)`,
Transition `.18s`, Hover `translateY(-3px) scale(1.05)`, Active `scale(.95)`:
- ✕ Ablehnen — 62px, Hover-Border `rgba(230,120,120,.75)`, Hover-BG `rgba(120,32,38,.5)`.
- ↺ Zurück — 52px, Hover-Border `rgba(255,255,255,.4)`.
- ♥ Merken — 70px, Text `#8fe3c1`, Border `rgba(126,226,184,.45)`, Hover-BG `rgba(20,88,68,.6)`, Hover `scale(1.06)`.
(Icons in der Implementierung: Lucide `x`, `rotate-ccw`, `heart`.)
- **Kürzel-Panel** (Info-Button): absolut `right:26px; top:26px`, 250px, `radius:14px`,
`background: rgba(10,26,22,.9)`, `blur(12px)`, `border:1px solid rgba(255,255,255,.14)`,
Einblendung `riseIn .22s`. Inhalt: ← Ablehnen, → Merken, ↑ Zurücknehmen, Maus Karte ziehen.
#### A2 Detailansicht (Overlay) #### A2 Detailspalte (rechts, 372px)
Ein Klick auf die Kachel öffnet sie — Pinterests Pin-Seite als Overlay. - `border-left:1px solid rgba(255,255,255,.07)`, `background: rgba(8,22,19,.34)`, `padding:30px 28px`,
- Backdrop: `position:fixed; inset:0; background:rgba(0,0,0,.6)`, Inhalt zentriert, Flex-Spalte `gap:24px`, `overflow:auto`.
`padding:32px`, `z-index:60`, Einblendung `fadeIn .16s`. Klick auf den Backdrop schließt. - Kicker „AKTUELLE KARTE" (11px, `letter-spacing:.16em`, uppercase, `rgba(242,239,233,.45)`),
- Karte: `width:min(1016px,100%)`, `max-height:calc(100vh - 64px)`, `background:#ffffff`, Titel = Caption der obersten Karte 26px/600 `letter-spacing:-.02em`, darunter „Name · Untertitel" 14px `rgba(242,239,233,.62)`.
`border-radius:32px`, `overflow:hidden`, `grid-template-columns:1fr 1fr`, `riseIn .2s`. - Hairline `1px rgba(255,255,255,.09)`.
- **Schließen**: 40px Kreis `#ffffff` mit `box-shadow: 0 1px 8px rgba(0,0,0,.2)`, - **Statistik-Raster** 3 Spalten, `gap:12px`: Kachel `border:1px solid rgba(255,255,255,.1)`, `radius:12px`,
absolut `left:16px; top:16px` über dem Medium. Zusätzlich `Escape`. `padding:12px`; Wert 17px JetBrains Mono tabular-nums, Label 11px `rgba(242,239,233,.5)`.
- **Links**: das Medium, `min-height:520px`, Verlauf + Streifen wie auf der Kachel. - **Beschreibung**: Kicker + Fließtext 14px, `line-height:1.75`, `rgba(242,239,233,.78)`, `text-wrap:pretty`.
- **Rechts**: `padding:32px`, `overflow:auto`, Flex-Spalte `gap:20px`: - **Tags**: Pills `padding:6px 12px`, `radius:999px`, `border:1px solid rgba(255,255,255,.18)`, 12px.
- Aktionszeile rechtsbündig: „Nicht mehr zeigen" (`#efefef`, 48px hoch, `radius:24px`, 15px/700) - **Aktionen unten** (`margin-top:auto`, `gap:10px`), beide `padding:13px; radius:12px`:
und die Merken-Pille (48px hoch, `padding:0 24px`, `radius:24px`, rot bzw. schwarz wie auf der Kachel). „Zur Sammlung hinzufügen" (Akzent-Outline `rgba(126,226,184,.55)`, Text `#8fe3c1`, Hover `rgba(126,226,184,.14)`) =
- Titel = Caption, 32px/700, `letter-spacing:-.02em`. gleiche Aktion wie ♥; „Überspringen" (neutrale Outline) = gleiche Aktion wie ✕.
- Creator-Zeile: 48px Avatar-Kreis, Name 16px/700, Untertitel 14px `#767676`.
- Beschreibung 16px, `line-height:1.6`, `#333333`.
- Statistik-Raster 3 Spalten, `gap:10px`: Kachel `background:#f5f5f5`, `radius:16px`, `padding:14px`;
Wert 18px/700 `tabular-nums`, Label 12px `#767676`.
- Tags: Pills `padding:8px 14px`, `radius:18px`, `background:#efefef`, 13px/600.
#### A3 Auswahlmodus
Für das Abarbeiten ganzer Strecken. Umschalter sitzt in der Tab-Leiste.
- Im Auswahlmodus **verschwinden Merken-Pille und ⋯** — auf der Kachel zählt nur noch die Auswahl.
- Jede Kachel bekommt oben links (`left:12px; top:12px`) einen **Auswahlkreis** 28px,
`border:2px solid #ffffff`: nicht gewählt `background:rgba(0,0,0,.35)`, Haken unsichtbar;
gewählt `background:#e60023` mit weißem ✓ 15px/700. `pointer-events:none` — geklickt wird die Kachel.
- Gewählte Kachel zusätzlich `outline:4px solid #e60023; outline-offset:-4px`.
- Ein Klick auf die Kachel wählt aus bzw. ab (öffnet **nicht** die Detailansicht).
- **Auswahlleiste**, sobald mindestens eine Kachel gewählt ist: fixiert `bottom:28px`, zentriert,
`padding:12px 12px 12px 22px`, `radius:32px`, `background:#ffffff`,
`box-shadow: 0 1px 20px rgba(0,0,0,.22)`, `riseIn .2s`, `z-index:45`. Inhalt:
„**3 ausgewählt**" 15px/700, roter Button „Zur Sammlung hinzufügen", grauer „Auswahl aufheben"
(beide `padding:12px 20px`, `radius:24px`, 15px/700).
- „Zur Sammlung hinzufügen" merkt alle gewählten Karten, leert die Auswahl und **verlässt den
Auswahlmodus**; Toast „N Karten zur Sammlung hinzugefügt".
- `Escape` verlässt den Auswahlmodus und verwirft die Auswahl.
### B. Profil ### B. Profil
- `padding:32px 24px 40px`, `overflow:auto`, Einblendung `riseIn .26s`. - `padding:38px 44px`, `overflow:auto`, Einblendung `riseIn .26s`.
- Kopf **zentriert** als Spalte, `gap:10px`, `padding-bottom:28px`: 120px Avatar-Kreis - Kopf: 96px Avatar-Kreis (`linear-gradient(140deg,#b2323c,#5d1620)`, `border:2px solid rgba(255,255,255,.2)`),
(`linear-gradient(140deg,#b2323c,#5d1620)`, ohne Rand), Name 36px/700 `letter-spacing:-.02em`, Name 32px/600 `letter-spacing:-.02em`, Meta „Studio-Konto · seit März 2026" 14px `rgba(242,239,233,.6)`;
Meta „Studio-Konto · seit März 2026" 15px `#5f5f5f`; darunter drei Kennzahlen (`gap:28px`, rechts drei Kennzahlen (`gap:34px`): Posts 38, Gemerkt (live aus dem Feed), Follower 1.204 —
zentriert): Posts 38, **Gemerkt (live: Anzahl der gemerkten Karten)**, Follower 1.204 — Werte 22px JetBrains Mono tabular-nums, Labels 12px. Unterkante Hairline `rgba(255,255,255,.09)`, `padding-bottom:26px`.
Werte 18px/700 `tabular-nums`, Labels 13px `#767676`. - Galerie: `grid-template-columns: repeat(4,1fr)`, `gap:18px`, `margin-top:28px`.
- **Die Galerie zeigt genau die gemerkten Karten** — dasselbe Masonry-Raster wie im Feed, Kachel: Höhe 230px, `radius:16px`, `border:1px solid rgba(255,255,255,.12)`, Kartenverlauf + Streifen (`opacity:.45`),
aber ohne Hover-Steuerung. Ein Klick öffnet die Detailansicht. Titel unten auf `linear-gradient(0deg, rgba(0,0,0,.7), transparent)`, 13px/600, `padding:14px`.
Hover: `translateY(-4px)`, Border `rgba(126,226,184,.5)`, Transition `.2s`.
### C. Erstellen ### C. Erstellen
- `padding:32px 24px 40px`; Inhalt in einer weißen Karte: `max-width:920px`, zentriert, - `padding:38px 44px`; Inhalt `max-width:920px`, `grid-template-columns: 1fr 320px`, `gap:34px`.
`radius:24px`, `box-shadow: 0 1px 20px rgba(0,0,0,.12)`, `padding:32px`, - Titel „Neuen Post erstellen" 30px/600, Untertitel 14px `rgba(242,239,233,.6)`.
`grid-template-columns: 1fr 320px`, `gap:32px`. - **Dropzone**: Höhe 220px, `border:1px dashed rgba(255,255,255,.24)`, `radius:18px`,
- Titel „Neuen Post erstellen" 30px/700, Untertitel 15px `#5f5f5f`. `background: rgba(255,255,255,.03)`; Hover Border `rgba(126,226,184,.6)`, BG `rgba(126,226,184,.06)`.
- **Dropzone**: Höhe 220px, `border:2px dashed #cdcdcd`, `radius:16px`, `background:#f5f5f5`; Text „Dateien hierher ziehen" 15px/600 + „MP4, MOV oder bis zu 10 Bilder" 13px.
Hover Border `#e60023`, BG `#fafafa`. - **Felder** Titel und Untertitel: Label 12px uppercase `letter-spacing:.14em` `rgba(242,239,233,.5)`;
Text „Dateien hierher ziehen" 16px/700 + „MP4, MOV oder bis zu 10 Bilder" 14px `#767676`. Input `padding:13px 15px`, `radius:11px`, `border:1px solid rgba(255,255,255,.16)`,
- **Felder** Titel und Untertitel: Label 13px/700 `#111111`; Input `padding:14px 16px`, `background: rgba(0,0,0,.24)`, Text `#f2efe9`, 15px. Beide sind an die Live-Vorschau gebunden.
`radius:16px`, `border:2px solid #cdcdcd`, `background:#ffffff`, 15px. - **Art**: Segmented Pills „Produkt / Video / Serie", `padding:10px 18px`, `radius:999px`;
Beide sind an die Live-Vorschau gebunden. aktiv `border rgba(126,226,184,.6)`, `background rgba(126,226,184,.12)`, Text `#a9f0d0`.
- **Art**: Pills „Produkt / Video / Serie", `padding:11px 18px`, `radius:22px`, `font:700 15px`; - **Aktionen**: „Veröffentlichen" (Akzent-Outline, löst Toast „Post veröffentlicht" aus),
aktiv `background:#111111`, Text `#ffffff`; inaktiv `background:#efefef`, Text `#111111`. „Als Entwurf sichern" (neutrale Outline), beide `padding:13px 24px`, `radius:12px`.
- **Aktionen**: „Veröffentlichen" (rot `#e60023`, löst Toast „Post veröffentlicht" aus), - **Vorschau** (rechte Spalte): Kicker + Karte 420px hoch, `radius:22px`,
„Als Entwurf sichern" (`#efefef`), beide `padding:14px 24px`, `radius:24px`, `font:700 15px`. `linear-gradient(155deg,#1f8163,#0d4034)` + `repeating-linear-gradient(48deg, rgba(255,255,255,.07) 0 2px, transparent 2px 15px)`,
- **Vorschau** (rechte Spalte): Kicker 12px/600 `#767676` + Karte 420px hoch, `radius:16px`, unten Titel 18px/700 und Untertitel 13px auf schwarzem Verlauf — spiegelt die Eingaben live.
Verlauf + Streifen, unten Titel 18px/700 und Untertitel 14px auf schwarzem Verlauf —
spiegelt die Eingaben live.
### D. Einstellungen ### D. Einstellungen
- `padding:32px 24px 40px`, Inhalt `max-width:660px`, zentriert. Titel „Einstellungen" 30px/700. - `padding:38px 44px`, Inhalt `max-width:660px`. Titel „Einstellungen" 30px/600.
- Liste: Container `radius:16px`, `overflow:hidden`, `box-shadow: 0 1px 20px rgba(0,0,0,.12)`, - Liste: Container `border:1px solid rgba(255,255,255,.1)`, `radius:16px`, `overflow:hidden`.
**kein Rahmen**. Zeile `padding:20px`, `background:#ffffff`, Trenner `1px solid #efefef`; Zeile `padding:18px 20px`, `background: rgba(255,255,255,.02)`, Trenner `1px solid rgba(255,255,255,.07)`;
Label 16px/700, Hinweis 14px `#767676`. Label 15px/600, Hinweis 13px `rgba(242,239,233,.55)`.
- Toggle: Track 52×30, `radius:999px`, kein Rahmen; aus `#cdcdcd`, an `#e60023`. - Toggle: Track 52×30, `radius:999px`; aus → `background rgba(255,255,255,.05)`, `border rgba(255,255,255,.2)`,
Knopf 24px, immer `#ffffff` mit `box-shadow: 0 1px 3px rgba(0,0,0,.25)`, links 3px → 25px, Knopf links 3px, `rgba(242,239,233,.55)`; an → `background rgba(126,226,184,.18)`,
Transition `left .2s cubic-bezier(.22,.9,.3,1)`. `border rgba(126,226,184,.6)`, Knopf links 25px, `#8fe3c1`. Knopf 22px, Transition
- Einträge: Videos automatisch abspielen (an), Ton beim Öffnen (aus), Wochenrückblick (an), `left .2s cubic-bezier(.22,.9,.3,1), background .2s`.
Bewegte Übergänge (an). - Einträge: Videos automatisch abspielen (an), Ton beim Öffnen (aus), Wochenrückblick (an), Bewegte Übergänge (an).
--- ---
## Interaktion & Verhalten ## Interaktion & Verhalten
**Merken**zwei Wege, beide schalten um (zweiter Klick nimmt zurück): **Karte entscheidendrei gleichwertige Wege:**
1. Merken-Pille auf der Kachel (erscheint beim Hover). 1. **Maus-Drag**: Pointer Events auf der obersten Karte (`pointerdown` → `setPointerCapture`,
2. Merken-Button in der Detailansicht. `pointermove` → Delta, `pointerup`/`pointercancel` → Auswertung).
Toast: „Zu <Name>' gemerkt" bzw. „Aus der Sammlung entfernt". Live-Transform: `translate3d(dx, dy, 0) rotate(dx/22 deg)`, Transition währenddessen aus.
Schwelle **±120px**: `dx > 120` = merken, `dx < -120` = ablehnen, sonst Rücksprung (Transition wieder an).
2. **Buttons** ✕ / ↺ / ♥ unter der Bühne (und die beiden Buttons in der Detailspalte).
3. **Tastatur** (global, nur auf dem Feed-Screen, mit `preventDefault`):
`ArrowLeft` = ablehnen, `ArrowRight` = merken, `ArrowUp` = zurücknehmen.
**Ausblenden** — „Nicht mehr zeigen" im ⋯-Menü oder in der Detailansicht. Die Kachel verschwindet **Exit-Animation:** `transform: translate3d(±620px, -40px, 0) rotate(±22deg)`, `opacity: 0`,
aus dem Raster, eine offene Detailansicht schließt sich, Toast „Wird nicht mehr gezeigt". Dauer **320ms**, danach Index +1 und `exiting` zurücksetzen. Während `exiting` sind weitere
Entscheidungen gesperrt.
**Öffnen** — Klick auf die Kachel oder „Details öffnen" im ⋯-Menü. **Undo:** letzte Entscheidung aus der History, Index zurück, „Gemerkt"-Zähler ggf. 1.
Leere History → Toast „Nichts zurückzunehmen".
**Mehrfachauswahl** — siehe [A3](#a3-auswahlmodus). **Toast:** fixiert `bottom:34px`, horizontal zentriert, `padding:12px 22px`, `radius:999px`,
`background: rgba(10,24,20,.92)`, `blur(10px)`, `border:1px solid rgba(255,255,255,.16)`, 14px,
**Tastatur:** `Escape` schließt der Reihe nach: offene Detailansicht → offenes ⋯-Menü → Einblendung `riseIn .2s`, verschwindet nach **1700ms**. Texte: „Zu <Name>' gemerkt",
Auswahlmodus. Weitere Kürzel gibt es nicht mehr; die Pfeiltasten sind mit dem Swipen entfallen. „Übersprungen", „Nichts zurückzunehmen", „Post veröffentlicht".
**Hover-Zustände:** Scrim, Merken-Pille und ⋯ hängen am Hover der *Kachel*, nicht am eigenen Element
(im Prototyp `.pin:hover .pin-hover`, zusätzlich `:focus-within` für Tastaturbedienung).
Im Ziel-Framework als Hover-State der Kartenkomponente umsetzen — und den Fokusfall nicht vergessen,
sonst sind Merken und ⋯ per Tastatur nicht erreichbar.
**Toast:** fixiert, horizontal zentriert, `bottom:28px` — bei sichtbarer Auswahlleiste `96px`,
damit beide nicht übereinanderliegen. `padding:16px 22px`, `radius:16px`, `background:#ffffff`,
`box-shadow: 0 1px 20px rgba(0,0,0,.22)`, 15px/600, `riseIn .2s`, verschwindet nach **1700ms**.
**Keyframes:** **Keyframes:**
- `riseIn`: `opacity:0; translateY(14px)` → `opacity:1; none` (.14.26s ease) - `riseIn`: `opacity:0; translateY(14px)` → `opacity:1; none` (.2.26s ease)
- `fadeIn`: `opacity:0` → `1` - `fadeIn`: `opacity:0` → `1`
- Fokus: `:focus-visible { outline: 2px solid #0074e8; outline-offset: 2px; }` — nie der Browser-Default. - Fokus: `:focus-visible { outline: 2px solid #7ee2b8; outline-offset: 2px; }` — nie der Browser-Default.
**Responsive:** Desktop-only, `min-width:1180px`; darunter horizontal scrollen. **Responsive:** Desktop-only, `min-width:1180px`; darunter horizontal scrollen.
Der Feed-Bereich scrollt bei geringer Fensterhöhe (`min-height:0; overflow:auto`).
--- ---
@@ -226,23 +205,22 @@ damit beide nicht übereinanderliegen. `padding:16px 22px`, `radius:16px`, `back
| State | Typ | Zweck | | State | Typ | Zweck |
|---|---|---| |---|---|---|
| `screen` | `'feed' \| 'profile' \| 'create' \| 'settings'` | aktive Ansicht (Icon-Rail) | | `screen` | `'feed' \| 'profile' \| 'create' \| 'settings'` | aktive Ansicht (Sidebar) |
| `tab` | `'videos' \| 'following' \| 'posts'` | aktives Raster; Wechsel verwirft Auswahl und Menü | | `tab` | `'videos' \| 'following' \| 'posts'` | aktiver Stapel; Wechsel setzt `idx`/`history` zurück und springt auf `feed` |
| `saved` | `string[]` (Karten-IDs) | gemerkte Karten — speist Profil-Galerie und Kennzahl | | `idx` | number | Index der obersten Karte im aktuellen Stapel |
| `hidden` | `string[]` (Karten-IDs) | ausgeblendete Karten, aus dem Raster gefiltert | | `drag` | `{x, y, active} \| null` | laufende Drag-Geste |
| `selectMode` | boolean | Auswahlmodus aktiv | | `exiting` | `'left' \| 'right' \| null` | laufende Exit-Animation (sperrt Eingaben) |
| `selection` | `string[]` (Karten-IDs) | aktuell gewählte Kacheln | | `history` | `{idx, dir}[]` | für Undo |
| `detail` | `string \| null` | ID der offenen Detailansicht | | `seen`, `saved` | number | Zähler Sidebar / Profil |
| `menuFor` | `string \| null` | ID der Kachel mit offenem ⋯-Menü | | `showInfo` | boolean | Kürzel-Panel |
| `toast` | string | Toast-Text (leer = versteckt) | | `toast` | string | Toast-Text (leer = versteckt) |
| `draftTitle`, `draftSub`, `draftKind` | string | Formular „Erstellen", live in der Vorschau | | `draftTitle`, `draftSub`, `draftKind` | string | Formular „Erstellen", live in der Vorschau |
| `settings` | `{autoplay, sound, digest, motion}` booleans | Einstellungen | | `settings` | `{autoplay, sound, digest, motion}` booleans | Einstellungen |
**Datenbedarf pro Karte:** `id`, `name`, `sub`, `kind`, `assets`, `caption`, `hue: [from, to]`, **Datenbedarf pro Karte:** `name`, `sub`, `kind`, `assets`, `caption`, `hue: [from, to]`,
`avatar` (Verlauf oder Bild-URL), `body`, `tags: string[]`, `stats: [wert, label][]`. `avatar` (Verlauf oder Bild-URL), `body`, `tags: string[]`, `stats: [wert, label][]`.
Die `id` ist neu und trägt jetzt alles — Merken, Ausblenden, Auswahl und Detailansicht referenzieren Im Produkt ersetzt das Medium (Bild/Video) die Verlaufs-/Streifenfläche; alles andere bleibt.
Karten darüber, nicht mehr über einen Index. Im Produkt ersetzt das Medium (Bild/Video) die Die drei Tabs liefern drei Stapel (Videos, Folge ich, Posts).
Verlaufs-/Streifenfläche. Die drei Tabs liefern drei Raster.
--- ---
@@ -251,49 +229,33 @@ Verlaufs-/Streifenfläche. Die drei Tabs liefern drei Raster.
**Farben** **Farben**
| Rolle | Wert | | Rolle | Wert |
|---|---| |---|---|
| Seite | `#ffffff` | | Seitenverlauf | `#0d4c3d` → `#0a3a30` → `#24121a` → `#40161b` |
| Text | `#111111` | | Text | `#f2efe9` |
| Text gedämpft | `#5f5f5f` (sekundär), `#767676` (Labels, inaktive Tabs), `#333333` (Beschreibung) | | Text gedämpft | `rgba(242,239,233,.62)` / `.55` / `.5` / `.45` |
| Akzent (Rot) | `#e60023`, Hover `#ad081b`; auch Auswahl-Outline und Auswahlkreis | | Akzent (Mint) | `#8fe3c1`, hell `#a9f0d0` / `#d6fbec`, Fokus `#7ee2b8` |
| Dunkel | `#111111` (aktiver Rail-Button, aktive Art-Pille, **gemerkt**-Zustand, „Fertig"), Hover `#000000` | | Akzent-Tint | `rgba(126,226,184,.12)` Fläche, `.4.6` Rahmen, `.14` Hover |
| Neutrale Flächen | `#efefef` (Suchfeld, Buttons, Tags), Hover `#e2e2e2`; `#e9e9e9` (Rail-/⋯-Hover); `#f5f5f5` (Statistik-Kacheln, Dropzone) | | Ablehnen | `rgba(232,120,120,.9)` Rahmen, `#f0a8a8` Text, `rgba(120,32,38,.5)` Hover |
| Linien | `#efefef` (Trenner, Kanten), `#cdcdcd` (Eingabe-Rahmen, Toggle aus) | | Flächen | `rgba(8,26,22,.42)` Sidebar, `rgba(8,22,19,.34)` Detailspalte, `rgba(255,255,255,.02.09)` |
| Auf Medien | `rgba(255,255,255,.82)` Untertitel, `rgba(0,0,0,.24)` Hover-Scrim, `rgba(0,0,0,.62)` Fußzeilen-Verlauf, `rgba(0,0,0,.35)` Auswahlkreis leer | | Rahmen | `rgba(255,255,255,.07)` / `.1` / `.12` / `.16` / `.18` / `.22` |
| Overlay | `rgba(0,0,0,.6)` Backdrop der Detailansicht |
| Fokus | `#0074e8` |
| Karten-Verläufe | grün `#1f8163#0d4034`, blau `#256f8b#0c2f3f`, ocker `#8a6a2f#3a2a11`, rot `#7a2f38#2c1014`, tannen `#2f6f5a#0e332b`, türkis `#1c6f7f#0a2c33`, violett `#6a2f7a#26102c`, gold `#7a5a2a#2f2110` | | Karten-Verläufe | grün `#1f8163#0d4034`, blau `#256f8b#0c2f3f`, ocker `#8a6a2f#3a2a11`, rot `#7a2f38#2c1014`, tannen `#2f6f5a#0e332b`, türkis `#1c6f7f#0a2c33`, violett `#6a2f7a#26102c`, gold `#7a5a2a#2f2110` |
**Typografie** — **nur Manrope** 400/500/600/700. Zahlen mit `font-variant-numeric: tabular-nums`. **Typografie** — Manrope 400/500/600/700 (UI), JetBrains Mono 400/500 (Zahlen, Captions).
Skala: 36 / 32 / 30 / 20 / 18 / 17 / 16 / 15 / 14 / 13 / 12 px. Skala: 32 / 30 / 26 / 22 / 19 / 17 / 16 / 15 / 14 / 13 / 12 / 11 px.
Große Überschriften `letter-spacing:-.02em`. Kicker sind 12px/600 in `#767676`, normal geschrieben. Große Überschriften `letter-spacing:-.02em`, Kicker `+.14em`/`.16em` uppercase.
Alle Zahlen mit `font-variant-numeric: tabular-nums`.
**Abstände** 2 / 4 / 6 / 8 / 10 / 12 / 14 / 16 / 18 / 20 / 22 / 24 / 28 / 32 / 40 / 56 / 96 px. **Abstände** 3 / 4 / 6 / 8 / 9 / 10 / 12 / 13 / 14 / 18 / 20 / 22 / 26 / 28 / 34 / 38 / 44 px.
**Radien** 12px (Menüeinträge), 16px (Kacheln, Eingaben, Toast, Menü), 18 / 22 / 24px (Pillen), **Radien** 9 / 11 / 12 / 14 / 16 / 18 / 22 / 26 px, Pills `999px`, Kreise `50%`.
32px (Detailansicht, Auswahlleiste), `50%` Kreise, `999px` Toggle-Track. **Schatten** nur an der Karte: `0 26px 60px rgba(0,0,0,.45)`.
**Schatten** `0 1px 8px rgba(0,0,0,.2)` schwebende Rundbuttons · `0 1px 20px rgba(0,0,0,.12)` **Blur** 4 / 6 / 10 / 12 / 14 px (`backdrop-filter`).
Flächen/Listen · `.22` Menü, Auswahlleiste, Toast · `0 1px 3px rgba(0,0,0,.25)` Toggle-Knopf. **Transitions** `.16s` / `.18s` / `.2s` / `.3s` ease, Karten-Bewegung `.34s cubic-bezier(.22,.9,.3,1)`, Exit `320ms`.
**Kein `backdrop-filter`** — die Oberfläche ist deckend.
**Transitions** `.12s ease` (Hover, Farbwechsel, Ein-/Ausblenden), `.2s` (Toggle).
Karten- und Exit-Animationen des alten Stapels entfallen ersatzlos.
--- ---
## Assets ## Assets
Keine Binärdateien im Prototyp. Alle Medien sind Platzhalter aus CSS-Verläufen und Streifenmustern — Keine Binärdateien. Alle Medien sind Platzhalter aus CSS-Verläufen und Streifenmustern —
im Produkt durch echte Bilder/Videos ersetzen. Icons: **Lucide** (im Prototyp Unicode-Glyphen als im Produkt durch echte Bilder/Videos ersetzen. Icons: **Lucide** (im Prototyp Unicode-Glyphen als Platzhalter).
Platzhalter). Fonts: Google Fonts — **nur Manrope**. Fonts: Google Fonts — Manrope, JetBrains Mono.
## Offene Punkte
- **Suchfeld ohne Funktion.** Die Pinterest-Kopfzeile lebt von der Suche, im Prototyp ist sie eine
reine Attrappe — es gibt bisher keine Suchfunktion in der App. Vor der Umsetzung klären, ob Suche
gebaut wird; wenn nicht, gehört das Feld raus, statt eine Funktion vorzutäuschen.
- **Ausblenden ist im Prototyp flüchtig.** `hidden` lebt nur im Zustand der Seite. Im Produkt muss
entschieden werden, ob „Nicht mehr zeigen" dauerhaft pro Konto gilt und ob es einen Weg zurück
gibt (im Prototyp nur über den Leerzustand).
- **Nachladen fehlt.** Das Raster zeigt einen endlichen Satz. Für echte Datenmengen braucht es
Infinite Scroll oder Paginierung samt Ladeplatzhaltern — beides ist hier nicht gestaltet.
- **Pinterest-Referenz liegt nicht im Repo.** Die Vorlage kam als Screenshot im Chat. Wer sie
dauerhaft braucht, legt sie als `referenz/pinterest-desktop.png` daneben.
## Dateien in diesem Bundle ## Dateien in diesem Bundle
- `PROMPT.md` — fertiger Auftrag zum Einfügen in Claude Code. - `PROMPT.md` — fertiger Auftrag zum Einfügen in Claude Code.

View File

@@ -10,138 +10,190 @@
<helmet> <helmet>
<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap" rel="stylesheet"> <link href="https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style> <style>
html, body { margin:0; padding:0; background:#ffffff; } html, body { margin:0; padding:0; background:#120b0d; }
* { box-sizing: border-box; } * { box-sizing: border-box; }
a { color:#111111; text-decoration:none; } a { color:#8fe3c1; text-decoration:none; }
::selection { background:rgba(230,0,35,.16); } a:hover { color:#b9f2da; }
button:focus-visible, [tabindex]:focus-visible, input:focus-visible { outline:2px solid #0074e8; outline-offset:2px; } ::selection { background:rgba(126,226,184,.28); }
button:focus-visible, [tabindex]:focus-visible { outline:2px solid #7ee2b8; outline-offset:2px; }
@keyframes riseIn { from { opacity:0; transform:translateY(14px);} to { opacity:1; transform:none;} } @keyframes riseIn { from { opacity:0; transform:translateY(14px);} to { opacity:1; transform:none;} }
@keyframes fadeIn { from { opacity:0 } to { opacity:1 } } @keyframes fadeIn { from { opacity:0 } to { opacity:1 } }
/* Hover-Steuerung haengt an der Kachel, nicht am Element selbst. */ @keyframes pulseRing { 0%{ box-shadow:0 0 0 0 rgba(126,226,184,.35)} 70%{ box-shadow:0 0 0 18px rgba(126,226,184,0)} 100%{ box-shadow:0 0 0 0 rgba(126,226,184,0)} }
.pin-hover { opacity:0; transition:opacity .12s ease; }
.pin:hover .pin-hover, .pin:focus-within .pin-hover { opacity:1; }
.pin-scrim { opacity:0; transition:opacity .12s ease; }
.pin:hover .pin-scrim { opacity:1; }
/* Rail-Zustaende als Klassen: das Runtime setzt style-hover mit !important
und wuerde ein inline gesetztes Aktiv-Schwarz beim Hover ueberschreiben. */
.rail { background:transparent; color:#111111; }
.rail:hover { background:#e9e9e9; }
.rail.active { background:#111111; color:#ffffff; }
.rail.active:hover { background:#000000; }
</style> </style>
</helmet> </helmet>
<div style="min-height:100vh; width:100%; min-width:1180px; display:flex; font-family:'Manrope',system-ui,sans-serif; color:#111111; background:#ffffff;"> <div style="min-height:100vh; width:100%; min-width:1180px; display:flex; font-family:'Manrope',system-ui,sans-serif; color:#f2efe9; background:radial-gradient(120% 80% at 50% 0%, #0d4c3d 0%, #0a3a30 34%, #24121a 68%, #40161b 100%); background-attachment:fixed;">
<aside style="width:80px; flex:none; display:flex; flex-direction:column; align-items:center; gap:8px; padding:12px 0 16px; border-right:1px solid #efefef; background:#ffffff; position:sticky; top:0; height:100vh;"> <aside style="width:236px; flex:none; display:flex; flex-direction:column; gap:28px; padding:26px 20px; border-right:1px solid rgba(255,255,255,.08); background:rgba(8,26,22,.42); backdrop-filter:blur(14px); position:sticky; top:0; height:100vh;">
<div style="width:48px; height:48px; display:grid; place-items:center; margin-bottom:4px;"> <div style="display:flex; align-items:center; gap:10px;">
<div style="width:32px; height:32px; border-radius:50%; background:#e60023; display:grid; place-items:center; color:#ffffff; font-size:17px; font-weight:700;">V</div> <div style="width:30px; height:30px; border-radius:9px; background:linear-gradient(150deg,#2fa87f,#0e5442); display:grid; place-items:center; font-size:15px; font-weight:700;">V</div>
<div style="font-size:16px; font-weight:600; letter-spacing:-.01em;">videogen</div>
</div> </div>
<nav style="display:flex; flex-direction:column; align-items:center; gap:8px;"> <nav style="display:flex; flex-direction:column; gap:4px;">
<sc-for list="{{ navTop }}" as="item" hint-placeholder-count="3"> <sc-for list="{{ navItems }}" as="item" hint-placeholder-count="4">
<button onClick="{{ item.go }}" title="{{ item.label }}" class="{{ item.cls }}" style="{{ item.style }}">{{ item.glyph }}</button> <button onClick="{{ item.go }}" style="{{ item.style }}" style-hover="background:rgba(255,255,255,.09); color:#ffffff;">
<span style="width:20px; display:grid; place-items:center; opacity:.9;">{{ item.glyph }}</span>
<span>{{ item.label }}</span>
</button>
</sc-for> </sc-for>
</nav> </nav>
<div style="margin-top:auto; display:flex; flex-direction:column; align-items:center; gap:10px;"> <div style="margin-top:auto; display:flex; flex-direction:column; gap:14px;">
<sc-for list="{{ navBottom }}" as="item" hint-placeholder-count="1"> <div style="border:1px solid rgba(255,255,255,.12); border-radius:14px; padding:14px; background:rgba(255,255,255,.03);">
<button onClick="{{ item.go }}" title="{{ item.label }}" class="{{ item.cls }}" style="{{ item.style }}">{{ item.glyph }}</button> <div style="font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:rgba(242,239,233,.5); margin-bottom:8px;">Heute</div>
</sc-for> <div style="font-family:'JetBrains Mono',monospace; font-size:22px; font-variant-numeric:tabular-nums;">{{ swipesToday }}</div>
<div style="font-size:12px; color:rgba(242,239,233,.55);">Karten gesichtet</div>
</div>
<div style="display:flex; align-items:center; gap:10px; padding:8px 6px;">
<div style="width:32px; height:32px; border-radius:50%; background:linear-gradient(140deg,#b2323c,#5d1620); flex:none;"></div>
<div style="min-width:0;">
<div style="font-size:13px; font-weight:600;">Jonas K.</div>
<div style="font-size:11px; color:rgba(242,239,233,.5);">Studio-Konto</div>
</div>
</div>
</div> </div>
</aside> </aside>
<main style="flex:1; min-width:0; display:flex; flex-direction:column;"> <main style="flex:1; min-width:0; display:flex; flex-direction:column;">
<header style="flex:none; display:flex; align-items:center; gap:8px; padding:12px 16px 4px;"> <header style="height:72px; flex:none; display:flex; align-items:center; justify-content:space-between; padding:0 34px; border-bottom:1px solid rgba(255,255,255,.07);">
<div style="flex:1; min-width:0; display:flex; align-items:center; gap:12px; height:48px; padding:0 16px; border-radius:24px; background:#efefef; color:#767676; font-size:16px;"> <div style="display:flex; gap:6px; align-items:center;">
<span style="font-size:15px;"></span> <sc-for list="{{ tabs }}" as="t" hint-placeholder-count="3">
<span style="flex:1; min-width:0;">Suchen</span> <button onClick="{{ t.go }}" style="{{ t.style }}" style-hover="color:#ffffff;">{{ t.label }}</button>
<span style="font-size:15px;"></span> </sc-for>
</div>
<div style="display:flex; align-items:center; gap:12px;">
<div style="font-size:12px; color:rgba(242,239,233,.5); font-family:'JetBrains Mono',monospace; font-variant-numeric:tabular-nums;">{{ counter }}</div>
<button onClick="{{ toggleInfo }}" title="Kürzel anzeigen" style="width:36px; height:36px; border-radius:50%; border:1px solid rgba(255,255,255,.18); background:rgba(255,255,255,.04); color:#f2efe9; font-size:14px; cursor:pointer; transition:background .18s ease, transform .18s ease;" style-hover="background:rgba(255,255,255,.12); transform:translateY(-1px);">i</button>
</div> </div>
<button title="Konto" style="width:40px; height:40px; flex:none; border:none; border-radius:50%; background:linear-gradient(140deg,#b2323c,#5d1620); color:#ffffff; font-size:15px; font-weight:700; cursor:pointer; display:grid; place-items:center;">J</button>
<button title="Konto-Menü" style="width:28px; height:28px; flex:none; border:none; border-radius:50%; background:transparent; color:#5f5f5f; font-size:12px; cursor:pointer;" style-hover="background:#e9e9e9;"></button>
</header> </header>
<div style="flex:none; display:flex; align-items:flex-end; justify-content:space-between; gap:20px; padding:0 24px; border-bottom:1px solid #efefef;">
<div style="display:flex; gap:24px; align-items:flex-end;">
<sc-for list="{{ tabs }}" as="t" hint-placeholder-count="3">
<button onClick="{{ t.go }}" style="{{ t.style }}" style-hover="color:#111111;">{{ t.label }}</button>
</sc-for>
</div>
<div style="display:flex; align-items:center; gap:12px; padding-bottom:8px;">
<div style="font-size:13px; color:#767676;">{{ counter }}</div>
<button onClick="{{ toggleSelectMode }}" style="{{ selectToggleStyle }}">{{ selectToggleLabel }}</button>
</div>
</div>
<sc-if value="{{ isFeed }}" hint-placeholder-val="{{ true }}"> <sc-if value="{{ isFeed }}" hint-placeholder-val="{{ true }}">
<section style="flex:1; min-height:0; overflow:auto; padding:24px 24px 96px;"> <section style="flex:1; min-height:0; overflow:auto; display:grid; grid-template-columns:minmax(0,1fr) 372px; gap:0;">
<div style="{{ gridStyle }}">
<sc-for list="{{ grid }}" as="c" hint-placeholder-count="10"> <div style="position:relative; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:26px 20px 30px; gap:22px; min-width:0; min-height:min-content;">
<div class="pin" style="position:relative; break-inside:avoid; margin-bottom:16px;"> <div style="position:relative; width:min(404px,100%); height:560px; flex:none;">
<div onClick="{{ c.activate }}" style="{{ c.media }}"> <sc-for list="{{ stack }}" as="c" hint-placeholder-count="3">
<div style="position:absolute; inset:0; background:{{ c.stripes }}; opacity:.45;"></div> <div style="{{ c.shell }}" onPointerDown="{{ c.down }}" onPointerMove="{{ c.move }}" onPointerUp="{{ c.up }}" onPointerCancel="{{ c.up }}">
<div class="pin-scrim" style="position:absolute; inset:0; background:rgba(0,0,0,.24);"></div> <div style="{{ c.media }}">
<div style="position:absolute; inset:auto 0 0 0; padding:14px; background:linear-gradient(0deg, rgba(0,0,0,.62), transparent); color:#ffffff;"> <div style="position:absolute; inset:0; background:{{ c.stripes }}; opacity:.5;"></div>
<div style="font-size:14px; font-weight:700; text-wrap:pretty;">{{ c.caption }}</div> <div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,0,0,.32) 0%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 52%, rgba(0,0,0,.66) 100%);"></div>
<div style="font-size:12px; color:rgba(255,255,255,.82); margin-top:2px;">{{ c.name }} · {{ c.kind }}</div>
</div> <div style="position:relative; display:flex; gap:9px; padding:18px;">
<div style="{{ c.checkbox }}"></div> <span style="padding:7px 15px; border-radius:999px; border:1px solid rgba(255,255,255,.34); background:rgba(0,0,0,.22); backdrop-filter:blur(6px); font-size:13px; font-weight:600;">{{ c.kind }}</span>
<span style="padding:7px 15px; border-radius:999px; border:1px solid rgba(255,255,255,.34); background:rgba(0,0,0,.22); backdrop-filter:blur(6px); font-size:13px; font-weight:600;">{{ c.assets }}</span>
</div> </div>
<div class="pin-hover" style="{{ c.saveSlot }}"> <div style="position:absolute; inset:auto 0 118px 0; display:flex; justify-content:center; padding:0 22px;">
<button onClick="{{ c.toggleSave }}" style="{{ c.saveStyle }}" style-hover="filter:brightness(.88);">{{ c.saveLabel }}</button> <div style="font-family:'JetBrains Mono',monospace; font-size:15px; padding:9px 16px; border-radius:9px; background:rgba(0,0,0,.5); backdrop-filter:blur(4px); text-align:center; text-wrap:pretty;">{{ c.caption }}</div>
</div>
<div class="pin-hover" style="{{ c.menuSlot }}">
<button onClick="{{ c.openMenu }}" title="Weitere Optionen" style="{{ c.menuBtn }}" style-hover="background:#e9e9e9;"></button>
</div> </div>
<sc-if value="{{ c.menuOpen }}" hint-placeholder-val="{{ false }}"> <div style="position:absolute; inset:auto 0 0 0; display:flex; align-items:center; gap:13px; padding:20px;">
<div style="position:absolute; right:12px; bottom:56px; width:210px; border-radius:16px; background:#ffffff; box-shadow:0 1px 20px rgba(0,0,0,.22); padding:8px; z-index:20; animation:riseIn .14s ease;"> <div style="width:46px; height:46px; border-radius:50%; flex:none; background:{{ c.avatar }}; border:2px solid rgba(255,255,255,.3);"></div>
<button onClick="{{ c.hide }}" style="width:100%; text-align:left; padding:12px 14px; border:none; border-radius:12px; background:transparent; color:#111111; font-family:inherit; font-size:15px; font-weight:600; cursor:pointer;" style-hover="background:#efefef;">Nicht mehr zeigen</button> <div style="min-width:0;">
<button onClick="{{ c.open }}" style="width:100%; text-align:left; padding:12px 14px; border:none; border-radius:12px; background:transparent; color:#111111; font-family:inherit; font-size:15px; font-weight:600; cursor:pointer;" style-hover="background:#efefef;">Details öffnen</button> <div style="font-size:19px; font-weight:700; letter-spacing:-.01em;">{{ c.name }}</div>
<div style="font-size:14px; color:rgba(255,255,255,.72);">{{ c.sub }}</div>
</div>
</div>
<div style="{{ c.badge }}">{{ c.badgeText }}</div>
</div>
</div>
</sc-for>
<sc-if value="{{ stackEmpty }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute; inset:0; border:1px dashed rgba(255,255,255,.2); border-radius:26px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:16px; text-align:center; padding:40px; animation:fadeIn .3s ease;">
<div style="font-size:20px; font-weight:600;">Alles gesichtet</div>
<div style="font-size:14px; color:rgba(242,239,233,.6); max-width:230px; text-wrap:pretty;">Für heute sind keine neuen Karten mehr in dieser Ansicht.</div>
<button onClick="{{ reset }}" style="padding:11px 20px; border-radius:999px; border:1px solid rgba(126,226,184,.6); background:transparent; color:#8fe3c1; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer; transition:background .18s ease;" style-hover="background:rgba(126,226,184,.14);">Stapel neu laden</button>
</div> </div>
</sc-if> </sc-if>
</div> </div>
<div style="display:flex; align-items:center; gap:18px;">
<button onClick="{{ onNope }}" title="Ablehnen — Pfeil links" style="width:62px; height:62px; border-radius:50%; border:1px solid rgba(255,255,255,.16); background:rgba(14,10,12,.72); color:#f2efe9; font-size:22px; cursor:pointer; display:grid; place-items:center; transition:transform .18s ease, border-color .18s ease, background .18s ease;" style-hover="transform:translateY(-3px) scale(1.05); border-color:rgba(230,120,120,.75); background:rgba(120,32,38,.5);" style-active="transform:scale(.95);"></button>
<button onClick="{{ onUndo }}" title="Zurück — Pfeil hoch" style="width:52px; height:52px; border-radius:50%; border:1px solid rgba(255,255,255,.16); background:rgba(14,10,12,.72); color:#f2efe9; font-size:17px; cursor:pointer; display:grid; place-items:center; transition:transform .18s ease, border-color .18s ease;" style-hover="transform:translateY(-3px) scale(1.05); border-color:rgba(255,255,255,.4);" style-active="transform:scale(.95);"></button>
<button onClick="{{ onLike }}" title="Merken — Pfeil rechts" style="width:70px; height:70px; border-radius:50%; border:1px solid rgba(126,226,184,.45); background:rgba(14,10,12,.72); color:#8fe3c1; font-size:26px; cursor:pointer; display:grid; place-items:center; transition:transform .18s ease, background .18s ease;" style-hover="transform:translateY(-3px) scale(1.06); background:rgba(20,88,68,.6);" style-active="transform:scale(.95);"></button>
</div>
<sc-if value="{{ showInfo }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute; right:26px; top:26px; width:250px; border:1px solid rgba(255,255,255,.14); border-radius:14px; background:rgba(10,26,22,.9); backdrop-filter:blur(12px); padding:16px; font-size:13px; line-height:1.8; animation:riseIn .22s ease;">
<div style="font-weight:700; margin-bottom:8px;">Bedienung</div>
<div style="display:grid; grid-template-columns:auto 1fr; gap:6px 12px; color:rgba(242,239,233,.72);">
<span style="font-family:'JetBrains Mono',monospace;"></span><span>Ablehnen</span>
<span style="font-family:'JetBrains Mono',monospace;"></span><span>Merken</span>
<span style="font-family:'JetBrains Mono',monospace;"></span><span>Zurücknehmen</span>
<span style="font-family:'JetBrains Mono',monospace;">Maus</span><span>Karte ziehen</span>
</div>
</div>
</sc-if>
</div>
<aside style="border-left:1px solid rgba(255,255,255,.07); background:rgba(8,22,19,.34); padding:30px 28px; display:flex; flex-direction:column; gap:24px; overflow:auto;">
<div>
<div style="font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:rgba(242,239,233,.45);">Aktuelle Karte</div>
<div style="font-size:26px; font-weight:600; letter-spacing:-.02em; margin-top:10px; text-wrap:pretty;">{{ detailTitle }}</div>
<div style="font-size:14px; color:rgba(242,239,233,.62); margin-top:6px;">{{ detailSub }}</div>
</div>
<div style="height:1px; background:rgba(255,255,255,.09);"></div>
<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:12px;">
<sc-for list="{{ detailStats }}" as="s" hint-placeholder-count="3">
<div style="border:1px solid rgba(255,255,255,.1); border-radius:12px; padding:12px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:17px; font-variant-numeric:tabular-nums;">{{ s.value }}</div>
<div style="font-size:11px; color:rgba(242,239,233,.5); margin-top:3px;">{{ s.label }}</div>
</div>
</sc-for> </sc-for>
</div> </div>
<sc-if value="{{ gridEmpty }}" hint-placeholder-val="{{ false }}"> <div>
<div style="max-width:420px; margin:80px auto; text-align:center; display:flex; flex-direction:column; align-items:center; gap:14px; animation:fadeIn .3s ease;"> <div style="font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:rgba(242,239,233,.45); margin-bottom:10px;">Beschreibung</div>
<div style="font-size:20px; font-weight:700;">Nichts mehr in dieser Ansicht</div> <p style="margin:0; font-size:14px; line-height:1.75; color:rgba(242,239,233,.78); text-wrap:pretty;">{{ detailBody }}</p>
<div style="font-size:15px; color:#5f5f5f; text-wrap:pretty;">Alle Karten dieses Tabs sind ausgeblendet.</div>
<button onClick="{{ unhideAll }}" style="padding:12px 20px; border:none; border-radius:24px; background:#e60023; color:#ffffff; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer;" style-hover="background:#ad081b;">Ausgeblendete wieder anzeigen</button>
</div> </div>
</sc-if>
<div>
<div style="font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:rgba(242,239,233,.45); margin-bottom:10px;">Tags</div>
<div style="display:flex; flex-wrap:wrap; gap:8px;">
<sc-for list="{{ detailTags }}" as="tag" hint-placeholder-count="4">
<span style="padding:6px 12px; border-radius:999px; border:1px solid rgba(255,255,255,.18); font-size:12px; color:rgba(242,239,233,.8);">{{ tag }}</span>
</sc-for>
</div>
</div>
<div style="margin-top:auto; display:flex; flex-direction:column; gap:10px;">
<button onClick="{{ onLike }}" style="padding:13px; border-radius:12px; border:1px solid rgba(126,226,184,.55); background:transparent; color:#8fe3c1; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer; transition:background .18s ease;" style-hover="background:rgba(126,226,184,.14);">Zur Sammlung hinzufügen</button>
<button onClick="{{ onNope }}" style="padding:13px; border-radius:12px; border:1px solid rgba(255,255,255,.16); background:transparent; color:rgba(242,239,233,.8); font-family:inherit; font-size:14px; cursor:pointer; transition:background .18s ease;" style-hover="background:rgba(255,255,255,.07);">Überspringen</button>
</div>
</aside>
</section> </section>
</sc-if> </sc-if>
<sc-if value="{{ isProfile }}" hint-placeholder-val="{{ false }}"> <sc-if value="{{ isProfile }}" hint-placeholder-val="{{ false }}">
<section style="flex:1; padding:32px 24px 40px; overflow:auto; animation:riseIn .26s ease;"> <section style="flex:1; padding:38px 44px; overflow:auto; animation:riseIn .26s ease;">
<div style="display:flex; flex-direction:column; align-items:center; gap:10px; padding-bottom:28px;"> <div style="display:flex; align-items:flex-end; gap:22px; padding-bottom:26px; border-bottom:1px solid rgba(255,255,255,.09);">
<div style="width:120px; height:120px; border-radius:50%; background:linear-gradient(140deg,#b2323c,#5d1620); flex:none;"></div> <div style="width:96px; height:96px; border-radius:50%; background:linear-gradient(140deg,#b2323c,#5d1620); flex:none; border:2px solid rgba(255,255,255,.2);"></div>
<div style="font-size:36px; font-weight:700; letter-spacing:-.02em; text-align:center;">Jonas K.</div> <div style="flex:1;">
<div style="font-size:15px; color:#5f5f5f;">Studio-Konto · seit März 2026</div> <div style="font-size:32px; font-weight:600; letter-spacing:-.02em;">Jonas K.</div>
<div style="display:flex; gap:28px; margin-top:6px;"> <div style="font-size:14px; color:rgba(242,239,233,.6); margin-top:4px;">Studio-Konto · seit März 2026</div>
</div>
<div style="display:flex; gap:34px; padding-bottom:6px;">
<sc-for list="{{ profileStats }}" as="s" hint-placeholder-count="3"> <sc-for list="{{ profileStats }}" as="s" hint-placeholder-count="3">
<div style="text-align:center;"> <div>
<div style="font-size:18px; font-weight:700; font-variant-numeric:tabular-nums;">{{ s.value }}</div> <div style="font-family:'JetBrains Mono',monospace; font-size:22px; font-variant-numeric:tabular-nums;">{{ s.value }}</div>
<div style="font-size:13px; color:#767676;">{{ s.label }}</div> <div style="font-size:12px; color:rgba(242,239,233,.5);">{{ s.label }}</div>
</div> </div>
</sc-for> </sc-for>
</div> </div>
</div> </div>
<div style="{{ gridStyle }}"> <div style="display:grid; grid-template-columns:repeat(4,1fr); gap:18px; margin-top:28px;">
<sc-for list="{{ gallery }}" as="g" hint-placeholder-count="8"> <sc-for list="{{ gallery }}" as="g" hint-placeholder-count="8">
<div class="pin" style="position:relative; break-inside:avoid; margin-bottom:16px;"> <div style="{{ g.tile }}" style-hover="transform:translateY(-4px); border-color:rgba(126,226,184,.5);">
<div onClick="{{ g.open }}" style="{{ g.media }}">
<div style="position:absolute; inset:0; background:{{ g.stripes }}; opacity:.45;"></div> <div style="position:absolute; inset:0; background:{{ g.stripes }}; opacity:.45;"></div>
<div class="pin-scrim" style="position:absolute; inset:0; background:rgba(0,0,0,.24);"></div> <div style="position:absolute; inset:auto 0 0 0; padding:14px; background:linear-gradient(0deg, rgba(0,0,0,.7), transparent); font-size:13px; font-weight:600;">{{ g.title }}</div>
<div style="position:absolute; inset:auto 0 0 0; padding:14px; background:linear-gradient(0deg, rgba(0,0,0,.62), transparent); color:#ffffff; font-size:14px; font-weight:600;">{{ g.caption }}</div>
</div>
</div> </div>
</sc-for> </sc-for>
</div> </div>
@@ -149,47 +201,47 @@
</sc-if> </sc-if>
<sc-if value="{{ isCreate }}" hint-placeholder-val="{{ false }}"> <sc-if value="{{ isCreate }}" hint-placeholder-val="{{ false }}">
<section style="flex:1; overflow:auto; padding:32px 24px 40px; animation:riseIn .26s ease;"> <section style="flex:1; overflow:auto; padding:38px 44px; animation:riseIn .26s ease;">
<div style="max-width:920px; margin:0 auto; border-radius:24px; box-shadow:0 1px 20px rgba(0,0,0,.12); padding:32px; display:grid; grid-template-columns:1fr 320px; gap:32px;"> <div style="max-width:920px; display:grid; grid-template-columns:1fr 320px; gap:34px;">
<div style="display:flex; flex-direction:column; gap:20px;"> <div style="display:flex; flex-direction:column; gap:22px;">
<div> <div>
<div style="font-size:30px; font-weight:700; letter-spacing:-.02em;">Neuen Post erstellen</div> <div style="font-size:30px; font-weight:600; letter-spacing:-.02em;">Neuen Post erstellen</div>
<div style="font-size:15px; color:#5f5f5f; margin-top:6px;">Material hochladen, beschriften, in den Feed geben.</div> <div style="font-size:14px; color:rgba(242,239,233,.6); margin-top:6px;">Material hochladen, beschriften, in den Feed geben.</div>
</div> </div>
<div style="border:2px dashed #cdcdcd; border-radius:16px; height:220px; display:grid; place-items:center; text-align:center; background:#f5f5f5; cursor:pointer; transition:border-color .12s ease, background .12s ease;" style-hover="border-color:#e60023; background:#fafafa;"> <div style="border:1px dashed rgba(255,255,255,.24); border-radius:18px; height:220px; display:grid; place-items:center; text-align:center; gap:8px; background:rgba(255,255,255,.03); cursor:pointer; transition:border-color .18s ease, background .18s ease;" style-hover="border-color:rgba(126,226,184,.6); background:rgba(126,226,184,.06);">
<div> <div>
<div style="font-size:16px; font-weight:700;">Dateien hierher ziehen</div> <div style="font-size:15px; font-weight:600;">Dateien hierher ziehen</div>
<div style="font-size:14px; color:#767676; margin-top:6px;">MP4, MOV oder bis zu 10 Bilder</div> <div style="font-size:13px; color:rgba(242,239,233,.55); margin-top:6px;">MP4, MOV oder bis zu 10 Bilder</div>
</div> </div>
</div> </div>
<div style="display:flex; flex-direction:column; gap:16px;"> <div style="display:flex; flex-direction:column; gap:16px;">
<label style="display:flex; flex-direction:column; gap:8px; font-size:13px; font-weight:600; color:#111111;">Titel <label style="display:flex; flex-direction:column; gap:8px; font-size:12px; letter-spacing:.14em; text-transform:uppercase; color:rgba(242,239,233,.5);">Titel
<input value="{{ draftTitle }}" onChange="{{ setTitle }}" style="padding:14px 16px; border-radius:16px; border:2px solid #cdcdcd; background:#ffffff; color:#111111; font-family:inherit; font-size:15px; font-weight:400;"> <input value="{{ draftTitle }}" onChange="{{ setTitle }}" style="padding:13px 15px; border-radius:11px; border:1px solid rgba(255,255,255,.16); background:rgba(0,0,0,.24); color:#f2efe9; font-family:inherit; font-size:15px; letter-spacing:normal; text-transform:none;">
</label> </label>
<label style="display:flex; flex-direction:column; gap:8px; font-size:13px; font-weight:600; color:#111111;">Untertitel <label style="display:flex; flex-direction:column; gap:8px; font-size:12px; letter-spacing:.14em; text-transform:uppercase; color:rgba(242,239,233,.5);">Untertitel
<input value="{{ draftSub }}" onChange="{{ setSub }}" style="padding:14px 16px; border-radius:16px; border:2px solid #cdcdcd; background:#ffffff; color:#111111; font-family:inherit; font-size:15px; font-weight:400;"> <input value="{{ draftSub }}" onChange="{{ setSub }}" style="padding:13px 15px; border-radius:11px; border:1px solid rgba(255,255,255,.16); background:rgba(0,0,0,.24); color:#f2efe9; font-family:inherit; font-size:15px; letter-spacing:normal; text-transform:none;">
</label> </label>
<div style="display:flex; flex-direction:column; gap:10px; font-size:13px; font-weight:600; color:#111111;">Art <div style="display:flex; flex-direction:column; gap:10px; font-size:12px; letter-spacing:.14em; text-transform:uppercase; color:rgba(242,239,233,.5);">Art
<div style="display:flex; gap:8px;"> <div style="display:flex; gap:8px;">
<sc-for list="{{ kindOptions }}" as="k" hint-placeholder-count="3"> <sc-for list="{{ kindOptions }}" as="k" hint-placeholder-count="3">
<button onClick="{{ k.go }}" style="{{ k.style }}">{{ k.label }}</button> <button onClick="{{ k.go }}" style="{{ k.style }}" style-hover="border-color:rgba(126,226,184,.6);">{{ k.label }}</button>
</sc-for> </sc-for>
</div> </div>
</div> </div>
</div> </div>
<div style="display:flex; gap:8px;"> <div style="display:flex; gap:12px;">
<button onClick="{{ publish }}" style="padding:14px 24px; border:none; border-radius:24px; background:#e60023; color:#ffffff; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:background .12s ease;" style-hover="background:#ad081b;">Veröffentlichen</button> <button onClick="{{ publish }}" style="padding:13px 24px; border-radius:12px; border:1px solid rgba(126,226,184,.6); background:transparent; color:#8fe3c1; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer; transition:background .18s ease;" style-hover="background:rgba(126,226,184,.14);">Veröffentlichen</button>
<button style="padding:14px 24px; border:none; border-radius:24px; background:#efefef; color:#111111; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:background .12s ease;" style-hover="background:#e2e2e2;">Als Entwurf sichern</button> <button style="padding:13px 24px; border-radius:12px; border:1px solid rgba(255,255,255,.16); background:transparent; color:rgba(242,239,233,.8); font-family:inherit; font-size:14px; cursor:pointer; transition:background .18s ease;" style-hover="background:rgba(255,255,255,.07);">Als Entwurf sichern</button>
</div> </div>
</div> </div>
<div> <div>
<div style="font-size:12px; font-weight:600; color:#767676; margin-bottom:12px;">Vorschau</div> <div style="font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:rgba(242,239,233,.45); margin-bottom:12px;">Vorschau</div>
<div style="position:relative; height:420px; border-radius:16px; overflow:hidden; background:linear-gradient(155deg,#1f8163 0%,#0d4034 100%);"> <div style="position:relative; height:420px; border-radius:22px; overflow:hidden; border:1px solid rgba(255,255,255,.12); background:linear-gradient(155deg,#1f8163 0%,#0d4034 100%);">
<div style="position:absolute; inset:0; background:repeating-linear-gradient(48deg, rgba(255,255,255,.07) 0 2px, transparent 2px 15px); opacity:.5;"></div> <div style="position:absolute; inset:0; background:repeating-linear-gradient(48deg, rgba(255,255,255,.07) 0 2px, transparent 2px 15px); opacity:.5;"></div>
<div style="position:absolute; inset:auto 0 0 0; padding:18px; background:linear-gradient(0deg, rgba(0,0,0,.68), transparent); color:#ffffff;"> <div style="position:absolute; inset:auto 0 0 0; padding:18px; background:linear-gradient(0deg, rgba(0,0,0,.72), transparent);">
<div style="font-size:18px; font-weight:700;">{{ draftTitle }}</div> <div style="font-size:18px; font-weight:700;">{{ draftTitle }}</div>
<div style="font-size:14px; color:rgba(255,255,255,.8);">{{ draftSub }}</div> <div style="font-size:13px; color:rgba(255,255,255,.7);">{{ draftSub }}</div>
</div> </div>
</div> </div>
</div> </div>
@@ -198,15 +250,15 @@
</sc-if> </sc-if>
<sc-if value="{{ isSettings }}" hint-placeholder-val="{{ false }}"> <sc-if value="{{ isSettings }}" hint-placeholder-val="{{ false }}">
<section style="flex:1; overflow:auto; padding:32px 24px 40px; animation:riseIn .26s ease;"> <section style="flex:1; overflow:auto; padding:38px 44px; animation:riseIn .26s ease;">
<div style="max-width:660px; margin:0 auto;"> <div style="max-width:660px;">
<div style="font-size:30px; font-weight:700; letter-spacing:-.02em;">Einstellungen</div> <div style="font-size:30px; font-weight:600; letter-spacing:-.02em;">Einstellungen</div>
<div style="margin-top:24px; border-radius:16px; overflow:hidden; box-shadow:0 1px 20px rgba(0,0,0,.12);"> <div style="margin-top:26px; border:1px solid rgba(255,255,255,.1); border-radius:16px; overflow:hidden;">
<sc-for list="{{ settings }}" as="s" hint-placeholder-count="4"> <sc-for list="{{ settings }}" as="s" hint-placeholder-count="4">
<div style="display:flex; align-items:center; justify-content:space-between; gap:20px; padding:20px; border-bottom:1px solid #efefef; background:#ffffff;"> <div style="display:flex; align-items:center; justify-content:space-between; gap:20px; padding:18px 20px; border-bottom:1px solid rgba(255,255,255,.07); background:rgba(255,255,255,.02);">
<div> <div>
<div style="font-size:16px; font-weight:700;">{{ s.label }}</div> <div style="font-size:15px; font-weight:600;">{{ s.label }}</div>
<div style="font-size:14px; color:#767676; margin-top:3px; text-wrap:pretty;">{{ s.hint }}</div> <div style="font-size:13px; color:rgba(242,239,233,.55); margin-top:3px; text-wrap:pretty;">{{ s.hint }}</div>
</div> </div>
<button onClick="{{ s.toggle }}" style="{{ s.track }}"> <button onClick="{{ s.toggle }}" style="{{ s.track }}">
<span style="{{ s.knob }}"></span> <span style="{{ s.knob }}"></span>
@@ -218,326 +270,232 @@
</section> </section>
</sc-if> </sc-if>
<sc-if value="{{ selectionOpen }}" hint-placeholder-val="{{ false }}">
<div style="position:fixed; left:50%; bottom:28px; transform:translateX(-50%); display:flex; align-items:center; gap:12px; padding:12px 12px 12px 22px; border-radius:32px; background:#ffffff; box-shadow:0 1px 20px rgba(0,0,0,.22); z-index:45; animation:riseIn .2s ease;">
<div style="font-size:15px; font-weight:700;">{{ selectionCount }}</div>
<button onClick="{{ saveSelection }}" style="padding:12px 20px; border:none; border-radius:24px; background:#e60023; color:#ffffff; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:background .12s ease;" style-hover="background:#ad081b;">Zur Sammlung hinzufügen</button>
<button onClick="{{ clearSelection }}" style="padding:12px 20px; border:none; border-radius:24px; background:#efefef; color:#111111; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer; transition:background .12s ease;" style-hover="background:#e2e2e2;">Auswahl aufheben</button>
</div>
</sc-if>
<sc-if value="{{ toast }}" hint-placeholder-val="{{ false }}"> <sc-if value="{{ toast }}" hint-placeholder-val="{{ false }}">
<div style="{{ toastStyle }}">{{ toast }}</div> <div style="position:fixed; left:50%; bottom:34px; transform:translateX(-50%); padding:12px 22px; border-radius:999px; border:1px solid rgba(255,255,255,.16); background:rgba(10,24,20,.92); backdrop-filter:blur(10px); font-size:14px; animation:riseIn .2s ease; z-index:40;">{{ toast }}</div>
</sc-if> </sc-if>
</main> </main>
<sc-if value="{{ detailOpen }}" hint-placeholder-val="{{ false }}">
<div onClick="{{ closeDetail }}" style="position:fixed; inset:0; background:rgba(0,0,0,.6); display:grid; place-items:center; padding:32px; z-index:60; animation:fadeIn .16s ease;">
<div onClick="{{ stop }}" style="position:relative; width:min(1016px,100%); max-height:calc(100vh - 64px); background:#ffffff; border-radius:32px; overflow:hidden; display:grid; grid-template-columns:1fr 1fr; animation:riseIn .2s ease;">
<button onClick="{{ closeDetail }}" title="Schließen" style="position:absolute; left:16px; top:16px; width:40px; height:40px; border:none; border-radius:50%; background:#ffffff; box-shadow:0 1px 8px rgba(0,0,0,.2); color:#111111; font-size:16px; cursor:pointer; z-index:2;" style-hover="background:#e9e9e9;"></button>
<div style="{{ detailMedia }}">
<div style="position:absolute; inset:0; background:{{ detailStripes }}; opacity:.45;"></div>
</div>
<div style="padding:32px; overflow:auto; display:flex; flex-direction:column; gap:20px;">
<div style="display:flex; align-items:center; justify-content:flex-end; gap:8px;">
<button onClick="{{ detailHide }}" style="height:48px; padding:0 18px; border:none; border-radius:24px; background:#efefef; color:#111111; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer;" style-hover="background:#e2e2e2;">Nicht mehr zeigen</button>
<button onClick="{{ detailToggleSave }}" style="{{ detailSaveStyle }}">{{ detailSaveLabel }}</button>
</div>
<div>
<div style="font-size:32px; font-weight:700; letter-spacing:-.02em; text-wrap:pretty;">{{ detailTitle }}</div>
<div style="display:flex; align-items:center; gap:12px; margin-top:16px;">
<div style="width:48px; height:48px; border-radius:50%; flex:none; background:{{ detailAvatar }};"></div>
<div style="min-width:0;">
<div style="font-size:16px; font-weight:700;">{{ detailName }}</div>
<div style="font-size:14px; color:#767676;">{{ detailSub }}</div>
</div>
</div>
</div>
<p style="margin:0; font-size:16px; line-height:1.6; color:#333333; text-wrap:pretty;">{{ detailBody }}</p>
<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:10px;">
<sc-for list="{{ detailStats }}" as="s" hint-placeholder-count="3">
<div style="background:#f5f5f5; border-radius:16px; padding:14px;">
<div style="font-size:18px; font-weight:700; font-variant-numeric:tabular-nums;">{{ s.value }}</div>
<div style="font-size:12px; color:#767676; margin-top:3px;">{{ s.label }}</div>
</div>
</sc-for>
</div>
<div style="display:flex; flex-wrap:wrap; gap:8px;">
<sc-for list="{{ detailTags }}" as="tag" hint-placeholder-count="4">
<span style="padding:8px 14px; border-radius:18px; background:#efefef; font-size:13px; font-weight:600; color:#111111;">{{ tag }}</span>
</sc-for>
</div>
</div>
</div>
</div>
</sc-if>
</div> </div>
</x-dc> </x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;columns&quot;:{&quot;editor&quot;:&quot;int&quot;,&quot;default&quot;:5,&quot;min&quot;:3,&quot;max&quot;:6,&quot;tsType&quot;:&quot;number&quot;,&quot;section&quot;:&quot;Raster&quot;}}"> <script type="text/x-dc" data-dc-script data-props="{&quot;stackDepth&quot;:{&quot;editor&quot;:&quot;int&quot;,&quot;default&quot;:3,&quot;min&quot;:1,&quot;max&quot;:5,&quot;tsType&quot;:&quot;number&quot;,&quot;section&quot;:&quot;Feed&quot;}}">
const DATA = { const DATA = {
posts: [ posts: [
{ id:'p1', name:'Mara S.', sub:'Modell · Glow Serum v3', kind:'Produkt', assets:'3 Bilder', caption:'Toner im Bogenfenster', hue:[ '#1f8163', '#0d4034' ], avatar:'linear-gradient(140deg,#b2323c,#5d1620)', body:'Drei Aufnahmen im Gegenlicht, aufgenommen im Studio Nord. Freigabe für Paid Social liegt vor.', tags:['Beauty','Studio','Hochformat','Freigegeben'], stats:[['3','Assets'],['12s','Länge'],['94','Score']] }, { name:'Mara S.', sub:'Modell · Glow Serum v3', kind:'Produkt', assets:'3 Bilder', caption:'Toner im Bogenfenster', hue:[ '#1f8163', '#0d4034' ], avatar:'linear-gradient(140deg,#b2323c,#5d1620)', body:'Drei Aufnahmen im Gegenlicht, aufgenommen im Studio Nord. Freigabe für Paid Social liegt vor.', tags:['Beauty','Studio','Hochformat','Freigegeben'], stats:[['3','Assets'],['12s','Länge'],['94','Score']] },
{ id:'p2', name:'Tobias R.', sub:'Creator · Kaltbrüh-Serie', kind:'Video', assets:'1 Clip', caption:'Erste Kanne am Morgen', hue:[ '#256f8b', '#0c2f3f' ], avatar:'linear-gradient(140deg,#2f7f9e,#10394a)', body:'Vertikaler Clip mit Naturton, geschnitten auf zwölf Sekunden. Untertitel bereits eingebrannt.', tags:['Food','Handheld','Untertitel'], stats:[['1','Asset'],['12s','Länge'],['88','Score']] }, { name:'Tobias R.', sub:'Creator · Kaltbrüh-Serie', kind:'Video', assets:'1 Clip', caption:'Erste Kanne am Morgen', hue:[ '#256f8b', '#0c2f3f' ], avatar:'linear-gradient(140deg,#2f7f9e,#10394a)', body:'Vertikaler Clip mit Naturton, geschnitten auf zwölf Sekunden. Untertitel bereits eingebrannt.', tags:['Food','Handheld','Untertitel'], stats:[['1','Asset'],['12s','Länge'],['88','Score']] },
{ id:'p3', name:'Leonie B.', sub:'Modell · Leinen SS26', kind:'Produkt', assets:'5 Bilder', caption:'Leinen gegen die Mittagssonne', hue:[ '#8a6a2f', '#3a2a11' ], avatar:'linear-gradient(140deg,#c99a4a,#6a4a17)', body:'Fünf Stills aus der Sommerstrecke, alle im gleichen Weißabgleich. Retusche steht noch aus.', tags:['Fashion','Außen','Serie'], stats:[['5','Assets'],['—','Länge'],['76','Score']] }, { name:'Leonie B.', sub:'Modell · Leinen SS26', kind:'Produkt', assets:'5 Bilder', caption:'Leinen gegen die Mittagssonne', hue:[ '#8a6a2f', '#3a2a11' ], avatar:'linear-gradient(140deg,#c99a4a,#6a4a17)', body:'Fünf Stills aus der Sommerstrecke, alle im gleichen Weißabgleich. Retusche steht noch aus.', tags:['Fashion','Außen','Serie'], stats:[['5','Assets'],['—','Länge'],['76','Score']] },
{ id:'p4', name:'Nils P.', sub:'Creator · Werkstattnotiz', kind:'Video', assets:'2 Clips', caption:'Hobel, zweiter Durchgang', hue:[ '#7a2f38', '#2c1014' ], avatar:'linear-gradient(140deg,#a63a44,#4d151c)', body:'Zwei Sequenzen aus der Werkstatt, Ton direkt aus der Kamera. Eignet sich als Hintergrundmaterial.', tags:['Handwerk','Ton','Roh'], stats:[['2','Assets'],['26s','Länge'],['81','Score']] }, { name:'Nils P.', sub:'Creator · Werkstattnotiz', kind:'Video', assets:'2 Clips', caption:'Hobel, zweiter Durchgang', hue:[ '#7a2f38', '#2c1014' ], avatar:'linear-gradient(140deg,#a63a44,#4d151c)', body:'Zwei Sequenzen aus der Werkstatt, Ton direkt aus der Kamera. Eignet sich als Hintergrundmaterial.', tags:['Handwerk','Ton','Roh'], stats:[['2','Assets'],['26s','Länge'],['81','Score']] },
{ id:'p5', name:'Anke V.', sub:'Modell · Keramik-Drop', kind:'Produkt', assets:'4 Bilder', caption:'Glasur, noch feucht', hue:[ '#2f6f5a', '#0e332b' ], avatar:'linear-gradient(140deg,#3f9a7c,#124334)', body:'Nahaufnahmen der neuen Glasur auf Naturpapier. Für den Katalog abgenommen.', tags:['Keramik','Makro','Katalog'], stats:[['4','Assets'],['—','Länge'],['90','Score']] }, { name:'Anke V.', sub:'Modell · Keramik-Drop', kind:'Produkt', assets:'4 Bilder', caption:'Glasur, noch feucht', hue:[ '#2f6f5a', '#0e332b' ], avatar:'linear-gradient(140deg,#3f9a7c,#124334)', body:'Nahaufnahmen der neuen Glasur auf Naturpapier. Für den Katalog abgenommen.', tags:['Keramik','Makro','Katalog'], stats:[['4','Assets'],['—','Länge'],['90','Score']] }
{ id:'p6', name:'Jorin M.', sub:'Creator · Leuchtenserie', kind:'Produkt', assets:'6 Bilder', caption:'Messing bei Abendlicht', hue:[ '#7a5a2a', '#2f2110' ], avatar:'linear-gradient(140deg,#b08434,#513c14)', body:'Sechs Aufnahmen derselben Leuchte über zwei Stunden hinweg. Die letzten drei sind die stärksten.', tags:['Interior','Abendlicht','Serie'], stats:[['6','Assets'],['—','Länge'],['85','Score']] },
{ id:'p7', name:'Selma D.', sub:'Modell · Denim-Kapsel', kind:'Produkt', assets:'2 Bilder', caption:'Naht, doppelt gesteppt', hue:[ '#1c6f7f', '#0a2c33' ], avatar:'linear-gradient(140deg,#2f8fa0,#0d3b44)', body:'Zwei Detailaufnahmen für die Produktseite. Farbe ist kalibriert, kein Nachschärfen nötig.', tags:['Detail','Produktseite','Kalibriert'], stats:[['2','Assets'],['—','Länge'],['79','Score']] },
{ id:'p8', name:'Ravi K.', sub:'Creator · Marktbesuch', kind:'Video', assets:'3 Clips', caption:'Kisten werden ausgeladen', hue:[ '#6a2f7a', '#26102c' ], avatar:'linear-gradient(140deg,#8b46a0,#341340)', body:'Handkamera am frühen Morgen, viel Umgebungsgeräusch. Schnitt steht noch aus.', tags:['Reportage','Handheld','Roh'], stats:[['3','Assets'],['38s','Länge'],['72','Score']] },
{ id:'p9', name:'Nora W.', sub:'Modell · Seifen-Relaunch', kind:'Produkt', assets:'4 Bilder', caption:'Schaum auf Steingut', hue:[ '#2f6f5a', '#0e332b' ], avatar:'linear-gradient(140deg,#3f9a7c,#124334)', body:'Vier Stills mit weichem Seitenlicht. Für Social freigegeben, Print noch offen.', tags:['Beauty','Studio','Teilfreigabe'], stats:[['4','Assets'],['—','Länge'],['87','Score']] },
{ id:'p10', name:'Elias T.', sub:'Creator · Nachtfahrt', kind:'Video', assets:'1 Clip', caption:'Scheinwerfer auf nasser Straße', hue:[ '#256f8b', '#0c2f3f' ], avatar:'linear-gradient(140deg,#2f7f9e,#10394a)', body:'Ein Take aus dem fahrenden Wagen, sehr kontrastreich. Braucht eine ruhige Tonspur.', tags:['Nacht','Bewegung','Kontrast'], stats:[['1','Asset'],['22s','Länge'],['83','Score']] }
], ],
videos: [ videos: [
{ id:'v1', name:'Studio Nord', sub:'Reel · Behind the scenes', kind:'Video', assets:'1 Clip', caption:'Aufbau um sechs Uhr', hue:[ '#1c6f7f', '#0a2c33' ], avatar:'linear-gradient(140deg,#2f8fa0,#0d3b44)', body:'Ein durchgehender Take vom Aufbau bis zum ersten Licht.', tags:['BTS','Longtake'], stats:[['1','Asset'],['48s','Länge'],['83','Score']] }, { name:'Studio Nord', sub:'Reel · Behind the scenes', kind:'Video', assets:'1 Clip', caption:'Aufbau um sechs Uhr', hue:[ '#1c6f7f', '#0a2c33' ], avatar:'linear-gradient(140deg,#2f8fa0,#0d3b44)', body:'Ein durchgehender Take vom Aufbau bis zum ersten Licht.', tags:['BTS','Longtake'], stats:[['1','Asset'],['48s','Länge'],['83','Score']] },
{ id:'v2', name:'Yara M.', sub:'Creator · Tanzstudie', kind:'Video', assets:'3 Clips', caption:'Drehung, dritter Versuch', hue:[ '#6a2f7a', '#26102c' ], avatar:'linear-gradient(140deg,#8b46a0,#341340)', body:'Drei Varianten derselben Bewegung, Slow Motion in der Mitte.', tags:['Bewegung','Slowmo'], stats:[['3','Assets'],['31s','Länge'],['87','Score']] }, { name:'Yara M.', sub:'Creator · Tanzstudie', kind:'Video', assets:'3 Clips', caption:'Drehung, dritter Versuch', hue:[ '#6a2f7a', '#26102c' ], avatar:'linear-gradient(140deg,#8b46a0,#341340)', body:'Drei Varianten derselben Bewegung, Slow Motion in der Mitte.', tags:['Bewegung','Slowmo'], stats:[['3','Assets'],['31s','Länge'],['87','Score']] },
{ id:'v3', name:'Felix H.', sub:'Creator · Küchennotiz', kind:'Video', assets:'1 Clip', caption:'Teig ruht dreißig Minuten', hue:[ '#7a5a2a', '#2f2110' ], avatar:'linear-gradient(140deg,#b08434,#513c14)', body:'Ruhiger Ablauf ohne Schnitt, gut für Voiceover.', tags:['Food','Ruhig'], stats:[['1','Asset'],['19s','Länge'],['79','Score']] }, { name:'Felix H.', sub:'Creator · Küchennotiz', kind:'Video', assets:'1 Clip', caption:'Teig ruht dreißig Minuten', hue:[ '#7a5a2a', '#2f2110' ], avatar:'linear-gradient(140deg,#b08434,#513c14)', body:'Ruhiger Ablauf ohne Schnitt, gut für Voiceover.', tags:['Food','Ruhig'], stats:[['1','Asset'],['19s','Länge'],['79','Score']] }
{ id:'v4', name:'Studio Nord', sub:'Reel · Lichtsetzung', kind:'Video', assets:'2 Clips', caption:'Softbox wandert nach links', hue:[ '#2f6f5a', '#0e332b' ], avatar:'linear-gradient(140deg,#3f9a7c,#124334)', body:'Zwei kurze Sequenzen, die den Aufbau erklären. Als Erklärstück brauchbar.', tags:['BTS','Erklärung'], stats:[['2','Assets'],['27s','Länge'],['80','Score']] },
{ id:'v5', name:'Mira L.', sub:'Creator · Werkbank', kind:'Video', assets:'4 Clips', caption:'Späne fallen langsam', hue:[ '#7a2f38', '#2c1014' ], avatar:'linear-gradient(140deg,#a63a44,#4d151c)', body:'Vier Makroeinstellungen in Zeitlupe. Sehr ruhig, funktioniert stumm.', tags:['Makro','Slowmo','Stumm'], stats:[['4','Assets'],['52s','Länge'],['91','Score']] },
{ id:'v6', name:'Tomas B.', sub:'Creator · Uferweg', kind:'Video', assets:'1 Clip', caption:'Gegenlicht am Wasser', hue:[ '#1f8163', '#0d4034' ], avatar:'linear-gradient(140deg,#2fa87f,#0e5442)', body:'Ein langer Schwenk gegen die tief stehende Sonne. Farbstich bewusst gelassen.', tags:['Außen','Gegenlicht'], stats:[['1','Asset'],['41s','Länge'],['77','Score']] },
{ id:'v7', name:'Ida F.', sub:'Reel · Verpackung', kind:'Video', assets:'2 Clips', caption:'Karton wird gefaltet', hue:[ '#8a6a2f', '#3a2a11' ], avatar:'linear-gradient(140deg,#c99a4a,#6a4a17)', body:'Zwei Durchgänge desselben Handgriffs, der zweite ist sauberer.', tags:['Unboxing','Hände'], stats:[['2','Assets'],['16s','Länge'],['84','Score']] },
{ id:'v8', name:'Kaan Ö.', sub:'Creator · Dachterrasse', kind:'Video', assets:'3 Clips', caption:'Wind in der Markise', hue:[ '#6a2f7a', '#26102c' ], avatar:'linear-gradient(140deg,#8b46a0,#341340)', body:'Drei Einstellungen bei starkem Wind, Ton unbrauchbar, Bild sehr gut.', tags:['Außen','Ton ersetzen'], stats:[['3','Assets'],['34s','Länge'],['75','Score']] }
], ],
following: [ following: [
{ id:'f1', name:'Mara S.', sub:'Folge ich · neuer Post', kind:'Produkt', assets:'2 Bilder', caption:'Nachschub aus dem Studio', hue:[ '#1f8163', '#0d4034' ], avatar:'linear-gradient(140deg,#b2323c,#5d1620)', body:'Zwei zusätzliche Aufnahmen aus derselben Strecke.', tags:['Beauty','Nachtrag'], stats:[['2','Assets'],['—','Länge'],['92','Score']] }, { name:'Mara S.', sub:'Folge ich · neuer Post', kind:'Produkt', assets:'2 Bilder', caption:'Nachschub aus dem Studio', hue:[ '#1f8163', '#0d4034' ], avatar:'linear-gradient(140deg,#b2323c,#5d1620)', body:'Zwei zusätzliche Aufnahmen aus derselben Strecke.', tags:['Beauty','Nachtrag'], stats:[['2','Assets'],['—','Länge'],['92','Score']] },
{ id:'f2', name:'Tobias R.', sub:'Folge ich · Serie läuft', kind:'Video', assets:'4 Clips', caption:'Kaltbrüh, Folge vier', hue:[ '#256f8b', '#0c2f3f' ], avatar:'linear-gradient(140deg,#2f7f9e,#10394a)', body:'Vierte Folge der Reihe, gleiche Bildsprache wie zuvor.', tags:['Serie','Food'], stats:[['4','Assets'],['44s','Länge'],['85','Score']] }, { name:'Tobias R.', sub:'Folge ich · Serie läuft', kind:'Video', assets:'4 Clips', caption:'Kaltbrüh, Folge vier', hue:[ '#256f8b', '#0c2f3f' ], avatar:'linear-gradient(140deg,#2f7f9e,#10394a)', body:'Vierte Folge der Reihe, gleiche Bildsprache wie zuvor.', tags:['Serie','Food'], stats:[['4','Assets'],['44s','Länge'],['85','Score']] }
{ id:'f3', name:'Leonie B.', sub:'Folge ich · Vorschau', kind:'Produkt', assets:'3 Bilder', caption:'Leinen, zweite Farbe', hue:[ '#8a6a2f', '#3a2a11' ], avatar:'linear-gradient(140deg,#c99a4a,#6a4a17)', body:'Drei Aufnahmen der zweiten Farbvariante, noch nicht freigegeben.', tags:['Fashion','Vorschau'], stats:[['3','Assets'],['—','Länge'],['78','Score']] },
{ id:'f4', name:'Anke V.', sub:'Folge ich · Werkstatt', kind:'Produkt', assets:'5 Bilder', caption:'Zweiter Brand im Ofen', hue:[ '#2f6f5a', '#0e332b' ], avatar:'linear-gradient(140deg,#3f9a7c,#124334)', body:'Fünf Stills vom zweiten Brand, Farben deutlich satter als beim ersten.', tags:['Keramik','Serie'], stats:[['5','Assets'],['—','Länge'],['89','Score']] },
{ id:'f5', name:'Yara M.', sub:'Folge ich · Probe', kind:'Video', assets:'2 Clips', caption:'Aufwärmen im Spiegelsaal', hue:[ '#6a2f7a', '#26102c' ], avatar:'linear-gradient(140deg,#8b46a0,#341340)', body:'Zwei ruhige Einstellungen vor der eigentlichen Aufnahme.', tags:['Bewegung','Probe'], stats:[['2','Assets'],['29s','Länge'],['81','Score']] },
{ id:'f6', name:'Nils P.', sub:'Folge ich · Nachtrag', kind:'Video', assets:'1 Clip', caption:'Letzter Schliff', hue:[ '#7a2f38', '#2c1014' ], avatar:'linear-gradient(140deg,#a63a44,#4d151c)', body:'Ein kurzer Nachtrag zur Werkstattreihe, sauber belichtet.', tags:['Handwerk','Nachtrag'], stats:[['1','Asset'],['14s','Länge'],['86','Score']] }
] ]
}; };
const ALL = DATA.posts.concat(DATA.videos, DATA.following);
const TILE_HEIGHTS = [320, 400, 260, 340, 280, 440, 300, 360, 240, 380];
class Component extends DCLogic { class Component extends DCLogic {
state = { state = {
screen: 'feed', tab: 'posts', screen: 'feed', tab: 'posts', idx: 0,
saved: ['p5'], hidden: [], drag: null, exiting: null, showInfo: false, toast: '',
selectMode: false, selection: [], seen: 0, saved: 24, history: [],
detail: null, menuFor: null, toast: '',
draftTitle: 'Toner im Bogenfenster', draftSub: 'Modell · Glow Serum v3', draftKind: 'Produkt', draftTitle: 'Toner im Bogenfenster', draftSub: 'Modell · Glow Serum v3', draftKind: 'Produkt',
settings: { autoplay: true, sound: false, digest: true, motion: true } settings: { autoplay: true, sound: false, digest: true, motion: true }
}; };
componentDidMount() { componentDidMount() {
this._key = (e) => { this._key = (e) => {
if (e.key !== 'Escape') return; if (this.state.screen !== 'feed') return;
if (this.state.detail) { e.preventDefault(); this.setState({ detail: null }); return; } if (e.key === 'ArrowLeft') { e.preventDefault(); this.commit('left'); }
if (this.state.menuFor) { e.preventDefault(); this.setState({ menuFor: null }); return; } if (e.key === 'ArrowRight') { e.preventDefault(); this.commit('right'); }
if (this.state.selectMode) { e.preventDefault(); this.setState({ selectMode: false, selection: [] }); } if (e.key === 'ArrowUp') { e.preventDefault(); this.undo(); }
}; };
window.addEventListener('keydown', this._key); window.addEventListener('keydown', this._key);
} }
componentWillUnmount() { window.removeEventListener('keydown', this._key); if (this._toast) clearTimeout(this._toast); } componentWillUnmount() { window.removeEventListener('keydown', this._key); if (this._t) clearTimeout(this._t); }
deck() { const t = this.state.tab; return t === 'videos' ? DATA.videos : t === 'following' ? DATA.following : DATA.posts; } deck() { const t = this.state.tab; return t === 'videos' ? DATA.videos : t === 'following' ? DATA.following : DATA.posts; }
visible() { return this.deck().filter(c => !this.state.hidden.includes(c.id)); }
byId(id) { return ALL.find(c => c.id === id); }
flash(msg) { this.setState({ toast: msg }); clearTimeout(this._toast); this._toast = setTimeout(() => this.setState({ toast: '' }), 1700); } flash(msg) { this.setState({ toast: msg }); clearTimeout(this._toast); this._toast = setTimeout(() => this.setState({ toast: '' }), 1700); }
toggleSave(id) { commit(dir) {
const card = this.byId(id); if (this.state.exiting) return;
const wasSaved = this.state.saved.includes(id); if (this.state.idx >= this.deck().length) return;
this.setState(s => ({ saved: wasSaved ? s.saved.filter(x => x !== id) : s.saved.concat([id]) })); const card = this.deck()[this.state.idx];
this.flash(wasSaved ? 'Aus der Sammlung entfernt' : 'Zu „' + card.name + '" gemerkt'); this.setState({ exiting: dir, drag: null });
this._t = setTimeout(() => this.setState(s => ({
exiting: null, idx: s.idx + 1, drag: null,
seen: s.seen + 1, saved: dir === 'right' ? s.saved + 1 : s.saved,
history: s.history.concat([{ idx: s.idx, dir }])
})), 320);
this.flash(dir === 'right' ? 'Zu „' + card.name + '" gemerkt' : 'Übersprungen');
} }
hide(id) { undo() {
this.setState(s => ({ hidden: s.hidden.concat([id]), menuFor: null, detail: s.detail === id ? null : s.detail, const h = this.state.history;
selection: s.selection.filter(x => x !== id) })); if (!h.length) { this.flash('Nichts zurückzunehmen'); return; }
this.flash('Wird nicht mehr gezeigt'); const last = h[h.length - 1];
this.setState(s => ({ idx: last.idx, history: s.history.slice(0, -1), saved: last.dir === 'right' ? Math.max(0, s.saved - 1) : s.saved, seen: Math.max(0, s.seen - 1), exiting: null, drag: null }));
} }
toggleSelect(id) { onDown = (e) => {
this.setState(s => ({ selection: s.selection.includes(id) ? s.selection.filter(x => x !== id) : s.selection.concat([id]) })); if (this.state.exiting) return;
} e.currentTarget.setPointerCapture?.(e.pointerId);
this._start = { x: e.clientX, y: e.clientY };
saveSelection() { this.setState({ drag: { x: 0, y: 0, active: true } });
const n = this.state.selection.length; };
this.setState(s => ({ saved: s.saved.concat(s.selection.filter(id => !s.saved.includes(id))), selection: [], selectMode: false })); onMove = (e) => {
this.flash(n + (n === 1 ? ' Karte' : ' Karten') + ' zur Sammlung hinzugefügt'); if (!this.state.drag || !this.state.drag.active) return;
} this.setState({ drag: { x: e.clientX - this._start.x, y: e.clientY - this._start.y, active: true } });
};
onUp = () => {
const d = this.state.drag;
if (!d) return;
if (d.x > 120) this.commit('right');
else if (d.x < -120) this.commit('left');
else this.setState({ drag: null });
};
stripes(i) { return 'repeating-linear-gradient(' + (40 + i * 9) + 'deg, rgba(255,255,255,.08) 0 2px, transparent 2px 16px)'; } stripes(i) { return 'repeating-linear-gradient(' + (40 + i * 9) + 'deg, rgba(255,255,255,.08) 0 2px, transparent 2px 16px)'; }
mediaStyle(card, h, selected) {
return {
position: 'relative', height: h + 'px', borderRadius: '16px', overflow: 'hidden',
cursor: this.state.selectMode ? 'pointer' : 'zoom-in',
background: 'linear-gradient(155deg,' + card.hue[0] + ' 0%,' + card.hue[1] + ' 100%)',
outline: selected ? '4px solid #e60023' : 'none', outlineOffset: '-4px',
transition: 'outline-color .12s ease'
};
}
saveButton(isSaved) {
return {
padding: '11px 16px', border: 'none', borderRadius: '22px',
background: isSaved ? '#111111' : '#e60023', color: '#ffffff',
font: '700 15px/1 Manrope, system-ui, sans-serif', cursor: 'pointer',
transition: 'background .12s ease'
};
}
renderVals() { renderVals() {
const s = this.state; const s = this.state, deck = this.deck(), depth = Math.max(1, this.props.stackDepth ?? 3);
const cols = Math.max(3, Math.min(6, this.props.columns ?? 5)); const current = deck[s.idx];
const gridStyle = { columns: String(cols), columnGap: '16px' }; const dx = s.drag ? s.drag.x : 0, dy = s.drag ? s.drag.y : 0;
const visible = this.visible(); const rot = dx / 22;
const grid = visible.map((card, i) => { const stack = [];
const isSaved = s.saved.includes(card.id); for (let k = 0; k < depth; k++) {
const selected = s.selection.includes(card.id); const card = deck[s.idx + k];
return { if (!card) break;
caption: card.caption, name: card.name, kind: card.kind, const top = k === 0;
stripes: this.stripes(i), let transform = 'translate3d(0,' + (k * -12) + 'px,0) scale(' + (1 - k * 0.045) + ')';
media: this.mediaStyle(card, TILE_HEIGHTS[i % TILE_HEIGHTS.length], selected), let transition = 'transform .34s cubic-bezier(.22,.9,.3,1), opacity .3s ease';
activate: () => s.selectMode ? this.toggleSelect(card.id) : this.setState({ detail: card.id, menuFor: null }), let opacity = 1;
open: (e) => { e.stopPropagation(); this.setState({ detail: card.id, menuFor: null }); }, if (top) {
toggleSave: (e) => { e.stopPropagation(); this.toggleSave(card.id); }, if (s.exiting) {
openMenu: (e) => { e.stopPropagation(); this.setState(st => ({ menuFor: st.menuFor === card.id ? null : card.id })); }, transform = 'translate3d(' + (s.exiting === 'right' ? 620 : -620) + 'px, -40px, 0) rotate(' + (s.exiting === 'right' ? 22 : -22) + 'deg)';
hide: (e) => { e.stopPropagation(); this.hide(card.id); }, opacity = 0;
saveLabel: isSaved ? 'Gemerkt' : 'Merken', } else if (s.drag) {
saveStyle: this.saveButton(isSaved), transform = 'translate3d(' + dx + 'px,' + dy + 'px,0) rotate(' + rot + 'deg)';
menuOpen: s.menuFor === card.id, transition = s.drag.active ? 'none' : transition;
// Im Auswahlmodus verschwinden Merken und ⋯ — dort zaehlt nur die Auswahl. }
saveSlot: s.selectMode }
? { display: 'none' } const shell = {
: { position: 'absolute', right: '12px', top: '12px', zIndex: 10 }, position: 'absolute', inset: 0, zIndex: 10 - k, opacity, transform, transition,
menuSlot: s.selectMode borderRadius: '26px', overflow: 'hidden', boxShadow: '0 26px 60px rgba(0,0,0,.45)',
? { display: 'none' } border: '1px solid rgba(255,255,255,.14)', touchAction: 'none',
: { position: 'absolute', right: '12px', bottom: '12px', zIndex: 10 }, cursor: top ? (s.drag && s.drag.active ? 'grabbing' : 'grab') : 'default',
menuBtn: { pointerEvents: top ? 'auto' : 'none', userSelect: 'none'
width: '40px', height: '40px', border: 'none', borderRadius: '50%',
background: '#ffffff', boxShadow: '0 1px 8px rgba(0,0,0,.2)', color: '#111111',
fontSize: '18px', lineHeight: '1', cursor: 'pointer'
},
checkbox: s.selectMode
? { position: 'absolute', left: '12px', top: '12px', width: '28px', height: '28px',
borderRadius: '50%', display: 'grid', placeItems: 'center', fontSize: '15px', fontWeight: 700,
border: '2px solid #ffffff', color: selected ? '#ffffff' : 'transparent',
background: selected ? '#e60023' : 'rgba(0,0,0,.35)', pointerEvents: 'none' }
: { display: 'none' }
}; };
const showBadge = top && (Math.abs(dx) > 40 || !!s.exiting);
const liked = (s.exiting === 'right') || dx > 40;
stack.push({
shell: { ...shell },
down: top ? this.onDown : undefined,
move: top ? this.onMove : undefined,
up: top ? this.onUp : undefined,
media: { position: 'absolute', inset: 0, background: 'linear-gradient(155deg,' + card.hue[0] + ' 0%,' + card.hue[1] + ' 100%)' },
stripes: this.stripes(s.idx + k),
kind: card.kind, assets: card.assets, caption: card.caption, avatar: card.avatar,
name: card.name, sub: card.sub,
badgeText: liked ? 'GEMERKT' : 'SKIP',
badge: {
position: 'absolute', top: '86px', left: liked ? '24px' : 'auto', right: liked ? 'auto' : '24px',
padding: '8px 16px', borderRadius: '10px', fontWeight: 800, fontSize: '15px', letterSpacing: '.12em',
border: '2px solid ' + (liked ? 'rgba(126,226,184,.9)' : 'rgba(232,120,120,.9)'),
color: liked ? '#a9f0d0' : '#f0a8a8',
transform: 'rotate(' + (liked ? -10 : 10) + 'deg)',
opacity: showBadge ? Math.min(1, Math.abs(dx) / 110 || 1) : 0,
transition: 'opacity .16s ease', pointerEvents: 'none'
}
}); });
}
const railStyle = { const navDef = [['feed','Feed','◧'],['profile','Profil','◍'],['create','Erstellen',''],['settings','Einstellungen','⚙']];
width: '48px', height: '48px', display: 'grid', placeItems: 'center', border: 'none', const navItems = navDef.map(([id, label, glyph]) => ({
borderRadius: '50%', cursor: 'pointer', fontSize: '20px', label, glyph, go: () => this.setState({ screen: id }),
transition: 'background .12s ease, color .12s ease' style: {
}; display: 'flex', alignItems: 'center', gap: '11px', padding: '11px 13px', borderRadius: '11px',
const navDef = [['feed','Feed','⌂'],['profile','Profil','◍'],['create','Erstellen',''],['settings','Einstellungen','⚙']]; border: '1px solid ' + (s.screen === id ? 'rgba(126,226,184,.4)' : 'transparent'),
const navAll = navDef.map(([id, label, glyph]) => ({ background: s.screen === id ? 'rgba(126,226,184,.12)' : 'transparent',
label, glyph, go: () => this.setState({ screen: id, menuFor: null }), color: s.screen === id ? '#d6fbec' : 'rgba(242,239,233,.7)',
cls: s.screen === id ? 'rail active' : 'rail', style: railStyle font: '500 14px/1 Manrope, system-ui, sans-serif', cursor: 'pointer',
transition: 'background .18s ease, color .18s ease', textAlign: 'left'
}
})); }));
const tabDef = [['videos','Videos'],['following','Folge ich'],['posts','Posts']]; const tabDef = [['videos','Videos'],['following','Folge ich'],['posts','Posts']];
const tabs = tabDef.map(([id, label]) => ({ const tabs = tabDef.map(([id, label]) => ({
label, go: () => this.setState({ tab: id, screen: 'feed', selection: [], menuFor: null, detail: null }), label, go: () => this.setState({ tab: id, idx: 0, history: [], drag: null, exiting: null, screen: 'feed' }),
style: { style: {
padding: '0 2px 12px', border: 'none', background: 'transparent', padding: '9px 18px', borderRadius: '999px', border: '1px solid ' + (s.tab === id ? 'rgba(255,255,255,.22)' : 'transparent'),
borderBottom: '3px solid ' + (s.tab === id ? '#111111' : 'transparent'), background: s.tab === id ? 'rgba(255,255,255,.08)' : 'transparent',
color: s.tab === id ? '#111111' : '#767676', color: s.tab === id ? '#ffffff' : 'rgba(242,239,233,.58)',
font: (s.tab === id ? '700' : '500') + ' 16px/1 Manrope, system-ui, sans-serif', font: (s.tab === id ? '600' : '500') + ' 16px/1 Manrope, system-ui, sans-serif',
cursor: 'pointer', transition: 'color .12s ease, border-color .12s ease' cursor: 'pointer', transition: 'background .18s ease, color .18s ease'
} }
})); }));
const kindOptions = ['Produkt','Video','Serie'].map(k => ({ const kindOptions = ['Produkt','Video','Serie'].map(k => ({
label: k, go: () => this.setState({ draftKind: k }), label: k, go: () => this.setState({ draftKind: k }),
style: { style: {
padding: '11px 18px', borderRadius: '22px', cursor: 'pointer', border: 'none', padding: '10px 18px', borderRadius: '999px', cursor: 'pointer',
background: s.draftKind === k ? '#111111' : '#efefef', border: '1px solid ' + (s.draftKind === k ? 'rgba(126,226,184,.6)' : 'rgba(255,255,255,.16)'),
color: s.draftKind === k ? '#ffffff' : '#111111', background: s.draftKind === k ? 'rgba(126,226,184,.12)' : 'transparent',
font: '700 15px/1 Manrope, system-ui, sans-serif', color: s.draftKind === k ? '#a9f0d0' : 'rgba(242,239,233,.75)',
transition: 'background .12s ease, color .12s ease' font: '500 14px/1 Manrope, system-ui, sans-serif', letterSpacing: 'normal', textTransform: 'none',
transition: 'border-color .18s ease, background .18s ease'
} }
})); }));
const setDef = [['autoplay','Videos automatisch abspielen','Clips starten stumm, sobald die Kachel im Bild ist.'], const setDef = [['autoplay','Videos automatisch abspielen','Clips starten stumm, sobald die Karte oben liegt.'],
['sound','Ton beim Öffnen','Beim Öffnen der Detailansicht den Ton aktivieren.'], ['sound','Ton beim Öffnen','Beim Anklicken einer Karte den Ton aktivieren.'],
['digest','Wochenrückblick','Jeden Montag eine Zusammenfassung der neuen Posts.'], ['digest','Wochenrückblick','Jeden Montag eine Zusammenfassung der neuen Posts.'],
['motion','Bewegte Übergänge','Kacheln und Detailansicht animiert ein- und ausblenden.']]; ['motion','Bewegte Übergänge','Karten animiert ein- und ausblenden.']];
const settings = setDef.map(([id, label, hint]) => { const settings = setDef.map(([id, label, hint]) => {
const on = s.settings[id]; const on = s.settings[id];
return { return {
label, hint, label, hint,
toggle: () => this.setState(st => ({ settings: { ...st.settings, [id]: !st.settings[id] } })), toggle: () => this.setState(st => ({ settings: { ...st.settings, [id]: !st.settings[id] } })),
track: { position: 'relative', width: '52px', height: '30px', flex: 'none', borderRadius: '999px', cursor: 'pointer', track: { position: 'relative', width: '52px', height: '30px', flex: 'none', borderRadius: '999px', cursor: 'pointer',
border: 'none', background: on ? '#e60023' : '#cdcdcd', transition: 'background .2s ease' }, border: '1px solid ' + (on ? 'rgba(126,226,184,.6)' : 'rgba(255,255,255,.2)'),
knob: { position: 'absolute', top: '3px', left: on ? '25px' : '3px', width: '24px', height: '24px', borderRadius: '50%', background: on ? 'rgba(126,226,184,.18)' : 'rgba(255,255,255,.05)', transition: 'background .2s ease, border-color .2s ease' },
background: '#ffffff', boxShadow: '0 1px 3px rgba(0,0,0,.25)', transition: 'left .2s cubic-bezier(.22,.9,.3,1)' } knob: { position: 'absolute', top: '3px', left: on ? '25px' : '3px', width: '22px', height: '22px', borderRadius: '50%',
background: on ? '#8fe3c1' : 'rgba(242,239,233,.55)', transition: 'left .2s cubic-bezier(.22,.9,.3,1), background .2s ease' }
}; };
}); });
const gallery = s.saved.map(id => this.byId(id)).filter(Boolean).map((c, i) => ({ const gallery = DATA.posts.concat(DATA.videos).map((c, i) => ({
caption: c.caption, stripes: this.stripes(i), title: c.caption, stripes: this.stripes(i),
media: this.mediaStyle(c, TILE_HEIGHTS[i % TILE_HEIGHTS.length], false), tile: { position: 'relative', height: '230px', borderRadius: '16px', overflow: 'hidden', cursor: 'pointer',
open: () => this.setState({ detail: c.id }) border: '1px solid rgba(255,255,255,.12)', transition: 'transform .2s ease, border-color .2s ease',
background: 'linear-gradient(155deg,' + c.hue[0] + ' 0%,' + c.hue[1] + ' 100%)' }
})); }));
const d = s.detail ? this.byId(s.detail) : null;
const detailSaved = d ? s.saved.includes(d.id) : false;
return { return {
navTop: navAll.slice(0, 3), navBottom: navAll.slice(3), tabs, kindOptions, settings, navItems, tabs, stack, kindOptions, settings, gallery,
grid, gridStyle, gallery,
isFeed: s.screen === 'feed', isProfile: s.screen === 'profile', isCreate: s.screen === 'create', isSettings: s.screen === 'settings', isFeed: s.screen === 'feed', isProfile: s.screen === 'profile', isCreate: s.screen === 'create', isSettings: s.screen === 'settings',
gridEmpty: visible.length === 0, stackEmpty: stack.length === 0,
unhideAll: () => this.setState({ hidden: [] }), showInfo: s.showInfo, toggleInfo: () => this.setState(st => ({ showInfo: !st.showInfo })),
counter: visible.length + ' Karten · ' + s.saved.length + ' gemerkt', counter: Math.min(s.idx + 1, deck.length) + ' / ' + deck.length,
swipesToday: String(s.seen).padStart(2, '0'),
toggleSelectMode: () => this.setState(st => ({ selectMode: !st.selectMode, selection: [], menuFor: null })), onLike: () => this.commit('right'), onNope: () => this.commit('left'), onUndo: () => this.undo(),
selectToggleLabel: s.selectMode ? 'Fertig' : 'Auswählen', reset: () => this.setState({ idx: 0, history: [] }),
selectToggleStyle: { detailTitle: current ? current.caption : 'Keine Karte',
padding: '10px 18px', border: 'none', borderRadius: '22px', cursor: 'pointer', detailSub: current ? current.name + ' · ' + current.sub : 'Stapel leer',
background: s.selectMode ? '#111111' : '#efefef', color: s.selectMode ? '#ffffff' : '#111111', detailBody: current ? current.body : 'Lade den Stapel neu, um weitere Karten zu sichten.',
font: '700 15px/1 Manrope, system-ui, sans-serif', transition: 'background .12s ease, color .12s ease' detailTags: current ? current.tags : [],
}, detailStats: current ? current.stats.map(([value, label]) => ({ value, label })) : [],
selectionOpen: s.selectMode && s.selection.length > 0, profileStats: [{ value: '38', label: 'Posts' }, { value: String(s.saved), label: 'Gemerkt' }, { value: '1.204', label: 'Follower' }],
selectionCount: s.selection.length + ' ausgewählt',
saveSelection: () => this.saveSelection(),
clearSelection: () => this.setState({ selection: [] }),
detailOpen: !!d,
closeDetail: () => this.setState({ detail: null }),
stop: (e) => e.stopPropagation(),
detailMedia: d ? { position: 'relative', minHeight: '520px', background: 'linear-gradient(155deg,' + d.hue[0] + ' 0%,' + d.hue[1] + ' 100%)' } : {},
detailStripes: d ? this.stripes(2) : 'none',
detailTitle: d ? d.caption : '',
detailName: d ? d.name : '',
detailSub: d ? d.sub : '',
detailAvatar: d ? d.avatar : 'none',
detailBody: d ? d.body : '',
detailTags: d ? d.tags : [],
detailStats: d ? d.stats.map(([value, label]) => ({ value, label })) : [],
detailSaveLabel: detailSaved ? 'Gemerkt' : 'Merken',
detailSaveStyle: { ...this.saveButton(detailSaved), height: '48px', padding: '0 24px', borderRadius: '24px' },
detailToggleSave: () => this.toggleSave(s.detail),
detailHide: () => this.hide(s.detail),
profileStats: [{ value: '38', label: 'Posts' }, { value: String(s.saved.length), label: 'Gemerkt' }, { value: '1.204', label: 'Follower' }],
draftTitle: s.draftTitle, draftSub: s.draftSub, draftTitle: s.draftTitle, draftSub: s.draftSub,
setTitle: (e) => this.setState({ draftTitle: e.target.value }), setTitle: (e) => this.setState({ draftTitle: e.target.value }),
setSub: (e) => this.setState({ draftSub: e.target.value }), setSub: (e) => this.setState({ draftSub: e.target.value }),
publish: () => this.flash('Post veröffentlicht'), publish: () => this.flash('Post veröffentlicht'),
toast: s.toast, toast: s.toast
toastStyle: {
position: 'fixed', left: '50%', bottom: s.selectMode && s.selection.length ? '96px' : '28px',
transform: 'translateX(-50%)', padding: '16px 22px', borderRadius: '16px',
background: '#ffffff', boxShadow: '0 1px 20px rgba(0,0,0,.22)', color: '#111111',
fontSize: '15px', fontWeight: 600, animation: 'riseIn .2s ease', zIndex: 50
}
}; };
} }
} }