Optional background image, disc-image placement fixes, browser polish

- Publishing no longer requires a background image: cover/jewel/disc
  designs publish with layers-only content. "Save design (JSON)" works
  for layer-only disc designs (removed a silent return + a payload throw).
- Disc background image now lands correctly: renderDesignerDisc() runs
  after the designer view is laid out, and the publish/feed preview uses
  the same cover-pan math as the editor instead of a naive translate(%).
- Publish-modal preview no longer squishes the disc vertically (dropped
  axis-independent max-width/height clamps; fitMiniPreview scales it).
- Template browser: "Fork off!" button renamed to "Use/Fork".

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Opus 4.7 committed 2026-05-20 22:44:55 -04:00
1 parent 159f60385a
commit 2298512dce
1 file changed
+44 -14
+44 -14
View File
@@ -1657,8 +1657,7 @@
.toast.toastOut { opacity:0; transition:opacity .2s; }
@keyframes toastIn { from { transform:translateY(8px); opacity:0; } to { transform:translateY(0); opacity:1; } }
.publishConfirmPreview { background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:10px; margin:0 0 12px; display:flex; justify-content:center; align-items:center; max-height:280px; overflow:hidden; }
.publishConfirmPreview > * { max-width:100%; max-height:260px; }
.publishConfirmPreview { background:var(--bg); border:1px solid var(--line); border-radius:8px; padding:10px; margin:0 0 12px; display:flex; justify-content:center; align-items:center; height:280px; overflow:hidden; }
.publishConfirmMeta { margin:0 0 12px; font-size:.82rem; }
.publishConfirmMetaRow { display:flex; gap:10px; padding:5px 0; border-bottom:1px solid var(--line); }
.publishConfirmMetaRow:last-child { border-bottom:0; }
@@ -4014,7 +4013,6 @@
function discDesignPayload() {
persistCurrentDiscDesign();
const design = state.discDesign;
if (!design?.src) throw new Error('Load an image URL before publishing this disc design.');
const layers = currentDesignerLayers();
return {
version: SCHEMA_VERSION,
@@ -6090,10 +6088,12 @@
renderJewelSlots();
renderCombined();
renderLayers();
renderDesignerDisc();
syncLayerInputs();
syncDesignerInputs();
updateBodyClasses();
// After updateBodyClasses so the designer view is displayed/laid out —
// the disc image's cover-pan math needs the wrapper's real size.
renderDesignerDisc();
refreshPenOverlayGeometry();
refreshPenOverlayContent();
if (_editorTool !== 'select') syncToolOptionInputs();
@@ -8960,8 +8960,8 @@
} else if (state.templateMode === 'jewel') {
payload = jewelTemplatePayload();
typeTag = 'casewrap-jewel';
if (!previewFromPayload(payload, 'jewel')) {
throw new Error('Load jewel insert artwork before publishing.');
if (!previewFromPayload(payload, 'jewel') && !(payload.layers || []).length) {
throw new Error('Add jewel insert artwork or at least one layer before publishing.');
}
} else if (state.templateMode === 'customart') {
payload = customArtPayload();
@@ -8974,8 +8974,8 @@
} else {
payload = coverTemplatePayload();
typeTag = 'casewrap-cover';
if (!previewFromPayload(payload, 'cover')) {
throw new Error('Load cover artwork before publishing.');
if (!previewFromPayload(payload, 'cover') && !(payload.layers || []).length) {
throw new Error('Add cover artwork or at least one layer before publishing.');
}
}
const mode = normalizeTemplateMode(payload);
@@ -8999,11 +8999,13 @@
async function publishDiscDesignToNostr() {
const design = state.discDesign;
if (!design?.src) throw new Error('Load an image URL before publishing this disc design.');
if (!state.meta.category?.trim()) {
throw new Error('Select a category in Project Metadata before publishing.');
}
const payload = discDesignPayload();
if (!design?.src && !(payload.layers || []).length) {
throw new Error('Load an image or add at least one layer before publishing this disc design.');
}
const designName = (design.name || '').trim() || state.meta.title;
if (!designName?.trim()) {
throw new Error('Add a title in Project Metadata or a custom design name before publishing.');
@@ -10709,7 +10711,6 @@
if (!title) { toastError('Please enter a title in Project Metadata before saving.'); return; }
if (!category) { toastError('Please select a category in Project Metadata before saving.'); return; }
const design = state.discDesign;
if (!design?.src) return;
const payload = discDesignPayload();
const name = (design.name || 'disc_design')
.toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_+|_+$/g, '');
@@ -13260,7 +13261,7 @@
prevBtn.type = 'button'; prevBtn.textContent = 'Preview';
prevBtn.addEventListener('click', e => { e.stopPropagation(); openDesignPreview(row); });
const btn = document.createElement('button');
btn.type = 'button'; btn.textContent = 'Fork off!';
btn.type = 'button'; btn.textContent = 'Use/Fork';
btn.title = 'Fork this design — opens it in the editor so you can save your own version';
btn.addEventListener('click', e => { e.stopPropagation(); onUse(); });
const tipBtn = makeFeedCardTipButton(row);
@@ -13803,7 +13804,7 @@
prevBtn.addEventListener('click', e => { e.stopPropagation(); openDesignPreview(row); });
const useBtn = document.createElement('button');
useBtn.type = 'button';
useBtn.textContent = 'Fork off!';
useBtn.textContent = 'Use/Fork';
useBtn.title = 'Fork this design — opens it in the editor so you can save your own version';
useBtn.addEventListener('click', e => { e.stopPropagation(); onUse(); });
actions.append(makeReactionBtn(row), prevBtn, useBtn);
@@ -13957,7 +13958,7 @@
const forkBtn = document.createElement('button');
forkBtn.type = 'button';
forkBtn.textContent = 'Fork off!';
forkBtn.textContent = 'Use/Fork';
forkBtn.title = 'Fork this design — opens it in the editor so you can save your own version';
forkBtn.addEventListener('click', (e) => {
e.stopPropagation();
@@ -14177,8 +14178,37 @@
if (disc.src) {
const img = document.createElement('img');
img.src = disc.src;
img.style.cssText = `position:absolute;left:50%;top:50%;width:100%;height:100%;object-fit:${disc.fit || 'cover'};transform:translate(-50%,-50%) translate(${disc.x||0}%,${disc.y||0}%) rotate(${disc.rotate||0}deg) scale(${disc.zoom||1});z-index:1;`;
img.alt = '';
img.style.cssText = 'position:absolute;left:50%;top:50%;width:100%;height:100%;object-fit:fill;transform-origin:center center;user-select:none;pointer-events:none;z-index:1;';
discWrap.appendChild(img);
// Match the editor's cover-pan math (applyRotationCover) so the
// background image sits exactly where the user placed it — the old
// translate(x%) model panned by a different, much larger range.
const updateDiscArt = () => {
const fit = disc.fit || 'cover';
const cW = discWrap.clientWidth, cH = discWrap.clientHeight;
if (!cW || !cH) return;
const nW = img.naturalWidth || cW, nH = img.naturalHeight || cH;
let baseW = cW, baseH = cH;
if (fit === 'none') { baseW = nW; baseH = nH; }
else if (fit !== 'fill') {
const fs = fit === 'contain' ? Math.min(cW / nW, cH / nH) : Math.max(cW / nW, cH / nH);
baseW = nW * fs; baseH = nH * fs;
}
const rotate = Number(disc.rotate) || 0, zoom = Number(disc.zoom) || 1;
const rad = rotate * Math.PI / 180, ac = Math.abs(Math.cos(rad)), as = Math.abs(Math.sin(rad));
const rotatedW = baseW * ac + baseH * as, rotatedH = baseW * as + baseH * ac;
const coverScale = fit === 'cover'
? Math.max(cW / Math.max(rotatedW, 1), cH / Math.max(rotatedH, 1), 1) : 1;
const finalScale = coverScale * zoom;
const xPx = ((Number(disc.x) || 0) / 100) * Math.max(0, (rotatedW * finalScale - cW) / 2);
const yPx = ((Number(disc.y) || 0) / 100) * Math.max(0, (rotatedH * finalScale - cH) / 2);
img.style.width = `${baseW}px`;
img.style.height = `${baseH}px`;
img.style.transform = `translate(-50%,-50%) translate(${xPx}px,${yPx}px) rotate(${rotate}deg) scale(${finalScale})`;
};
if (img.complete) requestAnimationFrame(updateDiscArt);
img.addEventListener('load', updateDiscArt, { once: true });
}
layers.forEach((layer, i) => {