ComfyCoders TranquilTools
Docs /Laravel Vue FormBuilder
Repository
v1

Standalone Components

Next to the form itself the package ships four components for the screens around a form: a tree, a sortable list and a tab bar. They know nothing about the schema, take plain props and are imported from the package root.

import { SortableList, Tabs, TreeView } from '@form-builder';

They follow the same Tailwind tokens as the fields (primary, muted, accent, input), so they pick up the host application's theme.

TreeView

A nested, selectable list with optional drag and drop.

<script setup lang="ts">
import { TreeView, type TreeNode } from '@form-builder';
import { ref } from 'vue';

const selected = ref<string | number | null>(null);

const nodes: TreeNode[] = [
    { id: 1, label: 'Furniture', children: [{ id: 2, label: 'Chairs' }] },
    { id: 3, label: 'Storage' },
];

function move(event: { dragged: TreeNode; target: TreeNode | null; position: 'before' | 'after' | 'inside' }) {
    // persist the new position
}
</script>

<template>
    <TreeView v-model="selected" :nodes="nodes" draggable @drop="move">
        <template #actions="{ node }">
            <button type="button" @click="edit(node)">Edit</button>
        </template>
    </TreeView>
</template>
Prop Purpose
nodes the tree, each node { id, label, children? } plus anything else you need
modelValue the selected node id
draggable enables dragging, off by default
expandedIds node ids that start out expanded
externalType drag data type the tree accepts from outside, emitted as @external-drop

@drop fires with the dragged node, the node it was dropped on and whether it landed before, after or inside it. Reordering is up to you: the component reports the intent and does not touch nodes.

A node can be dragged to any level of the tree, but never into its own subtree. Give a node draggable: false to keep it in place while it still accepts drops, as the fixed roots of a menu do.

Items from outside the tree, such as a list next to it, can be dropped on a node too. Give the tree an externalType and let the source put its data under that type on drag start; the tree then emits @external-drop with the parsed data, the node it landed on and the position. Drags of any other type are ignored.

<li draggable="true" @dragstart="$event.dataTransfer.setData('application/x-category', JSON.stringify(category))">…</li>

<TreeView :nodes="nodes" external-type="application/x-category" @external-drop="link" />

expandedIds seeds the expanded state once. Expanding and collapsing afterwards is the component's own state, and a subtree that gets collapsed forgets how its children were expanded.

SortableList

A flat list that reorders by dragging or with the arrow buttons on each row.

<script setup lang="ts">
import { SortableList } from '@form-builder';
import { ref } from 'vue';

const items = ref([
    { id: 1, name: 'First' },
    { id: 2, name: 'Second' },
]);
</script>

<template>
    <SortableList v-model="items" item-key="id" @reorder="persist">
        <template #default="{ item }">{{ item.name }}</template>
    </SortableList>
</template>
Prop Purpose
modelValue the items, in their current order
itemKey property used as the key, id by default
disabled blocks dragging and the arrow buttons

@reorder fires alongside update:modelValue with { from, to, items }, so you can persist the order without diffing the array yourself.

Tabs

A tab bar with optional close buttons and an unsaved marker.

<script setup lang="ts">
import { Tabs, type TabItem } from '@form-builder';
import { ref } from 'vue';

const active = ref('overview');

const tabs: TabItem[] = [
    { key: 'overview', label: 'Overview' },
    { key: 'brand-12', label: 'Acme', closable: true, dirty: true },
];
</script>

<template>
    <Tabs v-model="active" :tabs="tabs" @close="close">
        <template #append>
            <button type="button" @click="open">+</button>
        </template>
    </Tabs>
</template>

Each tab takes key, label and optionally icon (any component), closable, dirty and disabled. A dirty tab shows an amber dot. variant="sub" renders the lighter, second-level bar instead of the default main.

Closing does not switch to the tab that was closed, and clicking the active tab emits nothing.

DatePicker

The calendar behind the date field, for a date outside a form schema: a filter bar, a dialog, a table cell.

<script setup lang="ts">
import { DatePicker, type DisabledDate } from '@form-builder/components/ui/date-picker';
import { ref } from 'vue';

const arrival = ref<string | null>(null);

const closed: DisabledDate[] = ['2026-12-25', { from: '2026-07-20', to: '2026-08-02' }];
</script>

<template>
    <DatePicker v-model="arrival" :week-starts-on="1" :disabled-weekdays="[0, 6]" :disabled-dates="closed" />
</template>
Prop Purpose
modelValue Y-m-d (or Y-m-d H:i with enableTime); { start, end } with range
range, enableTime pick a period, pick a time as well
minDate, maxDate first and last selectable day
disabledWeekdays weekdays that cannot be picked, 0 = Sunday to 6 = Saturday
disabledDates days that cannot be picked: 'Y-m-d' strings and { from?, to? } ranges
weekStartsOn 0 Sunday (default) or 1 Monday
format, locale, placeholder display of the chosen value, format in Day.js tokens
timezone, serverTimezone see Date & Time
disabled, readonly block the picker

Disabled days only stop the user from clicking them. Validate on the server as well, for example with the AvailableDate rule described in Date & Time.

The same check is available as a function, for a table cell or a custom calendar. A project that only maps @form-builder/types/* in its tsconfig.json imports it from there (see Installation):

import { isUnavailableDate, type DisabledDate } from '@form-builder/types/dates';

const closed: DisabledDate[] = ['2026-12-25'];

isUnavailableDate(new Date(2026, 11, 25), [0, 6], closed); // true