Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .changeset/thick-hornets-act.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
"@buildnbuzz/buzzform": patch
---

**Fixes**
- Defaulted boolean schema values for `checkbox` and `switch` to `false` when values are missing, preventing submit-time `Invalid value` errors for untouched fields.
- Kept required boolean behavior intact by still requiring `true` when `required: true` is set.

**Types**
- Expanded `ui.columns` typing for `checkbox-group` and `radio` to `number | string | undefined`, enabling the builder's `Auto` (`""`) option for natural horizontal flow layouts.
3 changes: 2 additions & 1 deletion apps/web/app/(builder)/lib/properties/checkbox-group.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,9 +202,10 @@ export const checkboxGroupFieldProperties: Field[] = [
type: "select",
name: "ui.columns",
label: "Columns",
description: "Grid columns (responsive, 1 on mobile)",
description: "Grid columns. Choose 'Auto' for natural flow.",
condition: shouldShowColumns,
options: [
{ label: "Auto", value: "" },
{ label: "1 Column", value: 1 },
{ label: "2 Columns", value: 2 },
{ label: "3 Columns", value: 3 },
Expand Down
3 changes: 2 additions & 1 deletion apps/web/app/(builder)/lib/properties/radio.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,9 +191,10 @@ export const radioFieldProperties: Field[] = [
type: "select",
name: "ui.columns",
label: "Columns",
description: "Grid columns (responsive, 1 on mobile)",
description: "Grid columns. Choose 'Auto' for natural flow.",
condition: shouldShowColumns,
options: [
{ label: "Auto", value: "" },
{ label: "1 Column", value: 1 },
{ label: "2 Columns", value: 2 },
{ label: "3 Columns", value: 3 },
Expand Down
9 changes: 8 additions & 1 deletion apps/web/content/docs/fields/data/checkbox-group.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,13 @@ Selected values are submitted as a single array:
| ------------------ | ---------------------------- | ------------ | ------------------------------------ |
| `ui.variant` | `"default" \| "card"` | `"default"` | Option presentation style |
| `ui.direction` | `"vertical" \| "horizontal"` | `"vertical"` | Layout direction for default variant |
| `ui.columns` | `1 \| 2 \| 3 \| 4` | - | Responsive grid columns (card variant or horizontal default layout) |
| `ui.columns` | `number \| ""` | - | Grid columns. Use `""` (`Auto`) for natural horizontal flow instead of a fixed grid |
| `ui.card.size` | `"sm" \| "md" \| "lg"` | `"md"` | Card size preset |
| `ui.card.bordered` | `boolean` | `true` | Show card borders |

### Auto Columns

Use `ui.columns: ""` to enable `Auto` layout behavior.

- In `default` + `horizontal` layout, options flow naturally in a wrapping row.
- In card layouts, set a number when you want a fixed responsive grid.
9 changes: 8 additions & 1 deletion apps/web/content/docs/fields/data/radio.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,17 @@ export function RadioFieldDemo() {
| ------------------ | ---------------------------- | ------------ | ---------------- |
| `ui.variant` | `'default' \| 'card'` | `'default'` | Visual style |
| `ui.direction` | `'vertical' \| 'horizontal'` | `'vertical'` | Layout direction |
| `ui.columns` | `1 \| 2 \| 3 \| 4` | - | Grid columns (card variant or horizontal default layout) |
| `ui.columns` | `number \| ""` | - | Grid columns. Use `""` (`Auto`) for natural horizontal flow instead of a fixed grid |
| `ui.card.size` | `'sm' \| 'md' \| 'lg'` | `'md'` | Card size |
| `ui.card.bordered` | `boolean` | `true` | Show card border |

## Auto Columns

Use `ui.columns: ""` to enable `Auto` layout behavior.

- In `default` + `horizontal` layout, options flow naturally in a wrapping row.
- In card layouts, set a number when you want a fixed responsive grid.

## Variants

### Default Radio Buttons
Expand Down
2 changes: 1 addition & 1 deletion apps/web/public/r/checkbox-group.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion apps/web/public/r/radio.json

Large diffs are not rendered by default.

19 changes: 13 additions & 6 deletions apps/web/registry/base/fields/checkbox-group.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { cn } from "@/lib/utils";
type OptionValue = string | number | boolean;
type OptionGroupVariant = "default" | "card";
type OptionGroupDirection = "vertical" | "horizontal";
type OptionGroupColumns = 1 | 2 | 3 | 4;
type OptionGroupColumns = 1 | 2 | 3 | 4 | string | number | undefined;

export interface CheckboxGroupFieldProps {
field: CheckboxGroupFieldType;
Expand Down Expand Up @@ -60,11 +60,16 @@ function getOptionGroupLayoutClassName({
direction?: OptionGroupDirection;
columns?: OptionGroupColumns;
}) {
const usesGridColumns = variant === "card" || direction === "horizontal";
// Default to 2 columns for horizontal direction so users see immediate feedback
const effectiveColumns = usesGridColumns
? (columns ?? (direction === "horizontal" ? 2 : undefined))
: undefined;
const isCard = variant === "card";
const isHorizontal = direction === "horizontal";

// If horizontal and NO columns chosen, use fluid flex-row layout
if (isHorizontal && !columns) {
return "!flex flex-row flex-wrap gap-x-4 gap-y-2";
}

const effectiveColumns =
isCard || isHorizontal ? (columns ?? (isCard ? 2 : undefined)) : undefined;

if (!effectiveColumns || effectiveColumns === 1) {
return "flex flex-col gap-2";
Expand Down Expand Up @@ -221,6 +226,7 @@ export function CheckboxGroupField({
const maxSelected = field.maxSelected;

const isCardVariant = variant === "card";
const isHorizontal = direction === "horizontal";
const layoutClasses = getOptionGroupLayoutClassName({
variant,
direction,
Expand Down Expand Up @@ -370,6 +376,7 @@ export function CheckboxGroupField({
orientation="horizontal"
className={cn(
"items-center gap-2.5 space-y-0",
isHorizontal && !columns && "w-auto",
optDisabled && "opacity-50 cursor-not-allowed",
)}
>
Expand Down
23 changes: 15 additions & 8 deletions apps/web/registry/base/fields/radio.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface RadioFieldProps {

type OptionGroupVariant = "default" | "card";
type OptionGroupDirection = "vertical" | "horizontal";
type OptionGroupColumns = 1 | 2 | 3 | 4;
type OptionGroupColumns = 1 | 2 | 3 | 4 | string | number | undefined;

function getGridColumnsClass(columns: OptionGroupColumns | undefined) {
if (columns === 2) return "sm:grid-cols-2";
Expand All @@ -53,11 +53,16 @@ function getOptionGroupLayoutClassName({
direction?: OptionGroupDirection;
columns?: OptionGroupColumns;
}) {
const usesGridColumns = variant === "card" || direction === "horizontal";
// Default to 2 columns for horizontal direction so users see immediate feedback
const effectiveColumns = usesGridColumns
? (columns ?? (direction === "horizontal" ? 2 : undefined))
: undefined;
const isCard = variant === "card";
const isHorizontal = direction === "horizontal";

// If horizontal and NO columns chosen, use fluid flex-row layout
if (isHorizontal && !columns) {
return "!flex flex-row flex-wrap gap-x-4 gap-y-2";
}

const effectiveColumns =
isCard || isHorizontal ? (columns ?? (isCard ? 2 : undefined)) : undefined;

if (!effectiveColumns || effectiveColumns === 1) {
return "flex flex-col gap-2";
Expand Down Expand Up @@ -95,6 +100,7 @@ export function RadioField({
const cardBordered = field.ui?.card?.bordered ?? true;

const isCardVariant = variant === "card";
const isHorizontal = direction === "horizontal";

// Get options (only static for now, async would need useEffect)
const options = Array.isArray(field.options) ? field.options : [];
Expand Down Expand Up @@ -163,7 +169,7 @@ export function RadioField({

return (
<label
key={val}
key={`${val}-${i}`}
htmlFor={id}
className={cn(
// Base styles
Expand Down Expand Up @@ -229,10 +235,11 @@ export function RadioField({
// Default variant
return (
<Field
key={val}
key={`${val}-${i}`}
orientation="horizontal"
className={cn(
"items-center gap-2.5 space-y-0",
isHorizontal && !columns && "w-auto",
optDisabled && "opacity-50 cursor-not-allowed",
)}
>
Expand Down
8 changes: 4 additions & 4 deletions packages/buzzform/src/schema/builders/boolean.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@ import { z } from 'zod';
import type { CheckboxField, SwitchField } from '../../types';

export function createCheckboxFieldSchema(field: CheckboxField): z.ZodTypeAny {
let schema: z.ZodTypeAny = z.boolean({ error: 'Invalid value' });
let schema: z.ZodTypeAny = z.boolean({ error: 'Invalid value' }).default(false);

if (field.required) {
schema = z.boolean({ error: 'This field is required' }).refine(val => val === true, {
schema = z.boolean({ error: 'Invalid value' }).default(false).refine(val => val === true, {
error: 'This field is required',
});
}
Expand All @@ -14,10 +14,10 @@ export function createCheckboxFieldSchema(field: CheckboxField): z.ZodTypeAny {
}

export function createSwitchFieldSchema(field: SwitchField): z.ZodTypeAny {
let schema: z.ZodTypeAny = z.boolean({ error: 'Invalid value' });
let schema: z.ZodTypeAny = z.boolean({ error: 'Invalid value' }).default(false);

if (field.required) {
schema = z.boolean({ error: 'This field is required' }).refine(val => val === true, {
schema = z.boolean({ error: 'Invalid value' }).default(false).refine(val => val === true, {
error: 'This field is required',
});
}
Expand Down
80 changes: 40 additions & 40 deletions packages/buzzform/src/types/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,22 +192,22 @@ export interface BaseField<TValue = unknown, TData = Record<string, unknown>> {
* @example disabled: (data) => !data.country // Disable until country is selected
*/
disabled?:
| boolean
| ((data: TData, siblingData: Record<string, unknown>) => boolean);
| boolean
| ((data: TData, siblingData: Record<string, unknown>) => boolean);

/** Hide the field from the form UI (static or conditional) */
hidden?:
| boolean
| ((data: TData, siblingData: Record<string, unknown>) => boolean);
| boolean
| ((data: TData, siblingData: Record<string, unknown>) => boolean);

/**
* Make field read-only.
* Can be a boolean or a function for conditional read-only state.
* @example readOnly: (data) => data.status === 'published'
*/
readOnly?:
| boolean
| ((data: TData, siblingData: Record<string, unknown>) => boolean);
| boolean
| ((data: TData, siblingData: Record<string, unknown>) => boolean);

// === Data ===

Expand Down Expand Up @@ -391,11 +391,11 @@ export interface DateField extends BaseField<Date> {
inputFormat?: string;
/** Quick date presets */
presets?:
| boolean
| Array<{
label: string;
value: Date | (() => Date);
}>;
| boolean
| Array<{
label: string;
value: Date | (() => Date);
}>;
};
}

Expand All @@ -416,19 +416,19 @@ export interface DatetimeField extends BaseField<Date> {
inputFormat?: string;
/** Time picker config */
timePicker?:
| boolean
| {
interval?: number;
use24hr?: boolean;
includeSeconds?: boolean;
};
| boolean
| {
interval?: number;
use24hr?: boolean;
includeSeconds?: boolean;
};
/** Quick date presets */
presets?:
| boolean
| Array<{
label: string;
value: Date | (() => Date);
}>;
| boolean
| Array<{
label: string;
value: Date | (() => Date);
}>;
};
}

Expand Down Expand Up @@ -459,9 +459,9 @@ export interface SelectField extends BaseField<
type: "select";
/** Options (static, string array, or async with context for dependent dropdowns) */
options:
| SelectOption[]
| string[]
| ((context: ValidationContext) => Promise<SelectOption[]>);
| SelectOption[]
| string[]
| ((context: ValidationContext) => Promise<SelectOption[]>);
/**
* Field paths that trigger options refetch when changed.
* Required when using async options that depend on other field values.
Expand Down Expand Up @@ -498,9 +498,9 @@ export interface CheckboxGroupField extends BaseField<
type: "checkbox-group";
/** Static array or async function for options */
options:
| SelectOption[]
| string[]
| ((context: ValidationContext) => Promise<SelectOption[]>);
| SelectOption[]
| string[]
| ((context: ValidationContext) => Promise<SelectOption[]>);
/** Paths that trigger options refetch when changed */
dependencies?: string[];
/** Minimum number of selections */
Expand All @@ -511,8 +511,8 @@ export interface CheckboxGroupField extends BaseField<
ui?: {
/** Layout direction */
direction?: "vertical" | "horizontal";
/** Grid columns (responsive, 1 on mobile) */
columns?: 1 | 2 | 3 | 4;
/** Grid columns (responsive, 1 on mobile). Leave undefined/empty for Auto flow. */
columns?: number | string | undefined;
/** Visual variant */
variant?: "default" | "card";
/** Card settings (for variant: 'card') */
Expand Down Expand Up @@ -556,18 +556,18 @@ export interface RadioField extends BaseField<string | number | boolean> {
type: "radio";
/** Static array or async function for options */
options:
| SelectOption[]
| string[]
| ((context: ValidationContext) => Promise<SelectOption[]>);
| SelectOption[]
| string[]
| ((context: ValidationContext) => Promise<SelectOption[]>);
/** Paths that trigger options refetch when changed */
dependencies?: string[];
ui?: {
/** Visual variant ('default' or 'card') */
variant?: "default" | "card";
/** Layout direction for 'default' variant */
direction?: "vertical" | "horizontal";
/** Grid columns (responsive, 1 on mobile) */
columns?: 1 | 2 | 3 | 4;
/** Grid columns (responsive, 1 on mobile). Leave undefined or '' for Auto flow. */
columns?: number | string | undefined;
/** Card settings (for variant: 'card') */
card?: {
/** Size preset ('sm', 'md', 'lg') */
Expand Down Expand Up @@ -629,11 +629,11 @@ export interface UploadField extends BaseField<
size?: "xs" | "sm" | "md" | "lg" | "xl";
/** Enable cropping */
crop?:
| boolean
| {
aspectRatio?: number;
circular?: boolean;
};
| boolean
| {
aspectRatio?: number;
circular?: boolean;
};
/** Show progress */
showProgress?: boolean;
/** Dropzone text */
Expand Down
Loading