@sckyzo/cosmos-react 0.2.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +261 -0
- package/README.md +1641 -5
- package/calendar.css +4 -0
- package/dist/CodeBlock-CY18_3tZ.d.cts +52 -0
- package/dist/CodeBlock-CY18_3tZ.d.ts +52 -0
- package/dist/code/index.cjs +331 -0
- package/dist/code/index.cjs.map +1 -0
- package/dist/code/index.d.cts +29 -0
- package/dist/code/index.d.ts +29 -0
- package/dist/code/index.js +326 -0
- package/dist/code/index.js.map +1 -0
- package/dist/editor/index.cjs +879 -0
- package/dist/editor/index.cjs.map +1 -0
- package/dist/editor/index.d.cts +100 -0
- package/dist/editor/index.d.ts +100 -0
- package/dist/editor/index.js +872 -0
- package/dist/editor/index.js.map +1 -0
- package/dist/fieldRecipe-Cao5uUIK.d.cts +3 -0
- package/dist/fieldRecipe-Cao5uUIK.d.ts +3 -0
- package/dist/index.cjs +25053 -8751
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5411 -1147
- package/dist/index.d.ts +5411 -1147
- package/dist/index.js +24944 -8735
- package/dist/index.js.map +1 -1
- package/dist/intent-D0bANgYJ.d.cts +12 -0
- package/dist/intent-D0bANgYJ.d.ts +12 -0
- package/dist/map/index.cjs +559 -0
- package/dist/map/index.cjs.map +1 -0
- package/dist/map/index.d.cts +95 -0
- package/dist/map/index.d.ts +95 -0
- package/dist/map/index.js +556 -0
- package/dist/map/index.js.map +1 -0
- package/package.json +105 -17
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The meaning a component carries, as a color: the one vocabulary behind every
|
|
3
|
+
* `intent` prop. A component may accept a documented subset (a `Button` takes
|
|
4
|
+
* `brand`, `success`, `warning` and `danger`), but never a synonym: `danger`, not
|
|
5
|
+
* `error` or `crit`; `warning`, not `warn`.
|
|
6
|
+
*
|
|
7
|
+
* `variant` is the other axis: it describes structure or emphasis (`solid`,
|
|
8
|
+
* `outline`, `ghost`), never meaning.
|
|
9
|
+
*/
|
|
10
|
+
type Intent = 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'danger';
|
|
11
|
+
|
|
12
|
+
export type { Intent as I };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The meaning a component carries, as a color: the one vocabulary behind every
|
|
3
|
+
* `intent` prop. A component may accept a documented subset (a `Button` takes
|
|
4
|
+
* `brand`, `success`, `warning` and `danger`), but never a synonym: `danger`, not
|
|
5
|
+
* `error` or `crit`; `warning`, not `warn`.
|
|
6
|
+
*
|
|
7
|
+
* `variant` is the other axis: it describes structure or emphasis (`solid`,
|
|
8
|
+
* `outline`, `ghost`), never meaning.
|
|
9
|
+
*/
|
|
10
|
+
type Intent = 'neutral' | 'brand' | 'info' | 'success' | 'warning' | 'danger';
|
|
11
|
+
|
|
12
|
+
export type { Intent as I };
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
var react = require('react');
|
|
5
|
+
var reactDom = require('react-dom');
|
|
6
|
+
var clsx = require('clsx');
|
|
7
|
+
var lucideReact = require('lucide-react');
|
|
8
|
+
var jsxRuntime = require('react/jsx-runtime');
|
|
9
|
+
|
|
10
|
+
// src/map/MapView.tsx
|
|
11
|
+
function useControllableState({
|
|
12
|
+
value,
|
|
13
|
+
defaultValue,
|
|
14
|
+
onChange,
|
|
15
|
+
name
|
|
16
|
+
}) {
|
|
17
|
+
const isControlled = value !== void 0;
|
|
18
|
+
const [internal, setInternal] = react.useState(defaultValue);
|
|
19
|
+
const current = isControlled ? value : internal;
|
|
20
|
+
const latest = react.useRef({ current, isControlled, onChange });
|
|
21
|
+
react.useInsertionEffect(() => {
|
|
22
|
+
latest.current = { current, isControlled, onChange };
|
|
23
|
+
});
|
|
24
|
+
const setValue = react.useCallback((next, ...args) => {
|
|
25
|
+
const state = latest.current;
|
|
26
|
+
const resolved = typeof next === "function" ? next(state.current) : next;
|
|
27
|
+
if (!state.isControlled) {
|
|
28
|
+
state.current = resolved;
|
|
29
|
+
setInternal(resolved);
|
|
30
|
+
}
|
|
31
|
+
state.onChange?.(resolved, ...args);
|
|
32
|
+
}, []);
|
|
33
|
+
useModeSwitchWarning(isControlled, name);
|
|
34
|
+
return [current, setValue, isControlled];
|
|
35
|
+
}
|
|
36
|
+
var isProductionBuild = (() => {
|
|
37
|
+
try {
|
|
38
|
+
return process.env.NODE_ENV === "production";
|
|
39
|
+
} catch {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
})();
|
|
43
|
+
function useModeSwitchWarning(isControlled, name) {
|
|
44
|
+
const initial = react.useRef(isControlled);
|
|
45
|
+
const warned = react.useRef(false);
|
|
46
|
+
react.useEffect(() => {
|
|
47
|
+
if (isProductionBuild || warned.current || initial.current === isControlled) return;
|
|
48
|
+
warned.current = true;
|
|
49
|
+
const from = initial.current ? "controlled" : "uncontrolled";
|
|
50
|
+
const to = isControlled ? "controlled" : "uncontrolled";
|
|
51
|
+
console.warn(
|
|
52
|
+
`Cosmos: ${name} switched from ${from} to ${to}. Keep it one or the other for the life of the component: only \`undefined\` means uncontrolled.`
|
|
53
|
+
);
|
|
54
|
+
}, [isControlled, name]);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/settings/appearance.ts
|
|
58
|
+
var COSMOS_THEMES = [
|
|
59
|
+
{
|
|
60
|
+
id: "light",
|
|
61
|
+
label: "Light",
|
|
62
|
+
dark: false,
|
|
63
|
+
preview: { bg: "#f9fafb", surface: "#ffffff", border: "#e5e7eb", text: "#667085" }
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: "sand",
|
|
67
|
+
label: "Sand",
|
|
68
|
+
dark: false,
|
|
69
|
+
preview: { bg: "#f9f5eb", surface: "#fffcf7", border: "#e7dfd0", text: "#746656" }
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
id: "dark",
|
|
73
|
+
label: "Dark",
|
|
74
|
+
dark: true,
|
|
75
|
+
preview: { bg: "#0a0a0a", surface: "#141414", border: "#3d3d3d", text: "#a3a3a3" }
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: "deep-blue",
|
|
79
|
+
label: "Deep Blue",
|
|
80
|
+
dark: true,
|
|
81
|
+
preview: { bg: "#0c111d", surface: "#101828", border: "#344054", text: "#98a2b3" }
|
|
82
|
+
}
|
|
83
|
+
];
|
|
84
|
+
COSMOS_THEMES.map((t) => t.id).filter((id) => id !== "light");
|
|
85
|
+
var isDarkTheme = (theme) => COSMOS_THEMES.some((t) => t.id === theme && t.dark);
|
|
86
|
+
|
|
87
|
+
// src/settings/useCosmosTheme.ts
|
|
88
|
+
var readTheme = () => {
|
|
89
|
+
const { classList } = document.documentElement;
|
|
90
|
+
return COSMOS_THEMES.find((t) => t.id !== "light" && classList.contains(t.id))?.id ?? "light";
|
|
91
|
+
};
|
|
92
|
+
var subscribe = (onChange) => {
|
|
93
|
+
const observer = new MutationObserver(onChange);
|
|
94
|
+
observer.observe(document.documentElement, { attributes: true, attributeFilter: ["class"] });
|
|
95
|
+
return () => observer.disconnect();
|
|
96
|
+
};
|
|
97
|
+
var getSnapshot = () => isDarkTheme(readTheme()) ? "dark" : "light";
|
|
98
|
+
var getServerSnapshot = () => "light";
|
|
99
|
+
var useCosmosTheme = () => react.useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
|
|
100
|
+
var PIN_COLOR = {
|
|
101
|
+
neutral: "bg-gray-600 text-white",
|
|
102
|
+
brand: "bg-brand-500 text-white",
|
|
103
|
+
info: "bg-blue-600 text-white",
|
|
104
|
+
success: "bg-green-500 text-gray-900",
|
|
105
|
+
warning: "bg-amber-500 text-gray-900",
|
|
106
|
+
danger: "bg-red-600 text-white"
|
|
107
|
+
};
|
|
108
|
+
var PIN_BASE = "relative flex items-center justify-center rounded-full border-2 border-white text-xs font-semibold shadow-md transition-transform";
|
|
109
|
+
var MapPin = ({ marker, selected, interactive, onSelect, onClose }) => {
|
|
110
|
+
const { id, label, description, intent = "brand", icon } = marker;
|
|
111
|
+
const name = typeof icon === "string" || typeof icon === "number" ? `${label} ${icon}` : label;
|
|
112
|
+
const face = icon != null ? /* @__PURE__ */ jsxRuntime.jsx("span", { "aria-hidden": "true", children: icon }) : /* @__PURE__ */ jsxRuntime.jsx(lucideReact.MapPin, { className: "size-4", "aria-hidden": "true" });
|
|
113
|
+
const className = clsx.clsx(
|
|
114
|
+
PIN_BASE,
|
|
115
|
+
"size-8",
|
|
116
|
+
PIN_COLOR[intent],
|
|
117
|
+
selected ? "ring-brand-500/40 scale-110 ring-4" : "ring-0"
|
|
118
|
+
);
|
|
119
|
+
if (!interactive) {
|
|
120
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className, "aria-hidden": "true", children: face });
|
|
121
|
+
}
|
|
122
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
|
|
123
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
124
|
+
"button",
|
|
125
|
+
{
|
|
126
|
+
type: "button",
|
|
127
|
+
tabIndex: -1,
|
|
128
|
+
"aria-label": name,
|
|
129
|
+
"aria-expanded": selected,
|
|
130
|
+
className: clsx.clsx(className, "focus-visible:focus-indicator cursor-pointer"),
|
|
131
|
+
onClick: () => selected ? onClose() : onSelect(id),
|
|
132
|
+
children: face
|
|
133
|
+
}
|
|
134
|
+
),
|
|
135
|
+
selected && /* @__PURE__ */ jsxRuntime.jsxs(
|
|
136
|
+
"div",
|
|
137
|
+
{
|
|
138
|
+
role: "dialog",
|
|
139
|
+
"aria-label": label,
|
|
140
|
+
className: "inset-s-1/2 absolute bottom-full mb-3 w-max max-w-60 -translate-x-1/2 rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-panel)] p-3 pe-9 text-start text-sm text-[var(--color-text-base)] shadow-lg rtl:translate-x-1/2",
|
|
141
|
+
children: [
|
|
142
|
+
/* @__PURE__ */ jsxRuntime.jsx("p", { className: "font-medium", children: /* @__PURE__ */ jsxRuntime.jsx("bdi", { children: label }) }),
|
|
143
|
+
description != null && /* @__PURE__ */ jsxRuntime.jsx("div", { className: "mt-0.5 text-xs text-[var(--color-text-secondary)]", children: description }),
|
|
144
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
145
|
+
"button",
|
|
146
|
+
{
|
|
147
|
+
type: "button",
|
|
148
|
+
"aria-label": "Close",
|
|
149
|
+
onClick: onClose,
|
|
150
|
+
className: "focus-visible:focus-indicator inset-e-1.5 absolute top-1.5 flex size-6 cursor-pointer items-center justify-center rounded-md text-[var(--color-text-secondary)] hover:bg-gray-100 dark:hover:bg-white/10",
|
|
151
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "size-4", "aria-hidden": "true" })
|
|
152
|
+
}
|
|
153
|
+
)
|
|
154
|
+
]
|
|
155
|
+
}
|
|
156
|
+
)
|
|
157
|
+
] });
|
|
158
|
+
};
|
|
159
|
+
var ClusterPin = ({ count, interactive, onOpen }) => {
|
|
160
|
+
const size = count < 10 ? "size-9" : count < 100 ? "size-11" : "size-14";
|
|
161
|
+
const className = clsx.clsx(
|
|
162
|
+
"bg-brand-600 ring-brand-500/30 flex items-center justify-center rounded-full border-2 border-white text-sm font-semibold text-white shadow-md ring-4",
|
|
163
|
+
size
|
|
164
|
+
);
|
|
165
|
+
if (!interactive) {
|
|
166
|
+
return /* @__PURE__ */ jsxRuntime.jsx("span", { className, "aria-hidden": "true", children: count });
|
|
167
|
+
}
|
|
168
|
+
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
169
|
+
"button",
|
|
170
|
+
{
|
|
171
|
+
type: "button",
|
|
172
|
+
tabIndex: -1,
|
|
173
|
+
"aria-label": `${count} places, zoom in`,
|
|
174
|
+
className: clsx.clsx(className, "focus-visible:focus-indicator cursor-pointer"),
|
|
175
|
+
onClick: onOpen,
|
|
176
|
+
children: count
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
// src/map/mapGeo.ts
|
|
182
|
+
var isDrawable = ({ position: [lng, lat] }) => Number.isFinite(lng) && Number.isFinite(lat) && Math.abs(lng) <= 180 && Math.abs(lat) <= 90;
|
|
183
|
+
var markersToGeoJSON = (markers) => ({
|
|
184
|
+
type: "FeatureCollection",
|
|
185
|
+
features: markers.filter(isDrawable).map(({ id, position }) => ({
|
|
186
|
+
type: "Feature",
|
|
187
|
+
properties: { id },
|
|
188
|
+
geometry: { type: "Point", coordinates: [position[0], position[1]] }
|
|
189
|
+
}))
|
|
190
|
+
});
|
|
191
|
+
var markerBounds = (markers) => {
|
|
192
|
+
const drawable = markers.filter(isDrawable);
|
|
193
|
+
if (drawable.length === 0) return null;
|
|
194
|
+
let west = Infinity;
|
|
195
|
+
let south = Infinity;
|
|
196
|
+
let east = -Infinity;
|
|
197
|
+
let north = -Infinity;
|
|
198
|
+
for (const {
|
|
199
|
+
position: [lng, lat]
|
|
200
|
+
} of drawable) {
|
|
201
|
+
west = Math.min(west, lng);
|
|
202
|
+
east = Math.max(east, lng);
|
|
203
|
+
south = Math.min(south, lat);
|
|
204
|
+
north = Math.max(north, lat);
|
|
205
|
+
}
|
|
206
|
+
return [
|
|
207
|
+
[west, south],
|
|
208
|
+
[east, north]
|
|
209
|
+
];
|
|
210
|
+
};
|
|
211
|
+
var SOURCE = "cosmos-markers";
|
|
212
|
+
var CLUSTER_RADIUS = 48;
|
|
213
|
+
var CLUSTER_MAX_ZOOM = 14;
|
|
214
|
+
var FIT = { padding: 56, maxZoom: CLUSTER_MAX_ZOOM };
|
|
215
|
+
var missingMapLibre = (cause) => new Error(
|
|
216
|
+
'@sckyzo/cosmos-react/map needs the optional peer dependency "maplibre-gl". Install it with `pnpm add maplibre-gl` (or `npm install maplibre-gl`).',
|
|
217
|
+
{ cause }
|
|
218
|
+
);
|
|
219
|
+
var pending;
|
|
220
|
+
var loadMapLibre = () => pending ??= import('maplibre-gl').then(
|
|
221
|
+
// A CommonJS build of the library comes back as `default`.
|
|
222
|
+
(mod) => mod.default ?? mod,
|
|
223
|
+
(error) => {
|
|
224
|
+
pending = void 0;
|
|
225
|
+
throw missingMapLibre(error);
|
|
226
|
+
}
|
|
227
|
+
);
|
|
228
|
+
var raise = (el, on) => {
|
|
229
|
+
el.style.zIndex = on ? "2" : "";
|
|
230
|
+
};
|
|
231
|
+
var MapView = ({
|
|
232
|
+
markers,
|
|
233
|
+
style,
|
|
234
|
+
darkStyle,
|
|
235
|
+
variant = "interactive",
|
|
236
|
+
ariaLabel = "Map",
|
|
237
|
+
height = 360,
|
|
238
|
+
fit = true,
|
|
239
|
+
initialCenter,
|
|
240
|
+
initialZoom,
|
|
241
|
+
cluster = true,
|
|
242
|
+
selectedId: selectedIdProp,
|
|
243
|
+
defaultSelectedId = null,
|
|
244
|
+
onSelectedIdChange,
|
|
245
|
+
onViewChange,
|
|
246
|
+
unavailableMessage = "The map could not be displayed.",
|
|
247
|
+
className,
|
|
248
|
+
ref
|
|
249
|
+
}) => {
|
|
250
|
+
const interactive = variant === "interactive";
|
|
251
|
+
const theme = useCosmosTheme();
|
|
252
|
+
const wantedStyle = theme === "dark" && darkStyle ? darkStyle : style;
|
|
253
|
+
const [selectedId, setSelectedId] = useControllableState({
|
|
254
|
+
value: selectedIdProp,
|
|
255
|
+
defaultValue: defaultSelectedId,
|
|
256
|
+
onChange: onSelectedIdChange,
|
|
257
|
+
name: "MapView.selectedId"
|
|
258
|
+
});
|
|
259
|
+
const containerRef = react.useRef(null);
|
|
260
|
+
const mapRef = react.useRef(null);
|
|
261
|
+
const appliedStyle = react.useRef(wantedStyle);
|
|
262
|
+
const ariaLabelRef = react.useRef(ariaLabel);
|
|
263
|
+
const reveal = react.useRef(void 0);
|
|
264
|
+
const refit = react.useRef(void 0);
|
|
265
|
+
const [visible, setVisible] = react.useState([]);
|
|
266
|
+
const [failure, setFailure] = react.useState();
|
|
267
|
+
const [unavailable, setUnavailable] = react.useState(false);
|
|
268
|
+
const latest = react.useRef({ markers, fit, initialCenter, initialZoom, onViewChange, selectedId });
|
|
269
|
+
react.useInsertionEffect(() => {
|
|
270
|
+
latest.current = { markers, fit, initialCenter, initialZoom, onViewChange, selectedId };
|
|
271
|
+
});
|
|
272
|
+
react.useEffect(() => {
|
|
273
|
+
const container = containerRef.current;
|
|
274
|
+
if (!container) return;
|
|
275
|
+
let disposed = false;
|
|
276
|
+
let map;
|
|
277
|
+
const pins = /* @__PURE__ */ new Map();
|
|
278
|
+
(async () => {
|
|
279
|
+
let ml;
|
|
280
|
+
try {
|
|
281
|
+
ml = await loadMapLibre();
|
|
282
|
+
} catch (error) {
|
|
283
|
+
if (!disposed) setFailure(error);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (disposed) return;
|
|
287
|
+
const {
|
|
288
|
+
markers: initial,
|
|
289
|
+
fit: fitFirst,
|
|
290
|
+
initialCenter: center,
|
|
291
|
+
initialZoom: zoom
|
|
292
|
+
} = latest.current;
|
|
293
|
+
const bounds = fitFirst ? markerBounds(initial) : null;
|
|
294
|
+
try {
|
|
295
|
+
map = new ml.Map({
|
|
296
|
+
container,
|
|
297
|
+
style: appliedStyle.current,
|
|
298
|
+
interactive,
|
|
299
|
+
attributionControl: { compact: false },
|
|
300
|
+
...bounds ? { bounds, fitBoundsOptions: FIT } : { center, zoom }
|
|
301
|
+
});
|
|
302
|
+
} catch {
|
|
303
|
+
if (!disposed) setUnavailable(true);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const current = map;
|
|
307
|
+
mapRef.current = current;
|
|
308
|
+
const canvas = current.getCanvas();
|
|
309
|
+
canvas.setAttribute("aria-label", ariaLabelRef.current);
|
|
310
|
+
if (!interactive) {
|
|
311
|
+
canvas.removeAttribute("tabindex");
|
|
312
|
+
canvas.setAttribute("role", "img");
|
|
313
|
+
}
|
|
314
|
+
const addSource = () => {
|
|
315
|
+
if (current.getSource(SOURCE)) return;
|
|
316
|
+
current.addSource(SOURCE, {
|
|
317
|
+
type: "geojson",
|
|
318
|
+
data: markersToGeoJSON(latest.current.markers),
|
|
319
|
+
cluster,
|
|
320
|
+
clusterRadius: CLUSTER_RADIUS,
|
|
321
|
+
clusterMaxZoom: CLUSTER_MAX_ZOOM
|
|
322
|
+
});
|
|
323
|
+
current.addLayer({
|
|
324
|
+
id: SOURCE,
|
|
325
|
+
type: "circle",
|
|
326
|
+
source: SOURCE,
|
|
327
|
+
paint: { "circle-radius": 0, "circle-opacity": 0 }
|
|
328
|
+
});
|
|
329
|
+
};
|
|
330
|
+
const sync = () => {
|
|
331
|
+
if (!current.getSource(SOURCE)) return;
|
|
332
|
+
const next = /* @__PURE__ */ new Map();
|
|
333
|
+
for (const feature of current.querySourceFeatures(SOURCE)) {
|
|
334
|
+
const { cluster_id: clusterId, point_count: count, id } = feature.properties;
|
|
335
|
+
const [lng, lat] = feature.geometry.coordinates;
|
|
336
|
+
const isCluster = clusterId !== void 0;
|
|
337
|
+
const key = isCluster ? `c:${clusterId}` : `m:${id}`;
|
|
338
|
+
next.set(key, {
|
|
339
|
+
key,
|
|
340
|
+
position: [lng, lat],
|
|
341
|
+
marker: isCluster ? null : String(id),
|
|
342
|
+
cluster: isCluster ? { id: clusterId, count } : null
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
for (const [key, pin] of pins) {
|
|
346
|
+
if (next.has(key)) continue;
|
|
347
|
+
pin.marker.remove();
|
|
348
|
+
pins.delete(key);
|
|
349
|
+
}
|
|
350
|
+
const shown = [];
|
|
351
|
+
for (const [key, entry] of next) {
|
|
352
|
+
let pin = pins.get(key);
|
|
353
|
+
if (!pin) {
|
|
354
|
+
const el = document.createElement("div");
|
|
355
|
+
const marker = new ml.Marker({ element: el }).setLngLat(entry.position).addTo(current);
|
|
356
|
+
el.removeAttribute("role");
|
|
357
|
+
el.removeAttribute("aria-label");
|
|
358
|
+
pin = { marker, el };
|
|
359
|
+
pins.set(key, pin);
|
|
360
|
+
}
|
|
361
|
+
shown.push({ ...entry, el: pin.el });
|
|
362
|
+
}
|
|
363
|
+
setVisible(shown);
|
|
364
|
+
};
|
|
365
|
+
current.on("style.load", () => {
|
|
366
|
+
addSource();
|
|
367
|
+
sync();
|
|
368
|
+
});
|
|
369
|
+
current.on("sourcedata", (event) => {
|
|
370
|
+
if (event.sourceId === SOURCE && event.isSourceLoaded) sync();
|
|
371
|
+
});
|
|
372
|
+
current.on("moveend", () => {
|
|
373
|
+
sync();
|
|
374
|
+
const { lng, lat } = current.getCenter();
|
|
375
|
+
latest.current.onViewChange?.({ center: [lng, lat], zoom: current.getZoom() });
|
|
376
|
+
});
|
|
377
|
+
reveal.current = (id) => {
|
|
378
|
+
const target = latest.current.markers.find((m) => m.id === id);
|
|
379
|
+
if (!target || !isDrawable(target)) return;
|
|
380
|
+
const separate = !pins.has(`m:${id}`);
|
|
381
|
+
current.easeTo({
|
|
382
|
+
center: [target.position[0], target.position[1]],
|
|
383
|
+
zoom: separate ? Math.max(current.getZoom(), CLUSTER_MAX_ZOOM + 1) : current.getZoom(),
|
|
384
|
+
// The popup opens above the pin: keep the pin low enough for it to fit.
|
|
385
|
+
offset: [0, 56]
|
|
386
|
+
});
|
|
387
|
+
};
|
|
388
|
+
refit.current = () => {
|
|
389
|
+
const box = markerBounds(latest.current.markers);
|
|
390
|
+
if (box) current.fitBounds(box, FIT);
|
|
391
|
+
};
|
|
392
|
+
if (latest.current.selectedId) reveal.current(latest.current.selectedId);
|
|
393
|
+
})();
|
|
394
|
+
return () => {
|
|
395
|
+
disposed = true;
|
|
396
|
+
reveal.current = void 0;
|
|
397
|
+
refit.current = void 0;
|
|
398
|
+
for (const { marker } of pins.values()) marker.remove();
|
|
399
|
+
pins.clear();
|
|
400
|
+
map?.remove();
|
|
401
|
+
mapRef.current = null;
|
|
402
|
+
setVisible([]);
|
|
403
|
+
};
|
|
404
|
+
}, [interactive, cluster]);
|
|
405
|
+
react.useEffect(() => {
|
|
406
|
+
ariaLabelRef.current = ariaLabel;
|
|
407
|
+
mapRef.current?.getCanvas().setAttribute("aria-label", ariaLabel);
|
|
408
|
+
}, [ariaLabel]);
|
|
409
|
+
react.useEffect(() => {
|
|
410
|
+
if (appliedStyle.current === wantedStyle) return;
|
|
411
|
+
appliedStyle.current = wantedStyle;
|
|
412
|
+
mapRef.current?.setStyle(wantedStyle, { diff: false });
|
|
413
|
+
}, [wantedStyle]);
|
|
414
|
+
const ids = markers.map((m) => m.id).join("\0");
|
|
415
|
+
const framed = react.useRef(ids);
|
|
416
|
+
react.useEffect(() => {
|
|
417
|
+
const source = mapRef.current?.getSource(SOURCE);
|
|
418
|
+
source?.setData(markersToGeoJSON(markers));
|
|
419
|
+
}, [markers]);
|
|
420
|
+
react.useEffect(() => {
|
|
421
|
+
if (framed.current === ids) return;
|
|
422
|
+
framed.current = ids;
|
|
423
|
+
if (fit) refit.current?.();
|
|
424
|
+
}, [ids, fit]);
|
|
425
|
+
react.useEffect(() => {
|
|
426
|
+
if (selectedId) reveal.current?.(selectedId);
|
|
427
|
+
}, [selectedId]);
|
|
428
|
+
react.useEffect(() => {
|
|
429
|
+
for (const v of visible) raise(v.el, v.marker === selectedId);
|
|
430
|
+
}, [visible, selectedId]);
|
|
431
|
+
if (failure) throw failure;
|
|
432
|
+
const byId = new Map(markers.map((m) => [m.id, m]));
|
|
433
|
+
const zoomBy = (delta) => delta > 0 ? mapRef.current?.zoomIn() : mapRef.current?.zoomOut();
|
|
434
|
+
return /* @__PURE__ */ jsxRuntime.jsxs(
|
|
435
|
+
"div",
|
|
436
|
+
{
|
|
437
|
+
ref,
|
|
438
|
+
className: clsx.clsx(
|
|
439
|
+
"relative isolate w-full overflow-hidden rounded-xl border border-[var(--color-border)] bg-[var(--color-bg-base)]",
|
|
440
|
+
// MapLibre's attribution is a half-white box under the page's text colour, unreadable
|
|
441
|
+
// over dark tiles: it takes the panel and the secondary text, and its links an underline
|
|
442
|
+
// rather than MapLibre's black. `!`: MapLibre's stylesheet is loaded outside the layers.
|
|
443
|
+
"[&_.maplibregl-ctrl-attrib]:bg-[var(--color-bg-panel)]! [&_.maplibregl-ctrl-attrib]:text-[var(--color-text-secondary)] [&_.maplibregl-ctrl-attrib_a]:text-inherit! [&_.maplibregl-ctrl-attrib_a]:underline!",
|
|
444
|
+
className
|
|
445
|
+
),
|
|
446
|
+
style: { height },
|
|
447
|
+
onKeyDown: (event) => {
|
|
448
|
+
if (event.key === "Escape" && selectedId) {
|
|
449
|
+
event.stopPropagation();
|
|
450
|
+
setSelectedId(null);
|
|
451
|
+
}
|
|
452
|
+
},
|
|
453
|
+
children: [
|
|
454
|
+
/* @__PURE__ */ jsxRuntime.jsx("div", { ref: containerRef, className: "size-full" }),
|
|
455
|
+
unavailable && /* @__PURE__ */ jsxRuntime.jsx(
|
|
456
|
+
"p",
|
|
457
|
+
{
|
|
458
|
+
role: "status",
|
|
459
|
+
className: "absolute inset-0 flex items-center justify-center p-6 text-center text-sm text-[var(--color-text-secondary)]",
|
|
460
|
+
children: unavailableMessage
|
|
461
|
+
}
|
|
462
|
+
),
|
|
463
|
+
visible.map((v) => {
|
|
464
|
+
const content = v.cluster ? /* @__PURE__ */ jsxRuntime.jsx(
|
|
465
|
+
ClusterPin,
|
|
466
|
+
{
|
|
467
|
+
count: v.cluster.count,
|
|
468
|
+
interactive,
|
|
469
|
+
onOpen: () => {
|
|
470
|
+
const map = mapRef.current;
|
|
471
|
+
const source = map?.getSource(SOURCE);
|
|
472
|
+
const group = v.cluster;
|
|
473
|
+
if (!map || !source || !group) return;
|
|
474
|
+
source.getClusterExpansionZoom(group.id).then(
|
|
475
|
+
(zoom) => map.easeTo({ center: v.position, zoom }),
|
|
476
|
+
() => void 0
|
|
477
|
+
);
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
) : v.marker !== null && byId.has(v.marker) && /* @__PURE__ */ jsxRuntime.jsx(
|
|
481
|
+
MapPin,
|
|
482
|
+
{
|
|
483
|
+
marker: byId.get(v.marker),
|
|
484
|
+
selected: v.marker === selectedId,
|
|
485
|
+
interactive,
|
|
486
|
+
onSelect: setSelectedId,
|
|
487
|
+
onClose: () => setSelectedId(null)
|
|
488
|
+
}
|
|
489
|
+
);
|
|
490
|
+
return content ? reactDom.createPortal(content, v.el, v.key) : null;
|
|
491
|
+
}),
|
|
492
|
+
interactive && !unavailable && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "inset-e-3 absolute top-3 z-10 flex flex-col overflow-hidden rounded-lg border border-[var(--color-border)] bg-[var(--color-bg-panel)] shadow-sm", children: [
|
|
493
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
494
|
+
"button",
|
|
495
|
+
{
|
|
496
|
+
type: "button",
|
|
497
|
+
"aria-label": "Zoom in",
|
|
498
|
+
onClick: () => zoomBy(1),
|
|
499
|
+
className: "focus-visible:focus-indicator size-ctrl-36 flex cursor-pointer items-center justify-center text-[var(--color-text-base)] hover:bg-gray-100 focus-visible:ring-inset dark:hover:bg-white/10",
|
|
500
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Plus, { className: "size-4", "aria-hidden": "true" })
|
|
501
|
+
}
|
|
502
|
+
),
|
|
503
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
504
|
+
"button",
|
|
505
|
+
{
|
|
506
|
+
type: "button",
|
|
507
|
+
"aria-label": "Zoom out",
|
|
508
|
+
onClick: () => zoomBy(-1),
|
|
509
|
+
className: "focus-visible:focus-indicator size-ctrl-36 flex cursor-pointer items-center justify-center border-t border-[var(--color-border)] text-[var(--color-text-base)] hover:bg-gray-100 focus-visible:ring-inset dark:hover:bg-white/10",
|
|
510
|
+
children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Minus, { className: "size-4", "aria-hidden": "true" })
|
|
511
|
+
}
|
|
512
|
+
)
|
|
513
|
+
] })
|
|
514
|
+
]
|
|
515
|
+
}
|
|
516
|
+
);
|
|
517
|
+
};
|
|
518
|
+
var MapMarkerList = ({
|
|
519
|
+
markers,
|
|
520
|
+
selectedId = null,
|
|
521
|
+
onSelect,
|
|
522
|
+
ariaLabel = "Places",
|
|
523
|
+
className
|
|
524
|
+
}) => /* @__PURE__ */ jsxRuntime.jsx("ul", { "aria-label": ariaLabel, className: clsx.clsx("flex flex-col gap-1", className), children: markers.map(({ id, label, description }) => {
|
|
525
|
+
const selected = id === selectedId;
|
|
526
|
+
return /* @__PURE__ */ jsxRuntime.jsx("li", { children: /* @__PURE__ */ jsxRuntime.jsxs(
|
|
527
|
+
"button",
|
|
528
|
+
{
|
|
529
|
+
type: "button",
|
|
530
|
+
"aria-pressed": selected,
|
|
531
|
+
onClick: () => onSelect?.(id),
|
|
532
|
+
className: clsx.clsx(
|
|
533
|
+
"focus-visible:focus-indicator py-row-8 flex w-full cursor-pointer items-start gap-3 rounded-lg border px-3 text-start text-sm",
|
|
534
|
+
selected ? "border-brand-300 bg-brand-50 dark:border-brand-500/40 dark:bg-brand-500/10" : "border-transparent hover:bg-gray-100 dark:hover:bg-white/5"
|
|
535
|
+
),
|
|
536
|
+
children: [
|
|
537
|
+
/* @__PURE__ */ jsxRuntime.jsx(
|
|
538
|
+
lucideReact.MapPin,
|
|
539
|
+
{
|
|
540
|
+
className: clsx.clsx(
|
|
541
|
+
"mt-0.5 size-4 shrink-0",
|
|
542
|
+
selected ? "text-brand-600 dark:text-brand-400" : "text-[var(--color-text-secondary)]"
|
|
543
|
+
),
|
|
544
|
+
"aria-hidden": "true"
|
|
545
|
+
}
|
|
546
|
+
),
|
|
547
|
+
/* @__PURE__ */ jsxRuntime.jsxs("span", { className: "min-w-0", children: [
|
|
548
|
+
/* @__PURE__ */ jsxRuntime.jsx("span", { className: "block font-medium text-[var(--color-text-base)]", children: /* @__PURE__ */ jsxRuntime.jsx("bdi", { children: label }) }),
|
|
549
|
+
description != null && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "block text-xs text-[var(--color-text-secondary)]", children: description })
|
|
550
|
+
] })
|
|
551
|
+
]
|
|
552
|
+
}
|
|
553
|
+
) }, id);
|
|
554
|
+
}) });
|
|
555
|
+
|
|
556
|
+
exports.MapMarkerList = MapMarkerList;
|
|
557
|
+
exports.MapView = MapView;
|
|
558
|
+
//# sourceMappingURL=index.cjs.map
|
|
559
|
+
//# sourceMappingURL=index.cjs.map
|