components-architecture
GitHub定义UI组件架构规范,明确TurboUI纯组件与App桥接层的职责分离。指导组件创建、迁移及重构,确保数据流单向性及无副作用设计,适用于前端开发中的组件设计与代码审查。
Trigger Scenarios
Install
npx skills add operately/operately --skill components-architecture -g -y
SKILL.md
Frontmatter
{
"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.
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 asapp/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
ApiTypesdirectly (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 fromApiTypeswhere possible (seeturboui/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>) => voidfor 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 — checkturboui/src/Colors/Colors.stories.tsxandturboui/src/icons/index.tsxbefore adding new ones
Component reuse gate
Complete this before writing JSX for new or changed UI:
- List every required control and interaction pattern: forms, fields, buttons, links, selectors, modals, empty states, validation, and loading feedback.
- Search
turboui/src/index.tsx, relevant component directories, and existing usages for matching components. - 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.
- 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
- Develop and test components in Storybook (
make turboui.storybook) - Create stories for empty, loading, error, and interactive states
- 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.tsxapp/assets/js/pages/ResourceHubDraftsPage/page.tsx
Standard bridge — props object + model hooks for mutations:
app/assets/js/pages/ResourceHubPage/page.tsxapp/assets/js/pages/SpaceWorkMapPage/page.tsx
Complex bridge — field state, API updates, large typed props:
app/assets/js/pages/ProjectPage/index.tsxapp/assets/js/pages/MilestonePage/index.tsxapp/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 viausePageField) - 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 andmockData.tsturboui/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:
- Add or adjust a page in
app/assets/js/pages/(loader, navigation if needed) - Fetch data in the loader; pass API-shaped data to TurboUI (typed via
ApiTypes) - Wire callbacks to
Api.*/ model hooks - 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.
- Migrate the component to TurboUI as a pure component
- Adjust props to remove app dependencies (pass locale, timezone, etc. from app)
- Update all existing call sites to import from
turboui - Remove or thin the app copy
- 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.
- Create a new pure version in TurboUI that is visually identical to the legacy component
- Use it only in the new feature for now
- Do not refactor all existing call sites in the same PR
- 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
ApiTypesor a documented component-specific type — no new app-side parsers - Legacy migration uses the correct scenario (2a full migration vs 2b new-feature-only)
Version History
- 379b061 Current 2026-08-20 12:01


