diff --git a/app/templates/base.html b/app/templates/base.html
index 6cf449a..1cc24ea 100644
--- a/app/templates/base.html
+++ b/app/templates/base.html
@@ -116,14 +116,18 @@
@dragover.prevent="dragOver($event, {{ page.db_id }}, {{ 'true' if page.is_folder else 'false' }})"
@dragleave="dragLeave($event)"
@drop.prevent="drop($event, {{ page.db_id }})"
- style="padding-left:{{ 8 + depth * 16 }}px;">
+ style="padding-left:{{ 4 + depth * 12 }}px;"
+ @contextmenu.prevent="showWsContext($event, {{ page.db_id }}, '{{ page.name | e }}', {{ 'true' if page.is_folder else 'false' }})"
+ @touchstart="wsTouchStart($event)"
+ @touchend="wsTouchEnd($event, {{ page.db_id }}, '{{ page.name | e }}', {{ 'true' if page.is_folder else 'false' }})"
+ @touchmove="wsTouchMove($event)">
{% if page.is_folder %}
{% else %}
-
+
{% endif %}
{{ page.icon }}
{% if page.is_folder %}
@@ -134,13 +138,6 @@
{% else %}
{{ page.name }}
{% endif %}
-
{% if page.children %}
@@ -446,6 +443,36 @@
+
+
+
@@ -492,6 +519,64 @@
contextActions: {},
showNewPageMenu: false,
+ // ── Workspace tree context menu ──
+ wsCtx: { visible: false, x: 0, y: 0, id: null, name: '', isFolder: false },
+ wsTouchTimer: null,
+ showWsContext(ev, id, name, isFolder) {
+ this.wsCtx = { visible: true, x: ev.clientX, y: ev.clientY, id: id, name: name, isFolder: isFolder };
+ },
+ hideWsContext() { this.wsCtx.visible = false; },
+ wsTouchStart(ev) {
+ var self = this;
+ this.wsTouchTimer = setTimeout(function() {
+ self.wsTouchTimer = null;
+ var el = ev.target.closest('.ws-tree-item');
+ if (el) {
+ var id = parseInt(el.getAttribute('data-id'));
+ var name = el.getAttribute('data-name') || '';
+ var isFolder = el.getAttribute('data-is-folder') === 'true';
+ var touch = ev.touches[0];
+ self.wsCtx = { visible: true, x: touch.clientX, y: touch.clientY, id: id, name: name, isFolder: isFolder };
+ }
+ }, 500);
+ },
+ wsTouchEnd(ev, id, name, isFolder) {
+ if (this.wsTouchTimer) { clearTimeout(this.wsTouchTimer); this.wsTouchTimer = null; return; }
+ // Already opened by touchstart; prevent Alpine @touchend from also firing the context menu
+ ev.preventDefault();
+ },
+ wsTouchMove(ev) {
+ if (this.wsTouchTimer) { clearTimeout(this.wsTouchTimer); this.wsTouchTimer = null; }
+ },
+ wsCtxAction(action) {
+ var id = this.wsCtx.id;
+ var name = this.wsCtx.name;
+ this.wsCtx.visible = false;
+ if (!id) return;
+ switch(action) {
+ case 'new-file':
+ this.newPageInFolder(id);
+ break;
+ case 'new-folder':
+ this.newFolderInFolder(id);
+ break;
+ case 'rename':
+ var newName = prompt('Rename:', name);
+ if (newName && newName.trim()) {
+ fetch('/api/local-workspace/items/' + id, {
+ method: 'PUT',
+ headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': this.getCsrfToken() },
+ body: JSON.stringify({ name: newName.trim() })
+ }).then(function(r) { if (r.ok) window.location.reload(); });
+ }
+ break;
+ case 'delete':
+ if (!confirm('Delete "' + name + '"?')) break;
+ this.deleteWorkspacePage(id);
+ break;
+ }
+ },
+
// ── Favorites tracking ──
favoriteIds: new Set([{% for p in favorite_pages %}'{{ p.id }}'{% if not loop.last %},{% endif %}{% endfor %}]),
diff --git a/app/templates/local_workspace.html b/app/templates/local_workspace.html
index 7f1d481..504474f 100644
--- a/app/templates/local_workspace.html
+++ b/app/templates/local_workspace.html
@@ -158,6 +158,8 @@
📁
+
+
@@ -448,6 +450,41 @@ document.addEventListener('alpine:init', function() {
window.location = '/local-workspace?folder=' + id;
},
+ // ── Expand / Collapse all ──
+ expandAll() {
+ var self = this;
+ this._walkTree(this.tree, function(n) {
+ if (n.is_folder && n.children && n.children.length > 0) {
+ self.expanded[n.id] = true;
+ }
+ });
+ this.expanded = Object.assign({}, this.expanded);
+ // Toggle static HTML for renderChildren
+ this._toggleAllUl(this.$el, true);
+ },
+ collapseAll() {
+ this.expanded = {};
+ this._toggleAllUl(this.$el, false);
+ },
+ _walkTree(nodes, fn) {
+ for (var i=0; i