Files
RenewCMS/docs/design/source/RenewCMS Screens Localization.dc.html
T

107 lines
15 KiB
HTML

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<script src="./locale-fixtures.js"></script>
<script src="./locale-spec.js"></script>
<meta name="design_doc_mode" content="canvas">
<link rel="icon" href="brand/favicon.svg">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;1,8..60,400&display=swap" rel="stylesheet">
<style>body{margin:0;font-family:'IBM Plex Sans',sans-serif;font-size:13px;line-height:20px}*{box-sizing:border-box}a{color:inherit;text-underline-offset:3px}a:hover{opacity:.8}code{font-family:'IBM Plex Mono',monospace;font-size:12px}</style>
</helmet>
<div ref="{{ rootRef }}" data-screen-label="Back-office screens" style="--ui-bg:#ffffff;--ui-bg-muted:#fafafa;--ui-bg-elevated:#f3f2f0;--ui-bg-accented:#e6e4e0;--ui-bg-inverted:#111111;--ui-text-dimmed:#999999;--ui-text-muted:#666666;--ui-text-toned:#444444;--ui-text:#222222;--ui-text-highlighted:#111111;--ui-text-inverted:#ffffff;--ui-border:#e6e4e0;--ui-border-muted:#f3f2f0;--ui-border-accented:#8a8783;--ui-primary:#111111;--ui-secondary:#635643;--ui-success:#315b33;--ui-info:#34536d;--ui-warning:#6c4700;--ui-error:#882c21;--rc-sand:#e8dfd1;--rc-on-sand:#111111;--rc-success-soft:#eaefea;--rc-info-soft:#ebeef0;--rc-warning-soft:#f0ede6;--rc-error-soft:#f3eae9;--rc-overlay:rgb(10 10 10 / 0.4);--rc-shadow-pop:0 4px 16px rgb(17 17 17 / 0.08), 0 1px 2px rgb(17 17 17 / 0.06);--rc-shadow-modal:0 16px 48px rgb(17 17 17 / 0.18);--rc-canvas:#ecebe8;background:var(--rc-canvas);color:var(--ui-text);padding:64px;display:flex;flex-direction:column;gap:72px;width:max-content;min-width:100%">
<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%, 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. 183 states in 16 groups. L7s is the pixel-gate reference for a single-language article list. 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">
<button role="radio" aria-checked="{{ m.on }}" onClick="{{ m.pick }}" style="all:unset;cursor:pointer;height:36px;padding:0 14px;display:flex;align-items:center;font-weight:500;font-size:14px;background:{{ m.bg }};color:{{ m.fg }}" style-focus-visible="outline:2px solid var(--ui-primary);outline-offset:2px">{{ m.label }}</button>
</sc-for>
</div>
</div>
<sc-for list="{{ groups }}" as="g" hint-placeholder-count="7">
<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><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><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>
</sc-for>
</div>
</section>
</sc-for>
<sc-for list="{{ tables }}" as="t" hint-placeholder-count="3"><section style="display:flex;flex-direction:column;gap:10px;max-width:1400px"><h2 style="margin:0;font:600 28px/36px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ t.title }}</h2><div style="background:var(--ui-bg);border:1px solid var(--ui-border)"><div style="display:grid;grid-template-columns:{{ t.w }};gap:16px;padding:8px 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500"><sc-for list="{{ t.cols }}" as="c" hint-placeholder-count="3"><span>{{ c }}</span></sc-for></div><sc-for list="{{ t.rows }}" as="r" hint-placeholder-count="3"><div style="display:grid;grid-template-columns:{{ t.w }};gap:16px;padding:10px 12px;border-top:1px solid var(--ui-border);line-height:20px"><sc-for list="{{ r }}" as="c" hint-placeholder-count="3"><span style="color:var(--ui-text-highlighted);text-wrap:pretty;min-width:0">{{ c }}</span></sc-for></div></sc-for></div></section></sc-for>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;theme&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;system&quot;,&quot;light&quot;,&quot;dark&quot;],&quot;default&quot;:&quot;system&quot;,&quot;tsType&quot;:&quot;'system' | 'light' | 'dark'&quot;}}">
class Component extends DCLogic {
state = { mode: this.props.theme ?? 'light' }
rootRef = React.createRef()
light = { '--ui-bg': '#ffffff', '--ui-bg-muted': '#fafafa', '--ui-bg-elevated': '#f3f2f0', '--ui-bg-accented': '#e6e4e0', '--ui-bg-inverted': '#111111', '--ui-text-dimmed': '#999999', '--ui-text-muted': '#666666', '--ui-text-toned': '#444444', '--ui-text': '#222222', '--ui-text-highlighted': '#111111', '--ui-text-inverted': '#ffffff', '--ui-border': '#e6e4e0', '--ui-border-muted': '#f3f2f0', '--ui-border-accented': '#8a8783', '--ui-primary': '#111111', '--ui-secondary': '#635643', '--ui-success': '#315b33', '--ui-info': '#34536d', '--ui-warning': '#6c4700', '--ui-error': '#882c21', '--rc-sand': '#e8dfd1', '--rc-on-sand': '#111111', '--rc-success-soft': '#eaefea', '--rc-info-soft': '#ebeef0', '--rc-warning-soft': '#f0ede6', '--rc-error-soft': '#f3eae9', '--rc-overlay': 'rgb(10 10 10 / 0.4)', '--rc-shadow-pop': '0 4px 16px rgb(17 17 17 / 0.08), 0 1px 2px rgb(17 17 17 / 0.06)', '--rc-shadow-modal': '0 16px 48px rgb(17 17 17 / 0.18)', '--rc-canvas': '#ecebe8' }
dark = { '--ui-bg': '#111111', '--ui-bg-muted': '#0a0a0a', '--ui-bg-elevated': '#1a1a1a', '--ui-bg-accented': '#222222', '--ui-bg-inverted': '#fafafa', '--ui-text-dimmed': '#666666', '--ui-text-muted': '#999999', '--ui-text-toned': '#cfccc7', '--ui-text': '#e6e4e0', '--ui-text-highlighted': '#fafafa', '--ui-text-inverted': '#111111', '--ui-border': '#333333', '--ui-border-muted': '#222222', '--ui-border-accented': '#666666', '--ui-primary': '#fafafa', '--ui-secondary': '#d4c5b0', '--ui-success': '#a3caa3', '--ui-info': '#a5c2db', '--ui-warning': '#dfb67d', '--ui-error': '#fba394', '--rc-sand': '#d4c5b0', '--rc-on-sand': '#111111', '--rc-success-soft': '#1f241f', '--rc-info-soft': '#202428', '--rc-warning-soft': '#2a251e', '--rc-error-soft': '#2c201f', '--rc-overlay': 'rgb(0 0 0 / 0.6)', '--rc-shadow-pop': '0 4px 16px rgb(0 0 0 / 0.5)', '--rc-shadow-modal': '0 16px 48px rgb(0 0 0 / 0.6)', '--rc-canvas': '#1c1c1c' }
resolved() {
if (this.state.mode === 'system') return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
return this.state.mode
}
apply() {
const el = this.rootRef.current
if (!el) return
const vars = this.resolved() === 'dark' ? this.dark : this.light
for (const k in vars) el.style.setProperty(k, vars[k])
document.body.style.background = vars['--rc-canvas']
}
componentDidMount() { this.apply() }
componentDidUpdate(p) {
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, 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.concat(this.computed()) }
}
}
</script>
</body>
</html>