Skip to content

Commit f56897d

Browse files
committed
ssot changes
1 parent 7a0b960 commit f56897d

38 files changed

Lines changed: 361 additions & 431 deletions

apps/playground/src/components/inspector/pg-condition-editor.ts

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@ import { generateId } from '../../utils/id';
1212

1313
const CONDITION_TYPES: { value: Condition['type']; label: string }[] = [
1414
{ value: 'media', label: 'Media Query' },
15-
{ value: 'container', label: 'Container' },
1615
{ value: 'selector', label: 'Selector' },
1716
];
1817

1918
const PREDICATE_PLACEHOLDERS: Record<string, string> = {
2019
media: '(min-width: 768px)',
21-
container: '(min-width: 400px)',
2220
selector: ':nth-of-type(odd)',
2321
};
2422

interact-documentation-site-audit.md

Lines changed: 31 additions & 151 deletions
Large diffs are not rendered by default.

packages/interact-validate/README.md

Lines changed: 21 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -195,27 +195,27 @@ The single source of truth for every code the validator emits. The agent-facing
195195

196196
These encode statically-detectable authoring pitfalls from the trigger rule files. Each belongs to a [rule category](#severity-model), so set the category to `'off'` to silence it or `'error'` to make it fail `valid`.
197197

198-
| Code | Trigger | Rule category |
199-
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
200-
| `UNUSED_EFFECT` | A `config.effects` entry is never referenced. | `UNUSED_DEFINITION` |
201-
| `UNUSED_SEQUENCE` | A `config.sequences` entry is never referenced. | `UNUSED_DEFINITION` |
202-
| `UNUSED_CONDITION` | A `config.conditions` entry is never referenced. | `UNUSED_DEFINITION` |
203-
| `DUPLICATE_KEYFRAME_NAME` | A `keyframeEffect.name` is reused across effects. | `UNIQUE_DEFINITION_IDS` |
204-
| `SAME_ELEMENT_RETRIGGER` | `viewEnter` with a non-`once` `triggerType` on the same source+target element. | `SAME_ELEMENT_RETRIGGER` |
205-
| `HIT_AREA_SHIFT` | `hover`/`pointerMove` `keyframeEffect` with a `translate`/`scale`/`matrix` transform on the same source+target element. | `HIT_AREA_SHIFT` |
206-
| `SCROLL_PRESET_MISSING_RANGE` | A `*Scroll` `namedEffect` on `viewProgress` omits `range`. | `SCROLL_RANGE` |
207-
| `SCROLL_PRESET_BAD_RANGE` | A scroll preset `range` is not `'in'`/`'out'`/`'continuous'`. | `SCROLL_RANGE` |
208-
| `ANIMATION_END_SELF_REFERENCE` | An `animationEnd` interaction waits on an effect it also produces (never starts). | `ANIMATION_END_GRAPH` |
209-
| `LIST_ITEM_SELECTOR_WITHOUT_CONTAINER` | `listItemSelector` present without `listContainer` (inert). | `ELEMENT_SELECTION` |
210-
| `REDUNDANT_SELECTOR_WITH_LIST_ITEM` | `selector` ignored when `listContainer` + `listItemSelector` are both present. | `ELEMENT_SELECTION` |
211-
| `EMPTY_STYLE_PROPERTIES` | A state effect's `transition.styleProperties` / `transitionProperties` is `[]` (toggles nothing). | `STATE_EFFECT` |
212-
| `STATE_REMOVE_WITHOUT_EFFECT_ID` | `stateAction: 'remove'` with no `effectId` to pair with a matching `'add'`. | `STATE_EFFECT` |
213-
| `RECOMMENDED_FILL_BOTH` | A scrubbed (`viewProgress`/`pointerMove`) or toggling (`alternate`/`repeat`/`state`) effect omits `fill: 'both'`. | `RECOMMENDED_FILL` |
214-
| `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` |
215-
| `POINTER_AXIS_IGNORED` | `pointerMove` `params.axis` set on a `namedEffect`/`customEffect` (axis only applies to `keyframeEffect`). | `POINTER_AXIS` |
216-
| `INVALID_CSS_PROPERTY_NAME` | A keyframe or state-effect property name is neither camelCase nor kebab-case (both are accepted). | `CSS_PROPERTY_NAME` |
217-
| `INVALID_INSET` | `viewEnter` `params.inset` is not 1–4 CSS lengths/percentages. | `VIEW_INSET` |
218-
| `FUNCTION_OFFSET_EASING` | A sequence's `offsetEasing` is a function, so `generate()` omits that sequence from the generated CSS. | `OFFSET_EASING` |
198+
| Code | Trigger | Rule category |
199+
| -------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------ |
200+
| `UNUSED_EFFECT` | A `config.effects` entry is never referenced. | `UNUSED_DEFINITION` |
201+
| `UNUSED_SEQUENCE` | A `config.sequences` entry is never referenced. | `UNUSED_DEFINITION` |
202+
| `UNUSED_CONDITION` | A `config.conditions` entry is never referenced. | `UNUSED_DEFINITION` |
203+
| `DUPLICATE_KEYFRAME_NAME` | A `keyframeEffect.name` is reused across effects. | `UNIQUE_DEFINITION_IDS` |
204+
| `SAME_ELEMENT_RETRIGGER` | `viewEnter` with a non-`once` `triggerType` on the same source+target element. | `SAME_ELEMENT_RETRIGGER` |
205+
| `HIT_AREA_SHIFT` | `hover`/`pointerMove` `keyframeEffect` that moves or resizes the same source+target element (a `transform` function, an individual `translate`/`scale`/`rotate` property, or a box metric such as `width`/`margin`). | `HIT_AREA_SHIFT` |
206+
| `SCROLL_PRESET_MISSING_RANGE` | A `*Scroll` `namedEffect` on `viewProgress` omits `range` (`ParallaxScroll` is exempt - it has no `range`). | `SCROLL_RANGE` |
207+
| `SCROLL_PRESET_BAD_RANGE` | A scroll preset `range` is not `'in'`/`'out'`/`'continuous'`. | `SCROLL_RANGE` |
208+
| `ANIMATION_END_SELF_REFERENCE` | An `animationEnd` interaction waits on an effect it also produces (never starts). | `ANIMATION_END_GRAPH` |
209+
| `LIST_ITEM_SELECTOR_WITHOUT_CONTAINER` | `listItemSelector` present without `listContainer` (inert). | `ELEMENT_SELECTION` |
210+
| `REDUNDANT_SELECTOR_WITH_LIST_ITEM` | `selector` ignored when `listContainer` + `listItemSelector` are both present. | `ELEMENT_SELECTION` |
211+
| `EMPTY_STYLE_PROPERTIES` | A state effect's `transition.styleProperties` / `transitionProperties` is `[]` (toggles nothing). | `STATE_EFFECT` |
212+
| `STATE_REMOVE_WITHOUT_EFFECT_ID` | `stateAction: 'remove'` with no `effectId` to pair with a matching `'add'`. | `STATE_EFFECT` |
213+
| `RECOMMENDED_FILL_BOTH` | A scrubbed (`viewProgress`/`pointerMove`) or toggling (`alternate`/`repeat`/`state`) effect omits `fill: 'both'`. | `RECOMMENDED_FILL` |
214+
| `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` |
215+
| `POINTER_AXIS_IGNORED` | `pointerMove` `params.axis` set on a `namedEffect`/`customEffect` (axis only applies to `keyframeEffect`). | `POINTER_AXIS` |
216+
| `INVALID_CSS_PROPERTY_NAME` | A keyframe or state-effect property name is neither camelCase nor kebab-case (both are accepted). | `CSS_PROPERTY_NAME` |
217+
| `INVALID_INSET` | `viewEnter` `params.inset` is not 1–4 CSS lengths/percentages. | `VIEW_INSET` |
218+
| `FUNCTION_OFFSET_EASING` | A sequence's `offsetEasing` is a function, so `generate()` omits that sequence from the generated CSS. | `OFFSET_EASING` |
219219

220220
## Usage recipes
221221

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -164,7 +164,6 @@ const viewProgressEffectFields = {
164164
const pointerMoveEffectFields = {
165165
centeredToTarget: z.boolean().optional(),
166166
transitionDuration: z.number().int().nonnegative().optional(),
167-
transitionDelay: z.number().int().nonnegative().optional(),
168167
transitionEasing: z.enum(['linear', 'hardBackOut', 'easeOut', 'elastic', 'bounce']).optional(),
169168
};
170169

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ function isValidMediaQuery(query: string): boolean {
3838

3939
export const Condition = z
4040
.object({
41-
type: z.enum(['media', 'container', 'selector']),
41+
type: z.enum(['media', 'selector']),
4242
predicate: z.string().min(1),
4343
})
4444
.strict()

packages/interact-validate/src/semantic/fouc.ts

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,31 @@ import { RETRIGGER_TYPES } from '../types';
99
// false positives (e.g. registry effects with no owning interaction).
1010

1111
const DISCRETE_TRIGGERS = ['hover', 'click', 'interest', 'activate'];
12-
const HIT_AREA_TRANSFORM = /(translate|scale|matrix)/;
12+
const HIT_AREA_TRANSFORM = /(translate|scale|matrix|rotate|skew|perspective)/;
13+
// The individual CSS transform properties, plus the box metrics that move or
14+
// resize the element without going through `transform` at all.
15+
const HIT_AREA_PROPERTIES = [
16+
'translate',
17+
'scale',
18+
'rotate',
19+
'width',
20+
'height',
21+
'top',
22+
'right',
23+
'bottom',
24+
'left',
25+
'margin',
26+
'marginTop',
27+
'marginRight',
28+
'marginBottom',
29+
'marginLeft',
30+
'margin-top',
31+
'margin-right',
32+
'margin-bottom',
33+
'margin-left',
34+
'padding',
35+
'inset',
36+
];
1337

1438
export function targetsSameElementAsSource(owner: AnyInteraction, effect: AnyEffect): boolean {
1539
if (effect.key !== undefined && effect.key !== owner.key) return false;
@@ -60,15 +84,17 @@ export function checkHitAreaShift(
6084
if (!Array.isArray(keyframes)) return [];
6185
const shifts = keyframes.some(
6286
(frame) =>
63-
typeof frame?.transform === 'string' && HIT_AREA_TRANSFORM.test(frame.transform as string),
87+
(typeof frame?.transform === 'string' &&
88+
HIT_AREA_TRANSFORM.test(frame.transform as string)) ||
89+
HIT_AREA_PROPERTIES.some((prop) => frame?.[prop] !== undefined),
6490
);
6591
if (!shifts) return [];
6692
return [
6793
{
6894
code: 'custom',
6995
params: { domainCode: 'HIT_AREA_SHIFT' },
7096
path: [...path],
71-
message: `${owner.trigger} effect changes size/position (transform) on the same element used as the source; the shifting hit area causes jittery re-entry. Target a child via \`selector\` or set a different \`key\`.`,
97+
message: `${owner.trigger} effect changes size/position on the same element used as the source; the shifting hit area causes jittery re-entry. Target a child via \`selector\` or set a different \`key\`.`,
7298
},
7399
];
74100
}

packages/interact-validate/src/semantic/partialData.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,14 @@ const SCROLL_RANGE_VALUES = ['in', 'out', 'continuous'];
44

55
// Scroll presets (the `*Scroll` motion-presets) all end with `Scroll`; no other
66
// preset category does. Used to flag a missing/invalid `range` on `viewProgress`.
7-
function isScrollPresetType(type: unknown): boolean {
8-
return typeof type === 'string' && /Scroll$/.test(type);
7+
// `ParallaxScroll` is the one exception: it always spans the full timeline and
8+
// its implementation never reads `range`.
9+
const RANGELESS_SCROLL_PRESETS = ['ParallaxScroll'];
10+
11+
function doesPresetRequireRange(type: unknown): boolean {
12+
return (
13+
typeof type === 'string' && /Scroll$/.test(type) && !RANGELESS_SCROLL_PRESETS.includes(type)
14+
);
915
}
1016

1117
// `*Scroll` namedEffect on viewProgress without a valid `range`
@@ -16,7 +22,7 @@ export function checkScrollPresetRange(
1622
): SemanticIssue[] {
1723
if (!owner || owner.trigger !== 'viewProgress') return [];
1824
const named = effect.namedEffect;
19-
if (!named || !isScrollPresetType(named.type)) return [];
25+
if (!named || !doesPresetRequireRange(named.type)) return [];
2026
if (named.range === undefined) {
2127
return [
2228
{

packages/interact-validate/test/rules/hitAreaShift.spec.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,66 @@ describe('hitAreaShift — HIT_AREA_SHIFT', () => {
5151
expect(result.errors.some((e) => e.code === CODE)).toBe(false);
5252
});
5353

54+
it('warns for an individual `scale` transform property, not just a transform string', () => {
55+
const result = validateInteractConfig({
56+
interactions: [
57+
{
58+
key: 'el',
59+
trigger: 'hover',
60+
effects: [
61+
{
62+
triggerType: 'alternate',
63+
duration: 200,
64+
fill: 'both',
65+
keyframeEffect: { name: 'grow', keyframes: [{ scale: '1.2' }] },
66+
},
67+
],
68+
},
69+
],
70+
});
71+
expect(result.errors.some((e) => e.code === CODE)).toBe(true);
72+
});
73+
74+
it('warns for a box metric that resizes the element', () => {
75+
const result = validateInteractConfig({
76+
interactions: [
77+
{
78+
key: 'el',
79+
trigger: 'hover',
80+
effects: [
81+
{
82+
triggerType: 'alternate',
83+
duration: 200,
84+
fill: 'both',
85+
keyframeEffect: { name: 'widen', keyframes: [{ width: '200px' }] },
86+
},
87+
],
88+
},
89+
],
90+
});
91+
expect(result.errors.some((e) => e.code === CODE)).toBe(true);
92+
});
93+
94+
it('warns for a rotate() transform function', () => {
95+
const result = validateInteractConfig({
96+
interactions: [
97+
{
98+
key: 'el',
99+
trigger: 'hover',
100+
effects: [
101+
{
102+
triggerType: 'alternate',
103+
duration: 200,
104+
fill: 'both',
105+
keyframeEffect: { name: 'turn', keyframes: [{ transform: 'rotate(10deg)' }] },
106+
},
107+
],
108+
},
109+
],
110+
});
111+
expect(result.errors.some((e) => e.code === CODE)).toBe(true);
112+
});
113+
54114
describe('no warning for the documented valid patterns', () => {
55115
it('does not warn when the effect targets a child via selector', () => {
56116
const result = validateInteractConfig({

packages/interact-validate/test/rules/scrollPresetRange.spec.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ describe('scrollPresetRange', () => {
3131
{
3232
key: 'el',
3333
trigger: 'viewProgress',
34-
effects: [{ namedEffect: { type: 'ParallaxScroll', range: 'sideways' }, fill: 'both' }],
34+
effects: [{ namedEffect: { type: 'FadeScroll', range: 'sideways' }, fill: 'both' }],
3535
},
3636
],
3737
});
@@ -55,6 +55,20 @@ describe('scrollPresetRange', () => {
5555
expect(result.errors.filter((e) => scrollCodes.includes(e.code))).toHaveLength(0);
5656
});
5757

58+
it('does not warn for ParallaxScroll, which has no range option', () => {
59+
const result = validateInteractConfig({
60+
interactions: [
61+
{
62+
key: 'el',
63+
trigger: 'viewProgress',
64+
effects: [{ namedEffect: { type: 'ParallaxScroll' }, fill: 'both' }],
65+
},
66+
],
67+
});
68+
const scrollCodes = ['SCROLL_PRESET_MISSING_RANGE', 'SCROLL_PRESET_BAD_RANGE'];
69+
expect(result.errors.filter((e) => scrollCodes.includes(e.code))).toHaveLength(0);
70+
});
71+
5872
it('does not warn for a non-scroll preset on viewProgress', () => {
5973
const result = validateInteractConfig({
6074
interactions: [

packages/interact-validate/test/rules/validMediaQueries.spec.ts

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,7 @@ afterEach(() => {
2424
});
2525

2626
// Helper: a config that references the named condition so no UNUSED_CONDITION fires.
27-
function configWithCondition(
28-
id: string,
29-
predicate?: string,
30-
type: 'media' | 'container' | 'selector' = 'media',
31-
) {
27+
function configWithCondition(id: string, predicate?: string, type: 'media' | 'selector' = 'media') {
3228
return {
3329
conditions: { [id]: { type, predicate: predicate || '' } },
3430
interactions: [

0 commit comments

Comments
 (0)