- Introduced `admin-ui-config` skill for configuring admin UI for collections. - Added `content-authoring` skill detailing page and block creation in the CMS. - Included `frontend-architecture` skill explaining custom SPA routing and state management. - Updated `AGENTS.md` to reference new skills and provide infrastructure prerequisites. - Enhanced `frontend/AGENTS.md` with routing details and SPA navigation information.
13 KiB
name, description
| name | description |
|---|---|
| admin-ui-config | Configure the admin UI for collections — meta labels, views (table/list/cards), field widgets, inputProps, fieldLists, sidebar layout, choices, foreign references, and image handling. Use when setting up or customizing collection admin views. |
admin-ui-config
When to use this skill
Use this skill when:
- Configuring how a collection appears in the tibi-admin UI
- Setting up table/list/card views for a collection
- Configuring field widgets (dropdowns, media pickers, richtext, etc.)
- Organizing fields into sidebar groups or sections
- Setting up foreign key references between collections
- Customizing the admin module (
frontend/src/admin.ts)
Reference source
The canonical type definitions are in tibi-admin-nova/types/admin.d.ts (1296 lines). Always consult this file for the full API. This skill provides a practical summary.
Collection meta configuration
The meta key in a collection YAML controls how the collection appears in the admin sidebar and list views.
name: mycollection
meta:
label: { de: "Produkte", en: "Products" } # Sidebar label (i18n)
muiIcon: shopping_cart # Material UI icon name
group: shop # Group in admin sidebar
singleton: false # true = only one entry allowed
hideInNavigation: false # true = don't show in sidebar
defaultSort: "-insertTime" # Default sort (prefix - = descending)
rowIdentTpl: { twig: "{{ name }} ({{ price }})" } # Row display template
Row identification
rowIdentTpl uses Twig syntax with field names. Used in admin list to identify entries:
rowIdentTpl: { twig: "{{ name }}" } # Simple
rowIdentTpl: { twig: "{{ type }} — {{ language }}" } # Combined
Views: table, simpleList, cardList
The views array defines how entries are displayed in the admin list. Multiple views can coexist (e.g. table for desktop, simpleList for mobile).
Table view
meta:
views:
- type: table
columns:
- name # Simple: field name as column
- source: lang # With filter
filter: true
- source: active # Boolean column with filter
filter: true
- source: price # Custom label
label: { de: "Preis", en: "Price" }
- source: insertTime # Date field
width: 160
Simple list view (mobile)
meta:
views:
- type: simpleList
mediaQuery: "(max-width: 600px)" # Show only on small screens
primaryText: name
secondaryText: lang
tertiaryText: path
image: thumbnail # Optional: show image thumbnail
Card list view
meta:
views:
- type: cardList
fields:
- source: name
label: Name
- source: price
label: Preis
widget: currency
Field configuration
Each field in the fields array can have a meta section controlling its admin UI behavior.
Basic field with meta
fields:
- name: name
type: string
meta:
label: { de: "Name", en: "Name" }
helperText: { de: "Anzeigename", en: "Display name" }
position: main # "main" (default) or "sidebar"
Field types
YAML type |
Admin widget (default) | Notes |
|---|---|---|
string |
Text input | Use inputProps.multiline: true for textarea |
number |
Number input | |
boolean |
Toggle/checkbox | |
date |
Date picker | |
object |
Nested field group | Requires subFields |
object[] |
Repeatable group | Requires subFields, drag-to-reorder |
string[] |
Tag input | |
file |
File upload | |
file[] |
Multi-file upload |
inputProps — widget customization
inputProps passes props directly to the field widget:
# Multiline text (textarea)
- name: description
type: string
meta:
label: { de: "Beschreibung", en: "Description" }
inputProps:
multiline: true
rows: 5
# Number with min/max
- name: price
type: number
meta:
inputProps:
min: 0
max: 99999
step: 0.01
# Placeholder text
- name: email
type: string
meta:
inputProps:
placeholder: "name@example.com"
Widget override
Override the default widget with meta.widget:
- name: content
type: string
meta:
widget: richtext # Rich text editor (HTML)
- name: color
type: string
meta:
widget: color # Color picker
- name: image
type: string
meta:
widget: medialib # Media library picker
Common widget types: text (default), richtext, color, medialib, code, markdown, password, hidden.
Choices — dropdowns/selects
Static choices:
- name: type
type: string
meta:
label: { de: "Typ", en: "Type" }
choices:
- id: page
name: { de: "Seite", en: "Page" }
- id: blog
name: { de: "Blog", en: "Blog" }
- id: product
name: { de: "Produkt", en: "Product" }
Dynamic choices from API:
- name: category
type: string
meta:
choices:
endpoint: categories # Collection name
mapping:
id: _id
name: name
Foreign references
Link to entries in another collection:
- name: author
type: string
meta:
label: { de: "Autor", en: "Author" }
foreign:
collection: users
id: _id
sort: name
projection: name,email
render: { twig: "{{ name }} <{{ email }}>" }
autoFill: # Auto-fill other fields on selection
- source: email
target: authorEmail
Image fields
- name: image
type: file
meta:
widget: medialib
downscale: # Auto-resize on upload
maxWidth: 1920
maxHeight: 1080
quality: 0.85
imageEditor: true # Enable crop/rotate editor
Layout: position, sections, sidebar
Sidebar placement
- name: active
type: boolean
meta:
position: sidebar # Moves field to sidebar
- name: publishDate
type: date
meta:
position: "sidebar:Veröffentlichung" # Sidebar with group header
Sidebar groups (ordered)
Define sidebar group order in collection meta:
meta:
sidebar:
- Veröffentlichung
- SEO
- Einstellungen
Sections in main area
- name: seoTitle
type: string
meta:
section: SEO # Groups fields under a section header
- name: seoDescription
type: string
meta:
section: SEO
Grid layout (columns)
Use containerProps for multi-column layout:
- name: firstName
type: string
meta:
containerProps:
layout:
size: col-6 # Half width (12-column grid)
- name: lastName
type: string
meta:
containerProps:
layout:
size: col-6
Nested objects and arrays
Object (nested group)
- name: address
type: object
meta:
label: { de: "Adresse", en: "Address" }
subFields:
- name: street
type: string
- name: city
type: string
- name: zip
type: string
Object array (repeatable blocks)
- name: blocks
type: object[]
meta:
label: { de: "Inhaltsblöcke", en: "Content Blocks" }
preview: { eval: "item.type + ': ' + (item.headline || '')" }
subFields:
- name: type
type: string
meta:
choices:
- id: hero
name: Hero
- id: richtext
name: Richtext
- name: headline
type: string
- name: hide
type: boolean
The preview eval determines what's shown in the collapsed state of each array item.
Drill-down
For complex nested objects, use drillDown to render them as a sub-page:
- name: variants
type: object[]
meta:
drillDown: true # Opens as sub-page instead of inline
Admin module (frontend/src/admin.ts)
The admin.ts file exports custom Svelte components for injection into the tibi-admin UI. Components are rendered inside Shadow DOM to isolate styles.
import type { SvelteComponent } from "svelte"
function getRenderedElement(
component: typeof SvelteComponent,
options?: { props: { [key: string]: any }; addCss?: string[] },
nestedElements?: { tagName: string; className?: string }[]
) {
// Creates a Shadow DOM container, mounts the Svelte component inside
// addCss: CSS files to inject into Shadow DOM
// nestedElements: wrapper elements inside Shadow DOM
}
export { getRenderedElement }
Build with yarn build:admin. The output is loaded by tibi-admin-nova as a custom module.
Use case: Custom dashboard widgets, preview components, or field widgets that require Svelte rendering inside the admin UI.
Complete collection example
name: products
meta:
label: { de: "Produkte", en: "Products" }
muiIcon: inventory_2
group: shop
defaultSort: "-insertTime"
rowIdentTpl: { twig: "{{ name }} ({{ sku }})" }
sidebar:
- Veröffentlichung
- SEO
views:
- type: simpleList
mediaQuery: "(max-width: 600px)"
primaryText: name
secondaryText: sku
image: image
- type: table
columns:
- name
- sku
- source: price
label: { de: "Preis", en: "Price" }
- source: active
filter: true
- source: category
filter: true
permissions:
public:
methods:
get: true
user:
methods:
get: true
post: true
put: true
delete: true
hooks:
beforeRead: |
!include hooks/filter_public.js
afterWrite: |
!include hooks/clear_cache.js
fields:
- name: active
type: boolean
meta:
label: { de: "Aktiv", en: "Active" }
position: "sidebar:Veröffentlichung"
- name: name
type: string
meta:
label: { de: "Name", en: "Name" }
- name: sku
type: string
meta:
label: { de: "Artikelnummer", en: "SKU" }
containerProps:
layout:
size: col-6
- name: price
type: number
meta:
label: { de: "Preis", en: "Price" }
inputProps:
min: 0
step: 0.01
containerProps:
layout:
size: col-6
- name: category
type: string
meta:
label: { de: "Kategorie", en: "Category" }
choices:
- id: electronics
name: { de: "Elektronik", en: "Electronics" }
- id: clothing
name: { de: "Kleidung", en: "Clothing" }
- name: description
type: string
meta:
label: { de: "Beschreibung", en: "Description" }
inputProps:
multiline: true
rows: 4
- name: image
type: file
meta:
label: { de: "Produktbild", en: "Product Image" }
widget: medialib
downscale:
maxWidth: 1200
quality: 0.85
- name: seoTitle
type: string
meta:
label: { de: "SEO Titel", en: "SEO Title" }
position: "sidebar:SEO"
- name: seoDescription
type: string
meta:
label: { de: "SEO Beschreibung", en: "SEO Description" }
position: "sidebar:SEO"
inputProps:
multiline: true
rows: 3
Common pitfalls
meta.labelis i18n — Always provide{ de: "...", en: "..." }objects, not plain strings.viewsorder matters — First matching view (bymediaQuery) is shown. Put mobile views (withmediaQuery) before desktop views (without).choices.idmust match stored value — Theidin choices is what gets saved to the database.inputPropsdepends on widget — Not all props work with all widgets. Check tibi-admin-nova source if unsure.position: sidebarwithout group — Fields go to an ungrouped area. Useposition: "sidebar:GroupName"for grouping.type: object[]needssubFields— ForgettingsubFieldsrenders an empty repeater.- hooks path — Hook includes are relative to
api/directory:!include hooks/myfile.js.