This page is for engineers adding to the UI package. It documents how the codebase is organized so new code lands in the right place, with the right name, importing the right way. If you’re a designer or product person looking for visual rules, foundations and the pattern pages are what you want.Documentation Index
Fetch the complete documentation index at: https://docs.yourhq.ai/llms.txt
Use this file to discover all available pages before exploring further.
Layer model
Three concentric circles. Code in an outer layer can import from inner layers, never the reverse.apps/ui/src/components/ui/— generic, reusable UI building blocks. Imports: React, Radix,cnfrom@/lib/utils, other primitives. Never imports fromshared/or feature modules.apps/ui/src/components/shared/— HQ-specific compositions used across modules. Imports: primitives, other composites,lib/utils, generic types fromlib/. Never imports from a specific feature module.apps/ui/src/components/<module>/— feature-specific. Imports: anything inner. Free to import from its ownlib/<module>/andhooks/use-<module>.ts.
Module convention
A feature module spans four locations. Each has one responsibility.| Location | Owns |
|---|---|
apps/ui/src/lib/<module>/types.ts | TypeScript types mirrored from the Supabase schema, plus any module-specific enums and helpers. |
apps/ui/src/lib/<module>/ (siblings) | Pure helpers — formatters, derivers, validators. No React. |
apps/ui/src/hooks/use-<module>.ts | Data fetching, realtime subscription, filter/sort/selection state, CRUD action wrappers, form state. The orchestrator. |
apps/ui/src/components/<module>/ | Components — list orchestrator, view variants, detail view, form, sub-components. |
apps/ui/src/app/dashboard/<module>/ | Routes (page.tsx, [id]/page.tsx, error.tsx) and server actions (actions.ts). Server actions are the only place server-side mutations live. |
File and identifier naming
- Files — kebab-case.
contact-detail-view.tsx, notContactDetailView.tsx. Includes hooks (use-contacts.ts) and helpers (format-money.ts). - Components — PascalCase exports.
export function ContactDetailView(). - Hooks —
useprefix, camelCase identifier, kebab-case filename. - Types — PascalCase. Schema-mirrored types match the table singularized:
Contact,Task,Stream,Agent. - Server actions —
<verb><Noun>Action.archiveContactAction,createTaskAction. They live inapp/dashboard/<module>/actions.tsand start with"use server". - CSS classnames — none. We don’t write classnames; we use Tailwind utilities and tokens.
Imports
Always use aliases. Never relative-cross paths (../../../components/ui/button).
| Alias | Resolves to |
|---|---|
@/components | apps/ui/src/components/ |
@/components/ui | apps/ui/src/components/ui/ |
@/lib | apps/ui/src/lib/ |
@/lib/utils | apps/ui/src/lib/utils.ts (cn lives here) |
@/hooks | apps/ui/src/hooks/ |
apps/ui/components.json and tsconfig.json.
Within the same directory, relative imports are fine — ./contact-form, ./contact-list-row. Cross-directory: alias.
Styling rules
- Tokens, not hexes. Use Tailwind utilities mapped from
globals.css. If a value isn’t covered by a token, the right move is almost always to use the next step on the existing scale, not to inline a custom value. cn()for conditional classes. Imported from@/lib/utils. Use it whenever class strings depend on props or state.- CVA for variants. Components with two or more visual variants use
class-variance-authority. Reference:button.tsx,badge.tsx. data-slotattributes on composed primitives. Lets parents target sub-parts via[data-slot=…]selectors without prop drilling.- No CSS Modules, no styled-components, no Emotion. Tailwind plus the editor styles in
globals.cssare the only style surfaces.
Decision tree: where does this code go?
A new piece of UI lands in one of three places. Run through the questions in order.1. Is it a primitive (no business logic, no module imports)?
If yes, it goes incomponents/ui/ — but think twice. Most “I need a primitive” instincts are actually composites in disguise. The bar for adding to ui/ is high:
- It must be reusable in any context, not just HQ.
- It must not import from
shared/or any feature module. - It must not depend on any HQ-specific data shape.
data-slot attributes, cn for conditional classes. Reference: the seven HQ extensions in primitives.
2. Is it a composite (HQ-shaped, used across modules)?
A composite belongs incomponents/shared/ if all three of these are true:
- It’s used in two or more modules already. If you’re about to copy something from
crm/intotasks/, that’s the threshold. Lift it before the second copy. - The API is stable. Composites encode shape decisions. If you’re not sure of the props yet, leave it inside the feature for one more iteration before promoting.
- It depends only on
ui/, othershared/,lib/utils, and React. Composites must not import from feature modules. If yours needs a feature hook, pass the data in as props.
3. Is it feature-specific?
Then it goes incomponents/<module>/. Free to import anything inner. Free to consume hooks/use-<module>.ts, lib/<module>/, server actions from the same app/dashboard/<module>/ route.
Adding a new feature module
End-to-end recipe for a new module calledwidgets:
- Database — add the migration under
db/migrations/0NN_widgets.sql. Includetenant_id, RLS policies, explicitGRANTforauthenticatedandservice_role. Run in filename order. - Types —
apps/ui/src/lib/widgets/types.ts. Mirror the table. - Hook —
apps/ui/src/hooks/use-widgets.ts. Subscribes viause-realtime-sync, owns filters/sorting/selection state, exposes actions. - Server actions —
apps/ui/src/app/dashboard/widgets/actions.ts."use server". Each action returns{ data, error }or throws. - Routes —
app/dashboard/widgets/page.tsx,app/dashboard/widgets/[id]/page.tsx,app/dashboard/widgets/error.tsx. - Components —
components/widgets/widgets-tab.tsx(orchestrator),widgets-table-view.tsx,widget-detail-view.tsx,widget-form.tsx. Open with aPageHeader. List usesDataTableinside aFilterBar. Detail usesDetailHeaderplusDetailSidebar. Create usesSidePanelif widgets are record-style (the user manages them from a list), orDialogif widgets are capture-style (the user creates them from anywhere) — see creation and editing. - Shell registration — add the module to:
- The sidebar in
components/dashboard-shell.tsx. - The navigation list in
components/shared/command-palette.tsx. - The G-shortcut table in
components/shared/keyboard-shortcuts.tsx. - The workspace’s enabled-modules list (so
ModulesContextcan gate it).
- The sidebar in
When to add a token
Adding a CSS variable toglobals.css is appropriate when a new semantic role exists, not when a new shade is needed.
- “We need a color for the ‘snoozed’ task state” → add
--status-snoozedto:rootand.dark, wire through@theme inline. - “We need a slightly darker red for this one button” → don’t add a token. Use
bg-destructive/90or pick a color closer to an existing token.
When NOT to add a primitive
Most of these situations look like primitives but are actually composites or features:- “A list item with a checkbox and a status dot.” → composite (
Itemexists; compose). - “A button that opens a confirm dialog.” → composite (use
ConfirmDialog). - “An input that shows the user’s avatar inside it.” → feature (specific to one form).
- “A reusable card for showing an agent.” → feature inside
components/agents/.
ui/, find the closest existing primitive first. The answer is almost always “compose it.”
Code review checklist
Before opening a PR that touches the UI:- No inline hex codes, no
#fff, norgb(). Tokens via Tailwind utilities. - Aliases for cross-directory imports.
- No imports from
app/or feature modules intoui/orshared/. -
cn()from@/lib/utilsfor conditional classes (not template literals). - CVA for any component with two-plus visual variants.
- Components are kebab-case files, PascalCase exports.
- Hooks subscribe and unsubscribe correctly (cleanup in the
useEffectreturn). - Lists use
DataTable(not bareTable); creates useSidePanelfor record editing orDialogfor quick capture — never bareSheetorDialogprimitives directly. - Empty states are present on every list. Loading skeletons match the layout.
- No comments narrating what the code does. Only comments where the why is non-obvious.
-
npx tsc --noEmitpasses fromapps/ui/. -
npm run lintpasses.
globals.css, also update foundations so the docs stay aligned with the source of truth.
