diff --git a/.cursor/plans/context_ssot_restructure_c6889ec0.plan.md b/.cursor/plans/context_ssot_restructure_c6889ec0.plan.md new file mode 100644 index 00000000..f41985c1 --- /dev/null +++ b/.cursor/plans/context_ssot_restructure_c6889ec0.plan.md @@ -0,0 +1,650 @@ +--- +name: Context SSOT Restructure +overview: 'Restructure the rules/ and docs/ across all three packages (@wix/interact, @wix/motion, @wix/motion-presets) into a single-source-of-truth system where structured data (params, defaults, types, term definitions) lives in YAML glossary files. rules/ output is fully generated by renderer functions; docs/ is hand-authored with structured sections injected from YAML. No template files, no marker syntax. Work proceeds one package at a time: Interact, then Motion, then Motion-Presets.' +todos: + - id: phase-0-schema + content: Design YAML glossary schema and renderer function signatures + status: pending + - id: phase-0-build + content: Build scripts/build-context.js (YAML -> renderer functions -> rules/ full output; YAML -> inject structured sections into hand-authored docs/) + status: pending + - id: phase-1-audit + content: 'Interact: Audit and verify all ground truth claims via ad-hoc Vitest tests' + status: pending + - id: phase-1-glossary + content: 'Interact: Create context/glossary.yaml with all verified terms, params, defaults' + status: pending + - id: phase-1-rules-templates + content: 'Interact: Design renderer functions and write glossary entries for rules output (overview, config, triggers, effects, pitfalls)' + status: pending + - id: phase-1-docs-templates + content: 'Interact: Author hand-written docs files; add PARAMS:START/END markers for injected sections (guides, api, integration, examples)' + status: pending + - id: phase-1-build-validate + content: 'Interact: Run build, iterate until output is correct and readable; Vitest audit tests serve as the validation layer' + status: pending + - id: phase-1-replace + content: 'Interact: Replace old rules/ and docs/ with generated output, verify all builds pass' + status: pending + - id: phase-2-audit + content: 'Motion: Audit and verify ground truth (API signatures, return types, scroll/pointer)' + status: pending + - id: phase-2-migrate + content: 'Motion: Create glossary, renderer functions, build, and replace (add new rules/ dir)' + status: pending + - id: phase-3-audit + content: 'Motion-Presets: Audit all 62 production-facing presets params/defaults against source (19 entrance + 19 scroll + 13 ongoing + 11 mouse)' + status: pending + - id: phase-3-migrate + content: 'Motion-Presets: Create glossary, renderer functions, build, and replace (62 production presets)' + status: pending + - id: phase-3-cross-validate + content: 'Cross-package validation: verify shared concepts are consistent across all three packages' + status: pending +isProject: false +--- + +# Context SSOT Restructure + +## Motivation and Idea + +### The problem + +This monorepo publishes three packages (`@wix/interact`, `@wix/motion`, `@wix/motion-presets`) alongside context files designed for two audiences: **LLM-facing rules** (so AI agents can correctly integrate the packages) and **human-facing docs** (for developer onboarding and reference). Today these context files suffer from five interconnected problems: + +1. **Multiple contradicting sources of truth.** The same concept is described in different files with different phrasing, different defaults, and sometimes outright conflicting claims. For example, `allowA11yTriggers` defaults to `false` in one file and `true` in another; `ParallaxScroll` accepts a `speed` param in docs but the code uses `parallaxFactor`; trigger counts vary between 7, 8, and 9 depending on which file you read. A deep audit found **8 critical discrepancies** where docs would cause broken integrations, and **10 more significant ones**. +2. **No common structure.** Each package organizes its context differently. Interact has flat trigger-specific rule files plus two overlapping hub files; Motion has no rules at all; Motion-Presets has YAML-frontmatter rule files split by category. The docs folders vary in depth, naming, and section layout. There is no template or convention that applies across packages. +3. **Stale or incorrect information.** Defaults, param names, return types, and API signatures in the context files do not match the current implementation. There is no mechanism to detect this drift. +4. **Heavy repetition.** FOUC prevention is explained in 6 different files; element resolution order appears in 4; entry-point setup is repeated across every package's getting-started material. Each copy drifts independently. +5. **Broken links and scaffolding.** Over 36 internal links point to files that do not exist. Multiple sections are marked "TBD". README index pages link to planned-but-never-written guides. This erodes trust in the documentation for both humans and LLMs. + +Together, these create a **continuous development problem**: making any change to the context requires touching many files across multiple directories, producing large PRs that are hard to review and prone to introducing new inconsistencies. The cost of keeping context accurate compounds over time. + +### The idea + +Replace the current ad-hoc markdown files with a **structured, build-based system** where: + +- **Each piece of information is defined once** in a YAML glossary file (one per package). The glossary holds the data that is most prone to going stale: parameter names and types, default values, API signatures, term definitions (with separate LLM and human phrasings), and known caveats. +- **Markdown template files** provide the document structure and prose. They contain markers (e.g., `{{term:trigger-viewEnter.params-table}}`) where glossary data should be injected. Templates are authored separately for rules (compact, LLM-optimized) and docs (narrative, human-friendly). +- **A lightweight build script** reads the glossary and templates, performs marker replacement, and writes the final `rules/` and `docs/` output files. +- **A validation script** checks glossary entries against TypeScript source code, catching drift before it reaches the published context. + +This means: + +- Changing a default value or param name is a **one-line YAML edit** that propagates everywhere. +- Rules and docs always agree because they draw from the same data. +- The validation script catches code-vs-context drift in CI. +- Each package follows the same structure, making the system predictable and reviewable. + +### Why YAML for the glossary + +The glossary contains prose-heavy entries (descriptions, caveats) that humans will frequently hand-edit. YAML supports multi-line strings and inline comments natively, which makes authoring and PR review substantially easier than JSON. The motion-presets rules already use YAML frontmatter, so the pattern is familiar in this repo. Since the build script parses YAML into a plain JS object, switching to JSON later would be a trivial change. + +### Sequencing + +The migration is designed to proceed **one package at a time** (Interact, then Motion, then Motion-Presets), with each package's old context files replaced only after the new mechanism is fully built, validated, and reviewed. This keeps PRs scoped and reviewable, and ensures no package is left in a half-migrated state. + +--- + +## Audit Findings (Baseline) + +This section captures the findings from the deep analysis of all 72 context files across the three packages and their comparison against source code. These findings serve as the ground truth for the migration. + +### Current State Inventory + +**File counts:** + +| Package | `rules/` files | `docs/` files | Total | +| -------------------------------------- | -------------- | ------------- | ------ | +| Interact (`@wix/interact`) | 7 | 26 | 33 | +| Motion (`@wix/motion`) | 0 | 20 | 20 | +| Motion-Presets (`@wix/motion-presets`) | 5 | 14 | 19 | +| **Total** | **12** | **60** | **72** | + +**Structural asymmetry:** + +- **Interact** has both `rules/` (flat trigger-focused files: `click.md`, `hover.md`, `viewenter.md`, `viewprogress.md`, `pointermove.md`, plus two hub files `full-lean.md` at 692 lines and `integration.md` at 329 lines) and `docs/` (nested into `guides/`, `api/`, `examples/`, `integration/`, `advanced/`). +- **Motion** has only `docs/` (nested into `api/`, `categories/`, `guides/`, `examples/`) plus a stale internal `PLAN_DOCS.md`. No `rules/` directory exists at all. +- **Motion-Presets** has both `rules/` (5 files under `presets/` with YAML frontmatter, split by category) and `docs/` (14 files nested per category with individual preset pages). + +**Existing patterns for selective LLM reading:** + +- Interact rules: `## Table of Contents` with `#anchor` links; `---` thematic breaks between sections; no YAML frontmatter. +- Motion-Presets rules: YAML frontmatter with `name`/`description` (agent-loading hints); per-file TOCs down to per-preset anchors. +- Typical file sizes: 190-280 lines for single-trigger interact docs, 330 lines for integration hub, 690 lines for `full-lean`, 210-398 lines for presets rule files. + +### Discrepancies: Docs/Rules vs Code + +#### Critical (would cause broken integrations if an LLM follows the docs) + +| # | Topic | What docs/rules say | What the code does | Source files | +| --- | ----------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | +| 1 | `allowA11yTriggers` default | `rules/integration.md`: **false**; `docs/api/types.md`: **true** | Code: **true** -- `click` auto-maps to `activate`, `hover` to `interest` | `src/handlers/index.ts` | +| 2 | `ParallaxScroll` param name | All docs consistently use `**speed`\*\* | Code: `**parallaxFactor`\*\* (default `0.5`) | `motion-presets/src/library/scroll/ParallaxScroll.ts`, `types.ts` | +| 3 | `Pulse` intensity default | `docs/ongoing/pulse.md`: **1.0** | Code: **0** | `motion-presets/src/library/ongoing/Pulse.ts` | +| 4 | `ArcIn` default direction | **RESOLVED in rules** (`entrance-presets.md` now states `'right'`). `docs/entrance/arc-in.md` still needs update. | Code: `**'right'`\*\* | `motion-presets/src/library/entrance/ArcIn.ts` | +| 5 | `namedEffect` shape | Many docs use bare string: `namedEffect: 'FadeIn'` | Code requires object: `namedEffect: { type: 'FadeIn' }` | `motion/src/api/common.ts` `getNamedEffect` | +| 6 | `getCSSAnimation` return type | `api/core-functions.md`, `performance.md`: **string** | Code: **array of objects** `({ target, animation, keyframes, ... })` | `motion/src/api/cssAnimations.ts` | +| 7 | `AnimationEndParams.effectId` | Typed and documented as wiring mechanism | Handler **ignores it** (`__` param) | `interact/src/handlers/animationEnd.ts` | +| 8 | `viewProgress` params | `api/types.md` maps `viewProgress: ViewEnterParams` | Handler ignores those params; scroll options come from `Interact.setup({ scrollOptionsGetter })` | `interact/src/handlers/viewProgress.ts` | + +#### Significant (causes confusion, may lead to subtle bugs) + +| # | Topic | Discrepancy | +| --- | --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 9 | Sticky/tall-wrapper ViewTimeline source | `viewprogress.md` Rule 3: `key` on tall wrapper = source. `full-lean.md`: sticky child = source. Contradicts itself. | +| 10 | Trigger taxonomy undocumented | No file explains the trigger taxonomy. Counts vary (7 or 9) because `activate`/`interest` are a11y variants (not independent user triggers) and `pageVisible` is deprecated. The correct framing: **6 primary user-facing triggers** (hover, click, viewEnter, viewProgress, pointerMove, animationEnd) + 2 a11y variants (activate, interest) enabled by `allowA11yTriggers: true`. `guides/README.md`'s "7" and `understanding-triggers.md`'s "9" are both partially correct but unexplained. | +| 11 | `pageVisible` trigger | **Soon to be deprecated.** Currently absent from most trigger tables; only in `api/types.md`. Uses viewEnter's IntersectionObserver handler. Docs should note deprecation status and point to `viewEnter` as the replacement. | +| 12 | Ongoing preset count | `presets-main.md`: **RESOLVED** (now correctly lists 13). `docs/presets/README.md`: still says 16. Barrel exports: **13**; DVD exists but is not barrel-exported. | +| 13 | Total documented preset count | `docs/presets/README.md`: "82+"; `README.md` (package root): **62** (19+19+13+11, bg-scroll and CustomMouse intentionally excluded). Barrel exports: 75 (includes 12 bg-scroll and CustomMouse). **Background-scroll (12) is intentionally excluded (not production-ready). CustomMouse is intentionally excluded (internal use for `customEffect`).** `docs/presets/README.md`'s "82+" figure is the remaining wrong value. | +| 14 | **RESOLVED** Angle convention | ~~`presets-main.md`: 0 = right; `_template.md`: 0 = up.~~ `presets-main.md` now correctly documents 0° = right throughout. | +| 15 | `customEffect` signature | Varies: 2-arg in rules, 3-arg in some docs. Actual depends on context (time: `progress` number; pointer: `Progress { x, y, v, active }`) | +| 16 | `TurnScroll.rotation` param | Typed in `types.ts` but **ignored** in implementation (fixed +/-45deg) | +| 17 | `ParallaxScroll.range` param | Typed but **unused** in `ParallaxScroll.ts` implementation | +| 18 | `generate-llms.mjs` trigger list | `scripts/generate-llms.mjs` `STATIC_BODY` hardcodes "**Five trigger types**: hover, click, viewEnter, viewProgress, pointerMove" — `animationEnd` is missing. This string is published to `llms.txt` via CI and directly misleads LLMs. Should become "Six primary trigger types: hover, click, viewEnter, viewProgress, pointerMove, animationEnd — plus accessible variants activate and interest." | + +#### Minor (quality / completeness) + +| # | Topic | +| --- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 19 | **36+ broken internal links** across all packages: references to nonexistent files like `testing.md`, `performance.md`, `playground/`, `scroll-animations.md`, etc. | +| 20 | **5+ TBD placeholder sections** in interact docs (`configuration-structure`, `effects-and-animations`, `state-management`, `lists`, `custom-elements`) | +| 21 | **Code typos in docs**: `sytle`, `hitAea`, `docuement`, `getScrgetWebAnimationubScene`, truncated/invalid snippets | +| 22 | **RESOLVED** `unit` vs `type` for length objects: `presets-main.md` now consistently uses `{ value, unit: 'px' }` throughout. | +| 23 | `Interact.getElement` referenced in docs but does not exist as a public API | + +### Repetition Analysis + +The same information is repeated across multiple files with inconsistent phrasing: + +| Concept | Files that describe it | Copies | +| ---------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------- | +| FOUC prevention (`generate` + `initial`) | `rules/integration.md`, `rules/viewenter.md`, `rules/full-lean.md`, `docs/api/functions.md`, `docs/examples/entrance-animations.md`, `docs/guides/getting-started.md` | 6 | +| Element resolution order | `rules/integration.md`, `rules/full-lean.md`, `docs/api/element-selection.md`, `docs/guides/configuration-structure.md` | 4 | +| Entry-point setup | `rules/integration.md`, `docs/README.md`, `docs/guides/getting-started.md`, `docs/integration/react.md` | 4 | +| Trigger inventory table | `rules/full-lean.md`, `rules/integration.md`, `docs/guides/understanding-triggers.md`, `docs/api/types.md` | 4 (with different counts) | +| `registerEffects` usage | motion `docs/getting-started.md`, interact `rules/integration.md`, presets `docs/presets/README.md`, presets `rules/presets-main.md` | 4 | +| Scroll range semantics | interact `rules/viewprogress.md`, presets `rules/scroll-presets.md`, motion `docs/core-concepts.md`, presets `docs/scroll/README.md` | 4 | +| Stagger/sequence formula | motion `docs/core-concepts.md`, `docs/api/sequence.md`, `docs/api/get-sequence.md`, interact `docs/guides/sequences.md` | 4 | +| Reduced motion / a11y | Described independently across ~8 files in all three packages | ~8 | + +### Verified Ground Truth: What Each Package Actually Contains + +#### `@wix/interact` -- Declarative Interaction Layer + +**Entry points:** `@wix/interact` (vanilla), `@wix/interact/react`, `@wix/interact/web` + +**Exports:** + +- `Interact` class (static + instance API) +- Functions: `add`, `remove`, `generate` +- React: `Interaction` component, `createInteractRef` +- Web: `InteractElement` custom element (registered via `Interact.defineInteractElement`) + +**Config schema (`InteractConfig`):** `{ effects: Record, interactions: Interaction[], sequences?: Record, conditions?: Record }` + +**Trigger taxonomy (9 members in `TriggerType` union, but distinct roles):** + +- **6 primary user-facing triggers:** `hover`, `click`, `viewEnter`, `viewProgress`, `pointerMove`, `animationEnd` — these are the triggers users configure directly +- **2 a11y variant triggers:** `activate` (accessible version of `click`: adds `keydown`), `interest` (accessible version of `hover`: adds `focusin`/`focusout`) — enabled transparently by `allowA11yTriggers: true`; users do not configure them directly +- **1 deprecated trigger:** `pageVisible` — soon to be removed; currently shares the IntersectionObserver handler with `viewEnter`; documentation should note deprecated status and point to `viewEnter` as the replacement + +**Handler mappings (from `handlers/index.ts`):** + +- `viewEnter`, `pageVisible` (deprecated) -> IntersectionObserver handler +- `hover` -> `mouseenter`/`mouseleave`; when `allowA11yTriggers: true`, remaps to `interest` handler +- `click` -> `['click']`; when `allowA11yTriggers: true`, remaps to `activate` handler +- `activate` (a11y variant) -> `['click', 'keydown']` +- `interest` (a11y variant) -> enter: `mouseenter`+`focusin`, leave: `mouseleave`+`focusout` +- `animationEnd` -> listens on source `animationend`, plays on target +- `viewProgress` -> ViewTimeline scrub or `getScrubScene` fallback +- `pointerMove` -> `getScrubScene` + pointer library + +**3 effect types:** `TimeEffect` (has `duration`), `ScrubEffect` (has `rangeStart`/`rangeEnd`), `StateEffect` (has `transition`/`transitionProperties`) + +`triggerType` values: `'once' | 'repeat' | 'alternate' | 'state'`; defaults: `'once'` for viewEnter/animationEnd (and deprecated pageVisible), `'alternate'` for hover/click/activate/interest + +`**stateAction` values:\*\* `'add' | 'remove' | 'toggle' | 'clear'`; default: `'toggle'` + +**Condition types:** `'media' | 'container' | 'selector'` only (no `'custom'`) + +**Key param defaults:** + +- `ViewEnterParams.threshold`: `0.2` +- `PointerMoveParams.axis`: `'y'` +- `PointerMoveParams.hitArea`: undefined (covers document body) + +#### `@wix/motion` -- Core Animation Engine + +**Exported functions:** `getWebAnimation`, `getScrubScene`, `getCSSAnimation`, `prepareAnimation`, `getElementCSSAnimation`, `getElementAnimation`, `getSequence`, `createAnimationGroups`, `registerEffects` + +**Exported utilities:** `getCssUnits`, `getEasing`, `getJsEasing`, all Penner-style easings + `jsEasings`/`cssEasings` maps + +**Type-only exports (not constructable):** `AnimationGroup`, `Sequence` + +**Key behaviors:** + +- `getAnimation` (internal, used by interact): chooses CSS path (if preset has `style`) or WAAPI path (`getWebAnimation`) +- `getCSSAnimation` returns array of CSS rule descriptor objects, not a string +- ViewTimeline: native (`window.ViewTimeline`) with `duration: 'auto'`; fallback: `duration: 99.99` with manual scrub via `getScrubScene` +- Pointer: without keyframes uses factory `MouseAnimationInstance`; with keyframes uses `AnimationGroup.progress()` +- `registerEffects(effects)` merges into internal registry; presets resolve by `namedEffect.type` string +- `fastdom` used for DOM batching (measure/mutate), not re-exported + +`**AnimationGroup` API:\*\* `play`, `pause`, `reverse`, `cancel`, `progress(p)`, `setPlaybackRate`, `getProgress`, `onFinish`, `ready`, `finished`, `playState` + +`**Sequence`:\*_ extends `AnimationGroup`; stagger formula: `offset[i] = easing(i / last) _ last \* offsetMs`(integer-truncated); supports`addGroups`/`removeGroups` + +`**RangeOffset` names:\*\* `'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing'` + +#### `@wix/motion-presets` -- Ready-Made Effects + +**5 categories, 75 barrel-exported presets. Production-facing (documented in rules and docs): 62 across 4 categories.** + +Intentional exclusions from documentation: + +- **Background-scroll (12):** Not production-ready. Present in barrel but excluded from all rules and docs. +- **`CustomMouse` (1):** Internal use only (for `customEffect`). Present in barrel but excluded from rules and docs. + +**Entrance (19):** ArcIn, BlurIn, BounceIn, CurveIn, DropIn, ExpandIn, FadeIn, FlipIn, FloatIn, FoldIn, GlideIn, RevealIn, ShapeIn, ShuttersIn, SlideIn, SpinIn, TiltIn, TurnIn, WinkIn + +**Scroll (19):** ArcScroll, BlurScroll, FadeScroll, FlipScroll, GrowScroll, MoveScroll, PanScroll, ParallaxScroll, RevealScroll, ShapeScroll, ShuttersScroll, ShrinkScroll, SkewPanScroll, SlideScroll, Spin3dScroll, SpinScroll, StretchScroll, TiltScroll, TurnScroll + +**Ongoing (13):** Bounce, Breathe, Cross, Flash, Flip, Fold, Jello, Poke, Pulse, Rubber, Spin, Swing, Wiggle (DVD exists but is NOT barrel-exported) + +**Mouse (11 documented, 12 exported):** AiryMouse, BlobMouse, BlurMouse, BounceMouse, ScaleMouse, SkewMouse, SpinMouse, SwivelMouse, Tilt3DMouse, Track3DMouse, TrackMouse (CustomMouse excluded from docs — internal) + +**Registration:** `registerEffects` is in `@wix/motion`, not in this package. Presets are plain modules keyed by `namedEffect.type`. Typical usage: `registerEffects({ FadeIn, ParallaxScroll, ... })`. + +**Preset module shapes:** namespace with `web`/`style`/`getNames` (most), mouse presets export `create` factories, some presets have `prepare` (background-scroll). + +**Shared params:** All mouse presets share `inverted?: boolean` (default `false`). Scroll presets support `range?: 'in' | 'out' | 'continuous'` (default varies per preset). Ongoing presets support `iterationDelay?: number` (default `0`). + +**Angle convention in code:** 0 = right, counterclockwise increases (90 = top). + +**Known type-vs-implementation mismatches in presets:** + +- `TurnScroll.rotation`: typed but ignored (fixed +/-45deg) +- `ParallaxScroll.range`: typed but unused +- `DVD`: typed and implemented but not barrel-exported + +### Cross-Package Shared Concepts (need SSOT) + +| Concept | Owner (should be SSOT) | Referenced by | +| ------------------------------------------------------ | ---------------------- | ----------------- | +| `registerEffects` API | motion | interact, presets | +| `AnimationGroup` / `Sequence` types | motion | interact | +| `namedEffect` shape (`{ type: '...' }`) | motion | interact, presets | +| Scroll ranges (`RangeOffset`, range names) | motion | interact, presets | +| Pointer progress (`Progress { x, y, v, active }`) | motion | interact, presets | +| `EffectScrollRange` (`in`/`out`/`continuous`) | presets | presets only | +| Direction type families (`EffectFourDirections`, etc.) | presets | presets only | +| Easing values (CSS + JS) | motion | interact, presets | +| `prefers-reduced-motion` pattern | interact | motion, presets | +| Length/unit convention (`{ value, unit }`) | motion | presets | + +### Build and Test Infrastructure (Existing) + +- **Monorepo:** Yarn 4 workspaces, no Turbo/Nx +- **Build:** Vite for library bundles, `tsc` for types +- **Unit tests:** Vitest in all three packages (`jsdom` environment for interact) +- **E2E:** Playwright exists for `@wix/motion` only (`packages/motion/e2e/`). Interact has a CI workflow referencing Playwright but no actual Playwright config or tests. +- **Docs deployment:** `apps/docs` copies `packages/interact/docs` into Vite dist via `apps/docs/scripts/copy-docs.js`. Rules are served raw from the docs app under `/rules/`. +- **LLM context generation:** `scripts/generate-llms.mjs` (207 lines, plain ESM, zero deps) reads all `.md` files in `packages/interact/rules/`, generates `llms.txt` and `llms-full.txt` at repo root and copies `llms.txt` into `packages/interact/`. Wired to a `generate:llms` root script and used by CI workflows (`interactdocs.yml`, `preview-llms.yml`). **Any structural change to interact rules must remain compatible with this script.** Note: this script's static body currently says "Five trigger types" — see discrepancy #18. +- **No existing codegen, templating, or doc validation tooling** in the repo beyond `generate-llms.mjs`. + +--- + +## Recommendation: Glossary Format + +Use **YAML data files for structured/verifiable data** (parameter tables, defaults, type signatures, term definitions). A lightweight Node.js build script turns YAML into final `rules/` output (fully generated) and injects structured sections into hand-authored `docs/` files. + +Why YAML for the data layer: + +- Machine-parseable: the Vitest audit tests can import YAML and assert values match source +- Maps naturally to the structured data already present in current rules (param tables, trigger maps, preset catalogs) +- motion-presets rules already use YAML frontmatter, so the pattern is familiar +- Prose stays in markdown where it belongs — the YAML only holds data that is prone to going stale + +Why **no template files and no marker syntax:** + +- The existing motion-presets rules files already have a fixed, completely regular structure: frontmatter → H1 → intro → TOC → per-item sections (visual description, params, code example). This structure is the same for every category file. +- Encoding that layout as **renderer functions** in the build script (`renderPresetCategoryFile`, `renderTriggerFile`, etc.) is simpler and more maintainable than maintaining a separate template file per output file. +- No custom template syntax to parse; no marker replacement logic; the build script is plain string-building, following the same style as the existing `generate-llms.mjs`. +- For `docs/`, structured sections (param tables, API signatures) are injected between HTML comment markers (`` / ``). All prose outside the markers is hand-authored and untouched by the build script. + +--- + +## Directory Structure (per package) + +``` +packages// + context/ # NEW - single source of truth + glossary.yaml # All terms, params, defaults, descriptions + rules/ # OUTPUT - fully generated from glossary.yaml + docs/ # HAND-AUTHORED - structured sections injected from glossary.yaml + guides/ + api/ + ... +``` + +A shared build script lives at the monorepo root: + +``` +scripts/ + build-context.js # Reads glossary YAML, renders rules/ (full), injects into docs/ (partial) + generate-llms.mjs # EXISTING - reads interact rules/, generates llms.txt + llms-full.txt +``` + +No `templates/` directory. The output layout for `rules/` files is encoded as renderer functions inside `build-context.js`. + +> **Gitignore strategy:** Commit the generated `rules/` output and have CI verify it matches the source by running `build-context.js` and checking `git diff --exit-code`. This is the same pattern as lockfiles and avoids a pre-publish build step. The `docs/` files are always committed as they are hand-authored. + +--- + +## Glossary YAML Schema + +Each `glossary.yaml` contains entries like: + +```yaml +terms: + - id: trigger-viewEnter + name: viewEnter + category: trigger # trigger | effect-type | config | api | concept | preset + description: 'Fires when element crosses viewport threshold via IntersectionObserver.' + params: + - name: threshold + type: number + default: 0.2 + description: 'Fraction of element that must be visible' + - name: inset + type: string + default: null + description: 'Mapped to IntersectionObserver rootMargin' + caveats: + - "Same source+target: only triggerType 'once' is reliable" + sourceFile: src/types/triggers.ts # used by Vitest audit tests + related: [concept-fouc] +``` + +Presets get a `presets` section (motion-presets only) with the same structure but preset-specific fields (`category: entrance|scroll|ongoing|mouse`, `visual`, `example`, etc.). + +> **Implementation note:** The exact YAML schema should be finalized during the Interact package phase after the full audit confirms which fields are actually needed. The schema above is a starting point. A single `description` field is used — the output format (compact rules vs. narrative docs) provides audience differentiation, not the data layer. + +--- + +## Build Script Behavior + +`scripts/build-context.js` uses two distinct strategies for its two output targets: + +**For `rules/` (fully generated):** + +1. For a given package, reads `context/glossary.yaml` +2. Calls a renderer function for each output file — e.g., `renderPresetCategoryFile(entries)`, `renderTriggerFile(entries)`, `renderOverviewFile(data)` +3. Each renderer function builds a complete markdown string using the **fixed layout already established by the existing rules files** (frontmatter → H1 → intro → TOC → per-item sections) +4. Writes output files to `rules/` + +No template files. No marker syntax. The output structure is encoded in the renderer functions, which are plain string-building code following the same style as `scripts/generate-llms.mjs`. + +**For `docs/` (structured sections injected, prose hand-authored):** + +1. Reads `context/glossary.yaml` +2. Finds HTML comment markers in existing docs files: + ```html + + ...any existing content here is replaced... + + ``` +3. Generates a param table from YAML and replaces the content between markers +4. All prose, examples, and narrative outside the markers is untouched +5. Writes the updated docs files back in place + +**Validation:** The Vitest audit tests from Phase 1.1 serve as the validation layer — they import source modules and assert documented behavior. There is no separate `validate-context.js` script. CI runs `yarn test` as part of normal checks. + +> **Scope constraint:** The build script should be under 200 lines. Renderer functions should follow `generate-llms.mjs` style: plain ESM, zero external dependencies, simple file I/O. + +--- + +## Phase 0: Infrastructure Setup + +Before any package migration, set up the shared tooling. + +### 0.1 Design the YAML glossary schema + +- Draft the schema based on the Interact package audit (from the previous conversation's findings) +- Decide the fields needed for each renderer function: what data does `renderTriggerFile`, `renderPresetCategoryFile`, and `renderOverviewFile` consume? +- Use the existing motion-presets rules files as the reference for what renderer output should look like — the schema should map directly to that structure +- Single `description` field per term (no llm/human split — the output format handles audience differentiation) + +### 0.2 Build the context build script + +- `scripts/build-context.js` -- reads YAML, calls renderer functions for `rules/`, injects structured sections into `docs/` +- Renderer functions per file type (e.g., `renderPresetCategoryFile`, `renderTriggerFile`, `renderOverviewFile`): plain string-building, no template files, no marker parsing +- Docs injection: find `` / `` comment pairs in docs files and replace content between them with a generated param table +- Must support running per-package: `node scripts/build-context.js --package interact` +- Add a `build:context` script to root `package.json` +- Target: under 200 lines, plain ESM, zero external dependencies — same style as `scripts/generate-llms.mjs` + +### 0.3 CI verification + +- Add a CI step that runs `build-context.js` and then `git diff --exit-code` to verify `rules/` output is up to date +- The Vitest audit tests from Phase 1.1 serve as the data validation layer — no separate validation script is needed + +--- + +## Phase 1: Interact Package (`@wix/interact`) + +### 1.1 Audit and verify ground truth + +Before writing any glossary entries, verify every claim in the current rules against the actual code. This is critical because the previous analysis found **8 critical discrepancies** and **10 significant ones**. + +**Verification approach:** Write ad-hoc Vitest tests (in a temporary test file, e.g., `packages/interact/test/context-audit.spec.ts`) that import source modules and assert the documented behavior. These tests serve as one-time verification and can be kept as regression tests afterward. + +Items to verify (from discrepancy list): + +| # | What to verify | How | +| --- | -------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | `allowA11yTriggers` default is `true` | Check `Interact` class static field and handler mapping in [handlers/index.ts](packages/interact/src/handlers/index.ts) | +| 2 | `namedEffect` requires object `{ type: '...' }`, not bare string | Test that `getRegisteredEffect` resolves `{ type: 'FadeIn' }` but not `'FadeIn'` | +| 3 | 6 primary user-facing triggers + 2 a11y variants + 1 deprecated in `TriggerType` union | Import and enumerate from [types/triggers.ts](packages/interact/src/types/triggers.ts); confirm `interest`/`activate` only activate via `allowA11yTriggers` | +| 4 | `pageVisible` deprecated: verify handler + confirm no new usage should be added | Check handler mapping in [handlers/index.ts](packages/interact/src/handlers/index.ts); note removal timeline if known | +| 5 | `AnimationEndParams.effectId` is unused at runtime | Read [handlers/animationEnd.ts](packages/interact/src/handlers/animationEnd.ts) and verify the param is ignored | +| 6 | `viewProgress` handler ignores `ViewEnterParams` | Read [handlers/viewProgress.ts](packages/interact/src/handlers/viewProgress.ts) | +| 7 | `triggerType` defaults: `'once'` for viewEnter, `'alternate'` for event triggers | Check [core/resolvers.ts](packages/interact/src/core/resolvers.ts) and handler code | +| 8 | `stateAction` default is `'toggle'` | Check [handlers/effectHandlers.ts](packages/interact/src/handlers/effectHandlers.ts) `createTransitionHandler` | +| 9 | `Condition.type` accepts only `'media' | 'container' | +| 10 | Sticky/tall-wrapper ViewTimeline: which element is the source | Read [handlers/viewProgress.ts](packages/interact/src/handlers/viewProgress.ts) to determine actual behavior | +| 11 | Element resolution order (key cascade) | Read [core/Interact.ts](packages/interact/src/core/Interact.ts) `parseConfig` and [core/add.ts](packages/interact/src/core/add.ts) | +| 12 | `generate()` signature and return type | Import from [core/css.ts](packages/interact/src/core/css.ts) | + +### 1.2 Create the Interact glossary + +Based on verified ground truth, populate `packages/interact/context/glossary.yaml` with entries for: + +**Categories and approximate entry counts:** + +- **Triggers** (8 active entries): hover, click, viewEnter, viewProgress, pointerMove, animationEnd (6 primary), activate, interest (2 a11y variants) — each with params, defaults, caveats. `pageVisible` gets one entry marked as deprecated with a pointer to `viewEnter`. +- **Effect types** (3 entries): TimeEffect, ScrubEffect, StateEffect -- each with all typed fields and defaults +- **Config types** (5-6 entries): InteractConfig, Interaction, Effect/EffectRef, SequenceConfig, Condition -- schema shapes +- **API** (8-10 entries): Interact.create, Interact.destroy, Interact.setup, add, remove, generate, Interact.registerEffects, Interact.getSequence, etc. -- signatures and behavior +- **Concepts** (5-6 entries): FOUC prevention, element resolution, a11y trigger mapping, conditions cascading, custom elements lifecycle +- **Enums/unions** (4-5 entries): triggerType (once/repeat/alternate/state), stateAction, Fill, CompositeOperation + +### 1.3 Design the Interact rules output structure + +The current rules have two overlapping "hub" files (`full-lean.md` at ~700 lines, `integration.md` at ~334 lines) plus 5 trigger-specific files. The new structure should eliminate the overlap. All files are **fully generated** from `glossary.yaml` via renderer functions. + +**Proposed rules/ file set for Interact:** + +| File | Purpose | Approx. lines | +| ------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- | +| `overview.md` | Package purpose, entry points, imports, quick-start snippet | 60-80 | +| `config.md` | InteractConfig schema, Interaction shape, Effect/EffectRef, sequences, conditions | 150-200 | +| `triggers.md` | 6 primary triggers (full params/defaults/caveats) + a11y variants section (activate/interest, when enabled by `allowA11yTriggers`) + deprecated note for `pageVisible` | 200-250 | +| `effects.md` | TimeEffect, ScrubEffect, StateEffect: fields, defaults, triggerType/stateAction semantics | 150-200 | +| `pitfalls.md` | FOUC, overflow:clip, same-element source+target, hit-area jitter, a11y mapping | 80-100 | + +Each file gets: + +- YAML frontmatter (`name`, `description`) for agent-loading hints (matches existing presets pattern) +- A `## Table of Contents` with `#anchor` links for selective section reading +- `{{term:...}}` markers where glossary data should be injected + +**Key structural rule for LLM readability:** + +- Each file must be self-contained for its topic (no "see other file for the rest of this table") +- Cross-references between files use relative links but only for "related reading", never for completing a thought +- Param tables are compact: `| name | type | default | notes |` -- one row per param, no verbose descriptions +- Code examples are minimal (3-8 lines) and correct + +### 1.4 Design the Interact docs structure + +The current docs have 26 files but many are scaffolding (broken links, TBD sections, placeholder READMEs linking to nonexistent files). The new structure should contain only files with actual content. + +**Docs are hand-authored.** The build script only injects structured sections (param tables, type listings) between `` / `` markers. All prose, examples, and explanatory content is written directly in the docs files. + +**Proposed docs/ file set for Interact:** + +| File | Purpose | +| --------------------------- | ------------------------------------------------------- | +| `README.md` | Getting started, install, entry points, navigation | +| `guides/configuration.md` | Config structure explained for humans | +| `guides/triggers.md` | Trigger concepts, choosing triggers, combining triggers | +| `guides/effects.md` | Effect types explained, when to use which | +| `guides/sequences.md` | Sequence math, staggering, list integration | +| `guides/conditions.md` | Media queries, container queries, selector conditions | +| `guides/fouc.md` | FOUC prevention guide (generate + initial) | +| `guides/custom-elements.md` | `` usage and lifecycle | +| `api/README.md` | API overview and imports | +| `api/interact-class.md` | Static + instance methods | +| `api/functions.md` | add, remove, generate | +| `api/types.md` | Type reference | +| `integration/react.md` | React-specific guide | +| `examples/entrance.md` | Entrance animation recipes | +| `examples/hover-click.md` | Hover and click interaction recipes | + +All other current files (broken-link READMEs, TBD placeholders, the nonexistent targets) are **dropped**. Content from `full-lean.md` that overlaps with docs is reconciled — the rules version (generated from YAML) becomes the SSOT for correctness; the docs version is prose written for humans, with param tables injected from the same YAML. + +### 1.5 Build and verify + +1. Run `node scripts/build-context.js --package interact` to generate `rules/` and inject structured sections into `docs/` +2. Run `yarn workspace @wix/interact test` — the Vitest audit tests from 1.1 are the validation layer +3. Manually review generated rules for LLM readability: + +- Are tables compact and scannable? +- Can an LLM read just `triggers.md` and get everything it needs about triggers? +- Is the TOC + anchor pattern working for selective section reading? + +4. Run `node scripts/generate-llms.mjs` to verify `llms.txt` still generates correctly from the new rules structure. Update `STATIC_BODY` in `generate-llms.mjs` to reflect the correct trigger count (six primary trigger types) as part of this phase. +5. Run the existing `apps/docs` build to verify docs still copy correctly + +### 1.6 Replace and verify + +1. Back up current `rules/` and `docs/` (they are in git, so this is just a safety step) +2. Replace `rules/` with generated output; update `docs/` files with injected structured sections +3. Run existing Vitest tests (`yarn workspace @wix/interact test`) to ensure nothing depends on specific rules/docs file paths internally +4. Verify `apps/docs` build still works (it copies from `packages/interact/docs`) +5. Verify the docs app `copy-docs.js` script handles the new file structure +6. Keep the `context-audit.spec.ts` tests as ongoing regression + +--- + +## Phase 2: Motion Package (`@wix/motion`) + +### 2.1 Audit and verify ground truth + +Motion currently has **no rules/** directory. Its docs have significant issues: `getCSSAnimation` return type is wrong in multiple files, `TriggerVariant` shape differs between tutorial and type docs, preset counts don't match, and several linked files don't exist. + +**Key items to verify:** + +| # | What to verify | +| --- | -------------------------------------------------------------------------- | +| 1 | `getCSSAnimation` return type (array of objects, not string) | +| 2 | `AnimationGroup` is type-only export (not constructable by consumers) | +| 3 | `getWebAnimation` signature and return type union | +| 4 | `getScrubScene` signature, scroll vs pointer branches | +| 5 | `TriggerVariant` actual shape (`id`, `trigger`, `componentId`, `element?`) | +| 6 | `Sequence` stagger formula | +| 7 | `RangeOffset` range name enum | +| 8 | `prepareAnimation` behavior and `DomApi` contract | +| 9 | `CustomAnimation` rAF loop behavior for `customEffect` | +| 10 | ViewTimeline detection and fallback path | + +### 2.2 Create glossary, templates, build, replace + +Same pattern as Interact: + +- `packages/motion/context/glossary.yaml` -- API functions, animation types, scroll/pointer concepts, `AnimationGroup`/`Sequence` APIs +- **New `rules/` directory** (Motion currently lacks one): fully generated from glossary via renderer functions — `overview.md`, `api.md`, `animation-types.md`, `pitfalls.md` +- Restructured `docs/` -- hand-authored prose; param tables and API signatures injected from glossary. Drop `PLAN_DOCS.md`, fix or remove broken links, consolidate category docs that overlap with motion-presets docs + +**Important:** Motion's `docs/categories/` files (entrance-animations.md, scroll-animations.md, etc.) overlap heavily with motion-presets docs. These should be **removed or reduced to pointers** -- the preset details belong in motion-presets. Motion docs should cover the **engine API**, not preset catalogs. + +--- + +## Phase 3: Motion-Presets Package (`@wix/motion-presets`) + +### 3.1 Audit and verify ground truth + +Motion-presets rules are the most structured of the three (YAML frontmatter, per-category files), but have known data errors. The target scope for this phase is the **62 production-facing presets** (19 entrance + 19 scroll + 13 ongoing + 11 mouse). Background-scroll (12) and CustomMouse (1) are intentionally excluded from rules and docs. + +**Key items to verify:** + +| # | What to verify | +| --- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1 | All 62 production-facing preset names match rules and docs (barrel exports 75; bg-scroll 12 and CustomMouse 1 are intentionally excluded) | +| 2 | Per-preset params and defaults match implementation (ParallaxScroll `parallaxFactor` not `speed`, ArcIn default `'right'` already fixed in rules, Pulse intensity default `0` not `1.0`) | +| 3 | **RESOLVED** Angle convention is 0 = right — confirmed in `presets-main.md`. Verify against individual preset source files. | +| 4 | DVD is NOT exported from barrel | +| 5 | `range` param on ParallaxScroll is typed but unused | +| 6 | `TurnScroll.rotation` is typed but unused | +| 7 | `docs/presets/README.md` still says "82+" total presets — update to 62 production presets across 4 categories | + +**Verification approach:** Vitest test file that: + +- Imports the 62 production-facing presets from `@wix/motion-presets` barrel (excluding CustomMouse and bg-scroll) +- For each, checks it matches the glossary entry (name, category) +- For param defaults, asserts against the source destructuring patterns + +### 3.2 Create glossary, renderer functions, build, replace + +- `packages/motion-presets/context/glossary.yaml` -- includes a `presets` section with every production-facing preset's params and defaults +- **Rules:** Keep the current split-by-category approach (it already works well). Fully generate from glossary using renderer functions: `presets-main.md`, `entrance-presets.md`, `scroll-presets.md`, `ongoing-presets.md`, `mouse-presets.md`. No background-scroll rules file. +- **Docs:** Per-preset pages only for presets that warrant detailed explanation (not all 62 need a dedicated page). Category READMEs updated with param tables injected from glossary. Drop broken-link placeholder pages. `docs/presets/README.md` count updated to 62 across 4 categories. + +### 3.3 Cross-package validation + +After all three packages are migrated: + +- Verify cross-package references (interact rules referencing motion concepts, presets referencing motion API) +- Ensure `registerEffects` is described consistently: defined in motion glossary, referenced in interact and presets +- Ensure shared concepts (scroll ranges, pointer progress, namedEffect shape) use the same terminology everywhere + +--- + +## Sequencing and Safety + +```mermaid +flowchart TD + P0[Phase 0: Build tooling] --> P1A[1.1: Audit Interact ground truth] + P1A --> P1B[1.2: Create Interact glossary] + P1B --> P1C[1.3-1.4: Create templates] + P1C --> P1D[1.5: Build and validate] + P1D --> P1E{Output matches expectations?} + P1E -->|No| P1C + P1E -->|Yes| P1F[1.6: Replace Interact rules+docs] + P1F --> P2A[2.1: Audit Motion ground truth] + P2A --> P2B[2.2: Create Motion glossary + renderer functions + replace] + P2B --> P3A[3.1: Audit Presets ground truth] + P3A --> P3B[3.2: Create Presets glossary + renderer functions + replace] + P3B --> P3C[3.3: Cross-package validation] +``` + +**Safety rule:** The old `rules/` and `docs/` files for a package are only deleted/replaced after: + +1. The Vitest audit tests pass (these are the validation layer) +2. The build script produces output that passes manual review +3. Existing tests still pass +4. The docs app build still works (for interact) + +Each phase is a separate PR (or set of PRs) that can be reviewed independently. diff --git a/packages/interact/context/glossary.yaml b/packages/interact/context/glossary.yaml new file mode 100644 index 00000000..1eb94e9c --- /dev/null +++ b/packages/interact/context/glossary.yaml @@ -0,0 +1,838 @@ +# packages/interact/context/glossary.yaml +# +# Single source of truth for @wix/interact parameter tables, type shapes, +# defaults, and term definitions. +# +# Consumed by scripts/build-context.js to: +# - Fully generate rules/ output files via renderer functions +# - Inject structured param tables into hand-authored docs/ files +# between / markers +# +# Schema +# ------ +# Each term has: +# id - unique slug used in renderer lookups and PARAMS markers +# name - display name (code identifier or phrase) +# category - trigger | effect-type | config | api | concept | enum +# description - single authoritative description (prose) +# params - list of { name, type, default, required, description } +# values - list of { value, description } (enum terms only) +# caveats - list of string warnings / gotchas +# sourceFile - path relative to packages/interact/ (for audit references) +# related - list of other term IDs for cross-references +# +# Verified against source on 2026-05-31 (see audit in context_ssot_restructure plan). + +package: '@wix/interact' + +terms: + # ─── Triggers ──────────────────────────────────────────────────────────────── + + - id: trigger-hover + name: hover + category: trigger + description: >- + Fires on mouseenter / mouseleave. When allowA11yTriggers is true (default), + the library transparently remaps hover to the interest handler, which adds + focusin / focusout alongside the mouse events for keyboard accessibility. + params: [] + effect_note: >- + No trigger-level params. Control animation behavior via triggerType on + TimeEffect (default: 'alternate') or stateAction on StateEffect (default: 'toggle'). + default_triggerType: alternate + caveats: + - "Use triggerType: 'alternate' (default) for enter/leave animation pairs." + - 'Do not mix TimeEffect.triggerType with StateEffect.stateAction on the same effect.' + - 'When allowA11yTriggers is true (default), hover transparently maps to the interest handler.' + sourceFile: src/handlers/eventTrigger.ts + related: [trigger-interest, concept-a11y-mapping] + + - id: trigger-click + name: click + category: trigger + description: >- + Fires on click. When allowA11yTriggers is true (default), the library + transparently remaps click to the activate handler, which adds keydown + Enter/Space handling for keyboard accessibility. + params: [] + effect_note: >- + No trigger-level params. Control animation behavior via triggerType on + TimeEffect (default: 'alternate') or stateAction on StateEffect (default: 'toggle'). + default_triggerType: alternate + caveats: + - "Use triggerType: 'alternate' (default) for toggle-on / toggle-off behavior." + - 'When allowA11yTriggers is true (default), click transparently maps to the activate handler.' + sourceFile: src/handlers/eventTrigger.ts + related: [trigger-activate, concept-a11y-mapping] + + - id: trigger-viewEnter + name: viewEnter + category: trigger + description: >- + Fires when the element crosses the viewport threshold via IntersectionObserver. + Primary trigger for entrance animations. Supports FOUC prevention via + generate() CSS and the initial flag. + params: + - name: threshold + type: number + default: '0.2' + required: false + description: 'Fraction of the element visible in viewport to trigger (0–1).' + - name: inset + type: string + default: 'null' + required: false + description: 'Maps to IntersectionObserver rootMargin. Shrinks or expands the detection zone.' + - name: useSafeViewEnter + type: boolean + default: 'false' + required: false + description: 'Internal fallback for environments where IntersectionObserver timing is unreliable.' + default_triggerType: once + caveats: + - "When source and target are the same element, only triggerType: 'once' is reliable. Other triggerType values require separate source and target elements." + - 'FOUC prevention requires both generate(config) CSS injected into AND the initial flag on each entrance-animated element.' + - 'pageVisible is a deprecated alias that shares this IntersectionObserver handler.' + sourceFile: src/handlers/viewEnter.ts + related: [concept-fouc, trigger-pageVisible] + + - id: trigger-viewProgress + name: viewProgress + category: trigger + description: >- + Scroll-driven animation. Uses native ViewTimeline when available; falls back to + fizban Scroll + getScrubScene. Animation progress is driven by the element's + scroll position relative to the viewport. Configure the scrub range via + rangeStart/rangeEnd on the ScrubEffect — not on params. + params: [] + effect_note: >- + Trigger params (ViewEnterParams) are accepted in the TypeScript type but ignored + at runtime — the handler uses __ as the param name. Scrub range is configured + via rangeStart/rangeEnd on the ScrubEffect. + default_triggerType: null + caveats: + - 'Trigger-level params (ViewEnterParams shape) are ignored at runtime. Use ScrubEffect.rangeStart / rangeEnd instead.' + - 'For sticky/tall-wrapper patterns, the source element (the key element) is the ViewTimeline source — not the sticky child.' + - 'Respects reducedMotion: skips the animation entirely when reducedMotion is true.' + - 'Falls back to fizban Scroll + getScrubScene when window.ViewTimeline is unavailable.' + sourceFile: src/handlers/viewProgress.ts + related: [effect-scrub] + + - id: trigger-pointerMove + name: pointerMove + category: trigger + description: >- + Drives animation progress from mouse / pointer position using getScrubScene. + The pointer position along the chosen axis is mapped to a 0–1 progress value. + params: + - name: hitArea + type: "'self' | 'root'" + default: 'undefined (document body)' + required: false + description: "Element that captures pointer events. 'self' = source element, 'root' = document root. Omit to use document body." + - name: axis + type: "'x' | 'y'" + default: "'y'" + required: false + description: 'Pointer axis that drives animation progress.' + default_triggerType: null + caveats: + - 'hitArea omitted means document body is the pointer capture area.' + - "Do not use hitArea: 'self' with effects that change the element's size or position — the transform shifts the hit area, causing jitter." + sourceFile: src/handlers/pointerMove.ts + related: [effect-scrub] + + - id: trigger-animationEnd + name: animationEnd + category: trigger + description: >- + Chains one animation after another. Listens for animationend on the source + element and plays the effect on the target when it fires. + params: + - name: effectId + type: string + default: '—' + required: true + description: 'ID of the preceding CSS animation effect to listen for. Used to filter which animationend event triggers the chain.' + default_triggerType: once + caveats: + - 'effectId filters the animationend event — it is not the primary trigger mechanism.' + - 'The source element must be running a CSS animation for animationend to fire.' + sourceFile: src/handlers/animationEnd.ts + related: [] + + - id: trigger-activate + name: activate + category: trigger + description: >- + Accessible variant of click. Handles click + keydown Enter/Space. + When allowA11yTriggers is true (default), the library automatically uses this + handler when trigger is 'click' — users do not configure activate directly. + params: [] + effect_note: 'Same as click. Control via triggerType on TimeEffect or stateAction on StateEffect.' + default_triggerType: alternate + caveats: + - "Automatically activated when allowA11yTriggers: true and trigger: 'click'. Users configure 'click', not 'activate'." + - "Can be used directly as trigger: 'activate' when fine-grained control is needed." + sourceFile: src/handlers/eventTrigger.ts + related: [trigger-click, concept-a11y-mapping] + + - id: trigger-interest + name: interest + category: trigger + description: >- + Accessible variant of hover. Handles mouseenter + focusin (enter) and + mouseleave + focusout (leave). When allowA11yTriggers is true (default), + the library automatically uses this handler when trigger is 'hover'. + params: [] + effect_note: 'Same as hover. Control via triggerType on TimeEffect or stateAction on StateEffect.' + default_triggerType: alternate + caveats: + - "Automatically activated when allowA11yTriggers: true and trigger: 'hover'. Users configure 'hover', not 'interest'." + - "Can be used directly as trigger: 'interest' when fine-grained control is needed." + sourceFile: src/handlers/eventTrigger.ts + related: [trigger-hover, concept-a11y-mapping] + + - id: trigger-pageVisible + name: pageVisible + category: trigger + deprecated: true + description: >- + DEPRECATED. Shares the viewEnter IntersectionObserver handler. + Will be removed in a future version. Use trigger: 'viewEnter' instead. + params: [] + default_triggerType: once + caveats: + - "Deprecated. Use trigger: 'viewEnter' as the drop-in replacement." + sourceFile: src/handlers/viewEnter.ts + related: [trigger-viewEnter] + + # ─── Effect Types ───────────────────────────────────────────────────────────── + + - id: effect-time + name: TimeEffect + category: effect-type + description: >- + Duration-based animation. Plays a keyframe, named, or custom animation over + a fixed time. Used with hover, click, viewEnter, animationEnd, and their + a11y variants. + params: + - name: duration + type: number + default: '—' + required: true + description: 'Animation duration in milliseconds.' + - name: easing + type: string + default: 'null' + required: false + description: "CSS or named easing string (e.g. 'ease-out', 'cubicOut')." + - name: delay + type: number + default: '0' + required: false + description: 'Delay before animation starts in ms.' + - name: iterations + type: number + default: '1' + required: false + description: 'Number of times to repeat. Use Infinity for a continuous loop.' + - name: alternate + type: boolean + default: 'false' + required: false + description: 'Alternate animation direction on each iteration.' + - name: fill + type: "'none' | 'forwards' | 'backwards' | 'both'" + default: "'forwards'" + required: false + description: 'CSS animation fill mode.' + - name: reversed + type: boolean + default: 'false' + required: false + description: 'Play the animation in reverse.' + - name: triggerType + type: "'once' | 'repeat' | 'alternate' | 'state'" + default: 'trigger-dependent' + required: false + description: 'Controls animation response to repeated trigger events. Default varies by trigger type.' + - name: composite + type: CompositeOperation + default: 'null' + required: false + description: 'WAAPI composite operation for layering animations.' + effect_property_note: >- + Requires exactly one animation payload: namedEffect: { type: string, ...params }, + keyframeEffect: { name: string, keyframes: Keyframe[] }, or + customEffect: (element: Element, progress: any) => void. + caveats: + - "triggerType default is 'once' for viewEnter / animationEnd; 'alternate' for hover / click / activate / interest." + - "Do not set triggerType: 'state' alongside stateAction — those belong to StateEffect, not TimeEffect." + sourceFile: src/types/effects.ts + related: [enum-triggerType] + + - id: effect-scrub + name: ScrubEffect + category: effect-type + description: >- + Scroll- or pointer-driven animation. Progress is controlled externally by + scroll position (viewProgress) or pointer movement (pointerMove) rather than + playing over fixed time. + params: + - name: rangeStart + type: RangeOffset + default: 'null' + required: false + description: "Start of the ViewTimeline scroll range. Shape: { name: RangeOffsetName, offset: CSSPercentValue }. RangeOffset names: 'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing'." + - name: rangeEnd + type: RangeOffset + default: 'null' + required: false + description: 'End of the ViewTimeline scroll range. Same shape as rangeStart.' + - name: centeredToTarget + type: boolean + default: 'false' + required: false + description: 'Center the timeline scroll range on the target element rather than the source.' + - name: easing + type: string + default: 'null' + required: false + description: 'CSS easing string for the scrub animation.' + - name: iterations + type: number + default: '1' + required: false + description: 'Number of animation iterations.' + - name: alternate + type: boolean + default: 'false' + required: false + description: 'Alternate direction on each iteration.' + - name: fill + type: "'none' | 'forwards' | 'backwards' | 'both'" + default: "'both'" + required: false + description: 'CSS animation fill mode.' + - name: reversed + type: boolean + default: 'false' + required: false + description: 'Reverse the animation direction.' + - name: transitionDuration + type: number + default: 'null' + required: false + description: 'Duration (ms) of ease-in / ease-out transition at scrub endpoints.' + - name: transitionDelay + type: number + default: 'null' + required: false + description: 'Delay (ms) for the scrub endpoint transition.' + - name: transitionEasing + type: ScrubTransitionEasing + default: 'null' + required: false + description: 'Easing for the scrub endpoint transition.' + - name: composite + type: CompositeOperation + default: 'null' + required: false + description: 'WAAPI composite operation.' + effect_property_note: 'Requires exactly one of: namedEffect, keyframeEffect, or customEffect.' + caveats: + - "Native ViewTimeline path: uses duration: 'auto'. Fallback path: duration 99.99 with manual scrub." + - 'rangeStart / rangeEnd configure the scroll range — do not put these in trigger params.' + sourceFile: src/types/effects.ts + related: [trigger-viewProgress, trigger-pointerMove] + + - id: effect-state + name: StateEffect + category: effect-type + description: >- + CSS class / state toggle animation. Adds, removes, or toggles a CSS class or + custom state on the target element. Used with hover, click, and their a11y + variants. Timing is controlled by CSS transitions, not by duration. + params: + - name: stateAction + type: "'add' | 'remove' | 'toggle' | 'clear'" + default: "'toggle'" + required: false + description: 'Class manipulation action on trigger activation.' + - name: effectId + type: string + default: 'null' + required: false + description: 'ID for this state effect — referenced by animationEnd trigger chains.' + - name: key + type: string + default: 'null' + required: false + description: 'Override target element key.' + - name: transition + type: 'TransitionOptions & { styleProperties: StyleProperty[] }' + default: 'null' + required: false + description: 'CSS transition spec including target style properties. Mutually exclusive with transitionProperties.' + - name: transitionProperties + type: 'TransitionProperty[]' + default: 'null' + required: false + description: 'Per-property transition overrides. Mutually exclusive with transition.' + caveats: + - 'Use transition OR transitionProperties — not both on the same StateEffect.' + - "stateAction: 'clear' removes all tracked states from the element." + - 'Does not require duration — CSS transition declarations control timing.' + sourceFile: src/types/effects.ts + related: [enum-stateAction] + + # ─── Config Types ───────────────────────────────────────────────────────────── + + - id: config-InteractConfig + name: InteractConfig + category: config + description: >- + Root configuration object passed to Interact.create(). Defines all + trigger-effect bindings and shared named resources. JSON-serializable + by design. + params: + - name: interactions + type: 'Interaction[]' + default: '—' + required: true + description: 'Array of interaction definitions. Each entry binds a trigger to one or more effects.' + - name: effects + type: 'Record' + default: '{}' + required: false + description: 'Reusable effect registry. Effects referenced by effectId from interactions.' + - name: sequences + type: 'Record' + default: '{}' + required: false + description: 'Reusable sequence definitions. Referenced by sequenceId from interactions.' + - name: conditions + type: 'Record' + default: '{}' + required: false + description: 'Named conditions (media / container / selector). Referenced by ID from interactions or effects.' + caveats: + - 'JSON-serializable — no functions at the config root level (customEffect goes in the effect payload, not at config level).' + - 'Each Interact.create(config) call creates a separate independent instance.' + sourceFile: src/types/config.ts + related: [config-Interaction] + + - id: config-Interaction + name: Interaction + category: config + description: >- + Binds a trigger to one or more effects or sequences on a specific element. + The key field matches the element's data-interact-key (web) or interactKey (React). + params: + - name: key + type: string + default: '—' + required: true + description: 'Matches data-interact-key (web) or interactKey (React). Identifies the source element.' + - name: trigger + type: TriggerType + default: '—' + required: true + description: 'Trigger type: hover | click | viewEnter | viewProgress | pointerMove | animationEnd | activate | interest.' + - name: params + type: TriggerParams + default: 'null' + required: false + description: 'Trigger-specific params. See individual trigger entries for available fields.' + - name: effects + type: 'Effect[]' + default: 'null' + required: false + description: 'Effects to play when triggered. At least one of effects or sequences is required.' + - name: sequences + type: '(SequenceConfig | SequenceConfigRef)[]' + default: 'null' + required: false + description: 'Sequences to play when triggered. At least one of effects or sequences is required.' + - name: selector + type: string + default: 'null' + required: false + description: 'CSS selector to refine source / target within the keyed element.' + - name: listContainer + type: string + default: 'null' + required: false + description: 'CSS selector for a list container. Trigger attaches to each direct child.' + - name: listItemSelector + type: string + default: 'null' + required: false + description: 'Filter which children of listContainer participate. Omit to select all direct children.' + - name: conditions + type: 'string[]' + default: 'null' + required: false + description: 'IDs of conditions that must all pass for this interaction to activate.' + caveats: + - 'At least one of effects or sequences must be provided.' + - 'Multiple entries in effects all fire simultaneously when the trigger activates.' + sourceFile: src/types/config.ts + related: [config-InteractConfig, concept-element-resolution] + + - id: config-SequenceConfig + name: SequenceConfig + category: config + description: >- + Defines a staggered sequence of effects with coordinated timing offsets. + Used inline in Interaction.sequences or as a reusable entry in + InteractConfig.sequences referenced by sequenceId. + params: + - name: effects + type: '(Effect | EffectRef)[]' + default: '—' + required: true + description: 'Ordered list of effects in the sequence.' + - name: offset + type: number + default: '0' + required: false + description: 'Milliseconds between consecutive items in the stagger.' + - name: offsetEasing + type: 'string | ((p: number) => number)' + default: "'linear'" + required: false + description: 'Easing curve for the stagger distribution. CSS easing string or JS function.' + - name: delay + type: number + default: '0' + required: false + description: 'Base delay in ms before the entire sequence starts.' + - name: triggerType + type: "'once' | 'repeat' | 'alternate' | 'state'" + default: 'trigger-dependent' + required: false + description: 'Controls playback behavior on repeated triggers. Same semantics as TimeEffect.triggerType.' + - name: sequenceId + type: string + default: 'null' + required: false + description: 'When defined in InteractConfig.sequences, sets the ID for referencing via SequenceConfigRef.' + - name: conditions + type: 'string[]' + default: 'null' + required: false + description: 'Condition IDs that must all pass for this sequence to activate.' + caveats: + - 'Stagger formula: offset[i] = easing(i / last) * last * offsetMs (integer-truncated).' + - 'offsetEasing controls spacing between items; delay shifts the whole sequence.' + sourceFile: src/types/config.ts + related: [] + + - id: config-Condition + name: Condition + category: config + description: >- + Named condition that gates whether an interaction or effect is active. + Defined in InteractConfig.conditions and referenced by string ID. + params: + - name: type + type: "'media' | 'container' | 'selector'" + default: '—' + required: true + description: 'Condition category: media = CSS @media query, container = CSS container query, selector = CSS selector match on the element.' + - name: predicate + type: string + default: 'null' + required: false + description: "The query or selector string. E.g. '(min-width: 768px)' for media." + caveats: + - "Only 'media', 'container', and 'selector' are valid types — there is no 'custom' condition type." + - 'Multiple conditions on an interaction are AND-ed: all must pass.' + sourceFile: src/types/config.ts + related: [] + + # ─── API ────────────────────────────────────────────────────────────────────── + + - id: api-create + name: 'Interact.create' + category: api + description: >- + Static factory method. Creates a new Interact instance, initializes it with + the provided config, and returns the instance. Call after DOM is ready. + signature: 'static create(config: InteractConfig): Interact' + caveats: + - 'Each call creates an independent instance. Multiple configs are independent.' + - 'In React, wrap in useEffect to prevent SSR execution.' + sourceFile: src/core/Interact.ts + related: [api-destroy] + + - id: api-destroy + name: 'Interact.destroy / instance.destroy' + category: api + description: >- + Tears down all interactions and frees listeners. Static form destroys all + instances; instance form destroys only that instance. + signature: 'static destroy(): void | instance.destroy(): void' + caveats: + - 'In React, call instance.destroy() in the useEffect cleanup function.' + - 'Static Interact.destroy() clears all active instances at once.' + sourceFile: src/core/Interact.ts + related: [api-create] + + - id: api-setup + name: 'Interact.setup' + category: api + description: >- + Configure global defaults for scroll, pointer, and viewEnter trigger behavior. + Must be called before Interact.create for settings to take effect. + signature: 'static setup(options: Partial): void' + caveats: + - 'Must be called before Interact.create.' + - 'Accepts scrollOptionsGetter for configuring fizban Scroll internals (viewProgress).' + sourceFile: src/core/Interact.ts + related: [] + + - id: api-registerEffects + name: 'Interact.registerEffects' + category: api + description: >- + Register named effect presets from @wix/motion-presets into the motion + registry. Required before using namedEffect in any config. Delegates to + motion's registerEffects internally. + signature: 'static registerEffects(effects: Record): void' + caveats: + - 'Must be called before Interact.create.' + - "Presets are keyed by namedEffect.type string. namedEffect must be an object: { type: 'FadeIn' }, not a bare string." + sourceFile: src/core/Interact.ts + related: [] + + - id: api-add + name: 'instance.add' + category: api + description: >- + Register a DOM element with a key after Interact.create. Used in vanilla JS + integration to manually associate elements with interaction keys. + signature: 'add(element: HTMLElement, key: string): void' + caveats: + - 'Call only after the element exists in the DOM.' + - 'Vanilla JS only — web/React use declarative element wrapping.' + sourceFile: src/core/Interact.ts + related: [api-remove] + + - id: api-remove + name: 'instance.remove' + category: api + description: >- + Unregister all interactions for a given key, removing event listeners and + cleaning up animation state. + signature: 'remove(key: string): void' + sourceFile: src/core/Interact.ts + related: [api-add] + + - id: api-generate + name: generate + category: api + description: >- + Generates a complete CSS string for all interactions in a config — including + @keyframes, animation / transition custom properties, view-timeline declarations, + state-selector rules, coordinated-list aggregation, and FOUC-prevention initial + rules. Call server-side or at build time and inject the result into . + signature: 'generate(config: InteractConfig, useFirstChild?: boolean): string' + caveats: + - 'Returns a CSS string — inject into or beginning of .' + - 'useFirstChild: true when using web (custom element) integration — adds :first-child selectors.' + - 'FOUC prevention requires both generate() CSS AND the initial flag on each element.' + - 'Processes all interactions, not only viewEnter.' + sourceFile: src/core/css.ts + related: [concept-fouc] + + - id: api-allowA11yTriggers + name: 'Interact.allowA11yTriggers' + category: api + description: >- + Static boolean flag. When true, hover interactions transparently use the + interest handler (adds focusin / focusout) and click interactions use the + activate handler (adds keydown Enter/Space). + signature: 'static allowA11yTriggers: boolean' + default: 'true' + caveats: + - 'Default is true. Set to false only to explicitly opt out of a11y trigger remapping.' + - 'Affects all instances. Must be set before Interact.create.' + sourceFile: src/core/Interact.ts + related: [concept-a11y-mapping] + + - id: api-forceReducedMotion + name: 'Interact.forceReducedMotion' + category: api + description: >- + Static boolean flag. When true, forces reduced-motion behavior for all + interactions regardless of the OS prefers-reduced-motion setting. + signature: 'static forceReducedMotion: boolean' + default: 'false' + caveats: + - 'Does not affect CSS animations generated by generate() — handle prefers-reduced-motion in CSS.' + sourceFile: src/core/Interact.ts + related: [] + + # ─── Concepts ──────────────────────────────────────────────────────────────── + + - id: concept-fouc + name: 'FOUC Prevention' + category: concept + description: >- + Flash of Un-animated Content (FOUC) occurs when entrance-animated elements + are briefly visible in their final state before the animation sets the initial + keyframe. Prevention requires two things that must both be present: (1) the + CSS produced by generate(config) injected into , and (2) the initial + flag set on each entrance-animated element. + fouc_flags: + - integration: web + element_flag: "data-interact-initial='true' on " + - integration: react + element_flag: 'initial={true} on ' + - integration: vanilla + element_flag: "data-interact-initial='true' on the keyed element" + caveats: + - 'Both generate() CSS and the initial flag are required — neither alone prevents FOUC.' + - "initial is only valid for viewEnter + triggerType: 'once' where source and target are the same element." + - 'generate() processes all interactions, not just viewEnter.' + - 'generate() can be called client-side if the page is initially hidden (e.g. behind a loader).' + sourceFile: src/core/css.ts + related: [trigger-viewEnter, api-generate] + + - id: concept-element-resolution + name: 'Element Resolution Order' + category: concept + description: >- + How Interact resolves which DOM element is the source (trigger host) and + target (animation host) for each interaction. + source_priority_order: + - 'listContainer + listItemSelector — elements matching listItemSelector within the container' + - 'listContainer only — all direct children of the container' + - 'listContainer + selector — querySelector within each direct child' + - 'selector only — querySelectorAll within the root element' + - 'Fallback — firstElementChild of (web) or root element (react / vanilla)' + target_priority_order: + - 'Effect.key — root element with matching data-interact-key' + - 'EffectRef key — key from the referenced registry entry' + - 'Fallback to Interaction.key — source root acts as target root' + - 'After resolving root: Effect.selector / listContainer / listItemSelector further refine the animated element' + caveats: + - 'listItemSelector is optional — omit to select all direct children of listContainer.' + - 'Target resolution runs the same priority logic as source, starting from Effect-level fields.' + sourceFile: src/core/add.ts + related: [config-Interaction] + + - id: concept-a11y-mapping + name: 'Accessibility Trigger Mapping' + category: concept + description: >- + When Interact.allowA11yTriggers is true (default), hover and click are + transparently mapped to richer handlers. Users always configure hover/click + in the config; the library handles the a11y remapping automatically. + mappings: + - user_trigger: hover + a11y_handler: 'interest — mouseenter + focusin (enter) / mouseleave + focusout (leave)' + - user_trigger: click + a11y_handler: 'activate — click + keydown Enter/Space' + caveats: + - 'allowA11yTriggers default: true. Set to false only to opt out explicitly.' + - 'activate and interest can also be set directly as trigger values for fine-grained control.' + sourceFile: src/handlers/index.ts + related: [api-allowA11yTriggers, trigger-hover, trigger-click] + + - id: concept-entry-points + name: 'Entry Points' + category: concept + description: >- + Three integration patterns for @wix/interact, all using the same + InteractConfig schema. + entry_points: + - name: 'Web (Custom Elements)' + import: "import { Interact } from '@wix/interact/web'" + element: "" + notes: + - 'Use Interact.create(config). Wrap target elements with .' + - 'data-interact-key MUST be unique within the page.' + - 'Element MUST contain at least one child (library targets firstElementChild by default).' + - name: React + import: "import { Interact, Interaction } from '@wix/interact/react'" + element: "" + notes: + - 'Wrap Interact.create(config) in useEffect to prevent SSR execution.' + - 'Store the instance; call instance.destroy() in the useEffect cleanup.' + - 'interactKey MUST be unique within the page.' + - name: 'Vanilla JS' + import: "import { Interact } from '@wix/interact'" + element: 'any HTMLElement' + notes: + - 'const instance = Interact.create(config); instance.add(element, key).' + - 'Call add() only after the element exists in the DOM.' + caveats: + - 'All three entry points use the same InteractConfig schema.' + - 'React: never call Interact.create outside useEffect — it will run on the server.' + related: [api-create] + + - id: concept-named-effect + name: 'namedEffect' + category: concept + description: >- + The animation payload type that references a preset registered via + Interact.registerEffects / motion's registerEffects. Must be an object with + a type field matching the registered preset name. + shape: '{ type: string; [paramName: string]: any }' + example: "{ type: 'FadeIn' } or { type: 'ArcIn', direction: 'bottom' }" + caveats: + - "Must be an object { type: '...' } — bare strings like 'FadeIn' are NOT accepted." + - 'All presets from @wix/motion-presets must be registered before use.' + - 'Unknown namedEffect.type values are silently ignored at runtime.' + sourceFile: src/types/effects.ts + related: [api-registerEffects] + + # ─── Enums / Unions ────────────────────────────────────────────────────────── + + - id: enum-triggerType + name: TimeAnimationTriggerType + category: enum + description: >- + Controls how a TimeEffect or SequenceConfig responds to repeated trigger + events. Applies to duration-based animations only (not scrub or state). + values: + - value: once + description: 'Play once on first trigger. Ignore subsequent activations. Default for viewEnter, animationEnd.' + - value: repeat + description: 'Play from the start on every trigger activation.' + - value: alternate + description: 'Play forward on trigger activate, reverse on trigger deactivate. Default for hover, click, activate, interest.' + - value: state + description: 'Controlled externally by stateAction on StateEffect.' + defaults_by_trigger: + hover: alternate + click: alternate + activate: alternate + interest: alternate + viewEnter: once + animationEnd: once + pageVisible: once + viewProgress: null + pointerMove: null + sourceFile: src/types/effects.ts + related: [effect-time] + + - id: enum-stateAction + name: StateAction + category: enum + description: 'The CSS state manipulation action for StateEffect.' + values: + - value: toggle + description: 'Add state if absent, remove if present. Default.' + - value: add + description: 'Add the CSS class or custom state.' + - value: remove + description: 'Remove the CSS class or custom state.' + - value: clear + description: 'Remove all tracked states from the element.' + sourceFile: src/types/effects.ts + related: [effect-state] diff --git a/scripts/build-context.spec.md b/scripts/build-context.spec.md new file mode 100644 index 00000000..ae6595ea --- /dev/null +++ b/scripts/build-context.spec.md @@ -0,0 +1,514 @@ +# `scripts/build-context.js` — Implementation Spec + +## Purpose + +Reads `packages//context/glossary.yaml` and produces two kinds of output: + +1. **`rules/` files** — fully generated markdown from renderer functions. The glossary is the only input; output is always overwritten. +2. **`docs/` files** — hand-authored prose with structured sections (param tables) injected between HTML comment markers. All content outside the markers is untouched. + +This script must remain compatible with `scripts/generate-llms.mjs`, which reads all `.md` files from `packages/interact/rules/`. When the new rules file names are introduced, `KNOWN_ORDER` in `generate-llms.mjs` must be updated to list the new file names in priority order. + +--- + +## Usage + +```bash +node scripts/build-context.js --package interact +node scripts/build-context.js --package motion +node scripts/build-context.js --package presets +``` + +Add to root `package.json` scripts: + +```json +"build:context": "node scripts/build-context.js --package interact" +``` + +--- + +## Style Constraints + +- Plain ESM (`import`/`export`); shebang not needed. +- Node.js built-in modules only, plus one allowed external dependency: the `yaml` npm package for YAML parsing (add as a root workspace `devDependency` if not already present). +- Target: under 200 lines. Follow the same string-building style as `generate-llms.mjs` — no template engines, no marker-replacement DSL, just plain string concatenation and array `.join('\n')`. +- All file paths are relative to the monorepo root. + +--- + +## Module Structure + +``` +scripts/build-context.js + ├── loadGlossary(pkg) — reads and parses YAML, returns term arrays by category + ├── renderOverviewFile(data) → rules/overview.md + ├── renderConfigFile(terms) → rules/config.md + ├── renderTriggersFile(terms) → rules/triggers.md + ├── renderEffectsFile(terms) → rules/effects.md + ├── renderPitfallsFile(terms) → rules/pitfalls.md + ├── injectDocsSections(pkg, terms) — injects param tables into docs/ files between markers + └── main() — CLI entry: parse --package arg, orchestrate all steps +``` + +--- + +## 1. Loading the Glossary + +```javascript +import { parse } from 'yaml'; +import { readFileSync } from 'node:fs'; + +function loadGlossary(pkg) { + const path = `packages/${pkg}/context/glossary.yaml`; + const raw = readFileSync(path, 'utf-8'); + const { terms } = parse(raw); + return { + triggers: terms.filter((t) => t.category === 'trigger'), + effectTypes: terms.filter((t) => t.category === 'effect-type'), + configs: terms.filter((t) => t.category === 'config'), + apis: terms.filter((t) => t.category === 'api'), + concepts: terms.filter((t) => t.category === 'concept'), + enums: terms.filter((t) => t.category === 'enum'), + all: terms, + }; +} +``` + +--- + +## 2. Shared Helpers + +### `renderParamTable(params)` + +Renders a `| name | type | default | description |` markdown table from a term's `params` array. Returns an empty string if `params` is empty or absent. + +Column order: name, type, default, description. +Row format: `` `name` `` in the name column (backtick-wrapped); required params use `**—**` in the default column. + +### `renderFrontmatter(name, description)` + +Returns: + +``` +--- +name: +description: +--- +``` + +### `renderTOC(entries)` + +Returns a `## Table of Contents` block from an array of `{ label, anchor }` objects. Anchor format: lowercase, spaces → `-`, special chars stripped. Example: `'viewEnter'` → `#viewenter`. + +--- + +## 3. Renderer Functions + +All renderer functions return a complete markdown string. The calling code writes the string directly to the output file with no further processing. + +### 3.1 `renderOverviewFile(data)` + +Output: `rules/overview.md` + +``` +--- +name: overview +description: Quick-start reference for @wix/interact. Entry points, install, and a minimal working example. +--- + +# @wix/interact — Overview + + + +## Table of Contents + +- [Install](#install) +- [Entry Points](#entry-points) +- [Quick Start](#quick-start) +- [Static API Summary](#static-api-summary) + +--- + +## Install + +npm install @wix/interact @wix/motion-presets + +--- + +## Entry Points + + + +--- + +## Quick Start + + + +--- + +## Static API Summary + + +Only include api terms that are static methods or static flags (api-create, api-destroy, +api-setup, api-registerEffects, api-allowA11yTriggers, api-forceReducedMotion). +``` + +### 3.2 `renderConfigFile(terms)` + +Output: `rules/config.md` + +``` +--- +name: config +description: InteractConfig schema, Interaction shape, Effect types, sequences, and conditions. +--- + +# @wix/interact — Configuration + +## Table of Contents + +- [InteractConfig](#interactconfig) +- [Interaction](#interaction) +- [Effect Shared Fields](#effect-shared-fields) +- [SequenceConfig](#sequenceconfig) +- [SequenceConfigRef](#sequenceconfigref) +- [Condition](#condition) + +--- + +## + + + + + + + +--- (separator between config terms) +``` + +Render all terms where `category === 'config'` in this order: `config-InteractConfig`, `config-Interaction`, `config-SequenceConfig`, `config-Condition`. Include a brief note after the Interaction section: "At least one of `effects` or `sequences` must be provided per Interaction." + +### 3.3 `renderTriggersFile(terms)` + +Output: `rules/triggers.md` + +``` +--- +name: triggers +description: Full parameter reference for all @wix/interact triggers. Six primary triggers plus accessible variants and one deprecated trigger. +--- + +# @wix/interact — Triggers + +Six primary user-facing triggers: hover, click, viewEnter, viewProgress, pointerMove, animationEnd. +Two accessible variants (enabled when allowA11yTriggers is true): activate, interest. +One deprecated trigger: pageVisible (use viewEnter). + +## Table of Contents + +- [hover](#hover) +- [click](#click) +- [viewEnter](#viewenter) +- [viewProgress](#viewprogress) +- [pointerMove](#pointermove) +- [animationEnd](#animationend) +- [Accessible Variants](#accessible-variants) +- [Deprecated](#deprecated) + +--- +``` + +**Per-trigger section format** (for the 6 primary triggers): + +``` +### + + + +**Default triggerType:** + +**Params:** + + ← or "None." if params array is empty + + + +**Caveats:** + + + +--- +``` + +**Accessible Variants section**: One combined `## Accessible Variants` section. Render `trigger-activate` and `trigger-interest` as sub-sections (`###`). Include a preamble: "Enabled when `Interact.allowA11yTriggers` is `true` (the default). Users configure `hover`/`click` — these handlers are applied transparently." + +**Deprecated section**: One `## Deprecated` section listing `trigger-pageVisible` with its description and caveats. + +### 3.4 `renderEffectsFile(terms)` + +Output: `rules/effects.md` + +``` +--- +name: effects +description: TimeEffect, ScrubEffect, and StateEffect — fields, defaults, and when to use each. +--- + +# @wix/interact — Effects + +## Table of Contents + +- [TimeEffect](#timeeffect) +- [ScrubEffect](#scrubeffect) +- [StateEffect](#stateeffect) +- [Animation Payloads](#animation-payloads) +- [triggerType Defaults by Trigger](#triggertype-defaults-by-trigger) +- [stateAction Values](#stateaction-values) + +--- +``` + +**Per-effect section format**: + +``` +## + + + + + + + +**Caveats:** + + + +--- +``` + +**Animation Payloads section** (hardcoded prose + code block): + +``` +## Animation Payloads + +Every TimeEffect and ScrubEffect requires exactly one of: + +| Payload | Shape | Use when | +| --------------- | -------------------------------------------------- | ------------------------------- | +| `namedEffect` | `{ type: string, ...params }` | Using a registered preset | +| `keyframeEffect`| `{ name: string, keyframes: Keyframe[] }` | Custom WAAPI keyframes | +| `customEffect` | `(element: Element, progress: any) => void` | Fully custom JS animation | +``` + +Note: reference `concept-named-effect` caveats for the namedEffect row. + +**`triggerType` Defaults by Trigger section**: Render the `enum-triggerType.defaults_by_trigger` map as a two-column table (`| trigger | default triggerType |`). + +**`stateAction` Values section**: Render `enum-stateAction.values` as a table. + +### 3.5 `renderPitfallsFile(terms)` + +Output: `rules/pitfalls.md` + +``` +--- +name: pitfalls +description: Critical gotchas for @wix/interact integrations — FOUC, overflow:clip, same-element source/target, hit-area jitter, and a11y mapping. +--- + +# @wix/interact — Common Pitfalls + +Each item is CRITICAL — ignoring it will break animations or accessibility. + +## Table of Contents + +- [FOUC Prevention](#fouc-prevention) +- [overflow: clip](#overflow-clip) +- [Same-Element Source and Target](#same-element-source-and-target) +- [Hit-Area Jitter](#hit-area-jitter) +- [pointerMove + hitArea: self](#pointermove--hitarea-self) +- [Accessibility Trigger Mapping](#accessibility-trigger-mapping) + +--- +``` + +**Content**: Each pitfall is a `##` section. Content comes from a mix of: + +- `concept-fouc` caveats → **FOUC Prevention** section +- `concept-a11y-mapping` caveats + description → **Accessibility Trigger Mapping** section +- `trigger-viewEnter` caveat about same-element → **Same-Element Source and Target** section +- `trigger-pointerMove` caveat about hit-area → **pointerMove + hitArea: self** section +- **overflow: clip** and **Hit-Area Jitter** sections: hardcoded text (these are structural/CSS gotchas not directly expressible from glossary terms) + +The `overflow: clip` pitfall reads: + +> `overflow: hidden` breaks `viewProgress`. Replace with `overflow: clip` on all ancestors between source and scroll container. In Tailwind, replace `overflow-hidden` with `overflow-clip`. + +The **Hit-Area Jitter** pitfall reads: + +> When a hover effect changes the size or position of the hovered element (e.g. `transform: scale(...)`), use separate source and target elements. Otherwise the hit area shifts, causing rapid enter/leave events and flickering. Use `selector` to target a child element, or set the effect's `key` to a different element. + +--- + +## 4. Docs Injection + +Docs injection modifies hand-authored docs files by replacing content between HTML comment markers with generated param tables from the glossary. + +### Marker syntax + +```html + +...any existing content here is replaced... + +``` + +The `id` attribute references a term `id` in the glossary. Multiple START/END pairs can appear in one file. + +### Algorithm + +``` +function injectDocsSections(pkg, terms): + for each .md file in packages//docs/ (recursive): + content = readFileSync(file) + if content does not contain '\n${table}\n` + ) + writeFileSync(file, newContent) +``` + +**Regex pattern** (multiline, dotall): + +``` +/[\s\S]*?/g +``` + +The replacement string is the same opening marker, then the generated table, then the closing marker. The original content between markers is discarded on each run (idempotent). + +### Docs files that will use injection (Phase 1, Interact) + +| File | Term IDs injected | +| ------------------------------ | ---------------------------------------------------------------------- | +| `docs/api/types.md` | `effect-time`, `effect-scrub`, `effect-state`, `config-Condition` | +| `docs/guides/triggers.md` | `trigger-viewEnter`, `trigger-pointerMove`, `trigger-animationEnd` | +| `docs/api/interact-class.md` | `api-allowA11yTriggers`, `api-forceReducedMotion` | +| `docs/guides/configuration.md` | `config-InteractConfig`, `config-Interaction`, `config-SequenceConfig` | + +These files are hand-authored; only the param tables between markers are generated. + +--- + +## 5. `main()` Entry Point + +```javascript +function main() { + const pkgArg = process.argv.indexOf('--package'); + if (pkgArg === -1 || !process.argv[pkgArg + 1]) { + console.error('Usage: node scripts/build-context.js --package '); + process.exit(1); + } + const pkg = process.argv[pkgArg + 1]; + const PKG_DIR_MAP = { interact: 'interact', motion: 'motion', presets: 'motion-presets' }; + const pkgDir = PKG_DIR_MAP[pkg]; + if (!pkgDir) { + console.error(`Unknown package: ${pkg}`); + process.exit(1); + } + + const terms = loadGlossary(pkgDir); + const rulesDir = `packages/${pkgDir}/rules`; + + // Generate rules/ files + writeFileSync(`${rulesDir}/overview.md`, renderOverviewFile(terms)); + writeFileSync(`${rulesDir}/config.md`, renderConfigFile(terms.configs)); + writeFileSync(`${rulesDir}/triggers.md`, renderTriggersFile(terms.triggers)); + writeFileSync(`${rulesDir}/effects.md`, renderEffectsFile(terms)); + writeFileSync(`${rulesDir}/pitfalls.md`, renderPitfallsFile(terms)); + + // Inject structured sections into docs/ files + injectDocsSections(pkgDir, terms); + + console.log(`build-context: wrote rules/ and injected docs/ for @wix/${pkgDir}`); +} +``` + +The script must ensure `rules/` exists (`mkdirSync(rulesDir, { recursive: true })`). + +--- + +## 6. `generate-llms.mjs` Compatibility + +After the new rules files are generated, update `KNOWN_ORDER` in `scripts/generate-llms.mjs` from: + +```javascript +const KNOWN_ORDER = [ + 'full-lean.md', + 'integration.md', + 'click.md', + 'hover.md', + 'pointermove.md', + 'viewenter.md', + 'viewprogress.md', +]; +``` + +to: + +```javascript +const KNOWN_ORDER = ['overview.md', 'triggers.md', 'effects.md', 'config.md', 'pitfalls.md']; +``` + +Update `DOCS_LINK_TITLES` accordingly — `overview.md` maps to `'Overview'`, `triggers.md` maps to `'Triggers Reference'`. + +Also update `STATIC_BODY` to reflect the corrected trigger count: + +```javascript +'- Six primary trigger types: hover, click, viewEnter, viewProgress, pointerMove, animationEnd — plus accessible variants activate and interest (enabled by default)'; +``` + +--- + +## 7. CI Verification Step + +Add a CI step in `.github/workflows/interactdocs.yml` (or a new `context.yml` workflow): + +```yaml +- name: Verify rules/ is up to date + run: | + node scripts/build-context.js --package interact + git diff --exit-code packages/interact/rules/ +``` + +This fails if generated output differs from what is committed, catching glossary changes that weren't followed by a rebuild. + +--- + +## 8. Output File Counts + +After Phase 1 (Interact), the `packages/interact/rules/` directory will contain exactly five files: + +| File | Generated by | +| ------------- | -------------------- | +| `overview.md` | `renderOverviewFile` | +| `config.md` | `renderConfigFile` | +| `triggers.md` | `renderTriggersFile` | +| `effects.md` | `renderEffectsFile` | +| `pitfalls.md` | `renderPitfallsFile` | + +The old files (`full-lean.md`, `integration.md`, `click.md`, `hover.md`, `pointermove.md`, `viewenter.md`, `viewprogress.md`) are deleted after the new output is validated (Phase 1.6 in the plan). + +--- + +## 9. Not in Scope for This Script + +- The script does **not** validate glossary entries against TypeScript source — that is the job of the Vitest audit tests (`packages/interact/test/context-audit.spec.ts`) introduced in Phase 1.1. +- The script does **not** delete old rules files — deletion is a manual step in Phase 1.6 after review. +- The script does **not** handle motion or motion-presets renderer logic in Phase 0 — placeholder `renderOverviewFile` stubs are sufficient until those packages are migrated.