@gxp-dev/mod-ui
| Install | |
|---|---|
npm install @gxp-dev/mod-ui |
|
| Latest Version: | 2.0.41 |
| Vue: | 3 |
| License: | MIT |
| Last Updated: | Aug 20, 2026 |
| Links: | npm |
@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-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):
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.
Related Packages
Vue 3 UI component library for GXP platform applications, built on shadcn-vue, r...
A light library of components based on Tailwind Elements and Tailwind for Vuejs...
Version History
| Version | Released | License |
|---|---|---|
| 2.0.41 | MIT | |
| 2.0.40 | MIT | |
| 2.0.39 | MIT | |
| 2.0.38 | MIT | |
| 2.0.37 | MIT | |
| 2.0.35 | MIT | |
| 2.0.34 | MIT | |
| 2.0.33 | MIT | |
| 2.0.32 | MIT | |
| 2.0.31 | MIT | |
| 2.0.30 | MIT | |
| 2.0.29 | MIT | |
| 2.0.28 | MIT | |
| 2.0.27 | MIT | |
| 2.0.26 | MIT | |
| 2.0.25 | MIT | |
| 2.0.24 | MIT | |
| 2.0.23 | MIT | |
| 2.0.22 | MIT | |
| 2.0.21 | MIT | |
| 2.0.20 | MIT | |
| 2.0.19 | MIT | |
| 2.0.18 | MIT | |
| 2.0.17 | MIT | |
| 2.0.16 | MIT | |
| 2.0.15 | MIT | |
| 2.0.14 | MIT | |
| 2.0.12 | MIT | |
| 2.0.10 | MIT | |
| 2.0.8 | MIT |
Showing the latest 30 of 35. See every release on npm