chore: gdpr features design groundwork
This commit is contained in:
1 parent
c6d49db95e
commit
bb191c389a
16 files changed
+798
-6
No files matched your search
@@ -149,3 +149,8 @@ The Schema board group 7 (Relations) now has 4 states; the Integration page and
|
|||||||
- Pagination: articles return {items, total, page, size, counts}; media returns an array with X-Total-Count.
|
- Pagination: articles return {items, total, page, size, counts}; media returns an array with X-Total-Count.
|
||||||
- Event names follow the backend: role.create, role.edit, role.delete, role.reassign, user.change_role, user.transfer_ownership, user.remove.
|
- Event names follow the backend: role.create, role.edit, role.delete, role.reassign, user.change_role, user.transfer_ownership, user.remove.
|
||||||
- Counts above include the Media (+4) and Roles (+6) states only. States added this round to the Editor, Articles, Sites, Authors, Profile and Schema groups are on the boards and in the screens files, and are not yet recounted here.
|
- Counts above include the Media (+4) and Roles (+6) states only. States added this round to the Editor, Articles, Sites, Authors, Profile and Schema groups are on the boards and in the screens files, and are not yet recounted here.
|
||||||
|
|
||||||
|
## Pass 7: account self-service (email, export, delete)
|
||||||
|
Board `source/RenewCMS Screens Account.dc.html` (33 states: change email 14, export 9, delete 10), Auth board groups "Verify new email" (7) and "Account deleted" (1), four email templates in `source/emails/` (previews on the Emails board). Specs: `screens/account.md`, `emails.md`, `routes.md`, `components.md`, `states.md`. Frames: `source/AccountCards.dc.html` (new; copy in `source/account-copy.js`), `AppFrame.dc.html` (Profile hooks, modal list, warning, link and disabled confirm). Every Profile frame on the other boards shows the new cards in their default state. English only.
|
||||||
|
Decided: authored content is anonymized to "Deleted user"; email-change link 2 hours; export link 24 hours, one export per 15 minutes; direct download is the primary export flow, the emailed link is the documented alternative; the export-ready link needs a logged-in session; "last administrator" counts Owner; a change request for an address already in use answers like success; the account-deleted email goes to the address the account had.
|
||||||
|
Not done: Foundations and Components boards and the Prototype are not updated. The 15-minute rate-limit state is clock-sensitive (frozen clock 09:41) and is skipped by `task visual:check`.
|
||||||
@@ -157,3 +157,14 @@ Accessibility of the tri-state cell:
|
|||||||
| Announcement | Selected item is aria-checked="true". A polite live region announces changes: "Events, Update, set to Own." Disabled items are aria-disabled and described by the reason ("You can only grant what your role has."). |
|
| Announcement | Selected item is aria-checked="true". A polite live region announces changes: "Events, Update, set to Own." Disabled items are aria-disabled and described by the reason ("You can only grant what your role has."). |
|
||||||
| Not by color alone | Selected segment: sand fill plus weight 600 plus a 2 px underline in --ui-primary. Locked: lock icon. Changed cell: square marker. Mixed: text badge. |
|
| Not by color alone | Selected segment: sand fill plus weight 600 plus a 2 px underline in --ui-primary. Locked: lock icon. Changed cell: square marker. Mixed: text badge. |
|
||||||
| Targets | 26 px segments on desktop, 44 px on mobile. Focus: 2 px --ui-primary outline, offset 2 px. |
|
| Targets | 26 px segments on desktop, 44 px on mobile. Focus: 2 px --ui-primary outline, offset 2 px. |
|
||||||
|
|
||||||
|
## Account (change email, export, delete)
|
||||||
|
| Component | Status | Nuxt UI mapping | Where |
|
||||||
|
|---|---|---|---|
|
||||||
|
| EmailChangeForm | New | UFormField + UInput (email, password) + UButton, inside the Account UCard | Profile |
|
||||||
|
| PendingEmailNotice | New | UAlert (info) + two UButton (outline) | Profile |
|
||||||
|
| DataExportCard | New | UCard + UButton; notices as UAlert | Profile |
|
||||||
|
| DangerZoneCard | New | UCard with border var(--ui-error), UButton color error | Profile |
|
||||||
|
| DeleteAccountDialog | New | UModal (existing destructive ConfirmDialog shell) + UInput x2 + list + UAlert info + ULink | Profile |
|
||||||
|
| BlockedDialog | New | UModal with dismissible false, no close button; UButton primary for the next step | Profile |
|
||||||
|
Specs: field heights 40 (44 mobile), buttons 36 (44 mobile), card padding 16, section gap 16. Colors from tokens only. Destructive confirm is disabled at 40% opacity until valid. Implemented in AccountCards.dc.html and the AppFrame modal.
|
||||||
@@ -14,3 +14,15 @@ Previews: `source/RenewCMS Emails.dc.html`.
|
|||||||
Three Go html/template files: emails/verification.html, emails/password-reset.html and emails/invitation.html . They use a table layout with inline CSS and a 600 px maximum width. The button is bulletproof: a colored table cell plus a padded link, so it survives Outlook. Each has a plain-URL fallback, a hidden preheader, and prefers-color-scheme overrides for clients that support them. The mark is live text in a black cell rather than an image, so it shows even when images are blocked. Previews below use example values.
|
Three Go html/template files: emails/verification.html, emails/password-reset.html and emails/invitation.html . They use a table layout with inline CSS and a 600 px maximum width. The button is bulletproof: a colored table cell plus a padded link, so it survives Outlook. Each has a plain-URL fallback, a hidden preheader, and prefers-color-scheme overrides for clients that support them. The mark is live text in a black cell rather than an image, so it shows even when images are blocked. Previews below use example values.
|
||||||
|
|
||||||
Suggested subjects are "Verify your RenewCMS account", "Reset your RenewCMS password" and "<InvitedBy> invited you to RenewCMS", where .InvitedBy is the inviter's display name or username. html/template escapes the .Email value for the query string in the reset link (the @ becomes %40), and the confirm route reads it back decoded.
|
Suggested subjects are "Verify your RenewCMS account", "Reset your RenewCMS password" and "<InvitedBy> invited you to RenewCMS", where .InvitedBy is the inviter's display name or username. html/template escapes the .Email value for the query string in the reset link (the @ becomes %40), and the confirm route reads it back decoded.
|
||||||
|
|
||||||
|
## Account emails (change email, deletion, export)
|
||||||
|
Go `html/template` files built from `source/emails/verification.html` (same table layout, tokens, dark overrides, bulletproof button, URL fallback). English only.
|
||||||
|
|
||||||
|
| File | To | Subject | Variables |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `source/emails/verify-new-email.html` | the new address | Confirm your new RenewCMS email address | `.Host`, `.Username`, `.VerificationCode`, linking to `{{.Host}}/account/email/confirm?c={{.VerificationCode}}` (expires in 2 hours, works once) |
|
||||||
|
| `source/emails/email-change-notice.html` | the old address, at request time | Your RenewCMS email address change was requested | `.Host`, `.Username`, `.NewEmail`, linking to `{{.Host}}/profile` |
|
||||||
|
| `source/emails/account-deleted.html` | the address the account had, sent once | Your RenewCMS account has been deleted | `.Host`, `.Username`, `.DeletedAt`; no button, generic text, no personal data |
|
||||||
|
| `source/emails/export-ready.html` | the account address, async flow only | Your RenewCMS data export is ready | `.Host`, `.Username`, `.VerificationCode`, linking to `{{.Host}}/account/export/{{.VerificationCode}}` (expires in 24 hours, requires a logged-in session) |
|
||||||
|
|
||||||
|
`export-ready.html` is only needed if the async export alternative is built. Previews are rows on `source/RenewCMS Emails.dc.html`.
|
||||||
@@ -74,3 +74,10 @@ Needs backend:
|
|||||||
| Enforcement | Server checks every call against role permissions and the no-escalation rules. API keys resolve the creator's current role on each request. |
|
| Enforcement | Server checks every call against role permissions and the no-escalation rules. API keys resolve the creator's current role on each request. |
|
||||||
| Version field | role.version for the conflict state (409 role_conflict). |
|
| Version field | role.version for the conflict state (409 role_conflict). |
|
||||||
| Activity events | role.create, role.edit (with a diff of added and removed permissions), role.delete, role.reassign, user.change_role, user.transfer_ownership. Present tense, as the backend emits them. |
|
| Activity events | role.create, role.edit (with a diff of added and removed permissions), role.delete, role.reassign, user.change_role, user.transfer_ownership. Present tense, as the backend emits them. |
|
||||||
|
|
||||||
|
## Account: change email, export, delete
|
||||||
|
| Route | Screen | Auth | API (assumed) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| /account/email/confirm?c=CODE | Verify new email (AuthCard). States in screens/account.md | Session required; without one, inline log in (as /register/validate) | POST /v1/auth/email/confirm |
|
||||||
|
| /profile | Gains change email, Your data, Danger zone | Any verified user | POST /v1/auth/email/change, POST /v1/auth/email/resend, DELETE /v1/auth/email/change, GET /v1/user/me/export, DELETE /v1/user/me |
|
||||||
|
| /login?reason=account-deleted | One-time message after deletion | Public | none |
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
|
||||||
|
# Account: change email, export my data, delete account
|
||||||
|
|
||||||
|
Route: `/profile` (extends the Account card, adds "Your data" and "Danger zone" cards). Verification landing: `/account/email/confirm?c=CODE`. API calls are assumed, not specified.
|
||||||
|
|
||||||
|
Placement: Profile already holds the read-only Email in the Account card and the Password card, so the email change extends that card; the two other features are cards at the bottom. No new navigation. Order: Display name, Account, Your role, Author profile, Password, Your data, Danger zone.
|
||||||
|
|
||||||
|
Boards: `RenewCMS Screens Account.dc.html` (Profile states), `RenewCMS Screens Auth.dc.html` (groups "Verify new email" and "Account deleted"). Frames: `AppFrame`, `AccountCards`, `AuthCard`. Copy source: `account-copy.js`. English only.
|
||||||
|
|
||||||
|
## Change email
|
||||||
|
| State | Copy |
|
||||||
|
|---|---|
|
||||||
|
| Default | "Email" field, link "Change email" |
|
||||||
|
| Form | "New email address" · "Current password" · "We'll send a verification link to the new address. Your email stays the same until you open it." · "Cancel" · "Send verification link" |
|
||||||
|
| Invalid format | "Enter a full email address, like name@example.com." |
|
||||||
|
| Same as current | "That's already your email address." |
|
||||||
|
| Password missing | "Enter your current password." |
|
||||||
|
| Wrong password (403) | "That password isn't correct. Try again." |
|
||||||
|
| Sending | "Sending…" |
|
||||||
|
| Pending | "Waiting for verification" · "We sent a link to marta.alves@atelier-renew.example. Until you open it, you keep logging in with marta@renew.example." · "Resend link" · "Cancel change"; toast "Verification link sent" / "Check marta.alves@atelier-renew.example. The link works once, within 2 hours." |
|
||||||
|
| Resending | "Sending…" |
|
||||||
|
| Resent | toast "Link sent again" / "The previous link no longer works." |
|
||||||
|
| Resend rate limited (429) | "Resend in 4:30" (disabled) · "Too many requests. Try again in a few minutes." |
|
||||||
|
| Cancelled | toast "Email change cancelled" / "You keep logging in with marta@renew.example." |
|
||||||
|
| Rate limited (429) | "Too many requests. Try again in a few minutes.", Send disabled |
|
||||||
|
| Server or network | "Something went wrong on our side. Nothing was changed. Try again in a moment." |
|
||||||
|
|
||||||
|
Rules: the old email stays active for login until the link is opened. The old address gets the notice email at request time. A request for an address already in use answers like success, so the UI and the response never reveal that an account exists.
|
||||||
|
|
||||||
|
## Verification landing (AuthCard)
|
||||||
|
| State | Copy |
|
||||||
|
|---|---|
|
||||||
|
| Confirming | "Confirming your new email…" |
|
||||||
|
| Success | "Your email address has changed" · "Your account now uses marta.alves@atelier-renew.example. Use it the next time you log in." · "Go to profile"; toast "Email address changed" / "Your account now uses marta.alves@atelier-renew.example." |
|
||||||
|
| Invalid or expired (403) | "This link doesn't work" · "It may have expired (links last 2 hours), been cancelled or already been used." · "Request a new link from your profile." · "Go to profile" |
|
||||||
|
| Address unavailable (409) | "We couldn't change your email address" · "This address can't be used for your account. Go back to your profile and try a different one." · "Go to profile" (same copy whether the address is taken or rejected) |
|
||||||
|
| No session (401) | "Log in to confirm your new email" · "You opened this link somewhere you're not logged in. Log in here and we'll confirm the change right after." · "Log in and confirm" |
|
||||||
|
| Missing code | "This link is incomplete" · "The confirmation code is missing. Open the link from your email again, or copy the full address into your browser." · "Go to log in" |
|
||||||
|
| Server error | "Couldn't confirm the change" · "Something went wrong on our side. Nothing was changed. Try again in a moment." · "Try again" |
|
||||||
|
|
||||||
|
## Your data
|
||||||
|
Contents listed in the card: Profile (Username, display name, email, role and author profile.); Authored content (Your articles and entries.); Uploaded media (References to the files you uploaded.); Account activity (Your log-ins and the actions recorded under your name.). Card copy: "Your data" · "Download a copy of the personal data RenewCMS holds about you, as JSON files in a ZIP.".
|
||||||
|
| State | Copy |
|
||||||
|
|---|---|
|
||||||
|
| Direct (recommended): default | "Download my data" · "Ready in a few seconds." |
|
||||||
|
| Preparing | "Preparing download…" |
|
||||||
|
| Started | toast "Download started" / "renewcms-export-marta-2026-10-29.zip is in your downloads." |
|
||||||
|
| Rate limited (429, one per 15 minutes, clock-sensitive) | "You can export once every 15 minutes" · "Your last export was at 09:41. Try again in 12 minutes." · "Available in 12:00" |
|
||||||
|
| Error | "Couldn't prepare your data" · "Nothing was changed. Try again in a moment." |
|
||||||
|
| Alternative, emailed link: default | "Request my data" · "Larger exports are prepared in the background. We'll email a download link to marta@renew.example when it's ready." |
|
||||||
|
| Requested | "We're preparing your export" · "This can take a few minutes. We'll email a link to marta@renew.example when it's ready." · "Export requested"; toast "Export requested" / "Check marta@renew.example in a few minutes." |
|
||||||
|
| Already in progress | "An export is already being prepared" · "We'll email a link to marta@renew.example when it's ready." |
|
||||||
|
|
||||||
|
Recommendation: direct download; async is the documented alternative and uses the export-ready email.
|
||||||
|
|
||||||
|
## Delete account
|
||||||
|
Danger zone: "Danger zone" · "Delete your account and your personal data. This can't be undone." · "Delete my account".
|
||||||
|
| State | Copy |
|
||||||
|
|---|---|
|
||||||
|
| Dialog | "Delete your account?" · "This permanently deletes your account. It can't be undone." · list: "Your username, display name and email address", "Your author profile and API keys", "Your sessions: you'll be logged out everywhere" · "Your articles and media stay" / "They stay on the site, credited to "Deleted user". Your name is removed from them." · "Export my data first" · "Current password" · "Type DELETE to confirm" · "Keep my account" · "Delete my account" |
|
||||||
|
| Empty, typing | Confirm disabled until password is filled and the word equals "DELETE" (exact, case-sensitive) |
|
||||||
|
| Wrong password (403) | "That password isn't correct. Try again." |
|
||||||
|
| Deleting | "Deleting…"; fields and Cancel disabled; then sign out and redirect to /login?reason=account-deleted |
|
||||||
|
| Rate limited (429) | "Too many requests. Try again in a few minutes." |
|
||||||
|
| Server error | "Couldn't delete your account. Nothing was deleted. Try again in a moment." |
|
||||||
|
| Blocked: last Owner or administrator | "You can't delete your account yet" · Owner: "You are the last Owner. RenewCMS needs at least one. Transfer ownership or promote another user first, then come back to delete your account." · Administrator: "You are the last administrator. RenewCMS needs at least one. Promote another user first, then come back to delete your account." · "Back to profile" · "Go to Users" |
|
||||||
|
| Blocked: cannot be deleted | "This account can't be deleted" · "RenewCMS can't delete this account right now. Ask an administrator to help, then try again." · "Back to profile" |
|
||||||
|
| Login after deletion | "Your account has been deleted" · "Your articles and media were kept without your name." |
|
||||||
|
|
||||||
|
Blocked dialogs have no close button, do not close on Escape or backdrop click, and always give a next step.
|
||||||
|
|
||||||
|
Authored content: anonymized to "Deleted user", stated in the dialog. Export-ready email: the link needs a logged-in session, stated in the email and the card hint. Account-deleted email: generic, no personal data, sent to the address the account had at deletion.
|
||||||
@@ -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>
|
||||||
@@ -263,7 +263,7 @@
|
|||||||
</sc-if>
|
</sc-if>
|
||||||
|
|
||||||
<sc-if value="{{ isProfile }}">
|
<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>
|
<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="{{ 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 }}">
|
<sc-if value="{{ pfReady }}">
|
||||||
@@ -281,7 +281,8 @@
|
|||||||
</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="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: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>
|
||||||
<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>
|
<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.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>
|
<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>
|
<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>
|
</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>
|
</sc-if>
|
||||||
</div>
|
</div>
|
||||||
</sc-if>
|
</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;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">
|
<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.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">
|
<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>
|
<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-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.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.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 }}">
|
<sc-if value="{{ s.modal.upload }}">
|
||||||
@@ -362,7 +367,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="padding:20px 24px;display:flex;flex-direction:{{ mBtnDir }};justify-content:flex-end;gap:8px">
|
<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.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>
|
</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.',
|
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)',
|
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',
|
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)',
|
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'
|
hasToasts: toasts.length > 0, tL: m ? '16px' : 'auto', tR: m ? '16px' : '24px', tB: m ? '16px' : '24px', tW: m ? 'auto' : '356px'
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,9 +38,10 @@ class Component extends DCLogic {
|
|||||||
fetch('emails/verification.html').then(x => x.text()).then(v => this.setState({ v }))
|
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/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 }))
|
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) {
|
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) {
|
frames(t) {
|
||||||
if (!t) return []
|
if (!t) return []
|
||||||
@@ -52,7 +53,11 @@ class Component extends DCLogic {
|
|||||||
return { rows: [
|
return { rows: [
|
||||||
{ title: 'Verification email', vars: '{{.Host}} · {{.VerificationCode}} → /register/validate?c=', frames: this.frames(this.state.v) },
|
{ 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: '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="{"theme":{"editor":"enum","options":["system","light","dark"],"default":"system","tsType":"'system' | 'light' | 'dark'"}}">
|
||||||
|
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: '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 }] } }
|
{ 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: [
|
{ 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…' },
|
{ 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({}, {}) },
|
{ ...inv, name: 'Default', trigger: 'The code is valid and unused.', fields: invF({}, {}) },
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -177,3 +177,13 @@ A role with Read set to Own sees online articles plus its own drafts. For custom
|
|||||||
|
|
||||||
## Wording status
|
## Wording status
|
||||||
Strings that match messages.ts are verbatim. "Only the owner can manage sites…" and other board-only wording are proposals. Moving the app to permission-based wording is a separate code change.
|
Strings that match messages.ts are verbatim. "Only the owner can manage sites…" and other board-only wording are proposals. Moving the app to permission-based wording is a separate code change.
|
||||||
|
|
||||||
|
## Account: change email, export, delete
|
||||||
|
States and copy in screens/account.md. Board: RenewCMS Screens Account.dc.html (33 states) and the Auth board groups "Verify new email" (7) and "Account deleted" (1). Direct-download rate limit state is clock-sensitive (frozen clock 09:41).
|
||||||
|
| Status | Copy |
|
||||||
|
|---|---|
|
||||||
|
| 403 wrong_password | "That password isn't correct. Try again." (inline under password) |
|
||||||
|
| 429 | existing 429 copy, inline in the form or the dialog |
|
||||||
|
| 500, network | existing 500 copy, or "Couldn't delete your account. Nothing was deleted. Try again in a moment." |
|
||||||
|
| 409 last_admin | "You can't delete your account yet" (non-dismissable dialog) |
|
||||||
|
| 409 email_unavailable | Landing only: "We couldn't change your email address" (same for in use and rejected) |
|
||||||
Reference in new issue
Block a user