# @gxp-dev/mod-ui

JSON-driven Vue 3 admin component library for the GxP platform.
Provides shadcn-vue primitives, props-based composition components,
a templating engine (`IndexPage` / `ShowPage`), and a `FormBuilder`
ecosystem. Designed to be a drop-in replacement for the legacy
`Components/Mod` and `Components/Mod/v2` systems in the experience
portal.

> Status: **2.0.0-alpha.0** — feature-complete for rendering and JSON
> drop-in. 1410 passing tests across 124 files. See
> [CHANGELOG.md](./CHANGELOG.md) for details and known limitations.

## Two APIs in one library

**Props-based** (use any composition directly, no JSON, no schema):

```vue
<script setup lang="ts">
import { TextInput, DatePicker, Button } from "@gxp-dev/mod-ui"
</script>

<template>
  <TextInput v-model="firstName" label="First name" required />
  <DatePicker v-model="startDate" label="Start date" />
  <Button variant="default" @click="save">Save</Button>
</template>
```

**JSON-driven** (pass a config, the engine resolves and renders):

```vue
<script setup lang="ts">
import { ShowPage } from "@gxp-dev/mod-ui"
import type { ShowPageConfig } from "@gxp-dev/mod-ui"

const config: ShowPageConfig = {
  title: "User",
  tabsList: [
    {
      title: "Details",
      tabId: "details",
      cards: [
        {
          // v1 (`fields_list`) is auto-translated at the boundary.
          // Native v2: `cardType: "element-list"`.
          type: "fields_list",
          fieldsList: [
            { type: "text", name: "first_name", label: "First name" },
            { type: "email", name: "email", label: "Email" },
          ],
        },
      ],
    },
  ],
  updateConfig: {
    update: { route: "api.users.update", method: "patch" },
  },
}
</script>

<template>
  <ShowPage :config="config" :item="user" />
</template>
```

The same `ShowPage` accepts both v1 and v2 schemas — the adapter
detects the shape at the boundary.

## What's in the box

| Layer | What lives here | Purpose |
| --- | --- | --- |
| `components/ui/` | 30 shadcn-vue primitives (Button, Input, Dialog, Select, FieldFrame, …) | Atom-level building blocks built on reka-ui. |
| `components/compositions/elements/` | 79 props-based element compositions (TextInput, DatePicker, ColorPicker, RichTextEditor, ChartDisplay, …) | Self-contained Vue components with `defineModel('modelValue')`. Mountable in any app. |
| `components/compositions/cards/` | 7 card compositions (ElementList, DataTable, CardList, TabList, Info, GridView, ElementDisplay) | Container compositions that render resolved children. |
| `pages/` | `IndexPage`, `ShowPage` | Top-level templating consumers; take a JSON config. |
| `definitions/` | TypeScript schema source of truth (no Vue) | `Defaults`, `EditConfig`, and interfaces per element/card. |
| `engine/` | `ElementRenderer`, `CardRenderer`, loaders, `legacyPropRemap` | JSON → props dispatch. Resolves type, calls `useFormData`, strips schema-only keys, forwards as v-bind. |
| `adapters/` | v1 ↔ v2 schema translation, `normalisePageConfig` | Drop-in support for legacy v1 JSON. |
| `stores/` | 9 Pinia stores | Per-form data binding; per-builder schema state. |

## Architectural principles

1. **Compositions take normal Vue props.** Every renderer under
   `components/compositions/` is a self-contained Vue component with
   `defineModel<T>('modelValue')`, typed individual props, and standard
   events. Each is directly mountable in any Vue app without any
   knowledge of the JSON schema or Pinia.
2. **The engine is the JSON ↔ props bridge.** `ElementRenderer` and
   `CardRenderer` look up the matching composition by `element.type` /
   `card.cardType`, call `useFormData(element.name)` for the form
   binding, strip schema-only keys, and forward the rest as v-bind
   props. Compositions never import schema types.
3. **TypeScript definitions are the schema source.** Each element
   category has a single dot-notation discriminator (`"input.text"`,
   `"picker.date-picker"`, …) — no separate `style` field. The
   runtime registry and adapters derive from these definitions.
4. **Tree-shakeable.** Direct deep imports work alongside the barrel.
5. **Code-split.** The engine resolves the composition path via
   `import.meta.glob` + `defineAsyncComponent` — a `ShowPage` only
   downloads the elements it actually renders.
6. **No mdbootstrap, no Bootstrap CSS.** Pure Tailwind v4 + CSS
   variables. reka-ui for a11y primitives.
7. **Themes are CSS variables.** Override any token in `:root` (or a
   scoped class like `.kiosk`) and the whole library retunes.

## Install

```bash
pnpm add @gxp-dev/mod-ui
```

`vue`, `pinia`, and `reka-ui` are peer dependencies — modern package
managers (pnpm 9+, npm 7+, yarn 3+) auto-install them, so the single
command above is usually enough. If your manager warns about
unresolved peers, add them explicitly: `pnpm add vue pinia reka-ui`.

## Styles

Import the library's stylesheet once in your app entry (e.g. `main.ts` or `app.js`):

```ts
import "@gxp-dev/mod-ui/styles"
```

This loads the Tailwind theme — CSS custom properties for color, radius, shadow, etc. Without it every component will be unstyled.

### Themes

The library ships three built-in themes:

| File | Class | Description |
| --- | --- | --- |
| (default, included in `styles`) | — | Light theme |
| `@gxp-dev/mod-ui/styles/themes/dark.css` | `.dark` | Dark theme |
| `@gxp-dev/mod-ui/styles/themes/kiosk.css` | `.kiosk` | High-contrast kiosk theme |

To use dark or kiosk mode, import the extra stylesheet and add the class to a parent element:

```ts
import "@gxp-dev/mod-ui/styles"
import "@gxp-dev/mod-ui/styles/themes/dark.css"
```

```html
<body class="dark">...</body>
```

To override individual design tokens:

```css
:root {
  --primary: oklch(0.5 0.2 270);
  --radius: 1rem;
}
```

## Develop

```bash
pnpm install
pnpm dev            # vite dev server (rarely used; storybook is the demo)
pnpm storybook      # storybook on :6006
pnpm test           # vitest watch
pnpm test:run       # one-shot
pnpm typecheck      # vue-tsc --noEmit
pnpm build          # library build (dist/)
```

## Migrating from `Components/Mod` (v1) or `Components/Mod/v2`

The `adapters/` module accepts both shapes and emits the v2-internal
structure. `ShowPage` / `IndexPage` apply the adapter automatically
at the page boundary, so existing v1 JSON renders without code
changes in the consumer.

For direct schema migration without rendering:

```ts
import { v1ToV2, v2ToV1, normalisePageConfig } from "@gxp-dev/mod-ui"
```

### v2 schema notes

- Elements use a single `type` field in dot notation: `"input.text"`,
  `"picker.date-picker"`, `"option-select.select"`, etc. The separate
  `style` field that v2 used early on is gone (it collided with HTML
  `style` attribute fallthrough).
- A small `legacyPropRemap` shim handles known legacy v2 key renames
  (`selectOptions` → `nestedElements`, `triggerButtonLabel` →
  `triggerLabel`, etc.) and variant aliases (`variant: "primary"` →
  `"default"`) automatically at the engine boundary.
