This file is for AI coding agents and maintainers. It encodes the non‑obvious rules, constraints, and workflows of this repo so automated changes stay fast, safe, and consistent.
When in doubt, favor: (1) tests passing, (2) Core Web Vitals + bundle size, (3) ecosystem compatibility, in that order.
- This is a tree‑shakable React component + block library for the DashTrack / OpenSite ecosystem.
- It powers high‑performance marketing sites driven by a semantic site builder and
@opensite/blocks. - Styling is CSS‑variable + Tailwind CSS v4 driven (see
docs/STYLES.md). - Higher‑level blocks in
components/blocks/*are selected via a block registry exported from@opensite/ui/registry. - Forms in blocks use the framework‑agnostic
@page-speed/formslibrary (seedocs/FORMS_INTEGRATION_GUIDE.md). - This is a library, not an app: avoid baking in app‑specific behavior (Rails, Next, etc.).
-
Preserve tree‑shaking.
- Keep per‑entry files in
src/*.tsthat re‑export a single component/block. - Avoid new “barrel” files that aggregate many exports.
- Keep per‑entry files in
-
Do not hand‑edit the large
exportsmap inpackage.json.- When adding a new public entrypoint, mirror existing patterns and rely on the export‑generation scripts under
scripts/.
- When adding a new public entrypoint, mirror existing patterns and rely on the export‑generation scripts under
-
Respect performance budgets (see
docs/ECOSYSTEM_GUIDELINES.md).- Avoid heavy new runtime dependencies or expensive work during render.
- Prefer existing animation/utility libs already in the tree.
-
Use the shared styling system.
- Components and blocks should rely on the CSS variables defined in
docs/STYLES.md. - Only add new tokens when necessary, and document them in that file.
- Components and blocks should rely on the CSS variables defined in
-
Keep components framework‑agnostic.
- No direct imports from app frameworks or DashTrack apps inside this library.
- Integration‑specific logic belongs in separate helpers or consuming apps.
-
Blocks must stay JSON‑serializable.
- Props for
components/blocks/*should be plain data (no functions, React nodes, or non‑serializable values in block configuration).
- Props for
-
Form behavior uses FormEngine.
- Use
FormEnginefrom@page-speed/forms/integrationfor all block forms. - Expose
formEngineSetup?: FormEngineProps(not rawformConfig/onSubmit). - See
docs/FORMS_INTEGRATION_GUIDE.mdfor implementation patterns.
- Use
-
Prefer copying a proven pattern.
- For new work, start from the closest existing component/block and adapt it rather than designing a completely new structure.
-
Always run minimal checks for code changes.
- At least:
pnpm testandpnpm type-checkbefore considering work complete.
- At least:
components/ui/*– Low‑level UI primitives (buttons, containers, dialogs, etc.).components/blocks/*– High‑level page blocks grouped by domain (about, cta, footers, offer‑modal, etc.).src/*.ts– Tree‑shakable entry files that re‑export components/blocks and the registry.src/registry.ts+registry/*– Block registry implementation and helpers.lib/utils.ts– Utilities such ascn(...)(preferred way to merge class names).docs/ECOSYSTEM_GUIDELINES.md– Platform‑wide performance and architecture rules.docs/STYLES.md– Styling, CSS variables, Tailwind 4 integration (read before changing visuals).docs/FORMS_INTEGRATION_GUIDE.md– Canonical description of how blocks use@page-speed/forms.
Before modifying code in any of these areas, skim the matching doc.
- Accept and propagate
classNameto allow Tailwind customization. - Use
cn(...)fromlib/utils.tsto merge Tailwind classes safely. - Keep public props stable; renaming/removing public props is a breaking change for external consumers.
- Prefer composition (building new behavior from existing primitives) to duplicating logic.
- Blocks are designed to be:
- Semantic – represent real marketing patterns (hero, CTA, testimonials, etc.).
- Serializable – props should be simple data suitable for design payloads.
- Performant – no unnecessary side effects or heavy client logic.
- When adding a block:
- Place it under the right category folder in
components/blocks/*. - Define a clear, typed props interface and export it.
- Add a matching
src/<block-name>.tsentry that re‑exports the component and its types. - Register it in the block registry (see next section) with accurate category + semantic tags.
- Place it under the right category folder in
The registry powers AI‑driven block selection via functions exported from src/registry.ts:
BLOCK_REGISTRYgetBlocksBySemanticTaggetBlocksByCategorygetBlockByIdgetAllBlocks,getAllCategories,searchBlocks
When changing or adding blocks in the registry:
- Give each block a stable, unique ID that will not change across releases.
- Assign categories and semantic tags that clearly describe its purpose and layout.
- Keep descriptions short but precise so AI agents can pick blocks based on intent.
- Avoid reusing IDs/semantics for unrelated designs; prefer adding a new entry.
- If a block’s design/intent changes, update its metadata in
src/registry/blocks.ts(name/description/semanticTags/exampleUsage).pnpm buildregeneratesregistry-export.json.
- Tailwind v4 +
@theme inlineis the assumed environment. - Visual tokens (colors, radii, spacing, button variants, shadows, etc.) are defined as CSS variables in the base CSS template in
docs/STYLES.md. - To change visuals:
- First try adjusting existing CSS variables.
- Only introduce new variables if a concept truly cannot reuse an existing one.
- Update
docs/STYLES.mdwhen you add or repurpose tokens.
- Do not hard‑code theme values directly in components if they can be expressed via variables.
Form‑enabled blocks use the FormEngine component from @page-speed/forms/integration. This provides a unified, streamlined API for all form handling.
Form blocks expose these props:
formEngineSetup?: FormEngineProps– Full form configuration (endpoint, callbacks, fields, layout)buttonAction?: ActionConfig– Submit button customization (for newsletter/button‑group forms)formSlot?: React.ReactNode– Escape hatch for fully custom form rendering
-
Newsletter/Button‑Group (single‑field forms like email signup):
- Uses
formLayout: "button-group"in formLayoutSettings - Input field with inline submit button
buttonActioncontrols button icon/label/variant- Reference:
components/blocks/footers/footer-split-image-accordion.tsx
- Uses
-
Standard Multi‑Field (contact forms, support forms, etc.):
- Uses default or
formLayout: "standard"layout - Grid of fields with submit button below
- Reference:
components/blocks/contact/contact-support.tsx
- Uses default or
When adding forms to blocks:
-
Import from
@page-speed/forms/integration:import { FormEngine, type FormEngineProps, type FormEngineStyleRules, type FormFieldConfig, } from "@page-speed/forms/integration";
-
Define
DEFAULT_STYLE_RULESandDEFAULT_FORM_FIELDSat module level. -
Implement
renderFormusingReact.useMemo:- Check
formSlotfirst (escape hatch) - Return
nullif noformEngineSetupprovided - Pass defaults via
defaultFieldsanddefaultStyleRulesprops
- Check
-
Keep
formEngineSetupJSON‑serializable so it flows through design payloads. -
Do not embed API keys or environment‑specific URLs directly in reusable blocks.
See docs/FORMS_INTEGRATION_GUIDE.md for complete implementation examples.
- Build is handled by tsup (
tsup.config.ts) with many named entrypoints listed in theallEntriesmap. - For a new public module, you typically need to:
- Add a
src/<entry>.tsfile that re‑exports the component/types. - Add that entry to the
allEntriesmap intsup.config.ts. - Run the export‑generation scripts (see
package.jsongenerate:exportschain) instead of hand‑editingexports.
- Add a
pnpm buildregenerates export maps andregistry-export.jsonautomatically.- Keep
sideEffects: falseaccurate: if you introduce code with side effects at import time, refactor or mark it explicitly.
- Locate it under
components/ui/*and inspect usages if needed. - Check whether the change can be expressed via CSS variables instead of new hard‑coded styles.
- Update props and implementation conservatively; keep public API stable when possible.
- Add or update tests if behavior changes.
- Run
pnpm testandpnpm type-check.
- Identify the closest existing block category and use that as a template.
- Implement the block under
components/blocks/<category>/with JSON‑friendly props. - Create a
src/<block-name>.tsentry that re‑exports the block and its props type. - Register it in the block registry with good semantic tags.
- If it includes a form, wire it using the patterns in
docs/FORMS_INTEGRATION_GUIDE.md. - Add tests for basic rendering and key behaviors.
- Edit CSS variables in the base template described in
docs/STYLES.md. - Verify dark‑mode behavior and example components still look correct.
- Avoid introducing Tailwind config changes that would break consuming apps.
- Read
docs/FORMS_INTEGRATION_GUIDE.mdfor the current FormEngine patterns. - Use
FormEnginefrom@page-speed/forms/integration– do NOT use rawuseForm/Form/Fielddirectly in blocks. - Expose
formEngineSetup?: FormEnginePropsas the primary form configuration prop. - For newsletter/button‑group forms, also expose
buttonAction?: ActionConfig. - Always provide
formSlot?: React.ReactNodeas an escape hatch for custom forms. - Define
DEFAULT_STYLE_RULESandDEFAULT_FORM_FIELDSat module level. - Ensure all props remain JSON‑serializable for design payload compatibility.
- Reference existing blocks:
- Newsletter pattern:
components/blocks/footers/footer-split-image-accordion.tsx - Standard form pattern:
components/blocks/contact/contact-support.tsx
- Newsletter pattern:
Before considering a change “done”, an agent should:
- Run
pnpm test(or the narrowest relevant test command). - Run
pnpm type-check. - For export or build‑related changes, run
pnpm buildlocally or ensure CI will run it. - Confirm new/changed blocks remain JSON‑serializable and correctly wired into the registry.
- Confirm any styling changes go through CSS variables and respect Tailwind 4 expectations.
If any of these cannot be completed (e.g. missing environment or permissions), clearly note what was skipped and why in the PR description or commit message.