morphicons 1.5.0 → 1.7.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 +88 -7
- package/dist/MorphIcon.astro +109 -0
- package/dist/adapters.d.ts +45 -1
- package/dist/adapters.js +43 -1
- package/dist/astro.d.ts +32 -0
- package/dist/controller-CXZuwJ_M.js +152 -0
- package/dist/controller-Dkruvf3Y.d.ts +54 -0
- package/dist/element.d.ts +36 -0
- package/dist/element.js +261 -0
- package/dist/svelte-shared.d.ts +2 -41
- package/dist/svelte-shared.js +1 -150
- package/package.json +39 -4
package/README.md
CHANGED
|
@@ -43,7 +43,7 @@ The usual icon morphs either interpolate raw coordinates (shapes shrink and shea
|
|
|
43
43
|
bun add morphicons # or npm install / pnpm add
|
|
44
44
|
```
|
|
45
45
|
|
|
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`.
|
|
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`. `morphicons/element` (the `<morph-icon>` custom element) and `morphicons/astro` (its SSR shell) need no peer at all — Astro compiles the shell itself, like every `.astro` component shipped as source.
|
|
47
47
|
|
|
48
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
49
|
|
|
@@ -67,7 +67,7 @@ ref.current?.morphTo(Check); // animates
|
|
|
67
67
|
ref.current?.set(X); // jumps without animating
|
|
68
68
|
```
|
|
69
69
|
|
|
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-
|
|
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-five-bindings)).
|
|
71
71
|
|
|
72
72
|
### Vue — same three modes
|
|
73
73
|
|
|
@@ -143,15 +143,54 @@ ref.current?.set(X); // jumps without animating
|
|
|
143
143
|
|
|
144
144
|
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`). With `reducedMotion="user"`, the OS setting 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`.
|
|
145
145
|
|
|
146
|
-
###
|
|
146
|
+
### Astro — same three modes, zero framework runtime
|
|
147
147
|
|
|
148
|
-
|
|
148
|
+
```astro
|
|
149
|
+
---
|
|
150
|
+
import MorphIcon from "morphicons/astro";
|
|
151
|
+
import { Menu } from "lucide"; // data, not components
|
|
152
|
+
---
|
|
153
|
+
|
|
154
|
+
<MorphIcon icon={Menu} label="Menu" id="menu-icon" />
|
|
155
|
+
|
|
156
|
+
<script>
|
|
157
|
+
import { X } from "lucide";
|
|
158
|
+
import type { MorphIconElement } from "morphicons/element";
|
|
159
|
+
// Post-load control happens ON the element (it upgrades to <morph-icon>):
|
|
160
|
+
const el = document.querySelector<MorphIconElement>("#menu-icon")!;
|
|
161
|
+
el.morphTo(X); // 3. imperative: morphTo / set, like a ref
|
|
162
|
+
// el.icon = X; // 1. uncontrolled: assigning the property animates
|
|
163
|
+
// el.from = Menu; el.to = X; el.progress = 0.5; // 2. controlled: frozen seek
|
|
164
|
+
</script>
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
The server emits the exact static SVG with the pure core (zero flash, zero layout shift, works with static output and any SSR adapter) and hydration is just custom-element upgrade: the shell defines `<morph-icon>` once per page and the element adopts the server markup verbatim — zero `d` writes during upgrade, pinned by instrumented tests. No framework runtime ships — the only client bytes are `morphicons/element`. A controlled pair of `d` strings survives upgrade as attributes, so assigning `progress` from a script scrubs the server-frozen pair; `IconNode` pairs are server-only (attributes can't carry them) — control those by assigning the element's properties. Same presentation props as every binding (`size`, `strokeWidth`, `absoluteStrokeWidth`, `color`, `label`); rest attrs (`id`, `class`, `data-*`…) land on the `<morph-icon>` element, because grabbing it from a client script is the interaction model here. React/Vue/Svelte islands keep working for icons that live inside one — this entry is for the pages that don't need an island at all.
|
|
168
|
+
|
|
169
|
+
### Web component — `<morph-icon>` anywhere HTML reaches
|
|
170
|
+
|
|
171
|
+
The Astro shell is a thin SSR layer over `morphicons/element`, which stands on its own in plain HTML, HTMX, Rails, or any server-rendered stack:
|
|
172
|
+
|
|
173
|
+
```html
|
|
174
|
+
<script type="module">
|
|
175
|
+
import { defineMorphIcon } from "morphicons/element";
|
|
176
|
+
defineMorphIcon(); // idempotent; custom tag: defineMorphIcon("my-icon")
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<!-- attributes take d strings; properties take the full IconInput -->
|
|
180
|
+
<morph-icon icon="M4 6h16M4 12h16M4 18h16" label="Menu"></morph-icon>
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
The element is the fifth binding, not a second implementation: it wires attributes and properties to the same shared controller, passes the same mirrored mount suite, and exposes the same three modes — `icon` (uncontrolled), `from`/`to`/`progress` (controlled), `morphTo`/`set` methods (imperative, the element IS the handle). Attributes carry strings (`d` paths, preset names); properties accept everything (`IconNode`s, custom springs). If the element already contains a server-rendered `<svg><path>`, it adopts it verbatim and treats its `d` as the at-rest icon — SSR bytes are never rewritten at rest.
|
|
184
|
+
|
|
185
|
+
### Lifecycle contract (all five bindings)
|
|
186
|
+
|
|
187
|
+
The five components share one contract, pinned by mirrored client-mount tests:
|
|
149
188
|
|
|
150
189
|
- **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).
|
|
151
190
|
- **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.
|
|
152
191
|
- **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`.
|
|
153
192
|
|
|
154
|
-
### Reduced motion (all
|
|
193
|
+
### Reduced motion (all five bindings)
|
|
155
194
|
|
|
156
195
|
Icon morphs are small, short, communicative micro-transitions: the kind of motion the reduce-motion guidance considers generally acceptable, unlike parallax or full-screen movement. Auto-degrading them made the library look broken to every user with the OS setting on, so since 1.4.2 they play by default and the policy is an explicit prop (the same default Motion, formerly Framer Motion, ships):
|
|
157
196
|
|
|
@@ -230,6 +269,44 @@ It *is* `createMorph` — only the write target differs — so springs, reduced
|
|
|
230
269
|
|
|
231
270
|
**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
271
|
|
|
272
|
+
### `canvasTarget` — 2D canvas out
|
|
273
|
+
|
|
274
|
+
For surfaces where the DOM never enters: **the icon stops being a document node and becomes pixels you own.** The platform does the heavy lifting — `Path2D` parses `d` strings natively — so every frame is one `stroke(new Path2D(d))` on whatever you hand it: an `HTMLCanvasElement`, an `OffscreenCanvas`, or a bare 2D context (what a worker, or a host that already owns the context, has in hand).
|
|
275
|
+
|
|
276
|
+
```ts
|
|
277
|
+
import { canvasTarget } from "morphicons/adapters";
|
|
278
|
+
import { createMorph } from "morphicons/dom";
|
|
279
|
+
|
|
280
|
+
const m = createMorph(canvasTarget(canvas), MENU);
|
|
281
|
+
m.morphTo(X, "snappy");
|
|
282
|
+
// the canvas is a live texture: gl.texImage2D(gl.TEXTURE_2D, ..., canvas)
|
|
283
|
+
```
|
|
284
|
+
|
|
285
|
+
Once the icon is pixels, everything the SVG model can't reach opens up:
|
|
286
|
+
|
|
287
|
+
- **Canvas-first apps** with no DOM to put an icon in: Konva, Fabric.js, PixiJS, games, chart draws, tldraw-style editors.
|
|
288
|
+
- **`OffscreenCanvas` + workers**: the whole morph runs off the main thread.
|
|
289
|
+
- **Native export**: `toBlob()` for PNG sprites, `captureStream()` + MediaRecorder for the morph as a video asset.
|
|
290
|
+
- **An animated favicon**: draw, `toDataURL()`, swap the `<link>`.
|
|
291
|
+
- **HUDs over video or fullscreen surfaces**: same buffer as the player or the game, no DOM layer on top.
|
|
292
|
+
- **WebGL textures**: the canvas is a ready `texImage2D` source — shader effects over live icons are one branch of this list, not its headline.
|
|
293
|
+
- Down the road, the same gesture (`PathEl` → `Path2D`) is `PathEl` → `SkPath`: a react-native-skia / CanvasKit target would consume the identical contract.
|
|
294
|
+
|
|
295
|
+
It *is* `createMorph` — springs, mid-flight interruption and reduced motion behave identically. The target maps the icon grid onto whatever backing store it finds, per write: min-side scale, centered (the canvas equivalent of `preserveAspectRatio="xMidYMid meet"`), so non-square canvases letterbox and devicePixelRatio is simply whatever you sized the backing store to. Color cascades: the `color` option wins; without it, a CSS-styled canvas contributes its computed `color` (the moral equivalent of `currentColor`, which canvas lacks — read lazily, once); where CSS can't reach (OffscreenCanvas, workers) the context's current `strokeStyle` stands, which is the idiomatic canvas contract. `strokeWidth` and `viewBox` mirror the other adapters; `clear: false` skips the per-frame clear for manual compositing and trails.
|
|
296
|
+
|
|
297
|
+
**Hosts with their own render loop.** Don't hand a Konva/Chart.js/game context to a morph — the driver writes on its own rAF and the two clocks fight over the frame. Give the icon a small dedicated canvas and composite it into the scene (`drawImage`, `Konva.Image`, a texture upload), with `onWrite` as the dirty signal:
|
|
298
|
+
|
|
299
|
+
```ts
|
|
300
|
+
const icon = new OffscreenCanvas(64, 64);
|
|
301
|
+
const target = canvasTarget(icon, {
|
|
302
|
+
color: "#e8eaed", // CSS can't reach an OffscreenCanvas
|
|
303
|
+
onWrite: () => { textureDirty = true } // re-upload / blit on the host's next frame
|
|
304
|
+
});
|
|
305
|
+
const m = createMorph(target, PLAY);
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
**Cost.** The cheapest target of the three: no layout, no DOM paint — one native path parse and one stroke rasterization per frame, on a canvas that is typically small. 0.48 KB gzip standalone, the lightest adapter yet.
|
|
309
|
+
|
|
233
310
|
## Icon library compatibility
|
|
234
311
|
|
|
235
312
|
morphicons has no per-library adapters — any icon set that meets these requirements works out of the box:
|
|
@@ -257,6 +334,7 @@ This also covers the [shadcn registry](https://www.shadcn.io/icons), which re-pu
|
|
|
257
334
|
```
|
|
258
335
|
┌─────────────────────────────────────────────┐
|
|
259
336
|
│ bindings react · vue · svelte · rn │ thin: ref + createMorph
|
|
337
|
+
│ · element (+ astro SSR shell) │
|
|
260
338
|
├─────────────────────────────────────────────┤
|
|
261
339
|
│ drivers dom (setAttribute + rAF) │ singleton scheduler
|
|
262
340
|
├─────────────────────────────────────────────┤
|
|
@@ -267,7 +345,7 @@ This also covers the [shadcn registry](https://www.shadcn.io/icons), which re-pu
|
|
|
267
345
|
└─────────────────────────────────────────────┘
|
|
268
346
|
```
|
|
269
347
|
|
|
270
|
-
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`.
|
|
348
|
+
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`, `./element`, `./astro`), ESM only, `sideEffects: false`.
|
|
271
349
|
|
|
272
350
|
The plan is the central artifact:
|
|
273
351
|
|
|
@@ -407,14 +485,17 @@ CI budget (size-limit, gzip) as an anti-regression tripwire — gates carry ~10%
|
|
|
407
485
|
|---|---|---|
|
|
408
486
|
| `morphicons` (core) | 6.60 KB | 7 KB |
|
|
409
487
|
| `morphicons/dom` (core + driver) | 7.12 KB | 7.5 KB |
|
|
410
|
-
| `morphicons/adapters` (aggregate — grows by design) | 4.
|
|
488
|
+
| `morphicons/adapters` (aggregate — grows by design) | 4.32 KB | 4.8 KB |
|
|
411
489
|
| `adapters` › `{ svgToIcon }` alone | 3.34 KB | 3.7 KB |
|
|
412
490
|
| `adapters` › `{ maskTarget }` alone | 0.70 KB | 0.8 KB |
|
|
491
|
+
| `adapters` › `{ canvasTarget }` alone | 0.48 KB | 0.55 KB |
|
|
413
492
|
| `morphicons/react` (all, react external) | 8.01 KB | 8.5 KB |
|
|
414
493
|
| `morphicons/react-native` (all, react/rn/rnsvg external) | 8.37 KB | 9 KB |
|
|
415
494
|
| `morphicons/vue` (all, vue external) | 8.04 KB | 8.5 KB |
|
|
416
495
|
| `morphicons/svelte` TS half (all, svelte external) | 7.66 KB | 8 KB |
|
|
417
496
|
| `MorphIcon.svelte` (ships as source, consumer-compiled) | 1.26 KB | 1.4 KB |
|
|
497
|
+
| `morphicons/element` (all, `<morph-icon>`) | 8.86 KB | 9.5 KB |
|
|
498
|
+
| `MorphIcon.astro` (ships as source, consumer-compiled) | 1.58 KB | 1.75 KB |
|
|
418
499
|
|
|
419
500
|
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.
|
|
420
501
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
---
|
|
2
|
+
/* Astro binding: SSR shell over the <morph-icon> custom element. The server
|
|
3
|
+
emits the exact static SVG with the pure core (zero flash, zero layout
|
|
4
|
+
shift, no framework runtime) and the element adopts it verbatim on
|
|
5
|
+
upgrade; the processed <script> below defines the element once per page,
|
|
6
|
+
so hydration IS custom-element upgrade. Post-load control (icon swaps,
|
|
7
|
+
controlled scrubbing, imperative calls) happens on the element itself:
|
|
8
|
+
grab it from any client script and assign properties or call morphTo/set.
|
|
9
|
+
Thin shell on purpose, like MorphIcon.svelte: all logic lives in
|
|
10
|
+
typechecked modules; this file only wires props to markup. */
|
|
11
|
+
import type {
|
|
12
|
+
IconInput,
|
|
13
|
+
MorphOptions,
|
|
14
|
+
ReducedMotionMode,
|
|
15
|
+
SpringPreset,
|
|
16
|
+
} from "./element.js";
|
|
17
|
+
import { computeInitialD } from "./element.js";
|
|
18
|
+
|
|
19
|
+
interface Props {
|
|
20
|
+
/** Uncontrolled mode: the icon at rest (IconNode or `d` string). */
|
|
21
|
+
icon?: IconInput;
|
|
22
|
+
/** Controlled mode: source endpoint of the pair (server-side freeze). */
|
|
23
|
+
from?: IconInput;
|
|
24
|
+
/** Controlled mode: target endpoint of the pair (server-side freeze). */
|
|
25
|
+
to?: IconInput;
|
|
26
|
+
/** Controlled mode: 0..1 progress of the frozen morph. */
|
|
27
|
+
progress?: number;
|
|
28
|
+
/** Physics for client-side morphs: preset name or custom spring (presets
|
|
29
|
+
* ride the attribute; custom springs need a client script). */
|
|
30
|
+
spring?: SpringPreset | MorphOptions;
|
|
31
|
+
reducedMotion?: ReducedMotionMode;
|
|
32
|
+
size?: number | string;
|
|
33
|
+
color?: string;
|
|
34
|
+
strokeWidth?: number | string;
|
|
35
|
+
/** Like lucide: stroke width does not scale with `size`. */
|
|
36
|
+
absoluteStrokeWidth?: boolean;
|
|
37
|
+
/** Accessibility: with label → role="img" + <title>; without → aria-hidden. */
|
|
38
|
+
label?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const {
|
|
42
|
+
icon,
|
|
43
|
+
from,
|
|
44
|
+
to,
|
|
45
|
+
progress,
|
|
46
|
+
spring,
|
|
47
|
+
reducedMotion,
|
|
48
|
+
size = 24,
|
|
49
|
+
color = "currentColor",
|
|
50
|
+
strokeWidth = 2,
|
|
51
|
+
absoluteStrokeWidth = false,
|
|
52
|
+
label,
|
|
53
|
+
...rest
|
|
54
|
+
} = Astro.props;
|
|
55
|
+
|
|
56
|
+
const initialD = computeInitialD({ icon, from, to, progress });
|
|
57
|
+
const sw = absoluteStrokeWidth ? (Number(strokeWidth) * 24) / Number(size) : strokeWidth;
|
|
58
|
+
|
|
59
|
+
/* String pairs ride attributes so `progress` keeps scrubbing the frozen
|
|
60
|
+
pair after upgrade. BOTH or none — half a mixed pair would mount from the
|
|
61
|
+
lone endpoint (docs/adr/0003). IconNode pairs stay server-only. */
|
|
62
|
+
const stringPair = typeof from === "string" && typeof to === "string";
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
{/* Rest props land on <morph-icon>, not the svg: grabbing the element by
|
|
66
|
+
id/class/data-* from a client script IS the interaction model here. */}
|
|
67
|
+
<morph-icon
|
|
68
|
+
icon={typeof icon === "string" ? icon : undefined}
|
|
69
|
+
from={stringPair ? from : undefined}
|
|
70
|
+
to={stringPair ? to : undefined}
|
|
71
|
+
progress={progress}
|
|
72
|
+
spring={typeof spring === "string" ? spring : undefined}
|
|
73
|
+
reduced-motion={reducedMotion}
|
|
74
|
+
size={String(size)}
|
|
75
|
+
color={color}
|
|
76
|
+
stroke-width={String(strokeWidth)}
|
|
77
|
+
absolute-stroke-width={absoluteStrokeWidth ? "" : undefined}
|
|
78
|
+
label={label}
|
|
79
|
+
{...rest}
|
|
80
|
+
>
|
|
81
|
+
<svg
|
|
82
|
+
xmlns="http://www.w3.org/2000/svg"
|
|
83
|
+
width={size}
|
|
84
|
+
height={size}
|
|
85
|
+
viewBox="0 0 24 24"
|
|
86
|
+
fill="none"
|
|
87
|
+
stroke={color}
|
|
88
|
+
stroke-width={sw}
|
|
89
|
+
stroke-linecap="round"
|
|
90
|
+
stroke-linejoin="round"
|
|
91
|
+
role={label ? "img" : undefined}
|
|
92
|
+
aria-hidden={label ? undefined : "true"}
|
|
93
|
+
>
|
|
94
|
+
{label && <title>{label}</title>}
|
|
95
|
+
<path d={initialD}></path>
|
|
96
|
+
</svg>
|
|
97
|
+
</morph-icon>
|
|
98
|
+
|
|
99
|
+
<style>
|
|
100
|
+
/* Layout-neutral wrapper from the first paint, before any JS runs. */
|
|
101
|
+
morph-icon {
|
|
102
|
+
display: contents;
|
|
103
|
+
}
|
|
104
|
+
</style>
|
|
105
|
+
|
|
106
|
+
<script>
|
|
107
|
+
import { defineMorphIcon } from "./element.js";
|
|
108
|
+
defineMorphIcon();
|
|
109
|
+
</script>
|
package/dist/adapters.d.ts
CHANGED
|
@@ -1,5 +1,49 @@
|
|
|
1
1
|
import { n as IconInput } from "./types-JAF3s76Y.js";
|
|
2
2
|
import { i as PathEl } from "./index-DXp5b6BU.js";
|
|
3
|
+
//#region src/adapters/canvas.d.ts
|
|
4
|
+
/** The 2D context surface the target drives — structural, so a real
|
|
5
|
+
* CanvasRenderingContext2D, an OffscreenCanvasRenderingContext2D or a test
|
|
6
|
+
* fake all satisfy it. Only what canvasTarget actually touches. */
|
|
7
|
+
interface Canvas2DContext {
|
|
8
|
+
canvas: {
|
|
9
|
+
width: number;
|
|
10
|
+
height: number;
|
|
11
|
+
};
|
|
12
|
+
lineWidth: number;
|
|
13
|
+
lineCap: string;
|
|
14
|
+
lineJoin: string;
|
|
15
|
+
strokeStyle: string | object;
|
|
16
|
+
setTransform(a: number, b: number, c: number, d: number, e: number, f: number): void;
|
|
17
|
+
clearRect(x: number, y: number, w: number, h: number): void;
|
|
18
|
+
stroke(path: object): void;
|
|
19
|
+
}
|
|
20
|
+
/** Anything that can hand out a 2D context (an HTMLCanvasElement, an
|
|
21
|
+
* OffscreenCanvas, or a fake in tests). */
|
|
22
|
+
interface CanvasSurface {
|
|
23
|
+
getContext(contextId: "2d"): Canvas2DContext | null;
|
|
24
|
+
}
|
|
25
|
+
interface CanvasTargetOptions {
|
|
26
|
+
/** Grid the icon's coordinates live on (Lucide/Iconify default: "0 0 24 24").
|
|
27
|
+
* Used to map the geometry onto the backing store. */
|
|
28
|
+
viewBox?: string;
|
|
29
|
+
/** Stroke width in grid units (Lucide's default is 2). */
|
|
30
|
+
strokeWidth?: number;
|
|
31
|
+
/** Stroke color. Defaults to the canvas' computed CSS color when it is a
|
|
32
|
+
* styled element; otherwise the context's current `strokeStyle` stands. */
|
|
33
|
+
color?: string;
|
|
34
|
+
/** Clear the canvas before each frame. Turn off for manual compositing or
|
|
35
|
+
* trails — every frame then draws on top of the last. Default true. */
|
|
36
|
+
clear?: boolean;
|
|
37
|
+
/** Called after every drawn frame — the texture consumer's dirty signal
|
|
38
|
+
* (e.g. re-upload the canvas with `texImage2D`, or blit it into a scene). */
|
|
39
|
+
onWrite?: () => void;
|
|
40
|
+
}
|
|
41
|
+
/** Adapts a canvas (or its 2D context) into a `PathEl` whose writes stroke
|
|
42
|
+
* the morph's geometry onto the canvas, so
|
|
43
|
+
* `createMorph(canvasTarget(canvas), icon)` animates pixels instead of an
|
|
44
|
+
* inline `<svg><path>`. */
|
|
45
|
+
declare function canvasTarget(surface: CanvasSurface | Canvas2DContext, opts?: CanvasTargetOptions): PathEl;
|
|
46
|
+
//#endregion
|
|
3
47
|
//#region src/adapters/mask.d.ts
|
|
4
48
|
/** The subset of an element's inline style the mask target writes — structural
|
|
5
49
|
* so a real HTMLElement's `.style` satisfies it (and a test fake supplies
|
|
@@ -52,4 +96,4 @@ declare function maskTarget(el: MaskEl, opts?: MaskTargetOptions): MaskPathEl;
|
|
|
52
96
|
* an IconNode. */
|
|
53
97
|
declare function svgToIcon(markup: string): IconInput;
|
|
54
98
|
//#endregion
|
|
55
|
-
export { type MaskEl, type MaskPathEl, type MaskStyle, type MaskTargetOptions, maskTarget, svgToIcon };
|
|
99
|
+
export { type Canvas2DContext, type CanvasSurface, type CanvasTargetOptions, type MaskEl, type MaskPathEl, type MaskStyle, type MaskTargetOptions, canvasTarget, maskTarget, svgToIcon };
|
package/dist/adapters.js
CHANGED
|
@@ -1,4 +1,46 @@
|
|
|
1
1
|
import { t as fitIcon } from "./normalize-CYnN3Npw.js";
|
|
2
|
+
//#region src/adapters/canvas.ts
|
|
3
|
+
/** Adapts a canvas (or its 2D context) into a `PathEl` whose writes stroke
|
|
4
|
+
* the morph's geometry onto the canvas, so
|
|
5
|
+
* `createMorph(canvasTarget(canvas), icon)` animates pixels instead of an
|
|
6
|
+
* inline `<svg><path>`. */
|
|
7
|
+
function canvasTarget(surface, opts = {}) {
|
|
8
|
+
if (typeof Path2D === "undefined") throw new Error("morphicons: canvasTarget needs Path2D (browser or worker)");
|
|
9
|
+
const ctx = "getContext" in surface ? surface.getContext("2d") : surface;
|
|
10
|
+
if (!ctx) throw new Error("morphicons: canvasTarget needs a 2D context");
|
|
11
|
+
const vb = (opts.viewBox ?? "0 0 24 24").trim().split(/[\s,]+/).map(Number);
|
|
12
|
+
const [mx, my, vw, vh] = [
|
|
13
|
+
vb[0] || 0,
|
|
14
|
+
vb[1] || 0,
|
|
15
|
+
vb[2] || 24,
|
|
16
|
+
vb[3] || vb[2] || 24
|
|
17
|
+
];
|
|
18
|
+
const stroke = opts.strokeWidth ?? 2;
|
|
19
|
+
const clear = opts.clear !== false;
|
|
20
|
+
let color = opts.color;
|
|
21
|
+
let cssPending = color === void 0 && "style" in ctx.canvas;
|
|
22
|
+
return { setAttribute(name, value) {
|
|
23
|
+
if (name !== "d") return;
|
|
24
|
+
if (cssPending) {
|
|
25
|
+
cssPending = false;
|
|
26
|
+
color = getComputedStyle?.(ctx.canvas).color || void 0;
|
|
27
|
+
}
|
|
28
|
+
const c = ctx.canvas;
|
|
29
|
+
const s = Math.min(c.width / vw, c.height / vh);
|
|
30
|
+
if (clear) {
|
|
31
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
32
|
+
ctx.clearRect(0, 0, c.width, c.height);
|
|
33
|
+
}
|
|
34
|
+
ctx.setTransform(s, 0, 0, s, (c.width - vw * s) / 2 - mx * s, (c.height - vh * s) / 2 - my * s);
|
|
35
|
+
ctx.lineWidth = stroke;
|
|
36
|
+
ctx.lineCap = "round";
|
|
37
|
+
ctx.lineJoin = "round";
|
|
38
|
+
if (color !== void 0) ctx.strokeStyle = color;
|
|
39
|
+
ctx.stroke(new Path2D(value));
|
|
40
|
+
opts.onWrite?.();
|
|
41
|
+
} };
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
2
44
|
//#region src/adapters/mask.ts
|
|
3
45
|
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
4
46
|
let seq = 0;
|
|
@@ -136,4 +178,4 @@ function svgToIcon(markup) {
|
|
|
136
178
|
return vb ? fitIcon(node, vb[1]) : node;
|
|
137
179
|
}
|
|
138
180
|
//#endregion
|
|
139
|
-
export { maskTarget, svgToIcon };
|
|
181
|
+
export { canvasTarget, maskTarget, svgToIcon };
|
package/dist/astro.d.ts
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
IconInput,
|
|
3
|
+
MorphOptions,
|
|
4
|
+
ReducedMotionMode,
|
|
5
|
+
SpringPreset,
|
|
6
|
+
} from "./element.js";
|
|
7
|
+
|
|
8
|
+
export interface MorphIconAstroProps {
|
|
9
|
+
icon?: IconInput;
|
|
10
|
+
from?: IconInput;
|
|
11
|
+
to?: IconInput;
|
|
12
|
+
progress?: number;
|
|
13
|
+
spring?: SpringPreset | MorphOptions;
|
|
14
|
+
reducedMotion?: ReducedMotionMode;
|
|
15
|
+
size?: number | string;
|
|
16
|
+
color?: string;
|
|
17
|
+
strokeWidth?: number | string;
|
|
18
|
+
absoluteStrokeWidth?: boolean;
|
|
19
|
+
label?: string;
|
|
20
|
+
[attr: string]: unknown;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Astro component: SSR shell over the <morph-icon> custom element. */
|
|
24
|
+
declare const MorphIcon: (props: MorphIconAstroProps) => unknown;
|
|
25
|
+
export default MorphIcon;
|
|
26
|
+
export type {
|
|
27
|
+
IconInput,
|
|
28
|
+
IconNode,
|
|
29
|
+
MorphOptions,
|
|
30
|
+
ReducedMotionMode,
|
|
31
|
+
SpringPreset,
|
|
32
|
+
} from "./element.js";
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import { a as buildPlan, c as interpPolar, o as allocOutputs, r as resampleIcon } from "./spring-CFHloqPP.js";
|
|
2
|
+
import { i as serialize } from "./normalize-CYnN3Npw.js";
|
|
3
|
+
import { canonicalD, createMorph } from "./dom.js";
|
|
4
|
+
//#region src/dom/controller.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
|
+
/** The initial d is a constant for every binding: computed once from the
|
|
16
|
+
* mount-time props (server and client produce the same string → hydration
|
|
17
|
+
* without mismatch) and from then on only the driver mutates it outside the
|
|
18
|
+
* template. */
|
|
19
|
+
function computeInitialD({ icon, from, to, progress }) {
|
|
20
|
+
if (from !== void 0 && to !== void 0) return frozenD(from, to, progress ?? 0);
|
|
21
|
+
const first = icon ?? from ?? to;
|
|
22
|
+
return first !== void 0 ? canonicalD(first) : "";
|
|
23
|
+
}
|
|
24
|
+
/** Per-instance driver state — the exact logic of the React/Vue bindings
|
|
25
|
+
* (mount, mode watch, controlled seek with re-basing, imperative). Takes the
|
|
26
|
+
* init-time props: they seed the watch baselines so a watcher that fires on
|
|
27
|
+
* mount (Svelte's $effect, unlike Vue's watch) is a no-op. All change
|
|
28
|
+
* detection lives HERE, under tsc — the shells only wire their reactive
|
|
29
|
+
* surface to these methods.
|
|
30
|
+
*
|
|
31
|
+
* Lifecycle contract (shared verbatim by the bindings):
|
|
32
|
+
* - Lazy driver: an iconless mount keeps the element and births the driver
|
|
33
|
+
* on the FIRST icon that shows up (prop or imperative). `morphTo` with no
|
|
34
|
+
* driver behaves as `set` — there is nothing to fly from.
|
|
35
|
+
* - Controlled wins: while `from` and `to` are both present the pair owns
|
|
36
|
+
* the path and `icon` changes are ignored; dropping the pair hands the
|
|
37
|
+
* path back to `icon`.
|
|
38
|
+
* - Every exit from controlled mode (imperative call or icon takeover)
|
|
39
|
+
* invalidates the frozen pair, so returning to it re-bases on `from`. */
|
|
40
|
+
function createController({ icon, from, to, progress, reducedMotion }) {
|
|
41
|
+
let el = null;
|
|
42
|
+
let dead = false;
|
|
43
|
+
let morph = null;
|
|
44
|
+
let rm = reducedMotion ?? "never";
|
|
45
|
+
let based = false;
|
|
46
|
+
let pair = null;
|
|
47
|
+
let prevIcon = icon;
|
|
48
|
+
let prevControlled = from !== void 0 && to !== void 0;
|
|
49
|
+
let prevFrom = from;
|
|
50
|
+
let prevTo = to;
|
|
51
|
+
let prevProgress = progress;
|
|
52
|
+
/** Driver birth, lazy included: the first icon to show up creates it. */
|
|
53
|
+
const ensure = (birth) => {
|
|
54
|
+
if (morph) return morph;
|
|
55
|
+
if (dead || !el) return null;
|
|
56
|
+
morph = createMorph(el, birth, { reducedMotion: rm });
|
|
57
|
+
return morph;
|
|
58
|
+
};
|
|
59
|
+
/** Controlled mode: freeze the pair at `progress` via seek (no spring). */
|
|
60
|
+
const applyPair = (from, to, progress) => {
|
|
61
|
+
const m = morph;
|
|
62
|
+
if (!m) return;
|
|
63
|
+
const t = progress ?? 0;
|
|
64
|
+
if (!pair || pair[0] !== from || pair[1] !== to) {
|
|
65
|
+
pair = [from, to];
|
|
66
|
+
based = false;
|
|
67
|
+
}
|
|
68
|
+
if (t <= 0) {
|
|
69
|
+
m.set(from);
|
|
70
|
+
based = false;
|
|
71
|
+
} else if (t >= 1) {
|
|
72
|
+
m.set(to);
|
|
73
|
+
based = false;
|
|
74
|
+
} else {
|
|
75
|
+
if (!based) {
|
|
76
|
+
m.set(from);
|
|
77
|
+
based = true;
|
|
78
|
+
}
|
|
79
|
+
m.seek(to, t);
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
return {
|
|
83
|
+
mount(mountEl, { icon, from, to, progress, reducedMotion }) {
|
|
84
|
+
el = mountEl;
|
|
85
|
+
rm = reducedMotion ?? rm;
|
|
86
|
+
const controlled = from !== void 0 && to !== void 0;
|
|
87
|
+
const initialIcon = icon ?? from ?? to;
|
|
88
|
+
if (initialIcon === void 0) return;
|
|
89
|
+
const m = createMorph(el, controlled ? from : initialIcon, { reducedMotion: rm });
|
|
90
|
+
morph = m;
|
|
91
|
+
if (controlled) {
|
|
92
|
+
pair = [from, to];
|
|
93
|
+
const t = progress ?? 0;
|
|
94
|
+
if (t <= 0) m.set(from);
|
|
95
|
+
else if (t >= 1) m.set(to);
|
|
96
|
+
else {
|
|
97
|
+
m.seek(to, t);
|
|
98
|
+
based = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
destroy() {
|
|
103
|
+
dead = true;
|
|
104
|
+
el = null;
|
|
105
|
+
morph?.destroy();
|
|
106
|
+
morph = null;
|
|
107
|
+
based = false;
|
|
108
|
+
pair = null;
|
|
109
|
+
},
|
|
110
|
+
/** Prop watcher: ONE owner decides per run — the controlled pair while it
|
|
111
|
+
* is fully present, `icon` otherwise (mount doesn't fire thanks to the
|
|
112
|
+
* init-time baselines). */
|
|
113
|
+
watch({ icon, from, to, progress, spring, reducedMotion }) {
|
|
114
|
+
rm = reducedMotion ?? "never";
|
|
115
|
+
if (morph) morph.reducedMotion = rm;
|
|
116
|
+
const controlled = from !== void 0 && to !== void 0;
|
|
117
|
+
const left = prevControlled && !controlled;
|
|
118
|
+
const iconChanged = icon !== prevIcon;
|
|
119
|
+
const pairChanged = from !== prevFrom || to !== prevTo || progress !== prevProgress;
|
|
120
|
+
prevControlled = controlled;
|
|
121
|
+
prevIcon = icon;
|
|
122
|
+
prevFrom = from;
|
|
123
|
+
prevTo = to;
|
|
124
|
+
prevProgress = progress;
|
|
125
|
+
if (controlled) {
|
|
126
|
+
if (!pairChanged) return;
|
|
127
|
+
if (!(morph ?? ensure(from))) return;
|
|
128
|
+
applyPair(from, to, progress);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (icon === void 0 || !iconChanged && !left) return;
|
|
132
|
+
pair = null;
|
|
133
|
+
based = false;
|
|
134
|
+
if (morph) morph.morphTo(icon, spring);
|
|
135
|
+
else ensure(icon);
|
|
136
|
+
},
|
|
137
|
+
morphTo(icon, spring) {
|
|
138
|
+
pair = null;
|
|
139
|
+
based = false;
|
|
140
|
+
if (morph) morph.morphTo(icon, spring);
|
|
141
|
+
else ensure(icon);
|
|
142
|
+
},
|
|
143
|
+
set(icon) {
|
|
144
|
+
pair = null;
|
|
145
|
+
based = false;
|
|
146
|
+
if (morph) morph.set(icon);
|
|
147
|
+
else ensure(icon);
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
//#endregion
|
|
152
|
+
export { createController as n, computeInitialD as t };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { c as SpringPreset, n as IconInput } from "./types-JAF3s76Y.js";
|
|
2
|
+
import { a as ReducedMotionMode, i as PathEl, r as MorphOptions } from "./index-DXp5b6BU.js";
|
|
3
|
+
//#region src/dom/controller.d.ts
|
|
4
|
+
/** Imperative surface exposed by every binding (ref / bind:this / element). */
|
|
5
|
+
interface MorphHandle {
|
|
6
|
+
morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
|
|
7
|
+
set(icon: IconInput): void;
|
|
8
|
+
}
|
|
9
|
+
/** The mode-deciding props, shared by every binding's prop surface. */
|
|
10
|
+
interface MorphModeProps {
|
|
11
|
+
icon?: IconInput;
|
|
12
|
+
from?: IconInput;
|
|
13
|
+
to?: IconInput;
|
|
14
|
+
progress?: number;
|
|
15
|
+
}
|
|
16
|
+
interface MorphCtrlProps extends MorphModeProps {
|
|
17
|
+
reducedMotion?: ReducedMotionMode;
|
|
18
|
+
}
|
|
19
|
+
interface MorphWatchProps extends MorphCtrlProps {
|
|
20
|
+
spring?: SpringPreset | MorphOptions;
|
|
21
|
+
}
|
|
22
|
+
/** The initial d is a constant for every binding: computed once from the
|
|
23
|
+
* mount-time props (server and client produce the same string → hydration
|
|
24
|
+
* without mismatch) and from then on only the driver mutates it outside the
|
|
25
|
+
* template. */
|
|
26
|
+
declare function computeInitialD({ icon, from, to, progress }: MorphModeProps): string;
|
|
27
|
+
/** Per-instance driver state — the exact logic of the React/Vue bindings
|
|
28
|
+
* (mount, mode watch, controlled seek with re-basing, imperative). Takes the
|
|
29
|
+
* init-time props: they seed the watch baselines so a watcher that fires on
|
|
30
|
+
* mount (Svelte's $effect, unlike Vue's watch) is a no-op. All change
|
|
31
|
+
* detection lives HERE, under tsc — the shells only wire their reactive
|
|
32
|
+
* surface to these methods.
|
|
33
|
+
*
|
|
34
|
+
* Lifecycle contract (shared verbatim by the bindings):
|
|
35
|
+
* - Lazy driver: an iconless mount keeps the element and births the driver
|
|
36
|
+
* on the FIRST icon that shows up (prop or imperative). `morphTo` with no
|
|
37
|
+
* driver behaves as `set` — there is nothing to fly from.
|
|
38
|
+
* - Controlled wins: while `from` and `to` are both present the pair owns
|
|
39
|
+
* the path and `icon` changes are ignored; dropping the pair hands the
|
|
40
|
+
* path back to `icon`.
|
|
41
|
+
* - Every exit from controlled mode (imperative call or icon takeover)
|
|
42
|
+
* invalidates the frozen pair, so returning to it re-bases on `from`. */
|
|
43
|
+
declare function createController({ icon, from, to, progress, reducedMotion }: MorphCtrlProps): {
|
|
44
|
+
mount(mountEl: PathEl, { icon, from, to, progress, reducedMotion }: MorphCtrlProps): void;
|
|
45
|
+
destroy(): void;
|
|
46
|
+
/** Prop watcher: ONE owner decides per run — the controlled pair while it
|
|
47
|
+
* is fully present, `icon` otherwise (mount doesn't fire thanks to the
|
|
48
|
+
* init-time baselines). */
|
|
49
|
+
watch({ icon, from, to, progress, spring, reducedMotion }: MorphWatchProps): void;
|
|
50
|
+
morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
|
|
51
|
+
set(icon: IconInput): void;
|
|
52
|
+
};
|
|
53
|
+
//#endregion
|
|
54
|
+
export { computeInitialD as n, createController as r, MorphHandle as t };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
|
|
2
|
+
import { a as ReducedMotionMode, i as PathEl, n as Morph, r as MorphOptions } from "./index-DXp5b6BU.js";
|
|
3
|
+
import { n as computeInitialD, t as MorphHandle } from "./controller-Dkruvf3Y.js";
|
|
4
|
+
//#region src/element/index.d.ts
|
|
5
|
+
declare const Base: typeof HTMLElement;
|
|
6
|
+
declare class MorphIconElement extends Base implements MorphHandle {
|
|
7
|
+
#private;
|
|
8
|
+
static observedAttributes: string[];
|
|
9
|
+
get icon(): IconInput | undefined;
|
|
10
|
+
set icon(v: IconInput | undefined);
|
|
11
|
+
get from(): IconInput | undefined;
|
|
12
|
+
set from(v: IconInput | undefined);
|
|
13
|
+
get to(): IconInput | undefined;
|
|
14
|
+
set to(v: IconInput | undefined);
|
|
15
|
+
get progress(): number | undefined;
|
|
16
|
+
set progress(v: number | undefined);
|
|
17
|
+
get spring(): SpringPreset | MorphOptions | undefined;
|
|
18
|
+
set spring(v: SpringPreset | MorphOptions | undefined);
|
|
19
|
+
get reducedMotion(): ReducedMotionMode | undefined;
|
|
20
|
+
set reducedMotion(v: ReducedMotionMode | undefined);
|
|
21
|
+
morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
|
|
22
|
+
set(icon: IconInput): void;
|
|
23
|
+
connectedCallback(): void;
|
|
24
|
+
disconnectedCallback(): void;
|
|
25
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
|
|
26
|
+
}
|
|
27
|
+
/** Defines `<morph-icon>` (or a custom tag). Idempotent per tag; a no-op
|
|
28
|
+
* without a DOM (safe to call from code that also runs during SSR). */
|
|
29
|
+
declare function defineMorphIcon(tag?: string): void;
|
|
30
|
+
declare global {
|
|
31
|
+
interface HTMLElementTagNameMap {
|
|
32
|
+
"morph-icon": MorphIconElement;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
//#endregion
|
|
36
|
+
export { type IconInput, type IconNode, type Morph, type MorphHandle, MorphIconElement, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset, computeInitialD, defineMorphIcon };
|
package/dist/element.js
ADDED
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { n as createController, t as computeInitialD } from "./controller-CXZuwJ_M.js";
|
|
2
|
+
//#region src/element/index.ts
|
|
3
|
+
const SVG_NS = "http://www.w3.org/2000/svg";
|
|
4
|
+
const REACTIVE = [
|
|
5
|
+
"icon",
|
|
6
|
+
"from",
|
|
7
|
+
"to",
|
|
8
|
+
"progress",
|
|
9
|
+
"spring",
|
|
10
|
+
"reducedMotion"
|
|
11
|
+
];
|
|
12
|
+
const PRESENTATION = /* @__PURE__ */ new Set([
|
|
13
|
+
"size",
|
|
14
|
+
"color",
|
|
15
|
+
"stroke-width",
|
|
16
|
+
"absolute-stroke-width",
|
|
17
|
+
"label"
|
|
18
|
+
]);
|
|
19
|
+
const Base = typeof HTMLElement === "undefined" ? class {} : HTMLElement;
|
|
20
|
+
var MorphIconElement = class extends Base {
|
|
21
|
+
static observedAttributes = [
|
|
22
|
+
"icon",
|
|
23
|
+
"from",
|
|
24
|
+
"to",
|
|
25
|
+
"progress",
|
|
26
|
+
"spring",
|
|
27
|
+
"reduced-motion",
|
|
28
|
+
"size",
|
|
29
|
+
"color",
|
|
30
|
+
"stroke-width",
|
|
31
|
+
"absolute-stroke-width",
|
|
32
|
+
"label"
|
|
33
|
+
];
|
|
34
|
+
#props = {};
|
|
35
|
+
#ctrl = null;
|
|
36
|
+
#path = null;
|
|
37
|
+
#adopted = false;
|
|
38
|
+
#preConnectDirty = false;
|
|
39
|
+
#size = "24";
|
|
40
|
+
#color = "currentColor";
|
|
41
|
+
#strokeWidth = "2";
|
|
42
|
+
#absoluteStrokeWidth = false;
|
|
43
|
+
#label = null;
|
|
44
|
+
get icon() {
|
|
45
|
+
return this.#props.icon;
|
|
46
|
+
}
|
|
47
|
+
set icon(v) {
|
|
48
|
+
this.#props.icon = v;
|
|
49
|
+
this.#watch();
|
|
50
|
+
}
|
|
51
|
+
get from() {
|
|
52
|
+
return this.#props.from;
|
|
53
|
+
}
|
|
54
|
+
set from(v) {
|
|
55
|
+
this.#props.from = v;
|
|
56
|
+
this.#watch();
|
|
57
|
+
}
|
|
58
|
+
get to() {
|
|
59
|
+
return this.#props.to;
|
|
60
|
+
}
|
|
61
|
+
set to(v) {
|
|
62
|
+
this.#props.to = v;
|
|
63
|
+
this.#watch();
|
|
64
|
+
}
|
|
65
|
+
get progress() {
|
|
66
|
+
return this.#props.progress;
|
|
67
|
+
}
|
|
68
|
+
set progress(v) {
|
|
69
|
+
this.#props.progress = v;
|
|
70
|
+
this.#watch();
|
|
71
|
+
}
|
|
72
|
+
get spring() {
|
|
73
|
+
return this.#props.spring;
|
|
74
|
+
}
|
|
75
|
+
set spring(v) {
|
|
76
|
+
this.#props.spring = v;
|
|
77
|
+
this.#watch();
|
|
78
|
+
}
|
|
79
|
+
get reducedMotion() {
|
|
80
|
+
return this.#props.reducedMotion;
|
|
81
|
+
}
|
|
82
|
+
set reducedMotion(v) {
|
|
83
|
+
this.#props.reducedMotion = v;
|
|
84
|
+
this.#watch();
|
|
85
|
+
}
|
|
86
|
+
morphTo(icon, spring) {
|
|
87
|
+
const ctrl = this.#ctrl;
|
|
88
|
+
if (!ctrl) {
|
|
89
|
+
this.#imperativeBeforeDriver(icon);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
ctrl.morphTo(icon, spring ?? this.#props.spring);
|
|
93
|
+
}
|
|
94
|
+
set(icon) {
|
|
95
|
+
const ctrl = this.#ctrl;
|
|
96
|
+
if (!ctrl) {
|
|
97
|
+
this.#imperativeBeforeDriver(icon);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
ctrl.set(icon);
|
|
101
|
+
}
|
|
102
|
+
connectedCallback() {
|
|
103
|
+
for (const key of REACTIVE) this.#upgradeProperty(key);
|
|
104
|
+
if (!this.#path) this.#adoptOrRender();
|
|
105
|
+
if (this.style.display === "") this.style.display = "contents";
|
|
106
|
+
if (this.#path && !this.#ctrl) {
|
|
107
|
+
const path = this.#path;
|
|
108
|
+
const gate = this.#adopted && !this.#preConnectDirty;
|
|
109
|
+
this.#adopted = false;
|
|
110
|
+
this.#ctrl = createController(this.#props);
|
|
111
|
+
if (gate) {
|
|
112
|
+
let armed = false;
|
|
113
|
+
const gated = { setAttribute(name, value) {
|
|
114
|
+
if (armed) path.setAttribute(name, value);
|
|
115
|
+
} };
|
|
116
|
+
try {
|
|
117
|
+
this.#ctrl.mount(gated, this.#props);
|
|
118
|
+
} finally {
|
|
119
|
+
armed = true;
|
|
120
|
+
}
|
|
121
|
+
} else this.#ctrl.mount(path, this.#props);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
disconnectedCallback() {
|
|
125
|
+
this.#ctrl?.destroy();
|
|
126
|
+
this.#ctrl = null;
|
|
127
|
+
}
|
|
128
|
+
attributeChangedCallback(name, _old, value) {
|
|
129
|
+
switch (name) {
|
|
130
|
+
case "icon":
|
|
131
|
+
this.#props.icon = value ?? void 0;
|
|
132
|
+
break;
|
|
133
|
+
case "from":
|
|
134
|
+
this.#props.from = value ?? void 0;
|
|
135
|
+
break;
|
|
136
|
+
case "to":
|
|
137
|
+
this.#props.to = value ?? void 0;
|
|
138
|
+
break;
|
|
139
|
+
case "progress": {
|
|
140
|
+
const n = value === null ? NaN : Number(value);
|
|
141
|
+
this.#props.progress = Number.isFinite(n) ? n : void 0;
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
case "spring":
|
|
145
|
+
this.#props.spring = value ?? void 0;
|
|
146
|
+
break;
|
|
147
|
+
case "reduced-motion":
|
|
148
|
+
this.#props.reducedMotion = value ?? void 0;
|
|
149
|
+
break;
|
|
150
|
+
case "size":
|
|
151
|
+
this.#size = value ?? "24";
|
|
152
|
+
break;
|
|
153
|
+
case "color":
|
|
154
|
+
this.#color = value ?? "currentColor";
|
|
155
|
+
break;
|
|
156
|
+
case "stroke-width":
|
|
157
|
+
this.#strokeWidth = value ?? "2";
|
|
158
|
+
break;
|
|
159
|
+
case "absolute-stroke-width":
|
|
160
|
+
this.#absoluteStrokeWidth = value !== null;
|
|
161
|
+
break;
|
|
162
|
+
case "label": this.#label = value;
|
|
163
|
+
}
|
|
164
|
+
if (!this.#path) return;
|
|
165
|
+
if (PRESENTATION.has(name)) {
|
|
166
|
+
const svg = this.querySelector("svg");
|
|
167
|
+
if (svg) this.#applyPresentation(svg);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
this.#watch();
|
|
171
|
+
}
|
|
172
|
+
/** A property assigned before the element was defined lands as an own
|
|
173
|
+
* value that shadows the accessor; re-route it through the setter. */
|
|
174
|
+
#upgradeProperty(key) {
|
|
175
|
+
if (Object.hasOwn(this, key)) {
|
|
176
|
+
const self = this;
|
|
177
|
+
const v = self[key];
|
|
178
|
+
delete self[key];
|
|
179
|
+
self[key] = v;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
#watch() {
|
|
183
|
+
const ctrl = this.#ctrl;
|
|
184
|
+
if (!ctrl) {
|
|
185
|
+
this.#preConnectDirty = true;
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
ctrl.watch(this.#props);
|
|
189
|
+
}
|
|
190
|
+
/** Imperative call before the controller exists: adopt the icon as the
|
|
191
|
+
* mount state AND exit controlled mode — "every imperative call
|
|
192
|
+
* invalidates the frozen pair" must hold pre-connect too, or a pending
|
|
193
|
+
* pair would win the mount and silently discard this call. */
|
|
194
|
+
#imperativeBeforeDriver(icon) {
|
|
195
|
+
this.#props.icon = icon;
|
|
196
|
+
this.#props.from = void 0;
|
|
197
|
+
this.#props.to = void 0;
|
|
198
|
+
this.#preConnectDirty = true;
|
|
199
|
+
}
|
|
200
|
+
/** SSR markup is adopted verbatim (the element never rewrites server bytes
|
|
201
|
+
* at rest); without it, the element renders its own <svg> like the
|
|
202
|
+
* framework shells do. */
|
|
203
|
+
#adoptOrRender() {
|
|
204
|
+
const existing = this.querySelector("path");
|
|
205
|
+
if (existing) {
|
|
206
|
+
this.#path = existing;
|
|
207
|
+
this.#adopted = true;
|
|
208
|
+
const p = this.#props;
|
|
209
|
+
if (p.icon === void 0 && p.from === void 0 && p.to === void 0) {
|
|
210
|
+
const d = existing.getAttribute("d");
|
|
211
|
+
if (d) p.icon = d;
|
|
212
|
+
}
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
const doc = this.ownerDocument;
|
|
216
|
+
const svg = doc.createElementNS(SVG_NS, "svg");
|
|
217
|
+
svg.setAttribute("xmlns", SVG_NS);
|
|
218
|
+
svg.setAttribute("viewBox", "0 0 24 24");
|
|
219
|
+
svg.setAttribute("fill", "none");
|
|
220
|
+
svg.setAttribute("stroke-linecap", "round");
|
|
221
|
+
svg.setAttribute("stroke-linejoin", "round");
|
|
222
|
+
this.#applyPresentation(svg);
|
|
223
|
+
const path = doc.createElementNS(SVG_NS, "path");
|
|
224
|
+
path.setAttribute("d", computeInitialD(this.#props));
|
|
225
|
+
svg.appendChild(path);
|
|
226
|
+
this.appendChild(svg);
|
|
227
|
+
this.#path = path;
|
|
228
|
+
}
|
|
229
|
+
#applyPresentation(svg) {
|
|
230
|
+
svg.setAttribute("width", this.#size);
|
|
231
|
+
svg.setAttribute("height", this.#size);
|
|
232
|
+
svg.setAttribute("stroke", this.#color);
|
|
233
|
+
const sw = this.#absoluteStrokeWidth ? String(Number(this.#strokeWidth) * 24 / Number(this.#size)) : this.#strokeWidth;
|
|
234
|
+
svg.setAttribute("stroke-width", sw);
|
|
235
|
+
const label = this.#label;
|
|
236
|
+
let title = svg.querySelector("title");
|
|
237
|
+
if (label) {
|
|
238
|
+
svg.setAttribute("role", "img");
|
|
239
|
+
svg.removeAttribute("aria-hidden");
|
|
240
|
+
if (!title) {
|
|
241
|
+
title = this.ownerDocument.createElementNS(SVG_NS, "title");
|
|
242
|
+
svg.insertBefore(title, svg.firstChild);
|
|
243
|
+
}
|
|
244
|
+
title.textContent = label;
|
|
245
|
+
} else {
|
|
246
|
+
svg.removeAttribute("role");
|
|
247
|
+
svg.setAttribute("aria-hidden", "true");
|
|
248
|
+
title?.remove();
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
let registered = false;
|
|
253
|
+
/** Defines `<morph-icon>` (or a custom tag). Idempotent per tag; a no-op
|
|
254
|
+
* without a DOM (safe to call from code that also runs during SSR). */
|
|
255
|
+
function defineMorphIcon(tag = "morph-icon") {
|
|
256
|
+
if (typeof customElements === "undefined" || customElements.get(tag)) return;
|
|
257
|
+
customElements.define(tag, registered ? class extends MorphIconElement {} : MorphIconElement);
|
|
258
|
+
registered = true;
|
|
259
|
+
}
|
|
260
|
+
//#endregion
|
|
261
|
+
export { MorphIconElement, computeInitialD, defineMorphIcon };
|
package/dist/svelte-shared.d.ts
CHANGED
|
@@ -1,12 +1,8 @@
|
|
|
1
1
|
import { a as Sampled, c as SpringPreset, n as IconInput, r as IconNode } from "./types-JAF3s76Y.js";
|
|
2
2
|
import { a as ReducedMotionMode, i as PathEl, n as Morph, r as MorphOptions } from "./index-DXp5b6BU.js";
|
|
3
|
+
import { n as computeInitialD, r as createController, t as MorphHandle } from "./controller-Dkruvf3Y.js";
|
|
3
4
|
import { SVGAttributes } from "svelte/elements";
|
|
4
5
|
//#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
6
|
/** Rest props fall through to the `<svg>` fully typed (class, style, data-*,
|
|
11
7
|
* events, ARIA…) via svelte/elements, like lucide-svelte — typos in an SVG
|
|
12
8
|
* attr fail the build. `from`/`to` are omitted because the SMIL animation
|
|
@@ -34,40 +30,5 @@ interface MorphIconProps extends Omit<SVGAttributes<SVGSVGElement>, "from" | "to
|
|
|
34
30
|
/** Accessibility: with label → role="img" + <title>; without → aria-hidden. */
|
|
35
31
|
label?: string;
|
|
36
32
|
}
|
|
37
|
-
type ModeProps = Pick<MorphIconProps, "icon" | "from" | "to" | "progress">;
|
|
38
|
-
type CtrlProps = ModeProps & Pick<MorphIconProps, "reducedMotion">;
|
|
39
|
-
type WatchProps = CtrlProps & {
|
|
40
|
-
spring?: SpringPreset | MorphOptions;
|
|
41
|
-
};
|
|
42
|
-
/** The initial d is constant for Svelte: computed once from the mount-time
|
|
43
|
-
* props (server and client produce the same string → hydration without
|
|
44
|
-
* mismatch) and from then on only the driver mutates it outside the template. */
|
|
45
|
-
declare function computeInitialD({ icon, from, to, progress }: ModeProps): string;
|
|
46
|
-
/** Per-instance driver state — the exact logic of the React/Vue bindings
|
|
47
|
-
* (mount, mode watch, controlled seek with re-basing, imperative). Takes the
|
|
48
|
-
* init-time props: they seed the watch baselines so the first `$effect` run
|
|
49
|
-
* (which fires on mount in Svelte, unlike Vue's `watch`) is a no-op. All
|
|
50
|
-
* change detection lives HERE, under tsc — the .svelte shell only wires
|
|
51
|
-
* runes to these methods.
|
|
52
|
-
*
|
|
53
|
-
* Lifecycle contract (shared verbatim by the three bindings):
|
|
54
|
-
* - Lazy driver: an iconless mount keeps the element and births the driver
|
|
55
|
-
* on the FIRST icon that shows up (prop or imperative). `morphTo` with no
|
|
56
|
-
* driver behaves as `set` — there is nothing to fly from.
|
|
57
|
-
* - Controlled wins: while `from` and `to` are both present the pair owns
|
|
58
|
-
* the path and `icon` changes are ignored; dropping the pair hands the
|
|
59
|
-
* path back to `icon`.
|
|
60
|
-
* - Every exit from controlled mode (imperative call or icon takeover)
|
|
61
|
-
* invalidates the frozen pair, so returning to it re-bases on `from`. */
|
|
62
|
-
declare function createController({ icon, from, to, progress, reducedMotion }: CtrlProps): {
|
|
63
|
-
mount(mountEl: PathEl, { icon, from, to, progress, reducedMotion }: CtrlProps): void;
|
|
64
|
-
destroy(): void;
|
|
65
|
-
/** Prop watcher: ONE owner decides per run — the controlled pair while it
|
|
66
|
-
* is fully present, `icon` otherwise (mount doesn't fire thanks to the
|
|
67
|
-
* init-time baselines). */
|
|
68
|
-
watch({ icon, from, to, progress, spring, reducedMotion }: WatchProps): void;
|
|
69
|
-
morphTo(icon: IconInput, spring?: SpringPreset | MorphOptions): void;
|
|
70
|
-
set(icon: IconInput): void;
|
|
71
|
-
};
|
|
72
33
|
//#endregion
|
|
73
|
-
export { type IconInput, type IconNode, type Morph, MorphHandle, MorphIconProps, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset, computeInitialD, createController };
|
|
34
|
+
export { type IconInput, type IconNode, type Morph, type MorphHandle, MorphIconProps, type MorphOptions, type PathEl, type ReducedMotionMode, type Sampled, type SpringPreset, computeInitialD, createController };
|
package/dist/svelte-shared.js
CHANGED
|
@@ -1,151 +1,2 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { i as serialize } from "./normalize-CYnN3Npw.js";
|
|
3
|
-
import { canonicalD, createMorph } from "./dom.js";
|
|
4
|
-
//#region src/svelte/shared.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
|
-
/** The initial d is constant for Svelte: computed once from the mount-time
|
|
16
|
-
* props (server and client produce the same string → hydration without
|
|
17
|
-
* mismatch) and from then on only the driver mutates it outside the template. */
|
|
18
|
-
function computeInitialD({ icon, from, to, progress }) {
|
|
19
|
-
if (from !== void 0 && to !== void 0) return frozenD(from, to, progress ?? 0);
|
|
20
|
-
const first = icon ?? from ?? to;
|
|
21
|
-
return first !== void 0 ? canonicalD(first) : "";
|
|
22
|
-
}
|
|
23
|
-
/** Per-instance driver state — the exact logic of the React/Vue bindings
|
|
24
|
-
* (mount, mode watch, controlled seek with re-basing, imperative). Takes the
|
|
25
|
-
* init-time props: they seed the watch baselines so the first `$effect` run
|
|
26
|
-
* (which fires on mount in Svelte, unlike Vue's `watch`) is a no-op. All
|
|
27
|
-
* change detection lives HERE, under tsc — the .svelte shell only wires
|
|
28
|
-
* runes to these methods.
|
|
29
|
-
*
|
|
30
|
-
* Lifecycle contract (shared verbatim by the three bindings):
|
|
31
|
-
* - Lazy driver: an iconless mount keeps the element and births the driver
|
|
32
|
-
* on the FIRST icon that shows up (prop or imperative). `morphTo` with no
|
|
33
|
-
* driver behaves as `set` — there is nothing to fly from.
|
|
34
|
-
* - Controlled wins: while `from` and `to` are both present the pair owns
|
|
35
|
-
* the path and `icon` changes are ignored; dropping the pair hands the
|
|
36
|
-
* path back to `icon`.
|
|
37
|
-
* - Every exit from controlled mode (imperative call or icon takeover)
|
|
38
|
-
* invalidates the frozen pair, so returning to it re-bases on `from`. */
|
|
39
|
-
function createController({ icon, from, to, progress, reducedMotion }) {
|
|
40
|
-
let el = null;
|
|
41
|
-
let dead = false;
|
|
42
|
-
let morph = null;
|
|
43
|
-
let rm = reducedMotion ?? "never";
|
|
44
|
-
let based = false;
|
|
45
|
-
let pair = null;
|
|
46
|
-
let prevIcon = icon;
|
|
47
|
-
let prevControlled = from !== void 0 && to !== void 0;
|
|
48
|
-
let prevFrom = from;
|
|
49
|
-
let prevTo = to;
|
|
50
|
-
let prevProgress = progress;
|
|
51
|
-
/** Driver birth, lazy included: the first icon to show up creates it. */
|
|
52
|
-
const ensure = (birth) => {
|
|
53
|
-
if (morph) return morph;
|
|
54
|
-
if (dead || !el) return null;
|
|
55
|
-
morph = createMorph(el, birth, { reducedMotion: rm });
|
|
56
|
-
return morph;
|
|
57
|
-
};
|
|
58
|
-
/** Controlled mode: freeze the pair at `progress` via seek (no spring). */
|
|
59
|
-
const applyPair = (from, to, progress) => {
|
|
60
|
-
const m = morph;
|
|
61
|
-
if (!m) return;
|
|
62
|
-
const t = progress ?? 0;
|
|
63
|
-
if (!pair || pair[0] !== from || pair[1] !== to) {
|
|
64
|
-
pair = [from, to];
|
|
65
|
-
based = false;
|
|
66
|
-
}
|
|
67
|
-
if (t <= 0) {
|
|
68
|
-
m.set(from);
|
|
69
|
-
based = false;
|
|
70
|
-
} else if (t >= 1) {
|
|
71
|
-
m.set(to);
|
|
72
|
-
based = false;
|
|
73
|
-
} else {
|
|
74
|
-
if (!based) {
|
|
75
|
-
m.set(from);
|
|
76
|
-
based = true;
|
|
77
|
-
}
|
|
78
|
-
m.seek(to, t);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
return {
|
|
82
|
-
mount(mountEl, { icon, from, to, progress, reducedMotion }) {
|
|
83
|
-
el = mountEl;
|
|
84
|
-
rm = reducedMotion ?? rm;
|
|
85
|
-
const controlled = from !== void 0 && to !== void 0;
|
|
86
|
-
const initialIcon = icon ?? from ?? to;
|
|
87
|
-
if (initialIcon === void 0) return;
|
|
88
|
-
const m = createMorph(el, controlled ? from : initialIcon, { reducedMotion: rm });
|
|
89
|
-
morph = m;
|
|
90
|
-
if (controlled) {
|
|
91
|
-
pair = [from, to];
|
|
92
|
-
const t = progress ?? 0;
|
|
93
|
-
if (t <= 0) m.set(from);
|
|
94
|
-
else if (t >= 1) m.set(to);
|
|
95
|
-
else {
|
|
96
|
-
m.seek(to, t);
|
|
97
|
-
based = true;
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
},
|
|
101
|
-
destroy() {
|
|
102
|
-
dead = true;
|
|
103
|
-
el = null;
|
|
104
|
-
morph?.destroy();
|
|
105
|
-
morph = null;
|
|
106
|
-
based = false;
|
|
107
|
-
pair = null;
|
|
108
|
-
},
|
|
109
|
-
/** Prop watcher: ONE owner decides per run — the controlled pair while it
|
|
110
|
-
* is fully present, `icon` otherwise (mount doesn't fire thanks to the
|
|
111
|
-
* init-time baselines). */
|
|
112
|
-
watch({ icon, from, to, progress, spring, reducedMotion }) {
|
|
113
|
-
rm = reducedMotion ?? "never";
|
|
114
|
-
if (morph) morph.reducedMotion = rm;
|
|
115
|
-
const controlled = from !== void 0 && to !== void 0;
|
|
116
|
-
const left = prevControlled && !controlled;
|
|
117
|
-
const iconChanged = icon !== prevIcon;
|
|
118
|
-
const pairChanged = from !== prevFrom || to !== prevTo || progress !== prevProgress;
|
|
119
|
-
prevControlled = controlled;
|
|
120
|
-
prevIcon = icon;
|
|
121
|
-
prevFrom = from;
|
|
122
|
-
prevTo = to;
|
|
123
|
-
prevProgress = progress;
|
|
124
|
-
if (controlled) {
|
|
125
|
-
if (!pairChanged) return;
|
|
126
|
-
if (!(morph ?? ensure(from))) return;
|
|
127
|
-
applyPair(from, to, progress);
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
130
|
-
if (icon === void 0 || !iconChanged && !left) return;
|
|
131
|
-
pair = null;
|
|
132
|
-
based = false;
|
|
133
|
-
if (morph) morph.morphTo(icon, spring);
|
|
134
|
-
else ensure(icon);
|
|
135
|
-
},
|
|
136
|
-
morphTo(icon, spring) {
|
|
137
|
-
pair = null;
|
|
138
|
-
based = false;
|
|
139
|
-
if (morph) morph.morphTo(icon, spring);
|
|
140
|
-
else ensure(icon);
|
|
141
|
-
},
|
|
142
|
-
set(icon) {
|
|
143
|
-
pair = null;
|
|
144
|
-
based = false;
|
|
145
|
-
if (morph) morph.set(icon);
|
|
146
|
-
else ensure(icon);
|
|
147
|
-
}
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
//#endregion
|
|
1
|
+
import { n as createController, t as computeInitialD } from "./controller-CXZuwJ_M.js";
|
|
151
2
|
export { computeInitialD, createController };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "morphicons",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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)",
|
|
@@ -35,6 +35,10 @@
|
|
|
35
35
|
"lucide-vue-next",
|
|
36
36
|
"svelte",
|
|
37
37
|
"lucide-svelte",
|
|
38
|
+
"astro",
|
|
39
|
+
"astro-component",
|
|
40
|
+
"web-component",
|
|
41
|
+
"custom-element",
|
|
38
42
|
"icon-animation"
|
|
39
43
|
],
|
|
40
44
|
"type": "module",
|
|
@@ -71,6 +75,14 @@
|
|
|
71
75
|
"types": "./dist/svelte.d.ts",
|
|
72
76
|
"svelte": "./dist/svelte.js",
|
|
73
77
|
"default": "./dist/svelte.js"
|
|
78
|
+
},
|
|
79
|
+
"./element": {
|
|
80
|
+
"types": "./dist/element.d.ts",
|
|
81
|
+
"default": "./dist/element.js"
|
|
82
|
+
},
|
|
83
|
+
"./astro": {
|
|
84
|
+
"types": "./dist/astro.d.ts",
|
|
85
|
+
"default": "./dist/MorphIcon.astro"
|
|
74
86
|
}
|
|
75
87
|
},
|
|
76
88
|
"peerDependencies": {
|
|
@@ -102,11 +114,11 @@
|
|
|
102
114
|
],
|
|
103
115
|
"scripts": {
|
|
104
116
|
"test": "bun test",
|
|
105
|
-
"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",
|
|
117
|
+
"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 && tsc -p tsconfig.element.json --noEmit",
|
|
106
118
|
"play": "bun ./playground/index.html",
|
|
107
119
|
"lint": "biome check .",
|
|
108
120
|
"format": "biome check --write .",
|
|
109
|
-
"build": "tsdown && bun scripts/build-svelte.ts",
|
|
121
|
+
"build": "tsdown && bun scripts/build-svelte.ts && bun scripts/build-astro.ts",
|
|
110
122
|
"size": "size-limit",
|
|
111
123
|
"prepublishOnly": "bun run build"
|
|
112
124
|
},
|
|
@@ -129,7 +141,7 @@
|
|
|
129
141
|
"name": "adapters (aggregate — grows by design, see per-export gates)",
|
|
130
142
|
"path": "dist/adapters.js",
|
|
131
143
|
"import": "*",
|
|
132
|
-
"limit": "4.
|
|
144
|
+
"limit": "4.8 KB",
|
|
133
145
|
"gzip": true
|
|
134
146
|
},
|
|
135
147
|
{
|
|
@@ -146,6 +158,13 @@
|
|
|
146
158
|
"limit": "0.8 KB",
|
|
147
159
|
"gzip": true
|
|
148
160
|
},
|
|
161
|
+
{
|
|
162
|
+
"name": "adapters › canvasTarget (per-export shakeability gate)",
|
|
163
|
+
"path": "dist/adapters.js",
|
|
164
|
+
"import": "{ canvasTarget }",
|
|
165
|
+
"limit": "0.55 KB",
|
|
166
|
+
"gzip": true
|
|
167
|
+
},
|
|
149
168
|
{
|
|
150
169
|
"name": "core + dom + react (MorphIcon, react external)",
|
|
151
170
|
"path": "dist/react.js",
|
|
@@ -196,6 +215,22 @@
|
|
|
196
215
|
],
|
|
197
216
|
"limit": "1.4 KB",
|
|
198
217
|
"gzip": true
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
"name": "core + dom + element (<morph-icon> custom element)",
|
|
221
|
+
"path": "dist/element.js",
|
|
222
|
+
"import": "*",
|
|
223
|
+
"limit": "9.5 KB",
|
|
224
|
+
"gzip": true
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
"name": "astro shell (ships as source, consumer-compiled)",
|
|
228
|
+
"path": "dist/MorphIcon.astro",
|
|
229
|
+
"disablePlugins": [
|
|
230
|
+
"@size-limit/esbuild"
|
|
231
|
+
],
|
|
232
|
+
"limit": "1.75 KB",
|
|
233
|
+
"gzip": true
|
|
199
234
|
}
|
|
200
235
|
],
|
|
201
236
|
"devDependencies": {
|