diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..b7815994 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +packages/interact/rules/*.md linguist-generated=true diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c70eb010..17288036 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,6 +55,9 @@ jobs: - name: Build run: yarn build + - name: Verify generated rules are up to date + run: yarn workspace @wix/interact build:rules --check + - name: Lint run: yarn lint diff --git a/.prettierignore b/.prettierignore index ad72a0ac..94f0f18a 100644 --- a/.prettierignore +++ b/.prettierignore @@ -8,3 +8,6 @@ package-lock.json **/build/** index.html .yarnrc.yml + +# Build pipeline templates use {{}} placeholders that Prettier must not reformat +packages/interact/_build/templates/ diff --git a/packages/interact/SPEC.md b/packages/interact/SPEC.md new file mode 100644 index 00000000..a8d639b6 --- /dev/null +++ b/packages/interact/SPEC.md @@ -0,0 +1,982 @@ +# Rules Build v2 — Implementation Spec + +> Replaces the `_content/` + `scripts/build-rules.mjs` pipeline (PR #204) with a markdown-first system. + +## 1. Directory Layout + +All build sources live under `packages/interact/_build/`. Generated output stays in `packages/interact/rules/`. + +``` +packages/interact/ +├── _build/ +│ ├── glossary.mjs ← single source of truth +│ ├── assemble.mjs ← build script (CLI entry point) +│ └── templates/ +│ ├── sections/ ← shared reusable prose blocks +│ │ ├── fouc.md +│ │ ├── quick-start.md +│ │ ├── static-api.md +│ │ ├── element-resolution.md +│ │ ├── config-structure.md +│ │ ├── sequences.md +│ │ ├── pitfalls.md +│ │ ├── progress-type.md +│ │ └── multiple-effects-note.md +│ ├── triggers/ ← per-trigger rule templates +│ │ ├── event-trigger.md ← renders click.md AND hover.md +│ │ ├── viewenter.md ← renders viewenter.md +│ │ ├── viewprogress.md ← renders viewprogress.md +│ │ └── pointermove.md ← renders pointermove.md +│ └── composites/ ← large reference file templates +│ ├── integration.md ← renders integration.md +│ └── full-lean.md ← renders full-lean.md +├── rules/ ← generated output (committed to git) +│ ├── click.md +│ ├── hover.md +│ ├── viewenter.md +│ ├── viewprogress.md +│ ├── pointermove.md +│ ├── integration.md +│ └── full-lean.md +└── package.json ← "build:rules": "node _build/assemble.mjs" +``` + +## 2. Glossary (`glossary.mjs`) + +Single default export. All technical terms, types, descriptions, and per-trigger data in one flat, greppable module. No YAML, no external dependencies. + +### 2.1 Structure + +```javascript +export const glossary = { + + // ═══════════════════════════════════════════════════════════ + // Package metadata + // ═══════════════════════════════════════════════════════════ + meta: { + packageName: '@wix/interact', + presetsPackage: '@wix/motion-presets', + motionPackage: '@wix/motion', + installCommand: 'npm install @wix/interact @wix/motion-presets', + entry: { + web: '@wix/interact/web', + react: '@wix/interact/react', + vanilla: '@wix/interact', + }, + }, + + // ═══════════════════════════════════════════════════════════ + // Variable definitions (the [VARIABLE] placeholders in rules) + // ═══════════════════════════════════════════════════════════ + // + // Each key matches a [VARIABLE_NAME] used in code templates. + // Value is the default description rendered as: + // - `[KEY]` — + // + // Per-trigger overrides live in triggers..vars and take + // precedence when the template is rendered for that trigger. + // + vars: { + SOURCE_KEY: "identifier matching the element's key (`data-interact-key` for web, `interactKey` for React).", + TARGET_KEY: "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: "fill mode for the animation (`'none'`, `'forwards'`, `'backwards'`, `'both'`).", + 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.', + 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 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. Use to add inertia/lag to the effect (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).", + 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).", + }, + + // ═══════════════════════════════════════════════════════════ + // Effects data + // ═══════════════════════════════════════════════════════════ + 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', + ], + }, + + ranges: { + 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.', + }, + }, + + // ═══════════════════════════════════════════════════════════ + // Per-trigger data + // ═══════════════════════════════════════════════════════════ + // + // Each trigger entry contains: + // name — trigger identifier + // a11yAlias — accessible trigger name (if any) + // a11yNote — CRITICAL note about accessible alternative + // vars — overrides for the global vars above (only fields that differ) + // pitfalls — array of { id, variant } referencing sections/pitfalls.md + // triggerTypes — { name: { short, full } } for behavior tables + // stateActions — { name: { short, full } } for state effect tables (event triggers only) + // defaultTriggerType — default triggerType value + // flags — boolean flags: hasReversed, hasEffectId, showMultipleEffectsNote + // params — trigger-specific params array (for pointerMove params type rendering) + // prose — trigger-specific overrides for prose strings (fillCritical, customEffectExamples, offsetEasingSuffix) + // + triggers: { + + hover: { + name: 'hover', + a11yAlias: 'interest', + a11yNote: "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus.", + defaultTriggerType: 'alternate', + flags: { hasReversed: false, hasEffectId: false, showMultipleEffectsNote: true }, + 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: '', + }, + prose: { + 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`.', + }, + pitfalls: [{ id: 'hit-area', variant: 'full-lean-hover' }], + triggerTypes: { + 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' }, + }, + stateActions: { + 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' }, + }, + }, + + click: { + name: 'click', + a11yAlias: 'activate', + a11yNote: "Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space).", + defaultTriggerType: 'alternate', + flags: { hasReversed: true, hasEffectId: true, showMultipleEffectsNote: false }, + 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.", + }, + prose: { + 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: '', + }, + pitfalls: [], + triggerTypes: { + 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' }, + }, + stateActions: { + 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' }, + }, + }, + + viewEnter: { + name: 'viewEnter', + defaultTriggerType: 'once', + flags: { showMultipleEffectsNote: true }, + pitfalls: [{ id: 'same-element-viewenter', variant: 'short' }], + triggerTypes: { + 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' }, + }, + }, + + viewProgress: { + name: 'viewProgress', + flags: { showMultipleEffectsNote: true }, + pitfalls: [{ id: 'overflow-clip', variant: 'short' }], + }, + + pointerMove: { + name: 'pointerMove', + flags: { showMultipleEffectsNote: true }, + pitfalls: [{ id: 'hit-area', variant: 'pointermove-source' }], + 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' }, + ], + }, + + animationEnd: { + name: 'animationEnd', + params: [{ name: 'effectId', type: 'string', optional: false, description: 'ID of the preceding effect' }], + pitfalls: [], + }, + + pageVisible: { + name: 'pageVisible', + params: [], + pitfalls: [], + }, + }, +}; +``` + +### 2.2 Rules + +- All values are plain strings, arrays, or shallow objects (max 3 levels deep). +- Per-trigger `vars` override global `vars` — the assembler merges `{ ...glossary.vars, ...glossary.triggers[name].vars }` when resolving `{{var.X}}` for a trigger template. +- The glossary is the **only** `.mjs` file a maintainer edits for data changes (adding presets, fixing descriptions, changing package names). +- Adding a new easing or preset = adding one string to one array. + +## 3. Templating Syntax + +Templates are plain `.md` files with four directives. The assembler resolves them in order: includes first (recursive), then conditionals, then each-loops, then value substitutions. + +### 3.1 Value Substitution: `{{path.to.value}}` + +Replaces with the resolved value from the render context (glossary + trigger-specific data merged). + +```markdown +This document contains rules for `{{meta.packageName}}`. +``` + +For variable definitions, `{{var.SOURCE_KEY}}` resolves to the merged variable description (trigger override if available, else global default). + +**Computed values:** Some values need formatting before insertion. The assembler pre-computes these into the render context before template resolution: + +| Context key | Value | +|---|---| +| `computed.easingList` | All easings formatted as `` `'name'` `` joined with `, ` | +| `computed.presetTable` | Markdown table of preset categories | +| `computed.rangeTable` | Markdown table of range names | +| `computed.rangeList` | Bullet list of range names with descriptions | +| `computed.triggerTypeUnion` | `'once' \| 'repeat' \| ...` | +| `computed.rangeNameUnion` | `'cover' \| 'entry' \| ...` | +| `computed.transitionEasingUnion` | `'linear' \| 'hardBackOut' \| ...` | +| `computed.paramsType` | Formatted TypeScript type for pointerMove params | + +### 3.2 Section Include: `{{> path#variant}}` + +Includes content from a section file. The path is relative to `templates/sections/` (no `.md` extension needed). The `#variant` part selects a specific section within the file (defaults to `#default` if omitted). + +```markdown +{{> fouc#code-web}} +``` + +Resolves to the content of the `## code-web` section in `templates/sections/fouc.md`. + +**Section file format:** Standard markdown with `##` headings as variant keys: + +```markdown +## default +Content for the default variant. + +## brief +Shorter version. + +## detailed +Longer version with more context. +``` + +The `##` heading line is stripped from output. Content runs until the next `##` heading or EOF. + +**Parameterized includes:** Section content can itself contain `{{}}` placeholders. These are resolved using the same render context as the parent template. If a section needs caller-specific values, the caller sets them in the render context before the section is included. + +**Recursive includes:** Sections can include other sections. The assembler resolves recursively (with cycle detection). + +### 3.3 Conditional: `{{#if path.to.value}} ... {{/if}}` + +Includes the block only if the value is truthy. Supports `{{#else}}`. + +```markdown +{{#if trigger.flags.hasReversed}} + reversed: [INITIAL_REVERSED_BOOL], +{{/if}} +``` + +Nesting is allowed. The condition path is resolved against the render context. + +### 3.4 Iteration: `{{#each path.to.array as item}} ... {{/each}}` + +Iterates over an array or object entries. Inside the block, `{{item}}` refers to the current element. For object iteration, `{{item.key}}` and `{{item.value}}` are available. + +```markdown +{{#each trigger.triggerTypes as tt}} + - `'{{tt.key}}'` — {{tt.value.full}} +{{/each}} +``` + +For array iteration: + +```markdown +{{#each effects.presets.mouse as preset}} +`{{preset}}`{{#if !last}}, {{/if}} +{{/each}} +``` + +## 4. Section Files (`templates/sections/`) + +Each section file contains one or more variants of a reusable prose block. Variants are separated by `## variantName` headings. + +### 4.1 `fouc.md` + +Migrated from `_content/fragments/fouc.md`. Variants: + +| Variant | Content | Used by | +|---------|---------|---------| +| `code-inject` | The `` HTML snippet | viewenter, integration, full-lean | +| `code-web` | Web custom element with `data-interact-initial` | viewenter, integration, full-lean | +| `code-react` | React `` with `initial={true}` | viewenter, integration, full-lean | +| `code-vanilla` | Vanilla HTML with `data-interact-initial` | viewenter, integration, full-lean | + +The code examples use `{{key}}` and `{{classAttr}}` which are set in the render context by the calling template. For viewenter templates, `key` = `[SOURCE_KEY]`, `classAttr` = `""`. For composites, `key` = `hero`, `classAttr` = ` class="hero"` or ` className="hero"`. + +### 4.2 `quick-start.md` + +Migrated from `_content/fragments/quick-start.md`. Variants: + +| Variant | Used by | +|---------|---------| +| `install` | integration, full-lean | +| `web` | full-lean | +| `web-brief` | integration | +| `react` | full-lean | +| `vanilla` | full-lean | +| `vanilla-brief` | integration | +| `cdn` | full-lean | +| `register-presets` | full-lean | +| `multiple-instances` | full-lean | + +All variants use `{{meta.entry.web}}`, `{{meta.entry.react}}`, `{{meta.entry.vanilla}}`, `{{meta.presetsPackage}}`, `{{meta.installCommand}}` from the glossary. + +### 4.3 `static-api.md` + +Variants: `detailed`, `brief`. Migrated from `_content/fragments/static-api.md`. Pure markdown tables, no dynamic content. + +### 4.4 `element-resolution.md` + +Variants: `intro`, `source`, `target`, `source-brief`, `target-brief`. Migrated from `_content/fragments/element-resolution.md`. Pure prose, no dynamic content. + +### 4.5 `config-structure.md` + +Variants: `detailed`, `brief`. Migrated from `_content/fragments/config-structure.md`. Pure prose with code blocks. + +### 4.6 `sequences.md` + +Variants: `detailed`, `brief`. Migrated from `_content/fragments/sequences.md`. Pure prose with code blocks. + +### 4.7 `pitfalls.md` + +**All pitfalls consolidated in one file.** Each pitfall has multiple variants for different rendering contexts (trigger-specific rule files vs full-lean overview). + +| Section heading | Variant meaning | Used by | +|-----------------|-----------------|---------| +| `hit-area-trigger` | Trigger-specific (pointermove-source context) | pointermove | +| `hit-area-full-lean-hover` | Full-lean hover context | full-lean | +| `hit-area-full-lean-pointermove` | Full-lean pointermove context | full-lean | +| `same-element-viewenter-short` | Short version for trigger file | viewenter | +| `same-element-viewenter-long` | Long version for full-lean | full-lean | +| `overflow-clip-short` | Short version for trigger file | viewprogress | +| `overflow-clip-long` | Long version for full-lean | full-lean | +| `dont-guess-presets` | Single variant | full-lean | +| `reduced-motion` | Single variant | full-lean | +| `perspective` | Single variant | full-lean | + +### 4.8 `progress-type.md` + +Variants: `detailed`, `brief`. Migrated from `_content/fragments/progress-type.md`. + +### 4.9 `multiple-effects-note.md` + +Variants: `default`, `viewEnter`, `viewProgress`, `pointerMove`. Migrated from `_content/fragments/multiple-effects-note.md`. + +## 5. Trigger Templates (`templates/triggers/`) + +### 5.1 `event-trigger.md` → `click.md`, `hover.md` + +One template, rendered twice (once with `trigger = glossary.triggers.click`, once with `trigger = glossary.triggers.hover`). + +**Structure:** + +``` +# {{trigger.Name}} Trigger Rules for {{meta.packageName}} + intro paragraph + a11y note + pitfalls (if any) + +## Table of Contents + +## Rule 1: keyframeEffect / namedEffect (TimeEffect) + fill critical note + multiple-effects note (if flag set) + code template with: + {{#if trigger.flags.hasReversed}} reversed field {{/if}} + {{#if trigger.flags.hasEffectId}} effectId field {{/if}} + variables section: + SOURCE_KEY — {{var.SOURCE_KEY}} ← trigger override + TARGET_KEY — {{var.TARGET_KEY}} ← trigger override + TRIGGER_TYPE — {{#each trigger.triggerTypes}} + FILL_MODE — {{var.FILL_MODE}} ← trigger override + etc. + +## Rule 2: transition / transitionProperties (StateEffect) + code template + stateAction descriptions: {{#each trigger.stateActions}} + +## Rule 3: customEffect (TimeEffect) + code template + +## Rule 4: Sequences + code template +``` + +**Conditional blocks** (the ~5 places click/hover differ): + +1. `{{var.SOURCE_KEY}}` — trigger-specific override in glossary +2. `{{var.TARGET_KEY}}` — trigger-specific override +3. `{{var.FILL_MODE}}` — trigger-specific override +4. `{{var.EASING_FUNCTION}}` — trigger-specific override for hover +5. `{{var.ALTERNATE_BOOL}}` — trigger-specific override for click +6. `{{#if trigger.flags.hasReversed}}` — `reversed` field in code template (click only) +7. `{{#if trigger.flags.hasEffectId}}` — `effectId` field in code template (click only) +8. `{{trigger.prose.fillCritical}}` — different fill guidance wording +9. `{{trigger.prose.customEffectExamples}}` — click has extra examples +10. `{{trigger.prose.offsetEasingSuffix}}` — hover has extra suffix + +Most of these are handled by glossary variable overrides (no `{{#if}}` needed). Only `hasReversed` and `hasEffectId` need actual conditional blocks in the code template. + +### 5.2 `viewenter.md` → `viewenter.md` + +Rendered with `trigger = glossary.triggers.viewEnter`. + +**Structure:** + +``` +# ViewEnter Trigger Rules for {{meta.packageName}} + intro paragraph + pitfalls + +## Preventing Flash of Unstyled Content (FOUC) + explanation prose + Step 1: {{> fouc#code-inject}} + Step 2: {{> fouc#code-web}}, {{> fouc#code-react}}, {{> fouc#code-vanilla}} + rules list + +## Rule 1: keyframeEffect / namedEffect (TimeEffect) + multiple-effects note + code template (has params.threshold, params.inset, selector) + variables with triggerType descriptions + +## Rule 2: customEffect (TimeEffect) + code template + +## Rule 3: Sequences + code template +``` + +### 5.3 `viewprogress.md` → `viewprogress.md` + +Rendered with `trigger = glossary.triggers.viewProgress`. + +**Structure:** + +``` +# ViewProgress Trigger Rules for {{meta.packageName}} + intro paragraph + pitfalls + offset semantics note + +## Rule 1: keyframeEffect or namedEffect + multiple-effects note + code template (has rangeStart/rangeEnd, no duration) + variables with range name list: {{computed.rangeList}} + +## Rule 2: customEffect + code template + +## Rule 3: Tall Wrapper + Sticky Container (contain range) + layout explanation + code template +``` + +### 5.4 `pointermove.md` → `pointermove.md` + +Rendered with `trigger = glossary.triggers.pointerMove`. + +**Structure:** + +``` +# PointerMove Trigger Rules for {{meta.packageName}} + intro paragraph + +## Trigger Source Elements with hitArea + pitfalls + +## PointerMoveParams + params type: {{computed.paramsType}} + +## Progress Object Structure + {{> progress-type#detailed}} + +## Centering with centeredToTarget + explanation + +## Device Conditions + code example + +## Rule 1: namedEffect + mouse presets list + code template + +## Rule 2: keyframeEffect with Single Axis + code template + +## Rule 3: Two keyframeEffects with Two Axes and composite + code template + +## Rule 4: customEffect + code template +``` + +## 6. Composite Templates (`templates/composites/`) + +### 6.1 `integration.md` → `integration.md` + +A mid-level integration guide. Uses section includes for shared content. Links to per-trigger files for detailed rules. + +**Structure:** + +``` +# {{meta.packageName}} Integration Rules + intro + +## Entry Points + {{> quick-start#install}} + Web: {{> quick-start#web-brief}} + React: inline (uses {{meta.entry.react}}) + Vanilla: {{> quick-start#vanilla-brief}} + +## Named Effects & registerEffects + register pattern, links to full-lean for effect type syntax + +## Configuration Schema + {{> config-structure#brief}} + Interaction type block + Element Selection prose + {{> element-resolution#source-brief}} + {{> element-resolution#target-brief}} + +## Triggers + summary table (all 9 triggers, hardcoded markdown table with {{meta.packageName}} refs) + +## Sequences + {{> sequences#brief}} + +## Critical CSS (FOUC Prevention) + summary + {{> fouc#code-inject}}, {{> fouc#code-web}}, {{> fouc#code-react}}, {{> fouc#code-vanilla}} + +## Static API + {{> static-api#brief}} +``` + +### 6.2 `full-lean.md` → `full-lean.md` + +The comprehensive reference. Largest template (~350 lines). Uses section includes, glossary refs, and computed tables. + +**Structure:** + +``` +# {{meta.packageName}} — Rules + intro + +## Table of Contents + +## Common Pitfalls + all pitfalls from all triggers (full-lean variants) + + dont-guess-presets, reduced-motion, perspective + +## Quick Start + {{> quick-start#install}} + {{> quick-start#multiple-instances}} + {{> quick-start#web}} + {{> quick-start#react}} + {{> quick-start#vanilla}} + {{> quick-start#cdn}} + {{> quick-start#register-presets}} + +## Element Binding + web + react examples (uses {{meta.entry.react}}) + +## Config Structure + {{> config-structure#detailed}} + +## Interactions + interaction type block, prose + +## Triggers + detailed trigger reference: + - hover/click: behavior tables using {{#each}} over triggerTypes/stateActions + - viewEnter: params, pitfall + - viewProgress: no params, pitfall + - pointerMove: params, rules, centeredToTarget, progress type + - animationEnd: params + +## Effects + common fields, fill/composite/easing guidance + Time-based Effect type + Scroll/Pointer-driven Effect type (with range table, sticky pattern) + StateEffect type + Animation Payloads (namedEffect with preset table, keyframeEffect, customEffect) + +## Sequences + {{> sequences#detailed}} + +## Conditions + inline prose (single consumer) + +## FOUC Prevention + full explanation + {{> fouc}} includes + +## Element Resolution + {{> element-resolution#intro}} + {{> element-resolution#source}} + {{> element-resolution#target}} + +## Static API + {{> static-api#detailed}} +``` + +## 7. Assembler (`assemble.mjs`) + +### 7.1 CLI Interface + +```bash +node _build/assemble.mjs # generate all rule files +node _build/assemble.mjs --check # compare without writing (for CI) +``` + +### 7.2 Build Manifest + +Hardcoded in the script (no separate config file): + +```javascript +const manifest = [ + { template: 'triggers/event-trigger.md', trigger: 'hover', output: 'hover.md' }, + { template: 'triggers/event-trigger.md', trigger: 'click', output: 'click.md' }, + { template: 'triggers/viewenter.md', trigger: 'viewEnter', output: 'viewenter.md' }, + { template: 'triggers/viewprogress.md', trigger: 'viewProgress', output: 'viewprogress.md' }, + { template: 'triggers/pointermove.md', trigger: 'pointerMove', output: 'pointermove.md' }, + { template: 'composites/integration.md', trigger: null, output: 'integration.md' }, + { template: 'composites/full-lean.md', trigger: null, output: 'full-lean.md' }, +]; +``` + +### 7.3 Algorithm + +``` +1. Import glossary.mjs + +2. Load all section files: + - Read templates/sections/*.md + - Parse into Map> + - Variant = text between ## heading and next ## or EOF + - The ## line itself is stripped + +3. Pre-compute derived values: + - computed.easingList = glossary.effects.easings formatted + - computed.presetTable = markdown table from glossary.effects.presets + - computed.rangeTable = markdown table from glossary.effects.ranges + - computed.rangeList = bullet list from glossary.effects.ranges + - computed.triggerTypeUnion = union string + - computed.rangeNameUnion = union string + - computed.transitionEasingUnion = union string + +4. For each manifest entry: + a. Read the template .md file + b. Build render context: + - Base: { ...glossary, computed } + - If trigger: merge trigger-specific data: + context.trigger = glossary.triggers[trigger] + context.var = { ...glossary.vars, ...glossary.triggers[trigger].vars } + context.trigger.Name = capitalize(trigger) + c. Resolve directives (in order): + i. {{> path#variant}} — recursive include resolution + ii. {{#if condition}} ... {{/if}} — conditional blocks + iii. {{#each collection as item}} ... {{/each}} — iteration + iv. {{path.to.value}} — value substitution + d. Trim trailing whitespace per line, ensure single trailing newline + e. Write to rules/ or compare in --check mode + +5. --check mode: + - Compare each output against existing file + - Show first differing line on mismatch + - Exit 1 if any file is stale +``` + +### 7.4 Directive Resolution Details + +**Include resolution (`{{> path#variant}}`):** + +```javascript +function resolveIncludes(text, sections, context, depth = 0) { + if (depth > 10) throw new Error('Circular include detected'); + return text.replace(/\{\{>\s*([\w/.-]+)(?:#([\w-]+))?\s*\}\}/g, (_, path, variant) => { + variant = variant || 'default'; + const file = sections.get(path); + if (!file) throw new Error(`Section not found: ${path}`); + const content = file.get(variant); + if (content === undefined) throw new Error(`Variant "${variant}" not found in ${path}`); + return resolveIncludes(content, sections, context, depth + 1); + }); +} +``` + +**Conditional resolution (`{{#if}}`):** + +```javascript +function resolveConditionals(text, context) { + // Supports nesting. Processes from innermost outward. + const IF_RE = /\{\{#if\s+([\w.]+)\}\}([\s\S]*?)(?:\{\{#else\}\}([\s\S]*?))?\{\{\/if\}\}/g; + let prev; + do { + prev = text; + text = text.replace(IF_RE, (_, path, thenBlock, elseBlock) => { + return resolve(context, path) ? thenBlock : (elseBlock || ''); + }); + } while (text !== prev); + return text; +} +``` + +**Each resolution (`{{#each}}`):** + +```javascript +function resolveEach(text, context) { + const EACH_RE = /\{\{#each\s+([\w.]+)\s+as\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g; + return text.replace(EACH_RE, (_, path, itemName, body) => { + const collection = resolve(context, path); + if (Array.isArray(collection)) { + return collection.map((item, i) => + resolveValues(body, { ...context, [itemName]: item, last: i === collection.length - 1 }) + ).join(''); + } + // Object: iterate entries as { key, value } + const entries = Object.entries(collection); + return entries.map(([key, value], i) => + resolveValues(body, { ...context, [itemName]: { key, value }, last: i === entries.length - 1 }) + ).join(''); + }); +} +``` + +**Value resolution (`{{path.to.value}}`):** + +```javascript +function resolveValues(text, context) { + return text.replace(/\{\{([\w.]+)\}\}/g, (match, path) => { + const val = resolve(context, path); + if (val === undefined) throw new Error(`Unresolved placeholder: ${path}`); + if (val === '') return ''; + return String(val); + }); +} + +function resolve(context, path) { + return path.split('.').reduce((obj, key) => obj?.[key], context); +} +``` + +### 7.5 Error Handling + +The assembler throws on: +- Unknown section path or variant +- Unresolved `{{placeholder}}` in final output +- Circular includes (depth > 10) +- Missing trigger name in glossary +- Missing template file + +Errors include the template filename and the problematic placeholder/path for quick debugging. + +## 8. Section File Format + +Section files use `## variantName` headings as delimiters. The heading line is stripped from output. + +**Example (`fouc.md`):** + +```markdown +## code-inject +**Append to `` or beginning of ``:** + +```html + +``` + +## code-web +**Web (Custom Elements):** + +```html + + ... + +``` + +## code-react +**React:** + +```tsx + + ... + +``` + +## code-vanilla +**Vanilla:** + +```html +
...
+``` +``` + +Rules: +- First `##` heading is required (content before it is ignored or errors) +- Variant names are lowercase, hyphen-separated: `code-web`, `source-brief` +- Content runs from after the `##` line to the next `##` or EOF +- Leading/trailing blank lines within a variant are preserved (they're part of the markdown) +- Section files can contain `{{}}` placeholders — resolved using the caller's context + +## 9. CI Integration + +The existing CI step stays the same: + +```yaml +- name: Verify generated rules are up to date + run: yarn workspace @wix/interact build:rules --check +``` + +The `package.json` script changes from: + +```json +"build:rules": "node scripts/build-rules.mjs" +``` + +to: + +```json +"build:rules": "node _build/assemble.mjs" +``` + +## 10. Migration Checklist + +### Phase 1: Setup +- [ ] Create `_build/` directory structure +- [ ] Write `glossary.mjs` from existing data modules +- [ ] Write `assemble.mjs` with directive resolution + +### Phase 2: Sections +- [ ] Migrate each fragment to a section file (new `##` format) +- [ ] Consolidate all pitfall files into one `pitfalls.md` +- [ ] Verify section content matches original fragment content + +### Phase 3: Templates +- [ ] Write `event-trigger.md` from `event-trigger-rule.mjs` output +- [ ] Write `viewenter.md` from `viewenter-rule.mjs` output +- [ ] Write `viewprogress.md` from `viewprogress-rule.mjs` output +- [ ] Write `pointermove.md` from `pointermove-rule.mjs` output +- [ ] Write `integration.md` from `integration.mjs` output +- [ ] Write `full-lean.md` from `full-lean.mjs` output + +### Phase 4: Verify +- [ ] Run `node _build/assemble.mjs` +- [ ] Diff output against current `rules/*.md` — must match exactly (except planned typo fixes) +- [ ] Run `node _build/assemble.mjs --check` — must pass + +### Phase 5: Cleanup +- [ ] Delete `_content/` directory entirely +- [ ] Delete `scripts/build-rules.mjs` +- [ ] Update `package.json` build:rules script +- [ ] Update `.prettierignore` +- [ ] Verify CI passes + +## 11. File Size Budget + +Target: source-to-output ratio < 0.9:1. + +| File | Target lines | +|------|-------------| +| `glossary.mjs` | 230–270 | +| `assemble.mjs` | 100–130 | +| `sections/*.md` (9 files) | 230–270 total | +| `triggers/*.md` (4 files) | 700–800 total | +| `composites/*.md` (2 files) | 320–380 total | +| **Total source** | **1,580–1,850** | +| **Total output** | **~2,036** | + +If any template exceeds 250 lines, consider extracting more sections. If the glossary exceeds 300 lines, it's still acceptable — it's a single file and every line is data. + +## 12. What NOT to Do + +- **Don't use a templating library** (Handlebars, EJS, Mustache). The four directives above are trivial to implement (~60 lines) and avoid a dependency. +- **Don't generate composites from rendered trigger files.** After analysis, `full-lean.md` and `integration.md` have their own condensed trigger coverage — they don't include full trigger rules. They share *sections* with trigger files, which is where deduplication lives. +- **Don't over-abstract.** If a piece of text appears in only one template, inline it. Extract to a section only when it appears in 2+ templates. +- **Don't add computed columns/helpers to the glossary.** Keep it pure data. Formatting (capitalize, join, table rendering) belongs in `assemble.mjs` pre-computation. diff --git a/packages/interact/_build/assemble.mjs b/packages/interact/_build/assemble.mjs new file mode 100644 index 00000000..cb6dc3ba --- /dev/null +++ b/packages/interact/_build/assemble.mjs @@ -0,0 +1,227 @@ +import { readFileSync, writeFileSync, readdirSync, existsSync } from 'node:fs'; +import { join, dirname, basename, relative } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { glossary } from './glossary.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const check = process.argv.includes('--check'); +const TEMPLATES = join(__dirname, 'templates'); +const OUT_DIR = join(__dirname, '..', 'rules'); +const manifest = [ + { template: 'triggers/event-trigger.md', trigger: 'hover', output: 'hover.md' }, + { template: 'triggers/event-trigger.md', trigger: 'click', output: 'click.md' }, + { template: 'triggers/viewenter.md', trigger: 'viewEnter', output: 'viewenter.md' }, + { template: 'triggers/viewprogress.md', trigger: 'viewProgress', output: 'viewprogress.md' }, + { template: 'triggers/pointermove.md', trigger: 'pointerMove', output: 'pointermove.md' }, + { template: 'composites/integration.md', trigger: null, output: 'integration.md' }, + { template: 'composites/full-lean.md', trigger: null, output: 'full-lean.md' }, +]; + +const resolve = (ctx, path) => path.split('.').reduce((o, k) => o?.[k], ctx); +const cap = (s) => (s ? s[0].toUpperCase() + s.slice(1) : ''); +const union = (arr) => arr.map((x) => `'${x}'`).join(' | '); +const paramsType = (params) => + params?.length ? `{\n${params.map((p) => ` ${p.name}${p.optional ? '?' : ''}: ${p.type};`).join('\n')}\n}` : '{}'; + +function loadSections(dir) { + const map = new Map(); + if (!existsSync(dir)) return map; + const walk = (d, base) => { + for (const e of readdirSync(d, { withFileTypes: true })) { + const p = join(d, e.name); + if (e.isDirectory()) walk(p, base); + else if (e.name.endsWith('.md')) + map.set(relative(base, p).slice(0, -3).replace(/\\/g, '/'), parseSections(readFileSync(p, 'utf8'))); + } + }; + walk(dir, dir); + return map; +} + +function parseSections(src) { + const m = new Map(); + const lines = src.split(/\r?\n/); + let i = 0; + while (i < lines.length && !/^## /.test(lines[i])) i++; + while (i < lines.length) { + const h = lines[i].match(/^## (.+)$/); + if (!h) { + i++; + continue; + } + const v = h[1].trim(); + i++; + const buf = []; + while (i < lines.length && !/^## /.test(lines[i])) buf.push(lines[i++]); + m.set(v, buf.join('\n').replace(/\s+$/, '')); + } + return m; +} + +function resolveIncludes(text, sections, depth = 0) { + if (depth > 10) throw new Error('Circular include detected'); + return text.replace(/\{\{>\s*([\w/.-]+)(?:#([\w-]+))?\s*\}\}/g, (_, path, variant) => { + variant = variant || 'default'; + const file = sections.get(path); + if (!file) throw new Error(`Section not found: ${path}`); + const content = file.get(variant); + if (content === undefined) throw new Error(`Variant "${variant}" not found in ${path}`); + return resolveIncludes(content, sections, depth + 1); + }); +} + +function resolveConditionals(text, context) { + const IF_RE = /\{\{#if\s+([\w.!]+)\}\}([\s\S]*?)(?:\{\{#else\}\}([\s\S]*?))?\{\{\/if\}\}/g; + let prev; + do { + prev = text; + text = text.replace(IF_RE, (_, path, thenBlock, elseBlock) => { + const negate = path.startsWith('!'); + const p = negate ? path.slice(1) : path; + const val = resolve(context, p); + const truthy = negate ? !val : !!val; + return truthy ? thenBlock : (elseBlock || ''); + }); + } while (text !== prev); + return text; +} + +function resolveValues(text, context) { + return text.replace(/\{\{([\w.]+)\}\}/g, (_, path) => { + const val = resolve(context, path); + if (val === undefined) throw new Error(`Unresolved placeholder: ${path}`); + return val === '' ? '' : String(val); + }); +} + +function splitEach(text) { + const O = '{{#each ', C = '{{/each}}'; + const s = text.indexOf(O); + if (s === -1) return null; + const oe = text.indexOf('}}', s); + if (oe === -1) throw new Error('Malformed {{#each}}'); + const tag = text.slice(s, oe + 2); + const m = tag.match(/\{\{#each\s+([\w.]+)\s+as\s+(\w+)\}\}/); + if (!m) throw new Error(`Malformed {{#each}} open tag: ${tag}`); + const [, path, itemName] = m; + let depth = 1, + pos = oe + 2; + while (depth) { + const o = text.indexOf(O, pos); + const c = text.indexOf(C, pos); + if (c === -1) throw new Error('Unclosed {{#each}}'); + if (o !== -1 && o < c) { + depth++; + const ic = text.indexOf('}}', o); + if (ic === -1) throw new Error('Malformed nested {{#each}}'); + pos = ic + 2; + } else { + depth--; + if (!depth) return { head: text.slice(0, s), path, itemName, body: text.slice(oe + 2, c), tail: text.slice(c + C.length) }; + pos = c + C.length; + } + } + throw new Error('Unclosed {{#each}}'); +} + +function render(text, context, sections) { + const sp = splitEach(text); + if (!sp) { + let t = resolveIncludes(text, sections, 0); + t = resolveConditionals(t, context); + t = resolveValues(t, context); + return t; + } + let out = render(sp.head, context, sections); + const coll = resolve(context, sp.path); + if (coll === undefined) throw new Error(`{{#each ${sp.path}}}: collection missing`); + const len = Array.isArray(coll) ? coll.length : Object.keys(coll).length; + const run = (item, i, n) => render(sp.body, { ...context, [sp.itemName]: item, last: i === n - 1 }, sections); + if (len === 0) { + out = out.replace(/\n$/, ''); + return out + render(sp.tail, context, sections); + } + if (Array.isArray(coll)) for (let i = 0; i < coll.length; i++) out += run(coll[i], i, coll.length); + else { + const ent = Object.entries(coll); + for (let i = 0; i < ent.length; i++) out += run({ key: ent[i][0], value: ent[i][1] }, i, ent.length); + } + return out + render(sp.tail, context, sections); +} + +function normalize(s) { + return s.replace(/[ \t]+$/gm, '').replace(/\s+$/, '') + '\n'; +} + +function behaviorRows(behaviorKey, triad, defaultKey) { + const keys = [...new Set(triad.flatMap((t) => Object.keys(t[behaviorKey])))]; + return keys.map((k) => ({ + label: k === defaultKey ? `\`'${k}'\` (default)` : `\`'${k}'\``, + hoverShort: triad[0][behaviorKey][k]?.short ?? '—', + clickShort: triad[1][behaviorKey][k]?.short ?? '—', + })); +} + +function buildComputed() { + const { effects, triggers } = glossary; + const rows = (o, fn) => Object.entries(o).map(fn); + const presetBody = rows(effects.presets, ([c, ps]) => `| ${cap(c)} | ${ps.map((p) => `\`${p}\``).join(', ')} |`); + const presetTableCore = ['| Category | Presets |', '| :--- | :--- |', ...presetBody].join('\n'); + const presetTable = presetTableCore + .split('\n') + .map((line) => ` ${line}`) + .join('\n'); + const rangeTable = ['| Range name | Meaning |', '| :--- | :--- |', ...rows(effects.ranges, ([n, d]) => `| \`${n}\` | ${d} |`)].join('\n'); + const hover = triggers.hover; + const click = triggers.click; + const hoverClick = [hover, click]; + return { + easingList: effects.easings.map((e) => `'${e}'`).join(', '), + presetTable, + rangeTable, + rangeList: rows(effects.ranges, ([n, d]) => `- \`${n}\` — ${d}`).join('\n'), + triggerTypeUnion: union(effects.triggerTypes), + rangeNameUnion: union(Object.keys(effects.ranges)), + transitionEasingUnion: union(effects.transitionEasings), + paramsType: paramsType(triggers.pointerMove?.params), + triggerTypeBehaviorRows: behaviorRows('triggerTypes', hoverClick, hover.defaultTriggerType), + stateActionBehaviorRows: behaviorRows('stateActions', hoverClick, 'toggle'), + }; +} + +const sections = loadSections(join(TEMPLATES, 'sections')); +const computed = buildComputed(); +let stale = false; + +for (const { template, trigger: trig, output } of manifest) { + const tplPath = join(TEMPLATES, template); + if (!existsSync(tplPath)) throw new Error(`Missing template: ${template}`); + let ctx = { ...glossary, computed }; + if (trig != null) { + const t = glossary.triggers[trig]; + if (!t) throw new Error(`Missing trigger in glossary: ${trig}`); + ctx = { ...ctx, trigger: { ...t, Name: cap(t.name || trig) }, var: { ...glossary.vars, ...t.vars } }; + } + const text = readFileSync(tplPath, 'utf8'); + let result = render(text, ctx, sections); + if (result.includes('{{')) throw new Error(`${output}: unresolved template near ${JSON.stringify(result.match(/\{\{[^}]*\}\}?/)?.[0])}`); + result = normalize(result); + const outPath = join(OUT_DIR, output); + if (check) { + if (!existsSync(outPath)) throw new Error(`--check: missing ${output}`); + const exist = normalize(readFileSync(outPath, 'utf8')); + if (exist !== result) { + const a = exist.split('\n'), + b = result.split('\n'); + for (let i = 0; i < Math.max(a.length, b.length); i++) { + if (a[i] !== b[i]) { + console.error(`${basename(outPath)}: mismatch at line ${i + 1}\n want: ${JSON.stringify(b[i])}\n have: ${JSON.stringify(a[i])}`); + break; + } + } + stale = true; + } + } else writeFileSync(outPath, result); +} + +if (stale) process.exit(1); diff --git a/packages/interact/_build/glossary.mjs b/packages/interact/_build/glossary.mjs new file mode 100644 index 00000000..63bd65ac --- /dev/null +++ b/packages/interact/_build/glossary.mjs @@ -0,0 +1,256 @@ +// ═══════════════════════════════════════════════════════════════════════════ +// @wix/interact — rules glossary (single source of truth) +// ═══════════════════════════════════════════════════════════════════════════ + +export const glossary = { + // ═══════════════════════════════════════════════════════════════════════════ + // meta + // ═══════════════════════════════════════════════════════════════════════════ + meta: { + packageName: '@wix/interact', + presetsPackage: '@wix/motion-presets', + motionPackage: '@wix/motion', + installCommand: 'npm install @wix/interact @wix/motion-presets', + entry: { + web: '@wix/interact/web', + react: '@wix/interact/react', + vanilla: '@wix/interact', + }, + fouc: { + key: 'hero', + webSectionClass: ' class="hero"', + reactClassName: ' className="hero"', + }, + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // vars — [PLACEHOLDER] defaults; triggers..vars overrides + // ═══════════════════════════════════════════════════════════════════════════ + vars: { + SOURCE_KEY: + "identifier matching the element's key (`data-interact-key` for web, `interactKey` for React).", + TARGET_KEY: "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: + "fill mode for the animation (`'none'`, `'forwards'`, `'backwards'`, `'both'`).", + 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.', + 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 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).", + 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).", + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // effects + // ═══════════════════════════════════════════════════════════════════════════ + 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', + ], + }, + ranges: { + 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.", + }, + }, + + // ═══════════════════════════════════════════════════════════════════════════ + // triggers + // ═══════════════════════════════════════════════════════════════════════════ + triggers: { + hover: { + name: 'hover', + a11yAlias: 'interest', + a11yNote: + "Use `trigger: 'interest'` instead of `trigger: 'hover'` to also respond to keyboard focus.", + defaultTriggerType: 'alternate', + flags: { hasReversed: false, hasEffectId: false, showMultipleEffectsNote: true }, + vars: { + SOURCE_KEY: + "identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). 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'`.", + }, + prose: { + 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`.', + }, + pitfalls: [{ id: 'hit-area', variant: 'full-lean-hover' }], + triggerTypes: { + 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' }, + }, + stateActions: { + 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' }, + }, + }, + + click: { + name: 'click', + a11yAlias: 'activate', + a11yNote: + "Use `trigger: 'activate'` instead of `trigger: 'click'` to also respond to keyboard activation (Enter / Space).", + defaultTriggerType: 'alternate', + flags: { hasReversed: true, hasEffectId: true, showMultipleEffectsNote: false }, + vars: { + SOURCE_KEY: + "identifier matching the element's key (`data-interact-key` for web, `interactKey` for React). 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: + "optional. `true` to alternate direction on every other iteration (within a single playback). Different from `triggerType: 'alternate'` which alternates per click.", + }, + prose: { + 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: '', + }, + pitfalls: [], + triggerTypes: { + 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' }, + }, + stateActions: { + 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' }, + }, + }, + + viewEnter: { + name: 'viewEnter', + defaultTriggerType: 'once', + flags: { hasReversed: false, hasEffectId: false, showMultipleEffectsNote: true }, + 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)." }, + ], + vars: { + 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.", + 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.", + ITERATIONS: + "optional. Number of iterations, or `Infinity` for continuous loops. Primarily useful with `triggerType: 'state'`.", + 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.", + }, + pitfalls: [{ id: 'same-element-viewenter', variant: 'short' }], + triggerTypes: { + 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', + default: true, + }, + 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' }, + }, + }, + + viewProgress: { + name: 'viewProgress', + defaultTriggerType: null, + flags: { hasReversed: false, hasEffectId: false, showMultipleEffectsNote: true }, + pitfalls: [{ id: 'overflow-clip', variant: 'short' }], + }, + + pointerMove: { + name: 'pointerMove', + defaultTriggerType: null, + flags: { hasReversed: false, hasEffectId: false, showMultipleEffectsNote: true }, + 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', variant: 'pointermove-source' }], + }, + + animationEnd: { + name: 'animationEnd', + defaultTriggerType: null, + flags: { hasReversed: false, hasEffectId: false, showMultipleEffectsNote: false }, + params: [{ name: 'effectId', type: 'string', optional: false, description: 'ID of the preceding effect' }], + pitfalls: [], + }, + + pageVisible: { + name: 'pageVisible', + defaultTriggerType: null, + flags: { hasReversed: false, hasEffectId: false, showMultipleEffectsNote: false }, + params: [], + pitfalls: [], + }, + }, +}; diff --git a/packages/interact/_build/templates/composites/full-lean.md b/packages/interact/_build/templates/composites/full-lean.md new file mode 100644 index 00000000..3e1796ca --- /dev/null +++ b/packages/interact/_build/templates/composites/full-lean.md @@ -0,0 +1,490 @@ +# {{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. + +- **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. +{{> pitfalls#dont-guess-presets}} +{{> pitfalls#reduced-motion}} +{{> pitfalls#perspective}} + +--- + +## Quick Start + +{{> quick-start#install}} + +{{> quick-start#multiple-instances}} + +{{> quick-start#web}} + +{{> quick-start#react}} + +{{> quick-start#vanilla}} + +{{> quick-start#cdn}} + +{{> 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 '{{meta.entry.react}}'; + + + ... +; +``` + +--- + +## Config Structure + +{{> 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. + +--- + +## 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 | +| :--- | :--- | :--- | +{{#each computed.triggerTypeBehaviorRows as row}}| {{row.label}} | {{row.hoverShort}} | {{row.clickShort}} | +{{/each}} + +**`stateAction`** — on `StateEffect`: + +| Action | hover behavior | click behavior | +| :--- | :--- | :--- | +{{#each computed.stateActionBehaviorRows as row}}| {{row.label}} | {{row.hoverShort}} | {{row.clickShort}} | +{{/each}} + +### 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: {{computed.triggerTypeUnion}}; // default: '{{triggers.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. + +### 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-type#brief}} + +### 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'` — 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): 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 `{{meta.motionPackage}}` (in addition to standard CSS easings): + +{{computed.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?: {{computed.transitionEasingUnion}}; + // + exactly one animation payload (see below) +} +``` + +**RangeOffset** — works like CSS's `animation-range`: + +```ts +{ + name?: {{computed.rangeNameUnion}}; + offset?: { value: number; unit: 'percentage' | 'px' | 'vh' | 'vw' } +} +``` + +{{computed.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 `{{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: + +{{computed.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 + +{{> sequences#detailed}} + +--- + +## 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 + +**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: + +```ts +import { generate } from '{{meta.entry.web}}'; +const css = generate(config); +``` + +{{> fouc#code-inject}} + +### Step 2: Mark elements + +{{> fouc#code-web}} + +{{> fouc#code-react}} + +{{> fouc#code-vanilla}} + +### 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'`. + +--- + +## Element Resolution + +{{> element-resolution#intro}} + +{{> element-resolution#source}} + +{{> element-resolution#target}} + +--- + +## Static API + +{{> static-api#detailed}} diff --git a/packages/interact/_build/templates/composites/integration.md b/packages/interact/_build/templates/composites/integration.md new file mode 100644 index 00000000..29f2337b --- /dev/null +++ b/packages/interact/_build/templates/composites/integration.md @@ -0,0 +1,221 @@ +# {{meta.packageName}} Integration Rules + +Rules for integrating `{{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: + +{{> quick-start#install}} + +### Web (Custom Elements) + +{{> quick-start#web-brief}} + +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 '{{meta.entry.react}}'; + +useEffect(() => { + const instance = Interact.create(config); + + return () => { + instance.destroy(); + }; +}, [config]); +``` + +Replace target elements with ``: + +```tsx +import { Interaction } from '{{meta.entry.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 + +{{> quick-start#vanilla-brief}} + +**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 `{{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 {{meta.presetsPackage}} +``` + +**Import and register:** + +```typescript +import { Interact } from '{{meta.entry.web}}'; +import * as presets from '{{meta.presetsPackage}}'; + +Interact.registerEffects(presets); +``` + +Or register selectively: + +```typescript +import { FadeIn, ParallaxScroll } from '{{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 + +{{> config-structure#brief}} + +### 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. + +{{> element-resolution#source-brief}} + +{{> element-resolution#target-brief}} + +--- + +## 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 | — | +| `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. + +--- + +## Sequences + +{{> sequences#brief}} + +--- + +## Critical CSS (FOUC Prevention) + +**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. + +**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 '{{meta.entry.web}}'; +const css = generate(config); +``` + +{{> fouc#code-inject}} + +{{> fouc#code-web}} + +{{> fouc#code-react}} + +{{> fouc#code-vanilla}} + +--- + +## Static API + +{{> static-api#brief}} diff --git a/packages/interact/_build/templates/sections/config-structure.md b/packages/interact/_build/templates/sections/config-structure.md new file mode 100644 index 00000000..e1b2ce16 --- /dev/null +++ b/packages/interact/_build/templates/sections/config-structure.md @@ -0,0 +1,32 @@ +## detailed +```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. + +## brief +### 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/_build/templates/sections/element-resolution.md b/packages/interact/_build/templates/sections/element-resolution.md new file mode 100644 index 00000000..dcb33817 --- /dev/null +++ b/packages/interact/_build/templates/sections/element-resolution.md @@ -0,0 +1,40 @@ +## intro +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 +### 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 +### 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-brief +#### 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-brief +#### 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/_build/templates/sections/fouc.md b/packages/interact/_build/templates/sections/fouc.md new file mode 100644 index 00000000..68ff7c5e --- /dev/null +++ b/packages/interact/_build/templates/sections/fouc.md @@ -0,0 +1,52 @@ +## code-inject +**Append to `` or beginning of ``:** + +```html + +``` +## code-web +**Web (Custom Elements):** + +```html + + ... + +``` +## code-react +**React:** + +```tsx + + ... + +``` +## code-vanilla +**Vanilla:** + +```html +
...
+``` +## code-web-rules +**Web (Custom Elements):** + +```html + +
...
+
+``` +## code-react-rules +**React:** + +```tsx + + ... + +``` +## code-vanilla-rules +**Vanilla:** + +```html +
...
+``` diff --git a/packages/interact/_build/templates/sections/multiple-effects-note.md b/packages/interact/_build/templates/sections/multiple-effects-note.md new file mode 100644 index 00000000..c2625dd3 --- /dev/null +++ b/packages/interact/_build/templates/sections/multiple-effects-note.md @@ -0,0 +1,8 @@ +## default +**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}}. +## viewEnter +**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. +## viewProgress +**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. +## pointerMove +**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/_build/templates/sections/pitfalls.md b/packages/interact/_build/templates/sections/pitfalls.md new file mode 100644 index 00000000..88d903b7 --- /dev/null +++ b/packages/interact/_build/templates/sections/pitfalls.md @@ -0,0 +1,31 @@ +## hit-area-trigger +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. +## hit-area-hover-rule +- **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. +## hit-area-full-lean-hover +- **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 - 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. +## hit-area-full-lean-pointermove +- **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**: 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. +## same-element-viewenter-short +> **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. +## same-element-viewenter-long +- **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. +## overflow-clip-short +> **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`. +## overflow-clip-long +- **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`. +## dont-guess-presets +- **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 +- **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. +## perspective +- **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/_build/templates/sections/progress-type.md b/packages/interact/_build/templates/sections/progress-type.md new file mode 100644 index 00000000..23c66f75 --- /dev/null +++ b/packages/interact/_build/templates/sections/progress-type.md @@ -0,0 +1,23 @@ +## detailed +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 +}; +``` +## brief +**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/_build/templates/sections/quick-start.md b/packages/interact/_build/templates/sections/quick-start.md new file mode 100644 index 00000000..90a0b22e --- /dev/null +++ b/packages/interact/_build/templates/sections/quick-start.md @@ -0,0 +1,80 @@ +## install +```bash +{{meta.installCommand}} +``` +## web +**Web (Custom Elements):** + +```ts +import { Interact } from '{{meta.entry.web}}'; +const instance = Interact.create(config); +``` + +The `config` object is an `InteractConfig` containing `interactions` (required), and optionally shared `effects`, `sequences`, and `conditions`. +## web-brief +```typescript +import { Interact } from '{{meta.entry.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 +**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 '{{meta.entry.react}}'; + +useEffect(() => { + const instance = Interact.create(config); + + return () => { + instance.destroy(); + }; +}, [config]); +``` +## vanilla +**Vanilla JS:** + +```ts +import { Interact } from '{{meta.entry.vanilla}}'; +const instance = Interact.create(config); +instance.add(element, 'hero'); // bind after element exists in DOM +instance.remove('hero'); // unregister +``` +## vanilla-brief +```typescript +import { Interact } from '{{meta.entry.vanilla}}'; + +const interact = Interact.create(config); +interact.add(element, 'hero'); +``` +## cdn +**CDN (no build tools):** + +```html + +``` +## register-presets +**Registering presets** — MUST be called before calling `Interact.create()` with usage of `namedEffect`: + +```ts +import * as presets from '{{meta.presetsPackage}}'; +Interact.registerEffects(presets); +``` + +Or selectively: + +```ts +import { FadeIn, ParallaxScroll } from '{{meta.presetsPackage}}'; +Interact.registerEffects({ FadeIn, ParallaxScroll }); +``` +## multiple-instances +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/_build/templates/sections/sequences.md b/packages/interact/_build/templates/sections/sequences.md new file mode 100644 index 00000000..fd2db804 --- /dev/null +++ b/packages/interact/_build/templates/sections/sequences.md @@ -0,0 +1,96 @@ +## detailed +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 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. +- `[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`. + +## brief +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/_build/templates/sections/static-api.md b/packages/interact/_build/templates/sections/static-api.md new file mode 100644 index 00000000..2b4db2b2 --- /dev/null +++ b/packages/interact/_build/templates/sections/static-api.md @@ -0,0 +1,34 @@ +## detailed +| 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. + +## brief +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/_build/templates/triggers/event-trigger.md b/packages/interact/_build/templates/triggers/event-trigger.md new file mode 100644 index 00000000..ec1ca331 --- /dev/null +++ b/packages/interact/_build/templates/triggers/event-trigger.md @@ -0,0 +1,189 @@ +# {{trigger.Name}} Trigger Rules for {{meta.packageName}} + +This document contains rules for generating {{trigger.name}}-triggered interactions in `{{meta.packageName}}`. + +**CRITICAL — Accessible {{trigger.name}}**: {{trigger.a11yNote}} +{{#each trigger.pitfalls as pitfall}} +{{> pitfalls#hit-area-hover-rule}}{{/each}} + +## 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) + +Use `keyframeEffect` or `namedEffect` when the {{trigger.name}} should play an animation (CSS or WAAPI). Set `triggerType` on each effect to control playback behavior. + +**CRITICAL:** {{trigger.prose.fillCritical}} +{{#if trigger.flags.showMultipleEffectsNote}} +**Multiple effects:** The `effects` array can contain multiple effects — all share the same {{trigger.name}} trigger and fire together. Use this to animate different targets from a single {{trigger.name}} event. +{{/if}} +```typescript +{ + key: '[SOURCE_KEY]', + trigger: '{{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]',{{#if trigger.flags.hasReversed}} + reversed: [INITIAL_REVERSED_BOOL],{{/if}} + duration: [DURATION_MS], + easing: '[EASING_FUNCTION]', + delay: [DELAY_MS], + iterations: [ITERATIONS], + alternate: [ALTERNATE_BOOL]{{#if trigger.flags.hasEffectId}}, + effectId: '[UNIQUE_EFFECT_ID]'{{/if}} + }, + // additional effects targeting other elements can be added here + ] +} +``` + +### Variables + +- `[SOURCE_KEY]` — {{var.SOURCE_KEY}} +- `[TARGET_KEY]` — {{var.TARGET_KEY}} +- `[TRIGGER_TYPE]` — `triggerType` on the effect. One of: +{{#each trigger.triggerTypes as tt}} - `'{{tt.key}}'` — {{tt.value.full}} +{{/each}}- `[KEYFRAMES]` — {{var.KEYFRAMES}} +- `[EFFECT_NAME]` — {{var.EFFECT_NAME}} +- `[NAMED_EFFECT_DEFINITION]` — {{var.NAMED_EFFECT_DEFINITION}} +- `[FILL_MODE]` — {{var.FILL_MODE}} +{{#if trigger.flags.hasReversed}}- `[INITIAL_REVERSED_BOOL]` — optional. `true` to start in the finished state so the entire effect is reversed. +{{/if}}- `[DURATION_MS]` — {{var.DURATION_MS}} +- `[EASING_FUNCTION]` — {{var.EASING_FUNCTION}} +- `[DELAY_MS]` — {{var.DELAY_MS}} +- `[ITERATIONS]` — {{var.ITERATIONS}} +- `[ALTERNATE_BOOL]` — {{var.ALTERNATE_BOOL}} +{{#if trigger.flags.hasEffectId}}- `[UNIQUE_EFFECT_ID]` — {{var.UNIQUE_EFFECT_ID}} +{{/if}} +--- + +## Rule 2: transition / transitionProperties (StateEffect) + +Use `transition` or `transitionProperties` when the {{trigger.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`. + +```typescript +{ + key: '[SOURCE_KEY]', + trigger: '{{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 + ] + }, + // additional effects targeting other elements can be added here + ] +} +``` + +### Variables + +- `[SOURCE_KEY]` / `[TARGET_KEY]` — same as Rule 1. +- `[STATE_ACTION]` — `stateAction` on the effect. One of: +{{#each trigger.stateActions as sa}} - `'{{sa.key}}'` — {{sa.value.full}} +{{/each}}- `[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) + +Use `customEffect` when you need imperative control over the animation (e.g. counters, canvas drawing, custom DOM manipulation{{trigger.prose.customEffectExamples}}). The callback receives the target element and a `progress` value (0–1) driven by the animation timeline. + +```typescript +{ + key: '[SOURCE_KEY]', + trigger: '{{trigger.name}}', + effects: [ + { + key: '[TARGET_KEY]', + triggerType: '[TRIGGER_TYPE]', + customEffect: [CUSTOM_EFFECT_CALLBACK], + duration: [DURATION_MS], + easing: '[EASING_FUNCTION]' + }, + // additional effects targeting other elements can be added here + ] +} +``` + +### Variables + +- `[SOURCE_KEY]` / `[TARGET_KEY]` / `[TRIGGER_TYPE]` — same as Rule 1. +- `[CUSTOM_EFFECT_CALLBACK]` — {{var.CUSTOM_EFFECT_CALLBACK}} +- `[DURATION_MS]` — animation duration in milliseconds. +- `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`. + +--- + +## Rule 4: Sequences + +Use sequences when a {{trigger.name}} should sync/stagger animations across multiple elements. Set `triggerType` on the sequence config to control playback behavior. + +```typescript +{ + key: '[SOURCE_KEY]', + trigger: '{{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]` — easing curve for the offset staggering distribution.{{trigger.prose.offsetEasingSuffix}} Defaults to `'linear'`. +- `[EFFECT_DEFINITION]` — a definition of or a reference to a time-based animation effect. diff --git a/packages/interact/_build/templates/triggers/pointermove.md b/packages/interact/_build/templates/triggers/pointermove.md new file mode 100644 index 00000000..4cf307b8 --- /dev/null +++ b/packages/interact/_build/templates/triggers/pointermove.md @@ -0,0 +1,253 @@ +# PointerMove Trigger Rules for {{meta.packageName}} + +These rules help generate pointer-driven interactions using `{{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'` + +{{> pitfalls#hit-area-trigger}} + +--- + +## PointerMoveParams + +`params` object for `pointerMove` interactions: + +```typescript +type PointerMoveParams = {{computed.paramsType}}; +``` + +### 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 + +{{> progress-type#detailed}} + +--- + +## 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 `{{meta.presetsPackage}}` that handle 2D mouse tracking internally. Mouse presets are preferred over `keyframeEffect` for 2D effects. Available mouse presets: {{#each effects.presets.mouse as preset}}`{{preset}}`{{#if !last}}, {{/if}}{{/each}}. +{{#if trigger.flags.showMultipleEffectsNote}} +{{> multiple-effects-note#pointerMove}} +{{/if}} + +```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]` — {{var.HIT_AREA}} +- `[NAMED_EFFECT_TYPE]` — a registered effect name, or a preset from `{{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]` — {{var.CENTERED_TO_TARGET}} +- `[TRANSITION_DURATION_MS]` — {{var.TRANSITION_DURATION_MS}} +- `[TRANSITION_EASING]` — {{var.TRANSITION_EASING}} + +--- + +## 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]` / `[HIT_AREA]` — same as Rule 1. +- `[AXIS]` — `'x'` (horizontal) or `'y'` (vertical). Defaults to `'y'` when omitted. +- `[EFFECT_NAME]` — {{var.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]` / `[TRANSITION_DURATION_MS]` / `[TRANSITION_EASING]` / `[UNIQUE_EFFECT_ID]` — same as Rule 1. + +--- + +## 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]` / `[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]` / `[TRANSITION_EASING]` — same as Rule 1. + +--- + +## 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]` / `[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]` / `[TRANSITION_DURATION_MS]` / `[TRANSITION_EASING]` — same as Rule 1. diff --git a/packages/interact/_build/templates/triggers/viewenter.md b/packages/interact/_build/templates/triggers/viewenter.md new file mode 100644 index 00000000..bbfa660c --- /dev/null +++ b/packages/interact/_build/templates/triggers/viewenter.md @@ -0,0 +1,199 @@ +# {{trigger.Name}} Trigger Rules for {{meta.packageName}} + +This document contains rules for generating interactions that respond to elements entering the viewport using the `{{meta.packageName}}`. ViewEnter triggers use IntersectionObserver to detect when elements become visible and are ideal for entrance animations, content reveals, and lazy-loading effects. + +--- +{{#each trigger.pitfalls as pitfall}} +{{> pitfalls#same-element-viewenter-short}}{{/each}} + +## 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) + +**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: + +```typescript +import { generate } from '{{meta.packageName}}'; + +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); +``` + +{{> fouc#code-inject}} + +### Step 2: Mark elements with `initial` + +{{> fouc#code-web-rules}} + +{{> fouc#code-react-rules}} + +{{> fouc#code-vanilla-rules}} + +### 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`). +{{#if trigger.flags.showMultipleEffectsNote}} +{{> multiple-effects-note#viewEnter}} +{{/if}} +```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]` — {{var.SOURCE_KEY}} +- `[TARGET_KEY]` — {{var.TARGET_KEY}} +- `[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: +{{#each trigger.triggerTypes as tt}} - `'{{tt.key}}'`{{#if tt.value.default}} (default){{/if}} — {{tt.value.full}} +{{/each}}- `[VISIBILITY_THRESHOLD]` — {{var.VISIBILITY_THRESHOLD}} +- `[VIEWPORT_INSETS]` — {{var.VIEWPORT_INSETS}} +- `[KEYFRAMES]` — {{var.KEYFRAMES}} +- `[EFFECT_NAME]` — {{var.EFFECT_NAME}} +- `[NAMED_EFFECT_DEFINITION]` — {{var.NAMED_EFFECT_DEFINITION}} +- `[FILL_MODE]` — {{var.FILL_MODE}} +- `[DURATION_MS]` — {{var.DURATION_MS}} +- `[EASING_FUNCTION]` — {{var.EASING_FUNCTION}} +- `[DELAY_MS]` — {{var.DELAY_MS}} +- `[ITERATIONS]` — {{var.ITERATIONS}} +- `[ALTERNATE_BOOL]` — {{var.ALTERNATE_BOOL}} +- `[UNIQUE_EFFECT_ID]` — {{var.UNIQUE_EFFECT_ID}} + +--- + +## 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]` — {{var.CUSTOM_EFFECT_CALLBACK}} + +--- + +## 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/_build/templates/triggers/viewprogress.md b/packages/interact/_build/templates/triggers/viewprogress.md new file mode 100644 index 00000000..6759ff4f --- /dev/null +++ b/packages/interact/_build/templates/triggers/viewprogress.md @@ -0,0 +1,139 @@ +# ViewProgress Trigger Rules for {{meta.packageName}} + +These rules help generate scroll-driven interactions using `{{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. + +{{> 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. +{{#if trigger.flags.showMultipleEffectsNote}} +{{> multiple-effects-note#viewProgress}} +{{/if}} + +### 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 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: +{{#each effects.ranges as range}} + - `'{{range.key}}'` — {{range.value}} +{{/each}} +- `[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. String identifier used by `animationEnd` triggers for chaining, and by sequences for referencing effects from the top-level `effects` map. + +--- + +## 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 the target element and `progress` from 0 to 1. +- `[RANGE_NAME]` / `[START_PERCENTAGE]` / `[END_PERCENTAGE]` / `[EASING_FUNCTION]` / `[UNIQUE_EFFECT_ID]` — same as Rule 1. + +--- + +## 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. +- `[EASING_FUNCTION]` / `[UNIQUE_EFFECT_ID]` — same as Rule 1. diff --git a/packages/interact/package.json b/packages/interact/package.json index 1cc31494..07f20ff9 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 _build/assemble.mjs", "build:types": "tsc -p tsconfig.build.json", "lint": "tsc --noEmit", "test": "vitest run", diff --git a/packages/interact/rules/click.md b/packages/interact/rules/click.md index 83a2e0d8..446f7639 100644 --- a/packages/interact/rules/click.md +++ b/packages/interact/rules/click.md @@ -61,8 +61,8 @@ 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. -- `[FILL_MODE]` - optional. Always `'both'` with `triggerType: 'alternate'` or `'repeat'`, otherwise depends on the effect. +- `[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. - `[EASING_FUNCTION]` — CSS easing string, or named easing from `@wix/motion`. @@ -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`. @@ -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 ] } ``` @@ -153,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 `(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`. @@ -173,7 +175,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 +188,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..2ae1769b 100644 --- a/packages/interact/rules/full-lean.md +++ b/packages/interact/rules/full-lean.md @@ -32,10 +32,9 @@ Declarative configuration-driven interaction library. Binds animations to trigge Each item here is CRITICAL — ignoring any of them will break animations. +- **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**: 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**: 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. @@ -215,7 +214,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: @@ -238,21 +237,23 @@ 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 @@ -347,7 +348,7 @@ Each effect applies a visual change to a target element. An effect is either inl **`easing` guidance:** from `@wix/motion` (in addition to standard CSS 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'`, or any `'cubic-bezier(...)'` / `'linear(...)'` string. +'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', or any `'cubic-bezier(...)'` / `'linear(...)'` string. ### Time-based Effect @@ -393,19 +394,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 | +| 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: @@ -464,12 +465,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. @@ -545,7 +546,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/hover.md b/packages/interact/rules/hover.md index f2f5d3b6..a85945ac 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. --- @@ -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`. @@ -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/integration.md b/packages/interact/rules/integration.md index e5a75121..2afe17ab 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. @@ -248,9 +249,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' }], }, ], @@ -290,11 +291,11 @@ const css = generate(config); ``` -**Web:** +**Web (Custom Elements):** ```html -
...
+
...
``` diff --git a/packages/interact/rules/pointermove.md b/packages/interact/rules/pointermove.md index d471ea29..aedfb0e3 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`. --- @@ -100,10 +100,11 @@ 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. + ```typescript { key: '[SOURCE_KEY]', @@ -172,15 +173,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 +228,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 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. -- `[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 +266,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/viewenter.md b/packages/interact/rules/viewenter.md index e6cf1171..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. --- @@ -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/rules/viewprogress.md b/packages/interact/rules/viewprogress.md index a5758e30..232b4697 100644 --- a/packages/interact/rules/viewprogress.md +++ b/packages/interact/rules/viewprogress.md @@ -20,6 +20,7 @@ These rules help generate scroll-driven interactions using `@wix/interact`. View **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 @@ -50,19 +51,26 @@ 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. + - `'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. +- `[EASING_FUNCTION]` — CSS easing string or named easing from `@wix/motion`. Typically `'linear'` for scrolling 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. --- @@ -82,7 +90,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]' }, @@ -94,10 +102,8 @@ 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. -- `[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. +- `[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]` / `[EASING_FUNCTION]` / `[UNIQUE_EFFECT_ID]` — same as Rule 1. --- @@ -140,5 +146,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.