# @husar.ai/render

React component renderer for Husar CMS content with TailwindCSS UI support.

## Package Overview

- **Name:** `@husar.ai/render`
- **Version:** 0.4.1
- **Peer Dependency:** `react >= 18.2.0`
- **Type:** ES Module

## Installation

```bash
npm install @husar.ai/render
# or
pnpm add @husar.ai/render
```

## Core Exports

```typescript
// TailwindPlay rendering system
export { TailwindPlayOuter, TailwindPlay, ListField, FieldSingle } from '@husar.ai/render';

// Field components (SSR-compatible)
export {
  FieldBoolean,
  FieldContent,
  FieldDate,
  FieldFile,
  FieldLocalImage,
  FieldNumber,
  FieldRelation,
  FieldS3Image,
  FieldS3Video,
  FieldSelect,
  FieldString,
  FieldTitle,
} from '@husar.ai/render';

// Component registry
export { HusarComponents } from '@husar.ai/render';

// React hook
export { useHusar, noNulls } from '@husar.ai/render';

// State utilities
export { flattenState, unflattenState } from '@husar.ai/render';

// Type utilities
export type {
  TailwindPlayCommonParams,
  FlattenedKeyType,
  ComponentsOverride,
  AttributesOverride,
  RemoveArrays,
  RemoveNullKeys,
} from '@husar.ai/render';
```

---

## TailwindPlay Rendering System

The TailwindPlay system renders CMS fields to React components with TailwindCSS styling.

### TailwindPlayOuter

Full-page wrapper with background styling:

```tsx
import { TailwindPlayOuter } from '@husar.ai/render';

<TailwindPlayOuter
  fields={cmsFields}
  modelData={pageData}
  mainBackground="#f0f0f0"
  containerBackground="white"
  state={formState}
  setState={setFormState}
/>;
```

### TailwindPlay

Core renderer for CMS fields:

```tsx
import { TailwindPlay } from '@husar.ai/render';

<TailwindPlay
  fields={cmsFields}
  modelData={pageData}
  state={formState}
  setState={setFormState}
  components={componentOverrides}
  attributes={attributeOverrides}
  onSubmit={handleSubmit}
/>;
```

### Props Interface

```typescript
interface TailwindPlayCommonParams {
  // Field configuration
  prefix?: string; // Path prefix for nested fields
  dataPrefix?: string; // Data path prefix
  index?: number; // Array index for list fields
  f: FieldFrontType; // Field definition from CMS
  modelData?: Record<string, any>; // CMS content data
  rootFields?: FieldFrontType[]; // Root fields for path resolution

  // State management
  state?: Record<string, any>; // Form state (flattened)
  setState?: (state: Record<string, any>) => void;
  onSubmit?: (state: Record<string, any> | undefined, prefix: string) => Promise<void>;

  // Customization
  components?: ComponentsOverride; // Component overrides by path
  attributes?: AttributesOverride; // Attribute overrides by path
}
```

### ListField

Renders array/list fields:

```tsx
import { ListField } from '@husar.ai/render';

// Automatically iterates over array data
<ListField f={listFieldDefinition} modelData={data} prefix="items" />;
```

### FieldSingle

Renders individual fields with override support:

```tsx
import { FieldSingle } from '@husar.ai/render';

<FieldSingle
  f={fieldDefinition}
  modelData={data}
  components={{
    'hero.title': ({ data, attrs, component }) => <h1 className="custom-class">{data}</h1>,
  }}
/>;
```

---

## Field Components

### SSR-Compatible Fields

These fields render static content and work with Server-Side Rendering:

| Component         | CMS Type  | Description                                 |
| ----------------- | --------- | ------------------------------------------- |
| `FieldString`     | STRING    | Text content rendered to HTML element       |
| `FieldContent`    | CONTENT   | Rich HTML content (dangerouslySetInnerHTML) |
| `FieldNumber`     | NUMBER    | Numeric values                              |
| `FieldBoolean`    | BOOLEAN   | Boolean values                              |
| `FieldDate`       | DATE      | Date/time values                            |
| `FieldTitle`      | TITLE     | Title/heading text                          |
| `FieldSelect`     | SELECT    | Selected option value                       |
| `FieldRelation`   | RELATION  | Related content reference                   |
| `FieldS3Image`    | IMAGE     | S3-hosted image with URL                    |
| `FieldS3Video`    | VIDEO     | S3-hosted video                             |
| `FieldLocalImage` | IMAGE_URL | Local/external image URL                    |
| `FieldFile`       | FILE      | File attachment                             |

### CSR-Only Fields (Interactive)

These fields require client-side rendering for interactivity:

| Component       | CMS Type | Description                            |
| --------------- | -------- | -------------------------------------- |
| `FieldButton`   | BUTTON   | Interactive button with submit support |
| `FieldInput`    | INPUT    | Form input field                       |
| `FieldCheckbox` | CHECKBOX | Checkbox input                         |

### Field Usage Examples

#### FieldString

```tsx
// Renders to configured HTML element (div, span, p, etc.)
<FieldString
  f={{ name: 'title', type: 'STRING', visual: { component: 'h1', className: 'text-2xl' } }}
  modelData={{ title: 'Welcome' }}
/>
// Output: <h1 class="text-2xl">Welcome</h1>
```

#### FieldContent

```tsx
// Renders HTML content
<FieldContent
  f={{ name: 'body', type: 'CONTENT', visual: { component: 'article' } }}
  modelData={{ body: '<p>Rich <strong>content</strong></p>' }}
/>
// Output: <article><p>Rich <strong>content</strong></p></article>
```

#### FieldS3Image

```tsx
// Renders S3-hosted image
<FieldS3Image
  f={{ name: 'hero', type: 'IMAGE', visual: { className: 'w-full' } }}
  modelData={{ hero: { url: 'https://s3.../image.jpg' } }}
/>
// Output: <img src="https://s3.../image.jpg" class="w-full" />
```

#### FieldButton (Interactive)

```tsx
// Interactive button with form submission
<FieldButton
  f={{ name: 'submit', type: 'BUTTON' }}
  modelData={{ submit: { label: 'Submit', loadingLabel: 'Submitting...', type: 'submit' } }}
  state={formState}
  onChange={(key, value) => {
    /* handle state change */
  }}
  onSubmit={async (state, prefix) => {
    /* submit form */
  }}
/>
```

#### FieldInput (Interactive)

```tsx
// Form input with validation rules
<FieldInput
  f={{
    name: 'email',
    type: 'INPUT',
    visual: {
      className: 'form-input',
      rules: [
        /* validation rules */
      ],
    },
  }}
  modelData={{ email: { label: 'Email', type: 'email', placeholder: 'Enter email' } }}
  value={formState.email}
  onChange={(key, value) => setFormState({ ...formState, [key]: value })}
/>
```

---

## HusarComponents Registry

Type-safe component factory for Views, Models, and Shapes:

```tsx
import { HusarComponents } from '@husar.ai/render';
import type { Zeus, ZeusModels } from './generated/zeus';

// Create typed components
const { View, Model, Shape } = HusarComponents<Zeus, ZeusModels>(
  ['https://api.husar.ai/graphql', { headers: { husar_token: 'xxx' } }]
);

// Render a View
<View
  name="homepage"
  data={viewData}
  fields={viewFields}
  state={formState}
  setState={setFormState}
  components={{
    'hero.title': ({ data }) => <CustomTitle>{data}</CustomTitle>
  }}
/>

// Render a Model
<Model
  name="BlogPost"
  data={postData}
  fields={postFields}
/>

// Render a Shape
<Shape
  name="ContactForm"
  data={formData}
  fields={formFields}
  onSubmit={async (data) => { /* custom submit handler */ }}
/>
```

### Auto-Submit Feature

When a `host` is provided, form submissions are automatically sent to the GraphQL endpoint:

```tsx
const { View } = HusarComponents<Zeus, ZeusModels>([
  'https://api.husar.ai/graphql',
  { headers: { husar_token: 'your-token' } },
]);

// Submit button will auto-POST to the GraphQL endpoint
<View name="contact" data={data} fields={fields} state={state} setState={setState} />;
```

---

## useHusar Hook

Access CMS field definitions with type safety:

```tsx
import { useHusar } from '@husar.ai/render';

function MyComponent({ fields }) {
  const { cmsFields } = useHusar();
  const getField = cmsFields(fields);

  // Get field definition by path
  const titleField = getField<ViewHomepage>('hero.title');
  // Returns: { name: 'title', type: 'STRING', visual: {...}, ... }

  return <div className={titleField.visual?.className}>{...}</div>;
}
```

---

## State Utilities

### flattenState

Convert nested object to flat dot-notation paths:

```typescript
import { flattenState } from '@husar.ai/render';

const nested = {
  user: {
    name: 'John',
    address: {
      city: 'NYC',
    },
  },
  items: ['a', 'b'],
};

const flat = flattenState(nested);
// Result:
// {
//   'user.name': 'John',
//   'user.address.city': 'NYC',
//   'items[0]': 'a',
//   'items[1]': 'b'
// }
```

### unflattenState

Convert flat paths back to nested object:

```typescript
import { unflattenState } from '@husar.ai/render';

const flat = {
  'user.name': 'John',
  'user.address.city': 'NYC',
  'items[0]': 'a',
};

const nested = unflattenState(flat);
// Result:
// {
//   user: { name: 'John', address: { city: 'NYC' } },
//   items: ['a']
// }
```

---

## Conditional Visibility Rules

Fields support visibility rules based on state:

```typescript
const fieldWithRules = {
  name: 'discountCode',
  type: 'INPUT',
  visual: {
    rules: [
      {
        conditions: [
          {
            type: 'CONDITION',
            path: 'hasDiscount',
            operator: 'EQUAL',
            value: 'true',
          },
        ],
        actions: [], // Empty = visibility rule
      },
    ],
  },
};

// Field only shows when state.hasDiscount === true
```

### Rule Actions

| Action Type     | Description                                     |
| --------------- | ----------------------------------------------- |
| `SET_VALUE`     | Set a field value when conditions match         |
| `SET_DISABLED`  | Disable or hide field (`value: 'hidden'` hides) |
| `DISPLAY_VALUE` | Control display formatting                      |

### Condition Operators

| Operator        | Description                       |
| --------------- | --------------------------------- |
| `EQUAL`         | Exact match                       |
| `NOT_EQUAL`     | Not equal                         |
| `GREATER`       | Greater than                      |
| `GREATER_EQUAL` | Greater or equal                  |
| `LESS`          | Less than                         |
| `LESS_EQUAL`    | Less or equal                     |
| `EXISTS`        | Value is not null/undefined/empty |
| `NOT_EXISTS`    | Value is null/undefined/empty     |
| `REGEX`         | Matches regex pattern             |
| `NOT_REGEX`     | Does not match regex              |

---

## Component & Attribute Overrides

### ComponentsOverride

Replace entire field rendering:

```tsx
import { TailwindPlay, ComponentsOverride } from '@husar.ai/render';

const components: ComponentsOverride<ViewHomepage> = {
  'hero.title': ({ data, attrs, f, component }) => <motion.h1 className={attrs.className}>{data}</motion.h1>,
  features: ({ data, component }) => <CustomFeaturesGrid items={data} />,
};

<TailwindPlay fields={fields} modelData={data} components={components} />;
```

### AttributesOverride

Modify HTML attributes:

```tsx
const attributes: AttributesOverride<ViewHomepage> = {
  'hero.image': ({ data, attrs }) => ({
    ...attrs,
    loading: 'lazy',
    'data-src': data?.url,
    onLoad: () => console.log('Image loaded'),
  }),
};

<TailwindPlay fields={fields} modelData={data} attributes={attributes} />;
```

---

## Integration with @husar.ai/ssr

For server-side rendering with Next.js:

```tsx
// pages/[...slug].tsx
import { TailwindPlay } from '@husar.ai/render';
import { getPageData } from '@husar.ai/ssr';

export async function getServerSideProps({ params }) {
  const { fields, data } = await getPageData(params.slug);
  return { props: { fields, data } };
}

export default function Page({ fields, data }) {
  return <TailwindPlay fields={fields} modelData={data} />;
}
```

### Client-Side Interactivity

For forms and interactive elements:

```tsx
'use client';

import { useState } from 'react';
import { TailwindPlay, flattenState } from '@husar.ai/render';

export default function InteractivePage({ fields, initialData }) {
  const [state, setState] = useState(flattenState(initialData) || {});

  return (
    <TailwindPlay
      fields={fields}
      modelData={initialData}
      state={state}
      setState={setState}
      onSubmit={async (data, prefix) => {
        const response = await fetch('/api/submit', {
          method: 'POST',
          body: JSON.stringify(data),
        });
      }}
    />
  );
}
```

---

## Nested Shapes and Objects

The renderer handles nested structures automatically:

```tsx
// CMS field structure
const fields = [
  {
    name: 'hero',
    type: 'SHAPE',
    fields: [
      { name: 'title', type: 'STRING' },
      { name: 'subtitle', type: 'STRING' },
      {
        name: 'cta',
        type: 'SHAPE',
        fields: [
          { name: 'label', type: 'STRING' },
          { name: 'url', type: 'STRING' },
        ],
      },
    ],
  },
];

// Data structure
const data = {
  hero: {
    title: 'Welcome',
    subtitle: 'Get started today',
    cta: { label: 'Learn More', url: '/about' },
  },
};

// Renders nested components with proper path prefixing
<TailwindPlay fields={fields} modelData={data} />;
```

---

## Type Utilities

### FlattenedKeyType

Get the type at a flattened path:

```typescript
type TitleType = FlattenedKeyType<ViewHomepage, 'hero.title'>;
// Result: string | undefined
```

### RemoveArrays / RemoveNullKeys

Clean up types for safer access:

```typescript
type CleanItem = RemoveArrays<Item[]>; // Item
type NonNullKeys = RemoveNullKeys<MyType>; // Keys that can't be null
```

---

## File Structure

```
packages/render/
  src/
    index.tsx              # Main exports
    client/
      useHusar.ts          # React hook and type utilities
    react/
      HusarComponents.tsx  # Component registry factory
    shared/
      payload.ts           # State flatten/unflatten utilities
    TailwindPlay/
      TailwindRenderer.tsx # Main renderer components
      Fields/
        Boolean.tsx        # FieldBoolean
        Button.tsx         # FieldButton (CSR)
        Checkbox.tsx       # FieldCheckbox (CSR)
        Content.tsx        # FieldContent
        Date.tsx           # FieldDate
        File.tsx           # FieldFile
        Input.tsx          # FieldInput (CSR)
        Invalid.tsx        # Error display
        Link.tsx           # FieldLink
        LocalImage.tsx     # FieldLocalImage
        Number.tsx         # FieldNumber
        Relation.tsx       # FieldRelation
        S3Image.tsx        # FieldS3Image
        S3Video.tsx        # FieldS3Video
        Select.tsx         # FieldSelect
        String.tsx         # FieldString
        Title.tsx          # FieldTitle
        Variable.tsx       # FieldVariable
        types.ts           # Shared type definitions
      shared/
        constants.ts       # HTML element validation
        extract.ts         # Data extraction utilities
        rules.ts           # Visibility rule evaluation
    graphql/
      core/
        selectors.ts       # GraphQL field type definitions
    zeus/
      index.ts             # Generated GraphQL types
```

---

## Best Practices

1. **Use flattened state** for form handling - it simplifies path-based updates
2. **Provide host to HusarComponents** for automatic form submission
3. **Override components sparingly** - prefer attribute overrides when possible
4. **Handle CSR fields separately** in Next.js with 'use client'
5. **Pre-flatten initial data** when hydrating interactive pages
