@zappie/foundations 0.1.3 → 0.1.5
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 +24 -0
- package/README.md +154 -7
- package/dist/branch-map/colors.js +27 -0
- package/dist/branch-map/colors.js.map +7 -0
- package/dist/branch-map/delivery-map.js +60 -0
- package/dist/branch-map/delivery-map.js.map +7 -0
- package/dist/branch-map/delivery-runtime.js +118 -0
- package/dist/branch-map/delivery-runtime.js.map +7 -0
- package/dist/branch-map/fit.js +123 -0
- package/dist/branch-map/fit.js.map +7 -0
- package/dist/branch-map/geometry.js +1 -27
- package/dist/branch-map/geometry.js.map +2 -2
- package/dist/branch-map/index.js +42 -36
- package/dist/branch-map/index.js.map +2 -2
- package/dist/branch-map/markers.js +143 -0
- package/dist/branch-map/markers.js.map +7 -0
- package/dist/branch-map/runtime.js +258 -53
- package/dist/branch-map/runtime.js.map +3 -3
- package/dist/branch-map/use-map-runtime.js +53 -0
- package/dist/branch-map/use-map-runtime.js.map +7 -0
- package/dist/deferred/index.js +111 -0
- package/dist/deferred/index.js.map +7 -0
- package/dist/index.d.ts +179 -8
- package/dist/index.js +5 -1
- package/dist/index.js.map +2 -2
- package/dist/item-form/index.js.map +1 -1
- package/dist/item-form/root.js +25 -8
- package/dist/item-form/root.js.map +2 -2
- package/dist/overlay/react.js +245 -24
- package/dist/overlay/react.js.map +3 -3
- package/dist/version.js +1 -1
- package/package.json +3 -2
|
@@ -1,90 +1,295 @@
|
|
|
1
1
|
// src/branch-map/runtime.ts
|
|
2
|
+
import { MarkerClusterer } from "@googlemaps/markerclusterer";
|
|
3
|
+
import { clusterSize, fitPlan, focusedBranch } from "./fit.js";
|
|
2
4
|
import {
|
|
3
|
-
branchBounds,
|
|
4
5
|
validCoordinates,
|
|
5
6
|
visibleBranches
|
|
6
7
|
} from "./geometry.js";
|
|
7
8
|
import { loadMaps } from "./loader.js";
|
|
8
|
-
|
|
9
|
+
import {
|
|
10
|
+
branchMarker,
|
|
11
|
+
DEFAULT_COLORS,
|
|
12
|
+
labelOffset,
|
|
13
|
+
markerOverlayClass,
|
|
14
|
+
paintLabel,
|
|
15
|
+
paintMarker
|
|
16
|
+
} from "./markers.js";
|
|
17
|
+
var clusterable = (marker) => marker;
|
|
18
|
+
var ROUTE_OPACITY = 0.7;
|
|
19
|
+
var ROUTE_WEIGHT = 4;
|
|
20
|
+
var CLUSTER_EXPAND_PADDING = 56;
|
|
21
|
+
var CLUSTER_EXPAND_MAX_ZOOM = 21;
|
|
22
|
+
var CLUSTER_EXPAND_MAX_ATTEMPTS = 6;
|
|
23
|
+
function clusterIcon(count, colors) {
|
|
24
|
+
const size = clusterSize(count);
|
|
25
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size}" height="${size}" viewBox="0 0 ${size} ${size}"><circle cx="${size / 2}" cy="${size / 2}" r="${size / 2 - 3}" fill="${colors.accent}" stroke="#ffffff" stroke-width="3"/></svg>`;
|
|
26
|
+
return {
|
|
27
|
+
size,
|
|
28
|
+
url: `data:image/svg+xml;base64,${btoa(svg)}`,
|
|
29
|
+
label: {
|
|
30
|
+
text: String(count),
|
|
31
|
+
color: colors.accentForeground,
|
|
32
|
+
fontSize: "13px",
|
|
33
|
+
fontWeight: "600"
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async function createMap(element, key, signal, options) {
|
|
9
38
|
const maps = await loadMaps(key);
|
|
10
39
|
signal.throwIfAborted();
|
|
11
40
|
const map = new maps.Map(element, {
|
|
12
41
|
center: { lat: 0, lng: 0 },
|
|
13
42
|
zoom: 12,
|
|
14
|
-
|
|
15
|
-
mapTypeControl: false,
|
|
43
|
+
mapTypeControl: options.mapTypeControl,
|
|
16
44
|
streetViewControl: false
|
|
17
45
|
});
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
46
|
+
const Marker = markerOverlayClass(maps);
|
|
47
|
+
const pins = /* @__PURE__ */ new Map();
|
|
48
|
+
let shopperPin;
|
|
49
|
+
let route;
|
|
50
|
+
let clusterer;
|
|
51
|
+
let clusterColors;
|
|
52
|
+
let clustered;
|
|
53
|
+
let fitListener;
|
|
54
|
+
let stopExpanding;
|
|
55
|
+
let fitted;
|
|
56
|
+
let lastSelected;
|
|
57
|
+
let select = () => {
|
|
58
|
+
};
|
|
59
|
+
const fit = (plan) => {
|
|
60
|
+
fitListener?.remove();
|
|
61
|
+
fitListener = void 0;
|
|
62
|
+
if (plan.kind === "center") {
|
|
63
|
+
map.panTo(plan.center);
|
|
64
|
+
map.setZoom(plan.zoom);
|
|
65
|
+
return;
|
|
35
66
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
);
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
this.button.style.top = `${point.y}px`;
|
|
67
|
+
map.fitBounds(plan.bounds, plan.padding);
|
|
68
|
+
fitListener = maps.event.addListenerOnce(map, "idle", () => {
|
|
69
|
+
fitListener = void 0;
|
|
70
|
+
const zoom = map.getZoom();
|
|
71
|
+
if (typeof zoom === "number" && zoom > plan.maxZoom) {
|
|
72
|
+
map.setZoom(plan.maxZoom);
|
|
43
73
|
}
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
const ensureClusterer = (colors) => {
|
|
77
|
+
const key2 = `${colors.accent}|${colors.accentForeground}`;
|
|
78
|
+
if (clusterer && clusterColors === key2) {
|
|
79
|
+
return clusterer;
|
|
44
80
|
}
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
81
|
+
clusterer?.clearMarkers(true);
|
|
82
|
+
clusterer?.setMap(null);
|
|
83
|
+
clusterColors = key2;
|
|
84
|
+
clustered = void 0;
|
|
85
|
+
clusterer = new MarkerClusterer({
|
|
86
|
+
map,
|
|
87
|
+
renderer: {
|
|
88
|
+
render: ({ count, position }) => {
|
|
89
|
+
const icon = clusterIcon(count, colors);
|
|
90
|
+
return new maps.Marker({
|
|
91
|
+
position,
|
|
92
|
+
icon: {
|
|
93
|
+
url: icon.url,
|
|
94
|
+
scaledSize: new maps.Size(icon.size, icon.size)
|
|
95
|
+
},
|
|
96
|
+
label: icon.label,
|
|
97
|
+
zIndex: 999
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
return clusterer;
|
|
103
|
+
};
|
|
104
|
+
const expandTo = (marker) => {
|
|
105
|
+
stopExpanding?.();
|
|
106
|
+
const active = clusterer;
|
|
107
|
+
if (!active) {
|
|
108
|
+
return;
|
|
48
109
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
110
|
+
let attempts = 0;
|
|
111
|
+
let cancelled = false;
|
|
112
|
+
let idle;
|
|
113
|
+
const expand = () => {
|
|
114
|
+
if (cancelled) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
active.render();
|
|
118
|
+
const cluster = active.clusters?.find(
|
|
119
|
+
(candidate) => candidate.markers?.includes(clusterable(marker))
|
|
120
|
+
);
|
|
121
|
+
if (!cluster || cluster.count <= 1) {
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
if (attempts === 0 && cluster.bounds) {
|
|
125
|
+
map.fitBounds(cluster.bounds, CLUSTER_EXPAND_PADDING);
|
|
126
|
+
} else {
|
|
127
|
+
const zoom = map.getZoom();
|
|
128
|
+
if (typeof zoom !== "number" || zoom >= CLUSTER_EXPAND_MAX_ZOOM) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
map.setZoom(Math.min(zoom + 2, CLUSTER_EXPAND_MAX_ZOOM));
|
|
132
|
+
}
|
|
133
|
+
attempts += 1;
|
|
134
|
+
if (attempts < CLUSTER_EXPAND_MAX_ATTEMPTS) {
|
|
135
|
+
idle = maps.event.addListenerOnce(map, "idle", expand);
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
expand();
|
|
139
|
+
stopExpanding = () => {
|
|
140
|
+
cancelled = true;
|
|
141
|
+
idle?.remove();
|
|
142
|
+
stopExpanding = void 0;
|
|
143
|
+
};
|
|
53
144
|
};
|
|
54
145
|
return {
|
|
55
|
-
update(branches, selected, onSelect, location) {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
146
|
+
update(branches, selected, onSelect, location, options2) {
|
|
147
|
+
select = onSelect;
|
|
148
|
+
const colors = options2?.colors ?? DEFAULT_COLORS;
|
|
149
|
+
map.setOptions({ mapTypeControl: options2?.mapTypeControl ?? false });
|
|
150
|
+
const visible = visibleBranches(branches);
|
|
151
|
+
const shopper = validCoordinates(location) ? location : void 0;
|
|
152
|
+
const focused = focusedBranch(visible, selected, shopper);
|
|
153
|
+
for (const [id, pin] of pins) {
|
|
154
|
+
if (!visible.some((branch) => branch.id === id)) {
|
|
155
|
+
pin.marker.setMap(null);
|
|
156
|
+
pins.delete(id);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
for (const branch of visible) {
|
|
160
|
+
let pin = pins.get(branch.id);
|
|
161
|
+
if (!pin) {
|
|
162
|
+
const button2 = document.createElement("button");
|
|
163
|
+
button2.type = "button";
|
|
164
|
+
button2.dataset.branchMapPin = "";
|
|
165
|
+
const id = branch.id;
|
|
166
|
+
button2.onclick = () => {
|
|
167
|
+
const current = pins.get(id);
|
|
168
|
+
if (current) {
|
|
169
|
+
select(current.branch);
|
|
170
|
+
}
|
|
171
|
+
};
|
|
172
|
+
pin = {
|
|
173
|
+
marker: new Marker(branch.id, branch.coordinates, button2),
|
|
174
|
+
branch
|
|
175
|
+
};
|
|
176
|
+
pin.marker.label.dataset.branchMapPinLabel = "";
|
|
177
|
+
pins.set(branch.id, pin);
|
|
178
|
+
}
|
|
179
|
+
pin.branch = branch;
|
|
180
|
+
pin.marker.setPosition(branch.coordinates);
|
|
181
|
+
const isFocused = branch.id === focused?.id;
|
|
182
|
+
const spec = branchMarker(options2?.pinImageUrl, isFocused);
|
|
183
|
+
const button = pin.marker.element;
|
|
184
|
+
button.setAttribute("aria-label", branch.name);
|
|
185
|
+
button.setAttribute("aria-pressed", String(branch.id === selected));
|
|
186
|
+
button.dataset.selected = String(branch.id === selected);
|
|
187
|
+
button.dataset.focused = String(isFocused);
|
|
188
|
+
paintMarker(button, spec, colors);
|
|
189
|
+
const showLabel = options2?.pinLabel === "all" || options2?.pinLabel === "selected" && isFocused;
|
|
190
|
+
pin.marker.setLabel(showLabel ? branch.name : void 0);
|
|
191
|
+
pin.marker.label.dataset.selected = String(branch.id === selected);
|
|
192
|
+
paintLabel(pin.marker.label, {
|
|
193
|
+
offset: labelOffset(spec),
|
|
194
|
+
capitalize: true,
|
|
195
|
+
colors
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
const active = ensureClusterer(colors);
|
|
199
|
+
const markerSet = [...pins.keys()].join("\n");
|
|
200
|
+
if (markerSet !== clustered) {
|
|
201
|
+
clustered = markerSet;
|
|
202
|
+
active.clearMarkers(true);
|
|
203
|
+
active.addMarkers(
|
|
204
|
+
[...pins.values()].map(({ marker: marker2 }) => clusterable(marker2))
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
if (shopper) {
|
|
208
|
+
if (!shopperPin) {
|
|
209
|
+
const house = document.createElement("div");
|
|
210
|
+
house.setAttribute("role", "img");
|
|
211
|
+
house.setAttribute("aria-label", "Your location");
|
|
212
|
+
house.dataset.branchMapShopper = "";
|
|
213
|
+
shopperPin = new Marker("shopper", shopper, house);
|
|
214
|
+
shopperPin.setLabel("You");
|
|
215
|
+
shopperPin.setMap(map);
|
|
216
|
+
}
|
|
217
|
+
shopperPin.setPosition(shopper);
|
|
218
|
+
paintMarker(
|
|
219
|
+
shopperPin.element,
|
|
220
|
+
{ kind: "house", color: colors.accentForeground },
|
|
221
|
+
colors
|
|
63
222
|
);
|
|
64
|
-
|
|
65
|
-
|
|
223
|
+
paintLabel(shopperPin.label, {
|
|
224
|
+
offset: labelOffset({ kind: "house", color: "" }),
|
|
225
|
+
capitalize: false,
|
|
226
|
+
colors
|
|
227
|
+
});
|
|
228
|
+
} else {
|
|
229
|
+
shopperPin?.setMap(null);
|
|
230
|
+
shopperPin = void 0;
|
|
66
231
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
232
|
+
if (shopper && focused) {
|
|
233
|
+
route ?? (route = new maps.Polyline({ map }));
|
|
234
|
+
route.setOptions({
|
|
235
|
+
path: [shopper, focused.coordinates],
|
|
236
|
+
strokeColor: colors.accent,
|
|
237
|
+
strokeOpacity: ROUTE_OPACITY,
|
|
238
|
+
strokeWeight: ROUTE_WEIGHT,
|
|
239
|
+
clickable: false
|
|
240
|
+
});
|
|
241
|
+
} else {
|
|
242
|
+
route?.setMap(null);
|
|
243
|
+
route = void 0;
|
|
70
244
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
245
|
+
const plan = fitPlan({
|
|
246
|
+
branches: visible,
|
|
247
|
+
shopper,
|
|
248
|
+
focused,
|
|
249
|
+
maxZoom: options2?.maxZoom ?? 14,
|
|
250
|
+
compact: options2?.compact ?? false
|
|
251
|
+
});
|
|
252
|
+
const frame = JSON.stringify(plan ?? null);
|
|
253
|
+
const previous = lastSelected;
|
|
254
|
+
lastSelected = selected;
|
|
255
|
+
if (frame !== fitted) {
|
|
256
|
+
fitted = frame;
|
|
257
|
+
if (plan) {
|
|
258
|
+
fit(plan);
|
|
259
|
+
}
|
|
260
|
+
} else {
|
|
261
|
+
const target = visible.find((branch) => branch.id === selected);
|
|
262
|
+
if (target && selected !== previous && !shopper && !options2?.compact) {
|
|
263
|
+
map.panTo(target.coordinates);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
const marker = selected ? pins.get(selected)?.marker : void 0;
|
|
267
|
+
if (marker && selected !== previous) {
|
|
268
|
+
expandTo(marker);
|
|
75
269
|
}
|
|
76
270
|
},
|
|
77
271
|
locate(location) {
|
|
78
272
|
map.panTo(location);
|
|
79
273
|
},
|
|
80
274
|
destroy() {
|
|
81
|
-
|
|
275
|
+
fitListener?.remove();
|
|
276
|
+
stopExpanding?.();
|
|
277
|
+
clusterer?.clearMarkers(true);
|
|
278
|
+
clusterer?.setMap(null);
|
|
279
|
+
for (const { marker } of pins.values()) {
|
|
280
|
+
marker.setMap(null);
|
|
281
|
+
marker.element.onclick = null;
|
|
282
|
+
}
|
|
283
|
+
pins.clear();
|
|
284
|
+
shopperPin?.setMap(null);
|
|
285
|
+
route?.setMap(null);
|
|
82
286
|
maps.event.clearInstanceListeners(map);
|
|
83
287
|
element.replaceChildren();
|
|
84
288
|
}
|
|
85
289
|
};
|
|
86
290
|
}
|
|
87
291
|
export {
|
|
292
|
+
clusterIcon,
|
|
88
293
|
createMap
|
|
89
294
|
};
|
|
90
295
|
//# sourceMappingURL=runtime.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/branch-map/runtime.ts"],
|
|
4
|
-
"sourcesContent": ["import type { Branch } from \"@zappie/sdk\";\nimport {\n branchBounds,\n validCoordinates,\n visibleBranches,\n type Coordinates,\n} from \"./geometry\";\nimport { loadMaps } from \"./loader\";\n\nexport async function createMap(\n element: HTMLElement,\n key: string,\n signal: AbortSignal,\n) {\n const maps = await loadMaps(key);\n signal.throwIfAborted();\n const map = new maps.Map(element, {\n center: { lat: 0, lng: 0 },\n zoom: 12,\n maxZoom: 18,\n mapTypeControl: false,\n streetViewControl: false,\n });\n let pins: google.maps.OverlayView[] = [];\n class Pin extends maps.OverlayView {\n private button: HTMLButtonElement;\n constructor(\n private position: Coordinates,\n label: string,\n selected: boolean,\n onClick?: () => void,\n ) {\n super();\n this.button = document.createElement(\"button\");\n this.button.type = \"button\";\n this.button.setAttribute(\"aria-label\", label);\n this.button.setAttribute(\"aria-pressed\", String(selected));\n this.button.dataset.selected = String(selected);\n this.button.dataset.branchMapPin = \"\";\n this.button.style.cssText = `position:absolute;transform:translate(-50%,-100%);width:var(--branch-map-pin-size,24px);height:var(--branch-map-pin-size,24px);border-radius:50%;border:2px solid var(--branch-map-pin-border,currentColor);background:var(${selected ? \"--branch-map-pin-selected\" : \"--branch-map-pin\"},currentColor);cursor:pointer`;\n this.button.onclick = onClick ?? null;\n }\n onAdd() {\n this.getPanes()?.overlayMouseTarget.appendChild(this.button);\n maps.OverlayView.preventMapHitsAndGesturesFrom(this.button);\n }\n draw() {\n const point = this.getProjection().fromLatLngToDivPixel(\n new maps.LatLng(this.position),\n );\n if (point) {\n this.button.style.left = `${point.x}px`;\n this.button.style.top = `${point.y}px`;\n }\n }\n onRemove() {\n this.button.remove();\n this.button.onclick = null;\n }\n }\n const clear = () => {\n pins.forEach((pin) => pin.setMap(null));\n pins = [];\n };\n return {\n update(\n branches: Branch[],\n selected: string | undefined,\n onSelect: (branch: Branch) => void,\n location?: Coordinates,\n ) {\n clear();\n for (const branch of visibleBranches(branches)) {\n const pin = new Pin(\n branch.coordinates,\n branch.name,\n branch.id === selected,\n () => onSelect(branch),\n );\n pin.setMap(map);\n pins.push(pin);\n }\n const bounds = branchBounds(branches);\n if (bounds) {\n map.fitBounds(bounds, 32);\n }\n if (validCoordinates(location)) {\n const pin = new Pin(location, \"Your location\", false);\n pin.setMap(map);\n pins.push(pin);\n }\n },\n locate(location: Coordinates) {\n map.panTo(location);\n },\n destroy() {\n clear();\n maps.event.clearInstanceListeners(map);\n element.replaceChildren();\n },\n };\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { MarkerClusterer, type Cluster } from \"@googlemaps/markerclusterer\";\nimport type { Branch } from \"@zappie/sdk\";\nimport { clusterSize, fitPlan, focusedBranch, type FitPlan } from \"./fit\";\nimport {\n validCoordinates,\n visibleBranches,\n type Coordinates,\n} from \"./geometry\";\nimport { loadMaps } from \"./loader\";\nimport {\n branchMarker,\n DEFAULT_COLORS,\n labelOffset,\n markerOverlayClass,\n paintLabel,\n paintMarker,\n type MapColors,\n type MarkerOverlay,\n} from \"./markers\";\n\nexport type MapUpdateOptions = {\n maxZoom: number;\n pinImageUrl: string | undefined;\n mapTypeControl: boolean;\n pinLabel?: \"selected\" | \"all\" | undefined;\n compact?: boolean | undefined;\n colors?: MapColors | undefined;\n};\n\n// The clusterer reads only getPosition, getVisible and setMap, which overlays provide.\nconst clusterable = (marker: MarkerOverlay) =>\n marker as unknown as google.maps.Marker;\n\nconst ROUTE_OPACITY = 0.7;\nconst ROUTE_WEIGHT = 4;\nconst CLUSTER_EXPAND_PADDING = 56;\nconst CLUSTER_EXPAND_MAX_ZOOM = 21;\nconst CLUSTER_EXPAND_MAX_ATTEMPTS = 6;\n\n/** The cluster icon: an accent circle with a white ring and the branch count. */\nexport function clusterIcon(count: number, colors: MapColors) {\n const size = clusterSize(count);\n const svg = `<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"${size}\" height=\"${size}\" viewBox=\"0 0 ${size} ${size}\"><circle cx=\"${size / 2}\" cy=\"${size / 2}\" r=\"${size / 2 - 3}\" fill=\"${colors.accent}\" stroke=\"#ffffff\" stroke-width=\"3\"/></svg>`;\n return {\n size,\n url: `data:image/svg+xml;base64,${btoa(svg)}`,\n label: {\n text: String(count),\n color: colors.accentForeground,\n fontSize: \"13px\",\n fontWeight: \"600\",\n },\n };\n}\n\nexport async function createMap(\n element: HTMLElement,\n key: string,\n signal: AbortSignal,\n options: { mapTypeControl: boolean },\n) {\n const maps = await loadMaps(key);\n signal.throwIfAborted();\n const map = new maps.Map(element, {\n center: { lat: 0, lng: 0 },\n zoom: 12,\n mapTypeControl: options.mapTypeControl,\n streetViewControl: false,\n });\n const Marker = markerOverlayClass(maps);\n const pins = new Map<string, { marker: MarkerOverlay; branch: Branch }>();\n let shopperPin: MarkerOverlay | undefined;\n let route: google.maps.Polyline | undefined;\n let clusterer: MarkerClusterer | undefined;\n let clusterColors: string | undefined;\n let clustered: string | undefined;\n let fitListener: google.maps.MapsEventListener | undefined;\n let stopExpanding: (() => void) | undefined;\n let fitted: string | undefined;\n let lastSelected: string | undefined;\n let select: (branch: Branch) => void = () => {};\n\n const fit = (plan: FitPlan) => {\n fitListener?.remove();\n fitListener = undefined;\n if (plan.kind === \"center\") {\n map.panTo(plan.center);\n map.setZoom(plan.zoom);\n return;\n }\n map.fitBounds(plan.bounds, plan.padding);\n fitListener = maps.event.addListenerOnce(map, \"idle\", () => {\n fitListener = undefined;\n const zoom = map.getZoom();\n if (typeof zoom === \"number\" && zoom > plan.maxZoom) {\n map.setZoom(plan.maxZoom);\n }\n });\n };\n\n const ensureClusterer = (colors: MapColors) => {\n const key = `${colors.accent}|${colors.accentForeground}`;\n if (clusterer && clusterColors === key) {\n return clusterer;\n }\n clusterer?.clearMarkers(true);\n clusterer?.setMap(null);\n clusterColors = key;\n clustered = undefined;\n // The default click handler fits the map to the cluster, which expands it.\n clusterer = new MarkerClusterer({\n map,\n renderer: {\n render: ({ count, position }) => {\n const icon = clusterIcon(count, colors);\n return new maps.Marker({\n position,\n icon: {\n url: icon.url,\n scaledSize: new maps.Size(icon.size, icon.size),\n },\n label: icon.label,\n zIndex: 999,\n });\n },\n },\n });\n return clusterer;\n };\n\n // A selected branch hidden inside a cluster: zoom in until its own pin shows.\n const expandTo = (marker: MarkerOverlay) => {\n stopExpanding?.();\n const active = clusterer;\n if (!active) {\n return;\n }\n let attempts = 0;\n let cancelled = false;\n let idle: google.maps.MapsEventListener | undefined;\n const expand = () => {\n if (cancelled) {\n return;\n }\n active.render();\n const cluster = (\n active as MarkerClusterer & { clusters?: Cluster[] }\n ).clusters?.find((candidate) =>\n candidate.markers?.includes(clusterable(marker)),\n );\n if (!cluster || cluster.count <= 1) {\n return;\n }\n if (attempts === 0 && cluster.bounds) {\n map.fitBounds(cluster.bounds, CLUSTER_EXPAND_PADDING);\n } else {\n const zoom = map.getZoom();\n if (typeof zoom !== \"number\" || zoom >= CLUSTER_EXPAND_MAX_ZOOM) {\n return;\n }\n map.setZoom(Math.min(zoom + 2, CLUSTER_EXPAND_MAX_ZOOM));\n }\n attempts += 1;\n if (attempts < CLUSTER_EXPAND_MAX_ATTEMPTS) {\n idle = maps.event.addListenerOnce(map, \"idle\", expand);\n }\n };\n expand();\n stopExpanding = () => {\n cancelled = true;\n idle?.remove();\n stopExpanding = undefined;\n };\n };\n\n return {\n update(\n branches: Branch[],\n selected: string | undefined,\n onSelect: (branch: Branch) => void,\n location?: Coordinates,\n options?: MapUpdateOptions,\n ) {\n select = onSelect;\n const colors = options?.colors ?? DEFAULT_COLORS;\n map.setOptions({ mapTypeControl: options?.mapTypeControl ?? false });\n const visible = visibleBranches(branches);\n const shopper = validCoordinates(location) ? location : undefined;\n const focused = focusedBranch(visible, selected, shopper);\n\n for (const [id, pin] of pins) {\n if (!visible.some((branch) => branch.id === id)) {\n pin.marker.setMap(null);\n pins.delete(id);\n }\n }\n for (const branch of visible) {\n let pin = pins.get(branch.id);\n if (!pin) {\n const button = document.createElement(\"button\");\n button.type = \"button\";\n button.dataset.branchMapPin = \"\";\n const id = branch.id;\n button.onclick = () => {\n const current = pins.get(id);\n if (current) {\n select(current.branch);\n }\n };\n pin = {\n marker: new Marker(branch.id, branch.coordinates, button),\n branch,\n };\n pin.marker.label.dataset.branchMapPinLabel = \"\";\n pins.set(branch.id, pin);\n }\n pin.branch = branch;\n pin.marker.setPosition(branch.coordinates);\n const isFocused = branch.id === focused?.id;\n const spec = branchMarker(options?.pinImageUrl, isFocused);\n const button = pin.marker.element;\n button.setAttribute(\"aria-label\", branch.name);\n button.setAttribute(\"aria-pressed\", String(branch.id === selected));\n button.dataset.selected = String(branch.id === selected);\n button.dataset.focused = String(isFocused);\n paintMarker(button, spec, colors);\n const showLabel =\n options?.pinLabel === \"all\" ||\n (options?.pinLabel === \"selected\" && isFocused);\n pin.marker.setLabel(showLabel ? branch.name : undefined);\n pin.marker.label.dataset.selected = String(branch.id === selected);\n paintLabel(pin.marker.label, {\n offset: labelOffset(spec),\n capitalize: true,\n colors,\n });\n }\n\n const active = ensureClusterer(colors);\n const markerSet = [...pins.keys()].join(\"\\n\");\n if (markerSet !== clustered) {\n clustered = markerSet;\n active.clearMarkers(true);\n active.addMarkers(\n [...pins.values()].map(({ marker }) => clusterable(marker)),\n );\n }\n\n if (shopper) {\n if (!shopperPin) {\n const house = document.createElement(\"div\");\n house.setAttribute(\"role\", \"img\");\n house.setAttribute(\"aria-label\", \"Your location\");\n house.dataset.branchMapShopper = \"\";\n shopperPin = new Marker(\"shopper\", shopper, house);\n shopperPin.setLabel(\"You\");\n shopperPin.setMap(map);\n }\n shopperPin.setPosition(shopper);\n paintMarker(\n shopperPin.element,\n { kind: \"house\", color: colors.accentForeground },\n colors,\n );\n paintLabel(shopperPin.label, {\n offset: labelOffset({ kind: \"house\", color: \"\" }),\n capitalize: false,\n colors,\n });\n } else {\n shopperPin?.setMap(null);\n shopperPin = undefined;\n }\n\n if (shopper && focused) {\n route ??= new maps.Polyline({ map });\n route.setOptions({\n path: [shopper, focused.coordinates],\n strokeColor: colors.accent,\n strokeOpacity: ROUTE_OPACITY,\n strokeWeight: ROUTE_WEIGHT,\n clickable: false,\n });\n } else {\n route?.setMap(null);\n route = undefined;\n }\n\n const plan = fitPlan({\n branches: visible,\n shopper,\n focused,\n maxZoom: options?.maxZoom ?? 14,\n compact: options?.compact ?? false,\n });\n // Refit only when what the map frames changes. A new selection pans instead.\n const frame = JSON.stringify(plan ?? null);\n const previous = lastSelected;\n lastSelected = selected;\n if (frame !== fitted) {\n fitted = frame;\n if (plan) {\n fit(plan);\n }\n } else {\n const target = visible.find((branch) => branch.id === selected);\n if (target && selected !== previous && !shopper && !options?.compact) {\n map.panTo(target.coordinates);\n }\n }\n const marker = selected ? pins.get(selected)?.marker : undefined;\n if (marker && selected !== previous) {\n expandTo(marker);\n }\n },\n locate(location: Coordinates) {\n map.panTo(location);\n },\n destroy() {\n fitListener?.remove();\n stopExpanding?.();\n clusterer?.clearMarkers(true);\n clusterer?.setMap(null);\n for (const { marker } of pins.values()) {\n marker.setMap(null);\n marker.element.onclick = null;\n }\n pins.clear();\n shopperPin?.setMap(null);\n route?.setMap(null);\n maps.event.clearInstanceListeners(map);\n element.replaceChildren();\n },\n };\n}\n"],
|
|
5
|
+
"mappings": ";AAAA,SAAS,uBAAqC;AAE9C,SAAS,aAAa,SAAS,qBAAmC;AAClE;AAAA,EACE;AAAA,EACA;AAAA,OAEK;AACP,SAAS,gBAAgB;AACzB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAGK;AAYP,IAAM,cAAc,CAAC,WACnB;AAEF,IAAM,gBAAgB;AACtB,IAAM,eAAe;AACrB,IAAM,yBAAyB;AAC/B,IAAM,0BAA0B;AAChC,IAAM,8BAA8B;AAG7B,SAAS,YAAY,OAAe,QAAmB;AAC5D,QAAM,OAAO,YAAY,KAAK;AAC9B,QAAM,MAAM,kDAAkD,IAAI,aAAa,IAAI,kBAAkB,IAAI,IAAI,IAAI,iBAAiB,OAAO,CAAC,SAAS,OAAO,CAAC,QAAQ,OAAO,IAAI,CAAC,WAAW,OAAO,MAAM;AACvM,SAAO;AAAA,IACL;AAAA,IACA,KAAK,6BAA6B,KAAK,GAAG,CAAC;AAAA,IAC3C,OAAO;AAAA,MACL,MAAM,OAAO,KAAK;AAAA,MAClB,OAAO,OAAO;AAAA,MACd,UAAU;AAAA,MACV,YAAY;AAAA,IACd;AAAA,EACF;AACF;AAEA,eAAsB,UACpB,SACA,KACA,QACA,SACA;AACA,QAAM,OAAO,MAAM,SAAS,GAAG;AAC/B,SAAO,eAAe;AACtB,QAAM,MAAM,IAAI,KAAK,IAAI,SAAS;AAAA,IAChC,QAAQ,EAAE,KAAK,GAAG,KAAK,EAAE;AAAA,IACzB,MAAM;AAAA,IACN,gBAAgB,QAAQ;AAAA,IACxB,mBAAmB;AAAA,EACrB,CAAC;AACD,QAAM,SAAS,mBAAmB,IAAI;AACtC,QAAM,OAAO,oBAAI,IAAuD;AACxE,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI;AACJ,MAAI,SAAmC,MAAM;AAAA,EAAC;AAE9C,QAAM,MAAM,CAAC,SAAkB;AAC7B,iBAAa,OAAO;AACpB,kBAAc;AACd,QAAI,KAAK,SAAS,UAAU;AAC1B,UAAI,MAAM,KAAK,MAAM;AACrB,UAAI,QAAQ,KAAK,IAAI;AACrB;AAAA,IACF;AACA,QAAI,UAAU,KAAK,QAAQ,KAAK,OAAO;AACvC,kBAAc,KAAK,MAAM,gBAAgB,KAAK,QAAQ,MAAM;AAC1D,oBAAc;AACd,YAAM,OAAO,IAAI,QAAQ;AACzB,UAAI,OAAO,SAAS,YAAY,OAAO,KAAK,SAAS;AACnD,YAAI,QAAQ,KAAK,OAAO;AAAA,MAC1B;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,kBAAkB,CAAC,WAAsB;AAC7C,UAAMA,OAAM,GAAG,OAAO,MAAM,IAAI,OAAO,gBAAgB;AACvD,QAAI,aAAa,kBAAkBA,MAAK;AACtC,aAAO;AAAA,IACT;AACA,eAAW,aAAa,IAAI;AAC5B,eAAW,OAAO,IAAI;AACtB,oBAAgBA;AAChB,gBAAY;AAEZ,gBAAY,IAAI,gBAAgB;AAAA,MAC9B;AAAA,MACA,UAAU;AAAA,QACR,QAAQ,CAAC,EAAE,OAAO,SAAS,MAAM;AAC/B,gBAAM,OAAO,YAAY,OAAO,MAAM;AACtC,iBAAO,IAAI,KAAK,OAAO;AAAA,YACrB;AAAA,YACA,MAAM;AAAA,cACJ,KAAK,KAAK;AAAA,cACV,YAAY,IAAI,KAAK,KAAK,KAAK,MAAM,KAAK,IAAI;AAAA,YAChD;AAAA,YACA,OAAO,KAAK;AAAA,YACZ,QAAQ;AAAA,UACV,CAAC;AAAA,QACH;AAAA,MACF;AAAA,IACF,CAAC;AACD,WAAO;AAAA,EACT;AAGA,QAAM,WAAW,CAAC,WAA0B;AAC1C,oBAAgB;AAChB,UAAM,SAAS;AACf,QAAI,CAAC,QAAQ;AACX;AAAA,IACF;AACA,QAAI,WAAW;AACf,QAAI,YAAY;AAChB,QAAI;AACJ,UAAM,SAAS,MAAM;AACnB,UAAI,WAAW;AACb;AAAA,MACF;AACA,aAAO,OAAO;AACd,YAAM,UACJ,OACA,UAAU;AAAA,QAAK,CAAC,cAChB,UAAU,SAAS,SAAS,YAAY,MAAM,CAAC;AAAA,MACjD;AACA,UAAI,CAAC,WAAW,QAAQ,SAAS,GAAG;AAClC;AAAA,MACF;AACA,UAAI,aAAa,KAAK,QAAQ,QAAQ;AACpC,YAAI,UAAU,QAAQ,QAAQ,sBAAsB;AAAA,MACtD,OAAO;AACL,cAAM,OAAO,IAAI,QAAQ;AACzB,YAAI,OAAO,SAAS,YAAY,QAAQ,yBAAyB;AAC/D;AAAA,QACF;AACA,YAAI,QAAQ,KAAK,IAAI,OAAO,GAAG,uBAAuB,CAAC;AAAA,MACzD;AACA,kBAAY;AACZ,UAAI,WAAW,6BAA6B;AAC1C,eAAO,KAAK,MAAM,gBAAgB,KAAK,QAAQ,MAAM;AAAA,MACvD;AAAA,IACF;AACA,WAAO;AACP,oBAAgB,MAAM;AACpB,kBAAY;AACZ,YAAM,OAAO;AACb,sBAAgB;AAAA,IAClB;AAAA,EACF;AAEA,SAAO;AAAA,IACL,OACE,UACA,UACA,UACA,UACAC,UACA;AACA,eAAS;AACT,YAAM,SAASA,UAAS,UAAU;AAClC,UAAI,WAAW,EAAE,gBAAgBA,UAAS,kBAAkB,MAAM,CAAC;AACnE,YAAM,UAAU,gBAAgB,QAAQ;AACxC,YAAM,UAAU,iBAAiB,QAAQ,IAAI,WAAW;AACxD,YAAM,UAAU,cAAc,SAAS,UAAU,OAAO;AAExD,iBAAW,CAAC,IAAI,GAAG,KAAK,MAAM;AAC5B,YAAI,CAAC,QAAQ,KAAK,CAAC,WAAW,OAAO,OAAO,EAAE,GAAG;AAC/C,cAAI,OAAO,OAAO,IAAI;AACtB,eAAK,OAAO,EAAE;AAAA,QAChB;AAAA,MACF;AACA,iBAAW,UAAU,SAAS;AAC5B,YAAI,MAAM,KAAK,IAAI,OAAO,EAAE;AAC5B,YAAI,CAAC,KAAK;AACR,gBAAMC,UAAS,SAAS,cAAc,QAAQ;AAC9C,UAAAA,QAAO,OAAO;AACd,UAAAA,QAAO,QAAQ,eAAe;AAC9B,gBAAM,KAAK,OAAO;AAClB,UAAAA,QAAO,UAAU,MAAM;AACrB,kBAAM,UAAU,KAAK,IAAI,EAAE;AAC3B,gBAAI,SAAS;AACX,qBAAO,QAAQ,MAAM;AAAA,YACvB;AAAA,UACF;AACA,gBAAM;AAAA,YACJ,QAAQ,IAAI,OAAO,OAAO,IAAI,OAAO,aAAaA,OAAM;AAAA,YACxD;AAAA,UACF;AACA,cAAI,OAAO,MAAM,QAAQ,oBAAoB;AAC7C,eAAK,IAAI,OAAO,IAAI,GAAG;AAAA,QACzB;AACA,YAAI,SAAS;AACb,YAAI,OAAO,YAAY,OAAO,WAAW;AACzC,cAAM,YAAY,OAAO,OAAO,SAAS;AACzC,cAAM,OAAO,aAAaD,UAAS,aAAa,SAAS;AACzD,cAAM,SAAS,IAAI,OAAO;AAC1B,eAAO,aAAa,cAAc,OAAO,IAAI;AAC7C,eAAO,aAAa,gBAAgB,OAAO,OAAO,OAAO,QAAQ,CAAC;AAClE,eAAO,QAAQ,WAAW,OAAO,OAAO,OAAO,QAAQ;AACvD,eAAO,QAAQ,UAAU,OAAO,SAAS;AACzC,oBAAY,QAAQ,MAAM,MAAM;AAChC,cAAM,YACJA,UAAS,aAAa,SACrBA,UAAS,aAAa,cAAc;AACvC,YAAI,OAAO,SAAS,YAAY,OAAO,OAAO,MAAS;AACvD,YAAI,OAAO,MAAM,QAAQ,WAAW,OAAO,OAAO,OAAO,QAAQ;AACjE,mBAAW,IAAI,OAAO,OAAO;AAAA,UAC3B,QAAQ,YAAY,IAAI;AAAA,UACxB,YAAY;AAAA,UACZ;AAAA,QACF,CAAC;AAAA,MACH;AAEA,YAAM,SAAS,gBAAgB,MAAM;AACrC,YAAM,YAAY,CAAC,GAAG,KAAK,KAAK,CAAC,EAAE,KAAK,IAAI;AAC5C,UAAI,cAAc,WAAW;AAC3B,oBAAY;AACZ,eAAO,aAAa,IAAI;AACxB,eAAO;AAAA,UACL,CAAC,GAAG,KAAK,OAAO,CAAC,EAAE,IAAI,CAAC,EAAE,QAAAE,QAAO,MAAM,YAAYA,OAAM,CAAC;AAAA,QAC5D;AAAA,MACF;AAEA,UAAI,SAAS;AACX,YAAI,CAAC,YAAY;AACf,gBAAM,QAAQ,SAAS,cAAc,KAAK;AAC1C,gBAAM,aAAa,QAAQ,KAAK;AAChC,gBAAM,aAAa,cAAc,eAAe;AAChD,gBAAM,QAAQ,mBAAmB;AACjC,uBAAa,IAAI,OAAO,WAAW,SAAS,KAAK;AACjD,qBAAW,SAAS,KAAK;AACzB,qBAAW,OAAO,GAAG;AAAA,QACvB;AACA,mBAAW,YAAY,OAAO;AAC9B;AAAA,UACE,WAAW;AAAA,UACX,EAAE,MAAM,SAAS,OAAO,OAAO,iBAAiB;AAAA,UAChD;AAAA,QACF;AACA,mBAAW,WAAW,OAAO;AAAA,UAC3B,QAAQ,YAAY,EAAE,MAAM,SAAS,OAAO,GAAG,CAAC;AAAA,UAChD,YAAY;AAAA,UACZ;AAAA,QACF,CAAC;AAAA,MACH,OAAO;AACL,oBAAY,OAAO,IAAI;AACvB,qBAAa;AAAA,MACf;AAEA,UAAI,WAAW,SAAS;AACtB,0BAAU,IAAI,KAAK,SAAS,EAAE,IAAI,CAAC;AACnC,cAAM,WAAW;AAAA,UACf,MAAM,CAAC,SAAS,QAAQ,WAAW;AAAA,UACnC,aAAa,OAAO;AAAA,UACpB,eAAe;AAAA,UACf,cAAc;AAAA,UACd,WAAW;AAAA,QACb,CAAC;AAAA,MACH,OAAO;AACL,eAAO,OAAO,IAAI;AAClB,gBAAQ;AAAA,MACV;AAEA,YAAM,OAAO,QAAQ;AAAA,QACnB,UAAU;AAAA,QACV;AAAA,QACA;AAAA,QACA,SAASF,UAAS,WAAW;AAAA,QAC7B,SAASA,UAAS,WAAW;AAAA,MAC/B,CAAC;AAED,YAAM,QAAQ,KAAK,UAAU,QAAQ,IAAI;AACzC,YAAM,WAAW;AACjB,qBAAe;AACf,UAAI,UAAU,QAAQ;AACpB,iBAAS;AACT,YAAI,MAAM;AACR,cAAI,IAAI;AAAA,QACV;AAAA,MACF,OAAO;AACL,cAAM,SAAS,QAAQ,KAAK,CAAC,WAAW,OAAO,OAAO,QAAQ;AAC9D,YAAI,UAAU,aAAa,YAAY,CAAC,WAAW,CAACA,UAAS,SAAS;AACpE,cAAI,MAAM,OAAO,WAAW;AAAA,QAC9B;AAAA,MACF;AACA,YAAM,SAAS,WAAW,KAAK,IAAI,QAAQ,GAAG,SAAS;AACvD,UAAI,UAAU,aAAa,UAAU;AACnC,iBAAS,MAAM;AAAA,MACjB;AAAA,IACF;AAAA,IACA,OAAO,UAAuB;AAC5B,UAAI,MAAM,QAAQ;AAAA,IACpB;AAAA,IACA,UAAU;AACR,mBAAa,OAAO;AACpB,sBAAgB;AAChB,iBAAW,aAAa,IAAI;AAC5B,iBAAW,OAAO,IAAI;AACtB,iBAAW,EAAE,OAAO,KAAK,KAAK,OAAO,GAAG;AACtC,eAAO,OAAO,IAAI;AAClB,eAAO,QAAQ,UAAU;AAAA,MAC3B;AACA,WAAK,MAAM;AACX,kBAAY,OAAO,IAAI;AACvB,aAAO,OAAO,IAAI;AAClB,WAAK,MAAM,uBAAuB,GAAG;AACrC,cAAQ,gBAAgB;AAAA,IAC1B;AAAA,EACF;AACF;",
|
|
6
|
+
"names": ["key", "options", "button", "marker"]
|
|
7
7
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// src/branch-map/use-map-runtime.ts
|
|
2
|
+
import { useEffect, useRef, useState } from "react";
|
|
3
|
+
function useMapRuntime(browserKey, enabled, load, onLoadError) {
|
|
4
|
+
const element = useRef(null);
|
|
5
|
+
const [runtime, setRuntime] = useState();
|
|
6
|
+
const [failure, setFailure] = useState();
|
|
7
|
+
const [attempt, setAttempt] = useState(0);
|
|
8
|
+
const loader = useRef(load);
|
|
9
|
+
const reportError = useRef(onLoadError);
|
|
10
|
+
useEffect(() => {
|
|
11
|
+
loader.current = load;
|
|
12
|
+
reportError.current = onLoadError;
|
|
13
|
+
}, [load, onLoadError]);
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!browserKey || !element.current || !enabled) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
let disposed = false;
|
|
19
|
+
const controller = new AbortController();
|
|
20
|
+
let instance;
|
|
21
|
+
const target = element.current;
|
|
22
|
+
setFailure(void 0);
|
|
23
|
+
void loader.current(target, browserKey, controller.signal).then((map) => {
|
|
24
|
+
instance = map;
|
|
25
|
+
if (disposed) {
|
|
26
|
+
map.destroy();
|
|
27
|
+
} else {
|
|
28
|
+
setRuntime(map);
|
|
29
|
+
}
|
|
30
|
+
}).catch((error) => {
|
|
31
|
+
if (!disposed) {
|
|
32
|
+
setFailure({ error });
|
|
33
|
+
reportError.current?.(error);
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
return () => {
|
|
37
|
+
disposed = true;
|
|
38
|
+
controller.abort();
|
|
39
|
+
instance?.destroy();
|
|
40
|
+
setRuntime(void 0);
|
|
41
|
+
};
|
|
42
|
+
}, [browserKey, enabled, attempt]);
|
|
43
|
+
return {
|
|
44
|
+
element,
|
|
45
|
+
runtime,
|
|
46
|
+
failure,
|
|
47
|
+
retry: () => setAttempt((count) => count + 1)
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
useMapRuntime
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=use-map-runtime.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/branch-map/use-map-runtime.ts"],
|
|
4
|
+
"sourcesContent": ["import { useEffect, useRef, useState } from \"react\";\n\n/** What a template gets to render when a map fails to load. */\nexport type MapLoadErrorState = {\n /** Why the map failed to load. */\n error: unknown;\n /** Load the map again. */\n retry: () => void;\n};\n\n/** Load a map runtime after mount, retrying on request and cleaning up on unmount. */\nexport function useMapRuntime<T extends { destroy(): void }>(\n browserKey: string | undefined,\n enabled: boolean,\n load: (target: HTMLElement, key: string, signal: AbortSignal) => Promise<T>,\n onLoadError: ((error: unknown) => void) | undefined,\n) {\n const element = useRef<HTMLDivElement>(null);\n const [runtime, setRuntime] = useState<T>();\n const [failure, setFailure] = useState<{ error: unknown }>();\n const [attempt, setAttempt] = useState(0);\n const loader = useRef(load);\n const reportError = useRef(onLoadError);\n useEffect(() => {\n loader.current = load;\n reportError.current = onLoadError;\n }, [load, onLoadError]);\n useEffect(() => {\n if (!browserKey || !element.current || !enabled) {\n return;\n }\n let disposed = false;\n const controller = new AbortController();\n let instance: T | undefined;\n const target = element.current;\n setFailure(undefined);\n void loader\n .current(target, browserKey, controller.signal)\n .then((map) => {\n instance = map;\n if (disposed) {\n map.destroy();\n } else {\n setRuntime(map);\n }\n })\n .catch((error: unknown) => {\n if (!disposed) {\n setFailure({ error });\n reportError.current?.(error);\n }\n });\n return () => {\n disposed = true;\n controller.abort();\n instance?.destroy();\n setRuntime(undefined);\n };\n }, [browserKey, enabled, attempt]);\n return {\n element,\n runtime,\n failure,\n retry: () => setAttempt((count) => count + 1),\n };\n}\n"],
|
|
5
|
+
"mappings": ";AAAA,SAAS,WAAW,QAAQ,gBAAgB;AAWrC,SAAS,cACd,YACA,SACA,MACA,aACA;AACA,QAAM,UAAU,OAAuB,IAAI;AAC3C,QAAM,CAAC,SAAS,UAAU,IAAI,SAAY;AAC1C,QAAM,CAAC,SAAS,UAAU,IAAI,SAA6B;AAC3D,QAAM,CAAC,SAAS,UAAU,IAAI,SAAS,CAAC;AACxC,QAAM,SAAS,OAAO,IAAI;AAC1B,QAAM,cAAc,OAAO,WAAW;AACtC,YAAU,MAAM;AACd,WAAO,UAAU;AACjB,gBAAY,UAAU;AAAA,EACxB,GAAG,CAAC,MAAM,WAAW,CAAC;AACtB,YAAU,MAAM;AACd,QAAI,CAAC,cAAc,CAAC,QAAQ,WAAW,CAAC,SAAS;AAC/C;AAAA,IACF;AACA,QAAI,WAAW;AACf,UAAM,aAAa,IAAI,gBAAgB;AACvC,QAAI;AACJ,UAAM,SAAS,QAAQ;AACvB,eAAW,MAAS;AACpB,SAAK,OACF,QAAQ,QAAQ,YAAY,WAAW,MAAM,EAC7C,KAAK,CAAC,QAAQ;AACb,iBAAW;AACX,UAAI,UAAU;AACZ,YAAI,QAAQ;AAAA,MACd,OAAO;AACL,mBAAW,GAAG;AAAA,MAChB;AAAA,IACF,CAAC,EACA,MAAM,CAAC,UAAmB;AACzB,UAAI,CAAC,UAAU;AACb,mBAAW,EAAE,MAAM,CAAC;AACpB,oBAAY,UAAU,KAAK;AAAA,MAC7B;AAAA,IACF,CAAC;AACH,WAAO,MAAM;AACX,iBAAW;AACX,iBAAW,MAAM;AACjB,gBAAU,QAAQ;AAClB,iBAAW,MAAS;AAAA,IACtB;AAAA,EACF,GAAG,CAAC,YAAY,SAAS,OAAO,CAAC;AACjC,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,OAAO,MAAM,WAAW,CAAC,UAAU,QAAQ,CAAC;AAAA,EAC9C;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
// src/deferred/index.tsx
|
|
4
|
+
import {
|
|
5
|
+
Fragment,
|
|
6
|
+
useEffect,
|
|
7
|
+
useMemo,
|
|
8
|
+
useState
|
|
9
|
+
} from "react";
|
|
10
|
+
import { jsx } from "react/jsx-runtime";
|
|
11
|
+
function component(value) {
|
|
12
|
+
if (typeof value === "object" && value !== null && "default" in value && !("$$typeof" in value)) {
|
|
13
|
+
return value.default;
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
var ignore = () => {
|
|
18
|
+
};
|
|
19
|
+
function deferred(load) {
|
|
20
|
+
let loaded;
|
|
21
|
+
let pending;
|
|
22
|
+
const preload = () => {
|
|
23
|
+
if (loaded) {
|
|
24
|
+
return Promise.resolve(loaded);
|
|
25
|
+
}
|
|
26
|
+
if (!pending) {
|
|
27
|
+
const attempt = new Promise((resolve) => resolve(load())).then(
|
|
28
|
+
(value) => {
|
|
29
|
+
loaded = component(value);
|
|
30
|
+
return loaded;
|
|
31
|
+
},
|
|
32
|
+
(error) => {
|
|
33
|
+
if (pending === attempt) {
|
|
34
|
+
pending = void 0;
|
|
35
|
+
}
|
|
36
|
+
throw error;
|
|
37
|
+
}
|
|
38
|
+
);
|
|
39
|
+
attempt.catch(ignore);
|
|
40
|
+
pending = attempt;
|
|
41
|
+
}
|
|
42
|
+
return pending;
|
|
43
|
+
};
|
|
44
|
+
const preloadWhenIdle = (options = {}) => {
|
|
45
|
+
if (typeof window === "undefined" || loaded) {
|
|
46
|
+
return ignore;
|
|
47
|
+
}
|
|
48
|
+
const start = () => {
|
|
49
|
+
preload().catch(ignore);
|
|
50
|
+
};
|
|
51
|
+
if (typeof window.requestIdleCallback === "function") {
|
|
52
|
+
const handle2 = window.requestIdleCallback(
|
|
53
|
+
start,
|
|
54
|
+
options.timeout === void 0 ? void 0 : { timeout: options.timeout }
|
|
55
|
+
);
|
|
56
|
+
return () => window.cancelIdleCallback(handle2);
|
|
57
|
+
}
|
|
58
|
+
const handle = window.setTimeout(start, options.timeout ?? 1);
|
|
59
|
+
return () => window.clearTimeout(handle);
|
|
60
|
+
};
|
|
61
|
+
function DeferredComponent({ fallback = null, ...props }) {
|
|
62
|
+
const { open } = props;
|
|
63
|
+
const [ready, setReady] = useState(() => loaded);
|
|
64
|
+
const [opened, setOpened] = useState(open);
|
|
65
|
+
if (open && !opened) {
|
|
66
|
+
setOpened(true);
|
|
67
|
+
}
|
|
68
|
+
const Loaded = ready ?? loaded;
|
|
69
|
+
useEffect(() => {
|
|
70
|
+
if (!open || Loaded) {
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
let active = true;
|
|
74
|
+
preload().then((value) => {
|
|
75
|
+
if (active) {
|
|
76
|
+
setReady(() => value);
|
|
77
|
+
}
|
|
78
|
+
}, ignore);
|
|
79
|
+
return () => {
|
|
80
|
+
active = false;
|
|
81
|
+
};
|
|
82
|
+
}, [open, Loaded]);
|
|
83
|
+
if (!(open || opened)) {
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
if (!Loaded) {
|
|
87
|
+
return open ? /* @__PURE__ */ jsx(Fragment, { children: fallback }) : null;
|
|
88
|
+
}
|
|
89
|
+
return /* @__PURE__ */ jsx(Loaded, { ...props });
|
|
90
|
+
}
|
|
91
|
+
DeferredComponent.displayName = "Deferred";
|
|
92
|
+
return {
|
|
93
|
+
// The load resolved to the component typed by T.
|
|
94
|
+
preload,
|
|
95
|
+
preloadWhenIdle,
|
|
96
|
+
Component: DeferredComponent
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function useIntentPreload(target) {
|
|
100
|
+
return useMemo(() => {
|
|
101
|
+
const start = () => {
|
|
102
|
+
target.preload().catch(ignore);
|
|
103
|
+
};
|
|
104
|
+
return { onPointerEnter: start, onPointerDown: start, onFocus: start };
|
|
105
|
+
}, [target]);
|
|
106
|
+
}
|
|
107
|
+
export {
|
|
108
|
+
deferred,
|
|
109
|
+
useIntentPreload
|
|
110
|
+
};
|
|
111
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/deferred/index.tsx"],
|
|
4
|
+
"sourcesContent": ["\"use client\";\nimport {\n Fragment,\n useEffect,\n useMemo,\n useState,\n type ComponentType,\n type ReactElement,\n type ReactNode,\n} from \"react\";\n\n/** Props for a deferred component: its own props plus `open`, which the helper reads and passes on. */\nexport type DeferredProps<P> = Omit<P, \"open\"> & {\n /**\n * Load the code if needed and render the component. Before the first open\n * it renders nothing; after it, the component stays mounted and receives\n * `open`, so it can animate out and return focus when it closes.\n */\n open: boolean;\n /** Shown while `open` is true and the code is still loading, for inline parts that should hold their place. */\n fallback?: ReactNode;\n};\n\n/** Options for `preloadWhenIdle`. */\nexport type DeferredIdleOptions = {\n /** Start the load after this many milliseconds even if the browser never goes idle. */\n timeout?: number;\n};\n\n/**\n * A component whose code loads on demand and renders without suspending.\n * Create it once at module scope with `deferred()`.\n */\nexport type Deferred<P> = {\n /**\n * Start loading the code. Repeated calls share one load. A failed load is\n * forgotten, so the next call tries again.\n * @returns The loaded component, or the load error.\n */\n preload(): Promise<ComponentType<P>>;\n /**\n * Load the code once the browser is idle. Use it only for small chunks.\n * Does nothing on the server.\n * @returns A function that cancels the scheduled load.\n */\n preloadWhenIdle(options?: DeferredIdleOptions): () => void;\n /**\n * Renders nothing until `open` is first true and the code is in. When\n * `open` turns true it loads the code and renders as soon as it arrives. It\n * never suspends. After the first open the component stays mounted and\n * every prop passes through, `open` included, so it can run its own exit.\n */\n Component: (props: DeferredProps<P>) => ReactElement | null;\n};\n\n/** Event handlers that start loading a deferred component when a shopper reaches for its trigger. */\nexport type IntentPreloadHandlers = {\n onPointerEnter: () => void;\n onPointerDown: () => void;\n onFocus: () => void;\n};\n\n/** What a deferred load resolves to: a component, or a module whose default export is one. */\nexport type DeferredModule =\n ComponentType<never> | { default: ComponentType<never> };\n\n/** The props of the component a deferred load resolves to. */\nexport type DeferredModuleProps<T> = T extends {\n default: ComponentType<infer P>;\n}\n ? P\n : T extends ComponentType<infer P>\n ? P\n : never;\n\ntype AnyComponent = ComponentType<Record<string, unknown>>;\n\nfunction component(value: DeferredModule): AnyComponent {\n if (\n typeof value === \"object\" &&\n value !== null &&\n \"default\" in value &&\n !(\"$$typeof\" in value)\n ) {\n return value.default as AnyComponent;\n }\n return value as AnyComponent;\n}\n\nconst ignore = () => {};\n\n/**\n * Load a component's code on demand without `React.lazy` or `<Suspense>`.\n * React holds a suspended boundary's first reveal for about 300 ms, so a\n * lazy overlay opens late. A deferred one renders on the frame its code\n * arrives. Pass a module whose default export is the component, or resolve\n * to the component itself.\n *\n * @example\n * const Bag = deferred(() => import(\"./bag-sheet\"));\n * const Finder = deferred(() => import(\"./finder\").then((m) => m.Finder));\n * <Bag.Component open={open} onClose={close} />\n */\nexport function deferred<T extends DeferredModule>(\n load: () => Promise<T>,\n): Deferred<DeferredModuleProps<T>> {\n type P = DeferredModuleProps<T>;\n let loaded: AnyComponent | undefined;\n let pending: Promise<AnyComponent> | undefined;\n\n const preload = (): Promise<AnyComponent> => {\n if (loaded) {\n return Promise.resolve(loaded);\n }\n if (!pending) {\n // Starts the import now; a load that throws becomes a rejection.\n const attempt = new Promise<T>((resolve) => resolve(load())).then(\n (value) => {\n loaded = component(value);\n return loaded;\n },\n (error: unknown) => {\n // Forget the failure so the next open or preload tries again.\n if (pending === attempt) {\n pending = undefined;\n }\n throw error;\n },\n );\n // Callers may ignore the promise; the component reports nothing either.\n attempt.catch(ignore);\n pending = attempt;\n }\n return pending;\n };\n\n const preloadWhenIdle = (options: DeferredIdleOptions = {}) => {\n if (typeof window === \"undefined\" || loaded) {\n return ignore;\n }\n const start = () => {\n preload().catch(ignore);\n };\n if (typeof window.requestIdleCallback === \"function\") {\n const handle = window.requestIdleCallback(\n start,\n options.timeout === undefined\n ? undefined\n : { timeout: options.timeout },\n );\n return () => window.cancelIdleCallback(handle);\n }\n const handle = window.setTimeout(start, options.timeout ?? 1);\n return () => window.clearTimeout(handle);\n };\n\n function DeferredComponent({ fallback = null, ...props }: DeferredProps<P>) {\n const { open } = props;\n const [ready, setReady] = useState<AnyComponent | undefined>(() => loaded);\n const [opened, setOpened] = useState<boolean>(open);\n if (open && !opened) {\n setOpened(true);\n }\n const Loaded = ready ?? loaded;\n useEffect(() => {\n if (!open || Loaded) {\n return;\n }\n let active = true;\n preload().then((value) => {\n if (active) {\n setReady(() => value);\n }\n }, ignore);\n return () => {\n active = false;\n };\n }, [open, Loaded]);\n if (!(open || opened)) {\n return null;\n }\n if (!Loaded) {\n return open ? <Fragment>{fallback}</Fragment> : null;\n }\n return <Loaded {...(props as Record<string, unknown>)} />;\n }\n DeferredComponent.displayName = \"Deferred\";\n\n return {\n // The load resolved to the component typed by T.\n preload: preload as () => Promise<ComponentType<P>>,\n preloadWhenIdle,\n Component: DeferredComponent,\n };\n}\n\n/**\n * Start loading a deferred component when the shopper points at, presses or\n * focuses its trigger, so the code is usually in by the click. Spread the\n * result on the trigger.\n *\n * @example\n * const intent = useIntentPreload(Bag);\n * <button {...intent} onClick={openBag}>Bag</button>\n */\nexport function useIntentPreload(target: {\n preload(): Promise<unknown>;\n}): IntentPreloadHandlers {\n return useMemo(() => {\n const start = () => {\n target.preload().catch(ignore);\n };\n return { onPointerEnter: start, onPointerDown: start, onFocus: start };\n }, [target]);\n}\n"],
|
|
5
|
+
"mappings": ";;;AACA;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AA6Ka;AAzGpB,SAAS,UAAU,OAAqC;AACtD,MACE,OAAO,UAAU,YACjB,UAAU,QACV,aAAa,SACb,EAAE,cAAc,QAChB;AACA,WAAO,MAAM;AAAA,EACf;AACA,SAAO;AACT;AAEA,IAAM,SAAS,MAAM;AAAC;AAcf,SAAS,SACd,MACkC;AAElC,MAAI;AACJ,MAAI;AAEJ,QAAM,UAAU,MAA6B;AAC3C,QAAI,QAAQ;AACV,aAAO,QAAQ,QAAQ,MAAM;AAAA,IAC/B;AACA,QAAI,CAAC,SAAS;AAEZ,YAAM,UAAU,IAAI,QAAW,CAAC,YAAY,QAAQ,KAAK,CAAC,CAAC,EAAE;AAAA,QAC3D,CAAC,UAAU;AACT,mBAAS,UAAU,KAAK;AACxB,iBAAO;AAAA,QACT;AAAA,QACA,CAAC,UAAmB;AAElB,cAAI,YAAY,SAAS;AACvB,sBAAU;AAAA,UACZ;AACA,gBAAM;AAAA,QACR;AAAA,MACF;AAEA,cAAQ,MAAM,MAAM;AACpB,gBAAU;AAAA,IACZ;AACA,WAAO;AAAA,EACT;AAEA,QAAM,kBAAkB,CAAC,UAA+B,CAAC,MAAM;AAC7D,QAAI,OAAO,WAAW,eAAe,QAAQ;AAC3C,aAAO;AAAA,IACT;AACA,UAAM,QAAQ,MAAM;AAClB,cAAQ,EAAE,MAAM,MAAM;AAAA,IACxB;AACA,QAAI,OAAO,OAAO,wBAAwB,YAAY;AACpD,YAAMA,UAAS,OAAO;AAAA,QACpB;AAAA,QACA,QAAQ,YAAY,SAChB,SACA,EAAE,SAAS,QAAQ,QAAQ;AAAA,MACjC;AACA,aAAO,MAAM,OAAO,mBAAmBA,OAAM;AAAA,IAC/C;AACA,UAAM,SAAS,OAAO,WAAW,OAAO,QAAQ,WAAW,CAAC;AAC5D,WAAO,MAAM,OAAO,aAAa,MAAM;AAAA,EACzC;AAEA,WAAS,kBAAkB,EAAE,WAAW,MAAM,GAAG,MAAM,GAAqB;AAC1E,UAAM,EAAE,KAAK,IAAI;AACjB,UAAM,CAAC,OAAO,QAAQ,IAAI,SAAmC,MAAM,MAAM;AACzE,UAAM,CAAC,QAAQ,SAAS,IAAI,SAAkB,IAAI;AAClD,QAAI,QAAQ,CAAC,QAAQ;AACnB,gBAAU,IAAI;AAAA,IAChB;AACA,UAAM,SAAS,SAAS;AACxB,cAAU,MAAM;AACd,UAAI,CAAC,QAAQ,QAAQ;AACnB;AAAA,MACF;AACA,UAAI,SAAS;AACb,cAAQ,EAAE,KAAK,CAAC,UAAU;AACxB,YAAI,QAAQ;AACV,mBAAS,MAAM,KAAK;AAAA,QACtB;AAAA,MACF,GAAG,MAAM;AACT,aAAO,MAAM;AACX,iBAAS;AAAA,MACX;AAAA,IACF,GAAG,CAAC,MAAM,MAAM,CAAC;AACjB,QAAI,EAAE,QAAQ,SAAS;AACrB,aAAO;AAAA,IACT;AACA,QAAI,CAAC,QAAQ;AACX,aAAO,OAAO,oBAAC,YAAU,oBAAS,IAAc;AAAA,IAClD;AACA,WAAO,oBAAC,UAAQ,GAAI,OAAmC;AAAA,EACzD;AACA,oBAAkB,cAAc;AAEhC,SAAO;AAAA;AAAA,IAEL;AAAA,IACA;AAAA,IACA,WAAW;AAAA,EACb;AACF;AAWO,SAAS,iBAAiB,QAEP;AACxB,SAAO,QAAQ,MAAM;AACnB,UAAM,QAAQ,MAAM;AAClB,aAAO,QAAQ,EAAE,MAAM,MAAM;AAAA,IAC/B;AACA,WAAO,EAAE,gBAAgB,OAAO,eAAe,OAAO,SAAS,MAAM;AAAA,EACvE,GAAG,CAAC,MAAM,CAAC;AACb;",
|
|
6
|
+
"names": ["handle"]
|
|
7
|
+
}
|