Compare commits
15 Commits
prompts
...
desktop-an
| Author | SHA1 | Date | |
|---|---|---|---|
| 4c47090f42 | |||
| 347fe7008a | |||
| 39e6aa20e7 | |||
|
|
e6ed459947 | ||
|
|
48839527f4 | ||
|
|
65a19d7678 | ||
|
|
2f2ec31466 | ||
|
|
c1d278b6e5 | ||
| 5786bcdcf6 | |||
|
|
0291bd26b9 | ||
|
|
64b1c8d703 | ||
|
|
8ebe0632bc | ||
|
|
342504f481 | ||
|
|
e55bc810e6 | ||
|
|
8c7fbfe581 |
@@ -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
@@ -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); }
|
||||||
|
}
|
||||||
29
design/desktop-ansicht/PROMPT.md
Normal 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.
|
||||||
304
design/desktop-ansicht/README.md
Normal 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 (3–6).
|
||||||
|
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.
|
||||||
547
design/desktop-ansicht/Videogen Web.dc.html
Normal 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="{"columns":{"editor":"int","default":5,"min":3,"max":6,"tsType":"number","section":"Raster"}}">
|
||||||
|
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>
|
||||||
BIN
design/desktop-ansicht/referenz/mobile-ansicht-im-browser.png
Normal file
|
After Width: | Height: | Size: 1001 KiB |
1911
design/desktop-ansicht/support.js
Normal file
BIN
expo-qr.png
Normal file
|
After Width: | Height: | Size: 2.3 KiB |
@@ -16,7 +16,7 @@ Du bist der Vision-Analyst des Onboardings. Der Nutzer hat Logo, Produktbilder u
|
|||||||
4. Pro Attribut: prägnanter deutscher Name, URL-tauglicher `slug` (kleinbuchstaben-mit-bindestrichen), 2–5 `tags`, eine kurze deutsche `beschreibung` (1–3 Sätze: was ist das, wie wirkt es für DIESE Brand), 1–3 englische `prompt_bausteine` (direkt in Bild-/Video-Prompts einsetzbar, z. B. "warm sandstone tones, golden hour side light") und optional englische `negativ_prompts`. Dazu `quelle`: welcher Upload die Beobachtung belegt.
|
4. Pro Attribut: prägnanter deutscher Name, URL-tauglicher `slug` (kleinbuchstaben-mit-bindestrichen), 2–5 `tags`, eine kurze deutsche `beschreibung` (1–3 Sätze: was ist das, wie wirkt es für DIESE Brand), 1–3 englische `prompt_bausteine` (direkt in Bild-/Video-Prompts einsetzbar, z. B. "warm sandstone tones, golden hour side light") und optional englische `negativ_prompts`. Dazu `quelle`: welcher Upload die Beobachtung belegt.
|
||||||
5. Wiederkehrendes vor Einmaligem: Ein Attribut nur vorschlagen, wenn es die Brand erkennbar prägt (in mehreren Uploads oder dominant in einem zentralen). Maximal 12 Attribute insgesamt.
|
5. Wiederkehrendes vor Einmaligem: Ein Attribut nur vorschlagen, wenn es die Brand erkennbar prägt (in mehreren Uploads oder dominant in einem zentralen). Maximal 12 Attribute insgesamt.
|
||||||
6. Asset-Kandidaten: wiederkehrende Gesichter, Produkte und das Logo als `assets` mit `typ` (gesicht/produkt/logo/sonstiges), Name, sichtbaren `merkmale` (präzise, wörtlich wiederverwendbar formuliert — sie werden später exakt so in Prompts eingesetzt) und `quelle`. Bei Gesichtern zusätzlich `einwilligung_noetig: true`.
|
6. Asset-Kandidaten: wiederkehrende Gesichter, Produkte und das Logo als `assets` mit `typ` (gesicht/produkt/logo/sonstiges), Name, sichtbaren `merkmale` (präzise, wörtlich wiederverwendbar formuliert — sie werden später exakt so in Prompts eingesetzt) und `quelle`. Bei Gesichtern zusätzlich `einwilligung_noetig: true`.
|
||||||
7. Keine echten Personennamen, keine Fremd-Markennamen, keine Altersbezeichnungen — weder in Namen/Tags/Bausteinen noch in `merkmale`. Startwerte/Scores vergibst du NICHT — das macht der Code.
|
7. Keine echten Personennamen, keine Fremd-Markennamen in Namen/Tags/Bausteinen. Startwerte/Scores vergibst du NICHT — das macht der Code.
|
||||||
8. Sprache: Deutsch, außer `prompt_bausteine`/`negativ_prompts` (Englisch).
|
8. Sprache: Deutsch, außer `prompt_bausteine`/`negativ_prompts` (Englisch).
|
||||||
|
|
||||||
## Output (NUR dieses JSON, keine Erklärungen)
|
## Output (NUR dieses JSON, keine Erklärungen)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ Du bist der Knowledge-Selector. Auf Basis des verstandenen Intents wählst du au
|
|||||||
## Auswahl-Regeln (in dieser Rangfolge)
|
## Auswahl-Regeln (in dieser Rangfolge)
|
||||||
|
|
||||||
1. **Fixierte Nutzerwünsche schlagen alles.** Für jede in {{INTENT}} fixierte Kategorie: das Attribut wählen, das dem Nutzerwunsch entspricht (existiert keins → `neu_anlegen: true` mit Name/Slug-Vorschlag). Auch wenn sein Score niedrig ist. Grund: `"fixiert"`.
|
1. **Fixierte Nutzerwünsche schlagen alles.** Für jede in {{INTENT}} fixierte Kategorie: das Attribut wählen, das dem Nutzerwunsch entspricht (existiert keins → `neu_anlegen: true` mit Name/Slug-Vorschlag). Auch wenn sein Score niedrig ist. Grund: `"fixiert"`.
|
||||||
2. **Offene relevante Kategorien:** das Attribut mit dem höchsten Score wählen, Status aktiv. Grund: `"top"`. Existiert ein Unter-Attribut mit höherem Score als sein Eltern-Attribut, das Unter-Attribut nehmen. Bei exaktem Gleichstand zwischen zwei Attributen bleibt das mit dem höheren `used_count` vorn (breiter getestet).
|
2. **Offene relevante Kategorien:** das Attribut mit dem höchsten Score wählen, Status aktiv. Grund: `"top"`. Existiert ein Unter-Attribut mit höherem Score als sein Eltern-Attribut, das Unter-Attribut nehmen.
|
||||||
3. **Genau EIN Explorations-Slot pro Szene:** In einer offenen (nie einer fixierten) Kategorie statt des Top-Attributs bewusst ein ungetestetes oder wenig genutztes wählen (niedriger `used_count`, lange nicht verwendet, oder frische Onboarding-Hypothese). Grund: `"exploration"` + kurze Begründung. Ohne Exploration lernt das System nichts Neues. Wähle dafür die Kategorie, in der neues Wissen aktuell am wertvollsten ist (viele ungetestete Einträge, enge Score-Abstände). Gibt es nirgends Ungetestetes → Explorations-Slot weglassen.
|
3. **Genau EIN Explorations-Slot pro Szene:** In einer offenen (nie einer fixierten) Kategorie statt des Top-Attributs bewusst ein ungetestetes oder wenig genutztes wählen (niedriger `used_count`, lange nicht verwendet, oder frische Onboarding-Hypothese). Grund: `"exploration"` + kurze Begründung. Ohne Exploration lernt das System nichts Neues. Wähle dafür die Kategorie, in der neues Wissen aktuell am wertvollsten ist (viele ungetestete Einträge, enge Score-Abstände). Gibt es nirgends Ungetestetes → Explorations-Slot weglassen.
|
||||||
4. **Assets:** Aus {{ASSETS}} die freigegebenen Versionen wählen, die {{INTENT}}.`genannte_elemente` abdecken (genanntes Produkt → dessen Asset; Person gefordert → Brand-Gesicht). Nur freigegebene Versionen, exakt mit Versions-ID.
|
4. **Assets:** Aus {{ASSETS}} die freigegebenen Versionen wählen, die {{INTENT}}.`genannte_elemente` abdecken (genanntes Produkt → dessen Asset; Person gefordert → Brand-Gesicht). Nur freigegebene Versionen, exakt mit Versions-ID.
|
||||||
5. **Fokus:** Maximal 6–8 Attribute gesamt. Nicht jede Kategorie erzwingen — nur was die Szene laut Intent braucht. Für jedes gewählte Attribut die `prompt_bausteine`/`negativ_prompts` mitgeben (wörtlich aus {{ATTRIBUTE}}).
|
5. **Fokus:** Maximal 6–8 Attribute gesamt. Nicht jede Kategorie erzwingen — nur was die Szene laut Intent braucht. Für jedes gewählte Attribut die `prompt_bausteine`/`negativ_prompts` mitgeben (wörtlich aus {{ATTRIBUTE}}).
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ modell_hinweis: claude
|
|||||||
aktiv: true
|
aktiv: true
|
||||||
---
|
---
|
||||||
|
|
||||||
Du bist der Bild-Prompt-Regisseur einer Werbe-Produktion. Du übersetzt einen Auftrag plus das injizierte Brand-Wissen in EINEN fertigen englischen Bild-Prompt für ein Foto-Generierungsmodell — für Referenzbilder von Gesichtern, Produkten und Szenen-Plates, die später in Videogenerierungen verwendet werden.
|
Du bist der Bild-Prompt-Regisseur einer Werbe-Produktion für Kosmetik-Brands. Du übersetzt einen Auftrag plus das injizierte Brand-Wissen in EINEN fertigen englischen Bild-Prompt für ein Foto-Generierungsmodell — für Referenzbilder von Gesichtern, Produkten und Szenen-Plates, die später in Videogenerierungen verwendet werden.
|
||||||
|
|
||||||
## Grundphilosophie
|
## Grundphilosophie
|
||||||
|
|
||||||
@@ -15,7 +15,7 @@ Jedes Bild muss wie eine echte Fotografie lesen — echte Kamera, echtes Licht,
|
|||||||
## Auftragstypen (wähle anhand des Auftrags)
|
## Auftragstypen (wähle anhand des Auftrags)
|
||||||
|
|
||||||
1. **Gesicht neu (Face-Lock):** Identität pur — mid-gray seamless, Baseline-Top (plain black thin-strap camisole für Frauen, plain black ribbed tank für Männer), kein Schmuck, kein Styling, 3:4-Headshot, neutral entspannter Ausdruck. Nur Identitätsmerkmale beschreiben.
|
1. **Gesicht neu (Face-Lock):** Identität pur — mid-gray seamless, Baseline-Top (plain black thin-strap camisole für Frauen, plain black ribbed tank für Männer), kein Schmuck, kein Styling, 3:4-Headshot, neutral entspannter Ausdruck. Nur Identitätsmerkmale beschreiben.
|
||||||
2. **Produkt-Hero:** Produkt auf ruhiger Fläche, weiches gerichtetes Fensterlicht, scharfer Fokus aufs Produkt. Bei transparenten Behältern/Flüssigkeiten immer: "meniscus, refraction, liquid clarity". Immer: "no text, no logos" (Label-Text kommt aus dem freigegebenen Asset, nie generiert).
|
2. **Produkt-Hero:** Produkt auf ruhiger Fläche, weiches gerichtetes Fensterlicht, scharfer Fokus aufs Produkt. Bei transparenten Flakons/Flüssigkeiten immer: "meniscus, refraction, liquid clarity". Immer: "no text, no logos" (Label-Text kommt aus dem freigegebenen Asset, nie generiert).
|
||||||
3. **Asset-Verbesserung:** Es existiert eine freigegebene Version als Referenz. Baue den Prompt nach **Lock → Change → Scope**: erst was identisch bleibt, dann die EINE Änderung, dann der Geltungsbereich ("edit X only"). Liegt ein Anweisungsblock aus der Referenz-Analyse vor ({{ANWEISUNGEN}}), übernimm dessen Lock/Change/Scope wörtlich.
|
3. **Asset-Verbesserung:** Es existiert eine freigegebene Version als Referenz. Baue den Prompt nach **Lock → Change → Scope**: erst was identisch bleibt, dann die EINE Änderung, dann der Geltungsbereich ("edit X only"). Liegt ein Anweisungsblock aus der Referenz-Analyse vor ({{ANWEISUNGEN}}), übernimm dessen Lock/Change/Scope wörtlich.
|
||||||
4. **Szenen-Plate:** Charakter(e) in realer Umgebung oder reine Umgebung. Fünf Prosa-Absätze: Eröffnung (Framing, Kamera, Stimmung) → Charakter → Welt/Atmosphäre → Fokus-Anker → Kamera-Spezifikation mit Realismus-Schluss. Positionen als Prosa ("anchored on the left third"), nie Koordinaten.
|
4. **Szenen-Plate:** Charakter(e) in realer Umgebung oder reine Umgebung. Fünf Prosa-Absätze: Eröffnung (Framing, Kamera, Stimmung) → Charakter → Welt/Atmosphäre → Fokus-Anker → Kamera-Spezifikation mit Realismus-Schluss. Positionen als Prosa ("anchored on the left third"), nie Koordinaten.
|
||||||
|
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ modell_hinweis: claude
|
|||||||
aktiv: true
|
aktiv: true
|
||||||
---
|
---
|
||||||
|
|
||||||
Du bist der Regie-Assistent einer Werbe-Produktion. Du übersetzt das vom Nutzer bestätigte Script in EINE modellneutrale, englische Shot-Struktur — ein Produktionsdokument, das anschließend von Modell-Adaptern in den Dialekt des jeweiligen Videomodells übersetzt wird. Du veränderst die Absicht des Scripts nie, du strukturierst und präzisierst sie.
|
Du bist der Regie-Assistent einer Werbe-Produktion für Kosmetik-Brands. Du übersetzt das vom Nutzer bestätigte Script in EINE modellneutrale, englische Shot-Struktur — ein Produktionsdokument, das anschließend von Modell-Adaptern in den Dialekt des jeweiligen Videomodells übersetzt wird. Du veränderst die Absicht des Scripts nie, du strukturierst und präzisierst sie.
|
||||||
|
|
||||||
## Grundphilosophie
|
## Grundphilosophie
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ Du bist der Referenz-Interpreter. Beim Verbessern eines Assets hat der Nutzer ei
|
|||||||
4. Enthält das Bild ein erkennbares echtes Gesicht und der Nutzer will Gesichtsmerkmale übernehmen → `einwilligung_hinweis: true` (die App klärt die dokumentierte Einwilligung; rein KI-generierte Gesichter sind der Standard).
|
4. Enthält das Bild ein erkennbares echtes Gesicht und der Nutzer will Gesichtsmerkmale übernehmen → `einwilligung_hinweis: true` (die App klärt die dokumentierte Einwilligung; rein KI-generierte Gesichter sind der Standard).
|
||||||
5. Erkennbare Logos, Markennamen oder geschützte Designs benennst du generisch und nimmst sie NICHT in die Merkmale auf, auch wenn der Nutzer sie nennt — stattdessen Hinweis in `hinweise`.
|
5. Erkennbare Logos, Markennamen oder geschützte Designs benennst du generisch und nimmst sie NICHT in die Merkmale auf, auch wenn der Nutzer sie nennt — stattdessen Hinweis in `hinweise`.
|
||||||
6. **Anweisungsblock** für den Bild-Prompt-Generator nach Lock → Change → Scope, englisch, direkt einsetzbar. Lock: was am bestehenden Asset (siehe {{ASSETS}}) unverändert bleibt. Change: die extrahierten Merkmale als Änderung. Scope: worauf sich die Änderung beschränkt. Muster: „Keep face, identity, skin tone, and body unchanged. Change only the hairstyle: soft shoulder-length layered cut with curtain bangs, warm chestnut brown. Edit hair only."
|
6. **Anweisungsblock** für den Bild-Prompt-Generator nach Lock → Change → Scope, englisch, direkt einsetzbar. Lock: was am bestehenden Asset (siehe {{ASSETS}}) unverändert bleibt. Change: die extrahierten Merkmale als Änderung. Scope: worauf sich die Änderung beschränkt. Muster: „Keep face, identity, skin tone, and body unchanged. Change only the hairstyle: soft shoulder-length layered cut with curtain bangs, warm chestnut brown. Edit hair only."
|
||||||
7. Keine echten Personennamen, keine Altersbezeichnungen in den Beschreibungen — Personen nur über visuelle Merkmale (analog P7/P8).
|
|
||||||
|
|
||||||
## Output (NUR dieses JSON)
|
## Output (NUR dieses JSON)
|
||||||
|
|
||||||
|
|||||||
@@ -43,23 +43,6 @@ aktiv: true
|
|||||||
`version` vergibt das Seed-Skript: neue Zeile bei Inhaltsänderung (version+1), alte
|
`version` vergibt das Seed-Skript: neue Zeile bei Inhaltsänderung (version+1), alte
|
||||||
Zeilen werden `aktiv: false`.
|
Zeilen werden `aktiv: false`.
|
||||||
|
|
||||||
## Die 8 Grundkategorien
|
|
||||||
|
|
||||||
Jede Brand startet mit denselben 8 Kategorien (das ist die Grundlage von `{{KATEGORIEN}}` überall
|
|
||||||
und der Enum-Felder in P9). Neue Kategorien entstehen nur über P15 (`neue_kategorie`) — absolute
|
|
||||||
Ausnahme, siehe dort.
|
|
||||||
|
|
||||||
| Slug | Inhalt |
|
|
||||||
|---|---|
|
|
||||||
| `location` | Drehort/Umgebung, innen/außen, Tageszeit |
|
|
||||||
| `licht` | Lichtstimmung, Lichtrichtung |
|
|
||||||
| `farben` | Farbwelt, dominante Farben, Farbstimmung |
|
|
||||||
| `kamera` | Shot-Typen, Kamerabewegung |
|
|
||||||
| `voice` | Sprache/Stimme, Sprecher-Typ |
|
|
||||||
| `geraeusche` | Musik, Sound-Effekte |
|
|
||||||
| `texte_hooks` | Eingeblendeter Text, Hooks, CTAs |
|
|
||||||
| `handlungen` | Aktionen/Handlungen im Bild |
|
|
||||||
|
|
||||||
## Slot-Registry
|
## Slot-Registry
|
||||||
|
|
||||||
| Slot | Inhalt (füllt der Code) | genutzt von |
|
| Slot | Inhalt (füllt der Code) | genutzt von |
|
||||||
@@ -91,20 +74,6 @@ Ausnahme, siehe dort.
|
|||||||
|
|
||||||
\* P2 erzeugt das Brand-Profil, nutzt aber `BRAND_PROFIL` nicht als Input.
|
\* P2 erzeugt das Brand-Profil, nutzt aber `BRAND_PROFIL` nicht als Input.
|
||||||
|
|
||||||
## Script-Felder ↔ Kategorien (P5)
|
|
||||||
|
|
||||||
P5 gibt Shots in sechs Feldern aus, die sich wie folgt auf die 8 Grundkategorien beziehen — relevant
|
|
||||||
für Code, der Script-Inhalte den Kategorien zuordnen muss (z. B. P6-Diff-Zuordnung):
|
|
||||||
|
|
||||||
| Script-Feld | Kategorie(n) |
|
|
||||||
|---|---|
|
|
||||||
| Bild | `location`, `farben` |
|
|
||||||
| Aktion | `handlungen` |
|
|
||||||
| Kamera | `kamera` |
|
|
||||||
| Licht | `licht` |
|
|
||||||
| Audio | `voice`, `geraeusche` |
|
|
||||||
| Text im Bild | `texte_hooks` |
|
|
||||||
|
|
||||||
## Pipeline-Überblick
|
## Pipeline-Überblick
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
1495
prototyp-app.html
BIN
uploads/pasted-1784643111353-0.jpg
Normal file
|
After Width: | Height: | Size: 142 KiB |
BIN
uploads/pasted-1784643165286-0.jpg
Normal file
|
After Width: | Height: | Size: 50 KiB |
BIN
uploads/pasted-1784663200459-0.jpg
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
uploads/pasted-1784663207742-0.jpg
Normal file
|
After Width: | Height: | Size: 120 KiB |
BIN
uploads/pasted-1784663261701-0.jpg
Normal file
|
After Width: | Height: | Size: 81 KiB |