Advanced Patterns
Account workspace
Overview
Users
-
Pending approvals
-
| # | Actions | Name | Status | |
|---|---|---|---|---|
| No data available. | ||||
The Overview cards and Users tab read from the same root mock provider. Requests runs as a nested mock resource under the same tabbed workspace — approving a request (with its confirm dialog) refreshes that tab from the mock in place.
This page has two parts. First, one complete, coherent example — an account workspace — whose schema, registry, and page file are exactly what generates the "Generated UI preview" panel above, with a file structure that matches those files one-to-one. Second, a separate "Other patterns" section of standalone illustrative snippets for capabilities the main example doesn't use; those are not part of the file structure below and are not meant to be combined into one app.
Read the basic, client-side, server-side, and other advanced examples first — this page assumes you already understand resources, dashboard, permissions, and row actions individually.
Account workspace example
This is the multi-resource pattern rendered above: dashboard cards, a tabbed workspace whose first tab is the root account resource (a Users table with a permission-gated edit/delete form) and whose second tab is a nested requests resource with a custom "Approve" row action.
File structure
schemas/
account/
account.schema.ts
requests/
request.resource.schema.ts
lib/
vibeflui/
registry.tsx
app/
account/
page.tsxSchema files
schemas/requests/request.resource.schema.ts
export const requestResourceSchema = {
resource: "requests",
title: "Requests",
mode: "server",
identity: { key: "id", param: "id" },
endpoint: "/api/requests",
provider: {
type: "rest"
},
table: {
dataPath: "data",
totalPath: "total",
columns: [
{ key: "requester", label: "Requester", searchable: true },
{ key: "type", label: "Type" },
{ key: "status", label: "Status" }
]
},
actions: {
create: { enabled: false },
edit: { enabled: false },
delete: { enabled: false },
row: [
{
key: "approve",
label: "Approve",
icon: "mdi:check-circle-outline",
handler: "requests.approve",
refreshOnSuccess: true,
confirm: {
title: "Approve request?",
confirmLabel: "Approve"
}
}
]
},
registry: {
actionHandlers: ["requests.approve"]
}
} as const;schemas/account/account.schema.ts imports the schema above as a nested resource — a schema file may import another schema file, but a schema object is still only ever defined in its own dedicated file.
import { requestResourceSchema } from "@/schemas/requests/request.resource.schema";
export const accountSchema = {
resource: "account",
title: "Account workspace",
mode: "server",
identity: { key: "id", param: "id" },
endpoint: {
list: "/api/account/users",
update: { url: "/api/account/users/:id", method: "PATCH" },
delete: { url: "/api/account/users/:id", method: "DELETE" }
},
provider: {
type: "rest"
},
dashboard: {
enabled: true,
title: "Overview",
cards: [
{ key: "users", label: "Users", valueFrom: "total", format: "number", icon: "mdi:account-group-outline" },
{
key: "pendingApprovals",
label: "Pending approvals",
valueFrom: "meta.pendingApprovals",
format: "number",
icon: "mdi:clock-alert-outline"
}
]
},
layout: {
type: "tabs"
},
table: {
dataPath: "data",
totalPath: "total",
columns: [
{ key: "name", label: "Name", searchable: true },
{ key: "email", label: "Email", searchable: true },
{ key: "status", label: "Status" }
]
},
form: {
mode: "modal",
fields: [
{ name: "name", label: "Name", required: true },
{ name: "email", label: "Email", type: "email", required: true },
{
name: "status",
label: "Status",
type: "select",
options: [
{ label: "Active", value: "active" },
{ label: "Invited", value: "invited" }
]
}
]
},
actions: {
create: { enabled: false },
edit: { enabled: true, permission: "users.write" },
delete: { enabled: true, permission: "users.delete", confirm: true }
},
registry: {
permissionResolvers: ["users.write", "users.delete"]
},
resources: [requestResourceSchema]
} as const;dashboard.cards[].valueFrom: "meta.pendingApprovals" reads a value nested under the root resource's own list response (meta.pendingApprovals) — the backend for /api/account/users includes that count alongside the users data/total so the dashboard card doesn't need a second request. resources: [requestResourceSchema] is what turns this into a tabbed multi-resource page: the root resource (account, since it has its own table) renders as the first tab, and each entry in resources becomes an additional tab after it.
Registry file
lib/vibeflui/registry.tsx
import { createRegistry } from "@vibeflui/core";
export const vibefluiRegistry = createRegistry({
permissionResolvers: {
"users.write": ({ permissionContext }) => Boolean(permissionContext?.permissions?.includes("users.write")),
"users.delete": ({ permissionContext }) => Boolean(permissionContext?.permissions?.includes("users.delete"))
},
actionHandlers: {
"requests.approve": async ({ row }) => ({
status: true,
code: 200,
message: `Request from ${String(row?.requester ?? "requester")} approved`
})
}
});Page file
app/account/page.tsx
"use client";
import { FluiKit, FluiKitProvider } from "@vibeflui/core";
import { accountSchema } from "@/schemas/account/account.schema";
import { vibefluiRegistry } from "@/lib/vibeflui/registry";
export default function AccountPage() {
return (
<FluiKitProvider
registry={vibefluiRegistry}
permissionContext={{ permissions: ["users.write", "users.delete"] }}
>
<FluiKit schema={accountSchema} />
</FluiKitProvider>
);
}What renders
Two dashboard cards ("Users", "Pending approvals") sit above a tabbed workspace. The "Account workspace" tab is the root account table: search, a modal edit form, and delete — both gated by permission, so they only appear when permissionContext.permissions includes the matching key. The "Requests" tab is the nested requests resource: a plain table with one custom row action, "Approve", that opens a confirm dialog and, on success, reloads that tab's data because refreshOnSuccess: true.
Other patterns
The snippets below are standalone and illustrative — each shows one capability in isolation, using its own unrelated example resource. They are not part of the file structure above, are not combined with each other, and don't power the preview at the top of this page. Copy the relevant snippet into your own schema when you need that capability.
Dashboard cards with different value formats
schemas/admin/overview.schema.ts
dashboard: {
enabled: true,
title: "Overview",
cards: [
{ key: "users", label: "Users", valueFrom: "summary.users", format: "number", icon: "mdi:account-group-outline" },
{ key: "revenue", label: "Revenue", valueFrom: "summary.revenue", format: "currency", icon: "mdi:cash" }
]
}Dashboard cards can read from the data or response object passed to FluiKit, including provider responses loaded by the root schema. Card endpoints use the configured custom provider when available, then fall back to VibeFlui's REST endpoint request helper.
Detail surface
VibeFlui supports detail fields and detail surfaces. Use this as table plus detail, not as a separate named master-detail engine.
schemas/users/user.resource.schema.ts
detail: {
enabled: true,
mode: "drawer",
title: "User detail",
fields: ["name", "email", "role", "status"]
}Wizard and tabs layout
schemas/users/user.form.schema.ts
layout: {
type: "wizard",
steps: [
{ key: "profile", title: "Profile", fields: ["name", "email"] },
{ key: "access", title: "Access", fields: ["role", "status"] }
]
}Use layout.type: "tabs" with tabs for tabbed forms. Use layout.type: "sections" or layout.type: "page" on a root schema with resources (like the account workspace above) to stack child resources instead of rendering them as tabs.
Dynamic options
schemas/teams/team.form.schema.ts
form: {
fields: [
{ name: "team", type: "select", optionLoader: "teams.list" }
]
},
registry: {
optionLoaders: ["teams.list"]
}Delete with two-step confirmation
schemas/items/item.resource.schema.ts
delete: {
enabled: true,
variant: "danger",
confirm: {
title: "Delete item?",
description: "This action cannot be undone.",
secondConfirm: {
title: "Delete permanently?",
confirmLabel: "Delete permanently"
}
}
}Custom theme plus adapter usage
Use theme.classNames for visual styling and plugins for actual adapter behavior.
app/users/page.tsx
<FluiKitProvider plugins={[reactSelectPlugin]} registry={registry}>
<FluiKit schema={schema} data={rows} />
</FluiKitProvider>schemas/users/user.form.schema.ts
form: {
fields: [
{ name: "tags", type: "multiselect", adapter: "react-select", optionLoader: "tags.list" }
]
}