Guide

Examples & Recipes

Practical multi-component patterns and recipes for common UI scenarios.

Real-world patterns that combine multiple Docyrus UI components. Copy, adapt, and use in your projects.

CRUD Table with Filters

A common pattern: a data grid with toolbar filters, search, and create/delete dialogs.

import { useState } from 'react';

import { DataGrid } from '@/components/docyrus/data-grid';
import { DataTableFilter } from '@/components/docyrus/data-table-filter';
import { SearchInput } from '@/components/docyrus/search-input';
import { DeleteConfirmDialog } from '@/components/docyrus/delete-confirm-dialog';
import { CreateRecordDialog } from '@/components/docyrus/create-record-dialog';
import { Button } from '@/components/ui/button';

export function ContactsPage() {
  const [search, setSearch] = useState('');
  const [filters, setFilters] = useState([]);
  const [deleteId, setDeleteId] = useState<string | null>(null);
  const [showCreate, setShowCreate] = useState(false);

  return (
    <div className="flex flex-col gap-4">
      {/* Toolbar */}
      <div className="flex items-center gap-3">
        <SearchInput
          value={search}
          onChange={setSearch}
          placeholder="Search contacts..."
          className="w-64"
        />
        <DataTableFilter
          filters={filters}
          onChange={setFilters}
          fields={contactFields}
        />
        <Button className="ml-auto" onClick={() => setShowCreate(true)}>
          Add Contact
        </Button>
      </div>

      {/* Grid */}
      <DataGrid
        data={filteredContacts}
        columns={columns}
        onRowAction={(action, row) => {
          if (action === 'delete') setDeleteId(row.id);
        }}
      />

      {/* Dialogs */}
      <CreateRecordDialog
        open={showCreate}
        onOpenChange={setShowCreate}
        fields={contactFields}
        onSubmit={handleCreate}
      />
      <DeleteConfirmDialog
        open={!!deleteId}
        onOpenChange={() => setDeleteId(null)}
        onConfirm={() => handleDelete(deleteId)}
        title="Delete contact"
        description="This action cannot be undone."
      />
    </div>
  );
}

Form with Validation

Schema-driven form using TanStack Form with Docyrus form fields:

import { useForm } from '@tanstack/react-form';

import { DocyField } from '@/components/docyrus/form-fields/docy-field';
import { DocyFieldDate } from '@/components/docyrus/form-fields/docy-field-date';
import { DocyFieldSelect } from '@/components/docyrus/form-fields/docy-field-select';
import { Button } from '@/components/ui/button';

const priorityOptions = [
  { label: 'Low', value: 'low' },
  { label: 'Medium', value: 'medium' },
  { label: 'High', value: 'high' },
];

export function CreateTaskForm({ onSubmit }: { onSubmit: (data: TaskData) => void }) {
  const form = useForm({
    defaultValues: {
      title: '',
      description: '',
      priority: 'medium',
      dueDate: null as Date | null,
    },
    onSubmit: ({ value }) => onSubmit(value),
  });

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault();
        form.handleSubmit();
      }}
      className="flex flex-col gap-4"
    >
      <form.Field
        name="title"
        validators={{ onChange: ({ value }) => !value ? 'Title is required' : undefined }}
      >
        {(field) => (
          <DocyField
            label="Title"
            value={field.state.value}
            onChange={field.handleChange}
            error={field.state.meta.errors[0]}
            required
          />
        )}
      </form.Field>

      <form.Field name="description">
        {(field) => (
          <DocyField
            label="Description"
            value={field.state.value}
            onChange={field.handleChange}
            multiline
            rows={3}
          />
        )}
      </form.Field>

      <div className="grid grid-cols-2 gap-4">
        <form.Field name="priority">
          {(field) => (
            <DocyFieldSelect
              label="Priority"
              value={field.state.value}
              onChange={field.handleChange}
              options={priorityOptions}
            />
          )}
        </form.Field>

        <form.Field name="dueDate">
          {(field) => (
            <DocyFieldDate
              label="Due Date"
              value={field.state.value}
              onChange={field.handleChange}
            />
          )}
        </form.Field>
      </div>

      <Button type="submit" className="self-end">
        Create Task
      </Button>
    </form>
  );
}

Dashboard Layout

Combine stats, charts, and activity panels into a dashboard view:

import { AwesomeStats } from '@/components/docyrus/awesome-stats';
import { RecordActivityPanel } from '@/components/docyrus/record-activity-panel';
import { NotificationsPanel } from '@/components/docyrus/notifications-panel';
import { Calendar } from '@/components/docyrus/calendar';

export function DashboardPage() {
  return (
    <div className="flex flex-col gap-6">
      {/* Stats row */}
      <AwesomeStats
        stats={[
          { label: 'Total Revenue', value: '$48,200', change: 12.5 },
          { label: 'Active Users', value: '2,340', change: -3.2 },
          { label: 'Conversion', value: '3.24%', change: 0.8 },
          { label: 'Avg. Order', value: '$128', change: 5.1 },
        ]}
      />

      {/* Main content + sidebar */}
      <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
        <div className="lg:col-span-2">
          <Calendar
            view="week"
            events={events}
            onEventClick={handleEventClick}
          />
        </div>

        <div className="flex flex-col gap-4">
          <NotificationsPanel limit={5} />
          <RecordActivityPanel
            recordId={currentRecordId}
            limit={10}
          />
        </div>
      </div>
    </div>
  );
}

File Attachment with Preview

Handle file uploads with preview and attachment management:

import { useState } from 'react';

import { FileAttachmentPanel } from '@/components/docyrus/file-attachment-panel';
import { ImageEditor } from '@/components/docyrus/image-editor';
import { AwesomeDialog } from '@/components/docyrus/awesome-dialog';

export function DocumentManager() {
  const [files, setFiles] = useState<Attachment[]>([]);
  const [editingImage, setEditingImage] = useState<string | null>(null);

  return (
    <>
      <FileAttachmentPanel
        attachments={files}
        onUpload={async (newFiles) => {
          const uploaded = await uploadFiles(newFiles);
          setFiles((prev) => [...prev, ...uploaded]);
        }}
        onDelete={(id) => {
          setFiles((prev) => prev.filter((f) => f.id !== id));
        }}
        onPreview={(file) => {
          if (file.type.startsWith('image/')) {
            setEditingImage(file.url);
          }
        }}
        accept="image/*,.pdf,.docx"
        maxSize={10 * 1024 * 1024}
      />

      <AwesomeDialog
        open={!!editingImage}
        onOpenChange={() => setEditingImage(null)}
        title="Edit Image"
        size="xl"
      >
        {editingImage && (
          <ImageEditor
            src={editingImage}
            onSave={async (editedBlob) => {
              await saveEditedImage(editedBlob);
              setEditingImage(null);
            }}
          />
        )}
      </AwesomeDialog>
    </>
  );
}

Kanban with Detail Panel

Project board with click-to-expand detail view:

import { useState } from 'react';

import { Kanban } from '@/components/docyrus/kanban';
import { AwesomeCard } from '@/components/docyrus/awesome-card';
import { CommentsPanel } from '@/components/docyrus/comments-panel';
import { EditableValue } from '@/components/docyrus/editable-value';

export function ProjectBoard() {
  const [selectedTask, setSelectedTask] = useState<Task | null>(null);

  return (
    <div className="flex h-[calc(100vh-4rem)] gap-4">
      {/* Board */}
      <div className="flex-1 overflow-hidden">
        <Kanban
          columns={boardColumns}
          onCardClick={setSelectedTask}
          onCardMove={handleMove}
        />
      </div>

      {/* Detail sidebar */}
      {selectedTask && (
        <AwesomeCard className="w-96 shrink-0 overflow-y-auto">
          <div className="flex flex-col gap-4 p-4">
            <EditableValue
              value={selectedTask.title}
              onSave={(val) => updateTask(selectedTask.id, { title: val })}
              variant="heading"
            />
            <EditableValue
              value={selectedTask.description}
              onSave={(val) => updateTask(selectedTask.id, { description: val })}
              multiline
            />
            <CommentsPanel
              recordId={selectedTask.id}
              comments={selectedTask.comments}
              onAddComment={handleAddComment}
            />
          </div>
        </AwesomeCard>
      )}
    </div>
  );
}

Record Detail Page

Editable record with activity timeline, sharing, and delete confirmation:

import { EditableRecordDetail } from '@/components/docyrus/editable-record-detail';
import { RecordActivityPanel } from '@/components/docyrus/record-activity-panel';
import { RecordSharing } from '@/components/docyrus/record-sharing';
import { RecordDeleteConfirmDialog } from '@/components/docyrus/record-delete-confirm-dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';

export function RecordPage({ recordId }: { recordId: string }) {
  return (
    <div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
      <div className="lg:col-span-2">
        <EditableRecordDetail
          recordId={recordId}
          fields={recordFields}
          onSave={handleSave}
        />
      </div>

      <div>
        <Tabs defaultValue="activity">
          <TabsList className="w-full">
            <TabsTrigger value="activity">Activity</TabsTrigger>
            <TabsTrigger value="sharing">Sharing</TabsTrigger>
          </TabsList>
          <TabsContent value="activity">
            <RecordActivityPanel recordId={recordId} />
          </TabsContent>
          <TabsContent value="sharing">
            <RecordSharing
              recordId={recordId}
              onShare={handleShare}
            />
          </TabsContent>
        </Tabs>
      </div>
    </div>
  );
}

On this page