Project Architecture
This document describes where things live in the Tangible UI codebase.
File Structure
src/
├── components/ # React components (co-located)
│ ├── Button/
│ │ ├── Button.tsx
│ │ ├── Button.stories.ts
│ │ ├── styles.scss
│ │ └── index.ts
│ ├── Card/
│ ├── Chip/
│ ├── ...
│ └── index.ts # Main barrel export
│
├── icons/ # SVG icon registry
│ ├── registry.ts # Maps category/name → component
│ ├── system/ # Core UI icons
│ ├── cred/ # TangibleCred icons
│ ├── reaction/ # Emoji-style reactions
│ └── player/ # Media player icons
│
├── styles/
│ ├── system/ # Design tokens
│ │ ├── _colors.scss
│ │ ├── _spacing.scss
│ │ ├── _typography.scss
│ │ └── ...
│ ├── components/ # Style bundling (imports from co-located styles)
│ │ ├── _bundle.scss
│ │ └── input/ # CSS-only inputs (no React component)
│ ├── utilities/ # Layout helpers (flex, grid, spacing)
│ └── index.scss # Main entry point
│
└── stories/ # Storybook documentation (MDX)
├── Welcome.mdx
├── Colors.mdx
├── Spacing.mdx
└── ...
Component Co-location
Each component lives in its own folder with everything it needs:
| File | Purpose |
|---|---|
Component.tsx |
React component |
styles.scss |
Component styles (SCSS) |
Component.stories.tsx |
Storybook stories |
index.ts |
Barrel export |
This keeps related code together and makes it easy to find everything about a component in one place.
Why Co-location?
- Discoverability: Open folder, see everything
- Refactoring: Move/rename folder, done
- Ownership: Clear boundaries between components
- No hunting: Styles aren't in a separate tree
Import Patterns
Consuming components
// From the main barrel (recommended)
import { Button, Card, Icon } from '@tangible/ui';
// Direct import (also fine)
import { Button } from '@tangible/ui/components/Button';
Within the codebase
// Same folder
import { Button } from './Button';
// Sibling component
import { Icon } from '../Icon';
// Icons registry
import { iconRegistry } from '../../icons/registry';
Styles Architecture
Token System
All design tokens use CSS custom properties with tui- prefix:
--tui-spacing-md
--tui-color-primary-base
--tui-typography-size-lg
--tui-radius-md
Tokens are defined in src/styles/system/ and emitted to .tui-interface.
Component Styles
Component styles are written in SCSS and use the token system:
@use "../../styles/system" as sys;
@mixin styles() {
:where(.#{sys.$prefix}interface) .#{sys.$prefix}button {
padding: var(--#{sys.$prefix}spacing-sm);
// ...
}
}
CSS Layers
Styles are organized into CSS layers for cascade control:
@layer defaults, components, utilities;
- defaults: Resets and base styles
- components: Component styles
- utilities: Layout helpers (can override components)
Build Outputs
publish/
├── index.js # React components (ESM)
├── index.d.ts # TypeScript declarations
├── styles/
│ └── all.css # All styles bundled
└── ...
CSS-only Usage
For PHP-rendered HTML, include the CSS and use classes directly:
<link rel="stylesheet" href="@tangible/ui/styles/all.css">
<div class="tui-interface">
<button class="tui-button is-theme-primary">Click me</button>
</div>
Adding a New Component
- Create folder:
src/components/NewComponent/ - Add files:
NewComponent.tsx— React componentstyles.scss— Styles with@mixin styles()NewComponent.stories.tsx— Storybook storiesindex.ts— Export component and types
- Register styles in
src/styles/components/_bundle.scss - Export from
src/components/index.ts
Key Conventions
| Convention | Example |
|---|---|
| Component file | Button.tsx (PascalCase) |
| Styles file | styles.scss (always this name) |
| Stories file | Button.stories.tsx |
| CSS class | tui-button (kebab-case) |
| CSS element | tui-button__icon (BEM-ish) |
| CSS modifier | is-theme-primary, is-size-sm |
| CSS state | is-disabled, is-active |
| Token | --tui-spacing-md |
See Naming for full class naming conventions.