From 9cf2c24e15a8ff843a2620d176a0fd30ccc0a627 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 14:34:23 +0300 Subject: [PATCH 01/27] build script for rules - single source of truth --- packages/interact/_content/data/effects.yaml | 132 ++++ packages/interact/_content/data/meta.yaml | 8 + packages/interact/_content/data/triggers.yaml | 207 ++++++ .../_content/fragments/custom-effect-intro.md | 2 + .../_content/fragments/element-resolution.md | 40 ++ packages/interact/_content/fragments/fouc.md | 125 ++++ .../fragments/multiple-effects-note.md | 2 + .../fragments/pitfalls/dont-guess-presets.md | 2 + .../_content/fragments/pitfalls/hit-area.md | 14 + .../fragments/pitfalls/overflow-clip.md | 4 + .../fragments/pitfalls/perspective.md | 2 + .../fragments/pitfalls/reduced-motion.md | 2 + .../pitfalls/same-element-viewenter.md | 4 + .../_content/fragments/quick-start.md | 80 +++ .../_content/fragments/sequences-intro.md | 2 + .../interact/_content/templates/_helpers.mjs | 3 + .../_content/templates/event-trigger-rule.mjs | 256 ++++++++ .../interact/_content/templates/full-lean.mjs | 604 ++++++++++++++++++ .../_content/templates/integration.mjs | 285 +++++++++ .../_content/templates/pointermove-rule.mjs | 279 ++++++++ .../_content/templates/viewenter-rule.mjs | 173 +++++ .../_content/templates/viewprogress-rule.mjs | 147 +++++ packages/interact/package.json | 2 + packages/interact/rules/click.md | 4 +- packages/interact/rules/full-lean.md | 2 +- packages/interact/rules/hover.md | 4 +- packages/interact/rules/pointermove.md | 2 +- packages/interact/rules/viewenter.md | 4 +- packages/interact/scripts/build-rules.mjs | 162 +++++ 29 files changed, 2545 insertions(+), 8 deletions(-) create mode 100644 packages/interact/_content/data/effects.yaml create mode 100644 packages/interact/_content/data/meta.yaml create mode 100644 packages/interact/_content/data/triggers.yaml create mode 100644 packages/interact/_content/fragments/custom-effect-intro.md create mode 100644 packages/interact/_content/fragments/element-resolution.md create mode 100644 packages/interact/_content/fragments/fouc.md create mode 100644 packages/interact/_content/fragments/multiple-effects-note.md create mode 100644 packages/interact/_content/fragments/pitfalls/dont-guess-presets.md create mode 100644 packages/interact/_content/fragments/pitfalls/hit-area.md create mode 100644 packages/interact/_content/fragments/pitfalls/overflow-clip.md create mode 100644 packages/interact/_content/fragments/pitfalls/perspective.md create mode 100644 packages/interact/_content/fragments/pitfalls/reduced-motion.md create mode 100644 packages/interact/_content/fragments/pitfalls/same-element-viewenter.md create mode 100644 packages/interact/_content/fragments/quick-start.md create mode 100644 packages/interact/_content/fragments/sequences-intro.md create mode 100644 packages/interact/_content/templates/_helpers.mjs create mode 100644 packages/interact/_content/templates/event-trigger-rule.mjs create mode 100644 packages/interact/_content/templates/full-lean.mjs create mode 100644 packages/interact/_content/templates/integration.mjs create mode 100644 packages/interact/_content/templates/pointermove-rule.mjs create mode 100644 packages/interact/_content/templates/viewenter-rule.mjs create mode 100644 packages/interact/_content/templates/viewprogress-rule.mjs create mode 100644 packages/interact/scripts/build-rules.mjs diff --git a/packages/interact/_content/data/effects.yaml b/packages/interact/_content/data/effects.yaml new file mode 100644 index 00000000..b29c08ad --- /dev/null +++ b/packages/interact/_content/data/effects.yaml @@ -0,0 +1,132 @@ +fillGuidance: + both: "use for scroll-driven (`viewProgress`), pointer-driven (`pointerMove`), and toggling effects (`hover`/`click` with `alternate`, `repeat`, or `state` type)." + backwards: "use for entrance animations with `type: 'once'` when the element's own CSS already matches the final keyframe (applies the initial keyframe during any `delay`)." + +triggerTypes: + - once + - repeat + - alternate + - state + +stateActions: + - toggle + - add + - remove + - clear + +easings: + - linear + - ease + - ease-in + - ease-out + - ease-in-out + - sineIn + - sineOut + - sineInOut + - quadIn + - quadOut + - quadInOut + - cubicIn + - cubicOut + - cubicInOut + - quartIn + - quartOut + - quartInOut + - quintIn + - quintOut + - quintInOut + - expoIn + - expoOut + - expoInOut + - circIn + - circOut + - circInOut + - backIn + - backOut + - backInOut + +transitionEasings: + - linear + - hardBackOut + - easeOut + - elastic + - bounce + +presets: + entrance: + - FadeIn + - GlideIn + - SlideIn + - FloatIn + - RevealIn + - ExpandIn + - BlurIn + - FlipIn + - ArcIn + - ShuttersIn + - CurveIn + - DropIn + - FoldIn + - ShapeIn + - TiltIn + - WinkIn + - SpinIn + - TurnIn + - BounceIn + ongoing: + - Pulse + - Spin + - Breathe + - Bounce + - Wiggle + - Flash + - Flip + - Fold + - Jello + - Poke + - Rubber + - Swing + - Cross + scroll: + - FadeScroll + - RevealScroll + - ParallaxScroll + - MoveScroll + - SlideScroll + - GrowScroll + - ShrinkScroll + - TiltScroll + - PanScroll + - BlurScroll + - FlipScroll + - SpinScroll + - ArcScroll + - ShapeScroll + - ShuttersScroll + - SkewPanScroll + - Spin3dScroll + - StretchScroll + - TurnScroll + mouse: + - TrackMouse + - Tilt3DMouse + - Track3DMouse + - SwivelMouse + - AiryMouse + - ScaleMouse + - BlurMouse + - SkewMouse + - BlobMouse + +rangeNames: + entry: "Element entering viewport" + exit: "Element exiting viewport" + contain: "After `entry` range and before `exit` range" + cover: "Full range from `entry` through `contain` and `exit`" + entry-crossing: "From element's leading edge entering to trailing edge entering" + exit-crossing: "From element's leading edge exiting to trailing edge exiting" + +compositeOperations: + replace: "fully replaces prior values." + add: "concatenates transform/filter functions after any existing ones (e.g. existing `translateX(10px)` + added `translateY(20px)` → both apply)." + accumulate: "merges arguments of matching functions (e.g. `translateX(10px)` + `translateX(20px)` → `translateX(30px)`); non-matching functions concatenate like `'add'`." diff --git a/packages/interact/_content/data/meta.yaml b/packages/interact/_content/data/meta.yaml new file mode 100644 index 00000000..e34ad310 --- /dev/null +++ b/packages/interact/_content/data/meta.yaml @@ -0,0 +1,8 @@ +packageName: "@wix/interact" +presetsPackage: "@wix/motion-presets" +motionPackage: "@wix/motion" +installCommand: "npm install @wix/interact @wix/motion-presets" +entryPoints: + web: "@wix/interact/web" + react: "@wix/interact/react" + vanilla: "@wix/interact" diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml new file mode 100644 index 00000000..47afa0d7 --- /dev/null +++ b/packages/interact/_content/data/triggers.yaml @@ -0,0 +1,207 @@ +triggers: + - name: hover + a11yAlias: interest + a11yNote: "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus." + category: event + supportsTimeEffect: true + supportsStateEffect: true + supportsScrubEffect: false + supportsCustomEffect: true + defaultTriggerType: alternate + params: [] + pitfalls: + - id: hit-area + showMultipleEffectsNote: true + templateFields: + timeEffect: + - triggerType + - keyframeEffect + - namedEffect + - fill + - duration + - easing + - delay + - iterations + - alternate + stateEffect: + - stateAction + - transition + - transitionProperties + customEffect: + - triggerType + - customEffect + - duration + - easing + triggerTypeDescriptions: + alternate: "plays forward on enter, reverses on leave. Default. Most common for hover." + repeat: "restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses." + once: "plays once on the first enter and never again." + state: "resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`)." + stateActionDescriptions: + toggle: "applies the style state on enter, removes on leave. Default." + add: "applies the style state on enter. Leave does NOT remove it." + remove: "removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`." + clear: "clears all previously applied style states on enter. Use to reset multiple stacked `'add'` style changes at once (e.g. a \"reset\" hover area that undoes several accumulated states)." + fillNote: "while hovering" + fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`." + fillModeAtEnd: true + fillModeDash: "—" + sourceKeyDesc: "The element that listens for hover." + targetKeyDesc: "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above)." + fillModeDesc: "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished." + namedEffectDesc: "object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options." + easingDesc: "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`." + iterationsDesc: "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`." + alternateDesc: "optional. `true` to alternate direction on every other iteration (within a single playback)." + customEffectIntro: "Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline." + customEffectCallbackDesc: "function with signature `(target: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1." + sequenceEffectDefDesc: "a definition of or a reference to a time-based animation effect." + sequenceOffsetEasingDesc: "easing curve for the offset staggering distribution. CSS easing string, or named easing from `@wix/motion`. Defaults to `'linear'`." + timeEffectIntro: "Use `keyframeEffect` or `namedEffect` when the hover should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior." + stateEffectIntro: "Use `transition` or `transitionProperties` when the hover should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Set `stateAction` on the effect to control how the style is applied." + sequencesIntro: "Use sequences when a hover should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior." + + - name: click + a11yAlias: activate + a11yNote: "Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space)." + category: event + supportsTimeEffect: true + supportsStateEffect: true + supportsScrubEffect: false + supportsCustomEffect: true + defaultTriggerType: alternate + params: [] + pitfalls: [] + showMultipleEffectsNote: false + templateFields: + timeEffect: + - triggerType + - keyframeEffect + - namedEffect + - fill + - reversed + - duration + - easing + - delay + - iterations + - alternate + - effectId + stateEffect: + - stateAction + - transition + - transitionProperties + customEffect: + - triggerType + - customEffect + - duration + - easing + triggerTypeDescriptions: + alternate: "plays forward on first click, reverses on next click. Default." + repeat: "restarts the animation from the beginning on each click." + once: "plays once on the first click and never again." + state: "resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`)." + stateActionDescriptions: + toggle: "applies the style state, removes it on next click. Default." + add: "applies the style state. Does not remove on subsequent clicks." + remove: "removes a previously applied style state." + clear: "clears all previously applied style states. Useful for resetting multiple stacked style states at once." + fillNote: "while finished" + fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`." + fillModeAtEnd: false + fillModeDash: "-" + sourceKeyDesc: "The element that listens for clicks." + targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element." + fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect." + namedEffectDesc: "object with properties of pre-built, time-based animation effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options." + easingDesc: "CSS easing string, or named easing from `@wix/motion`." + iterationsDesc: "optional. Number of iterations, or `Infinity` for continuous loops." + alternateDesc: "optional. `true` to alternate direction on every other iteration (within a single playback). Different from `triggerType: 'alternate'` which alternates per click." + customEffectIntro: "Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation, randomized behavior). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline." + customEffectCallbackDesc: "function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with target element and `progress` from 0 to 1." + sequenceEffectDefDesc: "a definition of, or a reference to a time-based animation effect." + sequenceOffsetEasingDesc: "easing curve for the offset staggering distribution. Defaults to `'linear'`." + timeEffectIntro: "Use `keyframeEffect` or `namedEffect` when the click should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior." + stateEffectIntro: "Use `transition` or `transitionProperties` when the click should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Uses the `transition` CSS property. Set `stateAction` on the effect to control how the style is applied." + sequencesIntro: "Use sequences when a click should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior." + + - name: viewEnter + category: viewport + supportsTimeEffect: true + supportsStateEffect: false + supportsScrubEffect: false + supportsCustomEffect: true + defaultTriggerType: once + params: + - name: threshold + type: number + optional: true + description: "Number between 0–1 indicating how much of the source element must be visible to trigger (e.g. `0.3` = 30%)." + - name: inset + type: string + optional: true + description: "String adjusting the viewport detection area (e.g. `'-100px'` extends it, `'50px'` shrinks it)." + pitfalls: + - id: same-element-viewenter + showMultipleEffectsNote: true + + - name: viewProgress + category: scroll + supportsTimeEffect: false + supportsStateEffect: false + supportsScrubEffect: true + supportsCustomEffect: true + defaultTriggerType: null + params: [] + pitfalls: + - id: overflow-clip + showMultipleEffectsNote: true + + - name: pointerMove + category: pointer + supportsTimeEffect: false + supportsStateEffect: false + supportsScrubEffect: true + supportsCustomEffect: true + defaultTriggerType: null + params: + - name: hitArea + type: "'root' | 'self'" + optional: true + description: "determines where mouse movement is tracked" + - name: axis + type: "'x' | 'y'" + optional: true + description: "restricts pointer tracking to a single axis" + pitfalls: + - id: hit-area + + - name: animationEnd + category: chain + supportsTimeEffect: true + supportsStateEffect: false + supportsScrubEffect: false + supportsCustomEffect: false + defaultTriggerType: null + params: + - name: effectId + type: string + optional: false + description: "ID of the preceding effect" + + - name: pageVisible + category: event + supportsTimeEffect: true + supportsStateEffect: false + supportsScrubEffect: false + supportsCustomEffect: false + defaultTriggerType: null + params: [] + pitfalls: [] + + - name: activate + aliasOf: click + category: event + + - name: interest + aliasOf: hover + category: event diff --git a/packages/interact/_content/fragments/custom-effect-intro.md b/packages/interact/_content/fragments/custom-effect-intro.md new file mode 100644 index 00000000..99344971 --- /dev/null +++ b/packages/interact/_content/fragments/custom-effect-intro.md @@ -0,0 +1,2 @@ + +Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline. diff --git a/packages/interact/_content/fragments/element-resolution.md b/packages/interact/_content/fragments/element-resolution.md new file mode 100644 index 00000000..d3b4abf3 --- /dev/null +++ b/packages/interact/_content/fragments/element-resolution.md @@ -0,0 +1,40 @@ + +For simple use cases, `key` on the interaction matches the element, and the same element is both trigger source and animation target. The fields below are only needed for advanced patterns (lists, delegated triggers, child targeting). + +### Source element resolution (Interaction level) + +The source element is what the trigger attaches to. Resolved in priority order: + +1. **`listContainer` + `listItemSelector`** — trigger attaches to each element matching `listItemSelector` within the `listContainer`. Use `listItemSelector` only when you need to **filter** which children participate (e.g. select only `.active` items). If all immediate children should participate, omit `listItemSelector`. +2. **`listContainer` only** — trigger attaches to each immediate child of the container. This is the common case for lists. +3. **`listContainer` + `selector`** — trigger attaches to the element found via `querySelector` within each immediate child of the container. +4. **`selector` only** — trigger attaches to all elements matching `querySelectorAll` within the root ``. +5. **Fallback** — first child of `` (web) or the root element (react/vanilla). + +### Target element resolution (Effect level) + +The target element is what the effect animates. Resolved in priority order: + +1. **`Effect.key`** — the `` with matching `data-interact-key`. +2. **Registry Effect's `key`** — if the effect is an `EffectRef`, the `key` from the referenced registry entry is used. +3. **Fallback to `Interaction.key`** — the same `key` is used for the source will be used for the target. +4. After resolving the root target, `selector`, `listContainer`, and `listItemSelector` on the effect further refine which child elements within that target are animated (same priority order as source resolution). + +#### Source element resolution (Interaction level) + +The source element is what the trigger attaches to. Resolved in priority order: + +1. **`listContainer` + `listItemSelector`** — matches only the elements matching `listItemSelector` within the the `listContainer`. +2. **`listContainer` only** — trigger attaches to all immediate children of the container (common case). +3. **`listContainer` + `selector`** — matches via `querySelector` within each immediate child of the container. +4. **`selector` only** — matches via `querySelectorAll` within the root element. +5. **Fallback** — first child of `` (web) or the root element (react/vanilla). + +#### Target element resolution (Effect level) + +The target element is what the effect animates. Resolved in priority order: + +1. **`Effect.key`** — the root with matching `data-interact-key`. +2. **Registry Effect's `key`** — if the effect is an `EffectRef`, the `key` from the referenced registry entry is used. +3. **Fallback to `Interaction.key`** — the source element acts as the target's root. +4. After resolving the target's root, `selector`, `listContainer`, and `listItemSelector` on the effect further refine which child elements within that target are animated (same priority order as source resolution). diff --git a/packages/interact/_content/fragments/fouc.md b/packages/interact/_content/fragments/fouc.md new file mode 100644 index 00000000..646f9ff6 --- /dev/null +++ b/packages/interact/_content/fragments/fouc.md @@ -0,0 +1,125 @@ + +**Problem:** Elements with entrance animations (e.g. `FadeIn`) start in their final visible state (e.g. `opacity: 1`). Before the animation framework initializes and applies the starting keyframe (e.g. `opacity: 0`), the element is briefly visible at full opacity — a flash of un-animated content. + +**Solution:** Two things are required — **both** MUST be present for FOUC prevention to work: + +1. **Generate critical CSS** using `generate(config)` — produces CSS rules that hide entrance-animated elements from the moment the page renders, before JavaScript runs. +2. **Mark elements with `initial`** — set `data-interact-initial="true"` on ``, or `initial={true}` on the `` React component. This tells the runtime which elements have critical CSS applied. + +If only one of these is present, FOUC prevention will **not** work. Both the CSS and the `initial` attribute are required. + +**Problem:** Elements with entrance animations (e.g. `viewEnter` + `type: 'once'` with `FadeIn`) start in their final visible state. Before the animation framework initializes and applies the starting keyframe (e.g. `opacity: 0`), the element is briefly visible at full opacity — causing a flash of unstyled/un-animated content (FOUC). + +**Solution:** Two things are required — both MUST be present: + +1. **Generate critical CSS** using `generate(config)` — produces CSS rules that hide entrance-animated elements from the moment the page renders. +2. **Mark elements with `initial`** — tells the runtime which elements have critical CSS applied so it can coordinate with the generated styles. + +```typescript +import { generate } from '@wix/interact'; + +const config: InteractConfig = { + interactions: [ + { + key: '[SOURCE_KEY]', + trigger: 'viewEnter', + params: { + threshold: [VIEW_TRIGGER_THRESHOLD], + inset: [VIEW_TRIGGER_INSET], + }, + effects: [EFFECT_DEFINITIONS], + // and/or + sequences: [SEQUENCE_DEFINITIONS], + }, + ], +}; + +const css = generate(config); +``` + +```ts +import { generate } from '@wix/interact/web'; +const css = generate(config); +``` + +**Append to `` or beginning of ``:** + +```html + +``` + +**Web (Custom Elements):** + +```html + +
...
+
+``` + +**Web (Custom Elements):** + +```html + +
...
+
+``` + +**React:** + +```tsx + + ... + +``` + +**React:** + +```tsx + + ... + +``` + +**Vanilla:** + +```html +
...
+``` + +**Vanilla:** + +```html +
...
+``` + +### Rules + +- `generate()` should be called server-side or at build time. Can also be called on the client if the page content is initially hidden (e.g. behind a loader/splash screen). +- `initial` is only valid for `viewEnter` + `triggerType: 'once'` (or no `triggerType`, which defaults to `'once'`) where source and target are the same element. +- Do NOT use `initial` for `viewEnter` with `triggerType: 'repeat'`/`'alternate'`/`'state'`. For those, manually apply the initial keyframe as inline styles on the target element and use `fill: 'both'`. +- If other interactions in the config also need FOUC prevention, `generate(config)` covers them all — set `initial` only on the relevant `viewEnter` + `triggerType: 'once'` elements. + +**Rules:** + +- `generate()` should be called server-side or at build time. Can also be called on the client if page content is initially hidden (e.g. behind a loader). +- Only valid for `viewEnter` + `triggerType: 'once'` (or no `triggerType`, which defaults to `'once'`) where source and target are the same element. + +### Rules + +- `generate()` should be called server-side or at build time. Can also be called on client-side if page content is initially hidden (e.g. behind a loader/splash screen). +- **Both** `generate(config)` CSS **and** `initial` on the element are required. Using only one has no effect. +- `initial` is only valid for `viewEnter` + `type: 'once'` where source and target are the same element. +- For `repeat`/`alternate`/`state`, do NOT use `initial`. Instead, manually apply the initial keyframe as inline styles on the target element and use `fill: 'both'`. + +**Problem:** Elements with entrance animations (e.g. `FadeIn` on `viewEnter`) are initially visible in their final state. Before the animation framework applies the starting keyframe, the content flashes visibly — a flash of un-animated content (FOUC). + +**Solution:** Two things are required — both MUST be present: + +1. **Generate critical CSS** with `generate(config)` — produces CSS that hides entrance-animated elements until the animation plays. +2. **Mark elements with `initial`** — `data-interact-initial="true"` on ``, or `initial={true}` on `` in React. + +Using only one of these has no effect — both are required. + +See [viewenter.md](./viewenter.md) for full details. diff --git a/packages/interact/_content/fragments/multiple-effects-note.md b/packages/interact/_content/fragments/multiple-effects-note.md new file mode 100644 index 00000000..7a4a4382 --- /dev/null +++ b/packages/interact/_content/fragments/multiple-effects-note.md @@ -0,0 +1,2 @@ + +**Multiple effects:** The `effects` array can contain multiple effects — all share the same {{triggerName}} trigger and fire together. Use this to animate different targets from a single {{triggerEvent}}. diff --git a/packages/interact/_content/fragments/pitfalls/dont-guess-presets.md b/packages/interact/_content/fragments/pitfalls/dont-guess-presets.md new file mode 100644 index 00000000..7852bb4d --- /dev/null +++ b/packages/interact/_content/fragments/pitfalls/dont-guess-presets.md @@ -0,0 +1,2 @@ + +- **CRITICAL — Do NOT guess preset options**: If you don't know the expected type/structure for a `namedEffect` param, omit it — rely on defaults rather than guessing. diff --git a/packages/interact/_content/fragments/pitfalls/hit-area.md b/packages/interact/_content/fragments/pitfalls/hit-area.md new file mode 100644 index 00000000..844afd2a --- /dev/null +++ b/packages/interact/_content/fragments/pitfalls/hit-area.md @@ -0,0 +1,14 @@ + +- **CRITICAL**: MUST AVOID using the same element as both trigger source and effect target with effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. + +- **CRITICAL**: MUST AVOID using the same element as both source and target with effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. + +- **CRITICAL - Hit-area shift**: When a hover effect changes the size or position of the hovered element (e.g., `transform: scale(…)`), MUST use a 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. + +- **CRITICAL**: For `pointerMove` trigger MUST AVOID using the same element as both source and target with `hitArea: 'self'` and effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. + +When using `hitArea: 'self'`, the source element is the hit area for pointer tracking: + +- The source element **MUST NOT** have `pointer-events: none` — it needs to receive pointer events. +- **CRITICAL**: MUST AVOID using the same element as both source and target with effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. diff --git a/packages/interact/_content/fragments/pitfalls/overflow-clip.md b/packages/interact/_content/fragments/pitfalls/overflow-clip.md new file mode 100644 index 00000000..d8b67b33 --- /dev/null +++ b/packages/interact/_content/fragments/pitfalls/overflow-clip.md @@ -0,0 +1,4 @@ + +> **CRITICAL:** You MUST replace all usage of `overflow: hidden` with `overflow: clip` on every element between the trigger source element and the scroll container. `overflow: hidden` creates a new scroll context that breaks the ViewTimeline; `overflow: clip` clips overflow visually without affecting scroll ancestry. If using Tailwind, replace all `overflow-hidden` classes with `overflow-clip`. + +- **CRITICAL — `overflow: hidden` breaks `viewProgress`**: Replace with `overflow: clip` on all ancestors between source and scroll container. In Tailwind, replace `overflow-hidden` with `overflow-clip`. diff --git a/packages/interact/_content/fragments/pitfalls/perspective.md b/packages/interact/_content/fragments/pitfalls/perspective.md new file mode 100644 index 00000000..8ddcc117 --- /dev/null +++ b/packages/interact/_content/fragments/pitfalls/perspective.md @@ -0,0 +1,2 @@ + +- **Perspective**: Prefer `transform: perspective(...)` inside keyframes. Use the CSS `perspective` property only when multiple children share the same `perspective-origin`. diff --git a/packages/interact/_content/fragments/pitfalls/reduced-motion.md b/packages/interact/_content/fragments/pitfalls/reduced-motion.md new file mode 100644 index 00000000..5ce7d125 --- /dev/null +++ b/packages/interact/_content/fragments/pitfalls/reduced-motion.md @@ -0,0 +1,2 @@ + +- **Reduced motion**: Use conditions to provide gentler alternatives (shorter durations, fewer transforms, no perpetual motion) for users who prefer reduced motion. You can also set `Interact.forceReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches` to force a global reduced-motion behavior programmatically. diff --git a/packages/interact/_content/fragments/pitfalls/same-element-viewenter.md b/packages/interact/_content/fragments/pitfalls/same-element-viewenter.md new file mode 100644 index 00000000..39d4ffc9 --- /dev/null +++ b/packages/interact/_content/fragments/pitfalls/same-element-viewenter.md @@ -0,0 +1,4 @@ + +> **CRITICAL:** When the source (trigger) and target (effect) elements are the **same element**, use ONLY `triggerType: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`), MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing. + +- **CRITICAL**: When using `viewEnter` trigger and source (trigger) and target (effect) elements are the **same element**, use ONLY `type: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`) MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing. diff --git a/packages/interact/_content/fragments/quick-start.md b/packages/interact/_content/fragments/quick-start.md new file mode 100644 index 00000000..b10d5209 --- /dev/null +++ b/packages/interact/_content/fragments/quick-start.md @@ -0,0 +1,80 @@ + +```bash +npm install @wix/interact @wix/motion-presets +``` + +**Web (Custom Elements):** + +```ts +import { Interact } from '@wix/interact/web'; +const instance = Interact.create(config); +``` + +The `config` object is an `InteractConfig` containing `interactions` (required), and optionally shared `effects`, `sequences`, and `conditions`. + +```typescript +import { Interact } from '@wix/interact/web'; + +Interact.create(config); +``` + +The `config` object contains `interactions` (trigger-effect bindings), and optionally `effects`, `sequences`, and `conditions`. See [Configuration Schema](#configuration-schema) for full details. + +**React:** + +- Wrap the `Interact.create()` call in a `useEffect` hook to prevent it from running on server-side. +- Store the returned instance, and call its `.destroy()` method on the effect's cleanup function. + +```ts +import { useEffect } from 'react'; +import { Interact } from '@wix/interact/react'; + +useEffect(() => { + const instance = Interact.create(config); + + return () => { + instance.destroy(); + }; +}, [config]); +``` + +**Vanilla JS:** + +```ts +import { Interact } from '@wix/interact'; +const instance = Interact.create(config); +instance.add(element, 'hero'); // bind after element exists in DOM +instance.remove('hero'); // unregister +``` + +```typescript +import { Interact } from '@wix/interact'; + +const interact = Interact.create(config); +interact.add(element, 'hero'); +``` + +**CDN (no build tools):** + +```html + +``` + +**Registering presets** — MUST be called before calling `Interact.create()` with usage of `namedEffect`: + +```ts +import * as presets from '@wix/motion-presets'; +Interact.registerEffects(presets); +``` + +Or selectively: + +```ts +import { FadeIn, ParallaxScroll } from '@wix/motion-presets'; +Interact.registerEffects({ FadeIn, ParallaxScroll }); +``` + +Create the full config up-front and pass it in a single `create` call. Subsequent calls create new `Interact` instances. When creating multiple instances, each manages its own set of interactions independently — use separate instances for isolated component scopes or lazy-loaded sections. diff --git a/packages/interact/_content/fragments/sequences-intro.md b/packages/interact/_content/fragments/sequences-intro.md new file mode 100644 index 00000000..54ca8433 --- /dev/null +++ b/packages/interact/_content/fragments/sequences-intro.md @@ -0,0 +1,2 @@ + +Use sequences when a {{triggerName}} should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior. diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs new file mode 100644 index 00000000..35b85030 --- /dev/null +++ b/packages/interact/_content/templates/_helpers.mjs @@ -0,0 +1,3 @@ +export function capitalize(s) { + return s.charAt(0).toUpperCase() + s.slice(1); +} diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs new file mode 100644 index 00000000..2bcf614e --- /dev/null +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -0,0 +1,256 @@ +import { capitalize } from './_helpers.mjs'; + +export function render(trigger, data, fragments) { + const name = trigger.name; + const Name = capitalize(name); + const hasReversed = trigger.templateFields.timeEffect.includes('reversed'); + const hasEffectId = trigger.templateFields.timeEffect.includes('effectId'); + const showMultipleEffects = trigger.showMultipleEffectsNote; + + const pitfallsBlock = trigger.pitfalls.length > 0 + ? '\n' + trigger.pitfalls.map(p => fragments.get(`pitfalls/${p.id}`, name)).join('\n') + '\n' + : ''; + + const multipleEffectsNote = showMultipleEffects + ? `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event` })}\n` + : ''; + + const rule1Closing = ` },\n // additional effects targeting other elements can be added here`; + const rule23Closing = showMultipleEffects + ? ` },\n // additional effects targeting other elements can be added here` + : ` }`; + + const reversedField = hasReversed + ? `\n reversed: [INITIAL_REVERSED_BOOL],` + : ''; + + const effectIdField = hasEffectId + ? `\n effectId: '[UNIQUE_EFFECT_ID]'` + : ''; + + const fillModeVarDash = trigger.fillModeDash || '—'; + + const reversedVar = hasReversed + ? `\n- \`[INITIAL_REVERSED_BOOL]\` — optional. \`true\` to start in the finished state so the entire effect is reversed.` + : ''; + + const effectIdVar = hasEffectId + ? `\n- \`[UNIQUE_EFFECT_ID]\` — optional. String identifier used by \`animationEnd\` triggers for chaining, and by sequences for referencing effects from the top-level \`effects\` map.` + : ''; + + const fillModeVar = `- \`[FILL_MODE]\` ${fillModeVarDash} ${trigger.fillModeDesc}`; + + const variablesBlock = trigger.fillModeAtEnd + ? buildVariablesEndFill(trigger, fillModeVar, reversedVar, effectIdVar) + : buildVariablesMidFill(trigger, fillModeVar, reversedVar, effectIdVar); + + return `# ${Name} Trigger Rules for ${data.meta.packageName} + +This document contains rules for generating ${name}-triggered interactions in \`${data.meta.packageName}\`. + +**CRITICAL — Accessible ${name}**: ${trigger.a11yNote} +${pitfallsBlock} +## Table of Contents + +- [Rule 1: keyframeEffect / namedEffect (TimeEffect)](#rule-1-keyframeeffect--namedeffect-timeeffect) +- [Rule 2: transition / transitionProperties (StateEffect)](#rule-2-transition--transitionproperties-stateeffect) +- [Rule 3: customEffect (TimeEffect)](#rule-3-customeffect-timeeffect) +- [Rule 4: Sequences](#rule-4-sequences) + +--- + +## Rule 1: keyframeEffect / namedEffect (TimeEffect) + +${trigger.timeEffectIntro} + +**CRITICAL:** ${trigger.fillCritical} +${multipleEffectsNote} +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: '${name}', + effects: [ + { + key: '[TARGET_KEY]', + triggerType: '[TRIGGER_TYPE]', + + // --- pick ONE of the two effect types --- + keyframeEffect: { + name: '[EFFECT_NAME]', + keyframes: [KEYFRAMES], + }, + // OR + namedEffect: [NAMED_EFFECT_DEFINITION], + + fill: '[FILL_MODE]',${reversedField} + duration: [DURATION_MS], + easing: '[EASING_FUNCTION]', + delay: [DELAY_MS], + iterations: [ITERATIONS], + alternate: [ALTERNATE_BOOL]${hasEffectId ? ',' : ''}${effectIdField} +${rule1Closing} + ] +} +\`\`\` + +### Variables + +${variablesBlock} + +--- + +## Rule 2: transition / transitionProperties (StateEffect) + +${trigger.stateEffectIntro} + +Use \`transition\` when all properties share timing. Use \`transitionProperties\` when each property needs independent \`duration\`, \`delay\`, or \`easing\`. + +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: '${name}', + effects: [ + { + key: '[TARGET_KEY]', + stateAction: '[STATE_ACTION]', + + // --- pick ONE of the two transition forms --- + transition: { + duration: [DURATION_MS], + delay: [DELAY_MS], + easing: '[EASING_FUNCTION]', + styleProperties: [ + { name: '[CSS_PROP]', value: '[VALUE]' }, + // ... more properties + ] + }, + // OR (when each property needs its own timing) + transitionProperties: [ + { + name: '[CSS_PROP]', + value: '[VALUE]', + duration: [DURATION_MS], + delay: [DELAY_MS], + easing: '[EASING_FUNCTION]' + }, + // ... more properties + ] +${rule23Closing} + ] +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. +- \`[STATE_ACTION]\` — \`stateAction\` on the effect. One of: +${Object.entries(trigger.stateActionDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`).join('\n')} +- \`[CSS_PROP]\` — CSS property name as a string in camelCase format (e.g. \`'backgroundColor'\`, \`'borderRadius'\`, \`'opacity'\`). +- \`[VALUE]\` — target CSS value for the property. +- \`[DURATION_MS]\` — transition duration in milliseconds. +- \`[DELAY_MS]\` — optional transition delay in milliseconds. +- \`[EASING_FUNCTION]\` — CSS easing string, or named easing from \`@wix/motion\`. + +--- + +## Rule 3: customEffect (TimeEffect) + +${trigger.customEffectIntro} + +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: '${name}', + effects: [ + { + key: '[TARGET_KEY]', + triggerType: '[TRIGGER_TYPE]', + customEffect: [CUSTOM_EFFECT_CALLBACK], + duration: [DURATION_MS], + easing: '[EASING_FUNCTION]' +${rule23Closing} + ] +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` / \`[TRIGGER_TYPE]\` — same as Rule 1. +- \`[CUSTOM_EFFECT_CALLBACK]\` — ${trigger.customEffectCallbackDesc} +- \`[DURATION_MS]\` — animation duration in milliseconds. +- \`[EASING_FUNCTION]\` — CSS easing string, or named easing from \`@wix/motion\`. + +--- + +## Rule 4: Sequences + +${trigger.sequencesIntro} + +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: '${name}', + sequences: [ + { + triggerType: '[TRIGGER_TYPE]', + offset: [OFFSET_MS], + offsetEasing: '[OFFSET_EASING]', + effects: [ + [EFFECT_DEFINITION], + // .. more effects as necessary + ] + } + ] +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` / \`[TRIGGER_TYPE]\` — same as Rule 1. \`triggerType\` is set on the sequence config, not on individual effects within the sequence. +- \`[OFFSET_MS]\` — time offset for staggering each child's animation start, in milliseconds. +- \`[OFFSET_EASING]\` — ${trigger.sequenceOffsetEasingDesc} +- \`[EFFECT_DEFINITION]\` — ${trigger.sequenceEffectDefDesc} +`; +} + +function buildVariablesMidFill(trigger, fillModeVar, reversedVar, effectIdVar) { + const lines = [ + `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${trigger.sourceKeyDesc}`, + `- \`[TARGET_KEY]\` — ${trigger.targetKeyDesc}`, + `- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of:`, + ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`), + `- \`[KEYFRAMES]\` — array of keyframe objects (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase.`, + `- \`[EFFECT_NAME]\` — unique string identifier for a \`keyframeEffect\`.`, + `- \`[NAMED_EFFECT_DEFINITION]\` — ${trigger.namedEffectDesc}`, + fillModeVar, + ]; + if (reversedVar) lines.push(reversedVar.trim()); + lines.push( + `- \`[DURATION_MS]\` — animation duration in milliseconds.`, + `- \`[EASING_FUNCTION]\` — ${trigger.easingDesc}`, + `- \`[DELAY_MS]\` — optional delay before the effect starts, in milliseconds.`, + `- \`[ITERATIONS]\` — ${trigger.iterationsDesc}`, + `- \`[ALTERNATE_BOOL]\` — ${trigger.alternateDesc}`, + ); + if (effectIdVar) lines.push(effectIdVar.trim()); + return lines.join('\n'); +} + +function buildVariablesEndFill(trigger, fillModeVar, reversedVar, effectIdVar) { + const lines = [ + `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${trigger.sourceKeyDesc}`, + `- \`[TARGET_KEY]\` — ${trigger.targetKeyDesc}`, + `- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of:`, + ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`), + `- \`[KEYFRAMES]\` — array of keyframe objects (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase.`, + `- \`[EFFECT_NAME]\` — unique string identifier for a \`keyframeEffect\`.`, + `- \`[NAMED_EFFECT_DEFINITION]\` — ${trigger.namedEffectDesc}`, + `- \`[DURATION_MS]\` — animation duration in milliseconds.`, + `- \`[EASING_FUNCTION]\` — ${trigger.easingDesc}`, + `- \`[DELAY_MS]\` — optional delay before the effect starts, in milliseconds.`, + `- \`[ITERATIONS]\` — ${trigger.iterationsDesc}`, + `- \`[ALTERNATE_BOOL]\` — ${trigger.alternateDesc}`, + fillModeVar, + ]; + return lines.join('\n'); +} diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs new file mode 100644 index 00000000..884c0697 --- /dev/null +++ b/packages/interact/_content/templates/full-lean.mjs @@ -0,0 +1,604 @@ +export function render(triggers, data, fragments) { + const easingList = data.effects.easings.map(e => `\`'${e}'\``).join(', '); + + const presetEntries = Object.entries(data.effects.presets) + .map(([category, names]) => ({ + label: category.charAt(0).toUpperCase() + category.slice(1), + value: `\`${names.join('`, `')}\``, + })); + const maxPresetLen = Math.max(...presetEntries.map(e => e.value.length)); + const presetTable = presetEntries + .map(e => ` | ${e.label.padEnd(8)} | ${e.value.padEnd(maxPresetLen)} |`) + .join('\n'); + + const rangeEntries = Object.entries(data.effects.rangeNames) + .map(([name, desc]) => ({ name: `\`${name}\``, desc })); + const maxNameLen = Math.max(...rangeEntries.map(e => e.name.length)); + const maxDescLen = Math.max(...rangeEntries.map(e => e.desc.length)); + const rangeTable = rangeEntries + .map(e => `| ${e.name.padEnd(maxNameLen)} | ${e.desc.padEnd(maxDescLen)} |`) + .join('\n'); + + return `# ${data.meta.packageName} — Rules + +Declarative configuration-driven interaction library. Binds animations to triggers via JSON config. + +## Table of Contents + +- [Common Pitfalls](#common-pitfalls) +- [Quick Start](#quick-start) +- [Element Binding](#element-binding) +- [Config Structure](#config-structure) +- [Interactions](#interactions) +- [Triggers](#triggers) + - [hover / click](#hover--click) + - [viewEnter](#viewenter) + - [viewProgress](#viewprogress) + - [pointerMove](#pointermove) + - [animationEnd](#animationend) +- [Effects](#effects) + - [Time-based Effect](#time-based-effect) + - [Scroll / Pointer-driven Effect](#scroll--pointer-driven-effect) + - [State Effect](#stateeffect-css-style-toggle) + - [Animation Payloads](#animation-payloads) +- [Sequences](#sequences) +- [Conditions](#conditions) +- [FOUC Prevention](#fouc-prevention) +- [Element Resolution](#element-resolution) +- [Static API](#static-api) + +--- + +## Common Pitfalls + +Each item here is CRITICAL — ignoring any of them will break animations. + +${fragments.get('pitfalls/overflow-clip', 'long')} +${fragments.get('pitfalls/same-element-viewenter', 'long')} +${fragments.get('pitfalls/hit-area', 'full-lean-hover')} +${fragments.get('pitfalls/hit-area', 'full-lean-pointermove')} +${fragments.get('pitfalls/dont-guess-presets', 'default')} +${fragments.get('pitfalls/reduced-motion', 'default')} +${fragments.get('pitfalls/perspective', 'default')} + +--- + +## Quick Start + +${fragments.get('quick-start', 'install')} + +${fragments.get('quick-start', 'multiple-instances')} + +${fragments.get('quick-start', 'web')} + +${fragments.get('quick-start', 'react')} + +${fragments.get('quick-start', 'vanilla')} + +${fragments.get('quick-start', 'cdn')} + +${fragments.get('quick-start', 'register-presets')} + +--- + +## Element Binding + +**CRITICAL:** Do NOT add observers/event listeners manually. The runtime binds triggers and effects via element keys. + +### Web: \`\` + +- MUST set \`data-interact-key\` to a unique value. +- MUST contain at least one child element (the library targets \`.firstElementChild\`). +- If an effect targets a different element, that element also needs its own \`\`. + +\`\`\`html + +
...
+
+\`\`\` + +### React: \`\` component + +- MUST set \`tagName\` to the replaced element's HTML tag. +- MUST set \`interactKey\` to a unique string. + +\`\`\`tsx +import { Interaction } from '@wix/interact/react'; + + + ... +; +\`\`\` + +--- + +## Config Structure + +\`\`\`ts +type InteractConfig = { + interactions: Interaction[]; // REQUIRED + effects?: Record; // reusable effects referenced by effectId + sequences?: Record; // reusable sequences by sequenceId + conditions?: Record; // named conditions; keys are condition ids +}; +\`\`\` + +All cross-references (by id) MUST point to existing entries. Element keys MUST be stable for the config's lifetime. + +--- + +## Interactions + +Each interaction maps a source element + trigger to one or more effects. + +**Multiple effects per interaction:** A single interaction can contain multiple effects in its \`effects\` array. All effects in the same interaction share the same trigger — they all fire together when the trigger activates. Use this to apply different animations to different targets from the same trigger event, rather than creating separate interactions with duplicate trigger configs. + +\`\`\`ts +{ + key: string; // REQUIRED — matches data-interact-key / interactKey - the root element + trigger: TriggerType; // REQUIRED + params?: TriggerParams; // trigger-specific options + effects?: (Effect | EffectRef)[]; // possible to add multiple effects for same trigger + sequences?: (SequenceConfig | SequenceConfigRef)[]; // possible to add multiple sequences for same trigger + conditions?: string[]; // ids referencing the top-level conditions map; all must pass + selector?: string; // optional - CSS selector to refine source element selection within the root element + listContainer?: string; // optional — CSS selector for list container + listItemSelector?: string; // optional — CSS selector to filter which children of listContainer are observed as sources +} +\`\`\` + +At least one of \`effects\` or \`sequences\` MUST be provided. + +For most use cases, \`key\` alone is sufficient for both source and target resolution. The \`selector\`, \`listContainer\`, and \`listItemSelector\` fields are only needed for advanced patterns (lists, delegated triggers, child targeting). See [Element Resolution](#element-resolution) for details. + +--- + +## Triggers + +- **interactions: Interaction[]** + - **Purpose**: Declarative mapping from a source element and trigger to one or more target effects. + - Each \`Interaction\` contains: + - **key: string** + - REQUIRED. The source element path. The trigger attaches to this element. + - **listContainer?: string** + - OPTIONAL. A CSS selector for a list container context. When present, the trigger is scoped to items within this list. + - **listItemSelector?: string** + - OPTIONAL. A CSS selector used to select items within \`listContainer\`. + - **trigger: TriggerType** + - REQUIRED. One of: + - \`'hover' | 'click' | 'activate' | 'interest'\`: Pointer interactions (\`activate\` = click with keyboard Space/Enter; \`interest\` = hover with focus). + - \`'viewEnter' | 'viewProgress'\`: Viewport visibility/progress triggers. + - \`'animationEnd'\`: Fires when a specific effect completes on the source element. + - \`'pointerMove'\`: Continuous pointer motion over an area. + - **params?: TriggerParams** + - OPTIONAL. Parameter object that MUST match the trigger: + - hover/click/activate/interest: No params needed. Behavior is configured on the effect itself. + - viewEnter: \`ViewEnterParams\` + - \`threshold?\`: number in [0,1] describing intersection threshold + - \`inset?\`: string CSS-style inset for rootMargin/observer geometry + - viewProgress: No trigger params. Progress is driven by ViewTimeline/scroll scenes. Control the range via \`ScrubEffect.rangeStart/rangeEnd\` and \`namedEffect.range\`. + - animationEnd: \`AnimationEndParams\` + - \`effectId\`: string of the effect to wait for completion + - Usage: Fire when the specified effect (by \`effectId\`) on the source element finishes, useful for chaining sequences. + - pointerMove: \`PointerMoveParams\` + - \`hitArea?\`: \`'root' | 'self'\` (default \`'self'\`) + - \`axis?\`: \`'x' | 'y'\` - when using \`keyframeEffect\` with \`pointerMove\`, selects which pointer coordinate maps to linear 0-1 progress; defaults to \`'y'\`. Ignored for \`namedEffect\` and \`customEffect\`. + - Usage: + - \`'self'\`: Track pointer within the source element's bounds. + - \`'root'\`: Track pointer anywhere in the viewport (document root). + - Only use with \`ScrubEffect\` mouse presets (\`namedEffect\`) or \`customEffect\` that consumes pointer progress; avoid \`keyframeEffect\` with \`pointerMove\` unless mapping a single axis via \`axis\`. + - When using \`customEffect\` with \`pointerMove\`, the progress parameter is an object: + - \`\`\`typescript + type Progress = { + x: number; // 0-1: horizontal position (0 = left edge, 1 = right edge) + y: number; // 0-1: vertical position (0 = top edge, 1 = bottom edge) + v?: { + // Velocity (optional) + x: number; // Horizontal velocity + y: number; // Vertical velocity + }; + active?: boolean; // Whether mouse is currently in the hit area + }; + \`\`\` + +### hover / click + +For \`TimeEffect\` (keyframe/named/custom effects), set \`triggerType\` on the effect. For \`StateEffect\` (transitions), set \`stateAction\` on the effect. Do NOT mix \`triggerType\` and \`stateAction\` on the same effect. + +**\`triggerType\`** — on \`TimeEffect\`: + +| Type | hover behavior | click behavior | +| :---------------------- | :-------------------------------------- | :------------------------------- | +| \`'alternate'\` (default) | Play on enter, reverse on leave | Alternate play/reverse per click | +| \`'repeat'\` | Play on enter, stop and rewind on leave | Restart per click | +| \`'once'\` | Play once on first enter only | Play once on first click only | +| \`'state'\` | Play on enter, pause on leave | Toggle play/pause per click | + +**\`stateAction\`** — on \`StateEffect\`: + +| Action | hover behavior | click behavior | +| :------------------- | :---------------------------------------------- | :--------------------------- | +| \`'toggle'\` (default) | Add style state on enter, remove on leave | Toggle style state per click | +| \`'add'\` | Add style state on enter; leave does NOT remove | Add style state on click | +| \`'remove'\` | Remove style state on enter | Remove style state on click | +| \`'clear'\` | Clear/reset all style states on enter | Clear/reset all style states | + +### viewEnter + +\`\`\`ts +params: { + threshold?: number; // 0–1, IntersectionObserver threshold + inset?: string; // like view-timeline-inset, e.g. '-100px' or '-50px 0px' +} +// Playback behavior is set on each effect: +effect.triggerType: 'once' | 'repeat' | 'alternate' | 'state'; // default: 'once' +\`\`\` + +**CRITICAL:** When source and target are the **same element**, MUST use \`triggerType: 'once'\`. For \`'repeat'\` / \`'alternate'\` / \`'state'\`, ALWAYS use **separate** source and target elements — animating the observed element can cause it to leave/re-enter the viewport, causing rapid re-triggers. + +### viewProgress + +Scroll-driven animations using native \`ViewTimeline\`, with polyfill where not supported. Progress is driven by scroll position. Control the range via \`rangeStart\`/\`rangeEnd\` on the effect (see [Scroll / Pointer-driven Effect](#scroll--pointer-driven-effect)). + +\`viewProgress\` has no trigger params. Range configuration (\`rangeStart\`/\`rangeEnd\`) is on the effect, not on the trigger. + +**CRITICAL:** Replace ALL \`overflow: hidden\` with \`overflow: clip\` on every element between the trigger source and the scroll container. \`overflow: hidden\` creates a new scroll context that breaks ViewTimeline. In Tailwind replace \`overflow-hidden\` with \`overflow-clip\`. + +### pointerMove + +\`\`\`ts +params: { + hitArea?: 'self' | 'root'; // 'self' = source element bounds, 'root' = viewport + axis?: 'x' | 'y'; // restricts tracking to a single axis (for keyframeEffect) +} +\`\`\` + +**Rules:** + +- Source element MUST NOT have \`pointer-events: none\`. +- MUST NOT use the same element as both source and target with size or position effects — use \`selector\` to target a child or set a different \`key\`. +- Use a \`(hover: hover)\` media condition to disable on touch-only devices. On touch-only devices prefer \`viewEnter\` or \`viewProgress\` fallbacks. +- For 2D effects, use \`namedEffect\` mouse presets or \`customEffect\`. \`keyframeEffect\` only supports a single axis. +- For independent 2-axis control with keyframes, use two separate interactions (one \`axis: 'x'\`, one \`axis: 'y'\`) with \`composite: 'add'\` or \`'accumulate'\` on the second effect. + +**\`centeredToTarget\`** — set \`true\` to remap the \`0–1\` progress range so that \`0.5\` progress corresponds to the center of the target element. Use when source and target are different elements, or when \`hitArea: 'root'\` is used, so that the pointer resting over the target center produces 50% progress regardless of position in viewport. + +**Progress object** (for \`customEffect\`): + +\`\`\`ts +{ x: number; y: number; v?: { x: number; y: number }; active?: boolean } +// x, y: 0–1 normalized position within hit area +// v: velocity vector (unbounded, typically -1 to 1 range at moderate speed; 0 = stationary) +// active: whether pointer is within the active hit area +\`\`\` + +### animationEnd + +\`\`\`ts +params: { + effectId: string; +} // the effect to wait for +\`\`\` + +Fires when the specified effect completes on the source element. Useful for chaining sequences. + +--- + +## Effects + +Each effect applies a visual change to a target element. An effect is either inline or referenced by \`effectId\` from the top-level \`effects\` registry (\`EffectRef\`). An \`EffectRef\` inherits all properties from the registry entry, and can override any of them (e.g. \`key\`, \`duration\`, \`easing\`, \`fill\`, etc.) — not just the target. See [Element Resolution](#element-resolution) for how the target is determined. + +### Common fields + +\`\`\`ts +{ + key?: string; // target element key; omit to target the source + effectId?: string; // reference to effects registry (EffectRef) + conditions?: string[]; // ids referencing the top-level conditions map; all must pass + selector?: string; // optional — CSS selector to refine target element + listContainer?: string; // optional — CSS selector for list container + listItemSelector?: string; // optional — filter which children of listContainer are selected + composite?: 'replace' | 'add' | 'accumulate'; + fill?: 'none' | 'forwards' | 'backwards' | 'both'; +} +\`\`\` + +**\`fill\` guidance:** + +- \`'both'\` — ${data.effects.fillGuidance.both} +- \`'backwards'\` — ${data.effects.fillGuidance.backwards} + +**\`composite\`** — same as CSS's \`animation-composition\`. Controls how this effect combines with others on the same property (transforms & filters): + +- \`'replace'\` (default): ${data.effects.compositeOperations.replace} +- \`'add'\`: ${data.effects.compositeOperations.add} +- \`'accumulate'\`: ${data.effects.compositeOperations.accumulate} + +**\`easing\` guidance:** from \`@wix/motion\` (in addition to standard CSS easings): + +${easingList}, or any \`'cubic-bezier(...)'\` / \`'linear(...)'\` string. + +### Time-based Effect + +Used with \`hover\`, \`click\`, \`viewEnter\`, \`animationEnd\` triggers. + +\`\`\`ts +{ + duration: number; // REQUIRED (ms) + easing?: string; // CSS easing or named easing (see below) + delay?: number; // ms + iterations?: number; // >=1 or Infinity; 0 is treated as Infinity + alternate?: boolean; + reversed?: boolean; + fill?: 'none' | 'forwards' | 'backwards' | 'both'; + composite?: 'replace' | 'add' | 'accumulate'; + // + exactly one animation payload (see below) +} +\`\`\` + +### Scroll / Pointer-driven Effect + +Used with \`viewProgress\` and \`pointerMove\` triggers. + +\`\`\`ts +{ + rangeStart?: RangeOffset; // REQUIRED for viewProgress + rangeEnd?: RangeOffset; // REQUIRED for viewProgress + easing?: string; // CSS easing or named easing (see above) + iterations?: number; // NOT Infinity + alternate?: boolean; + reversed?: boolean; + fill?: 'none' | 'forwards' | 'backwards' | 'both'; + composite?: 'replace' | 'add' | 'accumulate'; + centeredToTarget?: boolean; + transitionDuration?: number; // ms, smoothing on progress jumps (primarily for pointerMove) + transitionDelay?: number; // ms (primarily for pointerMove) + transitionEasing?: '${data.effects.transitionEasings.join("' | '")}'; + // + exactly one animation payload (see below) +} +\`\`\` + +**RangeOffset** — works like CSS's \`animation-range\`: + +\`\`\`ts +{ + name?: 'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing'; + offset?: { value: number; unit: 'percentage' | 'px' | 'vh' | 'vw' } +} +\`\`\` + +| Range name | Meaning | +| :--------------- | :------------------------------------------------------------- | +${rangeTable} + +**Sticky container pattern** — for scroll-driven animations inside a stuck \`position: sticky\` container: + +- Tall wrapper: height defines scroll distance (e.g. \`300vh\` for ~2 viewport-heights of scroll travel). +- Sticky child (\`key\`) with \`position: sticky; top: 0; height: 100vh\`: stays fixed while the wrapper scrolls. This is the ViewTimeline source. +- Use \`rangeStart/rangeEnd\` with \`name: 'contain'\` to animate only during the stuck phase. + +### StateEffect (CSS style toggle) + +Used with \`hover\` / \`click\` triggers. Set \`stateAction\` on the effect to control state behavior. + +**StateEffect** (CSS transition-style state toggles): + +- \`key?\`: string (target override; see TARGET CASCADE) +- \`effectId?\`: string (when used as a reference identity) +- One of: + - \`transition?\`: \`{ duration?: number; delay?: number; easing?: string; styleProperties: { name: string; value: string }[] }\` + - Applies a single transition options block to all listed style properties. + - \`transitionProperties?\`: \`Array<{ name: string; value: string; duration?: number; delay?: number; easing?: string }>\` + - Allows per-property transition options. If both \`transition\` and \`transitionProperties\` are provided, the system SHOULD apply both with per-property entries taking precedence for overlapping properties. + +\`\`\`ts +// Shared timing for all properties: +{ + transition: { + duration?: number; delay?: number; easing?: string; + styleProperties: [{ name: string; value: string }] + } +} + +// Per-property timing: +{ + transitionProperties: [ + { name: string; value: string; duration?: number; delay?: number; easing?: string } + ] +} +\`\`\` + +CSS property names use **camelCase** (e.g. \`'backgroundColor'\`, \`'borderRadius'\`). + +### Animation Payloads + +Exactly one MUST be provided per time-based or scroll/pointer-driven effect: + +1. **\`namedEffect\`** (preferred) — pre-built presets from \`${data.meta.presetsPackage}\`. GPU-friendly and tuned. + + \`\`\`ts + namedEffect: { + type: '[PRESET_NAME]', + // ...optional [PRESET_OPTIONS] as additional properties + } + \`\`\` + + - \`[PRESET_NAME]\` — one of the registered preset names (see table below). + - \`[PRESET_OPTIONS]\` — optional preset-specific properties spread as additional keys on the object. **CRITICAL:** Do NOT guess option names/types. Omit unknown options and rely on defaults. + + Available presets: + + | Category | Presets | + | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +${presetTable} + - **CRITICAL** — Scroll presets (\`*Scroll\`) used with \`viewProgress\` MUST include \`range\` in options: \`'in'\` (ends at idle state), \`'out'\` (starts from idle state), or \`'continuous'\` (passes through idle). Prefer \`'continuous'\`. + - Mouse presets are preferred over \`keyframeEffect\` for \`pointerMove\` 2D effects. + +2. **\`keyframeEffect\`** — custom keyframe animations. + + \`\`\`ts + keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [KEYFRAMES] } + \`\`\` + + - \`[EFFECT_NAME]\` — unique string identifier for this effect. + - \`[KEYFRAMES]\` — array of keyframe objects using standard WAAPI format (e.g. \`[{ opacity: '0' }, { opacity: '1' }]\`). Property names in camelCase. + +3. **\`customEffect\`** — imperative update callback. Use only when CSS-based effects cannot express the desired behavior (e.g., animating SVG attributes, canvas, text content). + + \`\`\`ts + customEffect: [CUSTOM_EFFECT_CALLBACK]; + \`\`\` + + - \`[CUSTOM_EFFECT_CALLBACK]\` — function with signature \`(element: Element, progress: number | ProgressObject) => void\`. Called on each animation frame. + +--- + +## Sequences + +Coordinate multiple effects with staggered timing. Prefer sequences over manual delay stagger. + +### Sequence As type + +\`\`\`ts +{ + effects: (Effect | EffectRef)[]; // REQUIRED + delay?: number; // ms before sequence starts + offset?: number; // ms between each child's animation start + offsetEasing?: string; // easing curve for staggering offsets + sequenceId?: string; // for caching/referencing + conditions?: string[]; // ids referencing the top-level conditions map +} +\`\`\` + +### Template + +\`\`\`ts +{ + interactions: [ + { + key: '[SOURCE_KEY]', + trigger: '[TRIGGER]', + params: [TRIGGER_PARAMS], + sequences: [ + { + offset: [OFFSET_MS], // optional + offsetEasing: '[OFFSET_EASING]', // optional + delay: [DELAY_MS], // optional + effects: [ + // if used \`listContainer\` each item in the list is a target of a child effect + { + effectId: '[EFFECT_ID]', + listContainer: '[LIST_CONTAINER_SELECTOR]', + }, + // if multiple effects are given each generated effect is added to the sequence + ], + }, + ], + }, + ], + effects: { + '[EFFECT_ID]': { + // effect definition (namedEffect, keyframeEffect, or customEffect) + }, + }, +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for /vanilla, \`interactKey\` for React). +- \`[TRIGGER]\` — any trigger for time-based animation effects (e.g., \`'viewEnter'\`, \`'activate'\`, \`'interest'\`). +- \`[TRIGGER_PARAMS]\` — trigger-specific parameters (e.g., \`{ type: 'once', threshold: 0.3 }\`). +- \`[OFFSET_MS]\` — ms between each child's animation start. +- \`[OFFSET_EASING]\` — CSS easing string or named easing from \`@wix/motion\`. +- \`[DELAY_MS]\` — optional. Base delay (ms) before the entire sequence starts. +- \`[EFFECT_ID]\` — string key referencing an entry in the top-level \`effects\` map. +- \`[LIST_CONTAINER_SELECTOR]\` — optional. CSS selector for the container whose children will be staggered. + +Reusable sequences can be defined in \`InteractConfig.sequences\` and referenced by \`sequenceId\`. + +--- + +## Conditions + +Named conditions that gate interactions, effects, or sequences. + +| Type | Predicate | +| :--------- | :------------------------------------------------------------------------ | +| \`media\` | CSS media query condition without \`@media\` (e.g., \`'(min-width: 768px)'\`) | +| \`selector\` | CSS selector; \`&\` is replaced with the base element selector | + +Attach via \`conditions: ['[CONDITION_ID]']\` on interactions, effects, or sequences. On an interaction, conditions gate the entire trigger; on an effect, only that specific effect is skipped. All listed conditions must pass. + +### Examples + +\`\`\`ts +conditions: { + 'desktop': { type: 'media', predicate: '(min-width: 768px)' }, + 'hover-device': { type: 'media', predicate: '(hover: hover)' }, + 'reduced-motion': { type: 'media', predicate: '(prefers-reduced-motion: reduce)' }, + 'odd-items': { type: 'selector', predicate: ':nth-of-type(odd)' }, +} +\`\`\` + +--- + +## FOUC Prevention + +${fragments.get('fouc', 'long')} + +### Step 1: Generate CSS + +Call \`generate(config)\` server-side or at build time and inject the result into the \`\` (preferred), or insert to beginning of \`\`, so it loads before the page content is painted: + +${fragments.get('fouc', 'code-generate-web')} + +${fragments.get('fouc', 'code-inject')} + +### Step 2: Mark elements + +${fragments.get('fouc', 'code-web-hero')} + +${fragments.get('fouc', 'code-react-hero')} + +${fragments.get('fouc', 'code-vanilla-hero')} + +${fragments.get('fouc', 'rules-full-lean')} + +--- + +## Element Resolution + +${fragments.get('element-resolution', 'intro')} + +${fragments.get('element-resolution', 'source')} + +${fragments.get('element-resolution', 'target')} + +--- + +## Static API + +| Method / Property | Description | +| :---------------------------------- | :------------------------------------------------------------------------------------------------------------ | +| \`Interact.create(config)\` | Initialize with a config. Returns the instance. Store the instance to manage its lifecycle. | +| \`Interact.registerEffects(presets)\` | Register named effect presets. MUST be called before \`create\`. | +| \`Interact.destroy()\` | Tear down all instances. Call on unmount or route change to prevent memory leaks. | +| \`Interact.forceReducedMotion\` | \`boolean\` (default: \`false\`) — force reduced-motion behavior regardless of OS setting. | +| \`Interact.allowA11yTriggers\` | \`boolean\` (default: \`false\`) — enable accessibility trigger variants (\`interest\`, \`activate\`). | +| \`Interact.setup(options)\` | Configure global options for scroll, pointer, and viewEnter systems. Call before \`create\`. See options below. | + +**\`Interact.setup(options)\`** — optional configuration object: + +| Option | Type | Description | +| :--------------------- | :----------------------------- | :-------------------------------------------------------------------- | +| \`scrollOptionsGetter\` | \`() => Partial\` | Function returning defaults for scroll-driven animation configuration | +| \`pointerOptionsGetter\` | \`() => Partial\` | Function returning defaults for pointer-move animation configuration | +| \`viewEnter\` | \`Partial\` | Defaults for all viewEnter triggers (\`threshold\`,\`inset\`) | +| \`allowA11yTriggers\` | \`boolean\` | Enable accessibility trigger variants (use \`interest\` and \`activate\`) | + +Use \`setup()\` when you need to override default observer thresholds or provide global configuration that applies to all interactions of a given trigger type. + +Each \`Interact.create()\` call returns an instance. Store instances and call \`instance.destroy()\` when no longer needed (e.g. on component unmount) to prevent stale listeners and memory leaks. +`; +} diff --git a/packages/interact/_content/templates/integration.mjs b/packages/interact/_content/templates/integration.mjs new file mode 100644 index 00000000..6bb00cc5 --- /dev/null +++ b/packages/interact/_content/templates/integration.mjs @@ -0,0 +1,285 @@ +export function render(triggers, data, fragments) { + return `# ${data.meta.packageName} Integration Rules + +Rules for integrating \`${data.meta.packageName}\` into a webpage — binding animations and effects to user-driven triggers via declarative configuration. + +## Table of Contents + +- [Entry Points](#entry-points) + - [Web (Custom Elements)](#web-custom-elements) + - [React](#react) + - [Vanilla JS](#vanilla-js) +- [Named Effects & registerEffects](#named-effects--registereffects) +- [Configuration Schema](#configuration-schema) + - [InteractConfig](#interactconfig) + - [Interaction](#interaction) + - [Element Selection](#element-selection) +- [Triggers](#triggers) +- [Sequences](#sequences) +- [Critical CSS (FOUC Prevention)](#critical-css-fouc-prevention) +- [Static API](#static-api) + +--- + +## Entry Points + +Install with your package manager: + +${fragments.get('quick-start', 'install')} + +### Web (Custom Elements) + +${fragments.get('quick-start', 'web-integration')} + +Wrap target elements with \`\`: + +\`\`\`html + +
...
+
+\`\`\` + +**Rules:** + +- MUST set \`data-interact-key\` to a unique string within the page. +- MUST contain at least one child element (the library targets \`.firstElementChild\` by default). + +### React + +- Wrap the \`Interact.create()\` call in a \`useEffect\` hook to prevent it from running on server-side. +- Store the returned instance, and call its \`.destroy()\` method on the effect's cleanup function. + +\`\`\`typescript +import { useEffect } from 'react'; +import { Interact } from '@wix/interact/react'; + +useEffect(() => { + const instance = Interact.create(config); + + return () => { + instance.destroy(); + }; +}, [config]); +\`\`\` + +Replace target elements with \`\`: + +\`\`\`tsx +import { Interaction } from '@wix/interact/react'; + + + ... +; +\`\`\` + +**Rules:** + +- MUST set \`tagName\` to a valid HTML tag string for the element being replaced. +- MUST set \`interactKey\` to a unique string within the page. + +### Vanilla JS + +${fragments.get('quick-start', 'vanilla-integration')} + +**Rules:** + +- Call \`add(element, key)\` after elements exist in the DOM. +- Call \`remove(key)\` to unregister all interactions for a key. + +--- + +## Named Effects & registerEffects + +To use \`namedEffect\` presets from \`${data.meta.presetsPackage}\`, register them before calling \`Interact.create\`. For full effect type syntax (\`keyframeEffect\`, \`customEffect\`, \`StateEffect\`, \`ScrubEffect\`), see \`full-lean.md\`. + +**Install:** + +\`\`\`bash +> npm install ${data.meta.presetsPackage} +\`\`\` + +**Import and register:** + +\`\`\`typescript +import { Interact } from '${data.meta.entryPoints.web}'; +import * as presets from '${data.meta.presetsPackage}'; + +Interact.registerEffects(presets); +\`\`\` + +Or register selectively: + +\`\`\`typescript +import { FadeIn, ParallaxScroll } from '${data.meta.presetsPackage}'; +Interact.registerEffects({ FadeIn, ParallaxScroll }); +\`\`\` + +Then use in effects: + +\`\`\`typescript +{ namedEffect: { type: 'FadeIn' }, duration: 800, easing: 'ease-out' } +\`\`\` + +For full effect type syntax (\`keyframeEffect\`, \`namedEffect\`, \`customEffect\`, \`transition\`/\`transitionProperties\`), see [full-lean.md](./full-lean.md) and the trigger-specific rule files. + +--- + +## Configuration Schema + +### InteractConfig + +\`\`\`typescript +type InteractConfig = { + interactions: Interaction[]; + effects?: Record; + sequences?: Record; + conditions?: Record; +}; +\`\`\` + +| Field | Description | +| :------------- | :---------------------------------------------------------------------- | +| \`interactions\` | Required. Array of interaction definitions binding triggers to effects. | +| \`effects?\` | Reusable effects referenced by \`effectId\` from interactions. | +| \`sequences?\` | Reusable sequence definitions, referenced by \`sequenceId\`. | +| \`conditions?\` | Named conditions (media/container/selector queries), referenced by ID. | + +Each call to \`Interact.create(config)\` creates a new \`Interact\` instance. A single config can define multiple interactions. + +### Interaction + +\`\`\`typescript +{ + key: string; // REQUIRED — matches data-interact-key / interactKey + trigger: TriggerType; // REQUIRED — trigger type + params?: TriggerParams; // trigger-specific parameters + selector?: string; // CSS selector to refine target within the element + listContainer?: string; // CSS selector for a list container + listItemSelector?: string; // optional — CSS selector to filter which children of listContainer are selected + conditions?: string[]; // array of condition IDs; all must pass + effects?: Effect[]; // effects to apply + sequences?: SequenceConfig[]; // sequences to apply +} +\`\`\` + +At least one of \`effects\` or \`sequences\` MUST be provided. + +**Multiple effects per interaction:** A single interaction can contain multiple effects in its \`effects\` array. All effects share the same trigger — they fire together when the trigger activates. Use this to animate different targets from the same trigger event instead of duplicating interactions. + +### Element Selection + +**Most common**: Omit \`selector\`/\`listContainer\`/\`listItemSelector\` entirely — the element with the matching key is used as both source and target. Use \`selector\` to target a child element within the keyed element. Use \`listContainer\` for staggered sequences across list items. + +\`listItemSelector\` is **optional** — only use it when you need to **filter** which children of \`listContainer\` participate (e.g. select only \`.active\` items). When omitted, all immediate children of the \`listContainer\` are selected. + +${fragments.get('element-resolution', 'source-integration')} + +${fragments.get('element-resolution', 'target-integration')} + +--- + +## Triggers + +| Trigger | Description | Trigger \`params\` | Rules | +| :------------- | :------------------------------------- | :-------------------------------------------------------------------------------------- | :----------------------------------- | +| \`hover\` | Mouse enter/leave | No params. Set \`triggerType\` on TimeEffect or \`stateAction\` on StateEffect. | [hover.md](./hover.md) | +| \`click\` | Mouse click | Same as \`hover\` | [click.md](./click.md) | +| \`interest\` | Accessible hover (hover + focus) | Same as \`hover\` | [hover.md](./hover.md) | +| \`activate\` | Accessible click (click + Enter/Space) | Same as \`click\` | [click.md](./click.md) | +| \`viewEnter\` | Element enters viewport | \`threshold?\`; \`inset?\`. Set \`triggerType\` on TimeEffect or sequence config. | [viewenter.md](./viewenter.md) | +| \`viewProgress\` | Scroll-driven (ViewTimeline) | No trigger params. Configure \`rangeStart\`/\`rangeEnd\` on the **effect**, not on \`params\` | [viewprogress.md](./viewprogress.md) | +| \`pointerMove\` | Mouse movement | \`hitArea?\`: \`'self'\` \\| \`'root'\`; \`axis?\`: \`'x'\` \\| \`'y'\` | [pointermove.md](./pointermove.md) | +| \`animationEnd\` | Chain after another effect | \`effectId\`: ID of the preceding effect | — | + +For \`hover\`/\`click\` (and their accessible variants \`interest\`/\`activate\`): set \`triggerType\` on the effect for keyframe/named/custom effects (TimeEffect), or \`stateAction\` on the effect for transitions (StateEffect). Do not mix both on the same effect. + +--- + +## Sequences + +Sequences coordinate multiple effects with staggered timing. + +\`\`\`typescript +{ + offset: number, // ms between consecutive items + offsetEasing: string, // Any valid easing string for stagger distribution curve + delay: number, // ms base delay before the sequence starts + effects: [ + /* ... effect definitions */, + ], +} +\`\`\` + +Define reusable sequences in \`InteractConfig.sequences\` and reference by \`sequenceId\`: + +\`\`\`typescript +{ + sequences: { + 'stagger-fade': { + /* ... sequence definition */ + }, + }, + interactions: [ + { + key: \`'[SOURCE_KEY]'\`, + trigger: \`'[TRIGGER]'\`, + params: \`[TRIGGER_PARAMS]\`, + sequences: [{ sequenceId: 'stagger-fade' }], + }, + ], +} +\`\`\` + +--- + +## Critical CSS (FOUC Prevention) + +${fragments.get('fouc', 'integration-intro')} + +${fragments.get('fouc', 'rules-integration')} + +\`\`\`javascript +import { generate } from '${data.meta.entryPoints.web}'; +const css = generate(config); +\`\`\` + +${fragments.get('fouc', 'code-inject')} + +**Web:** + +\`\`\`html + +
...
+
+\`\`\` + +**React:** + +\`\`\`tsx + + ... + +\`\`\` + +**Vanilla:** + +\`\`\`html +
...
+\`\`\` + +--- + +## Static API + +Each \`Interact.create(config)\` call returns an instance. Keep a reference if you need to add/remove elements dynamically (vanilla JS) or to destroy a specific instance. Call \`Interact.destroy()\` to tear down all instances at once (e.g. on page navigation). + +| Method / Property | Description | +| :---------------------------------- | :------------------------------------------------------------------------------------------- | +| \`Interact.create(config)\` | Initialize with a config. Returns the instance. Multiple configs create separate instances. | +| \`Interact.registerEffects(presets)\` | Register named effect presets before \`create\`. Required for \`namedEffect\` usage. | +| \`Interact.destroy()\` | Tear down all instances. | +| \`Interact.forceReducedMotion\` | \`boolean\` — force reduced-motion behavior regardless of OS setting. Default: \`false\`. | +| \`Interact.allowA11yTriggers\` | \`boolean\` — enable accessibility triggers (\`interest\`, \`activate\`). Default: \`false\`. | +| \`Interact.setup(options)\` | Configure global defaults for scroll/pointer/viewEnter trigger params. Call before \`create\`. | +`; +} diff --git a/packages/interact/_content/templates/pointermove-rule.mjs b/packages/interact/_content/templates/pointermove-rule.mjs new file mode 100644 index 00000000..6af0c4af --- /dev/null +++ b/packages/interact/_content/templates/pointermove-rule.mjs @@ -0,0 +1,279 @@ +export function render(trigger, data, fragments) { + return `# PointerMove Trigger Rules for ${data.meta.packageName} + +These rules help generate pointer-driven interactions using \`${data.meta.packageName}\`. PointerMove triggers create real-time animations that respond to mouse movement over elements or the entire viewport. + +## Table of Contents + +- [Trigger Source Elements with \`hitArea: 'self'\`](#trigger-source-elements-with-hitarea-self) +- [PointerMoveParams](#pointermoveparams) +- [Progress Object Structure](#progress-object-structure) +- [Centering with \`centeredToTarget\`](#centering-with-centeredtotarget) +- [Device Conditions](#device-conditions) +- [Rule 1: namedEffect](#rule-1-namedeffect) +- [Rule 2: keyframeEffect with Single Axis](#rule-2-keyframeeffect-with-single-axis) +- [Rule 3: Two keyframeEffects with Two Axes and \`composite\`](#rule-3-two-keyframeeffects-with-two-axes-and-composite) +- [Rule 4: customEffect](#rule-4-customeffect) + +## Trigger Source Elements with \`hitArea: 'self'\` + +${fragments.get('pitfalls/hit-area', 'pointermove-source')} + +--- + +## PointerMoveParams + +\`params\` object for \`pointerMove\` interactions: + +\`\`\`typescript +type PointerMoveParams = { + hitArea?: 'root' | 'self'; + axis?: 'x' | 'y'; +}; +\`\`\` + +### Properties + +- \`hitArea\` — determines where mouse movement is tracked: + - \`'self'\` — tracks pointer within the source element's bounds only. Use for local pointer-tracking effects on a specific element. + - \`'root'\` — tracks pointer anywhere in the viewport. Use for global cursor followers, ambient effects. +- \`axis\` — restricts pointer tracking to a single axis. Used with \`keyframeEffect\` to map one axis to 0–1 progress; ignored by \`namedEffect\` and \`customEffect\` which receive the full 2D progress: + - \`'x'\` — maps horizontal pointer position to 0–1 progress for keyframe interpolation. + - \`'y'\` — maps vertical pointer position to 0–1 progress for keyframe interpolation. **Default** when \`keyframeEffect\` is used. + - For \`namedEffect\` or \`customEffect\` both axes are available via the 2D progress object, and will be ignored. + +--- + +## Progress Object Structure + +When using \`customEffect\` with \`pointerMove\`, the progress parameter is an object: + +\`\`\`typescript +type Progress = { + x: number; // 0-1: horizontal position (0 = left edge, 1 = right edge) + y: number; // 0-1: vertical position (0 = top edge, 1 = bottom edge) + v?: { + x: number; // Horizontal velocity: negative = moving left, positive = moving right. Magnitude reflects speed. + y: number; // Vertical velocity: negative = moving up, positive = moving down. Magnitude reflects speed. + }; + active?: boolean; // Whether mouse is currently in the hit area +}; +\`\`\` + +--- + +## Centering with \`centeredToTarget\` + +Controls which element's bounds define the 0–1 progress range. + +- **\`false\` (default)**: Progress is calculated against the **source element's** (or viewport's) bounds. The \`50%\` progress of the timeline is at the center of the source element. +- **\`true\`**: \`50%\` progress of the timeline is calculated against the **target element's center**. The edges of the timeline are still calculated against the edges of the source element/viewport depending on \`hitArea\`. + +--- + +## Device Conditions + +\`pointerMove\` works best on hover-capable devices. Use a \`conditions\` entry with a \`(hover: hover)\` media query to prevent the interaction from registering on touch-only devices. On touch-only devices, consider a fallback to \`viewEnter\` or \`viewProgress\` based interactions: + +\`\`\`typescript +{ + conditions: { + '[CONDITION_NAME]': { type: 'media', predicate: '(hover: hover)' } + }, + interactions: [ + { + key: '[SOURCE_KEY]', + trigger: 'pointerMove', + conditions: ['[CONDITION_NAME]'], + params: { hitArea: '[HIT_AREA]' }, + effects: [ /* ... */ ] + } + ] +} +\`\`\` + +For devices with dynamic viewport sizes (e.g. mobile browsers where the address bar collapses), consider using viewport-relative units carefully and prefer \`lvh\`/\`svh\` over \`dvh\` unless dynamic viewport behavior is specifically desired. + +--- + +## Rule 1: namedEffect + +Use pre-built mouse presets from \`@wix/motion-presets\` that handle 2D mouse tracking internally. Mouse presets are preferred over \`keyframeEffect\` for 2D effects. + +**Multiple effects:** The \`effects\` array can contain multiple effects — all share the same pointer tracking and fire together. Use this to animate different targets from the same pointer movement. + +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: 'pointerMove', + params: { + hitArea: '[HIT_AREA]' + }, + effects: [ + { + key: '[TARGET_KEY]', + namedEffect: { + type: '[NAMED_EFFECT_TYPE]', + [EFFECT_PROPERTIES] + }, + centeredToTarget: [CENTERED_TO_TARGET], + transitionDuration: [TRANSITION_DURATION_MS], + transitionEasing: '[TRANSITION_EASING]' + }, + // additional effects targeting other elements can be added here + ] +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). The element that tracks pointer movement. +- \`[TARGET_KEY]\` — identifier matching the element's key on the element to animate (can be same as source or different). +- \`[HIT_AREA]\` — \`'self'\` (track pointer within source element) or \`'root'\` (track pointer anywhere in viewport). +- \`[NAMED_EFFECT_TYPE]\` — a registered effect name, or a preset from \`@wix/motion-presets\` \`mouse\` library. +- \`[EFFECT_PROPERTIES]\` — preset-specific options. Refer to motion-presets rules for each preset's available options and their value types. Do NOT guess preset option names or types; omit unknown options and rely on defaults. +- \`[CENTERED_TO_TARGET]\` — \`true\` or \`false\`. See **Centering with \`centeredToTarget\`** above. +- \`[TRANSITION_DURATION_MS]\` — optional number. Milliseconds for smoothing (interpolating) between progress updates. The animation does not jump to the new progress value instantly; instead it transitions over this duration. Use to add inertia/lag to the effect, making it feel more physical (e.g. \`200\`–\`600\`). +- \`[TRANSITION_EASING]\` — optional string. CSS easing or named easing from \`@wix/motion\`. Adds a natural deceleration feel when used with \`transitionDuration\`. + +--- + +## Rule 2: keyframeEffect with Single Axis + +Use \`keyframeEffect\` when the pointer position along a single axis should drive a keyframe animation. The pointer's position on the chosen axis is mapped to linear 0–1 progress. + +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: 'pointerMove', + params: { + hitArea: '[HIT_AREA]', + axis: '[AXIS]' + }, + effects: [ + { + key: '[TARGET_KEY]', + keyframeEffect: { + name: '[EFFECT_NAME]', + keyframes: [KEYFRAMES] + }, + fill: 'both', + centeredToTarget: [CENTERED_TO_TARGET], + transitionDuration: [TRANSITION_DURATION_MS], + transitionEasing: '[TRANSITION_EASING]', + effectId: '[UNIQUE_EFFECT_ID]' + }, + // additional effects targeting other elements can be added here + ] +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. +- \`[HIT_AREA]\` — \`'self'\` or \`'root'\`. +- \`[AXIS]\` — \`'x'\` (horizontal) or \`'y'\` (vertical). Defaults to \`'y'\` when omitted. +- \`[EFFECT_NAME]\` — unique string name for the keyframe effect. +- \`[KEYFRAMES]\` — array of CSS keyframe objects (e.g. \`[{ transform: 'rotate(-10deg)' }, { transform: 'rotate(0)' }, { transform: 'rotate(10deg)' }]\`). Distributed evenly across 0–1 progress: first keyframe = progress 0 (left/top edge), last = progress 1 (right/bottom edge). Any number of keyframes is allowed. +- \`[CENTERED_TO_TARGET]\` — optional. \`true\` or \`false\`. See **Centering with \`centeredToTarget\`** above. Defaults to \`false\`. +- \`[TRANSITION_DURATION_MS]\` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details. +- \`[TRANSITION_EASING]\` — optional. CSS easing string or named easing from \`@wix/motion\`. See Rule 1 for supported values. +- \`[UNIQUE_EFFECT_ID]\` — optional string identifier. + +--- + +## Rule 3: Two keyframeEffects with Two Axes and \`composite\` + +Use two separate interactions on the same source/target pair — one for \`axis: 'x'\`, one for \`axis: 'y'\` — for independent 2D control with keyframes. When both effects animate the same CSS property (e.g. \`transform\` or \`filter\`), use \`composite\` to combine them. + +\`\`\`typescript +{ + interactions: [ + { + key: '[SOURCE_KEY]', + trigger: 'pointerMove', + params: { hitArea: '[HIT_AREA]', axis: 'x' }, + effects: [{ key: '[TARGET_KEY]', effectId: '[X_EFFECT_ID]' }] + }, + { + key: '[SOURCE_KEY]', + trigger: 'pointerMove', + params: { hitArea: '[HIT_AREA]', axis: 'y' }, + effects: [{ key: '[TARGET_KEY]', effectId: '[Y_EFFECT_ID]' }] + } + ], + effects: { + '[X_EFFECT_ID]': { + keyframeEffect: { + name: '[X_EFFECT_NAME]', + keyframes: [X_KEYFRAMES] + }, + fill: '[FILL_MODE]', // usually 'both' + composite: '[COMPOSITE_OPERATION]', + transitionDuration: [TRANSITION_DURATION_MS], + transitionEasing: '[TRANSITION_EASING]' + }, + '[Y_EFFECT_ID]': { + keyframeEffect: { + name: '[Y_EFFECT_NAME]', + keyframes: [Y_KEYFRAMES] + }, + fill: '[FILL_MODE]', // usually 'both' + composite: '[COMPOSITE_OPERATION]', + transitionDuration: [TRANSITION_DURATION_MS], + transitionEasing: '[TRANSITION_EASING]' + } + } +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. +- \`[HIT_AREA]\` — \`'self'\` or \`'root'\`. +- \`[X_EFFECT_ID]\` / \`[Y_EFFECT_ID]\` — unique string identifiers for the X-axis and Y-axis effects. Required — they map to keys in the top-level \`effects\` map. +- \`[X_EFFECT_NAME]\` / \`[Y_EFFECT_NAME]\` — unique string names for each keyframe effect. +- \`[X_KEYFRAMES]\` / \`[Y_KEYFRAMES]\` — arrays of WAAPI keyframe objects for the X-axis and Y-axis effects respectively. Each effect can vary in propertise and keyframes. +- \`[COMPOSITE_OPERATION]\` — \`'add'\` or \`'accumulate'\`. Required when both effects animate \`transform\` and/or both animate \`filter\`, so their values combine rather than override. \`'add'\`: composited transform functions are appended. \`'accumulate'\`: matching function arguments are summed. +- \`[FILL_MODE]\` — typically \`'both'\` to ensure the effect keeps applying after exiting the effect's active range. +- \`[TRANSITION_DURATION_MS]\` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details. +- \`[TRANSITION_EASING]\` — optional. CSS easing function for the smoothing transition. See Rule 1 for supported values. + +--- + +## Rule 4: customEffect + +Use \`customEffect\` when you need full imperative control over pointer-driven animations — custom physics, complex multi-property animations, velocity-reactive effects, or controlling WebGL/WebGPU and other JavaScript-driven effects. The callback receives the 2D progress object (see **Progress Object Structure**). + +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: 'pointerMove', + params: { + hitArea: '[HIT_AREA]' + }, + effects: [ + { + key: '[TARGET_KEY]', + customEffect: (element: Element, progress: Progress) => { + [CUSTOM_ANIMATION_LOGIC] + }, + centeredToTarget: [CENTERED_TO_TARGET], + transitionDuration: [TRANSITION_DURATION_MS], + transitionEasing: '[TRANSITION_EASING]' + }, + // additional effects targeting other elements can be added here + ] +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. +- \`[HIT_AREA]\` — \`'self'\` or \`'root'\`. +- \`[CUSTOM_ANIMATION_LOGIC]\` — JavaScript using \`progress.x\`, \`progress.y\`, \`progress.v\`, and \`progress.active\` to apply the effect. See **Progress Object Structure** above. +- \`[CENTERED_TO_TARGET]\` — optional. \`true\` or \`false\`. See **Centering with \`centeredToTarget\`** above. Defaults to \`false\`. +- \`[TRANSITION_DURATION_MS]\` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details. +- \`[TRANSITION_EASING]\` — optional. CSS easing function for the smoothing transition. See Rule 1 for supported values. +`; +} diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs new file mode 100644 index 00000000..6e975531 --- /dev/null +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -0,0 +1,173 @@ +export function render(trigger, data, fragments) { + return `# ViewEnter Trigger Rules for ${data.meta.packageName} + +This document contains rules for generating interactions that respond to elements entering the viewport using the \`${data.meta.packageName}\`. ViewEnter triggers use IntersectionObserver to detect when elements become visible and are ideal for entrance animations, content reveals, and lazy-loading effects. + +--- + +${fragments.get('pitfalls/same-element-viewenter', 'short')} + +## Table of Contents + +- [Preventing Flash of Unstyled Content (FOUC)](#preventing-flash-of-unstyled-content-fouc) +- [Rule 1: keyframeEffect / namedEffect (TimeEffect)](#rule-1-keyframeeffect--namedeffect-timeeffect) +- [Rule 2: customEffect (TimeEffect)](#rule-2-customeffect-timeeffect) +- [Rule 3: Sequences](#rule-3-sequences) + +--- + +## Preventing Flash of Unstyled Content (FOUC) + +${fragments.get('fouc', 'short')} + +### Step 1: Generate CSS and inject into \`\` (preferred), or beginning of \`\` + +Call \`generate(config)\` server-side or at build time. Inject the resulting CSS into the document \`\` (or in \`\` before your content) so it loads before the page content is painted: + +${fragments.get('fouc', 'code-generate-viewenter')} + +${fragments.get('fouc', 'code-inject')} + +### Step 2: Mark elements with \`initial\` + +${fragments.get('fouc', 'code-web')} + +${fragments.get('fouc', 'code-react')} + +${fragments.get('fouc', 'code-vanilla')} + +${fragments.get('fouc', 'rules-viewenter')} + +## Rule 1: keyframeEffect / namedEffect (TimeEffect) + +Use \`keyframeEffect\` or \`namedEffect\` when the viewEnter should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior. Use \`params\` only for observer configuration (\`threshold\`, \`inset\`). + +**Multiple effects:** The \`effects\` array can contain multiple effects — all share the same viewEnter trigger and fire together when the element enters the viewport. Each effect can have its own \`triggerType\`. Use this to animate different targets from a single viewport entry event. + +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: 'viewEnter', + params: { + threshold: [VISIBILITY_THRESHOLD], + inset: '[VIEWPORT_INSETS]' + }, + effects: [ + { + key: '[TARGET_KEY]', + selector: '[TARGET_SELECTOR]', + triggerType: '[TRIGGER_TYPE]', + + // --- pick ONE of the two effect types --- + keyframeEffect: { + name: '[EFFECT_NAME]', + keyframes: [KEYFRAMES], + }, + // OR + namedEffect: [NAMED_EFFECT_DEFINITION], + + fill: '[FILL_MODE]', + duration: [DURATION_MS], + easing: '[EASING_FUNCTION]', + delay: [DELAY_MS], + iterations: [ITERATIONS], + alternate: [ALTERNATE_BOOL], + effectId: '[UNIQUE_EFFECT_ID]' + }, + // additional effects targeting other elements can be added here + ] +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web/vanilla, \`interactKey\` for React). The **source element** is observed for viewport intersection. This is the element the IntersectionObserver watches. +- \`[TARGET_KEY]\` — identifier matching the element's key on the element that animates. +- \`[TARGET_SELECTOR]\` - optional. Selector for the child element to select inside the root element. For \`triggerType\` of \`'alternate'\`/\`'repeat'\`/\`'state'\` MUST either use a separate \`[TARGET_KEY]\` from \`[SOURCE_KEY]\` or \`selector\` for selecting a child element as target. +- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of: + - \`'once'\` (default) — plays once when the source element first enters the viewport and never again. Source and target may be the same element. + - \`'repeat'\` — restarts the animation every time the source element enters the viewport. Use separate source and target. + - \`'alternate'\` — plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target. + - \`'state'\` — resumes on enter, pauses on leave. Useful for continuous loops (\`iterations: Infinity\`). Use separate source and target. +- \`[VISIBILITY_THRESHOLD]\` — optional. Number between 0–1 indicating how much of the source element must be visible to trigger (e.g. \`0.3\` = 30%). +- \`[VIEWPORT_INSETS]\` — optional. String adjusting the viewport detection area (e.g. \`'-100px'\` extends it, \`'50px'\` shrinks it). +- \`[KEYFRAMES]\` — array of keyframe objects (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase. +- \`[EFFECT_NAME]\` — unique string identifier for a \`keyframeEffect\`. +- \`[NAMED_EFFECT_DEFINITION]\` — object with properties of pre-built effect from \`@wix/motion-presets\`. Refer to motion-presets rules for available presets and their options. +- \`[FILL_MODE]\` — \`'both'\` for \`triggerType: 'alternate'\`, \`'repeat'\`, or \`'state'\`. For \`triggerType: 'once'\`: use \`'backwards'\` when the animation's final keyframe has no additional effect (over element's base style); use \`'both'\` otherwise. +- \`[DURATION_MS]\` — animation duration in milliseconds. +- \`[EASING_FUNCTION]\` — CSS easing string or named easing from \`@wix/motion\`. +- \`[DELAY_MS]\` — optional delay before the effect starts, in milliseconds. +- \`[ITERATIONS]\` — optional. Number of iterations, or \`Infinity\` for continuous loops. Primarily useful with \`triggerType: 'state'\`. +- \`[ALTERNATE_BOOL]\` — optional. \`true\` to alternate direction on every other iteration (within a single playback). +- \`[UNIQUE_EFFECT_ID]\` — optional. String identifier used by \`animationEnd\` triggers for chaining, and by sequences for referencing effects. + +--- + +## Rule 2: customEffect (TimeEffect) + +Use \`customEffect\` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the target element and a \`progress\` value (0–1) driven by the animation timeline. + +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: 'viewEnter', + params: { + threshold: [VISIBILITY_THRESHOLD], + inset: '[VIEWPORT_INSETS]' + }, + effects: [ + { + key: '[TARGET_KEY]', + triggerType: '[TRIGGER_TYPE]', + customEffect: [CUSTOM_EFFECT_CALLBACK], + duration: [DURATION_MS], + easing: '[EASING_FUNCTION]', + effectId: '[UNIQUE_EFFECT_ID]' + } + ] +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` / \`[TRIGGER_TYPE]\` / \`[VISIBILITY_THRESHOLD]\` / \`[VIEWPORT_INSETS]\` / \`[DURATION_MS]\` / \`[EASING_FUNCTION]\` / \`[UNIQUE_EFFECT_ID]\` — same as Rule 1. +- \`[CUSTOM_EFFECT_CALLBACK]\` — function with signature \`(element: HTMLElement, progress: number) => void\`. Called on each animation frame with \`element\` being the target element, and \`progress\` from 0 to 1. + +--- + +## Rule 3: Sequences + +Use sequences when a viewEnter should sync/stagger animations across multiple elements. Set \`triggerType\` on the sequence config to control playback behavior. + +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: 'viewEnter', + params: { + threshold: [VISIBILITY_THRESHOLD], + inset: '[VIEWPORT_INSETS]' + }, + sequences: [ + { + triggerType: '[TRIGGER_TYPE]', + offset: [OFFSET_MS], + offsetEasing: '[OFFSET_EASING]', + effects: [ + [EFFECT_DEFINITION], + // .. more effects as necessary + ] + } + ] +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` / \`[VISIBILITY_THRESHOLD]\` / \`[VIEWPORT_INSETS]\` — same as Rule 1. +- \`[TRIGGER_TYPE]\` — same as Rule 1. \`triggerType\` is set on the sequence config, not on individual effects within the sequence. +- \`[OFFSET_MS]\` — time offset between each child's animation start, in milliseconds. +- \`[OFFSET_EASING]\` — CSS easing or named easing from \`@wix/motion\`, for the stagger distribution. Defaults to \`'linear'\`. +- \`[EFFECT_DEFINITION]\` — a definition of or a reference to a time-based animation effect. +`; +} diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs new file mode 100644 index 00000000..9c7a4217 --- /dev/null +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -0,0 +1,147 @@ +export function render(trigger, data, fragments) { + return `# ViewProgress Trigger Rules for ${data.meta.packageName} + +These rules help generate scroll-driven interactions using \`${data.meta.packageName}\`. ViewProgress triggers create animations that update continuously as elements move through the viewport, leveraging native CSS ViewTimelines where supported, and using a polyfill library where unsupported. Use when animation progress should be tied to the element's scroll position. + +${fragments.get('pitfalls/overflow-clip', 'short')} + +**Offset semantics:** The \`offset\` inside \`rangeStart\`/\`rangeEnd\` is an object \`{ unit: 'percentage', value: NUMBER }\` where value is 0–100. For absolute lengths use \`{ unit: 'px', value: NUMBER }\` (or other CSS length units). Positive values move the effective range boundary forward along the scroll axis. + +## Table of Contents + +- [Rule 1: ViewProgress with keyframeEffect or namedEffect](#rule-1-viewprogress-with-keyframeeffect-or-namedeffect) +- [Rule 2: ViewProgress with customEffect](#rule-2-viewprogress-with-customeffect) +- [Rule 3: ViewProgress with Tall Wrapper + Sticky Container (contain range)](#rule-3-viewprogress-with-tall-wrapper--sticky-container-contain-range) + +--- + +## Rule 1: ViewProgress with keyframeEffect or namedEffect + +**Use Case**: Scroll-driven CSS-based effects. + +**Multiple effects:** The \`effects\` array can contain multiple effects — all are driven by the same scroll progress. Use this to animate different targets or properties in sync with the same scroll position. + +### Template + +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: 'viewProgress', + effects: [ + { + key: '[TARGET_KEY]', + // --- pick ONE of the two effect types --- + namedEffect: [NAMED_EFFECT_DEFINITION], + // OR + keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [EFFECT_KEYFRAMES] }, + + rangeStart: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } }, + rangeEnd: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } }, + easing: '[EASING_FUNCTION]', // usually 'linear' + fill: 'both', + effectId: '[UNIQUE_EFFECT_ID]' + }, + // additional effects targeting other elements can be added here + ] +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). The element whose scroll position drives the animation. +- \`[TARGET_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React) on the element to animate (can be same as source or different). +- \`[NAMED_EFFECT_DEFINITION]\` — object with properties of pre-built effect from \`@wix/motion-presets\`. **CRITICAL:** Scroll presets (\`*Scroll\`) MUST include \`range: 'in' | 'out' | 'continuous'\` in their options. \`'in'\` ends at the idle state, \`'out'\` starts from the idle state, \`'continuous'\` passes through it. +- \`[EFFECT_NAME]\` — unique name for custom keyframe effect. +- \`[EFFECT_KEYFRAMES]\` — array of keyframe objects defining CSS property values (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase. +- \`[RANGE_NAME]\` — scroll range name: + - \`'cover'\` — full visibility span from first pixel entering to last pixel leaving. + - \`'entry'\` — the phase while the element is entering the viewport. + - \`'exit'\` — the phase while the element is exiting the viewport. + - \`'contain'\` — while the element is fully contained in the viewport. Typically used with a \`position: sticky\` container. + - \`'entry-crossing'\` — from the element's leading edge entering to its leading edge reaching the opposite side. + - \`'exit-crossing'\` — from the element's trailing edge reaching the start to its trailing edge leaving. +- \`[START_PERCENTAGE]\` — 0–100, starting point within the named range. +- \`[END_PERCENTAGE]\` — 0–100, end point within the named range. +- \`[EASING_FUNCTION]\` - CSS easing string or named easing from \`@wix/motion\`. Typically \`'linear'\` for scrolling effects. +- \`[UNIQUE_EFFECT_ID]\` — optional identifier for referencing the effect externally. + +--- + +## Rule 2: ViewProgress with customEffect + +**Use Case**: Scroll-driven effects requiring JavaScript logic (e.g., changing SVG attributes, controlling WebGL/WebGPU effects). + +### Template + +\`\`\`typescript +{ + key: '[SOURCE_KEY]', + trigger: 'viewProgress', + effects: [ + { + key: '[TARGET_KEY]', + customEffect: [CUSTOM_EFFECT_CALLBACK], + rangeStart: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } }, + rangeEnd: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } }, + easing: \`'[EASING_FUNCTION]'\`, // usually 'linear' + fill: 'both', + effectId: '[UNIQUE_EFFECT_ID]' + }, + // additional effects targeting other elements can be added here + ] +} +\`\`\` + +### Variables + +- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. +- \`[CUSTOM_EFFECT_CALLBACK]\` — function with signature \`(element: HTMLElement, progress: number) => void\`. Called on each animation frame with \`progress\` from 0 to 1. +- \`[RANGE_NAME]\` / \`[START_PERCENTAGE]\` / \`[END_PERCENTAGE]\` — same as Rule 1. +- \`[EASING_FUNCTION]\` — CSS easing string or named easing from \`@wix/motion\`. Typically \`'linear'\` for scrolling effects. +- \`[UNIQUE_EFFECT_ID]\` — optional identifier for referencing the effect externally. + +--- + +## Rule 3: ViewProgress with Tall Wrapper + Sticky Container (contain range) + +**Use Case**: Scroll-driven animations inside a sticky-positioned container, where the source element is a tall wrapper and the effect applies during the "stuck" phase using \`position: sticky\` to lock a container and \`contain\` range to animate only during the stuck phase. Good for heavy effects on large media elements or scrolly-telling effects. + +**Layout Structure**: + +- **Tall wrapper** (\`[TALL_WRAPPER_KEY]\`): An element with enough height to create scroll distance (e.g., \`height: 300vh\`). This is the ViewTimeline source. The taller it is relative to the viewport, the longer the scroll distance and the more "duration" the animation has. +- **Sticky container**: A direct child with \`position: sticky; top: 0; height: 100vh\` that stays fixed in the viewport while the wrapper scrolls past. +- **Animated elements** (\`[STICKY_CHILD_KEY]\`): Children of the sticky container that receive the effects. + +### Template + +\`\`\`typescript +{ + key: '[TALL_WRAPPER_KEY]', + trigger: 'viewProgress', + effects: [ + { + key: '[STICKY_CHILD_KEY]', + // Use keyframeEffect, namedEffect, or customEffect as in Rules 1–2 + keyframeEffect: { name: '[EFFECT_NAME]', keyframes: [EFFECT_KEYFRAMES] }, + rangeStart: { name: 'contain', offset: { unit: 'percentage', value: [START_PERCENTAGE] } }, + rangeEnd: { name: 'contain', offset: { unit: 'percentage', value: [END_PERCENTAGE] } }, + easing: '[EASING_FUNCTION]', // usually 'linear' + fill: 'both', + effectId: '[UNIQUE_EFFECT_ID]' + }, + // additional effects targeting other elements can be added here + ] +} +\`\`\` + +### Variables + +- \`[TALL_WRAPPER_KEY]\` — key for the tall outer element that defines the scroll distance — this is the ViewTimeline source. +- \`[STICKY_CHILD_KEY]\` — key for the animated element inside the sticky container. +- \`[EFFECT_NAME]\` / \`[EFFECT_KEYFRAMES]\` — same as Rule 1. +- \`[START_PERCENTAGE]\` — 0–100, starting point within the \`contain\` range (the stuck phase). +- \`[END_PERCENTAGE]\` — 0–100, end point within the \`contain\` range. +- \`[UNIQUE_EFFECT_ID]\` — same as Rule 1. +- \`[EASING_FUNCTION]\` — CSS easing string or named easing from \`@wix/motion\`. Typically \`'linear'\` for scrolling effects. +`; +} diff --git a/packages/interact/package.json b/packages/interact/package.json index 1cc31494..10b61229 100644 --- a/packages/interact/package.json +++ b/packages/interact/package.json @@ -33,6 +33,7 @@ "dev": "vite dev --open", "build": "rimraf dist && vite build && npm run build:types", "build:landing": "../../scripts/build-landing.sh", + "build:rules": "node scripts/build-rules.mjs", "build:types": "tsc -p tsconfig.build.json", "lint": "tsc --noEmit", "test": "vitest run", @@ -89,6 +90,7 @@ "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^4.0.14", + "js-yaml": "^4.1.0", "jsdom": "^24.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/packages/interact/rules/click.md b/packages/interact/rules/click.md index 83a2e0d8..ba94219f 100644 --- a/packages/interact/rules/click.md +++ b/packages/interact/rules/click.md @@ -173,7 +173,7 @@ Use sequences when a click should sync/stagger animations across multiple elemen offset: [OFFSET_MS], offsetEasing: '[OFFSET_EASING]', effects: [ - [EFFECT_DEFINTION], + [EFFECT_DEFINITION], // .. more effects as necessary ] } @@ -186,4 +186,4 @@ Use sequences when a click should sync/stagger animations across multiple elemen - `[SOURCE_KEY]` / `[TRIGGER_TYPE]` — same as Rule 1. `triggerType` is set on the sequence config, not on individual effects within the sequence. - `[OFFSET_MS]` — time offset for staggering each child's animation start, in milliseconds. - `[OFFSET_EASING]` — easing curve for the offset staggering distribution. Defaults to `'linear'`. -- `[EFFECT_DEFINTION]` — a definition of, or a reference to a time-based animation effect. +- `[EFFECT_DEFINITION]` — a definition of, or a reference to a time-based animation effect. diff --git a/packages/interact/rules/full-lean.md b/packages/interact/rules/full-lean.md index cb13b9df..43b4b131 100644 --- a/packages/interact/rules/full-lean.md +++ b/packages/interact/rules/full-lean.md @@ -215,7 +215,7 @@ For most use cases, `key` alone is sufficient for both source and target resolut - `hitArea?`: `'root' | 'self'` (default `'self'`) - `axis?`: `'x' | 'y'` - when using `keyframeEffect` with `pointerMove`, selects which pointer coordinate maps to linear 0-1 progress; defaults to `'y'`. Ignored for `namedEffect` and `customEffect`. - Usage: - - `'self'`: Track pointer within the source element’s bounds. + - `'self'`: Track pointer within the source element's bounds. - `'root'`: Track pointer anywhere in the viewport (document root). - Only use with `ScrubEffect` mouse presets (`namedEffect`) or `customEffect` that consumes pointer progress; avoid `keyframeEffect` with `pointerMove` unless mapping a single axis via `axis`. - When using `customEffect` with `pointerMove`, the progress parameter is an object: diff --git a/packages/interact/rules/hover.md b/packages/interact/rules/hover.md index f2f5d3b6..9fcfb2fe 100644 --- a/packages/interact/rules/hover.md +++ b/packages/interact/rules/hover.md @@ -175,7 +175,7 @@ Use sequences when a hover should sync/stagger animations across multiple elemen offset: [OFFSET_MS], offsetEasing: '[OFFSET_EASING]', effects: [ - [EFFECT_DEFINTION], + [EFFECT_DEFINITION], // .. more effects as necessary ] } @@ -188,4 +188,4 @@ Use sequences when a hover should sync/stagger animations across multiple elemen - `[SOURCE_KEY]` / `[TRIGGER_TYPE]` — same as Rule 1. `triggerType` is set on the sequence config, not on individual effects within the sequence. - `[OFFSET_MS]` — time offset for staggering each child's animation start, in milliseconds. - `[OFFSET_EASING]` — easing curve for the offset staggering distribution. CSS easing string, or named easing from `@wix/motion`. Defaults to `'linear'`. -- `[EFFECT_DEFINTION]` — a definition of or a reference to a time-based animation effect. +- `[EFFECT_DEFINITION]` — a definition of or a reference to a time-based animation effect. diff --git a/packages/interact/rules/pointermove.md b/packages/interact/rules/pointermove.md index d471ea29..06f347eb 100644 --- a/packages/interact/rules/pointermove.md +++ b/packages/interact/rules/pointermove.md @@ -69,7 +69,7 @@ type Progress = { Controls which element's bounds define the 0–1 progress range. - **`false` (default)**: Progress is calculated against the **source element's** (or viewport's) bounds. The `50%` progress of the timeline is at the center of the source element. -- **`true`**: `50%` progress of the timeline is calculated against the **target element's center**. The edges of the timeline are still calculated against the edges of the source element/viewport depending on `hitAea`. +- **`true`**: `50%` progress of the timeline is calculated against the **target element's center**. The edges of the timeline are still calculated against the edges of the source element/viewport depending on `hitArea`. --- diff --git a/packages/interact/rules/viewenter.md b/packages/interact/rules/viewenter.md index e6cf1171..a8a73f82 100644 --- a/packages/interact/rules/viewenter.md +++ b/packages/interact/rules/viewenter.md @@ -207,7 +207,7 @@ Use sequences when a viewEnter should sync/stagger animations across multiple el offset: [OFFSET_MS], offsetEasing: '[OFFSET_EASING]', effects: [ - [EFFECT_DEFINTION], + [EFFECT_DEFINITION], // .. more effects as necessary ] } @@ -221,4 +221,4 @@ Use sequences when a viewEnter should sync/stagger animations across multiple el - `[TRIGGER_TYPE]` — same as Rule 1. `triggerType` is set on the sequence config, not on individual effects within the sequence. - `[OFFSET_MS]` — time offset between each child's animation start, in milliseconds. - `[OFFSET_EASING]` — CSS easing or named easing from `@wix/motion`, for the stagger distribution. Defaults to `'linear'`. -- `[EFFECT_DEFINTION]` — a definition of or a reference to a time-based animation effect. +- `[EFFECT_DEFINITION]` — a definition of or a reference to a time-based animation effect. diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs new file mode 100644 index 00000000..388cf452 --- /dev/null +++ b/packages/interact/scripts/build-rules.mjs @@ -0,0 +1,162 @@ +#!/usr/bin/env node + +import { readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs'; +import { join, basename, extname, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import yaml from 'js-yaml'; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); +const PKG_ROOT = join(__dirname, '..'); +const CONTENT_DIR = join(PKG_ROOT, '_content'); +const OUTPUT_DIR = join(PKG_ROOT, 'rules'); + +// --------------------------------------------------------------------------- +// 1. Load YAML data +// --------------------------------------------------------------------------- + +function loadYaml(name) { + const raw = readFileSync(join(CONTENT_DIR, 'data', name), 'utf8'); + return yaml.load(raw); +} + +const triggersData = loadYaml('triggers.yaml'); +const effectsData = loadYaml('effects.yaml'); +const metaData = loadYaml('meta.yaml'); + +const data = { + triggers: triggersData.triggers, + effects: effectsData, + meta: metaData, +}; + +// --------------------------------------------------------------------------- +// 2. Load fragments — parse markers +// --------------------------------------------------------------------------- + +class Fragments { + constructor(dir) { + this.store = new Map(); + this._loadDir(dir, ''); + } + + _loadDir(dir, prefix) { + for (const entry of readdirSync(dir, { withFileTypes: true })) { + if (entry.isDirectory()) { + this._loadDir(join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name); + } else if (entry.name.endsWith('.md')) { + const key = prefix + ? `${prefix}/${basename(entry.name, '.md')}` + : basename(entry.name, '.md'); + const raw = readFileSync(join(dir, entry.name), 'utf8'); + this.store.set(key, this._parseSections(raw)); + } + } + } + + _parseSections(raw) { + const sections = new Map(); + let current = null; + let buf = []; + + for (const line of raw.split('\n')) { + const m = line.match(/^$/); + if (m) { + if (current !== null) { + sections.set(current, buf.join('\n').trim()); + } + current = m[1]; + buf = []; + } else { + buf.push(line); + } + } + if (current !== null) { + sections.set(current, buf.join('\n').trim()); + } + return sections; + } + + get(path, section = 'default', params = {}) { + const sectionMap = this.store.get(path); + if (!sectionMap) { + throw new Error(`Fragment not found: ${path}`); + } + let content = sectionMap.get(section); + if (content === undefined) { + throw new Error(`Section "${section}" not found in fragment "${path}". Available: ${[...sectionMap.keys()].join(', ')}`); + } + for (const [key, val] of Object.entries(params)) { + content = content.replaceAll(`{{${key}}}`, val); + } + return content; + } +} + +const fragments = new Fragments(join(CONTENT_DIR, 'fragments')); + +// --------------------------------------------------------------------------- +// 3. Import templates and render +// --------------------------------------------------------------------------- + +const outputs = []; + +// Event trigger rules: click.md and hover.md +const eventTemplate = await import(join(CONTENT_DIR, 'templates', 'event-trigger-rule.mjs')); +for (const triggerName of ['click', 'hover']) { + const trigger = data.triggers.find(t => t.name === triggerName); + if (!trigger) throw new Error(`Trigger "${triggerName}" not found in triggers.yaml`); + const md = eventTemplate.render(trigger, data, fragments); + outputs.push({ file: `${triggerName}.md`, content: md }); +} + +// viewenter.md +const viewenterTemplate = await import(join(CONTENT_DIR, 'templates', 'viewenter-rule.mjs')); +const viewEnterTrigger = data.triggers.find(t => t.name === 'viewEnter'); +outputs.push({ + file: 'viewenter.md', + content: viewenterTemplate.render(viewEnterTrigger, data, fragments), +}); + +// viewprogress.md +const viewprogressTemplate = await import(join(CONTENT_DIR, 'templates', 'viewprogress-rule.mjs')); +const viewProgressTrigger = data.triggers.find(t => t.name === 'viewProgress'); +outputs.push({ + file: 'viewprogress.md', + content: viewprogressTemplate.render(viewProgressTrigger, data, fragments), +}); + +// pointermove.md +const pointermoveTemplate = await import(join(CONTENT_DIR, 'templates', 'pointermove-rule.mjs')); +const pointerMoveTrigger = data.triggers.find(t => t.name === 'pointerMove'); +outputs.push({ + file: 'pointermove.md', + content: pointermoveTemplate.render(pointerMoveTrigger, data, fragments), +}); + +// full-lean.md +const fullLeanTemplate = await import(join(CONTENT_DIR, 'templates', 'full-lean.mjs')); +outputs.push({ + file: 'full-lean.md', + content: fullLeanTemplate.render(data.triggers, data, fragments), +}); + +// integration.md +const integrationTemplate = await import(join(CONTENT_DIR, 'templates', 'integration.mjs')); +outputs.push({ + file: 'integration.md', + content: integrationTemplate.render(data.triggers, data, fragments), +}); + +// --------------------------------------------------------------------------- +// 4. Write outputs +// --------------------------------------------------------------------------- + +mkdirSync(OUTPUT_DIR, { recursive: true }); + +for (const { file, content } of outputs) { + const outPath = join(OUTPUT_DIR, file); + writeFileSync(outPath, content, 'utf8'); + console.log(` ✓ ${relative(PKG_ROOT, outPath)}`); +} + +console.log(`\nGenerated ${outputs.length} rule files.`); From c8b166b988e238010ebbfd5c8cf52aff2343893a Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 14:42:43 +0300 Subject: [PATCH 02/27] adding plan --- .../rules_build_pipeline_ffe2a9f3.plan.md | 286 ++++++++++++++++++ 1 file changed, 286 insertions(+) create mode 100644 .cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md new file mode 100644 index 00000000..28a37d12 --- /dev/null +++ b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md @@ -0,0 +1,286 @@ +--- +name: Rules Build Pipeline +overview: Implement a build pipeline for `packages/interact/rules/` using structured YAML data + JavaScript template functions + markdown fragments, eliminating all content duplication and ensuring a single source of truth. +todos: + - id: scaffold + content: "Create `_content/` directory structure: `data/`, `fragments/`, `templates/`, and `scripts/build-rules.mjs` skeleton" + status: pending + - id: data-triggers + content: Create `data/triggers.yaml` with all 9 trigger definitions (hover, click, interest, activate, viewEnter, viewProgress, pointerMove, animationEnd, pageVisible) — pull field names from actual TS types + status: pending + - id: data-effects-meta + content: Create `data/effects.yaml` (effect field definitions, presets table, ranges, easings) and `data/meta.yaml` (package metadata) + status: pending + - id: fragments + content: "Extract ~12 fragments from existing rule files: fouc, element-resolution, 6 pitfalls, quick-start, multiple-effects-note, custom-effect-intro, sequences-intro" + status: pending + - id: template-event + content: Create `templates/event-trigger-rule.mjs` — generates click.md and hover.md from trigger data + shared fragments + status: pending + - id: template-viewport + content: Create `templates/viewenter-rule.mjs` and `templates/viewprogress-rule.mjs` + status: pending + - id: template-pointer + content: Create `templates/pointermove-rule.mjs` + status: pending + - id: template-reference + content: Create `templates/full-lean.mjs` and `templates/integration.mjs` — the two comprehensive reference files + status: pending + - id: build-script + content: "Implement `scripts/build-rules.mjs`: YAML loading, fragment parsing, template orchestration, file writing" + status: pending + - id: integrate + content: Add `build:rules` script to package.json, add `js-yaml` devDependency, update CI workflow + status: pending + - id: verify + content: Run build, diff generated output against current rule files, verify no information loss, fix any discrepancies + status: pending +isProject: false +--- + +# Rules Build Pipeline + +## Architecture + +The build produces the same 7 output `.md` files that exist today, from three source layers: + +```mermaid +graph LR + subgraph sources ["Source Layer (what you edit)"] + YAML["data/*.yaml"] + Fragments["fragments/*.md"] + Templates["templates/*.mjs"] + end + subgraph build ["Build"] + Script["scripts/build-rules.mjs"] + end + subgraph output ["Output (generated, gitignored)"] + Rules["rules/*.md"] + end + YAML --> Script + Fragments --> Script + Templates --> Script + Script --> Rules +``` + + + +All source files live under `packages/interact/_content/`. The build script reads them and writes the final `.md` files to `packages/interact/rules/`. + +## Source Layer + +### 1. Data files (`_content/data/`) + +`**triggers.yaml**` — one entry per trigger, capturing everything that varies: + +```yaml +triggers: + - name: hover + a11yAlias: interest + a11yNote: "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus." + category: event # event | viewport | scroll | pointer | chain + supportsTimeEffect: true + supportsStateEffect: true + supportsScrubEffect: false + supportsCustomEffect: true + params: [] # no trigger params + pitfalls: + - id: hit-area # references fragments/pitfalls/hit-area.md + templateFields: # which optional fields to show in config templates + timeEffect: [triggerType, keyframeEffect, namedEffect, fill, duration, easing, delay, iterations, alternate] + stateEffect: [stateAction, transition, transitionProperties] + customEffect: [triggerType, customEffect, duration, easing] + sequence: [triggerType, offset, offsetEasing] + triggerTypeDescriptions: # trigger-specific wording for each triggerType value + alternate: "plays forward on enter, reverses on leave" + repeat: "restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses" + once: "plays once on the first enter and never again" + state: "resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`)" + stateActionDescriptions: + toggle: "applies the style state on enter, removes on leave" + add: "applies the style state on enter. Leave does NOT remove it" + remove: "removes a previously applied style state on enter" + clear: "clears all previously applied style states on enter" + fillNote: "while hovering" # trigger-specific fill context + + - name: click + a11yAlias: activate + a11yNote: "Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space)." + category: event + supportsTimeEffect: true + supportsStateEffect: true + supportsScrubEffect: false + supportsCustomEffect: true + params: [] + pitfalls: [] + templateFields: + timeEffect: [triggerType, keyframeEffect, namedEffect, fill, reversed, duration, easing, delay, iterations, alternate, effectId] + # ... (click includes reversed + effectId that hover omits) + triggerTypeDescriptions: + alternate: "plays forward on first click, reverses on next click" + repeat: "restarts the animation from the beginning on each click" + once: "plays once on the first click and never again" + state: "resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`)" + stateActionDescriptions: + toggle: "applies the style state, removes it on next click" + add: "applies the style state. Does not remove on subsequent clicks" + remove: "removes a previously applied style state" + clear: "clears all previously applied style states. Useful for resetting multiple stacked style states at once" + fillNote: "while finished" + # ... viewEnter, viewProgress, pointerMove, animationEnd entries follow +``` + +The trigger entries for `viewEnter`, `viewProgress`, `pointerMove`, and `animationEnd` follow the same shape but will include their `params` definitions (from the real TypeScript types in `[packages/interact/src/types/triggers.ts](packages/interact/src/types/triggers.ts)`). + +`**effects.yaml**` — shared effect field definitions, fill guidance, easing list: + +```yaml +fillGuidance: + both: "use for scroll-driven, pointer-driven, and toggling effects (alternate, repeat, state)" + backwards: "use for entrance animations with triggerType 'once' when the final keyframe matches the element's base style" + +triggerTypes: [once, repeat, alternate, state] +stateActions: [toggle, add, remove, clear] + +easings: + - linear + - ease + # ... full list from full-lean.md line 350 + +presets: + entrance: [FadeIn, GlideIn, SlideIn, ...] + ongoing: [Pulse, Spin, Breathe, ...] + scroll: [FadeScroll, RevealScroll, ...] + mouse: [TrackMouse, Tilt3DMouse, ...] + +rangeNames: + entry: "Element entering viewport" + exit: "Element exiting viewport" + contain: "After entry range and before exit range" + cover: "Full range from entry through contain and exit" + entry-crossing: "From element's leading edge entering to trailing edge entering" + exit-crossing: "From element's leading edge exiting to trailing edge exiting" +``` + +`**meta.yaml**` — package metadata: + +```yaml +packageName: "@wix/interact" +presetsPackage: "@wix/motion-presets" +installCommand: "npm install @wix/interact @wix/motion-presets" +entryPoints: + web: "@wix/interact/web" + react: "@wix/interact/react" + vanilla: "@wix/interact" +``` + +### 2. Markdown fragments (`_content/fragments/`) + +Each fragment has `` markers for different detail levels. + +**Planned fragments** (extracted from the ~15 duplicated concepts): + + +| Fragment file | Sections | Used by | +| ------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------- | +| `fouc.md` | `#short`, `#long`, `#code-generate`, `#code-web`, `#code-react`, `#code-vanilla`, `#rules` | full-lean, integration, viewenter | +| `element-resolution.md` | `#source`, `#target`, `#intro` | full-lean, integration | +| `pitfalls/hit-area.md` | `#hover`, `#pointermove`, `#full-lean-hover`, `#full-lean-pointermove` | hover, pointermove, full-lean | +| `pitfalls/overflow-clip.md` | `#short`, `#long` | viewprogress, full-lean | +| `pitfalls/same-element-viewenter.md` | `#short`, `#long` | viewenter, full-lean | +| `pitfalls/dont-guess-presets.md` | `#default` | full-lean, pointermove (Rule 1 variables) | +| `pitfalls/reduced-motion.md` | `#default` | full-lean | +| `pitfalls/perspective.md` | `#default` | full-lean | +| `quick-start.md` | `#web`, `#react`, `#vanilla`, `#cdn`, `#register-presets` | full-lean, integration | +| `multiple-effects-note.md` | `#default` (parameterized with `{{triggerName}}`) | hover, viewenter, viewprogress, pointermove | +| `custom-effect-intro.md` | `#default` | click, hover, viewenter | +| `sequences-intro.md` | `#short` (parameterized with `{{triggerName}}`) | click, hover, viewenter | + + +### 3. Templates (`_content/templates/`) + +Each template is a `.mjs` file exporting a function that receives data + fragments and returns a markdown string. + + +| Template | Generates | Key logic | +| ------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `event-trigger-rule.mjs` | `click.md`, `hover.md` | Parameterized by trigger data. Generates Rule 1 (TimeEffect), Rule 2 (StateEffect), Rule 3 (customEffect), Rule 4 (sequences). The config block fields, variable descriptions, and trigger-specific wording all come from `triggers.yaml`. | +| `viewenter-rule.mjs` | `viewenter.md` | Includes FOUC section (from fragment), params on every template, no StateEffect rule. | +| `viewprogress-rule.mjs` | `viewprogress.md` | Scrub effects, range semantics, sticky pattern. | +| `pointermove-rule.mjs` | `pointermove.md` | Progress type, centering, device conditions, composite pattern. | +| `full-lean.mjs` | `full-lean.md` | Comprehensive reference. Pulls from all data + fragments. | +| `integration.mjs` | `integration.md` | Integration guide. Pulls from meta + triggers data + fragments. | + + +**Template function signature:** + +```javascript +// event-trigger-rule.mjs +export function render(trigger, data, fragments) { + return `# ${capitalize(trigger.name)} Trigger Rules for ${data.meta.packageName} +... +${trigger.a11yAlias ? `**CRITICAL — Accessible ${trigger.name}**: ${trigger.a11yNote}` : ''} +${trigger.pitfalls.map(p => fragments.get(`pitfalls/${p.id}`, trigger.name)).join('\n')} +...`; +} +``` + +## Build Script (`scripts/build-rules.mjs`) + +~150 lines of Node.js ESM. Core flow: + +1. Load all YAML files from `_content/data/` using `js-yaml` +2. Load all fragment `.md` files, parse `` markers into a `Map>` +3. For each template, call its `render()` function with the appropriate data and fragments +4. Write output to `packages/interact/rules/` +5. Report what was generated + +**Fragment resolution with parameterization:** + +```javascript +class Fragments { + get(path, section = 'default', params = {}) { + let content = this.store.get(path)?.get(section); + for (const [key, val] of Object.entries(params)) { + content = content.replaceAll(`{{${key}}}`, val); + } + return content; + } +} +``` + +**Dependencies:** Add `js-yaml` as a devDependency in `packages/interact/package.json`. No other new dependencies needed — Node 22 ESM handles everything. + +## Integration with Existing Pipeline + +- Add script to `[packages/interact/package.json](packages/interact/package.json)`: + +```json + "build:rules": "node scripts/build-rules.mjs" + + +``` + +- Add `build:rules` as a `prebuild` step or keep it manual (recommend manual for now — rules change infrequently) +- Update `[.github/workflows/interactdocs.yml](/.github/workflows/interactdocs.yml)` to run `build:rules` before copying rules to `_site/rules/` +- Generated `rules/*.md` files can be either committed (simpler CI) or gitignored (cleaner repo) — recommend **committed** initially so the npm package always has them, with a CI check that verifies they're up to date (`build:rules && git diff --exit-code rules/`) + +## Known Fixes Included in Migration + +These existing issues will be fixed as a natural consequence of the migration: + +- `[EFFECT_DEFINTION]` typo in click/hover/viewenter sequence blocks (fix in template once) +- `hitAea` typo in pointermove.md (fix in fragment once) +- `type: 'once'` vs `triggerType: 'once'` inconsistency in full-lean.md FOUC section +- hover.md missing `reversed` and `effectId` fields in TimeEffect template (decide once in `triggers.yaml` whether they belong) +- `generate` import path inconsistency (`@wix/interact` vs `@wix/interact/web`) — standardize in FOUC fragment +- Missing "Multiple effects" note in click.md (present in hover.md but absent in click.md) +- Inconsistent "additional effects" comments across files + +## Future Extension Points + +- **docs/**: Same `_content/data/` feeds docs templates — add `_content/templates/docs/` later +- **README**: Same meta.yaml + triggers.yaml generates README sections +- **Validation**: Add `scripts/validate-rules.mjs` that imports actual TS types and cross-checks trigger names, param fields, effect fields against the YAML data +- **motion-presets rules**: The build mechanism is scoped to `packages/interact/` but the architecture (YAML + fragments + templates) can be replicated in `packages/motion-presets/` with shared fragments if needed + From b55a6e907712989ef69f78db9d7ccfff14cb7c5d Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 15:23:21 +0300 Subject: [PATCH 03/27] PR fixes --- .../rules_build_pipeline_ffe2a9f3.plan.md | 186 ++++++++++++++++-- .github/workflows/ci.yml | 5 + packages/interact/_content/data/triggers.yaml | 34 ---- .../interact/_content/fragments/conditions.md | 20 ++ .../_content/fragments/config-structure.md | 32 +++ .../_content/fragments/custom-effect-intro.md | 2 - .../_content/fragments/sequences-intro.md | 2 - .../interact/_content/fragments/sequences.md | 96 +++++++++ .../interact/_content/fragments/static-api.md | 34 ++++ .../_content/templates/event-trigger-rule.mjs | 95 +++++---- .../interact/_content/templates/full-lean.mjs | 115 +---------- .../_content/templates/integration.mjs | 64 +----- .../_content/templates/viewprogress-rule.mjs | 2 +- packages/interact/rules/click.md | 2 +- packages/interact/rules/hover.md | 2 +- packages/interact/rules/integration.md | 6 +- packages/interact/rules/viewprogress.md | 2 +- packages/interact/scripts/build-rules.mjs | 69 +++---- yarn.lock | 3 +- 19 files changed, 455 insertions(+), 316 deletions(-) create mode 100644 packages/interact/_content/fragments/conditions.md create mode 100644 packages/interact/_content/fragments/config-structure.md delete mode 100644 packages/interact/_content/fragments/custom-effect-intro.md delete mode 100644 packages/interact/_content/fragments/sequences-intro.md create mode 100644 packages/interact/_content/fragments/sequences.md create mode 100644 packages/interact/_content/fragments/static-api.md diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md index 28a37d12..dfb3c74a 100644 --- a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md +++ b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md @@ -4,37 +4,64 @@ overview: Implement a build pipeline for `packages/interact/rules/` using struct todos: - id: scaffold content: "Create `_content/` directory structure: `data/`, `fragments/`, `templates/`, and `scripts/build-rules.mjs` skeleton" - status: pending + status: completed - id: data-triggers content: Create `data/triggers.yaml` with all 9 trigger definitions (hover, click, interest, activate, viewEnter, viewProgress, pointerMove, animationEnd, pageVisible) — pull field names from actual TS types - status: pending + status: completed - id: data-effects-meta content: Create `data/effects.yaml` (effect field definitions, presets table, ranges, easings) and `data/meta.yaml` (package metadata) - status: pending + status: completed - id: fragments content: "Extract ~12 fragments from existing rule files: fouc, element-resolution, 6 pitfalls, quick-start, multiple-effects-note, custom-effect-intro, sequences-intro" - status: pending + status: completed - id: template-event content: Create `templates/event-trigger-rule.mjs` — generates click.md and hover.md from trigger data + shared fragments - status: pending + status: completed - id: template-viewport content: Create `templates/viewenter-rule.mjs` and `templates/viewprogress-rule.mjs` - status: pending + status: completed - id: template-pointer content: Create `templates/pointermove-rule.mjs` - status: pending + status: completed - id: template-reference content: Create `templates/full-lean.mjs` and `templates/integration.mjs` — the two comprehensive reference files - status: pending + status: completed - id: build-script content: "Implement `scripts/build-rules.mjs`: YAML loading, fragment parsing, template orchestration, file writing" - status: pending + status: completed - id: integrate content: Add `build:rules` script to package.json, add `js-yaml` devDependency, update CI workflow - status: pending + status: completed - id: verify content: Run build, diff generated output against current rule files, verify no information loss, fix any discrepancies - status: pending + status: completed + - id: fix-lockfile + content: "CI fix: run `yarn install` so yarn.lock includes the new `js-yaml` resolution, commit the updated lockfile" + status: completed + - id: fix-dead-fragments + content: "Remove dead fragment files `custom-effect-intro.md` and `sequences-intro.md` (unused by any template; YAML prose fields are used instead)" + status: completed + - id: fix-yaml-prose + content: "Move 15+ prose description fields (timeEffectIntro, sourceKeyDesc, etc.) out of triggers.yaml into the event-trigger template directly, keeping only structured data in YAML" + status: completed + - id: fix-fill-variables + content: "Collapse `buildVariablesMidFill`/`buildVariablesEndFill` into a single `buildVariables` function — always place `[FILL_MODE]` after `[NAMED_EFFECT_DEFINITION]` (matching the config block order). Remove `fillModeAtEnd` from triggers.yaml." + status: completed + - id: fix-build-manifest + content: "Replace repetitive template orchestration in build-rules.mjs (lines 104-148) with a data-driven manifest array" + status: completed + - id: fix-viewprogress-backtick + content: "Fix stray backtick template literal in viewprogress-rule.mjs line 86 — normalize to plain string like other templates" + status: completed + - id: fix-shared-fragments + content: "Extract duplicated sections (Conditions, Static API, Config Structure, Sequences) from full-lean.mjs and integration.mjs into shared fragments" + status: completed + - id: fix-ci-freshness + content: "Add a freshness check step to `.github/workflows/ci.yml`: `yarn workspace @wix/interact build:rules && git diff --exit-code packages/interact/rules/`" + status: completed + - id: fix-regenerate + content: "Run `build:rules`, verify output, commit regenerated rules/*.md files" + status: completed isProject: false --- @@ -277,6 +304,143 @@ These existing issues will be fixed as a natural consequence of the migration: - Missing "Multiple effects" note in click.md (present in hover.md but absent in click.md) - Inconsistent "additional effects" comments across files +## Post-PR Fixes (from code review of PR #204) + +The initial implementation (PR #204) landed the full architecture. The fixes below address CI failures, dead code, and simplification opportunities found during review. + +### Fix 1: Commit updated `yarn.lock` (CI blocker) + +The CI `Install dependencies` step runs `yarn install --immutable` which refuses to modify the lockfile. Adding `js-yaml` to `devDependencies` in `package.json` without updating `yarn.lock` causes this failure. + +**Action:** Run `nvm use && yarn install` to regenerate the lockfile, then commit the updated `yarn.lock`. + +### Fix 2: Remove dead fragments + +`_content/fragments/custom-effect-intro.md` and `_content/fragments/sequences-intro.md` are not referenced by any template. The templates use `trigger.customEffectIntro` and `trigger.sequencesIntro` from YAML instead. These fragment files are dead code. + +**Action:** Delete both files. + +### Fix 3: Move prose descriptions out of `triggers.yaml` + +`triggers.yaml` currently stores 15+ full-sentence prose fields per trigger entry (`timeEffectIntro`, `stateEffectIntro`, `sequencesIntro`, `customEffectIntro`, `fillCritical`, `sourceKeyDesc`, `targetKeyDesc`, `fillModeDesc`, `namedEffectDesc`, `easingDesc`, `iterationsDesc`, `alternateDesc`, `customEffectCallbackDesc`, `sequenceEffectDefDesc`, `sequenceOffsetEasingDesc`). YAML is suited for structured data, not paragraphs of English. + +These fields only vary between hover and click. The viewEnter/viewProgress/pointerMove triggers don't use them at all (they have their own hardcoded templates). + +**Action:** +- Remove all prose description fields from `triggers.yaml` (hover + click entries). +- Move the hover/click prose differences into `event-trigger-rule.mjs` directly, keyed by a simple flag or `trigger.name` check. These are small trigger-specific word choices (e.g. "while hovering" vs "while finished"), not reusable data. +- Keep only structured data in YAML: name, category, support flags, params, templateFields, pitfalls, triggerType/stateAction enum descriptions, `a11yAlias`, `a11yNote`, `defaultTriggerType`, `showMultipleEffectsNote`. + +### Fix 4: Unify fill-mode variable placement + +[`event-trigger-rule.mjs`](packages/interact/_content/templates/event-trigger-rule.mjs) has two nearly identical functions — `buildVariablesMidFill` (click) and `buildVariablesEndFill` (hover) — that differ only in where `[FILL_MODE]` appears in the variables list. The code block template itself always shows `fill` right after `namedEffect`/`keyframeEffect`, so the variables list should match that order. + +**Action:** +- Collapse into a single `buildVariables` function that always places `[FILL_MODE]` after `[NAMED_EFFECT_DEFINITION]` (mid position, matching the config block order). +- Remove `fillModeAtEnd` and `fillModeDash` from `triggers.yaml`. +- Use a consistent em-dash separator for all triggers. + +The single function: + +```javascript +function buildVariables(trigger, fillModeVar, reversedVar, effectIdVar) { + const lines = [ + `- \`[SOURCE_KEY]\` — ...`, + `- \`[TARGET_KEY]\` — ...`, + `- \`[TRIGGER_TYPE]\` — ...`, + ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`), + `- \`[KEYFRAMES]\` — ...`, + `- \`[EFFECT_NAME]\` — ...`, + `- \`[NAMED_EFFECT_DEFINITION]\` — ...`, + fillModeVar, + ]; + if (reversedVar) lines.push(reversedVar.trim()); + lines.push( + `- \`[DURATION_MS]\` — ...`, + `- \`[EASING_FUNCTION]\` — ...`, + `- \`[DELAY_MS]\` — ...`, + `- \`[ITERATIONS]\` — ...`, + `- \`[ALTERNATE_BOOL]\` — ...`, + ); + if (effectIdVar) lines.push(effectIdVar.trim()); + return lines.join('\n'); +} +``` + +### Fix 5: Data-driven build manifest + +Lines 104-148 of [`build-rules.mjs`](packages/interact/scripts/build-rules.mjs) repeat the same import-find-render-push pattern 6 times. Replace with a declarative manifest: + +```javascript +const manifest = [ + { template: 'event-trigger-rule.mjs', triggers: ['click', 'hover'], output: name => `${name}.md` }, + { template: 'viewenter-rule.mjs', triggers: ['viewEnter'], output: () => 'viewenter.md' }, + { template: 'viewprogress-rule.mjs', triggers: ['viewProgress'], output: () => 'viewprogress.md' }, + { template: 'pointermove-rule.mjs', triggers: ['pointerMove'], output: () => 'pointermove.md' }, + { template: 'full-lean.mjs', triggers: null, output: () => 'full-lean.md' }, + { template: 'integration.mjs', triggers: null, output: () => 'integration.md' }, +]; + +for (const entry of manifest) { + const mod = await import(join(CONTENT_DIR, 'templates', entry.template)); + if (entry.triggers) { + for (const name of entry.triggers) { + const trigger = data.triggers.find(t => t.name === name); + if (!trigger) throw new Error(`Trigger "${name}" not found in triggers.yaml`); + outputs.push({ file: entry.output(name), content: mod.render(trigger, data, fragments) }); + } + } else { + outputs.push({ file: entry.output(), content: mod.render(data.triggers, data, fragments) }); + } +} +``` + +Adding a new template becomes a single manifest line. + +### Fix 6: Fix stray backtick in `viewprogress-rule.mjs` + +Line 86 of [`viewprogress-rule.mjs`](packages/interact/_content/templates/viewprogress-rule.mjs) uses `` easing: `'[EASING_FUNCTION]'` `` (backtick template literal) inside the code fence. All other templates use `easing: '[EASING_FUNCTION]'` (plain single-quoted string). + +**Action:** Change to `easing: '[EASING_FUNCTION]'` for consistency. + +### Fix 7: Extract shared sections into fragments + +`full-lean.mjs` (604 lines) and `integration.mjs` (285 lines) are mostly hardcoded prose with only a handful of `fragments.get()` calls. Several large sections are duplicated between them with minor variation: + +- **Conditions** block (~30 lines) +- **Static API** table (~20 lines) +- **Config Structure / InteractConfig** (~15 lines) +- **Sequences** section (~50 lines) + +**Action:** Extract each into a new fragment with `` / `` section markers (same pattern as `fouc.md`). This brings these templates closer to the single-source-of-truth goal and makes future edits to these shared concepts a one-file change. + +New fragment files: +- `_content/fragments/conditions.md` — `#full-lean`, `#integration` +- `_content/fragments/static-api.md` — `#full-lean`, `#integration` +- `_content/fragments/config-structure.md` — `#full-lean`, `#integration` +- `_content/fragments/sequences.md` — `#full-lean`, `#integration` + +### Fix 8: Add CI freshness check + +The plan and PR description both mention a freshness check, but [`.github/workflows/ci.yml`](.github/workflows/ci.yml) was not updated. + +**Action:** Add a step after `Build` in the `build` job: + +```yaml +- name: Verify generated rules are up to date + run: | + yarn workspace @wix/interact build:rules + git diff --exit-code packages/interact/rules/ +``` + +This ensures that if someone edits a source file but forgets to re-run `build:rules`, CI catches it. + +### Fix 9: Regenerate output + +After all fixes above, run `build:rules` and commit the regenerated `rules/*.md` files so they reflect the new `[FILL_MODE]` variable ordering in hover.md. + +--- + ## Future Extension Points - **docs/**: Same `_content/data/` feeds docs templates — add `_content/templates/docs/` later diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c70eb010..70be78de 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,11 @@ jobs: - name: Build run: yarn build + - name: Verify generated rules are up to date + run: | + yarn workspace @wix/interact build:rules + git diff --exit-code packages/interact/rules/ + - name: Lint run: yarn lint diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml index 47afa0d7..e8fd6b87 100644 --- a/packages/interact/_content/data/triggers.yaml +++ b/packages/interact/_content/data/triggers.yaml @@ -43,23 +43,6 @@ triggers: remove: "removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`." clear: "clears all previously applied style states on enter. Use to reset multiple stacked `'add'` style changes at once (e.g. a \"reset\" hover area that undoes several accumulated states)." fillNote: "while hovering" - fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`." - fillModeAtEnd: true - fillModeDash: "—" - sourceKeyDesc: "The element that listens for hover." - targetKeyDesc: "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above)." - fillModeDesc: "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished." - namedEffectDesc: "object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options." - easingDesc: "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`." - iterationsDesc: "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`." - alternateDesc: "optional. `true` to alternate direction on every other iteration (within a single playback)." - customEffectIntro: "Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline." - customEffectCallbackDesc: "function with signature `(target: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1." - sequenceEffectDefDesc: "a definition of or a reference to a time-based animation effect." - sequenceOffsetEasingDesc: "easing curve for the offset staggering distribution. CSS easing string, or named easing from `@wix/motion`. Defaults to `'linear'`." - timeEffectIntro: "Use `keyframeEffect` or `namedEffect` when the hover should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior." - stateEffectIntro: "Use `transition` or `transitionProperties` when the hover should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Set `stateAction` on the effect to control how the style is applied." - sequencesIntro: "Use sequences when a hover should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior." - name: click a11yAlias: activate @@ -106,23 +89,6 @@ triggers: remove: "removes a previously applied style state." clear: "clears all previously applied style states. Useful for resetting multiple stacked style states at once." fillNote: "while finished" - fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`." - fillModeAtEnd: false - fillModeDash: "-" - sourceKeyDesc: "The element that listens for clicks." - targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element." - fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect." - namedEffectDesc: "object with properties of pre-built, time-based animation effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options." - easingDesc: "CSS easing string, or named easing from `@wix/motion`." - iterationsDesc: "optional. Number of iterations, or `Infinity` for continuous loops." - alternateDesc: "optional. `true` to alternate direction on every other iteration (within a single playback). Different from `triggerType: 'alternate'` which alternates per click." - customEffectIntro: "Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation, randomized behavior). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline." - customEffectCallbackDesc: "function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with target element and `progress` from 0 to 1." - sequenceEffectDefDesc: "a definition of, or a reference to a time-based animation effect." - sequenceOffsetEasingDesc: "easing curve for the offset staggering distribution. Defaults to `'linear'`." - timeEffectIntro: "Use `keyframeEffect` or `namedEffect` when the click should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior." - stateEffectIntro: "Use `transition` or `transitionProperties` when the click should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Uses the `transition` CSS property. Set `stateAction` on the effect to control how the style is applied." - sequencesIntro: "Use sequences when a click should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior." - name: viewEnter category: viewport diff --git a/packages/interact/_content/fragments/conditions.md b/packages/interact/_content/fragments/conditions.md new file mode 100644 index 00000000..3a5005e2 --- /dev/null +++ b/packages/interact/_content/fragments/conditions.md @@ -0,0 +1,20 @@ + +Named conditions that gate interactions, effects, or sequences. + +| Type | Predicate | +| :--------- | :------------------------------------------------------------------------ | +| `media` | CSS media query condition without `@media` (e.g., `'(min-width: 768px)'`) | +| `selector` | CSS selector; `&` is replaced with the base element selector | + +Attach via `conditions: ['[CONDITION_ID]']` on interactions, effects, or sequences. On an interaction, conditions gate the entire trigger; on an effect, only that specific effect is skipped. All listed conditions must pass. + +### Examples + +```ts +conditions: { + 'desktop': { type: 'media', predicate: '(min-width: 768px)' }, + 'hover-device': { type: 'media', predicate: '(hover: hover)' }, + 'reduced-motion': { type: 'media', predicate: '(prefers-reduced-motion: reduce)' }, + 'odd-items': { type: 'selector', predicate: ':nth-of-type(odd)' }, +} +``` diff --git a/packages/interact/_content/fragments/config-structure.md b/packages/interact/_content/fragments/config-structure.md new file mode 100644 index 00000000..f4794859 --- /dev/null +++ b/packages/interact/_content/fragments/config-structure.md @@ -0,0 +1,32 @@ + +```ts +type InteractConfig = { + interactions: Interaction[]; // REQUIRED + effects?: Record; // reusable effects referenced by effectId + sequences?: Record; // reusable sequences by sequenceId + conditions?: Record; // named conditions; keys are condition ids +}; +``` + +All cross-references (by id) MUST point to existing entries. Element keys MUST be stable for the config's lifetime. + + +### InteractConfig + +```typescript +type InteractConfig = { + interactions: Interaction[]; + effects?: Record; + sequences?: Record; + conditions?: Record; +}; +``` + +| Field | Description | +| :------------- | :---------------------------------------------------------------------- | +| `interactions` | Required. Array of interaction definitions binding triggers to effects. | +| `effects?` | Reusable effects referenced by `effectId` from interactions. | +| `sequences?` | Reusable sequence definitions, referenced by `sequenceId`. | +| `conditions?` | Named conditions (media/container/selector queries), referenced by ID. | + +Each call to `Interact.create(config)` creates a new `Interact` instance. A single config can define multiple interactions. diff --git a/packages/interact/_content/fragments/custom-effect-intro.md b/packages/interact/_content/fragments/custom-effect-intro.md deleted file mode 100644 index 99344971..00000000 --- a/packages/interact/_content/fragments/custom-effect-intro.md +++ /dev/null @@ -1,2 +0,0 @@ - -Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline. diff --git a/packages/interact/_content/fragments/sequences-intro.md b/packages/interact/_content/fragments/sequences-intro.md deleted file mode 100644 index 54ca8433..00000000 --- a/packages/interact/_content/fragments/sequences-intro.md +++ /dev/null @@ -1,2 +0,0 @@ - -Use sequences when a {{triggerName}} should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior. diff --git a/packages/interact/_content/fragments/sequences.md b/packages/interact/_content/fragments/sequences.md new file mode 100644 index 00000000..1eddd99d --- /dev/null +++ b/packages/interact/_content/fragments/sequences.md @@ -0,0 +1,96 @@ + +Coordinate multiple effects with staggered timing. Prefer sequences over manual delay stagger. + +### Sequence As type + +```ts +{ + effects: (Effect | EffectRef)[]; // REQUIRED + delay?: number; // ms before sequence starts + offset?: number; // ms between each child's animation start + offsetEasing?: string; // easing curve for staggering offsets + sequenceId?: string; // for caching/referencing + conditions?: string[]; // ids referencing the top-level conditions map +} +``` + +### Template + +```ts +{ + interactions: [ + { + key: '[SOURCE_KEY]', + trigger: '[TRIGGER]', + params: [TRIGGER_PARAMS], + sequences: [ + { + offset: [OFFSET_MS], // optional + offsetEasing: '[OFFSET_EASING]', // optional + delay: [DELAY_MS], // optional + effects: [ + // if used `listContainer` each item in the list is a target of a child effect + { + effectId: '[EFFECT_ID]', + listContainer: '[LIST_CONTAINER_SELECTOR]', + }, + // if multiple effects are given each generated effect is added to the sequence + ], + }, + ], + }, + ], + effects: { + '[EFFECT_ID]': { + // effect definition (namedEffect, keyframeEffect, or customEffect) + }, + }, +} +``` + +### Variables + +- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for /vanilla, `interactKey` for React). +- `[TRIGGER]` — any trigger for time-based animation effects (e.g., `'viewEnter'`, `'activate'`, `'interest'`). +- `[TRIGGER_PARAMS]` — trigger-specific parameters (e.g., `{ type: 'once', threshold: 0.3 }`). +- `[OFFSET_MS]` — ms between each child's animation start. +- `[OFFSET_EASING]` — CSS easing string or named easing from `@wix/motion`. +- `[DELAY_MS]` — optional. Base delay (ms) before the entire sequence starts. +- `[EFFECT_ID]` — string key referencing an entry in the top-level `effects` map. +- `[LIST_CONTAINER_SELECTOR]` — optional. CSS selector for the container whose children will be staggered. + +Reusable sequences can be defined in `InteractConfig.sequences` and referenced by `sequenceId`. + + +Sequences coordinate multiple effects with staggered timing. + +```typescript +{ + offset: number, // ms between consecutive items + offsetEasing: string, // Any valid easing string for stagger distribution curve + delay: number, // ms base delay before the sequence starts + effects: [ + /* ... effect definitions */, + ], +} +``` + +Define reusable sequences in `InteractConfig.sequences` and reference by `sequenceId`: + +```typescript +{ + sequences: { + 'stagger-fade': { + /* ... sequence definition */ + }, + }, + interactions: [ + { + key: '[SOURCE_KEY]', + trigger: '[TRIGGER]', + params: [TRIGGER_PARAMS], + sequences: [{ sequenceId: 'stagger-fade' }], + }, + ], +} +``` diff --git a/packages/interact/_content/fragments/static-api.md b/packages/interact/_content/fragments/static-api.md new file mode 100644 index 00000000..b1e2b61f --- /dev/null +++ b/packages/interact/_content/fragments/static-api.md @@ -0,0 +1,34 @@ + +| Method / Property | Description | +| :---------------------------------- | :------------------------------------------------------------------------------------------------------------ | +| `Interact.create(config)` | Initialize with a config. Returns the instance. Store the instance to manage its lifecycle. | +| `Interact.registerEffects(presets)` | Register named effect presets. MUST be called before `create`. | +| `Interact.destroy()` | Tear down all instances. Call on unmount or route change to prevent memory leaks. | +| `Interact.forceReducedMotion` | `boolean` (default: `false`) — force reduced-motion behavior regardless of OS setting. | +| `Interact.allowA11yTriggers` | `boolean` (default: `false`) — enable accessibility trigger variants (`interest`, `activate`). | +| `Interact.setup(options)` | Configure global options for scroll, pointer, and viewEnter systems. Call before `create`. See options below. | + +**`Interact.setup(options)`** — optional configuration object: + +| Option | Type | Description | +| :--------------------- | :----------------------------- | :-------------------------------------------------------------------- | +| `scrollOptionsGetter` | `() => Partial` | Function returning defaults for scroll-driven animation configuration | +| `pointerOptionsGetter` | `() => Partial` | Function returning defaults for pointer-move animation configuration | +| `viewEnter` | `Partial` | Defaults for all viewEnter triggers (`threshold`,`inset`) | +| `allowA11yTriggers` | `boolean` | Enable accessibility trigger variants (use `interest` and `activate`) | + +Use `setup()` when you need to override default observer thresholds or provide global configuration that applies to all interactions of a given trigger type. + +Each `Interact.create()` call returns an instance. Store instances and call `instance.destroy()` when no longer needed (e.g. on component unmount) to prevent stale listeners and memory leaks. + + +Each `Interact.create(config)` call returns an instance. Keep a reference if you need to add/remove elements dynamically (vanilla JS) or to destroy a specific instance. Call `Interact.destroy()` to tear down all instances at once (e.g. on page navigation). + +| Method / Property | Description | +| :---------------------------------- | :------------------------------------------------------------------------------------------- | +| `Interact.create(config)` | Initialize with a config. Returns the instance. Multiple configs create separate instances. | +| `Interact.registerEffects(presets)` | Register named effect presets before `create`. Required for `namedEffect` usage. | +| `Interact.destroy()` | Tear down all instances. | +| `Interact.forceReducedMotion` | `boolean` — force reduced-motion behavior regardless of OS setting. Default: `false`. | +| `Interact.allowA11yTriggers` | `boolean` — enable accessibility triggers (`interest`, `activate`). Default: `false`. | +| `Interact.setup(options)` | Configure global defaults for scroll/pointer/viewEnter trigger params. Call before `create`. | diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 2bcf614e..f0f2d12f 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,8 +1,46 @@ import { capitalize } from './_helpers.mjs'; +const PROSE = { + hover: { + timeEffectIntro: "Use `keyframeEffect` or `namedEffect` when the hover should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior.", + stateEffectIntro: "Use `transition` or `transitionProperties` when the hover should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Set `stateAction` on the effect to control how the style is applied.", + customEffectIntro: "Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline.", + sequencesIntro: "Use sequences when a hover should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior.", + fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", + sourceKeyDesc: "The element that listens for hover.", + targetKeyDesc: "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", + fillModeDesc: "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.", + namedEffectDesc: "object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.", + easingDesc: "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.", + iterationsDesc: "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", + alternateDesc: "optional. `true` to alternate direction on every other iteration (within a single playback).", + customEffectCallbackDesc: "function with signature `(target: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1.", + sequenceEffectDefDesc: "a definition of or a reference to a time-based animation effect.", + sequenceOffsetEasingDesc: "easing curve for the offset staggering distribution. CSS easing string, or named easing from `@wix/motion`. Defaults to `'linear'`.", + }, + click: { + timeEffectIntro: "Use `keyframeEffect` or `namedEffect` when the click should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior.", + stateEffectIntro: "Use `transition` or `transitionProperties` when the click should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Uses the `transition` CSS property. Set `stateAction` on the effect to control how the style is applied.", + customEffectIntro: "Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation, randomized behavior). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline.", + sequencesIntro: "Use sequences when a click should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior.", + fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.", + sourceKeyDesc: "The element that listens for clicks.", + targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", + fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", + namedEffectDesc: "object with properties of pre-built, time-based animation effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.", + easingDesc: "CSS easing string, or named easing from `@wix/motion`.", + iterationsDesc: "optional. Number of iterations, or `Infinity` for continuous loops.", + alternateDesc: "optional. `true` to alternate direction on every other iteration (within a single playback). Different from `triggerType: 'alternate'` which alternates per click.", + customEffectCallbackDesc: "function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with target element and `progress` from 0 to 1.", + sequenceEffectDefDesc: "a definition of, or a reference to a time-based animation effect.", + sequenceOffsetEasingDesc: "easing curve for the offset staggering distribution. Defaults to `'linear'`.", + }, +}; + export function render(trigger, data, fragments) { const name = trigger.name; const Name = capitalize(name); + const prose = PROSE[name]; const hasReversed = trigger.templateFields.timeEffect.includes('reversed'); const hasEffectId = trigger.templateFields.timeEffect.includes('effectId'); const showMultipleEffects = trigger.showMultipleEffectsNote; @@ -28,8 +66,6 @@ export function render(trigger, data, fragments) { ? `\n effectId: '[UNIQUE_EFFECT_ID]'` : ''; - const fillModeVarDash = trigger.fillModeDash || '—'; - const reversedVar = hasReversed ? `\n- \`[INITIAL_REVERSED_BOOL]\` — optional. \`true\` to start in the finished state so the entire effect is reversed.` : ''; @@ -38,11 +74,9 @@ export function render(trigger, data, fragments) { ? `\n- \`[UNIQUE_EFFECT_ID]\` — optional. String identifier used by \`animationEnd\` triggers for chaining, and by sequences for referencing effects from the top-level \`effects\` map.` : ''; - const fillModeVar = `- \`[FILL_MODE]\` ${fillModeVarDash} ${trigger.fillModeDesc}`; + const fillModeVar = `- \`[FILL_MODE]\` — ${prose.fillModeDesc}`; - const variablesBlock = trigger.fillModeAtEnd - ? buildVariablesEndFill(trigger, fillModeVar, reversedVar, effectIdVar) - : buildVariablesMidFill(trigger, fillModeVar, reversedVar, effectIdVar); + const variablesBlock = buildVariables(trigger, prose, fillModeVar, reversedVar, effectIdVar); return `# ${Name} Trigger Rules for ${data.meta.packageName} @@ -61,9 +95,9 @@ ${pitfallsBlock} ## Rule 1: keyframeEffect / namedEffect (TimeEffect) -${trigger.timeEffectIntro} +${prose.timeEffectIntro} -**CRITICAL:** ${trigger.fillCritical} +**CRITICAL:** ${prose.fillCritical} ${multipleEffectsNote} \`\`\`typescript { @@ -101,7 +135,7 @@ ${variablesBlock} ## Rule 2: transition / transitionProperties (StateEffect) -${trigger.stateEffectIntro} +${prose.stateEffectIntro} Use \`transition\` when all properties share timing. Use \`transitionProperties\` when each property needs independent \`duration\`, \`delay\`, or \`easing\`. @@ -155,7 +189,7 @@ ${Object.entries(trigger.stateActionDescriptions).map(([k, v]) => ` - \`'${k}'\ ## Rule 3: customEffect (TimeEffect) -${trigger.customEffectIntro} +${prose.customEffectIntro} \`\`\`typescript { @@ -176,7 +210,7 @@ ${rule23Closing} ### Variables - \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` / \`[TRIGGER_TYPE]\` — same as Rule 1. -- \`[CUSTOM_EFFECT_CALLBACK]\` — ${trigger.customEffectCallbackDesc} +- \`[CUSTOM_EFFECT_CALLBACK]\` — ${prose.customEffectCallbackDesc} - \`[DURATION_MS]\` — animation duration in milliseconds. - \`[EASING_FUNCTION]\` — CSS easing string, or named easing from \`@wix/motion\`. @@ -184,7 +218,7 @@ ${rule23Closing} ## Rule 4: Sequences -${trigger.sequencesIntro} +${prose.sequencesIntro} \`\`\`typescript { @@ -208,49 +242,30 @@ ${trigger.sequencesIntro} - \`[SOURCE_KEY]\` / \`[TRIGGER_TYPE]\` — same as Rule 1. \`triggerType\` is set on the sequence config, not on individual effects within the sequence. - \`[OFFSET_MS]\` — time offset for staggering each child's animation start, in milliseconds. -- \`[OFFSET_EASING]\` — ${trigger.sequenceOffsetEasingDesc} -- \`[EFFECT_DEFINITION]\` — ${trigger.sequenceEffectDefDesc} +- \`[OFFSET_EASING]\` — ${prose.sequenceOffsetEasingDesc} +- \`[EFFECT_DEFINITION]\` — ${prose.sequenceEffectDefDesc} `; } -function buildVariablesMidFill(trigger, fillModeVar, reversedVar, effectIdVar) { +function buildVariables(trigger, prose, fillModeVar, reversedVar, effectIdVar) { const lines = [ - `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${trigger.sourceKeyDesc}`, - `- \`[TARGET_KEY]\` — ${trigger.targetKeyDesc}`, + `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${prose.sourceKeyDesc}`, + `- \`[TARGET_KEY]\` — ${prose.targetKeyDesc}`, `- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of:`, ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`), `- \`[KEYFRAMES]\` — array of keyframe objects (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase.`, `- \`[EFFECT_NAME]\` — unique string identifier for a \`keyframeEffect\`.`, - `- \`[NAMED_EFFECT_DEFINITION]\` — ${trigger.namedEffectDesc}`, + `- \`[NAMED_EFFECT_DEFINITION]\` — ${prose.namedEffectDesc}`, fillModeVar, ]; if (reversedVar) lines.push(reversedVar.trim()); lines.push( `- \`[DURATION_MS]\` — animation duration in milliseconds.`, - `- \`[EASING_FUNCTION]\` — ${trigger.easingDesc}`, + `- \`[EASING_FUNCTION]\` — ${prose.easingDesc}`, `- \`[DELAY_MS]\` — optional delay before the effect starts, in milliseconds.`, - `- \`[ITERATIONS]\` — ${trigger.iterationsDesc}`, - `- \`[ALTERNATE_BOOL]\` — ${trigger.alternateDesc}`, + `- \`[ITERATIONS]\` — ${prose.iterationsDesc}`, + `- \`[ALTERNATE_BOOL]\` — ${prose.alternateDesc}`, ); if (effectIdVar) lines.push(effectIdVar.trim()); return lines.join('\n'); } - -function buildVariablesEndFill(trigger, fillModeVar, reversedVar, effectIdVar) { - const lines = [ - `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${trigger.sourceKeyDesc}`, - `- \`[TARGET_KEY]\` — ${trigger.targetKeyDesc}`, - `- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of:`, - ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`), - `- \`[KEYFRAMES]\` — array of keyframe objects (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase.`, - `- \`[EFFECT_NAME]\` — unique string identifier for a \`keyframeEffect\`.`, - `- \`[NAMED_EFFECT_DEFINITION]\` — ${trigger.namedEffectDesc}`, - `- \`[DURATION_MS]\` — animation duration in milliseconds.`, - `- \`[EASING_FUNCTION]\` — ${trigger.easingDesc}`, - `- \`[DELAY_MS]\` — optional delay before the effect starts, in milliseconds.`, - `- \`[ITERATIONS]\` — ${trigger.iterationsDesc}`, - `- \`[ALTERNATE_BOOL]\` — ${trigger.alternateDesc}`, - fillModeVar, - ]; - return lines.join('\n'); -} diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 884c0697..eefc56fe 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -114,16 +114,7 @@ import { Interaction } from '@wix/interact/react'; ## Config Structure -\`\`\`ts -type InteractConfig = { - interactions: Interaction[]; // REQUIRED - effects?: Record; // reusable effects referenced by effectId - sequences?: Record; // reusable sequences by sequenceId - conditions?: Record; // named conditions; keys are condition ids -}; -\`\`\` - -All cross-references (by id) MUST point to existing entries. Element keys MUST be stable for the config's lifetime. +${fragments.get('config-structure', 'full-lean')} --- @@ -455,91 +446,13 @@ ${presetTable} ## Sequences -Coordinate multiple effects with staggered timing. Prefer sequences over manual delay stagger. - -### Sequence As type - -\`\`\`ts -{ - effects: (Effect | EffectRef)[]; // REQUIRED - delay?: number; // ms before sequence starts - offset?: number; // ms between each child's animation start - offsetEasing?: string; // easing curve for staggering offsets - sequenceId?: string; // for caching/referencing - conditions?: string[]; // ids referencing the top-level conditions map -} -\`\`\` - -### Template - -\`\`\`ts -{ - interactions: [ - { - key: '[SOURCE_KEY]', - trigger: '[TRIGGER]', - params: [TRIGGER_PARAMS], - sequences: [ - { - offset: [OFFSET_MS], // optional - offsetEasing: '[OFFSET_EASING]', // optional - delay: [DELAY_MS], // optional - effects: [ - // if used \`listContainer\` each item in the list is a target of a child effect - { - effectId: '[EFFECT_ID]', - listContainer: '[LIST_CONTAINER_SELECTOR]', - }, - // if multiple effects are given each generated effect is added to the sequence - ], - }, - ], - }, - ], - effects: { - '[EFFECT_ID]': { - // effect definition (namedEffect, keyframeEffect, or customEffect) - }, - }, -} -\`\`\` - -### Variables - -- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for /vanilla, \`interactKey\` for React). -- \`[TRIGGER]\` — any trigger for time-based animation effects (e.g., \`'viewEnter'\`, \`'activate'\`, \`'interest'\`). -- \`[TRIGGER_PARAMS]\` — trigger-specific parameters (e.g., \`{ type: 'once', threshold: 0.3 }\`). -- \`[OFFSET_MS]\` — ms between each child's animation start. -- \`[OFFSET_EASING]\` — CSS easing string or named easing from \`@wix/motion\`. -- \`[DELAY_MS]\` — optional. Base delay (ms) before the entire sequence starts. -- \`[EFFECT_ID]\` — string key referencing an entry in the top-level \`effects\` map. -- \`[LIST_CONTAINER_SELECTOR]\` — optional. CSS selector for the container whose children will be staggered. - -Reusable sequences can be defined in \`InteractConfig.sequences\` and referenced by \`sequenceId\`. +${fragments.get('sequences', 'full-lean')} --- ## Conditions -Named conditions that gate interactions, effects, or sequences. - -| Type | Predicate | -| :--------- | :------------------------------------------------------------------------ | -| \`media\` | CSS media query condition without \`@media\` (e.g., \`'(min-width: 768px)'\`) | -| \`selector\` | CSS selector; \`&\` is replaced with the base element selector | - -Attach via \`conditions: ['[CONDITION_ID]']\` on interactions, effects, or sequences. On an interaction, conditions gate the entire trigger; on an effect, only that specific effect is skipped. All listed conditions must pass. - -### Examples - -\`\`\`ts -conditions: { - 'desktop': { type: 'media', predicate: '(min-width: 768px)' }, - 'hover-device': { type: 'media', predicate: '(hover: hover)' }, - 'reduced-motion': { type: 'media', predicate: '(prefers-reduced-motion: reduce)' }, - 'odd-items': { type: 'selector', predicate: ':nth-of-type(odd)' }, -} -\`\`\` +${fragments.get('conditions', 'full-lean')} --- @@ -579,26 +492,6 @@ ${fragments.get('element-resolution', 'target')} ## Static API -| Method / Property | Description | -| :---------------------------------- | :------------------------------------------------------------------------------------------------------------ | -| \`Interact.create(config)\` | Initialize with a config. Returns the instance. Store the instance to manage its lifecycle. | -| \`Interact.registerEffects(presets)\` | Register named effect presets. MUST be called before \`create\`. | -| \`Interact.destroy()\` | Tear down all instances. Call on unmount or route change to prevent memory leaks. | -| \`Interact.forceReducedMotion\` | \`boolean\` (default: \`false\`) — force reduced-motion behavior regardless of OS setting. | -| \`Interact.allowA11yTriggers\` | \`boolean\` (default: \`false\`) — enable accessibility trigger variants (\`interest\`, \`activate\`). | -| \`Interact.setup(options)\` | Configure global options for scroll, pointer, and viewEnter systems. Call before \`create\`. See options below. | - -**\`Interact.setup(options)\`** — optional configuration object: - -| Option | Type | Description | -| :--------------------- | :----------------------------- | :-------------------------------------------------------------------- | -| \`scrollOptionsGetter\` | \`() => Partial\` | Function returning defaults for scroll-driven animation configuration | -| \`pointerOptionsGetter\` | \`() => Partial\` | Function returning defaults for pointer-move animation configuration | -| \`viewEnter\` | \`Partial\` | Defaults for all viewEnter triggers (\`threshold\`,\`inset\`) | -| \`allowA11yTriggers\` | \`boolean\` | Enable accessibility trigger variants (use \`interest\` and \`activate\`) | - -Use \`setup()\` when you need to override default observer thresholds or provide global configuration that applies to all interactions of a given trigger type. - -Each \`Interact.create()\` call returns an instance. Store instances and call \`instance.destroy()\` when no longer needed (e.g. on component unmount) to prevent stale listeners and memory leaks. +${fragments.get('static-api', 'full-lean')} `; } diff --git a/packages/interact/_content/templates/integration.mjs b/packages/interact/_content/templates/integration.mjs index 6bb00cc5..e69893ee 100644 --- a/packages/interact/_content/templates/integration.mjs +++ b/packages/interact/_content/templates/integration.mjs @@ -126,25 +126,7 @@ For full effect type syntax (\`keyframeEffect\`, \`namedEffect\`, \`customEffect ## Configuration Schema -### InteractConfig - -\`\`\`typescript -type InteractConfig = { - interactions: Interaction[]; - effects?: Record; - sequences?: Record; - conditions?: Record; -}; -\`\`\` - -| Field | Description | -| :------------- | :---------------------------------------------------------------------- | -| \`interactions\` | Required. Array of interaction definitions binding triggers to effects. | -| \`effects?\` | Reusable effects referenced by \`effectId\` from interactions. | -| \`sequences?\` | Reusable sequence definitions, referenced by \`sequenceId\`. | -| \`conditions?\` | Named conditions (media/container/selector queries), referenced by ID. | - -Each call to \`Interact.create(config)\` creates a new \`Interact\` instance. A single config can define multiple interactions. +${fragments.get('config-structure', 'integration')} ### Interaction @@ -197,38 +179,7 @@ For \`hover\`/\`click\` (and their accessible variants \`interest\`/\`activate\` ## Sequences -Sequences coordinate multiple effects with staggered timing. - -\`\`\`typescript -{ - offset: number, // ms between consecutive items - offsetEasing: string, // Any valid easing string for stagger distribution curve - delay: number, // ms base delay before the sequence starts - effects: [ - /* ... effect definitions */, - ], -} -\`\`\` - -Define reusable sequences in \`InteractConfig.sequences\` and reference by \`sequenceId\`: - -\`\`\`typescript -{ - sequences: { - 'stagger-fade': { - /* ... sequence definition */ - }, - }, - interactions: [ - { - key: \`'[SOURCE_KEY]'\`, - trigger: \`'[TRIGGER]'\`, - params: \`[TRIGGER_PARAMS]\`, - sequences: [{ sequenceId: 'stagger-fade' }], - }, - ], -} -\`\`\` +${fragments.get('sequences', 'integration')} --- @@ -271,15 +222,6 @@ ${fragments.get('fouc', 'code-inject')} ## Static API -Each \`Interact.create(config)\` call returns an instance. Keep a reference if you need to add/remove elements dynamically (vanilla JS) or to destroy a specific instance. Call \`Interact.destroy()\` to tear down all instances at once (e.g. on page navigation). - -| Method / Property | Description | -| :---------------------------------- | :------------------------------------------------------------------------------------------- | -| \`Interact.create(config)\` | Initialize with a config. Returns the instance. Multiple configs create separate instances. | -| \`Interact.registerEffects(presets)\` | Register named effect presets before \`create\`. Required for \`namedEffect\` usage. | -| \`Interact.destroy()\` | Tear down all instances. | -| \`Interact.forceReducedMotion\` | \`boolean\` — force reduced-motion behavior regardless of OS setting. Default: \`false\`. | -| \`Interact.allowA11yTriggers\` | \`boolean\` — enable accessibility triggers (\`interest\`, \`activate\`). Default: \`false\`. | -| \`Interact.setup(options)\` | Configure global defaults for scroll/pointer/viewEnter trigger params. Call before \`create\`. | +${fragments.get('static-api', 'integration')} `; } diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index 9c7a4217..0deab7b0 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -83,7 +83,7 @@ ${fragments.get('pitfalls/overflow-clip', 'short')} customEffect: [CUSTOM_EFFECT_CALLBACK], rangeStart: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } }, rangeEnd: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } }, - easing: \`'[EASING_FUNCTION]'\`, // usually 'linear' + easing: '[EASING_FUNCTION]', // usually 'linear' fill: 'both', effectId: '[UNIQUE_EFFECT_ID]' }, diff --git a/packages/interact/rules/click.md b/packages/interact/rules/click.md index ba94219f..9e521c99 100644 --- a/packages/interact/rules/click.md +++ b/packages/interact/rules/click.md @@ -62,7 +62,7 @@ Use `keyframeEffect` or `namedEffect` when the click should play an animation (C - `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase. - `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`. - `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built, time-based animation effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options. -- `[FILL_MODE]` - optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect. +- `[FILL_MODE]` — optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect. - `[INITIAL_REVERSED_BOOL]` — optional. `true` to start in the finished state so the entire effect is reversed. - `[DURATION_MS]` — animation duration in milliseconds. - `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`. diff --git a/packages/interact/rules/hover.md b/packages/interact/rules/hover.md index 9fcfb2fe..88938e0b 100644 --- a/packages/interact/rules/hover.md +++ b/packages/interact/rules/hover.md @@ -64,12 +64,12 @@ Use `keyframeEffect` or `namedEffect` when the hover should play an animation (C - `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase. - `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`. - `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options. +- `[FILL_MODE]` — usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished. - `[DURATION_MS]` — animation duration in milliseconds. - `[EASING_FUNCTION]` — CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`. - `[DELAY_MS]` — optional delay before the effect starts, in milliseconds. - `[ITERATIONS]` — optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`. - `[ALTERNATE_BOOL]` — optional. `true` to alternate direction on every other iteration (within a single playback). -- `[FILL_MODE]` — usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished. --- diff --git a/packages/interact/rules/integration.md b/packages/interact/rules/integration.md index e5a75121..7dffd861 100644 --- a/packages/interact/rules/integration.md +++ b/packages/interact/rules/integration.md @@ -248,9 +248,9 @@ Define reusable sequences in `InteractConfig.sequences` and reference by `sequen }, interactions: [ { - key: `'[SOURCE_KEY]'`, - trigger: `'[TRIGGER]'`, - params: `[TRIGGER_PARAMS]`, + key: '[SOURCE_KEY]', + trigger: '[TRIGGER]', + params: [TRIGGER_PARAMS], sequences: [{ sequenceId: 'stagger-fade' }], }, ], diff --git a/packages/interact/rules/viewprogress.md b/packages/interact/rules/viewprogress.md index a5758e30..cf06fb1b 100644 --- a/packages/interact/rules/viewprogress.md +++ b/packages/interact/rules/viewprogress.md @@ -82,7 +82,7 @@ These rules help generate scroll-driven interactions using `@wix/interact`. View customEffect: [CUSTOM_EFFECT_CALLBACK], rangeStart: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [START_PERCENTAGE] } }, rangeEnd: { name: '[RANGE_NAME]', offset: { unit: 'percentage', value: [END_PERCENTAGE] } }, - easing: `'[EASING_FUNCTION]'`, // usually 'linear' + easing: '[EASING_FUNCTION]', // usually 'linear' fill: 'both', effectId: '[UNIQUE_EFFECT_ID]' }, diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index 388cf452..a8e1fe1c 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -1,7 +1,7 @@ #!/usr/bin/env node import { readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs'; -import { join, basename, extname, relative } from 'node:path'; +import { join, basename, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; import yaml from 'js-yaml'; @@ -95,58 +95,33 @@ class Fragments { const fragments = new Fragments(join(CONTENT_DIR, 'fragments')); // --------------------------------------------------------------------------- -// 3. Import templates and render +// 3. Import templates and render via manifest // --------------------------------------------------------------------------- +const manifest = [ + { template: 'event-trigger-rule.mjs', triggers: ['click', 'hover'], output: name => `${name}.md` }, + { template: 'viewenter-rule.mjs', triggers: ['viewEnter'], output: () => 'viewenter.md' }, + { template: 'viewprogress-rule.mjs', triggers: ['viewProgress'], output: () => 'viewprogress.md' }, + { template: 'pointermove-rule.mjs', triggers: ['pointerMove'], output: () => 'pointermove.md' }, + { template: 'full-lean.mjs', triggers: null, output: () => 'full-lean.md' }, + { template: 'integration.mjs', triggers: null, output: () => 'integration.md' }, +]; + const outputs = []; -// Event trigger rules: click.md and hover.md -const eventTemplate = await import(join(CONTENT_DIR, 'templates', 'event-trigger-rule.mjs')); -for (const triggerName of ['click', 'hover']) { - const trigger = data.triggers.find(t => t.name === triggerName); - if (!trigger) throw new Error(`Trigger "${triggerName}" not found in triggers.yaml`); - const md = eventTemplate.render(trigger, data, fragments); - outputs.push({ file: `${triggerName}.md`, content: md }); +for (const entry of manifest) { + const mod = await import(join(CONTENT_DIR, 'templates', entry.template)); + if (entry.triggers) { + for (const name of entry.triggers) { + const trigger = data.triggers.find(t => t.name === name); + if (!trigger) throw new Error(`Trigger "${name}" not found in triggers.yaml`); + outputs.push({ file: entry.output(name), content: mod.render(trigger, data, fragments) }); + } + } else { + outputs.push({ file: entry.output(), content: mod.render(data.triggers, data, fragments) }); + } } -// viewenter.md -const viewenterTemplate = await import(join(CONTENT_DIR, 'templates', 'viewenter-rule.mjs')); -const viewEnterTrigger = data.triggers.find(t => t.name === 'viewEnter'); -outputs.push({ - file: 'viewenter.md', - content: viewenterTemplate.render(viewEnterTrigger, data, fragments), -}); - -// viewprogress.md -const viewprogressTemplate = await import(join(CONTENT_DIR, 'templates', 'viewprogress-rule.mjs')); -const viewProgressTrigger = data.triggers.find(t => t.name === 'viewProgress'); -outputs.push({ - file: 'viewprogress.md', - content: viewprogressTemplate.render(viewProgressTrigger, data, fragments), -}); - -// pointermove.md -const pointermoveTemplate = await import(join(CONTENT_DIR, 'templates', 'pointermove-rule.mjs')); -const pointerMoveTrigger = data.triggers.find(t => t.name === 'pointerMove'); -outputs.push({ - file: 'pointermove.md', - content: pointermoveTemplate.render(pointerMoveTrigger, data, fragments), -}); - -// full-lean.md -const fullLeanTemplate = await import(join(CONTENT_DIR, 'templates', 'full-lean.mjs')); -outputs.push({ - file: 'full-lean.md', - content: fullLeanTemplate.render(data.triggers, data, fragments), -}); - -// integration.md -const integrationTemplate = await import(join(CONTENT_DIR, 'templates', 'integration.mjs')); -outputs.push({ - file: 'integration.md', - content: integrationTemplate.render(data.triggers, data, fragments), -}); - // --------------------------------------------------------------------------- // 4. Write outputs // --------------------------------------------------------------------------- diff --git a/yarn.lock b/yarn.lock index 4bd98bd3..e1681071 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1392,6 +1392,7 @@ __metadata: "@wix/motion": "npm:^2.1.5" fastdom: "npm:^1.0.12" fizban: "npm:^0.7.2" + js-yaml: "npm:^4.1.0" jsdom: "npm:^24.0.0" kuliso: "npm:^0.4.13" react: "npm:^18.3.1" @@ -3554,7 +3555,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.1": +"js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": version: 4.1.1 resolution: "js-yaml@npm:4.1.1" dependencies: From 9d81c4ffb2b9901294ca202109503a3b9eddc072 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 15:28:35 +0300 Subject: [PATCH 04/27] PR fixes --- packages/interact/_content/data/triggers.yaml | 2 -- packages/interact/_content/fragments/sequences.md | 2 +- packages/interact/rules/full-lean.md | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml index e8fd6b87..d651f6b1 100644 --- a/packages/interact/_content/data/triggers.yaml +++ b/packages/interact/_content/data/triggers.yaml @@ -42,7 +42,6 @@ triggers: add: "applies the style state on enter. Leave does NOT remove it." remove: "removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`." clear: "clears all previously applied style states on enter. Use to reset multiple stacked `'add'` style changes at once (e.g. a \"reset\" hover area that undoes several accumulated states)." - fillNote: "while hovering" - name: click a11yAlias: activate @@ -88,7 +87,6 @@ triggers: add: "applies the style state. Does not remove on subsequent clicks." remove: "removes a previously applied style state." clear: "clears all previously applied style states. Useful for resetting multiple stacked style states at once." - fillNote: "while finished" - name: viewEnter category: viewport diff --git a/packages/interact/_content/fragments/sequences.md b/packages/interact/_content/fragments/sequences.md index 1eddd99d..6c374548 100644 --- a/packages/interact/_content/fragments/sequences.md +++ b/packages/interact/_content/fragments/sequences.md @@ -50,7 +50,7 @@ Coordinate multiple effects with staggered timing. Prefer sequences over manual ### Variables -- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for /vanilla, `interactKey` for React). +- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web/vanilla, `interactKey` for React). - `[TRIGGER]` — any trigger for time-based animation effects (e.g., `'viewEnter'`, `'activate'`, `'interest'`). - `[TRIGGER_PARAMS]` — trigger-specific parameters (e.g., `{ type: 'once', threshold: 0.3 }`). - `[OFFSET_MS]` — ms between each child's animation start. diff --git a/packages/interact/rules/full-lean.md b/packages/interact/rules/full-lean.md index 43b4b131..62afec5e 100644 --- a/packages/interact/rules/full-lean.md +++ b/packages/interact/rules/full-lean.md @@ -545,7 +545,7 @@ Coordinate multiple effects with staggered timing. Prefer sequences over manual ### Variables -- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for /vanilla, `interactKey` for React). +- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web/vanilla, `interactKey` for React). - `[TRIGGER]` — any trigger for time-based animation effects (e.g., `'viewEnter'`, `'activate'`, `'interest'`). - `[TRIGGER_PARAMS]` — trigger-specific parameters (e.g., `{ type: 'once', threshold: 0.3 }`). - `[OFFSET_MS]` — ms between each child's animation start. From c1fb058084b4e24bce174045c525685448b430f5 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 15:48:08 +0300 Subject: [PATCH 05/27] refactor: simplify rules build pipeline and fix Prettier formatting - Refactor PROSE in event-trigger-rule.mjs: base + overrides pattern eliminates duplication, unifies minor wording inconsistencies - Move fillGuidance/compositeOperations prose from effects.yaml into full-lean.mjs (YAML for structured data, not prose) - Unify render() signature to (data, fragments) across all templates - Import capitalize helper in full-lean.mjs instead of inlining - Add --check flag to build-rules.mjs for CI freshness verification without modifying the working tree - Remove unused aliasOf entries from triggers.yaml - Add _content/fragments/ to .prettierignore (section markers are build pipeline syntax) - Apply Prettier formatting to all source files Co-authored-by: Cursor --- .../rules_build_pipeline_ffe2a9f3.plan.md | 148 +++++++++++------- .github/workflows/ci.yml | 4 +- .prettierignore | 3 + packages/interact/_content/data/effects.yaml | 17 +- packages/interact/_content/data/meta.yaml | 14 +- packages/interact/_content/data/triggers.yaml | 48 +++--- .../_content/templates/event-trigger-rule.mjs | 107 ++++++++----- .../interact/_content/templates/full-lean.mjs | 43 ++--- .../_content/templates/integration.mjs | 2 +- .../_content/templates/pointermove-rule.mjs | 2 +- .../_content/templates/viewenter-rule.mjs | 2 +- .../_content/templates/viewprogress-rule.mjs | 2 +- packages/interact/rules/click.md | 6 +- packages/interact/rules/hover.md | 2 +- packages/interact/scripts/build-rules.mjs | 56 +++++-- 15 files changed, 264 insertions(+), 192 deletions(-) diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md index dfb3c74a..f95d8887 100644 --- a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md +++ b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md @@ -3,7 +3,7 @@ name: Rules Build Pipeline overview: Implement a build pipeline for `packages/interact/rules/` using structured YAML data + JavaScript template functions + markdown fragments, eliminating all content duplication and ensuring a single source of truth. todos: - id: scaffold - content: "Create `_content/` directory structure: `data/`, `fragments/`, `templates/`, and `scripts/build-rules.mjs` skeleton" + content: 'Create `_content/` directory structure: `data/`, `fragments/`, `templates/`, and `scripts/build-rules.mjs` skeleton' status: completed - id: data-triggers content: Create `data/triggers.yaml` with all 9 trigger definitions (hover, click, interest, activate, viewEnter, viewProgress, pointerMove, animationEnd, pageVisible) — pull field names from actual TS types @@ -12,7 +12,7 @@ todos: content: Create `data/effects.yaml` (effect field definitions, presets table, ranges, easings) and `data/meta.yaml` (package metadata) status: completed - id: fragments - content: "Extract ~12 fragments from existing rule files: fouc, element-resolution, 6 pitfalls, quick-start, multiple-effects-note, custom-effect-intro, sequences-intro" + content: 'Extract ~12 fragments from existing rule files: fouc, element-resolution, 6 pitfalls, quick-start, multiple-effects-note, custom-effect-intro, sequences-intro' status: completed - id: template-event content: Create `templates/event-trigger-rule.mjs` — generates click.md and hover.md from trigger data + shared fragments @@ -27,7 +27,7 @@ todos: content: Create `templates/full-lean.mjs` and `templates/integration.mjs` — the two comprehensive reference files status: completed - id: build-script - content: "Implement `scripts/build-rules.mjs`: YAML loading, fragment parsing, template orchestration, file writing" + content: 'Implement `scripts/build-rules.mjs`: YAML loading, fragment parsing, template orchestration, file writing' status: completed - id: integrate content: Add `build:rules` script to package.json, add `js-yaml` devDependency, update CI workflow @@ -36,31 +36,31 @@ todos: content: Run build, diff generated output against current rule files, verify no information loss, fix any discrepancies status: completed - id: fix-lockfile - content: "CI fix: run `yarn install` so yarn.lock includes the new `js-yaml` resolution, commit the updated lockfile" + content: 'CI fix: run `yarn install` so yarn.lock includes the new `js-yaml` resolution, commit the updated lockfile' status: completed - id: fix-dead-fragments - content: "Remove dead fragment files `custom-effect-intro.md` and `sequences-intro.md` (unused by any template; YAML prose fields are used instead)" + content: 'Remove dead fragment files `custom-effect-intro.md` and `sequences-intro.md` (unused by any template; YAML prose fields are used instead)' status: completed - id: fix-yaml-prose - content: "Move 15+ prose description fields (timeEffectIntro, sourceKeyDesc, etc.) out of triggers.yaml into the event-trigger template directly, keeping only structured data in YAML" + content: 'Move 15+ prose description fields (timeEffectIntro, sourceKeyDesc, etc.) out of triggers.yaml into the event-trigger template directly, keeping only structured data in YAML' status: completed - id: fix-fill-variables - content: "Collapse `buildVariablesMidFill`/`buildVariablesEndFill` into a single `buildVariables` function — always place `[FILL_MODE]` after `[NAMED_EFFECT_DEFINITION]` (matching the config block order). Remove `fillModeAtEnd` from triggers.yaml." + content: 'Collapse `buildVariablesMidFill`/`buildVariablesEndFill` into a single `buildVariables` function — always place `[FILL_MODE]` after `[NAMED_EFFECT_DEFINITION]` (matching the config block order). Remove `fillModeAtEnd` from triggers.yaml.' status: completed - id: fix-build-manifest - content: "Replace repetitive template orchestration in build-rules.mjs (lines 104-148) with a data-driven manifest array" + content: 'Replace repetitive template orchestration in build-rules.mjs (lines 104-148) with a data-driven manifest array' status: completed - id: fix-viewprogress-backtick - content: "Fix stray backtick template literal in viewprogress-rule.mjs line 86 — normalize to plain string like other templates" + content: 'Fix stray backtick template literal in viewprogress-rule.mjs line 86 — normalize to plain string like other templates' status: completed - id: fix-shared-fragments - content: "Extract duplicated sections (Conditions, Static API, Config Structure, Sequences) from full-lean.mjs and integration.mjs into shared fragments" + content: 'Extract duplicated sections (Conditions, Static API, Config Structure, Sequences) from full-lean.mjs and integration.mjs into shared fragments' status: completed - id: fix-ci-freshness - content: "Add a freshness check step to `.github/workflows/ci.yml`: `yarn workspace @wix/interact build:rules && git diff --exit-code packages/interact/rules/`" + content: 'Add a freshness check step to `.github/workflows/ci.yml`: `yarn workspace @wix/interact build:rules && git diff --exit-code packages/interact/rules/`' status: completed - id: fix-regenerate - content: "Run `build:rules`, verify output, commit regenerated rules/*.md files" + content: 'Run `build:rules`, verify output, commit regenerated rules/*.md files' status: completed isProject: false --- @@ -90,8 +90,6 @@ graph LR Script --> Rules ``` - - All source files live under `packages/interact/_content/`. The build script reads them and writes the final `.md` files to `packages/interact/rules/`. ## Source Layer @@ -105,30 +103,41 @@ triggers: - name: hover a11yAlias: interest a11yNote: "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus." - category: event # event | viewport | scroll | pointer | chain + category: event # event | viewport | scroll | pointer | chain supportsTimeEffect: true supportsStateEffect: true supportsScrubEffect: false supportsCustomEffect: true - params: [] # no trigger params + params: [] # no trigger params pitfalls: - - id: hit-area # references fragments/pitfalls/hit-area.md - templateFields: # which optional fields to show in config templates - timeEffect: [triggerType, keyframeEffect, namedEffect, fill, duration, easing, delay, iterations, alternate] + - id: hit-area # references fragments/pitfalls/hit-area.md + templateFields: # which optional fields to show in config templates + timeEffect: + [ + triggerType, + keyframeEffect, + namedEffect, + fill, + duration, + easing, + delay, + iterations, + alternate, + ] stateEffect: [stateAction, transition, transitionProperties] customEffect: [triggerType, customEffect, duration, easing] sequence: [triggerType, offset, offsetEasing] - triggerTypeDescriptions: # trigger-specific wording for each triggerType value - alternate: "plays forward on enter, reverses on leave" - repeat: "restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses" - once: "plays once on the first enter and never again" - state: "resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`)" + triggerTypeDescriptions: # trigger-specific wording for each triggerType value + alternate: 'plays forward on enter, reverses on leave' + repeat: 'restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses' + once: 'plays once on the first enter and never again' + state: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`)' stateActionDescriptions: - toggle: "applies the style state on enter, removes on leave" - add: "applies the style state on enter. Leave does NOT remove it" - remove: "removes a previously applied style state on enter" - clear: "clears all previously applied style states on enter" - fillNote: "while hovering" # trigger-specific fill context + toggle: 'applies the style state on enter, removes on leave' + add: 'applies the style state on enter. Leave does NOT remove it' + remove: 'removes a previously applied style state on enter' + clear: 'clears all previously applied style states on enter' + fillNote: 'while hovering' # trigger-specific fill context - name: click a11yAlias: activate @@ -141,19 +150,32 @@ triggers: params: [] pitfalls: [] templateFields: - timeEffect: [triggerType, keyframeEffect, namedEffect, fill, reversed, duration, easing, delay, iterations, alternate, effectId] + timeEffect: + [ + triggerType, + keyframeEffect, + namedEffect, + fill, + reversed, + duration, + easing, + delay, + iterations, + alternate, + effectId, + ] # ... (click includes reversed + effectId that hover omits) triggerTypeDescriptions: - alternate: "plays forward on first click, reverses on next click" - repeat: "restarts the animation from the beginning on each click" - once: "plays once on the first click and never again" - state: "resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`)" + alternate: 'plays forward on first click, reverses on next click' + repeat: 'restarts the animation from the beginning on each click' + once: 'plays once on the first click and never again' + state: 'resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`)' stateActionDescriptions: - toggle: "applies the style state, removes it on next click" - add: "applies the style state. Does not remove on subsequent clicks" - remove: "removes a previously applied style state" - clear: "clears all previously applied style states. Useful for resetting multiple stacked style states at once" - fillNote: "while finished" + toggle: 'applies the style state, removes it on next click' + add: 'applies the style state. Does not remove on subsequent clicks' + remove: 'removes a previously applied style state' + clear: 'clears all previously applied style states. Useful for resetting multiple stacked style states at once' + fillNote: 'while finished' # ... viewEnter, viewProgress, pointerMove, animationEnd entries follow ``` @@ -163,7 +185,7 @@ The trigger entries for `viewEnter`, `viewProgress`, `pointerMove`, and `animati ```yaml fillGuidance: - both: "use for scroll-driven, pointer-driven, and toggling effects (alternate, repeat, state)" + both: 'use for scroll-driven, pointer-driven, and toggling effects (alternate, repeat, state)' backwards: "use for entrance animations with triggerType 'once' when the final keyframe matches the element's base style" triggerTypes: [once, repeat, alternate, state] @@ -181,10 +203,10 @@ presets: mouse: [TrackMouse, Tilt3DMouse, ...] rangeNames: - entry: "Element entering viewport" - exit: "Element exiting viewport" - contain: "After entry range and before exit range" - cover: "Full range from entry through contain and exit" + entry: 'Element entering viewport' + exit: 'Element exiting viewport' + contain: 'After entry range and before exit range' + cover: 'Full range from entry through contain and exit' entry-crossing: "From element's leading edge entering to trailing edge entering" exit-crossing: "From element's leading edge exiting to trailing edge exiting" ``` @@ -192,13 +214,13 @@ rangeNames: `**meta.yaml**` — package metadata: ```yaml -packageName: "@wix/interact" -presetsPackage: "@wix/motion-presets" -installCommand: "npm install @wix/interact @wix/motion-presets" +packageName: '@wix/interact' +presetsPackage: '@wix/motion-presets' +installCommand: 'npm install @wix/interact @wix/motion-presets' entryPoints: - web: "@wix/interact/web" - react: "@wix/interact/react" - vanilla: "@wix/interact" + web: '@wix/interact/web' + react: '@wix/interact/react' + vanilla: '@wix/interact' ``` ### 2. Markdown fragments (`_content/fragments/`) @@ -207,7 +229,6 @@ Each fragment has `` markers for different detail levels. **Planned fragments** (extracted from the ~15 duplicated concepts): - | Fragment file | Sections | Used by | | ------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------- | | `fouc.md` | `#short`, `#long`, `#code-generate`, `#code-web`, `#code-react`, `#code-vanilla`, `#rules` | full-lean, integration, viewenter | @@ -223,12 +244,10 @@ Each fragment has `` markers for different detail levels. | `custom-effect-intro.md` | `#default` | click, hover, viewenter | | `sequences-intro.md` | `#short` (parameterized with `{{triggerName}}`) | click, hover, viewenter | - ### 3. Templates (`_content/templates/`) Each template is a `.mjs` file exporting a function that receives data + fragments and returns a markdown string. - | Template | Generates | Key logic | | ------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `event-trigger-rule.mjs` | `click.md`, `hover.md` | Parameterized by trigger data. Generates Rule 1 (TimeEffect), Rule 2 (StateEffect), Rule 3 (customEffect), Rule 4 (sequences). The config block fields, variable descriptions, and trigger-specific wording all come from `triggers.yaml`. | @@ -238,7 +257,6 @@ Each template is a `.mjs` file exporting a function that receives data + fragmen | `full-lean.mjs` | `full-lean.md` | Comprehensive reference. Pulls from all data + fragments. | | `integration.mjs` | `integration.md` | Integration guide. Pulls from meta + triggers data + fragments. | - **Template function signature:** ```javascript @@ -247,7 +265,7 @@ export function render(trigger, data, fragments) { return `# ${capitalize(trigger.name)} Trigger Rules for ${data.meta.packageName} ... ${trigger.a11yAlias ? `**CRITICAL — Accessible ${trigger.name}**: ${trigger.a11yNote}` : ''} -${trigger.pitfalls.map(p => fragments.get(`pitfalls/${p.id}`, trigger.name)).join('\n')} +${trigger.pitfalls.map((p) => fragments.get(`pitfalls/${p.id}`, trigger.name)).join('\n')} ...`; } ``` @@ -284,7 +302,7 @@ class Fragments { ```json "build:rules": "node scripts/build-rules.mjs" - + ``` @@ -327,6 +345,7 @@ The CI `Install dependencies` step runs `yarn install --immutable` which refuses These fields only vary between hover and click. The viewEnter/viewProgress/pointerMove triggers don't use them at all (they have their own hardcoded templates). **Action:** + - Remove all prose description fields from `triggers.yaml` (hover + click entries). - Move the hover/click prose differences into `event-trigger-rule.mjs` directly, keyed by a simple flag or `trigger.name` check. These are small trigger-specific word choices (e.g. "while hovering" vs "while finished"), not reusable data. - Keep only structured data in YAML: name, category, support flags, params, templateFields, pitfalls, triggerType/stateAction enum descriptions, `a11yAlias`, `a11yNote`, `defaultTriggerType`, `showMultipleEffectsNote`. @@ -336,6 +355,7 @@ These fields only vary between hover and click. The viewEnter/viewProgress/point [`event-trigger-rule.mjs`](packages/interact/_content/templates/event-trigger-rule.mjs) has two nearly identical functions — `buildVariablesMidFill` (click) and `buildVariablesEndFill` (hover) — that differ only in where `[FILL_MODE]` appears in the variables list. The code block template itself always shows `fill` right after `namedEffect`/`keyframeEffect`, so the variables list should match that order. **Action:** + - Collapse into a single `buildVariables` function that always places `[FILL_MODE]` after `[NAMED_EFFECT_DEFINITION]` (mid position, matching the config block order). - Remove `fillModeAtEnd` and `fillModeDash` from `triggers.yaml`. - Use a consistent em-dash separator for all triggers. @@ -373,9 +393,17 @@ Lines 104-148 of [`build-rules.mjs`](packages/interact/scripts/build-rules.mjs) ```javascript const manifest = [ - { template: 'event-trigger-rule.mjs', triggers: ['click', 'hover'], output: name => `${name}.md` }, + { + template: 'event-trigger-rule.mjs', + triggers: ['click', 'hover'], + output: (name) => `${name}.md`, + }, { template: 'viewenter-rule.mjs', triggers: ['viewEnter'], output: () => 'viewenter.md' }, - { template: 'viewprogress-rule.mjs', triggers: ['viewProgress'], output: () => 'viewprogress.md' }, + { + template: 'viewprogress-rule.mjs', + triggers: ['viewProgress'], + output: () => 'viewprogress.md', + }, { template: 'pointermove-rule.mjs', triggers: ['pointerMove'], output: () => 'pointermove.md' }, { template: 'full-lean.mjs', triggers: null, output: () => 'full-lean.md' }, { template: 'integration.mjs', triggers: null, output: () => 'integration.md' }, @@ -385,7 +413,7 @@ for (const entry of manifest) { const mod = await import(join(CONTENT_DIR, 'templates', entry.template)); if (entry.triggers) { for (const name of entry.triggers) { - const trigger = data.triggers.find(t => t.name === name); + const trigger = data.triggers.find((t) => t.name === name); if (!trigger) throw new Error(`Trigger "${name}" not found in triggers.yaml`); outputs.push({ file: entry.output(name), content: mod.render(trigger, data, fragments) }); } @@ -415,6 +443,7 @@ Line 86 of [`viewprogress-rule.mjs`](packages/interact/_content/templates/viewpr **Action:** Extract each into a new fragment with `` / `` section markers (same pattern as `fouc.md`). This brings these templates closer to the single-source-of-truth goal and makes future edits to these shared concepts a one-file change. New fragment files: + - `_content/fragments/conditions.md` — `#full-lean`, `#integration` - `_content/fragments/static-api.md` — `#full-lean`, `#integration` - `_content/fragments/config-structure.md` — `#full-lean`, `#integration` @@ -447,4 +476,3 @@ After all fixes above, run `build:rules` and commit the regenerated `rules/*.md` - **README**: Same meta.yaml + triggers.yaml generates README sections - **Validation**: Add `scripts/validate-rules.mjs` that imports actual TS types and cross-checks trigger names, param fields, effect fields against the YAML data - **motion-presets rules**: The build mechanism is scoped to `packages/interact/` but the architecture (YAML + fragments + templates) can be replicated in `packages/motion-presets/` with shared fragments if needed - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 70be78de..17288036 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,9 +56,7 @@ jobs: run: yarn build - name: Verify generated rules are up to date - run: | - yarn workspace @wix/interact build:rules - git diff --exit-code packages/interact/rules/ + run: yarn workspace @wix/interact build:rules --check - name: Lint run: yarn lint diff --git a/.prettierignore b/.prettierignore index ad72a0ac..5601c3b8 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,6 @@ package-lock.json **/build/** index.html .yarnrc.yml + +# Build pipeline fragments use markers that Prettier must not reformat +packages/interact/_content/fragments/ diff --git a/packages/interact/_content/data/effects.yaml b/packages/interact/_content/data/effects.yaml index b29c08ad..869a0dcb 100644 --- a/packages/interact/_content/data/effects.yaml +++ b/packages/interact/_content/data/effects.yaml @@ -1,7 +1,3 @@ -fillGuidance: - both: "use for scroll-driven (`viewProgress`), pointer-driven (`pointerMove`), and toggling effects (`hover`/`click` with `alternate`, `repeat`, or `state` type)." - backwards: "use for entrance animations with `type: 'once'` when the element's own CSS already matches the final keyframe (applies the initial keyframe during any `delay`)." - triggerTypes: - once - repeat @@ -119,14 +115,9 @@ presets: - BlobMouse rangeNames: - entry: "Element entering viewport" - exit: "Element exiting viewport" - contain: "After `entry` range and before `exit` range" - cover: "Full range from `entry` through `contain` and `exit`" + entry: 'Element entering viewport' + exit: 'Element exiting viewport' + contain: 'After `entry` range and before `exit` range' + cover: 'Full range from `entry` through `contain` and `exit`' entry-crossing: "From element's leading edge entering to trailing edge entering" exit-crossing: "From element's leading edge exiting to trailing edge exiting" - -compositeOperations: - replace: "fully replaces prior values." - add: "concatenates transform/filter functions after any existing ones (e.g. existing `translateX(10px)` + added `translateY(20px)` → both apply)." - accumulate: "merges arguments of matching functions (e.g. `translateX(10px)` + `translateX(20px)` → `translateX(30px)`); non-matching functions concatenate like `'add'`." diff --git a/packages/interact/_content/data/meta.yaml b/packages/interact/_content/data/meta.yaml index e34ad310..190365bc 100644 --- a/packages/interact/_content/data/meta.yaml +++ b/packages/interact/_content/data/meta.yaml @@ -1,8 +1,8 @@ -packageName: "@wix/interact" -presetsPackage: "@wix/motion-presets" -motionPackage: "@wix/motion" -installCommand: "npm install @wix/interact @wix/motion-presets" +packageName: '@wix/interact' +presetsPackage: '@wix/motion-presets' +motionPackage: '@wix/motion' +installCommand: 'npm install @wix/interact @wix/motion-presets' entryPoints: - web: "@wix/interact/web" - react: "@wix/interact/react" - vanilla: "@wix/interact" + web: '@wix/interact/web' + react: '@wix/interact/react' + vanilla: '@wix/interact' diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml index d651f6b1..3583ca32 100644 --- a/packages/interact/_content/data/triggers.yaml +++ b/packages/interact/_content/data/triggers.yaml @@ -33,15 +33,15 @@ triggers: - duration - easing triggerTypeDescriptions: - alternate: "plays forward on enter, reverses on leave. Default. Most common for hover." - repeat: "restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses." - once: "plays once on the first enter and never again." - state: "resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`)." + alternate: 'plays forward on enter, reverses on leave. Default. Most common for hover.' + repeat: 'restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses.' + once: 'plays once on the first enter and never again.' + state: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`).' stateActionDescriptions: - toggle: "applies the style state on enter, removes on leave. Default." - add: "applies the style state on enter. Leave does NOT remove it." - remove: "removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`." - clear: "clears all previously applied style states on enter. Use to reset multiple stacked `'add'` style changes at once (e.g. a \"reset\" hover area that undoes several accumulated states)." + toggle: 'applies the style state on enter, removes on leave. Default.' + add: 'applies the style state on enter. Leave does NOT remove it.' + remove: 'removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`.' + clear: 'clears all previously applied style states on enter. Use to reset multiple stacked `''add''` style changes at once (e.g. a "reset" hover area that undoes several accumulated states).' - name: click a11yAlias: activate @@ -78,15 +78,15 @@ triggers: - duration - easing triggerTypeDescriptions: - alternate: "plays forward on first click, reverses on next click. Default." - repeat: "restarts the animation from the beginning on each click." - once: "plays once on the first click and never again." - state: "resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`)." + alternate: 'plays forward on first click, reverses on next click. Default.' + repeat: 'restarts the animation from the beginning on each click.' + once: 'plays once on the first click and never again.' + state: 'resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`).' stateActionDescriptions: - toggle: "applies the style state, removes it on next click. Default." - add: "applies the style state. Does not remove on subsequent clicks." - remove: "removes a previously applied style state." - clear: "clears all previously applied style states. Useful for resetting multiple stacked style states at once." + toggle: 'applies the style state, removes it on next click. Default.' + add: 'applies the style state. Does not remove on subsequent clicks.' + remove: 'removes a previously applied style state.' + clear: 'clears all previously applied style states. Useful for resetting multiple stacked style states at once.' - name: viewEnter category: viewport @@ -99,7 +99,7 @@ triggers: - name: threshold type: number optional: true - description: "Number between 0–1 indicating how much of the source element must be visible to trigger (e.g. `0.3` = 30%)." + description: 'Number between 0–1 indicating how much of the source element must be visible to trigger (e.g. `0.3` = 30%).' - name: inset type: string optional: true @@ -131,11 +131,11 @@ triggers: - name: hitArea type: "'root' | 'self'" optional: true - description: "determines where mouse movement is tracked" + description: 'determines where mouse movement is tracked' - name: axis type: "'x' | 'y'" optional: true - description: "restricts pointer tracking to a single axis" + description: 'restricts pointer tracking to a single axis' pitfalls: - id: hit-area @@ -150,7 +150,7 @@ triggers: - name: effectId type: string optional: false - description: "ID of the preceding effect" + description: 'ID of the preceding effect' - name: pageVisible category: event @@ -161,11 +161,3 @@ triggers: defaultTriggerType: null params: [] pitfalls: [] - - - name: activate - aliasOf: click - category: event - - - name: interest - aliasOf: hover - category: event diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index f0f2d12f..8ed7e5a4 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,53 +1,76 @@ import { capitalize } from './_helpers.mjs'; -const PROSE = { +const TRIGGER_OVERRIDES = { hover: { - timeEffectIntro: "Use `keyframeEffect` or `namedEffect` when the hover should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior.", - stateEffectIntro: "Use `transition` or `transitionProperties` when the hover should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Set `stateAction` on the effect to control how the style is applied.", - customEffectIntro: "Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline.", - sequencesIntro: "Use sequences when a hover should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior.", - fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", - sourceKeyDesc: "The element that listens for hover.", - targetKeyDesc: "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", - fillModeDesc: "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.", - namedEffectDesc: "object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.", - easingDesc: "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.", - iterationsDesc: "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", - alternateDesc: "optional. `true` to alternate direction on every other iteration (within a single playback).", - customEffectCallbackDesc: "function with signature `(target: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1.", - sequenceEffectDefDesc: "a definition of or a reference to a time-based animation effect.", - sequenceOffsetEasingDesc: "easing curve for the offset staggering distribution. CSS easing string, or named easing from `@wix/motion`. Defaults to `'linear'`.", + fillCritical: + "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", + sourceKeyDesc: 'The element that listens for hover.', + targetKeyDesc: + "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", + fillModeDesc: + "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.", + easingDesc: + "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.", + iterationsDesc: + "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", + sequenceOffsetEasingDesc: + "easing curve for the offset staggering distribution. CSS easing string, or named easing from `@wix/motion`. Defaults to `'linear'`.", }, click: { - timeEffectIntro: "Use `keyframeEffect` or `namedEffect` when the click should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior.", - stateEffectIntro: "Use `transition` or `transitionProperties` when the click should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Uses the `transition` CSS property. Set `stateAction` on the effect to control how the style is applied.", - customEffectIntro: "Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation, randomized behavior). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline.", - sequencesIntro: "Use sequences when a click should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior.", - fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.", - sourceKeyDesc: "The element that listens for clicks.", - targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", - fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", - namedEffectDesc: "object with properties of pre-built, time-based animation effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.", - easingDesc: "CSS easing string, or named easing from `@wix/motion`.", - iterationsDesc: "optional. Number of iterations, or `Infinity` for continuous loops.", - alternateDesc: "optional. `true` to alternate direction on every other iteration (within a single playback). Different from `triggerType: 'alternate'` which alternates per click.", - customEffectCallbackDesc: "function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with target element and `progress` from 0 to 1.", - sequenceEffectDefDesc: "a definition of, or a reference to a time-based animation effect.", - sequenceOffsetEasingDesc: "easing curve for the offset staggering distribution. Defaults to `'linear'`.", + stateEffectExtra: ' Uses the `transition` CSS property.', + customEffectExtra: ', randomized behavior', + fillCritical: + "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.", + sourceKeyDesc: 'The element that listens for clicks.', + targetKeyDesc: + "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", + fillModeDesc: + "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", + easingDesc: 'CSS easing string, or named easing from `@wix/motion`.', + iterationsDesc: 'optional. Number of iterations, or `Infinity` for continuous loops.', + alternateExtra: " Different from `triggerType: 'alternate'` which alternates per click.", + sequenceOffsetEasingDesc: + "easing curve for the offset staggering distribution. Defaults to `'linear'`.", }, }; -export function render(trigger, data, fragments) { +function buildProse(name) { + const o = TRIGGER_OVERRIDES[name]; + return { + timeEffectIntro: `Use \`keyframeEffect\` or \`namedEffect\` when the ${name} should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior.`, + stateEffectIntro: `Use \`transition\` or \`transitionProperties\` when the ${name} should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations.${o.stateEffectExtra || ''} Set \`stateAction\` on the effect to control how the style is applied.`, + customEffectIntro: `Use \`customEffect\` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation${o.customEffectExtra || ''}). The callback receives the target element and a \`progress\` value (0–1) driven by the animation timeline.`, + sequencesIntro: `Use sequences when a ${name} should sync/stagger animations across multiple elements. Set \`triggerType\` on the sequence config to control playback behavior.`, + fillCritical: o.fillCritical, + sourceKeyDesc: o.sourceKeyDesc, + targetKeyDesc: o.targetKeyDesc, + fillModeDesc: o.fillModeDesc, + namedEffectDesc: + 'object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.', + easingDesc: o.easingDesc, + iterationsDesc: o.iterationsDesc, + alternateDesc: `optional. \`true\` to alternate direction on every other iteration (within a single playback).${o.alternateExtra || ''}`, + customEffectCallbackDesc: `function with signature \`(element: HTMLElement, progress: number) => void\`. Called on each animation frame with the target element and \`progress\` from 0 to 1.`, + sequenceEffectDefDesc: 'a definition of or a reference to a time-based animation effect.', + sequenceOffsetEasingDesc: o.sequenceOffsetEasingDesc, + }; +} + +export function render(data, fragments) { + const trigger = data.trigger; const name = trigger.name; const Name = capitalize(name); - const prose = PROSE[name]; + const prose = buildProse(name); const hasReversed = trigger.templateFields.timeEffect.includes('reversed'); const hasEffectId = trigger.templateFields.timeEffect.includes('effectId'); const showMultipleEffects = trigger.showMultipleEffectsNote; - const pitfallsBlock = trigger.pitfalls.length > 0 - ? '\n' + trigger.pitfalls.map(p => fragments.get(`pitfalls/${p.id}`, name)).join('\n') + '\n' - : ''; + const pitfallsBlock = + trigger.pitfalls.length > 0 + ? '\n' + + trigger.pitfalls.map((p) => fragments.get(`pitfalls/${p.id}`, name)).join('\n') + + '\n' + : ''; const multipleEffectsNote = showMultipleEffects ? `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event` })}\n` @@ -58,13 +81,9 @@ export function render(trigger, data, fragments) { ? ` },\n // additional effects targeting other elements can be added here` : ` }`; - const reversedField = hasReversed - ? `\n reversed: [INITIAL_REVERSED_BOOL],` - : ''; + const reversedField = hasReversed ? `\n reversed: [INITIAL_REVERSED_BOOL],` : ''; - const effectIdField = hasEffectId - ? `\n effectId: '[UNIQUE_EFFECT_ID]'` - : ''; + const effectIdField = hasEffectId ? `\n effectId: '[UNIQUE_EFFECT_ID]'` : ''; const reversedVar = hasReversed ? `\n- \`[INITIAL_REVERSED_BOOL]\` — optional. \`true\` to start in the finished state so the entire effect is reversed.` @@ -178,7 +197,9 @@ ${rule23Closing} - \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. - \`[STATE_ACTION]\` — \`stateAction\` on the effect. One of: -${Object.entries(trigger.stateActionDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`).join('\n')} +${Object.entries(trigger.stateActionDescriptions) + .map(([k, v]) => ` - \`'${k}'\` — ${v}`) + .join('\n')} - \`[CSS_PROP]\` — CSS property name as a string in camelCase format (e.g. \`'backgroundColor'\`, \`'borderRadius'\`, \`'opacity'\`). - \`[VALUE]\` — target CSS value for the property. - \`[DURATION_MS]\` — transition duration in milliseconds. diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index eefc56fe..86466bcf 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -1,22 +1,25 @@ -export function render(triggers, data, fragments) { - const easingList = data.effects.easings.map(e => `\`'${e}'\``).join(', '); - - const presetEntries = Object.entries(data.effects.presets) - .map(([category, names]) => ({ - label: category.charAt(0).toUpperCase() + category.slice(1), - value: `\`${names.join('`, `')}\``, - })); - const maxPresetLen = Math.max(...presetEntries.map(e => e.value.length)); +import { capitalize } from './_helpers.mjs'; + +export function render(data, fragments) { + const easingList = data.effects.easings.map((e) => `\`'${e}'\``).join(', '); + + const presetEntries = Object.entries(data.effects.presets).map(([category, names]) => ({ + label: capitalize(category), + value: `\`${names.join('`, `')}\``, + })); + const maxPresetLen = Math.max(...presetEntries.map((e) => e.value.length)); const presetTable = presetEntries - .map(e => ` | ${e.label.padEnd(8)} | ${e.value.padEnd(maxPresetLen)} |`) + .map((e) => ` | ${e.label.padEnd(8)} | ${e.value.padEnd(maxPresetLen)} |`) .join('\n'); - const rangeEntries = Object.entries(data.effects.rangeNames) - .map(([name, desc]) => ({ name: `\`${name}\``, desc })); - const maxNameLen = Math.max(...rangeEntries.map(e => e.name.length)); - const maxDescLen = Math.max(...rangeEntries.map(e => e.desc.length)); + const rangeEntries = Object.entries(data.effects.rangeNames).map(([name, desc]) => ({ + name: `\`${name}\``, + desc, + })); + const maxNameLen = Math.max(...rangeEntries.map((e) => e.name.length)); + const maxDescLen = Math.max(...rangeEntries.map((e) => e.desc.length)); const rangeTable = rangeEntries - .map(e => `| ${e.name.padEnd(maxNameLen)} | ${e.desc.padEnd(maxDescLen)} |`) + .map((e) => `| ${e.name.padEnd(maxNameLen)} | ${e.desc.padEnd(maxDescLen)} |`) .join('\n'); return `# ${data.meta.packageName} — Rules @@ -296,14 +299,14 @@ Each effect applies a visual change to a target element. An effect is either inl **\`fill\` guidance:** -- \`'both'\` — ${data.effects.fillGuidance.both} -- \`'backwards'\` — ${data.effects.fillGuidance.backwards} +- \`'both'\` — use for scroll-driven (\`viewProgress\`), pointer-driven (\`pointerMove\`), and toggling effects (\`hover\`/\`click\` with \`alternate\`, \`repeat\`, or \`state\` type). +- \`'backwards'\` — use for entrance animations with \`type: 'once'\` when the element's own CSS already matches the final keyframe (applies the initial keyframe during any \`delay\`). **\`composite\`** — same as CSS's \`animation-composition\`. Controls how this effect combines with others on the same property (transforms & filters): -- \`'replace'\` (default): ${data.effects.compositeOperations.replace} -- \`'add'\`: ${data.effects.compositeOperations.add} -- \`'accumulate'\`: ${data.effects.compositeOperations.accumulate} +- \`'replace'\` (default): fully replaces prior values. +- \`'add'\`: concatenates transform/filter functions after any existing ones (e.g. existing \`translateX(10px)\` + added \`translateY(20px)\` → both apply). +- \`'accumulate'\`: merges arguments of matching functions (e.g. \`translateX(10px)\` + \`translateX(20px)\` → \`translateX(30px)\`); non-matching functions concatenate like \`'add'\`. **\`easing\` guidance:** from \`@wix/motion\` (in addition to standard CSS easings): diff --git a/packages/interact/_content/templates/integration.mjs b/packages/interact/_content/templates/integration.mjs index e69893ee..1f6dbe47 100644 --- a/packages/interact/_content/templates/integration.mjs +++ b/packages/interact/_content/templates/integration.mjs @@ -1,4 +1,4 @@ -export function render(triggers, data, fragments) { +export function render(data, fragments) { return `# ${data.meta.packageName} Integration Rules Rules for integrating \`${data.meta.packageName}\` into a webpage — binding animations and effects to user-driven triggers via declarative configuration. diff --git a/packages/interact/_content/templates/pointermove-rule.mjs b/packages/interact/_content/templates/pointermove-rule.mjs index 6af0c4af..48944e1a 100644 --- a/packages/interact/_content/templates/pointermove-rule.mjs +++ b/packages/interact/_content/templates/pointermove-rule.mjs @@ -1,4 +1,4 @@ -export function render(trigger, data, fragments) { +export function render(data, fragments) { return `# PointerMove Trigger Rules for ${data.meta.packageName} These rules help generate pointer-driven interactions using \`${data.meta.packageName}\`. PointerMove triggers create real-time animations that respond to mouse movement over elements or the entire viewport. diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index 6e975531..43b62527 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -1,4 +1,4 @@ -export function render(trigger, data, fragments) { +export function render(data, fragments) { return `# ViewEnter Trigger Rules for ${data.meta.packageName} This document contains rules for generating interactions that respond to elements entering the viewport using the \`${data.meta.packageName}\`. ViewEnter triggers use IntersectionObserver to detect when elements become visible and are ideal for entrance animations, content reveals, and lazy-loading effects. diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index 0deab7b0..0c355cd4 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -1,4 +1,4 @@ -export function render(trigger, data, fragments) { +export function render(data, fragments) { return `# ViewProgress Trigger Rules for ${data.meta.packageName} These rules help generate scroll-driven interactions using \`${data.meta.packageName}\`. ViewProgress triggers create animations that update continuously as elements move through the viewport, leveraging native CSS ViewTimelines where supported, and using a polyfill library where unsupported. Use when animation progress should be tied to the element's scroll position. diff --git a/packages/interact/rules/click.md b/packages/interact/rules/click.md index 9e521c99..e41a70c3 100644 --- a/packages/interact/rules/click.md +++ b/packages/interact/rules/click.md @@ -61,7 +61,7 @@ Use `keyframeEffect` or `namedEffect` when the click should play an animation (C - `'state'` — resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`). - `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase. - `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`. -- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built, time-based animation effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options. +- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options. - `[FILL_MODE]` — optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect. - `[INITIAL_REVERSED_BOOL]` — optional. `true` to start in the finished state so the entire effect is reversed. - `[DURATION_MS]` — animation duration in milliseconds. @@ -153,7 +153,7 @@ Use `customEffect` when you need imperative control over the animation (e.g. cou ### Variables - `[SOURCE_KEY]` / `[TARGET_KEY]` / `[TRIGGER_TYPE]` — same as Rule 1. -- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with target element and `progress` from 0 to 1. +- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1. - `[DURATION_MS]` — animation duration in milliseconds. - `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`. @@ -186,4 +186,4 @@ Use sequences when a click should sync/stagger animations across multiple elemen - `[SOURCE_KEY]` / `[TRIGGER_TYPE]` — same as Rule 1. `triggerType` is set on the sequence config, not on individual effects within the sequence. - `[OFFSET_MS]` — time offset for staggering each child's animation start, in milliseconds. - `[OFFSET_EASING]` — easing curve for the offset staggering distribution. Defaults to `'linear'`. -- `[EFFECT_DEFINITION]` — a definition of, or a reference to a time-based animation effect. +- `[EFFECT_DEFINITION]` — a definition of or a reference to a time-based animation effect. diff --git a/packages/interact/rules/hover.md b/packages/interact/rules/hover.md index 88938e0b..a85945ac 100644 --- a/packages/interact/rules/hover.md +++ b/packages/interact/rules/hover.md @@ -155,7 +155,7 @@ Use `customEffect` when you need imperative control over the animation (e.g. cou ### Variables - `[SOURCE_KEY]` / `[TARGET_KEY]` / `[TRIGGER_TYPE]` — same as Rule 1. -- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(target: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1. +- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1. - `[DURATION_MS]` — animation duration in milliseconds. - `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`. diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index a8e1fe1c..976cfb18 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -83,7 +83,9 @@ class Fragments { } let content = sectionMap.get(section); if (content === undefined) { - throw new Error(`Section "${section}" not found in fragment "${path}". Available: ${[...sectionMap.keys()].join(', ')}`); + throw new Error( + `Section "${section}" not found in fragment "${path}". Available: ${[...sectionMap.keys()].join(', ')}`, + ); } for (const [key, val] of Object.entries(params)) { content = content.replaceAll(`{{${key}}}`, val); @@ -99,9 +101,17 @@ const fragments = new Fragments(join(CONTENT_DIR, 'fragments')); // --------------------------------------------------------------------------- const manifest = [ - { template: 'event-trigger-rule.mjs', triggers: ['click', 'hover'], output: name => `${name}.md` }, + { + template: 'event-trigger-rule.mjs', + triggers: ['click', 'hover'], + output: (name) => `${name}.md`, + }, { template: 'viewenter-rule.mjs', triggers: ['viewEnter'], output: () => 'viewenter.md' }, - { template: 'viewprogress-rule.mjs', triggers: ['viewProgress'], output: () => 'viewprogress.md' }, + { + template: 'viewprogress-rule.mjs', + triggers: ['viewProgress'], + output: () => 'viewprogress.md', + }, { template: 'pointermove-rule.mjs', triggers: ['pointerMove'], output: () => 'pointermove.md' }, { template: 'full-lean.mjs', triggers: null, output: () => 'full-lean.md' }, { template: 'integration.mjs', triggers: null, output: () => 'integration.md' }, @@ -113,25 +123,51 @@ for (const entry of manifest) { const mod = await import(join(CONTENT_DIR, 'templates', entry.template)); if (entry.triggers) { for (const name of entry.triggers) { - const trigger = data.triggers.find(t => t.name === name); + const trigger = data.triggers.find((t) => t.name === name); if (!trigger) throw new Error(`Trigger "${name}" not found in triggers.yaml`); - outputs.push({ file: entry.output(name), content: mod.render(trigger, data, fragments) }); + outputs.push({ + file: entry.output(name), + content: mod.render({ ...data, trigger }, fragments), + }); } } else { - outputs.push({ file: entry.output(), content: mod.render(data.triggers, data, fragments) }); + outputs.push({ file: entry.output(), content: mod.render(data, fragments) }); } } // --------------------------------------------------------------------------- -// 4. Write outputs +// 4. Write or check outputs // --------------------------------------------------------------------------- +const checkMode = process.argv.includes('--check'); + mkdirSync(OUTPUT_DIR, { recursive: true }); +let stale = 0; for (const { file, content } of outputs) { const outPath = join(OUTPUT_DIR, file); - writeFileSync(outPath, content, 'utf8'); - console.log(` ✓ ${relative(PKG_ROOT, outPath)}`); + if (checkMode) { + let existing = ''; + try { + existing = readFileSync(outPath, 'utf8'); + } catch {} + if (existing !== content) { + console.error(` ✗ ${relative(PKG_ROOT, outPath)} is stale`); + stale++; + } else { + console.log(` ✓ ${relative(PKG_ROOT, outPath)} is up to date`); + } + } else { + writeFileSync(outPath, content, 'utf8'); + console.log(` ✓ ${relative(PKG_ROOT, outPath)}`); + } } -console.log(`\nGenerated ${outputs.length} rule files.`); +if (checkMode && stale > 0) { + console.error(`\n${stale} file(s) are stale. Run \`yarn build:rules\` to regenerate.`); + process.exit(1); +} else if (checkMode) { + console.log(`\nAll ${outputs.length} rule files are up to date.`); +} else { + console.log(`\nGenerated ${outputs.length} rule files.`); +} From 90381a8f58945ed0ee94c43a56ef724c8e7b75f4 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 16:11:35 +0300 Subject: [PATCH 06/27] PR fixes --- .../interact/_content/fragments/conditions.md | 2 +- .../_content/fragments/config-structure.md | 4 +- .../_content/fragments/element-resolution.md | 4 +- packages/interact/_content/fragments/fouc.md | 12 +- .../_content/fragments/pitfalls/hit-area.md | 4 +- .../_content/fragments/quick-start.md | 4 +- .../interact/_content/fragments/sequences.md | 4 +- .../interact/_content/fragments/static-api.md | 4 +- .../interact/_content/templates/_helpers.mjs | 4 + .../_content/templates/event-trigger-rule.mjs | 134 +++++++----------- .../interact/_content/templates/full-lean.mjs | 37 +++-- .../_content/templates/integration.mjs | 23 +-- .../_content/templates/pointermove-rule.mjs | 5 + .../_content/templates/viewenter-rule.mjs | 5 + .../_content/templates/viewprogress-rule.mjs | 5 + 15 files changed, 130 insertions(+), 121 deletions(-) diff --git a/packages/interact/_content/fragments/conditions.md b/packages/interact/_content/fragments/conditions.md index 3a5005e2..ac8621c1 100644 --- a/packages/interact/_content/fragments/conditions.md +++ b/packages/interact/_content/fragments/conditions.md @@ -1,4 +1,4 @@ - + Named conditions that gate interactions, effects, or sequences. | Type | Predicate | diff --git a/packages/interact/_content/fragments/config-structure.md b/packages/interact/_content/fragments/config-structure.md index f4794859..b4068fe7 100644 --- a/packages/interact/_content/fragments/config-structure.md +++ b/packages/interact/_content/fragments/config-structure.md @@ -1,4 +1,4 @@ - + ```ts type InteractConfig = { interactions: Interaction[]; // REQUIRED @@ -10,7 +10,7 @@ type InteractConfig = { All cross-references (by id) MUST point to existing entries. Element keys MUST be stable for the config's lifetime. - + ### InteractConfig ```typescript diff --git a/packages/interact/_content/fragments/element-resolution.md b/packages/interact/_content/fragments/element-resolution.md index d3b4abf3..68fa75d5 100644 --- a/packages/interact/_content/fragments/element-resolution.md +++ b/packages/interact/_content/fragments/element-resolution.md @@ -19,7 +19,7 @@ The target element is what the effect animates. Resolved in priority order: 2. **Registry Effect's `key`** — if the effect is an `EffectRef`, the `key` from the referenced registry entry is used. 3. **Fallback to `Interaction.key`** — the same `key` is used for the source will be used for the target. 4. After resolving the root target, `selector`, `listContainer`, and `listItemSelector` on the effect further refine which child elements within that target are animated (same priority order as source resolution). - + #### Source element resolution (Interaction level) The source element is what the trigger attaches to. Resolved in priority order: @@ -29,7 +29,7 @@ The source element is what the trigger attaches to. Resolved in priority order: 3. **`listContainer` + `selector`** — matches via `querySelector` within each immediate child of the container. 4. **`selector` only** — matches via `querySelectorAll` within the root element. 5. **Fallback** — first child of `` (web) or the root element (react/vanilla). - + #### Target element resolution (Effect level) The target element is what the effect animates. Resolved in priority order: diff --git a/packages/interact/_content/fragments/fouc.md b/packages/interact/_content/fragments/fouc.md index 646f9ff6..550dc281 100644 --- a/packages/interact/_content/fragments/fouc.md +++ b/packages/interact/_content/fragments/fouc.md @@ -57,7 +57,7 @@ const css = generate(config);
...
``` - + **Web (Custom Elements):** ```html @@ -73,7 +73,7 @@ const css = generate(config); ...
``` - + **React:** ```tsx @@ -87,7 +87,7 @@ const css = generate(config); ```html
...
``` - + **Vanilla:** ```html @@ -100,19 +100,19 @@ const css = generate(config); - `initial` is only valid for `viewEnter` + `triggerType: 'once'` (or no `triggerType`, which defaults to `'once'`) where source and target are the same element. - Do NOT use `initial` for `viewEnter` with `triggerType: 'repeat'`/`'alternate'`/`'state'`. For those, manually apply the initial keyframe as inline styles on the target element and use `fill: 'both'`. - If other interactions in the config also need FOUC prevention, `generate(config)` covers them all — set `initial` only on the relevant `viewEnter` + `triggerType: 'once'` elements. - + **Rules:** - `generate()` should be called server-side or at build time. Can also be called on the client if page content is initially hidden (e.g. behind a loader). - Only valid for `viewEnter` + `triggerType: 'once'` (or no `triggerType`, which defaults to `'once'`) where source and target are the same element. - + ### Rules - `generate()` should be called server-side or at build time. Can also be called on client-side if page content is initially hidden (e.g. behind a loader/splash screen). - **Both** `generate(config)` CSS **and** `initial` on the element are required. Using only one has no effect. - `initial` is only valid for `viewEnter` + `type: 'once'` where source and target are the same element. - For `repeat`/`alternate`/`state`, do NOT use `initial`. Instead, manually apply the initial keyframe as inline styles on the target element and use `fill: 'both'`. - + **Problem:** Elements with entrance animations (e.g. `FadeIn` on `viewEnter`) are initially visible in their final state. Before the animation framework applies the starting keyframe, the content flashes visibly — a flash of un-animated content (FOUC). **Solution:** Two things are required — both MUST be present: diff --git a/packages/interact/_content/fragments/pitfalls/hit-area.md b/packages/interact/_content/fragments/pitfalls/hit-area.md index 844afd2a..994c9b54 100644 --- a/packages/interact/_content/fragments/pitfalls/hit-area.md +++ b/packages/interact/_content/fragments/pitfalls/hit-area.md @@ -2,10 +2,10 @@ - **CRITICAL**: MUST AVOID using the same element as both trigger source and effect target with effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. - **CRITICAL**: MUST AVOID using the same element as both source and target with effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. - + - **CRITICAL - Hit-area shift**: When a hover effect changes the size or position of the hovered element (e.g., `transform: scale(…)`), MUST use a 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. - + - **CRITICAL**: For `pointerMove` trigger MUST AVOID using the same element as both source and target with `hitArea: 'self'` and effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. When using `hitArea: 'self'`, the source element is the hit area for pointer tracking: diff --git a/packages/interact/_content/fragments/quick-start.md b/packages/interact/_content/fragments/quick-start.md index b10d5209..b696bf28 100644 --- a/packages/interact/_content/fragments/quick-start.md +++ b/packages/interact/_content/fragments/quick-start.md @@ -11,7 +11,7 @@ const instance = Interact.create(config); ``` The `config` object is an `InteractConfig` containing `interactions` (required), and optionally shared `effects`, `sequences`, and `conditions`. - + ```typescript import { Interact } from '@wix/interact/web'; @@ -46,7 +46,7 @@ const instance = Interact.create(config); instance.add(element, 'hero'); // bind after element exists in DOM instance.remove('hero'); // unregister ``` - + ```typescript import { Interact } from '@wix/interact'; diff --git a/packages/interact/_content/fragments/sequences.md b/packages/interact/_content/fragments/sequences.md index 6c374548..b9287100 100644 --- a/packages/interact/_content/fragments/sequences.md +++ b/packages/interact/_content/fragments/sequences.md @@ -1,4 +1,4 @@ - + Coordinate multiple effects with staggered timing. Prefer sequences over manual delay stagger. ### Sequence As type @@ -61,7 +61,7 @@ Coordinate multiple effects with staggered timing. Prefer sequences over manual Reusable sequences can be defined in `InteractConfig.sequences` and referenced by `sequenceId`. - + Sequences coordinate multiple effects with staggered timing. ```typescript diff --git a/packages/interact/_content/fragments/static-api.md b/packages/interact/_content/fragments/static-api.md index b1e2b61f..7dee2b9c 100644 --- a/packages/interact/_content/fragments/static-api.md +++ b/packages/interact/_content/fragments/static-api.md @@ -1,4 +1,4 @@ - + | Method / Property | Description | | :---------------------------------- | :------------------------------------------------------------------------------------------------------------ | | `Interact.create(config)` | Initialize with a config. Returns the instance. Store the instance to manage its lifecycle. | @@ -21,7 +21,7 @@ Use `setup()` when you need to override default observer thresholds or provide g Each `Interact.create()` call returns an instance. Store instances and call `instance.destroy()` when no longer needed (e.g. on component unmount) to prevent stale listeners and memory leaks. - + Each `Interact.create(config)` call returns an instance. Keep a reference if you need to add/remove elements dynamically (vanilla JS) or to destroy a specific instance. Call `Interact.destroy()` to tear down all instances at once (e.g. on page navigation). | Method / Property | Description | diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs index 35b85030..e79b8c2e 100644 --- a/packages/interact/_content/templates/_helpers.mjs +++ b/packages/interact/_content/templates/_helpers.mjs @@ -1,3 +1,7 @@ export function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); } + +export function when(condition, content) { + return condition ? content : ''; +} diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 8ed7e5a4..3d79e4bd 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,6 +1,6 @@ -import { capitalize } from './_helpers.mjs'; +import { capitalize, when } from './_helpers.mjs'; -const TRIGGER_OVERRIDES = { +const OVERRIDES = { hover: { fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", @@ -17,8 +17,6 @@ const TRIGGER_OVERRIDES = { "easing curve for the offset staggering distribution. CSS easing string, or named easing from `@wix/motion`. Defaults to `'linear'`.", }, click: { - stateEffectExtra: ' Uses the `transition` CSS property.', - customEffectExtra: ', randomized behavior', fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.", sourceKeyDesc: 'The element that listens for clicks.', @@ -28,75 +26,40 @@ const TRIGGER_OVERRIDES = { "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", easingDesc: 'CSS easing string, or named easing from `@wix/motion`.', iterationsDesc: 'optional. Number of iterations, or `Infinity` for continuous loops.', - alternateExtra: " Different from `triggerType: 'alternate'` which alternates per click.", sequenceOffsetEasingDesc: "easing curve for the offset staggering distribution. Defaults to `'linear'`.", }, }; -function buildProse(name) { - const o = TRIGGER_OVERRIDES[name]; - return { - timeEffectIntro: `Use \`keyframeEffect\` or \`namedEffect\` when the ${name} should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior.`, - stateEffectIntro: `Use \`transition\` or \`transitionProperties\` when the ${name} should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations.${o.stateEffectExtra || ''} Set \`stateAction\` on the effect to control how the style is applied.`, - customEffectIntro: `Use \`customEffect\` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation${o.customEffectExtra || ''}). The callback receives the target element and a \`progress\` value (0–1) driven by the animation timeline.`, - sequencesIntro: `Use sequences when a ${name} should sync/stagger animations across multiple elements. Set \`triggerType\` on the sequence config to control playback behavior.`, - fillCritical: o.fillCritical, - sourceKeyDesc: o.sourceKeyDesc, - targetKeyDesc: o.targetKeyDesc, - fillModeDesc: o.fillModeDesc, - namedEffectDesc: - 'object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.', - easingDesc: o.easingDesc, - iterationsDesc: o.iterationsDesc, - alternateDesc: `optional. \`true\` to alternate direction on every other iteration (within a single playback).${o.alternateExtra || ''}`, - customEffectCallbackDesc: `function with signature \`(element: HTMLElement, progress: number) => void\`. Called on each animation frame with the target element and \`progress\` from 0 to 1.`, - sequenceEffectDefDesc: 'a definition of or a reference to a time-based animation effect.', - sequenceOffsetEasingDesc: o.sequenceOffsetEasingDesc, - }; -} - +/** + * Renders a trigger-specific rule file (click.md or hover.md). + * @param {{ trigger: object, meta: object }} data — must include `trigger` (from triggers.yaml) and `meta` + * @param {import('../../scripts/build-rules.mjs').Fragments} fragments + */ export function render(data, fragments) { - const trigger = data.trigger; - const name = trigger.name; + const { trigger } = data; + const { name } = trigger; const Name = capitalize(name); - const prose = buildProse(name); + const o = OVERRIDES[name]; + const isClick = name === 'click'; const hasReversed = trigger.templateFields.timeEffect.includes('reversed'); const hasEffectId = trigger.templateFields.timeEffect.includes('effectId'); const showMultipleEffects = trigger.showMultipleEffectsNote; - const pitfallsBlock = - trigger.pitfalls.length > 0 - ? '\n' + - trigger.pitfalls.map((p) => fragments.get(`pitfalls/${p.id}`, name)).join('\n') + - '\n' - : ''; + const pitfallsBlock = when( + trigger.pitfalls.length > 0, + '\n' + trigger.pitfalls.map((p) => fragments.get(`pitfalls/${p.id}`, name)).join('\n') + '\n', + ); - const multipleEffectsNote = showMultipleEffects - ? `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event` })}\n` - : ''; + const multipleEffectsNote = when( + showMultipleEffects, + `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event` })}\n`, + ); - const rule1Closing = ` },\n // additional effects targeting other elements can be added here`; - const rule23Closing = showMultipleEffects + const effectsClosing = showMultipleEffects ? ` },\n // additional effects targeting other elements can be added here` : ` }`; - const reversedField = hasReversed ? `\n reversed: [INITIAL_REVERSED_BOOL],` : ''; - - const effectIdField = hasEffectId ? `\n effectId: '[UNIQUE_EFFECT_ID]'` : ''; - - const reversedVar = hasReversed - ? `\n- \`[INITIAL_REVERSED_BOOL]\` — optional. \`true\` to start in the finished state so the entire effect is reversed.` - : ''; - - const effectIdVar = hasEffectId - ? `\n- \`[UNIQUE_EFFECT_ID]\` — optional. String identifier used by \`animationEnd\` triggers for chaining, and by sequences for referencing effects from the top-level \`effects\` map.` - : ''; - - const fillModeVar = `- \`[FILL_MODE]\` — ${prose.fillModeDesc}`; - - const variablesBlock = buildVariables(trigger, prose, fillModeVar, reversedVar, effectIdVar); - return `# ${Name} Trigger Rules for ${data.meta.packageName} This document contains rules for generating ${name}-triggered interactions in \`${data.meta.packageName}\`. @@ -114,9 +77,9 @@ ${pitfallsBlock} ## Rule 1: keyframeEffect / namedEffect (TimeEffect) -${prose.timeEffectIntro} +Use \`keyframeEffect\` or \`namedEffect\` when the ${name} should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior. -**CRITICAL:** ${prose.fillCritical} +**CRITICAL:** ${o.fillCritical} ${multipleEffectsNote} \`\`\`typescript { @@ -135,26 +98,27 @@ ${multipleEffectsNote} // OR namedEffect: [NAMED_EFFECT_DEFINITION], - fill: '[FILL_MODE]',${reversedField} + fill: '[FILL_MODE]',${when(hasReversed, `\n reversed: [INITIAL_REVERSED_BOOL],`)} duration: [DURATION_MS], easing: '[EASING_FUNCTION]', delay: [DELAY_MS], iterations: [ITERATIONS], - alternate: [ALTERNATE_BOOL]${hasEffectId ? ',' : ''}${effectIdField} -${rule1Closing} + alternate: [ALTERNATE_BOOL]${when(hasEffectId, `,\n effectId: '[UNIQUE_EFFECT_ID]'`)} + }, + // additional effects targeting other elements can be added here ] } \`\`\` ### Variables -${variablesBlock} +${buildVariables(trigger, o, isClick, hasReversed, hasEffectId)} --- ## Rule 2: transition / transitionProperties (StateEffect) -${prose.stateEffectIntro} +Use \`transition\` or \`transitionProperties\` when the ${name} should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations.${when(isClick, ' Uses the `transition` CSS property.')} Set \`stateAction\` on the effect to control how the style is applied. Use \`transition\` when all properties share timing. Use \`transitionProperties\` when each property needs independent \`duration\`, \`delay\`, or \`easing\`. @@ -188,7 +152,7 @@ Use \`transition\` when all properties share timing. Use \`transitionProperties\ }, // ... more properties ] -${rule23Closing} +${effectsClosing} ] } \`\`\` @@ -210,7 +174,7 @@ ${Object.entries(trigger.stateActionDescriptions) ## Rule 3: customEffect (TimeEffect) -${prose.customEffectIntro} +Use \`customEffect\` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation${when(isClick, ', randomized behavior')}). The callback receives the target element and a \`progress\` value (0–1) driven by the animation timeline. \`\`\`typescript { @@ -223,7 +187,7 @@ ${prose.customEffectIntro} customEffect: [CUSTOM_EFFECT_CALLBACK], duration: [DURATION_MS], easing: '[EASING_FUNCTION]' -${rule23Closing} +${effectsClosing} ] } \`\`\` @@ -231,7 +195,7 @@ ${rule23Closing} ### Variables - \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` / \`[TRIGGER_TYPE]\` — same as Rule 1. -- \`[CUSTOM_EFFECT_CALLBACK]\` — ${prose.customEffectCallbackDesc} +- \`[CUSTOM_EFFECT_CALLBACK]\` — function with signature \`(element: HTMLElement, progress: number) => void\`. Called on each animation frame with the target element and \`progress\` from 0 to 1. - \`[DURATION_MS]\` — animation duration in milliseconds. - \`[EASING_FUNCTION]\` — CSS easing string, or named easing from \`@wix/motion\`. @@ -239,7 +203,7 @@ ${rule23Closing} ## Rule 4: Sequences -${prose.sequencesIntro} +Use sequences when a ${name} should sync/stagger animations across multiple elements. Set \`triggerType\` on the sequence config to control playback behavior. \`\`\`typescript { @@ -263,30 +227,38 @@ ${prose.sequencesIntro} - \`[SOURCE_KEY]\` / \`[TRIGGER_TYPE]\` — same as Rule 1. \`triggerType\` is set on the sequence config, not on individual effects within the sequence. - \`[OFFSET_MS]\` — time offset for staggering each child's animation start, in milliseconds. -- \`[OFFSET_EASING]\` — ${prose.sequenceOffsetEasingDesc} -- \`[EFFECT_DEFINITION]\` — ${prose.sequenceEffectDefDesc} +- \`[OFFSET_EASING]\` — ${o.sequenceOffsetEasingDesc} +- \`[EFFECT_DEFINITION]\` — a definition of or a reference to a time-based animation effect. `; } -function buildVariables(trigger, prose, fillModeVar, reversedVar, effectIdVar) { +function buildVariables(trigger, o, isClick, hasReversed, hasEffectId) { const lines = [ - `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${prose.sourceKeyDesc}`, - `- \`[TARGET_KEY]\` — ${prose.targetKeyDesc}`, + `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${o.sourceKeyDesc}`, + `- \`[TARGET_KEY]\` — ${o.targetKeyDesc}`, `- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of:`, ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`), `- \`[KEYFRAMES]\` — array of keyframe objects (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase.`, `- \`[EFFECT_NAME]\` — unique string identifier for a \`keyframeEffect\`.`, - `- \`[NAMED_EFFECT_DEFINITION]\` — ${prose.namedEffectDesc}`, - fillModeVar, + `- \`[NAMED_EFFECT_DEFINITION]\` — object with properties of pre-built effect from \`@wix/motion-presets\`. Refer to motion-presets rules for available presets and their options.`, + `- \`[FILL_MODE]\` — ${o.fillModeDesc}`, ]; - if (reversedVar) lines.push(reversedVar.trim()); + if (hasReversed) { + lines.push( + `- \`[INITIAL_REVERSED_BOOL]\` — optional. \`true\` to start in the finished state so the entire effect is reversed.`, + ); + } lines.push( `- \`[DURATION_MS]\` — animation duration in milliseconds.`, - `- \`[EASING_FUNCTION]\` — ${prose.easingDesc}`, + `- \`[EASING_FUNCTION]\` — ${o.easingDesc}`, `- \`[DELAY_MS]\` — optional delay before the effect starts, in milliseconds.`, - `- \`[ITERATIONS]\` — ${prose.iterationsDesc}`, - `- \`[ALTERNATE_BOOL]\` — ${prose.alternateDesc}`, + `- \`[ITERATIONS]\` — ${o.iterationsDesc}`, + `- \`[ALTERNATE_BOOL]\` — optional. \`true\` to alternate direction on every other iteration (within a single playback).${when(isClick, " Different from \`triggerType: 'alternate'\` which alternates per click.")}`, ); - if (effectIdVar) lines.push(effectIdVar.trim()); + if (hasEffectId) { + lines.push( + `- \`[UNIQUE_EFFECT_ID]\` — optional. String identifier used by \`animationEnd\` triggers for chaining, and by sequences for referencing effects from the top-level \`effects\` map.`, + ); + } return lines.join('\n'); } diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 86466bcf..8ce63d92 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -1,6 +1,19 @@ import { capitalize } from './_helpers.mjs'; +/** + * Renders full-lean.md — the comprehensive reference for all triggers, effects, and API surface. + * @param {{ triggers: object[], effects: object, meta: object }} data + * @param {import('../../scripts/build-rules.mjs').Fragments} fragments + */ export function render(data, fragments) { + const hover = data.triggers.find((t) => t.name === 'hover'); + const click = data.triggers.find((t) => t.name === 'click'); + const viewEnter = data.triggers.find((t) => t.name === 'viewEnter'); + + const triggerTypeUnion = data.effects.triggerTypes.map((t) => `'${t}'`).join(' | '); + const rangeNameUnion = Object.keys(data.effects.rangeNames) + .map((n) => `'${n}'`) + .join(' | '); const easingList = data.effects.easings.map((e) => `\`'${e}'\``).join(', '); const presetEntries = Object.entries(data.effects.presets).map(([category, names]) => ({ @@ -58,8 +71,8 @@ Each item here is CRITICAL — ignoring any of them will break animations. ${fragments.get('pitfalls/overflow-clip', 'long')} ${fragments.get('pitfalls/same-element-viewenter', 'long')} -${fragments.get('pitfalls/hit-area', 'full-lean-hover')} -${fragments.get('pitfalls/hit-area', 'full-lean-pointermove')} +${fragments.get('pitfalls/hit-area', 'detailed-hover')} +${fragments.get('pitfalls/hit-area', 'detailed-pointermove')} ${fragments.get('pitfalls/dont-guess-presets', 'default')} ${fragments.get('pitfalls/reduced-motion', 'default')} ${fragments.get('pitfalls/perspective', 'default')} @@ -117,7 +130,7 @@ import { Interaction } from '@wix/interact/react'; ## Config Structure -${fragments.get('config-structure', 'full-lean')} +${fragments.get('config-structure', 'detailed')} --- @@ -225,7 +238,7 @@ params: { inset?: string; // like view-timeline-inset, e.g. '-100px' or '-50px 0px' } // Playback behavior is set on each effect: -effect.triggerType: 'once' | 'repeat' | 'alternate' | 'state'; // default: 'once' +effect.triggerType: ${triggerTypeUnion}; // default: '${viewEnter.defaultTriggerType}' \`\`\` **CRITICAL:** When source and target are the **same element**, MUST use \`triggerType: 'once'\`. For \`'repeat'\` / \`'alternate'\` / \`'state'\`, ALWAYS use **separate** source and target elements — animating the observed element can cause it to leave/re-enter the viewport, causing rapid re-triggers. @@ -356,7 +369,7 @@ Used with \`viewProgress\` and \`pointerMove\` triggers. \`\`\`ts { - name?: 'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing'; + name?: ${rangeNameUnion}; offset?: { value: number; unit: 'percentage' | 'px' | 'vh' | 'vw' } } \`\`\` @@ -449,13 +462,13 @@ ${presetTable} ## Sequences -${fragments.get('sequences', 'full-lean')} +${fragments.get('sequences', 'detailed')} --- ## Conditions -${fragments.get('conditions', 'full-lean')} +${fragments.get('conditions', 'default')} --- @@ -473,13 +486,13 @@ ${fragments.get('fouc', 'code-inject')} ### Step 2: Mark elements -${fragments.get('fouc', 'code-web-hero')} +${fragments.get('fouc', 'code-web-example')} -${fragments.get('fouc', 'code-react-hero')} +${fragments.get('fouc', 'code-react-example')} -${fragments.get('fouc', 'code-vanilla-hero')} +${fragments.get('fouc', 'code-vanilla-example')} -${fragments.get('fouc', 'rules-full-lean')} +${fragments.get('fouc', 'rules-detailed')} --- @@ -495,6 +508,6 @@ ${fragments.get('element-resolution', 'target')} ## Static API -${fragments.get('static-api', 'full-lean')} +${fragments.get('static-api', 'detailed')} `; } diff --git a/packages/interact/_content/templates/integration.mjs b/packages/interact/_content/templates/integration.mjs index 1f6dbe47..3a9c5f9d 100644 --- a/packages/interact/_content/templates/integration.mjs +++ b/packages/interact/_content/templates/integration.mjs @@ -1,3 +1,8 @@ +/** + * Renders integration.md — integration guide covering entry points, config schema, and triggers overview. + * @param {{ triggers: object[], effects: object, meta: object }} data + * @param {import('../../scripts/build-rules.mjs').Fragments} fragments + */ export function render(data, fragments) { return `# ${data.meta.packageName} Integration Rules @@ -29,7 +34,7 @@ ${fragments.get('quick-start', 'install')} ### Web (Custom Elements) -${fragments.get('quick-start', 'web-integration')} +${fragments.get('quick-start', 'web-brief')} Wrap target elements with \`\`: @@ -79,7 +84,7 @@ import { Interaction } from '@wix/interact/react'; ### Vanilla JS -${fragments.get('quick-start', 'vanilla-integration')} +${fragments.get('quick-start', 'vanilla-brief')} **Rules:** @@ -126,7 +131,7 @@ For full effect type syntax (\`keyframeEffect\`, \`namedEffect\`, \`customEffect ## Configuration Schema -${fragments.get('config-structure', 'integration')} +${fragments.get('config-structure', 'brief')} ### Interaction @@ -154,9 +159,9 @@ At least one of \`effects\` or \`sequences\` MUST be provided. \`listItemSelector\` is **optional** — only use it when you need to **filter** which children of \`listContainer\` participate (e.g. select only \`.active\` items). When omitted, all immediate children of the \`listContainer\` are selected. -${fragments.get('element-resolution', 'source-integration')} +${fragments.get('element-resolution', 'source-brief')} -${fragments.get('element-resolution', 'target-integration')} +${fragments.get('element-resolution', 'target-brief')} --- @@ -179,15 +184,15 @@ For \`hover\`/\`click\` (and their accessible variants \`interest\`/\`activate\` ## Sequences -${fragments.get('sequences', 'integration')} +${fragments.get('sequences', 'brief')} --- ## Critical CSS (FOUC Prevention) -${fragments.get('fouc', 'integration-intro')} +${fragments.get('fouc', 'intro-brief')} -${fragments.get('fouc', 'rules-integration')} +${fragments.get('fouc', 'rules-brief')} \`\`\`javascript import { generate } from '${data.meta.entryPoints.web}'; @@ -222,6 +227,6 @@ ${fragments.get('fouc', 'code-inject')} ## Static API -${fragments.get('static-api', 'integration')} +${fragments.get('static-api', 'brief')} `; } diff --git a/packages/interact/_content/templates/pointermove-rule.mjs b/packages/interact/_content/templates/pointermove-rule.mjs index 48944e1a..98097267 100644 --- a/packages/interact/_content/templates/pointermove-rule.mjs +++ b/packages/interact/_content/templates/pointermove-rule.mjs @@ -1,3 +1,8 @@ +/** + * Renders pointermove.md — rules for pointer-driven interactions with 2D mouse tracking. + * @param {{ trigger: object, meta: object }} data — must include `trigger` (pointerMove from triggers.yaml) and `meta` + * @param {import('../../scripts/build-rules.mjs').Fragments} fragments + */ export function render(data, fragments) { return `# PointerMove Trigger Rules for ${data.meta.packageName} diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index 43b62527..78140fbf 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -1,3 +1,8 @@ +/** + * Renders viewenter.md — rules for viewport-entry triggered animations. + * @param {{ trigger: object, meta: object }} data — must include `trigger` (viewEnter from triggers.yaml) and `meta` + * @param {import('../../scripts/build-rules.mjs').Fragments} fragments + */ export function render(data, fragments) { return `# ViewEnter Trigger Rules for ${data.meta.packageName} diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index 0c355cd4..3d1555e1 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -1,3 +1,8 @@ +/** + * Renders viewprogress.md — rules for scroll-driven animations using ViewTimeline. + * @param {{ trigger: object, meta: object }} data — must include `trigger` (viewProgress from triggers.yaml) and `meta` + * @param {import('../../scripts/build-rules.mjs').Fragments} fragments + */ export function render(data, fragments) { return `# ViewProgress Trigger Rules for ${data.meta.packageName} From 0f616c567f73fb67a06c71ff00f65955f954b6c1 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 16:34:47 +0300 Subject: [PATCH 07/27] PR fixes --- packages/interact/_content/data/effects.yaml | 12 +-- .../_content/fragments/pitfalls/hit-area.md | 3 +- .../pitfalls/same-element-viewenter.md | 2 +- .../_content/templates/event-trigger-rule.mjs | 79 ++++++++----------- .../_content/templates/integration.mjs | 1 + .../_content/templates/pointermove-rule.mjs | 17 ++-- .../_content/templates/viewprogress-rule.mjs | 16 ++-- packages/interact/rules/click.md | 6 +- packages/interact/rules/full-lean.md | 19 +++-- packages/interact/rules/integration.md | 1 + packages/interact/rules/pointermove.md | 4 +- 11 files changed, 77 insertions(+), 83 deletions(-) diff --git a/packages/interact/_content/data/effects.yaml b/packages/interact/_content/data/effects.yaml index 869a0dcb..f56523ca 100644 --- a/packages/interact/_content/data/effects.yaml +++ b/packages/interact/_content/data/effects.yaml @@ -115,9 +115,9 @@ presets: - BlobMouse rangeNames: - entry: 'Element entering viewport' - exit: 'Element exiting viewport' - contain: 'After `entry` range and before `exit` range' - cover: 'Full range from `entry` through `contain` and `exit`' - entry-crossing: "From element's leading edge entering to trailing edge entering" - exit-crossing: "From element's leading edge exiting to trailing edge exiting" + cover: 'Full visibility span from first pixel entering to last pixel leaving' + entry: 'The phase while the element is entering the viewport' + exit: 'The phase while the element is exiting the viewport' + contain: 'While the element is fully contained in the viewport' + entry-crossing: "From the element's leading edge entering to its leading edge reaching the opposite side" + exit-crossing: "From the element's trailing edge reaching the start to its trailing edge leaving" diff --git a/packages/interact/_content/fragments/pitfalls/hit-area.md b/packages/interact/_content/fragments/pitfalls/hit-area.md index 994c9b54..75f91665 100644 --- a/packages/interact/_content/fragments/pitfalls/hit-area.md +++ b/packages/interact/_content/fragments/pitfalls/hit-area.md @@ -3,8 +3,7 @@ - **CRITICAL**: MUST AVOID using the same element as both source and target with effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. -- **CRITICAL - Hit-area shift**: When a hover effect changes the size or position of the hovered element (e.g., `transform: scale(…)`), MUST use a 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. +- **CRITICAL - Hit-area shift**: When a hover effect changes the size or position of the hovered element (e.g., `transform: scale(…)`), MUST use a 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. - **CRITICAL**: For `pointerMove` trigger MUST AVOID using the same element as both source and target with `hitArea: 'self'` and effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. diff --git a/packages/interact/_content/fragments/pitfalls/same-element-viewenter.md b/packages/interact/_content/fragments/pitfalls/same-element-viewenter.md index 39d4ffc9..3b3285ca 100644 --- a/packages/interact/_content/fragments/pitfalls/same-element-viewenter.md +++ b/packages/interact/_content/fragments/pitfalls/same-element-viewenter.md @@ -1,4 +1,4 @@ > **CRITICAL:** When the source (trigger) and target (effect) elements are the **same element**, use ONLY `triggerType: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`), MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing. -- **CRITICAL**: When using `viewEnter` trigger and source (trigger) and target (effect) elements are the **same element**, use ONLY `type: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`) MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing. +- **CRITICAL**: When using `viewEnter` trigger and source (trigger) and target (effect) elements are the **same element**, use ONLY `triggerType: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`) MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing. diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 3d79e4bd..cdb29eef 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,36 +1,5 @@ import { capitalize, when } from './_helpers.mjs'; -const OVERRIDES = { - hover: { - fillCritical: - "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", - sourceKeyDesc: 'The element that listens for hover.', - targetKeyDesc: - "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", - fillModeDesc: - "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.", - easingDesc: - "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.", - iterationsDesc: - "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", - sequenceOffsetEasingDesc: - "easing curve for the offset staggering distribution. CSS easing string, or named easing from `@wix/motion`. Defaults to `'linear'`.", - }, - click: { - fillCritical: - "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.", - sourceKeyDesc: 'The element that listens for clicks.', - targetKeyDesc: - "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", - fillModeDesc: - "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", - easingDesc: 'CSS easing string, or named easing from `@wix/motion`.', - iterationsDesc: 'optional. Number of iterations, or `Infinity` for continuous loops.', - sequenceOffsetEasingDesc: - "easing curve for the offset staggering distribution. Defaults to `'linear'`.", - }, -}; - /** * Renders a trigger-specific rule file (click.md or hover.md). * @param {{ trigger: object, meta: object }} data — must include `trigger` (from triggers.yaml) and `meta` @@ -40,8 +9,8 @@ export function render(data, fragments) { const { trigger } = data; const { name } = trigger; const Name = capitalize(name); - const o = OVERRIDES[name]; const isClick = name === 'click'; + const isHover = name === 'hover'; const hasReversed = trigger.templateFields.timeEffect.includes('reversed'); const hasEffectId = trigger.templateFields.timeEffect.includes('effectId'); const showMultipleEffects = trigger.showMultipleEffectsNote; @@ -56,9 +25,9 @@ export function render(data, fragments) { `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event` })}\n`, ); - const effectsClosing = showMultipleEffects - ? ` },\n // additional effects targeting other elements can be added here` - : ` }`; + const fillCritical = isHover + ? "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`." + : "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`."; return `# ${Name} Trigger Rules for ${data.meta.packageName} @@ -79,7 +48,7 @@ ${pitfallsBlock} Use \`keyframeEffect\` or \`namedEffect\` when the ${name} should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior. -**CRITICAL:** ${o.fillCritical} +**CRITICAL:** ${fillCritical} ${multipleEffectsNote} \`\`\`typescript { @@ -112,7 +81,7 @@ ${multipleEffectsNote} ### Variables -${buildVariables(trigger, o, isClick, hasReversed, hasEffectId)} +${buildVariables(trigger, isClick, isHover, hasReversed, hasEffectId)} --- @@ -152,7 +121,8 @@ Use \`transition\` when all properties share timing. Use \`transitionProperties\ }, // ... more properties ] -${effectsClosing} + }, + // additional effects targeting other elements can be added here ] } \`\`\` @@ -187,7 +157,8 @@ Use \`customEffect\` when you need imperative control over the animation (e.g. c customEffect: [CUSTOM_EFFECT_CALLBACK], duration: [DURATION_MS], easing: '[EASING_FUNCTION]' -${effectsClosing} + }, + // additional effects targeting other elements can be added here ] } \`\`\` @@ -227,21 +198,35 @@ Use sequences when a ${name} should sync/stagger animations across multiple elem - \`[SOURCE_KEY]\` / \`[TRIGGER_TYPE]\` — same as Rule 1. \`triggerType\` is set on the sequence config, not on individual effects within the sequence. - \`[OFFSET_MS]\` — time offset for staggering each child's animation start, in milliseconds. -- \`[OFFSET_EASING]\` — ${o.sequenceOffsetEasingDesc} +- \`[OFFSET_EASING]\` — easing curve for the offset staggering distribution.${when(isHover, " CSS easing string, or named easing from `@wix/motion`.")} Defaults to \`'linear'\`. - \`[EFFECT_DEFINITION]\` — a definition of or a reference to a time-based animation effect. `; } -function buildVariables(trigger, o, isClick, hasReversed, hasEffectId) { +function buildVariables(trigger, isClick, isHover, hasReversed, hasEffectId) { + const sourceKeyAction = isClick ? 'clicks' : trigger.name; + const targetKeyDesc = isClick + ? "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element." + : "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above)."; + const fillModeDesc = isHover + ? "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished." + : "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect."; + const easingDesc = isHover + ? "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`." + : 'CSS easing string, or named easing from `@wix/motion`.'; + const iterationsDesc = isHover + ? "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`." + : 'optional. Number of iterations, or `Infinity` for continuous loops.'; + const lines = [ - `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${o.sourceKeyDesc}`, - `- \`[TARGET_KEY]\` — ${o.targetKeyDesc}`, + `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). The element that listens for ${sourceKeyAction}.`, + `- \`[TARGET_KEY]\` — ${targetKeyDesc}`, `- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of:`, ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`), `- \`[KEYFRAMES]\` — array of keyframe objects (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase.`, `- \`[EFFECT_NAME]\` — unique string identifier for a \`keyframeEffect\`.`, `- \`[NAMED_EFFECT_DEFINITION]\` — object with properties of pre-built effect from \`@wix/motion-presets\`. Refer to motion-presets rules for available presets and their options.`, - `- \`[FILL_MODE]\` — ${o.fillModeDesc}`, + `- \`[FILL_MODE]\` — ${fillModeDesc}`, ]; if (hasReversed) { lines.push( @@ -250,10 +235,10 @@ function buildVariables(trigger, o, isClick, hasReversed, hasEffectId) { } lines.push( `- \`[DURATION_MS]\` — animation duration in milliseconds.`, - `- \`[EASING_FUNCTION]\` — ${o.easingDesc}`, + `- \`[EASING_FUNCTION]\` — ${easingDesc}`, `- \`[DELAY_MS]\` — optional delay before the effect starts, in milliseconds.`, - `- \`[ITERATIONS]\` — ${o.iterationsDesc}`, - `- \`[ALTERNATE_BOOL]\` — optional. \`true\` to alternate direction on every other iteration (within a single playback).${when(isClick, " Different from \`triggerType: 'alternate'\` which alternates per click.")}`, + `- \`[ITERATIONS]\` — ${iterationsDesc}`, + `- \`[ALTERNATE_BOOL]\` — optional. \`true\` to alternate direction on every other iteration (within a single playback).${when(isClick, " Different from `triggerType: 'alternate'` which alternates per click.")}`, ); if (hasEffectId) { lines.push( diff --git a/packages/interact/_content/templates/integration.mjs b/packages/interact/_content/templates/integration.mjs index 3a9c5f9d..82142623 100644 --- a/packages/interact/_content/templates/integration.mjs +++ b/packages/interact/_content/templates/integration.mjs @@ -177,6 +177,7 @@ ${fragments.get('element-resolution', 'target-brief')} | \`viewProgress\` | Scroll-driven (ViewTimeline) | No trigger params. Configure \`rangeStart\`/\`rangeEnd\` on the **effect**, not on \`params\` | [viewprogress.md](./viewprogress.md) | | \`pointerMove\` | Mouse movement | \`hitArea?\`: \`'self'\` \\| \`'root'\`; \`axis?\`: \`'x'\` \\| \`'y'\` | [pointermove.md](./pointermove.md) | | \`animationEnd\` | Chain after another effect | \`effectId\`: ID of the preceding effect | — | +| \`pageVisible\` | Page visibility change | No params. Fires when the page becomes visible (e.g. tab switch). | — | For \`hover\`/\`click\` (and their accessible variants \`interest\`/\`activate\`): set \`triggerType\` on the effect for keyframe/named/custom effects (TimeEffect), or \`stateAction\` on the effect for transitions (StateEffect). Do not mix both on the same effect. diff --git a/packages/interact/_content/templates/pointermove-rule.mjs b/packages/interact/_content/templates/pointermove-rule.mjs index 98097267..b88ccaf1 100644 --- a/packages/interact/_content/templates/pointermove-rule.mjs +++ b/packages/interact/_content/templates/pointermove-rule.mjs @@ -1,9 +1,15 @@ /** * Renders pointermove.md — rules for pointer-driven interactions with 2D mouse tracking. - * @param {{ trigger: object, meta: object }} data — must include `trigger` (pointerMove from triggers.yaml) and `meta` + * @param {{ trigger: object, effects: object, meta: object }} data * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { + const { trigger } = data; + + const paramsTypeFields = trigger.params + .map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type};`) + .join('\n'); + return `# PointerMove Trigger Rules for ${data.meta.packageName} These rules help generate pointer-driven interactions using \`${data.meta.packageName}\`. PointerMove triggers create real-time animations that respond to mouse movement over elements or the entire viewport. @@ -32,8 +38,7 @@ ${fragments.get('pitfalls/hit-area', 'pointermove-source')} \`\`\`typescript type PointerMoveParams = { - hitArea?: 'root' | 'self'; - axis?: 'x' | 'y'; +${paramsTypeFields} }; \`\`\` @@ -103,7 +108,7 @@ For devices with dynamic viewport sizes (e.g. mobile browsers where the address ## Rule 1: namedEffect -Use pre-built mouse presets from \`@wix/motion-presets\` that handle 2D mouse tracking internally. Mouse presets are preferred over \`keyframeEffect\` for 2D effects. +Use pre-built mouse presets from \`${data.meta.presetsPackage}\` that handle 2D mouse tracking internally. Mouse presets are preferred over \`keyframeEffect\` for 2D effects. Available mouse presets: ${data.effects.presets.mouse.map((n) => `\`${n}\``).join(', ')}. **Multiple effects:** The \`effects\` array can contain multiple effects — all share the same pointer tracking and fire together. Use this to animate different targets from the same pointer movement. @@ -135,7 +140,7 @@ Use pre-built mouse presets from \`@wix/motion-presets\` that handle 2D mouse tr - \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). The element that tracks pointer movement. - \`[TARGET_KEY]\` — identifier matching the element's key on the element to animate (can be same as source or different). - \`[HIT_AREA]\` — \`'self'\` (track pointer within source element) or \`'root'\` (track pointer anywhere in viewport). -- \`[NAMED_EFFECT_TYPE]\` — a registered effect name, or a preset from \`@wix/motion-presets\` \`mouse\` library. +- \`[NAMED_EFFECT_TYPE]\` — a registered effect name, or a preset from \`${data.meta.presetsPackage}\` \`mouse\` library. - \`[EFFECT_PROPERTIES]\` — preset-specific options. Refer to motion-presets rules for each preset's available options and their value types. Do NOT guess preset option names or types; omit unknown options and rely on defaults. - \`[CENTERED_TO_TARGET]\` — \`true\` or \`false\`. See **Centering with \`centeredToTarget\`** above. - \`[TRANSITION_DURATION_MS]\` — optional number. Milliseconds for smoothing (interpolating) between progress updates. The animation does not jump to the new progress value instantly; instead it transitions over this duration. Use to add inertia/lag to the effect, making it feel more physical (e.g. \`200\`–\`600\`). @@ -238,7 +243,7 @@ Use two separate interactions on the same source/target pair — one for \`axis: - \`[HIT_AREA]\` — \`'self'\` or \`'root'\`. - \`[X_EFFECT_ID]\` / \`[Y_EFFECT_ID]\` — unique string identifiers for the X-axis and Y-axis effects. Required — they map to keys in the top-level \`effects\` map. - \`[X_EFFECT_NAME]\` / \`[Y_EFFECT_NAME]\` — unique string names for each keyframe effect. -- \`[X_KEYFRAMES]\` / \`[Y_KEYFRAMES]\` — arrays of WAAPI keyframe objects for the X-axis and Y-axis effects respectively. Each effect can vary in propertise and keyframes. +- \`[X_KEYFRAMES]\` / \`[Y_KEYFRAMES]\` — arrays of WAAPI keyframe objects for the X-axis and Y-axis effects respectively. Each effect can vary in properties and keyframes. - \`[COMPOSITE_OPERATION]\` — \`'add'\` or \`'accumulate'\`. Required when both effects animate \`transform\` and/or both animate \`filter\`, so their values combine rather than override. \`'add'\`: composited transform functions are appended. \`'accumulate'\`: matching function arguments are summed. - \`[FILL_MODE]\` — typically \`'both'\` to ensure the effect keeps applying after exiting the effect's active range. - \`[TRANSITION_DURATION_MS]\` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details. diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index 3d1555e1..d9c4b06d 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -1,9 +1,16 @@ /** * Renders viewprogress.md — rules for scroll-driven animations using ViewTimeline. - * @param {{ trigger: object, meta: object }} data — must include `trigger` (viewProgress from triggers.yaml) and `meta` + * @param {{ trigger: object, effects: object, meta: object }} data * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { + const rangeList = Object.entries(data.effects.rangeNames) + .map(([name, desc]) => { + const extra = name === 'contain' ? '. Typically used with a `position: sticky` container' : ''; + return ` - \`'${name}'\` — ${desc.charAt(0).toLowerCase()}${desc.slice(1)}${extra}.`; + }) + .join('\n'); + return `# ViewProgress Trigger Rules for ${data.meta.packageName} These rules help generate scroll-driven interactions using \`${data.meta.packageName}\`. ViewProgress triggers create animations that update continuously as elements move through the viewport, leveraging native CSS ViewTimelines where supported, and using a polyfill library where unsupported. Use when animation progress should be tied to the element's scroll position. @@ -59,12 +66,7 @@ ${fragments.get('pitfalls/overflow-clip', 'short')} - \`[EFFECT_NAME]\` — unique name for custom keyframe effect. - \`[EFFECT_KEYFRAMES]\` — array of keyframe objects defining CSS property values (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase. - \`[RANGE_NAME]\` — scroll range name: - - \`'cover'\` — full visibility span from first pixel entering to last pixel leaving. - - \`'entry'\` — the phase while the element is entering the viewport. - - \`'exit'\` — the phase while the element is exiting the viewport. - - \`'contain'\` — while the element is fully contained in the viewport. Typically used with a \`position: sticky\` container. - - \`'entry-crossing'\` — from the element's leading edge entering to its leading edge reaching the opposite side. - - \`'exit-crossing'\` — from the element's trailing edge reaching the start to its trailing edge leaving. +${rangeList} - \`[START_PERCENTAGE]\` — 0–100, starting point within the named range. - \`[END_PERCENTAGE]\` — 0–100, end point within the named range. - \`[EASING_FUNCTION]\` - CSS easing string or named easing from \`@wix/motion\`. Typically \`'linear'\` for scrolling effects. diff --git a/packages/interact/rules/click.md b/packages/interact/rules/click.md index e41a70c3..1d9c3d58 100644 --- a/packages/interact/rules/click.md +++ b/packages/interact/rules/click.md @@ -109,7 +109,8 @@ Use `transition` when all properties share timing. Use `transitionProperties` wh }, // ... more properties ] - } + }, + // additional effects targeting other elements can be added here ] } ``` @@ -145,7 +146,8 @@ Use `customEffect` when you need imperative control over the animation (e.g. cou customEffect: [CUSTOM_EFFECT_CALLBACK], duration: [DURATION_MS], easing: '[EASING_FUNCTION]' - } + }, + // additional effects targeting other elements can be added here ] } ``` diff --git a/packages/interact/rules/full-lean.md b/packages/interact/rules/full-lean.md index 62afec5e..220bd101 100644 --- a/packages/interact/rules/full-lean.md +++ b/packages/interact/rules/full-lean.md @@ -33,9 +33,8 @@ Declarative configuration-driven interaction library. Binds animations to trigge Each item here is CRITICAL — ignoring any of them will break animations. - **CRITICAL — `overflow: hidden` breaks `viewProgress`**: Replace with `overflow: clip` on all ancestors between source and scroll container. In Tailwind, replace `overflow-hidden` with `overflow-clip`. -- **CRITICAL**: When using `viewEnter` trigger and source (trigger) and target (effect) elements are the **same element**, use ONLY `type: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`) MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing. -- **CRITICAL - Hit-area shift**: When a hover effect changes the size or position of the hovered element (e.g., `transform: scale(…)`), MUST use a 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. +- **CRITICAL**: When using `viewEnter` trigger and source (trigger) and target (effect) elements are the **same element**, use ONLY `triggerType: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`) MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing. +- **CRITICAL - Hit-area shift**: When a hover effect changes the size or position of the hovered element (e.g., `transform: scale(…)`), MUST use a 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. - **CRITICAL**: For `pointerMove` trigger MUST AVOID using the same element as both source and target with `hitArea: 'self'` and effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. - **CRITICAL — Do NOT guess preset options**: If you don't know the expected type/structure for a `namedEffect` param, omit it — rely on defaults rather than guessing. - **Reduced motion**: Use conditions to provide gentler alternatives (shorter durations, fewer transforms, no perpetual motion) for users who prefer reduced motion. You can also set `Interact.forceReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches` to force a global reduced-motion behavior programmatically. @@ -393,19 +392,19 @@ Used with `viewProgress` and `pointerMove` triggers. ```ts { - name?: 'entry' | 'exit' | 'contain' | 'cover' | 'entry-crossing' | 'exit-crossing'; + name?: 'cover' | 'entry' | 'exit' | 'contain' | 'entry-crossing' | 'exit-crossing'; offset?: { value: number; unit: 'percentage' | 'px' | 'vh' | 'vw' } } ``` | Range name | Meaning | | :--------------- | :------------------------------------------------------------- | -| `entry` | Element entering viewport | -| `exit` | Element exiting viewport | -| `contain` | After `entry` range and before `exit` range | -| `cover` | Full range from `entry` through `contain` and `exit` | -| `entry-crossing` | From element's leading edge entering to trailing edge entering | -| `exit-crossing` | From element's leading edge exiting to trailing edge exiting | +| `cover` | Full visibility span from first pixel entering to last pixel leaving | +| `entry` | The phase while the element is entering the viewport | +| `exit` | The phase while the element is exiting the viewport | +| `contain` | While the element is fully contained in the viewport | +| `entry-crossing` | From the element's leading edge entering to its leading edge reaching the opposite side | +| `exit-crossing` | From the element's trailing edge reaching the start to its trailing edge leaving | **Sticky container pattern** — for scroll-driven animations inside a stuck `position: sticky` container: diff --git a/packages/interact/rules/integration.md b/packages/interact/rules/integration.md index 7dffd861..aea8e657 100644 --- a/packages/interact/rules/integration.md +++ b/packages/interact/rules/integration.md @@ -217,6 +217,7 @@ The target element is what the effect animates. Resolved in priority order: | `viewProgress` | Scroll-driven (ViewTimeline) | No trigger params. Configure `rangeStart`/`rangeEnd` on the **effect**, not on `params` | [viewprogress.md](./viewprogress.md) | | `pointerMove` | Mouse movement | `hitArea?`: `'self'` \| `'root'`; `axis?`: `'x'` \| `'y'` | [pointermove.md](./pointermove.md) | | `animationEnd` | Chain after another effect | `effectId`: ID of the preceding effect | — | +| `pageVisible` | Page visibility change | No params. Fires when the page becomes visible (e.g. tab switch). | — | For `hover`/`click` (and their accessible variants `interest`/`activate`): set `triggerType` on the effect for keyframe/named/custom effects (TimeEffect), or `stateAction` on the effect for transitions (StateEffect). Do not mix both on the same effect. diff --git a/packages/interact/rules/pointermove.md b/packages/interact/rules/pointermove.md index 06f347eb..5aed2cad 100644 --- a/packages/interact/rules/pointermove.md +++ b/packages/interact/rules/pointermove.md @@ -100,7 +100,7 @@ For devices with dynamic viewport sizes (e.g. mobile browsers where the address ## Rule 1: namedEffect -Use pre-built mouse presets from `@wix/motion-presets` that handle 2D mouse tracking internally. Mouse presets are preferred over `keyframeEffect` for 2D effects. +Use pre-built mouse presets from `@wix/motion-presets` that handle 2D mouse tracking internally. Mouse presets are preferred over `keyframeEffect` for 2D effects. Available mouse presets: `TrackMouse`, `Tilt3DMouse`, `Track3DMouse`, `SwivelMouse`, `AiryMouse`, `ScaleMouse`, `BlurMouse`, `SkewMouse`, `BlobMouse`. **Multiple effects:** The `effects` array can contain multiple effects — all share the same pointer tracking and fire together. Use this to animate different targets from the same pointer movement. @@ -235,7 +235,7 @@ Use two separate interactions on the same source/target pair — one for `axis: - `[HIT_AREA]` — `'self'` or `'root'`. - `[X_EFFECT_ID]` / `[Y_EFFECT_ID]` — unique string identifiers for the X-axis and Y-axis effects. Required — they map to keys in the top-level `effects` map. - `[X_EFFECT_NAME]` / `[Y_EFFECT_NAME]` — unique string names for each keyframe effect. -- `[X_KEYFRAMES]` / `[Y_KEYFRAMES]` — arrays of WAAPI keyframe objects for the X-axis and Y-axis effects respectively. Each effect can vary in propertise and keyframes. +- `[X_KEYFRAMES]` / `[Y_KEYFRAMES]` — arrays of WAAPI keyframe objects for the X-axis and Y-axis effects respectively. Each effect can vary in properties and keyframes. - `[COMPOSITE_OPERATION]` — `'add'` or `'accumulate'`. Required when both effects animate `transform` and/or both animate `filter`, so their values combine rather than override. `'add'`: composited transform functions are appended. `'accumulate'`: matching function arguments are summed. - `[FILL_MODE]` — typically `'both'` to ensure the effect keeps applying after exiting the effect's active range. - `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details. From 0ac3c5f932ea4c335918d2c18c7c7ebbd00ecc3a Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 16:37:17 +0300 Subject: [PATCH 08/27] PR fixes --- packages/interact/_content/templates/event-trigger-rule.mjs | 2 +- packages/interact/_content/templates/viewprogress-rule.mjs | 3 ++- packages/interact/rules/full-lean.md | 4 ++-- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index cdb29eef..ec348bb8 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -198,7 +198,7 @@ Use sequences when a ${name} should sync/stagger animations across multiple elem - \`[SOURCE_KEY]\` / \`[TRIGGER_TYPE]\` — same as Rule 1. \`triggerType\` is set on the sequence config, not on individual effects within the sequence. - \`[OFFSET_MS]\` — time offset for staggering each child's animation start, in milliseconds. -- \`[OFFSET_EASING]\` — easing curve for the offset staggering distribution.${when(isHover, " CSS easing string, or named easing from `@wix/motion`.")} Defaults to \`'linear'\`. +- \`[OFFSET_EASING]\` — easing curve for the offset staggering distribution.${when(isHover, ' CSS easing string, or named easing from `@wix/motion`.')} Defaults to \`'linear'\`. - \`[EFFECT_DEFINITION]\` — a definition of or a reference to a time-based animation effect. `; } diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index d9c4b06d..134659a1 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -6,7 +6,8 @@ export function render(data, fragments) { const rangeList = Object.entries(data.effects.rangeNames) .map(([name, desc]) => { - const extra = name === 'contain' ? '. Typically used with a `position: sticky` container' : ''; + const extra = + name === 'contain' ? '. Typically used with a `position: sticky` container' : ''; return ` - \`'${name}'\` — ${desc.charAt(0).toLowerCase()}${desc.slice(1)}${extra}.`; }) .join('\n'); diff --git a/packages/interact/rules/full-lean.md b/packages/interact/rules/full-lean.md index 220bd101..fc08a4c3 100644 --- a/packages/interact/rules/full-lean.md +++ b/packages/interact/rules/full-lean.md @@ -397,8 +397,8 @@ Used with `viewProgress` and `pointerMove` triggers. } ``` -| Range name | Meaning | -| :--------------- | :------------------------------------------------------------- | +| Range name | Meaning | +| :--------------- | :-------------------------------------------------------------------------------------- | | `cover` | Full visibility span from first pixel entering to last pixel leaving | | `entry` | The phase while the element is entering the viewport | | `exit` | The phase while the element is exiting the viewport | From 4033eb8149daa700c90702d32c081f2b9d3c8422 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 17:03:20 +0300 Subject: [PATCH 09/27] PR fixes --- .../rules_build_pipeline_ffe2a9f3.plan.md | 193 +++++++++++------- packages/interact/_content/data/triggers.yaml | 115 ++++------- .../_content/templates/event-trigger-rule.mjs | 53 ++--- .../interact/_content/templates/full-lean.mjs | 24 ++- packages/interact/rules/click.md | 2 +- 5 files changed, 199 insertions(+), 188 deletions(-) diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md index f95d8887..ee3544a3 100644 --- a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md +++ b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md @@ -62,6 +62,21 @@ todos: - id: fix-regenerate content: 'Run `build:rules`, verify output, commit regenerated rules/*.md files' status: completed + - id: fix-prettier-range-table + content: 'Fix Prettier CI failure: range table in full-lean.mjs had hardcoded header widths that did not match generated data row widths. Made header + separator dynamic so column widths are computed from actual data.' + status: completed + - id: fix-variable-overrides-yaml + content: 'Move trigger-specific variable descriptions (sourceKeySuffix, targetKeyDesc, fillModeDesc, easingDesc, iterationsDesc, fillCritical, customEffectExamples, offsetEasingSuffix, alternateBoolSuffix) into `variableOverrides` in triggers.yaml. Eliminates all isClick/isHover ternaries from event-trigger-rule.mjs — template is now fully data-driven.' + status: completed + - id: fix-simplify-build-variables + content: 'Simplify buildVariables() — remove isClick/isHover/hasReversed/hasEffectId positional params, derive hasReversed/hasEffectId from trigger.templateFields internally, read all prose from trigger.variableOverrides.' + status: completed + - id: fix-remove-unused-yaml + content: 'Remove unused YAML fields from triggers.yaml: category, supportsTimeEffect, supportsStateEffect, supportsScrubEffect, supportsCustomEffect (never read by any template). Flatten templateFields from nested {timeEffect/stateEffect/customEffect} to a flat array (only timeEffect fields were ever consumed).' + status: completed + - id: fix-regenerate-2 + content: 'Regenerate rules/*.md, verify all CI checks pass (build, lint, format, test, rules freshness)' + status: completed isProject: false --- @@ -96,86 +111,81 @@ All source files live under `packages/interact/_content/`. The build script read ### 1. Data files (`_content/data/`) -`**triggers.yaml**` — one entry per trigger, capturing everything that varies: +`**triggers.yaml**` — one entry per trigger, capturing structured data: ```yaml triggers: - name: hover a11yAlias: interest a11yNote: "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus." - category: event # event | viewport | scroll | pointer | chain - supportsTimeEffect: true - supportsStateEffect: true - supportsScrubEffect: false - supportsCustomEffect: true - params: [] # no trigger params + defaultTriggerType: alternate + params: [] pitfalls: - - id: hit-area # references fragments/pitfalls/hit-area.md - templateFields: # which optional fields to show in config templates - timeEffect: - [ - triggerType, - keyframeEffect, - namedEffect, - fill, - duration, - easing, - delay, - iterations, - alternate, - ] - stateEffect: [stateAction, transition, transitionProperties] - customEffect: [triggerType, customEffect, duration, easing] - sequence: [triggerType, offset, offsetEasing] - triggerTypeDescriptions: # trigger-specific wording for each triggerType value - alternate: 'plays forward on enter, reverses on leave' - repeat: 'restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses' - once: 'plays once on the first enter and never again' - state: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`)' + - id: hit-area + showMultipleEffectsNote: true + templateFields: + [ + triggerType, + keyframeEffect, + namedEffect, + fill, + duration, + easing, + delay, + iterations, + alternate, + ] + triggerTypeDescriptions: + alternate: 'plays forward on enter, reverses on leave. Default. Most common for hover.' + repeat: 'restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses.' + once: 'plays once on the first enter and never again.' + state: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`).' stateActionDescriptions: - toggle: 'applies the style state on enter, removes on leave' - add: 'applies the style state on enter. Leave does NOT remove it' - remove: 'removes a previously applied style state on enter' - clear: 'clears all previously applied style states on enter' - fillNote: 'while hovering' # trigger-specific fill context + toggle: 'applies the style state on enter, removes on leave. Default.' + # ... + variableOverrides: # trigger-specific prose for variable descriptions in generated rule files + sourceKeySuffix: 'The element that listens for hover.' + targetKeyDesc: "identifier matching the element's key on the element that animates. ..." + fillModeDesc: "usually `'both'`. Keeps the final state applied while hovering..." + fillCritical: "Always include `fill: 'both'` for ..." + easingDesc: "CSS easing string (e.g. `'ease-out'`, ...), or named easing from `@wix/motion`." + iterationsDesc: 'optional. Number of iterations, or `Infinity` for continuous loops. ...' + customEffectExamples: '' + offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.' + alternateBoolSuffix: '' - name: click a11yAlias: activate - a11yNote: "Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space)." - category: event - supportsTimeEffect: true - supportsStateEffect: true - supportsScrubEffect: false - supportsCustomEffect: true + a11yNote: "Use `trigger: 'activate'` instead of `trigger: 'click'` ..." + defaultTriggerType: alternate params: [] pitfalls: [] + showMultipleEffectsNote: false templateFields: - timeEffect: - [ - triggerType, - keyframeEffect, - namedEffect, - fill, - reversed, - duration, - easing, - delay, - iterations, - alternate, - effectId, - ] - # ... (click includes reversed + effectId that hover omits) + [ + triggerType, + keyframeEffect, + namedEffect, + fill, + reversed, + duration, + easing, + delay, + iterations, + alternate, + effectId, + ] + # click includes reversed + effectId that hover omits triggerTypeDescriptions: - alternate: 'plays forward on first click, reverses on next click' - repeat: 'restarts the animation from the beginning on each click' - once: 'plays once on the first click and never again' - state: 'resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`)' - stateActionDescriptions: - toggle: 'applies the style state, removes it on next click' - add: 'applies the style state. Does not remove on subsequent clicks' - remove: 'removes a previously applied style state' - clear: 'clears all previously applied style states. Useful for resetting multiple stacked style states at once' - fillNote: 'while finished' + alternate: 'plays forward on first click, reverses on next click. Default.' + # ... + variableOverrides: + sourceKeySuffix: 'The element that listens for clicks.' + targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to ..." + fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, ..." + fillCritical: "Always include `fill: 'both'` for ..." + customEffectExamples: ', randomized behavior' + alternateBoolSuffix: " Different from `triggerType: 'alternate'` which alternates per click." # ... viewEnter, viewProgress, pointerMove, animationEnd entries follow ``` @@ -261,11 +271,15 @@ Each template is a `.mjs` file exporting a function that receives data + fragmen ```javascript // event-trigger-rule.mjs -export function render(trigger, data, fragments) { - return `# ${capitalize(trigger.name)} Trigger Rules for ${data.meta.packageName} +export function render(data, fragments) { + const { trigger } = data; + const { name, variableOverrides: vo } = trigger; + return `# ${capitalize(name)} Trigger Rules for ${data.meta.packageName} +... +**CRITICAL — Accessible ${name}**: ${trigger.a11yNote} +${trigger.pitfalls.map((p) => fragments.get(`pitfalls/${p.id}`, name)).join('\n')} ... -${trigger.a11yAlias ? `**CRITICAL — Accessible ${trigger.name}**: ${trigger.a11yNote}` : ''} -${trigger.pitfalls.map((p) => fragments.get(`pitfalls/${p.id}`, trigger.name)).join('\n')} +**CRITICAL:** ${vo.fillCritical} ...`; } ``` @@ -468,6 +482,45 @@ This ensures that if someone edits a source file but forgets to re-run `build:ru After all fixes above, run `build:rules` and commit the regenerated `rules/*.md` files so they reflect the new `[FILL_MODE]` variable ordering in hover.md. +## Post-PR Fixes — Round 2 (code review refinements) + +Fixes from a second review pass, addressing CI failures and simplification opportunities. + +### Fix 10: Fix Prettier CI failure on range table + +The range table in `full-lean.mjs` had a hardcoded header row (`| Range name | Meaning |`) with fixed column widths, but the generated data rows were wider (due to `entry-crossing` description). Prettier auto-pads markdown tables to the widest cell, so CI's `format:check` failed. + +**Action:** Made the header + separator row dynamic — compute column widths from `Math.max(headerWidth, ...dataWidths)` and include the header in the generated `rangeTable` variable. + +### Fix 11: Move variable description overrides into `triggers.yaml` + +Fix 3 moved 15+ prose description fields out of YAML into `isClick`/`isHover` ternaries in `event-trigger-rule.mjs`. However, the trigger-specific differences are short structured phrases (not paragraphs), and the ternaries made the template harder to extend with new event triggers. + +**Action:** + +- Added `variableOverrides` map to each event trigger entry in `triggers.yaml` with fields: `sourceKeySuffix`, `targetKeyDesc`, `fillModeDesc`, `easingDesc`, `iterationsDesc`, `fillCritical`, `customEffectExamples`, `offsetEasingSuffix`, `alternateBoolSuffix`. +- Updated `event-trigger-rule.mjs` to read all prose from `trigger.variableOverrides` (aliased as `vo`) instead of `isClick`/`isHover` ternaries. +- Template is now fully data-driven — adding a third event trigger requires only a new YAML entry, no template changes. + +### Fix 12: Simplify `buildVariables()` function + +The function took 5 positional parameters (`trigger, isClick, isHover, hasReversed, hasEffectId`), 4 of which were derivable from `trigger`. + +**Action:** Reduced to `buildVariables(trigger, hasReversed, hasEffectId)`. Reads all prose from `trigger.variableOverrides`. The `hasReversed`/`hasEffectId` booleans are still passed from the caller since they're also used in the template string. + +### Fix 13: Remove unused YAML fields + +`triggers.yaml` had fields that no template ever consumed: `category`, `supportsTimeEffect`, `supportsStateEffect`, `supportsScrubEffect`, `supportsCustomEffect`. The `templateFields` was also a nested object (`{timeEffect, stateEffect, customEffect}`) but only `timeEffect` was ever read. + +**Action:** + +- Removed all `category` and `supports*` fields from all 7 trigger entries. +- Flattened `templateFields` from nested object to a flat array (the fields that were under `timeEffect`). + +### Fix 14: Regenerate output (round 2) + +After all round 2 fixes, ran `build:rules` and verified all 5 CI checks pass: build, lint, format, test, rules freshness. + --- ## Future Extension Points @@ -476,3 +529,5 @@ After all fixes above, run `build:rules` and commit the regenerated `rules/*.md` - **README**: Same meta.yaml + triggers.yaml generates README sections - **Validation**: Add `scripts/validate-rules.mjs` that imports actual TS types and cross-checks trigger names, param fields, effect fields against the YAML data - **motion-presets rules**: The build mechanism is scoped to `packages/interact/` but the architecture (YAML + fragments + templates) can be replicated in `packages/motion-presets/` with shared fragments if needed +- **Common variable fragment**: Extract repeated variable descriptions (`[SOURCE_KEY]`, `[TARGET_KEY]`, `[DURATION_MS]`, etc.) into a shared fragment to deduplicate across viewenter/viewprogress/pointermove/event-trigger templates +- **FOUC fragment consolidation**: Reduce fouc.md's 13 sections to ~8 by using `{{param}}` interpolation for placeholder vs example variants diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml index 3583ca32..c49dea85 100644 --- a/packages/interact/_content/data/triggers.yaml +++ b/packages/interact/_content/data/triggers.yaml @@ -2,36 +2,21 @@ triggers: - name: hover a11yAlias: interest a11yNote: "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus." - category: event - supportsTimeEffect: true - supportsStateEffect: true - supportsScrubEffect: false - supportsCustomEffect: true defaultTriggerType: alternate params: [] pitfalls: - id: hit-area showMultipleEffectsNote: true templateFields: - timeEffect: - - triggerType - - keyframeEffect - - namedEffect - - fill - - duration - - easing - - delay - - iterations - - alternate - stateEffect: - - stateAction - - transition - - transitionProperties - customEffect: - - triggerType - - customEffect - - duration - - easing + - triggerType + - keyframeEffect + - namedEffect + - fill + - duration + - easing + - delay + - iterations + - alternate triggerTypeDescriptions: alternate: 'plays forward on enter, reverses on leave. Default. Most common for hover.' repeat: 'restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses.' @@ -42,41 +27,36 @@ triggers: add: 'applies the style state on enter. Leave does NOT remove it.' remove: 'removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`.' clear: 'clears all previously applied style states on enter. Use to reset multiple stacked `''add''` style changes at once (e.g. a "reset" hover area that undoes several accumulated states).' + variableOverrides: + sourceKeySuffix: 'The element that listens for hover.' + targetKeyDesc: "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above)." + fillModeDesc: "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished." + easingDesc: "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`." + iterationsDesc: "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`." + fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`." + customEffectExamples: '' + offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.' + alternateBoolSuffix: '' - name: click a11yAlias: activate a11yNote: "Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space)." - category: event - supportsTimeEffect: true - supportsStateEffect: true - supportsScrubEffect: false - supportsCustomEffect: true defaultTriggerType: alternate params: [] pitfalls: [] showMultipleEffectsNote: false templateFields: - timeEffect: - - triggerType - - keyframeEffect - - namedEffect - - fill - - reversed - - duration - - easing - - delay - - iterations - - alternate - - effectId - stateEffect: - - stateAction - - transition - - transitionProperties - customEffect: - - triggerType - - customEffect - - duration - - easing + - triggerType + - keyframeEffect + - namedEffect + - fill + - reversed + - duration + - easing + - delay + - iterations + - alternate + - effectId triggerTypeDescriptions: alternate: 'plays forward on first click, reverses on next click. Default.' repeat: 'restarts the animation from the beginning on each click.' @@ -87,13 +67,18 @@ triggers: add: 'applies the style state. Does not remove on subsequent clicks.' remove: 'removes a previously applied style state.' clear: 'clears all previously applied style states. Useful for resetting multiple stacked style states at once.' + variableOverrides: + sourceKeySuffix: 'The element that listens for clicks.' + targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element." + fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect." + easingDesc: 'CSS easing string, or named easing from `@wix/motion`.' + iterationsDesc: 'optional. Number of iterations, or `Infinity` for continuous loops.' + fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`." + customEffectExamples: ', randomized behavior' + offsetEasingSuffix: '' + alternateBoolSuffix: " Different from `triggerType: 'alternate'` which alternates per click." - name: viewEnter - category: viewport - supportsTimeEffect: true - supportsStateEffect: false - supportsScrubEffect: false - supportsCustomEffect: true defaultTriggerType: once params: - name: threshold @@ -109,11 +94,6 @@ triggers: showMultipleEffectsNote: true - name: viewProgress - category: scroll - supportsTimeEffect: false - supportsStateEffect: false - supportsScrubEffect: true - supportsCustomEffect: true defaultTriggerType: null params: [] pitfalls: @@ -121,11 +101,6 @@ triggers: showMultipleEffectsNote: true - name: pointerMove - category: pointer - supportsTimeEffect: false - supportsStateEffect: false - supportsScrubEffect: true - supportsCustomEffect: true defaultTriggerType: null params: - name: hitArea @@ -140,11 +115,6 @@ triggers: - id: hit-area - name: animationEnd - category: chain - supportsTimeEffect: true - supportsStateEffect: false - supportsScrubEffect: false - supportsCustomEffect: false defaultTriggerType: null params: - name: effectId @@ -153,11 +123,6 @@ triggers: description: 'ID of the preceding effect' - name: pageVisible - category: event - supportsTimeEffect: true - supportsStateEffect: false - supportsScrubEffect: false - supportsCustomEffect: false defaultTriggerType: null params: [] pitfalls: [] diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index ec348bb8..55d3625e 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -7,13 +7,10 @@ import { capitalize, when } from './_helpers.mjs'; */ export function render(data, fragments) { const { trigger } = data; - const { name } = trigger; + const { name, variableOverrides: vo } = trigger; const Name = capitalize(name); - const isClick = name === 'click'; - const isHover = name === 'hover'; - const hasReversed = trigger.templateFields.timeEffect.includes('reversed'); - const hasEffectId = trigger.templateFields.timeEffect.includes('effectId'); - const showMultipleEffects = trigger.showMultipleEffectsNote; + const hasReversed = trigger.templateFields.includes('reversed'); + const hasEffectId = trigger.templateFields.includes('effectId'); const pitfallsBlock = when( trigger.pitfalls.length > 0, @@ -21,14 +18,10 @@ export function render(data, fragments) { ); const multipleEffectsNote = when( - showMultipleEffects, + trigger.showMultipleEffectsNote, `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event` })}\n`, ); - const fillCritical = isHover - ? "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`." - : "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`."; - return `# ${Name} Trigger Rules for ${data.meta.packageName} This document contains rules for generating ${name}-triggered interactions in \`${data.meta.packageName}\`. @@ -48,7 +41,7 @@ ${pitfallsBlock} Use \`keyframeEffect\` or \`namedEffect\` when the ${name} should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior. -**CRITICAL:** ${fillCritical} +**CRITICAL:** ${vo.fillCritical} ${multipleEffectsNote} \`\`\`typescript { @@ -81,13 +74,13 @@ ${multipleEffectsNote} ### Variables -${buildVariables(trigger, isClick, isHover, hasReversed, hasEffectId)} +${buildVariables(trigger, hasReversed, hasEffectId)} --- ## Rule 2: transition / transitionProperties (StateEffect) -Use \`transition\` or \`transitionProperties\` when the ${name} should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations.${when(isClick, ' Uses the `transition` CSS property.')} Set \`stateAction\` on the effect to control how the style is applied. +Use \`transition\` or \`transitionProperties\` when the ${name} should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Set \`stateAction\` on the effect to control how the style is applied. Use \`transition\` when all properties share timing. Use \`transitionProperties\` when each property needs independent \`duration\`, \`delay\`, or \`easing\`. @@ -144,7 +137,7 @@ ${Object.entries(trigger.stateActionDescriptions) ## Rule 3: customEffect (TimeEffect) -Use \`customEffect\` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation${when(isClick, ', randomized behavior')}). The callback receives the target element and a \`progress\` value (0–1) driven by the animation timeline. +Use \`customEffect\` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation${when(vo.customEffectExamples, vo.customEffectExamples)}). The callback receives the target element and a \`progress\` value (0–1) driven by the animation timeline. \`\`\`typescript { @@ -198,35 +191,23 @@ Use sequences when a ${name} should sync/stagger animations across multiple elem - \`[SOURCE_KEY]\` / \`[TRIGGER_TYPE]\` — same as Rule 1. \`triggerType\` is set on the sequence config, not on individual effects within the sequence. - \`[OFFSET_MS]\` — time offset for staggering each child's animation start, in milliseconds. -- \`[OFFSET_EASING]\` — easing curve for the offset staggering distribution.${when(isHover, ' CSS easing string, or named easing from `@wix/motion`.')} Defaults to \`'linear'\`. +- \`[OFFSET_EASING]\` — easing curve for the offset staggering distribution.${when(vo.offsetEasingSuffix, vo.offsetEasingSuffix)} Defaults to \`'linear'\`. - \`[EFFECT_DEFINITION]\` — a definition of or a reference to a time-based animation effect. `; } -function buildVariables(trigger, isClick, isHover, hasReversed, hasEffectId) { - const sourceKeyAction = isClick ? 'clicks' : trigger.name; - const targetKeyDesc = isClick - ? "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element." - : "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above)."; - const fillModeDesc = isHover - ? "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished." - : "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect."; - const easingDesc = isHover - ? "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`." - : 'CSS easing string, or named easing from `@wix/motion`.'; - const iterationsDesc = isHover - ? "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`." - : 'optional. Number of iterations, or `Infinity` for continuous loops.'; +function buildVariables(trigger, hasReversed, hasEffectId) { + const vo = trigger.variableOverrides; const lines = [ - `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). The element that listens for ${sourceKeyAction}.`, - `- \`[TARGET_KEY]\` — ${targetKeyDesc}`, + `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${vo.sourceKeySuffix}`, + `- \`[TARGET_KEY]\` — ${vo.targetKeyDesc}`, `- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of:`, ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`), `- \`[KEYFRAMES]\` — array of keyframe objects (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase.`, `- \`[EFFECT_NAME]\` — unique string identifier for a \`keyframeEffect\`.`, `- \`[NAMED_EFFECT_DEFINITION]\` — object with properties of pre-built effect from \`@wix/motion-presets\`. Refer to motion-presets rules for available presets and their options.`, - `- \`[FILL_MODE]\` — ${fillModeDesc}`, + `- \`[FILL_MODE]\` — ${vo.fillModeDesc}`, ]; if (hasReversed) { lines.push( @@ -235,10 +216,10 @@ function buildVariables(trigger, isClick, isHover, hasReversed, hasEffectId) { } lines.push( `- \`[DURATION_MS]\` — animation duration in milliseconds.`, - `- \`[EASING_FUNCTION]\` — ${easingDesc}`, + `- \`[EASING_FUNCTION]\` — ${vo.easingDesc}`, `- \`[DELAY_MS]\` — optional delay before the effect starts, in milliseconds.`, - `- \`[ITERATIONS]\` — ${iterationsDesc}`, - `- \`[ALTERNATE_BOOL]\` — optional. \`true\` to alternate direction on every other iteration (within a single playback).${when(isClick, " Different from `triggerType: 'alternate'` which alternates per click.")}`, + `- \`[ITERATIONS]\` — ${vo.iterationsDesc}`, + `- \`[ALTERNATE_BOOL]\` — optional. \`true\` to alternate direction on every other iteration (within a single playback).${when(vo.alternateBoolSuffix, vo.alternateBoolSuffix)}`, ); if (hasEffectId) { lines.push( diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 8ce63d92..89ae6987 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -29,11 +29,23 @@ export function render(data, fragments) { name: `\`${name}\``, desc, })); - const maxNameLen = Math.max(...rangeEntries.map((e) => e.name.length)); - const maxDescLen = Math.max(...rangeEntries.map((e) => e.desc.length)); - const rangeTable = rangeEntries - .map((e) => `| ${e.name.padEnd(maxNameLen)} | ${e.desc.padEnd(maxDescLen)} |`) - .join('\n'); + const rangeHeaderName = 'Range name'; + const rangeHeaderDesc = 'Meaning'; + const rangeNameWidth = Math.max( + rangeHeaderName.length, + ...rangeEntries.map((e) => e.name.length), + ); + const rangeDescWidth = Math.max( + rangeHeaderDesc.length, + ...rangeEntries.map((e) => e.desc.length), + ); + const rangeTable = [ + `| ${rangeHeaderName.padEnd(rangeNameWidth)} | ${rangeHeaderDesc.padEnd(rangeDescWidth)} |`, + `| :${'-'.repeat(rangeNameWidth - 1)} | :${'-'.repeat(rangeDescWidth - 1)} |`, + ...rangeEntries.map( + (e) => `| ${e.name.padEnd(rangeNameWidth)} | ${e.desc.padEnd(rangeDescWidth)} |`, + ), + ].join('\n'); return `# ${data.meta.packageName} — Rules @@ -374,8 +386,6 @@ Used with \`viewProgress\` and \`pointerMove\` triggers. } \`\`\` -| Range name | Meaning | -| :--------------- | :------------------------------------------------------------- | ${rangeTable} **Sticky container pattern** — for scroll-driven animations inside a stuck \`position: sticky\` container: diff --git a/packages/interact/rules/click.md b/packages/interact/rules/click.md index 1d9c3d58..446f7639 100644 --- a/packages/interact/rules/click.md +++ b/packages/interact/rules/click.md @@ -75,7 +75,7 @@ Use `keyframeEffect` or `namedEffect` when the click should play an animation (C ## Rule 2: transition / transitionProperties (StateEffect) -Use `transition` or `transitionProperties` when the click should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Uses the `transition` CSS property. Set `stateAction` on the effect to control how the style is applied. +Use `transition` or `transitionProperties` when the click should toggle styles via DOM attribute change and CSS transitions rather than keyframe animations. Set `stateAction` on the effect to control how the style is applied. Use `transition` when all properties share timing. Use `transitionProperties` when each property needs independent `duration`, `delay`, or `easing`. From 60d5837f1c52a5ca7eb9f654579a06974a3760a5 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 17:23:35 +0300 Subject: [PATCH 10/27] PR fixes --- .../rules_build_pipeline_ffe2a9f3.plan.md | 60 +++++++++++++++++++ packages/interact/_content/data/triggers.yaml | 3 + .../_content/templates/pointermove-rule.mjs | 11 +++- .../_content/templates/viewenter-rule.mjs | 29 +++++++-- .../_content/templates/viewprogress-rule.mjs | 17 +++++- 5 files changed, 111 insertions(+), 9 deletions(-) diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md index ee3544a3..ccfa550f 100644 --- a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md +++ b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md @@ -77,6 +77,21 @@ todos: - id: fix-regenerate-2 content: 'Regenerate rules/*.md, verify all CI checks pass (build, lint, format, test, rules freshness)' status: completed + - id: refactor-pitfalls-yaml-section + content: 'Add `section` field to pitfall entries in triggers.yaml for viewEnter, viewProgress, pointerMove — enables all trigger templates to use trigger.pitfalls data-driven pattern (matching event-trigger-rule.mjs)' + status: completed + - id: refactor-viewenter-data-driven + content: 'Refactor viewenter-rule.mjs to use trigger.pitfalls and trigger.params from YAML instead of hardcoding fragment calls and param descriptions' + status: completed + - id: refactor-viewprogress-data-driven + content: 'Refactor viewprogress-rule.mjs to use trigger.pitfalls from YAML instead of hardcoding the overflow-clip fragment call' + status: completed + - id: refactor-pointermove-data-driven + content: 'Refactor pointermove-rule.mjs to use trigger.pitfalls from YAML instead of hardcoding the hit-area fragment call' + status: completed + - id: refactor-regenerate + content: 'Regenerate rules/*.md, verify output is identical to pre-refactor output' + status: completed isProject: false --- @@ -523,6 +538,51 @@ After all round 2 fixes, ran `build:rules` and verified all 5 CI checks pass: bu --- +## Post-PR Fixes — Round 3 (data-driven trigger templates) + +The trigger-specific templates (viewenter, viewprogress, pointermove) hardcode pitfall fragment calls and param descriptions that already exist in `triggers.yaml`. The event-trigger-rule.mjs template is fully data-driven (iterates `trigger.pitfalls`, reads `trigger.variableOverrides`), but the other templates bypass YAML data entirely. This violates the single-source-of-truth principle. + +### Fix 15: Add `section` field to YAML pitfall entries + +Different pitfall fragments use different section naming conventions. The event-trigger-rule.mjs uses the trigger name as the section (e.g. `fragments.get('pitfalls/hit-area', 'hover')`), which works because `hit-area.md` has a `#hover` section. But other fragments use generic names (`#short`, `#long`, `#pointermove-source`). + +**Action:** Add an optional `section` field to pitfall entries in `triggers.yaml`. Templates use `p.section || name` to resolve the fragment section — defaulting to the trigger name when no explicit section is given. + +```yaml +# hover — no section needed, defaults to 'hover' +pitfalls: + - id: hit-area + +# viewEnter — fragment section is 'short', not 'viewEnter' +pitfalls: + - id: same-element-viewenter + section: short + +# viewProgress +pitfalls: + - id: overflow-clip + section: short + +# pointerMove — fragment section is 'pointermove-source' +pitfalls: + - id: hit-area + section: pointermove-source +``` + +### Fix 16: Data-drive pitfalls in viewenter/viewprogress/pointermove templates + +Replace hardcoded `fragments.get('pitfalls/...', '...')` calls with iteration over `trigger.pitfalls`, matching the pattern in event-trigger-rule.mjs. + +### Fix 17: Data-drive param descriptions in viewenter-rule.mjs + +The variable descriptions for `[VISIBILITY_THRESHOLD]` and `[VIEWPORT_INSETS]` are hardcoded but match the `description` field in `trigger.params`. Pull descriptions from YAML to maintain a single source of truth. + +### Fix 18: Regenerate and verify + +Run `build:rules` and verify the generated output is byte-identical to pre-refactor output. + +--- + ## Future Extension Points - **docs/**: Same `_content/data/` feeds docs templates — add `_content/templates/docs/` later diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml index c49dea85..742ed833 100644 --- a/packages/interact/_content/data/triggers.yaml +++ b/packages/interact/_content/data/triggers.yaml @@ -91,6 +91,7 @@ triggers: description: "String adjusting the viewport detection area (e.g. `'-100px'` extends it, `'50px'` shrinks it)." pitfalls: - id: same-element-viewenter + section: short showMultipleEffectsNote: true - name: viewProgress @@ -98,6 +99,7 @@ triggers: params: [] pitfalls: - id: overflow-clip + section: short showMultipleEffectsNote: true - name: pointerMove @@ -113,6 +115,7 @@ triggers: description: 'restricts pointer tracking to a single axis' pitfalls: - id: hit-area + section: pointermove-source - name: animationEnd defaultTriggerType: null diff --git a/packages/interact/_content/templates/pointermove-rule.mjs b/packages/interact/_content/templates/pointermove-rule.mjs index b88ccaf1..d08cdc06 100644 --- a/packages/interact/_content/templates/pointermove-rule.mjs +++ b/packages/interact/_content/templates/pointermove-rule.mjs @@ -1,3 +1,5 @@ +import { when } from './_helpers.mjs'; + /** * Renders pointermove.md — rules for pointer-driven interactions with 2D mouse tracking. * @param {{ trigger: object, effects: object, meta: object }} data @@ -6,6 +8,13 @@ export function render(data, fragments) { const { trigger } = data; + const pitfallsBlock = when( + trigger.pitfalls?.length > 0, + trigger.pitfalls + .map((p) => fragments.get(`pitfalls/${p.id}`, p.section || trigger.name)) + .join('\n'), + ); + const paramsTypeFields = trigger.params .map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type};`) .join('\n'); @@ -28,7 +37,7 @@ These rules help generate pointer-driven interactions using \`${data.meta.packag ## Trigger Source Elements with \`hitArea: 'self'\` -${fragments.get('pitfalls/hit-area', 'pointermove-source')} +${pitfallsBlock} --- diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index 78140fbf..a85a7d02 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -1,17 +1,37 @@ +import { when } from './_helpers.mjs'; + /** * Renders viewenter.md — rules for viewport-entry triggered animations. * @param {{ trigger: object, meta: object }} data — must include `trigger` (viewEnter from triggers.yaml) and `meta` * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { + const { trigger } = data; + + const pitfallsBlock = when( + trigger.pitfalls?.length > 0, + '\n' + + trigger.pitfalls + .map((p) => fragments.get(`pitfalls/${p.id}`, p.section || trigger.name)) + .join('\n') + + '\n', + ); + + const paramVarNames = { threshold: 'VISIBILITY_THRESHOLD', inset: 'VIEWPORT_INSETS' }; + const paramDescriptions = trigger.params + .map((p) => { + const varName = paramVarNames[p.name] || p.name.toUpperCase(); + const optionalPrefix = p.optional ? 'optional. ' : ''; + return `- \`[${varName}]\` — ${optionalPrefix}${p.description}`; + }) + .join('\n'); + return `# ViewEnter Trigger Rules for ${data.meta.packageName} This document contains rules for generating interactions that respond to elements entering the viewport using the \`${data.meta.packageName}\`. ViewEnter triggers use IntersectionObserver to detect when elements become visible and are ideal for entrance animations, content reveals, and lazy-loading effects. --- - -${fragments.get('pitfalls/same-element-viewenter', 'short')} - +${pitfallsBlock} ## Table of Contents - [Preventing Flash of Unstyled Content (FOUC)](#preventing-flash-of-unstyled-content-fouc) @@ -94,8 +114,7 @@ Use \`keyframeEffect\` or \`namedEffect\` when the viewEnter should play an anim - \`'repeat'\` — restarts the animation every time the source element enters the viewport. Use separate source and target. - \`'alternate'\` — plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target. - \`'state'\` — resumes on enter, pauses on leave. Useful for continuous loops (\`iterations: Infinity\`). Use separate source and target. -- \`[VISIBILITY_THRESHOLD]\` — optional. Number between 0–1 indicating how much of the source element must be visible to trigger (e.g. \`0.3\` = 30%). -- \`[VIEWPORT_INSETS]\` — optional. String adjusting the viewport detection area (e.g. \`'-100px'\` extends it, \`'50px'\` shrinks it). +${paramDescriptions} - \`[KEYFRAMES]\` — array of keyframe objects (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase. - \`[EFFECT_NAME]\` — unique string identifier for a \`keyframeEffect\`. - \`[NAMED_EFFECT_DEFINITION]\` — object with properties of pre-built effect from \`@wix/motion-presets\`. Refer to motion-presets rules for available presets and their options. diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index 134659a1..35db0149 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -1,9 +1,22 @@ +import { when } from './_helpers.mjs'; + /** * Renders viewprogress.md — rules for scroll-driven animations using ViewTimeline. * @param {{ trigger: object, effects: object, meta: object }} data * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { + const { trigger } = data; + + const pitfallsBlock = when( + trigger.pitfalls?.length > 0, + '\n' + + trigger.pitfalls + .map((p) => fragments.get(`pitfalls/${p.id}`, p.section || trigger.name)) + .join('\n') + + '\n', + ); + const rangeList = Object.entries(data.effects.rangeNames) .map(([name, desc]) => { const extra = @@ -15,9 +28,7 @@ export function render(data, fragments) { return `# ViewProgress Trigger Rules for ${data.meta.packageName} These rules help generate scroll-driven interactions using \`${data.meta.packageName}\`. ViewProgress triggers create animations that update continuously as elements move through the viewport, leveraging native CSS ViewTimelines where supported, and using a polyfill library where unsupported. Use when animation progress should be tied to the element's scroll position. - -${fragments.get('pitfalls/overflow-clip', 'short')} - +${pitfallsBlock} **Offset semantics:** The \`offset\` inside \`rangeStart\`/\`rangeEnd\` is an object \`{ unit: 'percentage', value: NUMBER }\` where value is 0–100. For absolute lengths use \`{ unit: 'px', value: NUMBER }\` (or other CSS length units). Positive values move the effective range boundary forward along the scroll axis. ## Table of Contents From 6d5f0004ceffaf175a59047e85341f2d34a496e7 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 17:42:20 +0300 Subject: [PATCH 11/27] PR fixes --- .../rules_build_pipeline_ffe2a9f3.plan.md | 83 ++++++++++++++++++- packages/interact/_content/data/effects.yaml | 10 +++ packages/interact/_content/data/triggers.yaml | 45 ++++++++++ packages/interact/_content/fragments/fouc.md | 30 +------ .../interact/_content/templates/_helpers.mjs | 51 ++++++++++++ .../_content/templates/event-trigger-rule.mjs | 34 ++++---- .../interact/_content/templates/full-lean.mjs | 54 +++++++----- .../_content/templates/pointermove-rule.mjs | 21 ++--- .../_content/templates/viewenter-rule.mjs | 47 +++++------ .../_content/templates/viewprogress-rule.mjs | 24 ++---- packages/interact/rules/viewenter.md | 4 +- packages/interact/rules/viewprogress.md | 6 +- 12 files changed, 283 insertions(+), 126 deletions(-) diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md index ccfa550f..f921093c 100644 --- a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md +++ b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md @@ -92,6 +92,33 @@ todos: - id: refactor-regenerate content: 'Regenerate rules/*.md, verify output is identical to pre-refactor output' status: completed + - id: refactor-common-vars + content: 'Add shared varLine() helper to _helpers.mjs — common variable descriptions (SOURCE_KEY, TARGET_KEY, DURATION_MS, etc.) defined once, used by all trigger templates via varLine(name, suffix)' + status: completed + - id: refactor-pitfalls-helper + content: 'Extract buildPitfallsBlock() helper to _helpers.mjs — all trigger templates use shared function to iterate trigger.pitfalls from YAML' + status: completed + - id: refactor-fouc-merge + content: 'Merge 3 FOUC fragment section pairs (code-web/code-web-example, code-react/code-react-example, code-vanilla/code-vanilla-example) into parameterized sections using {{key}} and {{classAttr}} interpolation' + status: completed + - id: refactor-param-varname + content: 'Add varName field to YAML params entries (e.g. threshold→VISIBILITY_THRESHOLD) — eliminates hardcoded paramVarNames map in viewenter-rule.mjs' + status: completed + - id: refactor-yaml-schema + content: 'Add template field + section comments to triggers.yaml — documents which template renders each trigger, separates event vs viewport vs pointer trigger schemas' + status: completed + - id: refactor-full-lean-tables + content: 'Generate hover/click triggerType + stateAction comparison tables in full-lean.mjs from YAML fullLeanBehavior data — replaces hardcoded markdown tables with buildBehaviorTable() function' + status: completed + - id: refactor-full-lean-pitfalls + content: 'Data-drive full-lean.mjs pitfalls from YAML — add fullLeanSection field to trigger pitfall entries + fullLeanPitfallOrder array in effects.yaml, replaces hardcoded fragment calls' + status: completed + - id: refactor-optional-chaining + content: 'Normalize optional chaining on trigger.pitfalls — all templates now use buildPitfallsBlock() with consistent ?. handling' + status: completed + - id: refactor-regenerate-2 + content: 'Regenerate rules/*.md, verify output — only 5 minor consistency improvements from standardized varLine descriptions (no information loss)' + status: completed isProject: false --- @@ -583,11 +610,63 @@ Run `build:rules` and verify the generated output is byte-identical to pre-refac --- +## Post-PR Fixes — Round 4 (deduplication & consistency) + +Final deduplication pass addressing remaining copy-paste across templates and inconsistent data-driving patterns. + +### Fix 19: Shared variable descriptions helper (`varLine`) + +Variable descriptions like `[SOURCE_KEY]`, `[TARGET_KEY]`, `[DURATION_MS]`, `[EASING_FUNCTION]`, `[DELAY_MS]`, `[EFFECT_NAME]`, `[NAMED_EFFECT_DEFINITION]`, `[FILL_MODE]`, `[UNIQUE_EFFECT_ID]`, `[CUSTOM_EFFECT_CALLBACK]`, `[TRANSITION_DURATION_MS]`, `[TRANSITION_EASING]`, `[CENTERED_TO_TARGET]`, `[HIT_AREA]` were copy-pasted across 4 trigger templates with minor wording variations. + +**Action:** Added `varLine(name, ...args)` helper to `_helpers.mjs`. Each variable has a canonical description with optional trigger-specific suffix/override. All trigger templates now call `varLine('SOURCE_KEY', 'The element that listens for hover.')` instead of repeating the full description. + +### Fix 20: Shared `buildPitfallsBlock` helper + +All 4 trigger templates had an identical pattern: check `trigger.pitfalls?.length`, iterate, call `fragments.get()` with section resolution. Differed only in newline wrapping. + +**Action:** Extracted `buildPitfallsBlock(trigger, fragments)` to `_helpers.mjs`. Returns raw content; templates handle their own spacing. + +### Fix 21: Merge FOUC fragment section pairs + +`fouc.md` had 6 near-identical section pairs (`#code-web`/`#code-web-example`, etc.) that differed only in using `[SOURCE_KEY]` vs `"hero"` for the key value. + +**Action:** Merged each pair using `{{key}}` and `{{classAttr}}` interpolation. Templates now call `fragments.get('fouc', 'code-web', { key: 'hero', classAttr: ' class="hero"' })` for concrete examples and `{ key: '[SOURCE_KEY]', classAttr: '' }` for placeholders. Reduced from 6 sections to 3. + +### Fix 22: Add `varName` to YAML params + +`viewenter-rule.mjs` had a hardcoded `paramVarNames` map (`{ threshold: 'VISIBILITY_THRESHOLD', inset: 'VIEWPORT_INSETS' }`) that lived in the template rather than YAML. + +**Action:** Added `varName` field to each param entry in `triggers.yaml`. Template reads `p.varName || p.name.toUpperCase()`. + +### Fix 23: Add `template` field + schema comments to `triggers.yaml` + +YAML had two implicit schemas (event triggers with `templateFields`/`variableOverrides` vs other triggers with just `params`) with no documentation. + +**Action:** Added `template` field to each trigger entry indicating which `.mjs` template renders it. Added section comments separating event, viewport/scroll, and pointer/chaining trigger groups. + +### Fix 24: Data-drive full-lean.mjs tables from YAML + +The hover/click `triggerType` and `stateAction` comparison tables in `full-lean.mjs` were hardcoded markdown despite the data existing in `triggers.yaml`. + +**Action:** Added `fullLeanBehavior.triggerType` and `fullLeanBehavior.stateAction` maps to hover and click YAML entries with condensed behavior descriptions. Added `buildBehaviorTable()` function that generates Prettier-compatible padded tables from YAML data. + +### Fix 25: Data-drive full-lean.mjs pitfalls from YAML + +`full-lean.mjs` hardcoded all 4 trigger-specific pitfall fragment calls (`overflow-clip/long`, `same-element-viewenter/long`, `hit-area/detailed-hover`, `hit-area/detailed-pointermove`) rather than deriving them from YAML. + +**Action:** Added `fullLeanSection` field to pitfall entries in `triggers.yaml` (the section name used by full-lean.mjs). Added `fullLeanPitfallOrder` array in `effects.yaml` to control ordering. `full-lean.mjs` now iterates this array instead of hardcoding fragment calls. + +### Fix 26: Regenerate output (round 4) + +Regenerated all 7 rule files. 5 minor consistency improvements from standardized `varLine` descriptions (dropped redundant "/vanilla" in viewenter, standardized em-dash separator, added "from the top-level `effects` map" completeness note). Zero information loss. click.md, hover.md, full-lean.md, integration.md, pointermove.md are byte-identical to pre-refactor output. + +--- + ## Future Extension Points - **docs/**: Same `_content/data/` feeds docs templates — add `_content/templates/docs/` later - **README**: Same meta.yaml + triggers.yaml generates README sections - **Validation**: Add `scripts/validate-rules.mjs` that imports actual TS types and cross-checks trigger names, param fields, effect fields against the YAML data - **motion-presets rules**: The build mechanism is scoped to `packages/interact/` but the architecture (YAML + fragments + templates) can be replicated in `packages/motion-presets/` with shared fragments if needed -- **Common variable fragment**: Extract repeated variable descriptions (`[SOURCE_KEY]`, `[TARGET_KEY]`, `[DURATION_MS]`, etc.) into a shared fragment to deduplicate across viewenter/viewprogress/pointermove/event-trigger templates -- **FOUC fragment consolidation**: Reduce fouc.md's 13 sections to ~8 by using `{{param}}` interpolation for placeholder vs example variants +- ~~**Common variable fragment**: Extract repeated variable descriptions~~ — **Done** (Fix 19: `varLine` helper) +- ~~**FOUC fragment consolidation**: Reduce fouc.md's 13 sections~~ — **Done** (Fix 21: merged 6 sections to 3 via `{{key}}` interpolation) diff --git a/packages/interact/_content/data/effects.yaml b/packages/interact/_content/data/effects.yaml index f56523ca..ee4b6427 100644 --- a/packages/interact/_content/data/effects.yaml +++ b/packages/interact/_content/data/effects.yaml @@ -114,6 +114,16 @@ presets: - SkewMouse - BlobMouse +fullLeanPitfallOrder: + - id: overflow-clip + section: long + - id: same-element-viewenter + section: long + - id: hit-area + section: detailed-hover + - id: hit-area + section: detailed-pointermove + rangeNames: cover: 'Full visibility span from first pixel entering to last pixel leaving' entry: 'The phase while the element is entering the viewport' diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml index 742ed833..0213d69a 100644 --- a/packages/interact/_content/data/triggers.yaml +++ b/packages/interact/_content/data/triggers.yaml @@ -1,11 +1,18 @@ +# -------------------------------------------------------------------------- +# Event triggers — rendered by event-trigger-rule.mjs (click.md, hover.md) +# These have the full schema: templateFields, triggerTypeDescriptions, +# stateActionDescriptions, variableOverrides, showMultipleEffectsNote. +# -------------------------------------------------------------------------- triggers: - name: hover + template: event-trigger-rule.mjs a11yAlias: interest a11yNote: "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus." defaultTriggerType: alternate params: [] pitfalls: - id: hit-area + fullLeanSection: detailed-hover showMultipleEffectsNote: true templateFields: - triggerType @@ -27,6 +34,17 @@ triggers: add: 'applies the style state on enter. Leave does NOT remove it.' remove: 'removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`.' clear: 'clears all previously applied style states on enter. Use to reset multiple stacked `''add''` style changes at once (e.g. a "reset" hover area that undoes several accumulated states).' + fullLeanBehavior: + triggerType: + alternate: 'Play on enter, reverse on leave' + repeat: 'Play on enter, stop and rewind on leave' + once: 'Play once on first enter only' + state: 'Play on enter, pause on leave' + stateAction: + toggle: 'Add style state on enter, remove on leave' + add: 'Add style state on enter; leave does NOT remove' + remove: 'Remove style state on enter' + clear: 'Clear/reset all style states on enter' variableOverrides: sourceKeySuffix: 'The element that listens for hover.' targetKeyDesc: "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above)." @@ -39,6 +57,7 @@ triggers: alternateBoolSuffix: '' - name: click + template: event-trigger-rule.mjs a11yAlias: activate a11yNote: "Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space)." defaultTriggerType: alternate @@ -67,6 +86,17 @@ triggers: add: 'applies the style state. Does not remove on subsequent clicks.' remove: 'removes a previously applied style state.' clear: 'clears all previously applied style states. Useful for resetting multiple stacked style states at once.' + fullLeanBehavior: + triggerType: + alternate: 'Alternate play/reverse per click' + repeat: 'Restart per click' + once: 'Play once on first click only' + state: 'Toggle play/pause per click' + stateAction: + toggle: 'Toggle style state per click' + add: 'Add style state on click' + remove: 'Remove style state on click' + clear: 'Clear/reset all style states' variableOverrides: sourceKeySuffix: 'The element that listens for clicks.' targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element." @@ -78,31 +108,45 @@ triggers: offsetEasingSuffix: '' alternateBoolSuffix: " Different from `triggerType: 'alternate'` which alternates per click." + # -------------------------------------------------------------------------- + # Viewport/scroll triggers — each has its own dedicated template. + # Minimal schema: defaultTriggerType, params (with varName), pitfalls. + # -------------------------------------------------------------------------- - name: viewEnter + template: viewenter-rule.mjs defaultTriggerType: once params: - name: threshold + varName: VISIBILITY_THRESHOLD type: number optional: true description: 'Number between 0–1 indicating how much of the source element must be visible to trigger (e.g. `0.3` = 30%).' - name: inset + varName: VIEWPORT_INSETS type: string optional: true description: "String adjusting the viewport detection area (e.g. `'-100px'` extends it, `'50px'` shrinks it)." pitfalls: - id: same-element-viewenter section: short + fullLeanSection: long showMultipleEffectsNote: true - name: viewProgress + template: viewprogress-rule.mjs defaultTriggerType: null params: [] pitfalls: - id: overflow-clip section: short + fullLeanSection: long showMultipleEffectsNote: true + # -------------------------------------------------------------------------- + # Pointer/chaining triggers — dedicated templates or no template. + # -------------------------------------------------------------------------- - name: pointerMove + template: pointermove-rule.mjs defaultTriggerType: null params: - name: hitArea @@ -116,6 +160,7 @@ triggers: pitfalls: - id: hit-area section: pointermove-source + fullLeanSection: detailed-pointermove - name: animationEnd defaultTriggerType: null diff --git a/packages/interact/_content/fragments/fouc.md b/packages/interact/_content/fragments/fouc.md index 550dc281..b20b1f8e 100644 --- a/packages/interact/_content/fragments/fouc.md +++ b/packages/interact/_content/fragments/fouc.md @@ -53,31 +53,15 @@ const css = generate(config); **Web (Custom Elements):** ```html - -
...
-
-``` - -**Web (Custom Elements):** - -```html - -
...
+ + ... ``` **React:** ```tsx - - ... - -``` - -**React:** - -```tsx - + ... ``` @@ -85,13 +69,7 @@ const css = generate(config); **Vanilla:** ```html -
...
-``` - -**Vanilla:** - -```html -
...
+
...
``` ### Rules diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs index e79b8c2e..8c8af3a6 100644 --- a/packages/interact/_content/templates/_helpers.mjs +++ b/packages/interact/_content/templates/_helpers.mjs @@ -5,3 +5,54 @@ export function capitalize(s) { export function when(condition, content) { return condition ? content : ''; } + +/** + * Builds the pitfalls block for a trigger template. + * Iterates trigger.pitfalls from YAML, resolving each fragment section. + * Returns empty string if no pitfalls; raw content otherwise (caller handles spacing). + */ +export function buildPitfallsBlock(trigger, fragments) { + if (!trigger.pitfalls?.length) return ''; + return trigger.pitfalls + .map((p) => fragments.get(`pitfalls/${p.id}`, p.section || trigger.name)) + .join('\n'); +} + +const COMMON_VARS = { + SOURCE_KEY: (suffix) => + `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${suffix}`, + TARGET_KEY: (desc) => `- \`[TARGET_KEY]\` — ${desc}`, + EFFECT_NAME: () => '- `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`.', + NAMED_EFFECT_DEFINITION: () => + '- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.', + KEYFRAMES: () => + '- `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.', + FILL_MODE: (desc) => `- \`[FILL_MODE]\` — ${desc}`, + DURATION_MS: () => '- `[DURATION_MS]` — animation duration in milliseconds.', + EASING_FUNCTION: (desc) => + `- \`[EASING_FUNCTION]\` — ${desc || 'CSS easing string or named easing from `@wix/motion`.'}`, + DELAY_MS: () => '- `[DELAY_MS]` — optional delay before the effect starts, in milliseconds.', + ITERATIONS: (desc) => + `- \`[ITERATIONS]\` — ${desc || 'optional. Number of iterations, or `Infinity` for continuous loops.'}`, + ALTERNATE_BOOL: (suffix) => + `- \`[ALTERNATE_BOOL]\` — optional. \`true\` to alternate direction on every other iteration (within a single playback).${suffix || ''}`, + UNIQUE_EFFECT_ID: () => + '- `[UNIQUE_EFFECT_ID]` — optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects from the top-level `effects` map.', + CUSTOM_EFFECT_CALLBACK: () => + '- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1.', + TRANSITION_DURATION_MS: () => + '- `[TRANSITION_DURATION_MS]` — optional number. Milliseconds for smoothing (interpolating) between progress updates. The animation does not jump to the new progress value instantly; instead it transitions over this duration. Use to add inertia/lag to the effect, making it feel more physical (e.g. `200`–`600`).', + TRANSITION_EASING: () => + '- `[TRANSITION_EASING]` — optional string. CSS easing or named easing from `@wix/motion`. Adds a natural deceleration feel when used with `transitionDuration`.', + CENTERED_TO_TARGET: () => + '- `[CENTERED_TO_TARGET]` — `true` or `false`. See **Centering with `centeredToTarget`** above.', + HIT_AREA: () => + "- `[HIT_AREA]` — `'self'` (track pointer within source element) or `'root'` (track pointer anywhere in viewport).", +}; + +/** Returns a common variable description line. */ +export function varLine(name, ...args) { + const fn = COMMON_VARS[name]; + if (!fn) throw new Error(`Unknown common variable: ${name}`); + return fn(...args); +} diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 55d3625e..3f44b317 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,4 +1,4 @@ -import { capitalize, when } from './_helpers.mjs'; +import { capitalize, when, buildPitfallsBlock, varLine } from './_helpers.mjs'; /** * Renders a trigger-specific rule file (click.md or hover.md). @@ -12,10 +12,8 @@ export function render(data, fragments) { const hasReversed = trigger.templateFields.includes('reversed'); const hasEffectId = trigger.templateFields.includes('effectId'); - const pitfallsBlock = when( - trigger.pitfalls.length > 0, - '\n' + trigger.pitfalls.map((p) => fragments.get(`pitfalls/${p.id}`, name)).join('\n') + '\n', - ); + const pitfallsRaw = buildPitfallsBlock(trigger, fragments); + const pitfallsBlock = pitfallsRaw ? `\n${pitfallsRaw}\n` : ''; const multipleEffectsNote = when( trigger.showMultipleEffectsNote, @@ -200,14 +198,14 @@ function buildVariables(trigger, hasReversed, hasEffectId) { const vo = trigger.variableOverrides; const lines = [ - `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${vo.sourceKeySuffix}`, - `- \`[TARGET_KEY]\` — ${vo.targetKeyDesc}`, + varLine('SOURCE_KEY', vo.sourceKeySuffix), + varLine('TARGET_KEY', vo.targetKeyDesc), `- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of:`, ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`), - `- \`[KEYFRAMES]\` — array of keyframe objects (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase.`, - `- \`[EFFECT_NAME]\` — unique string identifier for a \`keyframeEffect\`.`, - `- \`[NAMED_EFFECT_DEFINITION]\` — object with properties of pre-built effect from \`@wix/motion-presets\`. Refer to motion-presets rules for available presets and their options.`, - `- \`[FILL_MODE]\` — ${vo.fillModeDesc}`, + varLine('KEYFRAMES'), + varLine('EFFECT_NAME'), + varLine('NAMED_EFFECT_DEFINITION'), + varLine('FILL_MODE', vo.fillModeDesc), ]; if (hasReversed) { lines.push( @@ -215,16 +213,14 @@ function buildVariables(trigger, hasReversed, hasEffectId) { ); } lines.push( - `- \`[DURATION_MS]\` — animation duration in milliseconds.`, - `- \`[EASING_FUNCTION]\` — ${vo.easingDesc}`, - `- \`[DELAY_MS]\` — optional delay before the effect starts, in milliseconds.`, - `- \`[ITERATIONS]\` — ${vo.iterationsDesc}`, - `- \`[ALTERNATE_BOOL]\` — optional. \`true\` to alternate direction on every other iteration (within a single playback).${when(vo.alternateBoolSuffix, vo.alternateBoolSuffix)}`, + varLine('DURATION_MS'), + varLine('EASING_FUNCTION', vo.easingDesc), + varLine('DELAY_MS'), + varLine('ITERATIONS', vo.iterationsDesc), + varLine('ALTERNATE_BOOL', vo.alternateBoolSuffix || undefined), ); if (hasEffectId) { - lines.push( - `- \`[UNIQUE_EFFECT_ID]\` — optional. String identifier used by \`animationEnd\` triggers for chaining, and by sequences for referencing effects from the top-level \`effects\` map.`, - ); + lines.push(varLine('UNIQUE_EFFECT_ID')); } return lines.join('\n'); } diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 89ae6987..ff186315 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -1,5 +1,34 @@ import { capitalize } from './_helpers.mjs'; +function buildBehaviorTable(headerLabel, behaviorKey, hover, click) { + const hoverBehavior = hover.fullLeanBehavior[behaviorKey]; + const clickBehavior = click.fullLeanBehavior[behaviorKey]; + const keys = Object.keys(hoverBehavior); + const defaultKey = keys[0]; + + const rows = keys.map((k) => { + const label = k === defaultKey ? `\`'${k}'\` (default)` : `\`'${k}'\``; + return { label, hover: hoverBehavior[k], click: clickBehavior[k] }; + }); + + const col0Width = Math.max(headerLabel.length, ...rows.map((r) => r.label.length)); + const col1Width = Math.max('hover behavior'.length, ...rows.map((r) => r.hover.length)); + const col2Width = Math.max('click behavior'.length, ...rows.map((r) => r.click.length)); + + return [ + `| ${headerLabel.padEnd(col0Width)} | ${'hover behavior'.padEnd(col1Width)} | ${'click behavior'.padEnd(col2Width)} |`, + `| :${'-'.repeat(col0Width - 1)} | :${'-'.repeat(col1Width - 1)} | :${'-'.repeat(col2Width - 1)} |`, + ...rows.map( + (r) => + `| ${r.label.padEnd(col0Width)} | ${r.hover.padEnd(col1Width)} | ${r.click.padEnd(col2Width)} |`, + ), + ].join('\n'); +} + +function buildFullLeanPitfalls(pitfallOrder, fragments) { + return pitfallOrder.map((p) => fragments.get(`pitfalls/${p.id}`, p.section)).join('\n'); +} + /** * Renders full-lean.md — the comprehensive reference for all triggers, effects, and API surface. * @param {{ triggers: object[], effects: object, meta: object }} data @@ -81,10 +110,7 @@ Declarative configuration-driven interaction library. Binds animations to trigge Each item here is CRITICAL — ignoring any of them will break animations. -${fragments.get('pitfalls/overflow-clip', 'long')} -${fragments.get('pitfalls/same-element-viewenter', 'long')} -${fragments.get('pitfalls/hit-area', 'detailed-hover')} -${fragments.get('pitfalls/hit-area', 'detailed-pointermove')} +${buildFullLeanPitfalls(data.effects.fullLeanPitfallOrder, fragments)} ${fragments.get('pitfalls/dont-guess-presets', 'default')} ${fragments.get('pitfalls/reduced-motion', 'default')} ${fragments.get('pitfalls/perspective', 'default')} @@ -226,21 +252,11 @@ For \`TimeEffect\` (keyframe/named/custom effects), set \`triggerType\` on the e **\`triggerType\`** — on \`TimeEffect\`: -| Type | hover behavior | click behavior | -| :---------------------- | :-------------------------------------- | :------------------------------- | -| \`'alternate'\` (default) | Play on enter, reverse on leave | Alternate play/reverse per click | -| \`'repeat'\` | Play on enter, stop and rewind on leave | Restart per click | -| \`'once'\` | Play once on first enter only | Play once on first click only | -| \`'state'\` | Play on enter, pause on leave | Toggle play/pause per click | +${buildBehaviorTable('Type', 'triggerType', hover, click)} **\`stateAction\`** — on \`StateEffect\`: -| Action | hover behavior | click behavior | -| :------------------- | :---------------------------------------------- | :--------------------------- | -| \`'toggle'\` (default) | Add style state on enter, remove on leave | Toggle style state per click | -| \`'add'\` | Add style state on enter; leave does NOT remove | Add style state on click | -| \`'remove'\` | Remove style state on enter | Remove style state on click | -| \`'clear'\` | Clear/reset all style states on enter | Clear/reset all style states | +${buildBehaviorTable('Action', 'stateAction', hover, click)} ### viewEnter @@ -496,11 +512,11 @@ ${fragments.get('fouc', 'code-inject')} ### Step 2: Mark elements -${fragments.get('fouc', 'code-web-example')} +${fragments.get('fouc', 'code-web', { key: 'hero', classAttr: ' class="hero"' })} -${fragments.get('fouc', 'code-react-example')} +${fragments.get('fouc', 'code-react', { key: 'hero', classAttr: ' className="hero"' })} -${fragments.get('fouc', 'code-vanilla-example')} +${fragments.get('fouc', 'code-vanilla', { key: 'hero', classAttr: ' class="hero"' })} ${fragments.get('fouc', 'rules-detailed')} diff --git a/packages/interact/_content/templates/pointermove-rule.mjs b/packages/interact/_content/templates/pointermove-rule.mjs index d08cdc06..6f7afd6d 100644 --- a/packages/interact/_content/templates/pointermove-rule.mjs +++ b/packages/interact/_content/templates/pointermove-rule.mjs @@ -1,4 +1,4 @@ -import { when } from './_helpers.mjs'; +import { buildPitfallsBlock, varLine } from './_helpers.mjs'; /** * Renders pointermove.md — rules for pointer-driven interactions with 2D mouse tracking. @@ -8,12 +8,7 @@ import { when } from './_helpers.mjs'; export function render(data, fragments) { const { trigger } = data; - const pitfallsBlock = when( - trigger.pitfalls?.length > 0, - trigger.pitfalls - .map((p) => fragments.get(`pitfalls/${p.id}`, p.section || trigger.name)) - .join('\n'), - ); + const pitfallsBlock = buildPitfallsBlock(trigger, fragments); // no extra newline wrapping — handled by template layout const paramsTypeFields = trigger.params .map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type};`) @@ -146,14 +141,14 @@ Use pre-built mouse presets from \`${data.meta.presetsPackage}\` that handle 2D ### Variables -- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). The element that tracks pointer movement. -- \`[TARGET_KEY]\` — identifier matching the element's key on the element to animate (can be same as source or different). -- \`[HIT_AREA]\` — \`'self'\` (track pointer within source element) or \`'root'\` (track pointer anywhere in viewport). +${varLine('SOURCE_KEY', 'The element that tracks pointer movement.')} +${varLine('TARGET_KEY', "identifier matching the element's key on the element to animate (can be same as source or different).")} +${varLine('HIT_AREA')} - \`[NAMED_EFFECT_TYPE]\` — a registered effect name, or a preset from \`${data.meta.presetsPackage}\` \`mouse\` library. - \`[EFFECT_PROPERTIES]\` — preset-specific options. Refer to motion-presets rules for each preset's available options and their value types. Do NOT guess preset option names or types; omit unknown options and rely on defaults. -- \`[CENTERED_TO_TARGET]\` — \`true\` or \`false\`. See **Centering with \`centeredToTarget\`** above. -- \`[TRANSITION_DURATION_MS]\` — optional number. Milliseconds for smoothing (interpolating) between progress updates. The animation does not jump to the new progress value instantly; instead it transitions over this duration. Use to add inertia/lag to the effect, making it feel more physical (e.g. \`200\`–\`600\`). -- \`[TRANSITION_EASING]\` — optional string. CSS easing or named easing from \`@wix/motion\`. Adds a natural deceleration feel when used with \`transitionDuration\`. +${varLine('CENTERED_TO_TARGET')} +${varLine('TRANSITION_DURATION_MS')} +${varLine('TRANSITION_EASING')} --- diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index a85a7d02..08e0a607 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -1,4 +1,4 @@ -import { when } from './_helpers.mjs'; +import { when, buildPitfallsBlock, varLine } from './_helpers.mjs'; /** * Renders viewenter.md — rules for viewport-entry triggered animations. @@ -8,21 +8,14 @@ import { when } from './_helpers.mjs'; export function render(data, fragments) { const { trigger } = data; - const pitfallsBlock = when( - trigger.pitfalls?.length > 0, - '\n' + - trigger.pitfalls - .map((p) => fragments.get(`pitfalls/${p.id}`, p.section || trigger.name)) - .join('\n') + - '\n', - ); + const pitfallsRaw = buildPitfallsBlock(trigger, fragments); + const pitfallsBlock = pitfallsRaw ? `\n${pitfallsRaw}\n` : ''; - const paramVarNames = { threshold: 'VISIBILITY_THRESHOLD', inset: 'VIEWPORT_INSETS' }; const paramDescriptions = trigger.params .map((p) => { - const varName = paramVarNames[p.name] || p.name.toUpperCase(); + const vn = p.varName || p.name.toUpperCase(); const optionalPrefix = p.optional ? 'optional. ' : ''; - return `- \`[${varName}]\` — ${optionalPrefix}${p.description}`; + return `- \`[${vn}]\` — ${optionalPrefix}${p.description}`; }) .join('\n'); @@ -55,11 +48,11 @@ ${fragments.get('fouc', 'code-inject')} ### Step 2: Mark elements with \`initial\` -${fragments.get('fouc', 'code-web')} +${fragments.get('fouc', 'code-web', { key: '[SOURCE_KEY]', classAttr: '' })} -${fragments.get('fouc', 'code-react')} +${fragments.get('fouc', 'code-react', { key: '[SOURCE_KEY]', classAttr: '' })} -${fragments.get('fouc', 'code-vanilla')} +${fragments.get('fouc', 'code-vanilla', { key: '[SOURCE_KEY]', classAttr: '' })} ${fragments.get('fouc', 'rules-viewenter')} @@ -106,8 +99,8 @@ Use \`keyframeEffect\` or \`namedEffect\` when the viewEnter should play an anim ### Variables -- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web/vanilla, \`interactKey\` for React). The **source element** is observed for viewport intersection. This is the element the IntersectionObserver watches. -- \`[TARGET_KEY]\` — identifier matching the element's key on the element that animates. +${varLine('SOURCE_KEY', 'The **source element** is observed for viewport intersection. This is the element the IntersectionObserver watches.')} +${varLine('TARGET_KEY', "identifier matching the element's key on the element that animates.")} - \`[TARGET_SELECTOR]\` - optional. Selector for the child element to select inside the root element. For \`triggerType\` of \`'alternate'\`/\`'repeat'\`/\`'state'\` MUST either use a separate \`[TARGET_KEY]\` from \`[SOURCE_KEY]\` or \`selector\` for selecting a child element as target. - \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of: - \`'once'\` (default) — plays once when the source element first enters the viewport and never again. Source and target may be the same element. @@ -115,16 +108,16 @@ Use \`keyframeEffect\` or \`namedEffect\` when the viewEnter should play an anim - \`'alternate'\` — plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target. - \`'state'\` — resumes on enter, pauses on leave. Useful for continuous loops (\`iterations: Infinity\`). Use separate source and target. ${paramDescriptions} -- \`[KEYFRAMES]\` — array of keyframe objects (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase. -- \`[EFFECT_NAME]\` — unique string identifier for a \`keyframeEffect\`. -- \`[NAMED_EFFECT_DEFINITION]\` — object with properties of pre-built effect from \`@wix/motion-presets\`. Refer to motion-presets rules for available presets and their options. -- \`[FILL_MODE]\` — \`'both'\` for \`triggerType: 'alternate'\`, \`'repeat'\`, or \`'state'\`. For \`triggerType: 'once'\`: use \`'backwards'\` when the animation's final keyframe has no additional effect (over element's base style); use \`'both'\` otherwise. -- \`[DURATION_MS]\` — animation duration in milliseconds. -- \`[EASING_FUNCTION]\` — CSS easing string or named easing from \`@wix/motion\`. -- \`[DELAY_MS]\` — optional delay before the effect starts, in milliseconds. -- \`[ITERATIONS]\` — optional. Number of iterations, or \`Infinity\` for continuous loops. Primarily useful with \`triggerType: 'state'\`. -- \`[ALTERNATE_BOOL]\` — optional. \`true\` to alternate direction on every other iteration (within a single playback). -- \`[UNIQUE_EFFECT_ID]\` — optional. String identifier used by \`animationEnd\` triggers for chaining, and by sequences for referencing effects. +${varLine('KEYFRAMES')} +${varLine('EFFECT_NAME')} +${varLine('NAMED_EFFECT_DEFINITION')} +${varLine('FILL_MODE', "`'both'` for `triggerType: 'alternate'`, `'repeat'`, or `'state'`. For `triggerType: 'once'`: use `'backwards'` when the animation's final keyframe has no additional effect (over element's base style); use `'both'` otherwise.")} +${varLine('DURATION_MS')} +${varLine('EASING_FUNCTION')} +${varLine('DELAY_MS')} +${varLine('ITERATIONS', "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.")} +${varLine('ALTERNATE_BOOL')} +${varLine('UNIQUE_EFFECT_ID')} --- diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index 35db0149..d6ea2000 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -1,4 +1,4 @@ -import { when } from './_helpers.mjs'; +import { buildPitfallsBlock, varLine } from './_helpers.mjs'; /** * Renders viewprogress.md — rules for scroll-driven animations using ViewTimeline. @@ -8,14 +8,8 @@ import { when } from './_helpers.mjs'; export function render(data, fragments) { const { trigger } = data; - const pitfallsBlock = when( - trigger.pitfalls?.length > 0, - '\n' + - trigger.pitfalls - .map((p) => fragments.get(`pitfalls/${p.id}`, p.section || trigger.name)) - .join('\n') + - '\n', - ); + const pitfallsRaw = buildPitfallsBlock(trigger, fragments); + const pitfallsBlock = pitfallsRaw ? `\n${pitfallsRaw}\n` : ''; const rangeList = Object.entries(data.effects.rangeNames) .map(([name, desc]) => { @@ -72,16 +66,16 @@ ${pitfallsBlock} ### Variables -- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). The element whose scroll position drives the animation. -- \`[TARGET_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React) on the element to animate (can be same as source or different). +${varLine('SOURCE_KEY', 'The element whose scroll position drives the animation.')} +${varLine('TARGET_KEY', "identifier matching the element's key (`data-interact-key` for web, `interactKey` for React) on the element to animate (can be same as source or different).")} - \`[NAMED_EFFECT_DEFINITION]\` — object with properties of pre-built effect from \`@wix/motion-presets\`. **CRITICAL:** Scroll presets (\`*Scroll\`) MUST include \`range: 'in' | 'out' | 'continuous'\` in their options. \`'in'\` ends at the idle state, \`'out'\` starts from the idle state, \`'continuous'\` passes through it. -- \`[EFFECT_NAME]\` — unique name for custom keyframe effect. +${varLine('EFFECT_NAME')} - \`[EFFECT_KEYFRAMES]\` — array of keyframe objects defining CSS property values (e.g. \`[{ opacity: 0 }, { opacity: 1 }]\`). Property names in camelCase. - \`[RANGE_NAME]\` — scroll range name: ${rangeList} - \`[START_PERCENTAGE]\` — 0–100, starting point within the named range. - \`[END_PERCENTAGE]\` — 0–100, end point within the named range. -- \`[EASING_FUNCTION]\` - CSS easing string or named easing from \`@wix/motion\`. Typically \`'linear'\` for scrolling effects. +${varLine('EASING_FUNCTION', "CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects.")} - \`[UNIQUE_EFFECT_ID]\` — optional identifier for referencing the effect externally. --- @@ -114,9 +108,9 @@ ${rangeList} ### Variables - \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. -- \`[CUSTOM_EFFECT_CALLBACK]\` — function with signature \`(element: HTMLElement, progress: number) => void\`. Called on each animation frame with \`progress\` from 0 to 1. +${varLine('CUSTOM_EFFECT_CALLBACK')} - \`[RANGE_NAME]\` / \`[START_PERCENTAGE]\` / \`[END_PERCENTAGE]\` — same as Rule 1. -- \`[EASING_FUNCTION]\` — CSS easing string or named easing from \`@wix/motion\`. Typically \`'linear'\` for scrolling effects. +${varLine('EASING_FUNCTION', "CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects.")} - \`[UNIQUE_EFFECT_ID]\` — optional identifier for referencing the effect externally. --- diff --git a/packages/interact/rules/viewenter.md b/packages/interact/rules/viewenter.md index a8a73f82..0894102c 100644 --- a/packages/interact/rules/viewenter.md +++ b/packages/interact/rules/viewenter.md @@ -134,7 +134,7 @@ Use `keyframeEffect` or `namedEffect` when the viewEnter should play an animatio ### Variables -- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web/vanilla, `interactKey` for React). The **source element** is observed for viewport intersection. This is the element the IntersectionObserver watches. +- `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). The **source element** is observed for viewport intersection. This is the element the IntersectionObserver watches. - `[TARGET_KEY]` — identifier matching the element's key on the element that animates. - `[TARGET_SELECTOR]` - optional. Selector for the child element to select inside the root element. For `triggerType` of `'alternate'`/`'repeat'`/`'state'` MUST either use a separate `[TARGET_KEY]` from `[SOURCE_KEY]` or `selector` for selecting a child element as target. - `[TRIGGER_TYPE]` — `triggerType` on the effect. One of: @@ -153,7 +153,7 @@ Use `keyframeEffect` or `namedEffect` when the viewEnter should play an animatio - `[DELAY_MS]` — optional delay before the effect starts, in milliseconds. - `[ITERATIONS]` — optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`. - `[ALTERNATE_BOOL]` — optional. `true` to alternate direction on every other iteration (within a single playback). -- `[UNIQUE_EFFECT_ID]` — optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects. +- `[UNIQUE_EFFECT_ID]` — optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects from the top-level `effects` map. --- diff --git a/packages/interact/rules/viewprogress.md b/packages/interact/rules/viewprogress.md index cf06fb1b..faaa33f7 100644 --- a/packages/interact/rules/viewprogress.md +++ b/packages/interact/rules/viewprogress.md @@ -50,7 +50,7 @@ These rules help generate scroll-driven interactions using `@wix/interact`. View - `[SOURCE_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). The element whose scroll position drives the animation. - `[TARGET_KEY]` — identifier matching the element's key (`data-interact-key` for web, `interactKey` for React) on the element to animate (can be same as source or different). - `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. **CRITICAL:** Scroll presets (`*Scroll`) MUST include `range: 'in' | 'out' | 'continuous'` in their options. `'in'` ends at the idle state, `'out'` starts from the idle state, `'continuous'` passes through it. -- `[EFFECT_NAME]` — unique name for custom keyframe effect. +- `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`. - `[EFFECT_KEYFRAMES]` — array of keyframe objects defining CSS property values (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase. - `[RANGE_NAME]` — scroll range name: - `'cover'` — full visibility span from first pixel entering to last pixel leaving. @@ -61,7 +61,7 @@ These rules help generate scroll-driven interactions using `@wix/interact`. View - `'exit-crossing'` — from the element's trailing edge reaching the start to its trailing edge leaving. - `[START_PERCENTAGE]` — 0–100, starting point within the named range. - `[END_PERCENTAGE]` — 0–100, end point within the named range. -- `[EASING_FUNCTION]` - CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects. +- `[EASING_FUNCTION]` — CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects. - `[UNIQUE_EFFECT_ID]` — optional identifier for referencing the effect externally. --- @@ -94,7 +94,7 @@ These rules help generate scroll-driven interactions using `@wix/interact`. View ### Variables - `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1. -- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with `progress` from 0 to 1. +- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1. - `[RANGE_NAME]` / `[START_PERCENTAGE]` / `[END_PERCENTAGE]` — same as Rule 1. - `[EASING_FUNCTION]` — CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects. - `[UNIQUE_EFFECT_ID]` — optional identifier for referencing the effect externally. From 375530d12fbaa5faf137f5adbd396ca5b65f9791 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Tue, 5 May 2026 17:54:02 +0300 Subject: [PATCH 12/27] PR fixes --- .../rules_build_pipeline_ffe2a9f3.plan.md | 75 +++++++++++++ packages/interact/_content/data/effects.yaml | 2 +- packages/interact/_content/data/triggers.yaml | 28 +---- .../interact/_content/templates/_helpers.mjs | 105 ++++++++++++------ .../interact/_content/templates/full-lean.mjs | 41 ++++++- .../_content/templates/integration.mjs | 22 +--- .../_content/templates/viewenter-rule.mjs | 2 +- .../_content/templates/viewprogress-rule.mjs | 4 +- packages/interact/rules/full-lean.md | 16 +-- packages/interact/rules/integration.md | 4 +- packages/interact/scripts/build-rules.mjs | 6 + 11 files changed, 205 insertions(+), 100 deletions(-) diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md index f921093c..3e2e62bb 100644 --- a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md +++ b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md @@ -119,6 +119,27 @@ todos: - id: refactor-regenerate-2 content: 'Regenerate rules/*.md, verify output — only 5 minor consistency improvements from standardized varLine descriptions (no information loss)' status: completed + - id: review-unused-import + content: 'Remove unused `when` import from viewenter-rule.mjs' + status: completed + - id: review-fouc-fragments + content: 'Use FOUC fragments in integration.mjs instead of hardcoded code blocks — fixes id="hero" → class="hero" inconsistency and standardizes heading text' + status: completed + - id: review-contain-note + content: 'Move hardcoded `contain` sticky-container note from viewprogress-rule.mjs into effects.yaml rangeNames data' + status: completed + - id: review-decouple-full-lean + content: 'Move fullLeanBehavior data from triggers.yaml into full-lean.mjs local constant; remove dead fullLeanSection fields from trigger pitfall entries (already in effects.yaml fullLeanPitfallOrder)' + status: completed + - id: review-varline-api + content: "Standardize varLine API in _helpers.mjs — declarative COMMON_VARS with explicit 'suffix' / 'override' modes replacing ad-hoc function signatures" + status: completed + - id: review-fragment-warning + content: 'Add unreplaced {{...}} placeholder warning to Fragments.get() in build-rules.mjs' + status: completed + - id: review-regenerate + content: 'Regenerate rules/*.md — full-lean.md range table widened for contain note, integration.md FOUC section uses fragments. All other 5 files byte-identical.' + status: completed isProject: false --- @@ -662,6 +683,60 @@ Regenerated all 7 rule files. 5 minor consistency improvements from standardized --- +## Post-PR Fixes — Round 5 (code review refinements) + +Fixes from a final review pass, addressing data/template coupling, API inconsistencies, dead code, and missed deduplication. + +### Fix 27: Remove unused `when` import + +`viewenter-rule.mjs` imported `when` from `_helpers.mjs` but never called it. + +**Action:** Removed unused import. + +### Fix 28: Use FOUC fragments in integration.mjs + +The FOUC code examples in `integration.mjs` (Web/React/Vanilla element markup) were hardcoded instead of using the parameterized `fouc.md` fragments that `full-lean.mjs` and `viewenter-rule.mjs` already use. This caused two inconsistencies: `
` (should be `class="hero"` like all other examples), and the heading said "**Web:**" instead of "**Web (Custom Elements):**". + +**Action:** Replaced hardcoded code blocks with `fragments.get('fouc', 'code-web', { key: 'hero', classAttr: ' class="hero"' })` etc., matching `full-lean.mjs`. + +### Fix 29: Move `contain` range note into YAML data + +`viewprogress-rule.mjs` hardcoded a special case for the `contain` range name (`'. Typically used with a position: sticky container'`). This note is useful context that belongs in the data layer. + +**Action:** Appended the sticky-container note to the `contain` description in `effects.yaml`. Removed the hardcoded special case from `viewprogress-rule.mjs`. The `full-lean.md` range table also now includes this note (wider column, auto-adjusted). + +### Fix 30: Decouple full-lean.mjs from triggers.yaml + +`fullLeanBehavior` data on hover/click trigger entries and `fullLeanSection` on pitfall entries existed solely to serve `full-lean.mjs`. This coupled the data layer to one specific template's rendering needs — adding a new output template would require adding more `fullXxx` fields to trigger entries. + +**Action:** + +- Moved `fullLeanBehavior` (hover + click triggerType/stateAction condensed descriptions) from `triggers.yaml` into a `FULL_LEAN_BEHAVIOR` constant in `full-lean.mjs`. Updated `buildBehaviorTable()` to read from the local constant instead of trigger data. +- Removed dead `fullLeanSection` field from all pitfall entries in `triggers.yaml` — this data was already present in `effects.yaml`'s `fullLeanPitfallOrder` array, which is what `full-lean.mjs` actually reads. + +### Fix 31: Standardize `varLine` API + +The `COMMON_VARS` map in `_helpers.mjs` used ad-hoc function signatures — `SOURCE_KEY(suffix)` appended text, `TARGET_KEY(desc)` replaced text, `EASING_FUNCTION(desc)` had a fallback default, `ALTERNATE_BOOL(suffix)` appended without space. This made the API hard to use without checking the source. + +**Action:** Replaced function-based `COMMON_VARS` with a declarative object where each variable has a `base` description and an optional `mode` (`'suffix'` or `'override'`). The `varLine(name, extra)` function applies the mode consistently: suffix mode always joins with a space, override mode fully replaces. Variables with no mode ignore the extra argument and always return the base. Updated `alternateBoolSuffix` in `triggers.yaml` to remove the leading space (suffix mode now adds one). + +### Fix 32: Add unreplaced placeholder warning + +The `Fragments.get()` method in `build-rules.mjs` silently ignored unmatched `{{...}}` placeholders after interpolation. A typo in a param name would produce output with raw `{{placeholder}}` text. + +**Action:** Added a post-interpolation check that warns about any remaining `{{...}}` patterns after parameter substitution. + +### Fix 33: Regenerate output (round 5) + +Regenerated all 7 rule files. Two files changed: + +- **full-lean.md**: Range table widened to accommodate `contain` note (all other content identical). +- **integration.md**: FOUC code examples now use fragments — `id="hero"` → `class="hero"`, "**Web:**" → "**Web (Custom Elements):**". + +All other 5 files (click.md, hover.md, viewenter.md, viewprogress.md, pointermove.md) are byte-identical. + +--- + ## Future Extension Points - **docs/**: Same `_content/data/` feeds docs templates — add `_content/templates/docs/` later diff --git a/packages/interact/_content/data/effects.yaml b/packages/interact/_content/data/effects.yaml index ee4b6427..56c99f73 100644 --- a/packages/interact/_content/data/effects.yaml +++ b/packages/interact/_content/data/effects.yaml @@ -128,6 +128,6 @@ rangeNames: cover: 'Full visibility span from first pixel entering to last pixel leaving' entry: 'The phase while the element is entering the viewport' exit: 'The phase while the element is exiting the viewport' - contain: 'While the element is fully contained in the viewport' + contain: 'While the element is fully contained in the viewport. Typically used with a `position: sticky` container' entry-crossing: "From the element's leading edge entering to its leading edge reaching the opposite side" exit-crossing: "From the element's trailing edge reaching the start to its trailing edge leaving" diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml index 0213d69a..045c9ce5 100644 --- a/packages/interact/_content/data/triggers.yaml +++ b/packages/interact/_content/data/triggers.yaml @@ -12,7 +12,6 @@ triggers: params: [] pitfalls: - id: hit-area - fullLeanSection: detailed-hover showMultipleEffectsNote: true templateFields: - triggerType @@ -34,17 +33,6 @@ triggers: add: 'applies the style state on enter. Leave does NOT remove it.' remove: 'removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`.' clear: 'clears all previously applied style states on enter. Use to reset multiple stacked `''add''` style changes at once (e.g. a "reset" hover area that undoes several accumulated states).' - fullLeanBehavior: - triggerType: - alternate: 'Play on enter, reverse on leave' - repeat: 'Play on enter, stop and rewind on leave' - once: 'Play once on first enter only' - state: 'Play on enter, pause on leave' - stateAction: - toggle: 'Add style state on enter, remove on leave' - add: 'Add style state on enter; leave does NOT remove' - remove: 'Remove style state on enter' - clear: 'Clear/reset all style states on enter' variableOverrides: sourceKeySuffix: 'The element that listens for hover.' targetKeyDesc: "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above)." @@ -86,17 +74,6 @@ triggers: add: 'applies the style state. Does not remove on subsequent clicks.' remove: 'removes a previously applied style state.' clear: 'clears all previously applied style states. Useful for resetting multiple stacked style states at once.' - fullLeanBehavior: - triggerType: - alternate: 'Alternate play/reverse per click' - repeat: 'Restart per click' - once: 'Play once on first click only' - state: 'Toggle play/pause per click' - stateAction: - toggle: 'Toggle style state per click' - add: 'Add style state on click' - remove: 'Remove style state on click' - clear: 'Clear/reset all style states' variableOverrides: sourceKeySuffix: 'The element that listens for clicks.' targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element." @@ -106,7 +83,7 @@ triggers: fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`." customEffectExamples: ', randomized behavior' offsetEasingSuffix: '' - alternateBoolSuffix: " Different from `triggerType: 'alternate'` which alternates per click." + alternateBoolSuffix: "Different from `triggerType: 'alternate'` which alternates per click." # -------------------------------------------------------------------------- # Viewport/scroll triggers — each has its own dedicated template. @@ -129,7 +106,6 @@ triggers: pitfalls: - id: same-element-viewenter section: short - fullLeanSection: long showMultipleEffectsNote: true - name: viewProgress @@ -139,7 +115,6 @@ triggers: pitfalls: - id: overflow-clip section: short - fullLeanSection: long showMultipleEffectsNote: true # -------------------------------------------------------------------------- @@ -160,7 +135,6 @@ triggers: pitfalls: - id: hit-area section: pointermove-source - fullLeanSection: detailed-pointermove - name: animationEnd defaultTriggerType: null diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs index 8c8af3a6..34c44973 100644 --- a/packages/interact/_content/templates/_helpers.mjs +++ b/packages/interact/_content/templates/_helpers.mjs @@ -18,41 +18,78 @@ export function buildPitfallsBlock(trigger, fragments) { .join('\n'); } +/** + * Common variable descriptions. + * - mode 'suffix': arg is appended after base (separated by space) + * - mode 'override': arg replaces base entirely + * - no mode: variable takes no argument + */ const COMMON_VARS = { - SOURCE_KEY: (suffix) => - `- \`[SOURCE_KEY]\` — identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React). ${suffix}`, - TARGET_KEY: (desc) => `- \`[TARGET_KEY]\` — ${desc}`, - EFFECT_NAME: () => '- `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`.', - NAMED_EFFECT_DEFINITION: () => - '- `[NAMED_EFFECT_DEFINITION]` — object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.', - KEYFRAMES: () => - '- `[KEYFRAMES]` — array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.', - FILL_MODE: (desc) => `- \`[FILL_MODE]\` — ${desc}`, - DURATION_MS: () => '- `[DURATION_MS]` — animation duration in milliseconds.', - EASING_FUNCTION: (desc) => - `- \`[EASING_FUNCTION]\` — ${desc || 'CSS easing string or named easing from `@wix/motion`.'}`, - DELAY_MS: () => '- `[DELAY_MS]` — optional delay before the effect starts, in milliseconds.', - ITERATIONS: (desc) => - `- \`[ITERATIONS]\` — ${desc || 'optional. Number of iterations, or `Infinity` for continuous loops.'}`, - ALTERNATE_BOOL: (suffix) => - `- \`[ALTERNATE_BOOL]\` — optional. \`true\` to alternate direction on every other iteration (within a single playback).${suffix || ''}`, - UNIQUE_EFFECT_ID: () => - '- `[UNIQUE_EFFECT_ID]` — optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects from the top-level `effects` map.', - CUSTOM_EFFECT_CALLBACK: () => - '- `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1.', - TRANSITION_DURATION_MS: () => - '- `[TRANSITION_DURATION_MS]` — optional number. Milliseconds for smoothing (interpolating) between progress updates. The animation does not jump to the new progress value instantly; instead it transitions over this duration. Use to add inertia/lag to the effect, making it feel more physical (e.g. `200`–`600`).', - TRANSITION_EASING: () => - '- `[TRANSITION_EASING]` — optional string. CSS easing or named easing from `@wix/motion`. Adds a natural deceleration feel when used with `transitionDuration`.', - CENTERED_TO_TARGET: () => - '- `[CENTERED_TO_TARGET]` — `true` or `false`. See **Centering with `centeredToTarget`** above.', - HIT_AREA: () => - "- `[HIT_AREA]` — `'self'` (track pointer within source element) or `'root'` (track pointer anywhere in viewport).", + SOURCE_KEY: { + base: "identifier matching the element's key (`data-interact-key` for web, `interactKey` for React).", + mode: 'suffix', + }, + TARGET_KEY: { + base: "identifier matching the element's key on the element that animates.", + mode: 'override', + }, + EFFECT_NAME: { base: 'unique string identifier for a `keyframeEffect`.' }, + NAMED_EFFECT_DEFINITION: { + base: 'object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.', + }, + KEYFRAMES: { + base: 'array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.', + }, + FILL_MODE: { + base: "fill mode for the animation (`'none'`, `'forwards'`, `'backwards'`, `'both'`).", + mode: 'override', + }, + DURATION_MS: { base: 'animation duration in milliseconds.' }, + EASING_FUNCTION: { + base: 'CSS easing string or named easing from `@wix/motion`.', + mode: 'override', + }, + DELAY_MS: { base: 'optional delay before the effect starts, in milliseconds.' }, + ITERATIONS: { + base: 'optional. Number of iterations, or `Infinity` for continuous loops.', + mode: 'override', + }, + ALTERNATE_BOOL: { + base: 'optional. `true` to alternate direction on every other iteration (within a single playback).', + mode: 'suffix', + }, + UNIQUE_EFFECT_ID: { + base: 'optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects from the top-level `effects` map.', + }, + CUSTOM_EFFECT_CALLBACK: { + base: 'function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1.', + }, + TRANSITION_DURATION_MS: { + base: 'optional number. Milliseconds for smoothing (interpolating) between progress updates. The animation does not jump to the new progress value instantly; instead it transitions over this duration. Use to add inertia/lag to the effect, making it feel more physical (e.g. `200`–`600`).', + }, + TRANSITION_EASING: { + base: 'optional string. CSS easing or named easing from `@wix/motion`. Adds a natural deceleration feel when used with `transitionDuration`.', + }, + CENTERED_TO_TARGET: { + base: '`true` or `false`. See **Centering with `centeredToTarget`** above.', + }, + HIT_AREA: { + base: "`'self'` (track pointer within source element) or `'root'` (track pointer anywhere in viewport).", + }, }; -/** Returns a common variable description line. */ -export function varLine(name, ...args) { - const fn = COMMON_VARS[name]; - if (!fn) throw new Error(`Unknown common variable: ${name}`); - return fn(...args); +/** + * Returns a formatted variable description line. + * For 'suffix' mode vars, `extra` is appended after the base description. + * For 'override' mode vars, `extra` replaces the base description. + * For vars with no mode, `extra` is ignored. + */ +export function varLine(name, extra) { + const v = COMMON_VARS[name]; + if (!v) throw new Error(`Unknown common variable: ${name}`); + let desc = v.base; + if (extra !== undefined && v.mode) { + desc = v.mode === 'suffix' ? `${v.base} ${extra}` : extra; + } + return `- \`[${name}]\` — ${desc}`; } diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index ff186315..7facc752 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -1,8 +1,39 @@ import { capitalize } from './_helpers.mjs'; -function buildBehaviorTable(headerLabel, behaviorKey, hover, click) { - const hoverBehavior = hover.fullLeanBehavior[behaviorKey]; - const clickBehavior = click.fullLeanBehavior[behaviorKey]; +const FULL_LEAN_BEHAVIOR = { + hover: { + triggerType: { + alternate: 'Play on enter, reverse on leave', + repeat: 'Play on enter, stop and rewind on leave', + once: 'Play once on first enter only', + state: 'Play on enter, pause on leave', + }, + stateAction: { + toggle: 'Add style state on enter, remove on leave', + add: 'Add style state on enter; leave does NOT remove', + remove: 'Remove style state on enter', + clear: 'Clear/reset all style states on enter', + }, + }, + click: { + triggerType: { + alternate: 'Alternate play/reverse per click', + repeat: 'Restart per click', + once: 'Play once on first click only', + state: 'Toggle play/pause per click', + }, + stateAction: { + toggle: 'Toggle style state per click', + add: 'Add style state on click', + remove: 'Remove style state on click', + clear: 'Clear/reset all style states', + }, + }, +}; + +function buildBehaviorTable(headerLabel, behaviorKey) { + const hoverBehavior = FULL_LEAN_BEHAVIOR.hover[behaviorKey]; + const clickBehavior = FULL_LEAN_BEHAVIOR.click[behaviorKey]; const keys = Object.keys(hoverBehavior); const defaultKey = keys[0]; @@ -252,11 +283,11 @@ For \`TimeEffect\` (keyframe/named/custom effects), set \`triggerType\` on the e **\`triggerType\`** — on \`TimeEffect\`: -${buildBehaviorTable('Type', 'triggerType', hover, click)} +${buildBehaviorTable('Type', 'triggerType')} **\`stateAction\`** — on \`StateEffect\`: -${buildBehaviorTable('Action', 'stateAction', hover, click)} +${buildBehaviorTable('Action', 'stateAction')} ### viewEnter diff --git a/packages/interact/_content/templates/integration.mjs b/packages/interact/_content/templates/integration.mjs index 82142623..043f40b6 100644 --- a/packages/interact/_content/templates/integration.mjs +++ b/packages/interact/_content/templates/integration.mjs @@ -202,27 +202,11 @@ const css = generate(config); ${fragments.get('fouc', 'code-inject')} -**Web:** +${fragments.get('fouc', 'code-web', { key: 'hero', classAttr: ' class="hero"' })} -\`\`\`html - -
...
-
-\`\`\` +${fragments.get('fouc', 'code-react', { key: 'hero', classAttr: ' className="hero"' })} -**React:** - -\`\`\`tsx - - ... - -\`\`\` - -**Vanilla:** - -\`\`\`html -
...
-\`\`\` +${fragments.get('fouc', 'code-vanilla', { key: 'hero', classAttr: ' class="hero"' })} --- diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index 08e0a607..efb0b35c 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -1,4 +1,4 @@ -import { when, buildPitfallsBlock, varLine } from './_helpers.mjs'; +import { buildPitfallsBlock, varLine } from './_helpers.mjs'; /** * Renders viewenter.md — rules for viewport-entry triggered animations. diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index d6ea2000..e20e0033 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -13,9 +13,7 @@ export function render(data, fragments) { const rangeList = Object.entries(data.effects.rangeNames) .map(([name, desc]) => { - const extra = - name === 'contain' ? '. Typically used with a `position: sticky` container' : ''; - return ` - \`'${name}'\` — ${desc.charAt(0).toLowerCase()}${desc.slice(1)}${extra}.`; + return ` - \`'${name}'\` — ${desc.charAt(0).toLowerCase()}${desc.slice(1)}.`; }) .join('\n'); diff --git a/packages/interact/rules/full-lean.md b/packages/interact/rules/full-lean.md index fc08a4c3..60a3ec53 100644 --- a/packages/interact/rules/full-lean.md +++ b/packages/interact/rules/full-lean.md @@ -397,14 +397,14 @@ Used with `viewProgress` and `pointerMove` triggers. } ``` -| Range name | Meaning | -| :--------------- | :-------------------------------------------------------------------------------------- | -| `cover` | Full visibility span from first pixel entering to last pixel leaving | -| `entry` | The phase while the element is entering the viewport | -| `exit` | The phase while the element is exiting the viewport | -| `contain` | While the element is fully contained in the viewport | -| `entry-crossing` | From the element's leading edge entering to its leading edge reaching the opposite side | -| `exit-crossing` | From the element's trailing edge reaching the start to its trailing edge leaving | +| Range name | Meaning | +| :--------------- | :------------------------------------------------------------------------------------------------------- | +| `cover` | Full visibility span from first pixel entering to last pixel leaving | +| `entry` | The phase while the element is entering the viewport | +| `exit` | The phase while the element is exiting the viewport | +| `contain` | While the element is fully contained in the viewport. Typically used with a `position: sticky` container | +| `entry-crossing` | From the element's leading edge entering to its leading edge reaching the opposite side | +| `exit-crossing` | From the element's trailing edge reaching the start to its trailing edge leaving | **Sticky container pattern** — for scroll-driven animations inside a stuck `position: sticky` container: diff --git a/packages/interact/rules/integration.md b/packages/interact/rules/integration.md index aea8e657..2afe17ab 100644 --- a/packages/interact/rules/integration.md +++ b/packages/interact/rules/integration.md @@ -291,11 +291,11 @@ const css = generate(config); ``` -**Web:** +**Web (Custom Elements):** ```html -
...
+
...
``` diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index 976cfb18..7e15af56 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -90,6 +90,12 @@ class Fragments { for (const [key, val] of Object.entries(params)) { content = content.replaceAll(`{{${key}}}`, val); } + const unreplaced = content.match(/\{\{[^}]+\}\}/g); + if (unreplaced) { + console.warn( + `Warning: unreplaced placeholders in fragment "${path}#${section}": ${unreplaced.join(', ')}`, + ); + } return content; } } From 82b00206bc5f4660f82e0732a052e14be2cb1fe8 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 11:56:34 +0300 Subject: [PATCH 13/27] PR fixes --- .../rules_build_pipeline_ffe2a9f3.plan.md | 78 +++++++++++++++++++ .../fragments/pitfalls/overflow-clip.md | 2 + .../pitfalls/same-element-viewenter.md | 2 + .../interact/_content/templates/_helpers.mjs | 24 +++++- .../_content/templates/event-trigger-rule.mjs | 5 +- .../interact/_content/templates/full-lean.mjs | 46 +++-------- .../_content/templates/integration.mjs | 2 +- .../_content/templates/pointermove-rule.mjs | 27 +++---- .../_content/templates/viewenter-rule.mjs | 5 +- .../_content/templates/viewprogress-rule.mjs | 14 ++-- packages/interact/rules/pointermove.md | 23 ++---- packages/interact/rules/viewprogress.md | 9 +-- 12 files changed, 141 insertions(+), 96 deletions(-) diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md index 3e2e62bb..833f73dd 100644 --- a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md +++ b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md @@ -140,6 +140,24 @@ todos: - id: review-regenerate content: 'Regenerate rules/*.md — full-lean.md range table widened for contain note, integration.md FOUC section uses fragments. All other 5 files byte-identical.' status: completed + - id: refactor6-varline-consistency + content: 'Extend varLine usage across all templates — use canonical varLine in Rule 1, standardize "same as Rule 1" shorthand in subsequent rules. Eliminates duplicated variable descriptions in viewprogress Rules 2-3 and pointermove Rules 2-4.' + status: completed + - id: refactor6-markdown-table + content: 'Extract generic buildMarkdownTable(headers, rows) helper to _helpers.mjs — replaces duplicated column-width padding logic in full-lean.mjs (behavior tables + range table).' + status: completed + - id: refactor6-pitfalls-wrapped + content: 'Add { wrapped: true } option to buildPitfallsBlock() — eliminates 3 identical 2-line wrapping patterns in event-trigger, viewenter, and viewprogress templates.' + status: completed + - id: refactor6-jsdoc + content: 'Standardize JSDoc @param on all render() functions — trigger-specific templates document { triggers, effects, meta, trigger }, reference templates document { triggers, effects, meta }.' + status: completed + - id: refactor6-full-lean-fragments + content: 'Add #full-lean sections to overflow-clip.md and same-element-viewenter.md fragments — full-lean.mjs triggers section now uses fragments instead of hardcoded CRITICAL notes for viewEnter and viewProgress.' + status: completed + - id: refactor6-regenerate + content: 'Regenerate rules/*.md — pointermove.md and viewprogress.md updated (collapsed duplicate descriptions to "same as Rule 1" shorthand, UNIQUE_EFFECT_ID uses canonical varLine). click.md, hover.md, viewenter.md, full-lean.md, integration.md byte-identical.' + status: completed isProject: false --- @@ -737,6 +755,66 @@ All other 5 files (click.md, hover.md, viewenter.md, viewprogress.md, pointermov --- +## Post-PR Fixes — Round 6 (consistency & deduplication) + +Refactors from a structural review of the final PR, addressing duplicated logic in helpers, inconsistent variable descriptions across templates, and hardcoded content in `full-lean.mjs` that duplicates existing fragments. + +### Fix 34: Extend `varLine` usage across all templates + +Variable descriptions in `viewprogress-rule.mjs` Rules 2-3 and `pointermove-rule.mjs` Rules 2-4 had fully-written-out descriptions that duplicated `COMMON_VARS` entries or each other. The "same as Rule 1" shorthand pattern (already used by `viewenter-rule.mjs`) was not applied consistently. + +**Action:** + +- Rule 1 of each template: uses `varLine` for all COMMON_VARS (viewprogress Rule 1 `UNIQUE_EFFECT_ID` converted from hardcoded to `varLine`). +- Rules 2+ of each template: standardized to "same as Rule 1" shorthand for variables already described in Rule 1 (collapsed 15+ standalone lines across viewprogress and pointermove into grouped shorthand references). +- Variables unique to a specific rule (e.g. `TALL_WRAPPER_KEY`, `COMPOSITE_OPERATION`, `AXIS`) remain hardcoded — they're not shared. + +### Fix 35: Extract generic `buildMarkdownTable` helper + +`full-lean.mjs` had two independent implementations of Prettier-compatible padded markdown tables: `buildBehaviorTable` (3 columns) and the range table construction (2 columns). Both computed column widths with `Math.max` and padded cells — identical logic with different data. + +**Action:** Added `buildMarkdownTable(headers, rows)` to `_helpers.mjs`. Takes an array of header strings and an array of row arrays, computes column widths, returns a left-aligned padded table string. Updated `buildBehaviorTable` and the range table in `full-lean.mjs` to use it. Output is byte-identical. + +### Fix 36: Add `{ wrapped }` option to `buildPitfallsBlock` + +Three templates (`event-trigger-rule.mjs`, `viewenter-rule.mjs`, `viewprogress-rule.mjs`) had an identical 2-line pattern after calling `buildPitfallsBlock`: + +```javascript +const pitfallsRaw = buildPitfallsBlock(trigger, fragments); +const pitfallsBlock = pitfallsRaw ? `\n${pitfallsRaw}\n` : ''; +``` + +**Action:** Added `{ wrapped: true }` option to `buildPitfallsBlock` in `_helpers.mjs`. When set, wraps non-empty output with leading/trailing newlines. Updated the 3 templates to use it, eliminating the intermediate variable. Output is byte-identical. + +### Fix 37: Standardize JSDoc on `render()` functions + +Template `render()` JSDoc annotations documented different subsets of the `data` parameter: + +- `event-trigger-rule.mjs`: `{ trigger, meta }` +- `viewprogress-rule.mjs`: `{ trigger, effects, meta }` +- `full-lean.mjs`: `{ triggers, effects, meta }` + +All trigger-specific templates actually receive `{ triggers, effects, meta, trigger }` (via `{ ...data, trigger }` in `build-rules.mjs`). Reference templates receive `{ triggers, effects, meta }`. + +**Action:** Standardized all trigger-specific templates to `@param {{ triggers: object[], effects: object, meta: object, trigger: object }} data`. Reference templates (`full-lean.mjs`, `integration.mjs`) document `{ triggers, effects, meta }` with a note that `trigger` is absent. + +### Fix 38: Extract full-lean.mjs trigger CRITICAL notes into fragments + +The viewEnter and viewProgress subsections in `full-lean.mjs`'s Triggers section contained hardcoded CRITICAL notes that duplicated content from `pitfalls/same-element-viewenter.md` and `pitfalls/overflow-clip.md`. If these pitfalls are updated in the fragments, `full-lean.md` would drift. + +**Action:** Added `#full-lean` sections to both pitfall fragment files with the condensed wording used by `full-lean.mjs`. Updated `full-lean.mjs` to call `fragments.get('pitfalls/same-element-viewenter', 'full-lean')` and `fragments.get('pitfalls/overflow-clip', 'full-lean')`. Output is byte-identical. + +### Fix 39: Regenerate output (round 6) + +Regenerated all 7 rule files. Two files changed: + +- **pointermove.md**: Collapsed 15 duplicate variable description lines across Rules 2-4 into grouped "same as Rule 1" shorthands. `EFFECT_NAME` in Rule 2 now uses the canonical `varLine` description. +- **viewprogress.md**: `UNIQUE_EFFECT_ID` in Rule 1 now uses canonical `varLine` description. Rules 2-3 collapsed duplicate descriptions into "same as Rule 1" shorthands. + +All other 5 files (click.md, hover.md, viewenter.md, full-lean.md, integration.md) are byte-identical. + +--- + ## Future Extension Points - **docs/**: Same `_content/data/` feeds docs templates — add `_content/templates/docs/` later diff --git a/packages/interact/_content/fragments/pitfalls/overflow-clip.md b/packages/interact/_content/fragments/pitfalls/overflow-clip.md index d8b67b33..1545efd1 100644 --- a/packages/interact/_content/fragments/pitfalls/overflow-clip.md +++ b/packages/interact/_content/fragments/pitfalls/overflow-clip.md @@ -2,3 +2,5 @@ > **CRITICAL:** You MUST replace all usage of `overflow: hidden` with `overflow: clip` on every element between the trigger source element and the scroll container. `overflow: hidden` creates a new scroll context that breaks the ViewTimeline; `overflow: clip` clips overflow visually without affecting scroll ancestry. If using Tailwind, replace all `overflow-hidden` classes with `overflow-clip`. - **CRITICAL — `overflow: hidden` breaks `viewProgress`**: Replace with `overflow: clip` on all ancestors between source and scroll container. In Tailwind, replace `overflow-hidden` with `overflow-clip`. + +**CRITICAL:** Replace ALL `overflow: hidden` with `overflow: clip` on every element between the trigger source and the scroll container. `overflow: hidden` creates a new scroll context that breaks ViewTimeline. In Tailwind replace `overflow-hidden` with `overflow-clip`. diff --git a/packages/interact/_content/fragments/pitfalls/same-element-viewenter.md b/packages/interact/_content/fragments/pitfalls/same-element-viewenter.md index 3b3285ca..d1445ff9 100644 --- a/packages/interact/_content/fragments/pitfalls/same-element-viewenter.md +++ b/packages/interact/_content/fragments/pitfalls/same-element-viewenter.md @@ -2,3 +2,5 @@ > **CRITICAL:** When the source (trigger) and target (effect) elements are the **same element**, use ONLY `triggerType: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`), MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing. - **CRITICAL**: When using `viewEnter` trigger and source (trigger) and target (effect) elements are the **same element**, use ONLY `triggerType: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`) MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing. + +**CRITICAL:** When source and target are the **same element**, MUST use `triggerType: 'once'`. For `'repeat'` / `'alternate'` / `'state'`, ALWAYS use **separate** source and target elements — animating the observed element can cause it to leave/re-enter the viewport, causing rapid re-triggers. diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs index 34c44973..1e07a9e6 100644 --- a/packages/interact/_content/templates/_helpers.mjs +++ b/packages/interact/_content/templates/_helpers.mjs @@ -9,13 +9,31 @@ export function when(condition, content) { /** * Builds the pitfalls block for a trigger template. * Iterates trigger.pitfalls from YAML, resolving each fragment section. - * Returns empty string if no pitfalls; raw content otherwise (caller handles spacing). + * When `wrapped` is true, wraps non-empty output with leading/trailing newlines + * (the common pattern used by event-trigger, viewenter, and viewprogress templates). */ -export function buildPitfallsBlock(trigger, fragments) { +export function buildPitfallsBlock(trigger, fragments, { wrapped = false } = {}) { if (!trigger.pitfalls?.length) return ''; - return trigger.pitfalls + const content = trigger.pitfalls .map((p) => fragments.get(`pitfalls/${p.id}`, p.section || trigger.name)) .join('\n'); + return wrapped ? `\n${content}\n` : content; +} + +/** + * Builds a Prettier-compatible padded markdown table. + * @param {string[]} headers — column header labels + * @param {string[][]} rows — array of rows, each an array of cell strings + */ +export function buildMarkdownTable(headers, rows) { + const widths = headers.map((h, i) => + Math.max(h.length, ...rows.map((r) => (r[i] || '').length)), + ); + return [ + `| ${headers.map((h, i) => h.padEnd(widths[i])).join(' | ')} |`, + `| ${widths.map((w) => `:${'-'.repeat(w - 1)}`).join(' | ')} |`, + ...rows.map((r) => `| ${r.map((c, i) => (c || '').padEnd(widths[i])).join(' | ')} |`), + ].join('\n'); } /** diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 3f44b317..59a23dee 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -2,7 +2,7 @@ import { capitalize, when, buildPitfallsBlock, varLine } from './_helpers.mjs'; /** * Renders a trigger-specific rule file (click.md or hover.md). - * @param {{ trigger: object, meta: object }} data — must include `trigger` (from triggers.yaml) and `meta` + * @param {{ triggers: object[], effects: object, meta: object, trigger: object }} data * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { @@ -12,8 +12,7 @@ export function render(data, fragments) { const hasReversed = trigger.templateFields.includes('reversed'); const hasEffectId = trigger.templateFields.includes('effectId'); - const pitfallsRaw = buildPitfallsBlock(trigger, fragments); - const pitfallsBlock = pitfallsRaw ? `\n${pitfallsRaw}\n` : ''; + const pitfallsBlock = buildPitfallsBlock(trigger, fragments, { wrapped: true }); const multipleEffectsNote = when( trigger.showMultipleEffectsNote, diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 7facc752..373bbf30 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -1,4 +1,4 @@ -import { capitalize } from './_helpers.mjs'; +import { capitalize, buildMarkdownTable } from './_helpers.mjs'; const FULL_LEAN_BEHAVIOR = { hover: { @@ -39,21 +39,10 @@ function buildBehaviorTable(headerLabel, behaviorKey) { const rows = keys.map((k) => { const label = k === defaultKey ? `\`'${k}'\` (default)` : `\`'${k}'\``; - return { label, hover: hoverBehavior[k], click: clickBehavior[k] }; + return [label, hoverBehavior[k], clickBehavior[k]]; }); - const col0Width = Math.max(headerLabel.length, ...rows.map((r) => r.label.length)); - const col1Width = Math.max('hover behavior'.length, ...rows.map((r) => r.hover.length)); - const col2Width = Math.max('click behavior'.length, ...rows.map((r) => r.click.length)); - - return [ - `| ${headerLabel.padEnd(col0Width)} | ${'hover behavior'.padEnd(col1Width)} | ${'click behavior'.padEnd(col2Width)} |`, - `| :${'-'.repeat(col0Width - 1)} | :${'-'.repeat(col1Width - 1)} | :${'-'.repeat(col2Width - 1)} |`, - ...rows.map( - (r) => - `| ${r.label.padEnd(col0Width)} | ${r.hover.padEnd(col1Width)} | ${r.click.padEnd(col2Width)} |`, - ), - ].join('\n'); + return buildMarkdownTable([headerLabel, 'hover behavior', 'click behavior'], rows); } function buildFullLeanPitfalls(pitfallOrder, fragments) { @@ -62,7 +51,7 @@ function buildFullLeanPitfalls(pitfallOrder, fragments) { /** * Renders full-lean.md — the comprehensive reference for all triggers, effects, and API surface. - * @param {{ triggers: object[], effects: object, meta: object }} data + * @param {{ triggers: object[], effects: object, meta: object }} data — no `trigger`; receives the full data object * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { @@ -85,27 +74,10 @@ export function render(data, fragments) { .map((e) => ` | ${e.label.padEnd(8)} | ${e.value.padEnd(maxPresetLen)} |`) .join('\n'); - const rangeEntries = Object.entries(data.effects.rangeNames).map(([name, desc]) => ({ - name: `\`${name}\``, - desc, - })); - const rangeHeaderName = 'Range name'; - const rangeHeaderDesc = 'Meaning'; - const rangeNameWidth = Math.max( - rangeHeaderName.length, - ...rangeEntries.map((e) => e.name.length), - ); - const rangeDescWidth = Math.max( - rangeHeaderDesc.length, - ...rangeEntries.map((e) => e.desc.length), + const rangeTable = buildMarkdownTable( + ['Range name', 'Meaning'], + Object.entries(data.effects.rangeNames).map(([name, desc]) => [`\`${name}\``, desc]), ); - const rangeTable = [ - `| ${rangeHeaderName.padEnd(rangeNameWidth)} | ${rangeHeaderDesc.padEnd(rangeDescWidth)} |`, - `| :${'-'.repeat(rangeNameWidth - 1)} | :${'-'.repeat(rangeDescWidth - 1)} |`, - ...rangeEntries.map( - (e) => `| ${e.name.padEnd(rangeNameWidth)} | ${e.desc.padEnd(rangeDescWidth)} |`, - ), - ].join('\n'); return `# ${data.meta.packageName} — Rules @@ -300,7 +272,7 @@ params: { effect.triggerType: ${triggerTypeUnion}; // default: '${viewEnter.defaultTriggerType}' \`\`\` -**CRITICAL:** When source and target are the **same element**, MUST use \`triggerType: 'once'\`. For \`'repeat'\` / \`'alternate'\` / \`'state'\`, ALWAYS use **separate** source and target elements — animating the observed element can cause it to leave/re-enter the viewport, causing rapid re-triggers. +${fragments.get('pitfalls/same-element-viewenter', 'full-lean')} ### viewProgress @@ -308,7 +280,7 @@ Scroll-driven animations using native \`ViewTimeline\`, with polyfill where not \`viewProgress\` has no trigger params. Range configuration (\`rangeStart\`/\`rangeEnd\`) is on the effect, not on the trigger. -**CRITICAL:** Replace ALL \`overflow: hidden\` with \`overflow: clip\` on every element between the trigger source and the scroll container. \`overflow: hidden\` creates a new scroll context that breaks ViewTimeline. In Tailwind replace \`overflow-hidden\` with \`overflow-clip\`. +${fragments.get('pitfalls/overflow-clip', 'full-lean')} ### pointerMove diff --git a/packages/interact/_content/templates/integration.mjs b/packages/interact/_content/templates/integration.mjs index 043f40b6..77662576 100644 --- a/packages/interact/_content/templates/integration.mjs +++ b/packages/interact/_content/templates/integration.mjs @@ -1,6 +1,6 @@ /** * Renders integration.md — integration guide covering entry points, config schema, and triggers overview. - * @param {{ triggers: object[], effects: object, meta: object }} data + * @param {{ triggers: object[], effects: object, meta: object }} data — no `trigger`; receives the full data object * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { diff --git a/packages/interact/_content/templates/pointermove-rule.mjs b/packages/interact/_content/templates/pointermove-rule.mjs index 6f7afd6d..fc5fefea 100644 --- a/packages/interact/_content/templates/pointermove-rule.mjs +++ b/packages/interact/_content/templates/pointermove-rule.mjs @@ -2,7 +2,7 @@ import { buildPitfallsBlock, varLine } from './_helpers.mjs'; /** * Renders pointermove.md — rules for pointer-driven interactions with 2D mouse tracking. - * @param {{ trigger: object, effects: object, meta: object }} data + * @param {{ triggers: object[], effects: object, meta: object, trigger: object }} data * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { @@ -184,15 +184,11 @@ Use \`keyframeEffect\` when the pointer position along a single axis should driv ### Variables -- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. -- \`[HIT_AREA]\` — \`'self'\` or \`'root'\`. +- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` / \`[HIT_AREA]\` — same as Rule 1. - \`[AXIS]\` — \`'x'\` (horizontal) or \`'y'\` (vertical). Defaults to \`'y'\` when omitted. -- \`[EFFECT_NAME]\` — unique string name for the keyframe effect. +${varLine('EFFECT_NAME')} - \`[KEYFRAMES]\` — array of CSS keyframe objects (e.g. \`[{ transform: 'rotate(-10deg)' }, { transform: 'rotate(0)' }, { transform: 'rotate(10deg)' }]\`). Distributed evenly across 0–1 progress: first keyframe = progress 0 (left/top edge), last = progress 1 (right/bottom edge). Any number of keyframes is allowed. -- \`[CENTERED_TO_TARGET]\` — optional. \`true\` or \`false\`. See **Centering with \`centeredToTarget\`** above. Defaults to \`false\`. -- \`[TRANSITION_DURATION_MS]\` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details. -- \`[TRANSITION_EASING]\` — optional. CSS easing string or named easing from \`@wix/motion\`. See Rule 1 for supported values. -- \`[UNIQUE_EFFECT_ID]\` — optional string identifier. +- \`[CENTERED_TO_TARGET]\` / \`[TRANSITION_DURATION_MS]\` / \`[TRANSITION_EASING]\` / \`[UNIQUE_EFFECT_ID]\` — same as Rule 1. --- @@ -243,15 +239,13 @@ Use two separate interactions on the same source/target pair — one for \`axis: ### Variables -- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. -- \`[HIT_AREA]\` — \`'self'\` or \`'root'\`. +- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` / \`[HIT_AREA]\` — same as Rule 1. - \`[X_EFFECT_ID]\` / \`[Y_EFFECT_ID]\` — unique string identifiers for the X-axis and Y-axis effects. Required — they map to keys in the top-level \`effects\` map. - \`[X_EFFECT_NAME]\` / \`[Y_EFFECT_NAME]\` — unique string names for each keyframe effect. - \`[X_KEYFRAMES]\` / \`[Y_KEYFRAMES]\` — arrays of WAAPI keyframe objects for the X-axis and Y-axis effects respectively. Each effect can vary in properties and keyframes. - \`[COMPOSITE_OPERATION]\` — \`'add'\` or \`'accumulate'\`. Required when both effects animate \`transform\` and/or both animate \`filter\`, so their values combine rather than override. \`'add'\`: composited transform functions are appended. \`'accumulate'\`: matching function arguments are summed. -- \`[FILL_MODE]\` — typically \`'both'\` to ensure the effect keeps applying after exiting the effect's active range. -- \`[TRANSITION_DURATION_MS]\` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details. -- \`[TRANSITION_EASING]\` — optional. CSS easing function for the smoothing transition. See Rule 1 for supported values. +${varLine('FILL_MODE', "typically `'both'` to ensure the effect keeps applying after exiting the effect's active range.")} +- \`[TRANSITION_DURATION_MS]\` / \`[TRANSITION_EASING]\` — same as Rule 1. --- @@ -283,11 +277,8 @@ Use \`customEffect\` when you need full imperative control over pointer-driven a ### Variables -- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. -- \`[HIT_AREA]\` — \`'self'\` or \`'root'\`. +- \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` / \`[HIT_AREA]\` — same as Rule 1. - \`[CUSTOM_ANIMATION_LOGIC]\` — JavaScript using \`progress.x\`, \`progress.y\`, \`progress.v\`, and \`progress.active\` to apply the effect. See **Progress Object Structure** above. -- \`[CENTERED_TO_TARGET]\` — optional. \`true\` or \`false\`. See **Centering with \`centeredToTarget\`** above. Defaults to \`false\`. -- \`[TRANSITION_DURATION_MS]\` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details. -- \`[TRANSITION_EASING]\` — optional. CSS easing function for the smoothing transition. See Rule 1 for supported values. +- \`[CENTERED_TO_TARGET]\` / \`[TRANSITION_DURATION_MS]\` / \`[TRANSITION_EASING]\` — same as Rule 1. `; } diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index efb0b35c..4dd9ac91 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -2,14 +2,13 @@ import { buildPitfallsBlock, varLine } from './_helpers.mjs'; /** * Renders viewenter.md — rules for viewport-entry triggered animations. - * @param {{ trigger: object, meta: object }} data — must include `trigger` (viewEnter from triggers.yaml) and `meta` + * @param {{ triggers: object[], effects: object, meta: object, trigger: object }} data * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { const { trigger } = data; - const pitfallsRaw = buildPitfallsBlock(trigger, fragments); - const pitfallsBlock = pitfallsRaw ? `\n${pitfallsRaw}\n` : ''; + const pitfallsBlock = buildPitfallsBlock(trigger, fragments, { wrapped: true }); const paramDescriptions = trigger.params .map((p) => { diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index e20e0033..e3be63cf 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -2,14 +2,13 @@ import { buildPitfallsBlock, varLine } from './_helpers.mjs'; /** * Renders viewprogress.md — rules for scroll-driven animations using ViewTimeline. - * @param {{ trigger: object, effects: object, meta: object }} data + * @param {{ triggers: object[], effects: object, meta: object, trigger: object }} data * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { const { trigger } = data; - const pitfallsRaw = buildPitfallsBlock(trigger, fragments); - const pitfallsBlock = pitfallsRaw ? `\n${pitfallsRaw}\n` : ''; + const pitfallsBlock = buildPitfallsBlock(trigger, fragments, { wrapped: true }); const rangeList = Object.entries(data.effects.rangeNames) .map(([name, desc]) => { @@ -74,7 +73,7 @@ ${rangeList} - \`[START_PERCENTAGE]\` — 0–100, starting point within the named range. - \`[END_PERCENTAGE]\` — 0–100, end point within the named range. ${varLine('EASING_FUNCTION', "CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects.")} -- \`[UNIQUE_EFFECT_ID]\` — optional identifier for referencing the effect externally. +${varLine('UNIQUE_EFFECT_ID')} --- @@ -107,9 +106,7 @@ ${varLine('EASING_FUNCTION', "CSS easing string or named easing from `@wix/motio - \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. ${varLine('CUSTOM_EFFECT_CALLBACK')} -- \`[RANGE_NAME]\` / \`[START_PERCENTAGE]\` / \`[END_PERCENTAGE]\` — same as Rule 1. -${varLine('EASING_FUNCTION', "CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects.")} -- \`[UNIQUE_EFFECT_ID]\` — optional identifier for referencing the effect externally. +- \`[RANGE_NAME]\` / \`[START_PERCENTAGE]\` / \`[END_PERCENTAGE]\` / \`[EASING_FUNCTION]\` / \`[UNIQUE_EFFECT_ID]\` — same as Rule 1. --- @@ -152,7 +149,6 @@ ${varLine('EASING_FUNCTION', "CSS easing string or named easing from `@wix/motio - \`[EFFECT_NAME]\` / \`[EFFECT_KEYFRAMES]\` — same as Rule 1. - \`[START_PERCENTAGE]\` — 0–100, starting point within the \`contain\` range (the stuck phase). - \`[END_PERCENTAGE]\` — 0–100, end point within the \`contain\` range. -- \`[UNIQUE_EFFECT_ID]\` — same as Rule 1. -- \`[EASING_FUNCTION]\` — CSS easing string or named easing from \`@wix/motion\`. Typically \`'linear'\` for scrolling effects. +- \`[EASING_FUNCTION]\` / \`[UNIQUE_EFFECT_ID]\` — same as Rule 1. `; } diff --git a/packages/interact/rules/pointermove.md b/packages/interact/rules/pointermove.md index 5aed2cad..19c72e89 100644 --- a/packages/interact/rules/pointermove.md +++ b/packages/interact/rules/pointermove.md @@ -172,15 +172,11 @@ Use `keyframeEffect` when the pointer position along a single axis should drive ### Variables -- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1. -- `[HIT_AREA]` — `'self'` or `'root'`. +- `[SOURCE_KEY]` / `[TARGET_KEY]` / `[HIT_AREA]` — same as Rule 1. - `[AXIS]` — `'x'` (horizontal) or `'y'` (vertical). Defaults to `'y'` when omitted. -- `[EFFECT_NAME]` — unique string name for the keyframe effect. +- `[EFFECT_NAME]` — unique string identifier for a `keyframeEffect`. - `[KEYFRAMES]` — array of CSS keyframe objects (e.g. `[{ transform: 'rotate(-10deg)' }, { transform: 'rotate(0)' }, { transform: 'rotate(10deg)' }]`). Distributed evenly across 0–1 progress: first keyframe = progress 0 (left/top edge), last = progress 1 (right/bottom edge). Any number of keyframes is allowed. -- `[CENTERED_TO_TARGET]` — optional. `true` or `false`. See **Centering with `centeredToTarget`** above. Defaults to `false`. -- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details. -- `[TRANSITION_EASING]` — optional. CSS easing string or named easing from `@wix/motion`. See Rule 1 for supported values. -- `[UNIQUE_EFFECT_ID]` — optional string identifier. +- `[CENTERED_TO_TARGET]` / `[TRANSITION_DURATION_MS]` / `[TRANSITION_EASING]` / `[UNIQUE_EFFECT_ID]` — same as Rule 1. --- @@ -231,15 +227,13 @@ Use two separate interactions on the same source/target pair — one for `axis: ### Variables -- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1. -- `[HIT_AREA]` — `'self'` or `'root'`. +- `[SOURCE_KEY]` / `[TARGET_KEY]` / `[HIT_AREA]` — same as Rule 1. - `[X_EFFECT_ID]` / `[Y_EFFECT_ID]` — unique string identifiers for the X-axis and Y-axis effects. Required — they map to keys in the top-level `effects` map. - `[X_EFFECT_NAME]` / `[Y_EFFECT_NAME]` — unique string names for each keyframe effect. - `[X_KEYFRAMES]` / `[Y_KEYFRAMES]` — arrays of WAAPI keyframe objects for the X-axis and Y-axis effects respectively. Each effect can vary in properties and keyframes. - `[COMPOSITE_OPERATION]` — `'add'` or `'accumulate'`. Required when both effects animate `transform` and/or both animate `filter`, so their values combine rather than override. `'add'`: composited transform functions are appended. `'accumulate'`: matching function arguments are summed. - `[FILL_MODE]` — typically `'both'` to ensure the effect keeps applying after exiting the effect's active range. -- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details. -- `[TRANSITION_EASING]` — optional. CSS easing function for the smoothing transition. See Rule 1 for supported values. +- `[TRANSITION_DURATION_MS]` / `[TRANSITION_EASING]` — same as Rule 1. --- @@ -271,9 +265,6 @@ Use `customEffect` when you need full imperative control over pointer-driven ani ### Variables -- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1. -- `[HIT_AREA]` — `'self'` or `'root'`. +- `[SOURCE_KEY]` / `[TARGET_KEY]` / `[HIT_AREA]` — same as Rule 1. - `[CUSTOM_ANIMATION_LOGIC]` — JavaScript using `progress.x`, `progress.y`, `progress.v`, and `progress.active` to apply the effect. See **Progress Object Structure** above. -- `[CENTERED_TO_TARGET]` — optional. `true` or `false`. See **Centering with `centeredToTarget`** above. Defaults to `false`. -- `[TRANSITION_DURATION_MS]` — optional. Milliseconds for smoothing between progress updates. See Rule 1 for details. -- `[TRANSITION_EASING]` — optional. CSS easing function for the smoothing transition. See Rule 1 for supported values. +- `[CENTERED_TO_TARGET]` / `[TRANSITION_DURATION_MS]` / `[TRANSITION_EASING]` — same as Rule 1. diff --git a/packages/interact/rules/viewprogress.md b/packages/interact/rules/viewprogress.md index faaa33f7..c5db681d 100644 --- a/packages/interact/rules/viewprogress.md +++ b/packages/interact/rules/viewprogress.md @@ -62,7 +62,7 @@ These rules help generate scroll-driven interactions using `@wix/interact`. View - `[START_PERCENTAGE]` — 0–100, starting point within the named range. - `[END_PERCENTAGE]` — 0–100, end point within the named range. - `[EASING_FUNCTION]` — CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects. -- `[UNIQUE_EFFECT_ID]` — optional identifier for referencing the effect externally. +- `[UNIQUE_EFFECT_ID]` — optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects from the top-level `effects` map. --- @@ -95,9 +95,7 @@ These rules help generate scroll-driven interactions using `@wix/interact`. View - `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1. - `[CUSTOM_EFFECT_CALLBACK]` — function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1. -- `[RANGE_NAME]` / `[START_PERCENTAGE]` / `[END_PERCENTAGE]` — same as Rule 1. -- `[EASING_FUNCTION]` — CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects. -- `[UNIQUE_EFFECT_ID]` — optional identifier for referencing the effect externally. +- `[RANGE_NAME]` / `[START_PERCENTAGE]` / `[END_PERCENTAGE]` / `[EASING_FUNCTION]` / `[UNIQUE_EFFECT_ID]` — same as Rule 1. --- @@ -140,5 +138,4 @@ These rules help generate scroll-driven interactions using `@wix/interact`. View - `[EFFECT_NAME]` / `[EFFECT_KEYFRAMES]` — same as Rule 1. - `[START_PERCENTAGE]` — 0–100, starting point within the `contain` range (the stuck phase). - `[END_PERCENTAGE]` — 0–100, end point within the `contain` range. -- `[UNIQUE_EFFECT_ID]` — same as Rule 1. -- `[EASING_FUNCTION]` — CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling effects. +- `[EASING_FUNCTION]` / `[UNIQUE_EFFECT_ID]` — same as Rule 1. From 3efbefa7397e010a04590def398df0c9c441774d Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 12:06:57 +0300 Subject: [PATCH 14/27] PR fixes --- .../rules_build_pipeline_ffe2a9f3.plan.md | 58 +++++++++++++++++ packages/interact/_content/data/effects.yaml | 10 --- packages/interact/_content/data/triggers.yaml | 50 ++------------- .../interact/_content/templates/_helpers.mjs | 4 -- .../_content/templates/event-trigger-rule.mjs | 63 ++++++++++++++----- .../interact/_content/templates/full-lean.mjs | 9 ++- packages/interact/scripts/build-rules.mjs | 4 +- 7 files changed, 121 insertions(+), 77 deletions(-) diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md index 833f73dd..f883b3b4 100644 --- a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md +++ b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md @@ -158,6 +158,24 @@ todos: - id: refactor6-regenerate content: 'Regenerate rules/*.md — pointermove.md and viewprogress.md updated (collapsed duplicate descriptions to "same as Rule 1" shorthand, UNIQUE_EFFECT_ID uses canonical varLine). click.md, hover.md, viewenter.md, full-lean.md, integration.md byte-identical.' status: completed + - id: refactor7-pitfall-order-local + content: 'Move fullLeanPitfallOrder from effects.yaml into full-lean.mjs as a local constant — rendering concern belongs in the template, not the data layer' + status: completed + - id: refactor7-placeholder-error + content: 'Make unreplaced {{...}} placeholder detection throw an error instead of console.warn — prevents silent bugs in generated output' + status: completed + - id: refactor7-inline-overrides + content: 'Move variableOverrides from triggers.yaml into event-trigger-rule.mjs as VARIABLE_OVERRIDES constant — prose descriptions with only 2 consumers do not belong in YAML data layer' + status: completed + - id: refactor7-explicit-booleans + content: 'Replace templateFields array in triggers.yaml with explicit hasReversed/hasEffectId booleans — clearer intent, no array scanning, no drift risk' + status: completed + - id: refactor7-remove-when + content: 'Remove when() helper from _helpers.mjs — replaced with inline ternaries in event-trigger-rule.mjs (5 usages). Eliminates unnecessary abstraction.' + status: completed + - id: refactor7-regenerate + content: 'Regenerate rules/*.md — all 7 files byte-identical to pre-refactor output. Zero content drift.' + status: completed isProject: false --- @@ -815,6 +833,46 @@ All other 5 files (click.md, hover.md, viewenter.md, full-lean.md, integration.m --- +## Post-PR Fixes — Round 7 (separation of concerns & simplification) + +Refactors from a final code review, addressing data/template boundary violations, unnecessary abstractions, and silent error handling. + +### Fix 40: Move `fullLeanPitfallOrder` into full-lean.mjs + +`fullLeanPitfallOrder` in `effects.yaml` was a rendering concern — it controlled the order pitfalls render in one specific template. This coupled the data layer to `full-lean.mjs`'s layout needs (the same issue Fix 30 addressed for `fullLeanBehavior`). + +**Action:** Moved into a `FULL_LEAN_PITFALL_ORDER` constant at the top of `full-lean.mjs`. Removed from `effects.yaml`. Output is byte-identical. + +### Fix 41: Make placeholder detection throw + +The `Fragments.get()` method warned about unmatched `{{...}}` placeholders via `console.warn` — easy to miss in CI output. An unreplaced placeholder in generated output is always a bug. + +**Action:** Changed `console.warn` to `throw new Error`. Build now fails fast on typos in fragment param names. + +### Fix 42: Inline `variableOverrides` into template + +`variableOverrides` in `triggers.yaml` stored prose descriptions that only served `event-trigger-rule.mjs` for 2 triggers (hover, click). The indirection added ~40 lines of English prose to what should be a structured data file. + +**Action:** Moved into a `VARIABLE_OVERRIDES` constant in `event-trigger-rule.mjs`, keyed by trigger name. Template reads `VARIABLE_OVERRIDES[name]` instead of `trigger.variableOverrides`. Removed `variableOverrides` from both trigger entries in `triggers.yaml`. Output is byte-identical. + +### Fix 43: Replace `templateFields` with explicit booleans + +The `templateFields` array in `triggers.yaml` existed solely to derive two booleans (`hasReversed`, `hasEffectId`) via `.includes()`. The array created a false sense of generated config blocks — the template still hardcoded the full code block regardless. + +**Action:** Replaced with explicit `hasReversed: true/false` and `hasEffectId: true/false` fields. Template reads them directly. Output is byte-identical. + +### Fix 44: Remove `when()` helper + +The `when(condition, content)` helper was used 5 times in `event-trigger-rule.mjs`. In each case, a standard ternary (`condition ? content : ''`) is equally readable and doesn't require importing/learning a custom abstraction. + +**Action:** Removed `when()` from `_helpers.mjs`. Replaced all 5 usages in `event-trigger-rule.mjs` with inline ternaries or `|| ''` for falsy string concatenation. Output is byte-identical. + +### Fix 45: Regenerate output (round 7) + +Ran `build:rules`. All 7 rule files are byte-identical to pre-refactor output. Zero content drift. + +--- + ## Future Extension Points - **docs/**: Same `_content/data/` feeds docs templates — add `_content/templates/docs/` later diff --git a/packages/interact/_content/data/effects.yaml b/packages/interact/_content/data/effects.yaml index 56c99f73..df49aacb 100644 --- a/packages/interact/_content/data/effects.yaml +++ b/packages/interact/_content/data/effects.yaml @@ -114,16 +114,6 @@ presets: - SkewMouse - BlobMouse -fullLeanPitfallOrder: - - id: overflow-clip - section: long - - id: same-element-viewenter - section: long - - id: hit-area - section: detailed-hover - - id: hit-area - section: detailed-pointermove - rangeNames: cover: 'Full visibility span from first pixel entering to last pixel leaving' entry: 'The phase while the element is entering the viewport' diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml index 045c9ce5..7b318715 100644 --- a/packages/interact/_content/data/triggers.yaml +++ b/packages/interact/_content/data/triggers.yaml @@ -1,7 +1,7 @@ # -------------------------------------------------------------------------- # Event triggers — rendered by event-trigger-rule.mjs (click.md, hover.md) -# These have the full schema: templateFields, triggerTypeDescriptions, -# stateActionDescriptions, variableOverrides, showMultipleEffectsNote. +# These have the full schema: hasReversed, hasEffectId, triggerTypeDescriptions, +# stateActionDescriptions, showMultipleEffectsNote. # -------------------------------------------------------------------------- triggers: - name: hover @@ -13,16 +13,8 @@ triggers: pitfalls: - id: hit-area showMultipleEffectsNote: true - templateFields: - - triggerType - - keyframeEffect - - namedEffect - - fill - - duration - - easing - - delay - - iterations - - alternate + hasReversed: false + hasEffectId: false triggerTypeDescriptions: alternate: 'plays forward on enter, reverses on leave. Default. Most common for hover.' repeat: 'restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses.' @@ -33,16 +25,6 @@ triggers: add: 'applies the style state on enter. Leave does NOT remove it.' remove: 'removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`.' clear: 'clears all previously applied style states on enter. Use to reset multiple stacked `''add''` style changes at once (e.g. a "reset" hover area that undoes several accumulated states).' - variableOverrides: - sourceKeySuffix: 'The element that listens for hover.' - targetKeyDesc: "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above)." - fillModeDesc: "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished." - easingDesc: "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`." - iterationsDesc: "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`." - fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`." - customEffectExamples: '' - offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.' - alternateBoolSuffix: '' - name: click template: event-trigger-rule.mjs @@ -52,18 +34,8 @@ triggers: params: [] pitfalls: [] showMultipleEffectsNote: false - templateFields: - - triggerType - - keyframeEffect - - namedEffect - - fill - - reversed - - duration - - easing - - delay - - iterations - - alternate - - effectId + hasReversed: true + hasEffectId: true triggerTypeDescriptions: alternate: 'plays forward on first click, reverses on next click. Default.' repeat: 'restarts the animation from the beginning on each click.' @@ -74,16 +46,6 @@ triggers: add: 'applies the style state. Does not remove on subsequent clicks.' remove: 'removes a previously applied style state.' clear: 'clears all previously applied style states. Useful for resetting multiple stacked style states at once.' - variableOverrides: - sourceKeySuffix: 'The element that listens for clicks.' - targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element." - fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect." - easingDesc: 'CSS easing string, or named easing from `@wix/motion`.' - iterationsDesc: 'optional. Number of iterations, or `Infinity` for continuous loops.' - fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`." - customEffectExamples: ', randomized behavior' - offsetEasingSuffix: '' - alternateBoolSuffix: "Different from `triggerType: 'alternate'` which alternates per click." # -------------------------------------------------------------------------- # Viewport/scroll triggers — each has its own dedicated template. diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs index 1e07a9e6..9006c16c 100644 --- a/packages/interact/_content/templates/_helpers.mjs +++ b/packages/interact/_content/templates/_helpers.mjs @@ -2,10 +2,6 @@ export function capitalize(s) { return s.charAt(0).toUpperCase() + s.slice(1); } -export function when(condition, content) { - return condition ? content : ''; -} - /** * Builds the pitfalls block for a trigger template. * Iterates trigger.pitfalls from YAML, resolving each fragment section. diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 59a23dee..1613b9aa 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,4 +1,37 @@ -import { capitalize, when, buildPitfallsBlock, varLine } from './_helpers.mjs'; +import { capitalize, buildPitfallsBlock, varLine } from './_helpers.mjs'; + +const VARIABLE_OVERRIDES = { + hover: { + sourceKeySuffix: 'The element that listens for hover.', + targetKeyDesc: + "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", + fillModeDesc: + "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.", + easingDesc: + "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.", + iterationsDesc: + "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", + fillCritical: + "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", + customEffectExamples: '', + offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.', + alternateBoolSuffix: '', + }, + click: { + sourceKeySuffix: 'The element that listens for clicks.', + targetKeyDesc: + "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", + fillModeDesc: + "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", + easingDesc: 'CSS easing string, or named easing from `@wix/motion`.', + iterationsDesc: 'optional. Number of iterations, or `Infinity` for continuous loops.', + fillCritical: + "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.", + customEffectExamples: ', randomized behavior', + offsetEasingSuffix: '', + alternateBoolSuffix: "Different from `triggerType: 'alternate'` which alternates per click.", + }, +}; /** * Renders a trigger-specific rule file (click.md or hover.md). @@ -7,17 +40,17 @@ import { capitalize, when, buildPitfallsBlock, varLine } from './_helpers.mjs'; */ export function render(data, fragments) { const { trigger } = data; - const { name, variableOverrides: vo } = trigger; + const { name } = trigger; + const vo = VARIABLE_OVERRIDES[name]; const Name = capitalize(name); - const hasReversed = trigger.templateFields.includes('reversed'); - const hasEffectId = trigger.templateFields.includes('effectId'); + const hasReversed = trigger.hasReversed; + const hasEffectId = trigger.hasEffectId; const pitfallsBlock = buildPitfallsBlock(trigger, fragments, { wrapped: true }); - const multipleEffectsNote = when( - trigger.showMultipleEffectsNote, - `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event` })}\n`, - ); + const multipleEffectsNote = trigger.showMultipleEffectsNote + ? `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event` })}\n` + : ''; return `# ${Name} Trigger Rules for ${data.meta.packageName} @@ -57,12 +90,12 @@ ${multipleEffectsNote} // OR namedEffect: [NAMED_EFFECT_DEFINITION], - fill: '[FILL_MODE]',${when(hasReversed, `\n reversed: [INITIAL_REVERSED_BOOL],`)} + fill: '[FILL_MODE]',${hasReversed ? `\n reversed: [INITIAL_REVERSED_BOOL],` : ''} duration: [DURATION_MS], easing: '[EASING_FUNCTION]', delay: [DELAY_MS], iterations: [ITERATIONS], - alternate: [ALTERNATE_BOOL]${when(hasEffectId, `,\n effectId: '[UNIQUE_EFFECT_ID]'`)} + alternate: [ALTERNATE_BOOL]${hasEffectId ? `,\n effectId: '[UNIQUE_EFFECT_ID]'` : ''} }, // additional effects targeting other elements can be added here ] @@ -71,7 +104,7 @@ ${multipleEffectsNote} ### Variables -${buildVariables(trigger, hasReversed, hasEffectId)} +${buildVariables(trigger, vo, hasReversed, hasEffectId)} --- @@ -134,7 +167,7 @@ ${Object.entries(trigger.stateActionDescriptions) ## Rule 3: customEffect (TimeEffect) -Use \`customEffect\` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation${when(vo.customEffectExamples, vo.customEffectExamples)}). The callback receives the target element and a \`progress\` value (0–1) driven by the animation timeline. +Use \`customEffect\` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation${vo.customEffectExamples || ''}). The callback receives the target element and a \`progress\` value (0–1) driven by the animation timeline. \`\`\`typescript { @@ -188,14 +221,12 @@ Use sequences when a ${name} should sync/stagger animations across multiple elem - \`[SOURCE_KEY]\` / \`[TRIGGER_TYPE]\` — same as Rule 1. \`triggerType\` is set on the sequence config, not on individual effects within the sequence. - \`[OFFSET_MS]\` — time offset for staggering each child's animation start, in milliseconds. -- \`[OFFSET_EASING]\` — easing curve for the offset staggering distribution.${when(vo.offsetEasingSuffix, vo.offsetEasingSuffix)} Defaults to \`'linear'\`. +- \`[OFFSET_EASING]\` — easing curve for the offset staggering distribution.${vo.offsetEasingSuffix || ''} Defaults to \`'linear'\`. - \`[EFFECT_DEFINITION]\` — a definition of or a reference to a time-based animation effect. `; } -function buildVariables(trigger, hasReversed, hasEffectId) { - const vo = trigger.variableOverrides; - +function buildVariables(trigger, vo, hasReversed, hasEffectId) { const lines = [ varLine('SOURCE_KEY', vo.sourceKeySuffix), varLine('TARGET_KEY', vo.targetKeyDesc), diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 373bbf30..64cc56d7 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -1,5 +1,12 @@ import { capitalize, buildMarkdownTable } from './_helpers.mjs'; +const FULL_LEAN_PITFALL_ORDER = [ + { id: 'overflow-clip', section: 'long' }, + { id: 'same-element-viewenter', section: 'long' }, + { id: 'hit-area', section: 'detailed-hover' }, + { id: 'hit-area', section: 'detailed-pointermove' }, +]; + const FULL_LEAN_BEHAVIOR = { hover: { triggerType: { @@ -113,7 +120,7 @@ Declarative configuration-driven interaction library. Binds animations to trigge Each item here is CRITICAL — ignoring any of them will break animations. -${buildFullLeanPitfalls(data.effects.fullLeanPitfallOrder, fragments)} +${buildFullLeanPitfalls(FULL_LEAN_PITFALL_ORDER, fragments)} ${fragments.get('pitfalls/dont-guess-presets', 'default')} ${fragments.get('pitfalls/reduced-motion', 'default')} ${fragments.get('pitfalls/perspective', 'default')} diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index 7e15af56..3d3471ba 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -92,8 +92,8 @@ class Fragments { } const unreplaced = content.match(/\{\{[^}]+\}\}/g); if (unreplaced) { - console.warn( - `Warning: unreplaced placeholders in fragment "${path}#${section}": ${unreplaced.join(', ')}`, + throw new Error( + `Unreplaced placeholders in fragment "${path}#${section}": ${unreplaced.join(', ')}`, ); } return content; From cdad8ed05d75e99bb04ea637fbe1d82c16f04d64 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 12:24:37 +0300 Subject: [PATCH 15/27] PR fixes --- .../rules_build_pipeline_ffe2a9f3.plan.md | 80 ++++++++++++++++++- packages/interact/_content/data/triggers.yaml | 69 ++++++++++++---- packages/interact/_content/fragments/fouc.md | 73 ----------------- .../fragments/multiple-effects-note.md | 6 ++ .../_content/templates/event-trigger-rule.mjs | 4 +- .../interact/_content/templates/full-lean.mjs | 73 ++++++----------- .../_content/templates/integration.mjs | 16 +++- .../_content/templates/pointermove-rule.mjs | 16 +--- .../_content/templates/viewenter-rule.mjs | 50 ++++++++++-- .../_content/templates/viewprogress-rule.mjs | 2 +- packages/interact/scripts/build-rules.mjs | 30 +++++++ 11 files changed, 252 insertions(+), 167 deletions(-) diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md index f883b3b4..e9833313 100644 --- a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md +++ b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md @@ -176,6 +176,30 @@ todos: - id: refactor7-regenerate content: 'Regenerate rules/*.md — all 7 files byte-identical to pre-refactor output. Zero content drift.' status: completed + - id: refactor8-multiple-effects-fragment + content: 'Add trigger-specific sections (#viewEnter, #viewProgress, #pointerMove) to multiple-effects-note.md — viewenter, viewprogress, pointermove templates now use shared fragment instead of hardcoded notes' + status: completed + - id: refactor8-viewenter-triggertype-yaml + content: 'Add triggerTypeDescriptions to viewEnter trigger in triggers.yaml — template renders them dynamically via Object.entries(), matching the event-trigger-rule.mjs pattern' + status: completed + - id: refactor8-progress-type-fragment + content: 'Extract Progress type into fragments/progress-type.md with #detailed and #brief sections — pointermove-rule.mjs and full-lean.mjs now share the same source' + status: completed + - id: refactor8-yaml-validation + content: 'Add TRIGGER_SCHEMA validation to build-rules.mjs — checks required fields per template, throws on missing fields to catch typos at build time' + status: completed + - id: refactor8-short-full-descriptions + content: 'Consolidate FULL_LEAN_BEHAVIOR with triggers.yaml — triggerTypeDescriptions and stateActionDescriptions now have short/full structure. Removed FULL_LEAN_BEHAVIOR constant from full-lean.mjs, buildBehaviorTable reads .short from YAML data' + status: completed + - id: refactor8-fragment-naming-guide + content: 'Add fragments/README.md documenting section naming conventions (#default, #brief/#detailed, #, #full-lean, #code-)' + status: completed + - id: refactor8-fouc-single-consumer + content: 'Move 8 single-consumer FOUC sections from fouc.md into their respective templates — fouc.md reduced from 12 to 4 genuinely shared sections (#code-inject, #code-web, #code-react, #code-vanilla)' + status: completed + - id: refactor8-regenerate + content: 'Regenerate rules/*.md — all 7 files byte-identical to pre-refactor output. Zero content drift.' + status: completed isProject: false --- @@ -873,11 +897,63 @@ Ran `build:rules`. All 7 rule files are byte-identical to pre-refactor output. Z --- +## Post-PR Fixes — Round 8 (single-source-of-truth & validation) + +Refactors from a structural review of the final PR, addressing remaining duplication, missing data-driving, and build-time safety. + +### Fix 46: Use multiple-effects-note fragment everywhere + +The `multiple-effects-note.md` fragment was only used by `event-trigger-rule.mjs`. The viewenter, viewprogress, and pointermove templates each hardcoded their own "Multiple effects" note with trigger-specific wording. + +**Action:** Added `#viewEnter`, `#viewProgress`, and `#pointerMove` sections to the fragment. Updated all three templates to use `fragments.get('multiple-effects-note', triggerName)`. + +### Fix 47: Data-drive viewEnter triggerType descriptions + +`viewenter-rule.mjs` hardcoded 4 `triggerType` descriptions while `event-trigger-rule.mjs` read them from `trigger.triggerTypeDescriptions` in YAML. The viewEnter trigger had no `triggerTypeDescriptions` entry. + +**Action:** Added `triggerTypeDescriptions` to the viewEnter trigger in `triggers.yaml`. Updated `viewenter-rule.mjs` to render them dynamically via `Object.entries(trigger.triggerTypeDescriptions).map(...)`, with `(default)` annotation derived from `trigger.defaultTriggerType`. Also added `triggerTypeDescriptions` to the YAML validation schema. + +### Fix 48: Extract Progress type into shared fragment + +The `Progress` type definition was duplicated between `pointermove-rule.mjs` (detailed, 13 lines) and `full-lean.mjs` (brief, 4 lines). A change to one would not propagate to the other. + +**Action:** Created `fragments/progress-type.md` with `#detailed` and `#brief` sections. Updated both templates to use the fragment. The deeply nested instance in full-lean.mjs's Triggers overview (lines 246-257) was left in place — it's inside a deeply indented markdown list where fragment extraction would be fragile. + +### Fix 49: Add YAML schema validation + +A typo in `triggers.yaml` (e.g. `hasReverse` instead of `hasReversed`) would produce silently wrong output. No build-time guard existed for field names. + +**Action:** Added `TRIGGER_SCHEMA` map to `build-rules.mjs` keyed by template name, listing required fields per trigger type. Build now throws on missing fields before template rendering begins. + +### Fix 50: Consolidate FULL_LEAN_BEHAVIOR with triggers.yaml + +`FULL_LEAN_BEHAVIOR` in `full-lean.mjs` maintained condensed descriptions for hover/click triggerType and stateAction behaviors — duplicating concepts from `triggers.yaml`'s `triggerTypeDescriptions` and `stateActionDescriptions`. Adding a new value to one required updating the other. + +**Action:** Changed `triggerTypeDescriptions` and `stateActionDescriptions` in hover and click trigger entries from flat strings to `{ full, short }` objects. `event-trigger-rule.mjs` reads `.full`, `full-lean.mjs` reads `.short` via `buildBehaviorTable()`. Removed the 30-line `FULL_LEAN_BEHAVIOR` constant entirely. Single source of truth for all behavior descriptions. + +### Fix 51: Document fragment section naming conventions + +Fragment sections used several different naming conventions with no documentation, making it hard for contributors to predict section names. + +**Action:** Added `fragments/README.md` documenting the naming convention patterns: `#default` for single-section fragments, `#brief`/`#detailed` for detail levels, `#` for trigger-specific variants, `#full-lean` for condensed reference wording, `#code-` for code examples. + +### Fix 52: Move single-consumer FOUC sections into templates + +`fouc.md` had 12 sections, 8 of which were used by exactly one template — providing no deduplication benefit, just indirection. + +**Action:** Inlined the 8 single-consumer sections (`#short`, `#long`, `#code-generate-viewenter`, `#code-generate-web`, `#rules-viewenter`, `#rules-brief`, `#rules-detailed`, `#intro-brief`) into their respective templates. `fouc.md` reduced from 12 to 4 genuinely shared sections (`#code-inject`, `#code-web`, `#code-react`, `#code-vanilla`). + +### Fix 53: Regenerate output (round 8) + +Ran `build:rules`. All 7 rule files are byte-identical to pre-refactor output. Zero content drift. + +--- + ## Future Extension Points - **docs/**: Same `_content/data/` feeds docs templates — add `_content/templates/docs/` later - **README**: Same meta.yaml + triggers.yaml generates README sections -- **Validation**: Add `scripts/validate-rules.mjs` that imports actual TS types and cross-checks trigger names, param fields, effect fields against the YAML data +- ~~**Validation**: Add `scripts/validate-rules.mjs` that imports actual TS types and cross-checks trigger names, param fields, effect fields against the YAML data~~ — **Partially done** (Fix 49: YAML schema validation in build-rules.mjs). Full TS type cross-checking remains a future extension. - **motion-presets rules**: The build mechanism is scoped to `packages/interact/` but the architecture (YAML + fragments + templates) can be replicated in `packages/motion-presets/` with shared fragments if needed - ~~**Common variable fragment**: Extract repeated variable descriptions~~ — **Done** (Fix 19: `varLine` helper) -- ~~**FOUC fragment consolidation**: Reduce fouc.md's 13 sections~~ — **Done** (Fix 21: merged 6 sections to 3 via `{{key}}` interpolation) +- ~~**FOUC fragment consolidation**: Reduce fouc.md's 13 sections~~ — **Done** (Fix 21: merged 6 sections to 3 via `{{key}}` interpolation; Fix 52: moved single-consumer sections to templates, reduced to 4 shared sections) diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml index 7b318715..2ca2d48b 100644 --- a/packages/interact/_content/data/triggers.yaml +++ b/packages/interact/_content/data/triggers.yaml @@ -16,15 +16,31 @@ triggers: hasReversed: false hasEffectId: false triggerTypeDescriptions: - alternate: 'plays forward on enter, reverses on leave. Default. Most common for hover.' - repeat: 'restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses.' - once: 'plays once on the first enter and never again.' - state: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`).' + alternate: + full: 'plays forward on enter, reverses on leave. Default. Most common for hover.' + short: 'Play on enter, reverse on leave' + repeat: + full: 'restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses.' + short: 'Play on enter, stop and rewind on leave' + once: + full: 'plays once on the first enter and never again.' + short: 'Play once on first enter only' + state: + full: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`).' + short: 'Play on enter, pause on leave' stateActionDescriptions: - toggle: 'applies the style state on enter, removes on leave. Default.' - add: 'applies the style state on enter. Leave does NOT remove it.' - remove: 'removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`.' - clear: 'clears all previously applied style states on enter. Use to reset multiple stacked `''add''` style changes at once (e.g. a "reset" hover area that undoes several accumulated states).' + toggle: + full: 'applies the style state on enter, removes on leave. Default.' + short: 'Add style state on enter, remove on leave' + add: + full: 'applies the style state on enter. Leave does NOT remove it.' + short: 'Add style state on enter; leave does NOT remove' + remove: + full: 'removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`.' + short: 'Remove style state on enter' + clear: + full: 'clears all previously applied style states on enter. Use to reset multiple stacked `''add''` style changes at once (e.g. a "reset" hover area that undoes several accumulated states).' + short: 'Clear/reset all style states on enter' - name: click template: event-trigger-rule.mjs @@ -37,15 +53,31 @@ triggers: hasReversed: true hasEffectId: true triggerTypeDescriptions: - alternate: 'plays forward on first click, reverses on next click. Default.' - repeat: 'restarts the animation from the beginning on each click.' - once: 'plays once on the first click and never again.' - state: 'resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`).' + alternate: + full: 'plays forward on first click, reverses on next click. Default.' + short: 'Alternate play/reverse per click' + repeat: + full: 'restarts the animation from the beginning on each click.' + short: 'Restart per click' + once: + full: 'plays once on the first click and never again.' + short: 'Play once on first click only' + state: + full: 'resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`).' + short: 'Toggle play/pause per click' stateActionDescriptions: - toggle: 'applies the style state, removes it on next click. Default.' - add: 'applies the style state. Does not remove on subsequent clicks.' - remove: 'removes a previously applied style state.' - clear: 'clears all previously applied style states. Useful for resetting multiple stacked style states at once.' + toggle: + full: 'applies the style state, removes it on next click. Default.' + short: 'Toggle style state per click' + add: + full: 'applies the style state. Does not remove on subsequent clicks.' + short: 'Add style state on click' + remove: + full: 'removes a previously applied style state.' + short: 'Remove style state on click' + clear: + full: 'clears all previously applied style states. Useful for resetting multiple stacked style states at once.' + short: 'Clear/reset all style states' # -------------------------------------------------------------------------- # Viewport/scroll triggers — each has its own dedicated template. @@ -69,6 +101,11 @@ triggers: - id: same-element-viewenter section: short showMultipleEffectsNote: true + triggerTypeDescriptions: + once: 'plays once when the source element first enters the viewport and never again. Source and target may be the same element.' + repeat: 'restarts the animation every time the source element enters the viewport. Use separate source and target.' + alternate: 'plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target.' + state: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`). Use separate source and target.' - name: viewProgress template: viewprogress-rule.mjs diff --git a/packages/interact/_content/fragments/fouc.md b/packages/interact/_content/fragments/fouc.md index b20b1f8e..e2f87038 100644 --- a/packages/interact/_content/fragments/fouc.md +++ b/packages/interact/_content/fragments/fouc.md @@ -1,46 +1,3 @@ - -**Problem:** Elements with entrance animations (e.g. `FadeIn`) start in their final visible state (e.g. `opacity: 1`). Before the animation framework initializes and applies the starting keyframe (e.g. `opacity: 0`), the element is briefly visible at full opacity — a flash of un-animated content. - -**Solution:** Two things are required — **both** MUST be present for FOUC prevention to work: - -1. **Generate critical CSS** using `generate(config)` — produces CSS rules that hide entrance-animated elements from the moment the page renders, before JavaScript runs. -2. **Mark elements with `initial`** — set `data-interact-initial="true"` on ``, or `initial={true}` on the `` React component. This tells the runtime which elements have critical CSS applied. - -If only one of these is present, FOUC prevention will **not** work. Both the CSS and the `initial` attribute are required. - -**Problem:** Elements with entrance animations (e.g. `viewEnter` + `type: 'once'` with `FadeIn`) start in their final visible state. Before the animation framework initializes and applies the starting keyframe (e.g. `opacity: 0`), the element is briefly visible at full opacity — causing a flash of unstyled/un-animated content (FOUC). - -**Solution:** Two things are required — both MUST be present: - -1. **Generate critical CSS** using `generate(config)` — produces CSS rules that hide entrance-animated elements from the moment the page renders. -2. **Mark elements with `initial`** — tells the runtime which elements have critical CSS applied so it can coordinate with the generated styles. - -```typescript -import { generate } from '@wix/interact'; - -const config: InteractConfig = { - interactions: [ - { - key: '[SOURCE_KEY]', - trigger: 'viewEnter', - params: { - threshold: [VIEW_TRIGGER_THRESHOLD], - inset: [VIEW_TRIGGER_INSET], - }, - effects: [EFFECT_DEFINITIONS], - // and/or - sequences: [SEQUENCE_DEFINITIONS], - }, - ], -}; - -const css = generate(config); -``` - -```ts -import { generate } from '@wix/interact/web'; -const css = generate(config); -``` **Append to `` or beginning of ``:** @@ -71,33 +28,3 @@ const css = generate(config); ```html
...
``` - -### Rules - -- `generate()` should be called server-side or at build time. Can also be called on the client if the page content is initially hidden (e.g. behind a loader/splash screen). -- `initial` is only valid for `viewEnter` + `triggerType: 'once'` (or no `triggerType`, which defaults to `'once'`) where source and target are the same element. -- Do NOT use `initial` for `viewEnter` with `triggerType: 'repeat'`/`'alternate'`/`'state'`. For those, manually apply the initial keyframe as inline styles on the target element and use `fill: 'both'`. -- If other interactions in the config also need FOUC prevention, `generate(config)` covers them all — set `initial` only on the relevant `viewEnter` + `triggerType: 'once'` elements. - -**Rules:** - -- `generate()` should be called server-side or at build time. Can also be called on the client if page content is initially hidden (e.g. behind a loader). -- Only valid for `viewEnter` + `triggerType: 'once'` (or no `triggerType`, which defaults to `'once'`) where source and target are the same element. - -### Rules - -- `generate()` should be called server-side or at build time. Can also be called on client-side if page content is initially hidden (e.g. behind a loader/splash screen). -- **Both** `generate(config)` CSS **and** `initial` on the element are required. Using only one has no effect. -- `initial` is only valid for `viewEnter` + `type: 'once'` where source and target are the same element. -- For `repeat`/`alternate`/`state`, do NOT use `initial`. Instead, manually apply the initial keyframe as inline styles on the target element and use `fill: 'both'`. - -**Problem:** Elements with entrance animations (e.g. `FadeIn` on `viewEnter`) are initially visible in their final state. Before the animation framework applies the starting keyframe, the content flashes visibly — a flash of un-animated content (FOUC). - -**Solution:** Two things are required — both MUST be present: - -1. **Generate critical CSS** with `generate(config)` — produces CSS that hides entrance-animated elements until the animation plays. -2. **Mark elements with `initial`** — `data-interact-initial="true"` on ``, or `initial={true}` on `` in React. - -Using only one of these has no effect — both are required. - -See [viewenter.md](./viewenter.md) for full details. diff --git a/packages/interact/_content/fragments/multiple-effects-note.md b/packages/interact/_content/fragments/multiple-effects-note.md index 7a4a4382..b9f33146 100644 --- a/packages/interact/_content/fragments/multiple-effects-note.md +++ b/packages/interact/_content/fragments/multiple-effects-note.md @@ -1,2 +1,8 @@ **Multiple effects:** The `effects` array can contain multiple effects — all share the same {{triggerName}} trigger and fire together. Use this to animate different targets from a single {{triggerEvent}}. + +**Multiple effects:** The `effects` array can contain multiple effects — all share the same viewEnter trigger and fire together when the element enters the viewport. Each effect can have its own `triggerType`. Use this to animate different targets from a single viewport entry event. + +**Multiple effects:** The `effects` array can contain multiple effects — all are driven by the same scroll progress. Use this to animate different targets or properties in sync with the same scroll position. + +**Multiple effects:** The `effects` array can contain multiple effects — all share the same pointer tracking and fire together. Use this to animate different targets from the same pointer movement. diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 1613b9aa..00b87ae9 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -155,7 +155,7 @@ Use \`transition\` when all properties share timing. Use \`transitionProperties\ - \`[SOURCE_KEY]\` / \`[TARGET_KEY]\` — same as Rule 1. - \`[STATE_ACTION]\` — \`stateAction\` on the effect. One of: ${Object.entries(trigger.stateActionDescriptions) - .map(([k, v]) => ` - \`'${k}'\` — ${v}`) + .map(([k, v]) => ` - \`'${k}'\` — ${v.full}`) .join('\n')} - \`[CSS_PROP]\` — CSS property name as a string in camelCase format (e.g. \`'backgroundColor'\`, \`'borderRadius'\`, \`'opacity'\`). - \`[VALUE]\` — target CSS value for the property. @@ -231,7 +231,7 @@ function buildVariables(trigger, vo, hasReversed, hasEffectId) { varLine('SOURCE_KEY', vo.sourceKeySuffix), varLine('TARGET_KEY', vo.targetKeyDesc), `- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of:`, - ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`), + ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v.full}`), varLine('KEYFRAMES'), varLine('EFFECT_NAME'), varLine('NAMED_EFFECT_DEFINITION'), diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 64cc56d7..dbad892b 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -7,46 +7,15 @@ const FULL_LEAN_PITFALL_ORDER = [ { id: 'hit-area', section: 'detailed-pointermove' }, ]; -const FULL_LEAN_BEHAVIOR = { - hover: { - triggerType: { - alternate: 'Play on enter, reverse on leave', - repeat: 'Play on enter, stop and rewind on leave', - once: 'Play once on first enter only', - state: 'Play on enter, pause on leave', - }, - stateAction: { - toggle: 'Add style state on enter, remove on leave', - add: 'Add style state on enter; leave does NOT remove', - remove: 'Remove style state on enter', - clear: 'Clear/reset all style states on enter', - }, - }, - click: { - triggerType: { - alternate: 'Alternate play/reverse per click', - repeat: 'Restart per click', - once: 'Play once on first click only', - state: 'Toggle play/pause per click', - }, - stateAction: { - toggle: 'Toggle style state per click', - add: 'Add style state on click', - remove: 'Remove style state on click', - clear: 'Clear/reset all style states', - }, - }, -}; - -function buildBehaviorTable(headerLabel, behaviorKey) { - const hoverBehavior = FULL_LEAN_BEHAVIOR.hover[behaviorKey]; - const clickBehavior = FULL_LEAN_BEHAVIOR.click[behaviorKey]; - const keys = Object.keys(hoverBehavior); +function buildBehaviorTable(headerLabel, behaviorKey, hover, click) { + const hoverDescs = hover[behaviorKey]; + const clickDescs = click[behaviorKey]; + const keys = Object.keys(hoverDescs); const defaultKey = keys[0]; const rows = keys.map((k) => { const label = k === defaultKey ? `\`'${k}'\` (default)` : `\`'${k}'\``; - return [label, hoverBehavior[k], clickBehavior[k]]; + return [label, hoverDescs[k].short, clickDescs[k].short]; }); return buildMarkdownTable([headerLabel, 'hover behavior', 'click behavior'], rows); @@ -262,11 +231,11 @@ For \`TimeEffect\` (keyframe/named/custom effects), set \`triggerType\` on the e **\`triggerType\`** — on \`TimeEffect\`: -${buildBehaviorTable('Type', 'triggerType')} +${buildBehaviorTable('Type', 'triggerTypeDescriptions', hover, click)} **\`stateAction\`** — on \`StateEffect\`: -${buildBehaviorTable('Action', 'stateAction')} +${buildBehaviorTable('Action', 'stateActionDescriptions', hover, click)} ### viewEnter @@ -308,14 +277,7 @@ params: { **\`centeredToTarget\`** — set \`true\` to remap the \`0–1\` progress range so that \`0.5\` progress corresponds to the center of the target element. Use when source and target are different elements, or when \`hitArea: 'root'\` is used, so that the pointer resting over the target center produces 50% progress regardless of position in viewport. -**Progress object** (for \`customEffect\`): - -\`\`\`ts -{ x: number; y: number; v?: { x: number; y: number }; active?: boolean } -// x, y: 0–1 normalized position within hit area -// v: velocity vector (unbounded, typically -1 to 1 range at moderate speed; 0 = stationary) -// active: whether pointer is within the active hit area -\`\`\` +${fragments.get('progress-type', 'brief')} ### animationEnd @@ -510,13 +472,21 @@ ${fragments.get('conditions', 'default')} ## FOUC Prevention -${fragments.get('fouc', 'long')} +**Problem:** Elements with entrance animations (e.g. \`viewEnter\` + \`type: 'once'\` with \`FadeIn\`) start in their final visible state. Before the animation framework initializes and applies the starting keyframe (e.g. \`opacity: 0\`), the element is briefly visible at full opacity — causing a flash of unstyled/un-animated content (FOUC). + +**Solution:** Two things are required — both MUST be present: + +1. **Generate critical CSS** using \`generate(config)\` — produces CSS rules that hide entrance-animated elements from the moment the page renders. +2. **Mark elements with \`initial\`** — tells the runtime which elements have critical CSS applied so it can coordinate with the generated styles. ### Step 1: Generate CSS Call \`generate(config)\` server-side or at build time and inject the result into the \`\` (preferred), or insert to beginning of \`\`, so it loads before the page content is painted: -${fragments.get('fouc', 'code-generate-web')} +\`\`\`ts +import { generate } from '@wix/interact/web'; +const css = generate(config); +\`\`\` ${fragments.get('fouc', 'code-inject')} @@ -528,7 +498,12 @@ ${fragments.get('fouc', 'code-react', { key: 'hero', classAttr: ' className="her ${fragments.get('fouc', 'code-vanilla', { key: 'hero', classAttr: ' class="hero"' })} -${fragments.get('fouc', 'rules-detailed')} +### Rules + +- \`generate()\` should be called server-side or at build time. Can also be called on client-side if page content is initially hidden (e.g. behind a loader/splash screen). +- **Both** \`generate(config)\` CSS **and** \`initial\` on the element are required. Using only one has no effect. +- \`initial\` is only valid for \`viewEnter\` + \`type: 'once'\` where source and target are the same element. +- For \`repeat\`/\`alternate\`/\`state\`, do NOT use \`initial\`. Instead, manually apply the initial keyframe as inline styles on the target element and use \`fill: 'both'\`. --- diff --git a/packages/interact/_content/templates/integration.mjs b/packages/interact/_content/templates/integration.mjs index 77662576..49eda837 100644 --- a/packages/interact/_content/templates/integration.mjs +++ b/packages/interact/_content/templates/integration.mjs @@ -191,9 +191,21 @@ ${fragments.get('sequences', 'brief')} ## Critical CSS (FOUC Prevention) -${fragments.get('fouc', 'intro-brief')} +**Problem:** Elements with entrance animations (e.g. \`FadeIn\` on \`viewEnter\`) are initially visible in their final state. Before the animation framework applies the starting keyframe, the content flashes visibly — a flash of un-animated content (FOUC). -${fragments.get('fouc', 'rules-brief')} +**Solution:** Two things are required — both MUST be present: + +1. **Generate critical CSS** with \`generate(config)\` — produces CSS that hides entrance-animated elements until the animation plays. +2. **Mark elements with \`initial\`** — \`data-interact-initial="true"\` on \`\`, or \`initial={true}\` on \`\` in React. + +Using only one of these has no effect — both are required. + +See [viewenter.md](./viewenter.md) for full details. + +**Rules:** + +- \`generate()\` should be called server-side or at build time. Can also be called on the client if page content is initially hidden (e.g. behind a loader). +- Only valid for \`viewEnter\` + \`triggerType: 'once'\` (or no \`triggerType\`, which defaults to \`'once'\`) where source and target are the same element. \`\`\`javascript import { generate } from '${data.meta.entryPoints.web}'; diff --git a/packages/interact/_content/templates/pointermove-rule.mjs b/packages/interact/_content/templates/pointermove-rule.mjs index fc5fefea..4dac9e72 100644 --- a/packages/interact/_content/templates/pointermove-rule.mjs +++ b/packages/interact/_content/templates/pointermove-rule.mjs @@ -60,19 +60,7 @@ ${paramsTypeFields} ## Progress Object Structure -When using \`customEffect\` with \`pointerMove\`, the progress parameter is an object: - -\`\`\`typescript -type Progress = { - x: number; // 0-1: horizontal position (0 = left edge, 1 = right edge) - y: number; // 0-1: vertical position (0 = top edge, 1 = bottom edge) - v?: { - x: number; // Horizontal velocity: negative = moving left, positive = moving right. Magnitude reflects speed. - y: number; // Vertical velocity: negative = moving up, positive = moving down. Magnitude reflects speed. - }; - active?: boolean; // Whether mouse is currently in the hit area -}; -\`\`\` +${fragments.get('progress-type', 'detailed')} --- @@ -114,7 +102,7 @@ For devices with dynamic viewport sizes (e.g. mobile browsers where the address Use pre-built mouse presets from \`${data.meta.presetsPackage}\` that handle 2D mouse tracking internally. Mouse presets are preferred over \`keyframeEffect\` for 2D effects. Available mouse presets: ${data.effects.presets.mouse.map((n) => `\`${n}\``).join(', ')}. -**Multiple effects:** The \`effects\` array can contain multiple effects — all share the same pointer tracking and fire together. Use this to animate different targets from the same pointer movement. +${fragments.get('multiple-effects-note', 'pointerMove')} \`\`\`typescript { diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index 4dd9ac91..617eb1b4 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -35,13 +35,40 @@ ${pitfallsBlock} ## Preventing Flash of Unstyled Content (FOUC) -${fragments.get('fouc', 'short')} +**Problem:** Elements with entrance animations (e.g. \`FadeIn\`) start in their final visible state (e.g. \`opacity: 1\`). Before the animation framework initializes and applies the starting keyframe (e.g. \`opacity: 0\`), the element is briefly visible at full opacity — a flash of un-animated content. + +**Solution:** Two things are required — **both** MUST be present for FOUC prevention to work: + +1. **Generate critical CSS** using \`generate(config)\` — produces CSS rules that hide entrance-animated elements from the moment the page renders, before JavaScript runs. +2. **Mark elements with \`initial\`** — set \`data-interact-initial="true"\` on \`\`, or \`initial={true}\` on the \`\` React component. This tells the runtime which elements have critical CSS applied. + +If only one of these is present, FOUC prevention will **not** work. Both the CSS and the \`initial\` attribute are required. ### Step 1: Generate CSS and inject into \`\` (preferred), or beginning of \`\` Call \`generate(config)\` server-side or at build time. Inject the resulting CSS into the document \`\` (or in \`\` before your content) so it loads before the page content is painted: -${fragments.get('fouc', 'code-generate-viewenter')} +\`\`\`typescript +import { generate } from '@wix/interact'; + +const config: InteractConfig = { + interactions: [ + { + key: '[SOURCE_KEY]', + trigger: 'viewEnter', + params: { + threshold: [VIEW_TRIGGER_THRESHOLD], + inset: [VIEW_TRIGGER_INSET], + }, + effects: [EFFECT_DEFINITIONS], + // and/or + sequences: [SEQUENCE_DEFINITIONS], + }, + ], +}; + +const css = generate(config); +\`\`\` ${fragments.get('fouc', 'code-inject')} @@ -53,13 +80,18 @@ ${fragments.get('fouc', 'code-react', { key: '[SOURCE_KEY]', classAttr: '' })} ${fragments.get('fouc', 'code-vanilla', { key: '[SOURCE_KEY]', classAttr: '' })} -${fragments.get('fouc', 'rules-viewenter')} +### Rules + +- \`generate()\` should be called server-side or at build time. Can also be called on the client if the page content is initially hidden (e.g. behind a loader/splash screen). +- \`initial\` is only valid for \`viewEnter\` + \`triggerType: 'once'\` (or no \`triggerType\`, which defaults to \`'once'\`) where source and target are the same element. +- Do NOT use \`initial\` for \`viewEnter\` with \`triggerType: 'repeat'\`/\`'alternate'\`/\`'state'\`. For those, manually apply the initial keyframe as inline styles on the target element and use \`fill: 'both'\`. +- If other interactions in the config also need FOUC prevention, \`generate(config)\` covers them all — set \`initial\` only on the relevant \`viewEnter\` + \`triggerType: 'once'\` elements. ## Rule 1: keyframeEffect / namedEffect (TimeEffect) Use \`keyframeEffect\` or \`namedEffect\` when the viewEnter should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior. Use \`params\` only for observer configuration (\`threshold\`, \`inset\`). -**Multiple effects:** The \`effects\` array can contain multiple effects — all share the same viewEnter trigger and fire together when the element enters the viewport. Each effect can have its own \`triggerType\`. Use this to animate different targets from a single viewport entry event. +${fragments.get('multiple-effects-note', 'viewEnter')} \`\`\`typescript { @@ -102,10 +134,12 @@ ${varLine('SOURCE_KEY', 'The **source element** is observed for viewport interse ${varLine('TARGET_KEY', "identifier matching the element's key on the element that animates.")} - \`[TARGET_SELECTOR]\` - optional. Selector for the child element to select inside the root element. For \`triggerType\` of \`'alternate'\`/\`'repeat'\`/\`'state'\` MUST either use a separate \`[TARGET_KEY]\` from \`[SOURCE_KEY]\` or \`selector\` for selecting a child element as target. - \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of: - - \`'once'\` (default) — plays once when the source element first enters the viewport and never again. Source and target may be the same element. - - \`'repeat'\` — restarts the animation every time the source element enters the viewport. Use separate source and target. - - \`'alternate'\` — plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target. - - \`'state'\` — resumes on enter, pauses on leave. Useful for continuous loops (\`iterations: Infinity\`). Use separate source and target. +${Object.entries(trigger.triggerTypeDescriptions) + .map(([k, v]) => { + const isDefault = k === trigger.defaultTriggerType; + return ` - \`'${k}'\`${isDefault ? ' (default)' : ''} — ${v}`; + }) + .join('\n')} ${paramDescriptions} ${varLine('KEYFRAMES')} ${varLine('EFFECT_NAME')} diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index e3be63cf..07fdcc80 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -34,7 +34,7 @@ ${pitfallsBlock} **Use Case**: Scroll-driven CSS-based effects. -**Multiple effects:** The \`effects\` array can contain multiple effects — all are driven by the same scroll progress. Use this to animate different targets or properties in sync with the same scroll position. +${fragments.get('multiple-effects-note', 'viewProgress')} ### Template diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index 3d3471ba..f8313eb3 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -29,6 +29,36 @@ const data = { meta: metaData, }; +// --------------------------------------------------------------------------- +// 1b. Validate YAML schema +// --------------------------------------------------------------------------- + +const TRIGGER_SCHEMA = { + 'event-trigger-rule.mjs': [ + 'a11yAlias', + 'a11yNote', + 'hasReversed', + 'hasEffectId', + 'triggerTypeDescriptions', + 'stateActionDescriptions', + ], + 'viewenter-rule.mjs': ['params', 'pitfalls', 'triggerTypeDescriptions'], + 'viewprogress-rule.mjs': ['params', 'pitfalls'], + 'pointermove-rule.mjs': ['params', 'pitfalls'], +}; + +for (const trigger of data.triggers) { + const required = TRIGGER_SCHEMA[trigger.template]; + if (!required) continue; + for (const field of required) { + if (trigger[field] === undefined) { + throw new Error( + `triggers.yaml: trigger "${trigger.name}" is missing required field "${field}" (needed by ${trigger.template})`, + ); + } + } +} + // --------------------------------------------------------------------------- // 2. Load fragments — parse markers // --------------------------------------------------------------------------- From 7cb8d82b4975b195cc1bedefb7487045bb003420 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 12:40:05 +0300 Subject: [PATCH 16/27] PR fixes --- .../rules_build_pipeline_ffe2a9f3.plan.md | 94 +++++++++++++++++++ packages/interact/_content/data/triggers.yaml | 16 +++- .../interact/_content/fragments/README.md | 17 ++++ .../fragments/multiple-effects-note.md | 4 +- .../_content/fragments/progress-type.md | 23 +++++ .../_content/templates/event-trigger-rule.mjs | 22 +++-- .../interact/_content/templates/full-lean.mjs | 15 ++- .../_content/templates/viewenter-rule.mjs | 4 +- packages/interact/scripts/build-rules.mjs | 32 ++++++- 9 files changed, 205 insertions(+), 22 deletions(-) create mode 100644 packages/interact/_content/fragments/README.md create mode 100644 packages/interact/_content/fragments/progress-type.md diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md index e9833313..5d711723 100644 --- a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md +++ b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md @@ -200,6 +200,36 @@ todos: - id: refactor8-regenerate content: 'Regenerate rules/*.md — all 7 files byte-identical to pre-refactor output. Zero content drift.' status: completed + - id: review2-missing-files + content: 'Add missing progress-type.md and README.md fragments to git — build fails without progress-type.md (referenced by pointermove-rule.mjs and full-lean.mjs)' + status: completed + - id: review2-schema-consistency + content: 'Make viewEnter triggerTypeDescriptions use { full, short } objects matching hover/click — uniform schema prevents runtime errors when new templates read .full' + status: completed + - id: review2-default-key + content: 'Fix buildBehaviorTable to accept explicit defaultKey parameter instead of relying on YAML key insertion order — pass defaultTriggerType for triggerType table, hardcode toggle for stateAction table' + status: completed + - id: review2-multiple-effects + content: 'Consolidate multiple-effects-note.md #viewEnter section into #default via {{triggerContext}} and {{extraNote}} params — reduces from 4 to 3 sections' + status: completed + - id: review2-full-lean-comment + content: 'Add JSDoc comment to full-lean.mjs explaining why static prose sections are kept inline rather than extracted to fragments (single consumer, no deduplication benefit)' + status: completed + - id: review2-deeper-validation + content: 'Add deeper YAML schema validation in build-rules.mjs: check pitfalls[].id exists, params[].name exists, triggerTypeDescriptions values are { full } objects' + status: completed + - id: review2-variable-overrides + content: 'Simplify VARIABLE_OVERRIDES in event-trigger-rule.mjs with shared VARIABLE_OVERRIDES_BASE — per-trigger objects only override what differs' + status: completed + - id: review2-empty-catch + content: 'Fix empty catch block in build-rules.mjs --check mode to only catch ENOENT, rethrow unexpected errors' + status: completed + - id: review2-readme-filter + content: 'Filter README.md from fragment loader to prevent accidental inclusion as fragment data' + status: completed + - id: review2-regenerate + content: 'Regenerate rules/*.md — all 7 files byte-identical to pre-refactor output. Zero content drift.' + status: completed isProject: false --- @@ -949,6 +979,70 @@ Ran `build:rules`. All 7 rule files are byte-identical to pre-refactor output. Z --- +## Post-PR Fixes — Round 9 (review hardening) + +Fixes from a structural review addressing a build-breaking bug, schema inconsistencies, fragile assumptions, and missing safety checks. + +### Fix 54: Add missing fragment files + +`progress-type.md` and `README.md` were created locally but never committed to the branch. `progress-type.md` is referenced by `pointermove-rule.mjs` and `full-lean.mjs` — the build throws `Fragment not found: progress-type` without it. + +**Action:** Staged both files via `git add`. + +### Fix 55: Uniform `triggerTypeDescriptions` schema + +viewEnter's `triggerTypeDescriptions` used flat strings while hover/click used `{ full, short }` objects. A new template reading `.full` on a viewEnter description would crash at runtime. + +**Action:** Changed viewEnter to `{ full, short }` objects. Updated `viewenter-rule.mjs` to read `.full`. Added schema validation in `build-rules.mjs` that enforces all `triggerTypeDescriptions` values must be objects with a `full` key. + +### Fix 56: Fix `buildBehaviorTable` default key derivation + +`buildBehaviorTable` assumed the first YAML key was the default (`const defaultKey = keys[0]`). Reordering keys in `triggers.yaml` would move the "(default)" annotation to the wrong row. + +**Action:** Added explicit `{ defaultKey }` option parameter. Callers pass `hover.defaultTriggerType` for the triggerType table and `'toggle'` for the stateAction table. No key-order dependency. + +### Fix 57: Consolidate `multiple-effects-note.md` sections + +The `#viewEnter` section was nearly identical to `#default` — only adding "when the element enters the viewport" and "Each effect can have its own `triggerType`." The `#viewProgress` and `#pointerMove` sections have meaningfully different sentence structures and remain separate. + +**Action:** Removed `#viewEnter` section. Added `{{triggerContext}}` and `{{extraNote}}` params to `#default`. Updated callers: event triggers pass empty strings, viewEnter passes context and note. Fragment reduced from 4 to 3 sections. + +### Fix 58: Document static prose design choice in `full-lean.mjs` + +`full-lean.mjs` is 524 lines with ~400 lines of static prose. Without context, this looks like content that should be extracted to fragments. + +**Action:** Added JSDoc explaining that single-consumer static sections are intentionally kept inline — they change together with surrounding template logic and extraction would add indirection without deduplication. + +### Fix 59: Deeper YAML schema validation + +Schema validation only checked top-level field existence. Missing `pitfalls[].id`, `params[].name`, or flat-string `triggerTypeDescriptions` values would pass validation but fail at render time. + +**Action:** Added checks for `pitfalls[].id`, `params[].name`, and `triggerTypeDescriptions` value shape (`typeof === 'object'` with `full` key). + +### Fix 60: Simplify `VARIABLE_OVERRIDES` with shared base + +The hover and click override objects had 9 fields each, most sharing the same value. Shared defaults were duplicated. + +**Action:** Extracted `VARIABLE_OVERRIDES_BASE` with common defaults. Per-trigger objects spread the base and only override what differs. + +### Fix 61: Fix empty catch block + +`build-rules.mjs` `--check` mode used `catch {}` when reading existing files. This silently swallowed all errors, not just missing files. + +**Action:** Changed to `catch (err) { if (err.code !== 'ENOENT') throw err; }`. + +### Fix 62: Filter `README.md` from fragment loader + +The recursive fragment loader picked up every `.md` file in the fragments directory, including `README.md`. This loaded documentation as fragment data under the key `"README"` — harmless but unintentional. + +**Action:** Added `entry.name !== 'README.md'` filter to `_loadDir`. + +### Fix 63: Regenerate output (round 9) + +Ran `build:rules`. All 7 rule files are byte-identical to pre-refactor output. Zero content drift. + +--- + ## Future Extension Points - **docs/**: Same `_content/data/` feeds docs templates — add `_content/templates/docs/` later diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml index 2ca2d48b..99ebab63 100644 --- a/packages/interact/_content/data/triggers.yaml +++ b/packages/interact/_content/data/triggers.yaml @@ -102,10 +102,18 @@ triggers: section: short showMultipleEffectsNote: true triggerTypeDescriptions: - once: 'plays once when the source element first enters the viewport and never again. Source and target may be the same element.' - repeat: 'restarts the animation every time the source element enters the viewport. Use separate source and target.' - alternate: 'plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target.' - state: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`). Use separate source and target.' + once: + full: 'plays once when the source element first enters the viewport and never again. Source and target may be the same element.' + short: 'Play once on first enter only' + repeat: + full: 'restarts the animation every time the source element enters the viewport. Use separate source and target.' + short: 'Restart on each viewport enter' + alternate: + full: 'plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target.' + short: 'Play on enter, reverse on leave' + state: + full: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`). Use separate source and target.' + short: 'Play on enter, pause on leave' - name: viewProgress template: viewprogress-rule.mjs diff --git a/packages/interact/_content/fragments/README.md b/packages/interact/_content/fragments/README.md new file mode 100644 index 00000000..35a2f622 --- /dev/null +++ b/packages/interact/_content/fragments/README.md @@ -0,0 +1,17 @@ +# Fragments + +Reusable markdown content with `` markers for granular inclusion at different detail levels. + +## Section Naming Conventions + +| Pattern | When to use | Example | +| :------------------------- | :------------------------------------------------------------- | :--------------------------- | +| `#default` | Fragment has only one section | `pitfalls/perspective.md` | +| `#brief` / `#detailed` | Two detail levels of the same content | `config-structure.md` | +| `#short` / `#long` | Short and long versions of the same concept | `fouc.md`, `overflow-clip.md`| +| `#` | Trigger-specific variant (e.g. `hover`, `viewEnter`) | `hit-area.md`, `multiple-effects-note.md` | +| `#full-lean` | Condensed wording used by the full-lean reference | `overflow-clip.md` | +| `#code-` | Code example variant (e.g. `code-web`, `code-react`) | `fouc.md`, `quick-start.md` | +| `#` / `#-` | Named concept sections | `element-resolution.md` | + +When adding a new section, follow the closest existing pattern. Prefer descriptive names over generic ones when the section has trigger-specific or template-specific content. diff --git a/packages/interact/_content/fragments/multiple-effects-note.md b/packages/interact/_content/fragments/multiple-effects-note.md index b9f33146..1e53dce1 100644 --- a/packages/interact/_content/fragments/multiple-effects-note.md +++ b/packages/interact/_content/fragments/multiple-effects-note.md @@ -1,7 +1,5 @@ -**Multiple effects:** The `effects` array can contain multiple effects — all share the same {{triggerName}} trigger and fire together. Use this to animate different targets from a single {{triggerEvent}}. - -**Multiple effects:** The `effects` array can contain multiple effects — all share the same viewEnter trigger and fire together when the element enters the viewport. Each effect can have its own `triggerType`. Use this to animate different targets from a single viewport entry event. +**Multiple effects:** The `effects` array can contain multiple effects — all share the same {{triggerName}} trigger and fire together{{triggerContext}}.{{extraNote}} Use this to animate different targets from a single {{triggerEvent}}. **Multiple effects:** The `effects` array can contain multiple effects — all are driven by the same scroll progress. Use this to animate different targets or properties in sync with the same scroll position. diff --git a/packages/interact/_content/fragments/progress-type.md b/packages/interact/_content/fragments/progress-type.md new file mode 100644 index 00000000..c9915bfa --- /dev/null +++ b/packages/interact/_content/fragments/progress-type.md @@ -0,0 +1,23 @@ + +When using `customEffect` with `pointerMove`, the progress parameter is an object: + +```typescript +type Progress = { + x: number; // 0-1: horizontal position (0 = left edge, 1 = right edge) + y: number; // 0-1: vertical position (0 = top edge, 1 = bottom edge) + v?: { + x: number; // Horizontal velocity: negative = moving left, positive = moving right. Magnitude reflects speed. + y: number; // Vertical velocity: negative = moving up, positive = moving down. Magnitude reflects speed. + }; + active?: boolean; // Whether mouse is currently in the hit area +}; +``` + +**Progress object** (for `customEffect`): + +```ts +{ x: number; y: number; v?: { x: number; y: number }; active?: boolean } +// x, y: 0–1 normalized position within hit area +// v: velocity vector (unbounded, typically -1 to 1 range at moderate speed; 0 = stationary) +// active: whether pointer is within the active hit area +``` diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 00b87ae9..0097d7b0 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,7 +1,21 @@ import { capitalize, buildPitfallsBlock, varLine } from './_helpers.mjs'; +const VARIABLE_OVERRIDES_BASE = { + sourceKeySuffix: '', + targetKeyDesc: + "identifier matching the element's key on the element that animates.", + fillModeDesc: '', + easingDesc: 'CSS easing string, or named easing from `@wix/motion`.', + iterationsDesc: 'optional. Number of iterations, or `Infinity` for continuous loops.', + fillCritical: '', + customEffectExamples: '', + offsetEasingSuffix: '', + alternateBoolSuffix: '', +}; + const VARIABLE_OVERRIDES = { hover: { + ...VARIABLE_OVERRIDES_BASE, sourceKeySuffix: 'The element that listens for hover.', targetKeyDesc: "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", @@ -13,22 +27,18 @@ const VARIABLE_OVERRIDES = { "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", - customEffectExamples: '', offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.', - alternateBoolSuffix: '', }, click: { + ...VARIABLE_OVERRIDES_BASE, sourceKeySuffix: 'The element that listens for clicks.', targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", - easingDesc: 'CSS easing string, or named easing from `@wix/motion`.', - iterationsDesc: 'optional. Number of iterations, or `Infinity` for continuous loops.', fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.", customEffectExamples: ', randomized behavior', - offsetEasingSuffix: '', alternateBoolSuffix: "Different from `triggerType: 'alternate'` which alternates per click.", }, }; @@ -49,7 +59,7 @@ export function render(data, fragments) { const pitfallsBlock = buildPitfallsBlock(trigger, fragments, { wrapped: true }); const multipleEffectsNote = trigger.showMultipleEffectsNote - ? `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event` })}\n` + ? `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event`, triggerContext: '', extraNote: '' })}\n` : ''; return `# ${Name} Trigger Rules for ${data.meta.packageName} diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index dbad892b..d1208f8c 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -7,14 +7,14 @@ const FULL_LEAN_PITFALL_ORDER = [ { id: 'hit-area', section: 'detailed-pointermove' }, ]; -function buildBehaviorTable(headerLabel, behaviorKey, hover, click) { +function buildBehaviorTable(headerLabel, behaviorKey, hover, click, { defaultKey } = {}) { const hoverDescs = hover[behaviorKey]; const clickDescs = click[behaviorKey]; const keys = Object.keys(hoverDescs); - const defaultKey = keys[0]; const rows = keys.map((k) => { - const label = k === defaultKey ? `\`'${k}'\` (default)` : `\`'${k}'\``; + const isDefault = defaultKey ? k === defaultKey : false; + const label = isDefault ? `\`'${k}'\` (default)` : `\`'${k}'\``; return [label, hoverDescs[k].short, clickDescs[k].short]; }); @@ -27,6 +27,11 @@ function buildFullLeanPitfalls(pitfallOrder, fragments) { /** * Renders full-lean.md — the comprehensive reference for all triggers, effects, and API surface. + * + * Large static prose sections (Element Binding, Interactions, Effects, FOUC Prevention) are kept + * inline rather than extracted to fragments. They have exactly one consumer and change together + * with the surrounding template logic — extracting them would add indirection without deduplication. + * * @param {{ triggers: object[], effects: object, meta: object }} data — no `trigger`; receives the full data object * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ @@ -231,11 +236,11 @@ For \`TimeEffect\` (keyframe/named/custom effects), set \`triggerType\` on the e **\`triggerType\`** — on \`TimeEffect\`: -${buildBehaviorTable('Type', 'triggerTypeDescriptions', hover, click)} +${buildBehaviorTable('Type', 'triggerTypeDescriptions', hover, click, { defaultKey: hover.defaultTriggerType })} **\`stateAction\`** — on \`StateEffect\`: -${buildBehaviorTable('Action', 'stateActionDescriptions', hover, click)} +${buildBehaviorTable('Action', 'stateActionDescriptions', hover, click, { defaultKey: 'toggle' })} ### viewEnter diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index 617eb1b4..50b76980 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -91,7 +91,7 @@ ${fragments.get('fouc', 'code-vanilla', { key: '[SOURCE_KEY]', classAttr: '' })} Use \`keyframeEffect\` or \`namedEffect\` when the viewEnter should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior. Use \`params\` only for observer configuration (\`threshold\`, \`inset\`). -${fragments.get('multiple-effects-note', 'viewEnter')} +${fragments.get('multiple-effects-note', 'default', { triggerName: 'viewEnter', triggerEvent: 'viewport entry event', triggerContext: ' when the element enters the viewport', extraNote: " Each effect can have its own `triggerType`." })} \`\`\`typescript { @@ -137,7 +137,7 @@ ${varLine('TARGET_KEY', "identifier matching the element's key on the element th ${Object.entries(trigger.triggerTypeDescriptions) .map(([k, v]) => { const isDefault = k === trigger.defaultTriggerType; - return ` - \`'${k}'\`${isDefault ? ' (default)' : ''} — ${v}`; + return ` - \`'${k}'\`${isDefault ? ' (default)' : ''} — ${v.full}`; }) .join('\n')} ${paramDescriptions} diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index f8313eb3..3197f376 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -57,6 +57,32 @@ for (const trigger of data.triggers) { ); } } + + if (trigger.pitfalls) { + for (const p of trigger.pitfalls) { + if (!p.id) { + throw new Error(`triggers.yaml: trigger "${trigger.name}" has a pitfall entry missing "id"`); + } + } + } + + if (trigger.params) { + for (const p of trigger.params) { + if (!p.name) { + throw new Error(`triggers.yaml: trigger "${trigger.name}" has a param entry missing "name"`); + } + } + } + + if (trigger.triggerTypeDescriptions) { + for (const [key, val] of Object.entries(trigger.triggerTypeDescriptions)) { + if (typeof val !== 'object' || !val.full) { + throw new Error( + `triggers.yaml: trigger "${trigger.name}".triggerTypeDescriptions.${key} must be an object with a "full" key`, + ); + } + } + } } // --------------------------------------------------------------------------- @@ -73,7 +99,7 @@ class Fragments { for (const entry of readdirSync(dir, { withFileTypes: true })) { if (entry.isDirectory()) { this._loadDir(join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name); - } else if (entry.name.endsWith('.md')) { + } else if (entry.name.endsWith('.md') && entry.name !== 'README.md') { const key = prefix ? `${prefix}/${basename(entry.name, '.md')}` : basename(entry.name, '.md'); @@ -186,7 +212,9 @@ for (const { file, content } of outputs) { let existing = ''; try { existing = readFileSync(outPath, 'utf8'); - } catch {} + } catch (err) { + if (err.code !== 'ENOENT') throw err; + } if (existing !== content) { console.error(` ✗ ${relative(PKG_ROOT, outPath)} is stale`); stale++; From 8e05bce000e8f090970af82c4205ca0cff6b9745 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 12:51:18 +0300 Subject: [PATCH 17/27] PR fixes --- packages/interact/_content/data/effects.yaml | 6 -- .../interact/_content/templates/_helpers.mjs | 6 +- .../_content/templates/event-trigger-rule.mjs | 22 +---- .../interact/_content/templates/full-lean.mjs | 93 ++----------------- .../_content/templates/viewenter-rule.mjs | 2 +- packages/interact/scripts/build-rules.mjs | 33 +++++-- 6 files changed, 42 insertions(+), 120 deletions(-) diff --git a/packages/interact/_content/data/effects.yaml b/packages/interact/_content/data/effects.yaml index df49aacb..7c50407d 100644 --- a/packages/interact/_content/data/effects.yaml +++ b/packages/interact/_content/data/effects.yaml @@ -4,12 +4,6 @@ triggerTypes: - alternate - state -stateActions: - - toggle - - add - - remove - - clear - easings: - linear - ease diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs index 9006c16c..ce507925 100644 --- a/packages/interact/_content/templates/_helpers.mjs +++ b/packages/interact/_content/templates/_helpers.mjs @@ -22,9 +22,7 @@ export function buildPitfallsBlock(trigger, fragments, { wrapped = false } = {}) * @param {string[][]} rows — array of rows, each an array of cell strings */ export function buildMarkdownTable(headers, rows) { - const widths = headers.map((h, i) => - Math.max(h.length, ...rows.map((r) => (r[i] || '').length)), - ); + const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || '').length))); return [ `| ${headers.map((h, i) => h.padEnd(widths[i])).join(' | ')} |`, `| ${widths.map((w) => `:${'-'.repeat(w - 1)}`).join(' | ')} |`, @@ -102,7 +100,7 @@ export function varLine(name, extra) { const v = COMMON_VARS[name]; if (!v) throw new Error(`Unknown common variable: ${name}`); let desc = v.base; - if (extra !== undefined && v.mode) { + if (extra && v.mode) { desc = v.mode === 'suffix' ? `${v.base} ${extra}` : extra; } return `- \`[${name}]\` — ${desc}`; diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 0097d7b0..7c6e1152 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,21 +1,7 @@ import { capitalize, buildPitfallsBlock, varLine } from './_helpers.mjs'; -const VARIABLE_OVERRIDES_BASE = { - sourceKeySuffix: '', - targetKeyDesc: - "identifier matching the element's key on the element that animates.", - fillModeDesc: '', - easingDesc: 'CSS easing string, or named easing from `@wix/motion`.', - iterationsDesc: 'optional. Number of iterations, or `Infinity` for continuous loops.', - fillCritical: '', - customEffectExamples: '', - offsetEasingSuffix: '', - alternateBoolSuffix: '', -}; - const VARIABLE_OVERRIDES = { hover: { - ...VARIABLE_OVERRIDES_BASE, sourceKeySuffix: 'The element that listens for hover.', targetKeyDesc: "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", @@ -30,12 +16,12 @@ const VARIABLE_OVERRIDES = { offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.', }, click: { - ...VARIABLE_OVERRIDES_BASE, sourceKeySuffix: 'The element that listens for clicks.', targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", + easingDesc: 'CSS easing string, or named easing from `@wix/motion`.', fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.", customEffectExamples: ', randomized behavior', @@ -241,7 +227,9 @@ function buildVariables(trigger, vo, hasReversed, hasEffectId) { varLine('SOURCE_KEY', vo.sourceKeySuffix), varLine('TARGET_KEY', vo.targetKeyDesc), `- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of:`, - ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v.full}`), + ...Object.entries(trigger.triggerTypeDescriptions).map( + ([k, v]) => ` - \`'${k}'\` — ${v.full}`, + ), varLine('KEYFRAMES'), varLine('EFFECT_NAME'), varLine('NAMED_EFFECT_DEFINITION'), @@ -257,7 +245,7 @@ function buildVariables(trigger, vo, hasReversed, hasEffectId) { varLine('EASING_FUNCTION', vo.easingDesc), varLine('DELAY_MS'), varLine('ITERATIONS', vo.iterationsDesc), - varLine('ALTERNATE_BOOL', vo.alternateBoolSuffix || undefined), + varLine('ALTERNATE_BOOL', vo.alternateBoolSuffix), ); if (hasEffectId) { lines.push(varLine('UNIQUE_EFFECT_ID')); diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index d1208f8c..e60ce94f 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -28,9 +28,10 @@ function buildFullLeanPitfalls(pitfallOrder, fragments) { /** * Renders full-lean.md — the comprehensive reference for all triggers, effects, and API surface. * - * Large static prose sections (Element Binding, Interactions, Effects, FOUC Prevention) are kept - * inline rather than extracted to fragments. They have exactly one consumer and change together - * with the surrounding template logic — extracting them would add indirection without deduplication. + * The largest static prose sections (Element Binding, Interactions, StateEffect) are extracted to + * `full-lean/` fragments for editor ergonomics (markdown highlighting, spell-checking). Remaining + * static sections (Effects preamble, FOUC Prevention) are kept inline — they interleave with + * dynamic content and extracting them would fragment the template's flow. * * @param {{ triggers: object[], effects: object, meta: object }} data — no `trigger`; receives the full data object * @param {import('../../scripts/build-rules.mjs').Fragments} fragments @@ -119,34 +120,7 @@ ${fragments.get('quick-start', 'register-presets')} --- -## Element Binding - -**CRITICAL:** Do NOT add observers/event listeners manually. The runtime binds triggers and effects via element keys. - -### Web: \`\` - -- MUST set \`data-interact-key\` to a unique value. -- MUST contain at least one child element (the library targets \`.firstElementChild\`). -- If an effect targets a different element, that element also needs its own \`\`. - -\`\`\`html - -
...
-
-\`\`\` - -### React: \`\` component - -- MUST set \`tagName\` to the replaced element's HTML tag. -- MUST set \`interactKey\` to a unique string. - -\`\`\`tsx -import { Interaction } from '@wix/interact/react'; - - - ... -; -\`\`\` +${fragments.get('full-lean/element-binding')} --- @@ -156,29 +130,7 @@ ${fragments.get('config-structure', 'detailed')} --- -## Interactions - -Each interaction maps a source element + trigger to one or more effects. - -**Multiple effects per interaction:** A single interaction can contain multiple effects in its \`effects\` array. All effects in the same interaction share the same trigger — they all fire together when the trigger activates. Use this to apply different animations to different targets from the same trigger event, rather than creating separate interactions with duplicate trigger configs. - -\`\`\`ts -{ - key: string; // REQUIRED — matches data-interact-key / interactKey - the root element - trigger: TriggerType; // REQUIRED - params?: TriggerParams; // trigger-specific options - effects?: (Effect | EffectRef)[]; // possible to add multiple effects for same trigger - sequences?: (SequenceConfig | SequenceConfigRef)[]; // possible to add multiple sequences for same trigger - conditions?: string[]; // ids referencing the top-level conditions map; all must pass - selector?: string; // optional - CSS selector to refine source element selection within the root element - listContainer?: string; // optional — CSS selector for list container - listItemSelector?: string; // optional — CSS selector to filter which children of listContainer are observed as sources -} -\`\`\` - -At least one of \`effects\` or \`sequences\` MUST be provided. - -For most use cases, \`key\` alone is sufficient for both source and target resolution. The \`selector\`, \`listContainer\`, and \`listItemSelector\` fields are only needed for advanced patterns (lists, delegated triggers, child targeting). See [Element Resolution](#element-resolution) for details. +${fragments.get('full-lean/interactions')} --- @@ -387,38 +339,7 @@ ${rangeTable} - Sticky child (\`key\`) with \`position: sticky; top: 0; height: 100vh\`: stays fixed while the wrapper scrolls. This is the ViewTimeline source. - Use \`rangeStart/rangeEnd\` with \`name: 'contain'\` to animate only during the stuck phase. -### StateEffect (CSS style toggle) - -Used with \`hover\` / \`click\` triggers. Set \`stateAction\` on the effect to control state behavior. - -**StateEffect** (CSS transition-style state toggles): - -- \`key?\`: string (target override; see TARGET CASCADE) -- \`effectId?\`: string (when used as a reference identity) -- One of: - - \`transition?\`: \`{ duration?: number; delay?: number; easing?: string; styleProperties: { name: string; value: string }[] }\` - - Applies a single transition options block to all listed style properties. - - \`transitionProperties?\`: \`Array<{ name: string; value: string; duration?: number; delay?: number; easing?: string }>\` - - Allows per-property transition options. If both \`transition\` and \`transitionProperties\` are provided, the system SHOULD apply both with per-property entries taking precedence for overlapping properties. - -\`\`\`ts -// Shared timing for all properties: -{ - transition: { - duration?: number; delay?: number; easing?: string; - styleProperties: [{ name: string; value: string }] - } -} - -// Per-property timing: -{ - transitionProperties: [ - { name: string; value: string; duration?: number; delay?: number; easing?: string } - ] -} -\`\`\` - -CSS property names use **camelCase** (e.g. \`'backgroundColor'\`, \`'borderRadius'\`). +${fragments.get('full-lean/state-effect')} ### Animation Payloads diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index 50b76980..676d2e23 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -91,7 +91,7 @@ ${fragments.get('fouc', 'code-vanilla', { key: '[SOURCE_KEY]', classAttr: '' })} Use \`keyframeEffect\` or \`namedEffect\` when the viewEnter should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior. Use \`params\` only for observer configuration (\`threshold\`, \`inset\`). -${fragments.get('multiple-effects-note', 'default', { triggerName: 'viewEnter', triggerEvent: 'viewport entry event', triggerContext: ' when the element enters the viewport', extraNote: " Each effect can have its own `triggerType`." })} +${fragments.get('multiple-effects-note', 'default', { triggerName: 'viewEnter', triggerEvent: 'viewport entry event', triggerContext: ' when the element enters the viewport', extraNote: ' Each effect can have its own `triggerType`.' })} \`\`\`typescript { diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index 3197f376..82beb442 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -58,22 +58,38 @@ for (const trigger of data.triggers) { } } - if (trigger.pitfalls) { + if (trigger.pitfalls !== undefined) { + if (!Array.isArray(trigger.pitfalls)) { + throw new Error(`triggers.yaml: trigger "${trigger.name}".pitfalls must be an array`); + } for (const p of trigger.pitfalls) { if (!p.id) { - throw new Error(`triggers.yaml: trigger "${trigger.name}" has a pitfall entry missing "id"`); + throw new Error( + `triggers.yaml: trigger "${trigger.name}" has a pitfall entry missing "id"`, + ); } } } - if (trigger.params) { + if (trigger.params !== undefined) { + if (!Array.isArray(trigger.params)) { + throw new Error(`triggers.yaml: trigger "${trigger.name}".params must be an array`); + } for (const p of trigger.params) { if (!p.name) { - throw new Error(`triggers.yaml: trigger "${trigger.name}" has a param entry missing "name"`); + throw new Error( + `triggers.yaml: trigger "${trigger.name}" has a param entry missing "name"`, + ); } } } + for (const field of ['hasReversed', 'hasEffectId']) { + if (trigger[field] !== undefined && typeof trigger[field] !== 'boolean') { + throw new Error(`triggers.yaml: trigger "${trigger.name}".${field} must be a boolean`); + } + } + if (trigger.triggerTypeDescriptions) { for (const [key, val] of Object.entries(trigger.triggerTypeDescriptions)) { if (typeof val !== 'object' || !val.full) { @@ -104,12 +120,12 @@ class Fragments { ? `${prefix}/${basename(entry.name, '.md')}` : basename(entry.name, '.md'); const raw = readFileSync(join(dir, entry.name), 'utf8'); - this.store.set(key, this._parseSections(raw)); + this.store.set(key, this._parseSections(raw, key)); } } } - _parseSections(raw) { + _parseSections(raw, filePath) { const sections = new Map(); let current = null; let buf = []; @@ -117,6 +133,11 @@ class Fragments { for (const line of raw.split('\n')) { const m = line.match(/^$/); if (m) { + if (current === null && buf.join('\n').trim()) { + throw new Error( + `Fragment "${filePath}" has content before the first marker. Add a marker or remove the content.`, + ); + } if (current !== null) { sections.set(current, buf.join('\n').trim()); } From edd35dcf75f01b36d0a684af9b10539e2d461f98 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 13:18:27 +0300 Subject: [PATCH 18/27] PR fixes --- .gitignore | 1 + packages/interact/_content/data/triggers.yaml | 2 ++ .../interact/_content/templates/_helpers.mjs | 8 +++++++- .../_content/templates/event-trigger-rule.mjs | 11 ++++++++++- .../interact/_content/templates/full-lean.mjs | 17 +++++++++-------- .../_content/templates/viewenter-rule.mjs | 2 +- packages/interact/scripts/build-rules.mjs | 9 ++++++++- 7 files changed, 38 insertions(+), 12 deletions(-) diff --git a/.gitignore b/.gitignore index b6fa3ad0..8716bf49 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ pnpm-lock.yaml package-lock.json .yarn/ tmp/ +.cursor/plans/ diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml index 99ebab63..1463eceb 100644 --- a/packages/interact/_content/data/triggers.yaml +++ b/packages/interact/_content/data/triggers.yaml @@ -144,6 +144,7 @@ triggers: section: pointermove-source - name: animationEnd + template: null defaultTriggerType: null params: - name: effectId @@ -152,6 +153,7 @@ triggers: description: 'ID of the preceding effect' - name: pageVisible + template: null defaultTriggerType: null params: [] pitfalls: [] diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs index ce507925..e1b90c02 100644 --- a/packages/interact/_content/templates/_helpers.mjs +++ b/packages/interact/_content/templates/_helpers.mjs @@ -94,11 +94,17 @@ const COMMON_VARS = { * Returns a formatted variable description line. * For 'suffix' mode vars, `extra` is appended after the base description. * For 'override' mode vars, `extra` replaces the base description. - * For vars with no mode, `extra` is ignored. + * Throws if `extra` is passed to a variable with no mode (would be silently ignored). */ export function varLine(name, extra) { const v = COMMON_VARS[name]; if (!v) throw new Error(`Unknown common variable: ${name}`); + if (extra && !v.mode) { + throw new Error( + `varLine('${name}'): extra argument passed but this variable has no mode (suffix/override). ` + + `Either add a mode to COMMON_VARS['${name}'] or remove the extra argument.`, + ); + } let desc = v.base; if (extra && v.mode) { desc = v.mode === 'suffix' ? `${v.base} ${extra}` : extra; diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 7c6e1152..0be0a589 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,7 +1,15 @@ import { capitalize, buildPitfallsBlock, varLine } from './_helpers.mjs'; +const VARIABLE_OVERRIDES_BASE = { + customEffectExamples: '', + offsetEasingSuffix: '', + alternateBoolSuffix: undefined, + iterationsDesc: undefined, +}; + const VARIABLE_OVERRIDES = { hover: { + ...VARIABLE_OVERRIDES_BASE, sourceKeySuffix: 'The element that listens for hover.', targetKeyDesc: "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", @@ -16,6 +24,7 @@ const VARIABLE_OVERRIDES = { offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.', }, click: { + ...VARIABLE_OVERRIDES_BASE, sourceKeySuffix: 'The element that listens for clicks.', targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", @@ -45,7 +54,7 @@ export function render(data, fragments) { const pitfallsBlock = buildPitfallsBlock(trigger, fragments, { wrapped: true }); const multipleEffectsNote = trigger.showMultipleEffectsNote - ? `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event`, triggerContext: '', extraNote: '' })}\n` + ? `\n${fragments.get('multiple-effects-note', { triggerName: name, triggerEvent: `${name} event`, triggerContext: '', extraNote: '' })}\n` : ''; return `# ${Name} Trigger Rules for ${data.meta.packageName} diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index e60ce94f..0c25ef42 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -7,18 +7,19 @@ const FULL_LEAN_PITFALL_ORDER = [ { id: 'hit-area', section: 'detailed-pointermove' }, ]; -function buildBehaviorTable(headerLabel, behaviorKey, hover, click, { defaultKey } = {}) { - const hoverDescs = hover[behaviorKey]; - const clickDescs = click[behaviorKey]; - const keys = Object.keys(hoverDescs); +function buildBehaviorTable(headerLabel, behaviorKey, triggers, { defaultKey } = {}) { + const keys = Object.keys(triggers[0][behaviorKey]); const rows = keys.map((k) => { const isDefault = defaultKey ? k === defaultKey : false; const label = isDefault ? `\`'${k}'\` (default)` : `\`'${k}'\``; - return [label, hoverDescs[k].short, clickDescs[k].short]; + return [label, ...triggers.map((t) => t[behaviorKey][k].short)]; }); - return buildMarkdownTable([headerLabel, 'hover behavior', 'click behavior'], rows); + return buildMarkdownTable( + [headerLabel, ...triggers.map((t) => `${t.name} behavior`)], + rows, + ); } function buildFullLeanPitfalls(pitfallOrder, fragments) { @@ -188,11 +189,11 @@ For \`TimeEffect\` (keyframe/named/custom effects), set \`triggerType\` on the e **\`triggerType\`** — on \`TimeEffect\`: -${buildBehaviorTable('Type', 'triggerTypeDescriptions', hover, click, { defaultKey: hover.defaultTriggerType })} +${buildBehaviorTable('Type', 'triggerTypeDescriptions', [hover, click], { defaultKey: hover.defaultTriggerType })} **\`stateAction\`** — on \`StateEffect\`: -${buildBehaviorTable('Action', 'stateActionDescriptions', hover, click, { defaultKey: 'toggle' })} +${buildBehaviorTable('Action', 'stateActionDescriptions', [hover, click], { defaultKey: 'toggle' })} ### viewEnter diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index 676d2e23..df3d2370 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -91,7 +91,7 @@ ${fragments.get('fouc', 'code-vanilla', { key: '[SOURCE_KEY]', classAttr: '' })} Use \`keyframeEffect\` or \`namedEffect\` when the viewEnter should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior. Use \`params\` only for observer configuration (\`threshold\`, \`inset\`). -${fragments.get('multiple-effects-note', 'default', { triggerName: 'viewEnter', triggerEvent: 'viewport entry event', triggerContext: ' when the element enters the viewport', extraNote: ' Each effect can have its own `triggerType`.' })} +${fragments.get('multiple-effects-note', { triggerName: 'viewEnter', triggerEvent: 'viewport entry event', triggerContext: ' when the element enters the viewport', extraNote: ' Each effect can have its own `triggerType`.' })} \`\`\`typescript { diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index 82beb442..08e148fe 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -153,7 +153,14 @@ class Fragments { return sections; } - get(path, section = 'default', params = {}) { + get(path, sectionOrParams = 'default', params = {}) { + let section; + if (typeof sectionOrParams === 'object') { + section = 'default'; + params = sectionOrParams; + } else { + section = sectionOrParams; + } const sectionMap = this.store.get(path); if (!sectionMap) { throw new Error(`Fragment not found: ${path}`); From ff411728cc219833e75051746f0155020a640005 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 13:26:34 +0300 Subject: [PATCH 19/27] PR fixes --- .../rules_build_pipeline_ffe2a9f3.plan.md | 1053 ----------------- .../interact/_content/fragments/README.md | 2 +- .../_content/fragments/pitfalls/hit-area.md | 4 +- .../_content/fragments/quick-start.md | 18 +- .../_content/templates/event-trigger-rule.mjs | 2 - .../interact/_content/templates/full-lean.mjs | 28 +- .../_content/templates/integration.mjs | 14 +- packages/interact/scripts/build-rules.mjs | 72 +- 8 files changed, 76 insertions(+), 1117 deletions(-) delete mode 100644 .cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md diff --git a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md b/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md deleted file mode 100644 index 5d711723..00000000 --- a/.cursor/plans/rules_build_pipeline_ffe2a9f3.plan.md +++ /dev/null @@ -1,1053 +0,0 @@ ---- -name: Rules Build Pipeline -overview: Implement a build pipeline for `packages/interact/rules/` using structured YAML data + JavaScript template functions + markdown fragments, eliminating all content duplication and ensuring a single source of truth. -todos: - - id: scaffold - content: 'Create `_content/` directory structure: `data/`, `fragments/`, `templates/`, and `scripts/build-rules.mjs` skeleton' - status: completed - - id: data-triggers - content: Create `data/triggers.yaml` with all 9 trigger definitions (hover, click, interest, activate, viewEnter, viewProgress, pointerMove, animationEnd, pageVisible) — pull field names from actual TS types - status: completed - - id: data-effects-meta - content: Create `data/effects.yaml` (effect field definitions, presets table, ranges, easings) and `data/meta.yaml` (package metadata) - status: completed - - id: fragments - content: 'Extract ~12 fragments from existing rule files: fouc, element-resolution, 6 pitfalls, quick-start, multiple-effects-note, custom-effect-intro, sequences-intro' - status: completed - - id: template-event - content: Create `templates/event-trigger-rule.mjs` — generates click.md and hover.md from trigger data + shared fragments - status: completed - - id: template-viewport - content: Create `templates/viewenter-rule.mjs` and `templates/viewprogress-rule.mjs` - status: completed - - id: template-pointer - content: Create `templates/pointermove-rule.mjs` - status: completed - - id: template-reference - content: Create `templates/full-lean.mjs` and `templates/integration.mjs` — the two comprehensive reference files - status: completed - - id: build-script - content: 'Implement `scripts/build-rules.mjs`: YAML loading, fragment parsing, template orchestration, file writing' - status: completed - - id: integrate - content: Add `build:rules` script to package.json, add `js-yaml` devDependency, update CI workflow - status: completed - - id: verify - content: Run build, diff generated output against current rule files, verify no information loss, fix any discrepancies - status: completed - - id: fix-lockfile - content: 'CI fix: run `yarn install` so yarn.lock includes the new `js-yaml` resolution, commit the updated lockfile' - status: completed - - id: fix-dead-fragments - content: 'Remove dead fragment files `custom-effect-intro.md` and `sequences-intro.md` (unused by any template; YAML prose fields are used instead)' - status: completed - - id: fix-yaml-prose - content: 'Move 15+ prose description fields (timeEffectIntro, sourceKeyDesc, etc.) out of triggers.yaml into the event-trigger template directly, keeping only structured data in YAML' - status: completed - - id: fix-fill-variables - content: 'Collapse `buildVariablesMidFill`/`buildVariablesEndFill` into a single `buildVariables` function — always place `[FILL_MODE]` after `[NAMED_EFFECT_DEFINITION]` (matching the config block order). Remove `fillModeAtEnd` from triggers.yaml.' - status: completed - - id: fix-build-manifest - content: 'Replace repetitive template orchestration in build-rules.mjs (lines 104-148) with a data-driven manifest array' - status: completed - - id: fix-viewprogress-backtick - content: 'Fix stray backtick template literal in viewprogress-rule.mjs line 86 — normalize to plain string like other templates' - status: completed - - id: fix-shared-fragments - content: 'Extract duplicated sections (Conditions, Static API, Config Structure, Sequences) from full-lean.mjs and integration.mjs into shared fragments' - status: completed - - id: fix-ci-freshness - content: 'Add a freshness check step to `.github/workflows/ci.yml`: `yarn workspace @wix/interact build:rules && git diff --exit-code packages/interact/rules/`' - status: completed - - id: fix-regenerate - content: 'Run `build:rules`, verify output, commit regenerated rules/*.md files' - status: completed - - id: fix-prettier-range-table - content: 'Fix Prettier CI failure: range table in full-lean.mjs had hardcoded header widths that did not match generated data row widths. Made header + separator dynamic so column widths are computed from actual data.' - status: completed - - id: fix-variable-overrides-yaml - content: 'Move trigger-specific variable descriptions (sourceKeySuffix, targetKeyDesc, fillModeDesc, easingDesc, iterationsDesc, fillCritical, customEffectExamples, offsetEasingSuffix, alternateBoolSuffix) into `variableOverrides` in triggers.yaml. Eliminates all isClick/isHover ternaries from event-trigger-rule.mjs — template is now fully data-driven.' - status: completed - - id: fix-simplify-build-variables - content: 'Simplify buildVariables() — remove isClick/isHover/hasReversed/hasEffectId positional params, derive hasReversed/hasEffectId from trigger.templateFields internally, read all prose from trigger.variableOverrides.' - status: completed - - id: fix-remove-unused-yaml - content: 'Remove unused YAML fields from triggers.yaml: category, supportsTimeEffect, supportsStateEffect, supportsScrubEffect, supportsCustomEffect (never read by any template). Flatten templateFields from nested {timeEffect/stateEffect/customEffect} to a flat array (only timeEffect fields were ever consumed).' - status: completed - - id: fix-regenerate-2 - content: 'Regenerate rules/*.md, verify all CI checks pass (build, lint, format, test, rules freshness)' - status: completed - - id: refactor-pitfalls-yaml-section - content: 'Add `section` field to pitfall entries in triggers.yaml for viewEnter, viewProgress, pointerMove — enables all trigger templates to use trigger.pitfalls data-driven pattern (matching event-trigger-rule.mjs)' - status: completed - - id: refactor-viewenter-data-driven - content: 'Refactor viewenter-rule.mjs to use trigger.pitfalls and trigger.params from YAML instead of hardcoding fragment calls and param descriptions' - status: completed - - id: refactor-viewprogress-data-driven - content: 'Refactor viewprogress-rule.mjs to use trigger.pitfalls from YAML instead of hardcoding the overflow-clip fragment call' - status: completed - - id: refactor-pointermove-data-driven - content: 'Refactor pointermove-rule.mjs to use trigger.pitfalls from YAML instead of hardcoding the hit-area fragment call' - status: completed - - id: refactor-regenerate - content: 'Regenerate rules/*.md, verify output is identical to pre-refactor output' - status: completed - - id: refactor-common-vars - content: 'Add shared varLine() helper to _helpers.mjs — common variable descriptions (SOURCE_KEY, TARGET_KEY, DURATION_MS, etc.) defined once, used by all trigger templates via varLine(name, suffix)' - status: completed - - id: refactor-pitfalls-helper - content: 'Extract buildPitfallsBlock() helper to _helpers.mjs — all trigger templates use shared function to iterate trigger.pitfalls from YAML' - status: completed - - id: refactor-fouc-merge - content: 'Merge 3 FOUC fragment section pairs (code-web/code-web-example, code-react/code-react-example, code-vanilla/code-vanilla-example) into parameterized sections using {{key}} and {{classAttr}} interpolation' - status: completed - - id: refactor-param-varname - content: 'Add varName field to YAML params entries (e.g. threshold→VISIBILITY_THRESHOLD) — eliminates hardcoded paramVarNames map in viewenter-rule.mjs' - status: completed - - id: refactor-yaml-schema - content: 'Add template field + section comments to triggers.yaml — documents which template renders each trigger, separates event vs viewport vs pointer trigger schemas' - status: completed - - id: refactor-full-lean-tables - content: 'Generate hover/click triggerType + stateAction comparison tables in full-lean.mjs from YAML fullLeanBehavior data — replaces hardcoded markdown tables with buildBehaviorTable() function' - status: completed - - id: refactor-full-lean-pitfalls - content: 'Data-drive full-lean.mjs pitfalls from YAML — add fullLeanSection field to trigger pitfall entries + fullLeanPitfallOrder array in effects.yaml, replaces hardcoded fragment calls' - status: completed - - id: refactor-optional-chaining - content: 'Normalize optional chaining on trigger.pitfalls — all templates now use buildPitfallsBlock() with consistent ?. handling' - status: completed - - id: refactor-regenerate-2 - content: 'Regenerate rules/*.md, verify output — only 5 minor consistency improvements from standardized varLine descriptions (no information loss)' - status: completed - - id: review-unused-import - content: 'Remove unused `when` import from viewenter-rule.mjs' - status: completed - - id: review-fouc-fragments - content: 'Use FOUC fragments in integration.mjs instead of hardcoded code blocks — fixes id="hero" → class="hero" inconsistency and standardizes heading text' - status: completed - - id: review-contain-note - content: 'Move hardcoded `contain` sticky-container note from viewprogress-rule.mjs into effects.yaml rangeNames data' - status: completed - - id: review-decouple-full-lean - content: 'Move fullLeanBehavior data from triggers.yaml into full-lean.mjs local constant; remove dead fullLeanSection fields from trigger pitfall entries (already in effects.yaml fullLeanPitfallOrder)' - status: completed - - id: review-varline-api - content: "Standardize varLine API in _helpers.mjs — declarative COMMON_VARS with explicit 'suffix' / 'override' modes replacing ad-hoc function signatures" - status: completed - - id: review-fragment-warning - content: 'Add unreplaced {{...}} placeholder warning to Fragments.get() in build-rules.mjs' - status: completed - - id: review-regenerate - content: 'Regenerate rules/*.md — full-lean.md range table widened for contain note, integration.md FOUC section uses fragments. All other 5 files byte-identical.' - status: completed - - id: refactor6-varline-consistency - content: 'Extend varLine usage across all templates — use canonical varLine in Rule 1, standardize "same as Rule 1" shorthand in subsequent rules. Eliminates duplicated variable descriptions in viewprogress Rules 2-3 and pointermove Rules 2-4.' - status: completed - - id: refactor6-markdown-table - content: 'Extract generic buildMarkdownTable(headers, rows) helper to _helpers.mjs — replaces duplicated column-width padding logic in full-lean.mjs (behavior tables + range table).' - status: completed - - id: refactor6-pitfalls-wrapped - content: 'Add { wrapped: true } option to buildPitfallsBlock() — eliminates 3 identical 2-line wrapping patterns in event-trigger, viewenter, and viewprogress templates.' - status: completed - - id: refactor6-jsdoc - content: 'Standardize JSDoc @param on all render() functions — trigger-specific templates document { triggers, effects, meta, trigger }, reference templates document { triggers, effects, meta }.' - status: completed - - id: refactor6-full-lean-fragments - content: 'Add #full-lean sections to overflow-clip.md and same-element-viewenter.md fragments — full-lean.mjs triggers section now uses fragments instead of hardcoded CRITICAL notes for viewEnter and viewProgress.' - status: completed - - id: refactor6-regenerate - content: 'Regenerate rules/*.md — pointermove.md and viewprogress.md updated (collapsed duplicate descriptions to "same as Rule 1" shorthand, UNIQUE_EFFECT_ID uses canonical varLine). click.md, hover.md, viewenter.md, full-lean.md, integration.md byte-identical.' - status: completed - - id: refactor7-pitfall-order-local - content: 'Move fullLeanPitfallOrder from effects.yaml into full-lean.mjs as a local constant — rendering concern belongs in the template, not the data layer' - status: completed - - id: refactor7-placeholder-error - content: 'Make unreplaced {{...}} placeholder detection throw an error instead of console.warn — prevents silent bugs in generated output' - status: completed - - id: refactor7-inline-overrides - content: 'Move variableOverrides from triggers.yaml into event-trigger-rule.mjs as VARIABLE_OVERRIDES constant — prose descriptions with only 2 consumers do not belong in YAML data layer' - status: completed - - id: refactor7-explicit-booleans - content: 'Replace templateFields array in triggers.yaml with explicit hasReversed/hasEffectId booleans — clearer intent, no array scanning, no drift risk' - status: completed - - id: refactor7-remove-when - content: 'Remove when() helper from _helpers.mjs — replaced with inline ternaries in event-trigger-rule.mjs (5 usages). Eliminates unnecessary abstraction.' - status: completed - - id: refactor7-regenerate - content: 'Regenerate rules/*.md — all 7 files byte-identical to pre-refactor output. Zero content drift.' - status: completed - - id: refactor8-multiple-effects-fragment - content: 'Add trigger-specific sections (#viewEnter, #viewProgress, #pointerMove) to multiple-effects-note.md — viewenter, viewprogress, pointermove templates now use shared fragment instead of hardcoded notes' - status: completed - - id: refactor8-viewenter-triggertype-yaml - content: 'Add triggerTypeDescriptions to viewEnter trigger in triggers.yaml — template renders them dynamically via Object.entries(), matching the event-trigger-rule.mjs pattern' - status: completed - - id: refactor8-progress-type-fragment - content: 'Extract Progress type into fragments/progress-type.md with #detailed and #brief sections — pointermove-rule.mjs and full-lean.mjs now share the same source' - status: completed - - id: refactor8-yaml-validation - content: 'Add TRIGGER_SCHEMA validation to build-rules.mjs — checks required fields per template, throws on missing fields to catch typos at build time' - status: completed - - id: refactor8-short-full-descriptions - content: 'Consolidate FULL_LEAN_BEHAVIOR with triggers.yaml — triggerTypeDescriptions and stateActionDescriptions now have short/full structure. Removed FULL_LEAN_BEHAVIOR constant from full-lean.mjs, buildBehaviorTable reads .short from YAML data' - status: completed - - id: refactor8-fragment-naming-guide - content: 'Add fragments/README.md documenting section naming conventions (#default, #brief/#detailed, #, #full-lean, #code-)' - status: completed - - id: refactor8-fouc-single-consumer - content: 'Move 8 single-consumer FOUC sections from fouc.md into their respective templates — fouc.md reduced from 12 to 4 genuinely shared sections (#code-inject, #code-web, #code-react, #code-vanilla)' - status: completed - - id: refactor8-regenerate - content: 'Regenerate rules/*.md — all 7 files byte-identical to pre-refactor output. Zero content drift.' - status: completed - - id: review2-missing-files - content: 'Add missing progress-type.md and README.md fragments to git — build fails without progress-type.md (referenced by pointermove-rule.mjs and full-lean.mjs)' - status: completed - - id: review2-schema-consistency - content: 'Make viewEnter triggerTypeDescriptions use { full, short } objects matching hover/click — uniform schema prevents runtime errors when new templates read .full' - status: completed - - id: review2-default-key - content: 'Fix buildBehaviorTable to accept explicit defaultKey parameter instead of relying on YAML key insertion order — pass defaultTriggerType for triggerType table, hardcode toggle for stateAction table' - status: completed - - id: review2-multiple-effects - content: 'Consolidate multiple-effects-note.md #viewEnter section into #default via {{triggerContext}} and {{extraNote}} params — reduces from 4 to 3 sections' - status: completed - - id: review2-full-lean-comment - content: 'Add JSDoc comment to full-lean.mjs explaining why static prose sections are kept inline rather than extracted to fragments (single consumer, no deduplication benefit)' - status: completed - - id: review2-deeper-validation - content: 'Add deeper YAML schema validation in build-rules.mjs: check pitfalls[].id exists, params[].name exists, triggerTypeDescriptions values are { full } objects' - status: completed - - id: review2-variable-overrides - content: 'Simplify VARIABLE_OVERRIDES in event-trigger-rule.mjs with shared VARIABLE_OVERRIDES_BASE — per-trigger objects only override what differs' - status: completed - - id: review2-empty-catch - content: 'Fix empty catch block in build-rules.mjs --check mode to only catch ENOENT, rethrow unexpected errors' - status: completed - - id: review2-readme-filter - content: 'Filter README.md from fragment loader to prevent accidental inclusion as fragment data' - status: completed - - id: review2-regenerate - content: 'Regenerate rules/*.md — all 7 files byte-identical to pre-refactor output. Zero content drift.' - status: completed -isProject: false ---- - -# Rules Build Pipeline - -## Architecture - -The build produces the same 7 output `.md` files that exist today, from three source layers: - -```mermaid -graph LR - subgraph sources ["Source Layer (what you edit)"] - YAML["data/*.yaml"] - Fragments["fragments/*.md"] - Templates["templates/*.mjs"] - end - subgraph build ["Build"] - Script["scripts/build-rules.mjs"] - end - subgraph output ["Output (generated, gitignored)"] - Rules["rules/*.md"] - end - YAML --> Script - Fragments --> Script - Templates --> Script - Script --> Rules -``` - -All source files live under `packages/interact/_content/`. The build script reads them and writes the final `.md` files to `packages/interact/rules/`. - -## Source Layer - -### 1. Data files (`_content/data/`) - -`**triggers.yaml**` — one entry per trigger, capturing structured data: - -```yaml -triggers: - - name: hover - a11yAlias: interest - a11yNote: "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus." - defaultTriggerType: alternate - params: [] - pitfalls: - - id: hit-area - showMultipleEffectsNote: true - templateFields: - [ - triggerType, - keyframeEffect, - namedEffect, - fill, - duration, - easing, - delay, - iterations, - alternate, - ] - triggerTypeDescriptions: - alternate: 'plays forward on enter, reverses on leave. Default. Most common for hover.' - repeat: 'restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses.' - once: 'plays once on the first enter and never again.' - state: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`).' - stateActionDescriptions: - toggle: 'applies the style state on enter, removes on leave. Default.' - # ... - variableOverrides: # trigger-specific prose for variable descriptions in generated rule files - sourceKeySuffix: 'The element that listens for hover.' - targetKeyDesc: "identifier matching the element's key on the element that animates. ..." - fillModeDesc: "usually `'both'`. Keeps the final state applied while hovering..." - fillCritical: "Always include `fill: 'both'` for ..." - easingDesc: "CSS easing string (e.g. `'ease-out'`, ...), or named easing from `@wix/motion`." - iterationsDesc: 'optional. Number of iterations, or `Infinity` for continuous loops. ...' - customEffectExamples: '' - offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.' - alternateBoolSuffix: '' - - - name: click - a11yAlias: activate - a11yNote: "Use `trigger: 'activate'` instead of `trigger: 'click'` ..." - defaultTriggerType: alternate - params: [] - pitfalls: [] - showMultipleEffectsNote: false - templateFields: - [ - triggerType, - keyframeEffect, - namedEffect, - fill, - reversed, - duration, - easing, - delay, - iterations, - alternate, - effectId, - ] - # click includes reversed + effectId that hover omits - triggerTypeDescriptions: - alternate: 'plays forward on first click, reverses on next click. Default.' - # ... - variableOverrides: - sourceKeySuffix: 'The element that listens for clicks.' - targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to ..." - fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, ..." - fillCritical: "Always include `fill: 'both'` for ..." - customEffectExamples: ', randomized behavior' - alternateBoolSuffix: " Different from `triggerType: 'alternate'` which alternates per click." - # ... viewEnter, viewProgress, pointerMove, animationEnd entries follow -``` - -The trigger entries for `viewEnter`, `viewProgress`, `pointerMove`, and `animationEnd` follow the same shape but will include their `params` definitions (from the real TypeScript types in `[packages/interact/src/types/triggers.ts](packages/interact/src/types/triggers.ts)`). - -`**effects.yaml**` — shared effect field definitions, fill guidance, easing list: - -```yaml -fillGuidance: - both: 'use for scroll-driven, pointer-driven, and toggling effects (alternate, repeat, state)' - backwards: "use for entrance animations with triggerType 'once' when the final keyframe matches the element's base style" - -triggerTypes: [once, repeat, alternate, state] -stateActions: [toggle, add, remove, clear] - -easings: - - linear - - ease - # ... full list from full-lean.md line 350 - -presets: - entrance: [FadeIn, GlideIn, SlideIn, ...] - ongoing: [Pulse, Spin, Breathe, ...] - scroll: [FadeScroll, RevealScroll, ...] - mouse: [TrackMouse, Tilt3DMouse, ...] - -rangeNames: - entry: 'Element entering viewport' - exit: 'Element exiting viewport' - contain: 'After entry range and before exit range' - cover: 'Full range from entry through contain and exit' - entry-crossing: "From element's leading edge entering to trailing edge entering" - exit-crossing: "From element's leading edge exiting to trailing edge exiting" -``` - -`**meta.yaml**` — package metadata: - -```yaml -packageName: '@wix/interact' -presetsPackage: '@wix/motion-presets' -installCommand: 'npm install @wix/interact @wix/motion-presets' -entryPoints: - web: '@wix/interact/web' - react: '@wix/interact/react' - vanilla: '@wix/interact' -``` - -### 2. Markdown fragments (`_content/fragments/`) - -Each fragment has `` markers for different detail levels. - -**Planned fragments** (extracted from the ~15 duplicated concepts): - -| Fragment file | Sections | Used by | -| ------------------------------------ | ------------------------------------------------------------------------------------------ | ------------------------------------------- | -| `fouc.md` | `#short`, `#long`, `#code-generate`, `#code-web`, `#code-react`, `#code-vanilla`, `#rules` | full-lean, integration, viewenter | -| `element-resolution.md` | `#source`, `#target`, `#intro` | full-lean, integration | -| `pitfalls/hit-area.md` | `#hover`, `#pointermove`, `#full-lean-hover`, `#full-lean-pointermove` | hover, pointermove, full-lean | -| `pitfalls/overflow-clip.md` | `#short`, `#long` | viewprogress, full-lean | -| `pitfalls/same-element-viewenter.md` | `#short`, `#long` | viewenter, full-lean | -| `pitfalls/dont-guess-presets.md` | `#default` | full-lean, pointermove (Rule 1 variables) | -| `pitfalls/reduced-motion.md` | `#default` | full-lean | -| `pitfalls/perspective.md` | `#default` | full-lean | -| `quick-start.md` | `#web`, `#react`, `#vanilla`, `#cdn`, `#register-presets` | full-lean, integration | -| `multiple-effects-note.md` | `#default` (parameterized with `{{triggerName}}`) | hover, viewenter, viewprogress, pointermove | -| `custom-effect-intro.md` | `#default` | click, hover, viewenter | -| `sequences-intro.md` | `#short` (parameterized with `{{triggerName}}`) | click, hover, viewenter | - -### 3. Templates (`_content/templates/`) - -Each template is a `.mjs` file exporting a function that receives data + fragments and returns a markdown string. - -| Template | Generates | Key logic | -| ------------------------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -| `event-trigger-rule.mjs` | `click.md`, `hover.md` | Parameterized by trigger data. Generates Rule 1 (TimeEffect), Rule 2 (StateEffect), Rule 3 (customEffect), Rule 4 (sequences). The config block fields, variable descriptions, and trigger-specific wording all come from `triggers.yaml`. | -| `viewenter-rule.mjs` | `viewenter.md` | Includes FOUC section (from fragment), params on every template, no StateEffect rule. | -| `viewprogress-rule.mjs` | `viewprogress.md` | Scrub effects, range semantics, sticky pattern. | -| `pointermove-rule.mjs` | `pointermove.md` | Progress type, centering, device conditions, composite pattern. | -| `full-lean.mjs` | `full-lean.md` | Comprehensive reference. Pulls from all data + fragments. | -| `integration.mjs` | `integration.md` | Integration guide. Pulls from meta + triggers data + fragments. | - -**Template function signature:** - -```javascript -// event-trigger-rule.mjs -export function render(data, fragments) { - const { trigger } = data; - const { name, variableOverrides: vo } = trigger; - return `# ${capitalize(name)} Trigger Rules for ${data.meta.packageName} -... -**CRITICAL — Accessible ${name}**: ${trigger.a11yNote} -${trigger.pitfalls.map((p) => fragments.get(`pitfalls/${p.id}`, name)).join('\n')} -... -**CRITICAL:** ${vo.fillCritical} -...`; -} -``` - -## Build Script (`scripts/build-rules.mjs`) - -~150 lines of Node.js ESM. Core flow: - -1. Load all YAML files from `_content/data/` using `js-yaml` -2. Load all fragment `.md` files, parse `` markers into a `Map>` -3. For each template, call its `render()` function with the appropriate data and fragments -4. Write output to `packages/interact/rules/` -5. Report what was generated - -**Fragment resolution with parameterization:** - -```javascript -class Fragments { - get(path, section = 'default', params = {}) { - let content = this.store.get(path)?.get(section); - for (const [key, val] of Object.entries(params)) { - content = content.replaceAll(`{{${key}}}`, val); - } - return content; - } -} -``` - -**Dependencies:** Add `js-yaml` as a devDependency in `packages/interact/package.json`. No other new dependencies needed — Node 22 ESM handles everything. - -## Integration with Existing Pipeline - -- Add script to `[packages/interact/package.json](packages/interact/package.json)`: - -```json - "build:rules": "node scripts/build-rules.mjs" - - -``` - -- Add `build:rules` as a `prebuild` step or keep it manual (recommend manual for now — rules change infrequently) -- Update `[.github/workflows/interactdocs.yml](/.github/workflows/interactdocs.yml)` to run `build:rules` before copying rules to `_site/rules/` -- Generated `rules/*.md` files can be either committed (simpler CI) or gitignored (cleaner repo) — recommend **committed** initially so the npm package always has them, with a CI check that verifies they're up to date (`build:rules && git diff --exit-code rules/`) - -## Known Fixes Included in Migration - -These existing issues will be fixed as a natural consequence of the migration: - -- `[EFFECT_DEFINTION]` typo in click/hover/viewenter sequence blocks (fix in template once) -- `hitAea` typo in pointermove.md (fix in fragment once) -- `type: 'once'` vs `triggerType: 'once'` inconsistency in full-lean.md FOUC section -- hover.md missing `reversed` and `effectId` fields in TimeEffect template (decide once in `triggers.yaml` whether they belong) -- `generate` import path inconsistency (`@wix/interact` vs `@wix/interact/web`) — standardize in FOUC fragment -- Missing "Multiple effects" note in click.md (present in hover.md but absent in click.md) -- Inconsistent "additional effects" comments across files - -## Post-PR Fixes (from code review of PR #204) - -The initial implementation (PR #204) landed the full architecture. The fixes below address CI failures, dead code, and simplification opportunities found during review. - -### Fix 1: Commit updated `yarn.lock` (CI blocker) - -The CI `Install dependencies` step runs `yarn install --immutable` which refuses to modify the lockfile. Adding `js-yaml` to `devDependencies` in `package.json` without updating `yarn.lock` causes this failure. - -**Action:** Run `nvm use && yarn install` to regenerate the lockfile, then commit the updated `yarn.lock`. - -### Fix 2: Remove dead fragments - -`_content/fragments/custom-effect-intro.md` and `_content/fragments/sequences-intro.md` are not referenced by any template. The templates use `trigger.customEffectIntro` and `trigger.sequencesIntro` from YAML instead. These fragment files are dead code. - -**Action:** Delete both files. - -### Fix 3: Move prose descriptions out of `triggers.yaml` - -`triggers.yaml` currently stores 15+ full-sentence prose fields per trigger entry (`timeEffectIntro`, `stateEffectIntro`, `sequencesIntro`, `customEffectIntro`, `fillCritical`, `sourceKeyDesc`, `targetKeyDesc`, `fillModeDesc`, `namedEffectDesc`, `easingDesc`, `iterationsDesc`, `alternateDesc`, `customEffectCallbackDesc`, `sequenceEffectDefDesc`, `sequenceOffsetEasingDesc`). YAML is suited for structured data, not paragraphs of English. - -These fields only vary between hover and click. The viewEnter/viewProgress/pointerMove triggers don't use them at all (they have their own hardcoded templates). - -**Action:** - -- Remove all prose description fields from `triggers.yaml` (hover + click entries). -- Move the hover/click prose differences into `event-trigger-rule.mjs` directly, keyed by a simple flag or `trigger.name` check. These are small trigger-specific word choices (e.g. "while hovering" vs "while finished"), not reusable data. -- Keep only structured data in YAML: name, category, support flags, params, templateFields, pitfalls, triggerType/stateAction enum descriptions, `a11yAlias`, `a11yNote`, `defaultTriggerType`, `showMultipleEffectsNote`. - -### Fix 4: Unify fill-mode variable placement - -[`event-trigger-rule.mjs`](packages/interact/_content/templates/event-trigger-rule.mjs) has two nearly identical functions — `buildVariablesMidFill` (click) and `buildVariablesEndFill` (hover) — that differ only in where `[FILL_MODE]` appears in the variables list. The code block template itself always shows `fill` right after `namedEffect`/`keyframeEffect`, so the variables list should match that order. - -**Action:** - -- Collapse into a single `buildVariables` function that always places `[FILL_MODE]` after `[NAMED_EFFECT_DEFINITION]` (mid position, matching the config block order). -- Remove `fillModeAtEnd` and `fillModeDash` from `triggers.yaml`. -- Use a consistent em-dash separator for all triggers. - -The single function: - -```javascript -function buildVariables(trigger, fillModeVar, reversedVar, effectIdVar) { - const lines = [ - `- \`[SOURCE_KEY]\` — ...`, - `- \`[TARGET_KEY]\` — ...`, - `- \`[TRIGGER_TYPE]\` — ...`, - ...Object.entries(trigger.triggerTypeDescriptions).map(([k, v]) => ` - \`'${k}'\` — ${v}`), - `- \`[KEYFRAMES]\` — ...`, - `- \`[EFFECT_NAME]\` — ...`, - `- \`[NAMED_EFFECT_DEFINITION]\` — ...`, - fillModeVar, - ]; - if (reversedVar) lines.push(reversedVar.trim()); - lines.push( - `- \`[DURATION_MS]\` — ...`, - `- \`[EASING_FUNCTION]\` — ...`, - `- \`[DELAY_MS]\` — ...`, - `- \`[ITERATIONS]\` — ...`, - `- \`[ALTERNATE_BOOL]\` — ...`, - ); - if (effectIdVar) lines.push(effectIdVar.trim()); - return lines.join('\n'); -} -``` - -### Fix 5: Data-driven build manifest - -Lines 104-148 of [`build-rules.mjs`](packages/interact/scripts/build-rules.mjs) repeat the same import-find-render-push pattern 6 times. Replace with a declarative manifest: - -```javascript -const manifest = [ - { - template: 'event-trigger-rule.mjs', - triggers: ['click', 'hover'], - output: (name) => `${name}.md`, - }, - { template: 'viewenter-rule.mjs', triggers: ['viewEnter'], output: () => 'viewenter.md' }, - { - template: 'viewprogress-rule.mjs', - triggers: ['viewProgress'], - output: () => 'viewprogress.md', - }, - { template: 'pointermove-rule.mjs', triggers: ['pointerMove'], output: () => 'pointermove.md' }, - { template: 'full-lean.mjs', triggers: null, output: () => 'full-lean.md' }, - { template: 'integration.mjs', triggers: null, output: () => 'integration.md' }, -]; - -for (const entry of manifest) { - const mod = await import(join(CONTENT_DIR, 'templates', entry.template)); - if (entry.triggers) { - for (const name of entry.triggers) { - const trigger = data.triggers.find((t) => t.name === name); - if (!trigger) throw new Error(`Trigger "${name}" not found in triggers.yaml`); - outputs.push({ file: entry.output(name), content: mod.render(trigger, data, fragments) }); - } - } else { - outputs.push({ file: entry.output(), content: mod.render(data.triggers, data, fragments) }); - } -} -``` - -Adding a new template becomes a single manifest line. - -### Fix 6: Fix stray backtick in `viewprogress-rule.mjs` - -Line 86 of [`viewprogress-rule.mjs`](packages/interact/_content/templates/viewprogress-rule.mjs) uses `` easing: `'[EASING_FUNCTION]'` `` (backtick template literal) inside the code fence. All other templates use `easing: '[EASING_FUNCTION]'` (plain single-quoted string). - -**Action:** Change to `easing: '[EASING_FUNCTION]'` for consistency. - -### Fix 7: Extract shared sections into fragments - -`full-lean.mjs` (604 lines) and `integration.mjs` (285 lines) are mostly hardcoded prose with only a handful of `fragments.get()` calls. Several large sections are duplicated between them with minor variation: - -- **Conditions** block (~30 lines) -- **Static API** table (~20 lines) -- **Config Structure / InteractConfig** (~15 lines) -- **Sequences** section (~50 lines) - -**Action:** Extract each into a new fragment with `` / `` section markers (same pattern as `fouc.md`). This brings these templates closer to the single-source-of-truth goal and makes future edits to these shared concepts a one-file change. - -New fragment files: - -- `_content/fragments/conditions.md` — `#full-lean`, `#integration` -- `_content/fragments/static-api.md` — `#full-lean`, `#integration` -- `_content/fragments/config-structure.md` — `#full-lean`, `#integration` -- `_content/fragments/sequences.md` — `#full-lean`, `#integration` - -### Fix 8: Add CI freshness check - -The plan and PR description both mention a freshness check, but [`.github/workflows/ci.yml`](.github/workflows/ci.yml) was not updated. - -**Action:** Add a step after `Build` in the `build` job: - -```yaml -- name: Verify generated rules are up to date - run: | - yarn workspace @wix/interact build:rules - git diff --exit-code packages/interact/rules/ -``` - -This ensures that if someone edits a source file but forgets to re-run `build:rules`, CI catches it. - -### Fix 9: Regenerate output - -After all fixes above, run `build:rules` and commit the regenerated `rules/*.md` files so they reflect the new `[FILL_MODE]` variable ordering in hover.md. - -## Post-PR Fixes — Round 2 (code review refinements) - -Fixes from a second review pass, addressing CI failures and simplification opportunities. - -### Fix 10: Fix Prettier CI failure on range table - -The range table in `full-lean.mjs` had a hardcoded header row (`| Range name | Meaning |`) with fixed column widths, but the generated data rows were wider (due to `entry-crossing` description). Prettier auto-pads markdown tables to the widest cell, so CI's `format:check` failed. - -**Action:** Made the header + separator row dynamic — compute column widths from `Math.max(headerWidth, ...dataWidths)` and include the header in the generated `rangeTable` variable. - -### Fix 11: Move variable description overrides into `triggers.yaml` - -Fix 3 moved 15+ prose description fields out of YAML into `isClick`/`isHover` ternaries in `event-trigger-rule.mjs`. However, the trigger-specific differences are short structured phrases (not paragraphs), and the ternaries made the template harder to extend with new event triggers. - -**Action:** - -- Added `variableOverrides` map to each event trigger entry in `triggers.yaml` with fields: `sourceKeySuffix`, `targetKeyDesc`, `fillModeDesc`, `easingDesc`, `iterationsDesc`, `fillCritical`, `customEffectExamples`, `offsetEasingSuffix`, `alternateBoolSuffix`. -- Updated `event-trigger-rule.mjs` to read all prose from `trigger.variableOverrides` (aliased as `vo`) instead of `isClick`/`isHover` ternaries. -- Template is now fully data-driven — adding a third event trigger requires only a new YAML entry, no template changes. - -### Fix 12: Simplify `buildVariables()` function - -The function took 5 positional parameters (`trigger, isClick, isHover, hasReversed, hasEffectId`), 4 of which were derivable from `trigger`. - -**Action:** Reduced to `buildVariables(trigger, hasReversed, hasEffectId)`. Reads all prose from `trigger.variableOverrides`. The `hasReversed`/`hasEffectId` booleans are still passed from the caller since they're also used in the template string. - -### Fix 13: Remove unused YAML fields - -`triggers.yaml` had fields that no template ever consumed: `category`, `supportsTimeEffect`, `supportsStateEffect`, `supportsScrubEffect`, `supportsCustomEffect`. The `templateFields` was also a nested object (`{timeEffect, stateEffect, customEffect}`) but only `timeEffect` was ever read. - -**Action:** - -- Removed all `category` and `supports*` fields from all 7 trigger entries. -- Flattened `templateFields` from nested object to a flat array (the fields that were under `timeEffect`). - -### Fix 14: Regenerate output (round 2) - -After all round 2 fixes, ran `build:rules` and verified all 5 CI checks pass: build, lint, format, test, rules freshness. - ---- - -## Post-PR Fixes — Round 3 (data-driven trigger templates) - -The trigger-specific templates (viewenter, viewprogress, pointermove) hardcode pitfall fragment calls and param descriptions that already exist in `triggers.yaml`. The event-trigger-rule.mjs template is fully data-driven (iterates `trigger.pitfalls`, reads `trigger.variableOverrides`), but the other templates bypass YAML data entirely. This violates the single-source-of-truth principle. - -### Fix 15: Add `section` field to YAML pitfall entries - -Different pitfall fragments use different section naming conventions. The event-trigger-rule.mjs uses the trigger name as the section (e.g. `fragments.get('pitfalls/hit-area', 'hover')`), which works because `hit-area.md` has a `#hover` section. But other fragments use generic names (`#short`, `#long`, `#pointermove-source`). - -**Action:** Add an optional `section` field to pitfall entries in `triggers.yaml`. Templates use `p.section || name` to resolve the fragment section — defaulting to the trigger name when no explicit section is given. - -```yaml -# hover — no section needed, defaults to 'hover' -pitfalls: - - id: hit-area - -# viewEnter — fragment section is 'short', not 'viewEnter' -pitfalls: - - id: same-element-viewenter - section: short - -# viewProgress -pitfalls: - - id: overflow-clip - section: short - -# pointerMove — fragment section is 'pointermove-source' -pitfalls: - - id: hit-area - section: pointermove-source -``` - -### Fix 16: Data-drive pitfalls in viewenter/viewprogress/pointermove templates - -Replace hardcoded `fragments.get('pitfalls/...', '...')` calls with iteration over `trigger.pitfalls`, matching the pattern in event-trigger-rule.mjs. - -### Fix 17: Data-drive param descriptions in viewenter-rule.mjs - -The variable descriptions for `[VISIBILITY_THRESHOLD]` and `[VIEWPORT_INSETS]` are hardcoded but match the `description` field in `trigger.params`. Pull descriptions from YAML to maintain a single source of truth. - -### Fix 18: Regenerate and verify - -Run `build:rules` and verify the generated output is byte-identical to pre-refactor output. - ---- - -## Post-PR Fixes — Round 4 (deduplication & consistency) - -Final deduplication pass addressing remaining copy-paste across templates and inconsistent data-driving patterns. - -### Fix 19: Shared variable descriptions helper (`varLine`) - -Variable descriptions like `[SOURCE_KEY]`, `[TARGET_KEY]`, `[DURATION_MS]`, `[EASING_FUNCTION]`, `[DELAY_MS]`, `[EFFECT_NAME]`, `[NAMED_EFFECT_DEFINITION]`, `[FILL_MODE]`, `[UNIQUE_EFFECT_ID]`, `[CUSTOM_EFFECT_CALLBACK]`, `[TRANSITION_DURATION_MS]`, `[TRANSITION_EASING]`, `[CENTERED_TO_TARGET]`, `[HIT_AREA]` were copy-pasted across 4 trigger templates with minor wording variations. - -**Action:** Added `varLine(name, ...args)` helper to `_helpers.mjs`. Each variable has a canonical description with optional trigger-specific suffix/override. All trigger templates now call `varLine('SOURCE_KEY', 'The element that listens for hover.')` instead of repeating the full description. - -### Fix 20: Shared `buildPitfallsBlock` helper - -All 4 trigger templates had an identical pattern: check `trigger.pitfalls?.length`, iterate, call `fragments.get()` with section resolution. Differed only in newline wrapping. - -**Action:** Extracted `buildPitfallsBlock(trigger, fragments)` to `_helpers.mjs`. Returns raw content; templates handle their own spacing. - -### Fix 21: Merge FOUC fragment section pairs - -`fouc.md` had 6 near-identical section pairs (`#code-web`/`#code-web-example`, etc.) that differed only in using `[SOURCE_KEY]` vs `"hero"` for the key value. - -**Action:** Merged each pair using `{{key}}` and `{{classAttr}}` interpolation. Templates now call `fragments.get('fouc', 'code-web', { key: 'hero', classAttr: ' class="hero"' })` for concrete examples and `{ key: '[SOURCE_KEY]', classAttr: '' }` for placeholders. Reduced from 6 sections to 3. - -### Fix 22: Add `varName` to YAML params - -`viewenter-rule.mjs` had a hardcoded `paramVarNames` map (`{ threshold: 'VISIBILITY_THRESHOLD', inset: 'VIEWPORT_INSETS' }`) that lived in the template rather than YAML. - -**Action:** Added `varName` field to each param entry in `triggers.yaml`. Template reads `p.varName || p.name.toUpperCase()`. - -### Fix 23: Add `template` field + schema comments to `triggers.yaml` - -YAML had two implicit schemas (event triggers with `templateFields`/`variableOverrides` vs other triggers with just `params`) with no documentation. - -**Action:** Added `template` field to each trigger entry indicating which `.mjs` template renders it. Added section comments separating event, viewport/scroll, and pointer/chaining trigger groups. - -### Fix 24: Data-drive full-lean.mjs tables from YAML - -The hover/click `triggerType` and `stateAction` comparison tables in `full-lean.mjs` were hardcoded markdown despite the data existing in `triggers.yaml`. - -**Action:** Added `fullLeanBehavior.triggerType` and `fullLeanBehavior.stateAction` maps to hover and click YAML entries with condensed behavior descriptions. Added `buildBehaviorTable()` function that generates Prettier-compatible padded tables from YAML data. - -### Fix 25: Data-drive full-lean.mjs pitfalls from YAML - -`full-lean.mjs` hardcoded all 4 trigger-specific pitfall fragment calls (`overflow-clip/long`, `same-element-viewenter/long`, `hit-area/detailed-hover`, `hit-area/detailed-pointermove`) rather than deriving them from YAML. - -**Action:** Added `fullLeanSection` field to pitfall entries in `triggers.yaml` (the section name used by full-lean.mjs). Added `fullLeanPitfallOrder` array in `effects.yaml` to control ordering. `full-lean.mjs` now iterates this array instead of hardcoding fragment calls. - -### Fix 26: Regenerate output (round 4) - -Regenerated all 7 rule files. 5 minor consistency improvements from standardized `varLine` descriptions (dropped redundant "/vanilla" in viewenter, standardized em-dash separator, added "from the top-level `effects` map" completeness note). Zero information loss. click.md, hover.md, full-lean.md, integration.md, pointermove.md are byte-identical to pre-refactor output. - ---- - -## Post-PR Fixes — Round 5 (code review refinements) - -Fixes from a final review pass, addressing data/template coupling, API inconsistencies, dead code, and missed deduplication. - -### Fix 27: Remove unused `when` import - -`viewenter-rule.mjs` imported `when` from `_helpers.mjs` but never called it. - -**Action:** Removed unused import. - -### Fix 28: Use FOUC fragments in integration.mjs - -The FOUC code examples in `integration.mjs` (Web/React/Vanilla element markup) were hardcoded instead of using the parameterized `fouc.md` fragments that `full-lean.mjs` and `viewenter-rule.mjs` already use. This caused two inconsistencies: `
` (should be `class="hero"` like all other examples), and the heading said "**Web:**" instead of "**Web (Custom Elements):**". - -**Action:** Replaced hardcoded code blocks with `fragments.get('fouc', 'code-web', { key: 'hero', classAttr: ' class="hero"' })` etc., matching `full-lean.mjs`. - -### Fix 29: Move `contain` range note into YAML data - -`viewprogress-rule.mjs` hardcoded a special case for the `contain` range name (`'. Typically used with a position: sticky container'`). This note is useful context that belongs in the data layer. - -**Action:** Appended the sticky-container note to the `contain` description in `effects.yaml`. Removed the hardcoded special case from `viewprogress-rule.mjs`. The `full-lean.md` range table also now includes this note (wider column, auto-adjusted). - -### Fix 30: Decouple full-lean.mjs from triggers.yaml - -`fullLeanBehavior` data on hover/click trigger entries and `fullLeanSection` on pitfall entries existed solely to serve `full-lean.mjs`. This coupled the data layer to one specific template's rendering needs — adding a new output template would require adding more `fullXxx` fields to trigger entries. - -**Action:** - -- Moved `fullLeanBehavior` (hover + click triggerType/stateAction condensed descriptions) from `triggers.yaml` into a `FULL_LEAN_BEHAVIOR` constant in `full-lean.mjs`. Updated `buildBehaviorTable()` to read from the local constant instead of trigger data. -- Removed dead `fullLeanSection` field from all pitfall entries in `triggers.yaml` — this data was already present in `effects.yaml`'s `fullLeanPitfallOrder` array, which is what `full-lean.mjs` actually reads. - -### Fix 31: Standardize `varLine` API - -The `COMMON_VARS` map in `_helpers.mjs` used ad-hoc function signatures — `SOURCE_KEY(suffix)` appended text, `TARGET_KEY(desc)` replaced text, `EASING_FUNCTION(desc)` had a fallback default, `ALTERNATE_BOOL(suffix)` appended without space. This made the API hard to use without checking the source. - -**Action:** Replaced function-based `COMMON_VARS` with a declarative object where each variable has a `base` description and an optional `mode` (`'suffix'` or `'override'`). The `varLine(name, extra)` function applies the mode consistently: suffix mode always joins with a space, override mode fully replaces. Variables with no mode ignore the extra argument and always return the base. Updated `alternateBoolSuffix` in `triggers.yaml` to remove the leading space (suffix mode now adds one). - -### Fix 32: Add unreplaced placeholder warning - -The `Fragments.get()` method in `build-rules.mjs` silently ignored unmatched `{{...}}` placeholders after interpolation. A typo in a param name would produce output with raw `{{placeholder}}` text. - -**Action:** Added a post-interpolation check that warns about any remaining `{{...}}` patterns after parameter substitution. - -### Fix 33: Regenerate output (round 5) - -Regenerated all 7 rule files. Two files changed: - -- **full-lean.md**: Range table widened to accommodate `contain` note (all other content identical). -- **integration.md**: FOUC code examples now use fragments — `id="hero"` → `class="hero"`, "**Web:**" → "**Web (Custom Elements):**". - -All other 5 files (click.md, hover.md, viewenter.md, viewprogress.md, pointermove.md) are byte-identical. - ---- - -## Post-PR Fixes — Round 6 (consistency & deduplication) - -Refactors from a structural review of the final PR, addressing duplicated logic in helpers, inconsistent variable descriptions across templates, and hardcoded content in `full-lean.mjs` that duplicates existing fragments. - -### Fix 34: Extend `varLine` usage across all templates - -Variable descriptions in `viewprogress-rule.mjs` Rules 2-3 and `pointermove-rule.mjs` Rules 2-4 had fully-written-out descriptions that duplicated `COMMON_VARS` entries or each other. The "same as Rule 1" shorthand pattern (already used by `viewenter-rule.mjs`) was not applied consistently. - -**Action:** - -- Rule 1 of each template: uses `varLine` for all COMMON_VARS (viewprogress Rule 1 `UNIQUE_EFFECT_ID` converted from hardcoded to `varLine`). -- Rules 2+ of each template: standardized to "same as Rule 1" shorthand for variables already described in Rule 1 (collapsed 15+ standalone lines across viewprogress and pointermove into grouped shorthand references). -- Variables unique to a specific rule (e.g. `TALL_WRAPPER_KEY`, `COMPOSITE_OPERATION`, `AXIS`) remain hardcoded — they're not shared. - -### Fix 35: Extract generic `buildMarkdownTable` helper - -`full-lean.mjs` had two independent implementations of Prettier-compatible padded markdown tables: `buildBehaviorTable` (3 columns) and the range table construction (2 columns). Both computed column widths with `Math.max` and padded cells — identical logic with different data. - -**Action:** Added `buildMarkdownTable(headers, rows)` to `_helpers.mjs`. Takes an array of header strings and an array of row arrays, computes column widths, returns a left-aligned padded table string. Updated `buildBehaviorTable` and the range table in `full-lean.mjs` to use it. Output is byte-identical. - -### Fix 36: Add `{ wrapped }` option to `buildPitfallsBlock` - -Three templates (`event-trigger-rule.mjs`, `viewenter-rule.mjs`, `viewprogress-rule.mjs`) had an identical 2-line pattern after calling `buildPitfallsBlock`: - -```javascript -const pitfallsRaw = buildPitfallsBlock(trigger, fragments); -const pitfallsBlock = pitfallsRaw ? `\n${pitfallsRaw}\n` : ''; -``` - -**Action:** Added `{ wrapped: true }` option to `buildPitfallsBlock` in `_helpers.mjs`. When set, wraps non-empty output with leading/trailing newlines. Updated the 3 templates to use it, eliminating the intermediate variable. Output is byte-identical. - -### Fix 37: Standardize JSDoc on `render()` functions - -Template `render()` JSDoc annotations documented different subsets of the `data` parameter: - -- `event-trigger-rule.mjs`: `{ trigger, meta }` -- `viewprogress-rule.mjs`: `{ trigger, effects, meta }` -- `full-lean.mjs`: `{ triggers, effects, meta }` - -All trigger-specific templates actually receive `{ triggers, effects, meta, trigger }` (via `{ ...data, trigger }` in `build-rules.mjs`). Reference templates receive `{ triggers, effects, meta }`. - -**Action:** Standardized all trigger-specific templates to `@param {{ triggers: object[], effects: object, meta: object, trigger: object }} data`. Reference templates (`full-lean.mjs`, `integration.mjs`) document `{ triggers, effects, meta }` with a note that `trigger` is absent. - -### Fix 38: Extract full-lean.mjs trigger CRITICAL notes into fragments - -The viewEnter and viewProgress subsections in `full-lean.mjs`'s Triggers section contained hardcoded CRITICAL notes that duplicated content from `pitfalls/same-element-viewenter.md` and `pitfalls/overflow-clip.md`. If these pitfalls are updated in the fragments, `full-lean.md` would drift. - -**Action:** Added `#full-lean` sections to both pitfall fragment files with the condensed wording used by `full-lean.mjs`. Updated `full-lean.mjs` to call `fragments.get('pitfalls/same-element-viewenter', 'full-lean')` and `fragments.get('pitfalls/overflow-clip', 'full-lean')`. Output is byte-identical. - -### Fix 39: Regenerate output (round 6) - -Regenerated all 7 rule files. Two files changed: - -- **pointermove.md**: Collapsed 15 duplicate variable description lines across Rules 2-4 into grouped "same as Rule 1" shorthands. `EFFECT_NAME` in Rule 2 now uses the canonical `varLine` description. -- **viewprogress.md**: `UNIQUE_EFFECT_ID` in Rule 1 now uses canonical `varLine` description. Rules 2-3 collapsed duplicate descriptions into "same as Rule 1" shorthands. - -All other 5 files (click.md, hover.md, viewenter.md, full-lean.md, integration.md) are byte-identical. - ---- - -## Post-PR Fixes — Round 7 (separation of concerns & simplification) - -Refactors from a final code review, addressing data/template boundary violations, unnecessary abstractions, and silent error handling. - -### Fix 40: Move `fullLeanPitfallOrder` into full-lean.mjs - -`fullLeanPitfallOrder` in `effects.yaml` was a rendering concern — it controlled the order pitfalls render in one specific template. This coupled the data layer to `full-lean.mjs`'s layout needs (the same issue Fix 30 addressed for `fullLeanBehavior`). - -**Action:** Moved into a `FULL_LEAN_PITFALL_ORDER` constant at the top of `full-lean.mjs`. Removed from `effects.yaml`. Output is byte-identical. - -### Fix 41: Make placeholder detection throw - -The `Fragments.get()` method warned about unmatched `{{...}}` placeholders via `console.warn` — easy to miss in CI output. An unreplaced placeholder in generated output is always a bug. - -**Action:** Changed `console.warn` to `throw new Error`. Build now fails fast on typos in fragment param names. - -### Fix 42: Inline `variableOverrides` into template - -`variableOverrides` in `triggers.yaml` stored prose descriptions that only served `event-trigger-rule.mjs` for 2 triggers (hover, click). The indirection added ~40 lines of English prose to what should be a structured data file. - -**Action:** Moved into a `VARIABLE_OVERRIDES` constant in `event-trigger-rule.mjs`, keyed by trigger name. Template reads `VARIABLE_OVERRIDES[name]` instead of `trigger.variableOverrides`. Removed `variableOverrides` from both trigger entries in `triggers.yaml`. Output is byte-identical. - -### Fix 43: Replace `templateFields` with explicit booleans - -The `templateFields` array in `triggers.yaml` existed solely to derive two booleans (`hasReversed`, `hasEffectId`) via `.includes()`. The array created a false sense of generated config blocks — the template still hardcoded the full code block regardless. - -**Action:** Replaced with explicit `hasReversed: true/false` and `hasEffectId: true/false` fields. Template reads them directly. Output is byte-identical. - -### Fix 44: Remove `when()` helper - -The `when(condition, content)` helper was used 5 times in `event-trigger-rule.mjs`. In each case, a standard ternary (`condition ? content : ''`) is equally readable and doesn't require importing/learning a custom abstraction. - -**Action:** Removed `when()` from `_helpers.mjs`. Replaced all 5 usages in `event-trigger-rule.mjs` with inline ternaries or `|| ''` for falsy string concatenation. Output is byte-identical. - -### Fix 45: Regenerate output (round 7) - -Ran `build:rules`. All 7 rule files are byte-identical to pre-refactor output. Zero content drift. - ---- - -## Post-PR Fixes — Round 8 (single-source-of-truth & validation) - -Refactors from a structural review of the final PR, addressing remaining duplication, missing data-driving, and build-time safety. - -### Fix 46: Use multiple-effects-note fragment everywhere - -The `multiple-effects-note.md` fragment was only used by `event-trigger-rule.mjs`. The viewenter, viewprogress, and pointermove templates each hardcoded their own "Multiple effects" note with trigger-specific wording. - -**Action:** Added `#viewEnter`, `#viewProgress`, and `#pointerMove` sections to the fragment. Updated all three templates to use `fragments.get('multiple-effects-note', triggerName)`. - -### Fix 47: Data-drive viewEnter triggerType descriptions - -`viewenter-rule.mjs` hardcoded 4 `triggerType` descriptions while `event-trigger-rule.mjs` read them from `trigger.triggerTypeDescriptions` in YAML. The viewEnter trigger had no `triggerTypeDescriptions` entry. - -**Action:** Added `triggerTypeDescriptions` to the viewEnter trigger in `triggers.yaml`. Updated `viewenter-rule.mjs` to render them dynamically via `Object.entries(trigger.triggerTypeDescriptions).map(...)`, with `(default)` annotation derived from `trigger.defaultTriggerType`. Also added `triggerTypeDescriptions` to the YAML validation schema. - -### Fix 48: Extract Progress type into shared fragment - -The `Progress` type definition was duplicated between `pointermove-rule.mjs` (detailed, 13 lines) and `full-lean.mjs` (brief, 4 lines). A change to one would not propagate to the other. - -**Action:** Created `fragments/progress-type.md` with `#detailed` and `#brief` sections. Updated both templates to use the fragment. The deeply nested instance in full-lean.mjs's Triggers overview (lines 246-257) was left in place — it's inside a deeply indented markdown list where fragment extraction would be fragile. - -### Fix 49: Add YAML schema validation - -A typo in `triggers.yaml` (e.g. `hasReverse` instead of `hasReversed`) would produce silently wrong output. No build-time guard existed for field names. - -**Action:** Added `TRIGGER_SCHEMA` map to `build-rules.mjs` keyed by template name, listing required fields per trigger type. Build now throws on missing fields before template rendering begins. - -### Fix 50: Consolidate FULL_LEAN_BEHAVIOR with triggers.yaml - -`FULL_LEAN_BEHAVIOR` in `full-lean.mjs` maintained condensed descriptions for hover/click triggerType and stateAction behaviors — duplicating concepts from `triggers.yaml`'s `triggerTypeDescriptions` and `stateActionDescriptions`. Adding a new value to one required updating the other. - -**Action:** Changed `triggerTypeDescriptions` and `stateActionDescriptions` in hover and click trigger entries from flat strings to `{ full, short }` objects. `event-trigger-rule.mjs` reads `.full`, `full-lean.mjs` reads `.short` via `buildBehaviorTable()`. Removed the 30-line `FULL_LEAN_BEHAVIOR` constant entirely. Single source of truth for all behavior descriptions. - -### Fix 51: Document fragment section naming conventions - -Fragment sections used several different naming conventions with no documentation, making it hard for contributors to predict section names. - -**Action:** Added `fragments/README.md` documenting the naming convention patterns: `#default` for single-section fragments, `#brief`/`#detailed` for detail levels, `#` for trigger-specific variants, `#full-lean` for condensed reference wording, `#code-` for code examples. - -### Fix 52: Move single-consumer FOUC sections into templates - -`fouc.md` had 12 sections, 8 of which were used by exactly one template — providing no deduplication benefit, just indirection. - -**Action:** Inlined the 8 single-consumer sections (`#short`, `#long`, `#code-generate-viewenter`, `#code-generate-web`, `#rules-viewenter`, `#rules-brief`, `#rules-detailed`, `#intro-brief`) into their respective templates. `fouc.md` reduced from 12 to 4 genuinely shared sections (`#code-inject`, `#code-web`, `#code-react`, `#code-vanilla`). - -### Fix 53: Regenerate output (round 8) - -Ran `build:rules`. All 7 rule files are byte-identical to pre-refactor output. Zero content drift. - ---- - -## Post-PR Fixes — Round 9 (review hardening) - -Fixes from a structural review addressing a build-breaking bug, schema inconsistencies, fragile assumptions, and missing safety checks. - -### Fix 54: Add missing fragment files - -`progress-type.md` and `README.md` were created locally but never committed to the branch. `progress-type.md` is referenced by `pointermove-rule.mjs` and `full-lean.mjs` — the build throws `Fragment not found: progress-type` without it. - -**Action:** Staged both files via `git add`. - -### Fix 55: Uniform `triggerTypeDescriptions` schema - -viewEnter's `triggerTypeDescriptions` used flat strings while hover/click used `{ full, short }` objects. A new template reading `.full` on a viewEnter description would crash at runtime. - -**Action:** Changed viewEnter to `{ full, short }` objects. Updated `viewenter-rule.mjs` to read `.full`. Added schema validation in `build-rules.mjs` that enforces all `triggerTypeDescriptions` values must be objects with a `full` key. - -### Fix 56: Fix `buildBehaviorTable` default key derivation - -`buildBehaviorTable` assumed the first YAML key was the default (`const defaultKey = keys[0]`). Reordering keys in `triggers.yaml` would move the "(default)" annotation to the wrong row. - -**Action:** Added explicit `{ defaultKey }` option parameter. Callers pass `hover.defaultTriggerType` for the triggerType table and `'toggle'` for the stateAction table. No key-order dependency. - -### Fix 57: Consolidate `multiple-effects-note.md` sections - -The `#viewEnter` section was nearly identical to `#default` — only adding "when the element enters the viewport" and "Each effect can have its own `triggerType`." The `#viewProgress` and `#pointerMove` sections have meaningfully different sentence structures and remain separate. - -**Action:** Removed `#viewEnter` section. Added `{{triggerContext}}` and `{{extraNote}}` params to `#default`. Updated callers: event triggers pass empty strings, viewEnter passes context and note. Fragment reduced from 4 to 3 sections. - -### Fix 58: Document static prose design choice in `full-lean.mjs` - -`full-lean.mjs` is 524 lines with ~400 lines of static prose. Without context, this looks like content that should be extracted to fragments. - -**Action:** Added JSDoc explaining that single-consumer static sections are intentionally kept inline — they change together with surrounding template logic and extraction would add indirection without deduplication. - -### Fix 59: Deeper YAML schema validation - -Schema validation only checked top-level field existence. Missing `pitfalls[].id`, `params[].name`, or flat-string `triggerTypeDescriptions` values would pass validation but fail at render time. - -**Action:** Added checks for `pitfalls[].id`, `params[].name`, and `triggerTypeDescriptions` value shape (`typeof === 'object'` with `full` key). - -### Fix 60: Simplify `VARIABLE_OVERRIDES` with shared base - -The hover and click override objects had 9 fields each, most sharing the same value. Shared defaults were duplicated. - -**Action:** Extracted `VARIABLE_OVERRIDES_BASE` with common defaults. Per-trigger objects spread the base and only override what differs. - -### Fix 61: Fix empty catch block - -`build-rules.mjs` `--check` mode used `catch {}` when reading existing files. This silently swallowed all errors, not just missing files. - -**Action:** Changed to `catch (err) { if (err.code !== 'ENOENT') throw err; }`. - -### Fix 62: Filter `README.md` from fragment loader - -The recursive fragment loader picked up every `.md` file in the fragments directory, including `README.md`. This loaded documentation as fragment data under the key `"README"` — harmless but unintentional. - -**Action:** Added `entry.name !== 'README.md'` filter to `_loadDir`. - -### Fix 63: Regenerate output (round 9) - -Ran `build:rules`. All 7 rule files are byte-identical to pre-refactor output. Zero content drift. - ---- - -## Future Extension Points - -- **docs/**: Same `_content/data/` feeds docs templates — add `_content/templates/docs/` later -- **README**: Same meta.yaml + triggers.yaml generates README sections -- ~~**Validation**: Add `scripts/validate-rules.mjs` that imports actual TS types and cross-checks trigger names, param fields, effect fields against the YAML data~~ — **Partially done** (Fix 49: YAML schema validation in build-rules.mjs). Full TS type cross-checking remains a future extension. -- **motion-presets rules**: The build mechanism is scoped to `packages/interact/` but the architecture (YAML + fragments + templates) can be replicated in `packages/motion-presets/` with shared fragments if needed -- ~~**Common variable fragment**: Extract repeated variable descriptions~~ — **Done** (Fix 19: `varLine` helper) -- ~~**FOUC fragment consolidation**: Reduce fouc.md's 13 sections~~ — **Done** (Fix 21: merged 6 sections to 3 via `{{key}}` interpolation; Fix 52: moved single-consumer sections to templates, reduced to 4 shared sections) diff --git a/packages/interact/_content/fragments/README.md b/packages/interact/_content/fragments/README.md index 35a2f622..9061ca20 100644 --- a/packages/interact/_content/fragments/README.md +++ b/packages/interact/_content/fragments/README.md @@ -10,7 +10,7 @@ Reusable markdown content with `` markers for granular inclusio | `#brief` / `#detailed` | Two detail levels of the same content | `config-structure.md` | | `#short` / `#long` | Short and long versions of the same concept | `fouc.md`, `overflow-clip.md`| | `#` | Trigger-specific variant (e.g. `hover`, `viewEnter`) | `hit-area.md`, `multiple-effects-note.md` | -| `#full-lean` | Condensed wording used by the full-lean reference | `overflow-clip.md` | +| `#full-lean` / `#full-lean-` | Condensed wording used by the full-lean reference | `overflow-clip.md`, `hit-area.md` | | `#code-` | Code example variant (e.g. `code-web`, `code-react`) | `fouc.md`, `quick-start.md` | | `#` / `#-` | Named concept sections | `element-resolution.md` | diff --git a/packages/interact/_content/fragments/pitfalls/hit-area.md b/packages/interact/_content/fragments/pitfalls/hit-area.md index 75f91665..70afd3c9 100644 --- a/packages/interact/_content/fragments/pitfalls/hit-area.md +++ b/packages/interact/_content/fragments/pitfalls/hit-area.md @@ -2,9 +2,9 @@ - **CRITICAL**: MUST AVOID using the same element as both trigger source and effect target with effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. - **CRITICAL**: MUST AVOID using the same element as both source and target with effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. - + - **CRITICAL - Hit-area shift**: When a hover effect changes the size or position of the hovered element (e.g., `transform: scale(…)`), MUST use a 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. - + - **CRITICAL**: For `pointerMove` trigger MUST AVOID using the same element as both source and target with `hitArea: 'self'` and effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. When using `hitArea: 'self'`, the source element is the hit area for pointer tracking: diff --git a/packages/interact/_content/fragments/quick-start.md b/packages/interact/_content/fragments/quick-start.md index b696bf28..eb689414 100644 --- a/packages/interact/_content/fragments/quick-start.md +++ b/packages/interact/_content/fragments/quick-start.md @@ -1,19 +1,19 @@ ```bash -npm install @wix/interact @wix/motion-presets +{{installCommand}} ``` **Web (Custom Elements):** ```ts -import { Interact } from '@wix/interact/web'; +import { Interact } from '{{webEntry}}'; const instance = Interact.create(config); ``` The `config` object is an `InteractConfig` containing `interactions` (required), and optionally shared `effects`, `sequences`, and `conditions`. ```typescript -import { Interact } from '@wix/interact/web'; +import { Interact } from '{{webEntry}}'; Interact.create(config); ``` @@ -27,7 +27,7 @@ The `config` object contains `interactions` (trigger-effect bindings), and optio ```ts import { useEffect } from 'react'; -import { Interact } from '@wix/interact/react'; +import { Interact } from '{{reactEntry}}'; useEffect(() => { const instance = Interact.create(config); @@ -41,14 +41,14 @@ useEffect(() => { **Vanilla JS:** ```ts -import { Interact } from '@wix/interact'; +import { Interact } from '{{vanillaEntry}}'; const instance = Interact.create(config); instance.add(element, 'hero'); // bind after element exists in DOM instance.remove('hero'); // unregister ``` ```typescript -import { Interact } from '@wix/interact'; +import { Interact } from '{{vanillaEntry}}'; const interact = Interact.create(config); interact.add(element, 'hero'); @@ -58,7 +58,7 @@ interact.add(element, 'hero'); ```html ``` @@ -66,14 +66,14 @@ interact.add(element, 'hero'); **Registering presets** — MUST be called before calling `Interact.create()` with usage of `namedEffect`: ```ts -import * as presets from '@wix/motion-presets'; +import * as presets from '{{presetsPackage}}'; Interact.registerEffects(presets); ``` Or selectively: ```ts -import { FadeIn, ParallaxScroll } from '@wix/motion-presets'; +import { FadeIn, ParallaxScroll } from '{{presetsPackage}}'; Interact.registerEffects({ FadeIn, ParallaxScroll }); ``` diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 0be0a589..c0860e43 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -3,8 +3,6 @@ import { capitalize, buildPitfallsBlock, varLine } from './_helpers.mjs'; const VARIABLE_OVERRIDES_BASE = { customEffectExamples: '', offsetEasingSuffix: '', - alternateBoolSuffix: undefined, - iterationsDesc: undefined, }; const VARIABLE_OVERRIDES = { diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 0c25ef42..c49ee14f 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -3,17 +3,17 @@ import { capitalize, buildMarkdownTable } from './_helpers.mjs'; const FULL_LEAN_PITFALL_ORDER = [ { id: 'overflow-clip', section: 'long' }, { id: 'same-element-viewenter', section: 'long' }, - { id: 'hit-area', section: 'detailed-hover' }, - { id: 'hit-area', section: 'detailed-pointermove' }, + { id: 'hit-area', section: 'full-lean-hover' }, + { id: 'hit-area', section: 'full-lean-pointermove' }, ]; function buildBehaviorTable(headerLabel, behaviorKey, triggers, { defaultKey } = {}) { - const keys = Object.keys(triggers[0][behaviorKey]); + const keys = [...new Set(triggers.flatMap((t) => Object.keys(t[behaviorKey])))]; const rows = keys.map((k) => { const isDefault = defaultKey ? k === defaultKey : false; const label = isDefault ? `\`'${k}'\` (default)` : `\`'${k}'\``; - return [label, ...triggers.map((t) => t[behaviorKey][k].short)]; + return [label, ...triggers.map((t) => t[behaviorKey][k]?.short ?? '—')]; }); return buildMarkdownTable( @@ -62,6 +62,14 @@ export function render(data, fragments) { Object.entries(data.effects.rangeNames).map(([name, desc]) => [`\`${name}\``, desc]), ); + const metaParams = { + installCommand: data.meta.installCommand, + webEntry: data.meta.entryPoints.web, + reactEntry: data.meta.entryPoints.react, + vanillaEntry: data.meta.entryPoints.vanilla, + presetsPackage: data.meta.presetsPackage, + }; + return `# ${data.meta.packageName} — Rules Declarative configuration-driven interaction library. Binds animations to triggers via JSON config. @@ -105,19 +113,19 @@ ${fragments.get('pitfalls/perspective', 'default')} ## Quick Start -${fragments.get('quick-start', 'install')} +${fragments.get('quick-start', 'install', metaParams)} ${fragments.get('quick-start', 'multiple-instances')} -${fragments.get('quick-start', 'web')} +${fragments.get('quick-start', 'web', metaParams)} -${fragments.get('quick-start', 'react')} +${fragments.get('quick-start', 'react', metaParams)} -${fragments.get('quick-start', 'vanilla')} +${fragments.get('quick-start', 'vanilla', metaParams)} -${fragments.get('quick-start', 'cdn')} +${fragments.get('quick-start', 'cdn', metaParams)} -${fragments.get('quick-start', 'register-presets')} +${fragments.get('quick-start', 'register-presets', metaParams)} --- diff --git a/packages/interact/_content/templates/integration.mjs b/packages/interact/_content/templates/integration.mjs index 49eda837..a815cb3b 100644 --- a/packages/interact/_content/templates/integration.mjs +++ b/packages/interact/_content/templates/integration.mjs @@ -4,6 +4,14 @@ * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { + const metaParams = { + installCommand: data.meta.installCommand, + webEntry: data.meta.entryPoints.web, + reactEntry: data.meta.entryPoints.react, + vanillaEntry: data.meta.entryPoints.vanilla, + presetsPackage: data.meta.presetsPackage, + }; + return `# ${data.meta.packageName} Integration Rules Rules for integrating \`${data.meta.packageName}\` into a webpage — binding animations and effects to user-driven triggers via declarative configuration. @@ -30,11 +38,11 @@ Rules for integrating \`${data.meta.packageName}\` into a webpage — binding an Install with your package manager: -${fragments.get('quick-start', 'install')} +${fragments.get('quick-start', 'install', metaParams)} ### Web (Custom Elements) -${fragments.get('quick-start', 'web-brief')} +${fragments.get('quick-start', 'web-brief', metaParams)} Wrap target elements with \`\`: @@ -84,7 +92,7 @@ import { Interaction } from '@wix/interact/react'; ### Vanilla JS -${fragments.get('quick-start', 'vanilla-brief')} +${fragments.get('quick-start', 'vanilla-brief', metaParams)} **Rules:** diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index 08e148fe..f1aa3a32 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -47,57 +47,55 @@ const TRIGGER_SCHEMA = { 'pointermove-rule.mjs': ['params', 'pitfalls'], }; -for (const trigger of data.triggers) { - const required = TRIGGER_SCHEMA[trigger.template]; - if (!required) continue; - for (const field of required) { - if (trigger[field] === undefined) { - throw new Error( - `triggers.yaml: trigger "${trigger.name}" is missing required field "${field}" (needed by ${trigger.template})`, - ); - } - } - - if (trigger.pitfalls !== undefined) { - if (!Array.isArray(trigger.pitfalls)) { +const FIELD_VALIDATORS = { + pitfalls(arr, trigger) { + if (!Array.isArray(arr)) throw new Error(`triggers.yaml: trigger "${trigger.name}".pitfalls must be an array`); - } - for (const p of trigger.pitfalls) { - if (!p.id) { + for (const p of arr) { + if (!p.id) throw new Error( `triggers.yaml: trigger "${trigger.name}" has a pitfall entry missing "id"`, ); - } } - } - - if (trigger.params !== undefined) { - if (!Array.isArray(trigger.params)) { + }, + params(arr, trigger) { + if (!Array.isArray(arr)) throw new Error(`triggers.yaml: trigger "${trigger.name}".params must be an array`); - } - for (const p of trigger.params) { - if (!p.name) { + for (const p of arr) { + if (!p.name) throw new Error( `triggers.yaml: trigger "${trigger.name}" has a param entry missing "name"`, ); - } } - } - - for (const field of ['hasReversed', 'hasEffectId']) { - if (trigger[field] !== undefined && typeof trigger[field] !== 'boolean') { - throw new Error(`triggers.yaml: trigger "${trigger.name}".${field} must be a boolean`); - } - } - - if (trigger.triggerTypeDescriptions) { - for (const [key, val] of Object.entries(trigger.triggerTypeDescriptions)) { - if (typeof val !== 'object' || !val.full) { + }, + hasReversed(val, trigger) { + if (typeof val !== 'boolean') + throw new Error(`triggers.yaml: trigger "${trigger.name}".hasReversed must be a boolean`); + }, + hasEffectId(val, trigger) { + if (typeof val !== 'boolean') + throw new Error(`triggers.yaml: trigger "${trigger.name}".hasEffectId must be a boolean`); + }, + triggerTypeDescriptions(obj, trigger) { + for (const [key, val] of Object.entries(obj)) { + if (typeof val !== 'object' || !val.full) throw new Error( `triggers.yaml: trigger "${trigger.name}".triggerTypeDescriptions.${key} must be an object with a "full" key`, ); - } } + }, +}; + +for (const trigger of data.triggers) { + const required = TRIGGER_SCHEMA[trigger.template]; + if (!required) continue; + for (const field of required) { + if (trigger[field] === undefined) { + throw new Error( + `triggers.yaml: trigger "${trigger.name}" is missing required field "${field}" (needed by ${trigger.template})`, + ); + } + FIELD_VALIDATORS[field]?.(trigger[field], trigger); } } From 01949996f744d037f2de3b571e3aad1d396a791d Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 13:33:27 +0300 Subject: [PATCH 20/27] PR fixes --- .../fragments/full-lean/element-binding.md | 29 ++++++++++++++++ .../fragments/full-lean/interactions.md | 24 ++++++++++++++ .../fragments/full-lean/state-effect.md | 33 +++++++++++++++++++ .../_content/templates/event-trigger-rule.mjs | 2 +- .../interact/_content/templates/full-lean.mjs | 8 +---- .../_content/templates/integration.mjs | 12 ++----- .../_content/templates/viewenter-rule.mjs | 2 +- packages/interact/scripts/build-rules.mjs | 33 ++++++++++++++----- 8 files changed, 117 insertions(+), 26 deletions(-) create mode 100644 packages/interact/_content/fragments/full-lean/element-binding.md create mode 100644 packages/interact/_content/fragments/full-lean/interactions.md create mode 100644 packages/interact/_content/fragments/full-lean/state-effect.md diff --git a/packages/interact/_content/fragments/full-lean/element-binding.md b/packages/interact/_content/fragments/full-lean/element-binding.md new file mode 100644 index 00000000..484dd018 --- /dev/null +++ b/packages/interact/_content/fragments/full-lean/element-binding.md @@ -0,0 +1,29 @@ + +## Element Binding + +**CRITICAL:** Do NOT add observers/event listeners manually. The runtime binds triggers and effects via element keys. + +### Web: `` + +- MUST set `data-interact-key` to a unique value. +- MUST contain at least one child element (the library targets `.firstElementChild`). +- If an effect targets a different element, that element also needs its own ``. + +```html + +
...
+
+``` + +### React: `` component + +- MUST set `tagName` to the replaced element's HTML tag. +- MUST set `interactKey` to a unique string. + +```tsx +import { Interaction } from '@wix/interact/react'; + + + ... +; +``` diff --git a/packages/interact/_content/fragments/full-lean/interactions.md b/packages/interact/_content/fragments/full-lean/interactions.md new file mode 100644 index 00000000..432ae2fa --- /dev/null +++ b/packages/interact/_content/fragments/full-lean/interactions.md @@ -0,0 +1,24 @@ + +## Interactions + +Each interaction maps a source element + trigger to one or more effects. + +**Multiple effects per interaction:** A single interaction can contain multiple effects in its `effects` array. All effects in the same interaction share the same trigger — they all fire together when the trigger activates. Use this to apply different animations to different targets from the same trigger event, rather than creating separate interactions with duplicate trigger configs. + +```ts +{ + key: string; // REQUIRED — matches data-interact-key / interactKey - the root element + trigger: TriggerType; // REQUIRED + params?: TriggerParams; // trigger-specific options + effects?: (Effect | EffectRef)[]; // possible to add multiple effects for same trigger + sequences?: (SequenceConfig | SequenceConfigRef)[]; // possible to add multiple sequences for same trigger + conditions?: string[]; // ids referencing the top-level conditions map; all must pass + selector?: string; // optional - CSS selector to refine source element selection within the root element + listContainer?: string; // optional — CSS selector for list container + listItemSelector?: string; // optional — CSS selector to filter which children of listContainer are observed as sources +} +``` + +At least one of `effects` or `sequences` MUST be provided. + +For most use cases, `key` alone is sufficient for both source and target resolution. The `selector`, `listContainer`, and `listItemSelector` fields are only needed for advanced patterns (lists, delegated triggers, child targeting). See [Element Resolution](#element-resolution) for details. diff --git a/packages/interact/_content/fragments/full-lean/state-effect.md b/packages/interact/_content/fragments/full-lean/state-effect.md new file mode 100644 index 00000000..7e8ee7c5 --- /dev/null +++ b/packages/interact/_content/fragments/full-lean/state-effect.md @@ -0,0 +1,33 @@ + +### StateEffect (CSS style toggle) + +Used with `hover` / `click` triggers. Set `stateAction` on the effect to control state behavior. + +**StateEffect** (CSS transition-style state toggles): + +- `key?`: string (target override; see TARGET CASCADE) +- `effectId?`: string (when used as a reference identity) +- One of: + - `transition?`: `{ duration?: number; delay?: number; easing?: string; styleProperties: { name: string; value: string }[] }` + - Applies a single transition options block to all listed style properties. + - `transitionProperties?`: `Array<{ name: string; value: string; duration?: number; delay?: number; easing?: string }>` + - Allows per-property transition options. If both `transition` and `transitionProperties` are provided, the system SHOULD apply both with per-property entries taking precedence for overlapping properties. + +```ts +// Shared timing for all properties: +{ + transition: { + duration?: number; delay?: number; easing?: string; + styleProperties: [{ name: string; value: string }] + } +} + +// Per-property timing: +{ + transitionProperties: [ + { name: string; value: string; duration?: number; delay?: number; easing?: string } + ] +} +``` + +CSS property names use **camelCase** (e.g. `'backgroundColor'`, `'borderRadius'`). diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index c0860e43..8d890b1d 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -52,7 +52,7 @@ export function render(data, fragments) { const pitfallsBlock = buildPitfallsBlock(trigger, fragments, { wrapped: true }); const multipleEffectsNote = trigger.showMultipleEffectsNote - ? `\n${fragments.get('multiple-effects-note', { triggerName: name, triggerEvent: `${name} event`, triggerContext: '', extraNote: '' })}\n` + ? `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event`, triggerContext: '', extraNote: '' })}\n` : ''; return `# ${Name} Trigger Rules for ${data.meta.packageName} diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index c49ee14f..d90ad85a 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -62,13 +62,7 @@ export function render(data, fragments) { Object.entries(data.effects.rangeNames).map(([name, desc]) => [`\`${name}\``, desc]), ); - const metaParams = { - installCommand: data.meta.installCommand, - webEntry: data.meta.entryPoints.web, - reactEntry: data.meta.entryPoints.react, - vanillaEntry: data.meta.entryPoints.vanilla, - presetsPackage: data.meta.presetsPackage, - }; + const { metaParams } = data; return `# ${data.meta.packageName} — Rules diff --git a/packages/interact/_content/templates/integration.mjs b/packages/interact/_content/templates/integration.mjs index a815cb3b..d62ac11c 100644 --- a/packages/interact/_content/templates/integration.mjs +++ b/packages/interact/_content/templates/integration.mjs @@ -4,13 +4,7 @@ * @param {import('../../scripts/build-rules.mjs').Fragments} fragments */ export function render(data, fragments) { - const metaParams = { - installCommand: data.meta.installCommand, - webEntry: data.meta.entryPoints.web, - reactEntry: data.meta.entryPoints.react, - vanillaEntry: data.meta.entryPoints.vanilla, - presetsPackage: data.meta.presetsPackage, - }; + const { metaParams } = data; return `# ${data.meta.packageName} Integration Rules @@ -64,7 +58,7 @@ Wrap target elements with \`\`: \`\`\`typescript import { useEffect } from 'react'; -import { Interact } from '@wix/interact/react'; +import { Interact } from '${data.meta.entryPoints.react}'; useEffect(() => { const instance = Interact.create(config); @@ -78,7 +72,7 @@ useEffect(() => { Replace target elements with \`\`: \`\`\`tsx -import { Interaction } from '@wix/interact/react'; +import { Interaction } from '${data.meta.entryPoints.react}'; ... diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index df3d2370..676d2e23 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -91,7 +91,7 @@ ${fragments.get('fouc', 'code-vanilla', { key: '[SOURCE_KEY]', classAttr: '' })} Use \`keyframeEffect\` or \`namedEffect\` when the viewEnter should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior. Use \`params\` only for observer configuration (\`threshold\`, \`inset\`). -${fragments.get('multiple-effects-note', { triggerName: 'viewEnter', triggerEvent: 'viewport entry event', triggerContext: ' when the element enters the viewport', extraNote: ' Each effect can have its own `triggerType`.' })} +${fragments.get('multiple-effects-note', 'default', { triggerName: 'viewEnter', triggerEvent: 'viewport entry event', triggerContext: ' when the element enters the viewport', extraNote: ' Each effect can have its own `triggerType`.' })} \`\`\`typescript { diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index f1aa3a32..4c0c5f78 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -23,10 +23,19 @@ const triggersData = loadYaml('triggers.yaml'); const effectsData = loadYaml('effects.yaml'); const metaData = loadYaml('meta.yaml'); +const metaParams = { + installCommand: metaData.installCommand, + webEntry: metaData.entryPoints.web, + reactEntry: metaData.entryPoints.react, + vanillaEntry: metaData.entryPoints.vanilla, + presetsPackage: metaData.presetsPackage, +}; + const data = { triggers: triggersData.triggers, effects: effectsData, meta: metaData, + metaParams, }; // --------------------------------------------------------------------------- @@ -41,6 +50,7 @@ const TRIGGER_SCHEMA = { 'hasEffectId', 'triggerTypeDescriptions', 'stateActionDescriptions', + 'showMultipleEffectsNote', ], 'viewenter-rule.mjs': ['params', 'pitfalls', 'triggerTypeDescriptions'], 'viewprogress-rule.mjs': ['params', 'pitfalls'], @@ -84,6 +94,20 @@ const FIELD_VALIDATORS = { ); } }, + stateActionDescriptions(obj, trigger) { + for (const [key, val] of Object.entries(obj)) { + if (typeof val !== 'object' || !val.full) + throw new Error( + `triggers.yaml: trigger "${trigger.name}".stateActionDescriptions.${key} must be an object with a "full" key`, + ); + } + }, + showMultipleEffectsNote(val, trigger) { + if (typeof val !== 'boolean') + throw new Error( + `triggers.yaml: trigger "${trigger.name}".showMultipleEffectsNote must be a boolean`, + ); + }, }; for (const trigger of data.triggers) { @@ -151,14 +175,7 @@ class Fragments { return sections; } - get(path, sectionOrParams = 'default', params = {}) { - let section; - if (typeof sectionOrParams === 'object') { - section = 'default'; - params = sectionOrParams; - } else { - section = sectionOrParams; - } + get(path, section = 'default', params = {}) { const sectionMap = this.store.get(path); if (!sectionMap) { throw new Error(`Fragment not found: ${path}`); From ba7d4ad5534161558a8f1b6d85e8885695bcbab4 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 14:02:12 +0300 Subject: [PATCH 21/27] PR fixes --- packages/interact/_content/data/effects.yaml | 117 ------------- packages/interact/_content/data/meta.yaml | 8 - packages/interact/_content/data/triggers.yaml | 159 ------------------ .../fragments/full-lean/element-binding.md | 29 ---- .../fragments/full-lean/interactions.md | 24 --- .../fragments/full-lean/state-effect.md | 33 ---- .../interact/_content/templates/_helpers.mjs | 16 +- .../_content/templates/event-trigger-rule.mjs | 51 +++--- .../interact/_content/templates/full-lean.mjs | 103 +++++++++++- packages/interact/package.json | 1 - packages/interact/scripts/build-rules.mjs | 120 ++----------- yarn.lock | 3 +- 12 files changed, 134 insertions(+), 530 deletions(-) delete mode 100644 packages/interact/_content/data/effects.yaml delete mode 100644 packages/interact/_content/data/meta.yaml delete mode 100644 packages/interact/_content/data/triggers.yaml delete mode 100644 packages/interact/_content/fragments/full-lean/element-binding.md delete mode 100644 packages/interact/_content/fragments/full-lean/interactions.md delete mode 100644 packages/interact/_content/fragments/full-lean/state-effect.md diff --git a/packages/interact/_content/data/effects.yaml b/packages/interact/_content/data/effects.yaml deleted file mode 100644 index 7c50407d..00000000 --- a/packages/interact/_content/data/effects.yaml +++ /dev/null @@ -1,117 +0,0 @@ -triggerTypes: - - once - - repeat - - alternate - - state - -easings: - - linear - - ease - - ease-in - - ease-out - - ease-in-out - - sineIn - - sineOut - - sineInOut - - quadIn - - quadOut - - quadInOut - - cubicIn - - cubicOut - - cubicInOut - - quartIn - - quartOut - - quartInOut - - quintIn - - quintOut - - quintInOut - - expoIn - - expoOut - - expoInOut - - circIn - - circOut - - circInOut - - backIn - - backOut - - backInOut - -transitionEasings: - - linear - - hardBackOut - - easeOut - - elastic - - bounce - -presets: - entrance: - - FadeIn - - GlideIn - - SlideIn - - FloatIn - - RevealIn - - ExpandIn - - BlurIn - - FlipIn - - ArcIn - - ShuttersIn - - CurveIn - - DropIn - - FoldIn - - ShapeIn - - TiltIn - - WinkIn - - SpinIn - - TurnIn - - BounceIn - ongoing: - - Pulse - - Spin - - Breathe - - Bounce - - Wiggle - - Flash - - Flip - - Fold - - Jello - - Poke - - Rubber - - Swing - - Cross - scroll: - - FadeScroll - - RevealScroll - - ParallaxScroll - - MoveScroll - - SlideScroll - - GrowScroll - - ShrinkScroll - - TiltScroll - - PanScroll - - BlurScroll - - FlipScroll - - SpinScroll - - ArcScroll - - ShapeScroll - - ShuttersScroll - - SkewPanScroll - - Spin3dScroll - - StretchScroll - - TurnScroll - mouse: - - TrackMouse - - Tilt3DMouse - - Track3DMouse - - SwivelMouse - - AiryMouse - - ScaleMouse - - BlurMouse - - SkewMouse - - BlobMouse - -rangeNames: - cover: 'Full visibility span from first pixel entering to last pixel leaving' - entry: 'The phase while the element is entering the viewport' - exit: 'The phase while the element is exiting the viewport' - contain: 'While the element is fully contained in the viewport. Typically used with a `position: sticky` container' - entry-crossing: "From the element's leading edge entering to its leading edge reaching the opposite side" - exit-crossing: "From the element's trailing edge reaching the start to its trailing edge leaving" diff --git a/packages/interact/_content/data/meta.yaml b/packages/interact/_content/data/meta.yaml deleted file mode 100644 index 190365bc..00000000 --- a/packages/interact/_content/data/meta.yaml +++ /dev/null @@ -1,8 +0,0 @@ -packageName: '@wix/interact' -presetsPackage: '@wix/motion-presets' -motionPackage: '@wix/motion' -installCommand: 'npm install @wix/interact @wix/motion-presets' -entryPoints: - web: '@wix/interact/web' - react: '@wix/interact/react' - vanilla: '@wix/interact' diff --git a/packages/interact/_content/data/triggers.yaml b/packages/interact/_content/data/triggers.yaml deleted file mode 100644 index 1463eceb..00000000 --- a/packages/interact/_content/data/triggers.yaml +++ /dev/null @@ -1,159 +0,0 @@ -# -------------------------------------------------------------------------- -# Event triggers — rendered by event-trigger-rule.mjs (click.md, hover.md) -# These have the full schema: hasReversed, hasEffectId, triggerTypeDescriptions, -# stateActionDescriptions, showMultipleEffectsNote. -# -------------------------------------------------------------------------- -triggers: - - name: hover - template: event-trigger-rule.mjs - a11yAlias: interest - a11yNote: "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus." - defaultTriggerType: alternate - params: [] - pitfalls: - - id: hit-area - showMultipleEffectsNote: true - hasReversed: false - hasEffectId: false - triggerTypeDescriptions: - alternate: - full: 'plays forward on enter, reverses on leave. Default. Most common for hover.' - short: 'Play on enter, reverse on leave' - repeat: - full: 'restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses.' - short: 'Play on enter, stop and rewind on leave' - once: - full: 'plays once on the first enter and never again.' - short: 'Play once on first enter only' - state: - full: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`).' - short: 'Play on enter, pause on leave' - stateActionDescriptions: - toggle: - full: 'applies the style state on enter, removes on leave. Default.' - short: 'Add style state on enter, remove on leave' - add: - full: 'applies the style state on enter. Leave does NOT remove it.' - short: 'Add style state on enter; leave does NOT remove' - remove: - full: 'removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`.' - short: 'Remove style state on enter' - clear: - full: 'clears all previously applied style states on enter. Use to reset multiple stacked `''add''` style changes at once (e.g. a "reset" hover area that undoes several accumulated states).' - short: 'Clear/reset all style states on enter' - - - name: click - template: event-trigger-rule.mjs - a11yAlias: activate - a11yNote: "Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space)." - defaultTriggerType: alternate - params: [] - pitfalls: [] - showMultipleEffectsNote: false - hasReversed: true - hasEffectId: true - triggerTypeDescriptions: - alternate: - full: 'plays forward on first click, reverses on next click. Default.' - short: 'Alternate play/reverse per click' - repeat: - full: 'restarts the animation from the beginning on each click.' - short: 'Restart per click' - once: - full: 'plays once on the first click and never again.' - short: 'Play once on first click only' - state: - full: 'resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`).' - short: 'Toggle play/pause per click' - stateActionDescriptions: - toggle: - full: 'applies the style state, removes it on next click. Default.' - short: 'Toggle style state per click' - add: - full: 'applies the style state. Does not remove on subsequent clicks.' - short: 'Add style state on click' - remove: - full: 'removes a previously applied style state.' - short: 'Remove style state on click' - clear: - full: 'clears all previously applied style states. Useful for resetting multiple stacked style states at once.' - short: 'Clear/reset all style states' - - # -------------------------------------------------------------------------- - # Viewport/scroll triggers — each has its own dedicated template. - # Minimal schema: defaultTriggerType, params (with varName), pitfalls. - # -------------------------------------------------------------------------- - - name: viewEnter - template: viewenter-rule.mjs - defaultTriggerType: once - params: - - name: threshold - varName: VISIBILITY_THRESHOLD - type: number - optional: true - description: 'Number between 0–1 indicating how much of the source element must be visible to trigger (e.g. `0.3` = 30%).' - - name: inset - varName: VIEWPORT_INSETS - type: string - optional: true - description: "String adjusting the viewport detection area (e.g. `'-100px'` extends it, `'50px'` shrinks it)." - pitfalls: - - id: same-element-viewenter - section: short - showMultipleEffectsNote: true - triggerTypeDescriptions: - once: - full: 'plays once when the source element first enters the viewport and never again. Source and target may be the same element.' - short: 'Play once on first enter only' - repeat: - full: 'restarts the animation every time the source element enters the viewport. Use separate source and target.' - short: 'Restart on each viewport enter' - alternate: - full: 'plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target.' - short: 'Play on enter, reverse on leave' - state: - full: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`). Use separate source and target.' - short: 'Play on enter, pause on leave' - - - name: viewProgress - template: viewprogress-rule.mjs - defaultTriggerType: null - params: [] - pitfalls: - - id: overflow-clip - section: short - showMultipleEffectsNote: true - - # -------------------------------------------------------------------------- - # Pointer/chaining triggers — dedicated templates or no template. - # -------------------------------------------------------------------------- - - name: pointerMove - template: pointermove-rule.mjs - defaultTriggerType: null - params: - - name: hitArea - type: "'root' | 'self'" - optional: true - description: 'determines where mouse movement is tracked' - - name: axis - type: "'x' | 'y'" - optional: true - description: 'restricts pointer tracking to a single axis' - pitfalls: - - id: hit-area - section: pointermove-source - - - name: animationEnd - template: null - defaultTriggerType: null - params: - - name: effectId - type: string - optional: false - description: 'ID of the preceding effect' - - - name: pageVisible - template: null - defaultTriggerType: null - params: [] - pitfalls: [] diff --git a/packages/interact/_content/fragments/full-lean/element-binding.md b/packages/interact/_content/fragments/full-lean/element-binding.md deleted file mode 100644 index 484dd018..00000000 --- a/packages/interact/_content/fragments/full-lean/element-binding.md +++ /dev/null @@ -1,29 +0,0 @@ - -## Element Binding - -**CRITICAL:** Do NOT add observers/event listeners manually. The runtime binds triggers and effects via element keys. - -### Web: `` - -- MUST set `data-interact-key` to a unique value. -- MUST contain at least one child element (the library targets `.firstElementChild`). -- If an effect targets a different element, that element also needs its own ``. - -```html - -
...
-
-``` - -### React: `` component - -- MUST set `tagName` to the replaced element's HTML tag. -- MUST set `interactKey` to a unique string. - -```tsx -import { Interaction } from '@wix/interact/react'; - - - ... -; -``` diff --git a/packages/interact/_content/fragments/full-lean/interactions.md b/packages/interact/_content/fragments/full-lean/interactions.md deleted file mode 100644 index 432ae2fa..00000000 --- a/packages/interact/_content/fragments/full-lean/interactions.md +++ /dev/null @@ -1,24 +0,0 @@ - -## Interactions - -Each interaction maps a source element + trigger to one or more effects. - -**Multiple effects per interaction:** A single interaction can contain multiple effects in its `effects` array. All effects in the same interaction share the same trigger — they all fire together when the trigger activates. Use this to apply different animations to different targets from the same trigger event, rather than creating separate interactions with duplicate trigger configs. - -```ts -{ - key: string; // REQUIRED — matches data-interact-key / interactKey - the root element - trigger: TriggerType; // REQUIRED - params?: TriggerParams; // trigger-specific options - effects?: (Effect | EffectRef)[]; // possible to add multiple effects for same trigger - sequences?: (SequenceConfig | SequenceConfigRef)[]; // possible to add multiple sequences for same trigger - conditions?: string[]; // ids referencing the top-level conditions map; all must pass - selector?: string; // optional - CSS selector to refine source element selection within the root element - listContainer?: string; // optional — CSS selector for list container - listItemSelector?: string; // optional — CSS selector to filter which children of listContainer are observed as sources -} -``` - -At least one of `effects` or `sequences` MUST be provided. - -For most use cases, `key` alone is sufficient for both source and target resolution. The `selector`, `listContainer`, and `listItemSelector` fields are only needed for advanced patterns (lists, delegated triggers, child targeting). See [Element Resolution](#element-resolution) for details. diff --git a/packages/interact/_content/fragments/full-lean/state-effect.md b/packages/interact/_content/fragments/full-lean/state-effect.md deleted file mode 100644 index 7e8ee7c5..00000000 --- a/packages/interact/_content/fragments/full-lean/state-effect.md +++ /dev/null @@ -1,33 +0,0 @@ - -### StateEffect (CSS style toggle) - -Used with `hover` / `click` triggers. Set `stateAction` on the effect to control state behavior. - -**StateEffect** (CSS transition-style state toggles): - -- `key?`: string (target override; see TARGET CASCADE) -- `effectId?`: string (when used as a reference identity) -- One of: - - `transition?`: `{ duration?: number; delay?: number; easing?: string; styleProperties: { name: string; value: string }[] }` - - Applies a single transition options block to all listed style properties. - - `transitionProperties?`: `Array<{ name: string; value: string; duration?: number; delay?: number; easing?: string }>` - - Allows per-property transition options. If both `transition` and `transitionProperties` are provided, the system SHOULD apply both with per-property entries taking precedence for overlapping properties. - -```ts -// Shared timing for all properties: -{ - transition: { - duration?: number; delay?: number; easing?: string; - styleProperties: [{ name: string; value: string }] - } -} - -// Per-property timing: -{ - transitionProperties: [ - { name: string; value: string; duration?: number; delay?: number; easing?: string } - ] -} -``` - -CSS property names use **camelCase** (e.g. `'backgroundColor'`, `'borderRadius'`). diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs index e1b90c02..aac6e749 100644 --- a/packages/interact/_content/templates/_helpers.mjs +++ b/packages/interact/_content/templates/_helpers.mjs @@ -4,7 +4,7 @@ export function capitalize(s) { /** * Builds the pitfalls block for a trigger template. - * Iterates trigger.pitfalls from YAML, resolving each fragment section. + * Iterates trigger.pitfalls from data, resolving each fragment section. * When `wrapped` is true, wraps non-empty output with leading/trailing newlines * (the common pattern used by event-trigger, viewenter, and viewprogress templates). */ @@ -16,20 +16,6 @@ export function buildPitfallsBlock(trigger, fragments, { wrapped = false } = {}) return wrapped ? `\n${content}\n` : content; } -/** - * Builds a Prettier-compatible padded markdown table. - * @param {string[]} headers — column header labels - * @param {string[][]} rows — array of rows, each an array of cell strings - */ -export function buildMarkdownTable(headers, rows) { - const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || '').length))); - return [ - `| ${headers.map((h, i) => h.padEnd(widths[i])).join(' | ')} |`, - `| ${widths.map((w) => `:${'-'.repeat(w - 1)}`).join(' | ')} |`, - ...rows.map((r) => `| ${r.map((c, i) => (c || '').padEnd(widths[i])).join(' | ')} |`), - ].join('\n'); -} - /** * Common variable descriptions. * - mode 'suffix': arg is appended after base (separated by space) diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 8d890b1d..b4b14957 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,40 +1,39 @@ import { capitalize, buildPitfallsBlock, varLine } from './_helpers.mjs'; -const VARIABLE_OVERRIDES_BASE = { - customEffectExamples: '', - offsetEasingSuffix: '', -}; - -const VARIABLE_OVERRIDES = { - hover: { - ...VARIABLE_OVERRIDES_BASE, - sourceKeySuffix: 'The element that listens for hover.', - targetKeyDesc: - "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", - fillModeDesc: - "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.", - easingDesc: - "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.", - iterationsDesc: - "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", - fillCritical: - "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", - offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.', - }, - click: { - ...VARIABLE_OVERRIDES_BASE, +function getOverrides(name) { + if (name === 'hover') { + return { + sourceKeySuffix: 'The element that listens for hover.', + targetKeyDesc: + "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", + fillModeDesc: + "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.", + easingDesc: + "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.", + iterationsDesc: + "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", + fillCritical: + "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", + customEffectExamples: '', + offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.', + alternateBoolSuffix: '', + }; + } + return { sourceKeySuffix: 'The element that listens for clicks.', targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", easingDesc: 'CSS easing string, or named easing from `@wix/motion`.', + iterationsDesc: undefined, fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.", customEffectExamples: ', randomized behavior', + offsetEasingSuffix: '', alternateBoolSuffix: "Different from `triggerType: 'alternate'` which alternates per click.", - }, -}; + }; +} /** * Renders a trigger-specific rule file (click.md or hover.md). @@ -44,7 +43,7 @@ const VARIABLE_OVERRIDES = { export function render(data, fragments) { const { trigger } = data; const { name } = trigger; - const vo = VARIABLE_OVERRIDES[name]; + const vo = getOverrides(name); const Name = capitalize(name); const hasReversed = trigger.hasReversed; const hasEffectId = trigger.hasEffectId; diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index d90ad85a..26ef362d 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -1,4 +1,4 @@ -import { capitalize, buildMarkdownTable } from './_helpers.mjs'; +import { capitalize } from './_helpers.mjs'; const FULL_LEAN_PITFALL_ORDER = [ { id: 'overflow-clip', section: 'long' }, @@ -7,6 +7,15 @@ const FULL_LEAN_PITFALL_ORDER = [ { id: 'hit-area', section: 'full-lean-pointermove' }, ]; +function buildMarkdownTable(headers, rows) { + const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || '').length))); + return [ + `| ${headers.map((h, i) => h.padEnd(widths[i])).join(' | ')} |`, + `| ${widths.map((w) => `:${'-'.repeat(w - 1)}`).join(' | ')} |`, + ...rows.map((r) => `| ${r.map((c, i) => (c || '').padEnd(widths[i])).join(' | ')} |`), + ].join('\n'); +} + function buildBehaviorTable(headerLabel, behaviorKey, triggers, { defaultKey } = {}) { const keys = [...new Set(triggers.flatMap((t) => Object.keys(t[behaviorKey])))]; @@ -29,10 +38,8 @@ function buildFullLeanPitfalls(pitfallOrder, fragments) { /** * Renders full-lean.md — the comprehensive reference for all triggers, effects, and API surface. * - * The largest static prose sections (Element Binding, Interactions, StateEffect) are extracted to - * `full-lean/` fragments for editor ergonomics (markdown highlighting, spell-checking). Remaining - * static sections (Effects preamble, FOUC Prevention) are kept inline — they interleave with - * dynamic content and extracting them would fragment the template's flow. + * Static prose sections are kept inline — they interleave with dynamic content and have a single + * consumer, so extraction would add indirection without deduplication benefit. * * @param {{ triggers: object[], effects: object, meta: object }} data — no `trigger`; receives the full data object * @param {import('../../scripts/build-rules.mjs').Fragments} fragments @@ -123,7 +130,34 @@ ${fragments.get('quick-start', 'register-presets', metaParams)} --- -${fragments.get('full-lean/element-binding')} +## Element Binding + +**CRITICAL:** Do NOT add observers/event listeners manually. The runtime binds triggers and effects via element keys. + +### Web: \`\` + +- MUST set \`data-interact-key\` to a unique value. +- MUST contain at least one child element (the library targets \`.firstElementChild\`). +- If an effect targets a different element, that element also needs its own \`\`. + +\`\`\`html + +
...
+
+\`\`\` + +### React: \`\` component + +- MUST set \`tagName\` to the replaced element's HTML tag. +- MUST set \`interactKey\` to a unique string. + +\`\`\`tsx +import { Interaction } from '@wix/interact/react'; + + + ... +; +\`\`\` --- @@ -133,7 +167,29 @@ ${fragments.get('config-structure', 'detailed')} --- -${fragments.get('full-lean/interactions')} +## Interactions + +Each interaction maps a source element + trigger to one or more effects. + +**Multiple effects per interaction:** A single interaction can contain multiple effects in its \`effects\` array. All effects in the same interaction share the same trigger — they all fire together when the trigger activates. Use this to apply different animations to different targets from the same trigger event, rather than creating separate interactions with duplicate trigger configs. + +\`\`\`ts +{ + key: string; // REQUIRED — matches data-interact-key / interactKey - the root element + trigger: TriggerType; // REQUIRED + params?: TriggerParams; // trigger-specific options + effects?: (Effect | EffectRef)[]; // possible to add multiple effects for same trigger + sequences?: (SequenceConfig | SequenceConfigRef)[]; // possible to add multiple sequences for same trigger + conditions?: string[]; // ids referencing the top-level conditions map; all must pass + selector?: string; // optional - CSS selector to refine source element selection within the root element + listContainer?: string; // optional — CSS selector for list container + listItemSelector?: string; // optional — CSS selector to filter which children of listContainer are observed as sources +} +\`\`\` + +At least one of \`effects\` or \`sequences\` MUST be provided. + +For most use cases, \`key\` alone is sufficient for both source and target resolution. The \`selector\`, \`listContainer\`, and \`listItemSelector\` fields are only needed for advanced patterns (lists, delegated triggers, child targeting). See [Element Resolution](#element-resolution) for details. --- @@ -342,7 +398,38 @@ ${rangeTable} - Sticky child (\`key\`) with \`position: sticky; top: 0; height: 100vh\`: stays fixed while the wrapper scrolls. This is the ViewTimeline source. - Use \`rangeStart/rangeEnd\` with \`name: 'contain'\` to animate only during the stuck phase. -${fragments.get('full-lean/state-effect')} +### StateEffect (CSS style toggle) + +Used with \`hover\` / \`click\` triggers. Set \`stateAction\` on the effect to control state behavior. + +**StateEffect** (CSS transition-style state toggles): + +- \`key?\`: string (target override; see TARGET CASCADE) +- \`effectId?\`: string (when used as a reference identity) +- One of: + - \`transition?\`: \`{ duration?: number; delay?: number; easing?: string; styleProperties: { name: string; value: string }[] }\` + - Applies a single transition options block to all listed style properties. + - \`transitionProperties?\`: \`Array<{ name: string; value: string; duration?: number; delay?: number; easing?: string }>\` + - Allows per-property transition options. If both \`transition\` and \`transitionProperties\` are provided, the system SHOULD apply both with per-property entries taking precedence for overlapping properties. + +\`\`\`ts +// Shared timing for all properties: +{ + transition: { + duration?: number; delay?: number; easing?: string; + styleProperties: [{ name: string; value: string }] + } +} + +// Per-property timing: +{ + transitionProperties: [ + { name: string; value: string; duration?: number; delay?: number; easing?: string } + ] +} +\`\`\` + +CSS property names use **camelCase** (e.g. \`'backgroundColor'\`, \`'borderRadius'\`). ### Animation Payloads diff --git a/packages/interact/package.json b/packages/interact/package.json index 10b61229..bb042891 100644 --- a/packages/interact/package.json +++ b/packages/interact/package.json @@ -90,7 +90,6 @@ "@types/react-dom": "^18.3.0", "@vitejs/plugin-react": "^4.3.4", "@vitest/coverage-v8": "^4.0.14", - "js-yaml": "^4.1.0", "jsdom": "^24.0.0", "react": "^18.3.1", "react-dom": "^18.3.1", diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index 4c0c5f78..9b8d79fc 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -3,7 +3,6 @@ import { readFileSync, writeFileSync, readdirSync, mkdirSync } from 'node:fs'; import { join, basename, relative } from 'node:path'; import { fileURLToPath } from 'node:url'; -import yaml from 'js-yaml'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); const PKG_ROOT = join(__dirname, '..'); @@ -11,120 +10,25 @@ const CONTENT_DIR = join(PKG_ROOT, '_content'); const OUTPUT_DIR = join(PKG_ROOT, 'rules'); // --------------------------------------------------------------------------- -// 1. Load YAML data +// 1. Load data modules // --------------------------------------------------------------------------- -function loadYaml(name) { - const raw = readFileSync(join(CONTENT_DIR, 'data', name), 'utf8'); - return yaml.load(raw); -} - -const triggersData = loadYaml('triggers.yaml'); -const effectsData = loadYaml('effects.yaml'); -const metaData = loadYaml('meta.yaml'); +const { triggers } = await import(join(CONTENT_DIR, 'data', 'triggers.mjs')); +const { effects } = await import(join(CONTENT_DIR, 'data', 'effects.mjs')); +const { meta } = await import(join(CONTENT_DIR, 'data', 'meta.mjs')); const metaParams = { - installCommand: metaData.installCommand, - webEntry: metaData.entryPoints.web, - reactEntry: metaData.entryPoints.react, - vanillaEntry: metaData.entryPoints.vanilla, - presetsPackage: metaData.presetsPackage, -}; - -const data = { - triggers: triggersData.triggers, - effects: effectsData, - meta: metaData, - metaParams, + installCommand: meta.installCommand, + webEntry: meta.entryPoints.web, + reactEntry: meta.entryPoints.react, + vanillaEntry: meta.entryPoints.vanilla, + presetsPackage: meta.presetsPackage, }; -// --------------------------------------------------------------------------- -// 1b. Validate YAML schema -// --------------------------------------------------------------------------- - -const TRIGGER_SCHEMA = { - 'event-trigger-rule.mjs': [ - 'a11yAlias', - 'a11yNote', - 'hasReversed', - 'hasEffectId', - 'triggerTypeDescriptions', - 'stateActionDescriptions', - 'showMultipleEffectsNote', - ], - 'viewenter-rule.mjs': ['params', 'pitfalls', 'triggerTypeDescriptions'], - 'viewprogress-rule.mjs': ['params', 'pitfalls'], - 'pointermove-rule.mjs': ['params', 'pitfalls'], -}; - -const FIELD_VALIDATORS = { - pitfalls(arr, trigger) { - if (!Array.isArray(arr)) - throw new Error(`triggers.yaml: trigger "${trigger.name}".pitfalls must be an array`); - for (const p of arr) { - if (!p.id) - throw new Error( - `triggers.yaml: trigger "${trigger.name}" has a pitfall entry missing "id"`, - ); - } - }, - params(arr, trigger) { - if (!Array.isArray(arr)) - throw new Error(`triggers.yaml: trigger "${trigger.name}".params must be an array`); - for (const p of arr) { - if (!p.name) - throw new Error( - `triggers.yaml: trigger "${trigger.name}" has a param entry missing "name"`, - ); - } - }, - hasReversed(val, trigger) { - if (typeof val !== 'boolean') - throw new Error(`triggers.yaml: trigger "${trigger.name}".hasReversed must be a boolean`); - }, - hasEffectId(val, trigger) { - if (typeof val !== 'boolean') - throw new Error(`triggers.yaml: trigger "${trigger.name}".hasEffectId must be a boolean`); - }, - triggerTypeDescriptions(obj, trigger) { - for (const [key, val] of Object.entries(obj)) { - if (typeof val !== 'object' || !val.full) - throw new Error( - `triggers.yaml: trigger "${trigger.name}".triggerTypeDescriptions.${key} must be an object with a "full" key`, - ); - } - }, - stateActionDescriptions(obj, trigger) { - for (const [key, val] of Object.entries(obj)) { - if (typeof val !== 'object' || !val.full) - throw new Error( - `triggers.yaml: trigger "${trigger.name}".stateActionDescriptions.${key} must be an object with a "full" key`, - ); - } - }, - showMultipleEffectsNote(val, trigger) { - if (typeof val !== 'boolean') - throw new Error( - `triggers.yaml: trigger "${trigger.name}".showMultipleEffectsNote must be a boolean`, - ); - }, -}; - -for (const trigger of data.triggers) { - const required = TRIGGER_SCHEMA[trigger.template]; - if (!required) continue; - for (const field of required) { - if (trigger[field] === undefined) { - throw new Error( - `triggers.yaml: trigger "${trigger.name}" is missing required field "${field}" (needed by ${trigger.template})`, - ); - } - FIELD_VALIDATORS[field]?.(trigger[field], trigger); - } -} +const data = { triggers, effects, meta, metaParams }; // --------------------------------------------------------------------------- -// 2. Load fragments — parse markers +// 2. Load fragments — parse markers // --------------------------------------------------------------------------- class Fragments { @@ -229,7 +133,7 @@ for (const entry of manifest) { if (entry.triggers) { for (const name of entry.triggers) { const trigger = data.triggers.find((t) => t.name === name); - if (!trigger) throw new Error(`Trigger "${name}" not found in triggers.yaml`); + if (!trigger) throw new Error(`Trigger "${name}" not found in data/triggers.mjs`); outputs.push({ file: entry.output(name), content: mod.render({ ...data, trigger }, fragments), diff --git a/yarn.lock b/yarn.lock index e1681071..4bd98bd3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1392,7 +1392,6 @@ __metadata: "@wix/motion": "npm:^2.1.5" fastdom: "npm:^1.0.12" fizban: "npm:^0.7.2" - js-yaml: "npm:^4.1.0" jsdom: "npm:^24.0.0" kuliso: "npm:^0.4.13" react: "npm:^18.3.1" @@ -3555,7 +3554,7 @@ __metadata: languageName: node linkType: hard -"js-yaml@npm:^4.1.0, js-yaml@npm:^4.1.1": +"js-yaml@npm:^4.1.1": version: 4.1.1 resolution: "js-yaml@npm:4.1.1" dependencies: From adb0cd573558168c7e8243888b191fba125abfcb Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 17:27:31 +0300 Subject: [PATCH 22/27] PR fixes --- packages/interact/_content/data/effects.mjs | 120 +++++++++++ packages/interact/_content/data/meta.mjs | 11 + packages/interact/_content/data/triggers.mjs | 199 ++++++++++++++++++ .../interact/_content/fragments/README.md | 8 + .../interact/_content/templates/full-lean.mjs | 6 +- packages/interact/scripts/build-rules.mjs | 10 + 6 files changed, 349 insertions(+), 5 deletions(-) create mode 100644 packages/interact/_content/data/effects.mjs create mode 100644 packages/interact/_content/data/meta.mjs create mode 100644 packages/interact/_content/data/triggers.mjs diff --git a/packages/interact/_content/data/effects.mjs b/packages/interact/_content/data/effects.mjs new file mode 100644 index 00000000..c6fb498b --- /dev/null +++ b/packages/interact/_content/data/effects.mjs @@ -0,0 +1,120 @@ +export const effects = { + triggerTypes: ['once', 'repeat', 'alternate', 'state'], + + easings: [ + 'linear', + 'ease', + 'ease-in', + 'ease-out', + 'ease-in-out', + 'sineIn', + 'sineOut', + 'sineInOut', + 'quadIn', + 'quadOut', + 'quadInOut', + 'cubicIn', + 'cubicOut', + 'cubicInOut', + 'quartIn', + 'quartOut', + 'quartInOut', + 'quintIn', + 'quintOut', + 'quintInOut', + 'expoIn', + 'expoOut', + 'expoInOut', + 'circIn', + 'circOut', + 'circInOut', + 'backIn', + 'backOut', + 'backInOut', + ], + + transitionEasings: ['linear', 'hardBackOut', 'easeOut', 'elastic', 'bounce'], + + presets: { + entrance: [ + 'FadeIn', + 'GlideIn', + 'SlideIn', + 'FloatIn', + 'RevealIn', + 'ExpandIn', + 'BlurIn', + 'FlipIn', + 'ArcIn', + 'ShuttersIn', + 'CurveIn', + 'DropIn', + 'FoldIn', + 'ShapeIn', + 'TiltIn', + 'WinkIn', + 'SpinIn', + 'TurnIn', + 'BounceIn', + ], + ongoing: [ + 'Pulse', + 'Spin', + 'Breathe', + 'Bounce', + 'Wiggle', + 'Flash', + 'Flip', + 'Fold', + 'Jello', + 'Poke', + 'Rubber', + 'Swing', + 'Cross', + ], + scroll: [ + 'FadeScroll', + 'RevealScroll', + 'ParallaxScroll', + 'MoveScroll', + 'SlideScroll', + 'GrowScroll', + 'ShrinkScroll', + 'TiltScroll', + 'PanScroll', + 'BlurScroll', + 'FlipScroll', + 'SpinScroll', + 'ArcScroll', + 'ShapeScroll', + 'ShuttersScroll', + 'SkewPanScroll', + 'Spin3dScroll', + 'StretchScroll', + 'TurnScroll', + ], + mouse: [ + 'TrackMouse', + 'Tilt3DMouse', + 'Track3DMouse', + 'SwivelMouse', + 'AiryMouse', + 'ScaleMouse', + 'BlurMouse', + 'SkewMouse', + 'BlobMouse', + ], + }, + + rangeNames: { + cover: 'Full visibility span from first pixel entering to last pixel leaving', + entry: 'The phase while the element is entering the viewport', + exit: 'The phase while the element is exiting the viewport', + contain: + 'While the element is fully contained in the viewport. Typically used with a `position: sticky` container', + 'entry-crossing': + "From the element's leading edge entering to its leading edge reaching the opposite side", + 'exit-crossing': + "From the element's trailing edge reaching the start to its trailing edge leaving", + }, +}; diff --git a/packages/interact/_content/data/meta.mjs b/packages/interact/_content/data/meta.mjs new file mode 100644 index 00000000..f3d0efd7 --- /dev/null +++ b/packages/interact/_content/data/meta.mjs @@ -0,0 +1,11 @@ +export const meta = { + packageName: '@wix/interact', + presetsPackage: '@wix/motion-presets', + motionPackage: '@wix/motion', + installCommand: 'npm install @wix/interact @wix/motion-presets', + entryPoints: { + web: '@wix/interact/web', + react: '@wix/interact/react', + vanilla: '@wix/interact', + }, +}; diff --git a/packages/interact/_content/data/triggers.mjs b/packages/interact/_content/data/triggers.mjs new file mode 100644 index 00000000..e4026f65 --- /dev/null +++ b/packages/interact/_content/data/triggers.mjs @@ -0,0 +1,199 @@ +// -------------------------------------------------------------------------- +// Event triggers — rendered by event-trigger-rule.mjs (click.md, hover.md) +// -------------------------------------------------------------------------- +export const triggers = [ + { + name: 'hover', + a11yAlias: 'interest', + a11yNote: + "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus.", + defaultTriggerType: 'alternate', + params: [], + pitfalls: [{ id: 'hit-area' }], + showMultipleEffectsNote: true, + hasReversed: false, + hasEffectId: false, + triggerTypeDescriptions: { + alternate: { + full: 'plays forward on enter, reverses on leave. Default. Most common for hover.', + short: 'Play on enter, reverse on leave', + }, + repeat: { + full: 'restarts the animation from the beginning on each enter. On leave, jumps to the beginning and pauses.', + short: 'Play on enter, stop and rewind on leave', + }, + once: { + full: 'plays once on the first enter and never again.', + short: 'Play once on first enter only', + }, + state: { + full: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`).', + short: 'Play on enter, pause on leave', + }, + }, + stateActionDescriptions: { + toggle: { + full: 'applies the style state on enter, removes on leave. Default.', + short: 'Add style state on enter, remove on leave', + }, + add: { + full: 'applies the style state on enter. Leave does NOT remove it.', + short: 'Add style state on enter; leave does NOT remove', + }, + remove: { + full: "removes a previously applied style state on enter. Use with provided `effectId` to map to a matching interaction with `add` and effect with same `effectId`.", + short: 'Remove style state on enter', + }, + clear: { + full: "clears all previously applied style states on enter. Use to reset multiple stacked `'add'` style changes at once (e.g. a \"reset\" hover area that undoes several accumulated states).", + short: 'Clear/reset all style states on enter', + }, + }, + }, + + { + name: 'click', + a11yAlias: 'activate', + a11yNote: + "Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space).", + defaultTriggerType: 'alternate', + params: [], + pitfalls: [], + showMultipleEffectsNote: false, + hasReversed: true, + hasEffectId: true, + triggerTypeDescriptions: { + alternate: { + full: 'plays forward on first click, reverses on next click. Default.', + short: 'Alternate play/reverse per click', + }, + repeat: { + full: 'restarts the animation from the beginning on each click.', + short: 'Restart per click', + }, + once: { + full: 'plays once on the first click and never again.', + short: 'Play once on first click only', + }, + state: { + full: 'resumes/pauses the animation on each click. Useful for continuous loops (`iterations: Infinity`).', + short: 'Toggle play/pause per click', + }, + }, + stateActionDescriptions: { + toggle: { + full: 'applies the style state, removes it on next click. Default.', + short: 'Toggle style state per click', + }, + add: { + full: 'applies the style state. Does not remove on subsequent clicks.', + short: 'Add style state on click', + }, + remove: { + full: 'removes a previously applied style state.', + short: 'Remove style state on click', + }, + clear: { + full: 'clears all previously applied style states. Useful for resetting multiple stacked style states at once.', + short: 'Clear/reset all style states', + }, + }, + }, + + // -------------------------------------------------------------------------- + // Viewport/scroll triggers — each has its own dedicated template. + // -------------------------------------------------------------------------- + { + name: 'viewEnter', + defaultTriggerType: 'once', + params: [ + { + name: 'threshold', + varName: 'VISIBILITY_THRESHOLD', + type: 'number', + optional: true, + description: + 'Number between 0–1 indicating how much of the source element must be visible to trigger (e.g. `0.3` = 30%).', + }, + { + name: 'inset', + varName: 'VIEWPORT_INSETS', + type: 'string', + optional: true, + description: + "String adjusting the viewport detection area (e.g. `'-100px'` extends it, `'50px'` shrinks it).", + }, + ], + pitfalls: [{ id: 'same-element-viewenter', section: 'short' }], + showMultipleEffectsNote: true, + triggerTypeDescriptions: { + once: { + full: 'plays once when the source element first enters the viewport and never again. Source and target may be the same element.', + short: 'Play once on first enter only', + }, + repeat: { + full: 'restarts the animation every time the source element enters the viewport. Use separate source and target.', + short: 'Restart on each viewport enter', + }, + alternate: { + full: 'plays forward when the source element enters the viewport, reverses when it leaves. Use separate source and target.', + short: 'Play on enter, reverse on leave', + }, + state: { + full: 'resumes on enter, pauses on leave. Useful for continuous loops (`iterations: Infinity`). Use separate source and target.', + short: 'Play on enter, pause on leave', + }, + }, + }, + + { + name: 'viewProgress', + defaultTriggerType: null, + params: [], + pitfalls: [{ id: 'overflow-clip', section: 'short' }], + showMultipleEffectsNote: true, + }, + + // -------------------------------------------------------------------------- + // Pointer/chaining triggers + // -------------------------------------------------------------------------- + { + name: 'pointerMove', + defaultTriggerType: null, + params: [ + { + name: 'hitArea', + type: "'root' | 'self'", + optional: true, + description: 'determines where mouse movement is tracked', + }, + { + name: 'axis', + type: "'x' | 'y'", + optional: true, + description: 'restricts pointer tracking to a single axis', + }, + ], + pitfalls: [{ id: 'hit-area', section: 'pointermove-source' }], + }, + + { + name: 'animationEnd', + defaultTriggerType: null, + params: [ + { + name: 'effectId', + type: 'string', + optional: false, + description: 'ID of the preceding effect', + }, + ], + }, + + { + name: 'pageVisible', + defaultTriggerType: null, + params: [], + pitfalls: [], + }, +]; diff --git a/packages/interact/_content/fragments/README.md b/packages/interact/_content/fragments/README.md index 9061ca20..1d8119b3 100644 --- a/packages/interact/_content/fragments/README.md +++ b/packages/interact/_content/fragments/README.md @@ -15,3 +15,11 @@ Reusable markdown content with `` markers for granular inclusio | `#` / `#-` | Named concept sections | `element-resolution.md` | When adding a new section, follow the closest existing pattern. Prefer descriptive names over generic ones when the section has trigger-specific or template-specific content. + +## Parameter Conventions + +Fragments use `{{paramName}}` placeholders for interpolation. Some conventions: + +- **`{{key}}`** — element key. Pass `'hero'` for concrete examples, `'[SOURCE_KEY]'` for template placeholders. +- **`{{classAttr}}`** — an HTML attribute string **including the leading space** (e.g. `' class="hero"'`), or empty string `''` when omitted. The leading space is required because the placeholder is adjacent to the tag name (``). +- **`{{installCommand}}`**, **`{{webEntry}}`**, etc. — package metadata from `data/meta.mjs`, passed as `metaParams` by the build script. diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 26ef362d..0f2ccc84 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -31,10 +31,6 @@ function buildBehaviorTable(headerLabel, behaviorKey, triggers, { defaultKey } = ); } -function buildFullLeanPitfalls(pitfallOrder, fragments) { - return pitfallOrder.map((p) => fragments.get(`pitfalls/${p.id}`, p.section)).join('\n'); -} - /** * Renders full-lean.md — the comprehensive reference for all triggers, effects, and API surface. * @@ -105,7 +101,7 @@ Declarative configuration-driven interaction library. Binds animations to trigge Each item here is CRITICAL — ignoring any of them will break animations. -${buildFullLeanPitfalls(FULL_LEAN_PITFALL_ORDER, fragments)} +${FULL_LEAN_PITFALL_ORDER.map((p) => fragments.get(`pitfalls/${p.id}`, p.section)).join('\n')} ${fragments.get('pitfalls/dont-guess-presets', 'default')} ${fragments.get('pitfalls/reduced-motion', 'default')} ${fragments.get('pitfalls/perspective', 'default')} diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index 9b8d79fc..0c08fd8f 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -164,6 +164,16 @@ for (const { file, content } of outputs) { } if (existing !== content) { console.error(` ✗ ${relative(PKG_ROOT, outPath)} is stale`); + const existingLines = existing.split('\n'); + const contentLines = content.split('\n'); + for (let i = 0; i < Math.max(existingLines.length, contentLines.length); i++) { + if (existingLines[i] !== contentLines[i]) { + console.error(` first diff at line ${i + 1}:`); + if (existingLines[i] !== undefined) console.error(` - ${existingLines[i]}`); + if (contentLines[i] !== undefined) console.error(` + ${contentLines[i]}`); + break; + } + } stale++; } else { console.log(` ✓ ${relative(PKG_ROOT, outPath)} is up to date`); From d0802e6eca97c94afc29107cd76ebe0ba6298b31 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 18:19:15 +0300 Subject: [PATCH 23/27] PR fixes --- packages/interact/_content/data/effects.mjs | 12 +- packages/interact/_content/data/triggers.mjs | 1 + .../interact/_content/fragments/conditions.md | 20 ---- .../fragments/multiple-effects-note.md | 4 +- .../interact/_content/templates/_helpers.mjs | 110 ++++++------------ .../_content/templates/event-trigger-rule.mjs | 47 ++++---- .../interact/_content/templates/full-lean.mjs | 22 +++- .../_content/templates/pointermove-rule.mjs | 2 +- .../_content/templates/viewenter-rule.mjs | 2 +- .../_content/templates/viewprogress-rule.mjs | 4 +- packages/interact/rules/full-lean.md | 12 +- 11 files changed, 97 insertions(+), 139 deletions(-) delete mode 100644 packages/interact/_content/fragments/conditions.md diff --git a/packages/interact/_content/data/effects.mjs b/packages/interact/_content/data/effects.mjs index c6fb498b..7b70aaa8 100644 --- a/packages/interact/_content/data/effects.mjs +++ b/packages/interact/_content/data/effects.mjs @@ -107,14 +107,14 @@ export const effects = { }, rangeNames: { - cover: 'Full visibility span from first pixel entering to last pixel leaving', - entry: 'The phase while the element is entering the viewport', - exit: 'The phase while the element is exiting the viewport', + cover: 'full visibility span from first pixel entering to last pixel leaving', + entry: 'the phase while the element is entering the viewport', + exit: 'the phase while the element is exiting the viewport', contain: - 'While the element is fully contained in the viewport. Typically used with a `position: sticky` container', + 'while the element is fully contained in the viewport. Typically used with a `position: sticky` container', 'entry-crossing': - "From the element's leading edge entering to its leading edge reaching the opposite side", + "from the element's leading edge entering to its leading edge reaching the opposite side", 'exit-crossing': - "From the element's trailing edge reaching the start to its trailing edge leaving", + "from the element's trailing edge reaching the start to its trailing edge leaving", }, }; diff --git a/packages/interact/_content/data/triggers.mjs b/packages/interact/_content/data/triggers.mjs index e4026f65..ca3a0cb3 100644 --- a/packages/interact/_content/data/triggers.mjs +++ b/packages/interact/_content/data/triggers.mjs @@ -175,6 +175,7 @@ export const triggers = [ }, ], pitfalls: [{ id: 'hit-area', section: 'pointermove-source' }], + showMultipleEffectsNote: true, }, { diff --git a/packages/interact/_content/fragments/conditions.md b/packages/interact/_content/fragments/conditions.md deleted file mode 100644 index ac8621c1..00000000 --- a/packages/interact/_content/fragments/conditions.md +++ /dev/null @@ -1,20 +0,0 @@ - -Named conditions that gate interactions, effects, or sequences. - -| Type | Predicate | -| :--------- | :------------------------------------------------------------------------ | -| `media` | CSS media query condition without `@media` (e.g., `'(min-width: 768px)'`) | -| `selector` | CSS selector; `&` is replaced with the base element selector | - -Attach via `conditions: ['[CONDITION_ID]']` on interactions, effects, or sequences. On an interaction, conditions gate the entire trigger; on an effect, only that specific effect is skipped. All listed conditions must pass. - -### Examples - -```ts -conditions: { - 'desktop': { type: 'media', predicate: '(min-width: 768px)' }, - 'hover-device': { type: 'media', predicate: '(hover: hover)' }, - 'reduced-motion': { type: 'media', predicate: '(prefers-reduced-motion: reduce)' }, - 'odd-items': { type: 'selector', predicate: ':nth-of-type(odd)' }, -} -``` diff --git a/packages/interact/_content/fragments/multiple-effects-note.md b/packages/interact/_content/fragments/multiple-effects-note.md index 1e53dce1..b9f33146 100644 --- a/packages/interact/_content/fragments/multiple-effects-note.md +++ b/packages/interact/_content/fragments/multiple-effects-note.md @@ -1,5 +1,7 @@ -**Multiple effects:** The `effects` array can contain multiple effects — all share the same {{triggerName}} trigger and fire together{{triggerContext}}.{{extraNote}} Use this to animate different targets from a single {{triggerEvent}}. +**Multiple effects:** The `effects` array can contain multiple effects — all share the same {{triggerName}} trigger and fire together. Use this to animate different targets from a single {{triggerEvent}}. + +**Multiple effects:** The `effects` array can contain multiple effects — all share the same viewEnter trigger and fire together when the element enters the viewport. Each effect can have its own `triggerType`. Use this to animate different targets from a single viewport entry event. **Multiple effects:** The `effects` array can contain multiple effects — all are driven by the same scroll progress. Use this to animate different targets or properties in sync with the same scroll position. diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs index aac6e749..92555e3a 100644 --- a/packages/interact/_content/templates/_helpers.mjs +++ b/packages/interact/_content/templates/_helpers.mjs @@ -16,84 +16,42 @@ export function buildPitfallsBlock(trigger, fragments, { wrapped = false } = {}) return wrapped ? `\n${content}\n` : content; } -/** - * Common variable descriptions. - * - mode 'suffix': arg is appended after base (separated by space) - * - mode 'override': arg replaces base entirely - * - no mode: variable takes no argument - */ const COMMON_VARS = { - SOURCE_KEY: { - base: "identifier matching the element's key (`data-interact-key` for web, `interactKey` for React).", - mode: 'suffix', - }, - TARGET_KEY: { - base: "identifier matching the element's key on the element that animates.", - mode: 'override', - }, - EFFECT_NAME: { base: 'unique string identifier for a `keyframeEffect`.' }, - NAMED_EFFECT_DEFINITION: { - base: 'object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.', - }, - KEYFRAMES: { - base: 'array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.', - }, - FILL_MODE: { - base: "fill mode for the animation (`'none'`, `'forwards'`, `'backwards'`, `'both'`).", - mode: 'override', - }, - DURATION_MS: { base: 'animation duration in milliseconds.' }, - EASING_FUNCTION: { - base: 'CSS easing string or named easing from `@wix/motion`.', - mode: 'override', - }, - DELAY_MS: { base: 'optional delay before the effect starts, in milliseconds.' }, - ITERATIONS: { - base: 'optional. Number of iterations, or `Infinity` for continuous loops.', - mode: 'override', - }, - ALTERNATE_BOOL: { - base: 'optional. `true` to alternate direction on every other iteration (within a single playback).', - mode: 'suffix', - }, - UNIQUE_EFFECT_ID: { - base: 'optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects from the top-level `effects` map.', - }, - CUSTOM_EFFECT_CALLBACK: { - base: 'function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1.', - }, - TRANSITION_DURATION_MS: { - base: 'optional number. Milliseconds for smoothing (interpolating) between progress updates. The animation does not jump to the new progress value instantly; instead it transitions over this duration. Use to add inertia/lag to the effect, making it feel more physical (e.g. `200`–`600`).', - }, - TRANSITION_EASING: { - base: 'optional string. CSS easing or named easing from `@wix/motion`. Adds a natural deceleration feel when used with `transitionDuration`.', - }, - CENTERED_TO_TARGET: { - base: '`true` or `false`. See **Centering with `centeredToTarget`** above.', - }, - HIT_AREA: { - base: "`'self'` (track pointer within source element) or `'root'` (track pointer anywhere in viewport).", - }, + SOURCE_KEY: (suffix) => + `identifier matching the element's key (\`data-interact-key\` for web, \`interactKey\` for React).${suffix ? ` ${suffix}` : ''}`, + TARGET_KEY: (desc) => + desc || "identifier matching the element's key on the element that animates.", + EFFECT_NAME: () => 'unique string identifier for a `keyframeEffect`.', + NAMED_EFFECT_DEFINITION: () => + 'object with properties of pre-built effect from `@wix/motion-presets`. Refer to motion-presets rules for available presets and their options.', + KEYFRAMES: () => + 'array of keyframe objects (e.g. `[{ opacity: 0 }, { opacity: 1 }]`). Property names in camelCase.', + FILL_MODE: (desc) => + desc || "fill mode for the animation (`'none'`, `'forwards'`, `'backwards'`, `'both'`).", + DURATION_MS: () => 'animation duration in milliseconds.', + EASING_FUNCTION: (desc) => + desc || 'CSS easing string or named easing from `@wix/motion`.', + DELAY_MS: () => 'optional delay before the effect starts, in milliseconds.', + ITERATIONS: (desc) => + desc || 'optional. Number of iterations, or `Infinity` for continuous loops.', + ALTERNATE_BOOL: (suffix) => + `optional. \`true\` to alternate direction on every other iteration (within a single playback).${suffix ? ` ${suffix}` : ''}`, + UNIQUE_EFFECT_ID: () => + 'optional. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects from the top-level `effects` map.', + CUSTOM_EFFECT_CALLBACK: () => + 'function with signature `(element: HTMLElement, progress: number) => void`. Called on each animation frame with the target element and `progress` from 0 to 1.', + TRANSITION_DURATION_MS: () => + 'optional number. Milliseconds for smoothing (interpolating) between progress updates. The animation does not jump to the new progress value instantly; instead it transitions over this duration. Use to add inertia/lag to the effect, making it feel more physical (e.g. `200`–`600`).', + TRANSITION_EASING: () => + 'optional string. CSS easing or named easing from `@wix/motion`. Adds a natural deceleration feel when used with `transitionDuration`.', + CENTERED_TO_TARGET: () => + '`true` or `false`. See **Centering with `centeredToTarget`** above.', + HIT_AREA: () => + "`'self'` (track pointer within source element) or `'root'` (track pointer anywhere in viewport).", }; -/** - * Returns a formatted variable description line. - * For 'suffix' mode vars, `extra` is appended after the base description. - * For 'override' mode vars, `extra` replaces the base description. - * Throws if `extra` is passed to a variable with no mode (would be silently ignored). - */ export function varLine(name, extra) { - const v = COMMON_VARS[name]; - if (!v) throw new Error(`Unknown common variable: ${name}`); - if (extra && !v.mode) { - throw new Error( - `varLine('${name}'): extra argument passed but this variable has no mode (suffix/override). ` + - `Either add a mode to COMMON_VARS['${name}'] or remove the extra argument.`, - ); - } - let desc = v.base; - if (extra && v.mode) { - desc = v.mode === 'suffix' ? `${v.base} ${extra}` : extra; - } - return `- \`[${name}]\` — ${desc}`; + const fn = COMMON_VARS[name]; + if (!fn) throw new Error(`Unknown common variable: ${name}`); + return `- \`[${name}]\` — ${fn(extra)}`; } diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index b4b14957..8d7a8c4e 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,25 +1,23 @@ import { capitalize, buildPitfallsBlock, varLine } from './_helpers.mjs'; -function getOverrides(name) { - if (name === 'hover') { - return { - sourceKeySuffix: 'The element that listens for hover.', - targetKeyDesc: - "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", - fillModeDesc: - "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.", - easingDesc: - "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.", - iterationsDesc: - "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", - fillCritical: - "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", - customEffectExamples: '', - offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.', - alternateBoolSuffix: '', - }; - } - return { +const OVERRIDES = { + hover: { + sourceKeySuffix: 'The element that listens for hover.', + targetKeyDesc: + "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", + fillModeDesc: + "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.", + easingDesc: + "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.", + iterationsDesc: + "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", + fillCritical: + "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", + customEffectExamples: '', + offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.', + alternateBoolSuffix: '', + }, + click: { sourceKeySuffix: 'The element that listens for clicks.', targetKeyDesc: "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", @@ -32,8 +30,8 @@ function getOverrides(name) { customEffectExamples: ', randomized behavior', offsetEasingSuffix: '', alternateBoolSuffix: "Different from `triggerType: 'alternate'` which alternates per click.", - }; -} + }, +}; /** * Renders a trigger-specific rule file (click.md or hover.md). @@ -43,7 +41,8 @@ function getOverrides(name) { export function render(data, fragments) { const { trigger } = data; const { name } = trigger; - const vo = getOverrides(name); + const vo = OVERRIDES[name]; + if (!vo) throw new Error(`Unknown event trigger: ${name}`); const Name = capitalize(name); const hasReversed = trigger.hasReversed; const hasEffectId = trigger.hasEffectId; @@ -51,7 +50,7 @@ export function render(data, fragments) { const pitfallsBlock = buildPitfallsBlock(trigger, fragments, { wrapped: true }); const multipleEffectsNote = trigger.showMultipleEffectsNote - ? `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event`, triggerContext: '', extraNote: '' })}\n` + ? `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event` })}\n` : ''; return `# ${Name} Trigger Rules for ${data.meta.packageName} diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 0f2ccc84..900c311e 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -148,7 +148,7 @@ ${fragments.get('quick-start', 'register-presets', metaParams)} - MUST set \`interactKey\` to a unique string. \`\`\`tsx -import { Interaction } from '@wix/interact/react'; +import { Interaction } from '${data.meta.entryPoints.react}'; ... @@ -478,7 +478,25 @@ ${fragments.get('sequences', 'detailed')} ## Conditions -${fragments.get('conditions', 'default')} +Named conditions that gate interactions, effects, or sequences. + +| Type | Predicate | +| :--------- | :------------------------------------------------------------------------ | +| \`media\` | CSS media query condition without \`@media\` (e.g., \`'(min-width: 768px)'\`) | +| \`selector\` | CSS selector; \`&\` is replaced with the base element selector | + +Attach via \`conditions: ['[CONDITION_ID]']\` on interactions, effects, or sequences. On an interaction, conditions gate the entire trigger; on an effect, only that specific effect is skipped. All listed conditions must pass. + +### Examples + +\`\`\`ts +conditions: { + 'desktop': { type: 'media', predicate: '(min-width: 768px)' }, + 'hover-device': { type: 'media', predicate: '(hover: hover)' }, + 'reduced-motion': { type: 'media', predicate: '(prefers-reduced-motion: reduce)' }, + 'odd-items': { type: 'selector', predicate: ':nth-of-type(odd)' }, +} +\`\`\` --- diff --git a/packages/interact/_content/templates/pointermove-rule.mjs b/packages/interact/_content/templates/pointermove-rule.mjs index 4dac9e72..57c0d391 100644 --- a/packages/interact/_content/templates/pointermove-rule.mjs +++ b/packages/interact/_content/templates/pointermove-rule.mjs @@ -102,7 +102,7 @@ For devices with dynamic viewport sizes (e.g. mobile browsers where the address Use pre-built mouse presets from \`${data.meta.presetsPackage}\` that handle 2D mouse tracking internally. Mouse presets are preferred over \`keyframeEffect\` for 2D effects. Available mouse presets: ${data.effects.presets.mouse.map((n) => `\`${n}\``).join(', ')}. -${fragments.get('multiple-effects-note', 'pointerMove')} +${trigger.showMultipleEffectsNote ? fragments.get('multiple-effects-note', 'pointerMove') : ''} \`\`\`typescript { diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index 676d2e23..6cd9b04e 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -91,7 +91,7 @@ ${fragments.get('fouc', 'code-vanilla', { key: '[SOURCE_KEY]', classAttr: '' })} Use \`keyframeEffect\` or \`namedEffect\` when the viewEnter should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior. Use \`params\` only for observer configuration (\`threshold\`, \`inset\`). -${fragments.get('multiple-effects-note', 'default', { triggerName: 'viewEnter', triggerEvent: 'viewport entry event', triggerContext: ' when the element enters the viewport', extraNote: ' Each effect can have its own `triggerType`.' })} +${fragments.get('multiple-effects-note', 'viewEnter')} \`\`\`typescript { diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index 07fdcc80..a3b32b9c 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -12,7 +12,7 @@ export function render(data, fragments) { const rangeList = Object.entries(data.effects.rangeNames) .map(([name, desc]) => { - return ` - \`'${name}'\` — ${desc.charAt(0).toLowerCase()}${desc.slice(1)}.`; + return ` - \`'${name}'\` — ${desc}.`; }) .join('\n'); @@ -34,7 +34,7 @@ ${pitfallsBlock} **Use Case**: Scroll-driven CSS-based effects. -${fragments.get('multiple-effects-note', 'viewProgress')} +${trigger.showMultipleEffectsNote ? fragments.get('multiple-effects-note', 'viewProgress') : ''} ### Template diff --git a/packages/interact/rules/full-lean.md b/packages/interact/rules/full-lean.md index 60a3ec53..9ac4f63d 100644 --- a/packages/interact/rules/full-lean.md +++ b/packages/interact/rules/full-lean.md @@ -399,12 +399,12 @@ Used with `viewProgress` and `pointerMove` triggers. | Range name | Meaning | | :--------------- | :------------------------------------------------------------------------------------------------------- | -| `cover` | Full visibility span from first pixel entering to last pixel leaving | -| `entry` | The phase while the element is entering the viewport | -| `exit` | The phase while the element is exiting the viewport | -| `contain` | While the element is fully contained in the viewport. Typically used with a `position: sticky` container | -| `entry-crossing` | From the element's leading edge entering to its leading edge reaching the opposite side | -| `exit-crossing` | From the element's trailing edge reaching the start to its trailing edge leaving | +| `cover` | full visibility span from first pixel entering to last pixel leaving | +| `entry` | the phase while the element is entering the viewport | +| `exit` | the phase while the element is exiting the viewport | +| `contain` | while the element is fully contained in the viewport. Typically used with a `position: sticky` container | +| `entry-crossing` | from the element's leading edge entering to its leading edge reaching the opposite side | +| `exit-crossing` | from the element's trailing edge reaching the start to its trailing edge leaving | **Sticky container pattern** — for scroll-driven animations inside a stuck `position: sticky` container: From 6ba9d12c234b2b10791e52de323f10a048d94308 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 18:30:33 +0300 Subject: [PATCH 24/27] PR fixes --- packages/interact/_content/data/triggers.mjs | 8 +-- .../interact/_content/templates/_helpers.mjs | 7 +-- .../_content/templates/event-trigger-rule.mjs | 5 +- .../interact/_content/templates/full-lean.mjs | 38 ++++++------- .../_content/templates/pointermove-rule.mjs | 2 +- .../_content/templates/viewenter-rule.mjs | 6 +- .../_content/templates/viewprogress-rule.mjs | 4 +- packages/interact/rules/full-lean.md | 56 +++++++++---------- packages/interact/scripts/build-rules.mjs | 5 +- 9 files changed, 63 insertions(+), 68 deletions(-) diff --git a/packages/interact/_content/data/triggers.mjs b/packages/interact/_content/data/triggers.mjs index ca3a0cb3..aae3d887 100644 --- a/packages/interact/_content/data/triggers.mjs +++ b/packages/interact/_content/data/triggers.mjs @@ -9,7 +9,7 @@ export const triggers = [ "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus.", defaultTriggerType: 'alternate', params: [], - pitfalls: [{ id: 'hit-area' }], + pitfalls: [{ id: 'hit-area', fullLeanSection: 'full-lean-hover' }], showMultipleEffectsNote: true, hasReversed: false, hasEffectId: false, @@ -124,7 +124,7 @@ export const triggers = [ "String adjusting the viewport detection area (e.g. `'-100px'` extends it, `'50px'` shrinks it).", }, ], - pitfalls: [{ id: 'same-element-viewenter', section: 'short' }], + pitfalls: [{ id: 'same-element-viewenter', section: 'short', fullLeanSection: 'long' }], showMultipleEffectsNote: true, triggerTypeDescriptions: { once: { @@ -150,7 +150,7 @@ export const triggers = [ name: 'viewProgress', defaultTriggerType: null, params: [], - pitfalls: [{ id: 'overflow-clip', section: 'short' }], + pitfalls: [{ id: 'overflow-clip', section: 'short', fullLeanSection: 'long' }], showMultipleEffectsNote: true, }, @@ -174,7 +174,7 @@ export const triggers = [ description: 'restricts pointer tracking to a single axis', }, ], - pitfalls: [{ id: 'hit-area', section: 'pointermove-source' }], + pitfalls: [{ id: 'hit-area', section: 'pointermove-source', fullLeanSection: 'full-lean-pointermove' }], showMultipleEffectsNote: true, }, diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs index 92555e3a..6f255131 100644 --- a/packages/interact/_content/templates/_helpers.mjs +++ b/packages/interact/_content/templates/_helpers.mjs @@ -5,15 +5,12 @@ export function capitalize(s) { /** * Builds the pitfalls block for a trigger template. * Iterates trigger.pitfalls from data, resolving each fragment section. - * When `wrapped` is true, wraps non-empty output with leading/trailing newlines - * (the common pattern used by event-trigger, viewenter, and viewprogress templates). */ -export function buildPitfallsBlock(trigger, fragments, { wrapped = false } = {}) { +export function buildPitfallsBlock(trigger, fragments) { if (!trigger.pitfalls?.length) return ''; - const content = trigger.pitfalls + return trigger.pitfalls .map((p) => fragments.get(`pitfalls/${p.id}`, p.section || trigger.name)) .join('\n'); - return wrapped ? `\n${content}\n` : content; } const COMMON_VARS = { diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 8d7a8c4e..5b489ed0 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -24,7 +24,6 @@ const OVERRIDES = { fillModeDesc: "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", easingDesc: 'CSS easing string, or named easing from `@wix/motion`.', - iterationsDesc: undefined, fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.", customEffectExamples: ', randomized behavior', @@ -47,7 +46,7 @@ export function render(data, fragments) { const hasReversed = trigger.hasReversed; const hasEffectId = trigger.hasEffectId; - const pitfallsBlock = buildPitfallsBlock(trigger, fragments, { wrapped: true }); + const pitfalls = buildPitfallsBlock(trigger, fragments); const multipleEffectsNote = trigger.showMultipleEffectsNote ? `\n${fragments.get('multiple-effects-note', 'default', { triggerName: name, triggerEvent: `${name} event` })}\n` @@ -58,7 +57,7 @@ export function render(data, fragments) { This document contains rules for generating ${name}-triggered interactions in \`${data.meta.packageName}\`. **CRITICAL — Accessible ${name}**: ${trigger.a11yNote} -${pitfallsBlock} +${pitfalls ? `\n${pitfalls}\n` : ''} ## Table of Contents - [Rule 1: keyframeEffect / namedEffect (TimeEffect)](#rule-1-keyframeeffect--namedeffect-timeeffect) diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 900c311e..c0b77d45 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -1,18 +1,18 @@ import { capitalize } from './_helpers.mjs'; -const FULL_LEAN_PITFALL_ORDER = [ - { id: 'overflow-clip', section: 'long' }, - { id: 'same-element-viewenter', section: 'long' }, - { id: 'hit-area', section: 'full-lean-hover' }, - { id: 'hit-area', section: 'full-lean-pointermove' }, -]; +function collectFullLeanPitfalls(triggers) { + return triggers.flatMap((t) => + (t.pitfalls || []) + .filter((p) => p.fullLeanSection) + .map((p) => ({ id: p.id, section: p.fullLeanSection })), + ); +} function buildMarkdownTable(headers, rows) { - const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => (r[i] || '').length))); return [ - `| ${headers.map((h, i) => h.padEnd(widths[i])).join(' | ')} |`, - `| ${widths.map((w) => `:${'-'.repeat(w - 1)}`).join(' | ')} |`, - ...rows.map((r) => `| ${r.map((c, i) => (c || '').padEnd(widths[i])).join(' | ')} |`), + `| ${headers.join(' | ')} |`, + `| ${headers.map(() => ':---').join(' | ')} |`, + ...rows.map((r) => `| ${r.join(' | ')} |`), ].join('\n'); } @@ -51,13 +51,13 @@ export function render(data, fragments) { .join(' | '); const easingList = data.effects.easings.map((e) => `\`'${e}'\``).join(', '); - const presetEntries = Object.entries(data.effects.presets).map(([category, names]) => ({ - label: capitalize(category), - value: `\`${names.join('`, `')}\``, - })); - const maxPresetLen = Math.max(...presetEntries.map((e) => e.value.length)); - const presetTable = presetEntries - .map((e) => ` | ${e.label.padEnd(8)} | ${e.value.padEnd(maxPresetLen)} |`) + const presetRows = Object.entries(data.effects.presets).map(([category, names]) => [ + capitalize(category), + `\`${names.join('`, `')}\``, + ]); + const presetTable = buildMarkdownTable(['Category', 'Presets'], presetRows) + .split('\n') + .map((line) => ` ${line}`) .join('\n'); const rangeTable = buildMarkdownTable( @@ -101,7 +101,7 @@ Declarative configuration-driven interaction library. Binds animations to trigge Each item here is CRITICAL — ignoring any of them will break animations. -${FULL_LEAN_PITFALL_ORDER.map((p) => fragments.get(`pitfalls/${p.id}`, p.section)).join('\n')} +${collectFullLeanPitfalls(data.triggers).map((p) => fragments.get(`pitfalls/${p.id}`, p.section)).join('\n')} ${fragments.get('pitfalls/dont-guess-presets', 'default')} ${fragments.get('pitfalls/reduced-motion', 'default')} ${fragments.get('pitfalls/perspective', 'default')} @@ -445,8 +445,6 @@ Exactly one MUST be provided per time-based or scroll/pointer-driven effect: Available presets: - | Category | Presets | - | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ${presetTable} - **CRITICAL** — Scroll presets (\`*Scroll\`) used with \`viewProgress\` MUST include \`range\` in options: \`'in'\` (ends at idle state), \`'out'\` (starts from idle state), or \`'continuous'\` (passes through idle). Prefer \`'continuous'\`. - Mouse presets are preferred over \`keyframeEffect\` for \`pointerMove\` 2D effects. diff --git a/packages/interact/_content/templates/pointermove-rule.mjs b/packages/interact/_content/templates/pointermove-rule.mjs index 57c0d391..910912ce 100644 --- a/packages/interact/_content/templates/pointermove-rule.mjs +++ b/packages/interact/_content/templates/pointermove-rule.mjs @@ -8,7 +8,7 @@ import { buildPitfallsBlock, varLine } from './_helpers.mjs'; export function render(data, fragments) { const { trigger } = data; - const pitfallsBlock = buildPitfallsBlock(trigger, fragments); // no extra newline wrapping — handled by template layout + const pitfallsBlock = buildPitfallsBlock(trigger, fragments); const paramsTypeFields = trigger.params .map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type};`) diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index 6cd9b04e..7c634ed3 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -8,7 +8,7 @@ import { buildPitfallsBlock, varLine } from './_helpers.mjs'; export function render(data, fragments) { const { trigger } = data; - const pitfallsBlock = buildPitfallsBlock(trigger, fragments, { wrapped: true }); + const pitfalls = buildPitfallsBlock(trigger, fragments); const paramDescriptions = trigger.params .map((p) => { @@ -23,7 +23,7 @@ export function render(data, fragments) { This document contains rules for generating interactions that respond to elements entering the viewport using the \`${data.meta.packageName}\`. ViewEnter triggers use IntersectionObserver to detect when elements become visible and are ideal for entrance animations, content reveals, and lazy-loading effects. --- -${pitfallsBlock} +${pitfalls ? `\n${pitfalls}\n` : ''} ## Table of Contents - [Preventing Flash of Unstyled Content (FOUC)](#preventing-flash-of-unstyled-content-fouc) @@ -91,7 +91,7 @@ ${fragments.get('fouc', 'code-vanilla', { key: '[SOURCE_KEY]', classAttr: '' })} Use \`keyframeEffect\` or \`namedEffect\` when the viewEnter should play an animation (CSS or WAAPI). Set \`triggerType\` on each effect to control playback behavior. Use \`params\` only for observer configuration (\`threshold\`, \`inset\`). -${fragments.get('multiple-effects-note', 'viewEnter')} +${trigger.showMultipleEffectsNote ? fragments.get('multiple-effects-note', 'viewEnter') : ''} \`\`\`typescript { diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index a3b32b9c..dcaeeb4d 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -8,7 +8,7 @@ import { buildPitfallsBlock, varLine } from './_helpers.mjs'; export function render(data, fragments) { const { trigger } = data; - const pitfallsBlock = buildPitfallsBlock(trigger, fragments, { wrapped: true }); + const pitfalls = buildPitfallsBlock(trigger, fragments); const rangeList = Object.entries(data.effects.rangeNames) .map(([name, desc]) => { @@ -19,7 +19,7 @@ export function render(data, fragments) { return `# ViewProgress Trigger Rules for ${data.meta.packageName} These rules help generate scroll-driven interactions using \`${data.meta.packageName}\`. ViewProgress triggers create animations that update continuously as elements move through the viewport, leveraging native CSS ViewTimelines where supported, and using a polyfill library where unsupported. Use when animation progress should be tied to the element's scroll position. -${pitfallsBlock} +${pitfalls ? `\n${pitfalls}\n` : ''} **Offset semantics:** The \`offset\` inside \`rangeStart\`/\`rangeEnd\` is an object \`{ unit: 'percentage', value: NUMBER }\` where value is 0–100. For absolute lengths use \`{ unit: 'px', value: NUMBER }\` (or other CSS length units). Positive values move the effective range boundary forward along the scroll axis. ## Table of Contents diff --git a/packages/interact/rules/full-lean.md b/packages/interact/rules/full-lean.md index 9ac4f63d..34b80072 100644 --- a/packages/interact/rules/full-lean.md +++ b/packages/interact/rules/full-lean.md @@ -32,9 +32,9 @@ Declarative configuration-driven interaction library. Binds animations to trigge Each item here is CRITICAL — ignoring any of them will break animations. -- **CRITICAL — `overflow: hidden` breaks `viewProgress`**: Replace with `overflow: clip` on all ancestors between source and scroll container. In Tailwind, replace `overflow-hidden` with `overflow-clip`. -- **CRITICAL**: When using `viewEnter` trigger and source (trigger) and target (effect) elements are the **same element**, use ONLY `triggerType: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`) MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing. - **CRITICAL - Hit-area shift**: When a hover effect changes the size or position of the hovered element (e.g., `transform: scale(…)`), MUST use a 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. +- **CRITICAL**: When using `viewEnter` trigger and source (trigger) and target (effect) elements are the **same element**, use ONLY `triggerType: 'once'`. For all other types (`'repeat'`, `'alternate'`, `'state'`) MUST use **separate** source and target elements — animating the observed element itself can cause it to leave/re-enter the viewport, leading to rapid re-triggers or the animation never firing. +- **CRITICAL — `overflow: hidden` breaks `viewProgress`**: Replace with `overflow: clip` on all ancestors between source and scroll container. In Tailwind, replace `overflow-hidden` with `overflow-clip`. - **CRITICAL**: For `pointerMove` trigger MUST AVOID using the same element as both source and target with `hitArea: 'self'` and effects that change size or position (e.g. `transform: translate(…)`, `scale(…)`). The transform shifts the hit area, causing jittery re-entry cycles. Instead, use `selector` to target a child element for the animation. - **CRITICAL — Do NOT guess preset options**: If you don't know the expected type/structure for a `namedEffect` param, omit it — rely on defaults rather than guessing. - **Reduced motion**: Use conditions to provide gentler alternatives (shorter durations, fewer transforms, no perpetual motion) for users who prefer reduced motion. You can also set `Interact.forceReducedMotion = matchMedia('(prefers-reduced-motion: reduce)').matches` to force a global reduced-motion behavior programmatically. @@ -237,21 +237,21 @@ For `TimeEffect` (keyframe/named/custom effects), set `triggerType` on the effec **`triggerType`** — on `TimeEffect`: -| Type | hover behavior | click behavior | -| :---------------------- | :-------------------------------------- | :------------------------------- | -| `'alternate'` (default) | Play on enter, reverse on leave | Alternate play/reverse per click | -| `'repeat'` | Play on enter, stop and rewind on leave | Restart per click | -| `'once'` | Play once on first enter only | Play once on first click only | -| `'state'` | Play on enter, pause on leave | Toggle play/pause per click | +| Type | hover behavior | click behavior | +| :--- | :--- | :--- | +| `'alternate'` (default) | Play on enter, reverse on leave | Alternate play/reverse per click | +| `'repeat'` | Play on enter, stop and rewind on leave | Restart per click | +| `'once'` | Play once on first enter only | Play once on first click only | +| `'state'` | Play on enter, pause on leave | Toggle play/pause per click | **`stateAction`** — on `StateEffect`: -| Action | hover behavior | click behavior | -| :------------------- | :---------------------------------------------- | :--------------------------- | -| `'toggle'` (default) | Add style state on enter, remove on leave | Toggle style state per click | -| `'add'` | Add style state on enter; leave does NOT remove | Add style state on click | -| `'remove'` | Remove style state on enter | Remove style state on click | -| `'clear'` | Clear/reset all style states on enter | Clear/reset all style states | +| Action | hover behavior | click behavior | +| :--- | :--- | :--- | +| `'toggle'` (default) | Add style state on enter, remove on leave | Toggle style state per click | +| `'add'` | Add style state on enter; leave does NOT remove | Add style state on click | +| `'remove'` | Remove style state on enter | Remove style state on click | +| `'clear'` | Clear/reset all style states on enter | Clear/reset all style states | ### viewEnter @@ -397,14 +397,14 @@ Used with `viewProgress` and `pointerMove` triggers. } ``` -| Range name | Meaning | -| :--------------- | :------------------------------------------------------------------------------------------------------- | -| `cover` | full visibility span from first pixel entering to last pixel leaving | -| `entry` | the phase while the element is entering the viewport | -| `exit` | the phase while the element is exiting the viewport | -| `contain` | while the element is fully contained in the viewport. Typically used with a `position: sticky` container | -| `entry-crossing` | from the element's leading edge entering to its leading edge reaching the opposite side | -| `exit-crossing` | from the element's trailing edge reaching the start to its trailing edge leaving | +| Range name | Meaning | +| :--- | :--- | +| `cover` | full visibility span from first pixel entering to last pixel leaving | +| `entry` | the phase while the element is entering the viewport | +| `exit` | the phase while the element is exiting the viewport | +| `contain` | while the element is fully contained in the viewport. Typically used with a `position: sticky` container | +| `entry-crossing` | from the element's leading edge entering to its leading edge reaching the opposite side | +| `exit-crossing` | from the element's trailing edge reaching the start to its trailing edge leaving | **Sticky container pattern** — for scroll-driven animations inside a stuck `position: sticky` container: @@ -463,12 +463,12 @@ Exactly one MUST be provided per time-based or scroll/pointer-driven effect: Available presets: - | Category | Presets | - | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | Entrance | `FadeIn`, `GlideIn`, `SlideIn`, `FloatIn`, `RevealIn`, `ExpandIn`, `BlurIn`, `FlipIn`, `ArcIn`, `ShuttersIn`, `CurveIn`, `DropIn`, `FoldIn`, `ShapeIn`, `TiltIn`, `WinkIn`, `SpinIn`, `TurnIn`, `BounceIn` | - | Ongoing | `Pulse`, `Spin`, `Breathe`, `Bounce`, `Wiggle`, `Flash`, `Flip`, `Fold`, `Jello`, `Poke`, `Rubber`, `Swing`, `Cross` | - | Scroll | `FadeScroll`, `RevealScroll`, `ParallaxScroll`, `MoveScroll`, `SlideScroll`, `GrowScroll`, `ShrinkScroll`, `TiltScroll`, `PanScroll`, `BlurScroll`, `FlipScroll`, `SpinScroll`, `ArcScroll`, `ShapeScroll`, `ShuttersScroll`, `SkewPanScroll`, `Spin3dScroll`, `StretchScroll`, `TurnScroll` | - | Mouse | `TrackMouse`, `Tilt3DMouse`, `Track3DMouse`, `SwivelMouse`, `AiryMouse`, `ScaleMouse`, `BlurMouse`, `SkewMouse`, `BlobMouse` | + | Category | Presets | + | :--- | :--- | + | Entrance | `FadeIn`, `GlideIn`, `SlideIn`, `FloatIn`, `RevealIn`, `ExpandIn`, `BlurIn`, `FlipIn`, `ArcIn`, `ShuttersIn`, `CurveIn`, `DropIn`, `FoldIn`, `ShapeIn`, `TiltIn`, `WinkIn`, `SpinIn`, `TurnIn`, `BounceIn` | + | Ongoing | `Pulse`, `Spin`, `Breathe`, `Bounce`, `Wiggle`, `Flash`, `Flip`, `Fold`, `Jello`, `Poke`, `Rubber`, `Swing`, `Cross` | + | Scroll | `FadeScroll`, `RevealScroll`, `ParallaxScroll`, `MoveScroll`, `SlideScroll`, `GrowScroll`, `ShrinkScroll`, `TiltScroll`, `PanScroll`, `BlurScroll`, `FlipScroll`, `SpinScroll`, `ArcScroll`, `ShapeScroll`, `ShuttersScroll`, `SkewPanScroll`, `Spin3dScroll`, `StretchScroll`, `TurnScroll` | + | Mouse | `TrackMouse`, `Tilt3DMouse`, `Track3DMouse`, `SwivelMouse`, `AiryMouse`, `ScaleMouse`, `BlurMouse`, `SkewMouse`, `BlobMouse` | - **CRITICAL** — Scroll presets (`*Scroll`) used with `viewProgress` MUST include `range` in options: `'in'` (ends at idle state), `'out'` (starts from idle state), or `'continuous'` (passes through idle). Prefer `'continuous'`. - Mouse presets are preferred over `keyframeEffect` for `pointerMove` 2D effects. diff --git a/packages/interact/scripts/build-rules.mjs b/packages/interact/scripts/build-rules.mjs index 0c08fd8f..fe38e663 100644 --- a/packages/interact/scripts/build-rules.mjs +++ b/packages/interact/scripts/build-rules.mjs @@ -41,11 +41,12 @@ class Fragments { for (const entry of readdirSync(dir, { withFileTypes: true })) { if (entry.isDirectory()) { this._loadDir(join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name); - } else if (entry.name.endsWith('.md') && entry.name !== 'README.md') { + } else if (entry.name.endsWith('.md')) { + const raw = readFileSync(join(dir, entry.name), 'utf8'); + if (!raw.includes(' markers +// 2. Load fragments // --------------------------------------------------------------------------- -class Fragments { - constructor(dir) { - this.store = new Map(); - this._loadDir(dir, ''); - } - - _loadDir(dir, prefix) { - for (const entry of readdirSync(dir, { withFileTypes: true })) { - if (entry.isDirectory()) { - this._loadDir(join(dir, entry.name), prefix ? `${prefix}/${entry.name}` : entry.name); - } else if (entry.name.endsWith('.md')) { - const raw = readFileSync(join(dir, entry.name), 'utf8'); - if (!raw.includes('$/); - if (m) { - if (current === null && buf.join('\n').trim()) { - throw new Error( - `Fragment "${filePath}" has content before the first marker. Add a marker or remove the content.`, - ); - } - if (current !== null) { - sections.set(current, buf.join('\n').trim()); - } - current = m[1]; - buf = []; - } else { - buf.push(line); - } - } - if (current !== null) { - sections.set(current, buf.join('\n').trim()); - } - return sections; - } - - get(path, section = 'default', params = {}) { - const sectionMap = this.store.get(path); - if (!sectionMap) { - throw new Error(`Fragment not found: ${path}`); - } - let content = sectionMap.get(section); - if (content === undefined) { - throw new Error( - `Section "${section}" not found in fragment "${path}". Available: ${[...sectionMap.keys()].join(', ')}`, - ); - } - for (const [key, val] of Object.entries(params)) { - content = content.replaceAll(`{{${key}}}`, val); - } - const unreplaced = content.match(/\{\{[^}]+\}\}/g); - if (unreplaced) { - throw new Error( - `Unreplaced placeholders in fragment "${path}#${section}": ${unreplaced.join(', ')}`, - ); - } - return content; - } -} - const fragments = new Fragments(join(CONTENT_DIR, 'fragments')); // --------------------------------------------------------------------------- From be089721ad93827ed880e1cb80b1d5422c5ccacc Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 18:59:47 +0300 Subject: [PATCH 26/27] PR fixes --- packages/interact/_content/data/effects.mjs | 12 ++++++------ packages/interact/_content/data/triggers.mjs | 3 +++ .../_content/templates/viewprogress-rule.mjs | 2 +- packages/interact/rules/full-lean.md | 12 ++++++------ 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/packages/interact/_content/data/effects.mjs b/packages/interact/_content/data/effects.mjs index 7b70aaa8..fb59b00d 100644 --- a/packages/interact/_content/data/effects.mjs +++ b/packages/interact/_content/data/effects.mjs @@ -107,14 +107,14 @@ export const effects = { }, rangeNames: { - cover: 'full visibility span from first pixel entering to last pixel leaving', - entry: 'the phase while the element is entering the viewport', - exit: 'the phase while the element is exiting the viewport', + cover: 'full visibility span from first pixel entering to last pixel leaving.', + entry: 'the phase while the element is entering the viewport.', + exit: 'the phase while the element is exiting the viewport.', contain: - 'while the element is fully contained in the viewport. Typically used with a `position: sticky` container', + 'while the element is fully contained in the viewport. Typically used with a `position: sticky` container.', 'entry-crossing': - "from the element's leading edge entering to its leading edge reaching the opposite side", + "from the element's leading edge entering to its leading edge reaching the opposite side.", 'exit-crossing': - "from the element's trailing edge reaching the start to its trailing edge leaving", + "from the element's trailing edge reaching the start to its trailing edge leaving.", }, }; diff --git a/packages/interact/_content/data/triggers.mjs b/packages/interact/_content/data/triggers.mjs index 64c86c1d..a32e557e 100644 --- a/packages/interact/_content/data/triggers.mjs +++ b/packages/interact/_content/data/triggers.mjs @@ -189,6 +189,8 @@ export const triggers = [ description: 'ID of the preceding effect', }, ], + pitfalls: [], + showMultipleEffectsNote: false, }, { @@ -196,5 +198,6 @@ export const triggers = [ defaultTriggerType: null, params: [], pitfalls: [], + showMultipleEffectsNote: false, }, ]; diff --git a/packages/interact/_content/templates/viewprogress-rule.mjs b/packages/interact/_content/templates/viewprogress-rule.mjs index 859d586a..efce65ce 100644 --- a/packages/interact/_content/templates/viewprogress-rule.mjs +++ b/packages/interact/_content/templates/viewprogress-rule.mjs @@ -12,7 +12,7 @@ export function render(data, fragments) { const rangeList = Object.entries(data.effects.rangeNames) .map(([name, desc]) => { - return ` - \`'${name}'\` — ${desc}.`; + return ` - \`'${name}'\` — ${desc}`; }) .join('\n'); diff --git a/packages/interact/rules/full-lean.md b/packages/interact/rules/full-lean.md index 34b80072..8b6b797e 100644 --- a/packages/interact/rules/full-lean.md +++ b/packages/interact/rules/full-lean.md @@ -399,12 +399,12 @@ Used with `viewProgress` and `pointerMove` triggers. | Range name | Meaning | | :--- | :--- | -| `cover` | full visibility span from first pixel entering to last pixel leaving | -| `entry` | the phase while the element is entering the viewport | -| `exit` | the phase while the element is exiting the viewport | -| `contain` | while the element is fully contained in the viewport. Typically used with a `position: sticky` container | -| `entry-crossing` | from the element's leading edge entering to its leading edge reaching the opposite side | -| `exit-crossing` | from the element's trailing edge reaching the start to its trailing edge leaving | +| `cover` | full visibility span from first pixel entering to last pixel leaving. | +| `entry` | the phase while the element is entering the viewport. | +| `exit` | the phase while the element is exiting the viewport. | +| `contain` | while the element is fully contained in the viewport. Typically used with a `position: sticky` container. | +| `entry-crossing` | from the element's leading edge entering to its leading edge reaching the opposite side. | +| `exit-crossing` | from the element's trailing edge reaching the start to its trailing edge leaving. | **Sticky container pattern** — for scroll-driven animations inside a stuck `position: sticky` container: From 19e3c16f44de46d0aae6f5876592d3e3945496c1 Mon Sep 17 00:00:00 2001 From: ameerabuf Date: Wed, 6 May 2026 19:08:12 +0300 Subject: [PATCH 27/27] PR fixes --- packages/interact/_content/data/triggers.mjs | 8 +-- .../interact/_content/templates/_helpers.mjs | 2 +- .../_content/templates/event-trigger-rule.mjs | 55 ++++++++++--------- .../interact/_content/templates/full-lean.mjs | 18 ++++-- .../_content/templates/viewenter-rule.mjs | 2 +- 5 files changed, 47 insertions(+), 38 deletions(-) diff --git a/packages/interact/_content/data/triggers.mjs b/packages/interact/_content/data/triggers.mjs index a32e557e..790ea099 100644 --- a/packages/interact/_content/data/triggers.mjs +++ b/packages/interact/_content/data/triggers.mjs @@ -9,7 +9,7 @@ export const triggers = [ "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus.", defaultTriggerType: 'alternate', params: [], - pitfalls: [{ id: 'hit-area', fullLeanSection: 'full-lean-hover' }], + pitfalls: [{ id: 'hit-area', sections: { fullLean: 'full-lean-hover' } }], showMultipleEffectsNote: true, hasReversed: false, hasEffectId: false, @@ -124,7 +124,7 @@ export const triggers = [ "String adjusting the viewport detection area (e.g. `'-100px'` extends it, `'50px'` shrinks it).", }, ], - pitfalls: [{ id: 'same-element-viewenter', section: 'short', fullLeanSection: 'long' }], + pitfalls: [{ id: 'same-element-viewenter', sections: { trigger: 'short', fullLean: 'long' } }], showMultipleEffectsNote: true, triggerTypeDescriptions: { once: { @@ -150,7 +150,7 @@ export const triggers = [ name: 'viewProgress', defaultTriggerType: null, params: [], - pitfalls: [{ id: 'overflow-clip', section: 'short', fullLeanSection: 'long' }], + pitfalls: [{ id: 'overflow-clip', sections: { trigger: 'short', fullLean: 'long' } }], showMultipleEffectsNote: true, }, @@ -174,7 +174,7 @@ export const triggers = [ description: 'restricts pointer tracking to a single axis', }, ], - pitfalls: [{ id: 'hit-area', section: 'pointermove-source', fullLeanSection: 'full-lean-pointermove' }], + pitfalls: [{ id: 'hit-area', sections: { trigger: 'pointermove-source', fullLean: 'full-lean-pointermove' } }], showMultipleEffectsNote: true, }, diff --git a/packages/interact/_content/templates/_helpers.mjs b/packages/interact/_content/templates/_helpers.mjs index 12d90260..94eb891f 100644 --- a/packages/interact/_content/templates/_helpers.mjs +++ b/packages/interact/_content/templates/_helpers.mjs @@ -9,7 +9,7 @@ export function capitalize(s) { export function buildPitfallsBlock(trigger, fragments) { if (!trigger.pitfalls?.length) return ''; return trigger.pitfalls - .map((p) => fragments.get(`pitfalls/${p.id}`, p.section || trigger.name)) + .map((p) => fragments.get(`pitfalls/${p.id}`, p.sections?.trigger || trigger.name)) .join('\n'); } diff --git a/packages/interact/_content/templates/event-trigger-rule.mjs b/packages/interact/_content/templates/event-trigger-rule.mjs index 8d5dd1dc..ef41e2db 100644 --- a/packages/interact/_content/templates/event-trigger-rule.mjs +++ b/packages/interact/_content/templates/event-trigger-rule.mjs @@ -1,32 +1,34 @@ import { capitalize, buildPitfallsBlock, varLine } from './_helpers.mjs'; -// Fields ending in `Extra` are passed to varLine(name, extra) — either appended or used as -// a full override depending on the COMMON_VARS function for that variable. const OVERRIDES = { hover: { - sourceKeyExtra: 'The element that listens for hover.', - targetKeyExtra: - "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", - fillModeExtra: - "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.", - easingExtra: - "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.", - iterationsExtra: - "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", - alternateBoolExtra: '', + vars: { + SOURCE_KEY: 'The element that listens for hover.', + TARGET_KEY: + "identifier matching the element's key on the element that animates. Use a different key from `[SOURCE_KEY]` when source and target must be separated (see hit-area shift above).", + FILL_MODE: + "usually `'both'`. Keeps the final state applied while hovering, and prevents garbage-collection of animation when finished.", + EASING_FUNCTION: + "CSS easing string (e.g. `'ease-out'`, `'ease-in-out'`, `'cubic-bezier(0.4, 0, 0.2, 1)'`), or named easing from `@wix/motion`.", + ITERATIONS: + "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", + ALTERNATE_BOOL: '', + }, fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'`, `'repeat'` — keeps the effect applied while hovering and prevents garbage-collection. For `triggerType: 'once'` use `fill: 'backwards'`.", customEffectExamples: '', offsetEasingSuffix: ' CSS easing string, or named easing from `@wix/motion`.', }, click: { - sourceKeyExtra: 'The element that listens for clicks.', - targetKeyExtra: - "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", - fillModeExtra: - "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", - easingExtra: 'CSS easing string, or named easing from `@wix/motion`.', - alternateBoolExtra: "Different from `triggerType: 'alternate'` which alternates per click.", + vars: { + SOURCE_KEY: 'The element that listens for clicks.', + TARGET_KEY: + "identifier matching the element's key on the element that animates. If missing it defaults to `[SOURCE_KEY]` for targeting the source element.", + FILL_MODE: + "optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect.", + EASING_FUNCTION: 'CSS easing string, or named easing from `@wix/motion`.', + ALTERNATE_BOOL: "Different from `triggerType: 'alternate'` which alternates per click.", + }, fillCritical: "Always include `fill: 'both'` for `triggerType: 'alternate'` or `'repeat'` — keeps the effect applied while finished and prevents garbage-collection, allowing efficient toggling. For `triggerType: 'once'` use `fill: 'backwards'`.", customEffectExamples: ', randomized behavior', @@ -35,17 +37,18 @@ const OVERRIDES = { }; function buildVariables(trigger, vo, hasReversed, hasEffectId) { + const v = vo.vars; const lines = [ - varLine('SOURCE_KEY', vo.sourceKeyExtra), - varLine('TARGET_KEY', vo.targetKeyExtra), + varLine('SOURCE_KEY', v.SOURCE_KEY), + varLine('TARGET_KEY', v.TARGET_KEY), `- \`[TRIGGER_TYPE]\` — \`triggerType\` on the effect. One of:`, ...Object.entries(trigger.triggerTypeDescriptions).map( - ([k, v]) => ` - \`'${k}'\` — ${v.full}`, + ([k, d]) => ` - \`'${k}'\` — ${d.full}`, ), varLine('KEYFRAMES'), varLine('EFFECT_NAME'), varLine('NAMED_EFFECT_DEFINITION'), - varLine('FILL_MODE', vo.fillModeExtra), + varLine('FILL_MODE', v.FILL_MODE), ]; if (hasReversed) { lines.push( @@ -54,10 +57,10 @@ function buildVariables(trigger, vo, hasReversed, hasEffectId) { } lines.push( varLine('DURATION_MS'), - varLine('EASING_FUNCTION', vo.easingExtra), + varLine('EASING_FUNCTION', v.EASING_FUNCTION), varLine('DELAY_MS'), - varLine('ITERATIONS', vo.iterationsExtra), - varLine('ALTERNATE_BOOL', vo.alternateBoolExtra), + varLine('ITERATIONS', v.ITERATIONS), + varLine('ALTERNATE_BOOL', v.ALTERNATE_BOOL), ); if (hasEffectId) { lines.push(varLine('UNIQUE_EFFECT_ID')); diff --git a/packages/interact/_content/templates/full-lean.mjs b/packages/interact/_content/templates/full-lean.mjs index 44b41a36..8c005616 100644 --- a/packages/interact/_content/templates/full-lean.mjs +++ b/packages/interact/_content/templates/full-lean.mjs @@ -3,11 +3,17 @@ import { capitalize } from './_helpers.mjs'; function collectFullLeanPitfalls(triggers) { return triggers.flatMap((t) => (t.pitfalls || []) - .filter((p) => p.fullLeanSection) - .map((p) => ({ id: p.id, section: p.fullLeanSection })), + .filter((p) => p.sections?.fullLean) + .map((p) => ({ id: p.id, section: p.sections.fullLean })), ); } +function requireTrigger(triggers, name) { + const t = triggers.find((t) => t.name === name); + if (!t) throw new Error(`Trigger "${name}" not found in data/triggers.mjs`); + return t; +} + function buildMarkdownTable(headers, rows) { return [ `| ${headers.join(' | ')} |`, @@ -41,9 +47,9 @@ function buildBehaviorTable(headerLabel, behaviorKey, triggers, { defaultKey } = * @param {import('../lib/fragments.mjs').Fragments} fragments */ export function render(data, fragments) { - const hover = data.triggers.find((t) => t.name === 'hover'); - const click = data.triggers.find((t) => t.name === 'click'); - const viewEnter = data.triggers.find((t) => t.name === 'viewEnter'); + const hover = requireTrigger(data.triggers, 'hover'); + const click = requireTrigger(data.triggers, 'click'); + const viewEnter = requireTrigger(data.triggers, 'viewEnter'); const triggerTypeUnion = data.effects.triggerTypes.map((t) => `'${t}'`).join(' | '); const rangeNameUnion = Object.keys(data.effects.rangeNames) @@ -512,7 +518,7 @@ conditions: { Call \`generate(config)\` server-side or at build time and inject the result into the \`\` (preferred), or insert to beginning of \`\`, so it loads before the page content is painted: \`\`\`ts -import { generate } from '@wix/interact/web'; +import { generate } from '${data.meta.entryPoints.web}'; const css = generate(config); \`\`\` diff --git a/packages/interact/_content/templates/viewenter-rule.mjs b/packages/interact/_content/templates/viewenter-rule.mjs index 7ce449a8..e13de24e 100644 --- a/packages/interact/_content/templates/viewenter-rule.mjs +++ b/packages/interact/_content/templates/viewenter-rule.mjs @@ -41,7 +41,7 @@ If only one of these is present, FOUC prevention will **not** work. Both the CSS Call \`generate(config)\` server-side or at build time. Inject the resulting CSS into the document \`\` (or in \`\` before your content) so it loads before the page content is painted: \`\`\`typescript -import { generate } from '@wix/interact'; +import { generate } from '${data.meta.packageName}'; const config: InteractConfig = { interactions: [