> ## Documentation Index
> Fetch the complete documentation index at: https://docs.softr.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Vibe Coding — Developer Guide

> Write code directly in the Vibe Coding Block for full control over markup, logic, and data

This guide is for developers who want to write code directly in the Vibe Coding Block, rather than prompting the AI. Use it when you need full control over your block's markup, logic, and data.

***

## The Basics

Block's source code is a TypeScript file with a default-exported React component:

```tsx theme={null}
export default function Block() {
  return (
    <div className="container">
      <div className="content">
        <h1 className="text-4xl font-extrabold tracking-tight text-balance">
          Hello, world!
        </h1>
      </div>
    </div>
  );
}
```

This only runs in the browser - you can fetch and mutate data from connected data sources, but you cannot run server-side code or use Node.js APIs.

Vibe coding block is configured to use [Tailwind](https://tailwindcss.com/) for styling and [shadcn/ui](https://ui.shadcn.com/) for components, but you are free to import any public npm package as needed. In fact, any npm package import you add will automatically install it for you.

```tsx theme={null}
import { format } from "date-fns"; // will be resolved at compile time

export default function Block() {
  const today = format(new Date(), "MMMM d, yyyy");
  return <div>Today's date is {today}</div>;
}
```

***

## shadcn/ui

shadcn/ui components are already pre-configured and follow your app's theme out of the box. They're available under the `@/components/ui` path, so you can import them like this:

```tsx theme={null}
import { Button } from "@/components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
```

Currently the following components are available:
`accordion`, `alert`, `alert-dialog`, `aspect-ratio`, `avatar`, `badge`, `button`, `calendar`, `card`, `carousel`, `chart`, `checkbox`, `collapsible`, `command`, `context-menu`, `dialog`, `drawer`, `dropdown-menu`, `empty`, `hover-card`, `input`, `input-group`, `input-otp`, `item`, `kbd`, `label`, `menubar`, `native-select`, `navigation-menu`, `pagination`, `popover`, `progress`, `radio-group`, `resizable`, `scroll-area`, `select`, `separator`, `sheet`, `skeleton`, `slider`, `sonner`, `spinner`, `switch`, `table`, `tabs`, `textarea`, `toggle`, `toggle-group`, `tooltip`

Check out the [shadcn/ui docs](https://ui.shadcn.com/docs/components) for usage details and examples for each component.

### Icons

Preferred icon pack is [Lucide Icons](https://lucide.dev/), but you can opt for a different one.

```tsx theme={null}
import { TrendingUp, User, Settings } from "lucide-react";
```

### Styling

We follow the default shadcn/ui naming convention for background/foreground color pairs (e.g. `bg-primary` / `text-primary-foreground`) which maps to your app's theme colors.

By default, block occupies full width of the page but special classes - `container` and `content` are available to constrain the width of content to match app's max width settings to ensure visual consistency with other blocks:

```tsx theme={null}
<div className="container">
  <div className="content">
    {/* your content here */}
  </div>
</div>
```

## Data from Your Datasource

Import data hooks from `@/lib/datasource`. Use these when you want to display records from your connected data source. All data fetching hooks follow a query builder pattern so you can be quite expressive with your queries.

### One or many datasources

A block can connect to multiple datasources. When it does, every fetch or mutation needs to say which datasource it targets. The `datasource.define` utility gives you readable aliases for that. Declare it once at the top of the file, then pass the alias as `from` on each hook:

```tsx theme={null}
import { datasource, useRecords, q } from "@/lib/datasource";

const ds = datasource.define({
  orders: "ds_id_1",
  customers: "ds_id_2",
});

const ordersSelect = q.select({ product: "field1", total: "field2" });
const customersSelect = q.select({ name: "field3", email: "field4" });

export default function Block() {
  const { data: orders } = useRecords({ from: ds.orders, select: ordersSelect });
  const { data: customers } = useRecords({ from: ds.customers, select: customersSelect });
  // ...
}
```

With a single datasource you can skip `datasource.define` and omit `from`. The hooks then default to that one datasource. As soon as a block has more than one, leaving out `from` throws an error, since the hook can't tell which datasource the call belongs to.

Every record hook takes `from` the same way: `useRecords`, `useRecord`, `useLinkedRecords`, `useFieldOptions`, `useMetric`, `useChartData`, `useRecordCreate`, `useRecordUpdate`, and `useRecordDelete`. `useUpload` and `useCurrentRecordId` work at the app level and thus `from` is not applicable to them.

### Defining a select query

Field mappings have to be static, we also use static analysis to determine which fields your block actually uses so we don't overfetch and don't accidentally expose potentially sensitive data.

```tsx theme={null}
import { q } from "@/lib/datasource";

const select = q.select({
  title: "FIELD_ID1",    // key = alias used in code, value = actual field ID
  description: "FIELD_ID2",
  createdAt: "FIELD_ID3",
});
```

So something like this is not allowed as it breaks static analysis:

```tsx theme={null}
const select = q.select({
  [dynamicKey]: "FIELD_ID1",   // ❌ dynamic keys not allowed
  title: getFieldId(),         // ❌ dynamic values not allowed
});
```

### `useRecords` — fetch a list of records

```tsx theme={null}
import { useRecords, q } from "@/lib/datasource";

export default function Block() {
  const {
    data,                // array of pages with shape { items: Record[]; total: number; offset: string | number | null }
    status,              // "pending" | "success" | "error"
    error,               // Error object if status is "error"
    fetchNextPage,       // function to fetch the next page of results
    hasNextPage,         // boolean indicating if there are more pages to fetch
    isFetching,          // boolean indicating if any page is currently being fetched
    isFetchingNextPage,  // boolean indicating if the next page is currently being fetched
    refetch,             // function to refetch the data (e.g. after a mutation)
    isRefetching,        // boolean indicating if a refetch is currently in progress
    isRefetchError,      // boolean indicating if the last refetch resulted in an error
  } = useRecords({
    select: q.select({
      name: "FIELD_ID1",
      email: "FIELD_ID2",
    }),
    count: 6,            // records per page, default 6, max 100
    // from: ds.orders,  // datasource alias, required when the block has multiple datasources
    // where: ...,       // optional filter
    // orderBy: ...,     // optional sort
    // enabled: ...,     // optional boolean to defer loading (e.g. until component is visible)
  });

  if (status === "pending") return <div>Loading...</div>;
  if (status === "error") return <div>Error loading data</div>;

  const items = data.pages.flatMap(page => page.items);

  return (
    <div className="container py-8">
      <div className="content space-y-4">
        {items.map((item) => (
          <div key={item.id}>
            <p className="font-semibold">{item.fields.name}</p>
            <p className="text-muted-foreground">{item.fields.email}</p>
          </div>
        ))}
        {hasNextPage && (
          <button onClick={() => fetchNextPage()} disabled={isFetching}>
            {isFetchingNextPage ? "Loading..." : "Load More"}
          </button>
        )}
      </div>
    </div>
  );
}
```

### `useRecord` — fetch a single record by ID

Use with `useCurrentRecordId()` to display details of the record currently shown in a list/detail context:

```tsx theme={null}
import { useRecord, useCurrentRecordId, q } from "@/lib/datasource";

export default function Block() {
  const recordId = useCurrentRecordId(); // resolves the ID of the current record from page, can also be null
  const { data, status } = useRecord({
    select: q.select({
      title: "FIELD_ID1",
      description: "FIELD_ID2",
    }),
    recordId,
    // from: ds.orders,  // datasource alias, required when the block has multiple datasources
  });

  if (status === "pending") return <div>Loading...</div>;
  if (status === "error") return <div>Error</div>;
  if (!data) return <div>Not found</div>;

  return (
    <div className="container py-8">
      <div className="content">
        <h1>{data.fields.title}</h1>
        <p>{data.fields.description}</p>
      </div>
    </div>
  );
}
```

### Filtering records

Use the `q` query builder for filters. Filters support up to **2 levels of nesting**.

```tsx theme={null}
useRecords({
  select,
  where: q.and(
    q.text("name").contains("Alice"),
    q.number("age").gte(18),
    q.or(
      q.boolean("isActive").is(true),
      q.text("notes").isNotEmpty()
    )
  ),
});
```

**Available filter methods:**

| Builder            | Methods                                                                                                         |
| ------------------ | --------------------------------------------------------------------------------------------------------------- |
| `q.text(field)`    | `is`, `isNot`, `contains`, `startsWith`, `endsWith`, `isOneOf`, `isNoneOf`, `hasAllOf`, `isEmpty`, `isNotEmpty` |
| `q.number(field)`  | `is`, `isNot`, `gt`, `gte`, `lt`, `lte`, `between`, `isEmpty`, `isNotEmpty`                                     |
| `q.boolean(field)` | `is`, `isNot`, `isEmpty`, `isNotEmpty`                                                                          |
| `q.date(field)`    | `is`, `isNot`, `gt`, `gte`, `lt`, `lte`, `between`, `isNotBetween`, `isEmpty`, `isNotEmpty`                     |
| `q.array(field)`   | `is`, `isOneOf`, `isNoneOf`, `hasAllOf`, `isEmpty`, `isNotEmpty`                                                |
| `q.and(...)`       | combine filters with AND                                                                                        |
| `q.or(...)`        | combine filters with OR                                                                                         |

### Sorting records

```tsx theme={null}
useRecords({
  select,
  orderBy: q.desc("createdAt"), // or q.asc("createdAt")
});

// Multiple sort fields (tie-breakers):
useRecords({
  select,
  orderBy: [q.asc("lastName"), q.asc("firstName")],
});
```

### `useLinkedRecords` — fetch options from a linked table

Use for dropdowns, comboboxes, or tag pickers where you need to show values from a related table:

```tsx theme={null}
import { q, useLinkedRecords } from "@/lib/datasource";

export default function Block() {
  const {
    data,
    status,
    error,
    fetchNextPage,
    hasNextPage,
    isFetching,
    isFetchingNextPage,
    refetch,
    isRefetching,
    isRefetchError,
  } = useLinkedRecords({
    select: q.select({ category: "$CATEGORY_FIELD_ID" }),

    field: "category",    // alias from select that holds the linked field
    sortOrder: "ASC",     // "ASC" | "DESC"
    search: "",           // optional search term
    enabled: true,        // set false to defer loading (e.g. until dropdown opens)
    // from: ds.orders,   // datasource alias, required when the block has multiple datasources
    // count: ...,        // optional page size, defaults to 100, max 1000
  });

  const options = data?.pages.flatMap(p => p.items) ?? [];

  return (
    <div>
      {options.map(opt => (
        <span key={opt.id}>{opt.title}</span>
      ))}
    </div>
  );
}
```

Each item only carries its `id` and `title` (the linked table's primary field). To read other fields off those records, connect the linked table as its own datasource and query it with `useRecords`. See [One or many datasources](#one-or-many-datasources).

***

## Mutating Records

All mutation hooks expose an `enabled` boolean — **always check it** before rendering the mutation UI or calling the function. It reflects whether the current user has sufficient permissions. If called without checking `enabled`, the mutation will throw an error.

### `useRecordCreate`

```tsx theme={null}
import { useRecordCreate, q } from "@/lib/datasource";
import { toast } from "sonner";

export default function Block() {
  const {
    enabled,      // boolean indicating if the user has permission to create records
    mutate,       // void function to create a record, accepts an object with shape from provided fields, in this case - `{ name: string; email: string }`
    mutateAsync,  // async version of mutate that returns a promise with the created record
    status,       // "idle" | "pending" | "success" | "error"
    error,        // Error object if status is "error"
    reset,        // function to reset the status and error state back to "idle" and null, useful for showing multiple create forms in a row
  } = useRecordCreate({
    fields: q.select({
      name: "FIELD_ID1",
      email: "FIELD_ID2",
    }),
    onSuccess: (newRecord) => toast.success("Created!"),
    onError: (error) => toast.error(error.message),
    // from: ds.orders,  // datasource alias, required when the block has multiple datasources
  });

  return (
    <div>
      {enabled && (
        <button
          onClick={() => mutate({ name: "Jane", email: "jane@example.com" })}
          disabled={status === "pending"}
        >
          {status === "pending" ? "Saving..." : "Add Record"}
        </button>
      )}
    </div>
  );
}
```

### `useRecordUpdate`

```tsx theme={null}
import { useRecordUpdate, q } from "@/lib/datasource";

export default function Block() {
  const { data, refetch } = useRecords({ ... });
  const {
    enabled,
    mutate,
    mutateAsync,
    status,
    error,
    reset,
  } = useRecordUpdate({
    fields: q.select({ status: "FIELD_ID1" }),
    onSuccess: async (updatedRecord) => {
      // refetch the data first to ensure the UI shows the latest value before notifying the user.
      await refetch();
      toast.success("Updated!");
    },
    onError: (error) => toast.error(error.message),
    // from: ds.orders,  // datasource alias, required when the block has multiple datasources
  });

  const onUpdateClick = () => {
    if (!enabled) return;
    mutate({
      recordId: "RECORD_ID", // ID of the record to update
      fields: { status: "active" }, // updated field values
    });
  };
}
```

### `useRecordDelete`

```tsx theme={null}
import { useRecordDelete } from "@/lib/datasource";

export default function Block() {
  const {
    enabled,
    mutate,
    mutateAsync,
    status,
    error,
    reset,
  } = useRecordDelete({
    onSuccess: async ({ recordId }) => {
      await refetch();
      toast.success("Deleted!");
    },
    onError: (error) => toast.error(error.message),
    // from: ds.orders,  // datasource alias, required when the block has multiple datasources
  });

  const onDeleteClick = () => {
    if (!enabled) return;
    mutate("RECORD_ID"); // ID of the record to delete
  };
}
```

***

## Uploading Files

Use `useUpload` from `@/lib/datasource` to upload files and get back a URL to store in a record.

```tsx theme={null}
import { useUpload, useRecordCreate, q } from "@/lib/datasource";
import { Input } from "@/components/ui/input";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
import { useState } from "react";

const fields = q.select({
  name: "$NAME_FIELD_ID",
  attachment: "$ATTACHMENT_FIELD_ID",
});

export default function Block() {
  const [name, setName] = useState("");
  const [file, setFile] = useState<File | null>(null);
  const { uploadAsync, isUploading } = useUpload();
  const createRecord = useRecordCreate({
    fields,
    onSuccess: () => toast.success("Submitted!"),
  });

  const handleSubmit = async () => {
    if (!file) return;
    const [result] = await uploadAsync(file);
    if (result.status === "completed") {
      createRecord.mutate({
        name,
        attachment: { filename: result.file.name, url: result.url },
      });
    } else {
      toast.error(result.error?.message ?? "Upload failed");
    }
  };

  return (
    <div className="container py-6">
      <div className="content space-y-4 max-w-md">
        <Input value={name} onChange={(e) => setName(e.target.value)} placeholder="Name" />
        <Input type="file" onChange={(e) => setFile(e.target.files?.[0] ?? null)} />
        <Button onClick={handleSubmit} disabled={isUploading || !file}>
          {isUploading ? "Uploading..." : "Submit"}
        </Button>
      </div>
    </div>
  );
}
```

For multiple files:

```tsx theme={null}
const results = await uploadAsync(Array.from(e.target.files));
const completed = results.filter(r => r.status === "completed");
```

***

## Current User

Get info about the logged-in user with `useCurrentUser` from `@/lib/user`. Returns `null` if no user is logged in.

```tsx theme={null}
import { useCurrentUser } from "@/lib/user";

export default function Block() {
  const user = useCurrentUser();

  if (!user) return <div>Please log in to continue.</div>;

  return (
    <div className="container py-8">
      <div className="content flex items-center gap-4">
        {user.avatar && (
          <img src={user.avatar} alt={user.fullName ?? ""} className="w-10 h-10 rounded-full" />
        )}
        <div>
          <p className="font-semibold">{user.fullName}</p>
          <p className="text-muted-foreground text-sm">{user.email}</p>
        </div>
      </div>
    </div>
  );
}
```

Available fields:

* `id`: `string | null` (only present when user sync is enabled)
* `fullName`: `string | null`
* `firstName`: `string | null`
* `lastName`: `string | null`
* `email`: `string | null`
* `avatar`: `string | null`

### Custom user properties

Beyond the reserved fields above, any custom fields that exist on your user record are available under `user.properties`. Pass a `properties` map to alias each field to a readable name, the same way a `select` query works.

```tsx theme={null}
import { useCurrentUser } from "@/lib/user";

export default function Block() {
  const user = useCurrentUser({
    properties: {
      stripeId: "FIELD_ID1",
      plan: "FIELD_ID2",
    },
  });

  if (!user) return <div>Please log in to continue.</div>;

  return (
    <div className="container py-8">
      <div className="content">
        <p className="font-semibold">Welcome, {user.fullName}!</p>
        {user.properties.stripeId && <p className="text-muted-foreground text-sm">Stripe ID: {user.properties.stripeId}</p>}
        {user.properties.plan && <p className="text-muted-foreground text-sm">Your plan: {user.properties.plan}</p>}
      </div>
    </div>
  );
}
```

***

## Metrics & Charts

### `useMetric` — single aggregated value

Useful for KPI cards (total sales, average rating, etc.):

```tsx theme={null}
import { useMetric, q, metric } from "@/lib/datasource";

export default function Block() {
  const { data, status } = useMetric({
    select: q.select({ revenue: "$REVENUE_FIELD_ID" }),
    metric: metric.sum("revenue"),
    // from: ds.orders,  // datasource alias, required when the block has multiple datasources
    // where: q.date("createdAt").gte("2025-01-01"),
  });

  if (status === "pending") return <div>Loading...</div>;

  return <div className="text-4xl font-bold">${data?.toFixed(2)}</div>;
}
```

**Aggregations:** `metric.sum(field)`, `metric.avg(field)`, `metric.max(field)`, `metric.min(field)`, `metric.distinct(field)`, `metric.count()`

### `useChartData` — grouped data for charts

```tsx theme={null}
import { useChartData, q, metric } from "@/lib/datasource";
import { LineChart, Line, XAxis, CartesianGrid } from "recharts";
import { ChartContainer, ChartTooltip, ChartTooltipContent, type ChartConfig } from "@/components/ui/chart";

const chartConfig = {
  revenue: { label: "Revenue", color: "var(--chart-1)" },
} satisfies ChartConfig;

export default function Block() {
  const { data, status } = useChartData({
    select: q.select({
      date: "$DATE_FIELD_ID",
      revenue: "$REVENUE_FIELD_ID",
    }),
    orderBy: q.asc("date"),
    metric: { revenue: metric.sum("revenue") },
    groupBy: metric.groupBy("date", metric.bucket.month.long),
    // from: ds.orders,  // datasource alias, required when the block has multiple datasources
  });

  if (status === "pending") return <div>Loading...</div>;

  return (
    <div className="container py-8">
      <div className="content">
        <ChartContainer config={chartConfig}>
          <LineChart data={data} margin={{ left: 12, right: 12 }}>
            <CartesianGrid vertical={false} />
            <XAxis dataKey="date" tickLine={false} axisLine={false} tickMargin={8} />
            <ChartTooltip content={<ChartTooltipContent />} />
            <Line dataKey="revenue" stroke="var(--color-revenue)" strokeWidth={2} dot={false} />
          </LineChart>
        </ChartContainer>
      </div>
    </div>
  );
}
```

**Grouping buckets:**

| Bucket                     | Format example   |
| -------------------------- | ---------------- |
| `metric.bucket.year`       | `2025`           |
| `metric.bucket.month.iso`  | `"2025-03"`      |
| `metric.bucket.month.long` | `"March 2025"`   |
| `metric.bucket.day.iso`    | `"2025-03-15"`   |
| `metric.bucket.day.long`   | `"Mar 15, 2025"` |

***

## Editable Settings

Editable settings let builders modify block content through the editor UI (**Content → Settings** tab) without touching code. Always use them for any text, images, icons, or lists that might change between block instances.

Import from `@/lib/editable-settings`.

### `useTextSetting`

Returns a `string`. Use for titles, descriptions, button labels, URLs, etc.

```tsx theme={null}
import { useTextSetting } from "@/lib/editable-settings";

export default function Block() {
  const title = useTextSetting({
    name: "title",           // unique identifier — changing this resets the value
    label: "Title",          // shown in the editor UI
    initialValue: "Welcome", // starting value
    required: false,         // optional, default false
  });

  return <h1>{title}</h1>;
}
```

### `useImageSetting`

Returns `{ src: string; alt: string }`.

```tsx theme={null}
import { useImageSetting } from "@/lib/editable-settings";

export default function Block() {
  const image = useImageSetting({
    name: "hero-image",
    label: "Hero Image",
    initialValue: {
      src: "https://images.unsplash.com/photo-...",
      alt: "A hero image",
    },
  });

  return <img src={image.src} alt={image.alt} className="w-full rounded-lg" />;
}
```

### `useVideoSetting`

Returns `{ src: string }`.

```tsx theme={null}
import { useVideoSetting } from "@/lib/editable-settings";

export default function Block() {
  const video = useVideoSetting({
    name: "intro-video",
    label: "Intro Video",
    initialValue: { src: "https://example.com/video.mp4" },
  });

  return <video src={video.src} controls className="w-full" />;
}
```

### `useVibeCodingBlockIconSetting`

Returns `{ icon: string }` where `icon` is a `lucide-react` icon name. Render it with the `DynamicIcon` component.

```tsx theme={null}
import { useVibeCodingBlockIconSetting } from "@/lib/editable-settings";
import { DynamicIcon } from "@/components/dynamic-icon";

export default function Block() {
  const { icon } = useVibeCodingBlockIconSetting({
    name: "feature-icon",
    label: "Feature Icon",
    initialValue: { icon: "trending-up" },
  });

  return <DynamicIcon name={icon} className="w-6 h-6" />;
}
```

### `useNavigationSetting`

Returns `{ action: "OPEN_URL" | "OPEN_PAGE"; destination: string; openIn: "SELF" | "TAB" } | { action: "OPEN_CHAT" } | { action: "TRIGGER_CUSTOM_WORKFLOW" }`.

```tsx theme={null}
import { NavigationAction } from "@/components/navigation-action";
import { Button } from "@/components/ui/button";
import { useNavigationSetting } from "@/lib/editable-settings";

export default function Block() {
  const navigation = useNavigationSetting({
    name: "cta-navigation",
    label: "CTA Navigation",
    initialValue: {
      action: "OPEN_PAGE",
      destination: "/pricing",
      openIn: "TAB",
    },
  });

  return (
      <Button asChild>
        <NavigationAction navigation={navigation}>
          Click me!
        </NavigationAction>
      </Button>
  );
}
```

The `NavigationAction` component also accepts an optional `recordId?: string` prop. When rendering record-specific links in a list/loop, pass the current record ID so it can be dynamically added as a URL parameter to the final URL `?recordId=<id>`.

```tsx theme={null}
<NavigationAction navigation={detailsLink} recordId={item.id}>
  View details
</NavigationAction>
```

### `useArraySetting`

Returns an array of items with a consistent shape. Use for feature lists, team members, FAQs, testimonials, etc.

```tsx theme={null}
import { useArraySetting } from "@/lib/editable-settings";
import { DynamicIcon } from "@/components/dynamic-icon";

export default function Block() {
  const features = useArraySetting({
    name: "features",
    label: "Features",
    schema: {
      title: { type: "text", label: "Title", initialValue: "Feature" },
      description: { type: "text", label: "Description" },
      icon: { type: "vibeCodingBlockIcon", label: "Icon" },
      image: { type: "image", label: "Image" },
    },
    initialValue: [
      {
        title: "Fast",
        description: "Blazing fast performance.",
        icon: { icon: "zap" },
        image: { src: "", alt: "" },
      },
      {
        title: "Reliable",
        description: "99.9% uptime guaranteed.",
        icon: { icon: "shield" },
        image: { src: "", alt: "" },
      },
    ],
  });

  return (
    <div className="container py-12">
      <div className="content grid md:grid-cols-2 gap-6">
        {features.map((feature, index) => (
          <div key={index} className="flex gap-4">
            <DynamicIcon name={feature.icon.icon} className="w-6 h-6 text-primary" />
            <div>
              <h3 className="font-semibold">{feature.title}</h3>
              <p className="text-muted-foreground text-sm">{feature.description}</p>
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
```

### `useBooleanSetting`

Returns a `boolean`. Use for toggles, switches, show/hide elements, etc.

```tsx theme={null}
import { useBooleanSetting } from "@/lib/editable-settings";

export default function Block() {
  const showHeader = useBooleanSetting({
    name: "toggleHeader",     // unique identifier — changing this resets the value
    label: "Toggle header",   // shown in the editor UI
    initialValue: false,      // starting value (default is true)
  });

  return <>{showHeader && <Header />}</>;
}
```

**Schema field types:** `"text"`, `"image"`, `"video"`, `"vibeCodingBlockIcon"`

**Constraints:**

* Schema cannot contain nested arrays — for list-like text, use a `"text"` field with a separator (e.g. comma) and split it in code
* Do not put a `vibeCodingBlockIcon` field as the **first** field in the schema
* Calling two settings hooks with the same `name` is not allowed

***

## Complete Example — Feature Showcase

A full-featured block combining editable settings, datasource records, and shadcn/ui:

```tsx theme={null}
import { useTextSetting, useArraySetting } from "@/lib/editable-settings";
import { useRecords, q } from "@/lib/datasource";
import { Card, CardHeader, CardTitle, CardContent } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { DynamicIcon } from "@/components/dynamic-icon";

const select = q.select({
  name: "$PRODUCT_NAME_FIELD",
  category: "$CATEGORY_FIELD",
  price: "$PRICE_FIELD",
});

export default function Block() {
  // Editable settings for the section header
  const heading = useTextSetting({
    name: "heading",
    label: "Section heading",
    initialValue: "Our Products",
  });
  const subheading = useTextSetting({
    name: "subheading",
    label: "Section subheading",
    initialValue: "Explore our latest offerings",
  });

  // Live records from datasource
  const { data, status, hasNextPage, fetchNextPage, isFetching } = useRecords({
    select,
    count: 6,
    orderBy: q.asc("name"),
  });

  const items = data?.pages.flatMap(p => p.items) ?? [];

  return (
    <div className="container py-12">
      <div className="content">
        <div className="text-center mb-10">
          <h2 className="text-3xl font-heading font-bold">{heading}</h2>
          <p className="text-muted-foreground mt-2">{subheading}</p>
        </div>

        {status === "pending" && (
          <div className="flex justify-center py-12">
            <div className="text-muted-foreground">Loading...</div>
          </div>
        )}

        {status === "error" && (
          <div className="text-destructive text-center">Failed to load products.</div>
        )}

        <div className="grid sm:grid-cols-2 lg:grid-cols-3 gap-6">
          {items.map((item) => (
            <Card key={item.id}>
              <CardHeader>
                <Badge variant="secondary" className="w-fit">
                  {item.fields.category?.label ?? "Uncategorized"}
                </Badge>
                <CardTitle className="mt-2">{item.fields.name}</CardTitle>
              </CardHeader>
              <CardContent>
                <p className="text-2xl font-bold">
                  ${(item.fields.price as number)?.toFixed(2)}
                </p>
              </CardContent>
            </Card>
          ))}
        </div>

        {hasNextPage && (
          <div className="flex justify-center mt-8">
            <button
              onClick={() => fetchNextPage()}
              disabled={isFetching}
              className="text-primary underline underline-offset-4 hover:no-underline"
            >
              {isFetching ? "Loading..." : "Load more"}
            </button>
          </div>
        )}
      </div>
    </div>
  );
}
```

***

## Fetching field options

Use `useFieldOptions` to fetch available options for SELECT or multi-select fields. This is useful for building filters, dropdowns, badges, or any UI that needs to display the available choices from the datasource without hardcoding them. Like the other data hooks, it accepts a `from` alias (`useFieldOptions({ from: ds.orders, select, field })`), required when the block has multiple datasources.

Example to build a filter UI using `useFieldOptions`:

```tsx theme={null}
import { useState } from "react";
import { q, useRecords, useFieldOptions } from "@/lib/datasource";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";

const select = q.select({
  title: "$TITLE_FIELD_ID",
  productStatus: "$PRODUCT_STATUS_FIELD_ID",
});

export default function Block() {
  const [selectedProductStatus, setSelectedProductStatus] = useState<string | null>(null);
  const { options } = useFieldOptions({ select, field: "productStatus" });

  const { data, status } = useRecords({
    select,
    where: selectedProductStatus ? q.text("productStatus").is(selectedProductStatus) : undefined,
    count: 10,
  });

  const items = data?.pages.flatMap((p) => p.items) ?? [];

  return (
    <div className="container py-6">
      <div className="content space-y-4">
        <Select
          value={selectedProductStatus ?? "all"}
          onValueChange={(val) => setSelectedProductStatus(val === "all" ? null : val)}
        >
          <SelectTrigger className="w-48">
            <SelectValue placeholder="Filter by product status" />
          </SelectTrigger>
          <SelectContent>
            <SelectItem value="all">All Product Statuses</SelectItem>
            {options.map((opt) => (
              <SelectItem key={opt.id} value={opt.id}>
                {opt.label}
              </SelectItem>
            ))}
          </SelectContent>
        </Select>

        {status === "pending" && <div>Loading...</div>}
        {items.map((item) => (
          <div key={item.id}>
            <div>{item.fields.title}</div>
            <div>{item.fields.productStatus?.label || "Unknown"}</div>
          </div>
        ))}
      </div>
    </div>
  );
}
```

***

## Fetching from a REST API

When using a REST API as a datasource, call it with `useProxyFetch` from `@/lib/datasource`. The proxy attaches authentication for you, so don't send tokens, API keys, or auth headers yourself.

`useProxyFetch` returns a function with the same signature as `fetch`. [TanStack Query](https://tanstack.com/query) (or a similar data-fetching library) pairs nicely with it for caching and request state, and we strongly recommend it over fetching inside a `useEffect`.

<Warning>
  **Caveats**: the proxy currently only supports text payloads. Streams, `FormData`, and file uploads won't work.
</Warning>

### Reading data

```tsx theme={null}
import { useQuery } from "@tanstack/react-query";
import { useProxyFetch } from "@/lib/datasource";

export default function Block() {
  const proxyFetch = useProxyFetch();

  const { data, status, error } = useQuery({
    queryKey: ["products"],
    queryFn: async () => {
      const res = await proxyFetch("https://api.example.com/products");
      if (!res.ok) throw new Error("Failed to fetch products");
      return res.json();
    },
  });

  if (status === "pending") return <div>Loading...</div>;
  if (status === "error") return <div>{error.message}</div>;

  return (
    <div className="container py-6">
      <div className="content space-y-2">
        {data.map((item) => (
          <div key={item.id}>{item.name}</div>
        ))}
      </div>
    </div>
  );
}
```

### Mutating data

Wrap writes in `useMutation` and invalidate the affected queries on success so the UI refetches:

```tsx theme={null}
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { useProxyFetch } from "@/lib/datasource";
import { toast } from "sonner";

function useCreateProduct() {
  const proxyFetch = useProxyFetch();
  const queryClient = useQueryClient();

  return useMutation({
    mutationFn: async (input: { name: string; price: number }) => {
      const res = await proxyFetch("https://api.example.com/products", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(input),
      });
      if (!res.ok) throw new Error("Failed to create product");
      return res.json();
    },
    onSuccess: () => {
      queryClient.invalidateQueries({ queryKey: ["products"] });
      toast.success("Created");
    },
  });
}
```

### Multiple datasources

When a block has more than one datasource, `useProxyFetch` needs to know which one to route the request to, so pass the datasource alias as its argument. With a single datasource you can call `useProxyFetch()` with no argument; once there's more than one, leaving it out throws an error. Define the aliases with `datasource.define`, the same pattern the record hooks use:

```tsx theme={null}
import { useQuery } from "@tanstack/react-query";
import { datasource, useProxyFetch } from "@/lib/datasource";

const ds = datasource.define({
  store: "ds_id_1",
});

function useProducts() {
  const proxyFetch = useProxyFetch(ds.store);

  return useQuery({
    queryKey: ["products"],
    queryFn: async () => {
      const res = await proxyFetch("https://api.example.com/products");
      if (!res.ok) throw new Error("Failed to fetch products");
      return res.json();
    },
  });
}
```
