diff --git a/.claude/scheduled_tasks.lock b/.claude/scheduled_tasks.lock new file mode 100644 index 0000000..112c3b4 --- /dev/null +++ b/.claude/scheduled_tasks.lock @@ -0,0 +1 @@ +{"sessionId":"dec1e8b5-dc16-43d2-b20f-0fad3fe0354e","pid":148363,"procStart":"635354","acquiredAt":1777523259923} \ No newline at end of file diff --git a/assets/themes/sonic/components/diamond/diamond.editor.html b/assets/themes/sonic/components/diamond/diamond.editor.html new file mode 100644 index 0000000..ccd0efc --- /dev/null +++ b/assets/themes/sonic/components/diamond/diamond.editor.html @@ -0,0 +1,71 @@ + + + + + Component Editor — sonic-diamond + + + + +
+

sonic-diamond

+ + + +
+
+
Snapshot copied
+ + + diff --git a/assets/themes/sonic/components/diamond/diamond.editor.mjs b/assets/themes/sonic/components/diamond/diamond.editor.mjs new file mode 100644 index 0000000..28bbea2 --- /dev/null +++ b/assets/themes/sonic/components/diamond/diamond.editor.mjs @@ -0,0 +1,115 @@ +/** + * 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. + */ +await customElements.whenDefined('sonic-diamond'); + +const Comp = customElements.get('sonic-diamond'); +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); + +document.getElementById('btn-copy').addEventListener('click', async () => { + const text = serializeSnapshot(Comp.anchorsDefault, Comp.variants); + await navigator.clipboard.writeText(text); + showToast('Snapshot copied'); +}); + +function renderGrid(title, chipped) { + const shapes = chipped + ? presetProps.shape.filter(s => chippedShapes.has(s)) + : presetProps.shape; + const colors = presetProps.color; + + const section = document.createElement('section'); + const h2 = document.createElement('h2'); + h2.textContent = `${title} — ${shapes.length} × ${colors.length}`; + section.appendChild(h2); + + const grid = document.createElement('div'); + grid.className = 'grid'; + grid.style.gridTemplateColumns = `repeat(${colors.length}, 1fr)`; + + for (const shape of shapes) { + for (const color of colors) { + grid.appendChild(makeCell(shape, color, chipped)); + } + } + + section.appendChild(grid); + main.appendChild(section); +} + +function makeCell(shape, color, chipped) { + const cell = document.createElement('div'); + cell.className = 'cell'; + + const preview = document.createElement('div'); + preview.className = 'preview'; + + const inst = document.createElement('sonic-diamond'); + inst.setAttribute('shape', shape); + inst.setAttribute('color', color); + inst.setAttribute('chipped', String(chipped)); + preview.appendChild(inst); + + const label = document.createElement('div'); + label.className = 'label'; + label.innerHTML = `
${shape} · ${color}
`; + + const key = document.createElement('div'); + key.className = 'key'; + key.textContent = inst._variantKey(); + + cell.appendChild(preview); + cell.appendChild(label); + cell.appendChild(key); + return cell; +} + +function serializeSnapshot(anchorsDefault, variants) { + const lines = []; + lines.push(' // @editor:anchors-start'); + lines.push(` static anchorsDefault = ${formatAnchors(anchorsDefault, ' ')};`); + lines.push(' // @editor:anchors-end'); + lines.push(''); + lines.push(' // @editor:variants-start'); + lines.push(' static variants = {'); + for (const [key, v] of Object.entries(variants)) { + const a = formatAnchorsInline(v.anchors || {}); + lines.push(` ${key}: { img: ${JSON.stringify(v.img)}, dx: ${v.dx}, dy: ${v.dy}, scale: ${v.scale}, anchors: ${a} },`); + } + lines.push(' };'); + lines.push(' // @editor:variants-end'); + return lines.join('\n'); +} + +function formatAnchors(anchors, indent) { + const keys = Object.keys(anchors); + if (keys.length === 0) return '{}'; + const inner = keys.map(k => `${indent} ${k}: { x: ${anchors[k].x}, y: ${anchors[k].y} }`).join(',\n'); + return `{\n${inner},\n${indent}}`; +} + +function formatAnchorsInline(anchors) { + const keys = Object.keys(anchors); + if (keys.length === 0) return '{}'; + const parts = keys.map(k => `${k}: { x: ${anchors[k].x}, y: ${anchors[k].y} }`); + return `{ ${parts.join(', ')} }`; +} + +function showToast(msg) { + const toast = document.getElementById('toast'); + toast.textContent = msg; + toast.classList.add('show'); + setTimeout(() => toast.classList.remove('show'), 1500); +} diff --git a/assets/themes/sonic/components/diamond/diamond.mjs b/assets/themes/sonic/components/diamond/diamond.mjs new file mode 100644 index 0000000..68cdaca --- /dev/null +++ b/assets/themes/sonic/components/diamond/diamond.mjs @@ -0,0 +1,85 @@ +import { FunPenComponent } from '/src/components/base.mjs'; + +class SonicDiamond extends FunPenComponent { + static origin = { x: 9, y: 9 }; + static baseSize = { w: 18, h: 18 }; + + static presetProps = { + shape: ['classic', 'elongated', 'flat', 'round', 'raw'], + color: ['blue', 'cyan', 'green', 'magenta', 'pink', 'red'], + chipped: ['false', 'true'], + }; + + // Chipped variants exist only for these shapes. Other combinations + // silently fall back to the non-chipped variant. + static chippedShapes = new Set(['classic', 'round']); + + // @editor:anchors-start + static anchorsDefault = {}; + // @editor:anchors-end + + // @editor:variants-start + static variants = { + classic_blue: { img: '/assets/themes/sonic/items/diamonds/01-classic-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + classic_cyan: { img: '/assets/themes/sonic/items/diamonds/01-classic-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + classic_green: { img: '/assets/themes/sonic/items/diamonds/01-classic-green.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + classic_magenta: { img: '/assets/themes/sonic/items/diamonds/01-classic-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + classic_pink: { img: '/assets/themes/sonic/items/diamonds/01-classic-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + classic_red: { img: '/assets/themes/sonic/items/diamonds/01-classic-red.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + + elongated_blue: { img: '/assets/themes/sonic/items/diamonds/02-elongated-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + elongated_cyan: { img: '/assets/themes/sonic/items/diamonds/02-elongated-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + elongated_green: { img: '/assets/themes/sonic/items/diamonds/02-elongated-green.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + elongated_magenta: { img: '/assets/themes/sonic/items/diamonds/02-elongated-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + elongated_pink: { img: '/assets/themes/sonic/items/diamonds/02-elongated-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + elongated_red: { img: '/assets/themes/sonic/items/diamonds/02-elongated-red.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + + flat_blue: { img: '/assets/themes/sonic/items/diamonds/03-flat-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + flat_cyan: { img: '/assets/themes/sonic/items/diamonds/03-flat-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + flat_green: { img: '/assets/themes/sonic/items/diamonds/03-flat-green.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + flat_magenta: { img: '/assets/themes/sonic/items/diamonds/03-flat-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + flat_pink: { img: '/assets/themes/sonic/items/diamonds/03-flat-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + flat_red: { img: '/assets/themes/sonic/items/diamonds/03-flat-red.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + + round_blue: { img: '/assets/themes/sonic/items/diamonds/04-round-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + round_cyan: { img: '/assets/themes/sonic/items/diamonds/04-round-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + round_green: { img: '/assets/themes/sonic/items/diamonds/04-round-green.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + round_magenta: { img: '/assets/themes/sonic/items/diamonds/04-round-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + round_pink: { img: '/assets/themes/sonic/items/diamonds/04-round-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + round_red: { img: '/assets/themes/sonic/items/diamonds/04-round-red.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + + raw_blue: { img: '/assets/themes/sonic/items/diamonds/05-raw-blue.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + raw_cyan: { img: '/assets/themes/sonic/items/diamonds/05-raw-cyan.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + raw_green: { img: '/assets/themes/sonic/items/diamonds/05-raw-green.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + raw_magenta: { img: '/assets/themes/sonic/items/diamonds/05-raw-magenta.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + raw_pink: { img: '/assets/themes/sonic/items/diamonds/05-raw-pink.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + raw_red: { img: '/assets/themes/sonic/items/diamonds/05-raw-red.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + + classic_blue_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-blue-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + classic_cyan_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-cyan-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + classic_green_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-green-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + classic_magenta_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-magenta-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + classic_pink_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-pink-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + classic_red_chipped: { img: '/assets/themes/sonic/items/diamonds/06-classic-red-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + + round_blue_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-blue-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + round_cyan_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-cyan-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + round_green_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-green-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + round_magenta_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-magenta-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + round_pink_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-pink-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + round_red_chipped: { img: '/assets/themes/sonic/items/diamonds/07-round-red-chipped.png', dx: 0, dy: 0, scale: 1, anchors: {} }, + }; + // @editor:variants-end + + _variantKey() { + const shape = this.getAttribute('shape') ?? this.constructor.presetProps.shape[0]; + const color = this.getAttribute('color') ?? this.constructor.presetProps.color[0]; + const chipped = this.getAttribute('chipped') === 'true'; + const supportsChipped = this.constructor.chippedShapes.has(shape); + return supportsChipped && chipped + ? `${shape}_${color}_chipped` + : `${shape}_${color}`; + } +} + +customElements.define('sonic-diamond', SonicDiamond); diff --git a/assets/themes/sonic/items/conduit/conduit-lvl1-abandoned.png b/assets/themes/sonic/items/conduit/conduit-lvl1-abandoned.png new file mode 100644 index 0000000..763f0dd Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl1-abandoned.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl1-broken.png b/assets/themes/sonic/items/conduit/conduit-lvl1-broken.png new file mode 100644 index 0000000..bc82fb0 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl1-broken.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl1-off.png b/assets/themes/sonic/items/conduit/conduit-lvl1-off.png new file mode 100644 index 0000000..d0a4447 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl1-off.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl1.png b/assets/themes/sonic/items/conduit/conduit-lvl1.png new file mode 100644 index 0000000..15cf92a Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl1.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl2-abandoned.png b/assets/themes/sonic/items/conduit/conduit-lvl2-abandoned.png new file mode 100644 index 0000000..59767c0 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl2-abandoned.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl2-broken.png b/assets/themes/sonic/items/conduit/conduit-lvl2-broken.png new file mode 100644 index 0000000..1e51d02 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl2-broken.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl2-off.png b/assets/themes/sonic/items/conduit/conduit-lvl2-off.png new file mode 100644 index 0000000..dcf48b6 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl2-off.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl2.png b/assets/themes/sonic/items/conduit/conduit-lvl2.png new file mode 100644 index 0000000..729d1a5 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl2.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl3-abandoned.png b/assets/themes/sonic/items/conduit/conduit-lvl3-abandoned.png new file mode 100644 index 0000000..af23da6 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl3-abandoned.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl3-broken.png b/assets/themes/sonic/items/conduit/conduit-lvl3-broken.png new file mode 100644 index 0000000..d2df8cd Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl3-broken.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl3-off.png b/assets/themes/sonic/items/conduit/conduit-lvl3-off.png new file mode 100644 index 0000000..ad9412f Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl3-off.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl3.png b/assets/themes/sonic/items/conduit/conduit-lvl3.png new file mode 100644 index 0000000..2c9a5d2 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl3.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl4-abandoned.png b/assets/themes/sonic/items/conduit/conduit-lvl4-abandoned.png new file mode 100644 index 0000000..98db2a9 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl4-abandoned.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl4-broken.png b/assets/themes/sonic/items/conduit/conduit-lvl4-broken.png new file mode 100644 index 0000000..76dab33 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl4-broken.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl4-off.png b/assets/themes/sonic/items/conduit/conduit-lvl4-off.png new file mode 100644 index 0000000..fed5008 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl4-off.png differ diff --git a/assets/themes/sonic/items/conduit/conduit-lvl4.png b/assets/themes/sonic/items/conduit/conduit-lvl4.png new file mode 100644 index 0000000..26bb906 Binary files /dev/null and b/assets/themes/sonic/items/conduit/conduit-lvl4.png differ diff --git a/assets/themes/sonic/items/conduit/conduit8.png b/assets/themes/sonic/items/conduit/conduit8.png deleted file mode 100644 index 2da3331..0000000 Binary files a/assets/themes/sonic/items/conduit/conduit8.png and /dev/null differ diff --git a/assets/themes/sonic/items/diamonds/01-classic-blue.png b/assets/themes/sonic/items/diamonds/01-classic-blue.png new file mode 100644 index 0000000..2b52e13 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-blue.png differ diff --git a/assets/themes/sonic/items/diamonds/01-classic-cyan.png b/assets/themes/sonic/items/diamonds/01-classic-cyan.png new file mode 100644 index 0000000..1d79224 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-cyan.png differ diff --git a/assets/themes/sonic/items/diamonds/01-classic-green.png b/assets/themes/sonic/items/diamonds/01-classic-green.png new file mode 100644 index 0000000..eb8f171 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-green.png differ diff --git a/assets/themes/sonic/items/diamonds/01-classic-magenta.png b/assets/themes/sonic/items/diamonds/01-classic-magenta.png new file mode 100644 index 0000000..5eaf994 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-magenta.png differ diff --git a/assets/themes/sonic/items/diamonds/01-classic-pink.png b/assets/themes/sonic/items/diamonds/01-classic-pink.png new file mode 100644 index 0000000..46c0934 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-pink.png differ diff --git a/assets/themes/sonic/items/diamonds/01-classic-red.png b/assets/themes/sonic/items/diamonds/01-classic-red.png new file mode 100644 index 0000000..f80f207 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/01-classic-red.png differ diff --git a/assets/themes/sonic/items/diamonds/02-elongated-blue.png b/assets/themes/sonic/items/diamonds/02-elongated-blue.png new file mode 100644 index 0000000..5b8cfa1 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-blue.png differ diff --git a/assets/themes/sonic/items/diamonds/02-elongated-cyan.png b/assets/themes/sonic/items/diamonds/02-elongated-cyan.png new file mode 100644 index 0000000..efc3d0f Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-cyan.png differ diff --git a/assets/themes/sonic/items/diamonds/02-elongated-green.png b/assets/themes/sonic/items/diamonds/02-elongated-green.png new file mode 100644 index 0000000..27c4317 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-green.png differ diff --git a/assets/themes/sonic/items/diamonds/02-elongated-magenta.png b/assets/themes/sonic/items/diamonds/02-elongated-magenta.png new file mode 100644 index 0000000..ce6ae12 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-magenta.png differ diff --git a/assets/themes/sonic/items/diamonds/02-elongated-pink.png b/assets/themes/sonic/items/diamonds/02-elongated-pink.png new file mode 100644 index 0000000..63205e0 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-pink.png differ diff --git a/assets/themes/sonic/items/diamonds/02-elongated-red.png b/assets/themes/sonic/items/diamonds/02-elongated-red.png new file mode 100644 index 0000000..aea6208 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/02-elongated-red.png differ diff --git a/assets/themes/sonic/items/diamonds/03-flat-blue.png b/assets/themes/sonic/items/diamonds/03-flat-blue.png new file mode 100644 index 0000000..ec572d1 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-blue.png differ diff --git a/assets/themes/sonic/items/diamonds/03-flat-cyan.png b/assets/themes/sonic/items/diamonds/03-flat-cyan.png new file mode 100644 index 0000000..fefaa58 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-cyan.png differ diff --git a/assets/themes/sonic/items/diamonds/03-flat-green.png b/assets/themes/sonic/items/diamonds/03-flat-green.png new file mode 100644 index 0000000..189ba72 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-green.png differ diff --git a/assets/themes/sonic/items/diamonds/03-flat-magenta.png b/assets/themes/sonic/items/diamonds/03-flat-magenta.png new file mode 100644 index 0000000..e642bd3 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-magenta.png differ diff --git a/assets/themes/sonic/items/diamonds/03-flat-pink.png b/assets/themes/sonic/items/diamonds/03-flat-pink.png new file mode 100644 index 0000000..bf38324 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-pink.png differ diff --git a/assets/themes/sonic/items/diamonds/03-flat-red.png b/assets/themes/sonic/items/diamonds/03-flat-red.png new file mode 100644 index 0000000..96e0f07 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/03-flat-red.png differ diff --git a/assets/themes/sonic/items/diamonds/04-round-blue.png b/assets/themes/sonic/items/diamonds/04-round-blue.png new file mode 100644 index 0000000..775b59d Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-blue.png differ diff --git a/assets/themes/sonic/items/diamonds/04-round-cyan.png b/assets/themes/sonic/items/diamonds/04-round-cyan.png new file mode 100644 index 0000000..a2fe600 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-cyan.png differ diff --git a/assets/themes/sonic/items/diamonds/04-round-green.png b/assets/themes/sonic/items/diamonds/04-round-green.png new file mode 100644 index 0000000..50289e8 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-green.png differ diff --git a/assets/themes/sonic/items/diamonds/04-round-magenta.png b/assets/themes/sonic/items/diamonds/04-round-magenta.png new file mode 100644 index 0000000..9564256 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-magenta.png differ diff --git a/assets/themes/sonic/items/diamonds/04-round-pink.png b/assets/themes/sonic/items/diamonds/04-round-pink.png new file mode 100644 index 0000000..f440ef2 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-pink.png differ diff --git a/assets/themes/sonic/items/diamonds/04-round-red.png b/assets/themes/sonic/items/diamonds/04-round-red.png new file mode 100644 index 0000000..68d8eff Binary files /dev/null and b/assets/themes/sonic/items/diamonds/04-round-red.png differ diff --git a/assets/themes/sonic/items/diamonds/05-raw-blue.png b/assets/themes/sonic/items/diamonds/05-raw-blue.png new file mode 100644 index 0000000..166232c Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-blue.png differ diff --git a/assets/themes/sonic/items/diamonds/05-raw-cyan.png b/assets/themes/sonic/items/diamonds/05-raw-cyan.png new file mode 100644 index 0000000..4ecf7a7 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-cyan.png differ diff --git a/assets/themes/sonic/items/diamonds/05-raw-green.png b/assets/themes/sonic/items/diamonds/05-raw-green.png new file mode 100644 index 0000000..390e9d9 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-green.png differ diff --git a/assets/themes/sonic/items/diamonds/05-raw-magenta.png b/assets/themes/sonic/items/diamonds/05-raw-magenta.png new file mode 100644 index 0000000..016dc54 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-magenta.png differ diff --git a/assets/themes/sonic/items/diamonds/05-raw-pink.png b/assets/themes/sonic/items/diamonds/05-raw-pink.png new file mode 100644 index 0000000..50b6dde Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-pink.png differ diff --git a/assets/themes/sonic/items/diamonds/05-raw-red.png b/assets/themes/sonic/items/diamonds/05-raw-red.png new file mode 100644 index 0000000..2972e7b Binary files /dev/null and b/assets/themes/sonic/items/diamonds/05-raw-red.png differ diff --git a/assets/themes/sonic/items/diamonds/06-classic-blue-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-blue-chipped.png new file mode 100644 index 0000000..748e59e Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-blue-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/06-classic-cyan-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-cyan-chipped.png new file mode 100644 index 0000000..b0f37a6 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-cyan-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/06-classic-green-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-green-chipped.png new file mode 100644 index 0000000..331cd26 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-green-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/06-classic-magenta-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-magenta-chipped.png new file mode 100644 index 0000000..6c974fb Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-magenta-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/06-classic-pink-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-pink-chipped.png new file mode 100644 index 0000000..312bfc1 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-pink-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/06-classic-red-chipped.png b/assets/themes/sonic/items/diamonds/06-classic-red-chipped.png new file mode 100644 index 0000000..8d25a59 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/06-classic-red-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/07-round-blue-chipped.png b/assets/themes/sonic/items/diamonds/07-round-blue-chipped.png new file mode 100644 index 0000000..3abf040 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-blue-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/07-round-cyan-chipped.png b/assets/themes/sonic/items/diamonds/07-round-cyan-chipped.png new file mode 100644 index 0000000..5dfea62 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-cyan-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/07-round-green-chipped.png b/assets/themes/sonic/items/diamonds/07-round-green-chipped.png new file mode 100644 index 0000000..effc77e Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-green-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/07-round-magenta-chipped.png b/assets/themes/sonic/items/diamonds/07-round-magenta-chipped.png new file mode 100644 index 0000000..f9f878a Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-magenta-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/07-round-pink-chipped.png b/assets/themes/sonic/items/diamonds/07-round-pink-chipped.png new file mode 100644 index 0000000..c070433 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-pink-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/07-round-red-chipped.png b/assets/themes/sonic/items/diamonds/07-round-red-chipped.png new file mode 100644 index 0000000..7f148b4 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/07-round-red-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/raw/base-classic-chipped.png b/assets/themes/sonic/items/diamonds/raw/base-classic-chipped.png new file mode 100644 index 0000000..c4e99ba Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-classic-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/raw/base-classic.png b/assets/themes/sonic/items/diamonds/raw/base-classic.png new file mode 100644 index 0000000..8b01920 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-classic.png differ diff --git a/assets/themes/sonic/items/diamonds/raw/base-elongated.png b/assets/themes/sonic/items/diamonds/raw/base-elongated.png new file mode 100644 index 0000000..7fc738b Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-elongated.png differ diff --git a/assets/themes/sonic/items/diamonds/raw/base-flat.png b/assets/themes/sonic/items/diamonds/raw/base-flat.png new file mode 100644 index 0000000..b59cb81 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-flat.png differ diff --git a/assets/themes/sonic/items/diamonds/raw/base-raw.png b/assets/themes/sonic/items/diamonds/raw/base-raw.png new file mode 100644 index 0000000..2d489fd Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-raw.png differ diff --git a/assets/themes/sonic/items/diamonds/raw/base-round-chipped.png b/assets/themes/sonic/items/diamonds/raw/base-round-chipped.png new file mode 100644 index 0000000..81479e2 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-round-chipped.png differ diff --git a/assets/themes/sonic/items/diamonds/raw/base-round.png b/assets/themes/sonic/items/diamonds/raw/base-round.png new file mode 100644 index 0000000..8fc60f9 Binary files /dev/null and b/assets/themes/sonic/items/diamonds/raw/base-round.png differ diff --git a/assets/themes/sonic/loader.mjs b/assets/themes/sonic/loader.mjs new file mode 100644 index 0000000..8e03fb4 --- /dev/null +++ b/assets/themes/sonic/loader.mjs @@ -0,0 +1,15 @@ +/** + * Sonic theme component loader. + * Scans the document for `sonic-*` custom elements and lazy-imports + * only those component modules. See src/components/CLAUDE.md. + */ +const PREFIX = 'sonic-'; +const tags = new Set(); +document.querySelectorAll('*').forEach(el => { + const t = el.tagName.toLowerCase(); + if (t.startsWith(PREFIX)) tags.add(t); +}); +await Promise.all([...tags].map(t => { + const name = t.slice(PREFIX.length); + return import(`./components/${name}/${name}.mjs`); +})); diff --git a/src/components/CLAUDE.md b/src/components/CLAUDE.md new file mode 100644 index 0000000..adbe3a5 --- /dev/null +++ b/src/components/CLAUDE.md @@ -0,0 +1,306 @@ +# Components System + +Themed Web Components for FunPen worksheets. Components wrap images so that: +- visual variants are picked via props (e.g. `shape="hex" color="blue"`) +- per-image positioning offsets are baked into the component file once and applied automatically +- key points (anchors / connectors) are exposed via a stable DOM API for document-level composition + +This folder holds the **shared infrastructure** (base class, editor framework). Actual components live next to their assets under `assets/themes/{theme}/components/{name}/`. + +--- + +## Where things live + +``` +src/components/ — shared infrastructure (this folder) + CLAUDE.md — decisions and conventions (this file) + base.mjs — FunPenComponent base class + editor/ — reusable component-editor framework + matrix-view.mjs — matrix mode (grid of preset variants) + single-view.mjs — single mode (tune one variant) + write-back.mjs — saves edits back into component .mjs + styles.css — shared editor styles + README-editor.md — how to build a component-editor + +assets/themes/{theme}/ + loader.mjs — tiny per-theme on-demand loader (~10 lines) + components/ + {component}/ + {component}.mjs — self-contained Web Component (final) + {component}.editor.html — component-editor page (dev only) + {component}.editor.mjs — component-editor logic (dev only) + +assets/themes/{theme}/items/{cat}/ — image assets (UNCHANGED — components reference these) +``` + +The editor framework files (`editor/*`) are written iteratively as we build the first component. Don't pre-create them empty — extract reusable pieces from the first concrete editor. + +--- + +## Decisions log + +These were agreed during the design discussion and should not be changed without revisiting: + +1. **Web Components with Shadow DOM** — for true encapsulation. Doc-editor cannot peek inside, no style bleed, real isolation. Single ` + +``` + +The loader runs as `module` → deferred by default → the DOM is parsed before it scans. If a component is added to the page after load (very rare in our static documents), the loader does not pick it up — that's fine for now. + +--- + +## Component-editor + +Two modes, one page (`{component}.editor.html`): + +### Matrix mode (default on open) + +Cartesian product of all `presetProps` rendered as a grid. Each cell is a live instance of the component with that prop combination plus a label. Clicking a cell enters single mode for those props. + +Free props (`x`, `y`, `scale`, `rotation`) are NOT iterated — they don't affect the variant. + +### Single mode + +One large instance of the selected variant. Sidebar with: +- Sliders/inputs for the selected variant's `dx`, `dy`, `scale` (adjustments to the image inside the component bounds — this is the main work) +- Drag handles on each anchor point — moving them stores per-variant overrides on top of `anchorsDefault` +- "Edit defaults" toggle: adjust `anchorsDefault` for ALL variants at once +- Save button → write changes back into the component's `.mjs` file +- Back button → return to matrix + +### Save mechanism — Claude in the middle, no automated write-back + +Following the project's orchestrator pattern: the editor produces data, the AI agent applies it. **No `/api/save-component` endpoint, no auto-rewrite of the source file.** + +The component's `.mjs` file marks two sections that contain editor-tunable data: + +```js +// @editor:anchors-start +static anchorsDefault = { ... }; +// @editor:anchors-end + +// @editor:variants-start +static variants = { ... }; +// @editor:variants-end +``` + +Workflow: + +1. User tunes in component-editor (matrix → single → adjusts dx/dy/scale and anchors) +2. User clicks **"Copy snapshot"** in the editor toolbar +3. Editor copies a JS source fragment to clipboard — the new `anchorsDefault = { ... };` and `variants = { ... };` blocks, formatted to drop in between the markers +4. User pastes into chat / asks Claude to apply +5. Claude opens the component `.mjs` file, replaces ONLY the content between `@editor:anchors-start/end` and `@editor:variants-start/end` markers using the Edit tool +6. Claude verifies: re-opens the editor, compares visual result against what the user tuned (screenshots of the matrix before/after), confirms nothing else in the file changed (`git diff`) + +This intentionally avoids chained automation. The cost of a 30-second manual paste-and-edit is much lower than the cost of a write-back script silently corrupting handwritten code, and the agent's review pass catches mistakes the editor cannot see (markers gone, ordering changed, formatting drift). + +If you need to add or rename a marker section, edit the `.mjs` file by hand. + +### What lives in `src/components/editor/` + +Reusable pieces extracted from the first concrete editor. Expected modules: + +- `matrix-view.mjs` — given a component class, render the cartesian grid of preset combinations +- `single-view.mjs` — given a component instance + sidebar root, wire up controls for `dx`/`dy`/`scale` and anchor drag +- `serialize.mjs` — turns the editor's in-memory `anchorsDefault` and `variants` into a formatted JS source fragment ready to paste between markers. Used by the "Copy snapshot" button — entirely client-side, no server round-trip. +- `styles.css` — common styles (toolbar, sidebar, grid, drag handles) + +Build these incrementally. Don't pre-create empty files. + +--- + +## Adding a new component + +1. Create `assets/themes/{theme}/components/{name}/{name}.mjs` from the skeleton above +2. Fill `origin`, `baseSize`, `presetProps`, `anchorsDefault` +3. Fill `variants` with rough defaults (any non-zero starting point — the editor refines them) +4. Create `{name}.editor.html` from the editor template (see `src/components/editor/`) +5. Open `http://localhost:3300/assets/themes/{theme}/components/{name}/{name}.editor.html` +6. Tune in matrix → single → "Copy snapshot" → ask Claude to paste between markers in `{name}.mjs` +7. Claude verifies the result matches the editor (re-open, compare screenshots) before considering the tune-pass done +8. Use the component in any document via `<{theme}-{name} x="..." y="..." {presetProps...}>` + +--- + +## Adding a new theme + +1. Create `assets/themes/{theme}/loader.mjs` (copy from existing, change `PREFIX`) +2. Add components under `components/` + +Nothing else changes — the base class and editor framework are theme-agnostic. + +--- + +## Document-side rules + +**To use components in a document:** + +1. Add loader once to the document ``: + ```html + + ``` +2. Place components anywhere in the body: + ```html + + ``` + +**Compatibility with doc-editor and generate.mjs:** + +- Doc-editor moves components by their `x`/`y` attributes (or wraps them in style.left/top — TBD on first integration). Components are atomic; the doc-editor never touches their internals. +- generate.mjs treats the component tag as plain HTML and copies it through. No special handling. +- Puppeteer (PDF) needs to wait for component registration before snapshotting. `generate-pdf.mjs` should `waitForFunction(() => customElements.get('sonic-crystal'))` for each used tag, or `waitForNetworkIdle()` is usually enough. + +**Compatibility with the existing "static HTML" rule:** + +The project rule "OUTPUT MUST BE STATIC HTML — no embedded `