const N = 'available now', L = 'later' const st = (name, scope, trigger, s, copy, o = {}) => ({ name, scope, trigger, s, copy, ...o }) export const groups = [ { label: '1 Schema list', title: '1 Schema: list of types', route: '/schema, /schema/:key (built-in)', api: 'GET /v1/content-type', auth: 'Owner or admin', note: 'Sidebar entry "Schema" for owners and admins only. Built-in types are listed with a lock and a Built-in badge, always after the custom ones. They open a read-only view. Sample data: the field counts of built-ins are illustrative.', states: [ st('Populated', N, 'An owner or admin opens /schema.', { v: 'types', k: 'ok' }, 'Schema · Content types and the fields they are made of. · New content type · Name · Key · Fields · Last updated · Built-in types are managed by RenewCMS. You can read their fields, but not change them. · Built-in · 3 content types · 5 built-in'), st('Loading', N, 'First load or retry. Skeleton rows appear after 150 ms. New content type is disabled.', { v: 'types', k: 'loading' }, '(skeleton) · New content type (disabled)'), st('Empty: no custom type yet', N, 'GET /v1/content-type returns []. Built-ins are still listed below.', { v: 'types', k: 'empty' }, 'No content types yet · Create a content type to manage your own kind of content, like events or recipes. The built-in types stay as they are. · New content type'), st('Error', N, '500 or network failure on the list request.', { v: 'types', k: 'error' }, 'Couldn\'t load content types · GET /v1/content-type failed. Check your connection and try again. · Try again'), st('Forbidden (403)', N, 'A user who is not owner or admin types /schema. There is no sidebar entry, so the page explains instead of redirecting.', { v: 'types', k: 'forbidden' }, 'You don\'t have access to Schema · Only owners and admins can change content types. You can still add and edit entries from the Content menu. · Go to Home'), 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('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('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'), st('Saving', N, 'PUT in flight. Inputs dim, Save shows the loader, nothing can be dragged.', { v: 'builder', k: 'saving' }, 'Saving…'), st('Saved', N, '200. A toast confirms, the badges clear.', { v: 'builder', k: 'saved' }, 'Saved at 11:02 · Content type saved · Events now has 8 fields.'), st('Save failed', N, '500 or network failure. Nothing was sent as saved; the edits stay.', { v: 'builder', k: 'fail' }, 'Save failed · Couldn\'t save the content type · Something went wrong on our side. Your changes are still here. Try again in a moment. · Try again'), st('Key already taken', N, '409 content_type_key_taken on POST.', { v: 'builder', k: 'taken' }, 'The key "events" is already used by another content type. Choose a different key.'), st('Schema incompatible', N, '422 schema_incompatible on PUT. The review dialog (group 3) closes into this state. Changes of the kinds that can cause it are marked.', { v: 'builder', k: 'incompat' }, 'Nothing was changed · Some entries don\'t fit the new fields, so the whole change was refused. RenewCMS can\'t tell which entries. The changes that can cause this are marked. · Review changes · Possible cause'), st('Offline', N, 'navigator.onLine is false. Edits are kept; Save is disabled.', { v: 'builder', k: 'offline' }, 'You\'re offline. Changes will be saved when you reconnect. · Waiting for connection') ] }, { label: '3 Review and delete', title: '3 Schema change review and delete type', route: '/schema/:key (dialogs)', api: 'PUT /v1/content-type/{key}, DELETE /v1/content-type/{key}', auth: 'Owner or admin', note: 'The review opens on Save when the type already has entries (37 here). It is informational: the server decides compatibility. Destructive changes (removing a field or a choice, tightening a limit, making a field unique or required) carry a warning flag. Per-change entry counts and a full diff are later.', states: [ st('Review before saving', N, 'Save on a type that has entries.', { v: 'review', k: 'review' }, 'Review changes to Events · Apply them to save the content type. · Add 2 fields, change 1, remove 1 · Events has 37 entries. Each one is checked against the new fields when you apply. · Add Speaker · Text, optional. · Add Room · Choice, required, default "Main hall". Existing entries get the default, so they still fit. · Change Capacity · Highest value goes from 5000 to 2000. · Entries may not fit · Remove Description · Entries stop returning its values. · Removes data · Flagged changes can be refused if existing entries don\'t fit. If that happens, nothing is changed. · Back to editing · Apply changes'), st('Applying', N, 'PUT in flight. Dialog cannot be dismissed.', { v: 'review', k: 'applying' }, 'Applying…'), st('Refused (schema_incompatible)', N, '422 schema_incompatible. States that nothing changed and names the kind of change that likely caused it, never an entry.', { v: 'review', k: 'refused' }, 'Nothing was changed · RenewCMS refused the changes because some entries don\'t fit the new fields. It doesn\'t say which entries. · Your content type is as it was · No field was added, changed or removed. · The change that can cause this in your edit · Possible cause · This kind of change is refused when an entry is over the new limit, a new required field has no default, a field becomes unique while entries share a value, or a removed choice is still in use. · Discard changes · Back to editing'), st('Review with counts and diff', L, 'Per-change entry counts and a full diff need an API that does not exist yet.', { v: 'review', k: 'later' }, 'Affects 0 entries · Affects 37 entries · Affects 3 entries · Affects 31 entries · View full diff · Per-change counts and diff come later', { notNow: true }), st('Delete type: blocked', N, 'Delete content type on a type with entries. No confirm button.', { v: 'review', k: 'blocked' }, 'You can\'t delete Events yet · A content type can only be deleted when it has no entries. · Events has 37 entries · Delete all of its entries first, then delete the content type. · Close · View entries'), st('Delete type: confirm', N, 'Delete content type on an empty type.', { v: 'review', k: 'confirm' }, 'Delete Recipes? · Recipes has no entries. Its fields and its API routes will be deleted. This can\'t be undone. · Key: recipes · Cancel · Delete content type'), 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: [ 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'), st('Many types in the sidebar', N, '14 custom types. Long names truncate. Mobile shows the drawer with the group expanded.', { v: 'entries', k: 'ok', many: true }, 'Content · Frequently asked questions and answers (truncated) · Show all 14 · All content types 14 (mobile, collapsed)'), st('Loading', N, 'First load or retry.', { v: 'entries', k: 'loading' }, '(skeleton)'), st('Empty for this type', N, 'Total is 0. The hint points to New entry.', { v: 'entries', k: 'empty' }, 'No entries yet · Use New entry to add the first event. Each entry follows the fields of this content type. · New entry'), st('Error', N, '500 or network failure.', { v: 'entries', k: 'error' }, 'Couldn\'t load entries · GET /v1/content-type/events/entry failed. Check your connection and try again. · Try again'), st('Forbidden (403)', N, 'The API answers 403. Unverified accounts are redirected to /register/pending before this.', { v: 'entries', k: 'forbidden', role: 'user' }, 'You don\'t have access to Events · Your account can\'t open this content right now. If you think this is a mistake, ask an owner or an admin. · Go to Home'), st('Type not found', N, '404 content_type_not_found on /content/unknown.', { v: 'entries', k: 'notfound' }, 'Content type not found · It may have been deleted, or the address may be mistyped. · Go to Home') ] }, { label: '6 Entry editor', title: '6 Entry editor', route: '/content/:key/new, /content/:key/:id', api: 'POST /v1/content-type/{key}/entry, GET/PUT/DELETE /v1/content-type/{key}/entry/{id}', auth: 'Any verified user', note: 'The form is generated from the schema, one control per field in schema order. Required fields carry * and the line "Fields marked * are required." Defaults are pre-filled on create and tagged Default. Saving replaces the whole entry.', states: [ st('New entry', N, '/content/events/new. Online is pre-filled from its default.', { v: 'entry', k: 'new' }, 'New entry · Events · Fields marked * are required. · Default · Each entry needs a different title. · Between 1 and 5000. · Choose an option · Pick a date · Save · Cancel · New entry · Not saved yet'), st('Editing', N, '/content/events/42. Save is disabled until something changes.', { v: 'entry', k: 'edit' }, 'Lime plaster workshop · Events · Entry 42 · Saved · Delete · Cancel · Save · Created by · Updated by · Inès Duarte · 12 Oct 2026, 09:14 · Marta Alves · 15 Oct 2026, 11:02'), st('Unsaved changes', N, 'Any edit.', { v: 'entry', k: 'dirty' }, 'Unsaved changes · 32 / 80'), st('Markdown preview', N, 'The Preview tab of a rich text field.', { v: 'entry', k: 'preview' }, 'Write · Preview'), st('Saving', N, 'PUT or POST in flight. Form dims.', { v: 'entry', k: 'saving' }, 'Saving…'), st('Saved', N, '200.', { v: 'entry', k: 'saved' }, 'Saved at 11:02 · Entry saved · "Lime plaster workshop" is up to date.'), st('Save failed', N, '500 or network failure. Values stay.', { v: 'entry', k: 'fail' }, 'Save failed · Couldn\'t save the entry · Something went wrong on our side. Your changes are still here. Try again in a moment. · Try again'), st('Required fields empty', N, '422 invalid_entry: title: required, starts_on: required. The first field with an error takes focus.', { v: 'entry', k: 'required' }, 'Fill in Title. · Pick a date for Starts on. · Not saved'), st('Rule errors', N, '422 invalid_entry for max, type, max and options. One rule per field is shown at a time.', { v: 'entry', k: 'rules' }, 'Title can be up to 80 characters. · 91 / 80 · Ends on must be a date, like 2026-11-21. · Capacity must be at most 5000. · Choose one of: Talk, Workshop or Meetup. · Not saved'), st('Duplicate unique value (409)', N, '409 invalid_entry: title: unique.', { v: 'entry', k: 'unique' }, 'Not saved · Another entry already uses this title. Change it and save again. · Another entry already uses this Title. Enter a different one.'), st('Entry not found', N, '404 entry_not_found, for example after a delete elsewhere.', { v: 'entry', k: 'notfound' }, 'Entry not found · It may have been deleted by someone else. · Back to Events'), st('Schema changed since load', N, '422 invalid_entry: room: required, or unknown_field. The form is read-only until reloaded.', { v: 'entry', k: 'changed' }, 'The fields of Events changed · A field was removed and a new required field, Room, was added after you opened this form. Reload the form to continue. What you typed here can\'t be saved against the old fields. · Reload form'), st('Delete entry: confirm', N, 'Delete.', { v: 'entry', k: 'delete' }, 'Delete this entry? · "Lime plaster workshop" will be deleted from Events. This can\'t be undone. · Cancel · Delete entry') ] }, { label: '7 Relations', title: '7 Relation field and picker', route: '/schema/:key, /content/:key/:id', api: 'not built', auth: 'Owner or admin (picker), any verified user (form)', note: 'All later. The model reads as two sentences, with a live preview of both sides. Three shapes: one to one, one to many, many to many.', states: [ st('Relation in the builder', L, 'A seventh type, Relation, in the type gallery.', { v: 'rel', k: 'builder' }, 'Relation · Related content type · How are they connected? · One to one · Each Event has one Venue, and each Venue has one Event. · One to many · Each Event has one Venue. A Venue has many Events. · Many to many · Each Event has many Venues, and each Venue has many Events. · Each Event has one Venue · Each Venue has many Events · shown on Venue as the field "Events"'), st('Relation in the entry form', L, 'Search and pick; chips for many.', { v: 'rel', k: 'form' }, 'Venue · Speakers · Search to add · An event can have many speakers.'), st('Relation in the list', L, 'One chip, two chips and +N, or a dash.', { v: 'rel', k: 'list' }, 'One chip for a single relation. Up to two chips and a "+N" for many. Empty shows a dash.') ] }, { label: '8 Permissions', title: '8 Permissions grid', route: '/settings/permissions', api: 'not built', auth: 'Owner or admin', note: 'All later. Rows are types, columns are actions, one role at a time. Built-in rows are locked. Mobile collapses to one card per type with four checkboxes.', states: [ st('Default: today\'s rules', L, 'Role User selected. Everything on, as today.', { v: 'perms', k: 'default' }, 'Permissions · What each role can do with each content type. · Today\'s rules are the starting point · Owners and admins edit the schema. Any verified user can read, add, edit and delete entries. · Owner · Admin · User · Read · Create · Update · Delete · Built-in types. RenewCMS sets these rules. · Schema (create, edit and delete content types): owners and admins only. Set by RenewCMS. · No changes · Discard · Save permissions'), st('Changed, not saved', L, 'Two types restricted for the User role.', { v: 'perms', k: 'changed' }, '4 changes not saved · Changed') ] }, { label: '9 Export and import', title: '9 Schema export and import', route: '/schema (dialogs)', api: 'not built', auth: 'Owner or admin', note: 'All later. Dev-to-production flow: export a file, import it elsewhere, preview, same compatibility review as group 3, explicit confirm.', states: [ st('Export', L, 'Export on /schema.', { v: 'xport', k: 'export' }, 'Export schema · Download the fields of your content types as a file. Entries aren\'t included. · All content types · 3 content types · One content type · Choose which one · File name · renewcms-schema.json · Import this file on another RenewCMS site to create the same content types. · Cancel · Download file'), st('Import preview', L, 'A file was chosen.', { v: 'xport', k: 'import' }, 'Import schema · renewcms-schema.json · 3 content types · Create 1, change 1, no change 1 · Recipes · New content type with 4 fields. · Events · Add 2 fields, remove Description. Events has 37 entries on this site. · Removes data · Press mentions · Same as on this site. · Existing entries are checked against the changes when you import. If any entry doesn\'t fit, nothing is imported. · Cancel · Import schema'), st('Import refused', L, 'The server answers schema_incompatible.', { v: 'xport', k: 'refused' }, 'Nothing was imported · Some entries on this site don\'t fit the imported fields. RenewCMS can\'t tell which entries. The flagged change can cause this. · Try again') ] } ] export const tables = [ { title: 'Scope: available now or later', tpl: '260px 110px minmax(0,1fr)', cols: ['Feature', 'Status', 'Notes'], rows: [ ['1 Schema list, built-in rows, read-only view', 'Available now', 'Built-in list is static client data (see Needs backend)'], ['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 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'], ['5 Content group in the sidebar, entry list', 'Available now', 'No filter or search on custom fields'], ['6 Entry editor, all states', 'Available now', ''], ['7 Relation field and picker', 'Later', ''], ['8 Permissions grid', 'Later', ''], ['9 Schema export and import', 'Later', ''], ['Filters on custom fields', 'Later', ''], ['Publish and draft state for entries', 'Later', 'Not drawn'], ['Public (anonymous) read access', 'Later', 'Not drawn'], ['Activity log entries for types and entries', 'Later', 'Not drawn']] }, { title: 'Routes', tpl: '190px 280px 200px minmax(0,1fr)', cols: ['Route', 'Screen', 'Auth', 'API'], rows: [ ['/schema', 'Schema list', 'Owner or admin. Others get the 403 page', 'GET /v1/content-type'], ['/schema/new', 'Schema builder, new type', 'Owner or admin', 'POST /v1/content-type'], ['/schema/:key', 'Schema builder (custom type) or read-only view (built-in)', 'Owner or admin', 'GET, PUT, DELETE /v1/content-type/{key}'], ['/content/:key', 'Entry list', 'Any verified user', 'GET /v1/content-type/{key}, GET /v1/content-type/{key}/entry'], ['/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']] }, { 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'], ['FieldList', 'New', 'List of UButton rows, useSortable, UBadge', 'No Nuxt UI sortable list. Keyboard: the grip is a button; Space picks up, arrows move, Space drops, Esc cancels. Move up and Move down buttons on the selected row. Same approach as AuthorList'], ['FieldInspector', 'New', 'UForm, UFormField, UInput, UInputNumber, UInputDate, USelect, UTextarea, USwitch', 'Composition only. Controls follow the field type'], ['FieldTypePicker', 'New', 'URadioGroup variant="card" in a grid, icon in the label', 'Existing radio cards fit; only the icon and the lock state are added'], ['KeyField', 'Existing', 'SlugField (UInput + UButton + UBadge)', 'Reused for the type key and field keys. Locked variant already exists'], ['ChoicesEditor', 'New', 'UInput + UButton rows, useSortable', 'UInputTags can\'t reorder or flag a blank or duplicate per row'], ['ApiPreview', 'New', 'UCollapsible + CodeSnippet + UButton', 'CodeSnippet is reused. Sheet on mobile'], ['ChangeReview', 'New', 'UModal (sheet on mobile), UBadge, list rows', 'Dialog pattern unchanged. Rows reuse the UAlert-style row of SuggestionList'], ['SchemaRefusal, DeleteTypeDialog', 'New', 'UModal + UAlert', 'Dialog pattern unchanged. Delete uses color="error"'], ['ContentNavGroup', 'New', 'UNavigationMenu group "Content" (NavGroup)', 'Adds the 5 + selected cap, "Show all N" and the collapsed mobile row'], ['EntryTable', 'New', 'UTable with column visibility', 'Cells per field type (FieldCell). Cards on mobile'], ['ColumnPicker', 'New', 'UPopover + UCheckbox list; bottom sheet on mobile', 'UDropdownMenu closes on each pick and has no cap message'], ['SortSelect', 'New', 'USelect + UButton icon', 'ID, Created and Updated only'], ['FieldControl', 'New', 'UInput, UInputNumber, UInputDate, USelect, USwitch (the single toggle), MarkdownField', 'One switch per field type, generated from the schema'], ['MarkdownField', 'New', 'UTextarea + UTabs (Write, Preview) + toolbar of UButton', 'The article editor is a full document editor. This is a compact one'], ['FieldCell', 'New', 'text, UBadge outline (choice), icon + text (yes or no)', 'Renders one value per type, with the empty dash'], ['EntryForm', 'New', 'UForm generated from the schema', 'Provenance footer, schema-changed alert'], ['RelationPicker (later)', 'New', 'URadioGroup card + USelect', 'Sentence preview is text'], ['RelationField (later)', 'New', 'UInputMenu multiple', 'Chips and search'], ['PermissionsGrid (later)', 'New', 'UTable + UCheckbox; UCard list on mobile', ''], ['SchemaImportDialog (later)', 'New', 'UModal + UFileUpload', '']] }, { title: 'Errors to copy', tpl: '300px 70px minmax(0,1fr)', cols: ['Error', 'HTTP', 'Copy and place'], rows: [ ['content_type_not_found', '404', 'Page: "Content type not found · It may have been deleted, or the address may be mistyped."'], ['entry_not_found', '404', 'Page: "Entry not found · It may have been deleted by someone else."'], ['content_type_key_taken', '409', 'Under the key: "The key "events" is already used by another content type. Choose a different key."'], ['content_type_has_entries', '409', 'Delete dialog: "Recipes now has entries · Someone added an entry after this page loaded, so nothing was deleted. Delete the entries first, then try again."'], ['invalid_content_type_key', '400', 'Under the key: "Use 2 to 40 lowercase letters, digits or hyphens."'], ['invalid_field', '400', 'On the marked field, per rule (label, key, limits, default, choices). If the client missed one: "Check the fields marked in the list."'], ['schema_incompatible', '422', 'Refusal dialog: "Nothing was changed · RenewCMS refused the changes because some entries don\'t fit the new fields. It doesn\'t say which entries."'], ['invalid_entry: key: required', '422', '"Fill in Title." (date: "Pick a date for Starts on.", choice: "Choose an option for Category.")'], ['invalid_entry: key: type', '422', '"Title must be text." / "must be a number." / "must be yes or no." / "must be a date, like 2026-11-21."'], ['invalid_entry: key: min, max', '422', 'Text: "needs at least N characters", "can be up to N characters". Number: "must be at least N", "must be at most N". Date: "can\'t be before D", "can\'t be after D"'], ['invalid_entry: key: options', '422', '"Choose one of: Talk, Workshop or Meetup."'], ['invalid_entry: key: unique', '409', '"Another entry already uses this Title. Enter a different one." plus the alert "Not saved"'], ['invalid_entry: key: unknown_field', '422', 'Alert "The fields of Events changed", button Reload form'], ['401', '401', 'Existing: redirect to /login?redirect=…&reason=expired'], ['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.']] }, { 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.']] }, { 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.'], ['Focus', '2 px --ui-primary, offset 2 px, as everywhere.'], ['Labels and errors', 'Every control has a visible label. Error text is tied to its input with aria-describedby and aria-invalid. Rows with errors also show an icon, not only a red border.'], ['Type picker', 'A radio group. The locked state sets aria-disabled and the line "Can\'t change after saving" is read with the group.'], ['Counters', 'Character counters are polite live regions on pause, not on every key.'], ['Contrast', 'Tokens already computed in foundations.md. Muted text is never placed on sand; selected tiles use --rc-on-sand for their description.'], ['Touch', 'Controls are 44 px on mobile. The inspector, columns and API preview are bottom sheets.']] } ]