Files
videogen/design/desktop-ansicht/Videogen Web.dc.html
JUSN 39e6aa20e7 Design-Handoff Desktop-Ansicht: Spezifikation + Referenz-Prototyp
Die Mobile-App laeuft im Browser bisher als schmale Handy-Spalte
(siehe design/desktop-ansicht/referenz/mobile-ansicht-im-browser.png).
Dieses Bundle beschreibt die Desktop-Adaption: dreispaltiges App-Layout
mit permanenter Sidebar, zentralem Kartenstapel und Detailspalte,
Swipe ersetzt durch Maus-Drag, Buttons und Tastatur.

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

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-27 08:56:54 +02:00

506 lines
36 KiB
HTML
Raw 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&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
<style>
html, body { margin:0; padding:0; background:#120b0d; }
* { box-sizing: border-box; }
a { color:#8fe3c1; text-decoration:none; }
a:hover { color:#b9f2da; }
::selection { background:rgba(126,226,184,.28); }
button:focus-visible, [tabindex]:focus-visible { outline:2px solid #7ee2b8; outline-offset:2px; }
@keyframes riseIn { from { opacity:0; transform:translateY(14px);} to { opacity:1; transform:none;} }
@keyframes fadeIn { from { opacity:0 } to { opacity:1 } }
@keyframes pulseRing { 0%{ box-shadow:0 0 0 0 rgba(126,226,184,.35)} 70%{ box-shadow:0 0 0 18px rgba(126,226,184,0)} 100%{ box-shadow:0 0 0 0 rgba(126,226,184,0)} }
</style>
</helmet>
<div style="min-height:100vh; width:100%; min-width:1180px; display:flex; font-family:'Manrope',system-ui,sans-serif; color:#f2efe9; background:radial-gradient(120% 80% at 50% 0%, #0d4c3d 0%, #0a3a30 34%, #24121a 68%, #40161b 100%); background-attachment:fixed;">
<aside style="width:236px; flex:none; display:flex; flex-direction:column; gap:28px; padding:26px 20px; border-right:1px solid rgba(255,255,255,.08); background:rgba(8,26,22,.42); backdrop-filter:blur(14px); position:sticky; top:0; height:100vh;">
<div style="display:flex; align-items:center; gap:10px;">
<div style="width:30px; height:30px; border-radius:9px; background:linear-gradient(150deg,#2fa87f,#0e5442); display:grid; place-items:center; font-size:15px; font-weight:700;">V</div>
<div style="font-size:16px; font-weight:600; letter-spacing:-.01em;">videogen</div>
</div>
<nav style="display:flex; flex-direction:column; gap:4px;">
<sc-for list="{{ navItems }}" as="item" hint-placeholder-count="4">
<button onClick="{{ item.go }}" style="{{ item.style }}" style-hover="background:rgba(255,255,255,.09); color:#ffffff;">
<span style="width:20px; display:grid; place-items:center; opacity:.9;">{{ item.glyph }}</span>
<span>{{ item.label }}</span>
</button>
</sc-for>
</nav>
<div style="margin-top:auto; display:flex; flex-direction:column; gap:14px;">
<div style="border:1px solid rgba(255,255,255,.12); border-radius:14px; padding:14px; background:rgba(255,255,255,.03);">
<div style="font-size:11px; letter-spacing:.14em; text-transform:uppercase; color:rgba(242,239,233,.5); margin-bottom:8px;">Heute</div>
<div style="font-family:'JetBrains Mono',monospace; font-size:22px; font-variant-numeric:tabular-nums;">{{ swipesToday }}</div>
<div style="font-size:12px; color:rgba(242,239,233,.55);">Karten gesichtet</div>
</div>
<div style="display:flex; align-items:center; gap:10px; padding:8px 6px;">
<div style="width:32px; height:32px; border-radius:50%; background:linear-gradient(140deg,#b2323c,#5d1620); flex:none;"></div>
<div style="min-width:0;">
<div style="font-size:13px; font-weight:600;">Jonas K.</div>
<div style="font-size:11px; color:rgba(242,239,233,.5);">Studio-Konto</div>
</div>
</div>
</div>
</aside>
<main style="flex:1; min-width:0; display:flex; flex-direction:column;">
<header style="height:72px; flex:none; display:flex; align-items:center; justify-content:space-between; padding:0 34px; border-bottom:1px solid rgba(255,255,255,.07);">
<div style="display:flex; gap:6px; align-items:center;">
<sc-for list="{{ tabs }}" as="t" hint-placeholder-count="3">
<button onClick="{{ t.go }}" style="{{ t.style }}" style-hover="color:#ffffff;">{{ t.label }}</button>
</sc-for>
</div>
<div style="display:flex; align-items:center; gap:12px;">
<div style="font-size:12px; color:rgba(242,239,233,.5); font-family:'JetBrains Mono',monospace; font-variant-numeric:tabular-nums;">{{ counter }}</div>
<button onClick="{{ toggleInfo }}" title="Kürzel anzeigen" style="width:36px; height:36px; border-radius:50%; border:1px solid rgba(255,255,255,.18); background:rgba(255,255,255,.04); color:#f2efe9; font-size:14px; cursor:pointer; transition:background .18s ease, transform .18s ease;" style-hover="background:rgba(255,255,255,.12); transform:translateY(-1px);">i</button>
</div>
</header>
<sc-if value="{{ isFeed }}" hint-placeholder-val="{{ true }}">
<section style="flex:1; min-height:0; overflow:auto; display:grid; grid-template-columns:minmax(0,1fr) 372px; gap:0;">
<div style="position:relative; display:flex; flex-direction:column; align-items:center; justify-content:center; padding:26px 20px 30px; gap:22px; min-width:0; min-height:min-content;">
<div style="position:relative; width:min(404px,100%); height:560px; flex:none;">
<sc-for list="{{ stack }}" as="c" hint-placeholder-count="3">
<div style="{{ c.shell }}" onPointerDown="{{ c.down }}" onPointerMove="{{ c.move }}" onPointerUp="{{ c.up }}" onPointerCancel="{{ c.up }}">
<div style="{{ c.media }}">
<div style="position:absolute; inset:0; background:{{ c.stripes }}; opacity:.5;"></div>
<div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,0,0,.32) 0%, rgba(0,0,0,0) 26%, rgba(0,0,0,0) 52%, rgba(0,0,0,.66) 100%);"></div>
<div style="position:relative; display:flex; gap:9px; padding:18px;">
<span style="padding:7px 15px; border-radius:999px; border:1px solid rgba(255,255,255,.34); background:rgba(0,0,0,.22); backdrop-filter:blur(6px); font-size:13px; font-weight:600;">{{ c.kind }}</span>
<span style="padding:7px 15px; border-radius:999px; border:1px solid rgba(255,255,255,.34); background:rgba(0,0,0,.22); backdrop-filter:blur(6px); font-size:13px; font-weight:600;">{{ c.assets }}</span>
</div>
<div style="position:absolute; inset:auto 0 118px 0; display:flex; justify-content:center; padding:0 22px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:15px; padding:9px 16px; border-radius:9px; background:rgba(0,0,0,.5); backdrop-filter:blur(4px); text-align:center; text-wrap:pretty;">{{ c.caption }}</div>
</div>
<div style="position:absolute; inset:auto 0 0 0; display:flex; align-items:center; gap:13px; padding:20px;">
<div style="width:46px; height:46px; border-radius:50%; flex:none; background:{{ c.avatar }}; border:2px solid rgba(255,255,255,.3);"></div>
<div style="min-width:0;">
<div style="font-size:19px; font-weight:700; letter-spacing:-.01em;">{{ c.name }}</div>
<div style="font-size:14px; color:rgba(255,255,255,.72);">{{ c.sub }}</div>
</div>
</div>
<div style="{{ c.badge }}">{{ c.badgeText }}</div>
</div>
</div>
</sc-for>
<sc-if value="{{ stackEmpty }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute; inset:0; border:1px dashed rgba(255,255,255,.2); border-radius:26px; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:16px; text-align:center; padding:40px; animation:fadeIn .3s ease;">
<div style="font-size:20px; font-weight:600;">Alles gesichtet</div>
<div style="font-size:14px; color:rgba(242,239,233,.6); max-width:230px; text-wrap:pretty;">Für heute sind keine neuen Karten mehr in dieser Ansicht.</div>
<button onClick="{{ reset }}" style="padding:11px 20px; border-radius:999px; border:1px solid rgba(126,226,184,.6); background:transparent; color:#8fe3c1; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer; transition:background .18s ease;" style-hover="background:rgba(126,226,184,.14);">Stapel neu laden</button>
</div>
</sc-if>
</div>
<div style="display:flex; align-items:center; gap:18px;">
<button onClick="{{ onNope }}" title="Ablehnen — Pfeil links" style="width:62px; height:62px; border-radius:50%; border:1px solid rgba(255,255,255,.16); background:rgba(14,10,12,.72); color:#f2efe9; font-size:22px; cursor:pointer; display:grid; place-items:center; transition:transform .18s ease, border-color .18s ease, background .18s ease;" style-hover="transform:translateY(-3px) scale(1.05); border-color:rgba(230,120,120,.75); background:rgba(120,32,38,.5);" style-active="transform:scale(.95);"></button>
<button onClick="{{ onUndo }}" title="Zurück — Pfeil hoch" style="width:52px; height:52px; border-radius:50%; border:1px solid rgba(255,255,255,.16); background:rgba(14,10,12,.72); color:#f2efe9; font-size:17px; cursor:pointer; display:grid; place-items:center; transition:transform .18s ease, border-color .18s ease;" style-hover="transform:translateY(-3px) scale(1.05); border-color:rgba(255,255,255,.4);" style-active="transform:scale(.95);"></button>
<button onClick="{{ onLike }}" title="Merken — Pfeil rechts" style="width:70px; height:70px; border-radius:50%; border:1px solid rgba(126,226,184,.45); background:rgba(14,10,12,.72); color:#8fe3c1; font-size:26px; cursor:pointer; display:grid; place-items:center; transition:transform .18s ease, background .18s ease;" style-hover="transform:translateY(-3px) scale(1.06); background:rgba(20,88,68,.6);" style-active="transform:scale(.95);"></button>
</div>
<sc-if value="{{ showInfo }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute; right:26px; top:26px; width:250px; border:1px solid rgba(255,255,255,.14); border-radius:14px; background:rgba(10,26,22,.9); backdrop-filter:blur(12px); padding:16px; font-size:13px; line-height:1.8; animation:riseIn .22s ease;">
<div style="font-weight:700; margin-bottom:8px;">Bedienung</div>
<div style="display:grid; grid-template-columns:auto 1fr; gap:6px 12px; color:rgba(242,239,233,.72);">
<span style="font-family:'JetBrains Mono',monospace;"></span><span>Ablehnen</span>
<span style="font-family:'JetBrains Mono',monospace;"></span><span>Merken</span>
<span style="font-family:'JetBrains Mono',monospace;"></span><span>Zurücknehmen</span>
<span style="font-family:'JetBrains Mono',monospace;">Maus</span><span>Karte ziehen</span>
</div>
</div>
</sc-if>
</div>
<aside style="border-left:1px solid rgba(255,255,255,.07); background:rgba(8,22,19,.34); padding:30px 28px; display:flex; flex-direction:column; gap:24px; overflow:auto;">
<div>
<div style="font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:rgba(242,239,233,.45);">Aktuelle Karte</div>
<div style="font-size:26px; font-weight:600; letter-spacing:-.02em; margin-top:10px; text-wrap:pretty;">{{ detailTitle }}</div>
<div style="font-size:14px; color:rgba(242,239,233,.62); margin-top:6px;">{{ detailSub }}</div>
</div>
<div style="height:1px; background:rgba(255,255,255,.09);"></div>
<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:12px;">
<sc-for list="{{ detailStats }}" as="s" hint-placeholder-count="3">
<div style="border:1px solid rgba(255,255,255,.1); border-radius:12px; padding:12px;">
<div style="font-family:'JetBrains Mono',monospace; font-size:17px; font-variant-numeric:tabular-nums;">{{ s.value }}</div>
<div style="font-size:11px; color:rgba(242,239,233,.5); margin-top:3px;">{{ s.label }}</div>
</div>
</sc-for>
</div>
<div>
<div style="font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:rgba(242,239,233,.45); margin-bottom:10px;">Beschreibung</div>
<p style="margin:0; font-size:14px; line-height:1.75; color:rgba(242,239,233,.78); text-wrap:pretty;">{{ detailBody }}</p>
</div>
<div>
<div style="font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:rgba(242,239,233,.45); margin-bottom:10px;">Tags</div>
<div style="display:flex; flex-wrap:wrap; gap:8px;">
<sc-for list="{{ detailTags }}" as="tag" hint-placeholder-count="4">
<span style="padding:6px 12px; border-radius:999px; border:1px solid rgba(255,255,255,.18); font-size:12px; color:rgba(242,239,233,.8);">{{ tag }}</span>
</sc-for>
</div>
</div>
<div style="margin-top:auto; display:flex; flex-direction:column; gap:10px;">
<button onClick="{{ onLike }}" style="padding:13px; border-radius:12px; border:1px solid rgba(126,226,184,.55); background:transparent; color:#8fe3c1; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer; transition:background .18s ease;" style-hover="background:rgba(126,226,184,.14);">Zur Sammlung hinzufügen</button>
<button onClick="{{ onNope }}" style="padding:13px; border-radius:12px; border:1px solid rgba(255,255,255,.16); background:transparent; color:rgba(242,239,233,.8); font-family:inherit; font-size:14px; cursor:pointer; transition:background .18s ease;" style-hover="background:rgba(255,255,255,.07);">Überspringen</button>
</div>
</aside>
</section>
</sc-if>
<sc-if value="{{ isProfile }}" hint-placeholder-val="{{ false }}">
<section style="flex:1; padding:38px 44px; overflow:auto; animation:riseIn .26s ease;">
<div style="display:flex; align-items:flex-end; gap:22px; padding-bottom:26px; border-bottom:1px solid rgba(255,255,255,.09);">
<div style="width:96px; height:96px; border-radius:50%; background:linear-gradient(140deg,#b2323c,#5d1620); flex:none; border:2px solid rgba(255,255,255,.2);"></div>
<div style="flex:1;">
<div style="font-size:32px; font-weight:600; letter-spacing:-.02em;">Jonas K.</div>
<div style="font-size:14px; color:rgba(242,239,233,.6); margin-top:4px;">Studio-Konto · seit März 2026</div>
</div>
<div style="display:flex; gap:34px; padding-bottom:6px;">
<sc-for list="{{ profileStats }}" as="s" hint-placeholder-count="3">
<div>
<div style="font-family:'JetBrains Mono',monospace; font-size:22px; font-variant-numeric:tabular-nums;">{{ s.value }}</div>
<div style="font-size:12px; color:rgba(242,239,233,.5);">{{ s.label }}</div>
</div>
</sc-for>
</div>
</div>
<div style="display:grid; grid-template-columns:repeat(4,1fr); gap:18px; margin-top:28px;">
<sc-for list="{{ gallery }}" as="g" hint-placeholder-count="8">
<div style="{{ g.tile }}" style-hover="transform:translateY(-4px); border-color:rgba(126,226,184,.5);">
<div style="position:absolute; inset:0; background:{{ g.stripes }}; opacity:.45;"></div>
<div style="position:absolute; inset:auto 0 0 0; padding:14px; background:linear-gradient(0deg, rgba(0,0,0,.7), transparent); font-size:13px; font-weight:600;">{{ g.title }}</div>
</div>
</sc-for>
</div>
</section>
</sc-if>
<sc-if value="{{ isCreate }}" hint-placeholder-val="{{ false }}">
<section style="flex:1; overflow:auto; padding:38px 44px; animation:riseIn .26s ease;">
<div style="max-width:920px; display:grid; grid-template-columns:1fr 320px; gap:34px;">
<div style="display:flex; flex-direction:column; gap:22px;">
<div>
<div style="font-size:30px; font-weight:600; letter-spacing:-.02em;">Neuen Post erstellen</div>
<div style="font-size:14px; color:rgba(242,239,233,.6); margin-top:6px;">Material hochladen, beschriften, in den Feed geben.</div>
</div>
<div style="border:1px dashed rgba(255,255,255,.24); border-radius:18px; height:220px; display:grid; place-items:center; text-align:center; gap:8px; background:rgba(255,255,255,.03); cursor:pointer; transition:border-color .18s ease, background .18s ease;" style-hover="border-color:rgba(126,226,184,.6); background:rgba(126,226,184,.06);">
<div>
<div style="font-size:15px; font-weight:600;">Dateien hierher ziehen</div>
<div style="font-size:13px; color:rgba(242,239,233,.55); margin-top:6px;">MP4, MOV oder bis zu 10 Bilder</div>
</div>
</div>
<div style="display:flex; flex-direction:column; gap:16px;">
<label style="display:flex; flex-direction:column; gap:8px; font-size:12px; letter-spacing:.14em; text-transform:uppercase; color:rgba(242,239,233,.5);">Titel
<input value="{{ draftTitle }}" onChange="{{ setTitle }}" style="padding:13px 15px; border-radius:11px; border:1px solid rgba(255,255,255,.16); background:rgba(0,0,0,.24); color:#f2efe9; font-family:inherit; font-size:15px; letter-spacing:normal; text-transform:none;">
</label>
<label style="display:flex; flex-direction:column; gap:8px; font-size:12px; letter-spacing:.14em; text-transform:uppercase; color:rgba(242,239,233,.5);">Untertitel
<input value="{{ draftSub }}" onChange="{{ setSub }}" style="padding:13px 15px; border-radius:11px; border:1px solid rgba(255,255,255,.16); background:rgba(0,0,0,.24); color:#f2efe9; font-family:inherit; font-size:15px; letter-spacing:normal; text-transform:none;">
</label>
<div style="display:flex; flex-direction:column; gap:10px; font-size:12px; letter-spacing:.14em; text-transform:uppercase; color:rgba(242,239,233,.5);">Art
<div style="display:flex; gap:8px;">
<sc-for list="{{ kindOptions }}" as="k" hint-placeholder-count="3">
<button onClick="{{ k.go }}" style="{{ k.style }}" style-hover="border-color:rgba(126,226,184,.6);">{{ k.label }}</button>
</sc-for>
</div>
</div>
</div>
<div style="display:flex; gap:12px;">
<button onClick="{{ publish }}" style="padding:13px 24px; border-radius:12px; border:1px solid rgba(126,226,184,.6); background:transparent; color:#8fe3c1; font-family:inherit; font-size:14px; font-weight:600; cursor:pointer; transition:background .18s ease;" style-hover="background:rgba(126,226,184,.14);">Veröffentlichen</button>
<button style="padding:13px 24px; border-radius:12px; border:1px solid rgba(255,255,255,.16); background:transparent; color:rgba(242,239,233,.8); font-family:inherit; font-size:14px; cursor:pointer; transition:background .18s ease;" style-hover="background:rgba(255,255,255,.07);">Als Entwurf sichern</button>
</div>
</div>
<div>
<div style="font-size:11px; letter-spacing:.16em; text-transform:uppercase; color:rgba(242,239,233,.45); margin-bottom:12px;">Vorschau</div>
<div style="position:relative; height:420px; border-radius:22px; overflow:hidden; border:1px solid rgba(255,255,255,.12); background:linear-gradient(155deg,#1f8163 0%,#0d4034 100%);">
<div style="position:absolute; inset:0; background:repeating-linear-gradient(48deg, rgba(255,255,255,.07) 0 2px, transparent 2px 15px); opacity:.5;"></div>
<div style="position:absolute; inset:auto 0 0 0; padding:18px; background:linear-gradient(0deg, rgba(0,0,0,.72), transparent);">
<div style="font-size:18px; font-weight:700;">{{ draftTitle }}</div>
<div style="font-size:13px; color:rgba(255,255,255,.7);">{{ draftSub }}</div>
</div>
</div>
</div>
</div>
</section>
</sc-if>
<sc-if value="{{ isSettings }}" hint-placeholder-val="{{ false }}">
<section style="flex:1; overflow:auto; padding:38px 44px; animation:riseIn .26s ease;">
<div style="max-width:660px;">
<div style="font-size:30px; font-weight:600; letter-spacing:-.02em;">Einstellungen</div>
<div style="margin-top:26px; border:1px solid rgba(255,255,255,.1); border-radius:16px; overflow:hidden;">
<sc-for list="{{ settings }}" as="s" hint-placeholder-count="4">
<div style="display:flex; align-items:center; justify-content:space-between; gap:20px; padding:18px 20px; border-bottom:1px solid rgba(255,255,255,.07); background:rgba(255,255,255,.02);">
<div>
<div style="font-size:15px; font-weight:600;">{{ s.label }}</div>
<div style="font-size:13px; color:rgba(242,239,233,.55); 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="{{ toast }}" hint-placeholder-val="{{ false }}">
<div style="position:fixed; left:50%; bottom:34px; transform:translateX(-50%); padding:12px 22px; border-radius:999px; border:1px solid rgba(255,255,255,.16); background:rgba(10,24,20,.92); backdrop-filter:blur(10px); font-size:14px; animation:riseIn .2s ease; z-index:40;">{{ toast }}</div>
</sc-if>
</main>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;stackDepth&quot;:{&quot;editor&quot;:&quot;int&quot;,&quot;default&quot;:3,&quot;min&quot;:1,&quot;max&quot;:5,&quot;tsType&quot;:&quot;number&quot;,&quot;section&quot;:&quot;Feed&quot;}}">
const DATA = {
posts: [
{ name:'Mara S.', sub:'Modell · Glow Serum v3', kind:'Produkt', assets:'3 Bilder', caption:'Toner im Bogenfenster', hue:[ '#1f8163', '#0d4034' ], avatar:'linear-gradient(140deg,#b2323c,#5d1620)', body:'Drei Aufnahmen im Gegenlicht, aufgenommen im Studio Nord. Freigabe für Paid Social liegt vor.', tags:['Beauty','Studio','Hochformat','Freigegeben'], stats:[['3','Assets'],['12s','Länge'],['94','Score']] },
{ name:'Tobias R.', sub:'Creator · Kaltbrüh-Serie', kind:'Video', assets:'1 Clip', caption:'Erste Kanne am Morgen', hue:[ '#256f8b', '#0c2f3f' ], avatar:'linear-gradient(140deg,#2f7f9e,#10394a)', body:'Vertikaler Clip mit Naturton, geschnitten auf zwölf Sekunden. Untertitel bereits eingebrannt.', tags:['Food','Handheld','Untertitel'], stats:[['1','Asset'],['12s','Länge'],['88','Score']] },
{ name:'Leonie B.', sub:'Modell · Leinen SS26', kind:'Produkt', assets:'5 Bilder', caption:'Leinen gegen die Mittagssonne', hue:[ '#8a6a2f', '#3a2a11' ], avatar:'linear-gradient(140deg,#c99a4a,#6a4a17)', body:'Fünf Stills aus der Sommerstrecke, alle im gleichen Weißabgleich. Retusche steht noch aus.', tags:['Fashion','Außen','Serie'], stats:[['5','Assets'],['—','Länge'],['76','Score']] },
{ name:'Nils P.', sub:'Creator · Werkstattnotiz', kind:'Video', assets:'2 Clips', caption:'Hobel, zweiter Durchgang', hue:[ '#7a2f38', '#2c1014' ], avatar:'linear-gradient(140deg,#a63a44,#4d151c)', body:'Zwei Sequenzen aus der Werkstatt, Ton direkt aus der Kamera. Eignet sich als Hintergrundmaterial.', tags:['Handwerk','Ton','Roh'], stats:[['2','Assets'],['26s','Länge'],['81','Score']] },
{ name:'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']] }
],
videos: [
{ name:'Studio Nord', sub:'Reel · Behind the scenes', kind:'Video', assets:'1 Clip', caption:'Aufbau um sechs Uhr', hue:[ '#1c6f7f', '#0a2c33' ], avatar:'linear-gradient(140deg,#2f8fa0,#0d3b44)', body:'Ein durchgehender Take vom Aufbau bis zum ersten Licht.', tags:['BTS','Longtake'], stats:[['1','Asset'],['48s','Länge'],['83','Score']] },
{ name:'Yara M.', sub:'Creator · Tanzstudie', kind:'Video', assets:'3 Clips', caption:'Drehung, dritter Versuch', hue:[ '#6a2f7a', '#26102c' ], avatar:'linear-gradient(140deg,#8b46a0,#341340)', body:'Drei Varianten derselben Bewegung, Slow Motion in der Mitte.', tags:['Bewegung','Slowmo'], stats:[['3','Assets'],['31s','Länge'],['87','Score']] },
{ name:'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']] }
],
following: [
{ name:'Mara S.', sub:'Folge ich · neuer Post', kind:'Produkt', assets:'2 Bilder', caption:'Nachschub aus dem Studio', hue:[ '#1f8163', '#0d4034' ], avatar:'linear-gradient(140deg,#b2323c,#5d1620)', body:'Zwei zusätzliche Aufnahmen aus derselben Strecke.', tags:['Beauty','Nachtrag'], stats:[['2','Assets'],['—','Länge'],['92','Score']] },
{ name:'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']] }
]
};
class Component extends DCLogic {
state = {
screen: 'feed', tab: 'posts', idx: 0,
drag: null, exiting: null, showInfo: false, toast: '',
seen: 0, saved: 24, history: [],
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 (this.state.screen !== 'feed') return;
if (e.key === 'ArrowLeft') { e.preventDefault(); this.commit('left'); }
if (e.key === 'ArrowRight') { e.preventDefault(); this.commit('right'); }
if (e.key === 'ArrowUp') { e.preventDefault(); this.undo(); }
};
window.addEventListener('keydown', this._key);
}
componentWillUnmount() { window.removeEventListener('keydown', this._key); if (this._t) clearTimeout(this._t); }
deck() { const t = this.state.tab; return t === 'videos' ? DATA.videos : t === 'following' ? DATA.following : DATA.posts; }
flash(msg) { this.setState({ toast: msg }); clearTimeout(this._toast); this._toast = setTimeout(() => this.setState({ toast: '' }), 1700); }
commit(dir) {
if (this.state.exiting) return;
if (this.state.idx >= this.deck().length) return;
const card = this.deck()[this.state.idx];
this.setState({ exiting: dir, drag: null });
this._t = setTimeout(() => this.setState(s => ({
exiting: null, idx: s.idx + 1, drag: null,
seen: s.seen + 1, saved: dir === 'right' ? s.saved + 1 : s.saved,
history: s.history.concat([{ idx: s.idx, dir }])
})), 320);
this.flash(dir === 'right' ? 'Zu „' + card.name + '" gemerkt' : 'Übersprungen');
}
undo() {
const h = this.state.history;
if (!h.length) { this.flash('Nichts zurückzunehmen'); return; }
const last = h[h.length - 1];
this.setState(s => ({ idx: last.idx, history: s.history.slice(0, -1), saved: last.dir === 'right' ? Math.max(0, s.saved - 1) : s.saved, seen: Math.max(0, s.seen - 1), exiting: null, drag: null }));
}
onDown = (e) => {
if (this.state.exiting) return;
e.currentTarget.setPointerCapture?.(e.pointerId);
this._start = { x: e.clientX, y: e.clientY };
this.setState({ drag: { x: 0, y: 0, active: true } });
};
onMove = (e) => {
if (!this.state.drag || !this.state.drag.active) return;
this.setState({ drag: { x: e.clientX - this._start.x, y: e.clientY - this._start.y, active: true } });
};
onUp = () => {
const d = this.state.drag;
if (!d) return;
if (d.x > 120) this.commit('right');
else if (d.x < -120) this.commit('left');
else this.setState({ drag: null });
};
stripes(i) { return 'repeating-linear-gradient(' + (40 + i * 9) + 'deg, rgba(255,255,255,.08) 0 2px, transparent 2px 16px)'; }
renderVals() {
const s = this.state, deck = this.deck(), depth = Math.max(1, this.props.stackDepth ?? 3);
const current = deck[s.idx];
const dx = s.drag ? s.drag.x : 0, dy = s.drag ? s.drag.y : 0;
const rot = dx / 22;
const stack = [];
for (let k = 0; k < depth; k++) {
const card = deck[s.idx + k];
if (!card) break;
const top = k === 0;
let transform = 'translate3d(0,' + (k * -12) + 'px,0) scale(' + (1 - k * 0.045) + ')';
let transition = 'transform .34s cubic-bezier(.22,.9,.3,1), opacity .3s ease';
let opacity = 1;
if (top) {
if (s.exiting) {
transform = 'translate3d(' + (s.exiting === 'right' ? 620 : -620) + 'px, -40px, 0) rotate(' + (s.exiting === 'right' ? 22 : -22) + 'deg)';
opacity = 0;
} else if (s.drag) {
transform = 'translate3d(' + dx + 'px,' + dy + 'px,0) rotate(' + rot + 'deg)';
transition = s.drag.active ? 'none' : transition;
}
}
const shell = {
position: 'absolute', inset: 0, zIndex: 10 - k, opacity, transform, transition,
borderRadius: '26px', overflow: 'hidden', boxShadow: '0 26px 60px rgba(0,0,0,.45)',
border: '1px solid rgba(255,255,255,.14)', touchAction: 'none',
cursor: top ? (s.drag && s.drag.active ? 'grabbing' : 'grab') : 'default',
pointerEvents: top ? 'auto' : 'none', userSelect: 'none'
};
const showBadge = top && (Math.abs(dx) > 40 || !!s.exiting);
const liked = (s.exiting === 'right') || dx > 40;
stack.push({
shell: { ...shell },
down: top ? this.onDown : undefined,
move: top ? this.onMove : undefined,
up: top ? this.onUp : undefined,
media: { position: 'absolute', inset: 0, background: 'linear-gradient(155deg,' + card.hue[0] + ' 0%,' + card.hue[1] + ' 100%)' },
stripes: this.stripes(s.idx + k),
kind: card.kind, assets: card.assets, caption: card.caption, avatar: card.avatar,
name: card.name, sub: card.sub,
badgeText: liked ? 'GEMERKT' : 'SKIP',
badge: {
position: 'absolute', top: '86px', left: liked ? '24px' : 'auto', right: liked ? 'auto' : '24px',
padding: '8px 16px', borderRadius: '10px', fontWeight: 800, fontSize: '15px', letterSpacing: '.12em',
border: '2px solid ' + (liked ? 'rgba(126,226,184,.9)' : 'rgba(232,120,120,.9)'),
color: liked ? '#a9f0d0' : '#f0a8a8',
transform: 'rotate(' + (liked ? -10 : 10) + 'deg)',
opacity: showBadge ? Math.min(1, Math.abs(dx) / 110 || 1) : 0,
transition: 'opacity .16s ease', pointerEvents: 'none'
}
});
}
const navDef = [['feed','Feed','◧'],['profile','Profil','◍'],['create','Erstellen',''],['settings','Einstellungen','⚙']];
const navItems = navDef.map(([id, label, glyph]) => ({
label, glyph, go: () => this.setState({ screen: id }),
style: {
display: 'flex', alignItems: 'center', gap: '11px', padding: '11px 13px', borderRadius: '11px',
border: '1px solid ' + (s.screen === id ? 'rgba(126,226,184,.4)' : 'transparent'),
background: s.screen === id ? 'rgba(126,226,184,.12)' : 'transparent',
color: s.screen === id ? '#d6fbec' : 'rgba(242,239,233,.7)',
font: '500 14px/1 Manrope, system-ui, sans-serif', cursor: 'pointer',
transition: 'background .18s ease, color .18s ease', textAlign: 'left'
}
}));
const tabDef = [['videos','Videos'],['following','Folge ich'],['posts','Posts']];
const tabs = tabDef.map(([id, label]) => ({
label, go: () => this.setState({ tab: id, idx: 0, history: [], drag: null, exiting: null, screen: 'feed' }),
style: {
padding: '9px 18px', borderRadius: '999px', border: '1px solid ' + (s.tab === id ? 'rgba(255,255,255,.22)' : 'transparent'),
background: s.tab === id ? 'rgba(255,255,255,.08)' : 'transparent',
color: s.tab === id ? '#ffffff' : 'rgba(242,239,233,.58)',
font: (s.tab === id ? '600' : '500') + ' 16px/1 Manrope, system-ui, sans-serif',
cursor: 'pointer', transition: 'background .18s ease, color .18s ease'
}
}));
const kindOptions = ['Produkt','Video','Serie'].map(k => ({
label: k, go: () => this.setState({ draftKind: k }),
style: {
padding: '10px 18px', borderRadius: '999px', cursor: 'pointer',
border: '1px solid ' + (s.draftKind === k ? 'rgba(126,226,184,.6)' : 'rgba(255,255,255,.16)'),
background: s.draftKind === k ? 'rgba(126,226,184,.12)' : 'transparent',
color: s.draftKind === k ? '#a9f0d0' : 'rgba(242,239,233,.75)',
font: '500 14px/1 Manrope, system-ui, sans-serif', letterSpacing: 'normal', textTransform: 'none',
transition: 'border-color .18s ease, background .18s ease'
}
}));
const setDef = [['autoplay','Videos automatisch abspielen','Clips starten stumm, sobald die Karte oben liegt.'],
['sound','Ton beim Öffnen','Beim Anklicken einer Karte den Ton aktivieren.'],
['digest','Wochenrückblick','Jeden Montag eine Zusammenfassung der neuen Posts.'],
['motion','Bewegte Übergänge','Karten 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: '1px solid ' + (on ? 'rgba(126,226,184,.6)' : 'rgba(255,255,255,.2)'),
background: on ? 'rgba(126,226,184,.18)' : 'rgba(255,255,255,.05)', transition: 'background .2s ease, border-color .2s ease' },
knob: { position: 'absolute', top: '3px', left: on ? '25px' : '3px', width: '22px', height: '22px', borderRadius: '50%',
background: on ? '#8fe3c1' : 'rgba(242,239,233,.55)', transition: 'left .2s cubic-bezier(.22,.9,.3,1), background .2s ease' }
};
});
const gallery = DATA.posts.concat(DATA.videos).map((c, i) => ({
title: c.caption, stripes: this.stripes(i),
tile: { position: 'relative', height: '230px', borderRadius: '16px', overflow: 'hidden', cursor: 'pointer',
border: '1px solid rgba(255,255,255,.12)', transition: 'transform .2s ease, border-color .2s ease',
background: 'linear-gradient(155deg,' + c.hue[0] + ' 0%,' + c.hue[1] + ' 100%)' }
}));
return {
navItems, tabs, stack, kindOptions, settings, gallery,
isFeed: s.screen === 'feed', isProfile: s.screen === 'profile', isCreate: s.screen === 'create', isSettings: s.screen === 'settings',
stackEmpty: stack.length === 0,
showInfo: s.showInfo, toggleInfo: () => this.setState(st => ({ showInfo: !st.showInfo })),
counter: Math.min(s.idx + 1, deck.length) + ' / ' + deck.length,
swipesToday: String(s.seen).padStart(2, '0'),
onLike: () => this.commit('right'), onNope: () => this.commit('left'), onUndo: () => this.undo(),
reset: () => this.setState({ idx: 0, history: [] }),
detailTitle: current ? current.caption : 'Keine Karte',
detailSub: current ? current.name + ' · ' + current.sub : 'Stapel leer',
detailBody: current ? current.body : 'Lade den Stapel neu, um weitere Karten zu sichten.',
detailTags: current ? current.tags : [],
detailStats: current ? current.stats.map(([value, label]) => ({ value, label })) : [],
profileStats: [{ value: '38', label: 'Posts' }, { value: String(s.saved), 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
};
}
}
</script>
</body>
</html>