chore: updated reference design to app changes and fixes

This commit is contained in:
Florian Sylvain committed 2026-10-29 18:07:06 +01:00
1 parent 8cdd70bd81
commit c6d49db95e
25 files changed
+287 -104

No files matched your search

@@ -286,7 +286,7 @@ class Component extends DCLogic {
Mp({ empty: 'first', name: 'Empty library, first run', trigger: 'Zero images. Toolbar and tabs are hidden; one call to action. Copy: "No images yet" / "Upload your first image. You can use it in any article." / "Upload an image".' }),
Mp({ tab: 'unused', q: 'wall', empty: 'search', name: 'No results', trigger: 'Search or filter matched nothing. "Clear filters" resets the search and goes back to All.' }),
Mp({ hover: 2, focus: 3, ann: 'aria-label: kitchen-before.webp, 1.1 MB, used in 1 article', name: 'Hover, focus and keyboard', trigger: 'Hover (second item) darkens the border, adds the pop shadow and reveals the checkbox. Keyboard focus (third item) shows the 2 px primary ring with a 2 px offset and reveals the checkbox. Tab moves between items, Enter opens the detail panel, Space toggles selection, and arrow keys move focus across the grid.' }),
Mp({ sel: [1, 2, 8], name: 'Selection and bulk bar', trigger: 'Checkbox on any item, or Space. Selected items take the sand fill with a primary outline, and Select all shows a partial state. The bar is sticky at the bottom (above the bottom edge on mobile, replacing the Upload button). Announced: "3 selected". Escape or Clear empties it.' }),
Mp({ sel: [1, 2, 8], name: 'Selection and bulk bar', trigger: 'Checkbox on any item, or Space. Selected items take the sand fill with a primary outline, and Select all shows a partial state. Outline badges ("Used in N", "Draft") keep an opaque --ui-bg fill over the sand tint with the text colour unchanged (--ui-text-toned on --ui-bg: 9.7:1 light, 11.7:1 dark; both above 4.5:1). The bar is sticky at the bottom (above the bottom edge on mobile, replacing the Upload button). Announced: "3 selected". Escape or Clear empties it.' }),
Mp({ sel: [1, 2, 8], modal: { k: 'bulk' }, name: 'Bulk delete: confirm', trigger: 'Delete in the bulk bar. The dialog says how many of the selection are in use and lists them. The confirm button carries the count.' }),
Mp({ sel: [1, 2, 8], modal: { k: 'bulk', loading: true }, name: 'Bulk delete: in progress', trigger: 'Requests in flight. The dialog can\'t be dismissed.' }),
Mp({ ids: [3, 4, 5, 6, 7, 8, 9, 10, 11, 12], sel: [8], toasts: [TT('error', 'circle-x', 'Couldn\'t delete 1 image', '2 images were deleted. window-frame.jpg is still in your library. Try again.', 'Retry')], name: 'Bulk delete: partly failed', trigger: 'Some requests failed. Deleted images leave the grid, failed ones stay selected, and the toast stays until retried or dismissed.' }),
@@ -294,6 +294,10 @@ class Component extends DCLogic {
Mp({ panel: { id: 1 }, name: 'Detail panel: in use', trigger: 'Click or Enter on an item. Right-hand slideover on desktop, full screen on mobile with a back arrow. Actions: Copy URL, Rename, Delete. "Used in 2 articles" lists title, Online or Draft, and links to the editor. Focus returns to the item on close.' }),
Mp({ panel: { id: 8 }, name: 'Detail panel: unused', trigger: 'Used in nothing. The section reads "Not used in any article" and says the image is safe to delete.' }),
Mp({ panel: { id: 12 }, name: 'Detail panel: duplicate', trigger: 'The image has identical copies. An info alert links to the duplicates view.' }),
Mp({ panel: { id: 8, ext: 'avatar' }, name: 'Detail panel: used elsewhere, avatar only', trigger: 'window-frame.jpg has no article or entry, and is the avatar of one author. "Used elsewhere" lists "Author avatar" linking to /authors. The "Used in 1" badge counts it, so "Not used in any article" is replaced by "Articles" with "No article shows it."' }),
Mp({ panel: { id: 8, ext: 'site' }, name: 'Detail panel: used elsewhere, site image only', trigger: 'The image is the site default social image. The row "Site default social image" links to /settings/sites. Badge reads "Used in 1".' }),
Mp({ panel: { id: 8, ext: 'both' }, name: 'Detail panel: used elsewhere, both', trigger: 'Both rows, avatar first. Badge reads "Used in 2".' }),
Mp({ panel: { id: 8, ext: 'none' }, name: 'Detail panel: used elsewhere, none', trigger: 'No section. Badge reads "Unused" and "Not used in any article" with "This image is safe to delete." appears, never next to a badge of 1 or more.' }),
Mp({ panel: { id: 1 }, toasts: [TT('success', 'circle-check', 'URL copied', 'Paste it wherever you need the image address.')], name: 'Copy URL', trigger: 'Copy URL. Clipboard write succeeded. Success toasts last 5 s.' }),
Mp({ panel: { id: 1 }, modal: { k: 'rename', val: 'north-wall' }, name: 'Rename: dialog', trigger: 'Rename in the panel. The field holds the name without its extension, and the .jpg ending is fixed. The description says the URL stays the same.' }),
Mp({ panel: { id: 1 }, modal: { k: 'rename', val: 'north-wall-01', loading: true }, name: 'Rename: saving', trigger: 'PATCH in flight. Field and buttons disabled, dialog can\'t be dismissed.' }),
@@ -792,7 +796,7 @@ class Component extends DCLogic {
const T = (cols, tpl, rows, o = {}) => ({ cols, tpl, rows: rows.map(r => r.map((t, i) => this.cell(t, i, { mono: o.mono && o.mono.includes(i), muted: t === '–' }))) })
return [
{ label: 'Request contract', title: '7. Public request contract', note: 'Shown as the "How requests are handled" card in section 1. Only variants that exist and are enabled are served. There is no arbitrary-size mode.', tables: [T(['Request', 'Status', 'Body or result'], '320px 80px minmax(0,1fr)', [
['/media/photo.png?v=thumb', '200', 'The cached image. Generated on first request.'],
['/static/uploadedImages/photo.png?v=thumb', '200', 'The cached image. Generated on first request.'],
['/media/photo.png?v=unknown', '400', 'Unknown variant'],
['/media/photo.png?v=hero (disabled)', '400', 'Unknown variant'],
['/media/photo.png?width=400', '400', 'Unsupported parameter'],
@@ -952,14 +956,23 @@ class Component extends DCLogic {
S('Create author: dialog', '"Create author…" in the dropdown, owner and admin only. The writer stays in the editor.', 'Create author · Adds a byline for this article. You can add a bio and avatar later in Authors. · Name · Slug · Zoe Lambert · zoe-lambert · Cancel · Create author', { v: 'editor', metaOpen: true, meta: { authors: ['Inès Duarte'] }, modal: { name: 'Zoe Lambert', slug: 'zoe-lambert' } }),
S('Create author: for me', 'Owner and admin only. When the user has no author profile, a checkbox links the new author to them.', 'This is me · Links the author to marta@renew.example.', { v: 'editor', metaOpen: true, meta: { authors: ['Inès Duarte'], hint: true }, modal: { name: 'Marta Alves', slug: 'marta-alves', me: true } }),
S('Create author: slug taken', 'The slug exists. The dialog suggests a free one.', 'The slug "zoe-lambert" is already used by another author. · Use zoe-lambert-2', { v: 'editor', metaOpen: true, meta: { authors: ['Inès Duarte'] }, modal: { name: 'Zoe Lambert', slug: 'zoe-lambert', err: true, slugErr: 'The slug "zoe-lambert" is already used by another author.', suggest: 'zoe-lambert-2' } }),
S('Create author: failed', '500 or network failure.', 'Couldn\'t create the author · The article is unchanged. Try again in a moment.', { v: 'editor', metaOpen: true, meta: { authors: ['Inès Duarte'] }, modal: { name: 'Zoe Lambert', slug: 'zoe-lambert', notice: true } })
S('Create author: failed', '500 or network failure. On mobile the dialog sits above the Details sheet; the sheet stays behind it.', 'Couldn\'t create the author · The article is unchanged. Try again in a moment.', { v: 'editor', metaOpen: true, meta: { authors: ['Inès Duarte'] }, modal: { name: 'Zoe Lambert', slug: 'zoe-lambert', notice: true } }),
S('Collapsed summary: authors', 'The summary under Details when the section is closed. Three authors show the first name and a count.', 'Inès Duarte +2', { v: 'editor', ed: { metaSummary: 'Inès Duarte +2' } }),
S('Collapsed summary: none', 'No authors.', 'No authors', { v: 'editor', ed: { metaSummary: 'No authors' } }),
S('Collapsed summary: site and author', 'Several sites: site name, then the first author.', 'Journal · Inès Duarte', { v: 'editor', ed: { metaSummary: 'Journal · Inès Duarte' } }),
S('Collapsed summary: site, no authors', 'Several sites and no author.', 'Journal · No authors', { v: 'editor', ed: { metaSummary: 'Journal · No authors' } }),
S('Reorder: dropped', 'Space on the grabbed row drops it. The live region (polite) announces the result.', 'Jonas Pereira dropped. Position 2 of 3.', { v: 'editor', metaOpen: true, meta: { ...{"authors":["Inès Duarte","Jonas Pereira","Camille Roche"]}, live: 'Jonas Pereira dropped. Position 2 of 3.' } }),
S('Reorder: cancelled', 'Escape returns the row to where it was.', 'Reorder cancelled. Jonas Pereira is back at position 3 of 3.', { v: 'editor', metaOpen: true, meta: { ...{"authors":["Inès Duarte","Jonas Pereira","Camille Roche"]}, live: 'Reorder cancelled. Jonas Pereira is back at position 3 of 3.' } })
]),
G('Profile: author profile', 'Profile: author profile', '/profile', 'POST /v1/author/me · GET /v1/auth/me (author field)', 'The "Author profile" card sits between Account and Password in every Profile frame, defined once in AppFrame. The existing Profile frames show its default state ("Create my author profile"). All roles can create their own profile and edit their own linked author. Only the states specific to the card are drawn here.', [
S('Author profile created: toast', 'Hint-success toast after Create my author profile from the Details hint. The author is added to the article.', 'Author profile created · Marta Alves is now your byline.', { v: 'editor', metaOpen: true, meta: { authors: ['Marta Alves'] }, toasts: [['success', 'circle-check', 'Author profile created', 'Marta Alves is now your byline.']] }),
S('No author profile', 'The user has no linked author.', 'Author profile · Your public byline on articles. It\'s separate from your account. · You don\'t have an author profile yet. Create one to be credited on your own articles. · Create my author profile · Uses your display name, Marta Alves. You can change it afterwards.', { v: 'profile', pa: { k: 'none' } }),
S('No author profile, no display name', 'The user has no linked author and no display name.', 'Uses your username, marta. You can change it afterwards.', { v: 'profile', pa: { k: 'none', user: true } }),
S('Error', '500 or network failure.', 'Couldn\'t create your author profile · Try again in a moment.', { v: 'profile', pa: { k: 'none', err: true } }),
S('Created', '201: the card shows the author. The author is named from the display name and linked to the user.', 'Author profile created · Marta Alves is now your byline. · Edit author · Linked to your account. New articles start with this author.', { v: 'profile', pa: { k: 'has' }, toasts: [['success', 'circle-check', 'Author profile created', 'Marta Alves is now your byline.']] }),
S('Created, slug adjusted', 'The slug was taken, so the server added a number. No error is shown.', 'Your slug is marta-alves-2 · marta-alves was already used by another author. You can change it in Edit author.', { v: 'profile', pa: { k: 'has', slug: 'marta-alves-2' } }),
S('Edit own author', 'Edit author on the card, by a role without author rights. The form opens with the linked user locked and no Unlink. There is no delete action in the form or on this screen.', 'Edit Marta Alves · Linked user · marta@renew.example · This is your author profile. Only an owner or admin can change the link. · Save changes', { v: 'profile', role: 'user', pa: { k: 'has' }, panel: { k: 'author', mode: 'edit', own: true, f: { name: 'Marta Alves', slug: 'marta-alves', slugMode: 'saved', bio: 'Project coordinator. Writes about permits and planning.', user: 'marta@renew.example' } } })
S('Edit own author', 'Edit author on the card, by a role without author rights. The form opens with the linked user locked and no Unlink. There is no delete action in the form or on this screen.', 'Edit Marta Alves · Linked user · marta@renew.example · This is your author profile. Only an owner or admin can change the link. · Save changes', { v: 'profile', role: 'user', pa: { k: 'has' }, panel: { k: 'author', mode: 'edit', own: true, f: { name: 'Marta Alves', slug: 'marta-alves', slugMode: 'saved', bio: 'Project coordinator. Writes about permits and planning.', user: 'marta@renew.example' } } }),
S('Edit own author: owner or admin', 'Edit author on the card, by an owner or admin. The form opens with the linked user and Unlink is allowed.', 'Edit Marta Alves · Linked user · marta@renew.example · Unlink · Save changes', { v: 'profile', role: 'owner', pa: { k: 'has' }, panel: { k: 'author', mode: 'edit', own: false, f: { name: 'Marta Alves', slug: 'marta-alves', slugMode: 'saved', bio: 'Project coordinator. Writes about permits and planning.', user: 'marta@renew.example' } } })
]),
G('Articles: sites and authors', 'Articles: sites and authors', '/articles?site=journal', 'GET /v1/article/?site={key}&page=&size=&q=&status=', 'Every Articles frame has the Authors column (avatars, at most 3 plus "+N"; "—" when none; on mobile avatars with the first name). When more than one site exists, a Site column and a Site filter appear, read-only text for every role; with one site they are not rendered.', [
S('Several sites', 'More than one site exists.', 'Authors · Site · All sites · Renew · Journal · Projects', { v: 'app', list: { multi: true } }),
@@ -969,7 +982,14 @@ class Component extends DCLogic {
G('Articles: search status', 'Articles: search status', '/articles?seo=suggestions', 'GET /v1/article/?seo=suggestions&page=&size= (items carry seo_status and seo_suggestions)', 'A quiet indicator in the Status cell on desktop and under the title on mobile cards: a lightbulb chip with a count when there are suggestions, an eye-off chip "Hidden" when noindex is on, nothing when the article is ready. The toolbar gets a "Needs attention" toggle with a count next to the search field. Tabs and their counts are unchanged. On a single-site install nothing changes: the Site filter and column stay hidden and the toggle works the same, over all articles.', [
S('Status chips', 'Row 2 has 2 suggestions, row 3 is hidden, row 4 has 3, row 5 has 1.', 'Needs attention · 3 · Hidden · 2 suggestions (tooltip) · Hidden from search engines (tooltip)', { v: 'app', list: {} }),
S('Needs attention on', 'The toggle is pressed. The list shows only articles with suggestions. Hidden-only articles are not included.', 'Needs attention · 3', { v: 'app', list: { needs: true } }),
S('Needs attention: none', 'Nothing to show with the toggle on.', 'Nothing needs attention · Every article has a clean check. Suggestions are optional, so this is just a bonus. · Show all articles', { v: 'app', list: { needs: true, needsN: 0, counts: [0, 0, 0], empty: { icon: 'circle-check', title: 'Nothing needs attention', text: 'Every article has a clean check. Suggestions are optional, so this is just a bonus.', action: 'Show all articles' } } })
S('Needs attention: none', 'Nothing to show with the toggle on.', 'Nothing needs attention · Every article has a clean check. Suggestions are optional, so this is just a bonus. · Show all articles', { v: 'app', list: { needs: true, needsN: 0, counts: [0, 0, 0], empty: { icon: 'circle-check', title: 'Nothing needs attention', text: 'Every article has a clean check. Suggestions are optional, so this is just a bonus.', action: 'Show all articles' } } }),
S('Count chip: 0', 'Zero suggestions: the toggle shows a 0 chip.', 'Needs attention · 0', { v: 'app', list: { needsN: 0 } }),
S('Count chip: absent', 'The count is not known yet, so the chip is not drawn.', 'Needs attention', { v: 'app', list: { naState: 'absent' } }),
S('Count chip: pending', 'The count request is in flight.', 'Needs attention · …', { v: 'app', list: { naState: 'pending' } }),
S('Count chip: failure', 'The count request failed. The toggle still works and the chip shows !.', 'Needs attention · !', { v: 'app', list: { naState: 'fail' } }),
S('Empty: suggestions + search', 'Precedence: search wins. The empty state names the search; Needs attention stays on.', 'No articles match "kitchen" · Try another search or clear it. · Clear search', { v: 'app', list: { needs: true, counts: [0, 0, 0], empty: { icon: 'search', title: 'No articles match "kitchen"', text: 'Try another search or clear it.', action: 'Clear search' } } }),
S('Empty: suggestions + tab', 'A tab other than All with Needs attention on and no match: the tab wins.', 'No drafts need attention · Switch to All to see the rest. · Show all articles', { v: 'app', list: { needs: true, counts: [0, 0, 0], empty: { icon: 'file-text', title: 'No drafts need attention', text: 'Switch to All to see the rest.', action: 'Show all articles' } } }),
S('Empty: suggestions + site', 'A site filter with Needs attention on and no match: the site wins.', 'No articles on "Archive" need attention · Show all sites', { v: 'app', list: { multi: true, site: 'Archive', needs: true, counts: [0, 0, 0], empty: { icon: 'file-text', title: 'No articles on "Archive" need attention', text: 'Choose another site or clear the filter.', action: 'Show all sites' } } })
]),
G('Editor: search and sharing', 'Editor: search and sharing', '/article/:id/edit', 'PATCH /v1/article/{id} {slug, excerpt, seo_title, seo_description, cover_image_id, cover_image_alt, social_title, social_description, social_image_id, canonical_url, noindex, seo_dismissed[]}', 'RenewCMS is headless: it never renders pages or adds meta tags. This section helps the writer fill in SEO data and shows what the frontend will receive. Everything is optional and each empty field shows its fallback. Placement: a "Search and sharing" section under "Site and authors" in the sidebar and in the mobile Details sheet. Collapsed by default with a one-line summary (Ready, N suggestions, Hidden from search), remembered per browser. The fields and live previews open in a slide-over (desktop, 960 px) or a 92% sheet over the Details sheet (mobile). Anyone who can edit the article can edit these fields. Suggestions never block saving or publishing: publishing is allowed regardless. Counters are soft guides and turn amber, never red, outside the recommended range. Light and dark follow the theme switch above.', [
S('Collapsed: ready', 'The default. Nothing to suggest.', 'Search and sharing · Ready', { v: 'editor', ed: { seoSummary: 'Ready' } }),
@@ -995,12 +1015,20 @@ class Component extends DCLogic {
S('Panel: checking', 'Checks run on open and after each pause in typing.', 'Checking this article…', { v: 'editor', ed: { seoSummary: 'Checking…' }, seo: { panel: { sugState: 'loading' } } }),
S('Panel: saving', 'Fields are disabled while the request runs.', 'Saving…', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { panel: { state: 'saving', mt: 'What a bathroom renovation costs', codes: [] } } }),
S('Panel: save failed', '500 or network failure. The values stay.', 'Couldn\'t save search and sharing. Your changes are still here. Try again in a moment.', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { panel: { notice: 'Couldn\'t save search and sharing. Your changes are still here. Try again in a moment.', mt: 'What a bathroom renovation costs', codes: [] } } }),
S('Check failed', 'The suggestion check returned an error. The rest of the section works.', 'Couldn\'t check this article. · Try again', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { side: { state: 'error' } } }),
S('Panel: check failed', 'Same state inside the panel.', 'Couldn\'t check this article. · Try again', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { panel: { sugState: 'error' } } }),
S('Panel: dismiss busy', 'The × shows a spinner and the row dims while the request runs.', 'Dismiss (busy)', { v: 'editor', ed: { seoSummary: '2 suggestions' }, seo: { panel: { codes: ['title_long', 'cover_alt'], busy: 0 } } }),
S('Panel: dismiss failed', 'The row stays. An inline error with Try again appears under it.', 'Couldn\'t dismiss the suggestion · Try again in a moment.', { v: 'editor', ed: { seoSummary: '2 suggestions' }, seo: { panel: { codes: ['title_long', 'cover_alt'], fail: 0 } } }),
S('Panel: show dismissed', 'All suggestions dismissed. The line says how many; Show dismissed brings them back.', 'All good · 2 dismissed suggestions are hidden. · Show dismissed', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { panel: { codes: [], dismissed: 2 } } }),
S('Panel: excerpt over 300', 'Inline error under the excerpt; the counter turns red. Save changes is disabled while it shows, and also while nothing changed.', 'The excerpt is 342 characters. Keep it to 300 or fewer. · 342 / 300', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { panel: { ex: 'x'.repeat(342).replace(/x/g, 'a'), exErr: 'The excerpt is 342 characters. Keep it to 300 or fewer.', noChange: true, codes: [] } } }),
S('Panel: nothing changed', 'Save changes is disabled until a field differs from the saved value.', 'Save changes (disabled)', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { panel: { noChange: true, codes: [] } } }),
S('Saved', 'The panel closes. The summary updates.', 'Search and sharing saved · Your search and sharing settings are up to date.', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { side: { codes: [] } }, toasts: [['success', 'circle-check', 'Search and sharing saved', 'Your search and sharing settings are up to date.']] })
]),
G('Editor: what the frontend gets', 'Editor: what the frontend gets', '/article/:id/edit', 'GET /v1/article/{id}/seo', 'A read-only dialog (desktop, 720 px) or bottom sheet (mobile) with the resolved SEO object exactly as the API returns it. Opened from the sidebar section or the panel. Each value that came from a fallback carries a chip. Formatted shows a compact key and value list, Raw shows the JSON. Copy JSON copies the raw object. Author name, profile URL and social links come from the Authors screen and surface in authors[] and in the JSON-LD author, so Authors itself needs no change.', [
S('Formatted', 'Default view.', 'What the frontend gets · The SEO data returned by GET /v1/article/12/seo, read only. RenewCMS doesn\'t render pages or add meta tags: your frontend does. · Formatted · Raw JSON · Copy JSON · Close', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { side: { codes: [] }, api: {} } }),
S('Raw JSON', 'The JSON as the API returns it.', 'Raw JSON', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { side: { codes: [] }, api: { view: 'raw' } } }),
S('Copied', 'The button reads Copied for 2 s and this is announced.', 'Copied', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { side: { codes: [] }, api: { view: 'raw', copied: true } } }),
S('Copy refused', 'navigator.clipboard was refused. The Raw JSON view opens, the text is selected and a note explains how to copy by hand. The button keeps reading Copy JSON.', 'The browser blocked copying. The JSON is selected below: press Ctrl+C or Cmd+C to copy it.', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { side: { codes: [] }, api: { view: 'raw', copyFail: true } } }),
S('Draft article', 'Drafts answer 404 to anonymous callers.', 'This article is a draft. The API returns this data only to signed-in callers until it is published.', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { side: { codes: [] }, api: { draft: true } } }),
S('Loading', 'Copy is disabled.', '', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { side: { codes: [] }, api: { state: 'loading' } } }),
S('Error', 'GET failed. Nothing in the article changes.', 'Couldn\'t load the SEO data · GET /v1/article/12/seo failed. Your article is unchanged. Try again in a moment. · Try again', { v: 'editor', ed: { seoSummary: 'Ready' }, seo: { side: { codes: [] }, api: { state: 'error' } } })