--- name: admin-ui-config description: Configure the admin UI for collections — meta labels, preview/viewHint, field widgets, inputProps, 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 - Configuring collection preview and default list presentation - 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`. Always consult this file for the full API. This skill provides a practical summary. Treat this skill as Nova-first. Use current Nova concepts such as `preview`, `singleton: { enabled }`, `drillDown`, `dependsOn`, `containerProps.layout`, `pagebuilder`, `viewHint`, `subNavigation`, and AI media assist. --- ## Collection meta configuration The `meta` key in a collection YAML controls how the collection appears in the admin sidebar and collection/list UI. ```yaml 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: enabled: false hide: false # Set to true to hide the collection for non-admin users preview: label: name secondary: price ``` ### Preview Use `meta.preview` as the universal entry representation for Nova lists, breadcrumbs, foreign-key widgets, and search result previews: ```yaml preview: name preview: label: name secondary: slug badge: status preview: eval: "`${$this.firstName} ${$this.lastName}`" ``` ## List presentation For current Nova, use `meta.viewHint` plus `meta.preview` for collection/list presentation. ```yaml meta: viewHint: table preview: label: name secondary: slug badge: status table: - name - source: status label: Status - source: author.name label: Author select: - author.name ``` - `meta.viewHint` controls the preferred collection presentation (`table`, `cards`, `media`, or `navigation` object where supported). - `preview.table` defines explicit list columns for Nova. - `preview.select` can reduce lookup work for preview table columns. - `meta.subNavigation` defines filtered entry tabs in the sidebar. --- ## Field configuration Each field in the `fields` array can have a `meta` section controlling its admin UI behavior. ### Basic field with meta ```yaml 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: ```yaml # 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`: ```yaml - name: content type: string meta: widget: richtext # Rich text editor (HTML) - name: heroImage type: file meta: widget: image # Image-focused file widget - name: relatedPages type: string[] meta: widget: foreignKeyChipArray ``` Common widget types: `text`, `checkbox`, `select`, `chipArray`, `checkboxArray`, `date`, `datetime`, `file`, `image`, `richtext`, `json`, `foreignKey`, `foreignKeyChipArray`, `pagebuilder`, `containerLessObject`, `containerLessObjectArray`. Important current widgets/features to consider when designing a real website backoffice: - `pagebuilder` for CMS-driven block/page authoring - `foreignKeyChipArray` for many-reference editing - `image` plus `imageEditor` / `downscale` for image-heavy workflows - `drillDown` editing for complex nested arrays ### Choices — dropdowns/selects Static choices: ```yaml - 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: ```yaml - name: category type: string meta: choices: endpoint: categories # Collection name mapping: id: id name: name ``` ### Foreign references Link to entries in another collection: ```yaml - name: author type: string meta: label: { de: "Autor", en: "Author" } foreign: collection: users id: id sort: name projection: name,email render: label: name secondary: email createDefaults: role: author ``` Use `foreign.id: id` for the outward FK field identity. Only Mongo-style filters/query conditions use `_id`. Use `foreign.render` or target-collection `meta.preview` so references stay readable. Bare IDs are not acceptable authoring UX for a serious website project. ### Image fields ```yaml - name: image type: file meta: widget: image downscale: # Auto-resize on upload maxWidth: 1920 maxHeight: 1080 quality: 0.85 imageEditor: true # Enable crop/rotate editor ``` --- ## Layout: position, sections, sidebar ### Sidebar placement ```yaml - name: active type: boolean meta: position: sidebar # Moves field to sidebar - name: publishDate type: date meta: position: "sidebar:publishing" # Sidebar with group key ``` ### Sidebar groups (ordered) Define sidebar group order in collection meta: ```yaml meta: sidebar: - group: publishing label: { de: "Veröffentlichung", en: "Publishing" } - group: seo label: { de: "SEO", en: "SEO" } - group: settings label: { de: "Einstellungen", en: "Settings" } ``` ### Sections in main area ```yaml - 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: ```yaml - 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 ``` `containerProps.layout` is one of the most important Nova ergonomics features. Use it aggressively to avoid long, single-column forms. Recommended pattern for real projects: - sidebar for publication, SEO, flags, relations, admin-only metadata - main area for editorial content - 2-column or 3-column layout for short related fields - section headings for repeated conceptual groups --- ## Nested objects and arrays ### Object (nested group) ```yaml - 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) ```yaml - name: blocks type: object[] meta: label: { de: "Inhaltsblöcke", en: "Content Blocks" } widget: pagebuilder preview: { eval: "`${$this.type}: ${$this.headline || ''}`" } drillDown: true 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 arrays For complex `object[]` data, prefer `drillDown: true` over dense inline editing. This is especially important for: - nested content blocks - FAQs / accordions - team members with nested metadata - pricing tables / feature matrices ### Pagebuilder fields Nova supports pagebuilder configuration at both collection and field level. Typical pattern: ```yaml meta: pagebuilder: blockTypeField: type defaultViewport: desktop blockRegistry: file: /_/assets/dist/admin.mjs fields: - name: blocks type: object[] meta: widget: pagebuilder pagebuilder: blockTypeField: type ``` Use pagebuilder when editors work with heterogeneous content blocks. Use plain `object[]` only when the structure is uniform and simple. ### dependsOn Use `dependsOn` to show only fields relevant to the selected block or mode: ```yaml - name: image type: file meta: dependsOn: eval: $parent.type == 'hero' ``` This is critical for keeping pagebuilder schemas usable. ### AI-aware media and admin features Current Nova types support AI-related admin capabilities, especially around media workflows. When appropriate for a project: - use AI-assisted alt/caption generation for image-heavy collections - prefer explicit target fields for generated metadata - keep AI assist opt-in and editorially reviewable Use AI only where it improves authoring quality; do not force it into every collection. ## Field-level permissions and authoring safety Current tibi-server supports `readonlyFields`, `hiddenFields`, and eval-based field visibility/readonly rules. Reflect these server rules in admin design: - do not put critical computed fields front-and-center if editors may not be allowed to modify them - use `dependsOn`, `hidden`, and readonly semantics deliberately - remember that server-side permissions are authoritative even if the UI looks editable ### Drill-down For complex nested objects, use `drillDown` to render them as a sub-page: ```yaml - 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. ```typescript 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`. The output includes the admin module and 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 ```yaml name: products meta: label: { de: "Produkte", en: "Products" } muiIcon: inventory_2 group: shop viewHint: table defaultSort: field: insertTime order: DESC preview: label: name secondary: sku badge: active table: - name - sku - source: price label: { de: "Preis", en: "Price" } - source: category label: { de: "Kategorie", en: "Category" } sidebar: - group: publishing label: { de: "Veröffentlichung", en: "Publishing" } - group: seo label: { de: "SEO", en: "SEO" } permissions: public: methods: get: true user: methods: get: true post: true put: true delete: true fields: - name: active type: boolean meta: label: { de: "Aktiv", en: "Active" } position: "sidebar:publishing" - 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: image 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` supports both strings and i18n objects** — Use i18n objects only when the collection or field label must be localized. - **`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.