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:
parent
76cd7ddda0
commit
2bba1f31d0
4 changed files with 81 additions and 57 deletions
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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");
|
||||||
|
|
|
||||||
|
|
@ -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 = [];
|
||||||
|
|
|
||||||
|
|
@ -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 %}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue