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

@zudello/ui-preset

v0.1.1

Published

Reusable UI preset components, design tokens, and theme engine for Zudello apps (Vue, React, iframe bridge)

Downloads

10

Readme

@zudello/ui-preset

Reusable UI components, design tokens, and theme engine for Zudello embedded applications. Ships Vue 3, React 18/19, and a framework-agnostic iframe bridge.

All visual styles are derived from the Zudello customer repo (feature-ui-improvements branch) so embedded apps look identical to the main platform.


Install

npm install @zudello/ui-preset

Peer dependencies (install whichever you need):

# React apps
npm install react react-dom

# Vue apps
npm install vue floating-vue

# Dark mode support (any framework)
npm install darkreader

Package Exports

| Import Path | Contents | |---|---| | @zudello/ui-preset | Vue 3 components | | @zudello/ui-preset/react | React components + UI primitives | | @zudello/ui-preset/iframe | Iframe bridge (IframeHost, IframeGuest) | | @zudello/ui-preset/theme | Theme engine (applyTheme, color accents, high contrast) | | @zudello/ui-preset/theme.css | Design tokens (140+ --zd-* CSS variables) | | @zudello/ui-preset/global.css | Global component styles (buttons, alerts, dialogs, forms, etc.) | | @zudello/ui-preset/react.css | React domain component runtime styles (ResourcePageShell, toolbar components) | | @zudello/ui-preset/grid.css | AG Grid theme overlay | | @zudello/ui-preset/style.css | Vue scoped component styles | | @zudello/ui-preset/components.json | shadcn/ui config template |


Quick Start (React Embedded App)

1. Import styles

/* src/index.css */
@import '@zudello/ui-preset/theme.css';
@import '@zudello/ui-preset/global.css';
@import '@zudello/ui-preset/react.css';

2. Wrap with EmbeddedAppProvider

import { EmbeddedAppProvider } from '@zudello/ui-preset/react'

function App() {
  return (
    <EmbeddedAppProvider autoResize>
      <MyPage />
    </EmbeddedAppProvider>
  )
}

3. Use components

import {
  ResourcePageShell,
  ToolbarSearch,
  ToolbarStatusFilter,
  ToolbarSortSelect,
  ToolbarViewToggle,
  ToolbarIconButton,
  Button,
  Dialog,
  Alert,
  Badge,
  Spinner,
  type StatusItem,
  type SortOption,
  ViewType
} from '@zudello/ui-preset/react'

Components

Domain Components

These compose the resource list page pattern used across the platform.

ResourcePageShell

Main page container with header, toolbar slot, loading state, and empty state.

<ResourcePageShell
  title="Invoices"
  count={214}
  showCount
  loading={false}
  empty={items.length === 0}
  showClearFilters={hasActiveFilters}
  onClearFilters={clearFilters}
  toolbar={<>...</>}
>
  {/* Your content: AG Grid, list, cards, etc. */}
</ResourcePageShell>

| Prop | Type | Default | Description | |---|---|---|---| | title | string | '' | Page title | | count | number | 0 | Item count shown in badge | | showCount | boolean | false | Show the count badge | | loading | boolean | false | Show loading spinner | | empty | boolean | false | Show empty state | | emptyMessage | string | 'No results for this search.' | Empty state text | | showClearFilters | boolean | true | Show clear filters button in empty state | | hideHeader | boolean | false | Hide the entire header row | | onClearFilters | () => void | — | Clear filters callback | | toolbar | ReactNode | — | Toolbar content (right side of header) | | titleSlot | ReactNode | — | Override title with custom content | | loadingSlot | ReactNode | — | Override loading state | | emptySlot | ReactNode | — | Override empty state |

ToolbarSearch

Collapsible search input that expands on click.

<ToolbarSearch
  value={search}
  onChange={setSearch}
  collapsible
  placeholder="Search invoices..."
/>

ToolbarStatusFilter

Popover with colored checkboxes for status filtering.

const [statuses, setStatuses] = useState<StatusItem[]>([
  { key: 'DRAFT', name: 'Draft', color: '#b4b4b4', isEnabled: true, isSelected: true },
  { key: 'REVIEW', name: 'User Review', color: '#fdae61', isEnabled: true, isSelected: true },
  { key: 'COMPLETE', name: 'Completed', color: '#5cb85c', isEnabled: true, isSelected: false },
])

<ToolbarStatusFilter
  statuses={statuses}
  label="Status"
  onToggle={(status) => {
    setStatuses(prev => prev.map(s =>
      s.key === status.key ? { ...s, isSelected: !s.isSelected } : s
    ))
  }}
  onToggleAll={(selectAll) => {
    setStatuses(prev => prev.map(s =>
      s.isEnabled ? { ...s, isSelected: selectAll } : s
    ))
  }}
/>

ToolbarSortSelect

Popover for choosing sort field and direction.

const sortOptions: SortOption[] = [
  { key: 'date_issued', label: 'Date issued' },
  { key: 'company_name', label: 'Supplier name' },
  { key: 'total', label: 'Total' },
]

<ToolbarSortSelect
  options={sortOptions}
  value={sortField}
  ascending={sortAsc}
  onValueChange={setSortField}
  onAscendingChange={setSortAsc}
/>

ToolbarViewToggle

Popover for switching between List, Board, and Table views.

<ToolbarViewToggle
  value={viewType}
  onChange={setViewType}
/>

ToolbarIconButton

Icon button with optional tooltip and notification badge.

<ToolbarIconButton icon="fal fa-filter" tooltip="Filter" badge={hasFilters} />
<ToolbarIconButton icon="fal fa-user-group" tooltip="Show mine" active={meFilter} onClick={toggleMe} />
<ToolbarIconButton icon="fal fa-columns" tooltip="Columns" />

Default Resource Toolbar (Gold Standard)

Use DEFAULT_RESOURCE_TOOLBAR_ACTIONS as the package-owned toolbar source of truth. This keeps icon count/order consistent across consuming apps by default:

import {
  DEFAULT_RESOURCE_TOOLBAR_ACTIONS,
  ToolbarIconButton,
  ToolbarSearch,
  ToolbarSortSelect,
  ToolbarStatusFilter,
  ToolbarViewToggle,
} from "@zudello/ui-preset/react"

Default action order is: search -> users -> filter -> status -> sort -> view -> columns -> automations

The defaults include:

  • icon/tooltip for icon actions (including fal fa-robot for automations)
  • search.collapsible: true (icon-first search that expands)
  • filter.showBadgeWhen: "filters_applied" (badge hidden by default)

To override a subset without copy-pasting the whole config:

const toolbarActions = DEFAULT_RESOURCE_TOOLBAR_ACTIONS.map((action) =>
  action.key === "filter"
    ? { ...action, tooltip: "Filters" }
    : action
)

Default-aware usage:

const hasFiltersApplied = !statuses.every((s) => s.isEnabled === s.isSelected)

const toolbar = DEFAULT_RESOURCE_TOOLBAR_ACTIONS.map((action) => {
  if (action.key === "search") {
    return <ToolbarSearch key={action.key} value={search} onChange={setSearch} collapsible={action.collapsible ?? true} />
  }
  if (action.key === "filter") {
    return (
      <ToolbarIconButton
        key={action.key}
        icon={action.icon}
        tooltip={action.tooltip}
        badge={action.showBadgeWhen === "filters_applied" ? hasFiltersApplied : false}
      />
    )
  }
  return null
})

Putting it together

const toolbar = (
  <>
    <ToolbarSearch value={search} onChange={setSearch} />
    <ToolbarIconButton icon="fal fa-user-group" tooltip="Show mine" active={meFilter} onClick={toggleMe} />
    <ToolbarStatusFilter statuses={statuses} onToggle={toggleStatus} onToggleAll={toggleAll} />
    <ToolbarSortSelect options={sortOptions} value={sortField} ascending={sortAsc} onValueChange={setSortField} onAscendingChange={setSortAsc} />
    <ToolbarViewToggle value={viewType} onChange={setViewType} />
    <ToolbarIconButton icon="fal fa-columns" tooltip="Columns" />
  </>
)

<ResourcePageShell title="Invoices" count={count} showCount toolbar={toolbar} empty={count === 0}>
  {/* AG Grid or list content */}
</ResourcePageShell>

UI Primitives

Pre-built components that use the --zd-* design tokens. Every embedded app gets the same visual output.

Button

<Button variant="filled" color="primary" size="md">Save</Button>
<Button variant="secondary">Cancel</Button>
<Button variant="outlined" color="danger">Delete</Button>
<Button variant="text" size="sm">Learn more</Button>
<Button loading>Saving...</Button>
<Button circle icon="fal fa-plus" />
<Button block>Full width</Button>

| Prop | Type | Default | Values | |---|---|---|---| | variant | string | 'filled' | filled, secondary, text, outlined | | color | string | 'primary' | primary, secondary, danger, success | | size | string | 'md' | sm (24px), md (31px), lg (36px) | | circle | boolean | false | Circular button | | block | boolean | false | Full width | | loading | boolean | false | Show spinner, disable clicks | | icon | string | — | Icon class (e.g. 'fal fa-plus') |

Also accepts all native <button> HTML attributes.

Dialog

<Dialog open={isOpen} onClose={() => setIsOpen(false)} title="Confirm Delete">
  <p>Are you sure you want to delete this item?</p>
  <div style={{ display: 'flex', gap: 8, justifyContent: 'flex-end', marginTop: 16 }}>
    <Button variant="secondary" onClick={() => setIsOpen(false)}>Cancel</Button>
    <Button color="danger" onClick={handleDelete}>Delete</Button>
  </div>
</Dialog>

| Prop | Type | Description | |---|---|---| | open | boolean | Show/hide the dialog | | onClose | () => void | Called on backdrop click or Escape key | | title | string | Dialog title | | description | string | Subtitle text |

Alert

Includes default icons per variant matching the customer platform (Font Awesome Light).

<Alert variant="success" title="Saved">Your changes have been saved.</Alert>
<Alert variant="danger">Something went wrong.</Alert>
<Alert variant="warning" title="Warning">This action cannot be undone.</Alert>
<Alert variant="info">Processing may take a few minutes.</Alert>
<Alert variant="danger" icon={false}>No icon alert.</Alert>
<Alert variant="info" icon="fal fa-bell">Custom icon.</Alert>

Default icons: info = fa-info-circle, warning = fa-lightbulb, success = fa-check-circle, danger = fa-exclamation-triangle.

Switch

<Switch checked={enabled} onChange={setEnabled} label="Enable notifications" />
<Switch checked={active} onChange={setActive} success />

Input

<Input placeholder="Enter name..." />
<Input icon="fal fa-search" placeholder="Search..." />
<Input small placeholder="Compact input" />

Select

<Select
  options={[
    { value: 'draft', label: 'Draft' },
    { value: 'active', label: 'Active' },
  ]}
  value={status}
  onChange={e => setStatus(e.target.value)}
  placeholder="Select status"
/>

Badge & Tag

<Badge color="purple">New</Badge>
<Badge color="green">Active</Badge>
<Badge color="red">Overdue</Badge>

<Tag color="grey">Category</Tag>
<Tag color="yellow">Pending</Tag>

Badge colors: grey, purple, red, green, yellow, highlight, team. Tag colors: grey, purple, red, green, yellow.

RadioGroup

Card-style radio options (used in settings pages).

<RadioGroup
  name="approval"
  options={[
    { value: 'auto', label: 'Auto', description: 'System decides based on rules' },
    { value: 'on', label: 'On', description: 'Always require approval' },
    { value: 'off', label: 'Off', description: 'Skip approval' },
  ]}
  value={approval}
  onChange={setApproval}
/>

Avatar

Auto-generates background color and initials from name (18 preset colors matching the customer platform).

<Avatar name="Nathan Chung" size="lg" />
<Avatar name="Bryson Camp" size="md" />
<Avatar src="/photo.jpg" alt="User" size="sm" />
<Avatar initials="ZD" color="#8b3694" />

Other Primitives

<Spinner />                          // Loading spinner (sm, md, lg)
<Tabs tabs={[{key: 'a', label: 'Tab A'}, {key: 'b', label: 'Tab B'}]} active="a" onChange={setTab} />
<Collapsible title="Advanced Settings" defaultOpen={false}>{children}</Collapsible>
<Tooltip content="More info">{children}</Tooltip>
<EmptyState icon="fal fa-inbox" title="No items" description="Try adjusting your filters" />
<FormSection label="Notifications" help="Choose how you want to be notified">{children}</FormSection>
<ContextMenu items={[...]} position={{x, y}} onSelect={handleSelect} onClose={close} />
<Dropdown items={[...]} onSelect={handleSelect} />

Iframe Bridge

Type-safe postMessage wrapper for host-guest communication between the Zudello platform and embedded apps.

Message Flow

Host (Zudello platform)              Guest (embedded app in iframe)
─────────────────────                 ─────────────────────────────
                          ← ready
theme-change →
auth-token →
module-context →
                          ← navigate
                          ← resize
                          ← open-document
                          ← notification

Message Types

| Direction | Message | Payload | |---|---|---| | Host → Guest | theme-change | { theme: string, highContrast?: boolean, colorAccent?: string } | | Host → Guest | auth-token | { token: string } | | Host → Guest | module-context | { module: string, submodule?: string, title?: string } | | Guest → Host | ready | — | | Guest → Host | navigate | { path: string, query?: Record<string, string> } | | Guest → Host | resize | { height: number } | | Guest → Host | open-document | { id: string, module?: string } | | Guest → Host | notification | { type: 'success' \| 'error' \| 'warning' \| 'info', message: string } |

All messages are sent on the zudello-iframe channel and are ignored by anything not listening on that channel.

Guest Side (Embedded React App)

The easiest way is to use EmbeddedAppProvider which handles everything automatically:

import { EmbeddedAppProvider, useEmbeddedApp } from '@zudello/ui-preset/react'
import { applyTheme } from '@zudello/ui-preset/theme'

function App() {
  return (
    <EmbeddedAppProvider
      autoResize
      onThemeChange={(payload) => {
        applyTheme({ theme: payload.theme, highContrast: payload.highContrast })
      }}
    >
      <MyPage />
    </EmbeddedAppProvider>
  )
}

function MyPage() {
  const { authToken, theme, moduleContext, guest } = useEmbeddedApp()

  // authToken: string | null — JWT from host
  // theme: ThemePayload | null — current theme
  // moduleContext: ModuleContextPayload | null — which module is active
  // guest: IframeGuest — send messages back to host

  const handleRowClick = (id: string) => {
    guest.openDocument(id, 'purchase-invoices')
  }

  const handleSave = () => {
    guest.notify('success', 'Invoice saved successfully')
  }

  const handleNavigate = () => {
    guest.navigate('/settings/team')
  }

  return <div>...</div>
}

EmbeddedAppProvider and useEmbeddedApp() are React-only APIs. For Vue (or any non-React app), use the manual IframeGuest approach in the next section.

What EmbeddedAppProvider does automatically:

  1. Creates an IframeGuest instance
  2. Sends ready signal to the host
  3. Listens for theme-change, auth-token, module-context messages
  4. Enables auto-resize (watches document.body.scrollHeight via ResizeObserver)
  5. Exposes everything via useEmbeddedApp() hook
  6. Cleans up listeners on unmount

Guest Side (Manual / Non-React)

import { IframeGuest } from '@zudello/ui-preset/iframe'
import { applyTheme } from '@zudello/ui-preset/theme'

const guest = new IframeGuest({ targetOrigin: 'https://app.zudello.com' })

guest.onThemeChange((payload) => {
  applyTheme({ theme: payload.theme, highContrast: payload.highContrast })
})

guest.onAuthToken(({ token }) => {
  // Store token for API calls
  localStorage.setItem('auth_token', token)
})

guest.onModuleContext((ctx) => {
  // ctx.module, ctx.submodule, ctx.title
})

guest.enableAutoResize()
guest.signalReady()

// Send messages to host
guest.navigate('/invoices/123')
guest.openDocument('inv-456', 'purchase-invoices')
guest.notify('success', 'Done!')

// Cleanup
guest.destroy()

Host Side (Zudello Platform)

import { IframeHost } from '@zudello/ui-preset/iframe'

const iframe = document.querySelector('iframe')!
const host = new IframeHost(iframe, { targetOrigin: 'https://expenses.zudello.com' })

// Wait for guest to be ready, then send initial data
host.onReady(() => {
  host.sendTheme({ theme: 'dark-navy', highContrast: false, colorAccent: 'lavender' })
  host.sendAuth({ token: currentUserJwt })
  host.sendModuleContext({ module: 'expenses', title: 'Expense Claims' })
})

// Listen for guest messages
host.onNavigate(({ path, query }) => {
  router.push({ path, query })
})

host.onOpenDocument(({ id, module }) => {
  openDocumentModal(id, module)
})

host.onNotification(({ type, message }) => {
  showToast(type, message)
})

// Auto-resize iframe to match content height
const unsubResize = host.autoResize()

// When user changes theme in host
host.sendTheme({ theme: 'light-warm', highContrast: false })

// Cleanup
host.destroy()

Theme Engine

7 themes + 10 color accents + high contrast mode, matching the Zudello platform exactly.

Themes

| ID | Name | Type | |---|---|---| | light | Light | Default, no filters | | light-soft | Light Soft | brightness 92%, contrast 95% | | light-warm | Light Warm | Warm sepia overlay | | dark-blue-gray | Dark Blue Gray | Dark Reader, bg #1e2128 | | dark-navy | Deep Navy | Dark Reader, bg #1a1b26 | | dark-charcoal | Cool Charcoal | Dark Reader, bg #282c34 | | dark-purple | Purple Tinted | Dark Reader, bg #1c1825 |

Usage

import {
  applyTheme,
  applyColorAccent,
  applyHighContrast,
  AppTheme,
  ColorAccent
} from '@zudello/ui-preset/theme'

// Apply a theme
applyTheme({ theme: AppTheme.DARK_NAVY, highContrast: false })

// Apply a color accent (light themes only)
applyColorAccent(ColorAccent.BLUE, false)

// Toggle high contrast mode
applyHighContrast({ highContrast: true, theme: AppTheme.LIGHT })

Typical iframe integration

<EmbeddedAppProvider
  onThemeChange={(payload) => {
    applyTheme({ theme: payload.theme, highContrast: payload.highContrast })
    if (payload.colorAccent) {
      const isDark = ['dark-blue-gray', 'dark-navy', 'dark-charcoal', 'dark-purple'].includes(payload.theme)
      applyColorAccent(payload.colorAccent, isDark)
    }
  }}
>

Design Tokens

Import @zudello/ui-preset/theme.css to get all 140+ CSS custom properties. These are the source of truth from customer/src/assets/variables/_design-tokens.scss.

Key tokens

/* Colors */
--zd-color-primary: #8b3694;
--zd-color-text-primary: rgb(21, 19, 27);
--zd-color-text-secondary: rgb(53, 44, 58);
--zd-color-text-muted: rgb(101, 105, 114);
--zd-color-surface: rgb(252, 251, 254);
--zd-color-surface-hover: rgb(244, 242, 248);
--zd-color-border: rgb(236, 234, 241);
--zd-color-border-input: #d1d5db;

/* Typography — base is 13px, NOT 16px */
--zd-font-size-xs: 11px;
--zd-font-size-sm: 12px;
--zd-font-size-base: 13px;
--zd-font-size-md: 14px;
--zd-font-size-lg: 16px;

/* Spacing */
--zd-space-1: 2px;  --zd-space-2: 4px;  --zd-space-3: 6px;
--zd-space-4: 8px;  --zd-space-5: 10px; --zd-space-6: 12px;
--zd-space-7: 14px; --zd-space-8: 16px; --zd-space-9: 20px; --zd-space-10: 24px;

/* Radius — default is 6px (md), NOT browser default */
--zd-radius-sm: 4px;
--zd-radius-md: 6px;
--zd-radius-lg: 8px;
--zd-radius-xl: 12px;

/* Shadows */
--zd-shadow-focus-ring: 0 0 0 3px color-mix(in srgb, var(--zd-color-primary) 15%, transparent);
--zd-shadow-popover: 0 4px 16px rgba(0, 0, 0, 0.12), 0 1px 4px rgba(0, 0, 0, 0.06);

/* Button sizing */
--zd-button-height-sm: 24px;
--zd-button-height-md: 31px;
--zd-button-height-lg: 36px;

/* Form controls */
--zd-form-field-height: 36px;
--zd-form-field-height-sm: 31px;

Tailwind v4

theme.css includes an @theme block that maps all tokens to Tailwind utilities. Import the CSS and Tailwind picks them up automatically — no JS config needed:

/* Your app CSS */
@import '@zudello/ui-preset/theme.css';
@import '@zudello/ui-preset/global.css';
@import '@zudello/ui-preset/react.css'; /* React apps using package primitives/domain components */

Then use in templates: text-zd-sm, bg-zd-surface, rounded-zd, shadow-zd-popover, text-zd-text-muted, etc.


Visual Contract (Token Ownership)

Use this package as the visual source of truth. The contract is:

  1. theme.css owns foundational design tokens (--zd-*)
  2. global.css owns primitive/component class styling that references those tokens
  3. react.css or style.css owns framework runtime styles for domain components
  4. grid.css owns grid aliases (--zd-grid-*) that should map to --zd-* when possible
  5. consuming apps own business logic and layout composition, but should not redefine the core --zd-* token set

Allowed consumer overrides

  • Add app-level spacing/layout utilities
  • Add route-specific wrappers and composition styles
  • Override brand accents through documented theme APIs

Avoid in consumers

  • Re-declaring core semantic design tokens already provided by theme.css (--zd-color-*, --zd-font-*, --zd-space-*, etc.)
  • Re-defining --zd-grid-* in app code as a second token source of truth; prefer overriding --zd-* first, then grid aliases only for intentional grid divergence
  • Hardcoding semantic color hex values where --zd-* tokens already exist
  • Re-implementing primitives with raw zd-* class markup when a React/Vue primitive exists (unless intentionally demonstrating CSS-only recipes)

If you need to diverge, document the reason in the consuming app so future embedded apps do not copy accidental drift.


AG Grid

Import the grid theme overlay alongside AG Grid's own CSS:

@import 'ag-grid-community/styles/ag-grid.css';
@import 'ag-grid-community/styles/ag-theme-alpine.css';
@import '@zudello/ui-preset/grid.css';

Wrap your grid with the .zd-grid class and use the canonical grid baseline:

import {
  GRID_CHECKBOX_COLUMN,
  DEFAULT_GRID_PROPS,
  getRowGroupColorStyle,
} from '@zudello/ui-preset/react'

const columnDefs = [
  { ...GRID_CHECKBOX_COLUMN },
  { field: 'status', headerName: 'Status', width: 120, minWidth: 120 },
  { field: 'name', headerName: 'Name', flex: 1 },
]

<div className="zd-grid ag-theme-alpine">
  <AgGridReact
    {...DEFAULT_GRID_PROPS}
    rowData={data}
    columnDefs={columnDefs}
    getRowStyle={(params) =>
      getRowGroupColorStyle(groupStatuses, params.data?.status)
    }
  />
</div>

Grid Baseline

The package exports two constants that enforce deterministic grid rendering:

GRID_CHECKBOX_COLUMN

Canonical checkbox column definition. The CSS in grid.css targets [col-id="checkbox"] for padding, alignment, and status-rail positioning — consumers must use this colId for the grid to render correctly.

GRID_CHECKBOX_COLUMN = {
    colId: 'checkbox',     // ← CSS contract: grid.css targets [col-id="checkbox"]
    headerName: '',
    width: 40,             // 40px fixed (matches customer platform)
    minWidth: 40,
    maxWidth: 40,
    pinned: 'left',        // Pinned so status rail (3px left border) is visible
    lockPosition: true,
    resizable: false,
    sortable: false,
    filter: false,
    checkboxSelection: true,
    headerCheckboxSelection: false,
}

Spread it as the first column. Override individual props if needed (e.g., custom cellRenderer for colored checkboxes):

const columnDefs = [
    { ...GRID_CHECKBOX_COLUMN },   // checkbox column — CSS relies on colId: 'checkbox'
    { field: 'status', ... },
    { field: 'name', ... },
]

DEFAULT_GRID_PROPS

Standard AG Grid component props matching the customer platform:

DEFAULT_GRID_PROPS = {
    rowHeight: 40,                    // 40px rows (matches customer)
    headerHeight: 36,                 // 36px header
    suppressRowClickSelection: true,  // Click opens, doesn't select
    theme: 'legacy',                  // Required for AG Grid v33+ with Alpine CSS
}

Spread onto <AgGridReact>:

<AgGridReact {...DEFAULT_GRID_PROPS} rowData={data} columnDefs={cols} />

Key specs

| Property | Value | Source | |---|---|---| | Row height | 40px | --ag-row-height in grid.css, DEFAULT_GRID_PROPS.rowHeight | | Header height | 36px | --ag-header-height in grid.css, DEFAULT_GRID_PROPS.headerHeight | | Cell font | 12px | --ag-font-size in grid.css | | Cell padding | 12px horizontal | --ag-cell-horizontal-padding in grid.css | | Checkbox column | 40px, pinned left | GRID_CHECKBOX_COLUMN | | Status rail | 3px left border | --group-color on .ag-pinned-left-cols-container .ag-row | | Column separators | Hidden | --ag-header-column-separator-display: none | | Wrapper radius | 8px | .ag-root-wrapper in grid.css |

--group-color contract

The --group-color CSS custom property drives two visual elements in grid.css:

  1. Status rail — 3px colored left border on pinned rows (.ag-pinned-left-cols-container .ag-row)
  2. Checkbox color — border and check/dash color on .zd-group-checkbox elements

Set it per-row via getRowStyle + getRowGroupColorStyle():

<AgGridReact
  getRowStyle={(params) =>
    getRowGroupColorStyle(GROUP_STATUSES, params.data?.status)
  }
/>

Without --group-color, the rail is transparent and checkboxes fall back to the default border color.

Grid Visual Components

Presentation-only primitives for AG Grid status-grouped views:

| Component | Description | |---|---| | GridGroupHeader | Collapsible "Active / 2 items" heading bar with chevron, colored name, count pill, optional group checkbox | | GridCheckbox | Native <input type="checkbox"> with colored border via --group-color and :indeterminate support | | StatusChip | Colored status label/pill (text or filled variant) |

Grid Rail Helper (--group-color)

The --group-color CSS custom property drives row-level color styling throughout grid.css:

  • Left border band — 3px colored left border on pinned rows
  • Checkbox border/check color — on .zd-group-checkbox elements

Set it per-row via AG Grid's getRowStyle, using the provided helper:

import {
  getRowGroupColorStyle,
  GridGroupHeader,
  GridCheckbox,
  StatusChip,
  type GridGroupStatus,
  type StatusItem,
} from '@zudello/ui-preset/react'

const statuses: GridGroupStatus[] = [
  { key: 'active',   name: 'Active',   color: '#4caf50' },
  { key: 'review',   name: 'Review',   color: '#ff9800' },
  { key: 'draft',    name: 'Draft',    color: '#9e9e9e' },
]

// AG Grid getRowStyle callback — sets --group-color per row
<AgGridReact
  getRowStyle={(params) =>
    getRowGroupColorStyle(statuses, params.data?.status)
  }
/>

Or use groupColorStyle(color) directly when you already have the hex color:

import { groupColorStyle } from '@zudello/ui-preset/react'

<AgGridReact getRowStyle={(params) => groupColorStyle(params.data?.statusColor)} />

Canonical Status Color Palette

GRID_STATUS_COLORS provides the canonical hex values used across the platform for status-grouped grids. Use these when defining StatusItem[] or GridGroupStatus[] to ensure checkbox borders, left-rail bands, and group headers all render with the correct colors:

import { GRID_STATUS_COLORS } from '@zudello/ui-preset/react'

// GRID_STATUS_COLORS = {
//   draft:    '#9e9e9e',  // grey
//   review:   '#ff9800',  // amber
//   active:   '#4caf50',  // green  ← matches screenshot
//   approved: '#4caf50',  // green (alias)
//   rejected: '#f44336',  // red
//   complete: '#2196f3',  // blue
//   archived: '#607d8b',  // blue-grey
//   inactive: '#9e9e9e',  // grey
// }

The --group-color CSS variable uses these same hex values. All visual elements in grid.css that read --group-color (left border, checkbox border, checkbox check/dash mark) will render in the matching color.

Page Configuration Helpers

Every embedded page (suppliers, invoices, purchase orders, etc.) needs its own statuses, columns, sort options, and text. The package provides factory helpers and shared defaults so you can set up any page in a few lines while keeping the UI consistent.

createStatuses(definitions)

Creates a StatusItem[] from minimal definitions. Colors auto-resolve from GRID_STATUS_COLORS when the key matches a known status. isEnabled and isSelected default to true.

import { createStatuses } from '@zudello/ui-preset/react'

// Minimal — colors auto-resolve from GRID_STATUS_COLORS
const INVOICE_STATUSES = createStatuses([
  { key: 'draft', name: 'Draft' },           // → #9e9e9e (grey)
  { key: 'review', name: 'Pending Review' },  // → #ff9800 (amber)
  { key: 'approved', name: 'Approved' },       // → #4caf50 (green)
  { key: 'rejected', name: 'Rejected' },       // → #f44336 (red)
])

// Custom color for non-standard statuses
const PO_STATUSES = createStatuses([
  { key: 'open', name: 'Open', color: '#2196f3' },
  { key: 'partial', name: 'Partially Received', color: '#ff9800' },
  { key: 'complete', name: 'Complete' },        // → #2196f3 (blue)
  { key: 'cancelled', name: 'Cancelled', color: '#9e9e9e' },
])

// Start with a status deselected
const STATUSES = createStatuses([
  { key: 'active', name: 'Active' },
  { key: 'archived', name: 'Archived', isSelected: false },
])

Input type: StatusDefinition

| Field | Type | Default | Description | |---|---|---|---| | key | string | required | Status identifier | | name | string | required | Display label | | color | string | GRID_STATUS_COLORS[key] or #9e9e9e | Hex color | | isEnabled | boolean | true | Whether it appears in the filter | | isSelected | boolean | true | Whether it starts selected |

toGroupStatuses(statuses)

Derives GridGroupStatus[] from a StatusItem[]. Use this for getRowGroupColorStyle:

import { toGroupStatuses, getRowGroupColorStyle } from '@zudello/ui-preset/react'

const INVOICE_GROUP_STATUSES = toGroupStatuses(INVOICE_STATUSES)

// AG Grid
<AgGridReact
  getRowStyle={(params) =>
    getRowGroupColorStyle(INVOICE_GROUP_STATUSES, params.data?.status)
  }
/>

COMMON_COLUMN_WIDTHS

Shared column widths for columns that appear on every grid page:

import { COMMON_COLUMN_WIDTHS, type ColumnWidthConfig } from '@zudello/ui-preset/react'

// COMMON_COLUMN_WIDTHS = {
//   select: { width: 40 },            // checkbox column
//   status: { width: 120, minWidth: 120 },  // status column
// }

// Compose with page-specific columns
const INVOICE_COLUMNS: Record<string, ColumnWidthConfig> = {
  select: COMMON_COLUMN_WIDTHS.select,
  status: COMMON_COLUMN_WIDTHS.status,
  invoiceNumber: { width: 150, minWidth: 120 },
  vendor: { minWidth: 180, flex: 1 },
  amount: { width: 120, minWidth: 100 },
  dueDate: { width: 130, minWidth: 110 },
}

ColumnWidthConfig type: { width?: number, minWidth?: number, maxWidth?: number, flex?: number }

DEFAULT_GRID_LAYOUT

Standard AG Grid sizing that matches the customer platform:

import { DEFAULT_GRID_LAYOUT } from '@zudello/ui-preset/react'

// DEFAULT_GRID_LAYOUT = { minHeight: 160, rowHeight: 40, headerHeight: 36 }

<AgGridReact
  rowHeight={DEFAULT_GRID_LAYOUT.rowHeight}
  headerHeight={DEFAULT_GRID_LAYOUT.headerHeight}
/>

DEFAULT_SEARCH_PLACEHOLDER / DEFAULT_EMPTY_MESSAGE

Sensible fallbacks. Override per-page with specific text:

import { DEFAULT_SEARCH_PLACEHOLDER, DEFAULT_EMPTY_MESSAGE } from '@zudello/ui-preset/react'

// DEFAULT_SEARCH_PLACEHOLDER = "Search..."
// DEFAULT_EMPTY_MESSAGE = "No results found for this filter."

Building Any Resource Page

Here's the generic pattern for creating a new embedded page. All you define is your data — statuses, columns, sort options, and placeholder text. The UI stays consistent automatically.

import { useState, useMemo } from 'react'
import { AgGridReact } from 'ag-grid-react'
import type { ColDef, ValueFormatterParams } from 'ag-grid-community'
import {
  // Factories & defaults
  createStatuses, toGroupStatuses, statusLabelMap,
  COMMON_COLUMN_WIDTHS, GRID_CHECKBOX_COLUMN, DEFAULT_GRID_PROPS,
  // Components
  ResourcePageShell, ResourceEmptyState,
  buildResourceToolbar, GridGroupHeader, IconTextCell,
  getRowGroupColorStyle,
  ViewType, type StatusItem, type ColumnWidthConfig,
} from '@zudello/ui-preset/react'

// ── 1. Define your row type ──
type InvoiceRow = {
  id: string
  status: 'draft' | 'review' | 'approved' | 'rejected'
  invoiceNumber: string
  vendor: string
  amount: number
  dueDate: string
}

// ── 2. Create statuses (colors auto-resolve) ──
const STATUSES = createStatuses([
  { key: 'draft', name: 'Draft' },
  { key: 'review', name: 'Pending Review' },
  { key: 'approved', name: 'Approved' },
  { key: 'rejected', name: 'Rejected' },
])
const GROUP_STATUSES = toGroupStatuses(STATUSES)
const LABELS = statusLabelMap(STATUSES)

// ── 3. Define columns (spread common widths) ──
const COLUMNS: Record<string, ColumnWidthConfig> = {
  select: COMMON_COLUMN_WIDTHS.select,
  status: COMMON_COLUMN_WIDTHS.status,
  invoiceNumber: { width: 150, minWidth: 120 },
  vendor: { minWidth: 180, flex: 1 },
  amount: { width: 120, minWidth: 100 },
  dueDate: { width: 130, minWidth: 110 },
}

const COLUMN_DEFS: ColDef<InvoiceRow>[] = [
  { ...GRID_CHECKBOX_COLUMN },
  { field: 'status', headerName: 'Status', ...COLUMNS.status, valueFormatter: (p: ValueFormatterParams) => LABELS[p.value] ?? p.value },
  { field: 'invoiceNumber', headerName: 'Invoice #', ...COLUMNS.invoiceNumber },
  { field: 'vendor', headerName: 'Vendor', ...COLUMNS.vendor },
  { field: 'amount', headerName: 'Amount', ...COLUMNS.amount },
  { field: 'dueDate', headerName: 'Due Date', ...COLUMNS.dueDate },
]

// ── 4. Define sort options ──
const SORT_OPTIONS = [
  { key: 'invoiceNumber', label: 'Invoice number' },
  { key: 'vendor', label: 'Vendor' },
  { key: 'amount', label: 'Amount' },
  { key: 'dueDate', label: 'Due date' },
]

// ── 5. Build the page ──
function InvoicesPage() {
  const [search, setSearch] = useState('')
  const [statuses, setStatuses] = useState<StatusItem[]>(STATUSES)
  const [sortField, setSortField] = useState('invoiceNumber')
  const [ascending, setAscending] = useState(true)
  const [viewType, setViewType] = useState(ViewType.GRID)
  const [showMine, setShowMine] = useState(false)

  // ... filtering/sorting logic ...

  const toolbar = buildResourceToolbar({
    search: { value: search, onChange: setSearch, placeholder: 'Search invoices...' },
    users: { active: showMine, onToggle: () => setShowMine(c => !c) },
    status: {
      statuses,
      onToggle: (s) => setStatuses(prev => prev.map(x => x.key === s.key ? { ...x, isSelected: !x.isSelected } : x)),
      onToggleAll: (all) => setStatuses(prev => prev.map(x => x.isEnabled ? { ...x, isSelected: all } : x)),
    },
    sort: { options: SORT_OPTIONS, value: sortField, ascending, onValueChange: setSortField, onAscendingChange: setAscending },
    view: { value: viewType, onChange: setViewType },
  })

  return (
    <ResourcePageShell title="Invoices" count={filtered.length} showCount toolbar={toolbar}
      empty={filtered.length === 0}
      emptySlot={<ResourceEmptyState message="No invoices found for this filter." showClearButton onClear={clearFilters} />}
    >
      <div className="zd-grid-group-layout">
        <GridGroupHeader name="All Invoices" count={filtered.length} color="var(--zd-grid-text)" />
        <div className="zd-grid ag-theme-alpine" style={{ height: 400 }}>
          <AgGridReact
            {...DEFAULT_GRID_PROPS}
            rowData={filtered}
            columnDefs={COLUMN_DEFS}
            getRowStyle={(params) => getRowGroupColorStyle(GROUP_STATUSES, params.data?.status)}
          />
        </div>
      </div>
    </ResourcePageShell>
  )
}

What changes per page: row type, status definitions, column definitions, sort options, placeholder text. What stays the same: all UI components, toolbar composition, grid styling, color palette, layout sizing.

Supplier Presets

Pre-built constants for the suppliers page, built on top of the generic helpers (createStatuses, toGroupStatuses, COMMON_COLUMN_WIDTHS, DEFAULT_GRID_LAYOUT). Use these as-is for the suppliers page, or as a reference for creating your own page presets:

import {
  SUPPLIER_STATUSES,          // StatusItem[] with canonical GRID_STATUS_COLORS
  SUPPLIER_GROUP_STATUSES,    // GridGroupStatus[] derived from SUPPLIER_STATUSES
  SUPPLIER_SORT_OPTIONS,      // SortOption[] — legalName, tradingAs, code
  SUPPLIER_COLUMN_WIDTHS,     // { select: {width:44}, status: {width:120, minWidth:120}, ... }
  SUPPLIER_SEARCH_PLACEHOLDER,// "Search suppliers..."
  SUPPLIER_EMPTY_MESSAGE,     // "No suppliers found for this filter."
  SUPPLIER_GRID_LAYOUT,       // { minHeight: 160, rowHeight: 40, headerHeight: 36 }
  statusLabelMap,             // (statuses) => Record<string, string> — derives labels from StatusItem.name
  IconTextCell,               // <IconTextCell icon="fal fa-file-lines">{value}</IconTextCell>
} from '@zudello/ui-preset/react'

Canonical Example: Suppliers Status Grid

Full toolbar + status-grouped grid using presets and buildResourceToolbar:

import {
  EmbeddedAppProvider,
  ResourcePageShell, ResourceEmptyState,
  buildResourceToolbar, GridGroupHeader, IconTextCell,
  getRowGroupColorStyle, statusLabelMap,
  GRID_CHECKBOX_COLUMN, DEFAULT_GRID_PROPS,
  SUPPLIER_STATUSES, SUPPLIER_GROUP_STATUSES,
  SUPPLIER_SORT_OPTIONS, SUPPLIER_COLUMN_WIDTHS,
  SUPPLIER_SEARCH_PLACEHOLDER, SUPPLIER_EMPTY_MESSAGE,
  ViewType, type StatusItem,
} from '@zudello/ui-preset/react'
import { applyTheme } from '@zudello/ui-preset/theme'
import { AgGridReact } from 'ag-grid-react'
import type { ColDef } from 'ag-grid-community'

type SupplierRow = { id: string; status: string; type: string; legalName: string; tradingAs: string; code: string; taxNumber: string; assignee: string }

const STATUS_LABELS = statusLabelMap(SUPPLIER_STATUSES)

const COLUMN_DEFS: ColDef<SupplierRow>[] = [
  { ...GRID_CHECKBOX_COLUMN },
  { field: 'status', headerName: 'Status', ...SUPPLIER_COLUMN_WIDTHS.status, valueFormatter: (p) => STATUS_LABELS[p.value ?? ''] ?? p.value },
  { field: 'type', headerName: 'Type', ...SUPPLIER_COLUMN_WIDTHS.type, cellRenderer: (p: {value: string}) => <IconTextCell>{p.value}</IconTextCell> },
  { field: 'legalName', headerName: 'Legal Name', ...SUPPLIER_COLUMN_WIDTHS.legalName },
  { field: 'tradingAs', headerName: 'Trading As', ...SUPPLIER_COLUMN_WIDTHS.tradingAs },
  { field: 'code', headerName: 'Code', ...SUPPLIER_COLUMN_WIDTHS.code },
  { field: 'taxNumber', headerName: 'Tax Number', ...SUPPLIER_COLUMN_WIDTHS.taxNumber },
  { field: 'assignee', headerName: 'Assignee', ...SUPPLIER_COLUMN_WIDTHS.assignee },
]

function SuppliersPage() {
  const [search, setSearch] = useState('')
  const [statuses, setStatuses] = useState<StatusItem[]>(SUPPLIER_STATUSES)
  const [sortField, setSortField] = useState('legalName')
  const [sortAsc, setSortAsc] = useState(true)
  const [viewType, setViewType] = useState(ViewType.GRID)
  const [showMine, setShowMine] = useState(false)

  // buildResourceToolbar: pass config, get composed toolbar — no hand-ordering needed
  const toolbar = buildResourceToolbar({
    search: { value: search, onChange: setSearch, placeholder: SUPPLIER_SEARCH_PLACEHOLDER },
    users: { active: showMine, onToggle: () => setShowMine(!showMine) },
    status: {
      statuses,
      onToggle: (s) => setStatuses(prev => prev.map(x => x.key === s.key ? {...x, isSelected: !x.isSelected} : x)),
      onToggleAll: (all) => setStatuses(prev => prev.map(x => x.isEnabled ? {...x, isSelected: all} : x)),
    },
    sort: { options: SUPPLIER_SORT_OPTIONS, value: sortField, ascending: sortAsc, onValueChange: setSortField, onAscendingChange: setSortAsc },
    view: { value: viewType, onChange: setViewType },
  })

  return (
    <ResourcePageShell title="Suppliers" count={filteredData.length} showCount toolbar={toolbar}
      empty={filteredData.length === 0}
      emptySlot={<ResourceEmptyState message={SUPPLIER_EMPTY_MESSAGE} showClearButton onClear={clearFilters} />}
    >
      {/* .zd-grid-group-layout provides gap-8 + pb-24 + minHeight on nested .zd-grid */}
      <div className="zd-grid-group-layout">
        <GridGroupHeader name="Active" count={filteredData.length} color="var(--zd-grid-text)" />
        <div className="zd-grid ag-theme-alpine" style={{ height: 240 }}>
          <AgGridReact
            {...DEFAULT_GRID_PROPS}
            rowData={filteredData}
            columnDefs={COLUMN_DEFS}
            getRowStyle={(params) => getRowGroupColorStyle(SUPPLIER_GROUP_STATUSES, params.data?.status)}
          />
        </div>
      </div>
    </ResourcePageShell>
  )
}

function App() {
  return (
    <EmbeddedAppProvider autoResize onThemeChange={(p) => applyTheme({ theme: p.theme, highContrast: p.highContrast })}>
      <SuppliersPage />
    </EmbeddedAppProvider>
  )
}

shadcn/ui Integration

For apps that need additional primitives beyond what this package ships, a components.json template is included.

# Copy the template to your app root
cp node_modules/@zudello/ui-preset/components.json .

# Generate components
npx shadcn@latest add button dialog switch input radio-group

Generated components will use Zudello tokens automatically because your CSS imports theme.css. See the shadcn setup doc for parity adjustments.


CSS Imports Cheatsheet

/* React minimum — tokens + primitives + domain runtime styles */
@import '@zudello/ui-preset/theme.css';
@import '@zudello/ui-preset/global.css';
@import '@zudello/ui-preset/react.css';

/* Vue minimum — tokens + global styles + Vue scoped component styles */
@import '@zudello/ui-preset/theme.css';
@import '@zudello/ui-preset/global.css';
@import '@zudello/ui-preset/style.css';

/* AG Grid (if using) */
@import 'ag-grid-community/styles/ag-grid.css';
@import 'ag-grid-community/styles/ag-theme-alpine.css';
@import '@zudello/ui-preset/grid.css';

Order matters — theme.css first (defines tokens), then global.css, then framework runtime CSS (react.css or style.css), then app overrides.


Shared Types

interface StatusItem {
  key: string
  name: string
  color: string       // Hex color for the status dot
  isEnabled: boolean   // Whether this status appears in the filter
  isSelected: boolean  // Whether this status is currently selected
}

// Minimal input for createStatuses() — only key and name are required
interface StatusDefinition {
  key: string
  name: string
  color?: string        // Falls back to GRID_STATUS_COLORS[key]
  isEnabled?: boolean   // Default: true
  isSelected?: boolean  // Default: true
}

interface SortOption {
  key: string
  label: string
}

enum ViewType {
  LIST = 'LIST',
  BOARD = 'BOARD',
  GRID = 'GRID'
}

type CheckboxState = 'none' | 'some' | 'all'

interface GridGroupStatus {
  key: string
  name: string
  color: string
}

// Generic column width config — spread into AG Grid ColDef
interface ColumnWidthConfig {
  width?: number
  minWidth?: number
  maxWidth?: number
  flex?: number
}

Development

# Vue dev server (port 5188)
npm run dev

# React dev server (port 5189)
npm run dev:react

# Build everything
npm run build

# Build individual targets
npm run build:vue
npm run build:react
npm run build:iframe
npm run build:theme

Architecture

src/
├── components/          # Vue 3 components (9)
├── react/               # React components (11 domain + 18 primitives)
│   └── primitives/      # Button, Dialog, Input, Switch, Alert, Badge, etc.
├── iframe/              # Framework-agnostic postMessage bridge
│   ├── bridge.ts        # Low-level send/receive
│   ├── host.ts          # Host-side API (Zudello platform)
│   ├── guest.ts         # Guest-side API (embedded app)
│   └── types.ts         # Message type definitions
├── presets/             # Page-specific preset configs (built on factories/defaults)
│   └── suppliers.ts     # Supplier page presets (example for other pages)
├── theme/               # Theme engine
│   ├── themes.ts        # 7 theme definitions
│   ├── applyTheme.ts    # DarkReader integration
│   ├── colorAccents.ts  # 10 color accent variants
│   └── highContrast.ts  # Accessibility mode
├── utils/
│   ├── color.ts         # pickContrastColor
│   ├── grid.ts          # getRowGroupColorStyle, groupColorStyle
│   └── status.ts        # statusLabelMap, selectedStatusCount
├── styles/
│   ├── theme.css        # Design tokens + Tailwind @theme
│   ├── global.css       # Component CSS classes
│   └── zd-grid.css      # AG Grid overlay
├── factories.ts         # createStatuses, toGroupStatuses — generic config builders
├── defaults.ts          # COMMON_COLUMN_WIDTHS, DEFAULT_GRID_LAYOUT, ColumnWidthConfig
├── grid.ts              # GRID_CHECKBOX_COLUMN, DEFAULT_GRID_PROPS — canonical grid baseline
└── types.ts             # Shared interfaces (StatusItem, SortOption, ViewType, GRID_STATUS_COLORS)