npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-ubiquitous

v1.3.0

Published

JSON-driven UI renderer — generate forms, pages, multi-step stages, carousels and list-detail views from pure config objects.

Readme

react-ubiquitous

The frontend is just a human-readable version of the API response.

Tailwind made CSS-limited developers build professional web pages. Shadcn made Tailwind-limited developers build professional web pages. react-ubiquitous aims to make any API response a professional web page.

npm version bundle size license TypeScript

react-ubiquitous is a JSON-driven UI renderer for React. You describe your UI as a plain TypeScript config object — layouts, fields, pages, section types — and the library renders it. No bespoke components, no role checks in JSX, no deploy to change what a user sees. The backend controls what data arrives; react-ubiquitous controls how it looks.


Why react-ubiquitous?

The frontend has become too complicated

At some point, building a frontend stopped being about presenting information and started being about managing complexity that doesn't belong there.

Access control logic creeps into components. Role checks scatter across route guards, conditional renders, and hidden buttons. Theming systems grow into their own runtime. Feature flags, permission matrices, tenant overrides — all running in the browser, all making the frontend harder to reason about, test, and change.

Here's a simpler mental model:

The frontend is just a human-readable version of the API response.

If the backend returns a field, the user should see it. If they shouldn't see it, the backend shouldn't return it. Access control belongs on the server — not in a maze of if (user.role === 'admin') guards scattered across your JSX.

This has a liberating consequence: the frontend no longer needs to know about permissions. It just needs to know how to render what it receives. Any data that reaches the browser is, by definition, data the user is allowed to see.

The contract model

This philosophy creates a clean, enforceable contract:

  • The frontend defines patterns — layouts, field types, section structures, interaction flows
  • The backend adheres to those patterns — it returns data shaped to match the UI config
  • When the two align, everything simply works — no bespoke rendering logic, no role-specific component trees, no deploy needed to change what a user sees

react-ubiquitous is built entirely around this contract. A stage config is just data. The renderer just renders it. What the backend sends, the user sees — shaped by the layout patterns you've defined once, reused everywhere.

Practically, this means

  • No access control in the UI — serve different configs to different users from the backend
  • No deploy to change a layout — update the JSON config, the UI updates automatically
  • No per-tenant component forks — one renderer, many configs
  • No bespoke forms — describe fields as data, let the renderer handle the rest
  • No drift between design and code — the visual builder exports the exact config the renderer consumes

The companion visual builder (included in the demo) lets you design stages interactively and export the JSON config, closing the loop between designer, developer, and backend in one portable format.


Table of Contents


Installation

npm install react-ubiquitous

Peer dependencies — React ≥ 18 is required:

npm install react react-dom

Quick Start

import 'react-ubiquitous/styles.css'   // ← required: CSS variables + Tailwind utilities
import { UIStage } from 'react-ubiquitous'
import type { UIStageConfig } from 'react-ubiquitous'

const config: UIStageConfig = {
  id: 'sign-up',
  title: 'Create Account',
  pages: [
    {
      id: 'personal',
      title: 'Personal Info',
      order: 0,
      sections: [
        {
          id: 'name-row',
          layout: 'grid',
          order: 0,
          gridTemplateColumns: 'repeat(2, 1fr)',
          gap: '1rem',
          elements: [
            {
              id: 'first',
              name: 'firstName',
              type: 'input',
              inputType: 'text',
              label: 'First name',
              order: 0,
              validations: [{ rule: 'required' }],
            },
            {
              id: 'last',
              name: 'lastName',
              type: 'input',
              inputType: 'text',
              label: 'Last name',
              order: 1,
              validations: [{ rule: 'required' }],
            },
          ],
        },
      ],
    },
  ],
}

export default function App() {
  return (
    <UIStage
      config={config}
      onChange={(name, value) => console.log(name, '=', value)}
    />
  )
}

How It Works

UIStageConfig
 └─ UIPageConfig[]           (rendered as a tab bar)
     └─ UISectionConfig[]    (flex | grid | hero | media | list-detail |
      |                       tree-view | chat | navbar | sidebar |
      |                       breadcrumbs | pagination | stepper | tabs | iframe)
         └─ UIElementConfig[]    (every HTML form control)

Each layer is a plain TypeScript object discriminated by a single string field:

  • Stage — top-level container with a title and a list of pages
  • Page — a named tab; contains an ordered list of sections
  • Section — a layout container; renders its elements in flex, grid, or a specialised display mode
  • Element — a single interactive or display control (input, select, button, etc.)

Section Types

Every section shares a set of common fields:

| Field | Type | Description | |---|---|---| | id | string | Unique identifier | | layout | string | Discriminant — see types below | | title | string | Optional heading above the section | | description | string | Optional subtitle | | order | number | Sort order within the page | | className | string | Extra CSS classes on the wrapper | | style | CSSProperties | Inline styles on the wrapper | | elements | UIElementConfig[] | Fields rendered inside the section |


Grid Section

Renders elements in a CSS grid.

{
  id: 'details',
  layout: 'grid',
  order: 0,
  gridTemplateColumns: 'repeat(3, 1fr)',  // any CSS grid-template-columns value
  gridTemplateRows: 'auto',
  gap: '1rem',
  rowGap: '0.5rem',
  columnGap: '1.5rem',
  alignItems: 'start',    // 'start' | 'end' | 'center' | 'stretch'
  justifyItems: 'stretch',
  elements: [...],
}

Flex Section

Renders elements in a CSS flexbox.

{
  id: 'toolbar',
  layout: 'flex',
  order: 0,
  flexDirection: 'row',         // 'row' | 'column' | 'row-reverse' | 'column-reverse'
  flexWrap: 'wrap',             // 'nowrap' | 'wrap' | 'wrap-reverse'
  justifyContent: 'flex-start', // all CSS justify-content values
  alignItems: 'center',
  gap: '0.75rem',
  elements: [...],
}

Hero Section

A full-width banner with a background (gradient, solid colour, or image URL), title, subtitle, description, and an optional CTA link.

{
  id: 'banner',
  layout: 'hero',
  order: 0,
  title: 'Welcome back',
  subtitle: 'Here is what happened while you were away.',
  description: 'Your dashboard shows the latest activity…',

  // Background
  backgroundType: 'gradient',      // 'gradient' | 'color' | 'image'
  gradientFrom: '#6366f1',
  gradientTo: '#1e293b',
  gradientDirection: 'to bottom right',
  // backgroundType: 'color', backgroundColor: '#1e293b'
  // backgroundType: 'image', backgroundImage: 'https://…'

  // Optional dark overlay (useful over images)
  overlay: true,
  overlayOpacity: 40,              // 0–100

  // Layout
  minHeight: '320px',
  textAlign: 'center',             // 'left' | 'center' | 'right'
  verticalAlign: 'center',         // 'top' | 'center' | 'bottom'

  // CTA link
  linkText: 'Learn more',
  linkUrl: 'https://example.com',
  linkRelative: false,             // true → same tab, no rel attr
  elements: [],
}

Media Carousel Section

A responsive, accessible image/video carousel. Items can be images, videos, or a mix. Supports aspect-ratio control, arrow buttons, and dot navigation.

{
  id: 'gallery',
  layout: 'media',
  order: 0,
  aspectRatio: '16/9',   // any CSS aspect-ratio value e.g. '4/3', '1/1'
  showArrows: true,
  showDots: true,
  items: [
    {
      id: 'img-1',
      type: 'image',
      url: 'https://example.com/photo.jpg',
      alt: 'A product photo',
      caption: 'Shown as an overlay at the bottom of the slide',
    },
    {
      id: 'vid-1',
      type: 'video',
      url: 'https://example.com/demo.mp4',
      caption: 'Product walkthrough',
    },
  ],
  elements: [],
}

List & Detail Section

A master-list / detail-panel layout — the canonical pattern for CRM records, settings screens, and admin dashboards. The left pane shows a searchable, paginated list; the right pane renders one or more detail pages each containing grid/flex sections with fields.

{
  id: 'contacts',
  layout: 'list-detail',
  order: 0,

  // ── Left pane ─────────────────────────────────────────────────────────
  listTitle: 'Contacts',
  listWidth: '280px',
  pageSize: 50,

  listItems: [
    { id: 'c1', label: 'Alice Smith',  sublabel: '[email protected]', badge: 'Admin' },
    { id: 'c2', label: 'Bob Jones',    sublabel: '[email protected]'                   },
    { id: 'c3', label: 'Carol Brown',  sublabel: '[email protected]', avatar: 'CB'   },
  ],

  // ── API endpoints (appended automatically at runtime) ─────────────────
  listEndpoint: {
    url: 'https://api.example.com/contacts',
    fromParam: 'from',  toParam: 'to',  sortParam: 'sort',
    fromValue: 1,       toValue: 50,    sortValue: 'asc',
    // resolves to: /contacts?from=1&to=50&sort=asc
  },
  filterEndpoint: {
    url: 'https://api.example.com/contacts/search',
    queryParam: 'query',
    // resolves to: /contacts/search?query=alice
  },
  detailEndpoint: {
    url: 'https://api.example.com/contacts',
    selectedParam: 'selected',
    // resolves to: /contacts?selected=c1
  },

  // ── Right pane: one or more detail pages ──────────────────────────────
  // Multiple pages → tab bar appears above the detail area automatically.
  detailPages: [
    {
      id: 'dp-overview',
      title: 'Overview',
      order: 0,
      sections: [
        {
          id: 'dp-name',
          layout: 'grid',
          order: 0,
          gridTemplateColumns: 'repeat(2, 1fr)',
          gap: '1rem',
          elements: [
            { id: 'dp-first', name: 'firstName', type: 'input', inputType: 'text', label: 'First name', order: 0 },
            { id: 'dp-last',  name: 'lastName',  type: 'input', inputType: 'text', label: 'Last name',  order: 1 },
          ],
        },
      ],
    },
    {
      id: 'dp-notes',
      title: 'Notes',
      order: 1,
      sections: [
        {
          id: 'dp-notes-sec',
          layout: 'flex',
          order: 0,
          flexDirection: 'column',
          gap: '0.75rem',
          elements: [
            { id: 'dp-note', name: 'notes', type: 'textarea', label: 'Notes', rows: 6, order: 0 },
          ],
        },
      ],
    },
  ],

  elements: [],
}

For very large datasets (thousands of rows), enable virtualization:

{
  "id": "big-list",
  "layout": "list-detail",
  "virtualScrolling": true,
  "virtualListHeight": 600,
  "listItems": [...]
}

See Virtualization for Large Lists for details.


Tree & Detail Section

A hierarchical tree view with a detail panel — ideal for representing organisational structures, file systems, or nested categories. The left pane shows an expandable/collapsible tree of nodes; the right pane renders the detail panel for the selected node (same detailPages / flat elements pattern as list-detail).

{
  id: 'org-tree',
  layout: 'tree-view',
  order: 0,

  // ── Left pane ─────────────────────────────────────────────────────────
  treeTitle: 'Organisation',
  treeWidth: '280px',
  treeMode: 'easy',       // 'easy' | 'compact'  (default: 'easy')

  treeNodes: [
    {
      id: 'eng',
      label: 'Engineering',
      badge: 'Dept',
      children: [
        { id: 'fe', label: 'Frontend', sublabel: '12 members' },
        { id: 'be', label: 'Backend',  sublabel: '10 members' },
      ],
    },
    {
      id: 'design',
      label: 'Design',
      badge: 'Dept',
    },
  ],

  // ── Right pane — same options as list-detail ──────────────────────────
  detailPages: [...],   // optional — falls back to flat elements
  elements: [],
}

| Field | Type | Default | Description | |---|---|---|---| | treeTitle | string | — | Heading above the tree pane | | treeWidth | string | '260px' | CSS width of the tree pane | | treeMode | 'easy' \| 'compact' | 'easy' | 'easy' = larger rows; 'compact' = tighter spacing | | treeNodes | TreeViewNode[] | [] | Root-level nodes (nested via children) | | detailPages | DetailPage[] | — | Multi-page detail panel (same as list-detail) | | elements | UIElementConfig[] | [] | Flat elements for a simple detail panel |


Chat Section

A chat window with a searchable conversations list on the left and an active chat panel on the right. The right pane displays messages as speech bubbles and provides a textarea + send button at the bottom. Reuses the same list rendering pattern as list-detail.

{
  id: 'team-chat',
  layout: 'chat',
  order: 0,

  // ── Left pane ─────────────────────────────────────────────────────────
  listTitle: 'Messages',
  listWidth: '280px',
  currentUserName: 'You',

  conversations: [
    {
      id: 'g1',
      label: 'Design Team',
      sublabel: 'Alice: Looks great! 🎉',
      avatar: 'DT',
      badge: '3',
      messages: [
        { id: 'm1', text: 'Hey team!', sender: 'Alice', role: 'other', timestamp: '2024-01-15T09:00:00Z', avatar: 'AL' },
        { id: 'm2', text: 'Hi Alice!', sender: 'You',   role: 'me',    timestamp: '2024-01-15T09:01:00Z' },
      ],
    },
    {
      id: 'dm1',
      label: 'Bob Smith',
      sublabel: 'Can you review my PR?',
      avatar: 'https://i.pravatar.cc/150?img=3',
      messages: [],
    },
  ],

  // ── Right pane ────────────────────────────────────────────────────────
  inputPlaceholder: 'Type a message…',
  sendButtonText: 'Send',

  elements: [],
}

| Field | Type | Default | Description | |---|---|---|---| | listTitle | string | 'Messages' | Heading shown above the conversations pane | | listWidth | string | '280px' | CSS width of the conversations pane | | conversations | ChatConversation[] | [] | Conversation / group entries | | inputPlaceholder | string | 'Type a message…' | Placeholder for the message textarea | | sendButtonText | string | 'Send' | Label of the send button | | currentUserName | string | 'You' | Display name used for sent messages |

ChatConversation fields

| Field | Type | Description | |---|---|---| | id | string | Unique identifier | | label | string | Display name | | sublabel | string | Last message preview | | avatar | string | Avatar URL or ≤ 2-char initials | | badge | string | Optional badge text (e.g. unread count) | | messages | ChatMessage[] | Pre-seeded messages for this conversation |

ChatMessage fields

| Field | Type | Description | |---|---|---| | id | string | Unique identifier | | text | string | Message body | | sender | string | Sender's display name | | role | 'me' \| 'other' | 'me' = right-aligned; 'other' = left-aligned | | timestamp | string | ISO 8601 timestamp (optional) | | avatar | string | Avatar URL or ≤ 2-char initials for the sender |


Navbar / Header Section

A top app bar with a logo, navigation links, and an auto-generated mobile hamburger menu. Supports light and dark themes, and static, sticky, or fixed positioning.

{
  id: 'top-nav',
  layout: 'navbar',
  order: 0,

  // Logo — text, image, or both
  logoText: 'My App',
  logoUrl: 'https://example.com/logo.svg',   // optional

  // Navigation links
  links: [
    { id: 'home',    label: 'Home',    href: '/',        active: true },
    { id: 'about',   label: 'About',   href: '/about' },
    { id: 'contact', label: 'Contact', href: '/contact' },
  ],

  position: 'sticky',   // 'static' | 'sticky' | 'fixed'  (default: 'static')
  theme:    'light',    // 'light' | 'dark'                (default: 'light')
  elements: [],
}

Sidebar / Drawer Section

A collapsible side navigation panel. Items support nested children (rendered with indentation) and an active state highlight.

{
  id: 'main-sidebar',
  layout: 'sidebar',
  order: 0,
  title: 'Navigation',
  width: '260px',           // CSS width when expanded (default: '260px')
  defaultCollapsed: false,  // start collapsed (default: false)
  collapsible: true,        // show toggle button (default: true)

  items: [
    { id: 'dash',     label: 'Dashboard',  href: '/dash',     active: true },
    { id: 'projects', label: 'Projects',   href: '/projects',
      children: [
        { id: 'active',   label: 'Active',   href: '/projects/active' },
        { id: 'archived', label: 'Archived', href: '/projects/archived' },
      ],
    },
    { id: 'settings', label: 'Settings',   href: '/settings' },
  ],
  elements: [],
}

Breadcrumbs Section

A hierarchical location trail. The last item is rendered as plain text (current page); all preceding items are rendered as links.

{
  id: 'breadcrumb-trail',
  layout: 'breadcrumbs',
  order: 0,
  separator: '/',   // default '/'

  items: [
    { id: 'home',     label: 'Home',     href: '/' },
    { id: 'projects', label: 'Projects', href: '/projects' },
    { id: 'current',  label: 'React Ubiquitous' },  // last item — no href
  ],
  elements: [],
}

Pagination Section

Page-number controls for navigating long lists. Manages active-page state internally; currentPage sets the initial page.

{
  id: 'results-pager',
  layout: 'pagination',
  order: 0,
  totalItems: 243,      // total number of items across all pages
  pageSize: 20,         // items per page (default: 10)
  currentPage: 1,       // initial page, 1-based (default: 1)
  maxPageButtons: 7,    // max page-number buttons before truncation (default: 7)
  showPrevNext: true,   // show ‹ / › buttons (default: true)
  showFirstLast: false, // show « / » jump-to-end buttons (default: false)
  elements: [],
}

Stepper Section

A multi-step wizard progress indicator. Manages currentStep state internally; steps are clickable to navigate backward. Step status is derived from currentStep unless an explicit status override is supplied on the step.

{
  id: 'checkout-steps',
  layout: 'stepper',
  order: 0,
  orientation: 'horizontal',  // 'horizontal' | 'vertical' (default: 'horizontal')
  currentStep: 1,             // 0-based initial step (default: 0)

  steps: [
    { id: 's1', label: 'Cart',     description: 'Review your items' },
    { id: 's2', label: 'Shipping', description: 'Enter your address' },
    { id: 's3', label: 'Payment',  description: 'Confirm and pay' },
    { id: 's4', label: 'Done',     description: 'Order confirmed' },
  ],
  elements: [],
}

Standalone Tabs Section

A general-purpose tabbed content container. Unlike UIStage (which uses pages as top-level tabs), this section renders tabs anywhere inside a page — including nested inside another section. Each tab can contain its own sections or a flat list of elements.

{
  id: 'feature-tabs',
  layout: 'tabs',
  order: 0,
  defaultTabId: 'tab-overview',

  tabs: [
    {
      id: 'tab-overview',
      label: 'Overview',
      // Sections with full grid/flex layout support
      sections: [
        {
          id: 'overview-grid',
          layout: 'grid',
          order: 0,
          gridTemplateColumns: 'repeat(2, 1fr)',
          gap: '1rem',
          elements: [
            { id: 'ov-name', name: 'name', type: 'input', inputType: 'text', label: 'Name', order: 0 },
          ],
        },
      ],
    },
    {
      id: 'tab-notes',
      label: 'Notes',
      // Flat elements when no sections are needed
      elements: [
        { id: 'note', name: 'notes', type: 'textarea', label: 'Notes', rows: 5, order: 0 },
      ],
    },
  ],
  elements: [],
}

Accordion Section

An expand/collapse accordion with one or more independently labelled panels. By default only one panel may be open at a time (allowMultiple: false). Each panel can contain its own nested sections (grid/flex) or a flat list of elements. defaultOpen: true on a panel causes it to start open.

{
  id: 'faq-accordion',
  layout: 'accordion',
  order: 0,
  allowMultiple: false,   // true = multiple panels may be open (default: false)

  panels: [
    {
      id: 'p1',
      label: 'What is react-ubiquitous?',
      description: 'A brief answer',    // optional subtitle in the header
      defaultOpen: true,                 // open on first render (default: false)
      elements: [
        { id: 'body1', name: 'body1', type: 'label', label: 'It is a JSON-driven UI renderer.', order: 0 },
      ],
    },
    {
      id: 'p2',
      label: 'How do I install it?',
      // sections: [ ... ]   ← use sections for rich grid/flex layouts
      elements: [
        { id: 'body2', name: 'body2', type: 'label', label: 'Run npm install react-ubiquitous.', order: 0 },
      ],
    },
  ],
  elements: [],
}

Collapse / Spoiler Section

A single expand/collapse toggle that hides content until the user clicks the trigger. Simpler than an Accordion when only one collapsible region is needed.

{
  id: 'advanced-options',
  layout: 'collapse',
  order: 0,
  label: 'Advanced options',     // trigger label (falls back to `title`)
  description: 'Rarely changed', // optional subtitle on the trigger
  defaultOpen: false,            // start open (default: false)
  icon: true,                    // show chevron icon (default: true)

  elements: [
    { id: 'timeout', name: 'timeout', type: 'input', inputType: 'number', label: 'Timeout (ms)', order: 0 },
  ],
}

Divider Section

A visual separator line with an optional centred text label. Useful for grouping sections or form fields with a lightweight visual boundary.

{
  id: 'section-sep',
  layout: 'divider',
  order: 0,
  label: 'OR',             // optional centred label (omit for a plain line)
  orientation: 'horizontal', // 'horizontal' | 'vertical' (default: 'horizontal')
  variant: 'solid',          // 'solid' | 'dashed' | 'dotted' (default: 'solid')
  elements: [],
}

Card Section

A generic content container with optional header and footer slots. title and description populate the header; elements populate the body; footerElements populate a distinct footer row.

{
  id: 'user-card',
  layout: 'card',
  order: 0,
  title: 'Profile',               // header title (optional)
  description: 'Your public info', // header subtitle (optional)
  bordered: true,                   // border around the card (default: true)
  shadow: 'sm',                     // false | 'sm' | 'md' | 'lg' (default: 'sm')
  padded: true,                     // body padding (default: true)

  // Card body
  elements: [
    { id: 'name',  name: 'name',  type: 'input', inputType: 'text',  label: 'Name',  order: 0 },
    { id: 'email', name: 'email', type: 'input', inputType: 'email', label: 'Email', order: 1 },
  ],

  // Card footer (e.g. action buttons)
  footerElements: [
    { id: 'save',   name: 'save',   type: 'button', label: 'Save',   order: 0 },
    { id: 'cancel', name: 'cancel', type: 'button', label: 'Cancel', order: 1 },
  ],
}

Iframe / Embed Section

Embeds an external URL in an <iframe>. The rendered src is recomputed automatically whenever queryParams changes — update the object and the iframe navigates to the new URL without a full component remount.

{
  id: 'analytics-embed',
  layout: 'iframe',
  order: 0,
  title: 'Sales Dashboard',         // optional heading above the frame

  // Base URL
  src: 'https://analytics.example.com/dashboard',

  // Merged onto the URL as ?key=value&…
  // Changing this object causes the iframe to navigate to the new URL.
  queryParams: {
    tab: 'revenue',
    year: 2025,
    region: 'EU',
  },
  // resolves to: …/dashboard?tab=revenue&year=2025&region=EU

  frameHeight: '600px',             // default '480px'
  frameWidth: '100%',               // default '100%'
  frameTitle: 'Sales analytics',    // accessible title (required for screen readers)
  sandbox: 'allow-scripts allow-same-origin', // omit for no restrictions
  allowFullscreen: true,            // default true
  showLoader: true,                 // spinner while loading (default true)
  elements: [],
}

To change a query parameter at runtime, update queryParams in your stage config — no page reload needed:

// Switch from 2025 to 2026 — the iframe src updates immediately
section.queryParams = { ...section.queryParams, year: 2026 }

Chart Section

Responsive, zero-dependency SVG charts that inherit the shadcn/tailwind design-token palette. Set layout: 'chart'.

Chart types (chartType):

| Value | Description | |---|---| | 'bar' | Grouped vertical bar chart | | 'line' | Multi-series line chart with data-point dots | | 'area' | Line chart with gradient-filled area below the curve | | 'pie' | Pie chart with built-in legend | | 'donut' | Ring chart with center total label | | 'radar' | Spider / polygon chart with grid polygons | | 'scatter' | X / Y scatter plot |

Key fields:

| Field | Type | Default | Description | |---|---|---|---| | chartType | string | — | Required — selects the chart variant | | data | ChartDataPoint[] | — | Required — array of { label, ...seriesKeys } objects | | series | ChartSeries[] | [{ key: 'value' }] | Named series: { key, label?, color? } | | showGrid | boolean | true | Horizontal grid lines | | showLegend | boolean | auto | Show series legend (auto-on for ≥ 2 series) | | showLabels | boolean | false | Value labels on bars / points | | height | number | 300 | Chart height in pixels |

const config: ChartSectionConfig = {
  layout: 'chart',
  id: 'revenue',
  chartType: 'bar',
  title: 'Monthly Revenue',
  elements: [],
  data: [
    { label: 'Jan', revenue: 42000, cost: 31000 },
    { label: 'Feb', revenue: 55000, cost: 38000 },
    { label: 'Mar', revenue: 49000, cost: 34000 },
  ],
  series: [
    { key: 'revenue', label: 'Revenue' },
    { key: 'cost',    label: 'Cost' },
  ],
  showGrid: true,
  height: 320,
}

All charts are responsive (SVG viewBox), theme-aware (--chart-1..5 CSS vars), and dark-mode ready.


Charts

react-ubiquitous ships seven zero-dependency SVG chart types that any consuming project can use. All charts are:

  • Responsive — SVG viewBox fills the container width automatically
  • Theme-aware — consume --chart-1--chart-5 CSS variables from the shadcn/Tailwind palette
  • Dark-mode ready — no hardcoded colours
  • Interactive — hover tooltips on every chart type (exact values, labels, percentages)

How charts are exposed

All charts are rendered through UISection with layout: 'chart'. There is no separate <BarChart /> etc. — the chart variant is selected with the chartType field:

import 'react-ubiquitous/styles.css'   // ← required: CSS variables + Tailwind utilities
import { UISection } from 'react-ubiquitous'
import type { ChartSectionConfig } from 'react-ubiquitous'

const config: ChartSectionConfig = {
  id: 'my-chart',
  layout: 'chart',
  chartType: 'bar',       // ← swap this for any chart type
  title: 'Monthly Revenue',
  data: [
    { label: 'Jan', value: 42000 },
    { label: 'Feb', value: 55000 },
    { label: 'Mar', value: 49000 },
  ],
  elements: [],
}

export function MyChart() {
  return <UISection config={config} />
}

Bar chart

chartType: 'bar' — vertical grouped bars. Supports single and multi-series.

const config: ChartSectionConfig = {
  id: 'revenue-bar',
  layout: 'chart',
  chartType: 'bar',
  title: 'Monthly Revenue',
  showGrid: true,
  showLabels: false,   // set true to render value labels above bars
  height: 300,
  data: [
    { label: 'Jan', value: 42000 },
    { label: 'Feb', value: 55000 },
    { label: 'Mar', value: 49000 },
    { label: 'Apr', value: 62000 },
  ],
  elements: [],
}

Line chart

chartType: 'line' — polyline with data-point dots. Hover snaps a crosshair to the nearest column.

const config: ChartSectionConfig = {
  id: 'traffic-line',
  layout: 'chart',
  chartType: 'line',
  title: 'Site Traffic',
  description: 'Unique visitors per month',
  showGrid: true,
  height: 300,
  data: [
    { label: 'Jan', value: 3200 },
    { label: 'Feb', value: 4100 },
    { label: 'Mar', value: 3800 },
    { label: 'Apr', value: 5200 },
    { label: 'May', value: 6100 },
    { label: 'Jun', value: 5700 },
  ],
  elements: [],
}

Area chart

chartType: 'area' — line chart with a gradient-filled area below the curve.

const config: ChartSectionConfig = {
  id: 'downloads-area',
  layout: 'chart',
  chartType: 'area',
  title: 'Downloads Over Time',
  showGrid: true,
  height: 300,
  data: [
    { label: 'Jan', value: 1200 },
    { label: 'Feb', value: 1900 },
    { label: 'Mar', value: 2400 },
    { label: 'Apr', value: 2100 },
    { label: 'May', value: 3500 },
    { label: 'Jun', value: 4200 },
  ],
  elements: [],
}

Pie chart

chartType: 'pie' — full pie with built-in legend. Set showLabels: true to render percentage labels on each slice.

const config: ChartSectionConfig = {
  id: 'market-pie',
  layout: 'chart',
  chartType: 'pie',
  title: 'Market Share',
  showLabels: true,
  height: 300,
  data: [
    { label: 'Product A', value: 38 },
    { label: 'Product B', value: 27 },
    { label: 'Product C', value: 19 },
    { label: 'Product D', value: 10 },
    { label: 'Other',     value:  6 },
  ],
  elements: [],
}

Hovering a slice scales it up and dims the rest; the tooltip shows the label, value, and percentage.


Donut chart

chartType: 'donut' — ring chart with a centre label displaying the total.

const config: ChartSectionConfig = {
  id: 'region-donut',
  layout: 'chart',
  chartType: 'donut',
  title: 'Sales by Region',
  description: 'Proportion of total sales by geographic region',
  height: 300,
  data: [
    { label: 'North America', value: 42 },
    { label: 'Europe',        value: 31 },
    { label: 'Asia Pacific',  value: 18 },
    { label: 'Other',         value:  9 },
  ],
  elements: [],
}

Radar chart

chartType: 'radar' — spider / polygon chart with concentric grid polygons. Works with single or multiple series. Each axis corresponds to a label in the data array.

const config: ChartSectionConfig = {
  id: 'skills-radar',
  layout: 'chart',
  chartType: 'radar',
  title: 'Skill Assessment',
  height: 320,
  data: [
    { label: 'Communication',   value: 85 },
    { label: 'Teamwork',        value: 90 },
    { label: 'Creativity',      value: 70 },
    { label: 'Problem Solving', value: 95 },
    { label: 'Leadership',      value: 75 },
    { label: 'Adaptability',    value: 80 },
  ],
  elements: [],
}

Hovering an axis label highlights it and shows all series values for that axis in the tooltip.


Scatter chart

chartType: 'scatter' — X / Y scatter plot. Each data point must have an x and y key. label is shown in the tooltip.

const config: ChartSectionConfig = {
  id: 'price-demand-scatter',
  layout: 'chart',
  chartType: 'scatter',
  title: 'Price vs Demand',
  description: 'Correlation between unit price and demand volume',
  showGrid: true,
  height: 300,
  data: [
    { label: 'P1', x: 10, y: 95 },
    { label: 'P2', x: 20, y: 85 },
    { label: 'P3', x: 35, y: 70 },
    { label: 'P4', x: 50, y: 58 },
    { label: 'P5', x: 65, y: 44 },
    { label: 'P6', x: 75, y: 32 },
    { label: 'P7', x: 90, y: 18 },
  ],
  elements: [],
}

Multi-series

All axis-based chart types (bar, line, area, radar) accept a series array to plot multiple datasets from the same data objects. Each entry maps a key in the data to a display label and optional colour.

const config: ChartSectionConfig = {
  id: 'financial-overview',
  layout: 'chart',
  chartType: 'bar',   // also works with 'line', 'area', 'radar'
  title: 'Financial Overview',
  height: 300,
  data: [
    { label: 'Jan', revenue: 42000, expenses: 28000, profit: 14000 },
    { label: 'Feb', revenue: 51000, expenses: 31000, profit: 20000 },
    { label: 'Mar', revenue: 48000, expenses: 29000, profit: 19000 },
    { label: 'Apr', revenue: 62000, expenses: 34000, profit: 28000 },
  ],
  series: [
    { key: 'revenue',  label: 'Revenue' },
    { key: 'expenses', label: 'Expenses' },
    { key: 'profit',   label: 'Profit' },
  ],
  showGrid: true,
  showLegend: true,
  elements: [],
}

When series is omitted the chart falls back to a single { key: 'value' } series.


Custom colours

By default charts cycle through CSS variables --chart-1--chart-5. Override per-series with an explicit color:

series: [
  { key: 'revenue',  label: 'Revenue',  color: '#6366f1' },
  { key: 'expenses', label: 'Expenses', color: '#f43f5e' },
]

Any valid CSS colour string (hex, rgb(), hsl(), var(--my-token)) works.


Embedding in a full stage

Charts slot directly into a UIStage config alongside forms, tables, or any other section type:

import { UIStage } from 'react-ubiquitous'
import type { UIStageConfig } from 'react-ubiquitous'

const dashboardConfig: UIStageConfig = {
  id: 'analytics-dashboard',
  title: 'Analytics Dashboard',
  pages: [
    {
      id: 'overview',
      title: 'Overview',
      sections: [
        {
          id: 'revenue-chart',
          layout: 'chart',
          chartType: 'line',
          title: 'Revenue Trend',
          showGrid: true,
          height: 280,
          data: [
            { label: 'Q1', value: 120000 },
            { label: 'Q2', value: 145000 },
            { label: 'Q3', value: 138000 },
            { label: 'Q4', value: 182000 },
          ],
          elements: [],
        },
        {
          id: 'share-donut',
          layout: 'chart',
          chartType: 'donut',
          title: 'Revenue by Product',
          height: 280,
          data: [
            { label: 'Enterprise', value: 60 },
            { label: 'Pro',        value: 28 },
            { label: 'Free',       value: 12 },
          ],
          elements: [],
        },
      ],
    },
  ],
}

export function Dashboard() {
  return <UIStage config={dashboardConfig} />
}

Chart TypeScript imports

import type {
  ChartSectionConfig,  // full section config (use as config object type)
  ChartDataPoint,      // { label: string; [key: string]: unknown }
  ChartSeries,         // { key: string; label?: string; color?: string }
} from 'react-ubiquitous'

All chart types share the same ChartSectionConfig interface — chartType is the discriminating field.


Element Types

All elements extend a shared base:

| Field | Type | Description | |---|---|---| | id | string | HTML id attribute | | name | string | Key emitted by onChange | | label | string | Visible label text | | type | string | Discriminant — see types below | | order | number | Sort order within section | | labelPosition | 'top' \| 'left' \| 'right' \| 'hidden' | Label placement (default 'top') | | tooltip | string | Renders a ? icon with tooltip text | | units | string | Unit suffix/prefix (e.g. "kg", "$") | | unitsPosition | 'prefix' \| 'suffix' | Default 'suffix' | | disabled | boolean | Disables the control | | readonly | boolean | Makes the control read-only | | required | boolean | Adds HTML required attribute | | hidden | boolean | Hides the field entirely | | hiddenExpr | string | Condition expression — hides the field when truthy (e.g. "{age} >= 18") | | disabledExpr | string | Condition expression — disables the field when truthy (e.g. "{role} !== \"admin\"") | | width | number \| string | Column span 112, or any CSS string | | className | string | Extra CSS classes on the wrapper | | style | CSSProperties | Inline styles on the wrapper | | validations | ValidationRule[] | Validation rules (evaluated on blur) |

input

{ type: 'input', inputType: 'text', placeholder: '', defaultValue: '', min, max, step, multiple, accept, datalistId }

inputType accepts any valid HTML <input> type: text, email, password, number, tel, url, date, time, datetime-local, month, week, color, range, file, search.

textarea

{ type: 'textarea', placeholder: '', rows: 4, cols, resize: 'vertical', defaultValue: '' }

resize: 'none' | 'both' | 'horizontal' | 'vertical'

select

{
  type: 'select',
  options: [
    { label: 'Option A', value: 'a' },
    { label: 'Option B', value: 'b' },
    { group: true, label: 'Group', options: [{ label: 'C', value: 'c' }] },
  ],
  multiple: false,
  defaultValue: 'a',
}

radio

{ type: 'radio', options: [{ label: 'Yes', value: 'yes' }, { label: 'No', value: 'no' }], orientation: 'horizontal' }

checkbox

{ type: 'checkbox', defaultChecked: false }

button

{ type: 'button', text: 'Submit', buttonType: 'submit', variant: 'default', size: 'md' }

variant: 'default' | 'destructive' | 'outline' | 'ghost' | 'link'
size: 'sm' | 'md' | 'lg'

label

{ type: 'label', text: 'Section heading', htmlFor: 'some-input-id' }

fieldset

Recursive — a fieldset contains its own elements array rendered as a labelled group.

{ type: 'fieldset', legend: 'Address', children: [ ...elements ] }

datalist

Pairs with an input via datalistId to provide autocomplete suggestions.

{ type: 'datalist', id: 'colours-list', options: [{ label: 'Red', value: 'red' }] }

output

Read-only display field with optional number formatting.

{ type: 'output', value: '1234.5', format: 'currency', locale: 'en-US', currency: 'USD' }

format: 'text' | 'number' | 'currency' | 'percentage'

custom

Renders a consumer-supplied React component. See Custom Component Injection.

{ type: 'custom', component: 'my-widget', props: { max: 5 } }

Validation

Rules are evaluated in order on blur. The first failure renders its message below the field. Every rule accepts an optional message override.

validations: [
  { rule: 'required' },
  { rule: 'minLength', value: 8, message: 'Password must be at least 8 characters.' },
  { rule: 'maxLength', value: 64 },
  { rule: 'pattern',  value: '^[A-Za-z0-9]+$', message: 'Alphanumeric only.' },
]

| Rule | Extra field | Default message | |---|---|---| | required | — | "This field is required." | | min | value: number | "Minimum value is {value}." | | max | value: number | "Maximum value is {value}." | | minLength | value: number | "Minimum length is {value} characters." | | maxLength | value: number | "Maximum length is {value} characters." | | pattern | value: string (regex) | "Invalid format." | | email | — | "Invalid email address." | | url | — | "Invalid URL." | | phone | — | "Invalid phone number." | | step | value: number | "Value must be a multiple of {value}." | | custom | value: string | Resolved via your validation registry |

Validation AND / OR Chaining

Use a ValidationGroup entry to express AND / OR logic:

validations: [
  { rule: 'required' },
  {
    rule: 'group',
    operator: 'or',
    rules: [{ rule: 'email' }, { rule: 'url' }],
    message: 'Must be a valid email address or URL.',
  },
]
  • operator: 'and' — all nested rules must pass (short-circuits on first failure).
  • operator: 'or' — at least one nested rule must pass; the group message is shown when all branches fail.
  • Groups can be nested to any depth to express complex boolean logic.
  • The message field is optional; a default message is used when omitted.
// Nested: (email) OR (minLength(5) AND uppercase)
{
  rule: 'group',
  operator: 'or',
  rules: [
    { rule: 'email' },
    {
      rule: 'group',
      operator: 'and',
      rules: [
        { rule: 'minLength', value: 5 },
        { rule: 'pattern', value: '[A-Z]', message: 'Must contain uppercase.' },
      ],
    },
  ],
  message: 'Must be an email or a strong code (5+ chars with uppercase).',
}

Computed / Derived Fields (Formulas)

Add a formula to any output element to derive its value from sibling fields:

{
  "type": "output",
  "id": "total",
  "name": "total",
  "label": "Order Total",
  "formula": "{price} * {quantity}",
  "format": "currency"
}
  • Field values are referenced by name inside { } braces.
  • Supports +, -, *, /, parentheses, decimal literals.
  • Missing or non-numeric field references resolve to 0.
  • Unsafe expressions (characters outside [\d\s+\-*/().]) return null silently.
  • When a formula is set, value and defaultValue are ignored.

Wiring up live formula evaluation

Pass the current form-values map to <UIStage>:

const [formValues, setFormValues] = useState<Record<string, unknown>>({})

<UIStage
  config={stageConfig}
  formValues={formValues}
  onChange={(name, value) =>
    setFormValues(prev => ({ ...prev, [name]: value }))
  }
/>

formValues flows automatically through UIPageUISectionUIElementOutputField.

useFormulaValue hook

For computing formula values outside of OutputField:

import { useFormulaValue, evaluateFormula } from 'react-ubiquitous'

// React hook (memoised, reacts to formValues changes)
const result = useFormulaValue('{price} * {qty}', formValues) // => "30"

// Pure function (synchronous)
const result = evaluateFormula('{price} * {qty}', { price: 10, qty: 3 }) // => 30

Page-Transition Animations

Add pageTransition to UIStageConfig to animate tab changes:

{
  "id": "my-stage",
  "pageTransition": "fade",
  "pages": [...]
}

| Value | Effect | |---|---| | 'none' (default) | Instant switch, no animation | | 'fade' | Incoming page fades in over 220 ms | | 'slide-left' | New page slides in from the right (forward navigation feel) | | 'slide-right' | New page slides in from the left (backward navigation feel) |

CSS keyframes are injected into <head> once on first render (SSR-safe).


TypeScript Type Generation from Config

Generate a typed FormValues interface from a stage config:

import { generateFormTypes } from 'react-ubiquitous'

const ts = generateFormTypes(myStageConfig)
// Outputs:
// export interface FormValues {
//   firstName: string;
//   lastName: string;
//   email: string;
//   age?: number;
//   subscribe?: boolean;
// }
  • Pass an optional second argument for a custom type name:
    generateFormTypes(config, 'ContactFormValues')
  • Required fields (required: true) are typed as non-optional.
  • Non-value elements (button, label, fieldset, datalist) are excluded.
  • Field names that are not valid JS identifiers are quoted.

onChange Callback

const [formData, setFormData] = useState<Record<string, unknown>>({})

<UIStage
  config={stageConfig}
  onChange={(name, value) =>
    setFormData(prev => ({ ...prev, [name]: value }))
  }
/>

| Element | Value type | |---|---| | input text / email / tel / url / search | string | | input number / range | number | | input date / time / datetime-local | string | | input file | FileList \| null | | checkbox | boolean | | radio | string | | textarea | string | | select (single) | string | | select (multiple) | string[] |


Server-Side Validation

Pass server-returned validation errors directly to UIStage via serverErrors. Each key is a field name and the value is the error message to display.

// After a failed form submission:
const [serverErrors, setServerErrors] = useState<Record<string, string>>({})

const handleSubmit = async () => {
  const response = await submitForm(formValues)
  if (!response.ok) {
    // Backend returns: { "email": "Email already in use.", "username": "Taken." }
    const errors = await response.json()
    setServerErrors(errors)
  }
}

<UIStage
  config={stageConfig}
  serverErrors={serverErrors}
/>

Server errors appear below client-side validation errors in the same field wrapper. They are keyed by the field's name attribute.


Conditional Rendering

Add hiddenExpr or disabledExpr to any element config to show / hide / disable it dynamically based on other field values.

{
  "type": "input",
  "id": "guardian-name",
  "name": "guardianName",
  "label": "Guardian Name",
  "hiddenExpr": "{age} >= 18"
}
{
  "type": "input",
  "id": "admin-notes",
  "name": "adminNotes",
  "label": "Admin Notes",
  "disabledExpr": "{role} !== \"admin\""
}

Expressions support: field references {fieldName}, comparison operators (<, >, <=, >=, ===, !==), logical operators (&&, ||, !), and arithmetic (+, -, *, /).

The static hidden and disabled flags still work as before — the expression props take precedence when present.


ReadOnly Mode

Add readOnly to UIStage to render the entire form in a non-editable state. Every field is forced into disabled mode. Useful for "View Details" pages that reuse the same form config as the edit page.

<UIStage config={stageConfig} readOnly />

i18n / Validation Messages

Override the default English validation messages by passing an i18n prop to UIStage. Supports static strings or factory functions for dynamic messages.

import type { I18nMessages } from 'react-ubiquitous'

const i18n: I18nMessages = {
  required: 'Ce champ est obligatoire.',
  email:    'Adresse e-mail invalide.',
  url:      'URL invalide.',
  minLength: (n) => `Minimum ${n} caractères.`,
  maxLength: (n) => `Maximum ${n} caractères.`,
  min:       (v) => `Valeur minimum : ${v}.`,
  max:       (v) => `Valeur maximum : ${v}.`,
}

<UIStage config={stageConfig} i18n={i18n} />

Any key that is omitted falls back to the built-in English message. Per-field message overrides in the config always take the highest priority.

Exported type: I18nMessages — import from react-ubiquitous.


Form Persistence & Auto-Save

Persist form values across page refreshes with localStorageKey. An optional onAutoSave callback fires (debounced 1 s) whenever values change.

<UIStage
  config={stageConfig}
  localStorageKey="contact-form-draft"
  onAutoSave={(values) => {
    console.log('Auto-saved:', values)
    // e.g. POST to /api/drafts
  }}
/>

Saved values are merged into the form's initial state on the next page load. If you also pass formValues, the controlled values take precedence over saved ones.

You can also use the hook directly in your own components:

import { useFormPersistence } from 'react-ubiquitous'

const { savedValues, persistValues } = useFormPersistence({
  localStorageKey: 'my-form',
  onAutoSave: (values) => console.log('Saved', values),
  debounceMs: 500,  // default: 1000
})

Custom Component Injection

Register bespoke React components to render inside the JSON config. This makes the library infinitely extensible without forking.

Step 1 — define a custom component:

import type { UIElementProps } from 'react-ubiquitous'

function MyRatingWidget({ config, onChange }: UIElementProps) {
  const cfg = config as CustomElementConfig & { props?: { max?: number } }
  return (
    <div>
      {/* your custom rendering */}
      <input type="range" max={cfg.props?.max ?? 5} onChange={...} />
    </div>
  )
}

Step 2 — pass it to UIStage:

<UIStage
  config={stageConfig}
  customComponents={{ 'my-rating': MyRatingWidget }}
/>

Step 3 — reference it in your config:

{
  "type": "custom",
  "component": "my-rating",
  "id": "satisfaction",
  "name": "satisfaction",
  "props": { "max": 10 }
}

You can also register components globally via componentRegistry (useful for libraries or plugins):

import { componentRegistry } from 'react-ubiquitous'
componentRegistry.register('my-rating', MyRatingWidget)

Exported type: CustomElementConfig — import from react-ubiquitous.


Virtualization for Large Lists

For list-detail sections with thousands of items, enable virtualScrolling to render only the visible rows using react-window:

{
  "layout": "list-detail",
  "id": "users-list",
  "virtualScrolling": true,
  "virtualListHeight": 600,
  "listItems": [...]
}

| Config | Type | Default | Description | |--------|------|---------|-------------| | virtualScrolling | boolean | false | Enables react-window row virtualization. Disables pagination. | | virtualListHeight | number | 400 | Height of the virtual scroll container in pixels. |


Theme Builder

A standalone Theme Builder ships as a static HTML page at public/theme-builder.html. Open it in any browser (no build step required) to:

  • Pick colors for every CSS variable
  • Choose from preset color schemes (Indigo, Emerald, Rose, Amber, Sky, Dark)
  • Adjust the global border-radius
  • Preview changes live against buttons, inputs, badges, and cards
  • Copy the generated :root { ... } block to paste into your project stylesheet
# Start the Storybook dev server, then visit:
http://localhost:6006/theme-builder.html

# Or open directly:
open public/theme-builder.html

HTTP Networking Client

react-ubiquitous ships a lightweight, zero-dependency HTTP client built on the browser's native fetch API. It supports Basic and Bearer authentication, query parameters, JSON request/response bodies, and request cancellation via AbortSignal.

Quick usage

import { get, post, createHttpClient } from 'react-ubiquitous'

// One-off GET with Bearer auth
const { data, status, ok } = await get<User[]>('/api/users', {
  auth: { type: 'bearer', token: 'my-token' },
  params: { page: 1, limit: 20 },
})

// POST with a JSON body
await post('/api/users', {
  auth: { type: 'bearer', token: 'my-token' },
  body: { name: 'Alice', role: 'admin' },
})

createHttpClient — pre-bound client

Bind a base URL and default auth once; all methods inherit them automatically. Per-request options can still override both.

import { createHttpClient } from 'react-ubiquitous'

const api = createHttpClient('https://api.example.com', {
  type: 'bearer',
  token: sessionStorage.getItem('token') ?? '',
})

// GET  /contacts?from=1&to=50&sort=asc
const { data } = await api.get<Contact[]>('/contacts', {
  params: { from: 1, to: 50, sort: 'asc' },
})

// POST /contacts  (body serialised as JSON automatically)
await api.post('/contacts', { body: { name: 'Bob' } })

// DELETE /contacts/42
await api.del('/contacts/42')

Available methods

| Export | Description | |---|---| | get(url, options?) | GET request | | post(url, options?) | POST request | | put(url, options?) | PUT request | | patch(url, options?) | PATCH request | | del(url, options?) | DELETE request | | head(url, options?) | HEAD request (no response body) | | request(method, url, options?) | Raw method for any HTTP verb | | createHttpClient(baseUrl?, auth?) | Factory returning pre-bound helpers | | buildAuthHeader(auth) | Returns the Authorization header value | | buildRequestUrl(url, params?) | Appends query params to a URL |

Auth strategies

// Bearer token (RFC 6750)
{ type: 'bearer', token: 'eyJhbGci…' }

// HTTP Basic (RFC 7617) — Base-64 encodes username:password
{ type: 'basic', username: 'alice', password: 's3cr3t' }

// No authentication header
{ type: 'none' }

RequestOptions

| Field | Type | Description | |---|---|---| | auth | AuthConfig | Override auth for this request | | headers | Record<string, string> | Extra headers merged with defaults | | params | Record<string, string \| number \| boolean> | Query-string parameters | | body | unknown | Request payload — serialised as JSON | | signal | AbortSignal | Cancellation signal |

HttpResponse<T>

| Field | Type | Description | |---|---|---| | data | T | Decoded response body (null when empty) | | status | number | HTTP status code | | headers | Headers | Raw Headers object from Fetch | | ok | boolean | true when status is 200–299 |


Component API

<UIStage>

<UIStage
  config={stageConfig}             // UIStageConfig — required
  onChange={(name, value) => {}}   // called on every field change
  onPageChange={(pageId) => {}}    // called when the active tab changes
  formValues={formValues}          // optional — current field-value map for formula evaluation
  serverErrors={{ email: 'Email already in use.' }}  // server-side validation errors keyed by field name
  readOnly                         // forces every element into disabled / read-only mode
  customComponents={{ 'my-widget': MyWidget }}       // map of custom element renderers
  i18n={{ required: 'Ce champ est obligatoire.' }}   // validation message overrides
  localStorageKey="my-form"        // auto-persists form values to localStorage
  onAutoSave={(values) => {}}      // debounced callback whenever values change
/>

| Prop | Type | Default | Description | |------|------|---------|-------------| | config | UIStageConfig | — | The stage configuration object. | | onChange | (name, value) => void | — | Called on every field change. | | onPageChange | (pageId) => void | — | Called when the active tab changes. | | formValues | Record<string, unknown> | — | Controlled form values (for formula evaluation). | | serverErrors | Record<string, string> | — | Server-supplied validation errors, keyed by field name. Displayed alongside client-side errors. | | readOnly | boolean | false | Forces every element into a disabled state. Ideal for view-only pages. | | customComponents | Record<string, ComponentType> | — | Map of custom React components for type: 'custom' elements. | | i18n | I18nMessages | — | Validation message overrides for built-in English messages. | | localStorageKey | string | — | Persist form values to localStorage under this key. Values are restored on first render. | | onAutoSave | (values) => void | — | Debounced callback (1 s) invoked whenever form values change while localStorageKey is set. |

<UIPage>, <UISection>, <UIElement>

Lower-level components for custom layouts:

import { UIPage, UISection, UIElement } from 'react-ubiquitous'

<UIPage    config={pageConfig}    onChange={handleChange} formValues={formValues} />
<UISection config={sectionConfig} onChange={handleChange} formValues={formValues} />
<UIElement config={elementConfig} onChange={handleChange} formValues={formValues} />

useValidation

import { useValidation } from 'react-ubiquitous'

const { error, validate, clear } = useValidation(element.validations)

useFormulaValue

import { useFormulaValue } from 'react-ubiquitous'

const computed = useFormulaValue('{price} * {quantity}', formValues)
// => "30" (string) or null when formula is absent / invalid

generateFormTypes

import { generateFormTypes } from 'react-ubiquitous'

const ts = generateFormTypes(stageConfig, 'MyFormValues')

DynamicForm

DynamicForm is a standalone server-driven interactive form renderer. Unlike UIStage (which hosts entire page layouts), DynamicForm is consumed directly — pass the raw JSON response from any /api/forms/{name} endpoint as the config prop and the component handles everything else.

Quick example

import { DynamicForm } from 'react-ubiquitous'

function AuthPage({ serverPrefix }: { serverPrefix: string }) {
  const [config, setConfig] = React.useState(null)

  React.useEffect(() => {
    fetch(`${serverPrefix}/api/forms/auth`)
      .then(r => r.json())
      .then(setConfig)
  }, [serverPrefix])

  if (!config) return null

  return (
    <DynamicForm
      config={config}
      serverPrefix={serverPrefix}
      onSubmit={values => console.log('Submitted', values)}
    />
  )
}

Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | config | DynamicFormConfig | — | JSON form config from the server. | | serverPrefix | string | "" | Base URL prefix for async validateUrl and submitUrl. | | onSubmit | (values) => void \| Promise<void> | — | Called after all validations pass. | | onChange | (name, value) => void | — | Called on every field value change. | | theme | UITheme | — | Visual theme token (inherits parent when omitted). | | className | string | — | Additional class names on the <form> element. |

Validation

All validation is Zod-powered — schemas are built dynamically from the validations array in each field config. Every rule runs inside a superRefine so all errors are collected in a single safeParse pass.

Supported rules: required · minLength · maxLength · min · max · pattern · email · url · matchField · notSameAs · mustBeTrue · luhn · notExpired · async (debounced HTTP GET)

Button-level validations apply per-field constraints shown as amber hints beneath the button so users know exactly what blocks submission.

Server contract

GET  /api/forms/{formName}                     → DynamicFormConfig
GET  {serverPrefix}{validateUrl}?field=&value= → { valid, error? }
POST {serverPrefix}{submitUrl}                 → any

See the api-specification.md for the full JSON schema.


Storybook Component Catalogue

An interactive Storybook catalogue ships alongside the library source and showcases every major component with default Tailwind styles:

npm run storybook          # dev server at http://localhost:6006
npm run build-storybook    # static build to storybook-static/

Stories are located in src/lib/stories/ and cover:

| Story | Variants | |---|---| | InputField | Text, Email, Password, Number, Disabled, OR Validation, Tooltip | | SelectField | Default, Option Groups, Multi-Select | | ButtonField | Default, Outline, Destructive, Ghost, Small, Large | | CheckboxField | Unchecked, Checked, Disabled | | OutputField | Static, Currency, Percentage, Formula, Nested Formula | | UIStage | Contact Form, Fade/Slide Transitions, Formula Fields, AND/OR Validations, Dark Theme | | Chart | Bar (single + multi-series + labels), Line (single + multi), Area (single + multi), Pie (default + labels), Donut, Radar (single + multi), Scatter | | ListDetail | Contacts, Simple (flat elements), Avatar Images | | TreeView | Organisation (easy mode), Compact Mode, Detail Pages | | Chat | Group Conversations, Direct Messages, Empty State |

All stories are also automatically smoke-tested in CI via @storybook/addon-vitest + Playwright Chromium as part of npm test.


TypeScript Reference

All config interfaces are exported:

import type {
  // Stage / Page
  UIStageConfig,
  UIPageConfig,

  // Section union + individual types
  UISectionConfig,
  FlexSectionConfig,
  GridSectionConfig,
  HeroSectionConfig,
  MediaSectionConfig,   MediaItem,
  ListDetailSectionConfig,
  ListDetailItem,
  ListEndpointConfig,
  FilterEndpointConfig,
  DetailEndpointConfig,
  DetailPage,
  TreeViewSectionConfig,
  TreeViewNode,
  ChatSectionConfig,
  ChatConversation,
  ChatMessage,

  // Navigation & Wayfinding section types
  NavLink,
  NavbarSectionConfig,
  SidebarItem,
  SidebarSectionConfig,
  BreadcrumbItem,
  BreadcrumbsSectionConfig,
  PaginationSectionConfig,
  StepperStep,
  StepperSectionConfig,
  TabItem,
  TabsSectionConfig,

  // Element union + individual types
  UIElementConfig,
  InputElementConfig,
  CheckboxElementConfig,
  RadioElementConfig,
  TextareaElementConfig,
  SelectElementConfig,
  ButtonElementConfig,
  LabelElementConfig,
  FieldsetElementConfig,
  DatalistElementConfig,
  OutputElementConfig,

  // Helpers
  ValidationRule,
  ValidationGroup,       // AND / OR validation group
  ElementWidth,
  StyleObject,
  UIPageTransition,      // 'none' | 'fade' | 'slide-left' | 'slide-right'
} from 'react-ubiquitous'

HTTP networking types:

import type {
  AuthConfig,        // 'basic' | 'bearer' | 'none' strategy
  RequestOptions,    // per-request options (auth, headers, params, body, signal)
  HttpResponse,      // { data, status, headers, ok }
} from 'react-ubiquitous'

Styling

react-ubiquitous uses Tailwind CSS internally. The rendered HTML uses semantic elements (<label>, <input>, <fieldset>, etc.) and each config accepts className and style for customisation at every level.

Default theme stylesheet

The bundled stylesheet (dist/react-ubiquitous.css) contains both the compiled Tailwind utility classes used by every component and the shadcn-compatible CSS custom-property theme tokens (e.g. --background, --foreground, --primary).

Import it once — in your app's entry point (or global CSS file):

// main.tsx / index.tsx (or your global CSS)
import 'react-ubiquitous/styles.css'

This gives you the default neutral light theme out of the box, full component styling, and a .dark variant for dark mode — matching the shadcn/ui default palette. No additional Tailwind configuration is required.

If you already use shadcn/ui (or any Tailwind-based design system that defines the same CSS variables), you can still i