Files
RenewCMS/docs/design/screens/users.md
T

5.7 KiB

Users

  • Route: /users
  • API: GET /v1/user/ · POST /v1/user/invite
  • Visual reference: source/RenewCMS Screens Back-office.dc.html, group "Users" (open it in a browser; each state is drawn at desktop and mobile, with a light/dark toggle).

One shared workspace: every user sees the same articles and this same list. Rows have no actions yet, so nobody can be removed, promoted or have an invite revoked from here. 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. The list is sorted by created date, newest first.

The list

Column Content
Username An accepted user: the display name if there is one (medium weight, highlighted) with the username beside it in muted text; otherwise the username alone. A pending or expired invite: the email address, regular weight, toned. The current user's row carries a muted mono "you" tag.
Invite status A badge: Accepted (circle-check, success), Pending (mail, warning), Expired (clock-alert, outline, muted). Shown by icon and word as well as color.
Created The date as 27 Sep 2026, or "Today" for the current day. For an invite it is the day the invite was sent, for a user the day the account was created.

Desktop is a table: 36 px header, 44 px rows, columns minmax(0,1fr) 160px 160px. Below md it becomes a card list (68 px minimum row height): the name line, then the badge and "Created {date}". The page header is a title "Users" with the subtitle "Everyone who can sign in and work on your articles." and a primary "Invite user" button (user-plus icon) on its right. On mobile the button becomes a full-width 48 px button pinned to the bottom of the screen.

The admin who registered first is always listed, so the list is never empty.

States

Default

Trigger: The list loaded, newest first.

  • Breadcrumbs: Users (mobile title "Users")
  • Rows: sam.oliveira@renew.example Pending, 27 Sep 2026 · p.huber@gmail.com Expired, 3 Aug 2026 · ines (display name "Inès Duarte") Accepted, 18 Jun 2026 · jonas Accepted, 2 Apr 2026 · marta (you) Accepted, 12 Mar 2026

Loading

Trigger: The first load, or a retry. Skeletons appear after 150 ms.

  • Breadcrumbs: Users (mobile title "Users")
  • Five skeleton rows

Only you

Trigger: Nobody has been invited yet. The admin who registered is always listed.

  • Breadcrumbs: Users (mobile title "Users")
  • One row: marta (you) Accepted
  • Hint under the list: "Invite someone so they can sign in with their own account instead of sharing yours."

Error

Trigger: A 500 or network failure on the list request.

  • Breadcrumbs: Users (mobile title "Users")
  • Alert (error): Couldn't load users "Check your connection and try again."
  • Action "Try again" (rotate-cw)
  • No table or cards

Invite: default

Trigger: "Invite user" in the page header, or the bottom button on mobile.

  • Breadcrumbs: Users (mobile title "Users")
  • Modal: Invite a user
    • text "We'll email them a link to choose a username and password. The link expires after 3 days."
    • field Email: placeholder "name@example.com"; focused
    • close button
    • cancel "Cancel"
    • confirm "Send invite"

Invite: validation

Trigger: On submit or blur. Nothing is sent.

  • Breadcrumbs: Users (mobile title "Users")
  • Modal: Invite a user (same text)
    • field Email: value "sam.oliveira@renew"; error "Enter a full email address, like name@example.com."
    • cancel "Cancel"
    • confirm "Send invite"

Invite: sending

Trigger: Request in flight. The field is disabled and the dialog can't be dismissed.

  • Breadcrumbs: Users (mobile title "Users")
  • Modal: Invite a user (same text)
    • field Email: value "lea.martin@renew.example"; disabled
    • cancel "Cancel"
    • confirm "Sending…", loading, not dismissible

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.

  • Breadcrumbs: Users (mobile title "Users")
  • Modal: Invite a user (same text)
    • field Email: value "jonas@renew.example"; error "This address already has an account."
    • cancel "Cancel"
    • confirm "Send invite"

Invite: failed

Trigger: 500, network failure, or the email couldn't be sent. The address stays in the field.

  • Breadcrumbs: Users (mobile title "Users")
  • Modal: Invite a user (same text)
    • field Email: value "lea.martin@renew.example"
    • Alert: "Couldn't send the invite. Try again in a moment."
    • cancel "Cancel"
    • confirm "Send invite"

Invite sent

Trigger: 201: the dialog closes, the new invite appears at the top as Pending (highlighted row), and a toast confirms.

  • Breadcrumbs: Users (mobile title "Users")
  • New first row: lea.martin@renew.example Pending, created "Today", row background --ui-bg-elevated
  • Toast (success, circle-check): Invite sent "lea.martin@renew.example has 3 days to accept."

The "Today" label makes this state clock-sensitive, so task visual:check skips it.

Notes for implementation

  • An accepted user is shown by username and display name only, never by email.
  • How display names look in this list is the state "Display names in the Users list" of profile.md. The board draws it in the Profile group, and task visual:check finds states by group.
  • 429 on the invite request is not drawn. Decide it in the spec before coding.
  • The email the invite sends is specified in emails.md. The screen the link opens is in accept-invite.md.