Updated the map markers so they can be edited as well as to permit markdown in the notes field.

This commit is contained in:
Kenneth Brewer 2026-04-27 16:02:21 -04:00
parent 5a55c8e632
commit 596d9f7172
9 changed files with 543 additions and 172 deletions

View File

@ -137,9 +137,11 @@ export default class MapsController {
vine.compile(
vine.object({
name: vine.string().trim().minLength(1).maxLength(255),
longitude: vine.number(),
latitude: vine.number(),
longitude: vine.number().min(-180).max(180),
latitude: vine.number().min(-90).max(90),
color: vine.string().trim().maxLength(20).optional(),
notes: vine.string().trim().nullable().optional(),
marker_type: vine.string().trim().maxLength(20).optional(),
})
)
)
@ -148,6 +150,8 @@ export default class MapsController {
longitude: payload.longitude,
latitude: payload.latitude,
color: payload.color ?? 'orange',
notes: payload.notes ?? null,
marker_type: payload.marker_type ?? 'pin',
})
return marker
}

View File

@ -0,0 +1,115 @@
import { Popup } from 'react-map-gl/maplibre'
import { PIN_COLORS } from '~/hooks/useMapMarkers'
import type { PinColorId } from '~/hooks/useMapMarkers'
const MAX_MARKER_NOTES_LENGTH = 1000
const inputClass =
'block w-full appearance-none rounded border border-gray-300 bg-transparent px-2 py-1 text-sm text-gray-900 leading-normal placeholder:text-gray-400 focus:border-gray-500 focus:outline-none'
type CreateMapMarkerPopupProps = {
longitude: number
latitude: number
markerName: string
markerNotes: string
markerColor: PinColorId
onNameChange: (value: string) => void
onNotesChange: (value: string) => void
onColorChange: (value: PinColorId) => void
onSave: () => void
onCancel: () => void
}
export default function CreateMapMarkerPopup({
longitude,
latitude,
markerName,
markerNotes,
markerColor,
onNameChange,
onNotesChange,
onColorChange,
onSave,
onCancel,
}: CreateMapMarkerPopupProps) {
return (
<Popup
longitude={longitude}
latitude={latitude}
anchor="bottom"
onClose={onCancel}
closeOnClick={false}
>
<div className="p-1">
<input
autoFocus
type="text"
placeholder="Name this location"
value={markerName}
onChange={(e) => onNameChange(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') onSave()
if (e.key === 'Escape') onCancel()
}}
className={inputClass}
/>
<textarea
placeholder="Add notes (optional)"
value={markerNotes}
rows={2}
maxLength={MAX_MARKER_NOTES_LENGTH}
onChange={(e) => {
onNotesChange(e.target.value)
e.currentTarget.style.height = 'auto'
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
}}
className={`mt-1 min-h-[64px] resize-none overflow-hidden ${inputClass}`}
/>
<div className="mt-1 text-[11px] text-gray-400">
{markerNotes.length}/{MAX_MARKER_NOTES_LENGTH}
</div>
<div className="mt-1.5 flex gap-1 items-center">
{PIN_COLORS.map((color) => (
<button
key={color.id}
type="button"
onClick={() => onColorChange(color.id)}
title={color.label}
className="rounded-full p-0.5 transition-transform"
style={{
outline:
markerColor === color.id ? `2px solid ${color.hex}` : '2px solid transparent',
outlineOffset: '1px',
}}
>
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: color.hex }} />
</button>
))}
</div>
<div className="mt-1.5 flex gap-1.5 justify-end">
<button
type="button"
onClick={onCancel}
className="text-xs text-gray-500 hover:text-gray-700 px-2 py-1 rounded transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={onSave}
disabled={!markerName.trim()}
className="text-xs bg-[#424420] text-white rounded px-2.5 py-1 hover:bg-[#525530] disabled:opacity-40 transition-colors"
>
Save
</button>
</div>
</div>
</Popup>
)
}

View File

@ -0,0 +1,111 @@
import { useState } from 'react'
import { Popup } from 'react-map-gl/maplibre'
import type { MapMarker, PinColorId } from '~/hooks/useMapMarkers'
import { PIN_COLORS } from '~/hooks/useMapMarkers'
const MAX_MARKER_NOTES_LENGTH = 1000
const inputClass =
'block w-full rounded border border-gray-300 bg-transparent px-2 py-1 text-sm text-gray-900 leading-normal placeholder:text-gray-400 focus:outline-none focus:border-gray-500'
type EditMapMarkerPopupProps = {
marker: MapMarker
onSave: (id: number, name: string, notes: string, color: PinColorId) => void
onCancel: () => void
}
export default function EditMapMarkerPopup({
marker,
onSave,
onCancel,
}: EditMapMarkerPopupProps) {
const [name, setName] = useState(marker.name)
const [notes, setNotes] = useState(marker.notes ?? '')
const [color, setColor] = useState<PinColorId>(marker.color)
const handleSave = () => {
if (!name.trim()) return
onSave(marker.id, name.trim(), notes.trim(), color)
}
return (
<Popup
longitude={marker.longitude}
latitude={marker.latitude}
anchor="bottom"
offset={[0, -36] as [number, number]}
onClose={onCancel}
closeOnClick={false}
>
<div className="p-1">
<input
autoFocus
type="text"
placeholder="Name this location"
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave()
if (e.key === 'Escape') onCancel()
}}
className={inputClass}
/>
<textarea
placeholder="Add notes (optional)"
value={notes}
rows={2}
maxLength={MAX_MARKER_NOTES_LENGTH}
onChange={(e) => {
setNotes(e.target.value)
e.currentTarget.style.height = 'auto'
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
}}
className={`mt-1 min-h-[64px] resize-none overflow-hidden ${inputClass}`}
/>
<div className="mt-1 text-[11px] text-gray-400">
{notes.length}/{MAX_MARKER_NOTES_LENGTH}
</div>
<div className="mt-1.5 flex gap-1 items-center">
{PIN_COLORS.map((pinColor) => (
<button
key={pinColor.id}
type="button"
onClick={() => setColor(pinColor.id)}
title={pinColor.label}
className="rounded-full p-0.5 transition-transform"
style={{
outline: color === pinColor.id ? `2px solid ${pinColor.hex}` : '2px solid transparent',
outlineOffset: '1px',
}}
>
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: pinColor.hex }} />
</button>
))}
</div>
<div className="mt-1.5 flex gap-1.5 justify-end">
<button
type="button"
onClick={onCancel}
className="text-xs text-gray-500 hover:text-gray-700 px-2 py-1 rounded transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={handleSave}
disabled={!name.trim()}
className="text-xs bg-[#424420] text-white rounded px-2.5 py-1 hover:bg-[#525530] disabled:opacity-40 transition-colors"
>
Save
</button>
</div>
</div>
</Popup>
)
}

View File

@ -3,7 +3,6 @@ import Map, {
NavigationControl,
ScaleControl,
Marker,
Popup,
MapProvider,
} from 'react-map-gl/maplibre'
import type { MapRef, MapLayerMouseEvent } from 'react-map-gl/maplibre'
@ -13,34 +12,26 @@ import { Protocol } from 'pmtiles'
import { useEffect, useRef, useState, useCallback } from 'react'
import { useMapMarkers, PIN_COLORS } from '~/hooks/useMapMarkers'
import type { PinColorId } from '~/hooks/useMapMarkers'
import MarkerPin from './MarkerPin'
import MarkerPanel from './MarkerPanel'
import ViewMapMarkerPopup from './ViewMapMarkerPopup'
import MapMarkerFormPopup from './MapMarkerFormPopup'
import ScaleUnitControl from './ScaleUnitControl'
type ScaleUnit = 'imperial' | 'metric'
export default function MapComponent() {
const mapRef = useRef<MapRef>(null)
const { markers, addMarker, deleteMarker } = useMapMarkers()
const { markers, addMarker, updateMarker, deleteMarker } = useMapMarkers()
const [placingMarker, setPlacingMarker] = useState<{ lng: number; lat: number } | null>(null)
const [markerName, setMarkerName] = useState('')
const [markerNotes, setMarkerNotes] = useState('')
const [markerColor, setMarkerColor] = useState<PinColorId>('orange')
const [selectedMarkerId, setSelectedMarkerId] = useState<number | null>(null)
const [editingMarkerId, setEditingMarkerId] = useState<number | null>(null)
const [scaleUnit, setScaleUnit] = useState<ScaleUnit>(
() => (localStorage.getItem('nomad:map-scale-unit') as ScaleUnit) || 'metric'
)
const toggleScaleUnit = useCallback(() => {
setScaleUnit((prev) => {
const next = prev === 'metric' ? 'imperial' : 'metric'
localStorage.setItem('nomad:map-scale-unit', next)
return next
})
}, [])
useEffect(() => {
const protocol = new Protocol()
maplibregl.addProtocol('pmtiles', protocol.tile)
@ -50,30 +41,16 @@ export default function MapComponent() {
}
}, [])
const handleMapClick = useCallback((e: MapLayerMouseEvent) => {
setPlacingMarker({ lng: e.lngLat.lng, lat: e.lngLat.lat })
setMarkerName('')
setMarkerNotes('')
setMarkerColor('orange')
setSelectedMarkerId(null)
const handleScaleUnitChange = useCallback((unit: ScaleUnit) => {
setScaleUnit(unit)
localStorage.setItem('nomad:map-scale-unit', unit)
}, [])
const handleSaveMarker = useCallback(() => {
if (placingMarker && markerName.trim()) {
addMarker(
markerName.trim(),
placingMarker.lng,
placingMarker.lat,
markerColor,
markerNotes.trim() || undefined
)
setPlacingMarker(null)
setMarkerName('')
setMarkerNotes('')
setMarkerColor('orange')
}
}, [placingMarker, markerName, markerNotes, markerColor, addMarker])
const handleMapClick = useCallback((e: MapLayerMouseEvent) => {
setPlacingMarker({ lng: e.lngLat.lng, lat: e.lngLat.lat })
setSelectedMarkerId(null)
setEditingMarkerId(null)
}, [])
const handleFlyTo = useCallback((longitude: number, latitude: number) => {
mapRef.current?.flyTo({ center: [longitude, latitude], zoom: 12, duration: 1500 })
@ -81,13 +58,20 @@ export default function MapComponent() {
const handleDeleteMarker = useCallback(
(id: number) => {
if (selectedMarkerId === id) setSelectedMarkerId(null)
if (selectedMarkerId === id) {
setSelectedMarkerId(null)
}
if (editingMarkerId === id) {
setEditingMarkerId(null)
}
deleteMarker(id)
},
[selectedMarkerId, deleteMarker]
[selectedMarkerId, editingMarkerId, deleteMarker]
)
const selectedMarker = selectedMarkerId ? markers.find((m) => m.id === selectedMarkerId) : null
const selectedMarker = selectedMarkerId ? markers.find((marker) => marker.id === selectedMarkerId) : null
return (
<MapProvider>
@ -110,52 +94,7 @@ export default function MapComponent() {
<NavigationControl style={{ marginTop: '110px', marginRight: '36px' }} />
<FullscreenControl style={{ marginTop: '30px', marginRight: '36px' }} />
<ScaleControl position="bottom-left" maxWidth={150} unit={scaleUnit} />
<div style={{ position: 'absolute', bottom: '30px', left: '10px', zIndex: 2 }}>
<div
style={{
display: 'inline-flex',
borderRadius: '4px',
boxShadow: '0 0 0 2px rgba(0,0,0,0.1)',
overflow: 'hidden',
fontSize: '11px',
fontWeight: 600,
lineHeight: 1,
}}
>
<button
type="button"
onClick={() => {
if (scaleUnit !== 'metric') toggleScaleUnit()
}}
style={{
background: scaleUnit === 'metric' ? '#424420' : 'white',
color: scaleUnit === 'metric' ? 'white' : '#666',
border: 'none',
padding: '4px 8px',
cursor: 'pointer',
}}
>
Metric
</button>
<button
type="button"
onClick={() => {
if (scaleUnit !== 'imperial') toggleScaleUnit()
}}
style={{
background: scaleUnit === 'imperial' ? '#424420' : 'white',
color: scaleUnit === 'imperial' ? 'white' : '#666',
border: 'none',
padding: '4px 8px',
cursor: 'pointer',
}}
>
Imperial
</button>
</div>
</div>
<ScaleUnitControl scaleUnit={scaleUnit} onChange={handleScaleUnitChange} />
{markers.map((marker) => (
<Marker
@ -167,103 +106,62 @@ export default function MapComponent() {
e.originalEvent.stopPropagation()
setSelectedMarkerId(marker.id === selectedMarkerId ? null : marker.id)
setPlacingMarker(null)
setEditingMarkerId(null)
}}
>
<MarkerPin
color={PIN_COLORS.find((c) => c.id === marker.color)?.hex}
color={PIN_COLORS.find((color) => color.id === marker.color)?.hex}
active={marker.id === selectedMarkerId}
/>
</Marker>
))}
{selectedMarker && (
<Popup
longitude={selectedMarker.longitude}
latitude={selectedMarker.latitude}
anchor="bottom"
offset={[0, -36] as [number, number]}
onClose={() => setSelectedMarkerId(null)}
closeOnClick={false}
>
<div className="text-sm font-medium">{selectedMarker.name}</div>
{selectedMarker.notes && (
<div className="mt-1 text-xs text-gray-500 whitespace-pre-wrap">
{selectedMarker.notes}
</div>
)}
</Popup>
)}
{placingMarker && (
<Popup
<MapMarkerFormPopup
longitude={placingMarker.lng}
latitude={placingMarker.lat}
anchor="bottom"
onClose={() => setPlacingMarker(null)}
closeOnClick={false}
>
<div className="p-1">
<input
autoFocus
type="text"
placeholder="Name this location"
value={markerName}
onChange={(e) => setMarkerName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSaveMarker()
if (e.key === 'Escape') setPlacingMarker(null)
}}
className="block w-full rounded border border-gray-300 px-2 py-1 text-sm placeholder:text-gray-400 focus:outline-none focus:border-gray-500"
/>
onSave={async ({ name, notes, color }) => {
await addMarker(name, placingMarker.lng, placingMarker.lat, color, notes || undefined)
setPlacingMarker(null)
}}
onCancel={() => setPlacingMarker(null)}
/>
)}
<textarea
placeholder="Add notes (optional)"
value={markerNotes}
onChange={(e) => setMarkerNotes(e.target.value)}
rows={3}
className="mt-1 block w-full resize-none rounded border border-gray-300 px-2 py-1 text-sm placeholder:text-gray-400 focus:outline-none focus:border-gray-500"
/>
{selectedMarker && editingMarkerId !== selectedMarker.id && (
<ViewMapMarkerPopup
marker={selectedMarker}
onClose={() => setSelectedMarkerId(null)}
onEdit={() => setEditingMarkerId(selectedMarker.id)}
/>
)}
<div className="mt-1.5 flex gap-1 items-center">
{PIN_COLORS.map((c) => (
<button
key={c.id}
type="button"
onClick={() => setMarkerColor(c.id)}
title={c.label}
className="rounded-full p-0.5 transition-transform"
style={{
outline:
markerColor === c.id ? `2px solid ${c.hex}` : '2px solid transparent',
outlineOffset: '1px',
}}
>
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: c.hex }} />
</button>
))}
</div>
{selectedMarker && editingMarkerId !== selectedMarker.id && (
<ViewMapMarkerPopup
marker={selectedMarker}
onClose={() => setSelectedMarkerId(null)}
onEdit={() => setEditingMarkerId(selectedMarker.id)}
/>
)}
<div className="mt-1.5 flex gap-1.5 justify-end">
<button
type="button"
onClick={() => setPlacingMarker(null)}
className="text-xs text-gray-500 hover:text-gray-700 px-2 py-1 rounded transition-colors"
>
Cancel
</button>
{selectedMarker && editingMarkerId === selectedMarker.id && (
<MapMarkerFormPopup
longitude={selectedMarker.longitude}
latitude={selectedMarker.latitude}
initialMarker={selectedMarker}
onSave={async ({ id, name, notes, color }) => {
if (!id) return
<button
type="button"
onClick={handleSaveMarker}
disabled={!markerName.trim()}
className="text-xs bg-[#424420] text-white rounded px-2.5 py-1 hover:bg-[#525530] disabled:opacity-40 transition-colors"
>
Save
</button>
</div>
</div>
</Popup>
await updateMarker(id, {
name,
notes: notes || null,
color,
})
setEditingMarkerId(null)
}}
onCancel={() => setEditingMarkerId(null)}
/>
)}
</Map>

View File

@ -0,0 +1,127 @@
import { useState } from 'react'
import { Popup } from 'react-map-gl/maplibre'
import { PIN_COLORS } from '~/hooks/useMapMarkers'
import type { MapMarker, PinColorId } from '~/hooks/useMapMarkers'
const MAX_MARKER_NOTES_LENGTH = 500
const inputClass =
'block w-full rounded border border-gray-300 bg-transparent px-2 py-1 text-sm text-gray-900 leading-normal placeholder:text-gray-400 focus:outline-none focus:border-gray-500'
type MapMarkerFormPopupProps = {
longitude: number
latitude: number
initialMarker?: MapMarker
onSave: (values: {
id?: number
name: string
notes: string
color: PinColorId
}) => void
onCancel: () => void
}
export default function MapMarkerFormPopup({
longitude,
latitude,
initialMarker,
onSave,
onCancel,
}: MapMarkerFormPopupProps) {
const [name, setName] = useState(initialMarker?.name ?? '')
const [notes, setNotes] = useState(initialMarker?.notes ?? '')
const [color, setColor] = useState<PinColorId>(initialMarker?.color ?? 'orange')
const handleSave = () => {
if (!name.trim()) return
onSave({
id: initialMarker?.id,
name: name.trim(),
notes: notes.trim(),
color,
})
}
return (
<Popup
longitude={longitude}
latitude={latitude}
anchor="bottom"
offset={[0, -36] as [number, number]}
onClose={onCancel}
closeOnClick={false}
>
<div className="p-1">
<input
autoFocus
type="text"
placeholder="Name this location"
value={name}
onChange={(e) => setName(e.target.value)}
onKeyDown={(e) => {
if (e.key === 'Enter') handleSave()
if (e.key === 'Escape') onCancel()
}}
className={inputClass}
/>
<textarea
placeholder="Add notes (optional)"
value={notes}
rows={2}
maxLength={MAX_MARKER_NOTES_LENGTH}
onChange={(e) => {
setNotes(e.target.value)
e.currentTarget.style.height = 'auto'
e.currentTarget.style.height = `${e.currentTarget.scrollHeight}px`
}}
className={`mt-1 min-h-[64px] resize-none overflow-hidden ${inputClass}`}
/>
<div className="mt-1 text-[11px] text-gray-400">
{notes.length}/{MAX_MARKER_NOTES_LENGTH}
</div>
<div className="mt-1.5 flex gap-1 items-center">
{PIN_COLORS.map((pinColor) => (
<button
key={pinColor.id}
type="button"
onClick={() => setColor(pinColor.id)}
title={pinColor.label}
className="rounded-full p-0.5 transition-transform"
style={{
outline:
color === pinColor.id ? `2px solid ${pinColor.hex}` : '2px solid transparent',
outlineOffset: '1px',
}}
>
<div className="w-4 h-4 rounded-full" style={{ backgroundColor: pinColor.hex }} />
</button>
))}
</div>
<div className="mt-1.5 flex gap-1.5 justify-end">
<button
type="button"
onClick={onCancel}
className="text-xs text-gray-500 hover:text-gray-700 px-2 py-1 rounded transition-colors"
>
Cancel
</button>
<button
type="button"
onClick={handleSave}
disabled={!name.trim()}
className="text-xs bg-[#424420] text-white rounded px-2.5 py-1 hover:bg-[#525530] disabled:opacity-40 transition-colors"
>
Save
</button>
</div>
</div>
</Popup>
)
}

View File

@ -0,0 +1,56 @@
type ScaleUnit = 'imperial' | 'metric'
type ScaleUnitControlProps = {
scaleUnit: ScaleUnit
onChange: (unit: ScaleUnit) => void
}
export default function ScaleUnitControl({ scaleUnit, onChange }: ScaleUnitControlProps) {
return (
<div style={{ position: 'absolute', bottom: '30px', left: '10px', zIndex: 2 }}>
<div
style={{
display: 'inline-flex',
borderRadius: '4px',
boxShadow: '0 0 0 2px rgba(0,0,0,0.1)',
overflow: 'hidden',
fontSize: '11px',
fontWeight: 600,
lineHeight: 1,
}}
>
<button
type="button"
onClick={() => {
if (scaleUnit !== 'metric') onChange('metric')
}}
style={{
background: scaleUnit === 'metric' ? '#424420' : 'white',
color: scaleUnit === 'metric' ? 'white' : '#666',
border: 'none',
padding: '4px 8px',
cursor: 'pointer',
}}
>
Metric
</button>
<button
type="button"
onClick={() => {
if (scaleUnit !== 'imperial') onChange('imperial')
}}
style={{
background: scaleUnit === 'imperial' ? '#424420' : 'white',
color: scaleUnit === 'imperial' ? 'white' : '#666',
border: 'none',
padding: '4px 8px',
cursor: 'pointer',
}}
>
Imperial
</button>
</div>
</div>
)
}

View File

@ -0,0 +1,48 @@
import { Popup } from 'react-map-gl/maplibre'
import ReactMarkdown from 'react-markdown'
import remarkGfm from 'remark-gfm'
import type { MapMarker } from '~/hooks/useMapMarkers'
type ViewMapMarkerPopupProps = {
marker: MapMarker
onClose: () => void
onEdit: () => void
}
export default function ViewMapMarkerPopup({
marker,
onClose,
onEdit,
}: ViewMapMarkerPopupProps) {
return (
<Popup
longitude={marker.longitude}
latitude={marker.latitude}
anchor="bottom"
offset={[0, -36] as [number, number]}
onClose={onClose}
closeOnClick={false}
>
<div className="text-sm font-medium">{marker.name}</div>
{marker.notes && (
<div className="mt-1 text-xs text-gray-500">
<ReactMarkdown remarkPlugins={[remarkGfm]}>
{marker.notes}
</ReactMarkdown>
</div>
)}
<div className="mt-2 flex justify-end">
<button
type="button"
onClick={onEdit}
className="rounded bg-[#424420] px-2.5 py-1 text-xs text-white hover:bg-[#525530]"
>
Edit
</button>
</div>
</Popup>
)
}

View File

@ -126,13 +126,15 @@ body {
color: #f7eedc;
}
[data-theme="dark"] .maplibregl-popup-content input {
[data-theme="dark"] .maplibregl-popup-content input,
[data-theme="dark"] .maplibregl-popup-content textarea {
background: #353420;
color: #f7eedc;
border-color: #424420;
}
[data-theme="dark"] .maplibregl-popup-content input::placeholder {
[data-theme="dark"] .maplibregl-popup-content input::placeholder,
[data-theme="dark"] .maplibregl-popup-content textarea::placeholder {
color: #8f8f82;
}
@ -155,4 +157,4 @@ body {
[data-theme="dark"] .maplibregl-popup-close-button:hover {
color: #f7eedc;
background: #353420;
}
}

View File

@ -4383,6 +4383,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4399,6 +4400,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4415,6 +4417,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@ -4431,6 +4434,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4447,6 +4451,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4463,6 +4468,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4479,6 +4485,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4495,6 +4502,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4511,6 +4519,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [
@ -4527,6 +4536,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND MIT",
"optional": true,
"os": [