@ship-it-ui/map 0.0.2
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/LICENSE +21 -0
- package/dist/index.cjs +219 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +80 -0
- package/dist/index.d.ts +80 -0
- package/dist/index.js +180 -0
- package/dist/index.js.map +1 -0
- package/package.json +79 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ship-it-ops
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __export = (target, all) => {
|
|
11
|
+
for (var name in all)
|
|
12
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
+
};
|
|
14
|
+
var __copyProps = (to, from, except, desc) => {
|
|
15
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
16
|
+
for (let key of __getOwnPropNames(from))
|
|
17
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
18
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
19
|
+
}
|
|
20
|
+
return to;
|
|
21
|
+
};
|
|
22
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
23
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
24
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
25
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
26
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
27
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
28
|
+
mod
|
|
29
|
+
));
|
|
30
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
31
|
+
|
|
32
|
+
// src/index.ts
|
|
33
|
+
var index_exports = {};
|
|
34
|
+
__export(index_exports, {
|
|
35
|
+
Map: () => Map,
|
|
36
|
+
MapMarker: () => MapMarker,
|
|
37
|
+
useMap: () => useMap
|
|
38
|
+
});
|
|
39
|
+
module.exports = __toCommonJS(index_exports);
|
|
40
|
+
|
|
41
|
+
// src/Map.tsx
|
|
42
|
+
var import_react2 = require("react");
|
|
43
|
+
var import_client = require("react-dom/client");
|
|
44
|
+
|
|
45
|
+
// src/MapMarker.tsx
|
|
46
|
+
var import_icons = require("@ship-it-ui/icons");
|
|
47
|
+
var import_react = require("react");
|
|
48
|
+
var import_jsx_runtime = require("react/jsx-runtime");
|
|
49
|
+
var variantClasses = {
|
|
50
|
+
default: "bg-panel text-text border-border",
|
|
51
|
+
accent: "bg-accent text-on-accent border-accent",
|
|
52
|
+
sale: "bg-sale text-on-accent border-sale"
|
|
53
|
+
};
|
|
54
|
+
var MapMarker = (0, import_react.forwardRef)(function MapMarker2({ label, icon, variant = "default", selected, onClick }, ref) {
|
|
55
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
56
|
+
"button",
|
|
57
|
+
{
|
|
58
|
+
ref,
|
|
59
|
+
type: "button",
|
|
60
|
+
onClick,
|
|
61
|
+
"aria-pressed": selected,
|
|
62
|
+
className: [
|
|
63
|
+
"inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-[12px] font-semibold shadow-md",
|
|
64
|
+
"transition-transform duration-(--duration-micro)",
|
|
65
|
+
"cursor-pointer hover:scale-105",
|
|
66
|
+
selected ? "ring-accent-glow scale-110 ring-2" : "",
|
|
67
|
+
variantClasses[variant]
|
|
68
|
+
].join(" "),
|
|
69
|
+
children: [
|
|
70
|
+
icon && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_icons.DynamicIconGlyph, { name: icon, size: 14 }),
|
|
71
|
+
label && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: label })
|
|
72
|
+
]
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
});
|
|
76
|
+
MapMarker.displayName = "MapMarker";
|
|
77
|
+
|
|
78
|
+
// src/Map.tsx
|
|
79
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
80
|
+
var NativeMap = globalThis.Map;
|
|
81
|
+
var DEFAULT_TILE_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
|
82
|
+
var Map = (0, import_react2.forwardRef)(function Map2({
|
|
83
|
+
center,
|
|
84
|
+
zoom = 12,
|
|
85
|
+
tileUrl = DEFAULT_TILE_URL,
|
|
86
|
+
markers,
|
|
87
|
+
selectedId,
|
|
88
|
+
onSelect,
|
|
89
|
+
style,
|
|
90
|
+
className,
|
|
91
|
+
"aria-label": ariaLabel = "Map"
|
|
92
|
+
}, ref) {
|
|
93
|
+
const containerRef = (0, import_react2.useRef)(null);
|
|
94
|
+
const mapRef = (0, import_react2.useRef)(null);
|
|
95
|
+
const markerRefs = (0, import_react2.useRef)(new NativeMap());
|
|
96
|
+
(0, import_react2.useImperativeHandle)(
|
|
97
|
+
ref,
|
|
98
|
+
() => ({
|
|
99
|
+
flyTo({ center: c, zoom: z }) {
|
|
100
|
+
mapRef.current?.flyTo({ center: c, zoom: z });
|
|
101
|
+
},
|
|
102
|
+
get raw() {
|
|
103
|
+
return mapRef.current;
|
|
104
|
+
}
|
|
105
|
+
}),
|
|
106
|
+
[]
|
|
107
|
+
);
|
|
108
|
+
(0, import_react2.useEffect)(() => {
|
|
109
|
+
if (!containerRef.current) return;
|
|
110
|
+
let cancelled = false;
|
|
111
|
+
(async () => {
|
|
112
|
+
const maplibre = await import("maplibre-gl").catch(() => null);
|
|
113
|
+
if (!maplibre || cancelled || !containerRef.current) return;
|
|
114
|
+
const map = new maplibre.Map({
|
|
115
|
+
container: containerRef.current,
|
|
116
|
+
style: {
|
|
117
|
+
version: 8,
|
|
118
|
+
sources: {
|
|
119
|
+
osm: {
|
|
120
|
+
type: "raster",
|
|
121
|
+
tiles: [tileUrl],
|
|
122
|
+
tileSize: 256,
|
|
123
|
+
attribution: "\xA9 OpenStreetMap contributors"
|
|
124
|
+
}
|
|
125
|
+
},
|
|
126
|
+
layers: [{ id: "osm", type: "raster", source: "osm" }]
|
|
127
|
+
},
|
|
128
|
+
center: [...center],
|
|
129
|
+
zoom
|
|
130
|
+
});
|
|
131
|
+
mapRef.current = map;
|
|
132
|
+
})();
|
|
133
|
+
const markersAtMount = markerRefs.current;
|
|
134
|
+
return () => {
|
|
135
|
+
cancelled = true;
|
|
136
|
+
markersAtMount.forEach(({ marker, root }) => {
|
|
137
|
+
try {
|
|
138
|
+
root.unmount();
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
marker.remove?.();
|
|
142
|
+
});
|
|
143
|
+
markersAtMount.clear();
|
|
144
|
+
mapRef.current?.remove?.();
|
|
145
|
+
mapRef.current = null;
|
|
146
|
+
};
|
|
147
|
+
}, []);
|
|
148
|
+
(0, import_react2.useEffect)(() => {
|
|
149
|
+
const map = mapRef.current;
|
|
150
|
+
if (!map || !markers) return;
|
|
151
|
+
let cancelled = false;
|
|
152
|
+
(async () => {
|
|
153
|
+
const maplibre = await import("maplibre-gl").catch(() => null);
|
|
154
|
+
if (!maplibre || cancelled) return;
|
|
155
|
+
const next = new Set(markers.map((m) => m.id));
|
|
156
|
+
markerRefs.current.forEach((entry, id) => {
|
|
157
|
+
if (!next.has(id)) {
|
|
158
|
+
try {
|
|
159
|
+
entry.root.unmount();
|
|
160
|
+
} catch {
|
|
161
|
+
}
|
|
162
|
+
entry.marker.remove?.();
|
|
163
|
+
markerRefs.current.delete(id);
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
for (const data of markers) {
|
|
167
|
+
const existing = markerRefs.current.get(data.id);
|
|
168
|
+
const el = existing?.marker.getElement() ?? document.createElement("div");
|
|
169
|
+
el.style.cursor = "pointer";
|
|
170
|
+
const root = existing?.root ?? (0, import_client.createRoot)(el);
|
|
171
|
+
root.render(
|
|
172
|
+
/* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
173
|
+
MapMarker,
|
|
174
|
+
{
|
|
175
|
+
label: data.label,
|
|
176
|
+
icon: data.icon,
|
|
177
|
+
variant: data.variant,
|
|
178
|
+
selected: data.id === selectedId,
|
|
179
|
+
onClick: () => onSelect?.(data)
|
|
180
|
+
}
|
|
181
|
+
)
|
|
182
|
+
);
|
|
183
|
+
if (existing) {
|
|
184
|
+
existing.marker.setLngLat([...data.location]);
|
|
185
|
+
} else {
|
|
186
|
+
const marker = new maplibre.Marker({ element: el, anchor: "bottom" }).setLngLat([...data.location]).addTo(map);
|
|
187
|
+
markerRefs.current.set(data.id, { marker, root });
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
})();
|
|
191
|
+
return () => {
|
|
192
|
+
cancelled = true;
|
|
193
|
+
};
|
|
194
|
+
}, [markers, selectedId, onSelect]);
|
|
195
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
196
|
+
"div",
|
|
197
|
+
{
|
|
198
|
+
role: "region",
|
|
199
|
+
"aria-label": ariaLabel,
|
|
200
|
+
className,
|
|
201
|
+
style: { position: "relative", height: 400, width: "100%", ...style },
|
|
202
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: containerRef, style: { position: "absolute", inset: 0 } })
|
|
203
|
+
}
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
Map.displayName = "Map";
|
|
207
|
+
|
|
208
|
+
// src/useMap.ts
|
|
209
|
+
var import_react3 = require("react");
|
|
210
|
+
function useMap() {
|
|
211
|
+
return (0, import_react3.useRef)(null);
|
|
212
|
+
}
|
|
213
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
214
|
+
0 && (module.exports = {
|
|
215
|
+
Map,
|
|
216
|
+
MapMarker,
|
|
217
|
+
useMap
|
|
218
|
+
});
|
|
219
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/Map.tsx","../src/MapMarker.tsx","../src/useMap.ts"],"sourcesContent":["export { Map, type MapProps, type MapHandle } from './Map';\nexport { MapMarker, type MapMarkerProps } from './MapMarker';\nexport { useMap } from './useMap';\nexport type { LngLat, MapMarkerData } from './types';\n","'use client';\n\nimport { forwardRef, useEffect, useImperativeHandle, useRef, type CSSProperties } from 'react';\nimport { createRoot, type Root } from 'react-dom/client';\n\nimport { MapMarker } from './MapMarker';\nimport type { LngLat, MapMarkerData } from './types';\n\n/**\n * Map — thin React wrapper around MapLibre GL JS. Loaded lazily so the\n * ~700 KB library only enters the bundle of consumers that actually use the\n * map.\n *\n * Tile URL defaults to OpenStreetMap raster so the component renders out of\n * the box. For production traffic, set `tileUrl` to a provider you have an\n * agreement with (MapTiler, Mapbox via maplibre style URL, Stadia, etc.).\n */\n\n/**\n * Aliased Map constructor — our exported `Map` component shadows the global\n * `Map` identifier in this module, so direct `new Map()` would try to\n * instantiate the React component. `NativeMap` keeps the type-safe path.\n */\nconst NativeMap: MapConstructor = globalThis.Map;\n\nexport interface MapHandle {\n /** Re-center the viewport. */\n flyTo(opts: { center: LngLat; zoom?: number }): void;\n /** Underlying MapLibre instance — escape hatch for unsupported features. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n raw: any | null;\n}\n\nexport interface MapProps {\n /** Initial center as `[lng, lat]`. */\n center: LngLat;\n /** Initial zoom (0–22). Default 12. */\n zoom?: number;\n /** Tile URL template. Default: OSM raster tiles. */\n tileUrl?: string;\n /** Marker data — re-rendered when the array identity changes. */\n markers?: ReadonlyArray<MapMarkerData>;\n /** Currently selected marker `id`. */\n selectedId?: string;\n /** Fires when a marker is clicked. */\n onSelect?: (marker: MapMarkerData) => void;\n /** Width / height override. */\n style?: CSSProperties;\n className?: string;\n /** Accessible label for the map region. */\n 'aria-label'?: string;\n}\n\nconst DEFAULT_TILE_URL = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';\n\nexport const Map = forwardRef<MapHandle, MapProps>(function Map(\n {\n center,\n zoom = 12,\n tileUrl = DEFAULT_TILE_URL,\n markers,\n selectedId,\n onSelect,\n style,\n className,\n 'aria-label': ariaLabel = 'Map',\n },\n ref,\n) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const mapRef = useRef<any | null>(null);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const markerRefs = useRef(new NativeMap<string, { marker: any; root: Root }>());\n\n useImperativeHandle(\n ref,\n () => ({\n flyTo({ center: c, zoom: z }) {\n mapRef.current?.flyTo({ center: c, zoom: z });\n },\n get raw() {\n return mapRef.current;\n },\n }),\n [],\n );\n\n // Initialize MapLibre lazily so SSR / non-map consumers don't import it.\n useEffect(() => {\n if (!containerRef.current) return;\n let cancelled = false;\n\n (async () => {\n const maplibre = await import('maplibre-gl').catch(() => null);\n if (!maplibre || cancelled || !containerRef.current) return;\n\n const map = new maplibre.Map({\n container: containerRef.current,\n style: {\n version: 8,\n sources: {\n osm: {\n type: 'raster',\n tiles: [tileUrl],\n tileSize: 256,\n attribution: '© OpenStreetMap contributors',\n },\n },\n layers: [{ id: 'osm', type: 'raster', source: 'osm' }],\n },\n center: [...center],\n zoom,\n });\n mapRef.current = map;\n })();\n\n const markersAtMount = markerRefs.current;\n return () => {\n cancelled = true;\n // Tear down markers first so React roots unmount cleanly.\n markersAtMount.forEach(({ marker, root }) => {\n try {\n root.unmount();\n } catch {\n // ignore unmount race\n }\n marker.remove?.();\n });\n markersAtMount.clear();\n mapRef.current?.remove?.();\n mapRef.current = null;\n };\n // tileUrl is read at init time; consumers should set it once.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Render / update markers.\n useEffect(() => {\n const map = mapRef.current;\n if (!map || !markers) return;\n let cancelled = false;\n\n (async () => {\n const maplibre = await import('maplibre-gl').catch(() => null);\n if (!maplibre || cancelled) return;\n\n // Diff: remove markers no longer present.\n const next = new Set(markers.map((m) => m.id));\n markerRefs.current.forEach((entry, id) => {\n if (!next.has(id)) {\n try {\n entry.root.unmount();\n } catch {\n // ignore\n }\n entry.marker.remove?.();\n markerRefs.current.delete(id);\n }\n });\n\n // Upsert each.\n for (const data of markers) {\n const existing = markerRefs.current.get(data.id);\n const el = existing?.marker.getElement() ?? document.createElement('div');\n el.style.cursor = 'pointer';\n\n const root = existing?.root ?? createRoot(el);\n root.render(\n <MapMarker\n label={data.label}\n icon={data.icon}\n variant={data.variant}\n selected={data.id === selectedId}\n onClick={() => onSelect?.(data)}\n />,\n );\n\n if (existing) {\n existing.marker.setLngLat([...data.location]);\n } else {\n const marker = new maplibre.Marker({ element: el, anchor: 'bottom' })\n .setLngLat([...data.location])\n .addTo(map);\n markerRefs.current.set(data.id, { marker, root });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [markers, selectedId, onSelect]);\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel}\n className={className}\n style={{ position: 'relative', height: 400, width: '100%', ...style }}\n >\n <div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />\n </div>\n );\n});\n\nMap.displayName = 'Map';\n","'use client';\n\nimport { DynamicIconGlyph } from '@ship-it-ui/icons';\nimport { forwardRef, type ReactNode } from 'react';\n\n/**\n * MapMarker — DOM marker styled with design-system tokens. Rendered by the\n * `<Map>` component for each entry in its `markers` prop; you usually don't\n * instantiate it directly. Exported so consumers can render their own\n * markers via the `useMap()` API and `marker.setDOMContent()`.\n */\n\nexport interface MapMarkerProps {\n label?: ReactNode;\n icon?: string;\n variant?: 'default' | 'accent' | 'sale';\n selected?: boolean;\n onClick?: () => void;\n}\n\nconst variantClasses = {\n default: 'bg-panel text-text border-border',\n accent: 'bg-accent text-on-accent border-accent',\n sale: 'bg-sale text-on-accent border-sale',\n} as const;\n\nexport const MapMarker = forwardRef<HTMLButtonElement, MapMarkerProps>(function MapMarker(\n { label, icon, variant = 'default', selected, onClick },\n ref,\n) {\n return (\n <button\n ref={ref}\n type=\"button\"\n onClick={onClick}\n aria-pressed={selected}\n className={[\n 'inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-[12px] font-semibold shadow-md',\n 'transition-transform duration-(--duration-micro)',\n 'cursor-pointer hover:scale-105',\n selected ? 'ring-accent-glow scale-110 ring-2' : '',\n variantClasses[variant],\n ].join(' ')}\n >\n {icon && <DynamicIconGlyph name={icon} size={14} />}\n {label && <span>{label}</span>}\n </button>\n );\n});\nMapMarker.displayName = 'MapMarker';\n","'use client';\n\nimport { useRef } from 'react';\n\nimport type { MapHandle } from './Map';\n\n/**\n * Convenience hook that creates a stable `ref` for the imperative `MapHandle`.\n * Use when you need to programmatically `flyTo` or reach the raw MapLibre\n * instance.\n *\n * ```tsx\n * const mapRef = useMap();\n * <Map ref={mapRef} center={[-122.4, 37.8]} />\n * mapRef.current?.flyTo({ center: [-118.2, 34.0], zoom: 10 });\n * ```\n */\nexport function useMap() {\n return useRef<MapHandle | null>(null);\n}\n"],"mappings":"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","names":["import_react","MapMarker","import_jsx_runtime","Map","import_react"]}
|
package/dist/index.d.cts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public types for `@ship-it-ui/map`.
|
|
6
|
+
*/
|
|
7
|
+
/** A `[longitude, latitude]` pair. MapLibre uses lng/lat ordering. */
|
|
8
|
+
type LngLat = readonly [number, number];
|
|
9
|
+
interface MapMarkerData {
|
|
10
|
+
/** Stable identifier — used as React key. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Marker coordinate. */
|
|
13
|
+
location: LngLat;
|
|
14
|
+
/** Optional label shown next to the pin (e.g. a price). */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Optional icon name (`@ship-it-ui/icons` glyph). */
|
|
17
|
+
icon?: string;
|
|
18
|
+
/** Visual variant. Default `'default'`. */
|
|
19
|
+
variant?: 'default' | 'accent' | 'sale';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface MapHandle {
|
|
23
|
+
/** Re-center the viewport. */
|
|
24
|
+
flyTo(opts: {
|
|
25
|
+
center: LngLat;
|
|
26
|
+
zoom?: number;
|
|
27
|
+
}): void;
|
|
28
|
+
/** Underlying MapLibre instance — escape hatch for unsupported features. */
|
|
29
|
+
raw: any | null;
|
|
30
|
+
}
|
|
31
|
+
interface MapProps {
|
|
32
|
+
/** Initial center as `[lng, lat]`. */
|
|
33
|
+
center: LngLat;
|
|
34
|
+
/** Initial zoom (0–22). Default 12. */
|
|
35
|
+
zoom?: number;
|
|
36
|
+
/** Tile URL template. Default: OSM raster tiles. */
|
|
37
|
+
tileUrl?: string;
|
|
38
|
+
/** Marker data — re-rendered when the array identity changes. */
|
|
39
|
+
markers?: ReadonlyArray<MapMarkerData>;
|
|
40
|
+
/** Currently selected marker `id`. */
|
|
41
|
+
selectedId?: string;
|
|
42
|
+
/** Fires when a marker is clicked. */
|
|
43
|
+
onSelect?: (marker: MapMarkerData) => void;
|
|
44
|
+
/** Width / height override. */
|
|
45
|
+
style?: CSSProperties;
|
|
46
|
+
className?: string;
|
|
47
|
+
/** Accessible label for the map region. */
|
|
48
|
+
'aria-label'?: string;
|
|
49
|
+
}
|
|
50
|
+
declare const Map: react.ForwardRefExoticComponent<MapProps & react.RefAttributes<MapHandle>>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* MapMarker — DOM marker styled with design-system tokens. Rendered by the
|
|
54
|
+
* `<Map>` component for each entry in its `markers` prop; you usually don't
|
|
55
|
+
* instantiate it directly. Exported so consumers can render their own
|
|
56
|
+
* markers via the `useMap()` API and `marker.setDOMContent()`.
|
|
57
|
+
*/
|
|
58
|
+
interface MapMarkerProps {
|
|
59
|
+
label?: ReactNode;
|
|
60
|
+
icon?: string;
|
|
61
|
+
variant?: 'default' | 'accent' | 'sale';
|
|
62
|
+
selected?: boolean;
|
|
63
|
+
onClick?: () => void;
|
|
64
|
+
}
|
|
65
|
+
declare const MapMarker: react.ForwardRefExoticComponent<MapMarkerProps & react.RefAttributes<HTMLButtonElement>>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convenience hook that creates a stable `ref` for the imperative `MapHandle`.
|
|
69
|
+
* Use when you need to programmatically `flyTo` or reach the raw MapLibre
|
|
70
|
+
* instance.
|
|
71
|
+
*
|
|
72
|
+
* ```tsx
|
|
73
|
+
* const mapRef = useMap();
|
|
74
|
+
* <Map ref={mapRef} center={[-122.4, 37.8]} />
|
|
75
|
+
* mapRef.current?.flyTo({ center: [-118.2, 34.0], zoom: 10 });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
declare function useMap(): react.MutableRefObject<MapHandle | null>;
|
|
79
|
+
|
|
80
|
+
export { type LngLat, Map, type MapHandle, MapMarker, type MapMarkerData, type MapMarkerProps, type MapProps, useMap };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import * as react from 'react';
|
|
2
|
+
import { CSSProperties, ReactNode } from 'react';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Public types for `@ship-it-ui/map`.
|
|
6
|
+
*/
|
|
7
|
+
/** A `[longitude, latitude]` pair. MapLibre uses lng/lat ordering. */
|
|
8
|
+
type LngLat = readonly [number, number];
|
|
9
|
+
interface MapMarkerData {
|
|
10
|
+
/** Stable identifier — used as React key. */
|
|
11
|
+
id: string;
|
|
12
|
+
/** Marker coordinate. */
|
|
13
|
+
location: LngLat;
|
|
14
|
+
/** Optional label shown next to the pin (e.g. a price). */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Optional icon name (`@ship-it-ui/icons` glyph). */
|
|
17
|
+
icon?: string;
|
|
18
|
+
/** Visual variant. Default `'default'`. */
|
|
19
|
+
variant?: 'default' | 'accent' | 'sale';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface MapHandle {
|
|
23
|
+
/** Re-center the viewport. */
|
|
24
|
+
flyTo(opts: {
|
|
25
|
+
center: LngLat;
|
|
26
|
+
zoom?: number;
|
|
27
|
+
}): void;
|
|
28
|
+
/** Underlying MapLibre instance — escape hatch for unsupported features. */
|
|
29
|
+
raw: any | null;
|
|
30
|
+
}
|
|
31
|
+
interface MapProps {
|
|
32
|
+
/** Initial center as `[lng, lat]`. */
|
|
33
|
+
center: LngLat;
|
|
34
|
+
/** Initial zoom (0–22). Default 12. */
|
|
35
|
+
zoom?: number;
|
|
36
|
+
/** Tile URL template. Default: OSM raster tiles. */
|
|
37
|
+
tileUrl?: string;
|
|
38
|
+
/** Marker data — re-rendered when the array identity changes. */
|
|
39
|
+
markers?: ReadonlyArray<MapMarkerData>;
|
|
40
|
+
/** Currently selected marker `id`. */
|
|
41
|
+
selectedId?: string;
|
|
42
|
+
/** Fires when a marker is clicked. */
|
|
43
|
+
onSelect?: (marker: MapMarkerData) => void;
|
|
44
|
+
/** Width / height override. */
|
|
45
|
+
style?: CSSProperties;
|
|
46
|
+
className?: string;
|
|
47
|
+
/** Accessible label for the map region. */
|
|
48
|
+
'aria-label'?: string;
|
|
49
|
+
}
|
|
50
|
+
declare const Map: react.ForwardRefExoticComponent<MapProps & react.RefAttributes<MapHandle>>;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* MapMarker — DOM marker styled with design-system tokens. Rendered by the
|
|
54
|
+
* `<Map>` component for each entry in its `markers` prop; you usually don't
|
|
55
|
+
* instantiate it directly. Exported so consumers can render their own
|
|
56
|
+
* markers via the `useMap()` API and `marker.setDOMContent()`.
|
|
57
|
+
*/
|
|
58
|
+
interface MapMarkerProps {
|
|
59
|
+
label?: ReactNode;
|
|
60
|
+
icon?: string;
|
|
61
|
+
variant?: 'default' | 'accent' | 'sale';
|
|
62
|
+
selected?: boolean;
|
|
63
|
+
onClick?: () => void;
|
|
64
|
+
}
|
|
65
|
+
declare const MapMarker: react.ForwardRefExoticComponent<MapMarkerProps & react.RefAttributes<HTMLButtonElement>>;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Convenience hook that creates a stable `ref` for the imperative `MapHandle`.
|
|
69
|
+
* Use when you need to programmatically `flyTo` or reach the raw MapLibre
|
|
70
|
+
* instance.
|
|
71
|
+
*
|
|
72
|
+
* ```tsx
|
|
73
|
+
* const mapRef = useMap();
|
|
74
|
+
* <Map ref={mapRef} center={[-122.4, 37.8]} />
|
|
75
|
+
* mapRef.current?.flyTo({ center: [-118.2, 34.0], zoom: 10 });
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
declare function useMap(): react.MutableRefObject<MapHandle | null>;
|
|
79
|
+
|
|
80
|
+
export { type LngLat, Map, type MapHandle, MapMarker, type MapMarkerData, type MapMarkerProps, type MapProps, useMap };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
// src/Map.tsx
|
|
4
|
+
import { forwardRef as forwardRef2, useEffect, useImperativeHandle, useRef } from "react";
|
|
5
|
+
import { createRoot } from "react-dom/client";
|
|
6
|
+
|
|
7
|
+
// src/MapMarker.tsx
|
|
8
|
+
import { DynamicIconGlyph } from "@ship-it-ui/icons";
|
|
9
|
+
import { forwardRef } from "react";
|
|
10
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
11
|
+
var variantClasses = {
|
|
12
|
+
default: "bg-panel text-text border-border",
|
|
13
|
+
accent: "bg-accent text-on-accent border-accent",
|
|
14
|
+
sale: "bg-sale text-on-accent border-sale"
|
|
15
|
+
};
|
|
16
|
+
var MapMarker = forwardRef(function MapMarker2({ label, icon, variant = "default", selected, onClick }, ref) {
|
|
17
|
+
return /* @__PURE__ */ jsxs(
|
|
18
|
+
"button",
|
|
19
|
+
{
|
|
20
|
+
ref,
|
|
21
|
+
type: "button",
|
|
22
|
+
onClick,
|
|
23
|
+
"aria-pressed": selected,
|
|
24
|
+
className: [
|
|
25
|
+
"inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-[12px] font-semibold shadow-md",
|
|
26
|
+
"transition-transform duration-(--duration-micro)",
|
|
27
|
+
"cursor-pointer hover:scale-105",
|
|
28
|
+
selected ? "ring-accent-glow scale-110 ring-2" : "",
|
|
29
|
+
variantClasses[variant]
|
|
30
|
+
].join(" "),
|
|
31
|
+
children: [
|
|
32
|
+
icon && /* @__PURE__ */ jsx(DynamicIconGlyph, { name: icon, size: 14 }),
|
|
33
|
+
label && /* @__PURE__ */ jsx("span", { children: label })
|
|
34
|
+
]
|
|
35
|
+
}
|
|
36
|
+
);
|
|
37
|
+
});
|
|
38
|
+
MapMarker.displayName = "MapMarker";
|
|
39
|
+
|
|
40
|
+
// src/Map.tsx
|
|
41
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
42
|
+
var NativeMap = globalThis.Map;
|
|
43
|
+
var DEFAULT_TILE_URL = "https://tile.openstreetmap.org/{z}/{x}/{y}.png";
|
|
44
|
+
var Map = forwardRef2(function Map2({
|
|
45
|
+
center,
|
|
46
|
+
zoom = 12,
|
|
47
|
+
tileUrl = DEFAULT_TILE_URL,
|
|
48
|
+
markers,
|
|
49
|
+
selectedId,
|
|
50
|
+
onSelect,
|
|
51
|
+
style,
|
|
52
|
+
className,
|
|
53
|
+
"aria-label": ariaLabel = "Map"
|
|
54
|
+
}, ref) {
|
|
55
|
+
const containerRef = useRef(null);
|
|
56
|
+
const mapRef = useRef(null);
|
|
57
|
+
const markerRefs = useRef(new NativeMap());
|
|
58
|
+
useImperativeHandle(
|
|
59
|
+
ref,
|
|
60
|
+
() => ({
|
|
61
|
+
flyTo({ center: c, zoom: z }) {
|
|
62
|
+
mapRef.current?.flyTo({ center: c, zoom: z });
|
|
63
|
+
},
|
|
64
|
+
get raw() {
|
|
65
|
+
return mapRef.current;
|
|
66
|
+
}
|
|
67
|
+
}),
|
|
68
|
+
[]
|
|
69
|
+
);
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
if (!containerRef.current) return;
|
|
72
|
+
let cancelled = false;
|
|
73
|
+
(async () => {
|
|
74
|
+
const maplibre = await import("maplibre-gl").catch(() => null);
|
|
75
|
+
if (!maplibre || cancelled || !containerRef.current) return;
|
|
76
|
+
const map = new maplibre.Map({
|
|
77
|
+
container: containerRef.current,
|
|
78
|
+
style: {
|
|
79
|
+
version: 8,
|
|
80
|
+
sources: {
|
|
81
|
+
osm: {
|
|
82
|
+
type: "raster",
|
|
83
|
+
tiles: [tileUrl],
|
|
84
|
+
tileSize: 256,
|
|
85
|
+
attribution: "\xA9 OpenStreetMap contributors"
|
|
86
|
+
}
|
|
87
|
+
},
|
|
88
|
+
layers: [{ id: "osm", type: "raster", source: "osm" }]
|
|
89
|
+
},
|
|
90
|
+
center: [...center],
|
|
91
|
+
zoom
|
|
92
|
+
});
|
|
93
|
+
mapRef.current = map;
|
|
94
|
+
})();
|
|
95
|
+
const markersAtMount = markerRefs.current;
|
|
96
|
+
return () => {
|
|
97
|
+
cancelled = true;
|
|
98
|
+
markersAtMount.forEach(({ marker, root }) => {
|
|
99
|
+
try {
|
|
100
|
+
root.unmount();
|
|
101
|
+
} catch {
|
|
102
|
+
}
|
|
103
|
+
marker.remove?.();
|
|
104
|
+
});
|
|
105
|
+
markersAtMount.clear();
|
|
106
|
+
mapRef.current?.remove?.();
|
|
107
|
+
mapRef.current = null;
|
|
108
|
+
};
|
|
109
|
+
}, []);
|
|
110
|
+
useEffect(() => {
|
|
111
|
+
const map = mapRef.current;
|
|
112
|
+
if (!map || !markers) return;
|
|
113
|
+
let cancelled = false;
|
|
114
|
+
(async () => {
|
|
115
|
+
const maplibre = await import("maplibre-gl").catch(() => null);
|
|
116
|
+
if (!maplibre || cancelled) return;
|
|
117
|
+
const next = new Set(markers.map((m) => m.id));
|
|
118
|
+
markerRefs.current.forEach((entry, id) => {
|
|
119
|
+
if (!next.has(id)) {
|
|
120
|
+
try {
|
|
121
|
+
entry.root.unmount();
|
|
122
|
+
} catch {
|
|
123
|
+
}
|
|
124
|
+
entry.marker.remove?.();
|
|
125
|
+
markerRefs.current.delete(id);
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
for (const data of markers) {
|
|
129
|
+
const existing = markerRefs.current.get(data.id);
|
|
130
|
+
const el = existing?.marker.getElement() ?? document.createElement("div");
|
|
131
|
+
el.style.cursor = "pointer";
|
|
132
|
+
const root = existing?.root ?? createRoot(el);
|
|
133
|
+
root.render(
|
|
134
|
+
/* @__PURE__ */ jsx2(
|
|
135
|
+
MapMarker,
|
|
136
|
+
{
|
|
137
|
+
label: data.label,
|
|
138
|
+
icon: data.icon,
|
|
139
|
+
variant: data.variant,
|
|
140
|
+
selected: data.id === selectedId,
|
|
141
|
+
onClick: () => onSelect?.(data)
|
|
142
|
+
}
|
|
143
|
+
)
|
|
144
|
+
);
|
|
145
|
+
if (existing) {
|
|
146
|
+
existing.marker.setLngLat([...data.location]);
|
|
147
|
+
} else {
|
|
148
|
+
const marker = new maplibre.Marker({ element: el, anchor: "bottom" }).setLngLat([...data.location]).addTo(map);
|
|
149
|
+
markerRefs.current.set(data.id, { marker, root });
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
})();
|
|
153
|
+
return () => {
|
|
154
|
+
cancelled = true;
|
|
155
|
+
};
|
|
156
|
+
}, [markers, selectedId, onSelect]);
|
|
157
|
+
return /* @__PURE__ */ jsx2(
|
|
158
|
+
"div",
|
|
159
|
+
{
|
|
160
|
+
role: "region",
|
|
161
|
+
"aria-label": ariaLabel,
|
|
162
|
+
className,
|
|
163
|
+
style: { position: "relative", height: 400, width: "100%", ...style },
|
|
164
|
+
children: /* @__PURE__ */ jsx2("div", { ref: containerRef, style: { position: "absolute", inset: 0 } })
|
|
165
|
+
}
|
|
166
|
+
);
|
|
167
|
+
});
|
|
168
|
+
Map.displayName = "Map";
|
|
169
|
+
|
|
170
|
+
// src/useMap.ts
|
|
171
|
+
import { useRef as useRef2 } from "react";
|
|
172
|
+
function useMap() {
|
|
173
|
+
return useRef2(null);
|
|
174
|
+
}
|
|
175
|
+
export {
|
|
176
|
+
Map,
|
|
177
|
+
MapMarker,
|
|
178
|
+
useMap
|
|
179
|
+
};
|
|
180
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/Map.tsx","../src/MapMarker.tsx","../src/useMap.ts"],"sourcesContent":["'use client';\n\nimport { forwardRef, useEffect, useImperativeHandle, useRef, type CSSProperties } from 'react';\nimport { createRoot, type Root } from 'react-dom/client';\n\nimport { MapMarker } from './MapMarker';\nimport type { LngLat, MapMarkerData } from './types';\n\n/**\n * Map — thin React wrapper around MapLibre GL JS. Loaded lazily so the\n * ~700 KB library only enters the bundle of consumers that actually use the\n * map.\n *\n * Tile URL defaults to OpenStreetMap raster so the component renders out of\n * the box. For production traffic, set `tileUrl` to a provider you have an\n * agreement with (MapTiler, Mapbox via maplibre style URL, Stadia, etc.).\n */\n\n/**\n * Aliased Map constructor — our exported `Map` component shadows the global\n * `Map` identifier in this module, so direct `new Map()` would try to\n * instantiate the React component. `NativeMap` keeps the type-safe path.\n */\nconst NativeMap: MapConstructor = globalThis.Map;\n\nexport interface MapHandle {\n /** Re-center the viewport. */\n flyTo(opts: { center: LngLat; zoom?: number }): void;\n /** Underlying MapLibre instance — escape hatch for unsupported features. */\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n raw: any | null;\n}\n\nexport interface MapProps {\n /** Initial center as `[lng, lat]`. */\n center: LngLat;\n /** Initial zoom (0–22). Default 12. */\n zoom?: number;\n /** Tile URL template. Default: OSM raster tiles. */\n tileUrl?: string;\n /** Marker data — re-rendered when the array identity changes. */\n markers?: ReadonlyArray<MapMarkerData>;\n /** Currently selected marker `id`. */\n selectedId?: string;\n /** Fires when a marker is clicked. */\n onSelect?: (marker: MapMarkerData) => void;\n /** Width / height override. */\n style?: CSSProperties;\n className?: string;\n /** Accessible label for the map region. */\n 'aria-label'?: string;\n}\n\nconst DEFAULT_TILE_URL = 'https://tile.openstreetmap.org/{z}/{x}/{y}.png';\n\nexport const Map = forwardRef<MapHandle, MapProps>(function Map(\n {\n center,\n zoom = 12,\n tileUrl = DEFAULT_TILE_URL,\n markers,\n selectedId,\n onSelect,\n style,\n className,\n 'aria-label': ariaLabel = 'Map',\n },\n ref,\n) {\n const containerRef = useRef<HTMLDivElement | null>(null);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const mapRef = useRef<any | null>(null);\n // eslint-disable-next-line @typescript-eslint/no-explicit-any\n const markerRefs = useRef(new NativeMap<string, { marker: any; root: Root }>());\n\n useImperativeHandle(\n ref,\n () => ({\n flyTo({ center: c, zoom: z }) {\n mapRef.current?.flyTo({ center: c, zoom: z });\n },\n get raw() {\n return mapRef.current;\n },\n }),\n [],\n );\n\n // Initialize MapLibre lazily so SSR / non-map consumers don't import it.\n useEffect(() => {\n if (!containerRef.current) return;\n let cancelled = false;\n\n (async () => {\n const maplibre = await import('maplibre-gl').catch(() => null);\n if (!maplibre || cancelled || !containerRef.current) return;\n\n const map = new maplibre.Map({\n container: containerRef.current,\n style: {\n version: 8,\n sources: {\n osm: {\n type: 'raster',\n tiles: [tileUrl],\n tileSize: 256,\n attribution: '© OpenStreetMap contributors',\n },\n },\n layers: [{ id: 'osm', type: 'raster', source: 'osm' }],\n },\n center: [...center],\n zoom,\n });\n mapRef.current = map;\n })();\n\n const markersAtMount = markerRefs.current;\n return () => {\n cancelled = true;\n // Tear down markers first so React roots unmount cleanly.\n markersAtMount.forEach(({ marker, root }) => {\n try {\n root.unmount();\n } catch {\n // ignore unmount race\n }\n marker.remove?.();\n });\n markersAtMount.clear();\n mapRef.current?.remove?.();\n mapRef.current = null;\n };\n // tileUrl is read at init time; consumers should set it once.\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, []);\n\n // Render / update markers.\n useEffect(() => {\n const map = mapRef.current;\n if (!map || !markers) return;\n let cancelled = false;\n\n (async () => {\n const maplibre = await import('maplibre-gl').catch(() => null);\n if (!maplibre || cancelled) return;\n\n // Diff: remove markers no longer present.\n const next = new Set(markers.map((m) => m.id));\n markerRefs.current.forEach((entry, id) => {\n if (!next.has(id)) {\n try {\n entry.root.unmount();\n } catch {\n // ignore\n }\n entry.marker.remove?.();\n markerRefs.current.delete(id);\n }\n });\n\n // Upsert each.\n for (const data of markers) {\n const existing = markerRefs.current.get(data.id);\n const el = existing?.marker.getElement() ?? document.createElement('div');\n el.style.cursor = 'pointer';\n\n const root = existing?.root ?? createRoot(el);\n root.render(\n <MapMarker\n label={data.label}\n icon={data.icon}\n variant={data.variant}\n selected={data.id === selectedId}\n onClick={() => onSelect?.(data)}\n />,\n );\n\n if (existing) {\n existing.marker.setLngLat([...data.location]);\n } else {\n const marker = new maplibre.Marker({ element: el, anchor: 'bottom' })\n .setLngLat([...data.location])\n .addTo(map);\n markerRefs.current.set(data.id, { marker, root });\n }\n }\n })();\n\n return () => {\n cancelled = true;\n };\n }, [markers, selectedId, onSelect]);\n\n return (\n <div\n role=\"region\"\n aria-label={ariaLabel}\n className={className}\n style={{ position: 'relative', height: 400, width: '100%', ...style }}\n >\n <div ref={containerRef} style={{ position: 'absolute', inset: 0 }} />\n </div>\n );\n});\n\nMap.displayName = 'Map';\n","'use client';\n\nimport { DynamicIconGlyph } from '@ship-it-ui/icons';\nimport { forwardRef, type ReactNode } from 'react';\n\n/**\n * MapMarker — DOM marker styled with design-system tokens. Rendered by the\n * `<Map>` component for each entry in its `markers` prop; you usually don't\n * instantiate it directly. Exported so consumers can render their own\n * markers via the `useMap()` API and `marker.setDOMContent()`.\n */\n\nexport interface MapMarkerProps {\n label?: ReactNode;\n icon?: string;\n variant?: 'default' | 'accent' | 'sale';\n selected?: boolean;\n onClick?: () => void;\n}\n\nconst variantClasses = {\n default: 'bg-panel text-text border-border',\n accent: 'bg-accent text-on-accent border-accent',\n sale: 'bg-sale text-on-accent border-sale',\n} as const;\n\nexport const MapMarker = forwardRef<HTMLButtonElement, MapMarkerProps>(function MapMarker(\n { label, icon, variant = 'default', selected, onClick },\n ref,\n) {\n return (\n <button\n ref={ref}\n type=\"button\"\n onClick={onClick}\n aria-pressed={selected}\n className={[\n 'inline-flex items-center gap-1 rounded-full border px-2.5 py-1 text-[12px] font-semibold shadow-md',\n 'transition-transform duration-(--duration-micro)',\n 'cursor-pointer hover:scale-105',\n selected ? 'ring-accent-glow scale-110 ring-2' : '',\n variantClasses[variant],\n ].join(' ')}\n >\n {icon && <DynamicIconGlyph name={icon} size={14} />}\n {label && <span>{label}</span>}\n </button>\n );\n});\nMapMarker.displayName = 'MapMarker';\n","'use client';\n\nimport { useRef } from 'react';\n\nimport type { MapHandle } from './Map';\n\n/**\n * Convenience hook that creates a stable `ref` for the imperative `MapHandle`.\n * Use when you need to programmatically `flyTo` or reach the raw MapLibre\n * instance.\n *\n * ```tsx\n * const mapRef = useMap();\n * <Map ref={mapRef} center={[-122.4, 37.8]} />\n * mapRef.current?.flyTo({ center: [-118.2, 34.0], zoom: 10 });\n * ```\n */\nexport function useMap() {\n return useRef<MapHandle | null>(null);\n}\n"],"mappings":";AAEA,SAAS,cAAAA,aAAY,WAAW,qBAAqB,cAAkC;AACvF,SAAS,kBAA6B;;;ACDtC,SAAS,wBAAwB;AACjC,SAAS,kBAAkC;AA4BvC,SAaW,KAbX;AAXJ,IAAM,iBAAiB;AAAA,EACrB,SAAS;AAAA,EACT,QAAQ;AAAA,EACR,MAAM;AACR;AAEO,IAAM,YAAY,WAA8C,SAASC,WAC9E,EAAE,OAAO,MAAM,UAAU,WAAW,UAAU,QAAQ,GACtD,KACA;AACA,SACE;AAAA,IAAC;AAAA;AAAA,MACC;AAAA,MACA,MAAK;AAAA,MACL;AAAA,MACA,gBAAc;AAAA,MACd,WAAW;AAAA,QACT;AAAA,QACA;AAAA,QACA;AAAA,QACA,WAAW,sCAAsC;AAAA,QACjD,eAAe,OAAO;AAAA,MACxB,EAAE,KAAK,GAAG;AAAA,MAET;AAAA,gBAAQ,oBAAC,oBAAiB,MAAM,MAAM,MAAM,IAAI;AAAA,QAChD,SAAS,oBAAC,UAAM,iBAAM;AAAA;AAAA;AAAA,EACzB;AAEJ,CAAC;AACD,UAAU,cAAc;;;ADwHd,gBAAAC,YAAA;AAlJV,IAAM,YAA4B,WAAW;AA8B7C,IAAM,mBAAmB;AAElB,IAAM,MAAMC,YAAgC,SAASC,KAC1D;AAAA,EACE;AAAA,EACA,OAAO;AAAA,EACP,UAAU;AAAA,EACV;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,cAAc,YAAY;AAC5B,GACA,KACA;AACA,QAAM,eAAe,OAA8B,IAAI;AAEvD,QAAM,SAAS,OAAmB,IAAI;AAEtC,QAAM,aAAa,OAAO,IAAI,UAA+C,CAAC;AAE9E;AAAA,IACE;AAAA,IACA,OAAO;AAAA,MACL,MAAM,EAAE,QAAQ,GAAG,MAAM,EAAE,GAAG;AAC5B,eAAO,SAAS,MAAM,EAAE,QAAQ,GAAG,MAAM,EAAE,CAAC;AAAA,MAC9C;AAAA,MACA,IAAI,MAAM;AACR,eAAO,OAAO;AAAA,MAChB;AAAA,IACF;AAAA,IACA,CAAC;AAAA,EACH;AAGA,YAAU,MAAM;AACd,QAAI,CAAC,aAAa,QAAS;AAC3B,QAAI,YAAY;AAEhB,KAAC,YAAY;AACX,YAAM,WAAW,MAAM,OAAO,aAAa,EAAE,MAAM,MAAM,IAAI;AAC7D,UAAI,CAAC,YAAY,aAAa,CAAC,aAAa,QAAS;AAErD,YAAM,MAAM,IAAI,SAAS,IAAI;AAAA,QAC3B,WAAW,aAAa;AAAA,QACxB,OAAO;AAAA,UACL,SAAS;AAAA,UACT,SAAS;AAAA,YACP,KAAK;AAAA,cACH,MAAM;AAAA,cACN,OAAO,CAAC,OAAO;AAAA,cACf,UAAU;AAAA,cACV,aAAa;AAAA,YACf;AAAA,UACF;AAAA,UACA,QAAQ,CAAC,EAAE,IAAI,OAAO,MAAM,UAAU,QAAQ,MAAM,CAAC;AAAA,QACvD;AAAA,QACA,QAAQ,CAAC,GAAG,MAAM;AAAA,QAClB;AAAA,MACF,CAAC;AACD,aAAO,UAAU;AAAA,IACnB,GAAG;AAEH,UAAM,iBAAiB,WAAW;AAClC,WAAO,MAAM;AACX,kBAAY;AAEZ,qBAAe,QAAQ,CAAC,EAAE,QAAQ,KAAK,MAAM;AAC3C,YAAI;AACF,eAAK,QAAQ;AAAA,QACf,QAAQ;AAAA,QAER;AACA,eAAO,SAAS;AAAA,MAClB,CAAC;AACD,qBAAe,MAAM;AACrB,aAAO,SAAS,SAAS;AACzB,aAAO,UAAU;AAAA,IACnB;AAAA,EAGF,GAAG,CAAC,CAAC;AAGL,YAAU,MAAM;AACd,UAAM,MAAM,OAAO;AACnB,QAAI,CAAC,OAAO,CAAC,QAAS;AACtB,QAAI,YAAY;AAEhB,KAAC,YAAY;AACX,YAAM,WAAW,MAAM,OAAO,aAAa,EAAE,MAAM,MAAM,IAAI;AAC7D,UAAI,CAAC,YAAY,UAAW;AAG5B,YAAM,OAAO,IAAI,IAAI,QAAQ,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC;AAC7C,iBAAW,QAAQ,QAAQ,CAAC,OAAO,OAAO;AACxC,YAAI,CAAC,KAAK,IAAI,EAAE,GAAG;AACjB,cAAI;AACF,kBAAM,KAAK,QAAQ;AAAA,UACrB,QAAQ;AAAA,UAER;AACA,gBAAM,OAAO,SAAS;AACtB,qBAAW,QAAQ,OAAO,EAAE;AAAA,QAC9B;AAAA,MACF,CAAC;AAGD,iBAAW,QAAQ,SAAS;AAC1B,cAAM,WAAW,WAAW,QAAQ,IAAI,KAAK,EAAE;AAC/C,cAAM,KAAK,UAAU,OAAO,WAAW,KAAK,SAAS,cAAc,KAAK;AACxE,WAAG,MAAM,SAAS;AAElB,cAAM,OAAO,UAAU,QAAQ,WAAW,EAAE;AAC5C,aAAK;AAAA,UACH,gBAAAF;AAAA,YAAC;AAAA;AAAA,cACC,OAAO,KAAK;AAAA,cACZ,MAAM,KAAK;AAAA,cACX,SAAS,KAAK;AAAA,cACd,UAAU,KAAK,OAAO;AAAA,cACtB,SAAS,MAAM,WAAW,IAAI;AAAA;AAAA,UAChC;AAAA,QACF;AAEA,YAAI,UAAU;AACZ,mBAAS,OAAO,UAAU,CAAC,GAAG,KAAK,QAAQ,CAAC;AAAA,QAC9C,OAAO;AACL,gBAAM,SAAS,IAAI,SAAS,OAAO,EAAE,SAAS,IAAI,QAAQ,SAAS,CAAC,EACjE,UAAU,CAAC,GAAG,KAAK,QAAQ,CAAC,EAC5B,MAAM,GAAG;AACZ,qBAAW,QAAQ,IAAI,KAAK,IAAI,EAAE,QAAQ,KAAK,CAAC;AAAA,QAClD;AAAA,MACF;AAAA,IACF,GAAG;AAEH,WAAO,MAAM;AACX,kBAAY;AAAA,IACd;AAAA,EACF,GAAG,CAAC,SAAS,YAAY,QAAQ,CAAC;AAElC,SACE,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ;AAAA,MACA,OAAO,EAAE,UAAU,YAAY,QAAQ,KAAK,OAAO,QAAQ,GAAG,MAAM;AAAA,MAEpE,0BAAAA,KAAC,SAAI,KAAK,cAAc,OAAO,EAAE,UAAU,YAAY,OAAO,EAAE,GAAG;AAAA;AAAA,EACrE;AAEJ,CAAC;AAED,IAAI,cAAc;;;AE5MlB,SAAS,UAAAG,eAAc;AAehB,SAAS,SAAS;AACvB,SAAOA,QAAyB,IAAI;AACtC;","names":["forwardRef","MapMarker","jsx","forwardRef","Map","useRef"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ship-it-ui/map",
|
|
3
|
+
"version": "0.0.2",
|
|
4
|
+
"description": "Map primitive for the Ship-It design system. Thin React wrapper over MapLibre GL JS with styled markers that consume design-system tokens.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://ship-it-ops.github.io/ship-it-design/",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/ship-it-ops/ship-it-design/issues"
|
|
9
|
+
},
|
|
10
|
+
"author": "Ship-It Ops",
|
|
11
|
+
"keywords": [
|
|
12
|
+
"design-system",
|
|
13
|
+
"map",
|
|
14
|
+
"maplibre",
|
|
15
|
+
"shipit"
|
|
16
|
+
],
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/ship-it-ops/ship-it-design.git",
|
|
20
|
+
"directory": "packages/map"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"module": "./dist/index.js",
|
|
25
|
+
"types": "./dist/index.d.ts",
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"require": "./dist/index.cjs"
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"README.md"
|
|
36
|
+
],
|
|
37
|
+
"publishConfig": {
|
|
38
|
+
"access": "public",
|
|
39
|
+
"provenance": true
|
|
40
|
+
},
|
|
41
|
+
"peerDependencies": {
|
|
42
|
+
"maplibre-gl": "^4.0.0 || ^5.0.0",
|
|
43
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
44
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
45
|
+
"@ship-it-ui/icons": "0.0.8",
|
|
46
|
+
"@ship-it-ui/ui": "0.0.8"
|
|
47
|
+
},
|
|
48
|
+
"devDependencies": {
|
|
49
|
+
"@testing-library/jest-dom": "^6.6.3",
|
|
50
|
+
"@testing-library/react": "^16.0.1",
|
|
51
|
+
"@types/react": "^18.3.12",
|
|
52
|
+
"@types/react-dom": "^18.3.1",
|
|
53
|
+
"axe-core": "^4.10.2",
|
|
54
|
+
"esbuild-plugin-preserve-directives": "^0.0.11",
|
|
55
|
+
"jsdom": "^29.1.1",
|
|
56
|
+
"maplibre-gl": "^4.7.1",
|
|
57
|
+
"react": "^18.3.1",
|
|
58
|
+
"react-dom": "^18.3.1",
|
|
59
|
+
"tsup": "^8.3.0",
|
|
60
|
+
"typescript": "^5.6.3",
|
|
61
|
+
"vitest": "^2.1.3",
|
|
62
|
+
"vitest-axe": "^0.1.0",
|
|
63
|
+
"@ship-it-ui/eslint-config": "0.0.1",
|
|
64
|
+
"@ship-it-ui/icons": "0.0.8",
|
|
65
|
+
"@ship-it-ui/tokens": "0.0.6",
|
|
66
|
+
"@ship-it-ui/tsconfig": "0.0.1",
|
|
67
|
+
"@ship-it-ui/ui": "0.0.8"
|
|
68
|
+
},
|
|
69
|
+
"scripts": {
|
|
70
|
+
"build": "tsup",
|
|
71
|
+
"dev": "tsup --watch",
|
|
72
|
+
"lint": "eslint src",
|
|
73
|
+
"lint:fix": "eslint src --fix",
|
|
74
|
+
"test": "vitest run --passWithNoTests",
|
|
75
|
+
"test:watch": "vitest",
|
|
76
|
+
"typecheck": "tsc --noEmit",
|
|
77
|
+
"clean": "rm -rf dist .turbo"
|
|
78
|
+
}
|
|
79
|
+
}
|