chore: redesign article localization before implementation
This commit is contained in:
1 parent
99a74dc3a2
commit
8c4a1825ab
31 files changed
+3365
-882
No files matched your search
@@ -8,7 +8,7 @@
|
||||
</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>
|
||||
<dc-import name="LocalizationShell" s="{{ shell }}" mobile="{{ mob }}" slot-body="{{ main }}" slot-overlays="{{ ov }}" src-pane="{{ srcPane }}" ed-banner="{{ edBanner }}" ed-body-el="{{ edBodyEl }}" ed-header="{{ edHeader }}" ed-mob-bar="{{ edMobBar }}" 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":"boolean","default":false,"tsType":"boolean"}}">
|
||||
class Component extends DCLogic {
|
||||
@@ -45,10 +45,11 @@ class Component extends DCLogic {
|
||||
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 Page = (title, sub, act, ...c) => D('flex:1;min-height:0;overflow:auto;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('flex-shrink:0;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 srcPane = null, edBanner = null, edBodyEl = null, edHeader = null, edMobBar = null
|
||||
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]
|
||||
@@ -56,7 +57,7 @@ class Component extends DCLogic {
|
||||
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 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:260px;width:max-content', Ic('languages'), S('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)),
|
||||
@@ -64,7 +65,7 @@ class Component extends DCLogic {
|
||||
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)),
|
||||
A('div', { role: 'menu', 'aria-label': 'Versions of this article' }, '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 }],
|
||||
@@ -72,270 +73,412 @@ class Component extends DCLogic {
|
||||
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' }]
|
||||
p5: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }],
|
||||
noAdd: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }],
|
||||
withAdd: [{ 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', add: 1 }]
|
||||
}
|
||||
// ---- 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 nr = !!s.narrow && !m
|
||||
const J = FX.SITES.journal, LNm = FX.LANG_NAMES
|
||||
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 }, '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)') + ';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 LONGT = 'Comment nous chiffrons une rénovation de salle de bain, poste par poste, avec les coûts cachés derrière les carreaux, sous la baignoire et autour des canalisations anciennes, expliqués avant la commande du premier carreau et de la première vanne'
|
||||
const TitleIn = (L, o) => {
|
||||
const st = o.tst || 'pristine', cap = o.cap, lab = 'Title (' + L.n + ')'
|
||||
if (!cap.e) return D('display:flex;flex-direction:column;gap:2px', S('font-size:12px;' + MU, lab), D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 44 : 36) + 'px', A('div', { dir: L.dir, lang: L.lang, role: 'textbox', 'aria-readonly': 'true', 'aria-label': lab, tabIndex: 0 }, 'flex:1;min-width:0;font:600 ' + (m ? 18 : 20) + 'px/28px \'IBM Plex Sans\',sans-serif;' + HL, L.t), Ic('lock', 14)))
|
||||
const val = { invalid: '', short: 'Sa', long: LONGT.slice(0, 203), edit: L.t + ' en 2026', unsaved: L.t + ' en 2026' }[st] ?? (o.titleVal ?? L.t)
|
||||
const err = ['invalid', 'short', 'long', 'failed'].includes(st), on = ['focus', 'edit'].includes(st), cnt = ['focus', 'edit', 'short', 'long'].includes(st)
|
||||
const msg = { invalid: 'Enter a title', short: 'Use at least 3 characters', long: 'Use 200 characters or fewer' }[st]
|
||||
return D('display:flex;flex-direction:column;gap:4px', S('font-size:12px;' + MU, lab),
|
||||
D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 44 : 40) + 'px;padding:2px 10px;border:1px solid ' + (err ? 'var(--ui-error)' : on ? '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': lab, 'aria-invalid': err || undefined, 'aria-describedby': 'title-help', tabIndex: 0 }, 'flex:1;min-width:0;font:600 ' + (m ? 18 : 20) + 'px/28px \'IBM Plex Sans\',sans-serif;' + (st === 'invalid' ? MU : HL) + ';text-wrap:pretty;' + (m ? '' : 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap'), st === 'invalid' ? 'Title' : val),
|
||||
cnt && S(mono + ';flex-shrink:0;color:' + (err ? 'var(--ui-error)' : 'var(--ui-text-muted)'), val.length + ' / 200'), st === 'saving' ? Ic('loader-circle', 14) : st === 'failed' ? Ic('circle-x', 14) : Ic('pencil', 14)),
|
||||
A('div', { id: 'title-help', role: err ? 'alert' : undefined }, 'display:flex;gap:8px;align-items:center;flex-wrap:wrap;font-size:12px;line-height:18px;' + (err ? 'color:var(--ui-error)' : MU),
|
||||
err && Ic('circle-x', 14), msg || (st === 'failed' ? 'Couldn\'t save the title. Your text is kept.' : st === 'saving' ? 'Saving title…' : (on || st === 'unsaved') ? 'Changing the title doesn\'t change the address.' : null), st === 'failed' && Btn('Try again', 'secondary', { sm: true })))
|
||||
}
|
||||
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 Acts = (L, o) => {
|
||||
const cap = o.cap, on = o.pub === 'pub', d = !!o.dirty || !!o.saveFail, a = { prim: null, sec: [], un: null }
|
||||
if (o.pubFail) a.prim = { l: 'Retry publish', ic: 'rotate-cw' }
|
||||
else if (o.saving) a.prim = { l: 'Saving…', busy: 1, dis: 1 }
|
||||
else if (cap.e && d && cap.p && !on && !o.saveFail) a.prim = { l: 'Save and publish ' + L.n, dis: !!o.saveBlock }
|
||||
else if (cap.e && d) a.prim = { l: o.saveFail ? 'Try saving again' : 'Save', dis: !!o.saveBlock }
|
||||
else if (cap.p && !on && !o.pubDis) a.prim = { l: 'Publish ' + L.n }
|
||||
const plain = a.prim && (a.prim.busy || a.prim.l === 'Save' || a.prim.l === 'Try saving again')
|
||||
if (cap.e && !plain) a.sec.push({ l: 'Save', dis: !d })
|
||||
if (cap.p && on && !o.saving) a.un = { l: 'Unpublish ' + L.n }
|
||||
if (o.pubDis) a.prim = { l: 'Publish ' + L.n, dis: 1 }
|
||||
return a
|
||||
}
|
||||
const OvItems = (L, o, k = {}) => {
|
||||
const cap = o.cap, a = Acts(L, o), it = []
|
||||
if (m && !k.skipRef) it.push({ l: o.src ? 'Hide source reference' : 'Show source reference', ic: 'columns-2' })
|
||||
if (a.un && (m || nr)) it.push({ l: a.un.l, ic: 'eye-off' })
|
||||
if (cap.e) it.push({ l: 'Edit slug', ic: 'link', why: 'Editing the title never changes the slug.' })
|
||||
if (cap.e) it.push(cap.part ? { l: 'Move article', ic: 'arrow-right-left', dis: 1, why: 'You can\'t move this article.' } : { l: 'Move article', ic: 'arrow-right-left' })
|
||||
if (cap.d) it.push({ l: 'Delete ' + L.n + ' version', ic: 'trash-2', danger: 1 })
|
||||
if (cap.d) it.push(cap.part ? { l: 'Delete article', ic: 'trash-2', dis: 1, why: 'You can\'t delete this article.' } : { l: 'Delete article', ic: 'trash-2', danger: 1 })
|
||||
return it
|
||||
}
|
||||
const OvMenu = items => D('position:absolute;z-index:45;' + (m ? 'left:0;right:0;bottom:0;border-top:1px solid var(--ui-border)' : 'right:24px;top:' + (nr ? 150 : 140) + 'px;width:320px;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', S('width:36px;height:4px;background:var(--ui-border-accented)')),
|
||||
A('div', { role: 'menu', 'aria-label': 'More actions' }, 'display:flex;flex-direction:column;padding:4px 0', ...items.map(i => A('div', { role: 'menuitem', 'aria-disabled': i.dis || undefined, tabIndex: -1 }, 'flex-shrink:0;display:flex;align-items:center;gap:10px;min-height:' + (m ? 52 : 40) + 'px;padding:8px 12px;' + (i.dis ? 'opacity:.6;' : '') + (i.danger ? 'color:var(--ui-error)' : HL), Ic(i.ic, 16), D('display:flex;flex-direction:column;gap:2px', S('font-weight:500', i.l), i.why && S(MU + ';font-size:12px;font-weight:400;line-height:16px', i.why))))))
|
||||
const Hdr = (L, o) => {
|
||||
const cap = o.cap, a = Acts(L, o), items = OvItems(L, o, { skipRef: m && !a.prim && !a.sec.length && !a.un })
|
||||
const refL = o.src ? 'Hide source reference' : 'Show source reference'
|
||||
const pb = x2 => x2 && Btn(x2.l, 'primary', { busy: x2.busy, dis: x2.dis, ic: x2.ic })
|
||||
const status = o.saving ? Bd('Saving…', 'neutral', 'loader-circle') : o.saveFail ? Bd('Save failed', 'error', 'circle-x') : o.pubFail || o.saved ? Bd('Saved', 'success', 'check') : (o.dirty || o.tst === 'unsaved') ? Bd('Unsaved changes', 'neutral', 'circle-dot') : null
|
||||
const fresh = o.fresh && (nr ? A('span', { 'aria-label': o.fresh, title: o.fresh }, 'display:inline-flex;align-items:center;justify-content:center;width:24px;height:20px;flex-shrink:0;border:1px solid currentColor;background:var(--ui-bg);color:var(--ui-warning)', Ic('refresh-cw', 12)) : Fr(o.fresh))
|
||||
const left = D('flex:1 1 auto;min-width:0;display:flex;align-items:center;gap:8px;flex-wrap:wrap', LangBtn(L.n, { code: L.c, open: !!o.open }), Pub(o.pub), status, fresh, !cap.e && Bd('Read only', 'neutral', 'lock'))
|
||||
const right = !m && D('display:flex;align-items:center;gap:8px;flex-shrink:0;margin-left:auto', Btn(nr ? '' : refL, 'secondary', { ic: 'columns-2', al: refL }), a.un && !nr && Btn(a.un.l), ...a.sec.map(x2 => Btn(x2.l, 'secondary', { dis: x2.dis })), pb(a.prim), items.length ? Btn('', 'secondary', { ic: 'ellipsis', al: 'More actions' }) : null)
|
||||
return D('flex:1 1 100%;min-width:0;display:flex;flex-direction:column;gap:10px', D('display:flex;align-items:flex-start;gap:8px', !m && D('height:60px;display:flex;align-items:flex-end;' + MU, Ic('arrow-left', 16)), D('flex:1;min-width:0', TitleIn(L, o))), D('display:flex;align-items:center;gap:12px;flex-wrap:wrap', left, right))
|
||||
}
|
||||
const MBar = (L, o) => {
|
||||
if (!m) return null
|
||||
const a = Acts(L, o), items = OvItems(L, o, { skipRef: !a.prim && !a.sec.length && !a.un }), bs = []
|
||||
if (a.prim) bs.push(D('flex:1 1 0;min-width:0;display:flex', Btn(a.prim.l, 'primary', { w: '100%', busy: a.prim.busy, dis: a.prim.dis, ic: a.prim.ic })))
|
||||
a.sec.forEach(x2 => bs.push(Btn(x2.l, 'secondary', { dis: x2.dis })))
|
||||
if (!a.prim && !a.sec.length) bs.push(D('flex:1 1 0;display:flex', Btn(a.un ? a.un.l : (o.src ? 'Hide source reference' : 'Show source reference'), 'secondary', { w: '100%', ic: a.un ? 'eye-off' : 'columns-2' })))
|
||||
if (items.length) bs.push(Btn('', 'secondary', { ic: 'ellipsis', al: 'More actions' }))
|
||||
return D('flex-shrink:0;background:var(--ui-bg);border-top:1px solid var(--ui-border);padding:12px 16px;display:flex;gap:8px;align-items:center', ...bs)
|
||||
}
|
||||
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 TempRef = S('white-space:nowrap;display:inline-flex;align-items:center;gap:4px;height:20px;padding:0 6px;font-size:12px;font-weight:500;border:1px dashed var(--ui-border-accented);color:var(--ui-text-toned);background:var(--ui-bg)', Ic('book-open', 12), 'Temporary reference')
|
||||
const Tracked = S0n => Bd('Tracked source: ' + S0n, 'info', 'link')
|
||||
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.'),
|
||||
D('flex-shrink:0;display:flex;flex-direction:column;gap:8px;padding:8px 12px;border:1px dashed var(--ui-border-accented);background:var(--ui-bg)', D('display:flex;align-items:center;gap:8px;flex-wrap:wrap', TempRef, S('font-weight:600;' + HL, S0.n), S(mono + ';' + MU, S0.c), Pub('pub')), D('display:flex;align-items:center;gap:8px', S('white-space:nowrap;display:inline-flex;gap:4px;' + MU + ';font-size:12px', Ic('lock', 14), 'Read only'), D('flex:1'), Btn('Change reference', 'secondary', { sm: true }))),
|
||||
S('flex-shrink:0;' + MU + ';font-size:12px;line-height:18px', 'For comparing only. Review is tracked against English. Changing the reference doesn\'t change the tracked source.'),
|
||||
o.gone ? Alert('warning', 'triangle-alert', 'This reference isn\'t available', 'It was deleted or you can no longer read it. Your version isn\'t affected.', Btn('Choose another reference', 'secondary', { sm: true })) : [
|
||||
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))
|
||||
}
|
||||
Body(S0, { ro: true, label: 'Reference', long: o.long })])
|
||||
// ---- 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 SOC = (site, code) => { const q = (FX.SOCIAL[site] || {})[code] || ['none']; return q[0] === 'derived' ? 'Derived: ' + q[1] : q[0] === 'set' ? 'Set: ' + q[1] : 'Not set' }
|
||||
const SiteRows = (siteKey, ro) => { const si = FX.SITES[siteKey], all = si.langs.map(k => [k, 1]).concat(si.disabled.map(k => [k, 0])); return all.map(([k, en]) => { const c = FX.count(k, si.stories), def = k === si.def, soc = SOC(siteKey, k)
|
||||
return D(ROW + ';flex-wrap:wrap;opacity:' + (en ? 1 : .85), D('flex:1 1 160px;min-width:0;display:flex;flex-direction:column', S('font-weight:500;' + HL + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', LNm[k] || k), S(mono + ';' + MU, k)),
|
||||
def ? Bd('Default', 'info', 'star') : !en ? Bd('Disabled', 'neutral', 'eye-off') : null, !m && S(MU + ';width:170px', FX.pl(c[0], 'version') + ' · ' + c[1] + ' published'),
|
||||
D('display:flex;align-items:center;gap:6px;' + (m ? 'flex:1 1 100%' : 'width:270px;white-space:nowrap'), S('font-size:12px;white-space:nowrap;' + MU, 'Social locale'), S(mono + ';' + (soc === 'Not set' ? MU : HL), soc), !ro && Btn('Edit', 'ghost', { sm: true, al: 'Edit social locale for ' + (LNm[k] || k) })),
|
||||
!ro && !en ? Btn('Re-enable', 'secondary', { sm: true }) : null, !ro && Btn('', 'ghost', { ic: 'ellipsis', sm: true, al: 'Actions for ' + (LNm[k] || k) })) }) }
|
||||
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.'
|
||||
const ro = x === 'readonly', single = x === 'single', skey = single ? 'archive' : x === 'social' || x === 'socialEdit' || x === 'socialInvalid' ? 'atelier' : 'journal', site = FX.SITES[skey].name, 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 })))
|
||||
const banner = 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.') : x === 'defaultFail' ? Alert('error', 'circle-x', 'Couldn\'t change the default language', 'Nothing changed. English is still the default. Try again.', Btn('Try again', 'secondary', { sm: true })) : x === 'removeFail' ? Alert('error', 'circle-x', 'Couldn\'t remove Italian', 'Nothing changed. Italian is still listed. Try again.', Btn('Try again', 'secondary', { sm: true })) : x === 'stale' ? Alert('error', 'circle-x', 'Languages changed. Reload.', 'Another owner changed the languages for Journal while this page was open. Reload to see the current list. Your change wasn\'t applied.', Btn('Reload', 'secondary', { sm: true })) : null
|
||||
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.'))
|
||||
ro && Alert('neutral', 'lock', 'Only owners can change languages', 'You can see which languages are enabled, their social locale and how many versions each has. Ask an owner to change them.'), banner,
|
||||
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'), S(MU + ';font-size:12px', 'Social locale'), S(mono + ';' + HL, SOC('archive', 'pt-BR') === 'Not set' ? 'Derived: 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'), ...SiteRows(skey, 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 locale: Derived: pt_BR, taken from the language code.'),
|
||||
!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. Social locale: Derived comes from the language code, Set is chosen by an owner, Not set leaves og:locale out.'))
|
||||
}
|
||||
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')])
|
||||
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 locale', 'Derived: 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.' })]
|
||||
const soc = (val, err, name, code) => Dlg({ title: 'Social locale for ' + name, desc: 'Used for og:locale on ' + name + ' pages. Leave it empty to omit the tag.' }, [Fd('Social locale', val, { mono: true, focus: !err, err, help: err ? null : 'Format: language_REGION, for example ' + code + '. Letters only, one underscore.' }), code === 'pt_BR' && Alert('info', 'info', 'Derived from the language code', 'pt-BR gives pt_BR. Setting a value replaces the derived one.')], [Btn('Cancel'), Btn('Save social locale', 'primary', { dis: !!err })])
|
||||
const dlgs = {
|
||||
add: () => 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')]),
|
||||
saveFail: () => Dlg({ title: 'Add a language', desc: 'Choose a language for Journal.' }, addBody(true), [Btn('Cancel'), Btn('Try again', 'primary')]),
|
||||
adddup: () => 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 })]),
|
||||
broader: () => Dlg({ title: 'Add a language', desc: 'Choose a language for Journal.' }, [Fd('Language', 'Portuguese (Portugal)', { sel: true }), Alert('warning', 'triangle-alert', 'Portuguese (Portugal) isn\'t available', 'Only the broader language Portuguese (pt) is supported. Pages and the API will say pt, not pt-PT. Articles written in this language can\'t be told apart from other Portuguese.'), D('display:flex;gap:10px;align-items:center;min-height:' + (m ? 44 : 24) + 'px', S('width:16px;height:16px;border:1px solid var(--ui-primary);background:var(--ui-primary);color:var(--ui-bg);display:flex;align-items:center;justify-content:center', Ic('check', 12)), S('font-weight:500;' + HL, 'Add Portuguese (pt) instead'))], [Btn('Cancel'), Btn('Add Portuguese', 'primary')]),
|
||||
default: () => Dlg({ title: 'Make French the default language?', desc: 'New articles start in the default language, and it is the first choice for each article\'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 not found where one doesn\'t. Requests that name a language aren\'t affected.')], [Btn('Cancel'), Btn('Make French default', 'primary')]),
|
||||
defaultFail: () => Dlg({ title: 'Make French the default language?', desc: 'New articles start in the default language.' }, [Alert('error', 'circle-x', 'Couldn\'t change the default language', 'Nothing changed. Try again.')], [Btn('Cancel'), Btn('Try again', 'primary')]),
|
||||
disable: () => 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')]),
|
||||
stale: () => Dlg({ title: 'Disable German?', desc: 'German is used by 4 versions.' }, [Alert('error', 'circle-x', 'Languages changed. Reload.', 'Your change wasn\'t applied.')], [Btn('Cancel'), Btn('Reload', 'primary')]),
|
||||
reenable: () => 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')]),
|
||||
reenableFail: () => Dlg({ title: 'Re-enable Spanish?', desc: 'Writers can create Spanish versions again.' }, [Alert('error', 'circle-x', 'Couldn\'t re-enable Spanish', 'Nothing changed. Spanish is still disabled. Try again.')], [Btn('Cancel'), Btn('Try again', 'primary')]),
|
||||
remove: () => Dlg({ title: 'Remove Italian?', desc: 'No article uses Italian. You can add it again later.' }, [], [Btn('Cancel'), Btn('Remove Italian', 'danger')]),
|
||||
removeFail: () => Dlg({ title: 'Remove Italian?', desc: 'No article uses Italian.' }, [Alert('error', 'circle-x', 'Couldn\'t remove Italian', 'Nothing changed. Italian is still listed. Try again.')], [Btn('Cancel'), Btn('Try again', 'danger')]),
|
||||
removeBlocked: () => 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')]),
|
||||
socialEdit: () => soc('fr_FR', null, 'French', 'fr_FR'),
|
||||
socialInvalid: () => soc('fr-FR', 'Use letters and one underscore, for example fr_FR.', 'French', 'fr_FR')
|
||||
}
|
||||
if (x === 'saved') return [Toast('success', 'circle-check', 'French added', 'Writers can now create French versions.')]
|
||||
return null
|
||||
if (x === 'reenableDone') return [Toast('success', 'circle-check', 'Spanish re-enabled', 'Writers can create Spanish versions again. Drafts can be published.')]
|
||||
return dlgs[x] ? dlgs[x]() : null
|
||||
}
|
||||
// ---- public URL pattern
|
||||
const puPage = () => {
|
||||
const bad = { unknown: ['https://journal.example.com/{language}/{slug}', 'Unknown placeholder {language}. Use {lang} and {slug}.'], scheme: ['ftp://journal.example.com/{lang}/{slug}', 'Start with https:// or http://.'], encoded: ['https://journal.example.com/my pages/{lang}/{slug}', 'Remove spaces and characters that need encoding. Use letters, digits, - and _.'], noSlug: ['https://journal.example.com/{lang}', 'Add {slug} so each article has its own address.'] }[x]
|
||||
const ro = x === 'viewOnly', unset = x === 'unset', val = bad ? bad[0] : unset ? '' : x === 'editing' ? 'https://journal.example.com/{lang}/articles/{slug}' : 'https://journal.example.com/{lang}/{slug}'
|
||||
const prev = bad ? null : unset ? null : x === 'editing' ? 'https://journal.example.com/fr/articles/comment-nous-chiffrons-une-renovation' : 'https://journal.example.com/fr/comment-nous-chiffrons-une-renovation'
|
||||
return Page('Public URL', 'Where Journal articles appear on your website. Used for canonical and alternate links.', null,
|
||||
ro && Alert('neutral', 'lock', 'Only owners can change this', 'You can read the pattern. Ask an owner to change it.'),
|
||||
unset && Alert('info', 'info', 'Public URL not configured', 'Page, canonical and alternate addresses stay hidden until you add a pattern. Links by article ID still work.'),
|
||||
Card(Fd('Public URL pattern', val || 'https://example.com/{lang}/{slug}', { mono: true, val: !!val, ro, focus: !!bad || x === 'editing', err: bad && bad[1], help: bad ? null : 'Placeholders: {lang} is the language code, such as fr or pt-BR. {slug} is the article address in that language.' }),
|
||||
prev && D('display:flex;flex-direction:column;gap:4px', S('font-size:12px;' + MU, 'Preview with "Comment nous chiffrons une rénovation de salle de bain" in French'), S(mono + ';' + HL + ';overflow-wrap:anywhere', prev)),
|
||||
!ro && D('display:flex;gap:8px', Btn('Save', 'primary', { dis: !!bad || unset }), Btn('Cancel'))))
|
||||
}
|
||||
// ---- 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'
|
||||
const done = x === 'done', busy = x === 'applying', fail = x === 'fail', prog = x === 'inProgress', nol = x === 'noLocale', chosen = x !== 'ready' && !nol, rowsSrc = nol ? SUV.filter(r => !r.name) : SUV, N = rowsSrc.reduce((a, r) => a + r.n, 0)
|
||||
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 })) :
|
||||
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 })) :
|
||||
prog ? Alert('info', 'loader-circle', 'Setup is already running', 'Another owner started it. Languages appear here when it finishes. Articles created meanwhile get each site\'s default language. Starting it again is safe: sites that already have a language are skipped.', Btn('Check 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.')))
|
||||
nol ? Alert('error', 'circle-x', 'Choose a language for Notes', 'Notes has no site locale, so there is nothing to start from. Choose the language its articles are written in.') :
|
||||
Alert('info', 'info', 'This labels articles. It doesn\'t translate them.', 'Each existing article becomes the only version of its own article group. Each site\'s language starts 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)', ...rowsSrc.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 && !prog, val: !!(r.name || chosen), err: nol && !r.name ? 'Required before you can apply.' : null, ro: prog, help: done || nol ? 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 ' + N + ' articles', 'primary', { busy, dis: busy || prog || x === 'ready' || nol }), S(MU + ';font-size:12px', 'Counts are articles in ' + rowsSrc.length + (rowsSrc.length === 1 ? ' site.' : ' 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' }]
|
||||
// ---- article list
|
||||
const LN = LNm
|
||||
const Pager = (V) => 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 ' + V.from + '–' + V.to + ' of ' + V.unit),
|
||||
D('display:flex;align-items:center;gap:8px;' + MU + ';font-size:12px', 'Rows', D('', Fd('', String(V.size), { sel: true }))),
|
||||
D('display:flex;align-items:center;gap:6px', Btn('', 'secondary', { ic: 'chevron-left', al: 'Previous page', dis: V.page < 2, sm: !m }), S(HL + ';font-weight:500;padding:0 6px', 'Page ' + V.page + ' of ' + V.pages), Btn('', 'secondary', { ic: 'chevron-right', al: 'Next page', dis: V.page >= V.pages, sm: !m })))
|
||||
const Chip = l => D('white-space:nowrap;flex-shrink:0;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 ChipC = (c, k) => A('span', { 'aria-label': (LN[c.k] || c.k) + ': ' + (c.st === 'pub' ? 'published' : c.st === 'draft' ? 'draft' : 'no version') + (c.nr ? ', needs review' : '') + (c.off ? ', language disabled' : '') }, 'white-space:nowrap;display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 6px;font:500 12px \'IBM Plex Mono\',monospace;' + (c.st ? 'border:1px solid var(--ui-border-accented);' + HL : 'border:1px dashed var(--ui-border-accented);' + MU) + (c.off ? ';opacity:.75' : ''), c.k.toUpperCase(), c.st === 'pub' ? Ic('circle-check', 12) : c.st === 'draft' ? Ic('pencil', 12) : Ic('plus', 12), c.nr && Ic('refresh-cw', 12))
|
||||
const Cov = (cs) => { const Lc = m || nr ? 4 : 5, vis = cs.slice(0, Lc), more = cs.slice(Lc); return D('display:flex;gap:6px;flex-wrap:' + (m ? 'wrap' : 'nowrap'), ...vis.map(ChipC), more.length ? A('span', { role: 'button', 'aria-label': more.length + ' more languages: ' + more.map(c => LN[c.k] || c.k).join(', ') }, 'display:inline-flex;align-items:center;height:22px;padding:0 6px;font:500 12px \'IBM Plex Mono\',monospace;background:var(--ui-bg-elevated);' + HL, '+' + more.length) : null) }
|
||||
const covW = m ? '' : (nr ? 'width:330px;' : 'width:430px;')
|
||||
const RowMenuBtn = () => Btn('', 'ghost', { ic: 'ellipsis', sm: true, al: 'Row actions' })
|
||||
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 C = { page2: { page: 2 }, fr: { lang: 'fr' }, frMissing: { lang: 'fr', tab: 'missing' }, emptyMissing: { site: 'press', lang: 'fr', tab: 'missing' }, noResults: { q: 'zinc' }, frLoading: { lang: 'fr', load: 1 }, listFail: { fail: 1 }, limited: { lim: 1 }, filters: { lang: 'fr', status: 'draft' }, tabsScroll: { lang: 'fr', tab: 'missing', shift: 1 }, long: { site: 'atelier', status: 'draft', seo: 1 }, author: { author: 'Marta Alves' }, review: { tab: 'review' }, frReview: { lang: 'fr', tab: 'review' }, langFilter: { menu: 1 }, rowMenu: { row: 1 }, ...(x === 'home' ? {} : {}) }[x] || {}
|
||||
const site = FX.SITES[C.site || 'journal'], lim = !!C.lim
|
||||
const V = FX.view({ site, lang: C.lang, tab: C.tab, status: C.status, author: C.author, seo: C.seo, q: C.q, page: C.page, read: lim ? ['en'] : null, create: lim ? [] : null, scope: lim ? (s0 => ['Camille Roche', 'Luis Ferrer'].includes(s0.by)) : null })
|
||||
const tabs = (C.load || C.fail) ? V.tabs.map(q => [q[0], '–', q[2]]) : V.tabs, tabOn = Math.max(0, V.tabs.findIndex(q => q[2] === V.tab))
|
||||
const act = [C.status && 'Status: Draft', C.author && 'Author: ' + C.author, C.seo && 'SEO: Needs attention'].filter(Boolean)
|
||||
const chips = act.length ? ['Site: ' + site.name].concat(C.lang ? ['Language: ' + LN[C.lang]] : [], act) : []
|
||||
const nF = chips.length
|
||||
const filt = () => D('display:flex;gap:8px;flex-wrap:wrap;align-items:center', D('flex:1;min-width:' + (m ? '0' : '200px'), Fd('', C.q ? C.q : C.lang ? 'Search ' + LN[C.lang] + ' titles' : 'Search titles in every language', { val: !!C.q })),
|
||||
m ? Btn('Filters' + (nF ? ' · ' + nF : ''), 'secondary', { ic: 'sliders-horizontal', al: nF ? 'Filters, ' + nF + ' active' : 'Filters' }) : [Fd('', site.name, { sel: true }), Fd('', C.lang ? LN[C.lang] : 'All languages', { sel: true }), Fd('', C.status ? 'Draft' : 'Any status', { sel: true }), Fd('', C.author || 'Any author', { sel: true, focus: x === 'author' }), Fd('', C.seo ? 'Needs attention' : 'Any SEO state', { sel: true })].map(e => D('flex:0 0 auto;width:' + (nr ? 128 : 150) + 'px', e)))
|
||||
const chipRow = chips.length ? D('display:flex;gap:6px;flex-wrap:wrap;align-items:center', ...chips.map(Chip), Btn('Clear all', 'ghost', { sm: !m })) : null
|
||||
const shift = C.shift ? -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)
|
||||
A('div', { role: 'tablist', 'aria-label': 'Article views' }, 'flex:1;min-width:0;overflow:hidden;display:flex;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 card = 'background:var(--ui-bg);border:1px solid var(--ui-border)', hd = !m && D('display:flex;gap:12px;padding:8px 16px;background:var(--ui-bg-elevated);' + MU + ';font-weight:500', S('flex:1', 'Article'), S(covW + 'flex-shrink:0', 'Languages'), S('width:110px', 'Author'), S('width:80px', 'Updated'), S('width:28px'))
|
||||
const creat = lim ? [] : site.langs
|
||||
const StoryRow = (r) => { const s0 = r.s, cs = FX.chips(s0, site, creat), t = FX.tt(s0, r.rep)
|
||||
return D(ROW + ';' + (m ? 'flex-wrap:wrap;gap:6px 12px' : ''), D('flex:1 1 ' + (m ? '70%' : '0') + ';min-width:0;display:flex;flex-direction:column;gap:2px', A('span', { lang: r.rep }, HL + ';font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', t), r.rep !== site.def && S(MU + ';font-size:12px', 'Shown in ' + (LN[r.rep] || r.rep) + (site.disabled.includes(r.rep) ? ' · language disabled' : ''))), m && D('order:2;flex:1 1 100%', Cov(cs)), !m && D(covW + 'flex-shrink:0', Cov(cs)), m ? S('order:3;' + MU + ';font-size:12px;flex:1', s0.by + ' · ' + s0.upd) : [S(MU + ';width:110px', s0.by), S(MU + ';width:80px', s0.upd)], D(m ? 'order:1' : '', RowMenuBtn())) }
|
||||
const VerRow = (r) => { const q = r.s.v[C.lang]; return D(ROW + ';' + (m ? 'flex-wrap:wrap' : ''), D('flex:1 1 ' + (m ? '70%' : '0') + ';min-width:0;display:flex;flex-direction:column;gap:2px', A('span', { lang: C.lang }, HL + ';font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', FX.tt(r.s, C.lang)), m && S(MU + ';font-size:12px', r.s.by + ' · ' + r.s.upd)), D('display:flex;gap:6px', Pub(q[0]), q[1] ? Fr('Needs review') : null), !m && [S(MU + ';width:110px;margin-left:12px', r.s.by), S(MU + ';width:80px', r.s.upd)], RowMenuBtn()) }
|
||||
const MissRow = (r) => D(ROW + ';' + (m ? 'flex-wrap:wrap' : ''), D('flex:1 1 0;min-width:0;display:flex;flex-direction:column;gap:2px', S(HL + ';font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', FX.tt(r.s, r.rep)), S(MU + ';font-size:12px', 'Opens ' + LN[r.rep] + ' · ' + (r.s.v[r.rep][0] === 'pub' ? 'published' : 'draft') + (site.disabled.includes(r.rep) ? ' · language disabled' : ''))), Btn(m ? '' : 'Add ' + LN[C.lang] + ' translation', 'secondary', { sm: true, ic: 'plus', al: 'Add ' + LN[C.lang] + ' translation to ' + FX.tt(r.s, r.rep) }), RowMenuBtn())
|
||||
let body
|
||||
if (C.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 (C.load) body = Sk(5)
|
||||
else if (!V.total && C.lang && V.tab === 'missing') 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 article you can see has a ' + LN[C.lang] + ' version'), S(TO + ';max-width:360px', 'Articles without a ' + LN[C.lang] + ' version will appear here.'))
|
||||
else if (!V.total) 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 articles match "' + (C.q || '') + '"'), S(TO, 'Search covers titles in every language you can read.'), Btn('Clear search', 'secondary', { sm: true }))
|
||||
else body = D(card, C.lang && V.tab === 'missing' ? null : (C.lang ? null : hd), ...V.rows.map(C.lang ? (V.tab === 'missing' ? MissRow : VerRow) : StoryRow))
|
||||
const unit = C.fail || C.load ? '' : V.unit
|
||||
return D('flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:' + (m ? 12 : 16) + 'px;padding:' + (m ? '16px' : '24px') + ';position:relative', D('display:flex;align-items:baseline;gap:12px', S('font:600 20px/28px \'IBM Plex Sans\',sans-serif;' + HL, 'Articles'), A('span', { 'aria-live': 'polite' }, 'flex:1;' + MU + ';font-weight:500', unit), 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 the versions you have permission to see. You can\'t create translations on this site.'), filt(), chipRow, tabRow, body, (V.total && !C.fail && !C.load) ? Pager(V) : 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)')),
|
||||
const sheet = (o) => 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%' })))))
|
||||
D('padding:16px;display:flex;flex-direction:column;gap:14px;overflow-y:auto;min-height:0', Fd('Site', 'Journal', { sel: true }), Fd('Language', o.lang || 'All languages', { sel: true }), Fd('Status', o.status || 'Any status', { sel: true }), Fd('Author', o.author || 'Any author', { sel: true, focus: !!o.author }), 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 ' + o.n + ' results', 'primary', { w: '100%' })))))
|
||||
const listOv = () => {
|
||||
const V1 = FX.view({ site: J, lang: 'fr', status: 'draft' }), V2 = FX.view({ site: J, author: 'Marta Alves' })
|
||||
if (x === 'filters' && m) return [sheet({ lang: 'French', status: 'Draft', n: V1.total })]
|
||||
if (x === 'author' && m) return [sheet({ author: 'Marta Alves', n: V2.total })]
|
||||
if (x === 'author') return [D('position:absolute;z-index:40;left:' + (nr ? 500 : 1018) + 'px;top:170px;width:240px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop)', A('div', { role: 'listbox', 'aria-label': 'Author' }, 'display:flex;flex-direction:column;padding:4px 0', ...[['Any author', J.stories.length]].concat(FX.AUTHORS).map((a, i) => A('div', { role: 'option', 'aria-selected': a[0] === 'Marta Alves' }, 'display:flex;align-items:center;gap:8px;min-height:36px;padding:6px 12px;' + (a[0] === 'Marta Alves' ? 'background:var(--rc-sand);color:var(--rc-on-sand);font-weight:600' : HL), S('width:16px', a[0] === 'Marta Alves' && Ic('check')), S('flex:1', a[0]), S(mono + ';' + (a[0] === 'Marta Alves' ? '' : MU), a[1])))))]
|
||||
const langRows = (grp) => (grp ? J.disabled : J.langs).map(k => D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + (grp ? MU : HL), S('width:16px'), S('flex:1', LN[k]), S(mono + ';' + MU, FX.count(k, J.stories)[0] + ' versions')))
|
||||
if (x === 'langFilter') return [D('position:absolute;z-index:40;' + (m ? 'left:0;right:0;bottom:0;border-top:1px solid var(--ui-border)' : 'left:' + (nr ? 330 : 792) + 'px;top:170px;width:280px;border:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);display:flex;flex-direction:column;max-height:70%', A('div', { role: 'listbox', 'aria-label': 'Language' }, 'display:flex;flex-direction:column;overflow-y:auto;padding:4px 0', D('padding:4px 12px;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;' + MU, 'Enabled'), D('display:flex;align-items:center;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + HL, S('width:16px'), 'All languages'), ...langRows(0), D('padding:8px 12px 4px;border-top:1px solid var(--ui-border);font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;' + MU, 'Disabled'), ...langRows(1), D('padding:6px 12px 8px;font-size:12px;' + MU, 'Disabled languages keep their versions. They can\'t get new ones.')))]
|
||||
if (x === 'rowMenu') return [OvMenu([{ l: 'Open English', ic: 'file-pen' }, { l: 'Open another language…', ic: 'languages' }, { l: 'Add translation…', ic: 'plus' }, { l: 'Unpublish English', ic: 'eye-off' }, { l: 'Copy API URL', ic: 'copy' }, { l: 'Delete English version', ic: 'trash-2', danger: 1 }])]
|
||||
return []
|
||||
}
|
||||
// ---- 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 }),
|
||||
return Dlg({ title: 'New article', desc: x === 'translation' ? 'Create a translation of an article you can read.' : 'Give the article a title. You can write the rest in the editor.' }, [
|
||||
x === 'translation' ? [Fd('Article', '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' : x === 'single' ? 'Archive' : '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))]
|
||||
multi && D('display:flex;flex-direction:column;gap:8px', Radio('New article', 'Starts an article in this language.', true), Radio('Translation of an existing article', '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.' }),
|
||||
const empty = x === 'empty', long = x === 'long', dupCase = x === 'dup' || x === 'dupHidden', st = dupCase ? S1 : S5, ft = dupCase ? FRT : FRT5
|
||||
const dis = ['gone', 'unreadable', 'langOff', 'dup', 'dupHidden', 'saving'].includes(x)
|
||||
const f = [Fd('Language', 'French', { sel: true, ro: x === 'langOff', err: x === 'langOff' ? 'French was disabled on this site.' : null }), Fd('Source version', x === 'unreadable' ? 'Choose a version' : 'English · published', { sel: true, val: x !== 'unreadable' && x !== 'gone', 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')])
|
||||
const T = 'Add a translation'
|
||||
if (x === 'dup') return Dlg({ title: T }, [Alert('error', 'circle-x', 'French already exists for this article', '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: T }, [Alert('error', 'circle-x', 'French can\'t be created for this article', '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: T }, [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: T }, [Alert('warning', 'triangle-alert', 'The source was deleted', 'Choose another source you can read, or start empty. Your title is kept.'), ...f.slice(0, 1), f[1], f[2]], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
|
||||
if (x === 'unreadable') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'The source isn\'t available to you', 'You can no longer read the version you chose. Choose another source you can read, or start empty. Your title is kept.'), ...f.slice(0, 1), f[1], f[2]], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
|
||||
if (x === 'langOff') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'French was disabled', 'French was disabled on this site while you were filling this in. Ask an owner to re-enable it, or choose another language. Your entries are kept.'), ...f], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
|
||||
if (x === 'saving') return Dlg({ title: T }, f, [Btn('Cancel', 'secondary', { dis: true }), Btn('Creating…', 'primary', { busy: true })])
|
||||
return Dlg({ title: T, 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')]),
|
||||
publishDisabled: () => [],
|
||||
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')]),
|
||||
unpublishDirty: () => Dlg({ title: 'Unpublish French?', desc: 'The saved French version goes offline and becomes a draft. English stays online.' }, [Alert('neutral', 'info', 'Your edits stay in the editor', 'Your unsaved changes are kept here. Nothing is saved when you unpublish. Save them yourself when you are ready.')], [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')]),
|
||||
deleteStoryOk: () => Dlg({ title: 'Delete English, French, German and Arabic?', desc: 'This deletes the article and every version you can read. It can\'t be undone.' }, [], [Btn('Cancel'), Btn('Delete article', 'danger')]),
|
||||
deleteLast: () => Dlg({ title: 'Delete the only version?', desc: 'English is the only version of this article. Deleting it deletes the article.' }, [], [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')]),
|
||||
deleteStory: () => Dlg({ title: 'You can\'t delete this article', desc: 'Deleting an article 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
|
||||
// ---- freshness and review (release 2)
|
||||
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.')],
|
||||
review: () => Dlg({ title: 'Mark French as reviewed?', desc: 'This records that the French version matches the English source. It doesn\'t change publication.' }, [Alert('neutral', 'info', 'Changed fields: Title, Body', 'Authors and the cover image don\'t count.'), D('display:grid;grid-template-columns:auto 1fr;gap:6px 16px;font-size:13px', S(MU, 'Source'), S(HL, 'English version saved 14 Oct 2026, 09:12'), S(MU, 'Translation'), S(HL, 'French version saved 15 Oct 2026, 16:40'))], [Btn('Cancel'), Btn('Mark as reviewed', 'primary')]),
|
||||
changed: () => Dlg({ title: 'Review not recorded' }, [Alert('warning', 'triangle-alert', 'The English source changed while you were reviewing. Review again.', 'English was saved again at 14 Oct 2026, 11:20. Check the latest changes first. Nothing was marked.')], [Btn('Cancel'), Btn('Show source reference', 'secondary'), Btn('Review again', 'primary')]),
|
||||
changedTr: () => Dlg({ title: 'Review not recorded' }, [Alert('warning', 'triangle-alert', 'French was changed by someone else while you were reviewing. Review again.', 'Someone saved the French version at 15 Oct 2026, 16:52. Nothing was marked.')], [Btn('Cancel'), Btn('Reload French', 'primary')]),
|
||||
replace: () => Dlg({ title: 'Choose a tracked source', desc: 'Pick a version of this article you can read. French will show Needs review until you review it against the new source.' }, [Radio('English', 'Published', false), Radio('German', 'Published', true), Alert('error', 'circle-x', 'German can\'t be the source', 'German already 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 the English version saved 14 Oct 2026.')],
|
||||
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]?.())
|
||||
// ---- conflicts, moves, permissions
|
||||
const CfOpt = (t, d, b, k = 'secondary', top) => D((top ? '' : 'border-top:1px solid var(--ui-border);padding-top:14px;') + 'display:flex;' + (m ? 'flex-direction:column;gap:8px' : 'align-items:center;gap:16px'), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S('font-weight:600;' + HL, t), S(MU + ';font-size:13px;line-height:19px;text-wrap:pretty', d)), D((m ? '' : 'width:200px;flex-shrink:0;') + 'display:flex', Btn(b, k, { w: '100%' })))
|
||||
const Cf = (o) => Dlg({ title: o.title || 'Someone else saved this version', desc: o.desc || 'Someone else saved this version at ' + (o.t || '14:32') + '. Your changes aren\'t saved. They stay here until you choose.', w: 560, top: 60 }, [o.alert, CfOpt('Keep my changes', 'Nothing is sent. Your changes stay in the editor.', 'Keep my changes', 'primary', 1), ...(o.rc ? [CfOpt('Reload theirs', 'This discards your unsaved changes.', 'Reload and discard my changes')] : [CfOpt('Download my version', 'Saves your title and body as a text file. Nothing is sent.', 'Download my version'), CfOpt('Reload theirs', 'Shows their version. You confirm first.', 'Reload theirs'), CfOpt('Save over theirs', 'Replaces the title, body and search fields of this version only. Other languages are untouched.', 'Save over theirs')])], [])
|
||||
const VList = [['English', 'Published'], ['French', 'Draft'], ['German', 'Published'], ['Arabic', 'Draft · Read only']]
|
||||
const moveNote = (hidden) => D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;' + HL, 'Moves all language versions of this article.'), ...VList.filter(r => !(hidden && r[0] === 'Arabic')).map(r => S(TO, r[0] + ' · ' + r[1])), hidden && S(MU + ';font-size:12px', 'Versions you can\'t read aren\'t listed or counted.'))
|
||||
const mvPanel = (hidden) => D('position:absolute;z-index:40;' + (m ? 'left:0;right:0;bottom:0;border-top:1px solid var(--ui-border)' : 'right:24px;top:215px;width:340px;border:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);padding:16px;display:flex;flex-direction:column;gap:12px', S('font-weight:600;' + HL, 'Site'), D('display:flex;flex-direction:column;gap:0;border:1px solid var(--ui-border)', ...['Journal', 'Atelier', 'Archive', 'Notes'].map(k => D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + (k === 'Journal' ? 'background:var(--rc-sand);color:var(--rc-on-sand);font-weight:600' : HL), S('width:16px', k === 'Journal' && Ic('check')), k))), moveNote(hidden), D('display:flex;gap:8px', Btn('Cancel'), D('flex:1;display:flex', Btn('Move article…', 'primary', { w: '100%' }))))
|
||||
const mvDlg = (kind) => {
|
||||
const base = { desc: 'This moves every version together. Nothing is split.' }
|
||||
return {
|
||||
moveConfirm: () => Dlg({ title: 'Move to Atelier?', ...base }, [moveNote(false), Alert('neutral', 'info', 'Addresses and links', 'Each version keeps its address. Links from the old site to it will stop working. RenewCMS does not create redirects.')], [Btn('Cancel'), Btn('Move article', 'primary')]),
|
||||
moveBusy: () => Dlg({ title: 'Moving to Atelier…', desc: 'All 4 versions move together. Keep this page open.' }, [Alert('info', 'loader-circle', 'Moving', 'This takes a few seconds.')], [Btn('Cancel', 'secondary', { dis: true }), Btn('Moving…', 'primary', { busy: true })]),
|
||||
move: () => Dlg({ title: 'Move to Notes?', ...base }, [Alert('warning', 'triangle-alert', 'Notes doesn\'t enable every language in this article', 'Notes enables English only. Enable French, German and Arabic on Notes first. Languages disabled on Journal count too. Nothing was moved.')], [Btn('Cancel'), Btn('Move article', 'primary', { dis: true })]),
|
||||
moveSlug: () => Dlg({ title: 'Move to Atelier?', ...base }, [Alert('error', 'circle-x', 'An address is already used on Atelier', 'The French address "comment-nous-chiffrons-une-renovation" is already used by another French article on Atelier. Change it before moving. Nothing was moved.', Btn('Edit slug', 'secondary', { sm: true }))], [Btn('Cancel'), Btn('Move article', 'primary', { dis: true })]),
|
||||
movePerm: () => Dlg({ title: 'You can\'t move this article' }, [Alert('error', 'circle-x', 'Moving needs edit permission on every version', 'Ask an owner to move it. Nothing was changed.')], [Btn('Close')]),
|
||||
moveDone: () => [Toast('success', 'circle-check', 'Moved to Atelier', 'All 4 versions moved together.')]
|
||||
}[kind]
|
||||
}
|
||||
const pmOv = () => {
|
||||
const mv = mvDlg(x)
|
||||
if (mv) return mv()
|
||||
if (x === 'moveSelect' || x === 'moveSelectHidden') return [mvPanel(x === 'moveSelectHidden')]
|
||||
const dl = { 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: () => Cf({}),
|
||||
conflictRestored: () => Cf({ alert: Alert('info', 'info', 'Restored unsaved changes from 14:18', 'Your session ended. These changes were kept in this browser.') }),
|
||||
reloadConfirm: () => Cf({ title: 'Reload theirs?', desc: 'This discards your unsaved changes.', rc: 1 }),
|
||||
conflict2: () => Cf({ t: '14:41', alert: Alert('error', 'circle-x', 'Couldn\'t replace theirs', 'Someone else saved this version again at 14:41, before your save went through. Nothing was overwritten. Your changes are still here.') }),
|
||||
download: () => [Cf({}), Toast('success', 'circle-check', 'Download started', 'Your French title and body are saved as a text file. Your changes are still in the editor.')],
|
||||
restored: () => [] }
|
||||
return dl[x]?.()
|
||||
}
|
||||
// ---- existing surfaces
|
||||
const sfPage = () => {
|
||||
const mk2 = (t, d, ...c) => Card(S('font-weight:600;' + HL, t), d && S(TO, d), ...c)
|
||||
const RowS = (a, b, c) => D(ROW + ';padding-left:0;padding-right:0', D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S(HL + ';font-weight:500', a), b && S(MU + ';font-size:12px', b)), c)
|
||||
if (x === 'home' || x === 'homeFr') { const fr = x === 'homeFr', V = FX.view({ site: J, read: fr ? ['fr'] : null }); return Page('Home', fr ? 'You can read French versions only.' : 'Welcome back, Marta.', null, mk2('Recently updated', null, ...V.rows.slice(0, 5).map(r => RowS(FX.tt(r.s, r.rep), r.s.by + ' · ' + r.s.upd, r.rep !== J.def ? S(mono + ';border:1px solid var(--ui-border-accented);padding:0 6px;' + HL, r.rep.toUpperCase()) : null))), S(MU + ';font-size:12px', 'Each article appears once, with its default-language title. A language code shows only when the title isn\'t in the default language.')) }
|
||||
if (x === 'activity') return Page('Activity', 'Recent changes in Journal and Archive.', null, mk2('Today and this week', null, ...[['Camille Roche', 'Published French version of "Guide de l\'enduit à la chaux"', '2 h ago'], ['Luis Ferrer', 'Published French version of "Liste d\'hiver pour les fenêtres en bois"', 'Yesterday'], ['Marta Alves', 'Published German version of "So kalkulieren wir eine Badsanierung"', '3 days ago'], ['Luis Ferrer', 'Created German version of "Checkliste für die Dachprüfung"', '14 Oct'], ['Camille Roche', 'Published "Guia de cal e areia"', '14 Oct']].map(r => RowS(r[1], r[0] + ' · ' + r[2]))), S(MU + ';font-size:12px', 'The last event is on Archive, which uses one language, so it names none.'))
|
||||
if (x === 'media') return Page('Media', 'Image details', null, mk2('bathroom-plan.jpg', null, D('height:160px;border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;' + MU + ';font:12px \'IBM Plex Mono\',monospace', 'image preview'), S('font-weight:500;' + HL, 'Used in 2 articles · 4 versions · 1 file'), S(MU + ';font-size:12px', 'A shared image is one file. It is counted once per article, and the versions that use it are listed.'), RowS('How we price a bathroom renovation', 'English, French, German'), RowS('A guide to lime plaster', 'English')))
|
||||
if (x === 'menu') return Page('Article menu', 'The same menu names the language on multilingual sites.', null, D('display:flex;gap:16px;flex-wrap:wrap', ...[['Journal (several languages)', ['Publish French', 'Delete French version']], ['Archive (one language)', ['Publish', 'Delete']]].map(g => D('flex:1 1 220px;display:flex;flex-direction:column;gap:6px', S('font-size:12px;' + MU, g[0]), D('border:1px solid var(--ui-border);background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);padding:4px 0', ...g[1].map((l, i) => D('display:flex;align-items:center;gap:10px;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + (i ? 'color:var(--ui-error)' : HL), Ic(i ? 'trash-2' : 'globe'), S('font-weight:500', l))))))))
|
||||
if (x === 'apiCopy') return Page('Details', 'French version', null, mk2('Public API URL', 'Each language version has its own address. The URL carries the language.', D('display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--ui-bg-elevated);' + mono, S('flex:1', '/v1/article/14?lang=fr'), Ic('copy', 14))))
|
||||
if (x === 'seoPreview') return Page('Search preview', 'French version', null, mk2('How it can appear in search', null, D('display:flex;flex-direction:column;gap:2px;padding:12px;background:var(--ui-bg-elevated)', S(mono + ';' + MU, 'journal.example.com › fr › comment-nous-chiffrons-une-renovation'), S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, FRT + ' | Journal'), S(TO, 'La plupart des devis échouent parce qu\'ils cachent les trois mêmes coûts.')), S(MU + ';font-size:12px', 'Shows the localized address and canonical. Description: Fallback: French excerpt.')))
|
||||
if (x === 'siteFallback') return Page('Search and sharing', 'Site-level text is used as a fallback only in its own language.', null, ...[['French article', 'Description', 'Fallback: French site description', 'Used: "Rénovation et conseils pour la maison" (site text in French)', 1], ['Arabic article', 'Description', 'No fallback', 'Empty. English site default not used.', 0]].map(r => Card(S('font-weight:600;' + HL, r[0]), Fd(r[1], r[4] ? 'Empty' : 'Empty', { val: false, help: r[2] + '. ' + r[3] }), r[4] ? Bd('Uses French site text', 'info', 'languages') : Bd('Tag left out', 'neutral', 'minus'))))
|
||||
return null
|
||||
}
|
||||
// ---- 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()] }
|
||||
const apiToast = x === 'apiCopy' ? Toast('success', 'circle-check', 'API URL copied', '/v1/article/14?lang=fr') : null
|
||||
if (v === 'sl') { Object.assign(shell, setS, x === 'single' ? { crumbs: ['Settings', 'Sites', 'Archive', 'Languages'], mTitle: 'Archive · Languages' } : x === 'social' || x === 'socialEdit' || x === 'socialInvalid' ? { crumbs: ['Settings', 'Sites', 'Atelier', 'Languages'], mTitle: 'Atelier · Languages' } : {}); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', slPage()); ov = [].concat(slOv() || []) }
|
||||
else if (v === 'pu') { Object.assign(shell, setS, { crumbs: ['Settings', 'Sites', 'Journal', 'Public URL'], mTitle: 'Journal · Public URL' }); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', puPage()); if (x === 'saved') ov = [Toast('success', 'circle-check', 'Public URL saved', 'Journal pages now use this pattern.')] }
|
||||
else if (v === 'su') { Object.assign(shell, setS, { crumbs: ['Settings', 'Sites', 'Journal', 'Languages', 'Setup'], mTitle: 'Set article 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()); ov = listOv() }
|
||||
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 === 'sf') { Object.assign(shell, { nav: x === 'home' || x === 'homeFr' ? 'home' : x === 'activity' ? 'activity' : x === 'media' ? 'media' : 'articles', crumbs: [x === 'home' || x === 'homeFr' ? 'Home' : x === 'activity' ? 'Activity' : x === 'media' ? 'Media' : 'Articles'], mTitle: 'Journal' }); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', sfPage()); if (apiToast) ov = [apiToast] }
|
||||
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') })))
|
||||
const unset = x === 'unset', slugD = x === 'editSlug'
|
||||
const rows = [['Page title', FRT + ' | Journal', 'Fallback: French title', 1], ['Description', 'Empty', 'No fallback. Empty. English site default not used. The tag is left out.', 0], ['Image alternative text', 'Un plan de salle de bain avec les mesures', 'Written per language.', 1]]
|
||||
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 Public URL settings', 'secondary', { sm: true })) : Card(D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('Canonical', 'neutral', 'link'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'https://journal.example.com/fr/comment-nous-chiffrons-une-renovation')), S(MU + ';font-size:12px', 'Built from the 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-a-bathroom-renovation'), Bd('Published', 'success', 'circle-check')), D('display:flex;gap:10px;align-items:center', S(mono, 'DE'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + TO, 'https://journal.example.com/de/so-kalkulieren-wir-eine-badsanierung'), Bd('Published', 'success', 'circle-check')), S(MU + ';font-size:12px', 'Drafts and the disabled Spanish language are left out.')),
|
||||
Card(S('font-weight:600;' + HL, 'Social locale'), D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('og:locale', 'neutral', 'share-2'), x === 'noSocial' ? S(MU, 'Social locale not set') : S(mono + ';' + HL, 'Set: 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. A region is never guessed from the language.')),
|
||||
Card(Fd('Address (slug)', 'comment-nous-chiffrons-une-renovation', { mono: true, ro: true, lock: true, help: 'Unique within Journal and French. Editing the title never changes the address.' }), D('display:flex;flex-direction:column;gap:6px;align-items:flex-start', Btn('Edit slug', 'secondary', { ic: 'pencil' }), S(MU + ';font-size:12px;line-height:18px', 'Editing the title never changes the slug. Only Edit slug does.'), S(HL + ';font-size:12px;line-height:18px', 'Links to the old address will stop working. RenewCMS does not create redirects.'))),
|
||||
...rows.map(r => Fd(r[0], r[1], { help: r[2], val: r[3] === 1 })))
|
||||
if (slugD) ov = [Dlg({ title: 'Edit slug for the French version', desc: 'The address changes for French only.' }, [Fd('Address (slug)', 'comment-nous-calculons-une-renovation', { mono: true, focus: true }), Alert('warning', 'triangle-alert', 'This version is published', 'Links to the old address will stop working. RenewCMS does not create redirects.'), S(MU + ';font-size:12px', 'Editing the title never changes the slug.')], [Btn('Cancel'), Btn('Change address', 'primary')])]
|
||||
} 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.')))
|
||||
Card(S('font-weight:600;' + HL, '1. Exact lookup: site + language + slug'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'GET /v1/public/site/{key}/lang/{lang}/article/{slug}'), S(TO, 'Matches the published version with that slug in that language, or returns article_not_found. It never falls back, because a slug belongs to one language.'), S(mono + ';' + MU + ';overflow-wrap:anywhere', '{ "story_id": "st_01", "lang": "fr", "slug": "…", "alternates": [{ "lang": "en", "slug": "…" }] }')),
|
||||
Card(S('font-weight:600;' + HL, '2. Resolve by article group, with fallback'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'GET /v1/public/site/{key}/story/{story_id}?lang=fr&fallback=default'), S(TO, 'Use story_id from any response or from alternates[]. French published: lang "fr". French missing or unpublished with fallback=default: the published default, with "lang": "en", "requested_lang": "fr", "fallback": true. Without fallback=default, a missing French is language_not_available. No other language is tried, and drafts are never returned.')),
|
||||
Card(S('font-weight:600;' + HL, '3. Errors'), S(mono + ';' + HL, 'article_not_found · language_not_available · language_disabled'), S(TO, 'article_not_found: no published version has that slug in that language. language_not_available: the requested language and the allowed fallback are both unavailable. language_disabled: publishing in a disabled language.')),
|
||||
Card(S('font-weight:600;' + HL, 'Existing, unchanged'), S(mono + ';' + HL, 'GET /v1/article/{id}'), S(mono + ';' + HL, 'PATCH /v1/article/{id}/publish'), S(mono + ';' + HL, 'Editor: /article/{id}/edit'), S(TO, 'Each article ID is one language version. Article-ID links keep working.')))
|
||||
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 }))
|
||||
const rtl = x === 'rtl', en = x === 'navFail' || (v === 'ed' && ['cleanOnline', 'onlineDirty', 'unpublishDirty'].includes(x))
|
||||
let L = rtl ? AR : FR
|
||||
const o = { cap: CAPS.full, pub: 'draft' }
|
||||
const bn = []
|
||||
let ovl = []
|
||||
const pop = (rows, q = {}) => ovl.push(LangPop(rows, { left: 248, top: 176, ...q }))
|
||||
const lifeOnline = 'This version is online. Saving updates the live page right away.'
|
||||
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 === 'dirty') { o.dirty = 1; o.cap = CAPS.p2 }
|
||||
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 === 'cleanOnline') { o.pub = 'pub'; bn.push(Alert('neutral', 'info', lifeOnline)) }
|
||||
else if (x === 'onlineDirty') { o.dirty = 1; o.pub = 'pub'; bn.push(Alert('neutral', 'info', lifeOnline)) }
|
||||
else if (x === 'saveFail') { o.dirty = 1; o.saveFail = 1; bn.push(Alert('error', 'circle-x', 'Couldn\'t save French', 'Your changes are kept here. Nothing was published. Try saving again.')) }
|
||||
else if (x === 'pubFail') { o.pubFail = 1; bn.push(Alert('error', 'circle-x', 'Saved as a draft. Publishing failed. Try publishing again.', 'Your edits are saved. Only the publishing step failed.', Btn('Retry publish', 'secondary', { sm: true }))) }
|
||||
else if (x === 'unsaved') { o.dirty = 1; ovl.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') { o.saved = 1; ovl.push(Toast('success', 'circle-check', 'French saved', 'English wasn\'t changed.')) }
|
||||
else if (x === 'ovf') { o.dirty = 1; ovl.push(OvMenu(OvItems(L, { ...o }))) }
|
||||
else if (['focus', 'titleEdit', 'titleUnsaved', 'invalid', 'titleShort', 'titleLong', 'titleSaving', 'failed'].includes(x)) { o.tst = { titleEdit: 'edit', titleUnsaved: 'unsaved', titleShort: 'short', titleLong: 'long', titleSaving: 'saving' }[x] || x; o.dirty = !['focus', 'saving', 'failed'].includes(o.tst) || o.tst === 'failed'; if (o.tst === 'failed') o.saveFail = 1; if (['invalid', 'short', 'long'].includes(o.tst)) o.saveBlock = 1; if (o.tst === 'saving') { o.saving = 1; o.dirty = 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 (x === 'navFail') ovl.push(Toast('error', 'circle-x', 'Couldn\'t open the French version', 'You\'re still editing English. Try again.'))
|
||||
}
|
||||
else if (v === 'pm') {
|
||||
if (x === 'restored') { o.dirty = 1; bn.push(Alert('info', 'info', 'Restored unsaved changes from 14:18', 'You logged in again. Your French title and body were kept in this browser.')) }
|
||||
else if (x === 'conflict' || x === 'conflict2' || x === 'download' || x === 'conflictRestored' || x === 'reloadConfirm') o.dirty = 1
|
||||
else if (x.startsWith('move') && x !== 'moveSelect' && x !== 'moveSelectHidden') { o.cap = { ...CAPS.full } }
|
||||
}
|
||||
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 === 'rtl') { o.S0 = EN }
|
||||
if (x === 'arRef') o.S0 = AR
|
||||
if (x === 'reopen') ovl.push(Toast('info', 'info', 'Reference: English', 'Restored. Your place in both panes is kept.'))
|
||||
if (x === 'changeRef') ovl.push(D('position:absolute;z-index:40;' + (m ? 'left:0;right:0;bottom:0' : 'left:260px;top:220px;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', 'This only changes what you compare against. The tracked source for review stays English. Use Change tracked source in the review banner to change that.')))
|
||||
} 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)
|
||||
const buttons = [Btn('Show source reference', 'secondary', { sm: true }), Btn('Mark as reviewed', 'primary', { sm: true }), Btn('Change tracked source', 'ghost', { sm: true })]
|
||||
if (x === 'gone') { o.fresh = 'No source'; bn.push(Alert('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'; bn.push(Alert('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'; bn.push(D('display:flex;flex-direction:column;gap:8px', D('display:flex;gap:8px;align-items:center', Tracked('English')), Alert('warning', 'triangle-alert', 'English changed after French was reviewed', 'Changed fields: Title, Body. English was saved 14 Oct 2026, 09:12. French was last reviewed 12 Oct 2026.' + (x === 'pub' ? ' French is still published. Reviewing doesn\'t change publication.' : ''), buttons))) }
|
||||
ovl = ovl.concat(frOv() || [])
|
||||
} else if (v === 'pd') { o.pub = ['unpublish', 'unpublishDirty', 'done', 'deleteSource', 'deleteLast', 'deleteStoryOk', 'deleteStory'].includes(x) ? 'pub' : 'draft'; if (['saveAndPublish', 'saveFailPublish', 'unpublishDirty'].includes(x)) o.dirty = 1; if (x === 'saveFailPublish') o.saveFail = 1; if (x === 'publishDisabled') { o.pubDis = 1; bn.push(Alert('warning', 'triangle-alert', 'French is disabled on this site. Re-enable it to publish.', 'You can still edit and save this version. Ask an owner to re-enable French.')) } ovl = ovl.concat(pdOv() || []) }
|
||||
else if (v === 'at') { o.pub = 'pub'; L = LS(x === 'dup' || x === 'dupHidden' ? S1 : S5); ovl = ovl.concat(atOv()) }
|
||||
if (v === 'pm') ovl = ovl.concat(pmOv() || [])
|
||||
if (v === 'perm') {
|
||||
const cp = { ro: CAPS.ro, roMenu: CAPS.ro, p2: CAPS.p2, p3: CAPS.p3, p4: CAPS.full, p5: CAPS.full, p6: CAPS.p6, noAdd: CAPS.full, withAdd: CAPS.full }[x] || CAPS.full
|
||||
o.cap = cp
|
||||
if (x === 'noAdd' || x === 'withAdd') L = LV('fr', 'French', 'FR', FX.STORIES[1])
|
||||
if (x === 'ro' || x === 'roMenu') bn.push(Alert('neutral', 'lock', 'You can read this version. You can\'t edit it.', 'The source reference and the versions you can read stay available.'))
|
||||
if (x === 'p2') bn.push(Alert('neutral', 'info', 'Publishing needs the publish permission', 'Saving keeps it as a draft.'))
|
||||
if (x === 'p3') bn.push(Alert('neutral', 'lock', 'You can publish this version but not edit it', 'Fields are read-only. Publish stays available.'))
|
||||
if (x === 'roMenu') { o.open = 1; pop(MENUS.ro) }
|
||||
if (x === 'p4') { o.open = 1; pop(MENUS.p4) }
|
||||
if (x === 'p5') { o.open = 1; pop(MENUS.p5) }
|
||||
if (x === 'noAdd') { o.open = 1; pop(MENUS.noAdd) }
|
||||
if (x === 'withAdd') { o.open = 1; pop(MENUS.withAdd) }
|
||||
if (x === 'p6') ovl.push(OvMenu(OvItems(L, o)))
|
||||
}
|
||||
ov = ovl
|
||||
if (!main) {
|
||||
const cap = o.cap, S0 = o.S0 || EN
|
||||
if (en) { L = EN }
|
||||
if (m && o.src) bn.push(Seg(!!o.srcMob, L, S0))
|
||||
if (o.src && (!m || o.srcMob)) srcPane = D((m ? 'flex:1 0 100%;padding:16px' : 'width:' + (o.narrow || nr ? '42%' : '50%') + ';padding:24px') + ';flex-shrink:0;min-width:0;border-right:1px solid var(--ui-border);overflow:hidden;display:flex;flex-direction:column;background:var(--ui-bg-muted)', Src(S0, o))
|
||||
if (bn.length) edBanner = D('flex-shrink:0;display:flex;flex-direction:column;gap:8px;padding:12px ' + (m ? 16 : 24) + 'px;border-bottom:1px solid var(--ui-border)', ...bn)
|
||||
const ps = o.long ? L.p.concat(L.p, L.p) : L.p
|
||||
edBodyEl = A('div', { dir: L.dir, lang: L.lang, 'aria-readonly': !cap.e || undefined }, !cap.e ? 'color:var(--ui-text-toned)' : '', ...ps.map(q => A('p', { dir: L.dir }, 'margin:0 0 20px;font-size:' + (m ? 17 : 18) + 'px;line-height:1.66', q)))
|
||||
const sv = o.saving ? 'saving' : o.saveFail ? 'error' : o.dirty ? 'dirty' : 'clean'
|
||||
edHeader = Hdr(L, o); edMobBar = MBar(L, o)
|
||||
Object.assign(shell, { kind: 'editor', crumbs: ['Articles', L.t], mTitle: 'Edit article', mLead: 'arrow-left', ...(nr ? { w: 1024, h: 768 } : {}), ed: { readonly: !cap.e, status: o.pub === 'pub' ? 'online' : 'draft', save: sv, title: L.t, loading: !!o.loading, len: 3200, seoSummary: v === 'ed' ? '1 suggestion' : 'Ready', lang: { name: L.n, code: L.c, open: !!o.open, ro: !cap.e, fresh: o.fresh || '' } } })
|
||||
}
|
||||
}
|
||||
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 }
|
||||
if (nr && shell.kind !== 'editor') Object.assign(shell, { w: 1024, h: 768 })
|
||||
return { srcPane, edBanner, edBodyEl, edHeader, edMobBar, 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>
|
||||
|
||||
@@ -148,7 +148,7 @@
|
||||
<div style="padding:{{ pad }};display:flex;flex-direction:column;gap:14px;max-width:560px"><span style="font:500 13px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">404</span><span style="font:600 24px/32px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">This article doesn't exist</span><span style="color:var(--ui-text-toned);font-size:14px;line-height:22px">It may have been deleted, or the link is wrong.</span><span data-act="nav-articles" style="align-self:flex-start;height:{{ btnH }};padding:0 12px;display:flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500">All articles</span></div>
|
||||
</sc-if>
|
||||
<sc-if value="{{ edFound }}">
|
||||
<div style="flex-shrink:0;background:var(--ui-bg);border-bottom:1px solid var(--ui-border);padding:{{ edHeadPad }};display:flex;flex-wrap:wrap;align-items:center;gap:{{ edHeadGap }}">
|
||||
<div style="flex-shrink:0;background:var(--ui-bg);border-bottom:1px solid var(--ui-border);padding:{{ edHeadPad }};display:flex;flex-wrap:wrap;align-items:center;gap:{{ edHeadGap }}">{{ edHeader }}<sc-if value="{{ edDefHead }}">
|
||||
<sc-if value="{{ desk }}"><span data-act="back" style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/arrow-left.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/arrow-left.svg') center / contain no-repeat"></span></span></sc-if>
|
||||
<div style="flex:1 1 {{ edTitleBasis }};min-width:0;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
|
||||
<sc-if value="{{ s.ed.loading }}"><span style="width:320px;max-width:70%;height:20px;background:var(--ui-bg-accented)"></span><span style="width:60px;height:22px;background:var(--ui-bg-accented)"></span></sc-if>
|
||||
@@ -164,7 +164,7 @@
|
||||
<span data-act="overflow" role="button" aria-label="Delete article" style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0;font-weight:500;border:1px solid var(--ui-error);color:var(--ui-error);cursor:pointer"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat"></span>{{ delText }}</span>
|
||||
</sc-if>
|
||||
</sc-if>
|
||||
</div>
|
||||
</sc-if></div>
|
||||
{{ edBanner }}<sc-if value="{{ showToolbar }}">
|
||||
<div role="toolbar" aria-label="Formatting" style="flex-shrink:0;padding:6px {{ tbPadX }};display:flex;align-items:center;gap:2px;border-bottom:1px solid var(--ui-border);position:relative;overflow:{{ tbOverflow }};background:var(--ui-bg)">
|
||||
<div style="display:flex;align-items:center;gap:2px;flex-shrink:0;transform:translateX({{ tbShift }})"><sc-for list="{{ toolbar }}" as="t" hint-placeholder-count="20"><sc-if value="{{ t.sep }}"><span style="width:1px;height:20px;background:var(--ui-border-accented);margin:0 6px;flex-shrink:0"></span></sc-if><sc-if value="{{ t.btn }}"><span data-act="tb-{{ t.icon }}" style="height:{{ tbBtn }};min-width:{{ tbBtn }};padding:0 6px;flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:4px;position:relative;color:{{ t.fg }};opacity:{{ t.op }};outline:{{ t.ring }};outline-offset:1px;background:{{ t.bg }}"><span aria-hidden="true" style="display:inline-block;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ t.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ t.icon }}.svg') center / contain no-repeat"></span>{{ t.text }}<sc-if value="{{ t.tip }}"><span role="tooltip" style="position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:6px;z-index:30;white-space:nowrap;padding:4px 8px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font:400 12px/16px 'IBM Plex Sans',sans-serif">{{ t.tip }}</span></sc-if></span></sc-if></sc-for></div><sc-if value="{{ tbFadeL }}"><span style="position:absolute;left:0;top:0;bottom:0;width:24px;background:linear-gradient(to right,var(--ui-bg),transparent);pointer-events:none"></span></sc-if><sc-if value="{{ tbFadeR }}"><span style="position:absolute;right:0;top:0;bottom:0;width:32px;background:linear-gradient(to left,var(--ui-bg),transparent);pointer-events:none"></span></sc-if>
|
||||
@@ -181,7 +181,7 @@
|
||||
<div style="font-size:{{ h2F }};line-height:1.3;font-weight:600;margin:32px 0 12px">What goes into the price</div>
|
||||
<ul style="margin:0 0 20px;padding-left:28px;list-style:square;font-size:{{ pF }};line-height:1.66"><li>Stripping out and making good</li><li>Plumbing moves, if any</li><li>Tiles, fittings and a day for silicone</li></ul>
|
||||
<sc-if value="{{ s.ed.image }}"><div style="margin:28px 0;aspect-ratio:16/9;background:var(--ui-bg-elevated);border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);font:400 13px 'IBM Plex Sans',sans-serif">/static/uploadedImages/3f9c…e1.jpg</div></sc-if>
|
||||
<p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">We don't add a margin on materials you choose yourself.<sc-if value="{{ caret }}"><span style="display:inline-block;width:2px;height:1.1em;background:var(--ui-text-highlighted);vertical-align:-3px;margin-left:1px"></span></sc-if></p></sc-if><sc-if value="{{ edBody }}"><p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">{{ edBody }}<sc-if value="{{ caret }}"><span style="display:inline-block;width:2px;height:1.1em;background:var(--ui-text-highlighted);vertical-align:-3px;margin-left:1px"></span></sc-if></p><sc-if value="{{ s.ed.image }}"><div style="margin:28px 0;aspect-ratio:16/9;background:var(--ui-bg-elevated);border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);font:400 13px 'IBM Plex Sans',sans-serif">/static/uploadedImages/3f9c…e1.jpg</div></sc-if></sc-if>
|
||||
<p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">We don't add a margin on materials you choose yourself.<sc-if value="{{ caret }}"><span style="display:inline-block;width:2px;height:1.1em;background:var(--ui-text-highlighted);vertical-align:-3px;margin-left:1px"></span></sc-if></p></sc-if>{{ edBodyEl }}<sc-if value="{{ edBody }}"><p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">{{ edBody }}<sc-if value="{{ caret }}"><span style="display:inline-block;width:2px;height:1.1em;background:var(--ui-text-highlighted);vertical-align:-3px;margin-left:1px"></span></sc-if></p><sc-if value="{{ s.ed.image }}"><div style="margin:28px 0;aspect-ratio:16/9;background:var(--ui-bg-elevated);border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);font:400 13px 'IBM Plex Sans',sans-serif">/static/uploadedImages/3f9c…e1.jpg</div></sc-if></sc-if>
|
||||
</div>
|
||||
</sc-if>
|
||||
</div>
|
||||
@@ -200,12 +200,12 @@
|
||||
</div>
|
||||
</sc-if>
|
||||
</div>
|
||||
<sc-if value="{{ mob }}"><sc-if value="{{ edReady }}">
|
||||
<sc-if value="{{ mob }}">{{ edMobBar }}<sc-if value="{{ edDefMob }}"><sc-if value="{{ edReady }}">
|
||||
<div style="flex-shrink:0;padding:12px 16px;border-top:1px solid var(--ui-border);background:var(--ui-bg);display:flex;flex-direction:column;gap:8px">
|
||||
<sc-if value="{{ counter.msg }}"><span style="color:{{ counter.fg }};font-size:13px;line-height:19px">{{ counter.msg }}</span></sc-if>
|
||||
<div style="display:flex;gap:8px"><span data-act="details" style="width:44px;height:44px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/panel-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/panel-right.svg') center / contain no-repeat"></span></span><span data-act="save" style="flex:1;height:44px;padding:0 12px;white-space:nowrap;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-weight:500;font-size:15px;opacity:{{ saveOp }}">Save</span><span data-act="publish" style="flex:1 0 auto;padding:0 16px;white-space:nowrap;height:44px;display:flex;align-items:center;justify-content:center;gap:6px;font-weight:500;font-size:15px;background:{{ pub.bg }};color:{{ pub.fg }};border:1px solid {{ pub.bd }};opacity:{{ pub.op }}">{{ pub.t }}</span></div>
|
||||
</div>
|
||||
</sc-if></sc-if>
|
||||
</sc-if></sc-if></sc-if>
|
||||
</sc-if>
|
||||
</sc-if>
|
||||
|
||||
@@ -552,9 +552,9 @@ class Component extends DCLogic {
|
||||
langSeg: [['en','English'],['fr','Français']].map(([k,tt])=>{const on=(s.lang||'en')===k;return {act:'lang-'+k,code:k,t:tt,on,op:on?1:0}}), themeSeg: [['system','monitor','System'],['light','sun','Light'],['dark','moon','Dark']].map(([k,icon,tt])=>{const on=(s.mode||'light')===k;return {act:'theme-'+k,icon,t:tt,on,op:on?1:0}}), menuOpen: !!s.userMenu, deskMenu: !m && !!s.userMenu, mobMenu: m && !!s.userMenu, pfItemBg: 'var(--ui-bg-elevated)',
|
||||
chipBg: s.userMenu || s.nav === 'profile' ? 'var(--rc-sand)' : 'transparent', chipFg: s.userMenu || s.nav === 'profile' ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)',
|
||||
isSettings: k === 'settings', isProfile: k === 'profile', pfLoading: k === 'profile' && !!pf.loading, pfReady: k === 'profile' && !pf.loading,
|
||||
upBtnDir: m ? 'column' : 'row', upBtnAlign: m ? 'stretch' : 'flex-start', cardPad: '16px', cardPadX: m ? '16px' : '20px', inH: m ? '44px' : '40px', pfCols: m ? 'minmax(0,1fr)' : 'minmax(0,1fr) minmax(0,1.4fr)', pwDir: m ? 'column' : 'row', pwAlign: m ? 'stretch' : 'flex-end', edTitle, edBody: e.body || '', sampleBody: !e.body, tabs, rows, save, counter, pub, mFields, up, toasts, endpoints, toolbar,
|
||||
upBtnDir: m ? 'column' : 'row', upBtnAlign: m ? 'stretch' : 'flex-start', cardPad: '16px', cardPadX: m ? '16px' : '20px', inH: m ? '44px' : '40px', pfCols: m ? 'minmax(0,1fr)' : 'minmax(0,1fr) minmax(0,1.4fr)', pwDir: m ? 'column' : 'row', pwAlign: m ? 'stretch' : 'flex-end', edTitle, edBody: e.body || '', edBodyEl: this.props.edBodyEl || null, sampleBody: !e.body && !this.props.edBodyEl, tabs, rows, save, counter, pub, mFields, up, toasts, endpoints, toolbar,
|
||||
contentShow: CT.length > 0, contentRows, contentColl: m && !s.contentOpen, contentCount: String(CT.length), contentMore: (!m || s.contentOpen) && CT.length > ctVis.length ? 'Show all ' + CT.length : '', desk: !m, mob: m, notBoot: !s.boot,
|
||||
fw: m ? '390px' : '1440px', fh: m ? '844px' : '900px', dir: m ? 'column' : 'row',
|
||||
fw: m ? '390px' : (s.w ? s.w + 'px' : '1440px'), fh: m ? '844px' : (s.h ? s.h + 'px' : '900px'), dir: m ? 'column' : 'row',
|
||||
mLead: s.mLead || 'menu',
|
||||
navDrawer: m && !!s.drawer, integSearch: k === 'integration' && !!s.search, isSlot: !!this.props.slotBody, slotBody: this.props.slotBody || null, slotOverlays: this.props.slotOverlays || null, isBoot: !!s.boot, isHome: k === 'home' && !s.boot, isList: k === 'articles', isEditor: k === 'editor', isInteg: k === 'integration', isKeys: k === 'apikeys', isError: k === 'error', isUsers: k === 'users',
|
||||
usRows, usError: !!US.error, usOnly: !!US.only && !US.error, usTable: !m && !US.error, usCards: m && !US.error,
|
||||
@@ -581,7 +581,7 @@ class Component extends DCLogic {
|
||||
emptyBg: L.empty && L.empty.primary ? 'var(--ui-primary)' : 'var(--ui-bg)', emptyFg: L.empty && L.empty.primary ? 'var(--ui-bg)' : 'var(--ui-text-highlighted)', emptyBd: L.empty && L.empty.primary ? 'var(--ui-primary)' : 'var(--ui-border-accented)',
|
||||
edFound: k === 'editor' && !e.notfound, edReady, edPending: edReady && !!e.pending, edOnline: edReady && !e.pending && online, edDraft: edReady && !e.pending && !online,
|
||||
edHeadPad: m ? '12px 16px' : '14px 24px', edHeadGap: m ? '8px 12px' : '12px', edTitleBasis: m ? '100%' : '200px',
|
||||
showToolbar: k === 'editor' && !e.notfound && !e.preview,
|
||||
showToolbar: k === 'editor' && !e.notfound && !e.preview && !e.readonly, edDefHead: !this.props.edHeader, edDefMob: !this.props.edMobBar, edHeader: this.props.edHeader || null, edMobBar: this.props.edMobBar || null,
|
||||
tbPadX: m ? '8px' : '24px', tbBtn: m ? '40px' : '28px',
|
||||
edBg: e.preview ? 'var(--ui-bg-muted)' : 'var(--ui-bg)', edPad: m ? '24px 16px' : '40px 48px', edMargin: e.preview ? '0 auto' : '0',
|
||||
h1F: m ? '28px' : '32px', h2F: m ? '23px' : '26px', pF: m ? '17px' : '18px', caret: !e.preview,
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<div style="display:flex;align-items:flex-end;gap:32px;max-width:1400px">
|
||||
<div style="flex:1;display:flex;flex-direction:column;gap:10px">
|
||||
<h1 style="margin:0;font:600 36px/44px 'IBM Plex Sans',sans-serif;letter-spacing:-0.02em;color:var(--ui-text-highlighted)">Screens: article localization</h1>
|
||||
<p style="margin:0;max-width:880px;font-size:16px;line-height:26px;color:var(--ui-text-toned);text-wrap:pretty">One story, several language versions. Proposed feature: every API change is labelled as proposed. 1440 px at 50%, 390 px at 75%. The theme switch shows every state in dark mode. Imported localization reference. Product and API corrections are in docs/design/localization.md. Rendered parity and prototype flows remain unverified.</p>
|
||||
<p style="margin:0;max-width:880px;font-size:16px;line-height:26px;color:var(--ui-text-toned);text-wrap:pretty">One story, several language versions. Proposed feature: every API change is labelled as proposed. 1440 px at 50%, 1024 px at 50% for layout-dependent states, 390 px at 75%. The theme switch shows every state in dark mode. Each state shows its ID and fixture. Groups L6 and L9 are release 2. Repository code was unavailable, so existing behaviour comes from the design project only.</p>
|
||||
</div>
|
||||
<div role="radiogroup" aria-label="Theme" style="display:flex;border:1px solid var(--ui-border-accented);background:var(--ui-bg)">
|
||||
<sc-for list="{{ modes }}" as="m" hint-placeholder-count="3">
|
||||
@@ -33,15 +33,16 @@
|
||||
<section data-screen-label="{{ g.label }}" style="display:flex;flex-direction:column;gap:28px">
|
||||
<div style="display:flex;flex-direction:column;gap:6px;max-width:1100px">
|
||||
<h2 style="margin:0;font:600 28px/36px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ g.title }}</h2>
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;font:400 14px/22px 'IBM Plex Mono',monospace;color:var(--ui-text-toned)"><span>{{ g.route }}</span><span style="color:var(--ui-text-muted)">{{ g.api }}</span></div>
|
||||
<div style="display:flex;gap:16px;flex-wrap:wrap;font:400 14px/22px 'IBM Plex Mono',monospace;color:var(--ui-text-toned)"><span>{{ g.route }}</span><span style="color:var(--ui-text-muted)">{{ g.api }}</span><span style="color:var(--ui-text-muted)">{{ g.count }}{{ g.rel }}</span></div>
|
||||
<sc-if value="{{ g.note }}"><p style="margin:4px 0 0;font-size:15px;line-height:24px;color:var(--ui-text-toned);text-wrap:pretty">{{ g.note }}</p></sc-if>
|
||||
</div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:40px;align-items:flex-start">
|
||||
<sc-for list="{{ g.states }}" as="st" hint-placeholder-count="4">
|
||||
<div style="display:flex;flex-direction:column;gap:12px">
|
||||
<div style="display:flex;flex-direction:column;gap:2px;max-width:1040px"><span style="font:600 16px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ st.name }}</span><span style="font-size:13px;color:var(--ui-text-muted)">{{ st.trigger }}</span></div>
|
||||
<div style="display:flex;flex-direction:column;gap:2px;max-width:1040px"><span style="font:600 16px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ st.name }}</span><span style="font-size:13px;color:var(--ui-text-muted)">{{ st.trigger }}</span><span style="font:400 12px/18px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">{{ st.id }} · frame {{ st.sel }} · fixture {{ st.fx }}</span></div>
|
||||
<div style="display:flex;gap:24px;align-items:flex-start">
|
||||
<div style="width:{{ st.dw }};height:{{ st.dh }};overflow:hidden;border:1px solid var(--ui-border);flex-shrink:0"><div style="width:{{ st.fw }};height:{{ st.fh }};zoom:{{ st.z }}"><dc-import name="LocaleFrame" s="{{ st.s }}" mobile="{{ false }}" hint-size="1440px,900px"></dc-import></div></div>
|
||||
<sc-if value="{{ st.tri }}"><div style="width:512px;height:384px;overflow:hidden;border:1px solid var(--ui-border);flex-shrink:0"><div style="width:1024px;height:768px;zoom:0.5"><dc-import name="LocaleFrame" s="{{ st.s2 }}" mobile="{{ false }}" hint-size="1024px,768px"></dc-import></div></div></sc-if>
|
||||
<div style="width:293px;height:633px;overflow:hidden;border:1px solid var(--ui-border);flex-shrink:0"><div style="width:390px;height:844px;zoom:0.75"><dc-import name="LocaleFrame" s="{{ st.s }}" mobile="{{ true }}" hint-size="390px,844px"></dc-import></div></div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -76,13 +77,28 @@ class Component extends DCLogic {
|
||||
if (p.theme !== this.props.theme && this.props.theme) this.setState({ mode: this.props.theme })
|
||||
this.apply()
|
||||
}
|
||||
groups() { return window.LOC.groups.map(g => ({ ...g, label: g.id + ' ' + g.title, states: g.states.map(a => ({ name: a[0], trigger: a[1], dw: a[4]==='narrow'?'717px':'720px', dh: a[4]==='narrow'?'538px':'450px', fw: a[4]==='narrow'?'1024px':'1440px', fh: a[4]==='narrow'?'768px':'900px', z: a[4]==='narrow'?0.7:0.5, s: { v: a[2], x: a[3] }, nar: a[4] === 'narrow' })) })) }
|
||||
groups() { return window.LOC.groups.map(g => ({ ...g, count: g.states.length + ' states · ', rel: g.release === 2 ? 'Release 2' : 'Release 1', label: g.id + ' ' + g.title, states: g.states.map(a => { const nar = a[4] === 'narrow', tri = a[4] === 'tri'; return { name: a[0], trigger: a[1], id: a[5], fx: a[6], sel: 'LocaleFrame{v:' + a[2] + ',x:' + (a[3] || "''") + '}', tri, dw: nar ? '717px' : '720px', dh: nar ? '538px' : '450px', fw: nar ? '1024px' : '1440px', fh: nar ? '768px' : '900px', z: nar ? 0.7 : 0.5, s: { v: a[2], x: a[3], narrow: nar }, s2: { v: a[2], x: a[3], narrow: true } } }) })) }
|
||||
ratio(a, b2) { const h = c => { c = c.replace('#', ''); return [0, 2, 4].map(i => parseInt(c.slice(i, i + 2), 16)) }, L = rgb => { const f = v => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4) }; return 0.2126 * f(rgb[0]) + 0.7152 * f(rgb[1]) + 0.0722 * f(rgb[2]) }, x = L(h(a)), y = L(h(b2)); return (Math.max(x, y) + 0.05) / (Math.min(x, y) + 0.05) }
|
||||
mix(fg, bg, p) { const h = c => [1, 3, 5].map(i => parseInt(c.slice(i, i + 2), 16)), f = h(fg), g = h(bg); return '#' + f.map((v, i) => Math.round(v * p + g[i] * (1 - p)).toString(16).padStart(2, '0')).join('') }
|
||||
computed() {
|
||||
const FX = window.LOCFX, LOC = window.LOC, J = FX.SITES.journal
|
||||
const jv = FX.view({ site: J }), p2 = FX.view({ site: J, page: 2 }), fr = FX.view({ site: J, lang: 'fr' }), frm = FX.view({ site: J, lang: 'fr', tab: 'missing' }), rv = FX.view({ site: J, tab: 'review' }), au = FX.view({ site: J, author: 'Marta Alves' })
|
||||
const cnt = k => FX.count(k, J.stories), ver = J.stories.reduce((a, s) => a + Object.keys(s.v).length, 0), eq = (a, b2) => a === b2 ? 'Matches' : 'MISMATCH'
|
||||
const checks = [['Articles', 8, J.stories.length], ['Versions', 17, ver], ['Page size', 5, jv.size], ['Page 1 rows', 5, jv.rows.length], ['Page 2 rows', 3, p2.rows.length], ['Pages', 2, jv.pages], ['en versions / published', '7 / 6', cnt('en').join(' / ')], ['fr versions / published', '4 / 2', cnt('fr').join(' / ')], ['fr needing review', 2, fr.tabs[3][1]], ['de versions / published', '4 / 3', cnt('de').join(' / ')], ['ar versions', 1, cnt('ar')[0]], ['es versions', 1, cnt('es')[0]], ['it versions', 0, cnt('it')[0]], ['Articles without French', 4, frm.total], ['Needs translation tab', 7, jv.tabs[1][1]], ['Needs review tab', 2, rv.total], ['Author Marta Alves', 3, au.total], ['Setup articles (Journal + Notes + Archive)', 16, FX.SETUP.reduce((a, r) => a + r.n, 0)], ['Press: not in French', 0, FX.view({ site: FX.SITES.press, lang: 'fr', tab: 'missing' }).total]]
|
||||
const checkRows = checks.map(c => [c[0], String(c[1]), String(c[2]), eq(String(c[1]), String(c[2]))])
|
||||
const byFx = {}; LOC.groups.forEach(g => g.states.forEach(a => { (byFx[a[6]] = byFx[a[6]] || []).push(a[5]) }))
|
||||
const fxRows = Object.entries(byFx).map(([k, v]) => [k, FX.FIXTURE_NOTES[k.split(',')[0].replace(' (limited scope)', '').trim()] || '', v.length + ' states', v.length > 12 ? v.slice(0, 8).join(', ') + ' and ' + (v.length - 8) + ' more (every state not listed under another fixture)' : v.join(', ')])
|
||||
const cl = ['--ui-success', '--rc-success-soft'], ct = (m2) => { const t = m2 === 'dark' ? this.dark : this.light; const pr = [['Published badge', t['--ui-success'], t['--rc-success-soft'], 4.5], ['Info badge (Default, Tracked source)', t['--ui-info'], t['--rc-info-soft'], 4.5], ['Warning badge', t['--ui-warning'], t['--rc-warning-soft'], 4.5], ['Error badge', t['--ui-error'], t['--rc-error-soft'], 4.5], ['Neutral badge (Draft, Read only)', t['--ui-text-toned'], t['--ui-bg-elevated'], 4.5], ['Freshness badge (outlined)', t['--ui-warning'], t['--ui-bg'], 4.5], ['Selected row and menu item', t['--rc-on-sand'], t['--rc-sand'], 4.5], ['Coverage chip', t['--ui-text-highlighted'], t['--ui-bg'], 4.5], ['Dashed missing chip text', t['--ui-text-muted'], t['--ui-bg'], 4.5], ['Disabled-language chip (75%)', this.mix(t['--ui-text-highlighted'], t['--ui-bg'], .75), t['--ui-bg'], 4.5], ['Primary button', t['--ui-bg'], t['--ui-primary'], 4.5], ['Disabled menu item (60%, exempt)', this.mix(t['--ui-text-highlighted'], t['--ui-bg'], .6), t['--ui-bg'], 4.5], ['Disabled button (40%, exempt)', this.mix(t['--ui-text-highlighted'], t['--ui-bg'], .4), t['--ui-bg'], 4.5], ['Dashed and input borders (non-text)', t['--ui-border-accented'], t['--ui-bg'], 3]]; return pr.map(r => [r[0], this.ratio(r[1], r[2]).toFixed(2), r[3]]) }
|
||||
const L = ct('light'), D = ct('dark'), conRows = L.map((r, i) => [r[0], r[1], D[i][1], r[2] + ':1', (+r[1] >= r[2] && +D[i][1] >= r[2]) ? 'Pass' : (/exempt/.test(r[0]) ? 'Exempt (disabled)' : 'FAIL')])
|
||||
const cntRows = LOC.groups.map(g => [g.id + ' ' + g.title, String(g.states.length), String(g.states.filter(a => a[4] === 'tri').length), g.release === 2 ? 'Release 2' : 'Release 1']).concat([['Total', String(LOC.groups.reduce((a, g) => a + g.states.length, 0)), String(LOC.groups.reduce((a, g) => a + g.states.filter(a2 => a2[4] === 'tri').length, 0)), '']])
|
||||
return [{ title: 'Fixture index', cols: ['Fixture', 'Contents', 'Used by', 'State IDs'], w: '1.2fr 2fr .8fr 3fr', rows: fxRows }, { title: 'Fixture totals check (computed from locale-fixtures.js now)', cols: ['Figure', 'Decided', 'Computed', 'Result'], w: '2fr 1fr 1fr 1fr', rows: checkRows }, { title: 'State counts per group', cols: ['Group', 'States', 'Also at 1024', 'Release'], w: '3fr 1fr 1fr 1fr', rows: cntRows }, { title: 'Contrast of badges, chips and variants (computed from the tokens)', cols: ['Element', 'Light', 'Dark', 'Needs', 'Result'], w: '3fr 1fr 1fr 1fr 1.4fr', rows: conRows }]
|
||||
}
|
||||
renderVals() {
|
||||
const modes = [['system', 'System'], ['light', 'Light'], ['dark', 'Dark']].map(([k, label]) => {
|
||||
const on = this.state.mode === k
|
||||
return { label, on, bg: on ? 'var(--ui-primary)' : 'transparent', fg: on ? 'var(--ui-bg)' : 'var(--ui-text-toned)', pick: () => this.setState({ mode: k }) }
|
||||
})
|
||||
return { rootRef: this.rootRef, modes, groups: this.groups(), tables: window.LOC.tables }
|
||||
return { rootRef: this.rootRef, modes, groups: this.groups(), tables: window.LOC.tables.concat(this.computed()) }
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
// Shared localization fixtures. One source for the Localization board (LocaleFrame), the Prototype (LocaleLive) and the Handoff.
|
||||
// Dataset "Journal": 8 stories, 17 versions. Every count shown anywhere is derived from STORIES or LANGS.
|
||||
// Separate fixtures (marked): PRESS (all stories translated, for the nothing-missing state), JOURNAL_PRE_FR (prototype start: French not enabled yet).
|
||||
(function () {
|
||||
const P = {
|
||||
en: ['Most quotes fail because they hide the same three costs. Here is how we itemise them before the first tile is ordered.', 'Start with the room, not the catalogue. Measure the walls, note where the pipes run and photograph the existing fixtures.', 'Next, separate what is fixed from what is chosen. Plumbing and waterproofing are fixed. Tiles and taps are chosen.', 'Price the fixed items first and ask for a written range. A range with a stated assumption is more useful than a single figure.', 'Allow ten percent for what the walls reveal. Old bathrooms almost always hide one surprise behind the tiles.', 'Check the floor falls toward the drain before any membrane goes down. Correcting a fall after tiling means lifting everything.', 'Choose the tiles after the layout is drawn. A tile that needs many cuts costs more in labour than it saves in material.', 'Ask the tiler to confirm the adhesive and grout for the wet area in writing, and keep the data sheets with the quote.', 'Agree who protects the hallway floor and who removes the old bath. These small jobs are the ones that start disputes.', 'Finally, agree the order of work with the tiler and the plumber before anything is ordered.'],
|
||||
fr: ['La plupart des devis échouent parce qu\'ils cachent les trois mêmes coûts. Voici comment nous les détaillons avant de commander le premier carreau.', 'Commencez par la pièce, pas par le catalogue. Mesurez les murs, repérez le passage des canalisations et photographiez les équipements existants.', 'Séparez ensuite ce qui est fixe de ce qui est choisi. La plomberie et l\'étanchéité sont fixes. Les carreaux et la robinetterie sont choisis.', 'Chiffrez d\'abord les postes fixes et demandez une fourchette écrite.'],
|
||||
de: ['Die meisten Angebote scheitern, weil sie dieselben drei Kosten verschweigen. So listen wir sie auf, bevor die erste Fliese bestellt wird.', 'Beginnen Sie mit dem Raum, nicht mit dem Katalog. Messen Sie die Wände und halten Sie fest, wo die Rohre verlaufen.'],
|
||||
ar: ['تفشل معظم عروض الأسعار لأنها تُخفي التكاليف الثلاثة نفسها. إليك كيف نفصّلها قبل طلب أول بلاطة.', 'ابدأ بالغرفة لا بالكتالوج. قِس الجدران، وحدّد مسار الأنابيب، وصوّر التركيبات الحالية.', 'افصل بعد ذلك ما هو ثابت عمّا هو مختار. السباكة والعزل ثابتان، أما البلاط والصنابير فمختارة.']
|
||||
}
|
||||
const LANG_NAMES = { en: 'English', fr: 'French', de: 'German', ar: 'Arabic', es: 'Spanish', it: 'Italian', 'pt-BR': 'Portuguese (Brazil)', 'en-GB': 'English (United Kingdom)' }
|
||||
const DIR = { ar: 'rtl' }
|
||||
// v: lang -> [status, needsReview, revision]
|
||||
const mk = (id, title, v, tr, by, upd) => ({ id, title, v, tr: tr || {}, by, upd })
|
||||
const STORIES = [
|
||||
mk('s1', 'How we price a bathroom renovation', { en: ['pub', 0, 8], fr: ['draft', 1, 5], de: ['pub', 0, 3], ar: ['draft', 1, 2] }, { fr: 'Comment nous chiffrons une rénovation de salle de bain', de: 'So kalkulieren wir eine Badsanierung', ar: 'كيف نسعّر تجديد الحمّام' }, 'Marta Alves', '2 h ago'),
|
||||
mk('s2', 'A guide to lime plaster', { en: ['pub', 0, 4], fr: ['pub', 0, 4], de: ['pub', 0, 2] }, { fr: 'Guide de l\'enduit à la chaux', de: 'Ein Leitfaden zu Kalkputz' }, 'Camille Roche', 'Yesterday'),
|
||||
mk('s3', 'Winter checklist for timber windows', { en: ['pub', 0, 6], fr: ['pub', 1, 3] }, { fr: 'Liste d\'hiver pour les fenêtres en bois' }, 'Luis Ferrer', '3 days ago'),
|
||||
mk('s4', 'Spring newsletter', { en: ['draft', 0, 1] }, {}, 'Marta Alves', '3 days ago'),
|
||||
mk('s5', 'Roof inspection checklist', { en: ['pub', 0, 3], de: ['draft', 0, 1] }, { de: 'Checkliste für die Dachprüfung' }, 'Luis Ferrer', '14 Oct'),
|
||||
mk('s6', 'Why we reuse original floorboards', { en: ['pub', 0, 2], fr: ['draft', 0, 1] }, { fr: 'Pourquoi nous réutilisons les lames de parquet d\'origine' }, 'Camille Roche', '13 Oct'),
|
||||
mk('s7', 'Our first passive-house retrofit', { en: ['pub', 0, 5], de: ['pub', 0, 2] }, { de: 'Unsere erste Passivhaus-Sanierung' }, 'Marta Alves', '12 Oct'),
|
||||
mk('s8', 'Cómo elegir un contratista', { es: ['pub', 0, 2] }, {}, 'Luis Ferrer', '10 Oct')
|
||||
]
|
||||
const ENABLED = ['en', 'fr', 'de', 'ar']
|
||||
const LANGS = [
|
||||
{ name: 'English', code: 'en', def: true, enabled: true, social: 'Not set' },
|
||||
{ name: 'French', code: 'fr', enabled: true, social: 'fr_FR' },
|
||||
{ name: 'German', code: 'de', enabled: true, social: 'Not set' },
|
||||
{ name: 'Arabic', code: 'ar', enabled: true, social: 'Not set' },
|
||||
{ name: 'Spanish', code: 'es', enabled: false, social: 'Not set' },
|
||||
{ name: 'Italian', code: 'it', enabled: false, social: 'Not set' }
|
||||
]
|
||||
const count = (code, stories) => { let n = 0, pub = 0; stories.forEach(s => { const x = s.v[code]; if (x) { n++; if (x[0] === 'pub') pub++ } }); return [n, pub] }
|
||||
const PRESS = [
|
||||
mk('p1', 'Opening hours over the holidays', { en: ['pub', 0, 2], fr: ['pub', 0, 2] }, { fr: 'Horaires pendant les fêtes' }, 'Marta Alves', 'Yesterday'),
|
||||
mk('p2', 'Meet the new site manager', { en: ['pub', 0, 3], fr: ['draft', 1, 1] }, { fr: 'Rencontrez la nouvelle cheffe de chantier' }, 'Luis Ferrer', '3 days ago'),
|
||||
mk('p3', 'Annual report', { en: ['draft', 0, 1], fr: ['pub', 0, 1] }, { fr: 'Rapport annuel' }, 'Marta Alves', '14 Oct')
|
||||
]
|
||||
// Prototype start: French is not enabled, so no French versions exist yet.
|
||||
const preFr = () => STORIES.map(s => { const v = { ...s.v }; delete v.fr; const tr = { ...s.tr }; delete tr.fr; return { ...s, v, tr } })
|
||||
// Setup fixture (before languages exist): articles per site, site locale as stored, the language entry it maps to.
|
||||
const SETUP = [
|
||||
{ site: 'Journal', n: 8, locale: 'en', tag: 'en', name: 'English', note: 'Site locale en' },
|
||||
{ site: 'Notes', n: 3, locale: null, tag: null, name: null, note: 'No site locale' },
|
||||
{ site: 'Archive', n: 5, locale: 'pt-BR', tag: 'pt-BR', name: 'Portuguese (Brazil)', note: 'Site locale pt-BR' }
|
||||
]
|
||||
|
||||
STORIES[3].seo = 1; STORIES[4].seo = 1
|
||||
Object.assign(LANG_NAMES, { 'zh-Hans': 'Chinese (Simplified)', ja: 'Japanese' })
|
||||
const ATELIER = [
|
||||
mk('a1', 'A complete guide to renovating a ground-floor kitchen and bathroom in a listed building, with every cost itemised for owners and tenants', { en: ['pub', 0, 4], 'en-GB': ['draft', 0, 1], fr: ['draft', 1, 2], de: ['pub', 0, 2], 'pt-BR': ['draft', 0, 1], 'zh-Hans': ['pub', 0, 2], ja: ['pub', 0, 2], ar: ['draft', 0, 1] }, {}, 'Marta Alves', '2 h ago'),
|
||||
mk('a2', 'Heat pumps for stone farmhouses', { en: ['pub', 0, 2], 'en-GB': ['pub', 0, 1], fr: ['draft', 0, 1], de: ['pub', 0, 1], ja: ['draft', 0, 1] }, {}, 'Luis Ferrer', 'Yesterday'),
|
||||
mk('a3', 'Sourcing reclaimed oak beams', { en: ['draft', 0, 1], 'en-GB': ['draft', 0, 1] }, {}, 'Camille Roche', '14 Oct')
|
||||
]
|
||||
ATELIER[0].seo = 1; ATELIER[2].seo = 1
|
||||
const ARCHIVE = [['Como restaurar azulejos antigos', 'pub', 'Marta Alves', '3 days ago'], ['Guia de cal e areia', 'pub', 'Camille Roche', '14 Oct'], ['Pintura para fachadas húmidas', 'draft', 'Luis Ferrer', '13 Oct'], ['Orçamento de uma reabilitação', 'pub', 'Marta Alves', '12 Oct'], ['Telhados de telha cerâmica', 'pub', 'Camille Roche', '10 Oct']].map((r, i) => mk('r' + (i + 1), r[0], { 'pt-BR': [r[1], 0, 1] }, {}, r[2], r[3]))
|
||||
const NOTES = [['Site visit notes', 'Luis Ferrer', '3 days ago'], ['Supplier call summary', 'Marta Alves', '14 Oct'], ['Tile samples list', 'Camille Roche', '10 Oct']].map((r, i) => mk('n' + (i + 1), r[0], {}, {}, r[1], r[2]))
|
||||
const SITES = {
|
||||
journal: { name: 'Journal', stories: STORIES, langs: ['en', 'fr', 'de', 'ar'], disabled: ['es', 'it'], def: 'en', size: 5, locale: 'en' },
|
||||
press: { name: 'Press', stories: PRESS, langs: ['en', 'fr'], disabled: [], def: 'en', size: 5, locale: 'en' },
|
||||
atelier: { name: 'Atelier', stories: ATELIER, langs: ['en', 'en-GB', 'fr', 'de', 'pt-BR', 'zh-Hans', 'ja', 'ar'], disabled: [], def: 'en', size: 5, locale: 'en' },
|
||||
archive: { name: 'Archive', stories: ARCHIVE, langs: ['pt-BR'], disabled: [], def: 'pt-BR', size: 5, locale: 'pt-BR' },
|
||||
notes: { name: 'Notes', stories: NOTES, langs: [], disabled: [], def: null, size: 5, locale: null },
|
||||
gallery: { name: 'Gallery', stories: [], langs: ['en-GB'], disabled: [], def: 'en-GB', size: 5, locale: 'en-GB' }
|
||||
}
|
||||
const pl = (n, w) => n + ' ' + w + (n === 1 ? '' : 's')
|
||||
const tt = (s, k) => s.tr[k] || s.title
|
||||
const repKey = (s, def) => s.v[def] ? def : Object.keys(s.v).sort()[0]
|
||||
const view = o => {
|
||||
const site = o.site, en = site.langs, lang = o.lang || null, create = o.create || en, st = o.status, au = o.author, q = (o.q || '').toLowerCase(), seo = o.seo, size = o.size || site.size
|
||||
const ss = site.stories.map(s => { const v = {}; Object.keys(s.v).forEach(k => { if (!o.read || o.read.includes(k)) v[k] = s.v[k] }); return { ...s, v } }).filter(s => Object.keys(s.v).length && (!o.scope || o.scope(s)))
|
||||
const pass = (s, vs, ks) => (!st || vs.some(x => x[0] === st)) && (!au || s.by === au) && (!seo || s.seo) && (!q || ks.some(k => tt(s, k).toLowerCase().includes(q)))
|
||||
const all = s => pass(s, Object.values(s.v), Object.keys(s.v))
|
||||
let tabs, rowsBy, LN = lang ? LANG_NAMES[lang] : ''
|
||||
if (lang) {
|
||||
const inL = ss.filter(s => s.v[lang] && pass(s, [s.v[lang]], [lang])), miss = ss.filter(s => !s.v[lang] && all(s))
|
||||
rowsBy = { all: inL, pub: inL.filter(s => s.v[lang][0] === 'pub'), draft: inL.filter(s => s.v[lang][0] === 'draft'), review: inL.filter(s => s.v[lang][1]), missing: miss }
|
||||
tabs = [['All', 'all'], ['Published', 'pub'], ['Drafts', 'draft'], ['Needs review', 'review'], ['Not in ' + LN, 'missing']]
|
||||
} else {
|
||||
const b = ss.filter(all)
|
||||
rowsBy = { all: b, missing: b.filter(s => create.some(k => en.includes(k) && !s.v[k])), review: b.filter(s => Object.values(s.v).some(x => x[1])) }
|
||||
tabs = [['All articles', 'all'], ['Needs translation', 'missing'], ['Needs review', 'review']]
|
||||
}
|
||||
const tab = o.tab || 'all', rows = rowsBy[tab], total = rows.length, pages = Math.max(1, Math.ceil(total / size)), page = Math.min(o.page || 1, pages), from = total ? (page - 1) * size + 1 : 0, to = Math.min(total, page * size)
|
||||
const unit = lang ? ({ all: pl(total, LN + ' version'), pub: 'published ' + LN + ' version' + (total === 1 ? '' : 's'), draft: pl(total, LN + ' draft'), review: pl(total, LN + ' version') + ' ' + (total === 1 ? 'needs' : 'need') + ' review', missing: pl(total, 'article') + ' not in ' + LN })[tab] : ({ all: pl(total, 'article'), missing: pl(total, 'article') + ' ' + (total === 1 ? 'needs' : 'need') + ' translation', review: pl(total, 'article') + ' ' + (total === 1 ? 'needs' : 'need') + ' review' })[tab]
|
||||
return { tabs: tabs.map(t => [t[0], rowsBy[t[1]].length, t[1]]), tab, total, pages, page, size, from, to, unit, rows: rows.slice((page - 1) * size, page * size).map(s => ({ s, rep: repKey(s, site.def) })) }
|
||||
}
|
||||
const chips = (s, site, create) => {
|
||||
const en = site.langs, out = [], has = k => !!s.v[k]
|
||||
const order = [site.def].concat(en.filter(k => k !== site.def))
|
||||
order.forEach(k => { if (has(k)) out.push({ k, st: s.v[k][0], nr: s.v[k][1] }) })
|
||||
Object.keys(s.v).filter(k => !en.includes(k)).forEach(k => out.push({ k, st: s.v[k][0], nr: s.v[k][1], off: 1 }))
|
||||
order.forEach(k => { if (!has(k) && (create || en).includes(k)) out.push({ k, st: null }) })
|
||||
return out
|
||||
}
|
||||
const SOCIAL = { journal: { en: ['none'], fr: ['set', 'fr_FR'], de: ['none'], ar: ['none'], es: ['none'], it: ['none'] }, atelier: { en: ['none'], 'en-GB': ['derived', 'en_GB'], fr: ['set', 'fr_FR'], de: ['none'], 'pt-BR': ['derived', 'pt_BR'], 'zh-Hans': ['none'], ja: ['none'], ar: ['none'] } }
|
||||
const AUTHORS = Object.entries(STORIES.reduce((a, s) => (a[s.by] = (a[s.by] || 0) + 1, a), {}))
|
||||
const FIXTURE_NOTES = { Journal: '8 articles, 17 versions, page size 5; en (default), fr, de, ar enabled; es, it disabled', Press: '3 articles, en + fr, all translated', Atelier: '3 articles, 8 languages, long labels', Archive: '5 articles, one language (pt-BR kept)', Notes: '3 articles, no site locale', Gallery: 'No articles, site locale en-GB' }
|
||||
window.LOCFX = { P, STORIES, PRESS, ATELIER, ARCHIVE, NOTES, SITES, ENABLED, LANGS, LANG_NAMES, DIR, SETUP, preFr, count, view, chips, pl, tt, repKey, SOCIAL, AUTHORS, FIXTURE_NOTES, TOTALS: { stories: 8, versions: 17 } }
|
||||
})()
|
||||
+1975
-271
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user