Mini-preview cards, fixed scaling, welcome close button, danger styling

Template browser cards now render WYSIWYG mini-previews instead of just the
background image. Each card shows the full layered composition (color fills,
text, image layers, slot artwork) at thumbnail scale, with the design's
natural silhouette preserved — discs stay circular, sheets stay rectangular,
cover stays wide.

Preview renderers (4 total):
- Added a `compact` option to renderTemplatePreviewDOM,
  renderDiscSheetPreviewDOM, renderJewelPreviewDOM, renderDiscPreviewDOM.
  When compact: drop labels (BACK/SPINE/FRONT, Disc 1/2, FRONT/TRAY,
  sheet titles), drop shadow, drop border lines.
- Compact mode renders each preview at a fixed natural pixel size
  (720 wide for sheets, 380 for disc-design); fitMiniPreview applies a
  CSS transform: scale() to fit the thumb. Everything — text, layer
  positions, slot art — scales together so the mini is a true miniature
  of what `Preview` shows.

Disc-design preview specific fixes:
- Was the only renderer not sorting layers by z, so they rendered in
  array order and a higher-z layer added earlier could be hidden behind
  a lower-z layer added later. Now sorted ascending and stacked in DOM.
- Added explicit z-index values matching the disc-sheet renderer
  (bg img z:1, layers z:3+sortedIndex, hole z:12) to avoid stacking
  context surprises from the per-layer transform: rotate().
- Used Number(layer.x) etc. for defensive coercion of legacy payloads.
- Made the center hole percentage-sized so it scales with the disc.

Cover preview fix:
- Dropped the unnecessary `row` wrapper that broke mini-preview sizing.
  preview is appended directly to container, same shape as the other
  three renderers. The `!hasArtwork` early-return now only fires in
  non-compact mode so empty covers still show their silhouette in the
  card thumb.

UI polish:
- Close button (×) in the top-right corner of the welcome modal.
- Both "Close project" buttons (template-mode and disc-designer)
  now use the .danger class to match the styling of the other
  destructive buttons in their panels.

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
thePR0M3TH3ANandClaude Sonnet 4.6 committed 2026-05-17 19:11:18 -04:00
1 parent d94eff9671
commit 20cbe78805
1 file changed
+187 -148
+187 -148
View File
@@ -1513,7 +1513,11 @@
.feedTrustBadge{font-size:.62rem;background:rgba(0,184,230,.12);color:var(--accent);border:1px solid rgba(0,184,230,.25);border-radius:4px;padding:1px 5px;margin-left:5px;white-space:nowrap;flex-shrink:0}
body.dark .feedTrustBadge{background:rgba(0,217,255,.1);border-color:rgba(0,217,255,.2)}
.feedCard.feedCardBlurred .feedCardThumb img,
.feedCard.feedCardBlurred .feedCardThumb canvas{filter:blur(14px) saturate(.5);transition:filter .15s}
.feedCard.feedCardBlurred .feedCardThumb canvas,
.feedCard.feedCardBlurred .miniPreviewWrap{filter:blur(14px) saturate(.5);transition:filter .15s}
.miniPreviewWrap{position:absolute;inset:0;display:flex;align-items:center;justify-content:center;overflow:hidden;contain:layout style paint;z-index:2}
.miniPreviewWrap > *{margin:0!important;box-shadow:none!important;flex:0 0 auto;transform-origin:center center}
.feedCardThumb .miniPreviewWrap ~ .feedCardThumbLabel{display:none}
.feedCard.feedCardBroken{opacity:.55}
.feedCard.feedCardBroken .feedCardThumb{background:repeating-linear-gradient(45deg,#f1f5f9,#f1f5f9 10px,#e2e8f0 10px,#e2e8f0 20px)}
.feedCardBrokenBadge{position:absolute;top:6px;right:6px;background:rgba(220,38,38,.92);color:#fff;font-size:.62rem;font-weight:700;border-radius:4px;padding:2px 6px;z-index:6;letter-spacing:.03em}
@@ -1527,6 +1531,8 @@
body.dark .feedTypeBtn.active{color:#fff}
.feedCategorySelect{width:auto;min-width:130px;padding:5px 8px;font-size:.78rem}
.welcomeCard { max-width: 900px; width: min(900px, 92vw); }
.welcomeCloseBtn { position:absolute; top:8px; right:10px; width:32px; height:32px; border:0; background:transparent; color:var(--muted); font-size:24px; line-height:1; cursor:pointer; padding:0; border-radius:6px; display:flex; align-items:center; justify-content:center; }
.welcomeCloseBtn:hover { background:rgba(127,127,127,.15); color:var(--ink); }
.welcomeSearchRow { display:grid; grid-template-columns: 1fr 160px 160px auto; gap:8px; align-items:end; margin-bottom:12px; }
.welcomeTabs { display:flex; gap:6px; border-bottom:1px solid var(--line); margin:4px 0 10px; flex-wrap:wrap; }
.welcomeTab { background:transparent; border:0; padding:8px 14px; cursor:pointer; font-weight:700; color:var(--muted); border-bottom:2px solid transparent; font-size:.85rem; }
@@ -1917,7 +1923,7 @@
</div>
<div class="buttonRow">
<button id="saveProjectBtn">Save project (JSON)</button>
<button id="closeProjectBtn">Close project</button>
<button type="button" class="danger" id="closeProjectBtn">Close project</button>
</div>
<details>
<summary>Nostr</summary>
@@ -2246,7 +2252,7 @@
<div class="buttonRow">
<button type="button" id="designerSaveProjectBtn">Save design (JSON)</button>
<button type="button" id="designerExportJpegBtn">Export JPEG</button>
<button type="button" id="designerCloseProjectBtn">Close project</button>
<button type="button" class="danger" id="designerCloseProjectBtn">Close project</button>
</div>
<details>
<summary>Nostr</summary>
@@ -2369,7 +2375,8 @@
<div class="modal" id="welcomeModal" aria-hidden="true">
<div class="modalCard welcomeCard">
<div class="modalCard welcomeCard" style="position:relative">
<button type="button" id="welcomeCloseBtn" class="welcomeCloseBtn" aria-label="Close">×</button>
<h2 style="margin:0 0 4px">Welcome to CaseWrap Studio</h2>
<p class="subhead" style="margin:0 0 14px">Browse community templates or start your own.</p>
<div class="welcomeSearchRow">
@@ -7354,20 +7361,37 @@
const isDisc = row.mode === 'disc' || row.mode === 'disc-design';
const modeLabel = MODE_LABELS[row.mode] || row.mode;
const thumb = document.createElement('div');
thumb.className = 'feedCardThumb' + (isDisc ? ' feedCardThumbDisc' : '');
thumb.className = 'feedCardThumb';
// Mini-preview: same DOM as the full preview rendered at its natural size,
// then CSS-scaled via transform to fit the thumb. Layers, text, and slot
// art all scale together so it's WYSIWYG with the click-Preview pane.
const previewWrap = document.createElement('div');
previewWrap.className = 'miniPreviewWrap';
try {
const payload = row.payload || {};
if (row.mode === 'disc-design') renderDiscPreviewDOM(previewWrap, payload, { compact: true });
else if (row.mode === 'disc') renderDiscSheetPreviewDOM(previewWrap, payload, { compact: true });
else if (row.mode === 'jewel') renderJewelPreviewDOM(previewWrap, payload, { compact: true });
else renderTemplatePreviewDOM(previewWrap, payload, { compact: true });
} catch {}
thumb.appendChild(previewWrap);
fitMiniPreview(previewWrap);
// Fallback label so empty designs still show their mode (and as a placeholder
// if the preview produced nothing visible).
const lbl = document.createElement('span');
lbl.className = 'feedCardThumbLabel';
lbl.textContent = modeLabel;
if (row.mode === 'disc') {
buildDiscSheetThumb(thumb, row.payload, false);
} else {
thumb.appendChild(lbl);
}
if (row.preview && row.mode !== 'disc') {
const img = document.createElement('img');
img.alt = ''; img.loading = 'lazy'; img.src = row.preview;
img.addEventListener('error', () => { img.remove(); markImageBroken(row.preview); });
thumb.insertBefore(img, lbl);
thumb.appendChild(lbl);
// Hidden detector image to keep broken-flag/sort behavior consistent.
// We can't read pixels from cross-origin images but we CAN listen for
// their onerror (display-only path) to know if the main artwork URL works.
if (row.preview) {
const detector = new Image();
detector.onerror = () => markImageBroken(row.preview);
detector.src = row.preview;
}
const isBroken = isPreviewBroken(row);
if (isBroken) {
@@ -8012,56 +8036,106 @@
}
}
function renderDiscPreviewDOM(container, payload) {
// Shared by all preview renderers. Mirrors the editor's `.textLayer` CSS
// so text rendering in the preview matches what the editor shows.
// pxPerInch is the preview's CSS-px per real inch (e.g., 720/sheetW or
// PREVIEW_PX/DISC_D), so the rendered metrics scale with the preview size.
function applyPreviewLayerTextStyle(el, layer, pxPerInch) {
const fontPx = (Number(layer.fontSize) || 14) * pxPerInch / 72;
const padPx = 0.04 * pxPerInch; // matches .textLayer padding: .04in
const align = layer.align || 'left';
const family = layer.fontFamily && layer.fontFamily !== 'inherit'
? layer.fontFamily
: 'inherit';
el.style.cssText += `font-family:${family};color:${layer.color || '#111827'};font-size:${fontPx}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};padding:${padPx}px;box-sizing:border-box;line-height:1.2;white-space:pre-wrap;word-wrap:break-word;overflow-wrap:break-word;`;
el.innerHTML = layer.html || '';
}
const PREVIEW_BASE_PX = 720;
function fitMiniPreview(wrap) {
const preview = wrap?.firstElementChild;
if (!preview) return;
const naturalW = parseFloat(preview.style.width) || preview.offsetWidth || 0;
const naturalH = parseFloat(preview.style.height) || preview.offsetHeight || 0;
if (!naturalW || !naturalH) return;
let attempts = 0;
const apply = () => {
const wrapW = wrap.clientWidth;
const wrapH = wrap.clientHeight;
if (!wrapW || !wrapH) {
if (attempts++ < 10) requestAnimationFrame(apply);
return;
}
const scale = Math.min(wrapW / naturalW, wrapH / naturalH);
preview.style.transform = `scale(${scale})`;
};
requestAnimationFrame(() => {
requestAnimationFrame(() => {
apply();
if (window.ResizeObserver) {
const ro = new ResizeObserver(apply);
ro.observe(wrap);
}
});
});
}
function renderDiscPreviewDOM(container, payload, opts = {}) {
const compact = !!opts.compact;
const disc = payload.discDesign || {};
const allLayers = payload.layers || disc.layers || [];
const layers = allLayers.filter(l => !l.target || l.target === 'designer-disc' || l._generatedDiscLayer);
const layers = allLayers
.filter(l => !l.target || l.target === 'designer-disc' || l._generatedDiscLayer)
.slice()
.sort((a, b) => (a.z || 0) - (b.z || 0));
const DISC_REAL_PX = 4.625 * 96;
const PREVIEW_PX = 380;
const SCALE = PREVIEW_PX / DISC_REAL_PX;
const DISC_D = 4.625;
const HOLE_D = 0.69;
const holePx = (HOLE_D / DISC_D) * PREVIEW_PX;
const holePct = (HOLE_D / DISC_D) * 100;
const outer = document.createElement('div');
outer.style.cssText = `position:relative;width:${PREVIEW_PX}px;height:${PREVIEW_PX}px;margin:0 auto;`;
outer.style.cssText = `position:relative;width:${PREVIEW_PX}px;height:${PREVIEW_PX}px;margin:0 auto;${compact ? 'transform-origin:center center;' : ''}`;
const discWrap = document.createElement('div');
discWrap.style.cssText = `position:absolute;inset:0;border-radius:50%;overflow:hidden;background:#111;border:1px solid rgba(17,24,39,.18);`;
discWrap.style.cssText = `position:absolute;inset:0;border-radius:50%;overflow:hidden;background:#111;${compact ? '' : 'border:1px solid rgba(17,24,39,.18);'}`;
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});`;
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;`;
discWrap.appendChild(img);
}
layers.forEach(layer => {
layers.forEach((layer, i) => {
const el = document.createElement('div');
const lx = (layer.x || 0) / DISC_D * 100;
const ly = (layer.y || 0) / DISC_D * 100;
const lw = (layer.w || DISC_D) / DISC_D * 100;
const lh = (layer.h || DISC_D) / DISC_D * 100;
el.style.cssText = `position:absolute;left:${lx}%;top:${ly}%;width:${lw}%;height:${lh}%;transform:rotate(${layer.rotate||0}deg);opacity:${layer.opacity !== undefined ? layer.opacity : 1};overflow:hidden;pointer-events:none;box-sizing:border-box;`;
const lx = (Number(layer.x) || 0) / DISC_D * 100;
const ly = (Number(layer.y) || 0) / DISC_D * 100;
const lw = (Number(layer.w) || DISC_D) / DISC_D * 100;
const lh = (Number(layer.h) || DISC_D) / DISC_D * 100;
// z-index from the sorted index so each layer sits cleanly above the
// last (and above the disc bg image at z:1). Hole is at z:12.
const zi = 3 + i;
el.style.cssText = `position:absolute;left:${lx}%;top:${ly}%;width:${lw}%;height:${lh}%;transform:rotate(${Number(layer.rotate)||0}deg);opacity:${layer.opacity !== undefined ? layer.opacity : 1};overflow:hidden;pointer-events:none;box-sizing:border-box;z-index:${zi};`;
if (layer.type === 'text') {
const fontPx = (layer.fontSize || 14) * (96/72) * SCALE;
el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color||'#fff'};font-size:${fontPx}px;font-weight:${layer.bold?'bold':'normal'};font-style:${layer.italic?'italic':'normal'};text-align:${layer.align||'left'};display:flex;align-items:flex-start;`;
el.innerHTML = layer.html || '';
applyPreviewLayerTextStyle(el, layer, PREVIEW_PX / DISC_D);
} else if (layer.type === 'color') {
el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
} else if (layer.type === 'image' && layer.src) {
const img = document.createElement('img');
img.src = layer.src;
img.style.cssText = 'width:100%;height:100%;object-fit:contain;';
img.style.cssText = `width:100%;height:100%;object-fit:${layer.fit || 'contain'};`;
img.addEventListener('error', () => img.remove());
el.appendChild(img);
}
discWrap.appendChild(el);
});
const hole = document.createElement('div');
hole.style.cssText = `position:absolute;left:50%;top:50%;width:${holePx}px;height:${holePx}px;border-radius:50%;background:#fff;border:1px solid rgba(17,24,39,.28);transform:translate(-50%,-50%);z-index:12;pointer-events:none;`;
hole.style.cssText = `position:absolute;left:50%;top:50%;width:${holePct}%;height:${holePct}%;border-radius:50%;background:#fff;${compact ? '' : 'border:1px solid rgba(17,24,39,.28);'}transform:translate(-50%,-50%);z-index:12;pointer-events:none;`;
discWrap.appendChild(hole);
outer.appendChild(discWrap);
@@ -8069,7 +8143,8 @@
}
function renderTemplatePreviewDOM(container, payload) {
function renderTemplatePreviewDOM(container, payload, opts = {}) {
const compact = !!opts.compact;
const ct = payload.coverTemplate || {};
const slots = ct.slots || payload.slots || {};
const preset = presets[ct.preset] || presets.dvd;
@@ -8082,15 +8157,16 @@
const combined = ct.combined || payload.combined || {};
const preview = document.createElement('div');
const _coverNaturalH = PREVIEW_BASE_PX * trimH / totalW;
preview.style.cssText = [
'position:relative',
'width:min(100%, 720px)',
`aspect-ratio:${totalW} / ${trimH}`,
'margin:0 auto',
compact ? `width:${PREVIEW_BASE_PX}px` : `width:min(100%, ${PREVIEW_BASE_PX}px)`,
compact ? `height:${_coverNaturalH}px` : `aspect-ratio:${totalW} / ${trimH}`,
compact ? 'margin:0;transform-origin:center center' : 'margin:0 auto',
'background:#fff',
'border:1px solid var(--line)',
compact ? 'border:0' : 'border:1px solid var(--line)',
'overflow:hidden',
'box-shadow:0 2px 10px rgba(15,23,42,.12)'
compact ? 'box-shadow:none' : 'box-shadow:0 2px 10px rgba(15,23,42,.12)'
].join(';');
const renderPreviewArt = (wrap, art) => {
@@ -8190,7 +8266,7 @@
addRegion('Spine', panelW, spineW, slots.spine || {});
addRegion('Front cover', panelW + spineW, panelW, slots.front || {});
const labels = [
const labels = compact ? [] : [
['BACK', 0, panelW],
['SPINE', panelW, spineW],
['FRONT', panelW + spineW, panelW]
@@ -8249,10 +8325,7 @@
'z-index:4'
].join(';');
if (layer.type === 'text') {
const scale = preview.clientWidth ? (preview.clientWidth / (totalW * 96)) : 1;
const align = layer.align || 'left';
el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color || '#fff'};font-size:${((layer.fontSize || 14) * (96 / 72) * scale)}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};display:flex;align-items:flex-start;justify-content:${align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'};`;
el.innerHTML = layer.html || '';
applyPreviewLayerTextStyle(el, layer, PREVIEW_BASE_PX / totalW);
} else if (layer.type === 'color') {
el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
} else if (layer.type === 'image' && layer.src) {
@@ -8268,11 +8341,8 @@
coverLayers.forEach(layer => renderCoverLayer(preview, layer));
const row = document.createElement('div');
row.appendChild(preview);
const hasArtwork = (isCombined && combined.src) || ['back','spine','front'].some(key => slots[key]?.src) || coverLayers.length > 0;
if (!hasArtwork) {
if (!hasArtwork && !compact) {
if (payload.preview) {
const img = document.createElement('img');
img.src = payload.preview;
@@ -8287,10 +8357,11 @@
return;
}
container.appendChild(row);
container.appendChild(preview);
}
function renderDiscSheetPreviewDOM(container, payload) {
function renderDiscSheetPreviewDOM(container, payload, opts = {}) {
const compact = !!opts.compact;
const template = payload.discTemplate || {};
const slots = template.discs || payload.discs || {};
const meta = template.meta || payload.discMeta || {};
@@ -8328,64 +8399,18 @@
].join(';');
if (layer.type === 'text') {
const scale = wrap.clientWidth && layerScale ? (wrap.clientWidth / (layerScale * 96)) : 1;
const align = layer.align || 'left';
el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color || '#fff'};font-size:${((layer.fontSize || 14) * (96 / 72) * scale)}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};display:flex;align-items:flex-start;justify-content:${align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'};`;
el.innerHTML = layer.html || '';
// px/in within this disc wrap = preview_width(720)/sheetW (sheet's
// CSS px per real inch, same scale everywhere on the sheet).
applyPreviewLayerTextStyle(el, layer, PREVIEW_BASE_PX / sheetW);
} else if (layer.type === 'color') {
el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
} else if (layer.type === 'image' && layer.src) {
const img = document.createElement('img');
img.src = layer.src;
img.alt = layer.name || '';
img.style.cssText = 'position:absolute;left:50%;top:50%;transform-origin:center center;user-select:none;pointer-events:none;display:block;';
img.style.cssText = `width:100%;height:100%;object-fit:${layer.fit || 'contain'};`;
img.addEventListener('error', () => img.remove());
el.appendChild(img);
const update = () => {
const cW = el.clientWidth;
const cH = el.clientHeight;
if (!cW || !cH) return;
const fit = layer.fit || 'contain';
const nW = img.naturalWidth || cW;
const nH = img.naturalHeight || cH;
let baseW = cW;
let baseH = cH;
if (fit === 'fill') {
baseW = cW;
baseH = cH;
} else if (fit === 'none') {
baseW = nW;
baseH = nH;
} else {
const fitScale = fit === 'contain'
? Math.min(cW / nW, cH / nH)
: Math.max(cW / nW, cH / nH);
baseW = nW * fitScale;
baseH = nH * fitScale;
}
const rotate = Number(layer.rotate) || 0;
const zoom = Number(layer.zoom) || 1;
const rad = rotate * Math.PI / 180;
const ac = Math.abs(Math.cos(rad));
const as = Math.abs(Math.sin(rad));
const rotatedW = (baseW * ac) + (baseH * as);
const 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 panRangeX = Math.max(0, (rotatedW * finalScale - cW) / 2);
const panRangeY = Math.max(0, (rotatedH * finalScale - cH) / 2);
const xPx = ((Number(layer.x) || 0) / 100) * panRangeX;
const yPx = ((Number(layer.y) || 0) / 100) * panRangeY;
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(update);
img.addEventListener('load', update, { once: true });
window.addEventListener('resize', update, { once: true });
}
wrap.appendChild(el);
@@ -8453,15 +8478,16 @@
};
const sheet = document.createElement('div');
const _sheetNaturalH = PREVIEW_BASE_PX * sheetH / sheetW;
sheet.style.cssText = [
'position:relative',
'width:min(100%, 720px)',
`aspect-ratio:${sheetW} / ${sheetH}`,
'margin:0 auto',
compact ? `width:${PREVIEW_BASE_PX}px` : `width:min(100%, ${PREVIEW_BASE_PX}px)`,
compact ? `height:${_sheetNaturalH}px` : `aspect-ratio:${sheetW} / ${sheetH}`,
compact ? 'margin:0;transform-origin:center center' : 'margin:0 auto',
'background:#fff',
'border:1px solid var(--line)',
compact ? 'border:0' : 'border:1px solid var(--line)',
'overflow:hidden',
'box-shadow:0 2px 10px rgba(15,23,42,.12)'
compact ? 'box-shadow:none' : 'box-shadow:0 2px 10px rgba(15,23,42,.12)'
].join(';');
const addDisc = (slotKey) => {
@@ -8500,19 +8526,21 @@
].join(';');
wrap.appendChild(hole);
const label = document.createElement('div');
label.style.cssText = [
'position:absolute',
'left:50%',
'bottom:-1.25rem',
'transform:translateX(-50%)',
'font-size:.72rem',
'font-weight:700',
'color:#64748b',
'white-space:nowrap'
].join(';');
label.textContent = meta?.[slotKey]?.title || (slotKey === 'top' ? 'Disk 1' : 'Disk 2');
wrap.appendChild(label);
if (!compact) {
const label = document.createElement('div');
label.style.cssText = [
'position:absolute',
'left:50%',
'bottom:-1.25rem',
'transform:translateX(-50%)',
'font-size:.72rem',
'font-weight:700',
'color:#64748b',
'white-space:nowrap'
].join(';');
label.textContent = meta?.[slotKey]?.title || (slotKey === 'top' ? 'Disk 1' : 'Disk 2');
wrap.appendChild(label);
}
sheet.appendChild(wrap);
};
@@ -8520,26 +8548,29 @@
addDisc('top');
addDisc('bottom');
const sheetLabel = document.createElement('div');
sheetLabel.style.cssText = [
'position:absolute',
'left:0',
'right:0',
'top:8px',
'text-align:center',
'font-size:.72rem',
'font-weight:700',
'color:#94a3b8',
'pointer-events:none',
'z-index:3'
].join(';');
sheetLabel.textContent = 'Disc sheet';
sheet.appendChild(sheetLabel);
if (!compact) {
const sheetLabel = document.createElement('div');
sheetLabel.style.cssText = [
'position:absolute',
'left:0',
'right:0',
'top:8px',
'text-align:center',
'font-size:.72rem',
'font-weight:700',
'color:#94a3b8',
'pointer-events:none',
'z-index:3'
].join(';');
sheetLabel.textContent = 'Disc sheet';
sheet.appendChild(sheetLabel);
}
container.appendChild(sheet);
}
function renderJewelPreviewDOM(container, payload) {
function renderJewelPreviewDOM(container, payload, opts = {}) {
const compact = !!opts.compact;
const template = payload.jewelTemplate || {};
const inserts = template.inserts || payload.jewelInserts || {};
const spineLabels = template.spineLabels || payload.jewelSpineLabels || {};
@@ -8602,10 +8633,8 @@
const lh = (Number(layer.h) || slotH) / slotH * 100;
el.style.cssText = `position:absolute;left:${lx}%;top:${ly}%;width:${lw}%;height:${lh}%;transform:rotate(${Number(layer.rotate)||0}deg);opacity:${layer.opacity !== undefined ? layer.opacity : 1};overflow:hidden;pointer-events:none;box-sizing:border-box;z-index:3;`;
if (layer.type === 'text') {
const scale = wrap.clientWidth ? (wrap.clientWidth / (slotW * 96)) : 1;
const align = layer.align || 'left';
el.style.cssText += `font-family:${layer.fontFamily || 'inherit'};color:${layer.color || '#111'};font-size:${((layer.fontSize || 14) * (96 / 72) * scale)}px;font-weight:${layer.bold ? '800' : '400'};font-style:${layer.italic ? 'italic' : 'normal'};text-align:${align};display:flex;align-items:flex-start;justify-content:${align === 'center' ? 'center' : align === 'right' ? 'flex-end' : 'flex-start'};`;
el.innerHTML = layer.html || '';
// px/in within this insert = sheet_width(720)/sheetW (uniform across sheet).
applyPreviewLayerTextStyle(el, layer, PREVIEW_BASE_PX / sheetW);
} else if (layer.type === 'color') {
el.style.background = layer.color || COLOR_LAYER_DEFAULT_FILL;
} else if (layer.type === 'image' && layer.src) {
@@ -8613,13 +8642,17 @@
img.src = layer.src;
img.alt = layer.name || '';
img.style.cssText = `width:100%;height:100%;object-fit:${layer.fit || 'contain'};`;
img.addEventListener('error', () => img.remove());
el.appendChild(img);
}
wrap.appendChild(el);
};
const sheet = document.createElement('div');
sheet.style.cssText = `position:relative;width:min(100%, 720px);aspect-ratio:${sheetW} / ${sheetH};margin:0 auto;background:#fff;border:1px solid var(--line);overflow:hidden;box-shadow:0 2px 10px rgba(15,23,42,.12);`;
const _jewelNaturalH = PREVIEW_BASE_PX * sheetH / sheetW;
sheet.style.cssText = compact
? `position:relative;width:${PREVIEW_BASE_PX}px;height:${_jewelNaturalH}px;margin:0;transform-origin:center center;background:#fff;border:0;overflow:hidden;box-shadow:none;`
: `position:relative;width:min(100%, ${PREVIEW_BASE_PX}px);aspect-ratio:${sheetW} / ${sheetH};margin:0 auto;background:#fff;border:1px solid var(--line);overflow:hidden;box-shadow:0 2px 10px rgba(15,23,42,.12);`;
slots.forEach((slotDef) => {
const insert = inserts[slotDef.key] || {};
@@ -8652,17 +8685,21 @@
buildSpine('right', spineLabels.right || '');
}
const label = document.createElement('div');
label.style.cssText = `position:absolute;left:50%;bottom:-1.25rem;transform:translateX(-50%);font-size:.72rem;font-weight:700;color:#64748b;white-space:nowrap;`;
label.textContent = slotDef.label || slotDef.key;
wrap.appendChild(label);
if (!compact) {
const label = document.createElement('div');
label.style.cssText = `position:absolute;left:50%;bottom:-1.25rem;transform:translateX(-50%);font-size:.72rem;font-weight:700;color:#64748b;white-space:nowrap;`;
label.textContent = slotDef.label || slotDef.key;
wrap.appendChild(label);
}
sheet.appendChild(wrap);
});
const sheetLabel = document.createElement('div');
sheetLabel.style.cssText = `position:absolute;left:0;right:0;top:8px;text-align:center;font-size:.72rem;font-weight:700;color:#94a3b8;pointer-events:none;z-index:3;`;
sheetLabel.textContent = 'Jewel case (front + tray with spines)';
sheet.appendChild(sheetLabel);
if (!compact) {
const sheetLabel = document.createElement('div');
sheetLabel.style.cssText = `position:absolute;left:0;right:0;top:8px;text-align:center;font-size:.72rem;font-weight:700;color:#94a3b8;pointer-events:none;z-index:3;`;
sheetLabel.textContent = 'Jewel case (front + tray with spines)';
sheet.appendChild(sheetLabel);
}
container.appendChild(sheet);
}
@@ -9377,6 +9414,8 @@
});
$('welcomeSearchBtn').addEventListener('click', runWelcomeSearch);
$('welcomeSkipBtn').addEventListener('click', dismissWelcomeModal);
const welcomeCloseBtn = $('welcomeCloseBtn');
if (welcomeCloseBtn) welcomeCloseBtn.addEventListener('click', dismissWelcomeModal);
$('welcomeBrowseAllBtn').addEventListener('click', openWelcomeBrowseAll);
$('welcomeTabNew').addEventListener('click', () => loadWelcomeTab('New'));
$('welcomeTabPopular').addEventListener('click', () => loadWelcomeTab('Popular'));