feature: move folders

This commit is contained in:
2026-07-27 12:37:27 -04:00
parent 03c06c0dd9
commit af7940a6d5
10 changed files with 839 additions and 38 deletions

View File

@@ -0,0 +1,101 @@
(function () {
'use strict';
function apiFolderPath(path) {
return (path || '')
.replace(/^\/+/, '')
.split('/')
.map(encodeURIComponent)
.join('/');
}
function parentFolder(path) {
var index = path.lastIndexOf('/');
return index < 0 ? '' : path.slice(0, index);
}
function baseName(path) {
var index = path.lastIndexOf('/');
return index < 0 ? path : path.slice(index + 1);
}
function postFolder(path, operation, body) {
return fetch('/api/folders/' + apiFolderPath(path) + '/' + operation, {
method: 'POST',
credentials: 'same-origin',
headers: body ? { 'Content-Type': 'application/json' } : undefined,
body: body ? JSON.stringify(body) : undefined,
}).then(function (response) {
return response.json().then(function (data) {
if (!response.ok) throw new Error(data.error || 'Folder operation failed');
return data;
});
});
}
function closeMenu(button) {
var details = button.closest('details');
if (details) details.removeAttribute('open');
}
document.addEventListener('click', function (event) {
var renameButton = event.target.closest('[data-folder-rename-path]');
if (renameButton) {
var renamePath = renameButton.getAttribute('data-folder-rename-path');
var newName = window.prompt('Rename folder to:', baseName(renamePath));
if (newName === null) return;
newName = newName.trim().replace(/^\/+|\/+$/g, '');
if (newName === '' || newName === baseName(renamePath)) return;
var parent = parentFolder(renamePath);
var renameDestination = (parent ? parent + '/' : '') + newName;
renameButton.disabled = true;
postFolder(renamePath, 'move', { path: renameDestination })
.then(function (data) {
window.location.assign(data.url);
})
.catch(function (error) {
window.alert(error.message || 'Failed to rename folder');
renameButton.disabled = false;
});
return;
}
var moveButton = event.target.closest('[data-folder-move-path]');
if (moveButton) {
var movePath = moveButton.getAttribute('data-folder-move-path');
var destination = window.prompt('Move folder to:', movePath);
if (destination === null) return;
destination = destination.trim().replace(/^\/+|\/+$/g, '');
if (destination === '' || destination === movePath) return;
moveButton.disabled = true;
postFolder(movePath, 'move', { path: destination })
.then(function (data) {
window.location.assign(data.url);
})
.catch(function (error) {
window.alert(error.message || 'Failed to move folder');
moveButton.disabled = false;
});
return;
}
var archiveButton = event.target.closest('[data-folder-archive-path]');
if (archiveButton) {
var archivePath = archiveButton.getAttribute('data-folder-archive-path');
if (!archivePath) return;
if (!window.confirm('Archive this folder and all its documents? They will be hidden from the site but can be restored later.')) {
return;
}
archiveButton.disabled = true;
closeMenu(archiveButton);
postFolder(archivePath, 'archive')
.then(function () {
window.location.assign('/');
})
.catch(function (error) {
window.alert(error.message || 'Failed to archive folder');
archiveButton.disabled = false;
});
}
});
})();

View File

@@ -71,9 +71,10 @@ code {
position: sticky;
top: 0;
z-index: 10;
border-bottom: 1px solid rgba(255, 255, 255, 0.42);
background: rgba(238, 246, 255, 0.92);
border-bottom: 1px solid rgba(24, 32, 42, 0.14);
background: rgba(253, 254, 255, 0.96);
backdrop-filter: blur(12px);
box-shadow: inset 0 -1px 0 rgba(255, 255, 255, 0.65);
}
.site-header__inner,
@@ -85,8 +86,9 @@ code {
display: flex;
align-items: center;
justify-content: space-between;
min-height: 64px;
padding: 0 1rem;
gap: 0.375rem;
min-height: 40px;
padding: 0 0.5rem;
}
.site-brand {
@@ -99,34 +101,46 @@ code {
.site-brand img {
display: block;
width: auto;
height: 2.5rem;
height: 1.375rem;
max-width: min(12rem, 42vw);
object-fit: contain;
image-rendering: pixelated;
}
.site-nav {
display: flex;
gap: 0.5rem;
align-items: center;
gap: 0.125rem;
}
.site-nav a {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
border-radius: 50%;
width: 28px;
height: 28px;
border: 1px solid transparent;
border-radius: 3px;
color: inherit;
text-decoration: none;
transition: background 0.15s;
transition: background 0.1s, border-color 0.1s;
}
.site-nav a:hover {
background: rgba(0, 0, 0, 0.06);
background: rgba(0, 0, 0, 0.05);
border-color: rgba(24, 32, 42, 0.16);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.site-nav a:active {
background: rgba(0, 0, 0, 0.09);
box-shadow: inset 0 1px 2px rgba(24, 32, 42, 0.18);
}
.site-nav a svg {
display: block;
width: 16px;
height: 16px;
}
/* Notification bell */
@@ -138,29 +152,38 @@ code {
display: inline-flex;
align-items: center;
justify-content: center;
width: 36px;
height: 36px;
width: 28px;
height: 28px;
padding: 0;
border: 0;
border-radius: 50%;
border: 1px solid transparent;
border-radius: 3px;
background: transparent;
color: inherit;
cursor: pointer;
transition: background 0.15s;
transition: background 0.1s, border-color 0.1s;
}
.notification-bell__trigger:hover {
background: rgba(0, 0, 0, 0.06);
background: rgba(0, 0, 0, 0.05);
border-color: rgba(24, 32, 42, 0.16);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.7);
}
.notification-bell__trigger:active {
background: rgba(0, 0, 0, 0.09);
box-shadow: inset 0 1px 2px rgba(24, 32, 42, 0.18);
}
.notification-bell__trigger svg {
display: block;
width: 16px;
height: 16px;
}
.notification-bell__count {
position: absolute;
top: 2px;
right: 2px;
top: -2px;
right: -2px;
min-width: 16px;
height: 16px;
padding: 0 4px;
@@ -279,13 +302,13 @@ code {
display: flex;
align-items: center;
flex: 1;
max-width: 320px;
margin: 0 1rem;
max-width: 300px;
margin: 0 0.375rem;
}
.site-search__icon {
position: absolute;
left: 0.6rem;
left: 0.45rem;
z-index: 1;
color: var(--muted);
pointer-events: none;
@@ -293,14 +316,16 @@ code {
.site-search input {
width: 100%;
padding: 0.4rem 0.75rem 0.4rem 2rem;
padding: 0.25rem 0.5rem 0.25rem 1.6rem;
border: 1px solid var(--border);
border-radius: 0;
background: var(--panel);
border-radius: 3px;
background: var(--panel-strong);
box-shadow: inset 0 1px 2px rgba(24, 32, 42, 0.08);
color: var(--text);
font: inherit;
font-size: 0.825rem;
transition: border-color 0.15s, background 0.15s;
font-size: 0.78rem;
line-height: 1.4;
transition: border-color 0.1s, background 0.1s;
}
.site-search input::placeholder {
@@ -311,11 +336,12 @@ code {
outline: none;
border-color: var(--accent);
background: var(--panel-strong);
box-shadow: inset 0 1px 2px rgba(24, 32, 42, 0.08), 0 0 0 2px var(--accent-soft);
}
.site-main {
padding: 0;
height: calc(100vh - 64px);
height: calc(100vh - 41px);
}
.auth-shell,
@@ -1632,6 +1658,66 @@ code {
flex: 0 0 auto;
}
.browser-item {
display: flex;
align-items: center;
}
.browser-item > a {
flex: 0 1 auto;
min-width: 0;
}
.browser-item > .browser-item-chevron {
margin-left: auto;
margin-right: 0.5rem;
color: var(--muted);
}
.browser-item-actions {
flex: 0 0 auto;
margin-right: 0.25rem;
opacity: 0;
transition: opacity 0.15s;
}
.browser-item:hover .browser-item-actions,
.browser-item:focus-within .browser-item-actions,
.browser-item-actions[open] {
opacity: 1;
}
.browser-item-actions > summary {
width: 1.5rem;
height: 1.5rem;
border-color: transparent;
background: transparent;
color: var(--muted);
}
.browser-item-actions > summary:hover {
background: var(--accent-soft);
border-color: transparent;
color: var(--accent);
}
.browser-item-actions > summary svg {
width: 0.9rem;
height: 0.9rem;
}
.browser-item-actions .document-actions-dropdown__menu {
right: auto;
left: 0;
min-width: 10rem;
}
@media (hover: none) {
.browser-item-actions {
opacity: 1;
}
}
/* Document shell */
.document-shell,
.error-panel,
@@ -2424,21 +2510,21 @@ code {
@media (max-width: 768px) {
.site-brand img {
height: 2.25rem;
height: 1.25rem;
}
.site-header__inner {
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
padding: 0.75rem 1rem;
min-height: auto;
gap: 0.375rem;
padding: 0 0.5rem;
min-height: 40px;
}
.site-main {
height: auto;
min-height: calc(100vh - 64px);
min-height: calc(100vh - 41px);
}
.workspace-shell,
@@ -2821,12 +2907,22 @@ code {
@media (prefers-color-scheme: dark) {
.site-header {
border-bottom-color: oklch(0.28 0.018 55 / 0.5);
background: oklch(0.16 0.014 55 / 0.92);
border-bottom-color: oklch(0.28 0.018 55 / 0.6);
background: oklch(0.19 0.014 55 / 0.96);
box-shadow: inset 0 -1px 0 oklch(0.26 0.016 55 / 0.5);
}
.site-nav a:hover,
.notification-bell__trigger:hover {
background: rgba(255, 255, 255, 0.08);
background: rgba(255, 255, 255, 0.07);
border-color: oklch(0.34 0.018 55 / 0.7);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.site-nav a:active,
.notification-bell__trigger:active {
background: rgba(255, 255, 255, 0.12);
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.4);
}
.miller-column h2 {