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 +51 -8
- package/dist/adapters.d.ts +55 -0
- package/dist/adapters.js +139 -0
- package/dist/dom.d.ts +1 -47
- package/dist/dom.js +2 -1
- package/dist/index-DXp5b6BU.d.ts +48 -0
- package/dist/index.js +2 -1
- package/dist/normalize-CYnN3Npw.js +540 -0
- package/dist/react-native.d.ts +1 -1
- package/dist/react-native.js +2 -1
- package/dist/react.d.ts +1 -1
- package/dist/react.js +2 -1
- package/dist/{spring-CBynweFS.js → spring-CFHloqPP.js} +2 -540
- package/dist/svelte-shared.d.ts +1 -1
- package/dist/svelte-shared.js +2 -1
- package/dist/vue.d.ts +1 -1
- package/dist/vue.js +2 -1
- package/package.json +26 -1
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.
|
|
372
|
-
| `morphicons/
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
375
|
-
| `morphicons/
|
|
376
|
-
| `
|
|
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 };
|
package/dist/adapters.js
ADDED
|
@@ -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 {
|
|
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
|
|
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
|
|
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 };
|