# @galaxy-dev/metadsl-adapter-mui

Material-UI 5 adapter for MetaDSL.

## Installation

```bash
pnpm add @galaxy-dev/metadsl-adapter-mui @mui/material @emotion/react @emotion/styled
```

## Usage

```tsx
import { DSLRenderer } from '@galaxy-dev/metadsl-runtime';
import { muiAdapter } from '@galaxy-dev/metadsl-adapter-mui';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import formDSL from './form.json';

// Optional: Create MUI theme
const theme = createTheme({
  palette: {
    primary: { main: '#1976d2' },
  },
});

function App() {
  return (
    <ThemeProvider theme={theme}>
      <DSLRenderer
        dsl={formDSL}
        adapter={muiAdapter}
        locale="zh-CN"
        onSubmit={(data) => console.log(data)}
      />
    </ThemeProvider>
  );
}
```

## Supported Components

- ✅ Input (TextField)
- ✅ Select
- ✅ Radio (RadioGroup)
- ✅ Checkbox
- ✅ Button
- ✅ Form (Box)
- ✅ Grid
- ✅ Flex (Box with flexbox)
- ✅ Tabs

## Custom Styling

You can use MUI's `sx` prop or className to customize components:

```json
{
  "type": "input",
  "name": "email",
  "label": { "en-US": "Email", "zh-CN": "邮箱" },
  "className": "custom-input",
  "props": {
    "sx": {
      "backgroundColor": "#f5f5f5",
      "borderRadius": 2
    }
  }
}
```

## Field Chrome & Sizing Utilities (Public API)

The sizing/chrome helpers used by every built-in field are exported from the
package root and are **semver-stable public API**. Custom fields should import
them instead of hand-rolling copies, so they stay pixel-consistent with the
built-ins:

```tsx
import {
  FIELD_HEIGHTS, // { small: 40, medium: 48 }
  FIELD_PADDING, // { horizontal: 14 }
  getFieldSx, // input sizing sx for OutlinedInput/Select/Autocomplete
  getFormControlSx, // FormControl-level sizing sx
  getControlContainerSx, // alignment container for Checkbox/Switch/Slider/...
  getComboFieldBorderSx, // bordered-container styles (PhoneField-like combos)
  getContainerInputPadding, // padding for custom bordered containers
  FieldShell, // FormControl + FormLabel + FormHelperText chrome
} from '@galaxy-dev/metadsl-adapter-mui';

function MyCustomField(props: MyCustomFieldProps) {
  return (
    <FieldShell label={label} helperText={helperText} error={error} disabled={disabled}>
      <MuiOutlinedInput sx={getFieldSx(size, style)} /* ... */ />
    </FieldShell>
  );
}
```

## Notable Field Capabilities

- **Formatted amount input**: set `format` on an `amount` field to switch to a
  text input with live thousands-separator grouping (`8000` → `8,000`) and
  caret preservation. The value contract is a raw decimal string (separators
  stripped) end-to-end - no float parsing anywhere, and `min`/`max` clamp via
  exact decimal string comparison (pass string bounds for values beyond float
  precision), so crypto amounts are precision-safe. Form state must
  round-trip the emitted string verbatim (the DSL runtime does). Without
  `format` the field stays a native number input emitting numbers.
- **`clearable`**: `text`, `password`, and `amount` fields can show a clear (×)
  button (themeable via the `dsl-field__clear` class or a custom `clearIcon`).
- **Real password field**: `fieldType: 'password'` renders a masked input with
  a show/hide toggle (`showToggle`, `autoComplete`).
- **Native input types**: `text` fields forward `inputType`
  (`email`/`url`/`tel`/`number`/`search`) to the input's `type`/`inputMode`;
  registered `email`/`number` fieldTypes do this automatically.
- **`inputFilter`**: per-character regex filter on `text` fields (e.g.
  `"[^0-9]"` strips digits from name fields).
- **`pickerVariant`**: `date`/`date-time`/`time` fields accept
  `'auto' | 'desktop' | 'mobile'`; use `'desktop'` when hosting forms inside a
  Shadow DOM where native mobile pickers break.

## Architecture

### 🚨 IMPORTANT: Adapter Component Design Principles

#### 1. DSL is Library-Agnostic

**DSL definitions are unified and contain NO library-specific properties.**

The DSL schema (ButtonDSL, FieldDSL, etc.) only defines common, universal properties that work across ALL adapters:

- ✅ `label`, `placeholder`, `helperText` - universal text fields
- ✅ `disabled`, `visible`, `readOnly` - universal states
- ✅ `variant: 'contained' | 'outlined' | 'text'` - universal button styles
- ❌ NO MUI-specific props (like `sx`, `color: 'inherit'`)
- ❌ NO Ant Design-specific props (like `danger`, `ghost`)

If you need library-specific customization, use the `props` field:

```json
{
  "type": "button",
  "label": "Submit",
  "variant": "contained",
  "props": {
    "sx": { "borderRadius": 2 }, // MUI specific
    "startIcon": "<SaveIcon />" // MUI specific
  }
}
```

#### 2. Adapters Should NOT Process i18n or Expressions

**Adapter components should NEVER process i18n or expressions themselves.**

All i18n translation and expression evaluation are handled by the runtime layer:

- **ComponentRenderer** - processes component props (label, text, title, etc.)
- **FieldRenderer** - processes field props (label, placeholder, helperText, defaultValue, etc.)

Adapter components receive **already processed props** (translated strings, evaluated booleans/numbers):

```tsx
// ❌ WRONG: Do NOT do this in adapter components
import type { ButtonDSL } from '@galaxy-dev/metadsl-core';

export interface ButtonProps extends ButtonDSL {
  // This would include I18nValue types which are NOT valid as ReactNode
}

export function Button({ label }: ButtonProps) {
  const { i18n } = useDSLContext();
  const translatedLabel = i18n.translate(label); // ❌ Don't translate here!
  return <MuiButton>{translatedLabel}</MuiButton>;
}

// ✅ CORRECT: Adapter components receive processed props
export interface ButtonProps {
  label?: string; // Already translated by ComponentRenderer
  loading?: boolean; // Already evaluated by ComponentRenderer
  disabled?: boolean; // Already evaluated by ComponentRenderer
  // ...
}

export function Button({ label, loading, disabled }: ButtonProps) {
  // Use props directly - they are already processed
  return (
    <MuiButton disabled={disabled}>{loading ? <CircularProgress size={20} /> : label}</MuiButton>
  );
}
```

**Why this matters:**

1. **Type Safety** - I18nValue (Record<string, string>) is not assignable to ReactNode
2. **Separation of Concerns** - Runtime handles logic, adapters handle presentation
3. **Performance** - Avoid duplicate processing
4. **Consistency** - Same processing logic for all adapters

**Exception:** Components that are NOT rendered through ComponentRenderer/FieldRenderer (like form containers that wrap FormRenderer) need to process their own props using `usePropsProcessor`.

## License

MIT
