Defines where UI components belong in Operately (TurboUI-first). Use when creating, changing, reviewing, or migrating UI components, adding features that need UI, or deciding whether to refactor legacy app UI in app/assets/js/components or app/assets/js/features. Covers pure TurboUI components, component reuse, the app bridge pattern, and legacy migration scenarios.
Scanned 8/30/2026
Install to Claude Code
npx -y skills add operately/operately --skill components-architecture --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Components Architecture?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/operately-components-architecture)More formats (shields.io, HTML) on the badges page.
---
name: components-architecture
description: >-
Defines where UI components belong in Operately (TurboUI-first). Use when
creating, changing, reviewing, or migrating UI components, adding features
that need UI, or deciding whether to refactor legacy app UI in
app/assets/js/components or app/assets/js/features. Covers pure TurboUI
components, component reuse, the app bridge pattern, and legacy migration
scenarios.
---
# Components Architecture
All UI components belong in TurboUI as pure components. The app bridges the
backend and TurboUI: it loads data, calls APIs, reads app contexts, and passes
data and callbacks as props.
TurboUI was introduced after much of the app UI was built. `app/assets/js/components/`
and UI components under `app/assets/js/features/` are **deprecated for new UI**.
Non-UI code in `app/assets/js/features/` (activity registration, API hooks,
loaders, model hooks) still belongs in the app.
For detailed examples and the canonical component selection map, see [reference.md](reference.md).
## Architecture
Data flows **down** as props; user actions flow **up** as callbacks. The app
sits in the middle and talks to both sides.
| Layer | Location | Responsibility |
| ----- | -------- | -------------- |
| UI | `turboui/src/` | Render UI; no API, routing, or app contexts |
| Bridge | `app/assets/js/pages/` | Load data, call APIs, read contexts, build props, handle callbacks |
| Backend | Elixir/GraphQL | Persist and serve data |
## Pure Components
TurboUI components must **not**:
- Import from `@/…` app paths
- Call `Api.*`, use React Router hooks, or read app contexts (`TimezoneContext`, `useMe`, etc.)
- Fetch data or perform server-side side effects
TurboUI components **should**:
- Accept display data and user preferences via props
- Use callbacks for actions (`onSave`, `onDelete`, `onTaskUpdate`)
- Keep local UI state only (open/closed, draft input) and notify the parent via callbacks
- Export from `turboui/src/index.tsx`
- Include Storybook stories
- Type props with `turboui/src/ApiTypes` (same shapes as `app/assets/js/api/index.tsx`) or a
component-specific type when the UI needs a different shape
### Data types
`turboui/src/ApiTypes/index.ts` mirrors the types in `app/assets/js/api/index.tsx`.
TurboUI components should expect those API shapes — the app fetches data via
`Api.*` and passes it through with minimal mapping.
- **Default**: props use `ApiTypes` directly (e.g. `Project`, `Person`, `Task`)
- **Custom prop type**: when the UI needs a view-specific shape, define it in
the component's `types.ts` — re-export from `ApiTypes` where possible (see
`turboui/src/ResourceHub/types.ts`)
- **Transform in TurboUI**: when display logic applies to API data, do it inside
the TurboUI component rather than in the app bridge
Do **not** add new `parse*ForTurboUi` or `prepare*` helpers in the app for new
work. Legacy parsers still exist in older pages but are not the target pattern.
### Component design
- **Self-contained**: manage UI state locally; notify parents via callbacks
- **Generic callbacks**: prefer `onTaskUpdate(id, updates)` over many specific handlers (`onAssigneeChange`, `onDueDateChange`, …)
- **Callback shape**: `(id: string, updates: Partial<Type>) => void` for entity updates
- **No mock data in components**: mock data belongs in Storybook stories only
- **Reuse TurboUI primitives**: `PrimaryButton`, design-system colors (`content-subtle`, `content-error`), Tabler icons — check `turboui/src/Colors/Colors.stories.tsx` and `turboui/src/icons/index.tsx` before adding new ones
### Component reuse gate
Complete this before writing JSX for new or changed UI:
1. List every required control and interaction pattern: forms, fields, buttons, links, selectors, modals, empty states, validation, and loading feedback.
2. Search `turboui/src/index.tsx`, relevant component directories, and existing usages for matching components.
3. Compose the UI from those components. Use raw interactive elements only when implementing a TurboUI primitive or when the inventory confirms that no suitable primitive exists.
4. If a raw `<input>`, `<button>`, `<select>`, `<textarea>`, dialog, link-like action, or validation message remains, document the reason in the implementation summary.
Use `Forms.Form`, `Forms.FieldGroup`, `Forms.TextInput`, and `Forms.Submit` as the default stack for new conventional forms. Use `TextField` for inline editing or existing surfaces already composed around it. Treat `FormElements/Textfield` as legacy and do not select it for new UI.
During code review, classify hand-rolled interactive UI that duplicates an existing TurboUI component as a P2 architecture issue.
### File organization
```
turboui/src/ComponentName/
├── index.tsx # Main component (+ exported Props type)
├── index.stories.tsx # Storybook stories
├── mockData.ts # Mock data for stories (optional)
└── types.ts # Component-specific types (optional)
```
### Storybook workflow
1. Develop and test components in Storybook (`make turboui.storybook`)
2. Create stories for empty, loading, error, and interactive states
3. Before committing: `make turboui.build && make turboui.test`
## App Bridge
App pages own all backend interaction. Typical page layout:
```
app/assets/js/pages/SomePage/
├── index.tsx # Page module (loader + Page export)
├── loader.tsx # Data fetching
├── navigation.tsx # Breadcrumbs / nav props (optional)
└── page.tsx # Build props, render TurboUI component
```
**Thin bridge** — loader fetches data, page passes it through:
- `app/assets/js/pages/ReviewPage/index.tsx`
- `app/assets/js/pages/ResourceHubDraftsPage/page.tsx`
**Standard bridge** — props object + model hooks for mutations:
- `app/assets/js/pages/ResourceHubPage/page.tsx`
- `app/assets/js/pages/SpaceWorkMapPage/page.tsx`
**Complex bridge** — field state, API updates, large typed props:
- `app/assets/js/pages/ProjectPage/index.tsx`
- `app/assets/js/pages/MilestonePage/index.tsx`
- `app/assets/js/pages/GoalPage/index.tsx`
App-side concerns that stay in the app (not TurboUI):
- Data fetching: loaders, `Api.*`, model hooks
- Mutations: wire callbacks to `Api.*` (often via `usePageField`)
- Routing: `usePaths()` — pass link strings or path-builder callbacks as props
- App contexts: locale, timezone, current user — pass as props
Matching TurboUI components to study:
- `turboui/src/ResourceHubPage/` — page component with stories and `mockData.ts`
- `turboui/src/ProjectPage/`, `turboui/src/MilestonePage/`, `turboui/src/WorkMapPage/`
## Scenario 1: New UI Components
**Always** create in `turboui/src/ComponentName/`. Do **not** add new UI to
`app/assets/js/components/` or `app/assets/js/features/`.
App work for a new feature:
1. Add or adjust a page in `app/assets/js/pages/` (loader, navigation if needed)
2. Fetch data in the loader; pass API-shaped data to TurboUI (typed via `ApiTypes`)
3. Wire callbacks to `Api.*` / model hooks
4. Render the TurboUI component
## Scenario 2: Feature Needs an Existing App UI Component
First, `grep` imports to find call sites of the existing component.
### 2a — Few usages (small refactor)
Use when updating every call site is a small, reviewable change.
1. Migrate the component to TurboUI as a pure component
2. Adjust props to remove app dependencies (pass locale, timezone, etc. from app)
3. Update **all** existing call sites to import from `turboui`
4. Remove or thin the app copy
5. Use the TurboUI component in the new feature
### 2b — Many usages (large refactor)
Use when a full migration would dominate the PR or touch unrelated features.
1. Create a **new pure version** in TurboUI that is **visually identical** to the legacy component
2. Use it **only in the new feature** for now
3. Do **not** refactor all existing call sites in the same PR
4. Leave the legacy app component in place until a dedicated migration
## Checklist
- [ ] Completed the component reuse inventory before writing JSX
- [ ] Reused existing TurboUI controls and interaction patterns where available
- [ ] Documented every remaining raw interactive element and why no suitable primitive exists
- [ ] New or changed UI lives in `turboui/src/`, not in deprecated app UI folders
- [ ] TurboUI component has no app imports or API calls
- [ ] App page passes data and callbacks; API and context logic stays in app
- [ ] Component exported from `turboui/src/index.tsx`
- [ ] Storybook story added or updated (`make turboui.build && make turboui.test`)
- [ ] Props typed with `ApiTypes` or a documented component-specific type — no new app-side parsers
- [ ] Legacy migration uses the correct scenario (2a full migration vs 2b new-feature-only)
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!