Migrate from pip-install-e + setuptools to a uv workspace with a
committed uv.lock for deterministic deps. Switch both members to
hatchling, and move package sources into nested standard layout
(l4d2host/l4d2host/, l4d2web/l4d2web/) so builds work from a
read-only source tree — setuptools wrote egg-info to source under
the old layout, which broke uv sync on the root-owned /opt/left4me/src.
Local dev install: `pip install -e ./l4d2host -e ./l4d2web` -> `uv sync`.
.envrc switches from `layout python python3.13` to `use uv`. Python
pinned to 3.13 via .python-version.
l4d2web now declares its cross-dep on l4d2host explicitly via
[tool.uv.sources] (workspace = true). l4d2web/alembic.ini and
l4d2web/alembic/ stay at the project root (standard alembic layout).
Test fixes:
- tests/__init__.py added to both test dirs so pytest doesn't shadow
l4d2host as a namespace package via outer-dir walk.
- 3 CWD-relative paths in tests (l4d2web/static/css/{tokens,layout}.css
and js/sse.js) anchored to Path(__file__) so they survive layout
changes.
- Two test_install.py tests now monkeypatch HOME to tmp_path so they
stop silently mutating ~/.steam/sdk32 on every run.
628 tests pass under sandboxed `uv run pytest`.
Per docs/superpowers/plans/2026-05-15-uv-workspace-execution.md;
prereq for the ckn-bw bundle's uv-sync action (queued).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
171 lines
5.4 KiB
JavaScript
171 lines
5.4 KiB
JavaScript
(() => {
|
||
const root = document.querySelector("[data-overlay-list]");
|
||
if (!root) return;
|
||
const select = document.querySelector("[data-overlay-add]");
|
||
const empty = document.querySelector("[data-overlay-empty]");
|
||
|
||
let dragRow = null;
|
||
|
||
const clearDropMarkers = () => {
|
||
root
|
||
.querySelectorAll(".drop-before, .drop-after")
|
||
.forEach((el) => el.classList.remove("drop-before", "drop-after"));
|
||
};
|
||
|
||
const refreshEmpty = () => {
|
||
if (!empty) return;
|
||
empty.hidden = root.children.length > 0;
|
||
};
|
||
|
||
const buildHiddenInput = (overlayId) => {
|
||
const input = document.createElement("input");
|
||
input.type = "hidden";
|
||
input.name = "overlay_ids";
|
||
input.value = overlayId;
|
||
return input;
|
||
};
|
||
|
||
const buildRow = (overlayId, overlayName) => {
|
||
const li = document.createElement("li");
|
||
li.className = "overlay-picker-row";
|
||
li.draggable = true;
|
||
li.dataset.overlayId = overlayId;
|
||
li.dataset.overlayName = overlayName;
|
||
|
||
const handle = document.createElement("span");
|
||
handle.className = "overlay-picker-handle";
|
||
handle.setAttribute("aria-hidden", "true");
|
||
handle.textContent = "⋮⋮";
|
||
|
||
const name = document.createElement("span");
|
||
name.className = "overlay-picker-name";
|
||
name.textContent = overlayName;
|
||
|
||
const exposeLabel = document.createElement("label");
|
||
exposeLabel.className = "overlay-picker-expose";
|
||
exposeLabel.title = "Auto-load this overlay's server.cfg before your blueprint config";
|
||
const exposeInput = document.createElement("input");
|
||
exposeInput.type = "checkbox";
|
||
exposeInput.name = "expose_server_cfg_ids";
|
||
exposeInput.value = overlayId;
|
||
const exposeText = document.createTextNode(" exec ");
|
||
const exposeCode = document.createElement("code");
|
||
exposeCode.textContent = "server.cfg";
|
||
exposeLabel.append(exposeInput, exposeText, exposeCode);
|
||
|
||
const remove = document.createElement("button");
|
||
remove.type = "button";
|
||
remove.className = "overlay-picker-remove";
|
||
remove.dataset.action = "remove";
|
||
remove.setAttribute("aria-label", `Remove ${overlayName}`);
|
||
remove.textContent = "×";
|
||
|
||
li.append(handle, name, exposeLabel, remove, buildHiddenInput(overlayId));
|
||
return li;
|
||
};
|
||
|
||
const insertOptionSorted = (overlayId, overlayName) => {
|
||
if (!select) return;
|
||
const option = document.createElement("option");
|
||
option.value = overlayId;
|
||
option.dataset.overlayName = overlayName;
|
||
option.textContent = overlayName;
|
||
|
||
const lower = overlayName.toLowerCase();
|
||
const existing = Array.from(select.querySelectorAll("option[value]:not([value=''])"));
|
||
const next = existing.find(
|
||
(opt) => (opt.dataset.overlayName || opt.textContent).toLowerCase() > lower,
|
||
);
|
||
if (next) {
|
||
select.insertBefore(option, next);
|
||
} else {
|
||
select.appendChild(option);
|
||
}
|
||
};
|
||
|
||
root.addEventListener("dragstart", (event) => {
|
||
const row = event.target.closest(".overlay-picker-row");
|
||
if (!row) return;
|
||
dragRow = row;
|
||
row.classList.add("is-dragging");
|
||
if (event.dataTransfer) {
|
||
event.dataTransfer.effectAllowed = "move";
|
||
event.dataTransfer.setData("text/plain", row.dataset.overlayId || "");
|
||
}
|
||
});
|
||
|
||
root.addEventListener("dragend", () => {
|
||
if (dragRow) {
|
||
dragRow.classList.remove("is-dragging");
|
||
}
|
||
clearDropMarkers();
|
||
dragRow = null;
|
||
});
|
||
|
||
root.addEventListener("dragover", (event) => {
|
||
if (!dragRow) return;
|
||
const row = event.target.closest(".overlay-picker-row");
|
||
event.preventDefault();
|
||
if (event.dataTransfer) {
|
||
event.dataTransfer.dropEffect = "move";
|
||
}
|
||
clearDropMarkers();
|
||
if (row && row !== dragRow) {
|
||
const rect = row.getBoundingClientRect();
|
||
const before = event.clientY < rect.top + rect.height / 2;
|
||
row.classList.add(before ? "drop-before" : "drop-after");
|
||
}
|
||
});
|
||
|
||
root.addEventListener("dragleave", (event) => {
|
||
const row = event.target.closest(".overlay-picker-row");
|
||
if (row) {
|
||
row.classList.remove("drop-before", "drop-after");
|
||
}
|
||
});
|
||
|
||
root.addEventListener("drop", (event) => {
|
||
if (!dragRow) return;
|
||
event.preventDefault();
|
||
const row = event.target.closest(".overlay-picker-row");
|
||
if (row && row !== dragRow) {
|
||
const before = row.classList.contains("drop-before");
|
||
row.classList.remove("drop-before", "drop-after");
|
||
if (before) {
|
||
root.insertBefore(dragRow, row);
|
||
} else {
|
||
root.insertBefore(dragRow, row.nextSibling);
|
||
}
|
||
} else if (!row) {
|
||
root.appendChild(dragRow);
|
||
}
|
||
clearDropMarkers();
|
||
});
|
||
|
||
root.addEventListener("click", (event) => {
|
||
const trigger = event.target.closest("[data-action='remove']");
|
||
if (!trigger) return;
|
||
const row = trigger.closest(".overlay-picker-row");
|
||
if (!row) return;
|
||
const overlayId = row.dataset.overlayId;
|
||
const overlayName = row.dataset.overlayName || "";
|
||
row.remove();
|
||
if (overlayId) insertOptionSorted(overlayId, overlayName);
|
||
refreshEmpty();
|
||
});
|
||
|
||
if (select) {
|
||
select.addEventListener("change", () => {
|
||
const value = select.value;
|
||
if (!value) return;
|
||
const option = select.querySelector(`option[value="${CSS.escape(value)}"]`);
|
||
const overlayName = option ? option.dataset.overlayName || option.textContent : "";
|
||
root.appendChild(buildRow(value, overlayName));
|
||
if (option) option.remove();
|
||
select.value = "";
|
||
refreshEmpty();
|
||
});
|
||
}
|
||
|
||
refreshEmpty();
|
||
})();
|