@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.
Files changed (44) hide show
  1. package/CHANGELOG.md +22 -0
  2. package/README.md +18 -1
  3. package/dist/src/cards.d.ts +15 -0
  4. package/dist/src/cards.js +138 -0
  5. package/dist/src/cluster-layer.d.ts +2 -0
  6. package/dist/src/cluster-layer.js +2 -0
  7. package/dist/src/explorer-controls.d.ts +26 -0
  8. package/dist/src/explorer-controls.js +120 -0
  9. package/dist/src/explorer-core.d.ts +6 -1
  10. package/dist/src/explorer-core.js +168 -701
  11. package/dist/src/explorer-shell.d.ts +62 -0
  12. package/dist/src/explorer-shell.js +228 -0
  13. package/dist/src/explorer-sidebar.d.ts +20 -0
  14. package/dist/src/explorer-sidebar.js +79 -0
  15. package/dist/src/guide-data.js +1 -0
  16. package/dist/src/guide-map.js +2 -1
  17. package/dist/src/guide-static.js +1 -0
  18. package/dist/src/label-candidates.d.ts +24 -0
  19. package/dist/src/label-candidates.js +29 -0
  20. package/dist/src/label-renderer.d.ts +1 -10
  21. package/dist/src/label-spread.d.ts +5 -0
  22. package/dist/src/label-spread.js +36 -0
  23. package/dist/src/map-core.d.ts +3 -1
  24. package/dist/src/map-core.js +7 -5
  25. package/dist/src/map.js +2 -1
  26. package/dist/src/neighborhood-layer.d.ts +67 -0
  27. package/dist/src/neighborhood-layer.js +256 -0
  28. package/dist/src/overlay-layer.d.ts +13 -0
  29. package/dist/src/overlay-layer.js +68 -0
  30. package/dist/src/palettes.d.ts +48 -0
  31. package/dist/src/palettes.js +40 -0
  32. package/dist/src/presets.d.ts +1 -0
  33. package/dist/src/presets.js +1 -0
  34. package/dist/src/static-labels.d.ts +7 -0
  35. package/dist/src/static-labels.js +115 -0
  36. package/dist/src/static.js +31 -3
  37. package/dist/src/types.d.ts +16 -1
  38. package/docs/API.md +38 -2
  39. package/docs/EXAMPLES.md +1 -0
  40. package/docs/consumer-integration.md +81 -1
  41. package/docs/guide-bundle-report.md +4 -4
  42. package/llms-full.txt +274 -26
  43. package/llms.txt +1 -1
  44. 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.3.2 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
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.3.2 · local working tree. -->
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. The interactive renderer is 34.6 KiB gzip, up from 34.2 KiB, within the unchanged 35 KiB limit; static rendering remains 7.9 KiB.
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.3.2 · local working tree. -->
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.3.2 · local working tree. -->
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 except `neighborhoodLabels`. 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.
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.3.2 · local working tree. -->
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.3.2 · local working tree. -->
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
- appearance: { theme: 'transit' },
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.3.2 · local working tree. -->
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.3.2 · local working tree. -->
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.3.2 · local working tree. -->
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.3.2 · 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.
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 = Omit<InteractiveLayers, 'neighborhoodLabels'>;
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.3.2 · local working tree. Node >=24.0.0 for server rendering; browsers need a DOM and a JSON-capable bundler.
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.2",
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.63.0",
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.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 && node --test",
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 && node scripts/check-docs.mjs && node scripts/build-llm-docs.mjs --check",
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 && tsc -p tsconfig.json",
188
- "typecheck:website": "pnpm build && tsc -p tsconfig.website.json",
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",