305 lines
50 KiB
HTML
305 lines
50 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>
|
||
<dc-import name="AppFrame" s="{{ shell }}" mobile="{{ mob }}" slot-body="{{ main }}" slot-overlays="{{ ov }}" hint-size="100%,100%"></dc-import>
|
||
</x-dc>
|
||
<script type="text/x-dc" data-dc-script data-props="{"$preview":{"width":1440,"height":900},"s":{"editor":null,"tsType":"object"},"mobile":{"editor":null,"tsType":"boolean"}}">
|
||
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 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 === 'inv' ? 'transparent' : 'var(--ui-bg)'
|
||
const fg = k === 'primary' || k === 'danger' ? 'var(--ui-bg)' : k === 'del' ? 'var(--ui-error)' : k === 'inv' ? 'var(--ui-text-inverted)' : 'var(--ui-text-highlighted)'
|
||
const bd = k === 'primary' || k === 'danger' ? bg : k === 'inv' ? 'var(--ui-text-inverted)' : 'var(--ui-border-accented)'
|
||
return S('height:' + (o.h || bh) + '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;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 CHECK = 'conic-gradient(var(--ui-bg-accented) 25%, var(--ui-bg) 0 50%, var(--ui-bg-accented) 0 75%, var(--ui-bg) 0) 0 0/16px 16px'
|
||
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 mark = h('svg', { width: 28, height: 28, viewBox: '0 0 16 16', shapeRendering: 'crispEdges' }, h('rect', { width: 16, height: 16, fill: '#111' }), h('path', { fill: '#fff', fillRule: 'evenodd', d: 'M4 3h6v1h1v1h1v3h-1v1h-1v1h1v1h1v2h-2v-2h-1v-1H6v3H4zm2 2v3h4V5z' }))
|
||
const Th = (im, t = '', z = 24) => D('position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);background:' + (im.alpha ? CHECK : 'repeating-linear-gradient(135deg, var(--ui-border) 0 1px, transparent 1px 12px), ' + TONES[im.tone % 4]) + ';' + t, im.svg ? mark : I('image', z, 'opacity:0.55'))
|
||
const BADGE = { unused: ['background:var(--rc-warning-soft);color:var(--ui-warning)', 'unlink'], dup: ['background:var(--rc-info-soft);color:var(--ui-info)', 'copy'], used: ['border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)'], on: ['background:var(--ui-bg-inverted);color:var(--ui-text-inverted)', 'globe'], draft: ['border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)', 'pencil-line'], vars: ['background:var(--rc-info-soft);color:var(--ui-info)', 'layers'] }
|
||
const varsBadge = im => im.vc > 0 && Bd(im.vc + (im.vc === 1 ? ' variant' : ' variants'), 'vars')
|
||
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;' + BADGE[k][0], BADGE[k][1] && I(BADGE[k][1], 12), t)
|
||
const Cb = (on, z = 20, ind) => S('width:' + z + 'px;height:' + z + 'px;flex-shrink:0;display:flex;align-items:center;justify-content:center;border:1px solid ' + (on || ind ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (on || ind ? 'var(--ui-primary)' : 'var(--ui-bg)') + ';color:var(--ui-bg)', (on || ind) && I(ind ? 'minus' : 'check', z - 6))
|
||
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', D('display:flex;flex-direction:column;gap:2px', S('font-weight:600', title), S('color:var(--ui-text-toned)', text)), action && B(action, { icon: 'rotate-cw', h: m ? 44 : 28, fs: 12, z: 14, ...(actionEl || {}) })))
|
||
const Empty = (icon, title, text, action, primary, pad = 56) => 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:320px', text), action && B(action, { k: primary ? 'primary' : 'outline', icon: primary ? 'upload' : null, px: 16 }))
|
||
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);color:var(--ui-text-toned);font:500 11px \'IBM Plex Mono\',monospace', t)
|
||
|
||
const IM = [
|
||
{ id: 1, n: 'north-wall.jpg', sz: '3.9 MB', dim: '4032 × 3024', ty: 'JPEG', used: 2, by: 'Marta Alves', d: '12 Sep 2026', tone: 0 },
|
||
{ id: 2, n: 'kitchen-after.webp', sz: '1.2 MB', dim: '2400 × 1600', ty: 'WEBP', used: 3, by: 'Inès Duarte', d: '8 Sep 2026', tone: 1 },
|
||
{ id: 3, n: 'kitchen-before.webp', sz: '1.1 MB', dim: '2400 × 1600', ty: 'WEBP', used: 1, by: 'Inès Duarte', d: '8 Sep 2026', tone: 2 },
|
||
{ id: 4, n: 'logo-renew.svg', sz: '8 KB', dim: 'Vector', ty: 'SVG', used: 5, by: 'jonas', d: '5 Sep 2026', alpha: true, svg: true, tone: 3 },
|
||
{ id: 5, n: 'floorboards-copy.jpg', sz: '2.4 MB', dim: '3600 × 2400', ty: 'JPEG', used: 3, by: 'Marta Alves', d: '2 Sep 2026', dup: true, tone: 0 },
|
||
{ id: 6, n: 'IMG_2208.jpg', sz: '2.4 MB', dim: '3600 × 2400', ty: 'JPEG', used: 2, by: 'jonas', d: '30 Aug 2026', dup: true, tone: 0 },
|
||
{ id: 7, n: 'IMG_4471.jpg', sz: '3.1 MB', dim: '3600 × 2400', ty: 'JPEG', used: 2, by: 'Marta Alves', d: '29 Aug 2026', dup: true, tone: 2 },
|
||
{ id: 8, n: 'window-frame.jpg', sz: '1.8 MB', dim: '3000 × 2000', ty: 'JPEG', used: 0, by: 'Inès Duarte', d: '28 Aug 2026', tone: 3 },
|
||
{ id: 9, n: 'stone-wall-detail.jpg', sz: '3.1 MB', dim: '3600 × 2400', ty: 'JPEG', used: 1, by: 'jonas', d: '21 Aug 2026', dup: true, tone: 2 },
|
||
{ id: 10, n: 'bathroom-tiles.png', sz: '860 KB', dim: '1800 × 1200', ty: 'PNG', used: 0, by: 'Marta Alves', d: '4 Aug 2026', alpha: true, tone: 1 },
|
||
{ id: 11, n: 'lime-plaster.jpg', sz: '2.1 MB', dim: '3200 × 2133', ty: 'JPEG', used: 2, by: 'Inès Duarte', d: '19 Jul 2026', tone: 1 },
|
||
{ id: 12, n: 'floorboards.jpg', sz: '2.4 MB', dim: '3600 × 2400', ty: 'JPEG', used: 4, by: 'Inès Duarte', d: '14 Jun 2026', dup: true, tone: 0 }
|
||
]
|
||
const VC = { 1: 4, 2: 5, 3: 5, 4: 0, 5: 3, 6: 0, 7: 2, 8: 0, 9: 1, 10: 3, 11: 4, 12: 5 }
|
||
IM.forEach(x => { x.vc = VC[x.id] || 0 })
|
||
const byId = id => IM.find(x => x.id === id)
|
||
const AR = [['How we price a bathroom renovation', 1], ['Winter checklist for timber windows', 0], ['Before and after: the Rue Verte kitchen', 1], ['Why we reuse original floorboards', 1], ['Choosing lime plaster for old stone walls', 0]]
|
||
const usedList = n => AR.slice(0, Math.min(n, 5))
|
||
const ids = s.ids || IM.map(x => x.id)
|
||
const imgs = ids.map(byId)
|
||
const sel = s.sel || []
|
||
const pl = n => n + (n === 1 ? ' article' : ' articles')
|
||
const label = im => im.n + ', ' + im.sz + ', ' + (im.used ? 'used in ' + pl(im.used) : 'unused')
|
||
const usedBadge = im => im.used ? Bd('Used in ' + im.used, 'used') : Bd('Unused', 'unused')
|
||
|
||
// ---------- Media page pieces
|
||
const Card = im => {
|
||
const on = sel.includes(im.id), hov = s.hover === im.id, foc = s.focus === im.id
|
||
const showCb = m || on || hov || foc || sel.length > 0
|
||
return D('position:relative;display:flex;flex-direction:column;background:' + (on ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text)') + ';border:1px solid ' + (on ? 'var(--ui-primary)' : hov ? 'var(--ui-border-accented)' : 'var(--ui-border)') + ';' + (on ? 'box-shadow:inset 0 0 0 1px var(--ui-primary);' : hov ? 'box-shadow:var(--rc-shadow-pop);' : '') + (foc ? 'outline:2px solid var(--ui-primary);outline-offset:2px;' : ''),
|
||
Th(im, 'aspect-ratio:4/3;border-bottom:1px solid ' + (on ? 'var(--ui-primary)' : 'var(--ui-border)')),
|
||
showCb && S('position:absolute;top:0;left:0;width:' + (m ? 44 : 36) + 'px;height:' + (m ? 44 : 36) + 'px;display:flex;align-items:center;justify-content:center', Cb(on, m ? 22 : 20)),
|
||
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:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), S('font-size:12px;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)') + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis', im.sz + ' · ' + im.dim)),
|
||
D('display:flex;flex-wrap:wrap;gap:4px', im.dup && Bd('Duplicate', 'dup'), usedBadge(im), varsBadge(im))))
|
||
}
|
||
const Grid = () => D('display:grid;grid-template-columns:repeat(' + (m ? 2 : 5) + ',minmax(0,1fr));gap:' + (m ? 10 : 16) + 'px', ...imgs.map(Card))
|
||
const Skel = () => D('display:grid;grid-template-columns:repeat(' + (m ? 2 : 5) + ',minmax(0,1fr));gap:' + (m ? 10 : 16) + 'px', ...Array.from({ length: m ? 6 : 10 }, () => D('background:var(--ui-bg);border:1px solid var(--ui-border)', S('display:block;aspect-ratio:4/3;background:var(--ui-bg-accented)'), D('padding:10px 12px;display:flex;flex-direction:column;gap:8px', S('display:block;width:70%;height:13px;background:var(--ui-bg-accented)'), S('display:block;width:45%;height:11px;background:var(--ui-bg-accented)'), S('display:block;width:60px;height:20px;background:var(--ui-bg-accented)')))))
|
||
const usedLink = im => im.used ? S('text-decoration:underline;text-underline-offset:3px;color:var(--ui-text-highlighted)', pl(im.used)) : Bd('Unused', 'unused')
|
||
const Table = () => {
|
||
const cols = '36px minmax(0,1.7fr) 110px 76px 64px 120px 130px 100px'
|
||
const hd = D('display:grid;grid-template-columns:' + cols + ';height:36px;align-items:center;padding:0 12px 0 0;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500', S('display:flex;justify-content:center', Cb(false, 18, sel.length > 0 && sel.length < imgs.length)), S('', 'Filename'), S('', 'Dimensions'), S('', 'Size'), S('', 'Type'), S('', 'Used in'), S('', 'Uploaded by'), S('display:flex;align-items:center;gap:4px;color:var(--ui-text-highlighted)', 'Uploaded', I('arrow-down', 12)))
|
||
return D('background:var(--ui-bg);border:1px solid var(--ui-border)', hd, ...imgs.map(im => {
|
||
const on = sel.includes(im.id), hov = s.hover === im.id
|
||
return D('display:grid;grid-template-columns:' + cols + ';height:56px;align-items:center;padding:0 12px 0 0;border-top:1px solid var(--ui-border);background:' + (on ? 'var(--rc-sand)' : hov ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)'),
|
||
S('display:flex;justify-content:center', Cb(on, 18)),
|
||
D('display:flex;align-items:center;gap:12px;min-width:0;padding-right:12px', Th(im, 'width:40px;height:40px;flex-shrink:0;border:1px solid var(--ui-border)', 16), S('font-weight:500;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), im.dup && Bd('Duplicate', 'dup'), varsBadge(im)),
|
||
S('', im.dim), S('', im.sz), S('', im.ty), S('', usedLink(im)), S('overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:8px', im.by), S('', im.d))
|
||
}))
|
||
}
|
||
const Cards = () => D('background:var(--ui-bg);border:1px solid var(--ui-border);margin:0 -16px;border-left:0;border-right:0', ...imgs.map(im => {
|
||
const on = sel.includes(im.id)
|
||
return D('display:flex;align-items:center;gap:0;min-height:80px;padding:8px 12px 8px 0;border-bottom:1px solid var(--ui-border);background:' + (on ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)'),
|
||
S('width:44px;height:44px;flex-shrink:0;display:flex;align-items:center;justify-content:center', Cb(on, 22)),
|
||
Th(im, 'width:56px;height:56px;flex-shrink:0;border:1px solid var(--ui-border)', 20),
|
||
D('flex:1;min-width:0;display:flex;flex-direction:column;gap:3px;padding-left:12px', S('font-weight:500;font-size:15px;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), S('font-size:12px', im.sz + ' · ' + im.dim + ' · ' + im.ty), D('display:flex;flex-wrap:wrap;gap:4px;align-items:center;font-size:12px', im.dup && Bd('Duplicate', 'dup'), usedBadge(im), varsBadge(im), S('', im.by + ' · ' + im.d))),
|
||
I('chevron-right', 16, 'margin-left:8px'))
|
||
}))
|
||
|
||
const dg = s.dupEmpty ? 0 : s.dupGroups ?? 2
|
||
const tabs = [['all', 'All', String(imgs.length)], ['unused', 'Unused', String(imgs.filter(x => !x.used).length)], ['dups', 'Duplicates', String(dg)]]
|
||
const tab = s.tab || 'all'
|
||
const TabRow = () => D('display:flex;gap:20px;flex:1;' + (m ? 'border-bottom:1px solid var(--ui-border)' : ''), ...tabs.map(([k, t, n]) => S('height:' + (m ? 44 : 36) + 'px;display:flex;align-items:center;gap:6px;border-bottom:2px solid ' + (tab === k ? 'var(--ui-primary)' : 'transparent') + ';margin-bottom:-1px;font-weight:' + (tab === k ? 500 : 400) + ';color:' + (tab === k ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)') + ';font-size:' + (m ? 15 : 13) + 'px', t, k === 'dups' ? (dg > 0 && S('height:18px;min-width:18px;padding:0 5px;display:inline-flex;align-items:center;justify-content:center;background:var(--rc-info-soft);color:var(--ui-info);font:500 11px \'IBM Plex Mono\',monospace', n)) : S('font:500 11px \'IBM Plex Mono\',monospace;color:var(--ui-text-muted)', n))))
|
||
const Search = () => D('height:' + (m ? 44 : 32) + 'px;' + (m ? '' : 'width:240px;margin-bottom:0;') + 'padding:0 4px 0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:' + (s.q ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)') + (m ? ';font-size:16px' : ''), I('search', 14), S('flex:1', s.q || 'Search by filename'), s.q && S('width:' + (m ? 36 : 24) + 'px;height:' + (m ? 36 : 24) + 'px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x', 14)))
|
||
const Sort = () => D('height:' + (m ? 44 : 32) + '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);' + (m ? 'flex:1;' : 'width:128px;'), S('flex:1', s.sort || 'Newest'), I('chevron-down', 14, 'color:var(--ui-text-muted)'))
|
||
const view = s.view || 'grid'
|
||
const Toggle = () => D('display:flex;border:1px solid var(--ui-border-accented);flex-shrink:0', ...[['grid', 'layout-grid'], ['list', 'list']].map(([k, ic]) => S('width:' + (m ? 44 : 32) + 'px;height:' + (m ? 42 : 30) + 'px;display:flex;align-items:center;justify-content:center;background:' + (view === k ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (view === k ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)'), I(ic))))
|
||
const SelectAll = () => D('display:flex;align-items:center;gap:10px;color:var(--ui-text-muted)', m ? S('width:44px;height:44px;margin-left:-11px;display:flex;align-items:center;justify-content:center', Cb(false, 22, sel.length > 0)) : Cb(false, 18, sel.length > 0), S('color:var(--ui-text-highlighted);font-weight:500', 'Select all'), S('', imgs.length + ' images'))
|
||
|
||
const UP = s.upload
|
||
const upRow = r => {
|
||
const sc = { err: 'var(--ui-error)', dup: 'var(--ui-info)' }[r.st] || 'var(--ui-text-muted)'
|
||
const body = {
|
||
prog: [S('', 'Uploading ' + r.p), D('width:100%;height:4px;background:var(--ui-bg-accented)', D('width:' + r.pct + ';height:4px;background:var(--ui-primary)'))],
|
||
wait: [S('color:var(--ui-text-muted)', 'Waiting')],
|
||
ok: [S('display:flex;align-items:center;gap:6px;color:var(--ui-success)', I('circle-check', 14), 'Added to your library')],
|
||
dup: [S('font-weight:600;color:var(--ui-text-highlighted)', 'Already in your library'), S('color:var(--ui-text-toned);font-size:12px', r.msg), D('display:flex;gap:8px;flex-wrap:wrap', B('Use existing', { h: m ? 44 : 28, fs: 12, px: 10, full: m }), B('Upload anyway', { h: m ? 44 : 28, fs: 12, px: 10, full: m }))],
|
||
rej: [S('display:flex;gap:6px;align-items:center;font-weight:600;color:var(--ui-error)', I('file-x', 14), r.title), S('color:var(--ui-text-toned);font-size:12px', r.msg)],
|
||
fail: [S('display:flex;gap:6px;align-items:center;font-weight:600;color:var(--ui-error)', I('circle-x', 14), 'Upload failed'), S('color:var(--ui-text-toned);font-size:12px', 'The server didn\'t accept the file. Nothing was added to your library.'), D('display:flex', B('Try again', { icon: 'rotate-cw', z: 14, h: m ? 44 : 28, fs: 12, px: 10, full: m }))]
|
||
}[r.st]
|
||
return D('display:flex;gap:12px;align-items:flex-start;padding:12px 14px;border-top:1px solid var(--ui-border-muted)', Th(byId(r.im || 8), 'width:40px;height:40px;flex-shrink:0;border:1px solid var(--ui-border)', 16), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:6px', D('display:flex;justify-content:space-between;gap:8px', S('font-weight:500;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', r.n), S('color:var(--ui-text-muted);white-space:nowrap', r.sz)), ...body), r.st === 'prog' && S('width:24px;height:24px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x', 14)))
|
||
}
|
||
const UpPanel = () => D('background:var(--ui-bg);border:1px solid var(--ui-border);display:flex;flex-direction:column', D('display:flex;align-items:center;gap:12px;padding:12px 14px', D('flex:1;display:flex;flex-direction:column;gap:2px', S('font-weight:600;color:var(--ui-text-highlighted)', UP.title), S('color:var(--ui-text-muted)', UP.sub)), UP.done ? B('Dismiss', { h: m ? 44 : 28, fs: 12 }) : B('Cancel all', { h: m ? 44 : 28, fs: 12 })), ...UP.rows.map(upRow), UP.summary && D('padding:12px 14px;border-top:1px solid var(--ui-border);background:var(--ui-bg-elevated);color:var(--ui-text-toned);font-weight:500', UP.summary))
|
||
|
||
const Groups = () => {
|
||
const G = [
|
||
{ n: 'floorboards.jpg', sub: '3 identical images · 2.4 MB each · 3600 × 2400', c: [12, 5, 6], keep: 12 },
|
||
{ n: 'IMG_4471.jpg', sub: '2 identical images · 3.1 MB each · 3600 × 2400', c: [7, 9], keep: 7 }
|
||
]
|
||
return D('display:flex;flex-direction:column;gap:16px', ...G.slice(2 - dg).map((g, gi) => D('background:var(--ui-bg);border:1px solid var(--ui-border)', D('padding:12px 14px;border-bottom:1px solid var(--ui-border);display:flex;flex-direction:column;gap:2px', S('font-weight:600;color:var(--ui-text-highlighted)', 'Group ' + (gi + 1)), S('color:var(--ui-text-muted)', g.sub)),
|
||
D('display:grid;grid-template-columns:' + (m ? 'minmax(0,1fr)' : 'repeat(' + g.c.length + ',minmax(0,1fr))') + ';gap:' + (m ? 0 : 16) + 'px;padding:' + (m ? 0 : 16) + 'px', ...g.c.map(id => {
|
||
const im = byId(id), k = id === g.keep
|
||
const others = g.c.filter(x => x !== id).length
|
||
return D('display:flex;' + (m ? 'flex-direction:column;gap:12px;padding:14px;border-bottom:1px solid var(--ui-border-muted)' : 'flex-direction:column;gap:12px;padding:12px;border:1px solid ' + (k ? 'var(--ui-primary)' : 'var(--ui-border)')) + ';background:' + (k ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (k ? 'var(--rc-on-sand)' : 'var(--ui-text)'),
|
||
D('display:flex;gap:12px;' + (m ? 'align-items:flex-start' : 'flex-direction:column'), Th(im, (m ? 'width:96px;height:72px;flex-shrink:0' : 'aspect-ratio:4/3') + ';border:1px solid var(--ui-border)'),
|
||
D('display:flex;flex-direction:column;gap:4px;min-width:0', D('display:flex;gap:6px;align-items:center;flex-wrap:wrap', S('font-weight:500;color:' + (k ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), k && S('height:20px;padding:0 6px;display:inline-flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-size:12px;font-weight:500', 'Suggested')),
|
||
S('font-size:12px;color:' + (k ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)'), 'Uploaded ' + im.d + ' by ' + im.by),
|
||
S('font-weight:500;text-decoration:underline;text-underline-offset:3px', 'Used in ' + pl(im.used)))),
|
||
B('Merge into this one', { k: k ? 'primary' : 'outline', full: false, icon: 'merge', h: bh }))
|
||
})),
|
||
D('padding:10px 14px;border-top:1px solid var(--ui-border);color:var(--ui-text-muted);font-size:12px', 'Suggested: the most used copy, then the oldest. Choosing another copy switches articles to it.'))))
|
||
}
|
||
|
||
const Header = () => D('display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap', D('flex:1;min-width:200px;display:flex;flex-direction:column;gap:2px', S('font:600 20px/28px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Media'), S('color:var(--ui-text-muted)', 'Every image you\'ve uploaded, ready to use in any article.')), !m && (s.limited ? B('Try again in 0:42', { k: 'primary', icon: 'timer', dis: true }) : B('Upload', { k: 'primary', icon: 'upload', dis: s.offline })))
|
||
|
||
const Pager = () => {
|
||
const g = { page: 1, per: 25, total: imgs.length, ...(s.pg || {}) }
|
||
g.pages = g.pages || Math.max(1, Math.ceil(g.total / g.per))
|
||
const busy = !!g.loading, ring = k => g.focus === k ? 'outline:2px solid var(--ui-primary);outline-offset:2px;' : ''
|
||
const fmt = n => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
|
||
const from = g.total ? (g.page - 1) * g.per + 1 : 0, to = Math.min(g.page * g.per, g.total)
|
||
const back = busy || g.page === 1 ? 0.4 : 1, fwd = busy || g.page === g.pages ? 0.4 : 1
|
||
const PB = (ic, op, rg, label) => S('width:' + bh + 'px;height:' + bh + 'px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);opacity:' + op + ';' + (rg || ''), I(ic))
|
||
const set = new Set([1, g.pages, g.page - 1, g.page, g.page + 1].filter(n => n >= 1 && n <= g.pages))
|
||
const arr = g.pages <= 7 ? Array.from({ length: g.pages }, (_, i) => i + 1) : [...set].sort((a, b) => a - b)
|
||
const nums = []
|
||
arr.forEach((n, i) => {
|
||
if (i && n - arr[i - 1] > 1) nums.push(S('min-width:' + bh + 'px;height:' + bh + 'px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', '…'))
|
||
const cur = n === g.page
|
||
nums.push(S('min-width:' + bh + 'px;height:' + bh + 'px;padding:0 6px;display:flex;align-items:center;justify-content:center;border:1px solid ' + (cur ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (cur ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (cur ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';font-weight:' + (cur ? 600 : 400), String(n)))
|
||
})
|
||
const Size = () => D('position:relative', S('height:' + bh + 'px;padding:0 8px 0 12px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);' + ring('size'), g.per + ' per page', I('chevron-down', 14)),
|
||
g.sizeOpen && D('position:absolute;' + (m ? 'right' : 'left') + ':0;bottom:calc(100% + 4px);z-index:40;width:160px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px', ...[5, 25, 50, 100].map(n => D('height:' + bh + 'px;padding:0 8px;display:flex;align-items:center;background:' + (n === g.per ? 'var(--rc-sand)' : 'transparent') + ';color:' + (n === g.per ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';font-weight:' + (n === g.per ? 600 : 400), n + ' per page'))))
|
||
const sum = S('color:var(--ui-text-muted)', busy ? 'Loading page ' + g.loading + '…' : 'Showing ' + from + '–' + to + ' of ' + fmt(g.total))
|
||
const out = []
|
||
if (g.err) out.push(Alert('error', 'circle-x', g.err, 'Check your connection and try again.', 'Retry'))
|
||
if (g.note) out.push(Alert('info', 'info', g.note))
|
||
if (m) {
|
||
out.push(D('display:flex;flex-direction:column;gap:12px;padding:4px 0 16px', D('display:flex;align-items:center;gap:12px;flex-wrap:wrap;justify-content:space-between', sum, Size()),
|
||
g.pages > 1 && D('display:flex;align-items:center;gap:8px', S('flex:1;height:44px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;opacity:' + back + ';' + ring('prev'), I('chevron-left'), 'Previous'), S('padding:0 4px;color:var(--ui-text-highlighted);font-weight:500;white-space:nowrap', 'Page ' + g.page + ' of ' + g.pages), S('flex:1;height:44px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;opacity:' + fwd + ';' + ring('next'), 'Next', I('chevron-right')))))
|
||
} else {
|
||
out.push(D('display:flex;align-items:center;gap:16px', D('flex:1;display:flex', sum), Size(), g.pages > 1 && D('display:flex;align-items:center;gap:4px;justify-content:flex-end', PB('chevrons-left', back), PB('chevron-left', back, ring('prev')), ...nums, PB('chevron-right', fwd, ring('next')), PB('chevrons-right', fwd))))
|
||
}
|
||
return out
|
||
}
|
||
const pageBody = () => {
|
||
const parts = []
|
||
if (s.empty === 'first') { parts.push(Empty('images', 'No images yet', 'Upload your first image. You can use it in any article.', 'Upload an image', true), S('align-self:center;color:var(--ui-text-muted);font-size:12px', 'PNG, JPEG, WEBP, SVG · max 10 MB each')); return parts }
|
||
if (s.limited) parts.push(Alert('info', 'timer', 'Too many uploads', 'For your security, wait a moment before uploading again.'))
|
||
if (s.error) parts.push(Alert('error', 'circle-x', 'Couldn\'t load your images', 'Check your connection and try again.', 'Try again'))
|
||
const pb = tab === 'dups' && !s.member && !s.dupEmpty && B('Purge duplicates', { icon: 'merge', dis: s.offline, full: m })
|
||
const tb = m
|
||
? D('display:flex;flex-direction:column;gap:12px', Search(), TabRow(), D('display:flex;gap:8px', Sort(), Toggle()), pb && D('display:flex', pb))
|
||
: D('display:flex;align-items:flex-end;gap:12px;border-bottom:1px solid var(--ui-border)', TabRow(), S('display:flex;padding-bottom:6px;gap:8px', Search(), Sort(), pb, Toggle()))
|
||
parts.push(tb)
|
||
if (UP) parts.push(UpPanel())
|
||
if (s.loading) { parts.push(Skel()); if (s.pg) parts.push(...Pager()) }
|
||
else if (s.error) { }
|
||
else if (s.empty === 'search') parts.push(Empty('search', 'No images match "wall"', 'Nothing matches in Unused. Check the spelling, or clear the filters.', 'Clear filters', false))
|
||
else if (tab === 'dups') parts.push(s.dupEmpty ? Empty('copy', 'No duplicates', 'Every image in your library is different.', 'Back to all images', false) : Groups())
|
||
else {
|
||
if (view === 'grid') parts.push(SelectAll(), Grid()); else parts.push(m ? Cards() : Table())
|
||
parts.push(...Pager())
|
||
}
|
||
return parts
|
||
}
|
||
const BulkBar = () => D('position:absolute;z-index:30;bottom:' + (m ? 16 : 24) + 'px;' + (m ? 'left:16px;right:16px' : 'left:calc(224px + (100% - 224px) / 2);transform:translateX(-50%);width:400px') + ';display:flex;align-items:center;gap:8px;padding:8px 8px 8px 16px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);box-shadow:var(--rc-shadow-pop)', S('flex:1;font-weight:500', sel.length + ' selected'), B('Delete', { k: 'danger', icon: 'trash-2' }), B('Clear', { k: 'inv' }))
|
||
|
||
// ---------- detail panel
|
||
const Panel = () => {
|
||
const im = byId(s.panel.id), list = usedList(im.used)
|
||
const meta = [['Type', im.ty], ['Size', im.sz], ['Dimensions', im.dim], ['Uploaded', im.d], ['Uploaded by', im.by]]
|
||
const w = m ? '100%' : '460px'
|
||
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:' + w + ';height:100%;background:var(--ui-bg);display:flex;flex-direction:column;' + (m ? '' : 'box-shadow:var(--rc-shadow-modal)'),
|
||
D('height:' + (m ? 56 : 56) + 'px;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'))),
|
||
D('flex:1;min-height:0;overflow:hidden;padding:' + (m ? 16 : 20) + 'px;display:flex;flex-direction:column;gap:20px',
|
||
Th(im, 'aspect-ratio:' + (m ? '4/3' : '16/10') + ';border:1px solid var(--ui-border);flex-shrink:0', 40),
|
||
D('display:flex;gap:8px;' + (m ? 'flex-wrap:wrap' : ''), B('Copy URL', { icon: 'link', full: m }), B('Rename', { icon: 'pencil', full: m }), B('Delete', { k: 'del', icon: 'trash-2', full: m })),
|
||
im.dup && Alert('info', 'copy', 'Identical to 2 other images', 'floorboards-copy.jpg and IMG_2208.jpg have the same content.', 'Review duplicates', { icon: 'arrow-right' }),
|
||
D('display:grid;grid-template-columns:110px minmax(0,1fr);gap:8px 12px;color:var(--ui-text-toned)', ...meta.flatMap(([k, v]) => [S('color:var(--ui-text-muted)', k), S('color:var(--ui-text-highlighted)', v)]), S('color:var(--ui-text-muted)', 'URL'), S('font:400 12px/20px \'IBM Plex Mono\',monospace;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', 'cms.renew.example/media/' + im.n)),
|
||
D('display:flex;flex-direction:column;gap:8px', S('font:600 14px/22px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', im.used ? 'Used in ' + pl(im.used) : 'Not used in any article'),
|
||
im.used ? D('border:1px solid var(--ui-border)', ...list.map(([t, on], i) => D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 48 : 40) + 'px;padding:0 12px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;text-decoration:underline;text-underline-offset:3px;color:var(--ui-text-highlighted)', t), on ? Bd('Online', 'on', 22) : Bd('Draft', 'draft', 22), I('arrow-up-right', 14, 'color:var(--ui-text-muted)'))))
|
||
: S('color:var(--ui-text-muted)', 'This image is safe to delete. No article shows it.'),
|
||
im.used ? S('color:var(--ui-text-muted);font-size:12px', 'Counted from the text of your articles each time you open this page.') : null))))
|
||
}
|
||
|
||
// ---------- modal
|
||
const Field = (lab, val, o = {}) => D('display:flex;flex-direction:column;gap:6px', D('display:flex;justify-content:space-between', S('font-weight:500;color:var(--ui-text-highlighted)', lab), o.count && S('font:400 12px \'IBM Plex Mono\',monospace;color:var(--ui-text-muted)', o.count)), D('display:flex;align-items:center;min-height:' + (o.area ? 72 : m ? 44 : 40) + 'px;border:1px solid ' + (o.err ? 'var(--ui-error)' : o.focus ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';' + (o.focus ? 'box-shadow:0 0 0 1px var(--ui-primary);' : '') + 'background:var(--ui-bg);font-size:' + (m ? 16 : 13) + 'px;' + (o.area ? 'align-items:flex-start;' : ''), S('flex:1;min-width:0;padding:' + (o.area ? '10px' : '0') + ' 12px;color:' + (o.val ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)') + (o.dis ? ';opacity:.6' : ''), val), o.suffix && S('padding:0 12px;color:var(--ui-text-muted);font:400 13px \'IBM Plex Mono\',monospace', o.suffix)), o.err && S('display:flex;gap:6px;align-items:center;color:var(--ui-error);font-size:12px', I('circle-alert', 14), o.err), o.help && S('color:var(--ui-text-muted);font-size:12px', o.help))
|
||
const ArtBox = (list, more) => D('border:1px solid var(--ui-border)', ...list.map(([t, on], i) => D('display:flex;align-items:center;gap:8px;min-height:36px;padding:0 12px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ui-text-highlighted)', t), on ? Bd('Online', 'on') : Bd('Draft', 'draft'))), more && D('padding:8px 12px;border-top:1px solid var(--ui-border-muted);color:var(--ui-text-muted)', more))
|
||
const modalCfg = () => {
|
||
const md = s.modal, k = md.k
|
||
if (k === 'rename') return { title: 'Rename image', desc: 'Only the filename changes. The URL stays the same, so articles that use this image keep working.', body: [Field('Filename', md.err ? '' : md.val || 'north-wall', { val: !md.err, suffix: '.jpg', focus: !md.loading, err: md.err, dis: md.loading, help: md.err ? '' : 'Letters, numbers, dashes and underscores. The .jpg ending stays.' })], cancel: 'Cancel', confirm: md.loading ? 'Renaming…' : 'Rename' }
|
||
if (k === 'del') return { title: 'Delete this image?', desc: '"window-frame.jpg" will be deleted from your library. No article uses it. This can\'t be undone.', cancel: 'Cancel', confirm: md.loading ? 'Deleting…' : 'Delete image', tone: 'danger' }
|
||
if (k === 'delUsed') return { title: 'Delete an image that\'s in use?', 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: [ArtBox(usedList(2))], cancel: 'Cancel', confirm: md.loading ? 'Deleting…' : 'Delete image', tone: 'danger' }
|
||
if (k === 'bulk') return { title: 'Delete 3 images?', desc: '2 of them are used in 5 articles, which will show a broken image. The other one isn\'t used anywhere. This can\'t be undone.', body: [D('display:flex;flex-direction:column;border:1px solid var(--ui-border)', ...[['north-wall.jpg', 'Used in 2'], ['kitchen-after.webp', 'Used in 3'], ['window-frame.jpg', 'Unused']].map(([n, u], i) => D('display:flex;align-items:center;gap:8px;min-height:36px;padding:0 12px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), S('flex:1;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', n), Bd(u === 'Unused' ? 'Unused' : u, u === 'Unused' ? 'unused' : 'used'))))], cancel: 'Cancel', confirm: md.loading ? 'Deleting…' : 'Delete 3 images', tone: 'danger' }
|
||
if (k === 'merge') return { title: 'Merge into floorboards.jpg?', desc: 'Articles that use the other 2 copies will be switched to floorboards.jpg, and the other 2 copies will be deleted. 5 articles will be updated. This can\'t be undone.', body: [D('display:flex;gap:12px;align-items:center;border:1px solid var(--ui-border);padding:10px 12px', Th(byId(12), 'width:56px;height:42px;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)', 'floorboards.jpg'), S('font-size:12px;color:var(--ui-text-muted)', 'Kept · used in 4 articles now')))], cancel: 'Cancel', confirm: md.loading ? 'Merging…' : 'Merge 2 copies', tone: 'danger' }
|
||
if (k === 'purge') return { title: 'Purge duplicates?', desc: 'Articles that use a duplicate will be switched to the most used copy of that image, and the other copies will be deleted. This can\'t be undone.', body: [S('font-weight:500;color:var(--ui-text-highlighted)', '4 groups · 9 copies will be deleted · 12 articles will be updated'), D('border:1px solid var(--ui-border)', ...[[12, 'floorboards.jpg', 4, '2 copies'], [7, 'IMG_4471.jpg', 2, '1 copy'], [2, 'kitchen-after.webp', 3, '3 copies'], [4, 'logo-renew.svg', 5, '3 copies']].map(([id, n, u, c], i) => D('display:flex;align-items:center;gap:12px;padding:8px 12px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), Th(byId(id), 'width:40px;height:30px;flex-shrink:0;border:1px solid var(--ui-border)', 16), D('flex:1;min-width:0;display:flex;flex-direction:column', S('font-weight:500;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', n), S('font-size:12px;color:var(--ui-text-muted)', 'Kept · used in ' + pl(u))), S('font-size:12px;color:var(--ui-text-toned);white-space:nowrap', c + ' merged')))), S('display:inline-flex;align-items:center;gap:4px;align-self:flex-start;font-weight:500;color:var(--ui-text-highlighted);text-decoration:underline;text-underline-offset:3px;min-height:' + (m ? 44 : 20) + 'px', 'Review in Duplicates', I('arrow-right', 14))], cancel: 'Cancel', confirm: 'Purge 4 groups', tone: 'danger' }
|
||
if (k === 'remove') return { title: 'Remove this image from the article?', desc: 'It\'s removed from this article only. The file stays in your library and in any other article that uses it.', body: [D('display:flex;gap:12px;align-items:center;border:1px solid var(--ui-border);padding:10px 12px', Th(byId(1), 'width:56px;height:42px;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)', 'Stays in the library · 3.9 MB')))], cancel: 'Cancel', confirm: 'Remove image' }
|
||
}
|
||
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 ? '0' : (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', m && D('display:flex;justify-content:center;padding-top:8px;flex-shrink:0', S('display:block;width:36px;height:4px;background:var(--ui-border-accented)')),
|
||
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:' + (m ? 44 : 28) + 'px;height:' + (m ? 44 : 28) + 'px;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 Modal = () => {
|
||
const md = s.modal, c = modalCfg()
|
||
return Shell(c, D('padding:20px 24px 0;display:flex;flex-direction:column;gap:16px;min-height:0;overflow:hidden', md.notice && D('display:flex;gap:8px;padding:10px 12px;background:var(--rc-error-soft);color:var(--ui-error)', S('display:flex;padding-top:2px', I('circle-x')), S('color:var(--ui-text-toned)', md.notice)), ...(c.body || [])))
|
||
}
|
||
const Toasts = () => D('position:absolute;z-index:60;display:flex;flex-direction:column;gap:8px;' + (m ? 'left:16px;right:16px;bottom:' + (s.sel && s.sel.length ? 80 : 16) + 'px' : 'right:24px;bottom:24px;width:356px'), ...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]), 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'))))
|
||
|
||
// ---------- picker (editor + dialog)
|
||
const Editor = () => 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 ' + (m ? 16 : 24) + '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.'),
|
||
s.inArticle && D('position:relative;aspect-ratio:16/9;border:1px solid var(--ui-border);overflow:hidden', Th(byId(1), 'position:absolute;inset:0', 40), S('position:absolute;top:8px;right:8px;height:28px;padding:0 10px;display:flex;align-items:center;gap:6px;background:var(--ui-bg);border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font:500 12px \'IBM Plex Sans\',sans-serif', I('trash-2', 14), 'Remove image')),
|
||
S('display:block', 'We don\'t add a margin on materials you choose yourself.'))))
|
||
const PT = s.pk || {}
|
||
const pImgs = (PT.ids || [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]).map(byId)
|
||
const inArt = [1, 4]
|
||
const cols = m ? 3 : 4
|
||
const PTile = im => {
|
||
const on = PT.sel === im.id
|
||
return D('display:flex;flex-direction:column;gap:6px;min-width:0', D('position:relative;' + (on ? 'outline:2px solid var(--ui-primary);outline-offset:2px;' : ''), Th(im, 'aspect-ratio:1/1;border:1px solid var(--ui-border)', 20),
|
||
inArt.includes(im.id) && S('position:absolute;left:4px;bottom:4px;height:18px;padding:0 5px;display:flex;align-items:center;background:var(--rc-sand);color:var(--rc-on-sand);font-size:11px;font-weight:500', 'In this article'),
|
||
on && 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', im.n))
|
||
}
|
||
const PGrid = () => D('display:grid;grid-template-columns:repeat(' + cols + ',minmax(0,1fr));gap:12px ' + (m ? 8 : 12) + 'px', ...pImgs.map(PTile), ...(PT.more ? Array.from({ length: cols }, () => D('display:flex;flex-direction:column;gap:6px', S('display:block;aspect-ratio:1/1;background:var(--ui-bg-accented)'), S('display:block;width:70%;height:10px;background:var(--ui-bg-accented)'))) : []))
|
||
const PSkel = () => D('display:grid;grid-template-columns:repeat(' + cols + ',minmax(0,1fr));gap:12px ' + (m ? 8 : 12) + 'px', ...Array.from({ length: m ? 9 : 8 }, () => D('display:flex;flex-direction:column;gap:6px', S('display:block;aspect-ratio:1/1;background:var(--ui-bg-accented)'), S('display:block;width:70%;height:10px;background:var(--ui-bg-accented)'))))
|
||
const PTabs = () => 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 ' + (PT.tab === t ? 'var(--ui-primary)' : 'transparent') + ';margin-bottom:-1px;font-weight:' + (PT.tab === t ? 500 : 400) + ';color:' + (PT.tab === t ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)'), I(t === 'Upload' ? 'upload' : 'images', 14), t)))
|
||
const drop = (icon, title, text, bd, ic) => D('height:180px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:16px;text-align:center;border:' + bd + ';background:var(--ui-bg)', I(icon, 24, 'color:' + (ic || 'var(--ui-text-toned)')), S('font-weight:500;color:var(--ui-text-highlighted)', title), S('color:var(--ui-text-toned);font-size:12px;max-width:300px', text))
|
||
const PBody = () => {
|
||
if (PT.tab === 'Upload') {
|
||
if (PT.dup) return D('display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--ui-info);background:var(--ui-bg)', D('display:flex;gap:12px;align-items:center', Th(byId(1), 'width:72px;height:54px;flex-shrink:0;border:1px solid var(--ui-border)', 16), D('display:flex;flex-direction:column;gap:2px;min-width:0', S('font-weight:600;color:var(--ui-text-highlighted)', 'Already in your library'), S('color:var(--ui-text-toned);font-size:12px', 'north-wall.jpg matches an image uploaded on 12 Sep 2026 by Marta Alves.'))), D('display:flex;gap:8px;' + (m ? 'flex-direction:column' : ''), B('Use existing', { k: 'primary', full: m }), B('Upload anyway', { full: m })))
|
||
return drop('image-plus', 'Drop an image here, or choose a file', 'PNG, JPEG, WEBP, SVG · max 10 MB', '1px dashed var(--ui-border-accented)')
|
||
}
|
||
const kids = []
|
||
kids.push(D('height:' + (m ? 44 : 32) + 'px;padding:0 4px 0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:' + (PT.q ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)') + (m ? ';font-size:16px' : ''), I('search', 14), S('flex:1', PT.q || 'Search by filename'), PT.q && S('width:24px;height:24px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x', 14))))
|
||
if (PT.error) kids.push(Alert('error', 'circle-x', 'Couldn\'t load your library', 'Check your connection and try again.', 'Try again'))
|
||
else if (PT.empty === 'first') kids.push(Empty('images', 'Your library is empty', 'Images you upload appear here, ready to use in any article.', 'Upload an image', true, 32))
|
||
else if (PT.empty === 'search') kids.push(Empty('search', 'No images match "wall"', 'Check the spelling, or search for a shorter part of the filename.', 'Clear search', false, 32))
|
||
else if (PT.loading) kids.push(PSkel())
|
||
else {
|
||
kids.push(S('color:var(--ui-text-muted);font-size:12px', 'Newest first · 48 images'), D('min-height:0;overflow:hidden;' + (PT.sel ? 'max-height:' + (m ? 120 : 190) + 'px' : 'max-height:' + (m ? 300 : 300) + 'px'), PGrid()))
|
||
if (!PT.sel) kids.push(D('display:flex;justify-content:center', B(PT.more ? 'Loading…' : 'Load more', { icon: PT.more ? 'loader-circle' : null, dis: !!PT.more })))
|
||
}
|
||
if (PT.sel) { const im = byId(PT.sel); kids.push(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(im, 'width:56px;height:56px;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)', im.n), S('font-size:12px;color:var(--ui-text-muted)', im.dim + ' · ' + im.sz))), Field('Alt text (optional)', PT.alt || 'e.g. North wall of the kitchen before plastering', { val: !!PT.alt, focus: !!PT.alt, help: 'Describes the image for people who use a screen reader, and shows if it doesn\'t load. Leave it empty if the image is only decoration.' }))) }
|
||
return D('display:flex;flex-direction:column;gap:12px', ...kids)
|
||
}
|
||
const Picker = () => {
|
||
const ins = PT.tab === 'Library'
|
||
return Shell({ title: 'Add an image', close: true, w: 640, top: 40, cancel: 'Cancel', confirm: ins ? 'Insert image' : null, cdis: ins && !PT.sel }, D('display:flex;flex-direction:column;min-height:0;flex:1', PTabs(), D('padding:16px 24px 0;min-height:0;overflow:hidden;display:flex;flex-direction:column', PBody())))
|
||
}
|
||
|
||
// ---------- shell
|
||
const isPicker = s.kind === 'picker'
|
||
const navKey = isPicker ? 'articles' : 'media'
|
||
const crumbs = isPicker ? ['Articles', 'How we price a bathroom renovation'] : ['Media']
|
||
const offline = s.offline && D('flex-shrink:0;display:flex;align-items:center;gap:10px;padding:10px 16px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted)', I('wifi-off'), S('flex:1', 'You\'re offline. You can look through the images that are loaded, but uploading, renaming and deleting need a connection.'))
|
||
const pad = m ? 16 : 24
|
||
const content = isPicker ? Editor() : D('flex:1;min-height:0;overflow:hidden;padding:' + pad + 'px;display:flex;flex-direction:column;gap:16px;position:relative', Header(), ...pageBody())
|
||
const dragOv = s.drag && D('position:absolute;z-index:35;inset:' + (m ? 8 : 12) + 'px;top:' + (m ? 64 : 60) + 'px;border:2px dashed var(--ui-primary);background:var(--rc-sand);color:var(--rc-on-sand);display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;text-align:center', I('upload', 28), S('font-weight:600;font-size:16px', 'Drop images to upload'), S('color:var(--rc-on-sand)', 'PNG, JPEG, WEBP, SVG · max 10 MB each'))
|
||
const fab = m && !isPicker && !(sel.length) && !s.panel && !s.modal && !s.error && s.empty !== 'first' && !s.drag && D('position:absolute;left:16px;right:16px;bottom:16px;height:48px;display:flex;align-items:center;justify-content:center;gap:8px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;font-size:15px;box-shadow:var(--rc-shadow-pop);opacity:' + (s.limited || s.offline ? 0.4 : 1), I(s.limited ? 'timer' : 'upload', 18), s.limited ? 'Try again in 0:42' : 'Upload')
|
||
const ann = s.ann && D('position:absolute;z-index:20;' + (m ? 'left:16px;bottom:24px' : 'left:248px;bottom:20px'), Note(s.ann))
|
||
const main = D('flex:1;min-width:0;min-height:0;display:flex;flex-direction:column', offline, content)
|
||
const ov = D('display:contents', dragOv, fab, ann, sel.length > 0 && !s.panel && !s.modal && BulkBar(), s.panel && Panel(), isPicker && s.picker && Picker(), s.modal && Modal(), s.toasts && Toasts())
|
||
const shell = { kind: 'slot', display: 'Marta Alves', nav: navKey, crumbs, mTitle: isPicker ? 'Edit article' : 'Media', mLead: isPicker ? 'arrow-left' : undefined, mAction: isPicker ? 'ellipsis' : undefined }
|
||
return { shell, mob: m, main, ov }
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|