kuinetic 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/README.md +3 -3
  2. package/dist/esm/{chunk-WPU33HDL.mjs → chunk-7WMNPIOZ.mjs} +1724 -240
  3. package/dist/esm/{chunk-LWS4OSLX.mjs → chunk-JT4PZL3A.mjs} +50 -7
  4. package/dist/esm/{chunk-5EVG7CIY.mjs → chunk-TJDDIQRG.mjs} +42 -16
  5. package/dist/esm/core/index.mjs +2 -2
  6. package/dist/esm/effects/index.mjs +2 -2
  7. package/dist/esm/index.mjs +3 -3
  8. package/dist/kuinetic.all.js +1813 -262
  9. package/dist/kuinetic.css +1491 -94
  10. package/dist/kuinetic.js +1812 -261
  11. package/dist/types/core/instances.d.ts +34 -3
  12. package/dist/types/core/path-morph.d.ts +19 -0
  13. package/dist/types/core/style-plan.d.ts +6 -1
  14. package/dist/types/core/types.d.ts +99 -1
  15. package/dist/types/effects/catalog/background-media.d.ts +152 -0
  16. package/dist/types/effects/catalog/index.d.ts +0 -6
  17. package/dist/types/effects/catalog/media-shared.d.ts +124 -0
  18. package/dist/types/effects/catalog/media.d.ts +4 -0
  19. package/dist/types/effects/catalog/numbers-shared.d.ts +5 -1
  20. package/dist/types/effects/catalog/shared.d.ts +2 -3
  21. package/dist/types/effects/catalog/subtree-capture.d.ts +23 -0
  22. package/dist/types/effects/catalog/text-shared.d.ts +11 -2
  23. package/dist/types/effects/gestures/index.d.ts +0 -1
  24. package/dist/types/effects/layout/index.d.ts +0 -2
  25. package/dist/types/effects/scroll-mechanics/index.d.ts +0 -4
  26. package/dist/types/effects/scroll-mechanics/scroll-spy.d.ts +26 -0
  27. package/dist/types/effects/scroll-mechanics/tracker.d.ts +61 -3
  28. package/dist/types/effects/shared.d.ts +40 -0
  29. package/dist/types/effects/step-marking.d.ts +99 -0
  30. package/dist/types/effects/three-d/index.d.ts +0 -7
  31. package/package.json +9 -2
@@ -0,0 +1,26 @@
1
+ import type { PrepareContext } from '../../core/effect-context.js';
2
+ import type { EffectParams } from '../../core/types.js';
3
+ import type { ContinuousSetup } from '../../core/instances.js';
4
+ /**
5
+ * `scroll-spy`'s two authoring shapes.
6
+ *
7
+ * Split out of `primitives.ts` once the container form pushed that file over its own line cap —
8
+ * same reason `stacking-cards` and `scroll-snap` got their own *test* files in commit 69253cf, one
9
+ * level up: a category that outgrows one file gets a file of its own rather than a bigger one.
10
+ *
11
+ * `prepareScrollSpy` is the only export `primitives.ts` needs; everything else here is either mode
12
+ * is private to how it marks `data-kui-active`.
13
+ */
14
+ /**
15
+ * Mark the navigation link pointing at the most recently entered section.
16
+ *
17
+ * Two authoring shapes share this primitive. `sections:` selects the container form — one
18
+ * instance on the element that has both the nav and the sections as descendants, which measures
19
+ * every section itself instead of taking an authored `distance:` per section. Its absence keeps
20
+ * the original per-section form running exactly as shipped, `distance:` and all: that form is
21
+ * public API and this dispatch is the whole of what changes for it, nothing inside it does.
22
+ *
23
+ * @complexity Dispatch only; see whichever form runs.
24
+ * @overallScore 100
25
+ */
26
+ export declare function prepareScrollSpy(el: Element, params: EffectParams, ctx: PrepareContext): ContinuousSetup;
@@ -18,6 +18,19 @@ export interface ElementGeometry {
18
18
  }
19
19
  export type Measurer = (el: Element) => ElementGeometry;
20
20
  export declare const domGeometry: Measurer;
21
+ /** Resolved `position` for one element. Injected for the same reason `Measurer` is. */
22
+ export type PositionReader = (el: Element) => string;
23
+ export declare const domPosition: PositionReader;
24
+ /**
25
+ * Resolved `top`, in pixels, for whichever element is actually `position: sticky`. Injected for
26
+ * the same reason `Measurer` and `PositionReader` are, and for an extra one of its own: the value
27
+ * is routinely an unresolved CSS expression — `demo/system.css` sets `--kui-pin-offset: 5.5rem`
28
+ * and every pin's `offset-top` defaults to `var(--kui-pin-offset, 0px)` — so a static parse of the
29
+ * authored string cannot recover it. `getComputedStyle` can, because the browser has already
30
+ * resolved the `var()`, the `calc()`, and any `vh` in it.
31
+ */
32
+ export type OffsetReader = (el: Element) => number;
33
+ export declare const domOffsetTop: OffsetReader;
21
34
  export interface TrackOptions {
22
35
  /**
23
36
  * Scroll distance the effect spans, as an authored CSS length. Defaults to the element's own
@@ -25,14 +38,59 @@ export interface TrackOptions {
25
38
  */
26
39
  distance?: string;
27
40
  measure?: Measurer;
41
+ positionOf?: PositionReader;
42
+ /**
43
+ * A spacer the caller inserted immediately after `el`, used instead of `geometrySource` to say
44
+ * where `el` really sits in the content.
45
+ *
46
+ * `geometrySource` escapes a sticky subtree by taking its parent, which is only honest when the
47
+ * parent is a tight box around the effect. Pages used to guarantee that by hand-writing a
48
+ * wrapper — `demo/scroll.html` carried a `.scrub-stage` whose entire job was `height: 260vh`.
49
+ * Delete the wrapper and the parent becomes whatever section happens to contain the scrub:
50
+ * measured there, the parent started 926px above it against a 1817px distance, so progress read
51
+ * 51% before the element had even stuck and half the sequence played off screen.
52
+ *
53
+ * The spacer has neither problem. The library inserts it, it is exactly `distance` tall, it is
54
+ * never sticky, and it moves with the content — so it describes the scroll the effect spans
55
+ * without depending on how the author nested anything.
56
+ *
57
+ * It sits *after* `el`, so its top is `el`'s bottom; subtracting `el`'s own height recovers the
58
+ * flow position sticky is hiding. That assumes no margin between the two, which holds because
59
+ * both boxes are the library's own.
60
+ */
61
+ contentAnchor?: Element;
62
+ /**
63
+ * The element that is actually `position: sticky`, so its resolved `top` can be read back and
64
+ * subtracted from progress.
65
+ *
66
+ * Every caller above computes "the flow position sticky is hiding" — `sourceTop` or
67
+ * `contentAnchor` — but that flow position is where the element's top reaches the *viewport's*
68
+ * top edge, y = 0. Sticky does not wait for that: it engages the instant the flow top reaches
69
+ * `top: <offset>`, `offset` pixels earlier. Without this, progress read 0 for the first `offset`
70
+ * pixels the element was visibly stuck (an author with `--kui-pin-offset: 5.5rem` set — the
71
+ * showcase default — got 88px of dead pin at the start of every `pin-*` and managed
72
+ * `media-scrub`/`horizontal-scroll` effect, and 88px of dead pin at the end, since the same
73
+ * untouched flow top also gated when progress reached 1). Measured on `demo/scroll.html`'s
74
+ * `pin-spacer` card: 71 of those 88px, the rest eaten by an unrelated layout shift in the probe
75
+ * — see `docs/live-testing-backlog.md` D8.
76
+ *
77
+ * Left undefined for primitives that never call `installSticky` themselves (`scroll-progress`,
78
+ * `scroll-spy`, the bare `horizontal-scroll` form, `video-scrub`) — a page-authored sticky
79
+ * ancestor is still escaped by `geometrySource`, but its offset is unknowable from here, so
80
+ * progress there is unchanged: 0 at the sticky ancestor's untouched flow top, same as before.
81
+ */
82
+ stickyEl?: Element;
83
+ /** Injected for the same reason `measure`/`positionOf` are; see `OffsetReader`. */
84
+ offsetOf?: OffsetReader;
28
85
  }
29
86
  export type ProgressHandler = (progress: number, frame: ScrollFrame) => void;
30
87
  /**
31
88
  * Drive `onProgress` with the element's scroll progress in [0, 1].
32
89
  *
33
- * Progress is 0 when the element's top reaches the top of the scrollport and 1 once the scroll has
34
- * advanced by `distance`. Measurements are cached against the scheduler's epoch, so a frame costs
35
- * arithmetic rather than a layout flush.
90
+ * Progress is 0 when the tracked flow position reaches `options.stickyEl`'s sticky offset (the
91
+ * scrollport's top edge, y = 0, when there is no `stickyEl`) and 1 once the scroll has advanced by
92
+ * `distance`. Measurements are cached against the scheduler's epoch, so a frame costs arithmetic
93
+ * rather than a layout flush.
36
94
  *
37
95
  * @param el - Element whose position drives progress.
38
96
  * @param ctx - Prepare context supplying the scheduler and root resolver.
@@ -1,4 +1,44 @@
1
1
  import type { Activation, ParameterSchema, Primitive, ReducedMotionPolicy, Timeline } from '../core/types.js';
2
+ /**
3
+ * The one `delay` declaration, spread by every primitive that waits for a trigger before playing.
4
+ *
5
+ * Two spellings of an authored delay exist and they arrive by different routes. The positional
6
+ * one (`count-up 320ms 300ms`) rides alongside the parameter record as `params.timing` — see the
7
+ * design note in `core/js-effect-preparer.ts` for why timing is deliberately *not* merged into
8
+ * that record — while the named one (`count-up delay:300ms`) reaches a primitive only if its
9
+ * schema declares `delay`, and is otherwise dropped by `resolveParams` as an unknown parameter.
10
+ * So every primitive whose start moment is a trigger has to declare this, and declaring it is
11
+ * safe precisely because `0ms` is a true no-op: `readEffectParams` pre-fills every declared
12
+ * parameter, so a non-zero default would be indistinguishable from an authored value.
13
+ *
14
+ * Spread (`...TRIGGER_DELAY_PARAM`) rather than retyped so that rationale has one home.
15
+ * `test/js-effect-timing.test.ts` asserts every `defaultActivation: 'enter'` primitive ends up
16
+ * with this entry, whichever route it took to get there.
17
+ */
18
+ export declare const TRIGGER_DELAY_PARAM: ParameterSchema;
19
+ /**
20
+ * "Accepts every timeline, is driven by none of them."
21
+ *
22
+ * A few primitives have no relationship to progress at all: `background-media` paints a backdrop,
23
+ * and the scroll-mechanics drivers read the scroll position themselves rather than being driven by
24
+ * an `animation-timeline`. None of them ever reads `Timeline`.
25
+ *
26
+ * They still have to *accept* one, because `compile.ts`'s `intersect` narrows a composed element's
27
+ * timeline support to what every primitive on it supports. Declaring the honest answer — `['time']`,
28
+ * or nothing — empties that intersection for `data-kui="background-media src:/hero.mp4, parallax"`
29
+ * with `timeline:view`, `style-plan.ts` then refuses the timeline, and the neighbour's scrub
30
+ * silently degrades to a one-shot. Nothing is wrong with either effect; the intersection is simply
31
+ * the wrong question to ask of a primitive that has no opinion.
32
+ *
33
+ * So the list stays exhaustive and this name is what makes it truthful. Written out inline it reads
34
+ * as a claim to support all four — the same lie in four places, each needing its own paragraph to
35
+ * walk back. Written as `supportedTimelines: TIMELINE_AGNOSTIC` it reads as what it is: an
36
+ * abstention, not a claim.
37
+ *
38
+ * One shared array instance, like `ENTRANCE_TIMELINES` in `catalog/core.ts`. Every consumer
39
+ * (`intersect`, `warnUnsupportedTimeline`) only reads it.
40
+ */
41
+ export declare const TIMELINE_AGNOSTIC: Timeline[];
2
42
  export interface CssPrimitiveOptions {
3
43
  parameters?: ParameterSchema;
4
44
  timelines?: Timeline[];
@@ -0,0 +1,99 @@
1
+ import type { PrepareContext } from '../core/effect-context.js';
2
+ import type { Cleanup } from '../core/types.js';
3
+ /**
4
+ * Marking the children of a stepped effect, shared by every primitive that has an index.
5
+ *
6
+ * Two primitives publish `data-kui-step` — `scroll-progress` (scroll-driven, backs
7
+ * `scrollytelling-step`) and `step-progress` (click-driven, forms) — and the attribute alone puts
8
+ * all the work on the author. Selecting "the child matching the current index" is the one thing CSS
9
+ * cannot express, so a page had to enumerate it:
10
+ *
11
+ * ```css
12
+ * [data-kui-step='0'] li:nth-child(1),
13
+ * [data-kui-step='1'] li:nth-child(2),
14
+ * [data-kui-step='2'] li:nth-child(3),
15
+ * [data-kui-step='3'] li:nth-child(4) { ... }
16
+ * ```
17
+ *
18
+ * Four selectors for four steps, doubled for every property group, and every one of them wrong the
19
+ * moment a fifth step is added or an element is inserted above the list. `demo/scroll.html` carried
20
+ * eight such selectors for one demo.
21
+ *
22
+ * Marking the children instead collapses that to `li[data-kui-step-state='active']`, which does not
23
+ * change when the step count does. Three values rather than a boolean, because a stepper wants
24
+ * "everything up to here" (`:not([data-kui-step-state='after'])`) as often as it wants the live one.
25
+ */
26
+ /** The attribute this module owns. Never write it from anywhere else. */
27
+ export declare const STEP_STATE_ATTR = "data-kui-step-state";
28
+ export type StepState = 'before' | 'active' | 'after';
29
+ /**
30
+ * Classify a selector as unusable, dangerously broad, or fine.
31
+ *
32
+ * `matches` against the two roots catches every selector that reaches the whole document — `*`,
33
+ * `html`, `body` and compounds like `*, a` with one rule and no bespoke parser — while leaving a
34
+ * deliberately scoped wildcard such as `.spy-nav > *` working, which a syntactic ban on `*` would
35
+ * not. `matches` throws on invalid syntax exactly as `querySelectorAll` does, so the same call
36
+ * still answers the validity question.
37
+ *
38
+ * @complexity O(1) time and space.
39
+ * @overallScore 100
40
+ */
41
+ export declare function selectorBreadth(selector: string, doc: Document): 'invalid' | 'document-wide' | 'ok';
42
+ /**
43
+ * Resolve a `target:` selector once at setup, rejecting both the unusable and the over-broad.
44
+ *
45
+ * Two distinct failures, one warning channel. An invalid selector thrown from inside the shared
46
+ * scheduler's frame callback would skip every other subscriber on that root, not just this one. An
47
+ * over-broad one is worse for being silent: `target:*` is perfectly valid syntax, so a syntax-only
48
+ * check passed it straight through to stamp an attribute onto every element in the document, on
49
+ * every frame.
50
+ *
51
+ * Rejecting rather than silently narrowing is the honest response, because there is no narrower
52
+ * selector that could be meant. A selector reaching `<html>` or `<body>` is not naming a set of
53
+ * steps or a nav link, it is naming the page, and guessing which of its thousands of descendants
54
+ * the author meant would be worse than saying so.
55
+ *
56
+ * @param selector - The authored `target:` value; empty is the no-op default, not an error.
57
+ * @param ctx - Prepare context, for `doc` and the warning channel.
58
+ * @param effect - Effect name, so the warning says which attribute to go and fix.
59
+ * @returns The selector, or `''` when it must be ignored.
60
+ * @complexity O(1) time and space; `matches` walks the selector, not the document.
61
+ * @overallScore 100
62
+ */
63
+ export declare function resolveTarget(selector: string, ctx: PrepareContext, effect: string): string;
64
+ /**
65
+ * A set of step elements plus the ledgers that let their original attributes survive teardown.
66
+ *
67
+ * One ledger per element ever written, rather than a bare Set of touched nodes: a Set records
68
+ * *which* elements were stamped but not *what they held first*, so teardown would remove a
69
+ * `data-kui-step-state` the consumer had authored themselves. Same reasoning as `scroll-spy`'s
70
+ * link ledgers and the inline-style ledger they were both modelled on.
71
+ */
72
+ export interface StepMarker {
73
+ /** Stamp `before`/`active`/`after` across the tracked elements. Cheap enough to call per flip. */
74
+ mark(index: number): void;
75
+ /** Give every touched element back the attribute value it had before this instance existed. */
76
+ restore: Cleanup;
77
+ }
78
+ /**
79
+ * Build a marker over the elements a stepped effect drives.
80
+ *
81
+ * `resolve` is called per flip rather than once at setup, so a list rendered or reordered after
82
+ * this element was prepared is still picked up. Flips are rare — the caller is expected to guard on
83
+ * the index actually changing — while frames are not.
84
+ *
85
+ * @param resolve - Produces the current step elements, in document order.
86
+ * @returns A marker; call `restore` from the primitive's teardown.
87
+ * @complexity O(n) per `mark` in the number of step elements; O(n) space in elements ever touched.
88
+ * @overallScore 100
89
+ */
90
+ export declare function createStepMarker(resolve: () => Iterable<Element>): StepMarker;
91
+ /**
92
+ * Where one step sits relative to the live one.
93
+ *
94
+ * Pure, so the three-way split is assertable without a DOM.
95
+ *
96
+ * @complexity O(1) time and space.
97
+ * @overallScore 100
98
+ */
99
+ export declare function stepStateFor(position: number, index: number): StepState;
@@ -1,12 +1,5 @@
1
1
  import type { Preset, Primitive } from '../../core/types.js';
2
2
  import type { Registry } from '../../core/registry.js';
3
- /**
4
- * 3D, perspective, and page-transition effects — all CSS-rendered.
5
- *
6
- * Nothing here needs JavaScript: the browser interpolates `rotate`, `clip-path`, and `opacity`
7
- * natively, so these cost a keyframe block and a registry row each. That ratio is the whole
8
- * architecture, and it is why tripling the catalog does not triple the payload.
9
- */
10
3
  export declare const THREE_D_PRIMITIVES: Primitive[];
11
4
  export declare const THREE_D_PRESETS: Preset[];
12
5
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kuinetic",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Declarative web animation from HTML attributes. Native CSS where possible, JS only where necessary.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -39,6 +39,7 @@
39
39
  "lint": "eslint src test scripts",
40
40
  "lint:deps": "depcruise src --config .dependency-cruiser.cjs",
41
41
  "lint:dead": "knip",
42
+ "ci": "npm run build:dist && npm run typecheck && npm run lint && npm run lint:deps && npm run lint:dead && npm run check:css-coverage && npm run test:coverage && npm run test:browser",
42
43
  "size": "npm run build:dist && size-limit",
43
44
  "build": "npm run generate:css && esbuild src/index.ts --bundle --format=iife --global-name=kuinetic --outfile=demo/kuinetic.js && esbuild src/css/index.css --bundle --outfile=demo/kuinetic.css && npx @tailwindcss/cli -i demo/tailwind-entry.css -o demo/tailwind.css && node scripts/build-standalone.mjs demo",
44
45
  "build:dist": "npm run clean:dist && npm run generate:css && esbuild src/index.ts src/core/index.ts src/effects/index.ts --bundle --format=esm --splitting --outdir=dist/esm --out-extension:.js=.mjs && esbuild src/index.ts --bundle --format=iife --global-name=kuinetic --outfile=dist/kuinetic.js && esbuild src/css/index.css --bundle --outfile=dist/kuinetic.css && tsc -p tsconfig.build.json && node scripts/build-standalone.mjs",
@@ -50,9 +51,13 @@
50
51
  "dev": "npm run build && node scripts/dev-server.mjs",
51
52
  "verify:browser": "npm run build && node scripts/verify-browser.mjs",
52
53
  "test:browser": "npm run build && node scripts/run-browser-tests.mjs",
54
+ "test:visual": "npm run build && node scripts/visual-regression.mjs",
55
+ "test:visual:update": "npm run build && node scripts/visual-regression.mjs --update",
53
56
  "generate:css": "node scripts/generate-preset-css.mjs",
57
+ "generate:nav": "node scripts/generate-nav-header.mjs",
54
58
  "record": "npm run build && node scripts/verify-browser.mjs --record",
55
- "check:showcase": "npm run build && node scripts/check-showcase.mjs"
59
+ "check:showcase": "npm run build && node scripts/check-showcase.mjs",
60
+ "check:css-coverage": "npm run build && node scripts/check-css-coverage.mjs"
56
61
  },
57
62
  "devDependencies": {
58
63
  "@eslint/js": "^9.39.5",
@@ -68,7 +73,9 @@
68
73
  "globals": "^14.0.0",
69
74
  "jsdom": "^25.0.1",
70
75
  "knip": "^6.32.2",
76
+ "pixelmatch": "^7.2.0",
71
77
  "playwright-core": "^1.61.1",
78
+ "pngjs": "^7.0.0",
72
79
  "size-limit": "^13.0.3",
73
80
  "tailwindcss": "^4.3.3",
74
81
  "typescript": "^5.9.3",