@@ -10,190 +10,138 @@
< 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" >
< 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 : #120b0d ; }
html , body { margin : 0 ; padding : 0 ; background : #ffffff ; }
* { box-sizing : border-box ; }
a { color : #8fe3c 1 ; text-decoration : none ; }
a : hover { color : #b9f2da ; }
:: selection { background : rgba ( 126 , 226 , 184 , .28 ) ; }
button : focus-visible , [ tabindex ] : focus-visible { outline : 2 px solid #7ee2b8 ; outline-offset : 2 px ; }
a { color : #11111 1 ; text-decoration : none ; }
:: selection { background : rgba ( 230 , 0 , 35 , .16 ) ; }
button : focus-visible , [ tabindex ] : focus-visible , input : focus-visible { outline : 2 px solid #0074e8 ; outline-offset : 2 px ; }
@ keyframes riseIn { from { opacity : 0 ; transform : translateY ( 14 px ) ; } 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 18 px rgba ( 126 , 226 , 184 , 0 ) } 100 % { box-shadow : 0 0 0 0 rgba ( 126 , 226 , 184 , 0 ) } }
/* Hover-Steuerung haengt an der Kachel, nicht am Element selbst. */
. pin-hover { opacity : 0 ; transition : opacity .12 s ease ; }
. pin : hover . pin-hover , . pin : focus-within . pin-hover { opacity : 1 ; }
. pin-scrim { opacity : 0 ; transition : opacity .12 s 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:#f2efe9; background:radial-gradient(120% 80% at 50% 0%, #0d4c3d 0%, #0a3a30 34%, #24121a 68%, #40161b 100%); background-attachment:fixed ;" >
< 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:236 px; flex:none; display:flex; flex-direction:column; gap:2 8px; padding:26 px 2 0px; 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:10 px;" >
< div style = "width:30 px; height:30 px; border-radius:9px ; background:linear-gradient(150deg,#2fa87f,#0e5442) ; display:grid; place-items:center; font-size:15 px; font-weight:700;" > V< / div >
< div style = "font-size:16px; font-weight:600; letter-spacing:-.01em;" > videogen< / div >
< aside style = "width:80 px; flex:none; display:flex; flex-direction:column; align-items:center; gap:8px; padding:1 2px 0 16 px; 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:4 px;" >
< div style = "width:32 px; height:32 px; border-radius:50% ; background:#e60023 ; display:grid; place-items:center; color:#ffffff; font-size:17 px; font-weight:700;" > V< / div >
< / div >
< nav style = "display:flex; flex-direction:column; gap:4 px;" >
< sc-for list = "{{ navItems }}" as = "item" hint-placeholder-count = "4 " >
< button onClick = "{{ item.go }}" sty le= "{{ 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 >
< nav style = "display:flex; flex-direction:column; align-items:center; gap:8 px;" >
< sc-for list = "{{ navTop }}" as = "item" hint-placeholder-count = "3 " >
< button onClick = "{{ item.go }}" tit le= "{{ item.label }}" class = "{{ item.cls }}" style = "{{ item.style }}" > {{ item.glyph }}< / button >
< / sc-for >
< / nav >
< div style = "margin-top:auto; display:flex; flex-direction:column; gap:14 px;" >
< 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 style = "margin-top:auto; display:flex; flex-direction:column; align-items:center; gap:10 px;" >
< 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 = "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:6 px; 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 }}< / butto n>
< / 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 >
< 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:12 px; 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< / spa n>
< 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; 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 }}" a s= "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 >
< section style = "flex:1; min-height:0; overflow:auto; padding:24px 24px 96px ;" >
< div style = "{{ gridStyle }}" >
< sc-for list = "{{ grid }}" as = "c" hint-placeholder -cou nt= "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 >
< / 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 class = "pin-hover" style = "{{ c.saveSlot }}">
< button onClick = "{{ c.toggleSave }}" style = "{{ c.saveStyle }}" style-hover = "filter:brightness(.88);" > {{ c.saveLabel }}< / 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 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-i f >
< / sc-for >
< / 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:11 px; letter-spacing:.16em; text-transform:uppercase; color:rgba(242,239,233,.45);" > Aktuelle Karte < / div >
< div style = "font-size:26 px; 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 >
< 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:20 px; font-weight:700;" > Nichts mehr in dieser Ansicht < / div >
< div style = "font-size:15 px; 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 >
< 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 >
< / sc-if >
< / section >
< / sc-if >
< sc-if value = "{{ isProfile }}" hint-placeholder-val = "{{ false }}" >
< section style = "flex:1; padding:38 px 44 px; overflow:auto; animation:riseIn .26s ease;" >
< div style = "display:flex; align-items:flex-end ; gap:22 px; padding-bottom:26px; border-bottom:1px solid rgba(255,255,255,.09) ;" >
< div style = "width:96 px; height:96 px; 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:32 px; font-weight:600; letter-spacing:-.02em;" > Jonas K. < / div >
< div style = "font-size:14px; color:rgba(242,239,233,.6) ; margin-top:4 px;" > Studio-Konto · seit März 2026< / div >
< / div >
< div style = "display:flex; gap:34px; padding-bottom:6px;" >
< section style = "flex:1; padding:32 px 24px 40 px; overflow:auto; animation:riseIn .26s ease;" >
< div style = "display:flex; flex-direction:column; align-items:center ; gap:10 px; padding-bottom:28px ;" >
< div style = "width:120 px; height:120 px; 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:15 px; color:#5f5f5f;" > Studio-Konto · seit März 2026 < / div >
< div style = "display:flex; gap:28px ; margin-top:6 px;" >
< 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:12 px; color:rgba(242,239,233,.5) ;" > {{ s.label }}< / div >
< 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:13 px; color:#767676 ;" > {{ s.label }}< / div >
< / div >
< / sc-for >
< / div >
< / div >
< div style = "display:grid; grid-template-columns:repeat(4,1fr); gap:18px; margin-top:28px; " >
< div style = "{{ gridStyle }} " >
< 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.stri pes }}; 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 class = "pin" style = "position:relative; break-inside:avoid; margin-bottom:16px ;" >
< div onClick = " {{ g.o pen }}" 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 >
@@ -201,47 +149,47 @@
< / sc-if >
< sc-if value = "{{ isCreate }}" hint-placeholder-val = "{{ false }}" >
< section style = "flex:1; overflow:auto; padding:38 px 44 px; animation:riseIn .26s ease;" >
< div style = "max-width:920px; display:grid; grid-template-columns:1fr 320px; gap:34 px;" >
< div style = "display:flex; flex-direction:column; gap:22 px;" >
< section style = "flex:1; overflow:auto; padding:32 px 24px 40 px; 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:32 px;" >
< div style = "display:flex; flex-direction:column; gap:20 px;" >
< div >
< div style = "font-size:30px; font-weight:6 00; letter-spacing:-.02em;" > Neuen Post erstellen< / div >
< div style = "font-size:14 px; color:rgba(242,239,233,.6) ; margin-top:6px;" > Material hochladen, beschriften, in den Feed geben.< / div >
< div style = "font-size:30px; font-weight:7 00; letter-spacing:-.02em;" > Neuen Post erstellen< / div >
< div style = "font-size:15 px; color:#5f5f5f ; margin-top:6px;" > Material hochladen, beschriften, in den Feed geben.< / div >
< / div >
< div style = "border:1 px dashed rgba(255,255,255,.24) ; border-radius:18 px; height:220px; display:grid; place-items:center; text-align:center; gap:8px; background:rgba(255,255,255,.03) ; cursor:pointer; transition:border-color .18 s ease, background .18 s ease;" style-hover = "border-color:rgba(126,226,184,.6); background:rgba(126,226,184,.06) ;" >
< div style = "border:2 px dashed #cdcdcd ; border-radius:16 px; height:220px; display:grid; place-items:center; text-align:center; background:#f5f5f5 ; cursor:pointer; transition:border-color .12 s ease, background .12 s ease;" style-hover = "border-color:#e60023; background:#fafafa ;" >
< div >
< div style = "font-size:15 px; font-weight:6 00;" > Dateien hierher ziehen< / div >
< div style = "font-size:13 px; color:rgba(242,239,233,.55) ; margin-top:6px;" > MP4, MOV oder bis zu 10 Bilder< / div >
< div style = "font-size:16 px; font-weight:7 00;" > Dateien hierher ziehen< / div >
< div style = "font-size:14 px; 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:12 px; letter-spacing:.14em; text-transform:uppercase; color:rgba(242,239,233,.5) ;" > Titel
< input value = "{{ draftTitle }}" onChange = "{{ setTitle }}" style = "padding:13 px 15 px; border-radius:11 px; border:1 px 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 style = "display:flex; flex-direction:column; gap:8px; font-size:13 px; font-weight:600; color:#111111 ;" > Titel
< input value = "{{ draftTitle }}" onChange = "{{ setTitle }}" style = "padding:14 px 16 px; border-radius:16 px; border:2 px 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:12 px; letter-spacing:.14em; text-transform:uppercase; color:rgba(242,239,233,.5) ;" > Untertitel
< input value = "{{ draftSub }}" onChange = "{{ setSub }}" style = "padding:13 px 15 px; border-radius:11 px; border:1 px 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 style = "display:flex; flex-direction:column; gap:8px; font-size:13 px; font-weight:600; color:#111111 ;" > Untertitel
< input value = "{{ draftSub }}" onChange = "{{ setSub }}" style = "padding:14 px 16 px; border-radius:16 px; border:2 px 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:12 px; letter-spacing:.14em; text-transform:uppercase; color:rgba(242,239,233,.5) ;" > Art
< div style = "display:flex; flex-direction:column; gap:10px; font-size:13 px; 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 }}" style-hover = "border-color:rgba(126,226,184,.6);" > {{ k.label }}< / button >
< button onClick = "{{ k.go }}" style = "{{ k.style }}" > {{ k.label }}< / button >
< / sc-for >
< / div >
< / div >
< / div >
< div style = "display:flex; gap:12 px;" >
< button onClick = "{{ publish }}" style = "padding:13 px 24px; border-radius:12px; border:1px solid rgba(126,226,184,.6) ; background:transparent ; color:#8fe3c1 ; font-family:inherit; font-size:14 px; font-weight:6 00; cursor:pointer; transition:background .18 s ease;" style-hover = "background:rgba(126,226,184,.14) ;" > Veröffentlichen< / button >
< button style = "padding:13 px 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:14 px; cursor:pointer; transition:background .18 s ease;" style-hover = "background:rgba(255,255,255,.07) ;" > Als Entwurf sichern< / button >
< div style = "display:flex; gap:8 px;" >
< button onClick = "{{ publish }}" style = "padding:14 px 24px; border:none; border-radius:24px ; background:#e60023 ; color:#ffffff ; font-family:inherit; font-size:15 px; font-weight:7 00; cursor:pointer; transition:background .12 s ease;" style-hover = "background:#ad081b ;" > Veröffentlichen< / button >
< button style = "padding:14 px 24px; border:none; border-radius:24px; background:#efefef; color:#111111 ; font-family:inherit; font-size:15 px; font-weight:700; cursor:pointer; transition:background .12 s ease;" style-hover = "background:#e2e2e2 ;" > Als Entwurf sichern< / button >
< / div >
< / div >
< div >
< div style = "font-size:11 px; 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:22 px; overflow:hidden; border:1px solid rgba(255,255,255,.12); background:linear-gradient(155deg,#1f8163 0%,#0d4034 100%);" >
< div style = "font-size:12 px; font-weight:600; color:#767676 ; margin-bottom:12px;" > Vorschau< / div >
< div style = "position:relative; height:420px; border-radius:16 px; 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,.72 ), transparent);" >
< 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:13 px; color:rgba(255,255,255,.7 );" > {{ draftSub }}< / div >
< div style = "font-size:14 px; color:rgba(255,255,255,.8 );" > {{ draftSub }}< / div >
< / div >
< / div >
< / div >
@@ -250,15 +198,15 @@
< / sc-if >
< sc-if value = "{{ isSettings }}" hint-placeholder-val = "{{ false }}" >
< section style = "flex:1; overflow:auto; padding:38 px 44 px; animation:riseIn .26s ease;" >
< div style = "max-width:660px;" >
< div style = "font-size:30px; font-weight:6 00; letter-spacing:-.02em;" > Einstellungen< / div >
< div style = "margin-top:26 px; border:1px solid rgba(255,255,255,.1); border-radius:16px; overflow:hidden ;" >
< section style = "flex:1; overflow:auto; padding:32 px 24px 40 px; animation:riseIn .26s ease;" >
< div style = "max-width:660px; margin:0 auto; " >
< div style = "font-size:30px; font-weight:7 00; letter-spacing:-.02em;" > Einstellungen< / div >
< div style = "margin-top:24 px; 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:18px 20px; border-bottom:1px solid rgba(255,255,255,.07); background:rgba(255,255,255,.02) ;" >
< 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:15 px; font-weight:6 00;" > {{ s.label }}< / div >
< div style = "font-size:13 px; color:rgba(242,239,233,.55) ; margin-top:3px; text-wrap:pretty;" > {{ s.hint }}< / div >
< div style = "font-size:16 px; font-weight:7 00;" > {{ s.label }}< / div >
< div style = "font-size:14 px; color:#767676 ; margin-top:3px; text-wrap:pretty;" > {{ s.hint }}< / div >
< / div >
< button onClick = "{{ s.toggle }}" style = "{{ s.track }}" >
< span style = "{{ s.knob }}" > < / span >
@@ -270,232 +218,326 @@
< / 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 = "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 >
< 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 = "{"stackDepth ":{"editor":"int","default":3 ,"min":1 ,"max":5 ,"tsType":"number","section":"Feed "}}" >
< script type = "text/x-dc" data-dc-script data-props = "{"columns ":{"editor":"int","default":5 ,"min":3 ,"max":6 ,"tsType":"number","section":"Raster "}}" >
const DATA = {
posts : [
{ 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' ] ] }
{ 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 : [
{ 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' ] ] }
{ 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 : [
{ 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' ] ] }
{ 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' , idx : 0 ,
drag : null , exiting : null , showInfo : false , toast : '' ,
seen : 0 , saved : 24 , history : [ ] ,
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 ( 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 ( ) ; }
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 . _t ) clearTimeout ( this . _t ) ; }
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 ) ; }
commit ( dir ) {
if ( this . state . exiting ) return ;
if ( this . state . idx >= this . deck ( ) . length ) return ;
const car d = thi s. deck ( ) [ thi s. 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' ) ;
toggleSave ( i d) {
const card = this . byId ( id ) ;
const wasSaved = this . state . saved . includes ( id ) ;
this . setState ( s => ( { saved : wasSave d ? s. saved . filter ( x => x !== id ) : s. saved . concat ( [ id] ) } ) ) ;
this . flash ( wasSaved ? 'Aus der Sammlung entfernt' : 'Zu „' + card . name + '" gemerkt' ) ;
}
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 } ) ) ;
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' ) ;
}
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 } ) ;
} ;
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 , 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 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 stack = [ ] ;
for ( let k = 0 ; k < depth ; k ++ ) {
const car d = 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' , inse t: 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 ( {
s hell : { .. .sh ell } ,
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 grid = visible . map ( ( card , i ) => {
const isSaved = s . saved . includes ( card . id ) ;
const selecte d = s . selection . includes ( card . id ) ;
return {
caption : card . caption , name : card . name , kind : card . kind ,
s tripes : 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' , righ t: '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'
} ,
c heckbox : 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 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 railStyle = {
width : '48px' , height : '48px' , display : 'gr id' , p laceItems : '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 , idx : 0 , history : [ ] , drag : null , exiting : null , screen : 'feed' } ) ,
label , go : ( ) => this . setState ( { tab : id , screen : 'feed' , selection : [ ] , menuFor : null , detail : null } ) ,
style : {
padding : '9 px 18 px' , 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 ? '6 00' : '500' ) + ' 16px/1 Manrope, system-ui, sans-serif' ,
cursor : 'pointer' , transition : 'background .18 s ease, color .18 s ease'
padding : '0 2 px 12 px' , border : 'none ' , background : 'transparent' ,
borderBottom : '3px solid ' + ( s . tab === id ? '#111111 ' : 'transparent' ) ,
color : s . tab === id ? '#111111 ' : '#767676 ' ,
font : ( s . tab === id ? '7 00' : '500' ) + ' 16px/1 Manrope, system-ui, sans-serif' ,
cursor : 'pointer' , transition : 'color .12 s ease, border- color .12 s ease'
}
} ) ) ;
const kindOptions = [ 'Produkt' , 'Video' , 'Serie' ] . map ( k => ( {
label : k , go : ( ) => this . setState ( { draftKind : k } ) ,
style : {
padding : '10 px 18px' , borderRadius : '999 px' , 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) ',
f ont : '500 14px/1 Manrope, system-ui, sans-serif' , letterSpacing : 'normal' , textTransform : 'none' ,
transition : 'border-color .18s ease, background .18s ease'
padding : '11 px 18px' , borderRadius : '22 px' , 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 ',
transiti on: 'background .12s ease, color .12s ease'
}
} ) ) ;
const setDef = [ [ 'autoplay' , 'Videos automatisch abspielen' , 'Clips starten stumm, sobald die Karte oben lieg t.' ] ,
[ 'sound' , 'Ton beim Öffnen' , 'Beim Anklicken einer Karte den Ton aktivieren.' ] ,
const setDef = [ [ 'autoplay' , 'Videos automatisch abspielen' , 'Clips starten stumm, sobald die Kachel im Bild is t.' ] ,
[ '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' , 'Karten animiert ein- und ausblenden.' ] ] ;
[ '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 : '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' }
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 = 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%)' }
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 {
navItems , tabs , s tack , kindOptions , settings , gallery ,
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' ,
stack Empty: 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' ) ,
onLik e: ( ) => 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 le er',
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' } ] ,
grid Empty: visible . length === 0 ,
unhideAll : ( ) => this . setState ( { hidden : [ ] } ) ,
counter : visible . length + ' Karten · ' + s . saved . length + ' gemerkt' ,
toggleSelectMod e: ( ) => this . setState ( st => ( { selectMode : ! st . selectMode , selection : [ ] , menuFor : null } ) ) ,
selectToggleLabel : s . selectMode ? 'Fertig' : 'Auswählen' ,
selectToggleStyle : {
padding : '10px 18px' , border : 'none' , borderRadius : '22px' , cursor : 'point er' ,
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
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
}
} ;
}
}