npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

hazo_collab_forms

v2.2.8

Published

Collaboration form elements

Readme

Hazo Collab Forms

npm version License: MIT

React form components with integrated chat collaboration, built for Next.js with TypeScript and Tailwind CSS.

Features

  • Integrated Chat Collaboration: Each form field supports real-time chat discussions via HazoChat with customizable display modes (embedded, side panel, overlay) and UI controls
  • Unified Form View: Multi-mode form display (Edit, Summary, Print, Approval) with shared context
  • Read-Only Summary Views: Display form data in summary format with status badges for locked/hidden fields
  • Reference Data Display: Show comparison values (e.g., prior year data) below form fields with automatic formatting for currency, booleans, custom styling, and collapsible table accordion for array reference data
  • Advanced Field Controls: Visibility toggle, lock/unlock, field duplication, soft delete, and notes
  • Notes Integration: Database-backed notes on fields via hazo_notes package (optional)
  • Field Tooltips: Help tooltips with hover cards for field labels and data table columns
  • Field Library: Database-backed library for creating, organizing, and reusing form field/group definitions (HazoFieldLibrary)
  • Template Generator: Visual drag-and-drop builder for creating form configurations (HazoTemplateGenerator - deprecated, use HazoFieldLibrary)
  • Editor Theme: Customizable theming system for template generator and field library UI
  • Dynamic Field Management: Add, edit, and delete fields at runtime with dialog components
  • Field Selector: Search and select fields from templates with HazoFieldSelectorDialog
  • Role-Based Icon Controls: Configure control visibility and behavior per role with icons_behaviour system
  • Unified Field Controls: Consistent icon controls across form and summary views with automatic kebab consolidation
  • Prop-Based Controls: Enable/disable features via enable_* props for full control
  • File Upload with Validation: Built-in file upload with custom validation callbacks
  • Document Fields: File-only fields with HazoCollabFormDoc component
  • Mandatory File Indicator: Visual indicators (red asterisk) for required file uploads
  • Custom File Validators: Implement custom validation logic before file upload
  • Form Sets: JSON-based dynamic form generation with full file upload support
  • Field Accordion: Wrap any field in a collapsible accordion to reduce visual noise (e.g., large autofill-populated data tables)
  • Data Form (Working Paper): Structured data form component with style variants, field type definitions, computed fields, formula evaluation, file management, and collapsible groups
  • Sidebar PDF Panel: Embedded PDF viewer panel that opens alongside form fields in HazoDataForm, with ghost-form alignment, pop-out dialog support, and file upload/delete callbacks
  • Financial Fields: Currency, percentage, masked input, and computed field components with locale-aware formatting
  • Style Variant System: Configurable style tokens and class definitions for form field styling
  • Autofill Dropzone: LLM-powered document extraction with multi-file support, progressive data table population, and overwrite conflict detection
  • Data Table in Summary Views: Proper HTML table rendering for data table fields in Summary, Print, and Approval views with aggregation support
  • Document Clarifications: Full lifecycle for document validation issues - flag problems, reference documents, collect structured client responses, and track resolution status (pending, responded, resolved, dismissed)
  • Validation Rule Editor: LLM-powered document validation with visual rule management UI, prompt editor with slash commands and variable references, and automatic clarification generation
  • Content Tagging: LLM-based document classification at upload time via hazo_llm_api integration
  • Server-side Route Handlers: Pre-built create_data_route, create_autofill_route, and create_validation_route factories for Next.js API routes
  • File Validation Badges: Visual indicators on uploaded files showing validation state (validating spinner, pass checkmark, error badge with details popover)
  • Data OK Workflows: Checkbox and multi-state validation with auto-hide/auto-lock features
  • Hidden States: Restrict available data OK states per field or form (e.g., hide "Pending" for agents)
  • Type-Safe: Written in TypeScript with comprehensive type definitions
  • Tailwind CSS Styling: Fully customizable with Tailwind CSS classes
  • shadcn/ui Components: Built on top of accessible Radix UI primitives

Installation

Step 1: Install the Package

npm install hazo_collab_forms

Step 2: Install Peer Dependencies

# Core React dependencies (skip if already installed)
npm install react react-dom

# UI dependencies
npm install react-icons sonner lucide-react
npm install @radix-ui/react-dialog @radix-ui/react-label @radix-ui/react-popover

# Hazo ecosystem packages (required for chat and notes functionality)
npm install hazo_chat hazo_ui hazo_auth hazo_config hazo_notes

# Optional: For LLM-powered validation rules and content tagging
npm install hazo_llm_api

Step 3: Install shadcn/ui Components

This package requires shadcn/ui components. If you haven't initialized shadcn/ui yet:

npx shadcn@latest init

Then install the required components:

# Core components (required for all form fields)
npx shadcn@latest add button label dialog tooltip sonner

# For HazoCollabFormCombo (dropdown/select)
npx shadcn@latest add popover command

# For HazoCollabFormDate (date picker)
npx shadcn@latest add calendar

# For file upload functionality (required when using accept_public_files / accept_private_files props)
npx shadcn@latest add accordion

# For HazoTemplateGenerator (visual form builder)
npx shadcn@latest add tabs resizable

# For field tooltips (optional - falls back to native title if not installed)
npx shadcn@latest add hover-card

# Optional but recommended
npx shadcn@latest add separator card

Step 4: Configure Next.js

Add to your next.config.js:

const nextConfig = {
  transpilePackages: ['hazo_collab_forms'],
};
module.exports = nextConfig;

Step 5: Configure Tailwind CSS

For Tailwind CSS v3:

Add to tailwind.config.ts content array:

content: [
  // ... your existing paths
  "./node_modules/hazo_collab_forms/**/*.{js,ts,jsx,tsx}",
],

For Tailwind CSS v4:

Add the @source directive to your globals.css (or main CSS file) after the tailwindcss import:

@import "tailwindcss";

/* Required: Enable Tailwind to scan this package's classes */
@source "../node_modules/hazo_collab_forms/dist";

This is required because Tailwind v4's JIT compiler does not scan node_modules/ by default. Without this directive, hover states, colors, and layout utilities from this package will not have CSS generated, resulting in broken styling (transparent backgrounds, missing colors, etc.).

Step 6: Create Config Files

Copy the template config files to your project root:

mkdir -p config
cp node_modules/hazo_collab_forms/templates/config/hazo_collab_forms_config.ini ./config/
cp node_modules/hazo_collab_forms/templates/*.ini ./

This creates:

  • config/hazo_collab_forms_config.ini - Main package config (in config/ subdirectory)
  • hazo_chat_config.ini - Chat functionality config
  • hazo_auth_config.ini - Authentication config

Step 7: Verify Installation

npx hazo-collab-forms-verify

This checks all dependencies, config files, and shadcn components are properly installed.


Quick Reference

One-Line Install (All Dependencies)

npm install hazo_collab_forms react react-dom react-icons sonner lucide-react \
  @radix-ui/react-dialog @radix-ui/react-label @radix-ui/react-popover hazo_chat hazo_ui hazo_auth hazo_config hazo_notes

# Optional: For LLM-powered validation rules and content tagging
npm install hazo_llm_api

One-Line shadcn Install (All Components)

npx shadcn@latest add button label dialog tooltip sonner popover command calendar accordion tabs resizable hover-card separator card sheet scroll-area checkbox select

Usage

Basic Example

'use client';

import { HazoCollabFormInputbox } from 'hazo_collab_forms';
import { useState } from 'react';

export default function MyForm() {
  const [value, setValue] = useState('');

  return (
    <HazoCollabFormInputbox
      label="Your Name"
      value={value}
      onChange={setValue}
      field_data_id="user-name"
      field_name="User Name"
      hazo_chat_receiver_user_id="recipient-user-id"
    />
  );
}

File Upload with hazo_files Integration

The file upload system uses hazo_files for centralized file management:

'use client';

import { HazoCollabFormView, FileManagerConfig, FileManagerCallbacks, FileUploadOptions, FormFileAttachment } from 'hazo_collab_forms';
import { useState } from 'react';

export default function DocumentUpload() {
  // Define file management callbacks
  const fileCallbacks: FileManagerCallbacks = {
    upload: async (file: File, options: FileUploadOptions) => {
      const formData = new FormData();
      formData.append('file', file);
      formData.append('field_id', options.field_id);
      formData.append('visibility', options.visibility);

      const response = await fetch('/api/files/upload', {
        method: 'POST',
        body: formData,
      });

      const data = await response.json();
      return {
        file_id: data.file_id,
        ref_id: options.field_id,
        file_name: file.name,
        file_size: file.size,
        mime_type: file.type,
        visibility: options.visibility,
        attached_at: new Date(),
      };
    },

    remove: async (file_id: string, field_id: string) => {
      await fetch(`/api/files/${file_id}`, { method: 'DELETE' });
    },

    get_download_url: (file_id: string, field_id: string) => {
      return `/api/files/${file_id}/download`;
    },

    // Optional: File status polling
    get_status: async (file_ids: string[]) => {
      const response = await fetch(`/api/files/status?ids=${file_ids.join(',')}`);
      const data = await response.json();
      return data.statuses; // Record<file_id, FileStatus>
    },

    // Optional: Reupload functionality
    reupload: async (file_id: string, file: File, options: FileUploadOptions) => {
      // Similar to upload, but replaces existing file
      const formData = new FormData();
      formData.append('file', file);
      formData.append('file_id', file_id);

      const response = await fetch(`/api/files/${file_id}/reupload`, {
        method: 'PUT',
        body: formData,
      });

      const data = await response.json();
      return {
        file_id: data.file_id,
        ref_id: options.field_id,
        file_name: file.name,
        file_size: file.size,
        mime_type: file.type,
        visibility: options.visibility,
        attached_at: new Date(),
      };
    },
  };

  const fileManager: FileManagerConfig = {
    callbacks: fileCallbacks,
    show_file_status: true,      // Show status badges (active, orphaned, etc.)
    allow_reupload: true,         // Enable reupload button
    status_poll_interval: 10000,  // Poll status every 10s
  };

  return (
    <HazoCollabFormView
      mode="edit"
      sections={sections}
      form_data={formData}
      file_manager={fileManager}  // Required for file upload UI to appear
    />
  );
}

File Manager Configuration

File upload now uses a callback-based system via FileManagerConfig. You must implement the following callbacks:

Required Callbacks:

| Callback | Description | |----------|-------------| | upload | Upload a file and return FormFileAttachment | | remove | Delete a file by file_id | | get_download_url | Generate download URL for a file |

Optional Callbacks:

| Callback | Description | |----------|-------------| | get_status | Poll file status (active, orphaned, soft_deleted, missing) | | reupload | Replace an existing file with a new version |

Configuration Options:

| Option | Type | Default | Description | |--------|------|---------|-------------| | callbacks | FileManagerCallbacks | required | File operation callbacks | | show_file_status | boolean | false | Show status badges on files | | allow_reupload | boolean | false | Enable reupload button | | status_poll_interval | number | undefined | Status polling interval (ms) |

Important: File upload UI will only render when file_manager prop is provided to HazoCollabFormView.

File Persistence (Multi-User Scenarios)

When using file uploads in HazoCollabFormView, files uploaded by one user must be persisted to your backend so other users can see them. The component provides callbacks for this purpose.

How File Storage Works:

  1. Files are stored in form_data using the key pattern __files_${field_id}
  2. Private files use the pattern __private_files_${field_id}
  3. When a user uploads a file, on_field_files_change is called with the file metadata
  4. You must save this metadata to your database
  5. When loading the form for another user, include the file metadata in form_data

Implementation Example:

'use client';

import { HazoCollabFormView, FileData } from 'hazo_collab_forms';
import { useState, useEffect } from 'react';

interface FormData {
  [key: string]: unknown;
}

export default function CollaborativeForm({ formId }: { formId: string }) {
  const [formData, setFormData] = useState<FormData>({});

  // Load form data including files from your backend
  useEffect(() => {
    async function loadForm() {
      const response = await fetch(`/api/forms/${formId}`);
      const data = await response.json();
      // data should include __files_* keys with FileData arrays
      // Example: { field_name: "value", __files_document_field: [{ file_id: "...", file_name: "..." }] }
      setFormData(data);
    }
    loadForm();
  }, [formId]);

  // Handle file changes - save to your backend
  const handleFilesChange = async (field_id: string, files: FileData[]) => {
    // Update local state
    setFormData(prev => ({
      ...prev,
      [`__files_${field_id}`]: files
    }));

    // Persist to your backend
    await fetch(`/api/forms/${formId}/files`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ field_id, files })
    });
  };

  // Handle private file changes (if using file visibility feature)
  const handlePrivateFilesChange = async (field_id: string, files: FileData[]) => {
    setFormData(prev => ({
      ...prev,
      [`__private_files_${field_id}`]: files
    }));

    await fetch(`/api/forms/${formId}/private-files`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ field_id, files })
    });
  };

  return (
    <HazoCollabFormView
      mode="edit"
      sections={sections}
      form_data={formData}
      on_form_data_change={setFormData}
      on_field_files_change={handleFilesChange}
      on_field_private_files_change={handlePrivateFilesChange}
    />
  );
}

File Attachment Interfaces:

// New callback-based attachment format
interface FormFileAttachment {
  file_id: string;         // Unique identifier
  ref_id: string;          // Field ID reference
  file_name: string;       // Display name
  file_size: number;       // Size in bytes
  mime_type: string;       // MIME type (e.g., 'application/pdf')
  visibility: 'public' | 'private';
  attached_at: Date;       // Attachment timestamp
}

// File status for status badges
type FileStatus = 'active' | 'orphaned' | 'soft_deleted' | 'missing';

// Legacy format (still supported for backward compatibility)
interface FileData {
  file_path: string;      // Server path for download
  file_name: string;      // Display name
  file_size: number;      // Size in bytes
  file_type: string;      // MIME type
  file_id: string;        // Unique identifier
  uploaded_at: Date;      // Upload timestamp
  visibility?: 'public' | 'private';
}

Conversion Utilities:

import {
  attachment_to_file_data,
  file_data_to_attachment,
  is_form_file_attachment
} from 'hazo_collab_forms';

// Convert new format to legacy
const legacy = attachment_to_file_data(attachment);

// Convert legacy to new format
const attachment = file_data_to_attachment(fileData, 'field_id');

// Type guard
if (is_form_file_attachment(item)) {
  // item is FormFileAttachment
}

Backend API Example (Next.js):

// app/api/forms/[formId]/files/route.ts
import { NextRequest, NextResponse } from 'next/server';

export async function POST(
  request: NextRequest,
  { params }: { params: { formId: string } }
) {
  const { field_id, files } = await request.json();

  // Save to your database
  // Example with Prisma:
  // await prisma.formData.upsert({
  //   where: { formId_fieldId: { formId: params.formId, fieldId: `__files_${field_id}` } },
  //   update: { value: JSON.stringify(files) },
  //   create: { formId: params.formId, fieldId: `__files_${field_id}`, value: JSON.stringify(files) }
  // });

  return NextResponse.json({ success: true });
}

Key Points:

  • Files are NOT automatically persisted - you must implement on_field_files_change
  • The form_data object must include __files_* keys when loading for other users
  • Summary mode will only display files if they exist in form_data
  • The actual file binary is uploaded separately (handled by the component's upload mechanism)
  • Only the file metadata (FileData) needs to be persisted in your form data store

Private File Access Control

Private file sections are only visible to users whose role has control_private_files: { visible: true } in the icons_behaviour controls configuration. This is the recommended way to control private file visibility at the form level.

Role-Based Access (Recommended):

// icons_behaviour.json
{
  "roles": [
    {
      "role_id": "tax_agent",
      "controls": {
        "control_private_files": { "visible": true, "enabled": true },  // Can see private files
        // ... other controls
      }
    },
    {
      "role_id": "client",
      "controls": {
        "control_private_files": { "visible": false, "enabled": false }, // Cannot see private files
        // ... other controls
      }
    }
  ]
}

// In your component
<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={formData}
  icons_behaviour={iconsBehaviour}
  active_role="tax_agent"  // Will have access to private files
/>

Priority (highest to lowest):

  1. Form-level can_access_private_files prop (explicit override)
  2. Role-based control_private_files.visible from icons_behaviour controls
  3. Field-level can_access_private_files prop in FieldConfig
  4. hazo_auth API permission check (using private_files_permission)

Advanced Field Control Icons

The package exports standalone control icons for building custom form workflows:

'use client';

import {
  HazoCollabFormInputbox,
  CollabFormVisibilityIcon,
  CollabFormLockIcon,
  CollabFormAddEntryIcon,
  CollabFormDeleteEntryIcon,
  CollabFormDeleteFieldIcon,
} from 'hazo_collab_forms';
import { useState } from 'react';

export default function AdvancedForm() {
  const [value, setValue] = useState('');
  const [visibility, setVisibility] = useState<'visible' | 'hidden'>('visible');
  const [locked, setLocked] = useState(false);

  return (
    <div>
      {/* Visibility Toggle */}
      <CollabFormVisibilityIcon
        label="Company Name"
        visibility={visibility}
        on_visibility_change={setVisibility}
      />

      {/* Lock/Unlock Field */}
      <CollabFormLockIcon
        label="Company Name"
        locked={locked}
        on_lock_change={setLocked}
      />

      {/* Add Entry - duplicate field */}
      <CollabFormAddEntryIcon
        label="Contact Person"
        field_id="contact_person"
        on_add_entry={(field_id) => console.log('Add entry:', field_id)}
      />

      {/* Delete Entry - remove duplicated field */}
      <CollabFormDeleteEntryIcon
        label="Contact Person"
        field_id="contact_person_2"
        on_delete_entry={(field_id) => console.log('Delete entry:', field_id)}
      />

      {/* Delete Field - soft delete */}
      <CollabFormDeleteFieldIcon
        label="Optional Field"
        field_id="optional_field"
        on_delete={(field_id) => console.log('Delete field:', field_id)}
      />
    </div>
  );
}

Icon Components:

| Icon Component | Description | |----------------|-------------| | CollabFormVisibilityIcon | Toggle field visibility (show/hide) | | CollabFormLockIcon | Toggle field lock (read-only mode) | | CollabFormAddEntryIcon | Duplicate a field entry | | CollabFormDeleteEntryIcon | Remove a duplicated entry | | CollabFormDeleteFieldIcon | Soft delete a field |

Note: All controls are shown based on enable_* props. The consuming app decides which controls to render by passing the appropriate props.

Field-Level Controls in HazoCollabFormView

When using HazoCollabFormView with JSON-based field definitions, you can configure lock, visibility, and delete controls at the field level. This allows different fields to have different controls.

Cascade Priority:

  1. Field-level FieldConfig properties → overrides form-level defaults
  2. Form-level props → applies to all fields
  3. Hardcoded fallback → false (disabled)
'use client';

import { HazoCollabFormView, type FieldConfig } from 'hazo_collab_forms';

const fields: FieldConfig[] = [
  {
    id: "customer_name",
    label: "Customer Name",
    field_type: "field",
    component_type: "HazoCollabFormInputbox",
    value: "",
    // Uses form-level defaults
  },
  {
    id: "tax_file_number",
    label: "Tax File Number",
    field_type: "field",
    component_type: "HazoCollabFormInputbox",
    value: "",
    enable_lock: true,
    enable_visibility_toggle: true,  // Field-level override
    enable_delete: false
  },
  {
    id: "internal_notes",
    label: "Internal Notes",
    field_type: "field",
    component_type: "HazoCollabFormTextArea",
    value: "",
    enable_lock: false,              // Hide lock on this field
    enable_visibility_toggle: true,
    enable_delete: true              // Show delete on this field
  }
];

export default function CustomerForm() {
  return (
    <HazoCollabFormView
      mode="edit"
      sections={[{ section_name: "Customer Form", groups: fields }]}
      form_data={{}}
      enable_lock={true}              // Default for all fields
      enable_visibility_toggle={false} // Default for all fields
      enable_delete={false}            // Default for all fields
    />
  );
}

Available FieldConfig Control Properties:

  • enable_lock?: boolean - Show lock icon
  • enable_visibility_toggle?: boolean - Show visibility toggle icon
  • enable_delete?: boolean - Show delete icon
  • enable_data_ok?: boolean - Show data OK control
  • enable_notes?: boolean - Show notes icon
  • notes_panel_style?: 'popover' | 'slide_panel' - Notes panel style (default: 'popover')
  • notes_background_color?: string - Notes panel background (Tailwind class)
  • notes_save_mode?: 'explicit' | 'auto' - Notes save behavior (default: 'explicit')
  • enable_chat?: boolean - Show chat icon
  • chat_read_only?: boolean - View-only chat mode (no input, just viewing)
  • locked?: boolean - Initial lock state
  • visibility?: 'visible' | 'hidden' - Initial visibility state

Group-Level Control Propagation (Inheritance)

Groups can set enable_* props that automatically propagate to all child fields. This simplifies configuration when all fields in a group need the same controls.

Cascade Priority (highest to lowest):

  1. Field-level enable_* in FieldConfig - Individual field override
  2. Parent-group enable_* - Inherited from parent group
  3. Form-level enable_* in HazoCollabFormViewProps - Applies to all fields
  4. Hardcoded fallback - false

Inheritable Control Props:

  • enable_lock - Lock/protect field control
  • enable_visibility_toggle - Show/hide field control
  • enable_delete - Delete field control
  • enable_data_ok - Data OK validation control
  • enable_notes - Notes control
  • enable_chat - Chat control (requires chat_group_id at form level)
const fields: FieldConfig[] = [
  {
    id: "tax_details",
    label: "Tax Information",
    field_type: "group",
    // These propagate to all children
    enable_lock: true,
    enable_visibility_toggle: true,
    enable_notes: true,
    enable_chat: true,
    sub_fields: [
      // Inherits all enable_* from parent
      { id: "tfn", label: "Tax File Number", component_type: "HazoCollabFormInputbox", value: "" },
      // Override: explicitly disable lock and chat
      { id: "abn", label: "ABN", component_type: "HazoCollabFormInputbox", value: "", enable_lock: false, enable_chat: false },
    ]
  }
];

Utility Function:

import { resolve_field_controls } from 'hazo_collab_forms';

// Resolve effective controls for a field considering inheritance
const resolved = resolve_field_controls(field, parent_group, form_settings);
// Returns: { enable_lock, enable_visibility_toggle, enable_delete, enable_data_ok, enable_notes, enable_chat }

Unified Field Metadata Sync

Control field metadata (visibility, locked, deleted) from a parent component without remounting the form. This enables syncing state between form and summary views.

Props:

  • field_metadata?: Record<string, FieldMetadataInput> - External metadata for controlled sync
  • on_field_metadata_change?: (change: FieldMetadataChange) => void - Unified callback for all metadata changes

Types:

interface FieldMetadataInput {
  visibility?: 'visible' | 'hidden';  // or hidden?: boolean
  locked?: boolean;
  data_ok?: boolean | DataOkState;    // or dataOk
  deleted?: boolean;
}

interface FieldMetadataChange {
  field_id: string;
  visibility?: 'visible' | 'hidden';
  locked?: boolean;
  data_ok?: boolean | DataOkState;
  deleted?: boolean;
}

Usage Example:

import { HazoCollabFormView } from 'hazo_collab_forms';
import type { FieldMetadataInput, FieldMetadataChange } from 'hazo_collab_forms';
import { useState } from 'react';

function MyForm({ sections }) {
  const [fieldMetadata, setFieldMetadata] = useState<Record<string, FieldMetadataInput>>({
    tax_file_number: { visibility: 'hidden', locked: true },
    name: { visibility: 'visible', locked: false },
  });

  // Unified handler - called for any metadata change
  const handleMetadataChange = (change: FieldMetadataChange) => {
    setFieldMetadata(prev => ({
      ...prev,
      [change.field_id]: { ...prev[change.field_id], ...change }
    }));
  };

  return (
    <HazoCollabFormView
      mode="edit"
      sections={sections}
      form_data={{}}
      enable_visibility_toggle={true}
      enable_lock={true}
      field_metadata={fieldMetadata}
      on_field_metadata_change={handleMetadataChange}
    />
  );
}

Benefits:

  • No component remount needed for metadata sync
  • Parent has full control over metadata state
  • Unified callback simplifies state management
  • Backward compatible with existing callbacks (on_visibility_change, on_lock_change, etc.)

Normalization Helper:

import { normalize_field_metadata_input } from 'hazo_collab_forms';

// Normalize input that uses alternative naming conventions
const input = { hidden: true, dataOk: 'ok', locked: true };
const normalized = normalize_field_metadata_input(input);
// Result: { visibility: 'hidden', data_ok: 'ok', locked: true }

Read-Only Summary View

Display form data in a read-only summary format for review workflows using HazoCollabFormView with mode="summary":

'use client';

import { HazoCollabFormView, type ViewSection } from 'hazo_collab_forms';
import { LuUser, LuFileText } from 'react-icons/lu';

const render_icon = (iconName: string) => {
  const iconMap = { LuUser, LuFileText };
  const Icon = iconMap[iconName];
  return Icon ? <Icon className="h-5 w-5" /> : null;
};

export default function FormSummaryPage() {
  return (
    <HazoCollabFormView
      mode="summary"
      sections={sections}
      form_data={{
        name: "John Doe",
        email: "[email protected]",
        tfn: "123456789"
      }}
      render_icon={render_icon}
      show_edit_buttons={true}
      on_edit_section={(index) => router.push(`/edit/${index}`)}
    />
  );
}

With Full Controls:

<HazoCollabFormView
  mode="summary"
  sections={sections}
  form_data={formData}
  field_metadata={fieldMetadata}

  // Enable field controls - each enabled via props
  // Use *_editable props to make controls clickable
  enable_data_ok={true}
  data_ok_mode="multi_state"
  data_ok_editable={true}
  enable_notes={true}
  enable_chat={true}
  chat_group_id="review-session-123"
  chat_read_only={false}  // Set to true for view-only chat mode
  enable_visibility_toggle={true}
  visibility_editable={true}
  enable_lock={true}
  lock_editable={true}
  enable_delete={true}

  // Display settings
  controls_display="inline"
  show_edit_buttons={true}
  on_edit_section={(index) => handleEdit(index)}

  // Callbacks
  on_data_ok_change={(fieldId, value) => console.log('Data OK changed:', fieldId, value)}
  on_notes_change={(fieldId, notes) => console.log('Notes updated:', fieldId, notes)}
  on_visibility_change={(fieldId, visibility) => console.log('Visibility changed:', fieldId, visibility)}
  on_lock_change={(fieldId, locked) => console.log('Lock changed:', fieldId, locked)}

  // Auto-actions
  on_data_ok_hidden={true}    // Auto-hide when marked OK
  on_data_ok_protected={true}  // Auto-lock when marked OK
/>

Key Features:

  • Composable design - enable only the features you need
  • Section icons with custom rendering
  • Edit navigation buttons
  • Field controls: data_ok, notes, chat, lock, visibility, delete
  • Two display modes: inline or popover
  • Row-level controls on DataTables
  • Auto-hide/auto-lock on data_ok change
  • Chat read-only mode for view-only scenarios

Chat Realtime Mode

Control how chat status is polled for all fields in the form. Useful for reducing network requests and log verbosity.

Props:

  • chat_realtime_mode?: 'polling' | 'manual' - Form-level setting
    • 'polling' (default): Automatically poll for chat status every 5 seconds
    • 'manual': Only fetch chat status once on mount and when refresh() is called
// Disable automatic polling - only fetch once on mount
<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={formData}
  enable_chat={true}
  chat_group_id="form-session-123"
  chat_realtime_mode="manual"  // No automatic polling
/>

use_field_chat_status Hook:

For custom implementations, use the use_field_chat_status hook directly:

import { use_field_chat_status } from 'hazo_collab_forms';

const { chat_status, refresh, is_loading } = use_field_chat_status({
  field_ids: ['field1', 'field2'],
  chat_group_id: 'group-id',
  enabled: true,
  realtime_mode: 'manual',  // or 'polling' (default)
  poll_interval: 5000,      // only used when realtime_mode is 'polling'
});

// chat_status: Record<string, 'none' | 'has_messages' | 'has_unread'>
// refresh(): manually trigger a status refresh

Chat Customization

Chat Display and Behavior:

The package provides several props to customize chat appearance and behavior:

Props:

  • chat_read_only?: boolean - View-only mode (users can see chat but cannot send messages)
  • chat_hide_references?: boolean - Hide the references section in chat
  • chat_hide_sidebar?: boolean - Hide the entire document viewer sidebar (requires hazo_chat v5.2.0+)
  • chat_hide_preview?: boolean - Hide message preview/attachment preview
  • chat_display_mode?: 'embedded' | 'side_panel' | 'overlay' - Controls chat rendering mode (default: 'embedded')
  • chat_container_element?: HTMLElement | null - Container for portal rendering with certain display modes
  • chat_log_polling?: boolean - Enable polling console logs for debugging (default: false)
// HazoCollabFormView - customized chat experience
<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={formData}
  enable_chat={true}
  chat_group_id={groupId}
  chat_read_only={false}
  chat_hide_references={true}     // Hide references section
  chat_hide_sidebar={false}        // Show sidebar (requires hazo_chat v5.2.0+)
  chat_hide_preview={false}        // Show message previews
  chat_display_mode="side_panel"   // Render as side panel
  chat_log_polling={false}         // Disable polling logs
/>

// HazoCollabFormView - summary with view-only chat
<HazoCollabFormView
  mode="summary"
  sections={sections}
  form_data={formData}
  enable_chat={true}
  chat_group_id={groupId}
  chat_read_only={true}
/>

// Field-level override in FieldConfig
const fields: FieldConfig[] = [
  {
    id: "active_discussion",
    label: "Active Discussion",
    component_type: "HazoCollabFormInputbox",
    chat_read_only: false  // Can chat on this field
  },
  {
    id: "archived_data",
    label: "Archived Data",
    component_type: "HazoCollabFormInputbox",
    chat_read_only: true   // View-only chat
  }
];

Field Tooltips

Add help tooltips to field labels and data table columns using the tooltip prop. Tooltips display on hover using shadcn's HoverCard component, with automatic fallback to native title attribute if HoverCard is not available.

Usage in Form Fields:

import { HazoCollabFormInputbox } from 'hazo_collab_forms';

<HazoCollabFormInputbox
  label="Tax File Number"
  tooltip="Your 9-digit tax identification number (TFN)"
  value={tfn}
  onChange={setTfn}
/>

// Or with full TooltipConfig
<HazoCollabFormInputbox
  label="ABN"
  tooltip={{
    enabled: true,
    content: "Australian Business Number - 11 digits",
    position: "right" // optional
  }}
  value={abn}
  onChange={setAbn}
/>

Usage in Data Tables:

const columns: DataTableColumn[] = [
  {
    id: "amount",
    label: "Amount",
    type: "number",
    tooltip: "Enter the invoice amount in AUD",
  },
  {
    id: "date",
    label: "Invoice Date",
    type: "date",
    tooltip: {
      enabled: true,
      content: "The date shown on the invoice document"
    }
  }
];

Tooltip in FieldConfig:

const fields: FieldConfig[] = [
  {
    id: "gst_amount",
    label: "GST Amount",
    component_type: "HazoCollabFormInputbox",
    tooltip: "10% of the invoice amount (automatically calculated if empty)",
    value: ""
  }
];

Optional Dependency:

  • Tooltips require hover-card shadcn component for best experience
  • Automatically falls back to native HTML title attribute if hover-card is not installed
  • Install with: npx shadcn@latest add hover-card

Field Library (New in v1.9.0)

Database-backed library for creating, organizing, and reusing form field and group definitions. Elements are stored in the hazo_collab_form_elts table and can be organized by area.

Basic Usage:

'use client';

import { HazoFieldLibrary } from 'hazo_collab_forms';

export default function FieldLibraryPage() {
  return (
    <div className="h-screen">
      <HazoFieldLibrary
        api_base_url="/api/hazo_connect/sqlite/data"
        table_name="hazo_collab_form_elts"
        scope_id="my-project"
      />
    </div>
  );
}

Features:

  • Fields & Groups tabs: Manage individual fields and group definitions
  • Area organization: Categorize elements by area with combobox search and inline creation
  • Property editors: Full field configuration using shared tab editors (Basic, Type, Validation, Controls)
  • Import/Export: JSON import/export for version control and sharing
  • Reference Picker: Link group elements to field library entries
  • Library Search: Search and add fields from the library via HazoAddFieldDialog integration

Props:

| Prop | Type | Description | |------|------|-------------| | api_base_url | string | Base URL for the hazo_connect REST API | | table_name | string | Database table name (default: hazo_collab_form_elts) | | scope_id | string | null | Scope ID for filtering elements | | className | string | Additional CSS class | | theme | Partial<EditorTheme> | Editor theme overrides |

With Template Loader:

Pass the templates prop to add an "Import Template" button in the toolbar. Clicking it opens a dialog where users can select a template and load its fields/groups into the library. Duplicate elements (matching area.key) are automatically skipped.

import { HazoFieldLibrary } from 'hazo_collab_forms';
import type { FieldLibraryTemplateOption } from 'hazo_collab_forms';

const templates: FieldLibraryTemplateOption[] = [
  {
    label: 'Banking Details',
    load: async () => {
      const res = await fetch('/data/form-sets/banking.json');
      const data = await res.json();
      return data.sections || [data];
    },
  },
  {
    label: 'Inline Data',
    sections: [{ section_name: 'Example', groups: [/* ... */] }],
  },
];

<HazoFieldLibrary templates={templates} className="flex-1" />

Database Table:

CREATE TABLE hazo_collab_form_elts (
  id TEXT PRIMARY KEY,
  scope_id TEXT,
  area TEXT NOT NULL,
  key TEXT NOT NULL,
  element TEXT NOT NULL,  -- JSON string of FieldConfig
  created_at TEXT NOT NULL,
  changed_at TEXT NOT NULL
);

Editor Theme

Customize the appearance of HazoFieldLibrary and HazoTemplateGenerator with the editor theme system.

import { HazoFieldLibrary, type EditorTheme } from 'hazo_collab_forms';

const customTheme: Partial<EditorTheme> = {
  label_style: 'uppercase',    // 'uppercase' | 'normal'
  tab_style: 'pill',           // 'pill' | 'underline'
  header_style: 'gradient',    // 'gradient' | 'accent' | 'simple'
  label_icons: true,           // Show icons alongside labels
  max_field_width: 'lg',       // 'full' | 'lg' | 'md'
};

<HazoFieldLibrary theme={customTheme} ... />
<HazoTemplateGenerator theme={customTheme} ... />

Theme Context:

Use EditorThemeProvider and useEditorTheme() for custom components:

import { EditorThemeProvider, useEditorTheme } from 'hazo_collab_forms';

function CustomEditor() {
  const theme = useEditorTheme();
  // Access theme.label_style, theme.tab_style, etc.
}

Autofill Dropzone (LLM Document Extraction)

Drop source documents onto a form group to automatically extract and populate field values using an LLM. Supports multiple files processed sequentially with progressive data application.

The dropzone renders as a compact "AI fill from document" bar at the top of any group that has it enabled. Users can drag-and-drop files or click to select. The file is uploaded, sent to your API endpoint for LLM extraction, and the extracted values are applied to the group's fields automatically.

Setup — Two Steps

Step 1: Enable on the group in your form config (JSON or object):

Set three properties on any group that should have autofill:

{
  "id": "gross_interest_income",
  "field_type": "group",
  "label": "Gross Interest Income",
  "accept_public_files": "ai_upload",  // Enable AI autofill dropzone inside Files accordion
  "prompt_area": "income",             // Domain context for LLM (e.g., "income", "deductions")
  "prompt_key": "income_10_gross_interest",  // Specific context for this group
  "fields": [
    { "id": "bank_name", "label": "Bank/Financial Institution", "component_type": "HazoCollabFormInputbox" },
    { "id": "amount", "label": "Amount ($)", "component_type": "HazoCollabFormInputbox", "input_type": "numeric" },
    { "id": "withheld", "label": "Withheld Amount ($)", "component_type": "HazoCollabFormInputbox", "input_type": "numeric" }
  ]
}

| Group Prop | Type | Required | Description | |------------|------|----------|-------------| | accept_public_files | FileAcceptMode | Yes | Set to 'ai_upload' for AI autofill dropzone, 'basic_upload' for standard file upload | | prompt_area | string | Yes | Domain context sent to LLM (e.g., "income", "deductions") | | prompt_key | string | Yes | Group-specific context sent to LLM (e.g., "income_10_gross_interest") |

Note: If accept_public_files is 'ai_upload' but prompt_area or prompt_key are missing, a console warning is logged and the dropzone falls back to standard upload.

Step 2: Provide props on HazoCollabFormView:

<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={formData}
  on_change={handleChange}
  // Required for autofill:
  autofill_api_endpoint="/api/autofill/extract"
  file_manager={fileManagerConfig}
/>

| View Prop | Type | Required | Description | |-----------|------|----------|-------------| | autofill_api_endpoint | string | Yes | Your API endpoint that handles extraction | | file_manager | FileManagerConfig | Yes | File upload callbacks (also used for regular file uploads) |

All four conditions must be met for the AI autofill dropzone to render on a group:

  1. accept_public_files: 'ai_upload' on the group config
  2. prompt_area set on the group config
  3. prompt_key set on the group config
  4. autofill_api_endpoint and file_manager provided on HazoCollabFormView

The AI dropzone renders inside the Files accordion (replacing the standard drag-and-drop area), keeping a single unified file section per group.

Backward Compatibility: The old boolean props accept_files_public: true and accept_files_private: true are still supported and automatically map to 'basic_upload'. Existing JSON configs will continue to work without changes.

Multi-File Behavior

  • Multiple files can be dropped or selected at once
  • Files are processed sequentially through the LLM API
  • Array values (data table rows) are appended to existing data, not overwritten
  • Scalar values trigger an overwrite confirmation dialog (only on the first conflicting file in a batch)
  • Progress indicator shows "Processing file N of M..."
  • The uploaded source document is automatically attached to the group's files (__files_{group_id})

API Contract

Your autofill_api_endpoint receives a POST request with this body:

AutofillRequest:

| Field | Type | Description | |-------|------|-------------| | file_id | string | Uploaded file ID (from file_manager) | | file_name | string | Original file name | | mime_type | string | File MIME type (e.g., image/jpeg, application/pdf) | | download_url | string | URL to download the file | | file_b64 | string? | Base64-encoded file content (sent inline to avoid re-download) | | group_id | string | Group identifier | | prompt_area | string? | Prompt area for LLM context | | prompt_key | string? | Prompt key for LLM context | | fields | AutofillFieldSchema[] | Field schemas describing what to extract |

AutofillFieldSchema:

| Field | Type | Description | |-------|------|-------------| | field_id | string | Field identifier (used as key in response data) | | label | string | Human-readable label | | component_type | string | Component type (e.g., HazoCollabFormInputbox, HazoCollabFormDate) | | description | string? | Optional description for LLM context | | input_type | string? | Input type hint (mixed, numeric, email, alpha) | | input_options | Array? | Options for radio/combo fields ([{ value, label }]) | | table_config | object? | Table configuration for data-table fields |

AutofillResponse (your API must return):

{
  success: boolean;          // Whether extraction succeeded
  data: Record<string, unknown>;  // Extracted values keyed by field_id
  error?: string;            // Error message if success=false
  message?: string;          // Info message (e.g., "No matching data found")
}

Response examples:

Scalar fields — values keyed by field_id:

{
  "success": true,
  "data": {
    "bank_name": "Commonwealth Bank",
    "amount": "1500.00",
    "withheld": "0"
  }
}

Data table fields — return arrays which are appended as rows:

{
  "success": true,
  "data": {
    "interest_table": [
      { "bank": "CBA", "amount": "1200.00", "withheld": "0" },
      { "bank": "ANZ", "amount": "300.50", "withheld": "15.00" }
    ]
  }
}

No matching data (file processed successfully but nothing relevant found):

{
  "success": true,
  "data": {},
  "message": "No matching data found in this document."
}

Pre-Built Server Route (Optional)

If you use hazo_llm_api, a ready-made route handler is provided — see Server-Side Route Handlers: Autofill Route below.

Custom API Endpoint (BYO LLM)

If you prefer your own LLM integration, implement the endpoint to:

  1. Receive the AutofillRequest body
  2. Use file_b64 (preferred) or fetch from download_url to get the file
  3. Send the file + field schemas to your LLM with an extraction prompt
  4. Parse the LLM response into a Record<string, unknown> keyed by field_id
  5. Return an AutofillResponse
// Example: app/api/autofill/extract/route.ts (Next.js App Router)
import { NextRequest, NextResponse } from 'next/server';

export async function POST(request: NextRequest) {
  const body = await request.json();
  const { file_b64, mime_type, fields } = body;

  // Build prompt from field schemas
  const field_descriptions = fields.map(
    (f: any) => `- "${f.field_id}" (${f.label}): ${f.component_type}`
  ).join('\n');

  const prompt = `Extract the following fields from this document and return ONLY valid JSON.
Fields: \n${field_descriptions}`;

  // Call your LLM (e.g., Claude, OpenAI, etc.)
  const llm_result = await call_your_llm({ prompt, file_b64, mime_type });

  return NextResponse.json({
    success: true,
    data: llm_result,  // { bank_name: "CBA", amount: "1200.00", ... }
  });
}

Exports

// Client-side component and types
import {
  AutofillDropzone,
  type AutofillFieldSchema,
  type AutofillRequest,
  type AutofillResponse,
  type AutofillDropzoneProps,
  type AutofillStatus,
  type OverwriteConflict,
} from 'hazo_collab_forms';

// Server-side route handler (Node.js only)
import {
  create_autofill_route,
  type AutofillRouteOptions,
} from 'hazo_collab_forms/lib';

Template Generator (Deprecated)

Deprecated: Use HazoFieldLibrary for managing form field definitions. HazoTemplateGenerator is still available but will be removed in a future major version.

Visual form builder for creating form configurations with drag-and-drop interface. Build complex form structures without writing JSON manually.

Basic Usage:

'use client';

import { HazoTemplateGenerator } from 'hazo_collab_forms';
import { useState } from 'react';

export default function TemplateBuilder() {
  const [template, setTemplate] = useState({
    sections: [],
    icons_behaviour: {}
  });

  return (
    <div className="h-screen">
      <HazoTemplateGenerator
        initial_template={template}
        on_template_change={setTemplate}
        on_save={(template) => {
          console.log('Saved template:', template);
          // Save to database or file
        }}
        on_cancel={() => router.push('/templates')}
      />
    </div>
  );
}

Features:

  • Visual Tree Editor: Organize sections, groups, and fields hierarchically
  • Live Preview: See your form render in real-time as you build
  • Property Editors: Configure field properties, validation, and controls
  • Data Table Editor: Visual builder for complex data table configurations
  • Icons Behaviour Editor: Configure role-based control visibility
  • Template Library: Load and save reusable templates
  • Import/Export: JSON import/export for version control
  • Full-Screen Preview: Test your form in full-screen dialog
  • Undo/Redo: Complete history navigation

Props:

| Prop | Type | Description | |------|------|-------------| | initial_template | Section[] | Starting template structure | | on_template_change | callback | Called when template changes | | on_save | callback | Save button handler | | on_cancel | callback | Cancel button handler | | available_templates | TemplateOption[] | Predefined templates to load | | on_load_template | callback | Called when template is loaded | | preview_chat_group_id | string | Chat group ID for live preview | | readonly | boolean | Disable editing (view-only mode) | | show_import_export | boolean | Show import/export buttons | | show_controls_tab | boolean | Show advanced controls configuration |

Template Structure:

The generator produces standard form configuration:

interface TemplateOutput {
  sections: Section[];
  icons_behaviour?: IconsBehaviour;
}

Required shadcn Components:

  • resizable (for panel layout)
  • tabs (for editor interface)
  • hover-card (optional, for tooltips)

Install with: npx shadcn@latest add resizable tabs

Unified Form View (HazoCollabFormView) - Recommended

A unified component that combines edit, summary, print, and approval views with shared context. Switch between view modes without remounting, preserving state and field metadata.

Note: HazoCollabFormView is the recommended component. It replaces the older HazoCollabFormSet (deprecated, internal only since v1.9.0) and HazoCollabFormSummary (fully removed in v1.9.0).

Basic Usage:

'use client';

import { HazoCollabFormView, type ViewMode } from 'hazo_collab_forms';
import { useState } from 'react';

export default function UnifiedFormPage() {
  const [viewMode, setViewMode] = useState<ViewMode>('edit');
  const [formData, setFormData] = useState<Record<string, unknown>>({});

  return (
    <div>
      {/* View mode switcher */}
      <div className="flex gap-2 mb-4">
        <button onClick={() => setViewMode('edit')}>Edit</button>
        <button onClick={() => setViewMode('summary')}>Summary</button>
        <button onClick={() => setViewMode('print')}>Print</button>
        <button onClick={() => setViewMode('approval')}>Approval</button>
      </div>

      <HazoCollabFormView
        sections={sections}
        view_mode={viewMode}
        form_data={formData}
        on_form_data_change={setFormData}
        icons_behaviour={iconsBehaviour}
        active_role="tax_agent"
        enable_chat={true}
        chat_group_id="form-session-123"
      />
    </div>
  );
}

View Modes:

| Mode | Description | |------|-------------| | edit | Full form editing with all field controls | | summary | Read-only summary view for review | | print | Print-optimized layout | | approval | Review with data_ok approval workflow |

Key Props:

| Prop | Type | Description | |------|------|-------------| | sections | ViewSection[] | Form sections | | view_mode | ViewMode | Current view mode | | form_data | Record<string, unknown> | Form values | | on_form_data_change | callback | Called when form data changes | | field_metadata | Record<string, FieldMetadata> | Field metadata (visibility, lock, etc.) | | on_field_metadata_change | callback | Called when metadata changes | | icons_behaviour | IconsBehaviour | Role-based control configuration | | active_role | string | Current user's role |

Context Hooks:

Access form state from nested components using hooks:

import {
  useFormViewContext,
  useViewMode,
  useFormData,
  useFieldMetadata,
  useFieldValue,
  useFieldMeta,
  useApprovalStats,
} from 'hazo_collab_forms';

function FieldComponent({ fieldId }) {
  const viewMode = useViewMode();
  const value = useFieldValue(fieldId);
  const { data_ok, locked, visibility } = useFieldMeta(fieldId);
  const { total, approved, pending } = useApprovalStats();

  // Render based on view mode and field state
}

Field Layout Properties (HazoDataForm)

Control how fields render their label and input using layout properties on ViewFieldConfig. These properties only apply to HazoDataForm (the structured data form component).

label_position

Controls label placement relative to the input.

| Value | Description | Result | |-------|-------------|--------| | 'inline' (default) | Label on the left, input on the right | Side-by-side layout with fixed-width input column | | 'stacked' | Label above, input below spanning full width | Vertical layout ideal for address fields and wide inputs |

Inline layout (default):

┌─────────────────────────────────┬──────────────┐
│ Label text                      │ [  input   ] │
└─────────────────────────────────┴──────────────┘

Stacked layout (label_position: "stacked"):

┌─────────────────────────────────────────────────┐
│ Label text                                      │
│ [  input (full width)                         ] │
└─────────────────────────────────────────────────┘

Usage:

{
  "id": "postal_address_line_1",
  "label": "Address",
  "field_type": "text",
  "label_position": "stacked"
}

orientation (groups only)

Controls how child fields within a group are arranged.

| Value | Description | Result | |-------|-------------|--------| | 'vertical' (default) | Fields stack top-to-bottom | Standard form layout | | 'horizontal' | Fields arrange side-by-side | Columns layout; forces label_position: "stacked" on all children |

Vertical group (default):

┌───────────────────────────────────────┐
│ Group Header                          │
│ ┌───────────────┬──────────────┐      │
│ │ State         │ [  VIC     ] │      │
│ ├───────────────┼──────────────┤      │
│ │ Postcode      │ [  3150    ] │      │
│ └───────────────┴──────────────┘      │
└───────────────────────────────────────┘

Horizontal group (orientation: "horizontal"):

┌──────────────────────────────────────────────────┐
│ Group Header                                     │
│ ┌──────────────────┐  ┌──────────────────┐       │
│ │ State            │  │ Postcode         │       │
│ │ [  VIC         ] │  │ [  3150        ] │       │
│ └──────────────────┘  └──────────────────┘       │
└──────────────────────────────────────────────────┘

Usage:

{
  "id": "address_fields",
  "label": "Address Details",
  "field_type": "group",
  "orientation": "horizontal",
  "fields": [
    { "id": "state", "label": "State", "field_type": "text" },
    { "id": "postcode", "label": "Postcode", "field_type": "text" }
  ]
}

Note: When a group has orientation: "horizontal", all child fields are automatically forced to label_position: "stacked" regardless of their individual setting.

value_width / value_column_width

Control the width of the input column in inline layout.

| Property | Scope | Default | Description | |----------|-------|---------|-------------| | value_column_width | Group | '200px' | Sets the input width for all fields in the group | | value_width | Field | inherits group | Overrides width for a single field |

These have no effect in stacked layout (input always spans full width).

Layout Resolution Priority

The final layout for any field is resolved in this order (first match wins):

  1. force_layout — Set internally when a group has orientation: "horizontal" (forces "stacked")
  2. field.label_position — Per-field setting in your JSON config
  3. "inline" — Default fallback

Full Example: Address Section

{
  "section_name": "Addresses",
  "groups": [
    {
      "id": "addresses_row",
      "field_type": "group",
      "orientation": "horizontal",
      "fields": [
        {
          "id": "postal_address",
          "label": "Your Postal Address",
          "field_type": "group",
          "fields": [
            { "id": "postal_address_line_1", "label": "Address", "field_type": "text", "label_position": "stacked" },
            { "id": "postal_state", "label": "State", "field_type": "text" },
            { "id": "postal_postcode", "label": "Postcode", "field_type": "text" }
          ]
        },
        {
          "id": "home_address",
          "label": "Your Home Address",
          "field_type": "group",
          "fields": [
            { "id": "home_address_line_1", "label": "Address", "field_type": "text", "label_position": "stacked" },
            { "id": "home_state", "label": "State", "field_type": "text" },
            { "id": "home_postcode", "label": "Postcode", "field_type": "text" }
          ]
        }
      ]
    }
  ]
}

This renders the address line with label on top and full-width input, while state/postcode use the default inline (label-left) layout.

Paired Fields / Dual-Column Layout (HazoDataForm)

Render two value columns side by side for fields that need "Gross" / "My share" style layouts (e.g., rental schedules). Uses paired_field on a field and column_headers on the parent group.

Schema:

{
  "id": "rental_income",
  "field_type": "group",
  "label": "Income",
  "value_column_width": "120px",
  "column_headers": [
    { "label": "Gross", "width": "120px" },
    { "label": "My share", "width": "120px" }
  ],
  "fields": [
    {
      "id": "rent_gross",
      "field_type": "currency",
      "label": "Rental income",
      "badge": "A",
      "paired_field": {
        "id": "rent_my_share",
        "field_type": "currency",
        "currency_symbol": "$",
        "decimal_places": 2
      }
    },
    {
      "id": "gross_rent_total",
      "field_type": "summary_row",
      "label": "Gross rent",
      "style_variant": "summary_bold",
      "paired_field": { "id": "gross_rent_my_share_total" }
    }
  ]
}

Supported field types: | Field Type | Paired Behavior | |------------|----------------| | currency | Two currency inputs (edit) or two formatted values (view) | | summary_row | Two formatted value columns | | date | Two date inputs within one value column (date ranges) |

How it works:

  • column_headers on the group renders header labels above the value columns
  • paired_field on a field causes the layout to render a second value column
  • The primary field's value uses form_data[field.id], the paired uses form_data[paired_field.id]
  • Both columns use value_column_width from the group for consistent alignment

Sidebar PDF Panel (HazoDataForm)

When file_panel_display_mode="sidebar" is set on HazoDataForm, clicking a field's file icon opens an embedded PDF viewer panel alongside the form instead of a modal dialog. A ghost form panel mirrors the form layout to align the PDF panel vertically with the active field.

Props:

| Prop | Type | Default | Description | |------|------|---------|-------------| | file_panel_display_mode | 'dialog' \| 'sidebar' | 'dialog' | Display mode for file panels | | pdf_panel_default_size | number | 50 | Sidebar width as percentage | | pdf_panel_min_size | number | 25 | Minimum sidebar width as percentage | | pdf_viewer_props | Record<string, unknown> | – | Pass-through props for the PdfViewer component | | file_access_provider | unknown | – | hazo_pdf FileAccessProvider for the embedded viewer | | on_panel_open | (field_id: string) => void | – | Called when the sidebar opens for a field | | on_panel_close | () => void | – | Called when the sidebar closes | | on_panel_pop_out | (field_id, file_items) => void | – | Custom pop-out handler (uses built-in PdfViewerDialog if not provided) | | on_panel_upload | (field_id, file, converted_pdf?) => Promise<unknown> | – | Upload handler receiving the active field_id | | on_panel_file_delete | (field_id, file_id) => void | – | Delete handler receiving the active field_id |

Usage:

import { HazoDataForm } from 'hazo_collab_forms';

<HazoDataForm
  sections={sections}
  form_data={formData}
  on_field_change={handleChange}
  enable_file_upload={true}
  default_accept_files={true}
  file_panel_display_mode="sidebar"
  pdf_panel_default_size={50}
  file_access_provider={fileAccessProvider}
  on_panel_upload={async (field_id, file) => {
    // Upload file and update form_data for the given field_id
    return { success: true };
  }}
  on_panel_file_delete={(field_id, file_id) => {
    // Remove file from form_data for the given field_id
  }}
/>

How it works:

  • The form splits into two columns: the real form on the left and a ghost form on the right
  • The ghost form mirrors the section/group/field hierarchy with invisible fields
  • The PDF panel renders at the active field's position with visibility: visible and position: sticky
  • For horizontal groups, the panel renders at full width below the flex row
  • The pop-out button opens a PdfViewerDialog (requires hazo_pdf)

Requires: hazo_pdf as an optional peer dependency (dynamically imported).

Collapsible Groups (HazoDataForm)

Groups can be made collapsible with a chevron toggle in the group header. Supports both form-level and per-group configuration.

Form-level (all groups collapsible):

<HazoDataForm
  sections={sections}
  form_data={formData}
  collapsible_groups={true}
  collapsed_groups={['group_id_1']}        // Controlled collapsed state
  on_group_toggle={(group_id, collapsed) => {
    // Update collapsed_groups state
  }}
/>

Per-group override (via ViewFieldConfig):

{
  "id": "details_group",
  "field_type": "group",
  "label": "Additional Details",
  "collapsible": true,
  "initial_collapsed": true,
  "fields": [...]
}

| Prop | Type | Description | |------|------|-------------| | collapsible_groups | boolean | Enable collapse for all groups (default: false) | | collapsed_groups | string[] | Currently collapsed group IDs (controlled) | | on_group_toggle | (group_id, collapsed) => void | Callback when a group is toggled |

Per-group collapsible overrides the form-level collapsible_groups. Only groups with a label (header) can be collapsed.

Field Accordion

Wrap any individual field in a collapsible accordion. Useful for large fields (e.g., data tables populated via autofill) that can be collapsed to reduce visual noise. The accordion starts collapsed by default and toggles on click.

{
  "id": "expense_table",
  "label": "Expense Details",
  "component_type": "HazoCollabFormDataTable",
  "accordion": true,
  "accordion_default_open": false
}

| Prop | Type | Description | |------|------|-------------| | accordion | boolean | Wrap this field in a collapsible accordion (default: false) | | accordion_default_open | boolean | Start accordion open (default: false — starts collapsed) |

Works in both edit and summary view modes.

Reference Data (Comparison Values)

Display reference or comparison values below form fields, such as prior year tax data or baseline values. Reference data supports automatic formatting for currency, booleans, and custom styling. Each field can have a single reference entry or multiple entries displayed as stacked tags.

Basic Usage (Single Entry):

import { HazoCollabFormView, type ReferenceData } from 'hazo_collab_forms';

const referenceData: ReferenceData = {
  salary_income: {
    value: 82450.00,                    // Formatted as $82,450.00
    label: "Prior Year (2023)",
    background_color: "bg-blue-50"      // Optional Tailwind class
  },
  has_dependents: {
    value: true,                        // Formatted as "Yes"
    label: "2023"
  },
  filing_status: {
    value: "married",                   // String pass-through
    label: "Last year"
  }
};

<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={formData}
  reference_data={referenceData}
/>

Multiple Reference Entries Per Field:

Each field can have an array of reference entries, displayed as stacked tags. This is useful when multiple sources contribute reference values (e.g., multiple document autofills).

const referenceData: ReferenceData = {
  state: [
    {
      id: "autofill_1",                 // Optional ID for deletion tracking
      value: "VIC",
      label: "Client Records",
      background_color: "bg-blue-50"
    },
    {
      id: "autofill_2",
      value: "NSW",
      label: "Prior Year (2024)",
      background_color: "bg-green-50"
    }
  ],
  // Single entry still works
  salary_income: {
    value: 82450.00,
    label: "Prior Year (2023)"
  }
};

Deleting Reference Entries:

Provide on_reference_delete to show a hover trash icon on each entry:

<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={formData}
  reference_data={referenc