Files
tibi-svelte-starter/.agents/skills/admin-ui-config/SKILL.md
Sebastian Frank a9a13a6b5b feat: add admin-ui-config, content-authoring, and frontend-architecture skills documentation
- 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.
2026-03-07 16:16:19 +00:00

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.label is i18n — Always provide { de: "...", en: "..." } objects, not plain strings.
  • views order matters — First matching view (by mediaQuery) is shown. Put mobile views (with mediaQuery) before desktop views (without).
  • choices.id must match stored value — The id in choices is what gets saved to the database.
  • inputProps depends on widget — Not all props work with all widgets. Check tibi-admin-nova source if unsure.
  • position: sidebar without group — Fields go to an ungrouped area. Use position: "sidebar:GroupName" for grouping.
  • type: object[] needs subFields — Forgetting subFields renders an empty repeater.
  • hooks path — Hook includes are relative to api/ directory: !include hooks/myfile.js.