diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1bd6dc0..89abffb 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,17 @@
# Changelog
+## v0.10.0 — 2026-06-14
+
+### Added
+
+- **Resizable overlay panes.** The folder tree and preview panes are now drag-resizable
+ via thin gutters on either side of the file grid — widen the tree for long folder names,
+ or trade preview width for grid space. Sizes persist per browser in `localStorage`, and
+ **double-clicking a gutter resets that pane** to its default (tree `200px` / preview `34%`).
+ Min-width clamps keep every pane usable (tree ≥120px, preview ≥160px, grid ≥200px) so
+ nothing collapses. The middle grid stays fluid and auto-reflows its thumbnail columns.
+ Frontend-only — a focused `web/resize.js` module, no backend changes, no new settings.
+
## v0.9.0 — 2026-06-02
### Added
diff --git a/README.md b/README.md
index 0e0ba25..994ed9a 100644
--- a/README.md
+++ b/README.md
@@ -22,7 +22,7 @@
## ✨ Features
-- 🗂️ **Explorer-style file manager** — a folder tree, a thumbnail grid, and a live preview pane, all in one fullscreen overlay.
+- 🗂️ **Explorer-style file manager** — a folder tree, a thumbnail grid, and a live preview pane, all in one fullscreen overlay with **drag-resizable panes** (double-click a divider to reset).
- 🖼️ **Rich previews** — inline images, an HTML5 **video** player, and an **audio** player. Generated files show their **resolution** (`1024 × 1024`), size, and date at a glance.
- 🧠 **See the generation behind the file** — embedded ComfyUI metadata (positive/negative prompt, seed, model, LoRAs) surfaced in the preview, with one-click **Copy JSON** and **Load on canvas** to drop the workflow straight onto your graph.
- 📤 **Full write operations** — create folders, rename, upload (button or drag from your desktop), copy/cut/paste, and move — within and across roots.
@@ -107,7 +107,7 @@ FileManaty can write to your filesystem, so please read this.
## 🗺️ Roadmap
-Shipped recently: auto-mounted Workflows root, in-folder name + type filter, rich video + audio preview, embedded-metadata cards, Load-on-canvas, and a native theme-following UI. Coming next:
+Shipped recently: drag-resizable overlay panes, auto-mounted Workflows root, in-folder name + type filter, rich video + audio preview, embedded-metadata cards, Load-on-canvas, and a native theme-following UI. Coming next:
- 🔍 **Server-side & metadata search** — search across a whole root (past the listing cap) and find files by the **prompt / model / seed** that made them. *(In-folder name + type filtering shipped in v0.8.0.)*
- 🔐 **Optional built-in authentication** — a lightweight password mode for small deployments.
diff --git a/filemanaty/__init__.py b/filemanaty/__init__.py
index e334469..d572a9f 100644
--- a/filemanaty/__init__.py
+++ b/filemanaty/__init__.py
@@ -1,3 +1,3 @@
"""filemanaty package — config, security, thumbnails, HTTP routes."""
-__version__ = "0.9.0"
+__version__ = "0.10.0"
diff --git a/web/filemanaty.js b/web/filemanaty.js
index 120679d..f071b4b 100644
--- a/web/filemanaty.js
+++ b/web/filemanaty.js
@@ -5,6 +5,7 @@ import { clickSelect, selectAll } from "./selection.js";
import { promptText, confirmDialog, toast, trashView, isDialogOpen } from "./dialogs.js";
import { attachContextMenu } from "./contextmenu.js";
import { renderTree } from "./tree.js";
+import { initPaneResize } from "./resize.js";
import { makeDraggable, makeDropTarget } from "./dnd.js";
import * as settings from "./settings.js";
import { buildSettingsDefinitions, KEYS as SETTINGS_KEYS } from "./settings.js";
@@ -194,9 +195,9 @@ function buildOverlay() {
-
+
-
+
`;
const style = document.createElement("style");
@@ -215,7 +216,9 @@ function buildOverlay() {
#filemanaty-overlay .fm-head-right{display:flex;align-items:center;gap:16px}
#filemanaty-overlay .fm-gh{display:inline-flex;align-items:center;gap:6px;color:var(--fm-text-muted);text-decoration:none;font-size:12px;transition:color .15s}
#filemanaty-overlay .fm-gh:hover{color:var(--fm-text)}
-#filemanaty-overlay .fm-gh svg{width:15px;height:15px}`;
+#filemanaty-overlay .fm-gh svg{width:15px;height:15px}
+#filemanaty-overlay .fm-gutter{background:var(--fm-border);cursor:col-resize;touch-action:none;transition:background .12s}
+#filemanaty-overlay .fm-gutter:hover{background:var(--fm-accent)}`;
root.appendChild(style);
return root;
}
@@ -233,6 +236,7 @@ async function loadVersion() {
async function initOverlay() {
document.getElementById("fm-close").addEventListener("click", closeOverlay);
+ initPaneResize(document.getElementById("fm-body"));
loadVersion();
document.addEventListener("keydown", (e) => {
if (!STATE.open) return;
diff --git a/web/resize.js b/web/resize.js
new file mode 100644
index 0000000..2643655
--- /dev/null
+++ b/web/resize.js
@@ -0,0 +1,117 @@
+// Drag-to-resize for the FileManaty overlay's tree and preview panes.
+// Mirrors ComfyUI/PrimeVue Splitter conventions (localStorage persistence,
+// min-size clamps, col-resize gutters) without a Vue dependency. The middle
+// grid track stays `1fr` and absorbs the difference, so the thumbnail grid
+// keeps auto-reflowing and keyboard nav's computeGridColumns() stays correct.
+
+const LS_TREE = "filemanaty.layout.treeWidth";
+const LS_PREVIEW = "filemanaty.layout.previewWidth";
+
+const DEFAULT_TREE = "200px";
+const DEFAULT_PREVIEW = "34%";
+
+const MIN_TREE = 120; // px
+const MIN_PREVIEW = 160; // px
+const MIN_GRID = 200; // px — the middle pane is never crushed below this
+const GUTTER = 4; // px — matches ComfyUI/PrimeVue's default gutter
+
+function applyColumns(bodyEl, treeCol, previewCol) {
+ bodyEl.style.gridTemplateColumns =
+ `${treeCol} ${GUTTER}px 1fr ${GUTTER}px ${previewCol}`;
+}
+
+function readStored(key) {
+ const v = parseInt(localStorage.getItem(key), 10);
+ return Number.isFinite(v) && v > 0 ? v : null;
+}
+
+export function initPaneResize(bodyEl) {
+ const treeEl = bodyEl.querySelector("#fm-tree");
+ const previewEl = bodyEl.querySelector("#fm-preview");
+
+ // Two gutter elements become grid items. DOM order must end up:
+ // tree, gutTree, grid, gutPreview, preview.
+ const gutTree = document.createElement("div");
+ const gutPreview = document.createElement("div");
+ gutTree.className = "fm-gutter";
+ gutPreview.className = "fm-gutter";
+ treeEl.after(gutTree);
+ previewEl.before(gutPreview);
+
+ // Current track values, applied to the grid. Only the dragged side is
+ // converted to px; the other keeps its default string until touched.
+ const tStored = readStored(LS_TREE);
+ const pStored = readStored(LS_PREVIEW);
+ let treeCol = tStored != null ? `${tStored}px` : DEFAULT_TREE;
+ let previewCol = pStored != null ? `${pStored}px` : DEFAULT_PREVIEW;
+ applyColumns(bodyEl, treeCol, previewCol);
+
+ function startDrag(which, e) {
+ e.preventDefault();
+ const rect = bodyEl.getBoundingClientRect();
+ const prevUserSelect = document.body.style.userSelect;
+ document.body.style.userSelect = "none";
+ document.body.style.cursor = "col-resize";
+ let moved = false;
+
+ function onMove(ev) {
+ moved = true;
+ const total = rect.width;
+ if (which === "tree") {
+ const maxTree =
+ total - GUTTER * 2 - MIN_GRID - previewEl.getBoundingClientRect().width;
+ let w = ev.clientX - rect.left;
+ w = Math.max(MIN_TREE, Math.min(w, maxTree));
+ treeCol = `${Math.round(w)}px`;
+ } else {
+ const maxPreview =
+ total - GUTTER * 2 - MIN_GRID - treeEl.getBoundingClientRect().width;
+ let w = rect.right - ev.clientX;
+ w = Math.max(MIN_PREVIEW, Math.min(w, maxPreview));
+ previewCol = `${Math.round(w)}px`;
+ }
+ applyColumns(bodyEl, treeCol, previewCol);
+ }
+
+ function onUp() {
+ document.removeEventListener("pointermove", onMove);
+ document.removeEventListener("pointerup", onUp);
+ document.body.style.userSelect = prevUserSelect;
+ document.body.style.cursor = "";
+ if (!moved) return; // a plain click should not freeze a % default to px
+ if (which === "tree") {
+ localStorage.setItem(
+ LS_TREE, String(Math.round(treeEl.getBoundingClientRect().width)));
+ } else {
+ localStorage.setItem(
+ LS_PREVIEW, String(Math.round(previewEl.getBoundingClientRect().width)));
+ }
+ }
+
+ document.addEventListener("pointermove", onMove);
+ document.addEventListener("pointerup", onUp);
+ }
+
+ // Capture the pointer on the gutter so drags stay tracked on touch/stylus
+ // (events still bubble to the document listeners below).
+ gutTree.addEventListener("pointerdown", (e) => {
+ gutTree.setPointerCapture?.(e.pointerId);
+ startDrag("tree", e);
+ });
+ gutPreview.addEventListener("pointerdown", (e) => {
+ gutPreview.setPointerCapture?.(e.pointerId);
+ startDrag("preview", e);
+ });
+
+ // Double-click a gutter resets that column to its CSS default.
+ gutTree.addEventListener("dblclick", () => {
+ localStorage.removeItem(LS_TREE);
+ treeCol = DEFAULT_TREE;
+ applyColumns(bodyEl, treeCol, previewCol);
+ });
+ gutPreview.addEventListener("dblclick", () => {
+ localStorage.removeItem(LS_PREVIEW);
+ previewCol = DEFAULT_PREVIEW;
+ applyColumns(bodyEl, treeCol, previewCol);
+ });
+}