-
Notifications
You must be signed in to change notification settings - Fork 0
Description
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 underlib/utils.ts - Hooks under
hooks/ - Styles via
style.csswhich importsstyles/shadcn.cssandstyles/animations.css
- Flat shadcn component files under
-
app/modules/datum-ui- Datum-branded and extended components
- Barrel exports (currently
index.tsexports 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:
-
DataTableimports app componentsEmptyContent(currently greets the user and uses app images +useApp())MoreActionsPageTitle
-
DataTablefilter code imports app state + utilsuseApp()for user timezone preferencestimezone.helper.tsfor timezone-aware day boundariesuseDebounce()from app hooks
-
Toast bridging uses app cookie types
useToast()importsToasttype 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/uiand Storybook. - Mark them as
"private": trueand 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 likeuseDebounce)
- These exist to keep the codebase modular and maintainable, but are only consumed by
-
@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
- Components:
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
@themeblocks 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)”)
- Imports real package CSS in the same order apps will use:
Why a monorepo?
@datum/uidepends 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-domtailwindcss(required — library styles rely on Tailwind v4 processing, including@themeblocks)
-
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:
DataTablerenders app-onlyEmptyContent(which usesuseApp()and app images)- Some actions/toolbar code uses
MoreActionsandPageTitlewhich are currently app components
Recommended fix:
- Move
PageTitleinto@datum/ui(it’s already generic) - Move
MoreActionsinto@datum/ui(already built on design system primitives) - Replace
EmptyContentusage with a library component such asEmptyState- No app images and no “Hey {user}”
- Support a
title,subtitle,actions, and arenderIllustration?slot
- Update
DataTableto accept:emptyStateProps?: EmptyStateProps(library-provided default)
2) Remove useApp() and app timezone helpers from date filter components
Current issues:
DatePickerFilterreads timezone from the app provider (useApp())- It uses app timezone helpers
Recommended fix:
- Make
DatePickerFilteraccepttimezone?: stringand/or a callback likegetTimezone?: () => 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
useDebouncefrom app hooks
Recommended fix:
- Move
useDebounceinto@datum/utils(or@datum/ui/hooks) - Update
useSearchStateto 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)
- Recommended for public publish:
- Pick workspace tooling:
pnpmorbunworkspaces
- 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; fromapp/modules/shadcn)packages/ui-tokens(private; fromapp/styles/tokens/*+ datum theme CSS)packages/ui-utils(private; shared hooks + timezone helpers)
- Create
apps/storybookand wire it to the packages (CSS, Tailwind v4, theming) - Add build (Vite library mode, tsup, or rollup) to emit:
- ESM output
.d.tstypes- CSS assets copied/published
- Ensure
exportsinpackage.jsoncover subpaths (ui/*,lib/utils, etc.)
Phase 2 (3–10 days): decouple app dependencies and cut a beta release
- Port
PageTitle,MoreActionsinto@datum/ui - Replace
EmptyContentusage 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/shadcnandapp/modules/datum-ui(or keep temporarily as thin re-export adapters) - Replace TS path aliases with package imports
- Remove
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 buildin CI to produceapps/storybook/storybook-static/ - Treat the output as the deployable artifact
Container
- Add
apps/storybook/Dockerfile(or a root-levelDockerfile.storybook) that:- Builds the repo
- Runs
storybook buildforapps/storybook - Copies the
storybook-static/directory into annginx(orcaddy) image
- Expose
:80and 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:
- component development in Storybook
- design system package build correctness (types, exports, CSS)
- 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/uivia 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
- In the design system repo: run a watch build for
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.xbetas
- Mitigation: keep a compatibility adapter layer in cloud-portal temporarily; publish
-
shadcn update workflow
- Mitigation: keep
components.json+ generator config in the design system repo; restrict updates to a controlled process
- Mitigation: keep
-
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/uior preserve@datum-ui/componentsstyle 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
- Confirm desired publishing target (public npm vs private registry)
- Decide package names (
@datum/*recommended) - Approve the package layout + decoupling strategy (especially for
DataTableand toast bridging) - Spin up the new repo and start Phase 1 extraction