math-tasks/assets/themes/sonic/components/conduit/conduit.editor.html

394 lines
19 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html 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>