394 lines
19 KiB
HTML
394 lines
19 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<title>Component Editor — sonic-conduit</title>
|
||
<style>
|
||
:root {
|
||
--bg: #f5f5f7;
|
||
--panel: #ffffff;
|
||
--line: #e5e5ea;
|
||
--text: #1c1c1e;
|
||
--muted: #8e8e93;
|
||
--accent: #0a84ff;
|
||
--warn: #ff9500;
|
||
--checker: #c8c8c8;
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body { margin: 0; font: 13px/1.4 -apple-system, system-ui, sans-serif;
|
||
background: var(--bg); color: var(--text); }
|
||
header { position: sticky; top: 0; z-index: 10;
|
||
background: var(--panel); border-bottom: 1px solid var(--line);
|
||
padding: 12px 20px; display: flex; align-items: center; gap: 16px; }
|
||
header h1 { margin: 0; font-size: 15px; font-weight: 600; }
|
||
header .meta { color: var(--muted); font-size: 12px; }
|
||
header button { font: inherit; padding: 6px 14px;
|
||
background: var(--accent); color: white;
|
||
border: none; border-radius: 6px; cursor: pointer; }
|
||
header button.ghost { background: transparent; color: var(--text);
|
||
border: 1px solid var(--line); }
|
||
main { padding: 20px; }
|
||
|
||
/* ---- Matrix view ---- */
|
||
section { margin-bottom: 32px; }
|
||
section > h2 { margin: 0 0 12px; font-size: 13px; font-weight: 600;
|
||
color: var(--muted); text-transform: uppercase;
|
||
letter-spacing: 0.5px; }
|
||
.grid { display: grid; gap: 12px; background: var(--panel);
|
||
padding: 16px; border-radius: 10px; border: 1px solid var(--line); }
|
||
.cell { display: flex; flex-direction: column; align-items: center;
|
||
gap: 6px; padding: 12px; border: 1px solid var(--line);
|
||
border-radius: 8px; background: white; min-height: 110px;
|
||
justify-content: center; cursor: pointer;
|
||
transition: transform 0.1s, border-color 0.1s; }
|
||
.cell:hover { border-color: var(--accent); transform: translateY(-1px); }
|
||
.cell .preview { position: relative; width: 60mm; height: 24mm;
|
||
display: flex; align-items: center; justify-content: center; }
|
||
.cell .label { font-size: 10px; color: var(--muted);
|
||
text-align: center; line-height: 1.3; }
|
||
.cell .key { font-family: ui-monospace, Menlo, monospace;
|
||
font-size: 9px; color: var(--text); }
|
||
|
||
/* ---- Single view ---- */
|
||
.single-toolbar { display: flex; align-items: center; gap: 16px; flex-wrap: wrap;
|
||
padding: 12px 16px; background: var(--panel);
|
||
border: 1px solid var(--line); border-radius: 10px;
|
||
margin-bottom: 16px; }
|
||
.single-toolbar code { background: var(--bg); padding: 2px 6px;
|
||
border-radius: 4px; font-size: 12px; }
|
||
.single-toolbar label { display: inline-flex; align-items: center; gap: 6px;
|
||
font-size: 12px; color: var(--text); cursor: pointer; }
|
||
.single-toolbar .group { display: flex; align-items: center; gap: 6px;
|
||
padding-left: 12px; border-left: 1px solid var(--line); }
|
||
.single-toolbar .group input[type=range] { width: 120px; }
|
||
.single-toolbar .group select { font: inherit; padding: 4px 8px;
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
background: white; }
|
||
.single-toolbar .group .num { font-family: ui-monospace, Menlo, monospace;
|
||
font-size: 11px; color: var(--muted);
|
||
min-width: 38px; text-align: right; }
|
||
.single-toolbar .group button { font: inherit; padding: 4px 8px;
|
||
background: white; color: var(--text);
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
cursor: pointer; }
|
||
.single-toolbar .group button:hover { border-color: var(--accent); }
|
||
|
||
.draft-banner { padding: 8px 14px; background: #fff8e1;
|
||
border: 1px solid #ffe082; border-radius: 8px;
|
||
margin-bottom: 12px; font-size: 12px;
|
||
display: flex; align-items: center; gap: 12px; }
|
||
.draft-banner button { font: inherit; padding: 4px 10px;
|
||
background: white; border: 1px solid #ffb300;
|
||
border-radius: 4px; cursor: pointer;
|
||
color: #c77700; }
|
||
.single-body { display: grid; grid-template-columns: 320px 1fr; gap: 16px; align-items: start; }
|
||
.single-controls { background: var(--panel); border: 1px solid var(--line);
|
||
border-radius: 10px; padding: 16px;
|
||
display: flex; flex-direction: column; gap: 14px; }
|
||
.ctrl-row { display: flex; flex-direction: column; gap: 4px; }
|
||
.ctrl-row > label { font-size: 11px; color: var(--muted);
|
||
text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.ctrl-row select { font: inherit; padding: 6px 8px;
|
||
border: 1px solid var(--line); border-radius: 6px;
|
||
background: white; }
|
||
.ctrl-divider { border-top: 1px solid var(--line); margin: 4px 0; }
|
||
|
||
.slider-row { display: grid; grid-template-columns: 40px 1fr 60px;
|
||
gap: 8px; align-items: center; }
|
||
.slider-row > .name { font-size: 11px; color: var(--muted);
|
||
text-transform: uppercase; letter-spacing: 0.5px; }
|
||
.slider-row input[type=range] { width: 100%; }
|
||
.slider-row input[type=number] { font: inherit; padding: 4px 6px;
|
||
border: 1px solid var(--line); border-radius: 4px;
|
||
width: 100%; font-family: ui-monospace, Menlo, monospace;
|
||
font-size: 11px; }
|
||
|
||
.anchor-list { display: flex; flex-direction: column; gap: 4px; max-height: 280px; overflow-y: auto; }
|
||
.anchor-row { display: grid; grid-template-columns: 60px 1fr 1fr;
|
||
gap: 6px; align-items: center; padding: 4px 6px;
|
||
border-radius: 4px; font-family: ui-monospace, Menlo, monospace;
|
||
font-size: 11px; }
|
||
.anchor-row:hover { background: var(--bg); }
|
||
.anchor-row .name { color: var(--text); font-weight: 600; }
|
||
.anchor-row .coord { color: var(--muted); }
|
||
.anchor-row.overridden .name { color: var(--warn); }
|
||
.anchor-row.active { background: #e3f2ff; }
|
||
|
||
.ctrl-info { padding-top: 12px; border-top: 1px solid var(--line);
|
||
display: flex; flex-direction: column; gap: 6px;
|
||
font-size: 11px; color: var(--muted); }
|
||
.ctrl-info code { font-family: ui-monospace, Menlo, monospace;
|
||
font-size: 10px; color: var(--text);
|
||
word-break: break-all; }
|
||
|
||
.single-stage { background: var(--panel); border: 1px solid var(--line);
|
||
border-radius: 10px;
|
||
overflow: auto;
|
||
padding: 28px 16px 16px;
|
||
min-height: 500px; max-height: 80vh; }
|
||
/* scene-zoom: explicit dimensions equal to scaled-visual size so the
|
||
parent's scrollable area matches what's actually rendered.
|
||
The actual `transform: scale()` is applied to .scene, not here. */
|
||
.scene-zoom { display: inline-block; position: relative; }
|
||
.scene-zoom .scene-label { position: absolute; top: -22px; left: 0;
|
||
font-size: 10px; color: var(--muted);
|
||
font-family: ui-monospace, Menlo, monospace;
|
||
pointer-events: none; white-space: nowrap; }
|
||
.scene { transform-origin: 0 0; }
|
||
.scene { position: relative;
|
||
width: 210mm; height: 297mm;
|
||
background-color: white;
|
||
background-image:
|
||
linear-gradient(45deg, var(--checker) 25%, transparent 25%),
|
||
linear-gradient(-45deg, var(--checker) 25%, transparent 25%),
|
||
linear-gradient(45deg, transparent 75%, var(--checker) 75%),
|
||
linear-gradient(-45deg, transparent 75%, var(--checker) 75%);
|
||
background-size: 4mm 4mm;
|
||
background-position: 0 0, 0 2mm, 2mm -2mm, -2mm 0;
|
||
border: 1px dashed var(--accent); border-radius: 1px; }
|
||
.scene-grid { position: absolute; inset: 0; pointer-events: none; z-index: 5; }
|
||
.scene-grid .grid-line { position: absolute; top: 0; bottom: 0;
|
||
width: 1px;
|
||
background: rgba(10, 132, 255, 0.35); }
|
||
|
||
/* Matrix cells: component centered visually */
|
||
.cell .preview sonic-conduit {
|
||
position: absolute !important;
|
||
left: 50% !important;
|
||
top: 50% !important;
|
||
transform: translate(-50%, -50%) !important;
|
||
}
|
||
|
||
/* Single mode: component at fixed point near A4 top-center.
|
||
Editable instance is interactive; overlay instance is below it. */
|
||
.scene sonic-conduit {
|
||
position: absolute !important;
|
||
left: 50% !important;
|
||
top: 50mm !important;
|
||
transform: translate(-50%, -50%) !important;
|
||
}
|
||
.scene sonic-conduit.editable {
|
||
cursor: grab; z-index: 4;
|
||
}
|
||
.scene sonic-conduit.editable:focus { outline: 2px solid var(--accent); outline-offset: 4px; }
|
||
.scene sonic-conduit.editable.dragging { cursor: grabbing; }
|
||
/* Overlay sits ABOVE the editable so its silhouette can be compared
|
||
on top. pointer-events: none lets clicks reach the editable below. */
|
||
.scene sonic-conduit.overlay-instance {
|
||
pointer-events: none;
|
||
z-index: 7;
|
||
}
|
||
|
||
/* Anchor overlay: aligned to component center (same point) */
|
||
.anchor-overlay { position: absolute;
|
||
left: 50%; top: 50mm;
|
||
transform: translate(-50%, -50%);
|
||
pointer-events: none;
|
||
outline: 1px dashed rgba(10, 132, 255, 0.3);
|
||
z-index: 10; }
|
||
.anchor-overlay.defaults-mode .anchor-handle:not(.overridden)::before,
|
||
.anchor-overlay.defaults-mode .anchor-handle:not(.overridden)::after {
|
||
background: #34c759;
|
||
}
|
||
.anchor-overlay.defaults-mode .anchor-handle.overridden {
|
||
opacity: 0.45;
|
||
}
|
||
|
||
.origin-marker { position: absolute;
|
||
width: 16px; height: 16px;
|
||
margin-left: -8px; margin-top: -8px;
|
||
pointer-events: auto;
|
||
cursor: grab;
|
||
transition: transform 0.1s; }
|
||
.origin-marker:hover { transform: scale(1.4); }
|
||
.origin-marker.dragging { transform: scale(1.6); cursor: grabbing; }
|
||
.origin-marker::before, .origin-marker::after {
|
||
content: ''; position: absolute; background: #ff3b30;
|
||
}
|
||
.origin-marker::before { left: 0; right: 0; top: 50%; height: 2px; margin-top: -1px; }
|
||
.origin-marker::after { top: 0; bottom: 0; left: 50%; width: 2px; margin-left: -1px; }
|
||
.origin-marker .lbl { position: absolute; left: 18px; top: -4px;
|
||
font: 9px ui-monospace, Menlo, monospace;
|
||
color: #ff3b30; white-space: nowrap; }
|
||
|
||
/* Guides — global, span the entire scene, draggable, scaled with zoom */
|
||
.guide { position: absolute; pointer-events: auto; z-index: 8; }
|
||
.guide-v { top: 0; bottom: 0; width: 1px;
|
||
background: rgba(255, 149, 0, 0.7); cursor: ew-resize; }
|
||
.guide-h { left: 0; right: 0; height: 1px;
|
||
background: rgba(255, 149, 0, 0.7); cursor: ns-resize; }
|
||
/* Bigger hit area via pseudo-element */
|
||
.guide-v::before { content: ''; position: absolute; top: 0; bottom: 0;
|
||
left: -3px; width: 7px; }
|
||
.guide-h::before { content: ''; position: absolute; left: 0; right: 0;
|
||
top: -3px; height: 7px; }
|
||
.guide-remove { position: absolute; padding: 0;
|
||
width: 16px; height: 16px;
|
||
background: rgba(255, 149, 0, 0.9); color: white;
|
||
border: none; border-radius: 50%;
|
||
cursor: pointer; font: 11px sans-serif; line-height: 1;
|
||
opacity: 0; transition: opacity 0.1s; }
|
||
.guide-v .guide-remove { right: -8px; top: 8px; }
|
||
.guide-h .guide-remove { right: 8px; top: -8px; }
|
||
.guide:hover .guide-remove { opacity: 1; }
|
||
.guide-coord { position: absolute;
|
||
background: rgba(255, 149, 0, 0.9); color: white;
|
||
padding: 1px 4px; border-radius: 3px;
|
||
font: 9px ui-monospace, Menlo, monospace;
|
||
pointer-events: none; }
|
||
.guide-v .guide-coord { left: 2px; top: 4px; }
|
||
.guide-h .guide-coord { top: 2px; left: 4px; }
|
||
/* Slot handles: transparent with a small × so the image beneath
|
||
remains visible. Hit area is the 14px wrapper; visual is the cross. */
|
||
.anchor-handle { position: absolute;
|
||
width: 14px; height: 14px;
|
||
margin-left: -7px; margin-top: -7px;
|
||
background: transparent;
|
||
cursor: grab;
|
||
pointer-events: auto;
|
||
transition: transform 0.1s; }
|
||
.anchor-handle::before, .anchor-handle::after {
|
||
content: ''; position: absolute;
|
||
background: var(--accent);
|
||
}
|
||
.anchor-handle::before { /* horizontal arm */
|
||
left: 25%; right: 25%; top: 50%; height: 1.5px; margin-top: -0.75px;
|
||
}
|
||
.anchor-handle::after { /* vertical arm */
|
||
top: 25%; bottom: 25%; left: 50%; width: 1.5px; margin-left: -0.75px;
|
||
}
|
||
.anchor-handle:hover { transform: scale(1.6); }
|
||
.anchor-handle.dragging { cursor: grabbing; transform: scale(2); }
|
||
.anchor-handle.overridden::before,
|
||
.anchor-handle.overridden::after { background: var(--warn); }
|
||
.anchor-handle.active { outline: 1px dashed var(--accent); outline-offset: 1px; }
|
||
.anchor-handle .lbl { position: absolute; left: 14px; top: 14px;
|
||
background: rgba(0,0,0,0.7); color: white;
|
||
padding: 1px 4px; border-radius: 3px;
|
||
font: 9px ui-monospace, Menlo, monospace;
|
||
white-space: nowrap; pointer-events: none;
|
||
opacity: 0; transition: opacity 0.1s; }
|
||
.anchor-handle:hover .lbl,
|
||
.anchor-handle.active .lbl,
|
||
.anchor-handle.dragging .lbl { opacity: 1; }
|
||
|
||
#toast { position: fixed; bottom: 20px; left: 50%;
|
||
transform: translateX(-50%); padding: 10px 20px;
|
||
background: var(--text); color: white; border-radius: 8px;
|
||
opacity: 0; pointer-events: none; transition: opacity 0.2s;
|
||
font-size: 12px; }
|
||
#toast.show { opacity: 1; }
|
||
</style>
|
||
<script type="module" src="./conduit.mjs"></script>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>sonic-conduit</h1>
|
||
<span class="meta" id="meta"></span>
|
||
<span style="flex: 1"></span>
|
||
<button id="btn-save" title="Save snapshot to disk for Claude to apply">Save</button>
|
||
<button id="btn-copy" class="ghost" style="background: white; color: var(--text); border: 1px solid var(--line)" title="Copy snapshot to clipboard (manual fallback)">Copy</button>
|
||
</header>
|
||
|
||
<main>
|
||
<div id="matrix-view"></div>
|
||
|
||
<div id="single-view" hidden>
|
||
<div class="single-toolbar">
|
||
<button class="ghost" id="btn-back">← Matrix</button>
|
||
<span>Variant: <code id="info-variant"></code></span>
|
||
<div class="group">
|
||
<label for="ctrl-zoom">Zoom</label>
|
||
<input type="range" id="ctrl-zoom" min="0.25" max="5" step="0.05" value="2">
|
||
<span class="num" id="zoom-label">200%</span>
|
||
</div>
|
||
<div class="group">
|
||
<label for="ctrl-grid">Grid</label>
|
||
<select id="ctrl-grid">
|
||
<option value="0">off</option>
|
||
<option value="2">2 cols</option>
|
||
<option value="3">3 cols</option>
|
||
<option value="4">4 cols</option>
|
||
</select>
|
||
</div>
|
||
<div class="group">
|
||
<label>Guides</label>
|
||
<button id="btn-guide-v">+ V</button>
|
||
<button id="btn-guide-h">+ H</button>
|
||
</div>
|
||
<div class="group">
|
||
<label for="ctrl-overlay-variant">Overlay</label>
|
||
<select id="ctrl-overlay-variant">
|
||
<option value="">none</option>
|
||
</select>
|
||
<input type="range" id="ctrl-overlay-opacity" min="0" max="1" step="0.05" value="0.5">
|
||
<span class="num" id="overlay-opacity-label">50%</span>
|
||
</div>
|
||
<span style="flex: 1"></span>
|
||
<button id="btn-reset" title="Reset dx/dy/scale and clear anchor overrides for this variant">Reset variant</button>
|
||
<label>
|
||
<input type="checkbox" id="ctrl-edit-defaults">
|
||
Edit anchor defaults
|
||
</label>
|
||
</div>
|
||
<div class="draft-banner" id="draft-banner" hidden>
|
||
<span>Draft restored from local storage. Edits since last reload will be re-applied.</span>
|
||
<span style="flex: 1"></span>
|
||
<button id="btn-discard-draft">Discard draft</button>
|
||
</div>
|
||
<div class="single-body">
|
||
<aside class="single-controls">
|
||
<div class="ctrl-row">
|
||
<label for="ctrl-type">Type</label>
|
||
<select id="ctrl-type"></select>
|
||
</div>
|
||
<div class="ctrl-row">
|
||
<label for="ctrl-state">State</label>
|
||
<select id="ctrl-state"></select>
|
||
</div>
|
||
|
||
<div class="ctrl-divider"></div>
|
||
<div class="ctrl-row"><label>Image position (per variant)</label></div>
|
||
<div class="slider-row">
|
||
<span class="name">DX</span>
|
||
<input type="range" id="ctrl-dx" min="-15" max="15" step="0.1" value="0">
|
||
<input type="number" id="ctrl-dx-num" min="-15" max="15" step="0.1" value="0">
|
||
</div>
|
||
<div class="slider-row">
|
||
<span class="name">DY</span>
|
||
<input type="range" id="ctrl-dy" min="-15" max="15" step="0.1" value="0">
|
||
<input type="number" id="ctrl-dy-num" min="-15" max="15" step="0.1" value="0">
|
||
</div>
|
||
<div class="slider-row">
|
||
<span class="name">Scale</span>
|
||
<input type="range" id="ctrl-scale" min="0.5" max="2" step="0.01" value="1">
|
||
<input type="number" id="ctrl-scale-num" min="0.5" max="2" step="0.01" value="1">
|
||
</div>
|
||
|
||
<div class="ctrl-divider"></div>
|
||
<div class="ctrl-row"><label>Anchors (10 slots) — drag handles on stage</label></div>
|
||
<div class="anchor-list" id="anchor-list"></div>
|
||
|
||
<div class="ctrl-info">
|
||
<div>baseSize: <code id="info-basesize"></code></div>
|
||
<div>image: <code id="info-img"></code></div>
|
||
</div>
|
||
</aside>
|
||
<main class="single-stage" id="single-stage">
|
||
<div class="scene-zoom" id="scene-zoom">
|
||
<span class="scene-label">A4 · 210 × 297 mm</span>
|
||
<div class="scene" id="scene">
|
||
<div class="scene-grid" id="scene-grid"></div>
|
||
<div class="anchor-overlay" id="anchor-overlay"></div>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
</div>
|
||
</div>
|
||
</main>
|
||
|
||
<div id="toast">Snapshot copied</div>
|
||
<script type="module" src="./conduit.editor.mjs"></script>
|
||
</body>
|
||
</html>
|