Feature: Schema list and content navigation in the back-office As an owner or admin I want to see the custom and built-in content types on a Schema screen And as any verified user I want a Content menu with one link per custom type So that I can reach the content types I manage and write entries for Background: Given the SPA calls the API with relative URLs under /v1 and the "jwt" HttpOnly cookie And GET /v1/content-type returns a JSON array of {id, key, name, icon, fields, created_at, updated_at} And GET /v1/builtin-type returns a JSON array of {key, name, fields}, for owners and admins only And API errors are plain text And the session store exposes the role of the signed-in user, and only owner and admin exist today And a session whose role is neither owner nor admin (absent or unknown) is a test double for the forbidden states And the Schema screen is the route /schema with meta auth "verified", title "Schema" And only owners and admins can use Schema; the SPA decides from the session role, the API still enforces it And the typed API client exposes listContentTypes() and listBuiltinTypes() And a content types store loads them once per session and exposes loading, error and items And this slice does not add the Vitest runner, it already exists (task test:web) Scenario Outline: The Schema entry is shown to owners and admins only Given a verified user with role "" is signed in When the back-office shell renders Then the sidebar a "Schema" entry linking to /schema, with the "shapes" icon, placed after "Users" And no existing nav item changed Examples: | role | schema_entry | | owner | shows | | admin | shows | | unknown | does not show | Scenario: The Content group lists one link per custom type Given a verified user is signed in And GET /v1/content-type returns the types "events" (Events), "recipes" (Recipes) and "press-mentions" (Press mentions) When the back-office shell renders Then the sidebar shows a "Content" group under the main items And the group contains three links, in the API order, to /content/events, /content/recipes and /content/press-mentions And each row shows the type name And the row of the type matching the current route /content/:key is marked as the current page Scenario Outline: Each type shows its icon, or the default icon Given a custom type "recipes" with icon When the sidebar renders Then the row for "recipes" shows Examples: | icon | shown | | "chef-hat" | the icon i-lucide-chef-hat (in bundle) | | null | the default icon "files" | | "not-bundled" | the default icon "files" | Scenario: A long type name is truncated in one line and keeps its full name available Given a custom type named "Frequently asked questions and answers" When the sidebar renders on desktop Then the label is cut with an ellipsis on one line and does not widen the sidebar And the full name is available in a tooltip and as the link's accessible name Scenario: With more than 5 types the desktop group shows 5 and the selected one Given 14 custom types and the current route is /content/ When the sidebar renders on desktop Then the group shows the first 5 types, plus the selected type if it is not among them And a "Show all 14" action follows When the user activates "Show all 14" Then all 14 types are listed Scenario: With 5 types or fewer there is no "Show all" Given 5 custom types When the sidebar renders Then all 5 are listed and no "Show all" action appears Scenario: On mobile the group is one collapsed row in the drawer Given 14 custom types and a 390 px wide viewport When the user opens the drawer Then the Content group shows one collapsed row "All content types" with the count 14 When the user expands it Then the 14 types are listed with rows 44 px high Scenario: No custom type hides the group Given GET /v1/content-type returns [] When the shell renders Then there is no Content group and no empty placeholder in the sidebar Scenario Outline: A failing content type request does not break the shell Given GET /v1/content-type When the shell renders Then the Content group is not shown And the rest of the sidebar and the page render normally Examples: | failure | | answers 500 | | fails (network) | Scenario: The Content group is loaded once for the whole session Given the user navigates between several back-office screens Then GET /v1/content-type is called once and the list is shared by the sidebar and the Schema list Scenario: Populated list Given an owner or admin is signed in And GET /v1/content-type returns three types And GET /v1/builtin-type returns articles, authors, sites, media and users When they open /schema Then the page shows the title "Schema" and the text "Content types and the fields they are made of." And a "New content type" action is enabled And a table has the columns "Name", "Key", "Fields", "Last updated" And each custom row shows name, key, the number of fields and the updated date as an absolute date, using the board's sample data And after the custom rows come the five built-in rows, each with a lock icon and a "Built-in" badge And a built-in row shows its field count from the length of fields and an em dash as Last updated And the text "Built-in types are managed by RenewCMS. You can read their fields, but not change them." is shown And the summary "3 content types ยท 5 built-in" is shown And below 640 px the table is a list of cards with the same content Scenario: Loading Given GET /v1/content-type has not answered When an owner opens /schema Then no skeleton is shown during the first 150 ms And after 150 ms skeleton rows are shown And "New content type" is disabled And the 403 page and the empty state are not shown Scenario: Empty, no custom type yet Given GET /v1/content-type returns [] When an owner opens /schema Then the empty state "No content types yet" is shown with the text "Create a content type to manage your own kind of content, like events or recipes. The built-in types stay as they are." And a "New content type" action is shown and enabled And the five built-in types are still listed below Scenario Outline: Error Given GET /v1/content-type When an owner opens /schema Then an alert shows "Couldn't load content types" And it shows "GET /v1/content-type failed. Check your connection and try again." And a "Try again" action is shown And no table is shown Examples: | failure | | answers 500 | | fails (network) | Scenario: Retry recovers Given the list is in the Error state When the user activates "Try again" Then the skeleton rows show after 150 ms and GET /v1/content-type is called again And when it answers 200 the Populated state replaces the alert Scenario: Forbidden for a user who is not owner or admin Given a verified user whose role is neither owner nor admin is signed in When they open /schema directly Then the URL stays /schema and nothing redirects And the page shows "You don't have access to Schema" And it shows "Only owners and admins can change content types. You can still add and edit entries from the Content menu." And a "Go to Home" action navigates to /home And GET /v1/builtin-type is never called Scenario: The API answers 403 for the list Given the session says owner or admin but GET /v1/builtin-type answers 403 When they open /schema Then the same "You don't have access to Schema" page is shown Scenario: Built-in types fail to load while custom types load Given GET /v1/content-type answers 200 and GET /v1/builtin-type answers 500 When an owner opens /schema Then the Error state is shown Scenario: An unverified or signed-out user keeps the existing redirects Given the user is not verified or not signed in When they open /schema Then they are redirected to /register/pending or /login as for any other "verified" route Scenario: New content type Given the list is Populated or Empty When an owner activates "New content type" Then the router navigates to /schema/new (the builder is a later slice) Scenario: Rows are not links in this slice Given the list is Populated Then custom and built-in rows do not navigate (the editor and the read-only view are later slices) Scenario Outline: The typed client maps responses When the client calls And the API answers Then it Examples: | call | answer | result | | listContentTypes | 200 with icon null | returns the types, icon typed string or null | | listContentTypes | 200 [] | returns an empty array | | listContentTypes | 500 plain text | rejects with an error carrying status 500 | | listContentTypes | network failure | rejects with a network error | | listBuiltinTypes | 200 | returns {key, name, fields} items | | listBuiltinTypes | 403 plain text | rejects with an error carrying status 403 | | listBuiltinTypes | 500 plain text | rejects with an error carrying status 500 |