Initial commit
This commit is contained in:
@@ -0,0 +1,947 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Pinkudex — Markers feature mockup</title>
|
||||
<style>
|
||||
:root {
|
||||
--bg-0: #0b0d10;
|
||||
--bg-1: #14171c;
|
||||
--bg-2: #1c2027;
|
||||
--bg-3: #252a33;
|
||||
--fg: #e6e8ec;
|
||||
--fg-dim: #a4abb6;
|
||||
--fg-muted: #6b7380;
|
||||
--cyan: #22d3ee;
|
||||
--mint: #34d399;
|
||||
--amber: #fbbf24;
|
||||
--coral: #f87171;
|
||||
--violet: #a78bfa;
|
||||
--pink: #f472b6;
|
||||
--glass: rgba(255,255,255,0.04);
|
||||
--glass-border: rgba(255,255,255,0.10);
|
||||
--glass-border-strong: rgba(255,255,255,0.18);
|
||||
}
|
||||
* { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; }
|
||||
body {
|
||||
background: var(--bg-0);
|
||||
color: var(--fg);
|
||||
font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI";
|
||||
min-height: 100vh;
|
||||
}
|
||||
.page { max-width: 1280px; margin: 0 auto; padding: 28px; }
|
||||
h1 { font-weight: 500; font-size: 22px; margin: 0 0 6px; letter-spacing: -0.01em; }
|
||||
h2 { font-weight: 500; font-size: 16px; margin: 28px 0 10px; }
|
||||
.sub { color: var(--fg-muted); font-size: 13px; margin-bottom: 18px; }
|
||||
|
||||
/* Tabs to switch between the three views */
|
||||
.tabs {
|
||||
display: flex; gap: 4px; padding: 4px;
|
||||
background: var(--bg-1); border: 1px solid var(--glass-border);
|
||||
border-radius: 10px; margin-bottom: 24px; width: fit-content;
|
||||
}
|
||||
.tabs button {
|
||||
background: transparent; border: 0; color: var(--fg-dim);
|
||||
font-family: inherit; font-size: 12px; font-weight: 500;
|
||||
padding: 9px 16px; border-radius: 7px; cursor: pointer;
|
||||
transition: background 120ms, color 120ms;
|
||||
}
|
||||
.tabs button:hover { color: var(--fg); }
|
||||
.tabs button.active {
|
||||
background: rgba(34,211,238,0.15);
|
||||
color: var(--cyan);
|
||||
box-shadow: inset 0 0 0 1px rgba(34,211,238,0.4);
|
||||
}
|
||||
.view { display: none; animation: fadeIn 200ms ease-out; }
|
||||
.view.active { display: block; }
|
||||
@keyframes fadeIn { from { opacity: 0; transform: translateY(4px); } to { opacity: 1; transform: none; } }
|
||||
|
||||
/* ============ MARKERS WALL ============ */
|
||||
.toolbar {
|
||||
display: flex; gap: 12px; align-items: center;
|
||||
padding: 10px 14px; background: var(--bg-1);
|
||||
border: 1px solid var(--glass-border); border-radius: 12px;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.toolbar input {
|
||||
flex: 1;
|
||||
background: var(--bg-0); color: var(--fg);
|
||||
border: 1px solid var(--glass-border-strong);
|
||||
padding: 7px 10px; border-radius: 8px;
|
||||
font-family: inherit; font-size: 13px; outline: none;
|
||||
}
|
||||
.toolbar input:focus { border-color: var(--cyan); }
|
||||
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
|
||||
.chip {
|
||||
font-family: ui-monospace, monospace; font-size: 11px;
|
||||
padding: 4px 10px; border-radius: 999px;
|
||||
background: var(--glass); border: 1px solid var(--glass-border);
|
||||
color: var(--fg-dim); cursor: pointer; user-select: none;
|
||||
transition: all 120ms;
|
||||
}
|
||||
.chip:hover { color: var(--fg); border-color: var(--glass-border-strong); }
|
||||
.chip.active {
|
||||
background: rgba(34,211,238,0.18);
|
||||
border-color: rgba(34,211,238,0.5);
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
gap: 14px;
|
||||
}
|
||||
.tile {
|
||||
background: var(--bg-1);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 12px; overflow: hidden;
|
||||
cursor: pointer;
|
||||
transition: transform 180ms, border-color 180ms;
|
||||
}
|
||||
.tile:hover {
|
||||
transform: translateY(-2px);
|
||||
border-color: var(--glass-border-strong);
|
||||
}
|
||||
.tile-media {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9;
|
||||
overflow: hidden;
|
||||
background: var(--bg-2);
|
||||
}
|
||||
.tile-media .static, .tile-media .animated {
|
||||
position: absolute; inset: 0; width: 100%; height: 100%;
|
||||
display: block;
|
||||
}
|
||||
.tile-media .animated {
|
||||
opacity: 0;
|
||||
transition: opacity 200ms;
|
||||
}
|
||||
.tile:hover .tile-media .animated {
|
||||
opacity: 1;
|
||||
}
|
||||
.tile-media .timestamp {
|
||||
position: absolute; bottom: 8px; right: 8px;
|
||||
background: rgba(0,0,0,0.75);
|
||||
color: var(--fg);
|
||||
font-family: ui-monospace, monospace; font-size: 10px;
|
||||
padding: 2px 6px; border-radius: 4px;
|
||||
backdrop-filter: blur(4px);
|
||||
}
|
||||
.tile-media .play-icon {
|
||||
position: absolute; top: 50%; left: 50%; transform: translate(-50%, -50%) scale(0.8);
|
||||
width: 44px; height: 44px;
|
||||
background: rgba(0,0,0,0.6); border: 2px solid var(--cyan);
|
||||
border-radius: 50%; display: flex; align-items: center; justify-content: center;
|
||||
color: var(--cyan); opacity: 0;
|
||||
transition: opacity 200ms, transform 200ms;
|
||||
pointer-events: none;
|
||||
}
|
||||
.tile:hover .play-icon { opacity: 1; transform: translate(-50%, -50%) scale(1); }
|
||||
.tile-meta { padding: 10px 12px; }
|
||||
.tile-tags {
|
||||
display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 6px;
|
||||
}
|
||||
.tile-tag {
|
||||
font-family: ui-monospace, monospace; font-size: 10px;
|
||||
padding: 2px 7px; border-radius: 4px;
|
||||
background: rgba(167,139,250,0.15);
|
||||
color: var(--violet);
|
||||
border: 1px solid rgba(167,139,250,0.3);
|
||||
}
|
||||
.tile-tag.primary {
|
||||
background: rgba(34,211,238,0.15);
|
||||
color: var(--cyan);
|
||||
border-color: rgba(34,211,238,0.4);
|
||||
font-weight: 600;
|
||||
}
|
||||
.tile-scene {
|
||||
font-size: 11px; color: var(--fg-muted);
|
||||
font-family: ui-monospace, monospace;
|
||||
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||
}
|
||||
.tile-scene .code { color: var(--fg-dim); font-weight: 600; }
|
||||
|
||||
/* Synthetic "thumbnail" — gradients with patterns to differentiate */
|
||||
.frame {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
color: rgba(255,255,255,0.55);
|
||||
font-family: ui-monospace, monospace; font-size: 10px;
|
||||
text-shadow: 0 1px 2px rgba(0,0,0,0.6);
|
||||
}
|
||||
|
||||
/* Animated preview synthesis: cycle 3 frames every 2s */
|
||||
.anim-frames {
|
||||
position: absolute; inset: 0;
|
||||
animation: cycleFrames 2.5s steps(3, end) infinite;
|
||||
}
|
||||
.anim-frames > div {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
}
|
||||
.anim-frames > div:nth-child(1) { transform: translateX(0%); }
|
||||
.anim-frames > div:nth-child(2) { transform: translateX(100%); }
|
||||
.anim-frames > div:nth-child(3) { transform: translateX(200%); }
|
||||
@keyframes cycleFrames {
|
||||
0%,33% { transform: translateX(0%); }
|
||||
34%,66% { transform: translateX(-100%); }
|
||||
67%,100% { transform: translateX(-200%); }
|
||||
}
|
||||
/* Container for the cycling animation */
|
||||
.anim-strip {
|
||||
position: absolute; inset: 0;
|
||||
width: 300%; height: 100%;
|
||||
display: flex;
|
||||
animation: stripScroll 2.4s linear infinite;
|
||||
}
|
||||
.anim-strip > .frame {
|
||||
position: relative; inset: auto;
|
||||
flex: 0 0 33.333%;
|
||||
}
|
||||
@keyframes stripScroll {
|
||||
0% { transform: translateX(0%); }
|
||||
33% { transform: translateX(0%); }
|
||||
34% { transform: translateX(-33.333%); }
|
||||
66% { transform: translateX(-33.333%); }
|
||||
67% { transform: translateX(-66.666%); }
|
||||
99% { transform: translateX(-66.666%); }
|
||||
100% { transform: translateX(0%); }
|
||||
}
|
||||
/* "REC" indicator on hover to make it obvious it's animated */
|
||||
.anim-indicator {
|
||||
position: absolute; top: 8px; left: 8px;
|
||||
background: rgba(248,113,113,0.85);
|
||||
color: white;
|
||||
font-family: ui-monospace, monospace; font-size: 9px;
|
||||
padding: 2px 6px; border-radius: 3px;
|
||||
text-transform: uppercase; letter-spacing: 0.08em;
|
||||
font-weight: 700;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms;
|
||||
}
|
||||
.tile:hover .anim-indicator { opacity: 1; animation: pulse 1.6s infinite; }
|
||||
@keyframes pulse { 50% { opacity: 0.5; } }
|
||||
|
||||
/* Synthetic gradient backgrounds for fake thumbnails */
|
||||
.bg-room { background: linear-gradient(135deg, #4a3b2a 0%, #2a1f15 100%); }
|
||||
.bg-bed { background: linear-gradient(135deg, #5b3a3a 0%, #2a1818 100%); }
|
||||
.bg-shower { background: linear-gradient(135deg, #2a4a5b 0%, #15252e 100%); }
|
||||
.bg-kitchen { background: linear-gradient(135deg, #3a4a3a 0%, #1a2a1a 100%); }
|
||||
.bg-office { background: linear-gradient(135deg, #2a3552 0%, #131a2e 100%); }
|
||||
.bg-outdoor { background: linear-gradient(135deg, #5a4a2a 0%, #2a2110 100%); }
|
||||
.bg-purple { background: linear-gradient(135deg, #3d2a52 0%, #1c1430 100%); }
|
||||
.bg-pink { background: linear-gradient(135deg, #5a2a4a 0%, #2e1428 100%); }
|
||||
|
||||
/* Frame variants — slight differences to fake motion */
|
||||
.frame::before {
|
||||
content: ""; position: absolute;
|
||||
width: 60%; height: 60%; border-radius: 50%;
|
||||
background: radial-gradient(circle at 35% 35%, rgba(255,255,255,0.18), transparent 70%);
|
||||
top: 50%; left: 50%; transform: translate(-50%, -50%);
|
||||
}
|
||||
.frame.f1::before { transform: translate(-50%, -50%) scale(0.95); }
|
||||
.frame.f2::before { transform: translate(-46%, -52%) scale(1.0); }
|
||||
.frame.f3::before { transform: translate(-54%, -48%) scale(1.02); }
|
||||
|
||||
/* ============ SCENE DETAIL with timeline pips ============ */
|
||||
.scene-detail {
|
||||
background: var(--bg-1);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 14px; overflow: hidden;
|
||||
}
|
||||
.player {
|
||||
position: relative;
|
||||
aspect-ratio: 16/9;
|
||||
background: #000;
|
||||
overflow: hidden;
|
||||
}
|
||||
.player-content {
|
||||
position: absolute; inset: 0;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: linear-gradient(135deg, #1a1a2e, #0a0a14);
|
||||
}
|
||||
.player-content::before {
|
||||
content: "▶"; font-size: 80px; color: rgba(34,211,238,0.3);
|
||||
}
|
||||
.player-controls {
|
||||
position: absolute; left: 0; right: 0; bottom: 0;
|
||||
background: linear-gradient(to top, rgba(0,0,0,0.92) 0%, transparent 100%);
|
||||
padding: 18px 16px 12px;
|
||||
}
|
||||
.timeline-row {
|
||||
position: relative;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
/* Pips ABOVE the seekbar */
|
||||
.pips {
|
||||
position: relative;
|
||||
height: 22px;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pip {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
width: 3px; height: 14px;
|
||||
background: var(--cyan);
|
||||
border-radius: 2px;
|
||||
cursor: pointer;
|
||||
transition: height 150ms, width 150ms, background 150ms;
|
||||
box-shadow: 0 0 4px rgba(34,211,238,0.5);
|
||||
}
|
||||
.pip:hover { height: 22px; width: 5px; }
|
||||
.pip.active { background: var(--amber); height: 22px; width: 5px; box-shadow: 0 0 8px rgba(251,191,36,0.7); }
|
||||
/* Pip preview popover on hover */
|
||||
.pip-tooltip {
|
||||
position: absolute;
|
||||
bottom: 28px;
|
||||
transform: translateX(-50%);
|
||||
background: var(--bg-2);
|
||||
border: 1px solid var(--glass-border-strong);
|
||||
border-radius: 8px;
|
||||
padding: 6px;
|
||||
width: 160px;
|
||||
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
||||
pointer-events: none;
|
||||
opacity: 0;
|
||||
transition: opacity 200ms;
|
||||
z-index: 10;
|
||||
}
|
||||
.pip:hover .pip-tooltip { opacity: 1; }
|
||||
.pip-tooltip .mini-thumb {
|
||||
width: 100%; aspect-ratio: 16/9;
|
||||
border-radius: 4px; overflow: hidden;
|
||||
position: relative;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
.pip-tooltip .label {
|
||||
font-family: ui-monospace, monospace; font-size: 10px;
|
||||
color: var(--fg);
|
||||
}
|
||||
.pip-tooltip .label .ts {
|
||||
color: var(--fg-muted); margin-left: 6px;
|
||||
}
|
||||
|
||||
.seekbar {
|
||||
position: relative;
|
||||
height: 6px;
|
||||
background: rgba(255,255,255,0.18);
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.seekbar .progress {
|
||||
position: absolute; inset: 0;
|
||||
width: 35%;
|
||||
background: var(--cyan);
|
||||
border-radius: 3px;
|
||||
box-shadow: 0 0 8px rgba(34,211,238,0.6);
|
||||
}
|
||||
.seekbar .head {
|
||||
position: absolute;
|
||||
top: 50%; transform: translate(-50%, -50%);
|
||||
left: 35%;
|
||||
width: 14px; height: 14px;
|
||||
background: var(--cyan);
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 10px rgba(34,211,238,0.9);
|
||||
}
|
||||
.player-controls-row {
|
||||
display: flex; align-items: center; gap: 10px;
|
||||
color: var(--fg);
|
||||
font-family: ui-monospace, monospace; font-size: 11px;
|
||||
}
|
||||
.player-controls-row .play-btn {
|
||||
width: 28px; height: 28px;
|
||||
border-radius: 50%;
|
||||
background: var(--cyan);
|
||||
color: black;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
font-size: 13px; cursor: pointer;
|
||||
}
|
||||
.timestamp-display {
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
.marker-hint {
|
||||
margin-left: auto;
|
||||
color: var(--fg-muted); font-size: 10px;
|
||||
}
|
||||
.marker-hint kbd {
|
||||
background: var(--bg-3);
|
||||
border: 1px solid var(--glass-border-strong);
|
||||
border-radius: 3px;
|
||||
padding: 1px 5px;
|
||||
font-family: ui-monospace, monospace; font-size: 10px;
|
||||
color: var(--cyan);
|
||||
}
|
||||
|
||||
.scene-info {
|
||||
padding: 18px;
|
||||
}
|
||||
.scene-info h3 { margin: 0 0 4px; font-weight: 500; }
|
||||
.scene-info .scene-meta {
|
||||
color: var(--fg-muted); font-family: ui-monospace, monospace;
|
||||
font-size: 11px; margin-bottom: 14px;
|
||||
}
|
||||
.markers-tabs {
|
||||
display: flex; gap: 16px; border-bottom: 1px solid var(--glass-border);
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
.markers-tabs button {
|
||||
background: transparent; border: 0; color: var(--fg-muted);
|
||||
font-family: inherit; font-size: 13px; padding: 6px 0;
|
||||
cursor: pointer; border-bottom: 2px solid transparent;
|
||||
}
|
||||
.markers-tabs button.active {
|
||||
color: var(--cyan); border-bottom-color: var(--cyan);
|
||||
}
|
||||
.marker-row {
|
||||
display: grid; grid-template-columns: 110px 1fr auto;
|
||||
gap: 12px; align-items: center;
|
||||
padding: 8px 6px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 120ms;
|
||||
}
|
||||
.marker-row:hover { background: var(--glass); }
|
||||
.marker-row .row-thumb {
|
||||
aspect-ratio: 16/9; border-radius: 4px;
|
||||
background: var(--bg-2);
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.marker-row .row-meta {
|
||||
display: flex; flex-direction: column; gap: 4px;
|
||||
}
|
||||
.marker-row .row-tags {
|
||||
display: flex; gap: 4px; flex-wrap: wrap;
|
||||
}
|
||||
.marker-row .row-time {
|
||||
font-family: ui-monospace, monospace; font-size: 11px;
|
||||
color: var(--fg-muted);
|
||||
}
|
||||
.marker-row .row-action {
|
||||
font-family: ui-monospace, monospace; font-size: 11px;
|
||||
color: var(--cyan);
|
||||
padding: 4px 10px;
|
||||
border: 1px solid rgba(34,211,238,0.4);
|
||||
border-radius: 6px;
|
||||
background: rgba(34,211,238,0.1);
|
||||
}
|
||||
|
||||
/* ============ CREATE MARKER DIALOG ============ */
|
||||
.dialog-backdrop {
|
||||
position: fixed; inset: 0;
|
||||
background: rgba(0,0,0,0.7);
|
||||
backdrop-filter: blur(4px);
|
||||
display: none;
|
||||
align-items: center; justify-content: center;
|
||||
z-index: 100;
|
||||
}
|
||||
.dialog-backdrop.show { display: flex; animation: fadeIn 200ms; }
|
||||
.dialog {
|
||||
background: var(--bg-1);
|
||||
border: 1px solid var(--glass-border-strong);
|
||||
border-radius: 14px;
|
||||
padding: 20px;
|
||||
width: 480px;
|
||||
box-shadow: 0 24px 80px rgba(0,0,0,0.6);
|
||||
}
|
||||
.dialog h3 {
|
||||
margin: 0 0 4px; font-weight: 500; font-size: 16px;
|
||||
display: flex; align-items: center; gap: 8px;
|
||||
}
|
||||
.dialog h3 .ts-pill {
|
||||
margin-left: auto;
|
||||
font-family: ui-monospace, monospace; font-size: 11px;
|
||||
background: rgba(34,211,238,0.15);
|
||||
color: var(--cyan);
|
||||
padding: 3px 8px; border-radius: 5px;
|
||||
border: 1px solid rgba(34,211,238,0.4);
|
||||
}
|
||||
.dialog .hint { color: var(--fg-muted); font-size: 12px; margin-bottom: 14px; }
|
||||
.dialog label {
|
||||
display: block; font-size: 11px;
|
||||
text-transform: uppercase; letter-spacing: 0.06em;
|
||||
color: var(--fg-muted); margin: 12px 0 5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.dialog input[type="text"] {
|
||||
width: 100%;
|
||||
background: var(--bg-0); color: var(--fg);
|
||||
border: 1px solid var(--glass-border-strong);
|
||||
padding: 8px 10px; border-radius: 7px;
|
||||
font-family: inherit; font-size: 13px; outline: none;
|
||||
}
|
||||
.dialog input[type="text"]:focus { border-color: var(--cyan); }
|
||||
.tag-search-results {
|
||||
margin-top: 4px;
|
||||
background: var(--bg-2);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 7px;
|
||||
max-height: 140px; overflow-y: auto;
|
||||
}
|
||||
.tag-search-results .opt {
|
||||
padding: 6px 10px;
|
||||
font-family: ui-monospace, monospace; font-size: 12px;
|
||||
color: var(--fg-dim);
|
||||
cursor: pointer;
|
||||
transition: background 120ms;
|
||||
}
|
||||
.tag-search-results .opt:hover {
|
||||
background: var(--glass);
|
||||
color: var(--fg);
|
||||
}
|
||||
.selected-tags {
|
||||
display: flex; gap: 6px; flex-wrap: wrap;
|
||||
margin-top: 6px;
|
||||
}
|
||||
.selected-tag {
|
||||
font-family: ui-monospace, monospace; font-size: 11px;
|
||||
padding: 3px 8px; border-radius: 5px;
|
||||
background: rgba(167,139,250,0.15);
|
||||
color: var(--violet);
|
||||
border: 1px solid rgba(167,139,250,0.3);
|
||||
display: flex; align-items: center; gap: 4px;
|
||||
}
|
||||
.selected-tag.primary {
|
||||
background: rgba(34,211,238,0.15);
|
||||
color: var(--cyan);
|
||||
border-color: rgba(34,211,238,0.4);
|
||||
}
|
||||
.selected-tag .x {
|
||||
cursor: pointer; opacity: 0.6;
|
||||
}
|
||||
.selected-tag .x:hover { opacity: 1; }
|
||||
.dialog-actions {
|
||||
display: flex; justify-content: flex-end; gap: 8px;
|
||||
margin-top: 16px;
|
||||
padding-top: 14px;
|
||||
border-top: 1px solid var(--glass-border);
|
||||
}
|
||||
.dialog-actions button {
|
||||
font-family: inherit; font-size: 13px;
|
||||
padding: 7px 14px; border-radius: 7px;
|
||||
cursor: pointer; transition: all 120ms;
|
||||
border: 1px solid var(--glass-border-strong);
|
||||
}
|
||||
.btn-cancel {
|
||||
background: transparent;
|
||||
color: var(--fg-dim);
|
||||
}
|
||||
.btn-cancel:hover { color: var(--fg); }
|
||||
.btn-create {
|
||||
background: var(--cyan);
|
||||
color: black; font-weight: 600;
|
||||
border-color: var(--cyan);
|
||||
}
|
||||
.btn-create:hover { background: #67e8f9; }
|
||||
|
||||
/* Storage / data inspector */
|
||||
.inspector {
|
||||
margin-top: 24px;
|
||||
background: var(--bg-1);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 12px;
|
||||
padding: 16px;
|
||||
}
|
||||
.inspector h2 {
|
||||
margin-top: 0;
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--cyan);
|
||||
font-weight: 600;
|
||||
}
|
||||
.inspector pre {
|
||||
background: var(--bg-0);
|
||||
border: 1px solid var(--glass-border);
|
||||
border-radius: 8px;
|
||||
padding: 12px;
|
||||
overflow-x: auto;
|
||||
font-family: ui-monospace, "SF Mono", monospace;
|
||||
font-size: 11.5px;
|
||||
color: var(--fg-dim);
|
||||
line-height: 1.6;
|
||||
}
|
||||
.inspector pre .key { color: var(--cyan); }
|
||||
.inspector pre .str { color: var(--mint); }
|
||||
.inspector pre .num { color: var(--amber); }
|
||||
.inspector pre .com { color: var(--fg-muted); font-style: italic; }
|
||||
|
||||
.legend {
|
||||
display: flex; gap: 18px;
|
||||
padding: 10px 14px; background: var(--bg-1);
|
||||
border: 1px solid var(--glass-border); border-radius: 10px;
|
||||
margin-bottom: 18px;
|
||||
font-size: 12px; color: var(--fg-dim);
|
||||
}
|
||||
.legend .item { display: flex; align-items: center; gap: 6px; }
|
||||
.legend .swatch {
|
||||
width: 12px; height: 12px; border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="page">
|
||||
<h1>Markers feature — visual mockup</h1>
|
||||
<div class="sub">
|
||||
Click a tab. Hover the tiles to see the static→animated swap.
|
||||
Click a tile or the timeline pips to simulate seeking. Press <kbd style="background: var(--bg-2); padding: 1px 5px; border-radius: 3px; border: 1px solid var(--glass-border-strong); font-family: ui-monospace, monospace; font-size: 11px;">N</kbd> while the Scene Detail tab is active to open the create-marker dialog.
|
||||
</div>
|
||||
|
||||
<div class="tabs" role="tablist">
|
||||
<button class="active" data-view="wall">Markers Wall</button>
|
||||
<button data-view="scene">Scene Detail</button>
|
||||
<button data-view="data">Data Model</button>
|
||||
</div>
|
||||
|
||||
<!-- =============== WALL =============== -->
|
||||
<div class="view active" id="view-wall">
|
||||
<div class="toolbar">
|
||||
<input type="text" placeholder="Search markers by tag, scene, or title…">
|
||||
<div class="chips">
|
||||
<span class="chip active">all</span>
|
||||
<span class="chip">doggy</span>
|
||||
<span class="chip">missionary</span>
|
||||
<span class="chip">cowgirl</span>
|
||||
<span class="chip">shower</span>
|
||||
<span class="chip">+12 tags</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="legend">
|
||||
<div class="item"><span class="swatch" style="background: linear-gradient(135deg, #4a3b2a, #2a1f15);"></span>static thumbnail (idle)</div>
|
||||
<div class="item"><span class="swatch" style="background: rgba(248,113,113,0.85);"></span>animated preview (hover)</div>
|
||||
<div class="item"><span class="swatch" style="background: var(--cyan);"></span>click → jump to timestamp</div>
|
||||
</div>
|
||||
|
||||
<div class="grid" id="markers-grid"></div>
|
||||
</div>
|
||||
|
||||
<!-- =============== SCENE DETAIL =============== -->
|
||||
<div class="view" id="view-scene">
|
||||
<div class="scene-detail">
|
||||
<div class="player">
|
||||
<div class="player-content"></div>
|
||||
<div class="player-controls">
|
||||
<div class="timeline-row">
|
||||
<div class="pips" id="pips-row"></div>
|
||||
<div class="seekbar">
|
||||
<div class="progress"></div>
|
||||
<div class="head"></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="player-controls-row">
|
||||
<div class="play-btn">▶</div>
|
||||
<span class="timestamp-display"><span id="cur-ts">14:32</span> / 1:42:18</span>
|
||||
<span class="marker-hint">Press <kbd>N</kbd> to mark this moment</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="scene-info">
|
||||
<h3>Code: <span style="color: var(--cyan);">SSIS-456</span></h3>
|
||||
<div class="scene-meta">part 1 · 1080p · 1h 42m · 5 markers</div>
|
||||
|
||||
<div class="markers-tabs">
|
||||
<button class="active">Markers (5)</button>
|
||||
<button>Cast</button>
|
||||
<button>Tags</button>
|
||||
<button>Files</button>
|
||||
</div>
|
||||
|
||||
<div id="markers-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- =============== DATA MODEL =============== -->
|
||||
<div class="view" id="view-data">
|
||||
<div class="inspector">
|
||||
<h2>What a marker actually is in the database</h2>
|
||||
<pre><span class="com">-- markers table — one row per labeled timestamp</span>
|
||||
CREATE TABLE markers (
|
||||
<span class="key">id</span> INTEGER PRIMARY KEY,
|
||||
<span class="key">image_id</span> INTEGER REFERENCES images(id) ON DELETE CASCADE,
|
||||
<span class="key">part_idx</span> INTEGER NOT NULL DEFAULT 0, <span class="com">-- multi-part videos</span>
|
||||
<span class="key">seconds</span> REAL NOT NULL, <span class="com">-- 872.3 = 14:32</span>
|
||||
<span class="key">end_seconds</span> REAL, <span class="com">-- optional, for ranges</span>
|
||||
<span class="key">title</span> TEXT, <span class="com">-- optional free-text</span>
|
||||
<span class="key">primary_tag_id</span> INTEGER REFERENCES tags(id),
|
||||
<span class="key">created_at</span> INTEGER NOT NULL,
|
||||
<span class="key">updated_at</span> INTEGER NOT NULL
|
||||
);
|
||||
|
||||
<span class="com">-- secondary tags (many-to-many)</span>
|
||||
CREATE TABLE marker_tags (
|
||||
<span class="key">marker_id</span> INTEGER REFERENCES markers(id) ON DELETE CASCADE,
|
||||
<span class="key">tag_id</span> INTEGER REFERENCES tags(id) ON DELETE CASCADE,
|
||||
PRIMARY KEY (marker_id, tag_id)
|
||||
);</pre>
|
||||
</div>
|
||||
|
||||
<div class="inspector">
|
||||
<h2>Example row + derived files on disk</h2>
|
||||
<pre><span class="com">-- row in markers table</span>
|
||||
{
|
||||
<span class="key">id</span>: <span class="num">142</span>,
|
||||
<span class="key">image_id</span>: <span class="num">4711</span>, <span class="com">-- the cover for SSIS-456</span>
|
||||
<span class="key">part_idx</span>: <span class="num">0</span>,
|
||||
<span class="key">seconds</span>: <span class="num">872.3</span>, <span class="com">-- 14:32</span>
|
||||
<span class="key">end_seconds</span>: null,
|
||||
<span class="key">title</span>: <span class="str">"close-up · bedroom"</span>,
|
||||
<span class="key">primary_tag_id</span>: <span class="num">38</span>, <span class="com">-- "doggy"</span>
|
||||
<span class="key">created_at</span>: <span class="num">1735776300000</span>,
|
||||
<span class="key">updated_at</span>: <span class="num">1735776300000</span>
|
||||
}
|
||||
|
||||
<span class="com">-- secondary tags from marker_tags</span>
|
||||
[ <span class="num">12</span> <span class="com">"bedroom"</span>, <span class="num">19</span> <span class="com">"close-up"</span>, <span class="num">22</span> <span class="com">"POV"</span> ]
|
||||
|
||||
<span class="com">-- derived assets generated by ffmpeg, keyed by marker.id</span>
|
||||
data/marker-thumbs/142.webp <span class="com">-- 320×180 static, ~40 KB</span>
|
||||
<span class="com"> ffmpeg -ss 872.3 -i video.mp4 -frames:v 1 -vf scale=320:-1 ...</span>
|
||||
data/marker-previews/142.webp <span class="com">-- 5s animated, no audio, ~280 KB</span>
|
||||
<span class="com"> ffmpeg -ss 872.3 -t 5 -i video.mp4 -vf "scale=320:-1,fps=12" -loop 0 ...</span>
|
||||
|
||||
<span class="com">-- both are caches. delete them and they regenerate on next request.</span>
|
||||
<span class="com">-- generation is queued, not blocking — same pattern as whisperjav.</span></pre>
|
||||
</div>
|
||||
|
||||
<div class="inspector">
|
||||
<h2>Wall tile rendering — how the hover swap works</h2>
|
||||
<pre><div class=<span class="str">"tile"</span>>
|
||||
<div class=<span class="str">"tile-media"</span>>
|
||||
<img src=<span class="str">"/api/marker-thumb/142"</span> <span class="com">// static, always loaded</span>
|
||||
class=<span class="str">"static"</span>>
|
||||
<img src=<span class="str">"/api/marker-preview/142"</span> <span class="com">// animated webp, lazy-loaded on hover</span>
|
||||
class=<span class="str">"animated"</span>
|
||||
loading=<span class="str">"lazy"</span>>
|
||||
<span class=<span class="str">"timestamp"</span>>14:32</span>
|
||||
</div>
|
||||
<div class=<span class="str">"tile-meta"</span>>
|
||||
<span class=<span class="str">"primary-tag"</span>>doggy</span>
|
||||
<span class=<span class="str">"scene"</span>>SSIS-456 · part 1</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<span class="com">// CSS handles the swap purely with hover + opacity transition.</span>
|
||||
<span class="com">// No JS needed for the swap; image fetch is browser-lazy.</span></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Create marker dialog -->
|
||||
<div class="dialog-backdrop" id="dialog-backdrop">
|
||||
<div class="dialog">
|
||||
<h3>
|
||||
Create marker
|
||||
<span class="ts-pill" id="dialog-ts">at 14:32.300</span>
|
||||
</h3>
|
||||
<div class="hint">Snapshot this moment so you can find it again. ffmpeg will queue a thumbnail and a 5s animated preview after save.</div>
|
||||
|
||||
<label>Primary tag (required)</label>
|
||||
<input type="text" id="primary-tag-input" placeholder="Type to search tags…" autocomplete="off" value="doggy">
|
||||
<div class="tag-search-results" id="primary-results" style="display: none;">
|
||||
<div class="opt">doggy</div>
|
||||
<div class="opt">doggy reverse</div>
|
||||
<div class="opt">cowgirl</div>
|
||||
</div>
|
||||
|
||||
<label>Secondary tags (optional)</label>
|
||||
<input type="text" id="secondary-tag-input" placeholder="Add more tags…" autocomplete="off">
|
||||
<div class="selected-tags">
|
||||
<span class="selected-tag primary">doggy <span class="x">×</span></span>
|
||||
<span class="selected-tag">bedroom <span class="x">×</span></span>
|
||||
<span class="selected-tag">close-up <span class="x">×</span></span>
|
||||
<span class="selected-tag">POV <span class="x">×</span></span>
|
||||
</div>
|
||||
|
||||
<label>Title (optional)</label>
|
||||
<input type="text" placeholder="e.g. close-up bedroom" value="close-up · bedroom">
|
||||
|
||||
<label>End time (optional, for ranges)</label>
|
||||
<input type="text" placeholder="leave blank for instant marker" value="">
|
||||
|
||||
<div class="dialog-actions">
|
||||
<button class="btn-cancel" id="dialog-cancel">Cancel</button>
|
||||
<button class="btn-create" id="dialog-create">Create marker</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ============= DATA =============
|
||||
const MARKERS = [
|
||||
{ id: 1, ts: "08:14", seconds: 494, primary: "shower", tags: ["wet", "solo"], scene: "MIDD-993", part: 0, bg: "bg-shower", frames: ["▦", "▣", "▤"] },
|
||||
{ id: 2, ts: "14:32", seconds: 872, primary: "doggy", tags: ["bedroom", "POV"], scene: "SSIS-456", part: 0, bg: "bg-bed", frames: ["◉", "◎", "◍"] },
|
||||
{ id: 3, ts: "23:05", seconds: 1385, primary: "missionary", tags: ["bedroom"], scene: "SSIS-456", part: 0, bg: "bg-bed", frames: ["◐", "◑", "◒"] },
|
||||
{ id: 4, ts: "31:48", seconds: 1908, primary: "cowgirl", tags: ["bedroom", "POV"], scene: "SSIS-456", part: 0, bg: "bg-bed", frames: ["◴", "◵", "◶"] },
|
||||
{ id: 5, ts: "42:09", seconds: 2529, primary: "kitchen", tags: ["standing"], scene: "PRED-123", part: 0, bg: "bg-kitchen", frames: ["◧", "◨", "◩"] },
|
||||
{ id: 6, ts: "55:21", seconds: 3321, primary: "office", tags: ["clothed"], scene: "JUFE-771", part: 0, bg: "bg-office", frames: ["◰", "◱", "◲"] },
|
||||
{ id: 7, ts: "01:02", seconds: 62, primary: "outdoor", tags: ["car", "POV"], scene: "REXD-501", part: 0, bg: "bg-outdoor", frames: ["◇", "◆", "◈"] },
|
||||
{ id: 8, ts: "08:55", seconds: 535, primary: "doggy", tags: ["bathroom", "wet"], scene: "ROOM-013", part: 0, bg: "bg-shower", frames: ["◔", "◕", "●"] },
|
||||
{ id: 9, ts: "17:22", seconds: 1042, primary: "facial", tags: ["close-up"], scene: "TPPN-172", part: 0, bg: "bg-pink", frames: ["✦", "✧", "★"] },
|
||||
{ id: 10, ts: "29:40", seconds: 1780, primary: "cowgirl", tags: ["clothed", "office"], scene: "JUFE-771", part: 0, bg: "bg-office", frames: ["▲", "△", "▴"] },
|
||||
{ id: 11, ts: "11:50", seconds: 710, primary: "blowjob", tags: ["POV"], scene: "USBA-055", part: 0, bg: "bg-purple", frames: ["✸", "✺", "✹"] },
|
||||
{ id: 12, ts: "47:15", seconds: 2835, primary: "missionary", tags: ["bedroom", "lingerie"], scene: "PRED-217", part: 0, bg: "bg-pink", frames: ["✿", "❀", "❁"] },
|
||||
{ id: 13, ts: "33:08", seconds: 1988, primary: "shower", tags: ["solo", "wet"], scene: "HSODA-027", part: 0, bg: "bg-shower", frames: ["◌", "◍", "◎"] },
|
||||
{ id: 14, ts: "01:18", seconds: 78, primary: "outdoor", tags: ["beach", "swimsuit"], scene: "REAL-768", part: 0, bg: "bg-outdoor", frames: ["⬡", "⬢", "⬣"] },
|
||||
{ id: 15, ts: "22:44", seconds: 1364, primary: "doggy", tags: ["bedroom"], scene: "SHKD-889", part: 0, bg: "bg-bed", frames: ["◇", "◈", "◆"] },
|
||||
{ id: 16, ts: "06:33", seconds: 393, primary: "kitchen", tags: ["domestic"], scene: "PKPD-046", part: 0, bg: "bg-kitchen", frames: ["▸", "▹", "►"] },
|
||||
];
|
||||
|
||||
// Markers used in the Scene Detail timeline (for SSIS-456)
|
||||
const SCENE_DURATION = 6138; // 1h 42m 18s = 6138 seconds
|
||||
const SCENE_MARKERS = MARKERS.filter(m => m.scene === "SSIS-456").concat([
|
||||
{ id: 21, ts: "37:51", seconds: 2271, primary: "facial", tags: ["close-up"], scene: "SSIS-456", part: 0, bg: "bg-pink", frames: ["✦", "✧", "★"] },
|
||||
{ id: 22, ts: "59:30", seconds: 3570, primary: "shower", tags: ["wet", "solo"], scene: "SSIS-456", part: 0, bg: "bg-shower", frames: ["◐", "◑", "◒"] },
|
||||
]).sort((a,b) => a.seconds - b.seconds);
|
||||
|
||||
// ============= RENDERING =============
|
||||
function renderTile(m) {
|
||||
return `
|
||||
<div class="tile" data-id="${m.id}">
|
||||
<div class="tile-media">
|
||||
<div class="static ${m.bg}">
|
||||
<div class="frame f1" style="font-size: 36px;">${m.frames[0]}</div>
|
||||
</div>
|
||||
<div class="animated ${m.bg}">
|
||||
<div class="anim-strip">
|
||||
<div class="frame f1" style="font-size: 36px;">${m.frames[0]}</div>
|
||||
<div class="frame f2" style="font-size: 36px;">${m.frames[1]}</div>
|
||||
<div class="frame f3" style="font-size: 36px;">${m.frames[2]}</div>
|
||||
</div>
|
||||
</div>
|
||||
<span class="anim-indicator">● live</span>
|
||||
<span class="timestamp">${m.ts}</span>
|
||||
<div class="play-icon">▶</div>
|
||||
</div>
|
||||
<div class="tile-meta">
|
||||
<div class="tile-tags">
|
||||
<span class="tile-tag primary">${m.primary}</span>
|
||||
${m.tags.map(t => `<span class="tile-tag">${t}</span>`).join("")}
|
||||
</div>
|
||||
<div class="tile-scene"><span class="code">${m.scene}</span> · part ${m.part + 1}</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
document.getElementById("markers-grid").innerHTML = MARKERS.map(renderTile).join("");
|
||||
|
||||
// Scene markers list (rows in the detail tab)
|
||||
function renderRow(m) {
|
||||
return `
|
||||
<div class="marker-row" data-id="${m.id}">
|
||||
<div class="row-thumb ${m.bg}">
|
||||
<div class="frame f1" style="font-size: 22px;">${m.frames[0]}</div>
|
||||
</div>
|
||||
<div class="row-meta">
|
||||
<div class="row-tags">
|
||||
<span class="tile-tag primary">${m.primary}</span>
|
||||
${m.tags.map(t => `<span class="tile-tag">${t}</span>`).join("")}
|
||||
</div>
|
||||
<div class="row-time">${m.ts}.000</div>
|
||||
</div>
|
||||
<div class="row-action">Seek</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
document.getElementById("markers-list").innerHTML = SCENE_MARKERS.map(renderRow).join("");
|
||||
|
||||
// Pips on the timeline (positioned by % of duration)
|
||||
function renderPips() {
|
||||
return SCENE_MARKERS.map(m => {
|
||||
const pct = (m.seconds / SCENE_DURATION) * 100;
|
||||
const isActive = m.ts === "14:32";
|
||||
return `
|
||||
<div class="pip ${isActive ? "active" : ""}" style="left: ${pct.toFixed(2)}%;" data-id="${m.id}">
|
||||
<div class="pip-tooltip" style="left: 50%;">
|
||||
<div class="mini-thumb ${m.bg}">
|
||||
<div class="frame f1" style="font-size: 18px;">${m.frames[0]}</div>
|
||||
</div>
|
||||
<div class="label">
|
||||
<span class="tile-tag primary" style="font-size: 9px; padding: 1px 5px;">${m.primary}</span>
|
||||
<span class="ts">${m.ts}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join("");
|
||||
}
|
||||
document.getElementById("pips-row").innerHTML = renderPips();
|
||||
|
||||
// ============= TAB SWITCHING =============
|
||||
document.querySelectorAll(".tabs button").forEach(btn => {
|
||||
btn.addEventListener("click", () => {
|
||||
document.querySelectorAll(".tabs button").forEach(b => b.classList.remove("active"));
|
||||
document.querySelectorAll(".view").forEach(v => v.classList.remove("active"));
|
||||
btn.classList.add("active");
|
||||
document.getElementById("view-" + btn.dataset.view).classList.add("active");
|
||||
});
|
||||
});
|
||||
|
||||
// ============= CREATE MARKER DIALOG =============
|
||||
const dialogBackdrop = document.getElementById("dialog-backdrop");
|
||||
function openDialog(timestamp) {
|
||||
document.getElementById("dialog-ts").textContent = "at " + timestamp;
|
||||
dialogBackdrop.classList.add("show");
|
||||
}
|
||||
function closeDialog() {
|
||||
dialogBackdrop.classList.remove("show");
|
||||
}
|
||||
document.getElementById("dialog-cancel").addEventListener("click", closeDialog);
|
||||
document.getElementById("dialog-create").addEventListener("click", () => {
|
||||
closeDialog();
|
||||
// simulate a flash on the seekbar
|
||||
const head = document.querySelector(".seekbar .head");
|
||||
if (head) {
|
||||
head.style.boxShadow = "0 0 20px rgba(251,191,36,0.9)";
|
||||
head.style.background = "var(--amber)";
|
||||
setTimeout(() => { head.style.boxShadow = ""; head.style.background = ""; }, 800);
|
||||
}
|
||||
});
|
||||
dialogBackdrop.addEventListener("click", (e) => {
|
||||
if (e.target === dialogBackdrop) closeDialog();
|
||||
});
|
||||
|
||||
// Hotkey: N to open create-marker dialog (only when Scene Detail is active)
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key === "n" || e.key === "N") {
|
||||
const sceneActive = document.getElementById("view-scene").classList.contains("active");
|
||||
if (sceneActive && !dialogBackdrop.classList.contains("show")) {
|
||||
e.preventDefault();
|
||||
openDialog(document.getElementById("cur-ts").textContent + ".300");
|
||||
}
|
||||
}
|
||||
if (e.key === "Escape" && dialogBackdrop.classList.contains("show")) {
|
||||
closeDialog();
|
||||
}
|
||||
});
|
||||
|
||||
// Click a pip → simulate seek (just update the timestamp display + progress)
|
||||
document.querySelectorAll(".pip").forEach(pip => {
|
||||
pip.addEventListener("click", (e) => {
|
||||
e.stopPropagation();
|
||||
document.querySelectorAll(".pip").forEach(p => p.classList.remove("active"));
|
||||
pip.classList.add("active");
|
||||
const m = SCENE_MARKERS.find(x => x.id == pip.dataset.id);
|
||||
if (m) {
|
||||
document.getElementById("cur-ts").textContent = m.ts;
|
||||
const pct = (m.seconds / SCENE_DURATION) * 100;
|
||||
document.querySelector(".seekbar .progress").style.width = pct + "%";
|
||||
document.querySelector(".seekbar .head").style.left = pct + "%";
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Click a marker row → same as clicking its pip
|
||||
document.querySelectorAll(".marker-row").forEach(row => {
|
||||
row.addEventListener("click", () => {
|
||||
const pip = document.querySelector(`.pip[data-id="${row.dataset.id}"]`);
|
||||
if (pip) pip.click();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user