Skip to content

[Proposal]: Create a new monorepo and publishable package for Datum UI #537

@mattdjenkinson

Description

@mattdjenkinson

Hey folks, would love your thoughts + feedback on this proposal to package-ify our design system so we can have a shared component and design system between cloud-portal, staff-portal and auth-ui. I'm using Cloud Portal as the source of truth as that has the most mature version of the Olli design system.

Proposal: Extract datum-ui + shadcn into a publishable design system

Executive summary

We should extract the UI primitives currently living in app/modules/shadcn and the Datum-branded components in app/modules/datum-ui into a separate repo and publish them as versioned packages. This will enable:

  • Reuse across multiple Datum apps (consistent UI + faster delivery)
  • A clear upgrade path (semver + changelog)
  • Better isolation

The main work is decoupling a handful of app-specific dependencies currently pulled into datum-ui (notably the DataTable and toast bridging).

Context: current state in cloud-portal

What we have today

  • app/modules/shadcn

    • Flat shadcn component files under ui/
    • Shared cn() util under lib/utils.ts
    • Hooks under hooks/
    • Styles via style.css which imports styles/shadcn.css and styles/animations.css
  • app/modules/datum-ui

    • Datum-branded and extended components
    • Barrel exports (currently index.ts exports from ./components)
    • Styles via datum-ui.css

Current integration points

  • TypeScript path aliases in tsconfig.json:

    • @shadcn/* → app/modules/shadcn/*
    • @datum-ui/* → app/modules/datum-ui/*
  • CSS import order (in app/styles/root.css):

    • Figma tokens
    • shadcn base styles (../modules/shadcn/style.css)
    • datum-ui styles (../modules/datum-ui/datum-ui.css)
    • theme overrides (./themes/alpha.css)

App-specific couplings we must address to be publishable

The design system packages cannot depend on app-only modules like @/providers/*, server cookies, or app routes.

Today, datum-ui has a few such couplings:

  • DataTable imports app components

    • EmptyContent (currently greets the user and uses app images + useApp())
    • MoreActions
    • PageTitle
  • DataTable filter code imports app state + utils

    • useApp() for user timezone preferences
    • timezone.helper.ts for timezone-aware day boundaries
    • useDebounce() from app hooks
  • Toast bridging uses app cookie types

    • useToast() imports Toast type from @/utils/cookies (server cookie module)

This proposal handles these by either:

  • Moving truly reusable pieces into the design system repo, or
  • Replacing hard imports with “ports” (component props / adapters) so consumers can wire their own app state.

Goals / non-goals

Goals

  • Publishable React design system for internal/external apps
  • Stable import paths, good DX (subpath exports, typed, tree-shakeable)
  • Tailwind v4-compatible styling consistent with current approach
  • Upgradeable via semver + automated releases

Non-goals (initially)

  • Refactoring every Datum component before first release (we can phase it)

Recommended target: a small monorepo with internal packages, but only one published npm package

Create a new repo, e.g. datum/datum-ui OR datum/olli-ui, using a workspace layout:

Package layout (publish only @datum/ui)

  • Internal workspace packages (NOT published)

    • These exist to keep the codebase modular and maintainable, but are only consumed by @datum/ui and Storybook.
    • Mark them as "private": true and ensure CI only publishes @datum/ui.
    • Suggested internal packages:
      • @datum/ui-shadcn (vendored shadcn/ui components, hooks, cn util, styles)
      • @datum/ui-tokens (Figma token CSS + Datum theme CSS)
      • @datum/ui-utils (timezone helpers + small shared hooks like useDebounce)
  • @datum/ui

    • The ONLY published npm package
    • Internally depends on the private workspace packages (shadcn/tokens/utils) and re-exports them
    • Public exports (examples; exact names TBD):
      • Components:
        • @datum/ui (barrel of Datum components)
      • Shadcn primitives (re-exported, not a separately published package):
        • @datum/ui/shadcn/ui/*
        • @datum/ui/shadcn/lib/utils
        • @datum/ui/shadcn/hooks/*
        • @datum/ui/shadcn/style.css
      • Tokens + themes (re-exported):
        • @datum/ui/tokens.css
        • @datum/ui/themes/*
      • Utilities (re-exported):
        • @datum/ui/utils/timezone
        • @datum/ui/utils/useDebounce

Storybook app

Add a dedicated Storybook app in the same repo (not published to npm), e.g.:

  • apps/storybook
    • Imports real package CSS in the same order apps will use:
      • @datum/ui/tokens.css
      • @datum/ui/shadcn/style.css
      • @datum/ui/style.css
    • Uses Tailwind v4 so @theme blocks are processed exactly like in production apps
    • Becomes the primary place to review component changes and test visual regressions
    • Deployed on Datum infrastructure as a dockerized static site (see “Storybook hosting (self-hosted via Docker)”)

Why a monorepo?

  • @datum/ui depends on shadcn primitives, tokens, and shared utilities
  • Shared build tooling (tsconfig, bundler, linting) is simpler
  • Internal modularity helps keep shadcn/vendor changes isolated while presenting a single public API

Public API & dependency policy

React and runtime dependencies

Recommended dependency strategy:

  • Peer dependencies

    • react, react-dom
    • tailwindcss (required — library styles rely on Tailwind v4 processing, including @theme blocks)
  • Direct dependencies (bundled as normal deps)

    • @radix-ui/*, class-variance-authority, clsx, tailwind-merge, lucide-react, sonner, etc.

This minimizes “dependency diamond” problems while ensuring apps don’t accidentally ship multiple React copies.

CSS strategy (keep current model, but make it package-friendly)

We should continue shipping CSS as importable files from the packages:

  • App’s root CSS should do something like:
    • @import '@datum/ui/tokens.css';
    • @import '@datum/ui/shadcn/style.css';
    • @import '@datum/ui/style.css';
    • @import './app-theme.css'; (app-specific overrides last)

If Tailwind v4 @theme blocks are used in the shipped CSS, then:

  • Consumers must compile these CSS files through Tailwind v4

Decoupling work (what must change before publishing)

1) Replace app-specific components inside DataTable

Current issues:

  • DataTable renders app-only EmptyContent (which uses useApp() and app images)
  • Some actions/toolbar code uses MoreActions and PageTitle which are currently app components

Recommended fix:

  • Move PageTitle into @datum/ui (it’s already generic)
  • Move MoreActions into @datum/ui (already built on design system primitives)
  • Replace EmptyContent usage with a library component such as EmptyState
    • No app images and no “Hey {user}”
    • Support a title, subtitle, actions, and a renderIllustration? slot
  • Update DataTable to accept:
    • emptyStateProps?: EmptyStateProps (library-provided default)

2) Remove useApp() and app timezone helpers from date filter components

Current issues:

  • DatePickerFilter reads timezone from the app provider (useApp())
  • It uses app timezone helpers

Recommended fix:

  • Make DatePickerFilter accept timezone?: string and/or a callback like getTimezone?: () => string
  • Keep timezone boundary helper functions exported from @datum/ui/utils/timezone

This avoids coupling the UI library to any app-level providers.

3) Make useSearchState self-contained

Current issue:

  • Imports useDebounce from app hooks

Recommended fix:

  • Move useDebounce into @datum/utils (or @datum/ui/hooks)
  • Update useSearchState to import from that package, not the app

4) Split “server toast cookie” from “client toast display”

Current issue:

  • UI hook useToast() depends on server cookie types in @/utils/cookies

Recommended fix:

  • In the design system, define a small UI-safe type, e.g.:
    • { id?: string; type: 'success'|'error'|'info'|'warning'; title?: string; description?: string }
  • Keep server cookie parsing in each app (cloud-portal can adapt its server cookie to the UI type)

Proposed migration plan

Phase 0 (1–2 days): decisions + repo bootstrap

  • Confirm package naming:
    • Recommended for public publish: @datum/ui (single package)
  • Pick workspace tooling:
    • pnpm or bun workspaces
  • Add Changesets + CI skeleton

Phase 1 (3–7 days): extract code + make it build

  • Create new repo with:
    • packages/ui (published @datum/ui)
    • packages/ui-shadcn (private; from app/modules/shadcn)
    • packages/ui-tokens (private; from app/styles/tokens/* + datum theme CSS)
    • packages/ui-utils (private; shared hooks + timezone helpers)
  • Create apps/storybook and wire it to the packages (CSS, Tailwind v4, theming)
  • Add build (Vite library mode, tsup, or rollup) to emit:
    • ESM output
    • .d.ts types
    • CSS assets copied/published
  • Ensure exports in package.json cover subpaths (ui/*, lib/utils, etc.)

Phase 2 (3–10 days): decouple app dependencies and cut a beta release

  • Port PageTitle, MoreActions into @datum/ui
  • Replace EmptyContent usage with a library empty state or slots
  • Remove useApp() and app timezone helper imports from UI code
  • Replace toast cookie coupling with an adapter pattern

Publish 0.x prereleases (e.g. 0.1.0-beta.1) for early integration.

Phase 3 (1–2 weeks): integrate into a second app + harden

  • Consume packages from npm (or registry) in:
    • cloud-portal
    • auth-ui
  • Fix any missing exports, styling assumptions, or bundling problems
  • Add Storybook docs + basic visual regression

Phase 4: stable 1.0 + deprecation of in-repo modules

  • Publish 1.0.0
  • In cloud-portal:
    • Remove app/modules/shadcn and app/modules/datum-ui (or keep temporarily as thin re-export adapters)
    • Replace TS path aliases with package imports

Publishing and distribution options

Public npm as a single package (@datum/ui)

  • Best developer experience for “any app can use it”
  • Works with Renovate/Dependabot easily
  • CI can publish on merge to main with Changesets
  • Requires owning the npm scope/org for @datum/ui (or whichever scope we choose)
  • Recommended: enable npm provenance / signed publishing in CI for supply-chain integrity

Storybook distribution options (independent of package publishing):

  • Keep Storybook internal even if packages are public
  • Host Storybook on Datum infrastructure using Docker

Storybook hosting (self-hosted via Docker)

Recommended model: build Storybook to static assets, then serve it from a minimal web container.

Build

  • Run storybook build in CI to produce apps/storybook/storybook-static/
  • Treat the output as the deployable artifact

Container

  • Add apps/storybook/Dockerfile (or a root-level Dockerfile.storybook) that:
    • Builds the repo
    • Runs storybook build for apps/storybook
    • Copies the storybook-static/ directory into an nginx (or caddy) image
  • Expose :80 and serve the static assets

Deployment

  • Deploy the image to Datum infra
  • Put it behind an internal auth boundary if needed (don't know if we need to do this or not?)
  • Add a health endpoint

Versioning strategy for Storybook

  • Tag/publish a Storybook image on each merge to main
  • Optional: publish a second “release” tag when Changesets publishes npm packages (keeps Storybook aligned with the latest released UI version)

Local development workflow

We want fast iteration in three loops:

  1. component development in Storybook
  2. design system package build correctness (types, exports, CSS)
  3. validation inside a real consuming app (e.g. cloud-portal)

Loop 1: develop components in Storybook (recommended default)

  • Run the repo in “dev mode”:
    • watch builds for internal packages (or use workspace TS references)
    • run Storybook dev server
  • Storybook should import the same CSS entrypoints consumers will use:
    • @datum/ui/tokens.css@datum/ui/shadcn/style.css@datum/ui/style.css

This catches the majority of regressions quickly without needing a separate app.

Loop 2: develop against a consuming app (cloud-portal)

  • File/link dependency + watch builds
    • In the design system repo: run a watch build for packages/ui (and any internal packages as needed)
    • In cloud-portal: install @datum/ui via a local path, e.g. file:../datum-ui/packages/ui (exact path depends on your folder layout)
    • This gives near-real-time updates without publishing to npm

Developer ergonomics additions

  • Add a single root command that:
    • installs dependencies
    • starts watch builds
    • starts Storybook
  • Add a second root command to:
    • build all packages
    • run typecheck
    • build Storybook (so CI can fail fast)

Release automation

  • Changesets for versioning + changelog
  • GitHub Actions to:
    • Build + test packages
    • Publish on main merges
    • Optionally use npm provenance / signed releases

Risks and mitigations

  • Tailwind v4 coupling

    • Mitigation: document Tailwind requirements; optionally provide a Tailwind preset package
  • Breaking changes during extraction

    • Mitigation: keep a compatibility adapter layer in cloud-portal temporarily; publish 0.x betas
  • shadcn update workflow

    • Mitigation: keep components.json + generator config in the design system repo; restrict updates to a controlled process
  • Asset coupling (images/fonts)

    • Mitigation: avoid app images in library components; if needed, ship assets via package and reference them explicitly

Open questions

  • Scope/ownership: Do we want this publishable publicly, or internal-only?
  • Naming: Do we want @datum/ui or preserve @datum-ui/components style imports?
  • Tokens pipeline: Will Figma tokens continue to be exported as CSS in the same format?
  • Theming: Do we want multiple themes (e.g. alpha/beta) supported as first-class exports?

Next steps

  1. Confirm desired publishing target (public npm vs private registry)
  2. Decide package names (@datum/* recommended)
  3. Approve the package layout + decoupling strategy (especially for DataTable and toast bridging)
  4. Spin up the new repo and start Phase 1 extraction

Metadata

Metadata

Type

No type

Projects

No projects

Relationships

None yet

Development

No branches or pull requests

Issue actions