Aller au contenu

| @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 every bricksoffice-* 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):

  1. TS bundler resolution requires the file extension on the RHS (*.tsx / *.ts). Without it, TS errors Import specifier '#x/y' does not exist in package.json scope. Relative imports get extension-probed; imports-field substitution doesn't.
  2. Rolldown does NOT honor imports array fallback. Multi-extension fallback typechecks fine but vite build fails. 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, optional Tabs + DetailTabsList, then DetailContent for the cards) + DetailAside (summary cards, shares the vertical border). Aside Cards are compact by descendant selectors — no size prop.
  • DetailHeader slots: back (consumer passes Button asChild + its router Link), actions, identifiers (a DetailIdentifiers), trailing (right column), icon, title, badges, subtitle. Router-agnostic.
  • DetailIdentifiers + DetailIdentifier { value, copyable? } — mono identifier line, automatic · separators, CopyButton under 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 a Card, rendered as a bordered tile (border bg-muted/30); trailing = right-aligned value (amount, date) before the chevron, which appears when interactive. Navigation goes through onClick (useNavigate).
  • TabsCount — count pill inside TabsTrigger. Detail tabs use DetailTabsList (line variant, edge to edge, bottom border, triggers hug their label); each TabsContent wraps its cards in DetailContent.
  • 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 » en xs pour un titre discret ; sans description l'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):

  1. Gate the router behind auth.isHydrating. Auth hydrates async. Mount <RouterProvider> only after hydration, else _authenticated's beforeLoad sees isAuthenticated=false and the user appears logged out on every reload. src/App.tsx: render <FullScreenLoader /> while isHydrating, mount the router after.
  2. beforeLoad is not reactive on context changes. TanStack Router runs beforeLoad on navigation, not on context ref change. logout() alone doesn't redirect — the user stays on the current page until they navigate. In routes/_authenticated.tsx, watch auth.isAuthenticated and useNavigate()({ to: '/login' }) in a useEffect when 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):

  1. turbo prune produces a stripped out/pnpm-lock.yaml that breaks --frozen-lockfile — the build copies the full root pnpm-lock.yaml instead. Same for root package.json + pnpm-workspace.yaml: turbo 2.10+ empties patchedDependencies in the pruned workspace when the prune graph doesn't use the patched dep.
  2. turbo prune --docker doesn't copy root files (tsconfig.base.json, vitest.config.base.ts, tsdown.config.base.ts) even though per-project configs import them — COPY each explicitly.
  3. Do not put tsc in consumer build. After prune the graph is only TS 6, which hoists; .bin/tsc still shims a nested path that is not installed (MODULE_NOT_FOUND). Vite emits the bundle (noEmit tsconfig). Typecheck is type-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. See feedback_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. See feedback_scaffold_folders_no_speculation.