@kahwee/sf-map-svg 4.0.1 → 4.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +23 -1
- package/README.md +7 -11
- package/dist/data/guide/coast.json +1 -1
- package/dist/data/guide/landmarks.json +1 -1
- package/dist/data/guide/neighborhoods-realtor.json +1 -1
- package/dist/data/stations.d.ts +2 -2
- package/dist/data/types.d.ts +8 -1
- package/dist/src/api.d.ts +1 -1
- package/dist/src/api.js +1 -1
- package/dist/src/configuration.js +18 -2
- package/dist/src/district-morph.d.ts +17 -0
- package/dist/src/district-morph.js +148 -0
- package/dist/src/explorer-core.js +66 -16
- package/dist/src/features.d.ts +6 -0
- package/dist/src/features.js +11 -1
- package/dist/src/layer-transition.d.ts +18 -0
- package/dist/src/layer-transition.js +102 -0
- package/dist/src/layers.d.ts +9 -7
- package/dist/src/layers.js +28 -18
- package/dist/src/map-core.js +7 -4
- package/dist/src/overlays.d.ts +3 -4
- package/dist/src/overlays.js +5 -1
- package/dist/src/static-animation.d.ts +13 -0
- package/dist/src/static-animation.js +36 -0
- package/dist/src/transit.js +2 -1
- package/dist/src/types.d.ts +25 -0
- package/dist/src/validation.js +2 -0
- package/docs/API.md +33 -0
- package/docs/consumer-integration.md +1 -1
- package/docs/developer-guide.md +137 -0
- package/docs/guide-bundle-report.md +4 -4
- package/llms-full.txt +1844 -0
- package/llms.txt +25 -0
- package/package.json +10 -4
package/dist/src/layers.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import type { BartStation, KeyRoad, Landmark } from './overlays.js';
|
|
|
5
5
|
import type { MapMarker, SFMapOptions } from './types.js';
|
|
6
6
|
interface LayerContext {
|
|
7
7
|
labels: boolean;
|
|
8
|
+
/** Animated static maps index items for staggering and measure lines for drawing. */
|
|
9
|
+
animated?: boolean;
|
|
8
10
|
theme: NonNullable<SFMapOptions['theme']>;
|
|
9
11
|
idPrefix: string;
|
|
10
12
|
colors: Required<NonNullable<SFMapOptions['colors']>>;
|
|
@@ -18,21 +20,21 @@ interface DistrictPath {
|
|
|
18
20
|
stroke?: string;
|
|
19
21
|
opacity?: number;
|
|
20
22
|
}
|
|
21
|
-
export declare function districtFills(districts: readonly DistrictPath[], { idPrefix }: LayerContext): string;
|
|
22
|
-
export declare function districtLines(districts: readonly DistrictPath[], { idPrefix, colors }: LayerContext): string;
|
|
23
|
+
export declare function districtFills(districts: readonly DistrictPath[], { idPrefix, animated }: LayerContext): string;
|
|
24
|
+
export declare function districtLines(districts: readonly DistrictPath[], { idPrefix, colors, animated }: LayerContext): string;
|
|
23
25
|
export declare function landmarks(items: readonly Landmark[], { idPrefix, path, colors }: LayerContext): string;
|
|
24
26
|
export declare function highways(items: readonly {
|
|
25
27
|
route: string;
|
|
26
28
|
geometry: Geometry;
|
|
27
|
-
}[], { path, colors, theme }: LayerContext): string;
|
|
29
|
+
}[], { path, colors, theme, animated }: LayerContext): string;
|
|
28
30
|
export declare function neighborhoods(items: readonly {
|
|
29
31
|
name: string;
|
|
30
32
|
geometry: Geometry;
|
|
31
|
-
}[], { idPrefix, path, colors }: LayerContext): string;
|
|
33
|
+
}[], { idPrefix, path, colors, animated }: LayerContext): string;
|
|
32
34
|
export declare function districtLabels(districts: readonly DistrictRow[], { project, colors }: LayerContext): string;
|
|
33
35
|
export declare function landmarkLabels(items: readonly Landmark[], { project, colors }: LayerContext): string;
|
|
34
|
-
export declare function bartStations(items: readonly BartStation[], { project, colors, labels }: LayerContext): string;
|
|
35
|
-
export declare function markers(items: readonly MapMarker[], { project, colors }: LayerContext): string;
|
|
36
|
-
export declare function keyRoads(items: readonly KeyRoad[], { idPrefix, path, colors }: LayerContext): string;
|
|
36
|
+
export declare function bartStations(items: readonly BartStation[], { project, colors, labels, animated }: LayerContext): string;
|
|
37
|
+
export declare function markers(items: readonly MapMarker[], { project, colors, animated }: LayerContext): string;
|
|
38
|
+
export declare function keyRoads(items: readonly KeyRoad[], { idPrefix, path, colors, animated }: LayerContext): string;
|
|
37
39
|
export declare function keyRoadLabels(items: readonly KeyRoad[], { project }: LayerContext): string;
|
|
38
40
|
export {};
|
package/dist/src/layers.js
CHANGED
|
@@ -1,29 +1,39 @@
|
|
|
1
1
|
import { escapeXml, number, overlayLabel, stroke } from './svg.js';
|
|
2
|
-
|
|
3
|
-
|
|
2
|
+
/** Custom JavaScript/JSON rows must not introduce markup or CSS through numeric IDs. */
|
|
3
|
+
function districtId(id) {
|
|
4
|
+
if (!Number.isFinite(id))
|
|
5
|
+
throw new TypeError('District IDs must be finite numbers.');
|
|
6
|
+
return id;
|
|
4
7
|
}
|
|
5
|
-
|
|
6
|
-
|
|
8
|
+
/** Stagger index for animated static maps. */
|
|
9
|
+
const order = (animated, index) => animated ? ` style="--i:${index}"` : '';
|
|
10
|
+
/** Normalized length so one dash can draw a line on. */
|
|
11
|
+
const drawable = (animated) => (animated ? ' pathLength="1"' : '');
|
|
12
|
+
export function districtFills(districts, { idPrefix, animated }) {
|
|
13
|
+
return `<g data-layer="district-fills" clip-path="url(#${idPrefix}-coast)">${districts.map((d) => `<path data-district="${districtId(d.id)}" d="${d.path}" fill="${escapeXml(d.color)}"${d.opacity === undefined ? '' : ` fill-opacity="${d.opacity}"`} fill-rule="evenodd"${order(animated, districtId(d.id))}/>`).join('')}</g>`;
|
|
14
|
+
}
|
|
15
|
+
export function districtLines(districts, { idPrefix, colors, animated }) {
|
|
16
|
+
return `<g data-layer="district-lines" clip-path="url(#${idPrefix}-coast)">${districts.map((d) => `<path data-district="${districtId(d.id)}" d="${d.path}" ${stroke(d.stroke ?? colors.district, 1.1)}${d.opacity === undefined ? '' : ` stroke-opacity="${d.opacity}"`}${drawable(animated)}${order(animated, districtId(d.id))}/>`).join('')}</g>`;
|
|
7
17
|
}
|
|
8
18
|
export function landmarks(items, { idPrefix, path, colors }) {
|
|
9
19
|
return `<g data-layer="landmarks" clip-path="url(#${idPrefix}-coast)">${items.map((item) => `<path data-landmark="${escapeXml(item.id)}" d="${path(item.geometry)}" fill="${escapeXml(colors.park)}" fill-rule="evenodd"><title>${escapeXml(item.name)}</title></path>`).join('')}</g>`;
|
|
10
20
|
}
|
|
11
|
-
export function highways(items, { path, colors, theme }) {
|
|
12
|
-
return `<g data-layer="highways">${items.map((road) => `<path data-route="${escapeXml(road.route)}" d="${path(road.geometry)}" ${stroke(colors.highway, theme === 'transit' ? 2 : 1.4)}/>`).join('')}</g>`;
|
|
21
|
+
export function highways(items, { path, colors, theme, animated }) {
|
|
22
|
+
return `<g data-layer="highways">${items.map((road) => `<path data-route="${escapeXml(road.route)}" d="${path(road.geometry)}" ${stroke(colors.highway, theme === 'transit' ? 2 : 1.4)}${drawable(animated)}/>`).join('')}</g>`;
|
|
13
23
|
}
|
|
14
|
-
export function neighborhoods(items, { idPrefix, path, colors }) {
|
|
15
|
-
return `<g data-layer="neighborhood-lines" clip-path="url(#${idPrefix}-coast)">${items.map((n) => `<path data-neighborhood="${escapeXml(n.name)}" d="${path(n.geometry)}" ${stroke(colors.neighborhood, 0.65)} stroke-dasharray="2 2"><title>${escapeXml(n.name)}</title></path>`).join('')}</g>`;
|
|
24
|
+
export function neighborhoods(items, { idPrefix, path, colors, animated }) {
|
|
25
|
+
return `<g data-layer="neighborhood-lines" clip-path="url(#${idPrefix}-coast)">${items.map((n, index) => `<path data-neighborhood="${escapeXml(n.name)}" d="${path(n.geometry)}" ${stroke(colors.neighborhood, 0.65)} stroke-dasharray="2 2"${order(animated, index)}><title>${escapeXml(n.name)}</title></path>`).join('')}</g>`;
|
|
16
26
|
}
|
|
17
27
|
export function districtLabels(districts, { project, colors }) {
|
|
18
28
|
// Primary badges first, then island badges, matching the original layer order.
|
|
19
29
|
const labels = [
|
|
20
|
-
...districts.map((d) => ({ id: d.id, label: d.labelPoints[0] })),
|
|
30
|
+
...districts.map((d) => ({ id: d.id, label: d.labelPoints[0] ?? d.label })),
|
|
21
31
|
...districts.flatMap((d) => d.labelPoints.slice(1).map((label) => ({ id: d.id, label }))),
|
|
22
32
|
];
|
|
23
33
|
return `<g data-layer="district-labels" font-family="system-ui,sans-serif" font-size="12" font-weight="600" text-anchor="middle" fill="${escapeXml(colors.label)}">${labels
|
|
24
34
|
.map((d) => {
|
|
25
35
|
const [x, y] = project(d.label).map(number);
|
|
26
|
-
return `<g transform="translate(${x},${y})"><circle r="10" fill="#ffffff" fill-opacity=".9"/><text dy=".35em">${d.id}</text></g>`;
|
|
36
|
+
return `<g transform="translate(${x},${y})"><circle r="10" fill="#ffffff" fill-opacity=".9"/><text dy=".35em">${districtId(d.id)}</text></g>`;
|
|
27
37
|
})
|
|
28
38
|
.join('')}</g>`;
|
|
29
39
|
}
|
|
@@ -35,29 +45,29 @@ export function landmarkLabels(items, { project, colors }) {
|
|
|
35
45
|
})
|
|
36
46
|
.join('')}</g>`;
|
|
37
47
|
}
|
|
38
|
-
export function bartStations(items, { project, colors, labels }) {
|
|
48
|
+
export function bartStations(items, { project, colors, labels, animated }) {
|
|
39
49
|
return `<g data-layer="bart-stations">${items
|
|
40
|
-
.map((station) => {
|
|
50
|
+
.map((station, index) => {
|
|
41
51
|
const [x, y] = project(station.coordinates).map(number);
|
|
42
|
-
return `<g data-bart-station="${escapeXml(station.id)}" transform="translate(${x},${y})"><title>${escapeXml(station.name)} BART station</title><circle r="5" fill="#fff" stroke="${escapeXml(colors.bart)}" stroke-width="2.5"/><circle r="1.5" fill="${escapeXml(colors.bart)}"/>${labels ? `<text x="10" y="4" ${overlayLabel} fill="${escapeXml(colors.bart)}">${escapeXml(station.name)}</text>` : ''}</g>`;
|
|
52
|
+
return `<g data-bart-station="${escapeXml(station.id)}" transform="translate(${x},${y})"${order(animated, index)}><title>${escapeXml(station.name)} BART station</title><circle r="5" fill="#fff" stroke="${escapeXml(colors.bart)}" stroke-width="2.5"/><circle r="1.5" fill="${escapeXml(colors.bart)}"/>${labels ? `<text x="10" y="4" ${overlayLabel} fill="${escapeXml(colors.bart)}">${escapeXml(station.name)}</text>` : ''}</g>`;
|
|
43
53
|
})
|
|
44
54
|
.join('')}</g>`;
|
|
45
55
|
}
|
|
46
|
-
export function markers(items, { project, colors }) {
|
|
56
|
+
export function markers(items, { project, colors, animated }) {
|
|
47
57
|
return `<g data-layer="markers">${items
|
|
48
|
-
.map((marker) => {
|
|
58
|
+
.map((marker, index) => {
|
|
49
59
|
if (marker.radius !== undefined && (!Number.isFinite(marker.radius) || marker.radius <= 0))
|
|
50
60
|
throw new RangeError('Marker radius must be positive and finite.');
|
|
51
61
|
const [x, y] = project([marker.lng, marker.lat]).map(number);
|
|
52
|
-
return `<circle data-marker-id="${escapeXml(marker.id)}" cx="${x}" cy="${y}" r="${marker.radius ?? (marker.selected ? 8 : 5)}" fill="${escapeXml(marker.color ?? (marker.selected ? colors.selected : colors.marker))}" stroke="#fff9e9" stroke-width="2" vector-effect="non-scaling-stroke"><title>${escapeXml(marker.label ?? marker.id)}</title></circle>`;
|
|
62
|
+
return `<circle data-marker-id="${escapeXml(marker.id)}" cx="${x}" cy="${y}" r="${marker.radius ?? (marker.selected ? 8 : 5)}" fill="${escapeXml(marker.color ?? (marker.selected ? colors.selected : colors.marker))}" stroke="#fff9e9" stroke-width="2" vector-effect="non-scaling-stroke"${order(animated, index)}><title>${escapeXml(marker.label ?? marker.id)}</title></circle>`;
|
|
53
63
|
})
|
|
54
64
|
.join('')}</g>`;
|
|
55
65
|
}
|
|
56
|
-
export function keyRoads(items, { idPrefix, path, colors }) {
|
|
66
|
+
export function keyRoads(items, { idPrefix, path, colors, animated }) {
|
|
57
67
|
return `<g data-layer="key-roads" clip-path="url(#${idPrefix}-coast)">${items
|
|
58
68
|
.map((road) => {
|
|
59
69
|
const level = road.level === 'secondary' ? 'secondary' : 'primary';
|
|
60
|
-
return `<path data-key-road="${escapeXml(road.id)}" data-key-road-level="${level}" d="${path(road.geometry)}" ${stroke(colors.road, 1)}><title>${escapeXml(road.name)}</title></path>`;
|
|
70
|
+
return `<path data-key-road="${escapeXml(road.id)}" data-key-road-level="${level}" d="${path(road.geometry)}" ${stroke(colors.road, 1)}${drawable(animated)}><title>${escapeXml(road.name)}</title></path>`;
|
|
61
71
|
})
|
|
62
72
|
.join('')}</g>`;
|
|
63
73
|
}
|
package/dist/src/map-core.js
CHANGED
|
@@ -2,6 +2,7 @@ import { validateMarkers, validateOverlays } from './validation.js';
|
|
|
2
2
|
import { prepareDistrictStyles } from './district-style.js';
|
|
3
3
|
import { geometryPath, positions, rawProject } from './geometry.js';
|
|
4
4
|
import * as layers from './layers.js';
|
|
5
|
+
import { normalizeStaticAnimation, staticAnimationStyle } from './static-animation.js';
|
|
5
6
|
import { escapeXml, stroke } from './svg.js';
|
|
6
7
|
export const districtYears = Object.freeze([2002, 2012, 2022]);
|
|
7
8
|
export const districtColors = Object.freeze([
|
|
@@ -115,6 +116,8 @@ export function createSFMapWithData(options, data) {
|
|
|
115
116
|
if (theme !== 'districts' && theme !== 'transit')
|
|
116
117
|
throw new TypeError('Theme must be districts or transit.');
|
|
117
118
|
const colors = { ...defaults, ...(theme === 'transit' ? transitColors : {}), ...options.colors };
|
|
119
|
+
const animation = normalizeStaticAnimation(options.animation);
|
|
120
|
+
const animated = animation !== null;
|
|
118
121
|
const geometry = getLayerPathsWithData(options, data, false);
|
|
119
122
|
const { project } = geometry;
|
|
120
123
|
const path = (geometry) => geometryPath(geometry, project);
|
|
@@ -123,7 +126,7 @@ export function createSFMapWithData(options, data) {
|
|
|
123
126
|
const landmarkData = data.landmarks ?? [];
|
|
124
127
|
const roadData = data.keyRoads ?? [];
|
|
125
128
|
const stationData = data.bartStations ?? [];
|
|
126
|
-
const context = { project, path, colors, idPrefix, theme, labels };
|
|
129
|
+
const context = { project, path, colors, idPrefix, theme, labels, animated };
|
|
127
130
|
const styles = prepareDistrictStyles(districtFills || districtLines ? districts : [], options.districtStyle);
|
|
128
131
|
const districtPaths = districtFills || districtLines
|
|
129
132
|
? districts.map((d, index) => {
|
|
@@ -138,7 +141,7 @@ export function createSFMapWithData(options, data) {
|
|
|
138
141
|
})
|
|
139
142
|
: [];
|
|
140
143
|
const parts = [
|
|
141
|
-
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-labelledby="${idPrefix}-title" data-sf-map="" data-year="${year}" style="max-width:100%;height:auto"><title id="${idPrefix}-title">${escapeXml(title)}</title><desc>San Francisco supervisorial district boundaries (${year}).${neighborhoodLines ? ' Dashed lines show SFAR realtor neighborhood areas, defined in August 2010.' : ''}${landmarks ? ' Highlighted areas show six parks and landmarks.' : ''}${keyRoads ? ' Thin gray lines show selected road corridors.' : ''}${bartStations ? ' Rings mark the eight San Francisco BART stations.' : ''} Geometry from DataSF${bartStations ? ' and BART' : ''}. See package SOURCES.md.</desc
|
|
144
|
+
`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}" height="${height}" role="img" aria-labelledby="${idPrefix}-title" data-sf-map="" data-year="${year}"${animated ? ` data-sf-animate="${idPrefix}"` : ''} style="max-width:100%;height:auto"><title id="${idPrefix}-title">${escapeXml(title)}</title><desc>San Francisco supervisorial district boundaries (${year}).${neighborhoodLines ? ' Dashed lines show SFAR realtor neighborhood areas, defined in August 2010.' : ''}${landmarks ? ' Highlighted areas show six parks and landmarks.' : ''}${keyRoads ? ' Thin gray lines show selected road corridors.' : ''}${bartStations ? ' Rings mark the eight San Francisco BART stations.' : ''} Geometry from DataSF${bartStations ? ' and BART' : ''}. See package SOURCES.md.</desc>${animation ? staticAnimationStyle(idPrefix, animation) : ''}<defs><clipPath id="${idPrefix}-coast"><path d="${coastPath}" fill-rule="evenodd" clip-rule="evenodd"/></clipPath></defs><rect width="${width}" height="${height}" fill="${escapeXml(colors.water)}"/><g data-layer="geography"><path data-layer="coast" d="${coastPath}" fill="${escapeXml(colors.land)}" fill-rule="evenodd"/>`,
|
|
142
145
|
];
|
|
143
146
|
// Explicit drawing order keeps optional overlays and user markers predictable.
|
|
144
147
|
if (districtFills)
|
|
@@ -153,7 +156,7 @@ export function createSFMapWithData(options, data) {
|
|
|
153
156
|
parts.push(layers.neighborhoods(data.neighborhoods ?? [], context));
|
|
154
157
|
if (districtLines)
|
|
155
158
|
parts.push(layers.districtLines(districtPaths, context));
|
|
156
|
-
parts.push(`<path data-layer="coastline" d="${coastPath}" ${stroke(colors.district, 0.65)}/>`);
|
|
159
|
+
parts.push(`<path data-layer="coastline" d="${coastPath}" ${stroke(colors.district, 0.65)}${animated ? ' pathLength="1"' : ''}/>`);
|
|
157
160
|
if (labels && districtLabels)
|
|
158
161
|
parts.push(layers.districtLabels(districts, context));
|
|
159
162
|
if (labels && landmarks)
|
|
@@ -163,7 +166,7 @@ export function createSFMapWithData(options, data) {
|
|
|
163
166
|
if (bartStations)
|
|
164
167
|
parts.push(layers.bartStations(stationData, context));
|
|
165
168
|
for (const overlay of options.overlays ?? []) {
|
|
166
|
-
parts.push(`<path data-overlay-id="${escapeXml(overlay.id)}"${overlay.label ? ` aria-label="${escapeXml(overlay.label)}"` : ''} d="${path(overlay.geometry)}" fill="${escapeXml(overlay.fill ?? 'none')}" fill-opacity="${overlay.fillOpacity ?? 1}" stroke="${escapeXml(overlay.stroke ?? colors.road)}" stroke-width="${overlay.strokeWidth ?? 2}" vector-effect="non-scaling-stroke"${overlay.visible === false ? ' display="none"' : ''}/>`);
|
|
169
|
+
parts.push(`<path data-overlay-id="${escapeXml(overlay.id)}"${overlay.label ? ` aria-label="${escapeXml(overlay.label)}"` : ''} d="${path(overlay.geometry)}" fill="${escapeXml(overlay.fill ?? 'none')}" fill-opacity="${overlay.fillOpacity ?? 1}" stroke="${escapeXml(overlay.stroke ?? colors.road)}" stroke-width="${overlay.strokeWidth ?? 2}" vector-effect="non-scaling-stroke"${animated ? ' pathLength="1"' : ''}${overlay.visible === false ? ' display="none"' : ''}/>`);
|
|
167
170
|
}
|
|
168
171
|
parts.push(layers.markers(markers, context), '</g></svg>');
|
|
169
172
|
return {
|
package/dist/src/overlays.d.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import type { Position } from '../data/types.js';
|
|
2
1
|
export declare const landmarks: {
|
|
3
2
|
name: string;
|
|
4
|
-
label: Position;
|
|
3
|
+
label: import("../data/types.js").Position;
|
|
5
4
|
offset: readonly [number, number];
|
|
6
5
|
anchor: 'middle' | 'start' | 'end';
|
|
7
6
|
id: string;
|
|
@@ -10,7 +9,7 @@ export declare const landmarks: {
|
|
|
10
9
|
export declare const bartStations: {
|
|
11
10
|
id: string;
|
|
12
11
|
name: string;
|
|
13
|
-
coordinates: Position;
|
|
12
|
+
coordinates: import("../data/types.js").Position;
|
|
14
13
|
}[];
|
|
15
14
|
export type Landmark = (typeof landmarks)[number];
|
|
16
15
|
export type BartStation = (typeof bartStations)[number];
|
|
@@ -18,7 +17,7 @@ export declare const keyRoads: {
|
|
|
18
17
|
name: string;
|
|
19
18
|
level?: 'primary' | 'secondary';
|
|
20
19
|
sourceNames: readonly string[];
|
|
21
|
-
label: Position;
|
|
20
|
+
label: import("../data/types.js").Position;
|
|
22
21
|
segmentIds: readonly string[];
|
|
23
22
|
id: string;
|
|
24
23
|
geometry: import("../data/types.js").Geometry;
|
package/dist/src/overlays.js
CHANGED
|
@@ -12,4 +12,8 @@ export const bartStations = deepFreeze(stationData.features.map(({ id, propertie
|
|
|
12
12
|
name: properties.name,
|
|
13
13
|
coordinates: geometry.coordinates,
|
|
14
14
|
})));
|
|
15
|
-
export const keyRoads = deepFreeze(roadData.features.map(({ id, properties, geometry }) => ({
|
|
15
|
+
export const keyRoads = deepFreeze(roadData.features.map(({ id, properties, geometry }) => ({
|
|
16
|
+
id,
|
|
17
|
+
...properties,
|
|
18
|
+
geometry,
|
|
19
|
+
})));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { SFMapOptions } from './types.js';
|
|
2
|
+
export interface StaticAnimation {
|
|
3
|
+
duration: number;
|
|
4
|
+
delay: number;
|
|
5
|
+
}
|
|
6
|
+
/** Validate `animation` and return timings, or null when animation is off. */
|
|
7
|
+
export declare function normalizeStaticAnimation(value: SFMapOptions['animation']): StaticAnimation | null;
|
|
8
|
+
/**
|
|
9
|
+
* Scoped, self-contained CSS for an animated static map. It runs when the SVG is
|
|
10
|
+
* inserted into a page or loaded as an image, and stays still under reduced motion.
|
|
11
|
+
* Line layers carry `pathLength="1"` so a single dash can draw them on.
|
|
12
|
+
*/
|
|
13
|
+
export declare function staticAnimationStyle(idPrefix: string, { duration, delay }: StaticAnimation): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { assertOptions } from './validation.js';
|
|
2
|
+
/** Reference choreography length; `duration` rescales every step proportionally. */
|
|
3
|
+
const REFERENCE = 2400;
|
|
4
|
+
/** Validate `animation` and return timings, or null when animation is off. */
|
|
5
|
+
export function normalizeStaticAnimation(value) {
|
|
6
|
+
if (value === undefined || value === false)
|
|
7
|
+
return null;
|
|
8
|
+
if (value === true)
|
|
9
|
+
return { duration: REFERENCE, delay: 0 };
|
|
10
|
+
assertOptions(value, 'animation', ['duration', 'delay']);
|
|
11
|
+
const { duration = REFERENCE, delay = 0 } = value;
|
|
12
|
+
if (typeof duration !== 'number' || !Number.isFinite(duration) || duration <= 0)
|
|
13
|
+
throw new RangeError('Animation duration must be a positive finite number.');
|
|
14
|
+
if (typeof delay !== 'number' || !Number.isFinite(delay) || delay < 0)
|
|
15
|
+
throw new RangeError('Animation delay must be a nonnegative finite number.');
|
|
16
|
+
return { duration, delay };
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Scoped, self-contained CSS for an animated static map. It runs when the SVG is
|
|
20
|
+
* inserted into a page or loaded as an image, and stays still under reduced motion.
|
|
21
|
+
* Line layers carry `pathLength="1"` so a single dash can draw them on.
|
|
22
|
+
*/
|
|
23
|
+
export function staticAnimationStyle(idPrefix, { duration, delay }) {
|
|
24
|
+
const scale = duration / REFERENCE;
|
|
25
|
+
const time = (ms) => `${Math.round(ms * scale)}ms`;
|
|
26
|
+
const at = (ms, step) => step === undefined
|
|
27
|
+
? `${Math.round(delay + ms * scale)}ms`
|
|
28
|
+
: `calc(${Math.round(delay + ms * scale)}ms + var(--i,0) * ${Math.round(step * scale)}ms)`;
|
|
29
|
+
const scope = `[data-sf-animate="${idPrefix}"]`;
|
|
30
|
+
const out = 'cubic-bezier(.16,1,.3,1)';
|
|
31
|
+
const inOut = 'cubic-bezier(.65,0,.35,1)';
|
|
32
|
+
const fade = `${idPrefix}-fade`;
|
|
33
|
+
const draw = `${idPrefix}-draw`;
|
|
34
|
+
const grow = `${idPrefix}-grow`;
|
|
35
|
+
return `<style>@keyframes ${fade}{from{opacity:0}}@keyframes ${draw}{from{stroke-dashoffset:1}}@keyframes ${grow}{from{opacity:0;transform:scale(.86)}}${scope} [data-layer="coast"]{animation:${fade} ${time(700)} ${out} ${at(0)} both}${scope} [data-layer="coastline"],${scope} [data-layer="district-lines"] path,${scope} [data-layer="highways"] path,${scope} [data-layer="key-roads"] path,${scope} [data-overlay-id]{stroke-dasharray:1}${scope} [data-layer="coastline"]{animation:${draw} ${time(1500)} ${inOut} ${at(0)} both}${scope} [data-layer="district-fills"] path{transform-box:fill-box;transform-origin:center;animation:${grow} ${time(800)} ${out} ${at(300, 60)} both}${scope} [data-layer="landmarks"]{animation:${fade} ${time(700)} ${out} ${at(900)} both}${scope} [data-layer="key-roads"] path,${scope} [data-layer="highways"] path{animation:${draw} ${time(1100)} ${inOut} ${at(1000)} both}${scope} [data-layer="neighborhood-lines"] path{animation:${fade} ${time(500)} ${out} ${at(1100, 7)} both}${scope} [data-layer="district-lines"] path{animation:${draw} ${time(1000)} ${inOut} ${at(1150, 45)} both}${scope} [data-overlay-id]{animation:${draw} ${time(1000)} ${inOut} ${at(1500)} both}${scope} [data-layer$="-labels"]{animation:${fade} ${time(600)} ${out} ${at(1850)} both}${scope} [data-bart-station]>circle,${scope} [data-marker-id]{transform-box:fill-box;transform-origin:center;animation:${grow} ${time(500)} ${out} ${at(1900, 70)} both}${scope} [data-bart-station]>text{animation:${fade} ${time(500)} ${out} ${at(2000, 70)} both}@media (prefers-reduced-motion:reduce){${scope} *{animation:none!important}}</style>`;
|
|
36
|
+
}
|
package/dist/src/transit.js
CHANGED
|
@@ -5,6 +5,7 @@ import parks from '../data/landmarks.json' with { type: 'json' };
|
|
|
5
5
|
import { createSFMapWithData } from './map-core.js';
|
|
6
6
|
const districtData = districts;
|
|
7
7
|
const parkData = parks;
|
|
8
|
+
const stationData = stations;
|
|
8
9
|
const transitMapData = {
|
|
9
10
|
coast: coast.features[0].geometry,
|
|
10
11
|
districts: {
|
|
@@ -38,7 +39,7 @@ export function createTransitAnimation() {
|
|
|
38
39
|
</style>${map.svg}<div class="controls"><button type="button">Play</button><label>Journey <input aria-label="Journey progress" type="range" min="0" max="1000" value="0"></label></div><output aria-live="off"></output><p>Schematic BART journey. Straight connections between official station locations; not track geometry, a timetable, or live trains. One loop takes 28 seconds.</p>`;
|
|
39
40
|
const svg = root.querySelector('svg');
|
|
40
41
|
const ns = 'http://www.w3.org/2000/svg';
|
|
41
|
-
const points =
|
|
42
|
+
const points = stationData.features.map((station) => map.project(station.geometry.coordinates));
|
|
42
43
|
const line = document.createElementNS(ns, 'polyline');
|
|
43
44
|
line.setAttribute('points', points.map((point) => point.join(',')).join(' '));
|
|
44
45
|
line.setAttribute('fill', 'none');
|
package/dist/src/types.d.ts
CHANGED
|
@@ -55,6 +55,16 @@ export interface SFMapOptions {
|
|
|
55
55
|
overlays?: readonly MapOverlay[];
|
|
56
56
|
title?: string;
|
|
57
57
|
idPrefix?: string;
|
|
58
|
+
/**
|
|
59
|
+
* Opt-in, self-contained CSS choreography for static SVGs: land fades in, the coast and
|
|
60
|
+
* lines draw, districts grow, then labels and points appear. It plays when the SVG is
|
|
61
|
+
* inserted into a page or loaded as an image, and stays still under reduced motion.
|
|
62
|
+
* `duration` (default 2400 ms) scales the whole sequence; `delay` offsets it.
|
|
63
|
+
*/
|
|
64
|
+
animation?: boolean | {
|
|
65
|
+
duration?: number;
|
|
66
|
+
delay?: number;
|
|
67
|
+
};
|
|
58
68
|
colors?: Partial<Record<'water' | 'land' | 'district' | 'neighborhood' | 'highway' | 'road' | 'park' | 'landmark' | 'bart' | 'label' | 'marker' | 'selected', string>>;
|
|
59
69
|
}
|
|
60
70
|
/** Serializable viewport in the interactive map's fixed 800 × 800 projected space. */
|
|
@@ -104,6 +114,21 @@ export interface MapFeatures {
|
|
|
104
114
|
};
|
|
105
115
|
northArrow?: boolean;
|
|
106
116
|
scaleBar?: boolean;
|
|
117
|
+
/**
|
|
118
|
+
* Fade layers as `setLayers` and `setMode` switch them, crossfade neighborhood boundaries
|
|
119
|
+
* when `setSource` changes the definition, and crossfade district fills on
|
|
120
|
+
* `setDistrictStyle`. Reduced motion takes precedence.
|
|
121
|
+
*/
|
|
122
|
+
layerTransitions?: boolean | {
|
|
123
|
+
duration?: number;
|
|
124
|
+
};
|
|
125
|
+
/**
|
|
126
|
+
* Morph district outlines from one map year to the next on `setDistrictYear`; pass
|
|
127
|
+
* `{ animate: false }` for an instant change. Reduced motion takes precedence.
|
|
128
|
+
*/
|
|
129
|
+
districtMorph?: boolean | {
|
|
130
|
+
duration?: number;
|
|
131
|
+
};
|
|
107
132
|
}
|
|
108
133
|
export interface NeighborhoodExplorerOptions extends MapFeatures {
|
|
109
134
|
mode?: ExplorerMode;
|
package/dist/src/validation.js
CHANGED
package/docs/API.md
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Map options and lifecycle
|
|
2
|
+
|
|
3
|
+
Start with the [static and interactive examples](../README.md). Exact option
|
|
4
|
+
and event types are exported by [src/api.ts](../src/api.ts) and defined in
|
|
5
|
+
[src/types.ts](../src/types.ts) and [src/controller-types.ts](../src/controller-types.ts).
|
|
6
|
+
|
|
7
|
+
## Static maps
|
|
8
|
+
|
|
9
|
+
Static 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.
|
|
10
|
+
|
|
11
|
+
Custom district IDs must be finite numbers. An empty `labelPoints` array uses the district's primary `label` position.
|
|
12
|
+
|
|
13
|
+
`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.
|
|
14
|
+
|
|
15
|
+
## Interactive maps
|
|
16
|
+
|
|
17
|
+
`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](developer-guide.md) compares static and interactive defaults and explains configuration resets, coordinates, and cleanup.
|
|
18
|
+
|
|
19
|
+
Construction options include `mode` (`basemap`, `neighborhoods`, or `districts`), `source`, `neighborhood`, `year`, `labels`, `layers`, `controls`, `features`, `appearance`, `markers`, `overlays`, `legend`, `strings`, `attribution`, and `fitPadding`. Enable map touch gestures with the touch control or `map.setTouchNavigation(true)`. `features` holds motion, marker entrances, selected marker rings, clustering, north arrow, scale bar, layer transitions, and district morphs. `appearance` holds theme, color tokens, label and area styles, marker colors and sizes, and district styling. `layers` controls district fill/line/labels, neighborhood lines/labels, landmarks, BART, highways, key roads, and road labels. See the exported `MapOptions` type for exact values and the [Storybook examples](../stories/) for live controls.
|
|
20
|
+
|
|
21
|
+
The controller also supports marker, neighborhood, and district selection; district year and style changes; source and mode changes; labels and touch navigation; screen projection; and typed `markerchange`, `neighborhoodchange`, `districtchange`, `districthover`, `districtactivate`, `districtyearchange`, `overlayactivate`, `clusteractivate`, `viewportchange`, and `mapresize` events. `destroy()` releases browser resources; operations after destruction throw.
|
|
22
|
+
|
|
23
|
+
If a district callback destroys the map or selects a newer district, the interrupted activation stops without moving keyboard focus or sending a stale event.
|
|
24
|
+
|
|
25
|
+
`setMarkers()` reconciles by stable marker `id`: retained markers keep their DOM nodes, focus, and in-progress entrance animations; only new IDs animate in. Identical ordered marker updates are a visual no-op. Camera state and `viewportchange` events remain synchronous, while animated camera steps and their dependent label/marker layout commit in the same browser frame.
|
|
26
|
+
|
|
27
|
+
For a small guide, import `guideMapData` from `/guide/data` and pass it to `createMap`. The optional `/guide` entry also provides `createGuideMap`, `mountGuideMap`, and detailed-data loading for existing guide layouts. `/transit` provides the standalone schematic transit animation. See [examples](EXAMPLES.md) and [consumer integration](consumer-integration.md).
|
|
28
|
+
|
|
29
|
+
For the guide preset with the same controller API, use `createGuideController(options)` from `/guide` (or `/guide/map`). It accepts grouped `MapOptions` and returns `MapController`; `mountGuideController(shell, options)` enhances a `createGuideShell()` container. Both use the lightweight guide geography. Existing `createGuideMap` and `mountGuideMap` calls retain their element-based API.
|
|
30
|
+
|
|
31
|
+
Two opt-in features animate the map's lines and layers. `features.layerTransitions` fades layers as `configure({ layers })` and `setMode()` switch them, crossfades neighborhood boundaries when `setSource()` changes the definition, and crossfades district fills on `setDistrictStyle()`. `features.districtMorph` moves district outlines from one map year to the next on `setDistrictYear()`, then settles on the exact published geometry; pass `{ animate: false }` for an instant change. Both default off, own and cancel their animations, and yield to reduced motion.
|
|
32
|
+
|
|
33
|
+
District style callbacks are evaluated once per district at construction and on style or year updates. Hover, selection, and layer toggles reuse the prepared styles; call `setDistrictStyle()` again when external styling data changes. Invalid styles leave the current map unchanged. District fades respect reduced motion and are removed on interruption or destruction.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Consumer integration
|
|
2
2
|
|
|
3
|
-
Use the
|
|
3
|
+
Use the root controller and import geographic data explicitly. The [migration guide](migration-v3.md) maps removed v2 imports to supported calls.
|
|
4
4
|
|
|
5
5
|
For a compact browser guide:
|
|
6
6
|
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Developer guide
|
|
2
|
+
|
|
3
|
+
Choose the renderer and geography separately. `renderMap` produces a string without a DOM; `createMap` produces a browser controller whose `.element` you mount. Neither root function imports geography for you.
|
|
4
|
+
|
|
5
|
+
## Choose an entry point
|
|
6
|
+
|
|
7
|
+
| Task | Renderer | Data |
|
|
8
|
+
| --- | --- | --- |
|
|
9
|
+
| Generate an SVG file or server-rendered image | `/static`: `renderMap` | `/data/static`: `staticMapData` |
|
|
10
|
+
| Explore districts or several neighborhood definitions | `/map`: `createMap` | `/data/full`: `fullMapData` |
|
|
11
|
+
| Embed a smaller neighborhood guide | `/guide/map`: `createGuideController` | Included lightweight guide geography |
|
|
12
|
+
| Use the guide data with the general controller | `/map`: `createMap` | `/guide/data`: `guideMapData` |
|
|
13
|
+
| Look up names without mounting a map | `/data/lookup` | Source-aware lookup helpers |
|
|
14
|
+
|
|
15
|
+
Imports starting with `/` in this table are package suffixes, such as `@kahwee/sf-map-svg/static`. Node 24+ is required for server rendering. Browser examples need a DOM and a bundler that supports JSON modules. This is an ESM package.
|
|
16
|
+
|
|
17
|
+
## Write a static SVG
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { writeFile } from 'node:fs/promises';
|
|
21
|
+
import { renderMap } from '@kahwee/sf-map-svg/static';
|
|
22
|
+
import { staticMapData } from '@kahwee/sf-map-svg/data/static';
|
|
23
|
+
|
|
24
|
+
const { svg } = renderMap(staticMapData, {
|
|
25
|
+
year: 2022,
|
|
26
|
+
landmarks: true,
|
|
27
|
+
bartStations: true,
|
|
28
|
+
title: 'San Francisco districts, 2022',
|
|
29
|
+
});
|
|
30
|
+
await writeFile('san-francisco.svg', svg);
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The SVG is self-contained. `project([lng, lat])` returns SVG coordinates; `viewBox` is `[0, 0, width, height]`. Use a different `idPrefix` for each inline SVG on the same page, or let the renderer generate one.
|
|
34
|
+
|
|
35
|
+
## Mount and dispose an interactive map
|
|
36
|
+
|
|
37
|
+
Provide a host such as `<div id="map"></div>` before running this code. Run browser construction after mounting, not during server rendering.
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import { createMap } from '@kahwee/sf-map-svg/map';
|
|
41
|
+
import { fullMapData } from '@kahwee/sf-map-svg/data/full';
|
|
42
|
+
|
|
43
|
+
const host = document.querySelector<HTMLElement>('#map');
|
|
44
|
+
if (!host) throw new Error('Missing #map host');
|
|
45
|
+
|
|
46
|
+
const map = createMap(fullMapData, {
|
|
47
|
+
mode: 'neighborhoods',
|
|
48
|
+
source: 'realtor',
|
|
49
|
+
layers: { bartStations: true },
|
|
50
|
+
features: { motion: true },
|
|
51
|
+
appearance: { theme: 'districts' },
|
|
52
|
+
});
|
|
53
|
+
host.append(map.element);
|
|
54
|
+
const unsubscribe = map.on('neighborhoodchange', ({ name }) => {
|
|
55
|
+
// name is null when the selection is cleared.
|
|
56
|
+
console.log(name);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
// Call from your framework's unmount hook or when removing the view.
|
|
60
|
+
function disposeMap() {
|
|
61
|
+
unsubscribe();
|
|
62
|
+
map.destroy();
|
|
63
|
+
map.element.remove();
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
`destroy()` is idempotent, cancels owned work and subscriptions, and leaves host-owned DOM in place. Other controller operations after destruction throw. In React, construct inside an effect and return cleanup; in Vue or Svelte, use their mount/unmount hooks. Do not create a new map on every render: update the existing controller instead.
|
|
68
|
+
|
|
69
|
+
## Know the defaults
|
|
70
|
+
|
|
71
|
+
| Setting | `renderMap` | `createMap` |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| Mode | No mode option | `basemap`; set `neighborhoods` or `districts` explicitly |
|
|
74
|
+
| Theme | `districts` | `transit`; use `appearance.theme` |
|
|
75
|
+
| District layers | On | Follow `districts` mode |
|
|
76
|
+
| Neighborhood layers | Lines off | Follow `neighborhoods` mode |
|
|
77
|
+
| Parks, highways, key roads | Off | On when supplied |
|
|
78
|
+
| Road labels | Follow `keyRoads` | On when supplied, subject to zoom/collisions |
|
|
79
|
+
| BART stations | Off | On when supplied |
|
|
80
|
+
| Visible labels | On | On, subject to zoom/collisions |
|
|
81
|
+
| Motion features | Static animation off | Off; opt in via `features` |
|
|
82
|
+
|
|
83
|
+
A layer switch does not download missing geography. `fullMapData` has `.map` for static rendering and separate lookup collections for interactive use. `staticMapData` is already a static data object. Use `renderMap(guideMapData.map, options)`, not `renderMap(guideMapData, options)`.
|
|
84
|
+
|
|
85
|
+
SFAR realtor neighborhoods are the default when supplied. If only an alternative collection is supplied, the controller selects that available source. Choose `source` explicitly when comparing definitions. Sources retain separate identities; they are not interchangeable polygons.
|
|
86
|
+
|
|
87
|
+
## Construction settings versus runtime updates
|
|
88
|
+
|
|
89
|
+
| Change | Call |
|
|
90
|
+
| --- | --- |
|
|
91
|
+
| Layers, controls, behaviors | `map.configure({ layers, controls, features })` |
|
|
92
|
+
| Map mode or neighborhood source | `map.setMode(mode)`, `map.setSource(source)` |
|
|
93
|
+
| District boundaries or choropleth | `map.setDistrictYear(year)`, `map.setDistrictStyle(callback)` |
|
|
94
|
+
| Master text visibility | `map.setLabels(visible)` |
|
|
95
|
+
| Markers or route overlays | `map.setMarkers(items)`, `map.setOverlays(items)` |
|
|
96
|
+
| Camera | `map.camera.set/pan/zoom/fit/reset(...)` |
|
|
97
|
+
| Palette, typography, control styling | Construction-only `appearance`; recreate the map to change it |
|
|
98
|
+
|
|
99
|
+
`configure` merges supplied keys; omitted keys keep their existing values. Set a layer or control key to `undefined` to remove its override. Set a whole group to `undefined` to reset that group. Set a feature to `false` to disable it. Explicit layer overrides keep winning after mode changes.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
map.configure({ layers: { neighborhoodLabels: false } });
|
|
103
|
+
map.configure({ layers: { neighborhoodLabels: undefined } }); // follow mode again
|
|
104
|
+
map.configure({ features: { motion: false } });
|
|
105
|
+
map.configure({ layers: undefined }); // clear all explicit layer overrides
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`getConfiguration()` returns a detached snapshot of these groups, including explicit layer overrides. It is not a full serialization of mode, source, year, camera, selection, or construction appearance. Invalid configuration patches leave the current map unchanged.
|
|
109
|
+
|
|
110
|
+
## Markers, routes and camera coordinates
|
|
111
|
+
|
|
112
|
+
Marker positions are `{ lng, lat }`. GeoJSON coordinates and the static projection use `[longitude, latitude]`, in WGS84. Camera viewports use `[x, y, size]` in the fixed 800 × 800 projected map space; they are not geographic bounds. `camera.pan` uses projected units. `projectToScreen(lng, lat)` returns pixels relative to the map canvas, plus visibility.
|
|
113
|
+
|
|
114
|
+
Marker and overlay IDs must be unique. Keep marker IDs stable across updates so retained markers preserve DOM nodes, focus and entrance animations. Use `null` to clear a selection; selection methods return a boolean. Selection events can contain null values. `map.on()` returns an unsubscribe function.
|
|
115
|
+
|
|
116
|
+
Reduced motion takes precedence over animations. Map touch gestures are opt-in; hiding a chooser requires an accessible alternative in your application.
|
|
117
|
+
|
|
118
|
+
## Common integration errors
|
|
119
|
+
|
|
120
|
+
| Symptom | Check |
|
|
121
|
+
| --- | --- |
|
|
122
|
+
| No neighborhoods or district fills | Set `mode` explicitly; the default is `basemap` |
|
|
123
|
+
| An enabled layer does not appear | Supply its dataset; check master labels, zoom and collision filtering |
|
|
124
|
+
| A theme or motion option throws | Put styling in `appearance` and behaviors in `features`; flat interactive options are rejected |
|
|
125
|
+
| Source switch throws | Supply that source collection, or use `/data/full` |
|
|
126
|
+
| A geographic coordinate produces a surprising camera view | Use `camera.fit(geometry)`; `camera.set` takes projected coordinates |
|
|
127
|
+
| An event handler breaks after clearing selection | Handle null `id`, `name`, `marker`, or `district` |
|
|
128
|
+
| Updates throw after navigating away | Dispose once and stop calling the destroyed controller |
|
|
129
|
+
| An import from `/legacy`, `/custom-map`, `/explorer`, `/interactive` or `/interactive-data` fails | Those entries were removed in v3; follow the migration guide |
|
|
130
|
+
|
|
131
|
+
## Versions and assistant-readable docs
|
|
132
|
+
|
|
133
|
+
The package declarations are the exact type contract for your installed version. Check the changelog before using features shown on a local preview: its Unreleased section can include functionality absent from npm.
|
|
134
|
+
|
|
135
|
+
The generated `llms.txt` index links to Markdown documentation; `llms-full.txt` combines the guides and type contracts into one file. Both are built from the same package selected for the site. Local previews are labeled as working-tree docs. Released builds use the installed npm package's docs and declarations.
|
|
136
|
+
|
|
137
|
+
See [API options](API.md), [worked examples](EXAMPLES.md), [consumer integration](consumer-integration.md), and [v3 migration](migration-v3.md).
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Guide bundle size report
|
|
2
2
|
|
|
3
|
-
Generated 2026-
|
|
3
|
+
Generated 2026-10-01 by `pnpm report:guide` with Vite production minification and gzip compression. Each emitted JS chunk is compressed independently. The report measures the explicit-data root and the optional `@kahwee/sf-map-svg/guide` preset.
|
|
4
4
|
|
|
5
5
|
| Entry | Initial JS, raw | Initial JS, gzip | Explicit detail JS, gzip |
|
|
6
6
|
| --- | ---: | ---: | ---: |
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
| Guide preset |
|
|
7
|
+
| Renderer root (explicit data) | 99.3 KB | 29.1 KB | — |
|
|
8
|
+
| Static renderer | 18.2 KB | 5.8 KB | — |
|
|
9
|
+
| Guide preset | 489.1 KB | 116.8 KB | 467.0 KB |
|
|
10
10
|
|
|
11
11
|
**500 KB target:** met.
|
|
12
12
|
|