Files
videogen/design/desktop-ansicht/Videogen Web.dc.html
JUSN 347fe7008a Reskin auf Pinterest-Desktop-Optik, Funktionen unveraendert
Vorlage ist die Pinterest-Web-App. Geaendert wurde ausschliesslich die
Gestaltung: state, Tastaturkuerzel, deck/flash/commit/undo, die
Drag-Handler und die Beispieldaten sind Byte-fuer-Byte identisch.

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

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

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

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

521 lines
35 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&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 } }
.pin-save { opacity:0; transition:opacity .12s ease; }
.pin:hover .pin-save, .pin:focus-within .pin-save { opacity:1; }
.pin-scrim { opacity:0; transition:opacity .12s ease; }
.pin:hover .pin-scrim { opacity:1; }
.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:14px; padding-bottom:10px;">
<div style="font-size:13px; color:#767676;"><span style="font-weight:700; color:#111111; font-variant-numeric:tabular-nums;">{{ swipesToday }}</span> heute gesichtet</div>
<div style="font-size:13px; color:#767676; font-variant-numeric:tabular-nums;">{{ counter }}</div>
<button onClick="{{ toggleInfo }}" title="Kürzel anzeigen" style="width:36px; height:36px; border:none; border-radius:50%; background:transparent; color:#111111; font-size:14px; font-weight:700; cursor:pointer; transition:background .12s ease;" style-hover="background:#e9e9e9;">i</button>
</div>
</div>
<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 class="pin" 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 class="pin-scrim" style="position:absolute; inset:0; background:rgba(0,0,0,.24);"></div>
<div style="position:absolute; inset:0; background:linear-gradient(180deg, rgba(0,0,0,0) 46%, rgba(0,0,0,.62) 100%);"></div>
<div style="position:relative; display:flex; align-items:flex-start; justify-content:space-between; gap:9px; padding:16px;">
<div style="display:flex; gap:8px;">
<span style="padding:6px 12px; border-radius:16px; background:rgba(255,255,255,.94); color:#111111; font-size:13px; font-weight:600;">{{ c.kind }}</span>
<span style="padding:6px 12px; border-radius:16px; background:rgba(255,255,255,.94); color:#111111; font-size:13px; font-weight:600;">{{ c.assets }}</span>
</div>
<button class="pin-save" onClick="{{ onLike }}" style="{{ c.save }}" style-hover="background:#ad081b;">Merken</button>
</div>
<div style="position:absolute; inset:auto 0 104px 0; display:flex; justify-content:center; padding:0 22px;">
<div style="font-size:15px; font-weight:600; padding:8px 14px; border-radius:16px; background:rgba(255,255,255,.94); color:#111111; 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:12px; padding:18px; color:#ffffff;">
<div style="width:44px; height:44px; border-radius:50%; flex:none; background:{{ c.avatar }};"></div>
<div style="min-width:0;">
<div style="font-size:18px; font-weight:700; letter-spacing:-.01em;">{{ c.name }}</div>
<div style="font-size:14px; color:rgba(255,255,255,.82);">{{ 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-radius:16px; background:#f5f5f5; display:flex; flex-direction:column; align-items:center; justify-content:center; gap:14px; text-align:center; padding:40px; animation:fadeIn .3s ease;">
<div style="font-size:20px; font-weight:700;">Alles gesichtet</div>
<div style="font-size:15px; color:#5f5f5f; max-width:240px; text-wrap:pretty;">Für heute sind keine neuen Karten mehr in dieser Ansicht.</div>
<button onClick="{{ reset }}" 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;">Stapel neu laden</button>
</div>
</sc-if>
</div>
<div style="display:flex; align-items:center; gap:16px;">
<button onClick="{{ onNope }}" title="Ablehnen — Pfeil links" style="width:56px; height:56px; border:none; border-radius:50%; background:#efefef; color:#111111; font-size:20px; cursor:pointer; display:grid; place-items:center; transition:background .12s ease, transform .12s ease;" style-hover="background:#e2e2e2; transform:scale(1.04);" style-active="transform:scale(.96);"></button>
<button onClick="{{ onUndo }}" title="Zurück — Pfeil hoch" style="width:48px; height:48px; border:none; border-radius:50%; background:#efefef; color:#111111; font-size:17px; cursor:pointer; display:grid; place-items:center; transition:background .12s ease, transform .12s ease;" style-hover="background:#e2e2e2; transform:scale(1.04);" style-active="transform:scale(.96);"></button>
<button onClick="{{ onLike }}" title="Merken — Pfeil rechts" style="height:56px; padding:0 30px; border:none; border-radius:28px; background:#e60023; color:#ffffff; font-family:inherit; font-size:17px; font-weight:700; cursor:pointer; display:grid; place-items:center; transition:background .12s ease, transform .12s ease;" style-hover="background:#ad081b; transform:scale(1.04);" style-active="transform:scale(.96);">Merken</button>
</div>
<sc-if value="{{ showInfo }}" hint-placeholder-val="{{ false }}">
<div style="position:absolute; right:26px; top:26px; width:250px; border-radius:16px; background:#ffffff; box-shadow:0 1px 20px rgba(0,0,0,.16); padding:16px; font-size:14px; line-height:1.7; 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:#5f5f5f;">
<span style="font-weight:600; color:#111111;"></span><span>Ablehnen</span>
<span style="font-weight:600; color:#111111;"></span><span>Merken</span>
<span style="font-weight:600; color:#111111;"></span><span>Zurücknehmen</span>
<span style="font-weight:600; color:#111111;">Maus</span><span>Karte ziehen</span>
</div>
</div>
</sc-if>
</div>
<aside style="border-left:1px solid #efefef; background:#ffffff; padding:28px; display:flex; flex-direction:column; gap:22px; overflow:auto;">
<div>
<div style="font-size:12px; font-weight:600; color:#767676;">Aktuelle Karte</div>
<div style="font-size:28px; font-weight:700; letter-spacing:-.02em; margin-top:8px; text-wrap:pretty;">{{ detailTitle }}</div>
<div style="font-size:15px; color:#5f5f5f; margin-top:6px;">{{ detailSub }}</div>
</div>
<div style="height:1px; background:#efefef;"></div>
<div style="display:grid; grid-template-columns:repeat(3,1fr); gap:10px;">
<sc-for list="{{ detailStats }}" as="s" hint-placeholder-count="3">
<div style="border-radius:16px; background:#f5f5f5; 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>
<div style="font-size:12px; font-weight:600; color:#767676; margin-bottom:8px;">Beschreibung</div>
<p style="margin:0; font-size:15px; line-height:1.7; color:#333333; text-wrap:pretty;">{{ detailBody }}</p>
</div>
<div>
<div style="font-size:12px; font-weight:600; color:#767676; 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:8px 14px; border-radius:18px; background:#efefef; font-size:13px; font-weight:600; color:#111111;">{{ tag }}</span>
</sc-for>
</div>
</div>
<div style="margin-top:auto; display:flex; flex-direction:column; gap:8px;">
<button onClick="{{ onLike }}" style="padding:14px; 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="{{ onNope }}" style="padding:14px; 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;">Überspringen</button>
</div>
</aside>
</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="columns:5; column-gap:16px;">
<sc-for list="{{ gallery }}" as="g" hint-placeholder-count="8">
<div class="pin" style="{{ g.tile }}">
<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; right:12px; top:12px;">
<button class="pin-save" onClick="{{ onLike }}" style="padding:11px 16px; border:none; border-radius:22px; background:#e60023; color:#ffffff; font-family:inherit; font-size:15px; font-weight:700; cursor:pointer;" style-hover="background:#ad081b;">Merken</button>
</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.title }}</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="{{ toast }}" hint-placeholder-val="{{ false }}">
<div style="position:fixed; left:50%; bottom:28px; transform:translateX(-50%); padding:16px 22px; border-radius:16px; background:#ffffff; box-shadow:0 1px 20px rgba(0,0,0,.22); color:#111111; font-size:15px; font-weight:600; 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: '16px', overflow: 'hidden', boxShadow: '0 1px 20px rgba(0,0,0,.18)',
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,
save: {
display: top ? 'block' : 'none', padding: '11px 16px', border: 'none', borderRadius: '22px',
background: '#e60023', color: '#ffffff', font: '700 15px/1 Manrope, system-ui, sans-serif',
cursor: 'pointer', transition: 'background .12s ease'
},
badgeText: liked ? 'GEMERKT' : 'SKIP',
badge: {
position: 'absolute', top: '86px', left: liked ? '24px' : 'auto', right: liked ? 'auto' : '24px',
padding: '10px 18px', borderRadius: '22px', fontWeight: 700, fontSize: '15px', letterSpacing: '.08em',
background: liked ? '#e60023' : '#111111', color: '#ffffff',
transform: 'rotate(' + (liked ? -10 : 10) + 'deg)',
opacity: showBadge ? Math.min(1, Math.abs(dx) / 110 || 1) : 0,
transition: 'opacity .16s ease', pointerEvents: 'none'
}
});
}
// Rail-Zustaende liegen in .rail/.rail.active (helmet), nicht inline: das Runtime setzt
// style-hover mit !important und wuerde den aktiven Button sonst beim Hover aufhellen.
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 }),
cls: s.screen === id ? 'rail active' : 'rail', style: railStyle
}));
const navTop = navAll.slice(0, 3);
const navBottom = navAll.slice(3);
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: '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 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: '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 tileHeights = [320, 240, 400, 280, 360, 220, 300, 260];
const gallery = DATA.posts.concat(DATA.videos).map((c, i) => ({
title: c.caption, stripes: this.stripes(i),
tile: { position: 'relative', height: tileHeights[i % tileHeights.length] + 'px', borderRadius: '16px',
overflow: 'hidden', cursor: 'zoom-in', breakInside: 'avoid', marginBottom: '16px',
background: 'linear-gradient(155deg,' + c.hue[0] + ' 0%,' + c.hue[1] + ' 100%)' }
}));
return {
navTop, navBottom, 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>