craft-library 0.3.0 → 0.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Craft Library
2
2
 
3
- Interface pieces by [Sara Lobo](https://www.saralobodesign.com/), designed, tested and polished one detail at a time: a liquid glass nav bar, a bursting heart, a magnetic icon, an iridescent 3D card and a glass piggy bank. Each one is the exact piece from the library, with its own CSS and motion, for React 18+ or plain HTML. The only dependency is three.js, for the two 3D pieces, and it loads only when one of them is on the page.
3
+ Interface pieces by [Sara Lobo](https://www.saralobodesign.com/), designed, tested and polished one detail at a time: a liquid glass nav bar, a bursting heart, a magnetic icon, an iridescent 3D card, a glass piggy bank, and two "thinking" indicators, the Magic sphere and the Sweep. Each one is the exact piece from the library, with its own CSS and motion, for React 18+ or plain HTML. The only dependency is three.js, for the two 3D pieces, and it loads only when one of them is on the page.
4
4
 
5
5
  ```bash
6
6
  npm install craft-library
@@ -101,9 +101,27 @@ import { PiggyBank } from 'craft-library';
101
101
 
102
102
  It is WebGL (three.js 0.169, which comes with the package and loads the first time it mounts), so one per screen is best. It is made for a dark background; black is best.
103
103
 
104
+ ## Magic sphere and Sweep
105
+
106
+ Two "thinking" indicators for the wait while an answer is being written: a ball of dots, drawn on a canvas, with the word beside it. The **Magic sphere** is a Rubik's cube on a ball, one grey tone per face: its slices turn and scramble the pieces, retrace their steps until every piece is home, and the ball lights up for a moment while a light passes through the word. In the **Sweep**, a wave crosses a dotted globe from left to right, lifting the dots it passes, runs on through the word at the same speed, and rests.
107
+
108
+ ```jsx
109
+ import { MagicSphere, SweepSphere } from 'craft-library';
110
+
111
+ {isThinking && <MagicSphere />}
112
+ {isThinking && <SweepSphere label="Searching" />}
113
+ ```
114
+
115
+ | Prop | | |
116
+ |---|---|---|
117
+ | `label` | `string` | The word beside the ball. Default `"Thinking"`; `""` shows the ball alone (still announced as "Thinking"). |
118
+ | `size` | `number \| string` | The ball, px or any CSS length. Default `32`. Under 64px it draws more, finer dots, made to sit beside text; larger, it shows the full detail. |
119
+
120
+ Both are made for a dark background: the dots and the word are light. On a light one, set `--thinking-ink` (the dots and the light in the word) and `--thinking-dim` (the word at rest) on it or a parent. The element is a `role="status"`.
121
+
104
122
  ## Plain HTML
105
123
 
106
- One script registers five tags. Same pieces, same props as attributes.
124
+ One script registers seven tags. Same pieces, same props as attributes.
107
125
 
108
126
  ```html
109
127
  <script src="https://cdn.jsdelivr.net/npm/craft-library@0/dist/craft.js"></script>
@@ -113,12 +131,14 @@ One script registers five tags. Same pieces, same props as attributes.
113
131
  <magnetic-icon size="40"></magnetic-icon>
114
132
  <iridescent-card></iridescent-card> <!-- "cardturn" event: e.detail.face; 100% wide, 480px tall unless you size it -->
115
133
  <piggy-bank></piggy-bank> <!-- "piggydone" event after each play; el.play() replays; 100% wide, 560px tall unless you size it -->
134
+ <magic-sphere></magic-sphere> <!-- label="Thinking" (default; label="" for the ball alone), size="32" -->
135
+ <sweep-sphere></sweep-sphere>
116
136
  ```
117
137
 
118
138
  With a bundler and no React: `import 'craft-library/elements'`.
119
139
 
120
140
  ## Motion and access
121
141
 
122
- Every item is a real `<button>` with an accessible name; the nav marks the chosen one with `aria-current` and moves with the arrow keys. The card is a `role="button"` that Enter, Space and the arrow keys turn over; the piggy bank is a labelled image. With `prefers-reduced-motion`, the heart only changes colour, the nav only slides, the magnet stays still the card swaps faces with a fade, with no entrance and no breathing, and the piggy bank stays in place while the coins appear inside it and its eyes close.
142
+ Every item is a real `<button>` with an accessible name; the nav marks the chosen one with `aria-current` and moves with the arrow keys. The card is a `role="button"` that Enter, Space and the arrow keys turn over; the piggy bank is a labelled image. With `prefers-reduced-motion`, the heart only changes colour, the nav only slides, the magnet stays still, the card swaps faces with a fade, with no entrance and no breathing, the piggy bank stays in place while the coins appear inside it and its eyes close, and the thinking balls hold still and only breathe.
123
143
 
124
144
  MIT · [Craft Library](https://claude.ai/artifact/LBBWCfEHJGvfBSynV3wDuR)
package/dist/cdn.js CHANGED
@@ -1,4 +1,4 @@
1
- // the one-file build for a <script> tag: registers the five tags and exposes the mount functions (the 3D pieces' scenes
1
+ // the one-file build for a <script> tag: registers the seven tags and exposes the mount functions (the 3D pieces' scenes
2
2
  // are fetched from next to this file the first time one is mounted)
3
- import { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank } from './elements.js';
4
- window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank };
3
+ import { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking } from './elements.js';
4
+ window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking };
package/dist/craft.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Craft Library, by Sara Lobo · liquid glass nav bar, bursting heart, magnetic icon, iridescent card, glass piggy bank · <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card>, <piggy-bank> */
1
+ /* Craft Library, by Sara Lobo · liquid glass nav bar, bursting heart, magnetic icon, iridescent card, glass piggy bank, magic sphere, sweep · <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card>, <piggy-bank>, <magic-sphere>, <sweep-sphere> */
2
2
  (() => {
3
3
  // dist/style.js
4
4
  var done = /* @__PURE__ */ new Set();
@@ -1392,6 +1392,389 @@
1392
1392
  };
1393
1393
  }
1394
1394
 
1395
+ // dist/thinking.js
1396
+ var CSS4 = `/* Thinking orbs \xB7 Craft Library, by Sara Lobo
1397
+ A ball of dots and the word beside it, for the wait while an answer is being written. thinking.js draws the ball
1398
+ and, for the Sweep, carries the ball's wave on through the word. Made for dark backgrounds: on a light one, set
1399
+ --thinking-ink (the dots and the light in the word) and --thinking-dim (the word at rest). */
1400
+
1401
+ .thinking {
1402
+ --thinking-size: 32px;
1403
+ --thinking-ink: #EDEDED;
1404
+ --thinking-dim: #8A8A8A;
1405
+ position: relative;
1406
+ display: inline-flex;
1407
+ align-items: center;
1408
+ gap: 10px;
1409
+ vertical-align: middle;
1410
+ }
1411
+ /* the ball: drawn by thinking.js at this size. Under 64px it uses more, finer dots, made to sit beside text */
1412
+ .thinking-orb {
1413
+ display: block;
1414
+ flex: none;
1415
+ width: var(--thinking-size);
1416
+ height: var(--thinking-size);
1417
+ }
1418
+ /* the word: a light passes through it, left to right, on a loop */
1419
+ .thinking-label {
1420
+ font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', system-ui, sans-serif;
1421
+ font-size: 14px;
1422
+ line-height: normal;
1423
+ letter-spacing: -.005em;
1424
+ white-space: nowrap;
1425
+ -webkit-font-smoothing: antialiased;
1426
+ text-rendering: optimizeLegibility;
1427
+ color: transparent;
1428
+ -webkit-background-clip: text;
1429
+ background-clip: text;
1430
+ background-image: linear-gradient(90deg, var(--thinking-dim) 0 35%, var(--thinking-ink) 50%, var(--thinking-dim) 65% 100%);
1431
+ background-size: 250% 100%;
1432
+ animation: thinking-shine 2.6s linear infinite;
1433
+ }
1434
+ @keyframes thinking-shine {
1435
+ from {
1436
+ background-position: 100% 0;
1437
+ }
1438
+ to {
1439
+ background-position: 0 0;
1440
+ }
1441
+ }
1442
+ /* the Sweep's word is lit by the ball's own wave: thinking.js moves --thinking-x across it */
1443
+ .thinking[data-kind="sweep"] .thinking-label {
1444
+ --thinking-x: -999px;
1445
+ background-image: linear-gradient(90deg, var(--thinking-dim) calc(var(--thinking-x) - 26px), var(--thinking-ink) var(--thinking-x), var(--thinking-dim) calc(var(--thinking-x) + 26px));
1446
+ background-size: auto;
1447
+ animation: none;
1448
+ }
1449
+
1450
+ /* less motion: the ball holds still and only breathes; the word stays plain */
1451
+ @media (prefers-reduced-motion: reduce) {
1452
+ .thinking-label {
1453
+ animation: none;
1454
+ background: none;
1455
+ color: var(--thinking-dim);
1456
+ }
1457
+ }
1458
+ `;
1459
+ var api4;
1460
+ function engine4() {
1461
+ if (api4) return api4;
1462
+ const window2 = {};
1463
+ (function() {
1464
+ var TAU = Math.PI * 2, SMALL = 64;
1465
+ var SPEED = 85, REST = 0.55, HALO = 26;
1466
+ var reduce = matchMedia("(prefers-reduced-motion: reduce)");
1467
+ var all = [], running = false, last = 0;
1468
+ function clamp01(x) {
1469
+ return x < 0 ? 0 : x > 1 ? 1 : x;
1470
+ }
1471
+ function smooth(a, b, x) {
1472
+ var t = clamp01((x - a) / (b - a));
1473
+ return t * t * (3 - 2 * t);
1474
+ }
1475
+ function ease(t) {
1476
+ return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2;
1477
+ }
1478
+ function painter() {
1479
+ var LEVELS = 16, d = [], n = [];
1480
+ for (var i = 0; i < LEVELS; i++) {
1481
+ d.push([]);
1482
+ n.push(0);
1483
+ }
1484
+ return {
1485
+ gain: 1,
1486
+ dot: function(x, y, r, a) {
1487
+ a *= this.gain;
1488
+ if (a <= 0.02) return;
1489
+ var b = Math.min(LEVELS - 1, a * LEVELS | 0), arr = d[b], k = n[b];
1490
+ arr[k] = x;
1491
+ arr[k + 1] = y;
1492
+ arr[k + 2] = r;
1493
+ n[b] = k + 3;
1494
+ },
1495
+ flush: function(c, ink) {
1496
+ c.fillStyle = ink;
1497
+ for (var b = 0; b < LEVELS; b++) {
1498
+ if (!n[b]) continue;
1499
+ var arr = d[b];
1500
+ c.globalAlpha = (b + 1) / LEVELS;
1501
+ c.beginPath();
1502
+ for (var i2 = 0; i2 < n[b]; i2 += 3) {
1503
+ c.moveTo(arr[i2] + arr[i2 + 2], arr[i2 + 1]);
1504
+ c.arc(arr[i2], arr[i2 + 1], arr[i2 + 2], 0, TAU);
1505
+ }
1506
+ c.fill();
1507
+ n[b] = 0;
1508
+ }
1509
+ c.globalAlpha = 1;
1510
+ }
1511
+ };
1512
+ }
1513
+ var CAM = 3.2, LX = -0.42, LY = -0.62, LZ = -0.66;
1514
+ function view(yaw, pitch) {
1515
+ return { cy: Math.cos(yaw), sy: Math.sin(yaw), cp: Math.cos(pitch), sp: Math.sin(pitch) };
1516
+ }
1517
+ function turn(V, x, y, z, o) {
1518
+ var x1 = x * V.cy - z * V.sy, z1 = x * V.sy + z * V.cy;
1519
+ o[0] = x1;
1520
+ o[1] = y * V.cp - z1 * V.sp;
1521
+ o[2] = y * V.sp + z1 * V.cp;
1522
+ }
1523
+ function persp(x, y, z, o) {
1524
+ var f = CAM / (CAM + z);
1525
+ o[3] = x * f;
1526
+ o[4] = y * f;
1527
+ o[5] = f;
1528
+ }
1529
+ function lit(nx, ny, nz) {
1530
+ return (0.32 + 0.68 * Math.max(0, nx * LX + ny * LY + nz * LZ)) * (0.07 + 0.93 * smooth(0.28, -0.3, nz));
1531
+ }
1532
+ var TONES = [1, 0.5, 0.78, 0.3, 0.62, 0.18];
1533
+ function spin(q, axis, cs, sn) {
1534
+ var b = (axis + 1) % 3, c = (axis + 2) % 3, qb = q[b], qc = q[c];
1535
+ q[b] = qb * cs - qc * sn;
1536
+ q[c] = qb * sn + qc * cs;
1537
+ }
1538
+ function sphere(small) {
1539
+ var per = small ? 2 : 4, COUNT = 5, SLOT = 0.5, DUR = 0.42, HOLD = 1.1, R0 = 0.8, D = [], v = [0, 0, 0], o = [0, 0, 0, 0, 0, 0];
1540
+ for (var a = 0; a < 3; a++) for (var sg = 1; sg >= -1; sg -= 2) {
1541
+ var s = TONES[a * 2 + (sg > 0 ? 0 : 1)], b = (a + 1) % 3, c = (a + 2) % 3;
1542
+ for (var su = -1; su <= 1; su++) for (var sv = -1; sv <= 1; sv++) for (var j = 0; j < per; j++) for (var k = 0; k < per; k++) {
1543
+ var pos = [0, 0, 0], cub = [0, 0, 0];
1544
+ pos[a] = sg;
1545
+ cub[a] = sg;
1546
+ cub[b] = su;
1547
+ cub[c] = sv;
1548
+ pos[b] = su * 2 / 3 + ((j + 0.5) / per * 2 - 1) * 0.26;
1549
+ pos[c] = sv * 2 / 3 + ((k + 0.5) / per * 2 - 1) * 0.26;
1550
+ D.push({ pos, cub, s });
1551
+ }
1552
+ }
1553
+ var t = 0, seq = [], mi = 0, mt = 0;
1554
+ function plan() {
1555
+ var sc = [], prev = -1;
1556
+ while (sc.length < COUNT) {
1557
+ var axis = Math.random() * 3 | 0, layer = (Math.random() * 3 | 0) - 1, key = axis * 3 + layer + 1;
1558
+ if (key === prev) continue;
1559
+ prev = key;
1560
+ sc.push({ axis, layer, dir: Math.random() < 0.5 ? -1 : 1 });
1561
+ }
1562
+ seq = sc.concat(sc.slice().reverse().map(function(m) {
1563
+ return { axis: m.axis, layer: m.layer, dir: -m.dir };
1564
+ }));
1565
+ }
1566
+ plan();
1567
+ return {
1568
+ time: function() {
1569
+ return t;
1570
+ },
1571
+ step: function(dt) {
1572
+ t += dt;
1573
+ mt += dt;
1574
+ if (mi < seq.length) {
1575
+ if (mt >= SLOT) {
1576
+ var m = seq[mi];
1577
+ D.forEach(function(d) {
1578
+ if (d.cub[m.axis] === m.layer) {
1579
+ spin(d.pos, m.axis, 0, m.dir);
1580
+ spin(d.cub, m.axis, 0, m.dir);
1581
+ }
1582
+ });
1583
+ mi++;
1584
+ mt -= SLOT;
1585
+ }
1586
+ } else if (mt >= HOLD) {
1587
+ plan();
1588
+ mi = 0;
1589
+ mt -= HOLD;
1590
+ }
1591
+ },
1592
+ draw: function(p, u) {
1593
+ var V = view(0.72 + 0.2 * Math.sin(t * TAU / 15), 0.5 + 0.07 * Math.sin(t * TAU / 21));
1594
+ var m = mi < seq.length ? seq[mi] : null;
1595
+ var th = m ? m.dir * Math.PI / 2 * ease(clamp01(mt / DUR)) : 0, cs = Math.cos(th), sn = Math.sin(th);
1596
+ var glow = m ? 0 : Math.sin(Math.PI * clamp01(mt / HOLD)), rb = R0 * (1 + 0.03 * glow);
1597
+ for (var i = 0; i < D.length; i++) {
1598
+ var d = D[i];
1599
+ v[0] = d.pos[0];
1600
+ v[1] = d.pos[1];
1601
+ v[2] = d.pos[2];
1602
+ if (m && d.cub[m.axis] === m.layer) spin(v, m.axis, cs, sn);
1603
+ var k2 = rb / Math.hypot(v[0], v[1], v[2]);
1604
+ turn(V, v[0] * k2, v[1] * k2, v[2] * k2, o);
1605
+ var shade = lit(o[0] / rb, o[1] / rb, o[2] / rb);
1606
+ persp(o[0], o[1], o[2], o);
1607
+ p.dot(u.cx + o[3] * u.R, u.cy + o[4] * u.R, u.rad((small ? 0.5 : 1.05) * o[5]), shade * (0.25 + 0.75 * (d.s + (1 - d.s) * 0.6 * glow)));
1608
+ }
1609
+ }
1610
+ };
1611
+ }
1612
+ function sweep(small, front) {
1613
+ var LAT = small ? 11 : 15, dens = small ? 26 : 40, R0 = 0.74, W = small ? 0.28 : 0.24, D = [], o = [0, 0, 0, 0, 0, 0];
1614
+ for (var a = 1; a < LAT; a++) {
1615
+ var lat = Math.PI * a / LAT, y = -Math.cos(lat), r = Math.sin(lat), n = Math.max(5, Math.round(r * dens));
1616
+ for (var b = 0; b < n; b++) {
1617
+ var th = TAU * b / n + a * 0.37;
1618
+ D.push([Math.cos(th) * r, y, Math.sin(th) * r]);
1619
+ }
1620
+ }
1621
+ front = front || function(t2) {
1622
+ return -1.3 + 2.6 * (t2 % 1.75) / 1.2;
1623
+ };
1624
+ var t = 0;
1625
+ return {
1626
+ time: function() {
1627
+ return t;
1628
+ },
1629
+ step: function(dt) {
1630
+ t += dt;
1631
+ },
1632
+ draw: function(p, u) {
1633
+ var V = view(0.35 * Math.sin(t * TAU / 17), 0.3 + 0.07 * Math.sin(t * TAU / 23)), f = front(t);
1634
+ for (var i = 0; i < D.length; i++) {
1635
+ turn(V, D[i][0], D[i][1], D[i][2], o);
1636
+ var nx = o[0], ny = o[1], nz = o[2], q = (nx * R0 - f) / W, w = Math.exp(-q * q), rho = R0 * (1 + 0.17 * w);
1637
+ persp(nx * rho, ny * rho, nz * rho, o);
1638
+ p.dot(u.cx + o[3] * u.R, u.cy + o[4] * u.R, u.rad((small ? 0.45 + 0.25 * w : 0.95 + 0.6 * w) * o[5]), lit(nx, ny, nz) * (0.75 + 0.25 * w) + 0.55 * w * smooth(0.3, -0.3, nz));
1639
+ }
1640
+ }
1641
+ };
1642
+ }
1643
+ function rowX(r, t) {
1644
+ return r.x0 + SPEED * (t % ((r.x1 - r.x0) / SPEED + REST));
1645
+ }
1646
+ function size(I) {
1647
+ var cw = I.cv.clientWidth, ch = I.cv.clientHeight;
1648
+ if (!cw || !ch) return;
1649
+ var small = Math.min(cw, ch) < SMALL, dpr = Math.min(devicePixelRatio || 1, small ? 3 : 2);
1650
+ var w = Math.round(cw * dpr), h = Math.round(ch * dpr);
1651
+ if (I.cv.width !== w || I.cv.height !== h) {
1652
+ I.cv.width = w;
1653
+ I.cv.height = h;
1654
+ }
1655
+ var R = Math.min(w, h) * 0.44, k = small ? dpr : R / 80, minR = (small ? 0.4 : 0.5) * dpr;
1656
+ I.w = w;
1657
+ I.h = h;
1658
+ I.u = { cx: w / 2, cy: h / 2, R, rad: function(d) {
1659
+ return Math.max(minR, d * k);
1660
+ } };
1661
+ I.ink = getComputedStyle(I.el).getPropertyValue("--thinking-ink").trim() || "#EDEDED";
1662
+ if (I.small !== small) {
1663
+ I.small = small;
1664
+ I.sim = I.kind === "sweep" ? sweep(small, I.row && function(t) {
1665
+ return (rowX(I.row, t) - I.row.cx) / I.row.R;
1666
+ }) : sphere(small);
1667
+ if (reduce.matches) for (var i = 0; i < 150; i++) I.sim.step(1 / 60);
1668
+ }
1669
+ if (I.row) {
1670
+ var L = I.label;
1671
+ I.row.cx = I.cv.offsetLeft + I.cv.offsetWidth / 2 - L.offsetLeft;
1672
+ I.row.R = R / dpr;
1673
+ I.row.x0 = I.row.cx - 1.3 * I.row.R;
1674
+ I.row.x1 = L.offsetWidth + HALO + 4;
1675
+ }
1676
+ }
1677
+ function frame(now) {
1678
+ if (!all.length) {
1679
+ running = false;
1680
+ return;
1681
+ }
1682
+ requestAnimationFrame(frame);
1683
+ var dt = Math.min(1 / 30, Math.max(0, (now - last) / 1e3));
1684
+ last = now;
1685
+ if (document.hidden) return;
1686
+ for (var i = 0; i < all.length; i++) {
1687
+ var I = all[i];
1688
+ if (!I.seen || !I.u) continue;
1689
+ if (reduce.matches) I.calm += dt;
1690
+ else I.sim.step(dt);
1691
+ I.ctx.clearRect(0, 0, I.w, I.h);
1692
+ I.p.gain = (I.small ? 1.35 : 1) * (reduce.matches ? 0.72 + 0.28 * Math.sin(I.calm * TAU / 3.2) : 1);
1693
+ I.sim.draw(I.p, I.u);
1694
+ I.p.flush(I.ctx, I.ink);
1695
+ if (I.row && !reduce.matches) I.label.style.setProperty("--thinking-x", rowX(I.row, I.sim.time()).toFixed(1) + "px");
1696
+ }
1697
+ }
1698
+ function thinking(el) {
1699
+ if (el.__thinking) return el.__thinking;
1700
+ var cv = el.querySelector(".thinking-orb");
1701
+ if (!cv) {
1702
+ cv = document.createElement("canvas");
1703
+ cv.className = "thinking-orb";
1704
+ cv.setAttribute("aria-hidden", "true");
1705
+ el.insertBefore(cv, el.firstChild);
1706
+ }
1707
+ var label = el.querySelector(".thinking-label"), kind = el.getAttribute("data-kind") === "sweep" ? "sweep" : "sphere";
1708
+ var I = {
1709
+ el,
1710
+ cv,
1711
+ ctx: cv.getContext("2d"),
1712
+ label,
1713
+ kind,
1714
+ row: kind === "sweep" && label ? { cx: 0, R: 1, x0: 0, x1: 1 } : null,
1715
+ sim: null,
1716
+ small: null,
1717
+ u: null,
1718
+ w: 0,
1719
+ h: 0,
1720
+ ink: "",
1721
+ seen: true,
1722
+ calm: 0,
1723
+ p: painter()
1724
+ };
1725
+ I.ro = new ResizeObserver(function() {
1726
+ size(I);
1727
+ });
1728
+ I.ro.observe(cv);
1729
+ if (label) I.ro.observe(label);
1730
+ I.io = new IntersectionObserver(function(es) {
1731
+ I.seen = es[es.length - 1].isIntersecting;
1732
+ });
1733
+ I.io.observe(el);
1734
+ size(I);
1735
+ all.push(I);
1736
+ if (!running) {
1737
+ running = true;
1738
+ last = performance.now();
1739
+ requestAnimationFrame(frame);
1740
+ }
1741
+ return el.__thinking = {
1742
+ destroy: function() {
1743
+ I.ro.disconnect();
1744
+ I.io.disconnect();
1745
+ all.splice(all.indexOf(I), 1);
1746
+ I.ctx.clearRect(0, 0, I.w, I.h);
1747
+ if (label) label.style.removeProperty("--thinking-x");
1748
+ el.__thinking = null;
1749
+ }
1750
+ };
1751
+ }
1752
+ function scan() {
1753
+ Array.prototype.forEach.call(document.querySelectorAll(".thinking"), thinking);
1754
+ }
1755
+ if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", scan);
1756
+ else scan();
1757
+ window2.thinking = thinking;
1758
+ })();
1759
+ return api4 = window2;
1760
+ }
1761
+ function mountThinking(host, o = {}) {
1762
+ css("craft-thinking", CSS4);
1763
+ const T = engine4();
1764
+ const kind = o.kind === "sweep" ? "sweep" : "sphere";
1765
+ const label = o.label == null || o.label === true ? "Thinking" : o.label === false ? "" : String(o.label);
1766
+ host.innerHTML = '<span class="thinking" data-kind="' + kind + '" role="status"><canvas class="thinking-orb" aria-hidden="true"></canvas>' + (label ? '<span class="thinking-label"></span>' : "") + "</span>";
1767
+ const el = host.firstChild;
1768
+ if (label) el.lastChild.textContent = label;
1769
+ else el.setAttribute("aria-label", "Thinking");
1770
+ if (o.size) el.style.setProperty("--thinking-size", typeof o.size === "number" ? o.size + "px" : String(o.size));
1771
+ const m = T.thinking(el);
1772
+ return { element: el, destroy() {
1773
+ m.destroy();
1774
+ host.innerHTML = "";
1775
+ } };
1776
+ }
1777
+
1395
1778
  // dist/elements.js
1396
1779
  function define(name, mount, options) {
1397
1780
  if (typeof customElements === "undefined" || customElements.get(name)) return;
@@ -1448,7 +1831,17 @@
1448
1831
  interactive: el.getAttribute("interactive") === "false" ? false : void 0,
1449
1832
  label: el.getAttribute("label") || void 0
1450
1833
  }));
1834
+ var thinkingTag = (kind) => (el, o) => {
1835
+ css("craft-thinking-host", "magic-sphere,sweep-sphere{display:inline-block;vertical-align:middle}");
1836
+ return mountThinking(el, { ...o, kind });
1837
+ };
1838
+ var thinkingOptions = (el) => ({
1839
+ size: el.getAttribute("size") ? isNaN(el.getAttribute("size")) ? el.getAttribute("size") : +el.getAttribute("size") : void 0,
1840
+ label: el.hasAttribute("label") ? el.getAttribute("label") : void 0
1841
+ });
1842
+ define("magic-sphere", thinkingTag("sphere"), thinkingOptions);
1843
+ define("sweep-sphere", thinkingTag("sweep"), thinkingOptions);
1451
1844
 
1452
1845
  // dist/cdn.js
1453
- window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank };
1846
+ window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking };
1454
1847
  })();
package/dist/elements.js CHANGED
@@ -1,10 +1,12 @@
1
- // <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card> and <piggy-bank>: the pieces as HTML tags, for pages without React.
2
- // They render into the page itself (no shadow root), so the glass can blend with what is behind it.
1
+ // <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card>, <piggy-bank>, <magic-sphere> and <sweep-sphere>: the
2
+ // pieces as HTML tags, for pages without React. They render into the page itself (no shadow root), so the glass can
3
+ // blend with what is behind it.
3
4
  import { mountLike } from './like.js';
4
5
  import { mountMagnet } from './magnet.js';
5
6
  import { mountGlassNav } from './glassnav.js';
6
7
  import { mountIridescentCard } from './card-mount.js';
7
8
  import { mountPiggyBank } from './piggy-mount.js';
9
+ import { mountThinking } from './thinking.js';
8
10
  import { css } from './style.js';
9
11
 
10
12
  function define(name, mount, options) {
@@ -65,4 +67,17 @@ define('piggy-bank', (el, o) => {
65
67
  label: el.getAttribute('label') || undefined
66
68
  }));
67
69
 
68
- export { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank };
70
+ // the thinking indicators: inline, beside text. size="32" (px) or any CSS length; label="Thinking" by default,
71
+ // label="" for the ball alone
72
+ const thinkingTag = (kind) => (el, o) => {
73
+ css('craft-thinking-host', 'magic-sphere,sweep-sphere{display:inline-block;vertical-align:middle}');
74
+ return mountThinking(el, { ...o, kind });
75
+ };
76
+ const thinkingOptions = (el) => ({
77
+ size: el.getAttribute('size') ? (isNaN(el.getAttribute('size')) ? el.getAttribute('size') : +el.getAttribute('size')) : undefined,
78
+ label: el.hasAttribute('label') ? el.getAttribute('label') : undefined
79
+ });
80
+ define('magic-sphere', thinkingTag('sphere'), thinkingOptions);
81
+ define('sweep-sphere', thinkingTag('sweep'), thinkingOptions);
82
+
83
+ export { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking };
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { mountMagnet } from './magnet.js';
6
6
  import { mountGlassNav } from './glassnav.js';
7
7
  import { mountIridescentCard } from './card-mount.js';
8
8
  import { mountPiggyBank } from './piggy-mount.js';
9
+ import { mountThinking } from './thinking.js';
9
10
 
10
11
  const HOST = { display: 'inline-block', lineHeight: 0, verticalAlign: 'middle' };
11
12
 
@@ -68,4 +69,22 @@ export function PiggyBank({ height = 560, autoplay, interactive, replay, label,
68
69
  return createElement('div', { ref: host, className, style: { position: 'relative', width: '100%', minWidth: 280, height, ...style } });
69
70
  }
70
71
 
71
- export { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank };
72
+ // The Magic sphere and the Sweep: a "thinking" indicator, a ball of dots with the word beside it. size: the ball, px or
73
+ // any CSS length (32 by default; under 64px it draws finer, denser dots made to sit beside text). label: the word
74
+ // ("Thinking" by default; '' for the ball alone). The host keeps its line height, so the word is never clipped.
75
+ function thinkingComponent(kind, name) {
76
+ function Thinking({ size, label, className, style }) {
77
+ const host = useRef(null);
78
+ useEffect(() => {
79
+ const m = mountThinking(host.current, { kind, size, label });
80
+ return () => m.destroy();
81
+ }, [size, label]);
82
+ return createElement('span', { ref: host, className, style: { display: 'inline-block', verticalAlign: 'middle', ...style } });
83
+ }
84
+ Thinking.displayName = name;
85
+ return Thinking;
86
+ }
87
+ export const MagicSphere = thinkingComponent('sphere', 'MagicSphere');
88
+ export const SweepSphere = thinkingComponent('sweep', 'SweepSphere');
89
+
90
+ export { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking };
@@ -0,0 +1,243 @@
1
+ // Magic sphere and Sweep · Craft Library, by Sara Lobo. thinking.css and thinking.js from the library, unchanged,
2
+ // around a mount.
3
+ import { css } from './style.js';
4
+ const CSS = "/* Thinking orbs · Craft Library, by Sara Lobo\n A ball of dots and the word beside it, for the wait while an answer is being written. thinking.js draws the ball\n and, for the Sweep, carries the ball's wave on through the word. Made for dark backgrounds: on a light one, set\n --thinking-ink (the dots and the light in the word) and --thinking-dim (the word at rest). */\n\n.thinking {\n --thinking-size: 32px;\n --thinking-ink: #EDEDED;\n --thinking-dim: #8A8A8A;\n position: relative;\n display: inline-flex;\n align-items: center;\n gap: 10px;\n vertical-align: middle;\n}\n/* the ball: drawn by thinking.js at this size. Under 64px it uses more, finer dots, made to sit beside text */\n.thinking-orb {\n display: block;\n flex: none;\n width: var(--thinking-size);\n height: var(--thinking-size);\n}\n/* the word: a light passes through it, left to right, on a loop */\n.thinking-label {\n font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', 'Helvetica Neue', system-ui, sans-serif;\n font-size: 14px;\n line-height: normal;\n letter-spacing: -.005em;\n white-space: nowrap;\n -webkit-font-smoothing: antialiased;\n text-rendering: optimizeLegibility;\n color: transparent;\n -webkit-background-clip: text;\n background-clip: text;\n background-image: linear-gradient(90deg, var(--thinking-dim) 0 35%, var(--thinking-ink) 50%, var(--thinking-dim) 65% 100%);\n background-size: 250% 100%;\n animation: thinking-shine 2.6s linear infinite;\n}\n@keyframes thinking-shine {\n from {\n background-position: 100% 0;\n }\n to {\n background-position: 0 0;\n }\n}\n/* the Sweep's word is lit by the ball's own wave: thinking.js moves --thinking-x across it */\n.thinking[data-kind=\"sweep\"] .thinking-label {\n --thinking-x: -999px;\n background-image: linear-gradient(90deg, var(--thinking-dim) calc(var(--thinking-x) - 26px), var(--thinking-ink) var(--thinking-x), var(--thinking-dim) calc(var(--thinking-x) + 26px));\n background-size: auto;\n animation: none;\n}\n\n/* less motion: the ball holds still and only breathes; the word stays plain */\n@media (prefers-reduced-motion: reduce) {\n .thinking-label {\n animation: none;\n background: none;\n color: var(--thinking-dim);\n }\n}\n";
5
+ let api;
6
+ function engine() {
7
+ if (api) return api;
8
+ const window = {};
9
+ // Thinking orbs · Craft Library, by Sara Lobo
10
+ // Two "thinking" indicators made of dots on a canvas, for the wait while an answer is being written:
11
+ // sphere · Magic sphere: a Rubik's cube on a ball, one grey tone per face. Its slices turn to scramble the pieces,
12
+ // then retrace their steps until every piece is home, and the ball lights up for a moment.
13
+ // sweep · Sweep: a dotted globe. A wave crosses it from left to right, lifting the dots it passes, then rests.
14
+ // Beside a label, the same wave runs on through the word at the same speed.
15
+ // Markup (styled by thinking.css):
16
+ // <span class="thinking" data-kind="sphere" role="status">
17
+ // <canvas class="thinking-orb" aria-hidden="true"></canvas><span class="thinking-label">Thinking</span>
18
+ // </span>
19
+ // Every .thinking on the page is set up as the page loads; for one added later, call thinking(el), which returns
20
+ // { destroy }. The ball takes the canvas's CSS size (--thinking-size). Under 64px it draws more, finer dots, made
21
+ // for sitting beside text; larger, it shows the full detail. Every motion runs on the frame's real time.
22
+ (function () {
23
+ var TAU = Math.PI * 2, SMALL = 64;
24
+ var SPEED = 85, REST = 0.55, HALO = 26; // the Sweep's wave beside a word: px per second, the pause, the light's reach
25
+ var reduce = matchMedia('(prefers-reduced-motion: reduce)');
26
+ var all = [], running = false, last = 0;
27
+
28
+ function clamp01(x) { return x < 0 ? 0 : x > 1 ? 1 : x; }
29
+ function smooth(a, b, x) { var t = clamp01((x - a) / (b - a)); return t * t * (3 - 2 * t); }
30
+ function ease(t) { return t < 0.5 ? 4 * t * t * t : 1 - Math.pow(-2 * t + 2, 3) / 2; }
31
+
32
+ // dots grouped by opacity, so a frame is a few fills instead of hundreds
33
+ function painter() {
34
+ var LEVELS = 16, d = [], n = [];
35
+ for (var i = 0; i < LEVELS; i++) { d.push([]); n.push(0); }
36
+ return {
37
+ gain: 1,
38
+ dot: function (x, y, r, a) {
39
+ a *= this.gain; if (a <= 0.02) return;
40
+ var b = Math.min(LEVELS - 1, (a * LEVELS) | 0), arr = d[b], k = n[b];
41
+ arr[k] = x; arr[k + 1] = y; arr[k + 2] = r; n[b] = k + 3;
42
+ },
43
+ flush: function (c, ink) {
44
+ c.fillStyle = ink;
45
+ for (var b = 0; b < LEVELS; b++) {
46
+ if (!n[b]) continue;
47
+ var arr = d[b];
48
+ c.globalAlpha = (b + 1) / LEVELS; c.beginPath();
49
+ for (var i = 0; i < n[b]; i += 3) { c.moveTo(arr[i] + arr[i + 2], arr[i + 1]); c.arc(arr[i], arr[i + 1], arr[i + 2], 0, TAU); }
50
+ c.fill(); n[b] = 0;
51
+ }
52
+ c.globalAlpha = 1;
53
+ }
54
+ };
55
+ }
56
+
57
+ // 3D: turn() rotates by the view into o[0..2] (y down, z away from the camera); persp() writes screen x, y and scale into o[3..5]
58
+ var CAM = 3.2, LX = -0.42, LY = -0.62, LZ = -0.66; // the light comes from the upper left, in front
59
+ function view(yaw, pitch) { return { cy: Math.cos(yaw), sy: Math.sin(yaw), cp: Math.cos(pitch), sp: Math.sin(pitch) }; }
60
+ function turn(V, x, y, z, o) {
61
+ var x1 = x * V.cy - z * V.sy, z1 = x * V.sy + z * V.cy;
62
+ o[0] = x1; o[1] = y * V.cp - z1 * V.sp; o[2] = y * V.sp + z1 * V.cp;
63
+ }
64
+ function persp(x, y, z, o) { var f = CAM / (CAM + z); o[3] = x * f; o[4] = y * f; o[5] = f; }
65
+ // lit from the upper left; the far side of the ball fades out, so it reads solid
66
+ function lit(nx, ny, nz) { return (0.32 + 0.68 * Math.max(0, nx * LX + ny * LY + nz * LZ)) * (0.07 + 0.93 * smooth(0.28, -0.3, nz)); }
67
+
68
+ // ── Magic sphere: 3×3 stickers per face, projected from a cube onto the ball. Each dot knows its cubie (-1…1 per axis).
69
+ // Five random slice turns of 90°, then the same five undone; when every piece is home the ball lights up, and again.
70
+ var TONES = [1, 0.5, 0.78, 0.3, 0.62, 0.18];
71
+ function spin(q, axis, cs, sn) { var b = (axis + 1) % 3, c = (axis + 2) % 3, qb = q[b], qc = q[c]; q[b] = qb * cs - qc * sn; q[c] = qb * sn + qc * cs; }
72
+ function sphere(small) {
73
+ var per = small ? 2 : 4, COUNT = 5, SLOT = 0.5, DUR = 0.42, HOLD = 1.1, R0 = 0.8, D = [], v = [0, 0, 0], o = [0, 0, 0, 0, 0, 0];
74
+ for (var a = 0; a < 3; a++) for (var sg = 1; sg >= -1; sg -= 2) {
75
+ var s = TONES[a * 2 + (sg > 0 ? 0 : 1)], b = (a + 1) % 3, c = (a + 2) % 3;
76
+ for (var su = -1; su <= 1; su++) for (var sv = -1; sv <= 1; sv++) for (var j = 0; j < per; j++) for (var k = 0; k < per; k++) {
77
+ var pos = [0, 0, 0], cub = [0, 0, 0];
78
+ pos[a] = sg; cub[a] = sg; cub[b] = su; cub[c] = sv;
79
+ pos[b] = su * 2 / 3 + ((j + 0.5) / per * 2 - 1) * 0.26;
80
+ pos[c] = sv * 2 / 3 + ((k + 0.5) / per * 2 - 1) * 0.26;
81
+ D.push({ pos: pos, cub: cub, s: s });
82
+ }
83
+ }
84
+ var t = 0, seq = [], mi = 0, mt = 0;
85
+ function plan() {
86
+ var sc = [], prev = -1;
87
+ while (sc.length < COUNT) {
88
+ var axis = (Math.random() * 3) | 0, layer = ((Math.random() * 3) | 0) - 1, key = axis * 3 + layer + 1;
89
+ if (key === prev) continue;
90
+ prev = key; sc.push({ axis: axis, layer: layer, dir: Math.random() < 0.5 ? -1 : 1 });
91
+ }
92
+ seq = sc.concat(sc.slice().reverse().map(function (m) { return { axis: m.axis, layer: m.layer, dir: -m.dir }; }));
93
+ }
94
+ plan();
95
+ return {
96
+ time: function () { return t; },
97
+ step: function (dt) {
98
+ t += dt; mt += dt;
99
+ if (mi < seq.length) {
100
+ if (mt >= SLOT) { // the turn is done: write it into the dots, exactly 90°
101
+ var m = seq[mi];
102
+ D.forEach(function (d) { if (d.cub[m.axis] === m.layer) { spin(d.pos, m.axis, 0, m.dir); spin(d.cub, m.axis, 0, m.dir); } });
103
+ mi++; mt -= SLOT;
104
+ }
105
+ } else if (mt >= HOLD) { plan(); mi = 0; mt -= HOLD; }
106
+ },
107
+ draw: function (p, u) {
108
+ var V = view(0.72 + 0.2 * Math.sin(t * TAU / 15), 0.5 + 0.07 * Math.sin(t * TAU / 21));
109
+ var m = mi < seq.length ? seq[mi] : null;
110
+ var th = m ? m.dir * Math.PI / 2 * ease(clamp01(mt / DUR)) : 0, cs = Math.cos(th), sn = Math.sin(th);
111
+ var glow = m ? 0 : Math.sin(Math.PI * clamp01(mt / HOLD)), rb = R0 * (1 + 0.03 * glow);
112
+ for (var i = 0; i < D.length; i++) {
113
+ var d = D[i];
114
+ v[0] = d.pos[0]; v[1] = d.pos[1]; v[2] = d.pos[2];
115
+ if (m && d.cub[m.axis] === m.layer) spin(v, m.axis, cs, sn);
116
+ var k = rb / Math.hypot(v[0], v[1], v[2]);
117
+ turn(V, v[0] * k, v[1] * k, v[2] * k, o);
118
+ var shade = lit(o[0] / rb, o[1] / rb, o[2] / rb);
119
+ persp(o[0], o[1], o[2], o);
120
+ p.dot(u.cx + o[3] * u.R, u.cy + o[4] * u.R, u.rad((small ? 0.5 : 1.05) * o[5]), shade * (0.25 + 0.75 * (d.s + (1 - d.s) * 0.6 * glow)));
121
+ }
122
+ }
123
+ };
124
+ }
125
+
126
+ // ── Sweep: rings of latitude. front(t) places the wave in ball radii; on its own it crosses in 1.2 s and rests 0.55 s.
127
+ function sweep(small, front) {
128
+ var LAT = small ? 11 : 15, dens = small ? 26 : 40, R0 = 0.74, W = small ? 0.28 : 0.24, D = [], o = [0, 0, 0, 0, 0, 0];
129
+ for (var a = 1; a < LAT; a++) {
130
+ var lat = Math.PI * a / LAT, y = -Math.cos(lat), r = Math.sin(lat), n = Math.max(5, Math.round(r * dens));
131
+ for (var b = 0; b < n; b++) { var th = TAU * b / n + a * 0.37; D.push([Math.cos(th) * r, y, Math.sin(th) * r]); }
132
+ }
133
+ front = front || function (t) { return -1.3 + 2.6 * (t % 1.75) / 1.2; };
134
+ var t = 0;
135
+ return {
136
+ time: function () { return t; },
137
+ step: function (dt) { t += dt; },
138
+ draw: function (p, u) {
139
+ var V = view(0.35 * Math.sin(t * TAU / 17), 0.3 + 0.07 * Math.sin(t * TAU / 23)), f = front(t);
140
+ for (var i = 0; i < D.length; i++) {
141
+ turn(V, D[i][0], D[i][1], D[i][2], o);
142
+ var nx = o[0], ny = o[1], nz = o[2], q = (nx * R0 - f) / W, w = Math.exp(-q * q), rho = R0 * (1 + 0.17 * w);
143
+ persp(nx * rho, ny * rho, nz * rho, o);
144
+ p.dot(u.cx + o[3] * u.R, u.cy + o[4] * u.R, u.rad((small ? 0.45 + 0.25 * w : 0.95 + 0.6 * w) * o[5]), lit(nx, ny, nz) * (0.75 + 0.25 * w) + 0.55 * w * smooth(0.3, -0.3, nz));
145
+ }
146
+ }
147
+ };
148
+ }
149
+
150
+ // beside a word, the Sweep's wave is one run in px along the row: it starts left of the ball, crosses it, goes on
151
+ // through the word and rests (positions are relative to the word's left edge)
152
+ function rowX(r, t) { return r.x0 + SPEED * (t % ((r.x1 - r.x0) / SPEED + REST)); }
153
+
154
+ function size(I) {
155
+ var cw = I.cv.clientWidth, ch = I.cv.clientHeight;
156
+ if (!cw || !ch) return;
157
+ var small = Math.min(cw, ch) < SMALL, dpr = Math.min(devicePixelRatio || 1, small ? 3 : 2);
158
+ var w = Math.round(cw * dpr), h = Math.round(ch * dpr);
159
+ if (I.cv.width !== w || I.cv.height !== h) { I.cv.width = w; I.cv.height = h; }
160
+ var R = Math.min(w, h) * 0.44, k = small ? dpr : R / 80, minR = (small ? 0.4 : 0.5) * dpr;
161
+ I.w = w; I.h = h;
162
+ I.u = { cx: w / 2, cy: h / 2, R: R, rad: function (d) { return Math.max(minR, d * k); } };
163
+ I.ink = getComputedStyle(I.el).getPropertyValue('--thinking-ink').trim() || '#EDEDED';
164
+ if (I.small !== small) {
165
+ I.small = small;
166
+ I.sim = I.kind === 'sweep' ? sweep(small, I.row && function (t) { return (rowX(I.row, t) - I.row.cx) / I.row.R; }) : sphere(small);
167
+ if (reduce.matches) for (var i = 0; i < 150; i++) I.sim.step(1 / 60); // a still pose, mid-thought
168
+ }
169
+ if (I.row) {
170
+ var L = I.label;
171
+ I.row.cx = I.cv.offsetLeft + I.cv.offsetWidth / 2 - L.offsetLeft; I.row.R = R / dpr;
172
+ I.row.x0 = I.row.cx - 1.3 * I.row.R; I.row.x1 = L.offsetWidth + HALO + 4;
173
+ }
174
+ }
175
+
176
+ function frame(now) {
177
+ if (!all.length) { running = false; return; }
178
+ requestAnimationFrame(frame);
179
+ var dt = Math.min(1 / 30, Math.max(0, (now - last) / 1000)); last = now;
180
+ if (document.hidden) return;
181
+ for (var i = 0; i < all.length; i++) {
182
+ var I = all[i];
183
+ if (!I.seen || !I.u) continue;
184
+ if (reduce.matches) I.calm += dt; else I.sim.step(dt);
185
+ I.ctx.clearRect(0, 0, I.w, I.h);
186
+ I.p.gain = (I.small ? 1.35 : 1) * (reduce.matches ? 0.72 + 0.28 * Math.sin(I.calm * TAU / 3.2) : 1);
187
+ I.sim.draw(I.p, I.u);
188
+ I.p.flush(I.ctx, I.ink);
189
+ if (I.row && !reduce.matches) I.label.style.setProperty('--thinking-x', (rowX(I.row, I.sim.time())).toFixed(1) + 'px');
190
+ }
191
+ }
192
+
193
+ function thinking(el) {
194
+ if (el.__thinking) return el.__thinking;
195
+ var cv = el.querySelector('.thinking-orb');
196
+ if (!cv) {
197
+ cv = document.createElement('canvas'); cv.className = 'thinking-orb'; cv.setAttribute('aria-hidden', 'true');
198
+ el.insertBefore(cv, el.firstChild);
199
+ }
200
+ var label = el.querySelector('.thinking-label'), kind = el.getAttribute('data-kind') === 'sweep' ? 'sweep' : 'sphere';
201
+ var I = { el: el, cv: cv, ctx: cv.getContext('2d'), label: label, kind: kind, row: kind === 'sweep' && label ? { cx: 0, R: 1, x0: 0, x1: 1 } : null,
202
+ sim: null, small: null, u: null, w: 0, h: 0, ink: '', seen: true, calm: 0, p: painter() };
203
+ I.ro = new ResizeObserver(function () { size(I); });
204
+ I.ro.observe(cv);
205
+ if (label) I.ro.observe(label); // the word's width moves with its font and language
206
+ I.io = new IntersectionObserver(function (es) { I.seen = es[es.length - 1].isIntersecting; });
207
+ I.io.observe(el);
208
+ size(I);
209
+ all.push(I);
210
+ if (!running) { running = true; last = performance.now(); requestAnimationFrame(frame); }
211
+ return (el.__thinking = {
212
+ destroy: function () {
213
+ I.ro.disconnect(); I.io.disconnect();
214
+ all.splice(all.indexOf(I), 1);
215
+ I.ctx.clearRect(0, 0, I.w, I.h);
216
+ if (label) label.style.removeProperty('--thinking-x');
217
+ el.__thinking = null;
218
+ }
219
+ });
220
+ }
221
+
222
+ function scan() { Array.prototype.forEach.call(document.querySelectorAll('.thinking'), thinking); }
223
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', scan); else scan();
224
+ window.thinking = thinking;
225
+ })();
226
+
227
+ return (api = window);
228
+ }
229
+
230
+ // host: the element it goes in. o: { kind: 'sphere' | 'sweep', size (px or any CSS length, default 32),
231
+ // label (the word beside the ball, default "Thinking"; '' or false for the ball alone) }
232
+ export function mountThinking(host, o = {}) {
233
+ css('craft-thinking', CSS);
234
+ const T = engine();
235
+ const kind = o.kind === 'sweep' ? 'sweep' : 'sphere';
236
+ const label = o.label == null || o.label === true ? 'Thinking' : o.label === false ? '' : String(o.label);
237
+ host.innerHTML = '<span class="thinking" data-kind="' + kind + '" role="status"><canvas class="thinking-orb" aria-hidden="true"></canvas>' + (label ? '<span class="thinking-label"></span>' : '') + '</span>';
238
+ const el = host.firstChild;
239
+ if (label) el.lastChild.textContent = label; else el.setAttribute('aria-label', 'Thinking');
240
+ if (o.size) el.style.setProperty('--thinking-size', typeof o.size === 'number' ? o.size + 'px' : String(o.size));
241
+ const m = T.thinking(el);
242
+ return { element: el, destroy() { m.destroy(); host.innerHTML = ''; } };
243
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "craft-library",
3
- "version": "0.3.0",
4
- "description": "Craft Library by Sara Lobo: a liquid glass nav bar, a bursting heart, a magnetic icon, an iridescent 3D card and a glass piggy bank, for React or plain HTML",
3
+ "version": "0.4.0",
4
+ "description": "Craft Library by Sara Lobo: a liquid glass nav bar, a bursting heart, a magnetic icon, an iridescent 3D card, a glass piggy bank and two thinking indicators (Magic sphere, Sweep), for React or plain HTML",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
@@ -47,7 +47,11 @@
47
47
  "credit-card",
48
48
  "piggy-bank",
49
49
  "congratulations",
50
- "glass"
50
+ "glass",
51
+ "thinking-indicator",
52
+ "loader",
53
+ "ai",
54
+ "particles"
51
55
  ],
52
56
  "author": "Sara Lobo",
53
57
  "license": "MIT",