refac: migration crash, real-database tests, site deleted during create, ruels out of the repo, lists

This commit is contained in:
Florian Sylvain committed 2026-11-03 05:06:04 +01:00
1 parent 680b9a1308
commit 9d284e65a9
62 files changed
+1858 -987

No files matched your search

+18 -14
View File
@@ -148,11 +148,11 @@
<div style="padding:{{ pad }};display:flex;flex-direction:column;gap:14px;max-width:560px"><span style="font:500 13px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">404</span><span style="font:600 24px/32px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">This article doesn't exist</span><span style="color:var(--ui-text-toned);font-size:14px;line-height:22px">It may have been deleted, or the link is wrong.</span><span data-act="nav-articles" style="align-self:flex-start;height:{{ btnH }};padding:0 12px;display:flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500">All articles</span></div>
</sc-if>
<sc-if value="{{ edFound }}">
<div style="flex-shrink:0;background:var(--ui-bg);border-bottom:1px solid var(--ui-border);padding:{{ edHeadPad }};display:flex;flex-wrap:wrap;align-items:center;gap:{{ edHeadGap }}">
<div style="flex-shrink:0;background:var(--ui-bg);border-bottom:1px solid var(--ui-border);padding:{{ edHeadPad }};display:flex;flex-wrap:wrap;align-items:center;gap:{{ edHeadGap }}">{{ edHeader }}<sc-if value="{{ edDefHead }}">
<sc-if value="{{ desk }}"><span data-act="back" style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/arrow-left.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/arrow-left.svg') center / contain no-repeat"></span></span></sc-if>
<div style="flex:1 1 {{ edTitleBasis }};min-width:0;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<sc-if value="{{ s.ed.loading }}"><span style="width:320px;max-width:70%;height:20px;background:var(--ui-bg-accented)"></span><span style="width:60px;height:22px;background:var(--ui-bg-accented)"></span></sc-if>
<sc-if value="{{ edReady }}"><span title="The title can't be changed after creation." style="font:600 18px/26px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ edTitle }}</span><sc-if value="{{ edPending }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--ui-border-accented);color:var(--ui-text-muted);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat"></span>{{ s.ed.pending }}</span></sc-if><sc-if value="{{ edOnline }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat"></span>Online</span></sc-if><sc-if value="{{ edDraft }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat"></span>Draft</span></sc-if></sc-if>
<sc-if value="{{ edReady }}"></sc-if><span title="{{ edTitleTip }}" style="font:600 18px/26px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted);{{ edTitleCss }}">{{ edTitle }}</span><sc-if value="{{ lg }}"><span role="button" aria-haspopup="listbox" aria-expanded="{{ lg.open }}" aria-label="{{ lg.label }}" style="height:{{ lg.h }};max-width:100%;width:max-content;padding:0 8px;display:inline-flex;align-items:center;gap:6px;border:1px solid {{ lg.bd }};background:var(--ui-bg);color:var(--ui-text-highlighted);font-size:13px;font-weight:500;white-space:nowrap;flex-shrink:0"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/languages.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/languages.svg') center / contain no-repeat"></span><span>{{ lg.name }}</span><span style="flex-shrink:0;font:400 12px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">{{ lg.code }}</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevrons-up-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevrons-up-down.svg') center / contain no-repeat"></span></span><sc-if value="{{ lg.ro }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;background:var(--ui-bg-elevated);color:var(--ui-text-toned);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/lock.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/lock.svg') center / contain no-repeat"></span>Read-only</span></sc-if><sc-if value="{{ lg.fresh }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--ui-warning);background:var(--ui-bg);color:var(--ui-warning);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/refresh-cw.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/refresh-cw.svg') center / contain no-repeat"></span>{{ lg.fresh }}</span></sc-if><sc-if value="{{ edPending }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--ui-border-accented);color:var(--ui-text-muted);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat"></span>{{ s.ed.pending }}</span></sc-if><sc-if value="{{ edOnline }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat"></span>Online</span></sc-if><sc-if value="{{ edDraft }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat"></span>Draft</span></sc-if></sc-if>
</div>
<sc-if value="{{ edReady }}">
<span aria-live="polite" style="display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0;color:{{ save.fg }}"><span aria-hidden="true" style="display:inline-block;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ save.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ save.icon }}.svg') center / contain no-repeat"></span>{{ save.t }}<sc-if value="{{ save.retry }}"><span data-act="save" style="height:24px;padding:0 8px;display:flex;align-items:center;gap:4px;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat"></span>Retry</span></sc-if></span>
@@ -161,17 +161,17 @@
<span data-act="preview" style="display:flex;align-items:center;gap:8px;white-space:nowrap;flex-shrink:0;color:var(--ui-text-toned)"><span style="width:32px;height:18px;padding:2px;display:flex;justify-content:{{ pvJust }};background:{{ pvBg }}"><span style="width:14px;height:14px;background:var(--ui-bg)"></span></span>Preview</span>
<span data-act="save" style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-weight:500;opacity:{{ saveOp }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/save.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/save.svg') center / contain no-repeat"></span>Save</span>
<span data-act="publish" style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0;font-weight:500;background:{{ pub.bg }};color:{{ pub.fg }};border:1px solid {{ pub.bd }};opacity:{{ pub.op }}"><span aria-hidden="true" style="display:inline-block;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ pub.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ pub.icon }}.svg') center / contain no-repeat"></span>{{ pub.t }}</span>
<span data-act="overflow" role="button" aria-label="Delete article" style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0;font-weight:500;border:1px solid var(--ui-error);color:var(--ui-error);cursor:pointer"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat"></span>Delete</span>
<span data-act="overflow" role="button" aria-label="Delete article" style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0;font-weight:500;border:1px solid var(--ui-error);color:var(--ui-error);cursor:pointer"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat"></span>{{ delText }}</span>
</sc-if>
</sc-if>
</div>
<sc-if value="{{ showToolbar }}">
</sc-if></div>
{{ edBanner }}<sc-if value="{{ showToolbar }}">
<div role="toolbar" aria-label="Formatting" style="flex-shrink:0;padding:6px {{ tbPadX }};display:flex;align-items:center;gap:2px;border-bottom:1px solid var(--ui-border);position:relative;overflow:{{ tbOverflow }};background:var(--ui-bg)">
<div style="display:flex;align-items:center;gap:2px;flex-shrink:0;transform:translateX({{ tbShift }})"><sc-for list="{{ toolbar }}" as="t" hint-placeholder-count="20"><sc-if value="{{ t.sep }}"><span style="width:1px;height:20px;background:var(--ui-border-accented);margin:0 6px;flex-shrink:0"></span></sc-if><sc-if value="{{ t.btn }}"><span data-act="tb-{{ t.icon }}" style="height:{{ tbBtn }};min-width:{{ tbBtn }};padding:0 6px;flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:4px;position:relative;color:{{ t.fg }};opacity:{{ t.op }};outline:{{ t.ring }};outline-offset:1px;background:{{ t.bg }}"><span aria-hidden="true" style="display:inline-block;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ t.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ t.icon }}.svg') center / contain no-repeat"></span>{{ t.text }}<sc-if value="{{ t.tip }}"><span role="tooltip" style="position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:6px;z-index:30;white-space:nowrap;padding:4px 8px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font:400 12px/16px 'IBM Plex Sans',sans-serif">{{ t.tip }}</span></sc-if></span></sc-if></sc-for></div><sc-if value="{{ tbFadeL }}"><span style="position:absolute;left:0;top:0;bottom:0;width:24px;background:linear-gradient(to right,var(--ui-bg),transparent);pointer-events:none"></span></sc-if><sc-if value="{{ tbFadeR }}"><span style="position:absolute;right:0;top:0;bottom:0;width:32px;background:linear-gradient(to left,var(--ui-bg),transparent);pointer-events:none"></span></sc-if>
</div>
</sc-if>
<div style="flex:1;min-height:0;display:flex">
<div style="flex:1;min-width:0;display:flex;flex-direction:column;background:{{ edBg }}">
{{ srcPane }}<div style="flex:1;min-width:0;display:flex;flex-direction:column;background:{{ edBg }}">
<div data-act="type" style="flex:1;min-height:0;overflow:hidden;padding:{{ edPad }};font-family:'Source Serif 4',serif;color:var(--ui-text-highlighted)">
<sc-if value="{{ s.ed.loading }}"><div style="display:flex;flex-direction:column;gap:14px;max-width:672px"><span style="height:14px;background:var(--ui-bg-accented)"></span><span style="height:14px;background:var(--ui-bg-accented);width:92%"></span><span style="height:14px;background:var(--ui-bg-accented);width:80%"></span><span style="height:22px;background:var(--ui-bg-accented);width:45%;margin-top:18px"></span><span style="height:14px;background:var(--ui-bg-accented)"></span><span style="height:14px;background:var(--ui-bg-accented);width:70%"></span></div></sc-if>
<sc-if value="{{ edReady }}">
@@ -181,7 +181,7 @@
<div style="font-size:{{ h2F }};line-height:1.3;font-weight:600;margin:32px 0 12px">What goes into the price</div>
<ul style="margin:0 0 20px;padding-left:28px;list-style:square;font-size:{{ pF }};line-height:1.66"><li>Stripping out and making good</li><li>Plumbing moves, if any</li><li>Tiles, fittings and a day for silicone</li></ul>
<sc-if value="{{ s.ed.image }}"><div style="margin:28px 0;aspect-ratio:16/9;background:var(--ui-bg-elevated);border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);font:400 13px 'IBM Plex Sans',sans-serif">/static/uploadedImages/3f9c…e1.jpg</div></sc-if>
<p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">We don't add a margin on materials you choose yourself.<sc-if value="{{ caret }}"><span style="display:inline-block;width:2px;height:1.1em;background:var(--ui-text-highlighted);vertical-align:-3px;margin-left:1px"></span></sc-if></p></sc-if><sc-if value="{{ edBody }}"><p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">{{ edBody }}<sc-if value="{{ caret }}"><span style="display:inline-block;width:2px;height:1.1em;background:var(--ui-text-highlighted);vertical-align:-3px;margin-left:1px"></span></sc-if></p><sc-if value="{{ s.ed.image }}"><div style="margin:28px 0;aspect-ratio:16/9;background:var(--ui-bg-elevated);border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);font:400 13px 'IBM Plex Sans',sans-serif">/static/uploadedImages/3f9c…e1.jpg</div></sc-if></sc-if>
<p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">We don't add a margin on materials you choose yourself.<sc-if value="{{ caret }}"><span style="display:inline-block;width:2px;height:1.1em;background:var(--ui-text-highlighted);vertical-align:-3px;margin-left:1px"></span></sc-if></p></sc-if>{{ edBodyEl }}<sc-if value="{{ edBody }}"><p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">{{ edBody }}<sc-if value="{{ caret }}"><span style="display:inline-block;width:2px;height:1.1em;background:var(--ui-text-highlighted);vertical-align:-3px;margin-left:1px"></span></sc-if></p><sc-if value="{{ s.ed.image }}"><div style="margin:28px 0;aspect-ratio:16/9;background:var(--ui-bg-elevated);border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);font:400 13px 'IBM Plex Sans',sans-serif">/static/uploadedImages/3f9c…e1.jpg</div></sc-if></sc-if>
</div>
</sc-if>
</div>
@@ -195,17 +195,17 @@
<div style="display:flex;flex-direction:column;gap:12px;padding-bottom:16px;border-bottom:1px solid var(--ui-border)"><div data-act="meta-toggle" role="button" tabindex="0" aria-expanded="{{ metaOpen }}" aria-controls="meta-body" style="display:flex;align-items:center;gap:8px;min-height:24px;color:var(--ui-text-highlighted)"><span style="display:flex;transform:rotate({{ metaRot }})"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span><span style="font-weight:600;font-size:14px">Site and authors</span></div><sc-if value="{{ metaOpen }}"><div id="meta-body" style="display:flex;flex-direction:column;gap:16px">{{ edMeta }}</div></sc-if><sc-if value="{{ metaClosed }}"><span style="padding-left:22px;color:var(--ui-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ metaSummary }}</span></sc-if></div><div style="display:flex;flex-direction:column;gap:12px;padding-bottom:16px;border-bottom:1px solid var(--ui-border)"><div data-act="seo-toggle" role="button" tabindex="0" aria-expanded="{{ seoOpen }}" aria-controls="seo-body" style="display:flex;align-items:center;gap:8px;min-height:24px;color:var(--ui-text-highlighted)"><span style="display:flex;transform:rotate({{ seoRot }})"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span><span style="font-weight:600;font-size:14px">Search and sharing</span></div><sc-if value="{{ seoOpen }}"><div id="seo-body" style="display:flex;flex-direction:column;gap:12px">{{ edSeo }}</div></sc-if><sc-if value="{{ seoClosed }}"><span style="padding-left:22px;color:{{ seoFg }};overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ seoSummary }}</span></sc-if></div>
<div style="display:grid;grid-template-columns:auto 1fr;gap:8px 12px"><span style="color:var(--ui-text-muted)">Created</span><span>12 Mar 2026, 09:14</span><span style="color:var(--ui-text-muted)">Updated</span><span>Today, 11:02</span><span style="color:var(--ui-text-muted)">ID</span><span style="font-family:'IBM Plex Mono',monospace">12</span><span style="color:var(--ui-text-muted)">Images</span><span>{{ imgCount }}</span></div>
<div style="display:flex;flex-direction:column;gap:6px"><span style="color:var(--ui-text-muted)">Public API URL</span><span style="display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--ui-bg-elevated);font:400 12px 'IBM Plex Mono',monospace"><span style="flex:1">/v1/article/12</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/copy.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/copy.svg') center / contain no-repeat"></span></span><span style="color:var(--ui-text-muted);font-size:12px;line-height:18px">{{ apiNote }}</span></div>
<div style="display:flex;flex-direction:column;gap:4px;padding-top:12px;border-top:1px solid var(--ui-border)"><span style="color:var(--ui-text-muted)">Title</span><span style="font-size:12px;line-height:18px;color:var(--ui-text-toned)">Titles can't be changed after an article is created. To use a different title, create a new article and delete this one.</span></div>
<div style="display:flex;flex-direction:column;gap:4px;padding-top:12px;border-top:1px solid var(--ui-border)"><span style="color:var(--ui-text-muted)">Title</span><span style="font-size:12px;line-height:18px;color:var(--ui-text-toned)">{{ edTitleNote }}</span></div>
</sc-if>
</div>
</sc-if>
</div>
<sc-if value="{{ mob }}"><sc-if value="{{ edReady }}">
<sc-if value="{{ mob }}">{{ edMobBar }}<sc-if value="{{ edDefMob }}"><sc-if value="{{ edReady }}">
<div style="flex-shrink:0;padding:12px 16px;border-top:1px solid var(--ui-border);background:var(--ui-bg);display:flex;flex-direction:column;gap:8px">
<sc-if value="{{ counter.msg }}"><span style="color:{{ counter.fg }};font-size:13px;line-height:19px">{{ counter.msg }}</span></sc-if>
<div style="display:flex;gap:8px"><span data-act="details" style="width:44px;height:44px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/panel-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/panel-right.svg') center / contain no-repeat"></span></span><span data-act="save" style="flex:1;height:44px;padding:0 12px;white-space:nowrap;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-weight:500;font-size:15px;opacity:{{ saveOp }}">Save</span><span data-act="publish" style="flex:1 0 auto;padding:0 16px;white-space:nowrap;height:44px;display:flex;align-items:center;justify-content:center;gap:6px;font-weight:500;font-size:15px;background:{{ pub.bg }};color:{{ pub.fg }};border:1px solid {{ pub.bd }};opacity:{{ pub.op }}">{{ pub.t }}</span></div>
</div>
</sc-if></sc-if>
</sc-if></sc-if></sc-if>
</sc-if>
</sc-if>
@@ -396,7 +396,7 @@ class Component extends DCLogic {
const k = s.kind || 'home'
const navItems = s.role === 'user' ? [['home', 'house', 'Home'], ['articles', 'file-text', 'Articles'], ['media', 'images', 'Media'], ['authors', 'feather', 'Authors'], ['activity', 'history', 'Activity']] : [['home', 'house', 'Home'], ['articles', 'file-text', 'Articles'], ['media', 'images', 'Media'], ['authors', 'feather', 'Authors'], ['users', 'users', 'Users'], ['schema', 'shapes', 'Schema'], ['activity', 'history', 'Activity'], ['integration', 'plug', 'Integration'], ['settings', 'settings', 'Settings']]
const cr0 = s.crumbs || [], setOpen = s.nav === 'settings' || !!s.sideOpen, parentOn = (key) => s.nav === key && (key !== 'settings' || cr0.length <= 1)
const nav = navItems.map(([key, icon, t]) => { const on = parentOn(key), open = key === 'settings' && setOpen; return { key, icon, t, bg: on ? 'var(--rc-sand)' : 'transparent', badge: key === 'settings' && !!s.navBadge, fg: on ? 'var(--rc-on-sand)' : open ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)', fw: on || open ? 500 : 400, chev: key === 'settings' ? (open ? 'chevron-down' : 'chevron-right') : '', chevML: key === 'settings' && s.navBadge ? '8px' : 'auto', sub: open, hasSites: !s.role || s.role === 'owner', subs2: (!s.role || s.role === 'owner') ? (() => { const o = cr0[cr0.length - 1] === 'Sites'; return [{ t: 'Sites', ic: 'globe', bg: o ? 'var(--rc-sand)' : 'transparent', fg: o ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: o ? 500 : 400 }] })() : [], hasAccess: s.role !== 'user', subs3: s.role === 'user' ? [] : [(() => { const o = cr0[1] === 'Roles'; return { t: 'Roles', ic: 'shield', bg: o ? 'var(--rc-sand)' : 'transparent', fg: o ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: o ? 500 : 400 } })()], subs: ['Variants', 'Cache'].map(x => { const o = cr0[cr0.length - 1] === x; return { t: x, ic: x === 'Variants' ? 'layers' : 'database', bg: o ? 'var(--rc-sand)' : 'transparent', fg: o ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: o ? 500 : 400 } }) } })
const nav = navItems.map(([key, icon, t]) => { const on = parentOn(key), open = key === 'settings' && setOpen; return { key, icon, t, bg: on ? 'var(--rc-sand)' : 'transparent', badge: key === 'settings' && !!s.navBadge, fg: on ? 'var(--rc-on-sand)' : open ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)', fw: on || open ? 500 : 400, chev: key === 'settings' ? (open ? 'chevron-down' : 'chevron-right') : '', chevML: key === 'settings' && s.navBadge ? '8px' : 'auto', sub: open, hasSites: !s.role || s.role === 'owner', subs2: (!s.role || s.role === 'owner') ? (() => { const o = cr0[cr0.length - 1] === 'Sites'; const q = cr0[cr0.length - 1] === 'Languages'; return [{ t: 'Sites', ic: 'globe', bg: o ? 'var(--rc-sand)' : 'transparent', fg: o ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: o ? 500 : 400 }, { t: 'Languages', ic: 'languages', bg: q ? 'var(--rc-sand)' : 'transparent', fg: q ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: q ? 500 : 400 }] })() : [], hasAccess: s.role !== 'user', subs3: s.role === 'user' ? [] : [(() => { const o = cr0[1] === 'Roles'; return { t: 'Roles', ic: 'shield', bg: o ? 'var(--rc-sand)' : 'transparent', fg: o ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: o ? 500 : 400 } })()], subs: ['Variants', 'Cache'].map(x => { const o = cr0[cr0.length - 1] === x; return { t: x, ic: x === 'Variants' ? 'layers' : 'database', bg: o ? 'var(--rc-sand)' : 'transparent', fg: o ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: o ? 500 : 400 } }) } })
const CT = s.content || [], ctOn = s.contentOn, ctMax = 5, ctVis = CT.slice(0, Math.max(ctMax, CT.findIndex(x => x.key === ctOn) + 1)), contentRows = (m ? (s.contentOpen ? ctVis : []) : ctVis).map(x => { const on = x.key === ctOn; return { key: x.key, name: x.name, icon: x.icon || 'file-text', bg: on ? 'var(--rc-sand)' : 'transparent', fg: on ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: on ? 500 : 400 } })
const crumbs = (s.crumbs || []).map((t, i, a) => ({ t, sep: i > 0, fg: i === a.length - 1 ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)', fw: i === a.length - 1 ? 500 : 400 }))
const L = s.list || {}
@@ -541,16 +541,20 @@ class Component extends DCLogic {
const toolbar = toolbarDef.map(x => { if (x === '|') return { sep: true, btn: false }; const base = { sep: false, btn: true, icon: x[0], text: x[1] || '', bg: x[2] ? 'var(--ui-bg-accented)' : 'transparent', fg: 'var(--ui-text-toned)', op: '1', ring: 'none', tip: '' }; if (x[0] !== 'link') return base; return { ...base, bg: lbs === 'hover' ? 'var(--ui-bg-elevated)' : lbs === 'active' ? 'var(--rc-sand)' : 'transparent', fg: lbs === 'hover' ? 'var(--ui-text-highlighted)' : lbs === 'active' ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', op: lbs === 'disabled' ? '0.4' : '1', ring: lbs === 'focus' ? '2px solid var(--ui-primary)' : 'none', tip: lbs === 'hover' && !m ? 'Insert link (Ctrl+K)' : '' } })
const lkT = LK.text || 'default'
const lk = { on: !!LK.on, pop: !!LK.pop && !m, sheet: !!LK.pop && m, popEdit: 'transparent', style: 'color:' + (lkT === 'selected' ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';text-decoration:underline;text-decoration-thickness:' + (lkT === 'hover' ? '2px' : '1px') + ';text-underline-offset:3px;' + (lkT === 'selected' ? 'background:var(--rc-sand);' : '') + (lkT === 'focus' ? 'outline:2px solid var(--ui-primary);outline-offset:2px;' : '') }
const LG = e.lang || null
if (LG && !e.pending && pub) pub = { ...pub, t: pub.t.replace('Save and publish', 'Save and publish ' + LG.name).replace(/^Publish$/, 'Publish ' + LG.name).replace(/^Unpublish$/, 'Unpublish ' + LG.name) }
const lg = LG ? { name: LG.name, code: LG.code, open: !!LG.open, ro: !!LG.ro, fresh: LG.fresh || '', bd: LG.open ? 'var(--ui-primary)' : 'var(--ui-border-accented)', h: m ? '44px' : '26px', label: 'Language: ' + LG.name + '. Switch version' } : null
const edReady = k === 'editor' && !e.loading && !e.notfound
const edTitle = e.title || 'How we price a bathroom renovation'
return {
lg, srcPane: this.props.srcPane || null, edBanner: this.props.edBanner || null, edTitleCss: LG ? 'flex:0 1 100%;min-width:0' : '', edTitleNote: LG ? 'Each language has its own title. Changing it doesn\'t change the URL; edit the slug in Search and sharing.' : "Titles can't be changed after an article is created. To use a different title, create a new article and delete this one.", edTitleTip: LG ? 'Titles can be changed. The URL only changes if you edit the slug.' : "The title can't be changed after creation.", delText: LG ? 'Delete ' + LG.name + ' version' : 'Delete',
lk, edSeo: this.props.edSeo || null, seoOpen: !!this.props.edSeo, seoClosed: !this.props.edSeo, seoRot: this.props.edSeo ? '0deg' : '-90deg', seoSummary: e.seoSummary || 'Ready', seoFg: /suggest/.test(e.seoSummary || '') ? 'var(--ui-warning)' : 'var(--ui-text-muted)', naOn: !!L.needs, naBg: L.needs ? 'var(--rc-sand)' : 'var(--ui-bg)', naFg: L.needs ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', naBd: L.needs ? 'var(--rc-sand)' : 'var(--ui-border-accented)', naFw: L.needs ? 500 : 400, naN: L.naState === 'pending' ? '…' : L.naState === 'fail' ? '!' : L.naState === 'absent' ? '' : String(L.needsN ?? 3), edMeta: this.props.edMeta || null, metaOpen: !!this.props.edMeta, metaClosed: !this.props.edMeta, metaRot: this.props.edMeta ? '0deg' : '-90deg', metaSummary: e.metaSummary || 'Inès Duarte', edSheet: m && !!e.sheet, pAu: (() => { const q = s.pa || {}; return { none: q.k !== 'has', has: q.k === 'has', err: !!q.err, slug: q.slug || 'marta-alves', slugNote: !!q.slug, btn: q.loading ? 'Creating…' : 'Create my author profile', icon: q.loading ? 'loader-circle' : 'feather', op: q.loading ? 0.7 : 1, uses: q.user ? 'Uses your username, marta. You can change it afterwards.' : 'Uses your display name, Marta Alves. You can change it afterwards.' } })(), showSites: !s.role || s.role === 'owner', authorsCol: L.authors !== false, multi: !!L.multi, multiDesk: !m && !!L.multi, multiMob: m && !!L.multi, gridCols: (L.authors !== false ? 'minmax(0,1fr) 104px ' : 'minmax(0,1fr) ') + (L.multi ? '130px ' : '') + '110px 150px 140px 44px', sfText: L.site || 'All sites', sfFg: L.site ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)', sfMenu: !!L.siteMenu, sfOpts: ['All sites', 'Renew', 'Journal', 'Projects', 'Archive'].map(x => { const on = (L.site || 'All sites') === x; return { t: x, on, op: on ? 1 : 0, bg: on ? 'var(--ui-bg-elevated)' : 'transparent' } }), tbOverflow: m ? 'hidden' : 'visible', tbShift: m ? -(LK.shift || 0) + 'px' : '0px', tbFadeL: m && !!LK.shift, tbFadeR: m && !LK.end, act, cl, rt, purgeDlg, rowDir: m ? 'column' : 'row', rowAlign: m ? 'stretch' : 'flex-start', rowJust: m ? 'flex-start' : 'space-between', rowGap: m ? '12px' : '16px', setScroll: -(s.scrollY || 0) + 'px', offText: s.offText || 'You\'re offline. You can keep writing, but saving and publishing will wait until you\'re back online.', cr, s, nav, crumbs, who, ini, pf, dn, dnSave, rsNote, rsBtn,
langSeg: [['en','English'],['fr','Français']].map(([k,tt])=>{const on=(s.lang||'en')===k;return {act:'lang-'+k,code:k,t:tt,on,op:on?1:0}}), themeSeg: [['system','monitor','System'],['light','sun','Light'],['dark','moon','Dark']].map(([k,icon,tt])=>{const on=(s.mode||'light')===k;return {act:'theme-'+k,icon,t:tt,on,op:on?1:0}}), menuOpen: !!s.userMenu, deskMenu: !m && !!s.userMenu, mobMenu: m && !!s.userMenu, pfItemBg: 'var(--ui-bg-elevated)',
chipBg: s.userMenu || s.nav === 'profile' ? 'var(--rc-sand)' : 'transparent', chipFg: s.userMenu || s.nav === 'profile' ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)',
isSettings: k === 'settings', isProfile: k === 'profile', pfLoading: k === 'profile' && !!pf.loading, pfReady: k === 'profile' && !pf.loading,
upBtnDir: m ? 'column' : 'row', upBtnAlign: m ? 'stretch' : 'flex-start', cardPad: '16px', cardPadX: m ? '16px' : '20px', inH: m ? '44px' : '40px', pfCols: m ? 'minmax(0,1fr)' : 'minmax(0,1fr) minmax(0,1.4fr)', pwDir: m ? 'column' : 'row', pwAlign: m ? 'stretch' : 'flex-end', edTitle, edBody: e.body || '', sampleBody: !e.body, tabs, rows, save, counter, pub, mFields, up, toasts, endpoints, toolbar,
upBtnDir: m ? 'column' : 'row', upBtnAlign: m ? 'stretch' : 'flex-start', cardPad: '16px', cardPadX: m ? '16px' : '20px', inH: m ? '44px' : '40px', pfCols: m ? 'minmax(0,1fr)' : 'minmax(0,1fr) minmax(0,1.4fr)', pwDir: m ? 'column' : 'row', pwAlign: m ? 'stretch' : 'flex-end', edTitle, edBody: e.body || '', edBodyEl: this.props.edBodyEl || null, sampleBody: !e.body && !this.props.edBodyEl, tabs, rows, save, counter, pub, mFields, up, toasts, endpoints, toolbar,
contentShow: CT.length > 0, contentRows, contentColl: m && !s.contentOpen, contentCount: String(CT.length), contentMore: (!m || s.contentOpen) && CT.length > ctVis.length ? 'Show all ' + CT.length : '', desk: !m, mob: m, notBoot: !s.boot,
fw: m ? '390px' : '1440px', fh: m ? '844px' : '900px', dir: m ? 'column' : 'row',
fw: m ? '390px' : (s.w ? s.w + 'px' : '1440px'), fh: m ? '844px' : (s.h ? s.h + 'px' : '900px'), dir: m ? 'column' : 'row',
mLead: s.mLead || 'menu',
navDrawer: m && !!s.drawer, integSearch: k === 'integration' && !!s.search, isSlot: !!this.props.slotBody, slotBody: this.props.slotBody || null, slotOverlays: this.props.slotOverlays || null, isBoot: !!s.boot, isHome: k === 'home' && !s.boot, isList: k === 'articles', isEditor: k === 'editor', isInteg: k === 'integration', isKeys: k === 'apikeys', isError: k === 'error', isUsers: k === 'users',
usRows, usError: !!US.error, usOnly: !!US.only && !US.error, usTable: !m && !US.error, usCards: m && !US.error,
@@ -577,7 +581,7 @@ class Component extends DCLogic {
emptyBg: L.empty && L.empty.primary ? 'var(--ui-primary)' : 'var(--ui-bg)', emptyFg: L.empty && L.empty.primary ? 'var(--ui-bg)' : 'var(--ui-text-highlighted)', emptyBd: L.empty && L.empty.primary ? 'var(--ui-primary)' : 'var(--ui-border-accented)',
edFound: k === 'editor' && !e.notfound, edReady, edPending: edReady && !!e.pending, edOnline: edReady && !e.pending && online, edDraft: edReady && !e.pending && !online,
edHeadPad: m ? '12px 16px' : '14px 24px', edHeadGap: m ? '8px 12px' : '12px', edTitleBasis: m ? '100%' : '200px',
showToolbar: k === 'editor' && !e.notfound && !e.preview,
showToolbar: k === 'editor' && !e.notfound && !e.preview && !e.readonly, edDefHead: !this.props.edHeader, edDefMob: !this.props.edMobBar, edHeader: this.props.edHeader || null, edMobBar: this.props.edMobBar || null,
tbPadX: m ? '8px' : '24px', tbBtn: m ? '40px' : '28px',
edBg: e.preview ? 'var(--ui-bg-muted)' : 'var(--ui-bg)', edPad: m ? '24px 16px' : '40px 48px', edMargin: e.preview ? '0 auto' : '0',
h1F: m ? '28px' : '32px', h2F: m ? '23px' : '26px', pF: m ? '17px' : '18px', caret: !e.preview,
+2 -2
View File
@@ -320,7 +320,7 @@ class Component extends DCLogic {
}
const newAuthorModal = () => {
const x = s.modal
return Shell({ z: 60, title: 'Create author', desc: 'Adds a byline for this article. You can add a bio and avatar later in Authors.', cancel: 'Cancel', confirm: x.loading ? 'Creating…' : 'Create author', loading: x.loading, close: true, cdis: !x.name },
return Shell({ z: 70, title: 'Create author', desc: 'Adds a byline for this article. You can add a bio and avatar later in Authors.', cancel: 'Cancel', confirm: x.loading ? 'Creating…' : 'Create author', loading: x.loading, close: true, cdis: !x.name },
D('display:flex;flex-direction:column;gap:16px', x.notice && Alert('error', 'circle-x', 'Couldn\'t create the author', 'The article is unchanged. Try again in a moment.'),
F('Name', x.name || 'e.g. Camille Roche', { val: !!x.name, focus: !x.name && !x.loading, err: x.err, dis: x.loading }),
Slug('Slug', { mode: x.name ? (x.err ? 'manual' : 'auto') : 'empty', value: x.slug, help: 'Generated from the name. Unique across all authors.', err: x.slugErr, suggest: x.suggest }),
@@ -427,7 +427,7 @@ class Component extends DCLogic {
if (s.seo && s.seo.panel) ov.push(seoPanel())
if (s.seo && s.seo.pick) ov.push(Picker({ title: 'Choose a cover image', desc: 'The cover for this article.', sel: 1, z: 70 }))
if (s.seo && s.seo.api) ov.push(apiDialog())
if (s.modal && m) { ov.push(Panel({ title: 'Details', z: 55, ro: true, confirm: '' }, metaUI())); edMeta = null }
if (s.modal && m) { ov.push(Panel({ title: 'Details', z: 55, ro: true, confirm: '' }, metaUI())); edMeta = null; if (shell && shell.ed) shell = { ...shell, ed: { ...shell.ed, sheet: false } } }
if (s.modal) ov.push(newAuthorModal())
if (s.ann) ov.push(D('position:absolute;z-index:20;' + (m ? 'left:16px;right:16px;bottom:88px' : 'left:248px;bottom:20px'), Note(s.ann)))
} else if (v === 'profile') {
+100 -65
View File
@@ -8,7 +8,7 @@
</head>
<body>
<x-dc>
<dc-import name="LocalizationShell" s="{{ shell }}" mobile="{{ mob }}" slot-body="{{ main }}" slot-overlays="{{ ov }}" src-pane="{{ srcPane }}" ed-banner="{{ edBanner }}" ed-body-el="{{ edBodyEl }}" ed-header="{{ edHeader }}" ed-mob-bar="{{ edMobBar }}" hint-size="100%,100%"></dc-import>
<dc-import name="AppFrame" s="{{ shell }}" mobile="{{ mob }}" slot-body="{{ main }}" slot-overlays="{{ ov }}" src-pane="{{ srcPane }}" ed-banner="{{ edBanner }}" ed-body-el="{{ edBodyEl }}" ed-header="{{ edHeader }}" ed-mob-bar="{{ edMobBar }}" hint-size="100%,100%"></dc-import>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1440,&quot;height&quot;:900},&quot;s&quot;:{&quot;editor&quot;:null,&quot;tsType&quot;:&quot;object&quot;},&quot;mobile&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:false,&quot;tsType&quot;:&quot;boolean&quot;}}">
class Component extends DCLogic {
@@ -24,7 +24,7 @@ class Component extends DCLogic {
const mono = "font-family:'IBM Plex Mono',monospace;font-size:12px"
const Btn = (l, k = 'secondary', o = {}) => {
const hh = m ? 44 : (o.sm ? 28 : 32)
const st = { primary: 'background:var(--ui-primary);color:var(--ui-bg)', secondary: 'background:var(--ui-bg);color:var(--ui-text-highlighted);border:1px solid var(--ui-border-accented)', ghost: 'color:var(--ui-text-toned)', danger: 'background:var(--ui-error);color:var(--ui-bg)' }[k]
const st = { primary: 'background:var(--ui-primary);color:var(--ui-bg)', secondary: 'background:var(--ui-bg);color:var(--ui-text-highlighted);border:1px solid var(--ui-border-accented)', ghost: 'color:var(--ui-text-toned)', danger: 'background:var(--ui-error);color:var(--ui-bg)', dangerOutline: 'background:var(--ui-bg);color:var(--ui-error);border:1px solid var(--ui-error)' }[k]
return A('span', { role: 'button', 'aria-label': o.al || (o.ic && !l ? o.ic : undefined), 'aria-disabled': !!o.dis || undefined, 'aria-busy': o.busy || undefined }, 'display:inline-flex;align-items:center;justify-content:center;gap:6px;height:' + hh + 'px;padding:0 ' + (o.ic && !l ? 0 : 12) + 'px;' + (o.ic && !l ? 'width:' + hh + 'px;' : '') + 'font-weight:500;white-space:nowrap;flex-shrink:0;' + st + ';' + (o.dis ? 'opacity:.4;' : '') + (o.w ? 'width:' + o.w + ';' : ''), o.busy && Ic('loader-circle', 14), o.ic && Ic(o.ic, 16), l)
}
const TN = { neutral: 'background:var(--ui-bg-elevated);color:var(--ui-text-toned)', success: 'background:var(--rc-success-soft);color:var(--ui-success)', info: 'background:var(--rc-info-soft);color:var(--ui-info)', warning: 'background:var(--rc-warning-soft);color:var(--ui-warning)', error: 'background:var(--rc-error-soft);color:var(--ui-error)' }
@@ -66,12 +66,13 @@ class Component extends DCLogic {
m && D('display:flex;justify-content:center;padding:8px 0 0;flex-shrink:0', S('width:36px;height:4px;background:var(--ui-border-accented)')),
D('padding:10px 12px 6px;display:flex;flex-direction:column;gap:6px;flex-shrink:0', S('font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;' + MU, 'Versions you can open'), o.find && Fd('', 'Find a language', { val: false })),
A('div', { role: 'menu', 'aria-label': 'Versions of this article' }, 'display:flex;flex-direction:column;overflow-y:auto;min-height:0', ...rows.map(LRow)),
D('padding:10px 12px;border-top:1px solid var(--ui-border);display:flex;gap:8px;flex-shrink:0;' + MU + ';font-size:12px;line-height:18px', Ic('info', 14), 'Switching opens that version. The interface language stays the same.'))
D('padding:10px 12px;border-top:1px solid var(--ui-border);display:flex;gap:8px;flex-shrink:0;' + MU + ';font-size:12px;line-height:18px', Ic('info', 14), o.note || 'Switching opens that version. The interface language stays the same.'))
const MENUS = {
all: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }, { n: 'Arabic', c: 'AR', st: 'draft', fr: 1 }],
two: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }],
many: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'English (United Kingdom)', c: 'EN-GB', st: 'draft' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }, { n: 'Italian', c: 'IT', st: 'draft' }, { n: 'Japanese', c: 'JA', st: 'pub' }, { n: 'Portuguese (Brazil)', c: 'PT-BR', add: 1 }, { n: 'Spanish', c: 'ES', add: 1 }, { n: 'Chinese (Simplified)', c: 'ZH-HANS', add: 1 }, { n: 'Arabic', c: 'AR', add: 1 }, { n: 'Dutch', c: 'NL', add: 1 }, { n: 'Polish', c: 'PL', add: 1 }],
ro: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }],
dis: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'Spanish', c: 'ES', st: 'draft', dis: 1 }],
p4: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }, { n: 'Arabic', c: 'AR', st: 'draft', fr: 1, tag: 'Read only', tagIc: 'lock' }],
p5: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }],
noAdd: [{ n: 'English', c: 'EN', st: 'pub' }, { n: 'French', c: 'FR', st: 'draft', fr: 1, cur: 1 }, { n: 'German', c: 'DE', st: 'pub' }],
@@ -118,7 +119,7 @@ class Component extends DCLogic {
if (cap.d) it.push(cap.part ? { l: 'Delete article', ic: 'trash-2', dis: 1, why: 'You can\'t delete this article.' } : { l: 'Delete article', ic: 'trash-2', danger: 1 })
return it
}
const OvMenu = items => D('position:absolute;z-index:45;' + (m ? 'left:0;right:0;bottom:0;border-top:1px solid var(--ui-border)' : 'right:24px;top:' + (nr ? 150 : 140) + 'px;width:320px;border:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);display:flex;flex-direction:column',
const OvMenu = (items, top0) => D('position:absolute;z-index:45;' + (m ? 'left:0;right:0;bottom:0;border-top:1px solid var(--ui-border)' : 'right:24px;top:' + (top0 ?? (nr ? 150 : 140)) + 'px;width:320px;border:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);display:flex;flex-direction:column',
m && D('display:flex;justify-content:center;padding:8px 0 0', S('width:36px;height:4px;background:var(--ui-border-accented)')),
A('div', { role: 'menu', 'aria-label': 'More actions' }, 'display:flex;flex-direction:column;padding:4px 0', ...items.map(i => A('div', { role: 'menuitem', 'aria-disabled': i.dis || undefined, tabIndex: -1 }, 'flex-shrink:0;display:flex;align-items:center;gap:10px;min-height:' + (m ? 52 : 40) + 'px;padding:8px 12px;' + (i.dis ? 'opacity:.6;' : '') + (i.danger ? 'color:var(--ui-error)' : HL), Ic(i.ic, 16), D('display:flex;flex-direction:column;gap:2px', S('font-weight:500', i.l), i.why && S(MU + ';font-size:12px;font-weight:400;line-height:16px', i.why))))))
const Hdr = (L, o) => {
@@ -158,7 +159,7 @@ class Component extends DCLogic {
const SOC = (site, code) => { const q = (FX.SOCIAL[site] || {})[code] || ['none']; return q[0] === 'derived' ? 'Derived: ' + q[1] : q[0] === 'set' ? 'Set: ' + q[1] : 'Not set' }
const SiteRows = (siteKey, ro) => { const si = FX.SITES[siteKey], all = si.langs.map(k => [k, 1]).concat(si.disabled.map(k => [k, 0])); return all.map(([k, en]) => { const c = FX.count(k, si.stories), def = k === si.def, soc = SOC(siteKey, k)
return D(ROW + ';flex-wrap:wrap;opacity:' + (en ? 1 : .85), D('flex:1 1 160px;min-width:0;display:flex;flex-direction:column', S('font-weight:500;' + HL + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', LNm[k] || k), S(mono + ';' + MU, k)),
def ? Bd('Default', 'info', 'star') : !en ? Bd('Disabled', 'neutral', 'eye-off') : null, !m && S(MU + ';width:170px', FX.pl(c[0], 'version') + ' · ' + c[1] + ' published'),
def ? Bd('Default', 'info', 'star') : !en ? Bd('Disabled', 'neutral', 'eye-off') : null, S((m ? 'flex:1 1 100%;font-size:12px;' : 'width:170px;') + MU, FX.pl(c[0], 'version') + ' · ' + c[1] + ' published'),
D('display:flex;align-items:center;gap:6px;' + (m ? 'flex:1 1 100%' : 'width:270px;white-space:nowrap'), S('font-size:12px;white-space:nowrap;' + MU, 'Social locale'), S(mono + ';' + (soc === 'Not set' ? MU : HL), soc), !ro && Btn('Edit', 'ghost', { sm: true, al: 'Edit social locale for ' + (LNm[k] || k) })),
!ro && !en ? Btn('Re-enable', 'secondary', { sm: true }) : null, !ro && Btn('', 'ghost', { ic: 'ellipsis', sm: true, al: 'Actions for ' + (LNm[k] || k) })) }) }
const slPage = () => {
@@ -168,7 +169,7 @@ class Component extends DCLogic {
const banner = x === 'disableDefault' ? Alert('warning', 'triangle-alert', 'English is the default', 'Choose another default first, then disable English.') : x === 'disableLast' ? Alert('warning', 'triangle-alert', 'At least one language must stay enabled', 'Journal needs a language for new articles.') : x === 'defaultFail' ? Alert('error', 'circle-x', 'Couldn\'t change the default language', 'Nothing changed. English is still the default. Try again.', Btn('Try again', 'secondary', { sm: true })) : x === 'removeFail' ? Alert('error', 'circle-x', 'Couldn\'t remove Italian', 'Nothing changed. Italian is still listed. Try again.', Btn('Try again', 'secondary', { sm: true })) : x === 'stale' ? Alert('error', 'circle-x', 'Languages changed. Reload.', 'Another owner changed the languages for Journal while this page was open. Reload to see the current list. Your change wasn\'t applied.', Btn('Reload', 'secondary', { sm: true })) : null
return Page(T, SUB, !ro && Btn('Add language', 'primary', { ic: 'plus' }),
ro && Alert('neutral', 'lock', 'Only owners can change languages', 'You can see which languages are enabled, their social locale and how many versions each has. Ask an owner to change them.'), banner,
single ? Card(S('font-weight:600;' + HL, 'Archive uses one language'), S(TO, 'New articles are written in Portuguese (Brazil). Add a language to write translations and publish them separately.'), D('display:flex;gap:10px;align-items:center', Bd('Default', 'info', 'star'), S('font-weight:500;' + HL, 'Portuguese (Brazil)'), S(mono + ';' + MU, 'pt-BR'), S(MU + ';font-size:12px', 'Social locale'), S(mono + ';' + HL, SOC('archive', 'pt-BR') === 'Not set' ? 'Derived: pt_BR' : '')))
single ? Card(S('font-weight:600;' + HL, 'Archive uses one language'), S(TO, 'New articles are written in Portuguese (Brazil). Add a language to write translations and publish them separately.'), D('display:flex;gap:10px;align-items:center;flex-wrap:wrap', Bd('Default', 'info', 'star'), S('font-weight:500;' + HL, 'Portuguese (Brazil)'), S(mono + ';' + MU, 'pt-BR'), D('display:flex;gap:6px;align-items:center;' + (m ? 'flex:1 1 100%' : ''), S(MU + ';font-size:12px', 'Social locale'), S(mono + ';' + HL, 'Derived: pt_BR'))))
: D('background:var(--ui-bg);border:1px solid var(--ui-border)', D('padding:8px 16px;background:var(--ui-bg-elevated);' + MU + ';font-weight:500', 'Language'), ...SiteRows(skey, ro)),
single && Alert('info', 'info', 'Regional language kept: pt-BR', 'Setup kept the full site locale, so the default content language is Portuguese (Brazil), not plain Portuguese. The site locale still formats dates and numbers. Social locale: Derived: pt_BR, taken from the language code.'),
!single && !ro && S(MU + ';font-size:12px;text-wrap:pretty', 'Disabling a language keeps its versions readable and editable, and published ones stay online. Drafts can be published again after you re-enable it. Social locale: Derived comes from the language code, Set is chosen by an owner, Not set leaves og:locale out.'))
@@ -199,14 +200,14 @@ class Component extends DCLogic {
}
// ---- public URL pattern
const puPage = () => {
const bad = { unknown: ['https://journal.example.com/{language}/{slug}', 'Unknown placeholder {language}. Use {lang} and {slug}.'], scheme: ['ftp://journal.example.com/{lang}/{slug}', 'Start with https:// or http://.'], encoded: ['https://journal.example.com/my pages/{lang}/{slug}', 'Remove spaces and characters that need encoding. Use letters, digits, - and _.'], noSlug: ['https://journal.example.com/{lang}', 'Add {slug} so each article has its own address.'] }[x]
const bad = { unknown: ['https://journal.example.com/{language}/{slug}', 'Unknown placeholder {language}. Use {lang} and {slug}.'], scheme: ['ftp://journal.example.com/{lang}/{slug}', 'Start with https:// or http://.'], encoded: ['https://journal.example.com/my pages/{lang}/{slug}', 'Remove spaces and characters that need encoding. Use letters, digits, - and _.'], invalidHost: ['https://journal..example.com/{lang}/{slug}', 'Enter a valid host name, such as journal.example.com.'], noSlug: ['https://journal.example.com/{lang}', 'Add {slug} so each article has its own address.'] }[x]
const ro = x === 'viewOnly', unset = x === 'unset', val = bad ? bad[0] : unset ? '' : x === 'editing' ? 'https://journal.example.com/{lang}/articles/{slug}' : 'https://journal.example.com/{lang}/{slug}'
const prev = bad ? null : unset ? null : x === 'editing' ? 'https://journal.example.com/fr/articles/comment-nous-chiffrons-une-renovation' : 'https://journal.example.com/fr/comment-nous-chiffrons-une-renovation'
const prev = bad ? null : unset ? null : x === 'editing' ? 'https://journal.example.com/en/articles/how-we-price-a-bathroom-renovation' : 'https://journal.example.com/en/how-we-price-a-bathroom-renovation'
return Page('Public URL', 'Where Journal articles appear on your website. Used for canonical and alternate links.', null,
ro && Alert('neutral', 'lock', 'Only owners can change this', 'You can read the pattern. Ask an owner to change it.'),
unset && Alert('info', 'info', 'Public URL not configured', 'Page, canonical and alternate addresses stay hidden until you add a pattern. Links by article ID still work.'),
Card(Fd('Public URL pattern', val || 'https://example.com/{lang}/{slug}', { mono: true, val: !!val, ro, focus: !!bad || x === 'editing', err: bad && bad[1], help: bad ? null : 'Placeholders: {lang} is the language code, such as fr or pt-BR. {slug} is the article address in that language.' }),
prev && D('display:flex;flex-direction:column;gap:4px', S('font-size:12px;' + MU, 'Preview with "Comment nous chiffrons une rénovation de salle de bain" in French'), S(mono + ';' + HL + ';overflow-wrap:anywhere', prev)),
prev && D('display:flex;flex-direction:column;gap:4px', S('font-size:12px;' + MU, 'Preview with "How we price a bathroom renovation" in English, the default language'), S(mono + ';' + HL + ';overflow-wrap:anywhere', prev), S('font-size:12px;' + MU, 'The preview uses the most recently updated article that has a version in the default language.')),
!ro && D('display:flex;gap:8px', Btn('Save', 'primary', { dis: !!bad || unset }), Btn('Cancel'))))
}
// ---- setup
@@ -227,29 +228,42 @@ class Component extends DCLogic {
}
// ---- article list
const LN = LNm
const Pager = (V) => D('display:flex;align-items:center;gap:12px;flex-wrap:wrap;' + (m ? 'justify-content:space-between' : ''), S(MU + ';font-size:12px;' + (m ? 'flex:1 1 100%' : 'flex:1'), 'Showing ' + V.from + '–' + V.to + ' of ' + V.unit),
D('display:flex;align-items:center;gap:8px;' + MU + ';font-size:12px', 'Rows', D('', Fd('', String(V.size), { sel: true }))),
D('display:flex;align-items:center;gap:6px', Btn('', 'secondary', { ic: 'chevron-left', al: 'Previous page', dis: V.page < 2, sm: !m }), S(HL + ';font-weight:500;padding:0 6px', 'Page ' + V.page + ' of ' + V.pages), Btn('', 'secondary', { ic: 'chevron-right', al: 'Next page', dis: V.page >= V.pages, sm: !m })))
const Chip = l => D('white-space:nowrap;flex-shrink:0;display:inline-flex;align-items:center;gap:6px;height:' + (m ? 36 : 24) + 'px;padding:0 8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);' + HL + ';font-size:12px', l, A('span', { role: 'button', 'aria-label': 'Remove filter ' + l }, 'display:inline-flex', Ic('x', 14)))
const ChipC = (c, k) => A('span', { 'aria-label': (LN[c.k] || c.k) + ': ' + (c.st === 'pub' ? 'published' : c.st === 'draft' ? 'draft' : 'no version') + (c.nr ? ', needs review' : '') + (c.off ? ', language disabled' : '') }, 'white-space:nowrap;display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 6px;font:500 12px \'IBM Plex Mono\',monospace;' + (c.st ? 'border:1px solid var(--ui-border-accented);' + HL : 'border:1px dashed var(--ui-border-accented);' + MU) + (c.off ? ';opacity:.75' : ''), c.k.toUpperCase(), c.st === 'pub' ? Ic('circle-check', 12) : c.st === 'draft' ? Ic('pencil', 12) : Ic('plus', 12), c.nr && Ic('refresh-cw', 12))
const Cov = (cs) => { const Lc = m || nr ? 4 : 5, vis = cs.slice(0, Lc), more = cs.slice(Lc); return D('display:flex;gap:6px;flex-wrap:' + (m ? 'wrap' : 'nowrap'), ...vis.map(ChipC), more.length ? A('span', { role: 'button', 'aria-label': more.length + ' more languages: ' + more.map(c => LN[c.k] || c.k).join(', ') }, 'display:inline-flex;align-items:center;height:22px;padding:0 6px;font:500 12px \'IBM Plex Mono\',monospace;background:var(--ui-bg-elevated);' + HL, '+' + more.length) : null) }
const Pager = (V) => {
const pb = m ? 44 : 32
const edge = (n, al, dis) => A('span', { role: 'button', 'aria-label': al, 'aria-disabled': dis || undefined }, 'width:' + pb + 'px;height:' + pb + 'px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);opacity:' + (dis ? 0.4 : 1), Ic(n))
const first = V.page === 1, last = V.page === V.pages
const set = new Set([1, V.pages, V.page - 1, V.page, V.page + 1].filter(n => n >= 1 && n <= V.pages))
const arr = V.pages <= 7 ? Array.from({ length: V.pages }, (_, i) => i + 1) : [...set].sort((p, q) => p - q)
const nums = []
arr.forEach((n, i) => { if (i && n - arr[i - 1] > 1) nums.push(S('min-width:' + pb + 'px;height:' + pb + 'px;display:flex;align-items:center;justify-content:center;' + MU, '…')); const cur = n === V.page; nums.push(A('span', { role: 'button', 'aria-label': 'Page ' + n, 'aria-current': cur ? 'page' : undefined }, 'min-width:' + pb + 'px;height:' + pb + 'px;padding:0 6px;display:flex;align-items:center;justify-content:center;border:1px solid ' + (cur ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (cur ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (cur ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';font-weight:' + (cur ? 600 : 400), String(n))) })
const summary = A('span', { 'aria-live': 'polite' }, (m ? 'flex:0 1 auto;' : 'flex:1;') + MU, 'Showing ' + V.from + '–' + V.to + ' of ' + V.total)
const size = A('span', { role: 'combobox', 'aria-label': 'Rows per page', 'aria-expanded': false }, 'height:' + pb + 'px;padding:0 8px 0 12px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);flex-shrink:0;' + HL, V.size + ' per page', Ic('chevron-down', 14))
if (!m) return D('flex-shrink:0;display:flex;align-items:center;gap:16px;padding:12px 0 0', summary, size, V.pages > 1 && A('nav', { 'aria-label': 'Pagination' }, 'display:flex;align-items:center;gap:4px;justify-content:flex-end', edge('chevrons-left', 'First page', first), edge('chevron-left', 'Previous page', first), ...nums, edge('chevron-right', 'Next page', last), edge('chevrons-right', 'Last page', last)))
const mb = (lab, ico, al, dis, right) => A('span', { role: 'button', 'aria-label': al, 'aria-disabled': dis || undefined }, 'flex:1;height:44px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;opacity:' + (dis ? 0.4 : 1), !right && Ic(ico), lab, right && Ic(ico))
return D('flex-shrink:0;display:flex;flex-direction:column;gap:12px;padding:12px 0 16px', D('display:flex;align-items:center;gap:12px;flex-wrap:wrap;justify-content:space-between', summary, size), V.pages > 1 && A('nav', { 'aria-label': 'Pagination' }, 'display:flex;align-items:center;gap:8px', mb('Previous', 'chevron-left', 'Previous page', first), S('flex:0 0 auto;padding:0 4px;white-space:nowrap;font-weight:500;' + HL, 'Page ' + V.page + ' of ' + V.pages), mb('Next', 'chevron-right', 'Next page', last, true)))
}
const Chip = l => D('white-space:nowrap;width:max-content;flex-shrink:0;display:inline-flex;align-items:center;gap:6px;height:' + (m ? 36 : 24) + 'px;padding:0 8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);' + HL + ';font-size:12px', l, A('span', { role: 'button', 'aria-label': 'Remove filter ' + l }, 'display:inline-flex', Ic('x', 14)))
const ChipC = (c, k) => A('span', { 'aria-label': (LN[c.k] || c.k) + ': ' + (c.st === 'pub' ? 'published' : c.st === 'draft' ? 'draft' : 'no version') + (c.nr ? ', needs review' : '') + (c.off ? ', language disabled' : '') }, 'white-space:nowrap;display:inline-flex;align-items:center;gap:4px;height:22px;padding:0 6px;font:500 12px \'IBM Plex Mono\',monospace;' + (c.st ? 'border:1px solid var(--ui-border-accented);' + HL : 'border:1px dashed var(--ui-border-accented);' + MU) + (c.off ? ';opacity:.75' : ''), c.k.toUpperCase(), c.st === 'pub' ? Ic('circle-check', 12) : c.st === 'draft' ? Ic('pencil', 12) : Ic('plus', 12), !!c.nr && Ic('refresh-cw', 12))
const Cov = (cs) => { const Lc = m || nr ? 4 : 5, vis = cs.slice(0, Lc), more = cs.slice(Lc); return D('display:flex;gap:6px;align-items:center;flex-wrap:' + (m ? 'wrap' : 'nowrap'), ...vis.map(ChipC), more.length ? (m ? (x0 => D('display:inline-flex;align-items:center;height:44px;margin:-11px 0', x0)) : (x0 => x0))(A('span', { role: 'button', 'aria-label': more.length + ' more languages: ' + more.map(c => LN[c.k] || c.k).join(', ') }, 'display:inline-flex;align-items:center;height:22px;padding:0 6px;font:500 12px \'IBM Plex Mono\',monospace;background:var(--ui-bg-elevated);' + HL, '+' + more.length)) : null) }
const covW = m ? '' : (nr ? 'width:330px;' : 'width:430px;')
const RowMenuBtn = () => Btn('', 'ghost', { ic: 'ellipsis', sm: true, al: 'Row actions' })
const liPage = () => {
const C = { page2: { page: 2 }, fr: { lang: 'fr' }, frMissing: { lang: 'fr', tab: 'missing' }, emptyMissing: { site: 'press', lang: 'fr', tab: 'missing' }, noResults: { q: 'zinc' }, frLoading: { lang: 'fr', load: 1 }, listFail: { fail: 1 }, limited: { lim: 1 }, filters: { lang: 'fr', status: 'draft' }, tabsScroll: { lang: 'fr', tab: 'missing', shift: 1 }, long: { site: 'atelier', status: 'draft', seo: 1 }, author: { author: 'Marta Alves' }, review: { tab: 'review' }, frReview: { lang: 'fr', tab: 'review' }, langFilter: { menu: 1 }, rowMenu: { row: 1 }, ...(x === 'home' ? {} : {}) }[x] || {}
const site = FX.SITES[C.site || 'journal'], lim = !!C.lim
const C = { page2: { page: 2 }, fr: { lang: 'fr' }, frMissing: { lang: 'fr', tab: 'missing' }, emptyMissing: { site: 'press', lang: 'fr', tab: 'missing' }, noResults: { q: 'zinc' }, frLoading: { lang: 'fr', load: 1 }, listFail: { fail: 1 }, limited: { lim: 1 }, filters: { lang: 'fr', status: 'draft' }, tabsScroll: { lang: 'fr', tab: 'missing', shift: 1 }, long: { site: 'atelier', status: 'draft', seo: 1 }, author: { author: 'Marta Alves' }, review: { tab: 'review' }, frReview: { lang: 'fr', tab: 'review' }, langFilter: { menu: 1 }, rowMenu: { row: 1 }, menuLang: { lang: 'fr' }, apiCopy: { lang: 'fr' }, single: { site: 'archive' }, singleNoResults: { site: 'archive', q: 'zinc' }, singleLoading: { site: 'archive', load: 1 }, singleFail: { site: 'archive', fail: 1 }, singleEmpty: { site: 'gallery' }, singleMenu: { site: 'archive' }, singleDraft: { site: 'archive', status: 'draft' }, ...(x === 'home' ? {} : {}) }[x] || {}
const site = FX.SITES[C.site || 'journal'], lim = !!C.lim, single = site.langs.length <= 1
const V = FX.view({ site, lang: C.lang, tab: C.tab, status: C.status, author: C.author, seo: C.seo, q: C.q, page: C.page, read: lim ? ['en'] : null, create: lim ? [] : null, scope: lim ? (s0 => ['Camille Roche', 'Luis Ferrer'].includes(s0.by)) : null })
const tabs = (C.load || C.fail) ? V.tabs.map(q => [q[0], '–', q[2]]) : V.tabs, tabOn = Math.max(0, V.tabs.findIndex(q => q[2] === V.tab))
const act = [C.status && 'Status: Draft', C.author && 'Author: ' + C.author, C.seo && 'SEO: Needs attention'].filter(Boolean)
const chips = act.length ? ['Site: ' + site.name].concat(C.lang ? ['Language: ' + LN[C.lang]] : [], act) : []
const nF = chips.length
const filt = () => D('display:flex;gap:8px;flex-wrap:wrap;align-items:center', D('flex:1;min-width:' + (m ? '0' : '200px'), Fd('', C.q ? C.q : C.lang ? 'Search ' + LN[C.lang] + ' titles' : 'Search titles in every language', { val: !!C.q })),
m ? Btn('Filters' + (nF ? ' · ' + nF : ''), 'secondary', { ic: 'sliders-horizontal', al: nF ? 'Filters, ' + nF + ' active' : 'Filters' }) : [Fd('', site.name, { sel: true }), Fd('', C.lang ? LN[C.lang] : 'All languages', { sel: true }), Fd('', C.status ? 'Draft' : 'Any status', { sel: true }), Fd('', C.author || 'Any author', { sel: true, focus: x === 'author' }), Fd('', C.seo ? 'Needs attention' : 'Any SEO state', { sel: true })].map(e => D('flex:0 0 auto;width:' + (nr ? 128 : 150) + 'px', e)))
m ? Btn('Filters' + (nF ? ' · ' + nF : ''), 'secondary', { ic: 'sliders-horizontal', al: nF ? 'Filters, ' + nF + ' active' : 'Filters' }) : [Fd('', site.name, { sel: true }), single ? null : Fd('', C.lang ? LN[C.lang] : 'All languages', { sel: true }), Fd('', C.status ? 'Draft' : 'Any status', { sel: true }), Fd('', C.author || 'Any author', { sel: true, focus: x === 'author' }), Fd('', C.seo ? 'Needs attention' : 'Any SEO state', { sel: true })].filter(Boolean).map(e => D('flex:0 0 auto;width:' + (nr ? 128 : 150) + 'px', e)))
const chipRow = chips.length ? D('display:flex;gap:6px;flex-wrap:wrap;align-items:center', ...chips.map(Chip), Btn('Clear all', 'ghost', { sm: !m })) : null
const shift = C.shift ? -96 : 0
const tabRow = D('position:relative;border-bottom:1px solid var(--ui-border);display:flex;align-items:center;gap:4px', m && Btn('', 'ghost', { ic: 'chevron-left', sm: true, al: 'Scroll tabs left', dis: !shift }),
A('div', { role: 'tablist', 'aria-label': 'Article views' }, 'flex:1;min-width:0;overflow:hidden;display:flex;position:relative', D('display:flex;gap:' + (m ? 16 : 24) + 'px;flex-shrink:0;transform:translateX(' + (m ? shift : 0) + 'px)', ...tabs.map((q, i) => A('div', { role: 'tab', 'aria-selected': i === tabOn }, 'padding:' + (m ? '12px 0' : '8px 0') + ';white-space:nowrap;font-weight:' + (i === tabOn ? 600 : 500) + ';color:' + (i === tabOn ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)') + ';border-bottom:2px solid ' + (i === tabOn ? 'var(--ui-primary)' : 'transparent'), q[0] + ' ', S(MU + ';font-weight:400', q[1])))), m && D('position:absolute;right:0;top:0;bottom:0;width:28px;background:linear-gradient(90deg,transparent,var(--ui-bg))'), m && shift && D('position:absolute;left:0;top:0;bottom:0;width:28px;background:linear-gradient(270deg,transparent,var(--ui-bg))')), m && Btn('', 'ghost', { ic: 'chevron-right', sm: true, al: 'Scroll tabs right' }))
A('div', { role: 'tablist', 'aria-label': 'Article views' }, 'flex:1;min-width:0;overflow:hidden;display:flex;position:relative', D('display:flex;gap:' + (m ? 16 : 24) + 'px;flex-shrink:0;transform:translateX(' + (m ? shift : 0) + 'px)', ...tabs.map((q, i) => A('div', { role: 'tab', 'aria-selected': i === tabOn }, 'padding:' + (m ? '12px 0' : '8px 0') + ';white-space:nowrap;font-weight:' + (i === tabOn ? 600 : 500) + ';color:' + (i === tabOn ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)') + ';border-bottom:2px solid ' + (i === tabOn ? 'var(--ui-primary)' : 'transparent'), q[0] + ' ', S(MU + ';font-weight:400', q[1])))), m && D('position:absolute;right:0;top:0;bottom:0;width:28px;background:linear-gradient(90deg,transparent,var(--ui-bg))'), m && !!shift && D('position:absolute;left:0;top:0;bottom:0;width:28px;background:linear-gradient(270deg,transparent,var(--ui-bg))')), m && Btn('', 'ghost', { ic: 'chevron-right', sm: true, al: 'Scroll tabs right' }))
const card = 'background:var(--ui-bg);border:1px solid var(--ui-border)', hd = !m && D('display:flex;gap:12px;padding:8px 16px;background:var(--ui-bg-elevated);' + MU + ';font-weight:500', S('flex:1', 'Article'), S(covW + 'flex-shrink:0', 'Languages'), S('width:110px', 'Author'), S('width:80px', 'Updated'), S('width:28px'))
const hdS = !m && D('display:flex;gap:12px;padding:8px 16px;background:var(--ui-bg-elevated);' + MU + ';font-weight:500', S('flex:1', 'Article'), S('width:110px', 'Status'), S('width:110px', 'Author'), S('width:80px', 'Updated'), S('width:28px'))
const SingleRow = (r) => D(ROW + ';' + (m ? 'flex-wrap:wrap;gap:6px 12px' : ''), D('flex:1 1 ' + (m ? '70%' : '0') + ';min-width:0;display:flex;flex-direction:column;gap:2px', A('span', { lang: r.rep }, HL + ';font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', FX.tt(r.s, r.rep)), m && S(MU + ';font-size:12px', r.s.by + ' · ' + r.s.upd)), D((m ? 'order:3' : 'width:110px'), Pub(r.s.v[r.rep][0])), !m && [S(MU + ';width:110px', r.s.by), S(MU + ';width:80px', r.s.upd)], D(m ? 'order:1' : '', RowMenuBtn()))
const creat = lim ? [] : site.langs
const StoryRow = (r) => { const s0 = r.s, cs = FX.chips(s0, site, creat), t = FX.tt(s0, r.rep)
return D(ROW + ';' + (m ? 'flex-wrap:wrap;gap:6px 12px' : ''), D('flex:1 1 ' + (m ? '70%' : '0') + ';min-width:0;display:flex;flex-direction:column;gap:2px', A('span', { lang: r.rep }, HL + ';font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap', t), r.rep !== site.def && S(MU + ';font-size:12px', 'Shown in ' + (LN[r.rep] || r.rep) + (site.disabled.includes(r.rep) ? ' · language disabled' : ''))), m && D('order:2;flex:1 1 100%', Cov(cs)), !m && D(covW + 'flex-shrink:0', Cov(cs)), m ? S('order:3;' + MU + ';font-size:12px;flex:1', s0.by + ' · ' + s0.upd) : [S(MU + ';width:110px', s0.by), S(MU + ';width:80px', s0.upd)], D(m ? 'order:1' : '', RowMenuBtn())) }
@@ -259,11 +273,12 @@ class Component extends DCLogic {
if (C.fail) body = Alert('error', 'circle-x', 'Couldn\'t load articles', 'Your filters and page are kept. Try again.', Btn('Try again', 'secondary', { sm: true }))
else if (C.load) body = Sk(5)
else if (!V.total && C.lang && V.tab === 'missing') body = D('padding:48px 16px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;' + card, Ic('circle-check', 28), S('font-weight:600;' + HL, 'Every article you can see has a ' + LN[C.lang] + ' version'), S(TO + ';max-width:360px', 'Articles without a ' + LN[C.lang] + ' version will appear here.'))
else if (!V.total && single && !C.q) body = D('padding:48px 16px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;' + card, Ic('file-text', 28), S('font-weight:600;' + HL, 'No articles yet'), S(TO + ';max-width:360px', 'Write your first one. It stays a draft until you publish it.'), Btn('New article', 'primary', { ic: 'plus' }))
else if (!V.total) body = D('padding:48px 16px;display:flex;flex-direction:column;align-items:center;gap:8px;text-align:center;' + card, Ic('search', 28), S('font-weight:600;' + HL, 'No articles match "' + (C.q || '') + '"'), S(TO, 'Search covers titles in every language you can read.'), Btn('Clear search', 'secondary', { sm: true }))
else body = D(card, C.lang && V.tab === 'missing' ? null : (C.lang ? null : hd), ...V.rows.map(C.lang ? (V.tab === 'missing' ? MissRow : VerRow) : StoryRow))
else body = D(card + ';min-height:0;overflow:auto;flex:0 1 auto', C.lang && V.tab === 'missing' ? null : (C.lang ? null : single ? hdS : hd), ...V.rows.map(C.lang ? (V.tab === 'missing' ? MissRow : VerRow) : single ? SingleRow : StoryRow))
const unit = C.fail || C.load ? '' : V.unit
return D('flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column;gap:' + (m ? 12 : 16) + 'px;padding:' + (m ? '16px' : '24px') + ';position:relative', D('display:flex;align-items:baseline;gap:12px', S('font:600 20px/28px \'IBM Plex Sans\',sans-serif;' + HL, 'Articles'), A('span', { 'aria-live': 'polite' }, 'flex:1;' + MU + ';font-weight:500', unit), Btn(m ? '' : 'New article', 'primary', { ic: 'plus', al: 'New article' })),
lim && Alert('neutral', 'lock', 'Showing only what you can read', 'Languages, counts and missing-translation results reflect the versions you have permission to see. You can\'t create translations on this site.'), filt(), chipRow, tabRow, body, (V.total && !C.fail && !C.load) ? Pager(V) : null)
lim && Alert('neutral', 'lock', 'Showing only what you can read', 'Languages, counts and missing-translation results reflect the versions you have permission to see. You can\'t create translations on this site.'), filt(), chipRow, single ? null : tabRow, body, (V.total && !C.fail && !C.load) ? Pager(V) : null)
}
const sheet = (o) => D('position:absolute;inset:0;z-index:50;background:var(--rc-overlay);display:flex;align-items:' + (m ? 'flex-end' : 'center') + ';justify-content:center', A('div', { role: 'dialog', 'aria-modal': 'true', 'aria-label': 'Filters' }, 'width:100%;max-width:' + (m ? '100%' : '420px') + ';max-height:92%;background:var(--ui-bg);display:flex;flex-direction:column', D('display:flex;justify-content:center;padding:8px 0 0', S('width:36px;height:4px;background:var(--ui-border-accented)')),
D('padding:16px 16px 0;display:flex;align-items:center', S('flex:1;font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, 'Filters'), Btn('', 'ghost', { ic: 'x', al: 'Close filters' })),
@@ -276,6 +291,10 @@ class Component extends DCLogic {
if (x === 'author') return [D('position:absolute;z-index:40;left:' + (nr ? 500 : 1018) + 'px;top:170px;width:240px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop)', A('div', { role: 'listbox', 'aria-label': 'Author' }, 'display:flex;flex-direction:column;padding:4px 0', ...[['Any author', J.stories.length]].concat(FX.AUTHORS).map((a, i) => A('div', { role: 'option', 'aria-selected': a[0] === 'Marta Alves' }, 'display:flex;align-items:center;gap:8px;min-height:36px;padding:6px 12px;' + (a[0] === 'Marta Alves' ? 'background:var(--rc-sand);color:var(--rc-on-sand);font-weight:600' : HL), S('width:16px', a[0] === 'Marta Alves' && Ic('check')), S('flex:1', a[0]), S(mono + ';' + (a[0] === 'Marta Alves' ? '' : MU), a[1])))))]
const langRows = (grp) => (grp ? J.disabled : J.langs).map(k => D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + (grp ? MU : HL), S('width:16px'), S('flex:1', LN[k]), S(mono + ';' + MU, FX.count(k, J.stories)[0] + ' versions')))
if (x === 'langFilter') return [D('position:absolute;z-index:40;' + (m ? 'left:0;right:0;bottom:0;border-top:1px solid var(--ui-border)' : 'left:' + (nr ? 330 : 792) + 'px;top:170px;width:280px;border:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);display:flex;flex-direction:column;max-height:70%', A('div', { role: 'listbox', 'aria-label': 'Language' }, 'display:flex;flex-direction:column;overflow-y:auto;padding:4px 0', D('padding:4px 12px;font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;' + MU, 'Enabled'), D('display:flex;align-items:center;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + HL, S('width:16px'), 'All languages'), ...langRows(0), D('padding:8px 12px 4px;border-top:1px solid var(--ui-border);font-size:11px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;' + MU, 'Disabled'), ...langRows(1), D('padding:6px 12px 8px;font-size:12px;' + MU, 'Disabled languages keep their versions. They can\'t get new ones.')))]
if (x === 'singleMenu') return [OvMenu([{ l: 'Open', ic: 'file-pen' }, { l: 'Unpublish', ic: 'eye-off' }, { l: 'Copy API URL', ic: 'copy' }, { l: 'Delete article', ic: 'trash-2', danger: 1 }], 272)]
if (x === 'menuLang') return [OvMenu([{ l: 'Open French', ic: 'file-pen' }, { l: 'Open another language…', ic: 'languages' }, { l: 'Publish French', ic: 'globe' }, { l: 'Copy API URL', ic: 'copy' }, { l: 'Delete French version', ic: 'trash-2', danger: 1 }], 272)]
if (x === 'apiCopy') return [Toast('success', 'circle-check', 'API URL copied', '/v1/article/14?lang=fr')]
if (x === 'deleteStoryDone') return [Toast('success', 'circle-check', 'Article deleted', 'The article and all of its translations were deleted.')]
if (x === 'rowMenu') return [OvMenu([{ l: 'Open English', ic: 'file-pen' }, { l: 'Open another language…', ic: 'languages' }, { l: 'Add translation…', ic: 'plus' }, { l: 'Unpublish English', ic: 'eye-off' }, { l: 'Copy API URL', ic: 'copy' }, { l: 'Delete English version', ic: 'trash-2', danger: 1 }])]
return []
}
@@ -283,9 +302,9 @@ class Component extends DCLogic {
const naOv = () => {
const multi = x !== 'single'
return Dlg({ title: 'New article', desc: x === 'translation' ? 'Create a translation of an article you can read.' : 'Give the article a title. You can write the rest in the editor.' }, [
x === 'translation' ? [Fd('Article', 'Roof inspection checklist', { sel: true, help: 'No French version yet. Copies from English · published' }), Fd('Language', 'French', { sel: true }), Fd('Title', 'Liste de contrôle pour l\'inspection de la toiture', { focus: true, lang: 'fr' })] :
x === 'translation' ? [D('display:flex;flex-direction:column;gap:8px', Radio('New article', 'Starts an article in this language.', false), Radio('Translation of an existing article', 'Uses the same rules as Add translation.', true)), Fd('Article', 'Roof inspection checklist', { sel: true, help: 'No French version yet.' }), Fd('Language', 'French', { sel: true, help: 'Only enabled languages this article doesn\'t have yet.' }), Fd('Source version', 'English · published', { sel: true, help: 'You choose which version to copy from. It is also the source tracked for review.' }), Fd('Title in French', 'Liste de contrôle pour l\'inspection de la toiture', { focus: true, lang: 'fr' }), D('display:flex;flex-direction:column;gap:8px', S('font-weight:500;' + HL, 'Starting content'), Radio('Start empty', 'Only the title. Freshness shows No source.', false), Radio('Copy from English', 'Copies body, excerpt, search and social text, image alternative text and authors. The version starts as Needs review.', true))] :
[Fd('Title', x === 'sitechange' ? 'Spring newsletter' : 'Title', { val: x === 'sitechange', focus: x !== 'sitechange', help: '0 / 200' }), Fd('Site', x === 'sitechange' ? 'Notes' : x === 'single' ? 'Archive' : 'Journal', { sel: true }),
multi && Fd('Language', 'English', { sel: true, help: x === 'sitechange' ? 'Notes only offers English, so the language was reset from French.' : 'Default for Journal.' }),
multi && Fd('Language', 'English', { sel: true, help: x === 'sitechange' ? 'Notes only offers English, so the language was reset from French.' : 'Default language for Journal.' }),
multi && D('display:flex;flex-direction:column;gap:8px', Radio('New article', 'Starts an article in this language.', true), Radio('Translation of an existing article', 'Uses the same rules as Add translation.', false))]
], [Btn('Cancel'), Btn(x === 'translation' ? 'Create French draft' : 'Create article', 'primary')])
}
@@ -295,15 +314,15 @@ class Component extends DCLogic {
const atOv = () => {
const empty = x === 'empty', long = x === 'long', dupCase = x === 'dup' || x === 'dupHidden', st = dupCase ? S1 : S5, ft = dupCase ? FRT : FRT5
const dis = ['gone', 'unreadable', 'langOff', 'dup', 'dupHidden', 'saving'].includes(x)
const f = [Fd('Language', 'French', { sel: true, ro: x === 'langOff', err: x === 'langOff' ? 'French was disabled on this site.' : null }), Fd('Source version', x === 'unreadable' ? 'Choose a version' : 'English · published', { sel: true, val: x !== 'unreadable' && x !== 'gone', help: 'You choose which version to copy from. It is also the source tracked for review. Access is checked again when you create the draft.' }), Fd('Title in French', long ? ft + ' avec les coûts cachés derrière les carreaux, sous la baignoire et autour des canalisations anciennes' : ft, { focus: x === 'form', lang: 'fr', help: 'Copying text is a starting point: it stays in English until you translate it.' }),
const f = [Fd('Language', 'French', { sel: true, ro: x === 'langOff', err: x === 'langOff' ? 'French was disabled on this site.' : null }), Fd('Source version', empty ? 'No source' : (x === 'unreadable' || x === 'gone') ? 'Choose a version' : 'English · published', { sel: true, val: !empty && x !== 'unreadable' && x !== 'gone', err: (x === 'unreadable' || x === 'gone') ? 'Choose a version you can read.' : null, help: (x === 'unreadable' || x === 'gone') ? null : empty ? 'Optional. Freshness shows No source until you pick a tracked source in the editor.' : 'You choose which version to copy from. It is also the source tracked for review. Access is checked again when you create the draft.' }), Fd('Title in French', long ? ft + ' avec les coûts cachés derrière les carreaux, sous la baignoire et autour des canalisations anciennes' : ft, { focus: x === 'form', lang: 'fr', help: 'Copying text is a starting point: it stays in English until you translate it.' }),
D('display:flex;flex-direction:column;gap:8px', S('font-weight:500;' + HL, 'Starting content'), Radio('Start empty', 'Only the title. Freshness shows No source until you pick a tracked source.', empty), Radio('Copy from English', 'Copies body, excerpt, search and social text, image alternative text and authors. Images are reused, files aren\'t duplicated. The version starts as Needs review.', !empty))]
if (long) f.push(Alert('neutral', 'info', 'Authors', 'Marta Alves, Camille Roche, Luis Ferrer, Ana Pereira, Jonas Weber and 4 more will be copied. You can change them in the editor.'), Alert('neutral', 'info', 'Images', 'Cover image and 7 inline images are reused. Alternative text is copied and stays in English until you translate it.'))
const T = 'Add a translation'
if (x === 'dup') return Dlg({ title: T }, [Alert('error', 'circle-x', 'French already exists for this article', 'Open it to continue. Your entries are kept here.', Btn('Open French version', 'secondary', { sm: true })), ...f], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
if (x === 'dupHidden') return Dlg({ title: T }, [Alert('error', 'circle-x', 'French can\'t be created for this article', 'Try a different language, or ask an owner. Your entries are kept.'), ...f], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
if (x === 'fail') return Dlg({ title: T }, [Alert('error', 'circle-x', 'Couldn\'t create the translation', 'Your entries are kept. Try again.'), ...f], [Btn('Cancel'), Btn('Try again', 'primary')])
if (x === 'gone') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'The source was deleted', 'Choose another source you can read, or start empty. Your title is kept.'), ...f.slice(0, 1), f[1], f[2]], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
if (x === 'unreadable') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'The source isn\'t available to you', 'You can no longer read the version you chose. Choose another source you can read, or start empty. Your title is kept.'), ...f.slice(0, 1), f[1], f[2]], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
if (x === 'gone') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'The source isn\'t available to you', 'Choose another source you can read, or start empty. Your title is kept.'), ...f.slice(0, 1), f[1], f[2]], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
if (x === 'unreadable') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'The source isn\'t available to you', 'Choose another source you can read, or start empty. Your title is kept.'), ...f.slice(0, 1), f[1], f[2]], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
if (x === 'langOff') return Dlg({ title: T }, [Alert('warning', 'triangle-alert', 'French was disabled', 'French was disabled on this site while you were filling this in. Ask an owner to re-enable it, or choose another language. Your entries are kept.'), ...f], [Btn('Cancel'), Btn('Create French draft', 'primary', { dis: true })])
if (x === 'saving') return Dlg({ title: T }, f, [Btn('Cancel', 'secondary', { dis: true }), Btn('Creating…', 'primary', { busy: true })])
return Dlg({ title: T, desc: 'Creates a French draft of "' + st.title + '". It isn\'t published.', top: long ? 24 : 60 }, f, [Btn('Cancel'), Btn('Create French draft', 'primary')])
@@ -315,14 +334,16 @@ class Component extends DCLogic {
unpublish: () => Dlg({ title: 'Unpublish French?', desc: 'The French version goes back to draft and stops being public. English stays online.' }, [], [Btn('Cancel'), Btn('Unpublish French', 'primary')]),
unpublishDirty: () => Dlg({ title: 'Unpublish French?', desc: 'The saved French version goes offline and becomes a draft. English stays online.' }, [Alert('neutral', 'info', 'Your edits stay in the editor', 'Your unsaved changes are kept here. Nothing is saved when you unpublish. Save them yourself when you are ready.')], [Btn('Cancel'), Btn('Unpublish French', 'primary')]),
delete: () => Dlg({ title: 'Delete the French version?', desc: '"' + FRT + '" will be deleted. Other versions stay.' }, [], [Btn('Cancel'), Btn('Delete French version', 'danger')]),
deleteSource: () => Dlg({ title: 'Delete the English version?', desc: 'English is the tracked source for versions you can read.' }, [Alert('warning', 'triangle-alert', 'Review tracking will pause', 'Versions that track English stay as they are. Their review history is kept, and they ask you to choose a new source.')], [Btn('Cancel'), Btn('Delete English version', 'danger')]),
deleteStoryOk: () => Dlg({ title: 'Delete English, French, German and Arabic?', desc: 'This deletes the article and every version you can read. It can\'t be undone.' }, [], [Btn('Cancel'), Btn('Delete article', 'danger')]),
deleteSource: () => Dlg({ title: 'Delete the English version?', desc: 'English is the tracked source for versions you can read.' }, [Alert('warning', 'triangle-alert', 'Review tracking will pause', 'French, German and Arabic track English. They stay as they are and keep their review history. Each one asks you to choose a new source.')], [Btn('Cancel'), Btn('Delete English version', 'danger')]),
deleteStoryOk: () => Dlg({ title: 'Delete English, French, German and Arabic?', desc: 'The article and all of its translations will be deleted. This can\'t be undone.' }, [], [Btn('Cancel'), Btn('Delete article', 'danger')]),
deleteLast: () => Dlg({ title: 'Delete the only version?', desc: 'English is the only version of this article. Deleting it deletes the article.' }, [], [Btn('Cancel'), Btn('Delete article', 'danger')]),
saveAndPublish: () => Dlg({ title: 'Save and publish French?', desc: 'Your unsaved changes to the French version are saved first, then only the French version goes online. English and German aren\'t affected.' }, [], [Btn('Cancel'), Btn('Save and publish French', 'primary')]),
saveFailPublish: () => [Toast('error', 'circle-x', 'Couldn\'t save French', 'Nothing was published. Your changes are kept. Try again.')],
deleteStory: () => Dlg({ title: 'You can\'t delete this article', desc: 'Deleting an article needs delete permission on every version. You can still delete the version you are editing.' }, [], [Btn('Close'), Btn('Delete French version', 'danger')]),
fail: () => [Toast('error', 'circle-x', 'Couldn\'t publish French', 'Nothing changed. Try again.')],
done: () => [Toast('success', 'circle-check', 'French published', 'Other versions weren\'t changed.')]
done: () => [Toast('success', 'circle-check', 'French published', 'Other versions weren\'t changed.')],
deleteDone: () => [Toast('success', 'circle-check', 'French version deleted', '"' + FRT + '" was deleted. English and German weren\'t changed.')],
deleteStoryFail: () => Dlg({ title: 'Delete English, French, German and Arabic?', desc: 'The article and all of its translations will be deleted. This can\'t be undone.' }, [Alert('error', 'circle-x', 'Couldn\'t delete the article', 'Nothing was deleted. Every version is still there. Try again in a moment.')], [Btn('Cancel'), Btn('Try again', 'danger')])
}[x]?.())
// ---- freshness and review (release 2)
const frOv = () => ({
@@ -334,8 +355,8 @@ class Component extends DCLogic {
fail: () => [Toast('error', 'circle-x', 'Couldn\'t mark as reviewed', 'Nothing was recorded. Try again.')]
}[x]?.())
// ---- conflicts, moves, permissions
const CfOpt = (t, d, b, k = 'secondary', top) => D((top ? '' : 'border-top:1px solid var(--ui-border);padding-top:14px;') + 'display:flex;' + (m ? 'flex-direction:column;gap:8px' : 'align-items:center;gap:16px'), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S('font-weight:600;' + HL, t), S(MU + ';font-size:13px;line-height:19px;text-wrap:pretty', d)), D((m ? '' : 'width:200px;flex-shrink:0;') + 'display:flex', Btn(b, k, { w: '100%' })))
const Cf = (o) => Dlg({ title: o.title || 'Someone else saved this version', desc: o.desc || 'Someone else saved this version at ' + (o.t || '14:32') + '. Your changes aren\'t saved. They stay here until you choose.', w: 560, top: 60 }, [o.alert, CfOpt('Keep my changes', 'Nothing is sent. Your changes stay in the editor.', 'Keep my changes', 'primary', 1), ...(o.rc ? [CfOpt('Reload theirs', 'This discards your unsaved changes.', 'Reload and discard my changes')] : [CfOpt('Download my version', 'Saves your title and body as a text file. Nothing is sent.', 'Download my version'), CfOpt('Reload theirs', 'Shows their version. You confirm first.', 'Reload theirs'), CfOpt('Save over theirs', 'Replaces the title, body and search fields of this version only. Other languages are untouched.', 'Save over theirs')])], [])
const CfOpt = (t, d, b, k = 'secondary', top, ic) => D((top ? '' : 'border-top:1px solid var(--ui-border);padding-top:14px;') + 'display:flex;' + (m ? 'flex-direction:column;gap:8px' : 'align-items:center;gap:16px'), D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', D('display:flex;align-items:center;gap:6px', ic && S('color:var(--ui-error)', Ic(ic)), S('font-weight:600;' + HL, t)), S(MU + ';font-size:13px;line-height:19px;text-wrap:pretty', d)), D((m ? '' : 'width:200px;flex-shrink:0;') + 'display:flex', Btn(b, k, { w: '100%', ic })))
const Cf = (o) => Dlg({ title: o.title || 'Someone else saved this version', desc: o.desc || 'Someone else saved this version at ' + (o.t || '14:32') + '. Your changes aren\'t saved. They stay here until you choose.', w: 560, top: 60 }, [o.alert, CfOpt('Keep my changes', 'Nothing is sent. Your changes stay in the editor.', 'Keep my changes', 'primary', 1), ...(o.rc ? [CfOpt('Reload theirs', 'This discards your unsaved changes.', 'Reload and discard my changes')] : [CfOpt('Download my version', 'Saves your title and body as a text file. Nothing is sent.', 'Download my version'), CfOpt('Reload theirs', 'Shows their version. You confirm first.', 'Reload theirs'), CfOpt('Save over theirs', 'Replaces the title, body and search fields of this version only. Other languages are untouched.', 'Save over theirs', 'dangerOutline', 0, 'triangle-alert')])], [])
const VList = [['English', 'Published'], ['French', 'Draft'], ['German', 'Published'], ['Arabic', 'Draft · Read only']]
const moveNote = (hidden) => D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;' + HL, 'Moves all language versions of this article.'), ...VList.filter(r => !(hidden && r[0] === 'Arabic')).map(r => S(TO, r[0] + ' · ' + r[1])), hidden && S(MU + ';font-size:12px', 'Versions you can\'t read aren\'t listed or counted.'))
const mvPanel = (hidden) => D('position:absolute;z-index:40;' + (m ? 'left:0;right:0;bottom:0;border-top:1px solid var(--ui-border)' : 'right:24px;top:215px;width:340px;border:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);padding:16px;display:flex;flex-direction:column;gap:12px', S('font-weight:600;' + HL, 'Site'), D('display:flex;flex-direction:column;gap:0;border:1px solid var(--ui-border)', ...['Journal', 'Atelier', 'Archive', 'Notes'].map(k => D('display:flex;align-items:center;gap:8px;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + (k === 'Journal' ? 'background:var(--rc-sand);color:var(--rc-on-sand);font-weight:600' : HL), S('width:16px', k === 'Journal' && Ic('check')), k))), moveNote(hidden), D('display:flex;gap:8px', Btn('Cancel'), D('flex:1;display:flex', Btn('Move article…', 'primary', { w: '100%' }))))
@@ -345,7 +366,8 @@ class Component extends DCLogic {
moveConfirm: () => Dlg({ title: 'Move to Atelier?', ...base }, [moveNote(false), Alert('neutral', 'info', 'Addresses and links', 'Each version keeps its address. Links from the old site to it will stop working. RenewCMS does not create redirects.')], [Btn('Cancel'), Btn('Move article', 'primary')]),
moveBusy: () => Dlg({ title: 'Moving to Atelier…', desc: 'All 4 versions move together. Keep this page open.' }, [Alert('info', 'loader-circle', 'Moving', 'This takes a few seconds.')], [Btn('Cancel', 'secondary', { dis: true }), Btn('Moving…', 'primary', { busy: true })]),
move: () => Dlg({ title: 'Move to Notes?', ...base }, [Alert('warning', 'triangle-alert', 'Notes doesn\'t enable every language in this article', 'Notes enables English only. Enable French, German and Arabic on Notes first. Languages disabled on Journal count too. Nothing was moved.')], [Btn('Cancel'), Btn('Move article', 'primary', { dis: true })]),
moveSlug: () => Dlg({ title: 'Move to Atelier?', ...base }, [Alert('error', 'circle-x', 'An address is already used on Atelier', 'The French address "comment-nous-chiffrons-une-renovation" is already used by another French article on Atelier. Change it before moving. Nothing was moved.', Btn('Edit slug', 'secondary', { sm: true }))], [Btn('Cancel'), Btn('Move article', 'primary', { dis: true })]),
moveSlug: () => Dlg({ title: 'Move to Atelier?', ...base }, [Alert('error', 'circle-x', 'An address is already used on Atelier', 'An address must be unique within its language on Atelier. Change the address below, then move again. Nothing was moved.'), D('display:flex;flex-direction:column;gap:6px', S('font-weight:500;' + HL, 'Conflicting version'), D('display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:10px 12px;border:1px solid var(--ui-border)', S('font-weight:500;' + HL, 'French'), S(mono + ';' + TO + ';flex:1 1 160px;min-width:0;overflow-wrap:anywhere', 'comment-nous-chiffrons-une-renovation'), Btn('Edit slug', 'secondary', { sm: true })), S(MU + ';font-size:12px;line-height:18px', 'Only versions you can read are named.'))], [Btn('Cancel'), Btn('Move article', 'primary', { dis: true })]),
moveFail: () => Dlg({ title: 'Move to Atelier?', ...base }, [Alert('error', 'circle-x', 'Couldn\'t move the article', 'Nothing was moved. All 4 versions are still on Journal. Try again.')], [Btn('Cancel'), Btn('Try again', 'primary')]),
movePerm: () => Dlg({ title: 'You can\'t move this article' }, [Alert('error', 'circle-x', 'Moving needs edit permission on every version', 'Ask an owner to move it. Nothing was changed.')], [Btn('Close')]),
moveDone: () => [Toast('success', 'circle-check', 'Moved to Atelier', 'All 4 versions moved together.')]
}[kind]
@@ -358,7 +380,7 @@ class Component extends DCLogic {
conflict: () => Cf({}),
conflictRestored: () => Cf({ alert: Alert('info', 'info', 'Restored unsaved changes from 14:18', 'Your session ended. These changes were kept in this browser.') }),
reloadConfirm: () => Cf({ title: 'Reload theirs?', desc: 'This discards your unsaved changes.', rc: 1 }),
conflict2: () => Cf({ t: '14:41', alert: Alert('error', 'circle-x', 'Couldn\'t replace theirs', 'Someone else saved this version again at 14:41, before your save went through. Nothing was overwritten. Your changes are still here.') }),
conflict2: () => Cf({ t: '14:41', alert: Alert('error', 'circle-x', 'Their version changed again', 'Someone else saved this version again at 14:41, before your save went through. Nothing was overwritten. Your changes are still here.') }),
download: () => [Cf({}), Toast('success', 'circle-check', 'Download started', 'Your French title and body are saved as a text file. Your changes are still in the editor.')],
restored: () => [] }
return dl[x]?.()
@@ -368,44 +390,56 @@ class Component extends DCLogic {
const mk2 = (t, d, ...c) => Card(S('font-weight:600;' + HL, t), d && S(TO, d), ...c)
const RowS = (a, b, c) => D(ROW + ';padding-left:0;padding-right:0', D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S(HL + ';font-weight:500', a), b && S(MU + ';font-size:12px', b)), c)
if (x === 'home' || x === 'homeFr') { const fr = x === 'homeFr', V = FX.view({ site: J, read: fr ? ['fr'] : null }); return Page('Home', fr ? 'You can read French versions only.' : 'Welcome back, Marta.', null, mk2('Recently updated', null, ...V.rows.slice(0, 5).map(r => RowS(FX.tt(r.s, r.rep), r.s.by + ' · ' + r.s.upd, r.rep !== J.def ? S(mono + ';border:1px solid var(--ui-border-accented);padding:0 6px;' + HL, r.rep.toUpperCase()) : null))), S(MU + ';font-size:12px', 'Each article appears once, with its default-language title. A language code shows only when the title isn\'t in the default language.')) }
if (x === 'activity') return Page('Activity', 'Recent changes in Journal and Archive.', null, mk2('Today and this week', null, ...[['Camille Roche', 'Published French version of "Guide de l\'enduit à la chaux"', '2 h ago'], ['Luis Ferrer', 'Published French version of "Liste d\'hiver pour les fenêtres en bois"', 'Yesterday'], ['Marta Alves', 'Published German version of "So kalkulieren wir eine Badsanierung"', '3 days ago'], ['Luis Ferrer', 'Created German version of "Checkliste für die Dachprüfung"', '14 Oct'], ['Camille Roche', 'Published "Guia de cal e areia"', '14 Oct']].map(r => RowS(r[1], r[0] + ' · ' + r[2]))), S(MU + ';font-size:12px', 'The last event is on Archive, which uses one language, so it names none.'))
if (x === 'media') return Page('Media', 'Image details', null, mk2('bathroom-plan.jpg', null, D('height:160px;border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;' + MU + ';font:12px \'IBM Plex Mono\',monospace', 'image preview'), S('font-weight:500;' + HL, 'Used in 2 articles · 4 versions · 1 file'), S(MU + ';font-size:12px', 'A shared image is one file. It is counted once per article, and the versions that use it are listed.'), RowS('How we price a bathroom renovation', 'English, French, German'), RowS('A guide to lime plaster', 'English')))
if (x === 'menu') return Page('Article menu', 'The same menu names the language on multilingual sites.', null, D('display:flex;gap:16px;flex-wrap:wrap', ...[['Journal (several languages)', ['Publish French', 'Delete French version']], ['Archive (one language)', ['Publish', 'Delete']]].map(g => D('flex:1 1 220px;display:flex;flex-direction:column;gap:6px', S('font-size:12px;' + MU, g[0]), D('border:1px solid var(--ui-border);background:var(--ui-bg);box-shadow:var(--rc-shadow-pop);padding:4px 0', ...g[1].map((l, i) => D('display:flex;align-items:center;gap:10px;min-height:' + (m ? 48 : 36) + 'px;padding:6px 12px;' + (i ? 'color:var(--ui-error)' : HL), Ic(i ? 'trash-2' : 'globe'), S('font-weight:500', l))))))))
if (x === 'apiCopy') return Page('Details', 'French version', null, mk2('Public API URL', 'Each language version has its own address. The URL carries the language.', D('display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--ui-bg-elevated);' + mono, S('flex:1', '/v1/article/14?lang=fr'), Ic('copy', 14))))
if (x === 'seoPreview') return Page('Search preview', 'French version', null, mk2('How it can appear in search', null, D('display:flex;flex-direction:column;gap:2px;padding:12px;background:var(--ui-bg-elevated)', S(mono + ';' + MU, 'journal.example.com › fr › comment-nous-chiffrons-une-renovation'), S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, FRT + ' | Journal'), S(TO, 'La plupart des devis échouent parce qu\'ils cachent les trois mêmes coûts.')), S(MU + ';font-size:12px', 'Shows the localized address and canonical. Description: Fallback: French excerpt.')))
if (x === 'siteFallback') return Page('Search and sharing', 'Site-level text is used as a fallback only in its own language.', null, ...[['French article', 'Description', 'Fallback: French site description', 'Used: "Rénovation et conseils pour la maison" (site text in French)', 1], ['Arabic article', 'Description', 'No fallback', 'Empty. English site default not used.', 0]].map(r => Card(S('font-weight:600;' + HL, r[0]), Fd(r[1], r[4] ? 'Empty' : 'Empty', { val: false, help: r[2] + '. ' + r[3] }), r[4] ? Bd('Uses French site text', 'info', 'languages') : Bd('Tag left out', 'neutral', 'minus'))))
if (x === 'activity') return Page('Activity', 'Recent changes in Journal and Archive.', null, mk2('Today and this week', null, ...[['Marta Alves', 'Added French to Journal', 'Just now'], ['Marta Alves', 'Made French the default language of Journal', '20 min ago'], ['Marta Alves', 'Changed the public URL pattern of Journal', '1 h ago'], ['Camille Roche', 'Published French version of "Guide de l\'enduit à la chaux"', '2 h ago'], ['Luis Ferrer', 'Published French version of "Liste d\'hiver pour les fenêtres en bois"', 'Yesterday'], ['Marta Alves', 'Published German version of "So kalkulieren wir eine Badsanierung"', '3 days ago'], ['Luis Ferrer', 'Created German version of "Checkliste für die Dachprüfung"', '14 Oct'], ['Camille Roche', 'Published "Guia de cal e areia"', '14 Oct']].map(r => RowS(r[1], r[0] + ' · ' + r[2]))), S(MU + ';font-size:12px', 'Site settings events name the site and the language. The last event is on Archive, which uses one language, so it names none.'))
if (x === 'media' || x === 'mediaElsewhere') return Page('Media', 'Images and files for every site.', Btn('Upload', 'primary', { ic: 'upload' }), D('display:grid;grid-template-columns:repeat(auto-fill,minmax(' + (m ? 140 : 180) + 'px,1fr));gap:12px', ...['bathroom-plan.jpg', 'window-frame.jpg', 'lime-plaster.jpg', 'roof-detail.jpg', 'floorboards.jpg', 'tile-samples.jpg'].map((n, i) => D('display:flex;flex-direction:column;gap:6px;padding:8px;background:var(--ui-bg);border:1px solid ' + (i === (x === 'mediaElsewhere' ? 1 : 0) ? 'var(--ui-primary)' : 'var(--ui-border)') + ';' + (i === (x === 'mediaElsewhere' ? 1 : 0) ? 'background:var(--rc-sand);color:var(--rc-on-sand)' : ''), D('height:' + (m ? 80 : 110) + 'px;border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;font:12px \'IBM Plex Mono\',monospace;' + (i === (x === 'mediaElsewhere' ? 1 : 0) ? '' : MU), 'image'), S('font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + (i === (x === 'mediaElsewhere' ? 1 : 0) ? '' : HL), n)))))
return null
}
const PanelShell = (title, sub, body, o = {}) => D('position:absolute;inset:0;z-index:50;background:var(--rc-overlay);display:flex;justify-content:' + (m ? 'center' : 'flex-end') + ';align-items:' + (m ? 'flex-end' : 'stretch'),
A('div', { role: 'dialog', 'aria-modal': 'true', 'aria-label': title }, 'width:' + (m ? '100%' : 'min(' + (o.w || 960) + 'px,100%)') + ';' + (m ? 'height:92%;border-top:1px solid var(--ui-border)' : 'height:100%;border-left:1px solid var(--ui-border)') + ';background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column',
m && D('display:flex;justify-content:center;padding:8px 0 0;flex-shrink:0', S('width:36px;height:4px;background:var(--ui-border-accented)')),
D('padding:' + (m ? '12px 16px' : '16px 24px') + ';border-bottom:1px solid var(--ui-border);display:flex;gap:12px;align-items:flex-start;flex-shrink:0', D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, title), sub && S(TO + ';text-wrap:pretty', sub)), Btn('', 'ghost', { ic: 'x', sm: true, al: 'Close' })),
D('flex:1;min-height:0;overflow-y:auto;padding:' + (m ? '16px' : '16px 24px') + ';display:flex;flex-direction:column;gap:16px', ...body)))
const seoPanel = () => {
const unset = x === 'unset', slugD = x === 'editSlug'
const rows = [['Page title', FRT + ' | Journal', 'Fallback: French title', 1], ['Description', 'Empty', 'No fallback. Empty. English site default not used. The tag is left out.', 0], ['Image alternative text', 'Un plan de salle de bain avec les mesures', 'Written per language.', 1]]
let body
if (x === 'preview') body = [Card(S('font-weight:600;' + HL, 'How it can appear in search'), D('display:flex;flex-direction:column;gap:2px;padding:12px;background:var(--ui-bg-elevated)', S(mono + ';' + MU + ';overflow-wrap:anywhere', 'journal.example.com › fr › comment-nous-chiffrons-une-renovation'), S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + HL, FRT + ' | Journal'), S(TO, 'La plupart des devis échouent parce qu\'ils cachent les trois mêmes coûts.')), S(MU + ';font-size:12px', 'Shows the localized address and canonical. Description: Fallback: French excerpt.'))]
else if (x === 'siteFallback') body = [S(TO, 'Site-level text is used as a fallback only in its own language.'), ...[['French version', 'Description', 'Fallback: French site description', 'Used: "Rénovation et conseils pour la maison" (site text in French)', 1], ['Arabic version', 'Description', 'No fallback', 'Empty. English site default not used.', 0]].map(r => Card(S('font-weight:600;' + HL, r[0]), Fd(r[1], 'Empty', { val: false, help: r[2] + '. ' + r[3] }), r[4] ? Bd('Uses French site text', 'info', 'languages') : Bd('Tag left out', 'neutral', 'minus')))]
else body = [unset ? Alert('info', 'info', 'Public URL not configured', 'Add a public URL pattern for Journal to show page and canonical addresses. API links by article ID still work.', Btn('Open Public URL settings', 'secondary', { sm: true })) : Card(D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('Canonical', 'neutral', 'link'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'https://journal.example.com/fr/comment-nous-chiffrons-une-renovation')), S(MU + ';font-size:12px', 'Built from the Public URL pattern https://journal.example.com/{lang}/{slug}. Each language points to itself unless you set a canonical override.')),
!unset && Card(S('font-weight:600;' + HL, 'Alternate languages'), S(TO, 'Published versions in enabled languages only.'), D('display:flex;gap:10px;align-items:center', S(mono, 'EN'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + TO, 'https://journal.example.com/en/how-we-price-a-bathroom-renovation'), Bd('Published', 'success', 'circle-check')), D('display:flex;gap:10px;align-items:center', S(mono, 'DE'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + TO, 'https://journal.example.com/de/so-kalkulieren-wir-eine-badsanierung'), Bd('Published', 'success', 'circle-check')), S(MU + ';font-size:12px', 'Drafts and the disabled Spanish language are left out.')),
Card(S('font-weight:600;' + HL, 'Social locale'), D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('og:locale', 'neutral', 'share-2'), x === 'noSocial' ? S(MU, 'Social locale not set') : S(mono + ';' + HL, 'Set: fr_FR')), S(MU + ';font-size:12px;line-height:18px;text-wrap:pretty', x === 'noSocial' ? 'French has no region in its code and no social locale is set for it, so og:locale and og:locale:alternate are left out. An owner can set one in Journal languages.' : 'Set by an owner for French in Journal languages. A region is never guessed from the language.')),
Card(Fd('Address (slug)', 'comment-nous-chiffrons-une-renovation', { mono: true, ro: true, lock: true, help: 'Unique within Journal and French. Editing the title never changes the address.' }), D('display:flex;flex-direction:column;gap:6px;align-items:flex-start', Btn('Edit slug', 'secondary', { ic: 'pencil' }), S(MU + ';font-size:12px;line-height:18px', 'Editing the title never changes the slug. Only Edit slug does.'), S(HL + ';font-size:12px;line-height:18px', 'Links to the old address will stop working. RenewCMS does not create redirects.'))),
...rows.map(r => Fd(r[0], r[1], { help: r[2], val: r[3] === 1 }))].filter(Boolean)
const p = PanelShell('Search and sharing · French', 'Applies only to the French version. Page language: fr.', body)
return slugD ? [p, Dlg({ z: 60, title: 'Edit slug for the French version', desc: 'The address changes for French only.' }, [Fd('Address (slug)', 'comment-nous-calculons-une-renovation', { mono: true, focus: true }), Alert('warning', 'triangle-alert', 'This version is published', 'Links to the old address will stop working. RenewCMS does not create redirects.'), S(MU + ';font-size:12px', 'Editing the title never changes the slug.')], [Btn('Cancel'), Btn('Change address', 'primary')])] : [p]
}
const mediaPanel = () => {
const el = x === 'mediaElsewhere'
const UR = (t, b2, link) => D('border-top:1px solid var(--ui-border);padding:10px 0;display:flex;align-items:center;gap:8px;min-height:' + (m ? 48 : 40) + 'px', D('flex:1;min-width:0;display:flex;flex-direction:column;gap:2px', S('font-weight:500;text-decoration:underline;text-underline-offset:3px;' + HL, t), b2 && S(MU + ';font-size:12px', b2)), link && S(mono + ';' + MU, link), Ic('arrow-up-right', 14))
const SH = t => S('font:600 14px/22px \'IBM Plex Sans\',sans-serif;' + HL, t)
const body = [D('height:160px;flex-shrink:0;border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;' + MU + ';font:12px \'IBM Plex Mono\',monospace', 'image preview'), D('display:flex', Bd(el ? 'Used in 1' : 'Used in 2', 'neutral'))]
if (el) body.push(D('display:flex;flex-direction:column;gap:6px', SH('Articles'), S(MU, 'No article shows it.')), D('display:flex;flex-direction:column;gap:6px', SH('Used elsewhere'), D('border-bottom:1px solid var(--ui-border)', UR('Author avatar', null, '/authors'))))
else body.push(S('font-weight:500;' + HL, 'Used in 2 articles · 4 versions · 1 file'), S(MU + ';font-size:12px', 'A shared image is one file. It is counted once per article, and the versions that use it are listed.'), D('display:flex;flex-direction:column', UR('How we price a bathroom renovation', 'English, French, German'), UR('A guide to lime plaster', 'English')))
return PanelShell(el ? 'window-frame.jpg' : 'bathroom-plan.jpg', 'Image details', body, { w: 420 })
}
const apiPage = () => Page('Integration · Languages', 'Proposed. Not available in the current API.', null, Alert('warning', 'triangle-alert', 'Proposed capability', 'These requests don\'t exist yet. Existing article-ID requests are unchanged.'),
Card(S('font-weight:600;' + HL, '1. Exact lookup: site + language + slug'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'GET /v1/public/site/{key}/lang/{lang}/article/{slug}'), S(TO, 'Matches the published version with that slug in that language, or returns article_not_found. It never falls back, because a slug belongs to one language.'), S(mono + ';' + MU + ';overflow-wrap:anywhere', '{ "story_id": "st_01", "lang": "fr", "slug": "…", "alternates": [{ "lang": "en", "slug": "…" }] }')),
Card(S('font-weight:600;' + HL, '2. Resolve by article group, with fallback'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'GET /v1/public/site/{key}/story/{story_id}?lang=fr&fallback=default'), S(TO, 'Use story_id from any response or from alternates[]. French published: lang "fr". French missing or unpublished with fallback=default: the published default, with "lang": "en", "requested_lang": "fr", "fallback": true. Without fallback=default, a missing French is language_not_available. No other language is tried, and drafts are never returned.')),
Card(S('font-weight:600;' + HL, '3. Errors'), S(mono + ';' + HL, 'article_not_found · language_not_available · language_disabled'), S(TO, 'article_not_found: no published version has that slug in that language. language_not_available: the requested language and the allowed fallback are both unavailable. language_disabled: publishing in a disabled language.')),
Card(S('font-weight:600;' + HL, 'Existing, unchanged'), S(mono + ';' + HL, 'GET /v1/article/{id}'), S(mono + ';' + HL, 'PATCH /v1/article/{id}/publish'), S(mono + ';' + HL, 'Editor: /article/{id}/edit'), S(TO, 'Each article ID is one language version. Article-ID links keep working.')))
// ---- compose
const apiToast = x === 'apiCopy' ? Toast('success', 'circle-check', 'API URL copied', '/v1/article/14?lang=fr') : null
if (v === 'sl') { Object.assign(shell, setS, x === 'single' ? { crumbs: ['Settings', 'Sites', 'Archive', 'Languages'], mTitle: 'Archive · Languages' } : x === 'social' || x === 'socialEdit' || x === 'socialInvalid' ? { crumbs: ['Settings', 'Sites', 'Atelier', 'Languages'], mTitle: 'Atelier · Languages' } : {}); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', slPage()); ov = [].concat(slOv() || []) }
if (v === 'sl') { Object.assign(shell, setS, x === 'single' ? { crumbs: ['Settings', 'Sites', 'Archive', 'Languages'], mTitle: 'Archive · Languages' } : x === 'social' || x === 'socialEdit' || x === 'socialInvalid' ? { crumbs: ['Settings', 'Sites', 'Atelier', 'Languages'], mTitle: 'Atelier · Languages' } : {}); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', slPage()); ov = [].concat(slOv() || []) }
else if (v === 'pu') { Object.assign(shell, setS, { crumbs: ['Settings', 'Sites', 'Journal', 'Public URL'], mTitle: 'Journal · Public URL' }); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', puPage()); if (x === 'saved') ov = [Toast('success', 'circle-check', 'Public URL saved', 'Journal pages now use this pattern.')] }
else if (v === 'su') { Object.assign(shell, setS, { crumbs: ['Settings', 'Sites', 'Journal', 'Languages', 'Setup'], mTitle: 'Set article languages' }); main = D('flex:1;min-height:0;display:flex;flex-direction:column', suBody()) }
else if (v === 'li') { Object.assign(shell, liS); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', liPage()); ov = listOv() }
else if (v === 'na') { Object.assign(shell, liS); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', liPage()); ov = [naOv()] }
else if (v === 'sf') { Object.assign(shell, { nav: x === 'home' || x === 'homeFr' ? 'home' : x === 'activity' ? 'activity' : x === 'media' ? 'media' : 'articles', crumbs: [x === 'home' || x === 'homeFr' ? 'Home' : x === 'activity' ? 'Activity' : x === 'media' ? 'Media' : 'Articles'], mTitle: 'Journal' }); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', sfPage()); if (apiToast) ov = [apiToast] }
else if (v === 'seo' || v === 'api') {
Object.assign(shell, edS)
let mainEl
if (v === 'seo') {
const unset = x === 'unset', slugD = x === 'editSlug'
const rows = [['Page title', FRT + ' | Journal', 'Fallback: French title', 1], ['Description', 'Empty', 'No fallback. Empty. English site default not used. The tag is left out.', 0], ['Image alternative text', 'Un plan de salle de bain avec les mesures', 'Written per language.', 1]]
mainEl = Page('Search and sharing · French', 'Applies only to the French version. Page language: fr.', null,
unset ? Alert('info', 'info', 'Public URL not configured', 'Add a public URL pattern for Journal to show page and canonical addresses. API links by article ID still work.', Btn('Open Public URL settings', 'secondary', { sm: true })) : Card(D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('Canonical', 'neutral', 'link'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'https://journal.example.com/fr/comment-nous-chiffrons-une-renovation')), S(MU + ';font-size:12px', 'Built from the Public URL pattern https://journal.example.com/{lang}/{slug}. Each language points to itself unless you set a canonical override.')),
!unset && Card(S('font-weight:600;' + HL, 'Alternate languages'), S(TO, 'Published versions in enabled languages only.'), D('display:flex;gap:10px;align-items:center', S(mono, 'EN'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + TO, 'https://journal.example.com/en/how-we-price-a-bathroom-renovation'), Bd('Published', 'success', 'circle-check')), D('display:flex;gap:10px;align-items:center', S(mono, 'DE'), S('flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;' + TO, 'https://journal.example.com/de/so-kalkulieren-wir-eine-badsanierung'), Bd('Published', 'success', 'circle-check')), S(MU + ';font-size:12px', 'Drafts and the disabled Spanish language are left out.')),
Card(S('font-weight:600;' + HL, 'Social locale'), D('display:flex;gap:8px;align-items:center;flex-wrap:wrap', Bd('og:locale', 'neutral', 'share-2'), x === 'noSocial' ? S(MU, 'Social locale not set') : S(mono + ';' + HL, 'Set: fr_FR')), S(MU + ';font-size:12px;line-height:18px;text-wrap:pretty', x === 'noSocial' ? 'French has no region in its code and no social locale is set for it, so og:locale and og:locale:alternate are left out. An owner can set one in Journal languages.' : 'Set by an owner for French in Journal languages. A region is never guessed from the language.')),
Card(Fd('Address (slug)', 'comment-nous-chiffrons-une-renovation', { mono: true, ro: true, lock: true, help: 'Unique within Journal and French. Editing the title never changes the address.' }), D('display:flex;flex-direction:column;gap:6px;align-items:flex-start', Btn('Edit slug', 'secondary', { ic: 'pencil' }), S(MU + ';font-size:12px;line-height:18px', 'Editing the title never changes the slug. Only Edit slug does.'), S(HL + ';font-size:12px;line-height:18px', 'Links to the old address will stop working. RenewCMS does not create redirects.'))),
...rows.map(r => Fd(r[0], r[1], { help: r[2], val: r[3] === 1 })))
if (slugD) ov = [Dlg({ title: 'Edit slug for the French version', desc: 'The address changes for French only.' }, [Fd('Address (slug)', 'comment-nous-calculons-une-renovation', { mono: true, focus: true }), Alert('warning', 'triangle-alert', 'This version is published', 'Links to the old address will stop working. RenewCMS does not create redirects.'), S(MU + ';font-size:12px', 'Editing the title never changes the slug.')], [Btn('Cancel'), Btn('Change address', 'primary')])]
} else mainEl = Page('Integration · Languages', 'Proposed. Not available in the current API.', null, Alert('warning', 'triangle-alert', 'Proposed capability', 'These requests don\'t exist yet. Existing article-ID requests are unchanged.'),
Card(S('font-weight:600;' + HL, '1. Exact lookup: site + language + slug'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'GET /v1/public/site/{key}/lang/{lang}/article/{slug}'), S(TO, 'Matches the published version with that slug in that language, or returns article_not_found. It never falls back, because a slug belongs to one language.'), S(mono + ';' + MU + ';overflow-wrap:anywhere', '{ "story_id": "st_01", "lang": "fr", "slug": "…", "alternates": [{ "lang": "en", "slug": "…" }] }')),
Card(S('font-weight:600;' + HL, '2. Resolve by article group, with fallback'), S(mono + ';' + HL + ';overflow-wrap:anywhere', 'GET /v1/public/site/{key}/story/{story_id}?lang=fr&fallback=default'), S(TO, 'Use story_id from any response or from alternates[]. French published: lang "fr". French missing or unpublished with fallback=default: the published default, with "lang": "en", "requested_lang": "fr", "fallback": true. Without fallback=default, a missing French is language_not_available. No other language is tried, and drafts are never returned.')),
Card(S('font-weight:600;' + HL, '3. Errors'), S(mono + ';' + HL, 'article_not_found · language_not_available · language_disabled'), S(TO, 'article_not_found: no published version has that slug in that language. language_not_available: the requested language and the allowed fallback are both unavailable. language_disabled: publishing in a disabled language.')),
Card(S('font-weight:600;' + HL, 'Existing, unchanged'), S(mono + ';' + HL, 'GET /v1/article/{id}'), S(mono + ';' + HL, 'PATCH /v1/article/{id}/publish'), S(mono + ';' + HL, 'Editor: /article/{id}/edit'), S(TO, 'Each article ID is one language version. Article-ID links keep working.')))
main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', mainEl)
else if (v === 'sf') { Object.assign(shell, { nav: x === 'home' || x === 'homeFr' ? 'home' : x === 'activity' ? 'activity' : x === 'media' || x === 'mediaElsewhere' ? 'media' : 'articles', crumbs: [x === 'home' || x === 'homeFr' ? 'Home' : x === 'activity' ? 'Activity' : x === 'media' || x === 'mediaElsewhere' ? 'Media' : 'Articles'], mTitle: 'Journal' }); main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', sfPage()); if (x === 'media' || x === 'mediaElsewhere') ov = [mediaPanel()] }
else if (v === 'api') {
Object.assign(shell, { nav: 'integration', crumbs: ['Integration'], mTitle: 'Integration', mLead: 'menu' })
main = D('flex:1;min-height:0;display:flex;flex-direction:column;position:relative', apiPage())
} else {
Object.assign(shell, edS)
const rtl = x === 'rtl', en = x === 'navFail' || (v === 'ed' && ['cleanOnline', 'onlineDirty', 'unpublishDirty'].includes(x))
const rtl = x === 'rtl', en = x === 'navFail' || (v === 'pd' && x === 'deleteDone') || (v === 'ed' && ['cleanOnline', 'onlineDirty', 'unpublishDirty'].includes(x))
let L = rtl ? AR : FR
const o = { cap: CAPS.full, pub: 'draft' }
const bn = []
@@ -413,7 +447,7 @@ class Component extends DCLogic {
const pop = (rows, q = {}) => ovl.push(LangPop(rows, { left: 248, top: 176, ...q }))
const lifeOnline = 'This version is online. Saving updates the live page right away.'
if (v === 'ed') {
if (x === 'menu') { o.open = 1; pop(MENUS.all) } else if (x === 'two') { o.open = 1; pop(MENUS.two) } else if (x === 'many') { o.open = 1; pop(MENUS.many, { find: 1, max: 520 }) }
if (x === 'menu') { o.open = 1; pop(MENUS.all) } else if (x === 'two') { o.open = 1; pop(MENUS.two) } else if (x === 'many') { o.open = 1; pop(MENUS.many, { find: 1, max: 520 }) } else if (x === 'menuDisabled') { o.open = 1; pop(MENUS.dis, { note: 'Spanish is disabled on this site. Its version stays readable and editable. It can\'t be published until an owner re-enables Spanish.' }) }
else if (x === 'dirty') { o.dirty = 1; o.cap = CAPS.p2 }
else if (x === 'dirtyPublish') o.dirty = 1
else if (x === 'cleanOnline') { o.pub = 'pub'; bn.push(Alert('neutral', 'info', lifeOnline)) }
@@ -431,7 +465,7 @@ class Component extends DCLogic {
}
else if (v === 'pm') {
if (x === 'restored') { o.dirty = 1; bn.push(Alert('info', 'info', 'Restored unsaved changes from 14:18', 'You logged in again. Your French title and body were kept in this browser.')) }
else if (x === 'conflict' || x === 'conflict2' || x === 'download' || x === 'conflictRestored' || x === 'reloadConfirm') o.dirty = 1
else if (x === 'session' || x === 'conflict' || x === 'conflict2' || x === 'download' || x === 'conflictRestored' || x === 'reloadConfirm') o.dirty = 1
else if (x.startsWith('move') && x !== 'moveSelect' && x !== 'moveSelectHidden') { o.cap = { ...CAPS.full } }
}
else if (v === 'src') {
@@ -447,8 +481,9 @@ class Component extends DCLogic {
else if (x === 'inaccessible') { o.fresh = 'Source unavailable'; bn.push(Alert('warning', 'lock', 'The source isn\'t available to you right now', 'It may be restricted. Review history is kept. Mark as reviewed is off until you can read it again.', Btn('Choose a new source', 'ghost', { sm: true }))) }
else { if (x === 'pub') o.pub = 'pub'; bn.push(D('display:flex;flex-direction:column;gap:8px', D('display:flex;gap:8px;align-items:center', Tracked('English')), Alert('warning', 'triangle-alert', 'English changed after French was reviewed', 'Changed fields: Title, Body. English was saved 14 Oct 2026, 09:12. French was last reviewed 12 Oct 2026.' + (x === 'pub' ? ' French is still published. Reviewing doesn\'t change publication.' : ''), buttons))) }
ovl = ovl.concat(frOv() || [])
} else if (v === 'pd') { o.pub = ['unpublish', 'unpublishDirty', 'done', 'deleteSource', 'deleteLast', 'deleteStoryOk', 'deleteStory'].includes(x) ? 'pub' : 'draft'; if (['saveAndPublish', 'saveFailPublish', 'unpublishDirty'].includes(x)) o.dirty = 1; if (x === 'saveFailPublish') o.saveFail = 1; if (x === 'publishDisabled') { o.pubDis = 1; bn.push(Alert('warning', 'triangle-alert', 'French is disabled on this site. Re-enable it to publish.', 'You can still edit and save this version. Ask an owner to re-enable French.')) } ovl = ovl.concat(pdOv() || []) }
} else if (v === 'pd') { o.pub = ['unpublish', 'unpublishDirty', 'done', 'deleteSource', 'deleteLast', 'deleteStoryOk', 'deleteStoryFail', 'deleteDone', 'deleteStory'].includes(x) ? 'pub' : 'draft'; if (['saveAndPublish', 'saveFailPublish', 'unpublishDirty'].includes(x)) o.dirty = 1; if (x === 'saveFailPublish') { o.saveFail = 1; bn.push(Alert('error', 'circle-x', 'Couldn\'t save French', 'Your changes are kept here. Nothing was published. Try saving again.')) } if (x === 'done') { o.saved = 1; bn.push(Alert('neutral', 'info', lifeOnline)) } if (x === 'publishDisabled') { o.pubDis = 1; bn.push(Alert('warning', 'triangle-alert', 'French is disabled on this site. Re-enable it to publish.', 'You can still edit and save this version. Ask an owner to re-enable French.')) } ovl = ovl.concat(pdOv() || []) }
else if (v === 'at') { o.pub = 'pub'; L = LS(x === 'dup' || x === 'dupHidden' ? S1 : S5); ovl = ovl.concat(atOv()) }
else if (v === 'seo') ovl = ovl.concat(seoPanel())
if (v === 'pm') ovl = ovl.concat(pmOv() || [])
if (v === 'perm') {
const cp = { ro: CAPS.ro, roMenu: CAPS.ro, p2: CAPS.p2, p3: CAPS.p3, p4: CAPS.full, p5: CAPS.full, p6: CAPS.p6, noAdd: CAPS.full, withAdd: CAPS.full }[x] || CAPS.full
@@ -1,600 +0,0 @@
<!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>
<div style="width:{{ fw }};height:{{ fh }};position:relative;display:flex;flex-direction:{{ dir }};background:var(--ui-bg-muted);color:var(--ui-text);font-family:'IBM Plex Sans',sans-serif;font-size:13px;line-height:20px;overflow:hidden">
<sc-if value="{{ desk }}" hint-placeholder-val="{{ true }}">
<div style="width:224px;flex-shrink:0;background:var(--ui-bg);border-right:1px solid var(--ui-border);display:flex;flex-direction:column;padding:12px;gap:2px">
<div style="display:flex;align-items:center;gap:10px;height:40px;padding:0 6px;margin-bottom:10px"><svg width="26" height="26" viewBox="0 0 16 16" shape-rendering="crispEdges"><rect width="16" height="16" fill="#111"></rect><path fill="#fff" fill-rule="evenodd" d="M4 3h6v1h1v1h1v3h-1v1h-1v1h1v1h1v2h-2v-2h-1v-1H6v3H4zm2 2v3h4V5z"></path></svg><span style="font-weight:600;font-size:14px;color:var(--ui-text-highlighted)">RenewCMS</span></div>
<sc-for list="{{ nav }}" as="n" hint-placeholder-count="3">
<div data-act="nav-{{ n.key }}" style="display:flex;align-items:center;gap:10px;height:32px;padding:0 10px;background:{{ n.bg }};color:{{ n.fg }};font-weight:{{ n.fw }}"><span aria-hidden="true" style="display:inline-block;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ n.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ n.icon }}.svg') center / contain no-repeat"></span>{{ n.t }}<sc-if value="{{ n.badge }}"><span style="margin-left:auto;height:18px;padding:0 6px;display:inline-flex;align-items:center;background:var(--ui-info);color:var(--ui-bg);font:500 11px 'IBM Plex Sans',sans-serif">Update</span></sc-if><sc-if value="{{ n.chev }}"><span aria-hidden="true" style="margin-left:{{ n.chevML }};display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ n.chev }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ n.chev }}.svg') center / contain no-repeat"></span></sc-if></div><sc-if value="{{ n.sub }}"><div style="display:flex;flex-direction:column;gap:2px;margin-left:19px;border-left:1px solid var(--ui-border)"><span style="padding:6px 10px 2px 12px;font-size:11px;line-height:16px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Media</span><sc-for list="{{ n.subs }}" as="x" hint-placeholder-count="2"><div data-act="nav-{{ x.t }}" style="display:flex;align-items:center;gap:10px;height:32px;padding:0 10px 0 12px;background:{{ x.bg }};color:{{ x.fg }};font-weight:{{ x.fw }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ x.ic }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ x.ic }}.svg') center / contain no-repeat"></span>{{ x.t }}</div></sc-for><sc-if value="{{ n.hasSites }}"><span style="padding:6px 10px 2px 12px;font-size:11px;line-height:16px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Publishing</span></sc-if><sc-for list="{{ n.subs2 }}" as="x2" hint-placeholder-count="1"><div data-act="nav-{{ x2.t }}" style="display:flex;align-items:center;gap:10px;height:32px;padding:0 10px 0 12px;background:{{ x2.bg }};color:{{ x2.fg }};font-weight:{{ x2.fw }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ x2.ic }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ x2.ic }}.svg') center / contain no-repeat"></span>{{ x2.t }}</div></sc-for><sc-if value="{{ n.hasAccess }}"><span style="padding:6px 10px 2px 12px;font-size:11px;line-height:16px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Access</span></sc-if><sc-for list="{{ n.subs3 }}" as="x3" hint-placeholder-count="1"><div data-act="nav-{{ x3.t }}" style="display:flex;align-items:center;gap:10px;height:32px;padding:0 10px 0 12px;background:{{ x3.bg }};color:{{ x3.fg }};font-weight:{{ x3.fw }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ x3.ic }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ x3.ic }}.svg') center / contain no-repeat"></span>{{ x3.t }}</div></sc-for></div></sc-if>
</sc-for>
<sc-if value="{{ contentShow }}"><div style="display:flex;flex-direction:column;gap:2px;margin-top:8px"><span style="padding:6px 10px 2px;font-size:11px;line-height:16px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Content</span><sc-if value="{{ contentColl }}"><div style="display:flex;align-items:center;gap:10px;height:32px;padding:0 10px;color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/layers.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/layers.svg') center / contain no-repeat"></span><span style="flex:1">All content types</span><span style="font-size:12px;color:var(--ui-text-muted)">{{ contentCount }}</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat"></span></div></sc-if><sc-for list="{{ contentRows }}" as="c" hint-placeholder-count="3"><div data-act="nav-content-{{ c.key }}" title="{{ c.name }}" style="display:flex;align-items:center;gap:10px;height:32px;padding:0 10px;background:{{ c.bg }};color:{{ c.fg }};font-weight:{{ c.fw }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ c.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ c.icon }}.svg') center / contain no-repeat"></span><span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ c.name }}</span></div></sc-for><sc-if value="{{ contentMore }}"><div style="display:flex;align-items:center;gap:10px;height:32px;padding:0 10px;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat"></span><span>{{ contentMore }}</span></div></sc-if></div></sc-if>
<div style="flex:1"></div>
<sc-if value="{{ s.boot }}"><div style="display:flex;align-items:center;gap:10px;height:40px;padding:0 6px"><span style="width:26px;height:26px;background:var(--ui-bg-accented)"></span><span style="width:80px;height:12px;background:var(--ui-bg-accented)"></span></div></sc-if>
<sc-if value="{{ notBoot }}"><div data-act="usermenu" role="button" aria-haspopup="menu" aria-expanded="{{ menuOpen }}" title="Account" style="display:flex;align-items:center;gap:10px;height:40px;padding:0 6px;background:{{ chipBg }};color:{{ chipFg }}"><span style="width:26px;height:26px;flex-shrink:0;background:var(--rc-sand);color:var(--rc-on-sand);display:flex;align-items:center;justify-content:center;font:600 11px 'IBM Plex Sans',sans-serif">{{ ini }}</span><span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500">{{ who }}</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevrons-up-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevrons-up-down.svg') center / contain no-repeat"></span></div></sc-if>
</div>
<sc-if value="{{ deskMenu }}"><div role="menu" aria-label="Account" style="position:absolute;left:12px;bottom:56px;z-index:40;width:224px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px"><div style="padding:8px 8px 10px;display:flex;flex-direction:column;gap:2px;border-bottom:1px solid var(--ui-border);margin-bottom:4px"><span style="font-weight:600;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ who }}</span><span style="font-size:12px;color:var(--ui-text-muted)">marta@renew.example</span></div><div data-act="nav-profile" role="menuitem" style="height:32px;padding:0 8px;display:flex;align-items:center;gap:10px;background:{{ pfItemBg }};color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-user.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-user.svg') center / contain no-repeat"></span>Profile</div><div style="height:1px;background:var(--ui-border);margin:4px 0"></div><div style="padding:6px 8px 2px;font-size:11px;font-weight:500;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Theme</div><sc-for list="{{ themeSeg }}" as="m" hint-placeholder-count="3"><div data-act="{{ m.act }}" role="menuitemradio" aria-checked="{{ m.on }}" style="height:32px;padding:0 8px;display:flex;align-items:center;justify-content:space-between;font-size:14px;color:var(--ui-text-highlighted)">{{ m.t }}<span aria-hidden="true" style="display:inline-block;width:16px;height:16px;background-color:currentColor;opacity:{{ m.op }};-webkit-mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat"></span></div></sc-for><div style="height:1px;background:var(--ui-border);margin:4px 0"></div><div style="padding:6px 8px 2px;font-size:11px;font-weight:500;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Language</div><sc-for list="{{ langSeg }}" as="m" hint-placeholder-count="2"><div data-act="{{ m.act }}" lang="{{ m.code }}" role="menuitemradio" aria-checked="{{ m.on }}" style="height:32px;padding:0 8px;display:flex;align-items:center;justify-content:space-between;font-size:14px;color:var(--ui-text-highlighted)">{{ m.t }}<span aria-hidden="true" style="display:inline-block;width:16px;height:16px;background-color:currentColor;opacity:{{ m.op }};-webkit-mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat"></span></div></sc-for><div style="height:1px;background:var(--ui-border);margin:4px 0"></div><div data-act="logout" role="menuitem" style="height:32px;padding:0 8px;display:flex;align-items:center;gap:10px;color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/log-out.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/log-out.svg') center / contain no-repeat"></span>Log out</div></div></sc-if>
</sc-if>
<div style="flex:1;min-width:0;min-height:0;display:flex;flex-direction:column">
<sc-if value="{{ desk }}" hint-placeholder-val="{{ true }}">
<div style="height:48px;flex-shrink:0;border-bottom:1px solid var(--ui-border);background:var(--ui-bg);display:flex;align-items:center;padding:0 24px;gap:8px;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/panel-left-close.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/panel-left-close.svg') center / contain no-repeat"></span><span style="width:1px;height:16px;background:var(--ui-border);margin:0 6px"></span><sc-for list="{{ crumbs }}" as="c" hint-placeholder-count="1"><sc-if value="{{ c.sep }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat"></span></sc-if><span style="color:{{ c.fg }};font-weight:{{ c.fw }}">{{ c.t }}</span></sc-for></div>
</sc-if>
<sc-if value="{{ mob }}">
<div style="height:56px;flex-shrink:0;border-bottom:1px solid var(--ui-border);background:var(--ui-bg);display:flex;align-items:center;padding:0 6px;gap:4px;color:var(--ui-text-highlighted)"><span data-act="m-lead" style="position:relative;width:44px;height:44px;display:flex;align-items:center;justify-content:center"><sc-if value="{{ s.navBadge }}"><span aria-label="Update available" style="position:absolute;top:9px;right:9px;width:10px;height:10px;background:var(--ui-info);border:2px solid var(--ui-bg)"></span></sc-if><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:20px;height:20px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ mLead }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ mLead }}.svg') center / contain no-repeat"></span></span><span style="flex:1;min-width:0;font-weight:600;font-size:16px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ s.mTitle }}</span><sc-if value="{{ s.mAction }}"><span data-act="m-action" style="width:44px;height:44px;display:flex;align-items:center;justify-content:center"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:20px;height:20px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ s.mAction }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ s.mAction }}.svg') center / contain no-repeat"></span></span></sc-if></div>
</sc-if>
<sc-if value="{{ s.offline }}"><div role="status" style="flex-shrink:0;display:flex;align-items:center;gap:10px;padding:10px 16px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/wifi-off.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/wifi-off.svg') center / contain no-repeat"></span><span style="flex:1">{{ offText }}</span></div></sc-if>
<div style="flex:1;min-height:0;overflow:hidden;display:flex;flex-direction:column">
<sc-if value="{{ isSlot }}">{{ slotBody }}</sc-if>
<sc-if value="{{ isBoot }}">
<div aria-busy="true" style="padding:{{ pad }};display:flex;flex-direction:column;gap:24px">
<div style="display:flex;flex-direction:column;gap:8px"><span style="width:220px;height:22px;background:var(--ui-bg-accented)"></span><span style="width:320px;max-width:80%;height:12px;background:var(--ui-bg-accented)"></span></div>
<div style="display:grid;grid-template-columns:{{ cols3 }};gap:12px"><span style="height:88px;background:var(--ui-bg-accented)"></span><span style="height:88px;background:var(--ui-bg-accented)"></span><span style="height:88px;background:var(--ui-bg-accented)"></span></div>
<span style="height:220px;background:var(--ui-bg-accented)"></span>
<span style="align-self:flex-start;height:20px;padding:0 6px;display:inline-flex;align-items:center;border:1px dashed var(--ui-border-accented);color:var(--ui-text-toned);font:500 11px 'IBM Plex Mono',monospace">API pending · GET /v1/auth/me</span>
</div>
</sc-if>
<sc-if value="{{ isHome }}">
<div style="padding:{{ pad }};display:flex;flex-direction:column;gap:{{ gap }};max-width:1040px">
<div style="display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap"><div style="flex:1;min-width:200px;display:flex;flex-direction:column;gap:4px"><span style="font:600 20px/28px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Good morning, {{ who }}</span><span style="color:var(--ui-text-muted);font-size:14px">{{ s.home.sub }}</span></div><sc-if value="{{ desk }}"><span data-act="new" style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/plus.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/plus.svg') center / contain no-repeat"></span>New article</span></sc-if></div>
<sc-if value="{{ s.home.banner }}"><div role="region" aria-label="Update available" style="display:flex;flex-direction:{{ upBtnDir }};align-items:{{ upBannerAlign }};gap:12px;padding:12px 14px;background:{{ s.home.banner.bg }};border:1px solid var(--ui-border)"><div style="flex:1;display:flex;gap:10px;min-width:0"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:{{ s.home.banner.fg }};-webkit-mask:url('https://api.iconify.design/lucide/{{ s.home.banner.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ s.home.banner.icon }}.svg') center / contain no-repeat"></span></span><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600;color:var(--ui-text-highlighted)">{{ s.home.banner.title }}</span><span style="color:var(--ui-text-toned);text-wrap:pretty">{{ s.home.banner.text }}</span></div></div><div style="display:flex;align-items:center;gap:8px"><span style="flex:1;height:{{ btnH }};padding:0 12px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500">{{ s.home.banner.btn }}</span><span role="button" aria-label="Dismiss" style="width:{{ btnH }};height:{{ btnH }};display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></div></div></sc-if>
<sc-if value="{{ s.home.error }}"><div role="alert" style="display:flex;gap:10px;padding:12px 14px;background:var(--rc-error-soft);color:var(--ui-error)"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:8px"><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">Couldn't load your articles</span><span style="color:var(--ui-text-toned)">Check your connection and try again.</span></div><span style="align-self:flex-start;height:28px;padding:0 10px;display:flex;align-items:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;font-size:12px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat"></span>Try again</span></div></div></sc-if>
<sc-if value="{{ s.home.stats }}">
<div style="display:grid;grid-template-columns:{{ cols3 }};gap:12px">
<sc-for list="{{ s.home.stats }}" as="k" hint-placeholder-count="3"><div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:16px;display:flex;flex-direction:column;gap:4px"><sc-if value="{{ k.skel }}"><span style="width:40px;height:28px;background:var(--ui-bg-accented)"></span><span style="width:70px;height:12px;background:var(--ui-bg-accented)"></span></sc-if><sc-if value="{{ k.n }}"><span style="font:600 24px/32px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ k.n }}</span><span style="color:var(--ui-text-muted)">{{ k.l }}</span></sc-if></div></sc-for>
</div>
</sc-if>
<sc-if value="{{ s.home.empty }}">
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:48px 24px;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center"><span style="width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated);color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:20px;height:20px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/file-text.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/file-text.svg') center / contain no-repeat"></span></span><span style="font-weight:600;font-size:18px;color:var(--ui-text-highlighted)">Write your first article</span><span style="color:var(--ui-text-muted);max-width:340px;font-size:14px;line-height:22px">It starts as a draft, so nothing appears on your site until you publish it.</span><span data-act="new" style="height:{{ btnH }};padding:0 16px;display:flex;align-items:center;gap:6px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/plus.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/plus.svg') center / contain no-repeat"></span>New article</span></div>
</sc-if>
<sc-if value="{{ s.home.recent }}">
<div style="display:flex;flex-direction:column;gap:10px">
<div style="display:flex;align-items:baseline;justify-content:space-between"><span style="font:600 16px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Recently updated</span><span data-act="nav-articles" style="color:var(--ui-text-toned);text-decoration:underline;text-underline-offset:3px">All articles</span></div>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border)">
<sc-for list="{{ s.home.recent }}" as="a" hint-placeholder-count="5">
<div data-act="open-{{ a.id }}" style="display:flex;align-items:center;gap:12px;min-height:{{ rowH }};padding:8px 12px;border-top:1px solid var(--ui-border-muted)">
<sc-if value="{{ a.skel }}"><span style="flex:1;height:12px;background:var(--ui-bg-accented);max-width:60%"></span><span style="width:56px;height:20px;background:var(--ui-bg-accented)"></span></sc-if>
<sc-if value="{{ a.t }}"><span style="flex:1;min-width:0;font-weight:500;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ a.t }}</span><sc-if value="{{ a.on }}"><span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;gap:4px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat"></span>Online</span></sc-if><sc-if value="{{ a.draft }}"><span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;gap:4px;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat"></span>Draft</span></sc-if><sc-if value="{{ desk }}"><span style="width:110px;text-align:right;color:var(--ui-text-muted)">{{ a.u }}</span></sc-if></sc-if>
</div>
</sc-for>
</div>
</div>
</sc-if>
<sc-if value="{{ act.show }}">
<div style="display:flex;flex-direction:column;gap:10px">
<div style="display:flex;align-items:baseline;justify-content:space-between"><span style="font:600 16px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ act.title }}</span><span data-act="nav-activity" style="color:var(--ui-text-toned);text-decoration:underline;text-underline-offset:3px">View all</span></div>
<sc-if value="{{ act.error }}"><div style="display:flex;gap:10px;padding:12px 14px;background:var(--rc-error-soft);color:var(--ui-error)"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:8px"><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">Couldn't load recent activity</span><span style="color:var(--ui-text-toned)">Check your connection and try again.</span></div><span role="button" style="height:28px;padding:0 10px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;font-size:12px;white-space:nowrap;opacity:1"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat;"></span>Try again</span></div></div></sc-if>
<sc-if value="{{ act.empty }}"><div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:32px 24px;display:flex;flex-direction:column;align-items:center;gap:6px;text-align:center"><span style="color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:20px;height:20px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/history.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/history.svg') center / contain no-repeat;"></span></span><span style="font-weight:600;color:var(--ui-text-highlighted)">No activity yet</span><span style="color:var(--ui-text-muted);max-width:260px">{{ act.emptyText }}</span></div></sc-if>
<sc-if value="{{ act.rows }}"><div style="background:var(--ui-bg);border:1px solid var(--ui-border)"><sc-for list="{{ act.rows }}" as="r" hint-placeholder-count="5"><div style="display:flex;align-items:center;gap:12px;min-height:{{ rowH }};padding:8px 12px;border-top:1px solid var(--ui-border-muted)"><sc-if value="{{ r.skel }}"><span style="width:24px;height:24px;flex-shrink:0;background:var(--ui-bg-accented)"></span><span style="flex:1;height:12px;background:var(--ui-bg-accented);max-width:60%"></span></sc-if><sc-if value="{{ r.s }}"><span style="width:24px;height:24px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--rc-sand);color:var(--rc-on-sand);font:600 10px 'IBM Plex Sans',sans-serif">{{ r.ini }}</span><div style="flex:1;min-width:0;display:flex;flex-direction:column"><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--ui-text)">{{ r.s }}<span style="font-weight:600;color:var(--ui-text-highlighted)">{{ r.em }}</span></span><span style="font-size:12px;color:var(--ui-text-muted)">{{ r.meta }}</span></div><sc-if value="{{ r.fail }}"><span style="height:20px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;background:var(--rc-error-soft);color:var(--ui-error);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;"></span>Failed</span></sc-if></sc-if></div></sc-for></div></sc-if>
</div>
</sc-if>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:16px;display:flex;gap:14px;align-items:flex-start"><span style="width:36px;height:36px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--rc-sand);color:var(--rc-on-sand)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/plug.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/plug.svg') center / contain no-repeat"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:4px"><span style="font-weight:600;font-size:14px;color:var(--ui-text-highlighted)">Connect your website</span><span style="color:var(--ui-text-muted);text-wrap:pretty">Your site reads published articles from RenewCMS. Share this guide with whoever builds it.</span><span data-act="nav-integration" style="color:var(--ui-text-highlighted);font-weight:500;text-decoration:underline;text-underline-offset:3px;margin-top:4px">Open the guide</span></div></div>
</div>
</sc-if>
<sc-if value="{{ isList }}">
<div style="padding:{{ pad }};display:flex;flex-direction:column;gap:16px;flex:1;min-height:0">
<div style="display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap"><div style="flex:1;min-width:200px;display:flex;flex-direction:column;gap:2px"><span style="font:600 20px/28px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Articles</span><span style="color:var(--ui-text-muted)">Everything you've written, online and in draft.</span></div><sc-if value="{{ desk }}"><span data-act="new" style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/plus.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/plus.svg') center / contain no-repeat"></span>New article</span></sc-if></div>
<div style="display:flex;flex-direction:{{ tbDir }};gap:{{ tbGap }};border-bottom:{{ tbBorder }}">
<sc-if value="{{ mob }}"><span style="height:44px;padding:0 4px 0 12px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:{{ qFg }};font-size:16px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/search.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/search.svg') center / contain no-repeat"></span><span style="flex:1">{{ qText }}</span><sc-if value="{{ s.list.q }}"><span style="width:36px;height:36px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></sc-if></span></sc-if>
<sc-if value="{{ mob }}"><span role="switch" aria-checked="{{ naOn }}" style="display:flex;flex-direction:column"><span style="height:44px;padding:0 10px;display:flex;align-items:center;gap:8px;border:1px solid {{ naBd }};background:{{ naBg }};color:{{ naFg }};font-size:15px;font-weight:{{ naFw }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/lightbulb.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/lightbulb.svg') center / contain no-repeat"></span>Needs attention<span style="font:500 11px 'IBM Plex Mono',monospace">{{ naN }}</span></span></span></sc-if><sc-if value="{{ multiMob }}"><span style="position:relative;display:flex;flex-direction:column;"><span style="height:44px;padding:0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:{{ sfFg }};font-size:16px;"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat"></span><span style="flex:1">{{ sfText }}</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span><sc-if value="{{ sfMenu }}"><div role="listbox" aria-label="Site" style="position:absolute;left:0;right:0;top:48px;z-index:5;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px"><sc-for list="{{ sfOpts }}" as="o" hint-placeholder-count="3"><div role="option" aria-selected="{{ o.on }}" style="height:44px;padding:0 8px;display:flex;align-items:center;justify-content:space-between;background:{{ o.bg }};color:var(--ui-text-highlighted)">{{ o.t }}<span style="opacity:{{ o.op }};display:flex"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat"></span></span></div></sc-for></div></sc-if></span></sc-if>
<div style="display:flex;gap:20px;flex:1;border-bottom:{{ tabBorder }}"><sc-for list="{{ tabs }}" as="t" hint-placeholder-count="3"><span style="height:{{ tabH }};display:flex;align-items:center;gap:6px;border-bottom:2px solid {{ t.line }};margin-bottom:-1px;font-weight:{{ t.fw }};color:{{ t.fg }};font-size:{{ tabF }}">{{ t.t }}<span style="font:500 11px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">{{ t.n }}</span></span></sc-for></div>
<sc-if value="{{ desk }}"><span role="switch" aria-checked="{{ naOn }}" style="margin-bottom:6px;display:flex"><span style="height:32px;padding:0 10px;display:flex;align-items:center;gap:8px;border:1px solid {{ naBd }};background:{{ naBg }};color:{{ naFg }};font-size:13px;font-weight:{{ naFw }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/lightbulb.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/lightbulb.svg') center / contain no-repeat"></span>Needs attention<span style="font:500 11px 'IBM Plex Mono',monospace">{{ naN }}</span></span></span></sc-if><sc-if value="{{ desk }}"><span style="width:260px;height:32px;margin-bottom:6px;padding:0 4px 0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:{{ qFg }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/search.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/search.svg') center / contain no-repeat"></span><span style="flex:1">{{ qText }}</span><sc-if value="{{ s.list.q }}"><span style="width:24px;height:24px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></sc-if></span></sc-if>
<sc-if value="{{ multiDesk }}"><span style="position:relative;display:flex;flex-direction:column;width:150px;margin-bottom:6px;"><span style="height:32px;padding:0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:{{ sfFg }};"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat"></span><span style="flex:1">{{ sfText }}</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span><sc-if value="{{ sfMenu }}"><div role="listbox" aria-label="Site" style="position:absolute;left:0;right:0;top:36px;z-index:5;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px"><sc-for list="{{ sfOpts }}" as="o" hint-placeholder-count="3"><div role="option" aria-selected="{{ o.on }}" style="height:32px;padding:0 8px;display:flex;align-items:center;justify-content:space-between;background:{{ o.bg }};color:var(--ui-text-highlighted)">{{ o.t }}<span style="opacity:{{ o.op }};display:flex"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat"></span></span></div></sc-for></div></sc-if></span></sc-if>
</div>
<sc-if value="{{ s.list.empty }}">
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:56px 24px;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center"><span style="width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated);color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;width:20px;height:20px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ s.list.empty.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ s.list.empty.icon }}.svg') center / contain no-repeat"></span></span><span style="font-weight:600;font-size:15px;color:var(--ui-text-highlighted)">{{ s.list.empty.title }}</span><span style="color:var(--ui-text-muted);max-width:280px">{{ s.list.empty.text }}</span><span data-act="empty" style="height:{{ btnH }};padding:0 12px;display:flex;align-items:center;font-weight:500;background:{{ emptyBg }};color:{{ emptyFg }};border:1px solid {{ emptyBd }}">{{ s.list.empty.action }}</span></div>
</sc-if>
<sc-if value="{{ pgErr }}"><div role="alert" style="display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--rc-error-soft);color:var(--ui-error)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat"></span><span style="flex:1;color:var(--ui-text-toned)">{{ pgErr }}</span><span data-act="pg-retry" style="height:{{ pgBtn }};padding:0 12px;display:flex;align-items:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat"></span>Retry</span></div></sc-if>
<sc-if value="{{ pgNote }}"><div role="status" style="display:flex;align-items:center;gap:10px;padding:10px 14px;background:var(--rc-info-soft);color:var(--ui-info)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat"></span><span style="flex:1;color:var(--ui-text-toned)">{{ pgNote }}</span></div></sc-if>
<sc-if value="{{ showTable }}">
<div style="background:var(--ui-bg);border:1px solid var(--ui-border)">
<div style="display:grid;grid-template-columns:{{ gridCols }};height:36px;align-items:center;padding:0 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500"><span>Title</span><sc-if value="{{ authorsCol }}"><span>Authors</span></sc-if><sc-if value="{{ multi }}"><span>Site</span></sc-if><span>Status</span><span>Created</span><span style="display:flex;align-items:center;gap:4px;color:var(--ui-text-highlighted)">Updated<span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/arrow-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/arrow-down.svg') center / contain no-repeat"></span></span><span></span></div>
<sc-for list="{{ rows }}" as="a" hint-placeholder-count="6">
<div data-act="open-{{ a.id }}" style="display:grid;grid-template-columns:{{ gridCols }};height:44px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border);background:{{ a.bg }};opacity:{{ a.op }};position:relative">
<sc-if value="{{ a.skel }}"><span style="height:12px;width:60%;background:var(--ui-bg-accented)"></span><sc-if value="{{ authorsCol }}"><span style="width:60px;height:24px;background:var(--ui-bg-accented)"></span></sc-if><sc-if value="{{ multi }}"><span style="width:70px;height:12px;background:var(--ui-bg-accented)"></span></sc-if><span style="width:60px;height:20px;background:var(--ui-bg-accented)"></span><span style="width:80px;height:12px;background:var(--ui-bg-accented)"></span><span style="width:80px;height:12px;background:var(--ui-bg-accented)"></span><span></span></sc-if>
<sc-if value="{{ a.t }}">
<span style="font-weight:500;color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:12px">{{ a.t }}</span>
<sc-if value="{{ authorsCol }}"><span title="{{ a.auNames }}" style="display:flex;align-items:center;min-width:0"><sc-if value="{{ a.hasAu }}"><sc-for list="{{ a.avs }}" as="v" hint-placeholder-count="2"><span style="width:24px;height:24px;margin-left:{{ v.ml }};flex-shrink:0;display:flex;align-items:center;justify-content:center;background:{{ v.bg }};color:var(--ui-text-highlighted);border:2px solid var(--ui-bg);font:600 10px/1 'IBM Plex Sans',sans-serif">{{ v.i }}</span></sc-for><sc-if value="{{ a.auMore }}"><span style="margin-left:6px;font-size:12px;color:var(--ui-text-muted)">{{ a.auMore }}</span></sc-if></sc-if><sc-if value="{{ a.noAu }}"><span aria-label="No authors" style="color:var(--ui-text-dimmed)">—</span></sc-if></span></sc-if><sc-if value="{{ multi }}"><span style="color:var(--ui-text-toned);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:12px">{{ a.site }}</span></sc-if>
<span><sc-if value="{{ a.busy }}"><span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;gap:4px;border:1px solid var(--ui-border-accented);color:var(--ui-text-muted);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat"></span>{{ a.busy }}</span></sc-if><sc-if value="{{ a.showOn }}"><span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;gap:4px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat"></span>Online</span></sc-if><sc-if value="{{ a.showDraft }}"><span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;gap:4px;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat"></span>Draft</span></sc-if><sc-if value="{{ a.seoSug }}"><span title="{{ a.seoTitle }}" style="margin-left:6px;height:22px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;background:var(--rc-warning-soft);color:var(--ui-warning);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/lightbulb.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/lightbulb.svg') center / contain no-repeat"></span>{{ a.seoN }}</span></sc-if><sc-if value="{{ a.seoHid }}"><span title="Hidden from search engines" style="margin-left:6px;height:22px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;white-space:nowrap;flex-shrink:0;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/eye-off.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/eye-off.svg') center / contain no-repeat"></span>Hidden</span></sc-if></span>
<span style="color:var(--ui-text-muted)">{{ a.c }}</span><span style="color:var(--ui-text-muted)">{{ a.u }}</span>
<span data-act="menu-{{ a.id }}" style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);background:{{ a.menuBg }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/ellipsis.svg') center / contain no-repeat"></span></span>
<sc-if value="{{ a.menu }}"><div role="menu" style="position:absolute;right:12px;top:40px;z-index:2;width:220px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px"><div data-act="open-{{ a.id }}" style="height:32px;padding:0 8px;display:flex;align-items:center;gap:8px;background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil.svg') center / contain no-repeat"></span>Edit</div><div data-act="pub-{{ a.id }}" style="height:32px;padding:0 8px;display:flex;align-items:center;gap:8px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ a.menuIcon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ a.menuIcon }}.svg') center / contain no-repeat"></span>{{ a.menuPub }}</div><div data-act="copyurl" style="height:32px;padding:0 8px;display:flex;align-items:center;gap:8px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/link.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/link.svg') center / contain no-repeat"></span>Copy API URL</div><div style="height:1px;background:var(--ui-border);margin:4px 0"></div><div data-act="del-{{ a.id }}" style="height:32px;padding:0 8px;display:flex;align-items:center;gap:8px;color:var(--ui-error)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat"></span>Delete…</div></div></sc-if>
</sc-if>
</div>
</sc-for>
</div>
</sc-if>
<sc-if value="{{ pagerD }}"><div style="display:flex;align-items:center;gap:16px;padding:12px 0 0"><div style="display:contents"><span aria-live="polite" style="flex:1;color:var(--ui-text-muted)">{{ pager.summary }}</span><div style="position:relative"><span data-act="pg-size" role="combobox" aria-label="Rows per page" aria-expanded="{{ pager.sizeOpen }}" style="height:{{ pgBtn }};padding:0 8px 0 12px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);outline:{{ pager.sizeRing }};outline-offset:2px">{{ pager.size }} per page<span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span><sc-if value="{{ pager.sizeOpen }}"><div role="listbox" style="position:absolute;left:0;bottom:calc(100% + 4px);z-index:40;width:160px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px"><sc-for list="{{ pager.sizes }}" as="o" hint-placeholder-count="3"><div role="option" aria-selected="{{ o.on }}" style="height:{{ pgBtn }};padding:0 8px;display:flex;align-items:center;gap:8px;background:{{ o.bg }};color:{{ o.fg }};font-weight:{{ o.fw }}">{{ o.t }}</div></sc-for></div></sc-if></div></div>
<sc-if value="{{ pager.ctl }}"><nav aria-label="Pagination" style="display:flex;align-items:center;gap:4px;justify-content:flex-end">
<span data-act="pg-first" aria-label="First page" style="width:{{ pgBtn }};height:{{ pgBtn }};display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);opacity:{{ pager.backOp }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevrons-left.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevrons-left.svg') center / contain no-repeat"></span></span>
<span data-act="pg-prev" aria-label="Previous page" style="width:{{ pgBtn }};height:{{ pgBtn }};display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);opacity:{{ pager.backOp }};outline:{{ pager.prevRing }};outline-offset:2px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-left.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-left.svg') center / contain no-repeat"></span></span>
<sc-for list="{{ pager.nums }}" as="n" hint-placeholder-count="5"><span data-act="pg-{{ n.t }}" aria-current="{{ n.cur }}" style="min-width:{{ pgBtn }};height:{{ pgBtn }};padding:0 6px;display:flex;align-items:center;justify-content:center;border:1px solid {{ n.bd }};background:{{ n.bg }};color:{{ n.fg }};font-weight:{{ n.fw }};outline:{{ n.ring }};outline-offset:2px">{{ n.t }}</span></sc-for>
<span data-act="pg-next" aria-label="Next page" style="width:{{ pgBtn }};height:{{ pgBtn }};display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);opacity:{{ pager.fwdOp }};outline:{{ pager.nextRing }};outline-offset:2px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat"></span></span>
<span data-act="pg-last" aria-label="Last page" style="width:{{ pgBtn }};height:{{ pgBtn }};display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);opacity:{{ pager.fwdOp }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevrons-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevrons-right.svg') center / contain no-repeat"></span></span>
</nav></sc-if></div></sc-if>
<sc-if value="{{ showCards }}">
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);margin:0 -16px;border-left:0;border-right:0">
<sc-for list="{{ rows }}" as="a" hint-placeholder-count="6">
<div data-act="open-{{ a.id }}" style="position:relative;display:flex;align-items:center;gap:8px;padding:12px 4px 12px 16px;border-bottom:1px solid var(--ui-border);min-height:68px;opacity:{{ a.op }}">
<sc-if value="{{ a.skel }}"><div style="flex:1;display:flex;flex-direction:column;gap:8px"><span style="width:70%;height:14px;background:var(--ui-bg-accented)"></span><span style="width:40%;height:12px;background:var(--ui-bg-accented)"></span></div></sc-if>
<sc-if value="{{ a.t }}"><div style="flex:1;min-width:0;display:flex;flex-direction:column;gap:6px"><span style="font-weight:500;font-size:15px;line-height:21px;color:var(--ui-text-highlighted)">{{ a.t }}</span><div style="display:flex;align-items:center;gap:10px;font-size:12px;color:var(--ui-text-muted)"><sc-if value="{{ a.busy }}"><span style="height:20px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;border:1px solid var(--ui-border-accented);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat"></span>{{ a.busy }}</span></sc-if><sc-if value="{{ a.showOn }}"><span style="height:20px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat"></span>Online</span></sc-if><sc-if value="{{ a.showDraft }}"><span style="height:20px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat"></span>Draft</span></sc-if><sc-if value="{{ a.seoSug }}"><span title="{{ a.seoTitle }}" style="height:20px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;background:var(--rc-warning-soft);color:var(--ui-warning);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/lightbulb.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/lightbulb.svg') center / contain no-repeat"></span>{{ a.seoN }}</span></sc-if><sc-if value="{{ a.seoHid }}"><span title="Hidden from search engines" style="height:20px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;white-space:nowrap;flex-shrink:0;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/eye-off.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/eye-off.svg') center / contain no-repeat"></span>Hidden</span></sc-if><span>Updated {{ a.u }}</span></div><sc-if value="{{ authorsCol }}"><div style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--ui-text-muted);min-width:0"><sc-if value="{{ a.hasAu }}"><span style="display:flex;align-items:center"><sc-for list="{{ a.avs }}" as="v" hint-placeholder-count="2"><span style="width:22px;height:22px;margin-left:{{ v.ml }};flex-shrink:0;display:flex;align-items:center;justify-content:center;background:{{ v.bg }};color:var(--ui-text-highlighted);border:2px solid var(--ui-bg);font:600 9px/1 'IBM Plex Sans',sans-serif">{{ v.i }}</span></sc-for></span></sc-if><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ a.auFirst }}</span><sc-if value="{{ multi }}"><span>· {{ a.site }}</span></sc-if></div></sc-if></div><span data-act="menu-{{ a.id }}" style="width:44px;height:44px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/ellipsis-vertical.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/ellipsis-vertical.svg') center / contain no-repeat"></span></span></sc-if><sc-if value="{{ a.menu }}"><div role="menu" style="position:absolute;right:8px;top:56px;z-index:2;width:220px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px"><div data-act="open-{{ a.id }}" style="height:32px;padding:0 8px;display:flex;align-items:center;gap:8px;background:var(--ui-bg-elevated);color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil.svg') center Line truncated
</div>
</sc-for>
</div>
</sc-if>
<sc-if value="{{ pagerM }}"><div style="display:flex;flex-direction:column;gap:12px;padding:12px 0 16px"><div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap;justify-content:space-between"><span aria-live="polite" style="flex:0 1 auto;color:var(--ui-text-muted)">{{ pager.summary }}</span><div style="position:relative"><span data-act="pg-size" role="combobox" aria-label="Rows per page" aria-expanded="{{ pager.sizeOpen }}" style="height:{{ pgBtn }};padding:0 8px 0 12px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);outline:{{ pager.sizeRing }};outline-offset:2px">{{ pager.size }} per page<span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span><sc-if value="{{ pager.sizeOpen }}"><div role="listbox" style="position:absolute;right:0;bottom:calc(100% + 4px);z-index:40;width:160px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);padding:4px"><sc-for list="{{ pager.sizes }}" as="o" hint-placeholder-count="3"><div role="option" aria-selected="{{ o.on }}" style="height:{{ pgBtn }};padding:0 8px;display:flex;align-items:center;gap:8px;background:{{ o.bg }};color:{{ o.fg }};font-weight:{{ o.fw }}">{{ o.t }}</div></sc-for></div></sc-if></div></div>
<sc-if value="{{ pager.ctl }}"><nav aria-label="Pagination" style="display:flex;align-items:center;gap:8px;">
<span data-act="pg-prev" aria-label="Previous page" style="flex:1;height:{{ pgBtn }};display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);opacity:{{ pager.backOp }};outline:{{ pager.prevRing }};outline-offset:2px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-left.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-left.svg') center / contain no-repeat"></span>Previous</span>
<span aria-current="page" style="flex:0 0 auto;padding:0 4px;color:var(--ui-text-highlighted);font-weight:500;white-space:nowrap">{{ pager.of }}</span>
<span data-act="pg-next" aria-label="Next page" style="flex:1;height:{{ pgBtn }};display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);opacity:{{ pager.fwdOp }};outline:{{ pager.nextRing }};outline-offset:2px;font-weight:500">Next<span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat"></span></span>
</nav></sc-if></div></sc-if>
</div>
<sc-if value="{{ mob }}"><sc-if value="{{ listFab }}"><div data-act="new" style="position:absolute;left:16px;right:16px;bottom:16px;height:48px;display:flex;align-items:center;justify-content:center;gap:8px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;font-size:15px;box-shadow:var(--rc-shadow-pop)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/plus.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/plus.svg') center / contain no-repeat"></span>New article</div></sc-if></sc-if>
</sc-if>
<sc-if value="{{ isEditor }}">
<sc-if value="{{ s.ed.notfound }}">
<div style="padding:{{ pad }};display:flex;flex-direction:column;gap:14px;max-width:560px"><span style="font:500 13px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">404</span><span style="font:600 24px/32px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">This article doesn't exist</span><span style="color:var(--ui-text-toned);font-size:14px;line-height:22px">It may have been deleted, or the link is wrong.</span><span data-act="nav-articles" style="align-self:flex-start;height:{{ btnH }};padding:0 12px;display:flex;align-items:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500">All articles</span></div>
</sc-if>
<sc-if value="{{ edFound }}">
<div style="flex-shrink:0;background:var(--ui-bg);border-bottom:1px solid var(--ui-border);padding:{{ edHeadPad }};display:flex;flex-wrap:wrap;align-items:center;gap:{{ edHeadGap }}">{{ edHeader }}<sc-if value="{{ edDefHead }}">
<sc-if value="{{ desk }}"><span data-act="back" style="width:32px;height:32px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/arrow-left.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/arrow-left.svg') center / contain no-repeat"></span></span></sc-if>
<div style="flex:1 1 {{ edTitleBasis }};min-width:0;display:flex;align-items:center;gap:10px;flex-wrap:wrap">
<sc-if value="{{ s.ed.loading }}"><span style="width:320px;max-width:70%;height:20px;background:var(--ui-bg-accented)"></span><span style="width:60px;height:22px;background:var(--ui-bg-accented)"></span></sc-if>
<sc-if value="{{ edReady }}"></sc-if><span title="{{ edTitleTip }}" style="font:600 18px/26px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted);{{ edTitleCss }}">{{ edTitle }}</span><sc-if value="{{ lg }}"><span role="button" aria-haspopup="listbox" aria-expanded="{{ lg.open }}" aria-label="{{ lg.label }}" style="height:{{ lg.h }};max-width:100%;width:max-content;padding:0 8px;display:inline-flex;align-items:center;gap:6px;border:1px solid {{ lg.bd }};background:var(--ui-bg);color:var(--ui-text-highlighted);font-size:13px;font-weight:500;white-space:nowrap;flex-shrink:0"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/languages.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/languages.svg') center / contain no-repeat"></span><span>{{ lg.name }}</span><span style="flex-shrink:0;font:400 12px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">{{ lg.code }}</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevrons-up-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevrons-up-down.svg') center / contain no-repeat"></span></span><sc-if value="{{ lg.ro }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;background:var(--ui-bg-elevated);color:var(--ui-text-toned);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/lock.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/lock.svg') center / contain no-repeat"></span>Read-only</span></sc-if><sc-if value="{{ lg.fresh }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--ui-warning);background:var(--ui-bg);color:var(--ui-warning);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/refresh-cw.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/refresh-cw.svg') center / contain no-repeat"></span>{{ lg.fresh }}</span></sc-if><sc-if value="{{ edPending }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--ui-border-accented);color:var(--ui-text-muted);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat"></span>{{ s.ed.pending }}</span></sc-if><sc-if value="{{ edOnline }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat"></span>Online</span></sc-if><sc-if value="{{ edDraft }}"><span style="height:26px;padding:0 9px;display:inline-flex;align-items:center;gap:5px;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned);font-size:13px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil-line.svg') center / contain no-repeat"></span>Draft</span></sc-if></sc-if>
</div>
<sc-if value="{{ edReady }}">
<span aria-live="polite" style="display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0;color:{{ save.fg }}"><span aria-hidden="true" style="display:inline-block;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ save.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ save.icon }}.svg') center / contain no-repeat"></span>{{ save.t }}<sc-if value="{{ save.retry }}"><span data-act="save" style="height:24px;padding:0 8px;display:flex;align-items:center;gap:4px;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat"></span>Retry</span></sc-if></span>
<sc-if value="{{ mob }}"><span style="flex:1"></span><span style="font:400 12px 'IBM Plex Mono',monospace;color:{{ counter.fg }}">{{ counter.t }}</span></sc-if>
<sc-if value="{{ desk }}">
<span data-act="preview" style="display:flex;align-items:center;gap:8px;white-space:nowrap;flex-shrink:0;color:var(--ui-text-toned)"><span style="width:32px;height:18px;padding:2px;display:flex;justify-content:{{ pvJust }};background:{{ pvBg }}"><span style="width:14px;height:14px;background:var(--ui-bg)"></span></span>Preview</span>
<span data-act="save" style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-weight:500;opacity:{{ saveOp }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/save.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/save.svg') center / contain no-repeat"></span>Save</span>
<span data-act="publish" style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0;font-weight:500;background:{{ pub.bg }};color:{{ pub.fg }};border:1px solid {{ pub.bd }};opacity:{{ pub.op }}"><span aria-hidden="true" style="display:inline-block;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ pub.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ pub.icon }}.svg') center / contain no-repeat"></span>{{ pub.t }}</span>
<span data-act="overflow" role="button" aria-label="Delete article" style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;white-space:nowrap;flex-shrink:0;font-weight:500;border:1px solid var(--ui-error);color:var(--ui-error);cursor:pointer"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat"></span>{{ delText }}</span>
</sc-if>
</sc-if>
</sc-if></div>
{{ edBanner }}<sc-if value="{{ showToolbar }}">
<div role="toolbar" aria-label="Formatting" style="flex-shrink:0;padding:6px {{ tbPadX }};display:flex;align-items:center;gap:2px;border-bottom:1px solid var(--ui-border);position:relative;overflow:{{ tbOverflow }};background:var(--ui-bg)">
<div style="display:flex;align-items:center;gap:2px;flex-shrink:0;transform:translateX({{ tbShift }})"><sc-for list="{{ toolbar }}" as="t" hint-placeholder-count="20"><sc-if value="{{ t.sep }}"><span style="width:1px;height:20px;background:var(--ui-border-accented);margin:0 6px;flex-shrink:0"></span></sc-if><sc-if value="{{ t.btn }}"><span data-act="tb-{{ t.icon }}" style="height:{{ tbBtn }};min-width:{{ tbBtn }};padding:0 6px;flex-shrink:0;display:flex;align-items:center;justify-content:center;gap:4px;position:relative;color:{{ t.fg }};opacity:{{ t.op }};outline:{{ t.ring }};outline-offset:1px;background:{{ t.bg }}"><span aria-hidden="true" style="display:inline-block;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ t.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ t.icon }}.svg') center / contain no-repeat"></span>{{ t.text }}<sc-if value="{{ t.tip }}"><span role="tooltip" style="position:absolute;top:100%;left:50%;transform:translateX(-50%);margin-top:6px;z-index:30;white-space:nowrap;padding:4px 8px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font:400 12px/16px 'IBM Plex Sans',sans-serif">{{ t.tip }}</span></sc-if></span></sc-if></sc-for></div><sc-if value="{{ tbFadeL }}"><span style="position:absolute;left:0;top:0;bottom:0;width:24px;background:linear-gradient(to right,var(--ui-bg),transparent);pointer-events:none"></span></sc-if><sc-if value="{{ tbFadeR }}"><span style="position:absolute;right:0;top:0;bottom:0;width:32px;background:linear-gradient(to left,var(--ui-bg),transparent);pointer-events:none"></span></sc-if>
</div>
</sc-if>
<div style="flex:1;min-height:0;display:flex">
{{ srcPane }}<div style="flex:1;min-width:0;display:flex;flex-direction:column;background:{{ edBg }}">
<div data-act="type" style="flex:1;min-height:0;overflow:hidden;padding:{{ edPad }};font-family:'Source Serif 4',serif;color:var(--ui-text-highlighted)">
<sc-if value="{{ s.ed.loading }}"><div style="display:flex;flex-direction:column;gap:14px;max-width:672px"><span style="height:14px;background:var(--ui-bg-accented)"></span><span style="height:14px;background:var(--ui-bg-accented);width:92%"></span><span style="height:14px;background:var(--ui-bg-accented);width:80%"></span><span style="height:22px;background:var(--ui-bg-accented);width:45%;margin-top:18px"></span><span style="height:14px;background:var(--ui-bg-accented)"></span><span style="height:14px;background:var(--ui-bg-accented);width:70%"></span></div></sc-if>
<sc-if value="{{ edReady }}">
<div style="max-width:672px;margin:{{ edMargin }}">
<sc-if value="{{ s.ed.preview }}"><div style="font-size:{{ h1F }};line-height:1.25;font-weight:600;margin:0 0 20px">{{ edTitle }}</div></sc-if>
<sc-if value="{{ sampleBody }}"><p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">Every bathroom is different, but the way we price them isn't. We start with a visit, measure everything, and <strong>write down what stays</strong>.</p><sc-if value="{{ lk.on }}"><p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">We also check the <span style="position:relative"><span data-act="link" style="{{ lk.style }}">timber windows</span><sc-if value="{{ lk.pop }}"><span role="dialog" aria-label="Link" style="position:absolute;left:0;top:100%;margin-top:8px;z-index:20;width:288px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop);font:400 13px/20px 'IBM Plex Sans',sans-serif;color:var(--ui-text);display:flex;flex-direction:column;text-align:left"><span style="display:flex;align-items:center;gap:8px;padding:8px 12px;border-bottom:1px solid var(--ui-border);font:400 12px/18px 'IBM Plex Mono',monospace;color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/link.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/link.svg') center / contain no-repeat"></span><span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">/articles/winter-checklist</span></span><span style="display:flex;padding:4px;gap:2px"><span style="height:32px;padding:0 10px;display:flex;align-items:center;gap:6px;color:var(--ui-text-highlighted);background:{{ lk.popEdit }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil.svg') center / contain no-repeat"></span>Edit</span><span style="height:32px;padding:0 10px;display:flex;align-items:center;gap:6px;color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/external-link.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/external-link.svg') center / contain no-repeat"></span>Open</span><span style="height:32px;padding:0 10px;display:flex;align-items:center;gap:6px;color:var(--ui-error)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/unlink.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/unlink.svg') center / contain no-repeat"></span>Remove</span></span></span></sc-if></span> before any tiling starts.</p></sc-if>
<div style="font-size:{{ h2F }};line-height:1.3;font-weight:600;margin:32px 0 12px">What goes into the price</div>
<ul style="margin:0 0 20px;padding-left:28px;list-style:square;font-size:{{ pF }};line-height:1.66"><li>Stripping out and making good</li><li>Plumbing moves, if any</li><li>Tiles, fittings and a day for silicone</li></ul>
<sc-if value="{{ s.ed.image }}"><div style="margin:28px 0;aspect-ratio:16/9;background:var(--ui-bg-elevated);border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);font:400 13px 'IBM Plex Sans',sans-serif">/static/uploadedImages/3f9c…e1.jpg</div></sc-if>
<p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">We don't add a margin on materials you choose yourself.<sc-if value="{{ caret }}"><span style="display:inline-block;width:2px;height:1.1em;background:var(--ui-text-highlighted);vertical-align:-3px;margin-left:1px"></span></sc-if></p></sc-if>{{ edBodyEl }}<sc-if value="{{ edBody }}"><p style="margin:0 0 20px;font-size:{{ pF }};line-height:1.66">{{ edBody }}<sc-if value="{{ caret }}"><span style="display:inline-block;width:2px;height:1.1em;background:var(--ui-text-highlighted);vertical-align:-3px;margin-left:1px"></span></sc-if></p><sc-if value="{{ s.ed.image }}"><div style="margin:28px 0;aspect-ratio:16/9;background:var(--ui-bg-elevated);border:1px dashed var(--ui-border-accented);display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);font:400 13px 'IBM Plex Sans',sans-serif">/static/uploadedImages/3f9c…e1.jpg</div></sc-if></sc-if>
</div>
</sc-if>
</div>
<sc-if value="{{ desk }}"><sc-if value="{{ showToolbar }}"><div style="flex-shrink:0;padding:8px 24px;border-top:1px solid var(--ui-border);background:var(--ui-bg);display:flex;align-items:center;gap:12px"><span style="flex:1;color:{{ counter.fg }};font-size:12px">{{ counter.msg }}</span><span style="display:flex;align-items:center;gap:6px;font:400 12px 'IBM Plex Mono',monospace;color:{{ counter.fg }}"><sc-if value="{{ counter.icon }}"><span aria-hidden="true" style="display:inline-block;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ counter.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ counter.icon }}.svg') center / contain no-repeat"></span></sc-if>{{ counter.t }}</span></div></sc-if></sc-if>
</div>
<sc-if value="{{ desk }}">
<div style="width:280px;flex-shrink:0;border-left:1px solid var(--ui-border);background:var(--ui-bg);padding:20px;display:flex;flex-direction:column;gap:16px">
<span style="font-weight:600;font-size:14px;color:var(--ui-text-highlighted)">Details</span>
<sc-if value="{{ s.ed.loading }}"><div style="display:flex;flex-direction:column;gap:12px;padding-bottom:16px;border-bottom:1px solid var(--ui-border)"><div style="display:flex;align-items:center;gap:8px;min-height:24px;color:var(--ui-text-highlighted)"><span style="display:flex;transform:rotate(-90deg)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span><span style="font-weight:600;font-size:14px">Site and authors</span></div><span style="margin-left:22px;width:40%;height:12px;background:var(--ui-bg-accented)"></span></div><div style="display:flex;flex-direction:column;gap:10px"><span style="height:12px;background:var(--ui-bg-accented)"></span><span style="height:12px;background:var(--ui-bg-accented)"></span><span style="height:12px;width:60%;background:var(--ui-bg-accented)"></span></div></sc-if>
<sc-if value="{{ edReady }}">
<div style="display:flex;flex-direction:column;gap:12px;padding-bottom:16px;border-bottom:1px solid var(--ui-border)"><div data-act="meta-toggle" role="button" tabindex="0" aria-expanded="{{ metaOpen }}" aria-controls="meta-body" style="display:flex;align-items:center;gap:8px;min-height:24px;color:var(--ui-text-highlighted)"><span style="display:flex;transform:rotate({{ metaRot }})"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span><span style="font-weight:600;font-size:14px">Site and authors</span></div><sc-if value="{{ metaOpen }}"><div id="meta-body" style="display:flex;flex-direction:column;gap:16px">{{ edMeta }}</div></sc-if><sc-if value="{{ metaClosed }}"><span style="padding-left:22px;color:var(--ui-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ metaSummary }}</span></sc-if></div><div style="display:flex;flex-direction:column;gap:12px;padding-bottom:16px;border-bottom:1px solid var(--ui-border)"><div data-act="seo-toggle" role="button" tabindex="0" aria-expanded="{{ seoOpen }}" aria-controls="seo-body" style="display:flex;align-items:center;gap:8px;min-height:24px;color:var(--ui-text-highlighted)"><span style="display:flex;transform:rotate({{ seoRot }})"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span><span style="font-weight:600;font-size:14px">Search and sharing</span></div><sc-if value="{{ seoOpen }}"><div id="seo-body" style="display:flex;flex-direction:column;gap:12px">{{ edSeo }}</div></sc-if><sc-if value="{{ seoClosed }}"><span style="padding-left:22px;color:{{ seoFg }};overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ seoSummary }}</span></sc-if></div>
<div style="display:grid;grid-template-columns:auto 1fr;gap:8px 12px"><span style="color:var(--ui-text-muted)">Created</span><span>12 Mar 2026, 09:14</span><span style="color:var(--ui-text-muted)">Updated</span><span>Today, 11:02</span><span style="color:var(--ui-text-muted)">ID</span><span style="font-family:'IBM Plex Mono',monospace">12</span><span style="color:var(--ui-text-muted)">Images</span><span>{{ imgCount }}</span></div>
<div style="display:flex;flex-direction:column;gap:6px"><span style="color:var(--ui-text-muted)">Public API URL</span><span style="display:flex;align-items:center;gap:6px;padding:6px 8px;background:var(--ui-bg-elevated);font:400 12px 'IBM Plex Mono',monospace"><span style="flex:1">/v1/article/12</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/copy.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/copy.svg') center / contain no-repeat"></span></span><span style="color:var(--ui-text-muted);font-size:12px;line-height:18px">{{ apiNote }}</span></div>
<div style="display:flex;flex-direction:column;gap:4px;padding-top:12px;border-top:1px solid var(--ui-border)"><span style="color:var(--ui-text-muted)">Title</span><span style="font-size:12px;line-height:18px;color:var(--ui-text-toned)">{{ edTitleNote }}</span></div>
</sc-if>
</div>
</sc-if>
</div>
<sc-if value="{{ mob }}">{{ edMobBar }}<sc-if value="{{ edDefMob }}"><sc-if value="{{ edReady }}">
<div style="flex-shrink:0;padding:12px 16px;border-top:1px solid var(--ui-border);background:var(--ui-bg);display:flex;flex-direction:column;gap:8px">
<sc-if value="{{ counter.msg }}"><span style="color:{{ counter.fg }};font-size:13px;line-height:19px">{{ counter.msg }}</span></sc-if>
<div style="display:flex;gap:8px"><span data-act="details" style="width:44px;height:44px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/panel-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/panel-right.svg') center / contain no-repeat"></span></span><span data-act="save" style="flex:1;height:44px;padding:0 12px;white-space:nowrap;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-weight:500;font-size:15px;opacity:{{ saveOp }}">Save</span><span data-act="publish" style="flex:1 0 auto;padding:0 16px;white-space:nowrap;height:44px;display:flex;align-items:center;justify-content:center;gap:6px;font-weight:500;font-size:15px;background:{{ pub.bg }};color:{{ pub.fg }};border:1px solid {{ pub.bd }};opacity:{{ pub.op }}">{{ pub.t }}</span></div>
</div>
</sc-if></sc-if></sc-if>
</sc-if>
</sc-if>
<sc-if value="{{ isInteg }}"><div style="position:relative;flex:1;min-height:0;overflow:hidden"><dc-import name="IntegrationPage" s="{{ s }}" mobile="{{ mob }}" style="display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden" hint-size="100%,100%"></dc-import></div></sc-if>
<sc-if value="{{ isKeys }}"><div style="display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden"><dc-import name="ApiKeysPage" s="{{ s }}" mobile="{{ mob }}" style="display:flex;flex-direction:column;flex:1;min-height:0;overflow:hidden" hint-size="100%,100%"></dc-import></div></sc-if>
<sc-if value="{{ isUsers }}">
<div style="padding:{{ pad }};display:flex;flex-direction:column;gap:16px;flex:1;min-height:0">
<div style="display:flex;align-items:flex-end;gap:16px;flex-wrap:wrap"><div style="flex:1;min-width:200px;display:flex;flex-direction:column;gap:2px"><span style="font:600 20px/28px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Users</span><span style="color:var(--ui-text-muted)">Everyone who can sign in and work on your articles.</span></div><sc-if value="{{ desk }}"><span data-act="invite" style="height:32px;padding:0 12px;display:flex;align-items:center;gap:6px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/user-plus.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/user-plus.svg') center / contain no-repeat"></span>Invite user</span></sc-if></div>
<sc-if value="{{ usError }}"><div role="alert" style="display:flex;gap:10px;padding:12px 14px;background:var(--rc-error-soft);color:var(--ui-error)"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:8px"><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">Couldn't load users</span><span style="color:var(--ui-text-toned)">Check your connection and try again.</span></div><span style="align-self:flex-start;height:{{ btnH }};padding:0 10px;display:flex;align-items:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;font-size:12px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat"></span>Try again</span></div></div></sc-if>
<sc-if value="{{ usTable }}">
<div style="background:var(--ui-bg);border:1px solid var(--ui-border)">
<div style="display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr) 84px 120px 110px 150px;gap:12px;height:36px;align-items:center;padding:0 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500"><span>Email</span><span>Display name</span><span>Role</span><span>Invite status</span><span style="display:flex;align-items:center;gap:4px;color:var(--ui-text-highlighted)">Created<span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/arrow-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/arrow-down.svg') center / contain no-repeat"></span></span><span></span></div>
<sc-for list="{{ usRows }}" as="r" hint-placeholder-count="5">
<div style="display:grid;grid-template-columns:minmax(0,1.3fr) minmax(0,1fr) 84px 120px 110px 150px;gap:12px;height:48px;align-items:center;padding:0 12px;border-top:1px solid var(--ui-border);background:{{ r.bg }}">
<sc-if value="{{ r.skel }}"><span style="height:12px;width:60%;background:var(--ui-bg-accented)"></span><span style="height:12px;width:50%;background:var(--ui-bg-accented)"></span><span style="width:52px;height:20px;background:var(--ui-bg-accented)"></span><span style="width:76px;height:20px;background:var(--ui-bg-accented)"></span><span style="width:80px;height:12px;background:var(--ui-bg-accented)"></span></sc-if>
<sc-if value="{{ r.email }}"><span style="display:flex;align-items:center;gap:8px;min-width:0"><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-weight:500;color:{{ r.fg }}">{{ r.email }}</span><sc-if value="{{ r.you }}"><span style="flex-shrink:0;font:500 11px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">you</span></sc-if></span><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:{{ r.dfg }}">{{ r.dn }}</span><span><span style="height:20px;padding:0 7px;display:inline-flex;align-items:center;background:{{ r.rl.bg }};color:{{ r.rl.fg }};border:1px solid {{ r.rl.bd }};font:500 11px 'IBM Plex Mono',monospace">{{ r.rl.t }}</span></span><span><span style="height:22px;padding:0 7px;display:inline-flex;align-items:center;gap:4px;background:{{ r.b.bg }};color:{{ r.b.fg }};border:1px solid {{ r.b.bd }};font-size:12px;font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ r.b.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ r.b.icon }}.svg') center / contain no-repeat"></span>{{ r.b.t }}</span></span><span style="color:var(--ui-text-muted)">{{ r.c }}</span><span style="display:flex;justify-content:flex-end"><sc-if value="{{ r.act }}"><span data-act="{{ r.act.key }}" style="height:28px;padding:0 9px;white-space:nowrap;flex-shrink:0;display:inline-flex;align-items:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:{{ r.act.fg }};font-weight:500;font-size:12px;opacity:{{ r.act.op }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ r.act.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ r.act.icon }}.svg') center / contain no-repeat"></span>{{ r.act.t }}</span></sc-if></span></sc-if>
</div>
</sc-for>
</div>
</sc-if>
<sc-if value="{{ usCards }}">
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);margin:0 -16px;border-left:0;border-right:0">
<sc-for list="{{ usRows }}" as="r" hint-placeholder-count="5">
<div style="display:flex;flex-direction:column;justify-content:center;gap:6px;padding:12px 16px;border-bottom:1px solid var(--ui-border);min-height:68px;background:{{ r.bg }}">
<sc-if value="{{ r.skel }}"><span style="width:55%;height:14px;background:var(--ui-bg-accented)"></span><span style="width:40%;height:12px;background:var(--ui-bg-accented)"></span></sc-if>
<sc-if value="{{ r.email }}"><span style="display:flex;align-items:center;gap:8px;min-width:0"><span style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:15px;line-height:21px;font-weight:500;color:{{ r.fg }}">{{ r.email }}</span><sc-if value="{{ r.you }}"><span style="flex-shrink:0;font:500 11px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">you</span></sc-if></span><sc-if value="{{ r.display2 }}"><span style="color:var(--ui-text-toned)">{{ r.dn }}</span></sc-if><div style="display:flex;align-items:center;gap:8px;font-size:12px;color:var(--ui-text-muted)"><span style="height:20px;padding:0 7px;display:inline-flex;align-items:center;background:{{ r.rl.bg }};color:{{ r.rl.fg }};border:1px solid {{ r.rl.bd }};font:500 11px 'IBM Plex Mono',monospace">{{ r.rl.t }}</span><span style="height:20px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;background:{{ r.b.bg }};color:{{ r.b.fg }};border:1px solid {{ r.b.bd }};font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ r.b.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ r.b.icon }}.svg') center / contain no-repeat"></span>{{ r.b.t }}</span><span>Created {{ r.c }}</span></div><sc-if value="{{ r.act }}"><div style="display:flex;padding-top:4px"><span data-act="{{ r.act.key }}" style="height:44px;padding:0 14px;white-space:nowrap;flex-shrink:0;display:inline-flex;align-items:center;gap:8px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:{{ r.act.fg }};font-weight:500;font-size:13px;opacity:{{ r.act.op }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ r.act.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ r.act.icon }}.svg') center / contain no-repeat"></span>{{ r.act.t }}</span></div></sc-if></sc-if>
</div>
</sc-for>
</div>
</sc-if>
<sc-if value="{{ usOnly }}"><span style="color:var(--ui-text-muted);text-wrap:pretty">Invite someone so they can sign in with their own account instead of sharing yours.</span></sc-if>
</div>
<sc-if value="{{ mob }}"><sc-if value="{{ listFab }}"><div data-act="invite" style="position:absolute;left:16px;right:16px;bottom:16px;height:48px;display:flex;align-items:center;justify-content:center;gap:8px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;font-size:15px;box-shadow:var(--rc-shadow-pop)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/user-plus.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/user-plus.svg') center / contain no-repeat"></span>Invite user</div></sc-if></sc-if>
</sc-if>
<sc-if value="{{ isSettings }}">
<div style="padding:{{ pad }};display:flex;flex-direction:column;gap:16px;max-width:720px;flex-shrink:0;margin-top:{{ setScroll }}">
<div style="display:flex;flex-direction:column;gap:2px"><span style="font:600 20px/28px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Settings</span><span style="color:var(--ui-text-muted)">Workspace information.</span></div>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;align-items:center;justify-content:space-between;gap:16px"><span style="font:600 15px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Version</span><span style="font:400 14px/22px 'IBM Plex Mono',monospace;color:var(--ui-text-toned)">{{ s.upd.v }}</span></div>
<div role="region" aria-labelledby="updates-h" style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;flex-direction:column;gap:16px">
<span id="updates-h" tabindex="-1" style="align-self:flex-start;font:600 15px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted);outline:{{ s.upd.focus }};outline-offset:4px">Updates</span>
<div aria-live="polite" style="display:flex;gap:12px;padding:14px;background:{{ s.upd.bg }}"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:{{ s.upd.fg }};-webkit-mask:url('https://api.iconify.design/lucide/{{ s.upd.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ s.upd.icon }}.svg') center / contain no-repeat"></span></span><div style="flex:1;min-width:0;display:flex;flex-direction:column;gap:4px"><div style="display:flex;align-items:center;flex-wrap:wrap;gap:8px"><span style="font:600 15px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ s.upd.title }}</span><sc-if value="{{ s.upd.pendingTag }}"><span style="height:20px;padding:0 6px;display:inline-flex;align-items:center;background:var(--ui-bg);color:var(--ui-warning);border:1px solid var(--ui-warning);font:500 11px 'IBM Plex Mono',monospace">Pending</span></sc-if></div><span style="color:var(--ui-text-toned);text-wrap:pretty">{{ s.upd.text }}</span><sc-if value="{{ s.upd.bar }}"><div style="display:flex;flex-direction:column;gap:6px;padding-top:8px"><div role="progressbar" aria-label="{{ s.upd.barLabel }}" aria-valuemin="0" aria-valuemax="100" aria-valuenow="{{ s.upd.pct }}" style="height:6px;background:var(--ui-bg-accented)"><div style="height:6px;width:{{ s.upd.barW }};margin-left:{{ s.upd.barL }};background:var(--ui-primary)"></div></div><span style="font:400 12px/16px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">{{ s.upd.barText }}</span></div></sc-if><sc-if value="{{ s.upd.meta }}"><span style="font-size:12px;color:var(--ui-text-muted)">{{ s.upd.meta }}</span></sc-if><sc-if value="{{ s.upd.inBtn }}"><div style="display:flex;padding-top:8px"><span role="button" style="height:{{ btnH }};padding:0 14px;display:flex;align-items:center;justify-content:center;gap:6px;background:{{ s.upd.inBtn.bg }};color:{{ s.upd.inBtn.fg }};border:1px solid {{ s.upd.inBtn.bd }};font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ s.upd.inBtn.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ s.upd.inBtn.icon }}.svg') center / contain no-repeat"></span>{{ s.upd.inBtn.t }}</span></div></sc-if></div></div>
<sc-if value="{{ s.upd.notes }}"><div style="display:flex;flex-direction:column;gap:10px"><span role="button" aria-expanded="{{ s.upd.notes.open }}" style="align-self:flex-start;display:flex;align-items:center;gap:6px;min-height:{{ btnH }};font-weight:500;color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ s.upd.notes.chev }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ s.upd.notes.chev }}.svg') center / contain no-repeat"></span>What's new in {{ s.upd.notes.ver }}</span><sc-if value="{{ s.upd.notes.open }}"><div style="display:flex;flex-direction:column;gap:8px;padding:0 0 0 20px"><span style="font-size:12px;color:var(--ui-text-muted)">{{ s.upd.notes.date }}</span><sc-for list="{{ s.upd.notes.items }}" as="it" hint-placeholder-count="3"><div style="display:flex;gap:8px"><span aria-hidden="true" style="flex-shrink:0;width:4px;height:4px;margin-top:8px;background:var(--ui-text-muted)"></span><span style="color:var(--ui-text-toned);text-wrap:pretty">{{ it.t }}</span></div></sc-for><span style="align-self:flex-start;color:var(--ui-text-highlighted);text-decoration:underline;text-underline-offset:3px;font-weight:500">Read the full release notes</span></div></sc-if></div></sc-if>
<sc-if value="{{ s.upd.cmd }}"><div style="display:flex;flex-direction:column;gap:8px"><span style="font-weight:500;color:var(--ui-text-highlighted)">For whoever manages the server</span><div style="display:flex;flex-direction:{{ upCmdDir }};align-items:{{ upBtnAlign }};gap:8px"><code style="flex:1;min-width:0;padding:8px 12px;background:var(--ui-bg-elevated);color:var(--ui-text-highlighted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;line-height:22px">{{ s.upd.cmd.line }}</code><span role="button" style="height:{{ btnH }};padding:0 12px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:{{ s.upd.cmd.fg }};font-weight:500"><span aria-hidden="true" style="display:inline-block;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ s.upd.cmd.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ s.upd.cmd.icon }}.svg') center / contain no-repeat"></span>{{ s.upd.cmd.t }}</span></div><span style="font-size:12px;color:var(--ui-text-muted)">{{ s.upd.cmd.note }}</span></div></sc-if>
<sc-if value="{{ s.upd.btns }}"><div style="display:flex;flex-direction:{{ upBtnDir }};align-items:{{ upBtnAlign }};gap:8px"><sc-for list="{{ s.upd.btns }}" as="b" hint-placeholder-count="1"><span role="button" style="height:{{ btnH }};padding:0 14px;display:flex;align-items:center;justify-content:center;gap:6px;background:{{ b.bg }};color:{{ b.fg }};border:1px solid {{ b.bd }};opacity:{{ b.op }};font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ b.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ b.icon }}.svg') center / contain no-repeat"></span>{{ b.t }}</span></sc-for></div></sc-if>
<sc-if value="{{ s.upd.hint }}"><span style="font-size:12px;color:var(--ui-text-muted);text-wrap:pretty">{{ s.upd.hint }}</span></sc-if>
<sc-if value="{{ s.upd.link }}"><span style="align-self:flex-start;display:flex;align-items:center;gap:6px;color:var(--ui-text-highlighted);text-decoration:underline;text-underline-offset:3px;font-weight:500">{{ s.upd.link }}<span aria-hidden="true" style="display:inline-block;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/external-link.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/external-link.svg') center / contain no-repeat"></span></span></sc-if>
</div>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;flex-direction:column;gap:12px"><div style="display:flex;flex-direction:column;gap:2px"><span style="font:600 15px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Media</span><span style="color:var(--ui-text-muted);text-wrap:pretty">Image sizes, cache and cleanup for your media library.</span></div><div style="display:flex;flex-direction:column"><div style="display:flex;align-items:center;gap:12px;min-height:44px;padding:8px 0;border-top:1px solid var(--ui-border);color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/layers.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/layers.svg') center / contain no-repeat"></span><div style="flex:1;min-width:0;display:flex;flex-direction:column"><span style="font-weight:500">Manage image variants</span><span style="color:var(--ui-text-muted)">Named sizes requested with ?v=</span></div><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat"></span></div><div style="display:flex;align-items:center;gap:12px;min-height:44px;padding:8px 0;border-top:1px solid var(--ui-border);color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/database.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/database.svg') center / contain no-repeat"></span><div style="flex:1;min-width:0;display:flex;flex-direction:column"><span style="font-weight:500">Cache usage</span><span style="color:var(--ui-text-muted)">Generated images and disk space</span></div><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat"></span></div></div></div>
<sc-if value="{{ showSites }}"><div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;flex-direction:column;gap:12px"><div style="display:flex;flex-direction:column;gap:2px"><span style="font:600 15px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Publishing</span><span style="color:var(--ui-text-muted);text-wrap:pretty">Where your content is shown.</span></div><div style="display:flex;flex-direction:column"><div style="display:flex;align-items:center;gap:12px;min-height:44px;padding:8px 0;border-top:1px solid var(--ui-border);color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/globe.svg') center / contain no-repeat"></span><div style="flex:1;min-width:0;display:flex;flex-direction:column"><span style="font-weight:500">Manage sites</span><span style="color:var(--ui-text-muted)">Base URLs, default image and robots policy</span></div><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-right.svg') center / contain no-repeat"></span></div></div></div></sc-if>
<div role="region" aria-labelledby="cleanup-h" style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;flex-direction:column;gap:16px"><sc-if value="{{ cl.loading }}"><div aria-busy="true" style="display:flex;flex-direction:column;gap:16px"><span style="display:block;width:140px;height:16px;background:var(--ui-bg-accented)"></span><span style="display:block;width:90%;height:13px;background:var(--ui-bg-accented)"></span><span style="display:block;width:55%;height:13px;background:var(--ui-bg-accented)"></span><div style="display:flex;justify-content:space-between;align-items:center"><div style="display:flex;flex-direction:column;gap:6px"><span style="display:block;width:120px;height:13px;background:var(--ui-bg-accented)"></span><span style="display:block;width:180px;height:11px;background:var(--ui-bg-accented)"></span></div><span style="display:block;width:36px;height:20px;background:var(--ui-bg-accented)"></span></div><span style="display:block;width:60%;height:13px;background:var(--ui-bg-accented)"></span><div style="height:1px;background:var(--ui-border)"></div><div style="display:flex;justify-content:space-between;align-items:center"><div style="display:flex;flex-direction:column;gap:6px"><span style="display:block;width:100px;height:13px;background:var(--ui-bg-accented)"></span><span style="display:block;width:220px;height:11px;background:var(--ui-bg-accented)"></span></div><span style="display:block;width:140px;height:32px;background:var(--ui-bg-accented)"></span></div></div></sc-if><sc-if value="{{ cl.ready }}"><div style="display:flex;align-items:flex-start;gap:12px"><div style="flex:1;display:flex;flex-direction:column;gap:2px"><span id="cleanup-h" tabindex="-1" style="align-self:flex-start;font:600 15px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Media cleanup</span><span style="color:var(--ui-text-muted);text-wrap:pretty">Merge identical images so your library stays tidy. Articles that use a copy are switched to the most used one.</span></div><div style="height:24px;display:flex;align-items:center;flex-shrink:0"><sc-if value="{{ cl.saving }}"><span style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;"></span>Saving…</span></sc-if><sc-if value="{{ cl.saved }}"><span role="status" style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ui-success)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;"></span>Saved</span></sc-if></div></div><sc-if value="{{ cl.ro }}"><div style="display:flex;gap:10px;padding:12px 14px;background:var(--rc-info-soft);color:var(--ui-info)"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat;"></span></span><span style="color:var(--ui-text-toned)">Only owners and admins can change this.</span></div></sc-if><div style="display:flex;flex-direction:{{ rowDir }};align-items:{{ rowAlign }};justify-content:{{ rowJust }};gap:{{ rowGap }}"><div style="display:flex;flex-direction:column;gap:2px;min-width:0"><span style="font-weight:500;color:var(--ui-text-highlighted)">Automatic purge</span><span style="font-size:12px;color:var(--ui-text-muted)">Runs in the background. Off by default.</span></div><div role="switch" aria-checked="{{ cl.on }}" style="display:flex;align-items:center;justify-content:center;width:{{ cl.swW }};height:{{ cl.swH }};flex-shrink:0;opacity:{{ cl.swOp }}"><div style="position:relative;width:36px;height:20px;border:1px solid {{ cl.swBd }};background:{{ cl.swBg }};outline:{{ cl.swRing }};outline-offset:2px"><div style="position:absolute;top:1px;left:{{ cl.knobL }};width:16px;height:16px;background:var(--ui-bg);border:1px solid {{ cl.knobBd }};box-sizing:border-box"></div></div></div></div><sc-if value="{{ cl.on }}"><div style="display:flex;flex-direction:{{ rowDir }};align-items:{{ rowAlign }};justify-content:{{ rowJust }};gap:{{ rowGap }}"><div style="display:flex;flex-direction:column;gap:2px;min-width:0"><span style="font-weight:500;color:var(--ui-text-highlighted)">Every</span><span style="font-size:12px;color:var(--ui-text-muted)">Between 1 and 365 days.</span></div><div style="display:flex;flex-direction:column;gaLine truncated
<sc-if value="{{ rt.show }}"><div role="region" aria-labelledby="retention-h" style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;flex-direction:column;gap:16px"><div style="display:flex;align-items:flex-start;gap:12px"><div style="flex:1;display:flex;flex-direction:column;gap:2px"><span id="retention-h" tabindex="-1" style="align-self:flex-start;font:600 15px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Retention</span><span style="color:var(--ui-text-muted);text-wrap:pretty">How long activity is kept, and how much log data to keep. Whichever limit is reached first removes the oldest events. Only owners can change it.</span></div><div style="height:24px;display:flex;align-items:center;flex-shrink:0"><sc-if value="{{ rt.saving }}"><span style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;"></span>Saving…</span></sc-if><sc-if value="{{ rt.saved }}"><span role="status" style="display:flex;align-items:center;gap:6px;font-size:12px;color:var(--ui-success)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;"></span>Saved</span></sc-if></div></div><sc-if value="{{ rt.err }}"><div style="display:flex;gap:10px;padding:12px 14px;background:var(--rc-error-soft);color:var(--ui-error)"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;"></span></span><div style="flex:1;display:flex;flex-direction:column;gap:8px"><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">Couldn't save the retention period</span><span style="color:var(--ui-text-toned)">Your previous setting is still in place.</span></div><span role="button" style="height:28px;padding:0 10px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;font-size:12px;white-space:nowrap;opacity:1">Try again</span></div></div></sc-if><div style="display:flex;flex-direction:{{ rowDir }};align-items:{{ rowAlign }};justify-content:{{ rowJust }};gap:{{ rowGap }}"><div style="display:flex;flex-direction:column;gap:2px;min-width:0"><span style="font-weight:500;color:var(--ui-text-highlighted)">Keep activity for</span><span style="font-size:12px;color:var(--ui-text-muted)">{{ rt.help }}</span></div><div style="display:flex;align-items:center;width:{{ rt.selW }};min-height:{{ inH }};padding:0 12px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);font-size:{{ inF }};color:var(--ui-text-highlighted)"><span style="flex:1">{{ rt.val }}</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;color:var(--ui-text-muted)"></span></div></div><div style="display:flex;align-items:center;gap:12px;color:var(--ui-text-muted);font-size:12px;font-weight:500"><span style="flex:1;height:1px;background:var(--ui-border)"></span><span>or</span><span style="flex:1;height:1px;background:var(--ui-border)"></span></div><div style="display:flex;flex-direction:{{ rowDir }};align-items:{{ rowAlign }};justify-content:{{ rowJust }};gap:{{ rowGap }}"><div style="display:flex;flex-direction:column;gap:2px;min-width:0"><span style="font-weight:500;color:var(--ui-text-highlighted)">Keep at most</span><span style="font-size:12px;color:var(--ui-text-muted)">{{ rt.sizeHelp }}</span></div><div style="display:flex;align-items:center;width:{{ rt.selW }};min-height:{{ inH }};padding:0 12px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);font-size:{{ inF }};color:var(--ui-text-highlighted)"><span style="flex:1">{{ rt.sizeVal }}</span><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;cLine truncated
</div>
</sc-if>
<sc-if value="{{ isProfile }}">
<div style="padding:{{ pad }};display:flex;flex-direction:column;gap:16px;max-width:720px;flex-shrink:0;margin-top:{{ pfShift }}">
<div style="display:flex;flex-direction:column;gap:2px"><span style="font:600 20px/28px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Profile</span><span style="color:var(--ui-text-muted)">Your account on this RenewCMS workspace.</span></div>
<sc-if value="{{ pfLoading }}"><div aria-busy="true" style="display:flex;flex-direction:column;gap:16px"><div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;flex-direction:column;gap:12px"><span style="width:120px;height:14px;background:var(--ui-bg-accented)"></span><span style="height:40px;background:var(--ui-bg-accented)"></span></div><div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;flex-direction:column;gap:12px"><span style="width:90px;height:14px;background:var(--ui-bg-accented)"></span><span style="height:40px;background:var(--ui-bg-accented)"></span><span style="height:40px;background:var(--ui-bg-accented)"></span></div><div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;flex-direction:column;gap:12px"><span style="width:100px;height:14px;background:var(--ui-bg-accented)"></span><span style="height:40px;background:var(--ui-bg-accented)"></span></div></div></sc-if>
<sc-if value="{{ pfReady }}">
<div style="background:var(--ui-bg);border:1px solid var(--ui-border)">
<div style="padding:{{ cardPad }};display:flex;flex-direction:column;gap:14px">
<div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600;font-size:14px;color:var(--ui-text-highlighted)">Display name</span><span style="color:var(--ui-text-muted);text-wrap:pretty">Shown instead of your username across the back-office and in the emails RenewCMS sends. Leave it empty to use your username.</span></div>
<sc-if value="{{ pf.notice }}"><div role="alert" style="display:flex;gap:8px;padding:10px 12px;background:var(--rc-error-soft);color:var(--ui-error)"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat"></span></span><span style="color:var(--ui-text-toned)">{{ pf.notice }}</span></div></sc-if>
<div style="display:flex;flex-direction:column;gap:6px">
<label style="font-weight:500;color:var(--ui-text-highlighted)">Display name <span style="font-weight:400;color:var(--ui-text-muted)">(optional)</span></label>
<div style="height:{{ inH }};padding:0 10px;display:flex;align-items:center;border:1px solid {{ dn.bd }};box-shadow:{{ dn.sh }};background:{{ dn.bg }};color:{{ dn.fg }};font-size:{{ inF }};overflow:hidden;white-space:nowrap"><span style="overflow:hidden;text-overflow:ellipsis">{{ dn.shown }}</span><sc-if value="{{ dn.caret }}"><span style="flex-shrink:0;display:inline-block;width:1px;height:18px;background:var(--ui-text-highlighted);margin-left:1px"></span></sc-if></div>
<div style="display:flex;gap:12px;align-items:flex-start"><span style="flex:1;display:flex;gap:6px;align-items:center;color:var(--ui-error);font-size:12px"><sc-if value="{{ dn.err }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-alert.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-alert.svg') center / contain no-repeat"></span>{{ dn.err }}</sc-if></span><span style="font:400 12px 'IBM Plex Mono',monospace;color:{{ dn.countFg }}">{{ dn.count }}</span></div>
</div>
</div>
<div style="padding:12px {{ cardPadX }};border-top:1px solid var(--ui-border);background:var(--ui-bg-muted);display:flex;align-items:center;gap:12px"><span style="flex:1;min-width:0;color:{{ dn.footFg }};font-size:12px;line-height:18px">{{ dn.foot }}</span><span data-act="pf-save" style="height:{{ btnH }};padding:0 14px;display:flex;align-items:center;gap:6px;flex-shrink:0;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;opacity:{{ dnSave.op }}"><sc-if value="{{ dnSave.loading }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat"></span></sc-if>{{ dnSave.t }}</span></div>
</div>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;flex-direction:column;gap:14px">
<div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600;font-size:14px;color:var(--ui-text-highlighted)">Account</span><span style="color:var(--ui-text-muted);text-wrap:pretty">Set when your account was created. You log in with your username, even after adding a display name.</span></div>
<div style="display:grid;grid-template-columns:{{ pfCols }};gap:14px"><div style="display:flex;flex-direction:column;gap:6px;min-width:0"><span style="font-weight:500;color:var(--ui-text-highlighted)">Username</span><div aria-disabled="true" style="height:{{ inH }};padding:0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border);background:var(--ui-bg-elevated);color:var(--ui-text-toned);font-size:{{ inF }};cursor:not-allowed"><span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">marta</span><span style="display:flex;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/lock.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/lock.svg') center / contain no-repeat"></span></span></div></div><dc-import name="AccountCards" part="email" ac="{{ pfAc }}" c="{{ pfC }}" mobile="{{ mob }}" hint-size="100%,60px" style="display:block;height:auto;min-height:0;flex-shrink:0"></dc-import></div>
<sc-if value="{{ pfPanel }}"><dc-import name="AccountCards" part="panel" ac="{{ pfAc }}" c="{{ pfC }}" mobile="{{ mob }}" hint-size="100%,60px" style="display:block;height:auto;min-height:0;flex-shrink:0"></dc-import></sc-if>
</div>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;flex-direction:column;gap:14px"><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600;font-size:14px;color:var(--ui-text-highlighted)">Author profile</span><span style="color:var(--ui-text-muted);text-wrap:pretty">Your public byline on articles. It's separate from your account.</span></div>
<sc-if value="{{ pAu.err }}"><div role="alert" style="display:flex;gap:10px;padding:12px 14px;background:var(--rc-error-soft);color:var(--ui-error)"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat"></span></span><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">Couldn't create your author profile</span><span style="color:var(--ui-text-toned)">Try again in a moment.</span></div></div></sc-if>
<sc-if value="{{ pAu.none }}"><span style="color:var(--ui-text-toned);text-wrap:pretty">You don't have an author profile yet. Create one to be credited on your own articles.</span><div style="display:flex;flex-direction:{{ pwDir }};align-items:{{ pwAlign }};gap:12px"><span style="height:{{ btnH }};padding:0 12px;display:flex;align-items:center;justify-content:center;gap:6px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;opacity:{{ pAu.op }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ pAu.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ pAu.icon }}.svg') center / contain no-repeat"></span>{{ pAu.btn }}</span><span style="color:var(--ui-text-muted);font-size:12px">{{ pAu.uses }}</span></div></sc-if>
<sc-if value="{{ pAu.has }}"><div style="display:flex;align-items:center;gap:12px;flex-wrap:wrap"><span style="width:40px;height:40px;flex-shrink:0;display:flex;align-items:center;justify-content:center;background:var(--rc-sand);color:var(--rc-on-sand);border:1px solid var(--ui-border);font:600 14px/1 'IBM Plex Sans',sans-serif">MA</span><div style="flex:1;min-width:140px;display:flex;flex-direction:column"><span style="font-weight:500;color:var(--ui-text-highlighted)">Marta Alves</span><span style="font:400 12px/18px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">{{ pAu.slug }}</span></div><span style="height:{{ btnH }};padding:0 12px;display:flex;align-items:center;justify-content:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil.svg') center / contain no-repeat"></span>Edit author</span></div>
<sc-if value="{{ pAu.slugNote }}"><div role="status" style="display:flex;gap:10px;padding:12px 14px;background:var(--rc-info-soft);color:var(--ui-info)"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat"></span></span><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">Your slug is {{ pAu.slug }}</span><span style="color:var(--ui-text-toned)">marta-alves was already used by another author. You can change it in Edit author.</span></div></div></sc-if>
<div style="display:flex;align-items:center;gap:6px;color:var(--ui-text-muted);font-size:12px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:12px;height:12px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/link.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/link.svg') center / contain no-repeat"></span>Linked to your account. New articles start with this author.</div></sc-if></div>
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:{{ cardPad }};display:flex;flex-direction:column;gap:14px">
<div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600;font-size:14px;color:var(--ui-text-highlighted)">Password</span><span style="color:var(--ui-text-muted);text-wrap:pretty">To change it, we'll email a reset link to marta@renew.example. The link works once, within 1 hour.</span></div>
<div style="display:flex;flex-direction:{{ pwDir }};align-items:{{ pwAlign }};gap:12px"><div style="flex:1;min-width:0"><div style="display:flex;flex-direction:column;gap:6px;min-width:0"><span style="font-weight:500;color:var(--ui-text-highlighted)">Password</span><div aria-disabled="true" style="height:{{ inH }};padding:0 10px;display:flex;align-items:center;gap:8px;border:1px solid var(--ui-border);background:var(--ui-bg-elevated);color:var(--ui-text-toned);font-size:{{ inF }};cursor:not-allowed"><span style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;letter-spacing:0.12em">••••••••••••</span><span style="display:flex;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/lock.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/lock.svg') center / contain no-repeat"></span></span></div></div></div><span data-act="pf-reset" style="height:{{ inH }};padding:0 14px;display:flex;align-items:center;justify-content:center;gap:6px;flex-shrink:0;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;white-space:nowrap;opacity:{{ rsBtn.op }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ rsBtn.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ rsBtn.icon }}.svg') center / contain no-repeat"></span>{{ rsBtn.t }}</span></div>
<sc-if value="{{ rsNote }}"><div role="status" style="display:flex;gap:10px;padding:12px 14px;background:{{ rsNote.bg }}"><span style="display:flex;padding-top:2px;color:{{ rsNote.fg }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ rsNote.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ rsNote.icon }}.svg') center / contain no-repeat"></span></span><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600;color:{{ rsNote.fg }}">{{ rsNote.title }}</span><span style="color:var(--ui-text-toned);text-wrap:pretty">{{ rsNote.text }}</span></div></div></sc-if>
</div>
<dc-import name="AccountCards" part="cards" ac="{{ pfAc }}" c="{{ pfC }}" mobile="{{ mob }}" hint-size="100%,60px" style="display:block;height:auto;min-height:0;flex-shrink:0"></dc-import>
</sc-if>
</div>
</sc-if>
<sc-if value="{{ isError }}">
<div style="padding:{{ errPad }};display:flex;flex-direction:column;gap:14px;max-width:560px"><span style="font:500 13px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">{{ s.err.code }}</span><span style="font:600 24px/32px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ s.err.title }}</span><span style="color:var(--ui-text-toned);font-size:14px;line-height:22px;text-wrap:pretty">{{ s.err.text }}</span><div style="display:flex;gap:8px;flex-wrap:wrap"><span data-act="err-primary" style="height:{{ btnH }};padding:0 12px;display:flex;align-items:center;gap:6px;background:var(--ui-primary);color:var(--ui-bg);font-weight:500">{{ s.err.primary }}</span><sc-if value="{{ s.err.secondary }}"><span style="height:{{ btnH }};padding:0 12px;display:flex;align-items:center;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-weight:500">{{ s.err.secondary }}</span></sc-if></div></div>
</sc-if>
</div>
</div>
<sc-if value="{{ mobMenu }}"><div style="position:absolute;inset:0;z-index:50;background:var(--rc-overlay);display:flex;align-items:flex-end"><div role="menu" aria-label="Account" style="width:100%;background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);padding:8px 0 24px"><div style="margin:0 auto 8px;width:36px;height:4px;background:var(--ui-bg-accented)"></div><div style="padding:8px 16px 14px;display:flex;align-items:center;gap:12px;border-bottom:1px solid var(--ui-border);margin-bottom:4px"><span style="width:36px;height:36px;flex-shrink:0;background:var(--rc-sand);color:var(--rc-on-sand);display:flex;align-items:center;justify-content:center;font:600 13px 'IBM Plex Sans',sans-serif">{{ ini }}</span><div style="display:flex;flex-direction:column;min-width:0"><span style="font-weight:600;font-size:15px;color:var(--ui-text-highlighted)">{{ who }}</span><span style="color:var(--ui-text-muted)">marta@renew.example</span></div></div><div data-act="nav-profile" role="menuitem" style="height:48px;padding:0 16px;display:flex;align-items:center;gap:12px;font-size:15px;background:{{ pfItemBg }};color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-user.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-user.svg') center / contain no-repeat"></span>Profile</div><div style="height:1px;background:var(--ui-border);margin:4px 0"></div><div style="padding:8px 16px 2px;font-size:11px;font-weight:500;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Theme</div><sc-for list="{{ themeSeg }}" as="m" hint-placeholder-count="3"><div data-act="{{ m.act }}" role="menuitemradio" aria-checked="{{ m.on }}" style="height:48px;padding:0 16px;display:flex;align-items:center;justify-content:space-between;font-size:15px;color:var(--ui-text-highlighted)">{{ m.t }}<span aria-hidden="true" style="display:inline-block;width:18px;height:18px;background-color:currentColor;opacity:{{ m.op }};-webkit-mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat"></span></div></sc-for><div style="height:1px;background:var(--ui-border);margin:4px 0"></div><div style="padding:8px 16px 2px;font-size:11px;font-weight:500;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Language</div><sc-for list="{{ langSeg }}" as="m" hint-placeholder-count="2"><div data-act="{{ m.act }}" lang="{{ m.code }}" role="menuitemradio" aria-checked="{{ m.on }}" style="height:48px;padding:0 16px;display:flex;align-items:center;justify-content:space-between;font-size:15px;color:var(--ui-text-highlighted)">{{ m.t }}<span aria-hidden="true" style="display:inline-block;width:18px;height:18px;background-color:currentColor;opacity:{{ m.op }};-webkit-mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/check.svg') center / contain no-repeat"></span></div></sc-for><div style="height:1px;background:var(--ui-border);margin:4px 0"></div><div data-act="logout" role="menuitem" style="height:48px;padding:0 16px;display:flex;align-items:center;gap:12px;font-size:15px;color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/log-out.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/log-out.svg') center / contain no-repeat"></span>Log out</div></div></div></sc-if>
<sc-if value="{{ s.overlay }}"><div role="status" aria-live="polite" style="position:absolute;inset:0;z-index:70;background:var(--ui-bg-muted);display:flex;align-items:center;justify-content:center;padding:24px"><div style="width:100%;max-width:440px;display:flex;flex-direction:column;gap:16px;align-items:flex-start"><svg width="26" height="26" viewBox="0 0 16 16" shape-rendering="crispEdges" aria-hidden="true"><rect width="16" height="16" fill="#111"></rect><path fill="#fff" fill-rule="evenodd" d="M4 3h6v1h1v1h1v3h-1v1h-1v1h1v1h1v2h-2v-2h-1v-1H6v3H4zm2 2v3h4V5z"></path></svg><div style="display:flex;flex-direction:column;gap:6px"><span style="font:600 22px/30px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ s.overlay.title }}</span><span style="font-size:14px;line-height:22px;color:var(--ui-text-toned);text-wrap:pretty">{{ s.overlay.text }}</span></div><div role="progressbar" aria-label="Restarting" style="width:100%;height:4px;background:var(--ui-bg-accented)"><div style="width:35%;height:4px;margin-left:{{ s.overlay.barL }};background:var(--ui-primary)"></div></div><span style="font-size:12px;color:var(--ui-text-muted)">{{ s.overlay.meta }}</span><sc-if value="{{ s.overlay.btn }}"><span role="button" style="height:{{ btnH }};padding:0 14px;display:flex;align-items:center;gap:6px;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500">{{ s.overlay.btn }}</span></sc-if></div></div></sc-if>
<sc-if value="{{ s.modal }}">
<div style="position:absolute;inset:0;z-index:50;background:var(--rc-overlay);display:flex;align-items:{{ modalAlign }};justify-content:center;padding:{{ modalPad }}">
<div role="dialog" aria-modal="true" style="width:{{ modalW }};background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column">
<sc-if value="{{ mob }}"><div style="display:flex;justify-content:center;padding-top:8px"><span style="display:block;width:36px;height:4px;background:var(--ui-border-accented)"></span></div></sc-if>
<div style="padding:20px 24px 0;display:flex;align-items:flex-start;gap:12px"><div style="flex:1;display:flex;flex-direction:column;gap:4px"><span style="font:600 16px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ s.modal.title }}</span><sc-if value="{{ s.modal.desc }}"><span style="color:var(--ui-text-muted);text-wrap:pretty">{{ s.modal.desc }}</span></sc-if></div><sc-if value="{{ s.modal.close }}"><span data-act="modal-close" style="width:{{ closeSz }};height:{{ closeSz }};display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></sc-if></div>
<div style="padding:20px 24px 0;display:flex;flex-direction:column;gap:16px">
<sc-if value="{{ s.modal.notice }}"><div role="alert" style="display:flex;gap:8px;padding:10px 12px;background:var(--rc-error-soft);color:var(--ui-error)"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-x.svg') center / contain no-repeat"></span></span><span style="color:var(--ui-text-toned)">{{ s.modal.notice }}</span></div></sc-if>
<sc-if value="{{ s.modal.list }}"><div style="display:flex;flex-direction:column;gap:6px"><sc-for list="{{ mList }}" as="li" hint-placeholder-count="3"><div style="display:flex;gap:8px;align-items:flex-start;color:var(--ui-text-toned)"><span style="display:flex;padding-top:3px;color:var(--ui-error)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span><span style="text-wrap:pretty">{{ li.t }}</span></div></sc-for></div></sc-if>
<sc-if value="{{ mWarn }}"><div role="status" style="display:flex;gap:10px;padding:12px 14px;background:{{ mWarn.bg }};color:{{ mWarn.fg }}"><span style="display:flex;padding-top:2px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ mWarn.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ mWarn.icon }}.svg') center / contain no-repeat"></span></span><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">{{ mWarn.title }}</span><span style="color:var(--ui-text-toned);text-wrap:pretty">{{ mWarn.text }}</span></div></div></sc-if>
<sc-for list="{{ mFields }}" as="f" hint-placeholder-count="0">
<div style="display:flex;flex-direction:column;gap:6px"><div style="display:flex;justify-content:space-between"><span style="font-weight:500;color:var(--ui-text-highlighted)">{{ f.label }}</span><sc-if value="{{ f.count }}"><span style="font:400 12px 'IBM Plex Mono',monospace;color:{{ f.countFg }}">{{ f.count }}</span></sc-if></div><div style="min-height:{{ f.h }};padding:{{ f.p }};display:flex;align-items:{{ f.ai }};border:1px solid {{ f.bd }};box-shadow:{{ f.sh }};background:{{ f.bg }};color:{{ f.fg }};font-size:{{ inF }}">{{ f.shown }}</div><sc-if value="{{ f.err }}"><span style="display:flex;gap:6px;align-items:center;color:var(--ui-error);font-size:12px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/circle-alert.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/circle-alert.svg') center / contain no-repeat"></span>{{ f.err }}</span></sc-if><sc-if value="{{ f.help }}"><span style="color:var(--ui-text-muted);font-size:12px">{{ f.help }}</span></sc-if></div>
</sc-for>
<sc-if value="{{ s.modal.link }}"><span data-act="modal-link" role="link" style="align-self:flex-start;min-height:{{ closeSz }};display:flex;align-items:center;gap:6px;color:var(--ui-text-highlighted);font-weight:500;text-decoration:underline;text-underline-offset:3px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/download.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/download.svg') center / contain no-repeat"></span>{{ s.modal.link }}</span></sc-if>
<sc-if value="{{ s.modal.toggle }}"><div style="display:flex;align-items:center;gap:12px;min-height:{{ inH }}"><span style="width:32px;height:18px;padding:2px;display:flex;justify-content:{{ s.modal.toggle.j }};background:{{ s.modal.toggle.bg }}"><span style="width:14px;height:14px;background:var(--ui-bg)"></span></span><span style="color:var(--ui-text-highlighted);font-weight:500">Open in new tab</span></div></sc-if>
<sc-if value="{{ s.modal.removeLink }}"><div style="display:flex;align-items:center;gap:8px;min-height:{{ inH }};color:var(--ui-error);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/unlink.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/unlink.svg') center / contain no-repeat"></span>Remove link</div></sc-if>
<sc-if value="{{ s.modal.upload }}">
<div data-act="upload-pick" style="height:180px;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;padding:16px;text-align:center;border:{{ up.bd }};background:{{ up.bg }};color:{{ up.fg }}"><span aria-hidden="true" style="display:inline-block;width:24px;height:24px;background-color:{{ up.ic }};-webkit-mask:url('https://api.iconify.design/lucide/{{ up.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ up.icon }}.svg') center / contain no-repeat"></span><span style="font-weight:500;color:var(--ui-text-highlighted)">{{ up.title }}</span><sc-if value="{{ up.progress }}"><div style="width:80%;height:4px;background:var(--ui-bg-accented)"><div style="width:{{ up.progress }};height:4px;background:var(--ui-primary)"></div></div></sc-if><span style="color:var(--ui-text-toned);font-size:12px;max-width:300px">{{ up.text }}</span></div>
</sc-if>
<sc-if value="{{ s.modal.crop }}">
<div style="display:flex;flex-direction:column;gap:16px">
<div style="height:{{ cr.stageH }};padding:16px;background:var(--ui-bg-elevated);display:flex;align-items:center;justify-content:center">
<div role="img" aria-label="north-wall.jpg, 4032 × 3024 px" style="position:relative;height:100%;max-width:100%;aspect-ratio:4/3;overflow:hidden;background:#d8d0c3">
<div style="position:absolute;left:0;right:0;bottom:0;height:32%;background:#8f8373"></div>
<div style="position:absolute;left:56%;top:14%;width:22%;height:42%;background:var(--rc-sand);border:6px solid var(--ui-border-accented)"></div>
<div tabindex="0" aria-label="Crop area. Arrow keys move it, Shift and arrow keys resize it." style="position:absolute;left:{{ cr.l }};top:{{ cr.t }};width:{{ cr.w }};height:{{ cr.h }};box-shadow:0 0 0 2000px var(--rc-overlay);outline:1px solid var(--ui-bg);cursor:move">
<div style="position:absolute;left:33.333%;top:0;bottom:0;width:1px;background:rgb(255 255 255 / 0.45)"></div>
<div style="position:absolute;left:66.666%;top:0;bottom:0;width:1px;background:rgb(255 255 255 / 0.45)"></div>
<div style="position:absolute;top:33.333%;left:0;right:0;height:1px;background:rgb(255 255 255 / 0.45)"></div>
<div style="position:absolute;top:66.666%;left:0;right:0;height:1px;background:rgb(255 255 255 / 0.45)"></div>
<div style="position:absolute;left:-6px;top:-6px;width:12px;height:12px;background:var(--ui-bg);border:1px solid var(--ui-primary)"></div>
<div style="position:absolute;right:-6px;top:-6px;width:12px;height:12px;background:var(--ui-bg);border:1px solid var(--ui-primary)"></div>
<div style="position:absolute;left:-6px;bottom:-6px;width:12px;height:12px;background:var(--ui-bg);border:1px solid var(--ui-primary)"></div>
<div style="position:absolute;right:-6px;bottom:-6px;width:12px;height:12px;background:var(--ui-bg);border:1px solid var(--ui-primary)"></div>
<span style="position:absolute;left:8px;bottom:8px;padding:1px 6px;background:var(--ui-bg-inverted);color:var(--ui-text-inverted);font:400 11px/18px 'IBM Plex Mono',monospace;white-space:nowrap">{{ cr.dims }}</span>
</div>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:6px">
<div style="display:flex;align-items:center;gap:4px"><span style="flex:1;font-weight:500;color:var(--ui-text-highlighted)">Aspect ratio</span><span data-act="crop-rotate" style="height:{{ btnH }};padding:0 8px;display:flex;align-items:center;gap:6px;color:var(--ui-text-toned);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/rotate-ccw-square.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/rotate-ccw-square.svg') center / contain no-repeat"></span>Rotate</span><span data-act="crop-reset" style="height:{{ btnH }};padding:0 8px;display:flex;align-items:center;gap:6px;color:var(--ui-text-toned);font-weight:500"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/undo-2.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/undo-2.svg') center / contain no-repeat"></span>Reset</span></div>
<div role="radiogroup" aria-label="Aspect ratio" style="display:flex;border:1px solid var(--ui-border-accented);width:{{ cr.groupW }}">
<sc-for list="{{ cr.ratios }}" as="p" hint-placeholder-count="5"><span role="radio" aria-checked="{{ p.on }}" data-act="{{ p.act }}" style="flex:{{ cr.pillFlex }};height:{{ btnH }};padding:0 12px;display:flex;align-items:center;justify-content:center;font-weight:500;white-space:nowrap;background:{{ p.bg }};color:{{ p.fg }}">{{ p.t }}</span></sc-for>
</div>
</div>
<div style="display:flex;flex-direction:column;gap:6px">
<span style="font-weight:500;color:var(--ui-text-highlighted)">Width</span>
<div role="radiogroup" aria-label="Width" style="display:flex;border:1px solid var(--ui-border-accented);width:{{ cr.groupW }}">
<sc-for list="{{ cr.sizes }}" as="p" hint-placeholder-count="4"><span role="radio" aria-checked="{{ p.on }}" data-act="{{ p.act }}" style="flex:{{ cr.pillFlex }};height:{{ btnH }};padding:0 12px;display:flex;align-items:center;justify-content:center;font-weight:500;white-space:nowrap;background:{{ p.bg }};color:{{ p.fg }}">{{ p.t }}</span></sc-for>
</div>
<span style="font:400 12px/18px 'IBM Plex Mono',monospace;color:var(--ui-text-muted)">{{ cr.out }}</span>
</div>
<sc-if value="{{ cr.note }}"><div role="status" style="display:flex;gap:8px;padding:10px 12px;background:var(--rc-info-soft)"><span style="display:flex;padding-top:2px;color:var(--ui-info)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/info.svg') center / contain no-repeat"></span></span><span style="color:var(--ui-text-toned);text-wrap:pretty">{{ cr.note }}</span></div></sc-if>
</div>
</sc-if>
</div>
<div style="padding:20px 24px;display:flex;flex-direction:{{ mBtnDir }};justify-content:flex-end;gap:8px">
<sc-if value="{{ s.modal.cancel }}"><span data-act="modal-cancel" style="height:{{ btnH }};padding:0 12px;display:flex;align-items:center;justify-content:center;border:1px solid var(--ui-border-accented);color:{{ mCancelFg }};font-weight:500">{{ s.modal.cancel }}</span></sc-if>
<sc-if value="{{ s.modal.confirm }}"><span data-act="modal-confirm" style="height:{{ btnH }};padding:0 12px;display:flex;align-items:center;justify-content:center;gap:6px;background:{{ mConfirmBg }};color:var(--ui-bg);font-weight:500;opacity:{{ mConfirmOp }};text-align:center"><sc-if value="{{ s.modal.loading }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/loader-circle.svg') center / contain no-repeat"></span></sc-if>{{ s.modal.confirm }}</span></sc-if>
</div>
</div>
</div>
</sc-if>
{{ purgeDlg }}
<sc-if value="{{ navDrawer }}"><div data-act="mnav-close" style="position:absolute;inset:0;z-index:60;display:flex;background:var(--rc-overlay)"><div role="navigation" aria-label="Main" style="width:300px;max-width:85%;height:100%;background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column;padding:12px;gap:2px"><div style="display:flex;align-items:center;gap:10px;height:40px;padding:0 6px;margin-bottom:10px"><svg width="26" height="26" viewBox="0 0 16 16" shape-rendering="crispEdges"><rect width="16" height="16" fill="#111"></rect><path fill="#fff" fill-rule="evenodd" d="M4 3h6v1h1v1h1v3h-1v1h-1v1h1v1h1v2h-2v-2h-1v-1H6v3H4zm2 2v3h4V5z"></path></svg><span style="flex:1;font-weight:600;font-size:14px;color:var(--ui-text-highlighted)">RenewCMS</span></div><sc-for list="{{ nav }}" as="n" hint-placeholder-count="3"><div data-act="nav-{{ n.key }}" style="display:flex;align-items:center;gap:10px;height:44px;padding:0 10px;background:{{ n.bg }};color:{{ n.fg }};font-weight:{{ n.fw }}"><span aria-hidden="true" style="display:inline-block;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ n.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ n.icon }}.svg') center / contain no-repeat"></span><span style="flex:1">{{ n.t }}</span><sc-if value="{{ n.chev }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ n.chev }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ n.chev }}.svg') center / contain no-repeat"></span></sc-if></div><sc-if value="{{ n.sub }}"><div style="display:flex;flex-direction:column;gap:2px;margin-left:19px;border-left:1px solid var(--ui-border)"><span style="padding:6px 10px 2px 12px;font-size:11px;line-height:16px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Media</span><sc-for list="{{ n.subs }}" as="x" hint-placeholder-count="2"><div data-act="nav-{{ x.t }}" style="display:flex;align-items:center;gap:10px;height:44px;padding:0 10px 0 12px;background:{{ x.bg }};color:{{ x.fg }};font-weight:{{ x.fw }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ x.ic }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ x.ic }}.svg') center / contain no-repeat"></span>{{ x.t }}</div></sc-for><sc-if value="{{ n.hasSites }}"><span style="padding:6px 10px 2px 12px;font-size:11px;line-height:16px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Publishing</span></sc-if><sc-for list="{{ n.subs2 }}" as="x2" hint-placeholder-count="1"><div data-act="nav-{{ x2.t }}" style="display:flex;align-items:center;gap:10px;height:44px;padding:0 10px 0 12px;background:{{ x2.bg }};color:{{ x2.fg }};font-weight:{{ x2.fw }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ x2.ic }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ x2.ic }}.svg') center / contain no-repeat"></span>{{ x2.t }}</div></sc-for><sc-if value="{{ n.hasAccess }}"><span style="padding:6px 10px 2px 12px;font-size:11px;line-height:16px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Access</span></sc-if><sc-for list="{{ n.subs3 }}" as="x3" hint-placeholder-count="1"><div data-act="nav-{{ x3.t }}" style="display:flex;align-items:center;gap:10px;height:44px;padding:0 10px 0 12px;background:{{ x3.bg }};color:{{ x3.fg }};font-weight:{{ x3.fw }}"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/{{ x3.ic }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ x3.ic }}.svg') center / contain no-repeat"></span>{{ x3.t }}</div></sc-for></div></sc-if></sc-for><sc-if value="{{ contentShow }}"><div style="display:flex;flex-direction:column;gap:2px;margin-top:8px"><span style="padding:6px 10px 2px;font-size:11px;line-height:16px;font-weight:600;letter-spacing:.04em;text-transform:uppercase;color:var(--ui-text-muted)">Content</span><sc-if value="{{ contentColl }}"><div style="display:flex;align-items:center;gap:10px;height:44px;padding:0 10px;color:var(--ui-text-toned)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/layers.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/layers.svg') center / contain no-repeat"></span><span style="flex:1">All content types</span><span stLine truncated
<sc-if value="{{ lk.sheet }}"><div style="position:absolute;inset:0;z-index:55;background:var(--rc-overlay);display:flex;align-items:flex-end"><div role="dialog" aria-label="Link" style="width:100%;background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column"><div style="display:flex;justify-content:center;padding-top:8px"><span style="display:block;width:36px;height:4px;background:var(--ui-border-accented)"></span></div><div style="display:flex;flex-direction:column;gap:2px;padding:12px 16px 12px;border-bottom:1px solid var(--ui-border)"><span style="font-weight:600;font-size:16px;line-height:24px;color:var(--ui-text-highlighted)">timber windows</span><span style="font:400 12px/18px 'IBM Plex Mono',monospace;color:var(--ui-text-toned);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">/articles/winter-checklist</span></div><div style="display:flex;flex-direction:column;padding:8px 0 16px"><span style="height:48px;padding:0 16px;display:flex;align-items:center;gap:12px;color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/pencil.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/pencil.svg') center / contain no-repeat"></span>Edit link</span><span style="height:48px;padding:0 16px;display:flex;align-items:center;gap:12px;color:var(--ui-text-highlighted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/external-link.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/external-link.svg') center / contain no-repeat"></span>Open link</span><span style="height:48px;padding:0 16px;display:flex;align-items:center;gap:12px;color:var(--ui-error)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:18px;height:18px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/unlink.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/unlink.svg') center / contain no-repeat"></span>Remove link</span></div></div></div></sc-if>
<sc-if value="{{ edSheet }}"><div style="position:absolute;inset:0;z-index:55;background:var(--rc-overlay);display:flex;align-items:flex-end"><div role="dialog" aria-modal="true" aria-label="Details" style="width:100%;height:92%;background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column"><div style="display:flex;justify-content:center;padding-top:8px;flex-shrink:0"><span style="display:block;width:36px;height:4px;background:var(--ui-border-accented)"></span></div><div style="padding:8px 16px 0 16px;display:flex;align-items:center;gap:12px;flex-shrink:0"><span style="flex:1;font:600 16px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">Details</span><span data-act="modal-close" style="width:44px;height:44px;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></span></div><div style="flex:1;min-height:0;overflow:hidden;padding:12px 16px 16px;display:flex;flex-direction:column;gap:16px"><div style="display:flex;flex-direction:column;gap:12px;padding-bottom:16px;border-bottom:1px solid var(--ui-border)"><div data-act="meta-toggle" role="button" tabindex="0" aria-expanded="{{ metaOpen }}" aria-controls="meta-body" style="display:flex;align-items:center;gap:8px;min-height:24px;color:var(--ui-text-highlighted)"><span style="display:flex;transform:rotate({{ metaRot }})"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span><span style="font-weight:600;font-size:14px">Site and authors</span></div><sc-if value="{{ metaOpen }}"><div id="meta-body" style="display:flex;flex-direction:column;gap:16px">{{ edMeta }}</div></sc-if><sc-if value="{{ metaClosed }}"><span style="padding-left:22px;color:var(--ui-text-muted);overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ metaSummary }}</span></sc-if></div><div style="display:flex;flex-direction:column;gap:12px;padding-bottom:16px;border-bottom:1px solid var(--ui-border)"><div data-act="seo-toggle" role="button" tabindex="0" aria-expanded="{{ seoOpen }}" aria-controls="seo-body" style="display:flex;align-items:center;gap:8px;min-height:24px;color:var(--ui-text-highlighted)"><span style="display:flex;transform:rotate({{ seoRot }})"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/chevron-down.svg') center / contain no-repeat"></span></span><span style="font-weight:600;font-size:14px">Search and sharing</span></div><sc-if value="{{ seoOpen }}"><div id="seo-body" style="display:flex;flex-direction:column;gap:12px">{{ edSeo }}</div></sc-if><sc-if value="{{ seoClosed }}"><span style="padding-left:22px;color:{{ seoFg }};overflow:hidden;text-overflow:ellipsis;white-space:nowrap">{{ seoSummary }}</span></sc-if></div><div style="display:grid;grid-template-columns:auto 1fr;gap:8px 12px"><span style="color:var(--ui-text-muted)">Created</span><span>12 Mar 2026, 09:14</span><span style="color:var(--ui-text-muted)">Updated</span><span>Today, 11:02</span><span style="color:var(--ui-text-muted)">ID</span><span style="font-family:'IBM Plex Mono',monospace">12</span></div><div style="display:flex;flex-direction:column;gap:6px"><span style="color:var(--ui-text-muted)">Public API URL</span><span style="padding:6px 8px;background:var(--ui-bg-elevated);font:400 12px 'IBM Plex Mono',monospace">/v1/article/12</span></div></div></div></div></sc-if>
<sc-if value="{{ integSearch }}"><dc-import name="IntegrationSearch" s="{{ s.search }}" mobile="{{ mob }}" hint-size="100%,100%"></dc-import></sc-if>
{{ slotOverlays }}
<sc-if value="{{ hasToasts }}">
<div style="position:absolute;z-index:60;display:flex;flex-direction:column;gap:8px;left:{{ tL }};right:{{ tR }};bottom:{{ tB }};width:{{ tW }}">
<sc-for list="{{ toasts }}" as="t" hint-placeholder-count="1">
<div style="display:flex;gap:10px;padding:12px 14px;background:var(--ui-bg);border:1px solid var(--ui-border);box-shadow:var(--rc-shadow-pop)"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;margin-top:2px;width:16px;height:16px;background-color:{{ t.fg }};-webkit-mask:url('https://api.iconify.design/lucide/{{ t.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ t.icon }}.svg') center / contain no-repeat"></span><div style="flex:1;display:flex;flex-direction:column;gap:6px"><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600;color:var(--ui-text-highlighted)">{{ t.title }}</span><span style="color:var(--ui-text-muted)">{{ t.text }}</span></div><sc-if value="{{ t.action }}"><span data-act="retry" style="align-self:flex-start;height:28px;padding:0 10px;display:flex;align-items:center;gap:6px;border:1px solid var(--ui-border-accented);color:var(--ui-text-highlighted);font-weight:500;font-size:12px"><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:14px;height:14px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/rotate-cw.svg') center / contain no-repeat"></span>{{ t.action }}</span></sc-if></div><span aria-hidden="true" style="display:inline-block;flex-shrink:0;width:16px;height:16px;background-color:currentColor;-webkit-mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/x.svg') center / contain no-repeat"></span></div>
</sc-for>
</div>
</sc-if>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;$preview&quot;:{&quot;width&quot;:1440,&quot;height&quot;:900},&quot;s&quot;:{&quot;editor&quot;:null,&quot;tsType&quot;:&quot;object&quot;},&quot;slotBody&quot;:{&quot;editor&quot;:null,&quot;tsType&quot;:&quot;ReactNode&quot;},&quot;slotOverlays&quot;:{&quot;editor&quot;:null,&quot;tsType&quot;:&quot;ReactNode&quot;},&quot;mobile&quot;:{&quot;editor&quot;:&quot;boolean&quot;,&quot;default&quot;:false,&quot;tsType&quot;:&quot;boolean&quot;}}">
class Component extends DCLogic {
renderVals() {
const s = this.props.s || {}
const m = !!this.props.mobile
const k = s.kind || 'home'
const navItems = s.role === 'user' ? [['home', 'house', 'Home'], ['articles', 'file-text', 'Articles'], ['media', 'images', 'Media'], ['authors', 'feather', 'Authors'], ['activity', 'history', 'Activity']] : [['home', 'house', 'Home'], ['articles', 'file-text', 'Articles'], ['media', 'images', 'Media'], ['authors', 'feather', 'Authors'], ['users', 'users', 'Users'], ['schema', 'shapes', 'Schema'], ['activity', 'history', 'Activity'], ['integration', 'plug', 'Integration'], ['settings', 'settings', 'Settings']]
const cr0 = s.crumbs || [], setOpen = s.nav === 'settings' || !!s.sideOpen, parentOn = (key) => s.nav === key && (key !== 'settings' || cr0.length <= 1)
const nav = navItems.map(([key, icon, t]) => { const on = parentOn(key), open = key === 'settings' && setOpen; return { key, icon, t, bg: on ? 'var(--rc-sand)' : 'transparent', badge: key === 'settings' && !!s.navBadge, fg: on ? 'var(--rc-on-sand)' : open ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)', fw: on || open ? 500 : 400, chev: key === 'settings' ? (open ? 'chevron-down' : 'chevron-right') : '', chevML: key === 'settings' && s.navBadge ? '8px' : 'auto', sub: open, hasSites: !s.role || s.role === 'owner', subs2: (!s.role || s.role === 'owner') ? (() => { const o = cr0[cr0.length - 1] === 'Sites'; const q = cr0[cr0.length - 1] === 'Languages'; return [{ t: 'Sites', ic: 'globe', bg: o ? 'var(--rc-sand)' : 'transparent', fg: o ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: o ? 500 : 400 }, { t: 'Languages', ic: 'languages', bg: q ? 'var(--rc-sand)' : 'transparent', fg: q ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: q ? 500 : 400 }] })() : [], hasAccess: s.role !== 'user', subs3: s.role === 'user' ? [] : [(() => { const o = cr0[1] === 'Roles'; return { t: 'Roles', ic: 'shield', bg: o ? 'var(--rc-sand)' : 'transparent', fg: o ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: o ? 500 : 400 } })()], subs: ['Variants', 'Cache'].map(x => { const o = cr0[cr0.length - 1] === x; return { t: x, ic: x === 'Variants' ? 'layers' : 'database', bg: o ? 'var(--rc-sand)' : 'transparent', fg: o ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: o ? 500 : 400 } }) } })
const CT = s.content || [], ctOn = s.contentOn, ctMax = 5, ctVis = CT.slice(0, Math.max(ctMax, CT.findIndex(x => x.key === ctOn) + 1)), contentRows = (m ? (s.contentOpen ? ctVis : []) : ctVis).map(x => { const on = x.key === ctOn; return { key: x.key, name: x.name, icon: x.icon || 'file-text', bg: on ? 'var(--rc-sand)' : 'transparent', fg: on ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: on ? 500 : 400 } })
const crumbs = (s.crumbs || []).map((t, i, a) => ({ t, sep: i > 0, fg: i === a.length - 1 ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)', fw: i === a.length - 1 ? 500 : 400 }))
const L = s.list || {}
const counts = L.counts || [6, 4, 2]
const tabs = ['All', 'Online', 'Drafts'].map((t, i) => { const on = (L.tab || 0) === i; return { t, n: counts[i], line: on ? 'var(--ui-primary)' : 'transparent', fg: on ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)', fw: on ? 500 : 400 } })
const AVT = ['var(--rc-sand)', 'var(--ui-bg-accented)', 'var(--ui-bg-elevated)'], ini2 = n => n.replace(/^Dr\.\s*/, '').split(/\s+/).slice(0, 2).map(x => x[0]).join('').toUpperCase()
const auInfo = a => { if (a.skel) return {}; const au = a.au || []; return { hasAu: au.length > 0, noAu: au.length === 0, avs: au.slice(0, 3).map((n, i) => ({ i: ini2(n), bg: AVT[(n.length + i) % 3], ml: i ? '-6px' : '0' })), auMore: au.length > 3 ? '+' + (au.length - 3) : '', auNames: au.join(', '), auFirst: au.length ? au[0] + (au.length > 1 ? ' +' + (au.length - 1) : '') : 'No authors', site: a.site || '' } }
const rows = (L.loading ? Array.from({ length: 6 }, () => ({ skel: true })) : (L.rows || [])).map(a => ({ ...a, seoSug: a.seo > 0, seoN: a.seo, seoTitle: a.seo + (a.seo === 1 ? ' suggestion' : ' suggestions'), seoHid: a.seo === 'hidden', showOn: !a.busy && a.on, showDraft: !a.busy && a.t && !a.on, op: a.busy ? '0.55' : '1', bg: a.menu ? 'var(--ui-bg-elevated)' : 'transparent', menuBg: a.menu ? 'var(--ui-bg-accented)' : 'transparent', menuPub: a.on ? 'Unpublish' : 'Publish', menuIcon: a.on ? 'globe-lock' : 'globe', ...auInfo(a) }))
const e = s.ed || {}
const saveMap = { 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)'], error: ['circle-alert', 'Save failed', 'var(--ui-error)'], offline: ['wifi-off', 'Waiting for connection', 'var(--ui-text-muted)'] }
const sv = saveMap[e.save || 'saved']
const save = { icon: sv[0], t: sv[1], fg: sv[2], retry: e.save === 'error' }
const len = e.len ?? 4318
const fmt = n => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
const counter = { t: fmt(Math.round(len / 6)) + ' words · ' + fmt(len) + ' characters', fg: 'var(--ui-text-muted)', icon: '', msg: e.note || '' }
const dirty = e.save === 'dirty' || e.save === 'error'
const online = e.status === 'online'
const blocked = !!s.offline
let pub
if (e.pending) pub = { t: e.pending, icon: 'loader-circle', bg: online ? 'var(--ui-bg)' : 'var(--ui-primary)', fg: online ? 'var(--ui-text-highlighted)' : 'var(--ui-bg)', bd: online ? 'var(--ui-border-accented)' : 'var(--ui-primary)', op: '1' }
else if (online) pub = { t: 'Unpublish', icon: 'globe-lock', bg: 'var(--ui-bg)', fg: 'var(--ui-text-highlighted)', bd: 'var(--ui-border-accented)', op: s.offline ? '0.4' : '1' }
else pub = { t: dirty ? 'Save and publish' : 'Publish', icon: 'globe', bg: 'var(--ui-primary)', fg: 'var(--ui-bg)', bd: 'var(--ui-primary)', op: blocked ? '0.4' : '1' }
const md = s.modal || {}
const mFields = (md.fields || []).map(f => { const has = f.value; return { ...f, shown: has || f.ph || '', fg: md.loading ? 'var(--ui-text-dimmed)' : has ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)', bd: f.err ? 'var(--ui-error)' : f.focus ? 'var(--ui-primary)' : 'var(--ui-border-accented)', sh: f.err ? 'inset 0 0 0 1px var(--ui-error)' : f.focus ? 'inset 0 0 0 1px var(--ui-primary)' : 'none', bg: md.loading ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)', h: f.area ? (m ? '120px' : '96px') : (m ? '44px' : '40px'), p: f.area ? '8px 10px' : '0 10px', ai: f.area ? 'flex-start' : 'center', countFg: f.countErr ? 'var(--ui-error)' : 'var(--ui-text-muted)' } })
const U = md.upload || {}
const upMap = {
uploading: { icon: 'image-plus', ic: 'var(--ui-text-toned)', title: 'north-wall.jpg', text: 'Uploading 180 of 420 KB', progress: '43%', bd: '1px solid var(--ui-border)', bg: 'var(--ui-bg)' },
rejected: { icon: 'file-x', ic: 'var(--ui-error)', title: 'north-wall.tif is 14.2 MB', text: 'Images can be up to 10 MB. Use a PNG, JPEG, WEBP or SVG image.', bd: '1px solid var(--ui-error)', bg: 'var(--ui-bg)' },
idle: { icon: 'image-plus', ic: 'var(--ui-text-toned)', title: 'Drop an image here, or choose a file', text: 'PNG, JPEG, WEBP, SVG · max 10 MB', bd: '1px dashed var(--ui-border-accented)', bg: 'var(--ui-bg)' },
error: { icon: 'circle-x', ic: 'var(--ui-error)', title: 'Upload failed', text: 'The server didn\'t accept the file. Your article hasn\'t changed.', bd: '1px solid var(--ui-error)', bg: 'var(--ui-bg)' }
}
const up = upMap[U.state || 'idle'] || upMap.idle
let cr = {}
if (md.crop) {
const C = md.crop, IW = 4032, IH = 3024
const ri = C.ratio || 0, si = C.size ?? 1
const presets = [[0, 0, 100, 100], [6, 8, 88, 78], [0, 12.5, 100, 75], [0, 0, 100, 100], [12.5, 0, 75, 100]]
const r = C.rect || presets[ri]
const cw = Math.round(IW * r[2] / 100), ch = Math.round(IH * r[3] / 100)
const target = [cw, 1600, 1200, 800][si]
const ow = Math.min(target, cw), oh = Math.round(ch * ow / cw)
const kb = Math.round(ow * oh * 0.22 / 1000)
const pill = (pre, cur) => (t, i) => ({ t, on: i === cur, act: pre + i, bg: i === cur ? 'var(--ui-primary)' : 'transparent', fg: i === cur ? 'var(--ui-bg)' : 'var(--ui-text-toned)' })
cr = {
l: r[0] + '%', t: r[1] + '%', w: r[2] + '%', h: r[3] + '%', dims: fmt(cw) + ' × ' + fmt(ch),
ratios: ['Original', 'Free', '16:9', '4:3', '1:1'].map(pill('crop-r-', ri)),
sizes: ['Original', '1600 px', '1200 px', '800 px'].map(pill('crop-s-', si)),
out: 'Uploads as ' + fmt(ow) + ' × ' + fmt(oh) + ' px · about ' + (kb >= 1000 ? (kb / 1000).toFixed(1) + ' MB' : kb + ' KB'),
note: target > cw ? 'The crop is ' + cw + ' px wide, so it\'s uploaded at that size. Enlarging it would make it blurry.' : '',
stageH: m ? '240px' : '340px', pillFlex: m ? '1 1 0' : '0 0 auto', groupW: m ? '100%' : 'max-content'
}
}
const US = s.users || {}
const badges = { accepted: { t: 'Accepted', icon: 'circle-check', bg: 'var(--rc-success-soft)', fg: 'var(--ui-success)', bd: 'var(--rc-success-soft)' }, pending: { t: 'Pending', icon: 'mail', bg: 'var(--rc-warning-soft)', fg: 'var(--ui-warning)', bd: 'var(--rc-warning-soft)' }, expired: { t: 'Expired', icon: 'clock-alert', bg: 'transparent', fg: 'var(--ui-text-muted)', bd: 'var(--ui-border-accented)' } }
const roles = { owner: { t: 'owner', bg: 'var(--ui-primary)', fg: 'var(--ui-bg)', bd: 'var(--ui-primary)' }, admin: { t: 'admin', bg: 'transparent', fg: 'var(--ui-text-toned)', bd: 'var(--ui-border-accented)' } }
const acts = { pending: { key: 'cancel-invite', t: 'Cancel invite', icon: 'user-x', fg: 'var(--ui-error)', op: '1' }, expired: { key: 'renew-invite', t: 'Renew invite', icon: 'refresh-cw', fg: 'var(--ui-text-highlighted)', op: '1' } }
const usRows = (US.loading ? Array.from({ length: 5 }, () => ({ skel: true })) : (US.rows || [])).map(r => { if (r.skel) return r; const dn = (r.you && s.display) || r.display || ''; const a0 = acts[r.st]; return { ...r, dn: dn || '—', dfg: dn ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)', display2: !!dn, fg: 'var(--ui-text-highlighted)', rl: roles[r.role] || roles.admin, b: badges[r.st] || badges.pending, bg: r.hl ? 'var(--ui-bg-elevated)' : 'transparent', act: a0 && (r.busy ? { ...a0, t: r.busy, icon: 'loader-circle', op: '0.5' } : a0) } })
const who = s.display || 'marta'
const ini = s.display ? s.display.trim().split(/\s+/).slice(0, 2).map(w => w[0]).join('').toUpperCase() : 'MA'
const pf = s.pf || {}
const dnVal = pf.value ?? s.display ?? ''
const dnLen = [...dnVal].length
const dn = { shown: dnVal || (pf.focus ? '' : 'e.g. Marta Alves'), caret: !!pf.focus, fg: pf.saving ? 'var(--ui-text-dimmed)' : dnVal ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)', bd: pf.err ? 'var(--ui-error)' : pf.focus ? 'var(--ui-primary)' : 'var(--ui-border-accented)', sh: pf.err ? 'inset 0 0 0 1px var(--ui-error)' : pf.focus ? 'inset 0 0 0 1px var(--ui-primary)' : 'none', bg: pf.saving ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)', count: dnLen + ' / 60', countFg: dnLen > 60 ? 'var(--ui-error)' : 'var(--ui-text-muted)', err: pf.err || '', foot: pf.dirty && !pf.saving ? 'Unsaved changes' : 'You appear as \u201c' + who + '\u201d' + (s.display ? '.' : ', your username.'), footFg: pf.dirty && !pf.saving ? 'var(--ui-warning)' : 'var(--ui-text-muted)' }
const dnSave = { t: pf.saving ? 'Saving…' : 'Save', loading: !!pf.saving, op: pf.saving || (pf.dirty && !pf.err) ? '1' : '0.4' }
const rs = pf.reset || 'idle'
const rsMap = { sent: { bg: 'var(--rc-success-soft)', fg: 'var(--ui-success)', icon: 'mail-check', title: 'Reset link sent', text: 'Check marta@renew.example. The link works once, within 1 hour.' }, limited: { bg: 'var(--rc-warning-soft)', fg: 'var(--ui-warning)', icon: 'clock-alert', title: 'Too many requests', text: 'You\'ve asked for several links recently. Wait a few minutes, then try again.' }, error: { bg: 'var(--rc-error-soft)', fg: 'var(--ui-error)', icon: 'circle-x', title: 'Couldn\'t send the link', text: 'Check your connection and try again.' } }
const rsNote = rsMap[rs] || null
const rsBtn = { t: rs === 'sending' ? 'Sending…' : rs === 'sent' ? 'Send again' : 'Send reset link', icon: rs === 'sending' ? 'loader-circle' : 'mail', op: rs === 'limited' ? '0.4' : '1' }
const E5 = [['Marta Alves', 'Published the article ', 'Spring release', '2 min ago', 1], ['Marta Alves', 'Edited the article ', 'Why we reuse original floorboards', '29 min ago', 1], ['Not signed in', 'Failed login attempt for ', 'alex@example.com', '1 h ago', 0], ['Jonas Weber', 'Revoked API key ', 'Mobile app', '2 h ago', 1], ['Scheduler', 'Ran the nightly backup', '', '7 h ago', 1]]
const E4 = [['Marta Alves', 'Published the article ', 'Spring release', '2 min ago', 1], ['Marta Alves', 'Edited the article ', 'Why we reuse original floorboards', '29 min ago', 1], ['Marta Alves', 'Deleted the image ', 'old-banner.png', 'Yesterday', 1], ['Marta Alves', 'Uploaded the image ', 'floorboards-copy.jpg', '3 Oct 2026', 1]]
const ho = s.home || {}, own = s.role === 'user'
const actRows = ho.act === 'loading' ? Array.from({ length: 5 }, () => ({ skel: true })) : ho.act === 'ok' ? (own ? E4 : E5).map(([a, t, em, rel, ok]) => ({ s: t, em, meta: own ? rel : a + ' · ' + rel, fail: !ok, ini: a === 'Scheduler' ? 'SY' : a === 'Not signed in' ? '?' : a.split(' ').map(w => w[0]).join('') })) : null
const act = { show: !!ho.act && k === 'home' && !s.boot, title: own ? 'Your recent activity' : 'Recent activity', rows: actRows, error: ho.act === 'error', empty: ho.act === 'empty', emptyText: own ? 'Your actions will appear here.' : 'Actions on your site will appear here.' }
const C = s.cleanup || {}, cOn = !!C.on, cDis = !!(C.ro || C.offline || C.loading), cErr = !!(C.invalid || C.saveErr)
const cl = { loading: !!C.loading, ready: !C.loading, ro: !!C.ro, notRo: !C.ro, on: cOn, saving: !!C.saving, saved: !!C.saved && !C.saving, swW: m ? '44px' : '36px', swH: m ? '44px' : '24px', swOp: cDis ? '0.4' : '1', swRing: C.focusSw ? '2px solid var(--ui-primary)' : '0', swBd: cOn ? 'var(--ui-primary)' : 'var(--ui-border-accented)', swBg: cOn ? 'var(--ui-primary)' : 'var(--ui-bg-accented)', knobL: cOn ? '17px' : '1px', knobBd: cOn ? 'transparent' : 'var(--ui-border-accented)', daysW: m ? '100%' : '140px', daysAlign: m ? 'stretch' : 'flex-end', daysBd: cErr ? 'var(--ui-error)' : C.focusDays ? 'var(--ui-primary)' : 'var(--ui-border-accented)', daysSh: C.focusDays && !cErr ? '0 0 0 1px var(--ui-primary)' : 'none', daysOp: cDis ? '0.4' : '1', daysVal: C.days ?? '30', hasErr: cErr, errTxt: C.invalid ? 'Enter a whole number of days between 1 and 365.' : 'Couldn\'t save. Try again in a moment.', errMax: m ? 'none' : '300px', errTA: m ? 'left' : 'right', lastLine: C.last ? 'Last purge: ' + C.last.d + ' · ' + C.last.c + ' copies merged · ' + C.last.a + ' articles updated' : 'Never run', nextLine: 'Next purge: ' + (C.next || '14 Jul 2026'), purgeOp: C.offline || C.loading ? '0.4' : '1', btnShrink: m ? '1' : '0' }
const R = s.ret || {}, rd = R.days || '90', fv = rd === '0', sz = R.size || '0', hs = sz !== '0'
const rt = { show: !s.noRet, saving: !!R.saving, saved: !!R.saved, err: !!R.err, shorten: !!R.shorten, val: { '30': '30 days', '90': '90 days', '365': '365 days', '0': 'Forever' }[R.shorten ? '30' : rd], help: fv ? (hs ? 'No age limit. Only the size limit applies.' : 'Nothing is removed automatically. The log grows over time.') : (hs ? 'Events older than this are removed.' : 'Older events are removed automatically.'), last: fv && !hs ? 'Last cleanup: Never' : 'Last cleanup: 6 Oct 2026, 03:00 · 142 events removed', next: fv && !hs ? 'Next cleanup: Not scheduled' : 'Next cleanup: 7 Oct 2026, 03:00', sizeVal: hs ? sz + ' GB' : 'No size limit', sizeHelp: hs ? 'When logs pass ' + sz + ' GB, the oldest events are removed. Currently 3.2 GB.' : 'Optional. Logs can grow without a size cap.', selW: m ? '100%' : '160px' }
const purgeDlg = (() => {
if (!s.purge) return null
const h = React.createElement, bh = m ? 44 : 32
const P = t => Object.fromEntries(t.split(';').filter(x => x.trim()).map(x => { const i = x.indexOf(':'); const k = x.slice(0, i).trim(); return [k.startsWith('--') ? k : k.replace(/-([a-z])/g, (_, c) => c.toUpperCase()), x.slice(i + 1).trim()] }))
const D = (t, ...c) => h('div', { style: P(t) }, ...c)
const S = (t, ...c) => h('span', { style: P(t) }, ...c)
const I = (n, z = 16, t = '') => h('span', { 'aria-hidden': 'true', style: { display: 'inline-block', flexShrink: 0, width: z, height: z, backgroundColor: 'currentColor', WebkitMask: 'url(https://api.iconify.design/lucide/' + n + '.svg) center / contain no-repeat', mask: 'url(https://api.iconify.design/lucide/' + n + '.svg) center / contain no-repeat', ...(t ? P(t) : {}) } })
const B = (label, o = {}) => {
const k = o.k || 'outline'
const bg = k === 'primary' ? 'var(--ui-primary)' : k === 'danger' ? 'var(--ui-error)' : 'var(--ui-bg)'
const fg = k === 'primary' || k === 'danger' ? 'var(--ui-bg)' : 'var(--ui-text-highlighted)'
const bd = k === 'primary' || k === 'danger' ? bg : 'var(--ui-border-accented)'
return S('height:' + (o.h || bh) + 'px;padding:0 ' + (o.px || 12) + 'px;display:flex;align-items:center;justify-content:center;gap:6px;background:' + bg + ';color:' + fg + ';border:1px solid ' + bd + ';font-weight:500;font-size:13px;white-space:nowrap;opacity:' + (o.dis ? 0.4 : 1) + ';' + (o.full ? 'flex:1;' : ''), o.icon && I(o.icon, 16), label)
}
const CHECK = 'conic-gradient(var(--ui-bg-accented) 25%, var(--ui-bg) 0 50%, var(--ui-bg-accented) 0 75%, var(--ui-bg) 0) 0 0/16px 16px'
const TONES = ['var(--rc-sand)', 'var(--ui-bg-accented)', 'color-mix(in srgb, var(--ui-border-accented) 35%, var(--ui-bg))', 'var(--ui-bg-elevated)']
const mark = h('svg', { width: 20, height: 20, viewBox: '0 0 16 16', shapeRendering: 'crispEdges' }, h('rect', { width: 16, height: 16, fill: '#111' }), h('path', { fill: '#fff', fillRule: 'evenodd', d: 'M4 3h6v1h1v1h1v3h-1v1h-1v1h1v1h1v2h-2v-2h-1v-1H6v3H4zm2 2v3h4V5z' }))
const Th = (im, t = '') => D('position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);background:' + (im.alpha ? CHECK : 'repeating-linear-gradient(135deg, var(--ui-border) 0 1px, transparent 1px 12px), ' + TONES[im.tone % 4]) + ';' + t, im.svg ? mark : I('image', 16, 'opacity:0.55'))
const IMX = { 12: { tone: 0 }, 7: { tone: 2 }, 2: { tone: 1 }, 4: { tone: 3, alpha: true, svg: true }, 11: { tone: 1 } }
const Alert = (tone, icon, text, title) => 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:2px', title && S('font-weight:600', title), S('color:var(--ui-text-toned)', text)))
const hl = 'color:var(--ui-text-highlighted)'
const mono = "font-family:'IBM Plex Mono',monospace"
const SkelBar = (w, hh = 13) => S('display:block;width:' + w + ';height:' + hh + 'px;background:var(--ui-bg-accented)')
// ---------- dialog
const G = [{ im: 12, n: 'floorboards.jpg', u: 4, c: 2 }, { im: 7, n: 'IMG_4471.jpg', u: 2, c: 1 }, { im: 2, n: 'kitchen-after.webp', u: 3, c: 3 }, { im: 4, n: 'logo-renew.svg', u: 5, c: 3 }, { im: 11, n: 'lime-plaster.jpg', u: 2, c: 1 }]
const pl = (n, w) => n + ' ' + w + (n === 1 ? '' : 's')
const PV = { groups: 4, copies: 9, arts: 12, list: G.slice(0, 4), more: 0, ...(s.purge && s.purge.pv) }
const summary = pv => pl(pv.groups, 'group') + ' · ' + pv.copies + (pv.copies === 1 ? ' copy' : ' copies') + ' will be deleted · ' + pl(pv.arts, 'article') + ' will be updated'
const GList = pv => D('border:1px solid var(--ui-border)', ...pv.list.map((g, i) => D('display:flex;align-items:center;gap:12px;padding:8px 12px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), Th(IMX[g.im], 'width:40px;height:30px;flex-shrink:0;border:1px solid var(--ui-border)'), D('flex:1;min-width:0;display:flex;flex-direction:column', S('font-weight:500;' + hl + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', g.n), S('font-size:12px;color:var(--ui-text-muted)', 'Kept · used in ' + pl(g.u, 'article'))), S('font-size:12px;color:var(--ui-text-toned);white-space:nowrap;text-align:right', pl(g.c, 'copy').replace('copys', 'copies') + ' merged'))), pv.more > 0 && D('padding:8px 12px;border-top:1px solid var(--ui-border-muted);color:var(--ui-text-muted)', 'and ' + pv.more + ' more groups'))
const Link = () => S('display:inline-flex;align-items:center;gap:4px;align-self:flex-start;font-weight:500;' + hl + ';text-decoration:underline;text-underline-offset:3px;min-height:' + (m ? 44 : 20) + 'px', 'Review in Duplicates', I('arrow-right', 14))
const dg = s.purge
const Shell = (o, body) => D('position:absolute;inset:0;z-index:50;background:var(--rc-overlay);display:flex;align-items:' + (m ? 'flex-end' : 'flex-start') + ';justify-content:center;padding:' + (m ? 16 : 140) + 'px 16px 16px',
D('width:' + (m ? '100%' : '448px') + ';max-height:100%;background:var(--ui-bg);box-shadow:var(--rc-shadow-modal);display:flex;flex-direction:column',
o.center ? D('padding:28px 24px 0;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(--rc-info-soft);color:var(--ui-info)', I(o.icon, 20)), S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + hl, o.title), S('color:var(--ui-text-muted);text-wrap:pretty;max-width:340px', o.desc))
: D('padding:20px 24px 0;display:flex;flex-direction:column;gap:4px', S('font:600 16px/24px \'IBM Plex Sans\',sans-serif;' + hl, o.title), o.desc && S('color:var(--ui-text-muted);text-wrap:pretty', o.desc)),
body && 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, full: false }), o.confirm && S('height:' + bh + 'px;padding:0 12px;display:flex;align-items:center;justify-content:center;gap:6px;background:var(--ui-error);color:var(--ui-bg);font-weight:500;opacity:' + (o.cdis ? 0.4 : 1), o.loading ? I('loader-circle') : o.cicon && I(o.cicon), o.confirm))))
const DESC = 'Articles that use a duplicate will be switched to the most used copy of that image, and the other copies will be deleted. This can\'t be undone.'
const DSkel = () => [SkelBar('80%', 16), D('border:1px solid var(--ui-border)', ...[0, 1, 2].map(i => D('display:flex;align-items:center;gap:12px;padding:8px 12px;border-top:' + (i ? '1px solid var(--ui-border-muted)' : '0'), SkelBar('40px', 30), D('flex:1;display:flex;flex-direction:column;gap:6px', SkelBar('45%'), SkelBar('60%', 11)))))]
const Dialog = () => {
const k = dg.k, pv = PV
const gp = 'Purge ' + pl(pv.groups, 'group')
const note = (tone, icon, t) => Alert(tone, icon, t)
if (k === 'loading') return Shell({ title: 'Purge duplicates?', desc: DESC, cancel: 'Cancel', confirm: 'Purge duplicates', cdis: true }, DSkel())
if (k === 'nothing') return Shell({ center: true, icon: 'copy', title: 'Nothing to purge', desc: 'No duplicate is used in an article, so there is nothing to merge.', cancel: 'Close' }, null)
const body = [S('font-weight:500;' + hl, summary(pv)), GList(pv), Link()]
if (k === 'confirm') return Shell({ title: 'Purge duplicates?', desc: DESC, cancel: 'Cancel', confirm: gp }, body)
if (k === 'progress') return Shell({ title: 'Purge duplicates?', desc: DESC, cancel: 'Cancel', confirm: 'Purging…', loading: true }, body)
if (k === 'error') return Shell({ title: 'Purge duplicates?', desc: DESC, cancel: 'Cancel', confirm: gp }, [note('error', 'circle-x', 'Couldn\'t purge the duplicates. Nothing was changed. Try again in a moment.'), ...body])
if (k === 'partial') return Shell({ title: 'Purge duplicates?', desc: DESC, cancel: 'Close', confirm: 'Retry', cicon: 'rotate-cw' }, [note('warning', 'triangle-alert', 'Some groups couldn\'t be merged. 3 of 4 groups were purged. Try again to finish.'), ...body])
if (k === 'conflict') return Shell({ title: 'Purge duplicates?', desc: DESC, cancel: 'Cancel', confirm: gp }, [note('warning', 'triangle-alert', 'The library changed, so nothing was purged. Check the new summary and try again.'), ...body])
}
return Dialog()
})()
const toasts = (s.toasts || []).map(t => ({ ...t, fg: 'var(--ui-' + t.tone + ')' }))
const endpoints = [
{ path: '/v1/article/', desc: 'Lists every online article.', l1: 'var(--ui-primary)', code: "const res = await fetch('https://cms.renew.example/v1/article/')\nconst articles = await res.json()" },
{ path: '/v1/article/{id}', desc: 'Returns one online article. Drafts and unknown ids both return 404.', l1: 'var(--ui-primary)', code: "const res = await fetch('https://cms.renew.example/v1/article/12')\nif (res.status === 404) return notFound()\nconst article = await res.json()" }
]
const toolbarDef = [['undo-2'], ['redo-2'], '|', ['pilcrow', 'Paragraph ▾'], '|', ['bold', '', true], ['italic'], ['underline'], ['strikethrough'], '|', ['align-left', '', true], ['align-center'], ['align-right'], ['align-justify'], '|', ['list'], ['list-ordered'], ['indent-decrease'], ['indent-increase'], '|', ['link'], ['remove-formatting'], ['image-plus', 'Image']]
const LK = e.lk || {}
const lbs = LK.btn || 'default'
const toolbar = toolbarDef.map(x => { if (x === '|') return { sep: true, btn: false }; const base = { sep: false, btn: true, icon: x[0], text: x[1] || '', bg: x[2] ? 'var(--ui-bg-accented)' : 'transparent', fg: 'var(--ui-text-toned)', op: '1', ring: 'none', tip: '' }; if (x[0] !== 'link') return base; return { ...base, bg: lbs === 'hover' ? 'var(--ui-bg-elevated)' : lbs === 'active' ? 'var(--rc-sand)' : 'transparent', fg: lbs === 'hover' ? 'var(--ui-text-highlighted)' : lbs === 'active' ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', op: lbs === 'disabled' ? '0.4' : '1', ring: lbs === 'focus' ? '2px solid var(--ui-primary)' : 'none', tip: lbs === 'hover' && !m ? 'Insert link (Ctrl+K)' : '' } })
const lkT = LK.text || 'default'
const lk = { on: !!LK.on, pop: !!LK.pop && !m, sheet: !!LK.pop && m, popEdit: 'transparent', style: 'color:' + (lkT === 'selected' ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';text-decoration:underline;text-decoration-thickness:' + (lkT === 'hover' ? '2px' : '1px') + ';text-underline-offset:3px;' + (lkT === 'selected' ? 'background:var(--rc-sand);' : '') + (lkT === 'focus' ? 'outline:2px solid var(--ui-primary);outline-offset:2px;' : '') }
const LG = e.lang || null
if (LG && !e.pending && pub) pub = { ...pub, t: pub.t.replace('Save and publish', 'Save and publish ' + LG.name).replace(/^Publish$/, 'Publish ' + LG.name).replace(/^Unpublish$/, 'Unpublish ' + LG.name) }
const lg = LG ? { name: LG.name, code: LG.code, open: !!LG.open, ro: !!LG.ro, fresh: LG.fresh || '', bd: LG.open ? 'var(--ui-primary)' : 'var(--ui-border-accented)', h: m ? '44px' : '26px', label: 'Language: ' + LG.name + '. Switch version' } : null
const edReady = k === 'editor' && !e.loading && !e.notfound
const edTitle = e.title || 'How we price a bathroom renovation'
return {
lg, srcPane: this.props.srcPane || null, edBanner: this.props.edBanner || null, edTitleCss: LG ? 'flex:0 1 100%;min-width:0' : '', edTitleNote: LG ? 'Each language has its own title. Changing it doesn\'t change the URL; edit the slug in Search and sharing.' : "Titles can't be changed after an article is created. To use a different title, create a new article and delete this one.", edTitleTip: LG ? 'Titles can be changed. The URL only changes if you edit the slug.' : "The title can't be changed after creation.", delText: LG ? 'Delete ' + LG.name + ' version' : 'Delete',
lk, edSeo: this.props.edSeo || null, seoOpen: !!this.props.edSeo, seoClosed: !this.props.edSeo, seoRot: this.props.edSeo ? '0deg' : '-90deg', seoSummary: e.seoSummary || 'Ready', seoFg: /suggest/.test(e.seoSummary || '') ? 'var(--ui-warning)' : 'var(--ui-text-muted)', naOn: !!L.needs, naBg: L.needs ? 'var(--rc-sand)' : 'var(--ui-bg)', naFg: L.needs ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', naBd: L.needs ? 'var(--rc-sand)' : 'var(--ui-border-accented)', naFw: L.needs ? 500 : 400, naN: L.naState === 'pending' ? '…' : L.naState === 'fail' ? '!' : L.naState === 'absent' ? '' : String(L.needsN ?? 3), edMeta: this.props.edMeta || null, metaOpen: !!this.props.edMeta, metaClosed: !this.props.edMeta, metaRot: this.props.edMeta ? '0deg' : '-90deg', metaSummary: e.metaSummary || 'Inès Duarte', edSheet: m && !!e.sheet, pAu: (() => { const q = s.pa || {}; return { none: q.k !== 'has', has: q.k === 'has', err: !!q.err, slug: q.slug || 'marta-alves', slugNote: !!q.slug, btn: q.loading ? 'Creating…' : 'Create my author profile', icon: q.loading ? 'loader-circle' : 'feather', op: q.loading ? 0.7 : 1, uses: q.user ? 'Uses your username, marta. You can change it afterwards.' : 'Uses your display name, Marta Alves. You can change it afterwards.' } })(), showSites: !s.role || s.role === 'owner', authorsCol: L.authors !== false, multi: !!L.multi, multiDesk: !m && !!L.multi, multiMob: m && !!L.multi, gridCols: (L.authors !== false ? 'minmax(0,1fr) 104px ' : 'minmax(0,1fr) ') + (L.multi ? '130px ' : '') + '110px 150px 140px 44px', sfText: L.site || 'All sites', sfFg: L.site ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)', sfMenu: !!L.siteMenu, sfOpts: ['All sites', 'Renew', 'Journal', 'Projects', 'Archive'].map(x => { const on = (L.site || 'All sites') === x; return { t: x, on, op: on ? 1 : 0, bg: on ? 'var(--ui-bg-elevated)' : 'transparent' } }), tbOverflow: m ? 'hidden' : 'visible', tbShift: m ? -(LK.shift || 0) + 'px' : '0px', tbFadeL: m && !!LK.shift, tbFadeR: m && !LK.end, act, cl, rt, purgeDlg, rowDir: m ? 'column' : 'row', rowAlign: m ? 'stretch' : 'flex-start', rowJust: m ? 'flex-start' : 'space-between', rowGap: m ? '12px' : '16px', setScroll: -(s.scrollY || 0) + 'px', offText: s.offText || 'You\'re offline. You can keep writing, but saving and publishing will wait until you\'re back online.', cr, s, nav, crumbs, who, ini, pf, dn, dnSave, rsNote, rsBtn,
langSeg: [['en','English'],['fr','Français']].map(([k,tt])=>{const on=(s.lang||'en')===k;return {act:'lang-'+k,code:k,t:tt,on,op:on?1:0}}), themeSeg: [['system','monitor','System'],['light','sun','Light'],['dark','moon','Dark']].map(([k,icon,tt])=>{const on=(s.mode||'light')===k;return {act:'theme-'+k,icon,t:tt,on,op:on?1:0}}), menuOpen: !!s.userMenu, deskMenu: !m && !!s.userMenu, mobMenu: m && !!s.userMenu, pfItemBg: 'var(--ui-bg-elevated)',
chipBg: s.userMenu || s.nav === 'profile' ? 'var(--rc-sand)' : 'transparent', chipFg: s.userMenu || s.nav === 'profile' ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)',
isSettings: k === 'settings', isProfile: k === 'profile', pfLoading: k === 'profile' && !!pf.loading, pfReady: k === 'profile' && !pf.loading,
upBtnDir: m ? 'column' : 'row', upBtnAlign: m ? 'stretch' : 'flex-start', cardPad: '16px', cardPadX: m ? '16px' : '20px', inH: m ? '44px' : '40px', pfCols: m ? 'minmax(0,1fr)' : 'minmax(0,1fr) minmax(0,1.4fr)', pwDir: m ? 'column' : 'row', pwAlign: m ? 'stretch' : 'flex-end', edTitle, edBody: e.body || '', edBodyEl: this.props.edBodyEl || null, sampleBody: !e.body && !this.props.edBodyEl, tabs, rows, save, counter, pub, mFields, up, toasts, endpoints, toolbar,
contentShow: CT.length > 0, contentRows, contentColl: m && !s.contentOpen, contentCount: String(CT.length), contentMore: (!m || s.contentOpen) && CT.length > ctVis.length ? 'Show all ' + CT.length : '', desk: !m, mob: m, notBoot: !s.boot,
fw: m ? '390px' : (s.w ? s.w + 'px' : '1440px'), fh: m ? '844px' : (s.h ? s.h + 'px' : '900px'), dir: m ? 'column' : 'row',
mLead: s.mLead || 'menu',
navDrawer: m && !!s.drawer, integSearch: k === 'integration' && !!s.search, isSlot: !!this.props.slotBody, slotBody: this.props.slotBody || null, slotOverlays: this.props.slotOverlays || null, isBoot: !!s.boot, isHome: k === 'home' && !s.boot, isList: k === 'articles', isEditor: k === 'editor', isInteg: k === 'integration', isKeys: k === 'apikeys', isError: k === 'error', isUsers: k === 'users',
usRows, usError: !!US.error, usOnly: !!US.only && !US.error, usTable: !m && !US.error, usCards: m && !US.error,
pad: m ? '16px 16px 88px' : '24px', gap: '16px', cols3: m ? 'repeat(3,minmax(0,1fr))' : 'repeat(3,minmax(0,1fr))',
upBannerAlign: m ? 'stretch' : 'center', upCmdDir: m ? 'column' : 'row', btnH: m ? '44px' : '32px', rowH: m ? '52px' : '44px', inF: m ? '16px' : '14px',
tbDir: m ? 'column' : 'row', tbGap: m ? '12px' : '16px', tbBorder: m ? '0' : '1px solid var(--ui-border)', tabBorder: m ? '1px solid var(--ui-border)' : '0', tabH: m ? '44px' : '36px', tabF: m ? '14px' : '13px',
qText: L.q ? L.q : 'Search by title', qFg: L.q ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)',
pgBtn: m ? '44px' : '32px', ...(() => {
const g = L.pg
if (!g) return { pager: null, pagerD: false, pagerM: false, pgErr: '', pgNote: '' }
const mk = (t, cur) => ({ t: String(t), cur: cur ? 'page' : 'false', bg: cur ? 'var(--rc-sand)' : 'var(--ui-bg)', fg: cur ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)', bd: cur ? 'var(--ui-primary)' : 'var(--ui-border-accented)', fw: cur ? 600 : 400, ring: g.focus === 'p' + t ? '2px solid var(--ui-primary)' : 'none' })
const set = new Set([1, g.pages, g.page - 1, g.page, g.page + 1].filter(n => n >= 1 && n <= g.pages))
const arr = g.pages <= 7 ? Array.from({ length: g.pages }, (_, i) => i + 1) : [...set].sort((x, y) => x - y)
const nums = []
arr.forEach((n, i) => { if (i && n - arr[i - 1] > 1) nums.push({ t: '…', cur: 'false', bg: 'transparent', fg: 'var(--ui-text-muted)', bd: 'transparent', fw: 400, ring: 'none' }); nums.push(mk(n, n === g.page)) })
const from = g.total ? (g.page - 1) * g.per + 1 : 0, to = Math.min(g.page * g.per, g.total)
const busy = !!g.loading
const fmtN = n => String(n).replace(/\B(?=(\d{3})+(?!\d))/g, ',')
const sizes = [5, 25, 50, 100].map(n => ({ t: n + ' per page', on: n === g.per, bg: n === g.per ? 'var(--rc-sand)' : 'transparent', fg: n === g.per ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)', fw: n === g.per ? 600 : 400 }))
const pager = { summary: busy ? 'Loading page ' + g.loading + '…' : 'Showing ' + from + '–' + to + ' of ' + fmtN(g.total), size: g.per, sizes, sizeOpen: !!g.sizeOpen, sizeRing: g.focus === 'size' ? '2px solid var(--ui-primary)' : 'none', ctl: g.pages > 1, nums, of: 'Page ' + g.page + ' of ' + g.pages, backOp: busy || g.page === 1 ? '0.4' : '1', fwdOp: busy || g.page === g.pages ? '0.4' : '1', prevRing: g.focus === 'prev' ? '2px solid var(--ui-primary)' : 'none', nextRing: g.focus === 'next' ? '2px solid var(--ui-primary)' : 'none' }
return { pager, pagerD: !m, pagerM: m, pgErr: g.err || '', pgNote: g.note || '' }
})(),
showTable: !m && !L.empty, showCards: m && !L.empty, listFab: !s.modal,
emptyBg: L.empty && L.empty.primary ? 'var(--ui-primary)' : 'var(--ui-bg)', emptyFg: L.empty && L.empty.primary ? 'var(--ui-bg)' : 'var(--ui-text-highlighted)', emptyBd: L.empty && L.empty.primary ? 'var(--ui-primary)' : 'var(--ui-border-accented)',
edFound: k === 'editor' && !e.notfound, edReady, edPending: edReady && !!e.pending, edOnline: edReady && !e.pending && online, edDraft: edReady && !e.pending && !online,
edHeadPad: m ? '12px 16px' : '14px 24px', edHeadGap: m ? '8px 12px' : '12px', edTitleBasis: m ? '100%' : '200px',
showToolbar: k === 'editor' && !e.notfound && !e.preview && !e.readonly, edDefHead: !this.props.edHeader, edDefMob: !this.props.edMobBar, edHeader: this.props.edHeader || null, edMobBar: this.props.edMobBar || null,
tbPadX: m ? '8px' : '24px', tbBtn: m ? '40px' : '28px',
edBg: e.preview ? 'var(--ui-bg-muted)' : 'var(--ui-bg)', edPad: m ? '24px 16px' : '40px 48px', edMargin: e.preview ? '0 auto' : '0',
h1F: m ? '28px' : '32px', h2F: m ? '23px' : '26px', pF: m ? '17px' : '18px', caret: !e.preview,
pvJust: e.preview ? 'flex-end' : 'flex-start', pvBg: e.preview ? 'var(--ui-primary)' : 'var(--ui-bg-accented)',
saveOp: (dirty && !blocked) ? '1' : '0.4', imgCount: e.imgs != null ? String(e.imgs) : (e.image ? '4' : '3'), apiNote: online ? 'Live. Your website can read this article.' : 'Returns 404 until the article is online.',
copyIcon: s.copied ? 'check' : 'copy', copyText: s.copied ? 'Copied' : 'Copy', copyFg: s.copied ? 'var(--ui-success)' : 'var(--ui-text-highlighted)',
errPad: m ? '40px 20px' : '64px 48px',
closeSz: m ? '44px' : '28px', modalAlign: m ? 'flex-end' : 'flex-start', modalPad: m ? '0' : (md.crop ? '56px 16px 16px' : '140px 16px 16px'), modalW: m ? '100%' : (md.crop ? '640px' : md.wide ? '560px' : '460px'), mBtnDir: m ? 'column-reverse' : 'row', mConfirmOp: md.disabled ? '0.4' : '1', mList: (md.list || []).map(t => ({ t })), mWarn: md.warn ? { ...md.warn, bg: 'var(--rc-' + md.warn.tone + '-soft)', fg: 'var(--ui-' + md.warn.tone + ')' } : null, pfShift: pf.shift ? '-' + (m ? pf.shift[1] : pf.shift[0]) + 'px' : '0px', pfAc: pf.ac || {}, pfC: s.copy || null, pfPanel: !!(pf.ac && (pf.ac.form || pf.ac.pending)),
mConfirmBg: md.tone === 'danger' ? 'var(--ui-error)' : 'var(--ui-primary)', mCancelFg: md.loading ? 'var(--ui-text-dimmed)' : 'var(--ui-text-highlighted)',
hasToasts: toasts.length > 0, tL: m ? '16px' : 'auto', tR: m ? '16px' : '24px', tB: m ? '16px' : '24px', tW: m ? 'auto' : '356px'
}
}
}
</script>
</body>
</html>
+5 -5
View File
@@ -30,8 +30,8 @@ class Component extends DCLogic {
const mark = h('svg', { width: 28, height: 28, viewBox: '0 0 16 16', shapeRendering: 'crispEdges' }, h('rect', { width: 16, height: 16, fill: '#111' }), h('path', { fill: '#fff', fillRule: 'evenodd', d: 'M4 3h6v1h1v1h1v3h-1v1h-1v1h1v1h1v2h-2v-2h-1v-1H6v3H4zm2 2v3h4V5z' }))
const Th = (im, t = '', z = 24) => D('position:relative;overflow:hidden;display:flex;align-items:center;justify-content:center;color:var(--ui-text-muted);background:' + (im.alpha ? CHECK : 'repeating-linear-gradient(135deg, var(--ui-border) 0 1px, transparent 1px 12px), ' + TONES[im.tone % 4]) + ';' + t, im.svg ? mark : I('image', z, 'opacity:0.55'))
const BADGE = { unused: ['background:var(--rc-warning-soft);color:var(--ui-warning)', 'unlink'], dup: ['background:var(--rc-info-soft);color:var(--ui-info)', 'copy'], used: ['border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-toned)'], on: ['background:var(--ui-bg-inverted);color:var(--ui-text-inverted)', 'globe'], draft: ['border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-toned)', 'pencil-line'], vars: ['background:var(--rc-info-soft);color:var(--ui-info)', 'layers'] }
const varsBadge = im => im.vc > 0 && Bd(im.vc + (im.vc === 1 ? ' variant' : ' variants'), 'vars')
const Bd = (t, k, sz = 20) => S('height:' + sz + 'px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:500;white-space:nowrap;' + BADGE[k][0], BADGE[k][1] && I(BADGE[k][1], 12), t)
const varsBadge = (im, on) => im.vc > 0 && Bd(im.vc + (im.vc === 1 ? ' variant' : ' variants'), 'vars', 20, on)
const Bd = (t, k, sz = 20, on) => S('height:' + sz + 'px;padding:0 6px;display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:500;white-space:nowrap;' + BADGE[k][0] + (on && k !== 'used' && k !== 'draft' ? ';background:var(--ui-bg);border:1px solid currentColor' : ''), BADGE[k][1] && I(BADGE[k][1], 12), t)
const Cb = (on, z = 20, ind) => S('width:' + z + 'px;height:' + z + 'px;flex-shrink:0;display:flex;align-items:center;justify-content:center;border:1px solid ' + (on || ind ? 'var(--ui-primary)' : 'var(--ui-border-accented)') + ';background:' + (on || ind ? 'var(--ui-primary)' : 'var(--ui-bg)') + ';color:var(--ui-bg)', (on || ind) && I(ind ? 'minus' : 'check', z - 6))
const Alert = (tone, icon, title, text, action, actionEl) => D('display:flex;gap:10px;padding:12px 14px;background:var(--rc-' + tone + '-soft);color:var(--ui-' + tone + ')', S('display:flex;padding-top:2px', I(icon)), D('flex:1;display:flex;flex-direction:column;gap:8px', D('display:flex;flex-direction:column;gap:2px', S('font-weight:600', title), S('color:var(--ui-text-toned)', text)), action && B(action, { icon: 'rotate-cw', h: m ? 44 : 28, fs: 12, z: 14, ...(actionEl || {}) })))
const Empty = (icon, title, text, action, primary, pad = 56) => D('background:var(--ui-bg);border:1px solid var(--ui-border);padding:' + pad + 'px 24px;display:flex;flex-direction:column;align-items:center;gap:12px;text-align:center', S('width:44px;height:44px;display:flex;align-items:center;justify-content:center;background:var(--ui-bg-elevated);color:var(--ui-text-toned)', I(icon, 20)), S('font-weight:600;font-size:15px;color:var(--ui-text-highlighted)', title), S('color:var(--ui-text-muted);max-width:320px', text), action && B(action, { k: primary ? 'primary' : 'outline', icon: primary ? 'upload' : null, px: 16 }))
@@ -74,7 +74,7 @@ class Component extends DCLogic {
Th(im, 'aspect-ratio:4/3;border-bottom:1px solid ' + (on ? 'var(--ui-primary)' : 'var(--ui-border)')),
showCb && S('position:absolute;top:0;left:0;width:' + (m ? 44 : 36) + 'px;height:' + (m ? 44 : 36) + 'px;display:flex;align-items:center;justify-content:center', Cb(on, m ? 22 : 20)),
D('padding:10px 12px;display:flex;flex-direction:column;gap:6px;min-width:0', D('display:flex;flex-direction:column;gap:2px;min-width:0', S('font-weight:500;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), S('font-size:12px;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)') + ';white-space:nowrap;overflow:hidden;text-overflow:ellipsis', im.sz + ' · ' + im.dim)),
D('display:flex;flex-wrap:wrap;gap:4px', im.dup && Bd('Duplicate', 'dup'), usedBadge(im), varsBadge(im))))
D('display:flex;flex-wrap:wrap;gap:4px', im.dup && Bd('Duplicate', 'dup', 20, on), usedBadge(im, on), varsBadge(im, on))))
}
const Grid = () => D('display:grid;grid-template-columns:repeat(' + (m ? 2 : 5) + ',minmax(0,1fr));gap:' + (m ? 10 : 16) + 'px', ...imgs.map(Card))
const Skel = () => D('display:grid;grid-template-columns:repeat(' + (m ? 2 : 5) + ',minmax(0,1fr));gap:' + (m ? 10 : 16) + 'px', ...Array.from({ length: m ? 6 : 10 }, () => D('background:var(--ui-bg);border:1px solid var(--ui-border)', S('display:block;aspect-ratio:4/3;background:var(--ui-bg-accented)'), D('padding:10px 12px;display:flex;flex-direction:column;gap:8px', S('display:block;width:70%;height:13px;background:var(--ui-bg-accented)'), S('display:block;width:45%;height:11px;background:var(--ui-bg-accented)'), S('display:block;width:60px;height:20px;background:var(--ui-bg-accented)')))))
@@ -86,7 +86,7 @@ class Component extends DCLogic {
const on = sel.includes(im.id), hov = s.hover === im.id
return D('display:grid;grid-template-columns:' + cols + ';height:56px;align-items:center;padding:0 12px 0 0;border-top:1px solid var(--ui-border);background:' + (on ? 'var(--rc-sand)' : hov ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)') + ';color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)'),
S('display:flex;justify-content:center', Cb(on, 18)),
D('display:flex;align-items:center;gap:12px;min-width:0;padding-right:12px', Th(im, 'width:40px;height:40px;flex-shrink:0;border:1px solid var(--ui-border)', 16), S('font-weight:500;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), im.dup && Bd('Duplicate', 'dup'), varsBadge(im)),
D('display:flex;align-items:center;gap:12px;min-width:0;padding-right:12px', Th(im, 'width:40px;height:40px;flex-shrink:0;border:1px solid var(--ui-border)', 16), S('font-weight:500;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), im.dup && Bd('Duplicate', 'dup', 20, on), varsBadge(im, on)),
S('', im.dim), S('', im.sz), S('', im.ty), S('', usedLink(im)), S('overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding-right:8px', im.by), S('', im.d))
}))
}
@@ -95,7 +95,7 @@ class Component extends DCLogic {
return D('display:flex;align-items:center;gap:0;min-height:80px;padding:8px 12px 8px 0;border-bottom:1px solid var(--ui-border);background:' + (on ? 'var(--rc-sand)' : 'var(--ui-bg)') + ';color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-muted)'),
S('width:44px;height:44px;flex-shrink:0;display:flex;align-items:center;justify-content:center', Cb(on, 22)),
Th(im, 'width:56px;height:56px;flex-shrink:0;border:1px solid var(--ui-border)', 20),
D('flex:1;min-width:0;display:flex;flex-direction:column;gap:3px;padding-left:12px', S('font-weight:500;font-size:15px;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), S('font-size:12px', im.sz + ' · ' + im.dim + ' · ' + im.ty), D('display:flex;flex-wrap:wrap;gap:4px;align-items:center;font-size:12px', im.dup && Bd('Duplicate', 'dup'), usedBadge(im), varsBadge(im), S('', im.by + ' · ' + im.d))),
D('flex:1;min-width:0;display:flex;flex-direction:column;gap:3px;padding-left:12px', S('font-weight:500;font-size:15px;color:' + (on ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)') + ';overflow:hidden;text-overflow:ellipsis;white-space:nowrap', im.n), S('font-size:12px', im.sz + ' · ' + im.dim + ' · ' + im.ty), D('display:flex;flex-wrap:wrap;gap:4px;align-items:center;font-size:12px', im.dup && Bd('Duplicate', 'dup', 20, on), usedBadge(im, on), varsBadge(im, on), S('', im.by + ' · ' + im.d))),
I('chevron-right', 16, 'margin-left:8px'))
}))
@@ -229,6 +229,8 @@ class Component extends DCLogic {
{ label: 'Editor', title: 'Editor', route: '/article/:id/edit', api: 'GET /v1/article/{id} · PATCH /v1/article/{id} · PATCH …/publish|unpublish · POST …/image · DELETE', note: 'On mobile, Save and Publish sit in a sticky bottom bar and the details panel opens as a slideover. A session expiry mid-edit keeps the draft in sessionStorage and goes to /login?redirect=/article/12/edit. After logging in, the editor restores the draft and shows "Unsaved changes".', states: [
E({ loading: true }, { name: 'Loading', trigger: 'GET in flight. Skeletons for the title, content and details.' }),
E({}, { name: 'Saved', trigger: 'Just opened or just saved. Save is disabled.' }),
E({ title: 'Comment nous chiffrons une rénovation de salle de bain', lang: { name: 'French', code: 'FR' } }, { name: 'Multilingual site: French draft', trigger: 'Sites with more than one language show the language control after the status badge. Publish, Unpublish and Delete name the language. Single-language sites are unchanged. Menu, source reference and review states: Localization board.' }),
E({ status: 'online', title: 'Comment nous chiffrons une rénovation de salle de bain', lang: { name: 'French', code: 'FR', fresh: 'Needs review' } }, { name: 'Multilingual site: published, needs review', trigger: 'Publication (Online) and freshness (outlined Needs review) are separate badges.' }),
E({ save: 'dirty' }, { name: 'Unsaved changes', trigger: 'Any edit. Publish becomes "Save and publish". Leaving asks for confirmation.' }),
E({ save: 'saving' }, { name: 'Saving', trigger: 'Save, or Ctrl/⌘ S.' }),
E({ save: 'error' }, { name: 'Save failed', trigger: '500 or network failure. The changes are kept and Retry resends them.' }),
@@ -20,7 +20,7 @@
<div style="display:flex;align-items:flex-end;gap:32px;max-width:1400px">
<div style="flex:1;display:flex;flex-direction:column;gap:10px">
<h1 style="margin:0;font:600 36px/44px 'IBM Plex Sans',sans-serif;letter-spacing:-0.02em;color:var(--ui-text-highlighted)">Screens: article localization</h1>
<p style="margin:0;max-width:880px;font-size:16px;line-height:26px;color:var(--ui-text-toned);text-wrap:pretty">One story, several language versions. Proposed feature: every API change is labelled as proposed. 1440 px at 50%, 1024 px at 50% for layout-dependent states, 390 px at 75%. The theme switch shows every state in dark mode. Each state shows its ID and fixture. Groups L6 and L9 are release 2. Repository code was unavailable, so existing behaviour comes from the design project only.</p>
<p style="margin:0;max-width:880px;font-size:16px;line-height:26px;color:var(--ui-text-toned);text-wrap:pretty">One story, several language versions. Proposed feature: every API change is labelled as proposed. 1440 px at 50%, 1024 px at 50% for layout-dependent states, 390 px at 75%. The theme switch shows every state in dark mode. Each state shows its ID and fixture. 183 states in 16 groups. L7s is the pixel-gate reference for a single-language article list. Groups L6 and L9 are release 2. Repository code was unavailable, so existing behaviour comes from the design project only.</p>
</div>
<div role="radiogroup" aria-label="Theme" style="display:flex;border:1px solid var(--ui-border-accented);background:var(--ui-bg)">
<sc-for list="{{ modes }}" as="m" hint-placeholder-count="3">
+432 -32
View File
@@ -1,3 +1,4 @@
window.LOC = {
"groups": [
{
@@ -357,6 +358,16 @@ window.LOC = {
"loc.l1b.viewOnly",
"Journal",
"loc.l1b.viewOnly"
],
[
"Invalid host",
"Pattern with an empty host label",
"pu",
"invalidHost",
"",
"loc.l1b.invalidHost",
"Journal",
"loc.l1b.invalidHost"
]
]
},
@@ -470,7 +481,7 @@ window.LOC = {
],
[
"Translation of an article",
"Article picker + language",
"Same fields as Add translation: article, language, source version, title, starting content",
"na",
"translation",
"",
@@ -510,7 +521,7 @@ window.LOC = {
],
[
"Start empty",
"Title only",
"Title only; Source version reads No source, Freshness will show No source",
"at",
"empty",
"",
@@ -560,7 +571,7 @@ window.LOC = {
],
[
"Source deleted",
"Deleted while the dialog was open",
"Deleted while the dialog was open: same neutral message as unreadable, no distinction shown",
"at",
"gone",
"",
@@ -570,7 +581,7 @@ window.LOC = {
],
[
"Source no longer readable",
"Access lost between opening and submitting",
"Access lost between opening and submitting: same neutral message as deleted",
"at",
"unreadable",
"",
@@ -857,6 +868,16 @@ window.LOC = {
"loc.l5.failed",
"Journal",
"loc.l5.failed"
],
[
"Language menu with a disabled language",
"Spanish is disabled: Disabled tag, version stays readable",
"ed",
"menuDisabled",
"",
"loc.l5.menuDisabled",
"Journal",
"loc.l5.menuDisabled"
]
]
},
@@ -1260,6 +1281,86 @@ window.LOC = {
]
]
},
{
"id": "L7s",
"title": "Article list, single-language site",
"route": "/articles?site=archive&q&status&page&size",
"api": "GET /v1/article/?page=&size=&q=&status=&site= (existing; total in X-Total-Count)",
"note": "Replaces the old Articles board frames as the pixel-gate reference for the list. A site with one language has no language filter, no coverage column, no Needs translation or Needs review tabs, and its menu labels name no language. The pager is AppPaginator: summary \"Showing 1–5 of 5\", rows-per-page select, first, previous, page numbers, next and last (390 px: summary and select, then Previous, \"Page N of M\", Next). Hidden controls when everything fits on one page; the summary stays.",
"release": 1,
"states": [
[
"Default",
"Archive: 5 articles on one page, no language filter, coverage column or tabs",
"li",
"single",
"",
"loc.l7s.default",
"Archive",
"loc.l7s.default"
],
[
"Draft filter",
"Status: Draft",
"li",
"singleDraft",
"",
"loc.l7s.draft",
"Archive",
"loc.l7s.draft"
],
[
"No results",
"Search \"zinc\"",
"li",
"singleNoResults",
"",
"loc.l7s.noResults",
"Archive",
"loc.l7s.noResults"
],
[
"Empty",
"Gallery: no articles",
"li",
"singleEmpty",
"",
"loc.l7s.empty",
"Gallery",
"loc.l7s.empty"
],
[
"Loading",
"Counts shown as –",
"li",
"singleLoading",
"",
"loc.l7s.loading",
"Archive",
"loc.l7s.loading"
],
[
"Failed",
"Filters kept",
"li",
"singleFail",
"",
"loc.l7s.fail",
"Archive",
"loc.l7s.fail"
],
[
"Row menu",
"Open, Unpublish, Copy API URL, Delete article: no language named",
"li",
"singleMenu",
"",
"loc.l7s.menu",
"Archive",
"loc.l7s.menu"
]
]
},
{
"id": "L8",
"title": "Publishing and deletion",
@@ -1300,7 +1401,7 @@ window.LOC = {
],
[
"Save failed, nothing published",
"Toast; edits kept and retryable",
"Error banner above the editor and an error toast; Save failed badge, primary Try saving again, edits kept, nothing published",
"pd",
"saveFailPublish",
"",
@@ -1340,7 +1441,7 @@ window.LOC = {
],
[
"Delete a source version",
"Warns about review tracking",
"Names the readable versions that track the source; warns review tracking pauses",
"pd",
"deleteSource",
"",
@@ -1380,7 +1481,7 @@ window.LOC = {
],
[
"Published",
"Toast",
"Toast \"French published\" and the online banner; badge Published, status Saved",
"pd",
"done",
"",
@@ -1397,6 +1498,36 @@ window.LOC = {
"loc.l8.fail",
"Journal",
"loc.l8.fail"
],
[
"French version deleted",
"Toast; editor opens English",
"pd",
"deleteDone",
"",
"loc.l8.deleteDone",
"Journal",
"loc.l8.deleteDone"
],
[
"Delete article failed",
"Dialog error; nothing deleted",
"pd",
"deleteStoryFail",
"",
"loc.l8.deleteStoryFail",
"Journal",
"loc.l8.deleteStoryFail"
],
[
"Article deleted",
"Toast on the Articles list",
"li",
"deleteStoryDone",
"",
"loc.l8.deleteStoryDone",
"Journal",
"loc.l8.deleteStoryDone"
]
]
},
@@ -1530,7 +1661,7 @@ window.LOC = {
"states": [
[
"Search and sharing: French",
"Localized slug, canonical, alternates, fallback labels, Edit slug",
"Search and sharing slide-over on the editor: localized slug, canonical, alternates, fallback labels, Edit slug",
"seo",
"",
"",
@@ -1540,7 +1671,7 @@ window.LOC = {
],
[
"Edit slug",
"Published version: warning about old links",
"Edit slug dialog over the Search and sharing slide-over: published version warns about old links",
"seo",
"editSlug",
"",
@@ -1550,7 +1681,7 @@ window.LOC = {
],
[
"Public URL not configured",
"No pattern, no fabricated URL",
"Search and sharing slide-over: no pattern, no fabricated URL",
"seo",
"unset",
"",
@@ -1560,7 +1691,7 @@ window.LOC = {
],
[
"Social locale not set",
"fr has no region and none is set: og:locale omitted",
"Search and sharing slide-over: fr has no region and none is set, og:locale omitted",
"seo",
"noSocial",
"",
@@ -1570,7 +1701,7 @@ window.LOC = {
],
[
"Integration: languages",
"Exact lookup, resolve by article group, errors",
"Integration page: exact lookup, resolve by article group, errors",
"api",
"",
"",
@@ -1590,7 +1721,7 @@ window.LOC = {
"states": [
[
"Session expired",
"Draft kept locally",
"Draft kept in this browser; editor behind shows the unsaved changes",
"pm",
"session",
"",
@@ -1610,7 +1741,7 @@ window.LOC = {
],
[
"Concurrent update",
"Four choices, local work kept",
"Four choices, local work kept; Save over theirs outlined in error colour with a warning icon",
"pm",
"conflict",
"",
@@ -1730,7 +1861,7 @@ window.LOC = {
],
[
"Address conflict",
"Names the conflicting readable version only",
"Names the conflicting readable version and its address; unreadable ones are never named",
"pm",
"moveSlug",
"",
@@ -1747,6 +1878,16 @@ window.LOC = {
"loc.l11b.movePerm",
"Journal",
"loc.l11b.movePerm"
],
[
"Move failed",
"Server error, nothing moved",
"pm",
"moveFail",
"",
"loc.l11b.moveFail",
"Journal",
"loc.l11b.moveFail"
]
]
},
@@ -1790,7 +1931,7 @@ window.LOC = {
],
[
"Media usage",
"Shared image counted once, versions listed",
"Media page with the image detail panel: shared image counted once, versions listed, \"Used in 2\"",
"sf",
"media",
"tri",
@@ -1799,10 +1940,20 @@ window.LOC = {
"loc.l12.media"
],
[
"Article menu labels",
"Publish and Delete name the language",
"Media usage: used elsewhere only",
"Image used only as an author avatar: badge \"Used in 1\", \"Articles\" with \"No article shows it.\", \"Used elsewhere\" with Author avatar linking to /authors",
"sf",
"menu",
"mediaElsewhere",
"",
"loc.l12.mediaElsewhere",
"Journal",
"loc.l12.mediaElsewhere"
],
[
"Article menu labels",
"Articles list, French selected: row menu names the language (Publish French, Delete French version). Bottom sheet at 390",
"li",
"menuLang",
"",
"loc.l12.menu",
"Journal",
@@ -1810,8 +1961,8 @@ window.LOC = {
],
[
"API URL copy",
"Copies the version URL with ?lang=",
"sf",
"Articles list: toast after Copy API URL with the ?lang= address. Bottom toast at 390",
"li",
"apiCopy",
"",
"loc.l12.apiCopy",
@@ -1820,9 +1971,9 @@ window.LOC = {
],
[
"SEO preview",
"Localized address and canonical",
"sf",
"seoPreview",
"Search and sharing slide-over: localized address and canonical",
"seo",
"preview",
"",
"loc.l12.seoPreview",
"Journal",
@@ -1830,8 +1981,8 @@ window.LOC = {
],
[
"Site text fallback",
"Used only when its language matches the article",
"sf",
"Search and sharing slide-over: site text used only in its own language",
"seo",
"siteFallback",
"",
"loc.l12.siteFallback",
@@ -2133,6 +2284,38 @@ window.LOC = {
"All localization copy",
"None",
"Initial"
],
[
"Languages in the sidebar",
"No sidebar item. Languages is reached only through Settings → Sites → [site] → Languages, which routes.md lists as the only languages route",
"One route, one entry point, per-site scope",
"Sidebar unchanged",
"None",
"Initial"
],
[
"Public URL preview",
"The preview uses the most recently updated article that has a version in the site default language, in that language. A site without articles previews a sample address",
"Predictable and never fabricates a language",
"Public URL page",
"None",
"Initial"
],
[
"Deleting an article",
"Dialog names the readable versions and states that the article and all of its translations are deleted. Failure copy: \"Couldn't delete the article\" with \"Nothing was deleted. Every version is still there. Try again in a moment.\"",
"Delete is atomic, so there is no partial delete",
"Delete dialog",
"None",
"Initial"
],
[
"Destructive cue in the conflict dialog",
"Keep my changes is the only filled button. Save over theirs is outlined in --ui-error with a triangle-alert icon, the same size as the other secondary buttons",
"Marks the destructive choice without changing the equal-weight rule",
"Conflict dialog",
"None",
"Initial"
]
]
},
@@ -2398,7 +2581,7 @@ window.LOC = {
"Public URL pattern",
"Proposed",
"GET/PUT /v1/site/{key} { public_url_pattern }. Placeholders {lang}, {slug}. Owner only",
"422 unknown_placeholder · invalid_scheme · not_encoded · missing_slug · 403"
"422 unknown_placeholder · invalid_scheme · invalid_host · not_encoded · missing_slug · 403"
],
[
"Social locale",
@@ -2638,7 +2821,7 @@ window.LOC = {
],
[
"Source unavailable",
"Deleted: \"The source was deleted.\"",
"Deleted or unreadable at submit: the same neutral 404, shown as \"The source isn't available to you.\" The review banner (L9) says \"Source deleted\" only for a source the user already knew about",
"\"The source isn't available to you.\""
],
[
@@ -2877,7 +3060,7 @@ window.LOC = {
],
[
"Conflict",
"\"Someone else saved this version at <time>.\" Choices: Keep my changes (filled; nothing sent), Download my version, Reload theirs (confirm: \"This discards your unsaved changes.\"), Save over theirs (sends the version fetched now; a newer save gives a new conflict)"
"\"Someone else saved this version at <time>.\" Choices: Keep my changes (filled; nothing sent), Download my version, Reload theirs (confirm: \"This discards your unsaved changes.\"), Save over theirs (sends the version fetched now; a newer save gives a new conflict) Save over theirs is outlined in the error colour with a warning icon."
],
[
"Overwrite scope",
@@ -3369,7 +3552,7 @@ window.LOC = {
[
"Valid",
"https://journal.example.com/{lang}/{slug}",
"Preview: https://journal.example.com/fr/comment-nous-chiffrons-une-renovation"
"Preview: https://journal.example.com/en/how-we-price-a-bathroom-renovation"
],
[
"Unknown placeholder",
@@ -3391,6 +3574,11 @@ window.LOC = {
"https://journal.example.com/{lang}",
"Add {slug} so each article has its own address."
],
[
"Invalid host",
"https://journal..example.com/{lang}/{slug}",
"Enter a valid host name, such as journal.example.com."
],
[
"Unset",
"",
@@ -3475,7 +3663,219 @@ window.LOC = {
"Drawn in full so the checker has frames"
]
]
},
{
"title": "Copy decisions (pass 3)",
"cols": [
"Item",
"Final copy or rule"
],
"rows": [
[
"Editor banner: save failed",
"Couldn't save French. Your changes are kept here. Nothing was published. Try saving again."
],
[
"Editor banner: publish failed",
"Saved as a draft. Publishing failed. Try publishing again. (detail: Your edits are saved. Only the publishing step failed.)"
],
[
"Editor banner: online",
"This version is online. Saving updates the live page right away."
],
[
"Row menu, all languages",
"Open <language>, Open another language…, Add translation…, Unpublish <language>, Copy API URL, Delete <language> version"
],
[
"Row menu, language selected",
"Open <language>, Open another language…, Publish or Unpublish <language>, Copy API URL, Delete <language> version"
],
[
"Disabled-language version in the menu",
"Row shows the Disabled tag. Footer: \"<Language> is disabled on this site. Its version stays readable and editable. It can't be published until an owner re-enables <Language>.\""
],
[
"Setup done",
"Languages assigned. N articles now have a language. Nothing was translated, and no articles were grouped."
],
[
"Languages list on 390 px",
"The version count line (\"4 versions · 2 published\") is shown, on its own line under the language name"
],
[
"Single-language card on 390 px",
"\"Social locale\" and \"Derived: pt_BR\" sit on their own line below the language"
],
[
"Partial or failed delete",
"Couldn't delete the article. Nothing was deleted. Every version is still there. Try again in a moment."
],
[
"Version deleted toast",
"French version deleted. \"<title>\" was deleted. English and German weren't changed."
],
[
"Article deleted toast",
"Article deleted. The article and all of its translations were deleted."
],
[
"Move failed",
"Couldn't move the article. Nothing was moved. All 4 versions are still on Journal. Try again."
],
[
"Second conflict",
"Their version changed again. Someone else saved this version again at <time>, before your save went through. Nothing was overwritten. Your changes are still here."
],
[
"New article, language help",
"Default language for <site>."
],
[
"Invalid host",
"Enter a valid host name, such as journal.example.com."
],
[
"Toast icon box",
"28 × 28 px, 16 px icon, tone colour on its -soft ground (--rc-success-soft, --rc-info-soft, --rc-warning-soft, --rc-error-soft). Body and title as in the Toast table"
],
[
"Activity: site.language.add",
"Added <language> to <site>"
],
[
"Activity: site.language.enable",
"Re-enabled <language> on <site>"
],
[
"Activity: site.language.disable",
"Disabled <language> on <site>"
],
[
"Activity: site.language.remove",
"Removed <language> from <site>"
],
[
"Activity: site.language.default",
"Made <language> the default language of <site>"
],
[
"Activity: site.language.social_locale",
"Set the social locale for <language> on <site> to <value>"
],
[
"Activity: site.public_url.update",
"Changed the public URL pattern of <site>"
],
[
"Activity: site.language.setup",
"Assigned languages to N articles on <sites>"
],
[
"Activity: article.move",
"Moved \"<title>\" from <site> to <site>"
]
]
}
]
],
"changes": {
"added": [
"L1|sl|broader",
"L1|sl|defaultFail",
"L1|sl|stale",
"L1|sl|reenableDone",
"L1|sl|reenableFail",
"L1|sl|removeFail",
"L1|sl|social",
"L1|sl|socialEdit",
"L1|sl|socialInvalid",
"L1b|pu|set",
"L1b|pu|editing",
"L1b|pu|unknown",
"L1b|pu|scheme",
"L1b|pu|encoded",
"L1b|pu|noSlug",
"L1b|pu|saved",
"L1b|pu|unset",
"L1b|pu|viewOnly",
"L2|su|noLocale",
"L2|su|inProgress",
"L4|at|unreadable",
"L4|at|langOff",
"L5|ed|cleanOnline",
"L5|ed|pubFail",
"L5|ed|ovf",
"L5|ed|titleEdit",
"L5|ed|titleUnsaved",
"L5|ed|titleShort",
"L5|ed|titleLong",
"L5b|perm|ro",
"L5b|perm|roMenu",
"L5b|perm|p2",
"L5b|perm|p3",
"L5b|perm|p4",
"L5b|perm|p5",
"L5b|perm|p6",
"L5b|perm|noAdd",
"L5b|perm|withAdd",
"L6|src|arRef",
"L7|li|page2",
"L7|li|review",
"L7|li|author",
"L7|li|langFilter",
"L7|li|rowMenu",
"L8|pd|publishDisabled",
"L8|pd|unpublishDirty",
"L8|pd|deleteStoryOk",
"L9|li|frReview",
"L10|seo|editSlug",
"L11|pm|restored",
"L11|pm|conflictRestored",
"L11|pm|reloadConfirm",
"L11|pm|conflict2",
"L11|pm|download",
"L11b|pm|moveSelect",
"L11b|pm|moveSelectHidden",
"L11b|pm|moveConfirm",
"L11b|pm|moveBusy",
"L11b|pm|moveDone",
"L11b|pm|move",
"L11b|pm|moveSlug",
"L11b|pm|movePerm",
"L12|sf|home",
"L12|sf|homeFr",
"L12|sf|activity",
"L12|sf|media",
"L12|sf|menu",
"L12|sf|apiCopy",
"L12|sf|seoPreview",
"L12|sf|siteFallback",
"L5|ed|menuDisabled",
"L1b|pu|invalidHost",
"L8|pd|deleteDone",
"L8|pd|deleteStoryFail",
"L8|li|deleteStoryDone",
"L11b|pm|moveFail",
"L7s|li|single",
"L7s|li|singleDraft",
"L7s|li|singleNoResults",
"L7s|li|singleEmpty",
"L7s|li|singleLoading",
"L7s|li|singleFail",
"L7s|li|singleMenu"
],
"removed": [
"L5b|ed|ro (Read-only version)",
"L5b|ed|roMenu (Read-only: menu)",
"L5b|ed|p2 (Edit, no publish)",
"L5b|ed|p3 (Publish, no edit)",
"L5b|ed|p4 (Sibling readable, not editable)",
"L5b|ed|p5 (Sibling not readable)",
"L5b|ed|p6 (Can create, not modify all)",
"L11|pm|move (Move: language missing)",
"L11|pm|moveSlug (Move: address conflict)",
"L11|pm|movePerm (Move: no permission)"
]
}
}
window.LOC.changes = {"added":["L1|sl|broader","L1|sl|defaultFail","L1|sl|stale","L1|sl|reenableDone","L1|sl|reenableFail","L1|sl|removeFail","L1|sl|social","L1|sl|socialEdit","L1|sl|socialInvalid","L1b|pu|set","L1b|pu|editing","L1b|pu|unknown","L1b|pu|scheme","L1b|pu|encoded","L1b|pu|noSlug","L1b|pu|saved","L1b|pu|unset","L1b|pu|viewOnly","L2|su|noLocale","L2|su|inProgress","L4|at|unreadable","L4|at|langOff","L5|ed|cleanOnline","L5|ed|pubFail","L5|ed|ovf","L5|ed|titleEdit","L5|ed|titleUnsaved","L5|ed|titleShort","L5|ed|titleLong","L5b|perm|ro","L5b|perm|roMenu","L5b|perm|p2","L5b|perm|p3","L5b|perm|p4","L5b|perm|p5","L5b|perm|p6","L5b|perm|noAdd","L5b|perm|withAdd","L6|src|arRef","L7|li|page2","L7|li|review","L7|li|author","L7|li|langFilter","L7|li|rowMenu","L8|pd|publishDisabled","L8|pd|unpublishDirty","L8|pd|deleteStoryOk","L9|li|frReview","L10|seo|editSlug","L11|pm|restored","L11|pm|conflictRestored","L11|pm|reloadConfirm","L11|pm|conflict2","L11|pm|download","L11b|pm|moveSelect","L11b|pm|moveSelectHidden","L11b|pm|moveConfirm","L11b|pm|moveBusy","L11b|pm|moveDone","L11b|pm|move","L11b|pm|moveSlug","L11b|pm|movePerm","L12|sf|home","L12|sf|homeFr","L12|sf|activity","L12|sf|media","L12|sf|menu","L12|sf|apiCopy","L12|sf|seoPreview","L12|sf|siteFallback"],"removed":["L5b|ed|ro (Read-only version)","L5b|ed|roMenu (Read-only: menu)","L5b|ed|p2 (Edit, no publish)","L5b|ed|p3 (Publish, no edit)","L5b|ed|p4 (Sibling readable, not editable)","L5b|ed|p5 (Sibling not readable)","L5b|ed|p6 (Can create, not modify all)","L11|pm|move (Move: language missing)","L11|pm|moveSlug (Move: address conflict)","L11|pm|movePerm (Move: no permission)"]}
window.LOC.changes = {"added":["L1|sl|broader","L1|sl|defaultFail","L1|sl|stale","L1|sl|reenableDone","L1|sl|reenableFail","L1|sl|removeFail","L1|sl|social","L1|sl|socialEdit","L1|sl|socialInvalid","L1b|pu|set","L1b|pu|editing","L1b|pu|unknown","L1b|pu|scheme","L1b|pu|encoded","L1b|pu|noSlug","L1b|pu|saved","L1b|pu|unset","L1b|pu|viewOnly","L2|su|noLocale","L2|su|inProgress","L4|at|unreadable","L4|at|langOff","L5|ed|cleanOnline","L5|ed|pubFail","L5|ed|ovf","L5|ed|titleEdit","L5|ed|titleUnsaved","L5|ed|titleShort","L5|ed|titleLong","L5b|perm|ro","L5b|perm|roMenu","L5b|perm|p2","L5b|perm|p3","L5b|perm|p4","L5b|perm|p5","L5b|perm|p6","L5b|perm|noAdd","L5b|perm|withAdd","L6|src|arRef","L7|li|page2","L7|li|review","L7|li|author","L7|li|langFilter","L7|li|rowMenu","L8|pd|publishDisabled","L8|pd|unpublishDirty","L8|pd|deleteStoryOk","L9|li|frReview","L10|seo|editSlug","L11|pm|restored","L11|pm|conflictRestored","L11|pm|reloadConfirm","L11|pm|conflict2","L11|pm|download","L11b|pm|moveSelect","L11b|pm|moveSelectHidden","L11b|pm|moveConfirm","L11b|pm|moveBusy","L11b|pm|moveDone","L11b|pm|move","L11b|pm|moveSlug","L11b|pm|movePerm","L12|sf|home","L12|sf|homeFr","L12|sf|activity","L12|sf|media","L12|sf|menu","L12|sf|apiCopy","L12|sf|seoPreview","L12|sf|siteFallback","L5|ed|menuDisabled","L1b|pu|invalidHost","L8|pd|deleteDone","L8|pd|deleteStoryFail","L8|li|deleteStoryDone","L11b|pm|moveFail","L7s|li|single","L7s|li|singleDraft","L7s|li|singleNoResults","L7s|li|singleEmpty","L7s|li|singleLoading","L7s|li|singleFail","L7s|li|singleMenu","L12|sf|mediaElsewhere"],"removed":["L5b|ed|ro (Read-only version)","L5b|ed|roMenu (Read-only: menu)","L5b|ed|p2 (Edit, no publish)","L5b|ed|p3 (Publish, no edit)","L5b|ed|p4 (Sibling readable, not editable)","L5b|ed|p5 (Sibling not readable)","L5b|ed|p6 (Can create, not modify all)","L11|pm|move (Move: language missing)","L11|pm|moveSlug (Move: address conflict)","L11|pm|movePerm (Move: no permission)"]}