From fbbd3be0d38dceffb24944329e8e1fbc2ef2f0b0 Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Thu, 17 Jul 2025 17:57:52 +0545 Subject: [PATCH 01/45] refactor: workspace showing both pinned and not pinned tabs --- sections/workspace.js | 140 +++++++++++++++++------------------------- style.css | 41 +++++++------ 2 files changed, 79 insertions(+), 102 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index 03c6b5e..f121858 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -22,7 +22,9 @@ export const workspacesSection = { `, init: function() { - const container = parseElement(`
`); + const container = parseElement(`
`); + const innerContainer = container.querySelector('#haven-workspace-inner-container') + // const outerContainer = container.querySelector('#haven-workspace-outer-container') const addWorkspaceButton = parseElement(`
@@ -41,103 +43,73 @@ export const workspacesSection = { } }); - const workspacesButton = document.getElementById("zen-workspaces-button"); - if (workspacesButton) { - console.log("[ZenHaven] Found workspace button:", workspacesButton); - const workspaceElements = Array.from(workspacesButton.children); - console.log("[ZenHaven] Workspace elements:", workspaceElements); + if (typeof gZenWorkspaces === "undefined") { + console.error("[ZenHaven] gZenWorkspaces is not available."); + innerContainer.appendChild(addWorkspaceButton); + return innerContainer; + } + + gZenWorkspaces + ._workspaces() + .then(({ workspaces: allWorkspaces }) => { + const allTabs = gZenWorkspaces.allStoredTabs || []; - workspaceElements.forEach((workspace) => { - // Create base workspace div - const workspaceDiv = parseElement( - `
`, - ); - const uuid = workspace.getAttribute("zen-workspace-id"); + allWorkspaces.forEach((workspace) => { + const workspaceDiv = parseElement( + `
`, + ); + const { uuid, theme } = workspace; - ZenWorkspacesStorage.getWorkspaces().then((allWorkspaces) => { - const data = allWorkspaces.find((ws) => ws.uuid === uuid); - if ( - data?.theme?.type === "gradient" && - data.theme.gradientColors?.length - ) { - workspaceDiv.style.background = getGradientCSS(data.theme); - workspaceDiv.style.opacity = data.theme.opacity ?? 1; + if (theme?.type === "gradient" && theme.gradientColors?.length) { + workspaceDiv.style.background = getGradientCSS(theme); + workspaceDiv.style.opacity = theme.opacity ?? 1; } else { workspaceDiv.style.background = "var(--zen-colors-border)"; workspaceDiv.style.opacity = 1; } - }); - - // Create content container - const contentDiv = parseElement( - `
`, - ); - - // Find workspace sections using the workspace's own ID - const sections = document.querySelectorAll( - `.zen-workspace-tabs-section[zen-workspace-id="${workspace.getAttribute( - "zen-workspace-id", - )}"]`, - ); - sections.forEach((section) => { - const root = section.shadowRoot || section; - const sectionWrapper = parseElement( - `
`, + const contentDiv = parseElement( + `
`, ); - - // Copy computed styles from original section - const computedStyle = window.getComputedStyle(section); - sectionWrapper.style.cssText = Array.from(computedStyle).reduce( - (str, property) => { - return `${str}${property}:${computedStyle.getPropertyValue( - property, - )};`; - }, - "", + const pinnedTabsContainer = parseElement( + `
`, + ); + const regularTabsContainer = parseElement( + `
`, ); - // Clone tab groups with their styles - const tabGroups = root.querySelectorAll("tab-group"); - tabGroups.forEach((group) => { - const groupClone = group.cloneNode(true); - const groupStyle = window.getComputedStyle(group); - groupClone.style.cssText = Array.from(groupStyle).reduce( - (str, property) => { - return `${str}${property}:${groupStyle.getPropertyValue( - property, - )};`; - }, - "", - ); - sectionWrapper.appendChild(groupClone); - }); + allTabs + .filter( + (tabEl) => + tabEl && + tabEl.getAttribute("zen-workspace-id") === uuid && + !tabEl.hasAttribute("zen-essential"), + ) + .forEach((tabEl) => { + const clonedTab = tabEl.cloneNode(true); + if (clonedTab.hasAttribute("pinned")) { + pinnedTabsContainer.appendChild(clonedTab); + } else { + regularTabsContainer.appendChild(clonedTab); + } + }); + + if (pinnedTabsContainer.hasChildNodes()) { + contentDiv.appendChild(pinnedTabsContainer); + } + if (regularTabsContainer.hasChildNodes()) { + contentDiv.appendChild(regularTabsContainer); + } - // Clone remaining children with their styles - Array.from(root.children).forEach((child) => { - if (!child.classList.contains("zen-tab-group")) { - const clone = child.cloneNode(true); - const childStyle = window.getComputedStyle(child); - clone.style.cssText = Array.from(childStyle).reduce( - (str, property) => { - return `${str}${property}:${childStyle.getPropertyValue( - property, - )};`; - }, - "", - ); - sectionWrapper.appendChild(clone); - } - }); - contentDiv.appendChild(sectionWrapper); + workspaceDiv.appendChild(contentDiv); + innerContainer.insertBefore(workspaceDiv, addWorkspaceButton); }); - - workspaceDiv.appendChild(contentDiv); - container.appendChild(workspaceDiv); + }) + .catch((error) => { + console.error("[ZenHaven] Error building workspaces section:", error); }); - } - container.appendChild(addWorkspaceButton); + innerContainer.appendChild(addWorkspaceButton); return container; }, }; diff --git a/style.css b/style.css index f7ab470..e1ec3c7 100644 --- a/style.css +++ b/style.css @@ -104,7 +104,8 @@ box-shadow: inset 10px 0 20px rgba(0, 0, 0, 0.5), /* left */ inset 0 0 20px rgba(0, 0, 0, 0.5), /* bottom */ inset 0 0 20px rgba(0, 0, 0, 0.5); /* top */ - + max-height:94vh !important; + &[haven-workspaces] { div { display: flex !important; @@ -113,17 +114,25 @@ } } + #haven-workspace-outer-container{ + width: 100% !important; + overflow-x: auto !important; + position: relative !important; + } + #haven-workspace-inner-container{ + gap:30px !important; + } + .haven-workspace { height: 85% !important; - min-width: 20%; + max-height: 85% !important; + min-width: 150px; background-color: var(--zen-primary-color); - margin-left: 30px; - margin-right: 30px; border-radius: 8px; border: 2px solid var(--zen-colors-border); - display: flex; - flex-direction: column; - align-items: center; + display: block !important; + align-items: start !important; + justify-content: start !important; padding: 10px !important; .tab-reset-pin-button { @@ -151,19 +160,15 @@ margin: 0 !important; padding: 10px !important; display: flex !important; - align-items: center !important; - height: fit-content !important; width: 100% !important; - overflow: hidden !important; - align-items: flex-start; - - .haven-workspace-section { + align-items: flex-start !important; + .haven-workspace-pinned-tabs,.haven-workspace-regular-tabs{ display: flex !important; - position: relative !important; - min-height: 70px !important; - margin: 0 !important; - padding-inline: 2px !important; - transform: translateX(0) !important; + height: fit-content !important; + flex-direction: column !important; + } + .haven-workspace-pinned-tabs{ + border-bottom:2px solid rgba(0,0,0,0.3) !important; } } } From 7a3aa30753c89db6cbfe96cc84a1354edd7c1264 Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Thu, 17 Jul 2025 18:03:22 +0545 Subject: [PATCH 02/45] adding header to workspace section --- sections/workspace.js | 14 +++++++++++--- style.css | 4 +++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index f121858..f33ef31 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -46,7 +46,7 @@ export const workspacesSection = { if (typeof gZenWorkspaces === "undefined") { console.error("[ZenHaven] gZenWorkspaces is not available."); innerContainer.appendChild(addWorkspaceButton); - return innerContainer; + return container; } gZenWorkspaces @@ -58,7 +58,7 @@ export const workspacesSection = { const workspaceDiv = parseElement( `
`, ); - const { uuid, theme } = workspace; + const { uuid, theme, name, icon } = workspace; if (theme?.type === "gradient" && theme.gradientColors?.length) { workspaceDiv.style.background = getGradientCSS(theme); @@ -68,6 +68,14 @@ export const workspacesSection = { workspaceDiv.style.opacity = 1; } + const headerDiv = parseElement( + `
+ ${icon} + ${name} +
`, + ); + workspaceDiv.appendChild(headerDiv); + const contentDiv = parseElement( `
`, ); @@ -102,7 +110,7 @@ export const workspacesSection = { } workspaceDiv.appendChild(contentDiv); - innerContainer.insertBefore(workspaceDiv, addWorkspaceButton); + innerContainer.appendChild(workspaceDiv); }); }) .catch((error) => { diff --git a/style.css b/style.css index e1ec3c7..9d25a0d 100644 --- a/style.css +++ b/style.css @@ -130,7 +130,8 @@ background-color: var(--zen-primary-color); border-radius: 8px; border: 2px solid var(--zen-colors-border); - display: block !important; + display: flex !important; + flex-direction: column !important; align-items: start !important; justify-content: start !important; padding: 10px !important; @@ -144,6 +145,7 @@ } .haven-workspace-header { + height: fit-content !important; margin: 2px !important; .workspace-icon { From 00763ce1ebce68fb483a16a8061076bde9190e67 Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Thu, 17 Jul 2025 18:07:28 +0545 Subject: [PATCH 03/45] fix: opacity of workspace section --- sections/workspace.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index f33ef31..15b3a0e 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -5,10 +5,11 @@ function getGradientCSS(theme) { return "transparent"; const angle = Math.round(theme.rotation || 0); + const opacity = theme.opacity ?? 1; const stops = theme.gradientColors .map(({ c }) => { const [r, g, b] = c; - return `rgb(${r}, ${g}, ${b})`; + return `rgba(${r}, ${g}, ${b}, ${opacity})`; }) .join(", "); @@ -62,10 +63,8 @@ export const workspacesSection = { if (theme?.type === "gradient" && theme.gradientColors?.length) { workspaceDiv.style.background = getGradientCSS(theme); - workspaceDiv.style.opacity = theme.opacity ?? 1; } else { workspaceDiv.style.background = "var(--zen-colors-border)"; - workspaceDiv.style.opacity = 1; } const headerDiv = parseElement( @@ -121,3 +120,4 @@ export const workspacesSection = { return container; }, }; + From 3b37de4ad914900d557a06fa5143e5e119937606 Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Thu, 17 Jul 2025 19:55:27 +0545 Subject: [PATCH 04/45] feat: popup added for selecting options in header --- sections/workspace.js | 39 ++++++++++++++++++++++++++++++--------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index 15b3a0e..7d770f4 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -56,10 +56,15 @@ export const workspacesSection = { const allTabs = gZenWorkspaces.allStoredTabs || []; allWorkspaces.forEach((workspace) => { + const { uuid, theme, name, icon } = workspace; const workspaceDiv = parseElement( - `
`, + `
+
+ ${icon} + ${name} +
+
`, ); - const { uuid, theme, name, icon } = workspace; if (theme?.type === "gradient" && theme.gradientColors?.length) { workspaceDiv.style.background = getGradientCSS(theme); @@ -67,13 +72,29 @@ export const workspacesSection = { workspaceDiv.style.background = "var(--zen-colors-border)"; } - const headerDiv = parseElement( - `
- ${icon} - ${name} -
`, - ); - workspaceDiv.appendChild(headerDiv); + const header = workspaceDiv.querySelector(".haven-workspace-header"); + const popupOpenButton = parseElement(` + `, 'xul'); + const menuPopup = parseElement(` + + + + + + + `, 'xul'); + header.appendChild(popupOpenButton); + container.appendChild(menuPopup); + + popupOpenButton.addEventListener("click", (event) => { + event.stopPropagation(); + menuPopup.openPopup(popupOpenButton, "after_start"); + }); const contentDiv = parseElement( `
`, From 2ae7456d4c09aeed0e58a9937a2682a2e9447f92 Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Thu, 17 Jul 2025 20:13:44 +0545 Subject: [PATCH 05/45] feat: made the menu useful --- sections/workspace.js | 96 ++++++++++++++++++++++++++++++++----------- 1 file changed, 73 insertions(+), 23 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index 7d770f4..011453c 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -23,8 +23,12 @@ export const workspacesSection = { `, init: function() { - const container = parseElement(`
`); - const innerContainer = container.querySelector('#haven-workspace-inner-container') + const container = parseElement( + `
`, + ); + const innerContainer = container.querySelector( + "#haven-workspace-inner-container", + ); // const outerContainer = container.querySelector('#haven-workspace-outer-container') const addWorkspaceButton = @@ -57,13 +61,15 @@ export const workspacesSection = { allWorkspaces.forEach((workspace) => { const { uuid, theme, name, icon } = workspace; + // TODO: make renamable in douple click + // TODO: Icon picker const workspaceDiv = parseElement( `
-
- ${icon} - ${name} -
-
`, +
+ ${icon} + ${name} +
+
`, ); if (theme?.type === "gradient" && theme.gradientColors?.length) { @@ -73,21 +79,27 @@ export const workspacesSection = { } const header = workspaceDiv.querySelector(".haven-workspace-header"); - const popupOpenButton = parseElement(` - `, 'xul'); - const menuPopup = parseElement(` - - - - - - - `, 'xul'); + const popupOpenButton = parseElement( + ` + `, + "xul", + ); + const menuPopup = parseElement( + ` + + + + + + + `, + "xul", + ); header.appendChild(popupOpenButton); container.appendChild(menuPopup); @@ -96,6 +108,45 @@ export const workspacesSection = { menuPopup.openPopup(popupOpenButton, "after_start"); }); + // TODO: after rename on douple click is implimented, this will not prompt + menuPopup + .querySelector(".rename") + .addEventListener("click", async () => { + const workspace = gZenWorkspaces.getWorkspaceFromId(uuid); + if (workspace) { + const newName = prompt( + "Enter new name for workspace:", + workspace.name, + ); + if (newName?.trim()) { + workspace.name = newName.trim(); + await gZenWorkspaces.saveWorkspace(workspace); + workspaceDiv.querySelector(".workspace-name").textContent = + workspace.name; + } + } + }); + + menuPopup + .querySelector(".switch") + .addEventListener("click", async () => { + await gZenWorkspaces.changeWorkspaceWithID(uuid); + + // close haven + window.haven.destroyUI(); + }); + + menuPopup + .querySelector(".delete-workspace") + .addEventListener("click", async () => { + if ( + confirm(`Are you sure you want to delete workspace "${name}"?`) + ) { + await gZenWorkspaces.removeWorkspace(uuid); + workspaceDiv.remove(); + } + }); + const contentDiv = parseElement( `
`, ); @@ -141,4 +192,3 @@ export const workspacesSection = { return container; }, }; - From 752dd542bd3927f03d286a925a5209b35b0d96a9 Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Thu, 17 Jul 2025 20:20:07 +0545 Subject: [PATCH 06/45] fix: add workspace button --- sections/workspace.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index 011453c..dff653b 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -37,11 +37,19 @@ export const workspacesSection = { `); addWorkspaceButton.addEventListener("click", () => { try { - if (typeof ZenWorkspaces?.openSaveDialog === "function") { - console.log("[ZenHaven] Attempting to open workspace save dialog..."); - ZenWorkspaces.openSaveDialog(); + if (typeof gZenWorkspaces?.openWorkspaceCreation === "function") { + console.log( + "[ZenHaven] Attempting to open workspace creation dialog...", + ); + + // close haven + window.haven.destroyUI(); + + gZenWorkspaces.openWorkspaceCreation(); } else { - throw new Error("ZenWorkspaces.openSaveDialog is not available"); + throw new Error( + "gZenWorkspaces.openWorkspaceCreation is not available", + ); } } catch (error) { console.error("[ZenHaven] Error opening workspace dialog:", error); From 144e6fcff5191b700e7bf4088ed3b6614c7509de Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Thu, 17 Jul 2025 20:29:55 +0545 Subject: [PATCH 07/45] feat: workspace can be dragged to reorder --- sections/workspace.js | 62 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/sections/workspace.js b/sections/workspace.js index dff653b..43e7801 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -31,6 +31,56 @@ export const workspacesSection = { ); // const outerContainer = container.querySelector('#haven-workspace-outer-container') + function getDragAfterElement(container, x) { + const draggableElements = [ + ...container.querySelectorAll(".haven-workspace:not(.dragging)"), + ]; + + return draggableElements.reduce( + (closest, child) => { + const box = child.getBoundingClientRect(); + const offset = x - box.left - box.width / 2; + if (offset < 0 && offset > closest.offset) { + return { offset: offset, element: child }; + } else { + return closest; + } + }, + { offset: Number.NEGATIVE_INFINITY }, + ).element; + } + + innerContainer.addEventListener("dragover", (e) => { + e.preventDefault(); + const afterElement = getDragAfterElement(innerContainer, e.clientX); + const dragging = innerContainer.querySelector(".dragging"); + if (dragging) { + if (afterElement == null) { + innerContainer.appendChild(dragging); + } else { + innerContainer.insertBefore(dragging, afterElement); + } + } + }); + + innerContainer.addEventListener("drop", async (e) => { + e.preventDefault(); + const draggedElement = innerContainer.querySelector(".dragging"); + if (!draggedElement) return; + + const draggedUuid = draggedElement.dataset.uuid; + const workspaceElements = [ + ...innerContainer.querySelectorAll(".haven-workspace"), + ]; + const newIndex = workspaceElements.findIndex( + (el) => el === draggedElement, + ); + + if (newIndex !== -1) { + await gZenWorkspaces.reorderWorkspace(draggedUuid, newIndex); + } + }); + const addWorkspaceButton = parseElement(`
@@ -80,6 +130,18 @@ export const workspacesSection = {
`, ); + workspaceDiv.draggable = true; + workspaceDiv.dataset.uuid = uuid; + + workspaceDiv.addEventListener("dragstart", (e) => { + e.target.classList.add("dragging"); + e.dataTransfer.effectAllowed = "move"; + }); + + workspaceDiv.addEventListener("dragend", (e) => { + e.target.classList.remove("dragging"); + }); + if (theme?.type === "gradient" && theme.gradientColors?.length) { workspaceDiv.style.background = getGradientCSS(theme); } else { From 81c8e3314d4d86dcbcbca861c7ca17c1c8e5fb3a Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Thu, 17 Jul 2025 23:43:25 +0545 Subject: [PATCH 08/45] feat: workspace drag handle --- sections/workspace.js | 18 ++++++++++++------ style.css | 10 ++++++++++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index 43e7801..cc3e795 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -124,22 +124,27 @@ export const workspacesSection = { const workspaceDiv = parseElement( `
+ + ${icon} ${name}
`, ); - - workspaceDiv.draggable = true; + workspaceDiv.dataset.uuid = uuid; - workspaceDiv.addEventListener("dragstart", (e) => { - e.target.classList.add("dragging"); + const dragHandle = workspaceDiv.querySelector('.workspace-drag-handle'); + + dragHandle.addEventListener("dragstart", (e) => { + const workspaceElement = e.target.closest('.haven-workspace'); + workspaceElement.classList.add("dragging"); e.dataTransfer.effectAllowed = "move"; }); - workspaceDiv.addEventListener("dragend", (e) => { - e.target.classList.remove("dragging"); + dragHandle.addEventListener("dragend", (e) => { + const workspaceElement = e.target.closest('.haven-workspace'); + workspaceElement.classList.remove("dragging"); }); if (theme?.type === "gradient" && theme.gradientColors?.length) { @@ -262,3 +267,4 @@ export const workspacesSection = { return container; }, }; + diff --git a/style.css b/style.css index 9d25a0d..a1cf9fb 100644 --- a/style.css +++ b/style.css @@ -135,6 +135,7 @@ align-items: start !important; justify-content: start !important; padding: 10px !important; + position: relative !important; .tab-reset-pin-button { display: none; @@ -173,6 +174,15 @@ border-bottom:2px solid rgba(0,0,0,0.3) !important; } } + + .workspace-drag-handle{ + width: 50% !important; + height: 20px !important; + background: red !important; + position: absolute !important; + bottom:0 !important; + } + } } } From d263bf86ece131be16f59dd4d5cad594825e0729 Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Fri, 18 Jul 2025 00:15:07 +0545 Subject: [PATCH 09/45] feat: rename on double click --- sections/workspace.js | 70 ++++++++++++++++++++++++++++++++----------- 1 file changed, 52 insertions(+), 18 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index cc3e795..bbe8493 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -119,7 +119,6 @@ export const workspacesSection = { allWorkspaces.forEach((workspace) => { const { uuid, theme, name, icon } = workspace; - // TODO: make renamable in douple click // TODO: Icon picker const workspaceDiv = parseElement( `
@@ -183,24 +182,60 @@ export const workspacesSection = { menuPopup.openPopup(popupOpenButton, "after_start"); }); - // TODO: after rename on douple click is implimented, this will not prompt + const workspaceNameEl = workspaceDiv.querySelector(".workspace-name"); + + const enableRename = () => { + const originalName = workspace.name; + workspaceNameEl.contentEditable = true; + workspaceNameEl.style.cursor = "text"; + workspaceNameEl.focus(); + + const selection = window.getSelection(); + const range = document.createRange(); + range.selectNodeContents(workspaceNameEl); + selection.removeAllRanges(); + selection.addRange(range); + + const cleanup = () => { + workspaceNameEl.removeEventListener("blur", handleBlur); + workspaceNameEl.removeEventListener("keydown", handleKeyDown); + workspaceNameEl.contentEditable = false; + workspaceNameEl.style.cursor = ""; + }; + + const handleBlur = async () => { + cleanup(); + const newName = workspaceNameEl.textContent.trim(); + const currentWorkspace = gZenWorkspaces.getWorkspaceFromId(uuid); + if (currentWorkspace && newName && newName !== originalName) { + currentWorkspace.name = newName; + await gZenWorkspaces.saveWorkspace(currentWorkspace); + workspace.name = newName; + } else { + workspaceNameEl.textContent = originalName; + } + }; + + const handleKeyDown = (e) => { + if (e.key === "Enter") { + e.preventDefault(); + workspaceNameEl.blur(); + } else if (e.key === "Escape") { + e.preventDefault(); + workspaceNameEl.textContent = originalName; + workspaceNameEl.blur(); + } + }; + + workspaceNameEl.addEventListener("blur", handleBlur); + workspaceNameEl.addEventListener("keydown", handleKeyDown); + }; + + workspaceNameEl.addEventListener("dblclick", enableRename); + menuPopup .querySelector(".rename") - .addEventListener("click", async () => { - const workspace = gZenWorkspaces.getWorkspaceFromId(uuid); - if (workspace) { - const newName = prompt( - "Enter new name for workspace:", - workspace.name, - ); - if (newName?.trim()) { - workspace.name = newName.trim(); - await gZenWorkspaces.saveWorkspace(workspace); - workspaceDiv.querySelector(".workspace-name").textContent = - workspace.name; - } - } - }); + .addEventListener("click", enableRename); menuPopup .querySelector(".switch") @@ -267,4 +302,3 @@ export const workspacesSection = { return container; }, }; - From 8b32a89cf348ef0d1aea663220f0adebe9baaec1 Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Fri, 18 Jul 2025 01:28:37 +0545 Subject: [PATCH 10/45] refactor: formatting spacing --- sections/workspace.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index bbe8493..17bf68a 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -130,19 +130,21 @@ export const workspacesSection = {
`, ); - + workspaceDiv.dataset.uuid = uuid; - const dragHandle = workspaceDiv.querySelector('.workspace-drag-handle'); + const dragHandle = workspaceDiv.querySelector( + ".workspace-drag-handle", + ); dragHandle.addEventListener("dragstart", (e) => { - const workspaceElement = e.target.closest('.haven-workspace'); + const workspaceElement = e.target.closest(".haven-workspace"); workspaceElement.classList.add("dragging"); e.dataTransfer.effectAllowed = "move"; }); dragHandle.addEventListener("dragend", (e) => { - const workspaceElement = e.target.closest('.haven-workspace'); + const workspaceElement = e.target.closest(".haven-workspace"); workspaceElement.classList.remove("dragging"); }); @@ -230,7 +232,7 @@ export const workspacesSection = { workspaceNameEl.addEventListener("blur", handleBlur); workspaceNameEl.addEventListener("keydown", handleKeyDown); }; - + workspaceNameEl.addEventListener("dblclick", enableRename); menuPopup From b71587bbda6c02cd92df51f7da468a061f0501df Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Fri, 18 Jul 2025 01:35:11 +0545 Subject: [PATCH 11/45] feat: add icon picker --- sections/workspace.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/sections/workspace.js b/sections/workspace.js index 17bf68a..65f1100 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -119,7 +119,6 @@ export const workspacesSection = { allWorkspaces.forEach((workspace) => { const { uuid, theme, name, icon } = workspace; - // TODO: Icon picker const workspaceDiv = parseElement( `
@@ -137,6 +136,25 @@ export const workspacesSection = { ".workspace-drag-handle", ); + const iconEl = workspaceDiv.querySelector(".workspace-icon"); + iconEl.addEventListener("click", () => { + gZenEmojiPicker + .open(iconEl) + .then(async (newIcon) => { + console.log("Selected emoji:", newIcon); + iconEl.innerText = newIcon; + const currentWorkspace = + gZenWorkspaces.getWorkspaceFromId(uuid); + if (currentWorkspace && newIcon && newIcon !== icon) { + currentWorkspace.icon = newIcon; + await gZenWorkspaces.saveWorkspace(currentWorkspace); + } else { + workspaceNameEl.textContent = originalName; + } + }) + .catch((e) => console.error(e)); + }); + dragHandle.addEventListener("dragstart", (e) => { const workspaceElement = e.target.closest(".haven-workspace"); workspaceElement.classList.add("dragging"); From 771eb80c238a027f4e26f0e37ee3138c82b2c2f8 Mon Sep 17 00:00:00 2001 From: BibekBhusal0 Date: Fri, 18 Jul 2025 02:00:05 +0545 Subject: [PATCH 12/45] feat: copy url --- sections/workspace.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/sections/workspace.js b/sections/workspace.js index 65f1100..b9e81ce 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -296,6 +296,15 @@ export const workspacesSection = { ) .forEach((tabEl) => { const clonedTab = tabEl.cloneNode(true); + let url; + try { + const browser = gBrowser.getBrowserForTab(tabEl); + url = browser.currentURI.spec; + // save url in tab + clonedTab.setAttribute("data-url", url); + } catch (e) { + console.error("Could not get tab URL", e); + } if (clonedTab.hasAttribute("pinned")) { pinnedTabsContainer.appendChild(clonedTab); } else { @@ -311,6 +320,27 @@ export const workspacesSection = { } workspaceDiv.appendChild(contentDiv); + const closeButtons = + workspaceDiv.querySelectorAll(".tab-close-button"); + closeButtons.forEach((btn) => { + btn.addEventListener("click", (e) => { + e.stopPropagation(); + e.preventDefault(); + const tab = e.target.closest("tab.tabbrowser-tab"); + // get saved url + const url = tab.getAttribute("data-url"); + if (url) { + try { + navigator.clipboard.writeText(url); + gZenUIManager.showToast("zen-copy-current-url-confirmation"); + } catch { + (err) => { + console.error("Failed to copy URL:", err); + }; + } + } + }); + }); innerContainer.appendChild(workspaceDiv); }); }) From 499108b3be92a4a39bfbe5b922600bfd21614801 Mon Sep 17 00:00:00 2001 From: Anoms12 <117039159+Anoms12@users.noreply.github.com> Date: Thu, 17 Jul 2025 18:28:52 -0400 Subject: [PATCH 13/45] feat: add Theme Picker support to the context menu (solution was in welcome.mjs, as it uses an empy Picker for the first workspace, is has similar creation conditions as us) --- sections/workspace.js | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/sections/workspace.js b/sections/workspace.js index b9e81ce..a0ae236 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -277,6 +277,33 @@ export const workspacesSection = { } }); + menuPopup + .querySelector(".change-theme") + .addEventListener("click", async (event) => { + try { + // Switch to the selected workspace first + await gZenWorkspaces.changeWorkspaceWithID(uuid); + + // Create or select an anchor element for the popup + let anchor = workspaceDiv.querySelector('.haven-workspace-header'); + if (!anchor) { + // fallback: use the menu button itself + anchor = popupOpenButton; + } + + // Open the theme picker panel at the anchor + if (typeof PanelMultiView !== "undefined" && gZenThemePicker?.panel) { + PanelMultiView.openPopup(gZenThemePicker.panel, anchor, { + position: "bottomcenter topright", // or another position as needed + }); + } else { + throw new Error("PanelMultiView or gZenThemePicker.panel is not available"); + } + } catch (e) { + console.error("Error opening theme picker:", e); + } + }); + const contentDiv = parseElement( `
`, ); From f87bbd7f66dd3620c3a04183e067229402cd7928 Mon Sep 17 00:00:00 2001 From: Anoms12 <117039159+Anoms12@users.noreply.github.com> Date: Thu, 17 Jul 2025 18:35:37 -0400 Subject: [PATCH 14/45] feat: add support for Theme Picker (welcome.mjs) --- sections/workspace.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sections/workspace.js b/sections/workspace.js index a0ae236..7c9baa1 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -370,12 +370,13 @@ export const workspacesSection = { }); innerContainer.appendChild(workspaceDiv); }); + // After all workspaces are rendered, append the add workspace button + innerContainer.appendChild(addWorkspaceButton); }) .catch((error) => { console.error("[ZenHaven] Error building workspaces section:", error); }); - innerContainer.appendChild(addWorkspaceButton); return container; }, }; From 94e413e4f8aec7f4f5f8f8727f3a946760e00248 Mon Sep 17 00:00:00 2001 From: Anoms12 <117039159+Anoms12@users.noreply.github.com> Date: Thu, 17 Jul 2025 20:54:31 -0400 Subject: [PATCH 15/45] feat: move workspace drag handle to bottom of workspace container dom --- sections/workspace.js | 9 +++++---- style.css | 11 ++++++----- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index 7c9baa1..c99d854 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -122,8 +122,6 @@ export const workspacesSection = { const workspaceDiv = parseElement( `
- - ${icon} ${name}
@@ -132,8 +130,9 @@ export const workspacesSection = { workspaceDiv.dataset.uuid = uuid; - const dragHandle = workspaceDiv.querySelector( - ".workspace-drag-handle", + // Create the drag handle early so it can be referenced by event listeners + const dragHandle = parseElement( + `` ); const iconEl = workspaceDiv.querySelector(".workspace-icon"); @@ -347,6 +346,7 @@ export const workspacesSection = { } workspaceDiv.appendChild(contentDiv); + workspaceDiv.appendChild(dragHandle); const closeButtons = workspaceDiv.querySelectorAll(".tab-close-button"); closeButtons.forEach((btn) => { @@ -380,3 +380,4 @@ export const workspacesSection = { return container; }, }; + \ No newline at end of file diff --git a/style.css b/style.css index 9ba7334..32c8d17 100644 --- a/style.css +++ b/style.css @@ -173,12 +173,13 @@ } } - .workspace-drag-handle{ - width: 50% !important; + .workspace-drag-handle { + display: flex !important; + width: 50px !important; height: 20px !important; - background: red !important; - position: absolute !important; - bottom:0 !important; + background-color: red !important; + position: relative !important; + bottom: 0 !important; } } From 7783785cc6e092e9147337ba62e5b99dae16e76f Mon Sep 17 00:00:00 2001 From: Anoms12 <117039159+Anoms12@users.noreply.github.com> Date: Sat, 19 Jul 2025 12:40:59 -0400 Subject: [PATCH 16/45] feat: 1/2, refactor tabs to use Custom solution --- sections/workspace.js | 645 +++++++++++++++++++++++++++++++++--------- style.css | 198 +++++++++++-- 2 files changed, 695 insertions(+), 148 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index c99d854..e28ad1a 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -16,6 +16,32 @@ function getGradientCSS(theme) { return `linear-gradient(${angle}deg, ${stops})`; } +function isAncestor(child, ancestor) { + let node = child; + while (node) { + if (node === ancestor) return true; + node = node.parentNode; + } + return false; +} + +// Track original parents for zen-workspace elements +const zenWorkspaceOriginalParents = new Map(); + +function restoreZenWorkspace(uuid) { + const info = zenWorkspaceOriginalParents.get(uuid); + const zenWorkspaceEl = document.getElementById(uuid); + if (info && zenWorkspaceEl) { + if (info.nextSibling && info.parent.contains(info.nextSibling)) { + info.parent.insertBefore(zenWorkspaceEl, info.nextSibling); + } else { + info.parent.appendChild(zenWorkspaceEl); + } + zenWorkspaceOriginalParents.delete(uuid); + console.log(`[ZenHaven] Restored zen-workspace element for uuid: ${uuid} to its original parent.`); + } +} + export const workspacesSection = { id: "workspaces", label: "Workspaces", @@ -23,6 +49,14 @@ export const workspacesSection = { `, init: function() { + // Ensure every real Firefox tab has a unique id for drag-and-drop matching + if (typeof gBrowser !== 'undefined' && gBrowser.tabs) { + Array.from(gBrowser.tabs).forEach(tab => { + if (!tab.hasAttribute('id')) { + tab.setAttribute('id', 'zen-real-tab-' + Math.random().toString(36).slice(2)); + } + }); + } const container = parseElement( `
`, ); @@ -88,13 +122,7 @@ export const workspacesSection = { addWorkspaceButton.addEventListener("click", () => { try { if (typeof gZenWorkspaces?.openWorkspaceCreation === "function") { - console.log( - "[ZenHaven] Attempting to open workspace creation dialog...", - ); - - // close haven window.haven.destroyUI(); - gZenWorkspaces.openWorkspaceCreation(); } else { throw new Error( @@ -116,9 +144,9 @@ export const workspacesSection = { ._workspaces() .then(({ workspaces: allWorkspaces }) => { const allTabs = gZenWorkspaces.allStoredTabs || []; - - allWorkspaces.forEach((workspace) => { + allWorkspaces.forEach((workspace, idx) => { const { uuid, theme, name, icon } = workspace; + // Build a proxy UI for each workspace const workspaceDiv = parseElement( `
@@ -127,50 +155,30 @@ export const workspacesSection = {
`, ); - workspaceDiv.dataset.uuid = uuid; - // Create the drag handle early so it can be referenced by event listeners + // Drag handle const dragHandle = parseElement( `` ); - - const iconEl = workspaceDiv.querySelector(".workspace-icon"); - iconEl.addEventListener("click", () => { - gZenEmojiPicker - .open(iconEl) - .then(async (newIcon) => { - console.log("Selected emoji:", newIcon); - iconEl.innerText = newIcon; - const currentWorkspace = - gZenWorkspaces.getWorkspaceFromId(uuid); - if (currentWorkspace && newIcon && newIcon !== icon) { - currentWorkspace.icon = newIcon; - await gZenWorkspaces.saveWorkspace(currentWorkspace); - } else { - workspaceNameEl.textContent = originalName; - } - }) - .catch((e) => console.error(e)); - }); - dragHandle.addEventListener("dragstart", (e) => { const workspaceElement = e.target.closest(".haven-workspace"); workspaceElement.classList.add("dragging"); e.dataTransfer.effectAllowed = "move"; }); - dragHandle.addEventListener("dragend", (e) => { const workspaceElement = e.target.closest(".haven-workspace"); workspaceElement.classList.remove("dragging"); }); + // Theme background if (theme?.type === "gradient" && theme.gradientColors?.length) { workspaceDiv.style.background = getGradientCSS(theme); } else { workspaceDiv.style.background = "var(--zen-colors-border)"; } + // Proxy menu and actions const header = workspaceDiv.querySelector(".haven-workspace-header"); const popupOpenButton = parseElement( ` @@ -201,27 +209,24 @@ export const workspacesSection = { menuPopup.openPopup(popupOpenButton, "after_start"); }); + // Proxy rename const workspaceNameEl = workspaceDiv.querySelector(".workspace-name"); - const enableRename = () => { const originalName = workspace.name; workspaceNameEl.contentEditable = true; workspaceNameEl.style.cursor = "text"; workspaceNameEl.focus(); - const selection = window.getSelection(); const range = document.createRange(); range.selectNodeContents(workspaceNameEl); selection.removeAllRanges(); selection.addRange(range); - const cleanup = () => { workspaceNameEl.removeEventListener("blur", handleBlur); workspaceNameEl.removeEventListener("keydown", handleKeyDown); workspaceNameEl.contentEditable = false; workspaceNameEl.style.cursor = ""; }; - const handleBlur = async () => { cleanup(); const newName = workspaceNameEl.textContent.trim(); @@ -234,7 +239,6 @@ export const workspacesSection = { workspaceNameEl.textContent = originalName; } }; - const handleKeyDown = (e) => { if (e.key === "Enter") { e.preventDefault(); @@ -245,74 +249,48 @@ export const workspacesSection = { workspaceNameEl.blur(); } }; - workspaceNameEl.addEventListener("blur", handleBlur); workspaceNameEl.addEventListener("keydown", handleKeyDown); }; - workspaceNameEl.addEventListener("dblclick", enableRename); + menuPopup.querySelector(".rename").addEventListener("click", enableRename); - menuPopup - .querySelector(".rename") - .addEventListener("click", enableRename); - - menuPopup - .querySelector(".switch") - .addEventListener("click", async () => { - await gZenWorkspaces.changeWorkspaceWithID(uuid); - - // close haven - window.haven.destroyUI(); - }); - - menuPopup - .querySelector(".delete-workspace") - .addEventListener("click", async () => { - if ( - confirm(`Are you sure you want to delete workspace "${name}"?`) - ) { - await gZenWorkspaces.removeWorkspace(uuid); - workspaceDiv.remove(); - } - }); - - menuPopup - .querySelector(".change-theme") - .addEventListener("click", async (event) => { - try { - // Switch to the selected workspace first - await gZenWorkspaces.changeWorkspaceWithID(uuid); + // Proxy switch workspace + menuPopup.querySelector(".switch").addEventListener("click", async () => { + await gZenWorkspaces.changeWorkspaceWithID(uuid); + window.haven.destroyUI(); + }); - // Create or select an anchor element for the popup - let anchor = workspaceDiv.querySelector('.haven-workspace-header'); - if (!anchor) { - // fallback: use the menu button itself - anchor = popupOpenButton; - } + // Proxy delete workspace + menuPopup.querySelector(".delete-workspace").addEventListener("click", async () => { + if (confirm(`Are you sure you want to delete workspace "${name}"?`)) { + await gZenWorkspaces.removeWorkspace(uuid); + workspaceDiv.remove(); + } + }); - // Open the theme picker panel at the anchor - if (typeof PanelMultiView !== "undefined" && gZenThemePicker?.panel) { - PanelMultiView.openPopup(gZenThemePicker.panel, anchor, { - position: "bottomcenter topright", // or another position as needed - }); - } else { - throw new Error("PanelMultiView or gZenThemePicker.panel is not available"); - } - } catch (e) { - console.error("Error opening theme picker:", e); + // Proxy change theme + menuPopup.querySelector(".change-theme").addEventListener("click", async (event) => { + try { + await gZenWorkspaces.changeWorkspaceWithID(uuid); + let anchor = workspaceDiv.querySelector('.haven-workspace-header'); + if (!anchor) anchor = popupOpenButton; + if (typeof PanelMultiView !== "undefined" && gZenThemePicker?.panel) { + PanelMultiView.openPopup(gZenThemePicker.panel, anchor, { + position: "bottomcenter topright", + }); + } else { + throw new Error("PanelMultiView or gZenThemePicker.panel is not available"); } - }); - - const contentDiv = parseElement( - `
`, - ); - const pinnedTabsContainer = parseElement( - `
`, - ); - const regularTabsContainer = parseElement( - `
`, - ); + } catch (e) { + console.error("Error opening theme picker:", e); + } + }); + // Proxy tab list (visual only) + const contentDiv = parseElement(`
`); + const pinnedTabsContainer = parseElement(`
`); + const regularTabsContainer = parseElement(`
`); allTabs .filter( (tabEl) => @@ -321,63 +299,466 @@ export const workspacesSection = { !tabEl.hasAttribute("zen-essential"), ) .forEach((tabEl) => { - const clonedTab = tabEl.cloneNode(true); - let url; - try { - const browser = gBrowser.getBrowserForTab(tabEl); - url = browser.currentURI.spec; - // save url in tab - clonedTab.setAttribute("data-url", url); - } catch (e) { - console.error("Could not get tab URL", e); + // Proxy: show tab info with icon, title, and copy-link + const tabUrl = tabEl.linkedBrowser?.currentURI?.spec || tabEl.getAttribute('data-url') || tabEl.getAttribute('label') || ''; + const tabTitle = tabEl.getAttribute('label') || tabEl.getAttribute('title') || 'Tab'; + let faviconUrl = tabEl.getAttribute('image') || tabEl.getAttribute('icon') || ''; + if (!faviconUrl && tabUrl.startsWith('http')) { + faviconUrl = `https://www.google.com/s2/favicons?sz=32&domain_url=${encodeURIComponent(tabUrl)}`; + } + const tabProxy = parseElement(` +
+ ${faviconUrl ? `` : ''} + ${tabTitle} + +
+ `); + // Tab click: switch to this tab + tabProxy.addEventListener('click', (e) => { + if (e.target.classList.contains('copy-link')) return; + if (typeof gBrowser !== 'undefined' && gBrowser.selectedTab !== tabEl) { + gBrowser.selectedTab = tabEl; + } + }); + tabProxy.addEventListener('contextmenu', (e) => { + e.preventDefault(); + if (tabEl && typeof tabEl.dispatchEvent === 'function') { + const evt = new MouseEvent('contextmenu', { + bubbles: true, + cancelable: true, + view: window, + clientX: e.clientX, + clientY: e.clientY, + screenX: e.screenX, + screenY: e.screenY, + button: 2 + }); + tabEl.dispatchEvent(evt); + } + }); + tabProxy.querySelector('.copy-link').addEventListener('click', (e) => { + e.stopPropagation(); + if (tabUrl) { + navigator.clipboard.writeText(tabUrl).then(() => { + if (typeof gZenUIManager !== 'undefined' && gZenUIManager.showToast) { + gZenUIManager.showToast('zen-copy-current-url-confirmation'); + } + }); + } + }); + // --- Drag-and-drop logic for tabs --- + tabProxy.tabEl = tabEl; // Attach real tab reference + tabProxy.dataset.tabId = tabEl.getAttribute('id') || ''; + tabProxy.dataset.pinned = tabEl.hasAttribute('pinned') ? 'true' : 'false'; + tabProxy.dataset.workspaceUuid = uuid; + // Track original parent and index for restoration + let originalTabParent = null; + let originalTabIndex = null; + tabProxy.addEventListener('dragstart', (e) => { + tabProxy.classList.add('dragging'); + e.dataTransfer.effectAllowed = 'move'; + e.dataTransfer.setData('text/plain', tabProxy.dataset.tabId); + tabProxy.parentNode.classList.add('drag-source'); + contentDiv.classList.add('tab-drag-context'); + // Save original parent and index + originalTabParent = tabProxy.parentNode; + originalTabIndex = Array.from(tabProxy.parentNode.children).indexOf(tabProxy); + }); + tabProxy.addEventListener('dragend', (e) => { + tabProxy.classList.remove('dragging'); + document.querySelectorAll('.haven-workspace-pinned-tabs, .haven-workspace-regular-tabs').forEach(c => c.classList.remove('drag-over', 'drag-source')); + contentDiv.classList.remove('tab-drag-context'); + // If not in a valid container, restore to original position + const validContainers = [pinnedTabsContainer, regularTabsContainer]; + if (!validContainers.includes(tabProxy.parentNode)) { + if (originalTabParent && originalTabIndex !== null) { + const children = Array.from(originalTabParent.children); + if (children.length > originalTabIndex) { + originalTabParent.insertBefore(tabProxy, children[originalTabIndex]); + } else { + originalTabParent.appendChild(tabProxy); + } + } + } + originalTabParent = null; + originalTabIndex = null; + }); + // Prevent default drop on document/body to avoid accidental drops outside + if (!window.__zenTabDropPrevented) { + window.addEventListener('dragover', e => e.preventDefault()); + window.addEventListener('drop', e => e.preventDefault()); + window.__zenTabDropPrevented = true; + } + // Only allow drop on containers within this workspace + [pinnedTabsContainer, regularTabsContainer].forEach(container => { + container.addEventListener('dragover', (e) => { + // Only allow drop if this workspace is the drag context + if (!contentDiv.classList.contains('tab-drag-context')) return; + e.preventDefault(); + container.classList.add('drag-over'); + }); + container.addEventListener('dragleave', (e) => { + container.classList.remove('drag-over'); + }); + container.addEventListener('drop', async (e) => { + // Only allow drop if this workspace is the drag context + if (!contentDiv.classList.contains('tab-drag-context')) return; + e.preventDefault(); + container.classList.remove('drag-over'); + const dragging = container.querySelector('.dragging') || document.querySelector('.haven-tab.dragging'); + if (!dragging) return; + // Only allow drop if the tab belongs to this workspace + if (dragging.dataset.workspaceUuid !== uuid) return; + // Always use vertical position to determine drop location + const after = getTabAfterElement(container, e.clientY); + if (after == null) { + container.appendChild(dragging); + } else { + container.insertBefore(dragging, after); + } + // Update pin state if moved between containers + const isPinnedTarget = container === pinnedTabsContainer; + const tabEl = dragging.tabEl; + if (tabEl) { + if (isPinnedTarget) { + tabEl.setAttribute('pinned', 'true'); + } else { + tabEl.removeAttribute('pinned'); + } + } + // Update order in gZenWorkspaces + if (typeof gZenWorkspaces?.reorderTab === 'function') { + const newIndex = Array.from(container.children).indexOf(dragging); + await gZenWorkspaces.reorderTab(tabEl, newIndex, isPinnedTarget); + } + }); + }); + // Helper for reordering within container + function getTabAfterElement(container, y) { + const draggableTabs = [...container.querySelectorAll('.haven-tab:not(.dragging)')]; + return draggableTabs.reduce((closest, child) => { + const box = child.getBoundingClientRect(); + const offset = y - box.top - box.height / 2; + if (offset < 0 && offset > closest.offset) { + return { offset: offset, element: child }; + } else { + return closest; + } + }, { offset: Number.NEGATIVE_INFINITY }).element; + } + // --- End drag-and-drop logic --- + // --- Custom drag-and-drop logic for vertical tabs --- + function getAllTabProxies() { + return [ + ...pinnedTabsContainer.querySelectorAll('.haven-tab'), + ...regularTabsContainer.querySelectorAll('.haven-tab'), + ]; + } + + let isDragging = false; + let dragTab = null; + let dragStartY = 0; + let dragStartX = 0; + let dragOffsetY = 0; + let dragOffsetX = 0; + let dragMouseOffset = 0; + let placeholder = null; + let lastContainer = null; + + tabProxy.addEventListener('mousedown', (e) => { + if (e.button !== 0) return; // Only left click + // --- Only allow pinned tab reordering if workspace is active --- + const isPinned = tabEl && tabEl.hasAttribute('pinned'); + if (isPinned) { + // Find the workspace element for this tab + const workspaceEl = gZenWorkspaces.workspaceElement(uuid); + if (!workspaceEl || !workspaceEl.hasAttribute('active')) { + if (window.gZenUIManager && gZenUIManager.showToast) { + gZenUIManager.showToast('Pinned tab order can only be changed in the active workspace.'); + } else { + alert('Pinned tab order can only be changed in the active workspace.'); + } + return; + } + } + // --- Assign a unique, persistent id to the real tab element if missing --- + if (tabEl && !tabEl.hasAttribute('id')) { + tabEl.setAttribute('id', 'zen-real-tab-' + Math.random().toString(36).slice(2)); + } + e.preventDefault(); + isDragging = true; + dragTab = tabProxy; + const tabRect = tabProxy.getBoundingClientRect(); + dragStartY = tabRect.top; + dragStartX = tabRect.left; + dragMouseOffset = e.clientY - tabRect.top; + dragOffsetY = 0; + dragOffsetX = e.clientX - tabRect.left; + lastContainer = tabProxy.parentNode; + dragTab._dragSection = isPinned ? 'pinned' : 'regular'; + // --- Insert placeholder BEFORE moving tab out of DOM --- + placeholder = document.createElement('div'); + placeholder.className = 'haven-tab drag-placeholder'; + placeholder.style.height = `${tabProxy.offsetHeight}px`; + placeholder.style.width = `${tabProxy.offsetWidth}px`; + tabProxy.parentNode.insertBefore(placeholder, tabProxy); + // --- Now move tab out of flow: fixed position at its original screen position --- + tabProxy.style.position = 'fixed'; + tabProxy.style.top = `${tabRect.top}px`; + tabProxy.style.left = `${tabRect.left}px`; + tabProxy.style.width = `${tabRect.width}px`; + tabProxy.style.height = `${tabRect.height}px`; + tabProxy.style.zIndex = 1000; + tabProxy.style.pointerEvents = 'none'; + tabProxy.style.transition = 'none'; + tabProxy.classList.add('dragging-tab'); + document.body.appendChild(tabProxy); + document.body.style.userSelect = 'none'; + // Animate all tabs + getAllTabProxies().forEach(tab => { + if (tab !== dragTab) { + tab.style.transition = 'transform 0.18s cubic-bezier(.4,1.3,.5,1)'; + } + }); + window.addEventListener('mousemove', onDragMove); + window.addEventListener('mouseup', onDragEnd); + }); + + // Helper: Sync the custom UI with the real Firefox tab order + function syncCustomUIWithRealTabs() { + // For the active workspace only + const workspaceEl = gZenWorkspaces.workspaceElement(uuid); + if (!workspaceEl) return; + // Pinned + const pinnedContainer = workspaceEl.pinnedTabsContainer; + const realPinnedTabs = Array.from(gBrowser.tabs).filter(t => t.pinned && t.getAttribute('zen-workspace-id') === uuid); + realPinnedTabs.forEach(tab => { + const proxy = Array.from(pinnedContainer.querySelectorAll('.haven-tab')).find(t => t.tabEl && t.tabEl.getAttribute('id') === tab.getAttribute('id')); + if (proxy) pinnedContainer.appendChild(proxy); + }); + // Regular + const regularContainer = workspaceEl.tabsContainer; + const realRegularTabs = Array.from(gBrowser.tabs).filter(t => !t.pinned && t.getAttribute('zen-workspace-id') === uuid); + realRegularTabs.forEach(tab => { + const proxy = Array.from(regularContainer.querySelectorAll('.haven-tab')).find(t => t.tabEl && t.tabEl.getAttribute('id') === tab.getAttribute('id')); + if (proxy) regularContainer.appendChild(proxy); + }); + } + + function onDragMove(e) { + if (!isDragging || !dragTab) return; + // Move the tab visually with the mouse + const newY = e.clientY - dragMouseOffset; + const newX = dragStartX; // lock X axis + dragTab.style.top = `${newY}px`; + dragTab.style.left = `${newX}px`; + // --- Restrict drag to section --- + const section = dragTab._dragSection; + const sectionContainer = section === 'pinned' ? pinnedTabsContainer : regularTabsContainer; + // Only consider tabs in the same section + // Move placeholder to correct position within section + if (section === 'pinned') { + // Only allow placeholder to be inserted before/after other pinned tabs, never after the last pinned tab + const pinnedTabs = Array.from(pinnedTabsContainer.querySelectorAll('.haven-tab')); + let insertBefore = null; + for (let i = 0; i < pinnedTabs.length; i++) { + const tab = pinnedTabs[i]; + if (tab === dragTab) continue; + const rect = tab.getBoundingClientRect(); + if (e.clientY < rect.top + rect.height / 2) { + insertBefore = tab; + break; + } + } + // Prevent inserting after the last pinned tab (which would unpin) + if (!insertBefore) { + // Always insert before the last pinned tab (not after) + pinnedTabsContainer.insertBefore(placeholder, pinnedTabs[pinnedTabs.length - 1]); + } else { + pinnedTabsContainer.insertBefore(placeholder, insertBefore); + } + } else { + // Regular tabs logic (unchanged) + let insertBefore = null; + for (const tab of regularTabsContainer.querySelectorAll('.haven-tab')) { + if (tab === dragTab) continue; + const rect = tab.getBoundingClientRect(); + if (e.clientY < rect.top + rect.height / 2) { + insertBefore = tab; + break; + } + } + if (insertBefore) { + regularTabsContainer.insertBefore(placeholder, insertBefore); + } else { + regularTabsContainer.appendChild(placeholder); + } + } + // Animate other tabs to move out of the way + getAllTabProxies().forEach(tab => { + if (tab === dragTab) return; + tab.style.transition = 'transform 0.18s cubic-bezier(.4,1.3,.5,1)'; + tab.style.transform = ''; + if (tab.parentNode === sectionContainer) { + const tabRect = tab.getBoundingClientRect(); + const placeholderRect = placeholder.getBoundingClientRect(); + if (tabRect.top < placeholderRect.top && tabRect.bottom > newY) { + tab.style.transform = `translateY(-${placeholderRect.height}px)`; + } else if (tabRect.top > placeholderRect.top) { + tab.style.transform = `translateY(${placeholderRect.height}px)`; + } + } + }); } - if (clonedTab.hasAttribute("pinned")) { - pinnedTabsContainer.appendChild(clonedTab); + + function onDragEnd(e) { + if (!isDragging || !dragTab) return; + // Insert tab at placeholder + placeholder.parentNode.insertBefore(dragTab, placeholder); + // Restore tab's styles + dragTab.style.position = ''; + dragTab.style.top = ''; + dragTab.style.left = ''; + dragTab.style.width = ''; + dragTab.style.height = ''; + dragTab.style.zIndex = ''; + dragTab.style.pointerEvents = ''; + dragTab.style.transition = ''; + dragTab.classList.remove('dragging-tab'); + // Update pin state if moved between containers (should never happen now) + const isPinnedTarget = placeholder.parentNode === pinnedTabsContainer; + const tabEl = dragTab.tabEl; + // --- Ensure tabEl has a unique id --- + if (tabEl && !tabEl.getAttribute('id')) { + tabEl.setAttribute('id', 'zen-tab-' + Math.random().toString(36).slice(2)); + } + if (tabEl) { + if (isPinnedTarget) { + tabEl.setAttribute('pinned', 'true'); + } else { + tabEl.removeAttribute('pinned'); + } + } + // --- Update the underlying tab order in the workspace --- + // Always use the real tab's id for matching + function getTabIdList(container) { + return Array.from(container.querySelectorAll('.haven-tab')).map(t => t.tabEl && t.tabEl.getAttribute('id')).filter(Boolean); + } + // Only update the order within the section + let order, section; + if (isPinnedTarget) { + order = getTabIdList(pinnedTabsContainer); + section = 'pinned'; + } else { + order = getTabIdList(regularTabsContainer); + section = 'regular'; + } + // Debug log for tab order + console.log('[ZenHaven] New', section, 'tab order:', order); + // --- Update the real Firefox tab order using gBrowser.moveTabTo --- + // Note: Pinned tab order is global, not per workspace! + function reorderFirefoxPinnedTabs(order) { + // Get all real pinned tabs (global, not per workspace) + const allTabs = Array.from(gBrowser.tabs); + let pinnedTabs = allTabs.filter(t => t.pinned); + console.log('[ZenHaven] Real pinned tabs before reorder:', pinnedTabs.map(t => t.getAttribute('id'))); + // For each tab in the new order, move it to the correct index among pinned tabs + for (let i = 0; i < order.length; i++) { + // Always match by the real tab's id + const tab = allTabs.find(t => t.getAttribute('id') === order[i]); + if (tab && !tab.pinned) { + console.log(`[ZenHaven] Pinning tab ${tab.getAttribute('id')}`); + gBrowser.pinTab(tab); + } + // Always move to index i among pinned tabs + if (tab && pinnedTabs[i] !== tab) { + console.log(`[ZenHaven] Moving tab ${tab.getAttribute('id')} to pinned index ${i}`); + gBrowser.moveTabTo(tab, i); + // After move, update pinnedTabs to reflect the new order + pinnedTabs = Array.from(gBrowser.tabs).filter(t => t.pinned); + console.log('[ZenHaven] Real pinned tabs after move:', pinnedTabs.map(t => t.getAttribute('id'))); + } + } + // Final pinned tab order + pinnedTabs = Array.from(gBrowser.tabs).filter(t => t.pinned); + console.log('[ZenHaven] Final real pinned tab order:', pinnedTabs.map(t => t.getAttribute('id'))); + } + function reorderFirefoxRegularTabs(order) { + const allTabs = Array.from(gBrowser.tabs); + const pinnedCount = gBrowser.tabs.filter(t => t.pinned).length; + for (let i = 0; i < order.length; i++) { + // Always match by the real tab's id + const tab = allTabs.find(t => t.getAttribute('id') === order[i]); + if (tab && tab.pinned) gBrowser.unpinTab(tab); + if (tab) gBrowser.moveTabTo(tab, pinnedCount + i); + } + } + if (section === 'pinned') { + reorderFirefoxPinnedTabs(order); + } else { + reorderFirefoxRegularTabs(order); + } + // --- End Firefox tab order update --- + if (typeof gZenWorkspaces?.reorderTabsInWorkspace === 'function') { + if (isPinnedTarget) { + gZenWorkspaces.reorderTabsInWorkspace(uuid, order, getTabIdList(regularTabsContainer)); + } else { + gZenWorkspaces.reorderTabsInWorkspace(uuid, getTabIdList(pinnedTabsContainer), order); + } + } else if (typeof gZenWorkspaces?.reorderTab === 'function') { + const newIndex = Array.from(placeholder.parentNode.children).indexOf(dragTab); + gZenWorkspaces.reorderTab(tabEl, newIndex, isPinnedTarget); + } + document.body.style.userSelect = ''; + if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder); + getAllTabProxies().forEach(tab => { + tab.style.transition = ''; + tab.style.transform = ''; + }); + // --- Always sync the custom UI with the real tab order after a move --- + setTimeout(syncCustomUIWithRealTabs, 0); + isDragging = false; + dragTab = null; + placeholder = null; + window.removeEventListener('mousemove', onDragMove); + window.removeEventListener('mouseup', onDragEnd); + } + // --- End custom drag-and-drop logic --- + if (tabEl.hasAttribute("pinned")) { + pinnedTabsContainer.appendChild(tabProxy); } else { - regularTabsContainer.appendChild(clonedTab); + regularTabsContainer.appendChild(tabProxy); } }); - if (pinnedTabsContainer.hasChildNodes()) { contentDiv.appendChild(pinnedTabsContainer); } if (regularTabsContainer.hasChildNodes()) { contentDiv.appendChild(regularTabsContainer); } - workspaceDiv.appendChild(contentDiv); workspaceDiv.appendChild(dragHandle); - const closeButtons = - workspaceDiv.querySelectorAll(".tab-close-button"); - closeButtons.forEach((btn) => { - btn.addEventListener("click", (e) => { - e.stopPropagation(); - e.preventDefault(); - const tab = e.target.closest("tab.tabbrowser-tab"); - // get saved url - const url = tab.getAttribute("data-url"); - if (url) { - try { - navigator.clipboard.writeText(url); - gZenUIManager.showToast("zen-copy-current-url-confirmation"); - } catch { - (err) => { - console.error("Failed to copy URL:", err); - }; - } - } - }); - }); innerContainer.appendChild(workspaceDiv); }); - // After all workspaces are rendered, append the add workspace button innerContainer.appendChild(addWorkspaceButton); }) .catch((error) => { console.error("[ZenHaven] Error building workspaces section:", error); }); - return container; }, }; + +// Hook into UI destroy/cleanup (example, adapt to your actual destroy logic) +if (window.haven && typeof window.haven.destroyUI === 'function') { + const originalDestroyUI = window.haven.destroyUI; + window.haven.destroyUI = function(...args) { + // Find the workspaces container and call cleanup + const container = document.getElementById('haven-workspace-outer-container')?.parentNode; + if (container && typeof container._restoreZenWorkspaces === 'function') { + container._restoreZenWorkspaces(); + } + return originalDestroyUI.apply(this, args); + }; +} \ No newline at end of file diff --git a/style.css b/style.css index 32c8d17..a648016 100644 --- a/style.css +++ b/style.css @@ -1,3 +1,38 @@ +.haven-tab { + display: flex; + align-items: center; + height: 36px; + border-radius: var(--border-radius-medium); + color: var(--tab-selected-textcolor); + background-color: transparent; + &:hover { + background-color: var(--tab-hover-background-color); + } + .tab-icon { + height: 16px; + width: 16px; + margin-left: 2.5px; + margin-right: 10px; + } + + .tab-title { + font-size: 14px; + white-space: nowrap !important; + overflow: hidden; + text-overflow: ellipsis; + max-width: 67% !important; + } + + + .copy-link { + margin: 5px; + margin-left: auto; + border: none; + -moz-appearance: none; + background-color: none !important; + } +} + :root:has(#navigator-toolbox[haven]) { #custom-toolbar { height: 100%; @@ -85,6 +120,8 @@ cursor: auto; transition: background 0.2s; margin-left: auto; + box-shadow: 0 3px 2px rgba(0, 0, 0, 0.35); + &:hover { background: light-dark(color-mix(in srgb, var(--zen-primary-color) 30%, white 70%), color-mix(in srgb, var(--zen-primary-color) 30%, black 70%)); } @@ -105,28 +142,24 @@ /* left */ inset 0 0 20px rgba(0, 0, 0, 0.5), /* bottom */ inset 0 0 20px rgba(0, 0, 0, 0.5); /* top */ max-height:94vh !important; - - &[haven-workspaces] { - div { - display: flex !important; - align-items: center !important; - height: 100%; - gap: 60px; - } - } #haven-workspace-outer-container{ width: 100% !important; overflow-x: auto !important; position: relative !important; } - #haven-workspace-inner-container{ - gap:30px !important; + + #haven-workspace-inner-container { + display: flex !important; + align-items: center !important; + height: 100%; + gap: 60px; } + .haven-workspace { height: 85% !important; - min-width: 225px; + min-width: 200px; background-color: var(--zen-primary-color); border-radius: 8px; display: flex; @@ -144,6 +177,9 @@ } .haven-workspace-header { + width: 100%; + display: flex; + flex-direction: row; height: fit-content !important; margin: 2px !important; @@ -155,6 +191,10 @@ font-size: 16px !important; margin-right: 10px !important; } + + .haven-workspace-options { + margin-left: auto !important; + } } .haven-workspace-content { @@ -174,10 +214,11 @@ } .workspace-drag-handle { + margin-top: auto; display: flex !important; - width: 50px !important; - height: 20px !important; - background-color: red !important; + width: 60% !important; + height: 3px !important; + background-color: light-dark(#333, #fff) !important; position: relative !important; bottom: 0 !important; } @@ -221,6 +262,131 @@ width: 100%; } +/* New tab elements styling */ +.haven-tab-item { + display: flex; + align-items: center; + justify-content: space-between; + padding: 6px 8px; + margin: 2px 0; + background: var(--tab-hover-background-color); + border-radius: 4px; + cursor: pointer; + transition: background-color 0.2s; + border-left: 3px solid transparent; +} + +.haven-tab-item:hover { + background: var(--tab-selected-background-color); +} + +.haven-tab-item[data-pinned="true"] { + background: var(--zen-primary-color); + color: white; +} + +.haven-tab-content { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + min-width: 0; +} + +.haven-tab-favicon { + width: 16px; + height: 16px; + flex-shrink: 0; +} + +.haven-tab-title { + font-size: 12px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + flex: 1; +} + +.haven-tab-actions { + display: flex; + gap: 4px; + opacity: 0; + transition: opacity 0.2s; +} + +.haven-tab-item:hover .haven-tab-actions { + opacity: 1; +} + +.haven-tab-copy-url, +.haven-tab-close { + background: none; + border: none; + color: inherit; + cursor: pointer; + padding: 2px 4px; + border-radius: 2px; + font-size: 10px; + transition: background-color 0.2s; +} + +.haven-tab-copy-url:hover, +.haven-tab-close:hover { + background: rgba(255, 255, 255, 0.2); +} + +/* Tab groups styling */ +.haven-tab-group { + margin: 8px 0; + border: 1px solid var(--zen-colors-border); + border-radius: 6px; + overflow: hidden; +} + +.haven-tab-group-header { + display: flex; + align-items: center; + gap: 8px; + padding: 6px 8px; + background: var(--tab-hover-background-color); + border-bottom: 1px solid var(--zen-colors-border); +} + +.haven-tab-group-color { + width: 12px; + height: 12px; + border-radius: 50%; + flex-shrink: 0; +} + +.haven-tab-group-title { + font-size: 12px; + font-weight: 500; + flex: 1; +} + +.haven-tab-group-count { + font-size: 10px; + color: var(--zen-colors-text-secondary); + background: var(--zen-colors-border); + padding: 2px 6px; + border-radius: 10px; +} + +.haven-tab-group-tabs { + padding: 4px; +} + +/* Empty state styling */ +.haven-workspace-empty { + display: flex; + align-items: center; + justify-content: center; + padding: 20px; + color: var(--zen-colors-text-secondary); + font-style: italic; +} + /* --- Downloads --- */ .haven-downloads-container { display: flex; @@ -1032,4 +1198,4 @@ font-size: 12px; color: var(--toolbar-color); opacity: 0.7; -} +} \ No newline at end of file From 3765ee783f9ac4b34f36f370745b9ffe9f16f412 Mon Sep 17 00:00:00 2001 From: Anoms12 <117039159+Anoms12@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:45:36 -0400 Subject: [PATCH 17/45] fix: switch user to workspace when tryign to drag tabs (has issues) --- sections/workspace.js | 34 +++++++++++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index e28ad1a..13bfbae 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -465,10 +465,30 @@ export const workspacesSection = { let placeholder = null; let lastContainer = null; - tabProxy.addEventListener('mousedown', (e) => { + tabProxy.addEventListener('mousedown', async (e) => { if (e.button !== 0) return; // Only left click - // --- Only allow pinned tab reordering if workspace is active --- const isPinned = tabEl && tabEl.hasAttribute('pinned'); + const workspaceEl = gZenWorkspaces.workspaceElement(uuid); + if (workspaceEl && !workspaceEl.hasAttribute('active')) { + // Switch to the workspace, then re-trigger drag + await gZenWorkspaces.changeWorkspaceWithID(uuid); + // Optionally, re-open Haven UI if it closes on workspace switch + if (window.haven && typeof window.haven.initializeUI === 'function' && !window.haven.uiInitialized) { + window.haven.initializeUI(); + } + // Simulate another mousedown to start drag after switch + setTimeout(() => { + tabProxy.dispatchEvent(new MouseEvent('mousedown', { + bubbles: true, + cancelable: true, + clientX: e.clientX, + clientY: e.clientY, + button: 0 + })); + }, 100); // Give the UI a moment to update + return; + } + // --- Only allow pinned tab reordering if workspace is active --- if (isPinned) { // Find the workspace element for this tab const workspaceEl = gZenWorkspaces.workspaceElement(uuid); @@ -694,7 +714,15 @@ export const workspacesSection = { } } if (section === 'pinned') { - reorderFirefoxPinnedTabs(order); + // Update the workspace's pinned tab order in the data model + if (typeof gZenWorkspaces?.updateWorkspacePinnedOrder === 'function') { + gZenWorkspaces.updateWorkspacePinnedOrder(uuid, order); + } + // If this workspace is active, also update the real tab strip + const workspaceEl = gZenWorkspaces.workspaceElement(uuid); + if (workspaceEl && workspaceEl.hasAttribute('active')) { + reorderFirefoxPinnedTabs(order); + } } else { reorderFirefoxRegularTabs(order); } From b6b6a7bdf716c97dc0e052a3033e9c043786269e Mon Sep 17 00:00:00 2001 From: Anoms12 <117039159+Anoms12@users.noreply.github.com> Date: Sat, 19 Jul 2025 16:52:58 -0400 Subject: [PATCH 18/45] feat: update some small stuff --- sections/workspace.js | 150 ++++++++++++++++++++++++++---------------- style.css | 4 ++ 2 files changed, 99 insertions(+), 55 deletions(-) diff --git a/sections/workspace.js b/sections/workspace.js index 13bfbae..ac5c438 100644 --- a/sections/workspace.js +++ b/sections/workspace.js @@ -310,7 +310,12 @@ export const workspacesSection = {
${faviconUrl ? `` : ''} ${tabTitle} - +
`); // Tab click: switch to this tab @@ -338,12 +343,21 @@ export const workspacesSection = { }); tabProxy.querySelector('.copy-link').addEventListener('click', (e) => { e.stopPropagation(); + console.log(`[ZenHaven] Copying URL for tab: ${tabTitle} (${tabUrl})`); if (tabUrl) { - navigator.clipboard.writeText(tabUrl).then(() => { - if (typeof gZenUIManager !== 'undefined' && gZenUIManager.showToast) { - gZenUIManager.showToast('zen-copy-current-url-confirmation'); - } - }); + // Try modern clipboard API first + if (navigator.clipboard && navigator.clipboard.writeText) { + navigator.clipboard.writeText(tabUrl).then(() => { + if (typeof gZenUIManager !== 'undefined' && gZenUIManager.showToast) { + gZenUIManager.showToast('zen-copy-current-url-confirmation'); + } + }).catch(() => { + // Fallback if clipboard API fails + console.error(`[ZenHaven] Clipboard API failed, falling back to execCommand for tab: ${tabTitle} (${tabUrl})`); + }); + } else { + console.error(`[ZenHaven] Clipboard API failed, falling back to execCommand for tab: ${tabTitle} (${tabUrl})`); + } } }); // --- Drag-and-drop logic for tabs --- @@ -464,19 +478,20 @@ export const workspacesSection = { let dragMouseOffset = 0; let placeholder = null; let lastContainer = null; + let dragHoldTimeout = null; + // --- Drag-and-drop logic for tabProxy --- tabProxy.addEventListener('mousedown', async (e) => { if (e.button !== 0) return; // Only left click + if (e.target.closest('.copy-link')) return; // Don't start drag on copy-link button const isPinned = tabEl && tabEl.hasAttribute('pinned'); const workspaceEl = gZenWorkspaces.workspaceElement(uuid); if (workspaceEl && !workspaceEl.hasAttribute('active')) { // Switch to the workspace, then re-trigger drag await gZenWorkspaces.changeWorkspaceWithID(uuid); - // Optionally, re-open Haven UI if it closes on workspace switch if (window.haven && typeof window.haven.initializeUI === 'function' && !window.haven.uiInitialized) { window.haven.initializeUI(); } - // Simulate another mousedown to start drag after switch setTimeout(() => { tabProxy.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, @@ -485,12 +500,10 @@ export const workspacesSection = { clientY: e.clientY, button: 0 })); - }, 100); // Give the UI a moment to update + }, 100); return; } - // --- Only allow pinned tab reordering if workspace is active --- if (isPinned) { - // Find the workspace element for this tab const workspaceEl = gZenWorkspaces.workspaceElement(uuid); if (!workspaceEl || !workspaceEl.hasAttribute('active')) { if (window.gZenUIManager && gZenUIManager.showToast) { @@ -501,47 +514,59 @@ export const workspacesSection = { return; } } - // --- Assign a unique, persistent id to the real tab element if missing --- if (tabEl && !tabEl.hasAttribute('id')) { tabEl.setAttribute('id', 'zen-real-tab-' + Math.random().toString(36).slice(2)); } e.preventDefault(); - isDragging = true; - dragTab = tabProxy; - const tabRect = tabProxy.getBoundingClientRect(); - dragStartY = tabRect.top; - dragStartX = tabRect.left; - dragMouseOffset = e.clientY - tabRect.top; - dragOffsetY = 0; - dragOffsetX = e.clientX - tabRect.left; - lastContainer = tabProxy.parentNode; - dragTab._dragSection = isPinned ? 'pinned' : 'regular'; - // --- Insert placeholder BEFORE moving tab out of DOM --- - placeholder = document.createElement('div'); - placeholder.className = 'haven-tab drag-placeholder'; - placeholder.style.height = `${tabProxy.offsetHeight}px`; - placeholder.style.width = `${tabProxy.offsetWidth}px`; - tabProxy.parentNode.insertBefore(placeholder, tabProxy); - // --- Now move tab out of flow: fixed position at its original screen position --- - tabProxy.style.position = 'fixed'; - tabProxy.style.top = `${tabRect.top}px`; - tabProxy.style.left = `${tabRect.left}px`; - tabProxy.style.width = `${tabRect.width}px`; - tabProxy.style.height = `${tabRect.height}px`; - tabProxy.style.zIndex = 1000; - tabProxy.style.pointerEvents = 'none'; - tabProxy.style.transition = 'none'; - tabProxy.classList.add('dragging-tab'); - document.body.appendChild(tabProxy); - document.body.style.userSelect = 'none'; - // Animate all tabs - getAllTabProxies().forEach(tab => { - if (tab !== dragTab) { - tab.style.transition = 'transform 0.18s cubic-bezier(.4,1.3,.5,1)'; - } - }); - window.addEventListener('mousemove', onDragMove); - window.addEventListener('mouseup', onDragEnd); + let dragStarted = false; + dragHoldTimeout = setTimeout(() => { + dragStarted = true; + isDragging = true; + dragTab = tabProxy; + const tabRect = tabProxy.getBoundingClientRect(); + dragStartY = tabRect.top; + dragStartX = tabRect.left; + dragMouseOffset = e.clientY - tabRect.top; + dragOffsetY = 0; + dragOffsetX = e.clientX - tabRect.left; + lastContainer = tabProxy.parentNode; + dragTab._dragSection = isPinned ? 'pinned' : 'regular'; + // --- Insert placeholder BEFORE moving tab out of DOM --- + placeholder = document.createElement('div'); + placeholder.className = 'haven-tab drag-placeholder'; + placeholder.style.height = `${tabProxy.offsetHeight}px`; + placeholder.style.width = `${tabProxy.offsetWidth}px`; + tabProxy.parentNode.insertBefore(placeholder, tabProxy); + // --- Now move tab out of flow: fixed position at its original screen position --- + tabProxy.style.position = 'fixed'; + tabProxy.style.top = `${tabRect.top}px`; + tabProxy.style.left = `${tabRect.left}px`; + tabProxy.style.width = `${tabRect.width}px`; + tabProxy.style.height = `${tabRect.height}px`; + tabProxy.style.zIndex = 1000; + tabProxy.style.pointerEvents = 'none'; + tabProxy.style.transition = 'transform 0.18s cubic-bezier(.4,1.3,.5,1), scale 0.18s cubic-bezier(.4,1.3,.5,1)'; + tabProxy.style.transform = 'scale(0.92)'; + tabProxy.setAttribute('drag-tab', ''); + tabProxy.classList.add('dragging-tab'); + document.body.appendChild(tabProxy); + document.body.style.userSelect = 'none'; + getAllTabProxies().forEach(tab => { + if (tab !== dragTab) { + tab.style.transition = 'transform 0.18s cubic-bezier(.4,1.3,.5,1)'; + } + }); + window.addEventListener('mousemove', onDragMove); + window.addEventListener('mouseup', onDragEnd); + }, 500); + // If mouse is released before 0.5s, cancel drag + function cancelHold(e2) { + clearTimeout(dragHoldTimeout); + window.removeEventListener('mouseup', cancelHold); + window.removeEventListener('mouseleave', cancelHold); + } + window.addEventListener('mouseup', cancelHold); + window.addEventListener('mouseleave', cancelHold); }); // Helper: Sync the custom UI with the real Firefox tab order @@ -578,14 +603,14 @@ export const workspacesSection = { // Only consider tabs in the same section // Move placeholder to correct position within section if (section === 'pinned') { - // Only allow placeholder to be inserted before/after other pinned tabs, never after the last pinned tab const pinnedTabs = Array.from(pinnedTabsContainer.querySelectorAll('.haven-tab')); let insertBefore = null; for (let i = 0; i < pinnedTabs.length; i++) { const tab = pinnedTabs[i]; if (tab === dragTab) continue; const rect = tab.getBoundingClientRect(); - if (e.clientY < rect.top + rect.height / 2) { + // Use a small deadzone to avoid jitter + if (e.clientY < rect.top + rect.height / 2 - 2) { insertBefore = tab; break; } @@ -593,9 +618,13 @@ export const workspacesSection = { // Prevent inserting after the last pinned tab (which would unpin) if (!insertBefore) { // Always insert before the last pinned tab (not after) - pinnedTabsContainer.insertBefore(placeholder, pinnedTabs[pinnedTabs.length - 1]); + if (placeholder !== pinnedTabs[pinnedTabs.length - 1]) { + pinnedTabsContainer.insertBefore(placeholder, pinnedTabs[pinnedTabs.length - 1]); + } } else { - pinnedTabsContainer.insertBefore(placeholder, insertBefore); + if (placeholder !== insertBefore) { + pinnedTabsContainer.insertBefore(placeholder, insertBefore); + } } } else { // Regular tabs logic (unchanged) @@ -603,15 +632,19 @@ export const workspacesSection = { for (const tab of regularTabsContainer.querySelectorAll('.haven-tab')) { if (tab === dragTab) continue; const rect = tab.getBoundingClientRect(); - if (e.clientY < rect.top + rect.height / 2) { + if (e.clientY < rect.top + rect.height / 2 - 2) { insertBefore = tab; break; } } if (insertBefore) { - regularTabsContainer.insertBefore(placeholder, insertBefore); + if (placeholder !== insertBefore) { + regularTabsContainer.insertBefore(placeholder, insertBefore); + } } else { - regularTabsContainer.appendChild(placeholder); + if (placeholder !== regularTabsContainer.lastChild) { + regularTabsContainer.appendChild(placeholder); + } } } // Animate other tabs to move out of the way @@ -632,6 +665,10 @@ export const workspacesSection = { } function onDragEnd(e) { + if (dragHoldTimeout) { + clearTimeout(dragHoldTimeout); + dragHoldTimeout = null; + } if (!isDragging || !dragTab) return; // Insert tab at placeholder placeholder.parentNode.insertBefore(dragTab, placeholder); @@ -645,6 +682,8 @@ export const workspacesSection = { dragTab.style.pointerEvents = ''; dragTab.style.transition = ''; dragTab.classList.remove('dragging-tab'); + dragTab.removeAttribute('drag-tab'); + dragTab.style.transform = ''; // Update pin state if moved between containers (should never happen now) const isPinnedTarget = placeholder.parentNode === pinnedTabsContainer; const tabEl = dragTab.tabEl; @@ -739,6 +778,7 @@ export const workspacesSection = { } document.body.style.userSelect = ''; if (placeholder && placeholder.parentNode) placeholder.parentNode.removeChild(placeholder); + // After drop, reset all transforms and transitions immediately getAllTabProxies().forEach(tab => { tab.style.transition = ''; tab.style.transform = ''; diff --git a/style.css b/style.css index a648016..c20401e 100644 --- a/style.css +++ b/style.css @@ -8,6 +8,9 @@ &:hover { background-color: var(--tab-hover-background-color); } + &[drag-tab] { + background-color: var(--tab-hover-background-color); + } .tab-icon { height: 16px; width: 16px; @@ -144,6 +147,7 @@ max-height:94vh !important; #haven-workspace-outer-container{ + height: 100%; width: 100% !important; overflow-x: auto !important; position: relative !important; From 272eb237bad27cd799c01fd27bc245f0582b71e2 Mon Sep 17 00:00:00 2001 From: Anoms12 <117039159+Anoms12@users.noreply.github.com> Date: Sat, 19 Jul 2025 20:27:27 -0400 Subject: [PATCH 19/45] feat: add exit button (sorry it took so long) --- style.css | 18 ++++++++++++++++++ zenhaven.uc.js | 29 +++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/style.css b/style.css index c20401e..351a9b8 100644 --- a/style.css +++ b/style.css @@ -41,6 +41,24 @@ height: 100%; display: flex; flex-direction: column; + + #haven-exit-button { + height: fit-content; + width: fit-content; + padding: 7px; + border-radius: 5px; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + font-size: 12px; + color: var(--toolbar-color); + transition: background-color 0.2s; + + &:hover { + background: var(--toolbar-bgcolor); + } + } } #toolbar-header { diff --git a/zenhaven.uc.js b/zenhaven.uc.js index de3f429..f10a673 100644 --- a/zenhaven.uc.js +++ b/zenhaven.uc.js @@ -107,6 +107,9 @@ import { notesSection } from "./sections/notes.js"; // Create buttons from registered sections this.sections.forEach((section) => this.createNavButton(section)); + // Create exit button (always visible) + this.createExitButton(); + // Handle bottom buttons this.elements.bottomButtons = document.getElementById( "zen-sidebar-bottom-buttons" @@ -210,6 +213,28 @@ import { notesSection } from "./sections/notes.js"; this.elements.functionsContainer.appendChild(customDiv); } + createExitButton() { + // Remove existing exit button if it exists + const existingExitButton = this.elements.customToolbar.querySelector("#haven-exit-button"); + if (existingExitButton) { + existingExitButton.remove(); + } + + const exitButton = parseElement(`
+ ← Exit +
`); + + exitButton.addEventListener("click", () => { + // Close haven mode by removing the haven attribute + const toolbox = document.getElementById("navigator-toolbox"); + if (toolbox) { + toolbox.removeAttribute("haven"); + } + }); + + this.elements.customToolbar.appendChild(exitButton); + } + activateSection(id) { if (!this.uiInitialized) return; @@ -236,6 +261,8 @@ import { notesSection } from "./sections/notes.js"; this.elements.havenContainer.appendChild(contentElement); this.elements.havenContainer.style.display = "flex"; + + document.getElementById(`haven-${id}-button`)?.classList.add("active"); this.activeSectionId = id; } else { @@ -259,6 +286,8 @@ import { notesSection } from "./sections/notes.js"; delete oldSection.contentElement; } + + Array.from(this.elements.havenContainer.attributes) .filter((attr) => attr.name.startsWith("haven-")) .forEach((attr) => From 4e87b5b60a4d05f0f05c6a45726a92657408490b Mon Sep 17 00:00:00 2001 From: Anoms12 <117039159+Anoms12@users.noreply.github.com> Date: Sat, 19 Jul 2025 21:45:11 -0400 Subject: [PATCH 20/45] fix: move from attribute based open/close system to a more normal approch --- style.css | 41 +++++++------- zenhaven.uc.js | 142 ++++++++++++++++++++++++++----------------------- 2 files changed, 97 insertions(+), 86 deletions(-) diff --git a/style.css b/style.css index 351a9b8..81064cc 100644 --- a/style.css +++ b/style.css @@ -36,10 +36,9 @@ } } -:root:has(#navigator-toolbox[haven]) { - #custom-toolbar { + #zen-library-sidebar { height: 100%; - display: flex; + display: flex !important; flex-direction: column; #haven-exit-button { @@ -94,21 +93,23 @@ } .custom-button { - display: flex; - flex-direction: column; + width: 100px; height: 100px; - margin-top: auto; - margin-bottom: auto; - align-items: center; - justify-content: center; + margin-top: auto !important; + margin-bottom: auto !important; transition: transform 0.1s ease-in-out, background-color 0.2s ease; cursor: pointer; padding: 8px; border-radius: 4px; - position: relative; - - + position: relative; + display: flex !important; + flex-direction: column !important; + align-items: center !important; + justify-content: center !important; + .icon { + width: 24px; + } & .icon svg { height: 24px !important; } @@ -119,13 +120,16 @@ text-align: center; } &:hover { - background-color: var(--toolbarbutton-hover-background); + background-color: var(--toolbarbutton-hover-background) !important; } + .custom-button.active { + background-color: var(--toolbarbutton-active-background); + } + + } + } - .custom-button.active { - background-color: var(--toolbarbutton-active-background); - } -} + /* --- Workspaces --- */ .haven-workspace-add-button { @@ -148,13 +152,12 @@ } } -:root:has(#navigator-toolbox[haven]) { +:root:has(#navigator-toolbox) { #zen-haven-container { height: unset !important; overflow: hidden; padding: 20px !important; - display: flex !important; flex-direction: column !important; align-items: center !important; overflow-x: scroll !important; diff --git a/zenhaven.uc.js b/zenhaven.uc.js index f10a673..2e32113 100644 --- a/zenhaven.uc.js +++ b/zenhaven.uc.js @@ -23,6 +23,7 @@ import { notesSection } from "./sections/notes.js"; this.sections = new Map(); this.activeSectionId = null; this.uiInitialized = false; + this.isOpen = false; this.elements = { toolbox: null, customToolbar: null, @@ -30,6 +31,7 @@ import { notesSection } from "./sections/notes.js"; havenContainer: null, bottomButtons: null, mediaToolbar: null, + navbar: null, }; console.log("[ZenHaven] Core object created"); } @@ -69,25 +71,17 @@ import { notesSection } from "./sections/notes.js"; initializeUI() { console.log("[ZenHaven] Setting up UI..."); this.elements.toolbox = document.getElementById("navigator-toolbox"); - if ( - !this.elements.toolbox || - !this.elements.toolbox.hasAttribute("haven") - ) { - console.log( - "[ZenHaven] Toolbox not found or haven attribute is missing." - ); + this.elements.navbar = document.getElementById("nav-bar"); + + if (!this.elements.toolbox) { + console.log("[ZenHaven] Toolbox not found."); return; } - console.log("[ZenHaven] Haven attribute found, proceeding with UI setup"); - - // Hide all children except the toolbox itself - Array.from(this.elements.toolbox.children).forEach((child) => { - child.style.display = "none"; - }); + console.log("[ZenHaven] Setting up Haven UI"); // Create container for new UI elements - const customContainer = parseElement(`
+ const customContainer = parseElement(`