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.
- package/README.md +3 -3
- package/dist/esm/{chunk-WPU33HDL.mjs → chunk-7WMNPIOZ.mjs} +1724 -240
- package/dist/esm/{chunk-LWS4OSLX.mjs → chunk-JT4PZL3A.mjs} +50 -7
- package/dist/esm/{chunk-5EVG7CIY.mjs → chunk-TJDDIQRG.mjs} +42 -16
- package/dist/esm/core/index.mjs +2 -2
- package/dist/esm/effects/index.mjs +2 -2
- package/dist/esm/index.mjs +3 -3
- package/dist/kuinetic.all.js +1813 -262
- package/dist/kuinetic.css +1491 -94
- package/dist/kuinetic.js +1812 -261
- package/dist/types/core/instances.d.ts +34 -3
- package/dist/types/core/path-morph.d.ts +19 -0
- package/dist/types/core/style-plan.d.ts +6 -1
- package/dist/types/core/types.d.ts +99 -1
- package/dist/types/effects/catalog/background-media.d.ts +152 -0
- package/dist/types/effects/catalog/index.d.ts +0 -6
- package/dist/types/effects/catalog/media-shared.d.ts +124 -0
- package/dist/types/effects/catalog/media.d.ts +4 -0
- package/dist/types/effects/catalog/numbers-shared.d.ts +5 -1
- package/dist/types/effects/catalog/shared.d.ts +2 -3
- package/dist/types/effects/catalog/subtree-capture.d.ts +23 -0
- package/dist/types/effects/catalog/text-shared.d.ts +11 -2
- package/dist/types/effects/gestures/index.d.ts +0 -1
- package/dist/types/effects/layout/index.d.ts +0 -2
- package/dist/types/effects/scroll-mechanics/index.d.ts +0 -4
- package/dist/types/effects/scroll-mechanics/scroll-spy.d.ts +26 -0
- package/dist/types/effects/scroll-mechanics/tracker.d.ts +61 -3
- package/dist/types/effects/shared.d.ts +40 -0
- package/dist/types/effects/step-marking.d.ts +99 -0
- package/dist/types/effects/three-d/index.d.ts +0 -7
- 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
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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.
|
|
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",
|