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.
============================================================ */
.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;
}

View file

@ -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 <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 () {
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");

View file

@ -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 = [];

View file

@ -65,17 +65,20 @@
<div class="files-manager"
data-overlay-id="{{ overlay.id }}"
data-base-url="/overlays/{{ overlay.id }}">
<div class="files-manager-toolbar">
<span class="muted files-manager-hint">Drop files / folders onto a folder row to upload, drag rows to move</span>
<span class="files-row-actions files-root-actions" aria-label="Overlay root actions">
<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>
<ul class="file-tree files-tree-root" data-files-overlay="1">
<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-folder" data-target-path="">+ new folder</button>
<button type="button" class="files-row-action" data-action="zip" data-target-path="">⬇ zip</button>
</span>
</div>
<div class="files-tree-root" data-row-kind="dir" data-target-path="">
<div class="file-tree-children files-root-children">
{% 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 %}
{% set entries = file_tree_root_entries %}
{% set truncated = file_tree_truncated %}
@ -86,6 +89,8 @@
{% include "_overlay_file_tree.html" %}
{% endif %}
</div>
</li>
</ul>
</div>
{% else %}
{% if not file_tree_root_entries %}