@zappie/foundations 0.1.4 → 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 +14 -0
- package/README.md +68 -13
- 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 -30
- package/dist/branch-map/geometry.js.map +2 -2
- package/dist/branch-map/index.js +21 -53
- 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 +235 -99
- 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/index.d.ts +56 -10
- package/dist/index.js +2 -1
- package/dist/index.js.map +2 -2
- package/dist/item-form/root.js.map +1 -1
- package/dist/overlay/react.js +224 -13
- package/dist/overlay/react.js.map +2 -2
- package/dist/version.js +1 -1
- package/package.json +3 -2
|
@@ -1,12 +1,39 @@
|
|
|
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
|
-
|
|
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
|
+
}
|
|
10
37
|
async function createMap(element, key, signal, options) {
|
|
11
38
|
const maps = await loadMaps(key);
|
|
12
39
|
signal.throwIfAborted();
|
|
@@ -16,130 +43,229 @@ async function createMap(element, key, signal, options) {
|
|
|
16
43
|
mapTypeControl: options.mapTypeControl,
|
|
17
44
|
streetViewControl: false
|
|
18
45
|
});
|
|
19
|
-
|
|
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;
|
|
20
53
|
let fitListener;
|
|
54
|
+
let stopExpanding;
|
|
21
55
|
let fitted;
|
|
22
56
|
let lastSelected;
|
|
23
|
-
|
|
24
|
-
constructor(position, pin) {
|
|
25
|
-
super();
|
|
26
|
-
this.position = position;
|
|
27
|
-
const { label, selected, imageUrl } = pin;
|
|
28
|
-
const size = imageUrl ? `${selected ? 40 : 32}px` : "var(--branch-map-pin-size,24px)";
|
|
29
|
-
this.button = document.createElement("button");
|
|
30
|
-
this.button.type = "button";
|
|
31
|
-
this.button.setAttribute("aria-label", label);
|
|
32
|
-
this.button.setAttribute("aria-pressed", String(selected));
|
|
33
|
-
this.button.dataset.selected = String(selected);
|
|
34
|
-
this.button.dataset.branchMapPin = "";
|
|
35
|
-
this.button.style.cssText = imageUrl ? `position:absolute;transform:translate(-50%,-100%);width:${size};height:${size};padding:0;border-radius:50%;overflow:hidden;border:2px solid white;background:white;box-shadow:${selected ? "0 2px 6px rgb(0 0 0 / 35%)" : "0 1px 3px rgb(0 0 0 / 25%)"};cursor:pointer` : `position:absolute;transform:translate(-50%,-100%);width:${size};height:${size};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`;
|
|
36
|
-
if (imageUrl) {
|
|
37
|
-
const image = document.createElement("img");
|
|
38
|
-
image.alt = "";
|
|
39
|
-
image.decoding = "async";
|
|
40
|
-
image.style.cssText = "display:block;width:100%;height:100%;object-fit:cover";
|
|
41
|
-
image.src = imageUrl;
|
|
42
|
-
this.button.appendChild(image);
|
|
43
|
-
}
|
|
44
|
-
if (pin.caption) {
|
|
45
|
-
this.caption = document.createElement("span");
|
|
46
|
-
this.caption.textContent = pin.caption;
|
|
47
|
-
this.caption.setAttribute("aria-hidden", "true");
|
|
48
|
-
this.caption.dataset.branchMapPinLabel = "";
|
|
49
|
-
this.caption.dataset.selected = String(selected);
|
|
50
|
-
this.caption.style.cssText = `position:absolute;transform:translate(-50%,calc(-100% - ${size} - 4px));white-space:nowrap;pointer-events:none;padding:2px 8px;border-radius:4px;font-size:12px;line-height:16px;font-weight:500;background:var(--branch-map-label-background,white);color:var(--branch-map-label-color,#111);border:1px solid var(--branch-map-label-border,transparent);box-shadow:0 1px 3px rgb(0 0 0 / 20%)`;
|
|
51
|
-
}
|
|
52
|
-
this.button.onclick = pin.onClick ?? null;
|
|
53
|
-
}
|
|
54
|
-
onAdd() {
|
|
55
|
-
const pane = this.getPanes()?.overlayMouseTarget;
|
|
56
|
-
pane?.appendChild(this.button);
|
|
57
|
-
if (this.caption) {
|
|
58
|
-
pane?.appendChild(this.caption);
|
|
59
|
-
}
|
|
60
|
-
maps.OverlayView.preventMapHitsAndGesturesFrom(this.button);
|
|
61
|
-
}
|
|
62
|
-
draw() {
|
|
63
|
-
const point = this.getProjection().fromLatLngToDivPixel(
|
|
64
|
-
new maps.LatLng(this.position)
|
|
65
|
-
);
|
|
66
|
-
if (point) {
|
|
67
|
-
for (const node of [this.button, this.caption]) {
|
|
68
|
-
if (node) {
|
|
69
|
-
node.style.left = `${point.x}px`;
|
|
70
|
-
node.style.top = `${point.y}px`;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
onRemove() {
|
|
76
|
-
this.button.remove();
|
|
77
|
-
this.caption?.remove();
|
|
78
|
-
this.button.onclick = null;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
const clear = () => {
|
|
82
|
-
pins.forEach((pin) => pin.setMap(null));
|
|
83
|
-
pins = [];
|
|
57
|
+
let select = () => {
|
|
84
58
|
};
|
|
85
|
-
const fit = (
|
|
59
|
+
const fit = (plan) => {
|
|
86
60
|
fitListener?.remove();
|
|
87
61
|
fitListener = void 0;
|
|
88
|
-
|
|
89
|
-
|
|
62
|
+
if (plan.kind === "center") {
|
|
63
|
+
map.panTo(plan.center);
|
|
64
|
+
map.setZoom(plan.zoom);
|
|
90
65
|
return;
|
|
91
66
|
}
|
|
92
|
-
map.fitBounds(bounds,
|
|
67
|
+
map.fitBounds(plan.bounds, plan.padding);
|
|
93
68
|
fitListener = maps.event.addListenerOnce(map, "idle", () => {
|
|
94
69
|
fitListener = void 0;
|
|
95
70
|
const zoom = map.getZoom();
|
|
96
|
-
if (typeof zoom === "number" && zoom > maxZoom) {
|
|
97
|
-
map.setZoom(maxZoom);
|
|
71
|
+
if (typeof zoom === "number" && zoom > plan.maxZoom) {
|
|
72
|
+
map.setZoom(plan.maxZoom);
|
|
98
73
|
}
|
|
99
74
|
});
|
|
100
75
|
};
|
|
76
|
+
const ensureClusterer = (colors) => {
|
|
77
|
+
const key2 = `${colors.accent}|${colors.accentForeground}`;
|
|
78
|
+
if (clusterer && clusterColors === key2) {
|
|
79
|
+
return clusterer;
|
|
80
|
+
}
|
|
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;
|
|
109
|
+
}
|
|
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
|
+
};
|
|
144
|
+
};
|
|
101
145
|
return {
|
|
102
146
|
update(branches, selected, onSelect, location, options2) {
|
|
103
|
-
|
|
147
|
+
select = onSelect;
|
|
148
|
+
const colors = options2?.colors ?? DEFAULT_COLORS;
|
|
104
149
|
map.setOptions({ mapTypeControl: options2?.mapTypeControl ?? false });
|
|
105
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
|
+
}
|
|
106
159
|
for (const branch of visible) {
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
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
|
|
114
196
|
});
|
|
115
|
-
pin.setMap(map);
|
|
116
|
-
pins.push(pin);
|
|
117
197
|
}
|
|
118
|
-
const
|
|
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
|
+
}
|
|
119
207
|
if (shopper) {
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
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
|
|
222
|
+
);
|
|
223
|
+
paintLabel(shopperPin.label, {
|
|
224
|
+
offset: labelOffset({ kind: "house", color: "" }),
|
|
225
|
+
capitalize: false,
|
|
226
|
+
colors
|
|
123
227
|
});
|
|
124
|
-
|
|
125
|
-
|
|
228
|
+
} else {
|
|
229
|
+
shopperPin?.setMap(null);
|
|
230
|
+
shopperPin = void 0;
|
|
126
231
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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;
|
|
244
|
+
}
|
|
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);
|
|
133
253
|
const previous = lastSelected;
|
|
134
254
|
lastSelected = selected;
|
|
135
255
|
if (frame !== fitted) {
|
|
136
256
|
fitted = frame;
|
|
137
|
-
|
|
138
|
-
|
|
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
|
+
}
|
|
139
265
|
}
|
|
140
|
-
const
|
|
141
|
-
if (
|
|
142
|
-
|
|
266
|
+
const marker = selected ? pins.get(selected)?.marker : void 0;
|
|
267
|
+
if (marker && selected !== previous) {
|
|
268
|
+
expandTo(marker);
|
|
143
269
|
}
|
|
144
270
|
},
|
|
145
271
|
locate(location) {
|
|
@@ -147,13 +273,23 @@ async function createMap(element, key, signal, options) {
|
|
|
147
273
|
},
|
|
148
274
|
destroy() {
|
|
149
275
|
fitListener?.remove();
|
|
150
|
-
|
|
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);
|
|
151
286
|
maps.event.clearInstanceListeners(map);
|
|
152
287
|
element.replaceChildren();
|
|
153
288
|
}
|
|
154
289
|
};
|
|
155
290
|
}
|
|
156
291
|
export {
|
|
292
|
+
clusterIcon,
|
|
157
293
|
createMap
|
|
158
294
|
};
|
|
159
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\n/** Padding around fitted branches, and around branches plus the shopper. */\nconst BRANCH_PADDING = 32;\nconst SHOPPER_PADDING = 88;\n\nexport type MapUpdateOptions = {\n maxZoom: number;\n pinImageUrl: string | undefined;\n mapTypeControl: boolean;\n pinLabel?: \"selected\" | \"all\" | undefined;\n};\n\ntype PinOptions = {\n label: string;\n selected: boolean;\n imageUrl?: string | undefined;\n /** Text shown above the pin. */\n caption?: string | undefined;\n onClick?: () => void;\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 let pins: google.maps.OverlayView[] = [];\n let fitListener: google.maps.MapsEventListener | undefined;\n let fitted: string | undefined;\n let lastSelected: string | undefined;\n class Pin extends maps.OverlayView {\n private button: HTMLButtonElement;\n private caption: HTMLElement | undefined;\n constructor(\n private position: Coordinates,\n pin: PinOptions,\n ) {\n super();\n const { label, selected, imageUrl } = pin;\n const size = imageUrl\n ? `${selected ? 40 : 32}px`\n : \"var(--branch-map-pin-size,24px)\";\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 = imageUrl\n ? `position:absolute;transform:translate(-50%,-100%);width:${size};height:${size};padding:0;border-radius:50%;overflow:hidden;border:2px solid white;background:white;box-shadow:${selected ? \"0 2px 6px rgb(0 0 0 / 35%)\" : \"0 1px 3px rgb(0 0 0 / 25%)\"};cursor:pointer`\n : `position:absolute;transform:translate(-50%,-100%);width:${size};height:${size};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 if (imageUrl) {\n const image = document.createElement(\"img\");\n image.alt = \"\";\n image.decoding = \"async\";\n image.style.cssText =\n \"display:block;width:100%;height:100%;object-fit:cover\";\n image.src = imageUrl;\n this.button.appendChild(image);\n }\n if (pin.caption) {\n // The button already carries the name, so the caption is visual only.\n this.caption = document.createElement(\"span\");\n this.caption.textContent = pin.caption;\n this.caption.setAttribute(\"aria-hidden\", \"true\");\n this.caption.dataset.branchMapPinLabel = \"\";\n this.caption.dataset.selected = String(selected);\n this.caption.style.cssText = `position:absolute;transform:translate(-50%,calc(-100% - ${size} - 4px));white-space:nowrap;pointer-events:none;padding:2px 8px;border-radius:4px;font-size:12px;line-height:16px;font-weight:500;background:var(--branch-map-label-background,white);color:var(--branch-map-label-color,#111);border:1px solid var(--branch-map-label-border,transparent);box-shadow:0 1px 3px rgb(0 0 0 / 20%)`;\n }\n this.button.onclick = pin.onClick ?? null;\n }\n onAdd() {\n const pane = this.getPanes()?.overlayMouseTarget;\n pane?.appendChild(this.button);\n if (this.caption) {\n pane?.appendChild(this.caption);\n }\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 for (const node of [this.button, this.caption]) {\n if (node) {\n node.style.left = `${point.x}px`;\n node.style.top = `${point.y}px`;\n }\n }\n }\n }\n onRemove() {\n this.button.remove();\n this.caption?.remove();\n this.button.onclick = null;\n }\n }\n const clear = () => {\n pins.forEach((pin) => pin.setMap(null));\n pins = [];\n };\n const fit = (\n branches: Branch[],\n location: Coordinates | undefined,\n maxZoom: number,\n ) => {\n fitListener?.remove();\n fitListener = undefined;\n const bounds = branchBounds(branches, location);\n if (!bounds) {\n return;\n }\n map.fitBounds(bounds, location ? SHOPPER_PADDING : BRANCH_PADDING);\n fitListener = maps.event.addListenerOnce(map, \"idle\", () => {\n fitListener = undefined;\n const zoom = map.getZoom();\n if (typeof zoom === \"number\" && zoom > maxZoom) {\n map.setZoom(maxZoom);\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 clear();\n map.setOptions({ mapTypeControl: options?.mapTypeControl ?? false });\n const visible = visibleBranches(branches);\n for (const branch of visible) {\n const isSelected = branch.id === selected;\n const pin = new Pin(branch.coordinates, {\n label: branch.name,\n selected: isSelected,\n imageUrl: options?.pinImageUrl,\n caption:\n options?.pinLabel === \"all\" ||\n (options?.pinLabel === \"selected\" && isSelected)\n ? branch.name\n : undefined,\n onClick: () => onSelect(branch),\n });\n pin.setMap(map);\n pins.push(pin);\n }\n const shopper = validCoordinates(location) ? location : undefined;\n if (shopper) {\n const pin = new Pin(shopper, {\n label: \"Your location\",\n selected: false,\n });\n pin.setMap(map);\n pins.push(pin);\n }\n const maxZoom = options?.maxZoom ?? 14;\n // Refit only when what the map frames changes. A new selection pans instead.\n const frame = JSON.stringify([\n visible.map(({ coordinates }) => [coordinates.lat, coordinates.lng]),\n shopper ? [shopper.lat, shopper.lng] : null,\n maxZoom,\n ]);\n const previous = lastSelected;\n lastSelected = selected;\n if (frame !== fitted) {\n fitted = frame;\n fit(branches, shopper, maxZoom);\n return;\n }\n const target = visible.find((branch) => branch.id === selected);\n if (target && selected !== previous) {\n map.panTo(target.coordinates);\n }\n },\n locate(location: Coordinates) {\n map.panTo(location);\n },\n destroy() {\n fitListener?.remove();\n clear();\n maps.event.clearInstanceListeners(map);\n element.replaceChildren();\n },\n };\n}\n"],
|
|
5
|
-
"mappings": ";
|
|
6
|
-
"names": ["options"]
|
|
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": "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",
|
|
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
|
+
}
|