@kahwee/sf-map-svg 4.3.2 → 4.5.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 +22 -0
- package/README.md +18 -1
- package/dist/src/cards.d.ts +15 -0
- package/dist/src/cards.js +138 -0
- package/dist/src/cluster-layer.d.ts +2 -0
- package/dist/src/cluster-layer.js +2 -0
- package/dist/src/explorer-controls.d.ts +26 -0
- package/dist/src/explorer-controls.js +120 -0
- package/dist/src/explorer-core.d.ts +6 -1
- package/dist/src/explorer-core.js +168 -701
- package/dist/src/explorer-shell.d.ts +62 -0
- package/dist/src/explorer-shell.js +228 -0
- package/dist/src/explorer-sidebar.d.ts +20 -0
- package/dist/src/explorer-sidebar.js +79 -0
- package/dist/src/guide-data.js +1 -0
- package/dist/src/guide-map.js +2 -1
- package/dist/src/guide-static.js +1 -0
- package/dist/src/label-candidates.d.ts +24 -0
- package/dist/src/label-candidates.js +29 -0
- package/dist/src/label-renderer.d.ts +1 -10
- package/dist/src/label-spread.d.ts +5 -0
- package/dist/src/label-spread.js +36 -0
- package/dist/src/map-core.d.ts +3 -1
- package/dist/src/map-core.js +7 -5
- package/dist/src/map.js +2 -1
- package/dist/src/neighborhood-layer.d.ts +67 -0
- package/dist/src/neighborhood-layer.js +256 -0
- package/dist/src/overlay-layer.d.ts +13 -0
- package/dist/src/overlay-layer.js +68 -0
- package/dist/src/palettes.d.ts +48 -0
- package/dist/src/palettes.js +40 -0
- package/dist/src/presets.d.ts +1 -0
- package/dist/src/presets.js +1 -0
- package/dist/src/static-labels.d.ts +7 -0
- package/dist/src/static-labels.js +115 -0
- package/dist/src/static.js +31 -3
- package/dist/src/types.d.ts +16 -1
- package/docs/API.md +38 -2
- package/docs/EXAMPLES.md +1 -0
- package/docs/consumer-integration.md +81 -1
- package/docs/guide-bundle-report.md +4 -4
- package/llms-full.txt +274 -26
- package/llms.txt +1 -1
- package/package.json +16 -8
package/llms-full.txt
CHANGED
|
@@ -2,13 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
> Offline San Francisco SVG maps with explicit geographic imports, no runtime dependencies, static rendering, and a browser controller.
|
|
4
4
|
|
|
5
|
-
@kahwee/sf-map-svg 4.
|
|
5
|
+
@kahwee/sf-map-svg 4.5.0 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
|
|
6
6
|
|
|
7
7
|
These are working-tree docs. Unreleased changelog entries may describe APIs absent from npm; verify the installed version before copying them.
|
|
8
8
|
|
|
9
9
|
Use renderMap(data, options) for SVG strings; createMap(data, options) returns a controller. Mount map.element and destroy it on unmount. Choose mode explicitly: createMap defaults to basemap. Geography is separate; staticMapData and fullMapData have different shapes.
|
|
10
10
|
|
|
11
|
-
<!-- Generated from README.md; @kahwee/sf-map-svg 4.
|
|
11
|
+
<!-- Generated from README.md; @kahwee/sf-map-svg 4.5.0 · local working tree. -->
|
|
12
12
|
|
|
13
13
|
# SF Map SVG
|
|
14
14
|
|
|
@@ -45,6 +45,23 @@ const { svg, project, viewBox } = renderMap(staticMapData, {
|
|
|
45
45
|
|
|
46
46
|
See [map options and animations](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md#static-maps) for optional layers and static animation.
|
|
47
47
|
|
|
48
|
+
## Labels before JavaScript loads
|
|
49
|
+
|
|
50
|
+
Version 4.4 adds an opt-in overview layout for server-rendered guides:
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { createGuideShell } from '@kahwee/sf-map-svg/guide/static';
|
|
54
|
+
|
|
55
|
+
const guideMarkup = createGuideShell({
|
|
56
|
+
title: 'Explore San Francisco',
|
|
57
|
+
labelLayout: { width: 390 },
|
|
58
|
+
});
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`width` is the expected canvas width in CSS pixels. The SVG keeps its 800 × 800 projection while labels share the interactive map's priorities, anchors, sizes and collision rules. Guide shells include neighborhood names in this layout; calls without `labelLayout` keep their previous output.
|
|
62
|
+
|
|
63
|
+
Enhance the shell with `mountGuideController()` using the same data, colors, markers and layers. Static font widths are estimated, so browser fonts and resize can change which labels fit. See the [complete integration recipe and width guidance](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md#keep-overview-labels-consistent). No migration is required from 4.3.
|
|
64
|
+
|
|
48
65
|
## Interactive map
|
|
49
66
|
|
|
50
67
|
```ts
|
|
@@ -177,7 +194,7 @@ pnpm benchmark:markers --counts=2000 --browsers=chromium,firefox,webkit \
|
|
|
177
194
|
|
|
178
195
|
### Cached label preparation and renderer components
|
|
179
196
|
|
|
180
|
-
Neighborhood priority is sorted once per source. Styled label candidates are reused until selection, catalog, source, layers, appearance, or zoom/width eligibility changes. Overlay bounds are measured before marker and cluster SVG updates; secondary-road visibility and scale furniture skip unchanged writes. Marker catalog reconciliation and cluster rendering now have separate modules, reducing the explorer controller from 2,316 to 2,151 lines.
|
|
197
|
+
Neighborhood priority is sorted once per source. Styled label candidates are reused until selection, catalog, source, layers, appearance, or zoom/width eligibility changes. Overlay bounds are measured before marker and cluster SVG updates; secondary-road visibility and scale furniture skip unchanged writes. Marker catalog reconciliation and cluster rendering now have separate modules, reducing the explorer controller from 2,316 to 2,151 lines. At the 4.3.2 marker-performance baseline, the renderer was 34.6 KiB gzip and static rendering was 7.9 KiB. See the [current bundle report](https://github.com/kahwee/sf-map-svg/blob/main/docs/guide-bundle-report.md) for measurements including the opt-in static label layout.
|
|
181
198
|
|
|
182
199
|
A fresh Chromium comparison against `e774fbd` used 2,000 labeled markers, 20 operation samples, both widths, and all six frame workloads in basemap and neighborhood modes. Neighborhood-mode frame interval p95 values (milliseconds):
|
|
183
200
|
|
|
@@ -203,7 +220,7 @@ pnpm benchmark:markers --counts=2000 --mode=neighborhoods \
|
|
|
203
220
|
|
|
204
221
|
---
|
|
205
222
|
|
|
206
|
-
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 4.
|
|
223
|
+
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 4.5.0 · local working tree. -->
|
|
207
224
|
|
|
208
225
|
# Developer guide
|
|
209
226
|
|
|
@@ -347,7 +364,7 @@ See [API options](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md), [
|
|
|
347
364
|
|
|
348
365
|
---
|
|
349
366
|
|
|
350
|
-
<!-- Generated from docs/API.md; @kahwee/sf-map-svg 4.
|
|
367
|
+
<!-- Generated from docs/API.md; @kahwee/sf-map-svg 4.5.0 · local working tree. -->
|
|
351
368
|
|
|
352
369
|
# Map options and lifecycle
|
|
353
370
|
|
|
@@ -370,9 +387,9 @@ const { svg } = renderMap(staticMapData, presentation);
|
|
|
370
387
|
const map = createMap(fullMapData, presentation);
|
|
371
388
|
```
|
|
372
389
|
|
|
373
|
-
Static `layers` supports every interactive layer switch
|
|
390
|
+
Static `layers` supports every interactive layer switch. `neighborhoodLabels` applies to the opt-in `labelLayout` described below. Static `appearance` supports `theme`, `colors`, and `districtStyle`; browser typography, area interaction styles, and screen-space marker sizing remain interactive settings. Unknown keys and invalid values are rejected for both flat and grouped options. Supplied grouped keys override flat compatibility keys; omitted or `undefined` grouped keys preserve the flat value or renderer default. Undefined color tokens use the theme default instead of entering SVG attributes. The two renderers retain their existing defaults. Marker arrays accept readonly inputs.
|
|
374
391
|
|
|
375
|
-
Flat compatibility options include `theme`, `width`, `height`, `padding`, `year` (2002, 2012, 2022), `districtLines`, `districtFills`, `districtStyle`, `districtLabels`, `neighborhoodLines`, `labels`, `highways`, `keyRoads`, `roadLabels`, `landmarks`, `bartStations`, `markers`, `overlays`, `title`, `idPrefix`, `colors`, and `animation`. Each optional layer is independent. User-supplied text and attributes are escaped in SVG output.
|
|
392
|
+
Flat compatibility options include `theme`, `width`, `height`, `padding`, `year` (2002, 2012, 2022), `districtLines`, `districtFills`, `districtStyle`, `districtLabels`, `neighborhoodLines`, `neighborhoodLabels`, `labels`, `labelLayout`, `highways`, `keyRoads`, `roadLabels`, `landmarks`, `bartStations`, `markers`, `overlays`, `title`, `idPrefix`, `colors`, and `animation`. Each optional layer is independent. User-supplied text and attributes are escaped in SVG output.
|
|
376
393
|
|
|
377
394
|
Custom district IDs must be finite numbers. An empty `labelPoints` array uses the district's primary `label` position.
|
|
378
395
|
|
|
@@ -380,6 +397,34 @@ The SVG description names the selected neighborhood source for source-aware data
|
|
|
380
397
|
|
|
381
398
|
`animation: true` (or `{ duration, delay }`) makes a static map draw itself with self-contained, scoped CSS: land fades in, the coast and lines draw, districts grow, then labels and points appear. It plays when the SVG is inserted into a page or loaded as an image, needs no JavaScript, and stays still under `prefers-reduced-motion`. Re-insert the markup to replay it. Default output is unchanged.
|
|
382
399
|
|
|
400
|
+
Static `labelLayout: { width: 390 }` opts into shared interactive **overview** label priorities, sizes, anchors and collision rules. `width` must be a positive finite CSS canvas width; it does not alter the SVG viewBox. Set `neighborhoodLabels: true` (flat or under `layers`) to add area names in this layout. Guide SVGs and shells enable neighborhood names by default only when opting into `labelLayout`. `labels: false` remains the master switch. Default static output is unchanged.
|
|
401
|
+
|
|
402
|
+
For editorial previews, `labelLayout` also accepts `maxLabels` (a nonnegative safe integer), `haloColor` (an SVG color string), and `compact: { width, maxLabels }`. The compact width must be positive, finite, and smaller than the main layout width. Each layout places labels around pins independently, then chooses up to its budget for spatial coverage and nearby-pin context (selected marker first when it fits). Without a budget the existing overview order is unchanged. Equal spatial scores retain the original priority order. A zero budget hides text, never pins. The default halo remains white; use the land color for a quieter dark map.
|
|
403
|
+
|
|
404
|
+
Compact mode embeds two label groups and a media query in the same SVG. At or below the compact width it shows the compact group; above it shows the main group. In an `<img>` or standalone SVG, the breakpoint follows the SVG viewport. In inline SVG it follows the document viewport, so use `<img>` for cards whose width differs from the page. No JavaScript, duplicated geometry, external fonts, or extra image requests are needed. The compact width is also its font-layout hint; very narrow images still scale the text. The interactive renderer continues to use its live collision layout after enhancement.
|
|
405
|
+
|
|
406
|
+
```js
|
|
407
|
+
createGuideSVG({
|
|
408
|
+
labelLayout: {
|
|
409
|
+
width: 800,
|
|
410
|
+
haloColor: '#1b2c28',
|
|
411
|
+
compact: { width: 480, maxLabels: 4 },
|
|
412
|
+
},
|
|
413
|
+
colors: { water: '#0a1412', land: '#1b2c28', label: '#d5e3e8' },
|
|
414
|
+
markers: places,
|
|
415
|
+
});
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
See the [field-guide preview recipe](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md#editorial-map-previews) for a complete build-time example.
|
|
419
|
+
|
|
420
|
+
Static placement uses estimated system-font metrics without browser globals. Interactive placement measures actual fonts and includes live selection and HTML furniture, so exact glyph positions and the visible label set can differ. See the [integration recipe](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md#keep-overview-labels-consistent) for width and progressive-enhancement guidance.
|
|
421
|
+
|
|
422
|
+
## Editorial palettes
|
|
423
|
+
|
|
424
|
+
Import `mapPalettes` from `@kahwee/sf-map-svg/presets`. `nightForest` and `warmPaper` are deeply frozen, opt-in token sets. Each supplies `colors`, `haloColor`, `markerColor`, `selectedMarkerColor`, and `selectedMarkerRingColor`. They work with any supplied geography and do not change the existing themes. Text clears 4.5:1 and pins/rings clear 3:1 against the palette's land, park, and water surfaces; consumers must check their own overrides and surrounding UI.
|
|
425
|
+
|
|
426
|
+
For static maps pass `colors: palette.colors`, `labelLayout: { width: 800, haloColor: palette.haloColor }`, and the marker color on each marker. For interactive maps pass `appearance: { colors: palette.colors, labelStyle: { haloColor: palette.haloColor }, markerColor: palette.markerColor, selectedMarkerColor: palette.selectedMarkerColor }` and `features: { selectedMarkerRing: { color: palette.selectedMarkerRingColor } }`. Choose the palette in the consuming application; these tokens do not read system preferences or install global CSS.
|
|
427
|
+
|
|
383
428
|
## Interactive maps
|
|
384
429
|
|
|
385
430
|
`createMap` defaults to `mode: 'basemap'` and `appearance.theme: 'transit'`. Set the mode explicitly for neighborhood or district exploration. District and neighborhood layer defaults follow the mode; other supplied layers default on. The [developer guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/developer-guide.md) compares static and interactive defaults and explains configuration resets, coordinates, and cleanup.
|
|
@@ -457,9 +502,17 @@ GeoJSON and static projection inputs use `[longitude, latitude]` in WGS84; marke
|
|
|
457
502
|
|
|
458
503
|
Explore presentation options in the [live map design playground](https://kahwee.github.io/sf-map-svg/playground.html). The page identifies its package version; released builds use compatible options for that installed version.
|
|
459
504
|
|
|
505
|
+
## Linked place cards
|
|
506
|
+
|
|
507
|
+
`bindMapCards(map, root, options)` from `@kahwee/sf-map-svg/cards` connects consumer-owned cards to a `MapController`. Each card uses `data-map-marker="stable-marker-id"`; native `<button type="button" data-map-select>` controls select that place without panning. Article links keep their ordinary navigation. `options.markers` is the same catalog supplied to the map; `highlightColor` optionally changes the preview ring and `scrollOnSelect: false` disables automatic card reveal.
|
|
508
|
+
|
|
509
|
+
Pointer hover and keyboard focus draw a temporary ring through `projectToScreen()`. They do not call `selectMarker`, change the camera, or emit selection events. Focused cards take precedence over hovered cards. Offscreen points do not draw a ring. Touch users use the selection button. Selected cards receive `data-map-selected` and their buttons receive `aria-pressed`; style the card attribute in your own CSS. Selecting a pin reveals its card with immediate native scrolling, without moving focus. Filtering/reordering/removing cards is safe; IDs are compared directly, not inserted into CSS selectors.
|
|
510
|
+
|
|
511
|
+
Call `binding.setMarkers(next)` after `map.setMarkers(next)` when the catalog changes. Call `binding.destroy()` before `map.destroy()` on unmount; cleanup is idempotent and restores attributes that the binding owned. The module does not create cards, fetch data, change URLs, install global CSS, or import geography. See the [linked-card recipe](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md#link-place-cards-to-pins).
|
|
512
|
+
|
|
460
513
|
---
|
|
461
514
|
|
|
462
|
-
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 4.
|
|
515
|
+
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 4.5.0 · local working tree. -->
|
|
463
516
|
|
|
464
517
|
# Examples
|
|
465
518
|
|
|
@@ -470,6 +523,7 @@ Choose by task. All snippets use the public package API; browser examples need a
|
|
|
470
523
|
| I want to… | Start here |
|
|
471
524
|
| --- | --- |
|
|
472
525
|
| Render an SVG on a server | [Static map](#static-svg) |
|
|
526
|
+
| Show labels before JavaScript loads | [Static-to-interactive guide](https://github.com/kahwee/sf-map-svg/blob/main/docs/consumer-integration.md#keep-overview-labels-consistent) |
|
|
473
527
|
| Add a small map to a browser | [Lightweight guide](#lightweight-interactive-guide) |
|
|
474
528
|
| Bundle only selected geography | [Selected data](#selected-geography) |
|
|
475
529
|
| Draw a route over the city | [Route overlay](#route-overlay) |
|
|
@@ -570,7 +624,7 @@ For the controller and explicit data imports, see [the v3 migration guide](https
|
|
|
570
624
|
|
|
571
625
|
---
|
|
572
626
|
|
|
573
|
-
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 4.
|
|
627
|
+
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 4.5.0 · local working tree. -->
|
|
574
628
|
|
|
575
629
|
# Embedding and progressive enhancement
|
|
576
630
|
|
|
@@ -589,6 +643,46 @@ const markup = createGuideShell({ title: 'Explore San Francisco' });
|
|
|
589
643
|
|
|
590
644
|
The shell uses an 800 × 800 projection with 28 units of padding, matching the interactive map. Use `createGuideSVG()` for custom dimensions. Rendering is offline, with zero runtime dependencies.
|
|
591
645
|
|
|
646
|
+
## Keep overview labels consistent
|
|
647
|
+
|
|
648
|
+
Opt into the browser's overview label priorities and collision rules with a target **canvas width in CSS pixels**:
|
|
649
|
+
|
|
650
|
+
```ts
|
|
651
|
+
const labeledMarkup = createGuideShell({
|
|
652
|
+
title: 'Explore San Francisco',
|
|
653
|
+
labelLayout: { width: 390 },
|
|
654
|
+
});
|
|
655
|
+
```
|
|
656
|
+
|
|
657
|
+
This keeps the 800 × 800 projection. It sizes and places text for a 390 px canvas, prioritizes major parks and primary roads, includes neighborhood names, and avoids label, marker, and BART-symbol collisions. BART names and smaller parks remain deferred until interactive zoom. Use the same geography, colors, markers and layer switches when mounting. Set `layers.neighborhoodLabels: false` on both renderers to omit neighborhood names.
|
|
658
|
+
|
|
659
|
+
The width is a rendering hint, not a responsive script. Generate an appropriate variant for your known embed width; an SVG displayed at another width scales its labels with the artwork. Static layout uses conservative estimated system-font widths, while the browser measures actual fonts and recalculates on resize. Font metrics, custom browser label styles, selection, clustering and HTML overlays can therefore change the final label set or position. This reduces the transition without promising pixel-identical hydration. Existing calls without `labelLayout` keep their original fixed labels.
|
|
660
|
+
|
|
661
|
+
For a fixed-width embed, use shared inputs on the server and in the browser:
|
|
662
|
+
|
|
663
|
+
```ts
|
|
664
|
+
// Shared inputs: coordinates, IDs, radii, colors and layer choices must match.
|
|
665
|
+
const guideMarkers = [
|
|
666
|
+
{ id: 'ferry', label: 'Ferry Building', lng: -122.3937, lat: 37.7955, radius: 7 },
|
|
667
|
+
];
|
|
668
|
+
const guideLayers = { neighborhoodLabels: true, roadLabels: true };
|
|
669
|
+
const guideColors = { label: '#183e58', landmark: '#426641' };
|
|
670
|
+
|
|
671
|
+
// Server: place this HTML in a canvas that is 390 CSS pixels wide.
|
|
672
|
+
const labeledPlacesMarkup = createGuideShell({
|
|
673
|
+
labelLayout: { width: 390 },
|
|
674
|
+
markers: guideMarkers,
|
|
675
|
+
layers: guideLayers,
|
|
676
|
+
appearance: { colors: guideColors },
|
|
677
|
+
});
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
After inserting that markup, pass `markers: guideMarkers`, `layers: guideLayers`, and `appearance: { colors: guideColors }` to `mountGuideController()` as shown below. The static-only `labelLayout` option is not a browser option: the controller measures the actual canvas and font. The shell border is outside the canvas, so a default shell with a 390 px outer width has a 388 px canvas.
|
|
681
|
+
|
|
682
|
+
For responsive layouts whose canvas width is unknown at build time, use `labelLayout.compact` for an embedded image with two static label layouts (see [editorial previews](#editorial-map-previews)), choose a representative width and accept text scaling until enhancement, or serve independently rendered SVG variants through your application's responsive markup. The package does not detect a device on the server. Keep `labels: false` if your design requires no visible text change at enhancement.
|
|
683
|
+
|
|
684
|
+
For `renderMap()`, also enable `neighborhoodLabels` explicitly; it defaults off outside the guide preset. `labels: false` hides all text in either layout while preserving symbols and accessible titles.
|
|
685
|
+
|
|
592
686
|
## Enhance the existing frame
|
|
593
687
|
|
|
594
688
|
In your browser entry, mount the controller into that shell:
|
|
@@ -601,7 +695,9 @@ if (!shell) throw new Error('Missing guide shell');
|
|
|
601
695
|
const guide = mountGuideController(shell, {
|
|
602
696
|
attribution: 'compact',
|
|
603
697
|
features: { motion: { duration: 400 }, markerEntrance: true },
|
|
604
|
-
|
|
698
|
+
markers: guideMarkers,
|
|
699
|
+
layers: guideLayers,
|
|
700
|
+
appearance: { theme: 'transit', colors: guideColors },
|
|
605
701
|
});
|
|
606
702
|
|
|
607
703
|
// Call before your application removes the containing view.
|
|
@@ -620,9 +716,47 @@ For a new browser-only guide, use `createGuideController()` and mount its `.elem
|
|
|
620
716
|
|
|
621
717
|
See the [exact API contract](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md), [worked examples](https://github.com/kahwee/sf-map-svg/blob/main/docs/EXAMPLES.md), and [bundle measurements](https://github.com/kahwee/sf-map-svg/blob/main/docs/guide-bundle-report.md).
|
|
622
718
|
|
|
719
|
+
|
|
720
|
+
## Editorial map previews
|
|
721
|
+
|
|
722
|
+
YorkSF uses one city overview to connect its field notes and compact locator maps to orient individual articles. These patterns work for any local guide: keep the place catalog in the consuming application, generate SVG images at build time, and link each preview to the application's map or article route.
|
|
723
|
+
|
|
724
|
+
Run `pnpm demo` and open `examples/generated/field-guide.html` for a self-contained light/dark example. Its overview uses `labelLayout.compact` to limit mobile text without removing pins, and `haloColor` to match the night palette. The locator cards share those colors but omit labels at small sizes. Images reserve their aspect ratio and load lazily; the links work without JavaScript. The example derives its sample park coordinates from the package's existing landmark data rather than maintaining another geographic dataset.
|
|
725
|
+
|
|
726
|
+
Use stable marker IDs for deep links. Keep addresses, visit dates, historical-status caveats, and editorial descriptions in your own content model. The package supplies map graphics; it does not imply that a place is currently open or that an author visited it. Keep a visible list beside the overview so the image is not the only way to find a place. SVG alt text should describe its purpose; link text should name the destination.
|
|
727
|
+
|
|
728
|
+
For an image card, `compact.width` follows the image's rendered viewport. For inline SVG it follows the outer document viewport; do not assume it behaves like a container query. Avoid embedding scripts in exported images. Interactive maps remain a separate progressive enhancement.
|
|
729
|
+
|
|
730
|
+
## Link place cards to pins
|
|
731
|
+
|
|
732
|
+
Keep ordinary article links and add a separate map-selection button. Native buttons give keyboard and touch users the same action as pointer users:
|
|
733
|
+
|
|
734
|
+
```html
|
|
735
|
+
<article data-map-marker="park">
|
|
736
|
+
<h2><a href="/places/park/">A place in the park</a></h2>
|
|
737
|
+
<button type="button" data-map-select>Show on map</button>
|
|
738
|
+
</article>
|
|
739
|
+
```
|
|
740
|
+
|
|
741
|
+
```js
|
|
742
|
+
import { bindMapCards } from '@kahwee/sf-map-svg/cards';
|
|
743
|
+
import { mapPalettes } from '@kahwee/sf-map-svg/presets';
|
|
744
|
+
|
|
745
|
+
const binding = bindMapCards(map, document.querySelector('#place-cards'), {
|
|
746
|
+
markers: places,
|
|
747
|
+
highlightColor: mapPalettes.nightForest.selectedMarkerRingColor,
|
|
748
|
+
});
|
|
749
|
+
// On catalog change: map.setMarkers(next); binding.setMarkers(next);
|
|
750
|
+
// On unmount: binding.destroy(); map.destroy();
|
|
751
|
+
```
|
|
752
|
+
|
|
753
|
+
Style `[data-map-selected]` with an outline or background change that remains legible in both themes. Use a visible focus outline on card buttons and links. Hover/focus only previews the location: it preserves selection and camera. Map selection reveals the card without shifting keyboard focus; set `scrollOnSelect: false` if your application owns scrolling. Match cards and markers by stable IDs, and keep hidden/filtered places out of both catalogs when that is your intended filter behavior.
|
|
754
|
+
|
|
755
|
+
Run `pnpm demo` and open `examples/generated/linked-cards.html` for the complete pattern. Storybook **Maps / Linked place cards** checks hover, keyboard selection, card reveal, both palettes, and disposal.
|
|
756
|
+
|
|
623
757
|
---
|
|
624
758
|
|
|
625
|
-
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 4.
|
|
759
|
+
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 4.5.0 · local working tree. -->
|
|
626
760
|
|
|
627
761
|
# Migrate to version 3
|
|
628
762
|
|
|
@@ -663,7 +797,7 @@ District styles are now prepared once per district on construction and style/yea
|
|
|
663
797
|
|
|
664
798
|
---
|
|
665
799
|
|
|
666
|
-
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg 4.
|
|
800
|
+
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg 4.5.0 · local working tree. -->
|
|
667
801
|
|
|
668
802
|
# Geometry and provenance
|
|
669
803
|
|
|
@@ -847,7 +981,7 @@ display geometry, and no precinct polygons or raw workbook data are shipped.
|
|
|
847
981
|
|
|
848
982
|
---
|
|
849
983
|
|
|
850
|
-
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 4.
|
|
984
|
+
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 4.5.0 · local working tree. -->
|
|
851
985
|
|
|
852
986
|
# Changelog
|
|
853
987
|
|
|
@@ -855,6 +989,28 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
|
|
|
855
989
|
|
|
856
990
|
## Unreleased
|
|
857
991
|
|
|
992
|
+
## 4.5.0 — 2026-10-14
|
|
993
|
+
|
|
994
|
+
- Add the optional `/cards` entry with `bindMapCards()`: hover/focus previews a pin without changing selection or camera, native buttons select places, map selection reveals matching cards without stealing focus, and explicit cleanup restores owned DOM attributes.
|
|
995
|
+
- Spread budgeted static labels across the map while favoring nearby-pin context and retaining a selected-place label when it fits. Keep collision avoidance, all pins, unbudgeted labels, and default output unchanged.
|
|
996
|
+
|
|
997
|
+
- Add opt-in `mapPalettes.nightForest` and `mapPalettes.warmPaper` tokens for consistent static/interactive map colors, label halos, markers, and selection rings, with contrast checks.
|
|
998
|
+
|
|
999
|
+
- Port YorkSF’s editorial map-preview pattern into static rendering: opt-in compact SVG label layouts, spatial label budgets, and configurable halos for dark palettes. Add a build-time field-guide example and light/dark Storybook preview without runtime dependencies or changes to default output.
|
|
1000
|
+
|
|
1001
|
+
- Separate interactive neighborhood state, controls, sidebar downloads and overlays into lifecycle-owned modules; revoke neighborhood hover listeners when replacing sources and keep optional explorer sidebar code out of normal map bundles.
|
|
1002
|
+
- Build the package once during `pnpm check`, while keeping standalone test and typecheck commands self-contained.
|
|
1003
|
+
|
|
1004
|
+
- Include compatible development-tool updates to Vite 8.3.4 and Playwright 1.64.0. Keep Storybook/addons 10.6.1 and aligned Vitest/browser/coverage 5.0.3; their newer 10.6.2 and 5.0.4 releases remain inside pnpm's release-age window. Make the consumer examples' custom touch-button accessible names include their visible text. Remove the obsolete Storybook 10.6.0 peer overrides; retain Node 24+ and zero runtime dependencies. These tooling changes were previously prepared as 4.4.1 but not published separately.
|
|
1005
|
+
|
|
1006
|
+
## 4.4.0 — 2026-10-12
|
|
1007
|
+
|
|
1008
|
+
- Add opt-in static `labelLayout: { width }` to `renderMap`, `createGuideSVG`, and `createGuideShell`. Static and interactive maps now share overview priorities, neighborhood anchors, font sizes, and collision rules; centered labels retain their anchors during enhancement.
|
|
1009
|
+
- Add static `neighborhoodLabels`, available flat or under `layers`, for the new layout. Guide SVGs and shells enable neighborhood names when opting in. Existing fixed-label output and interactive defaults remain unchanged; `labels: false` still hides text while preserving symbols and accessible titles.
|
|
1010
|
+
- Keep static layout server-safe and dependency-free. The width is a CSS canvas-width hint, not the SVG projection width; portable font estimates can produce a different visible label set from browser-measured fonts. Document matching data, markers, colors, and layers, plus responsive-width and custom-style limitations.
|
|
1011
|
+
- Keep server font estimation out of interactive-only imports and browser runtime code out of static imports. Refresh bundle reports and enforce bounded size budgets for the combined renderer and static-capable Pages tools.
|
|
1012
|
+
- Add desktop and phone enhancement stories, collision and input-validation regressions, an SVG example, and copyable integration documentation. Existing screenshot baselines remain unchanged.
|
|
1013
|
+
|
|
858
1014
|
## 4.3.2 — 2026-10-10
|
|
859
1015
|
|
|
860
1016
|
- Update compatible development tooling and refresh dependency security fixes.
|
|
@@ -1191,7 +1347,7 @@ consistent home, and makes animation and subscription ownership predictable.
|
|
|
1191
1347
|
|
|
1192
1348
|
# Published type contracts
|
|
1193
1349
|
|
|
1194
|
-
@kahwee/sf-map-svg 4.
|
|
1350
|
+
@kahwee/sf-map-svg 4.5.0 · local working tree. These declarations are generated by TypeScript. Use package export paths, not the internal filenames below. Imported relative contracts are included for context, not as additional public entry points.
|
|
1195
1351
|
|
|
1196
1352
|
| Public import | Declaration |
|
|
1197
1353
|
| --- | --- |
|
|
@@ -1220,6 +1376,7 @@ consistent home, and makes animation and subscription ownership predictable.
|
|
|
1220
1376
|
| @kahwee/sf-map-svg/map | ./dist/src/map.d.ts |
|
|
1221
1377
|
| @kahwee/sf-map-svg/static | ./dist/src/static.d.ts |
|
|
1222
1378
|
| @kahwee/sf-map-svg/presets | ./dist/src/presets.d.ts |
|
|
1379
|
+
| @kahwee/sf-map-svg/cards | ./dist/src/cards.d.ts |
|
|
1223
1380
|
|
|
1224
1381
|
### ./dist/src/api.d.ts
|
|
1225
1382
|
|
|
@@ -1608,12 +1765,27 @@ export interface SFMapOptions {
|
|
|
1608
1765
|
source?: NeighborhoodSource;
|
|
1609
1766
|
districtLines?: boolean;
|
|
1610
1767
|
neighborhoodLines?: boolean;
|
|
1768
|
+
/** Neighborhood names in the opt-in collision layout. */
|
|
1769
|
+
neighborhoodLabels?: boolean;
|
|
1611
1770
|
districtFills?: boolean;
|
|
1612
1771
|
/** Evaluated once per district at construction/style/year updates; call setDistrictStyle again when external data changes. */
|
|
1613
1772
|
districtStyle?: (district: import('./map-core.js').DistrictRowData) => DistrictStyle;
|
|
1614
1773
|
districtLabels?: boolean;
|
|
1615
1774
|
/** Hide all visible text labels while retaining geographic symbols and accessible titles. */
|
|
1616
1775
|
labels?: boolean;
|
|
1776
|
+
/** Opt into interactive overview label priorities/collisions at this CSS display width. Uses estimated system-font metrics without a DOM. */
|
|
1777
|
+
labelLayout?: {
|
|
1778
|
+
width: number;
|
|
1779
|
+
/** Maximum placed labels, spread across the map with nearby-pin context; zero hides text only. */
|
|
1780
|
+
maxLabels?: number;
|
|
1781
|
+
/** Label outline color, for example the land color on a dark map. */
|
|
1782
|
+
haloColor?: string;
|
|
1783
|
+
/** Alternate layout below this SVG viewport width, without browser JavaScript. */
|
|
1784
|
+
compact?: {
|
|
1785
|
+
width: number;
|
|
1786
|
+
maxLabels: number;
|
|
1787
|
+
};
|
|
1788
|
+
};
|
|
1617
1789
|
highways?: boolean;
|
|
1618
1790
|
keyRoads?: boolean;
|
|
1619
1791
|
roadLabels?: boolean;
|
|
@@ -1636,7 +1808,7 @@ export interface SFMapOptions {
|
|
|
1636
1808
|
colors?: Partial<Record<'water' | 'land' | 'district' | 'neighborhood' | 'highway' | 'road' | 'park' | 'landmark' | 'bart' | 'label' | 'marker' | 'selected', string>>;
|
|
1637
1809
|
}
|
|
1638
1810
|
/** Presentation supported by both the static and interactive renderers. */
|
|
1639
|
-
export type StaticMapLayers =
|
|
1811
|
+
export type StaticMapLayers = InteractiveLayers;
|
|
1640
1812
|
export type StaticMapAppearance = Pick<SFMapOptions, 'theme' | 'colors' | 'districtStyle'>;
|
|
1641
1813
|
/** Serializable viewport in the interactive map's fixed 800 × 800 projected space. */
|
|
1642
1814
|
export type MapViewport = readonly [x: number, y: number, size: number];
|
|
@@ -1839,10 +2011,12 @@ export interface InteractiveSFMapData {
|
|
|
1839
2011
|
import type { Geometry, Position } from '../data/types.js';
|
|
1840
2012
|
import type { DistrictYear, SFMapOptions } from './types.js';
|
|
1841
2013
|
export type { DistrictYear, MapMarker, MapOverlay, SFMapOptions } from './types.js';
|
|
2014
|
+
import { type Project } from './geometry.js';
|
|
1842
2015
|
export interface SFMapData {
|
|
1843
2016
|
coast: Geometry;
|
|
1844
2017
|
districts?: Readonly<Partial<Record<DistrictYear, readonly DistrictRowData[]>>>;
|
|
1845
2018
|
neighborhoods?: readonly {
|
|
2019
|
+
id?: string;
|
|
1846
2020
|
name: string;
|
|
1847
2021
|
geometry: Geometry;
|
|
1848
2022
|
}[];
|
|
@@ -1933,13 +2107,23 @@ export declare function resolveMapColors(theme: SFMapOptions['theme'], colors?:
|
|
|
1933
2107
|
selected: string;
|
|
1934
2108
|
};
|
|
1935
2109
|
/** Make an offline SVG and the matching longitude/latitude projection. */
|
|
1936
|
-
export declare function createSFMapWithData(options: SFMapOptions, data: SFMapData): {
|
|
2110
|
+
export declare function createSFMapWithData(options: SFMapOptions, data: SFMapData, renderLabels?: (project: Project) => string): {
|
|
1937
2111
|
svg: string;
|
|
1938
2112
|
project: (coordinates: Position) => [number, number];
|
|
1939
2113
|
viewBox: [number, number, number, number];
|
|
1940
2114
|
};
|
|
1941
2115
|
```
|
|
1942
2116
|
|
|
2117
|
+
### dist/src/geometry.d.ts
|
|
2118
|
+
|
|
2119
|
+
```ts
|
|
2120
|
+
import type { Geometry, Position } from '../data/types.js';
|
|
2121
|
+
export type Project = (position: Position) => [number, number];
|
|
2122
|
+
export declare function rawProject([longitude, latitude]: Position): [number, number];
|
|
2123
|
+
export declare function positions(geometry: Geometry | null | undefined): Position[];
|
|
2124
|
+
export declare function geometryPath(geometry: Geometry | null | undefined, project: Project): string;
|
|
2125
|
+
```
|
|
2126
|
+
|
|
1943
2127
|
### dist/src/static.d.ts
|
|
1944
2128
|
|
|
1945
2129
|
```ts
|
|
@@ -2116,16 +2300,6 @@ import type { FeatureCollection, NeighborhoodProperties } from './types.js';
|
|
|
2116
2300
|
export declare const sfFindNeighborhoods: FeatureCollection<NeighborhoodProperties>;
|
|
2117
2301
|
```
|
|
2118
2302
|
|
|
2119
|
-
### ./dist/src/geometry.d.ts
|
|
2120
|
-
|
|
2121
|
-
```ts
|
|
2122
|
-
import type { Geometry, Position } from '../data/types.js';
|
|
2123
|
-
export type Project = (position: Position) => [number, number];
|
|
2124
|
-
export declare function rawProject([longitude, latitude]: Position): [number, number];
|
|
2125
|
-
export declare function positions(geometry: Geometry | null | undefined): Position[];
|
|
2126
|
-
export declare function geometryPath(geometry: Geometry | null | undefined, project: Project): string;
|
|
2127
|
-
```
|
|
2128
|
-
|
|
2129
2303
|
### ./dist/src/guide.d.ts
|
|
2130
2304
|
|
|
2131
2305
|
```ts
|
|
@@ -2198,4 +2372,78 @@ export declare function createGuideShell(options?: SFMapOptions): string;
|
|
|
2198
2372
|
import type { MapOptions } from './controller-types.js';
|
|
2199
2373
|
/** Configuration only: pair with guide/data or a compatible caller-supplied dataset. */
|
|
2200
2374
|
export declare const guideOptions: Readonly<MapOptions>;
|
|
2375
|
+
export { type MapPalette, mapPalettes } from './palettes.js';
|
|
2376
|
+
```
|
|
2377
|
+
|
|
2378
|
+
### dist/src/palettes.d.ts
|
|
2379
|
+
|
|
2380
|
+
```ts
|
|
2381
|
+
import type { SFMapOptions } from './types.js';
|
|
2382
|
+
/** Shared tokens for static SVGs, interactive maps, and consumer-owned place cards. */
|
|
2383
|
+
export interface MapPalette {
|
|
2384
|
+
readonly colors: NonNullable<SFMapOptions['colors']>;
|
|
2385
|
+
readonly haloColor: string;
|
|
2386
|
+
readonly markerColor: string;
|
|
2387
|
+
readonly selectedMarkerColor: string;
|
|
2388
|
+
readonly selectedMarkerRingColor: string;
|
|
2389
|
+
}
|
|
2390
|
+
/** Opt-in editorial palettes inspired by YorkSF. Existing renderer themes stay unchanged. */
|
|
2391
|
+
export declare const mapPalettes: {
|
|
2392
|
+
nightForest: {
|
|
2393
|
+
colors: {
|
|
2394
|
+
water: string;
|
|
2395
|
+
land: string;
|
|
2396
|
+
park: string;
|
|
2397
|
+
neighborhood: string;
|
|
2398
|
+
district: string;
|
|
2399
|
+
road: string;
|
|
2400
|
+
highway: string;
|
|
2401
|
+
landmark: string;
|
|
2402
|
+
bart: string;
|
|
2403
|
+
label: string;
|
|
2404
|
+
};
|
|
2405
|
+
haloColor: string;
|
|
2406
|
+
markerColor: string;
|
|
2407
|
+
selectedMarkerColor: string;
|
|
2408
|
+
selectedMarkerRingColor: string;
|
|
2409
|
+
};
|
|
2410
|
+
warmPaper: {
|
|
2411
|
+
colors: {
|
|
2412
|
+
water: string;
|
|
2413
|
+
land: string;
|
|
2414
|
+
park: string;
|
|
2415
|
+
neighborhood: string;
|
|
2416
|
+
district: string;
|
|
2417
|
+
road: string;
|
|
2418
|
+
highway: string;
|
|
2419
|
+
landmark: string;
|
|
2420
|
+
bart: string;
|
|
2421
|
+
label: string;
|
|
2422
|
+
};
|
|
2423
|
+
haloColor: string;
|
|
2424
|
+
markerColor: string;
|
|
2425
|
+
selectedMarkerColor: string;
|
|
2426
|
+
selectedMarkerRingColor: string;
|
|
2427
|
+
};
|
|
2428
|
+
};
|
|
2429
|
+
```
|
|
2430
|
+
|
|
2431
|
+
### ./dist/src/cards.d.ts
|
|
2432
|
+
|
|
2433
|
+
```ts
|
|
2434
|
+
import type { MapController } from './controller-types.js';
|
|
2435
|
+
import type { MapMarker } from './types.js';
|
|
2436
|
+
export interface MapCardsOptions {
|
|
2437
|
+
markers: readonly MapMarker[];
|
|
2438
|
+
highlightColor?: string;
|
|
2439
|
+
/** Reveal selected cards with native scrolling, without moving keyboard focus. Default true. */
|
|
2440
|
+
scrollOnSelect?: boolean;
|
|
2441
|
+
}
|
|
2442
|
+
/** Connect consumer-owned [data-map-marker] cards and [data-map-select] buttons to a map. */
|
|
2443
|
+
export declare function bindMapCards(map: MapController, root: HTMLElement, options: MapCardsOptions): {
|
|
2444
|
+
/** Call after map.setMarkers() when your place catalog changes. */
|
|
2445
|
+
setMarkers(next: readonly MapMarker[]): void;
|
|
2446
|
+
/** Remove owned listeners/decoration and restore card attributes. Call before map.destroy(). */
|
|
2447
|
+
destroy(): void;
|
|
2448
|
+
};
|
|
2201
2449
|
```
|
package/llms.txt
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
> Offline San Francisco SVG maps with explicit geographic imports, no runtime dependencies, static rendering, and a browser controller.
|
|
4
4
|
|
|
5
|
-
@kahwee/sf-map-svg 4.
|
|
5
|
+
@kahwee/sf-map-svg 4.5.0 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
|
|
6
6
|
|
|
7
7
|
These are working-tree docs. Unreleased changelog entries may describe APIs absent from npm; verify the installed version before copying them.
|
|
8
8
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kahwee/sf-map-svg",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.5.0",
|
|
4
4
|
"description": "Offline SVG maps of San Francisco with district boundaries, parks, landmarks, and BART stations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/src/api.js",
|
|
@@ -107,6 +107,10 @@
|
|
|
107
107
|
"./presets": {
|
|
108
108
|
"types": "./dist/src/presets.d.ts",
|
|
109
109
|
"import": "./dist/src/presets.js"
|
|
110
|
+
},
|
|
111
|
+
"./cards": {
|
|
112
|
+
"types": "./dist/src/cards.d.ts",
|
|
113
|
+
"import": "./dist/src/cards.js"
|
|
110
114
|
}
|
|
111
115
|
},
|
|
112
116
|
"files": [
|
|
@@ -143,13 +147,13 @@
|
|
|
143
147
|
"@vitest/browser-playwright": "^5.0.3",
|
|
144
148
|
"@vitest/coverage-v8": "5.0.3",
|
|
145
149
|
"@xmldom/xmldom": "^0.9.12",
|
|
146
|
-
"playwright": "^1.
|
|
150
|
+
"playwright": "^1.64.0",
|
|
147
151
|
"polygon-clipping": "^0.15.7",
|
|
148
152
|
"react": "^19.3.0",
|
|
149
153
|
"react-dom": "^19.3.0",
|
|
150
154
|
"storybook": "^10.6.1",
|
|
151
155
|
"typescript": "^7.0.2",
|
|
152
|
-
"vite": "^8.3.
|
|
156
|
+
"vite": "^8.3.4",
|
|
153
157
|
"vitest": "^5.0.3"
|
|
154
158
|
},
|
|
155
159
|
"homepage": "https://kahwee.github.io/sf-map-svg/",
|
|
@@ -168,7 +172,8 @@
|
|
|
168
172
|
"registry": "https://registry.npmjs.org/"
|
|
169
173
|
},
|
|
170
174
|
"scripts": {
|
|
171
|
-
"test": "pnpm build &&
|
|
175
|
+
"test": "pnpm build && pnpm test:built",
|
|
176
|
+
"test:built": "node --test",
|
|
172
177
|
"test:stories": "pnpm build && vitest run --project=storybook",
|
|
173
178
|
"test:stories:coverage": "pnpm build && vitest run --project=storybook --coverage",
|
|
174
179
|
"test:visual": "playwright test --config=playwright.visual.config.mjs",
|
|
@@ -178,14 +183,17 @@
|
|
|
178
183
|
"demo": "pnpm build && node examples/build.mjs",
|
|
179
184
|
"format": "biome check --write .",
|
|
180
185
|
"format:check": "biome format .",
|
|
181
|
-
"check": "pnpm peers check && pnpm lint && pnpm check:docs && pnpm data:check && pnpm typecheck && pnpm typecheck:stories && pnpm typecheck:website && pnpm test && pnpm test:bundle",
|
|
182
|
-
"check:docs": "pnpm build &&
|
|
186
|
+
"check": "pnpm peers check && pnpm lint && pnpm build && pnpm check:docs:built && pnpm data:check && pnpm typecheck:built && pnpm typecheck:stories && pnpm typecheck:website:built && pnpm test:built && pnpm test:bundle",
|
|
187
|
+
"check:docs": "pnpm build && pnpm check:docs:built",
|
|
188
|
+
"check:docs:built": "node scripts/check-docs.mjs && node scripts/build-llm-docs.mjs --check",
|
|
183
189
|
"storybook": "pnpm build && storybook dev -p 6006 --host 127.0.0.1 --no-open",
|
|
184
190
|
"build-storybook": "pnpm build && storybook build",
|
|
185
191
|
"data:catalog": "node scripts/build-data-catalog.mjs",
|
|
186
192
|
"data:check": "node scripts/build-data-catalog.mjs --check",
|
|
187
|
-
"typecheck": "pnpm build &&
|
|
188
|
-
"typecheck:
|
|
193
|
+
"typecheck": "pnpm build && pnpm typecheck:built",
|
|
194
|
+
"typecheck:built": "tsc -p tsconfig.json",
|
|
195
|
+
"typecheck:website": "pnpm build && pnpm typecheck:website:built",
|
|
196
|
+
"typecheck:website:built": "tsc -p tsconfig.website.json",
|
|
189
197
|
"typecheck:stories": "tsc -p tsconfig.stories.json",
|
|
190
198
|
"data:normalize-realtor": "node scripts/normalize-realtor.mjs",
|
|
191
199
|
"data:guide": "node scripts/build-guide-data.mjs",
|