The arrow that unbound half of wire(), and openings for the unit's top

Eric: styled buttons look great, but edit-description does nothing.
One missing character — .forEach(b = instead of (b => — turned the
callback into a strict-mode ReferenceError that killed wire() midway:
everything bound before the typo (rename, duplicate) worked,
everything after (edit description, its save, the add-opening submit)
silently never bound, and the throw hid inside an async refresh where
no pageerror fires. Fixed and verified live: description edits save,
add-opening works again.

And Eric's next question answered in code: things live ON the
furniture too. The add-opening form gains an "at the top" checkbox —
new openings (or whole grids) land above the existing rows instead of
below, so "on top of the Kallax" is one labeled opening away. Give it
real width/depth and the height to your ceiling, or no sizes for a
no-limit display spot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
This commit is contained in:
Eric Wagoner
2026-08-09 15:22:56 -04:00
co-authored by Claude Fable 5
parent 2f526335e8
commit 4ce7706521
5 changed files with 95 additions and 3 deletions
+46
View File
@@ -166,6 +166,52 @@
"depth_in": 9.5 "depth_in": 9.5
} }
] ]
},
{
"name": "Library Left Shelving",
"description": "To the right of the Kallax",
"openings": [
{
"label": "A1",
"zone": "",
"width_in": 23.25,
"height_in": 10.25,
"depth_in": 9.5,
"id": "library-right-shelving-2-a1"
},
{
"label": "B1",
"zone": "",
"width_in": 23.25,
"height_in": 10.25,
"depth_in": 9.5,
"id": "library-right-shelving-2-b1"
},
{
"label": "C1",
"zone": "",
"width_in": 23.25,
"height_in": 10.25,
"depth_in": 9.5,
"id": "library-right-shelving-2-c1"
},
{
"label": "D1",
"zone": "",
"width_in": 23.25,
"height_in": 10.25,
"depth_in": 9.5,
"id": "library-right-shelving-2-d1"
},
{
"label": "E1",
"zone": "",
"width_in": 23.25,
"height_in": 10.25,
"depth_in": 9.5,
"id": "library-right-shelving-2-e1"
}
]
} }
] ]
} }
+3
View File
@@ -552,6 +552,9 @@ select {
margin-bottom: .8rem; } margin-bottom: .8rem; }
.orow { display: flex; gap: .5rem; } .orow { display: flex; gap: .5rem; }
.formlabel { margin: 1.2rem 0 .35rem; } .formlabel { margin: 1.2rem 0 .35rem; }
.editform label.attop { flex-direction: row; align-items: center; gap: .35rem;
text-transform: none; letter-spacing: normal; align-self: end;
padding-bottom: .45rem; }
.renamerow:not([hidden]) { display: flex; gap: .4rem; margin: .3rem 0 .6rem; } .renamerow:not([hidden]) { display: flex; gap: .4rem; margin: .3rem 0 .6rem; }
.renamerow input { font: inherit; font-size: .9rem; .renamerow input { font: inherit; font-size: .9rem;
border: 2px solid var(--board-edge); border-radius: var(--radius); border: 2px solid var(--board-edge); border-radius: var(--radius);
+3 -1
View File
@@ -148,6 +148,7 @@ function unitBlock(u) {
<label>Columns <input name="cols" inputmode="numeric" size="3" value="1"></label> <label>Columns <input name="cols" inputmode="numeric" size="3" value="1"></label>
<label>Label <input name="label" placeholder="wide top — for a single opening"></label> <label>Label <input name="label" placeholder="wide top — for a single opening"></label>
<label>Zone <input name="zone"></label> <label>Zone <input name="zone"></label>
<label class="attop"><input type="checkbox" name="at_top"> at the top</label>
<label>W <input name="width_in" size="5" placeholder="13.25"></label> <label>W <input name="width_in" size="5" placeholder="13.25"></label>
<label>H <input name="height_in" size="5" placeholder="13.25"></label> <label>H <input name="height_in" size="5" placeholder="13.25"></label>
<label>D <input name="depth_in" size="5" placeholder="15.4"></label> <label>D <input name="depth_in" size="5" placeholder="15.4"></label>
@@ -325,7 +326,7 @@ function wire() {
name: b.dataset.unit, name: b.dataset.unit,
new_name: b.closest(".renamerow").querySelector("input").value, new_name: b.closest(".renamerow").querySelector("input").value,
})); }));
document.querySelectorAll(".editunitdesc").forEach(b = document.querySelectorAll(".editunitdesc").forEach(b =>
b.onclick = () => { b.onclick = () => {
const form = b.closest(".unitdesc").querySelector(".descform"); const form = b.closest(".unitdesc").querySelector(".descform");
form.hidden = !form.hidden; form.hidden = !form.hidden;
@@ -359,6 +360,7 @@ function wire() {
try { try {
await post("/api/furniture/add-openings", { await post("/api/furniture/add-openings", {
unit: form.dataset.unit, zone: f.zone.value, unit: form.dataset.unit, zone: f.zone.value,
at_top: f.at_top.checked,
...(grid ? {rows, cols} : {label: f.label.value}), ...(grid ? {rows, cols} : {label: f.label.value}),
width_in: parseFloat(f.width_in.value) || null, width_in: parseFloat(f.width_in.value) || null,
height_in: parseFloat(f.height_in.value) || null, height_in: parseFloat(f.height_in.value) || null,
+12 -2
View File
@@ -237,6 +237,7 @@ class OpeningsBody(BaseModel):
unit: str unit: str
zone: str = "" zone: str = ""
at_top: bool = False # new openings land ABOVE the existing rows
label: str = "" # single mode label: str = "" # single mode
rows: int = 0 # grid mode when rows*cols > 0 rows: int = 0 # grid mode when rows*cols > 0
cols: int = 0 cols: int = 0
@@ -1307,6 +1308,14 @@ def create_app(
# a half-sized opening would silently become limitless: # a half-sized opening would silently become limitless:
# fit checks need all three axes, or none (virtual) # fit checks need all three axes, or none (virtual)
raise HTTPException(400, "give all three interior dimensions, or none") raise HTTPException(400, "give all three interior dimensions, or none")
added: list[dict] = []
def place(new_openings):
if body.at_top:
unit["openings"][:0] = new_openings
else:
unit["openings"].extend(new_openings)
if body.rows and body.cols: if body.rows and body.cols:
# a second grid on the same unit continues the row letters # a second grid on the same unit continues the row letters
# (A1 already exists -> next section starts at C, say): # (A1 already exists -> next section starts at C, say):
@@ -1329,7 +1338,7 @@ def create_app(
break break
for c in range(body.cols): for c in range(body.cols):
label = f"{letters}{c + 1}" label = f"{letters}{c + 1}"
unit["openings"].append( added.append(
{ {
"id": new_opening_id(units, unit["name"], label), "id": new_opening_id(units, unit["name"], label),
"label": label, "label": label,
@@ -1341,7 +1350,7 @@ def create_app(
label = body.label.strip() label = body.label.strip()
if not label: if not label:
raise HTTPException(400, "the opening needs a label") raise HTTPException(400, "the opening needs a label")
unit["openings"].append( added.append(
{ {
"id": new_opening_id(units, unit["name"], label), "id": new_opening_id(units, unit["name"], label),
"label": label, "label": label,
@@ -1349,6 +1358,7 @@ def create_app(
**dims, **dims,
} }
) )
place(added)
return _mutate_furniture(add) return _mutate_furniture(add)
+31
View File
@@ -978,3 +978,34 @@ def test_rename_unit_keeps_openings_locations_and_description(tmp_path):
).status_code ).status_code
== 409 == 409
) )
def test_add_openings_at_top(tmp_path):
"""Things live ON the furniture too: an opening added at_top lands
above the existing rows and renders first in the wall diagram."""
web, cfg = _web(tmp_path, {})
web.post("/api/furniture/add-unit", json={"name": "Kallax"})
web.post(
"/api/furniture/add-openings",
json={
"unit": "Kallax",
"rows": 2,
"cols": 2,
"width_in": 13.25,
"height_in": 13.25,
"depth_in": 15.4,
},
)
web.post(
"/api/furniture/add-openings",
json={
"unit": "Kallax",
"label": "on top",
"zone": "display",
"at_top": True,
},
)
labels = [
o["label"] for o in web.get("/api/shelves").json()["units"][0]["openings"]
]
assert labels == ["on top", "A1", "A2", "B1", "B2"]