Skip to content

CloudAlt Component Layering - Complete Guide

Comprehensive Documentation for the Three-Tier Component Architecture

Last Updated: October 18, 2025
Status: ✅ Active


  1. Overview
  2. Architecture Model
  3. Quick Start Guide
  4. Nx Layer Tagging System
  5. Component Promotion Guide
  6. Storybook Integration
  7. Design Tokens & Theming
  8. Testing & Validation
  9. Best Practices
  10. Troubleshooting
  11. Phase 1 Implementation Summary

CloudAlt uses a three-tier component architecture designed to maximize component reuse across our multi-brand monorepo while enabling app-specific customization when needed.

┌─────────────────────────────────────┐
│ App Layer (@app/components) │ ← App-specific overrides
│ Last-mile customizations │
└──────────────┬──────────────────────┘
│ imports from ↓
┌──────────────▼──────────────────────┐
│ Division Layer (ui-<division>) │ ← Shared within family
│ Brand-family patterns │
└──────────────┬──────────────────────┘
│ imports from ↓
┌──────────────▼──────────────────────┐
│ Core Layer (ui, ui-web) │ ← Generic primitives
│ Cross-app components │
└─────────────────────────────────────┘
  1. Bottom-Up Dependencies: Apps depend on Division, Division depends on Core
  2. Promotion Over Duplication: Successful patterns move up the stack
  3. Token-First Theming: Visual customization via design tokens
  4. Enforcement: Nx module boundaries ensure architectural integrity

Scope: Cross-app primitives that work for all brands/divisions.

Location:

  • packages/ui — Platform-agnostic core components
  • packages/ui-web — Web-specific core components (shadcn-based)
  • Supporting packages: packages/common, packages/icons, packages/theme, packages/brands, packages/config

Import pattern:

import { Button, Input, Typography } from '@cloudalt-frontend/ui-web'
import { Icon } from '@cloudalt-frontend/icons'

Dependencies: Only core packages and external libraries. No division or app code.

Nx tags: ["layer:core-ui", "scope:shared"]


Scope: Patterns shared within a brand family (e.g., Stay Match apps share hero sections, CTA blocks, navigation menus, auth flows).

Location: packages/ui-<division>

Existing divisions:

  • ui-stay-match — pinkguest, orangeguest, purpleguest, rainbowhost, staymatch_app
  • ui-altfinder — altFinder_app, orangeFinder, pinkFinder
  • ui-roommate — roommate_guru, roommate_help, roommate_works
  • ui-guestroom — guestroom_city, guestroom_host, guestroom_travel, etc.
  • ui-bonjour-services — bonjour_locker, bonjour_it_com, lingua_it_com
  • ui-greenhost, ui-homestay, ui-pride-city, ui-room-lgbt, ui-hostguest

Import pattern:

import { Hero, StayMatchNavigation } from '@cloudalt-frontend/ui-stay-match'

Dependencies: Can depend on Core layer only. Cannot depend on other divisions or apps.

Nx tags: ["layer:division-ui", "division:<name>"]


3. App Layer (App-Specific Customizations)

Section titled “3. App Layer (App-Specific Customizations)”

Scope: Last-mile “this app only” components — branded wrappers, page-level composites, one-off variations.

Location: apps/<division>/<app>/<platform>/src/components/

Import pattern:

import { Hero } from '@app/components' // App-specific override

How it works:

  • Each app defines a local @app/components path alias in its tsconfig.app.json
  • Override wraps/extends the division or core component
  • Keeps same props API for easy promotion later

Dependencies: Can depend on Division and Core layers.

Nx tags: ["layer:app", "division:<name>", "platform:web|mobile"]


When working in an app, follow this import order:

  1. ✅ Division components (if available):

    import { Hero } from '@cloudalt-frontend/ui-stay-match'
  2. ✅ Core components (fallback):

    import { Button } from '@cloudalt-frontend/ui-web'
    import { Icon } from '@cloudalt-frontend/icons'
  3. ⚠️ App overrides (use sparingly):

    import { Hero } from '@app/components'

Good reasons:

  • ✅ App-specific branding or styling
  • ✅ One-off layout adjustments
  • ✅ Integration with app-specific state/context
  • ✅ Testing a new pattern before promoting

Bad reasons:

  • ❌ Fixing a bug (fix it in the division/core instead)
  • ❌ Adding a feature useful to other apps (promote it)
  • ❌ Working around poor component design (refactor the source)

Terminal window
# In your app directory
touch apps/<division>/<app>/<platform>/src/components/Hero.tsx

Option A: Wrap the division component

apps/stay_match/pinkguest/web/src/components/Hero.tsx
import { Hero as DivisionHero, HeroProps } from '@cloudalt-frontend/ui-stay-match'
export const Hero = (props: HeroProps) => {
return (
<DivisionHero
{...props}
className="pinkguest-custom-hero"
theme="gradient"
/>
)
}

Option B: Extend with additional props

import { Hero as DivisionHero, HeroProps } from '@cloudalt-frontend/ui-stay-match'
interface PinkguestHeroProps extends HeroProps {
showPromotion?: boolean
}
export const Hero = ({ showPromotion, ...props }: PinkguestHeroProps) => {
return (
<div>
<DivisionHero {...props} />
{showPromotion && <div className="promotion-banner">Special Offer!</div>}
</div>
)
}

Option C: Fully custom (same API)

import { HeroProps } from '@cloudalt-frontend/ui-stay-match'
export const Hero = ({ title, subtitle, image }: HeroProps) => {
return (
<section className="pinkguest-hero">
<h1>{title}</h1>
<p>{subtitle}</p>
<img src={image} alt="" />
</section>
)
}
apps/stay_match/pinkguest/web/src/components/index.ts
export { Hero } from './Hero'
apps/stay_match/pinkguest/web/src/pages/Home.tsx
import { Hero } from '@app/components'
export const HomePage = () => {
return <Hero title="Welcome to Pink Guest" />
}

All projects in the monorepo are tagged with layer identifiers that enforce architectural boundaries through ESLint’s @nx/enforce-module-boundaries rule.

┌─────────────────────────────────────┐
│ layer:core-ui │ ← Generic, reusable components
│ packages/ui, packages/ui-web │ (Button, Card, Modal, etc.)
└─────────────────────────────────────┘
↓ can depend on
┌─────────────────────────────────────┐
│ layer:division-ui │ ← Division-specific components
│ packages/ui-stay-match │ (ListingCard, SearchFilters)
│ packages/ui-altfinder │
│ packages/ui-roommate (etc.) │
└─────────────────────────────────────┘
↓ can depend on
┌─────────────────────────────────────┐
│ layer:app │ ← App-specific customizations
│ apps/stay_match/pinkguest/web │ (rare, only when needed)
│ apps/roommate/roommate_guru/web │
└─────────────────────────────────────┘

Applied to: packages/ui, packages/ui-web

Can depend on:

  • Other layer:core-ui packages
  • scope:shared utilities (types, hooks, services)
  • type:lib or type:package foundational packages

Cannot depend on:

  • layer:division-ui packages
  • layer:app projects

Additional tags:

  • scope:shared - Indicates shared/common usage
  • platform:web (for ui-web only)

Applied to: packages/ui-{division} (e.g., ui-stay-match, ui-altfinder)

Can depend on:

  • layer:core-ui packages (core components)
  • Other layer:division-ui packages (cross-division reuse, use sparingly)
  • scope:shared utilities
  • type:lib or type:package foundational packages

Cannot depend on:

  • layer:app projects

Additional tags:

  • division:{name} - Links to division (e.g., division:stay-match)

Applied to: apps/**/* (all web and mobile apps)

Can depend on:

  • Everything (no restrictions)
  • Recommended: Prefer your division’s UI package

Additional tags:

  • division:{name} - Division membership
  • platform:web or platform:mobile - Platform identifier

Tags are automatically assigned to project.json files using the tagging script:

Terminal window
node scripts/add-layer-tags.mjs

Enforcement is configured in eslint.config.mjs:

'@nx/enforce-module-boundaries': [
'error',
{
depConstraints: [
// Core UI: Only depend on core/shared packages
{
sourceTag: 'layer:core-ui',
onlyDependOnLibsWithTags: ['layer:core-ui', 'scope:shared', 'type:lib', 'type:package'],
},
// Division UI: Can use core UI, but not apps
{
sourceTag: 'layer:division-ui',
onlyDependOnLibsWithTags: ['layer:core-ui', 'layer:division-ui', 'scope:shared', 'type:lib', 'type:package'],
notDependOnLibsWithTags: ['layer:app'],
},
// Apps: Can depend on anything
{
sourceTag: 'layer:app',
onlyDependOnLibsWithTags: ['*'],
},
],
},
],
Terminal window
# Lint all projects
nx run-many --target=lint --all
# Lint specific project
nx lint pinkguest-web

❌ Core UI importing from Division UI

packages/ui/src/Button.tsx
import { ListingCard } from '@cloudalt-frontend/ui-stay-match'; // ERROR!

Error:

A project tagged with "layer:core-ui" can only depend on libs tagged with "layer:core-ui", "scope:shared", "type:lib", "type:package"

❌ Division UI importing from App

packages/ui-stay-match/src/SearchFilters.tsx
import { CustomFilter } from 'apps/stay_match/pinkguest/web/src/components'; // ERROR!

Error:

A project tagged with "layer:division-ui" cannot depend on libs tagged with "layer:app"

Promote when:

  • ✅ The component is used (or will be used) by multiple apps in the same division
  • ✅ The component has division-specific business logic or styling
  • ✅ You want to establish a division-wide pattern

Example: ListingCard starts in pinkguest-web, but rainbowhost and purpleguest need it → promote to ui-stay-match

Promote when:

  • ✅ The component is used (or will be used) by multiple divisions
  • ✅ The component is generic with no division-specific concerns
  • ✅ You want to establish a monorepo-wide pattern

Example: RatingStars used in stay-match, altfinder, and roommate → promote to ui-web

❌ Don’t promote if:

  • Component is only used in one place (YAGNI principle)
  • Component contains app-specific hacks or temporary code
  • Unsure if pattern will be reused (wait for second use case - Rule of Three)

Section titled “Promotion Method: Nx Generator (Recommended)”

The automated Nx generator handles file moving, export updates, and creates TODO comments for import updates.

Terminal window
# Interactive prompts
yarn nx g @cloudalt-frontend/generators:promote-component
# With arguments
yarn nx g @cloudalt-frontend/generators:promote-component \
ListingCard \
--from=app \
--to=division \
--sourceProject=pinkguest-web \
--targetProject=ui-stay-match
# Dry run (preview only)
yarn nx g @cloudalt-frontend/generators:promote-component \
Button \
--from=division \
--to=core \
--sourceProject=ui-stay-match \
--targetProject=ui-web \
--dryRun
OptionDescriptionRequiredValues
componentComponent name to promoteYese.g., ListingCard, Button
--fromSource layerYesapp, division, core
--toTarget layerYesdivision, core
--sourceProjectSource Nx project nameConditionale.g., pinkguest-web, ui-stay-match
--targetProjectTarget Nx project nameConditionale.g., ui-stay-match, ui-web
--dryRunPreview without executingNotrue, false (default)
  1. ✅ Validates layer transition (only upward moves allowed)
  2. ✅ Finds all component files (directory or single file)
  3. ✅ Moves files to target location
  4. ✅ Removes export from source index.ts
  5. ✅ Adds export to target index.ts
  6. ✅ Adds TODO comments for manual import updates
  7. ✅ Formats files with Prettier

The generator cannot automatically update import statements (too risky). You must:

  1. Search for imports:

    Terminal window
    # Find all imports of the component
    grep -r "import.*ListingCard" apps/ packages/
  2. Update import paths:

    // Before (app layer)
    import { ListingCard } from '@app/components';
    // After (division layer)
    import { ListingCard } from '@cloudalt-frontend/ui-stay-match';
  3. Validate:

    Terminal window
    # Run lint to check module boundaries
    yarn nx lint ui-stay-match
    # Run tests
    yarn nx test ui-stay-match
    # Check for remaining TODOs
    grep -r "TODO.*ListingCard" .

If the generator doesn’t fit your needs:

Terminal window
# Example: promoting ListingCard from app to division
cp -r apps/stay_match/pinkguest/web/src/components/ListingCard \
packages/ui-stay-match/src/ListingCard

Add export to packages/ui-stay-match/src/index.ts:

export * from './ListingCard';

Remove export from apps/stay_match/pinkguest/web/src/components/index.ts:

// Remove this line:
// export * from './ListingCard';

Find and update all imports:

Terminal window
# Find imports
grep -r "from '@app/components'" apps/stay_match/pinkguest/web/src/
# Update imports in consuming files
# Before:
import { ListingCard } from '@app/components';
# After:
import { ListingCard } from '@cloudalt-frontend/ui-stay-match';
Terminal window
rm -rf apps/stay_match/pinkguest/web/src/components/ListingCard
Terminal window
# Type check
yarn nx typecheck pinkguest-web
yarn nx typecheck ui-stay-match
# Lint (module boundaries)
yarn nx lint ui-stay-match
# Test
yarn nx test ui-stay-match
# Build
yarn nx build pinkguest-web

When promoting components, you often need to make them more flexible:

Before (App-specific):

apps/.../components/ListingCard.tsx
export function ListingCard({ listing }: { listing: Listing }) {
return (
<Card>
<Image src={listing.image} />
<Title>{listing.title}</Title>
<Price>${listing.pricePerNight}/night</Price>
</Card>
);
}

After (Division-flexible):

packages/ui-stay-match/src/ListingCard.tsx
export interface ListingCardProps {
listing: Listing;
priceLabel?: string; // Configurable
showHost?: boolean; // Optional features
onFavorite?: () => void;
}
export function ListingCard({
listing,
priceLabel = '/night',
showHost = false,
onFavorite,
}: ListingCardProps) {
return (
<Card>
<Image src={listing.image} />
<Title>{listing.title}</Title>
<Price>${listing.pricePerNight}{priceLabel}</Price>
{showHost && <Host>{listing.host.name}</Host>}
{onFavorite && <FavoriteButton onClick={onFavorite} />}
</Card>
);
}

Before:

// All logic in one component
export function SearchFilters() {
const { filters, setFilters } = useStayMatchFilters(); // Division-specific
// ... rendering
}

After (Core):

// packages/ui-web/src/FilterPanel.tsx - Generic structure
export function FilterPanel({ filters, onChange, children }) {
return <Panel>{children}</Panel>;
}
// packages/ui-stay-match/src/SearchFilters.tsx - Division logic
export function SearchFilters() {
const { filters, setFilters } = useStayMatchFilters();
return (
<FilterPanel filters={filters} onChange={setFilters}>
<PriceRangeFilter />
<DateRangeFilter />
<AmenityFilter />
</FilterPanel>
);
}

Before:

<Button className="bg-pink-600 hover:bg-pink-700">
Book Now
</Button>

After:

<Button variant="primary">
Book Now
</Button>
// Variants handle brand colors via CSS variables

Storybook provides three toolbar selectors for brand-aware component development:

Division Selector:

  • Options: altFinder, Stay Match, Roommate, Bonjour Services, Guestroom, Greenhost, Homestay, Pride City, Room LGBT, Hostguest
  • Default: Stay Match

App Selector:

  • Dynamically populated based on division
  • Example (Stay Match): pinkguest, orangeguest, purpleguest, rainbowhost, staymatch_app
  • Default: first app in division

Theme Selector:

  • Options: light, dark
  • Existing functionality, preserved

What it does:

  1. Reads globals.division and globals.app
  2. Loads corresponding brand theme from @cloudalt-frontend/theme/brands
  3. Applies CSS variables for shadcn components (e.g., --primary, --background)
  4. Applies Tailwind classes for theme/brand
  5. Provides context.globals object to stories with:
    • brandId
    • brandConfig (display name, description, features)
    • theme (color scales, typography)

Implementation location: packages/storybook/.storybook/preview.ts

Core component stories:

packages/storybook/stories/Button.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Button } from '@cloudalt-frontend/ui-web'
const meta: Meta<typeof Button> = {
title: 'Core/Button',
component: Button,
}
export default meta
type Story = StoryObj<typeof Button>
export const Primary: Story = {
args: {
children: 'Click me',
color: 'primary',
},
}

Division component stories:

packages/storybook/stories/StayMatch/Hero.stories.tsx
import type { Meta, StoryObj } from '@storybook/react'
import { Hero } from '@cloudalt-frontend/ui-stay-match'
const meta: Meta<typeof Hero> = {
title: 'Stay Match/Hero',
component: Hero,
}
export default meta
type Story = StoryObj<typeof Hero>
export const Default: Story = {
args: {
title: 'Find Your Perfect Match',
subtitle: 'Connect with hosts worldwide',
},
}
  1. Open Storybook: yarn storybook
  2. Use toolbar controls:
    • Division: Select your brand family
    • App: Select specific app
    • Theme: Toggle light/dark

Components will render with the selected brand’s tokens and styling.


Principle: Division/app look comes from tokens first. Only use props/styles when tokens aren’t enough.

Token sources:

  • packages/theme/src/tokens.ts — Base design system (colors, typography, spacing, shadows, etc.)
  • packages/theme/src/brands.ts — Brand-specific overrides (e.g., pinkguestTheme, orangeguestTheme)
  • packages/config/tailwind-preset.js — Tailwind integration via createBrandPreset(brandName)

Tailwind classes (preferred):

<button className="bg-primary-500 text-white">
Click me
</button>

CSS variables (shadcn components):

<button className="bg-primary text-primary-foreground">
Click me
</button>

JavaScript tokens:

import { colors, spacing } from '@cloudalt-frontend/theme'
const styles = {
background: colors.primary[500],
padding: spacing[4],
}

In Tailwind config:

apps/stay_match/pinkguest/web/tailwind.config.js
const { createBrandPreset } = require('../../../../packages/config/tailwind-preset')
module.exports = {
presets: [createBrandPreset('pinkguest')],
content: ['./src/**/*.{js,jsx,ts,tsx}']
}

At runtime (React):

import { getBrandTheme } from '@cloudalt-frontend/theme/brands'
const theme = getBrandTheme('pinkguest')
console.log(theme.colors.primary[500]) // #ec4899

CSS Variables (shadcn components):

  • Components using bg-primary, text-foreground get themed via CSS vars
  • Decorator injects vars based on selected brand

Token Scale Classes (other components):

  • Components using bg-primary-500 require Tailwind config changes
  • Partial runtime theming until migrated to variable-based utilities

apps/stay_match/pinkguest/web/src/components/Hero.test.tsx
import { render, screen } from '@testing-library/react'
import { Hero } from './Hero'
test('renders hero with title', () => {
render(<Hero title="Test Title" />)
expect(screen.getByText('Test Title')).toBeInTheDocument()
})
Terminal window
# Move test file
mv apps/.../components/ListingCard.test.tsx \
packages/ui-stay-match/src/ListingCard/ListingCard.test.tsx
# Update test imports
- import { ListingCard } from './ListingCard';
+ import { ListingCard } from '../ListingCard';
Terminal window
# Run tests for affected projects
yarn nx affected -t test
# Run specific app tests
yarn nx test pinkguest-web
Terminal window
# Run Storybook
yarn storybook
# Build static Storybook
yarn build-storybook
# Future: Visual regression testing
yarn test:visual
Terminal window
# Type check
yarn nx typecheck <project>
# Lint (module boundaries)
yarn nx lint <project>
# Test
yarn nx test <project>
# Build
yarn nx build <project>
# Check all affected projects
yarn nx affected -t lint test build

  1. Keep core components generic — No brand-specific logic
  2. Use tokens for styling — Avoid hardcoded colors/spacing
  3. Export TypeScript types — Enable type-safe overrides
  4. Document props — Use JSDoc comments
  5. Write stories — Every component needs a Storybook story
packages/ui-stay-match/
├── src/
│ ├── components/
│ │ ├── Hero/
│ │ │ ├── Hero.tsx
│ │ │ ├── index.ts
│ │ │ └── README.md
│ │ └── Button/
│ │ ├── Button.tsx
│ │ └── index.ts
│ ├── types/
│ │ └── index.ts
│ └── index.ts ← Export everything here
├── package.json
├── project.json
├── tsconfig.json
└── README.md
// 1. External dependencies
import React from 'react'
import { clsx } from 'clsx'
// 2. Core packages
import { Button } from '@cloudalt-frontend/ui-web'
import { Icon } from '@cloudalt-frontend/icons'
// 3. Division packages
import { Hero } from '@cloudalt-frontend/ui-stay-match'
// 4. App overrides (last resort)
import { CustomHeader } from '@app/components'
// 5. Relative imports
import { helper } from './utils'
✅ Apps → Division + Core
✅ Division → Core only
✅ Core → External libs only
❌ Apps ↔ Apps
❌ Division ↔ Division
❌ Core → Division
❌ Core → Apps

Do:

  • Promote when you have 2+ use cases (Rule of Three)
  • Make promoted components flexible with props
  • Write comprehensive prop documentation
  • Add Storybook stories showing variants
  • Test in multiple consuming contexts

Don’t:

  • Promote prematurely (“we might need it later”)
  • Keep app-specific logic in promoted components
  • Forget to update imports everywhere
  • Skip validation (lint, test, build)
  • Promote components with incomplete functionality

TypeScript: Cannot find module ‘@app/components’

Section titled “TypeScript: Cannot find module ‘@app/components’”

Cause: tsconfig.app.json missing path mapping.

Fix:

{
"compilerOptions": {
"paths": {
"@app/components": ["./src/components/index.ts"],
"@app/components/*": ["./src/components/*"]
}
}
}

Cause: Component not exported from src/components/index.ts.

Fix:

apps/.../src/components/index.ts
export { Hero } from './Hero'

Cause: Check toolbar selection or CSS variable mapping.

Fix: Verify division/app selector is set correctly in toolbar.

Cause: Invalid import (e.g., division importing from another division).

Fix: Refactor to use core components or move shared code to core.

”Cannot find module” errors after promotion

Section titled “”Cannot find module” errors after promotion”

Cause: Import paths not updated

Fix:

Terminal window
# Find all imports
grep -r "YourComponent" .
# Update to new path
- import { YourComponent } from '@app/components';
+ import { YourComponent } from '@cloudalt-frontend/ui-stay-match';

Cause: Division UI trying to import from apps

Fix: Check ESLint output and remove invalid imports

Terminal window
yarn nx lint ui-stay-match --verbose

Cause: Type definitions not exported properly

Fix: Check index.ts exports

// Ensure both component and types are exported
export * from './ListingCard';
export type { ListingCardProps } from './ListingCard';

Created:

  • ✅ Component layering comprehensive documentation
  • ✅ Developer quick-start guide with examples

File: packages/storybook/.storybook/preview.ts

Added:

  • ✅ Division selector toolbar (10 divisions)
  • ✅ App selector toolbar (dynamically filtered by division)
  • ✅ Brand theming decorator that applies CSS variables based on selection
  • ✅ Support for 5 brand themes (pinkguest, orangeguest, purpleguest, rainbowhost, roomlgbt)

How it works:

  1. User selects Division (e.g., “Stay Match”)
  2. App selector shows only apps in that division (e.g., “Pink Guest”, “Orange Guest”)
  3. Decorator applies brand-specific CSS variables (--primary, --secondary, etc.)
  4. shadcn components automatically reflect the brand theme
  5. Stories render with the selected brand’s visual style

Updated:

  • apps/stay_match/pinkguest/web/tsconfig.app.json — Added @app/components path mapping
  • apps/roommate/roommate_guru/web/tsconfig.app.json — Added @app/components path mapping

Created:

  • ✅ Component override directories with index files and READMEs for sample apps

Developer experience:

apps/stay_match/pinkguest/web/src/components/Hero.tsx
// Create app-specific override
import { Hero as DivisionHero, HeroProps } from '@cloudalt-frontend/ui-stay-match'
export const Hero = (props: HeroProps) => {
return <DivisionHero {...props} className="custom-pink" />
}
// Use in app
import { Hero } from '@app/components' // ✅ Works!
Terminal window
# Start Storybook
yarn storybook
# Test toolbar:
# - Select "Division" → "Stay Match"
# - Select "App" → "Pink Guest"
# - Verify button/card colors are pink
# - Switch to "Orange Guest"
# - Verify colors change to orange
# - Toggle "Theme" → "Dark"
# - Verify dark mode works alongside brand theme
Terminal window
# Test pinkguest-web
cd apps/stay_match/pinkguest/web
# Verify TypeScript resolves
npx tsc --noEmit
# Should pass with no errors
Terminal window
# Check for TypeScript/ESLint errors
nx lint storybook
nx lint pinkguest-web
nx lint roommate_guru-web
  • ✅ Storybook shows 10 divisions with correct apps
  • ✅ Brand themes apply via CSS variables
  • ✅ Two sample apps have working @app/components alias
  • ✅ Documentation is comprehensive and actionable
  • ✅ Zero breaking changes
  • ✅ All tests/lint pass

  • packages/ui — Platform-agnostic core
  • packages/ui-web — Web-specific core (shadcn)
  • packages/common — Shared utilities
  • packages/icons — Icon components
  • packages/theme — Design tokens
  • packages/brands — Brand configurations
  • packages/config — Shared configs
  • packages/ui-stay-match
  • packages/ui-altfinder
  • packages/ui-roommate
  • packages/ui-guestroom
  • packages/ui-bonjour-services
  • packages/ui-greenhost
  • packages/ui-homestay
  • packages/ui-pride-city
  • packages/ui-room-lgbt
  • packages/ui-hostguest


  • Check existing components in packages/ui-web and packages/ui-<division>
  • Review stories in packages/storybook/stories
  • Ask in #frontend-architecture Slack channel
  • Refer to this guide: /docs/COMPONENT_LAYERING_COMPLETE_GUIDE.md

Remember: When in doubt, start with an app override, validate it works, then promote if useful! 🚀

Document History:

  • Merged from: COMPONENT_LAYERING_GUIDE.md, COMPONENT_LAYERING_IMPLEMENTATION.md, NX_LAYER_TAGGING.md, COMPONENT_PROMOTION_GUIDE.md, PR_COMPONENT_LAYERING_PHASE1.md
  • Last Updated: October 18, 2025