@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 CHANGED
@@ -1,5 +1,29 @@
1
1
  # @zappie/foundations
2
2
 
3
+ ## 0.1.5
4
+
5
+ ### Patch Changes
6
+
7
+ - 282781d: Maps draw the markers the default storefront draws. `BranchMap` shows each branch as the `pinImageUrl` logo in a white ring, 40 px for the focused branch and 32 px for the rest. The focused branch is the selected one or, once the shopper's location is known, the nearest. A shopper location adds a house marker labelled “You” and a line to the focused branch, and the map frames the two with 88 px of padding. Nearby branches group into numbered clusters that zoom in when clicked, and selecting a branch inside a cluster zooms in until its pin shows. Without a shopper the map frames every branch with 64 px of padding, and a single branch is centred at zoom 14. `compact` frames a small map. Coordinates of (0, 0) count as missing. Colour clusters, the house marker, the line and label borders with `--map-accent` and `--map-accent-foreground`.
8
+
9
+ The new `DeliveryMap` shows a delivery for a cart or checkout: the branch logo, the shopper's house and the line between them, fitted to all of it, with no controls. Pass `route` to draw a path instead of a straight line. Both maps load after mount, and the clusterer loads only with the branch map.
10
+
11
+ - 51d437f: An overlay's first open on a page paints on the tap. The first open used to run the overlay engine's modal code for the first time before anything showed, which took 60 to 90 ms on a slow phone. Now the backdrop and the panel with its content paint first, and the engine takes over on the next frame. Focus handling, scroll lock, Escape, outside presses and hiding the page from assistive technology work as before once it does. Until then the panel is hidden from assistive technology and holds every key, press and scroll, so nothing on the page can be reached. Escape and an outside press still close it. Later opens, nested overlays and `forceMount` content use the engine straight away.
12
+ - 1c3cbbf: `ItemForm.Root` accepts a typed `onInvalid={(groups: ItemFormInvalidGroup[]) => ...}` callback. `ItemFormProps` no longer inherits the DOM `onInvalid` handler, and the root never calls `onInvalid` for a DOM `invalid` event.
13
+ - Updated dependencies [e7cc5f0]
14
+ - Updated dependencies [1c3cbbf]
15
+ - @zappie/sdk@0.1.9
16
+
17
+ ## 0.1.4
18
+
19
+ ### Patch Changes
20
+
21
+ - 4f3cdc3: Add `deferred()` and `useIntentPreload()`, which load an overlay's code on demand and render it without suspending, so a first open no longer waits for React's 300 ms reveal. `BranchMap` now fits the shopper's location with the branches, pans to a newly selected branch instead of fitting again, reports load failures through `onLoadError` and `renderLoadError` with a retry, and can label pins with the branch name. `ItemForm.Root` focuses the first invalid group without scrolling and reports the invalid groups through `onInvalid`.
22
+ - e1f4cf0: `Overlay` prefetches its engine once the page has loaded and the browser is idle, without waiting for an `Overlay.Root` to mount. An overlay loaded with `deferred()` mounts its Root only on the tap, so its first open used to wait for the engine's download as well.
23
+ - 3d56337: Add a fitted-zoom limit, image pins, and an optional map and satellite switch to `BranchMap`.
24
+ - Updated dependencies [4f3cdc3]
25
+ - @zappie/sdk@0.1.8
26
+
3
27
  ## 0.1.3
4
28
 
5
29
  ### Patch Changes
package/README.md CHANGED
@@ -50,11 +50,86 @@ Foundations is at `0.x`. Its API can change in a minor release while it settles.
50
50
 
51
51
  MIT
52
52
 
53
+ ## Deferred overlays
54
+
55
+ Keep a bag, branch finder or search dialog off the first load without `React.lazy`.
56
+ React holds a suspended boundary's first reveal for about 300 ms, so a lazy overlay
57
+ opens late. `deferred()` loads the code on demand and renders on the frame it arrives.
58
+ It never suspends.
59
+
60
+ ```tsx
61
+ import { deferred, useIntentPreload } from "@zappie/foundations";
62
+
63
+ const BagSheet = deferred(() => import("./bag-sheet"));
64
+ // Or a named export: deferred(() => import("./finder").then((m) => m.Finder))
65
+
66
+ function Header() {
67
+ const [open, setOpen] = useState(false);
68
+ const intent = useIntentPreload(BagSheet);
69
+ return (
70
+ <>
71
+ <button {...intent} onClick={() => setOpen(true)}>
72
+ Bag
73
+ </button>
74
+ <BagSheet.Component open={open} onClose={() => setOpen(false)} />
75
+ </>
76
+ );
77
+ }
78
+ ```
79
+
80
+ - `deferred(load)` takes a function that imports a module whose default export
81
+ is the component, or that resolves to the component. Create it once at module
82
+ scope.
83
+ - `Component` renders nothing until `open` is first true and the code is in.
84
+ When `open` turns true it loads the code and renders as soon as it arrives.
85
+ After that first open the component stays mounted and receives every prop,
86
+ `open` included, so it can animate out and return focus when it closes.
87
+ - Pass `fallback` to hold an inline part's place while its code loads, for
88
+ example a placeholder label. It shows only while `open` is true and the code
89
+ is not in yet, and it never suspends.
90
+ - `preload()` starts the load and returns its promise. Calls share one load. A
91
+ failed load is forgotten, so the next `preload()` or the next open tries again.
92
+ - `preloadWhenIdle({ timeout? })` loads the code when the browser is idle and
93
+ returns a function that cancels it. Use it only for small chunks. It does
94
+ nothing on the server.
95
+ - `useIntentPreload(target)` returns `{ onPointerEnter, onPointerDown, onFocus }`
96
+ for the trigger, so the code is usually in by the click.
97
+
98
+ Importing the helper never touches `window`. On the server `Component` renders
99
+ nothing, and it works under `StrictMode`.
100
+
101
+ The first overlay you open on a page paints on the tap. Its backdrop and panel,
102
+ with the content, show in the engine's markup straight away, and the overlay's
103
+ focus handling, scroll lock and dismissal take over on the next frame. Until
104
+ then the panel is hidden from assistive technology and takes no keys, presses or
105
+ scrolls, apart from Escape and an outside press, which close it. Style the panel
106
+ and `[data-zappie-backdrop]` as usual: both frames match them.
107
+
108
+ ## Item form focus
109
+
110
+ When an add fails validation, `ItemForm.Root` focuses the first invalid group
111
+ with `preventScroll`, so the page doesn't jump. Pass `onInvalid(groups)` to
112
+ scroll it into view yourself. `groups` lists the invalid groups in display
113
+ order, each with its `groupId`, its `group` (undefined for a group the
114
+ template renders from its own model), its `element` and its `issues`.
115
+
116
+ ```tsx
117
+ <ItemForm.Root
118
+ item={item}
119
+ onInvalid={(groups) =>
120
+ groups[0]?.element?.scrollIntoView({ behavior: "smooth", block: "center" })
121
+ }
122
+ >
123
+ ```
124
+
53
125
  ## Branch map
54
126
 
55
127
  `BranchMap` accepts SDK `Branch[]` data, `selectedBranchId`, `onSelect(branch)`,
56
- `browserKey`, and optional `shopperLocation: { lat, lng }`. It also accepts
57
- standard div attributes such as `className` and `style`.
128
+ `browserKey`, and optional `shopperLocation: { lat, lng }`. Use `maxZoom` to
129
+ limit the zoom after fitting, `pinImageUrl` to show an image in each branch pin,
130
+ `mapTypeControl` to show the map and satellite switch, and `pinLabel` to show
131
+ the branch name above the selected pin (`"selected"`) or every pin (`"all"`).
132
+ It also accepts standard div attributes such as `className` and `style`.
58
133
 
59
134
  ```tsx
60
135
  import { BranchMap } from "@zappie/foundations";
@@ -65,18 +140,90 @@ import { BranchMap } from "@zappie/foundations";
65
140
  onSelect={(branch) => setSelectedBranchId(branch.id)}
66
141
  browserKey={integrations.googleMaps?.browserKey}
67
142
  shopperLocation={shopperLocation}
143
+ pinImageUrl={store.brand.logoUrl}
144
+ pinLabel="selected"
145
+ mapTypeControl
146
+ onLoadError={(error) => report(error)}
147
+ renderLoadError={({ retry }) => (
148
+ <div role="status">
149
+ <p>Map didn't load</p>
150
+ <button type="button" onClick={retry}>
151
+ Try again
152
+ </button>
153
+ </div>
154
+ )}
68
155
  className="branch-map"
69
156
  />;
70
157
  ```
71
158
 
72
- Set a height on `.branch-map`. Theme pins with `--branch-map-pin`,
73
- `--branch-map-pin-selected`, `--branch-map-pin-border`, and
74
- `--branch-map-pin-size`. Pins expose `data-selected` and `aria-pressed`.
159
+ Set a height on `.branch-map`. With a `pinImageUrl`, such as the business logo,
160
+ each branch pin is the logo in a white ring: 32 px, or 40 px for the focused
161
+ branch. The focused branch is the selected one or, once a `shopperLocation` is
162
+ known, the nearest. Without a `pinImageUrl`, pins are dots themed with
163
+ `--branch-map-pin`, `--branch-map-pin-selected`, `--branch-map-pin-border` and
164
+ `--branch-map-pin-size`. Pins expose `data-selected`, `data-focused` and
165
+ `aria-pressed`. Theme pin labels with `--branch-map-label-background`,
166
+ `--branch-map-label-color` and `--branch-map-label-border`. Labels are marked
167
+ `data-branch-map-pin-label` and hidden from assistive technology, because the pin
168
+ already carries the name. `pinLabel="selected"` labels the focused pin.
169
+
170
+ With a `shopperLocation`, the map shows a house marker labelled “You” and draws a
171
+ line from it to the focused branch. Nearby branches group into numbered
172
+ clusters. Clicking a cluster zooms into it, and selecting a branch inside a
173
+ cluster zooms in until its pin shows. Colour the clusters, the house marker, the
174
+ line and the label borders with `--map-accent`, and the cluster count and house
175
+ icon with `--map-accent-foreground`. The map reads these when it updates, because
176
+ Maps draws clusters and lines without CSS.
177
+
75
178
  The map fits branches with valid coordinates, including across the date line.
76
- Branches without coordinates remain selectable with “Map location unavailable”.
179
+ Coordinates of (0, 0) count as missing. With a `shopperLocation`, it fits the
180
+ shopper and the focused branch with 88 px of padding, and fits again when the
181
+ focused branch changes. Without one, it fits every branch with 64 px of padding
182
+ and a new selection pans to that branch. A single point is centred at zoom 14.
183
+ `compact` frames a small map: the route gets 72 px of padding above, 52 px at
184
+ the sides and 36 px below, and a single point is centred at zoom 13. The map
185
+ fits again only when what it frames or `maxZoom` changes. Branches without
186
+ coordinates remain selectable with “Map location unavailable”. `maxZoom`
187
+ defaults to `15` with a shopper location and `14` without one. `mapTypeControl`
188
+ defaults to `false`.
77
189
 
78
190
  The component renders nothing without a key. It loads its map runtime and the
79
191
  Maps JavaScript API after mount, sharing one script across mounted maps.
80
- Use one browser key per page. Loading failures show “Map location unavailable”.
192
+ Use one browser key per page. A loading failure calls `onLoadError(error)`.
193
+ With `renderLoadError({ error, retry })` you render the message, and `retry`
194
+ loads the map again. Without it the map shows “Map location unavailable”.
81
195
  The optional location button pans to coordinates supplied by your app. Your app
82
196
  requests geolocation permission and supplies those coordinates.
197
+
198
+ ## Delivery map
199
+
200
+ `DeliveryMap` shows where an order goes: the branch logo marker, the shopper's
201
+ house marker and the line between them, fitted to all of it. Use it in a cart or
202
+ checkout in place of a static map image. It is a preview, so it has no controls
203
+ and ignores gestures. Pass `route`, such as a decoded driving route, to draw that
204
+ path instead of a straight line.
205
+
206
+ ```tsx
207
+ import { DeliveryMap } from "@zappie/foundations";
208
+
209
+ <DeliveryMap
210
+ branch={branch}
211
+ shopper={deliveryAddress.coordinates}
212
+ browserKey={integrations.googleMaps?.browserKey}
213
+ pinImageUrl={store.brand.logoUrl}
214
+ renderLoadError={({ retry }) => (
215
+ <button type="button" onClick={retry}>
216
+ Try again
217
+ </button>
218
+ )}
219
+ className="delivery-map"
220
+ />;
221
+ ```
222
+
223
+ Set a height on `.delivery-map`. The default storefront uses 180 px. The branch
224
+ marker is a 36 px logo, the house marker uses `--map-accent` and the line uses
225
+ `--delivery-map-route` at 60% opacity. Without a `pinImageUrl` the branch marker
226
+ is the themed dot. Like `BranchMap`, it renders nothing without a key, loads
227
+ Maps after mount, calls `onLoadError(error)` on a failure and shows “Map location
228
+ unavailable” when either point has no valid coordinates. It fits again when the
229
+ points, the route or its size change.
@@ -0,0 +1,27 @@
1
+ // src/branch-map/colors.ts
2
+ var DEFAULT_COLORS = {
3
+ accent: "#111111",
4
+ accentForeground: "#ffffff"
5
+ };
6
+ var COLOR_PROBE_STYLE = {
7
+ color: "var(--map-accent, currentColor)",
8
+ backgroundColor: "var(--map-accent-foreground, #ffffff)",
9
+ borderColor: "var(--delivery-map-route, #163300)"
10
+ };
11
+ function readColors(probe) {
12
+ if (!probe || typeof getComputedStyle !== "function") {
13
+ return DEFAULT_COLORS;
14
+ }
15
+ const style = getComputedStyle(probe);
16
+ return {
17
+ accent: style.color || DEFAULT_COLORS.accent,
18
+ accentForeground: style.backgroundColor || DEFAULT_COLORS.accentForeground,
19
+ route: style.borderTopColor || void 0
20
+ };
21
+ }
22
+ export {
23
+ COLOR_PROBE_STYLE,
24
+ DEFAULT_COLORS,
25
+ readColors
26
+ };
27
+ //# sourceMappingURL=colors.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/branch-map/colors.ts"],
4
+ "sourcesContent": ["/** Colours resolved from CSS, for map parts that cannot read CSS variables. */\nexport type MapColors = {\n accent: string;\n accentForeground: string;\n /** The delivery map's line. */\n route?: string | undefined;\n};\n\nexport const DEFAULT_COLORS: MapColors = {\n accent: \"#111111\",\n accentForeground: \"#ffffff\",\n};\n\n/** Inline style for the hidden element whose computed colours the map reads. */\nexport const COLOR_PROBE_STYLE = {\n color: \"var(--map-accent, currentColor)\",\n backgroundColor: \"var(--map-accent-foreground, #ffffff)\",\n borderColor: \"var(--delivery-map-route, #163300)\",\n} as const;\n\n// Cluster icons and lines are drawn by Maps, which cannot read CSS variables.\nexport function readColors(probe: HTMLElement | null): MapColors {\n if (!probe || typeof getComputedStyle !== \"function\") {\n return DEFAULT_COLORS;\n }\n const style = getComputedStyle(probe);\n return {\n accent: style.color || DEFAULT_COLORS.accent,\n accentForeground: style.backgroundColor || DEFAULT_COLORS.accentForeground,\n route: style.borderTopColor || undefined,\n };\n}\n"],
5
+ "mappings": ";AAQO,IAAM,iBAA4B;AAAA,EACvC,QAAQ;AAAA,EACR,kBAAkB;AACpB;AAGO,IAAM,oBAAoB;AAAA,EAC/B,OAAO;AAAA,EACP,iBAAiB;AAAA,EACjB,aAAa;AACf;AAGO,SAAS,WAAW,OAAsC;AAC/D,MAAI,CAAC,SAAS,OAAO,qBAAqB,YAAY;AACpD,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,iBAAiB,KAAK;AACpC,SAAO;AAAA,IACL,QAAQ,MAAM,SAAS,eAAe;AAAA,IACtC,kBAAkB,MAAM,mBAAmB,eAAe;AAAA,IAC1D,OAAO,MAAM,kBAAkB;AAAA,EACjC;AACF;",
6
+ "names": []
7
+ }
@@ -0,0 +1,60 @@
1
+ "use client";
2
+
3
+ // src/branch-map/delivery-map.tsx
4
+ import { useEffect, useRef } from "react";
5
+ import { COLOR_PROBE_STYLE, readColors } from "./colors.js";
6
+ import { validCoordinates } from "./geometry.js";
7
+ import { useMapRuntime } from "./use-map-runtime.js";
8
+ import { jsx, jsxs } from "react/jsx-runtime";
9
+ var loadDeliveryMap = (target, key, signal) => import("./delivery-runtime.js").then(
10
+ ({ createDeliveryMap }) => createDeliveryMap(target, key, signal)
11
+ );
12
+ function DeliveryMap({
13
+ branch,
14
+ shopper,
15
+ route,
16
+ browserKey,
17
+ pinImageUrl,
18
+ onLoadError,
19
+ renderLoadError,
20
+ ...props
21
+ }) {
22
+ const probe = useRef(null);
23
+ const origin = validCoordinates(branch.coordinates) ? branch.coordinates : void 0;
24
+ const destination = validCoordinates(shopper) ? shopper : void 0;
25
+ const mappable = !!origin && !!destination;
26
+ const { element, runtime, failure, retry } = useMapRuntime(browserKey, mappable, loadDeliveryMap, onLoadError);
27
+ const failed = failure !== void 0;
28
+ const coordinates = branch.coordinates;
29
+ useEffect(() => {
30
+ if (runtime && validCoordinates(coordinates) && validCoordinates(shopper)) {
31
+ runtime.update({
32
+ branch: coordinates,
33
+ shopper,
34
+ route,
35
+ pinImageUrl,
36
+ colors: readColors(probe.current)
37
+ });
38
+ }
39
+ }, [runtime, coordinates, shopper, route, pinImageUrl]);
40
+ if (!browserKey) {
41
+ return null;
42
+ }
43
+ return /* @__PURE__ */ jsxs("div", { ...props, "data-delivery-map": "", children: [
44
+ /* @__PURE__ */ jsx("span", { ref: probe, hidden: true, style: COLOR_PROBE_STYLE }),
45
+ /* @__PURE__ */ jsx(
46
+ "div",
47
+ {
48
+ hidden: failed || !mappable,
49
+ ref: element,
50
+ "data-delivery-map-canvas": "",
51
+ style: { height: "100%" }
52
+ }
53
+ ),
54
+ failure && renderLoadError ? renderLoadError({ error: failure.error, retry }) : failed || !mappable ? /* @__PURE__ */ jsx("p", { role: "status", children: "Map location unavailable" }) : null
55
+ ] });
56
+ }
57
+ export {
58
+ DeliveryMap
59
+ };
60
+ //# sourceMappingURL=delivery-map.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/branch-map/delivery-map.tsx"],
4
+ "sourcesContent": ["\"use client\";\nimport type { Branch } from \"@zappie/sdk\";\nimport { useEffect, useRef, type HTMLAttributes, type ReactNode } from \"react\";\nimport { COLOR_PROBE_STYLE, readColors } from \"./colors\";\nimport type { createDeliveryMap } from \"./delivery-runtime\";\nimport { validCoordinates, type Coordinates } from \"./geometry\";\nimport { useMapRuntime, type MapLoadErrorState } from \"./use-map-runtime\";\n\n/** The branch, the shopper's address and an optional route for DeliveryMap. */\nexport type DeliveryMapProps = HTMLAttributes<HTMLDivElement> & {\n /** The branch the order leaves from. Only its coordinates are read. */\n branch: Pick<Branch, \"coordinates\">;\n /** Coordinates of the delivery address. */\n shopper?: Coordinates | undefined;\n /**\n * The path to draw, such as a decoded driving route, from the branch to the\n * shopper. Without it the map draws a straight line.\n */\n route?: Coordinates[];\n browserKey?: string;\n /** Image shown inside the branch marker, such as the business logo. */\n pinImageUrl?: string;\n /** Called each time the map fails to load. */\n onLoadError?: (error: unknown) => void;\n /**\n * Render your own message when the map fails to load, such as a retry\n * button that calls `retry`. Without it the map shows \"Map location unavailable\".\n */\n renderLoadError?: (state: MapLoadErrorState) => ReactNode;\n};\n\nconst loadDeliveryMap = (\n target: HTMLElement,\n key: string,\n signal: AbortSignal,\n) =>\n import(\"./delivery-runtime\").then(({ createDeliveryMap }) =>\n createDeliveryMap(target, key, signal),\n );\n\n/**\n * A static map of a delivery: the branch marker, the shopper's house marker\n * and the line between them, fitted to both. It has no controls and ignores\n * gestures. Maps loads after mount when you supply a browser key. Set the\n * container height with CSS. Colour the house marker with --map-accent and\n * the line with --delivery-map-route.\n */\nexport function DeliveryMap({\n branch,\n shopper,\n route,\n browserKey,\n pinImageUrl,\n onLoadError,\n renderLoadError,\n ...props\n}: DeliveryMapProps) {\n const probe = useRef<HTMLSpanElement>(null);\n const origin = validCoordinates(branch.coordinates)\n ? branch.coordinates\n : undefined;\n const destination = validCoordinates(shopper) ? shopper : undefined;\n const mappable = !!origin && !!destination;\n const { element, runtime, failure, retry } = useMapRuntime<\n Awaited<ReturnType<typeof createDeliveryMap>>\n >(browserKey, mappable, loadDeliveryMap, onLoadError);\n const failed = failure !== undefined;\n const coordinates = branch.coordinates;\n useEffect(() => {\n if (runtime && validCoordinates(coordinates) && validCoordinates(shopper)) {\n runtime.update({\n branch: coordinates,\n shopper,\n route,\n pinImageUrl,\n colors: readColors(probe.current),\n });\n }\n }, [runtime, coordinates, shopper, route, pinImageUrl]);\n if (!browserKey) {\n return null;\n }\n return (\n <div {...props} data-delivery-map=\"\">\n <span ref={probe} hidden style={COLOR_PROBE_STYLE} />\n <div\n hidden={failed || !mappable}\n ref={element}\n data-delivery-map-canvas=\"\"\n style={{ height: \"100%\" }}\n />\n {failure && renderLoadError ? (\n renderLoadError({ error: failure.error, retry })\n ) : failed || !mappable ? (\n <p role=\"status\">Map location unavailable</p>\n ) : null}\n </div>\n );\n}\n"],
5
+ "mappings": ";;;AAEA,SAAS,WAAW,cAAmD;AACvE,SAAS,mBAAmB,kBAAkB;AAE9C,SAAS,wBAA0C;AACnD,SAAS,qBAA6C;AA6ElD,SACE,KADF;AApDJ,IAAM,kBAAkB,CACtB,QACA,KACA,WAEA,OAAO,uBAAoB,EAAE;AAAA,EAAK,CAAC,EAAE,kBAAkB,MACrD,kBAAkB,QAAQ,KAAK,MAAM;AACvC;AASK,SAAS,YAAY;AAAA,EAC1B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAqB;AACnB,QAAM,QAAQ,OAAwB,IAAI;AAC1C,QAAM,SAAS,iBAAiB,OAAO,WAAW,IAC9C,OAAO,cACP;AACJ,QAAM,cAAc,iBAAiB,OAAO,IAAI,UAAU;AAC1D,QAAM,WAAW,CAAC,CAAC,UAAU,CAAC,CAAC;AAC/B,QAAM,EAAE,SAAS,SAAS,SAAS,MAAM,IAAI,cAE3C,YAAY,UAAU,iBAAiB,WAAW;AACpD,QAAM,SAAS,YAAY;AAC3B,QAAM,cAAc,OAAO;AAC3B,YAAU,MAAM;AACd,QAAI,WAAW,iBAAiB,WAAW,KAAK,iBAAiB,OAAO,GAAG;AACzE,cAAQ,OAAO;AAAA,QACb,QAAQ;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA,QAAQ,WAAW,MAAM,OAAO;AAAA,MAClC,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,SAAS,aAAa,SAAS,OAAO,WAAW,CAAC;AACtD,MAAI,CAAC,YAAY;AACf,WAAO;AAAA,EACT;AACA,SACE,qBAAC,SAAK,GAAG,OAAO,qBAAkB,IAChC;AAAA,wBAAC,UAAK,KAAK,OAAO,QAAM,MAAC,OAAO,mBAAmB;AAAA,IACnD;AAAA,MAAC;AAAA;AAAA,QACC,QAAQ,UAAU,CAAC;AAAA,QACnB,KAAK;AAAA,QACL,4BAAyB;AAAA,QACzB,OAAO,EAAE,QAAQ,OAAO;AAAA;AAAA,IAC1B;AAAA,IACC,WAAW,kBACV,gBAAgB,EAAE,OAAO,QAAQ,OAAO,MAAM,CAAC,IAC7C,UAAU,CAAC,WACb,oBAAC,OAAE,MAAK,UAAS,sCAAwB,IACvC;AAAA,KACN;AAEJ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,118 @@
1
+ // src/branch-map/delivery-runtime.ts
2
+ import { pointBounds } from "./fit.js";
3
+ import { loadMaps } from "./loader.js";
4
+ import {
5
+ DEFAULT_COLORS,
6
+ markerOverlayClass,
7
+ paintMarker
8
+ } from "./markers.js";
9
+ var DELIVERY_ROUTE = {
10
+ color: "#163300",
11
+ opacity: 0.6,
12
+ weight: 5
13
+ };
14
+ var DELIVERY_LOGO_SIZE = 36;
15
+ function deliveryPath(update) {
16
+ const via = (update.route ?? []).filter(
17
+ (point) => Number.isFinite(point.lat) && Number.isFinite(point.lng)
18
+ );
19
+ return via.length >= 2 ? via : [update.branch, update.shopper];
20
+ }
21
+ function deliveryBounds(update) {
22
+ return pointBounds([update.branch, update.shopper, ...deliveryPath(update)]);
23
+ }
24
+ async function createDeliveryMap(element, key, signal) {
25
+ const maps = await loadMaps(key);
26
+ signal.throwIfAborted();
27
+ const map = new maps.Map(element, {
28
+ center: { lat: 0, lng: 0 },
29
+ zoom: 12,
30
+ gestureHandling: "none",
31
+ disableDefaultUI: true,
32
+ keyboardShortcuts: false,
33
+ clickableIcons: false
34
+ });
35
+ const Marker = markerOverlayClass(maps);
36
+ let branchPin;
37
+ let shopperPin;
38
+ let line;
39
+ let framed;
40
+ let last;
41
+ const pin = (key2, position) => {
42
+ const node = document.createElement("div");
43
+ node.setAttribute("aria-hidden", "true");
44
+ node.dataset.deliveryMapPin = key2;
45
+ const marker = new Marker(key2, position, node);
46
+ marker.setMap(map);
47
+ return marker;
48
+ };
49
+ const frame = (update) => {
50
+ map.fitBounds(deliveryBounds(update));
51
+ };
52
+ const resize = typeof ResizeObserver === "function" ? new ResizeObserver(() => {
53
+ if (last) {
54
+ frame(last);
55
+ }
56
+ }) : void 0;
57
+ resize?.observe(element);
58
+ return {
59
+ update(update) {
60
+ last = update;
61
+ const colors = update.colors ?? DEFAULT_COLORS;
62
+ branchPin ?? (branchPin = pin("branch", update.branch));
63
+ branchPin.setPosition(update.branch);
64
+ if (update.pinImageUrl) {
65
+ paintMarker(
66
+ branchPin.element,
67
+ {
68
+ kind: "logo",
69
+ size: DELIVERY_LOGO_SIZE,
70
+ imageUrl: update.pinImageUrl,
71
+ shadow: "sm",
72
+ lift: DELIVERY_LOGO_SIZE / 2
73
+ },
74
+ colors
75
+ );
76
+ } else {
77
+ paintMarker(branchPin.element, { kind: "dot", selected: true }, colors);
78
+ }
79
+ branchPin.element.style.cursor = "default";
80
+ shopperPin ?? (shopperPin = pin("shopper", update.shopper));
81
+ shopperPin.setPosition(update.shopper);
82
+ paintMarker(
83
+ shopperPin.element,
84
+ { kind: "house", color: "#ffffff", lift: 16 },
85
+ colors
86
+ );
87
+ line ?? (line = new maps.Polyline({ map }));
88
+ line.setOptions({
89
+ path: deliveryPath(update),
90
+ strokeColor: update.colors?.route ?? DELIVERY_ROUTE.color,
91
+ strokeOpacity: DELIVERY_ROUTE.opacity,
92
+ strokeWeight: DELIVERY_ROUTE.weight,
93
+ clickable: false
94
+ });
95
+ const key2 = JSON.stringify(deliveryBounds(update));
96
+ if (key2 !== framed) {
97
+ framed = key2;
98
+ frame(update);
99
+ }
100
+ },
101
+ destroy() {
102
+ resize?.disconnect();
103
+ branchPin?.setMap(null);
104
+ shopperPin?.setMap(null);
105
+ line?.setMap(null);
106
+ maps.event.clearInstanceListeners(map);
107
+ element.replaceChildren();
108
+ }
109
+ };
110
+ }
111
+ export {
112
+ DELIVERY_LOGO_SIZE,
113
+ DELIVERY_ROUTE,
114
+ createDeliveryMap,
115
+ deliveryBounds,
116
+ deliveryPath
117
+ };
118
+ //# sourceMappingURL=delivery-runtime.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/branch-map/delivery-runtime.ts"],
4
+ "sourcesContent": ["import { pointBounds } from \"./fit\";\nimport type { Coordinates } from \"./geometry\";\nimport { loadMaps } from \"./loader\";\nimport {\n DEFAULT_COLORS,\n markerOverlayClass,\n paintMarker,\n type MapColors,\n type MarkerOverlay,\n} from \"./markers\";\n\nexport type DeliveryMapUpdate = {\n branch: Coordinates;\n shopper: Coordinates;\n route?: Coordinates[] | undefined;\n pinImageUrl?: string | undefined;\n colors?: MapColors | undefined;\n};\n\n/** The line between the branch and the shopper. */\nexport const DELIVERY_ROUTE = {\n color: \"#163300\",\n opacity: 0.6,\n weight: 5,\n} as const;\n/** Diameter of the branch logo marker. */\nexport const DELIVERY_LOGO_SIZE = 36;\n\n/** The points the delivery map frames and the path it draws. */\nexport function deliveryPath(update: DeliveryMapUpdate) {\n const via = (update.route ?? []).filter(\n (point) => Number.isFinite(point.lat) && Number.isFinite(point.lng),\n );\n return via.length >= 2 ? via : [update.branch, update.shopper];\n}\n\nexport function deliveryBounds(update: DeliveryMapUpdate) {\n return pointBounds([update.branch, update.shopper, ...deliveryPath(update)])!;\n}\n\nexport async function createDeliveryMap(\n element: HTMLElement,\n key: string,\n signal: AbortSignal,\n) {\n const maps = await loadMaps(key);\n signal.throwIfAborted();\n // A static preview: no panning, zooming, dragging or controls.\n const map = new maps.Map(element, {\n center: { lat: 0, lng: 0 },\n zoom: 12,\n gestureHandling: \"none\",\n disableDefaultUI: true,\n keyboardShortcuts: false,\n clickableIcons: false,\n });\n const Marker = markerOverlayClass(maps);\n let branchPin: MarkerOverlay | undefined;\n let shopperPin: MarkerOverlay | undefined;\n let line: google.maps.Polyline | undefined;\n let framed: string | undefined;\n let last: DeliveryMapUpdate | undefined;\n const pin = (key: string, position: Coordinates) => {\n const node = document.createElement(\"div\");\n node.setAttribute(\"aria-hidden\", \"true\");\n node.dataset.deliveryMapPin = key;\n const marker = new Marker(key, position, node);\n marker.setMap(map);\n return marker;\n };\n const frame = (update: DeliveryMapUpdate) => {\n map.fitBounds(deliveryBounds(update));\n };\n const resize =\n typeof ResizeObserver === \"function\"\n ? new ResizeObserver(() => {\n if (last) {\n frame(last);\n }\n })\n : undefined;\n resize?.observe(element);\n return {\n update(update: DeliveryMapUpdate) {\n last = update;\n const colors = update.colors ?? DEFAULT_COLORS;\n branchPin ??= pin(\"branch\", update.branch);\n branchPin.setPosition(update.branch);\n if (update.pinImageUrl) {\n // The logo is the marker itself, so its centre sits half its size above the location.\n paintMarker(\n branchPin.element,\n {\n kind: \"logo\",\n size: DELIVERY_LOGO_SIZE,\n imageUrl: update.pinImageUrl,\n shadow: \"sm\",\n lift: DELIVERY_LOGO_SIZE / 2,\n },\n colors,\n );\n } else {\n paintMarker(branchPin.element, { kind: \"dot\", selected: true }, colors);\n }\n branchPin.element.style.cursor = \"default\";\n shopperPin ??= pin(\"shopper\", update.shopper);\n shopperPin.setPosition(update.shopper);\n paintMarker(\n shopperPin.element,\n { kind: \"house\", color: \"#ffffff\", lift: 16 },\n colors,\n );\n line ??= new maps.Polyline({ map });\n line.setOptions({\n path: deliveryPath(update),\n strokeColor: update.colors?.route ?? DELIVERY_ROUTE.color,\n strokeOpacity: DELIVERY_ROUTE.opacity,\n strokeWeight: DELIVERY_ROUTE.weight,\n clickable: false,\n });\n const key = JSON.stringify(deliveryBounds(update));\n if (key !== framed) {\n framed = key;\n frame(update);\n }\n },\n destroy() {\n resize?.disconnect();\n branchPin?.setMap(null);\n shopperPin?.setMap(null);\n line?.setMap(null);\n maps.event.clearInstanceListeners(map);\n element.replaceChildren();\n },\n };\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["key"]
7
+ }
@@ -0,0 +1,123 @@
1
+ // src/branch-map/fit.ts
2
+ import {
3
+ validCoordinates,
4
+ visibleBranches
5
+ } from "./geometry.js";
6
+ var OVERVIEW_PADDING = 64;
7
+ var ROUTE_PADDING = {
8
+ top: 88,
9
+ right: 88,
10
+ bottom: 88,
11
+ left: 88
12
+ };
13
+ var COMPACT_ROUTE_PADDING = {
14
+ top: 72,
15
+ right: 52,
16
+ bottom: 36,
17
+ left: 52
18
+ };
19
+ var SINGLE_POINT_ZOOM = 14;
20
+ var COMPACT_SINGLE_POINT_ZOOM = 13;
21
+ function distanceKm(from, to) {
22
+ const radians = (degrees) => degrees * Math.PI / 180;
23
+ const dLat = radians(to.lat - from.lat);
24
+ const dLng = radians(to.lng - from.lng);
25
+ const a = Math.sin(dLat / 2) ** 2 + Math.cos(radians(from.lat)) * Math.cos(radians(to.lat)) * Math.sin(dLng / 2) ** 2;
26
+ return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
27
+ }
28
+ function focusedBranch(branches, selectedId, shopper) {
29
+ const visible = visibleBranches(branches);
30
+ const selected = visible.find((branch) => branch.id === selectedId);
31
+ if (selected || !validCoordinates(shopper)) {
32
+ return selected;
33
+ }
34
+ let nearest;
35
+ let best = Infinity;
36
+ for (const branch of visible) {
37
+ const distance = distanceKm(shopper, branch.coordinates);
38
+ if (distance < best) {
39
+ best = distance;
40
+ nearest = branch;
41
+ }
42
+ }
43
+ return nearest;
44
+ }
45
+ function clusterSize(count) {
46
+ return count < 10 ? 40 : count < 50 ? 48 : 56;
47
+ }
48
+ function pointBounds(points) {
49
+ if (!points.length) {
50
+ return void 0;
51
+ }
52
+ const wrapped = (lng) => (lng + 360) % 360;
53
+ const longitudes = points.map((point) => point.lng).sort((a, b) => wrapped(a) - wrapped(b));
54
+ let gap = -1;
55
+ let start = 0;
56
+ for (let index = 0; index < longitudes.length; index++) {
57
+ const next = wrapped(longitudes[(index + 1) % longitudes.length]) + (index === longitudes.length - 1 ? 360 : 0);
58
+ if (next - wrapped(longitudes[index]) > gap) {
59
+ gap = next - wrapped(longitudes[index]);
60
+ start = (index + 1) % longitudes.length;
61
+ }
62
+ }
63
+ return {
64
+ north: Math.max(...points.map((point) => point.lat)),
65
+ south: Math.min(...points.map((point) => point.lat)),
66
+ west: longitudes[start],
67
+ east: longitudes[(start + longitudes.length - 1) % longitudes.length]
68
+ };
69
+ }
70
+ function branchBounds(branches, location) {
71
+ const points = visibleBranches(branches).map((branch) => branch.coordinates);
72
+ if (validCoordinates(location)) {
73
+ points.push(location);
74
+ }
75
+ return pointBounds(points);
76
+ }
77
+ function fitPlan({
78
+ branches,
79
+ shopper,
80
+ focused,
81
+ maxZoom,
82
+ compact = false
83
+ }) {
84
+ const location = validCoordinates(shopper) ? shopper : void 0;
85
+ const points = location && focused ? [location, focused.coordinates] : [
86
+ ...location ? [location] : [],
87
+ ...visibleBranches(branches).map((branch) => branch.coordinates)
88
+ ];
89
+ if (!points.length) {
90
+ return void 0;
91
+ }
92
+ const route = !!(location && focused);
93
+ if (points.length === 1) {
94
+ return {
95
+ kind: "center",
96
+ center: points[0],
97
+ zoom: Math.min(
98
+ maxZoom,
99
+ compact ? COMPACT_SINGLE_POINT_ZOOM : SINGLE_POINT_ZOOM
100
+ )
101
+ };
102
+ }
103
+ return {
104
+ kind: "bounds",
105
+ bounds: pointBounds(points),
106
+ padding: route ? compact ? COMPACT_ROUTE_PADDING : ROUTE_PADDING : OVERVIEW_PADDING,
107
+ maxZoom
108
+ };
109
+ }
110
+ export {
111
+ COMPACT_ROUTE_PADDING,
112
+ COMPACT_SINGLE_POINT_ZOOM,
113
+ OVERVIEW_PADDING,
114
+ ROUTE_PADDING,
115
+ SINGLE_POINT_ZOOM,
116
+ branchBounds,
117
+ clusterSize,
118
+ distanceKm,
119
+ fitPlan,
120
+ focusedBranch,
121
+ pointBounds
122
+ };
123
+ //# sourceMappingURL=fit.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/branch-map/fit.ts"],
4
+ "sourcesContent": ["import type { Branch } from \"@zappie/sdk\";\nimport {\n validCoordinates,\n visibleBranches,\n type Bounds,\n type Coordinates,\n} from \"./geometry\";\n\nexport type Padding =\n number | { top: number; right: number; bottom: number; left: number };\n\n/** Padding around every fitted point when no route is drawn. */\nexport const OVERVIEW_PADDING = 64;\n/** Padding around the shopper-to-branch route. */\nexport const ROUTE_PADDING: Padding = {\n top: 88,\n right: 88,\n bottom: 88,\n left: 88,\n};\n/** Route padding in a compact map, with room for both pin labels. */\nexport const COMPACT_ROUTE_PADDING: Padding = {\n top: 72,\n right: 52,\n bottom: 36,\n left: 52,\n};\n/** Zoom for a single point, in a full and a compact map. */\nexport const SINGLE_POINT_ZOOM = 14;\nexport const COMPACT_SINGLE_POINT_ZOOM = 13;\n\n/** Great-circle distance in kilometres. */\nexport function distanceKm(from: Coordinates, to: Coordinates) {\n const radians = (degrees: number) => (degrees * Math.PI) / 180;\n const dLat = radians(to.lat - from.lat);\n const dLng = radians(to.lng - from.lng);\n const a =\n Math.sin(dLat / 2) ** 2 +\n Math.cos(radians(from.lat)) *\n Math.cos(radians(to.lat)) *\n Math.sin(dLng / 2) ** 2;\n return 6371 * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n}\n\n/**\n * The branch the map highlights: the selected one, or, once the shopper's\n * location is known, the nearest branch.\n */\nexport function focusedBranch(\n branches: Branch[],\n selectedId: string | undefined,\n shopper: Coordinates | undefined,\n) {\n const visible = visibleBranches(branches);\n const selected = visible.find((branch) => branch.id === selectedId);\n if (selected || !validCoordinates(shopper)) {\n return selected;\n }\n let nearest: (typeof visible)[number] | undefined;\n let best = Infinity;\n for (const branch of visible) {\n const distance = distanceKm(shopper, branch.coordinates);\n if (distance < best) {\n best = distance;\n nearest = branch;\n }\n }\n return nearest;\n}\n\n/** Diameter of a cluster icon for the number of branches it holds. */\nexport function clusterSize(count: number) {\n return count < 10 ? 40 : count < 50 ? 48 : 56;\n}\n\nexport function pointBounds(points: Coordinates[]): Bounds | undefined {\n if (!points.length) {\n return undefined;\n }\n // The largest longitude gap lies outside the bounds, including across the date line.\n // Sort by the wrapped longitude but report the original values, so they stay exact.\n const wrapped = (lng: number) => (lng + 360) % 360;\n const longitudes = points\n .map((point) => point.lng)\n .sort((a, b) => wrapped(a) - wrapped(b));\n let gap = -1;\n let start = 0;\n for (let index = 0; index < longitudes.length; index++) {\n const next =\n wrapped(longitudes[(index + 1) % longitudes.length]!) +\n (index === longitudes.length - 1 ? 360 : 0);\n if (next - wrapped(longitudes[index]!) > gap) {\n gap = next - wrapped(longitudes[index]!);\n start = (index + 1) % longitudes.length;\n }\n }\n return {\n north: Math.max(...points.map((point) => point.lat)),\n south: Math.min(...points.map((point) => point.lat)),\n west: longitudes[start]!,\n east: longitudes[(start + longitudes.length - 1) % longitudes.length]!,\n };\n}\n\nexport function branchBounds(branches: Branch[], location?: Coordinates) {\n const points = visibleBranches(branches).map((branch) => branch.coordinates);\n if (validCoordinates(location)) {\n points.push(location);\n }\n return pointBounds(points);\n}\n\n/** How the map frames its points. */\nexport type FitPlan =\n | { kind: \"center\"; center: Coordinates; zoom: number }\n | { kind: \"bounds\"; bounds: Bounds; padding: Padding; maxZoom: number };\n\n/**\n * With a route, frame the shopper and the focused branch. Otherwise frame the\n * shopper and every branch. A single point is centred at a fixed zoom.\n */\nexport function fitPlan({\n branches,\n shopper,\n focused,\n maxZoom,\n compact = false,\n}: {\n branches: Branch[];\n shopper?: Coordinates | undefined;\n focused?: (Branch & { coordinates: Coordinates }) | undefined;\n maxZoom: number;\n compact?: boolean;\n}): FitPlan | undefined {\n const location = validCoordinates(shopper) ? shopper : undefined;\n const points =\n location && focused\n ? [location, focused.coordinates]\n : [\n ...(location ? [location] : []),\n ...visibleBranches(branches).map((branch) => branch.coordinates),\n ];\n if (!points.length) {\n return undefined;\n }\n const route = !!(location && focused);\n if (points.length === 1) {\n return {\n kind: \"center\",\n center: points[0]!,\n zoom: Math.min(\n maxZoom,\n compact ? COMPACT_SINGLE_POINT_ZOOM : SINGLE_POINT_ZOOM,\n ),\n };\n }\n return {\n kind: \"bounds\",\n bounds: pointBounds(points)!,\n padding: route\n ? compact\n ? COMPACT_ROUTE_PADDING\n : ROUTE_PADDING\n : OVERVIEW_PADDING,\n maxZoom,\n };\n}\n"],
5
+ "mappings": ";AACA;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AAMA,IAAM,mBAAmB;AAEzB,IAAM,gBAAyB;AAAA,EACpC,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AACR;AAEO,IAAM,wBAAiC;AAAA,EAC5C,KAAK;AAAA,EACL,OAAO;AAAA,EACP,QAAQ;AAAA,EACR,MAAM;AACR;AAEO,IAAM,oBAAoB;AAC1B,IAAM,4BAA4B;AAGlC,SAAS,WAAW,MAAmB,IAAiB;AAC7D,QAAM,UAAU,CAAC,YAAqB,UAAU,KAAK,KAAM;AAC3D,QAAM,OAAO,QAAQ,GAAG,MAAM,KAAK,GAAG;AACtC,QAAM,OAAO,QAAQ,GAAG,MAAM,KAAK,GAAG;AACtC,QAAM,IACJ,KAAK,IAAI,OAAO,CAAC,KAAK,IACtB,KAAK,IAAI,QAAQ,KAAK,GAAG,CAAC,IACxB,KAAK,IAAI,QAAQ,GAAG,GAAG,CAAC,IACxB,KAAK,IAAI,OAAO,CAAC,KAAK;AAC1B,SAAO,OAAO,IAAI,KAAK,MAAM,KAAK,KAAK,CAAC,GAAG,KAAK,KAAK,IAAI,CAAC,CAAC;AAC7D;AAMO,SAAS,cACd,UACA,YACA,SACA;AACA,QAAM,UAAU,gBAAgB,QAAQ;AACxC,QAAM,WAAW,QAAQ,KAAK,CAAC,WAAW,OAAO,OAAO,UAAU;AAClE,MAAI,YAAY,CAAC,iBAAiB,OAAO,GAAG;AAC1C,WAAO;AAAA,EACT;AACA,MAAI;AACJ,MAAI,OAAO;AACX,aAAW,UAAU,SAAS;AAC5B,UAAM,WAAW,WAAW,SAAS,OAAO,WAAW;AACvD,QAAI,WAAW,MAAM;AACnB,aAAO;AACP,gBAAU;AAAA,IACZ;AAAA,EACF;AACA,SAAO;AACT;AAGO,SAAS,YAAY,OAAe;AACzC,SAAO,QAAQ,KAAK,KAAK,QAAQ,KAAK,KAAK;AAC7C;AAEO,SAAS,YAAY,QAA2C;AACrE,MAAI,CAAC,OAAO,QAAQ;AAClB,WAAO;AAAA,EACT;AAGA,QAAM,UAAU,CAAC,SAAiB,MAAM,OAAO;AAC/C,QAAM,aAAa,OAChB,IAAI,CAAC,UAAU,MAAM,GAAG,EACxB,KAAK,CAAC,GAAG,MAAM,QAAQ,CAAC,IAAI,QAAQ,CAAC,CAAC;AACzC,MAAI,MAAM;AACV,MAAI,QAAQ;AACZ,WAAS,QAAQ,GAAG,QAAQ,WAAW,QAAQ,SAAS;AACtD,UAAM,OACJ,QAAQ,YAAY,QAAQ,KAAK,WAAW,MAAM,CAAE,KACnD,UAAU,WAAW,SAAS,IAAI,MAAM;AAC3C,QAAI,OAAO,QAAQ,WAAW,KAAK,CAAE,IAAI,KAAK;AAC5C,YAAM,OAAO,QAAQ,WAAW,KAAK,CAAE;AACvC,eAAS,QAAQ,KAAK,WAAW;AAAA,IACnC;AAAA,EACF;AACA,SAAO;AAAA,IACL,OAAO,KAAK,IAAI,GAAG,OAAO,IAAI,CAAC,UAAU,MAAM,GAAG,CAAC;AAAA,IACnD,OAAO,KAAK,IAAI,GAAG,OAAO,IAAI,CAAC,UAAU,MAAM,GAAG,CAAC;AAAA,IACnD,MAAM,WAAW,KAAK;AAAA,IACtB,MAAM,YAAY,QAAQ,WAAW,SAAS,KAAK,WAAW,MAAM;AAAA,EACtE;AACF;AAEO,SAAS,aAAa,UAAoB,UAAwB;AACvE,QAAM,SAAS,gBAAgB,QAAQ,EAAE,IAAI,CAAC,WAAW,OAAO,WAAW;AAC3E,MAAI,iBAAiB,QAAQ,GAAG;AAC9B,WAAO,KAAK,QAAQ;AAAA,EACtB;AACA,SAAO,YAAY,MAAM;AAC3B;AAWO,SAAS,QAAQ;AAAA,EACtB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,UAAU;AACZ,GAMwB;AACtB,QAAM,WAAW,iBAAiB,OAAO,IAAI,UAAU;AACvD,QAAM,SACJ,YAAY,UACR,CAAC,UAAU,QAAQ,WAAW,IAC9B;AAAA,IACE,GAAI,WAAW,CAAC,QAAQ,IAAI,CAAC;AAAA,IAC7B,GAAG,gBAAgB,QAAQ,EAAE,IAAI,CAAC,WAAW,OAAO,WAAW;AAAA,EACjE;AACN,MAAI,CAAC,OAAO,QAAQ;AAClB,WAAO;AAAA,EACT;AACA,QAAM,QAAQ,CAAC,EAAE,YAAY;AAC7B,MAAI,OAAO,WAAW,GAAG;AACvB,WAAO;AAAA,MACL,MAAM;AAAA,MACN,QAAQ,OAAO,CAAC;AAAA,MAChB,MAAM,KAAK;AAAA,QACT;AAAA,QACA,UAAU,4BAA4B;AAAA,MACxC;AAAA,IACF;AAAA,EACF;AACA,SAAO;AAAA,IACL,MAAM;AAAA,IACN,QAAQ,YAAY,MAAM;AAAA,IAC1B,SAAS,QACL,UACE,wBACA,gBACF;AAAA,IACJ;AAAA,EACF;AACF;",
6
+ "names": []
7
+ }
@@ -4,36 +4,10 @@ function validCoordinates(value) {
4
4
  }
5
5
  function visibleBranches(branches) {
6
6
  return branches.filter(
7
- (branch) => validCoordinates(branch.coordinates)
7
+ (branch) => validCoordinates(branch.coordinates) && (branch.coordinates.lat !== 0 || branch.coordinates.lng !== 0)
8
8
  );
9
9
  }
10
- function branchBounds(branches) {
11
- const points = visibleBranches(branches).map((branch) => branch.coordinates);
12
- if (!points.length) {
13
- return void 0;
14
- }
15
- const longitudes = points.map((point) => (point.lng + 360) % 360).sort((a, b) => a - b);
16
- let gap = -1;
17
- let start = 0;
18
- for (let index = 0; index < longitudes.length; index++) {
19
- const next = longitudes[(index + 1) % longitudes.length] + (index === longitudes.length - 1 ? 360 : 0);
20
- if (next - longitudes[index] > gap) {
21
- gap = next - longitudes[index];
22
- start = (index + 1) % longitudes.length;
23
- }
24
- }
25
- const normalize = (lng) => lng > 180 ? lng - 360 : lng;
26
- return {
27
- north: Math.max(...points.map((point) => point.lat)),
28
- south: Math.min(...points.map((point) => point.lat)),
29
- west: normalize(longitudes[start]),
30
- east: normalize(
31
- longitudes[(start + longitudes.length - 1) % longitudes.length]
32
- )
33
- };
34
- }
35
10
  export {
36
- branchBounds,
37
11
  validCoordinates,
38
12
  visibleBranches
39
13
  };