15 Commits

Author SHA1 Message Date
4c47090f42 Feed wird Raster statt Kartenstapel, Swipen entfaellt
Rueckmeldung vom Kunden: der Screenshot war als Funktionsaenderung
gemeint, nicht nur als Optik. Statt eine Karte nach der anderen
durchzuschieben, liegen jetzt alle nebeneinander und werden einzeln
gemerkt, ausgeblendet oder geoeffnet.

Raus:
- Kartenstapel, Maus-Drag, Pfeiltasten, Exit-Animation
- Undo samt History - "gemerkt" ist ein Zustand, den ein zweiter Klick
  zuruecknimmt, keine Entscheidung in einer History
- die drei Aktions-Buttons, das Kuerzel-Panel, die feste Detailspalte

Rein:
- Masonry-Raster (columns 3-6, Standard 5) mit gestaffelten Hoehen
- Merken-Pille und ⋯ beim Hover auf der Kachel; ⋯ bietet "Nicht mehr
  zeigen" und "Details oeffnen"
- Detailansicht als Overlay mit Medium, Beschreibung, Tags, Kennzahlen
- Auswahlmodus mit Auswahlkreisen und Sammelaktion "Zur Sammlung
  hinzufuegen"
- Escape schliesst der Reihe nach Detailansicht, Menue, Auswahlmodus

Karten haben jetzt eine id; Merken, Ausblenden, Auswahl und Detail
referenzieren darueber statt ueber einen Index. Die Profil-Galerie zeigt
dadurch genau die gemerkten Karten, die Kennzahl "Gemerkt" zaehlt sie.
Beispieldaten auf 10/8/6 Karten erweitert, damit das Raster traegt.

Offen und im README vermerkt: Suche ist weiterhin eine Attrappe,
Ausblenden ist fluechtig, Nachladen fuer echte Datenmengen fehlt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 23:38:14 +02:00
347fe7008a Reskin auf Pinterest-Desktop-Optik, Funktionen unveraendert
Vorlage ist die Pinterest-Web-App. Geaendert wurde ausschliesslich die
Gestaltung: state, Tastaturkuerzel, deck/flash/commit/undo, die
Drag-Handler und die Beispieldaten sind Byte-fuer-Byte identisch.

- Hell statt dunkel: weisser Grund, Text #111, roter Akzent #e60023
  statt Mint auf Tannen-/Weinrot-Verlauf
- Sidebar (236px, beschriftet) wird zur 80px-Icon-Rail, Einstellungen
  sitzt unten; Zustaende ueber .rail/.rail.active statt inline, weil die
  dc-Laufzeit style-hover mit !important setzt und den aktiven Button
  sonst beim Hover aufhellt
- Kopfzeile bekommt das Pinterest-Suchfeld (Pille, 48px) plus Konto;
  Tabs wandern in eine eigene Leiste mit 3px-Unterstrich
- Karten randlos mit 16px-Radius; Merken-Pille erscheint beim Hover oben
  rechts, dazu ein Scrim - beides an :hover/:focus-within der Karte
- Profil-Galerie wird Masonry (columns:5, gestaffelte Hoehen)
- Nur noch Manrope, JetBrains Mono und backdrop-filter entfallen

Der Tageszaehler "Karten gesichtet" hat mit der schmalen Rail seinen
Platz verloren und sitzt jetzt in der Tab-Leiste.

Offen: das Suchfeld ist eine Attrappe - die App hat keine Suche. Im
README unter "Offene Punkte" vermerkt.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 23:25:15 +02:00
39e6aa20e7 Design-Handoff Desktop-Ansicht: Spezifikation + Referenz-Prototyp
Die Mobile-App laeuft im Browser bisher als schmale Handy-Spalte
(siehe design/desktop-ansicht/referenz/mobile-ansicht-im-browser.png).
Dieses Bundle beschreibt die Desktop-Adaption: dreispaltiges App-Layout
mit permanenter Sidebar, zentralem Kartenstapel und Detailspalte,
Swipe ersetzt durch Maus-Drag, Buttons und Tastatur.

- README.md: vollstaendige Spezifikation (App-Shell, Screens Feed/Profil/
  Erstellen/Einstellungen, Interaktionen, State-Modell, Design-Tokens)
- Videogen Web.dc.html + support.js: Referenz-Prototyp zum Ansehen,
  ausdruecklich kein Produktionscode
- PROMPT.md: Umsetzungsauftrag, Pfade an die Ablage im Repo angepasst

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 08:56:54 +02:00
root
e6ed459947 Design-Sync: 3D-Ordner-System im Profil
prototyp-app.html aus dem Design-Projekt uebernommen: Ordnerbaum im Profil
mit 3D-Ordner-Optik, Ordner-Detail-Screen, Erstellen/Umbenennen/Loeschen/
Farbwahl-Dialoge, Bild-Kacheln im Ordner-Raster; alte Asset-/Attribut-
Karten des Profils dafuer entfernt. Bildpfade wieder auf .jpg gedreht.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 20:16:45 +00:00
root
48839527f4 Design-Sync: Liquid-Glass-Reskin, Create-Menue am Plus-Button, Nav-Hide
prototyp-app.html komplett aus dem Design-Projekt uebernommen (dort auf
Basis unseres Stands weiterentwickelt): Liquid-Glass-Optik fuer Chips,
Feed-Chips, Action-Buttons und Backpills (.lg-Look wie die Navbar),
Create-Menue-Popup ueber dem Plus-Button (Bild/Video/Modell), Nav blendet
sich situativ aus (.nav.hidden), Prompt-Feld-Restyle (.pf/.pf-box,
beam-round), Tastatur-Padding via .screen.kb-pad; Dither-Shader-Test auf
den Modell-Chips wie im Design entfernt. Bildpfade wieder auf .jpg gedreht.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 15:00:44 +00:00
root
65a19d7678 Design-Sync: Kapsel-Shape (8) fuer Dither-Chips, Liquid-Metal u_shape 0
Aus prototyp-app.html im Design-Projekt uebernommen: neue GLSL-Shape 8
(aspect-korrigierte Kapsel statt zentrierter Kugel - passt sich der
Pill-Breite an), Chips auf shape:8, Liquid-Metal-Uniform u_shape 1->0.
Bildpfade bleiben .jpg (Design-Kopie basierte auf aelterem .png-Stand).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 19:14:15 +00:00
root
2f2ec31466 Testeffekt: Dithering-Shader (Sphere/Random) auf den Modell-Chips im Create-Screen
WebGL2-Port von designali-in/dithering-shader als Vanilla-Modul (mountDither):
Shader laeuft als Canvas-Layer in den drei Chips, ausgewaehlt = weiss/silber
(#f2f2f2), nicht ausgewaehlt = dunkelgrau (#555); Auswahlwechsel aendert nur
die Front-Farbe (keine neuen GL-Kontexte). Mount lazy beim ersten Oeffnen des
Create-Screens; ohne WebGL2 bleibt der normale Chip-Look.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 15:02:28 +00:00
root
c1d278b6e5 Design-Update 4: Liquid-Metal-Buttons, Glas-Refraktion, neuer Create-Screen mit Fake-Tastatur
- Like/Plus/Nachmachen/Generieren als Liquid-Metal: WebGL-Shader (esm.sh
  @paper-design/shaders, liquidMetalFragmentShader) ueber Metall-Gradient-
  Fallback, schwarzer Kern, graues Icon mit Schattenwurf
- Nav-Glas: bl-glass-refract (feImage-Displacement-Map mit RGB-Split, aus
  Nav-Groesse dynamisch generiert) + blur(3px) saturate(1.15), Weiss .12,
  Ring .4/.3; Layer als echte divs statt Pseudo-Elemente
- Create komplett neu (aus BrandLoop.dc.html): Vorschau-Stack mit Ketten-
  Faecher + Dots + Video-Badge, Optionen-Accordion (Typ/Format/Kette),
  Prompt-Box mit Border-Beam (border-beam.css), Liquid-Metal-Generate,
  Fake-iOS-Tastatur mit @Modell-Tokens (inputmode=none gegen echte Tastatur),
  interner Scroll + body.noscroll auch fuer Create

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-23 14:38:29 +00:00
5786bcdcf6 Update preview config from ticket system 2026-07-22 19:53:57 +00:00
root
0291bd26b9 Fix: Explore-Screen scrollt nicht mehr (body.noscroll + overscroll-behavior)
Der globale .app-padding-bottom machte die Seite hoeher als den Viewport ->
auf dem Feed war vertikales Wippen moeglich. Explore sperrt jetzt body-Scroll
(overflow:hidden, height:100dvh, padding-bottom:0), scrollbare Screens
(Detail/Create/Profil) bleiben unveraendert.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 19:52:42 +00:00
root
64b1c8d703 Design-Update 3: neue Liquid-Glass-Nav (bl-glass-distortion), Nav auf Design-Hoehe, Feed-Animationen
- Nav: Specular-Lighting-Glasfilter + Weiss-Layer rgba(255,255,255,.25) +
  Kanten-Ring + Aussenschatten; bottom:14px ohne Safe-Area-Aufschlag
  (= exakt die Hoehe aus BrandLoop.dc.html), Bounce-Transition
- Fling: Top-Karte fadet aus (.3s), Stack rueckt mit Settle-Animation nach
  (.45s cubic-bezier(.22,1,.36,1)), 4 Karten im Stack, Snap-back weicher
- Tab-Wechsel: Deck+Blur Crossfade (180ms), Hood animiert sofort
- Blur-Layer: background .7s ease + opacity .25s ease
- x-actions Abstand auf Design-Wert 84px

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 19:30:06 +00:00
root
8ebe0632bc 5 Demo-Bilder (Pinterest-Auswahl des Nutzers) in Feed/Detail verdrahtet
okolo->Mara2, celimax-Poster->Lena, Balenciaga-Subway->Jonas+Avatar,
MODAILY->Mara3, THE-ROW->Tom1; Bildpfade auf .jpg umgestellt.
Offen: Mara1 (Bogenfenster-Toner), Tom4/Feld1/Kai, Feld2/Nina - naechste Lieferung.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 18:27:12 +00:00
root
342504f481 QR-Code für Expo Go (exps://expo-brandloop.project.webklar.com)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:32:19 +00:00
root
e55bc810e6 Design-Update 2 aus BrandLoop.dc.html: roter Glow, Liquid-Glass-Nav, Mara-Avatar, Karten-Stack
- Radial-Glow rgba(241,99,99,.21) am unteren Rand auf Explore, Detail und Create
- Bottom-Nav als Liquid-Glass: feTurbulence+feDisplacementMap-Filter als
  backdrop-filter (blur(20px) als Fallback) + Glas-Kanten-Ring via inset-Shadows
- Mara S. mit Avatar-Foto (50% 16% / 300%-Ausschnitt) im Feed und Detail-Header,
  Fallback bleibt Gradient+Initial solange das Bild fehlt
- Hintere Deck-Karten zeigen ihr eigenes erstes Bild (brightness .85) statt des
  geblurrten Top-Bilds

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:26:17 +00:00
root
8c7fbfe581 Explore-Update aus BrandLoop.dc.html: adaptive Hood-Tab-Leiste + echte Beispielbilder
- Hood-Kurve 1:1 aus dem Design: pro Tab eigene Plateau-Breite (hoodPath(cx,nh)),
  Tab-Positionen werden aus dem DOM gemessen, am Rand verschmilzt die Hood mit
  der Bildschirmkante; Center+Breite werden gemeinsam animiert (460ms, Overshoot)
- Tabs als zentrierte Flex-Row (gap 96px) mit vergroesserter Hit-Area
- Neue Geometrie: viewBox 430x120, floor 76, Statushoehe 64px (Design-Basis)
- POSTS mit pics-Arrays (uploads/pasted-*.png): Karten, Hintergrund-Blur und
  Detail-Strip nutzen echte Bilder; Fallback auf Dummy-Verlaeufe solange eine
  Datei fehlt (Preload-Probe), Labels nur ohne Bild
- Detail: neuer Prompt (Roter Toner im rosa Bogenfenster), Maras Ketten-Labels
  (Toner im Bogenfenster / Age Decoder Essence / Poster-Ad), Strip JS-gerendert

Hinweis: uploads/*.png folgen separat (Design-API kappt Dateien >256KiB)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-22 17:26:17 +00:00
14 changed files with 4294 additions and 179 deletions

View File

@@ -3,6 +3,7 @@
KI-Video-Generierung mit lernender Brand-Knowledge-Base (Elo-Scoring über Attribute). KI-Video-Generierung mit lernender Brand-Knowledge-Base (Elo-Scoring über Attribute).
- **Prototyp:** [prototyp-app.html](prototyp-app.html) statische Preview unter `videogen.project.webklar.com` - **Prototyp:** [prototyp-app.html](prototyp-app.html) statische Preview unter `videogen.project.webklar.com`
- **Desktop-Ansicht:** Design-Handoff für die Desktop-Adaption in [design/desktop-ansicht/](design/desktop-ansicht/) (Spezifikation: [README.md](design/desktop-ansicht/README.md))
- **Backend:** selbst gehostetes Appwrite (`https://appwrite.webklar.com/v1`), Projekt **BrandLoop** `6a5cee34002bb8360c34`, Datenbank `brandloop` - **Backend:** selbst gehostetes Appwrite (`https://appwrite.webklar.com/v1`), Projekt **BrandLoop** `6a5cee34002bb8360c34`, Datenbank `brandloop`
## Datenbank-Setup ## Datenbank-Setup

178
border-beam.css Normal file
View File

@@ -0,0 +1,178 @@
@property --beam-angle-bb {
syntax: "<angle>";
initial-value: 0deg;
inherits: true;
}
@property --beam-opacity-bb {
syntax: "<number>";
initial-value: 0;
inherits: true;
}
[data-beam="bb"] {
position: relative;
border-radius: 24px;
overflow: hidden;
}
[data-beam="bb"][data-active] {
animation:
beam-spin-bb 1.96s linear infinite,
beam-fade-in-bb 0.6s ease forwards;
}
[data-beam="bb"][data-active]::after {
content: "";
position: absolute;
inset: 0;
border-radius: 23px;
padding: 1px;
clip-path: inset(0 round 24px);
background: conic-gradient(
from var(--beam-angle-bb),
transparent 0%, transparent 54%,
rgba(255, 255, 255, 0.1) 57%,
rgba(255, 255, 255, 0.3) 60%,
rgba(255, 255, 255, 0.6) 63%,
rgba(255, 255, 255, 0.75) 66%,
rgba(255, 255, 255, 0.6) 69%,
rgba(255, 255, 255, 0.3) 72%,
rgba(255, 255, 255, 0.1) 75%,
transparent 78%, transparent 100%
),radial-gradient(ellipse 70px 40px at 33% -7.4%, rgb(255, 50, 100), transparent),
radial-gradient(ellipse 60px 35px at 12% -5%, rgb(40, 140, 255), transparent),
radial-gradient(ellipse 40px 70px at 2.1% 68.3%, rgb(50, 200, 80), transparent),
radial-gradient(ellipse 20px 35px at 2.1% 68.3%, rgb(30, 185, 170), transparent),
radial-gradient(ellipse 180px 32px at 74.4% 100%, rgb(100, 70, 255), transparent),
radial-gradient(ellipse 85px 26px at 55% 100%, rgb(40, 140, 255), transparent),
radial-gradient(ellipse 74px 32px at 93.9% 0%, rgb(255, 120, 40), transparent),
radial-gradient(ellipse 26px 42px at 100% 27.1%, rgb(240, 50, 180), transparent),
radial-gradient(ellipse 52px 48px at 100% 27.1%, rgb(180, 40, 240), transparent);
-webkit-mask:
conic-gradient(
from var(--beam-angle-bb),
transparent 0%, transparent 30%,
rgba(255, 255, 255, 0.1) 36%, rgba(255, 255, 255, 0.35) 44%,
white 52%, white 80%,
rgba(255, 255, 255, 0.35) 86%, rgba(255, 255, 255, 0.1) 92%,
transparent 95%, transparent 100%
),
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
-webkit-mask-composite: source-in, xor;
mask:
conic-gradient(
from var(--beam-angle-bb),
transparent 0%, transparent 30%,
rgba(255, 255, 255, 0.1) 36%, rgba(255, 255, 255, 0.35) 44%,
white 52%, white 80%,
rgba(255, 255, 255, 0.35) 86%, rgba(255, 255, 255, 0.1) 92%,
transparent 95%, transparent 100%
),
linear-gradient(#fff 0 0) content-box,
linear-gradient(#fff 0 0);
mask-composite: intersect, exclude;
pointer-events: none;
z-index: 2;
opacity: calc(var(--beam-opacity-bb) * 0.26 * var(--beam-stroke-opacity, 1) * var(--beam-strength, 1));
animation: beam-hue-shift-bb 12s ease-in-out infinite;
}
[data-beam="bb"][data-active]::before {
content: "";
position: absolute;
inset: 0;
border-radius: 24px;
background: radial-gradient(ellipse 63px 36px at 33% -7.4%, rgba(255, 50, 100, 0.45), transparent),
radial-gradient(ellipse 54px 32px at 12% -5%, rgba(40, 140, 255, 0.45), transparent),
radial-gradient(ellipse 36px 63px at 2.1% 68.3%, rgba(50, 200, 80, 0.45), transparent),
radial-gradient(ellipse 18px 32px at 2.1% 68.3%, rgba(30, 185, 170, 0.45), transparent),
radial-gradient(ellipse 162px 29px at 74.4% 100%, rgba(100, 70, 255, 0.45), transparent),
radial-gradient(ellipse 77px 23px at 55% 100%, rgba(40, 140, 255, 0.45), transparent),
radial-gradient(ellipse 67px 29px at 93.9% 0%, rgba(255, 120, 40, 0.45), transparent),
radial-gradient(ellipse 23px 38px at 100% 27.1%, rgba(240, 50, 180, 0.45), transparent),
radial-gradient(ellipse 47px 43px at 100% 27.1%, rgba(180, 40, 240, 0.45), transparent);
box-shadow: inset 0 0 9px 1px rgba(255, 255, 255, 0.27);
-webkit-mask-image:
conic-gradient(
from var(--beam-angle-bb),
transparent 0%, transparent 30%,
rgba(255, 255, 255, 0.1) 36%, rgba(255, 255, 255, 0.35) 44%,
white 52%, white 80%,
rgba(255, 255, 255, 0.35) 86%, rgba(255, 255, 255, 0.1) 92%,
transparent 95%, transparent 100%
),
linear-gradient(white, transparent 28px, transparent calc(100% - 28px), white),
linear-gradient(to right, white, transparent 28px, transparent calc(100% - 28px), white);
-webkit-mask-composite: source-in, source-over;
mask-image:
conic-gradient(
from var(--beam-angle-bb),
transparent 0%, transparent 30%,
rgba(255, 255, 255, 0.1) 36%, rgba(255, 255, 255, 0.35) 44%,
white 52%, white 80%,
rgba(255, 255, 255, 0.35) 86%, rgba(255, 255, 255, 0.1) 92%,
transparent 95%, transparent 100%
),
linear-gradient(white, transparent 28px, transparent calc(100% - 28px), white),
linear-gradient(to right, white, transparent 28px, transparent calc(100% - 28px), white);
mask-composite: intersect, add;
pointer-events: none;
z-index: 1;
opacity: calc(var(--beam-opacity-bb) * 0.42 * var(--beam-inner-opacity, 1) * var(--beam-strength, 1));
clip-path: inset(0 round 24px);
animation: beam-hue-shift-bb 12s ease-in-out infinite;
}
[data-beam="bb"] [data-beam-bloom] {
display: none;
position: absolute;
inset: 0;
border-radius: 23px;
clip-path: inset(0 round 24px);
background: conic-gradient(
from var(--beam-angle-bb),
transparent 0%, transparent 58%,
rgba(255, 255, 255, 0.03) 62%,
rgba(255, 255, 255, 0.08) 65%,
rgba(255, 255, 255, 0.2) 67%,
rgba(255, 255, 255, 0.45) 69%,
rgba(255, 255, 255, 0.85) 70%,
rgba(255, 255, 255, 0.85) 70.5%,
rgba(255, 255, 255, 0.45) 71.5%,
rgba(255, 255, 255, 0.2) 73%,
rgba(255, 255, 255, 0.08) 75%,
rgba(255, 255, 255, 0.03) 78%,
transparent 82%
);
-webkit-mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
-webkit-mask-composite: xor;
mask: linear-gradient(#fff 0 0) content-box, linear-gradient(#fff 0 0);
mask-composite: exclude;
padding: 1px;
filter: blur(8px) brightness(1.30) saturate(1.20);
pointer-events: none;
z-index: 3;
opacity: 0;
}
[data-beam="bb"][data-active] [data-beam-bloom] {
display: block;
opacity: calc(var(--beam-opacity-bb) * 0.24 * var(--beam-bloom-opacity, 1) * var(--beam-strength, 1));
}
@keyframes beam-spin-bb {
to { --beam-angle-bb: 360deg; }
}
@keyframes beam-fade-in-bb {
to { --beam-opacity-bb: 1; }
}
@keyframes beam-hue-shift-bb {
0% { filter: hue-rotate(calc(var(--beam-hue-base, 0deg) - 30deg)) brightness(1.30) saturate(1.20); }
50% { filter: hue-rotate(calc(var(--beam-hue-base, 0deg) + 30deg)) brightness(1.30) saturate(1.20); }
100% { filter: hue-rotate(calc(var(--beam-hue-base, 0deg) - 30deg)) brightness(1.30) saturate(1.20); }
}

View File

@@ -0,0 +1,29 @@
Setze das Design aus diesem Ordner in unserem Stack um.
1. Lies `design/desktop-ansicht/README.md` vollständig — es enthält Layout, exakte
Farben, Typografie, Abstände, Interaktionen, State-Modell und Design-Tokens.
2. Referenz-Prototyp: `design/desktop-ansicht/Videogen Web.dc.html`
(Template oben, Logikklasse mit Zustand/Interaktion unten, Beispieldaten in `DATA`).
Zum Ansehen im Browser öffnen — `support.js` muss im selben Ordner liegen.
Diese Dateien sind Design-Referenz, kein Produktionscode: nicht kopieren, sondern
in unserer bestehenden Umgebung mit unseren Patterns und Komponenten nachbauen.
3. Fidelity ist high — pixelgenau umsetzen: Farben, Radien, Übergänge, Hover-/Focus-States
und Animationsdauern wie im README angegeben.
Die visuelle Sprache ist Pinterest Desktop: weißer Hintergrund, 80px-Icon-Rail ohne
Beschriftung, Suchfeld als Pille, rote Primäraktion `#e60023`, randlose 16px-Kacheln,
Masonry-Raster.
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.
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

@@ -0,0 +1,304 @@
# Handoff: Videogen — Web-App (Desktop-Adaption der Mobile-App)
## Überblick
Die bestehende Mobile-App („videogen") als Desktop-Web-App. Statt einer schmalen Handy-Spalte im
Browser: Icon-Rail links, breite Kopfzeile mit Suche und ein **Masonry-Raster** als Feed.
**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
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
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 +
TypeScript) und dort umsetzen.
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
`.dc.html` im Browser öffnen. Die Logikklasse am Dateiende ist lesbarer JS-Code und dient als Referenz
für Zustand und Interaktion.
## Fidelity
**High-fidelity.** Farben, Typografie, Abstände, Radien, Übergänge und Interaktionen sind final gemeint
und sollen pixelgenau nachgebaut werden — mit den Komponenten/Utilities des Ziel-Codebases.
---
## Globales Layout (App-Shell)
- Wurzel: `display:flex`, `min-height:100vh`, **`min-width:1180px`** (darunter horizontales Scrollen,
die App ist Desktop-only gedacht).
- Hintergrund durchgehend `#ffffff`, Grundschrift `#111111`. Keine Verläufe, kein `backdrop-filter`.
### 1. Icon-Rail (links)
- Breite `80px`, `flex:none`, `position:sticky; top:0; height:100vh`, `background:#ffffff`,
rechte Kante `1px solid #efefef`. Padding `12px 0 16px`, zentrierte Flex-Spalte, `gap:8px`.
- **Brand**: 32×32 Kreis `#e60023` in einem 48px-Slot, weißes „V" 17px/700.
- **Nav-Buttons**: 48×48, `border-radius:50%`, Icon 20px, **ohne Textlabel** — der Name steht im
`title`-Attribut und wird als Tooltip gezeigt.
- Inaktiv: transparent, Icon `#111111`. Hover: `background:#e9e9e9`.
- Aktiv: `background:#111111`, Icon `#ffffff`. Aktiv + Hover: `background:#000000`.
- Transition `.12s ease`.
- **Umsetzungshinweis:** Die Zustände gehören in Klassen (`.rail`, `.rail.active`), nicht in
Inline-Styles. Im Prototyp setzt die `.dc`-Laufzeit `style-hover` mit `!important`; ein inline
gesetztes Aktiv-Schwarz würde beim Hover überschrieben und der aktive Button hellte auf.
- Reihenfolge oben: **Feed, Profil, Erstellen**. Unten (`margin-top:auto`): **Einstellungen**.
- Icons (im Prototyp Unicode-Glyphen als Platzhalter) — **Lucide**: `home`, `user`, `plus`, `settings`.
### 2. Kopfzeile
- `padding:12px 16px 4px`, Flex-Zeile, `gap:8px`.
- **Suchfeld**: `flex:1`, Höhe `48px`, `border-radius:24px`, `background:#efefef`, `padding:0 16px`,
Platzhaltertext „Suchen" 16px `#767676`; links Lupe, rechts Objektiv-Symbol
(Lucide `search` und `camera`).
- **Konto**: 40px Kreis `linear-gradient(140deg,#b2323c,#5d1620)` mit weißer Initiale 15px/700,
daneben 28px Chevron-Button (`▾`, Hover `#e9e9e9`).
### 3. Tab-Leiste
- `padding:0 24px`, Unterkante `1px solid #efefef`, Tabs mit `gap:24px`, an der Grundlinie ausgerichtet.
- **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
### A. Feed (Startansicht)
**Zweck:** Material sichten, Gutes merken, Uninteressantes ausblenden.
- Bereich: `flex:1; min-height:0; overflow:auto; padding:24px 24px 96px`
(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
- Medienfläche: `position:relative`, **variable Höhe** (Zyklus 320 / 400 / 260 / 340 / 280 / 440 /
300 / 360 / 240 / 380 px), `border-radius:16px`, `overflow:hidden`, **kein Rahmen**,
`cursor:zoom-in` (im Auswahlmodus `pointer`).
- Inhalt von unten nach oben:
1. Grundfläche `linear-gradient(155deg, <hue0> 0%, <hue1> 100%)`.
2. Streifen-Overlay `repeating-linear-gradient(<40+9·i>deg, rgba(255,255,255,.08) 0 2px, transparent 2px 16px)`,
`opacity:.45`. (Platzhalter für das Medium — im Produkt liegt hier Bild oder Video.)
3. **Hover-Scrim** `rgba(0,0,0,.24)`, `opacity` 0 → 1 in `.12s`.
4. Fußzeile auf `linear-gradient(0deg, rgba(0,0,0,.62), transparent)`, `padding:14px`:
Caption 14px/700 weiß, darunter „Name · Art" 12px `rgba(255,255,255,.82)`.
- **Hover-Steuerung** (beide `opacity` 0 → 1 in `.12s`, gebunden an den Hover der *Kachel*):
- **Merken-Pille** oben rechts (`right:12px; top:12px`): `padding:11px 16px; radius:22px;
font:700 15px`, ungemerkt `#e60023` mit weißem Text, **gemerkt `#111111` mit „Gemerkt"**.
Klick schaltet um — ein zweiter Klick nimmt zurück.
- **⋯-Button** unten rechts (`right:12px; bottom:12px`): 40px Kreis `#ffffff`,
`box-shadow: 0 1px 8px rgba(0,0,0,.2)`, Hover `#e9e9e9`.
- **⋯-Menü**: öffnet über dem Button (`right:12px; bottom:56px`), 210px, `radius:16px`,
`background:#ffffff`, `box-shadow: 0 1px 20px rgba(0,0,0,.22)`, `padding:8px`,
Einblendung `riseIn .14s`. Einträge `padding:12px 14px`, `radius:12px`, 15px/600,
Hover `#efefef`: **„Nicht mehr zeigen"** (blendet die Kachel aus) und **„Details öffnen"**.
Ein zweiter Klick auf ⋯ schließt das Menü, `Escape` ebenfalls.
- **Leerzustand** (alles ausgeblendet): zentriert, Titel „Nichts mehr in dieser Ansicht" 20px/700,
Text 15px `#5f5f5f`, roter Pill-Button „Ausgeblendete wieder anzeigen".
#### A2 Detailansicht (Overlay)
Ein Klick auf die Kachel öffnet sie — Pinterests Pin-Seite als Overlay.
- Backdrop: `position:fixed; inset:0; background:rgba(0,0,0,.6)`, Inhalt zentriert,
`padding:32px`, `z-index:60`, Einblendung `fadeIn .16s`. Klick auf den Backdrop schließt.
- Karte: `width:min(1016px,100%)`, `max-height:calc(100vh - 64px)`, `background:#ffffff`,
`border-radius:32px`, `overflow:hidden`, `grid-template-columns:1fr 1fr`, `riseIn .2s`.
- **Schließen**: 40px Kreis `#ffffff` mit `box-shadow: 0 1px 8px rgba(0,0,0,.2)`,
absolut `left:16px; top:16px` über dem Medium. Zusätzlich `Escape`.
- **Links**: das Medium, `min-height:520px`, Verlauf + Streifen wie auf der Kachel.
- **Rechts**: `padding:32px`, `overflow:auto`, Flex-Spalte `gap:20px`:
- Aktionszeile rechtsbündig: „Nicht mehr zeigen" (`#efefef`, 48px hoch, `radius:24px`, 15px/700)
und die Merken-Pille (48px hoch, `padding:0 24px`, `radius:24px`, rot bzw. schwarz wie auf der Kachel).
- Titel = Caption, 32px/700, `letter-spacing:-.02em`.
- 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
- `padding:32px 24px 40px`, `overflow:auto`, Einblendung `riseIn .26s`.
- Kopf **zentriert** als Spalte, `gap:10px`, `padding-bottom:28px`: 120px Avatar-Kreis
(`linear-gradient(140deg,#b2323c,#5d1620)`, ohne Rand), Name 36px/700 `letter-spacing:-.02em`,
Meta „Studio-Konto · seit März 2026" 15px `#5f5f5f`; darunter drei Kennzahlen (`gap:28px`,
zentriert): Posts 38, **Gemerkt (live: Anzahl der gemerkten Karten)**, Follower 1.204 —
Werte 18px/700 `tabular-nums`, Labels 13px `#767676`.
- **Die Galerie zeigt genau die gemerkten Karten** — dasselbe Masonry-Raster wie im Feed,
aber ohne Hover-Steuerung. Ein Klick öffnet die Detailansicht.
### C. Erstellen
- `padding:32px 24px 40px`; Inhalt in einer weißen Karte: `max-width:920px`, zentriert,
`radius:24px`, `box-shadow: 0 1px 20px rgba(0,0,0,.12)`, `padding:32px`,
`grid-template-columns: 1fr 320px`, `gap:32px`.
- Titel „Neuen Post erstellen" 30px/700, Untertitel 15px `#5f5f5f`.
- **Dropzone**: Höhe 220px, `border:2px dashed #cdcdcd`, `radius:16px`, `background:#f5f5f5`;
Hover Border `#e60023`, BG `#fafafa`.
Text „Dateien hierher ziehen" 16px/700 + „MP4, MOV oder bis zu 10 Bilder" 14px `#767676`.
- **Felder** Titel und Untertitel: Label 13px/700 `#111111`; Input `padding:14px 16px`,
`radius:16px`, `border:2px solid #cdcdcd`, `background:#ffffff`, 15px.
Beide sind an die Live-Vorschau gebunden.
- **Art**: Pills „Produkt / Video / Serie", `padding:11px 18px`, `radius:22px`, `font:700 15px`;
aktiv `background:#111111`, Text `#ffffff`; inaktiv `background:#efefef`, Text `#111111`.
- **Aktionen**: „Veröffentlichen" (rot `#e60023`, löst Toast „Post veröffentlicht" aus),
„Als Entwurf sichern" (`#efefef`), beide `padding:14px 24px`, `radius:24px`, `font:700 15px`.
- **Vorschau** (rechte Spalte): Kicker 12px/600 `#767676` + Karte 420px hoch, `radius:16px`,
Verlauf + Streifen, unten Titel 18px/700 und Untertitel 14px auf schwarzem Verlauf —
spiegelt die Eingaben live.
### D. Einstellungen
- `padding:32px 24px 40px`, Inhalt `max-width:660px`, zentriert. Titel „Einstellungen" 30px/700.
- Liste: Container `radius:16px`, `overflow:hidden`, `box-shadow: 0 1px 20px rgba(0,0,0,.12)`,
**kein Rahmen**. Zeile `padding:20px`, `background:#ffffff`, Trenner `1px solid #efefef`;
Label 16px/700, Hinweis 14px `#767676`.
- Toggle: Track 52×30, `radius:999px`, kein Rahmen; aus `#cdcdcd`, an `#e60023`.
Knopf 24px, immer `#ffffff` mit `box-shadow: 0 1px 3px rgba(0,0,0,.25)`, links 3px → 25px,
Transition `left .2s cubic-bezier(.22,.9,.3,1)`.
- Einträge: Videos automatisch abspielen (an), Ton beim Öffnen (aus), Wochenrückblick (an),
Bewegte Übergänge (an).
---
## Interaktion & Verhalten
**Merken** — zwei Wege, beide schalten um (zweiter Klick nimmt zurück):
1. Merken-Pille auf der Kachel (erscheint beim Hover).
2. Merken-Button in der Detailansicht.
Toast: „Zu <Name>' gemerkt" bzw. „Aus der Sammlung entfernt".
**Ausblenden** — „Nicht mehr zeigen" im ⋯-Menü oder in der Detailansicht. Die Kachel verschwindet
aus dem Raster, eine offene Detailansicht schließt sich, Toast „Wird nicht mehr gezeigt".
**Öffnen** — Klick auf die Kachel oder „Details öffnen" im ⋯-Menü.
**Mehrfachauswahl** — siehe [A3](#a3-auswahlmodus).
**Tastatur:** `Escape` schließt der Reihe nach: offene Detailansicht → offenes ⋯-Menü →
Auswahlmodus. Weitere Kürzel gibt es nicht mehr; die Pfeiltasten sind mit dem Swipen entfallen.
**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:**
- `riseIn`: `opacity:0; translateY(14px)` → `opacity:1; none` (.14.26s ease)
- `fadeIn`: `opacity:0` → `1`
- Fokus: `:focus-visible { outline: 2px solid #0074e8; outline-offset: 2px; }` — nie der Browser-Default.
**Responsive:** Desktop-only, `min-width:1180px`; darunter horizontal scrollen.
---
## State
| State | Typ | Zweck |
|---|---|---|
| `screen` | `'feed' \| 'profile' \| 'create' \| 'settings'` | aktive Ansicht (Icon-Rail) |
| `tab` | `'videos' \| 'following' \| 'posts'` | aktives Raster; Wechsel verwirft Auswahl und Menü |
| `saved` | `string[]` (Karten-IDs) | gemerkte Karten — speist Profil-Galerie und Kennzahl |
| `hidden` | `string[]` (Karten-IDs) | ausgeblendete Karten, aus dem Raster gefiltert |
| `selectMode` | boolean | Auswahlmodus aktiv |
| `selection` | `string[]` (Karten-IDs) | aktuell gewählte Kacheln |
| `detail` | `string \| null` | ID der offenen Detailansicht |
| `menuFor` | `string \| null` | ID der Kachel mit offenem ⋯-Menü |
| `toast` | string | Toast-Text (leer = versteckt) |
| `draftTitle`, `draftSub`, `draftKind` | string | Formular „Erstellen", live in der Vorschau |
| `settings` | `{autoplay, sound, digest, motion}` booleans | Einstellungen |
**Datenbedarf pro Karte:** `id`, `name`, `sub`, `kind`, `assets`, `caption`, `hue: [from, to]`,
`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
Karten darüber, nicht mehr über einen Index. Im Produkt ersetzt das Medium (Bild/Video) die
Verlaufs-/Streifenfläche. Die drei Tabs liefern drei Raster.
---
## Design-Tokens
**Farben**
| Rolle | Wert |
|---|---|
| Seite | `#ffffff` |
| Text | `#111111` |
| Text gedämpft | `#5f5f5f` (sekundär), `#767676` (Labels, inaktive Tabs), `#333333` (Beschreibung) |
| Akzent (Rot) | `#e60023`, Hover `#ad081b`; auch Auswahl-Outline und Auswahlkreis |
| Dunkel | `#111111` (aktiver Rail-Button, aktive Art-Pille, **gemerkt**-Zustand, „Fertig"), Hover `#000000` |
| Neutrale Flächen | `#efefef` (Suchfeld, Buttons, Tags), Hover `#e2e2e2`; `#e9e9e9` (Rail-/⋯-Hover); `#f5f5f5` (Statistik-Kacheln, Dropzone) |
| Linien | `#efefef` (Trenner, Kanten), `#cdcdcd` (Eingabe-Rahmen, Toggle aus) |
| 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 |
| 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` |
**Typografie** — **nur Manrope** 400/500/600/700. Zahlen mit `font-variant-numeric: tabular-nums`.
Skala: 36 / 32 / 30 / 20 / 18 / 17 / 16 / 15 / 14 / 13 / 12 px.
Große Überschriften `letter-spacing:-.02em`. Kicker sind 12px/600 in `#767676`, normal geschrieben.
**Abstände** 2 / 4 / 6 / 8 / 10 / 12 / 14 / 16 / 18 / 20 / 22 / 24 / 28 / 32 / 40 / 56 / 96 px.
**Radien** 12px (Menüeinträge), 16px (Kacheln, Eingaben, Toast, Menü), 18 / 22 / 24px (Pillen),
32px (Detailansicht, Auswahlleiste), `50%` Kreise, `999px` Toggle-Track.
**Schatten** `0 1px 8px rgba(0,0,0,.2)` schwebende Rundbuttons · `0 1px 20px rgba(0,0,0,.12)`
Flächen/Listen · `.22` Menü, Auswahlleiste, Toast · `0 1px 3px rgba(0,0,0,.25)` Toggle-Knopf.
**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
Keine Binärdateien im Prototyp. Alle Medien sind Platzhalter aus CSS-Verläufen und Streifenmustern —
im Produkt durch echte Bilder/Videos ersetzen. Icons: **Lucide** (im Prototyp Unicode-Glyphen als
Platzhalter). Fonts: Google Fonts — **nur Manrope**.
## 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
- `PROMPT.md` — fertiger Auftrag zum Einfügen in Claude Code.
- `Videogen Web.dc.html` — der Prototyp (Template oben, Logikklasse mit Zustand/Interaktion unten, Beispieldaten in `DATA`).
- `support.js` — Laufzeit, damit die HTML-Datei sich lokal öffnen lässt. Nicht portieren.
- `README.md` — dieses Dokument.
- `referenz/mobile-ansicht-im-browser.png` — Ausgangslage: die Mobile-App als schmale Spalte
unter `videogen.project.webklar.com`, die diese Desktop-Adaption ersetzt.

View File

@@ -0,0 +1,547 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<link rel="preconnect" href="https://fonts.googleapis.com">
<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">
<style>
html, body { margin:0; padding:0; background:#ffffff; }
* { box-sizing: border-box; }
a { color:#111111; text-decoration:none; }
::selection { background:rgba(230,0,35,.16); }
button:focus-visible, [tabindex]:focus-visible, input:focus-visible { outline:2px solid #0074e8; outline-offset:2px; }
@keyframes riseIn { from { opacity:0; transform:translateY(14px);} to { opacity:1; transform:none;} }
@keyframes fadeIn { from { opacity:0 } to { opacity:1 } }
/* Hover-Steuerung haengt an der Kachel, nicht am Element selbst. */
.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>
</helmet>
<div style="min-height:100vh; width:100%; min-width:1180px; display:flex; font-family:'Manrope',system-ui,sans-serif; color:#111111; background:#ffffff;">
<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;">
<div style="width:48px; height:48px; display:grid; place-items:center; margin-bottom:4px;">
<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>
<nav style="display:flex; flex-direction:column; align-items:center; gap:8px;">
<sc-for list="{{ navTop }}" as="item" hint-placeholder-count="3">
<button onClick="{{ item.go }}" title="{{ item.label }}" class="{{ item.cls }}" style="{{ item.style }}">{{ item.glyph }}</button>
</sc-for>
</nav>
<div style="margin-top:auto; display:flex; flex-direction:column; align-items:center; gap:10px;">
<sc-for list="{{ navBottom }}" as="item" hint-placeholder-count="1">
<button onClick="{{ item.go }}" title="{{ item.label }}" class="{{ item.cls }}" style="{{ item.style }}">{{ item.glyph }}</button>
</sc-for>
</div>
</aside>
<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;">
<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;">
<span style="font-size:15px;"></span>
<span style="flex:1; min-width:0;">Suchen</span>
<span style="font-size:15px;"></span>
</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>
<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 }}">
<section style="flex:1; min-height:0; overflow:auto; padding:24px 24px 96px;">
<div style="{{ gridStyle }}">
<sc-for list="{{ grid }}" as="c" hint-placeholder-count="10">
<div class="pin" style="position:relative; break-inside:avoid; margin-bottom:16px;">
<div onClick="{{ c.activate }}" style="{{ c.media }}">
<div style="position:absolute; inset:0; background:{{ c.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,.62), transparent); color:#ffffff;">
<div style="font-size:14px; font-weight:700; text-wrap:pretty;">{{ c.caption }}</div>
<div style="font-size:12px; color:rgba(255,255,255,.82); margin-top:2px;">{{ c.name }} · {{ c.kind }}</div>
</div>
<div style="{{ c.checkbox }}"></div>
</div>
<div class="pin-hover" style="{{ c.saveSlot }}">
<button onClick="{{ c.toggleSave }}" style="{{ c.saveStyle }}" style-hover="filter:brightness(.88);">{{ c.saveLabel }}</button>
</div>
<div class="pin-hover" style="{{ c.menuSlot }}">
<button onClick="{{ c.openMenu }}" title="Weitere Optionen" style="{{ c.menuBtn }}" style-hover="background:#e9e9e9;"></button>
</div>
<sc-if value="{{ c.menuOpen }}" hint-placeholder-val="{{ false }}">
<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;">
<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>
<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>
</sc-if>
</div>
</sc-for>
</div>
<sc-if value="{{ gridEmpty }}" hint-placeholder-val="{{ false }}">
<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:20px; font-weight:700;">Nichts mehr in dieser Ansicht</div>
<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>
</sc-if>
</section>
</sc-if>
<sc-if value="{{ isProfile }}" hint-placeholder-val="{{ false }}">
<section style="flex:1; padding:32px 24px 40px; overflow:auto; animation:riseIn .26s ease;">
<div style="display:flex; flex-direction:column; align-items:center; gap:10px; padding-bottom:28px;">
<div style="width:120px; height:120px; border-radius:50%; background:linear-gradient(140deg,#b2323c,#5d1620); flex:none;"></div>
<div style="font-size:36px; font-weight:700; letter-spacing:-.02em; text-align:center;">Jonas K.</div>
<div style="font-size:15px; color:#5f5f5f;">Studio-Konto · seit März 2026</div>
<div style="display:flex; gap:28px; margin-top:6px;">
<sc-for list="{{ profileStats }}" as="s" hint-placeholder-count="3">
<div style="text-align:center;">
<div style="font-size:18px; font-weight:700; font-variant-numeric:tabular-nums;">{{ s.value }}</div>
<div style="font-size:13px; color:#767676;">{{ s.label }}</div>
</div>
</sc-for>
</div>
</div>
<div style="{{ gridStyle }}">
<sc-for list="{{ gallery }}" as="g" hint-placeholder-count="8">
<div class="pin" style="position:relative; break-inside:avoid; margin-bottom:16px;">
<div onClick="{{ g.open }}" style="{{ g.media }}">
<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,.62), transparent); color:#ffffff; font-size:14px; font-weight:600;">{{ g.caption }}</div>
</div>
</div>
</sc-for>
</div>
</section>
</sc-if>
<sc-if value="{{ isCreate }}" hint-placeholder-val="{{ false }}">
<section style="flex:1; overflow:auto; padding:32px 24px 40px; 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="display:flex; flex-direction:column; gap:20px;">
<div>
<div style="font-size:30px; font-weight:700; 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>
<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>
<div style="font-size:16px; font-weight:700;">Dateien hierher ziehen</div>
<div style="font-size:14px; color:#767676; margin-top:6px;">MP4, MOV oder bis zu 10 Bilder</div>
</div>
</div>
<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
<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;">
</label>
<label style="display:flex; flex-direction:column; gap:8px; font-size:13px; font-weight:600; color:#111111;">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;">
</label>
<div style="display:flex; flex-direction:column; gap:10px; font-size:13px; font-weight:600; color:#111111;">Art
<div style="display:flex; gap:8px;">
<sc-for list="{{ kindOptions }}" as="k" hint-placeholder-count="3">
<button onClick="{{ k.go }}" style="{{ k.style }}">{{ k.label }}</button>
</sc-for>
</div>
</div>
</div>
<div style="display:flex; gap:8px;">
<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 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>
</div>
</div>
<div>
<div style="font-size:12px; font-weight:600; color:#767676; 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: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="font-size:18px; font-weight:700;">{{ draftTitle }}</div>
<div style="font-size:14px; color:rgba(255,255,255,.8);">{{ draftSub }}</div>
</div>
</div>
</div>
</div>
</section>
</sc-if>
<sc-if value="{{ isSettings }}" hint-placeholder-val="{{ false }}">
<section style="flex:1; overflow:auto; padding:32px 24px 40px; animation:riseIn .26s ease;">
<div style="max-width:660px; margin:0 auto;">
<div style="font-size:30px; font-weight:700; 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);">
<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>
<div style="font-size:16px; font-weight:700;">{{ s.label }}</div>
<div style="font-size:14px; color:#767676; margin-top:3px; text-wrap:pretty;">{{ s.hint }}</div>
</div>
<button onClick="{{ s.toggle }}" style="{{ s.track }}">
<span style="{{ s.knob }}"></span>
</button>
</div>
</sc-for>
</div>
</div>
</section>
</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 }}">
<div style="{{ toastStyle }}">{{ toast }}</div>
</sc-if>
</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>
</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;}}">
const DATA = {
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']] },
{ 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']] },
{ 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']] },
{ 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']] },
{ 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']] },
{ 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: [
{ 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']] },
{ 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']] },
{ 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']] },
{ 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: [
{ 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']] },
{ 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']] },
{ 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 {
state = {
screen: 'feed', tab: 'posts',
saved: ['p5'], hidden: [],
selectMode: false, selection: [],
detail: null, menuFor: null, toast: '',
draftTitle: 'Toner im Bogenfenster', draftSub: 'Modell · Glow Serum v3', draftKind: 'Produkt',
settings: { autoplay: true, sound: false, digest: true, motion: true }
};
componentDidMount() {
this._key = (e) => {
if (e.key !== 'Escape') return;
if (this.state.detail) { e.preventDefault(); this.setState({ detail: null }); return; }
if (this.state.menuFor) { e.preventDefault(); this.setState({ menuFor: null }); return; }
if (this.state.selectMode) { e.preventDefault(); this.setState({ selectMode: false, selection: [] }); }
};
window.addEventListener('keydown', this._key);
}
componentWillUnmount() { window.removeEventListener('keydown', this._key); if (this._toast) clearTimeout(this._toast); }
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); }
toggleSave(id) {
const card = this.byId(id);
const wasSaved = this.state.saved.includes(id);
this.setState(s => ({ saved: wasSaved ? s.saved.filter(x => x !== id) : s.saved.concat([id]) }));
this.flash(wasSaved ? 'Aus der Sammlung entfernt' : 'Zu „' + card.name + '" gemerkt');
}
hide(id) {
this.setState(s => ({ hidden: s.hidden.concat([id]), menuFor: null, detail: s.detail === id ? null : s.detail,
selection: s.selection.filter(x => x !== id) }));
this.flash('Wird nicht mehr gezeigt');
}
toggleSelect(id) {
this.setState(s => ({ selection: s.selection.includes(id) ? s.selection.filter(x => x !== id) : s.selection.concat([id]) }));
}
saveSelection() {
const n = this.state.selection.length;
this.setState(s => ({ saved: s.saved.concat(s.selection.filter(id => !s.saved.includes(id))), selection: [], selectMode: false }));
this.flash(n + (n === 1 ? ' Karte' : ' Karten') + ' zur Sammlung hinzugefügt');
}
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() {
const s = this.state;
const cols = Math.max(3, Math.min(6, this.props.columns ?? 5));
const gridStyle = { columns: String(cols), columnGap: '16px' };
const visible = this.visible();
const grid = visible.map((card, i) => {
const isSaved = s.saved.includes(card.id);
const selected = s.selection.includes(card.id);
return {
caption: card.caption, name: card.name, kind: card.kind,
stripes: this.stripes(i),
media: this.mediaStyle(card, TILE_HEIGHTS[i % TILE_HEIGHTS.length], selected),
activate: () => s.selectMode ? this.toggleSelect(card.id) : this.setState({ detail: card.id, menuFor: null }),
open: (e) => { e.stopPropagation(); this.setState({ detail: card.id, menuFor: null }); },
toggleSave: (e) => { e.stopPropagation(); this.toggleSave(card.id); },
openMenu: (e) => { e.stopPropagation(); this.setState(st => ({ menuFor: st.menuFor === card.id ? null : card.id })); },
hide: (e) => { e.stopPropagation(); this.hide(card.id); },
saveLabel: isSaved ? 'Gemerkt' : 'Merken',
saveStyle: this.saveButton(isSaved),
menuOpen: s.menuFor === card.id,
// Im Auswahlmodus verschwinden Merken und ⋯ — dort zaehlt nur die Auswahl.
saveSlot: s.selectMode
? { display: 'none' }
: { position: 'absolute', right: '12px', top: '12px', zIndex: 10 },
menuSlot: s.selectMode
? { display: 'none' }
: { position: 'absolute', right: '12px', bottom: '12px', zIndex: 10 },
menuBtn: {
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 railStyle = {
width: '48px', height: '48px', display: 'grid', placeItems: 'center', border: 'none',
borderRadius: '50%', cursor: 'pointer', fontSize: '20px',
transition: 'background .12s ease, color .12s ease'
};
const navDef = [['feed','Feed','⌂'],['profile','Profil','◍'],['create','Erstellen',''],['settings','Einstellungen','⚙']];
const navAll = navDef.map(([id, label, glyph]) => ({
label, glyph, go: () => this.setState({ screen: id, menuFor: null }),
cls: s.screen === id ? 'rail active' : 'rail', style: railStyle
}));
const tabDef = [['videos','Videos'],['following','Folge ich'],['posts','Posts']];
const tabs = tabDef.map(([id, label]) => ({
label, go: () => this.setState({ tab: id, screen: 'feed', selection: [], menuFor: null, detail: null }),
style: {
padding: '0 2px 12px', border: 'none', background: 'transparent',
borderBottom: '3px solid ' + (s.tab === id ? '#111111' : 'transparent'),
color: s.tab === id ? '#111111' : '#767676',
font: (s.tab === id ? '700' : '500') + ' 16px/1 Manrope, system-ui, sans-serif',
cursor: 'pointer', transition: 'color .12s ease, border-color .12s ease'
}
}));
const kindOptions = ['Produkt','Video','Serie'].map(k => ({
label: k, go: () => this.setState({ draftKind: k }),
style: {
padding: '11px 18px', borderRadius: '22px', cursor: 'pointer', border: 'none',
background: s.draftKind === k ? '#111111' : '#efefef',
color: s.draftKind === k ? '#ffffff' : '#111111',
font: '700 15px/1 Manrope, system-ui, sans-serif',
transition: 'background .12s ease, color .12s ease'
}
}));
const setDef = [['autoplay','Videos automatisch abspielen','Clips starten stumm, sobald die Kachel im Bild ist.'],
['sound','Ton beim Öffnen','Beim Öffnen der Detailansicht den Ton aktivieren.'],
['digest','Wochenrückblick','Jeden Montag eine Zusammenfassung der neuen Posts.'],
['motion','Bewegte Übergänge','Kacheln und Detailansicht animiert ein- und ausblenden.']];
const settings = setDef.map(([id, label, hint]) => {
const on = s.settings[id];
return {
label, hint,
toggle: () => this.setState(st => ({ settings: { ...st.settings, [id]: !st.settings[id] } })),
track: { position: 'relative', width: '52px', height: '30px', flex: 'none', borderRadius: '999px', cursor: 'pointer',
border: 'none', background: on ? '#e60023' : '#cdcdcd', transition: 'background .2s ease' },
knob: { position: 'absolute', top: '3px', left: on ? '25px' : '3px', width: '24px', height: '24px', borderRadius: '50%',
background: '#ffffff', boxShadow: '0 1px 3px rgba(0,0,0,.25)', transition: 'left .2s cubic-bezier(.22,.9,.3,1)' }
};
});
const gallery = s.saved.map(id => this.byId(id)).filter(Boolean).map((c, i) => ({
caption: c.caption, stripes: this.stripes(i),
media: this.mediaStyle(c, TILE_HEIGHTS[i % TILE_HEIGHTS.length], false),
open: () => this.setState({ detail: c.id })
}));
const d = s.detail ? this.byId(s.detail) : null;
const detailSaved = d ? s.saved.includes(d.id) : false;
return {
navTop: navAll.slice(0, 3), navBottom: navAll.slice(3), tabs, kindOptions, settings,
grid, gridStyle, gallery,
isFeed: s.screen === 'feed', isProfile: s.screen === 'profile', isCreate: s.screen === 'create', isSettings: s.screen === 'settings',
gridEmpty: visible.length === 0,
unhideAll: () => this.setState({ hidden: [] }),
counter: visible.length + ' Karten · ' + s.saved.length + ' gemerkt',
toggleSelectMode: () => this.setState(st => ({ selectMode: !st.selectMode, selection: [], menuFor: null })),
selectToggleLabel: s.selectMode ? 'Fertig' : 'Auswählen',
selectToggleStyle: {
padding: '10px 18px', border: 'none', borderRadius: '22px', cursor: 'pointer',
background: s.selectMode ? '#111111' : '#efefef', color: s.selectMode ? '#ffffff' : '#111111',
font: '700 15px/1 Manrope, system-ui, sans-serif', transition: 'background .12s ease, color .12s ease'
},
selectionOpen: s.selectMode && s.selection.length > 0,
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,
setTitle: (e) => this.setState({ draftTitle: e.target.value }),
setSub: (e) => this.setState({ draftSub: e.target.value }),
publish: () => this.flash('Post veröffentlicht'),
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
}
};
}
}
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1001 KiB

File diff suppressed because it is too large Load Diff

BIN
expo-qr.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB