morphicons 1.4.2 → 1.5.0

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 CHANGED
@@ -45,6 +45,8 @@ bun add morphicons # or npm install / pnpm add
45
45
 
46
46
  ESM only. `react` (>= 18), `vue` (>= 3.3), `svelte` (>= 5) and `react-native` (>= 0.71) + `react-native-svg` (>= 14) are optional peers — only needed for `morphicons/react`, `morphicons/vue`, `morphicons/svelte` and `morphicons/react-native`.
47
47
 
48
+ **Icons come from a data package, not a component package.** morphicons consumes icon *data* (an `IconNode` or a raw `d` string). For Lucide that means the vanilla `lucide` package: `import { Menu, X } from "lucide"` gives you `IconNode`s, which is why every snippet below says "data, not components". The framework packages (`lucide-react`, `lucide-vue-next`, `@lucide/svelte`, `lucide-react-native`) export components, and `MorphIcon` can't consume those. If your app already renders static icons with one of them, keep it: the data and component packages coexist by design and both tree-shake cleanly, so you only pay for the icons you import. Just keep their versions aligned, so the icons you morph match the ones you render statically.
49
+
48
50
  ## Usage
49
51
 
50
52
  ### React — three modes
@@ -65,7 +67,7 @@ ref.current?.morphTo(Check); // animates
65
67
  ref.current?.set(X); // jumps without animating
66
68
  ```
67
69
 
68
- Drop-in replacement for lucide-react: `size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, `className` and the rest of the `<svg>` props pass straight through. Correct accessibility by default: `aria-hidden` unless you pass `label` (→ `role="img"` + `<title>`). Clean SSR: the server emits the exact static SVG (zero flash, zero layout shift); the runtime is born on hydration. Morphs play regardless of the OS reduce-motion setting by default; opt into honoring it with `reducedMotion="user"` (see [Reduced motion](#reduced-motion-all-four-bindings)).
70
+ Drop-in replacement for lucide-react (the props surface: the `icon` prop takes Lucide *data*, not lucide-react components, see [Install](#install)): `size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, `className` and the rest of the `<svg>` props pass straight through. Correct accessibility by default: `aria-hidden` unless you pass `label` (→ `role="img"` + `<title>`). Clean SSR: the server emits the exact static SVG (zero flash, zero layout shift); the runtime is born on hydration. Morphs play regardless of the OS reduce-motion setting by default; opt into honoring it with `reducedMotion="user"` (see [Reduced motion](#reduced-motion-all-four-bindings)).
69
71
 
70
72
  ### Vue — same three modes
71
73
 
@@ -118,7 +120,7 @@ Same surface as the React binding: presentation props (`size`, `strokeWidth`, `a
118
120
  <MorphIcon bind:this={morph} icon={Menu} />
119
121
  ```
120
122
 
121
- Same surface again: presentation props, `class`/`style`/rest-attr fall-through — fully typed via `svelte/elements`, so SVG attrs, events and ARIA autocomplete and typos fail the build, like lucide-svelte — the same accessibility defaults and the same clean SSR — works with SvelteKit out of the box. Svelte 5 (runes); the component ships as `.svelte` source and your bundler compiles it via the `svelte` export condition, like every Svelte library.
123
+ Same surface again: presentation props, `class`/`style`/rest-attr fall-through — fully typed via `svelte/elements`, so SVG attrs, events and ARIA autocomplete and typos fail the build, like lucide-svelte — the same accessibility defaults and the same clean SSR — works with SvelteKit out of the box. Svelte 5 (runes); the component ships as `.svelte` source and your bundler compiles it via the `svelte` export condition, like every Svelte library. If you followed Lucide's official Svelte guide you already have `@lucide/svelte`: keep it for your static icons; `MorphIcon` takes the data exports from the vanilla `lucide` package (see [Install](#install)).
122
124
 
123
125
  ### React Native — same three modes
124
126
 
@@ -192,6 +194,42 @@ const d = serialize(out, plan.items.map(it => it.closed)); // `d` attribute
192
194
 
193
195
  Accepts `IconNode` (Lucide's data format, structurally typed — Lucide is neither a dependency nor a peer) or a raw `d` attribute. Tabler, Heroicons, Iconoir and custom paths work the same.
194
196
 
197
+ ## Format adapters — `morphicons/adapters`
198
+
199
+ The core has exactly two structural contracts: `IconInput` on the way in (an `IconNode` or a `d` string) and `PathEl` on the way out (anything with `setAttribute` — the same contract that lets the React Native binding drive `setNativeProps`). A **format adapter** converts a foreign icon representation into one of those two contracts. All of them live in one opt-in entry, so the unit of payment is the import: core, driver and bindings never grow for a capability you didn't ask for, and inside the entry each adapter is an independent module — a named import tree-shakes to just that adapter, pinned by per-export size gates in CI. Adapters are per *format*, never per vendor: there is no `/iconify` and never will be.
200
+
201
+ ### `svgToIcon` — SVG markup in
202
+
203
+ For icons that exist as markup rather than data: an Iconify body from `@iconify-json/*` (what UnoCSS `presetIcons` and the Tailwind icon plugins consume), a full `<svg>` copied from a website, a `<path>` from the shadcn registry. One call turns it into an `IconInput` any entry accepts:
204
+
205
+ ```ts
206
+ import { svgToIcon } from "morphicons/adapters";
207
+
208
+ // Parse ONCE, at module scope — reuse the reference so the plan cache holds.
209
+ const MENU = svgToIcon(menuBody); // '<path stroke="currentColor" d="M4 5h16…"/>'
210
+ const X = svgToIcon(xBody); // works in createMorph and all four bindings
211
+ ```
212
+
213
+ Everything else happens under the hood: non-rendered containers (`<defs>`, `<mask>`, `<clipPath>`, `<symbol>`) are stripped, the morphability scan rejects what can't honestly morph (a fill-drawn icon with no stroke — Material Symbols style — a `transform`, an element outside the seven stroke primitives) with a clear error at parse time, and when the markup carries a `viewBox` the geometry is re-gridded onto 24 via [`fitIcon`](#icon-library-compatibility) automatically, so off-grid collections just work.
214
+
215
+ ### `maskTarget` — CSS-mask elements out
216
+
217
+ For apps that render icons as CSS masks — UnoCSS `presetIcons`, `@iconify/tailwind`, `@egoist/tailwindcss-icons` (`i-lucide-*`, `icon-[lucide-x]`): the icon is a `mask-image` data URI on a `<span>`/`<div>`, with **no `<path>` in the DOM** to write to. `maskTarget` creates a hidden pair of referenced `<svg><mask><path>` buffers once, points the element's mask at them, and hands the driver a `PathEl` whose `d` writes land on that real geometry — so the element morphs in place, keeping your `size-*`/`text-*` classes. Two engine traps shaped this mechanism (see `docs/adr/0002`): a data-URI mask swapped per frame never decodes in time in Chromium (blank flight), and WebKit doesn't repaint when a referenced mask's *content* mutates (frozen flight) — hence the pair: each write flips `mask-image` between the two buffers, which forces re-resolution everywhere.
218
+
219
+ ```ts
220
+ import { maskTarget } from "morphicons/adapters";
221
+ import { createMorph } from "morphicons/dom";
222
+
223
+ const target = maskTarget(spanEl); // spanEl: class="size-5 text-current"
224
+ const m = createMorph(target, MENU);
225
+ m.morphTo(X, "snappy"); // full Morph handle: set/seek/progress/destroy
226
+ // on unmount: m.destroy(); target.dispose();
227
+ ```
228
+
229
+ It *is* `createMorph` — only the write target differs — so springs, reduced motion and mid-flight interruption behave identically. On setup it sets `background-color: currentColor`, so a bare `<span>` works with no icon class (`{ paint: false }` keeps an existing `bg-*`; `strokeWidth` and `viewBox` are options). The returned target adds `dispose()` — call it after `morph.destroy()` when the element unmounts, so the hidden mask nodes go with it.
230
+
231
+ **Cost.** A live inline `<path>` gets the cheapest possible write; a mask element additionally makes the browser re-rasterize the mask and repaint the masked box every frame (main thread, no compositor-only path). Great for toggles and short lists (menu↔close, chevrons, play↔pause); for icon-heavy views, the inline-SVG bindings remain the leanest option — same styling, one less indirection.
232
+
195
233
  ## Icon library compatibility
196
234
 
197
235
  morphicons has no per-library adapters — any icon set that meets these requirements works out of the box:
@@ -368,12 +406,17 @@ CI budget (size-limit, gzip) as an anti-regression tripwire — gates carry ~10%
368
406
  | entry | measured | gate |
369
407
  |---|---|---|
370
408
  | `morphicons` (core) | 6.60 KB | 7 KB |
371
- | `morphicons/dom` (core + driver) | 7.04 KB | 7.5 KB |
372
- | `morphicons/react` (all, react external) | 7.89 KB | 8.5 KB |
373
- | `morphicons/react-native` (all, react/rn/rnsvg external) | 8.25 KB | 9 KB |
374
- | `morphicons/vue` (all, vue external) | 7.93 KB | 8.5 KB |
375
- | `morphicons/svelte` TS half (all, svelte external) | 7.54 KB | 8 KB |
376
- | `MorphIcon.svelte` (ships as source, consumer-compiled) | 1.25 KB | 1.4 KB |
409
+ | `morphicons/dom` (core + driver) | 7.12 KB | 7.5 KB |
410
+ | `morphicons/adapters` (aggregate — grows by design) | 4.03 KB | 4.4 KB |
411
+ | `adapters` › `{ svgToIcon }` alone | 3.34 KB | 3.7 KB |
412
+ | `adapters` › `{ maskTarget }` alone | 0.70 KB | 0.8 KB |
413
+ | `morphicons/react` (all, react external) | 8.01 KB | 8.5 KB |
414
+ | `morphicons/react-native` (all, react/rn/rnsvg external) | 8.37 KB | 9 KB |
415
+ | `morphicons/vue` (all, vue external) | 8.04 KB | 8.5 KB |
416
+ | `morphicons/svelte` TS half (all, svelte external) | 7.66 KB | 8 KB |
417
+ | `MorphIcon.svelte` (ships as source, consumer-compiled) | 1.26 KB | 1.4 KB |
418
+
419
+ The adapters entry carries **two kinds of gate**: the aggregate (allowed to grow as adapters are added) and one per named export, which pins each adapter as independently tree-shakeable — if two adapters ever couple, the per-export gate trips in CI. Standalone, `{ svgToIcon }` measures 3.34 KB because auto-fit drags the core's lowering machinery; an app that already imports the core shares that chunk and pays ~0.4 KB incremental.
377
420
 
378
421
  ## Playground
379
422
 
@@ -0,0 +1,55 @@
1
+ import { n as IconInput } from "./types-JAF3s76Y.js";
2
+ import { i as PathEl } from "./index-DXp5b6BU.js";
3
+ //#region src/adapters/mask.d.ts
4
+ /** The subset of an element's inline style the mask target writes — structural
5
+ * so a real HTMLElement's `.style` satisfies it (and a test fake supplies
6
+ * just these). */
7
+ interface MaskStyle {
8
+ maskImage: string;
9
+ webkitMaskImage: string;
10
+ backgroundColor: string;
11
+ }
12
+ /** An element the mask target can drive (an HTMLElement, or a fake in tests). */
13
+ interface MaskEl {
14
+ style: MaskStyle;
15
+ }
16
+ interface MaskTargetOptions {
17
+ /** Stroke width of the mask geometry (Lucide's default is 2). */
18
+ strokeWidth?: number;
19
+ /** Grid the icon's coordinates live on (Lucide/Iconify default: "0 0 24 24").
20
+ * Used to map the geometry onto the element's box. */
21
+ viewBox?: string;
22
+ /** Set `background-color: currentColor` on first setup so the element shows
23
+ * through the mask in the current text color, standalone (no icon class).
24
+ * Turn off to keep an existing paint (e.g. a `bg-*` utility). Default true. */
25
+ paint?: boolean;
26
+ }
27
+ /** What `maskTarget` returns: the driver's write contract plus a `dispose`
28
+ * that removes the hidden mask node (call it after `morph.destroy()` when
29
+ * the element unmounts). */
30
+ interface MaskPathEl extends PathEl {
31
+ dispose(): void;
32
+ }
33
+ /** Adapts an element into a `PathEl` whose writes drive a referenced SVG
34
+ * mask, so `createMorph(maskTarget(el), icon)` animates a mask-styled
35
+ * element instead of an inline `<svg><path>`.
36
+ *
37
+ * DOUBLE BUFFER, on purpose: WebKit does not reliably repaint a masked
38
+ * element when the CONTENT of its referenced `<mask>` mutates — a lone mask
39
+ * whose inner path `d` changes per frame renders frozen in Safari until the
40
+ * flight ends. So the target keeps TWO masks and every `d` write lands on
41
+ * the back buffer and re-points `mask-image` at it: the property VALUE flips
42
+ * (`url(#a)` ↔ `url(#b)`) each frame, which forces reference re-resolution
43
+ * in every engine. Chromium/Gecko pay nothing extra for it. */
44
+ declare function maskTarget(el: MaskEl, opts?: MaskTargetOptions): MaskPathEl;
45
+ //#endregion
46
+ //#region src/adapters/svg.d.ts
47
+ /** SVG markup → an IconInput the pipeline can lower. Strips non-rendered
48
+ * containers, rejects what can't be honestly morphed (a fill-drawn icon with
49
+ * no stroke, a transform, an unsupported element), and re-grids onto 24 via
50
+ * the viewBox when one is present — so an off-grid collection lands on the
51
+ * shared grid automatically. Returns a `d` (when a viewBox forced a fit) or
52
+ * an IconNode. */
53
+ declare function svgToIcon(markup: string): IconInput;
54
+ //#endregion
55
+ export { type MaskEl, type MaskPathEl, type MaskStyle, type MaskTargetOptions, maskTarget, svgToIcon };
@@ -0,0 +1,139 @@
1
+ import { t as fitIcon } from "./normalize-CYnN3Npw.js";
2
+ //#region src/adapters/mask.ts
3
+ const SVG_NS = "http://www.w3.org/2000/svg";
4
+ let seq = 0;
5
+ let host = null;
6
+ /** Adapts an element into a `PathEl` whose writes drive a referenced SVG
7
+ * mask, so `createMorph(maskTarget(el), icon)` animates a mask-styled
8
+ * element instead of an inline `<svg><path>`.
9
+ *
10
+ * DOUBLE BUFFER, on purpose: WebKit does not reliably repaint a masked
11
+ * element when the CONTENT of its referenced `<mask>` mutates — a lone mask
12
+ * whose inner path `d` changes per frame renders frozen in Safari until the
13
+ * flight ends. So the target keeps TWO masks and every `d` write lands on
14
+ * the back buffer and re-points `mask-image` at it: the property VALUE flips
15
+ * (`url(#a)` ↔ `url(#b)`) each frame, which forces reference re-resolution
16
+ * in every engine. Chromium/Gecko pay nothing extra for it. */
17
+ function maskTarget(el, opts = {}) {
18
+ if (typeof document === "undefined") throw new Error("morphicons: maskTarget needs a DOM (client-side only)");
19
+ const doc = document;
20
+ if (!host) {
21
+ host = doc.createElementNS(SVG_NS, "svg");
22
+ host.setAttribute("width", "0");
23
+ host.setAttribute("height", "0");
24
+ host.setAttribute("aria-hidden", "true");
25
+ host.setAttribute("style", "position:absolute");
26
+ doc.body.appendChild(host);
27
+ }
28
+ const vb = (opts.viewBox ?? "0 0 24 24").trim().split(/[\s,]+/).map(Number);
29
+ const [mx, my, w, h] = [
30
+ vb[0] || 0,
31
+ vb[1] || 0,
32
+ vb[2] || 24,
33
+ vb[3] || vb[2] || 24
34
+ ];
35
+ const makeLayer = () => {
36
+ const id = `morphicons-mask-${++seq}`;
37
+ const mask = doc.createElementNS(SVG_NS, "mask");
38
+ mask.setAttribute("id", id);
39
+ mask.setAttribute("maskContentUnits", "objectBoundingBox");
40
+ const g = doc.createElementNS(SVG_NS, "g");
41
+ g.setAttribute("transform", `scale(${1 / w} ${1 / h}) translate(${-mx} ${-my})`);
42
+ const path = doc.createElementNS(SVG_NS, "path");
43
+ path.setAttribute("fill", "none");
44
+ path.setAttribute("stroke", "#fff");
45
+ path.setAttribute("stroke-width", String(opts.strokeWidth ?? 2));
46
+ path.setAttribute("stroke-linecap", "round");
47
+ path.setAttribute("stroke-linejoin", "round");
48
+ g.appendChild(path);
49
+ mask.appendChild(g);
50
+ if (host) host.appendChild(mask);
51
+ return {
52
+ url: `url(#${id})`,
53
+ mask,
54
+ path
55
+ };
56
+ };
57
+ const layers = [makeLayer(), makeLayer()];
58
+ let front = 0;
59
+ const s = el.style;
60
+ s.maskImage = s.webkitMaskImage = layers[0].url;
61
+ if (opts.paint !== false) s.backgroundColor = "currentColor";
62
+ return {
63
+ setAttribute(name, d) {
64
+ if (name !== "d") return;
65
+ front = 1 - front;
66
+ const layer = layers[front];
67
+ layer.path.setAttribute("d", d);
68
+ s.maskImage = s.webkitMaskImage = layer.url;
69
+ },
70
+ dispose() {
71
+ layers[0].mask.remove();
72
+ layers[1].mask.remove();
73
+ }
74
+ };
75
+ }
76
+ //#endregion
77
+ //#region src/adapters/svg.ts
78
+ const SVG_CONTAINER = /<(defs|mask|clipPath|symbol)\b[^>]*>[\s\S]*?<\/\1>/gi;
79
+ const SVG_ELEMENT = /<([a-zA-Z][\w:-]*)((?:[^>"']|"[^"]*"|'[^']*')*?)\/?>/g;
80
+ const SVG_ATTR = /([\w:-]+)\s*=\s*"([^"]*)"|([\w:-]+)\s*=\s*'([^']*)'/g;
81
+ const SVG_VIEWBOX = /<svg\b[^>]*\bviewBox\s*=\s*["']([^"']+)["']/i;
82
+ const SVG_GEOMETRY = /* @__PURE__ */ new Set([
83
+ "path",
84
+ "line",
85
+ "circle",
86
+ "ellipse",
87
+ "rect",
88
+ "polyline",
89
+ "polygon"
90
+ ]);
91
+ const SVG_SKIP = /* @__PURE__ */ new Set([
92
+ "svg",
93
+ "g",
94
+ "title",
95
+ "desc",
96
+ "defs",
97
+ "mask",
98
+ "clippath",
99
+ "symbol"
100
+ ]);
101
+ function readSvgAttrs(raw) {
102
+ const attrs = {};
103
+ SVG_ATTR.lastIndex = 0;
104
+ let m;
105
+ while ((m = SVG_ATTR.exec(raw)) !== null) {
106
+ const name = m[1] ?? m[3];
107
+ if (name) attrs[name] = m[2] ?? m[4] ?? "";
108
+ }
109
+ return attrs;
110
+ }
111
+ /** SVG markup → an IconInput the pipeline can lower. Strips non-rendered
112
+ * containers, rejects what can't be honestly morphed (a fill-drawn icon with
113
+ * no stroke, a transform, an unsupported element), and re-grids onto 24 via
114
+ * the viewBox when one is present — so an off-grid collection lands on the
115
+ * shared grid automatically. Returns a `d` (when a viewBox forced a fit) or
116
+ * an IconNode. */
117
+ function svgToIcon(markup) {
118
+ if (markup.trimStart().charCodeAt(0) !== 60) throw new Error("morphicons: svgToIcon expects SVG markup — a d string or IconNode is already an IconInput");
119
+ const body = markup.replace(SVG_CONTAINER, "");
120
+ const hasStroke = /stroke\s*=\s*["'](?!\s*none\s*["'])/i.test(body);
121
+ const fillNone = /fill\s*=\s*["']\s*none\s*["']/i.test(body);
122
+ if (!hasStroke && !fillNone) throw new Error("morphicons: SVG looks fill-drawn (no stroke) — only stroke-centerline icons morph");
123
+ const node = [];
124
+ SVG_ELEMENT.lastIndex = 0;
125
+ let m;
126
+ while ((m = SVG_ELEMENT.exec(body)) !== null) {
127
+ const tag = m[1].toLowerCase();
128
+ if (SVG_SKIP.has(tag)) continue;
129
+ if (!SVG_GEOMETRY.has(tag)) throw new Error(`morphicons: unsupported SVG element <${tag}>`);
130
+ const attrs = readSvgAttrs(m[2]);
131
+ if ("transform" in attrs) throw new Error(`morphicons: <${tag}> has a transform (unsupported)`);
132
+ node.push([tag, attrs]);
133
+ }
134
+ if (node.length === 0) throw new Error("morphicons: no morphable geometry in SVG");
135
+ const vb = SVG_VIEWBOX.exec(markup);
136
+ return vb ? fitIcon(node, vb[1]) : node;
137
+ }
138
+ //#endregion
139
+ export { maskTarget, svgToIcon };
package/dist/dom.d.ts CHANGED
@@ -1,48 +1,2 @@
1
- import { c as SpringPreset, n as IconInput } from "./types-JAF3s76Y.js";
2
- //#region src/dom/index.d.ts
3
- /** Target element, structurally typed: any object with setAttribute works
4
- * (a real SVGPathElement, or a fake in tests). */
5
- interface PathEl {
6
- setAttribute(name: string, value: string): void;
7
- }
8
- /** Morph physics: named preset or custom spring. */
9
- interface MorphOptions {
10
- stiffness?: number;
11
- damping?: number;
12
- }
13
- /** Reduced-motion policy. `"never"` (default): morphs always animate,
14
- * ignoring the OS setting. `"user"`: while the OS reduce-motion setting is
15
- * on, `morphTo` degrades to an instant `set`. `"always"`: every `morphTo`
16
- * jumps (tests, screenshots). */
17
- type ReducedMotionMode = "never" | "user" | "always";
18
- interface CreateMorphOptions {
19
- /** Reduced-motion policy; also live via the `reducedMotion` property. */
20
- reducedMotion?: ReducedMotionMode;
21
- }
22
- interface Morph {
23
- /** Animates toward the icon with spring physics. Interruptible: mid-flight
24
- * it re-plans from the intermediate shape while preserving velocity. */
25
- morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
26
- /** Jumps to the icon without animating (canonical d, no frames). */
27
- set(icon: IconInput): void;
28
- /** Renders the morph toward `icon` frozen at t (no spring) — the
29
- * controlled-mode primitive: scrubbing, gestures, scroll. A later
30
- * morphTo takes off from that intermediate shape. */
31
- seek(icon: IconInput, t: number): void;
32
- /** Current progress (t of the last frame; 1 at rest). Assigning it is
33
- * equivalent to seek(target, t) on the active target. */
34
- progress: number;
35
- /** Reduced-motion policy, live: assigning it applies to the next morphTo. */
36
- reducedMotion: ReducedMotionMode;
37
- /** Unregisters the instance; later calls are no-ops. */
38
- destroy(): void;
39
- }
40
- /** Canonical `d` of an icon: the input string verbatim, or the real cubics
41
- * quantized to 4 decimals (the at-rest snap; engine-stable bytes so SSR
42
- * hydration matches, see fmtCanon in core/serialize). Exported because it
43
- * is what a binding renders at SSR/rest before any runtime exists. */
44
- declare function canonicalD(icon: IconInput): string;
45
- /** Creates the morph instance over a `<path>` and paints the initial icon. */
46
- declare function createMorph(el: PathEl, icon: IconInput, options?: CreateMorphOptions): Morph;
47
- //#endregion
1
+ import { a as ReducedMotionMode, i as PathEl, n as Morph, o as canonicalD, r as MorphOptions, s as createMorph, t as CreateMorphOptions } from "./index-DXp5b6BU.js";
48
2
  export { CreateMorphOptions, Morph, MorphOptions, PathEl, ReducedMotionMode, canonicalD, createMorph };
package/dist/dom.js CHANGED
@@ -1,4 +1,5 @@
1
- import { a as buildPlan, c as cubicsToPathD, f as interpPolar, l as serialize, n as Spring, r as resampleIcon, s as iconToCubics, t as SPRING_PRESETS, u as allocOutputs } from "./spring-CBynweFS.js";
1
+ import { a as buildPlan, c as interpPolar, n as Spring, o as allocOutputs, r as resampleIcon, t as SPRING_PRESETS } from "./spring-CFHloqPP.js";
2
+ import { i as serialize, n as iconToCubics, r as cubicsToPathD } from "./normalize-CYnN3Npw.js";
2
3
  //#region src/dom/index.ts
3
4
  const tickers = /* @__PURE__ */ new Set();
4
5
  let rafId = 0;
@@ -0,0 +1,48 @@
1
+ import { c as SpringPreset, n as IconInput } from "./types-JAF3s76Y.js";
2
+ //#region src/dom/index.d.ts
3
+ /** Target element, structurally typed: any object with setAttribute works
4
+ * (a real SVGPathElement, or a fake in tests). */
5
+ interface PathEl {
6
+ setAttribute(name: string, value: string): void;
7
+ }
8
+ /** Morph physics: named preset or custom spring. */
9
+ interface MorphOptions {
10
+ stiffness?: number;
11
+ damping?: number;
12
+ }
13
+ /** Reduced-motion policy. `"never"` (default): morphs always animate,
14
+ * ignoring the OS setting. `"user"`: while the OS reduce-motion setting is
15
+ * on, `morphTo` degrades to an instant `set`. `"always"`: every `morphTo`
16
+ * jumps (tests, screenshots). */
17
+ type ReducedMotionMode = "never" | "user" | "always";
18
+ interface CreateMorphOptions {
19
+ /** Reduced-motion policy; also live via the `reducedMotion` property. */
20
+ reducedMotion?: ReducedMotionMode;
21
+ }
22
+ interface Morph {
23
+ /** Animates toward the icon with spring physics. Interruptible: mid-flight
24
+ * it re-plans from the intermediate shape while preserving velocity. */
25
+ morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
26
+ /** Jumps to the icon without animating (canonical d, no frames). */
27
+ set(icon: IconInput): void;
28
+ /** Renders the morph toward `icon` frozen at t (no spring) — the
29
+ * controlled-mode primitive: scrubbing, gestures, scroll. A later
30
+ * morphTo takes off from that intermediate shape. */
31
+ seek(icon: IconInput, t: number): void;
32
+ /** Current progress (t of the last frame; 1 at rest). Assigning it is
33
+ * equivalent to seek(target, t) on the active target. */
34
+ progress: number;
35
+ /** Reduced-motion policy, live: assigning it applies to the next morphTo. */
36
+ reducedMotion: ReducedMotionMode;
37
+ /** Unregisters the instance; later calls are no-ops. */
38
+ destroy(): void;
39
+ }
40
+ /** Canonical `d` of an icon: the input string verbatim, or the real cubics
41
+ * quantized to 4 decimals (the at-rest snap; engine-stable bytes so SSR
42
+ * hydration matches, see fmtCanon in core/serialize). Exported because it
43
+ * is what a binding renders at SSR/rest before any runtime exists. */
44
+ declare function canonicalD(icon: IconInput): string;
45
+ /** Creates the morph instance over a `<path>` and paints the initial icon. */
46
+ declare function createMorph(el: PathEl, icon: IconInput, options?: CreateMorphOptions): Morph;
47
+ //#endregion
48
+ export { ReducedMotionMode as a, PathEl as i, Morph as n, canonicalD as o, MorphOptions as r, createMorph as s, CreateMorphOptions as t };
package/dist/index.js CHANGED
@@ -1,2 +1,3 @@
1
- import { a as buildPlan, c as cubicsToPathD, d as interpLinear, f as interpPolar, i as resamplePath, l as serialize, n as Spring, o as fitIcon, r as resampleIcon, s as iconToCubics, t as SPRING_PRESETS, u as allocOutputs } from "./spring-CBynweFS.js";
1
+ import { a as buildPlan, c as interpPolar, i as resamplePath, n as Spring, o as allocOutputs, r as resampleIcon, s as interpLinear, t as SPRING_PRESETS } from "./spring-CFHloqPP.js";
2
+ import { i as serialize, n as iconToCubics, r as cubicsToPathD, t as fitIcon } from "./normalize-CYnN3Npw.js";
2
3
  export { SPRING_PRESETS, Spring, allocOutputs, buildPlan, cubicsToPathD, fitIcon, iconToCubics, interpLinear, interpPolar, resampleIcon, resamplePath, serialize };