# Customizing the UI
Three tiers, from no code to your own build. Use the lowest one that
does the job.
1. **Descriptor and settings.** Fields, filters, actions, widgets,
navigation and the **Branding** settings. No JavaScript.
2. **Runtime extensions.** ES modules the admin loads at startup, adding
cells, inputs, widget renderers and pages. JavaScript, no rebuild.
3. **Your own build.** A Nuxt application extending the layer,
overriding any of its files (Nuxt layer semantics: the same path in
your application wins), registering global components named in
`app.config`, and generating its own bundle; in hosted mode, point
`VECTOR_UI_DIR` at it ({doc}`deployment`).
## Application config
A Nuxt application extending `@kinetis/vector-ui` sets:
```ts
// app/app.config.ts
export default defineAppConfig({
vector: {
brand: { title: 'My Admin' }, // before the Branding settings load
basePath: '', // a router prefix inside the app
commandPalette: true, // Ctrl/⌘+K search
usersResource: 'users', // enables Impersonate on those records
idleMinutes: 30, // sign out after this long unused; 0 never
fields: {}, inputs: {}, widgets: {}, // component overrides
locale: 'en', messages: {}, // see Languages
pages: {}, // custom pages, by name
},
})
```
## Cells, inputs and widget renderers
Three registries decide what renders a value: **cells** (lists and
record pages, the layer's `VxCell/*`), **inputs** (forms, `VxInput/*`)
and **widget renderers** (`VxWidget/*`). The `app.config` overrides are
globally registered component names, keyed by field type, by a field's
`component` name or by widget type. For a field, the lookup order is:
1. an `app.config` override by the field's `component('name')`;
2. a runtime extension registered under that name;
3. an `app.config` override by field type;
4. the built-in for the type.
A cell receives `value`, `field`, `record`, `resource` and `context`
(`index` or `show`). An input receives `modelValue`, `field`, `rules`,
`errorMessages`, `record` and `resource` and emits `update:modelValue`.
A widget renderer receives `data` and `widget`.
## Runtime extensions
An extension is a plain ES module. List it in `VECTOR_UI_EXTENSIONS`
(hosted) or `runtimeConfig.public.vector.extensions` (standalone). Its
default export receives `window.Vector`, which hands it Vue, a curated
set of Vuetify components (`VAlert`, `VAutocomplete`, `VBtn`, `VCard`,
`VCheckbox`, `VChip`, `VIcon`, `VProgressLinear`, `VRating`, `VSelect`,
`VSwitch`, `VTable`, `VTextField`, `VTextarea`) and the registration
functions. For a table, render `VTable` with your `thead` and `tbody` as
its content: Vuetify's table styles belong to its wrapper, so the class
on a bare `
` leaves the columns unaligned.
```js
export default function ({ vue, vuetify, defineCell }) {
defineCell('stock-level', {
props: { value: {}, field: {}, record: {}, resource: {}, context: {} },
setup: props => () => vue.h(vuetify.VChip, { size: 'small' }, () => String(props.value)),
})
}
```
```php
Number::make('stock')->component('stock-level'),
```
An extension is served under `public/`, same-origin or CORS-enabled. The
demo's `public/vector-extensions/stock-level.js` is this one.
`window.Vector` also offers `defineInput`, `defineWidget`, `definePage`,
`defineMessages`, `defineRichTextExtension`, `api` (the admin's API
client, with its credentials), `notify`, `navigate`, `t` (the admin's
translator) and `tiptap` (`Node`, `Mark`, `Extension`, `mergeAttributes`,
to build a rich-text node without a bundler).
## Rich-text extensions
A node or mark type — a callout, an embed — joins the rich-text schema
on both sides:
```js
export default function ({ tiptap, defineRichTextExtension }) {
defineRichTextExtension('callout', tiptap.Node.create({
name: 'callout', group: 'block', content: 'paragraph+',
addAttributes: () => ({ tone: { default: 'info' } }),
parseHTML: () => [{ tag: 'aside[data-callout]' }],
renderHTML: ({ HTMLAttributes }) => ['aside', tiptap.mergeAttributes(HTMLAttributes, { 'data-callout': '' }), 0],
}))
}
```
```php
$app->instance(Document::class, (new Document())->extend(new NodeSpec('callout', ['tone'])));
```
Bind the extended `Document` in `bootstrap.php`. A `NodeSpec` may name
its own HTML renderer for `Document::toHtml()`
(`new NodeSpec('callout', ['tone'], renderer: CalloutNode::class)`). A
field then lists `callout` in its `features()`. The server accepts the
node only once registered, so both sides agree.
## Custom pages
A custom page is a menu entry and a route, `/p/{name}`:
```js
export default function ({ vue, definePage }) {
definePage('stock-overview', {
title: 'Stock overview', icon: 'mdi-warehouse', group: 'Catalog',
component: {
props: { vector: Object },
setup(props) {
const data = vue.ref(null)
vue.onMounted(async () => { data.value = await props.vector.api('/stock-overview') })
return () => vue.h('pre', JSON.stringify(data.value, null, 2))
},
},
})
}
```
`group` places the page under a menu section (a resource group's name,
or its own); without one it goes under *Pages*. The component receives
one prop, `vector`, with `api` (the admin's API client, carrying the
bearer token or the session cookie and CSRF token),
`notify.success/error` and `navigate(path)`. An application extending
the layer can instead list the page in `app.config` `vector.pages`,
naming a component it registered globally:
`{ stock: { title: 'Stock', icon: 'mdi-warehouse', component: 'StockPage' } }`.
An endpoint the page calls is your own code: put it in the `vector`
middleware group (`#[Middleware('@vector')]`) and the admin's
authentication and access policy apply to it as to Vector's own
routes.
## Forms in your own pages
`VxResourceForm` is the generated form. It tracks unsaved changes
(`dirty`, exposed with `reset()`), and with `variants` — as the create
and edit pages use it — offers the save variants (back to the record,
continue editing, add another), remembers the last one as the user
preference `form.saveVariant`, and saves with Ctrl/⌘+S.
`saved` emits the record and the variant. The pages guard leaving a
changed form through `useLeaveGuard()`, the same confirm dialog the
list's unsaved inline edits use.
## Theming
The layer sets Vuetify's defaults and both themes in its own
`nuxt.config.ts`; your application overrides any of them. At runtime,
the **Branding** settings apply over them.
## Safety
Script authority is the deployer's: `VECTOR_SHELL_HEAD` and the
extension list are configuration. No setting, field or descriptor value
is ever rendered as markup or executed.
## See also
- {doc}`languages` — translating the admin.
- {doc}`deployment` — where extensions are served from.