craft-library 0.0.0-stage → 0.1.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/dist/magnet.js ADDED
@@ -0,0 +1,308 @@
1
+ // Magnetic icon · Craft Library, by Sara Lobo. magnet.css and magnet.js from the library, unchanged, around a mount.
2
+ import { css } from './style.js';
3
+ const CSS = "/* Magnetic icon · Craft Library, by Sara Lobo\n A dark disc with a brushed-metal rim and a search glyph (Figma: 40×40). Near the cursor the rim acts like metal\n near a strong magnet: the disc is drawn toward the pointer, the strained rim creases and trembles, its tip cracks\n into shards, and small voxels break off and fly to the pointer. Pairs with magnet.js.\n Everything is drawn in a 40-unit box, so --mag-size scales the whole piece, the pull included. */\n\n.mag {\n all: unset;\n position: relative;\n display: inline-grid;\n place-items: center;\n width: var(--mag-size, 40px);\n height: var(--mag-size, 40px);\n border-radius: 50%;\n color: #fff;\n cursor: pointer;\n -webkit-tap-highlight-color: transparent;\n transition: transform .2s cubic-bezier(.2, .8, .2, 1);\n}\n.mag:active {\n transform: scale(.94);\n}\n.mag:focus-visible {\n outline: 2px solid #9DB8FF;\n outline-offset: 4px;\n}\n/* the disc and its rim, redrawn by magnet.js; the stretch reaches past the button */\n.mag-ring {\n position: absolute;\n inset: 0;\n width: 100%;\n height: 100%;\n overflow: visible;\n pointer-events: none;\n}\n/* the shards and voxels: 46 units of room on every side of the 40-unit button (46 / 40 = 115%) */\n.mag-dust {\n position: absolute;\n left: -115%;\n top: -115%;\n width: 330%;\n height: 330%;\n pointer-events: none;\n}\n.mag-icon {\n position: relative;\n display: block;\n width: 50%;\n height: 50%;\n}\n";
4
+ const MARKUP = "<svg class=\"mag-ring\" viewBox=\"0 0 40 40\" aria-hidden=\"true\"><defs><linearGradient id=\"magMetal\" x1=\"32\" y1=\"3.5\" x2=\"20\" y2=\"40\" gradientUnits=\"userSpaceOnUse\"><stop stop-color=\"#999\"></stop><stop offset=\".452\" stop-color=\"#fff\"></stop><stop offset=\"1\" stop-color=\"#A2A2A2\"></stop></linearGradient><radialGradient id=\"magFade\" cx=\"20\" cy=\"20\" r=\"40\" gradientUnits=\"userSpaceOnUse\"><stop offset=\".506\" stop-color=\"#fff\"></stop><stop offset=\".507\" stop-color=\"#000\"></stop></radialGradient><mask id=\"magMask\" maskUnits=\"userSpaceOnUse\" x=\"-30\" y=\"-30\" width=\"100\" height=\"100\"><rect x=\"-30\" y=\"-30\" width=\"100\" height=\"100\" fill=\"url(#magFade)\"></rect></mask></defs><path class=\"mag-rim\" mask=\"url(#magMask)\" d=\"M39.25 20C39.25 30.6315 30.6315 39.25 20 39.25C9.36852 39.25 .75 30.6315 .75 20C.75 9.36852 9.36852 .75 20 .75C30.6315 .75 39.25 9.36852 39.25 20Z\" fill=\"#fff\" fill-opacity=\".2\" stroke=\"url(#magMetal)\" stroke-width=\"1.5\"></path></svg><svg class=\"mag-icon\" viewBox=\"0 0 20 20\" fill=\"none\" aria-hidden=\"true\"><path d=\"M17.5 17.5L13.8807 13.8807M13.8807 13.8807C15.0871 12.6743 15.8333 11.0076 15.8333 9.16667C15.8333 5.48477 12.8486 2.5 9.16667 2.5C5.48477 2.5 2.5 5.48477 2.5 9.16667C2.5 12.8486 5.48477 15.8333 9.16667 15.8333C11.0076 15.8333 12.6743 15.0871 13.8807 13.8807Z\" stroke=\"currentColor\" stroke-width=\"1.66667\" stroke-linecap=\"round\" stroke-linejoin=\"round\"></path></svg>";
5
+ let api;
6
+ function engine() {
7
+ if (api) return api;
8
+ const window = {};
9
+ // Magnetic icon · Craft Library, by Sara Lobo
10
+ // Every .mag on the page reads the pointer and its rim follows it while it is near: the disc is drawn toward it, the
11
+ // strained rim creases and trembles, the tip cracks into shards and voxels break off and fly to the pointer. Let go,
12
+ // and the rim springs back with a wobble while the loose voxels return to it. Pairs with magnet.css.
13
+ // Buttons added later: magnetize(root). To steer one without a mouse (a demo, a tour): magnetPoint(button, x, y) with
14
+ // a point on screen, and magnetPoint(button, null) to hand it back to the pointer.
15
+ (function () {
16
+ var C = 20, R = 19.25, N = 96; // the rim's centre and radius in the 40-unit box, and how many points draw it
17
+ var REACH = 44; // how far past the rim the magnet is felt
18
+ var PULL = 15, FALL = 1.1; // the furthest the rim reaches out, and how fast the pull drops with distance
19
+ var BODY = 0.3, SHIFT = 0.14; // how much of the disc goes with the tip, and how far the disc itself is drawn
20
+ var JAG = 0.18; // how deep the strained rim creases
21
+ var DENT = 3.5; // the furthest the rim is drawn in while the pointer is over the button
22
+ var RATE = 90, MAXDUST = 90; // voxels shed per second at full pull, and at most at once
23
+ var ROOM = 46; // room for the shards and voxels on every side of the button (matches .mag-dust)
24
+ var pointer = null, mags = [], raf = 0, last = 0, uid = 0;
25
+
26
+ if (matchMedia('(prefers-reduced-motion: reduce)').matches) { // less motion: the button stays as drawn
27
+ window.magnetize = window.magnetPoint = function () {};
28
+ return;
29
+ }
30
+
31
+ function clamp(x, a, b) { return Math.max(a, Math.min(b, x)); }
32
+ function wrap(a) { return Math.atan2(Math.sin(a), Math.cos(a)); }
33
+ function bell(x, s) { return Math.exp(-x * x / (2 * s * s)); }
34
+ function spike(x, s) { return Math.exp((0.16 - Math.sqrt(x * x + 0.0256)) / s); } // a rounded peak with flared shoulders, like metal drawn out
35
+ function hash(x, y) { var h = Math.sin(x * 127.1 + y * 311.7) * 43758.5453; return h - Math.floor(h); }
36
+ function n2(x) { return Math.round(x * 100) / 100; }
37
+ // a quick, smooth random shiver for point i, between -0.5 and 0.5
38
+ function tremble(i, now) { var f = now / 1000 * 14, k = Math.floor(f), u = f - k; u = u * u * (3 - 2 * u); return hash(i, k) * (1 - u) + hash(i, k + 1) * u - 0.5; }
39
+
40
+ // the rim's radius at angle t, around the disc's (drawn) centre: pulled out toward the pointer, the disc going with it,
41
+ // or pressed in; the far side gives a little
42
+ function width(m) { return 0.55 - 0.25 * clamp(m.d / PULL, 0, 1); }
43
+ function radius(m, t) {
44
+ var dt = wrap(t - m.a), d = m.d;
45
+ return R + (d > 0 ? d * (spike(dt, width(m)) + BODY * bell(dt, 1)) : d * bell(dt, 0.7)) - 0.12 * d * bell(wrap(dt - Math.PI), 1.3);
46
+ }
47
+
48
+ // the rim as one closed curve: smooth where it is calm (Catmull-Rom turned into cubic Béziers), creased into straight,
49
+ // trembling facets where the magnet strains it
50
+ function rimPath(m, now) {
51
+ var pts = [], strain = clamp((Math.abs(m.d) + 3 * m.heat) / PULL, 0, 1), i, t, r, env, sharp, n, p, a, b, c, e;
52
+ for (i = 0; i < N; i++) {
53
+ t = i / N * 2 * Math.PI;
54
+ r = radius(m, t);
55
+ env = bell(wrap(t - m.a), 0.6) * strain;
56
+ sharp = env > 0.12;
57
+ if (sharp && i % 3) continue;
58
+ if (sharp) r += 2 * JAG * PULL * env * tremble(i, now);
59
+ pts.push([m.cx + r * Math.cos(t), m.cy + r * Math.sin(t), sharp]);
60
+ }
61
+ n = pts.length;
62
+ p = 'M' + n2(pts[0][0]) + ' ' + n2(pts[0][1]);
63
+ for (i = 0; i < n; i++) {
64
+ a = pts[(i + n - 1) % n]; b = pts[i]; c = pts[(i + 1) % n]; e = pts[(i + 2) % n];
65
+ p += b[2] || c[2] ? 'L' + n2(c[0]) + ' ' + n2(c[1])
66
+ : 'C' + n2(b[0] + (c[0] - a[0]) / 6) + ' ' + n2(b[1] + (c[1] - a[1]) / 6) + ' ' +
67
+ n2(c[0] - (e[0] - b[0]) / 6) + ' ' + n2(c[1] - (e[1] - b[1]) / 6) + ' ' + n2(c[0]) + ' ' + n2(c[1]);
68
+ }
69
+ return p + 'Z';
70
+ }
71
+
72
+ // where the rim wants to be: outside, it reaches toward the pointer (never past it); over the button, it is drawn in
73
+ function aim(m) {
74
+ var p = m.ghost || pointer;
75
+ if (!p) return null;
76
+ var k = 40 / m.box.width;
77
+ var x = (p.x - m.box.left) * k - C, y = (p.y - m.box.top) * k - C;
78
+ var dist = Math.hypot(x, y), edge = dist - R, near = 1 - Math.max(edge, 0) / REACH;
79
+ if (edge > REACH) return null;
80
+ var d = edge >= 0
81
+ ? Math.min(edge * 0.9 / (1 + BODY), PULL * Math.pow(1 - edge / REACH, FALL))
82
+ : -Math.min(-edge * 0.85, DENT * clamp(dist / R, 0, 1));
83
+ // heat: how hard the field strains the metal, strongest at the rim and calm at the very centre
84
+ return { d: d, a: Math.atan2(y, x), x: x, y: y, dist: dist, near: near, heat: edge >= 0 ? near * near : clamp(dist / R, 0, 1) };
85
+ }
86
+
87
+ // one tick of the springs; true once everything has come to rest
88
+ function step(m, dt) {
89
+ var t = m.t = aim(m), to = t ? t.d : 0, heat = t ? t.heat : 0, turn = 0, k, c, ix = 0, iy = 0, e;
90
+ // a spring toward the target; looser once the magnet lets go, so the metal overshoots and wobbles back
91
+ if (t) { k = 420; c = 30; } else { k = 260; c = 9; }
92
+ m.v += (k * (to - m.d) - c * m.v) * dt;
93
+ m.d += m.v * dt;
94
+ m.heat += (heat - m.heat) * (1 - Math.exp(-dt * 6));
95
+ if (t) {
96
+ // the stretched side turns toward the pointer; close to the centre the direction jitters, so it turns slower there
97
+ turn = wrap(t.a - m.a);
98
+ m.a += turn * (1 - Math.exp(-dt * 22 * clamp(t.dist / 8, 0, 1)));
99
+ // the glyph is drawn a little toward the pointer too
100
+ e = 1.4 * t.near * t.near / Math.max(t.dist, 1);
101
+ ix = t.x * e; iy = t.y * e;
102
+ }
103
+ e = 1 - Math.exp(-dt * 14);
104
+ m.ix += (ix - m.ix) * e; m.iy += (iy - m.iy) * e;
105
+ // the disc itself is drawn toward the magnet
106
+ e = SHIFT * Math.abs(m.d);
107
+ m.cx = C + e * Math.cos(m.a); m.cy = C + e * Math.sin(m.a);
108
+ return Math.abs(to - m.d) < 0.02 && Math.abs(m.v) < 0.05 && Math.abs(turn) < 0.002 && Math.abs(heat - m.heat) < 0.01 &&
109
+ Math.abs(ix - m.ix) < 0.01 && Math.abs(iy - m.iy) < 0.01;
110
+ }
111
+
112
+ // voxels break off where the magnet pulls hardest: more the further the rim is pulled, and more while the pointer moves
113
+ function shed(m, dt) {
114
+ var t = m.t, pull, th, r, x, y, dx, dy, l;
115
+ if (!t) { m.acc = 0; return; }
116
+ pull = m.d > 0 ? clamp(m.d / PULL, 0, 1) : 0.5 * clamp(-m.d / DENT, 0, 1);
117
+ m.acc += dt * RATE * Math.pow(pull, 1.4) * (0.35 + 0.65 * clamp(m.pace / 160, 0, 1));
118
+ for (; m.acc >= 1; m.acc--) {
119
+ if (m.dust.length >= MAXDUST) continue;
120
+ th = m.a + (Math.random() + Math.random() - 1) * width(m) * 1.4;
121
+ r = radius(m, th) - Math.random() * 1.2;
122
+ if (m.d > 0) r += 0.35 * m.d * clamp((r - R) / (m.d * (1 + BODY)), 0, 1); // off the shards, which have pulled away
123
+ x = m.cx + r * Math.cos(th); y = m.cy + r * Math.sin(th);
124
+ dx = C + t.x - x; dy = C + t.y - y; l = Math.hypot(dx, dy) || 1;
125
+ m.dust.push({ x: x, y: y, vx: dx / l * 12, vy: dy / l * 12, hx: C + R * Math.cos(th), hy: C + R * Math.sin(th),
126
+ s: 1.2 + Math.random() * 1.6, k: 1, age: 0, life: 0.6 + Math.random() * 0.6, back: Math.random() < 0.3,
127
+ spin: Math.random() < 0.5 ? -1 : 1, c: Math.random() < 0.35 ? 190 : 255 });
128
+ }
129
+ }
130
+
131
+ // loose voxels: drawn to the pointer and swallowed there; once the magnet lets go (and for the few that hold on), back to the rim
132
+ function drift(m, dt) {
133
+ var t = m.t, e = 1 - Math.exp(-dt * 5), k, p, home, tx, ty, dx, dy, dist, sp, sw;
134
+ for (k = m.dust.length - 1; k >= 0; k--) {
135
+ p = m.dust[k];
136
+ p.age += dt;
137
+ home = !t || (p.back && p.age > 0.18);
138
+ if (home && !p.ret) { p.ret = true; p.life = Math.max(p.life, p.age + 0.5); }
139
+ tx = home ? p.hx : C + t.x; ty = home ? p.hy : C + t.y;
140
+ dx = tx - p.x; dy = ty - p.y; dist = Math.hypot(dx, dy) || 0.001;
141
+ sp = (home ? 18 : 30) + 420 / (dist + 6);
142
+ sw = p.spin * sp * 0.4 * clamp(dist / 20, 0, 1); // a slight swirl, like filings along a field line
143
+ p.vx += (dx / dist * sp - dy / dist * sw - p.vx) * e;
144
+ p.vy += (dy / dist * sp + dx / dist * sw - p.vy) * e;
145
+ p.x += p.vx * dt; p.y += p.vy * dt;
146
+ p.k = 0.45 + 0.55 * clamp(dist / 12, 0, 1);
147
+ if (dist < (home ? 1 : 2)) p.age = Math.max(p.age, p.life - 0.1); // swallowed by the magnet, or back in the metal
148
+ if (p.age >= p.life) m.dust.splice(k, 1);
149
+ }
150
+ return m.dust.length > 0;
151
+ }
152
+
153
+ // where the drawn rim gives way to the shards: a clean crack line a little under halfway up the stretch
154
+ function crack(m) { var d = Math.max(m.d, 0); return [R + 1 + 0.45 * d, 0.04 * d + 0.001]; }
155
+
156
+ // the cracked tip: past the crack line the rim splits into shards that pull away toward the pointer, the ones nearest
157
+ // the point the most, each turned a little; as the rim springs back they close up again
158
+ function drawShards(m, g) {
159
+ if (m.d < 1) return;
160
+ var f = crack(m)[0], top = radius(m, m.a) - f, cuts = [], lo, hi, x, k, i, t0, t1, tc, r0, r1, ro, rm, q;
161
+ if (top < 0.5) return;
162
+ for (lo = 0; lo > -Math.PI && radius(m, m.a + lo) > f; lo -= 0.02);
163
+ for (hi = 0; hi < Math.PI && radius(m, m.a + hi) > f; hi += 0.02);
164
+ // the cracks ride with the tip, so the shards keep their shapes as it turns
165
+ for (x = -(0.08 + 0.06 * hash(9, 1)), k = 0; x > lo; x -= 0.15 + 0.15 * hash(k++, 2)) cuts.unshift(x);
166
+ cuts.unshift(lo);
167
+ for (x = 0.08 + 0.06 * hash(9, 3), k = 0; x < hi; x += 0.15 + 0.15 * hash(k++, 4)) cuts.push(x);
168
+ cuts.push(hi);
169
+ function at(t, r) { return [m.cx + r * Math.cos(m.a + t), m.cy + r * Math.sin(m.a + t)]; }
170
+ for (i = 0; i < cuts.length - 1; i++) {
171
+ t0 = cuts[i] + 0.012; t1 = cuts[i + 1] - 0.012;
172
+ if (t1 - t0 < 0.03) continue;
173
+ tc = (t0 + t1) / 2; r0 = radius(m, m.a + t0); r1 = radius(m, m.a + t1); ro = radius(m, m.a + tc);
174
+ q = clamp((ro - f) / top, 0, 1);
175
+ if (ro - f > 6) { // the long piece at the point breaks once more across
176
+ rm = f + (ro - f) * (0.4 + 0.2 * hash(i, 7));
177
+ shard(m, g, i * 2, q * 0.5, [at(t0, f), at(tc, f), at(t1, f), at(t1, Math.min(rm, r1)), at(t0, Math.min(rm, r0))], 0);
178
+ shard(m, g, i * 2 + 1, q, [at(t0, Math.min(rm, r0)), at(t1, Math.min(rm, r1)), at(t1, r1), at(tc, ro), at(t0, r0)], 2);
179
+ } else shard(m, g, i * 2, q, [at(t0, f), at(tc, f), at(t1, f), at(t1, r1), at(tc, ro), at(t0, r0)], 3);
180
+ }
181
+ }
182
+ // one shard; its rim edge starts at point `rim` of its outline (0: it carries none)
183
+ function shard(m, g, k, q, pts, rim) {
184
+ var cx = 0, cy = 0, l = m.d * (0.04 + 0.45 * Math.pow(q, 1.5)) * (0.8 + 0.4 * hash(k, 1)), ux, uy, tx, ty, dl, i;
185
+ pts.forEach(function (p) { cx += p[0] / pts.length; cy += p[1] / pts.length; });
186
+ // pulled partly outward, partly straight at the pointer
187
+ ux = cx - m.cx; uy = cy - m.cy; dl = Math.hypot(ux, uy) || 1; ux /= dl; uy /= dl;
188
+ if (m.t) { tx = C + m.t.x - cx; ty = C + m.t.y - cy; dl = Math.hypot(tx, ty) || 1; ux = (ux + tx / dl) / 2; uy = (uy + ty / dl) / 2; }
189
+ g.save();
190
+ g.translate(cx + ux * l, cy + uy * l); g.rotate((hash(k, 3) - 0.5) * 0.5 * q); g.translate(-cx, -cy);
191
+ g.beginPath(); g.moveTo(pts[0][0], pts[0][1]);
192
+ for (i = 1; i < pts.length; i++) g.lineTo(pts[i][0], pts[i][1]);
193
+ g.closePath();
194
+ g.fillStyle = 'rgba(255,255,255,0.2)'; g.fill();
195
+ g.lineWidth = 0.5; g.lineJoin = 'miter'; g.strokeStyle = 'rgba(255,255,255,0.2)'; g.stroke(); // the fresh break catches the light
196
+ if (rim) { // the piece of metal rim it carries
197
+ g.beginPath(); g.moveTo(pts[rim][0], pts[rim][1]);
198
+ for (i = rim + 1; i < pts.length; i++) g.lineTo(pts[i][0], pts[i][1]);
199
+ g.lineWidth = 1.5; g.strokeStyle = 'rgba(236,236,236,0.95)'; g.stroke();
200
+ }
201
+ g.restore();
202
+ }
203
+
204
+ function draw(m, now) {
205
+ var s = m.box.width / 40, dpr = Math.min(2, devicePixelRatio || 1), size = Math.round((40 + 2 * ROOM) * s * dpr), f = crack(m), g = m.g;
206
+ m.rim.setAttribute('d', rimPath(m, now));
207
+ // the drawn rim gives way at the crack line, where the shards take over
208
+ m.grad.setAttribute('cx', n2(m.cx)); m.grad.setAttribute('cy', n2(m.cy));
209
+ m.stops[0].setAttribute('offset', (f[0] / 40).toFixed(4));
210
+ m.stops[1].setAttribute('offset', ((f[0] + f[1]) / 40).toFixed(4));
211
+ if (m.icon) m.icon.style.transform = 'translate(' + n2((m.ix + m.cx - C) * s) + 'px,' + n2((m.iy + m.cy - C) * s) + 'px)';
212
+ if (m.cv.width !== size) m.cv.width = m.cv.height = size;
213
+ g.setTransform(1, 0, 0, 1, 0, 0);
214
+ g.clearRect(0, 0, size, size);
215
+ g.setTransform(s * dpr, 0, 0, s * dpr, ROOM * s * dpr, ROOM * s * dpr);
216
+ drawShards(m, g);
217
+ m.dust.forEach(function (p) {
218
+ // never under 2 screen pixels, so the voxels still read on a small button
219
+ var q = p.age / p.life, a = 0.9 * Math.min(1, p.age / 0.06) * (1 - q * q * q), z = Math.max(p.s, 2 / s) * p.k;
220
+ g.fillStyle = 'rgba(' + p.c + ',' + p.c + ',' + p.c + ',' + n2(a) + ')';
221
+ g.fillRect(p.x - z / 2, p.y - z / 2, z, z);
222
+ });
223
+ }
224
+
225
+ function frame(now) {
226
+ var dt = Math.min((now - last) / 1000, 1 / 30), rest = true;
227
+ last = now;
228
+ mags = mags.filter(function (m) { return m.el.isConnected; }); // buttons taken off the page let go
229
+ mags.forEach(function (m) {
230
+ m.box = m.el.getBoundingClientRect();
231
+ if (!m.box.width) return;
232
+ // how fast the magnet moves, in units a second: a quick pass tears off more than a slow one
233
+ var p = m.ghost || pointer;
234
+ if (p && m.lp && dt > 0) m.pace += (Math.hypot(p.x - m.lp.x, p.y - m.lp.y) * 40 / m.box.width / dt - m.pace) * (1 - Math.exp(-dt * 8));
235
+ else m.pace *= Math.exp(-dt * 4);
236
+ m.lp = p ? { x: p.x, y: p.y } : null;
237
+ // two half steps keep the springs steady when frames come in slow
238
+ var still = step(m, dt / 2) & step(m, dt / 2);
239
+ shed(m, dt);
240
+ var live = drift(m, dt) || !still || (m.t && (Math.abs(m.d) > 0.5 || m.heat > 0.05));
241
+ if (live) { rest = false; draw(m, now); m.still = false; }
242
+ else if (!m.still) { draw(m, now); m.still = true; }
243
+ });
244
+ raf = rest ? 0 : requestAnimationFrame(frame);
245
+ }
246
+ function wake() { if (!raf) { last = performance.now(); raf = requestAnimationFrame(frame); } }
247
+
248
+ function wire(el) {
249
+ var svg = el.querySelector('.mag-ring'), icon = el.querySelector('.mag-icon'), cv;
250
+ if (el.__mag || !svg) return;
251
+ // each button gets its own ids, so several can live on one page
252
+ uid++;
253
+ svg.querySelectorAll('[id]').forEach(function (def) {
254
+ var id = def.id + '-' + uid;
255
+ svg.querySelectorAll('[fill],[stroke],[mask]').forEach(function (s) {
256
+ ['fill', 'stroke', 'mask'].forEach(function (at) { if (s.getAttribute(at) === 'url(#' + def.id + ')') s.setAttribute(at, 'url(#' + id + ')'); });
257
+ });
258
+ def.id = id;
259
+ });
260
+ cv = document.createElement('canvas');
261
+ cv.className = 'mag-dust';
262
+ cv.setAttribute('aria-hidden', 'true');
263
+ el.insertBefore(cv, icon);
264
+ el.__mag = { el: el, rim: svg.querySelector('.mag-rim'), grad: svg.querySelector('radialGradient'), stops: svg.querySelectorAll('radialGradient stop'),
265
+ icon: icon, cv: cv, g: cv.getContext('2d'), dust: [], acc: 0, pace: 0, lp: null, ghost: null,
266
+ d: 0, v: 0, a: -Math.PI / 2, heat: 0, cx: C, cy: C, ix: 0, iy: 0, t: null, still: true };
267
+ mags.push(el.__mag);
268
+ }
269
+ window.magnetize = function (root) {
270
+ root = root || document;
271
+ if (root.matches && root.matches('.mag')) wire(root);
272
+ root.querySelectorAll('.mag').forEach(wire);
273
+ };
274
+ window.magnetPoint = function (el, x, y) {
275
+ wire(el);
276
+ if (!el.__mag) return;
277
+ el.__mag.ghost = x == null ? null : { x: x, y: y };
278
+ wake();
279
+ };
280
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', function () { window.magnetize(); });
281
+ else window.magnetize();
282
+
283
+ function track(e) { pointer = { x: e.clientX, y: e.clientY }; wake(); }
284
+ function drop() { pointer = null; wake(); }
285
+ addEventListener('pointermove', track, { passive: true });
286
+ addEventListener('pointerdown', track, { passive: true });
287
+ addEventListener('pointerup', function (e) { if (e.pointerType !== 'mouse') drop(); });
288
+ addEventListener('pointercancel', drop);
289
+ addEventListener('pointerout', function (e) { if (!e.relatedTarget) drop(); });
290
+ addEventListener('blur', drop);
291
+ addEventListener('scroll', wake, { passive: true }); // the page moves under a still pointer
292
+ })();
293
+
294
+ return (api = window);
295
+ }
296
+
297
+ // host: the element the button goes in. o: { label, size (px or any CSS length), onClick }
298
+ export function mountMagnet(host, o = {}) {
299
+ css('craft-magnet', CSS);
300
+ const M = engine();
301
+ host.innerHTML = '<button class="mag" type="button">' + MARKUP + '</button>';
302
+ const btn = host.firstChild;
303
+ btn.setAttribute('aria-label', o.label || 'Search');
304
+ if (o.size) btn.style.setProperty('--mag-size', typeof o.size === 'number' ? o.size + 'px' : String(o.size));
305
+ if (o.onClick) btn.addEventListener('click', o.onClick);
306
+ M.magnetize(btn);
307
+ return { button: btn, point: (x, y) => M.magnetPoint(btn, x, y), destroy() { host.innerHTML = ''; } };
308
+ }
package/dist/style.js ADDED
@@ -0,0 +1,11 @@
1
+ // each piece's CSS goes into the page once, the first time one is mounted
2
+ const done = new Set();
3
+ export function css(id, text) {
4
+ if (done.has(id) || typeof document === 'undefined') return;
5
+ done.add(id);
6
+ if (document.getElementById(id)) return;
7
+ const s = document.createElement('style');
8
+ s.id = id;
9
+ s.textContent = text;
10
+ document.head.appendChild(s);
11
+ }
package/package.json CHANGED
@@ -1,6 +1,48 @@
1
1
  {
2
2
  "name": "craft-library",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Craft Library by Sara Lobo: a liquid glass nav bar, a bursting heart and a magnetic icon, for React or plain HTML",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "module": "./dist/index.js",
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./elements": "./dist/elements.js",
11
+ "./craft.js": "./dist/craft.js"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "README.md"
16
+ ],
17
+ "sideEffects": [
18
+ "./dist/elements.js",
19
+ "./dist/craft.js"
20
+ ],
21
+ "peerDependencies": {
22
+ "react": ">=18"
23
+ },
24
+ "peerDependenciesMeta": {
25
+ "react": {
26
+ "optional": true
27
+ }
28
+ },
29
+ "devDependencies": {
30
+ "esbuild": "0.24.0"
31
+ },
32
+ "scripts": {
33
+ "build": "node build.mjs",
34
+ "prepublishOnly": "node build.mjs"
35
+ },
36
+ "keywords": [
37
+ "react",
38
+ "web-components",
39
+ "liquid-glass",
40
+ "animation",
41
+ "micro-interaction",
42
+ "navbar",
43
+ "like-button"
44
+ ],
45
+ "author": "Sara Lobo",
46
+ "license": "MIT",
47
+ "homepage": "https://claude.ai/artifact/LBBWCfEHJGvfBSynV3wDuR"
48
+ }