Aller au contenu

Shared back-office lib — @bricks-common/bo

Lives at projects/common/back-office, consumed by every bricksoffice-* app (bricksoffice-projects, bricksoffice-invest). 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. Keep in sync: BO public surface, #-import map, no-build-step, providers/router-coupling, or build/deploy → update this file in the same PR.

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.

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).

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, withGlobalErrorHandling, useSubmitWithError, formatDate/formatDateTime, formatCentsFull/formatCentsShort, parseEurosInputToCents, parseDecimalInput, createAdminAxios
@bricks-common/bo/modules/auth AdminAuthProvider, useAdminAuth, LoginForm, AdminLoginPage, createAdminAuthClient, useAdminGoogleSignIn, AdminSignInError, buildAdminApiErrorMessage, getAdminApiErrorCode
@bricks-common/bo/modules/component-stories Dev-only DS gallery — ComponentStories, componentStoriesNavItems, COMPONENT_STORIES_PATH. Gate behind import.meta.env.DEV.
@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
#components/* src/design-system/components/*.tsx
#layouts/* src/design-system/layouts/*.tsx
#inputs/* src/design-system/inputs/*.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
#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 pnpm --filter @bricks/bricksoffice-projects build (Rolldown) and pnpm --filter @bricks/bricksoffice-invest build (Rollup). Typecheck alone isn't sufficient.

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/)

Currently just cn. 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 bricksoffice-projects:

  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

bricksoffice-projects Dockerfile: 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 tsconfig.base.json even though per-project tsconfig.build.json extends it — COPY it explicitly.

Build-time env: VITE_API_URL baked at build. Runtime nginx env: just PORT. Deployed by Railway from railway.bricksoffice-projects.json.

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.