fix: accessibility issues for builder's design

This commit is contained in:
Florian Sylvain committed 2026-10-20 05:11:14 +02:00
1 parent 8e344b2500
commit 76e9e1220c
16 files changed
+290 -147

No files matched your search

+19 -21
View File
@@ -11,12 +11,13 @@ export const groups = [
st('Built-in type: read-only view', N, 'Click Articles in the list. No edit, no delete, no add field.', { v: 'types', k: 'builtin' }, 'Articles · Built-in · Managed by RenewCMS · Articles is a built-in type. You can read its fields here, but you can\'t edit it, delete it or add fields. · Label · Key · Type · Rules · Entries of built-in types are managed from their own screens, like Articles and Authors.')
] },
{ label: '2 Schema builder', title: '2 Schema builder', route: '/schema/new, /schema/:key', api: 'POST /v1/content-type, GET/PUT /v1/content-type/{key}', auth: 'Owner or admin', note: 'One screen, three regions: type name and key on top, ordered field list on the left, field inspector on the right, collapsible API preview at the bottom. Mobile: the list is the page, the inspector is a 92% sheet, the API preview is a sheet. Field keys and types of saved fields are locked; fields added since the last save can still choose both.', states: [
st('New type', N, '/schema/new. One starter field is added because a type needs at least one.', { v: 'builder', k: 'new' }, 'New content type · Name · For example, Events · Key · events · Used in URLs and the API. Choose it carefully: it can\'t change after you save. · Fields · 1 of 50 · Add field · Field: Title · Label · Key · The name used in the API. Lowercase letters, digits and underscores. · Type · Required · An entry can\'t be saved with this field empty. · Unique values · Default value · Used when an entry is created and this field is left out. · Shortest · Longest · Counted in characters. Entries show a counter. · Save · Discard'),
st('New type', N, '/schema/new. One starter field is added because a type needs at least one.', { v: 'builder', k: 'new' }, 'New content type · Name · For example, Events · Key · events · Used in URLs and the API. Lowercase letters, digits and hyphens. It can\'t change after you save. · Fields · 1 of 50 · Add field · Field: Title · Label · Key · The name used in the API. Starts with a letter. Lowercase letters, digits and underscores. · Type · Required · An entry can\'t be saved with this field empty. · Unique values · Default value · Used when an entry is created and this field is left out. · Shortest · Longest · Counted in characters. Entries show a counter. · Save · Discard'),
st('Editing an existing type', N, '/schema/events with 37 entries. Key is locked, saved field keys and types are locked.', { v: 'builder', k: 'edit' }, '37 entries use this content type. Changes are checked against them when you save. · Key (lock) · Used in the API, so it can\'t change. · Can\'t change after saving · Saved'),
st('Locked key: tooltip', N, 'Hover or focus the locked key.', { v: 'builder', k: 'locked' }, 'The key is used in URLs and the API, so it can\'t change after the type is created.'),
st('Unsaved changes', N, 'Two fields added, Capacity changed, Description removed. Rows carry New and Changed badges; removal shows an undo row.', { v: 'builder', k: 'dirty' }, 'Unsaved changes · New · Changed · Description will be removed · Undo · Save · Discard'),
st('Field errors', N, 'Checked as the user types, and again on Save. The row is outlined and shows an alert icon. Save is disabled until all are fixed.', { v: 'builder', k: 'ferr' }, '3 problems to fix · Give the field a label. · Lowest value can\'t be above the highest value. · Choices can\'t be blank. · Two choices are the same.'),
st('Type-level errors', N, 'New type with no name, a one-letter key and no fields.', { v: 'builder', k: 'terr' }, 'Give the content type a name. · Use 2 to 40 lowercase letters, digits or hyphens. · No fields yet · A content type needs at least one field. · 3 problems to fix'),
st('Field key error', N, 'A field added since the last save has an invalid key (hyphen). Checked as the user types. Field keys use underscores; type keys use hyphens.', { v: 'builder', k: 'fkey' }, 'Key · Start with a letter. Use lowercase letters, digits and underscores. · 1 problem to fix'),
st('Choice list editor', N, 'A Choice field is selected. Add, remove and reorder; blanks and duplicates are flagged per row.', { v: 'builder', k: 'choices', sheet: true }, 'Choices · 5 of 50 · Add a choice · Add · Add 1 to 50 choices. Each one needs a different name. Removing a choice that entries use will be refused when you save. · Choices can\'t be blank. · Two choices are the same.'),
st('Keyboard reorder', N, 'Focus the grip, press Space. Up and Down move, Space drops, Esc cancels. Move up, Move down and Remove buttons are on the selected row.', { v: 'builder', k: 'kb' }, 'Moving Ends on. Position 2 of 7. Press Up or Down to move, Space to drop, Esc to cancel. (live region, polite)'),
st('API preview open', N, 'The panel at the bottom. Updates as fields change. Copy JSON copies the sample entry.', { v: 'builder', k: 'api' }, 'API preview · Where entries of this type live · A sample entry. Values come from each field\'s type and default. · Updates as you edit. Fields you add or remove here appear after you save. · Copy JSON · Copied'),
@@ -37,7 +38,7 @@ export const groups = [
st('Delete type: server fallback (409)', N, '409 content_type_has_entries. An entry was added after the page loaded.', { v: 'review', k: 'fallback' }, 'Recipes now has entries · Someone added an entry after this page loaded, so nothing was deleted. Delete the entries first, then try again. · Close · View entries')
] },
{ label: '4 Field types', title: '4 Field-type gallery', route: 'reference board', api: '', auth: 'n/a', note: 'One frame per type: inspector options, the entry form control (default and open states), the list cell with its empty state, the error message for each rule, and the filter widget. Filters are later: filtering on custom fields is not in the API. Mobile drops the inspector column (the builder frames show it as a sheet).', states: ['text', 'richtext', 'number', 'boolean', 'date', 'select'].map((t, i) => st(['Text', 'Rich text', 'Number', 'Yes or no', 'Date', 'Choice'][i], N, 'type: "' + t + '"', { v: 'gallery', k: t }, ({ text: 'Text · Short text, like a title or a name · Shortest · Longest · characters · 74 / 80 · Fill in Title. · Title must be text. · Title needs at least 3 characters. · Title can be up to 80 characters. · Another entry already uses this Title. Enter a different one.', richtext: 'Rich text · Long text with formatting, written in Markdown · Write · Preview · Markdown. Switch to Preview to check the result. · 86 / 5000 · Not available for rich text. (Unique)', number: 'Number · Whole or decimal numbers · Lowest value · Highest value · Enter a number · Between 1 and 5000. · Description must be a number. · Capacity must be at least 1. · Capacity must be at most 5000.', boolean: 'Yes or no · An on or off switch · On by default · Yes or no fields have no limits. · Not available for yes or no fields. (Unique) · Choose yes or no for Online. · Online must be yes or no.', date: 'Date · A calendar day, without a time · Earliest date · Latest date · Pick a date · Pick a date for Starts on. · Starts on must be a date, like 2026-11-21. · Starts on can\'t be before 1 Jan 2026. · Starts on can\'t be after 31 Dec 2027.', select: 'Choice · One option from a list you define · Choices · Choose an option · Choose one of: Talk, Workshop or Meetup. · Another entry already uses this choice for Category.' })[t])) },
{ label: '5 Content and entry list', title: '5 Content navigation and entry list', route: '/content/:key', api: 'GET /v1/content-type/{key}, GET /v1/content-type/{key}/entry', auth: 'Any verified user', note: 'The sidebar gets a "Content" group, one item per custom type (a default icon: types have no icon in the API). With more than 5 types it shows the first 5 and the selected one, then "Show all N"; long names truncate with the full name in a tooltip. On mobile the group is one collapsed row in the drawer. List: ID, up to 6 chosen field columns (default: first 4), Created, Updated, Updated by. Sort: ID, Created, Updated, either direction (default Updated, newest first). Page size 5, 25, 50, 100 (default 25). No filter or search.', states: [
{ label: '5 Content and entry list', title: '5 Content navigation and entry list', route: '/content/:key', api: 'GET /v1/content-type/{key}, GET /v1/content-type/{key}/entry', auth: 'Any verified user', note: 'The sidebar gets a "Content" group, one item per custom type (a default icon: types have no icon in the API). With more than 5 types it shows the first 5 and the selected one, then "Show all N"; long names truncate with the full name in a tooltip. On mobile the group is one collapsed row in the drawer. List: ID, up to 6 chosen field columns (default: first 4), Created, Updated, Updated by. Sort: ID, Created, Updated, either direction (default Updated, newest first). Page size 5, 25, 50, 100 (default 25). No filter or search. URL: /content/events?page=2&size=50&sort=created_at:desc (defaults omitted). The response carries total, page, size and sort; the summary, the pager and the review dialog use them.', states: [
st('Populated', N, '/content/events. 7 entries, one page: no pager, only the count.', { v: 'entries', k: 'ok' }, 'Content · Events · events · 7 fields · New entry · Sort by · Last updated · Columns · Edit content type · ID · Title · Starts on · Ends on · Online · Created · Updated · Updated by · 7 entries'),
st('Paginated, 5 per page', N, '/content/events?size=5. 37 entries.', { v: 'entries', k: 'paged' }, 'Showing 1–5 of 37 · 5 per page · Page 1 of 8 (mobile)'),
st('Columns menu', N, 'The Columns button. Popover on desktop, sheet on mobile. Created and updated always show.', { v: 'entries', k: 'cols' }, 'Columns · 4 of 6 · Choose up to 6 fields to show. 4 selected. · Created and updated dates always show · Reset · Done'),
@@ -85,7 +86,7 @@ export const tables = [
['2 Schema builder, all states', 'Available now', ''],
['Live API preview', 'Available now', 'Built on the client from the field list'],
['Type-level and field-level validation', 'Available now', 'Same rules as the server, shown while typing'],
['3 Review step and Apply', 'Available now', 'Needs the entry total (see Needs backend)'],
['3 Review step and Apply', 'Available now', 'Uses the total from the entry list response'],
['3 Refusal state, delete type, blocked delete', 'Available now', ''],
['3 Per-change entry counts, full diff', 'Later', 'The API does not return them'],
['4 Field-type gallery', 'Available now', 'Filter widgets inside it are Later'],
@@ -106,7 +107,7 @@ export const tables = [
['/content/:key/new', 'New entry', 'Any verified user', 'POST /v1/content-type/{key}/entry'],
['/content/:key/:id', 'Entry editor', 'Any verified user', 'GET, PUT, DELETE /v1/content-type/{key}/entry/{id}'],
['/settings/permissions (later)', 'Permissions grid', 'Owner or admin', 'Not built'],
['/content/:key query', 'page, size (5, 25, 50, 100; default 25), sort (id, created_at, updated_at) and dir (asc, desc); default updated_at desc', 'Any verified user', 'Defaults omitted from the URL. Parameter names sort and dir are an assumption']] },
['/content/:key query', 'page, size (5, 25, 50, 100; default 25), sort=<id|created_at|updated_at>:<asc|desc>, default updated_at:desc', 'Any verified user', 'Defaults omitted from the URL. Query: page, size, sort=<id|created_at|updated_at>:<asc|desc>, default updated_at:desc. The response carries total, page, size and sort']] },
{ title: 'New components and their Nuxt UI mapping', tpl: '210px 110px 330px minmax(0,1fr)', cols: ['Component', 'Status', 'Nuxt UI', 'Why an existing one does not fit'], rows: [
['SchemaTable', 'New', 'UTable; below 640 px a list of UCard', 'Same pattern as SitesTable. Built-in rows add the lock and a Built-in badge'],
['BuiltInBadge', 'New', 'UBadge outline + lock icon', 'Existing badges have no lock; same size as the Linked badge'],
@@ -148,24 +149,21 @@ export const tables = [
['403', '403', 'Unverified: redirect to /register/pending. Wrong role on /schema: "You don\'t have access to Schema"'],
['400, 500', '', 'Existing table. Saves: "Couldn\'t save the … Something went wrong on our side. Your changes are still here. Try again in a moment."']] },
{ title: 'Needs backend', tpl: '40px minmax(0,1fr)', cols: ['#', 'What the design assumes'], rows: [
['1', 'Total number of entries of a type (X-Total-Count or a body field, as on Articles). Used for pagination, "37 entries use this content type" and the blocked delete.'],
['2', 'Names of the sort and direction parameters on the entry list (drawn as sort and dir).'],
['3', 'Built-in types and their fields. GET /v1/content-type returns custom types only, so the read-only views use static client data, or a new read-only endpoint.'],
['4', 'created_by and updated_by are user ids. Any verified user needs their display names, but the user list is not open to all roles. Needs a name in the entry response or a light lookup.'],
['5', 'A per-type icon does not exist. The sidebar uses one default icon.'],
['6', 'The exact alphabet of the type key ("lowercase slug"). Drawn as letters, digits and hyphens.'],
['7', 'Locked field keys and types of saved fields are a client rule: a PUT with a changed key would remove one field and add another. The server does not reject it.'],
['8', 'No signal that the schema changed while a form was open. The design relies on 422 invalid_entry (required, unknown_field) on save.'],
['9', 'default of false versus no default for a yes or no field: needs null for "no default".'],
['10', 'Later: per-change entry counts and diff, relations, permissions, export and import, filters on custom fields, publish and draft, public read, activity log.']] },
['1', 'Built-in types and their fields. GET /v1/content-type returns custom types only, so the read-only views use static client data (sample counts on the board are illustrative). To be closed in a backend slice before the SPA work.'],
['2', 'Display names for created_by and updated_by. They are user ids and the user list is not open to every role. To be closed in the same slice.'],
['3', 'A per-type icon. It does not exist, so the sidebar uses one default icon. To be closed in the same slice.'],
['4', 'Later: per-change entry counts and diff (the API does not return them), relations, permissions, export and import, filters on custom fields, publish and draft, public read, activity log.']] },
{ title: 'Decisions to confirm', tpl: '40px minmax(0,1fr)', cols: ['#', 'Decision'], rows: [
['1', 'Field keys and types lock per field once saved. Fields added since the last save can still change both.'],
['2', 'The review opens only when the type has entries. A type without entries saves directly.'],
['3', 'A new type starts with one field (Title), because a type needs at least one field.'],
['4', 'Yes or no is the label for the boolean type, Choice for select, Rich text for richtext.'],
['5', 'Required fields carry * and the line "Fields marked * are required." Optional fields carry nothing, unlike the "(optional)" tag used in settings forms, because most fields are optional here.'],
['6', 'The Schema item sits after Users in the sidebar. The Content group sits under the main items.'],
['7', 'The list shows 4 field columns by default and at most 6.']] },
['1', 'Confirmed: field keys and types lock per field once saved. Fields added since the last save can still change both. This is a client rule: a PUT with a changed key would remove one field and add another, and the server does not reject it.'],
['2', 'Confirmed: the review opens only when the type already has entries (the list response total tells the client). A type without entries saves directly.'],
['3', 'Confirmed: a new type starts with one field (Title), because a type needs at least one field.'],
['4', 'Confirmed: the type key is 2 to 40 lowercase letters, digits and hyphens, the same slug rule as Sites. A field key is different: lowercase letters, digits and underscores, starts with a letter, up to 40. The two inputs show their own rule.'],
['5', 'Yes or no is the label for the boolean type, Choice for select, Rich text for richtext.'],
['6', 'Required fields carry * and the line "Fields marked * are required." Optional fields carry nothing.'],
['7', 'A yes or no field with default false and one with no default look the same in the API sample. The inspector offers "On by default" only; no default means off. Confirm that no null default is needed.'],
['8', 'The client does not know when the schema changes while a form is open. The design relies on 422 invalid_entry (required, unknown_field) on save and then shows the reload alert.'],
['9', 'The Schema item sits after Users in the sidebar. The Content group sits under the main items.'],
['10', 'The list shows 4 field columns by default and at most 6.']] },
{ title: 'Accessibility', tpl: '220px minmax(0,1fr)', cols: ['Topic', 'Rule'], rows: [
['Reorder', 'The grip is a button with aria-label "Move Ends on". Space picks up, Up and Down move, Space drops, Esc cancels, with a polite live region ("Moving Ends on. Position 2 of 7."). Move up, Move down and Remove buttons exist on the selected row and in the mobile sheet.'],
['Focus order', 'Name, key, Save, Discard, field list (one tab stop, arrows move between rows), inspector top to bottom, API preview. Opening the inspector sheet on mobile moves focus to its title; closing returns it to the row.'],