chore: gdpr features design groundwork

This commit is contained in:
Florian Sylvain committed 2026-10-29 23:24:15 +01:00
1 parent c6d49db95e
commit bb191c389a
16 files changed
+798 -6

No files matched your search

+131
View File
@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<script src="./account-copy.js"></script>
</helmet>
<div style="display:contents;font-family:'IBM Plex Sans',sans-serif;font-size:13px;line-height:20px;color:var(--ui-text)">
<sc-if value="{{ pEmail }}" hint-placeholder-val="{{ false }}">
<div style="display:flex;flex-direction:column;gap:6px;min-width:0">
<div style="display:flex;align-items:center;justify-content:space-between;gap:8px"><span style="font-weight:500;color:var(--ui-text-highlighted)">{{ c.emailLabel }}</span><sc-if value="{{ showChange }}"><span data-act="ac-change" role="button" style="min-height:{{ linkH }};display:flex;align-items:center;font-weight:500;color:var(--ui-text-highlighted);text-decoration:underline;text-underline-offset:3px">{{ c.changeEmail }}</span></sc-if></div>
<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">{{ old }}</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>
</sc-if>
<sc-if value="{{ pPanel }}" hint-placeholder-val="{{ false }}">
<sc-if value="{{ form }}">
<div style="display:flex;flex-direction:column;gap:14px;padding:{{ panelPad }};background:var(--ui-bg-muted);border:1px solid var(--ui-border)">
<sc-if value="{{ form.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);text-wrap:pretty">{{ form.notice }}</span></div></sc-if>
<sc-for list="{{ form.fields }}" as="f" hint-placeholder-count="2">
<div style="display:flex;flex-direction:column;gap:6px">
<label style="font-weight:500;color:var(--ui-text-highlighted)">{{ f.label }}</label>
<div style="height:{{ inH }};padding:0 10px;display:flex;align-items:center;border:1px solid {{ f.bd }};box-shadow:{{ f.sh }};background:{{ f.bg }};color:{{ f.fg }};font-size:{{ inF }};overflow:hidden;white-space:nowrap"><span style="overflow:hidden;text-overflow:ellipsis;letter-spacing:{{ f.ls }}">{{ f.shown }}</span><sc-if value="{{ f.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>
<sc-if value="{{ f.err }}"><span style="display:flex;gap:6px;align-items:center;color:var(--ui-error);font-size:12px;line-height:18px"><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>
</div>
</sc-for>
<span style="color:var(--ui-text-muted);font-size:12px;line-height:18px;text-wrap:pretty">{{ c.formHelp }}</span>
<div style="display:flex;flex-direction:{{ actDir }};justify-content:flex-end;gap:8px">
<span data-act="ac-form-cancel" style="min-height:{{ btnH }};padding:0 12px;display:flex;align-items:center;justify-content:center;text-align:center;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:{{ form.cancelFg }};font-weight:500">{{ c.cancel }}</span>
<span data-act="ac-form-send" style="min-height:{{ btnH }};padding:6px 12px;display:flex;align-items:center;justify-content:center;gap:6px;text-align:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;opacity:{{ form.op }}"><sc-if value="{{ form.sending }}"><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>{{ form.sendLabel }}</span>
</div>
</div>
</sc-if>
<sc-if value="{{ pend }}">
<div role="status" style="display:flex;flex-direction:column;gap:12px;padding:12px 14px;background:var(--rc-info-soft);color:var(--ui-info)">
<div style="display:flex;gap:10px"><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/mail.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/mail.svg') center / contain no-repeat"></span></span><div style="display:flex;flex-direction:column;gap:2px;min-width:0"><span style="font-weight:600">{{ c.pendTitle }}</span><span style="color:var(--ui-text-toned);text-wrap:pretty;overflow-wrap:anywhere">{{ pend.text }}</span></div></div>
<div style="display:flex;flex-direction:{{ colDir }};gap:8px">
<span data-act="ac-resend" style="min-height:{{ btnH }};padding:0 12px;display:flex;align-items:center;justify-content:center;gap:6px;text-align:center;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500;opacity:{{ pend.op }}"><sc-if value="{{ pend.sending }}"><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>{{ pend.resendLabel }}</span>
<span data-act="ac-cancel-change" style="min-height:{{ btnH }};padding:0 12px;display:flex;align-items:center;justify-content:center;text-align:center;border:1px solid var(--ui-border-accented);background:var(--ui-bg);color:var(--ui-text-highlighted);font-weight:500">{{ c.cancelChange }}</span>
</div>
<sc-if value="{{ pend.note }}"><span style="display:flex;gap:6px;align-items:center;color:var(--ui-error);font-size:12px;line-height:18px"><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>{{ pend.note }}</span></sc-if>
</div>
</sc-if>
</sc-if>
<sc-if value="{{ pCards }}" hint-placeholder-val="{{ false }}">
<div style="display:flex;flex-direction:column;gap:16px;flex-shrink:0">
<div style="background:var(--ui-bg);border:1px solid var(--ui-border);padding:16px;flex-shrink:0;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)">{{ c.dataTitle }}</span><span style="color:var(--ui-text-muted);text-wrap:pretty">{{ c.dataDesc }}</span></div>
<div style="display:grid;grid-template-columns:{{ itemCols }};gap:12px 16px">
<sc-for list="{{ items }}" as="it" hint-placeholder-count="4">
<div style="display:flex;gap:10px;min-width:0"><span style="display:flex;padding-top:2px;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/{{ it.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ it.icon }}.svg') center / contain no-repeat"></span></span><div style="display:flex;flex-direction:column;min-width:0"><span style="font-weight:500;color:var(--ui-text-highlighted)">{{ it.title }}</span><span style="color:var(--ui-text-muted);text-wrap:pretty">{{ it.text }}</span></div></div>
</sc-for>
</div>
<sc-if value="{{ data.notice }}"><div role="status" style="display:flex;gap:10px;padding:12px 14px;background:{{ data.notice.bg }};color:{{ data.notice.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/{{ data.notice.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ data.notice.icon }}.svg') center / contain no-repeat"></span></span><div style="display:flex;flex-direction:column;gap:2px"><span style="font-weight:600">{{ data.notice.title }}</span><span style="color:var(--ui-text-toned);text-wrap:pretty">{{ data.notice.text }}</span></div></div></sc-if>
<div style="display:flex;flex-direction:{{ colDir }};align-items:{{ rowAlign }};gap:12px">
<span data-act="ac-export" style="min-height:{{ btnH }};padding:0 14px;display:flex;align-items:center;justify-content:center;gap:6px;text-align:center;background:var(--ui-primary);color:var(--ui-bg);font-weight:500;opacity:{{ data.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/{{ data.icon }}.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/{{ data.icon }}.svg') center / contain no-repeat"></span>{{ data.label }}</span>
<span style="flex:1;min-width:0;color:var(--ui-text-muted);font-size:12px;line-height:18px;text-wrap:pretty">{{ data.hint }}</span>
</div>
</div>
<div style="background:var(--ui-bg);border:1px solid var(--ui-error);display:flex;flex-direction:column;flex-shrink:0">
<div style="padding:16px;display:flex;flex-direction:{{ colDir }};align-items:{{ rowAlign }};justify-content:space-between;gap:14px">
<div style="flex:1;display:flex;flex-direction:column;gap:2px;min-width:0"><span style="display:flex;align-items:center;gap:8px;font-weight:600;font-size:14px;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/triangle-alert.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/triangle-alert.svg') center / contain no-repeat"></span>{{ c.dzTitle }}</span><span style="color:var(--ui-text-muted);text-wrap:pretty">{{ c.dzDesc }}</span></div>
<span data-act="ac-delete" style="flex-shrink:0;min-height:{{ btnH }};padding:0 14px;display:flex;align-items:center;justify-content:center;gap:6px;text-align:center;background:var(--ui-error);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/trash-2.svg') center / contain no-repeat;mask:url('https://api.iconify.design/lucide/trash-2.svg') center / contain no-repeat"></span>{{ c.dzBtn }}</span>
</div>
</div>
</div>
</sc-if>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script>
class Component extends DCLogic {
componentDidMount() {
if (!window.ACCOUNT_COPY) this.t = setInterval(() => { if (window.ACCOUNT_COPY) { clearInterval(this.t); this.forceUpdate() } }, 100)
}
componentWillUnmount() { clearInterval(this.t) }
renderVals() {
const p = this.props, m = !!p.mobile, part = p.part || 'cards', ac = p.ac || {}
const C = p.c || (window.ACCOUNT_COPY && window.ACCOUNT_COPY.en) || {}
const old = ac.old || 'marta@renew.example', nw = ac.new || 'marta.alves@atelier-renew.example'
const fmt = s => (s || '').split('{new}').join(nw).split('{old}').join(old)
const tone = t => ({ bg: 'var(--rc-' + t + '-soft)', fg: 'var(--ui-' + t + ')' })
const fld = (label, val, ph, o = {}) => ({
label, shown: val || ph || '', caret: !!o.focus && !o.dis,
fg: o.dis ? 'var(--ui-text-dimmed)' : val ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)',
bd: o.err ? 'var(--ui-error)' : o.focus ? 'var(--ui-primary)' : 'var(--ui-border-accented)',
sh: o.err ? 'inset 0 0 0 1px var(--ui-error)' : o.focus ? 'inset 0 0 0 1px var(--ui-primary)' : 'none',
bg: o.dis ? 'var(--ui-bg-elevated)' : 'var(--ui-bg)', err: o.err || '', ls: o.mask && val ? '0.12em' : 'normal'
})
const F = ac.form
const form = F ? {
fields: [
fld(C.newEmail, F.email, C.newEmailPh, { err: F.errEmail, focus: F.focus === 'email', dis: F.sending }),
fld(C.password, F.pw ? '•'.repeat(F.pw.length) : '', '', { err: F.errPw, focus: F.focus === 'pw', dis: F.sending, mask: true })
],
notice: F.notice || '', sending: !!F.sending, sendLabel: F.sending ? C.sending : C.send,
op: F.disabled ? '0.4' : '1', cancelFg: F.sending ? 'var(--ui-text-dimmed)' : 'var(--ui-text-highlighted)'
} : null
const P = ac.pending
const pend = P ? {
text: fmt(C.pendText), sending: P.resend === 'sending',
resendLabel: P.resend === 'sending' ? C.sending : P.resend === 'rate' ? C.resendRate : C.resend,
op: P.resend === 'rate' ? '0.4' : '1',
note: P.resend === 'rate' ? C.rate : P.note || ''
} : null
const D = ac.data || {}, async_ = D.mode === 'async', ds = D.state || 'idle'
let data = { label: async_ ? C.request : C.download, icon: async_ ? 'mail' : 'download', op: '1', hint: async_ ? fmt(C.asyncHint) : C.dlHint, notice: null }
if (!async_ && ds === 'preparing') data = { ...data, label: C.preparing, icon: 'loader-circle', hint: '' }
if (!async_ && ds === 'rate') data = { ...data, label: C.exRateBtn, op: '0.4', icon: 'download', hint: '', notice: { ...tone('info'), icon: 'timer', title: C.exRateT, text: C.exRateB } }
if (ds === 'error') data = { ...data, notice: { ...tone('error'), icon: 'circle-x', title: C.exErrT, text: C.exErrB } }
if (async_ && ds === 'queued') data = { ...data, label: C.queuedBtn, op: '0.4', icon: 'mail-check', hint: '', notice: { ...tone('success'), icon: 'mail-check', title: C.queuedT, text: fmt(C.queuedB) } }
if (async_ && ds === 'inprogress') data = { ...data, label: C.queuedBtn, op: '0.4', icon: 'mail-check', hint: '', notice: { ...tone('info'), icon: 'info', title: C.inprogT, text: fmt(C.inprogB) } }
return {
c: C, old, form, pend, data,
items: (C.dataItems || []).map(x => ({ icon: x[0], title: x[1], text: x[2] })),
pEmail: part === 'email', pPanel: part === 'panel', pCards: part === 'cards',
showChange: !F && !P,
inH: m ? '44px' : '40px', inF: m ? '16px' : '14px', btnH: m ? '44px' : '36px', linkH: m ? '44px' : '20px',
panelPad: '16px', actDir: m ? 'column-reverse' : 'row', colDir: m ? 'column' : 'row', rowAlign: m ? 'stretch' : 'center',
itemCols: m ? 'minmax(0,1fr)' : 'repeat(2,minmax(0,1fr))', delAlign: m ? 'stretch' : 'flex-start'
}
}
}
</script>
</body>
</html>
+9 -4
View File
@@ -263,7 +263,7 @@
</sc-if>
<sc-if value="{{ isProfile }}">
<div style="padding:{{ pad }};display:flex;flex-direction:column;gap:16px;max-width:720px;overflow:hidden">
<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 }}">
@@ -281,7 +281,8 @@
</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><div style="display:flex;flex-direction:column;gap:6px;min-width:0"><span style="font-weight:500;color:var(--ui-text-highlighted)">Email</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@renew.example</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>
<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>
@@ -294,6 +295,7 @@
<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>
@@ -316,9 +318,12 @@
<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 }}">
@@ -362,7 +367,7 @@
</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"><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>
<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>
@@ -580,7 +585,7 @@ class Component extends DCLogic {
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',
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'
}
+7 -2
View File
@@ -38,9 +38,10 @@ class Component extends DCLogic {
fetch('emails/verification.html').then(x => x.text()).then(v => this.setState({ v }))
fetch('emails/password-reset.html').then(x => x.text()).then(r => this.setState({ r }))
fetch('emails/invitation.html').then(x => x.text()).then(i => this.setState({ i }))
;['verify-new-email','email-change-notice','account-deleted','export-ready'].forEach(n => fetch('emails/' + n + '.html').then(x => x.text()).then(t => this.setState({ ['x_' + n]: t })))
}
fill(t) {
return t.split('{{.InvitedBy}}').join('marta').split('{{.Email}} It').join('lea.martin@renew.example. It').split('sent to {{.Email}}').join('sent to lea.martin@renew.example').split('{{.Host}}').join('https://cms.renew.example').split('{{.VerificationCode}}').join('7f3c9a2e4b1d').split('{{.Email}}').join('marta%40renew.example').replace('uses marta%40renew.example', 'uses marta@renew.example')
return t.split('{{.InvitedBy}}').join('marta').split('{{.Email}} It').join('lea.martin@renew.example. It').split('sent to {{.Email}}').join('sent to lea.martin@renew.example').split('{{.Username}}').join('marta').split('{{.NewEmail}}').join('marta.alves@atelier-renew.example').split('{{.DeletedAt}}').join('29 Oct 2026').split('{{.Host}}').join('https://cms.renew.example').split('{{.VerificationCode}}').join('7f3c9a2e4b1d').split('{{.Email}}').join('marta%40renew.example').replace('uses marta%40renew.example', 'uses marta@renew.example')
}
frames(t) {
if (!t) return []
@@ -52,7 +53,11 @@ class Component extends DCLogic {
return { rows: [
{ title: 'Verification email', vars: '{{.Host}} · {{.VerificationCode}} → /register/validate?c=', frames: this.frames(this.state.v) },
{ title: 'Password reset email', vars: '{{.Host}} · {{.VerificationCode}} · {{.Email}} → /register/reset/validate?c=&email=', frames: this.frames(this.state.r) },
{ title: 'Invitation email', vars: '{{.Host}} · {{.VerificationCode}} · {{.Email}} · {{.InvitedBy}} → /invite/accept?c=', frames: this.frames(this.state.i) }
{ title: 'Invitation email', vars: '{{.Host}} · {{.VerificationCode}} · {{.Email}} · {{.InvitedBy}} → /invite/accept?c=', frames: this.frames(this.state.i) },
{ title: 'Verify new email', vars: '{{.Host}} · {{.Username}} · {{.VerificationCode}} → /account/email/confirm?c=', frames: this.frames(this.state['x_verify-new-email']) },
{ title: 'Email change notice (to the old address)', vars: '{{.Host}} · {{.Username}} · {{.NewEmail}}', frames: this.frames(this.state['x_email-change-notice']) },
{ title: 'Account deleted', vars: '{{.Host}} · {{.Username}} · {{.DeletedAt}}', frames: this.frames(this.state['x_account-deleted']) },
{ title: 'Export ready (alternative flow)', vars: '{{.Host}} · {{.Username}} · {{.VerificationCode}} → /account/export/', frames: this.frames(this.state['x_export-ready']) }
] }
}
}
@@ -0,0 +1,145 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<script src="./support.js"></script>
</head>
<body>
<x-dc>
<helmet>
<script src="./account-copy.js"></script>
<meta name="design_doc_mode" content="canvas">
<link rel="icon" href="brand/favicon.svg">
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=IBM+Plex+Mono:wght@400;500&family=Source+Serif+4:ital,opsz,wght@0,8..60,400;0,8..60,600;1,8..60,400&display=swap" rel="stylesheet">
<style>body{margin:0;font-family:'IBM Plex Sans',sans-serif;font-size:13px;line-height:20px}*{box-sizing:border-box}a{color:inherit;text-underline-offset:3px}a:hover{opacity:.8}code{font-family:'IBM Plex Mono',monospace;font-size:12px}</style>
</helmet>
<div ref="{{ rootRef }}" data-screen-label="Back-office screens" style="--ui-bg:#ffffff;--ui-bg-muted:#fafafa;--ui-bg-elevated:#f3f2f0;--ui-bg-accented:#e6e4e0;--ui-bg-inverted:#111111;--ui-text-dimmed:#999999;--ui-text-muted:#666666;--ui-text-toned:#444444;--ui-text:#222222;--ui-text-highlighted:#111111;--ui-text-inverted:#ffffff;--ui-border:#e6e4e0;--ui-border-muted:#f3f2f0;--ui-border-accented:#8a8783;--ui-primary:#111111;--ui-secondary:#635643;--ui-success:#315b33;--ui-info:#34536d;--ui-warning:#6c4700;--ui-error:#882c21;--rc-sand:#e8dfd1;--rc-success-soft:#eaefea;--rc-info-soft:#ebeef0;--rc-warning-soft:#f0ede6;--rc-error-soft:#f3eae9;--rc-overlay:rgb(10 10 10 / 0.4);--rc-shadow-pop:0 4px 16px rgb(17 17 17 / 0.08), 0 1px 2px rgb(17 17 17 / 0.06);--rc-shadow-modal:0 16px 48px rgb(17 17 17 / 0.18);--rc-canvas:#ecebe8;background:var(--rc-canvas);color:var(--ui-text);padding:64px;display:flex;flex-direction:column;gap:72px;width:max-content;min-width:100%">
<div style="display:flex;align-items:flex-end;gap:32px;max-width:1400px">
<div style="flex:1;display:flex;flex-direction:column;gap:10px">
<h1 style="margin:0;font:600 36px/44px 'IBM Plex Sans',sans-serif;letter-spacing:-0.02em;color:var(--ui-text-highlighted)">Screens: account (email, data, delete)</h1>
<p style="margin:0;max-width:880px;font-size:16px;line-height:26px;color:var(--ui-text-toned);text-wrap:pretty">Extends Profile with change email, export my data and delete account. 1440 px at 50%, 390 px at 75%. The theme switch shows every state in dark mode. </p>
</div>
<div role="radiogroup" aria-label="Theme" style="display:flex;border:1px solid var(--ui-border-accented);background:var(--ui-bg)">
<sc-for list="{{ modes }}" as="m" hint-placeholder-count="3">
<button role="radio" aria-checked="{{ m.on }}" onClick="{{ m.pick }}" style="all:unset;cursor:pointer;height:36px;padding:0 14px;display:flex;align-items:center;font-weight:500;font-size:14px;background:{{ m.bg }};color:{{ m.fg }}" style-focus-visible="outline:2px solid var(--ui-primary);outline-offset:2px">{{ m.label }}</button>
</sc-for>
</div>
</div>
<sc-for list="{{ groups }}" as="g" hint-placeholder-count="7">
<section data-screen-label="{{ g.label }}" style="display:flex;flex-direction:column;gap:28px">
<div style="display:flex;flex-direction:column;gap:6px;max-width:1100px">
<h2 style="margin:0;font:600 28px/36px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ g.title }}</h2>
<div style="display:flex;gap:16px;flex-wrap:wrap;font:400 14px/22px 'IBM Plex Mono',monospace;color:var(--ui-text-toned)"><span>{{ g.route }}</span><span style="color:var(--ui-text-muted)">{{ g.api }}</span></div>
<sc-if value="{{ g.note }}"><p style="margin:4px 0 0;font-size:15px;line-height:24px;color:var(--ui-text-toned);text-wrap:pretty">{{ g.note }}</p></sc-if>
</div>
<div style="display:flex;flex-wrap:wrap;gap:40px;align-items:flex-start">
<sc-for list="{{ g.states }}" as="st" hint-placeholder-count="4">
<div style="display:flex;flex-direction:column;gap:12px">
<div style="display:flex;flex-direction:column;gap:2px;max-width:1040px"><span style="font:600 16px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ st.name }}</span><span style="font-size:13px;color:var(--ui-text-muted)">{{ st.trigger }}</span></div>
<div style="display:flex;gap:24px;align-items:flex-start">
<div style="width:720px;height:450px;overflow:hidden;border:1px solid var(--ui-border);flex-shrink:0"><div style="width:1440px;height:900px;zoom:0.5"><sc-if value="{{ st.notMedia }}"><dc-import name="AppFrame" s="{{ st }}" mobile="{{ false }}" hint-size="1440px,900px"></dc-import></sc-if><sc-if value="{{ st.isMedia }}"><dc-import name="MediaFrame" s="{{ st }}" mobile="{{ false }}" hint-size="1440px,900px"></dc-import></sc-if></div></div>
<div style="width:293px;height:633px;overflow:hidden;border:1px solid var(--ui-border);flex-shrink:0"><div style="width:390px;height:844px;zoom:0.75"><sc-if value="{{ st.notMedia }}"><dc-import name="AppFrame" s="{{ st.m }}" mobile="{{ true }}" hint-size="390px,844px"></dc-import></sc-if><sc-if value="{{ st.isMedia }}"><dc-import name="MediaFrame" s="{{ st.m }}" mobile="{{ true }}" hint-size="390px,844px"></dc-import></sc-if></div></div>
</div>
</div>
</sc-for>
</div>
<sc-if value="{{ g.table }}"><div style="display:flex;flex-direction:column;gap:8px;max-width:1240px"><span style="font:600 16px/24px 'IBM Plex Sans',sans-serif;color:var(--ui-text-highlighted)">{{ g.tableTitle }}</span><div style="background:var(--ui-bg);border:1px solid var(--ui-border)"><div style="display:grid;grid-template-columns:90px 240px minmax(0,1fr);padding:8px 12px;background:var(--ui-bg-elevated);color:var(--ui-text-muted);font-weight:500"><span>Status</span><span>Where</span><span>Copy</span></div><sc-for list="{{ g.table }}" as="r" hint-placeholder-count="3"><div style="display:grid;grid-template-columns:90px 240px minmax(0,1fr);gap:0;padding:10px 12px;border-top:1px solid var(--ui-border);line-height:20px"><span style="font:500 13px 'IBM Plex Mono',monospace;color:var(--ui-text-highlighted)">{{ r.a }}</span><span style="color:var(--ui-text-toned);padding-right:16px">{{ r.b }}</span><span style="color:var(--ui-text-highlighted);text-wrap:pretty">{{ r.c }}</span></div></sc-for></div></div></sc-if>
</section>
</sc-for>
</div>
</x-dc>
<script type="text/x-dc" data-dc-script data-props="{&quot;theme&quot;:{&quot;editor&quot;:&quot;enum&quot;,&quot;options&quot;:[&quot;system&quot;,&quot;light&quot;,&quot;dark&quot;],&quot;default&quot;:&quot;system&quot;,&quot;tsType&quot;:&quot;'system' | 'light' | 'dark'&quot;}}">
class Component extends DCLogic {
state = { mode: this.props.theme ?? 'light' }
rootRef = React.createRef()
light = { '--ui-bg': '#ffffff', '--ui-bg-muted': '#fafafa', '--ui-bg-elevated': '#f3f2f0', '--ui-bg-accented': '#e6e4e0', '--ui-bg-inverted': '#111111', '--ui-text-dimmed': '#999999', '--ui-text-muted': '#666666', '--ui-text-toned': '#444444', '--ui-text': '#222222', '--ui-text-highlighted': '#111111', '--ui-text-inverted': '#ffffff', '--ui-border': '#e6e4e0', '--ui-border-muted': '#f3f2f0', '--ui-border-accented': '#8a8783', '--ui-primary': '#111111', '--ui-secondary': '#635643', '--ui-success': '#315b33', '--ui-info': '#34536d', '--ui-warning': '#6c4700', '--ui-error': '#882c21', '--rc-sand': '#e8dfd1', '--rc-success-soft': '#eaefea', '--rc-info-soft': '#ebeef0', '--rc-warning-soft': '#f0ede6', '--rc-error-soft': '#f3eae9', '--rc-overlay': 'rgb(10 10 10 / 0.4)', '--rc-shadow-pop': '0 4px 16px rgb(17 17 17 / 0.08), 0 1px 2px rgb(17 17 17 / 0.06)', '--rc-shadow-modal': '0 16px 48px rgb(17 17 17 / 0.18)', '--rc-canvas': '#ecebe8' }
dark = { '--ui-bg': '#111111', '--ui-bg-muted': '#0a0a0a', '--ui-bg-elevated': '#1a1a1a', '--ui-bg-accented': '#222222', '--ui-bg-inverted': '#fafafa', '--ui-text-dimmed': '#666666', '--ui-text-muted': '#999999', '--ui-text-toned': '#cfccc7', '--ui-text': '#e6e4e0', '--ui-text-highlighted': '#fafafa', '--ui-text-inverted': '#111111', '--ui-border': '#333333', '--ui-border-muted': '#222222', '--ui-border-accented': '#666666', '--ui-primary': '#fafafa', '--ui-secondary': '#d4c5b0', '--ui-success': '#a3caa3', '--ui-info': '#a5c2db', '--ui-warning': '#dfb67d', '--ui-error': '#fba394', '--rc-sand': '#d4c5b0', '--rc-success-soft': '#1f241f', '--rc-info-soft': '#202428', '--rc-warning-soft': '#2a251e', '--rc-error-soft': '#2c201f', '--rc-overlay': 'rgb(0 0 0 / 0.6)', '--rc-shadow-pop': '0 4px 16px rgb(0 0 0 / 0.5)', '--rc-shadow-modal': '0 16px 48px rgb(0 0 0 / 0.6)', '--rc-canvas': '#1c1c1c' }
resolved() {
if (this.state.mode === 'system') return window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light'
return this.state.mode
}
apply() {
const el = this.rootRef.current
if (!el) return
const vars = this.resolved() === 'dark' ? this.dark : this.light
for (const k in vars) el.style.setProperty(k, vars[k])
document.body.style.background = vars['--rc-canvas']
}
componentDidMount() { this.apply() }
componentDidUpdate(p) {
if (p.theme !== this.props.theme && this.props.theme) this.setState({ mode: this.props.theme })
this.apply()
}
groups() {
const C = (window.ACCOUNT_COPY || {}).en || {}
const nw = 'marta.alves@atelier-renew.example', old = 'marta@renew.example'
const fmt = t => (t || '').split('{new}').join(nw).split('{old}').join(old)
const toast = (tone, icon, t, b) => ({ tone, icon, title: C[t], text: fmt(C[b]) })
const S = (name, trigger, ac, extra = {}) => { const x = { kind: 'profile', nav: 'profile', crumbs: ['Profile'], mTitle: 'Profile', copy: C, name, trigger, ...extra, pf: { ac: { old, new: nw, ...ac }, shift: extra.shift || ES } }; return { ...x, m: { ...x } } }
const form = (o = {}) => ({ form: { email: nw, pw: 'limewash2026', focus: '', ...o } })
const pend = (o = {}) => ({ pending: { resend: 'idle', ...o } })
const D = (mode, state) => ({ data: { mode, state } })
const ES = [300, 330], DS = [780, 1100], ZS = [1000, 1500]
const del = (o = {}) => ({ title: C.delTitle, desc: C.delDesc, list: C.delList, warn: { tone: 'info', icon: 'info', title: C.delKeepT, text: C.delKeepB }, link: C.delExport, cancel: C.delKeep, confirm: C.delConfirm, tone: 'danger', disabled: true, close: true, ...o })
const pw = '••••••••••'
const dm = (fields, o = {}) => ({ modal: del({ fields, ...o }), shift: ZS })
const ff = (p, w, e = {}) => [{ label: C.delPw, value: p, ...(e.p || {}) }, { label: C.delWordLabel, value: w, ph: C.delWord, ...(e.w || {}) }]
const blk = (t, b, go) => ({ modal: { title: C[t], desc: C[b], cancel: C.blockBack, confirm: go ? C.blockGo : '' }, shift: ZS })
return this.finish([
{ label: 'Change email', title: 'Account: change email', route: '/profile', api: 'assumed: POST /v1/auth/email/change, resend, cancel', note: 'Extends the Account card on Profile. The email stays read-only until verified. The old email keeps working for login until the link is opened. A notice goes to the old address. A request for an address already in use looks like success, so nothing leaks.', states: [
S('Default', 'The Email field gains a Change email link.', {}),
S('Form open', 'Change email opens an inline form in the Account card.', form({ email: '', pw: '', focus: 'email' })),
S('Invalid format', 'On blur or submit. Nothing is sent.', form({ email: 'marta.alves@atelier', errEmail: C.errFormat })),
S('Same as current', 'Checked on submit, before any request.', form({ email: old, errEmail: C.errSame })),
S('Password missing', 'Submit with an empty password.', form({ pw: '', errPw: C.errPwEmpty, focus: 'pw' })),
S('Wrong password (403)', 'The password is cleared and gets focus.', form({ pw: '', errPw: C.errPwWrong, focus: 'pw' })),
S('Sending', 'Fields and button are disabled.', form({ sending: true })),
S('Pending', '200. The form closes; the old email stays active.', pend(), { toasts: [toast('success', 'circle-check', 'sentT', 'sentB')] }),
S('Pending: resending', 'Resend link in flight.', pend({ resend: 'sending' })),
S('Pending: resent', 'A new link replaces the old one.', pend(), { toasts: [toast('info', 'info', 'resentT', 'resentB')] }),
S('Pending: resend rate limited (429)', 'Resend is disabled with a countdown.', pend({ resend: 'rate' })),
S('Cancelled', 'Cancel change clears the pending state. No confirmation, it only discards a request.', {}, { toasts: [toast('info', 'info', 'cancelledT', 'cancelledB')] }),
S('Rate limited (429)', 'Submit is disabled.', form({ disabled: true, notice: C.rate })),
S('Server or network error', 'Values are kept.', form({ notice: C.server }))
] },
{ label: 'Your data', title: 'Account: export my data', route: '/profile', api: 'assumed: GET /v1/user/me/export (direct) or POST (async, emailed link)', note: 'Recommended: direct download, since one user\'s data in a small self-hosted CMS prepares in seconds. Alternative: a background export with an emailed link, for larger sites.', states: [
S('Direct (recommended): default', 'Your data card.', D('direct', 'idle'), { shift: DS }),
S('Direct: preparing', 'Button disabled, spinner.', D('direct', 'preparing'), { shift: DS }),
S('Direct: started', 'Browser download starts, then a toast.', D('direct', 'idle'), { shift: DS, toasts: [toast('success', 'circle-check', 'dlT', 'dlB')] }),
S('Direct: rate limited (429)', 'One export every 15 minutes.', D('direct', 'rate'), { shift: DS }),
S('Direct: error', '500 or network failure.', D('direct', 'error'), { shift: DS }),
S('Alternative, emailed link: default', 'Same card, request button.', D('async', 'idle'), { shift: DS }),
S('Alternative: requested', 'Export queued.', D('async', 'queued'), { shift: DS, toasts: [toast('success', 'circle-check', 'queuedToastT', 'queuedToastB')] }),
S('Alternative: already in progress', 'A second request while one is queued.', D('async', 'inprogress'), { shift: DS }),
S('Alternative: error', 'The request failed.', D('async', 'error'), { shift: DS })
] },
{ label: 'Delete account', title: 'Account: delete', route: '/profile', api: 'assumed: DELETE /v1/user/me {password}', note: 'Danger zone card with the existing destructive styling. Default for authored content: anonymize (credited to Deleted user). Open question. Confirm stays disabled until the password is filled and the word matches. Blocked states have no close button, no Escape and no backdrop dismissal.', states: [
S('Danger zone', 'Bottom of Profile.', {}, { shift: ZS }),
S('Confirm: empty', 'Confirm is disabled.', {}, dm(ff('', ''))),
S('Confirm: typing', 'Password entered, word partial. Still disabled.', {}, dm(ff(pw, 'DEL', { w: { focus: true } }))),
S('Confirm: ready', 'Both valid. Confirm is enabled.', {}, dm(ff(pw, 'DELETE'), { disabled: false })),
S('Wrong password (403)', 'The password field is cleared and marked.', {}, dm(ff('', 'DELETE', { p: { err: C.errPwWrong, focus: true } }), { disabled: false })),
S('Deleting', 'Fields and Cancel are disabled. Then sign out and redirect to log in.', {}, dm(ff(pw, 'DELETE'), { disabled: false, loading: true, confirm: C.deleting, close: false })),
S('Rate limited (429)', 'Confirm stays enabled.', {}, dm(ff(pw, 'DELETE'), { disabled: false, notice: C.rate })),
S('Server error', 'Nothing was deleted.', {}, dm(ff(pw, 'DELETE'), { disabled: false, notice: C.delServer })),
S('Blocked: last administrator', 'Not dismissable. Next step: Users.', {}, blk('blockT', 'blockB', true)),
S('Blocked: cannot be deleted', 'Generic server refusal. Not dismissable.', {}, blk('protT', 'protB', false))
] }
])
}
finish(gs) { return gs.map(g => ({ ...g, states: g.states.map(st => ({ notMedia: !st.isMedia, ...st, m: { ...st.m } })) })) }
renderVals() {
const modes = [['system', 'System'], ['light', 'Light'], ['dark', 'Dark']].map(([k, label]) => {
const on = this.state.mode === k
return { label, on, bg: on ? 'var(--ui-primary)' : 'transparent', fg: on ? 'var(--ui-bg)' : 'var(--ui-text-toned)', pick: () => this.setState({ mode: k }) }
})
return { rootRef: this.rootRef, modes, groups: this.groups() }
}
}
</script>
</body>
</html>
@@ -156,6 +156,19 @@ class Component extends DCLogic {
{ name: 'No session, retrying', trigger: 'Logged in inline. Now verifying.', spinner: 'Verifying your account…' },
{ name: 'Missing code', trigger: 'The link has no ?c= parameter. No request is made.', result: { tone: 'error', icon: 'link-2-off', title: 'This link is incomplete', text: 'The verification code is missing. Open the link from your email again, or copy the full address into your browser.', actions: [{ t: 'Go to log in', secondary: true }] } }
] },
{ label: 'Verify new email', title: 'Verify new email', route: '/account/email/confirm?c=CODE', api: 'assumed: POST /v1/auth/email/confirm. Needs a session; without one, inline log in', note: 'Opened from the verification email sent to the new address. Submits on load. The unavailable state says the same thing whether the address is in use or rejected, so it never reveals whether an account exists.', states: [
{ name: 'Confirming', trigger: 'On load, with a code in the URL.', spinner: 'Confirming your new email…' },
{ name: 'Success', trigger: '200 → /profile after 800 ms, with a toast.', result: { tone: 'success', icon: 'mail-check', title: 'Your email address has changed', text: 'Your account now uses marta.alves@atelier-renew.example. Use it the next time you log in.', actions: [{ t: 'Go to profile' }] }, toast: toast('success', 'circle-check', 'Email address changed', 'Your account now uses marta.alves@atelier-renew.example.') },
{ name: 'Invalid or expired (403)', trigger: 'The code is wrong, used, cancelled or older than 2 hours.', result: { tone: 'error', icon: 'link-2-off', title: 'This link doesn\'t work', text: 'It may have expired (links last 2 hours), been cancelled or already been used.', note: 'Request a new link from your profile.', actions: [{ t: 'Go to profile' }] } },
{ name: 'Address unavailable (409)', trigger: 'The address is taken or rejected. One message for both.', result: { tone: 'error', icon: 'mail-x', title: 'We couldn\'t change your email address', text: 'This address can\'t be used for your account. Go back to your profile and try a different one.', actions: [{ t: 'Go to profile' }] } },
{ name: 'No session (401)', trigger: 'Opened in another browser. Log in inline, then the change is confirmed with the same code.', title: 'Log in to confirm your new email', desc: 'You opened this link somewhere you\'re not logged in. Log in here and we\'ll confirm the change right after.', fields: [U(), P()], submit: { t: 'Log in and confirm' } },
{ name: 'No session, retrying', trigger: 'Logged in inline. Now confirming.', spinner: 'Confirming your new email…' },
{ name: 'Missing code', trigger: 'The link has no ?c= parameter. No request is made.', result: { tone: 'error', icon: 'link-2-off', title: 'This link is incomplete', text: 'The confirmation code is missing. Open the link from your email again, or copy the full address into your browser.', actions: [{ t: 'Go to log in', secondary: true }] } },
{ name: 'Server error', trigger: '500 or network failure. Nothing was changed.', result: { tone: 'error', icon: 'circle-x', title: 'Couldn\'t confirm the change', text: 'Something went wrong on our side. Nothing was changed. Try again in a moment.', actions: [{ t: 'Try again' }] } }
] },
{ label: 'Account deleted', title: 'Log in: after account deletion', route: '/login?reason=account-deleted', api: 'none', note: 'After a successful deletion the user is signed out and sent here. The message shows once and is gone on reload.', states: [
{ ...login, name: 'Account deleted', trigger: 'Redirected from the delete dialog.', notice: { tone: 'success', icon: 'circle-check', title: 'Your account has been deleted', text: 'Your articles and media were kept without your name.' }, fields: loginF({}, {}) }
] },
{ label: 'Accept invite', title: 'Accept invite', route: '/invite/accept?c=CODE', api: 'GET /v1/auth/invite?c= (pending) · POST /v1/auth/invite/accept (pending)', note: 'Opened from the invitation email. The link carries only the code, so the invited email is looked up on load and shown read-only. Username and password follow the Register rules, including PasswordStrengthHint. On success the person is logged in and sent to /home with a toast; they are already verified because the invite went to their email.', states: [
{ name: 'Checking the link', trigger: 'On load, while the invite is looked up.', spinner: 'Checking your invite…' },
{ ...inv, name: 'Default', trigger: 'The code is valid and unused.', fields: invF({}, {}) },
+101
View File
@@ -0,0 +1,101 @@
window.ACCOUNT_COPY = {
en: {
emailLabel: 'Email',
changeEmail: 'Change email',
newEmail: 'New email address',
newEmailPh: 'name@example.com',
password: 'Current password',
formHelp: "We'll send a verification link to the new address. Your email stays the same until you open it.",
cancel: 'Cancel',
send: 'Send verification link',
sending: 'Sending…',
errFormat: 'Enter a full email address, like name@example.com.',
errSame: "That's already your email address.",
errPwEmpty: 'Enter your current password.',
errPwWrong: "That password isn't correct. Try again.",
rate: 'Too many requests. Try again in a few minutes.',
server: 'Something went wrong on our side. Nothing was changed. Try again in a moment.',
pendTitle: 'Waiting for verification',
pendText: 'We sent a link to {new}. Until you open it, you keep logging in with {old}.',
resend: 'Resend link',
resendRate: 'Resend in 4:30',
cancelChange: 'Cancel change',
sentT: 'Verification link sent',
sentB: 'Check {new}. The link works once, within 2 hours.',
resentT: 'Link sent again',
resentB: 'The previous link no longer works.',
cancelledT: 'Email change cancelled',
cancelledB: 'You keep logging in with {old}.',
dataTitle: 'Your data',
dataDesc: 'Download a copy of the personal data RenewCMS holds about you, as JSON files in a ZIP.',
dataItems: [
['user', 'Profile', 'Username, display name, email, role and author profile.'],
['file-text', 'Authored content', 'Your articles and entries.'],
['image', 'Uploaded media', 'References to the files you uploaded.'],
['history', 'Account activity', 'Your log-ins and the actions recorded under your name.']
],
download: 'Download my data',
preparing: 'Preparing download…',
dlHint: 'Ready in a few seconds.',
dlT: 'Download started',
dlB: 'renewcms-export-marta-2026-10-29.zip is in your downloads.',
exRateT: 'You can export once every 15 minutes',
exRateB: 'Your last export was at 09:41. Try again in 12 minutes.',
exRateBtn: 'Available in 12:00',
exErrT: "Couldn't prepare your data",
exErrB: 'Nothing was changed. Try again in a moment.',
request: 'Request my data',
asyncHint: "Larger exports are prepared in the background. We'll email a download link to {old} when it's ready. You'll need to be logged in to download it.",
queuedT: "We're preparing your export",
queuedB: "This can take a few minutes. We'll email a link to {old} when it's ready.",
queuedBtn: 'Export requested',
inprogT: 'An export is already being prepared',
inprogB: "We'll email a link to {old} when it's ready.",
queuedToastT: 'Export requested',
queuedToastB: 'Check {old} in a few minutes.',
dzTitle: 'Danger zone',
dzDesc: "Delete your account and your personal data. This can't be undone.",
dzBtn: 'Delete my account',
delTitle: 'Delete your account?',
delDesc: "This permanently deletes your account. It can't be undone.",
delList: ['Your username, display name and email address', 'Your author profile and API keys', "Your sessions: you'll be logged out everywhere"],
delKeepT: 'Your articles and media stay, without your name',
delKeepB: 'They stay on the site and are credited to "Deleted user". Your name is removed from them and this cannot be reversed.',
delExport: 'Export my data first',
delPw: 'Current password',
delWordLabel: 'Type DELETE to confirm',
delWord: 'DELETE',
delKeep: 'Keep my account',
delConfirm: 'Delete my account',
deleting: 'Deleting…',
delServer: "Couldn't delete your account. Nothing was deleted. Try again in a moment.",
blockT: "You can't delete your account yet",
blockB: "You are the last Owner. RenewCMS needs at least one. Transfer ownership or promote another user first, then come back to delete your account.",
blockBack: 'Back to profile',
blockGo: 'Go to Users',
protT: "This account can't be deleted",
protB: "RenewCMS can't delete this account right now. Ask an administrator to help, then try again.",
vVerifying: 'Confirming your new email…',
vOkT: 'Your email address has changed',
vOkB: 'Your account now uses {new}. Use it the next time you log in.',
vOkBtn: 'Go to profile',
vOkToastT: 'Email address changed',
vOkToastB: 'Your account now uses {new}.',
vBadT: "This link doesn't work",
vBadB: 'It may have expired (links last 2 hours), been cancelled or already been used.',
vBadN: 'Request a new link from your profile.',
vNoT: "We couldn't change your email address",
vNoB: "This address can't be used for your account. Go back to your profile and try a different one.",
vLoginT: 'Log in to confirm your new email',
vLoginB: "You opened this link somewhere you're not logged in. Log in here and we'll confirm the change right after.",
vLoginBtn: 'Log in and confirm',
vMissT: 'This link is incomplete',
vMissB: 'The confirmation code is missing. Open the link from your email again, or copy the full address into your browser.',
vMissBtn: 'Go to log in',
vErrT: "Couldn't confirm the change",
vErrB: 'Something went wrong on our side. Nothing was changed. Try again in a moment.',
vErrBtn: 'Try again',
goneT: 'Your account has been deleted',
goneB: 'Your account has been deleted. Your articles and media were kept without your name.'
}
}
@@ -0,0 +1,62 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>Your account has been deleted</title>
<style>
body { margin: 0; padding: 0; width: 100%; -webkit-text-size-adjust: 100%; }
table { border-collapse: collapse; }
a { color: #111111; }
@media (max-width: 620px) {
.rc-card { padding: 28px 20px !important; }
.rc-btn a { display: block !important; }
}
@media (prefers-color-scheme: dark) {
.rc-bg { background-color: #0a0a0a !important; }
.rc-card { background-color: #111111 !important; border-color: #333333 !important; }
.rc-h1, .rc-word { color: #fafafa !important; }
.rc-text { color: #e6e4e0 !important; }
.rc-muted { color: #999999 !important; }
.rc-mark { border: 1px solid #444444 !important; }
.rc-btn td { background-color: #fafafa !important; }
.rc-btn a { background-color: #fafafa !important; color: #111111 !important; border-color: #fafafa !important; }
.rc-url { color: #e6e4e0 !important; background-color: #1a1a1a !important; }
.rc-rule { border-color: #333333 !important; }
}
</style>
</head>
<body class="rc-bg" style="margin:0;padding:0;background-color:#fafafa;">
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;">Your RenewCMS account was deleted and your personal data removed.</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="rc-bg" style="background-color:#fafafa;">
<tr>
<td align="center" style="padding:40px 12px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;">
<tr>
<td style="padding:0 0 20px 0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td class="rc-mark" width="32" height="32" align="center" valign="middle" bgcolor="#111111" style="width:32px;height:32px;background-color:#111111;color:#ffffff;font-family:Arial,Helvetica,sans-serif;font-size:19px;font-weight:bold;line-height:32px;">R</td>
<td class="rc-word" style="padding-left:10px;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:17px;font-weight:600;color:#111111;">RenewCMS</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="rc-card" style="background-color:#ffffff;border:1px solid #e6e4e0;padding:40px;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;">
<h1 class="rc-h1" style="margin:0 0 16px 0;font-size:24px;line-height:32px;font-weight:600;color:#111111;">Your account has been deleted</h1>
<p class="rc-text" style="margin:0 0 28px 0;font-size:16px;line-height:26px;color:#222222;">Your RenewCMS account at {{.Host}} has been deleted.</p>
<p class="rc-text rc-rule" style="margin:0;padding-top:0;border-top:0;font-size:14px;line-height:22px;color:#222222;">If you didn't ask for this, contact an administrator of {{.Host}}.</p>
</td>
</tr>
<tr>
<td class="rc-muted" style="padding:20px 0 0 0;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:12px;line-height:18px;color:#666666;">RenewCMS · {{.Host}}</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1,69 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>Your email address change was requested</title>
<style>
body { margin: 0; padding: 0; width: 100%; -webkit-text-size-adjust: 100%; }
table { border-collapse: collapse; }
a { color: #111111; }
@media (max-width: 620px) {
.rc-card { padding: 28px 20px !important; }
.rc-btn a { display: block !important; }
}
@media (prefers-color-scheme: dark) {
.rc-bg { background-color: #0a0a0a !important; }
.rc-card { background-color: #111111 !important; border-color: #333333 !important; }
.rc-h1, .rc-word { color: #fafafa !important; }
.rc-text { color: #e6e4e0 !important; }
.rc-muted { color: #999999 !important; }
.rc-mark { border: 1px solid #444444 !important; }
.rc-btn td { background-color: #fafafa !important; }
.rc-btn a { background-color: #fafafa !important; color: #111111 !important; border-color: #fafafa !important; }
.rc-url { color: #e6e4e0 !important; background-color: #1a1a1a !important; }
.rc-rule { border-color: #333333 !important; }
}
</style>
</head>
<body class="rc-bg" style="margin:0;padding:0;background-color:#fafafa;">
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;">A change of email address was requested for your RenewCMS account. Nothing has changed yet.</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="rc-bg" style="background-color:#fafafa;">
<tr>
<td align="center" style="padding:40px 12px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;">
<tr>
<td style="padding:0 0 20px 0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td class="rc-mark" width="32" height="32" align="center" valign="middle" bgcolor="#111111" style="width:32px;height:32px;background-color:#111111;color:#ffffff;font-family:Arial,Helvetica,sans-serif;font-size:19px;font-weight:bold;line-height:32px;">R</td>
<td class="rc-word" style="padding-left:10px;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:17px;font-weight:600;color:#111111;">RenewCMS</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="rc-card" style="background-color:#ffffff;border:1px solid #e6e4e0;padding:40px;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;">
<h1 class="rc-h1" style="margin:0 0 16px 0;font-size:24px;line-height:32px;font-weight:600;color:#111111;">Your email address change was requested</h1>
<p class="rc-text" style="margin:0 0 28px 0;font-size:16px;line-height:26px;color:#222222;">A request was made to change the email address of the RenewCMS account {{.Username}} at {{.Host}} from this address to {{.NewEmail}}. Nothing has changed yet: you keep logging in with this address until the new one is confirmed.</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" class="rc-btn">
<tr>
<td bgcolor="#111111" style="background-color:#111111;">
<a href="{{.Host}}/profile" target="_blank" style="display:inline-block;padding:14px 24px;border:1px solid #111111;background-color:#111111;color:#ffffff;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:16px;font-weight:600;line-height:20px;text-decoration:none;text-align:center;">Review my profile</a>
</td>
</tr>
</table>
<p class="rc-text rc-rule" style="margin:28px 0 0 0;padding-top:20px;border-top:1px solid #e6e4e0;font-size:14px;line-height:22px;color:#222222;">If this was you, no action is needed. If it wasn't, log in, cancel the change from your profile and change your password.</p>
</td>
</tr>
<tr>
<td class="rc-muted" style="padding:20px 0 0 0;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:12px;line-height:18px;color:#666666;">RenewCMS · {{.Host}}</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>Your data export is ready</title>
<style>
body { margin: 0; padding: 0; width: 100%; -webkit-text-size-adjust: 100%; }
table { border-collapse: collapse; }
a { color: #111111; }
@media (max-width: 620px) {
.rc-card { padding: 28px 20px !important; }
.rc-btn a { display: block !important; }
}
@media (prefers-color-scheme: dark) {
.rc-bg { background-color: #0a0a0a !important; }
.rc-card { background-color: #111111 !important; border-color: #333333 !important; }
.rc-h1, .rc-word { color: #fafafa !important; }
.rc-text { color: #e6e4e0 !important; }
.rc-muted { color: #999999 !important; }
.rc-mark { border: 1px solid #444444 !important; }
.rc-btn td { background-color: #fafafa !important; }
.rc-btn a { background-color: #fafafa !important; color: #111111 !important; border-color: #fafafa !important; }
.rc-url { color: #e6e4e0 !important; background-color: #1a1a1a !important; }
.rc-rule { border-color: #333333 !important; }
}
</style>
</head>
<body class="rc-bg" style="margin:0;padding:0;background-color:#fafafa;">
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;">Your RenewCMS data export is ready to download. The link expires in 24 hours.</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="rc-bg" style="background-color:#fafafa;">
<tr>
<td align="center" style="padding:40px 12px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;">
<tr>
<td style="padding:0 0 20px 0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td class="rc-mark" width="32" height="32" align="center" valign="middle" bgcolor="#111111" style="width:32px;height:32px;background-color:#111111;color:#ffffff;font-family:Arial,Helvetica,sans-serif;font-size:19px;font-weight:bold;line-height:32px;">R</td>
<td class="rc-word" style="padding-left:10px;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:17px;font-weight:600;color:#111111;">RenewCMS</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="rc-card" style="background-color:#ffffff;border:1px solid #e6e4e0;padding:40px;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;">
<h1 class="rc-h1" style="margin:0 0 16px 0;font-size:24px;line-height:32px;font-weight:600;color:#111111;">Your data export is ready</h1>
<p class="rc-text" style="margin:0 0 28px 0;font-size:16px;line-height:26px;color:#222222;">The export you requested for the RenewCMS account {{.Username}} at {{.Host}} is ready. It contains your profile, authored content, uploaded media references and account activity. You need to be logged in to download it.</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" class="rc-btn">
<tr>
<td bgcolor="#111111" style="background-color:#111111;">
<a href="{{.Host}}/account/export/{{.VerificationCode}}" target="_blank" style="display:inline-block;padding:14px 24px;border:1px solid #111111;background-color:#111111;color:#ffffff;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:16px;font-weight:600;line-height:20px;text-decoration:none;text-align:center;">Download my data</a>
</td>
</tr>
</table>
<p class="rc-muted" style="margin:16px 0 0 0;font-size:14px;line-height:22px;color:#666666;">This link expires in 24 hours and only works while you are logged in.</p>
<p class="rc-muted" style="margin:28px 0 8px 0;font-size:14px;line-height:22px;color:#666666;">If the button doesn't work, copy this address into your browser:</p>
<p class="rc-url" style="margin:0;padding:10px 12px;background-color:#f3f2f0;font-family:'IBM Plex Mono',Menlo,Consolas,monospace;font-size:13px;line-height:20px;color:#222222;word-break:break-all;">{{.Host}}/account/export/{{.VerificationCode}}</p>
<p class="rc-text rc-rule" style="margin:28px 0 0 0;padding-top:20px;border-top:1px solid #e6e4e0;font-size:14px;line-height:22px;color:#222222;">If you didn't request this export, log in and change your password.</p>
</td>
</tr>
<tr>
<td class="rc-muted" style="padding:20px 0 0 0;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:12px;line-height:18px;color:#666666;">RenewCMS · {{.Host}}</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light dark">
<meta name="supported-color-schemes" content="light dark">
<title>Confirm your new email address</title>
<style>
body { margin: 0; padding: 0; width: 100%; -webkit-text-size-adjust: 100%; }
table { border-collapse: collapse; }
a { color: #111111; }
@media (max-width: 620px) {
.rc-card { padding: 28px 20px !important; }
.rc-btn a { display: block !important; }
}
@media (prefers-color-scheme: dark) {
.rc-bg { background-color: #0a0a0a !important; }
.rc-card { background-color: #111111 !important; border-color: #333333 !important; }
.rc-h1, .rc-word { color: #fafafa !important; }
.rc-text { color: #e6e4e0 !important; }
.rc-muted { color: #999999 !important; }
.rc-mark { border: 1px solid #444444 !important; }
.rc-btn td { background-color: #fafafa !important; }
.rc-btn a { background-color: #fafafa !important; color: #111111 !important; border-color: #fafafa !important; }
.rc-url { color: #e6e4e0 !important; background-color: #1a1a1a !important; }
.rc-rule { border-color: #333333 !important; }
}
</style>
</head>
<body class="rc-bg" style="margin:0;padding:0;background-color:#fafafa;">
<div style="display:none;max-height:0;overflow:hidden;opacity:0;color:transparent;">Confirm this address to finish changing your RenewCMS email. The link expires in 2 hours.</div>
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" class="rc-bg" style="background-color:#fafafa;">
<tr>
<td align="center" style="padding:40px 12px;">
<table role="presentation" width="100%" cellpadding="0" cellspacing="0" border="0" style="max-width:600px;">
<tr>
<td style="padding:0 0 20px 0;">
<table role="presentation" cellpadding="0" cellspacing="0" border="0">
<tr>
<td class="rc-mark" width="32" height="32" align="center" valign="middle" bgcolor="#111111" style="width:32px;height:32px;background-color:#111111;color:#ffffff;font-family:Arial,Helvetica,sans-serif;font-size:19px;font-weight:bold;line-height:32px;">R</td>
<td class="rc-word" style="padding-left:10px;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:17px;font-weight:600;color:#111111;">RenewCMS</td>
</tr>
</table>
</td>
</tr>
<tr>
<td class="rc-card" style="background-color:#ffffff;border:1px solid #e6e4e0;padding:40px;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;">
<h1 class="rc-h1" style="margin:0 0 16px 0;font-size:24px;line-height:32px;font-weight:600;color:#111111;">Confirm your new email address</h1>
<p class="rc-text" style="margin:0 0 28px 0;font-size:16px;line-height:26px;color:#222222;">Someone, hopefully you, asked to change the email address of the RenewCMS account {{.Username}} at {{.Host}} to this address. Confirm it to finish the change.</p>
<table role="presentation" cellpadding="0" cellspacing="0" border="0" class="rc-btn">
<tr>
<td bgcolor="#111111" style="background-color:#111111;">
<a href="{{.Host}}/account/email/confirm?c={{.VerificationCode}}" target="_blank" style="display:inline-block;padding:14px 24px;border:1px solid #111111;background-color:#111111;color:#ffffff;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:16px;font-weight:600;line-height:20px;text-decoration:none;text-align:center;">Confirm new email address</a>
</td>
</tr>
</table>
<p class="rc-muted" style="margin:16px 0 0 0;font-size:14px;line-height:22px;color:#666666;">This link expires in 2 hours.</p>
<p class="rc-muted" style="margin:28px 0 8px 0;font-size:14px;line-height:22px;color:#666666;">If the button doesn't work, copy this address into your browser:</p>
<p class="rc-url" style="margin:0;padding:10px 12px;background-color:#f3f2f0;font-family:'IBM Plex Mono',Menlo,Consolas,monospace;font-size:13px;line-height:20px;color:#222222;word-break:break-all;">{{.Host}}/account/email/confirm?c={{.VerificationCode}}</p>
<p class="rc-text rc-rule" style="margin:28px 0 0 0;padding-top:20px;border-top:1px solid #e6e4e0;font-size:14px;line-height:22px;color:#222222;">If this wasn't you, ignore this email; nothing will change.</p>
</td>
</tr>
<tr>
<td class="rc-muted" style="padding:20px 0 0 0;font-family:'IBM Plex Sans',Arial,Helvetica,sans-serif;font-size:12px;line-height:18px;color:#666666;">RenewCMS · {{.Host}}</td>
</tr>
</table>
</td>
</tr>
</table>
</body>
</html>