mapped-image 0.2.0 → 0.3.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/README.md +6 -1
- package/dist/MappedImage.d.ts +15 -0
- package/dist/MappedImage.js +138 -21
- package/dist/MappedImage.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -46,6 +46,10 @@ Clicking a cell toggles its selection (filled red) and reports the cell's row nu
|
|
|
46
46
|
| `onSelectedImageIndexChange` | `(index: number) => void` | Fires whenever the active image changes. |
|
|
47
47
|
| `renderImageSelector` | `(props: ImageSelectorProps) => ReactNode` | Optional override for the image-switcher UI rendered over the map. Defaults to a row of buttons; pass your own to fully customize it, or build your own selector entirely outside this component using the controlled `selectedImageIndex`/`onSelectedImageIndexChange` props instead. |
|
|
48
48
|
| `maxBoundsPadding` | `number` | Extra pannable margin, in pixels, beyond the image edges. Defaults to `50`. |
|
|
49
|
+
| `zoomMultiplier` | `number` | Scales the default fit-to-container zoom. `<1` zooms out, `>1` zooms in. Defaults to `1`. |
|
|
50
|
+
| `weightsEditable` | `boolean` | When `true`, draggable handles appear on every internal row/column boundary, letting the user resize relative weights with the mouse. Defaults to `false`. |
|
|
51
|
+
| `onColumnWeightsChange` | `(weights: number[]) => void` | Fires once a column-weight drag is released, with the full resolved weight array for the currently displayed image. |
|
|
52
|
+
| `onRowWeightsChange` | `(weights: number[]) => void` | Same as `onColumnWeightsChange`, for row-weight drags. |
|
|
49
53
|
|
|
50
54
|
### `ImageConfig`
|
|
51
55
|
|
|
@@ -59,10 +63,11 @@ Clicking a cell toggles its selection (filled red) and reports the cell's row nu
|
|
|
59
63
|
| `rowWeights` | `Record<number, number>` | Same as `columnWeights`, but for row heights. |
|
|
60
64
|
|
|
61
65
|
> Both `rows`/`columns` must be greater than `0`.
|
|
66
|
+
> `columnWeights`/`rowWeights` indices follow the array order used internally (column index `0` = leftmost; row index `0` = the row nearest the *bottom* of the image, not the top, since that's the underlying coordinate system's origin) — keep this in mind if you see row weights affecting the opposite row from what you expected.
|
|
62
67
|
|
|
63
68
|
## Building an editor
|
|
64
69
|
|
|
65
|
-
This package intentionally exposes `selectedImageIndex`/`onSelectedImageIndexChange` and a controllable `selectedCells`/`onSelectedCellsChange` so a separate editor app (outside this package) can drive the grid entirely from its own state — e.g. to add/remove rows and columns,
|
|
70
|
+
This package intentionally exposes `selectedImageIndex`/`onSelectedImageIndexChange` and a controllable `selectedCells`/`onSelectedCellsChange` so a separate editor app (outside this package) can drive the grid entirely from its own state — e.g. to add/remove rows and columns, persist the resulting `ImageConfig[]` plus selection as a JSON config to reload later, and build its own row/column-count controls. For weight editing specifically, set `weightsEditable` and read the result from `onColumnWeightsChange`/`onRowWeightsChange` to update your own copy of `ImageConfig.columnWeights`/`rowWeights` (drags are previewed locally for responsiveness and only reported once released). There's no other editor UI in this package itself.
|
|
66
71
|
|
|
67
72
|
## Development
|
|
68
73
|
|
package/dist/MappedImage.d.ts
CHANGED
|
@@ -30,6 +30,14 @@ type MappedImageProps = {
|
|
|
30
30
|
renderImageSelector?: (props: ImageSelectorProps) => ReactNode;
|
|
31
31
|
/** Extra pannable margin (in pixels) beyond the image edges. Defaults to 50. */
|
|
32
32
|
maxBoundsPadding?: number;
|
|
33
|
+
/** Scales the default fit-to-container zoom. <1 zooms out, >1 zooms in. Defaults to 1. */
|
|
34
|
+
zoomMultiplier?: number;
|
|
35
|
+
/** When true, draggable handles appear between rows/columns to resize their relative weights via mouse. Defaults to false. */
|
|
36
|
+
weightsEditable?: boolean;
|
|
37
|
+
/** Fires once a column-weight drag is released, with the full resolved weight array for the current image. */
|
|
38
|
+
onColumnWeightsChange?: (weights: number[]) => void;
|
|
39
|
+
/** Fires once a row-weight drag is released, with the full resolved weight array for the current image. */
|
|
40
|
+
onRowWeightsChange?: (weights: number[]) => void;
|
|
33
41
|
};
|
|
34
42
|
type ImageLayerProps = {
|
|
35
43
|
image: ImageConfig;
|
|
@@ -40,6 +48,10 @@ type ImageLayerProps = {
|
|
|
40
48
|
alt: string;
|
|
41
49
|
selectedCells: Set<string>;
|
|
42
50
|
maxBoundsPadding?: number;
|
|
51
|
+
zoomMultiplier?: number;
|
|
52
|
+
weightsEditable?: boolean;
|
|
53
|
+
onColumnWeightsChange?: (weights: number[]) => void;
|
|
54
|
+
onRowWeightsChange?: (weights: number[]) => void;
|
|
43
55
|
onCellClick: GridProps["onCellClick"];
|
|
44
56
|
};
|
|
45
57
|
type GridProps = {
|
|
@@ -50,6 +62,9 @@ type GridProps = {
|
|
|
50
62
|
columnWeights: number[];
|
|
51
63
|
imgBounds: LatLngTuple;
|
|
52
64
|
selectedCells: Set<string>;
|
|
65
|
+
weightsEditable?: boolean;
|
|
66
|
+
onColumnWeightsChange?: (weights: number[]) => void;
|
|
67
|
+
onRowWeightsChange?: (weights: number[]) => void;
|
|
53
68
|
onCellClick?: (props: ICellClickProps) => void;
|
|
54
69
|
};
|
|
55
70
|
interface ICellClickProps {
|
package/dist/MappedImage.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// MappedImage.tsx
|
|
2
|
-
import { useState as
|
|
2
|
+
import { useState as useState3 } from "react";
|
|
3
3
|
import "leaflet/dist/leaflet.css";
|
|
4
4
|
|
|
5
5
|
// #style-inject:#style-inject
|
|
@@ -25,7 +25,7 @@ function styleInject(css, { insertAt } = {}) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
// MappedImage.css
|
|
28
|
-
styleInject(".grid-header-label-icon {\n background: none;\n border: none;\n}\n.grid-header-label {\n position: absolute;\n display: block;\n font-weight: bold;\n white-space: nowrap;\n}\n.grid-header-label--column {\n transform: translate(-50%, -100%);\n}\n.grid-header-label--row {\n transform: translate(-100%, -50%);\n}\n");
|
|
28
|
+
styleInject(".grid-header-label-icon {\n background: none;\n border: none;\n}\n.grid-header-label {\n position: absolute;\n display: block;\n font-weight: bold;\n white-space: nowrap;\n}\n.grid-header-label--column {\n transform: translate(-50%, -100%);\n}\n.grid-header-label--row {\n transform: translate(-100%, -50%);\n}\n.grid-weight-handle--column {\n cursor: col-resize;\n}\n.grid-weight-handle--row {\n cursor: row-resize;\n}\n.grid-weight-handle:hover {\n stroke-opacity: 0.6;\n}\n");
|
|
29
29
|
|
|
30
30
|
// components/DefaultImageSelector.tsx
|
|
31
31
|
import { jsx } from "react/jsx-runtime";
|
|
@@ -62,11 +62,12 @@ var DefaultImageSelector = ({ images, selectedIndex, onSelect }) => /* @__PURE__
|
|
|
62
62
|
);
|
|
63
63
|
|
|
64
64
|
// components/ImageLayer.tsx
|
|
65
|
-
import { useEffect, useState } from "react";
|
|
65
|
+
import { useEffect as useEffect2, useState as useState2 } from "react";
|
|
66
66
|
import { CRS } from "leaflet";
|
|
67
67
|
import { MapContainer, ImageOverlay } from "react-leaflet";
|
|
68
68
|
|
|
69
69
|
// components/Grid.tsx
|
|
70
|
+
import { useState } from "react";
|
|
70
71
|
import { Rectangle, Marker } from "react-leaflet";
|
|
71
72
|
|
|
72
73
|
// helpers/weights.ts
|
|
@@ -76,6 +77,16 @@ var weightOffsets = (weights) => {
|
|
|
76
77
|
weights.forEach((weight) => offsets.push(offsets[offsets.length - 1] + weight));
|
|
77
78
|
return offsets;
|
|
78
79
|
};
|
|
80
|
+
var MIN_WEIGHT = 0.2;
|
|
81
|
+
var applyWeightDelta = (weights, index, delta) => {
|
|
82
|
+
const a = weights[index];
|
|
83
|
+
const b = weights[index + 1];
|
|
84
|
+
const clampedDelta = Math.max(-(a - MIN_WEIGHT), Math.min(b - MIN_WEIGHT, delta));
|
|
85
|
+
const next = [...weights];
|
|
86
|
+
next[index] = a + clampedDelta;
|
|
87
|
+
next[index + 1] = b - clampedDelta;
|
|
88
|
+
return next;
|
|
89
|
+
};
|
|
79
90
|
|
|
80
91
|
// helpers/headerIcon.ts
|
|
81
92
|
import { divIcon } from "leaflet";
|
|
@@ -104,8 +115,53 @@ var bindCellAccessibility = (instance, label, selected, onActivate) => {
|
|
|
104
115
|
};
|
|
105
116
|
};
|
|
106
117
|
|
|
107
|
-
// components/
|
|
118
|
+
// components/WeightHandle.tsx
|
|
119
|
+
import { useRef } from "react";
|
|
120
|
+
import { Polyline, useMap } from "react-leaflet";
|
|
108
121
|
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
122
|
+
var WeightHandle = ({ axis, boundary, length, onDrag, onDragEnd }) => {
|
|
123
|
+
const map = useMap();
|
|
124
|
+
const dragStartRef = useRef(null);
|
|
125
|
+
const positions = axis === "column" ? [[0, boundary], [length, boundary]] : [[boundary, 0], [boundary, length]];
|
|
126
|
+
const handleMouseDown = (event) => {
|
|
127
|
+
event.originalEvent.preventDefault();
|
|
128
|
+
map.dragging.disable();
|
|
129
|
+
dragStartRef.current = axis === "column" ? event.latlng.lng : event.latlng.lat;
|
|
130
|
+
const handleMouseMove = (moveEvent) => {
|
|
131
|
+
if (dragStartRef.current === null) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const latlng = map.mouseEventToLatLng(moveEvent);
|
|
135
|
+
const current = axis === "column" ? latlng.lng : latlng.lat;
|
|
136
|
+
onDrag(current - dragStartRef.current);
|
|
137
|
+
};
|
|
138
|
+
const handleMouseUp = () => {
|
|
139
|
+
dragStartRef.current = null;
|
|
140
|
+
map.dragging.enable();
|
|
141
|
+
onDragEnd();
|
|
142
|
+
window.removeEventListener("mousemove", handleMouseMove);
|
|
143
|
+
window.removeEventListener("mouseup", handleMouseUp);
|
|
144
|
+
};
|
|
145
|
+
window.addEventListener("mousemove", handleMouseMove);
|
|
146
|
+
window.addEventListener("mouseup", handleMouseUp);
|
|
147
|
+
};
|
|
148
|
+
return /* @__PURE__ */ jsx2(
|
|
149
|
+
Polyline,
|
|
150
|
+
{
|
|
151
|
+
positions,
|
|
152
|
+
pathOptions: {
|
|
153
|
+
color: "#3388ff",
|
|
154
|
+
weight: 6,
|
|
155
|
+
opacity: 0.15,
|
|
156
|
+
className: `grid-weight-handle grid-weight-handle--${axis}`
|
|
157
|
+
},
|
|
158
|
+
eventHandlers: { mousedown: handleMouseDown }
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
// components/Grid.tsx
|
|
164
|
+
import { jsx as jsx3 } from "react/jsx-runtime";
|
|
109
165
|
var Grid = ({
|
|
110
166
|
rows,
|
|
111
167
|
rowOffset,
|
|
@@ -114,14 +170,20 @@ var Grid = ({
|
|
|
114
170
|
columnWeights,
|
|
115
171
|
imgBounds,
|
|
116
172
|
selectedCells,
|
|
173
|
+
weightsEditable,
|
|
174
|
+
onColumnWeightsChange,
|
|
175
|
+
onRowWeightsChange,
|
|
117
176
|
onCellClick
|
|
118
177
|
}) => {
|
|
178
|
+
const [dragState, setDragState] = useState(null);
|
|
119
179
|
const [height, width] = imgBounds;
|
|
120
|
-
const
|
|
180
|
+
const displayColumnWeights = dragState?.axis === "column" ? applyWeightDelta(columnWeights, dragState.index, dragState.deltaWeight) : columnWeights;
|
|
181
|
+
const displayRowWeights = dragState?.axis === "row" ? applyWeightDelta(rowWeights, dragState.index, dragState.deltaWeight) : rowWeights;
|
|
182
|
+
const colOffsets = weightOffsets(displayColumnWeights);
|
|
121
183
|
const pxPerColWeightUnit = width / colOffsets[colOffsets.length - 1];
|
|
122
184
|
const colLeft = (colCount) => colOffsets[colCount] * pxPerColWeightUnit;
|
|
123
185
|
const colRight = (colCount) => colOffsets[colCount + 1] * pxPerColWeightUnit;
|
|
124
|
-
const rowOffsets = weightOffsets(
|
|
186
|
+
const rowOffsets = weightOffsets(displayRowWeights);
|
|
125
187
|
const pxPerRowWeightUnit = height / rowOffsets[rowOffsets.length - 1];
|
|
126
188
|
const rowBottom = (rowCount) => rowOffsets[rowCount] * pxPerRowWeightUnit;
|
|
127
189
|
const rowTop = (rowCount) => rowOffsets[rowCount + 1] * pxPerRowWeightUnit;
|
|
@@ -135,7 +197,7 @@ var Grid = ({
|
|
|
135
197
|
const cellId = `${rowLabel},${columnLabel}`;
|
|
136
198
|
const selected = selectedCells.has(cellId);
|
|
137
199
|
const activate = () => onCellClick?.({ col: columnLabel, row: rowLabel });
|
|
138
|
-
return /* @__PURE__ */
|
|
200
|
+
return /* @__PURE__ */ jsx3(
|
|
139
201
|
Rectangle,
|
|
140
202
|
{
|
|
141
203
|
ref: (instance) => bindCellAccessibility(instance, `${columnLabel}${rowLabel}`, selected, activate),
|
|
@@ -153,7 +215,7 @@ var Grid = ({
|
|
|
153
215
|
);
|
|
154
216
|
})
|
|
155
217
|
);
|
|
156
|
-
const columnHeaders = columnLabels.map((columnLabel, colCount) => /* @__PURE__ */
|
|
218
|
+
const columnHeaders = columnLabels.map((columnLabel, colCount) => /* @__PURE__ */ jsx3(
|
|
157
219
|
Marker,
|
|
158
220
|
{
|
|
159
221
|
position: [height, (colLeft(colCount) + colRight(colCount)) / 2],
|
|
@@ -164,7 +226,7 @@ var Grid = ({
|
|
|
164
226
|
));
|
|
165
227
|
const rowHeaders = Array.from({ length: rows }).map((_, rowCount) => {
|
|
166
228
|
const rowLabel = rowOffset + (rows - rowCount);
|
|
167
|
-
return /* @__PURE__ */
|
|
229
|
+
return /* @__PURE__ */ jsx3(
|
|
168
230
|
Marker,
|
|
169
231
|
{
|
|
170
232
|
position: [(rowBottom(rowCount) + rowTop(rowCount)) / 2, -7],
|
|
@@ -174,7 +236,50 @@ var Grid = ({
|
|
|
174
236
|
`row-${rowLabel}`
|
|
175
237
|
);
|
|
176
238
|
});
|
|
177
|
-
|
|
239
|
+
const columnHandles = weightsEditable ? columnLabels.slice(0, -1).map((_, index) => /* @__PURE__ */ jsx3(
|
|
240
|
+
WeightHandle,
|
|
241
|
+
{
|
|
242
|
+
axis: "column",
|
|
243
|
+
boundary: colRight(index),
|
|
244
|
+
length: height,
|
|
245
|
+
onDrag: (deltaUnits) => setDragState({ axis: "column", index, deltaWeight: deltaUnits / pxPerColWeightUnit }),
|
|
246
|
+
onDragEnd: () => {
|
|
247
|
+
onColumnWeightsChange?.(displayColumnWeights);
|
|
248
|
+
setDragState(null);
|
|
249
|
+
}
|
|
250
|
+
},
|
|
251
|
+
`col-handle-${index}`
|
|
252
|
+
)) : [];
|
|
253
|
+
const rowHandles = weightsEditable ? Array.from({ length: rows - 1 }).map((_, index) => /* @__PURE__ */ jsx3(
|
|
254
|
+
WeightHandle,
|
|
255
|
+
{
|
|
256
|
+
axis: "row",
|
|
257
|
+
boundary: rowTop(index),
|
|
258
|
+
length: width,
|
|
259
|
+
onDrag: (deltaUnits) => setDragState({ axis: "row", index, deltaWeight: deltaUnits / pxPerRowWeightUnit }),
|
|
260
|
+
onDragEnd: () => {
|
|
261
|
+
onRowWeightsChange?.(displayRowWeights);
|
|
262
|
+
setDragState(null);
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
`row-handle-${index}`
|
|
266
|
+
)) : [];
|
|
267
|
+
return [...cells, columnHeaders, rowHeaders, columnHandles, rowHandles];
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
// components/ZoomController.tsx
|
|
271
|
+
import { useEffect, useRef as useRef2 } from "react";
|
|
272
|
+
import { useMap as useMap2 } from "react-leaflet";
|
|
273
|
+
var ZoomController = ({ zoomMultiplier }) => {
|
|
274
|
+
const map = useMap2();
|
|
275
|
+
const baseZoomRef = useRef2(null);
|
|
276
|
+
useEffect(() => {
|
|
277
|
+
if (baseZoomRef.current === null) {
|
|
278
|
+
baseZoomRef.current = map.getZoom();
|
|
279
|
+
}
|
|
280
|
+
map.setZoom(baseZoomRef.current + Math.log2(zoomMultiplier));
|
|
281
|
+
}, [map, zoomMultiplier]);
|
|
282
|
+
return null;
|
|
178
283
|
};
|
|
179
284
|
|
|
180
285
|
// helpers/imageBounds.ts
|
|
@@ -202,7 +307,7 @@ var toColumnLetter = (n) => {
|
|
|
202
307
|
var colNames = (startIndex, count) => Array.from({ length: count }, (_, i) => toColumnLetter(startIndex + i + 1));
|
|
203
308
|
|
|
204
309
|
// components/ImageLayer.tsx
|
|
205
|
-
import { jsx as
|
|
310
|
+
import { jsx as jsx4, jsxs } from "react/jsx-runtime";
|
|
206
311
|
var ImageLayer = ({
|
|
207
312
|
image,
|
|
208
313
|
colOffset,
|
|
@@ -212,11 +317,15 @@ var ImageLayer = ({
|
|
|
212
317
|
alt,
|
|
213
318
|
selectedCells,
|
|
214
319
|
maxBoundsPadding = 100,
|
|
320
|
+
zoomMultiplier = 1,
|
|
321
|
+
weightsEditable,
|
|
322
|
+
onColumnWeightsChange,
|
|
323
|
+
onRowWeightsChange,
|
|
215
324
|
onCellClick
|
|
216
325
|
}) => {
|
|
217
|
-
const [imgBounds, setImgBounds] =
|
|
218
|
-
const [loadError, setLoadError] =
|
|
219
|
-
|
|
326
|
+
const [imgBounds, setImgBounds] = useState2(null);
|
|
327
|
+
const [loadError, setLoadError] = useState2(null);
|
|
328
|
+
useEffect2(() => {
|
|
220
329
|
imageBounds(image.src, width, height).then(setImgBounds).catch(setLoadError);
|
|
221
330
|
}, [image.src, width, height]);
|
|
222
331
|
if (loadError) {
|
|
@@ -245,7 +354,8 @@ var ImageLayer = ({
|
|
|
245
354
|
style: { width, height },
|
|
246
355
|
maxZoom: 2,
|
|
247
356
|
children: [
|
|
248
|
-
/* @__PURE__ */
|
|
357
|
+
/* @__PURE__ */ jsx4(ZoomController, { zoomMultiplier }),
|
|
358
|
+
/* @__PURE__ */ jsx4(
|
|
249
359
|
Grid,
|
|
250
360
|
{
|
|
251
361
|
rows: image.rows,
|
|
@@ -255,17 +365,20 @@ var ImageLayer = ({
|
|
|
255
365
|
columnWeights,
|
|
256
366
|
imgBounds,
|
|
257
367
|
selectedCells,
|
|
368
|
+
weightsEditable,
|
|
369
|
+
onColumnWeightsChange,
|
|
370
|
+
onRowWeightsChange,
|
|
258
371
|
onCellClick
|
|
259
372
|
}
|
|
260
373
|
),
|
|
261
|
-
/* @__PURE__ */
|
|
374
|
+
/* @__PURE__ */ jsx4(ImageOverlay, { url: image.src, bounds, alt })
|
|
262
375
|
]
|
|
263
376
|
}
|
|
264
377
|
);
|
|
265
378
|
};
|
|
266
379
|
|
|
267
380
|
// MappedImage.tsx
|
|
268
|
-
import { jsx as
|
|
381
|
+
import { jsx as jsx5, jsxs as jsxs2 } from "react/jsx-runtime";
|
|
269
382
|
var MappedImage = (props) => {
|
|
270
383
|
if (props.images.length === 0) {
|
|
271
384
|
throw new Error("MappedImage requires at least one entry in `images`.");
|
|
@@ -276,12 +389,12 @@ var MappedImage = (props) => {
|
|
|
276
389
|
`MappedImage: image "${invalidImage.name}" must have rows > 0 and columns > 0.`
|
|
277
390
|
);
|
|
278
391
|
}
|
|
279
|
-
const [internalSelectedCells, setInternalSelectedCells] =
|
|
392
|
+
const [internalSelectedCells, setInternalSelectedCells] = useState3(
|
|
280
393
|
props.selectedCells ?? /* @__PURE__ */ new Set()
|
|
281
394
|
);
|
|
282
395
|
const isSelectionControlled = props.selectedCells !== void 0;
|
|
283
396
|
const selectedCells = isSelectionControlled ? props.selectedCells : internalSelectedCells;
|
|
284
|
-
const [selectedImageIndex, setSelectedImageIndex] =
|
|
397
|
+
const [selectedImageIndex, setSelectedImageIndex] = useState3(
|
|
285
398
|
props.selectedImageIndex ?? 0
|
|
286
399
|
);
|
|
287
400
|
const selectedImage = props.images[selectedImageIndex];
|
|
@@ -305,9 +418,9 @@ var MappedImage = (props) => {
|
|
|
305
418
|
props.onSelectedCellsChange?.(next);
|
|
306
419
|
props.onCellClick?.({ row, col });
|
|
307
420
|
};
|
|
308
|
-
const renderSelector = props.renderImageSelector ?? ((selectorProps) => /* @__PURE__ */
|
|
421
|
+
const renderSelector = props.renderImageSelector ?? ((selectorProps) => /* @__PURE__ */ jsx5(DefaultImageSelector, { ...selectorProps }));
|
|
309
422
|
return /* @__PURE__ */ jsxs2("div", { style: { position: "relative", width: props.width, height: props.height }, children: [
|
|
310
|
-
/* @__PURE__ */
|
|
423
|
+
/* @__PURE__ */ jsx5(
|
|
311
424
|
ImageLayer,
|
|
312
425
|
{
|
|
313
426
|
image: selectedImage,
|
|
@@ -318,6 +431,10 @@ var MappedImage = (props) => {
|
|
|
318
431
|
alt: props.alt,
|
|
319
432
|
selectedCells,
|
|
320
433
|
maxBoundsPadding: props.maxBoundsPadding,
|
|
434
|
+
zoomMultiplier: props.zoomMultiplier,
|
|
435
|
+
weightsEditable: props.weightsEditable,
|
|
436
|
+
onColumnWeightsChange: props.onColumnWeightsChange,
|
|
437
|
+
onRowWeightsChange: props.onRowWeightsChange,
|
|
321
438
|
onCellClick: handleCellClick
|
|
322
439
|
},
|
|
323
440
|
selectedImage.src
|
package/dist/MappedImage.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../MappedImage.tsx","#style-inject:#style-inject","../MappedImage.css","../components/DefaultImageSelector.tsx","../components/ImageLayer.tsx","../components/Grid.tsx","../helpers/weights.ts","../helpers/headerIcon.ts","../helpers/cellAccessibility.ts","../helpers/imageBounds.ts","../helpers/colNames.ts"],"sourcesContent":["import { useState } from \"react\";\nimport \"leaflet/dist/leaflet.css\";\nimport \"./MappedImage.css\";\nimport type { ICellClickProps, ImageSelectorProps, MappedImageProps } from \"./types\";\nimport { DefaultImageSelector } from \"./components/DefaultImageSelector\";\nimport { ImageLayer } from \"./components/ImageLayer\";\n\nexport * from \"./types\";\n\nexport const MappedImage = (props: MappedImageProps) => {\n if (props.images.length === 0) {\n throw new Error(\"MappedImage requires at least one entry in `images`.\");\n }\n\n const invalidImage = props.images.find((image) => image.rows <= 0 || image.columns <= 0);\n if (invalidImage) {\n throw new Error(\n `MappedImage: image \"${invalidImage.name}\" must have rows > 0 and columns > 0.`,\n );\n }\n\n const [internalSelectedCells, setInternalSelectedCells] = useState<Set<string>>(\n props.selectedCells ?? new Set(),\n );\n const isSelectionControlled = props.selectedCells !== undefined;\n const selectedCells = isSelectionControlled ? props.selectedCells! : internalSelectedCells;\n\n const [selectedImageIndex, setSelectedImageIndex] = useState(\n props.selectedImageIndex ?? 0,\n );\n\n const selectedImage = props.images[selectedImageIndex];\n const colOffset = props.images\n .slice(0, selectedImageIndex)\n .reduce((sum, image) => sum + image.columns, 0);\n const rowOffset = props.images\n .slice(0, selectedImageIndex)\n .reduce((sum, image) => sum + image.rows, 0);\n\n const handleSelectImage = (index: number) => {\n setSelectedImageIndex(index);\n props.onSelectedImageIndexChange?.(index);\n };\n\n const handleCellClick = ({ row, col }: ICellClickProps) => {\n const cellId = `${row},${col}`;\n const next = new Set(selectedCells);\n if (next.has(cellId)) {\n next.delete(cellId);\n } else {\n next.add(cellId);\n }\n\n if (!isSelectionControlled) {\n setInternalSelectedCells(next);\n }\n props.onSelectedCellsChange?.(next);\n props.onCellClick?.({ row, col });\n };\n\n const renderSelector = props.renderImageSelector ?? (\n (selectorProps: ImageSelectorProps) => <DefaultImageSelector {...selectorProps} />\n );\n\n return (\n <div style={{ position: \"relative\", width: props.width, height: props.height }}>\n <ImageLayer\n key={selectedImage.src}\n image={selectedImage}\n colOffset={colOffset}\n rowOffset={rowOffset}\n width={props.width}\n height={props.height}\n alt={props.alt}\n selectedCells={selectedCells}\n maxBoundsPadding={props.maxBoundsPadding}\n onCellClick={handleCellClick}\n />\n {renderSelector({\n images: props.images,\n selectedIndex: selectedImageIndex,\n onSelect: handleSelectImage,\n })}\n </div>\n );\n};\n","\n export default function styleInject(css, { insertAt } = {}) {\n if (!css || typeof document === 'undefined') return\n \n const head = document.head || document.getElementsByTagName('head')[0]\n const style = document.createElement('style')\n style.type = 'text/css'\n \n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild)\n } else {\n head.appendChild(style)\n }\n } else {\n head.appendChild(style)\n }\n \n if (style.styleSheet) {\n style.styleSheet.cssText = css\n } else {\n style.appendChild(document.createTextNode(css))\n }\n }\n ","import styleInject from '#style-inject';styleInject(\".grid-header-label-icon {\\n background: none;\\n border: none;\\n}\\n.grid-header-label {\\n position: absolute;\\n display: block;\\n font-weight: bold;\\n white-space: nowrap;\\n}\\n.grid-header-label--column {\\n transform: translate(-50%, -100%);\\n}\\n.grid-header-label--row {\\n transform: translate(-100%, -50%);\\n}\\n\")","import type { ImageSelectorProps } from \"../types\";\n\nexport const DefaultImageSelector = ({ images, selectedIndex, onSelect }: ImageSelectorProps) => (\n <div\n style={{\n position: \"absolute\",\n top: 10,\n right: 10,\n zIndex: 1000,\n display: \"flex\",\n gap: 8,\n }}\n >\n {images.map((image, index) => (\n <button\n key={image.name}\n type=\"button\"\n onClick={() => onSelect(index)}\n style={{\n padding: \"12px 20px\",\n fontSize: 16,\n fontWeight: index === selectedIndex ? \"bold\" : \"normal\",\n background: index === selectedIndex ? \"#3388ff\" : \"white\",\n color: index === selectedIndex ? \"white\" : \"black\",\n border: \"1px solid #3388ff\",\n borderRadius: 6,\n }}\n >\n {image.name}\n </button>\n ))}\n </div>\n);\n","import { useEffect, useState } from \"react\";\nimport { CRS, type LatLngBoundsExpression, type LatLngTuple } from \"leaflet\";\nimport { MapContainer, ImageOverlay } from \"react-leaflet\";\nimport type { ImageLayerProps } from \"../types\";\nimport { Grid } from \"./Grid\";\nimport { imageBounds } from \"../helpers/imageBounds\";\nimport { colNames } from \"../helpers/colNames\";\nimport { resolveWeights } from \"../helpers/weights\";\n\nexport const ImageLayer = ({\n image,\n colOffset,\n rowOffset,\n width,\n height,\n alt,\n selectedCells,\n maxBoundsPadding = 100,\n onCellClick,\n}: ImageLayerProps) => {\n const [imgBounds, setImgBounds] = useState<LatLngTuple | null>(null);\n const [loadError, setLoadError] = useState<Error | null>(null);\n\n useEffect(() => {\n imageBounds(image.src, width, height).then(setImgBounds).catch(setLoadError);\n }, [image.src, width, height]);\n\n if (loadError) {\n throw loadError;\n }\n\n if (!imgBounds) {\n return null;\n }\n\n const [imgHeight, imgWidth] = imgBounds;\n const bounds: LatLngBoundsExpression = [[0, 0], imgBounds];\n const maxBounds: LatLngBoundsExpression = [\n [-maxBoundsPadding , -maxBoundsPadding],\n [imgHeight + maxBoundsPadding, imgWidth + maxBoundsPadding],\n ];\n const columnLabels = colNames(colOffset, image.columns);\n const columnWeights = resolveWeights(image.columns, image.columnWeights);\n const rowWeights = resolveWeights(image.rows, image.rowWeights);\n\n return (\n <MapContainer\n crs={CRS.Simple}\n bounds={bounds}\n boundsOptions={{ padding: [50, 50] }}\n maxBounds={maxBounds}\n maxBoundsViscosity={0.5}\n style={{ width, height }}\n maxZoom={2}\n >\n <Grid\n rows={image.rows}\n rowOffset={rowOffset}\n rowWeights={rowWeights}\n columnLabels={columnLabels}\n columnWeights={columnWeights}\n imgBounds={imgBounds}\n selectedCells={selectedCells}\n onCellClick={onCellClick}\n />\n <ImageOverlay url={image.src} bounds={bounds} alt={alt} />\n </MapContainer>\n );\n};\n","import { Rectangle, Marker } from \"react-leaflet\";\nimport type { LatLngBoundsExpression } from \"leaflet\";\nimport type { GridProps } from \"../types\";\nimport { weightOffsets } from \"../helpers/weights\";\nimport { headerIcon } from \"../helpers/headerIcon\";\nimport { bindCellAccessibility } from \"../helpers/cellAccessibility\";\n\nexport const Grid = ({\n rows,\n rowOffset,\n rowWeights,\n columnLabels,\n columnWeights,\n imgBounds,\n selectedCells,\n onCellClick,\n}: GridProps) => {\n const [height, width] = imgBounds;\n\n const colOffsets = weightOffsets(columnWeights);\n const pxPerColWeightUnit = width / colOffsets[colOffsets.length - 1];\n const colLeft = (colCount: number) => colOffsets[colCount] * pxPerColWeightUnit;\n const colRight = (colCount: number) => colOffsets[colCount + 1] * pxPerColWeightUnit;\n\n const rowOffsets = weightOffsets(rowWeights);\n const pxPerRowWeightUnit = height / rowOffsets[rowOffsets.length - 1];\n const rowBottom = (rowCount: number) => rowOffsets[rowCount] * pxPerRowWeightUnit;\n const rowTop = (rowCount: number) => rowOffsets[rowCount + 1] * pxPerRowWeightUnit;\n\n const cells = Array.from({ length: rows }).map((_, rowCount) =>\n columnLabels.map((columnLabel, colCount) => {\n const bounds: LatLngBoundsExpression = [\n [rowTop(rowCount), colRight(colCount)],\n [rowBottom(rowCount), colLeft(colCount)],\n ];\n\n const rowLabel = rowOffset + (rows - rowCount);\n const cellId = `${rowLabel},${columnLabel}`;\n const selected = selectedCells.has(cellId);\n const activate = () => onCellClick?.({ col: columnLabel, row: rowLabel });\n\n return (\n <Rectangle\n key={cellId}\n ref={(instance) =>\n bindCellAccessibility(instance, `${columnLabel}${rowLabel}`, selected, activate)\n }\n bounds={bounds}\n pathOptions={{\n fillColor: selected ? \"red\" : \"transparent\",\n fillOpacity: selected ? 0.5 : 0,\n weight: 1.5,\n }}\n eventHandlers={{\n click: activate,\n }}\n />\n );\n }),\n );\n\n const columnHeaders = columnLabels.map((columnLabel, colCount) => (\n <Marker\n key={`col-${columnLabel}`}\n position={[height, (colLeft(colCount) + colRight(colCount)) / 2]}\n icon={headerIcon(columnLabel, \"grid-header-label--column\")}\n interactive={false}\n />\n ));\n\n const rowHeaders = Array.from({ length: rows }).map((_, rowCount) => {\n const rowLabel = rowOffset + (rows - rowCount);\n return (\n <Marker\n key={`row-${rowLabel}`}\n position={[(rowBottom(rowCount) + rowTop(rowCount)) / 2, -7]}\n icon={headerIcon(String(rowLabel), \"grid-header-label--row\")}\n interactive={false}\n />\n );\n });\n\n return [...cells, columnHeaders, rowHeaders];\n};\n","export const resolveWeights = (\n count: number,\n overrides: Record<number, number> = {},\n): number[] => Array.from({ length: count }, (_, i) => overrides[i] ?? 1);\n\n/** Cumulative leading edge (in weight units) of each index, plus the total at the end. */\nexport const weightOffsets = (weights: number[]): number[] => {\n const offsets = [0];\n weights.forEach((weight) => offsets.push(offsets[offsets.length - 1] + weight));\n return offsets;\n};\n","import { divIcon } from \"leaflet\";\n\nexport const headerIcon = (label: string, className: string) =>\n divIcon({\n html: `<span class=\"grid-header-label ${className}\">${label}</span>`,\n className: \"grid-header-label-icon\",\n iconSize: [0, 0],\n });\n","import type { Rectangle as LeafletRectangle } from \"leaflet\";\n\n// Path.getElement() returns the underlying SVG node but isn't declared in @types/leaflet, hence the cast.\nconst getPathElement = (instance: LeafletRectangle) =>\n (instance as unknown as { getElement(): SVGElement | undefined }).getElement();\n\nexport const bindCellAccessibility = (\n instance: LeafletRectangle | null,\n label: string,\n selected: boolean,\n onActivate: () => void,\n) => {\n const el = instance && getPathElement(instance);\n if (!el) {\n return;\n }\n\n el.setAttribute(\"role\", \"button\");\n el.setAttribute(\"tabindex\", \"0\");\n el.setAttribute(\"aria-label\", label);\n el.setAttribute(\"aria-pressed\", String(selected));\n el.onkeydown = (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onActivate();\n }\n };\n};\n","import type { LatLngTuple } from \"leaflet\";\r\n\r\nexport const imageBounds = (src: string, containerWidth: number, containerHeight: number): Promise<LatLngTuple> =>\r\n new Promise((resolve, reject) => {\r\n const img = new Image();\r\n img.src = src;\r\n img.onload = () => {\r\n const scale = Math.min(containerWidth / img.width, containerHeight / img.height);\r\n resolve([img.height * scale, img.width * scale]);\r\n };\r\n img.onerror = () => reject(new Error(`Failed to load image: ${src}`));\r\n });","const toColumnLetter = (n: number): string => {\n let result = \"\";\n let remaining = n;\n\n while (remaining > 0) {\n const remainder = (remaining - 1) % 26;\n result = String.fromCharCode(65 + remainder) + result;\n remaining = Math.floor((remaining - 1) / 26);\n }\n\n return result;\n};\n\nexport const colNames = (startIndex: number, count: number): string[] =>\n Array.from({ length: count }, (_, i) => toColumnLetter(startIndex + i + 1));\n"],"mappings":"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","names":["useState","jsx","jsx","jsx","jsxs","useState"]}
|
|
1
|
+
{"version":3,"sources":["../MappedImage.tsx","#style-inject:#style-inject","../MappedImage.css","../components/DefaultImageSelector.tsx","../components/ImageLayer.tsx","../components/Grid.tsx","../helpers/weights.ts","../helpers/headerIcon.ts","../helpers/cellAccessibility.ts","../components/WeightHandle.tsx","../components/ZoomController.tsx","../helpers/imageBounds.ts","../helpers/colNames.ts"],"sourcesContent":["import { useState } from \"react\";\nimport \"leaflet/dist/leaflet.css\";\nimport \"./MappedImage.css\";\nimport type { ICellClickProps, ImageSelectorProps, MappedImageProps } from \"./types\";\nimport { DefaultImageSelector } from \"./components/DefaultImageSelector\";\nimport { ImageLayer } from \"./components/ImageLayer\";\n\n// Type-only re-export; doesn't add a non-component export for fast-refresh purposes.\n// eslint-disable-next-line react-refresh/only-export-components\nexport * from \"./types\";\n\nexport const MappedImage = (props: MappedImageProps) => {\n if (props.images.length === 0) {\n throw new Error(\"MappedImage requires at least one entry in `images`.\");\n }\n\n const invalidImage = props.images.find((image) => image.rows <= 0 || image.columns <= 0);\n if (invalidImage) {\n throw new Error(\n `MappedImage: image \"${invalidImage.name}\" must have rows > 0 and columns > 0.`,\n );\n }\n\n const [internalSelectedCells, setInternalSelectedCells] = useState<Set<string>>(\n props.selectedCells ?? new Set(),\n );\n const isSelectionControlled = props.selectedCells !== undefined;\n const selectedCells = isSelectionControlled ? props.selectedCells! : internalSelectedCells;\n\n const [selectedImageIndex, setSelectedImageIndex] = useState(\n props.selectedImageIndex ?? 0,\n );\n\n const selectedImage = props.images[selectedImageIndex];\n const colOffset = props.images\n .slice(0, selectedImageIndex)\n .reduce((sum, image) => sum + image.columns, 0);\n const rowOffset = props.images\n .slice(0, selectedImageIndex)\n .reduce((sum, image) => sum + image.rows, 0);\n\n const handleSelectImage = (index: number) => {\n setSelectedImageIndex(index);\n props.onSelectedImageIndexChange?.(index);\n };\n\n const handleCellClick = ({ row, col }: ICellClickProps) => {\n const cellId = `${row},${col}`;\n const next = new Set(selectedCells);\n if (next.has(cellId)) {\n next.delete(cellId);\n } else {\n next.add(cellId);\n }\n\n if (!isSelectionControlled) {\n setInternalSelectedCells(next);\n }\n props.onSelectedCellsChange?.(next);\n props.onCellClick?.({ row, col });\n };\n\n const renderSelector = props.renderImageSelector ?? (\n (selectorProps: ImageSelectorProps) => <DefaultImageSelector {...selectorProps} />\n );\n\n return (\n <div style={{ position: \"relative\", width: props.width, height: props.height }}>\n <ImageLayer\n key={selectedImage.src}\n image={selectedImage}\n colOffset={colOffset}\n rowOffset={rowOffset}\n width={props.width}\n height={props.height}\n alt={props.alt}\n selectedCells={selectedCells}\n maxBoundsPadding={props.maxBoundsPadding}\n zoomMultiplier={props.zoomMultiplier}\n weightsEditable={props.weightsEditable}\n onColumnWeightsChange={props.onColumnWeightsChange}\n onRowWeightsChange={props.onRowWeightsChange}\n onCellClick={handleCellClick}\n />\n {renderSelector({\n images: props.images,\n selectedIndex: selectedImageIndex,\n onSelect: handleSelectImage,\n })}\n </div>\n );\n};\n","\n export default function styleInject(css, { insertAt } = {}) {\n if (!css || typeof document === 'undefined') return\n \n const head = document.head || document.getElementsByTagName('head')[0]\n const style = document.createElement('style')\n style.type = 'text/css'\n \n if (insertAt === 'top') {\n if (head.firstChild) {\n head.insertBefore(style, head.firstChild)\n } else {\n head.appendChild(style)\n }\n } else {\n head.appendChild(style)\n }\n \n if (style.styleSheet) {\n style.styleSheet.cssText = css\n } else {\n style.appendChild(document.createTextNode(css))\n }\n }\n ","import styleInject from '#style-inject';styleInject(\".grid-header-label-icon {\\n background: none;\\n border: none;\\n}\\n.grid-header-label {\\n position: absolute;\\n display: block;\\n font-weight: bold;\\n white-space: nowrap;\\n}\\n.grid-header-label--column {\\n transform: translate(-50%, -100%);\\n}\\n.grid-header-label--row {\\n transform: translate(-100%, -50%);\\n}\\n.grid-weight-handle--column {\\n cursor: col-resize;\\n}\\n.grid-weight-handle--row {\\n cursor: row-resize;\\n}\\n.grid-weight-handle:hover {\\n stroke-opacity: 0.6;\\n}\\n\")","import type { ImageSelectorProps } from \"../types\";\n\nexport const DefaultImageSelector = ({ images, selectedIndex, onSelect }: ImageSelectorProps) => (\n <div\n style={{\n position: \"absolute\",\n top: 10,\n right: 10,\n zIndex: 1000,\n display: \"flex\",\n gap: 8,\n }}\n >\n {images.map((image, index) => (\n <button\n key={image.name}\n type=\"button\"\n onClick={() => onSelect(index)}\n style={{\n padding: \"12px 20px\",\n fontSize: 16,\n fontWeight: index === selectedIndex ? \"bold\" : \"normal\",\n background: index === selectedIndex ? \"#3388ff\" : \"white\",\n color: index === selectedIndex ? \"white\" : \"black\",\n border: \"1px solid #3388ff\",\n borderRadius: 6,\n }}\n >\n {image.name}\n </button>\n ))}\n </div>\n);\n","import { useEffect, useState } from \"react\";\nimport { CRS, type LatLngBoundsExpression, type LatLngTuple } from \"leaflet\";\nimport { MapContainer, ImageOverlay } from \"react-leaflet\";\nimport type { ImageLayerProps } from \"../types\";\nimport { Grid } from \"./Grid\";\nimport { ZoomController } from \"./ZoomController\";\nimport { imageBounds } from \"../helpers/imageBounds\";\nimport { colNames } from \"../helpers/colNames\";\nimport { resolveWeights } from \"../helpers/weights\";\n\nexport const ImageLayer = ({\n image,\n colOffset,\n rowOffset,\n width,\n height,\n alt,\n selectedCells,\n maxBoundsPadding = 100,\n zoomMultiplier = 1,\n weightsEditable,\n onColumnWeightsChange,\n onRowWeightsChange,\n onCellClick,\n}: ImageLayerProps) => {\n const [imgBounds, setImgBounds] = useState<LatLngTuple | null>(null);\n const [loadError, setLoadError] = useState<Error | null>(null);\n\n useEffect(() => {\n imageBounds(image.src, width, height).then(setImgBounds).catch(setLoadError);\n }, [image.src, width, height]);\n\n if (loadError) {\n throw loadError;\n }\n\n if (!imgBounds) {\n return null;\n }\n\n const [imgHeight, imgWidth] = imgBounds;\n const bounds: LatLngBoundsExpression = [[0, 0], imgBounds];\n const maxBounds: LatLngBoundsExpression = [\n [-maxBoundsPadding , -maxBoundsPadding],\n [imgHeight + maxBoundsPadding, imgWidth + maxBoundsPadding],\n ];\n const columnLabels = colNames(colOffset, image.columns);\n const columnWeights = resolveWeights(image.columns, image.columnWeights);\n const rowWeights = resolveWeights(image.rows, image.rowWeights);\n\n return (\n <MapContainer\n crs={CRS.Simple}\n bounds={bounds}\n boundsOptions={{ padding: [50, 50] }}\n maxBounds={maxBounds}\n maxBoundsViscosity={0.5}\n style={{ width, height }}\n maxZoom={2}\n >\n <ZoomController zoomMultiplier={zoomMultiplier} />\n <Grid\n rows={image.rows}\n rowOffset={rowOffset}\n rowWeights={rowWeights}\n columnLabels={columnLabels}\n columnWeights={columnWeights}\n imgBounds={imgBounds}\n selectedCells={selectedCells}\n weightsEditable={weightsEditable}\n onColumnWeightsChange={onColumnWeightsChange}\n onRowWeightsChange={onRowWeightsChange}\n onCellClick={onCellClick}\n />\n <ImageOverlay url={image.src} bounds={bounds} alt={alt} />\n </MapContainer>\n );\n};\n","import { useState } from \"react\";\nimport { Rectangle, Marker } from \"react-leaflet\";\nimport type { LatLngBoundsExpression } from \"leaflet\";\nimport type { GridProps } from \"../types\";\nimport { applyWeightDelta, weightOffsets } from \"../helpers/weights\";\nimport { headerIcon } from \"../helpers/headerIcon\";\nimport { bindCellAccessibility } from \"../helpers/cellAccessibility\";\nimport { WeightHandle } from \"./WeightHandle\";\n\ntype DragState = { axis: \"column\" | \"row\"; index: number; deltaWeight: number };\n\nexport const Grid = ({\n rows,\n rowOffset,\n rowWeights,\n columnLabels,\n columnWeights,\n imgBounds,\n selectedCells,\n weightsEditable,\n onColumnWeightsChange,\n onRowWeightsChange,\n onCellClick,\n}: GridProps) => {\n const [dragState, setDragState] = useState<DragState | null>(null);\n const [height, width] = imgBounds;\n\n const displayColumnWeights =\n dragState?.axis === \"column\"\n ? applyWeightDelta(columnWeights, dragState.index, dragState.deltaWeight)\n : columnWeights;\n const displayRowWeights =\n dragState?.axis === \"row\"\n ? applyWeightDelta(rowWeights, dragState.index, dragState.deltaWeight)\n : rowWeights;\n\n const colOffsets = weightOffsets(displayColumnWeights);\n const pxPerColWeightUnit = width / colOffsets[colOffsets.length - 1];\n const colLeft = (colCount: number) => colOffsets[colCount] * pxPerColWeightUnit;\n const colRight = (colCount: number) => colOffsets[colCount + 1] * pxPerColWeightUnit;\n\n const rowOffsets = weightOffsets(displayRowWeights);\n const pxPerRowWeightUnit = height / rowOffsets[rowOffsets.length - 1];\n const rowBottom = (rowCount: number) => rowOffsets[rowCount] * pxPerRowWeightUnit;\n const rowTop = (rowCount: number) => rowOffsets[rowCount + 1] * pxPerRowWeightUnit;\n\n const cells = Array.from({ length: rows }).map((_, rowCount) =>\n columnLabels.map((columnLabel, colCount) => {\n const bounds: LatLngBoundsExpression = [\n [rowTop(rowCount), colRight(colCount)],\n [rowBottom(rowCount), colLeft(colCount)],\n ];\n\n const rowLabel = rowOffset + (rows - rowCount);\n const cellId = `${rowLabel},${columnLabel}`;\n const selected = selectedCells.has(cellId);\n const activate = () => onCellClick?.({ col: columnLabel, row: rowLabel });\n\n return (\n <Rectangle\n key={cellId}\n ref={(instance) =>\n bindCellAccessibility(instance, `${columnLabel}${rowLabel}`, selected, activate)\n }\n bounds={bounds}\n pathOptions={{\n fillColor: selected ? \"red\" : \"transparent\",\n fillOpacity: selected ? 0.5 : 0,\n weight: 1.5,\n }}\n eventHandlers={{\n click: activate,\n }}\n />\n );\n }),\n );\n\n const columnHeaders = columnLabels.map((columnLabel, colCount) => (\n <Marker\n key={`col-${columnLabel}`}\n position={[height, (colLeft(colCount) + colRight(colCount)) / 2]}\n icon={headerIcon(columnLabel, \"grid-header-label--column\")}\n interactive={false}\n />\n ));\n\n const rowHeaders = Array.from({ length: rows }).map((_, rowCount) => {\n const rowLabel = rowOffset + (rows - rowCount);\n return (\n <Marker\n key={`row-${rowLabel}`}\n position={[(rowBottom(rowCount) + rowTop(rowCount)) / 2, -7]}\n icon={headerIcon(String(rowLabel), \"grid-header-label--row\")}\n interactive={false}\n />\n );\n });\n\n const columnHandles = weightsEditable\n ? columnLabels.slice(0, -1).map((_, index) => (\n <WeightHandle\n key={`col-handle-${index}`}\n axis=\"column\"\n boundary={colRight(index)}\n length={height}\n onDrag={(deltaUnits) =>\n setDragState({ axis: \"column\", index, deltaWeight: deltaUnits / pxPerColWeightUnit })\n }\n onDragEnd={() => {\n onColumnWeightsChange?.(displayColumnWeights);\n setDragState(null);\n }}\n />\n ))\n : [];\n\n const rowHandles = weightsEditable\n ? Array.from({ length: rows - 1 }).map((_, index) => (\n <WeightHandle\n key={`row-handle-${index}`}\n axis=\"row\"\n boundary={rowTop(index)}\n length={width}\n onDrag={(deltaUnits) =>\n setDragState({ axis: \"row\", index, deltaWeight: deltaUnits / pxPerRowWeightUnit })\n }\n onDragEnd={() => {\n onRowWeightsChange?.(displayRowWeights);\n setDragState(null);\n }}\n />\n ))\n : [];\n\n return [...cells, columnHeaders, rowHeaders, columnHandles, rowHandles];\n};\n","export const resolveWeights = (\n count: number,\n overrides: Record<number, number> = {},\n): number[] => Array.from({ length: count }, (_, i) => overrides[i] ?? 1);\n\n/** Cumulative leading edge (in weight units) of each index, plus the total at the end. */\nexport const weightOffsets = (weights: number[]): number[] => {\n const offsets = [0];\n weights.forEach((weight) => offsets.push(offsets[offsets.length - 1] + weight));\n return offsets;\n};\n\nconst MIN_WEIGHT = 0.2;\n\n/** Shifts weight from index+1 into index (or back, for a negative delta), clamped so neither drops below MIN_WEIGHT. Total weight is preserved. */\nexport const applyWeightDelta = (weights: number[], index: number, delta: number): number[] => {\n const a = weights[index];\n const b = weights[index + 1];\n const clampedDelta = Math.max(-(a - MIN_WEIGHT), Math.min(b - MIN_WEIGHT, delta));\n\n const next = [...weights];\n next[index] = a + clampedDelta;\n next[index + 1] = b - clampedDelta;\n return next;\n};\n","import { divIcon } from \"leaflet\";\n\nexport const headerIcon = (label: string, className: string) =>\n divIcon({\n html: `<span class=\"grid-header-label ${className}\">${label}</span>`,\n className: \"grid-header-label-icon\",\n iconSize: [0, 0],\n });\n","import type { Rectangle as LeafletRectangle } from \"leaflet\";\n\n// Path.getElement() returns the underlying SVG node but isn't declared in @types/leaflet, hence the cast.\nconst getPathElement = (instance: LeafletRectangle) =>\n (instance as unknown as { getElement(): SVGElement | undefined }).getElement();\n\nexport const bindCellAccessibility = (\n instance: LeafletRectangle | null,\n label: string,\n selected: boolean,\n onActivate: () => void,\n) => {\n const el = instance && getPathElement(instance);\n if (!el) {\n return;\n }\n\n el.setAttribute(\"role\", \"button\");\n el.setAttribute(\"tabindex\", \"0\");\n el.setAttribute(\"aria-label\", label);\n el.setAttribute(\"aria-pressed\", String(selected));\n el.onkeydown = (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onActivate();\n }\n };\n};\n","import { useRef } from \"react\";\nimport { Polyline, useMap } from \"react-leaflet\";\nimport type { LatLngExpression, LeafletMouseEvent } from \"leaflet\";\n\ntype WeightHandleProps = {\n axis: \"column\" | \"row\";\n /** Position along the cross-axis: lng for a column boundary, lat for a row boundary. */\n boundary: number;\n /** Full span of the line: image height for column boundaries, image width for row boundaries. */\n length: number;\n onDrag: (deltaUnits: number) => void;\n onDragEnd: () => void;\n};\n\nexport const WeightHandle = ({ axis, boundary, length, onDrag, onDragEnd }: WeightHandleProps) => {\n const map = useMap();\n const dragStartRef = useRef<number | null>(null);\n\n const positions: LatLngExpression[] =\n axis === \"column\" ? [[0, boundary], [length, boundary]] : [[boundary, 0], [boundary, length]];\n\n const handleMouseDown = (event: LeafletMouseEvent) => {\n event.originalEvent.preventDefault();\n map.dragging.disable();\n dragStartRef.current = axis === \"column\" ? event.latlng.lng : event.latlng.lat;\n\n const handleMouseMove = (moveEvent: MouseEvent) => {\n if (dragStartRef.current === null) {\n return;\n }\n const latlng = map.mouseEventToLatLng(moveEvent);\n const current = axis === \"column\" ? latlng.lng : latlng.lat;\n onDrag(current - dragStartRef.current);\n };\n\n const handleMouseUp = () => {\n dragStartRef.current = null;\n map.dragging.enable();\n onDragEnd();\n window.removeEventListener(\"mousemove\", handleMouseMove);\n window.removeEventListener(\"mouseup\", handleMouseUp);\n };\n\n window.addEventListener(\"mousemove\", handleMouseMove);\n window.addEventListener(\"mouseup\", handleMouseUp);\n };\n\n return (\n <Polyline\n positions={positions}\n pathOptions={{\n color: \"#3388ff\",\n weight: 6,\n opacity: 0.15,\n className: `grid-weight-handle grid-weight-handle--${axis}`,\n }}\n eventHandlers={{ mousedown: handleMouseDown }}\n />\n );\n};\n","import { useEffect, useRef } from \"react\";\nimport { useMap } from \"react-leaflet\";\n\ntype ZoomControllerProps = {\n zoomMultiplier: number;\n};\n\nexport const ZoomController = ({ zoomMultiplier }: ZoomControllerProps) => {\n const map = useMap();\n const baseZoomRef = useRef<number | null>(null);\n\n useEffect(() => {\n // Capture the zoom react-leaflet's fitBounds already settled on, before any multiplier is applied.\n if (baseZoomRef.current === null) {\n baseZoomRef.current = map.getZoom();\n }\n\n map.setZoom(baseZoomRef.current + Math.log2(zoomMultiplier));\n }, [map, zoomMultiplier]);\n\n return null;\n};\n","import type { LatLngTuple } from \"leaflet\";\r\n\r\nexport const imageBounds = (src: string, containerWidth: number, containerHeight: number): Promise<LatLngTuple> =>\r\n new Promise((resolve, reject) => {\r\n const img = new Image();\r\n img.src = src;\r\n img.onload = () => {\r\n const scale = Math.min(containerWidth / img.width, containerHeight / img.height);\r\n resolve([img.height * scale, img.width * scale]);\r\n };\r\n img.onerror = () => reject(new Error(`Failed to load image: ${src}`));\r\n });","const toColumnLetter = (n: number): string => {\n let result = \"\";\n let remaining = n;\n\n while (remaining > 0) {\n const remainder = (remaining - 1) % 26;\n result = String.fromCharCode(65 + remainder) + result;\n remaining = Math.floor((remaining - 1) / 26);\n }\n\n return result;\n};\n\nexport const colNames = (startIndex: number, count: number): string[] =>\n Array.from({ length: count }, (_, i) => toColumnLetter(startIndex + i + 1));\n"],"mappings":";AAAA,SAAS,YAAAA,iBAAgB;AACzB,OAAO;;;ACAkB,SAAR,YAA6B,KAAK,EAAE,SAAS,IAAI,CAAC,GAAG;AAC1D,MAAI,CAAC,OAAO,OAAO,aAAa,YAAa;AAE7C,QAAM,OAAO,SAAS,QAAQ,SAAS,qBAAqB,MAAM,EAAE,CAAC;AACrE,QAAM,QAAQ,SAAS,cAAc,OAAO;AAC5C,QAAM,OAAO;AAEb,MAAI,aAAa,OAAO;AACtB,QAAI,KAAK,YAAY;AACnB,WAAK,aAAa,OAAO,KAAK,UAAU;AAAA,IAC1C,OAAO;AACL,WAAK,YAAY,KAAK;AAAA,IACxB;AAAA,EACF,OAAO;AACL,SAAK,YAAY,KAAK;AAAA,EACxB;AAEA,MAAI,MAAM,YAAY;AACpB,UAAM,WAAW,UAAU;AAAA,EAC7B,OAAO;AACL,UAAM,YAAY,SAAS,eAAe,GAAG,CAAC;AAAA,EAChD;AACF;;;ACvB8B,YAAY,yeAAye;;;ACcjhB;AAZL,IAAM,uBAAuB,CAAC,EAAE,QAAQ,eAAe,SAAS,MACnE;AAAA,EAAC;AAAA;AAAA,IACG,OAAO;AAAA,MACH,UAAU;AAAA,MACV,KAAK;AAAA,MACL,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,SAAS;AAAA,MACT,KAAK;AAAA,IACT;AAAA,IAEC,iBAAO,IAAI,CAAC,OAAO,UAChB;AAAA,MAAC;AAAA;AAAA,QAEG,MAAK;AAAA,QACL,SAAS,MAAM,SAAS,KAAK;AAAA,QAC7B,OAAO;AAAA,UACH,SAAS;AAAA,UACT,UAAU;AAAA,UACV,YAAY,UAAU,gBAAgB,SAAS;AAAA,UAC/C,YAAY,UAAU,gBAAgB,YAAY;AAAA,UAClD,OAAO,UAAU,gBAAgB,UAAU;AAAA,UAC3C,QAAQ;AAAA,UACR,cAAc;AAAA,QAClB;AAAA,QAEC,gBAAM;AAAA;AAAA,MAbF,MAAM;AAAA,IAcf,CACH;AAAA;AACL;;;AC/BJ,SAAS,aAAAC,YAAW,YAAAC,iBAAgB;AACpC,SAAS,WAA0D;AACnE,SAAS,cAAc,oBAAoB;;;ACF3C,SAAS,gBAAgB;AACzB,SAAS,WAAW,cAAc;;;ACD3B,IAAM,iBAAiB,CAC1B,OACA,YAAoC,CAAC,MAC1B,MAAM,KAAK,EAAE,QAAQ,MAAM,GAAG,CAAC,GAAG,MAAM,UAAU,CAAC,KAAK,CAAC;AAGjE,IAAM,gBAAgB,CAAC,YAAgC;AAC1D,QAAM,UAAU,CAAC,CAAC;AAClB,UAAQ,QAAQ,CAAC,WAAW,QAAQ,KAAK,QAAQ,QAAQ,SAAS,CAAC,IAAI,MAAM,CAAC;AAC9E,SAAO;AACX;AAEA,IAAM,aAAa;AAGZ,IAAM,mBAAmB,CAAC,SAAmB,OAAe,UAA4B;AAC3F,QAAM,IAAI,QAAQ,KAAK;AACvB,QAAM,IAAI,QAAQ,QAAQ,CAAC;AAC3B,QAAM,eAAe,KAAK,IAAI,EAAE,IAAI,aAAa,KAAK,IAAI,IAAI,YAAY,KAAK,CAAC;AAEhF,QAAM,OAAO,CAAC,GAAG,OAAO;AACxB,OAAK,KAAK,IAAI,IAAI;AAClB,OAAK,QAAQ,CAAC,IAAI,IAAI;AACtB,SAAO;AACX;;;ACxBA,SAAS,eAAe;AAEjB,IAAM,aAAa,CAAC,OAAe,cACtC,QAAQ;AAAA,EACJ,MAAM,kCAAkC,SAAS,KAAK,KAAK;AAAA,EAC3D,WAAW;AAAA,EACX,UAAU,CAAC,GAAG,CAAC;AACnB,CAAC;;;ACJL,IAAM,iBAAiB,CAAC,aACnB,SAAiE,WAAW;AAE1E,IAAM,wBAAwB,CACjC,UACA,OACA,UACA,eACC;AACD,QAAM,KAAK,YAAY,eAAe,QAAQ;AAC9C,MAAI,CAAC,IAAI;AACL;AAAA,EACJ;AAEA,KAAG,aAAa,QAAQ,QAAQ;AAChC,KAAG,aAAa,YAAY,GAAG;AAC/B,KAAG,aAAa,cAAc,KAAK;AACnC,KAAG,aAAa,gBAAgB,OAAO,QAAQ,CAAC;AAChD,KAAG,YAAY,CAAC,MAAM;AAClB,QAAI,EAAE,QAAQ,WAAW,EAAE,QAAQ,KAAK;AACpC,QAAE,eAAe;AACjB,iBAAW;AAAA,IACf;AAAA,EACJ;AACJ;;;AC3BA,SAAS,cAAc;AACvB,SAAS,UAAU,cAAc;AA+CzB,gBAAAC,YAAA;AAlCD,IAAM,eAAe,CAAC,EAAE,MAAM,UAAU,QAAQ,QAAQ,UAAU,MAAyB;AAC9F,QAAM,MAAM,OAAO;AACnB,QAAM,eAAe,OAAsB,IAAI;AAE/C,QAAM,YACF,SAAS,WAAW,CAAC,CAAC,GAAG,QAAQ,GAAG,CAAC,QAAQ,QAAQ,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,UAAU,MAAM,CAAC;AAEhG,QAAM,kBAAkB,CAAC,UAA6B;AAClD,UAAM,cAAc,eAAe;AACnC,QAAI,SAAS,QAAQ;AACrB,iBAAa,UAAU,SAAS,WAAW,MAAM,OAAO,MAAM,MAAM,OAAO;AAE3E,UAAM,kBAAkB,CAAC,cAA0B;AAC/C,UAAI,aAAa,YAAY,MAAM;AAC/B;AAAA,MACJ;AACA,YAAM,SAAS,IAAI,mBAAmB,SAAS;AAC/C,YAAM,UAAU,SAAS,WAAW,OAAO,MAAM,OAAO;AACxD,aAAO,UAAU,aAAa,OAAO;AAAA,IACzC;AAEA,UAAM,gBAAgB,MAAM;AACxB,mBAAa,UAAU;AACvB,UAAI,SAAS,OAAO;AACpB,gBAAU;AACV,aAAO,oBAAoB,aAAa,eAAe;AACvD,aAAO,oBAAoB,WAAW,aAAa;AAAA,IACvD;AAEA,WAAO,iBAAiB,aAAa,eAAe;AACpD,WAAO,iBAAiB,WAAW,aAAa;AAAA,EACpD;AAEA,SACI,gBAAAA;AAAA,IAAC;AAAA;AAAA,MACG;AAAA,MACA,aAAa;AAAA,QACT,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,SAAS;AAAA,QACT,WAAW,0CAA0C,IAAI;AAAA,MAC7D;AAAA,MACA,eAAe,EAAE,WAAW,gBAAgB;AAAA;AAAA,EAChD;AAER;;;AJAgB,gBAAAC,YAAA;AAhDT,IAAM,OAAO,CAAC;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,MAAiB;AACb,QAAM,CAAC,WAAW,YAAY,IAAI,SAA2B,IAAI;AACjE,QAAM,CAAC,QAAQ,KAAK,IAAI;AAExB,QAAM,uBACF,WAAW,SAAS,WACd,iBAAiB,eAAe,UAAU,OAAO,UAAU,WAAW,IACtE;AACV,QAAM,oBACF,WAAW,SAAS,QACd,iBAAiB,YAAY,UAAU,OAAO,UAAU,WAAW,IACnE;AAEV,QAAM,aAAa,cAAc,oBAAoB;AACrD,QAAM,qBAAqB,QAAQ,WAAW,WAAW,SAAS,CAAC;AACnE,QAAM,UAAU,CAAC,aAAqB,WAAW,QAAQ,IAAI;AAC7D,QAAM,WAAW,CAAC,aAAqB,WAAW,WAAW,CAAC,IAAI;AAElE,QAAM,aAAa,cAAc,iBAAiB;AAClD,QAAM,qBAAqB,SAAS,WAAW,WAAW,SAAS,CAAC;AACpE,QAAM,YAAY,CAAC,aAAqB,WAAW,QAAQ,IAAI;AAC/D,QAAM,SAAS,CAAC,aAAqB,WAAW,WAAW,CAAC,IAAI;AAEhE,QAAM,QAAQ,MAAM,KAAK,EAAE,QAAQ,KAAK,CAAC,EAAE;AAAA,IAAI,CAAC,GAAG,aAC/C,aAAa,IAAI,CAAC,aAAa,aAAa;AACxC,YAAM,SAAiC;AAAA,QACnC,CAAC,OAAO,QAAQ,GAAG,SAAS,QAAQ,CAAC;AAAA,QACrC,CAAC,UAAU,QAAQ,GAAG,QAAQ,QAAQ,CAAC;AAAA,MAC3C;AAEA,YAAM,WAAW,aAAa,OAAO;AACrC,YAAM,SAAS,GAAG,QAAQ,IAAI,WAAW;AACzC,YAAM,WAAW,cAAc,IAAI,MAAM;AACzC,YAAM,WAAW,MAAM,cAAc,EAAE,KAAK,aAAa,KAAK,SAAS,CAAC;AAExE,aACI,gBAAAA;AAAA,QAAC;AAAA;AAAA,UAEG,KAAK,CAAC,aACF,sBAAsB,UAAU,GAAG,WAAW,GAAG,QAAQ,IAAI,UAAU,QAAQ;AAAA,UAEnF;AAAA,UACA,aAAa;AAAA,YACT,WAAW,WAAW,QAAQ;AAAA,YAC9B,aAAa,WAAW,MAAM;AAAA,YAC9B,QAAQ;AAAA,UACZ;AAAA,UACA,eAAe;AAAA,YACX,OAAO;AAAA,UACX;AAAA;AAAA,QAZK;AAAA,MAaT;AAAA,IAER,CAAC;AAAA,EACL;AAEA,QAAM,gBAAgB,aAAa,IAAI,CAAC,aAAa,aACjD,gBAAAA;AAAA,IAAC;AAAA;AAAA,MAEG,UAAU,CAAC,SAAS,QAAQ,QAAQ,IAAI,SAAS,QAAQ,KAAK,CAAC;AAAA,MAC/D,MAAM,WAAW,aAAa,2BAA2B;AAAA,MACzD,aAAa;AAAA;AAAA,IAHR,OAAO,WAAW;AAAA,EAI3B,CACH;AAED,QAAM,aAAa,MAAM,KAAK,EAAE,QAAQ,KAAK,CAAC,EAAE,IAAI,CAAC,GAAG,aAAa;AACjE,UAAM,WAAW,aAAa,OAAO;AACrC,WACI,gBAAAA;AAAA,MAAC;AAAA;AAAA,QAEG,UAAU,EAAE,UAAU,QAAQ,IAAI,OAAO,QAAQ,KAAK,GAAG,EAAE;AAAA,QAC3D,MAAM,WAAW,OAAO,QAAQ,GAAG,wBAAwB;AAAA,QAC3D,aAAa;AAAA;AAAA,MAHR,OAAO,QAAQ;AAAA,IAIxB;AAAA,EAER,CAAC;AAED,QAAM,gBAAgB,kBAChB,aAAa,MAAM,GAAG,EAAE,EAAE,IAAI,CAAC,GAAG,UAC9B,gBAAAA;AAAA,IAAC;AAAA;AAAA,MAEG,MAAK;AAAA,MACL,UAAU,SAAS,KAAK;AAAA,MACxB,QAAQ;AAAA,MACR,QAAQ,CAAC,eACL,aAAa,EAAE,MAAM,UAAU,OAAO,aAAa,aAAa,mBAAmB,CAAC;AAAA,MAExF,WAAW,MAAM;AACb,gCAAwB,oBAAoB;AAC5C,qBAAa,IAAI;AAAA,MACrB;AAAA;AAAA,IAVK,cAAc,KAAK;AAAA,EAW5B,CACH,IACD,CAAC;AAEP,QAAM,aAAa,kBACb,MAAM,KAAK,EAAE,QAAQ,OAAO,EAAE,CAAC,EAAE,IAAI,CAAC,GAAG,UACrC,gBAAAA;AAAA,IAAC;AAAA;AAAA,MAEG,MAAK;AAAA,MACL,UAAU,OAAO,KAAK;AAAA,MACtB,QAAQ;AAAA,MACR,QAAQ,CAAC,eACL,aAAa,EAAE,MAAM,OAAO,OAAO,aAAa,aAAa,mBAAmB,CAAC;AAAA,MAErF,WAAW,MAAM;AACb,6BAAqB,iBAAiB;AACtC,qBAAa,IAAI;AAAA,MACrB;AAAA;AAAA,IAVK,cAAc,KAAK;AAAA,EAW5B,CACH,IACD,CAAC;AAEP,SAAO,CAAC,GAAG,OAAO,eAAe,YAAY,eAAe,UAAU;AAC1E;;;AKxIA,SAAS,WAAW,UAAAC,eAAc;AAClC,SAAS,UAAAC,eAAc;AAMhB,IAAM,iBAAiB,CAAC,EAAE,eAAe,MAA2B;AACvE,QAAM,MAAMA,QAAO;AACnB,QAAM,cAAcD,QAAsB,IAAI;AAE9C,YAAU,MAAM;AAEZ,QAAI,YAAY,YAAY,MAAM;AAC9B,kBAAY,UAAU,IAAI,QAAQ;AAAA,IACtC;AAEA,QAAI,QAAQ,YAAY,UAAU,KAAK,KAAK,cAAc,CAAC;AAAA,EAC/D,GAAG,CAAC,KAAK,cAAc,CAAC;AAExB,SAAO;AACX;;;ACnBO,IAAM,cAAc,CAAC,KAAa,gBAAwB,oBAC7D,IAAI,QAAQ,CAAC,SAAS,WAAW;AAC7B,QAAM,MAAM,IAAI,MAAM;AACtB,MAAI,MAAM;AACV,MAAI,SAAS,MAAM;AACf,UAAM,QAAQ,KAAK,IAAI,iBAAiB,IAAI,OAAO,kBAAkB,IAAI,MAAM;AAC/E,YAAQ,CAAC,IAAI,SAAS,OAAO,IAAI,QAAQ,KAAK,CAAC;AAAA,EACnD;AACA,MAAI,UAAU,MAAM,OAAO,IAAI,MAAM,yBAAyB,GAAG,EAAE,CAAC;AACxE,CAAC;;;ACXL,IAAM,iBAAiB,CAAC,MAAsB;AAC1C,MAAI,SAAS;AACb,MAAI,YAAY;AAEhB,SAAO,YAAY,GAAG;AAClB,UAAM,aAAa,YAAY,KAAK;AACpC,aAAS,OAAO,aAAa,KAAK,SAAS,IAAI;AAC/C,gBAAY,KAAK,OAAO,YAAY,KAAK,EAAE;AAAA,EAC/C;AAEA,SAAO;AACX;AAEO,IAAM,WAAW,CAAC,YAAoB,UACzC,MAAM,KAAK,EAAE,QAAQ,MAAM,GAAG,CAAC,GAAG,MAAM,eAAe,aAAa,IAAI,CAAC,CAAC;;;ARqCtE,SASI,OAAAE,MATJ;AAzCD,IAAM,aAAa,CAAC;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,mBAAmB;AAAA,EACnB,iBAAiB;AAAA,EACjB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACJ,MAAuB;AACnB,QAAM,CAAC,WAAW,YAAY,IAAIC,UAA6B,IAAI;AACnE,QAAM,CAAC,WAAW,YAAY,IAAIA,UAAuB,IAAI;AAE7D,EAAAC,WAAU,MAAM;AACZ,gBAAY,MAAM,KAAK,OAAO,MAAM,EAAE,KAAK,YAAY,EAAE,MAAM,YAAY;AAAA,EAC/E,GAAG,CAAC,MAAM,KAAK,OAAO,MAAM,CAAC;AAE7B,MAAI,WAAW;AACX,UAAM;AAAA,EACV;AAEA,MAAI,CAAC,WAAW;AACZ,WAAO;AAAA,EACX;AAEA,QAAM,CAAC,WAAW,QAAQ,IAAI;AAC9B,QAAM,SAAiC,CAAC,CAAC,GAAG,CAAC,GAAG,SAAS;AACzD,QAAM,YAAoC;AAAA,IACtC,CAAC,CAAC,kBAAmB,CAAC,gBAAgB;AAAA,IACtC,CAAC,YAAY,kBAAkB,WAAW,gBAAgB;AAAA,EAC9D;AACA,QAAM,eAAe,SAAS,WAAW,MAAM,OAAO;AACtD,QAAM,gBAAgB,eAAe,MAAM,SAAS,MAAM,aAAa;AACvE,QAAM,aAAa,eAAe,MAAM,MAAM,MAAM,UAAU;AAE9D,SACI;AAAA,IAAC;AAAA;AAAA,MACG,KAAK,IAAI;AAAA,MACT;AAAA,MACA,eAAe,EAAE,SAAS,CAAC,IAAI,EAAE,EAAE;AAAA,MACnC;AAAA,MACA,oBAAoB;AAAA,MACpB,OAAO,EAAE,OAAO,OAAO;AAAA,MACvB,SAAS;AAAA,MAET;AAAA,wBAAAF,KAAC,kBAAe,gBAAgC;AAAA,QAChD,gBAAAA;AAAA,UAAC;AAAA;AAAA,YACG,MAAM,MAAM;AAAA,YACZ;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA,YACA;AAAA;AAAA,QACJ;AAAA,QACA,gBAAAA,KAAC,gBAAa,KAAK,MAAM,KAAK,QAAgB,KAAU;AAAA;AAAA;AAAA,EAC5D;AAER;;;AJd+C,gBAAAG,MAIvC,QAAAC,aAJuC;AApDxC,IAAM,cAAc,CAAC,UAA4B;AACpD,MAAI,MAAM,OAAO,WAAW,GAAG;AAC3B,UAAM,IAAI,MAAM,sDAAsD;AAAA,EAC1E;AAEA,QAAM,eAAe,MAAM,OAAO,KAAK,CAAC,UAAU,MAAM,QAAQ,KAAK,MAAM,WAAW,CAAC;AACvF,MAAI,cAAc;AACd,UAAM,IAAI;AAAA,MACN,uBAAuB,aAAa,IAAI;AAAA,IAC5C;AAAA,EACJ;AAEA,QAAM,CAAC,uBAAuB,wBAAwB,IAAIC;AAAA,IACtD,MAAM,iBAAiB,oBAAI,IAAI;AAAA,EACnC;AACA,QAAM,wBAAwB,MAAM,kBAAkB;AACtD,QAAM,gBAAgB,wBAAwB,MAAM,gBAAiB;AAErE,QAAM,CAAC,oBAAoB,qBAAqB,IAAIA;AAAA,IAChD,MAAM,sBAAsB;AAAA,EAChC;AAEA,QAAM,gBAAgB,MAAM,OAAO,kBAAkB;AACrD,QAAM,YAAY,MAAM,OACnB,MAAM,GAAG,kBAAkB,EAC3B,OAAO,CAAC,KAAK,UAAU,MAAM,MAAM,SAAS,CAAC;AAClD,QAAM,YAAY,MAAM,OACnB,MAAM,GAAG,kBAAkB,EAC3B,OAAO,CAAC,KAAK,UAAU,MAAM,MAAM,MAAM,CAAC;AAE/C,QAAM,oBAAoB,CAAC,UAAkB;AACzC,0BAAsB,KAAK;AAC3B,UAAM,6BAA6B,KAAK;AAAA,EAC5C;AAEA,QAAM,kBAAkB,CAAC,EAAE,KAAK,IAAI,MAAuB;AACvD,UAAM,SAAS,GAAG,GAAG,IAAI,GAAG;AAC5B,UAAM,OAAO,IAAI,IAAI,aAAa;AAClC,QAAI,KAAK,IAAI,MAAM,GAAG;AAClB,WAAK,OAAO,MAAM;AAAA,IACtB,OAAO;AACH,WAAK,IAAI,MAAM;AAAA,IACnB;AAEA,QAAI,CAAC,uBAAuB;AACxB,+BAAyB,IAAI;AAAA,IACjC;AACA,UAAM,wBAAwB,IAAI;AAClC,UAAM,cAAc,EAAE,KAAK,IAAI,CAAC;AAAA,EACpC;AAEA,QAAM,iBAAiB,MAAM,wBACzB,CAAC,kBAAsC,gBAAAF,KAAC,wBAAsB,GAAG,eAAe;AAGpF,SACI,gBAAAC,MAAC,SAAI,OAAO,EAAE,UAAU,YAAY,OAAO,MAAM,OAAO,QAAQ,MAAM,OAAO,GACzE;AAAA,oBAAAD;AAAA,MAAC;AAAA;AAAA,QAEG,OAAO;AAAA,QACP;AAAA,QACA;AAAA,QACA,OAAO,MAAM;AAAA,QACb,QAAQ,MAAM;AAAA,QACd,KAAK,MAAM;AAAA,QACX;AAAA,QACA,kBAAkB,MAAM;AAAA,QACxB,gBAAgB,MAAM;AAAA,QACtB,iBAAiB,MAAM;AAAA,QACvB,uBAAuB,MAAM;AAAA,QAC7B,oBAAoB,MAAM;AAAA,QAC1B,aAAa;AAAA;AAAA,MAbR,cAAc;AAAA,IAcvB;AAAA,IACC,eAAe;AAAA,MACZ,QAAQ,MAAM;AAAA,MACd,eAAe;AAAA,MACf,UAAU;AAAA,IACd,CAAC;AAAA,KACL;AAER;","names":["useState","useEffect","useState","jsx","jsx","useRef","useMap","jsx","useState","useEffect","jsx","jsxs","useState"]}
|
package/package.json
CHANGED