Customization
Generated UI previewREGISTRY
Customized users
| # | Actions | Name | Status | Display | |
|---|---|---|---|---|---|
| 1 | Ada Lovelace | ada@example.test | active | Ada Lovelace <ada@example.test> | |
| 2 | Grace Hopper | grace@example.test | pending | Grace Hopper <grace@example.test> |
Every highlighted piece is registry-driven here: the computed “Display” column, the status filter’s option loader, the custom Notes field, and the email validator. This demo only grants the archive permission (not delete), so try archiving a row and notice Delete stays disabled — that is the permission resolver below at work.
Customization in VibeFlui is registry-driven.
The schema references runtime behavior by string keys. The registry defines the actual functions or components.
File structure
TXT
schemas/
users/
customized-users.schema.ts
lib/
vibeflui/
registry.tsx
components/
vibeflui/
fields/
NotesField.tsx
renderers/
status/
StatusBadge.tsx
app/
users/
page.tsxSchema snippets
schemas/users/customized-users.schema.ts
TS
export const customizedUsersSchema = {
resource: "users",
title: "Customized users",
identity: {
key: "id",
param: "id"
},
table: {
columns: [
{ key: "name", label: "Name", searchable: true },
{ key: "email", label: "Email", searchable: true },
{ key: "status", label: "Status", renderer: "StatusBadge" },
{ key: "displayName", label: "Display", type: "computed", computed: "users.displayName" }
],
filters: [
{
key: "status",
label: "Status",
type: "select",
optionLoader: "users.statusOptions"
}
]
},
form: {
fields: [
{ name: "name", label: "Name", required: true, transformer: "trim" },
{ name: "email", label: "Email", type: "email", required: true, validator: "users.email" },
{ name: "status", type: "select", optionLoader: "users.statusOptions" },
{ name: "notes", label: "Notes", type: "custom", component: "NotesField" }
]
},
actions: {
create: { enabled: true, handler: "users.create" },
delete: {
enabled: true,
confirm: {
title: "Delete user?",
description: "This action cannot be undone.",
secondConfirm: {
title: "Delete permanently?",
confirmLabel: "Delete permanently"
}
},
permission: "users.delete"
},
row: [
{
key: "archive",
label: "Archive",
icon: "mdi:archive-outline",
handler: "users.archive",
permission: "users.archive"
}
]
},
messages: {
defaults: {
createSuccess: "User saved successfully."
}
},
hooks: {
beforeCreate: "audit.beforeCreate"
},
events: {
rowClicked: "analytics.rowClicked"
},
registry: {
renderers: ["StatusBadge"],
components: ["NotesField"],
validators: ["users.email"],
transformers: ["trim"],
actionHandlers: ["users.create", "users.archive"],
permissionResolvers: ["users.delete", "users.archive"],
optionLoaders: ["users.statusOptions"],
hooks: ["audit.beforeCreate"],
events: ["analytics.rowClicked"],
computedResolvers: ["users.displayName"]
}
} as const;Renderer component
components/vibeflui/renderers/status/StatusBadge.tsx
TSX
export function StatusBadge({ value }: { value?: unknown }) {
return <span>{String(value ?? "-")}</span>;
}Custom field component
components/vibeflui/fields/NotesField.tsx
TSX
type NotesFieldProps = {
value?: unknown;
onChange?: (value: string) => void;
};
export function NotesField({ value, onChange }: NotesFieldProps) {
return (
<textarea
className="min-h-24 w-full rounded-md border px-3 py-2"
value={String(value ?? "")}
onChange={(event) => onChange?.(event.target.value)}
/>
);
}Registry snippets
lib/vibeflui/registry.tsx
TSX
import { createRegistry } from "@vibeflui/core";
import { NotesField } from "@/components/vibeflui/fields/NotesField";
import { StatusBadge } from "@/components/vibeflui/renderers/status/StatusBadge";
export const vibefluiRegistry = createRegistry({
renderers: {
StatusBadge
},
components: {
NotesField
},
validators: {
"users.email": async (value) =>
String(value ?? "").endsWith("@example.test") ? undefined : "Use an example.test email"
},
transformers: {
trim: (value) => (typeof value === "string" ? value.trim() : value)
},
optionLoaders: {
"users.statusOptions": async () => [
{ label: "Active", value: "active" },
{ label: "Pending", value: "pending" },
{ label: "Suspended", value: "suspended" }
]
},
actionHandlers: {
"users.create": async ({ values }) => ({ status: true, code: 200, message: "User created", data: values }),
"users.archive": async ({ row }) => ({ status: true, code: 200, message: `${String(row?.name ?? "User")} archived` })
},
permissionResolvers: {
"users.delete": ({ permissionContext }) => Boolean(permissionContext?.permissions?.includes("users.delete")),
"users.archive": ({ permissionContext }) => Boolean(permissionContext?.permissions?.includes("users.archive"))
},
hooks: {
"audit.beforeCreate": async () => undefined
},
events: {
"analytics.rowClicked": async () => undefined
},
computedResolvers: {
"users.displayName": ({ row }) => `${String(row?.name ?? "")} <${String(row?.email ?? "")}>`
}
});Page usage
app/users/page.tsx
TSX
<FluiKitProvider
registry={vibefluiRegistry}
permissionContext={{ permissions: ["users.archive"] }}
>
<FluiKit schema={customizedUsersSchema} data={rows} />
</FluiKitProvider>Notes
- Use registry validators for custom runtime validation.
- Keep final validation and authorization on the backend.
- Use schema strings to reference runtime behavior. Keep functions in the registry instead of schema objects.