# Client-Side Angular → React Migration Workflow (Reusable Template)

This document describes how to create a `client-side-react` project from an existing Angular `client-side` project, keeping **the same structure and behavior**.

It is designed to be copied into a new addon repo and adjusted (AddonUUID, element names, ports, etc.).

---

## Goals

- Keep the Angular domain model and naming 1:1 (same configuration shape and keys).
- Use Pepperi UI libraries so styling is consistent with the platform.
- Support **both** addon types:
  - **Settings-only addons** (single custom element, like AddonsManager)
  - **Pages block addons** (Block + BlockEditor custom elements, like ButtonsBar/Gallery/BannersBar)

---

## 1) Create `client-side-react/` next to Angular

Typical layout:

```text
<addon-root>/
  client-side/          # existing Angular UI (legacy)
  client-side-react/    # new React UI
  server-side/
  cpi-side/
  shared/
  publish/
```

---

## 2) Initialize React + Vite + Module Federation

### 2.1 Scaffold

Inside `<addon-root>`:

```bash
npm create vite@latest client-side-react -- --template react-ts
```

### 2.2 Install dependencies

Minimum for most Pepperi UIs:

```bash
npm i \
  @pepperi-addons/ngx-lib-react \
  @pepperi-addons/ngx-composite-lib-react \
  @pepperi-addons/papi-sdk \
  i18next react-i18next \
  react react-dom
```

Also install the Vite federation plugin:

```bash
npm i -D @originjs/vite-plugin-federation
```

SCSS support (recommended for component styling):

```bash
npm i -D sass
```

### 2.3 Configure Vite Module Federation

In `vite.config.ts`:

- Use `file_<AddonUUID>.js` as the remote entry name.
- Expose a single entry that registers your web components (example: `./src/bootstrap.tsx`).
- Write the build output to `../publish` (same as Angular Pages expectations).

Example structure:

```ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import federation from '@originjs/vite-plugin-federation';
import addonConfig from '../addon.config.json';

const filename = `file_${(addonConfig as any).AddonUUID}`;

export default defineConfig(({ mode }) => {
  const isDevBlocks = mode === 'devblocks';

  return {
    plugins: [
      react(),
      federation({
        name: filename,
        filename: `${filename}.js`,
        exposes: {
          './WebComponents': './src/bootstrap.tsx',
        },
        shared: ['react', 'react-dom'],
      }),
    ],
    build: {
      outDir: '../publish',
      emptyOutDir: false,
      target: 'esnext',
      minify: isDevBlocks ? false : 'esbuild',
      sourcemap: isDevBlocks ? true : false,
      modulePreload: false,
      cssCodeSplit: false,
      assetsDir: '',
      rollupOptions: {
        output: {
          chunkFileNames: '[name].js',
          assetFileNames: '[name].[ext]',
          minifyInternalExports: false,
        },
      },
    },
  };
});
```

---

## 3) Output expectations (Pages / MF)

For Pages, the remote entry must be named:

```text
file_<AddonUUID>.js
```

And should be built into:

```text
../publish/
```

---

## 3.1 Azure publish compatibility checklist

When the addon includes `client-side-react/` and/or `cpi-side/`, update the repo for Azure pipelines before the first publish.

### A) Pin Azure pipelines to Node 20

Vite 5 and modern React toolchains use newer JavaScript syntax that is not supported by the default Azure template runtime (`nodeVersion: 14`).

In both files below, uncomment `parameters` and set:

```yaml
parameters:
  nodeVersion: 20
```

Files:

- `.pipelines/azure-pipeline-build.yml`
- `.pipelines/azure-pipeline-publish.yml`

### B) Harden `cpi-side` for modern Node

If the repo has `cpi-side/`, update `cpi-side/package.json`:

- Upgrade `rollup-plugin-typescript2` to `^0.36.0`
- Add `tslib` with version `^2.6.2`

Example:

```json
"dependencies": {
  "@pepperi-addons/cpi-node": "*",
  "rollup-plugin-typescript2": "^0.36.0",
  "tslib": "^2.6.2"
}
```

### C) Keep JSON imports valid in CPI Rollup configs

If `cpi-side/tsconfig.json` contains `"resolveJsonModule": true`, then both Rollup configs must explicitly set Node module resolution.

Update:

- `cpi-side/rollup.config.js`
- `cpi-side/debug.rollup.config.js`

Inside `typescript({ tsconfigOverride: { compilerOptions: ... } })`, use:

```ts
compilerOptions: {
  module: 'es2015',
  moduleResolution: 'node',
  declaration: false,
}
```

### D) Refresh lockfiles after dependency changes

After changing `cpi-side/package.json`, regenerate the lockfile so Azure `npm ci` installs the new dependency tree instead of stale locked versions.

Run:

```bash
npm install --prefix ./cpi-side
```

If multiple addon repos were updated, run this once per repo.

### E) Expected failures this prevents

These changes prevent the common Azure publish failures:

- `SyntaxError: Unexpected token '??='`
- `Error loading tslib helper library`
- `Package subpath './package.json' is not defined by "exports"`
- `TS5070: Option '--resolveJsonModule' cannot be specified without 'node' module resolution strategy`

---

## 4) Custom elements

### A) Settings-only addons

- Usually register **one** element (example pattern):

```text
settings-element-<AddonUUID>
```

### B) Pages block addons

Register **two** elements:

```text
block-element-<AddonUUID>
block-editor-element-<AddonUUID>
```

These must match server-side relations.

### 4.1 `bootstrap.tsx` (recommended pattern)

Expose a single entry (from federation) that registers your custom elements.

Key points:

- Read `AddonUUID` from `addon.config.json`.
- Define `block-element-<AddonUUID>` and `block-editor-element-<AddonUUID>`.
- When the host sets `hostObject`, re-render.
- Emit host events by dispatching `CustomEvent('hostEvents', { detail, bubbles: true })`.

Minimal shape:

```ts
import React from 'react';
import { createRoot } from 'react-dom/client';
import addonConfig from '../../addon.config.json';
import './i18n';

const ADDON_UUID: string = (addonConfig as any).AddonUUID;

type HostProps = { hostObject?: any; hostEmit?: (evt: any) => void };

function defineElement(tag: string, Component: React.ComponentType<HostProps>) {
  if (customElements.get(tag)) return;

  class ReactElement extends HTMLElement {
    private root?: ReturnType<typeof createRoot>;
    private container?: HTMLDivElement;
    private _hostObject?: any;

    connectedCallback() {
      if (!this.container) {
        this.container = document.createElement('div');
        this.container.classList.add('pepperi-theme');
        this.appendChild(this.container);
        this.root = createRoot(this.container);
      }
      this.render();
    }

    set hostObject(value: any) {
      this._hostObject = value;
      this.render();
    }

    private render() {
      const hostEmit = (evt: any) => {
        this.dispatchEvent(new CustomEvent('hostEvents', { detail: evt, bubbles: true }));
      };

      this.root?.render(React.createElement(Component, { hostObject: this._hostObject, hostEmit }));
    }
  }

  customElements.define(tag, ReactElement);
}

// defineElement(`block-element-${ADDON_UUID}`, Block);
// defineElement(`block-editor-element-${ADDON_UUID}`, BlockEditor);

export default {};
```

---

## 5) Host integration pattern (Pages)

Pages inject a `hostObject` into your custom element and listen to `hostEvents` emitted back.

- **Input**: `hostObject` contains `configuration` + page context
- **Output**: emit host events by dispatching `CustomEvent('hostEvents', { detail, bubbles: true })`

Common actions:

- Update a single field:

```ts
hostEmit({ action: 'set-configuration-field', key: '<path>', value })
```

- Replace full configuration and ask Pages to persist it:

```ts
hostEmit({ action: 'set-configuration', configuration: next, updatePageConfiguration: true })
```

### Configuration Persistence Best Practices

Based on real-world migration experience (FilterBlock, BannersBar, Gallery2):

#### When to Use `set-configuration-field`:
- ✅ Simple primitive values (strings, numbers, booleans)
- ✅ Fields that persist when user saves the page
- ✅ Lightweight updates

```ts
hostEmit({ action: 'set-configuration-field', key: 'spacing', value: 'md' });
hostEmit({ action: 'set-configuration-field', key: 'alignment.Horizontal', value: 'center' });
```

#### When to Use `setConfigurationFull` with `updatePageConfiguration: true`:
- ✅ Complex objects (e.g., padding with multiple properties)
- ✅ Array operations (add/remove/reorder)
- ✅ Changes that must persist immediately
- ✅ When `set-configuration-field` doesn't persist correctly

```ts
// For complex objects
const next = { ...configuration, padding: { IsUniform: true, PaddingValue: '2' } };
hostEmit({ action: 'set-configuration', configuration: next, updatePageConfiguration: true });

// For array operations
const next = { ...configuration, items: [...configuration.items, newItem] };
hostEmit({ action: 'set-configuration', configuration: next, updatePageConfiguration: true });
```

#### Troubleshooting: Configuration Not Persisting

**Symptom:** Changes appear in editor/block but revert after page reload.

**Solution:** Use `setConfigurationFull` with `updatePageConfiguration: true` instead of `set-configuration-field`.

**Common causes:**
- Using `set-configuration-field` for complex objects
- Missing `updatePageConfiguration: true` flag
- Value extraction logic pulling wrong property from event

---

## 6) devBlocks workflow (Pages)

### 6.1 Build

From `client-side-react/`:

```bash
npm run build:debug
```

### 6.2 Serve

```bash
npm run serve:dist
```

The server should expose:

```text
http://localhost:4443/file_<AddonUUID>.js
```

### 6.3 devBlocks URL param

```text
&devBlocks=[
  ["block-element-<AddonUUID>","http://localhost:4443/file_<AddonUUID>.js"],
  ["block-editor-element-<AddonUUID>","http://localhost:4443/file_<AddonUUID>.js"]
]
```

---

## 7) Migration rules

- Keep configuration keys identical to Angular
- Port services into plain TS services first
- Replace dialogs with `openReactPepDialog` (when using Pepperi React libs)
- Keep custom SCSS minimal and prefer Pepperi styling

### 7.1 Dialog migration pattern (`openReactPepDialog`)

When migrating Angular confirmation dialogs to React, use the same structure that works in Pepperi addons such as Themes, Slugs, Configurations, and PageBuilder.

Rules:

- Return exactly **two top-level nodes** from `renderContent`: one for content and one for actions.
- Put `pep-dialog-content` on the first node.
- Put `pep-dialog-actions` on the **actual second top-level node**.
- Put the footer layout class on that same `pep-dialog-actions` node.
- Do **not** move the footer class into an extra nested wrapper, because the second top-level child is the node that gets portaled into the Angular dialog actions host.
- Prefer class-based footer layout in SCSS: `display: flex`, `justify-content: flex-end`, `align-items: center`, `gap: 0.5rem`.

Recommended pattern:

```tsx
const shouldContinue = await openReactPepDialog<boolean>({
  title: t('MESSAGES.TITLE_NOTICE'),
  showClose: true,
  showHeader: true,
  showFooter: true,
  renderContent: (close) => (
    <>
      <div pep-dialog-content>
        <p>{t('MESSAGES.CONFIRMATION_TEXT')}</p>
      </div>
      <div pep-dialog-actions className='confirm-dialog-actions'>
        <PepButton
          value={t('CANCEL')}
          styleType='weak'
          sizeType='md'
          onButtonClick={() => close(false)}
        />
        <PepButton
          value={t('DELETE')}
          styleType='strong'
          styleStateType='caution'
          sizeType='md'
          onButtonClick={() => close(true)}
        />
      </div>
    </>
  ),
});
```

SCSS example:

```scss
.confirm-dialog-actions {
  display: flex;
  justify-content: flex-end;
  align-items: center;
  gap: 0.5rem;
}
```

### 7.2 Styling (SCSS)

- **Always use `.scss` files** for component styling, never `.css` files.
- Implemented as requested, and I followed your SCSS preference memory: keep `@use` with ngx-lib-react abstractions (vars/mixins/functions), not hardcoded replacements.
- Use proper SCSS nesting syntax with the parent selector (`&`) for modifiers.
- Import the SCSS file in the component:

```ts
import './BlockEditor.scss'
```

- In SCSS files, prefer ngx-lib-react abstractions via `@use`:

```scss
@use '@pepperi-addons/ngx-lib-react/styles/abstracts/variables.scss' as vars;
@use '@pepperi-addons/ngx-lib-react/styles/abstracts/mixins.scss' as ngx-mixins;
@use '@pepperi-addons/ngx-lib-react/styles/abstracts/functions.scss' as ngx-functions;
```

- Vite will compile SCSS as long as `sass` is installed:

```bash
npm i -D sass
```

#### SCSS Best Practices

**Use nesting and parent selector:**

```scss
.checkbox-as-sub-title {
  background: transparent !important;
  padding: 0 !important;

  .pep-checkbox,
  .pep-field {
    background: transparent !important;
    padding: 0 !important;
  }

  &.pep-field-no-spacing {
    pep-checkbox-element {
      min-height: unset;
      margin-bottom: 0;
    }
  }
}
```

**Don't write flat CSS-style:**

```scss
// ❌ Bad - flat CSS style
.checkbox-as-sub-title { }
.checkbox-as-sub-title .pep-checkbox { }
.checkbox-as-sub-title.pep-field-no-spacing { }

// ✅ Good - nested SCSS style (see above)
```

### 7.3 `PepGroupButtonsSettings` (composite-react)

Use `PepGroupButtonsSettings` from `@pepperi-addons/ngx-composite-lib-react` for enum-like selections (sizes, alignments, etc.).

Notes:

- Do not create a local wrapper workaround for `pep-group-buttons-settings-element`.
- The upstream `PepGroupButtonsSettings` wrapper handles initialization timing (including first-load rendering and event wiring).

#### Common usage (`groupType="sizes"`)

```tsx
import { PepGroupButtonsSettings } from '@pepperi-addons/ngx-composite-lib-react';

<PepGroupButtonsSettings
  groupType="sizes"
  header={t('EDITOR.GENERAL.BORDER_RADIUS')}
  subHeader={t('EDITOR.GENERAL.CHOOSE_BORDER_RADIUS')}
  btnKey={configuration?.BannerConfig?.Structure?.BorderRadius ?? 'none'}
  useNone={true}
  excludeKeys={['xs', 'lg', '2xl']}
  onBtnkeyChange={(value) => onFieldChange('BorderRadius', value)}
/>;
```

#### Values emitted (`groupType="sizes"`)

The component emits a string key on `onBtnkeyChange`. For `groupType="sizes"`:

```text
none, xs, sm, md, lg, xl, 2xl
```

Store the selected key in your configuration (example: `BannerConfig.Structure.BorderRadius = 'sm'`).

#### Event payload shape

`onBtnkeyChange` receives the selected key as a string. Treat it as the source of truth (do not rely on DOM state).

### 7.4 Asset Handling with `PepAssetPickerButton`

Use `PepAssetPickerButton` from `@pepperi-addons/ngx-composite-lib-react` for image/asset selection.

#### Critical Pattern: Atomic State Updates

**❌ Wrong - Race condition (two separate calls):**

```tsx
// This causes a race condition - second call overwrites first
onAssetChange={(detail: any) => {
  if (!detail?.url) return;
  onFieldChange(id, 'AssetURL', detail.url);  // First update
  onFieldChange(id, 'AssetKey', detail.key);  // Second update loses first
}}
```

**✅ Correct - Atomic update (single call):**

```tsx
// Update both properties atomically in a single operation
onAssetChange={(detail: any) => {
  if (!detail?.url) return;
  const assetData = { AssetURL: detail.url, AssetKey: detail.key };
  onFieldChange(id, '__ASSET__', assetData);
}}
```

Then handle the special `__ASSET__` key in your `onFieldChange` handler:

```tsx
const onFieldChange = (id: number, key: string, value: any) => {
  const next = JSON.parse(JSON.stringify(configuration || {}));
  
  // Special handling for asset data (both URL and Key together)
  if (key === '__ASSET__' && value && typeof value === 'object') {
    next.Items[id].AssetURL = value.AssetURL || '';
    next.Items[id].AssetKey = value.AssetKey || '';
    
    // Emit both fields to host
    if (hostEmit) {
      hostEmit({ action: 'set-configuration-field', key: `Items[${id}].AssetURL`, value: value.AssetURL });
      hostEmit({ action: 'set-configuration-field', key: `Items[${id}].AssetKey`, value: value.AssetKey });
    }
    
    setConfiguration(next);
    return;
  }
  
  // ... rest of field change logic
};
```

#### Why This Matters

React state updates are **asynchronous**. When you call `onFieldChange` twice in quick succession, the second call uses stale configuration data from before the first update completed, causing data loss.

### 7.5 Pepperi Angular Elements bundles (important)

Pepperi React UI libraries are implemented as web components (Angular Elements) under the hood.

- When running inside Pages / PageBuilder, those bundles are usually loaded by the host container.
- If you need to run the remote standalone (outside Pages), you may need to explicitly load:

```ts
import '@pepperi-addons/ngx-lib-react/elements/runtime.js';
import '@pepperi-addons/ngx-lib-react/elements/polyfills.js';
import '@pepperi-addons/ngx-lib-react/elements/main.js';
import '@pepperi-addons/ngx-composite-lib-react/elements/runtime.js';
import '@pepperi-addons/ngx-composite-lib-react/elements/polyfills.js';
import '@pepperi-addons/ngx-composite-lib-react/elements/main.js';
import '@pepperi-addons/ngx-lib-react/elements/styles.css';
import '@pepperi-addons/ngx-composite-lib-react/elements/styles.css';
```

If you see a Pepperi component partially rendering or missing its initial options, confirm the relevant custom element tag is defined:

```ts
await customElements.whenDefined('<tag-name>');
```

### 7.6 Translations (i18n)

#### Setup

1. **Install dependencies**:
```bash
npm i i18next react-i18next
```

2. **Create translation files (MANDATORY)** in `src/locales/i18n/`:

Create separate JSON files for each supported language. **All files below are required** and all languages must keep the same keys as the `en.json` structure:

```text
src/locales/i18n/
  ├── ar.json
  ├── de.json
  ├── en.json
  ├── es.json
  ├── fr.json
  ├── he.json
  ├── it.json
  ├── ja.json
  ├── pl.json
  ├── pt.json
  ├── sr.json
  └── zh.json
```

**Mandatory migration rule for each language file above:**

1. If the same language file exists in Angular (usually under `client-side/src/assets/i18n/`), **copy that file** into React `src/locales/i18n/`.
2. If the Angular language file does not exist, **create it by duplicating `en.json` keys and values** (temporary fallback).
3. Do **not** leave any language file missing.

**Example `en.json`:**
```json
{
  "CONTENT": "Content",
  "HEIGHT": "Height (rem)",
  "NESTED": {
    "KEY": "Nested value"
  }
}
```

3. **Configure i18n** in `src/i18n.ts`:
```ts
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import ar from './locales/i18n/ar.json';
import de from './locales/i18n/de.json';
import en from './locales/i18n/en.json';
import es from './locales/i18n/es.json';
import fr from './locales/i18n/fr.json';
import he from './locales/i18n/he.json';
import it from './locales/i18n/it.json';
import ja from './locales/i18n/ja.json';
import pl from './locales/i18n/pl.json';
import pt from './locales/i18n/pt.json';
import sr from './locales/i18n/sr.json';
import zh from './locales/i18n/zh.json';

const supportedLngs = ['ar', 'de', 'en', 'es', 'fr', 'he', 'it', 'ja', 'pl', 'pt', 'sr', 'zh'] as const;

function getInitialLanguage(): string {
  try {
    const nav: any = globalThis?.navigator;
    const candidate = (Array.isArray(nav?.languages) && nav.languages.length > 0)
      ? nav.languages[0]
      : nav?.language;

    const base = (candidate || 'en').toLowerCase().split('-')[0];
    const detectedLang = (supportedLngs as readonly string[]).includes(base) ? base : 'en';
    
    console.log('[i18n] Browser languages:', nav?.languages);
    console.log('[i18n] Detected language:', detectedLang);
    
    return detectedLang;
  } catch {
    return 'en';
  }
}

i18n.use(initReactI18next).init({
  resources: {
    ar: { translation: ar },
    de: { translation: de },
    en: { translation: en },
    es: { translation: es },
    fr: { translation: fr },
    he: { translation: he },
    it: { translation: it },
    ja: { translation: ja },
    pl: { translation: pl },
    pt: { translation: pt },
    sr: { translation: sr },
    zh: { translation: zh },
  },
  lng: getInitialLanguage(),
  fallbackLng: 'en',
  supportedLngs: supportedLngs as unknown as string[],
  nonExplicitSupportedLngs: true,
  interpolation: {
    escapeValue: false,
  },
  returnNull: false,
  returnEmptyString: false,
});

export default i18n;
```

4. **Import i18n** in `src/bootstrap.tsx`:
```ts
import './i18n';
```

**Important:** The i18n configuration must be imported before any components that use translations.

#### Usage in Components

**Use `useTranslation()` hook** for translations:
```ts
import { useTranslation } from 'react-i18next';

function MyComponent() {
  const { t } = useTranslation();
  return <label>{t('EDITOR.TABS.GENERAL')}</label>;
}
```

#### Critical Best Practices

**❌ NEVER use `i18n.t()` for dynamic/default values**:
```ts
// BAD - translation is captured at creation time, not render time
const defaultConfig = {
  label: i18n.t('EDITOR.GENERAL.BANNER')
};
```

**✅ Use `i18n.t()` ONLY at render time via `useTranslation()`**:
```ts
// GOOD - translation updates when language changes
const { t } = useTranslation();
const label = t('EDITOR.GENERAL.BANNER');
```

**For default values**, use plain English strings:
```ts
// GOOD - plain strings for defaults, translate at render time
function createDefaultBanners() {
  return [{
    FirstTitle: { Label: '1st Banner' },  // Plain string
    SecondTitle: { Label: '2nd Title' }   // Plain string
  }];
}
```

#### Language Detection

- Language is detected from `navigator.languages[0]` on app load
- **Users must reload the page** after changing browser language settings
- Use hard reload (Cmd+Shift+R / Ctrl+Shift+R) to clear cache

---

## 8) MUI Tabs Styling (Block Editor)

For block editors using MUI Tabs (General/Content tabs), use the shared Pepperi helper styles.

### Mandatory rules

1. **Must import helper CSS in the editor `.tsx` file**:

```ts
import '@pepperi-addons/ngx-lib-react/elements/mui-tabs.css';
```

2. **Must add helper classes on the tabs wrapper element**:

```tsx
<div className="page-builder-editor-tabs pepperi-mui-tabs pepperi-mui-tabs--sticky">
```

3. **Do not add private block-level MUI tab styling** (`.MuiTabs-*`, `.MuiTab-*`) in block/editor SCSS.
   - Keep only block-specific layout/content styles (for example `.page-builder-editor-tab`, list spacing, etc.).
   - Tab visual behavior must come from `@pepperi-addons/ngx-lib-react/elements/mui-tabs.css`.

### Example

```tsx
import { Tabs, Tab, Box } from '@mui/material';
import '@pepperi-addons/ngx-lib-react/elements/mui-tabs.css';

<div className="page-builder-editor-tabs pepperi-mui-tabs pepperi-mui-tabs--sticky">
  <Box>
    <Tabs value={tab} onChange={onTabChange} variant="fullWidth">
      <Tab label={t('EDITOR.TABS.GENERAL')} />
      <Tab label={t('EDITOR.TABS.CONTENT')} />
    </Tabs>
  </Box>
</div>
```

---

## 9) Angular Web Component Integration Patterns

### 8.1 PepDraggableItem Toggle Event Behavior

When using `PepDraggableItem` (or its wrapper `PepDraggableItemWrapper`), the `contentToggle` event emits the **new state**, not the current state.

#### ❌ Wrong - Double inversion

```ts
const onToggle = useCallback((event: boolean, index: number) => {
  const newEvent = !event;  // ❌ Wrong - event is already the new state
  setCurrentIndex(newEvent ? index : -1);
}, []);
```

**Problem:** This causes the component to require two clicks to toggle because:
- Angular component sends `true` when opening → Code inverts to `false` → Stays closed
- Second click sends `true` again → Code inverts to `false` → Finally opens

#### ✅ Correct - Use event directly

```ts
const onToggle = useCallback((event: boolean, index: number) => {
  // event is already the new state (true = opening, false = closing)
  setCurrentIndex(event ? index : -1);
}, []);
```

**Key insight:** The Angular component's `contentToggle` event already represents the **new state** after the toggle action, not the current state before it.

### 8.2 PepDraggableItemWrapper for React Children

When rendering React components inside `pep-draggable-item-element`, use `PepDraggableItemWrapper` to properly manage the React root lifecycle.

**Why needed:** The Angular component manipulates the DOM directly when collapsing/expanding, causing React's virtual DOM to become out of sync.

**Solution:** The wrapper unmounts and recreates the React root when `isToggleContentOpen` changes, ensuring React and the actual DOM stay synchronized.

See `PepDraggableItemWrapper.tsx` in Gallery2 or FilterBlock for the complete implementation pattern.

### 8.3 PepSelect Empty Option Behavior

The `emptyOption` prop on `PepSelect` controls whether a "None" option appears in dropdowns.

#### Pattern from Angular

```html
<pep-select 
  [emptyOption]="configuration?.filters[i]?.addNoneOption || false"
  ...
></pep-select>
```

#### React Implementation

```tsx
<PepSelect
  emptyOption={configuration?.filters[i]?.addNoneOption || false}
  ...
/>
```

**Key points:**
- When `emptyOption={true}`, the component displays "None" as the first option
- When `emptyOption={false}`, shows "Please select an option" as placeholder
- The text "None" is built into the component - you cannot customize it
- Bind `emptyOption` to your configuration property (e.g., `addNoneOption`) to let users control this behavior

### 8.4 Default Item Naming Pattern

When initializing collections (filters, items, cards, etc.), follow the Gallery pattern for consistency:

#### ✅ Recommended Pattern

```ts
// Initial default item
const getDefaultHostObject = useCallback((): IEditorConfig => {
  const defaultItem = new IItem();
  defaultItem.title = '1 Item';  // or '1 Filter', '1 Card', etc.
  
  return {
    items: [defaultItem],
    // ... other config
  };
}, []);

// Adding new items
const addNewItem = useCallback(() => {
  if (!configuration) return;
  
  const item = new IItem();
  item.title = `${configuration.items.length + 1} Item`;  // Auto-numbered
  
  const next = { ...configuration };
  next.items = [...next.items, item];
  setConfigurationFull(next, true);
}, [configuration, setConfigurationFull]);
```

**Benefits:**
- Consistent UX across all Pepperi blocks
- Clear visual indication of item count
- Users can easily identify and rename items
- Matches Gallery, FilterBlock, and other block patterns

#### ❌ Avoid

```ts
// Don't use hardcoded values
defaultItem.title = 'Accounts';
defaultItem.pageParameterKey = 'AccountUUID';

// Don't leave empty (shows as "UNTITLED")
defaultItem.title = '';
```

---

## 9. Drag-and-Drop with @dnd-kit

Implementing sortable lists requires proper setup of both the container and individual items.

### 9.1 Container Setup (BlockEditor)

```tsx
import { DndContext, closestCenter, DragEndEvent, PointerSensor, useSensor, useSensors } from '@dnd-kit/core';
import { SortableContext, verticalListSortingStrategy } from '@dnd-kit/sortable';

const BlockEditor: React.FC<BlockEditorProps> = ({ hostObject, hostEmit }) => {
  // Configure sensors for drag-and-drop
  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: {
        distance: 8, // Prevents accidental drags
      },
    })
  );

  return (
    <DndContext 
      sensors={sensors}
      collisionDetection={closestCenter} 
      onDragEnd={handleDragEnd}
    >
      <SortableContext
        items={configuration.filters.map((_, i) => `filter-${i}`)}
        strategy={verticalListSortingStrategy}
      >
        {configuration.filters.map((filter, i) => (
          <FilterEditor
            key={`filter-${i}`}
            id={`filter-${i}`}
            index={i}
            filter={filter}
            isOpen={i === currentFilterIndex}
            onToggle={(event) => onFilterToggle(event, i)}
            onRemoveClick={() => onFilterRemoveClick(i)}
            onFilterChange={(event) => onFilterChange(event, i)}
          />
        ))}
      </SortableContext>
    </DndContext>
  );
};
```

### 9.2 Sortable Item Setup (FilterEditor)

```tsx
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';

const FilterEditor: React.FC<FilterEditorProps> = ({ id, isOpen, ... }) => {
  // Setup sortable drag-and-drop
  const {
    attributes,
    listeners,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ 
    id: id,
    disabled: isOpen // Disable dragging when editor is open
  });

  const style = {
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: isDragging ? 0.5 : 1,
    cursor: isOpen ? 'default' : 'grab',
  };

  return (
    <div ref={setNodeRef} style={style} {...attributes} {...listeners}>
      <PepDraggableItemWrapper
        title={filter.title}
        isToggleContentOpen={isOpen}
        onContentToggle={handleToggle}
      >
        {/* Filter editor content */}
      </PepDraggableItemWrapper>
    </div>
  );
};
```

### 9.3 Drag End Handler

```tsx
const handleDragEnd = useCallback((event: DragEndEvent) => {
  const { active, over } = event;
  
  if (!over || active.id === over.id) return;
  
  setConfiguration((prev: any) => {
    if (!prev) return prev;
    
    const oldIndex = prev.filters.findIndex((_: any, i: number) => `filter-${i}` === active.id);
    const newIndex = prev.filters.findIndex((_: any, i: number) => `filter-${i}` === over.id);
    
    const next = { ...prev };
    next.filters = arrayMove(prev.filters, oldIndex, newIndex);
    
    setConfigurationFull(next, true);
    return next;
  });
}, [setConfigurationFull]);
```

**Key points:**
- **PointerSensor**: Required for drag-and-drop to work (not optional!)
- **activationConstraint**: 8px distance prevents accidental drags when clicking
- **Disable when open**: Set `disabled: isOpen` to prevent dragging expanded items
- **Visual feedback**: Use opacity and cursor changes during drag
- **Wrapper div**: Must wrap the draggable component with ref, style, attributes, and listeners

---

## 10. Show If Dialog Implementation

The Show If dialog uses `PepQueryBuilder` within a React dialog for conditional visibility logic.

### 10.1 Dialog Implementation

```tsx
import { PepQueryBuilder, openReactPepDialog, type PepDialogSizeType } from '@pepperi-addons/ngx-lib-react';

const openFilterBuilderModal = async () => {
  type DialogData = {
    query: any;
    fields: any[];
    isValid: boolean;
    outputData: { query: any };
  };

  type DialogResult = { query: any };

  const showIfData = getShowIfData(); // Get current query and fields
  const data: DialogData = {
    query: showIfData.query,
    fields: showIfData.fields,
    isValid: true,
    outputData: { query: '' },
  };

  const QueryBuilderDialogContent: React.FC<{ close: (result?: DialogResult) => void }> = ({ close }) => {
    const dataRef = React.useRef<DialogData>(data);
    const queryRef = React.useRef<any>(dataRef.current.query);
    const isValidRef = React.useRef<boolean>(true);
    const [isValid, setIsValid] = React.useState<boolean>(true);

    return (
      <PepQueryBuilder
        query={dataRef.current.query}
        fields={dataRef.current.fields}
        onQueryChange={(q: any) => {
          queryRef.current = q;
          dataRef.current.outputData.query = q ?? '';
        }}
        onFormValidationChange={(valid: boolean) => {
          const v = !!valid;
          isValidRef.current = v;
          dataRef.current.isValid = v;
          setIsValid(v);
        }}
      />
    );
  };

  const res = await openReactPepDialog<DialogResult>({
    title: t('SHOW_IF_TITLE') ?? 'Show If',
    size: 'large' as PepDialogSizeType,
    showClose: true,
    showHeader: true,
    showFooter: true,
    renderContent: (close) => {
      return (
        <>
          <div pep-dialog-content className="show-if-dialog-content">
            <QueryBuilderDialogContent close={close} />
          </div>
          <div pep-dialog-actions className="show-if-dialog-actions">
            <PepButton
              value={t('CANCEL') ?? 'Cancel'}
              styleType="weak"
              onButtonClick={() => close(undefined)}
            />
            <PepButton
              value={t('SAVE') ?? 'Save'}
              styleType="strong"
              onButtonClick={() => {
                if (!data.isValid) return;
                close({ query: data.outputData.query });
              }}
            />
          </div>
        </>
      );
    }
  });

  if (res?.query != null) {
    saveShowIfQuery(res.query);
  }
};
```

### 10.2 Required SCSS

```scss
.show-if-dialog-content {
  min-width: 600px;
  width: 100%;
  overflow-x: auto;
  
  pep-query-builder-element {
    width: 100%;
    display: block;
  }
}

.show-if-dialog-actions {
  display: flex;
  flex-flow: row;
  gap: var(--pep-spacing-sm, 0.5rem);
  justify-content: flex-end;
}
```

**Key points:**
- Use refs to track query and validation state
- Dialog size should be `'large'` or `'xl'` for proper column display
- Always check `isValid` before saving
- Use `pep-dialog-content` and `pep-dialog-actions` attributes for proper styling

---

## 11. Placeholder vs useFirstValue Behavior

When implementing filter dropdowns, the `useFirstValue` setting controls whether the first option is auto-selected or a placeholder is shown.

### 11.1 Implementation

```tsx
// In Block.tsx
const displayValue = (!configuration?.filters[i]?.useFirstValue && calculateFilter.placeholder) 
  ? undefined 
  : calculateFilter.value;

<PepSelect
  value={displayValue}
  placeholder={calculateFilter.placeholder}
  options={calculateFilter.options || []}
  emptyOption={configuration?.filters[i]?.addNoneOption || false}
/>
```

**Logic:**
- When `useFirstValue={true}`: Display the first option's value automatically
- When `useFirstValue={false}` AND `placeholder` exists: Show placeholder text (value is `undefined`)
- When `useFirstValue={false}` AND no placeholder: Show "Please select an option"

### 11.2 Filter Visibility with Show If

```tsx
// In Block.tsx - hide filters based on Show If logic
{calculatedFilters.map((calculateFilter, i) => {
  // Hide filter if ShowIfFilter.ShowFilter is false
  if (configuration?.filters[i]?.ShowIfFilter?.ShowFilter === false) {
    return null;
  }
  
  // Hide filter when hideWhenNoOptions is true and it's hidden
  if (calculateFilter.hideWhenNoOptions && calculateFilter.hidden) {
    return null;
  }

  return (
    <PepSelect ... />
  );
})}
```

**Key points:**
- Check `ShowIfFilter.ShowFilter === false` to respect Show If conditions
- Check `hideWhenNoOptions && hidden` to hide filters with no options
- Both conditions must be checked independently

---

## 12. Real-Time Preview Updates

To ensure configuration changes update the block preview in real-time, use a `key` prop on the container element.

### 12.1 Force Re-render with Key

```tsx
// In Block.tsx
const direction = shouldBeVerticalOnSmallScreenSize ? 'vertical' : configuration.direction;

return (
  <div className="pepperi-theme">
    <div 
      key={`${direction}-${configuration.spacing}`}
      className={`filters-container direction-${direction} spacing-${configuration.spacing}`}
      style={containerStyle}
    >
      {/* Filter content */}
    </div>
  </div>
);
```

**Why this is needed:**
- React may not detect CSS class changes that affect layout (like `direction-horizontal` → `direction-vertical`)
- Adding a `key` that includes the changing values forces React to completely re-render the element
- This ensures CSS classes are properly applied and layout updates are visible immediately

**What to include in the key:**
- Any configuration values that affect CSS classes
- Common examples: `direction`, `spacing`, `alignment`
- Combine multiple values: `key={`${direction}-${spacing}`}`

**When to use:**
- Layout direction changes (horizontal ↔ vertical)
- Spacing changes (sm, md, lg, xl)
- Any CSS class-based styling that doesn't update in real-time

---

## 13. Translation Best Practices

### 13.1 Remove Fallback Values

**❌ Avoid:**
```tsx
<PepButton value={t('SAVE') ?? 'Save'} />
```

**✅ Recommended:**
```tsx
<PepButton value={t('SAVE')} />
```

**Reason:** Translation files should always have all required keys. Fallback values hide missing translations and make debugging harder.

### 13.2 Ensure All UI Text Uses Translations

```tsx
// ✅ Good
label={t('TITLE')}
placeholder={t('PLACEHOLDER_TITLE')}
value={t('ADD_FILTER')}

// ❌ Bad
label="Title"
placeholder="Enter value"
value="Add Filter"
```

---

## 14. Code Cleanup Checklist

Before finalizing your React migration:

1. **Remove console.log statements** (except critical error logging)
2. **Remove translation fallbacks** (`?? 'Text'`)
3. **Check for commented-out code**
4. **Verify all imports are used**
5. **Ensure all UI text uses `t()` function**
6. **Remove TODO/FIXME comments** (or address them)
7. **Check for hardcoded values** that should be constants
8. **Verify CSS class names** match conventions

---
