craft-library 0.5.0 → 0.6.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
  # Sara Lobo 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, a glass piggy bank, two "thinking" indicators, the Magic sphere and the Sweep, and the Panorama, a hero for moving pictures. 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 two of the 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, two "thinking" indicators, the Magic sphere and the Sweep, the Panorama, a hero for moving pictures, and the Map pin, an address picker. 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 three of the 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,6 +101,29 @@ 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
+ ## Map pin
105
+
106
+ A 3D address picker: a hollow frosted-glass map pin over a dark city map in perspective. A click or a tap on a street sends the pin flying on springs to the nearest curb: it lifts, its head leans with the acceleration, it rises a touch to aim, falls under gravity and sticks its tip in, the glass vibrating about it. The map is sharp where it lands and soft further away, the streets carry their names, and a card along the bottom shows the address, with the number on the right side of the street, to confirm. With a mouse, hovering shows the address a click would give. The city and its names are invented, to show the gesture.
107
+
108
+ ```jsx
109
+ import { MapPin } from 'craft-library';
110
+
111
+ <MapPin height={560} onConfirm={(a) => saveAddress(a)} /> // saveAddress: your own handler
112
+ ```
113
+
114
+ | Prop | | |
115
+ |---|---|---|
116
+ | `height` | `number \| string` | The box it fills: 100% wide and this tall. Default `560`. |
117
+ | `onChange` | `(address) => void` | Each address the pin is sent to: `{ address, neighbourhood, place }`. |
118
+ | `onConfirm` | `(address) => void` | When Confirm is pressed, with the same address. |
119
+ | `labels` | `{ title, confirm, confirmed }` | The card's words. Defaults `"Delivery address"`, `"Confirm"`, `"Confirmed"`. |
120
+ | `replay` | `any` | Change it (a counter, say) to fly the pin off and back in. |
121
+ | `autoplay` | `boolean` | `false` keeps the pin out until `replay` changes. Default `true`. |
122
+ | `fonts` | `boolean` | `false` doesn't load DM Sans from Google Fonts. Default `true`. |
123
+ | `label` | `string` | The map's accessible name. |
124
+
125
+ 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 paints its own dark ground.
126
+
104
127
  ## Magic sphere and Sweep
105
128
 
106
129
  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.
@@ -145,7 +168,7 @@ It is WebGL with no dependencies, so one per screen is best. Your clip and pictu
145
168
 
146
169
  ## Plain HTML
147
170
 
148
- One script registers eight tags. Same pieces, same props as attributes.
171
+ One script registers nine tags. Same pieces, same props as attributes.
149
172
 
150
173
  ```html
151
174
  <script src="https://cdn.jsdelivr.net/npm/craft-library@0/dist/craft.js"></script>
@@ -158,12 +181,13 @@ One script registers eight tags. Same pieces, same props as attributes.
158
181
  <magic-sphere></magic-sphere> <!-- label="Thinking" (default; label="" for the ball alone), size="32" -->
159
182
  <sweep-sphere></sweep-sphere>
160
183
  <panorama-reel heading="Lorem ipsum"></panorama-reel> <!-- text, text-italic; video and stills as JSON; el.replay(); 100% wide, 640px tall unless you size it -->
184
+ <map-pin></map-pin> <!-- "pinchange" and "pinconfirm" events: e.detail { address, neighbourhood, place }; label-title, label-confirm, label-confirmed; el.replay(); 100% wide, 560px tall unless you size it -->
161
185
  ```
162
186
 
163
187
  With a bundler and no React: `import 'craft-library/elements'`.
164
188
 
165
189
  ## Motion and access
166
190
 
167
- 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, the thinking balls hold still and only breathe, and the panorama skips its opening and starts settled. The panorama's title is a heading; its screens and cursor are decorative.
191
+ 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 and the map pin's map are labelled images; the pin's address is announced as it changes and Confirm is a real button. 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, the thinking balls hold still and only breathe, the panorama skips its opening and starts settled, and the map pin appears in place without its flight. The panorama's title is a heading; its screens and cursor are decorative.
168
192
 
169
193
  MIT · [Sara Lobo Library](https://saralobolibrary.com)
package/dist/cdn.js CHANGED
@@ -1,4 +1,4 @@
1
- // the one-file build for a <script> tag: registers the eight tags and exposes the mount functions (the 3D pieces' scenes
1
+ // the one-file build for a <script> tag: registers the nine 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, and so are the Panorama's clip and pictures)
3
- import { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking, mountPanoramaReel } from './elements.js';
4
- window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking, mountPanoramaReel };
3
+ import { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking, mountPanoramaReel, mountMapPin } from './elements.js';
4
+ window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking, mountPanoramaReel, mountMapPin };
package/dist/craft.js CHANGED
@@ -1,4 +1,4 @@
1
- /* Sara Lobo Library · liquid glass nav bar, bursting heart, magnetic icon, iridescent card, glass piggy bank, magic sphere, sweep, panorama · <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card>, <piggy-bank>, <magic-sphere>, <sweep-sphere>, <panorama-reel> */
1
+ /* Sara Lobo Library · liquid glass nav bar, bursting heart, magnetic icon, iridescent card, glass piggy bank, magic sphere, sweep, panorama, map pin · <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card>, <piggy-bank>, <magic-sphere>, <sweep-sphere>, <panorama-reel>, <map-pin> */
2
2
  (() => {
3
3
  // dist/style.js
4
4
  var done = /* @__PURE__ */ new Set();
@@ -2469,6 +2469,56 @@ void main() {
2469
2469
  return mountPanorama(host, opts);
2470
2470
  }
2471
2471
 
2472
+ // src/pin-load-cdn.js
2473
+ var me4 = typeof document !== "undefined" && document.currentScript ? document.currentScript.src : "";
2474
+ var base3 = me4 ? me4.slice(0, me4.lastIndexOf("/") + 1) : "https://cdn.jsdelivr.net/npm/craft-library@0/dist/";
2475
+ var loadPin = () => import(base3 + "pin.cdn.js");
2476
+
2477
+ // dist/pin-mount.js
2478
+ function mountMapPin(host, o = {}) {
2479
+ let pin = null, gone = false, wantReplay = false;
2480
+ const opts = { labels: o.labels, label: o.label, manual: o.manual };
2481
+ if (o.autoplay === false) opts.autoplay = false;
2482
+ if (o.fonts === false) opts.fonts = false;
2483
+ const change = (e) => {
2484
+ if (o.onChange) o.onChange(e.detail);
2485
+ };
2486
+ const confirm = (e) => {
2487
+ if (o.onConfirm) o.onConfirm(e.detail);
2488
+ };
2489
+ host.addEventListener("pinchange", change);
2490
+ host.addEventListener("pinconfirm", confirm);
2491
+ const ready = loadPin().then((m) => {
2492
+ if (gone) return null;
2493
+ pin = m.mountPin(host, opts);
2494
+ if (wantReplay) pin.replay();
2495
+ return pin.ready.then(() => pin);
2496
+ });
2497
+ return {
2498
+ ready,
2499
+ replay: () => {
2500
+ if (pin) pin.replay();
2501
+ else wantReplay = true;
2502
+ },
2503
+ confirm: () => {
2504
+ if (pin) pin.confirm();
2505
+ },
2506
+ tick: (dt, now) => {
2507
+ if (pin) pin.tick(dt, now);
2508
+ },
2509
+ get address() {
2510
+ return pin ? pin.address : null;
2511
+ },
2512
+ destroy() {
2513
+ gone = true;
2514
+ host.removeEventListener("pinchange", change);
2515
+ host.removeEventListener("pinconfirm", confirm);
2516
+ if (pin) pin.destroy();
2517
+ pin = null;
2518
+ }
2519
+ };
2520
+ }
2521
+
2472
2522
  // dist/elements.js
2473
2523
  function define(name, mount, options) {
2474
2524
  if (typeof customElements === "undefined" || customElements.get(name)) return;
@@ -2560,7 +2610,18 @@ void main() {
2560
2610
  cursor: el.getAttribute("cursor") === "false" ? false : void 0,
2561
2611
  fonts: el.getAttribute("fonts") === "false" ? false : void 0
2562
2612
  }));
2613
+ define("map-pin", (el, o) => {
2614
+ css("craft-pin-host", ":where(map-pin){display:block;position:relative;width:100%;height:560px}");
2615
+ const m = mountMapPin(el, o);
2616
+ el.replay = m.replay;
2617
+ return m;
2618
+ }, (el) => ({
2619
+ labels: { title: el.getAttribute("label-title") ?? void 0, confirm: el.getAttribute("label-confirm") ?? void 0, confirmed: el.getAttribute("label-confirmed") ?? void 0 },
2620
+ label: el.getAttribute("label") || void 0,
2621
+ autoplay: el.getAttribute("autoplay") === "false" ? false : void 0,
2622
+ fonts: el.getAttribute("fonts") === "false" ? false : void 0
2623
+ }));
2563
2624
 
2564
2625
  // dist/cdn.js
2565
- window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking, mountPanoramaReel };
2626
+ window.CraftLibrary = { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking, mountPanoramaReel, mountMapPin };
2566
2627
  })();
package/dist/elements.js CHANGED
@@ -1,4 +1,4 @@
1
- // <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card>, <piggy-bank>, <magic-sphere>, <sweep-sphere> and <panorama-reel>: the
1
+ // <glass-nav>, <like-button>, <magnetic-icon>, <iridescent-card>, <piggy-bank>, <magic-sphere>, <sweep-sphere>, <panorama-reel> and <map-pin>: the
2
2
  // pieces as HTML tags, for pages without React. They render into the page itself (no shadow root), so the glass can
3
3
  // blend with what is behind it.
4
4
  import { mountLike } from './like.js';
@@ -8,6 +8,7 @@ import { mountIridescentCard } from './card-mount.js';
8
8
  import { mountPiggyBank } from './piggy-mount.js';
9
9
  import { mountThinking } from './thinking.js';
10
10
  import { mountPanoramaReel } from './panorama-mount.js';
11
+ import { mountMapPin } from './pin-mount.js';
11
12
  import { css } from './style.js';
12
13
 
13
14
  function define(name, mount, options) {
@@ -101,4 +102,19 @@ define('panorama-reel', (el, o) => {
101
102
  fonts: el.getAttribute('fonts') === 'false' ? false : undefined
102
103
  }));
103
104
 
104
- export { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking, mountPanoramaReel };
105
+ // the map pin fills the tag: a block, 100% wide and 560px tall unless the page sizes it. Each address the pin is sent to
106
+ // fires "pinchange" and a confirmation "pinconfirm", both with e.detail { address, neighbourhood, place }. The words:
107
+ // label-title, label-confirm, label-confirmed. el.replay() flies the pin off and back in.
108
+ define('map-pin', (el, o) => {
109
+ css('craft-pin-host', ':where(map-pin){display:block;position:relative;width:100%;height:560px}');
110
+ const m = mountMapPin(el, o);
111
+ el.replay = m.replay;
112
+ return m;
113
+ }, (el) => ({
114
+ labels: { title: el.getAttribute('label-title') ?? undefined, confirm: el.getAttribute('label-confirm') ?? undefined, confirmed: el.getAttribute('label-confirmed') ?? undefined },
115
+ label: el.getAttribute('label') || undefined,
116
+ autoplay: el.getAttribute('autoplay') === 'false' ? false : undefined,
117
+ fonts: el.getAttribute('fonts') === 'false' ? false : undefined
118
+ }));
119
+
120
+ export { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking, mountPanoramaReel, mountMapPin };
package/dist/index.js CHANGED
@@ -8,6 +8,7 @@ import { mountIridescentCard } from './card-mount.js';
8
8
  import { mountPiggyBank } from './piggy-mount.js';
9
9
  import { mountThinking } from './thinking.js';
10
10
  import { mountPanoramaReel } from './panorama-mount.js';
11
+ import { mountMapPin } from './pin-mount.js';
11
12
 
12
13
  const HOST = { display: 'inline-block', lineHeight: 0, verticalAlign: 'middle' };
13
14
 
@@ -102,4 +103,19 @@ export function PanoramaReel({ height = 640, title, text, textItalic, video, sti
102
103
  return createElement('div', { ref: host, className, style: { position: 'relative', width: '100%', height, ...style } });
103
104
  }
104
105
 
105
- export { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking, mountPanoramaReel };
106
+ // The map pin fills the box it renders (100% wide and `height` tall, 560px by default) with a city map and a glass pin:
107
+ // a click on a street sends the pin to its curb, and the card under the map confirms the address. onChange and onConfirm
108
+ // get { address, neighbourhood, place }. labels: { title, confirm, confirmed }, the card's words. replay: change it (a
109
+ // counter) to fly the pin off and back in. It loads its 3D scene (three.js) the first time it mounts.
110
+ export function MapPin({ height = 560, labels, label, autoplay, fonts, replay, onChange, onConfirm, className, style }) {
111
+ const host = useRef(null), pin = useRef(null), last = useRef(replay), change = useLatest(onChange), confirm = useLatest(onConfirm);
112
+ const key = JSON.stringify([labels, label, autoplay, fonts]);
113
+ useEffect(() => {
114
+ const m = pin.current = mountMapPin(host.current, { labels, label, autoplay, fonts, onChange: (a) => change.current && change.current(a), onConfirm: (a) => confirm.current && confirm.current(a) });
115
+ return () => { m.destroy(); pin.current = null; };
116
+ }, [key]);
117
+ useEffect(() => { if (last.current === replay) return; last.current = replay; if (pin.current) pin.current.replay(); }, [replay]);
118
+ return createElement('div', { ref: host, className, style: { position: 'relative', width: '100%', height, ...style } });
119
+ }
120
+
121
+ export { mountLike, mountMagnet, mountGlassNav, mountIridescentCard, mountPiggyBank, mountThinking, mountPanoramaReel, mountMapPin };
@@ -1,2 +1,2 @@
1
1
  // where the Panorama's own clip and pictures are: jsDelivr, at this version of the package
2
- export const ASSETS = 'https://cdn.jsdelivr.net/npm/craft-library@0.5.0/dist/panorama/';
2
+ export const ASSETS = 'https://cdn.jsdelivr.net/npm/craft-library@0.6.0/dist/panorama/';
@@ -0,0 +1,2 @@
1
+ // where the map pin comes from: for bundlers, the package's pin.js (it imports the `three` dependency)
2
+ export const loadPin = () => import('./pin.js');
@@ -0,0 +1,30 @@
1
+ // Map pin: the mount around the library's pin.js. pin.js, and three.js with it, loads only when a map pin is mounted,
2
+ // so the other pieces stay light.
3
+ import { loadPin } from './pin-load.js';
4
+
5
+ // host: the element the map fills (give it a size; it is made for about 560 × 560). o: { labels: { title, confirm,
6
+ // confirmed }, label (the map's accessible name), autoplay (false: the pin waits for replay()), fonts (false: don't load
7
+ // DM Sans), onChange(address), onConfirm(address) }; address: { address, neighbourhood, place }
8
+ export function mountMapPin(host, o = {}) {
9
+ let pin = null, gone = false, wantReplay = false;
10
+ const opts = { labels: o.labels, label: o.label, manual: o.manual };
11
+ if (o.autoplay === false) opts.autoplay = false;
12
+ if (o.fonts === false) opts.fonts = false;
13
+ const change = (e) => { if (o.onChange) o.onChange(e.detail); };
14
+ const confirm = (e) => { if (o.onConfirm) o.onConfirm(e.detail); };
15
+ host.addEventListener('pinchange', change); host.addEventListener('pinconfirm', confirm);
16
+ const ready = loadPin().then((m) => {
17
+ if (gone) return null;
18
+ pin = m.mountPin(host, opts);
19
+ if (wantReplay) pin.replay();
20
+ return pin.ready.then(() => pin);
21
+ });
22
+ return {
23
+ ready,
24
+ replay: () => { if (pin) pin.replay(); else wantReplay = true; },
25
+ confirm: () => { if (pin) pin.confirm(); },
26
+ tick: (dt, now) => { if (pin) pin.tick(dt, now); },
27
+ get address() { return pin ? pin.address : null; },
28
+ destroy() { gone = true; host.removeEventListener('pinchange', change); host.removeEventListener('pinconfirm', confirm); if (pin) pin.destroy(); pin = null; }
29
+ };
30
+ }