morphicons 1.1.1 → 1.4.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 +94 -8
- package/dist/MorphIcon.svelte +86 -0
- package/dist/react-native.d.ts +31 -0
- package/dist/react-native.js +180 -0
- package/dist/react.js +66 -27
- package/dist/svelte-shared.d.ts +68 -0
- package/dist/svelte-shared.js +146 -0
- package/dist/svelte.d.ts +13 -0
- package/dist/svelte.js +1 -0
- package/dist/vue.d.ts +121 -0
- package/dist/vue.js +198 -0
- package/package.json +87 -5
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ The usual icon morphs either interpolate raw coordinates (shapes shrink and shea
|
|
|
36
36
|
bun add morphicons # or npm install / pnpm add
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
ESM only. `react`
|
|
39
|
+
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`.
|
|
40
40
|
|
|
41
41
|
## Usage
|
|
42
42
|
|
|
@@ -60,6 +60,88 @@ ref.current?.set(X); // jumps without animating
|
|
|
60
60
|
|
|
61
61
|
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. `prefers-reduced-motion` degrades to an instant swap.
|
|
62
62
|
|
|
63
|
+
### Vue — same three modes
|
|
64
|
+
|
|
65
|
+
```vue
|
|
66
|
+
<script setup lang="ts">
|
|
67
|
+
import { ref } from "vue";
|
|
68
|
+
import { MorphIcon, type MorphHandle } from "morphicons/vue";
|
|
69
|
+
import { Menu, X, Check } from "lucide"; // data, not components
|
|
70
|
+
|
|
71
|
+
const open = ref(false);
|
|
72
|
+
const morph = ref<MorphHandle | null>(null);
|
|
73
|
+
// morph.value?.morphTo(Check) → animates
|
|
74
|
+
// morph.value?.set(X) → jumps without animating
|
|
75
|
+
</script>
|
|
76
|
+
|
|
77
|
+
<template>
|
|
78
|
+
<!-- 1. Uncontrolled (90% of uses): change the prop and morphicons animates -->
|
|
79
|
+
<MorphIcon :icon="open ? X : Menu" spring="snappy" />
|
|
80
|
+
|
|
81
|
+
<!-- 2. Controlled (gestures, scroll): explicit progress, no spring -->
|
|
82
|
+
<MorphIcon :from="Menu" :to="X" :progress="dragProgress" />
|
|
83
|
+
|
|
84
|
+
<!-- 3. Imperative (sequences): template ref → morphTo / set -->
|
|
85
|
+
<MorphIcon ref="morph" :icon="Menu" />
|
|
86
|
+
</template>
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Same surface as the React binding: presentation props (`size`, `strokeWidth`, `absoluteStrokeWidth`, `color`; `class`, `style` and the rest of the `<svg>` attrs fall through), the same accessibility defaults (`aria-hidden` unless you pass `label`) and the same clean SSR — works with Nuxt out of the box: the server emits the exact static SVG and the runtime is born on hydration. The binding is a plain render function; no SFC compiler or JSX involved.
|
|
90
|
+
|
|
91
|
+
### Svelte — same three modes
|
|
92
|
+
|
|
93
|
+
```svelte
|
|
94
|
+
<script lang="ts">
|
|
95
|
+
import { MorphIcon, type MorphHandle } from "morphicons/svelte";
|
|
96
|
+
import { Menu, X, Check } from "lucide"; // data, not components
|
|
97
|
+
|
|
98
|
+
let open = $state(false);
|
|
99
|
+
let morph = $state<MorphHandle>();
|
|
100
|
+
// morph?.morphTo(Check) → animates
|
|
101
|
+
// morph?.set(X) → jumps without animating
|
|
102
|
+
</script>
|
|
103
|
+
|
|
104
|
+
<!-- 1. Uncontrolled (90% of uses): change the prop and morphicons animates -->
|
|
105
|
+
<MorphIcon icon={open ? X : Menu} spring="snappy" />
|
|
106
|
+
|
|
107
|
+
<!-- 2. Controlled (gestures, scroll): explicit progress, no spring -->
|
|
108
|
+
<MorphIcon from={Menu} to={X} progress={dragProgress} />
|
|
109
|
+
|
|
110
|
+
<!-- 3. Imperative (sequences): bind:this → morphTo / set -->
|
|
111
|
+
<MorphIcon bind:this={morph} icon={Menu} />
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
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.
|
|
115
|
+
|
|
116
|
+
### React Native — same three modes
|
|
117
|
+
|
|
118
|
+
```tsx
|
|
119
|
+
import { MorphIcon, type MorphHandle } from "morphicons/react-native";
|
|
120
|
+
import { Menu, X, Check } from "lucide"; // data, not components
|
|
121
|
+
|
|
122
|
+
// 1. Uncontrolled (90% of uses): change the prop and morphicons animates
|
|
123
|
+
<MorphIcon icon={open ? X : Menu} spring="snappy" />
|
|
124
|
+
|
|
125
|
+
// 2. Controlled (gestures, scroll): explicit progress, no spring
|
|
126
|
+
<MorphIcon from={Menu} to={X} progress={dragProgress} />
|
|
127
|
+
|
|
128
|
+
// 3. Imperative (sequences)
|
|
129
|
+
const ref = useRef<MorphHandle>(null);
|
|
130
|
+
<MorphIcon ref={ref} icon={Menu} />
|
|
131
|
+
ref.current?.morphTo(Check); // animates
|
|
132
|
+
ref.current?.set(X); // jumps without animating
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
The DOM-free core beyond the browser: the dom driver is reused verbatim as the engine (React Native has a global `requestAnimationFrame`, and `PathEl` is structural), so the whole platform difference is a shim that forwards the per-frame `d` write to `Path.setNativeProps` of react-native-svg — outside the React render, exactly like the web mutation. Same surface as the React binding (`size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, plus the native `Svg` props: `testID`, touch handlers…), same accessibility defaults (`aria-hidden` unless you pass `label` → `role="img"` + `aria-label`). Reduced motion comes from `AccessibilityInfo` (best-effort: the query is async; a `reduceMotionChanged` subscription keeps it exact from then on). Requires Metro with package `exports` resolution — default since React Native 0.79; on older versions enable `unstable_enablePackageExports`.
|
|
136
|
+
|
|
137
|
+
### Lifecycle contract (all four bindings)
|
|
138
|
+
|
|
139
|
+
The four components share one contract, pinned by mirrored client-mount tests:
|
|
140
|
+
|
|
141
|
+
- **Lazy driver.** Mounting without any icon is fine — SSR emits `<path d="">` and the driver is born with the FIRST icon that shows up, whether a late `icon` prop (data that loads async), a late `from`/`to` pair, or an imperative `set`/`morphTo`. The first icon paints without animating; `morphTo` before the driver exists behaves as `set` (there is nothing to fly from).
|
|
142
|
+
- **Controlled wins.** While `from` AND `to` are both present, the pair owns the path: `icon` changes are ignored, no spring fires. Drop the pair and the current `icon` takes over (animated). Mixing the modes is not an error — the precedence is just explicit.
|
|
143
|
+
- **Clean re-entry.** Any exit from controlled mode (an imperative call or an icon takeover) invalidates the frozen pair, so returning to the same `from`/`to` re-bases on `from` and renders exactly like a clean mount at that `progress`.
|
|
144
|
+
|
|
63
145
|
### Vanilla (no React)
|
|
64
146
|
|
|
65
147
|
```ts
|
|
@@ -103,7 +185,7 @@ morphicons has no per-library adapters — any icon set that meets these require
|
|
|
103
185
|
const bell = fitIcon(teenySmallBell, 15); // also "0 0 15 15" or [0, 0, 15, 15]
|
|
104
186
|
```
|
|
105
187
|
|
|
106
|
-
It returns a plain `d`, so it goes anywhere an icon is accepted. Call it at module scope, not per render. Skipping it doesn't throw — Procrustes is similarity-invariant, so the grid mismatch never reads as false rotation; it lands in σ as an unwanted zoom, and the target draws outside the canvas. The React
|
|
188
|
+
It returns a plain `d`, so it goes anywhere an icon is accepted. Call it at module scope, not per render. Skipping it doesn't throw — Procrustes is similarity-invariant, so the grid mismatch never reads as false rotation; it lands in σ as an unwanted zoom, and the target draws outside the canvas. The React, Vue and Svelte bindings default to `viewBox="0 0 24 24"`, overridable via props.
|
|
107
189
|
4. **Uniform stroke.** Cosmetic rather than structural: a consistent stroke width and round caps/joins make in-flight shapes look native. These live on the `<svg>`, not in morphicons.
|
|
108
190
|
|
|
109
191
|
Beyond the libraries in the playground, sets known to qualify on the 24×24 grid — no fitting needed: **Heroicons** (outline style), **Iconoir** (1.5px stroke), **Akar Icons**, **Untitled UI** and **Hugeicons** (stroke style). Off-grid packs work through `fitIcon`: **Teenyicons** (15×15), **Carbon** (32×32), **Heroicons solid** (20×20).
|
|
@@ -114,7 +196,7 @@ This also covers the [shadcn registry](https://www.shadcn.io/icons), which re-pu
|
|
|
114
196
|
|
|
115
197
|
```
|
|
116
198
|
┌─────────────────────────────────────────────┐
|
|
117
|
-
│ bindings react
|
|
199
|
+
│ bindings react · vue · svelte · rn │ thin: ref + createMorph
|
|
118
200
|
├─────────────────────────────────────────────┤
|
|
119
201
|
│ drivers dom (setAttribute + rAF) │ singleton scheduler
|
|
120
202
|
├─────────────────────────────────────────────┤
|
|
@@ -125,7 +207,7 @@ This also covers the [shadcn registry](https://www.shadcn.io/icons), which re-pu
|
|
|
125
207
|
└─────────────────────────────────────────────┘
|
|
126
208
|
```
|
|
127
209
|
|
|
128
|
-
The hard rule: **the core never touches the DOM**. Pure functions consume icon data and produce `d` strings. Direct consequence:
|
|
210
|
+
The hard rule: **the core never touches the DOM**. Pure functions consume icon data and produce `d` strings. Direct consequence: the React Native binding over `react-native-svg` is just another adapter, not a rewrite — it reuses the dom driver verbatim through a `setNativeProps` shim. One package with subpath exports (`.` core, `./dom`, `./react`, `./react-native`, `./vue`, `./svelte`), ESM only, `sideEffects: false`.
|
|
129
211
|
|
|
130
212
|
The plan is the central artifact:
|
|
131
213
|
|
|
@@ -265,7 +347,11 @@ CI budget (size-limit, gzip) as an anti-regression tripwire — gates carry ~10%
|
|
|
265
347
|
|---|---|---|
|
|
266
348
|
| `morphicons` (core) | 6.60 KB | 7 KB |
|
|
267
349
|
| `morphicons/dom` (core + driver) | 7.04 KB | 7.5 KB |
|
|
268
|
-
| `morphicons/react` (all, react external) | 7.
|
|
350
|
+
| `morphicons/react` (all, react external) | 7.89 KB | 8.5 KB |
|
|
351
|
+
| `morphicons/react-native` (all, react/rn/rnsvg external) | 8.25 KB | 9 KB |
|
|
352
|
+
| `morphicons/vue` (all, vue external) | 7.93 KB | 8.5 KB |
|
|
353
|
+
| `morphicons/svelte` TS half (all, svelte external) | 7.54 KB | 8 KB |
|
|
354
|
+
| `MorphIcon.svelte` (ships as source, consumer-compiled) | 1.25 KB | 1.4 KB |
|
|
269
355
|
|
|
270
356
|
## Playground
|
|
271
357
|
|
|
@@ -278,10 +364,10 @@ bun run play # → http://localhost:3000
|
|
|
278
364
|
## Development
|
|
279
365
|
|
|
280
366
|
```bash
|
|
281
|
-
bun test #
|
|
282
|
-
bun run typecheck # strict ×
|
|
367
|
+
bun test # 156 tests / ~13,700 asserts
|
|
368
|
+
bun run typecheck # strict ×6: core+dom without lib DOM, playground, react, react-native, vue, svelte
|
|
283
369
|
bun run format # biome
|
|
284
|
-
bun run build # tsdown → dist/
|
|
370
|
+
bun run build # tsdown → dist/ + scripts/build-svelte.ts (svelte entry)
|
|
285
371
|
bun run size # size gates
|
|
286
372
|
```
|
|
287
373
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
<!-- Svelte 5 binding: MorphIcon with the three modes — uncontrolled (change
|
|
2
|
+
the `icon` prop and morphicons animates), controlled (`from`/`to`/`progress`,
|
|
3
|
+
no spring) and imperative (`bind:this` → { morphTo, set }).
|
|
4
|
+
|
|
5
|
+
Clean SSR by construction: the initial `d` is computed ONCE with the pure
|
|
6
|
+
core at init (server and client produce the same string → hydration without
|
|
7
|
+
mismatch) and stays a plain const, so Svelte never rewrites the attribute —
|
|
8
|
+
the morph mutates it externally via createMorph. Thin shell on purpose: all
|
|
9
|
+
logic lives in shared.ts (typechecked); this file is only compiled.
|
|
10
|
+
Drop-in with lucide-svelte: same presentation props. -->
|
|
11
|
+
<script lang="ts">
|
|
12
|
+
import { onMount, untrack } from "svelte";
|
|
13
|
+
import {
|
|
14
|
+
computeInitialD,
|
|
15
|
+
createController,
|
|
16
|
+
type IconInput,
|
|
17
|
+
type MorphIconProps,
|
|
18
|
+
type MorphOptions,
|
|
19
|
+
type SpringPreset,
|
|
20
|
+
} from "./svelte-shared.js";
|
|
21
|
+
|
|
22
|
+
let {
|
|
23
|
+
icon,
|
|
24
|
+
from,
|
|
25
|
+
to,
|
|
26
|
+
progress,
|
|
27
|
+
spring,
|
|
28
|
+
size = 24,
|
|
29
|
+
color = "currentColor",
|
|
30
|
+
strokeWidth = 2,
|
|
31
|
+
absoluteStrokeWidth = false,
|
|
32
|
+
label,
|
|
33
|
+
...rest
|
|
34
|
+
}: MorphIconProps = $props();
|
|
35
|
+
|
|
36
|
+
// svelte-ignore state_referenced_locally — mount-time value on purpose: the
|
|
37
|
+
// initial d must stay a constant so Svelte never rewrites the attribute.
|
|
38
|
+
const initialD = computeInitialD({ icon, from, to, progress });
|
|
39
|
+
|
|
40
|
+
let pathEl: SVGPathElement | undefined = $state();
|
|
41
|
+
// svelte-ignore state_referenced_locally — init-time values on purpose: they
|
|
42
|
+
// seed the controller's watch baselines so the first $effect run is a no-op.
|
|
43
|
+
const ctrl = createController({ icon, from, to, progress });
|
|
44
|
+
|
|
45
|
+
onMount(() => {
|
|
46
|
+
if (pathEl) ctrl.mount(pathEl, { icon, from, to, progress });
|
|
47
|
+
return () => ctrl.destroy();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
// Prop changes: one watcher owns the mode logic (controlled wins — change
|
|
51
|
+
// detection and precedence live in shared.ts).
|
|
52
|
+
$effect(() => {
|
|
53
|
+
const next = { icon, from, to, progress, spring };
|
|
54
|
+
untrack(() => ctrl.watch(next));
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export function morphTo(i: IconInput, s?: SpringPreset | MorphOptions): void {
|
|
58
|
+
ctrl.morphTo(i, s ?? spring);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function set(i: IconInput): void {
|
|
62
|
+
ctrl.set(i);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const sw = $derived(
|
|
66
|
+
absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth,
|
|
67
|
+
);
|
|
68
|
+
</script>
|
|
69
|
+
|
|
70
|
+
<svg
|
|
71
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
72
|
+
width={size}
|
|
73
|
+
height={size}
|
|
74
|
+
viewBox="0 0 24 24"
|
|
75
|
+
fill="none"
|
|
76
|
+
stroke={color}
|
|
77
|
+
stroke-width={sw}
|
|
78
|
+
stroke-linecap="round"
|
|
79
|
+
stroke-linejoin="round"
|
|
80
|
+
role={label ? "img" : undefined}
|
|
81
|
+
aria-hidden={label ? undefined : "true"}
|
|
82
|
+
{...rest}
|
|
83
|
+
>
|
|
84
|
+
{#if label}<title>{label}</title>{/if}
|
|
85
|
+
<path bind:this={pathEl} d={initialD} />
|
|
86
|
+
</svg>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
|
|
2
|
+
import { Morph, MorphOptions, PathEl } from "./dom.js";
|
|
3
|
+
import { SvgProps } from "react-native-svg";
|
|
4
|
+
//#region src/react-native/index.d.ts
|
|
5
|
+
/** Imperative surface exposed via ref. */
|
|
6
|
+
interface MorphHandle {
|
|
7
|
+
morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
|
|
8
|
+
set(icon: IconInput): void;
|
|
9
|
+
}
|
|
10
|
+
interface MorphIconProps extends Omit<SvgProps, "from" | "to"> {
|
|
11
|
+
/** Uncontrolled mode: the current icon; changing the prop animates. */
|
|
12
|
+
icon?: IconInput;
|
|
13
|
+
/** Controlled mode: source endpoint of the pair. */
|
|
14
|
+
from?: IconInput;
|
|
15
|
+
/** Controlled mode: target endpoint of the pair. */
|
|
16
|
+
to?: IconInput;
|
|
17
|
+
/** Controlled mode: 0..1 progress of the frozen morph (no spring). */
|
|
18
|
+
progress?: number;
|
|
19
|
+
/** Physics for uncontrolled/imperative mode: preset or custom spring. */
|
|
20
|
+
spring?: SpringPreset | MorphOptions;
|
|
21
|
+
size?: number | string;
|
|
22
|
+
color?: string;
|
|
23
|
+
strokeWidth?: number | string;
|
|
24
|
+
/** Like lucide: stroke width does not scale with `size`. */
|
|
25
|
+
absoluteStrokeWidth?: boolean;
|
|
26
|
+
/** Accessibility: with label → role="img" + aria-label; without → aria-hidden. */
|
|
27
|
+
label?: string;
|
|
28
|
+
}
|
|
29
|
+
declare const MorphIcon: import("react").ForwardRefExoticComponent<MorphIconProps & import("react").RefAttributes<MorphHandle>>;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, MorphIconProps, type MorphOptions, type PathEl, type Sampled, type SpringPreset };
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-BTcHiHR5.js";
|
|
2
|
+
import { canonicalD, createMorph } from "./dom.js";
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
4
|
+
import { jsx } from "react/jsx-runtime";
|
|
5
|
+
import { AccessibilityInfo } from "react-native";
|
|
6
|
+
import Svg, { Path } from "react-native-svg";
|
|
7
|
+
//#region src/react-native/index.tsx
|
|
8
|
+
const useIsoLayoutEffect = typeof document === "undefined" ? useEffect : useLayoutEffect;
|
|
9
|
+
let reduced = false;
|
|
10
|
+
let rmInit = false;
|
|
11
|
+
function initReducedMotion() {
|
|
12
|
+
if (rmInit) return;
|
|
13
|
+
rmInit = true;
|
|
14
|
+
try {
|
|
15
|
+
AccessibilityInfo.isReduceMotionEnabled().then((value) => {
|
|
16
|
+
reduced = value;
|
|
17
|
+
}, () => {});
|
|
18
|
+
AccessibilityInfo.addEventListener("reduceMotionChanged", (value) => {
|
|
19
|
+
reduced = value;
|
|
20
|
+
});
|
|
21
|
+
} catch {}
|
|
22
|
+
}
|
|
23
|
+
/** Frozen shape of the from→to pair at t, using the pure core. At exact
|
|
24
|
+
* endpoints returns the canonical `d` (real curves, not polyline). */
|
|
25
|
+
function frozenD(from, to, t) {
|
|
26
|
+
if (t <= 0) return canonicalD(from);
|
|
27
|
+
if (t >= 1) return canonicalD(to);
|
|
28
|
+
const plan = buildPlan(resampleIcon(from), resampleIcon(to));
|
|
29
|
+
const out = allocOutputs(plan);
|
|
30
|
+
interpPolar(plan, t, out);
|
|
31
|
+
return serialize(out, plan.items.map((it) => it.closed));
|
|
32
|
+
}
|
|
33
|
+
const MorphIcon = forwardRef(function MorphIcon(props, ref) {
|
|
34
|
+
const { icon, from, to, progress, spring, size = 24, color = "currentColor", strokeWidth = 2, absoluteStrokeWidth, label, ...rest } = props;
|
|
35
|
+
const controlled = from !== void 0 && to !== void 0;
|
|
36
|
+
const initialIcon = icon ?? from ?? to;
|
|
37
|
+
const [initialD] = useState(() => {
|
|
38
|
+
if (controlled) return frozenD(from, to, progress ?? 0);
|
|
39
|
+
return initialIcon !== void 0 ? canonicalD(initialIcon) : "";
|
|
40
|
+
});
|
|
41
|
+
const pathRef = useRef(null);
|
|
42
|
+
const morphRef = useRef(null);
|
|
43
|
+
const springRef = useRef(spring);
|
|
44
|
+
springRef.current = spring;
|
|
45
|
+
const prevIcon = useRef(icon);
|
|
46
|
+
const prevControlled = useRef(controlled);
|
|
47
|
+
const dead = useRef(false);
|
|
48
|
+
const based = useRef(false);
|
|
49
|
+
const pair = useRef(null);
|
|
50
|
+
/** The PathEl shim: createMorph only ever writes `d`, and Path forwards
|
|
51
|
+
* it to the native view without re-rendering. Stable identity. */
|
|
52
|
+
const el = useRef({ setAttribute: (name, value) => {
|
|
53
|
+
if (name === "d") pathRef.current?.setNativeProps({ d: value });
|
|
54
|
+
} });
|
|
55
|
+
/** morphTo that honors the OS reduce-motion setting (best-effort). */
|
|
56
|
+
const fly = (m, i, s) => {
|
|
57
|
+
if (reduced) m.set(i);
|
|
58
|
+
else m.morphTo(i, s);
|
|
59
|
+
};
|
|
60
|
+
const flyRef = useRef(fly);
|
|
61
|
+
flyRef.current = fly;
|
|
62
|
+
/** Driver birth, lazy included (#1 of the lifecycle contract): an
|
|
63
|
+
* iconless mount keeps the element and the FIRST icon to show up (prop
|
|
64
|
+
* or imperative) creates the driver, already showing it — no flight.
|
|
65
|
+
* Stable across renders: closes over refs only. */
|
|
66
|
+
const ensure = useCallback((birth) => {
|
|
67
|
+
if (morphRef.current) return morphRef.current;
|
|
68
|
+
if (dead.current || !pathRef.current) return null;
|
|
69
|
+
morphRef.current = createMorph(el.current, birth);
|
|
70
|
+
return morphRef.current;
|
|
71
|
+
}, []);
|
|
72
|
+
useIsoLayoutEffect(() => {
|
|
73
|
+
dead.current = false;
|
|
74
|
+
initReducedMotion();
|
|
75
|
+
if (pathRef.current && initialIcon !== void 0) {
|
|
76
|
+
const m = createMorph(el.current, controlled ? from : initialIcon);
|
|
77
|
+
morphRef.current = m;
|
|
78
|
+
if (controlled) {
|
|
79
|
+
pair.current = [from, to];
|
|
80
|
+
const t = progress ?? 0;
|
|
81
|
+
if (t <= 0) m.set(from);
|
|
82
|
+
else if (t >= 1) m.set(to);
|
|
83
|
+
else {
|
|
84
|
+
m.seek(to, t);
|
|
85
|
+
based.current = true;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return () => {
|
|
90
|
+
dead.current = true;
|
|
91
|
+
morphRef.current?.destroy();
|
|
92
|
+
morphRef.current = null;
|
|
93
|
+
based.current = false;
|
|
94
|
+
pair.current = null;
|
|
95
|
+
};
|
|
96
|
+
}, []);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
const left = prevControlled.current && !controlled;
|
|
99
|
+
prevControlled.current = controlled;
|
|
100
|
+
const changed = icon !== prevIcon.current;
|
|
101
|
+
prevIcon.current = icon;
|
|
102
|
+
if (controlled) return;
|
|
103
|
+
if (icon === void 0 || !changed && !left) return;
|
|
104
|
+
pair.current = null;
|
|
105
|
+
based.current = false;
|
|
106
|
+
const m = morphRef.current;
|
|
107
|
+
if (m) flyRef.current(m, icon, springRef.current);
|
|
108
|
+
else ensure(icon);
|
|
109
|
+
}, [
|
|
110
|
+
icon,
|
|
111
|
+
controlled,
|
|
112
|
+
ensure
|
|
113
|
+
]);
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
if (!controlled) return;
|
|
116
|
+
const m = morphRef.current ?? ensure(from);
|
|
117
|
+
if (!m) return;
|
|
118
|
+
const t = progress ?? 0;
|
|
119
|
+
if (!pair.current || pair.current[0] !== from || pair.current[1] !== to) {
|
|
120
|
+
pair.current = [from, to];
|
|
121
|
+
based.current = false;
|
|
122
|
+
}
|
|
123
|
+
if (t <= 0) {
|
|
124
|
+
m.set(from);
|
|
125
|
+
based.current = false;
|
|
126
|
+
} else if (t >= 1) {
|
|
127
|
+
m.set(to);
|
|
128
|
+
based.current = false;
|
|
129
|
+
} else {
|
|
130
|
+
if (!based.current) {
|
|
131
|
+
m.set(from);
|
|
132
|
+
based.current = true;
|
|
133
|
+
}
|
|
134
|
+
m.seek(to, t);
|
|
135
|
+
}
|
|
136
|
+
}, [
|
|
137
|
+
controlled,
|
|
138
|
+
from,
|
|
139
|
+
to,
|
|
140
|
+
progress,
|
|
141
|
+
ensure
|
|
142
|
+
]);
|
|
143
|
+
useImperativeHandle(ref, () => ({
|
|
144
|
+
morphTo: (i, s) => {
|
|
145
|
+
pair.current = null;
|
|
146
|
+
based.current = false;
|
|
147
|
+
const m = morphRef.current;
|
|
148
|
+
if (m) flyRef.current(m, i, s ?? springRef.current);
|
|
149
|
+
else ensure(i);
|
|
150
|
+
},
|
|
151
|
+
set: (i) => {
|
|
152
|
+
pair.current = null;
|
|
153
|
+
based.current = false;
|
|
154
|
+
const m = morphRef.current;
|
|
155
|
+
if (m) m.set(i);
|
|
156
|
+
else ensure(i);
|
|
157
|
+
}
|
|
158
|
+
}), [ensure]);
|
|
159
|
+
const sw = absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth;
|
|
160
|
+
return /* @__PURE__ */ jsx(Svg, {
|
|
161
|
+
width: size,
|
|
162
|
+
height: size,
|
|
163
|
+
viewBox: "0 0 24 24",
|
|
164
|
+
fill: "none",
|
|
165
|
+
stroke: color,
|
|
166
|
+
strokeWidth: sw,
|
|
167
|
+
strokeLinecap: "round",
|
|
168
|
+
strokeLinejoin: "round",
|
|
169
|
+
role: label ? "img" : void 0,
|
|
170
|
+
"aria-label": label,
|
|
171
|
+
"aria-hidden": label ? void 0 : true,
|
|
172
|
+
...rest,
|
|
173
|
+
children: /* @__PURE__ */ jsx(Path, {
|
|
174
|
+
ref: pathRef,
|
|
175
|
+
d: initialD
|
|
176
|
+
})
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
//#endregion
|
|
180
|
+
export { MorphIcon };
|
package/dist/react.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-BTcHiHR5.js";
|
|
2
2
|
import { canonicalD, createMorph } from "./dom.js";
|
|
3
|
-
import { forwardRef, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
3
|
+
import { forwardRef, useCallback, useEffect, useImperativeHandle, useLayoutEffect, useRef, useState } from "react";
|
|
4
4
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
5
|
//#region src/react/index.tsx
|
|
6
6
|
const useIsoLayoutEffect = typeof document === "undefined" ? useEffect : useLayoutEffect;
|
|
@@ -26,42 +26,68 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
|
|
|
26
26
|
const morphRef = useRef(null);
|
|
27
27
|
const springRef = useRef(spring);
|
|
28
28
|
springRef.current = spring;
|
|
29
|
-
const
|
|
29
|
+
const prevIcon = useRef(icon);
|
|
30
|
+
const prevControlled = useRef(controlled);
|
|
31
|
+
const dead = useRef(false);
|
|
30
32
|
const based = useRef(false);
|
|
31
33
|
const pair = useRef(null);
|
|
34
|
+
/** Driver birth, lazy included (#1 of the lifecycle contract): an
|
|
35
|
+
* iconless mount keeps the element and the FIRST icon to show up (prop
|
|
36
|
+
* or imperative) creates the driver, already showing it — no flight.
|
|
37
|
+
* Stable across renders: closes over refs only. */
|
|
38
|
+
const ensure = useCallback((birth) => {
|
|
39
|
+
if (morphRef.current) return morphRef.current;
|
|
40
|
+
const el = pathRef.current;
|
|
41
|
+
if (dead.current || !el) return null;
|
|
42
|
+
morphRef.current = createMorph(el, birth);
|
|
43
|
+
return morphRef.current;
|
|
44
|
+
}, []);
|
|
32
45
|
useIsoLayoutEffect(() => {
|
|
46
|
+
dead.current = false;
|
|
33
47
|
const el = pathRef.current;
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
if (el && initialIcon !== void 0) {
|
|
49
|
+
const m = createMorph(el, controlled ? from : initialIcon);
|
|
50
|
+
morphRef.current = m;
|
|
51
|
+
if (controlled) {
|
|
52
|
+
pair.current = [from, to];
|
|
53
|
+
const t = progress ?? 0;
|
|
54
|
+
if (t <= 0) m.set(from);
|
|
55
|
+
else if (t >= 1) m.set(to);
|
|
56
|
+
else {
|
|
57
|
+
m.seek(to, t);
|
|
58
|
+
based.current = true;
|
|
59
|
+
}
|
|
45
60
|
}
|
|
46
61
|
}
|
|
47
62
|
return () => {
|
|
48
|
-
|
|
63
|
+
dead.current = true;
|
|
64
|
+
morphRef.current?.destroy();
|
|
49
65
|
morphRef.current = null;
|
|
50
66
|
based.current = false;
|
|
51
67
|
pair.current = null;
|
|
52
68
|
};
|
|
53
69
|
}, []);
|
|
54
70
|
useEffect(() => {
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
const left = prevControlled.current && !controlled;
|
|
72
|
+
prevControlled.current = controlled;
|
|
73
|
+
const changed = icon !== prevIcon.current;
|
|
74
|
+
prevIcon.current = icon;
|
|
75
|
+
if (controlled) return;
|
|
76
|
+
if (icon === void 0 || !changed && !left) return;
|
|
77
|
+
pair.current = null;
|
|
78
|
+
based.current = false;
|
|
63
79
|
const m = morphRef.current;
|
|
64
|
-
if (
|
|
80
|
+
if (m) m.morphTo(icon, springRef.current);
|
|
81
|
+
else ensure(icon);
|
|
82
|
+
}, [
|
|
83
|
+
icon,
|
|
84
|
+
controlled,
|
|
85
|
+
ensure
|
|
86
|
+
]);
|
|
87
|
+
useEffect(() => {
|
|
88
|
+
if (!controlled) return;
|
|
89
|
+
const m = morphRef.current ?? ensure(from);
|
|
90
|
+
if (!m) return;
|
|
65
91
|
const t = progress ?? 0;
|
|
66
92
|
if (!pair.current || pair.current[0] !== from || pair.current[1] !== to) {
|
|
67
93
|
pair.current = [from, to];
|
|
@@ -84,12 +110,25 @@ const MorphIcon = forwardRef(function MorphIcon(props, ref) {
|
|
|
84
110
|
controlled,
|
|
85
111
|
from,
|
|
86
112
|
to,
|
|
87
|
-
progress
|
|
113
|
+
progress,
|
|
114
|
+
ensure
|
|
88
115
|
]);
|
|
89
116
|
useImperativeHandle(ref, () => ({
|
|
90
|
-
morphTo: (i, s) =>
|
|
91
|
-
|
|
92
|
-
|
|
117
|
+
morphTo: (i, s) => {
|
|
118
|
+
pair.current = null;
|
|
119
|
+
based.current = false;
|
|
120
|
+
const m = morphRef.current;
|
|
121
|
+
if (m) m.morphTo(i, s ?? springRef.current);
|
|
122
|
+
else ensure(i);
|
|
123
|
+
},
|
|
124
|
+
set: (i) => {
|
|
125
|
+
pair.current = null;
|
|
126
|
+
based.current = false;
|
|
127
|
+
const m = morphRef.current;
|
|
128
|
+
if (m) m.set(i);
|
|
129
|
+
else ensure(i);
|
|
130
|
+
}
|
|
131
|
+
}), [ensure]);
|
|
93
132
|
const sw = absoluteStrokeWidth ? Number(strokeWidth) * 24 / Number(size) : strokeWidth;
|
|
94
133
|
return /* @__PURE__ */ jsxs("svg", {
|
|
95
134
|
xmlns: "http://www.w3.org/2000/svg",
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
|
|
2
|
+
import { Morph, MorphOptions, PathEl } from "./dom.js";
|
|
3
|
+
import { SVGAttributes } from "svelte/elements";
|
|
4
|
+
//#region src/svelte/shared.d.ts
|
|
5
|
+
/** Imperative surface exposed on the component instance (`bind:this`). */
|
|
6
|
+
interface MorphHandle {
|
|
7
|
+
morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
|
|
8
|
+
set(icon: IconInput): void;
|
|
9
|
+
}
|
|
10
|
+
/** Rest props fall through to the `<svg>` fully typed (class, style, data-*,
|
|
11
|
+
* events, ARIA…) via svelte/elements, like lucide-svelte — typos in an SVG
|
|
12
|
+
* attr fail the build. `from`/`to` are omitted because the SMIL animation
|
|
13
|
+
* attributes of the same name collide with the controlled-mode pair. */
|
|
14
|
+
interface MorphIconProps extends Omit<SVGAttributes<SVGSVGElement>, "from" | "to"> {
|
|
15
|
+
/** Uncontrolled mode: the current icon; changing the prop animates. */
|
|
16
|
+
icon?: IconInput;
|
|
17
|
+
/** Controlled mode: source endpoint of the pair. */
|
|
18
|
+
from?: IconInput;
|
|
19
|
+
/** Controlled mode: target endpoint of the pair. */
|
|
20
|
+
to?: IconInput;
|
|
21
|
+
/** Controlled mode: 0..1 progress of the frozen morph (no spring). */
|
|
22
|
+
progress?: number;
|
|
23
|
+
/** Physics for uncontrolled/imperative mode: preset or custom spring. */
|
|
24
|
+
spring?: SpringPreset | MorphOptions;
|
|
25
|
+
size?: number | string;
|
|
26
|
+
color?: string;
|
|
27
|
+
strokeWidth?: number | string;
|
|
28
|
+
/** Like lucide-svelte: stroke width does not scale with `size`. */
|
|
29
|
+
absoluteStrokeWidth?: boolean;
|
|
30
|
+
/** Accessibility: with label → role="img" + <title>; without → aria-hidden. */
|
|
31
|
+
label?: string;
|
|
32
|
+
}
|
|
33
|
+
type ModeProps = Pick<MorphIconProps, "icon" | "from" | "to" | "progress">;
|
|
34
|
+
type WatchProps = ModeProps & {
|
|
35
|
+
spring?: SpringPreset | MorphOptions;
|
|
36
|
+
};
|
|
37
|
+
/** The initial d is constant for Svelte: computed once from the mount-time
|
|
38
|
+
* props (server and client produce the same string → hydration without
|
|
39
|
+
* mismatch) and from then on only the driver mutates it outside the template. */
|
|
40
|
+
declare function computeInitialD({ icon, from, to, progress }: ModeProps): string;
|
|
41
|
+
/** Per-instance driver state — the exact logic of the React/Vue bindings
|
|
42
|
+
* (mount, mode watch, controlled seek with re-basing, imperative). Takes the
|
|
43
|
+
* init-time props: they seed the watch baselines so the first `$effect` run
|
|
44
|
+
* (which fires on mount in Svelte, unlike Vue's `watch`) is a no-op. All
|
|
45
|
+
* change detection lives HERE, under tsc — the .svelte shell only wires
|
|
46
|
+
* runes to these methods.
|
|
47
|
+
*
|
|
48
|
+
* Lifecycle contract (shared verbatim by the three bindings):
|
|
49
|
+
* - Lazy driver: an iconless mount keeps the element and births the driver
|
|
50
|
+
* on the FIRST icon that shows up (prop or imperative). `morphTo` with no
|
|
51
|
+
* driver behaves as `set` — there is nothing to fly from.
|
|
52
|
+
* - Controlled wins: while `from` and `to` are both present the pair owns
|
|
53
|
+
* the path and `icon` changes are ignored; dropping the pair hands the
|
|
54
|
+
* path back to `icon`.
|
|
55
|
+
* - Every exit from controlled mode (imperative call or icon takeover)
|
|
56
|
+
* invalidates the frozen pair, so returning to it re-bases on `from`. */
|
|
57
|
+
declare function createController({ icon, from, to, progress }: ModeProps): {
|
|
58
|
+
mount(mountEl: PathEl, { icon, from, to, progress }: ModeProps): void;
|
|
59
|
+
destroy(): void;
|
|
60
|
+
/** Prop watcher: ONE owner decides per run — the controlled pair while it
|
|
61
|
+
* is fully present, `icon` otherwise (mount doesn't fire thanks to the
|
|
62
|
+
* init-time baselines). */
|
|
63
|
+
watch({ icon, from, to, progress, spring }: WatchProps): void;
|
|
64
|
+
morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
|
|
65
|
+
set(icon: IconInput): void;
|
|
66
|
+
};
|
|
67
|
+
//#endregion
|
|
68
|
+
export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIconProps, type MorphOptions, type PathEl, type Sampled, type SpringPreset, computeInitialD, createController };
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-BTcHiHR5.js";
|
|
2
|
+
import { canonicalD, createMorph } from "./dom.js";
|
|
3
|
+
//#region src/svelte/shared.ts
|
|
4
|
+
/** Frozen shape of the from→to pair at t, using the pure core (SSR-safe).
|
|
5
|
+
* At exact endpoints returns the canonical `d` (real curves, not polyline). */
|
|
6
|
+
function frozenD(from, to, t) {
|
|
7
|
+
if (t <= 0) return canonicalD(from);
|
|
8
|
+
if (t >= 1) return canonicalD(to);
|
|
9
|
+
const plan = buildPlan(resampleIcon(from), resampleIcon(to));
|
|
10
|
+
const out = allocOutputs(plan);
|
|
11
|
+
interpPolar(plan, t, out);
|
|
12
|
+
return serialize(out, plan.items.map((it) => it.closed));
|
|
13
|
+
}
|
|
14
|
+
/** The initial d is constant for Svelte: computed once from the mount-time
|
|
15
|
+
* props (server and client produce the same string → hydration without
|
|
16
|
+
* mismatch) and from then on only the driver mutates it outside the template. */
|
|
17
|
+
function computeInitialD({ icon, from, to, progress }) {
|
|
18
|
+
if (from !== void 0 && to !== void 0) return frozenD(from, to, progress ?? 0);
|
|
19
|
+
const first = icon ?? from ?? to;
|
|
20
|
+
return first !== void 0 ? canonicalD(first) : "";
|
|
21
|
+
}
|
|
22
|
+
/** Per-instance driver state — the exact logic of the React/Vue bindings
|
|
23
|
+
* (mount, mode watch, controlled seek with re-basing, imperative). Takes the
|
|
24
|
+
* init-time props: they seed the watch baselines so the first `$effect` run
|
|
25
|
+
* (which fires on mount in Svelte, unlike Vue's `watch`) is a no-op. All
|
|
26
|
+
* change detection lives HERE, under tsc — the .svelte shell only wires
|
|
27
|
+
* runes to these methods.
|
|
28
|
+
*
|
|
29
|
+
* Lifecycle contract (shared verbatim by the three bindings):
|
|
30
|
+
* - Lazy driver: an iconless mount keeps the element and births the driver
|
|
31
|
+
* on the FIRST icon that shows up (prop or imperative). `morphTo` with no
|
|
32
|
+
* driver behaves as `set` — there is nothing to fly from.
|
|
33
|
+
* - Controlled wins: while `from` and `to` are both present the pair owns
|
|
34
|
+
* the path and `icon` changes are ignored; dropping the pair hands the
|
|
35
|
+
* path back to `icon`.
|
|
36
|
+
* - Every exit from controlled mode (imperative call or icon takeover)
|
|
37
|
+
* invalidates the frozen pair, so returning to it re-bases on `from`. */
|
|
38
|
+
function createController({ icon, from, to, progress }) {
|
|
39
|
+
let el = null;
|
|
40
|
+
let dead = false;
|
|
41
|
+
let morph = null;
|
|
42
|
+
let based = false;
|
|
43
|
+
let pair = null;
|
|
44
|
+
let prevIcon = icon;
|
|
45
|
+
let prevControlled = from !== void 0 && to !== void 0;
|
|
46
|
+
let prevFrom = from;
|
|
47
|
+
let prevTo = to;
|
|
48
|
+
let prevProgress = progress;
|
|
49
|
+
/** Driver birth, lazy included: the first icon to show up creates it. */
|
|
50
|
+
const ensure = (birth) => {
|
|
51
|
+
if (morph) return morph;
|
|
52
|
+
if (dead || !el) return null;
|
|
53
|
+
morph = createMorph(el, birth);
|
|
54
|
+
return morph;
|
|
55
|
+
};
|
|
56
|
+
/** Controlled mode: freeze the pair at `progress` via seek (no spring). */
|
|
57
|
+
const applyPair = (from, to, progress) => {
|
|
58
|
+
const m = morph;
|
|
59
|
+
if (!m) return;
|
|
60
|
+
const t = progress ?? 0;
|
|
61
|
+
if (!pair || pair[0] !== from || pair[1] !== to) {
|
|
62
|
+
pair = [from, to];
|
|
63
|
+
based = false;
|
|
64
|
+
}
|
|
65
|
+
if (t <= 0) {
|
|
66
|
+
m.set(from);
|
|
67
|
+
based = false;
|
|
68
|
+
} else if (t >= 1) {
|
|
69
|
+
m.set(to);
|
|
70
|
+
based = false;
|
|
71
|
+
} else {
|
|
72
|
+
if (!based) {
|
|
73
|
+
m.set(from);
|
|
74
|
+
based = true;
|
|
75
|
+
}
|
|
76
|
+
m.seek(to, t);
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
return {
|
|
80
|
+
mount(mountEl, { icon, from, to, progress }) {
|
|
81
|
+
el = mountEl;
|
|
82
|
+
const controlled = from !== void 0 && to !== void 0;
|
|
83
|
+
const initialIcon = icon ?? from ?? to;
|
|
84
|
+
if (initialIcon === void 0) return;
|
|
85
|
+
const m = createMorph(el, controlled ? from : initialIcon);
|
|
86
|
+
morph = m;
|
|
87
|
+
if (controlled) {
|
|
88
|
+
pair = [from, to];
|
|
89
|
+
const t = progress ?? 0;
|
|
90
|
+
if (t <= 0) m.set(from);
|
|
91
|
+
else if (t >= 1) m.set(to);
|
|
92
|
+
else {
|
|
93
|
+
m.seek(to, t);
|
|
94
|
+
based = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
destroy() {
|
|
99
|
+
dead = true;
|
|
100
|
+
el = null;
|
|
101
|
+
morph?.destroy();
|
|
102
|
+
morph = null;
|
|
103
|
+
based = false;
|
|
104
|
+
pair = null;
|
|
105
|
+
},
|
|
106
|
+
/** Prop watcher: ONE owner decides per run — the controlled pair while it
|
|
107
|
+
* is fully present, `icon` otherwise (mount doesn't fire thanks to the
|
|
108
|
+
* init-time baselines). */
|
|
109
|
+
watch({ icon, from, to, progress, spring }) {
|
|
110
|
+
const controlled = from !== void 0 && to !== void 0;
|
|
111
|
+
const left = prevControlled && !controlled;
|
|
112
|
+
const iconChanged = icon !== prevIcon;
|
|
113
|
+
const pairChanged = from !== prevFrom || to !== prevTo || progress !== prevProgress;
|
|
114
|
+
prevControlled = controlled;
|
|
115
|
+
prevIcon = icon;
|
|
116
|
+
prevFrom = from;
|
|
117
|
+
prevTo = to;
|
|
118
|
+
prevProgress = progress;
|
|
119
|
+
if (controlled) {
|
|
120
|
+
if (!pairChanged) return;
|
|
121
|
+
if (!(morph ?? ensure(from))) return;
|
|
122
|
+
applyPair(from, to, progress);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
if (icon === void 0 || !iconChanged && !left) return;
|
|
126
|
+
pair = null;
|
|
127
|
+
based = false;
|
|
128
|
+
if (morph) morph.morphTo(icon, spring);
|
|
129
|
+
else ensure(icon);
|
|
130
|
+
},
|
|
131
|
+
morphTo(icon, spring) {
|
|
132
|
+
pair = null;
|
|
133
|
+
based = false;
|
|
134
|
+
if (morph) morph.morphTo(icon, spring);
|
|
135
|
+
else ensure(icon);
|
|
136
|
+
},
|
|
137
|
+
set(icon) {
|
|
138
|
+
pair = null;
|
|
139
|
+
based = false;
|
|
140
|
+
if (morph) morph.set(icon);
|
|
141
|
+
else ensure(icon);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
//#endregion
|
|
146
|
+
export { computeInitialD, createController };
|
package/dist/svelte.d.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Component } from "svelte";
|
|
2
|
+
import type { MorphHandle, MorphIconProps } from "./svelte-shared.js";
|
|
3
|
+
export declare const MorphIcon: Component<MorphIconProps, MorphHandle>;
|
|
4
|
+
export type { MorphHandle, MorphIconProps };
|
|
5
|
+
export type {
|
|
6
|
+
IconInput,
|
|
7
|
+
IconNode,
|
|
8
|
+
Morph,
|
|
9
|
+
MorphOptions,
|
|
10
|
+
PathEl,
|
|
11
|
+
Sampled,
|
|
12
|
+
SpringPreset,
|
|
13
|
+
} from "./svelte-shared.js";
|
package/dist/svelte.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default as MorphIcon } from "./MorphIcon.svelte";
|
package/dist/vue.d.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
|
|
2
|
+
import { Morph, MorphOptions, PathEl } from "./dom.js";
|
|
3
|
+
import { PropType } from "vue";
|
|
4
|
+
//#region src/vue/index.d.ts
|
|
5
|
+
/** Imperative surface exposed on the component instance (template ref). */
|
|
6
|
+
interface MorphHandle {
|
|
7
|
+
morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
|
|
8
|
+
set(icon: IconInput): void;
|
|
9
|
+
}
|
|
10
|
+
declare const MorphIcon: import("vue").DefineComponent<import("vue").ExtractPropTypes<{
|
|
11
|
+
/** Uncontrolled mode: the current icon; changing the prop animates. */
|
|
12
|
+
icon: {
|
|
13
|
+
type: PropType<IconInput>;
|
|
14
|
+
default: undefined;
|
|
15
|
+
};
|
|
16
|
+
/** Controlled mode: source endpoint of the pair. */
|
|
17
|
+
from: {
|
|
18
|
+
type: PropType<IconInput>;
|
|
19
|
+
default: undefined;
|
|
20
|
+
};
|
|
21
|
+
/** Controlled mode: target endpoint of the pair. */
|
|
22
|
+
to: {
|
|
23
|
+
type: PropType<IconInput>;
|
|
24
|
+
default: undefined;
|
|
25
|
+
};
|
|
26
|
+
/** Controlled mode: 0..1 progress of the frozen morph (no spring). */
|
|
27
|
+
progress: {
|
|
28
|
+
type: NumberConstructor;
|
|
29
|
+
default: undefined;
|
|
30
|
+
};
|
|
31
|
+
/** Physics for uncontrolled/imperative mode: preset or custom spring. */
|
|
32
|
+
spring: {
|
|
33
|
+
type: PropType<SpringPreset | MorphOptions>;
|
|
34
|
+
default: undefined;
|
|
35
|
+
};
|
|
36
|
+
size: {
|
|
37
|
+
type: (NumberConstructor | StringConstructor)[];
|
|
38
|
+
default: number;
|
|
39
|
+
};
|
|
40
|
+
color: {
|
|
41
|
+
type: StringConstructor;
|
|
42
|
+
default: string;
|
|
43
|
+
};
|
|
44
|
+
strokeWidth: {
|
|
45
|
+
type: (NumberConstructor | StringConstructor)[];
|
|
46
|
+
default: number;
|
|
47
|
+
};
|
|
48
|
+
/** Like lucide: stroke width does not scale with `size`. */
|
|
49
|
+
absoluteStrokeWidth: {
|
|
50
|
+
type: BooleanConstructor;
|
|
51
|
+
default: boolean;
|
|
52
|
+
};
|
|
53
|
+
/** Accessibility: with label → role="img" + <title>; without → aria-hidden. */
|
|
54
|
+
label: {
|
|
55
|
+
type: StringConstructor;
|
|
56
|
+
default: undefined;
|
|
57
|
+
};
|
|
58
|
+
}>, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
59
|
+
[key: string]: any;
|
|
60
|
+
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<import("vue").ExtractPropTypes<{
|
|
61
|
+
/** Uncontrolled mode: the current icon; changing the prop animates. */
|
|
62
|
+
icon: {
|
|
63
|
+
type: PropType<IconInput>;
|
|
64
|
+
default: undefined;
|
|
65
|
+
};
|
|
66
|
+
/** Controlled mode: source endpoint of the pair. */
|
|
67
|
+
from: {
|
|
68
|
+
type: PropType<IconInput>;
|
|
69
|
+
default: undefined;
|
|
70
|
+
};
|
|
71
|
+
/** Controlled mode: target endpoint of the pair. */
|
|
72
|
+
to: {
|
|
73
|
+
type: PropType<IconInput>;
|
|
74
|
+
default: undefined;
|
|
75
|
+
};
|
|
76
|
+
/** Controlled mode: 0..1 progress of the frozen morph (no spring). */
|
|
77
|
+
progress: {
|
|
78
|
+
type: NumberConstructor;
|
|
79
|
+
default: undefined;
|
|
80
|
+
};
|
|
81
|
+
/** Physics for uncontrolled/imperative mode: preset or custom spring. */
|
|
82
|
+
spring: {
|
|
83
|
+
type: PropType<SpringPreset | MorphOptions>;
|
|
84
|
+
default: undefined;
|
|
85
|
+
};
|
|
86
|
+
size: {
|
|
87
|
+
type: (NumberConstructor | StringConstructor)[];
|
|
88
|
+
default: number;
|
|
89
|
+
};
|
|
90
|
+
color: {
|
|
91
|
+
type: StringConstructor;
|
|
92
|
+
default: string;
|
|
93
|
+
};
|
|
94
|
+
strokeWidth: {
|
|
95
|
+
type: (NumberConstructor | StringConstructor)[];
|
|
96
|
+
default: number;
|
|
97
|
+
};
|
|
98
|
+
/** Like lucide: stroke width does not scale with `size`. */
|
|
99
|
+
absoluteStrokeWidth: {
|
|
100
|
+
type: BooleanConstructor;
|
|
101
|
+
default: boolean;
|
|
102
|
+
};
|
|
103
|
+
/** Accessibility: with label → role="img" + <title>; without → aria-hidden. */
|
|
104
|
+
label: {
|
|
105
|
+
type: StringConstructor;
|
|
106
|
+
default: undefined;
|
|
107
|
+
};
|
|
108
|
+
}>> & Readonly<{}>, {
|
|
109
|
+
icon: IconInput;
|
|
110
|
+
from: IconInput;
|
|
111
|
+
to: IconInput;
|
|
112
|
+
progress: number;
|
|
113
|
+
spring: "bouncy" | "smooth" | "snappy" | MorphOptions;
|
|
114
|
+
size: string | number;
|
|
115
|
+
color: string;
|
|
116
|
+
strokeWidth: string | number;
|
|
117
|
+
absoluteStrokeWidth: boolean;
|
|
118
|
+
label: string;
|
|
119
|
+
}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|
|
120
|
+
//#endregion
|
|
121
|
+
export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIcon, type MorphOptions, type PathEl, type Sampled, type SpringPreset };
|
package/dist/vue.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import { a as buildPlan, f as interpPolar, l as serialize, r as resampleIcon, u as allocOutputs } from "./spring-BTcHiHR5.js";
|
|
2
|
+
import { canonicalD, createMorph } from "./dom.js";
|
|
3
|
+
import { defineComponent, h, onBeforeUnmount, onMounted, shallowRef, watch } from "vue";
|
|
4
|
+
//#region src/vue/index.ts
|
|
5
|
+
/** Frozen shape of the from→to pair at t, using the pure core (SSR-safe).
|
|
6
|
+
* At exact endpoints returns the canonical `d` (real curves, not polyline). */
|
|
7
|
+
function frozenD(from, to, t) {
|
|
8
|
+
if (t <= 0) return canonicalD(from);
|
|
9
|
+
if (t >= 1) return canonicalD(to);
|
|
10
|
+
const plan = buildPlan(resampleIcon(from), resampleIcon(to));
|
|
11
|
+
const out = allocOutputs(plan);
|
|
12
|
+
interpPolar(plan, t, out);
|
|
13
|
+
return serialize(out, plan.items.map((it) => it.closed));
|
|
14
|
+
}
|
|
15
|
+
const MorphIcon = defineComponent({
|
|
16
|
+
name: "MorphIcon",
|
|
17
|
+
props: {
|
|
18
|
+
/** Uncontrolled mode: the current icon; changing the prop animates. */
|
|
19
|
+
icon: {
|
|
20
|
+
type: [String, Array],
|
|
21
|
+
default: void 0
|
|
22
|
+
},
|
|
23
|
+
/** Controlled mode: source endpoint of the pair. */
|
|
24
|
+
from: {
|
|
25
|
+
type: [String, Array],
|
|
26
|
+
default: void 0
|
|
27
|
+
},
|
|
28
|
+
/** Controlled mode: target endpoint of the pair. */
|
|
29
|
+
to: {
|
|
30
|
+
type: [String, Array],
|
|
31
|
+
default: void 0
|
|
32
|
+
},
|
|
33
|
+
/** Controlled mode: 0..1 progress of the frozen morph (no spring). */
|
|
34
|
+
progress: {
|
|
35
|
+
type: Number,
|
|
36
|
+
default: void 0
|
|
37
|
+
},
|
|
38
|
+
/** Physics for uncontrolled/imperative mode: preset or custom spring. */
|
|
39
|
+
spring: {
|
|
40
|
+
type: [String, Object],
|
|
41
|
+
default: void 0
|
|
42
|
+
},
|
|
43
|
+
size: {
|
|
44
|
+
type: [Number, String],
|
|
45
|
+
default: 24
|
|
46
|
+
},
|
|
47
|
+
color: {
|
|
48
|
+
type: String,
|
|
49
|
+
default: "currentColor"
|
|
50
|
+
},
|
|
51
|
+
strokeWidth: {
|
|
52
|
+
type: [Number, String],
|
|
53
|
+
default: 2
|
|
54
|
+
},
|
|
55
|
+
/** Like lucide: stroke width does not scale with `size`. */
|
|
56
|
+
absoluteStrokeWidth: {
|
|
57
|
+
type: Boolean,
|
|
58
|
+
default: false
|
|
59
|
+
},
|
|
60
|
+
/** Accessibility: with label → role="img" + <title>; without → aria-hidden. */
|
|
61
|
+
label: {
|
|
62
|
+
type: String,
|
|
63
|
+
default: void 0
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
setup(props, { expose }) {
|
|
67
|
+
const initialD = (() => {
|
|
68
|
+
const { icon, from, to, progress } = props;
|
|
69
|
+
if (from !== void 0 && to !== void 0) return frozenD(from, to, progress ?? 0);
|
|
70
|
+
const first = icon ?? from ?? to;
|
|
71
|
+
return first !== void 0 ? canonicalD(first) : "";
|
|
72
|
+
})();
|
|
73
|
+
const pathEl = shallowRef(null);
|
|
74
|
+
let morph = null;
|
|
75
|
+
let dead = false;
|
|
76
|
+
let based = false;
|
|
77
|
+
let pair = null;
|
|
78
|
+
let prevIcon = props.icon;
|
|
79
|
+
let prevControlled = props.from !== void 0 && props.to !== void 0;
|
|
80
|
+
/** Driver birth, lazy included (#1 of the lifecycle contract): an
|
|
81
|
+
* iconless mount keeps the element and the FIRST icon to show up (prop
|
|
82
|
+
* or imperative) creates the driver, already showing it — no flight. */
|
|
83
|
+
const ensure = (birth) => {
|
|
84
|
+
if (morph) return morph;
|
|
85
|
+
const el = pathEl.value;
|
|
86
|
+
if (dead || !el) return null;
|
|
87
|
+
morph = createMorph(el, birth);
|
|
88
|
+
return morph;
|
|
89
|
+
};
|
|
90
|
+
onMounted(() => {
|
|
91
|
+
const el = pathEl.value;
|
|
92
|
+
const { icon, from, to, progress } = props;
|
|
93
|
+
const controlled = from !== void 0 && to !== void 0;
|
|
94
|
+
const initialIcon = icon ?? from ?? to;
|
|
95
|
+
if (!el || initialIcon === void 0) return;
|
|
96
|
+
const m = createMorph(el, controlled ? from : initialIcon);
|
|
97
|
+
morph = m;
|
|
98
|
+
if (controlled) {
|
|
99
|
+
pair = [from, to];
|
|
100
|
+
const t = progress ?? 0;
|
|
101
|
+
if (t <= 0) m.set(from);
|
|
102
|
+
else if (t >= 1) m.set(to);
|
|
103
|
+
else {
|
|
104
|
+
m.seek(to, t);
|
|
105
|
+
based = true;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
onBeforeUnmount(() => {
|
|
110
|
+
dead = true;
|
|
111
|
+
morph?.destroy();
|
|
112
|
+
morph = null;
|
|
113
|
+
based = false;
|
|
114
|
+
pair = null;
|
|
115
|
+
});
|
|
116
|
+
watch(() => [
|
|
117
|
+
props.icon,
|
|
118
|
+
props.from,
|
|
119
|
+
props.to
|
|
120
|
+
], () => {
|
|
121
|
+
const { icon, from, to } = props;
|
|
122
|
+
const controlled = from !== void 0 && to !== void 0;
|
|
123
|
+
const left = prevControlled && !controlled;
|
|
124
|
+
prevControlled = controlled;
|
|
125
|
+
const changed = icon !== prevIcon;
|
|
126
|
+
prevIcon = icon;
|
|
127
|
+
if (controlled) return;
|
|
128
|
+
if (icon === void 0 || !changed && !left) return;
|
|
129
|
+
pair = null;
|
|
130
|
+
based = false;
|
|
131
|
+
if (morph) morph.morphTo(icon, props.spring);
|
|
132
|
+
else ensure(icon);
|
|
133
|
+
});
|
|
134
|
+
watch(() => [
|
|
135
|
+
props.from,
|
|
136
|
+
props.to,
|
|
137
|
+
props.progress
|
|
138
|
+
], () => {
|
|
139
|
+
const { from, to, progress } = props;
|
|
140
|
+
if (from === void 0 || to === void 0) return;
|
|
141
|
+
const m = morph ?? ensure(from);
|
|
142
|
+
if (!m) return;
|
|
143
|
+
const t = progress ?? 0;
|
|
144
|
+
if (!pair || pair[0] !== from || pair[1] !== to) {
|
|
145
|
+
pair = [from, to];
|
|
146
|
+
based = false;
|
|
147
|
+
}
|
|
148
|
+
if (t <= 0) {
|
|
149
|
+
m.set(from);
|
|
150
|
+
based = false;
|
|
151
|
+
} else if (t >= 1) {
|
|
152
|
+
m.set(to);
|
|
153
|
+
based = false;
|
|
154
|
+
} else {
|
|
155
|
+
if (!based) {
|
|
156
|
+
m.set(from);
|
|
157
|
+
based = true;
|
|
158
|
+
}
|
|
159
|
+
m.seek(to, t);
|
|
160
|
+
}
|
|
161
|
+
});
|
|
162
|
+
expose({
|
|
163
|
+
morphTo: (i, s) => {
|
|
164
|
+
pair = null;
|
|
165
|
+
based = false;
|
|
166
|
+
if (morph) morph.morphTo(i, s ?? props.spring);
|
|
167
|
+
else ensure(i);
|
|
168
|
+
},
|
|
169
|
+
set: (i) => {
|
|
170
|
+
pair = null;
|
|
171
|
+
based = false;
|
|
172
|
+
if (morph) morph.set(i);
|
|
173
|
+
else ensure(i);
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
return () => {
|
|
177
|
+
const sw = props.absoluteStrokeWidth ? Number(props.strokeWidth) * 24 / Number(props.size) : props.strokeWidth;
|
|
178
|
+
return h("svg", {
|
|
179
|
+
xmlns: "http://www.w3.org/2000/svg",
|
|
180
|
+
width: props.size,
|
|
181
|
+
height: props.size,
|
|
182
|
+
viewBox: "0 0 24 24",
|
|
183
|
+
fill: "none",
|
|
184
|
+
stroke: props.color,
|
|
185
|
+
"stroke-width": sw,
|
|
186
|
+
"stroke-linecap": "round",
|
|
187
|
+
"stroke-linejoin": "round",
|
|
188
|
+
role: props.label ? "img" : void 0,
|
|
189
|
+
"aria-hidden": props.label ? void 0 : "true"
|
|
190
|
+
}, [props.label ? h("title", props.label) : null, h("path", {
|
|
191
|
+
ref: pathEl,
|
|
192
|
+
d: initialD
|
|
193
|
+
})]);
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
//#endregion
|
|
198
|
+
export { MorphIcon };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morphicons",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Universal morphing for stroke-based icons with spring physics — zero runtime dependencies",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "Guillermo (https://guillermolg.com)",
|
|
@@ -29,6 +29,12 @@
|
|
|
29
29
|
"shadcn",
|
|
30
30
|
"feather",
|
|
31
31
|
"react",
|
|
32
|
+
"react-native",
|
|
33
|
+
"lucide-react-native",
|
|
34
|
+
"vue",
|
|
35
|
+
"lucide-vue-next",
|
|
36
|
+
"svelte",
|
|
37
|
+
"lucide-svelte",
|
|
32
38
|
"icon-animation"
|
|
33
39
|
],
|
|
34
40
|
"type": "module",
|
|
@@ -48,14 +54,43 @@
|
|
|
48
54
|
"./react": {
|
|
49
55
|
"types": "./dist/react.d.ts",
|
|
50
56
|
"default": "./dist/react.js"
|
|
57
|
+
},
|
|
58
|
+
"./react-native": {
|
|
59
|
+
"types": "./dist/react-native.d.ts",
|
|
60
|
+
"default": "./dist/react-native.js"
|
|
61
|
+
},
|
|
62
|
+
"./vue": {
|
|
63
|
+
"types": "./dist/vue.d.ts",
|
|
64
|
+
"default": "./dist/vue.js"
|
|
65
|
+
},
|
|
66
|
+
"./svelte": {
|
|
67
|
+
"types": "./dist/svelte.d.ts",
|
|
68
|
+
"svelte": "./dist/svelte.js",
|
|
69
|
+
"default": "./dist/svelte.js"
|
|
51
70
|
}
|
|
52
71
|
},
|
|
53
72
|
"peerDependencies": {
|
|
54
|
-
"react": ">=18"
|
|
73
|
+
"react": ">=18",
|
|
74
|
+
"react-native": ">=0.71",
|
|
75
|
+
"react-native-svg": ">=14",
|
|
76
|
+
"svelte": ">=5",
|
|
77
|
+
"vue": ">=3.3"
|
|
55
78
|
},
|
|
56
79
|
"peerDependenciesMeta": {
|
|
57
80
|
"react": {
|
|
58
81
|
"optional": true
|
|
82
|
+
},
|
|
83
|
+
"react-native": {
|
|
84
|
+
"optional": true
|
|
85
|
+
},
|
|
86
|
+
"react-native-svg": {
|
|
87
|
+
"optional": true
|
|
88
|
+
},
|
|
89
|
+
"svelte": {
|
|
90
|
+
"optional": true
|
|
91
|
+
},
|
|
92
|
+
"vue": {
|
|
93
|
+
"optional": true
|
|
59
94
|
}
|
|
60
95
|
},
|
|
61
96
|
"files": [
|
|
@@ -63,11 +98,11 @@
|
|
|
63
98
|
],
|
|
64
99
|
"scripts": {
|
|
65
100
|
"test": "bun test",
|
|
66
|
-
"typecheck": "tsc --noEmit && tsc -p playground --noEmit && tsc -p tsconfig.react.json --noEmit",
|
|
101
|
+
"typecheck": "tsc --noEmit && tsc -p playground --noEmit && tsc -p tsconfig.react.json --noEmit && tsc -p tsconfig.react-native.json --noEmit && tsc -p tsconfig.vue.json --noEmit && tsc -p tsconfig.svelte.json --noEmit",
|
|
67
102
|
"play": "bun ./playground/index.html",
|
|
68
103
|
"lint": "biome check .",
|
|
69
104
|
"format": "biome check --write .",
|
|
70
|
-
"build": "tsdown",
|
|
105
|
+
"build": "tsdown && bun scripts/build-svelte.ts",
|
|
71
106
|
"size": "size-limit",
|
|
72
107
|
"prepublishOnly": "bun run build"
|
|
73
108
|
},
|
|
@@ -95,21 +130,68 @@
|
|
|
95
130
|
],
|
|
96
131
|
"limit": "8.5 KB",
|
|
97
132
|
"gzip": true
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
"name": "core + dom + react-native (MorphIcon, react/rn/rnsvg external)",
|
|
136
|
+
"path": "dist/react-native.js",
|
|
137
|
+
"import": "*",
|
|
138
|
+
"ignore": [
|
|
139
|
+
"react",
|
|
140
|
+
"react-native",
|
|
141
|
+
"react-native-svg"
|
|
142
|
+
],
|
|
143
|
+
"limit": "9 KB",
|
|
144
|
+
"gzip": true
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
"name": "core + dom + vue (MorphIcon, vue external)",
|
|
148
|
+
"path": "dist/vue.js",
|
|
149
|
+
"import": "*",
|
|
150
|
+
"ignore": [
|
|
151
|
+
"vue"
|
|
152
|
+
],
|
|
153
|
+
"limit": "8.5 KB",
|
|
154
|
+
"gzip": true
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
"name": "core + dom + svelte TS half (svelte external)",
|
|
158
|
+
"path": "dist/svelte-shared.js",
|
|
159
|
+
"import": "*",
|
|
160
|
+
"ignore": [
|
|
161
|
+
"svelte"
|
|
162
|
+
],
|
|
163
|
+
"limit": "8 KB",
|
|
164
|
+
"gzip": true
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
"name": "svelte component (ships as source, consumer-compiled)",
|
|
168
|
+
"path": "dist/MorphIcon.svelte",
|
|
169
|
+
"disablePlugins": [
|
|
170
|
+
"@size-limit/esbuild"
|
|
171
|
+
],
|
|
172
|
+
"limit": "1.4 KB",
|
|
173
|
+
"gzip": true
|
|
98
174
|
}
|
|
99
175
|
],
|
|
100
176
|
"devDependencies": {
|
|
101
177
|
"@biomejs/biome": "^2.5.6",
|
|
178
|
+
"@happy-dom/global-registrator": "^20.11.1",
|
|
102
179
|
"@size-limit/preset-small-lib": "^13.0.3",
|
|
103
180
|
"@tabler/icons": "^3.46.0",
|
|
104
181
|
"@types/bun": "^1.3.14",
|
|
105
182
|
"@types/react": "^19.2.18",
|
|
106
183
|
"@types/react-dom": "^19.2.4",
|
|
107
184
|
"feather-icons": "^4.29.2",
|
|
185
|
+
"happy-dom": "^20.11.1",
|
|
108
186
|
"lucide": "^1.28.0",
|
|
109
187
|
"react": "^19.2.8",
|
|
110
188
|
"react-dom": "^19.2.8",
|
|
189
|
+
"react-native": "^0.86.2",
|
|
190
|
+
"react-native-svg": "^15.15.5",
|
|
111
191
|
"size-limit": "^13.0.3",
|
|
192
|
+
"svelte": "^5.56.8",
|
|
112
193
|
"tsdown": "^0.22.14",
|
|
113
|
-
"typescript": "^7.0.2"
|
|
194
|
+
"typescript": "^7.0.2",
|
|
195
|
+
"vue": "^3.5.40"
|
|
114
196
|
}
|
|
115
197
|
}
|