Files
videogen/design/desktop-ansicht/Videogen Web.dc.html
JUSN 4c47090f42 Feed wird Raster statt Kartenstapel, Swipen entfaellt
Rueckmeldung vom Kunden: der Screenshot war als Funktionsaenderung
gemeint, nicht nur als Optik. Statt eine Karte nach der anderen
durchzuschieben, liegen jetzt alle nebeneinander und werden einzeln
gemerkt, ausgeblendet oder geoeffnet.

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

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

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

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-05 23:38:14 +02:00

548 lines
40 KiB
HTML
Raw Permalink Blame History

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