chore: articles localization groundwork

This commit is contained in:
Florian Sylvain committed 2026-10-31 21:22:44 +01:00
1 parent 0bdcca8a90
commit 99a74dc3a2
23 files changed
+3524

No files matched your search

+343
View File
@@ -0,0 +1,343 @@
<!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="LocalizationShell" s="{{ shell }}" mobile="{{ mob }}" slot-body="{{ main }}" slot-overlays="{{ ov }}" src-pane="{{ srcPane }}" ed-banner="{{ edBanner }}" hint-size="100%,100%"></dc-import>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1440,&quot;height&quot;:900},&quot;s&quot;:{&quot;editor&quot;:null,&quot;tsType&quot;:&quot;object&quot;},&quot;mobile&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:false,&quot;tsType&quot;:&quot;boolean&quot;}}">
class Component extends DCLogic {
renderVals() {
const s = this.props.s || {}, m = !!this.props.mobile, v = s.v, x = s.x || ''
const P = t => Object.fromEntries(t.split(';').filter(q => q.trim()).map(q => { const i = q.indexOf(':'); let k = q.slice(0, i).trim(); const val = q.slice(i + 1).trim(); if (!k.startsWith('--')) { k = k.replace(/^-webkit-/, 'Webkit-').replace(/-([a-z])/g, (_, c) => c.toUpperCase()) } return [k, val] }))
const h = React.createElement
const D = (t, ...c) => h('div', { style: P(t) }, ...c)
const S = (t, ...c) => h('span', { style: P(t) }, ...c)
const A = (tag, pr, t, ...c) => h(tag, { ...pr, style: P(t) }, ...c)
const Ic = (n, z = 16) => S('display:inline-block;flex-shrink:0;width:' + z + 'px;height:' + z + 'px;background-color:currentColor;-webkit-mask: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')
const HL = 'color:var(--ui-text-highlighted)', MU = 'color:var(--ui-text-muted)', TO = 'color:var(--ui-text-toned)'
const mono = "font-family:'IBM Plex Mono',monospace;font-size:12px"
const Btn = (l, k = 'secondary', o = {}) => {
const hh = m ? 44 : (o.sm ? 28 : 32)
const st = { primary: 'background:var(--ui-primary);color:var(--ui-bg)', secondary: 'background:var(--ui-bg);color:var(--ui-text-highlighted);border:1px solid var(--ui-border-accented)', ghost: 'color:var(--ui-text-toned)', danger: 'background:var(--ui-error);color:var(--ui-bg)' }[k]
return A('span', { role: 'button', 'aria-label': o.al || (o.ic && !l ? o.ic : undefined), 'aria-disabled': !!o.dis || undefined, 'aria-busy': o.busy || undefined }, 'display:inline-flex;align-items:center;justify-content:center;gap:6px;height:' + hh + 'px;padding:0 ' + (o.ic && !l ? 0 : 12) + 'px;' + (o.ic && !l ? 'width:' + hh + 'px;' : '') + 'font-weight:500;white-space:nowrap;flex-shrink:0;' + st + ';' + (o.dis ? 'opacity:.4;' : '') + (o.w ? 'width:' + o.w + ';' : ''), o.busy && Ic('loader-circle', 14), o.ic && Ic(o.ic, 16), l)
}
const TN = { neutral: 'background:var(--ui-bg-elevated);color:var(--ui-text-toned)', success: 'background:var(--rc-success-soft);color:var(--ui-success)', info: 'background:var(--rc-info-soft);color:var(--ui-info)', warning: 'background:var(--rc-warning-soft);color:var(--ui-warning)', error: 'background:var(--rc-error-soft);color:var(--ui-error)' }
const Bd = (l, tone = 'neutral', ic) => S('display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 6px;font-size:12px;font-weight:500;white-space:nowrap;flex-shrink:0;' + TN[tone], ic && Ic(ic, 12), l)
// freshness uses an outline so it never reads as a publication state
const Fr = (l, tone = 'warning', ic = 'refresh-cw') => S('display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 6px;font-size:12px;font-weight:500;white-space:nowrap;flex-shrink:0;border:1px solid currentColor;background:var(--ui-bg);color:var(--ui-' + tone + ')', Ic(ic, 12), l)
const Pub = st => st === 'pub' ? Bd('Published', 'success', 'circle-check') : Bd('Draft', 'neutral', 'pencil')
const Alert = (tone, ic, title, text, act) => D('display:flex;gap:10px;padding:12px 14px;align-items:flex-start;' + TN[tone].split(';')[0] + ';color:var(--ui-text-highlighted)', S('color:var(--ui-' + (tone === 'neutral' ? 'text-toned' : tone) + ')', Ic(ic)), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S('font-weight:600', title), text && S(TO + ';text-wrap:pretty', text), act && D('margin-top:8px;display:flex;gap:8px;flex-wrap:wrap', ...[].concat(act))))
const Fd = (label, val, o = {}) => D('display:flex;flex-direction:column;gap:6px;min-width:0', S('font-weight:500;' + HL, label, o.opt && S(MU + ';font-weight:400', ' (optional)')),
D('display:flex;align-items:' + (o.area ? 'flex-start' : 'center') + ';gap:8px;min-height:' + (m ? 44 : 32) + 'px;padding:' + (o.area ? 8 : 0) + 'px 10px;border:1px solid ' + (o.err ? 'var(--ui-error)' : o.focus ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (o.ro ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';' + (o.focus ? 'outline:2px solid var(--ui-primary);outline-offset:1px;' : ''), o.pre && S(MU + ';' + mono + ';white-space:nowrap', o.pre), A('span', { dir: o.dir, lang: o.lang }, 'flex:1;min-width:0;' + (o.mono ? mono + ';' : '') + (o.val === false ? MU : HL) + ';overflow:hidden;text-overflow:ellipsis;' + (o.area ? 'white-space:normal' : 'white-space:nowrap'), val), o.sel && Ic('chevrons-up-down', 14), o.lock && Ic('lock', 14)),
o.err ? S('display:flex;gap:6px;color:var(--ui-error);font-size:12px;line-height:18px', Ic('circle-x', 14), o.err) : o.help && S(MU + ';font-size:12px;line-height:18px;text-wrap:pretty', o.help))
const Radio = (l, d, on) => D('display:flex;gap:10px;padding:10px 12px;border:1px solid ' + (on ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (on ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)'), S('width:16px;height:16px;margin-top:2px;flex-shrink:0;border:1px solid var(--ui-border-accented);display:flex;align-items:center;justify-content:center', on && S('width:8px;height:8px;background:var(--ui-primary)')), D('display:flex;flex-direction:column;gap:2px', S('font-weight:500;' + HL, l), d && S(MU + ';font-size:12px;line-height:18px;text-wrap:pretty', d)))
const Dlg = (o, body, foot) => D('position:absolute;inset:0;z-index:' + (o.z || 50) + ';background:var(--rc-overlay);display:flex;align-items:' + (m ? 'flex-end' : 'flex-start') + ';justify-content:center;padding-top:' + (m ? 0 : o.top ?? 100) + 'px',
A('div', { role: 'dialog', 'aria-modal': 'true', 'aria-label': o.title }, 'width:' + (m ? '100%' : (o.w || 460) + 'px') + ';max-height:' + (m ? '92%' : 'calc(100% - ' + ((o.top ?? 100) + 24) + 'px)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column;' + (m ? 'border-top:1px solid var(--ui-border)' : ''),
m && D('display:flex;justify-content:center;padding:8px 0 0', S('width:36px;height:4px;background:var(--ui-border-accented)')),
D('padding:20px 24px 0;display:flex;gap:12px;align-items:flex-start', D('flex:1;display:flex;flex-direction:column;gap:4px;min-width:0', S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, o.title), o.desc && S(TO + ';text-wrap:pretty', o.desc)), Btn('', 'ghost', { ic: 'x', sm: true, al: 'Close' })),
D('padding:16px 24px 0;display:flex;flex-direction:column;gap:14px;overflow-y:auto;min-height:0', ...[].concat(body)),
D('padding:20px 24px;flex-shrink:0;flex-wrap:wrap;display:flex;gap:8px;' + (m ? 'flex-direction:column-reverse' : 'justify-content:flex-end'), ...[].concat(foot))))
const Toast = (tone, ic, t, d) => D('position:absolute;z-index:80;' + (m ? 'left:16px;right:16px;bottom:16px' : 'right:24px;bottom:24px;width:356px') + ';background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:12px;display:flex;gap:10px', S('width:28px;height:28px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:var(--ui-' + tone + ');' + TN[tone].split(';')[0], Ic(ic)), D('display:flex;flex-direction:column;gap:2px', S('font-weight:600;' + HL, t), d && S(TO, d)))
const Sk = n => D('display:flex;flex-direction:column;gap:10px', ...Array.from({ length: n }, (_, i) => D('height:' + (m ? 56 : 44) + 'px;background:var(--ui-bg-elevated);opacity:' + (1 - i * .12))))
const Page = (title, sub, act, ...c) => D('flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:16px;padding:' + (m ? '16px 16px 88px' : '24px') + ';position:relative', D('display:flex;gap:12px;align-items:flex-start;' + (m ? 'flex-direction:column' : ''), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S('font:600 20px/28px \'IBM Plex Sans\',sans-serif;' + HL, title), sub && S(TO + ';text-wrap:pretty', sub)), act), ...c)
const Card = (...c) => D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:16px;display:flex;flex-direction:column;gap:12px', ...c)
const ROW = 'display:flex;align-items:center;gap:12px;border-top:1px solid var(--ui-border);padding:10px 16px;min-height:' + (m ? 56 : 48) + 'px'
const Av = i => 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', i)
let main = null, shell = { kind: 'slot', display: 'Marta Alves', role: s.role }, ov = []
// ---- data
const FX = window.LOCFX, S1 = FX.STORIES[0], S5 = FX.STORIES[4]
const LV = (k, n, c, st) => ({ n, c, lang: k, dir: FX.DIR[k] || 'ltr', t: k === 'en' ? (st || S1).title : (st || S1).tr[k], p: FX.P[k] })
const EN = LV('en', 'English', 'EN'), FR = LV('fr', 'French', 'FR'), AR = LV('ar', 'Arabic', 'AR'), DE = LV('de', 'German', 'DE')
const CAPS = { full: { e: 1, p: 1, d: 1, c: 1, rv: 1 }, ro: {}, p2: { e: 1, d: 1, c: 1, rv: 1 }, p3: { p: 1 }, p6: { e: 1, p: 1, d: 1, c: 1, rv: 1, part: 1 } }
// ---- language menu (menu button + menu; rows are single menu items, no nested controls)
const LangBtn = (cur, o = {}) => A('span', { role: 'button', 'aria-haspopup': 'menu', 'aria-expanded': !!o.open, 'aria-label': 'Language: ' + cur + '. Switch version' }, 'display:inline-flex;align-items:center;gap:8px;height:' + (m ? 44 : 32) + 'px;padding:0 10px;border:1px solid ' + (o.open ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;flex-shrink:0;max-width:100%;width:max-content', Ic('languages'), S('min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', cur), o.code && S(mono + ';flex-shrink:0;' + MU, o.code), Ic('chevrons-up-down', 14))
const Tag = (l, ic) => S('display:inline-flex;align-items:center;gap:4px;font-size:12px;' + MU, ic && Ic(ic, 12), l)
const LRow = r => { const cur = r.cur, add = r.add
return A('div', { role: 'menuitem', 'aria-current': cur || undefined, tabIndex: -1 }, 'display:flex;align-items:center;gap:10px;min-height:' + (m ? 52 : 40) + 'px;padding:6px 12px;' + (cur ? 'background:var(--rc-sand);color:var(--rc-on-sand)' : add ? 'color:var(--ui-text-toned)' : ''), S('width:16px', cur && Ic('check')), D('flex:1;min-width:0;display:flex;flex-direction:column', S('overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:' + (cur ? 600 : 500), r.n), S(mono + (cur ? '' : ';' + MU), r.c)),
add ? S('display:inline-flex;align-items:center;gap:4px;font-weight:500', Ic('plus', 14), 'Add translation') : D('display:flex;gap:4px;flex-wrap:wrap;justify-content:flex-end', r.tag && Tag(r.tag, r.tagIc), Pub(r.st), r.fr && Fr('Needs review'), r.dis && Bd('Disabled', 'neutral', 'eye-off'))) }
const LangPop = (rows, o = {}) => D('position:absolute;z-index:40;' + (m ? 'left:0;right:0;bottom:0;max-height:70%;border-top:1px solid var(--ui-border)' : 'left:' + (o.left ?? 292) + 'px;top:' + (o.top ?? 150) + 'px;width:400px;max-height:' + (o.max ?? 420) + 'px;border:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);display:flex;flex-direction:column',
m && D('display:flex;justify-content:center;padding:8px 0 0;flex-shrink:0', S('width:36px;height:4px;background:var(--ui-border-accented)')),
D('padding:10px 12px 6px;display:flex;flex-direction:column;gap:6px;flex-shrink:0', S('font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;' + MU, 'Versions you can open'), o.find && Fd('', 'Find a language', { val: false })),
A('div', { role: 'menu', 'aria-label': 'Versions of this story' }, 'display:flex;flex-direction:column;overflow-y:auto;min-height:0', ...rows.map(LRow)),
D('padding:10px 12px;border-top:1px solid var(--ui-border);display:flex;gap:8px;flex-shrink:0;' + MU + ';font-size:12px;line-height:18px', Ic('info', 14), 'Switching opens that version. The interface language stays the same.'))
const MENUS = {
all: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }, { n: 'Arabic', c: 'AR', st: 'draft', fr: 1 }],
two: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }],
many: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'English (United Kingdom)', c: 'EN-GB', st: 'draft' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }, { n: 'Italian', c: 'IT', st: 'draft' }, { n: 'Japanese', c: 'JA', st: 'pub' }, { n: 'Portuguese (Brazil)', c: 'PT-BR', add: 1 }, { n: 'Spanish', c: 'ES', add: 1 }, { n: 'Chinese (Simplified)', c: 'ZH-HANS', add: 1 }, { n: 'Arabic', c: 'AR', add: 1 }, { n: 'Dutch', c: 'NL', add: 1 }, { n: 'Polish', c: 'PL', add: 1 }],
ro: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }],
p4: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }, { n: 'Arabic', c: 'AR', st: 'draft', fr: 1, tag: 'Read only', tagIc: 'lock' }],
p5: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }]
}
// ---- editor pieces
const ToolRow = ro => D('display:flex;gap:2px;padding:6px;border:1px solid var(--ui-border);background:var(--ui-bg);overflow:hidden;flex-shrink:0;' + TO, A('div', { role: 'toolbar', 'aria-label': 'Formatting' }, 'display:flex;gap:2px', ...['bold', 'italic', 'underline', 'heading-2', 'list', 'list-ordered', 'quote', 'link', 'image'].map(n => A('span', { role: 'button', 'aria-label': n }, 'width:' + (m ? 44 : 28) + 'px;height:' + (m ? 44 : 28) + 'px;display:flex;align-items:center;justify-content:center;flex-shrink:0', Ic(n)))))
const Body = (L, o = {}) => A('div', { dir: L.dir, lang: L.lang, role: 'textbox', 'aria-multiline': 'true', 'aria-readonly': o.ro || undefined, 'aria-label': (o.label || 'Body') + ' (' + L.n + ')', tabIndex: 0 }, 'position:relative;display:flex;flex-direction:column;gap:12px;padding:16px;border:1px solid var(--ui-border);background:' + (o.ro ? 'var(--ui-bg-muted)' : 'var(--ui-bg)') + ';' + ((m && !o.pane) ? 'flex:none;' : 'flex:1;min-height:0;overflow:auto;') + 'font:400 ' + (L.dir === 'rtl' ? '17px/30px' : '15px/26px') + ' \'Source Serif 4\',serif;color:var(--ui-text)', ...(o.long ? L.p.concat(L.p, L.p) : L.p).map(t => S('text-wrap:pretty', t)))
const TitleField = (L, o) => {
const cap = o.cap, v = o.titleVal ?? L.t
if (!cap.e) return D('display:flex;flex-direction:column;gap:2px', S('font-size:12px;' + MU, 'Title (' + L.n + ')'), A('div', { dir: L.dir, lang: L.lang, role: 'textbox', 'aria-readonly': 'true', 'aria-label': 'Title (' + L.n + ')', tabIndex: 0 }, 'font:600 18px/26px \'IBM Plex Sans\',sans-serif;' + HL, v))
const st = o.titleState, err = st === 'invalid' || st === 'failed'
return D('display:flex;flex-direction:column;gap:4px', S('font-size:12px;' + MU, 'Title (' + L.n + ')'),
D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 44 : 36) + 'px;padding:2px 10px;border:1px solid ' + (err ? 'var(--ui-error)' : st === 'focus' || st === 'dirty' ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:var(--ui-bg);' + (st === 'focus' ? 'outline:2px solid var(--ui-primary);outline-offset:1px' : ''),
A('div', { dir: L.dir, lang: L.lang, role: 'textbox', 'aria-label': 'Title (' + L.n + ')', 'aria-invalid': err || undefined, tabIndex: 0 }, 'flex:1;min-width:0;font:600 18px/26px \'IBM Plex Sans\',sans-serif;' + (st === 'invalid' ? MU : HL) + ';text-wrap:pretty', st === 'invalid' ? 'Title' : v), st === 'saving' ? Ic('loader-circle', 14) : Ic('pencil', 14)),
st === 'invalid' ? S('display:flex;gap:6px;color:var(--ui-error);font-size:12px', Ic('circle-x', 14), 'Enter a title.') : st === 'failed' ? D('display:flex;gap:8px;align-items:center;flex-wrap:wrap;color:var(--ui-error);font-size:12px', Ic('circle-x', 14), 'Couldn\'t save the title. Your text is kept.', Btn('Try again', 'secondary', { sm: true })) : st === 'saving' ? S(MU + ';font-size:12px', 'Saving title…') : st === 'dirty' || st === 'focus' ? S(MU + ';font-size:12px', 'Changing the title doesn\'t change the address. Edit the address in Search and sharing.') : null)
}
const PubAct = (L, o) => ({ l: o.pub === 'pub' ? 'Unpublish ' + L.n : (o.dirty ? 'Save and publish ' : 'Publish ') + L.n, dis: !!o.saving || !!o.saveBlock })
const Life = (L, o) => (o.pub === 'pub' ? 'This version is online. Saving updates the live page right away.' : 'Saving keeps this version as a draft. It isn\'t public until you publish it.') + (o.cap.p && o.pub !== 'pub' ? ' Save and publish saves first, then publishes.' : '')
const SaveLbl = o => o.saving ? 'Saving…' : o.saveFail ? 'Try saving again' : 'Save'
const SaveDis = o => (!o.dirty && !o.saving && !o.saveFail) || !!o.saveBlock
const Edit = (L, o) => {
const cap = o.cap, sr = !!o.src, pa = PubAct(L, o)
const state = o.saveFail ? Bd('Save failed', 'error', 'circle-x') : o.dirty ? Bd('Unsaved changes', 'neutral', 'circle-dot') : null
const btns = []
btns.push(Btn(sr ? 'Hide source reference' : 'Show source reference', 'secondary', { ic: 'columns-2' }))
if (cap.e) btns.push(Btn(SaveLbl(o), 'secondary', { busy: o.saving, dis: SaveDis(o) }))
if (cap.p) btns.push(Btn(pa.l, 'primary', { dis: pa.dis || o.pubDis }))
if (cap.d || cap.c) btns.push(Btn('', 'ghost', { ic: 'ellipsis', al: 'More actions' }))
return D('display:flex;flex-direction:column;gap:10px;flex-shrink:0', D('display:flex;align-items:center;gap:8px;' + MU, Ic('arrow-left', 14), S('font-size:12px', 'Articles')),
TitleField(L, o),
D('display:flex;align-items:center;gap:8px;flex-wrap:wrap', LangBtn(L.n, { code: L.c, open: o.open }), Pub(o.pub || 'draft'), o.fresh && Fr(o.fresh), state, !cap.e && Bd('Read only', 'neutral', 'lock'), ...(m ? [] : [D('flex:1'), ...btns])),
m && D('display:flex;gap:8px', ...btns.slice(0, 1)),
cap.e && o.saveFail && !o.titleState && Alert('error', 'circle-x', 'Couldn\'t save ' + L.n, 'Your changes are kept here. Nothing was published. Try saving again.'),
cap.e && o.dirty && !o.saveFail && !o.titleState && S(MU + ';font-size:12px;line-height:18px;text-wrap:pretty', Life(L, o)),
!cap.e && Alert('neutral', 'lock', cap.p ? 'You can publish this version but not edit it' : 'You can read this version', cap.p ? 'Fields are read-only. Publish and Unpublish stay available. The source reference is still available.' : 'You can\'t edit it. The source reference and versions you can read stay available.'),
cap.e && !cap.p && Alert('neutral', 'info', 'You can edit this version but not publish it', (o.pub === 'pub' ? 'This version is online, so saving updates the live page right away.' : 'Saving keeps it as a draft.') + ' Publishing and unpublishing need the publish permission.'))
}
const MBar = (L, o) => { if (!m) return null; const cap = o.cap, pa = PubAct(L, o), bs = []; if (cap.e) bs.push(Btn(SaveLbl(o), 'secondary', { busy: o.saving, dis: SaveDis(o) })); if (cap.p) bs.push(D('flex:1 1 170px;display:flex', Btn(pa.l, 'primary', { w: '100%', dis: pa.dis }))); if (cap.d || cap.c) bs.push(Btn('', 'ghost', { ic: 'ellipsis', al: 'More actions' })); return bs.length ? D('position:absolute;left:0;right:0;bottom:0;z-index:30;background:var(--ui-bg);border-top:1px solid var(--ui-border);padding:12px 16px;display:flex;gap:8px;flex-wrap:wrap', ...bs) : null }
const Seg = (on, L, S0) => A('div', { role: 'radiogroup', 'aria-label': 'Show' }, 'display:flex;border:1px solid var(--ui-border-accented);flex-shrink:0', ...['Source (' + S0.n + ')', 'Edit (' + L.n + ')'].map((t, i) => A('span', { role: 'radio', 'aria-checked': (on ? i === 0 : i === 1) }, 'flex:1;height:44px;display:flex;align-items:center;justify-content:center;font-weight:500;background:' + ((on ? i === 0 : i === 1) ? 'var(--ui-primary)' : 'var(--ui-bg)') + ';color:' + ((on ? i === 0 : i === 1) ? 'var(--ui-bg)' : 'var(--ui-text-toned)'), t)))
const Src = (S0, o = {}) => D('flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;gap:10px',
D('display:flex;align-items:center;gap:8px;padding:8px 12px;background:var(--ui-bg-elevated);flex-wrap:wrap', Ic('book-open', 14), S('font-weight:600;' + HL, 'Reference: ' + S0.n), S(mono + ';' + MU, S0.c), Pub('pub'), S('margin-left:auto;display:inline-flex;gap:4px;' + MU + ';font-size:12px', Ic('lock', 14), 'Read only'), Btn('Change reference', 'secondary', { sm: true })),
S(MU + ';font-size:12px;line-height:18px', 'Reference only. The tracked source for review is English, revision 8. Changing the reference doesn\'t change it.'),
o.gone ? Alert('warning', 'triangle-alert', 'This reference isn\'t available', 'It was deleted or you can no longer read it. Your French version isn\'t affected.', Btn('Choose another reference', 'secondary', { sm: true })) : [
o.chg && Alert('neutral', 'info', 'Changed fields since French was reviewed (revision 7 → 8)', 'Title, Body (2 paragraphs). Fields are listed, not compared line by line.'),
A('div', { dir: S0.dir, lang: S0.lang, role: 'heading', 'aria-level': '2', tabIndex: 0 }, 'font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, S0.t),
Body(S0, { ro: true, label: 'Reference', long: o.long, pane: true })])
const EdPage = (L, o, extra) => D('flex:1;min-width:0;min-height:0;overflow:' + (m ? 'auto' : 'hidden') + ';display:flex;flex-direction:column;gap:12px;padding:' + (m ? '16px 16px 128px' : '24px') + ';position:relative', Edit(L, o), ...[].concat(extra || []), (o.cap.e && !(m && o.srcMob)) ? ToolRow() : null, (m && o.srcMob) ? null : Body(L, { ro: !o.cap.e, long: o.long }), (!m && o.seoNote) ? Alert('neutral', 'info', 'Search and sharing: description is empty', 'No fallback: the English site default isn\'t used for French. Add a French description or the tag is left out.') : null)
const Editor = (L, o, extra) => {
const S0 = o.S0 || EN
if (!o.src) return D('flex:1;min-height:0;display:flex;position:relative', EdPage(L, o, extra), MBar(L, o))
if (m) { const on = !!o.srcMob; return D('flex:1;min-height:0;position:relative;display:flex;flex-direction:column', D('flex:1;min-height:0;display:flex;flex-direction:column;gap:12px;padding:16px 16px 128px', Seg(on, L, S0), on ? Src(S0, o) : D('flex:1;min-height:0;overflow-y:auto;display:flex;flex-direction:column;gap:12px', Edit(L, { ...o, src: 1 }), o.cap.e && ToolRow(), Body(L, { ro: !o.cap.e, long: o.long }))), MBar(L, o)) }
return D('flex:1;min-height:0;display:flex', D('width:' + (o.narrow ? '42%' : '50%') + ';flex-shrink:0;min-width:0;border-right:1px solid var(--ui-border);overflow:hidden;padding:24px;display:flex;flex-direction:column;background:var(--ui-bg-muted)', Src(S0, o)), EdPage(L, o, extra))
}
// ---- site shells
const setS = { nav: 'settings', crumbs: ['Settings', 'Sites', 'Journal', 'Languages'], mTitle: 'Journal · Languages', mLead: 'arrow-left' }
const edS = { nav: 'articles', crumbs: ['Articles', 'Editor'], mTitle: 'Editor', mLead: 'arrow-left' }
const liS = { nav: 'articles', crumbs: ['Articles'], mTitle: 'Articles' }
// ---- site languages
const LNG = FX.LANGS.map(l => { const c = FX.count(l.code, FX.STORIES); return [l.name, l.code, l.def ? 'Default' : l.enabled ? '' : 'Disabled', c[0], c[1], l.social] })
const LRows = ro => LNG.map(r => D(ROW + ';opacity:' + (r[2] === 'Disabled' ? .8 : 1), D('flex:1;min-width:0;display:flex;flex-direction:column', S('font-weight:500;' + HL + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', r[0]), S(mono + ';' + MU, r[1] + ' · social locale ' + (r[5] === 'Not set' ? 'not set' : r[5]))), r[2] === 'Default' ? Bd('Default', 'info', 'star') : r[2] === 'Disabled' ? Bd('Disabled', 'neutral', 'eye-off') : null, !m && S(MU + ';width:190px', r[3] + (r[3] === 1 ? ' version · ' : ' versions · ') + r[4] + ' published'), !ro && r[2] === 'Disabled' ? Btn('Re-enable', 'secondary', { sm: true }) : null, !ro && Btn('', 'ghost', { ic: 'ellipsis', sm: true, al: 'Actions for ' + r[0] })))
const slPage = () => {
const ro = x === 'readonly', single = x === 'single', site = single ? 'Archive' : 'Journal', T = 'Languages', SUB = 'Content languages for ' + site + '. Each article version is written in one language.'
if (x === 'loading') return Page(T, SUB, null, Sk(4))
if (x === 'error') return Page(T, SUB, null, Alert('error', 'circle-x', 'Couldn\'t load languages', 'Check your connection and try again.', Btn('Try again', 'secondary', { sm: true })))
return Page(T, SUB, !ro && Btn('Add language', 'primary', { ic: 'plus' }),
ro && Alert('neutral', 'lock', 'Only owners can change languages', 'You can see which languages are enabled and how many versions each has. Ask an owner to add, disable or change a language.'),
x === 'disableDefault' && Alert('warning', 'triangle-alert', 'English is the default', 'Choose another default first, then disable English.'),
x === 'disableLast' && Alert('warning', 'triangle-alert', 'At least one language must stay enabled', 'Journal needs a language for new articles.'),
single ? Card(S('font-weight:600;' + HL, 'Archive uses one language'), S(TO, 'New articles are written in Portuguese (Brazil). Add a language to write translations and publish them separately.'), D('display:flex;gap:10px;align-items:center', Bd('Default', 'info', 'star'), S('font-weight:500;' + HL, 'Portuguese (Brazil)'), S(mono + ';' + MU, 'pt-BR'))) :
D('background:var(--ui-bg);border:1px solid var(--ui-border)', D('padding:8px 16px;background:var(--ui-bg-elevated);' + MU + ';font-weight:500', 'Language'), ...LRows(ro)),
single && Alert('info', 'info', 'Regional language kept: pt-BR', 'Setup kept the full site locale, so the default content language is Portuguese (Brazil), not plain Portuguese. The site locale still formats dates and numbers. Social metadata locale: pt_BR, taken from the language tag.'),
!single && !ro && S(MU + ';font-size:12px;text-wrap:pretty', 'Disabling a language keeps its versions readable and editable, and published ones stay online. Drafts can be published again after you re-enable it.'))
}
const slOv = () => {
const addBody = (err) => [err && Alert('error', 'circle-x', 'Couldn\'t save languages', 'Nothing changed. Your selection is kept. Try again.'), Fd('Language', 'Portuguese (Brazil)', { sel: true, focus: !err }), Fd('Language code', 'pt-BR', { mono: true, ro: true, lock: true, help: 'Used in the API and in page language metadata.' }), Fd('Social metadata locale', 'pt_BR', { mono: true, ro: true, lock: true, help: 'Taken from the language code. For a code without a region, such as fr, an owner sets one or leaves it empty and og:locale is left out.' })]
if (x === 'add') return Dlg({ title: 'Add a language', desc: 'Choose a language for Journal. Regional variants such as English (United Kingdom) are separate entries.' }, addBody(), [Btn('Cancel'), Btn('Add language', 'primary')])
if (x === 'saveFail') return Dlg({ title: 'Add a language', desc: 'Choose a language for Journal.' }, addBody(true), [Btn('Cancel'), Btn('Try again', 'primary')])
if (x === 'adddup') return Dlg({ title: 'Add a language', desc: 'Choose a language for Journal.' }, [Fd('Language', 'French', { sel: true, err: 'French is already enabled for Journal.' })], [Btn('Cancel'), Btn('Add language', 'primary', { dis: true })])
if (x === 'default') return Dlg({ title: 'Make French the default language?', desc: 'New articles start in the default language, and it is the first choice for each story\'s title in the list.' }, [Alert('info', 'info', 'No content changes', 'Existing versions keep their language, status and links. Public requests that name no language will resolve French where a published French version exists, and return 404 where one doesn\'t. Requests that name a language aren\'t affected.')], [Btn('Cancel'), Btn('Make French default', 'primary')])
if (x === 'disable') return Dlg({ title: 'Disable German?', desc: 'German is used by 4 versions: 1 draft and 3 published.' }, [Alert('warning', 'triangle-alert', 'Nothing is deleted or unpublished', 'The 3 published German versions stay online. All 4 versions stay readable and editable. The draft can\'t be published until you re-enable German. Nobody can create new German versions.')], [Btn('Cancel'), Btn('Disable German', 'primary')])
if (x === 'reenable') return Dlg({ title: 'Re-enable Spanish?', desc: 'Writers can create Spanish versions again. The 1 existing Spanish version stays as it is. Nothing else changes.' }, [], [Btn('Cancel'), Btn('Re-enable Spanish', 'primary')])
if (x === 'remove') return Dlg({ title: 'Remove Italian?', desc: 'No article uses Italian. You can add it again later.' }, [], [Btn('Cancel'), Btn('Remove Italian', 'danger')])
if (x === 'removeBlocked') return Dlg({ title: 'Spanish can\'t be removed', desc: 'Spanish is used by 1 version. Disable the language to keep it, or delete it first.' }, [], [Btn('Close'), Btn('Disable Spanish', 'primary')])
if (x === 'saved') return [Toast('success', 'circle-check', 'French added', 'Writers can now create French versions.')]
return null
}
// ---- setup
const SUV = FX.SETUP, SUN = SUV.reduce((a, r) => a + r.n, 0)
const suVal = (r, chosen) => r.name ? r.name + ' · ' + r.tag : chosen ? 'English · en' : 'Choose a language'
const suBody = () => {
if (x === 'empty') return Page('Languages', 'Content languages for Gallery.', null, Card(S('font-weight:600;' + HL, 'Confirm the default language'), S(TO, 'Gallery has no articles yet. New articles will be written in this language.'), Fd('Default content language', 'English (United Kingdom) · en-GB', { sel: true, help: 'Seeded from site locale en-GB. The full locale is kept. Confirm or change it.' }), D('display:flex', Btn('Save', 'primary'))))
const done = x === 'done', busy = x === 'applying', fail = x === 'fail', chosen = x !== 'ready'
return Page('Set article languages', 'Existing articles don\'t have a language yet. Confirm or change the language for each site.', null,
done ? Alert('success', 'circle-check', 'Languages assigned', SUN + ' articles now have a language. Nothing was translated, and no articles were grouped.', Btn('Open Articles', 'secondary', { sm: true })) :
fail ? Alert('error', 'circle-x', 'Nothing was changed', 'The update failed, so no article has a language yet. Your choices are kept. Try again; running it twice is safe.', Btn('Try again', 'secondary', { sm: true })) :
busy ? Alert('info', 'loader-circle', 'Assigning languages', 'Articles created while this runs get the site\'s default language.') :
Alert('info', 'info', 'This labels articles. It doesn\'t translate them.', 'Each existing article becomes the only version of its own story. Each site\'s language is seeded from its locale with the region kept: pt-BR stays Portuguese (Brazil). Nothing is applied until you confirm.'),
D('background:var(--ui-bg);border:1px solid var(--ui-border)', ...SUV.map(r => D(ROW + ';' + (m ? 'flex-wrap:wrap' : ''), D('flex:1;min-width:140px;display:flex;flex-direction:column', S('font-weight:600;' + HL, r.site), S(MU + ';font-size:12px', r.n + ' articles · ' + r.note)), D('width:' + (m ? '100%' : '300px'), Fd('', suVal(r, chosen), { sel: !done, val: !!(r.name || chosen), help: done ? null : r.name ? 'Seeded from site locale ' + r.locale + '. Confirm or change.' : chosen ? null : 'Required before you can apply.' }))))),
!done && D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Btn(busy ? 'Assigning…' : 'Confirm and assign to ' + SUN + ' articles', 'primary', { busy, dis: busy || x === 'ready' }), S(MU + ';font-size:12px', 'Counts are articles in ' + SUV.length + ' sites.')))
}
// ---- list
const FXS = FX.STORIES, LN = { en: 'English', fr: 'French', de: 'German', ar: 'Arabic', es: 'Spanish' }
const upMap = v => Object.fromEntries(Object.entries(v).map(([k, q]) => [k.toUpperCase(), q]))
const Cov = (map, o = {}) => D('display:flex;gap:6px;flex-wrap:wrap', ...(o.langs || FX.ENABLED.map(k => k.toUpperCase()).concat(map.ES ? ['ES'] : [])).filter(k => !o.hideMissing || map[k]).map(k => { const q = map[k], st = q && q[0], nr = q && q[1]; return A('span', { 'aria-label': k + ': ' + (st === 'pub' ? 'published' : st === 'draft' ? 'draft' : 'no version') + (nr ? ', needs review' : '') + (k === 'ES' ? ', language disabled' : '') }, 'display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 6px;font:500 12px \'IBM Plex Mono\',monospace;' + (st ? 'border:1px solid var(--ui-border-accented);' + HL : 'border:1px dashed var(--ui-border-accented);' + MU), k, st === 'pub' ? Ic('circle-check', 12) : st === 'draft' ? Ic('pencil', 12) : Ic('minus', 12), nr && Ic('refresh-cw', 12)) }))
const Pager = (total, unitName, pages, size = 5) => D('display:flex;align-items:center;gap:12px;flex-wrap:wrap;' + (m ? 'justify-content:space-between' : ''), S(MU + ';font-size:12px;' + (m ? 'flex:1 1 100%' : 'flex:1'), 'Showing 1–' + Math.min(size, total) + ' of ' + total + ' ' + unitName),
D('display:flex;align-items:center;gap:8px;' + MU + ';font-size:12px', 'Rows', D('', Fd('', String(size), { sel: true }))),
D('display:flex;align-items:center;gap:6px', Btn('', 'secondary', { ic: 'chevron-left', al: 'Previous page', dis: true, sm: !m }), S(HL + ';font-weight:500;padding:0 6px', m ? 'Page 1 of ' + pages : '1 of ' + pages), Btn('', 'secondary', { ic: 'chevron-right', al: 'Next page', dis: pages < 2, sm: !m })))
const Chip = l => D('display:inline-flex;align-items:center;gap:6px;height:' + (m ? 36 : 24) + 'px;padding:0 8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);' + HL + ';font-size:12px', l, A('span', { role: 'button', 'aria-label': 'Remove filter ' + l }, 'display:inline-flex', Ic('x', 14)))
const AT = [{ t: 'A complete guide to renovating a ground-floor kitchen and bathroom in a listed building, with every cost itemised for owners and tenants', v: { EN: ['pub', 0], 'EN-GB': ['draft', 0], FR: ['draft', 1], DE: ['pub', 0], 'PT-BR': ['draft', 0], 'ZH-HANS': ['pub', 0], JA: ['pub', 0], AR: ['draft', 0] }, by: 'Marta Alves', upd: '2 h ago' }, { t: 'Heat pumps for stone farmhouses', v: { EN: ['pub', 0], 'EN-GB': ['pub', 0], FR: ['draft', 0], DE: ['pub', 0], JA: ['draft', 0] }, by: 'Luis Ferrer', upd: 'Yesterday' }, { t: 'Sourcing reclaimed oak beams', v: { EN: ['draft', 0], 'EN-GB': ['draft', 0] }, by: 'Camille Roche', upd: '14 Oct' }]
const liPage = () => {
const press = x === 'emptyMissing', atel = x === 'long', lim = x === 'limited', fail = x === 'listFail', none = x === 'noResults', load = x === 'frLoading', draftF = x === 'filters'
const missView = x === 'frMissing' || x === 'tabsScroll' || press, frView = x === 'fr' || load || draftF || missView, active = draftF || atel
const DS = press ? FX.PRESS : FXS, isD = q => q && q[0] === 'draft'
const frV = DS.filter(s => s.v.fr && (!draftF || isD(s.v.fr))), frM = DS.filter(s => !s.v.fr && (!draftF || Object.values(s.v).some(isD)))
let tabs
if (frView) tabs = [['All', frV.length], ['Published', frV.filter(s => s.v.fr[0] === 'pub').length], ['Drafts', frV.filter(s => isD(s.v.fr)).length], ['Needs review', frV.filter(s => s.v.fr[1]).length], ['Not in French', frM.length]]
else if (atel) tabs = [['All stories', 3], ['Needs translation', 3], ['Needs review', 1]]
else if (none) tabs = [['All stories', 0], ['Needs translation', 0], ['Needs review', 0]]
else if (lim) tabs = [['All stories', 3], ['Needs translation', 3], ['Needs review', 0]]
else tabs = [['All stories', DS.length], ['Needs translation', DS.filter(s => FX.ENABLED.some(k => !s.v[k])).length], ['Needs review', DS.filter(s => Object.values(s.v).some(q => q[1])).length]]
if (load || fail) tabs = tabs.map(q => [q[0], '–'])
const tabOn = missView ? 4 : 0, site = press ? 'Press' : atel ? 'Atelier' : 'Journal'
const filt = () => D('display:flex;gap:8px;flex-wrap:wrap;align-items:center', D('flex:1;min-width:' + (m ? '0' : '220px'), Fd('', none ? 'zinc' : frView ? 'Search French titles' : 'Search titles in every language', { val: none })),
m ? Btn('Filters' + (active ? ' · 3' : ''), 'secondary', { ic: 'sliders-horizontal', al: active ? 'Filters, 3 active' : 'Filters' }) : [Fd('', site, { sel: true }), Fd('', frView ? 'French' : 'All languages', { sel: true }), Fd('', active ? 'Draft' : 'Any status', { sel: true }), Fd('', 'Any author', { sel: true }), Fd('', atel ? 'SEO: Needs attention' : 'Any SEO state', { sel: true })])
const chips = active && D('display:flex;gap:6px;flex-wrap:wrap', ...(draftF ? [Chip('Site: Journal'), Chip('Language: French'), Chip('Status: Draft')] : [Chip('Site: Atelier'), Chip('Status: Draft'), Chip('SEO: Needs attention')]), Btn('Clear all', 'ghost', { sm: true }))
const shift = x === 'tabsScroll' ? -96 : 0
const tabRow = D('position:relative;border-bottom:1px solid var(--ui-border);display:flex;align-items:center;gap:4px', m && Btn('', 'ghost', { ic: 'chevron-left', sm: true, al: 'Scroll tabs left', dis: !shift }),
A('div', { role: 'tablist', 'aria-label': 'Article views' }, 'flex:1;min-width:0;overflow:hidden;display:flex;gap:' + (m ? 16 : 24) + 'px;position:relative', D('display:flex;gap:' + (m ? 16 : 24) + 'px;flex-shrink:0;transform:translateX(' + (m ? shift : 0) + 'px)', ...tabs.map((q, i) => A('div', { role: 'tab', 'aria-selected': i === tabOn }, 'padding:' + (m ? '12px 0' : '8px 0') + ';white-space:nowrap;font-weight:' + (i === tabOn ? 600 : 500) + ';color:' + (i === tabOn ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)') + ';border-bottom:2px solid ' + (i === tabOn ? 'var(--ui-primary)' : 'transparent'), q[0] + ' ', S(MU + ';font-weight:400', q[1])))), m && D('position:absolute;right:0;top:0;bottom:0;width:28px;background:linear-gradient(90deg,transparent,var(--ui-bg))'), m && shift && D('position:absolute;left:0;top:0;bottom:0;width:28px;background:linear-gradient(270deg,transparent,var(--ui-bg))')), m && Btn('', 'ghost', { ic: 'chevron-right', sm: true, al: 'Scroll tabs right' }))
const Row = (title, sub, right, lg) => D(ROW + ';' + (m ? 'flex-wrap:wrap' : ''), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', A('span', { lang: lg }, HL + ';font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', title), sub && S(MU + ';font-size:12px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', sub)), right)
const Story = (title, sub, map, o, by, upd) => D(ROW + ';' + (m ? 'flex-direction:column;align-items:stretch;gap:6px' : ''), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S(HL + ';font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', title), sub && S(MU + ';font-size:12px', sub)), D((m ? '' : 'width:230px;') + 'flex-shrink:0', Cov(map, o)), m ? S(MU + ';font-size:12px', by + ' · ' + upd) : S(MU + ';width:120px', by), !m && S(MU + ';width:90px', upd))
const card = 'background:var(--ui-bg);border:1px solid var(--ui-border)', head = !m && D('display:flex;gap:12px;padding:8px 16px;background:var(--ui-bg-elevated);' + MU + ';font-weight:500', S('flex:1', 'Story'), S('width:230px', 'Languages'), S('width:120px', 'Author'), S('width:90px', 'Updated'))
let body, total = 0, unit = '', pages = 1
if (fail) body = Alert('error', 'circle-x', 'Couldn\'t load articles', 'Your filters and page are kept. Try again.', Btn('Try again', 'secondary', { sm: true }))
else if (load) body = Sk(5)
else if (press) body = D('padding:48px 16px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;' + card, Ic('circle-check', 28), S('font-weight:600;' + HL, 'Every story you can see has a French version'), S(TO + ';max-width:360px', 'Stories without a French version will appear here.'))
else if (none) body = D('padding:48px 16px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;' + card, Ic('search', 28), S('font-weight:600;' + HL, 'No stories match "zinc"'), S(TO, 'Search covers titles in every language you can read.'), Btn('Clear search', 'secondary', { sm: true }))
else if (missView) { const rep = s => ['en', 'fr', 'de', 'ar', 'es'].find(k => s.v[k]); body = D(card, ...frM.map(s => { const k = rep(s), q = s.v[k]; return Row(s.title, 'Opens ' + LN[k] + ' · ' + (q[0] === 'pub' ? 'published' : 'draft') + (k === 'es' ? ' · language disabled' : ''), Btn(m ? '' : 'Add French translation', 'secondary', { sm: true, ic: 'plus', al: 'Add French translation to ' + s.title })) })); total = frM.length; unit = 'stories with no French version you can read' }
else if (frView) { body = D(card, ...frV.map(s => Row(s.tr.fr, m ? s.by + ' · ' + s.upd : null, D('display:flex;gap:4px;flex-wrap:wrap;justify-content:flex-end', Pub(s.v.fr[0]), s.v.fr[1] ? Fr('Needs review') : null, !m && S(MU + ';width:120px;margin-left:12px', s.by), !m && S(MU + ';width:80px', s.upd)), 'fr'))); total = frV.length; unit = 'French versions' }
else if (atel) { body = D(card, head, ...AT.map(r => Story(r.t, null, r.v, { langs: ['EN', 'EN-GB', 'FR', 'DE', 'PT-BR', 'ZH-HANS', 'JA', 'AR'] }, r.by, r.upd))); total = 3; unit = 'stories' }
else { const rows = lim ? [FXS[1], FXS[2], FXS[3]] : FXS.slice(0, 5); body = D(card, head, ...rows.map(s => Story(s.title, s.v.es && !s.v.en ? 'Spanish only · language disabled on Journal' : null, lim ? { EN: s.v.en } : upMap(s.v), { hideMissing: lim }, s.by, s.upd))); total = lim ? 3 : FXS.length; unit = lim ? 'stories you can read' : 'stories'; pages = lim ? 1 : Math.ceil(total / 5) }
return D('flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:' + (m ? 12 : 16) + 'px;padding:' + (m ? '16px 16px 16px' : '24px') + ';position:relative', D('display:flex;align-items:center;gap:12px', S('flex:1;font:600 20px/28px \'IBM Plex Sans\',sans-serif;' + HL, 'Articles'), Btn(m ? '' : 'New article', 'primary', { ic: 'plus', al: 'New article' })), lim && Alert('neutral', 'lock', 'Showing only what you can read', 'Languages, counts and missing-translation results reflect versions you have permission to see.'), filt(), chips, tabRow, body, total ? Pager(total, unit, pages) : null)
}
const sheet = () => D('position:absolute;inset:0;z-index:50;background:var(--rc-overlay);display:flex;align-items:' + (m ? 'flex-end' : 'center') + ';justify-content:center', A('div', { role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Filters' }, 'width:100%;max-width:' + (m ? '100%' : '420px') + ';max-height:92%;background:var(--ui-bg);display:flex;flex-direction:column', D('display:flex;justify-content:center;padding:8px 0 0', S('width:36px;height:4px;background:var(--ui-border-accented)')),
D('padding:16px 16px 0;display:flex;align-items:center', S('flex:1;font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, 'Filters'), Btn('', 'ghost', { ic: 'x', al: 'Close filters' })),
D('padding:16px;display:flex;flex-direction:column;gap:14px;overflow-y:auto;min-height:0', Fd('Site', 'Journal', { sel: true }), Fd('Language', 'French', { sel: true }), Fd('Status', 'Draft', { sel: true }), Fd('Author', 'Any author', { sel: true }), Fd('SEO', 'Any SEO state', { sel: true })),
D('padding:12px 16px;border-top:1px solid var(--ui-border);display:flex;gap:8px', Btn('Clear all', 'secondary'), D('flex:1;display:flex', Btn('Show 2 results', 'primary', { w: '100%' })))))
// ---- new article
const naOv = () => {
const multi = x !== 'single'
return Dlg({ title: 'New article', desc: x === 'translation' ? 'Create a translation of a story you can read.' : 'Give the article a title. You can write the rest in the editor.' }, [
x === 'translation' ? [Fd('Story', 'Roof inspection checklist', { sel: true, help: 'No French version yet. Copies from English · published' }), Fd('Language', 'French', { sel: true }), Fd('Title', 'Liste de contrôle pour l\'inspection de la toiture', { focus: true, lang: 'fr' })] :
[Fd('Title', x === 'sitechange' ? 'Spring newsletter' : 'Title', { val: x === 'sitechange', focus: x !== 'sitechange', help: '0 / 200' }), Fd('Site', x === 'sitechange' ? 'Notes' : 'Journal', { sel: true }),
multi && Fd('Language', 'English', { sel: true, help: x === 'sitechange' ? 'Notes only offers English, so the language was reset from French.' : 'Default for Journal.' }),
multi && D('display:flex;flex-direction:column;gap:8px', Radio('New story', 'Starts a story in this language.', true), Radio('Translation of an existing story', 'Uses the same rules as Add translation.', false))]
], [Btn('Cancel'), Btn(x === 'translation' ? 'Create French draft' : 'Create article', 'primary')])
}
// ---- add translation
const FRT = S1.tr.fr, FRT5 = 'Liste de contrôle pour l\'inspection de la toiture'
const LS = st => LV('en', 'English', 'EN', st)
const atOv = () => {
const empty = x === 'empty', long = x === 'long', dupCase = x === 'dup' || x === 'dupHidden', st = dupCase ? S1 : S5, ft = dupCase ? FRT : FRT5, rev = st.v.en[2]
const f = [Fd('Language', 'French', { sel: true }), Fd('Source version', 'English · published · revision ' + rev, { sel: true, help: 'You choose which version to copy from. It is also the source tracked for review. Access is checked again when you create the draft.' }), Fd('Title in French', long ? ft + ' avec les coûts cachés derrière les carreaux, sous la baignoire et autour des canalisations anciennes' : ft, { focus: x === 'form', lang: 'fr', help: 'Copying text is a starting point: it stays in English until you translate it.' }),
D('display:flex;flex-direction:column;gap:8px', S('font-weight:500;' + HL, 'Starting content'), Radio('Start empty', 'Only the title. Freshness shows No source until you pick a tracked source.', empty), Radio('Copy from English', 'Copies body, excerpt, search and social text, image alternative text and authors. Images are reused, files aren\'t duplicated. The version starts as Needs review.', !empty))]
if (long) f.push(Alert('neutral', 'info', 'Authors', 'Marta Alves, Camille Roche, Luis Ferrer, Ana Pereira, Jonas Weber and 4 more will be copied. You can change them in the editor.'), Alert('neutral', 'info', 'Images', 'Cover image and 7 inline images are reused. Alternative text is copied and stays in English until you translate it.'))
if (x === 'dup') return Dlg({ title: 'Add a translation' }, [Alert('error', 'circle-x', 'French already exists for this story', 'Open it to continue. Your entries are kept here.', Btn('Open French version', 'secondary', { sm: true })), ...f], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
if (x === 'dupHidden') return Dlg({ title: 'Add a translation' }, [Alert('error', 'circle-x', 'French can\'t be created for this story', 'Try a different language, or ask an owner. Your entries are kept.'), ...f], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
if (x === 'fail') return Dlg({ title: 'Add a translation' }, [Alert('error', 'circle-x', 'Couldn\'t create the translation', 'Your entries are kept. Try again.'), ...f], [Btn('Cancel'), Btn('Try again', 'primary')])
if (x === 'gone') return Dlg({ title: 'Add a translation' }, [Alert('warning', 'triangle-alert', 'The source isn\'t available', 'It was deleted or you can no longer read it. Choose another source you can read, or start empty.'), Fd('Source version', 'Choose a version', { sel: true, val: false }), Fd('Title in French', ft)], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
if (x === 'saving') return Dlg({ title: 'Add a translation' }, f, [Btn('Cancel', 'secondary', { dis: true }), Btn('Creating…', 'primary', { busy: true })])
return Dlg({ title: 'Add a translation', desc: 'Creates a French draft of "' + st.title + '". It isn\'t published.', top: long ? 24 : 60 }, f, [Btn('Cancel'), Btn('Create French draft', 'primary')])
}
// ---- publish / delete
const pdOv = () => ({
publish: () => Dlg({ title: 'Publish French?', desc: 'Only the French version goes online. English and German aren\'t affected.' }, [], [Btn('Cancel'), Btn('Publish French', 'primary')]),
unpublish: () => Dlg({ title: 'Unpublish French?', desc: 'The French version goes back to draft and stops being public. English stays online.' }, [], [Btn('Cancel'), Btn('Unpublish French', 'primary')]),
delete: () => Dlg({ title: 'Delete the French version?', desc: '"' + FRT + '" will be deleted. Other versions stay.' }, [], [Btn('Cancel'), Btn('Delete French version', 'danger')]),
deleteSource: () => Dlg({ title: 'Delete the English version?', desc: 'English is the tracked source for versions you can read.' }, [Alert('warning', 'triangle-alert', 'Review tracking will pause', 'Versions that track English stay as they are. Their review history is kept, and they ask you to choose a new source.')], [Btn('Cancel'), Btn('Delete English version', 'danger')]),
deleteLast: () => Dlg({ title: 'Delete the only version?', desc: 'English is the only version of this story. Deleting it deletes the story.' }, [], [Btn('Cancel'), Btn('Delete article', 'danger')]),
saveAndPublish: () => Dlg({ title: 'Save and publish French?', desc: 'Your unsaved changes to the French version are saved first, then only the French version goes online. English and German aren\'t affected.' }, [], [Btn('Cancel'), Btn('Save and publish French', 'primary')]),
saveFailPublish: () => [Toast('error', 'circle-x', 'Couldn\'t save French', 'Nothing was published. Your changes are kept. Try again.')],
deleteStory: () => Dlg({ title: 'You can\'t delete this story', desc: 'Deleting a story needs delete permission on every version. You can still delete the version you are editing.' }, [], [Btn('Close'), Btn('Delete French version', 'danger')]),
fail: () => [Toast('error', 'circle-x', 'Couldn\'t publish French', 'Nothing changed. Try again.')],
done: () => [Toast('success', 'circle-check', 'French published', 'Other versions weren\'t changed.')]
}[x]?.())
// ---- freshness
const frOv = () => ({
review: () => Dlg({ title: 'Mark French as reviewed?', desc: 'This records that French revision 5 matches English revision 8 (14 Oct 2026). It doesn\'t change publication.' }, [Alert('neutral', 'info', 'Changed fields', 'Title, Body. Authors and the cover image don\'t count.')], [Btn('Cancel'), Btn('Mark as reviewed', 'primary')]),
changed: () => Dlg({ title: 'English changed again', desc: 'English is now at revision 9. You were reviewing revision 8.' }, [Alert('warning', 'triangle-alert', 'Review not recorded', 'Check the latest changes first. Nothing was marked.')], [Btn('Cancel'), Btn('Show source reference', 'secondary'), Btn('Review revision 9', 'primary')]),
changedTr: () => Dlg({ title: 'French changed while you were reviewing', desc: 'Camille Roche saved French revision 6 after you opened this.' }, [Alert('warning', 'triangle-alert', 'Review not recorded', 'This changed since you opened it. Review again.')], [Btn('Cancel'), Btn('Reload French', 'primary')]),
replace: () => Dlg({ title: 'Choose a tracked source', desc: 'Pick a version of this story you can read. French will show Needs review until you review it against the new source.' }, [Radio('English', 'Published · revision 8', false), Radio('German', 'Published · revision 3', true), Alert('error', 'circle-x', 'German can\'t be the source', 'It tracks French as its source. A source can\'t track back to this version.')], [Btn('Cancel'), Btn('Use German as source', 'primary', { dis: true })]),
done: () => [Toast('success', 'circle-check', 'French marked as reviewed', 'Reviewed against English revision 8.')],
fail: () => [Toast('error', 'circle-x', 'Couldn\'t mark as reviewed', 'Nothing was recorded. Try again.')]
}[x]?.())
// ---- permissions / conflicts
const pmOv = () => ({
session: () => Dlg({ title: 'Your session expired', desc: 'Log in to continue. Your title, body and search fields for this French version are kept in this browser and restored after you log in, with a notice.' }, [], [Btn('Log in', 'primary')]),
conflict: () => Dlg({ title: 'Someone else saved this version', desc: 'Camille Roche saved French revision 6 at 14:32. Your changes aren\'t saved. They stay here until you discard them.', w: 520 }, [Alert('neutral', 'info', 'Save over theirs', 'Replaces the title, body and search fields of the French version only. Other languages aren\'t changed. If someone saves again first, you\'ll be asked again.')], [Btn('Keep my changes'), Btn('Download my version', 'secondary', { ic: 'download' }), Btn('Reload theirs…'), Btn('Save over theirs', 'danger')]),
move: () => Dlg({ title: 'Move the story to Notes?', desc: 'This moves every version together.' }, [Fd('Site', 'Notes', { sel: true }), Alert('warning', 'triangle-alert', 'Notes doesn\'t enable every language in this story', 'Enable the missing languages on Notes first. Disabled-language versions count too. Nothing is split.')], [Btn('Cancel'), Btn('Move story', 'primary', { dis: true })]),
moveSlug: () => Dlg({ title: 'Move the story to Archive?', desc: 'All versions move together.' }, [Fd('Site', 'Archive', { sel: true }), Alert('error', 'circle-x', 'A French address is already used on Archive', 'Change the French address before moving.', Btn('Edit French address', 'secondary', { sm: true }))], [Btn('Cancel'), Btn('Move story', 'primary', { dis: true })]),
movePerm: () => Dlg({ title: 'You can\'t move this story' }, [Alert('error', 'circle-x', 'Moving needs edit permission on every version', 'Ask an owner to move it. Nothing was changed.')], [Btn('Close')])
}[x]?.())
// ---- compose
const EDV = { v: 'ed' }
if (v === 'sl') { Object.assign(shell, setS, x === 'single' ? { crumbs: ['Settings', 'Sites', 'Archive', 'Languages'], mTitle: 'Archive · Languages' } : {}); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', slPage()); ov = [].concat(slOv() || []) }
else if (v === 'su') { Object.assign(shell, setS, { crumbs: ['Settings', 'Languages'] }); main = D('flex:1;min-height:0;display:flex;flex-direction:column', suBody()) }
else if (v === 'li') { Object.assign(shell, liS); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', liPage()); if (x === 'filters' && m) ov = [sheet()] }
else if (v === 'na') { Object.assign(shell, liS); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', liPage()); ov = [naOv()] }
else if (v === 'seo' || v === 'api') {
Object.assign(shell, edS)
let mainEl
if (v === 'seo') {
const unset = x === 'unset'
const rows = [['Address (slug)', 'comment-nous-chiffrons-une-renovation', 'Unique within Journal and French. Changing the title never changes it. Edit address changes it on purpose. Published: "Links to the old address will stop working. RenewCMS does not create redirects."'], ['Page title', FRT + ' | Journal', 'Empty uses the French title. Fallback: French title.'], ['Description', 'Empty', 'No fallback: the English site default isn\'t used for French, and the tag is left out.'], ['Image alternative text', 'Un plan de salle de bain avec les mesures', 'Written per language.']]
mainEl = Page('Search and sharing · French', 'Applies only to the French version. Page language: fr.', null, unset ? Alert('info', 'info', 'Public URL not configured', 'Add a public URL pattern for Journal to show page and canonical addresses. API links by article ID still work.', Btn('Open site settings', 'secondary', { sm: true })) : Card(D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('Canonical', 'neutral', 'link'), S(mono + ';' + HL, 'https://journal.example.com/fr/comment-nous-chiffrons-une-renovation')), S(MU + ';font-size:12px', 'Built from the site\'s public URL pattern https://journal.example.com/{lang}/{slug}. Each language points to itself unless you set a canonical override.')), !unset && Card(S('font-weight:600;' + HL, 'Alternate languages'), S(TO, 'Published versions in enabled languages only.'), D('display:flex;gap:10px;align-items:center', S(mono, 'EN'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + TO, 'https://journal.example.com/en/how-we-price…'), Bd('Published', 'success', 'circle-check')), S(MU + ';font-size:12px', 'Drafts and the disabled Spanish language are left out.')), !unset && Card(S('font-weight:600;' + HL, 'Social metadata locale'), D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('og:locale', 'neutral', 'share-2'), x === 'noSocial' ? S(MU, 'Not set') : S(mono + ';' + HL, 'fr_FR')), S(MU + ';font-size:12px;line-height:18px;text-wrap:pretty', x === 'noSocial' ? 'French has no region in its code and no social locale is set for it, so og:locale and og:locale:alternate are left out. An owner can set one in Journal languages.' : 'Set by an owner for French in Journal languages. Without it the tag would be left out: a region is never guessed from the language.')), ...rows.map(r => Fd(r[0], r[1], { help: r[2], val: r[1] !== 'Empty', mono: r[0].startsWith('Address') })))
} else mainEl = Page('Integration · Languages', 'Proposed. Not available in the current API.', null, Alert('warning', 'triangle-alert', 'Proposed capability', 'These requests don\'t exist yet. Existing article-ID requests are unchanged.'),
Card(S('font-weight:600;' + HL, 'Exact lookup: site + language + slug'), S(mono + ';' + HL, 'GET /v1/public/site/journal/lang/fr/article/{slug}'), S(TO, 'Matches the published French version with that slug, or returns 404 article_not_found. It never falls back, because a slug belongs to one language.'), S(mono + ';' + MU, '{ "story_id": "st_01", "lang": "fr", "slug": "…", "alternates": [{ "lang": "en", "slug": "…" }] }')),
Card(S('font-weight:600;' + HL, 'Resolve a story: story_id + language'), S(mono + ';' + HL, 'GET /v1/public/site/journal/story/{story_id}?lang=fr&fallback=default'), S(TO, 'Use the story_id from any response or from alternates[]. French published: 200, "lang": "fr". French missing or unpublished and fallback=default: 200 with the published default, "lang": "en", "requested_lang": "fr", "fallback": true. Default also missing or unpublished: 404 language_not_available. Without fallback=default a missing French is 404 language_not_available. No other language is tried, and drafts are never returned.')),
Card(S('font-weight:600;' + HL, 'Existing, unchanged'), S(mono + ';' + HL, 'GET /v1/article/{id}'), S(TO, 'Article-ID endpoints and links keep working. Each id is one language version.')))
main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', mainEl)
} else {
Object.assign(shell, edS)
const rtl = x === 'rtl', en = x === 'navFail'
let L = rtl ? AR : en ? EN : FR
const o = { cap: CAPS.full, pub: en ? 'pub' : 'draft', seoNote: false }
let extra = null
const popLeft = m ? 0 : 292
const pop = (rows, q = {}) => ov.push(LangPop(rows, { left: popLeft, top: 188, ...q }))
if (v === 'ed') {
o.seoNote = !m && !['menu', 'many', 'two'].includes(x)
if (x === 'menu') { o.open = 1; pop(MENUS.all) } else if (x === 'two') { o.open = 1; pop(MENUS.two) } else if (x === 'many') { o.open = 1; pop(MENUS.many, { find: 1, max: 520 }) }
else if (x === 'ro') o.cap = CAPS.ro
else if (x === 'roMenu') { o.cap = CAPS.ro; o.open = 1; pop(MENUS.ro) }
else if (x === 'p2') o.cap = CAPS.p2
else if (x === 'p3') o.cap = CAPS.p3
else if (x === 'p4') { o.open = 1; pop(MENUS.p4) }
else if (x === 'p5') { o.open = 1; pop(MENUS.p5) }
else if (x === 'p6') { o.cap = CAPS.p6; ov.push(D('position:absolute;z-index:40;' + (m ? 'left:0;right:0;bottom:0' : 'right:24px;top:188px;width:300px') + ';background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);display:flex;flex-direction:column', A('div', { role: 'menu', 'aria-label': 'More actions' }, 'display:flex;flex-direction:column', A('div', { role: 'menuitem' }, 'padding:10px 12px;min-height:' + (m ? 48 : 36) + 'px;display:flex;align-items:center;color:var(--ui-error)', 'Delete French version'), A('div', { role: 'menuitem', 'aria-disabled': 'true' }, 'padding:10px 12px;display:flex;flex-direction:column;gap:2px;opacity:.6', 'Delete story', S(MU + ';font-size:12px', 'You can\'t delete this story.')), A('div', { role: 'menuitem', 'aria-disabled': 'true' }, 'padding:10px 12px;display:flex;flex-direction:column;gap:2px;opacity:.6', 'Move story', S(MU + ';font-size:12px', 'Needs edit permission on every version.'))))) }
else if (x === 'unsaved') { o.dirty = 1; ov.push(Dlg({ title: 'Leave without saving?', desc: 'Your changes to the French version aren\'t saved.' }, [], [Btn('Keep editing'), Btn('Discard and open English', 'danger'), Btn('Save and open English', 'primary')])) }
else if (x === 'saving') { o.dirty = 1; o.saving = 1 }
else if (x === 'saved') ov.push(Toast('success', 'circle-check', 'French saved', 'English wasn\'t changed.'))
else if (x === 'dirtyPublish') o.dirty = 1
else if (x === 'saveFail') { o.dirty = 1; o.saveFail = 1 }
else if (x === 'onlineDirty') { o.dirty = 1; o.pub = 'pub' }
else if (['invalid', 'titleSaving', 'failed', 'focus', 'dirty'].includes(x)) { o.titleState = { titleSaving: 'saving' }[x] || x; o.dirty = x !== 'focus'; if (x === 'invalid') { o.titleVal = ''; o.saveBlock = 1 } if (x === 'titleSaving') o.saving = 1; if (x === 'failed') o.saveFail = 1 }
else if (x === 'loading') o.loading = 1
else if (x === 'gone') { main = Page('This version isn\'t available', 'It may have been deleted, or you may not have access.', null, Btn('Back to Articles', 'secondary')) }
else if (x === 'navFail') ov.push(Toast('error', 'circle-x', 'Couldn\'t open the French version', 'You\'re still editing English. Try again.'))
} else if (v === 'src') {
o.src = x !== 'closed' && x !== 'mobileClosed'; o.narrow = x === 'narrow'; o.long = x === 'long'; o.srcMob = x === 'mobileSrc' || x === 'unavail' && m; o.gone = x === 'unavail'
if (x === 'reopen') ov.push(Toast('info', 'info', 'Reference: English', 'Restored. Your place in both panes is kept.'))
if (x === 'changeRef') ov.push(D('position:absolute;z-index:40;' + (m ? 'left:0;right:0;bottom:0' : 'left:200px;top:210px;width:340px') + ';background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);display:flex;flex-direction:column', A('div', { role: 'menu', 'aria-label': 'Reference version' }, 'display:flex;flex-direction:column', LRow({ n: 'English', c: 'EN', st: 'pub', cur: 1 }), LRow({ n: 'German', c: 'DE', st: 'pub' })), D('padding:10px 12px;border-top:1px solid var(--ui-border);' + MU + ';font-size:12px;line-height:18px', 'Only changes what you compare against. The tracked review source stays English. Use Change tracked source in the review banner to change that.')))
if (x === 'changeRef') { o.src = true }
} else if (v === 'fr') {
o.fresh = 'Needs review'
const buttons = [Btn('Show source reference', 'secondary', { sm: true }), CAPS.full.rv && Btn('Mark as reviewed', 'primary', { sm: true }), Btn('Change tracked source', 'ghost', { sm: true })].filter(Boolean)
const ban = (tone, ic, t, d, act) => Alert(tone, ic, t, d, act)
if (x === 'gone') { o.fresh = 'No source'; extra = ban('neutral', 'info', 'Source deleted', 'The tracked English version was deleted. Review history is kept. Choose a new source to track changes again.', Btn('Choose a new source', 'secondary', { sm: true })) }
else if (x === 'inaccessible') { o.fresh = 'Source unavailable'; extra = ban('warning', 'lock', 'The source isn\'t available to you right now', 'It may be restricted. Review history is kept. Mark as reviewed is off until you can read it again.', Btn('Choose a new source', 'ghost', { sm: true })) }
else { if (x === 'pub') o.pub = 'pub'; extra = ban('warning', 'triangle-alert', 'English changed after French was reviewed', 'French was reviewed against English revision 7 (12 Oct 2026). English is at revision 8 (14 Oct 2026). Changed fields: Title, Body.' + (x === 'pub' ? ' French is still published. Reviewing doesn\'t change publication.' : ''), buttons) }
ov = ov.concat(frOv() || [])
} else if (v === 'pd') { o.pub = x === 'unpublish' || x === 'done' ? 'pub' : 'draft'; if (x === 'saveAndPublish' || x === 'saveFailPublish') o.dirty = 1; if (x === 'saveFailPublish') o.saveFail = 1; ov = ov.concat(pdOv() || []) }
else if (v === 'at') { o.pub = 'pub'; o.cap = CAPS.full; L = LS(x === 'dup' || x === 'dupHidden' ? S1 : S5); ov = ov.concat(atOv()) }
else if (v === 'pm') { if (x === 'conflict') { o.dirty = 1 } ; ov = ov.concat(pmOv() || []) }
if (!main) main = o.loading ? D('flex:1;padding:24px;display:flex;flex-direction:column;gap:14px', Sk(6)) : Editor(L, o, extra)
}
return { srcPane: null, edBanner: null, shell, mob: m, main, ov: ov.length ? D('position:absolute;inset:0;pointer-events:none', ...ov.map(e => D('pointer-events:none', e))) : null }
}
}
</script>
</body>
</html>