Skip to content

Commit 8e121a9

Browse files
committed
Merge branch 'master' of github.com:wix/interact into documentation-finalize
2 parents d6128f0 + 84626c8 commit 8e121a9

68 files changed

Lines changed: 2646 additions & 329 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/release-splittext.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,14 @@ jobs:
8080
- name: Build splittext package
8181
run: yarn workspace @wix/splittext build
8282

83+
# @wix/interact is a devDependency used only by the plugin-bridge integration test —
84+
# splittext itself neither builds nor runs against it (note the build step above runs first).
85+
- name: Build motion package (test-only dependency of @wix/interact)
86+
run: yarn workspace @wix/motion build
87+
88+
- name: Build interact package (test-only dependency)
89+
run: yarn workspace @wix/interact build
90+
8391
- name: Test splittext package
8492
run: yarn workspace @wix/splittext test
8593

CHANGELOG.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
1010

1111
## @wix/splittext
1212

13+
### [0.2.0] - unreleased
14+
15+
#### Added
16+
17+
- `@wix/splittext/plugin` entry points: `splitTextPlugin` for `Interact.use()`, and `splitTextStyle` for `generate()` (#275)
18+
- `hideUntilReady`: `splitTextStyle` hides the container until the runtime split sets `data-splittext-ready` (#275)
19+
1320
### [0.1.2] - 2026-07-14
1421

1522
#### Added
@@ -26,6 +33,17 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
2633

2734
## @wix/interact-validate
2835

36+
### [0.2.0] - unreleased
37+
38+
#### Added
39+
40+
- Plugin fields: `$`-prefixed keys on interactions and effects are accepted; every other unknown key is still reported as `SCHEMA_UNRECOGNIZED_KEYS` (#275)
41+
42+
#### Changed
43+
44+
- `KEYFRAME_PROP_NOT_CAMEL_CASE` (rule category `KEYFRAME_STYLE`) is replaced by `INVALID_CSS_PROPERTY_NAME` (rule category `CSS_PROPERTY_NAME`): both camelCase and kebab-case CSS property names are valid input, so only names that are neither are reported, and the check now covers `transition.styleProperties` / `transitionProperties` names in addition to `keyframeEffect` keyframes
45+
- `severityOverrides` keyed on `KEYFRAME_STYLE` are now silently ignored. Rename the key to `CSS_PROPERTY_NAME` to keep an override in effect
46+
2947
### [0.1.2] - 2026-07-29
3048

3149
#### Added
@@ -63,6 +81,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
6381

6482
## @wix/interact
6583

84+
### [2.6.0] - unreleased
85+
86+
#### Added
87+
88+
- Generic plugin bridge: `Interact.use(name, plugin)` registers a plugin, and a `$<name>` field on an interaction or effect (#275)
89+
- `generate()` accepts a `plugins` option — a map of plugin name → build-time style generator (#275)
90+
91+
#### Changed
92+
93+
- `generate(config, options?)`: the second argument now accepts an options bag — `{ useFirstChild?, plugins? }` — exported as the `GenerateOptions` (#275)
94+
- CSS property names may be authored in either camelCase or kebab-case in `transition.styleProperties`, `transitionProperties` and `keyframeEffect.keyframes`; state-effect properties are normalized to kebab-case for the generated CSS (state rules and the `transition:` shorthand) and keyframes to camelCase for WAAPI
95+
96+
#### Fixed
97+
98+
- camelCase property names in `transition.styleProperties` / `transitionProperties` are now normalized to kebab-case
99+
- CSS custom properties in keyframes (e.g. `--fooBar`) are no longer lower-cased when emitted into `@keyframes`
100+
- Vendor-prefixed keyframe properties (e.g. `webkitTextStroke`) now emit a valid CSS property name (`-webkit-text-stroke`)
101+
66102
### [2.5.5] - 2026-07-29
67103

68104
#### Fixed
@@ -275,6 +311,16 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
275311

276312
## @wix/motion
277313

314+
### [Unreleased]
315+
316+
#### Added
317+
318+
- `toCSSPropertyName()`, `toWAAPIPropertyName()` and `normalizeKeyframes()` utilities for converting CSS property names between their CSS and WAAPI forms
319+
320+
#### Changed
321+
322+
- Keyframe property names may be authored in either camelCase or kebab-case and are normalized to WAAPI's camelCase on every animation path — `keyframeEffect`, presets, and effects registered via `registerEffects()`
323+
278324
### [2.1.8] - 2026-07-29
279325

280326
#### Added

apps/demo/package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77
"dev": "vite",
88
"build": "tsc && vite build",
99
"preview": "vite preview",
10-
"lint": "tsc --noEmit",
11-
"test": "vitest run"
10+
"lint": "tsc --noEmit"
1211
},
1312
"dependencies": {
1413
"@wix/interact": "^2.5.5",
14+
"@wix/splittext": "^0.1.0",
1515
"react": "^18.3.1",
1616
"react-dom": "^18.3.1"
1717
},

apps/demo/src/plugins/splitText.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
/**
2+
* Demo-side glue for the reusable `@wix/splittext` Interact plugin.
3+
*
4+
* The plugin itself now lives in `@wix/splittext/plugin`, which ships WITHOUT a dependency on
5+
* `@wix/interact` so the two packages stay decoupled. This file is where the demo — the one place
6+
* that depends on BOTH — "resolves the typing":
7+
*
8+
* 1. It binds the package's structurally-typed callbacks to Interact's real `InteractPlugin` /
9+
* `InteractPluginStyleGenerator` contract. The assignments below double as a compile-time
10+
* check that `@wix/splittext/plugin` stays compatible with `@wix/interact`.
11+
* 2. It ties the `$splitText` config field to {@link SplitTextPluginConfig} via declaration
12+
* merging on `InteractPluginConfigMap`, so demo configs get autocomplete + checking.
13+
*/
14+
import type { InteractPlugin, InteractPluginStyleGenerator } from '@wix/interact';
15+
import {
16+
splitTextPlugin as splitTextPluginImpl,
17+
splitTextStyle as splitTextStyleImpl,
18+
type SplitTextPluginConfig,
19+
} from '@wix/splittext/plugin';
20+
21+
export const splitTextPlugin: InteractPlugin = splitTextPluginImpl;
22+
export const splitTextStyle: InteractPluginStyleGenerator = splitTextStyleImpl;
23+
export type { SplitTextPluginConfig };
24+
25+
declare module '@wix/interact' {
26+
interface InteractPluginConfigMap {
27+
splitText: SplitTextPluginConfig;
28+
}
29+
}

apps/demo/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
"@/*": ["./src/*"],
99
"@wix/interact/web": ["../../packages/interact/src/web"],
1010
"@wix/interact/react": ["../../packages/interact/src/react"],
11-
"@wix/interact": ["../../packages/interact/src/index"]
11+
"@wix/interact": ["../../packages/interact/src/index"],
12+
"@wix/splittext/plugin": ["../../packages/splittext/dist/types/plugin/index"]
1213
}
1314
},
1415
"include": ["src"],

apps/playground/SPEC.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -339,7 +339,7 @@ For authoring custom `keyframeEffect` animations (`{ name: string; keyframes: Ke
339339
- Optional `offset` (0–1, step 0.01) — shown for 3+ keyframes
340340
- CSS property/value rows with text inputs
341341
- Property name autocomplete via `<datalist>` (opacity, transform, background-color, clip-path, filter, border-radius, etc.)
342-
- Kebab-case input auto-converts to camelCase on blur (Web Animations API requirement)
342+
- Kebab-case input auto-converts to camelCase on blur (display preference — Interact accepts either casing)
343343
- Add/remove property rows (min 1 per keyframe)
344344
- **Add Keyframe** button — appends an empty keyframe
345345
- Remove keyframe button (min 1 keyframe enforced)

packages/interact-validate/README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ const ExperienceSchema = z.object({
125125
});
126126
```
127127

128-
> `InteractConfigSchema` carries a `.transform()`, so a successful `.parse()` returns the config augmented with an internal `warnings` array (`validateInteractConfig` consumes that for you). `customEffect` and function-valued `offsetEasing` are accepted as opaque functions and not deep-validated.
128+
> `InteractConfigSchema` carries a `.transform()`, so a successful `.parse()` returns the config augmented with an internal `warnings` array (`validateInteractConfig` consumes that for you). `customEffect` and function-valued `offsetEasing` are accepted as opaque functions and not deep-validated. Interactions and effects also accept `$`-prefixed plugin fields (e.g. `$splitText`) — config routed to `Interact.use()` plugins. Their schemas use `.catchall(z.unknown())` + a key check rather than `.strict()`: `$`-prefixed fields are accepted with opaque values, while any non-prefixed unknown key is still rejected as `SCHEMA_UNRECOGNIZED_KEYS`.
129129
130130
## Severity model
131131

@@ -147,7 +147,7 @@ Every issue is `'error'` or `'warning'`. `valid` is `true` **iff** no `'error'`
147147
| `STATE_EFFECT` | `EMPTY_STYLE_PROPERTIES`, `STATE_REMOVE_WITHOUT_EFFECT_ID` | warning |
148148
| `RECOMMENDED_FILL` | `RECOMMENDED_FILL_BOTH` | info |
149149
| `POINTER_AXIS` | `POINTER_AXIS_IGNORED` | warning |
150-
| `KEYFRAME_STYLE` | `KEYFRAME_PROP_NOT_CAMEL_CASE` | warning |
150+
| `CSS_PROPERTY_NAME` | `INVALID_CSS_PROPERTY_NAME` | warning |
151151
| `VIEW_INSET` | `INVALID_INSET` | warning |
152152

153153
Set a category to `'off'` to drop those issues, or `'warning'` / `'error'` to set their severity. **All other codes** (every `SCHEMA_*`, numeric, effect-source, and referential code) are not in a category and **cannot** be silenced or re-leveled via `severityOverrides` — they always emit at their built-in severity. Precedence: `'off'` first (drops the issue), then a `'warning'`/`'error'` override, then `strict` (forces the rest to `'error'`).
@@ -212,7 +212,7 @@ These encode statically-detectable authoring pitfalls from the trigger rule file
212212
| `RECOMMENDED_FILL_BOTH` | A scrubbed (`viewProgress`/`pointerMove`) or toggling (`alternate`/`repeat`/`state`) effect omits `fill: 'both'`. | `RECOMMENDED_FILL` |
213213
| `RECOMMENDED_FILL_BACKWARDS` | A `viewEnter` + `once` named/keyframe effect targeting another element or using a same-element delay omits `fill: 'backwards'` or `'both'`. | `RECOMMENDED_FILL` |
214214
| `POINTER_AXIS_IGNORED` | `pointerMove` `params.axis` set on a `namedEffect`/`customEffect` (axis only applies to `keyframeEffect`). | `POINTER_AXIS` |
215-
| `KEYFRAME_PROP_NOT_CAMEL_CASE` | A `keyframeEffect` property name is kebab-case (not WAAPI camelCase). | `KEYFRAME_STYLE` |
215+
| `INVALID_CSS_PROPERTY_NAME` | A keyframe or state-effect property name is neither camelCase nor kebab-case (both are accepted). | `CSS_PROPERTY_NAME` |
216216
| `INVALID_INSET` | `viewEnter` `params.inset` is not 1–4 CSS lengths/percentages. | `VIEW_INSET` |
217217

218218
## Usage recipes

packages/interact-validate/src/errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ const RULE_CODE_MAP: Record<string, string> = {
3131
RECOMMENDED_FILL_BOTH: 'RECOMMENDED_FILL',
3232
RECOMMENDED_FILL_BACKWARDS: 'RECOMMENDED_FILL',
3333
POINTER_AXIS_IGNORED: 'POINTER_AXIS',
34-
KEYFRAME_PROP_NOT_CAMEL_CASE: 'KEYFRAME_STYLE',
34+
INVALID_CSS_PROPERTY_NAME: 'CSS_PROPERTY_NAME',
3535
INVALID_INSET: 'VIEW_INSET',
3636
};
3737

packages/interact-validate/src/schema/effects.ts

Lines changed: 65 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { z } from 'zod';
22
import { Keyframe, RangeOffset } from './primitives';
3+
import { withPluginFields } from './plugins';
34

45
export const StateActionType = z.enum(['add', 'remove', 'toggle', 'clear']);
56
export const TimeTriggerType = z.enum(['once', 'repeat', 'alternate', 'state']);
@@ -132,19 +133,19 @@ const EffectBase = {
132133
conditions: z.array(z.string().min(1)).optional(),
133134
};
134135

135-
export const StateEffect = TransitionEffectSourceBase.extend({
136-
...EffectBase,
137-
stateAction: StateActionType.optional(),
138-
})
139-
.strict()
140-
.check(checkExactlyOneTransition);
141-
export const StateEffectRef = TransitionEffectSourceBase.extend({
142-
...EffectBase,
143-
effectId: z.string().min(1),
144-
stateAction: StateActionType.optional(),
145-
})
146-
.strict()
147-
.check(checkAtMostOneTransition);
136+
export const StateEffect = withPluginFields(
137+
TransitionEffectSourceBase.extend({
138+
...EffectBase,
139+
stateAction: StateActionType.optional(),
140+
}),
141+
).check(checkExactlyOneTransition);
142+
export const StateEffectRef = withPluginFields(
143+
TransitionEffectSourceBase.extend({
144+
...EffectBase,
145+
effectId: z.string().min(1),
146+
stateAction: StateActionType.optional(),
147+
}),
148+
).check(checkAtMostOneTransition);
148149

149150
const AnimationEffectBase = {
150151
...EffectBase,
@@ -167,57 +168,57 @@ const pointerMoveEffectFields = {
167168
transitionEasing: z.enum(['linear', 'hardBackOut', 'easeOut', 'elastic', 'bounce']).optional(),
168169
};
169170

170-
export const TimeEffect = EffectSourceBase.extend({
171-
...AnimationEffectBase,
172-
iterations: TimeIterations,
173-
duration: z.number().nonnegative(),
174-
delay: z.number().nonnegative().optional(),
175-
triggerType: TimeTriggerType.optional(),
176-
})
177-
.strict()
178-
.check(checkExactlyOneEffectSource);
179-
export const TimeEffectRef = EffectSourceBase.extend({
180-
...AnimationEffectBase,
181-
iterations: TimeIterations,
182-
effectId: z.string().min(1),
183-
duration: z.number().nonnegative().optional(),
184-
delay: z.number().nonnegative().optional(),
185-
triggerType: TimeTriggerType.optional(),
186-
})
187-
.strict()
188-
.check(checkAtMostOneEffectSource);
189-
190-
export const ViewProgressEffect = EffectSourceBase.extend({
191-
...AnimationEffectBase,
192-
iterations: ScrubIterations,
193-
...viewProgressEffectFields,
194-
})
195-
.strict()
196-
.check(checkExactlyOneEffectSource);
197-
export const ViewProgressEffectRef = EffectSourceBase.extend({
198-
...AnimationEffectBase,
199-
iterations: ScrubIterations,
200-
effectId: z.string().min(1),
201-
...viewProgressEffectFields,
202-
})
203-
.strict()
204-
.check(checkAtMostOneEffectSource);
205-
206-
export const PointerMoveEffect = EffectSourceBase.extend({
207-
...AnimationEffectBase,
208-
iterations: ScrubIterations,
209-
...pointerMoveEffectFields,
210-
})
211-
.strict()
212-
.check(checkExactlyOneEffectSource);
213-
export const PointerMoveEffectRef = EffectSourceBase.extend({
214-
...AnimationEffectBase,
215-
iterations: ScrubIterations,
216-
effectId: z.string().min(1),
217-
...pointerMoveEffectFields,
218-
})
219-
.strict()
220-
.check(checkAtMostOneEffectSource);
171+
export const TimeEffect = withPluginFields(
172+
EffectSourceBase.extend({
173+
...AnimationEffectBase,
174+
iterations: TimeIterations,
175+
duration: z.number().nonnegative(),
176+
delay: z.number().nonnegative().optional(),
177+
triggerType: TimeTriggerType.optional(),
178+
}),
179+
).check(checkExactlyOneEffectSource);
180+
export const TimeEffectRef = withPluginFields(
181+
EffectSourceBase.extend({
182+
...AnimationEffectBase,
183+
iterations: TimeIterations,
184+
effectId: z.string().min(1),
185+
duration: z.number().nonnegative().optional(),
186+
delay: z.number().nonnegative().optional(),
187+
triggerType: TimeTriggerType.optional(),
188+
}),
189+
).check(checkAtMostOneEffectSource);
190+
191+
export const ViewProgressEffect = withPluginFields(
192+
EffectSourceBase.extend({
193+
...AnimationEffectBase,
194+
iterations: ScrubIterations,
195+
...viewProgressEffectFields,
196+
}),
197+
).check(checkExactlyOneEffectSource);
198+
export const ViewProgressEffectRef = withPluginFields(
199+
EffectSourceBase.extend({
200+
...AnimationEffectBase,
201+
iterations: ScrubIterations,
202+
effectId: z.string().min(1),
203+
...viewProgressEffectFields,
204+
}),
205+
).check(checkAtMostOneEffectSource);
206+
207+
export const PointerMoveEffect = withPluginFields(
208+
EffectSourceBase.extend({
209+
...AnimationEffectBase,
210+
iterations: ScrubIterations,
211+
...pointerMoveEffectFields,
212+
}),
213+
).check(checkExactlyOneEffectSource);
214+
export const PointerMoveEffectRef = withPluginFields(
215+
EffectSourceBase.extend({
216+
...AnimationEffectBase,
217+
iterations: ScrubIterations,
218+
effectId: z.string().min(1),
219+
...pointerMoveEffectFields,
220+
}),
221+
).check(checkAtMostOneEffectSource);
221222

222223
export const ScrubEffect = z.union([ViewProgressEffect, PointerMoveEffect]);
223224
export const ScrubEffectRef = z.union([ViewProgressEffectRef, PointerMoveEffectRef]);

0 commit comments

Comments
 (0)