@kahwee/sf-map-svg 5.1.2 → 5.1.3
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 +4 -0
- package/dist/src/configuration-controller.d.ts +21 -0
- package/dist/src/configuration-controller.js +43 -0
- package/dist/src/explorer-core.d.ts +2 -1
- package/dist/src/explorer-core.js +114 -100
- package/dist/src/map.js +12 -35
- package/llms-full.txt +15 -11
- package/llms.txt +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 5.1.3 — 2026-11-05
|
|
8
|
+
|
|
9
|
+
- Give configuration snapshots and revisions a dedicated internal owner. Split presentation preparation from a guarded, single-use commit, rejecting stale or disposed updates before mutating presentation. Preserve public configuration and reset behavior.
|
|
10
|
+
|
|
7
11
|
## 5.1.2 — 2026-11-05
|
|
8
12
|
|
|
9
13
|
- Separate marker selection, activation and focus recovery into an internal controller; separate label candidate caching, screen-space collision layout and metrics invalidation into a label coordinator. Preserve the public API and shared animation frame scheduling.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { prepareConfiguration } from './configuration.js';
|
|
2
|
+
import type { MapConfiguration } from './controller-types.js';
|
|
3
|
+
import type { NeighborhoodExplorerElement } from './types.js';
|
|
4
|
+
/** A prepared engine update may commit once, only while all revisions remain current. */
|
|
5
|
+
export interface PreparedPresentation {
|
|
6
|
+
commit(onCommit: () => void, isCurrent: () => boolean): boolean;
|
|
7
|
+
}
|
|
8
|
+
export interface ConfigurationTarget extends Pick<NeighborhoodExplorerElement, 'setFeatures' | 'setLayers' | 'setControls'> {
|
|
9
|
+
preparePresentation(patch: MapConfiguration): PreparedPresentation | undefined;
|
|
10
|
+
}
|
|
11
|
+
/** Own the public configuration snapshot and invalidate stale updates before DOM mutation. */
|
|
12
|
+
export declare function createConfigurationController(initial: ReturnType<typeof prepareConfiguration>, element: ConfigurationTarget, isDestroyed: () => boolean): {
|
|
13
|
+
configure: (patch: MapConfiguration) => boolean;
|
|
14
|
+
readonly current: {
|
|
15
|
+
features: import("./types.js").MapFeatures;
|
|
16
|
+
layers: import("./types.js").InteractiveLayers;
|
|
17
|
+
controls: import("./controller-types.js").MapControls;
|
|
18
|
+
appearance: import("./controller-types.js").MapAppearance;
|
|
19
|
+
};
|
|
20
|
+
invalidate(): void;
|
|
21
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { prepareConfiguration } from './configuration.js';
|
|
2
|
+
import { controlKeys, layerKeys } from './features.js';
|
|
3
|
+
/** Own the public configuration snapshot and invalidate stale updates before DOM mutation. */
|
|
4
|
+
export function createConfigurationController(initial, element, isDestroyed) {
|
|
5
|
+
let config = initial;
|
|
6
|
+
let configurationRevision = 0;
|
|
7
|
+
function configure(patch) {
|
|
8
|
+
const revision = ++configurationRevision;
|
|
9
|
+
const next = prepareConfiguration(config, patch);
|
|
10
|
+
const hasFeatures = 'features' in patch, hasLayers = 'layers' in patch, hasControls = 'controls' in patch;
|
|
11
|
+
const layers = Object.fromEntries(layerKeys.map((key) => [key, next.layers[key]]));
|
|
12
|
+
const controls = Object.fromEntries(controlKeys.map((key) => [key, next.controls[key]]));
|
|
13
|
+
const features = patch.features === undefined
|
|
14
|
+
? next.features
|
|
15
|
+
: Object.fromEntries(Object.keys(patch.features).map((key) => [
|
|
16
|
+
key,
|
|
17
|
+
next.features[key],
|
|
18
|
+
]));
|
|
19
|
+
const presentation = element.preparePresentation({
|
|
20
|
+
...patch,
|
|
21
|
+
...('appearance' in patch ? { appearance: next.appearance } : {}),
|
|
22
|
+
});
|
|
23
|
+
const committed = presentation?.commit(() => {
|
|
24
|
+
config = next;
|
|
25
|
+
if (hasFeatures)
|
|
26
|
+
element.setFeatures(features);
|
|
27
|
+
if (hasLayers)
|
|
28
|
+
element.setLayers(layers);
|
|
29
|
+
if (hasControls)
|
|
30
|
+
element.setControls(controls);
|
|
31
|
+
}, () => !isDestroyed() && revision === configurationRevision);
|
|
32
|
+
return committed === true && !isDestroyed() && revision === configurationRevision;
|
|
33
|
+
}
|
|
34
|
+
return {
|
|
35
|
+
configure,
|
|
36
|
+
get current() {
|
|
37
|
+
return config;
|
|
38
|
+
},
|
|
39
|
+
invalidate() {
|
|
40
|
+
configurationRevision++;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { NeighborhoodSource } from '../data/types.js';
|
|
2
|
+
import type { PreparedPresentation } from './configuration-controller.js';
|
|
2
3
|
import type { MapConfiguration } from './controller-types.js';
|
|
3
4
|
import type { InteractiveSFMapData } from './explorer-data.js';
|
|
4
5
|
import type { createMapShell } from './explorer-shell.js';
|
|
@@ -7,7 +8,7 @@ import type { DistrictYear, ExplorerMode, NeighborhoodExplorerElement, Neighborh
|
|
|
7
8
|
export type { ExplorerMode, NeighborhoodExplorerElement, NeighborhoodExplorerOptions, } from './types.js';
|
|
8
9
|
/** Private bridge between the renderer and its public lifecycle controller. */
|
|
9
10
|
type MapEngineElement = NeighborhoodExplorerElement & {
|
|
10
|
-
|
|
11
|
+
preparePresentation(patch: MapConfiguration): PreparedPresentation | undefined;
|
|
11
12
|
getMapState(): {
|
|
12
13
|
mode: ExplorerMode;
|
|
13
14
|
source: NeighborhoodSource;
|
|
@@ -860,7 +860,7 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
|
|
|
860
860
|
root.dispatchEvent(new CustomEvent('mapresize', { bubbles: true }));
|
|
861
861
|
};
|
|
862
862
|
let presentationRevision = 0;
|
|
863
|
-
function
|
|
863
|
+
function preparePresentation(patch) {
|
|
864
864
|
const revision = ++presentationRevision;
|
|
865
865
|
const nextYear = patch.year ?? year;
|
|
866
866
|
const nextMode = patch.mode ?? mode;
|
|
@@ -880,11 +880,8 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
|
|
|
880
880
|
const prepared = appearance || nextYear !== year
|
|
881
881
|
? prepareDistrictStyles(data.map.districts?.[nextYear] ?? [], nextStyle)
|
|
882
882
|
: districts.styles;
|
|
883
|
-
if (destroyed ||
|
|
884
|
-
|
|
885
|
-
revision !== presentationRevision ||
|
|
886
|
-
districtVersion !== districts.revision)
|
|
887
|
-
return false;
|
|
883
|
+
if (destroyed || revision !== presentationRevision || districtVersion !== districts.revision)
|
|
884
|
+
return undefined;
|
|
888
885
|
if (nextYear !== year) {
|
|
889
886
|
// Validate target paths and label coordinates before any presentation group commits.
|
|
890
887
|
getLayerPathsWithData({ year: nextYear }, data.map, false);
|
|
@@ -892,103 +889,120 @@ export function createNeighborhoodExplorerCore(options = {}, data, ui) {
|
|
|
892
889
|
for (const point of feature.properties.labelPoints)
|
|
893
890
|
map.project(point);
|
|
894
891
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
892
|
+
let consumed = false;
|
|
893
|
+
return {
|
|
894
|
+
commit(onCommit, isCurrent) {
|
|
895
|
+
if (consumed ||
|
|
896
|
+
destroyed ||
|
|
897
|
+
!isCurrent() ||
|
|
898
|
+
revision !== presentationRevision ||
|
|
899
|
+
districtVersion !== districts.revision)
|
|
900
|
+
return false;
|
|
901
|
+
consumed = true;
|
|
902
|
+
onCommit();
|
|
903
|
+
if (destroyed ||
|
|
904
|
+
!isCurrent() ||
|
|
905
|
+
revision !== presentationRevision ||
|
|
906
|
+
districtVersion !== districts.revision)
|
|
907
|
+
return false;
|
|
908
|
+
if (appearance) {
|
|
909
|
+
districts.invalidate();
|
|
910
|
+
const next = copyAppearance(appearance);
|
|
911
|
+
theme = next.theme ?? 'transit';
|
|
912
|
+
colors = next.colors ?? {};
|
|
913
|
+
labelStyle = next.labelStyle ?? {};
|
|
914
|
+
areaStyle = next.areaStyle ?? {};
|
|
915
|
+
styleOptions = next.style ?? {};
|
|
916
|
+
minLabelSize = next.labelSize?.min ?? 11;
|
|
917
|
+
maxLabelSize = next.labelSize?.max ?? 12;
|
|
918
|
+
markerRadius = next.markerRadius ?? 6;
|
|
919
|
+
markerHitSize = next.markerHitSize ?? 44;
|
|
920
|
+
markerColor = next.markerColor ?? colors.marker ?? '#245b61';
|
|
921
|
+
selectedMarkerColor = next.selectedMarkerColor ?? colors.selected ?? '#f04f32';
|
|
922
|
+
districtTransition.cancel();
|
|
923
|
+
const fills = districtLayers.find((layer) => layer.dataset.layer === 'district-fills');
|
|
924
|
+
const fade = layerFades.duration();
|
|
925
|
+
const stylesChanged = nextStyle !== districts.style ||
|
|
926
|
+
[...prepared].some(([id, value]) => ['fill', 'stroke', 'opacity'].some((key) => value[key] !== districts.styles.get(id)?.[key]));
|
|
927
|
+
if (fills && fade && enabled('districtFills') && nextYear === year && stylesChanged)
|
|
928
|
+
districtTransition.fade(fills, fade);
|
|
929
|
+
districts.commitStyles(nextStyle, prepared);
|
|
930
|
+
const palette = resolveMapColors(theme, colors);
|
|
931
|
+
svg.querySelector('rect')?.setAttribute('fill', palette.water);
|
|
932
|
+
svg.querySelector('[data-layer="coast"]')?.setAttribute('fill', palette.land);
|
|
933
|
+
svg.querySelector('[data-layer="coastline"]')?.setAttribute('stroke', palette.district);
|
|
934
|
+
for (const [layer, color] of [
|
|
935
|
+
['landmarks', palette.park],
|
|
936
|
+
['highways', palette.highway],
|
|
937
|
+
['key-roads', palette.road],
|
|
938
|
+
])
|
|
939
|
+
for (const node of svg.querySelectorAll(`[data-layer="${layer}"] path`)) {
|
|
940
|
+
node.setAttribute(layer === 'landmarks' ? 'fill' : 'stroke', color);
|
|
941
|
+
if (layer === 'highways')
|
|
942
|
+
node.setAttribute('stroke-width', theme === 'transit' ? '2' : '1.4');
|
|
943
|
+
}
|
|
944
|
+
for (const [token, fallback, name] of [
|
|
945
|
+
['ink', '#18364f', 'ink'],
|
|
946
|
+
['surface', '#fff', 'surface'],
|
|
947
|
+
['accent', '#163d61', 'accent'],
|
|
948
|
+
['border', '#cedae3', 'border'],
|
|
949
|
+
['focus', '#1676b8', 'focus'],
|
|
950
|
+
['controlGap', '6px', 'control-gap'],
|
|
951
|
+
['font', 'system-ui,sans-serif', 'font'],
|
|
952
|
+
])
|
|
953
|
+
root.style.setProperty(`--sf-map-${name}`, styleOptions[token] ?? fallback);
|
|
954
|
+
labelLayer.setAttribute('font-family', labelStyle.fontFamily ?? styleOptions.font ?? 'system-ui,sans-serif');
|
|
955
|
+
labelCoordinator.invalidateMetrics();
|
|
956
|
+
for (const station of stationItems)
|
|
957
|
+
station.node.setAttribute('stroke', colors.bart ?? '#0073ae');
|
|
958
|
+
for (const kind of ['bart', 'park', 'highway', 'road']) {
|
|
959
|
+
const symbol = legend.querySelector(`.sf-explorer-legend-${kind}`);
|
|
960
|
+
if (symbol) {
|
|
961
|
+
symbol.style.removeProperty(kind === 'bart' ? 'border-color' : 'background-color');
|
|
962
|
+
if (colors[kind])
|
|
963
|
+
symbol.style.setProperty(kind === 'bart' ? 'border-color' : 'background-color', colors[kind]);
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
for (const item of neighborhoods.items) {
|
|
967
|
+
const active = item.feature === neighborhoods.selected;
|
|
968
|
+
item.node.setAttribute('fill', active ? (areaStyle.selectedFill ?? '#408dbe') : 'transparent');
|
|
969
|
+
item.node.setAttribute('stroke', active ? (areaStyle.selectedStroke ?? '#176ba2') : (colors.neighborhood ?? '#9caebc'));
|
|
970
|
+
}
|
|
971
|
+
for (const item of markerCatalog.items) {
|
|
972
|
+
const visual = item.visual;
|
|
973
|
+
if (!visual)
|
|
974
|
+
continue;
|
|
975
|
+
visual.dot.setAttribute('fill', item.marker.id === markers.selected
|
|
976
|
+
? selectedMarkerColor
|
|
977
|
+
: (item.marker.color ?? markerColor));
|
|
978
|
+
visual.ring.setAttribute('stroke', features.selectedMarkerRing
|
|
979
|
+
? (features.selectedMarkerRing.color ?? selectedMarkerColor)
|
|
980
|
+
: selectedMarkerColor);
|
|
981
|
+
}
|
|
982
|
+
invalidateClusters();
|
|
983
|
+
updateDistrictAppearance();
|
|
952
984
|
}
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
}
|
|
970
|
-
|
|
971
|
-
updateDistrictAppearance();
|
|
972
|
-
}
|
|
973
|
-
if (nextYear !== year)
|
|
974
|
-
setDistrictYear(nextYear, {}, prepared);
|
|
975
|
-
if (destroyed || revision !== presentationRevision || !isCurrent())
|
|
976
|
-
return false;
|
|
977
|
-
commitSource?.();
|
|
978
|
-
if (destroyed || revision !== presentationRevision || !isCurrent())
|
|
979
|
-
return false;
|
|
980
|
-
if (commitSource && appearance)
|
|
981
|
-
for (const item of neighborhoods.items)
|
|
982
|
-
item.node.setAttribute('stroke', colors.neighborhood ?? '#9caebc');
|
|
983
|
-
if (patch.mode !== undefined)
|
|
984
|
-
setMode(patch.mode, false);
|
|
985
|
-
if (patch.labels !== undefined)
|
|
986
|
-
setLabels(patch.labels);
|
|
987
|
-
scheduleLabels();
|
|
988
|
-
return true;
|
|
985
|
+
if (nextYear !== year)
|
|
986
|
+
setDistrictYear(nextYear, {}, prepared);
|
|
987
|
+
if (destroyed || revision !== presentationRevision || !isCurrent())
|
|
988
|
+
return false;
|
|
989
|
+
commitSource?.();
|
|
990
|
+
if (destroyed || revision !== presentationRevision || !isCurrent())
|
|
991
|
+
return false;
|
|
992
|
+
if (commitSource && appearance)
|
|
993
|
+
for (const item of neighborhoods.items)
|
|
994
|
+
item.node.setAttribute('stroke', colors.neighborhood ?? '#9caebc');
|
|
995
|
+
if (patch.mode !== undefined)
|
|
996
|
+
setMode(patch.mode, false);
|
|
997
|
+
if (patch.labels !== undefined)
|
|
998
|
+
setLabels(patch.labels);
|
|
999
|
+
scheduleLabels();
|
|
1000
|
+
return true;
|
|
1001
|
+
},
|
|
1002
|
+
};
|
|
989
1003
|
}
|
|
990
1004
|
const explorer = Object.assign(root, {
|
|
991
|
-
|
|
1005
|
+
preparePresentation,
|
|
992
1006
|
getMapState: () => ({ mode, source: neighborhoods.source, year, labels }),
|
|
993
1007
|
selectNeighborhood,
|
|
994
1008
|
getSelection,
|
package/dist/src/map.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import { copyAppearance } from './appearance.js';
|
|
2
2
|
import { validateCameraOptions } from './camera.js';
|
|
3
3
|
import { configurationPatch, expandMapOptions, prepareConfiguration } from './configuration.js';
|
|
4
|
+
import { createConfigurationController } from './configuration-controller.js';
|
|
4
5
|
import { createNeighborhoodExplorerCore } from './explorer-core.js';
|
|
5
6
|
import { createMapShell } from './explorer-shell.js';
|
|
6
|
-
import {
|
|
7
|
+
import { layerKeys } from './features.js';
|
|
7
8
|
import { assertOptions } from './validation.js';
|
|
8
9
|
/** No geography is imported. Supply a preset or your own immutable data. */
|
|
9
10
|
export function createMap(data, options = {}) {
|
|
10
11
|
const expanded = expandMapOptions(options);
|
|
11
|
-
|
|
12
|
+
const initialConfiguration = prepareConfiguration({ features: {}, layers: {}, controls: {}, appearance: {} }, {
|
|
12
13
|
features: options.features,
|
|
13
14
|
layers: options.layers,
|
|
14
15
|
controls: options.controls,
|
|
@@ -21,7 +22,8 @@ export function createMap(data, options = {}) {
|
|
|
21
22
|
onOverlayActivate: () => { },
|
|
22
23
|
}, data, { createShell: createMapShell });
|
|
23
24
|
const subscriptions = new Set();
|
|
24
|
-
|
|
25
|
+
const configuration = createConfigurationController(initialConfiguration, element, () => destroyed);
|
|
26
|
+
const configure = configuration.configure;
|
|
25
27
|
const sourceList = ['realtor', 'sf-find', 'analysis'].filter((source) => !!data.neighborhoods[source]);
|
|
26
28
|
const yearList = [2002, 2012, 2022].filter((year) => !!data.map.districts?.[year] && !!data.districts?.[year]);
|
|
27
29
|
const capabilities = () => ({
|
|
@@ -42,11 +44,11 @@ export function createMap(data, options = {}) {
|
|
|
42
44
|
const snapshot = () => ({
|
|
43
45
|
...element.getMapState(),
|
|
44
46
|
...structuredClone({
|
|
45
|
-
features:
|
|
46
|
-
layers:
|
|
47
|
-
controls:
|
|
47
|
+
features: configuration.current.features,
|
|
48
|
+
layers: configuration.current.layers,
|
|
49
|
+
controls: configuration.current.controls,
|
|
48
50
|
}),
|
|
49
|
-
appearance: copyAppearance(
|
|
51
|
+
appearance: copyAppearance(configuration.current.appearance),
|
|
50
52
|
});
|
|
51
53
|
const resolved = () => {
|
|
52
54
|
const state = element.getMapState();
|
|
@@ -58,7 +60,7 @@ export function createMap(data, options = {}) {
|
|
|
58
60
|
key,
|
|
59
61
|
supplied[key] &&
|
|
60
62
|
(!key.endsWith('Labels') || state.labels) &&
|
|
61
|
-
(
|
|
63
|
+
(configuration.current.layers[key] ??
|
|
62
64
|
(key.startsWith('district')
|
|
63
65
|
? state.mode === 'districts'
|
|
64
66
|
: key.startsWith('neighborhood')
|
|
@@ -119,31 +121,6 @@ export function createMap(data, options = {}) {
|
|
|
119
121
|
element.fitGeometry(geometry, padding, motion);
|
|
120
122
|
}),
|
|
121
123
|
});
|
|
122
|
-
function configure(patch) {
|
|
123
|
-
const revision = ++configurationRevision;
|
|
124
|
-
const next = prepareConfiguration(config, patch);
|
|
125
|
-
const hasFeatures = 'features' in patch, hasLayers = 'layers' in patch, hasControls = 'controls' in patch;
|
|
126
|
-
const layers = Object.fromEntries(layerKeys.map((key) => [key, next.layers[key]]));
|
|
127
|
-
const controls = Object.fromEntries(controlKeys.map((key) => [key, next.controls[key]]));
|
|
128
|
-
const features = patch.features === undefined
|
|
129
|
-
? next.features
|
|
130
|
-
: Object.fromEntries(Object.keys(patch.features).map((key) => [
|
|
131
|
-
key,
|
|
132
|
-
next.features[key],
|
|
133
|
-
]));
|
|
134
|
-
let committed = false;
|
|
135
|
-
element.applyPresentation({ ...patch, ...('appearance' in patch ? { appearance: next.appearance } : {}) }, () => {
|
|
136
|
-
committed = true;
|
|
137
|
-
config = next;
|
|
138
|
-
if (hasFeatures)
|
|
139
|
-
element.setFeatures(features);
|
|
140
|
-
if (hasLayers)
|
|
141
|
-
element.setLayers(layers);
|
|
142
|
-
if (hasControls)
|
|
143
|
-
element.setControls(controls);
|
|
144
|
-
}, () => !destroyed && revision === configurationRevision);
|
|
145
|
-
return committed && !destroyed && revision === configurationRevision;
|
|
146
|
-
}
|
|
147
124
|
return Object.freeze({
|
|
148
125
|
element,
|
|
149
126
|
overlayElement: element.overlayElement,
|
|
@@ -152,7 +129,7 @@ export function createMap(data, options = {}) {
|
|
|
152
129
|
return destroyed;
|
|
153
130
|
},
|
|
154
131
|
configure: use((patch) => {
|
|
155
|
-
configure(configurationPatch(patch,
|
|
132
|
+
configure(configurationPatch(patch, configuration.current));
|
|
156
133
|
}),
|
|
157
134
|
resetConfiguration: use((groups = ['features', 'layers', 'controls', 'appearance']) => {
|
|
158
135
|
if (!Array.isArray(groups) ||
|
|
@@ -242,7 +219,7 @@ export function createMap(data, options = {}) {
|
|
|
242
219
|
selectDistrict: use(element.selectDistrict),
|
|
243
220
|
getSelectedDistrict: use(element.getSelectedDistrict),
|
|
244
221
|
setDistrictYear: use((...args) => {
|
|
245
|
-
|
|
222
|
+
configuration.invalidate();
|
|
246
223
|
element.setDistrictYear(...args);
|
|
247
224
|
}),
|
|
248
225
|
setDistrictStyle: use((style) => {
|
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 5.1.
|
|
5
|
+
@kahwee/sf-map-svg 5.1.3 · 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 5.1.
|
|
11
|
+
<!-- Generated from README.md; @kahwee/sf-map-svg 5.1.3 · local working tree. -->
|
|
12
12
|
|
|
13
13
|
# SF Map SVG
|
|
14
14
|
|
|
@@ -267,7 +267,7 @@ for raw samples, output-equivalence checks, and reproducible commands.
|
|
|
267
267
|
|
|
268
268
|
---
|
|
269
269
|
|
|
270
|
-
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 5.1.
|
|
270
|
+
<!-- Generated from docs/developer-guide.md; @kahwee/sf-map-svg 5.1.3 · local working tree. -->
|
|
271
271
|
|
|
272
272
|
# Developer guide
|
|
273
273
|
|
|
@@ -433,7 +433,7 @@ See [API options](https://github.com/kahwee/sf-map-svg/blob/main/docs/API.md), [
|
|
|
433
433
|
|
|
434
434
|
---
|
|
435
435
|
|
|
436
|
-
<!-- Generated from docs/API.md; @kahwee/sf-map-svg 5.1.
|
|
436
|
+
<!-- Generated from docs/API.md; @kahwee/sf-map-svg 5.1.3 · local working tree. -->
|
|
437
437
|
|
|
438
438
|
# Map options and lifecycle
|
|
439
439
|
|
|
@@ -619,7 +619,7 @@ Call `binding.setMarkers(next)` after `map.setMarkers(next)` when the catalog ch
|
|
|
619
619
|
|
|
620
620
|
---
|
|
621
621
|
|
|
622
|
-
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 5.1.
|
|
622
|
+
<!-- Generated from docs/EXAMPLES.md; @kahwee/sf-map-svg 5.1.3 · local working tree. -->
|
|
623
623
|
|
|
624
624
|
# Examples
|
|
625
625
|
|
|
@@ -760,7 +760,7 @@ See [query semantics and limitations](https://github.com/kahwee/sf-map-svg/blob/
|
|
|
760
760
|
|
|
761
761
|
---
|
|
762
762
|
|
|
763
|
-
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 5.1.
|
|
763
|
+
<!-- Generated from docs/consumer-integration.md; @kahwee/sf-map-svg 5.1.3 · local working tree. -->
|
|
764
764
|
|
|
765
765
|
# Embedding and progressive enhancement
|
|
766
766
|
|
|
@@ -938,7 +938,7 @@ keyboard, focus, and dismissal behavior. See [coordinates and units](https://git
|
|
|
938
938
|
|
|
939
939
|
---
|
|
940
940
|
|
|
941
|
-
<!-- Generated from docs/migration-v5.md; @kahwee/sf-map-svg 5.1.
|
|
941
|
+
<!-- Generated from docs/migration-v5.md; @kahwee/sf-map-svg 5.1.3 · local working tree. -->
|
|
942
942
|
|
|
943
943
|
# Migrating to v5
|
|
944
944
|
|
|
@@ -1043,7 +1043,7 @@ collections. Shared edges can match more than one feature. See the
|
|
|
1043
1043
|
|
|
1044
1044
|
---
|
|
1045
1045
|
|
|
1046
|
-
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 5.1.
|
|
1046
|
+
<!-- Generated from docs/migration-v3.md; @kahwee/sf-map-svg 5.1.3 · local working tree. -->
|
|
1047
1047
|
|
|
1048
1048
|
# Migrate to version 3
|
|
1049
1049
|
|
|
@@ -1084,7 +1084,7 @@ District styles are now prepared once per district on construction and style/yea
|
|
|
1084
1084
|
|
|
1085
1085
|
---
|
|
1086
1086
|
|
|
1087
|
-
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg 5.1.
|
|
1087
|
+
<!-- Generated from SOURCES.md; @kahwee/sf-map-svg 5.1.3 · local working tree. -->
|
|
1088
1088
|
|
|
1089
1089
|
# Geometry and provenance
|
|
1090
1090
|
|
|
@@ -1321,7 +1321,7 @@ Report-history points preserve only actual validated source counts and their dis
|
|
|
1321
1321
|
|
|
1322
1322
|
---
|
|
1323
1323
|
|
|
1324
|
-
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 5.1.
|
|
1324
|
+
<!-- Generated from CHANGELOG.md; @kahwee/sf-map-svg 5.1.3 · local working tree. -->
|
|
1325
1325
|
|
|
1326
1326
|
# Changelog
|
|
1327
1327
|
|
|
@@ -1329,6 +1329,10 @@ User-visible changes are recorded here. Unreleased entries describe changes on `
|
|
|
1329
1329
|
|
|
1330
1330
|
## Unreleased
|
|
1331
1331
|
|
|
1332
|
+
## 5.1.3 — 2026-11-05
|
|
1333
|
+
|
|
1334
|
+
- Give configuration snapshots and revisions a dedicated internal owner. Split presentation preparation from a guarded, single-use commit, rejecting stale or disposed updates before mutating presentation. Preserve public configuration and reset behavior.
|
|
1335
|
+
|
|
1332
1336
|
## 5.1.2 — 2026-11-05
|
|
1333
1337
|
|
|
1334
1338
|
- Separate marker selection, activation and focus recovery into an internal controller; separate label candidate caching, screen-space collision layout and metrics invalidation into a label coordinator. Preserve the public API and shared animation frame scheduling.
|
|
@@ -1721,7 +1725,7 @@ consistent home, and makes animation and subscription ownership predictable.
|
|
|
1721
1725
|
|
|
1722
1726
|
# Published type contracts
|
|
1723
1727
|
|
|
1724
|
-
@kahwee/sf-map-svg 5.1.
|
|
1728
|
+
@kahwee/sf-map-svg 5.1.3 · 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.
|
|
1725
1729
|
|
|
1726
1730
|
| Public import | Declaration |
|
|
1727
1731
|
| --- | --- |
|
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 5.1.
|
|
5
|
+
@kahwee/sf-map-svg 5.1.3 · 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