# Hazo Collab Forms

[![npm version](https://img.shields.io/npm/v/hazo_collab_forms.svg)](https://www.npmjs.com/package/hazo_collab_forms)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/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
- **Multi-Column Layout**: Render fields in 1, 2, or 3 columns per row in edit mode via the `field_columns` prop; wide fields (textarea, data table, file groups) span the full row automatically. Override per-field grid placement with `container_class_name` on `FieldConfig`.
- **Section Chrome Control**: Strip the bordered card (`disable_section_card`) and/or section title bar (`hide_section_header`) from each `EditSection` — useful when embedding the form inside an existing styled container
- **KeyInfoDrawer Enhancements**: Chevron position (`chevron_position`), ReactNode title, explicit `aria_label`, and section chrome forwarding for flexible embedding
- **ThreadForm `task_number_offset`**: Shift Q-pill numbering by a fixed amount — pass `1` when the KeyInfoDrawer occupies Q1 so tasks start at Q2
- **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
- **Split Amount Field**: `HazoCollabFormSplitAmount` composite field that splits an expense into total / work % / computed deductible (`total × work_percent / 100`), with pure helpers (`parse_split_value`, `compute_deductible`, `clamp_work_percent`, `is_split_value`) exported from `hazo_collab_forms/utils` and a `split_amount` entry in the `BaseFieldType` union
- **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)
- **Batch Decisions Panel (Agent View)**: Multi-rule files render a per-rule radio (Pending / Accept / Send Back) plus a single "Submit Decisions" button — the agent picks the action per rule, optionally edits the LLM-suggested client comment, and commits all decisions in one click. Single-rule files keep the existing fast-path UX.
- **Send Back Merging**: Per-rule Send Back clicks for the same parent question merge into a single open "Clarifications regarding…" task as separate messages instead of spawning a task per click. Closed clarifications tasks start a fresh round.
- **Consolidated Action Required Rendering**: Consecutive send_back messages sharing the same source file render as one panel — file row shown once, N stacked "ACTION REQUIRED" boxes. The `ref` badge is clickable, scrolling to the original file in the parent question and briefly highlighting it.
- **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
- **Configurable Response Types & Sets**: Manage response options for validation rules via config editor, with dynamic type/set loading and tag picker UI for interactive option management
- **Interactive Response Options Editor**: Agents can add/remove response options in Send-Back view with options persisting to JSON data model
- **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)
- **Hierarchical JSON Data Model**: Complete form data serialization combining all instances with activity tracking, option set history, and processed data arrays
- **Review Queue Accordion UI**: Collapsible review and send-back items with status indicators (pink for pending, blue for actioned)
- **ThreadForm Key-Info Drawer** (v5.6.0+): Collapsible region above the task list that embeds HazoCollabFormView with H.7 confirm-state for prefill review workflows
- **H.7 Confirm State**: First-class field metadata for prefill review workflows with confirm/reject/edited states and auto-edit detection
- **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

```bash
npm install hazo_collab_forms
```

### Step 2: Install Peer Dependencies

```bash
# 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 (hazo_core and hazo_ui are required, others are optional)
npm install hazo_core hazo_ui hazo_chat hazo_auth hazo_config hazo_notes

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

### Step 3: Configure Next.js

Add to your `next.config.js`:

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

### Step 4: Configure Tailwind CSS

**For Tailwind CSS v3:**

Add to `tailwind.config.ts` content array:

```typescript
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:

```css
@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 5: Create Config Files

Copy the template config files to your project root:

```bash
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

```bash
npx hazo-collab-forms-verify
```

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

---

## Quick Reference

### One-Line Install (All Dependencies)

```bash
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_core 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)

```bash
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

```typescript
'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:

```typescript
'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:**

```typescript
'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:**

```typescript
// 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:**

```typescript
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):**

```typescript
// 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):**

```typescript
// 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:

```typescript
'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)

```typescript
'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)

```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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:**
```typescript
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 }
```

### Per-field action slots (`field_action_components`)

Render consumer-owned components inline next to every scalar field row. Use
this for audit-history icons, RFI buttons, validation explanations, or any
per-field detail the form itself does not own.

Available on both `HazoCollabFormView` (all four modes — `edit | summary |
print | approval`) and `HazoDataForm` (`edit | view`).

Two slot positions, used independently or together:

| Slot | Position |
|---|---|
| `row_end` | Inline at the end of the row, after the field's value column, file column, and `UnifiedFieldControls`. Right place for a small icon or pill. |
| `row_below` | Full-width block, immediately below the row, before the next field starts. Right place for an inline audit log, validation panel, or expandable detail. |

```typescript
'use client';

import { HazoCollabFormView, type FieldActionContext } from 'hazo_collab_forms';
import { FieldAuditIcon } from '@/lib/audit/ui/FieldAuditIcon';
import { FieldAuditHistory } from '@/lib/audit/ui/FieldAuditHistory';

const field_actions = {
  // Clock icon at the end of every row
  row_end: ({ field }: FieldActionContext) => (
    <FieldAuditIcon
      subjectKind="client_folder"
      subjectId={folderId}
      fieldPath={field.id}
    />
  ),
  // Inline history panel below every row (returns null when there's no
  // history for the given field)
  row_below: ({ field }: FieldActionContext) => (
    <FieldAuditHistory
      subjectKind="client_folder"
      subjectId={folderId}
      fieldPath={field.id}
    />
  ),
};

<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={form_data}
  field_action_components={field_actions}
/>
```

The `FieldActionContext` passed to `row_end` contains:

| Field | Type | Notes |
|---|---|---|
| `field` | `ViewFieldConfig` | The same field config rendered in the row. |
| `value` | `unknown` | The current `form_data[field.id]`. |
| `mode` | `ViewMode \| 'edit' \| 'view'` | Active render mode. |
| `section_id` | `string?` | `section.section_name` when available. |
| `group_id` | `string?` | Id of the nearest enclosing group, when available. |

Return `null` from `row_end` to render nothing for a given field. When the
prop is omitted entirely the rendered DOM is byte-identical to the previous
version.

**Coverage note:** the slot fires for scalar fields only — inputbox,
textarea, currency, percentage, computed, date, combo, radio, checkbox,
masked, static text, summary row, and `HazoCollabFormDoc`. Group header
rows, data table fields (`HazoCollabFormDataTable` / `table`), per-cell
positions inside tables, and `HazoCollabFormSplitAmount` are not slotted in
this release. `ThreadForm` forwards both `field_action_components` and
`enable_field_audit` to its embedded Key-Info `HazoCollabFormView` since v5.9.0.

### Built-in FieldAudit (`enable_field_audit`)

Since v5.9.0, the package ships a ready-made audit history feature you can
turn on with a single flag.

```typescript
<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={form_data}
  on_form_data_change={set_form_data}
  current_user={current_user}
  enable_field_audit  // <-- that's the whole feature
/>
```

When `true`:

- a `FieldAuditProvider` is mounted around the form,
- a `FormDataAuditor` auto-records each value change as a `source: 'manual'`
  entry, **debounced per field at 750ms by default** (tune with
  `audit_debounce_ms`) so a typing burst becomes one entry, not one per
  keystroke,
- a small clock icon is overlaid into `row_end` and an inline history
  panel into `row_below` for every scalar field.

Any consumer-supplied `field_action_components` wins on collision, so you
can mix the built-in audit with your own custom slot in the other
position.

For non-manual sources (e.g. a clarification flow that resolves into a
value update), call `useFieldAudit().record_change({ field_id,
source: 'clarification', new_value, prev_value, user })` from your
handler. If the auto-watcher then sees the same value within ~500ms it
upgrades its existing `manual` entry to `'clarification'` instead of
duplicating it.

The audit lives entirely in-session — there is no persistence on reload.
If you need durable audit logs, pair the flag with your own write inside
`on_field_change`.

**Visibility:** intended for the tax-agent surface of a workflow. Omit
the flag on the client portal to hide audit history.

## Per-Field Actions and Audit Integration (v5.10+)

### Composable field actions

The `field_actions` prop accepts an array of `FieldAction` descriptors, rendered at `row_end` or `row_below` position per field:

```tsx
import type { FieldAction } from 'hazo_collab_forms'

const actions: FieldAction[] = [
  {
    id: 'customer_query',
    position: 'row_end',
    order: 50,
    render: (ctx) => <QueryButton fieldId={ctx.field.id} />,
    when: (ctx) => ctx.field.client_eligible === true,
  },
]

<HazoCollabFormView
  field_actions={actions}
  // ...other props
/>
```

### hazo_audit integration

Install `hazo_audit` and pass `field_audit` to get a per-field audit trail icon:

```bash
npm install hazo_audit
```

```tsx
<HazoCollabFormView
  field_audit={{
    subjectKind: 'form_instance',
    subjectId: instanceId,
  }}
  form_id={formId}
  // ...other props
/>
```

The audit icon appears in `row_end` at order 100 (after custom actions). Clicking it opens a popover with the field's change history from the hazo_audit backend.

Intent events (`form_submitted`, `field_confirmed`, `autofill_applied`, `field_revalidated`) are emitted to `POST /api/audit/intent` automatically.

### Action ordering

| Built-in id | Order | Description |
|---|---|---|
| (custom) | 50 | Recommended for customer-query/RFI buttons |
| (custom) | 80 | Recommended for chat-adjacent actions |
| audit | 100 | hazo_audit icon (built-in when `field_audit` is set) |
| (custom) | 200 | Recommended for low-priority custom actions |

### Migration from `field_action_components`

`field_action_components` is deprecated. Replace:

```tsx
// Before (deprecated)
field_action_components={{ row_end: MyAuditIcon }}

// After
field_actions={[{
  id: 'my_action',
  position: 'row_end',
  order: 100,
  render: (ctx) => <MyAuditIcon {...ctx} />,
}]}
```

### Migration from `enable_field_audit`

`enable_field_audit` (in-session only, no backend) is deprecated:

```tsx
// Before (deprecated — in-session only)
enable_field_audit={true}

// After — real backend via hazo_audit
field_audit={{ subjectKind: 'form', subjectId: id }}
```

### Confirm-required Field State (H.7)

Use `confirm_state` in `field_metadata` to mark prefilled fields that a user must review and confirm. Three states: `'prefilled'` (AI/system-filled, needs review), `'confirmed'` (user approved), `'edited'` (user changed the value).

#### CSS classes

Both the `[data-field-id]` wrapper and the inner container receive `hcf-confirm-{state}`. Import the optional stylesheet for default highlight colours:

```css
/* In your app globals.css */
@import 'hazo_collab_forms/styles/confirm-state.css';
```

Override the CSS variables to match your design system:

```css
:root {
  --hcf-confirm-prefilled-bg: hsl(38 92% 95%);   /* amber */
  --hcf-confirm-confirmed-bg: hsl(142 72% 95%);   /* green */
  --hcf-confirm-edited-bg:    hsl(217 91% 95%);   /* blue  */
}
```

#### Inline Confirm button

Set `show_confirm_action={true}` on `HazoCollabFormView` to render a small green ✓ button next to each prefilled field. Clicking it fires `on_metadata_change` with `{ field_id, changes: { confirm_state: 'confirmed' } }`.

```tsx
<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={form_data}
  field_metadata={field_metadata}
  show_confirm_action={true}
  on_metadata_change={({ field_id, changes }) => {
    setFieldMetadata(prev => ({
      ...prev,
      [field_id]: { ...prev[field_id], ...changes },
    }));
  }}
/>
```

#### Auto-edit detection

The `EditView` automatically transitions `'prefilled'` → `'edited'` when the user changes a value, and `'edited'` → `'prefilled'` if the value returns to the original snapshot. No extra props needed — it fires `on_metadata_change` when transitions occur.

#### Bulk confirm via ref

```tsx
const viewRef = useRef<HazoCollabFormViewRef>(null);

// Confirm all prefilled fields at once (e.g. "Looks good" button)
const ids = viewRef.current?.confirm_all_prefilled();
// ids: string[] of field_ids that were prefilled and are now confirmed
```

#### ThreadForm Key-Info Drawer (v5.6.0+)

`ThreadForm` exposes a collapsible "Key Information" drawer above the task list that embeds `HazoCollabFormView` in `edit` mode with the same H.7 plumbing. Use it when you want a prefill-review surface co-located with the thread conversation.

```tsx
import { useRef } from 'react';
import { ThreadForm } from 'hazo_collab_forms/thread-form';
import type { ThreadFormRef, FieldMetadataInput } from 'hazo_collab_forms/thread-form';

const form_ref = useRef<ThreadFormRef>(null);
const [field_metadata, set_field_metadata] = useState<Record<string, FieldMetadataInput>>({
  abn: { confirm_state: 'prefilled' },
  business_address: { confirm_state: 'prefilled' },
});

<ThreadForm
  ref={form_ref}
  data={thread_data}
  role="client"
  current_user={current_user}
  // Key-Info drawer
  key_info_sections={key_info_sections}
  key_info_form_data={key_info_form_data}
  on_key_info_form_data_change={set_key_info_form_data}
  field_metadata={field_metadata}
  on_metadata_change={({ field_id, changes }) =>
    set_field_metadata((prev) => ({
      ...prev,
      [field_id]: { ...prev[field_id], ...changes },
    }))
  }
  show_confirm_action
/>

// Bulk-confirm every still-prefilled field:
const confirmed_ids = form_ref.current?.confirm_all_prefilled() ?? [];
```

The drawer:
- Renders nothing when `key_info_sections` is omitted (zero overhead for existing consumers).
- Header summary shows counts like `3 prefilled • 1 edited • 5 confirmed` when collapsed.
- Always uses `mode="edit"`. Visible in both agent and client roles.
- Apply the same `styles/confirm-state.css` import you use for `HazoCollabFormView`; the `[data-field-id]` wrappers inside the drawer carry the `hcf-confirm-*` classes automatically.

**Numbering tasks after the KeyInfoDrawer (`task_number_offset`):**

When the KeyInfoDrawer carries its own Q1 pill in the `key_info_title`, pass `task_number_offset={1}` on `ThreadForm` so the task list starts at Q2:

```tsx
<ThreadForm
  ...
  key_info_title={<><span aria-label="Question 1">Q1</span> Confirm Key Information</>}
  task_number_offset={1}   // tasks render Q2, Q3, Q4 …
/>
```

- `aria-label` values shift with the visible number, so screen-reader output remains consistent.
- "From Q{N}" back-references in Send Back chips shift by the same offset automatically.
- Default is `0` — no change for existing consumers.

**v6.0.3 additions — `KeyInfoDrawer` can also be used standalone** (exported from `hazo_collab_forms/thread-form`):

```tsx
import { KeyInfoDrawer } from 'hazo_collab_forms/thread-form';

<KeyInfoDrawer
  sections={sections}
  form_data={form_data}
  on_form_data_change={set_form_data}
  // v6.0.3 props:
  chevron_position="right"          // 'left' | 'right' (default 'right')
  title={<span>Key Details <Badge /></span>}  // ReactNode, not just string
  aria_label="View key applicant details"     // explicit button aria-label
  disable_section_card                        // strip border + bg from section
  hide_section_header                         // hide the section title bar
/>
```

| Prop | Type | Default | Description |
|---|---|---|---|
| `chevron_position` | `'left' \| 'right'` | `'right'` | Which side the expand/collapse chevron appears on |
| `title` | `React.ReactNode` | `'Key Information'` | Drawer header — can be JSX (badge, icon, etc.) |
| `aria_label` | `string` | title string or `'Key information'` | Explicit `aria-label` for the toggle button |
| `disable_section_card` | `boolean` | `false` | Strips `border rounded-lg bg-card shadow-sm` from each section |
| `hide_section_header` | `boolean` | `false` | Hides the section title bar (h3) inside each section |

`disable_section_card` and `hide_section_header` are forwarded to the embedded `HazoCollabFormView` — see [Section Chrome](#section-chrome-disablesectioncard--hidesectionheader) for the direct form-view API.

### Read-Only Summary View

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

```typescript
'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: "john@example.com",
        tfn: "123456789"
      }}
      render_icon={render_icon}
      show_edit_buttons={true}
      on_edit_section={(index) => router.push(`/edit/${index}`)}
    />
  );
}
```

**With Full Controls:**

```typescript
<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

### Multi-Column Layout (`field_columns`)

Render fields in 2 or 3 columns per row in `edit` mode using the `field_columns` prop. Useful for compact forms where fields are short (e.g. date, currency, short text inputs).

```tsx
<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={formData}
  field_columns={2}  // or 3 for three columns
/>
```

**How it works:**

- `field_columns={1}` (default): each field spans the full row in the standard chat-aligned grid — identical to pre-6.0.1 behaviour.
- `field_columns={2}`: group wrapper switches to `grid-cols-2`; each field takes one column.
- `field_columns={3}`: group wrapper switches to `grid-cols-3`; each field takes one column.

Wide fields automatically opt out and always span the full row (`col-span-full`):
- `HazoCollabFormTextArea`
- `HazoCollabFormDataTable`
- File-upload groups

For any other field that must not share a row, pass `container_class_name="col-span-full"` in `FieldConfig`. You can also use `container_class_name` to force a field to start at a specific column:

```tsx
// Force this field to always start on column 1 (no wrapping mid-row)
{ id: 'address', label: 'Address', component_type: 'HazoCollabFormTextArea',
  container_class_name: '[grid-column-start:1]' }
```

> **Note**: Only `edit` mode is affected. Summary, print, and approval views are unaffected.

**Context hook (advanced use):**

```tsx
import { useFieldColumns } from 'hazo_collab_forms';

function MyCustomField() {
  const columns = useFieldColumns(); // 1 | 2 | 3
  // Adjust rendering based on available column width
}
```

### Section Chrome (`disable_section_card` + `hide_section_header`)

By default, each `EditSection` wraps its content in a bordered card with a section title bar. Two props on `HazoCollabFormView` let you opt out of either element:

```tsx
<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={form_data}
  disable_section_card   // strips border, rounded-lg, bg-card, shadow-sm
  hide_section_header    // hides the section name h3
/>
```

| Variant | `disable_section_card` | `hide_section_header` | Use case |
|---|---|---|---|
| Default | — | — | Standalone form on a white page |
| No card | `true` | — | Embedded inside a panel or dialog that already provides a container |
| No header | — | `true` | Section name shown elsewhere (e.g. drawer title) |
| Both | `true` | `true` | Full embed — just the fields, no wrapper chrome |

`overflow-hidden` and the `cls_edit_section` class are always kept regardless of these settings.

Both props are also available on `KeyInfoDrawer` and forwarded to its embedded view.

### Group Border (`disable_border` + `container_class_name` on groups)

By default, `HazoCollabFormGroup` wraps its field grid in a `border border-input rounded-md p-4` container. Set `disable_border: true` on a group `FieldConfig` to strip that wrapper:

```typescript
const sections = [{
  section_name: "Details",
  groups: [{
    id: "key_info",
    label: "",
    field_type: "group",
    disable_border: true,         // removes border + rounded-md + p-4
    container_class_name: "!p-0", // optional: fine-tune padding further
    fields: [...],
  }],
}];
```

`container_class_name` on a group `FieldConfig` is merged with (or overrides) the default wrapper classes, giving full Tailwind control over the container.

| Prop | Type | Effect |
|---|---|---|
| `disable_border` | `boolean` | Removes `border border-input rounded-md p-4` from the group container |
| `container_class_name` | `string` | Additional/override Tailwind classes on the group container |

Both props already existed on `HazoCollabFormGroup` — this release fixes the forwarding gap that caused them to be silently ignored when set on a declarative group schema.

### 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

```typescript
// 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:

```typescript
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)

```typescript
// 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:**

```typescript
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:**

```typescript
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:**

```typescript
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:**

```typescript
'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.

```typescript
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:**

```sql
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.

```typescript
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:

```typescript
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:

```jsonc
{
  "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`:**

```tsx
<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):

```typescript
{
  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:
```json
{
  "success": true,
  "data": {
    "bank_name": "Commonwealth Bank",
    "amount": "1500.00",
    "withheld": "0"
  }
}
```

Data table fields — return arrays which are appended as rows:
```json
{
  "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):
```json
{
  "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](#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`

```typescript
// 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

```typescript
// 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:**

```typescript
'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:

```typescript
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:**

```typescript
'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 |
| `field_columns` | `1 \| 2 \| 3` | Fields per row in edit mode (default `1`). Wide fields (textarea, data table, file groups) span the full row automatically. Summary/print/approval views are unaffected. |
| `disable_section_card` | `boolean` | Strips `border rounded-lg bg-card shadow-sm` from each section wrapper in edit mode. `overflow-hidden` is kept. Default `false`. |
| `hide_section_header` | `boolean` | Hides the section title bar (icon + `<h3>` name) inside each section in edit mode. Default `false`. |

**Context Hooks:**

Access form state from nested components using hooks:

```typescript
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:**
```json
{
  "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:**
```json
{
  "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

```json
{
  "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:**
```json
{
  "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:**

```tsx
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):**

```tsx
<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):**

```json
{
  "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.

```json
{
  "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):**

```typescript
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).

```typescript
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:

```typescript
<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={formData}
  reference_data={referenceData}
  on_reference_delete={(field_id, index, reference_id) => {
    // field_id: which field the reference belongs to
    // index: position in the array (0 for single entries)
    // reference_id: the entry's id property (if set)
    console.log(`Delete reference ${reference_id} from ${field_id}`);
  }}
/>
```

Without `on_reference_delete`, no trash icons appear (read-only references).

**ReferenceValueData Interface:**

```typescript
interface ReferenceValueData {
  id?: string;                          // Unique ID for deletion tracking
  value: string | number | boolean | Record<string, unknown>[];
  label?: string;                       // e.g., "Prior Year (2023)"
  tax_year?: number;
  background_color?: string;            // Tailwind class (default: "bg-muted")
}

type ReferenceData = Record<string, ReferenceValueData | ReferenceValueData[]>;
```

**Value Formatting:**

| Type | Formatting | Example |
|------|------------|---------|
| Boolean | "Yes" / "No" | `true` → "Yes" |
| Number >= 100 | Currency (AUD) | `82450` → "$82,450.00" |
| Number < 100 | Plain | `15` → "15" |
| String | Pass-through | "text" → "text" |
| Array | Table accordion (HazoDataForm) | `[{name: "Item", amount: 100}]` → collapsible table |

**Table Reference Data (HazoDataForm):**

For table fields, reference data can contain an array of row objects. This renders as a collapsible `TableReferenceAccordion` below the table, showing prior year rows with subtotals:

```typescript
const referenceData: ReferenceData = {
  income_table: {
    value: [
      { source: "Salary", employer: "ACME Corp", amount: 85000 },
      { source: "Dividends", employer: "Vanguard", amount: 3200 },
    ],
    label: "Prior Year (2023)",
    tax_year: 2023,
  }
};
```

The accordion displays a dashed toggle bar with the label, year badge, and row count. When expanded, it renders a read-only table matching the main table's columns with automatic subtotal computation for columns marked `subtotal: true`.

The `TableReferenceAccordion` component is also exported for standalone use:

```tsx
import { TableReferenceAccordion } from 'hazo_collab_forms';

<TableReferenceAccordion
  reference={referenceValueData}
  columns={tableColumns}
  config={dataFormConfig}
/>
```

**Supported Modes:**
- ✅ `mode="edit"` - Edit view
- ✅ `mode="summary"` - Summary view
- ✅ `mode="print"` - Print view
- ✅ `mode="approval"` - Approval view

**Priority:**
Field-level props (`reference_value`, `reference_label`, `reference_tag_background_color`) take precedence over form-level `reference_data`.

### Role-Based Icon Controls (icons_behaviour / GlobalSettings)

Configure which control icons are visible and enabled based on user roles. Supports automatic consolidation into kebab (3-dots) menus and extraction of active controls.

> **Note**: The `IconsBehaviour` type has a new alias `GlobalSettings` that better represents its expanded scope, which now includes role-based permissions (like `control_private_files`) in addition to icon behaviors.

**JSON Configuration Options:**

You can configure icons_behaviour in two ways:

1. **Embedded** (original): Include icons_behaviour directly in the form JSON file
2. **Separate file** (recommended): Store icons_behaviour in a dedicated JSON file for reuse across forms

**Separate File Approach (v1.7.0+):**

```
public/data/form-sets/
  tax_form_individual_full.json  # Sections only
  icon_behaviour.json            # Icons behaviour config (reusable)
```

Load both files and pass icons_behaviour as a prop:

```typescript
// Load form configuration
const sectionsResponse = await fetch('/data/form-sets/tax_form_individual_full.json');
const { sections } = await sectionsResponse.json();

const behaviourResponse = await fetch('/data/form-sets/icon_behaviour.json');
const icons_behaviour = await behaviourResponse.json();

// Pass to HazoCollabFormView (edit mode)
<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={formData}
  icons_behaviour={icons_behaviour}
  active_role="tax_agent"
/>

// Pass to HazoCollabFormView (summary mode)
<HazoCollabFormView
  mode="summary"
  sections={sections}
  form_data={formData}
  icons_behaviour={icons_behaviour}
  active_role="tax_agent"
/>
```

**Prop Priority:**
1. `icons_behaviour` prop (if provided)
2. `section.icons_behaviour` or `sections[0]?.icons_behaviour` (backward compatible)

This separation allows:
- Reusing the same icon behavior across multiple forms
- Changing roles without modifying form structure
- Easier maintenance of large form configurations

**Configuration Structure:**

```typescript
const icons_behaviour: IconsBehaviour = {
  // Data OK display mode
  data_ok_mode: 'multistate', // or 'checkbox'

  // Controls that have borders
  border_on: ['control_data_ok'],

  // Display order (left to right)
  icon_order: [
    'control_data_ok',
    'control_skip',
    'control_notes',
    'control_chat',
    'control_visibility',
    'control_lock',
    'control_delete',
    'control_edit_field',
    'control_edit_group',
    'control_delete_field',
  ],

  // Always shown individually (never in kebab)
  always_extracted: ['control_data_ok'],

  // Extracted when they have activity
  extracted_on_value: ['control_notes', 'control_chat'],

  // Role definitions
  roles: [
    {
      role_id: 'tax_agent',
      role_name: 'Tax Agent',
      role_description: 'Full access to all controls',
      controls: {
        control_data_ok: { visible: true, enabled: true },
        control_notes: { visible: true, enabled: true },
        control_chat: { visible: true, enabled: true },
        control_visibility: { visible: true, enabled: true },
        control_lock: { visible: true, enabled: true },
        control_delete: { visible: true, enabled: false },
        control_edit_field: { visible: true, enabled: true },
        control_edit_group: { visible: true, enabled: true },
        control_delete_field: { visible: true, enabled: true },
        control_private_files: { visible: true, enabled: true }  // Can see private file sections
      }
    },
    {
      role_id: 'client',
      role_name: 'Client',
      role_description: 'Limited to chat and notes',
      controls: {
        control_chat: { visible: true, enabled: true },
        control_notes: { visible: true, enabled: true },
        control_data_ok: { visible: true, enabled: false },
        control_visibility: { visible: false, enabled: false },
        control_lock: { visible: false, enabled: false },
        control_delete: { visible: false, enabled: false },
        control_edit_field: { visible: false, enabled: false },
        control_edit_group: { visible: false, enabled: false },
        control_delete_field: { visible: false, enabled: false },
        control_private_files: { visible: false, enabled: false }  // Cannot see private file sections
      }
    }
  ]
};
```

**Usage in HazoCollabFormView:**

```typescript
// Edit mode
<HazoCollabFormView
  mode="edit"
  sections={sections}
  form_data={formData}
  icons_behaviour={icons_behaviour}
  active_role="tax_agent"
/>

// Summary mode
<HazoCollabFormView
  mode="summary"
  sections={sections}
  form_data={formData}
  icons_behaviour={icons_behaviour}
  active_role="client"
/>
```

**Hidden Field Filtering by Role:**

Roles without `control_visibility: { visible: true }` automatically have hidden fields, empty groups, and empty sections filtered out across all view modes (edit, summary, print, approval). This works the same as the existing responder filtering but extends to any role that lacks visibility toggle capability.

```typescript
// A "client" role with control_visibility: { visible: false }
// will NOT see fields with visibility: 'hidden'
// Empty groups and sections are also hidden automatically
```

**Consolidation Rules (Automatic):**

When 2+ non-extracted controls are visible, they consolidate into a kebab menu:

1. **Data OK, Skip, Edit Field, Edit Group**: Always inline (hardcoded, never in kebab)
2. **Controls in always_extracted**: Always inline
3. **Controls in extracted_on_value with activity**: Extracted from kebab (shown inline)
   - `control_notes`: Extracted when notes exist
   - `control_chat`: Extracted when messages exist
4. **Remaining visible controls**: Kebab menu if 2+, otherwise inline

**Icon Categories:**

Icons are split into two categories with independent overrides:

| Category | Controls | Override Prop |
|----------|----------|---------------|
| **Edit Icons** | `control_edit_field`, `control_edit_group`, `control_delete_field` | `show_edit_icons` |
| **Control Icons** | `control_data_ok`, `control_skip`, `control_notes`, `control_chat`, `control_lock`, `control_visibility`, `control_delete`, `control_private_files` | `show_control_icons` |
| **Standalone** | `control_add_elements`, `control_edit_elements` | None (always role-driven) |

Groups use `control_edit_group` (not `control_edit_field`) for their edit pencil icon, allowing roles to independently control group vs field editing.

**Field/Group Level Override:**

Icons show automatically based on role config. Use `show_edit_icons: false` or `show_control_icons: false` to force-hide on specific fields/groups:

```typescript
const fields: FieldConfig[] = [
  {
    id: "sensitive_group",
    field_type: "group",
    label: "Sensitive Information",
    // Icons show automatically from role config - no show_icons needed
    // Use overrides only to force-hide:
    // show_edit_icons: false,    // Hide edit/delete icons on this group
    // show_control_icons: false, // Hide data_ok/notes/chat/etc on this group
    sub_fields: [
      {
        id: "ssn",
        label: "Social Security Number",
        component_type: "HazoCollabFormInputbox",
        value: ""
      }
    ]
  }
];
```

> **Migration from `show_icons`:** The `show_icons` prop is deprecated. Remove all `show_icons: true` from your form configs — icons now show automatically from role config. If you used `show_icons: false` to hide icons, replace with `show_edit_icons: false` and/or `show_control_icons: false`.

**Role-Level Permissions:**

Private file access is controlled via the `control_private_files` control in the role's controls object:

```typescript
{
  role_id: 'tax_agent',
  role_name: 'Tax Agent',
  controls: {
    control_private_files: { visible: true, enabled: true },  // Shows private file sections
    // ... other controls
  }
}
```

When `control_private_files.visible` is `true`, users with that role will see private file upload sections on fields that have `accept_private_files: 'basic_upload'`. This is automatically resolved from the `active_role` prop.

**Benefits:**
- Single configuration for entire form
- Consistent behavior across form and summary views
- Automatic UI optimization (kebab consolidation)
- Easy role switching without code changes
- Role-based permissions (private files, etc.)
- Visual editor in Template Generator

### Dynamic Field Dialogs

Add, edit, and manage form fields at runtime using dialog components.

**HazoAddFieldDialog:**

Add new fields or groups to your form dynamically:

```typescript
import { HazoAddFieldDialog, type NewFieldDefinition } from 'hazo_collab_forms';
import { useState } from 'react';

export default function DynamicForm() {
  const [isOpen, setIsOpen] = useState(false);

  const handleAddField = (field: NewFieldDefinition) => {
    console.log('New field:', field);
    // Add field to your form sections
  };

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Add Field</button>
      <HazoAddFieldDialog
        open={isOpen}
        on_open_change={setIsOpen}
        on_add={handleAddField}
        parent_type="group" // 'section' | 'group'
        parent_id="personal_info"
      />
    </>
  );
}
```

**HazoEditFieldDialog:**

Edit existing field configurations:

```typescript
import { HazoEditFieldDialog } from 'hazo_collab_forms';

<HazoEditFieldDialog
  open={isEditOpen}
  on_open_change={setIsEditOpen}
  field={selectedField}
  on_save={(updated) => updateField(updated)}
  on_delete={() => deleteField(selectedField.id)}  // Optional delete callback
/>
```

**HazoFieldSelectorDialog:**

Search and select fields from a template:

```typescript
import { HazoFieldSelectorDialog } from 'hazo_collab_forms';

<HazoFieldSelectorDialog
  open={isSelectorOpen}
  on_open_change={setIsSelectorOpen}
  template={formTemplate}
  on_select={(selection) => {
    // Handle selected fields
    console.log('Selected:', selection);
  }}
/>
```

### Shared Utilities

The package exports utility functions for working with form structures:

```typescript
import {
  // Field traversal
  traverse_fields,
  traverse_sections,
  collect_field_ids,
  collect_leaf_field_ids,
  should_render_field,
  is_field_empty,
  get_field_value,

  // Dependency checking
  parse_dependency,
  check_dependency,

  // Field metadata normalization
  normalize_field_metadata,
  normalize_field_metadata_input,

  // File management
  get_field_attachments,
  get_field_private_attachments,
  is_form_file_attachment,
  attachment_to_file_data,
  file_data_to_attachment,
} from 'hazo_collab_forms';

// Traverse all fields in sections
traverse_sections(sections, (field, path) => {
  console.log(`Field: ${field.id} at path: ${path.join('/')}`);
});

// Check if field should render based on dependencies
const shouldRender = should_render_field(field, formData, {
  check_dependencies: true,
  show_empty_fields: false,
});

// Parse and check field dependencies
const dependency = parse_dependency(field.depends_on);
const isActive = check_dependency(dependency, formData);

// Get file attachments for a field
const publicFiles = get_field_attachments(formData, 'document_field');
const privateFiles = get_field_private_attachments(formData, 'sensitive_field');
```

## Components

### Form Field Components

| Component | Description | Required shadcn |
|-----------|-------------|-----------------|
| `HazoCollabFormInputbox` | Text input with validation | button, label, dialog, tooltip |
| `HazoCollabFormTextArea` | Multi-line text input | button, label, dialog, tooltip |
| `HazoCollabFormCheckbox` | Boolean toggle | button, label, dialog, tooltip |
| `HazoCollabFormCombo` | Dropdown select with search | + popover, command |
| `HazoCollabFormRadio` | Radio button group | button, label, dialog, tooltip |
| `HazoCollabFormDate` | Date or date-range picker | + calendar |
| `HazoCollabFormDoc` | Document/file-only field | button, label, dialog, tooltip, accordion |
| `HazoCollabFormGroup` | Field grouping container | button, label, dialog, tooltip |
| `HazoCollabFormDataTable` | Editable data table with aggregations | button, label, dialog, tooltip |
| `HazoCollabFormCurrency` | Currency input with locale formatting | button, label, dialog, tooltip |
| `HazoCollabFormPercentage` | Percentage input | button, label, dialog, tooltip |
| `HazoCollabFormComputed` | Read-only computed/formula field | button, label, dialog, tooltip |
| `HazoCollabFormStaticText` | Static display text | button, label, dialog, tooltip |
| `HazoCollabFormSummaryRow` | Summary/total row display | button, label, dialog, tooltip |
| `HazoCollabFormMasked` | Masked input (phone, ABN, etc.) | button, label, dialog, tooltip |

### Form Container Components

| Component | Description | Required shadcn |
|-----------|-------------|-----------------|
| `HazoCollabFormView` | **Recommended** - Unified multi-mode view (Edit/Summary/Print/Approval) | all components |
| `HazoCollabFormSet` | ⚠️ *Deprecated* - Use `HazoCollabFormView` with `mode="edit"` | all components |
| `HazoDataForm` | Structured data form with style variants, computed fields, file management, and sidebar PDF panel | all components |
| `HazoCollabFormFileTextbox` | Rich file+text field with validation status and content tagging | button, label, dialog, tooltip, accordion |

### RFI Box & Clarification Components

| Component | Description | Required shadcn |
|-----------|-------------|-----------------|
| `HazoCollabFormRFIbox` | Interactive review/send-back container with clarification management | all components |
| `RfiReviewActions` | Review decision buttons and action controls | button, dialog |
| `RfiSendBackForm` | Send-back workflow with collapsible review items | accordion |
| `RfiOptionTagSelector` | Interactive tag picker for response options in send-back items | popover, command |

### Builder & Library Components

| Component | Description | Required shadcn |
|-----------|-------------|-----------------|
| `HazoFieldLibrary` | **New** - Database-backed field/group library manager | button, label, dialog, tooltip, tabs, popover, command |
| `HazoTemplateGenerator` | ⚠️ *Deprecated* - Visual form builder | button, label, dialog, tooltip, tabs, resizable |
| `HazoAddFieldDialog` | Add new fields dynamically (with library search) | dialog, popover, command |
| `HazoAddGroupDialog` | Add new groups dynamically | dialog |
| `HazoEditFieldDialog` | Edit field configurations | dialog, popover, command |
| `HazoFieldSelectorDialog` | Search and select fields | dialog, command |

### Optional Features

| Feature | Required shadcn |
|---------|-----------------|
| File Upload (`accept_public_files` / `accept_private_files` props) | + **accordion** (required) |
| AI Autofill (`accept_public_files: 'ai_upload'` on group) | requires `file_manager` + API endpoint |
| Field Tooltips (`tooltip` prop) | + **hover-card** (optional) |

## Import Paths

```typescript
// Default: All client-safe components and utilities
import { HazoCollabFormInputbox, cn } from 'hazo_collab_forms';

// Components only
import { HazoCollabFormInputbox } from 'hazo_collab_forms/components';

// Utilities only
import { cn, use_collab_chat } from 'hazo_collab_forms/utils';

// Server-only (config functions)
import { get_config } from 'hazo_collab_forms/lib';
```

## Troubleshooting

### lucide-react Version Conflicts

Different Hazo packages require different versions of `lucide-react`. Add this to your `package.json`:

```json
"overrides": {
  "lucide-react": "^0.553.0"
}
```

### Missing shadcn Components

If you see errors about missing components, install the specific shadcn component:

```bash
npx shadcn@latest add [component-name]
```

**Important**: If you're using file upload functionality (`accept_public_files` or `accept_private_files` props on any form field), you **must** install the accordion component:

```bash
npx shadcn@latest add accordion
```

### Verify Your Setup

Run the verification tool to check for common issues:

```bash
npx hazo-collab-forms-verify
```

See [SETUP_CHECKLIST.md](./SETUP_CHECKLIST.md) for detailed troubleshooting.

---

## Document Clarifications

Clarifications provide a full lifecycle for document validation issues: flag problems on specific fields, reference relevant documents, collect structured responses from clients, and track resolution status.

### Enabling Clarifications

```typescript
import {
  HazoCollabFormView,
  use_clarifications,
  type ClarificationCallbacks,
  type ClarificationItem,
} from 'hazo_collab_forms';

export default function FormWithClarifications() {
  const [clarifications, setClarifications] = useState<ClarificationItem[]>([]);

  const callbacks: ClarificationCallbacks = {
    on_respond: (id, response) => { /* Save response to backend */ },
    on_resolve: (id) => { /* Mark clarification resolved */ },
    on_dismiss: (id) => { /* Mark clarification dismissed */ },
    on_reopen: (id) => { /* Reopen a resolved/dismissed clarification */ },
  };

  return (
    <HazoCollabFormView
      mode="edit"
      sections={sections}
      form_data={formData}
      enable_clarifications={true}
      clarification_callbacks={callbacks}
    />
  );
}
```

### Clarification Components

| Component | Description |
|-----------|-------------|
| `ClarificationCard` | Full card displaying issue details, doc references, and response form |
| `ClarificationSection` | Container for multiple clarification cards per field/group |
| `ClarificationBadge` | Count badge showing pending/responded clarifications |
| `ClarificationLabelBadge` | Inline badge for field labels |
| `ClarificationDialog` | Modal dialog for viewing and responding to clarifications |
| `ClarificationDocReferenceList` | List of referenced documents with download links |
| `ClarificationResponseForm` | Structured response form with radio options and file upload |

### Clarification Types

| Type | Description |
|------|-------------|
| `none` | No clarification needed |
| `invalid_document` | Document does not match expected type/content |
| `additional_info_required` | More information needed from client |

### Clarification Lifecycle

```
pending → responded → resolved
                   ↘ dismissed
resolved/dismissed → pending (via reopen)
```

### Hooks

```typescript
import { use_clarifications, get_clarifications, CLARIFICATIONS_KEY } from 'hazo_collab_forms';

// Hook for managing clarifications within form data
const { clarifications, counts, add, update, remove } = use_clarifications(form_data);

// Utility to extract clarifications from form_data
const items = get_clarifications(form_data); // reads form_data[CLARIFICATIONS_KEY]
```

---

## Validation Rules

LLM-powered document validation with rule management, prompt editing, and automatic clarification generation.

### Validation Rule Editor

```typescript
import { HazoValidationRuleEditor, parse_rule_actions_config } from 'hazo_collab_forms';
import type { RuleActionsConfig } from 'hazo_collab_forms';

// Load config from your JSON file (actions and variables)
const ruleActionsConfig: RuleActionsConfig = await fetch('/data/rule_actions.json').then(r => r.json());
const promptConfig = parse_rule_actions_config(ruleActionsConfig);

<HazoValidationRuleEditor
  scope_id="client_123"
  api_base_url="/api/hazo_connect/sqlite"
  table_name="hazo_validation_rules"
  prompt_config={promptConfig}
/>
```

#### Rule Actions Config File

The editor's prompt commands (`/`) and variables (`@`) are driven by a JSON config file:

```json
{
  "actions": [
    {
      "action": "Return_To_Tax_Client",
      "action_label": "Return To Tax Client",
      "action_description": "Return the question and document to the client to clarify",
      "technical_description": "Triggers workflow to send item back to client",
      "source_data": "N/A - action trigger"
    }
  ],
  "variables": [
    {
      "variable": "Beginning_Of_Tax_Year",
      "variable_label": "Beginning Of Tax Year",
      "variable_description": "The start date of the Australian financial year",
      "technical_description": "Computed as 1st July of (tax_year - 1)",
      "source_data": "app_client_folders.tax_year",
      "format": "YYYY-MM-DD"
    }
  ]
}
```

Use `parse_rule_actions_config()` to convert this into the `PromptEditorConfig` expected by the editor. Actions become `/Action_Name` slash commands, variables become `@Variable_Name` references.

The editor UI has three tabs: **Rule** (name, description, document type, prompt), **Clarification** (auto-generated clarification settings with drag-and-drop reordering of response options), and **Technical** (rule ID, priority, enabled toggle).

The sidebar header provides **Export** and **Import** buttons for bulk rule management:
- **Export** (↓): downloads all loaded rules as a portable JSON file — `id`, `created_at`, `changed_at`, and `scope_id` are stripped so the file works across environments.
- **Import** (↑): accepts a JSON array of rule objects exported from any instance. Rules missing `name` or `document_type` are skipped; `rule_id` conflicts are resolved automatically. Hidden in `read_only` mode.

### Using Validation Rules with File Upload

Assign validation rules to fields or groups, then validate uploaded documents automatically:

```typescript
// In your form section JSON
{
  "id": "payg_summary",
  "label": "PAYG Payment Summary",
  "field_type": "group",
  "accept_public_files": "basic_upload",
  "validation_rules": ["check_payg_coverage", "verify_employer_abn"]
}
```

### Client-Side Validation Hook

```typescript
import { use_file_validation, use_validation_rules, build_rule_field_map } from 'hazo_collab_forms';

// Fetch rules from database
const { rules } = use_validation_rules({
  api_base_url: '/api/hazo_connect/sqlite',
  table_name: 'hazo_validation_rules',
  scope_id: 'client_123',
});

// Build reverse mapping from rule IDs to field IDs
const rule_field_map = build_rule_field_map(sections);

// Hook for triggering validation
const { validate_file, is_validating } = use_file_validation({
  enabled: true,
  api_url: '/api/validation',
  rules,
  form_data: formData,
  rule_field_map,
  on_clarifications_created: (items) => { /* Merge into form data */ },
});

// Trigger validation after file upload
const clarifications = await validate_file(file_b64, file_name, mime_type, content_tag);
```

### Server-Side Validation Route

```typescript
// app/api/validation/route.ts
import { create_validation_route } from 'hazo_collab_forms/lib';
import { getHazoConnect } from '@/lib/hazo_connect_setup';

// Preferred: pass hazo_connect adapter (works with SQLite, PostgREST, Supabase)
export const POST = create_validation_route({ hazo_connect_adapter: getHazoConnect() });

// Legacy: pass SQLite path directly (deprecated)
// export const POST = create_validation_route({ sqlite_path: './data/prompts.sqlite' });
```

### Validation Rule Types

```typescript
interface ValidationRule {
  id: string;                      // UUID primary key (auto-generated by DB)
  rule_id: string;                 // Human-friendly identifier (unique, used in field references)
  name: string;
  description?: string;
  document_type: string;           // e.g., "payg_summary", "bank_statement"
  prompt: string;                  // LLM prompt with {{variable}} placeholders
  clarification_type: ClarificationType;
  enabled: boolean;
  priority?: number;               // Lower = earlier (default: 100)
  custom_issue_description?: string;
  custom_action_description?: string;
  custom_response_options?: ClarificationResponseOption[];
}
```

### Database Schema Migration (v2.0.2+)

If upgrading from v2.0.0/v2.0.1, the `hazo_collab_form_validations` table now requires a `rule_id` column:

```sql
ALTER TABLE hazo_collab_form_validations ADD COLUMN rule_id TEXT NOT NULL DEFAULT '';
CREATE UNIQUE INDEX IF NOT EXISTS idx_validations_rule_id ON hazo_collab_form_validations(rule_id);
-- Populate rule_id from existing id values:
UPDATE hazo_collab_form_validations SET rule_id = id WHERE rule_id = '';
```

For new installations, the updated `init-db.sh` script creates the table with `rule_id` and auto-generated UUID `id` columns.

### File Validation Badges

When `file_manager` includes validation state, files show visual indicators:

```typescript
const fileManager: FileManagerConfig = {
  callbacks: fileCallbacks,
  validating_file_ids: new Set(['file_1']),    // Shows spinner overlay
  passed_file_ids: new Set(['file_2']),         // Shows green checkmark
  error_file_ids: new Set(['file_3']),          // Shows error badge
  error_file_details: { file_3: ['Missing ABN', 'Incorrect period'] },
};
```

---

## Content Tagging

LLM-based document classification at upload time. When enabled, uploaded files are automatically classified (e.g., "bank_statement", "payg_summary") which is used to match validation rules.

```typescript
import type { ContentTagConfig } from 'hazo_collab_forms';

const contentTagConfig: ContentTagConfig = {
  content_tag_set_by_llm: true,
  content_tag_prompt_area: 'tax_docs',
  content_tag_prompt_key: 'classify_document',
  content_tag_prompt_return_fieldname: 'document_type',
};

const fileManager: FileManagerConfig = {
  callbacks: {
    ...fileCallbacks,
    get_content_tag: async (file_id) => {
      const res = await fetch(`/api/files/${file_id}/content-tag`);
      const data = await res.json();
      return data.content_tag;
    },
  },
  content_tag_config: contentTagConfig,
};
```

**Requires**: `hazo_llm_api` optional peer dependency for LLM integration.

---

## Inline extracted fields (Collected Data tab)

`<ThreadForm>` and `<CollectedDataView>` can display each file's
`classification_result.extracted_fields` inline below its row in the
Collected Data tab. As of v5.2.0 the display supports scalar fields,
**repeating-row tables** (declared via `array_of`), and bullet lists
for scalar arrays — plus a per-row accordion, an info-icon dialog,
and an agent-only re-extract action.

```tsx
<ThreadForm
  data={data}
  role="agent"
  current_user={user}
  show_extracted_fields
  collected_data_locale="en-AU"
  collected_data_currency_code="AUD"
  on_request_reextraction={async (content_id, scope) => {
    // 'classification' = re-run classify only; 'full' = + back-office
    await my_api.reclassify(content_id, scope);
  }}
  available_document_types={[
    {
      type_id: 'bank_statement',
      type_label: 'Bank Statement',
      expected_fields: [
        { key: 'bank', type: 'string', description: 'Issuing bank' },
        { key: 'interest_earned', type: 'currency', description: 'Total interest' },
      ],
    },
    {
      type_id: 'rental_statement',
      type_label: 'Rental Statement',
      expected_fields: [
        { key: 'vendor', type: 'string', label: 'Vendor' },
        { key: 'property_address', type: 'string', label: 'Property address' },
        { key: 'total', type: 'currency', label: 'Total' },
        {
          // Object-array shape → renders as a table with one column per shape entry
          key: 'line_items',
          label: 'Line items',
          array_of: {
            type: 'object',
            shape: [
              { key: 'description', type: 'string', label: 'Description' },
              { key: 'type', type: 'string', label: 'Type' },
              { key: 'amount', type: 'currency', label: 'Amount' },
            ],
          },
        },
      ],
    },
  ]}
/>
```

### Render decision tree (per `extracted_fields` key)

| Field declaration | Value shape | Renders as |
|---|---|---|
| `array_of: { type: 'object', shape }` | `Array<object>` | **Table** with declared columns |
| `array_of: { type: 'object', shape }` | empty array `[]` | Dropped (entry hidden) |
| `array_of: { type: 'object', shape }` | non-array (string, number, single object) | JSON-string scalar (strict mismatch — surface the bug) |
| Plain `type: 'string' \| 'number' \| 'currency' \| 'date' \| 'boolean'` | `Array<object>` | **Table** with inferred columns (lenient — uses declared label) |
| Plain `type` | `Array<scalar>` | **Bullet list** (lenient) |
| Plain `type` | scalar | Inline dot-list entry |
| Undeclared (LLM-emitted extra) | `Array<object>` | **Table** with inferred columns |
| Undeclared | `Array<scalar>` | **Bullet list** |
| Undeclared | scalar | Inline dot-list entry |

Strict-for-declared-`array_of` semantics surface schema mismatches as
readable JSON so they're noticed; everything else degrades gracefully
into the most useful display.

### Per-row accordion + Expand/Collapse all

Each file row's extracted-fields block is collapsed by default. A
chevron between the checkbox and the file icon toggles that row.
The header bar above the categories shows a single button that
flips between **Expand all** and **Collapse all** based on current
state. No prop required — it's on by default.

### Keep selections — `on_selection_change` (v5.3.0+)

Each file row has a Keep checkbox; each multi-file message has a
"select all / unselect all" link. Toggling either drives which files
flow into the export. As of v5.3.0, ThreadForm exposes a dedicated
state setter for these toggles so the UI updates regardless of how
`on_form_change` is wired:

```tsx
import { use_thread_form, ThreadForm } from 'hazo_collab_forms/thread-form';

const { data, update_selections, /* …other setters… */ } = use_thread_form({
  initial_data,
  on_form_change: debounced_save_to_backend, // debounced full-state sink
});

<ThreadForm
  data={data}
  on_selection_change={update_selections}     // ← state setter (delta merge)
  on_form_change={debounced_save_to_backend}  // optional, fires alongside
  // …other setters…
/>
```

`on_selection_change` receives a **delta** — only the keys that
changed, chain-expanded so toggling either end of a replacement
chain syncs both ends. The consumer merges it into
`data.collected_data_selections`. This is exactly what
`use_thread_form.update_selections` does, so wiring the two together
is the canonical setup.

> **Heads up:** `on_form_change` is a debounced full-state persistence
> sink, not a state setter. Wiring it as the only state path silently
> drops UI updates from controls that have no per-event setter.
> Always pair it with the per-event setters listed in `ThreadFormProps`
> (or use `use_thread_form`).

### File info dialog (agent-only)

Each agent-side file row in the Collected Data tab has an `(i)`
icon next to "View" that opens `FileInfoDialog` showing the
classification result, validation rule outcomes, and debug JSON.
Same component as the one on `FileBar`. Hidden when the row has
no classification AND no validation results.

### Re-extract action (agent-only)

When `on_request_reextraction` is provided AND `role === 'agent'`,
`ExtractedFieldsInline` renders a refresh icon on the meta line plus
a kebab popover with two scopes:

- **Re-extract fields** (or click the icon): scope `'classification'`
  — re-run the classify-document LLM call.
- **Re-run full analysis**: scope `'full'` — consumer-defined; typically
  re-classify and re-run back-office validation.

While the callback is in flight, the content dims (`opacity-50
pointer-events-none`) and the icon spins. The buttons stay clickable
(visibly disabled). Concurrent re-extracts on different files are fine;
re-clicking the same row is a no-op until the callback resolves.

The consumer is expected to call `on_update_content_item(content_id,
{ classification_result })` once their API returns; the renderer
re-renders automatically.

### Custom layout escape hatch

For fully custom layouts use `render_collected_item_details` (takes
precedence over `show_extracted_fields`):

```tsx
<ThreadForm
  // ...
  render_collected_item_details={item => <MyCustomCard item={item} />}
/>
```

### Prompt variable: `{{extracted_fields_instruction}}`

`create_classification_route` resolves `{{extracted_fields_instruction}}`
to one of three strings depending on what's declared in
`available_document_types`:

- **No doctype declares `expected_fields`** → generic fallback
  (`"Return any fields you would consider salient..."`).
- **At least one doctype declares `expected_fields`** → targeted
  instruction (`"Return the listed expected_fields for the chosen
  doctype above..."`).
- **At least one doctype declares `array_of`** → targeted instruction
  PLUS array clause (`"...For fields marked 'array of objects', return
  a JSON array where each element is an object with the listed keys."`).

The doctype block in the prompt also auto-emits a nested shape
sub-block for `array_of` fields so the LLM knows the expected JSON
structure.

Existing prompts that don't reference `{{extracted_fields_instruction}}`
keep working unchanged.

---

## Server-Side Route Handlers

Pre-built route handler factories for common API patterns. All handlers support `hazo_connect_adapter` for backend-agnostic database access (SQLite, PostgREST, Supabase).

### Data Route (NEW in v2.0.3)

Backend-agnostic CRUD route factory. Replaces boilerplate REST endpoints (~200 lines) with a single function call.

```typescript
// app/api/hazo_connect/sqlite/data/route.ts
import { create_data_route } from 'hazo_collab_forms/lib';
import { getHazoConnect } from '@/lib/hazo_connect_setup';

export const dynamic = 'force-dynamic';
export const { GET, POST, PATCH, DELETE } = create_data_route(getHazoConnect());
```

**With table whitelist** (for security):

```typescript
export const { GET, POST, PATCH, DELETE } = create_data_route(getHazoConnect(), {
  allowed_tables: ['hazo_collab_form_validations', 'hazo_collab_form_elts'],
});
```

**Request format** (matches existing client hooks):
- `GET ?table=X&filter[col]=val&orderBy=col&orderDirection=asc` → `{ data, total }`
- `POST { table, data }` → `{ data }`
- `PATCH { table, criteria, data }` → `{ data, updated }`
- `DELETE { table, criteria }` → `{ data, deleted }`

**Requirements**: `hazo_connect` must be installed (`npm install hazo_connect`).

### Autofill Route

Ready-made POST handler for LLM-based document extraction. Wraps `hazo_llm_api` so consumers can set up autofill in ~3 lines.

```typescript
// app/api/autofill/route.ts
import { create_autofill_route } from 'hazo_collab_forms/lib';
import { getHazoConnect } from '@/lib/hazo_connect_setup';

// Preferred: pass adapter (works with any hazo_connect backend)
export const POST = create_autofill_route({ hazo_connect_adapter: getHazoConnect() });

// Legacy: pass SQLite path directly (deprecated)
// export const POST = create_autofill_route({ sqlite_path: './data/prompts.sqlite' });
```

**How it works:**

1. Receives `AutofillRequest` from the `AutofillDropzone` component
2. Validates that `prompt_area`, `prompt_key`, and `fields` are present
3. Lazily initializes `hazo_llm_api` (dynamic import — only loaded at runtime)
4. Calls `hazo_llm_dynamic_data_extract` with the file + prompt context + field schemas
5. Falls back to a direct LLM call with a generated prompt if dynamic extract fails
6. Parses the LLM response as JSON and returns `AutofillResponse`

**Options:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `hazo_connect_adapter` | HazoConnectAdapterLike | - | Adapter from hazo_connect (preferred) |
| `sqlite_path` | string | `'data/prompt_library.sqlite'` | Path to SQLite DB (deprecated) |
| `on_error` | function | `console.error` | Error handler `(error, context) => void` |

**Requirements:**
- `hazo_llm_api` must be installed as a dependency (`npm install hazo_llm_api`)
- The database should contain prompts keyed by `(area, key)` with a `prompt_text_full` field
- The prompt can include `{{field_list}}` which is auto-substituted with formatted field schemas
- Supports image files (JPEG, PNG, etc.) and PDF documents

**Prompt Lookup Fallback Chain:**
1. `hazo_llm_dynamic_data_extract` (prompt DB + multi-step extraction)
2. Direct LLM call with prompt from DB (`get_by_area_key(prompt_area, prompt_key)`)
3. Auto-generated extraction prompt from field schemas (no DB needed)

This means the autofill route works even without a prompt database — it will generate a reasonable extraction prompt from the field schemas automatically.

### Validation Route

```typescript
// app/api/validation/route.ts
import { create_validation_route } from 'hazo_collab_forms/lib';
import { getHazoConnect } from '@/lib/hazo_connect_setup';

// Preferred: pass adapter
export const POST = create_validation_route({ hazo_connect_adapter: getHazoConnect() });

// Legacy: pass SQLite path directly (deprecated)
// export const POST = create_validation_route({ sqlite_path: './data/prompts.sqlite' });
```

**Options:**

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `hazo_connect_adapter` | HazoConnectAdapterLike | - | Adapter from hazo_connect (preferred) |
| `sqlite_path` | string | from INI config or `'data/prompt_library.sqlite'` | Path to SQLite DB (deprecated) |
| `on_error` | function | `console.error` | Error handler `(error, context) => void` |

**Note**: Server-side imports use the `hazo_collab_forms/lib` entry point which includes Node.js-only utilities.

---

## Collected Data Export (Excel / CSV / Google Sheets)

`<ThreadForm>` and `<CollectedDataView>` expose an export menu in the Collected Data
tab header. Set `enable_export` to show it. Each document category becomes one worksheet;
columns are `File Name` + the union of that category's `extracted_fields` (ordered and
labelled by `available_document_types[].expected_fields`).

```tsx
<ThreadForm
  data={data}
  role="agent"
  current_user={user}
  enable_export
  export_filename="collected-data"           // default 'collected-data'
  google_client_id={process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID}  // omit to hide Sheets item
  available_document_types={[...]}
  // ... other props
/>
```

### File Name hyperlinks (v6.4.0+)

Pass `export_file_links` and `get_external_links` to make the **File Name** cell in
Excel and Google Sheets a clickable hyperlink. CSV always outputs plain text (no hyperlink
concept).

```tsx
<ThreadForm
  enable_export
  export_file_links
  get_external_links={async (file_ids) => {
    // Call your server route that runs hazo_files external_file_link:
    const res = await fetch('/api/files/external-link', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ file_ids }),
    });
    const { links } = await res.json();
    return links; // Record<string, { url: string } | null>
  }}
/>
```

**Server route** (calls `hazo_files/server`):

```ts
// app/api/files/external-link/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { external_file_links } from 'hazo_files/server';

export async function POST(request: NextRequest) {
  const { file_ids } = await request.json();
  const resolved = await external_file_links(
    file_ids,
    { metadataService: server.metadataService },
    { serve_base: request.nextUrl.origin, serve_path: '/api/files/serve' },
  );
  const links = Object.fromEntries(
    Object.entries(resolved).map(([id, r]) => [id, 'url' in r ? { url: r.url } : null])
  );
  return NextResponse.json({ links });
}
```

**Behaviour:**
- Only absolute `http(s)` URLs are embedded (blob:, data:, relative paths, javascript: are silently dropped).
- If `get_external_links` throws, a toast is shown and the export proceeds without links.
- Files absent from the resolved map fall back to plain text silently.
- Google Drive files use the stored `webViewLink` (no app route needed).

## Development

### Package Structure

- **Root**: ES module npm package
- **test-app**: Next.js application for testing

### Commands

```bash
npm run build          # Build the package
npm run dev:package    # Watch mode for development
npm run dev:test-app   # Build and run test app
npm run build:test-app # Build for production
npm run clean          # Remove dist directory
```

### TypeScript Configuration

- `tsconfig.json`: Development (bundler module resolution)
- `tsconfig.build.json`: Build (Node16 for ES module output)

### ES Module Exports

All exports use explicit `.js` extensions as required for ES modules:

```typescript
export * from './lib/index.js';
export * from './components/index.js';
```

## License

MIT
