From 2bba1f31d0eb59feb74e3d5c4e727a8eed54d843 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Sat, 9 May 2026 19:46:19 +0200 Subject: [PATCH] fix(files-overlay): post-deploy bug sweep + root-as-row UX MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three bugs surfaced in browser testing, plus one UX request: 1. The Uploads panel and the binary-mode editor sub-panels stayed visible after `el.hidden = true` because their `display: flex/grid` rules in components.css have the same specificity as the UA's `[hidden]{display:none}` and come later in cascade. Add a targeted `[hidden]!important` rule for the affected classes. 2. Clicking a folder toggle inside a `files` overlay did nothing. `file-tree.js` looked for `.file-tree-children` via `button.nextElementSibling`, but the files-overlay row template inserts a per-row action span between the toggle and the children div. Switch to `closest('.file-tree-row').querySelector(':scope > .file-tree-children')` so both row variants resolve correctly. 3. Pressing Enter on the new-folder dialog did nothing — the keydown handler was attached with `{once:true}` inside `openNewFolder`, so the first letter the user typed consumed the listener and Enter never fired. Move the listener to module init so it survives subsequent keystrokes and dialog reopenings. UX: render the overlay root as a row inside the tree (label "(overlay root)") rather than as a separate toolbar. The root row carries the same `+ new file · + new folder · ⬇ zip` hover-action column as every other folder row, so drop-on-row, hover-reveal, and data-target-path semantics are uniform across the tree. Co-Authored-By: Claude Opus 4.7 (1M context) --- l4d2web/static/css/components.css | 43 +++++++++++++++--------- l4d2web/static/js/file-tree.js | 9 +++-- l4d2web/static/js/files-overlay.js | 39 ++++++++++++---------- l4d2web/templates/overlay_detail.html | 47 +++++++++++++++------------ 4 files changed, 81 insertions(+), 57 deletions(-) diff --git a/l4d2web/static/css/components.css b/l4d2web/static/css/components.css index 625e278..e8e4b3f 100644 --- a/l4d2web/static/css/components.css +++ b/l4d2web/static/css/components.css @@ -517,21 +517,29 @@ button.danger-outline:hover { floating Uploads panel. ============================================================ */ -.files-manager { - display: grid; - gap: var(--space-m); +/* The display: flex / grid declarations on the elements below have the + same specificity as the UA's `[hidden]{display:none}` rule and come + later in the cascade, so without this they'd win and elements would + stay visible after JS sets `el.hidden = true`. Targeted rather than + a global `[hidden]!important` so we don't fight unknown UA defaults. */ +.files-uploads[hidden], +.files-editor-binary[hidden], +.files-editor-text[hidden], +.files-editor-replace-idle[hidden], +.files-editor-replace-queued[hidden], +.files-editor-rename-hint[hidden], +.files-uploads-clear[hidden] { + display: none !important; } -.files-manager-toolbar { - display: flex; - align-items: center; - gap: var(--space-m); - flex-wrap: wrap; +.files-manager { + display: grid; + gap: var(--space-s); } .files-manager-hint { - flex: 1; font-size: 0.85em; + margin: 0; } .files-tree-root { @@ -551,6 +559,16 @@ button.danger-outline:hover { background: color-mix(in srgb, var(--color-success) 10%, transparent); } +.files-row-root > .files-row-root-label { + font-style: italic; + color: var(--color-muted); +} + +.files-root-children { + flex-basis: 100%; + margin-top: var(--space-xs); +} + .files-empty { margin: var(--space-s) var(--space-xs); } @@ -613,13 +631,6 @@ button.danger-outline:hover { border-color: var(--color-danger); } -.files-root-actions { - margin-left: auto; - display: inline-flex; - align-items: center; - gap: var(--space-xs); -} - .files-row.is-drag-source { opacity: 0.5; } diff --git a/l4d2web/static/js/file-tree.js b/l4d2web/static/js/file-tree.js index 5cfd006..f9ade86 100644 --- a/l4d2web/static/js/file-tree.js +++ b/l4d2web/static/js/file-tree.js @@ -4,13 +4,18 @@ // carries `data-files-url`. First expand fires a fetch and innerHTMLs the // returned partial into the next `.file-tree-children`; subsequent clicks // just toggle visibility — no re-fetch. +// +// Children-div lookup goes through the row's
  • rather than the button's +// nextElementSibling so the files-overlay variant — where a per-row action +// column sits between the toggle button and the children div — works too. (function () { document.addEventListener("click", function (event) { const button = event.target.closest(".file-tree-toggle"); if (!button) return; - const children = button.nextElementSibling; - if (!children || !children.classList.contains("file-tree-children")) return; + const row = button.closest(".file-tree-row"); + const children = row ? row.querySelector(":scope > .file-tree-children") : null; + if (!children) return; const wasExpanded = button.getAttribute("aria-expanded") === "true"; button.setAttribute("aria-expanded", wasExpanded ? "false" : "true"); diff --git a/l4d2web/static/js/files-overlay.js b/l4d2web/static/js/files-overlay.js index d3c500a..0654d4f 100644 --- a/l4d2web/static/js/files-overlay.js +++ b/l4d2web/static/js/files-overlay.js @@ -136,20 +136,22 @@ } if (!path) { - // Overlay root — replace the tree-root container's children. - const empty = treeRoot.querySelector(".files-empty"); + // Overlay root — swap into the synthetic root row's children div. + const target = manager.querySelector(".files-root-children"); + if (!target) return; + const empty = target.querySelector(".files-empty"); if (empty) empty.remove(); - const existingUl = treeRoot.querySelector(":scope > ul.file-tree"); + const existingUl = target.querySelector(":scope > ul.file-tree"); if (existingUl) existingUl.remove(); - treeRoot.insertAdjacentHTML("beforeend", html); + target.insertAdjacentHTML("beforeend", html); // If the new content is also empty, restore the placeholder. - const newUl = treeRoot.querySelector(":scope > ul.file-tree"); + const newUl = target.querySelector(":scope > ul.file-tree"); if (newUl && newUl.children.length === 0) { newUl.remove(); const p = document.createElement("p"); p.className = "muted files-empty"; - p.textContent = 'Empty — drop files here, or click "+ new file" above.'; - treeRoot.appendChild(p); + p.textContent = 'Empty — drop files here, or click "+ new file" on this row.'; + target.appendChild(p); } return; } @@ -602,21 +604,22 @@ } }); - input.addEventListener( - "keydown", - (event) => { - if (event.key === "Enter") { - event.preventDefault(); - fresh.click(); - } - }, - { once: true } - ); - newFolderDialog.showModal(); setTimeout(() => input.focus(), 0); } + // Enter on the new-folder input submits — bound once at module init so + // it survives multiple openings of the dialog. (A previous version used + // `{once: true}` inside openNewFolder, which was consumed by the first + // letter the user typed and never saw Enter.) + newFolderDialog + .querySelector(".files-new-folder-name") + .addEventListener("keydown", (event) => { + if (event.key !== "Enter") return; + event.preventDefault(); + newFolderDialog.querySelector(".files-new-folder-create")?.click(); + }); + // ---------- upload queue + progress panel ------------------------------- const uploadQueue = []; diff --git a/l4d2web/templates/overlay_detail.html b/l4d2web/templates/overlay_detail.html index e927e68..d2d9add 100644 --- a/l4d2web/templates/overlay_detail.html +++ b/l4d2web/templates/overlay_detail.html @@ -65,27 +65,32 @@
    -
    - Drop files / folders onto a folder row to upload, drag rows to move - - - - - -
    -
    - {% if not file_tree_root_entries %} -

    Empty — drop files here, or click "+ new file" above.

    - {% else %} - {% set entries = file_tree_root_entries %} - {% set truncated = file_tree_truncated %} - {% set truncated_count = file_tree_truncated_count %} - {% set files_base_url = "/overlays/" ~ overlay.id %} - {% set download_supported = True %} - {% set files_overlay = True %} - {% include "_overlay_file_tree.html" %} - {% endif %} -
    +

    Drop files or folders onto a folder row to upload. Drag rows inside the tree to move them.

    +
      +
    • + (overlay root) + + + + + +
      + {% if not file_tree_root_entries %} +

      Empty — drop files here, or click "+ new file" on this row.

      + {% else %} + {% set entries = file_tree_root_entries %} + {% set truncated = file_tree_truncated %} + {% set truncated_count = file_tree_truncated_count %} + {% set files_base_url = "/overlays/" ~ overlay.id %} + {% set download_supported = True %} + {% set files_overlay = True %} + {% include "_overlay_file_tree.html" %} + {% endif %} +
      +
    • +
    {% else %} {% if not file_tree_root_entries %}