Skip to content

Latest commit

 

History

History
251 lines (212 loc) · 16.7 KB

File metadata and controls

251 lines (212 loc) · 16.7 KB

Agent Instructions

@layerfi/components — Layer's embeddable React accounting component library, published to npm from src/index.tsx. Consumers nest our components under LayerProvider and brand them with CSS variables, so everything is themeable, localizable, and mountable more than once on a page. Most conventions below exist to protect one of those three properties.

Working style

  • Before creating a new definition, check whether a similar implementation already exists in the repo; extend it instead of adding a parallel one
  • Generate designs and code in reviewable phases
  • Do not add comments when writing code, beyond a short note on a genuinely non-obvious constraint
  • Pause and ask for guidance when: task scope is unclear, complications arise, or a phase completes
  • Never recreate a file that is missing from the tree without asking — deletions are intentional
  • Verify with npm run typecheck, npm run lint, and npm test -- --run before handing work back
  • Do not run npm install / npm i — ask first; dependency changes belong in their own PR
  • PRs follow .github/PULL_REQUEST_TEMPLATE.md: Description, Changes, Blockers, How this has been tested

Where to read before you write

Each area has a colocated SKILL.md. Read the relevant one(s) before making changes there.

Working on… Read
Which layer code belongs in, feature-domain boundaries, fixing a boundary error src/SKILL.md
API contracts, Effect Schema, enums, nullability, envelopes, money types src/schemas/SKILL.md
Fetching, mutating, caching — SWR hook factories, cache tags, invalidation src/hooks/api/SKILL.md
Feature/util hooks — which directory, composition and return conventions src/hooks/SKILL.md
Zustand stores, contexts, providers, feature visibility src/providers/SKILL.md
Pure helpers — whether one belongs in utils, and where src/utils/SKILL.md
Internal types — features/ vs shared/ vs utility/ src/types/SKILL.md
Component structure, loading/empty states, responsive UX src/components/SKILL.md
Design-system primitives, style props, variant data attributes src/components/ui/SKILL.md
Building a form — fields, validators, submit and error handling src/components/blocks/Form/SKILL.md
Building a data table — variant choice, columns, row behaviour src/components/blocks/Table/SKILL.md
SCSS, CSS variables, BEM naming, property order src/styles/SKILL.md
Translated strings, plurals, the Crowdin pipeline src/assets/locales/SKILL.md
Formatting money, numbers, percentages, dates, durations src/utils/shared/i18n/SKILL.md
Mocking endpoints — MSW handlers, stateful stores src/msw/SKILL.md
Fixture data — handwritten factories vs generated rows src/fixtures/SKILL.md
Writing tests src/testUtils/SKILL.md
Stories and visual regression .storybook/SKILL.md

Those skills plus this file are the only convention docs — each is the single source of truth for its area, so update the relevant one when a convention changes rather than describing it somewhere new. Cross-cutting rules (TypeScript, imports, lint, commands, CI) live here, because they belong to no single directory. .cursor/BUGBOT.md and .augment/code_review_guidelines.yaml are the same idea for the PR reviewers — the two say the same thing in each tool's format, so update both whenever a non-negotiable here changes, or they drift silently. The remaining root docs are not agent instructions: README.md is consumer-facing usage and PUBLISHING.md is the release process.

Repo map

Path Alias Contains
src/schemas @schemas/* Effect schemas — the source of truth for every API contract, split into features/<domain>/ and common/
src/types @internal-types/* internal-only types (no wire format), split into features/<domain>/, shared/<capability>, utility/ (type-level helpers) and ambient/ (global declarations)
src/utils @utils/* pure helpers, split into features/<domain>/ (domain-aware) and shared/<capability>/ (api, swr, i18n, date, form, number, zustand, styles, …)
src/hooks/api/** @api/* one file per endpoint in a tree mirroring the REST path, named for the HTTP method (get.ts, post.ts, …)
src/hooks/{features,utils,legacy} @hooks/* composed feature logic · generic hooks · pre-factory hooks (don't extend)
src/providers @providers/* scoped Zustand stores and DI contexts, split into features/<domain>/, global/ (the LayerProvider stack) and common/ (domain-agnostic)
src/components/ui @ui/* design-system primitives (domain-agnostic)
src/components/blocks @blocks/* composed patterns: tables, cards, wizards (domain-agnostic)
src/components/features/<domain> @features/* feature UI, one directory per domain object; fetches its own data
src/components/utility @components/utility/* rendering helpers: ConditionalBlock, ResponsiveComponent, withRenderProp
src/views @views/* full-page compositions that mount providers
src/styles design tokens and base CSS, bundled to dist/index.css
src/msw @msw/* mock API, mirroring the same route tree as hooks/api
src/fixtures @fixtures/* fixture factories, generators, and committed generated/*.gen.ts
src/testUtils @testUtils/* LayerTestProvider, form fillers, fixed dates, story helpers

Import boundaries

Two lint-enforced rules. Full detail, including why each layer sits where it does and how to fix a violation, is in src/SKILL.md — read it before moving code between directories.

Layers. Every file belongs to one layer and may import strictly lower layers only:

  1. foundation — @internal-types @schemas @utils @icons @assets
  2. context — @providers/global @providers/common (never fetches)
  3. generic hooks — @hooks/utils
  4. data loading — @api
  5. stores — @providers/features @hooks/legacy
  6. feature hooks — @hooks/features
  7. render helpers — @components/utility
  8. primitives — @ui
  9. patterns — @blocks
  10. feature UI — @features
  11. views — @views
  12. app root — LayerProvider, src/index.tsx

Context sits below the hooks because the SWR factories read LayerContext; stores sit below feature hooks because that is the direction the dependency already ran, 38 to 7.

Feature domains. In schemas, components, hooks, providers and utils, a domain under features/ may import itself plus that partition's declared shared set — nothing else. The sets live in eslint.config.mjs.

Tests and stories are exempt from both rules; production source still may not import @msw, @fixtures, @testUtils or story modules. A domain name means the same thing in every partition.

Non-negotiables

  • Imports: always the most specific alias. No relative parent imports (../), no barrel index.ts files, style imports last. Import order is lint-enforced — run lint:fix, don't sort by hand. Details under TypeScript and imports below.
  • Components: build on @ui before creating anything new; new reusable primitives go in src/components/ui. <HStack>/<VStack> instead of <div>, <Span>/<P>/<Label> instead of raw text elements.
  • Styling: no style prop, no inline styles, no utility class strings, no concatenated class names. Colors and spacing come from src/styles/variables.scss. Prefer component props over new CSS; express variants as data-* attributes via toDataProperties. Write flat, greppable selectors — nest only modifiers of the current selector, never &__Element.
  • Strings: every user-visible string — including aria-label, table headers, and empty states — goes through t('ns:category.key', 'Default') with an inline default. Never hand-edit src/assets/locales/**; it's generated from code and Crowdin.
  • Formatting: never format money, numbers, percentages, or dates by hand. Use useIntlFormatter() / <MoneySpan>. Currency inputs are cents; percent inputs are fractions; dates take a DateFormat enum value, never a format string.
  • Data: no useSWR or fetch in feature code — use the factories in @hooks/utils/swr. businessId and auth are injected; never pass them from a component. When consuming a new backend response, stop and ask for the API contract rather than guessing the schema.
  • State: SWR owns server state, Zustand owns UI state, Context is for DI. Never mirror server data into a store. Never call setState during render, and avoid useEffect and setState for values you can derive — see src/components/SKILL.md.
  • Stabilize deliberately: useCallback only for props to memo()ed children or hook dependency arrays; useMemo only for expensive computations, object/array props to memo()ed children, or dependency-array values. Never memoize primitives. Do memoize object literals returned from custom hooks.

Things to avoid

Each of these has broken something before:

  • Unmocked requests fail. Vitest and Storybook both error on unhandled layerfi.com calls. A new endpoint needs an MSW handler registered in the enclosing handlers.ts.
  • Schema.NullishOr is the default for optional/nullable API fields — the backend omits a field on one endpoint and returns null on another.
  • Never hand-write snake_case JSON. Mocks hold decoded fixtures and encode through the schema, so wire-format changes propagate automatically.
  • Committed fixtures go stale. Touching a fixture schema or generator means npm run fixtures:generate and committing the .gen.ts output; CI checks it.
  • Raw BigDecimal in form or React state triggers TS2589. Use NonRecursiveBigDecimal.
  • CSS variables only exist under the design-system root classes (.Layer__component, .Layer__Portal, …). Portals and bare primitives in stories need one on an ancestor.
  • Locale is part of every SWR cache key, so switching locale refetches. Leave isLocalized at its default.
  • src/msw may not value-import @api/* or @hooks/* — handlers load before per-test mocks apply and would break unrelated suites. Share contracts via @schemas.
  • Import boundaries are lint-enforced on both axes — tiers and feature domains. See Import boundaries; the table lives at the top of eslint.config.mjs.
  • Every @api method file needs an MSW handler at the mirrored path in src/msw/api; npm run msw:check-coverage enforces it in CI.
  • Production source may not import @msw/*, @fixtures/*, @testUtils/*, or *.stories*. Tests and stories are exempt from the tier and domain rules, but not from this one.
  • Responsiveness is measured in JS, not media queries — hence ResponsiveComponent and Chromatic's per-width iframe resizing.
  • Every story is a Chromatic snapshot. Pack primitive variants into one gallery story rather than adding a story per variant.
  • react-hooks/exhaustive-deps is an error. Fix the dependencies; don't disable the rule.
  • Pushing .github/workflows/ changes needs the SSH remote — HTTPS pushes are rejected.

Useful abstractions

Reach for these before writing your own:

Need Use
One data object with loading/error/inactive states ConditionalBlock
An array with loading/error/empty states ConditionalList
Different components per width ResponsiveComponent
Empty/error/loading visuals DataState, SkeletonLoader, SkeletonTableLoader
Tables SimpleDataTable, DataTable, PaginatedDataTable, ExpandableDataTable, VirtualizedDataTable
Forms useAppForm + the Form*Field components; validators in @utils/shared/form/validators
Pagination state @hooks/utils/pagination (usePaginationState, useTablePaginationProps)
A GET / paginated GET / write createQueryHook · createInfiniteQueryHook · createMutationHook
Cache invalidation after a write createResourceGlobalCacheActions + useOnTriggerSuccess
Variant styling on a primitive toDataProperties + data-* selectors

TypeScript and imports

strict is on (noImplicitAny, strictNullChecks, noImplicitOverride, useUnknownInCatchVariables). Vite transpiles; tsc only typechecks.

  • No any. Use unknown and narrow. Avoid as casts; when one is genuinely required at a boundary, add a short comment saying why.
  • Prefer type aliases; use interface when you need declaration merging or self-reference (recursive schemas require it — see src/schemas/SKILL.md).
  • Derive types instead of restating them: typeof Schema.Type, Parameters<typeof useHook>[0], Pick<RawThing, …>, ReturnType<…>.
  • readonly/ReadonlyArray for data you don't own; asMutable (@utils/shared/array/asMutable) at the boundary of an API that demands a mutable array.
  • Shared utility types live in src/types/utility/**: OneOf (exclusive unions), EnumWithUnknownValues (open string enums), pagination, awaitable, table. Check there before writing a new type-level helper.
  • src/types/** is for internal-only types with no wire format. Anything the API sends or receives is a schema. Runtime code (classes, data tables, guards) belongs with its owner in src/utils or the feature — see src/types/SKILL.md.
  • Not yet enabled in tsconfig.json but worth honoring: isolatedModules, verbatimModuleSyntax, noUncheckedIndexedAccess.

Aliases, most specific first: @ui/*, @blocks/*, @features/*, @components/*, @api/*, @hooks/*, @providers/*, @utils/*, @internal-types/*, @schemas/*, @views/*, @icons/*, @assets/*, @msw/*, @fixtures/*, @testUtils/*.

simple-import-sort sorts imports by layer, lowest first, so a block reads bottom-up through the stack. Run lint:fix; never sort by hand. Adding an alias touches two files — see src/SKILL.md.

Type imports are inline-style and enforced: import { type Foo } from '…'.

Lint style rules

Single quotes (in JSX too), no semicolons, 2-space indent, 160-char lines, newline at EOF, operators before line breaks, _-prefix to intentionally ignore a binding, console limited to warn/error/debug.

Don't add explanatory comments describing what the code does. A short comment is warranted only for a non-obvious constraint — a workaround, a required ordering, why a cast is safe, etc.

Commands and CI

Command What
npm test vitest watch (-- --run for one pass)
npm run typecheck tsc --noEmit
npm run lint / lint:fix ESLint + stylelint, with auto-fix
npm run storybook Storybook on :6006
npm run fixtures:generate / fixtures:check regenerate / verify committed fixtures
npm run dev watch-build the library (dev:js + dev:types)
npm run build production build (build:js esm + cjs, then build:types)

Every PR runs eslint, stylelint, typecheck, vitest, build, bundle-size (fails past a growth budget), fixtures (staleness check), storybook, chromatic, and npm-audit.

Publishing

@layerfi/components ships from src/index.tsx; prepack runs typecheck plus a clean build. See PUBLISHING.md and the release-* workflows. Adding an export to src/index.tsx is a public API change — call it out in the PR. It also creates a subpath: build:exports reads that file and generates dist/exports/<Name>.{mjs,cjs,d.mts,d.cts} for every public name, so @layerfi/components/<Name> resolves. Nothing to maintain by hand, but the new subpath is part of the same API commitment.

build:js produces three outputs — per-module ESM, a bundled dist/cjs/index.cjs for the . require condition, and a per-module dist/cjs/modules/** reached only through the subpath shims. The CJS barrel stays bundled on purpose: require never tree-shakes, so splitting it only made require('@layerfi/components') slower.