@gxp-dev/mod-ui
v2.0.14
Published
JSON-driven Vue 3 module UI component library for the GxP platform. Provides shadcn-vue primitives, a templating engine (IndexPage / ShowPage), card/element renderers, and a FormBuilder.
Maintainers
Readme
@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 for details and known limitations.
Two APIs in one library
Props-based (use any composition directly, no JSON, no schema):
<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):
<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
- Compositions take normal Vue props. Every renderer under
components/compositions/is a self-contained Vue component withdefineModel<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. - The engine is the JSON ↔ props bridge.
ElementRendererandCardRendererlook up the matching composition byelement.type/card.cardType, calluseFormData(element.name)for the form binding, strip schema-only keys, and forward the rest as v-bind props. Compositions never import schema types. - TypeScript definitions are the schema source. Each element
category has a single dot-notation discriminator (
"input.text","picker.date-picker", …) — no separatestylefield. The runtime registry and adapters derive from these definitions. - Tree-shakeable. Direct deep imports work alongside the barrel.
- Code-split. The engine resolves the composition path via
import.meta.glob+defineAsyncComponent— aShowPageonly downloads the elements it actually renders. - No mdbootstrap, no Bootstrap CSS. Pure Tailwind v4 + CSS variables. reka-ui for a11y primitives.
- Themes are CSS variables. Override any token in
:root(or a scoped class like.kiosk) and the whole library retunes.
Install
pnpm add @gxp-dev/mod-uivue, 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):
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:
import "@gxp-dev/mod-ui/styles"
import "@gxp-dev/mod-ui/styles/themes/dark.css"<body class="dark">...</body>To override individual design tokens:
:root {
--primary: oklch(0.5 0.2 270);
--radius: 1rem;
}Develop
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:
import { v1ToV2, v2ToV1, normalisePageConfig } from "@gxp-dev/mod-ui"v2 schema notes
- Elements use a single
typefield in dot notation:"input.text","picker.date-picker","option-select.select", etc. The separatestylefield that v2 used early on is gone (it collided with HTMLstyleattribute fallthrough). - A small
legacyPropRemapshim handles known legacy v2 key renames (selectOptions→nestedElements,triggerButtonLabel→triggerLabel, etc.) and variant aliases (variant: "primary"→"default") automatically at the engine boundary.
