@kahwee/sf-map-svg 1.4.1 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,17 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
4
4
 
5
5
  ## Unreleased
6
6
 
7
+ ## 1.5.1 — 2026-09-26
8
+
9
+ - Run focused Storybook 10 browser interactions and accessibility checks in CI and before npm publishing, covering overlapping markers, route overlays, and keyboard selection.
10
+ - Validate certified proposition district sums against city totals in the data test suite.
11
+ - Keep Biome and the development dependency set current after an audit and outdated-package review.
12
+
13
+ ## 1.5.0 — 2026-09-26
14
+
15
+ - Add `controls.neighborhoodPicker`, `controls.markerPicker`, `controls.help`, and `controls.status` so compact embeds can drop redundant chrome. Hidden help remains the map's accessible description; a hidden status line remains a polite live region; source attribution stays visible.
16
+ - Keep a configured `strings.chooseMarker` label after `setMarkers()` updates instead of reverting to “Choose marker”.
17
+
7
18
  ## 1.4.1 — 2026-09-26
8
19
 
9
20
  - Add a task-based example gallery and code recipes, with clearer routes among the Pages atlas, local measures, district history, transit, and lightweight map.
package/README.md CHANGED
@@ -255,7 +255,19 @@ aliases. Import types including `InteractiveSFMapOptions`, `InteractiveSFMapElem
255
255
  | `overlays` | `[]` | GeoJSON line or polygon overlays with stable IDs and optional SVG styles |
256
256
  | `style` | Built-in tokens | Explorer CSS tokens: `ink`, `surface`, `accent`, `border`, `focus`, `controlGap`, `font` |
257
257
  | `strings` | English defaults | Replace visible map labels and gesture help for localization |
258
- | `controls` | All enabled | Independently hide `zoom`, `pan`, `reset`, `labels`, `touch`, or `legend`; source attribution remains visible |
258
+ | `controls` | All enabled | Independently hide `zoom`, `pan`, `reset`, `labels`, `touch`, `legend`, `neighborhoodPicker`, `markerPicker`, `help`, or `status`; source attribution remains visible |
259
+
260
+ For a compact embed, hide the native choosers only when the page already lists every
261
+ marker or area as an accessible control. Hidden help remains the map's accessible
262
+ description, and a hidden status line remains a polite live region:
263
+
264
+ ```js
265
+ const map = createGuideMap({
266
+ interface: 'map',
267
+ controls: { labels: false, neighborhoodPicker: false, markerPicker: false, help: false, status: false },
268
+ strings: { chooseMarker: 'Place on map' }, // also used after setMarkers() updates
269
+ });
270
+ ```
259
271
 
260
272
  Explicit layer options override mode defaults even after `setMode()`. District fills, outlines,
261
273
  and badges can therefore be composed with neighborhood names without requiring district
@@ -370,10 +382,11 @@ pnpm install --frozen-lockfile
370
382
  pnpm check # formatting, data catalog, types, and tests
371
383
  pnpm demo # generated SVGs and example pages
372
384
  pnpm build-storybook # static component documentation
385
+ pnpm test:stories # Storybook 10 browser checks in Chromium
373
386
  pnpm test:package # install and check the packed package
374
387
  ```
375
388
 
376
- Use `pnpm format` to apply Biome formatting and safe lint fixes. Run `pnpm storybook` for interactive component examples at http://127.0.0.1:6006. CI runs checks on Node 22, 24, and 26. See [CONTRIBUTING.md](CONTRIBUTING.md) for source structure and release instructions.
389
+ Use `pnpm format` to apply Biome formatting and safe lint fixes. Install Chromium once with `pnpm exec playwright install chromium` before local Storybook tests. Run `pnpm storybook` for interactive component examples at http://127.0.0.1:6006. CI runs package checks on Node 22, 24, and 26 plus Chromium Storybook checks on Node 24. See [CONTRIBUTING.md](CONTRIBUTING.md) for source structure and release instructions.
377
390
 
378
391
  ### Browser verification
379
392
 
@@ -30,6 +30,7 @@ function svgElement(tag, attributes = {}) {
30
30
  node.setAttribute(key, String(value));
31
31
  return node;
32
32
  }
33
+ let explorerCount = 0;
33
34
  /** Create an offline, browser-only neighborhood explorer. Call destroy() before disposal. */
34
35
  export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'neighborhoods', labels = true, neighborhood, year = 2022, theme = 'transit', interface: chrome = 'explorer', layers = {}, selectableNeighborhoods = true, labelSize = {}, fitPadding = 24, markers: initialMarkers = [], markerRadius = 6, markerHitSize = 44, markerColor = '#245b61', selectedMarkerColor = '#f04f32', onMarkerActivate, overlays: initialOverlays = [], onOverlayActivate, style: styleOptions = {}, strings = {}, controls = {}, } = {}, data) {
35
36
  if (!['neighborhoods', 'districts', 'basemap'].includes(mode))
@@ -141,6 +142,7 @@ export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'nei
141
142
  .sf-explorer-hint{padding:6px 16px 10px;font-size:12px;color:#586f80;background:#fff}
142
143
  .sf-explorer text{pointer-events:none}
143
144
  .sf-explorer-status{padding:0 16px 14px;font-size:12px;background:#fff;color:#496578}
145
+ .sf-explorer-visually-hidden{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;border:0}
144
146
  .sf-explorer[data-interface=map] .sf-explorer-body{display:block}
145
147
  .sf-explorer[data-interface=map] .sf-explorer-header,.sf-explorer[data-interface=map] .sf-explorer-panel,.sf-explorer[data-interface=map] .sf-explorer-detail{display:none}
146
148
  .sf-explorer-feature-controls{display:flex;flex-wrap:wrap;gap:8px;padding:10px;background:#fff}
@@ -222,6 +224,7 @@ export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'nei
222
224
  const canvas = element('div', '', 'sf-explorer-canvas');
223
225
  const hint = element('p', strings.gestureHelp ??
224
226
  'Drag to pan · Ctrl/⌘ + scroll to zoom · Focus map: arrows pan, +/− zoom, Home resets. Touch: scroll the page, or enable Touch navigation to pan and pinch the map. Escape exits.', 'sf-explorer-hint');
227
+ hint.id = `sf-explorer-help-${++explorerCount}`;
225
228
  const status = element('p', '', 'sf-explorer-status');
226
229
  status.setAttribute('aria-live', 'polite');
227
230
  const legend = element('div', '', 'sf-explorer-legend');
@@ -764,12 +767,19 @@ export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'nei
764
767
  ? `${year} supervisorial districts. Numbers identify each district.`
765
768
  : `${formatSourceLabel(source)}. Select a neighborhood to begin.`;
766
769
  }
770
+ function syncFeatureControls() {
771
+ featureControls.hidden = neighborhoodLabel.hidden && markerLabel.hidden;
772
+ }
767
773
  function updateComposition() {
768
774
  const neighborhoodsVisible = enabled('neighborhoodLines') || (labels && enabled('neighborhoodLabels'));
769
775
  areas.style.display = neighborhoodsVisible ? '' : 'none';
770
776
  areas.style.pointerEvents = selectableNeighborhoods ? '' : 'none';
771
777
  neighborhoodLabel.hidden =
772
- !neighborhoodsVisible || !selectableNeighborhoods || chrome === 'explorer';
778
+ !neighborhoodsVisible ||
779
+ !selectableNeighborhoods ||
780
+ chrome === 'explorer' ||
781
+ controls.neighborhoodPicker === false;
782
+ syncFeatureControls();
773
783
  for (const item of items) {
774
784
  item.node.setAttribute('stroke-width', item.feature === selected ? '2.2' : enabled('neighborhoodLines') ? '.55' : '0');
775
785
  item.node.setAttribute('tabindex', selectableNeighborhoods && item === items[0] ? '0' : '-1');
@@ -881,9 +891,10 @@ export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'nei
881
891
  markerSelect.append(option);
882
892
  return { marker, point, node, dot, hit };
883
893
  });
884
- markerLabel.hidden = !markerItems.length;
894
+ markerLabel.hidden = !markerItems.length || controls.markerPicker === false;
885
895
  if (markerLabel.firstChild)
886
- markerLabel.firstChild.textContent = `Choose marker (${markerItems.length})`;
896
+ markerLabel.firstChild.textContent = `${strings.chooseMarker ?? 'Choose marker'} (${markerItems.length})`;
897
+ syncFeatureControls();
887
898
  const nextSelection = previous && ids.has(previous)
888
899
  ? previous
889
900
  : (markers.find((marker) => marker.selected)?.id ?? null);
@@ -902,6 +913,13 @@ export function createNeighborhoodExplorerCore({ source = 'realtor', mode = 'nei
902
913
  labelsButton.hidden = true;
903
914
  if (controls.legend === false)
904
915
  legend.hidden = true;
916
+ if (controls.help === false) {
917
+ // The instructions stay available to assistive technology as the map's description.
918
+ hint.hidden = true;
919
+ svg.setAttribute('aria-describedby', hint.id);
920
+ }
921
+ if (controls.status === false)
922
+ status.classList.add('sf-explorer-visually-hidden');
905
923
  function setMode(next) {
906
924
  if (destroyed)
907
925
  return;
@@ -101,7 +101,12 @@ export interface NeighborhoodExplorerOptions {
101
101
  /** Stable theme tokens consumed by the explorer chrome. */
102
102
  style?: Partial<Record<'ink' | 'surface' | 'accent' | 'border' | 'focus' | 'controlGap' | 'font', string>>;
103
103
  strings?: Partial<Record<'title' | 'mode' | 'source' | 'search' | 'chooseNeighborhood' | 'chooseMarker' | 'touchNavigation' | 'reset' | 'emptyResults' | 'gestureHelp', string>>;
104
- controls?: Partial<Record<'zoom' | 'pan' | 'reset' | 'labels' | 'touch' | 'legend', boolean>>;
104
+ /**
105
+ * Independently hide chrome. `neighborhoodPicker` and `markerPicker` hide the native
106
+ * choosers (supply your own accessible list); `help` keeps the gesture help as the map's
107
+ * accessible description; `status` keeps a visually hidden live region. Attribution stays.
108
+ */
109
+ controls?: Partial<Record<'zoom' | 'pan' | 'reset' | 'labels' | 'touch' | 'legend' | 'neighborhoodPicker' | 'markerPicker' | 'help' | 'status', boolean>>;
105
110
  }
106
111
  export interface NeighborhoodExplorerElement extends HTMLElement {
107
112
  selectNeighborhood(name: string | null, options?: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kahwee/sf-map-svg",
3
- "version": "1.4.1",
3
+ "version": "1.5.1",
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/index.js",
@@ -118,13 +118,20 @@
118
118
  },
119
119
  "devDependencies": {
120
120
  "@biomejs/biome": "2.5.14",
121
+ "@storybook/addon-a11y": "10.6.0",
121
122
  "@storybook/addon-docs": "^10.6.0",
123
+ "@storybook/addon-vitest": "10.6.0",
122
124
  "@storybook/html-vite": "^10.6.0",
125
+ "@vitest/browser-playwright": "^4.1.11",
123
126
  "@xmldom/xmldom": "^0.9.12",
127
+ "playwright": "^1.63.0",
124
128
  "polygon-clipping": "^0.15.7",
129
+ "react": "^19.3.0",
130
+ "react-dom": "^19.3.0",
125
131
  "storybook": "^10.6.0",
126
132
  "typescript": "^7.0.2",
127
- "vite": "^8.3.1"
133
+ "vite": "^8.3.1",
134
+ "vitest": "^4.1.11"
128
135
  },
129
136
  "homepage": "https://kahwee.github.io/sf-map-svg/",
130
137
  "bugs": {
@@ -143,6 +150,7 @@
143
150
  },
144
151
  "scripts": {
145
152
  "test": "pnpm build && node --test",
153
+ "test:stories": "pnpm build && vitest run --project=storybook",
146
154
  "demo": "pnpm build && node examples/build.mjs",
147
155
  "format": "biome check --write .",
148
156
  "format:check": "biome format .",
@@ -158,6 +166,7 @@
158
166
  "test:package": "node scripts/smoke-package.mjs",
159
167
  "build": "node scripts/build.mjs",
160
168
  "lint": "biome check .",
169
+ "audit": "pnpm audit --audit-level high",
161
170
  "build:pages": "pnpm build && node scripts/build-pages.mjs"
162
171
  }
163
172
  }