From 04c7310749f571c107f5c8b28b3810660b290533 Mon Sep 17 00:00:00 2001 From: Oleg Proskurin Date: Thu, 30 Apr 2026 15:14:02 +0700 Subject: [PATCH] feat: component editor --- .../components/diamond/diamond.editor.html | 109 +++++++++++++-- .../components/diamond/diamond.editor.mjs | 126 ++++++++++++++++-- .../sonic/components/diamond/diamond.mjs | 4 +- 3 files changed, 220 insertions(+), 19 deletions(-) diff --git a/assets/themes/sonic/components/diamond/diamond.editor.html b/assets/themes/sonic/components/diamond/diamond.editor.html index ccd0efc..beab263 100644 --- a/assets/themes/sonic/components/diamond/diamond.editor.html +++ b/assets/themes/sonic/components/diamond/diamond.editor.html @@ -12,6 +12,7 @@ --muted: #8e8e93; --accent: #0a84ff; --invalid: #fafafa; + --checker: #c8c8c8; } * { box-sizing: border-box; } body { margin: 0; font: 13px/1.4 -apple-system, system-ui, sans-serif; @@ -25,7 +26,12 @@ background: var(--accent); color: white; border: none; border-radius: 6px; cursor: pointer; } header button:hover { opacity: 0.9; } + 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; @@ -35,19 +41,71 @@ .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; } - .cell.invalid { background: var(--invalid); opacity: 0.4; } + 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: 24mm; height: 24mm; 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); 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; + 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%; transform: translateX(-50%); padding: 10px 20px; background: var(--text); color: white; border-radius: 8px; @@ -64,7 +122,42 @@ -
+ +
+
+ + +
+
Snapshot copied
diff --git a/assets/themes/sonic/components/diamond/diamond.editor.mjs b/assets/themes/sonic/components/diamond/diamond.editor.mjs index 28bbea2..afec063 100644 --- a/assets/themes/sonic/components/diamond/diamond.editor.mjs +++ b/assets/themes/sonic/components/diamond/diamond.editor.mjs @@ -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'); diff --git a/assets/themes/sonic/components/diamond/diamond.mjs b/assets/themes/sonic/components/diamond/diamond.mjs index 68cdaca..a353b38 100644 --- a/assets/themes/sonic/components/diamond/diamond.mjs +++ b/assets/themes/sonic/components/diamond/diamond.mjs @@ -1,8 +1,8 @@ import { FunPenComponent } from '/src/components/base.mjs'; class SonicDiamond extends FunPenComponent { - static origin = { x: 9, y: 9 }; - static baseSize = { w: 18, h: 18 }; + static origin = { x: 5, y: 5 }; + static baseSize = { w: 10, h: 10 }; static presetProps = { shape: ['classic', 'elongated', 'flat', 'round', 'raw'],