left4me/l4d2web/l4d2web/static/js/blueprint-overlay-picker.js
mwiegand 49992b3a26
refactor(repo): uv workspace + hatchling + layout restructure
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>
2026-05-15 22:04:29 +02:00

171 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(() => {
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();
})();