chore: new design groundwork for next features
This commit is contained in:
1 parent
b29d0ed045
commit
792aba433d
14 files changed
+676
-25
No files matched your search
@@ -95,6 +95,18 @@ class Component extends DCLogic {
|
||||
const E = (o = {}, extra = {}) => withM({ ...ed, ...extra, ed: { status: 'draft', save: 'saved', ...o } })
|
||||
const cropM = crop => ({ title: 'Crop and resize', desc: 'north-wall.jpg · 3.9 MB', close: true, crop, cancel: 'Choose another file', confirm: 'Upload image' })
|
||||
const del = (loading, err) => ({ title: 'Delete this article?', desc: '"Winter checklist for timber windows" and its 3 images will be deleted. This can\'t be undone.', cancel: 'Cancel', confirm: loading ? 'Deleting…' : 'Delete article', tone: 'danger', loading, notice: err })
|
||||
const people = [
|
||||
{ email: 'sam.oliveira@renew.example', st: 'pending', c: '27 Sep 2026' },
|
||||
{ email: 'p.huber@gmail.com', st: 'expired', c: '3 Aug 2026' },
|
||||
{ u: 'ines', display: 'Inès Duarte', st: 'accepted', c: '18 Jun 2026' },
|
||||
{ u: 'jonas', st: 'accepted', c: '2 Apr 2026' },
|
||||
{ u: 'marta', st: 'accepted', c: '12 Mar 2026', you: true }
|
||||
]
|
||||
const U = (o = {}) => ({ kind: 'users', nav: 'users', crumbs: ['Users'], mTitle: 'Users', ...o, users: { rows: people, ...(o.users || {}) } })
|
||||
const inv = (o = {}) => ({ title: 'Invite a user', desc: 'We\'ll email them a link to choose a username and password. The link expires after 3 days.', close: true, cancel: 'Cancel', confirm: 'Send invite', ...o, fields: o.fields || [{ label: 'Email', ph: 'name@example.com', focus: true }] })
|
||||
const invF = (value, extra = {}) => [{ label: 'Email', value, ...extra }]
|
||||
const P = (pf = {}, o = {}) => withM({ kind: 'profile', nav: 'profile', crumbs: ['Profile'], mTitle: 'Profile', ...o, pf })
|
||||
const dname = 'Marta Alves'
|
||||
return [
|
||||
{ label: 'Boot', title: 'App boot', route: 'any route, first load', api: 'GET /v1/auth/me (pending)', note: 'While the session is checked, the shell renders with skeletons, never a spinner. The possible outcomes are: 200 with a verified user, which renders the route; 200 with an unverified user, which goes to /register/pending; and 401, which goes to /login?redirect=<path>. Public routes skip this check.', states: [
|
||||
withM({ ...home, name: 'Checking session', trigger: 'The first paint on any back-office URL, and usually visible for well under a second.', boot: true })
|
||||
@@ -156,6 +168,33 @@ class Component extends DCLogic {
|
||||
withM({ kind: 'integration', nav: 'integration', crumbs: ['Integration'], mTitle: 'Integration', name: 'Default', trigger: 'Opened from the sidebar or the Home card.' }),
|
||||
withM({ kind: 'integration', nav: 'integration', crumbs: ['Integration'], mTitle: 'Integration', copied: true, name: 'Copied', trigger: 'The base URL was copied. The button reads "Copied" for 2 s and this is announced.' })
|
||||
] },
|
||||
{ label: 'Users', title: 'Users', route: '/users', api: 'GET /v1/user/ · POST /v1/user/invite', note: 'One shared workspace: every user sees the same articles and this same list. Rows have no actions yet. Pending and expired invites show the email address, because a username only exists once the invite is accepted. An invite is Pending until its link is used, Expired after 3 days, and Accepted once the account is created.', states: [
|
||||
withM({ ...U(), name: 'Default', trigger: 'The list loaded, newest first.' }),
|
||||
withM({ ...U({ users: { loading: true } }), name: 'Loading', trigger: 'The first load, or a retry. Skeletons appear after 150 ms.' }),
|
||||
withM({ ...U({ users: { rows: people.slice(-1), only: true } }), name: 'Only you', trigger: 'Nobody has been invited yet. The admin who registered is always listed.' }),
|
||||
withM({ ...U({ users: { error: true } }), name: 'Error', trigger: 'A 500 or network failure on the list request.' }),
|
||||
withM({ ...U({ modal: inv() }), name: 'Invite: default', trigger: '"Invite user" in the page header, or the bottom button on mobile.' }),
|
||||
withM({ ...U({ modal: inv({ fields: invF('sam.oliveira@renew', { err: 'Enter a full email address, like name@example.com.' }) }) }), name: 'Invite: validation', trigger: 'On submit or blur. Nothing is sent.' }),
|
||||
withM({ ...U({ modal: inv({ loading: true, confirm: 'Sending…', fields: invF('lea.martin@renew.example') }) }), name: 'Invite: sending', trigger: 'Request in flight. The field is disabled and the dialog can\'t be dismissed.' }),
|
||||
withM({ ...U({ modal: inv({ fields: invF('jonas@renew.example', { err: 'This address already has an account.' }) }) }), name: 'Invite: already a user (409)', trigger: 'The email belongs to an existing user. For a live pending invite the message reads "There\'s already a pending invite for this address." An expired invite can be sent again.' }),
|
||||
withM({ ...U({ modal: inv({ notice: 'Couldn\'t send the invite. Try again in a moment.', fields: invF('lea.martin@renew.example') }) }), name: 'Invite: failed', trigger: '500, network failure, or the email couldn\'t be sent. The address stays in the field.' }),
|
||||
withM({ ...U({ users: { rows: [{ email: 'lea.martin@renew.example', st: 'pending', c: 'Today', hl: true }, ...people] } , toasts: [T('success', 'circle-check', 'Invite sent', 'lea.martin@renew.example has 3 days to accept.')] }), name: 'Invite sent', trigger: '201: the dialog closes, the new invite appears at the top as Pending, and a toast confirms.' })
|
||||
] },
|
||||
{ label: 'Profile', title: 'Profile', route: '/profile', api: 'GET /v1/auth/me · PATCH /v1/user/me (pending) · POST /v1/auth/password-reset', note: 'Opened from the account menu at the bottom of the sidebar. Username, email and password are read-only. The display name is optional, trimmed, and up to 60 characters. Once saved it replaces the username everywhere a person is named: the sidebar, the account menu, the Home greeting, the Users list (with the username beside it) and the emails RenewCMS sends, such as "Marta Alves invited you to RenewCMS". Saving it empty goes back to the username. The reset link uses the same flow as "Forgot password" and ends on the auth board\'s "Choose a new password" screen.', states: [
|
||||
withM({ ...home, home: { sub: 'Here\'s where things stand on your site.', stats, recent }, userMenu: true, name: 'Account menu', trigger: 'The account chip at the bottom of the sidebar. On mobile, the account row in the navigation drawer opens it as a sheet. Escape or a click outside closes it.' }),
|
||||
P({ loading: true }, { name: 'Loading', trigger: 'Opened directly by URL before /auth/me has returned. From the menu, the cached user renders straight away.' }),
|
||||
P({}, { name: 'Default, no display name', trigger: 'The usual first visit. The username is used everywhere.' }),
|
||||
P({ value: dname, focus: true, dirty: true }, { name: 'Editing', trigger: 'Typing in the field. Save turns on, and leaving the page asks for confirmation.' }),
|
||||
P({ value: 'Marta Alves, Head of Renovations, Brussels and Wallonia office', focus: true, dirty: true, err: 'Use 60 characters or fewer.' }, { name: 'Too long', trigger: 'Over 60 characters. Checked as you type, and Save stays off.' }),
|
||||
P({ value: dname, dirty: true, saving: true }, { name: 'Saving', trigger: 'Save, or Enter in the field. The field is disabled.' }),
|
||||
P({}, { display: dname, name: 'Saved', trigger: '200: the sidebar and account menu switch to the display name at once, and a toast confirms.', toasts: [T('success', 'circle-check', 'Display name saved', 'You now appear as Marta Alves across RenewCMS and in emails.')] }),
|
||||
P({ value: dname, dirty: true, notice: 'Couldn\'t save your display name. Your text is still here. Try again in a moment.' }, { name: 'Save failed', trigger: '500 or network failure. The typed value is kept.' }),
|
||||
P({ reset: 'sending' }, { display: dname, name: 'Reset: sending', trigger: '"Send reset link". No confirmation, since it only sends an email.' }),
|
||||
P({ reset: 'sent' }, { display: dname, name: 'Reset: sent', trigger: '200. The message stays until the page is left. "Send again" sends a new link and the old one stops working.' }),
|
||||
P({ reset: 'limited' }, { display: dname, name: 'Reset: too many requests (429)', trigger: 'Rate limited. The button is disabled for 5 minutes.' }),
|
||||
P({ reset: 'error' }, { display: dname, name: 'Reset: failed', trigger: '500 or network failure. The button can be pressed again.' }),
|
||||
withM({ ...U(), display: dname, name: 'Display names in the Users list', trigger: 'Accounts with a display name show it first, with the username beside it in grey. Accounts without one show the username.' })
|
||||
] },
|
||||
{ label: 'Global', title: 'Global states', route: 'any route', api: '', note: 'A 401 on any back-office call redirects to /login?redirect=<path>&reason=expired (see "Session expired" on the auth board). A 403 from an unverified account redirects to /register/pending. A 429 appears only on log in and password reset. Toasts sit bottom right on desktop and bottom center on mobile, at most 3 at once. Success and info last 5 s; errors stay until dismissed.', states: [
|
||||
withM({ kind: 'error', nav: '', crumbs: ['Not found'], mTitle: 'Not found', name: '404 inside the shell', trigger: 'An unknown route while logged in. Logged out, the same content appears inside AuthShell.', err: { code: '404', title: 'Page not found', text: 'The link may be mistyped, or the page may have moved.', primary: 'Go to Home', secondary: 'All articles' } }),
|
||||
withM({ kind: 'error', nav: 'home', crumbs: ['Home'], mTitle: 'Home', name: 'Generic error', trigger: 'An unexpected error while rendering a route.', err: { code: 'Error', title: 'Something went wrong', text: 'This page couldn\'t load. Your saved work is safe. Try again, and if it keeps happening, restart the RenewCMS server.', primary: 'Try again', secondary: 'Go to Home' } }),
|
||||
|
||||
Reference in new issue
Block a user