diff --git a/frontend/src/components/maps/LocationMapBottomSheet.jsx b/frontend/src/components/maps/LocationMapBottomSheet.jsx
index f9cddfc..b6b8a6c 100644
--- a/frontend/src/components/maps/LocationMapBottomSheet.jsx
+++ b/frontend/src/components/maps/LocationMapBottomSheet.jsx
@@ -32,6 +32,7 @@ export default function LocationMapBottomSheet({
savingAction,
hasUnsavedChanges,
mapState,
+ mapSize,
onAddObject,
onUndo,
onRedo,
@@ -75,6 +76,12 @@ export default function LocationMapBottomSheet({
const changedLayerCount = DISPLAY_CONTROLS.filter(([key]) => filters[key] !== DEFAULT_MAP_FILTERS[key]).length;
const unmappedItemPreview = visibleUnmappedItems.slice(0, 8);
const hiddenUnmappedItemCount = visibleUnmappedItems.length - unmappedItemPreview.length;
+ const selectedWidth = Number(selectedObject?.width) || 40;
+ const selectedHeight = Number(selectedObject?.height) || 40;
+ const maxX = Math.max(0, Math.round((mapSize?.width || selectedWidth) - selectedWidth));
+ const maxY = Math.max(0, Math.round((mapSize?.height || selectedHeight) - selectedHeight));
+ const maxWidth = Math.max(40, Math.round(mapSize?.width || selectedWidth));
+ const maxHeight = Math.max(40, Math.round(mapSize?.height || selectedHeight));
const saveDraftLabel = savingAction === "save" ? "Saving..." : "Save Draft";
const publishLabel = savingAction === "publish" ? "Publishing..." : "Publish";
const showSelectedZoneItems = () => {
@@ -234,6 +241,9 @@ export default function LocationMapBottomSheet({
X
onObjectField("x", Number(event.target.value))}
@@ -243,6 +253,9 @@ export default function LocationMapBottomSheet({
Y
onObjectField("y", Number(event.target.value))}
@@ -252,6 +265,9 @@ export default function LocationMapBottomSheet({
W
onObjectField("width", Number(event.target.value))}
@@ -261,6 +277,9 @@ export default function LocationMapBottomSheet({
H
onObjectField("height", Number(event.target.value))}
diff --git a/frontend/src/pages/LocationMapManager.jsx b/frontend/src/pages/LocationMapManager.jsx
index 330a1a4..e351a0b 100644
--- a/frontend/src/pages/LocationMapManager.jsx
+++ b/frontend/src/pages/LocationMapManager.jsx
@@ -543,6 +543,7 @@ export default function LocationMapManager() {
savingAction={savingAction}
hasUnsavedChanges={hasUnsavedChanges}
mapState={mapState}
+ mapSize={mapSize}
onAddObject={handleAddObject}
onUndo={handleUndo}
onRedo={handleRedo}
diff --git a/frontend/tests/location-map-manager.spec.ts b/frontend/tests/location-map-manager.spec.ts
index 5d355e8..5e954fa 100644
--- a/frontend/tests/location-map-manager.spec.ts
+++ b/frontend/tests/location-map-manager.spec.ts
@@ -596,6 +596,61 @@ test("admin selecting an object does not mark a draft dirty until it changes", a
await expect(page.getByRole("button", { name: "Undo" })).toBeEnabled();
});
+test("admin object numeric fields expose map bounds and clamp edits", async ({ page }) => {
+ await mockMapShell(page);
+
+ const mapState = draftMapState([
+ {
+ id: 1352,
+ location_map_id: 900,
+ zone_id: 501,
+ zone_name: "Bakery",
+ type: "zone",
+ label: "Bakery",
+ x: 40,
+ y: 40,
+ width: 260,
+ height: 160,
+ rotation: 0,
+ locked: false,
+ visible: true,
+ sort_order: 1,
+ },
+ ], true);
+
+ await page.route("**/households/1/locations/10/map", async (route) => {
+ await route.fulfill({
+ status: 200,
+ contentType: "application/json",
+ body: JSON.stringify(mapState),
+ });
+ });
+
+ await page.goto("/stores/100/locations/10/map");
+ await page.getByRole("button", { name: "Continue Editing" }).click();
+ await page.getByRole("button", { name: "Edit Objects" }).click();
+ await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
+
+ const xInput = page.getByRole("spinbutton", { name: "X" });
+ const yInput = page.getByRole("spinbutton", { name: "Y" });
+ const widthInput = page.getByRole("spinbutton", { name: "W" });
+ const heightInput = page.getByRole("spinbutton", { name: "H" });
+
+ await expect(xInput).toHaveAttribute("min", "0");
+ await expect(xInput).toHaveAttribute("max", "740");
+ await expect(yInput).toHaveAttribute("max", "540");
+ await expect(widthInput).toHaveAttribute("min", "40");
+ await expect(widthInput).toHaveAttribute("max", "1000");
+ await expect(heightInput).toHaveAttribute("max", "700");
+
+ await xInput.fill("9999");
+ await expect(xInput).toHaveValue("740");
+
+ await widthInput.fill("5000");
+ await expect(widthInput).toHaveValue("1000");
+ await expect(xInput).toHaveValue("0");
+});
+
test("admin save progress locks draft controls", async ({ page }) => {
await mockMapShell(page);