UX ifxes
This commit is contained in:
@@ -715,5 +715,10 @@
|
|||||||
"map_description": "Description",
|
"map_description": "Description",
|
||||||
"map_color": "Color",
|
"map_color": "Color",
|
||||||
"map_pen_hint_points": "{count} point(s)",
|
"map_pen_hint_points": "{count} point(s)",
|
||||||
"map_pen_hint_close": "click near first point to close, or press Esc to cancel"
|
"map_pen_hint_close": "click near first point to close, or press Esc to cancel",
|
||||||
|
"map_show_pin_labels": "Show pin labels",
|
||||||
|
"map_show_shape_labels": "Show shape labels",
|
||||||
|
"map_address": "Address",
|
||||||
|
"map_address_placeholder": "e.g., Tallinn, Estonia",
|
||||||
|
"map_change_address": "Change address"
|
||||||
}
|
}
|
||||||
@@ -725,5 +725,10 @@
|
|||||||
"map_description": "Kirjeldus",
|
"map_description": "Kirjeldus",
|
||||||
"map_color": "Värv",
|
"map_color": "Värv",
|
||||||
"map_pen_hint_points": "{count} punkt(i)",
|
"map_pen_hint_points": "{count} punkt(i)",
|
||||||
"map_pen_hint_close": "kliki esimese punkti lähedal sulgemiseks või vajuta Esc tühistamiseks"
|
"map_pen_hint_close": "kliki esimese punkti lähedal sulgemiseks või vajuta Esc tühistamiseks",
|
||||||
|
"map_show_pin_labels": "Näita markerite silte",
|
||||||
|
"map_show_shape_labels": "Näita kujundite silte",
|
||||||
|
"map_address": "Aadress",
|
||||||
|
"map_address_placeholder": "nt. Tallinn, Eesti",
|
||||||
|
"map_change_address": "Muuda aadressi"
|
||||||
}
|
}
|
||||||
@@ -86,6 +86,12 @@
|
|||||||
// Layer modal
|
// Layer modal
|
||||||
let showLayerModal = $state(false);
|
let showLayerModal = $state(false);
|
||||||
let layerName = $state("");
|
let layerName = $state("");
|
||||||
|
let layerAddress = $state("");
|
||||||
|
|
||||||
|
// Address change modal
|
||||||
|
let showAddressModal = $state(false);
|
||||||
|
let addressLayerId = $state<string | null>(null);
|
||||||
|
let addressValue = $state("");
|
||||||
|
|
||||||
// Layer context menu
|
// Layer context menu
|
||||||
let layerCtx = $state<{
|
let layerCtx = $state<{
|
||||||
@@ -98,6 +104,15 @@
|
|||||||
let renameLayerId = $state<string | null>(null);
|
let renameLayerId = $state<string | null>(null);
|
||||||
let renameValue = $state("");
|
let renameValue = $state("");
|
||||||
|
|
||||||
|
// Object context menu (right-click on pin/shape)
|
||||||
|
let objCtx = $state<{
|
||||||
|
id: string;
|
||||||
|
type: "pin" | "shape";
|
||||||
|
label: string;
|
||||||
|
x: number;
|
||||||
|
y: number;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
// Image modal
|
// Image modal
|
||||||
let showImageModal = $state(false);
|
let showImageModal = $state(false);
|
||||||
let imageUrlInput = $state("");
|
let imageUrlInput = $state("");
|
||||||
@@ -114,7 +129,18 @@
|
|||||||
let rectStart: any = null;
|
let rectStart: any = null;
|
||||||
let rectPreview: any = null;
|
let rectPreview: any = null;
|
||||||
|
|
||||||
// Shape drag state
|
// Label visibility
|
||||||
|
let showPinLabels = $state(false);
|
||||||
|
let showShapeLabels = $state(false);
|
||||||
|
let leafletLabels: any[] = [];
|
||||||
|
|
||||||
|
// Object panel drag-reorder state
|
||||||
|
let dragObjId = $state<string | null>(null);
|
||||||
|
let dragObjType = $state<"pin" | "shape" | null>(null);
|
||||||
|
let dropTargetId = $state<string | null>(null);
|
||||||
|
let dropPosition = $state<"before" | "after" | null>(null);
|
||||||
|
|
||||||
|
// Shape drag state (map canvas)
|
||||||
let draggingShapeId: string | null = null;
|
let draggingShapeId: string | null = null;
|
||||||
let dragStartLatLng: any = null;
|
let dragStartLatLng: any = null;
|
||||||
|
|
||||||
@@ -520,6 +546,8 @@
|
|||||||
leafletShapes.clear();
|
leafletShapes.clear();
|
||||||
leafletBoundsRects.forEach((r) => r.remove());
|
leafletBoundsRects.forEach((r) => r.remove());
|
||||||
leafletBoundsRects.clear();
|
leafletBoundsRects.clear();
|
||||||
|
leafletLabels.forEach((l) => l.remove());
|
||||||
|
leafletLabels = [];
|
||||||
|
|
||||||
// Pins
|
// Pins
|
||||||
(layer.pins ?? []).forEach((pin) => {
|
(layer.pins ?? []).forEach((pin) => {
|
||||||
@@ -566,10 +594,34 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
marker.on("click", () => selectObject(pin.id, "pin"));
|
marker.on("click", () => selectObject(pin.id, "pin"));
|
||||||
|
marker.on("contextmenu", (e: any) => {
|
||||||
|
L.DomEvent.stopPropagation(e);
|
||||||
|
L.DomEvent.preventDefault(e);
|
||||||
|
objCtx = {
|
||||||
|
id: pin.id,
|
||||||
|
type: "pin",
|
||||||
|
label: pin.label,
|
||||||
|
x: e.originalEvent.clientX,
|
||||||
|
y: e.originalEvent.clientY,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
leafletMarkers.set(pin.id, marker);
|
leafletMarkers.set(pin.id, marker);
|
||||||
|
|
||||||
|
if (showPinLabels && pin.label) {
|
||||||
|
const lbl = L.tooltip({
|
||||||
|
permanent: true,
|
||||||
|
direction: "bottom",
|
||||||
|
offset: [0, 0],
|
||||||
|
className: "map-obj-label",
|
||||||
|
})
|
||||||
|
.setLatLng([pin.lat, pin.lng])
|
||||||
|
.setContent(pin.label)
|
||||||
|
.addTo(map);
|
||||||
|
leafletLabels.push(lbl);
|
||||||
|
}
|
||||||
|
|
||||||
// Bounds rect for pin
|
// Bounds rect for pin
|
||||||
if (
|
if (
|
||||||
pin.bounds_north != null &&
|
pin.bounds_north != null &&
|
||||||
@@ -623,9 +675,33 @@
|
|||||||
dragStartLatLng = e.latlng;
|
dragStartLatLng = e.latlng;
|
||||||
map.dragging.disable();
|
map.dragging.disable();
|
||||||
});
|
});
|
||||||
|
poly.on("contextmenu", (e: any) => {
|
||||||
|
L.DomEvent.stopPropagation(e);
|
||||||
|
L.DomEvent.preventDefault(e);
|
||||||
|
objCtx = {
|
||||||
|
id: shape.id,
|
||||||
|
type: "shape",
|
||||||
|
label: shape.label || shape.shape_type,
|
||||||
|
x: e.originalEvent.clientX,
|
||||||
|
y: e.originalEvent.clientY,
|
||||||
|
};
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
leafletShapes.set(shape.id, poly);
|
leafletShapes.set(shape.id, poly);
|
||||||
|
|
||||||
|
if (showShapeLabels && shape.label) {
|
||||||
|
const center = poly.getBounds().getCenter();
|
||||||
|
const lbl = L.tooltip({
|
||||||
|
permanent: true,
|
||||||
|
direction: "center",
|
||||||
|
className: "map-obj-label",
|
||||||
|
})
|
||||||
|
.setLatLng(center)
|
||||||
|
.setContent(shape.label)
|
||||||
|
.addTo(map);
|
||||||
|
leafletLabels.push(lbl);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -832,6 +908,11 @@
|
|||||||
layers = [...layers, withData];
|
layers = [...layers, withData];
|
||||||
activeLayerIdx = layers.length - 1;
|
activeLayerIdx = layers.length - 1;
|
||||||
showLayerOnMap(withData);
|
showLayerOnMap(withData);
|
||||||
|
const addr = layerAddress.trim() || venueAddress;
|
||||||
|
if (type === "osm" && addr) {
|
||||||
|
geocodeAddress(addr);
|
||||||
|
}
|
||||||
|
layerAddress = "";
|
||||||
setupRealtime();
|
setupRealtime();
|
||||||
} catch {
|
} catch {
|
||||||
toasts.error("Failed to create layer");
|
toasts.error("Failed to create layer");
|
||||||
@@ -892,6 +973,180 @@
|
|||||||
renameValue = "";
|
renameValue = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function startChangeAddress() {
|
||||||
|
if (!layerCtx) return;
|
||||||
|
addressLayerId = layerCtx.id;
|
||||||
|
addressValue = "";
|
||||||
|
showAddressModal = true;
|
||||||
|
closeLayerContextMenu();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleChangeAddress() {
|
||||||
|
const addr = addressValue.trim() || venueAddress;
|
||||||
|
if (!addressLayerId || !addr) return;
|
||||||
|
showAddressModal = false;
|
||||||
|
|
||||||
|
// Switch to the target layer
|
||||||
|
const idx = layers.findIndex((l) => l.id === addressLayerId);
|
||||||
|
if (idx >= 0 && idx !== activeLayerIdx) {
|
||||||
|
activeLayerIdx = idx;
|
||||||
|
showLayerOnMap(layers[idx]);
|
||||||
|
}
|
||||||
|
|
||||||
|
await geocodeAddress(addr);
|
||||||
|
addressLayerId = null;
|
||||||
|
addressValue = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeObjCtx() {
|
||||||
|
objCtx = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ctxEditObject() {
|
||||||
|
if (!objCtx) return;
|
||||||
|
const { id, type } = objCtx;
|
||||||
|
closeObjCtx();
|
||||||
|
if (type === "pin") {
|
||||||
|
const pin = activeLayer?.pins.find((p) => p.id === id);
|
||||||
|
if (pin) openEditPin(pin);
|
||||||
|
} else {
|
||||||
|
const shape = (activeLayer?.shapes ?? []).find((s) => s.id === id);
|
||||||
|
if (shape) openEditShape(shape);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ctxDeleteObject() {
|
||||||
|
if (!objCtx) return;
|
||||||
|
const { id, type } = objCtx;
|
||||||
|
closeObjCtx();
|
||||||
|
if (type === "pin") {
|
||||||
|
handleDeletePin(id);
|
||||||
|
} else {
|
||||||
|
handleDeleteShape(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleObjDragOver(e: DragEvent, targetId: string) {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!dragObjId || targetId === dragObjId) {
|
||||||
|
dropTargetId = null;
|
||||||
|
dropPosition = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rect = (e.currentTarget as HTMLElement).getBoundingClientRect();
|
||||||
|
const midY = rect.top + rect.height / 2;
|
||||||
|
dropTargetId = targetId;
|
||||||
|
dropPosition = e.clientY < midY ? "before" : "after";
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleObjDrop() {
|
||||||
|
if (
|
||||||
|
!dragObjId ||
|
||||||
|
!dragObjType ||
|
||||||
|
!dropTargetId ||
|
||||||
|
!dropPosition ||
|
||||||
|
!activeLayer
|
||||||
|
) {
|
||||||
|
resetObjDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build a combined ordered list: pins first, then shapes
|
||||||
|
type ObjItem = { id: string; type: "pin" | "shape" };
|
||||||
|
const items: ObjItem[] = [
|
||||||
|
...activeLayer.pins.map((p) => ({
|
||||||
|
id: p.id,
|
||||||
|
type: "pin" as const,
|
||||||
|
})),
|
||||||
|
...(activeLayer.shapes ?? []).map((s) => ({
|
||||||
|
id: s.id,
|
||||||
|
type: "shape" as const,
|
||||||
|
})),
|
||||||
|
];
|
||||||
|
|
||||||
|
const fromIdx = items.findIndex((i) => i.id === dragObjId);
|
||||||
|
let toIdx = items.findIndex((i) => i.id === dropTargetId);
|
||||||
|
if (fromIdx < 0 || toIdx < 0) {
|
||||||
|
resetObjDrag();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove dragged item and insert at target position
|
||||||
|
const [moved] = items.splice(fromIdx, 1);
|
||||||
|
toIdx = items.findIndex((i) => i.id === dropTargetId);
|
||||||
|
if (toIdx < 0) toIdx = items.length;
|
||||||
|
if (dropPosition === "after") toIdx++;
|
||||||
|
items.splice(toIdx, 0, moved);
|
||||||
|
|
||||||
|
// Split back into pins and shapes with new sort_order
|
||||||
|
const newPins = items.filter((i) => i.type === "pin");
|
||||||
|
const newShapes = items.filter((i) => i.type === "shape");
|
||||||
|
|
||||||
|
const oldPins = activeLayer.pins;
|
||||||
|
const oldShapes = activeLayer.shapes ?? [];
|
||||||
|
|
||||||
|
layers = layers.map((l, i) =>
|
||||||
|
i === activeLayerIdx
|
||||||
|
? {
|
||||||
|
...l,
|
||||||
|
pins: newPins
|
||||||
|
.map((np, idx) => {
|
||||||
|
const pin = l.pins.find((p) => p.id === np.id);
|
||||||
|
return pin
|
||||||
|
? { ...pin, sort_order: idx }
|
||||||
|
: l.pins[idx];
|
||||||
|
})
|
||||||
|
.filter(Boolean),
|
||||||
|
shapes: newShapes
|
||||||
|
.map((ns, idx) => {
|
||||||
|
const shape = (l.shapes ?? []).find(
|
||||||
|
(s) => s.id === ns.id,
|
||||||
|
);
|
||||||
|
return shape
|
||||||
|
? { ...shape, sort_order: idx }
|
||||||
|
: (l.shapes ?? [])[idx];
|
||||||
|
})
|
||||||
|
.filter(Boolean),
|
||||||
|
}
|
||||||
|
: l,
|
||||||
|
);
|
||||||
|
syncAllObjects(layers[activeLayerIdx]);
|
||||||
|
|
||||||
|
// Only persist sort_order for objects whose order actually changed
|
||||||
|
const changedPins = newPins.filter((np, idx) => {
|
||||||
|
const old = oldPins.findIndex((p) => p.id === np.id);
|
||||||
|
return old !== idx;
|
||||||
|
});
|
||||||
|
const changedShapes = newShapes.filter((ns, idx) => {
|
||||||
|
const old = oldShapes.findIndex((s) => s.id === ns.id);
|
||||||
|
return old !== idx;
|
||||||
|
});
|
||||||
|
if (changedPins.length > 0 || changedShapes.length > 0) {
|
||||||
|
const updates = [
|
||||||
|
...changedPins.map((np, _i) => {
|
||||||
|
const idx = newPins.indexOf(np);
|
||||||
|
return updateMapPin(supabase, np.id, { sort_order: idx });
|
||||||
|
}),
|
||||||
|
...changedShapes.map((ns, _i) => {
|
||||||
|
const idx = newShapes.indexOf(ns);
|
||||||
|
return updateMapShape(supabase, ns.id, { sort_order: idx });
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
Promise.all(updates).catch(() =>
|
||||||
|
toasts.error("Failed to reorder objects"),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
resetObjDrag();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetObjDrag() {
|
||||||
|
dragObjId = null;
|
||||||
|
dragObjType = null;
|
||||||
|
dropTargetId = null;
|
||||||
|
dropPosition = null;
|
||||||
|
}
|
||||||
|
|
||||||
function ctxDeleteLayer() {
|
function ctxDeleteLayer() {
|
||||||
if (!layerCtx) return;
|
if (!layerCtx) return;
|
||||||
const id = layerCtx.id;
|
const id = layerCtx.id;
|
||||||
@@ -1011,11 +1266,11 @@
|
|||||||
let layerId = payload.new?.layer_id ?? payload.old?.layer_id;
|
let layerId = payload.new?.layer_id ?? payload.old?.layer_id;
|
||||||
|
|
||||||
if (payload.event === "DELETE") {
|
if (payload.event === "DELETE") {
|
||||||
if (!layerId)
|
const resolvedLayerId =
|
||||||
layerId = layers.find((l) =>
|
layerId ??
|
||||||
l.pins.some((p) => p.id === id),
|
layers.find((l) => l.pins.some((p) => p.id === id))?.id;
|
||||||
)?.id;
|
if (!resolvedLayerId) return;
|
||||||
if (!layerId) return;
|
layerId = resolvedLayerId;
|
||||||
layers = layers.map((l) =>
|
layers = layers.map((l) =>
|
||||||
l.id === layerId
|
l.id === layerId
|
||||||
? { ...l, pins: l.pins.filter((p) => p.id !== id) }
|
? { ...l, pins: l.pins.filter((p) => p.id !== id) }
|
||||||
@@ -1065,13 +1320,14 @@
|
|||||||
let layerId = payload.new?.layer_id ?? payload.old?.layer_id;
|
let layerId = payload.new?.layer_id ?? payload.old?.layer_id;
|
||||||
|
|
||||||
if (payload.event === "DELETE") {
|
if (payload.event === "DELETE") {
|
||||||
if (!layerId)
|
const resolvedLayerId =
|
||||||
layerId = layers.find((l) =>
|
layerId ??
|
||||||
(l.shapes ?? []).some((s) => s.id === id),
|
layers.find((l) => (l.shapes ?? []).some((s) => s.id === id))
|
||||||
)?.id;
|
?.id;
|
||||||
if (!layerId) return;
|
if (!resolvedLayerId) return;
|
||||||
|
layerId = resolvedLayerId;
|
||||||
layers = layers.map((l) =>
|
layers = layers.map((l) =>
|
||||||
l.id === layerId
|
l.id === resolvedLayerId
|
||||||
? {
|
? {
|
||||||
...l,
|
...l,
|
||||||
shapes: (l.shapes ?? []).filter((s) => s.id !== id),
|
shapes: (l.shapes ?? []).filter((s) => s.id !== id),
|
||||||
@@ -1407,15 +1663,69 @@
|
|||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div
|
||||||
|
class="flex items-center gap-1 px-3 py-1.5 border-b border-light/10"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="label-toggle {showPinLabels ? 'active' : ''}"
|
||||||
|
onclick={() => {
|
||||||
|
showPinLabels = !showPinLabels;
|
||||||
|
if (activeLayer) syncAllObjects(activeLayer);
|
||||||
|
}}
|
||||||
|
title={m.map_show_pin_labels()}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded"
|
||||||
|
style={icon("", 13)}>location_on</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded"
|
||||||
|
style={icon("", 11)}>label</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="label-toggle {showShapeLabels ? 'active' : ''}"
|
||||||
|
onclick={() => {
|
||||||
|
showShapeLabels = !showShapeLabels;
|
||||||
|
if (activeLayer) syncAllObjects(activeLayer);
|
||||||
|
}}
|
||||||
|
title={m.map_show_shape_labels()}
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded"
|
||||||
|
style={icon("", 13)}>crop_landscape</span
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="material-symbols-rounded"
|
||||||
|
style={icon("", 11)}>label</span
|
||||||
|
>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div class="flex-1 overflow-y-auto">
|
<div class="flex-1 overflow-y-auto">
|
||||||
{#if activeLayer}
|
{#if activeLayer}
|
||||||
<!-- Pins -->
|
<!-- Pins -->
|
||||||
{#each activeLayer.pins as pin (pin.id)}
|
{#each [...activeLayer.pins].reverse() as pin (pin.id)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="obj-row {selectedObjectId === pin.id
|
class="obj-row {selectedObjectId === pin.id
|
||||||
? 'selected'
|
? 'selected'
|
||||||
|
: ''} {dropTargetId === pin.id &&
|
||||||
|
dropPosition === 'before'
|
||||||
|
? 'drop-before'
|
||||||
|
: ''} {dropTargetId === pin.id &&
|
||||||
|
dropPosition === 'after'
|
||||||
|
? 'drop-after'
|
||||||
: ''}"
|
: ''}"
|
||||||
|
draggable={isEditor}
|
||||||
|
ondragstart={() => {
|
||||||
|
dragObjId = pin.id;
|
||||||
|
dragObjType = "pin";
|
||||||
|
}}
|
||||||
|
ondragover={(e) => handleObjDragOver(e, pin.id)}
|
||||||
|
ondragend={resetObjDrag}
|
||||||
|
ondrop={handleObjDrop}
|
||||||
onclick={() => selectObject(pin.id, "pin")}
|
onclick={() => selectObject(pin.id, "pin")}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -1462,12 +1772,27 @@
|
|||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
<!-- Shapes -->
|
<!-- Shapes -->
|
||||||
{#each activeLayer.shapes ?? [] as shape (shape.id)}
|
{#each [...(activeLayer.shapes ?? [])].reverse() as shape (shape.id)}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="obj-row {selectedObjectId === shape.id
|
class="obj-row {selectedObjectId === shape.id
|
||||||
? 'selected'
|
? 'selected'
|
||||||
|
: ''} {dropTargetId === shape.id &&
|
||||||
|
dropPosition === 'before'
|
||||||
|
? 'drop-before'
|
||||||
|
: ''} {dropTargetId === shape.id &&
|
||||||
|
dropPosition === 'after'
|
||||||
|
? 'drop-after'
|
||||||
: ''}"
|
: ''}"
|
||||||
|
draggable={isEditor}
|
||||||
|
ondragstart={() => {
|
||||||
|
dragObjId = shape.id;
|
||||||
|
dragObjType = "shape";
|
||||||
|
}}
|
||||||
|
ondragover={(e) =>
|
||||||
|
handleObjDragOver(e, shape.id)}
|
||||||
|
ondragend={resetObjDrag}
|
||||||
|
ondrop={handleObjDrop}
|
||||||
onclick={() => selectObject(shape.id, "shape")}
|
onclick={() => selectObject(shape.id, "shape")}
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
@@ -1731,6 +2056,12 @@
|
|||||||
bind:value={layerName}
|
bind:value={layerName}
|
||||||
placeholder="e.g., Floor 1, Parking Area"
|
placeholder="e.g., Floor 1, Parking Area"
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
variant="compact"
|
||||||
|
label={m.map_address()}
|
||||||
|
bind:value={layerAddress}
|
||||||
|
placeholder={venueAddress || m.map_address_placeholder()}
|
||||||
|
/>
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
@@ -1795,6 +2126,12 @@
|
|||||||
>
|
>
|
||||||
Rename
|
Rename
|
||||||
</button>
|
</button>
|
||||||
|
<button type="button" class="ctx-item" onclick={startChangeAddress}>
|
||||||
|
<span class="material-symbols-rounded" style={icon("", 16)}
|
||||||
|
>location_on</span
|
||||||
|
>
|
||||||
|
{m.map_change_address()}
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
class="ctx-item danger"
|
class="ctx-item danger"
|
||||||
@@ -1809,6 +2146,47 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
<!-- Object context menu (right-click on pin/shape) -->
|
||||||
|
{#if objCtx}
|
||||||
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||||
|
<div
|
||||||
|
class="fixed inset-0 z-50"
|
||||||
|
onclick={closeObjCtx}
|
||||||
|
oncontextmenu={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
closeObjCtx();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="layer-ctx-menu"
|
||||||
|
style="left:{objCtx.x}px;top:{objCtx.y}px"
|
||||||
|
onclick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="px-2.5 py-1.5 text-[10px] text-light/30 uppercase tracking-wider truncate max-w-[160px]"
|
||||||
|
>
|
||||||
|
{objCtx.label}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="ctx-item" onclick={ctxEditObject}>
|
||||||
|
<span class="material-symbols-rounded" style={icon("", 16)}
|
||||||
|
>edit</span
|
||||||
|
>
|
||||||
|
{objCtx.type === "pin" ? m.map_edit_pin() : m.map_edit_shape()}
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="ctx-item danger"
|
||||||
|
onclick={ctxDeleteObject}
|
||||||
|
>
|
||||||
|
<span class="material-symbols-rounded" style={icon("", 16)}
|
||||||
|
>delete</span
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<!-- Rename Layer Modal -->
|
<!-- Rename Layer Modal -->
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={showRenameModal}
|
isOpen={showRenameModal}
|
||||||
@@ -1843,9 +2221,47 @@
|
|||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
<!-- Change Address Modal -->
|
||||||
|
<Modal
|
||||||
|
isOpen={showAddressModal}
|
||||||
|
onClose={() => (showAddressModal = false)}
|
||||||
|
title={m.map_change_address()}
|
||||||
|
>
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<Input
|
||||||
|
variant="compact"
|
||||||
|
label={m.map_address()}
|
||||||
|
bind:value={addressValue}
|
||||||
|
placeholder={venueAddress || m.map_address_placeholder()}
|
||||||
|
onkeydown={(e) => {
|
||||||
|
if (e.key === "Enter") handleChangeAddress();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="flex items-center justify-end gap-3 pt-2 border-t border-light/5"
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="px-4 py-2 text-body-sm text-light/60 hover:text-white transition-colors"
|
||||||
|
onclick={() => (showAddressModal = false)}>Cancel</button
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={!addressValue.trim() && !venueAddress}
|
||||||
|
class="px-4 py-2 bg-primary text-background rounded-xl font-body text-body-sm hover:bg-primary-hover transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
||||||
|
onclick={handleChangeAddress}>Go</button
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
onkeydown={(e) => {
|
onkeydown={(e) => {
|
||||||
if (e.key === "Escape") {
|
if (e.key === "Escape") {
|
||||||
|
if (objCtx) {
|
||||||
|
closeObjCtx();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (layerCtx) {
|
if (layerCtx) {
|
||||||
closeLayerContextMenu();
|
closeLayerContextMenu();
|
||||||
return;
|
return;
|
||||||
@@ -1874,6 +2290,20 @@
|
|||||||
background: #0a1628 !important;
|
background: #0a1628 !important;
|
||||||
z-index: 0 !important;
|
z-index: 0 !important;
|
||||||
}
|
}
|
||||||
|
:global(.map-obj-label) {
|
||||||
|
background: rgba(0, 0, 0, 0.75) !important;
|
||||||
|
border: none !important;
|
||||||
|
border-radius: 4px !important;
|
||||||
|
color: #fff !important;
|
||||||
|
font-size: 11px !important;
|
||||||
|
font-weight: 500 !important;
|
||||||
|
padding: 2px 6px !important;
|
||||||
|
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.3) !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
}
|
||||||
|
:global(.map-obj-label::before) {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
.cursor-crosshair {
|
.cursor-crosshair {
|
||||||
cursor: crosshair;
|
cursor: crosshair;
|
||||||
@@ -2023,6 +2453,12 @@
|
|||||||
background: rgba(0, 163, 224, 0.1);
|
background: rgba(0, 163, 224, 0.1);
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
.obj-row.drop-before {
|
||||||
|
box-shadow: inset 0 2px 0 0 #00a3e0;
|
||||||
|
}
|
||||||
|
.obj-row.drop-after {
|
||||||
|
box-shadow: inset 0 -2px 0 0 #00a3e0;
|
||||||
|
}
|
||||||
|
|
||||||
.obj-action {
|
.obj-action {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
@@ -2079,4 +2515,27 @@
|
|||||||
background: rgba(239, 68, 68, 0.15);
|
background: rgba(239, 68, 68, 0.15);
|
||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Label toggle buttons */
|
||||||
|
.label-toggle {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
padding: 3px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||||
|
background: none;
|
||||||
|
color: rgba(255, 255, 255, 0.3);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.15s;
|
||||||
|
}
|
||||||
|
.label-toggle:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.06);
|
||||||
|
color: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
.label-toggle.active {
|
||||||
|
background: rgba(0, 163, 224, 0.15);
|
||||||
|
border-color: rgba(0, 163, 224, 0.3);
|
||||||
|
color: #00a3e0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user