From e6e088c9635207aa47f72920252dbf211765edb3 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Mon, 19 Oct 2026 17:38:41 +0200 Subject: [PATCH] chore: custom content types design groundwork --- docs/design/README.md | 6 + docs/design/components.md | 32 ++ docs/design/routes.md | 10 + docs/design/screens/content.md | 23 + docs/design/screens/entry.md | 27 ++ docs/design/screens/schema-later.md | 40 ++ docs/design/screens/schema.md | 78 ++++ docs/design/source/AppFrame.dc.html | 8 +- .../source/RenewCMS Screens Schema.dc.html | 79 ++++ docs/design/source/SchemaFrame.dc.html | 426 ++++++++++++++++++ docs/design/source/schema-spec.js | 178 ++++++++ docs/design/states.md | 25 + 12 files changed, 929 insertions(+), 3 deletions(-) create mode 100644 docs/design/screens/content.md create mode 100644 docs/design/screens/entry.md create mode 100644 docs/design/screens/schema-later.md create mode 100644 docs/design/screens/schema.md create mode 100644 docs/design/source/RenewCMS Screens Schema.dc.html create mode 100644 docs/design/source/SchemaFrame.dc.html create mode 100644 docs/design/source/schema-spec.js diff --git a/docs/design/README.md b/docs/design/README.md index 9de2ced..7a6b0f7 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -20,6 +20,12 @@ Back-office board groups: Sites, Sites: form, Sites: delete, Authors, Authors: f - Only individual states that render a relative time or a countdown carry the "Clock-sensitive" tag (13 states, listed in states.md). No Home or Articles frame is tagged as a whole. - Role-restricted variants drawn: read-only Authors list and detail, hidden Sites entry, Sites 403, own-author edit. +## Pass 4: custom content types +Board `RenewCMS Screens Schema.dc.html` (frames: `SchemaFrame`, mounted in `AppFrame`): 9 groups, 65 states (56 available now, 9 later). 1 Schema list 6, 2 Schema builder 16, 3 Review and delete 7, 4 Field types 6, 5 Content and entry list 9, 6 Entry editor 13, 7 Relations 3, 8 Permissions 2, 9 Export and import 3. Plus tables: scope, routes, new components with their Nuxt UI mapping, errors to copy, needs backend (4 rows), decisions to confirm, accessibility. Specs: screens/schema.md, content.md, entry.md, schema-later.md. +- AppFrame gains a Schema item (owner and admin) and a Content group (sidebar and mobile drawer). Nothing else in AppFrame changed. +- Backend gaps still open: built-in types and fields, display names for created_by and updated_by, a type icon, per-change entry counts. +- Not done: Back-office and Prototype boards were not compared visually after the sidebar change (see Checks below). + ## Specs routes.md · components.md · states.md · foundations.md · change-list.md · activity-spec.js · screens/*.md (24 files) diff --git a/docs/design/components.md b/docs/design/components.md index 55322e0..875e849 100644 --- a/docs/design/components.md +++ b/docs/design/components.md @@ -89,3 +89,35 @@ Frames: `DirectoryFrame.dc.html` (Sites, Authors, editor section content; the li | SeoStatusChip | New | UBadge | Lightbulb + count (warning soft) or eye-off "Hidden" (outline). Articles rows and cards | | NeedsAttentionToggle | New | UButton (pressed state) | Articles toolbar, sand when on | | TitleTemplateField | New | UInput + live example | Sites form | + + +## Custom content types (pass 4) + +Frames: `SchemaFrame.dc.html` (all schema, content and entry bodies; relation, permissions and import frames are Later), mounted in `AppFrame.dc.html`. Existing pieces are reused unchanged: dialogs and sheets, toasts, alerts, paginator, skeletons, empty states, SlugField, CodeSnippet, the single switch. + +**AppFrame changes.** Sidebar: a "Schema" item (icon shapes) after Users, owner and admin only. A "Content" group under the main items, one row per custom type (default icon files, name truncated with the full name in the tooltip, selected row sand). Over 5 types it shows the first 5 and the selected one, then "Show all N". Mobile drawer: the group is one collapsed row "All content types" with the count; expanded it lists the types at 44 px. New props on the shell state: `content` (types), `contentOn`, `contentOpen`. The group is hidden when there are no custom types. No existing item changed. + +| Component | Status | Nuxt UI | Notes | +|---|---|---|---| +| 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 | | diff --git a/docs/design/routes.md b/docs/design/routes.md index 150b60d..94c9869 100644 --- a/docs/design/routes.md +++ b/docs/design/routes.md @@ -16,6 +16,13 @@ | /article/:id/edit | Editor gains the "Site and authors" section (Site select and authors, any role that can edit the article) | none | PATCH /v1/article/{id} {site_id, author_ids[]} | | /profile | Profile gains the "Author profile" card, all roles | none | POST /v1/author/me | | /integration/api-keys | API keys | none | GET/POST /v1/api-key, DELETE /v1/api-key/{id} | +| /schema | Schema list, custom and built-in types (owner or admin; others get a 403 page and no sidebar entry). Available now | none | GET /v1/content-type | +| /schema/new | Schema builder, new type (owner or admin). Available now | none | POST /v1/content-type | +| /schema/:key | Schema builder for a custom type, read-only view for a built-in (owner or admin). Review step on save when the type has entries. Available now | none | GET/PUT/DELETE /v1/content-type/{key} | +| /content/:key | Entry list (any verified user). Available now | page, size, sort | GET /v1/content-type/{key}, GET /v1/content-type/{key}/entry?page=&size=&sort= | +| /content/:key/new | New entry (any verified user). Available now | none | POST /v1/content-type/{key}/entry | +| /content/:key/:id | Entry editor (any verified user). Available now | none | GET/PUT/DELETE /v1/content-type/{key}/entry/{id} | +| /settings/permissions | Permissions grid (owner or admin). Later | none | not built | ## Pagination in the URL (Articles, Media) `/articles?page=2&size=50` and `/media?page=7&size=50`. Parameter names follow the repo (activity already uses `page` and `size`). Allowed sizes 5, 25, 50, 100. Defaults `page=1`, `size=25`, omitted from the URL when default. Changing search, tab, filter or size resets `page` to 1. `page` above the last page redirects (replace) to the last page and shows "That page doesn't exist. Showing the last page." An invalid `size` falls back to 25. @@ -43,3 +50,6 @@ Sites pagination: `/settings/sites?q=&page=&size=` with the same rules as Articl ## SEO No new page routes. The panel and dialog are overlays on `/article/:id/edit`: `?panel=seo` (optionally `&field=seo_title|seo_description|slug|cover|authors|body` for the suggestion jump) and `?dialog=seo-api`. `/articles?seo=suggestions` is the Needs attention filter. API: GET /v1/article/{id}/seo, GET /v1/site/{key}/sitemap?page=&size=, PATCH /v1/article/{id} (new fields), PATCH /v1/site/{id} {site_name, title_template, default_description, social_image_id, default_robots}, GET /v1/article/?seo=ready|suggestions|hidden. 401 without a session; 403 owner_only on site SEO fields; 409 slug_taken {suggestion} on PATCH slug; 400 on invalid canonical_url or title_template. + +## Custom content types in the URL +`/content/events?page=2&size=50&sort=created_at:desc`. `page` and `size` follow Articles (sizes 5, 25, 50, 100; default 25). `sort` is one parameter, `:`, default `updated_at:desc`. Defaults are omitted from the URL; a size or sort change resets `page` to 1; invalid values are replaced silently by the default; a `page` past the end falls back to the last page. The response carries `total`, `page`, `size` and `sort`: use them for the summary ("Showing 1–5 of 37"), the pager and the "37 entries use this content type" line of the review step. No filter or search on custom fields yet. Auth: `/schema*` owner or admin (403 page otherwise), `/content*` any verified user (unverified accounts are redirected to /register/pending). Unknown type: 404 content_type_not_found page. Unknown entry: 404 entry_not_found page. diff --git a/docs/design/screens/content.md b/docs/design/screens/content.md new file mode 100644 index 0000000..3cfbc42 --- /dev/null +++ b/docs/design/screens/content.md @@ -0,0 +1,23 @@ +# Content + +Route: `/content/:key`. API: GET /v1/content-type/{key} · GET /v1/content-type/{key}/entry?page=&size=&sort=. Auth: any verified user. + +Visual reference: `RenewCMS Screens Schema.dc.html`, groups "Content and entry list". Frames: `SchemaFrame.dc.html` mounted in `AppFrame.dc.html` (shell, sidebar, topbar, drawer). Every state is drawn in light and dark (theme switch) at 1440×900 (shown at 50%) and 390×844 (shown at 75%). Copy is exact. Strings in a Copy cell are separated by " · " and listed in reading order. Nothing is clock-sensitive. + +### 5 Content navigation and entry list (group "5 Content and entry list", 9 states) + +Route: `/content/:key`. API: GET /v1/content-type/{key}, GET /v1/content-type/{key}/entry. Auth: Any verified user. + +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. + +| # | State | Trigger | Copy | Scope | +|---|---|---|---|---| +| 1 | Populated | /content/events. 7 entries, one page: no pager, only the count. | 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 | Available now | +| 2 | Paginated, 5 per page | /content/events?size=5. 37 entries. | Showing 1–5 of 37 · 5 per page · Page 1 of 8 (mobile) | Available now | +| 3 | Columns menu | The Columns button. Popover on desktop, sheet on mobile. Created and updated always show. | Columns · 4 of 6 · Choose up to 6 fields to show. 4 selected. · Created and updated dates always show · Reset · Done | Available now | +| 4 | Many types in the sidebar | 14 custom types. Long names truncate. Mobile shows the drawer with the group expanded. | Content · Frequently asked questions and answers (truncated) · Show all 14 · All content types 14 (mobile, collapsed) | Available now | +| 5 | Loading | First load or retry. | (skeleton) | Available now | +| 6 | Empty for this type | Total is 0. The hint points to New entry. | No entries yet · Use New entry to add the first event. Each entry follows the fields of this content type. · New entry | Available now | +| 7 | Error | 500 or network failure. | Couldn't load entries · GET /v1/content-type/events/entry failed. Check your connection and try again. · Try again | Available now | +| 8 | Forbidden (403) | The API answers 403. Unverified accounts are redirected to /register/pending before this. | 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 | Available now | +| 9 | Type not found | 404 content_type_not_found on /content/unknown. | Content type not found · It may have been deleted, or the address may be mistyped. · Go to Home | Available now | diff --git a/docs/design/screens/entry.md b/docs/design/screens/entry.md new file mode 100644 index 0000000..71685ae --- /dev/null +++ b/docs/design/screens/entry.md @@ -0,0 +1,27 @@ +# 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. + +Visual reference: `RenewCMS Screens Schema.dc.html`, groups "Entry editor". Frames: `SchemaFrame.dc.html` mounted in `AppFrame.dc.html` (shell, sidebar, topbar, drawer). Every state is drawn in light and dark (theme switch) at 1440×900 (shown at 50%) and 390×844 (shown at 75%). Copy is exact. Strings in a Copy cell are separated by " · " and listed in reading order. Nothing is clock-sensitive. + +### 6 Entry editor (group "6 Entry editor", 13 states) + +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. + +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. + +| # | State | Trigger | Copy | Scope | +|---|---|---|---|---| +| 1 | New entry | /content/events/new. Online is pre-filled from its default. | 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 | Available now | +| 2 | Editing | /content/events/42. Save is disabled until something changes. | 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 | Available now | +| 3 | Unsaved changes | Any edit. | Unsaved changes · 32 / 80 | Available now | +| 4 | Markdown preview | The Preview tab of a rich text field. | Write · Preview | Available now | +| 5 | Saving | PUT or POST in flight. Form dims. | Saving… | Available now | +| 6 | Saved | 200. | Saved at 11:02 · Entry saved · "Lime plaster workshop" is up to date. | Available now | +| 7 | Save failed | 500 or network failure. Values stay. | 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 | Available now | +| 8 | Required fields empty | 422 invalid_entry: title: required, starts_on: required. The first field with an error takes focus. | Fill in Title. · Pick a date for Starts on. · Not saved | Available now | +| 9 | Rule errors | 422 invalid_entry for max, type, max and options. One rule per field is shown at a time. | 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 | Available now | +| 10 | Duplicate unique value (409) | 409 invalid_entry: title: unique. | Not saved · Another entry already uses this title. Change it and save again. · Another entry already uses this Title. Enter a different one. | Available now | +| 11 | Entry not found | 404 entry_not_found, for example after a delete elsewhere. | Entry not found · It may have been deleted by someone else. · Back to Events | Available now | +| 12 | Schema changed since load | 422 invalid_entry: room: required, or unknown_field. The form is read-only until reloaded. | 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 | Available now | +| 13 | Delete entry: confirm | Delete. | Delete this entry? · "Lime plaster workshop" will be deleted from Events. This can't be undone. · Cancel · Delete entry | Available now | diff --git a/docs/design/screens/schema-later.md b/docs/design/screens/schema-later.md new file mode 100644 index 0000000..19abe84 --- /dev/null +++ b/docs/design/screens/schema-later.md @@ -0,0 +1,40 @@ +# Schema: later + +All states on this page are **Later**: relations, permissions and schema export and import are not built. Drawn now so the model and the layout are settled. + +Visual reference: `RenewCMS Screens Schema.dc.html`, groups "Relations", "Permissions", "Export and import". Frames: `SchemaFrame.dc.html` mounted in `AppFrame.dc.html` (shell, sidebar, topbar, drawer). Every state is drawn in light and dark (theme switch) at 1440×900 (shown at 50%) and 390×844 (shown at 75%). Copy is exact. Strings in a Copy cell are separated by " · " and listed in reading order. Nothing is clock-sensitive. + +### 7 Relation field and picker (group "7 Relations", 3 states) + +Route: `/schema/:key, /content/:key/:id`. API: not built. Auth: Owner or admin (picker), any verified user (form). + +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. + +| # | State | Trigger | Copy | Scope | +|---|---|---|---|---| +| 1 | Relation in the builder | A seventh type, Relation, in the type gallery. | 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" | Later | +| 2 | Relation in the entry form | Search and pick; chips for many. | Venue · Speakers · Search to add · An event can have many speakers. | Later | +| 3 | Relation in the list | One chip, two chips and +N, or a dash. | One chip for a single relation. Up to two chips and a "+N" for many. Empty shows a dash. | Later | + +### 8 Permissions grid (group "8 Permissions", 2 states) + +Route: `/settings/permissions`. API: not built. Auth: Owner or admin. + +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. + +| # | State | Trigger | Copy | Scope | +|---|---|---|---|---| +| 1 | Default: today's rules | Role User selected. Everything on, as today. | 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 | Later | +| 2 | Changed, not saved | Two types restricted for the User role. | 4 changes not saved · Changed | Later | + +### 9 Schema export and import (group "9 Export and import", 3 states) + +Route: `/schema (dialogs)`. API: not built. Auth: Owner or admin. + +All later. Dev-to-production flow: export a file, import it elsewhere, preview, same compatibility review as group 3, explicit confirm. + +| # | State | Trigger | Copy | Scope | +|---|---|---|---|---| +| 1 | Export | Export on /schema. | 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 | Later | +| 2 | Import preview | A file was chosen. | 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 | Later | +| 3 | Import refused | The server answers schema_incompatible. | 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 | Later | diff --git a/docs/design/screens/schema.md b/docs/design/screens/schema.md new file mode 100644 index 0000000..2017c4f --- /dev/null +++ b/docs/design/screens/schema.md @@ -0,0 +1,78 @@ +# Schema + +Route: `/schema`, `/schema/new`, `/schema/:key`. API: GET/POST /v1/content-type · GET/PUT/DELETE /v1/content-type/{key}. Auth: owner or admin. + +Custom content types: an owner or admin defines a type (for example Events) made of 1 to 50 ordered fields; any verified user then writes entries. Everything about a type happens on one screen. Primary words: content type, field, choice list, entry. "Key" is kept for the API identifier. The key of a type is chosen at creation and never changes; the key and type of a saved field never change. Built-in types (Articles, Authors, Sites, Media, Users) are listed with a lock and a "Built-in" badge and open a read-only view. Non-owner, non-admin users see no Schema entry; a direct link shows a 403 page. + +Visual reference: `RenewCMS Screens Schema.dc.html`, groups "Schema list", "Schema builder", "Review and delete", "Field types". Frames: `SchemaFrame.dc.html` mounted in `AppFrame.dc.html` (shell, sidebar, topbar, drawer). Every state is drawn in light and dark (theme switch) at 1440×900 (shown at 50%) and 390×844 (shown at 75%). Copy is exact. Strings in a Copy cell are separated by " · " and listed in reading order. Nothing is clock-sensitive. + +### 1 Schema: list of types (group "1 Schema list", 6 states) + +Route: `/schema, /schema/:key (built-in)`. API: GET /v1/content-type. Auth: Owner or admin. + +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. + +| # | State | Trigger | Copy | Scope | +|---|---|---|---|---| +| 1 | Populated | An owner or admin opens /schema. | 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 | Available now | +| 2 | Loading | First load or retry. Skeleton rows appear after 150 ms. New content type is disabled. | (skeleton) · New content type (disabled) | Available now | +| 3 | Empty: no custom type yet | GET /v1/content-type returns []. Built-ins are still listed below. | 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 | Available now | +| 4 | Error | 500 or network failure on the list request. | Couldn't load content types · GET /v1/content-type failed. Check your connection and try again. · Try again | Available now | +| 5 | Forbidden (403) | A user who is not owner or admin types /schema. There is no sidebar entry, so the page explains instead of redirecting. | 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 | Available now | +| 6 | Built-in type: read-only view | Click Articles in the list. No edit, no delete, no add field. | 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. | Available now | + +### 2 Schema builder (group "2 Schema builder", 16 states) + +Route: `/schema/new, /schema/:key`. API: POST /v1/content-type, GET/PUT /v1/content-type/{key}. Auth: Owner or admin. + +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. + +| # | State | Trigger | Copy | Scope | +|---|---|---|---|---| +| 1 | New type | /schema/new. One starter field is added because a type needs at least one. | 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 | Available now | +| 2 | Editing an existing type | /schema/events with 37 entries. Key is locked, saved field keys and types are locked. | 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 | Available now | +| 3 | Locked key: tooltip | Hover or focus the locked key. | The key is used in URLs and the API, so it can't change after the type is created. | Available now | +| 4 | Unsaved changes | Two fields added, Capacity changed, Description removed. Rows carry New and Changed badges; removal shows an undo row. | Unsaved changes · New · Changed · Description will be removed · Undo · Save · Discard | Available now | +| 5 | Field errors | 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. | 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. | Available now | +| 6 | Type-level errors | New type with no name, a one-letter key and no fields. | 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 | Available now | +| 7 | Field key error | 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. | Key · Start with a letter. Use lowercase letters, digits and underscores. · 1 problem to fix | Available now | +| 8 | Choice list editor | A Choice field is selected. Add, remove and reorder; blanks and duplicates are flagged per row. | 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. | Available now | +| 9 | Keyboard reorder | 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. | Moving Ends on. Position 2 of 7. Press Up or Down to move, Space to drop, Esc to cancel. (live region, polite) | Available now | +| 10 | API preview open | The panel at the bottom. Updates as fields change. Copy JSON copies the sample entry. | 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 | Available now | +| 11 | Saving | PUT in flight. Inputs dim, Save shows the loader, nothing can be dragged. | Saving… | Available now | +| 12 | Saved | 200. A toast confirms, the badges clear. | Saved at 11:02 · Content type saved · Events now has 8 fields. | Available now | +| 13 | Save failed | 500 or network failure. Nothing was sent as saved; the edits stay. | 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 | Available now | +| 14 | Key already taken | 409 content_type_key_taken on POST. | The key "events" is already used by another content type. Choose a different key. | Available now | +| 15 | Schema incompatible | 409 schema_incompatible on PUT. The review dialog (group 3) closes into this state. Changes of the kinds that can cause it are marked. | 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 | Available now | +| 16 | Offline | navigator.onLine is false. Edits are kept; Save is disabled. | You're offline. Changes will be saved when you reconnect. · Waiting for connection | Available now | + +### 3 Schema change review and delete type (group "3 Review and delete", 7 states) + +Route: `/schema/:key (dialogs)`. API: PUT /v1/content-type/{key}, DELETE /v1/content-type/{key}. Auth: Owner or admin. + +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. + +| # | State | Trigger | Copy | Scope | +|---|---|---|---|---| +| 1 | Review before saving | Save on a type that has entries. | 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 | Available now | +| 2 | Applying | PUT in flight. Dialog cannot be dismissed. | Applying… | Available now | +| 3 | Refused (schema_incompatible) | 409 schema_incompatible. States that nothing changed and names the kind of change that likely caused it, never an entry. | 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 | Available now | +| 4 | Review with counts and diff | Per-change entry counts and a full diff need an API that does not exist yet. | Affects 0 entries · Affects 37 entries · Affects 3 entries · Affects 31 entries · View full diff · Per-change counts and diff come later | Later | +| 5 | Delete type: blocked | Delete content type on a type with entries. No confirm button. | 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 | Available now | +| 6 | Delete type: confirm | Delete content type on an empty type. | 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 | Available now | +| 7 | Delete type: server fallback (409) | 409 content_type_has_entries. An entry was added after the page loaded. | 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 | Available now | + +### 4 Field-type gallery (group "4 Field types", 6 states) + +Route: `reference board`. API: none. Auth: n/a. + +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). + +| # | State | Trigger | Copy | Scope | +|---|---|---|---|---| +| 1 | Text | type: "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. | Available now | +| 2 | Rich text | type: "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) | Available now | +| 3 | Number | type: "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. | Available now | +| 4 | Yes or no | type: "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. | Available now | +| 5 | Date | type: "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. | Available now | +| 6 | Choice | type: "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. | Available now | diff --git a/docs/design/source/AppFrame.dc.html b/docs/design/source/AppFrame.dc.html index 80c6304..dab4fa0 100644 --- a/docs/design/source/AppFrame.dc.html +++ b/docs/design/source/AppFrame.dc.html @@ -14,6 +14,7 @@
{{ n.t }}Update
Media
{{ x.t }}
Publishing
{{ x2.t }}
+
Content
All content types{{ contentCount }}
{{ c.name }}
{{ contentMore }}
{{ ini }}{{ who }}
@@ -368,7 +369,7 @@ {{ purgeDlg }} -
RenewCMS
{{ n.t }}
Media
{{ x.t }}
Publishing
{{ x2.t }}
{{ ini }}{{ who }}
+
RenewCMS
{{ n.t }}
Media
{{ x.t }}
Publishing
{{ x2.t }}
Content
All content types{{ contentCount }}
{{ c.name }}
{{ contentMore }}
{{ ini }}{{ who }}
timber windows/articles/winter-checklist
Edit linkOpen linkRemove link
Details
Site and authors
{{ edMeta }}
{{ metaSummary }}
Search and sharing
{{ edSeo }}
{{ seoSummary }}
Created12 Mar 2026, 09:14UpdatedToday, 11:02ID12
Public API URL/v1/article/12
{{ slotOverlays }} @@ -387,9 +388,10 @@ class Component extends DCLogic { const s = this.props.s || {} const m = !!this.props.mobile const k = s.kind || 'home' - const navItems = s.role === 'user' ? [['home', 'house', 'Home'], ['articles', 'file-text', 'Articles'], ['media', 'images', 'Media'], ['authors', 'feather', 'Authors'], ['activity', 'history', 'Activity']] : [['home', 'house', 'Home'], ['articles', 'file-text', 'Articles'], ['media', 'images', 'Media'], ['authors', 'feather', 'Authors'], ['users', 'users', 'Users'], ['activity', 'history', 'Activity'], ['integration', 'plug', 'Integration'], ['settings', 'settings', 'Settings']] + const navItems = s.role === 'user' ? [['home', 'house', 'Home'], ['articles', 'file-text', 'Articles'], ['media', 'images', 'Media'], ['authors', 'feather', 'Authors'], ['activity', 'history', 'Activity']] : [['home', 'house', 'Home'], ['articles', 'file-text', 'Articles'], ['media', 'images', 'Media'], ['authors', 'feather', 'Authors'], ['users', 'users', 'Users'], ['schema', 'shapes', 'Schema'], ['activity', 'history', 'Activity'], ['integration', 'plug', 'Integration'], ['settings', 'settings', 'Settings']] const cr0 = s.crumbs || [], setOpen = s.nav === 'settings' || !!s.sideOpen, parentOn = (key) => s.nav === key && (key !== 'settings' || cr0.length <= 1) const nav = navItems.map(([key, icon, t]) => { const on = parentOn(key), open = key === 'settings' && setOpen; return { key, icon, t, bg: on ? 'var(--rc-sand)' : 'transparent', badge: key === 'settings' && !!s.navBadge, fg: on ? 'var(--rc-on-sand)' : open ? 'var(--ui-text-highlighted)' : 'var(--ui-text-toned)', fw: on || open ? 500 : 400, chev: key === 'settings' ? (open ? 'chevron-down' : 'chevron-right') : '', chevML: key === 'settings' && s.navBadge ? '8px' : 'auto', sub: open, hasSites: !s.role || s.role === 'owner', subs2: (!s.role || s.role === 'owner') ? (() => { const o = cr0[cr0.length - 1] === 'Sites'; return [{ t: 'Sites', ic: 'globe', bg: o ? 'var(--rc-sand)' : 'transparent', fg: o ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: o ? 500 : 400 }] })() : [], subs: ['Variants', 'Cache'].map(x => { const o = cr0[cr0.length - 1] === x; return { t: x, ic: x === 'Variants' ? 'layers' : 'database', bg: o ? 'var(--rc-sand)' : 'transparent', fg: o ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: o ? 500 : 400 } }) } }) + const CT = s.content || [], ctOn = s.contentOn, ctMax = 5, ctVis = CT.slice(0, Math.max(ctMax, CT.findIndex(x => x.key === ctOn) + 1)), contentRows = (m ? (s.contentOpen ? ctVis : []) : ctVis).map(x => { const on = x.key === ctOn; return { key: x.key, name: x.name, icon: x.icon || 'file-text', bg: on ? 'var(--rc-sand)' : 'transparent', fg: on ? 'var(--rc-on-sand)' : 'var(--ui-text-toned)', fw: on ? 500 : 400 } }) const crumbs = (s.crumbs || []).map((t, i, a) => ({ t, sep: i > 0, fg: i === a.length - 1 ? 'var(--ui-text-highlighted)' : 'var(--ui-text-muted)', fw: i === a.length - 1 ? 500 : 400 })) const L = s.list || {} const counts = L.counts || [6, 4, 2] @@ -541,7 +543,7 @@ class Component extends DCLogic { chipBg: s.userMenu || s.nav === 'profile' ? 'var(--rc-sand)' : 'transparent', chipFg: s.userMenu || s.nav === 'profile' ? 'var(--rc-on-sand)' : 'var(--ui-text-highlighted)', isSettings: k === 'settings', isProfile: k === 'profile', pfLoading: k === 'profile' && !!pf.loading, pfReady: k === 'profile' && !pf.loading, upBtnDir: m ? 'column' : 'row', upBtnAlign: m ? 'stretch' : 'flex-start', cardPad: '16px', cardPadX: m ? '16px' : '20px', inH: m ? '44px' : '40px', pfCols: m ? 'minmax(0,1fr)' : 'minmax(0,1fr) minmax(0,1.4fr)', pwDir: m ? 'column' : 'row', pwAlign: m ? 'stretch' : 'flex-end', edTitle, edBody: e.body || '', sampleBody: !e.body, tabs, rows, save, counter, pub, mFields, up, toasts, endpoints, toolbar, - desk: !m, mob: m, notBoot: !s.boot, + contentShow: CT.length > 0, contentRows, contentColl: m && !s.contentOpen, contentCount: String(CT.length), contentMore: (!m || s.contentOpen) && CT.length > ctVis.length ? 'Show all ' + CT.length : '', desk: !m, mob: m, notBoot: !s.boot, fw: m ? '390px' : '1440px', fh: m ? '844px' : '900px', dir: m ? 'column' : 'row', mLead: s.mLead || 'menu', navDrawer: m && !!s.drawer, isSlot: !!this.props.slotBody, slotBody: this.props.slotBody || null, slotOverlays: this.props.slotOverlays || null, isBoot: !!s.boot, isHome: k === 'home' && !s.boot, isList: k === 'articles', isEditor: k === 'editor', isInteg: k === 'integration', isKeys: k === 'apikeys', isError: k === 'error', isUsers: k === 'users', diff --git a/docs/design/source/RenewCMS Screens Schema.dc.html b/docs/design/source/RenewCMS Screens Schema.dc.html new file mode 100644 index 0000000..0d13488 --- /dev/null +++ b/docs/design/source/RenewCMS Screens Schema.dc.html @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + +
+
+
+

Screens: custom content types

+

Schema list, builder, change review, field-type gallery, content navigation, entry list and entry editor, plus the later screens: relations, permissions, export and import. Every state at 1440 px (shown at 50%) and 390 px (shown at 75%); the switch shows dark. Each state is tagged Available now or Later. Spec tables at the end: scope, routes, components with their Nuxt UI mapping, errors, needs backend, decisions, accessibility. Same shell, tokens and copy tone as the back-office board. Changed in AppFrame: a Schema item and a Content group in the sidebar and the mobile drawer.

+
+
+ + + +
+
+ +
+
+

{{ g.title }}

+
{{ g.route }}{{ g.api }}Auth: {{ g.auth }}
+

{{ g.note }}

+
+
+ +
+
{{ st.name }}{{ st.scopeLabel }}{{ st.trigger }}
+
+
+
+
+

Copy: {{ st.copy }}

+
+
+
+
+
+ +
+

{{ t.title }}

+
+
{{ c }}
+
{{ c }}
+
+
+
+
+
+ + + diff --git a/docs/design/source/SchemaFrame.dc.html b/docs/design/source/SchemaFrame.dc.html new file mode 100644 index 0000000..5242c98 --- /dev/null +++ b/docs/design/source/SchemaFrame.dc.html @@ -0,0 +1,426 @@ + + + + + + + + + + +
{{ main }}
+
+ + + diff --git a/docs/design/source/schema-spec.js b/docs/design/source/schema-spec.js new file mode 100644 index 0000000..1dbcd2b --- /dev/null +++ b/docs/design/source/schema-spec.js @@ -0,0 +1,178 @@ +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.']] } +] diff --git a/docs/design/states.md b/docs/design/states.md index ebf849b..dd0ffa2 100644 --- a/docs/design/states.md +++ b/docs/design/states.md @@ -54,6 +54,19 @@ Glossary pass 2, changes made: activity log lines "Removed N expired invites" an | 404 site_not_found (sitemap) | GET /v1/site/{key}/sitemap, unknown key. The body is the code `site_not_found`, shown on Integration in the code style (not rewritten as prose). | | 400 (sitemap page or size) | "The request cannot be processed due to a mismatch in the format of the body." (same text as the other routes on Integration) | | 403 owner_only vs article SEO | Integration note under the 403 row: "Site SEO defaults are owner only. Article SEO fields need only the right to edit the article." | +| 404 content_type_not_found | Page: "Content type not found · It may have been deleted, or the address may be mistyped." | +| 404 entry_not_found | Page: "Entry not found · It may have been deleted by someone else." | +| 409 content_type_key_taken | Under the key: "The key "events" is already used by another content type. Choose a different key." | +| 409 content_type_has_entries | 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." | +| 400 invalid_content_type_key | Under the key: "Use 2 to 40 lowercase letters, digits or hyphens." | +| 400 invalid_field | On the marked field, per rule (label, key, limits, default, choices). If the client missed one: "Check the fields marked in the list." | +| 409 schema_incompatible | Refusal dialog: "Nothing was changed · RenewCMS refused the changes because some entries don't fit the new fields. It doesn't say which entries." | +| 422 invalid_entry: key: required | "Fill in Title." (date: "Pick a date for Starts on.", choice: "Choose an option for Category.") | +| 422 invalid_entry: key: type | "Title must be text." / "must be a number." / "must be yes or no." / "must be a date, like 2026-11-21." | +| 422 invalid_entry: key: min, max | 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" | +| 422 invalid_entry: key: options | "Choose one of: Talk, Workshop or Meetup." | +| 409 invalid_entry: key: unique | "Another entry already uses this Title. Enter a different one." plus the alert "Not saved" | +| 422 invalid_entry: key: unknown_field | Alert "The fields of Events changed", button Reload form | | 500 – | "Couldn't save the site. Your changes are still here. Try again in a moment." / "Couldn't save the author. Your changes are still here. Try again in a moment." / "Couldn't create the author" / "The article is unchanged. Try again in a moment." (notice inside the panel or dialog) | ## Public image 400 bodies @@ -79,3 +92,15 @@ screens/.md, section "Copy per state" (generated from the board source). ## SEO round Frames: Editor: search and sharing (24), Editor: what the frontend gets (6), Sites: SEO defaults (5), Articles: search status (3). Each is drawn desktop and mobile; light and dark follow the theme switch. None is clock-sensitive: dates in the API preview are fixed sample values. Disabled state: all fields at 40% while saving. Validation: slug taken, canonical not absolute, title template without %title%, excerpt over 300. Suggestions never block saving or publishing. Copy per state: screens/editor.md, articles.md, sites.md. + + +## Custom content types +Groups on `RenewCMS Screens Schema.dc.html`: 1 Schema list (6), 2 Schema builder (16), 3 Review and delete (7), 4 Field types (6), 5 Content and entry list (9), 6 Entry editor (13), 7 Relations (3), 8 Permissions (2), 9 Export and import (3). 65 states, 56 available now and 9 later. Each has a 1440 and a 390 frame; light and dark follow the theme switch. Nothing is clock-sensitive. Tag per state: "Available now" or "Later". Copy per state: screens/schema.md, content.md, entry.md, schema-later.md. + +Roles: owners and admins see Schema and edit types. Any verified user sees the Content group and reads and writes entries. A user who is not owner or admin and opens /schema gets "You don't have access to Schema". + +Keys: a type key is 2 to 40 lowercase letters, digits and hyphens (chosen at creation, never changes). A field key is lowercase letters, digits and underscores, starts with a letter, up to 40 (editable until its field is first saved). The two inputs show their own rule. + +Schema changes with entries: Save opens the review ("Add 2 fields, change 1, remove 1"); Apply sends the PUT; schema_incompatible shows "Nothing was changed" and names the kind of change that likely caused it, never an entry. A type with entries cannot be deleted ("You can't delete Events yet"). + +Entry rules, 422 except unique (409): required, type, min, max, options, unique, unknown_field. Messages are per type and drawn in the field-type gallery (group "Field types").