VVibeFlui

Customization

Generated UI previewREGISTRY

Customized users

#ActionsNameEmailStatusDisplay
1
Ada Lovelaceada@example.testactiveAda Lovelace <ada@example.test>
2
Grace Hoppergrace@example.testpendingGrace 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.tsx

Schema 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.