| @bricks-common/bo/modules/project-financing-request | Domain badges + labels shared by every BO surface that renders a project_financing_request: ProjectFinancingRequestStatusBadge, ProjectFinancingRequestCategoryBadge (one categorical tone per type, never neutral so a type never reads as a status), ProjectFinancingRequestUserRoleBadge (representative info, collaborator success, apporteur_affaires warning), translateProjectFinancingRequest{Status,Category,UserRole} (lib paraglide project_financing_request.*). Tone maps live here only — a consumer never re-maps a status to a StatusBadgeTone. |¶
description: Shared back-office lib @bricks-common/bo — architecture, public surface, #-imports, routing/auth, build/deploy globs: - 'projects/bricksoffice-projects//*' - 'projects/bricksoffice-app-config//' - 'projects/bricksoffice-invest//' - 'projects/bricksoffice-notaire//*' - 'projects/bricksoffice-technique//' - 'projects/common/back-office//' alwaysApply: false
Shared back-office lib — @bricks-common/bo¶
Lives at
projects/common/back-office, consumed by everybricksoffice-*app (bricksoffice-projects,bricksoffice-app-config,bricksoffice-invest,bricksoffice-notaire,bricksoffice-technique). Single home for design system, hooks, utils, providers, core infra. Read this before adding code to a consumer app — almost always, what you're about to write belongs in BO.Naming: "BO" or "back-office shared". Its design system is one part — don't conflate.
Architecture¶
projects/common/back-office/
├── package.json # name: @bricks-common/bo · ships RAW TS · "main": ./src/index.ts
├── components.json # shadcn: new-york / zinc / lucide
└── src/
├── index.ts # top-level barrel
├── core/ # api client, env, dayjs, appQueryClient
├── providers/ # HttpClientProvider, …
├── hooks/ # use-mobile, useFormSubmissionContext, useIdempotentSubmit
├── utils/ # cn
└── design-system/ # see ./styling.mdc
├── components/ inputs/ layouts/ modals/ tables/
├── text-editor/ forms/
└── styles/ # styles.css, tokens.css, tokens.ts, theme/
No build step. "main" and "types" both point at ./src/index.ts. Consumers compile BO source through pnpm workspace symlinks. Cmd+click lands on the actual .tsx. Consumer tsc must typecheck that source: allowJs: true (paraglide emits JS, no .d.ts) and BO must pass noUncheckedIndexedAccess (invest enables it).
Peer deps: react / react-dom / tailwindcss / @tanstack/* are peers because BO is source-only and ships JSX/hooks — duplicating any hook-bearing package across consumer + lib silently breaks context. Versions pinned in the workspace backoffice catalog (pnpm-workspace.yaml). Same for sonner: two copies = two toaster instances — consumers use catalog:backoffice and vite.resolve.dedupe.
Public surface¶
Defined by exports in BO's package.json. Consumers import via these paths only:
| Subpath | Resolves to |
|---|---|
@bricks-common/bo |
src/index.ts (full barrel) |
@bricks-common/bo/design-system |
src/design-system/index.ts |
@bricks-common/bo/hooks |
src/hooks/index.ts |
@bricks-common/bo/utils |
src/utils/index.ts |
@bricks-common/bo/providers |
HttpClientProvider / useHttpClient |
@bricks-common/bo/core |
appQueryClient, processHttpResult, isAxiosError / isNotFoundError (404 = the page says so itself: no toast, no retry), withGlobalErrorHandling, useSubmitWithError, formatDate/formatDateTime, formatCentsFull/formatCentsShort, parseEurosInputToCents, parseDecimalInput, createAdminAxios, useBetaFlag / BetaFlagKey (core/beta-flags.ts, key bo:beta:<flag>:enabled), getHubCatalog / CatalogApp (core/hub-catalog.ts) |
@bricks-common/bo/modules/auth |
AdminAuthProvider, useAdminAuth, LoginForm, AdminLoginPage, createAdminAuthClient, useAdminGoogleSignIn, AdminSignInError, buildAdminApiErrorMessage, getAdminApiErrorCode |
@bricks-common/bo/modules/feedback |
FeedbackSidebarItem — « Feedback » entry mounted once in the sidebar bottomSlot (<FeedbackSidebarItem app="projects" />), next to the changelog item. Element picker → viewport screenshot (modern-screenshot) → annotated dialog → POST /administration/feedbacks → Slack @sos_captain. Admin session only (not notaire). Presentational pieces in design-system/feedback/. |
@bricks-common/bo/modules/component-stories |
Non-prod DS gallery — ComponentStories, componentStoriesNavItems, COMPONENT_STORIES_PATH, isComponentStoriesEnabled (hostname allowlist, the only gate). |
@bricks-common/bo/styles.css |
Tailwind + tokens entrypoint |
@bricks-common/bo/tokens.css |
tokens only |
No deep imports. @bricks-common/bo/design-system/components/button is intentionally not exposed. Export from the barrel first.
Internal authoring — #-prefixed package imports (never ../../)¶
All cross-folder imports inside BO use Node package.json "imports" aliases. Package-private (Node spec) — consumers can't see them. Same-folder imports stay relative.
| Alias | Resolves to |
|---|---|
#beta/* |
src/design-system/beta/*.tsx |
#detail/* |
src/design-system/detail/*.tsx |
#feedback/* |
src/design-system/feedback/*.tsx |
#components/* |
src/design-system/components/*.tsx |
#layouts/* |
src/design-system/layouts/*.tsx |
#inputs/* |
src/design-system/inputs/*.tsx |
#modals/* |
src/design-system/modals/*.tsx |
#tables/* |
src/design-system/tables/*.tsx |
#text-editor/* |
src/design-system/text-editor/*.tsx |
#forms/* |
src/design-system/forms/*.tsx |
#styles/* |
src/design-system/styles/*.ts |
#core/* |
src/core/*.ts |
#hooks/* |
src/hooks/*.ts |
#utils/* |
src/utils/*.ts |
// inside BO — correct
import { cn } from '#utils/cn'
import { Button } from '#components/button'
// inside BO — WRONG
import { cn } from '../../utils/cn'
import { cn } from '@bricks-common/bo/utils' // self-reference
Two tooling gotchas (project_bo_imports_field_gotchas):
- TS bundler resolution requires the file extension on the RHS (
*.tsx/*.ts). Without it, TS errorsImport specifier '#x/y' does not exist in package.json scope. Relative imports get extension-probed;imports-field substitution doesn't. - Rolldown does NOT honor
importsarray fallback. Multi-extension fallback typechecks fine butvite buildfails. One single-target entry per topical folder, dominant extension only. Need both → split into two prefixes.
After any imports change, verify with both a Rolldown consumer (pnpm --filter @bricks/bricksoffice-projects build or …bricksoffice-app-config) and pnpm --filter @bricks/bricksoffice-invest build (Rollup). Typecheck alone isn't sufficient.
Beta kit (design-system/beta/)¶
Smart components: each takes flag: BetaFlagKey and reads the flag itself — consumers never branch on useBetaFlag in JSX. BetaSwitchCard (sidebar topSlot, collapsed = icon toggle + dot), BetaGate (flag off → rendered but blurred/faded, inert, tooltip — it is Gate from #components/gate driven by the flag; a block locked for a business reason uses Gate directly with isLocked + reason), BetaBanner (Alert warning, optional external href/description), BetaPageLayout (PageLayout + banner + gated content — the whole-page form; consumers pass flag/href, never wrap it in a flag-specific component). Adding a beta = one member in the BetaFlagKey union, never an ad-hoc component. Nav: NavLeaf.badge?: string + NavSidebar.topSlot?: ReactNode are the only DS hooks; filtering is the consumer's job.
Detail kit (design-system/detail/, #detail/*)¶
Every entity detail page (financing request, company, project) composes the same kit — never rebuild a header, a two-column shell or a Modifier / Annuler / Enregistrer toggle in a consumer:
DetailLayout(one framed card,flex-1) >DetailMain(unpadded:DetailHeader, optionalTabs+DetailTabsList, thenDetailContentfor the cards) +DetailAside(summary cards, shares the vertical border). AsideCards are compact by descendant selectors — nosizeprop.DetailHeaderslots:back(consumer passesButton asChild+ its routerLink),actions,identifiers(aDetailIdentifiers),trailing(right column),icon,title,badges,subtitle. Router-agnostic.DetailIdentifiers+DetailIdentifier { value, copyable? }— mono identifier line, automatic·separators,CopyButtonunder the hood.DetailSection { title, description?, action?, children }— titled block inside a card, an aside or a drawer.DetailCardRow { icon?, title, description?, badges?, trailing?, onClick? }— person / company / document row inside aCard, rendered as a bordered tile (border bg-muted/30);trailing= right-aligned value (amount, date) before the chevron, which appears when interactive. Navigation goes throughonClick(useNavigate).TabsCount— count pill insideTabsTrigger. Detail tabs useDetailTabsList(line variant, edge to edge, bottom border, triggers hug their label); eachTabsContentwraps its cards inDetailContent.DetailSkeleton— the loading state of a fiche (same frame: header, content, aside), rendered before the query resolves.DetailEditableCard { title, description?, badges?, form, validator?, onSubmit, editDisabledReason?, compact? }(compact= densité aside : « Modifier » enxspour un titre discret ; sansdescriptionl'en-tête passe en une ligne centrée) — see form.FormTextField { control, name, label, … }— label +Input+ helper + error for one RHF field.
Story: Pages › « Detail page kit » renders the whole page full width with each region tagged.
Hooks (src/hooks/)¶
Cross-cutting only:
- use-mobile — viewport breakpoint (Sidebar reads it for mobile drawer).
- useFormSubmissionContext — exposes { isPending } from <Form />. Read via useFormSubmission().
- useIdempotentSubmit — used internally by <Form />.
Bar for adding: no DS/form/style dependency. Single-feature → that app.
Utils (src/utils/)¶
cn, translateValidationError, canvasToJpegBlob. Bar: does it appear in DS source files? If no, it doesn't belong here.
Routing & auth (consumer concern)¶
BO ships AdminAuthProvider + useAdminAuth. Consumer owns the router and the navigation reaction. Two non-obvious rules in TanStack consumers (bricksoffice-projects, bricksoffice-app-config):
- Gate the router behind
auth.isHydrating. Auth hydrates async. Mount<RouterProvider>only after hydration, else_authenticated'sbeforeLoadseesisAuthenticated=falseand the user appears logged out on every reload.src/App.tsx: render<FullScreenLoader />whileisHydrating, mount the router after. beforeLoadis not reactive on context changes. TanStack Router runsbeforeLoadon navigation, not on context ref change.logout()alone doesn't redirect — the user stays on the current page until they navigate. Inroutes/_authenticated.tsx, watchauth.isAuthenticatedanduseNavigate()({ to: '/login' })in auseEffectwhen it flips false. Keep the navigation in the consumer's route layer — BO stays router-agnostic.
Build / deploy pruning gotchas¶
TanStack BO Dockerfiles (bricksoffice-projects, bricksoffice-app-config): 4 stages (pnpm+turbo base → turbo prune <pkg> --docker → install + turbo run build → nginx static via entrypoint.sh that envsubst's $PORT into nginx.conf).
Two worked-around traps (project_turbo_prune_tsconfig_base):
turbo pruneproduces a strippedout/pnpm-lock.yamlthat breaks--frozen-lockfile— the build copies the full rootpnpm-lock.yamlinstead. Same for rootpackage.json+pnpm-workspace.yaml: turbo 2.10+ emptiespatchedDependenciesin the pruned workspace when the prune graph doesn't use the patched dep.turbo prune --dockerdoesn't copy root files (tsconfig.base.json,vitest.config.base.ts,tsdown.config.base.ts) even though per-project configs import them —COPYeach explicitly.- Do not put
tscin consumerbuild. After prune the graph is only TS 6, which hoists;.bin/tscstill shims a nested path that is not installed (MODULE_NOT_FOUND). Vite emits the bundle (noEmittsconfig). Typecheck istype-check.
Build-time env: VITE_API_URL baked at build. Runtime nginx env: just PORT. Railway: Dockerfile in bricksoffice-app-config (dashboard settings); CaC railway.bricksoffice-projects.json in projects.
App switcher (Bricks hub)¶
AppSwitcher / BricksHub are DS-only (popover + trigger). The catalog is not in the DS: getHubCatalog() lives in @bricks-common/bo/core. NavSidebar only renders bottomSlot. The host composes <BricksHub hubApps={getHubCatalog()} /> (projects: changelog then hub). Do not reassemble the trigger. No boot script, no Shadow DOM, no @bricks-common/bo-switcher, no vitest/type-check on the BO package. getHubCatalog() reads the 8 host VITE_* with static import.meta.env so Vite inlines them. Do not map tile URLs in host env.ts and do not invent localhost fallbacks. Always 8 tiles (bricksoffice-technique is not one of them: shared adminAuth, other BO users must not discover it); empty URL = ghost (opacity-50, tooltip « URL manquante », no navigation, favorite off). Popover width follows the grid content. Tiles use a fixed square card (rounded-lg); the BricksLogo fills the card's free height and stays square (rounded-md). Mini variant logo (BO = text-foreground on dark, gold/App = white on amber/orange) is flush bottom-right of the logo. CRM / Défauts / Analyse = gold, no « BO » prefix. « Actuel » sits inside the logo top-left; the favorite star straddles the logo top-right corner. Labels stay on one line with ellipsis. Tile URLs reuse the host’s existing Vite keys (VITE_APP_BO_PROJECTS_URL, VITE_APP_BO_INVEST_URL, VITE_APP_BO_APP_CONFIG_URL, VITE_APP_PROJECT_ANALYSIS_URL, VITE_APP_INVEST_URL, VITE_APP_PDP_URL) plus VITE_BO_CRM_URL / VITE_BO_DEFAUTS_URL. Do not reintroduce footer apps={…} links.
What NOT to do¶
- ❌ Add a
dist/build step to BO — source-only on purpose. - ❌ Add a per-package
biome.json— root config authoritative. Seefeedback_no_per_package_root_configs. - ❌ Replicate any DS component / hook / util inside a consumer app — add it here.
- ❌ Couple BO to a router (
@tanstack/react-router,react-router,expo-router). Auth-driven navigation lives in the consumer's route layer. - ❌ Pass axios as a prop — use
<HttpClientProvider>+useHttpClient(). See data-fetching. - ❌ Hardcode user-facing copy in BO — BO has its own paraglide. See i18n.
- ❌ Write speculative code into empty scaffold folders (
core/,providers/,modals/,tables/,text-editor/) — wait for a spec. Seefeedback_scaffold_folders_no_speculation.