diff --git a/packages/interact-validate/README.md b/packages/interact-validate/README.md index faa69957..fb916437 100644 --- a/packages/interact-validate/README.md +++ b/packages/interact-validate/README.md @@ -152,7 +152,6 @@ Every issue is `'error'` or `'warning'`. `valid` is `true` **iff** no `'error'` | `VALID_MEDIA_QUERIES` | `INVALID_MEDIA_QUERY` | error | | `SAME_ELEMENT_RETRIGGER` | `SAME_ELEMENT_RETRIGGER` | warning | | `HIT_AREA_SHIFT` | `HIT_AREA_SHIFT` | warning | -| `SCROLL_RANGE` | `SCROLL_PRESET_MISSING_RANGE`, `SCROLL_PRESET_BAD_RANGE` | warning | | `ANIMATION_END_GRAPH` | `ANIMATION_END_SELF_REFERENCE`, `ANIMATION_END_CYCLE` | warning | | `ELEMENT_SELECTION` | `LIST_ITEM_SELECTOR_WITHOUT_CONTAINER`, `REDUNDANT_SELECTOR_WITH_LIST_ITEM` | warning | | `STATE_EFFECT` | `EMPTY_STYLE_PROPERTIES`, `STATE_REMOVE_WITHOUT_EFFECT_ID` | warning | @@ -214,8 +213,6 @@ These encode statically-detectable authoring pitfalls from the trigger rule file | `DUPLICATE_KEYFRAME_NAME` | A `keyframeEffect.name` is reused across effects. | `UNIQUE_DEFINITION_IDS` | | `SAME_ELEMENT_RETRIGGER` | `viewEnter` with a non-`once` `triggerType` on the same source+target element. | `SAME_ELEMENT_RETRIGGER` | | `HIT_AREA_SHIFT` | `hover`/`pointerMove` `keyframeEffect` with a `translate`/`scale`/`matrix` transform on the same source+target element. | `HIT_AREA_SHIFT` | -| `SCROLL_PRESET_MISSING_RANGE` | A `*Scroll` `namedEffect` on `viewProgress` omits `range`. | `SCROLL_RANGE` | -| `SCROLL_PRESET_BAD_RANGE` | A scroll preset `range` is not `'in'`/`'out'`/`'continuous'`. | `SCROLL_RANGE` | | `ANIMATION_END_SELF_REFERENCE` | An `animationEnd` interaction waits on an effect it also produces (never starts). | `ANIMATION_END_GRAPH` | | `LIST_ITEM_SELECTOR_WITHOUT_CONTAINER` | `listItemSelector` present without `listContainer` (inert). | `ELEMENT_SELECTION` | | `REDUNDANT_SELECTOR_WITH_LIST_ITEM` | `selector` ignored when `listContainer` + `listItemSelector` are both present. | `ELEMENT_SELECTION` | diff --git a/packages/interact-validate/src/errors.ts b/packages/interact-validate/src/errors.ts index 2b144003..a91199ae 100644 --- a/packages/interact-validate/src/errors.ts +++ b/packages/interact-validate/src/errors.ts @@ -20,8 +20,6 @@ const RULE_CODE_MAP: Record = { // Rule-derived semantic checks (see packages/interact/rules/*.md). SAME_ELEMENT_RETRIGGER: 'SAME_ELEMENT_RETRIGGER', HIT_AREA_SHIFT: 'HIT_AREA_SHIFT', - SCROLL_PRESET_MISSING_RANGE: 'SCROLL_RANGE', - SCROLL_PRESET_BAD_RANGE: 'SCROLL_RANGE', ANIMATION_END_SELF_REFERENCE: 'ANIMATION_END_GRAPH', ANIMATION_END_CYCLE: 'ANIMATION_END_GRAPH', LIST_ITEM_SELECTOR_WITHOUT_CONTAINER: 'ELEMENT_SELECTION', @@ -34,6 +32,8 @@ const RULE_CODE_MAP: Record = { INVALID_CSS_PROPERTY_NAME: 'CSS_PROPERTY_NAME', INVALID_INSET: 'VIEW_INSET', FUNCTION_OFFSET_EASING: 'OFFSET_EASING', + NAMED_EFFECT_UNKNOWN_PARAM: 'NAMED_EFFECT_PARAMS', + NAMED_EFFECT_INVALID_PARAM: 'NAMED_EFFECT_PARAMS', }; export function finalize( diff --git a/packages/interact-validate/src/semantic/collectSemanticWarnings.ts b/packages/interact-validate/src/semantic/collectSemanticWarnings.ts index f969c94d..adf4e88c 100644 --- a/packages/interact-validate/src/semantic/collectSemanticWarnings.ts +++ b/packages/interact-validate/src/semantic/collectSemanticWarnings.ts @@ -12,13 +12,10 @@ import { checkRedundantSelector, checkPointerAxisIgnored, } from './ignored'; -import { - checkScrollPresetRange, - checkEmptyStyleProperties, - checkStateRemoveWithoutEffectId, -} from './partialData'; +import { checkEmptyStyleProperties, checkStateRemoveWithoutEffectId } from './partialData'; import { checkRecommendedFill } from './recommendedPatterns'; import { findAnimationEndWarnings } from './animationEndGraph'; +import { checkNamedEffectParams } from './namedEffectParams'; // Single traversal of top-level registry effects/sequences and per-interaction // effects/sequences, supplying each node's `path`, whether it is a top-level @@ -71,7 +68,6 @@ export function collectSemanticWarnings(config: AnyConfig): SemanticIssue[] { : effect; warnings.push(...checkSameElementRetrigger(path, resolvedEffect, owner)); warnings.push(...checkHitAreaShift(path, resolvedEffect, owner)); - warnings.push(...checkScrollPresetRange(path, resolvedEffect, owner)); warnings.push(...checkListItemSelectorWithoutContainer(path, resolvedEffect)); warnings.push(...checkRedundantSelector(path, resolvedEffect)); warnings.push(...checkEmptyStyleProperties(path, effect)); @@ -79,6 +75,7 @@ export function collectSemanticWarnings(config: AnyConfig): SemanticIssue[] { warnings.push(...checkRecommendedFill(path, resolvedEffect, owner)); warnings.push(...checkPointerAxisIgnored(path, resolvedEffect, owner)); warnings.push(...checkCSSPropertyNames(path, effect)); + warnings.push(...checkNamedEffectParams(path, effect)); }, onSequence: (path, sequence, isTopLevel, owner) => { const { sequenceId } = sequence; diff --git a/packages/interact-validate/src/semantic/namedEffectParams.ts b/packages/interact-validate/src/semantic/namedEffectParams.ts new file mode 100644 index 00000000..b4ecfafc --- /dev/null +++ b/packages/interact-validate/src/semantic/namedEffectParams.ts @@ -0,0 +1,99 @@ +import { Interact } from '@wix/interact'; +import type { AnyEffect, EffectParamSchema, EffectSchema, Path, SemanticIssue } from '../types'; + +const LENGTH_REGEX = /^-?\d*\.?\d+(px|%|em|rem|vw|vh|vmin|vmax|ch|ex|cm|mm|in|pt|pc)?$/i; +const ANGLE_REGEX = /^-?\d*\.?\d+(deg|rad|grad|turn)?$/i; +const CALC_REGEX = /^calc\(.+\)$/i; +const SIDES = ['top', 'right', 'bottom', 'left']; + +const isNumber = (value: unknown): value is number => + typeof value === 'number' && Number.isFinite(value); +const matches = (value: unknown, regex: RegExp) => + typeof value === 'string' && (regex.test(value.trim()) || CALC_REGEX.test(value.trim())); +const isLengthObject = (value: unknown): value is { value: unknown; unit: string } => + typeof value === 'object' && + value !== null && + typeof (value as { unit?: unknown }).unit === 'string'; + +// [is valid, expected value description] per param type - unknown types are not validated +const PARAM_TYPES: Record< + string, + { + isValid: (value: unknown, param: EffectParamSchema) => boolean; + expected: (param: EffectParamSchema) => string; + } +> = { + number: { + isValid: (value, { min = -Infinity, max = Infinity, int }) => + isNumber(value) && (!int || Number.isInteger(value)) && value >= min && value <= max, + expected: ({ min, max, int }) => + `${int ? 'an integer' : 'a number'}${min !== undefined ? ` >= ${min}` : ''}${max !== undefined ? ` <= ${max}` : ''}`, + }, + bool: { + isValid: (value) => typeof value === 'boolean', + expected: () => 'a boolean', + }, + enum: { + isValid: (value, { values = [] }) => values.includes(value), + expected: ({ values = [] }) => `one of ${values.map((v) => JSON.stringify(v)).join(' | ')}`, + }, + length: { + isValid: (value) => + isNumber(value) || + matches(value, LENGTH_REGEX) || + (isLengthObject(value) && (isNumber(value.value) || matches(value.value, LENGTH_REGEX))), + expected: () => "a length (number, '', calc() or { value, unit })", + }, + lengthObject: { + isValid: (value) => isLengthObject(value) && isNumber(value.value), + expected: () => 'a length object ({ value: number, unit })', + }, + angle: { + isValid: (value) => + isNumber(value) || + matches(value, ANGLE_REGEX) || + (typeof value === 'string' && SIDES.includes(value.trim())), + expected: () => + `an angle (number, 'deg', calc() or ${SIDES.map((s) => `'${s}'`).join(' | ')})`, + }, +}; + +const issue = ( + path: Path, + domainCode: string, + message: string, + severity: SemanticIssue['severity'], +) => ({ code: 'custom', path, message, params: { domainCode }, severity }) as SemanticIssue; + +// the namedEffect's params against the schema of the effect registered on Interact under its type +export function checkNamedEffectParams(path: Path, effect: AnyEffect): SemanticIssue[] { + const { type, ...params } = effect.namedEffect ?? {}; + const module = typeof type === 'string' ? Interact.getRegisteredEffect?.(type, false) : null; + const schema = (module as { schema?: EffectSchema } | null | undefined)?.schema; + if (!schema) return []; + + return Object.entries(params).flatMap(([key, value]) => { + const paramPath = [...path, 'namedEffect', key]; + const param = schema[key]; + if (!param) { + return [ + issue( + paramPath, + 'NAMED_EFFECT_UNKNOWN_PARAM', + `'${key}' is not a parameter of ${type} (expected one of ${Object.keys(schema).join(', ')}).`, + 'warning', + ), + ]; + } + const paramType = PARAM_TYPES[param.type]; + if (value === undefined || !paramType || paramType.isValid(value, param)) return []; + return [ + issue( + paramPath, + 'NAMED_EFFECT_INVALID_PARAM', + `${type} \`${key}\` must be ${paramType.expected(param)}; got ${JSON.stringify(value)}.`, + 'error', + ), + ]; + }); +} diff --git a/packages/interact-validate/src/semantic/partialData.ts b/packages/interact-validate/src/semantic/partialData.ts index 3a96fe5a..4f7c66b0 100644 --- a/packages/interact-validate/src/semantic/partialData.ts +++ b/packages/interact-validate/src/semantic/partialData.ts @@ -1,43 +1,4 @@ -import type { Path, SemanticIssue, AnyEffect, AnyInteraction } from '../types'; - -const SCROLL_RANGE_VALUES = ['in', 'out', 'continuous']; - -// Scroll presets (the `*Scroll` motion-presets) all end with `Scroll`; no other -// preset category does. Used to flag a missing/invalid `range` on `viewProgress`. -function isScrollPresetType(type: unknown): boolean { - return typeof type === 'string' && /Scroll$/.test(type); -} - -// `*Scroll` namedEffect on viewProgress without a valid `range` -export function checkScrollPresetRange( - path: Path, - effect: AnyEffect, - owner?: AnyInteraction, -): SemanticIssue[] { - if (!owner || owner.trigger !== 'viewProgress') return []; - const named = effect.namedEffect; - if (!named || !isScrollPresetType(named.type)) return []; - if (named.range === undefined) { - return [ - { - code: 'custom', - params: { domainCode: 'SCROLL_PRESET_MISSING_RANGE' }, - path: [...path, 'namedEffect', 'range'], - message: `Scroll preset '${named.type}' on viewProgress requires \`range: 'in' | 'out' | 'continuous'\` (prefer 'continuous').`, - }, - ]; - } else if (typeof named.range !== 'string' || !SCROLL_RANGE_VALUES.includes(named.range)) { - return [ - { - code: 'custom', - params: { domainCode: 'SCROLL_PRESET_BAD_RANGE' }, - path: [...path, 'namedEffect', 'range'], - message: `Scroll preset \`range\` must be 'in', 'out', or 'continuous' (prefer 'continuous'); got ${JSON.stringify(named.range)}.`, - }, - ]; - } - return []; -} +import type { Path, SemanticIssue, AnyEffect } from '../types'; // state effect that toggles nothing (empty style arrays) export function checkEmptyStyleProperties(path: Path, effect: AnyEffect): SemanticIssue[] { diff --git a/packages/interact-validate/src/types.ts b/packages/interact-validate/src/types.ts index 6921d0dd..86c82fdb 100644 --- a/packages/interact-validate/src/types.ts +++ b/packages/interact-validate/src/types.ts @@ -25,6 +25,17 @@ export type ValidationResult = { errors: ValidationError[]; }; +export type EffectParamSchema = { + type: string; + values?: readonly unknown[]; + min?: number; + max?: number; + int?: boolean; + default?: unknown; +}; + +export type EffectSchema = Record; + export type ValidateOptions = { strict?: boolean; max?: number; diff --git a/packages/interact-validate/test/rules/namedEffectParams.spec.ts b/packages/interact-validate/test/rules/namedEffectParams.spec.ts new file mode 100644 index 00000000..3f53b6e0 --- /dev/null +++ b/packages/interact-validate/test/rules/namedEffectParams.spec.ts @@ -0,0 +1,120 @@ +import { describe, expect, it } from 'vitest'; +import { Interact } from '@wix/interact'; +import { validateInteractConfig } from '../../src'; + +const Move = { + schema: { + from: { type: 'enum', values: ['left', 'right'], default: 'left' }, + angle: { type: 'angle', default: 0 }, + travel: { type: 'length', default: { value: 10, unit: 'px' } }, + depth: { type: 'lengthObject', default: { value: 10, unit: 'px' } }, + count: { type: 'number', int: true, min: 1, default: 1 }, + scale: { type: 'number', min: 0, max: 1, default: 0.5 }, + fade: { type: 'bool', default: true }, + }, +}; + +Interact.registerEffects({ Move, Unschemed: {} } as any); + +const configWith = (namedEffect: Record) => ({ + interactions: [ + { + key: 'el', + trigger: 'viewEnter', + effects: [ + { namedEffect: { type: 'Move', ...namedEffect }, duration: 400, fill: 'backwards' }, + ], + }, + ], +}); + +const paramErrors = (namedEffect: Record) => + validateInteractConfig(configWith(namedEffect)).errors.filter((e) => + e.code.startsWith('NAMED_EFFECT_'), + ); + +describe('namedEffectParams', () => { + it('accepts valid values of every param type', () => { + expect( + paramErrors({ + from: 'right', + angle: '45deg', + travel: { value: 20, unit: '%' }, + depth: { value: -5, unit: 'em' }, + count: 3, + scale: 1, + fade: false, + }), + ).toEqual([]); + expect(paramErrors({ angle: 'top', travel: 'calc(100% - 10px)' })).toEqual([]); + expect(paramErrors({ angle: 90, travel: 12 })).toEqual([]); + expect(paramErrors({ angle: '-0.5turn', travel: '2rem' })).toEqual([]); + }); + + it('errors (NAMED_EFFECT_INVALID_PARAM) on values that do not match the schema', () => { + expect(validateInteractConfig(configWith({ scale: 2 })).valid).toBe(false); + const errors = paramErrors({ + from: 'top', + angle: 'sideways', + travel: '10 apples', + scale: 2, + fade: 'yes', + depth: '10px', + count: 1.5, + }); + expect(errors.map((e) => e.path.at(-1)).sort()).toEqual([ + 'angle', + 'count', + 'depth', + 'fade', + 'from', + 'scale', + 'travel', + ]); + expect(errors.every((e) => e.code === 'NAMED_EFFECT_INVALID_PARAM')).toBe(true); + expect(errors.every((e) => e.severity === 'error')).toBe(true); + expect(errors.find((e) => e.path.at(-1) === 'from')?.path).toEqual([ + 'interactions', + 0, + 'effects', + 0, + 'namedEffect', + 'from', + ]); + expect(paramErrors({ travel: { value: 'far', unit: 'px' } })).toHaveLength(1); + expect(paramErrors({ depth: { value: 'calc(1px + 2px)', unit: 'px' } })).toHaveLength(1); + expect(paramErrors({ depth: 10 })).toHaveLength(1); + }); + + it('warns (NAMED_EFFECT_UNKNOWN_PARAM) on params the schema does not declare', () => { + const [error] = paramErrors({ direction: 'left' }); + expect(error.code).toBe('NAMED_EFFECT_UNKNOWN_PARAM'); + expect(error.severity).toBe('warning'); + expect(error.path).toEqual(['interactions', 0, 'effects', 0, 'namedEffect', 'direction']); + }); + + it('skips effects without a schema, or not registered', () => { + for (const type of ['Unschemed', 'NotRegistered']) { + const config = configWith({ anything: 1 }); + config.interactions[0].effects[0].namedEffect.type = type; + const errors = validateInteractConfig(config).errors; + expect(errors.filter((e) => e.code.startsWith('NAMED_EFFECT_'))).toEqual([]); + } + }); + + it('validates top-level effect definitions', () => { + const result = validateInteractConfig({ + effects: { move: { namedEffect: { type: 'Move', scale: -1 } } }, + interactions: [{ key: 'el', trigger: 'viewEnter', effects: [{ effectId: 'move' }] }], + }); + const errors = result.errors.filter((e) => e.code === 'NAMED_EFFECT_INVALID_PARAM'); + expect(errors.map((e) => e.path)).toEqual([['effects', 'move', 'namedEffect', 'scale']]); + }); + + it('can be turned off with severityOverrides', () => { + const result = validateInteractConfig(configWith({ scale: 5, direction: 'left' }), { + severityOverrides: { NAMED_EFFECT_PARAMS: 'off' }, + }); + expect(result.errors.filter((e) => e.code.startsWith('NAMED_EFFECT_'))).toEqual([]); + }); +}); diff --git a/packages/interact-validate/test/rules/scrollPresetRange.spec.ts b/packages/interact-validate/test/rules/scrollPresetRange.spec.ts deleted file mode 100644 index 91cb46c1..00000000 --- a/packages/interact-validate/test/rules/scrollPresetRange.spec.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { describe, expect, it } from 'vitest'; -import { validateInteractConfig } from '../../src'; - -// A3 — `*Scroll` namedEffect presets used with viewProgress MUST declare -// `range: 'in' | 'out' | 'continuous'` (prefer 'continuous'). -// Source: viewprogress.md Rule 1 + full-lean.md Animation-Payloads CRITICAL. - -describe('scrollPresetRange', () => { - it('warns (SCROLL_PRESET_MISSING_RANGE) when a scroll preset omits range', () => { - const result = validateInteractConfig({ - interactions: [ - { - key: 'el', - trigger: 'viewProgress', - effects: [ - { namedEffect: { type: 'FadeScroll' }, rangeStart: { name: 'cover' }, fill: 'both' }, - ], - }, - ], - }); - const err = result.errors.find((e) => e.code === 'SCROLL_PRESET_MISSING_RANGE'); - expect(err).toBeDefined(); - expect(err?.severity).toBe('warning'); - expect(err?.path).toEqual(['interactions', 0, 'effects', 0, 'namedEffect', 'range']); - expect(result.valid).toBe(true); - }); - - it('warns (SCROLL_PRESET_BAD_RANGE) when range is not one of in/out/continuous', () => { - const result = validateInteractConfig({ - interactions: [ - { - key: 'el', - trigger: 'viewProgress', - effects: [{ namedEffect: { type: 'ParallaxScroll', range: 'sideways' }, fill: 'both' }], - }, - ], - }); - const err = result.errors.find((e) => e.code === 'SCROLL_PRESET_BAD_RANGE'); - expect(err).toBeDefined(); - expect(err?.path).toEqual(['interactions', 0, 'effects', 0, 'namedEffect', 'range']); - }); - - describe('no warning for the documented valid patterns', () => { - it('does not warn when a scroll preset declares range: continuous', () => { - const result = validateInteractConfig({ - interactions: [ - { - key: 'el', - trigger: 'viewProgress', - effects: [{ namedEffect: { type: 'FadeScroll', range: 'continuous' }, fill: 'both' }], - }, - ], - }); - const scrollCodes = ['SCROLL_PRESET_MISSING_RANGE', 'SCROLL_PRESET_BAD_RANGE']; - expect(result.errors.filter((e) => scrollCodes.includes(e.code))).toHaveLength(0); - }); - - it('does not warn for a non-scroll preset on viewProgress', () => { - const result = validateInteractConfig({ - interactions: [ - { - key: 'el', - trigger: 'viewProgress', - effects: [ - { namedEffect: { type: 'FadeIn' }, rangeStart: { name: 'cover' }, fill: 'both' }, - ], - }, - ], - }); - const scrollCodes = ['SCROLL_PRESET_MISSING_RANGE', 'SCROLL_PRESET_BAD_RANGE']; - expect(result.errors.filter((e) => scrollCodes.includes(e.code))).toHaveLength(0); - }); - }); -}); diff --git a/packages/interact/rules/validate.md b/packages/interact/rules/validate.md index cab2129d..68119e2d 100644 --- a/packages/interact/rules/validate.md +++ b/packages/interact/rules/validate.md @@ -134,7 +134,6 @@ Severity is one of `'error' | 'warning'`. There are exactly two levers: | `VALID_MEDIA_QUERIES` | `INVALID_MEDIA_QUERY` | error | | `SAME_ELEMENT_RETRIGGER` | `SAME_ELEMENT_RETRIGGER` | warning | | `HIT_AREA_SHIFT` | `HIT_AREA_SHIFT` | warning | -| `SCROLL_RANGE` | `SCROLL_PRESET_MISSING_RANGE`, `SCROLL_PRESET_BAD_RANGE` | warning | | `ANIMATION_END_GRAPH` | `ANIMATION_END_SELF_REFERENCE`, `ANIMATION_END_CYCLE` | warning | | `ELEMENT_SELECTION` | `LIST_ITEM_SELECTOR_WITHOUT_CONTAINER`, `REDUNDANT_SELECTOR_WITH_LIST_ITEM` | warning | | `STATE_EFFECT` | `EMPTY_STYLE_PROPERTIES`, `STATE_REMOVE_WITHOUT_EFFECT_ID` | warning | @@ -143,6 +142,7 @@ Severity is one of `'error' | 'warning'`. There are exactly two levers: | `CSS_PROPERTY_NAME` | `INVALID_CSS_PROPERTY_NAME` | warning | | `VIEW_INSET` | `INVALID_INSET` | warning | | `OFFSET_EASING` | `FUNCTION_OFFSET_EASING` | warning | +| `NAMED_EFFECT_PARAMS` | `NAMED_EFFECT_INVALID_PARAM` (error), `NAMED_EFFECT_UNKNOWN_PARAM` | warning | For each category, set `'off'` to drop those issues entirely, `'warning'` / `'error'` to set their severity: @@ -226,8 +226,6 @@ Statically-detectable authoring pitfalls lifted from the trigger rule files. Eac | -------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ | | `SAME_ELEMENT_RETRIGGER` | `viewEnter` with `triggerType` `repeat`/`alternate`/`state` on the same source+target element (re-trigger loop). | `SAME_ELEMENT_RETRIGGER` | | `HIT_AREA_SHIFT` | `hover`/`click`/`interest`/`activate`/`pointerMove` `keyframeEffect` with a `translate`/`scale`/`matrix` transform on the same source+target element. | `HIT_AREA_SHIFT` | -| `SCROLL_PRESET_MISSING_RANGE` | A `*Scroll` `namedEffect` on `viewProgress` omits `range`. | `SCROLL_RANGE` | -| `SCROLL_PRESET_BAD_RANGE` | A scroll preset `range` is not `'in'`/`'out'`/`'continuous'`. | `SCROLL_RANGE` | | `ANIMATION_END_SELF_REFERENCE` | An `animationEnd` interaction waits for an effect it also produces → it can never start. | `ANIMATION_END_GRAPH` | | `LIST_ITEM_SELECTOR_WITHOUT_CONTAINER` | `listItemSelector` present without `listContainer` (inert) — on an interaction or an effect. | `ELEMENT_SELECTION` | | `REDUNDANT_SELECTOR_WITH_LIST_ITEM` | `selector` is ignored when both `listContainer` and `listItemSelector` are present. | `ELEMENT_SELECTION` | @@ -239,6 +237,8 @@ Statically-detectable authoring pitfalls lifted from the trigger rule files. Eac | `INVALID_CSS_PROPERTY_NAME` | A keyframe or state-effect property name is neither camelCase nor kebab-case (both casings are accepted; this one cannot be normalized). | `CSS_PROPERTY_NAME` | | `INVALID_INSET` | `viewEnter` `params.inset` is not 1–4 whitespace-separated CSS lengths/percentages. | `VIEW_INSET` | | `FUNCTION_OFFSET_EASING` | A sequence's `offsetEasing` is a function, so `generate()` omits that sequence from the generated CSS. | `OFFSET_EASING` | +| `NAMED_EFFECT_INVALID_PARAM` | A `namedEffect` param does not match its effect's registered `schema` (type, enum values, min/max, length/angle format). | `NAMED_EFFECT_PARAMS` | +| `NAMED_EFFECT_UNKNOWN_PARAM` | A `namedEffect` param is not declared in its effect's registered `schema` (e.g. a renamed param). | `NAMED_EFFECT_PARAMS` | --- @@ -310,6 +310,6 @@ The validator is **static**. It cannot see the DOM, the browser, or the preset r - Whether a `keyframeEffect`/`namedEffect` actually changes size/position when it cannot be introspected (e.g. `namedEffect` options) — only inline `keyframeEffect` transforms are scanned for `HIT_AREA_SHIFT`. - Reduced-motion alternatives, perspective usage, and other authoring guidance with no static signal. -> **Note:** Several pitfalls that previously had "no static signal" are now flagged as **warnings** (see the rule-derived semantic warnings above): same source+target on `viewEnter` with a non-`once` `triggerType` (`SAME_ELEMENT_RETRIGGER`), hit-area shift from inline `keyframeEffect` transforms on `hover`/`pointerMove` (`HIT_AREA_SHIFT`), scroll presets missing/invalid `range` (`SCROLL_PRESET_*`), and missing `fill: 'both'` (`RECOMMENDED_FILL_BOTH`). These are heuristic and conservative (they skip ambiguous cases to avoid false positives), so the trigger rule files remain the authoritative guidance. +> **Note:** Several pitfalls that previously had "no static signal" are now flagged as **warnings** (see the rule-derived semantic warnings above): same source+target on `viewEnter` with a non-`once` `triggerType` (`SAME_ELEMENT_RETRIGGER`), hit-area shift from inline `keyframeEffect` transforms on `hover`/`pointerMove` (`HIT_AREA_SHIFT`), and missing `fill: 'both'` (`RECOMMENDED_FILL_BOTH`). These are heuristic and conservative (they skip ambiguous cases to avoid false positives), so the trigger rule files remain the authoritative guidance. For the full API, usage recipes, and the same catalogue in package form, see the [`@wix/interact-validate` README](https://github.com/wix/interact/blob/master/packages/interact-validate/README.md). diff --git a/packages/interact/src/core/Interact.ts b/packages/interact/src/core/Interact.ts index 2b393ef3..66564e1a 100644 --- a/packages/interact/src/core/Interact.ts +++ b/packages/interact/src/core/Interact.ts @@ -17,6 +17,7 @@ import { getKeyframeSlots, getSlotKey } from './css'; import TRIGGER_TO_HANDLER_MODULE_MAP from '../handlers'; import { registerEffects, + getRegisteredEffect, getSequence as getMotionSequence, createAnimationGroups, Sequence, @@ -246,6 +247,7 @@ export class Interact { } static registerEffects = registerEffects; + static getRegisteredEffect = getRegisteredEffect; /** * Registers a plugin under a name. When the config carries a plugin field (e.g. `$splitText`) with a matching key diff --git a/skills/interactor/references/validate.md b/skills/interactor/references/validate.md index c2ebb227..603431c4 100644 --- a/skills/interactor/references/validate.md +++ b/skills/interactor/references/validate.md @@ -35,7 +35,7 @@ type ValidationError = { }; ``` -**Options:** `strict` (promote warnings to errors), `max` (truncate issue list), `severityOverrides` (per rule category — see full docs). +**Options:** `strict` (promote warnings to errors), `max` (truncate issue list), `severityOverrides` (per rule category — see full docs). `namedEffect` params are validated against the `schema` of the effects registered with `Interact.registerEffects`, so register them before validating. ---