canvas-globe 1.1.1 → 1.2.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/CHANGELOG.md CHANGED
@@ -6,6 +6,30 @@ All notable changes to this package are documented here. The format follows
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.2.0] - 2026-09-18
10
+
11
+ ### Added
12
+
13
+ - Added an opt-in effect system with 54 composable effects, three paint stages,
14
+ deterministic timeline controls, pointer state, path tracing, and land-point
15
+ sampling through `canvas-globe/fx`.
16
+ - Added nine chart layers in `canvas-globe/charts`, six reversible recipes in
17
+ `canvas-globe/recipes`, and four markup-independent DOM controls in
18
+ `canvas-globe/controls`.
19
+ - Added `canvas-globe/places`, an optional offline search module containing
20
+ 6,772 populated places from Natural Earth.
21
+ - Added 26 effect-authoring helpers for easing, deterministic particles,
22
+ scratch buffers, spherical geometry, HUD elements, and pointer interaction.
23
+ - Added five effect and interaction demo pages plus export, type, package, and
24
+ documentation coverage for all new public entry points.
25
+
26
+ ### Changed
27
+
28
+ - Made auto-rotation, arcs, orbits, and marker pulses follow the seekable clock
29
+ during frame rendering.
30
+ - Kept all add-on modules outside the core entry point, leaving the core bundle
31
+ at 125.0 KB gzipped.
32
+
9
33
  ## [1.1.1] - 2026-09-17
10
34
 
11
35
  - Prevented the production licensing presentation from covering live globes on
package/README.md CHANGED
@@ -56,6 +56,10 @@ key, tile service, or runtime network request.
56
56
  - **Viewer location:** estimate a region from the browser time zone without a permission prompt
57
57
  - **Live pings:** display recent activity without requiring a CanvasGlobe backend
58
58
  - **Recording:** export a WebM clip in the browser
59
+ - **Optional effects:** 54 seekable visual and interaction effects in `canvas-globe/fx`
60
+ - **Chart layers:** nine animated geographic chart treatments in `canvas-globe/charts`
61
+ - **Recipes and controls:** complete looks and DOM bindings without adding framework code
62
+ - **Place search:** an optional 6,772-place search table with no runtime API request
59
63
  - **Presets:** ten included visual styles
60
64
  - **Day and night:** calculate the solar terminator for a given time
61
65
  - **Four projections:** orthographic, equirectangular, Mercator, and Natural Earth
@@ -135,19 +139,19 @@ your existing application rather than installing a second React copy.
135
139
  For a plain `<script>` installation, use the versioned UMD build:
136
140
 
137
141
  ```html
138
- <script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.1.1/dist/canvas-globe.umd.js"></script>
142
+ <script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.2.0/dist/canvas-globe.umd.js"></script>
139
143
  ```
140
144
 
141
145
  The same npm release is also available from UNPKG:
142
146
 
143
147
  ```html
144
- <script src="https://unpkg.com/canvas-globe@1.1.1/dist/canvas-globe.umd.js"></script>
148
+ <script src="https://unpkg.com/canvas-globe@1.2.0/dist/canvas-globe.umd.js"></script>
145
149
  ```
146
150
 
147
151
  Modern browsers can import the package through an ESM CDN:
148
152
 
149
153
  ```js
150
- import { createGlobe } from "https://esm.sh/canvas-globe@1.1.1";
154
+ import { createGlobe } from "https://esm.sh/canvas-globe@1.2.0";
151
155
  ```
152
156
 
153
157
  Pin an exact version in production so a future release cannot change a deployed page unexpectedly.
@@ -167,7 +171,7 @@ import { createGlobe } from "canvas-globe";
167
171
  Or drop the UMD build on a page with no build step at all:
168
172
 
169
173
  ```html
170
- <script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.1.1/dist/canvas-globe.umd.js"></script>
174
+ <script src="https://cdn.jsdelivr.net/npm/canvas-globe@1.2.0/dist/canvas-globe.umd.js"></script>
171
175
  <canvas id="globe" style="width:520px;aspect-ratio:1"></canvas>
172
176
  <script>
173
177
  CanvasGlobe.createGlobe(document.getElementById("globe"), {
@@ -284,6 +288,74 @@ npx skills add https://github.com/Shree-hari/canvas-globe --skill canvas-globe
284
288
 
285
289
  Or copy the maintained prompt from the [AI-assisted setup guide](https://canvasglobe.swiftools.com/getting-started/ai-assisted-setup). The skill and prompt select the correct framework entry point, include cleanup and accessibility, and require the user to purchase a production license before shipping.
286
290
 
291
+ ## Effects, charts, recipes, controls, and place search
292
+
293
+ The animation and data-visualization modules are separate entry points. The
294
+ core globe remains about 125 KB gzipped, and applications only download the
295
+ modules they import.
296
+
297
+ | Import | Approximate gzip size | Includes |
298
+ | --- | ---: | --- |
299
+ | `canvas-globe/fx` | 25.4 KB | 54 visual, transition, data, camera, and interaction effects plus authoring helpers |
300
+ | `canvas-globe/charts` | 4.3 KB | Nine animated chart layers |
301
+ | `canvas-globe/recipes` | 2.0 KB | Six complete, reversible compositions |
302
+ | `canvas-globe/controls` | 1.8 KB | Search, timeline, threshold, and crossfilter bindings |
303
+ | `canvas-globe/places` | 93.6 KB | Search over 6,772 bundled places |
304
+
305
+ Effects are plain objects installed on a globe. They can be combined, removed,
306
+ and rendered at an exact timeline position:
307
+
308
+ ```js
309
+ import { createGlobe } from "canvas-globe";
310
+ import { aurora, counterRoll, routeDashes } from "canvas-globe/fx";
311
+ import { tilegram } from "canvas-globe/charts";
312
+
313
+ const globe = createGlobe(canvas, { preset: "midnight", markers });
314
+ globe
315
+ .use(aurora())
316
+ .use(routeDashes({ routes }))
317
+ .use(counterRoll({ to: 21947, caption: "customers", position: "bottom-center" }));
318
+
319
+ globe.renderFrame(1200); // draw the frame at 1.2 seconds
320
+ globe.use(tilegram({ values: countryValues }));
321
+ ```
322
+
323
+ Use a recipe when you want a complete look rather than individual effects:
324
+
325
+ ```js
326
+ import { applyRecipe, keynoteGlobe } from "canvas-globe/recipes";
327
+
328
+ const undo = applyRecipe(globe, keynoteGlobe({ countries: 68 }));
329
+ undo(); // restores the previous options and removes the recipe effects
330
+ ```
331
+
332
+ Controls bind elements you already own and return cleanup functions. They do
333
+ not inject markup or styles:
334
+
335
+ ```js
336
+ import { searchAndFly } from "canvas-globe/controls";
337
+ import { placeSource } from "canvas-globe/places";
338
+
339
+ const unbind = searchAndFly(globe, document.querySelector("#place-search"), {
340
+ source: placeSource(),
341
+ });
342
+ ```
343
+
344
+ `canvas-globe/places` is optional because its city table is almost as large as
345
+ the country geometry. Core already resolves countries and roughly 300 major
346
+ cities. Import the place module only when broader offline city search is worth
347
+ the extra payload.
348
+
349
+ `seek(ms)` and `renderFrame(ms)` pin the effect clock, auto-rotation, arcs,
350
+ orbits, and marker pulses. `windField` and part of
351
+ `glitch` accumulate the previous frame by design, so reproduce them by
352
+ exporting frames sequentially. Live media, newly fired pings, and a terminator
353
+ using the current time are external dynamic state and should be fixed or
354
+ disabled for frame-exact export.
355
+
356
+ Custom effects use the same public contract and authoring helpers as the
357
+ included effects. See `types/fx.d.ts` for every option and callback signature.
358
+
287
359
  ## Options
288
360
 
289
361
  | Option | Default | Description |
@@ -5,6 +5,7 @@ CanvasGlobe includes geographic data derived from these sources:
5
5
  | Files | Source | Terms |
6
6
  | --- | --- | --- |
7
7
  | `src/data/world.js` | [Natural Earth 1:110m](https://www.naturalearthdata.com/about/terms-of-use/) through [world-atlas](https://github.com/topojson/world-atlas) | Natural Earth states that its data is public domain |
8
+ | `src/data/places.js` | [Natural Earth 1:10m populated places](https://www.naturalearthdata.com/downloads/10m-cultural-vectors/10m-populated-places/) | Natural Earth states that its data is public domain |
8
9
  | `src/data/india.js` | [Datameet india-composite](https://github.com/datameet/maps) | CC0 / public-domain dedication as stated by the source repository |
9
10
 
10
11
  The CanvasGlobe Software License Agreement applies to CanvasGlobe's original software
package/codemeta.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "identifier": "canvas-globe",
6
6
  "description": "CanvasGlobe helps developers add interactive 3D globes and flat world maps to JavaScript and React apps with Canvas 2D, without WebGL, map tiles, API keys, or runtime network calls.",
7
7
  "url": "https://canvasglobe.swiftools.com/",
8
- "version": "1.1.1",
8
+ "version": "1.2.0",
9
9
  "codeRepository": "https://github.com/Shree-hari/canvas-globe",
10
10
  "issueTracker": "https://github.com/Shree-hari/canvas-globe/issues",
11
11
  "downloadUrl": "https://www.npmjs.com/package/canvas-globe",
@@ -1314,7 +1314,7 @@ function drawFitted(ctx, media, box) {
1314
1314
  }
1315
1315
 
1316
1316
  // Keep in sync with package.json. Release checks enforce this value.
1317
- const CANVAS_GLOBE_VERSION = "1.1.1";
1317
+ const CANVAS_GLOBE_VERSION = "1.2.0";
1318
1318
 
1319
1319
  /** Local license-key checks and production-use presentation helpers. */
1320
1320
 
@@ -1520,10 +1520,24 @@ const DEFAULTS = {
1520
1520
  onCountryHover: null,
1521
1521
  onCountryClick: null,
1522
1522
  onRender: null,
1523
+ effects: null,
1523
1524
  };
1524
1525
 
1525
1526
  const coord = (v) => (Array.isArray(v) ? [v[0], v[1]] : [v.lon ?? v.lng ?? v.longitude, v.lat ?? v.latitude]);
1526
1527
 
1528
+ const now = () => (typeof performance !== "undefined" ? performance.now() : Date.now());
1529
+
1530
+ /**
1531
+ * An effect's own 0→1 progress at a point on the shared clock. Derived purely
1532
+ * from `ms`, so the same millisecond always renders the same frame.
1533
+ */
1534
+ const phaseOf = (fx, ms) => {
1535
+ const duration = Math.max(1, fx.duration ?? 1000);
1536
+ const total = duration + (fx.hold ?? 0);
1537
+ const elapsed = fx.loop === false ? Math.min(ms, total) : ((ms % total) + total) % total;
1538
+ return Math.min(1, elapsed / duration);
1539
+ };
1540
+
1527
1541
  const defaultTooltip = (target, kind) => {
1528
1542
  if (kind === "country") return target.name || String(target.id ?? "");
1529
1543
  if (kind === "cluster") return `${target.count} in this area`;
@@ -1578,6 +1592,11 @@ class GeoGlobe {
1578
1592
  this._licenseHits = [];
1579
1593
  this._licenseDismissed = false;
1580
1594
  this._licenseHovered = false;
1595
+ this._fx = [];
1596
+ this._clock = null;
1597
+ this._t0 = now();
1598
+ this._spinBase = this.lon;
1599
+ this.pointer = { x: 0, y: 0, lon: 0, lat: 0, over: false };
1581
1600
 
1582
1601
  this._applyWorld();
1583
1602
  this._applyMarkers(this.o.markers);
@@ -1589,12 +1608,138 @@ class GeoGlobe {
1589
1608
  this.resize();
1590
1609
  if (this.o.focus) this.focusOn(this.o.focus, { instant: true });
1591
1610
  if (this.o.showViewer) this._initViewer();
1611
+ for (const fx of this.o.effects || []) this.use(fx);
1592
1612
  this._loop = this._loop.bind(this);
1593
1613
  this._raf = requestAnimationFrame(this._loop);
1594
1614
  }
1595
1615
 
1596
1616
  /* ------------------------------ public API ------------------------------ */
1597
1617
 
1618
+ /* --------------------------------- effects ------------------------------- */
1619
+
1620
+ /** Installs an effect. Returns `this`, so calls chain. */
1621
+ use(effect) {
1622
+ if (!effect || typeof effect.frame !== "function") return this;
1623
+ this._fx.push({ fx: effect, state: effect.setup?.(this) ?? {} });
1624
+ this._fx.sort((a, b) => (a.fx.z ?? 0) - (b.fx.z ?? 0));
1625
+ return this.invalidate();
1626
+ }
1627
+
1628
+ /** Removes an effect by reference or by name. */
1629
+ remove(effect) {
1630
+ const i = this._fx.findIndex((e) => e.fx === effect || e.fx.name === effect);
1631
+ if (i < 0) return this;
1632
+ const [entry] = this._fx.splice(i, 1);
1633
+ entry.fx.dispose?.(entry.state, this);
1634
+ return this.invalidate();
1635
+ }
1636
+
1637
+ clearEffects() {
1638
+ while (this._fx.length) this.remove(this._fx[0].fx);
1639
+ return this;
1640
+ }
1641
+
1642
+ get effects() {
1643
+ return this._fx.map((e) => e.fx);
1644
+ }
1645
+
1646
+ /**
1647
+ * Pins the effect clock to `ms`. Rendering becomes reproducible, which is
1648
+ * what frame-exact export and seamless loops need. `play()` releases it.
1649
+ */
1650
+ seek(ms) {
1651
+ // Capture where auto-rotation started from, so longitude can be derived
1652
+ // from the clock instead of accumulated per frame. Only on the way in:
1653
+ // seeking again while already seeked must not shift the origin.
1654
+ if (this._clock == null) this._spinBase = wrapLon(this.lon - this._spinRate() * this._fxTime());
1655
+ this._clock = Math.max(0, ms);
1656
+ if (this._autoSpin()) this.lon = wrapLon(this._spinBase + this._spinRate() * this._clock);
1657
+ return this.invalidate();
1658
+ }
1659
+
1660
+ play() {
1661
+ // Resume the timeline where it was paused rather than snapping back to 0.
1662
+ if (this._clock != null) this._t0 = now() - this._clock;
1663
+ this._clock = null;
1664
+ return this.invalidate();
1665
+ }
1666
+
1667
+ /** Draws the single frame that belongs at `ms` on the effect clock. */
1668
+ renderFrame(ms) {
1669
+ return this.seek(ms).render();
1670
+ }
1671
+
1672
+ _fxTime() {
1673
+ return this._clock != null ? this._clock : now() - this._t0;
1674
+ }
1675
+
1676
+ /**
1677
+ * Wall clock for looping scene animation. Pinned while seeking, so arcs,
1678
+ * orbits and pulses land on the same phase every time a frame is redrawn.
1679
+ */
1680
+ _animTime() {
1681
+ return this._clock != null ? this._clock : Date.now();
1682
+ }
1683
+
1684
+ /** Degrees of auto-rotation per millisecond. */
1685
+ _spinRate() {
1686
+ return ((this.o.rotateSpeed || 0) * (this.o.fps || 30)) / 1000;
1687
+ }
1688
+
1689
+ _runPasses(stage) {
1690
+ if (!this._fx.length) return;
1691
+ const ms = this._fxTime();
1692
+ const { ctx } = this;
1693
+ for (const entry of this._fx) {
1694
+ if ((entry.fx.stage || "above") !== stage) continue;
1695
+ ctx.save();
1696
+ try {
1697
+ entry.fx.frame(ctx, this, phaseOf(entry.fx, ms), entry.state);
1698
+ } catch (err) {
1699
+ // One bad effect must not take the render loop down with it.
1700
+ if (!entry.failed) {
1701
+ entry.failed = true;
1702
+ console.error(`canvas-globe: effect "${entry.fx.name || "anonymous"}" failed`, err);
1703
+ }
1704
+ }
1705
+ ctx.restore();
1706
+ }
1707
+ }
1708
+
1709
+ /** Land sample points as `[lon, lat]` pairs — what the dot matrix draws. */
1710
+ landPoints(spacing) {
1711
+ const prev = this.o.dotSpacing;
1712
+ if (spacing != null) this.o.dotSpacing = spacing;
1713
+ const flat = this._dotPoints();
1714
+ this.o.dotSpacing = prev;
1715
+ const out = [];
1716
+ for (let i = 0; i < flat.length; i += 2) out.push([flat[i], flat[i + 1]]);
1717
+ return out;
1718
+ }
1719
+
1720
+ /** Traces a country outline into a path using the current projection. */
1721
+ tracePath(shape, ctx = this.ctx, transform) {
1722
+ const geom = shape?.geometry || shape;
1723
+ if (!geom?.coordinates) return this;
1724
+ const polys = geom.type === "Polygon" ? [geom.coordinates] : geom.coordinates;
1725
+ for (const poly of polys) {
1726
+ for (const ring of poly) {
1727
+ let open = false;
1728
+ for (const c of ring) {
1729
+ const src = transform ? transform(c) : c;
1730
+ const p = this.project(src[0], src[1]);
1731
+ if (!p) {
1732
+ open = false;
1733
+ continue;
1734
+ }
1735
+ open ? ctx.lineTo(p.x, p.y) : (ctx.moveTo(p.x, p.y), (open = true));
1736
+ }
1737
+ if (open) ctx.closePath();
1738
+ }
1739
+ }
1740
+ return this;
1741
+ }
1742
+
1598
1743
  setMarkers(markers = []) {
1599
1744
  this._applyMarkers(markers);
1600
1745
  this._focus = -1;
@@ -2151,9 +2296,12 @@ class GeoGlobe {
2151
2296
  const { w, h } = this._size();
2152
2297
  ctx.setTransform(this._dpr, 0, 0, this._dpr, 0, 0);
2153
2298
  ctx.clearRect(0, 0, w, h);
2299
+ this._runPasses("beneath");
2154
2300
  this.hits = this.o.mode === "map" ? this._paintMap(w, h) : this._paintGlobe(w, h);
2155
2301
  this._dirty = false;
2302
+ this._runPasses("above");
2156
2303
  this.o.onRender?.(this);
2304
+ this._runPasses("post");
2157
2305
  this._paintLicenseNotice(w, h);
2158
2306
  return this;
2159
2307
  }
@@ -2171,6 +2319,7 @@ class GeoGlobe {
2171
2319
  destroy() {
2172
2320
  this._destroyed = true;
2173
2321
  cancelAnimationFrame(this._raf);
2322
+ this.clearEffects();
2174
2323
  this.stopTour();
2175
2324
  this.stopStory();
2176
2325
  this.stopTimeline();
@@ -2625,6 +2774,15 @@ class GeoGlobe {
2625
2774
  this._onMove = (e) => {
2626
2775
  const [x, y] = this._local(e);
2627
2776
  this._pointer = { x: e.clientX, y: e.clientY };
2777
+ this.pointer.x = x;
2778
+ this.pointer.y = y;
2779
+ this.pointer.over = true;
2780
+ const at = this.unproject(x, y);
2781
+ if (at) {
2782
+ this.pointer.lon = at[0];
2783
+ this.pointer.lat = at[1];
2784
+ }
2785
+ if (this._fx.length) this._dirty = true;
2628
2786
  const licenseHovered = this._licenseHitAt(x, y);
2629
2787
  if (licenseHovered !== this._licenseHovered) {
2630
2788
  this._licenseHovered = licenseHovered;
@@ -2695,6 +2853,7 @@ class GeoGlobe {
2695
2853
  this._onLeave = (e) => {
2696
2854
  this._onUp(e);
2697
2855
  this._pointer = null;
2856
+ this.pointer.over = false;
2698
2857
  this._licenseHovered = false;
2699
2858
  if (this._hovered || this._hoveredCountry) {
2700
2859
  if (this._hovered) this.o.onHover?.(null, null);
@@ -2875,13 +3034,20 @@ class GeoGlobe {
2875
3034
 
2876
3035
  /* --------------------------------- loop --------------------------------- */
2877
3036
 
2878
- _spinning() {
3037
+ /** Auto-rotation is configured and permitted, regardless of the clock. */
3038
+ _autoSpin() {
2879
3039
  return !!this.o.autoRotate && !this._reducedMotion() && !this._drag && !this._vel && this.o.mode === "globe";
2880
3040
  }
2881
3041
 
3042
+ /** Auto-rotation should advance on its own this frame. */
3043
+ _spinning() {
3044
+ return this._autoSpin() && this._clock == null;
3045
+ }
3046
+
2882
3047
  /** True while something on screen still needs to move. */
2883
3048
  _animating() {
2884
3049
  if (this._target || this._drag || this._vel) return true;
3050
+ if (this._fx.length && this._clock == null) return true;
2885
3051
  if (this._spinning()) return true;
2886
3052
  if (this._reducedMotion()) return false;
2887
3053
  if (this._hasLive || this._pings.length) return true;
@@ -3155,7 +3321,7 @@ class GeoGlobe {
3155
3321
  const list = this._orbitList();
3156
3322
  if (!list.length) return;
3157
3323
  const { ctx } = this;
3158
- const spin = this._reducedMotion() ? 0 : (Date.now() / 1000) % 3600;
3324
+ const spin = this._reducedMotion() ? 0 : (this._animTime() / 1000) % 3600;
3159
3325
  ctx.lineCap = "round";
3160
3326
  for (const orbit of list) {
3161
3327
  const inc = (orbit.inclination ?? 30) * D2R;
@@ -3709,7 +3875,7 @@ class GeoGlobe {
3709
3875
  const arcs = this.o.arcs;
3710
3876
  if (!arcs || !arcs.length) return;
3711
3877
  const { ctx } = this;
3712
- const now = Date.now();
3878
+ const now = this._animTime();
3713
3879
  const still = this._reducedMotion();
3714
3880
  ctx.lineCap = "round";
3715
3881
  ctx.lineJoin = "round";
@@ -4438,7 +4604,7 @@ class GeoGlobe {
4438
4604
  }
4439
4605
 
4440
4606
  if (m.live && !this._reducedMotion()) {
4441
- const pulse = (Date.now() % 2200) / 2200;
4607
+ const pulse = (this._animTime() % 2200) / 2200;
4442
4608
  ctx.globalAlpha = 1 - pulse;
4443
4609
  ctx.strokeStyle = t.live;
4444
4610
  ctx.lineWidth = 1.4 * scale;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "canvas-globe",
3
- "version": "1.1.1",
3
+ "version": "1.2.0",
4
4
  "description": "Build interactive 3D globes and flat world maps in JavaScript, React, Vue, Angular, or Svelte with Canvas 2D and no WebGL.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "author": {
@@ -66,6 +66,36 @@
66
66
  "import": "./src/react.js",
67
67
  "default": "./src/react.js"
68
68
  },
69
+ "./fx": {
70
+ "types": "./types/fx.d.ts",
71
+ "import": "./src/fx/index.js",
72
+ "default": "./src/fx/index.js"
73
+ },
74
+ "./fx/interaction": {
75
+ "types": "./types/fx-interaction.d.ts",
76
+ "import": "./src/fx/effects/interaction.js",
77
+ "default": "./src/fx/effects/interaction.js"
78
+ },
79
+ "./recipes": {
80
+ "types": "./types/recipes.d.ts",
81
+ "import": "./src/recipes.js",
82
+ "default": "./src/recipes.js"
83
+ },
84
+ "./charts": {
85
+ "types": "./types/charts.d.ts",
86
+ "import": "./src/charts.js",
87
+ "default": "./src/charts.js"
88
+ },
89
+ "./controls": {
90
+ "types": "./types/controls.d.ts",
91
+ "import": "./src/controls.js",
92
+ "default": "./src/controls.js"
93
+ },
94
+ "./places": {
95
+ "types": "./types/places.d.ts",
96
+ "import": "./src/places.js",
97
+ "default": "./src/places.js"
98
+ },
69
99
  "./data/world": {
70
100
  "types": "./types/data/world.d.ts",
71
101
  "import": "./src/data/world.js",
@@ -95,7 +125,9 @@
95
125
  "scripts": {
96
126
  "start": "node scripts/serve.mjs",
97
127
  "build": "node scripts/build.mjs",
98
- "data": "node scripts/generate-data.mjs && node scripts/generate-timezones.mjs",
128
+ "data": "node scripts/generate-data.mjs && node scripts/generate-timezones.mjs && node scripts/generate-places.mjs",
129
+ "places": "node scripts/generate-places.mjs",
130
+ "audit:docs": "node scripts/audit-docs.mjs",
99
131
  "example": "node scripts/serve.mjs",
100
132
  "storybook": "storybook dev -p 6006",
101
133
  "build-storybook": "storybook build",