427 lines
94 KiB
HTML
427 lines
94 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>
|
||
<sc-if value="{{ framed }}" hint-placeholder-val="{{ true }}"><dc-import name="AppFrame" s="{{ shell }}" mobile="{{ mob }}" slot-body="{{ main }}" slot-overlays="{{ ov }}" hint-size="100%,100%"></dc-import></sc-if>
|
||
<sc-if value="{{ bare }}"><div style="width:{{ bw }};height:{{ bh }};overflow:hidden;position:relative;background:var(--ui-bg-muted);color:var(--ui-text);font-family:'IBM Plex Sans',sans-serif;font-size:13px;line-height:20px">{{ main }}</div></sc-if>
|
||
</x-dc>
|
||
<script type="text/x-dc" data-dc-script data-props="{"$preview":{"width":1440,"height":900},"s":{"editor":null,"tsType":"object"},"mobile":{"editor":null,"tsType":"boolean"}}">
|
||
class Component extends DCLogic {
|
||
renderVals() {
|
||
const s = this.props.s || {}, m = !!this.props.mobile
|
||
const P = t => Object.fromEntries(t.split(';').filter(x => x.trim()).map(x => { const i = x.indexOf(':'); const k = x.slice(0, i).trim(); return [k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase()), x.slice(i + 1).trim()] }))
|
||
const h = React.createElement
|
||
const D = (t, ...c) => h('div', { style: P(t) }, ...c)
|
||
const S = (t, ...c) => h('span', { style: P(t) }, ...c)
|
||
const I = (n, z = 16, t = '') => h('span', { 'aria-hidden': 'true', style: { display: 'inline-block', flexShrink: 0, width: z, height: z, backgroundColor: 'currentColor', WebkitMask: 'url(https://api.iconify.design/lucide/' + n + '.svg) center / contain no-repeat', mask: 'url(https://api.iconify.design/lucide/' + n + '.svg) center / contain no-repeat', ...(t ? P(t) : {}) } })
|
||
const mono = 'font-family:\'IBM Plex Mono\',monospace;'
|
||
const bh = m ? 44 : 32, ih = m ? 44 : 32, fs = m ? 16 : 13
|
||
const B = (label, o = {}) => {
|
||
const k = o.k || 'outline'
|
||
const bg = k === 'primary' ? 'var(--ui-primary)' : k === 'danger' ? 'var(--ui-error)' : k === 'ghost' ? 'transparent' : 'var(--ui-bg)'
|
||
const fg = k === 'primary' || k === 'danger' ? 'var(--ui-bg)' : k === 'del' ? 'var(--ui-error)' : 'var(--ui-text-highlighted)'
|
||
const bd = k === 'primary' || k === 'danger' ? bg : k === 'ghost' ? 'transparent' : 'var(--ui-border-accented)'
|
||
return S('height:' + (o.h || bh) + 'px;' + (o.w ? 'width:' + o.w + 'px;' : '') + 'padding:0 ' + (o.px ?? 12) + 'px;display:flex;align-items:center;justify-content:center;gap:6px;background:' + bg + ';color:' + fg + ';border:1px solid ' + bd + ';font-weight:500;font-size:' + (o.fs || 13) + 'px;white-space:nowrap;flex-shrink:0;opacity:' + (o.dis ? 0.4 : 1) + ';' + (o.full ? 'flex:1;' : '') + (o.ring ? 'outline:2px solid var(--ui-primary);outline-offset:2px;' : ''), o.icon && I(o.icon, o.z || 16), label)
|
||
}
|
||
const IB = (ic, o = {}) => B(null, { k: 'ghost', icon: ic, w: o.w || (m ? 44 : 28), h: o.h || (m ? 44 : 28), px: 0, dis: o.dis, ring: o.ring, z: o.z || 16 })
|
||
const Alert = (tone, icon, title, text, ...actions) => D('display:flex;gap:10px;padding:12px 14px;background:var(--rc-' + tone + '-soft);color:var(--ui-' + tone + ')', S('display:flex;padding-top:2px', I(icon)), D('flex:1;display:flex;flex-direction:column;gap:8px', D('display:flex;flex-direction:column;gap:2px', S('font-weight:600', title), text && S('color:var(--ui-text-toned);text-wrap:pretty', text)), actions.length > 0 && D('display:flex;gap:8px;flex-wrap:wrap', ...actions.map((a, i) => B(a, { icon: i === 0 && /again|Retry|Reload/.test(a) ? 'rotate-cw' : null, z: 14, h: m ? 44 : 28, fs: 12, px: 10, k: 'outline' })))))
|
||
const Later = (t = 'Later') => S('align-self:flex-start;height:20px;padding:0 6px;display:inline-flex;align-items:center;border:1px dashed var(--ui-border-accented);color:var(--ui-text-toned);white-space:nowrap;flex-shrink:0;font:500 11px/16px \'IBM Plex Mono\',monospace', t)
|
||
const BDG = { req: 'border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)', uniq: 'border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)', neu: 'background:var(--rc-sand);color:var(--rc-on-sand)', chg: 'background:var(--rc-info-soft);color:var(--ui-info)', err: 'background:var(--rc-error-soft);color:var(--ui-error)', warn: 'background:var(--rc-warning-soft);color:var(--ui-warning)', built: 'border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)', ok: 'background:var(--rc-success-soft);color:var(--ui-success)' }
|
||
const Bd = (t, k, ic) => S('height:20px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:500;white-space:nowrap;flex-shrink:0;' + BDG[k], ic && I(ic, 12), t)
|
||
const Seg = (opts, on, w) => D('display:flex;border:1px solid var(--ui-border-accented);width:' + (w || '100%'), ...opts.map(([k, t]) => S('flex:1;height:' + bh + 'px;padding:0 10px;display:flex;align-items:center;justify-content:center;font-weight:' + (on === k ? 500 : 400) + ';background:' + (on === k ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (on === k ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)') + ';font-size:' + fs + 'px', t)))
|
||
const Sw = (on, lab, help, dis) => D('display:flex;gap:12px;align-items:flex-start;opacity:' + (dis ? 0.6 : 1), S('width:32px;height:18px;margin-top:2px;padding:2px;display:flex;flex-shrink:0;justify-content:' + (on ? 'flex-end' : 'flex-start') + ';background:' + (on ? 'var(--ui-primary)' : 'var(--ui-bg-accented)') + ';' + (on ? '' : 'outline:1px solid var(--ui-border-accented);outline-offset:-1px'), S('width:14px;height:14px;background:var(--ui-bg)')), D('display:flex;flex-direction:column;gap:2px', lab && S('font-weight:500;color:var(--ui-text-highlighted)', lab), help && S('color:var(--ui-text-muted);font-size:12px;line-height:18px;text-wrap:pretty', help)))
|
||
const Chk = on => S('width:16px;height:16px;flex-shrink:0;display:inline-flex;align-items:center;justify-content:center;box-sizing:border-box;border:1px solid ' + (on ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (on ? 'var(--ui-primary)' : 'var(--ui-bg)') + ';color:var(--ui-bg)', on && I('check', 12))
|
||
const Skel = (w, hh = 14) => S('display:block;height:' + hh + 'px;width:' + w + ';background:var(--ui-bg-accented)')
|
||
const F = (lab, val, o = {}) => D('display:flex;flex-direction:column;gap:6px;min-width:0', (lab || o.count) && D('display:flex;justify-content:space-between;gap:8px', S('font-weight:500;color:var(--ui-text-highlighted)', lab, o.req && ' *', o.opt && S('font-weight:400;color:var(--ui-text-muted)', ' (optional)')), o.count && S(mono + 'font-size:12px;color:' + (o.countErr ? 'var(--ui-error)' : 'var(--ui-text-muted)'), o.count)),
|
||
D('display:flex;align-items:' + (o.area ? 'flex-start' : 'center') + ';min-height:' + (o.area ? (m ? 120 : 88) : ih) + 'px;border:1px solid ' + (o.err ? 'var(--ui-error)' : o.focus ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';' + (o.err ? 'box-shadow:inset 0 0 0 1px var(--ui-error);' : o.focus ? 'box-shadow:inset 0 0 0 1px var(--ui-primary);' : '') + 'background:' + (o.dis || o.lock ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';font-size:' + fs + 'px;opacity:' + (o.dis ? 0.6 : 1), o.pre, S('flex:1;min-width:0;padding:' + (o.area ? '8px 10px' : '0 10px') + ';color:' + (o.ph || val === '' ? 'var(--ui-text-muted)' : 'var(--ui-text-highlighted)') + ';' + (o.area ? '' : 'overflow:hidden;text-overflow:ellipsis;white-space:nowrap;') + (o.mono ? mono + 'font-size:' + (m ? 14 : 12) + 'px;' : ''), val === '' && o.phText ? o.phText : val), o.auto && Bd('Auto', 'neu'), o.auto && I('pencil', 14, 'margin:0 10px 0 8px;color:var(--ui-text-muted)'), o.lock && I('lock', 14, 'margin-right:10px;color:var(--ui-text-muted)'), o.sel && I('chevron-down', 14, 'margin-right:10px;color:var(--ui-text-muted)'), o.suffix && S('padding-right:10px;color:var(--ui-text-muted);font-size:12px', o.suffix), o.ico && I(o.ico, 14, 'margin-right:10px;color:var(--ui-text-muted)')),
|
||
o.err && S('display:flex;gap:6px;align-items:flex-start;color:var(--ui-error);font-size:12px;line-height:18px', I('circle-alert', 14, 'margin-top:2px'), o.err), o.help && S('color:var(--ui-text-muted);font-size:12px;line-height:18px;text-wrap:pretty', o.help))
|
||
const Shell = (o, body) => D('position:absolute;inset:0;z-index:' + (o.z || 50) + ';background:var(--rc-overlay);display:flex;align-items:' + (m ? 'flex-end' : 'flex-start') + ';justify-content:center;padding:' + (m ? '0' : (o.top || 140) + 'px 16px 16px'),
|
||
D('width:' + (m ? '100%' : (o.w || 460) + 'px') + ';max-height:100%;background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column', m && D('display:flex;justify-content:center;padding-top:8px;flex-shrink:0', S('display:block;width:36px;height:4px;background:var(--ui-border-accented)')),
|
||
D('padding:20px 24px 0;display:flex;align-items:flex-start;gap:12px', D('flex:1;display:flex;flex-direction:column;gap:4px', S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', o.title), o.desc && S('color:var(--ui-text-muted);text-wrap:pretty', o.desc)), o.later && Later(), o.close && S('width:' + (m ? 44 : 28) + 'px;height:' + (m ? 44 : 28) + 'px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x'))),
|
||
D('padding:20px 24px 0;display:flex;flex-direction:column;gap:16px;min-height:0;overflow:hidden', body),
|
||
D('padding:20px 24px;display:flex;flex-direction:' + (m ? 'column-reverse' : 'row') + ';justify-content:flex-end;gap:8px;flex-shrink:0', o.cancel && B(o.cancel, { dis: o.loading }), o.confirm && S('height:' + bh + 'px;padding:0 12px;display:flex;align-items:center;justify-content:center;gap:6px;background:' + (o.tone === 'danger' ? 'var(--ui-error)' : 'var(--ui-primary)') + ';color:var(--ui-bg);font-weight:500;white-space:nowrap;opacity:' + (o.cdis ? 0.4 : 1), o.loading && I('loader-circle'), o.confirm))))
|
||
const Sheet = (o, body, foot) => D('position:absolute;inset:0;z-index:' + (o.z || 40) + ';display:flex;align-items:flex-end;background:var(--rc-overlay)',
|
||
D('width:100%;height:92%;background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column', D('display:flex;justify-content:center;padding-top:8px;flex-shrink:0', S('display:block;width:36px;height:4px;background:var(--ui-border-accented)')),
|
||
D('flex-shrink:0;display:flex;align-items:center;gap:12px;padding:4px 6px 4px 16px;height:56px;border-bottom:1px solid var(--ui-border)', S('flex:1;min-width:0;font:600 16px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', o.title), o.later && Later(), S('width:44px;height:44px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('x'))),
|
||
D('flex:1;min-height:0;overflow:hidden;padding:16px;display:flex;flex-direction:column;gap:16px', body), foot && D('flex-shrink:0;padding:12px 16px;border-top:1px solid var(--ui-border);display:flex;gap:8px', foot)))
|
||
const Toast = (tone, icon, title, text, action) => D('position:absolute;z-index:60;display:flex;gap:10px;padding:12px 14px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);' + (m ? 'left:16px;right:16px;bottom:' + (s.toastB || 16) + 'px' : 'right:24px;bottom:24px;width:356px'), S('margin-top:2px;display:flex;color:var(--ui-' + tone + ')', I(icon)), D('flex:1;display:flex;flex-direction:column;gap:6px', D('display:flex;flex-direction:column;gap:2px', S('font-weight:600;color:var(--ui-text-highlighted)', title), S('color:var(--ui-text-muted)', text)), action && B(action, { icon: 'rotate-cw', z: 14, h: m ? 44 : 28, fs: 12, px: 10 })), I('x'))
|
||
const Offline = () => D('flex-shrink:0;display:flex;align-items:center;gap:10px;padding:10px 16px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted)', I('wifi-off'), S('flex:1', 'You\'re offline. Changes will be saved when you reconnect.'))
|
||
const Tip = t => D('position:absolute;z-index:30;width:260px;padding:8px 10px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font-size:12px;line-height:18px;box-shadow:var(--rc-shadow-pop)', t)
|
||
const Page = (pad, ...c) => D('flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;position:relative;overflow:hidden;padding:' + pad + ';gap:16px', ...c)
|
||
const Title = (t, sub, ...acts) => D('display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap;flex-shrink:0', D('flex:1;min-width:200px;display:flex;flex-direction:column;gap:2px', !m && S('font:600 20px/28px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', t), sub && S('color:var(--ui-text-muted)', sub)), ...acts)
|
||
const pad = m ? '16px' : '24px'
|
||
const Fab = l => m && D('position:absolute;left:16px;right:16px;bottom:16px;height:48px;z-index:5;display:flex;align-items:center;justify-content:center;gap:8px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;font-size:15px;box-shadow:var(--rc-shadow-pop)', I('plus', 18), l)
|
||
const Center = (icon, title, text, ...acts) => D('flex:1;display:flex;align-items:center;justify-content:center;padding:24px', D('max-width:420px;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center', S('width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated);color:var(--ui-text-toned)', I(icon, 20)), S('font:600 24px/32px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', title), S('color:var(--ui-text-muted);text-wrap:pretty', text), D('display:flex;gap:8px', ...acts)))
|
||
const Empty = (icon, title, text, action) => D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:40px 24px;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center', S('width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated);color:var(--ui-text-toned)', I(icon, 20)), S('font-weight:600;font-size:15px;color:var(--ui-text-highlighted)', title), S('color:var(--ui-text-muted);max-width:360px;text-wrap:pretty', text), action && B(action, { k: 'primary', icon: 'plus', px: 16 }))
|
||
|
||
// ---------- data
|
||
const FT = { text: ['type', 'Text', 'Short text, like a title or a name'], richtext: ['pilcrow', 'Rich text', 'Long text with formatting, written in Markdown'], number: ['hash', 'Number', 'Whole or decimal numbers'], boolean: ['toggle-left', 'Yes or no', 'An on or off switch'], date: ['calendar', 'Date', 'A calendar day, without a time'], select: ['list', 'Choice', 'One option from a list you define'] }
|
||
const EV = [
|
||
{ key: 'title', label: 'Title', type: 'text', req: true, uniq: true, max: 80 }, { key: 'starts_on', label: 'Starts on', type: 'date', req: true }, { key: 'ends_on', label: 'Ends on', type: 'date' },
|
||
{ key: 'online', label: 'Online', type: 'boolean', def: false }, { key: 'capacity', label: 'Capacity', type: 'number', min: 1, max: 5000 }, { key: 'category', label: 'Category', type: 'select', opts: ['Talk', 'Workshop', 'Meetup'] }, { key: 'description', label: 'Description', type: 'richtext' }].map(f => ({ ...f, saved: true }))
|
||
const RC = [{ key: 'title', label: 'Title', type: 'text', req: true }, { key: 'servings', label: 'Servings', type: 'number', min: 1 }, { key: 'vegetarian', label: 'Vegetarian', type: 'boolean' }, { key: 'method', label: 'Method', type: 'richtext' }].map(f => ({ ...f, saved: true }))
|
||
const SPK = { key: 'speaker', label: 'Speaker', type: 'text', isNew: true }, ROOM = { key: 'room', label: 'Room', type: 'select', req: true, def: 'Main hall', opts: ['Main hall', 'Studio'], isNew: true }
|
||
const CAPC = { ...EV[4], max: 2000, changed: true }
|
||
const DIRTY = [EV[0], EV[1], EV[2], EV[3], CAPC, EV[5], SPK, ROOM]
|
||
const CT = n => ['Events', 'Recipes', 'Press mentions', 'Frequently asked questions and answers', 'Job openings', 'Testimonials', 'Suppliers', 'Certifications', 'Case studies', 'Glossary terms', 'Partners', 'Downloads', 'Locations', 'Awards'].slice(0, n).map(x => ({ key: x === 'Events' ? 'events' : x.toLowerCase().replace(/[^a-z0-9]+/g, '-'), name: x, icon: 'files' }))
|
||
const shell = { kind: 'slot', display: 'Marta Alves', role: s.role || 'owner', content: CT(s.many ? 14 : 3), contentOn: 'events', navBadge: false }
|
||
const BUILTIN = [['Articles', 'articles', 5], ['Authors', 'authors', 5], ['Sites', 'sites', 8], ['Media', 'media', 6], ['Users', 'users', 6]]
|
||
const CUSTOM = [['Events', 'events', 7, '15 Oct 2026'], ['Recipes', 'recipes', 4, '2 Oct 2026'], ['Press mentions', 'press-mentions', 5, '18 Sep 2026']]
|
||
const ROWS = [
|
||
{ id: 42, title: 'Lime plaster workshop', starts_on: '21 Nov 2026', ends_on: '22 Nov 2026', online: false, capacity: 18, category: 'Workshop', description: 'Two days of hands-on work with lime putty and old stone.', c: '12 Oct 2026', u: '15 Oct 2026, 11:02', by: 'Marta Alves' },
|
||
{ id: 41, title: 'Heat pumps: what the numbers say', starts_on: '3 Nov 2026', ends_on: '', online: true, capacity: 500, category: 'Talk', description: 'A look at running costs after two winters.', c: '8 Oct 2026', u: '14 Oct 2026, 16:40', by: 'Jonas Pereira' },
|
||
{ id: 40, title: 'Open day at the Rue Verte site', starts_on: '12 Nov 2026', ends_on: '12 Nov 2026', online: false, capacity: 120, category: 'Meetup', description: '', c: '2 Oct 2026', u: '12 Oct 2026, 09:15', by: 'Inès Duarte' },
|
||
{ id: 39, title: 'Winter windows clinic', starts_on: '5 Dec 2026', ends_on: '', online: false, capacity: null, category: 'Workshop', description: 'Bring a window, leave with a plan.', c: '30 Sep 2026', u: '9 Oct 2026, 14:22', by: 'Marta Alves' },
|
||
{ id: 38, title: 'Timber joinery for beginners', starts_on: '14 Jan 2027', ends_on: '15 Jan 2027', online: false, capacity: 12, category: 'Workshop', description: 'Mortise, tenon and a lot of patience.', c: '28 Sep 2026', u: '7 Oct 2026, 10:05', by: 'Inès Duarte' },
|
||
{ id: 37, title: 'Q&A with the site managers', starts_on: '28 Oct 2026', ends_on: '', online: true, capacity: null, category: 'Meetup', description: '', c: '21 Sep 2026', u: '2 Oct 2026, 17:48', by: 'Jonas Pereira' },
|
||
{ id: 36, title: 'Reclaimed floorboards talk', starts_on: '17 Oct 2026', ends_on: '', online: false, capacity: 60, category: 'Talk', description: 'Where to find them and how to lay them.', c: '14 Sep 2026', u: '29 Sep 2026, 08:30', by: 'Marta Alves' }]
|
||
const mdSample = 'Two days of hands-on work with **lime putty** and old stone.\n\n- Bring work clothes\n- Lunch is included'
|
||
const sampleVal = f => f.type === 'text' ? (f.def ?? 'Example text') : f.type === 'richtext' ? (f.def ?? 'Markdown text') : f.type === 'number' ? (f.def ?? f.min ?? 0) : f.type === 'boolean' ? (f.def ?? false) : f.type === 'date' ? (f.def ?? '2026-11-21') : (f.def ?? (f.opts && f.opts[0]) ?? null)
|
||
const apiJson = (fs_, key) => JSON.stringify({ id: 42, content_type: key, data: Object.fromEntries(fs_.map(f => [f.key, sampleVal(f)])), created_by: 3, updated_by: 3, created_at: '2026-10-15T11:02:00Z', updated_at: '2026-10-15T11:02:00Z' }, null, 2)
|
||
const MM = t => t === 'number' ? ['Lowest value', 'Highest value', ''] : t === 'date' ? ['Earliest date', 'Latest date', ''] : ['Shortest', 'Longest', 'characters']
|
||
const dot = (t, ic) => S('display:inline-flex;align-items:center;gap:6px', I(ic, 14), t)
|
||
|
||
// ---------- inspector
|
||
const Insp = (f, o = {}) => {
|
||
const t = f.type, e = f.err || {}, one = m || o.one, locked = !!f.saved
|
||
const [mnL, mxL, suf] = MM(t), canU = ['text', 'number', 'date', 'select'].includes(t), hasMM = ['text', 'richtext', 'number', 'date'].includes(t)
|
||
const g2 = one ? '1fr' : '1fr 1fr'
|
||
const tile = k => { const on = k === t, dim = locked && !on; return D('display:flex;gap:10px;align-items:flex-start;padding:10px;min-height:56px;border:1px solid ' + (on ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (on ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';opacity:' + (dim ? 0.4 : 1), I(FT[k][0], 16, 'margin-top:2px'), D('display:flex;flex-direction:column;min-width:0', S('font-weight:500', FT[k][1]), S('font-size:12px;line-height:16px;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)') + ';text-wrap:pretty', FT[k][2]))) }
|
||
const mmErr = e.min || ''
|
||
const minV = f.min != null ? String(f.min) : '', maxV = f.max != null ? String(f.max) : ''
|
||
const defCtl = t === 'boolean' ? Sw(!!f.def, 'On by default', 'New entries start with this switch ' + (f.def ? 'on' : 'off') + ' when the field is left out.') : F('Default value', f.def != null ? String(f.def) : '', { opt: true, ph: f.def == null, phText: t === 'select' ? 'None' : t === 'date' ? 'No default' : 'No default', sel: t === 'select', ico: t === 'date' ? 'calendar' : null, area: t === 'richtext', err: e.def, dis: !!f.uniq, help: f.uniq ? 'A field with unique values can\'t have a default.' : e.def ? '' : 'Used when an entry is created and this field is left out.' })
|
||
const left = D('display:flex;flex-direction:column;gap:14px;min-width:0', Sw(!!f.req, 'Required', 'An entry can\'t be saved with this field empty.'), Sw(!!f.uniq, 'Unique values', canU ? 'Each entry needs a different value.' : t === 'richtext' ? 'Not available for rich text.' : 'Not available for yes or no fields.', !canU), defCtl)
|
||
const right = hasMM ? D('display:flex;flex-direction:column;gap:14px;min-width:0', F(mnL, minV, { suffix: suf, ph: minV === '', phText: 'No minimum', err: mmErr, help: mmErr ? '' : null }), F(mxL, maxV, { suffix: suf, ph: maxV === '', phText: 'No maximum', help: mmErr ? '' : t === 'text' || t === 'richtext' ? 'Counted in characters. Entries show a counter.' : t === 'number' ? 'Entries outside the range are refused.' : 'Entries outside the range are refused.' })) : D('display:flex;flex-direction:column;gap:6px;min-width:0', S('font-weight:500;color:var(--ui-text-highlighted)', 'Limits'), S('color:var(--ui-text-muted);font-size:12px;line-height:18px', t === 'boolean' ? 'Yes or no fields have no limits.' : 'Limits come from the choices below.'))
|
||
const ch = t === 'select' ? Choices(f) : null
|
||
return D('display:flex;flex-direction:column;gap:' + (m ? 16 : 14) + 'px;min-width:0',
|
||
D('display:grid;grid-template-columns:' + g2 + ';gap:14px', F('Label', f.label, { req: true, err: e.label, focus: o.focus === 'label', ph: !f.label, phText: 'For example, Title' }),
|
||
D('display:flex;flex-direction:column;gap:6px;min-width:0', F('Key', f.key || '', { mono: true, auto: !locked && !e.key && !!f.key, lock: locked, err: e.key, ph: !f.key, phText: 'title', dis: false, help: locked ? 'Used in the API, so it can\'t change.' : e.key ? '' : 'The name used in the API. Lowercase letters, digits and underscores.' }))),
|
||
D('display:flex;flex-direction:column;gap:6px', D('display:flex;justify-content:space-between;gap:8px', S('font-weight:500;color:var(--ui-text-highlighted)', 'Type'), locked && S('color:var(--ui-text-muted);font-size:12px;display:flex;gap:4px;align-items:center', I('lock', 12), 'Can\'t change after saving')), D('display:grid;grid-template-columns:repeat(' + (one ? 2 : 3) + ',minmax(0,1fr));gap:8px', ...Object.keys(FT).map(tile))),
|
||
D('display:grid;grid-template-columns:' + g2 + ';gap:14px;align-items:start', left, right),
|
||
ch)
|
||
}
|
||
const Choices = f => {
|
||
const e = f.err || {}, ops = f.opts || [], bad = e.choices || {}
|
||
return D('display:flex;flex-direction:column;gap:8px', D('display:flex;justify-content:space-between', S('font-weight:500;color:var(--ui-text-highlighted)', 'Choices', ' *'), S(mono + 'font-size:12px;color:var(--ui-text-muted)', ops.length + ' of 50')),
|
||
...ops.map((o_, i) => D('display:flex;flex-direction:column;gap:4px', D('display:flex;align-items:center;gap:4px', S('width:' + (m ? 44 : 28) + 'px;height:' + (m ? 44 : 28) + 'px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('grip-vertical', 16)), D('flex:1;height:' + ih + 'px;display:flex;align-items:center;padding:0 10px;font-size:' + fs + 'px;border:1px solid ' + (bad[i] ? 'var(--ui-error)' : 'var(--ui-border-accented)') + ';background:var(--ui-bg);color:' + (o_ ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)'), o_ || 'Choice'), IB('x')), bad[i] && S('margin-left:' + (m ? 48 : 32) + 'px;display:flex;gap:6px;color:var(--ui-error);font-size:12px;line-height:18px', I('circle-alert', 14, 'margin-top:2px'), bad[i]))),
|
||
D('display:flex;gap:8px;padding-left:' + (m ? 48 : 32) + 'px', D('flex:1;height:' + ih + 'px;display:flex;align-items:center;padding:0 10px;border:1px dashed var(--ui-border-accented);color:var(--ui-text-muted);font-size:' + fs + 'px', 'Add a choice'), B('Add', { icon: 'plus' })),
|
||
S('color:var(--ui-text-muted);font-size:12px;line-height:18px', 'Add 1 to 50 choices. Each one needs a different name. Removing a choice that entries use will be refused when you save.'))
|
||
}
|
||
|
||
// ---------- field list
|
||
const FRow = (f, i, o = {}) => {
|
||
const on = o.sel === i, err = !!f.err, kb = o.kb === i
|
||
const rm = on && D('display:flex;gap:2px;margin-left:auto', IB('chevron-up', { dis: i === 0 }), IB('chevron-down'), IB('trash-2'))
|
||
return D('display:flex;align-items:center;gap:' + (m ? 4 : 2) + 'px;min-height:' + (m ? 64 : 56) + 'px;padding:6px 8px 6px 4px;border:1px solid ' + (err ? 'var(--ui-error)' : kb ? 'var(--ui-primary)' : 'var(--ui-border)') + ';' + (kb ? 'outline:2px solid var(--ui-primary);outline-offset:-1px;box-shadow:var(--rc-shadow-pop);' : '') + 'background:' + (on ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';opacity:' + (o.dim ? 0.6 : 1),
|
||
S('width:' + (m ? 36 : 24) + 'px;height:32px;display:flex;align-items:center;justify-content:center;flex-shrink:0;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)'), I('grip-vertical', 16)),
|
||
S('width:28px;height:28px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:' + (on ? 'var(--ui-bg)' : 'var(--ui-bg-elevated)') + ';color:var(--ui-text-highlighted)', I(FT[f.type][0], 16)),
|
||
D('flex:1;min-width:0;display:flex;flex-direction:column;margin-left:8px', S('font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', f.label || 'Untitled field'), S(mono + 'font-size:12px;line-height:16px;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)') + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', f.key || '–')),
|
||
!on && D('display:flex;gap:4px;flex-shrink:0;align-items:center', f.isNew && Bd('New', 'neu'), f.changed && Bd('Changed', 'chg'), o.cause && f.changed && Bd('Possible cause', 'warn', 'triangle-alert'), f.req && Bd('Required', 'req'), f.uniq && Bd('Unique', 'uniq'), err && I('circle-alert', 16, 'color:var(--ui-error)')),
|
||
on && D('display:flex;gap:4px;flex-shrink:0;align-items:center;margin-right:2px', f.isNew && Bd('New', 'neu'), f.changed && Bd('Changed', 'chg'), f.req && Bd('Required', 'req'), f.uniq && Bd('Unique', 'uniq')), on && !m && rm)
|
||
}
|
||
const ApiPanel = (fl, key, open) => {
|
||
const routes = [['GET', '/v1/content-type/' + key + '/entry'], ['POST', '/v1/content-type/' + key + '/entry'], ['GET', '/v1/content-type/' + key + '/entry/{id}'], ['PUT', '/v1/content-type/' + key + '/entry/{id}'], ['DELETE', '/v1/content-type/' + key + '/entry/{id}']]
|
||
const head = D('height:44px;flex-shrink:0;display:flex;align-items:center;gap:10px;padding:0 ' + (m ? 12 : 24) + 'px;border-top:1px solid var(--ui-border);background:var(--ui-bg);color:var(--ui-text-highlighted)', I(open ? 'chevron-down' : 'chevron-right', 14), S('font-weight:600', 'API preview'), !m && S(mono + 'font-size:12px;color:var(--ui-text-muted);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', 'GET /v1/content-type/' + key + '/entry'), m && S('flex:1'), B('Copy JSON', { icon: 'copy', h: m ? 44 : 28, fs: 12, px: 10 }))
|
||
if (!open) return head
|
||
return D('flex-shrink:0;display:flex;flex-direction:column', head, D('display:grid;grid-template-columns:1fr 1.4fr;gap:16px;height:248px;padding:0 24px 16px;background:var(--ui-bg)', D('display:flex;flex-direction:column;gap:6px;min-width:0', S('color:var(--ui-text-muted);font-size:12px', 'Where entries of this type live'), D('display:flex;flex-direction:column;gap:2px;padding:10px 12px;background:var(--ui-bg-elevated);border:1px solid var(--ui-border)', ...routes.map(r => S(mono + 'font-size:12px;line-height:19px;color:var(--ui-text-highlighted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis', S('display:inline-block;width:56px;color:var(--ui-text-muted)', r[0]), r[1]))), S('color:var(--ui-text-muted);font-size:12px;line-height:18px;text-wrap:pretty', 'Updates as you edit. Fields you add or remove here appear after you save.')), D('display:flex;flex-direction:column;gap:6px;min-width:0', S('color:var(--ui-text-muted);font-size:12px', 'A sample entry. Values come from each field\'s type and default.'), S('display:block;flex:1;overflow:hidden;padding:10px 12px;background:var(--ui-bg-elevated);border:1px solid var(--ui-border);white-space:pre;' + mono + 'font-size:12px;line-height:19px;color:var(--ui-text-highlighted)', apiJson(fl, key)))))
|
||
}
|
||
const stChip = (kind, n) => { const M = { clean: ['check', 'Saved', 'var(--ui-text-muted)'], dirty: ['circle-dot', 'Unsaved changes', 'var(--ui-warning)'], saving: ['loader-circle', 'Saving…', 'var(--ui-text-muted)'], saved: ['check', 'Saved at 11:02', 'var(--ui-text-muted)'], fail: ['circle-alert', 'Save failed', 'var(--ui-error)'], offline: ['wifi-off', 'Waiting for connection', 'var(--ui-text-muted)'], errs: ['circle-alert', n + (n === 1 ? ' problem' : ' problems') + ' to fix', 'var(--ui-error)'] }[kind]; return S('display:flex;align-items:center;gap:6px;white-space:nowrap;font-size:12px;color:' + M[2], I(M[0], 14), M[1]) }
|
||
|
||
// ---------- entry controls
|
||
const hint = f => { const t = f.type; if (t === 'text' || t === 'richtext') return [f.min && 'At least ' + f.min + ' characters', f.max && 'Up to ' + f.max + ' characters', f.uniq && 'Each entry needs a different one'].filter(Boolean).join('. ') + (f.min || f.max || f.uniq ? '.' : ''); if (t === 'number') return f.min != null && f.max != null ? 'Between ' + f.min + ' and ' + f.max + '.' : f.min != null ? 'At least ' + f.min + '.' : f.max != null ? 'At most ' + f.max + '.' : ''; if (t === 'date') return [f.min && 'From ' + f.min, f.max && 'until ' + f.max].filter(Boolean).join(' ') ; return t === 'select' ? '' : '' }
|
||
const MdBox = (o, val) => D('display:flex;flex-direction:column;border:1px solid ' + (o.err ? 'var(--ui-error)' : o.focus ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (o.dis ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + (o.err ? ';box-shadow:inset 0 0 0 1px var(--ui-error)' : ''),
|
||
D('display:flex;align-items:center;border-bottom:1px solid var(--ui-border);padding:0 6px;gap:2px;overflow:hidden', D('display:flex;gap:14px;padding:0 6px;margin-right:8px', ...[['write', 'Write'], ['preview', 'Preview']].map(([k, t]) => S('height:' + (m ? 44 : 32) + 'px;display:flex;align-items:center;font-weight:' + (o.tab === k || (!o.tab && k === 'write') ? 500 : 400) + ';color:' + (o.tab === k || (!o.tab && k === 'write') ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)') + ';border-bottom:2px solid ' + (o.tab === k || (!o.tab && k === 'write') ? 'var(--ui-primary)' : 'transparent'), t))), ...(o.tab === 'preview' ? [] : ['bold', 'italic', 'heading', 'link', 'list'].map(ic => S('width:' + (m ? 40 : 28) + 'px;height:' + (m ? 40 : 28) + 'px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned)', I(ic, 16))))),
|
||
o.tab === 'preview' ? D('padding:10px 12px;min-height:' + (m ? 120 : 96) + 'px;display:flex;flex-direction:column;gap:8px;font-size:' + fs + 'px', S('', 'Two days of hands-on work with ', S('font-weight:600', 'lime putty'), ' and old stone.'), D('display:flex;flex-direction:column;gap:2px;padding-left:18px', S('display:list-item;list-style:square', 'Bring work clothes'), S('display:list-item;list-style:square', 'Lunch is included'))) : S('display:block;white-space:pre-wrap;padding:8px 10px;min-height:' + (m ? 120 : 96) + 'px;font-size:' + (m ? 14 : 12) + 'px;line-height:' + (m ? 22 : 19) + 'px;' + mono + 'color:' + (val ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)'), val || 'Write in Markdown'))
|
||
const Cal = () => { const cells = [...Array(6).fill(''), ...Array.from({ length: 30 }, (_, i) => i + 1)]; return D('width:' + (m ? 280 : 252) + 'px;padding:8px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop)', D('display:flex;align-items:center;justify-content:space-between;height:32px', IB('chevron-left'), S('font-weight:600', 'November 2026'), IB('chevron-right')), D('display:grid;grid-template-columns:repeat(7,1fr)', ...['Mo', 'Tu', 'We', 'Th', 'Fr', 'Sa', 'Su'].map(d => S('height:28px;display:flex;align-items:center;justify-content:center;font-size:12px;color:var(--ui-text-muted)', d)), ...cells.map(c => S('height:' + (m ? 34 : 30) + 'px;display:flex;align-items:center;justify-content:center;background:' + (c === 21 ? 'var(--ui-primary)' : 'transparent') + ';color:' + (c === 21 ? 'var(--ui-bg)' : 'var(--ui-text-highlighted)'), c))) ) }
|
||
const Ctl = (f, o = {}) => {
|
||
const t = f.type, v = o.v == null ? '' : String(o.v)
|
||
const lab = D('display:flex;justify-content:space-between;gap:8px', S('font-weight:500;color:var(--ui-text-highlighted)', f.label, f.req && ' *'), o.count && S(mono + 'font-size:12px;color:' + (o.countErr ? 'var(--ui-error)' : 'var(--ui-text-muted)'), o.count), o.def && Bd('Default', 'neu'))
|
||
const bd = o.err ? 'var(--ui-error)' : o.focus ? 'var(--ui-primary)' : 'var(--ui-border-accented)'
|
||
const box = (...inner) => D('display:flex;align-items:center;min-height:' + ih + 'px;border:1px solid ' + bd + ';background:' + (o.dis ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';font-size:' + fs + 'px;' + (o.err ? 'box-shadow:inset 0 0 0 1px var(--ui-error);' : o.focus ? 'box-shadow:inset 0 0 0 1px var(--ui-primary);' : ''), ...inner)
|
||
const txt = (val, ph) => S('flex:1;min-width:0;padding:0 10px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:' + (val ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)'), val || ph || '')
|
||
let ctl
|
||
if (t === 'boolean') ctl = Sw(v === 'true', v === 'true' ? 'On' : 'Off', null, o.dis)
|
||
else if (t === 'richtext') ctl = MdBox(o, v)
|
||
else if (t === 'number') ctl = box(txt(v, 'Enter a number'), D('display:flex;border-left:1px solid var(--ui-border);height:' + ih + 'px;color:var(--ui-text-muted)', S('width:' + (m ? 40 : 28) + 'px;display:flex;align-items:center;justify-content:center', I('minus', 14)), S('width:' + (m ? 40 : 28) + 'px;display:flex;align-items:center;justify-content:center;border-left:1px solid var(--ui-border)', I('plus', 14))))
|
||
else if (t === 'date') ctl = box(txt(v, 'Pick a date'), I('calendar', 14, 'margin-right:10px;color:var(--ui-text-muted)'))
|
||
else if (t === 'select') ctl = box(txt(v, 'Choose an option'), I('chevron-down', 14, 'margin-right:10px;color:var(--ui-text-muted)'))
|
||
else ctl = box(txt(v, ''))
|
||
const hp = o.help === undefined ? hint(f) : o.help
|
||
return D('display:flex;flex-direction:column;gap:6px;min-width:0;position:relative', lab, ctl, o.err && S('display:flex;gap:6px;align-items:flex-start;color:var(--ui-error);font-size:12px;line-height:18px', I('circle-alert', 14, 'margin-top:2px'), o.err), !o.err && hp && S('color:var(--ui-text-muted);font-size:12px;line-height:18px;text-wrap:pretty', hp), o.extra)
|
||
}
|
||
const Cell = (f, v) => {
|
||
if (v === undefined || v === null || v === '') return S('color:var(--ui-text-muted)', '–')
|
||
if (f.type === 'boolean') return S('display:flex;align-items:center;gap:6px', I(v ? 'check' : 'minus', 14, 'color:var(--ui-text-muted)'), v ? 'Yes' : 'No')
|
||
if (f.type === 'select') return S('height:20px;padding:0 6px;display:inline-flex;align-items:center;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned);font-size:12px;justify-self:start', v)
|
||
if (f.type === 'number') return S('justify-self:end;font-variant-numeric:tabular-nums', v)
|
||
return S('overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + (f.type === 'richtext' ? 'color:var(--ui-text-toned)' : ''), v)
|
||
}
|
||
const mt = (k) => k === 'true' || k === 'false'
|
||
|
||
// ---------- pages
|
||
let main = null; const ov = []; let bare = false, bw = '1440px', bhh = '900px'
|
||
const v = s.v, k = s.k
|
||
|
||
// 1 schema list
|
||
const typesTable = (custom) => {
|
||
const cols = '24px minmax(0,1.4fr) minmax(0,1fr) 90px 150px 24px'
|
||
const hdr = D('display:grid;grid-template-columns:' + cols + ';gap:12px;height:36px;align-items:center;padding:0 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500', S('', ''), S('', 'Name'), S('', 'Key'), S('', 'Fields'), S('', 'Last updated'), S('', ''))
|
||
const r = (name, key, n, up, built) => D('display:grid;grid-template-columns:' + cols + ';gap:12px;height:48px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border)', I(built ? 'lock' : 'file-stack', 16, 'color:var(--ui-text-muted)'), D('display:flex;align-items:center;gap:8px;min-width:0', S('font-weight:500;color:var(--ui-text-highlighted)', name), built && Bd('Built-in', 'built', 'lock')), S(mono + 'font-size:12px;color:var(--ui-text-muted)', key), S('', String(n)), S('color:var(--ui-text-muted)', up || 'Managed by RenewCMS'), I('chevron-right', 14, 'color:var(--ui-text-muted)'))
|
||
const mr = (name, key, n, up, built) => D('display:flex;align-items:center;gap:12px;padding:12px 8px 12px 16px;border-top:1px solid var(--ui-border);min-height:68px', D('flex:1;min-width:0;display:flex;flex-direction:column;gap:4px', D('display:flex;align-items:center;gap:8px;flex-wrap:wrap', S('font-weight:500;font-size:15px;line-height:21px;color:var(--ui-text-highlighted)', name), built && Bd('Built-in', 'built', 'lock')), S('font-size:12px;color:var(--ui-text-muted)', S(mono, key), ' · ' + n + ' fields' + (up ? ' · Updated ' + up : ''))), S('width:36px;height:44px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('chevron-right', 18)))
|
||
const note = D('display:flex;align-items:center;gap:8px;padding:10px 12px;border-top:1px solid var(--ui-border);background:var(--ui-bg-muted);color:var(--ui-text-toned);font-size:12px;line-height:18px', I('lock', 14, 'flex-shrink:0'), S('', 'Built-in types are managed by RenewCMS. You can read their fields, but not change them.'))
|
||
const rr = m ? mr : r
|
||
return D('background:var(--ui-bg);border:1px solid var(--ui-border)', !m && hdr, ...(custom ? CUSTOM.map(x => rr(x[0], x[1], x[2], x[3])) : []), note, ...BUILTIN.map(x => rr(x[0], x[1], x[2], '', true)))
|
||
}
|
||
if (v === 'types') {
|
||
Object.assign(shell, { nav: 'schema', crumbs: ['Schema'], mTitle: 'Schema' })
|
||
const newBtn = B('New content type', { k: 'primary', icon: 'plus', px: 14, dis: k === 'loading' })
|
||
const impExp = s.later ? [B('Import', { icon: 'upload' }), B('Export', { icon: 'download' })] : []
|
||
if (k === 'forbidden') { shell.role = 'user'; shell.crumbs = ['Schema']; main = Center('lock', 'You don\'t have access to Schema', 'Only owners and admins can change content types. You can still add and edit entries from the Content menu.', B('Go to Home', { k: 'primary', px: 16 })) }
|
||
else if (k === 'builtin') {
|
||
shell.crumbs = ['Schema', 'Articles']; shell.mTitle = 'Articles'; shell.mLead = 'arrow-left'
|
||
const AF = [['Title', 'title', 'text', 'Required · up to 200 characters'], ['Slug', 'slug', 'text', 'Required · unique'], ['Body', 'body', 'richtext', 'Up to 10,000 characters'], ['Online', 'online', 'boolean', 'Off by default'], ['Published on', 'published_on', 'date', '']]
|
||
const cols = '28px minmax(0,1fr) minmax(0,1fr) 120px minmax(0,1.2fr)'
|
||
main = Page(pad, D('display:flex;align-items:center;gap:12px;flex-shrink:0', I('lock', 18, 'color:var(--ui-text-muted)'), S('font:600 20px/28px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Articles'), Bd('Built-in', 'built', 'lock')),
|
||
Alert('info', 'info', 'Managed by RenewCMS', 'Articles is a built-in type. You can read its fields here, but you can\'t edit it, delete it or add fields.'),
|
||
m ? D('background:var(--ui-bg);border:1px solid var(--ui-border)', ...AF.map(f => D('display:flex;gap:12px;align-items:center;padding:12px 16px;border-top:1px solid var(--ui-border);min-height:64px', S('width:28px;height:28px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated)', I(FT[f[2]][0], 16)), D('flex:1;min-width:0;display:flex;flex-direction:column', S('font-weight:500', f[0]), S('font-size:12px;color:var(--ui-text-muted)', S(mono, f[1]), f[3] ? ' · ' + f[3] : ''))))) :
|
||
D('background:var(--ui-bg);border:1px solid var(--ui-border)', D('display:grid;grid-template-columns:' + cols + ';gap:12px;height:36px;align-items:center;padding:0 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500', S('', ''), S('', 'Label'), S('', 'Key'), S('', 'Type'), S('', 'Rules')), ...AF.map(f => D('display:grid;grid-template-columns:' + cols + ';gap:12px;height:48px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border)', I(FT[f[2]][0], 16, 'color:var(--ui-text-toned)'), S('font-weight:500', f[0]), S(mono + 'font-size:12px;color:var(--ui-text-muted)', f[1]), S('', FT[f[2]][1]), S('color:var(--ui-text-muted)', f[3] || '–')))), S('color:var(--ui-text-muted);font-size:12px', 'Entries of built-in types are managed from their own screens, like Articles and Authors.'))
|
||
} else {
|
||
const body = []
|
||
if (k === 'loading') body.push(D('background:var(--ui-bg);border:1px solid var(--ui-border)', ...Array.from({ length: m ? 6 : 8 }, (_, i) => D('height:' + (m ? 68 : 48) + 'px;display:flex;align-items:center;gap:16px;padding:0 16px;border-top:' + (i ? '1px solid var(--ui-border)' : 'none'), Skel('18px', 16), Skel(i % 2 ? '28%' : '20%'), Skel('14%', 12), !m && Skel('40px', 12), !m && Skel('90px', 12)))))
|
||
else if (k === 'error') body.push(Alert('error', 'circle-x', 'Couldn\'t load content types', 'GET /v1/content-type failed. Check your connection and try again.', 'Try again'))
|
||
else if (k === 'empty') body.push(Empty('shapes', 'No content types yet', 'Create a content type to manage your own kind of content, like events or recipes. The built-in types stay as they are.', 'New content type'), typesTable(false))
|
||
else body.push(typesTable(true), S('color:var(--ui-text-muted)', CUSTOM.length + ' content types · ' + BUILTIN.length + ' built-in'))
|
||
main = Page(pad, Title('Schema', 'Content types and the fields they are made of.', ...impExp, s.later && Later(), newBtn), ...body)
|
||
if (m && k !== 'loading' && k !== 'error' && k !== 'empty') ov.push(Fab('New content type'))
|
||
}
|
||
}
|
||
|
||
// 2 builder
|
||
const bld = () => {
|
||
let fl = EV, name = 'Events', key = 'events', sel = 0, st = 'clean', isNew = false, api = k === 'api', kb = -1, tErr = {}, banner = null, removed = null, cause = false, tip = false, toast = null, info = true, nErr = 0
|
||
if (v === 'review') { const rk = k; if (rk === 'confirm' || rk === 'fallback') { fl = RC; name = 'Recipes'; key = 'recipes'; info = false } else if (rk === 'blocked') { sel = 0 } else { fl = DIRTY; sel = 4; st = 'dirty'; removed = EV[6] } }
|
||
if (v === 'rel') { }
|
||
if (v === 'builder') {
|
||
if (k === 'locked') { sel = 1; tip = true }
|
||
if (['dirty', 'saving', 'saved', 'fail', 'incompat', 'offline'].includes(k)) { fl = DIRTY; sel = 4; st = k === 'incompat' ? 'dirty' : k; removed = EV[6] }
|
||
if (k === 'saved') { removed = null; st = 'saved'; fl = DIRTY.map(f => ({ ...f, isNew: false, changed: false, saved: true })); toast = Toast('success', 'circle-check', 'Content type saved', 'Events now has 8 fields.') }
|
||
if (k === 'fail') banner = Alert('error', 'circle-x', 'Couldn\'t save the content type', 'Something went wrong on our side. Your changes are still here. Try again in a moment.', 'Try again')
|
||
if (k === 'incompat') { banner = Alert('error', 'circle-x', 'Nothing was changed', 'Some entries don\'t fit the new fields, so the whole change was refused. RenewCMS can\'t tell which entries. The changes that can cause this are marked.', 'Review changes'); cause = true }
|
||
if (k === 'offline') { }
|
||
if (k === 'ferr') { fl = EV.map((f, i) => i === 2 ? { ...f, label: '', err: { label: 'Give the field a label.' } } : i === 4 ? { ...f, min: 6000, err: { min: 'Lowest value can\'t be above the highest value.' } } : i === 5 ? { ...f, opts: ['Talk', 'Workshop', '', 'Talk'], err: { choices: { 2: 'Choices can\'t be blank.', 3: 'Two choices are the same.' } } } : f); sel = 4; st = 'errs'; nErr = 3 }
|
||
if (k === 'choices') { fl = EV.map((f, i) => i === 5 ? { ...f, opts: ['Talk', 'Workshop', 'Meetup', '', 'Talk'], err: { choices: { 3: 'Choices can\'t be blank.', 4: 'Two choices are the same.' } } } : f); sel = 5; st = 'errs'; nErr = 1 }
|
||
if (k === 'kb') { fl = [EV[0], EV[2], EV[1], ...EV.slice(3)]; sel = 1; kb = 1; st = 'dirty' }
|
||
if (k === 'new') { fl = [{ key: 'title', label: 'Title', type: 'text', isNew: true }]; name = ''; key = ''; isNew = true; sel = 0; st = 'clean'; info = false }
|
||
if (k === 'terr') { fl = []; name = ''; key = 'e'; isNew = true; sel = -1; st = 'errs'; nErr = 3; info = false; tErr = { name: 'Give the content type a name.', key: 'Use 2 to 40 lowercase letters, digits or hyphens.', fields: 'A content type needs at least one field.' } }
|
||
if (k === 'taken') { fl = [{ key: 'title', label: 'Title', type: 'text', req: true, isNew: true }, { key: 'starts_on', label: 'Starts on', type: 'date', isNew: true }]; name = 'Events'; key = 'events'; isNew = true; sel = 0; st = 'errs'; nErr = 1; info = false; tErr = { key: 'The key "events" is already used by another content type. Choose a different key.' } }
|
||
}
|
||
const f = fl[sel], dim = st === 'saving'
|
||
const nameF = F('Name', name, { req: true, err: tErr.name, ph: !name, phText: 'For example, Events', dis: dim })
|
||
const keyF = F('Key', key, { mono: true, lock: !isNew, auto: isNew && !!key && !tErr.key, err: tErr.key, ph: !key, phText: 'events', dis: dim, help: tErr.key ? '' : isNew ? 'Used in URLs and the API. Choose it carefully: it can\'t change after you save.' : null })
|
||
const save = D('display:flex;align-items:center;gap:' + (m ? 8 : 12) + 'px;flex-shrink:0', !m && stChip(st, nErr), !m && B('Discard', { dis: st === 'clean' || st === 'saving' || st === 'saved' || (st === 'errs' && isNew && k === 'new') }), !m && B(st === 'saving' ? 'Saving…' : 'Save', { k: 'primary', icon: st === 'saving' ? 'loader-circle' : null, px: 16, dis: st === 'clean' || st === 'saved' || st === 'saving' || st === 'errs' || st === 'offline' }))
|
||
const top = m ? D('display:flex;flex-direction:column;gap:12px;padding:16px;flex-shrink:0;background:var(--ui-bg);border-bottom:1px solid var(--ui-border)', nameF, keyF) : D('display:flex;align-items:' + (tErr.name || tErr.key ? 'flex-start' : 'flex-end') + ';gap:16px;padding:16px 24px;flex-shrink:0;background:var(--ui-bg);border-bottom:1px solid var(--ui-border);position:relative', D('width:320px', nameF), D('width:' + (tErr.key ? 360 : 240) + 'px', keyF), D('flex:1'), D('display:flex;align-items:center;gap:12px;height:32px;margin-top:' + (tErr.name || tErr.key ? 22 : 0) + 'px', save, IB('ellipsis')), tip && D('position:absolute;left:352px;top:84px;z-index:30', Tip('The key is used in URLs and the API, so it can\'t change after the type is created.')))
|
||
const infoBar = info && !isNew && D('flex-shrink:0;display:flex;align-items:center;gap:8px;padding:8px ' + (m ? 16 : 24) + 'px;background:var(--rc-info-soft);color:var(--ui-info);font-size:12px;line-height:18px', I('info', 14, 'flex-shrink:0'), S('color:var(--ui-text-toned)', '37 entries use this content type. Changes are checked against them when you save.'))
|
||
const listHdr = D('display:flex;align-items:center;justify-content:space-between;height:28px', S('font-weight:600;color:var(--ui-text-highlighted)', 'Fields'), S(mono + 'font-size:12px;color:var(--ui-text-muted)', fl.length + ' of 50'))
|
||
const live = kb >= 0 && D('padding:8px 10px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font-size:12px;line-height:18px', S('font-weight:600', 'Moving Ends on. Position 2 of 7.'), ' Press Up or Down to move, Space to drop, Esc to cancel.')
|
||
const rows = fl.length ? fl.map((x, i) => FRow(x, i, { sel: m ? -1 : sel, kb, dim, cause })) : [D('display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;padding:32px 16px;border:1px dashed ' + (tErr.fields ? 'var(--ui-error)' : 'var(--ui-border-accented)'), I('rows-3', 20, 'color:var(--ui-text-muted)'), S('font-weight:600', 'No fields yet'), S('color:' + (tErr.fields ? 'var(--ui-error)' : 'var(--ui-text-muted)'), tErr.fields || 'Add the first field.'))]
|
||
const rem = removed && D('display:flex;align-items:center;gap:8px;min-height:44px;padding:0 12px;border:1px dashed var(--ui-border-accented);color:var(--ui-text-toned)', I('minus', 14), S('flex:1', removed.label + ' will be removed'), S('text-decoration:underline;text-underline-offset:3px;font-weight:500;color:var(--ui-text-highlighted)', 'Undo'))
|
||
const addB = B('Add field', { icon: 'plus', full: false, dis: dim })
|
||
const apiRow = m && D('display:flex;align-items:center;gap:10px;min-height:44px;padding:0 12px;border:1px solid var(--ui-border);background:var(--ui-bg);color:var(--ui-text-highlighted)', I('braces', 16), S('flex:1;font-weight:500', 'API preview'), I('chevron-right', 14, 'color:var(--ui-text-muted)'))
|
||
const list = D('display:flex;flex-direction:column;gap:8px;' + (m ? 'padding:16px;flex:1;min-height:0;overflow:hidden' : 'width:380px;flex-shrink:0;border-right:1px solid var(--ui-border);padding:16px;overflow:hidden;background:var(--ui-bg-muted)'), listHdr, live, ...rows, rem, addB, apiRow)
|
||
const inspPane = !m && D('flex:1;min-width:0;overflow:hidden;padding:16px 24px;background:var(--ui-bg);display:flex;flex-direction:column;gap:14px', f ? D('display:flex;align-items:center;gap:8px', S('font:600 15px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Field: ' + (f.label || 'Untitled field')), f.isNew && Bd('New', 'neu')) : S('font:600 15px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Field settings'), f ? D('opacity:' + (dim ? 0.6 : 1), Insp(f)) : S('color:var(--ui-text-muted)', 'Add a field, then choose its type and rules here.'))
|
||
const bar = m && D('flex-shrink:0;display:flex;align-items:center;gap:8px;padding:12px 16px;border-top:1px solid var(--ui-border);background:var(--ui-bg)', stChip(st, nErr), D('flex:1'), B('Discard', { dis: st === 'clean' || st === 'saved' || st === 'saving' || k === 'new' }), B(st === 'saving' ? 'Saving…' : 'Save', { k: 'primary', icon: st === 'saving' ? 'loader-circle' : null, px: 16, dis: ['clean', 'saved', 'saving', 'errs', 'offline'].includes(st) }))
|
||
shell.nav = 'schema'; shell.crumbs = ['Schema', isNew ? 'New content type' : name]; shell.mTitle = isNew ? 'New content type' : name; shell.mLead = 'arrow-left'; shell.mAction = 'ellipsis'
|
||
shell.offline = false
|
||
main = D('flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;position:relative', st === 'offline' && Offline(), top, infoBar, banner && D('padding:12px ' + (m ? 16 : 24) + 'px 0', banner), D('flex:1;min-height:0;display:flex;' + (m ? 'flex-direction:column' : ''), list, inspPane), m ? bar : ApiPanel(fl, key || 'events', api))
|
||
if (m && f && (s.sheet || (v === 'builder' && ['edit-sheet', 'choices'].includes(k)))) ov.push(Sheet({ title: 'Field: ' + (f.label || 'Untitled field') }, Insp(f), [B('Done', { k: 'primary', full: true })]))
|
||
if (m && api) ov.push(Sheet({ title: 'API preview' }, [S('color:var(--ui-text-muted);font-size:12px', 'GET /v1/content-type/' + key + '/entry'), S('display:block;flex:1;overflow:hidden;padding:10px 12px;background:var(--ui-bg-elevated);border:1px solid var(--ui-border);white-space:pre;' + mono + 'font-size:12px;line-height:19px;color:var(--ui-text-highlighted)', apiJson(fl, key))], [B('Copy JSON', { k: 'primary', full: true, icon: 'copy' })]))
|
||
if (toast) ov.push(toast)
|
||
}
|
||
if (v === 'builder') { bld() }
|
||
|
||
// 3 review
|
||
const chg = (icon, tone, title, text, flag, extra) => D('display:flex;gap:10px;padding:10px 12px;border:1px solid var(--ui-border);background:' + (flag ? 'var(--rc-warning-soft)' : 'var(--ui-bg)'), S('margin-top:2px;display:flex;color:' + (flag ? 'var(--ui-warning)' : 'var(--ui-text-toned)'), I(icon)), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', D('display:flex;align-items:center;gap:8px;flex-wrap:wrap', S('font-weight:600;color:var(--ui-text-highlighted)', title), flag && Bd(flag, 'warn', 'triangle-alert'), extra), S('color:var(--ui-text-toned);text-wrap:pretty', text)))
|
||
if (v === 'review') {
|
||
bld()
|
||
const sum = D('display:flex;flex-direction:column;gap:2px', S('font-weight:600;font-size:15px;color:var(--ui-text-highlighted)', 'Add 2 fields, change 1, remove 1'), S('color:var(--ui-text-muted)', 'Events has 37 entries. Each one is checked against the new fields when you apply.'))
|
||
const lat = t => k === 'later' ? Later(t) : null
|
||
const list = [chg('plus', 'n', 'Add Speaker', 'Text, optional.', null, lat('Affects 0 entries')), chg('plus', 'n', 'Add Room', 'Choice, required, default "Main hall". Existing entries get the default, so they still fit.', null, lat('Affects 37 entries')), chg('pencil', 'n', 'Change Capacity', 'Highest value goes from 5000 to 2000.', 'Entries may not fit', lat('Affects 3 entries')), chg('minus', 'n', 'Remove Description', 'Entries stop returning its values.', 'Removes data', lat('Affects 31 entries'))]
|
||
if (k === 'review' || k === 'later') ov.push(Shell({ title: 'Review changes to Events', desc: 'Apply them to save the content type.', w: k === 'later' ? 640 : 560, top: 60, cancel: 'Back to editing', confirm: 'Apply changes', later: false }, [sum, D('display:flex;flex-direction:column;gap:8px', ...list), k === 'later' ? D('display:flex;align-items:center;gap:8px', B('View full diff', { icon: 'git-compare', h: m ? 44 : 28, fs: 12, px: 10 }), Later('Per-change counts and diff come later')) : S('color:var(--ui-text-muted);font-size:12px;line-height:18px', 'Flagged changes can be refused if existing entries don\'t fit. If that happens, nothing is changed.')]))
|
||
if (k === 'applying') ov.push(Shell({ title: 'Review changes to Events', desc: 'Apply them to save the content type.', w: 560, top: 60, cancel: 'Back to editing', confirm: 'Applying…', loading: true, cdis: true }, [sum, D('display:flex;flex-direction:column;gap:8px', ...list)]))
|
||
if (k === 'refused') ov.push(Shell({ title: 'Nothing was changed', desc: 'RenewCMS refused the changes because some entries don\'t fit the new fields. It doesn\'t say which entries.', w: 560, top: 60, close: true, cancel: 'Discard changes', confirm: 'Back to editing' }, [Alert('error', 'circle-x', 'Your content type is as it was', 'No field was added, changed or removed.'), S('font-weight:600;color:var(--ui-text-highlighted)', 'The change that can cause this in your edit'), chg('pencil', 'n', 'Change Capacity', 'Highest value goes from 5000 to 2000. An entry may have a higher value.', 'Possible cause'), S('color:var(--ui-text-muted);font-size:12px;line-height:18px;text-wrap:pretty', 'This kind of change is refused when an entry is over the new limit, a new required field has no default, a field becomes unique while entries share a value, or a removed choice is still in use.')]))
|
||
if (k === 'blocked') ov.push(Shell({ title: 'You can\'t delete Events yet', desc: 'A content type can only be deleted when it has no entries.', close: true, cancel: 'Close', confirm: 'View entries' }, [Alert('warning', 'triangle-alert', 'Events has 37 entries', 'Delete all of its entries first, then delete the content type.')]))
|
||
if (k === 'confirm') ov.push(Shell({ title: 'Delete Recipes?', desc: 'Recipes has no entries. Its fields and its API routes will be deleted. This can\'t be undone.', close: true, cancel: 'Cancel', confirm: 'Delete content type', tone: 'danger' }, [S('color:var(--ui-text-toned)', 'Key: ', S(mono + 'font-size:12px', 'recipes'))]))
|
||
if (k === 'fallback') ov.push(Shell({ title: 'Delete Recipes?', desc: 'Recipes has no entries. Its fields and its API routes will be deleted. This can\'t be undone.', close: true, cancel: 'Close', confirm: 'View entries' }, [Alert('error', 'circle-x', 'Recipes now has entries', 'Someone added an entry after this page loaded, so nothing was deleted. Delete the entries first, then try again.')]))
|
||
}
|
||
|
||
// 4 gallery
|
||
const GF = { text: EV[0], richtext: { key: 'description', label: 'Description', type: 'richtext', max: 5000 }, number: EV[4], boolean: EV[3], date: { key: 'starts_on', label: 'Starts on', type: 'date', req: true, max: '31 Dec 2027' }, select: { ...EV[5], req: false } }
|
||
if (v === 'gallery') {
|
||
bare = true; if (m) { bw = '390px'; bhh = '844px' }
|
||
const f = { ...GF[k], saved: false }, t = k, L = f.label
|
||
const ERR = { text: [['required', 422, 'Fill in ' + L + '.'], ['type', 422, L + ' must be text.'], ['min', 422, L + ' needs at least 3 characters.'], ['max', 422, L + ' can be up to 80 characters.'], ['options', '', 'Not used for text.'], ['unique', 409, 'Another entry already uses this ' + L + '. Enter a different one.']], richtext: [['required', 422, 'Fill in ' + L + '.'], ['type', 422, L + ' must be text.'], ['min', 422, L + ' needs at least 10 characters.'], ['max', 422, L + ' can be up to 5000 characters.'], ['options', '', 'Not used for rich text.'], ['unique', '', 'Not used for rich text.']], number: [['required', 422, 'Fill in ' + L + '.'], ['type', 422, L + ' must be a number.'], ['min', 422, L + ' must be at least 1.'], ['max', 422, L + ' must be at most 5000.'], ['options', '', 'Not used for numbers.'], ['unique', 409, 'Another entry already uses this ' + L + '.']], boolean: [['required', 422, 'Choose yes or no for ' + L + '.'], ['type', 422, L + ' must be yes or no.'], ['min', '', 'Not used for yes or no.'], ['max', '', 'Not used for yes or no.'], ['options', '', 'Not used for yes or no.'], ['unique', '', 'Not used for yes or no.']], date: [['required', 422, 'Pick a date for ' + L + '.'], ['type', 422, L + ' must be a date, like 2026-11-21.'], ['min', 422, L + ' can\'t be before 1 Jan 2026.'], ['max', 422, L + ' can\'t be after 31 Dec 2027.'], ['options', '', 'Not used for dates.'], ['unique', 409, 'Another entry already uses this date for ' + L + '.']], select: [['required', 422, 'Choose an option for ' + L + '.'], ['type', 422, L + ' must be one of the choices.'], ['min', '', 'Not used for choices.'], ['max', '', 'Not used for choices.'], ['options', 422, 'Choose one of: Talk, Workshop or Meetup.'], ['unique', 409, 'Another entry already uses this choice for ' + L + '.']] }[t]
|
||
const SAMPLE = { text: ['Lime plaster workshop', ''], richtext: [mdSample, ''], number: ['18', ''], boolean: ['true', 'false'], date: ['21 Nov 2026', ''], select: ['Workshop', ''] }[t]
|
||
const sec = (title, ...c) => D('display:flex;flex-direction:column;gap:10px;min-width:0', S('font:600 15px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', title), ...c)
|
||
const card = (...c) => D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:16px;display:flex;flex-direction:column;gap:14px', ...c)
|
||
const specCtl = { text: Ctl(f, { v: 'Lime plaster workshop, two days of work with the whole team on site', count: '74 / 80', focus: true }), richtext: Ctl({ ...f, max: 5000 }, { v: mdSample, tab: 'write', count: '86 / 5000', help: 'Markdown. Switch to Preview to check the result.' }), number: Ctl(f, { v: '18', focus: true }), boolean: Ctl(f, { v: 'true' }), date: Ctl(f, { v: '21 Nov 2026', focus: true, extra: D('position:absolute;left:0;top:68px;z-index:3', Cal()) }), select: Ctl(f, { v: 'Workshop', focus: true, extra: D('position:absolute;left:0;top:66px;right:0;z-index:3;border:1px solid var(--ui-border);background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);padding:4px', ...['Talk', 'Workshop', 'Meetup'].map(o_ => D('height:' + (m ? 44 : 32) + 'px;padding:0 8px;display:flex;align-items:center;justify-content:space-between;background:' + (o_ === 'Workshop' ? 'var(--ui-bg-elevated)' : 'transparent'), o_, o_ === 'Workshop' && I('check', 14)))) }) }[t]
|
||
const spacer = { date: 'height:236px', select: 'height:120px', richtext: '', text: '', number: '', boolean: '' }[t]
|
||
const stateLbl = { text: 'Typing, close to the limit', richtext: 'Write tab, with counter', number: 'Focused', boolean: 'On', date: 'Date picker open', select: 'Choices open' }[t]
|
||
const ctlCol = sec('Entry form control', card(S('font-size:12px;color:var(--ui-text-muted)', stateLbl), D('', specCtl, spacer && D(spacer)), D('border-top:1px solid var(--ui-border)'), S('font-size:12px;color:var(--ui-text-muted)', t === 'richtext' ? 'Preview tab' : t === 'boolean' ? 'Off, and not set' : 'Empty'), t === 'richtext' ? Ctl(f, { v: mdSample, tab: 'preview', help: '' }) : t === 'boolean' ? Ctl(f, { v: 'false' }) : Ctl(f, { v: '' })))
|
||
const cell = (val, lab) => D('display:grid;grid-template-columns:' + (m ? '90px' : '110px') + ' minmax(0,1fr);gap:12px;align-items:center;min-height:40px;padding:0 12px;border-top:1px solid var(--ui-border)', S('font-size:12px;color:var(--ui-text-muted)', lab), Cell(f, val))
|
||
const cellVals = { text: ['Lime plaster workshop', ''], richtext: ['Two days of hands-on work with lime putty and old stone.', ''], number: [18, null], boolean: [true, null], date: ['21 Nov 2026', ''], select: ['Workshop', ''] }[t]
|
||
const listCol = sec('List cell', D('background:var(--ui-bg);border:1px solid var(--ui-border)', D('display:grid;grid-template-columns:' + (m ? '90px' : '110px') + ' minmax(0,1fr);gap:12px;height:36px;align-items:center;padding:0 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500', S('', 'Value'), S('', L)), D('', cell(cellVals[0], 'Filled'), cell(cellVals[1], 'Empty'))), S('font-size:12px;color:var(--ui-text-muted)', { text: 'Truncated on one line, full text on hover.', richtext: 'First line as plain text, truncated.', number: 'Right-aligned, tabular figures.', boolean: 'Yes or No with an icon. Empty only when the field has no value.', date: 'Day, month and year.', select: 'The chosen option as a small tag.' }[t]))
|
||
const errCol = sec('Errors by rule', D('background:var(--ui-bg);border:1px solid var(--ui-border)', D('display:grid;grid-template-columns:' + (m ? '64px 38px' : '72px 40px') + ' minmax(0,1fr);gap:10px;height:32px;align-items:center;padding:0 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500', S('', 'Rule'), S('', 'HTTP'), S('', 'Message')), ...ERR.map(r => D('display:grid;grid-template-columns:' + (m ? '64px 38px' : '72px 40px') + ' minmax(0,1fr);gap:10px;padding:8px 12px;border-top:1px solid var(--ui-border);align-items:start;line-height:18px', S(mono + 'font-size:12px;color:var(--ui-text-highlighted)', r[0]), S(mono + 'font-size:12px;color:var(--ui-text-muted)', String(r[1] || '–')), S('color:' + (r[1] ? 'var(--ui-error)' : 'var(--ui-text-muted)') + ';text-wrap:pretty', r[2])))))
|
||
const FW = { text: [['contains', 'Any text'], 'text'], richtext: [['contains', 'Any text'], 'text'], number: [['between'], 'num'], boolean: [['is'], 'bool'], date: [['between'], 'date'], select: [['is one of'], 'sel'] }[t]
|
||
const fw = FW[1] === 'bool' ? Seg([['any', 'Any'], ['yes', 'Yes'], ['no', 'No']], 'any') : FW[1] === 'sel' ? D('display:flex;gap:6px;flex-wrap:wrap', ...['Talk', 'Workshop', 'Meetup'].map((o_, i) => S('height:28px;padding:0 10px;display:flex;align-items:center;gap:6px;border:1px solid ' + (i === 1 ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (i === 1 ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (i === 1 ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)'), i === 1 && I('check', 12), o_))) : FW[1] === 'num' || FW[1] === 'date' ? D('display:flex;gap:8px;align-items:center', F('', '', { ph: true, phText: FW[1] === 'date' ? 'From' : 'Min', ico: FW[1] === 'date' ? 'calendar' : null }), S('color:var(--ui-text-muted)', 'to'), F('', '', { ph: true, phText: FW[1] === 'date' ? 'Until' : 'Max', ico: FW[1] === 'date' ? 'calendar' : null })) : F('', '', { ph: true, phText: 'Contains…', ico: 'search' })
|
||
const filtCol = sec('Filter in the entry list', D('border:1px dashed var(--ui-border-accented);padding:14px;display:flex;flex-direction:column;gap:10px', D('display:flex;align-items:center;gap:8px', S('font-weight:500;color:var(--ui-text-highlighted)', L + ': ' + FW[0][0]), Later('Later')), fw, S('font-size:12px;color:var(--ui-text-muted);line-height:18px', 'Filtering on custom fields isn\'t available yet. The list only sorts by ID, created and updated.')))
|
||
const head = D('display:flex;align-items:center;gap:14px;flex-shrink:0', S('width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:var(--rc-sand);color:var(--rc-on-sand)', I(FT[t][0], 22)), D('flex:1;display:flex;flex-direction:column', S('font:600 ' + (m ? 20 : 24) + 'px/32px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', FT[t][1]), S('color:var(--ui-text-muted)', FT[t][2])), !m && S(mono + 'font-size:12px;color:var(--ui-text-toned)', 'type: "' + t + '"'))
|
||
const inspCol = !m && sec('Inspector options', card(Insp(f, { one: true })))
|
||
main = D('position:absolute;inset:0;padding:' + (m ? '16px' : '32px') + ';display:flex;flex-direction:column;gap:' + (m ? 14 : 20) + 'px;overflow:hidden', head, m ? D('display:flex;flex-direction:column;gap:14px', ctlCol, listCol, errCol, filtCol) : D('display:grid;grid-template-columns:400px 440px minmax(0,1fr);gap:24px;align-items:start;flex:1;min-height:0;overflow:hidden', inspCol, D('display:flex;flex-direction:column;gap:20px;min-width:0', ctlCol, listCol), D('display:flex;flex-direction:column;gap:20px;min-width:0', errCol, filtCol)))
|
||
}
|
||
|
||
// 5 entries
|
||
const visCols = k === 'cols' ? [EV[0], EV[1], EV[2], EV[3]] : [EV[0], EV[1], EV[2], EV[3]]
|
||
if (v === 'entries') {
|
||
shell.nav = 'content'; shell.crumbs = ['Content', 'Events']; shell.mTitle = 'Events'; shell.contentOn = k === 'notfound' ? null : 'events'
|
||
if (s.many && m) { shell.drawer = true; shell.contentOpen = true }
|
||
if (k === 'forbidden') main = Center('lock', 'You don\'t have access to Events', 'Your account can\'t open this content right now. If you think this is a mistake, ask an owner or an admin.', B('Go to Home', { k: 'primary', px: 16 }))
|
||
else if (k === 'notfound') { shell.crumbs = ['Content']; shell.mTitle = 'Content'; main = Center('file-x', 'Content type not found', 'It may have been deleted, or the address may be mistyped.', B('Go to Home', { k: 'primary', px: 16 })) }
|
||
else {
|
||
const paged = k === 'paged', total = paged ? 37 : 7, size = paged ? 5 : 25
|
||
const editB = B('Edit content type', { icon: 'settings-2', k: 'ghost' })
|
||
const sortB = D('display:flex;align-items:center;gap:8px;height:' + bh + 'px;padding:0 10px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);white-space:nowrap;' + (m ? 'flex:1' : 'width:220px;flex-shrink:0'), S('color:var(--ui-text-muted)', 'Sort by'), S('font-weight:500;flex:1', 'Last updated'), I('chevron-down', 14, 'color:var(--ui-text-muted)'))
|
||
const dirB = B(null, { icon: 'arrow-down-wide-narrow', w: bh, px: 0 }), colB = B(m ? null : 'Columns', { icon: 'columns-3', w: m ? 44 : null, px: m ? 0 : 12, ring: false })
|
||
const toolbar = D('display:flex;align-items:center;gap:8px;flex-shrink:0;position:relative', sortB, dirB, colB, !m && D('flex:1'), !m && editB)
|
||
let body
|
||
if (k === 'loading') body = D('background:var(--ui-bg);border:1px solid var(--ui-border);flex:1;min-height:0;overflow:hidden', ...Array.from({ length: m ? 6 : 8 }, (_, i) => D('height:' + (m ? 68 : 48) + 'px;display:flex;align-items:center;gap:20px;padding:0 16px;border-top:' + (i ? '1px solid var(--ui-border)' : 'none'), !m && Skel('32px', 12), Skel(i % 2 ? '30%' : '24%'), !m && Skel('90px', 12), !m && Skel('90px', 12), !m && Skel('50px', 12))))
|
||
else if (k === 'error') body = Alert('error', 'circle-x', 'Couldn\'t load entries', 'GET /v1/content-type/events/entry failed. Check your connection and try again.', 'Try again')
|
||
else if (k === 'empty') body = Empty('file-plus', 'No entries yet', 'Use New entry to add the first event. Each entry follows the fields of this content type.', 'New entry')
|
||
else {
|
||
const rows = ROWS.slice(0, size > 7 ? 7 : 5)
|
||
const gc = '48px ' + visCols.map((f, i) => i === 0 ? 'minmax(0,1.6fr)' : f.type === 'boolean' || f.type === 'number' ? '80px' : '120px').join(' ') + ' 110px 150px 130px'
|
||
const hdrC = (t, o = {}) => S('display:flex;align-items:center;gap:4px;' + (o.r ? 'justify-content:flex-end;' : ''), t, o.sort && I('arrow-down', 12))
|
||
const tbl = D('background:var(--ui-bg);border:1px solid var(--ui-border)', D('display:grid;grid-template-columns:' + gc + ';gap:12px;height:36px;align-items:center;padding:0 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500', hdrC('ID'), ...visCols.map(f => hdrC(f.label, { r: f.type === 'number' })), hdrC('Created'), hdrC('Updated', { sort: true }), hdrC('Updated by')),
|
||
...rows.map((r, i) => D('display:grid;grid-template-columns:' + gc + ';gap:12px;height:48px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border);background:' + (i === 0 && false ? 'var(--ui-bg-elevated)' : 'transparent'), S(mono + 'font-size:12px;color:var(--ui-text-muted)', String(r.id)), ...visCols.map((f, j) => D('min-width:0;' + (j === 0 ? 'font-weight:500;color:var(--ui-text-highlighted);display:flex' : 'display:grid'), Cell(f, r[f.key]))), S('color:var(--ui-text-muted);white-space:nowrap', r.c), S('color:var(--ui-text-muted);white-space:nowrap', r.u.split(',')[0]), S('overflow:hidden;text-overflow:ellipsis;white-space:nowrap', r.by))))
|
||
const cards = D('background:var(--ui-bg);border:1px solid var(--ui-border)', ...ROWS.slice(0, 6).map((r, i) => D('display:flex;align-items:center;gap:8px;padding:12px 4px 12px 16px;border-top:' + (i ? '1px solid var(--ui-border)' : 'none') + ';min-height:68px', D('flex:1;min-width:0;display:flex;flex-direction:column;gap:4px', S('font-weight:500;font-size:15px;line-height:21px;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', r.title), D('display:flex;align-items:center;gap:8px;font-size:12px;color:var(--ui-text-muted)', S('', r.starts_on), r.category && Cell(EV[5], r.category), S('overflow:hidden;text-overflow:ellipsis;white-space:nowrap', '· ' + r.by))), S('width:36px;height:44px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)', I('chevron-right', 18)))))
|
||
const pgB = (c, on, dis) => S('width:' + bh + 'px;height:' + bh + 'px;display:flex;align-items:center;justify-content:center;border:1px solid ' + (on ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (on ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';opacity:' + (dis ? 0.4 : 1) + ';font-weight:' + (on ? 500 : 400), typeof c === 'string' && c.length > 2 ? I(c, 14) : c)
|
||
const pager = !paged ? S('color:var(--ui-text-muted)', '7 entries') : m ? D('display:flex;align-items:center;justify-content:space-between;gap:8px', pgB('chevron-left', false, true), S('color:var(--ui-text-muted)', 'Page 1 of 8'), pgB('chevron-right')) : D('display:flex;align-items:center;gap:16px', S('flex:1;color:var(--ui-text-muted)', 'Showing 1–5 of 37'), D('display:flex;align-items:center;gap:8px;height:32px;padding:0 10px;border:1px solid var(--ui-border-accented);background:var(--ui-bg)', '5 per page', I('chevron-down', 14, 'color:var(--ui-text-muted)')), D('display:flex;gap:4px', pgB('chevrons-left', false, true), pgB('chevron-left', false, true), pgB('1', true), pgB('2'), pgB('3'), pgB('…'), pgB('8'), pgB('chevron-right'), pgB('chevrons-right')))
|
||
body = D('display:flex;flex-direction:column;gap:12px;min-height:0', m ? cards : tbl, pager)
|
||
}
|
||
main = Page(pad, Title('Events', 'events · 7 fields', !m && B('New entry', { k: 'primary', icon: 'plus', px: 14, dis: k === 'loading' })), ...(k === 'error' || k === 'empty' || k === 'loading' ? [k !== 'error' ? toolbar : null, body] : [toolbar, body]))
|
||
if (m && k !== 'error' && k !== 'loading') ov.push(Fab('New entry'))
|
||
if (k === 'cols') {
|
||
const items = EV.map((f, i) => [f, i < 4])
|
||
const lst = D('display:flex;flex-direction:column', ...items.map(([f, on]) => D('display:flex;align-items:center;gap:10px;min-height:' + (m ? 48 : 36) + 'px;padding:0 8px', Chk(on), I(FT[f.type][0], 14, 'color:var(--ui-text-muted)'), S('flex:1', f.label), S(mono + 'font-size:12px;color:var(--ui-text-muted)', f.key))), D('display:flex;align-items:center;gap:10px;min-height:' + (m ? 48 : 36) + 'px;padding:0 8px;color:var(--ui-text-muted)', Chk(false), I('clock', 14), S('', 'Created and updated dates always show')))
|
||
if (m) ov.push(Sheet({ title: 'Columns', z: 45 }, [S('color:var(--ui-text-muted)', 'Choose up to 6 fields to show. 4 selected.'), lst], [B('Reset', { full: true }), B('Done', { k: 'primary', full: true })]))
|
||
else ov.push(D('position:absolute;z-index:40;left:' + (224 + 24 + 330) + 'px;top:' + (48 + 24 + 28 + 18 + 16 + 40) + 'px;width:300px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:8px;display:flex;flex-direction:column;gap:8px', D('display:flex;justify-content:space-between;padding:4px 8px', S('font-weight:600', 'Columns'), S('color:var(--ui-text-muted);font-size:12px', '4 of 6')), lst, D('display:flex;justify-content:flex-end;padding:4px 8px', B('Reset', { h: 28, fs: 12 }))))
|
||
}
|
||
}
|
||
}
|
||
|
||
// 6 entry
|
||
if (v === 'entry') {
|
||
shell.nav = 'content'; shell.contentOn = 'events'; shell.crumbs = ['Content', 'Events', k === 'new' || k === 'required' ? 'New entry' : 'Lime plaster workshop']; shell.mTitle = k === 'new' || k === 'required' ? 'New entry' : 'Lime plaster workshop'; shell.mLead = 'arrow-left'; shell.mAction = k === 'new' || k === 'required' ? null : 'trash-2'
|
||
if (k === 'notfound') main = Center('file-x', 'Entry not found', 'It may have been deleted by someone else.', B('Back to Events', { k: 'primary', px: 16 }))
|
||
else {
|
||
const isNew = k === 'new' || k === 'required'
|
||
const val = { title: isNew ? '' : 'Lime plaster workshop', starts_on: isNew ? '' : '21 Nov 2026', ends_on: isNew ? '' : '22 Nov 2026', online: 'false', capacity: isNew ? '' : '18', category: isNew ? '' : 'Workshop', description: isNew ? '' : mdSample }
|
||
const E = {}
|
||
if (k === 'dirty' || k === 'saving' || k === 'fail') val.title = 'Lime plaster workshop, two days'
|
||
if (k === 'required') { E.title = 'Fill in Title.'; E.starts_on = 'Pick a date for Starts on.' }
|
||
if (k === 'rules') { val.title = 'Lime plaster workshop, two days of hands-on work with the whole team on site and lunch'; E.title = 'Title can be up to 80 characters.'; val.ends_on = '31/11/2026'; E.ends_on = 'Ends on must be a date, like 2026-11-21.'; val.capacity = '6000'; E.capacity = 'Capacity must be at most 5000.'; val.category = 'Seminar'; E.category = 'Choose one of: Talk, Workshop or Meetup.' }
|
||
if (k === 'unique') { val.title = 'Heat pumps: what the numbers say'; E.title = 'Another entry already uses this Title. Enter a different one.' }
|
||
const dis = k === 'saving' || k === 'changed'
|
||
const sk = k === 'dirty' ? 'dirty' : k === 'saving' ? 'saving' : k === 'saved' ? 'saved' : k === 'fail' ? 'fail' : (k === 'required' || k === 'rules' || k === 'unique') ? 'fail' : 'clean'
|
||
const chip = k === 'required' || k === 'rules' || k === 'unique' ? S('display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ui-error);white-space:nowrap', I('circle-alert', 14), 'Not saved') : isNew ? S('font-size:12px;color:var(--ui-text-muted);white-space:nowrap', 'New entry') : stChip(sk)
|
||
const canSave = k === 'dirty' || k === 'fail' || k === 'new' || k === 'required' || k === 'rules' || k === 'unique'
|
||
const acts = D('display:flex;align-items:center;gap:' + (m ? 8 : 12) + 'px', !m && chip, !m && !isNew && B('Delete', { k: 'del', icon: 'trash-2' }), !m && B('Cancel', { dis: dis }), !m && B(k === 'saving' ? 'Saving…' : 'Save', { k: 'primary', icon: k === 'saving' ? 'loader-circle' : null, px: 16, dis: !canSave && k !== 'saving' }))
|
||
const fieldsUI = EV.map(f => { const vv = val[f.key], e = E[f.key]; const o = { v: vv, err: e, dis, def: isNew && f.key === 'online', focus: k === 'dirty' && f.key === 'title' }; if (f.key === 'title') { const n = (vv || '').length; o.count = n + ' / 80'; o.countErr = n > 80; if (k === 'unique') o.count = n + ' / 80' } if (f.key === 'description') { o.tab = k === 'preview' ? 'preview' : 'write'; o.help = 'Markdown. Switch to Preview to check the result.' } if (f.key === 'capacity') o.help = 'Between 1 and 5000.'; if (f.key === 'title' && !e) o.help = 'Each entry needs a different title.'; return Ctl(f, o) })
|
||
const grid = (a, b) => D('display:grid;grid-template-columns:' + (m ? '1fr' : '1fr 1fr') + ';gap:16px', a, b)
|
||
const form = D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:' + (m ? 16 : 24) + 'px;display:flex;flex-direction:column;gap:20px;max-width:760px;opacity:' + (dis ? 0.7 : 1), fieldsUI[0], grid(fieldsUI[1], fieldsUI[2]), grid(fieldsUI[3], fieldsUI[4]), fieldsUI[5], fieldsUI[6], D('border-top:1px solid var(--ui-border);padding-top:16px;display:grid;grid-template-columns:' + (m ? '1fr' : 'auto 1fr') + ';gap:6px 16px;font-size:12px;line-height:18px', S('color:var(--ui-text-muted)', 'Entry'), S('', isNew ? 'Not saved yet' : S(mono, 'events / 42')), S('color:var(--ui-text-muted)', 'Created by'), S('', isNew ? '–' : 'Inès Duarte · 12 Oct 2026, 09:14'), S('color:var(--ui-text-muted)', 'Updated by'), S('', isNew ? '–' : k === 'saved' ? 'Marta Alves · 19 Oct 2026, 11:02' : 'Marta Alves · 15 Oct 2026, 11:02')))
|
||
const ban = k === 'fail' ? Alert('error', 'circle-x', 'Couldn\'t save the entry', 'Something went wrong on our side. Your changes are still here. Try again in a moment.', 'Try again') : k === 'changed' ? Alert('warning', 'triangle-alert', 'The fields of Events changed', 'A field was removed and a new required field, Room, was added after you opened this form. Reload the form to continue. What you typed here can\'t be saved against the old fields.', 'Reload form') : k === 'unique' ? Alert('error', 'circle-x', 'Not saved', 'Another entry already uses this title. Change it and save again.') : null
|
||
const bar = m && D('flex-shrink:0;display:flex;align-items:center;gap:8px;padding:12px 16px;border-top:1px solid var(--ui-border);background:var(--ui-bg);position:relative;z-index:1', B('Cancel', { full: true, dis }), B(k === 'saving' ? 'Saving…' : 'Save', { k: 'primary', full: true, icon: k === 'saving' ? 'loader-circle' : null, dis: !canSave && k !== 'saving' }))
|
||
main = D('flex:1;min-width:0;min-height:0;display:flex;flex-direction:column;position:relative', Page(pad, Title(isNew ? 'New entry' : 'Lime plaster workshop', isNew ? 'Events · Fields marked * are required.' : 'Events · Entry 42', acts), m && D('display:flex;align-items:center;justify-content:space-between', chip), ban, form), bar)
|
||
if (k === 'delete') ov.push(Shell({ title: 'Delete this entry?', desc: '"Lime plaster workshop" will be deleted from Events. This can\'t be undone.', close: true, cancel: 'Cancel', confirm: 'Delete entry', tone: 'danger' }, []))
|
||
if (k === 'saved') ov.push(Toast('success', 'circle-check', 'Entry saved', '"Lime plaster workshop" is up to date.'))
|
||
}
|
||
}
|
||
|
||
// 7 relation (later)
|
||
if (v === 'rel') {
|
||
shell.nav = 'schema'
|
||
if (k === 'builder') {
|
||
shell.crumbs = ['Schema', 'Events']; shell.mTitle = 'Events'; shell.mLead = 'arrow-left'
|
||
const shape = (on, a, b, diag) => D('display:flex;flex-direction:column;gap:6px;padding:10px;border:1px solid ' + (on ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (on ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)'), diag, S('font-weight:500', a), S('font-size:12px;line-height:16px;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)'), b))
|
||
const sq = (n, c) => D('display:flex;gap:3px', ...Array.from({ length: n }, () => S('width:10px;height:10px;background:' + c)))
|
||
const dg = (l, r) => D('display:flex;align-items:center;gap:6px', sq(l, 'currentColor'), S('width:20px;height:1px;background:currentColor'), sq(r, 'currentColor'))
|
||
const sel = (t) => S('display:inline-flex;align-items:center;gap:6px;height:28px;padding:0 8px;margin:0 4px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);font-weight:500;color:var(--ui-text-highlighted)', t, I('chevron-down', 12))
|
||
const card_ = (t, ...c) => D('flex:1;min-width:0;padding:12px;background:var(--ui-bg);border:1px solid var(--ui-border);display:flex;flex-direction:column;gap:8px', S('font-weight:600;color:var(--ui-text-highlighted)', t), ...c)
|
||
const chipR = t => S('height:22px;padding:0 8px;display:inline-flex;align-items:center;gap:4px;background:var(--rc-sand);color:var(--rc-on-sand);font-size:12px;font-weight:500', t)
|
||
const insp = D('display:flex;flex-direction:column;gap:14px;min-width:0', D('display:flex;align-items:center;gap:8px', S('font:600 15px/24px \'IBM Plex Sans\',sans-serif;color:var(--ui-text-highlighted)', 'Field: Venue'), Later()), D('display:grid;grid-template-columns:' + (m ? '1fr' : '1fr 1fr') + ';gap:14px', F('Label', 'Venue', { req: true }), F('Key', 'venue', { mono: true, auto: true })),
|
||
D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;color:var(--ui-text-highlighted)', 'Type'), D('display:grid;grid-template-columns:repeat(' + (m ? 2 : 4) + ',minmax(0,1fr));gap:8px', ...['text', 'number', 'date', 'select'].map(t => D('display:flex;gap:8px;align-items:center;padding:8px 10px;border:1px solid var(--ui-border-accented);opacity:.6', I(FT[t][0], 16), FT[t][1])), D('display:flex;gap:8px;align-items:center;padding:8px 10px;border:1px solid var(--ui-primary);background:var(--rc-sand);color:var(--rc-on-sand)', I('link', 16), S('font-weight:500', 'Relation'), S('margin-left:auto;font-size:11px', 'Later')))),
|
||
F('Related content type', 'Venue', { sel: true, help: 'Choose the content type this field points to.' }),
|
||
D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;color:var(--ui-text-highlighted)', 'How are they connected?'), D('display:grid;grid-template-columns:' + (m ? '1fr' : 'repeat(3,minmax(0,1fr))') + ';gap:8px', shape(false, 'One to one', 'Each Event has one Venue, and each Venue has one Event.', dg(1, 1)), shape(true, 'One to many', 'Each Event has one Venue. A Venue has many Events.', dg(3, 1)), shape(false, 'Many to many', 'Each Event has many Venues, and each Venue has many Events.', dg(2, 2)))),
|
||
D('display:flex;flex-direction:column;gap:8px;padding:12px;background:var(--ui-bg-elevated)', S('', 'Each Event has', sel('one'), 'Venue'), S('', 'Each Venue has', sel('many'), 'Events', S('color:var(--ui-text-muted)', ' · shown on Venue as the field "Events"'))),
|
||
D('display:flex;gap:12px;flex-direction:' + (m ? 'column' : 'row'), card_('Event', S('color:var(--ui-text-muted);font-size:12px', 'Venue'), D('display:flex;gap:6px', chipR('Rue Verte hall'))), card_('Venue', S('color:var(--ui-text-muted);font-size:12px', 'Events'), D('display:flex;gap:6px;flex-wrap:wrap', chipR('Open day'), chipR('Lime plaster workshop'), S('font-size:12px;color:var(--ui-text-muted);align-self:center', '+1')))))
|
||
main = Page(pad, Title('Edit content type', 'Events · Relation field, shown as it will work later.'), D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:16px;overflow:hidden', insp))
|
||
} else if (k === 'form') {
|
||
shell.nav = 'content'; shell.crumbs = ['Content', 'Events', 'Lime plaster workshop']; shell.mTitle = 'Lime plaster workshop'; shell.contentOn = 'events'
|
||
const res = D('position:absolute;left:0;right:0;top:64px;z-index:5;border:1px solid var(--ui-border);background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);padding:4px', ...[['Rue Verte hall', 'Lyon · 120 seats'], ['Rue Verte yard', 'Lyon · outdoor'], ['Rue du Port studio', 'Nantes · 24 seats']].map(([a, b], i) => D('min-height:' + (m ? 48 : 40) + 'px;padding:4px 8px;display:flex;flex-direction:column;justify-content:center;background:' + (i === 0 ? 'var(--ui-bg-elevated)' : 'transparent'), S('font-weight:500', a), S('font-size:12px;color:var(--ui-text-muted)', b))))
|
||
const chipX = t => S('height:' + (m ? 32 : 24) + 'px;padding:0 4px 0 8px;display:inline-flex;align-items:center;gap:6px;background:var(--rc-sand);color:var(--rc-on-sand);font-weight:500', t, I('x', 14))
|
||
main = Page(pad, Title('Lime plaster workshop', 'Events · Entry 42', Later('Relation field: later')), D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:' + (m ? 16 : 24) + 'px;display:flex;flex-direction:column;gap:' + (m ? 16 : 20) + 'px;max-width:760px;overflow:visible;flex:1;min-height:0', Ctl(EV[0], { v: 'Lime plaster workshop', count: '21 / 80' }),
|
||
D('display:flex;flex-direction:column;gap:6px;position:relative', S('font-weight:500;color:var(--ui-text-highlighted)', 'Venue'), F('', 'rue', { focus: true, ico: 'search' }), res, S('height:176px')),
|
||
D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;color:var(--ui-text-highlighted)', 'Speakers'), D('display:flex;gap:6px;flex-wrap:wrap;align-items:center;min-height:' + ih + 'px;padding:4px 8px;border:1px solid var(--ui-border-accented)', chipX('Inès Duarte'), chipX('Jonas Pereira'), S('color:var(--ui-text-muted);margin-left:4px', 'Search to add')), S('color:var(--ui-text-muted);font-size:12px', 'An event can have many speakers.'))))
|
||
} else {
|
||
shell.nav = 'content'; shell.crumbs = ['Content', 'Events']; shell.mTitle = 'Events'; shell.contentOn = 'events'
|
||
const chipR = t => S('height:20px;padding:0 6px;display:inline-flex;align-items:center;background:var(--rc-sand);color:var(--rc-on-sand);font-size:12px;font-weight:500;white-space:nowrap', t)
|
||
const gc = '48px minmax(0,1.4fr) 110px 200px 200px'
|
||
const rr = [[42, 'Lime plaster workshop', '21 Nov 2026', [chipR('Rue Verte hall')], [chipR('Inès Duarte'), chipR('Jonas Pereira'), S('color:var(--ui-text-muted);font-size:12px', '+2')]], [41, 'Heat pumps: what the numbers say', '3 Nov 2026', [S('color:var(--ui-text-muted)', '–')], [chipR('Jonas Pereira')]], [40, 'Open day at the Rue Verte site', '12 Nov 2026', [chipR('Rue Verte yard')], [S('color:var(--ui-text-muted)', '–')]]]
|
||
main = Page(pad, Title('Events', 'events · 9 fields', Later('Relation columns: later')), m ? D('background:var(--ui-bg);border:1px solid var(--ui-border)', ...rr.map(r => D('padding:12px 16px;border-top:1px solid var(--ui-border);display:flex;flex-direction:column;gap:6px', S('font-weight:500;font-size:15px', r[1]), D('display:flex;gap:6px;align-items:center', S('font-size:12px;color:var(--ui-text-muted)', 'Venue'), ...r[3]), D('display:flex;gap:6px;align-items:center;flex-wrap:wrap', S('font-size:12px;color:var(--ui-text-muted)', 'Speakers'), ...r[4])))) : D('background:var(--ui-bg);border:1px solid var(--ui-border)', D('display:grid;grid-template-columns:' + gc + ';gap:12px;height:36px;align-items:center;padding:0 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500', S('', 'ID'), S('', 'Title'), S('', 'Starts on'), S('', 'Venue'), S('', 'Speakers')), ...rr.map(r => D('display:grid;grid-template-columns:' + gc + ';gap:12px;height:48px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border)', S(mono + 'font-size:12px;color:var(--ui-text-muted)', String(r[0])), S('font-weight:500;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap', r[1]), S('', r[2]), D('display:flex;gap:4px;align-items:center', ...r[3]), D('display:flex;gap:4px;align-items:center', ...r[4])))), S('color:var(--ui-text-muted);font-size:12px;line-height:18px', 'One chip for a single relation. Up to two chips and a "+N" for many. Empty shows a dash.'))
|
||
}
|
||
}
|
||
|
||
// 8 permissions (later)
|
||
if (v === 'perms') {
|
||
shell.nav = 'settings'; shell.crumbs = ['Settings', 'Permissions']; shell.mTitle = 'Permissions'; shell.mLead = 'arrow-left'; shell.sideOpen = true
|
||
const ch = k === 'changed'
|
||
const rowsC = [['Events', 'events', [1, 1, 1, 1]], ['Recipes', 'recipes', ch ? [1, 1, 1, 0] : [1, 1, 1, 1]], ['Press mentions', 'press-mentions', ch ? [1, 0, 0, 0] : [1, 1, 1, 1]]]
|
||
const rowsB = [['Articles', [1, 1, 1, 1]], ['Authors', [1, 1, 1, 0]], ['Sites', [1, 0, 0, 0]], ['Media', [1, 1, 1, 1]], ['Users', [0, 0, 0, 0]]]
|
||
const A4 = ['Read', 'Create', 'Update', 'Delete']
|
||
const cols = 'minmax(0,1fr) 80px 80px 80px 80px'
|
||
const cellP = (on, lock, changed) => D('display:flex;align-items:center;justify-content:center;gap:4px;opacity:' + (lock ? 0.55 : 1), Chk(!!on), changed && S('width:6px;height:6px;background:var(--ui-info)'))
|
||
const desk = D('background:var(--ui-bg);border:1px solid var(--ui-border)', D('display:grid;grid-template-columns:' + cols + ';gap:8px;height:36px;align-items:center;padding:0 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500', S('', 'Content type'), ...A4.map(a => S('text-align:center', a))),
|
||
...rowsC.map(r => D('display:grid;grid-template-columns:' + cols + ';gap:8px;height:44px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border)', D('display:flex;align-items:center;gap:8px', S('font-weight:500', r[0]), S(mono + 'font-size:12px;color:var(--ui-text-muted)', r[1])), ...r[2].map((x, i) => cellP(x, false, ch && ((r[0] === 'Recipes' && i === 3) || (r[0] === 'Press mentions' && i > 0)))))),
|
||
D('display:flex;align-items:center;gap:8px;padding:8px 12px;border-top:1px solid var(--ui-border);background:var(--ui-bg-muted);color:var(--ui-text-toned);font-size:12px', I('lock', 14), 'Built-in types. RenewCMS sets these rules.'),
|
||
...rowsB.map(r => D('display:grid;grid-template-columns:' + cols + ';gap:8px;height:44px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border)', D('display:flex;align-items:center;gap:8px', I('lock', 14, 'color:var(--ui-text-muted)'), S('font-weight:500', r[0]), Bd('Built-in', 'built')), ...r[1].map(x => cellP(x, true)))))
|
||
const mob = D('display:flex;flex-direction:column;gap:12px', ...rowsC.map(r => D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:12px 16px;display:flex;flex-direction:column;gap:2px', S('font-weight:600;font-size:15px;color:var(--ui-text-highlighted)', r[0]), ...r[2].map((x, i) => D('display:flex;align-items:center;gap:12px;min-height:44px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : 'none'), Chk(!!x), S('flex:1', A4[i]), ch && ((r[0] === 'Recipes' && i === 3) || (r[0] === 'Press mentions' && i > 0)) && Bd('Changed', 'chg'))))), D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:12px 16px;display:flex;align-items:center;gap:10px;min-height:48px', I('lock', 16, 'color:var(--ui-text-muted)'), S('flex:1', '5 built-in types'), S('color:var(--ui-text-muted);font-size:12px', 'Set by RenewCMS'), I('chevron-right', 14, 'color:var(--ui-text-muted)')))
|
||
const foot = D('flex-shrink:0;display:flex;align-items:center;gap:8px;padding:12px ' + (m ? 16 : 24) + 'px;border-top:1px solid var(--ui-border);background:var(--ui-bg)', S('flex:1;font-size:12px;color:' + (ch ? 'var(--ui-warning)' : 'var(--ui-text-muted)'), ch ? '4 changes not saved' : 'No changes'), B('Discard', { dis: !ch }), B('Save permissions', { k: 'primary', dis: !ch }))
|
||
main = D('flex:1;min-width:0;min-height:0;display:flex;flex-direction:column', Page(pad, Title('Permissions', 'What each role can do with each content type.', Later()), Alert('info', 'info', 'Today\'s rules are the starting point', 'Owners and admins edit the schema. Any verified user can read, add, edit and delete entries.'), Seg([['owner', 'Owner'], ['admin', 'Admin'], ['user', 'User']], 'user', m ? '100%' : '320px'), m ? mob : desk, D('display:flex;align-items:center;gap:8px;color:var(--ui-text-toned);font-size:12px;line-height:18px;background:var(--ui-bg-elevated);padding:10px 12px', I('lock', 14), S('', 'Schema (create, edit and delete content types): owners and admins only. Set by RenewCMS.'))), foot)
|
||
}
|
||
|
||
// 9 export / import (later)
|
||
if (v === 'xport') {
|
||
Object.assign(shell, { nav: 'schema', crumbs: ['Schema'], mTitle: 'Schema' })
|
||
main = Page(pad, Title('Schema', 'Content types and the fields they are made of.', B('Import', { icon: 'upload' }), B('Export', { icon: 'download' }), Later(), B('New content type', { k: 'primary', icon: 'plus', px: 14 })), typesTable(true))
|
||
const rd = (on, t, sub) => D('display:flex;gap:10px;align-items:flex-start;padding:10px 12px;border:1px solid ' + (on ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (on ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)'), S('width:16px;height:16px;margin-top:2px;flex-shrink:0;border:1px solid currentColor;display:flex;align-items:center;justify-content:center', on && S('width:8px;height:8px;background:currentColor')), D('display:flex;flex-direction:column', S('font-weight:500', t), S('font-size:12px;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)'), sub)))
|
||
if (k === 'export') ov.push(Shell({ title: 'Export schema', desc: 'Download the fields of your content types as a file. Entries aren\'t included.', close: true, later: true, cancel: 'Cancel', confirm: 'Download file' }, [D('display:flex;flex-direction:column;gap:8px', rd(true, 'All content types', '3 content types'), rd(false, 'One content type', 'Choose which one')), F('File name', 'renewcms-schema.json', { mono: true, dis: true, help: 'Import this file on another RenewCMS site to create the same content types.' })]))
|
||
else {
|
||
const ref = k === 'refused'
|
||
const rowI = (ic, t, text, flag, tag) => D('display:flex;gap:10px;padding:10px 12px;border:1px solid var(--ui-border);background:' + (flag ? 'var(--rc-warning-soft)' : 'var(--ui-bg)'), S('margin-top:2px;display:flex;color:' + (flag ? 'var(--ui-warning)' : 'var(--ui-text-toned)'), I(ic)), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', D('display:flex;align-items:center;gap:8px;flex-wrap:wrap', S('font-weight:600;color:var(--ui-text-highlighted)', t), tag && Bd(tag, 'chg'), flag && Bd(flag, 'warn', 'triangle-alert')), S('color:var(--ui-text-toned);text-wrap:pretty', text)))
|
||
ov.push(Shell({ title: 'Import schema', desc: 'renewcms-schema.json · 3 content types', w: 600, top: 70, close: true, later: true, cancel: 'Cancel', confirm: ref ? 'Try again' : 'Import schema', cdis: false }, [ref ? Alert('error', 'circle-x', 'Nothing was imported', 'Some entries on this site don\'t fit the imported fields. RenewCMS can\'t tell which entries. The flagged change can cause this.') : S('font-weight:600;font-size:15px;color:var(--ui-text-highlighted)', 'Create 1, change 1, no change 1'), D('display:flex;flex-direction:column;gap:8px', rowI('plus', 'Recipes', 'New content type with 4 fields.', null, 'Create'), rowI('pencil', 'Events', 'Add 2 fields, remove Description. Events has 37 entries on this site.', 'Removes data', 'Change'), rowI('equal', 'Press mentions', 'Same as on this site.', null, null)), S('color:var(--ui-text-muted);font-size:12px;line-height:18px', 'Existing entries are checked against the changes when you import. If any entry doesn\'t fit, nothing is imported.')]))
|
||
}
|
||
}
|
||
|
||
if (m && v !== 'gallery' && s.drawer) shell.drawer = true
|
||
const framed = !bare
|
||
return { framed, bare, shell, mob: m, main, ov: D('display:contents', ...ov), bw, bh: bhh }
|
||
}
|
||
}
|
||
</script>
|
||
</body>
|
||
</html>
|