VVibeFlui

Simple Table

Generated UI previewTABLE

Users

#ActionsEmailRoleStatus
1
Ada Lovelaceada@example.testadminactive
2
Grace Hoppergrace@example.testeditorpending

Row numbers, the custom status renderer, and the view/edit/delete/invite row actions are all live — edits and deletes update the in-memory rows shown here.

This tutorial renders a static table with view, edit, delete, and one custom row action.

Use this when your page already has rows in memory and you want a table with common row commands.

File structure

TXT
app/
  users/
    page.tsx
schemas/
  users/
    user.table.schema.ts
components/
  vibeflui/
    renderers/
      status/
        StatusBadge.tsx
lib/
  vibeflui/
    examples/
      user-crud-actions.ts
    registry.tsx

Schema file

schemas/users/user.table.schema.ts

TS
export const userTableSchema = {
  resource: "users",
  title: "Users",
  identity: {
    key: "id",
    param: "id"
  },
  table: {
    rowNumber: {
      enabled: true,
      label: "#"
    },
    columns: [
      { key: "name", label: "Name", searchable: true, sortable: true },
      { key: "email", label: "Email", type: "email", searchable: true },
      { key: "role", label: "Role" },
      { key: "status", label: "Status", renderer: "StatusBadge" }
    ],
    search: {
      enabled: true,
      placeholder: "Search users..."
    },
    states: {
      empty: {
        text: "No users yet."
      }
    }
  },
  detail: {
    enabled: true,
    fields: ["name", "email", "role", "status"]
  },
  form: {
    mode: "modal",
    fields: [
      { name: "name", label: "Name", required: true },
      { name: "email", label: "Email", type: "email", required: true },
      { name: "role", label: "Role" },
      {
        name: "status",
        label: "Status",
        type: "select",
        options: [
          { label: "Active", value: "active" },
          { label: "Pending", value: "pending" }
        ]
      }
    ]
  },
  actions: {
    create: {
      enabled: false
    },
    view: {
      enabled: true,
      label: "View",
      display: "icon",
      icon: "mdi:eye-outline"
    },
    edit: {
      enabled: true,
      label: "Edit",
      display: "icon",
      icon: "mdi:pencil-outline"
    },
    delete: {
      enabled: true,
      label: "Delete",
      display: "icon",
      icon: "mdi:trash-can-outline",
      variant: "danger",
      confirm: true
    },
    row: [
      {
        key: "invite",
        label: "Invite",
        tooltip: "Send invite",
        ariaLabel: "Send invite",
        display: "icon",
        icon: "mdi:send-outline",
        handler: "users.invite"
      }
    ]
  },
  registry: {
    renderers: ["StatusBadge"],
    actionHandlers: ["users.invite"]
  }
} as const;

Renderer component

components/vibeflui/renderers/status/StatusBadge.tsx

TSX
export function StatusBadge({ value }: { value?: unknown }) {
  const tone = value === "active" ? "text-green-700" : "text-orange-700";
  return <span className={tone}>{String(value ?? "-")}</span>;
}

Registry file

lib/vibeflui/registry.tsx

TSX
import { createRegistry } from "@vibeflui/core";
import { StatusBadge } from "@/components/vibeflui/renderers/status/StatusBadge";
import { inviteUser } from "@/lib/vibeflui/examples/user-crud-actions";

export const vibefluiRegistry = createRegistry({
  renderers: {
    StatusBadge
  },
  actionHandlers: {
    "users.invite": inviteUser
  }
});

Reusable action helpers

lib/vibeflui/examples/user-crud-actions.ts

TS
import type { Dispatch, SetStateAction } from "react";
import type { FluiKitSubmitContext } from "@vibeflui/core";

export type UserRow = {
  id: string;
  name: string;
  email: string;
  role: string;
  status: string;
};

export const initialUsers: UserRow[] = [
  { id: "usr_1", name: "Ada Lovelace", email: "ada@example.test", role: "admin", status: "active" },
  { id: "usr_2", name: "Grace Hopper", email: "grace@example.test", role: "editor", status: "pending" }
];

export async function inviteUser({ row }: { row?: Record<string, unknown> }) {
  return {
    status: true,
    code: 200,
    message: `Invitation sent to ${String(row?.email ?? "user")}`
  };
}

export function createUserCrudActions(setUsers: Dispatch<SetStateAction<UserRow[]>>) {
  return {
    updateUser: async (values: Record<string, unknown>, context: FluiKitSubmitContext) => {
      const id = String(context.row?.id ?? "");
      const nextUser = mapValuesToUser(values, id);
      setUsers((current) => current.map((user) => (user.id === id ? nextUser : user)));
      return { status: true, code: 200, message: "User updated" };
    },
    deleteUser: async (row: Record<string, unknown>) => {
      const id = String(row.id ?? "");
      setUsers((current) => current.filter((user) => user.id !== id));
      return { status: true, code: 200, message: "User deleted" };
    }
  };
}

function mapValuesToUser(values: Record<string, unknown>, id: string): UserRow {
  return {
    id,
    name: String(values.name ?? ""),
    email: String(values.email ?? ""),
    role: String(values.role ?? "viewer"),
    status: String(values.status ?? "active")
  };
}

Page file

app/users/page.tsx

TSX
"use client";

import { useState } from "react";
import { FluiKit, FluiKitProvider } from "@vibeflui/core";
import { userTableSchema } from "@/schemas/users/user.table.schema";
import { vibefluiRegistry } from "@/lib/vibeflui/registry";
import { createUserCrudActions, initialUsers } from "@/lib/vibeflui/examples/user-crud-actions";

export default function UsersPage() {
  const [users, setUsers] = useState(initialUsers);
  const userActions = createUserCrudActions(setUsers);

  return (
    <FluiKitProvider registry={vibefluiRegistry}>
      <FluiKit
        schema={userTableSchema}
        data={users}
        onUpdate={userActions.updateUser}
        onDelete={userActions.deleteUser}
      />
    </FluiKitProvider>
  );
}

What renders

The table renders row numbers, searchable columns, a custom status renderer, view/edit/delete icon actions, and an invite icon action.