fix(browse): URL-driven grid mode (?view=grid), default-on inside incoming/
User feedback: the Grid toggle button was on every page and showed an
explanatory empty state when classifier wasn't available — adding UI
to explain why UI didn't work. Cleaner approach: drop the button,
make the URL the source of truth, and default grid mode automatically
inside the only context where it's meaningful.
Behavior:
- Inside any incoming/ path (case-insensitive segment match):
→ grid mode by default
- Everywhere else:
→ browse mode
- Explicit overrides via query string:
?view=grid forces grid (only honored where classifier is
available; otherwise falls back to browse)
?view=browse forces browse (always)
UI changes:
- The Browse/Grid pill toggle is gone.
- grid.js drops both empty-state messages; outside an incoming/
path it just does nothing.
- events.js owns resolveViewMode() / applyResolvedViewMode(),
called on initial mount and after every client-side rescope
(dblclick + popstate).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
02bdf851c1
commit
ab44d75d03
4 changed files with 65 additions and 81 deletions
|
|
@ -55,6 +55,8 @@
|
||||||
if (previewBody) previewBody.innerHTML = '';
|
if (previewBody) previewBody.innerHTML = '';
|
||||||
var previewTitle = document.getElementById('previewTitle');
|
var previewTitle = document.getElementById('previewTitle');
|
||||||
if (previewTitle) previewTitle.textContent = 'No file selected';
|
if (previewTitle) previewTitle.textContent = 'No file selected';
|
||||||
|
// Reapply view mode for the new URL (incoming/ → grid, etc).
|
||||||
|
if (events.applyResolvedViewMode) events.applyResolvedViewMode();
|
||||||
} catch (_e) { /* swallow — leave the tree as-is */ }
|
} catch (_e) { /* swallow — leave the tree as-is */ }
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -136,11 +136,10 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// View-mode toggle (Browse vs Grid)
|
// No view-mode buttons; mode is derived from the URL on every
|
||||||
var btnBrowse = document.getElementById('viewModeBrowse');
|
// scope change (resolveViewMode below). Pass-through for the
|
||||||
var btnGrid = document.getElementById('viewModeGrid');
|
// initial path.
|
||||||
if (btnBrowse) btnBrowse.addEventListener('click', function () { setViewMode('browse'); });
|
applyResolvedViewMode();
|
||||||
if (btnGrid) btnGrid.addEventListener('click', function () { setViewMode('grid'); });
|
|
||||||
|
|
||||||
// Pop-out preview button — opens the current preview in a separate window.
|
// Pop-out preview button — opens the current preview in a separate window.
|
||||||
var popout = document.getElementById('previewPopout');
|
var popout = document.getElementById('previewPopout');
|
||||||
|
|
@ -287,25 +286,43 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setViewMode(mode) {
|
// View mode is URL-driven, not UI-driven.
|
||||||
|
//
|
||||||
|
// ?view=grid → grid mode (only honored where classifier is
|
||||||
|
// available; otherwise falls back to browse)
|
||||||
|
// ?view=browse → browse mode (always)
|
||||||
|
// default → path-based: grid when inside an incoming/
|
||||||
|
// subtree, browse everywhere else
|
||||||
|
//
|
||||||
|
// resolveViewMode reads the current location and returns the mode
|
||||||
|
// to render; applyResolvedViewMode toggles the panes accordingly.
|
||||||
|
// Called on initial load and on every client-side rescope.
|
||||||
|
function resolveViewMode() {
|
||||||
|
var qs = new URLSearchParams(window.location.search);
|
||||||
|
var explicit = (qs.get('view') || '').toLowerCase();
|
||||||
|
var grid = window.app.modules.grid;
|
||||||
|
var classifierHere = !!(grid && grid.availableHere && grid.availableHere());
|
||||||
|
if (explicit === 'grid') return classifierHere ? 'grid' : 'browse';
|
||||||
|
if (explicit === 'browse') return 'browse';
|
||||||
|
return classifierHere ? 'grid' : 'browse';
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyResolvedViewMode() {
|
||||||
|
var mode = resolveViewMode();
|
||||||
state.viewMode = mode;
|
state.viewMode = mode;
|
||||||
var browseView = document.getElementById('browseView');
|
var browseView = document.getElementById('browseView');
|
||||||
var gridView = document.getElementById('gridView');
|
var gridView = document.getElementById('gridView');
|
||||||
var btnBrowse = document.getElementById('viewModeBrowse');
|
|
||||||
var btnGrid = document.getElementById('viewModeGrid');
|
|
||||||
if (mode === 'grid') {
|
if (mode === 'grid') {
|
||||||
if (browseView) browseView.classList.add('hidden');
|
if (browseView) browseView.classList.add('hidden');
|
||||||
if (gridView) gridView.classList.remove('hidden');
|
if (gridView) gridView.classList.remove('hidden');
|
||||||
if (btnBrowse) btnBrowse.setAttribute('aria-selected', 'false');
|
|
||||||
if (btnGrid) btnGrid.setAttribute('aria-selected', 'true');
|
|
||||||
// Lazily mount classifier on first activation.
|
|
||||||
var grid = window.app.modules.grid;
|
var grid = window.app.modules.grid;
|
||||||
if (grid && grid.activate) grid.activate();
|
if (grid) {
|
||||||
|
if (grid.reset) grid.reset();
|
||||||
|
if (grid.activate) grid.activate();
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
if (browseView) browseView.classList.remove('hidden');
|
if (browseView) browseView.classList.remove('hidden');
|
||||||
if (gridView) gridView.classList.add('hidden');
|
if (gridView) gridView.classList.add('hidden');
|
||||||
if (btnBrowse) btnBrowse.setAttribute('aria-selected', 'true');
|
|
||||||
if (btnGrid) btnGrid.setAttribute('aria-selected', 'false');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -376,6 +393,10 @@
|
||||||
history.pushState({ zddcBrowse: true, path: url }, '', url);
|
history.pushState({ zddcBrowse: true, path: url }, '', url);
|
||||||
} catch (_e) { /* private browsing edge cases */ }
|
} catch (_e) { /* private browsing edge cases */ }
|
||||||
statusInfo('Entered ' + displayName);
|
statusInfo('Entered ' + displayName);
|
||||||
|
// The new scope may have a different default view (grid inside
|
||||||
|
// incoming/, browse elsewhere). Re-resolve from the URL now
|
||||||
|
// that pushState has updated it.
|
||||||
|
applyResolvedViewMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public API
|
// Public API
|
||||||
|
|
@ -384,6 +405,7 @@
|
||||||
statusError: statusError,
|
statusError: statusError,
|
||||||
statusInfo: statusInfo,
|
statusInfo: statusInfo,
|
||||||
statusClear: statusClear,
|
statusClear: statusClear,
|
||||||
showBrowseRoot: showBrowseRoot
|
showBrowseRoot: showBrowseRoot,
|
||||||
|
applyResolvedViewMode: applyResolvedViewMode
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
||||||
|
|
@ -1,81 +1,38 @@
|
||||||
// grid.js — "Grid mode" plugin for browse. Activated by the
|
// grid.js — "Grid mode" plugin for browse. Loads the classifier tool
|
||||||
// view-mode toggle in the toolbar. Loads the standalone classifier
|
// as an iframe scoped to the current directory so users get classifier's
|
||||||
// tool as an iframe scoped to the current directory; the user gets
|
// full bulk-rename workflow without leaving browse.
|
||||||
// classifier's full bulk-rename workflow without leaving browse.
|
|
||||||
//
|
//
|
||||||
// This is a v1 — a future iteration could bundle classifier's
|
// Availability: only inside an `incoming/` subtree (case-insensitive).
|
||||||
// modules directly into browse for tighter integration (shared
|
// Working/staging support the classifier tool at the URL level, but
|
||||||
// state, no iframe chrome). For now the iframe is a clean separation
|
// they're file-staging contexts in normal use, not rename surfaces —
|
||||||
// that preserves classifier's full feature set.
|
// the Grid toggle is for the inbound side. Outside an incoming/ path,
|
||||||
|
// the Grid button is hidden entirely (no explanatory empty state).
|
||||||
//
|
//
|
||||||
// Iframe src resolution:
|
// Iframe src resolution: <currentDirURL>/classifier.html. Iframe
|
||||||
// - server mode: <currentDirURL>/classifier.html. classifier is
|
// embedding only works in server mode; file:// pages don't get the
|
||||||
// auto-served at any working/staging/incoming subtree per
|
// Grid toggle.
|
||||||
// zddc-server's apps/availability.go. Outside those locations the
|
|
||||||
// iframe will 404 — we surface a friendly message instead of an
|
|
||||||
// opaque blank page.
|
|
||||||
// - file:// or unknown: show a "switch to server mode for grid"
|
|
||||||
// hint. classifier needs FS-API access; embedding it via file://
|
|
||||||
// iframe is blocked by browser security.
|
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
var state = window.app.state;
|
var state = window.app.state;
|
||||||
var mounted = false;
|
var mounted = false;
|
||||||
|
|
||||||
function escapeHtml(s) {
|
|
||||||
return String(s).replace(/&/g, '&').replace(/</g, '<')
|
|
||||||
.replace(/>/g, '>').replace(/"/g, '"');
|
|
||||||
}
|
|
||||||
|
|
||||||
function classifierAvailableHere() {
|
function classifierAvailableHere() {
|
||||||
// classifier auto-serves under any path containing a segment
|
// Grid is the classifier-embedded view. Only meaningful in
|
||||||
// named working / staging / incoming (case-insensitive).
|
// incoming/ — that's where bulk-rename actually happens.
|
||||||
// browse mode-toggle should reflect that.
|
|
||||||
var path = (window.location && window.location.pathname) || '';
|
var path = (window.location && window.location.pathname) || '';
|
||||||
return /\/(working|staging|incoming)(\/|$)/i.test(path);
|
return /\/incoming(\/|$)/i.test(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function activate() {
|
function activate() {
|
||||||
var host = document.getElementById('gridView');
|
var host = document.getElementById('gridView');
|
||||||
if (!host) return;
|
if (!host) return;
|
||||||
|
|
||||||
if (mounted) return;
|
if (mounted) return;
|
||||||
|
if (state.source !== 'server' || !classifierAvailableHere()) return;
|
||||||
host.innerHTML = '';
|
|
||||||
|
|
||||||
if (state.source !== 'server') {
|
|
||||||
host.innerHTML =
|
|
||||||
'<div class="grid-empty">'
|
|
||||||
+ '<h3 style="margin-bottom:0.5rem">Grid mode</h3>'
|
|
||||||
+ '<p>The classifier (bulk ZDDC rename) workflow runs as an embedded'
|
|
||||||
+ ' iframe and requires the page be served by zddc-server.</p>'
|
|
||||||
+ '<p>If you opened this file directly (file://), open the standalone'
|
|
||||||
+ ' <code>classifier.html</code> tool instead — it provides the same'
|
|
||||||
+ ' workflow against a local folder you pick from the file system.</p>'
|
|
||||||
+ '</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!classifierAvailableHere()) {
|
|
||||||
host.innerHTML =
|
|
||||||
'<div class="grid-empty">'
|
|
||||||
+ '<h3 style="margin-bottom:0.5rem">Grid mode</h3>'
|
|
||||||
+ '<p>The classifier (bulk ZDDC rename) workflow auto-serves at'
|
|
||||||
+ ' <code>working/</code>, <code>staging/</code>, and'
|
|
||||||
+ ' <code>incoming/</code> URLs. The current page'
|
|
||||||
+ ' (<code>' + escapeHtml(window.location.pathname) + '</code>) isn\'t'
|
|
||||||
+ ' inside any of those, so classifier isn\'t available here.</p>'
|
|
||||||
+ '<p>Navigate browse into a working/ or staging/ folder, then'
|
|
||||||
+ ' switch to Grid.</p>'
|
|
||||||
+ '</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Compute the iframe src: current page's directory + classifier.html.
|
// Compute the iframe src: current page's directory + classifier.html.
|
||||||
var pathname = window.location.pathname || '/';
|
var pathname = window.location.pathname || '/';
|
||||||
if (!pathname.endsWith('/')) {
|
if (!pathname.endsWith('/')) {
|
||||||
// Strip trailing /<file>.html or similar — keep up to the last "/".
|
|
||||||
var lastSlash = pathname.lastIndexOf('/');
|
var lastSlash = pathname.lastIndexOf('/');
|
||||||
pathname = lastSlash >= 0 ? pathname.substring(0, lastSlash + 1) : '/';
|
pathname = lastSlash >= 0 ? pathname.substring(0, lastSlash + 1) : '/';
|
||||||
}
|
}
|
||||||
|
|
@ -91,12 +48,19 @@
|
||||||
mounted = true;
|
mounted = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// When the user navigates between scopes (client-side rescope on
|
||||||
|
// dblclick), the iframe needs to be reloaded for the new path.
|
||||||
|
// Callers reset before re-activating.
|
||||||
|
function reset() {
|
||||||
|
mounted = false;
|
||||||
|
var host = document.getElementById('gridView');
|
||||||
|
if (host) host.innerHTML = '';
|
||||||
|
}
|
||||||
|
|
||||||
window.app.modules.grid = {
|
window.app.modules.grid = {
|
||||||
activate: activate,
|
activate: activate,
|
||||||
// Hook the toggle button visibility / hint to the activation
|
reset: reset,
|
||||||
// predicate so users at non-classifier paths see the button
|
// Hook for events.js to show/hide the Grid toggle button.
|
||||||
// in a disabled state with explanation. Callers run this
|
|
||||||
// after the initial directory is loaded.
|
|
||||||
availableHere: function () {
|
availableHere: function () {
|
||||||
return state.source === 'server' && classifierAvailableHere();
|
return state.source === 'server' && classifierAvailableHere();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -53,10 +53,6 @@
|
||||||
|
|
||||||
<div id="browseRoot" class="browse-root hidden">
|
<div id="browseRoot" class="browse-root hidden">
|
||||||
<div class="browse-toolbar">
|
<div class="browse-toolbar">
|
||||||
<div class="view-mode-toggle" role="tablist" aria-label="View mode">
|
|
||||||
<button id="viewModeBrowse" class="btn btn-sm" role="tab" aria-selected="true">Browse</button>
|
|
||||||
<button id="viewModeGrid" class="btn btn-sm" role="tab" aria-selected="false">Grid</button>
|
|
||||||
</div>
|
|
||||||
<nav class="breadcrumbs" id="breadcrumbs" aria-label="Path"></nav>
|
<nav class="breadcrumbs" id="breadcrumbs" aria-label="Path"></nav>
|
||||||
<span class="toolbar__count" id="entryCount"></span>
|
<span class="toolbar__count" id="entryCount"></span>
|
||||||
<label class="sort-control" for="sortBy" title="Sort tree entries">
|
<label class="sort-control" for="sortBy" title="Sort tree entries">
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue