@kahwee/sf-map-svg 1.5.1 → 2.0.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 (50) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +146 -6
  3. package/dist/data/README.md +5 -1
  4. package/dist/data/types.d.ts +4 -1
  5. package/dist/src/api.d.ts +3 -0
  6. package/dist/src/api.js +3 -0
  7. package/dist/src/camera.d.ts +16 -0
  8. package/dist/src/camera.js +55 -0
  9. package/dist/src/clusters.d.ts +7 -0
  10. package/dist/src/clusters.js +33 -0
  11. package/dist/src/configuration.d.ts +5 -0
  12. package/dist/src/configuration.js +73 -0
  13. package/dist/src/controller-types.d.ts +84 -0
  14. package/dist/src/controller-types.js +1 -0
  15. package/dist/src/explorer-core.d.ts +1 -1
  16. package/dist/src/explorer-core.js +570 -145
  17. package/dist/src/features.d.ts +24 -0
  18. package/dist/src/features.js +99 -0
  19. package/dist/src/guide-data.js +3 -2
  20. package/dist/src/guide-detailed.js +3 -2
  21. package/dist/src/guide-map.d.ts +4 -0
  22. package/dist/src/guide-map.js +35 -10
  23. package/dist/src/guide-shell.d.ts +2 -0
  24. package/dist/src/guide-shell.js +16 -0
  25. package/dist/src/guide-static.d.ts +9 -0
  26. package/dist/src/guide-static.js +26 -0
  27. package/dist/src/guide.d.ts +2 -1
  28. package/dist/src/guide.js +1 -1
  29. package/dist/src/interactive-data.d.ts +1 -1
  30. package/dist/src/interactive-data.js +2 -0
  31. package/dist/src/interactive.d.ts +1 -1
  32. package/dist/src/interactive.js +2 -0
  33. package/dist/src/layers.js +3 -1
  34. package/dist/src/map-core.js +3 -8
  35. package/dist/src/map.d.ts +7 -0
  36. package/dist/src/map.js +106 -0
  37. package/dist/src/presets.d.ts +3 -0
  38. package/dist/src/presets.js +17 -0
  39. package/dist/src/static.d.ts +10 -0
  40. package/dist/src/static.js +5 -0
  41. package/dist/src/types.d.ts +73 -9
  42. package/dist/src/validation.d.ts +7 -0
  43. package/dist/src/validation.js +226 -0
  44. package/dist/src/viewport.js +9 -2
  45. package/docs/EXAMPLES.md +11 -1
  46. package/docs/api-audit.md +80 -0
  47. package/docs/consumer-integration.md +165 -0
  48. package/docs/guide-bundle-report.md +19 -0
  49. package/docs/migration-v2.md +238 -0
  50. package/package.json +40 -10
@@ -0,0 +1,19 @@
1
+ # Guide bundle size report
2
+
3
+ Generated 2026-09-27 by `pnpm report:guide` with Vite production minification and gzip compression. Each emitted JS chunk is compressed independently. The before measurement uses the compatibility `@kahwee/sf-map-svg/interactive` entry; the after measurement uses `@kahwee/sf-map-svg/guide` initial static imports.
4
+
5
+ | Entry | Initial JS, raw | Initial JS, gzip | Explicit detail JS, gzip |
6
+ | --- | ---: | ---: | ---: |
7
+ | v2 root (explicit data) | 76.0 KB | 22.8 KB | — |
8
+ | v2 static renderer | 13.0 KB | 4.5 KB | — |
9
+ | Compatibility interactive (before) | 6309.8 KB | 1813.6 KB | — |
10
+ | Data-free interactive renderer | 72.5 KB | 21.8 KB | — |
11
+ | Guide preset (after) | 461.7 KB | 110.2 KB | 473.7 KB |
12
+
13
+ **Change in initial gzip:** 93.9% smaller. **500 KB target:** met.
14
+
15
+ The initial guide chunk graph includes only the overview coast, SFAR realtor neighborhoods, major parks, selected highways, six selected streets, and BART points. It excludes historical districts, SF Find neighborhoods, analysis neighborhoods, and the full catalog. Detailed coast, selected SFAR boundaries, parks, selected highway routes, streets, and BART data are in dynamic chunks and load only when `loadGuideDetailedData()` is called. The data inclusion assertions run as part of this report command.
16
+
17
+ ## Emitted entry chunks
18
+
19
+ - `guide.js`
@@ -0,0 +1,238 @@
1
+ # Version 2: data, configuration and lifecycle boundaries
2
+
3
+ Version 2 changes the root export. Geography is an explicit dependency, rather than
4
+ an accidental consequence of importing a renderer. The new `createMap` returns a
5
+ controller with `element` and `camera`; consumers no longer need an extended DOM
6
+ node as their application API. The existing renderer engine stays shared, preserving
7
+ its visual defaults and the established browser regression coverage.
8
+
9
+ ## Choose your migration path
10
+
11
+ Install `@kahwee/sf-map-svg@2.0.0`. You can migrate in two steps:
12
+
13
+ 1. **Keep your existing map working:** change old root imports to `/legacy`.
14
+ Existing `/guide`, `/interactive`, `/interactive-data`, `/explorer`, `/custom-map`,
15
+ `/transit`, and data subpaths remain available. No controller rewrite is required
16
+ just to adopt 2.0.0 through these compatibility paths.
17
+ 2. **Adopt the v2 API:** choose explicit data, group options, mount `map.element`,
18
+ move camera calls to `map.camera`, and register events with `map.on()`.
19
+
20
+ Unknown configuration keys, malformed values, duplicate marker/overlay IDs and
21
+ mutation of frozen preset data are no longer tolerated. These validation changes
22
+ also affect compatibility renderers where the shared engine applies them. Remove
23
+ app-only fields from options (keep them in your own state), correct misspellings,
24
+ and use `structuredClone(guideMapData)` before deriving a custom dataset.
25
+
26
+ ### Smallest change for an existing static map
27
+
28
+ Before (v1):
29
+
30
+ ```js
31
+ import { renderSFMap } from '@kahwee/sf-map-svg';
32
+ const svg = renderSFMap({ landmarks: true });
33
+ ```
34
+
35
+ After (v2 compatibility, same full dataset and result):
36
+
37
+ ```js
38
+ import { renderSFMap } from '@kahwee/sf-map-svg/legacy';
39
+ const svg = renderSFMap({ landmarks: true });
40
+ ```
41
+
42
+ Also move `createSFMap`, `districtColors`, `districtYears`, `neighborhoodNames`,
43
+ `DistrictYear`, and `SFMapOptions` root imports to `/legacy`. `MapMarker` and
44
+ `MapOverlay` types remain available at the v2 root. The new static options type is
45
+ named `StaticMapOptions`.
46
+
47
+ To explicitly choose smaller overview data (a deliberate detail change):
48
+
49
+ ```js
50
+ import { renderMap } from '@kahwee/sf-map-svg/static';
51
+ import { guideMapData } from '@kahwee/sf-map-svg/guide/data';
52
+ const { svg, project } = renderMap(guideMapData.map, {
53
+ theme: 'transit',
54
+ districtFills: false,
55
+ districtLines: false,
56
+ districtLabels: false,
57
+ landmarks: true,
58
+ neighborhoodLines: true,
59
+ });
60
+ ```
61
+
62
+ `renderMap` returns an object containing `svg` and projection helpers; it does not
63
+ return the markup string directly. It takes **data first, options second**, unlike
64
+ legacy `createSFMapWithData(options, data)` on `/custom-map`.
65
+
66
+ ### Complete interactive guide migration
67
+
68
+ Both examples expect `<div id="map"></div>` in the page and a bundler with JSON
69
+ import support. Call the shown cleanup function when your component unmounts.
70
+
71
+ Before (v1):
72
+
73
+ ```js
74
+ import { createGuideMap } from '@kahwee/sf-map-svg/guide';
75
+ const places = [{ id: 'dolores', lng: -122.4269, lat: 37.7596, label: 'Dolores Park' }];
76
+ const map = createGuideMap({
77
+ markers: places,
78
+ layers: { roadLabels: false },
79
+ controls: { pan: false },
80
+ });
81
+ document.querySelector('#map').append(map);
82
+ const selected = event => console.log(event.detail.marker?.id);
83
+ map.addEventListener('markerchange', selected);
84
+ map.selectMarker('dolores');
85
+ const saved = map.getViewport();
86
+ map.setViewport(saved);
87
+ const cleanup = () => {
88
+ map.removeEventListener('markerchange', selected);
89
+ map.destroy();
90
+ map.remove();
91
+ };
92
+ ```
93
+
94
+ After (v2 controller, with optional motion):
95
+
96
+ ```js
97
+ import { createMap } from '@kahwee/sf-map-svg';
98
+ import { guideMapData } from '@kahwee/sf-map-svg/guide/data';
99
+ import { guideOptions } from '@kahwee/sf-map-svg/presets';
100
+ const places = [{ id: 'dolores', lng: -122.4269, lat: 37.7596, label: 'Dolores Park' }];
101
+ const map = createMap(guideMapData, {
102
+ ...guideOptions,
103
+ markers: places,
104
+ // Merge the preset's layer group; a shallow top-level spread replaces it.
105
+ layers: { ...guideOptions.layers, roadLabels: false },
106
+ controls: { pan: false },
107
+ features: { motion: { duration: 400 }, markerEntrance: true },
108
+ });
109
+ document.querySelector('#map').append(map.element);
110
+ const unsubscribe = map.on('markerchange', ({ marker }) => console.log(marker?.id));
111
+ map.selectMarker('dolores');
112
+ const saved = map.camera.get();
113
+ map.camera.set(saved, { animate: false });
114
+ map.configure({ features: { motion: false }, controls: { pan: true } });
115
+ const cleanup = () => {
116
+ map.destroy(); // Also removes subscriptions created through on().
117
+ map.element.remove();
118
+ };
119
+ // unsubscribe() can remove this one subscription earlier; it is idempotent.
120
+ ```
121
+
122
+ Do not spread unconverted v1 options directly into `createMap`: colors/theme and
123
+ other style fields move into `appearance`, while new opt-in behavior lives under
124
+ `features`. Motion, entrance, ring and clustering options are new in this release;
125
+ references below to their flat form describe the **v2 compatibility API**, not
126
+ features that existed in the published v1 package.
127
+
128
+ ### Data types and bundle ownership
129
+
130
+ `StaticMapData` is the coast plus optional district rows, parks, roads, stations and
131
+ neighborhood geometry consumed by `renderMap`. `MapData` wraps that as `map` and adds
132
+ source-aware `neighborhoods` collections and optional interactive `districts`.
133
+ Use `renderMap(guideMapData.map, options)` for static SVG and
134
+ `createMap(guideMapData, options)` for the browser controller. Do not interchange
135
+ these two shapes. Data is borrowed for the map lifetime; treat supplied collections
136
+ as immutable. A coast-only controller can pass `{ map: { coast }, neighborhoods: {} }`.
137
+
138
+ ## Migration
139
+
140
+ | Version 1 or compatibility API | Version 2 |
141
+ | --- | --- |
142
+ | Root `renderSFMap(options)` | `renderMap(data, options).svg`; or move the old import to `/legacy` |
143
+ | Root `createSFMap(options)` | `renderMap(data, options)`; or `/legacy` |
144
+ | `createGuideMap(options)` | `createMap(guideMapData, options)` with the preset and explicit layer merge shown below |
145
+ | `host.append(map)` | `host.append(map.element)` |
146
+ | Top-level `motion`, `clustering`, entrances, rings, furniture | `features: { ... }` |
147
+ | Top-level colors, theme, typography, area/marker styling, chrome tokens | `appearance: { ... }` |
148
+ | `setFeatures`, `setLayers`, `setControls` | `configure({ features, layers, controls })` |
149
+ | `getFeatures()` | `getConfiguration().features` |
150
+ | `setViewport`, `getViewport`, `resetView`, `zoomBy`, `panBy`, `stopAnimation` | `camera.set`, `.get`, `.reset`, `.zoom`, `.pan`, `.stop` |
151
+ | `fitGeometry(geometry, padding, motion)` | `camera.fit(geometry, { padding, ...motion })` |
152
+ | `addEventListener('markerchange', e => e.detail)` | `on('markerchange', detail => ...)` returns unsubscribe |
153
+ | `onMarkerActivate` / `onOverlayActivate` options | `on('markerchange', ...)` / `on('overlayactivate', ...)`; markerchange also reports clearing |
154
+ | `getSelection()` | `getSelectedNeighborhood()`; `getSelectedMarker()` remains distinct |
155
+ | Calls after destroy silently ignored | Controller operations throw; repeated `destroy()` and unsubscribe are safe |
156
+
157
+ `on()` supports markerchange, neighborhoodchange, overlayactivate, clusteractivate,
158
+ viewportchange, and mapresize. Each listener receives a detached detail snapshot;
159
+ mapresize has undefined detail. Browser-native events still belong on `map.element`. V2 overlays are activatable
160
+ buttons (mouse and keyboard), whether or not a listener is registered; provide a
161
+ meaningful `label`. Static overlays remain presentation-only.
162
+ Do not access the compatibility methods that happen to exist on its underlying DOM
163
+ implementation: they are not part of the v2 controller contract.
164
+
165
+ For the full sidebar explorer or compact progressive shell, existing `/explorer`
166
+ and `/guide/map` APIs remain supported. They keep their established element return
167
+ value and options. Their names do not silently change meaning in this major version.
168
+ This staged migration avoids forcing a host to rebuild working SSR shell integration.
169
+
170
+ ## One configuration boundary
171
+
172
+ Construction and `configure()` use the same `features`, `layers`, and `controls`
173
+ groups. All groups in a patch are validated before mutation. Updating controls does
174
+ not restart marker entrances or cancel an in-progress camera move. Setting a motion
175
+ option deliberately cancels the old camera timeline.
176
+
177
+ - Omitted groups and keys retain their values.
178
+ - `false` disables a feature/switch.
179
+ - An explicit undefined key resets that key to its default.
180
+ - An undefined group resets that entire group.
181
+ - Nested feature objects replace; they do not deep-merge.
182
+ - `getConfiguration()` returns detached normalized features and explicit layer/control
183
+ overrides. Missing overrides continue to follow mode/library defaults.
184
+
185
+ Appearance is immutable configuration for one instance. Markers, overlays, source,
186
+ mode, labels and selection have explicit methods; they are not silently mixed into
187
+ configuration patches. This keeps a change of visual controls from triggering
188
+ geographic replacement or camera movement. Defaults remain stable and motion remains
189
+ opt-in; reduced motion always overrides application choices.
190
+
191
+ `destroy()` removes owned subscriptions and cancels runtime work. It leaves the DOM
192
+ in place so the host framework can unmount it; it cannot dispose application-owned
193
+ listeners or arbitrary callout resources. All controller operations, including reads
194
+ and new subscriptions, throw after disposal. The `destroyed` flag and element references
195
+ remain readable for cleanup.
196
+
197
+ ## Bundle contract
198
+
199
+ | Entry | Runtime | Geographic data |
200
+ | --- | --- | --- |
201
+ | root / `/map` | Controller and browser renderer | None |
202
+ | `/static` | SVG renderer only | None |
203
+ | `/presets` | Small immutable configuration | None |
204
+ | `/guide/data` | Immutable overview dataset | Selected SFAR/parks/roads/BART/coast |
205
+ | `/guide/detailed` | Explicit asynchronous loader | Detailed data in lazy chunks |
206
+ | `/legacy`, `/interactive`, `/explorer` | Compatibility renderer | Broad bundled datasets |
207
+
208
+ Use named imports. The production bundle test imports `renderMap` from the root
209
+ and verifies that tree shaking removes the browser runtime. It also rejects any
210
+ geographic JSON in the v2 renderer graph and enforces 35 KiB gzip for the root and
211
+ 10 KiB for the static consumer. The guide data budget remains separately enforced.
212
+
213
+ Optional feature *behavior* can be switched at runtime; that does not mean each
214
+ feature's implementation disappears from the renderer bundle. Geography dominates
215
+ size, so explicit dataset imports and separately cached, versioned assets are the
216
+ first optimization. A generic plugin registration framework would add lifecycle
217
+ complexity without a demonstrated size benefit; the internal camera, clustering,
218
+ configuration and validation modules already provide testable boundaries for a later
219
+ code-split feature if measurements justify it.
220
+
221
+ Never import the compatibility barrel merely to obtain one dataset. Keep static
222
+ rendering on the server/build side. Load browser code when needed, and request detail
223
+ only on demand. See the measured [bundle report](guide-bundle-report.md) and
224
+ [consumer guide](consumer-integration.md).
225
+
226
+ ## Compatibility and testing
227
+
228
+ The v2 root is intentionally breaking; the package metadata is 2.0.0. The old root
229
+ is available under `/legacy`. Existing named subpaths continue to work, while new
230
+ examples exercise the controller. Node configuration tests, browser stories, packed
231
+ consumer declaration checks, and production import-graph checks cover the new
232
+ boundary. No runtime dependency or geographic data change is introduced.
233
+
234
+ A minimal browser basemap can supply `{ map: { coast }, neighborhoods: {} }`.
235
+ Neighborhood and district modes require their respective datasets; requesting a
236
+ missing mode/source fails without changing the live map. With a single alternative
237
+ neighborhood source and no explicit `source`, the renderer chooses the available
238
+ source. SFAR remains the default whenever it is supplied.
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@kahwee/sf-map-svg",
3
- "version": "1.5.1",
3
+ "version": "2.0.0",
4
4
  "description": "Offline SVG maps of San Francisco with district boundaries, parks, landmarks, and BART stations.",
5
5
  "type": "module",
6
- "main": "./dist/src/index.js",
7
- "types": "./dist/src/index.d.ts",
6
+ "main": "./dist/src/api.js",
7
+ "types": "./dist/src/api.d.ts",
8
8
  "exports": {
9
9
  ".": {
10
- "types": "./dist/src/index.d.ts",
11
- "import": "./dist/src/index.js"
10
+ "types": "./dist/src/api.d.ts",
11
+ "import": "./dist/src/api.js"
12
12
  },
13
13
  "./custom-map": {
14
14
  "types": "./dist/src/custom-map.d.ts",
@@ -63,6 +63,7 @@
63
63
  "import": "./dist/data/stations.js"
64
64
  },
65
65
  "./data/*.json": "./dist/data/*.json",
66
+ "./data/candidates/*.json": "./dist/data/candidates/*.json",
66
67
  "./geometry": {
67
68
  "types": "./dist/src/geometry.d.ts",
68
69
  "import": "./dist/src/geometry.js"
@@ -98,6 +99,26 @@
98
99
  "./transit": {
99
100
  "types": "./dist/src/transit.d.ts",
100
101
  "import": "./dist/src/transit.js"
102
+ },
103
+ "./guide/static": {
104
+ "types": "./dist/src/guide-static.d.ts",
105
+ "import": "./dist/src/guide-static.js"
106
+ },
107
+ "./legacy": {
108
+ "types": "./dist/src/index.d.ts",
109
+ "import": "./dist/src/index.js"
110
+ },
111
+ "./map": {
112
+ "types": "./dist/src/map.d.ts",
113
+ "import": "./dist/src/map.js"
114
+ },
115
+ "./static": {
116
+ "types": "./dist/src/static.d.ts",
117
+ "import": "./dist/src/static.js"
118
+ },
119
+ "./presets": {
120
+ "types": "./dist/src/presets.d.ts",
121
+ "import": "./dist/src/presets.js"
101
122
  }
102
123
  },
103
124
  "files": [
@@ -105,7 +126,12 @@
105
126
  "docs/EXAMPLES.md",
106
127
  "README.md",
107
128
  "SOURCES.md",
108
- "LICENSE"
129
+ "LICENSE",
130
+ "docs/consumer-integration.md",
131
+ "docs/api-audit.md",
132
+ "docs/guide-bundle-report.md",
133
+ "docs/migration-v2.md",
134
+ "CHANGELOG.md"
109
135
  ],
110
136
  "sideEffects": false,
111
137
  "repository": {
@@ -122,7 +148,8 @@
122
148
  "@storybook/addon-docs": "^10.6.0",
123
149
  "@storybook/addon-vitest": "10.6.0",
124
150
  "@storybook/html-vite": "^10.6.0",
125
- "@vitest/browser-playwright": "^4.1.11",
151
+ "@vitest/browser-playwright": "^5.0.2",
152
+ "@vitest/coverage-v8": "5.0.2",
126
153
  "@xmldom/xmldom": "^0.9.12",
127
154
  "playwright": "^1.63.0",
128
155
  "polygon-clipping": "^0.15.7",
@@ -131,7 +158,7 @@
131
158
  "storybook": "^10.6.0",
132
159
  "typescript": "^7.0.2",
133
160
  "vite": "^8.3.1",
134
- "vitest": "^4.1.11"
161
+ "vitest": "^5.0.2"
135
162
  },
136
163
  "homepage": "https://kahwee.github.io/sf-map-svg/",
137
164
  "bugs": {
@@ -151,10 +178,11 @@
151
178
  "scripts": {
152
179
  "test": "pnpm build && node --test",
153
180
  "test:stories": "pnpm build && vitest run --project=storybook",
181
+ "test:stories:coverage": "pnpm build && vitest run --project=storybook --coverage",
154
182
  "demo": "pnpm build && node examples/build.mjs",
155
183
  "format": "biome check --write .",
156
184
  "format:check": "biome format .",
157
- "check": "pnpm lint && pnpm data:check && pnpm typecheck && pnpm test",
185
+ "check": "pnpm peers check && pnpm lint && pnpm data:check && pnpm typecheck && pnpm test && pnpm test:bundle",
158
186
  "storybook": "pnpm build && storybook dev -p 6006 --host 127.0.0.1 --no-open",
159
187
  "build-storybook": "pnpm build && storybook build",
160
188
  "data:catalog": "node scripts/build-data-catalog.mjs",
@@ -167,6 +195,8 @@
167
195
  "build": "node scripts/build.mjs",
168
196
  "lint": "biome check .",
169
197
  "audit": "pnpm audit --audit-level high",
170
- "build:pages": "pnpm build && node scripts/build-pages.mjs"
198
+ "build:pages": "pnpm build && node scripts/build-pages.mjs",
199
+ "test:bundle": "node scripts/report-guide-bundle.mjs --check",
200
+ "release:notes": "node scripts/release-notes.mjs"
171
201
  }
172
202
  }