329 lines
56 KiB
HTML
329 lines
56 KiB
HTML
<!DOCTYPE html>
|
||
<html>
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<script src="./support.js"></script>
|
||
</head>
|
||
<body>
|
||
<x-dc>
|
||
<div style="display:contents">{{ view }}</div>
|
||
</x-dc>
|
||
<script type="text/x-dc" data-dc-script>
|
||
class Component extends DCLogic {
|
||
renderVals() {
|
||
const s = this.props.s || {}, m = !!this.props.mobile, h = React.createElement
|
||
const P = t => Object.fromEntries(t.split(';').filter(x => x.trim()).map(x => { const i = x.indexOf(':'); const k = x.slice(0, i).trim(); return [k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase()), x.slice(i + 1).trim()] }))
|
||
const D = (t, ...c) => h('div', { style: P(t) }, ...c)
|
||
const S = (t, ...c) => h('span', { style: P(t) }, ...c)
|
||
const I = (n, z = 16, t = '') => h('span', { 'aria-hidden': 'true', style: { display: 'inline-block', flexShrink: 0, width: z, height: z, backgroundColor: 'currentColor', WebkitMask: 'url(https://api.iconify.design/lucide/' + n + '.svg) center / contain no-repeat', mask: 'url(https://api.iconify.design/lucide/' + n + '.svg) center / contain no-repeat', ...(t ? P(t) : {}) } })
|
||
const MONO = "font-family:'IBM Plex Mono',monospace"
|
||
const bh = m ? 44 : 32
|
||
const B = (label, o = {}) => {
|
||
const k = o.k || 'outline'
|
||
const bg = k === 'primary' ? 'var(--ui-primary)' : k === 'danger' ? 'var(--ui-error)' : k === 'ghost' ? 'transparent' : 'var(--ui-bg)'
|
||
const fg = k === 'primary' || k === 'danger' ? 'var(--ui-bg)' : k === 'del' ? 'var(--ui-error)' : 'var(--ui-text-highlighted)'
|
||
const bd = k === 'primary' || k === 'danger' ? bg : k === 'ghost' ? 'transparent' : k === 'del' ? 'var(--ui-error)' : 'var(--ui-border-accented)'
|
||
const ht = o.h || bh
|
||
return S('height:' + ht + 'px;' + (o.sq ? 'width:' + ht + 'px;' : 'padding:0 ' + (o.px || 12) + 'px;') + 'display:flex;align-items:center;justify-content:center;gap:6px;background:' + bg + ';color:' + fg + ';border:1px solid ' + bd + ';font-weight:500;font-size:' + (o.fs || 13) + 'px;white-space:nowrap;flex-shrink:0;opacity:' + (o.dis ? 0.4 : 1) + ';' + (o.full ? 'flex:1;' : '') + (o.ring ? 'outline:2px solid var(--ui-primary);outline-offset:2px;' : ''), o.icon && I(o.icon, o.z || 16), label)
|
||
}
|
||
const TONES = ['var(--rc-sand)', 'var(--ui-bg-accented)', 'color-mix(in srgb, var(--ui-border-accented) 35%, var(--ui-bg))', 'var(--ui-bg-elevated)']
|
||
const Th = (tone, t = '', z = 24, icon = 'image') => D('position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);background:repeating-linear-gradient(135deg, rgb(128 128 128 / 0.12) 0 1px, transparent 1px 12px), ' + TONES[tone % 4] + ';' + t, I(icon, z, 'opacity:0.55'))
|
||
const BADGE = {
|
||
ok: ['background:var(--rc-success-soft);color:var(--ui-success)', 'circle-check'], none: ['border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)', 'circle-dashed'],
|
||
busy: ['background:var(--rc-info-soft);color:var(--ui-info)', 'loader-circle'], fail: ['background:var(--rc-error-soft);color:var(--ui-error)', 'circle-x'],
|
||
used: ['border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)'], unused: ['background:var(--rc-warning-soft);color:var(--ui-warning)', 'unlink'],
|
||
vars: ['background:var(--rc-info-soft);color:var(--ui-info)', 'layers'], off: ['border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)', 'circle-off'], warn: ['background:var(--rc-warning-soft);color:var(--ui-warning)', 'triangle-alert']
|
||
}
|
||
const Bd = (t, k, sz = 20) => S('height:' + sz + 'px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:500;white-space:nowrap;flex-shrink:0;' + BADGE[k][0], BADGE[k][1] && I(BADGE[k][1], 12), t)
|
||
const Sw = (on, dis) => S('width:32px;height:18px;padding:2px;flex-shrink:0;display:flex;justify-content:' + (on ? 'flex-end' : 'flex-start') + ';background:' + (on ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';opacity:' + (dis ? 0.4 : 1), S('width:14px;height:14px;background:var(--ui-bg)'))
|
||
const Alert = (tone, icon, title, text, action, actionEl) => D('display:flex;gap:10px;padding:12px 14px;background:var(--rc-' + tone + '-soft);color:var(--ui-' + tone + ')', S('display:flex;padding-top:2px', I(icon)), D('flex:1;display:flex;flex-direction:column;gap:8px;min-width:0', D('display:flex;flex-direction:column;gap:2px', title && S('font-weight:600', title), text && S('color:var(--ui-text-toned)', text)), action && D('display:flex', B(action, { icon: 'rotate-cw', h: m ? 44 : 28, fs: 12, z: 14, ...(actionEl || {}) }))))
|
||
const Empty = (icon, title, text, pad = 56, ...extra) => D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:' + pad + 'px 24px;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center', S('width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated);color:var(--ui-text-toned)', I(icon, 20)), S('font-weight:600;font-size:15px;color:var(--ui-text-highlighted)', title), S('color:var(--ui-text-muted);max-width:380px;text-wrap:pretty', text), ...extra)
|
||
const Note = t => S('align-self:flex-start;height:20px;padding:0 6px;display:inline-flex;align-items:center;border:1px dashed var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-toned);font:500 11px \'IBM Plex Mono\',monospace', t)
|
||
const Skl = (w, ht = 13) => S('display:block;width:' + w + ';height:' + ht + 'px;background:var(--ui-bg-accented)')
|
||
const Field = (lab, val, o = {}) => D('display:flex;flex-direction:column;gap:6px;min-width:0', D('display:flex;justify-content:space-between', S('font-weight:500;color:var(--ui-text-highlighted)', lab), o.count && S(MONO + ';font-size:12px;color:var(--ui-text-muted)', o.count)),
|
||
D('display:flex;align-items:center;min-height:' + (m ? 44 : 40) + 'px;border:1px solid ' + (o.err || o.eb ? 'var(--ui-error)' : o.focus ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';' + (o.focus ? 'box-shadow:0 0 0 1px var(--ui-primary);' : '') + 'background:' + (o.dis || o.lock ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';font-size:' + (m ? 16 : 13) + 'px;opacity:' + (o.dis ? 0.6 : 1),
|
||
S('flex:1;min-width:0;padding:0 12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:' + (val ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)') + ';' + (o.mono ? MONO : ''), val || o.ph || ''),
|
||
o.suffix && S('padding:0 12px;color:var(--ui-text-muted);' + MONO + ';font-size:13px', o.suffix), o.lock && S('padding:0 12px;display:flex;color:var(--ui-text-muted)', I('lock', 14)), o.sel && S('padding:0 12px;display:flex;color:var(--ui-text-muted)', I('chevron-down', 16))),
|
||
o.err && S('display:flex;gap:6px;align-items:flex-start;color:var(--ui-error);font-size:12px', I('circle-alert', 14, 'margin-top:3px'), S('', o.err)), o.help && !o.err && S('color:var(--ui-text-muted);font-size:12px;text-wrap:pretty', o.help))
|
||
const SwRow = (lab, help, on, dis) => D('display:flex;align-items:flex-start;gap:12px;min-height:' + (m ? 44 : 32) + 'px', D('flex:1;display:flex;flex-direction:column;min-width:0', S('font-weight:500;color:var(--ui-text-highlighted)', lab), S('color:var(--ui-text-muted);font-size:12px', help)), D('padding-top:2px', Sw(on, dis)))
|
||
const Shell = (o, body) => D('position:absolute;inset:0;z-index:50;background:var(--rc-overlay);display:flex;align-items:' + (m ? 'flex-end' : 'flex-start') + ';justify-content:center;padding:' + (m ? 16 : o.top || 140) + 'px 16px 16px',
|
||
D('width:' + (m ? '100%' : (o.w || 460) + 'px') + ';max-height:100%;background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column',
|
||
D('padding:20px 24px 0;display:flex;align-items:flex-start;gap:12px', D('flex:1;display:flex;flex-direction:column;gap:4px', S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', o.title), o.desc && S('color:var(--ui-text-muted);text-wrap:pretty', o.desc)), o.close && S('width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x'))),
|
||
body,
|
||
D('padding:20px 24px;display:flex;flex-direction:' + (m ? 'column-reverse' : 'row') + ';justify-content:flex-end;gap:8px;flex-shrink:0', o.cancel && B(o.cancel, { dis: o.loading }), o.confirm && S('height:' + bh + 'px;padding:0 12px;display:flex;align-items:center;justify-content:center;gap:6px;background:' + (o.tone === 'danger' ? 'var(--ui-error)' : 'var(--ui-primary)') + ';color:var(--ui-bg);font-weight:500;opacity:' + (o.cdis ? 0.4 : 1), o.loading && I('loader-circle'), o.confirm))))
|
||
const Toasts = () => D('position:absolute;z-index:60;display:flex;flex-direction:column;gap:8px;' + (m ? 'left:16px;right:16px;bottom:16px' : 'right:24px;bottom:24px;width:372px'), ...s.toasts.map(t => D('display:flex;gap:10px;padding:12px 14px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop)', S('margin-top:2px;display:flex;color:var(--ui-' + t[0] + ')', I(t[1])), D('flex:1;display:flex;flex-direction:column;gap:6px', D('display:flex;flex-direction:column;gap:2px', S('font-weight:600;color:var(--ui-text-highlighted)', t[2]), t[3] && S('color:var(--ui-text-muted)', t[3])), t[4] && B(t[4], { icon: 'rotate-cw', z: 14, h: m ? 44 : 28, fs: 12, px: 10 })), I('x'))))
|
||
|
||
// ---------- data
|
||
const V = [
|
||
{ n: 'thumb', w: 400, h: 300, fit: 'cover', fmt: 'WebP', q: 80, files: 412, size: '21 MB', on: true, out: '400×300', kb: '18 KB', sv: '-99%' },
|
||
{ n: 'card', w: 800, h: 0, fit: 'inside', fmt: 'WebP', q: 80, files: 318, size: '58 MB', on: true, out: '800×600', kb: '64 KB', sv: '-98%' },
|
||
{ n: 'hero', w: 1200, h: 630, fit: 'cover', fmt: 'WebP', q: 82, files: 244, size: '84 MB', on: true, out: '1200×630', kb: '96 KB', sv: '-98%' },
|
||
{ n: 'square', w: 600, h: 600, fit: 'cover', fmt: 'JPEG', q: 85, files: 140, size: '19 MB', on: true, out: '600×600', kb: '82 KB', sv: '-98%' },
|
||
{ n: 'og', w: 1200, h: 630, fit: 'cover', fmt: 'JPEG', q: 80, files: 96, size: '9 MB', on: true, out: '1200×630', kb: '142 KB', sv: '-96%' },
|
||
{ n: 'print', w: 0, h: 2400, fit: 'inside', fmt: 'PNG', q: 0, files: 74, size: '21 MB', on: false, out: '3200×2400', kb: '3.1 MB', sv: '-21%' }
|
||
]
|
||
const dim = v => v.w && v.h ? v.w + '×' + v.h : v.w ? v.w + ' × auto' : 'auto × ' + v.h
|
||
const en = V.filter(v => v.on)
|
||
const IM = [
|
||
{ id: 1, n: 'north-wall.jpg', sz: '3.9 MB', dim: '4032 × 3024', ty: 'JPEG', used: 2, vc: 4, vs: '260 KB', tot: '4.2 MB', tone: 0 },
|
||
{ id: 2, n: 'kitchen-after.webp', sz: '1.2 MB', dim: '2400 × 1600', ty: 'WEBP', used: 3, vc: 5, vs: '410 KB', tot: '1.6 MB', tone: 1 },
|
||
{ id: 3, n: 'kitchen-before.webp', sz: '1.1 MB', dim: '2400 × 1600', ty: 'WEBP', used: 1, vc: 5, vs: '395 KB', tot: '1.5 MB', tone: 2 },
|
||
{ id: 4, n: 'logo-renew.svg', sz: '8 KB', dim: 'Vector', ty: 'SVG', used: 5, vc: 0, tot: '8 KB', tone: 3 },
|
||
{ id: 5, n: 'floorboards-copy.jpg', sz: '2.4 MB', dim: '3600 × 2400', ty: 'JPEG', used: 3, vc: 3, vs: '240 KB', tot: '2.6 MB', tone: 0 },
|
||
{ id: 6, n: 'IMG_2208.jpg', sz: '2.4 MB', dim: '3600 × 2400', ty: 'JPEG', used: 2, vc: 0, tot: '2.4 MB', tone: 0 },
|
||
{ id: 7, n: 'IMG_4471.jpg', sz: '3.1 MB', dim: '3600 × 2400', ty: 'JPEG', used: 2, vc: 2, vs: '150 KB', tot: '3.3 MB', tone: 2 },
|
||
{ id: 8, n: 'window-frame.jpg', sz: '1.8 MB', dim: '3000 × 2000', ty: 'JPEG', used: 0, vc: 0, tot: '1.8 MB', tone: 3 },
|
||
{ id: 9, n: 'stone-wall-detail.jpg', sz: '3.1 MB', dim: '3600 × 2400', ty: 'JPEG', used: 1, vc: 1, vs: '60 KB', tot: '3.2 MB', tone: 2 },
|
||
{ id: 10, n: 'bathroom-tiles.png', sz: '860 KB', dim: '1800 × 1200', ty: 'PNG', used: 0, vc: 3, vs: '190 KB', tot: '1.1 MB', tone: 1 },
|
||
{ id: 11, n: 'lime-plaster.jpg', sz: '2.1 MB', dim: '3200 × 2133', ty: 'JPEG', used: 2, vc: 4, vs: '280 KB', tot: '2.4 MB', tone: 1 },
|
||
{ id: 12, n: 'floorboards.jpg', sz: '2.4 MB', dim: '3600 × 2400', ty: 'JPEG', used: 4, vc: 5, vs: '430 KB', tot: '2.8 MB', tone: 0 }
|
||
]
|
||
const byId = id => IM.find(x => x.id === id)
|
||
const mark = k => h('svg', { width: k, height: k, viewBox: '0 0 16 16', shapeRendering: 'crispEdges' }, h('rect', { width: 16, height: 16, fill: 'var(--ui-primary)' }), h('path', { fill: 'var(--ui-bg)', fillRule: 'evenodd', d: 'M4 3h6v1h1v1h1v3h-1v1h-1v1h1v1h1v2h-2v-2h-1v-1H6v3H4zm2 2v3h4V5z' }))
|
||
const pad = m ? 16 : 24
|
||
|
||
// ---------- shell
|
||
const NAV = [['home', 'house', 'Home'], ['articles', 'file-text', 'Articles'], ['media', 'images', 'Media'], ['users', 'users', 'Users'], ['integration', 'plug', 'Integration'], ['settings', 'settings', 'Settings']]
|
||
const nav = s.role === 'user' ? NAV.filter(n => ['home', 'articles', 'media'].includes(n[0])) : NAV
|
||
const side = D('width:224px;flex-shrink:0;background:var(--ui-bg);border-right:1px solid var(--ui-border);display:flex;flex-direction:column;padding:12px;gap:2px',
|
||
D('display:flex;align-items:center;gap:10px;height:40px;padding:0 6px;margin-bottom:10px', mark(26), S('font-weight:600;font-size:14px;color:var(--ui-text-highlighted)', 'RenewCMS')),
|
||
...nav.map(([k, ic, t]) => D('display:flex;align-items:center;gap:10px;height:32px;padding:0 10px;background:' + (k === s.nav ? 'var(--rc-sand)' : 'transparent') + ';color:' + (k === s.nav ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)') + ';font-weight:' + (k === s.nav ? 500 : 400), I(ic), t)),
|
||
D('flex:1'), D('display:flex;align-items:center;gap:10px;height:40px;padding:0 6px;color:var(--ui-text-highlighted)', S('width:26px;height:26px;flex-shrink:0;background:var(--rc-sand);color:var(--rc-on-sand);display:flex;align-items:center;justify-content:center;font:600 11px \'IBM Plex Sans\',sans-serif', 'MA'), S('flex:1;font-weight:500', 'Marta Alves'), I('chevrons-up-down', 14)))
|
||
const crumbs = s.crumbs || []
|
||
const topbar = m
|
||
? D('height:56px;flex-shrink:0;border-bottom:1px solid var(--ui-border);background:var(--ui-bg);display:flex;align-items:center;padding:0 6px;gap:4px;color:var(--ui-text-highlighted)', S('width:44px;height:44px;display:flex;align-items:center;justify-content:center', I(s.back ? 'arrow-left' : 'menu', 20)), S('flex:1;font-weight:600;font-size:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', s.mTitle || crumbs[crumbs.length - 1]))
|
||
: D('height:48px;flex-shrink:0;border-bottom:1px solid var(--ui-border);background:var(--ui-bg);display:flex;align-items:center;padding:0 24px;gap:8px;color:var(--ui-text-muted)', I('panel-left-close'), S('width:1px;height:16px;background:var(--ui-border);margin:0 6px'), ...crumbs.flatMap((c, i) => [i > 0 && I('chevron-right', 14), S('color:' + (i === crumbs.length - 1 ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)') + ';font-weight:' + (i === crumbs.length - 1 ? 500 : 400), c)]))
|
||
const PH = (title, desc, ...acts) => D('display:flex;flex-direction:' + (m ? 'column' : 'row') + ';align-items:' + (m ? 'stretch' : 'flex-start') + ';gap:' + (m ? 12 : 16) + 'px;flex-shrink:0', D('flex:1;display:flex;flex-direction:column;gap:4px;min-width:0', S('font:600 ' + (m ? 20 : 24) + 'px/32px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', title), desc && S('color:var(--ui-text-muted);max-width:620px;text-wrap:pretty', desc)), acts.filter(Boolean).length > 0 && D('display:flex;gap:8px;flex-shrink:0;' + (m ? 'flex-direction:column' : ''), ...acts.filter(Boolean)))
|
||
const SetTabs = act => D('display:flex;gap:20px;border-bottom:1px solid var(--ui-border);flex-shrink:0', ...['Variants', 'Cache'].map(t => S('height:' + (m ? 44 : 36) + 'px;display:flex;align-items:center;gap:6px;border-bottom:2px solid ' + (t === act ? 'var(--ui-primary)' : 'transparent') + ';margin-bottom:-1px;font-weight:' + (t === act ? 500 : 400) + ';color:' + (t === act ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)'), I(t === 'Variants' ? 'layers' : 'database', 14), t)))
|
||
const Pg = (...kids) => D('flex:1;min-height:0;overflow:hidden;padding:' + pad + 'px;display:flex;flex-direction:column;gap:16px;position:relative', D('display:flex;flex-direction:column;gap:16px;flex-shrink:0;margin-top:' + (-(s.scroll || 0)) + 'px', ...kids))
|
||
const Card = (title, right, ...kids) => D('background:var(--ui-bg);border:1px solid var(--ui-border);display:flex;flex-direction:column;min-width:0', D('display:flex;align-items:center;gap:8px;min-height:44px;padding:0 16px;border-bottom:1px solid var(--ui-border)', S('flex:1;font-weight:600;color:var(--ui-text-highlighted)', title), right), ...kids)
|
||
|
||
// ---------- 1. variants list
|
||
const L = s.list || {}
|
||
const Menu = () => D('position:absolute;z-index:20;right:0;top:' + (m ? 44 : 34) + 'px;width:176px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px;display:flex;flex-direction:column;font-size:13px', ...[['pencil', 'Edit', ''], ['eraser', 'Purge cache', ''], ['trash-2', 'Delete', 'color:var(--ui-error)']].map(([ic, t, c], i) => D('display:flex;align-items:center;gap:8px;height:' + (m ? 44 : 32) + 'px;padding:0 8px;color:var(--ui-text-highlighted);' + (i === 0 && L.menu !== undefined ? 'background:var(--ui-bg-elevated);' : '') + c, I(ic, 14), t)))
|
||
const stat = (v, i) => { const busy = L.busy === i, on = v.on; return D('display:flex;align-items:center;gap:8px;color:' + (busy ? 'var(--ui-info)' : on ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)'), Sw(on, busy), S('display:flex;align-items:center;gap:4px;white-space:nowrap', I(busy ? 'loader-circle' : on ? 'circle-check' : 'circle-off', 14), busy ? 'Purging…' : on ? 'Enabled' : 'Disabled')) }
|
||
const act = (i) => S('position:relative;display:flex', B(null, { icon: 'ellipsis', sq: true, k: 'ghost', h: m ? 44 : 28, dis: L.busy === i, ring: L.menu === i }), L.menu === i && Menu())
|
||
const cols = 'minmax(0,1.1fr) 104px 76px 72px 68px 92px 92px 132px 44px'
|
||
const ListTable = () => {
|
||
const hd = D('display:grid;grid-template-columns:' + cols + ';height:36px;align-items:center;padding:0 8px 0 16px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500', ...['Name', 'Dimensions', 'Fit', 'Format', 'Quality', 'Generated', 'Cache size', 'Status', ''].map(t => S('', t)))
|
||
const rows = V.map((v, i) => D('position:relative;display:grid;grid-template-columns:' + cols + ';min-height:48px;align-items:center;padding:0 8px 0 16px;border-top:1px solid var(--ui-border-muted);background:' + (L.busy === i ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';color:' + (v.on ? 'var(--ui-text)' : 'var(--ui-text-muted)'),
|
||
S(MONO + ';font-weight:500;font-size:13px;color:' + (v.on ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)'), v.n), S(MONO + ';font-size:12px', dim(v)), S('', v.fit), S('', v.fmt), S('', v.q ? v.q : '—'), S('', v.files.toLocaleString('en-US')), S('', v.size), stat(v, i), act(i)))
|
||
return D('background:var(--ui-bg);border:1px solid var(--ui-border)', hd, ...rows, D('padding:10px 16px;border-top:1px solid var(--ui-border);color:var(--ui-text-toned)', '6 variants · 1,284 generated files · 212 MB'))
|
||
}
|
||
const ListCards = () => D('display:flex;flex-direction:column;gap:8px', ...V.map((v, i) => D('position:relative;background:' + (L.busy === i ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';border:1px solid var(--ui-border);padding:12px;display:flex;flex-direction:column;gap:6px', D('display:flex;align-items:center;gap:8px', S(MONO + ';font-weight:500;font-size:14px;flex:1;color:' + (v.on ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)'), v.n), stat(v, i)),
|
||
S('color:var(--ui-text-toned);' + MONO + ';font-size:12px', dim(v) + ' · ' + v.fit + ' · ' + v.fmt + (v.q ? ' · q' + v.q : '')),
|
||
D('display:flex;align-items:center', S('flex:1;color:var(--ui-text-muted)', v.files.toLocaleString('en-US') + ' generated · ' + v.size), act(i)))), S('color:var(--ui-text-muted);padding:4px 0', '6 variants · 1,284 generated files · 212 MB'))
|
||
const RefCard = () => {
|
||
const rows = [['/media/photo.png?v=thumb', '200', 'Image', 'ok'], ['Unknown or disabled variant', '400', 'Unknown variant', 'err'], ['width, height, format or quality', '400', 'Unsupported parameter', 'err'], ['Missing source image', '404', 'Not found', 'err']]
|
||
const json = '{\n "status": 400,\n "error": "unknown_variant",\n "message": "Unknown variant",\n "detail": "banner"\n}'
|
||
return D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:16px;display:flex;flex-direction:column;gap:12px', S('font-weight:600;color:var(--ui-text-highlighted)', 'How requests are handled'),
|
||
D('display:grid;grid-template-columns:' + (m ? 'minmax(0,1fr)' : 'minmax(0,1.3fr) minmax(0,1fr)') + ';gap:16px', D('display:flex;flex-direction:column;border:1px solid var(--ui-border)', ...rows.map(([a, b, c, k], i) => D('display:flex;align-items:center;gap:10px;min-height:' + (m ? 44 : 36) + 'px;padding:6px 12px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), S('flex:1;min-width:0;' + (i === 0 ? MONO + ';font-size:12px' : '') + ';color:var(--ui-text-highlighted)', a), Bd(b, k === 'ok' ? 'ok' : 'fail'), S('width:' + (m ? 120 : 130) + 'px;color:var(--ui-text-toned)', c)))),
|
||
D('display:flex;flex-direction:column;gap:6px', S('color:var(--ui-text-muted);font-size:12px', 'Body of a 400 response'), h('pre', { style: { margin: 0, padding: 12, background: 'var(--ui-bg-elevated)', color: 'var(--ui-text-highlighted)', fontFamily: "'IBM Plex Mono',monospace", fontSize: 12, lineHeight: '18px', overflow: 'hidden' } }, json))), S('color:var(--ui-text-muted);font-size:12px', 'unsupported_parameter replaces unknown_variant when a raw parameter is sent. There is no arbitrary-size mode.'))
|
||
}
|
||
const TPL = [['thumb', '400×300 · cover · WebP'], ['hero', '1200×630 · cover · WebP']]
|
||
const ListPage = () => {
|
||
let body
|
||
if (s.forbidden) body = [Empty('lock', 'You don\'t have access', 'Only admins can manage image variants. Ask an admin if you need one changed.', 72, B('Back to Home', { icon: 'arrow-left' }))]
|
||
else {
|
||
const add = B('Add variant', { k: 'primary', icon: 'plus' })
|
||
body = [PH('Image variants', 'Named sizes and formats generated from your original images. Originals are never changed.', add), SetTabs('Variants')]
|
||
if (L.state === 'loading') body.push(m ? D('display:flex;flex-direction:column;gap:8px', ...Array.from({ length: 4 }, () => D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:12px;display:flex;flex-direction:column;gap:10px', Skl('40%'), Skl('70%', 11), Skl('55%', 11)))) : D('background:var(--ui-bg);border:1px solid var(--ui-border)', D('height:36px;background:var(--ui-bg-elevated)'), ...Array.from({ length: 5 }, () => D('height:48px;border-top:1px solid var(--ui-border-muted);display:flex;align-items:center;gap:32px;padding:0 16px', Skl('120px'), Skl('90px'), Skl('60px'), Skl('60px'), Skl('80px'), Skl('160px')))))
|
||
else if (L.state === 'error') body.push(Alert('error', 'circle-x', 'Couldn\'t load variants', 'Check your connection and try again.', 'Try again'))
|
||
else if (L.state === 'empty') body.push(Empty('layers', 'No variants yet', 'A variant is a named size that an image can be requested in, like ?v=thumb. Start with one of these.', 40, D('display:flex;gap:12px;width:100%;max-width:520px;' + (m ? 'flex-direction:column' : ''), ...TPL.map(([n, d]) => D('flex:1;border:1px solid var(--ui-border-accented);padding:12px;display:flex;flex-direction:column;gap:2px;text-align:left;min-height:' + (m ? 64 : 0) + 'px', S(MONO + ';font-weight:500;color:var(--ui-text-highlighted)', n), S('font-size:12px;color:var(--ui-text-muted)', d), S('display:flex;align-items:center;gap:4px;margin-top:8px;font-weight:500;color:var(--ui-text-highlighted)', I('plus', 14), 'Use template')))), B('Add variant', { k: 'primary', icon: 'plus', px: 16 })))
|
||
else body.push(m ? ListCards() : ListTable(), RefCard())
|
||
}
|
||
return D('flex:1;min-width:0;min-height:0;display:flex;flex-direction:column', topbar, Pg(...body))
|
||
}
|
||
|
||
// ---------- 2. edit slide-over
|
||
const E = s.ed || {}, edit = E.mode === 'edit', er = E.err || {}, dis = !!E.saving
|
||
const Cmp = () => {
|
||
const pos = E.pos || 50, hasV = !!E.out
|
||
const chip = (t, side) => S('position:absolute;top:8px;' + side + ':8px;height:20px;padding:0 6px;display:flex;align-items:center;background:var(--ui-bg);border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-weight:500;font-size:12px;' + (side === 'right' ? MONO : ''), t)
|
||
return D('position:relative;aspect-ratio:3/2;border:1px solid var(--ui-border);overflow:hidden;flex-shrink:0', Th(0, 'position:absolute;inset:0', 32),
|
||
hasV && D('position:absolute;inset:0;clip-path:inset(0 0 0 ' + pos + '%);background:repeating-linear-gradient(45deg, rgb(128 128 128 / 0.22) 0 2px, transparent 2px 6px), ' + TONES[1] + ';display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)'),
|
||
chip('Original', 'left'), hasV && chip(E.name || 'name', 'right'),
|
||
hasV && D('position:absolute;top:0;bottom:0;left:' + pos + '%;width:2px;margin-left:-1px;background:var(--ui-bg);box-shadow:0 0 0 1px var(--ui-border-accented)', S('position:absolute;top:50%;left:50%;width:' + (m ? 44 : 32) + 'px;height:' + (m ? 44 : 32) + 'px;margin:' + (m ? -22 : -16) + 'px 0 0 ' + (m ? -22 : -16) + 'px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg);border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);' + (E.focus ? 'outline:2px solid var(--ui-primary);outline-offset:2px;' : ''), I('chevrons-left-right', 16))),
|
||
!hasV && D('position:absolute;left:12px;right:12px;bottom:12px;padding:8px 12px;background:var(--ui-bg);border:1px solid var(--ui-border);color:var(--ui-text-toned)', 'Set a width or height to see a preview.'))
|
||
}
|
||
const Preview = () => D('display:flex;flex-direction:column;gap:12px;min-width:0', D('display:flex;align-items:baseline;gap:8px', S('font-weight:600;color:var(--ui-text-highlighted);flex:1', 'Preview'), S('font-size:12px;color:var(--ui-text-muted)', 'Sample: north-wall.jpg')), Cmp(),
|
||
D('display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px', D('display:flex;flex-direction:column', S('font-weight:500;color:var(--ui-text-highlighted)', 'Original'), S('font-size:12px;color:var(--ui-text-muted)', '4032 × 3024 · 3.9 MB')), D('display:flex;flex-direction:column', S('font-weight:500;color:var(--ui-text-highlighted);' + MONO, E.name || 'name'), S('font-size:12px;color:var(--ui-text-muted)', E.out ? E.out + ' · about ' + E.est : 'No output yet'))),
|
||
S('font-size:12px;color:var(--ui-text-muted)', 'Drag the divider, or focus it and use the arrow keys. The estimate comes from a sample; output is never larger than the source.'),
|
||
D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;color:var(--ui-text-highlighted)', 'URL'), D('display:flex;gap:8px', D('flex:1;min-width:0;display:flex;align-items:center;min-height:' + (m ? 44 : 32) + 'px;padding:0 12px;background:var(--ui-bg-elevated);border:1px solid var(--ui-border);' + MONO + ';font-size:12px;overflow:hidden;white-space:nowrap;text-overflow:ellipsis;color:' + (E.name ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)'), '/media/north-wall.jpg?v=' + (E.name ? E.name : '<name>')), B(E.copied ? 'Copied' : 'Copy', { icon: E.copied ? 'check' : 'copy', dis: !E.name }))))
|
||
const FITS = [['cover', 'Cover', 'Fills the box and crops what overflows.'], ['contain', 'Contain', 'Whole image inside the box, padded to the box size.'], ['inside', 'Inside', 'Whole image inside the box, no padding.']]
|
||
const Form = () => {
|
||
const k = []
|
||
if (E.saveErr) k.push(Alert('error', 'circle-x', 'Couldn\'t save the variant', 'Check your connection and try again.', 'Try again'))
|
||
if (edit) k.push(Alert('warning', 'triangle-alert', null, 'Changing dimensions, fit, format or quality purges ' + (E.files || 244) + ' generated files; they will be regenerated on next request.'))
|
||
k.push(Field('Name', E.name, { mono: true, ph: 'e.g. hero', lock: edit, err: er.name, dis, focus: !edit && E.focusName, help: edit ? 'The name can\'t be changed.' : 'Lowercase letters, numbers and dashes. Can\'t be changed later.', count: !edit && E.name ? E.name.length + '/32' : '' }))
|
||
k.push(D('display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:12px', Field('Width', E.w, { suffix: 'px', ph: 'auto', err: er.w || er.dim, dis }), Field('Height', E.h, { suffix: 'px', ph: 'auto', err: er.h, eb: !!er.dim, dis })), !er.w && !er.h && !er.dim && S('color:var(--ui-text-muted);font-size:12px;margin-top:-8px', 'Set a width, a height, or both. 1–8000 px. Images are never enlarged.'))
|
||
const fit = E.fit || 'cover'
|
||
k.push(D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;color:var(--ui-text-highlighted)', 'Fit'), D('display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:8px;opacity:' + (dis ? 0.6 : 1), ...FITS.map(([id, t]) => D('min-height:' + (m ? 44 : 36) + 'px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid ' + (id === fit ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (id === fit ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';font-weight:' + (id === fit ? 600 : 400) + ';color:var(--ui-text-highlighted)', id === fit && I('check', 14), t))), S('color:var(--ui-text-muted);font-size:12px', FITS.find(f => f[0] === fit)[2])))
|
||
const fmt = E.fmt || 'Original'
|
||
k.push(Field('Format', fmt, { sel: true, dis, help: fmt === 'Original' ? 'Keeps the format of each source image.' : null }))
|
||
if (fmt !== 'PNG') k.push(D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;color:var(--ui-text-highlighted)', 'Quality'), D('display:flex;align-items:center;gap:16px;opacity:' + (dis ? 0.6 : 1), D('flex:1;position:relative;height:' + (m ? 44 : 24) + 'px;display:flex;align-items:center', S('position:absolute;left:0;right:0;height:4px;background:var(--ui-bg-accented)'), S('position:absolute;left:0;width:' + Math.min(100, Math.max(0, E.qv === undefined ? 80 : E.qv)) + '%;height:4px;background:var(--ui-primary)'), S('position:absolute;left:calc(' + Math.min(100, Math.max(0, E.qv === undefined ? 80 : E.qv)) + '% - 8px);width:16px;height:16px;background:var(--ui-primary);border:2px solid var(--ui-bg);box-shadow:0 0 0 1px var(--ui-primary)')), D('width:84px', Field('', String(E.q === undefined ? 80 : E.q), { mono: true, err: er.q ? ' ' : null, eb: !!er.q }))), er.q ? S('display:flex;gap:6px;color:var(--ui-error);font-size:12px', I('circle-alert', 14, 'margin-top:3px'), er.q) : S('color:var(--ui-text-muted);font-size:12px', '1–100. Higher is sharper and larger.')))
|
||
k.push(D('display:flex;flex-direction:column;gap:12px;padding-top:4px', SwRow('Generate on upload', 'Creates this variant when an image is uploaded.', !!E.gen, dis), SwRow('Enabled', 'Disabled variants return 400 on the public site.', E.en !== false, dis)))
|
||
return D('display:flex;flex-direction:column;gap:16px;min-width:0', ...k)
|
||
}
|
||
const EditPanel = () => {
|
||
const title = edit ? 'Edit variant: ' + E.name : 'Add variant'
|
||
const label = E.saving ? 'Saving…' : edit ? 'Save changes' : 'Add variant'
|
||
const hd = D('height:56px;flex-shrink:0;display:flex;align-items:center;gap:' + (m ? 4 : 12) + 'px;padding:0 ' + (m ? 6 : 20) + 'px;border-bottom:1px solid var(--ui-border);background:var(--ui-bg)', m && S('width:44px;height:44px;display:flex;align-items:center;justify-content:center', I('arrow-left', 20)), S('flex:1;min-width:0;font:600 16px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', title), !m && S('width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x')))
|
||
const ft = D('height:' + (m ? 72 : 64) + 'px;flex-shrink:0;display:flex;align-items:center;gap:' + (m ? 8 : 12) + 'px;padding:0 ' + (m ? 16 : 20) + 'px;border-top:1px solid var(--ui-border);background:var(--ui-bg)', !m && D('flex:1;display:flex;align-items:center;gap:6px;color:var(--ui-text-toned)', E.dirty && I('circle-dot', 14), E.dirty ? 'Unsaved changes' : ''), B('Cancel', { full: m, dis }), S('height:' + bh + 'px;padding:0 16px;display:flex;align-items:center;justify-content:center;gap:6px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;flex:' + (m ? 1 : '0 0 auto') + ';opacity:' + (E.disSave ? 0.4 : 1), E.saving && I('loader-circle'), label))
|
||
const body = m
|
||
? D('flex:1;min-height:0;overflow:hidden;background:var(--ui-bg)', D('padding:16px;display:flex;flex-direction:column;gap:24px;margin-top:' + (-(s.scroll || 0)) + 'px', Form(), D('border-top:1px solid var(--ui-border);padding-top:16px', Preview())))
|
||
: D('flex:1;min-height:0;overflow:hidden;display:grid;grid-template-columns:440px minmax(0,1fr);background:var(--ui-bg)', D('padding:20px;overflow:hidden', Form()), D('padding:20px;border-left:1px solid var(--ui-border);background:var(--ui-bg-muted);overflow:hidden', Preview()))
|
||
return D('width:' + (m ? '100%' : '960px') + ';height:100%;display:flex;flex-direction:column;background:var(--ui-bg);' + (m ? '' : 'box-shadow:var(--rc-shadow-modal)'), hd, body, ft)
|
||
}
|
||
|
||
// ---------- 3. media library
|
||
const ML = s.ml || {}
|
||
const Tb = () => {
|
||
const f = ML.filter || 'All'
|
||
const seg = D('display:flex;border:1px solid var(--ui-border-accented);background:var(--ui-bg);flex-shrink:0', ...['All', 'With variants', 'Without'].map(t => S('height:' + (m ? 44 : 30) + 'px;padding:0 ' + (m ? 10 : 12) + 'px;display:flex;align-items:center;font-weight:500;background:' + (t === f ? 'var(--ui-primary)' : 'transparent') + ';color:' + (t === f ? 'var(--ui-bg)' : 'var(--ui-text-toned)') + ';' + (m ? 'flex:1;justify-content:center;font-size:12px' : ''), t)))
|
||
const sortLbl = ML.sort === 'total' ? (m ? 'Largest total size' : 'Largest total size (original + variants)') : 'Newest first'
|
||
const sortBox = D('position:relative;' + (m ? 'flex:1' : ''), D('height:' + (m ? 44 : 30) + 'px;padding:0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;', I('arrow-up-down', 14), S('flex:1', sortLbl), I('chevron-down', 14)),
|
||
ML.menu && D('position:absolute;z-index:20;top:' + (m ? 48 : 34) + 'px;right:0;width:' + (m ? '100%' : '340px') + ';background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px', ...['Newest first', 'Oldest first', 'Filename', 'Largest total size (original + variants)'].map((t, i) => D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 44 : 32) + 'px;padding:0 8px;' + (i === 3 ? 'background:var(--ui-bg-elevated);font-weight:500;' : '') + 'color:var(--ui-text-highlighted)', S('width:16px;display:flex', i === 3 && I('check', 14)), t, i === 3 && S('margin-left:auto;font-size:11px;color:var(--ui-text-muted);' + MONO, 'new')))))
|
||
return D('display:flex;flex-direction:' + (m ? 'column' : 'row') + ';gap:8px;align-items:' + (m ? 'stretch' : 'center') + ';flex-shrink:0', D('height:' + (m ? 44 : 30) + 'px;padding:0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-muted);' + (m ? '' : 'width:240px'), I('search', 14), 'Search by filename'), !m && D('flex:1'), m ? D('display:flex;gap:8px', seg) : seg, sortBox)
|
||
}
|
||
const MCard = im => {
|
||
const hover = ML.sort === 'total'
|
||
return D('display:flex;flex-direction:column;background:var(--ui-bg);border:1px solid var(--ui-border);min-width:0', Th(im.tone, 'aspect-ratio:4/3;border-bottom:1px solid var(--ui-border)'),
|
||
D('padding:10px 12px;display:flex;flex-direction:column;gap:6px;min-width:0', D('display:flex;flex-direction:column;gap:2px;min-width:0', S('font-weight:500;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), S('font-size:12px;color:var(--ui-text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis', hover ? im.tot + ' total' + (im.vc ? ' · ' + im.sz + ' + ' + im.vs : '') : im.sz + ' · ' + im.dim)),
|
||
D('display:flex;flex-wrap:wrap;gap:4px', im.used ? Bd('Used in ' + im.used, 'used') : Bd('Unused', 'unused'), im.vc > 0 && Bd(im.vc + (im.vc === 1 ? ' variant' : ' variants'), 'vars'))))
|
||
}
|
||
const MediaPage = () => {
|
||
let ids = ML.ids || IM.map(x => x.id)
|
||
const body = [PH('Media', 'Images you upload are kept as they are. Variants are generated from them.', B('Upload', { k: 'primary', icon: 'upload' })), Tb()]
|
||
if (ML.empty) body.push(Empty('layers', 'No images with variants', 'Variants are generated when an image is requested with ?v=<name>, or from its Variants tab.', 56, B('Show all images')))
|
||
else body.push(D('display:grid;grid-template-columns:repeat(' + (m ? 2 : 5) + ',minmax(0,1fr));gap:' + (m ? 10 : 16) + 'px', ...ids.map(byId).map(MCard)))
|
||
return D('flex:1;min-width:0;min-height:0;display:flex;flex-direction:column', topbar, Pg(...body), ML.ann && D('position:absolute;z-index:20;' + (m ? 'left:16px;bottom:16px' : 'left:248px;bottom:20px'), Note(ML.ann)))
|
||
}
|
||
|
||
// ---------- 4. detail panel
|
||
const DT = s.dt || {}
|
||
const stOf = (v, i) => (DT.rows && DT.rows[i]) || 'none'
|
||
const VR = (v, i) => {
|
||
const st = stOf(v, i), gen = st === 'gen', none = st === 'none', busy = st === 'busy', fail = st === 'fail'
|
||
const bk = { gen: ['Generated', 'ok'], none: ['Not generated', 'none'], busy: ['Generating', 'busy'], fail: ['Failed', 'fail'] }[st]
|
||
const meta = gen ? v.out + ' · ' + v.fmt + ' · ' + v.kb + ' · ' : v.out + ' · ' + v.fmt
|
||
const ib = (ic, lab) => m ? B(lab, { icon: ic, h: 44, px: 12 }) : B(null, { icon: ic, sq: true, h: 28 })
|
||
const acts = gen ? [ib('link', 'Copy URL'), ib('eye', 'Preview'), ib('eraser', 'Purge')] : none ? [ib('link', 'Copy URL'), ib('zap', 'Generate now')] : fail ? [B('Retry', { icon: 'rotate-cw', h: m ? 44 : 28, fs: 12, z: 14 })] : []
|
||
return D('display:flex;flex-direction:column;gap:' + (m ? 8 : 4) + 'px;padding:10px 12px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0') + ';background:' + (busy ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)'),
|
||
D('display:flex;align-items:center;gap:8px', S(MONO + ';font-weight:500;font-size:13px;color:var(--ui-text-highlighted)', v.n), Bd(bk[0], bk[1]), D('flex:1'), !m && D('display:flex;gap:4px', ...acts)),
|
||
D('color:var(--ui-text-toned);font-size:12px;display:flex;gap:4px;flex-wrap:wrap', S('', meta), gen && S('font-weight:500;color:var(--ui-success)', v.sv + ' vs original')),
|
||
fail && D('display:flex;align-items:center;gap:6px;color:var(--ui-error);font-size:12px', I('circle-alert', 14), DT.cause && DT.cause[i] || 'Image too large'),
|
||
m && acts.length > 0 && D('display:flex;gap:8px;flex-wrap:wrap', ...acts))
|
||
}
|
||
const DetailPanel = () => {
|
||
const im = byId(DT.id || 1), vlist = DT.none ? [] : en
|
||
const gens = vlist.filter((v, i) => stOf(v, i) === 'gen').length
|
||
const anyBusy = vlist.some((v, i) => stOf(v, i) === 'busy')
|
||
const noFmt = DT.nofmt
|
||
const tabs = D('display:flex;gap:20px;border-bottom:1px solid var(--ui-border);padding:0 ' + (m ? 16 : 20) + 'px;flex-shrink:0', ...['Details', 'Variants'].map(t => S('height:' + (m ? 44 : 40) + 'px;display:flex;align-items:center;border-bottom:2px solid ' + (t === 'Variants' ? 'var(--ui-primary)' : 'transparent') + ';margin-bottom:-1px;font-weight:' + (t === 'Variants' ? 500 : 400) + ';color:' + (t === 'Variants' ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)'), t)))
|
||
const orig = D('display:flex;gap:12px;align-items:center;padding:12px;border:1px solid var(--ui-border);background:var(--ui-bg-muted)', Th(im.tone, 'width:72px;height:54px;flex-shrink:0;border:1px solid var(--ui-border)', 16), D('display:flex;flex-direction:column;min-width:0', S('font-size:12px;color:var(--ui-text-muted)', 'Original · kept untouched'), S('font-weight:600;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), S('font-size:12px;color:var(--ui-text-toned)', im.ty + ' · ' + im.dim + ' · ' + im.sz)))
|
||
const k = [orig]
|
||
if (noFmt) k.push(Alert('info', 'info', 'Variants aren\'t generated for this format.', noFmt))
|
||
else if (DT.none) k.push(Empty('layers', 'No variants defined', 'Define a variant in Settings to get resized copies of this image.', 28, S('display:inline-flex;align-items:center;gap:4px;font-weight:500;color:var(--ui-text-highlighted);text-decoration:underline;text-underline-offset:3px;min-height:' + (m ? 44 : 20) + 'px;align-items:center', 'Settings → Media → Variants', I('arrow-up-right', 14))))
|
||
else {
|
||
k.push(D('display:flex;align-items:center;gap:12px', D('flex:1;display:flex;flex-direction:column', S('font-weight:600;color:var(--ui-text-highlighted)', gens + ' of ' + vlist.length + ' generated'), gens === 0 && !anyBusy && S('font-size:12px;color:var(--ui-text-muted)', 'Nothing generated yet.'), anyBusy && S('font-size:12px;color:var(--ui-info)', 'Generating variants…')), B(anyBusy ? 'Generating…' : 'Generate all', { icon: anyBusy ? 'loader-circle' : 'zap', dis: anyBusy })))
|
||
k.push(D('border:1px solid var(--ui-border)', ...vlist.map(VR)))
|
||
}
|
||
return D('position:absolute;inset:0;z-index:40;display:flex;justify-content:flex-end;background:' + (m ? 'var(--ui-bg)' : 'var(--rc-overlay)'),
|
||
D('width:' + (m ? '100%' : '460px') + ';height:100%;background:var(--ui-bg);display:flex;flex-direction:column;' + (m ? '' : 'box-shadow:var(--rc-shadow-modal)'),
|
||
D('height:56px;flex-shrink:0;display:flex;align-items:center;gap:' + (m ? 4 : 12) + 'px;padding:0 ' + (m ? 6 : 20) + 'px;border-bottom:1px solid var(--ui-border)', m && S('width:44px;height:44px;display:flex;align-items:center;justify-content:center', I('arrow-left', 20)), S('flex:1;min-width:0;font:600 16px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), !m && S('width:28px;height:28px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x'))),
|
||
tabs, D('flex:1;min-height:0;overflow:hidden;padding:' + (m ? 16 : 20) + 'px;display:flex;flex-direction:column;gap:16px', ...k)))
|
||
}
|
||
const PrevDlg = () => {
|
||
const v = en[0], pane = (title, sub, inner) => D('display:flex;flex-direction:column;gap:8px;min-width:0', D('height:' + (m ? 180 : 300) + 'px;overflow:hidden;border:1px solid var(--ui-border);background:var(--ui-bg-muted);display:flex;align-items:' + (m ? 'flex-start' : 'center') + ';justify-content:' + (m ? 'flex-start' : 'center'), inner), D('display:flex;flex-direction:column', S('font-weight:600;color:var(--ui-text-highlighted)', title), S('font-size:12px;color:var(--ui-text-toned)', sub)))
|
||
const body = D('padding:20px 24px 0;display:flex;flex-direction:column;gap:12px;min-height:0;overflow:hidden', D('display:grid;grid-template-columns:' + (m ? 'minmax(0,1fr)' : 'minmax(0,1fr) minmax(0,1fr)') + ';gap:16px', pane(v.n + ' · ' + v.out + ' · ' + v.fmt, v.kb + ' · saves 99%', Th(1, 'width:400px;height:300px', 32)), pane('Original · 4032×3024 · JPEG', '3.9 MB · cropped to the preview area', Th(0, 'width:4032px;height:3024px;flex-shrink:0', 32))), S('font-size:12px;color:var(--ui-text-muted)', 'Both images are shown at actual size. Scroll to pan.'))
|
||
return Shell({ title: 'Preview: ' + v.n, w: 880, top: 80, close: true, cancel: 'Close' }, body)
|
||
}
|
||
|
||
// ---------- 5. cache
|
||
const C = s.cache || {}
|
||
const empty = C.state === 'empty'
|
||
const Tile = (ic, lab, val, sub) => D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:' + (m ? 12 : 16) + 'px;display:flex;flex-direction:column;gap:4px;min-width:0', D('display:flex;align-items:center;gap:6px;color:var(--ui-text-muted)', I(ic, 14), S('', lab)), C.state === 'loading' ? Skl('60%', 28) : S('font:600 ' + (m ? 22 : 28) + 'px/36px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', val), C.state === 'loading' ? Skl('40%', 11) : S('font-size:12px;color:var(--ui-text-muted)', sub))
|
||
const BL = () => {
|
||
const rows = [...V].sort((a, b) => parseInt(b.size) - parseInt(a.size))
|
||
const mx = 84
|
||
return Card('By variant', null, D('padding:8px 16px;display:flex;flex-direction:column;gap:' + (m ? 16 : 12) + 'px;margin:8px 0', ...rows.map(v => D('display:flex;flex-direction:column;gap:6px', D('display:flex;align-items:baseline;gap:8px', S(MONO + ';font-weight:500;color:var(--ui-text-highlighted)', v.n), !v.on && Bd('Disabled', 'off', 18), D('flex:1'), S('color:var(--ui-text-toned);font-size:12px', v.files.toLocaleString('en-US') + ' files · ' + v.size)), D('height:8px;background:var(--ui-bg-accented)', S('display:block;height:8px;width:' + (parseInt(v.size) / mx * 100) + '%;background:' + (v.on ? 'var(--ui-primary)' : 'var(--ui-border-accented)')))))))
|
||
}
|
||
const TOP = [['north-wall.jpg', 6, '5.8 MB', 0], ['lime-plaster.jpg', 6, '4.9 MB', 1], ['kitchen-after.webp', 5, '4.4 MB', 1], ['kitchen-before.webp', 5, '3.9 MB', 2], ['floorboards.jpg', 5, '3.6 MB', 0], ['IMG_4471.jpg', 4, '3.1 MB', 2], ['stone-wall-detail.jpg', 4, '2.8 MB', 2], ['bathroom-tiles.png', 4, '2.6 MB', 1], ['roof-lantern.jpg', 3, '2.3 MB', 3], ['chimney-stack.jpg', 3, '2.1 MB', 0]]
|
||
const TL = () => Card('Top 10 images by variant disk usage', null, ...TOP.map(([n, c, sz, t], i) => D('display:flex;align-items:center;gap:12px;min-height:' + (m ? 60 : 40) + 'px;padding:0 ' + (m ? 12 : 16) + 'px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), S('width:20px;color:var(--ui-text-muted);font-size:12px;' + MONO, i + 1), Th(t, 'width:32px;height:24px;flex-shrink:0;border:1px solid var(--ui-border)', 12), D('flex:1;min-width:0;display:flex;flex-direction:' + (m ? 'column' : 'row') + ';gap:' + (m ? 0 : 12) + 'px', S('flex:1;min-width:0;font-weight:500;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', n), S('color:var(--ui-text-muted);font-size:12px;white-space:nowrap;' + (m ? '' : 'width:150px'), c + ' variants' + (m ? ' · ' + sz : ''))), !m && S('width:60px;text-align:right;color:var(--ui-text-toned)', sz), B(m ? null : 'Purge', { icon: 'eraser', h: m ? 44 : 26, fs: 12, z: 14, px: 10, sq: m }))))
|
||
const CachePage = () => {
|
||
const busy = C.state === 'purging'
|
||
const body = [PH('Cache', 'Generated variant files. Purged files are generated again on the next request.', B('Purge unused', { icon: 'clock', dis: busy || empty }), B('Purge all variants', { k: 'del', icon: 'trash-2', dis: busy || empty })), SetTabs('Cache')]
|
||
if (C.state === 'error') body.push(Alert('error', 'circle-x', 'Couldn\'t load cache stats', 'Check your connection and try again.', 'Try again'))
|
||
else {
|
||
if (busy) body.push(D('display:flex;flex-direction:column;gap:10px;padding:12px 14px;background:var(--rc-info-soft);color:var(--ui-info)', D('display:flex;gap:10px;align-items:center', I('loader-circle'), S('font-weight:600', 'Purging 1,284 files'), D('flex:1'), S('color:var(--ui-text-toned)', '640 of 1,284 · 106 MB freed')), D('height:6px;background:var(--ui-bg-accented)', S('display:block;height:6px;width:50%;background:var(--ui-info)'))))
|
||
const z = empty
|
||
body.push(D('display:grid;grid-template-columns:repeat(' + (m ? 2 : 4) + ',minmax(0,1fr));gap:' + (m ? 8 : 16) + 'px;opacity:' + (busy ? 0.6 : 1), Tile('files', 'Generated files', z ? '0' : '1,284', z ? 'Nothing cached' : 'Across 6 variants'), Tile('hard-drive', 'Disk used', z ? '0 MB' : '212 MB', z ? 'Nothing cached' : 'Originals not included'), Tile('layers', 'Variants defined', '6', '5 enabled, 1 disabled'), Tile('ban', 'Rejected requests', '37', 'Last 7 days')))
|
||
if (z) body.push(Empty('database', 'Nothing generated yet', 'Variants are generated on the first request with ?v=<name>, or from an image\'s Variants tab.', 48))
|
||
else if (C.state === 'loading') body.push(D('display:grid;grid-template-columns:' + (m ? 'minmax(0,1fr)' : '460px minmax(0,1fr)') + ';gap:16px', Card('By variant', null, D('padding:16px;display:flex;flex-direction:column;gap:20px', ...Array.from({ length: 5 }, () => D('display:flex;flex-direction:column;gap:8px', Skl('50%'), Skl('100%', 8))))), !m && Card('Top 10 images by variant disk usage', null, D('padding:16px;display:flex;flex-direction:column;gap:20px', ...Array.from({ length: 6 }, () => Skl('90%'))))))
|
||
else body.push(D('display:grid;grid-template-columns:' + (m ? 'minmax(0,1fr)' : '460px minmax(0,1fr)') + ';gap:16px;align-items:start;opacity:' + (busy ? 0.6 : 1), BL(), TL()))
|
||
}
|
||
return D('flex:1;min-width:0;min-height:0;display:flex;flex-direction:column', topbar, Pg(...body))
|
||
}
|
||
const cacheModal = () => {
|
||
const k = s.modal
|
||
if (k === 'purgeAll' || k === 'purgeAllOk') return { title: 'Purge all variants?', desc: 'This removes 1,284 generated files (212 MB) for every variant. Originals aren\'t touched. Files are generated again on request.', body: [Field('Type purge to confirm', k === 'purgeAllOk' ? 'purge' : 'pur', { mono: true, focus: true })], cancel: 'Cancel', confirm: 'Purge all variants', tone: 'danger', cdis: k === 'purgeAll' }
|
||
if (k === 'purgeUnused') return { title: 'Purge unused variants?', desc: 'This removes 312 files (41 MB) from enabled variants that haven\'t been requested for 30 days. Files are generated again on request.', cancel: 'Cancel', confirm: 'Purge 312 files' }
|
||
if (k === 'purgeVariant') return { title: 'Purge cache for “hero”?', desc: 'This removes 244 generated files (84 MB). Files are generated again on the next request.', cancel: 'Cancel', confirm: 'Purge 244 files' }
|
||
if (k === 'deleteVariant') return { title: 'Delete variant “hero”?', desc: 'This removes the variant and its 244 generated files (84 MB). Articles that use ?v=hero will start returning 400. This can\'t be undone.', cancel: 'Cancel', confirm: 'Delete variant', tone: 'danger', loading: !!s.loadingModal }
|
||
if (k === 'discard') return { title: 'Discard changes?', desc: 'You have unsaved changes to “hero”. If you close now, they\'re lost.', cancel: 'Keep editing', confirm: 'Discard changes', tone: 'danger' }
|
||
if (k === 'deleteImage') return { title: 'Delete this image?', desc: '"north-wall.jpg" is used in 2 articles. If you delete it, they will show a broken image. This can\'t be undone.', body: [D('display:flex;align-items:center;gap:8px;padding:10px 12px;background:var(--rc-warning-soft);color:var(--ui-warning)', I('layers'), S('font-weight:500;color:var(--ui-text-highlighted)', 'Also removes 4 generated variants (260 KB)'))], cancel: 'Cancel', confirm: 'Delete image', tone: 'danger' }
|
||
}
|
||
const Modal = () => { const c = cacheModal(); return Shell(c, D('padding:' + (c.body ? '20px 24px 0' : '0') + ';display:flex;flex-direction:column;gap:16px', ...(c.body || []))) }
|
||
|
||
// ---------- 6. editor + picker
|
||
const ED = s.edr || {}
|
||
const VOPT = [['Original', '4032×3024', 'JPEG'], ...en.map(v => [v.n[0].toUpperCase() + v.n.slice(1), dim(v), v.fmt === 'JPEG' ? 'JPEG' : v.fmt])]
|
||
const VSel = () => {
|
||
const cur = ED.cur ?? 0, open = ED.open, none = ED.none
|
||
const opts = none ? [VOPT[0]] : VOPT
|
||
const lab = o => o[0] + ' · ' + o[1] + ' · ' + o[2]
|
||
return D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;color:var(--ui-text-highlighted)', 'Variant'),
|
||
D('display:flex;align-items:center;min-height:' + (m ? 44 : 40) + 'px;border:1px solid ' + (open ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:var(--ui-bg);font-size:' + (m ? 16 : 13) + 'px;' + (open ? 'box-shadow:0 0 0 1px var(--ui-primary)' : ''), S('flex:1;min-width:0;padding:0 12px;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', lab(opts[cur])), S('padding:0 12px;display:flex;color:var(--ui-text-muted)', I('chevron-down', 16))),
|
||
open && D('border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);background:var(--ui-bg);padding:4px', ...opts.map((o, i) => D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 44 : 36) + 'px;padding:0 8px;background:' + (i === cur ? 'var(--ui-bg-elevated)' : 'transparent') + ';color:var(--ui-text-highlighted)', S('width:16px;display:flex', i === cur && I('check', 14)), S('font-weight:' + (i === cur ? 600 : 400) + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis', lab(o))))),
|
||
none ? D('display:flex;align-items:center;gap:6px;color:var(--ui-text-muted);font-size:12px', I('info', 14), 'No variants defined. Add one in Settings → Media → Variants.') : D('color:var(--ui-text-muted);font-size:12px;' + MONO, '/media/north-wall.jpg' + (cur ? '?v=' + opts[cur][0].toLowerCase() : '')))
|
||
}
|
||
const pT = id => D('display:flex;flex-direction:column;gap:6px;min-width:0', D('position:relative;' + (id === 1 ? 'outline:2px solid var(--ui-primary);outline-offset:2px;' : ''), Th(byId(id).tone, 'aspect-ratio:1/1;border:1px solid var(--ui-border)', 20), id === 1 && S('position:absolute;top:4px;right:4px;width:20px;height:20px;display:flex;align-items:center;justify-content:center;background:var(--ui-primary);color:var(--ui-bg)', I('check', 14))), S('font-size:12px;color:var(--ui-text-toned);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', byId(id).n))
|
||
const Picker = () => Shell({ title: 'Add an image', close: true, w: 640, top: 24, cancel: 'Cancel', confirm: 'Insert image' },
|
||
D('display:flex;flex-direction:column;min-height:0;flex:1;overflow:hidden', D('display:flex;gap:20px;border-bottom:1px solid var(--ui-border);padding:0 24px;margin-top:16px', ...['Upload', 'Library'].map(t => S('height:' + (m ? 44 : 36) + 'px;display:flex;align-items:center;gap:6px;border-bottom:2px solid ' + (t === 'Library' ? 'var(--ui-primary)' : 'transparent') + ';margin-bottom:-1px;font-weight:' + (t === 'Library' ? 500 : 400) + ';color:' + (t === 'Library' ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)'), I(t === 'Upload' ? 'upload' : 'images', 14), t))),
|
||
D('padding:16px 24px 0;display:flex;flex-direction:column;gap:12px;overflow:hidden', D('display:grid;grid-template-columns:repeat(' + (m ? 3 : 4) + ',minmax(0,1fr));gap:12px ' + (m ? 8 : 12) + 'px', ...[1, 2, 3, 5].slice(0, m ? 3 : 4).map(pT)),
|
||
D('display:flex;flex-direction:column;gap:12px;padding-top:12px;border-top:1px solid var(--ui-border)', D('display:flex;gap:12px;align-items:center', Th(0, 'width:48px;height:48px;flex-shrink:0;border:1px solid var(--ui-border)', 16), D('display:flex;flex-direction:column;min-width:0', S('font-weight:500;color:var(--ui-text-highlighted)', 'north-wall.jpg'), S('font-size:12px;color:var(--ui-text-muted)', '4032 × 3024 · 3.9 MB'))), VSel()))))
|
||
const Editor = () => {
|
||
const bl = ED.block
|
||
const img = D('position:relative;aspect-ratio:16/9;border:1px solid var(--ui-border);overflow:hidden', Th(0, 'position:absolute;inset:0;opacity:' + (bl ? 0.5 : 1), 40, bl ? 'image-off' : 'image'), S('position:absolute;top:8px;left:8px;height:20px;padding:0 6px;display:flex;align-items:center;background:var(--ui-bg);border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-size:12px;font-weight:500;' + MONO, 'hero · 1200×630'))
|
||
return D('flex:1;min-height:0;overflow:hidden;background:var(--ui-bg-muted);display:flex;flex-direction:column',
|
||
D('background:var(--ui-bg);border-bottom:1px solid var(--ui-border);padding:12px ' + pad + 'px;display:flex;align-items:center;gap:12px', S('flex:1;font:600 ' + (m ? 16 : 20) + 'px/28px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'How we price a bathroom renovation'), !m && B('Save', { dis: true }), !m && B('Publish', { k: 'primary' })),
|
||
D('flex:1;overflow:hidden;padding:' + (m ? '20px 16px' : '32px 0') + ';display:flex;justify-content:center', D('width:100%;max-width:680px;display:flex;flex-direction:column;gap:16px;font-family:\'Source Serif 4\',serif;font-size:' + (m ? 16 : 18) + 'px;line-height:1.66;color:var(--ui-text-toned)',
|
||
S('display:block', 'A bathroom is priced by what has to happen behind the walls before anything gets tiled.'),
|
||
bl && D('display:flex;flex-direction:column;border:1px solid var(--ui-warning);font-family:\'IBM Plex Sans\',sans-serif;font-size:13px;line-height:20px', img, Alert('warning', 'triangle-alert', ED.msg, 'This image returns an error on the public site.', 'Pick another variant', { icon: 'layers' })),
|
||
S('display:block', 'We don\'t add a margin on materials you choose yourself.'))))
|
||
}
|
||
const EditorPage = () => D('flex:1;min-width:0;min-height:0;display:flex;flex-direction:column', topbar, Editor())
|
||
|
||
// ---------- compose
|
||
const k = s.kind
|
||
let main
|
||
if (k === 'list') main = ListPage()
|
||
else if (k === 'edit') main = m ? null : ListPage()
|
||
else if (k === 'media' || k === 'detail') main = MediaPage()
|
||
else if (k === 'cache') main = CachePage()
|
||
else main = EditorPage()
|
||
const root = D('width:' + (m ? 390 : 1440) + 'px;height:' + (m ? 844 : 900) + 'px;position:relative;display:flex;flex-direction:' + (m ? 'column' : 'row') + ';background:var(--ui-bg-muted);color:var(--ui-text);font-family:\'IBM Plex Sans\',sans-serif;font-size:13px;line-height:20px;overflow:hidden',
|
||
!m && side, main,
|
||
k === 'edit' && (m ? D('position:absolute;inset:0;z-index:40;display:flex', EditPanel()) : D('position:absolute;inset:0;z-index:40;display:flex;justify-content:flex-end;background:var(--rc-overlay)', EditPanel())),
|
||
k === 'detail' && (DT.preview ? [DetailPanel(), PrevDlg()] : DetailPanel()),
|
||
k === 'editor' && ED.picker && Picker(),
|
||
s.modal && Modal(), s.toasts && Toasts())
|
||
return { view: root }
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|