feat: component editor
This commit is contained in:
@@ -1,10 +1,18 @@
|
||||
/**
|
||||
* Component-editor for sonic-diamond.
|
||||
*
|
||||
* MVP: matrix view only. Diamonds need no per-variant tuning (all images
|
||||
* are pre-aligned), so single-mode and anchor editing are not implemented.
|
||||
* The "Copy snapshot" button still works — it produces the current
|
||||
* `anchorsDefault` and `variants` ready to paste between marker comments.
|
||||
* Modes:
|
||||
* - Matrix (default): grid of all valid preset combinations. Click → Single.
|
||||
* - Single: one component instance with dropdown/checkbox controls and
|
||||
* 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');
|
||||
|
||||
@@ -14,9 +22,10 @@ const { presetProps, chippedShapes } = Comp;
|
||||
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})`;
|
||||
|
||||
const main = document.getElementById('main');
|
||||
renderGrid('Full', false);
|
||||
renderGrid('Chipped', true);
|
||||
initMatrix();
|
||||
initSingle();
|
||||
window.addEventListener('hashchange', route);
|
||||
route();
|
||||
|
||||
document.getElementById('btn-copy').addEventListener('click', async () => {
|
||||
const text = serializeSnapshot(Comp.anchorsDefault, Comp.variants);
|
||||
@@ -24,7 +33,37 @@ document.getElementById('btn-copy').addEventListener('click', async () => {
|
||||
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
|
||||
? presetProps.shape.filter(s => chippedShapes.has(s))
|
||||
: presetProps.shape;
|
||||
@@ -46,12 +85,16 @@ function renderGrid(title, chipped) {
|
||||
}
|
||||
|
||||
section.appendChild(grid);
|
||||
main.appendChild(section);
|
||||
root.appendChild(section);
|
||||
}
|
||||
|
||||
function makeCell(shape, color, chipped) {
|
||||
const cell = document.createElement('div');
|
||||
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');
|
||||
preview.className = 'preview';
|
||||
@@ -76,6 +119,71 @@ function makeCell(shape, color, chipped) {
|
||||
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) {
|
||||
const lines = [];
|
||||
lines.push(' // @editor:anchors-start');
|
||||
|
||||
Reference in New Issue
Block a user