fix(files-overlay): post-deploy bug sweep + root-as-row UX

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) <noreply@anthropic.com>
This commit is contained in:
mwiegand 2026-05-09 19:46:19 +02:00
parent 76cd7ddda0
commit 2bba1f31d0
No known key found for this signature in database
4 changed files with 81 additions and 57 deletions

View file

@ -517,21 +517,29 @@ button.danger-outline:hover {
floating Uploads panel. floating Uploads panel.
============================================================ */ ============================================================ */
.files-manager { /* The display: flex / grid declarations on the elements below have the
display: grid; same specificity as the UA's `[hidden]{display:none}` rule and come
gap: var(--space-m); 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 { .files-manager {
display: flex; display: grid;
align-items: center; gap: var(--space-s);
gap: var(--space-m);
flex-wrap: wrap;
} }
.files-manager-hint { .files-manager-hint {
flex: 1;
font-size: 0.85em; font-size: 0.85em;
margin: 0;
} }
.files-tree-root { .files-tree-root {
@ -551,6 +559,16 @@ button.danger-outline:hover {
background: color-mix(in srgb, var(--color-success) 10%, transparent); 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 { .files-empty {
margin: var(--space-s) var(--space-xs); margin: var(--space-s) var(--space-xs);
} }
@ -613,13 +631,6 @@ button.danger-outline:hover {
border-color: var(--color-danger); 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 { .files-row.is-drag-source {
opacity: 0.5; opacity: 0.5;
} }

View file

@ -4,13 +4,18 @@
// carries `data-files-url`. First expand fires a fetch and innerHTMLs the // carries `data-files-url`. First expand fires a fetch and innerHTMLs the
// returned partial into the next `.file-tree-children`; subsequent clicks // returned partial into the next `.file-tree-children`; subsequent clicks
// just toggle visibility — no re-fetch. // just toggle visibility — no re-fetch.
//
// Children-div lookup goes through the row's <li> 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 () { (function () {
document.addEventListener("click", function (event) { document.addEventListener("click", function (event) {
const button = event.target.closest(".file-tree-toggle"); const button = event.target.closest(".file-tree-toggle");
if (!button) return; if (!button) return;
const children = button.nextElementSibling; const row = button.closest(".file-tree-row");
if (!children || !children.classList.contains("file-tree-children")) return; const children = row ? row.querySelector(":scope > .file-tree-children") : null;
if (!children) return;
const wasExpanded = button.getAttribute("aria-expanded") === "true"; const wasExpanded = button.getAttribute("aria-expanded") === "true";
button.setAttribute("aria-expanded", wasExpanded ? "false" : "true"); button.setAttribute("aria-expanded", wasExpanded ? "false" : "true");

View file

@ -136,20 +136,22 @@
} }
if (!path) { if (!path) {
// Overlay root — replace the tree-root container's children. // Overlay root — swap into the synthetic root row's children div.
const empty = treeRoot.querySelector(".files-empty"); const target = manager.querySelector(".files-root-children");
if (!target) return;
const empty = target.querySelector(".files-empty");
if (empty) empty.remove(); if (empty) empty.remove();
const existingUl = treeRoot.querySelector(":scope > ul.file-tree"); const existingUl = target.querySelector(":scope > ul.file-tree");
if (existingUl) existingUl.remove(); if (existingUl) existingUl.remove();
treeRoot.insertAdjacentHTML("beforeend", html); target.insertAdjacentHTML("beforeend", html);
// If the new content is also empty, restore the placeholder. // 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) { if (newUl && newUl.children.length === 0) {
newUl.remove(); newUl.remove();
const p = document.createElement("p"); const p = document.createElement("p");
p.className = "muted files-empty"; p.className = "muted files-empty";
p.textContent = 'Empty — drop files here, or click "+ new file" above.'; p.textContent = 'Empty — drop files here, or click "+ new file" on this row.';
treeRoot.appendChild(p); target.appendChild(p);
} }
return; return;
} }
@ -602,21 +604,22 @@
} }
}); });
input.addEventListener(
"keydown",
(event) => {
if (event.key === "Enter") {
event.preventDefault();
fresh.click();
}
},
{ once: true }
);
newFolderDialog.showModal(); newFolderDialog.showModal();
setTimeout(() => input.focus(), 0); 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 ------------------------------- // ---------- upload queue + progress panel -------------------------------
const uploadQueue = []; const uploadQueue = [];

View file

@ -65,17 +65,20 @@
<div class="files-manager" <div class="files-manager"
data-overlay-id="{{ overlay.id }}" data-overlay-id="{{ overlay.id }}"
data-base-url="/overlays/{{ overlay.id }}"> data-base-url="/overlays/{{ overlay.id }}">
<div class="files-manager-toolbar"> <p class="muted files-manager-hint">Drop files or folders onto a folder row to upload. Drag rows inside the tree to move them.</p>
<span class="muted files-manager-hint">Drop files / folders onto a folder row to upload, drag rows to move</span> <ul class="file-tree files-tree-root" data-files-overlay="1">
<span class="files-row-actions files-root-actions" aria-label="Overlay root actions"> <li class="file-tree-row file-tree-row-dir files-row files-row-root"
data-target-path=""
data-row-kind="dir">
<span class="files-row-root-label">(overlay root)</span>
<span class="files-row-actions" aria-label="Overlay root actions">
<button type="button" class="files-row-action" data-action="new-file" data-target-path="">+ new file</button> <button type="button" class="files-row-action" data-action="new-file" data-target-path="">+ new file</button>
<button type="button" class="files-row-action" data-action="new-folder" data-target-path="">+ new folder</button> <button type="button" class="files-row-action" data-action="new-folder" data-target-path="">+ new folder</button>
<button type="button" class="files-row-action" data-action="zip" data-target-path="">⬇ zip</button> <button type="button" class="files-row-action" data-action="zip" data-target-path="">⬇ zip</button>
</span> </span>
</div> <div class="file-tree-children files-root-children">
<div class="files-tree-root" data-row-kind="dir" data-target-path="">
{% if not file_tree_root_entries %} {% if not file_tree_root_entries %}
<p class="muted files-empty">Empty — drop files here, or click "+ new file" above.</p> <p class="muted files-empty">Empty — drop files here, or click "+ new file" on this row.</p>
{% else %} {% else %}
{% set entries = file_tree_root_entries %} {% set entries = file_tree_root_entries %}
{% set truncated = file_tree_truncated %} {% set truncated = file_tree_truncated %}
@ -86,6 +89,8 @@
{% include "_overlay_file_tree.html" %} {% include "_overlay_file_tree.html" %}
{% endif %} {% endif %}
</div> </div>
</li>
</ul>
</div> </div>
{% else %} {% else %}
{% if not file_tree_root_entries %} {% if not file_tree_root_entries %}