feat: component editor
This commit is contained in:
parent
2319f2549b
commit
04c7310749
|
|
@ -12,6 +12,7 @@
|
||||||
--muted: #8e8e93;
|
--muted: #8e8e93;
|
||||||
--accent: #0a84ff;
|
--accent: #0a84ff;
|
||||||
--invalid: #fafafa;
|
--invalid: #fafafa;
|
||||||
|
--checker: #c8c8c8;
|
||||||
}
|
}
|
||||||
* { box-sizing: border-box; }
|
* { box-sizing: border-box; }
|
||||||
body { margin: 0; font: 13px/1.4 -apple-system, system-ui, sans-serif;
|
body { margin: 0; font: 13px/1.4 -apple-system, system-ui, sans-serif;
|
||||||
|
|
@ -25,7 +26,12 @@
|
||||||
background: var(--accent); color: white;
|
background: var(--accent); color: white;
|
||||||
border: none; border-radius: 6px; cursor: pointer; }
|
border: none; border-radius: 6px; cursor: pointer; }
|
||||||
header button:hover { opacity: 0.9; }
|
header button:hover { opacity: 0.9; }
|
||||||
|
header button.ghost { background: transparent; color: var(--text);
|
||||||
|
border: 1px solid var(--line); }
|
||||||
main { padding: 20px; }
|
main { padding: 20px; }
|
||||||
|
|
||||||
|
/* ---- Matrix view ---- */
|
||||||
|
|
||||||
section { margin-bottom: 32px; }
|
section { margin-bottom: 32px; }
|
||||||
section > h2 { margin: 0 0 12px; font-size: 13px; font-weight: 600;
|
section > h2 { margin: 0 0 12px; font-size: 13px; font-weight: 600;
|
||||||
color: var(--muted); text-transform: uppercase;
|
color: var(--muted); text-transform: uppercase;
|
||||||
|
|
@ -35,19 +41,71 @@
|
||||||
.cell { display: flex; flex-direction: column; align-items: center;
|
.cell { display: flex; flex-direction: column; align-items: center;
|
||||||
gap: 6px; padding: 12px; border: 1px solid var(--line);
|
gap: 6px; padding: 12px; border: 1px solid var(--line);
|
||||||
border-radius: 8px; background: white; min-height: 110px;
|
border-radius: 8px; background: white; min-height: 110px;
|
||||||
justify-content: center; }
|
justify-content: center; cursor: pointer; transition: transform 0.1s, border-color 0.1s; }
|
||||||
.cell.invalid { background: var(--invalid); opacity: 0.4; }
|
.cell:hover { border-color: var(--accent); transform: translateY(-1px); }
|
||||||
.cell .preview { position: relative; width: 24mm; height: 24mm;
|
.cell .preview { position: relative; width: 24mm; height: 24mm;
|
||||||
display: flex; align-items: center; justify-content: center; }
|
display: flex; align-items: center; justify-content: center; }
|
||||||
/* Stage component absolutely inside preview, centered. The component
|
|
||||||
uses absolute positioning, so we offset to center it visually. */
|
|
||||||
.cell .preview sonic-diamond { position: absolute !important;
|
|
||||||
left: 50% !important; top: 50% !important;
|
|
||||||
transform: translate(-50%, -50%) !important; }
|
|
||||||
.cell .label { font-size: 10px; color: var(--muted);
|
.cell .label { font-size: 10px; color: var(--muted);
|
||||||
text-align: center; line-height: 1.3; }
|
text-align: center; line-height: 1.3; }
|
||||||
.cell .key { font-family: ui-monospace, Menlo, monospace;
|
.cell .key { font-family: ui-monospace, Menlo, monospace;
|
||||||
font-size: 9px; color: var(--text); }
|
font-size: 9px; color: var(--text); }
|
||||||
|
|
||||||
|
/* ---- Single view ---- */
|
||||||
|
|
||||||
|
.single-toolbar { display: flex; align-items: center; gap: 16px;
|
||||||
|
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-body { display: grid; grid-template-columns: 280px 1fr; gap: 16px; }
|
||||||
|
.single-controls { background: var(--panel); border: 1px solid var(--line);
|
||||||
|
border-radius: 10px; padding: 16px;
|
||||||
|
display: flex; flex-direction: column; gap: 14px;
|
||||||
|
align-self: start; }
|
||||||
|
.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-row.checkbox { flex-direction: row; align-items: center;
|
||||||
|
gap: 8px; padding-top: 4px; }
|
||||||
|
.ctrl-row.checkbox label { font-size: 13px; color: var(--text);
|
||||||
|
text-transform: none; letter-spacing: 0; }
|
||||||
|
.ctrl-row.checkbox input[disabled] + label { color: var(--muted); }
|
||||||
|
.ctrl-info { margin-top: 12px; padding-top: 12px;
|
||||||
|
border-top: 1px solid var(--line);
|
||||||
|
display: flex; flex-direction: column; gap: 8px;
|
||||||
|
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; padding: 24px;
|
||||||
|
display: flex; align-items: center; justify-content: center; }
|
||||||
|
.single-frame { position: relative;
|
||||||
|
width: 60mm; height: 60mm;
|
||||||
|
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 solid var(--line); border-radius: 4px; }
|
||||||
|
|
||||||
|
/* ---- Component centering inside any preview frame ---- */
|
||||||
|
|
||||||
|
.cell .preview sonic-diamond,
|
||||||
|
.single-frame sonic-diamond {
|
||||||
|
position: absolute !important;
|
||||||
|
left: 50% !important;
|
||||||
|
top: 50% !important;
|
||||||
|
transform: translate(-50%, -50%) !important;
|
||||||
|
}
|
||||||
|
|
||||||
#toast { position: fixed; bottom: 20px; left: 50%;
|
#toast { position: fixed; bottom: 20px; left: 50%;
|
||||||
transform: translateX(-50%); padding: 10px 20px;
|
transform: translateX(-50%); padding: 10px 20px;
|
||||||
background: var(--text); color: white; border-radius: 8px;
|
background: var(--text); color: white; border-radius: 8px;
|
||||||
|
|
@ -64,7 +122,42 @@
|
||||||
<span style="flex: 1"></span>
|
<span style="flex: 1"></span>
|
||||||
<button id="btn-copy">Copy snapshot</button>
|
<button id="btn-copy">Copy snapshot</button>
|
||||||
</header>
|
</header>
|
||||||
<main id="main"></main>
|
|
||||||
|
<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>
|
||||||
|
<div class="single-body">
|
||||||
|
<aside class="single-controls">
|
||||||
|
<div class="ctrl-row">
|
||||||
|
<label for="ctrl-shape">Shape</label>
|
||||||
|
<select id="ctrl-shape"></select>
|
||||||
|
</div>
|
||||||
|
<div class="ctrl-row">
|
||||||
|
<label for="ctrl-color">Color</label>
|
||||||
|
<select id="ctrl-color"></select>
|
||||||
|
</div>
|
||||||
|
<div class="ctrl-row checkbox">
|
||||||
|
<input type="checkbox" id="ctrl-chipped">
|
||||||
|
<label for="ctrl-chipped" id="ctrl-chipped-label">Chipped</label>
|
||||||
|
</div>
|
||||||
|
<div class="ctrl-info">
|
||||||
|
<div>baseSize: <code id="info-basesize"></code></div>
|
||||||
|
<div>origin: <code id="info-origin"></code></div>
|
||||||
|
<div>image: <code id="info-img"></code></div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
<main class="single-stage">
|
||||||
|
<div class="single-frame" id="single-frame"></div>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
<div id="toast">Snapshot copied</div>
|
<div id="toast">Snapshot copied</div>
|
||||||
<script type="module" src="./diamond.editor.mjs"></script>
|
<script type="module" src="./diamond.editor.mjs"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,18 @@
|
||||||
/**
|
/**
|
||||||
* Component-editor for sonic-diamond.
|
* Component-editor for sonic-diamond.
|
||||||
*
|
*
|
||||||
* MVP: matrix view only. Diamonds need no per-variant tuning (all images
|
* Modes:
|
||||||
* are pre-aligned), so single-mode and anchor editing are not implemented.
|
* - Matrix (default): grid of all valid preset combinations. Click → Single.
|
||||||
* The "Copy snapshot" button still works — it produces the current
|
* - Single: one component instance with dropdown/checkbox controls and
|
||||||
* `anchorsDefault` and `variants` ready to paste between marker comments.
|
* a checker-pattern preview frame. Diamond has no per-variant tuning,
|
||||||
|
* so this mode here only demonstrates the prop-switching flow.
|
||||||
|
*
|
||||||
|
* Mode is driven by the URL hash:
|
||||||
|
* '' → Matrix
|
||||||
|
* 'shape=...&color=...&chipped=...' → Single
|
||||||
|
*
|
||||||
|
* "Copy snapshot" works in either mode and produces JS source ready to
|
||||||
|
* paste between `@editor:anchors-*` and `@editor:variants-*` markers.
|
||||||
*/
|
*/
|
||||||
await customElements.whenDefined('sonic-diamond');
|
await customElements.whenDefined('sonic-diamond');
|
||||||
|
|
||||||
|
|
@ -14,9 +22,10 @@ const { presetProps, chippedShapes } = Comp;
|
||||||
document.getElementById('meta').textContent =
|
document.getElementById('meta').textContent =
|
||||||
`${Object.keys(Comp.variants).length} variants · baseSize ${Comp.baseSize.w}×${Comp.baseSize.h}mm · origin (${Comp.origin.x}, ${Comp.origin.y})`;
|
`${Object.keys(Comp.variants).length} variants · baseSize ${Comp.baseSize.w}×${Comp.baseSize.h}mm · origin (${Comp.origin.x}, ${Comp.origin.y})`;
|
||||||
|
|
||||||
const main = document.getElementById('main');
|
initMatrix();
|
||||||
renderGrid('Full', false);
|
initSingle();
|
||||||
renderGrid('Chipped', true);
|
window.addEventListener('hashchange', route);
|
||||||
|
route();
|
||||||
|
|
||||||
document.getElementById('btn-copy').addEventListener('click', async () => {
|
document.getElementById('btn-copy').addEventListener('click', async () => {
|
||||||
const text = serializeSnapshot(Comp.anchorsDefault, Comp.variants);
|
const text = serializeSnapshot(Comp.anchorsDefault, Comp.variants);
|
||||||
|
|
@ -24,7 +33,37 @@ document.getElementById('btn-copy').addEventListener('click', async () => {
|
||||||
showToast('Snapshot copied');
|
showToast('Snapshot copied');
|
||||||
});
|
});
|
||||||
|
|
||||||
function renderGrid(title, chipped) {
|
// ---- Routing ----
|
||||||
|
|
||||||
|
function route() {
|
||||||
|
const params = new URLSearchParams(location.hash.slice(1));
|
||||||
|
const matrixView = document.getElementById('matrix-view');
|
||||||
|
const singleView = document.getElementById('single-view');
|
||||||
|
if (params.has('shape')) {
|
||||||
|
matrixView.hidden = true;
|
||||||
|
singleView.hidden = false;
|
||||||
|
applySingle(params);
|
||||||
|
} else {
|
||||||
|
matrixView.hidden = false;
|
||||||
|
singleView.hidden = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setHashParams(updates) {
|
||||||
|
const params = new URLSearchParams(location.hash.slice(1));
|
||||||
|
for (const [k, v] of Object.entries(updates)) params.set(k, String(v));
|
||||||
|
location.hash = params.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Matrix mode ----
|
||||||
|
|
||||||
|
function initMatrix() {
|
||||||
|
const root = document.getElementById('matrix-view');
|
||||||
|
renderGrid(root, 'Full', false);
|
||||||
|
renderGrid(root, 'Chipped', true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGrid(root, title, chipped) {
|
||||||
const shapes = chipped
|
const shapes = chipped
|
||||||
? presetProps.shape.filter(s => chippedShapes.has(s))
|
? presetProps.shape.filter(s => chippedShapes.has(s))
|
||||||
: presetProps.shape;
|
: presetProps.shape;
|
||||||
|
|
@ -46,12 +85,16 @@ function renderGrid(title, chipped) {
|
||||||
}
|
}
|
||||||
|
|
||||||
section.appendChild(grid);
|
section.appendChild(grid);
|
||||||
main.appendChild(section);
|
root.appendChild(section);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeCell(shape, color, chipped) {
|
function makeCell(shape, color, chipped) {
|
||||||
const cell = document.createElement('div');
|
const cell = document.createElement('div');
|
||||||
cell.className = 'cell';
|
cell.className = 'cell';
|
||||||
|
cell.title = 'Open in single mode';
|
||||||
|
cell.addEventListener('click', () => {
|
||||||
|
location.hash = `shape=${shape}&color=${color}&chipped=${chipped}`;
|
||||||
|
});
|
||||||
|
|
||||||
const preview = document.createElement('div');
|
const preview = document.createElement('div');
|
||||||
preview.className = 'preview';
|
preview.className = 'preview';
|
||||||
|
|
@ -76,6 +119,71 @@ function makeCell(shape, color, chipped) {
|
||||||
return cell;
|
return cell;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Single mode ----
|
||||||
|
|
||||||
|
let singleInstance = null;
|
||||||
|
|
||||||
|
function initSingle() {
|
||||||
|
fillSelect('ctrl-shape', presetProps.shape);
|
||||||
|
fillSelect('ctrl-color', presetProps.color);
|
||||||
|
|
||||||
|
document.getElementById('ctrl-shape').addEventListener('change', e =>
|
||||||
|
setHashParams({ shape: e.target.value })
|
||||||
|
);
|
||||||
|
document.getElementById('ctrl-color').addEventListener('change', e =>
|
||||||
|
setHashParams({ color: e.target.value })
|
||||||
|
);
|
||||||
|
document.getElementById('ctrl-chipped').addEventListener('change', e =>
|
||||||
|
setHashParams({ chipped: e.target.checked })
|
||||||
|
);
|
||||||
|
document.getElementById('btn-back').addEventListener('click', () => {
|
||||||
|
location.hash = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function applySingle(params) {
|
||||||
|
if (!singleInstance) {
|
||||||
|
singleInstance = document.createElement('sonic-diamond');
|
||||||
|
document.getElementById('single-frame').appendChild(singleInstance);
|
||||||
|
}
|
||||||
|
|
||||||
|
const shape = params.get('shape') ?? presetProps.shape[0];
|
||||||
|
const color = params.get('color') ?? presetProps.color[0];
|
||||||
|
const chipped = params.get('chipped') === 'true';
|
||||||
|
|
||||||
|
singleInstance.setAttribute('shape', shape);
|
||||||
|
singleInstance.setAttribute('color', color);
|
||||||
|
singleInstance.setAttribute('chipped', String(chipped));
|
||||||
|
|
||||||
|
document.getElementById('ctrl-shape').value = shape;
|
||||||
|
document.getElementById('ctrl-color').value = color;
|
||||||
|
const cb = document.getElementById('ctrl-chipped');
|
||||||
|
const cbLabel = document.getElementById('ctrl-chipped-label');
|
||||||
|
const supportsChipped = chippedShapes.has(shape);
|
||||||
|
cb.checked = chipped && supportsChipped;
|
||||||
|
cb.disabled = !supportsChipped;
|
||||||
|
cbLabel.textContent = supportsChipped ? 'Chipped' : 'Chipped (n/a for this shape)';
|
||||||
|
|
||||||
|
const key = singleInstance._variantKey();
|
||||||
|
document.getElementById('info-variant').textContent = key;
|
||||||
|
document.getElementById('info-basesize').textContent = `${Comp.baseSize.w}×${Comp.baseSize.h}mm`;
|
||||||
|
document.getElementById('info-origin').textContent = `(${Comp.origin.x}, ${Comp.origin.y})`;
|
||||||
|
document.getElementById('info-img').textContent = Comp.variants[key]?.img ?? '—';
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillSelect(id, options) {
|
||||||
|
const sel = document.getElementById(id);
|
||||||
|
sel.innerHTML = '';
|
||||||
|
for (const opt of options) {
|
||||||
|
const o = document.createElement('option');
|
||||||
|
o.value = opt;
|
||||||
|
o.textContent = opt;
|
||||||
|
sel.appendChild(o);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Snapshot serialization ----
|
||||||
|
|
||||||
function serializeSnapshot(anchorsDefault, variants) {
|
function serializeSnapshot(anchorsDefault, variants) {
|
||||||
const lines = [];
|
const lines = [];
|
||||||
lines.push(' // @editor:anchors-start');
|
lines.push(' // @editor:anchors-start');
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import { FunPenComponent } from '/src/components/base.mjs';
|
import { FunPenComponent } from '/src/components/base.mjs';
|
||||||
|
|
||||||
class SonicDiamond extends FunPenComponent {
|
class SonicDiamond extends FunPenComponent {
|
||||||
static origin = { x: 9, y: 9 };
|
static origin = { x: 5, y: 5 };
|
||||||
static baseSize = { w: 18, h: 18 };
|
static baseSize = { w: 10, h: 10 };
|
||||||
|
|
||||||
static presetProps = {
|
static presetProps = {
|
||||||
shape: ['classic', 'elongated', 'flat', 'round', 'raw'],
|
shape: ['classic', 'elongated', 'flat', 'round', 'raw'],
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue