feat: New drag and drop promo for essentials, p=#11891

* feat: New drag and drop promo for essentials, b=no-bug, c=tabs, common, split-view, workspaces

* feat: Finish impl, b=no-bug, c=common, compact-mode, glance, split-view, tabs, vendor

* feat: Add a pref to disable it, b=no-bug, c=tabs

* fix: Fix split views having wrong color, b=no-bug, c=split-view

* chore: Finish suggestions, b=no-bug, c=tabs
This commit is contained in:
mr. m
2026-01-14 01:31:45 +01:00
committed by GitHub
parent 2b4e46ce4e
commit 58a745ec2f
24 changed files with 215 additions and 85 deletions

View File

@@ -75,6 +75,9 @@ zen-generic-manage = Manage
zen-generic-more = More
zen-generic-next = Next
zen-essentials-promo-label = Add to Essentials
zen-essentials-promo-sublabel = Keep your favorite tabs just a click away
# These labels will be used for the site data panel settings
zen-site-data-setting-allow = Allowed
zen-site-data-setting-block = Blocked

View File

@@ -14,6 +14,9 @@
- name: zen.tabs.essentials.max
value: 12
- name: zen.tabs.essentials.dnd-promo-enabled
value: true
- name: zen.tabs.show-newtab-vertical
value: true

View File

@@ -1,5 +1,5 @@
diff --git a/browser/components/tabbrowser/content/tabs.js b/browser/components/tabbrowser/content/tabs.js
index a61b4e7af40f1404bf3555a7011c6211de917635..bb3c822ad14c4ebf0b8792665ff8242f97e501d9 100644
index a61b4e7af40f1404bf3555a7011c6211de917635..69e3e360b3040dfcb4541bda57e88b0da1d59be3 100644
--- a/browser/components/tabbrowser/content/tabs.js
+++ b/browser/components/tabbrowser/content/tabs.js
@@ -240,7 +240,7 @@
@@ -90,7 +90,7 @@ index a61b4e7af40f1404bf3555a7011c6211de917635..bb3c822ad14c4ebf0b8792665ff8242f
+ tabs.splice(i, 1);
+ // add the tabs in the group to the list
+ tabs.splice(i, 0, ...tab.tabs);
+ } else if (tab.classList.contains("zen-tab-group-start")) {
+ } else if (!isTab(tab)) {
+ tabs.splice(i, 1);
+ }
}

View File

@@ -396,8 +396,7 @@
content: url("arrow-right.svg") !important;
}
#PlacesChevron,
#urlbar-go-button {
#PlacesChevron {
list-style-image: url("arrow-right.svg") !important;
}

View File

@@ -1359,7 +1359,6 @@ window.gZenVerticalTabsManager = {
gBrowser.setTabTitle(this._tabEdited);
}
// Maybe add some confetti here?!?
gZenUIManager.motion.animate(
this._tabEdited,
{

View File

@@ -242,9 +242,9 @@
:root[zen-single-toolbar="true"] & .urlbar-input-box,
&[zen-floating-urlbar="true"] .urlbar-input-box {
font-weight: 400;
@media (-moz-platform: windows) {
font-weight: 500;
font-weight: 500;
@media (-moz-platform: linux) {
font-weight: 400;
}
}
@@ -292,10 +292,6 @@
}
}
#urlbar-go-button {
display: none;
}
:root[zen-single-toolbar="true"] {
--urlbar-icon-border-radius: 8px !important;
--urlbar-inner-border-radius: var(--toolbarbutton-border-radius) !important;
@@ -724,7 +720,7 @@
* displayed anymore, since now zen displays
* them into a single, unified button */
#reader-mode-button,
#urlbar-go-button,
.urlbar-go-button,
#star-button-box,
#pageActionButton:not([open]) {
display: none !important;

View File

@@ -39,10 +39,6 @@
display: none !important;
}
body > #confetti {
z-index: 1;
}
/* Bookmarks */
#PersonalToolbar:not([collapsed]) {
min-height: 30px;

View File

@@ -152,7 +152,7 @@
);
/* Toolbar */
--tab-selected-color-scheme: inherit;
--tab-selected-color-scheme: inherit !important;
--tabstrip-inner-border: transparent;
--zen-toolbar-height: 38px;

View File

@@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-disable consistent-return */
const lazy = {};
XPCOMUtils.defineLazyPreferenceGetter(
@@ -369,6 +371,7 @@ window.gZenCompactModeManager = {
delete gZenVerticalTabsManager._hadSidebarCollapse;
this.sidebar.style.setProperty("--zen-sidebar-width", `${sidebarWidth}px`);
}
return sidebarWidth;
},
get canHideSidebar() {

View File

@@ -2,12 +2,15 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-disable consistent-return */
"use strict";
// Wrap in a block to prevent leaking to window scope.
{
const isTab = (element) => gBrowser.isTab(element);
const isTabGroupLabel = (element) => gBrowser.isTabGroupLabel(element);
const isEssentialsPromo = (element) => element?.tagName.toUpperCase() == "ZEN-ESSENTIALS-PROMO";
/**
* The elements in the tab strip from `this.ariaFocusableItems` that contain
@@ -33,8 +36,10 @@
*/
const elementToMove = (element) => {
if (
!element ||
element.closest(".zen-current-workspace-indicator") ||
element.hasAttribute("split-view-group")
element.hasAttribute("split-view-group") ||
isEssentialsPromo(element)
) {
return element;
}
@@ -75,6 +80,15 @@
"zen.tabs.dnd-switch-space-delay",
1000
);
ChromeUtils.defineESModuleGetters(
this,
{
createZenEssentialsPromo:
"chrome://browser/content/zen-components/ZenEssentialsPromo.mjs",
},
{ global: "current" }
);
}
init() {
@@ -97,7 +111,7 @@
const { offsetX, offsetY } = this.#getDragImageOffset(event, tab, draggingTabs);
const dragImage = this.#createDragImageForTabs(tab, draggingTabs);
this.originalDragImageArgs = [dragImage, offsetX, offsetY];
dt.setDragImage(...this.originalDragImageArgs);
dt.updateDragImage(...this.originalDragImageArgs);
}
#createDragImageForTabs(draggedTab, movingTabs) {
@@ -128,12 +142,12 @@
}
// Apply a transform translate to the tab in order to center it within the drag image
// based on the event coordinates.
if (!movingTabs.length > 1) {
if (movingTabs.length === 1) {
tabClone.style.transform = `translate(${(tabRect.width - dragData.offsetX) / 2}px, ${(tabRect.height - dragData.offsetY) / 2}px)`;
}
tabClone.setAttribute("drag-image", "true");
wrapper.appendChild(tabClone);
if (isTab(tabClone) && !tabClone.hasAttribute("zen-essential")) {
if (isTab(tabClone)) {
// We need to limit the label content so the drag image doesn't grow too big.
const label = tabClone.textLabel;
const tabLabelParentWidth = label.parentElement.getBoundingClientRect().width;
@@ -168,13 +182,12 @@
// eslint-disable-next-line complexity
_animateTabMove(event) {
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
if (event.target.closest("#zen-essentials")) {
if (event.target.closest("#zen-essentials") && !isEssentialsPromo(event.target)) {
if (!isTab(draggedTab)) {
this.clearDragOverVisuals();
return;
}
this.#animateVerticalPinnedGridDragOver(event);
return;
return this.#animateVerticalPinnedGridDragOver(event);
} else if (this._fakeEssentialTab) {
this.#makeDragImageNonEssential(event);
}
@@ -256,24 +269,6 @@
translate = Math.min(Math.max(translate, startBound), endBound);
}
if (!gBrowser.pinnedTabCount && !this._dragToPinPromoCard.shouldRender) {
let pinnedDropIndicatorMargin = parseFloat(
window.getComputedStyle(this._pinnedDropIndicator).marginInline
);
this._checkWithinPinnedContainerBounds({
firstMovingTabScreen,
lastMovingTabScreen,
pinnedTabsStartEdge: this._rtlMode
? endEdge(this._tabbrowserTabs.arrowScrollbox) + pinnedDropIndicatorMargin
: this[screenAxis],
pinnedTabsEndEdge: this._rtlMode
? endEdge(this._tabbrowserTabs)
: this._tabbrowserTabs.arrowScrollbox[screenAxis] - pinnedDropIndicatorMargin,
translate,
draggedTab,
});
}
dragData.translatePos = translate;
tabs = tabs.filter((t) => !movingTabsSet.has(t) || t == draggedTab);
@@ -798,6 +793,10 @@
}
handle_dragend(event) {
let currentEssenialContainer = gZenWorkspaces.getCurrentEssentialsContainer();
if (currentEssenialContainer?.essentialsPromo) {
currentEssenialContainer.essentialsPromo.remove();
}
this.ZenDragAndDropService.onDragEnd();
super.handle_dragend(event);
this.#removeDragOverBackground();
@@ -817,9 +816,14 @@
}
#applyDragOverBackground(element) {
if (this.#dragOverBackground && this.#lastDropTarget === element) {
if (this.#lastDropTarget === element) {
return false;
}
if (isEssentialsPromo(element)) {
element.setAttribute("dragover", "true");
this.#lastDropTarget = element;
return true;
}
const margin = 2;
const rect = window.windowUtils.getBoundsWithoutFlushing(element);
this.#dragOverBackground = document.createElement("div");
@@ -835,6 +839,9 @@
if (this.#dragOverBackground) {
this.#dragOverBackground.remove();
this.#dragOverBackground = null;
}
if (this.#lastDropTarget) {
this.#lastDropTarget.removeAttribute("dragover");
this.#lastDropTarget = null;
}
}
@@ -966,6 +973,10 @@
// eslint-disable-next-line complexity
#animateVerticalPinnedGridDragOver(event) {
let essentialsPromoStatus = this.createZenEssentialsPromo();
if (essentialsPromoStatus === "shown") {
return;
}
let draggedTab = event.dataTransfer.mozGetDataAt(TAB_DROP_TYPE, 0);
let dragData = draggedTab._dragData;
let movingTabs = dragData.movingTabs;
@@ -976,6 +987,9 @@
) {
return;
}
if (essentialsPromoStatus === "created") {
return;
}
if (!this._fakeEssentialTab) {
const numEssentials = gBrowser._numZenEssentials;
@@ -987,6 +1001,7 @@
event.target.closest(".zen-essentials-container").appendChild(this._fakeEssentialTab);
gZenWorkspaces.updateTabsContainers();
pinnedTabs.push(this._fakeEssentialTab);
this._fakeEssentialTab.getBoundingClientRect(); // Initialize layout
}
this.#makeDragImageEssential(event);
let tabsPerRow = 0;
@@ -1086,6 +1101,12 @@
translateY = screen - elementMoving.screenY - tabHeight / 2;
}
if (!usingFakeElement) {
for (let tab of movingTabs) {
tab.style.transform = `translate(${translateX}px, ${translateY}px)`;
}
}
dragData.translateX = translateX;
dragData.translateY = translateY;

View File

@@ -2,6 +2,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
/* eslint-disable consistent-return */
import { nsZenDOMOperatedFeature } from "chrome://browser/content/zen-components/ZenCommonUtils.mjs";
/**
@@ -885,7 +887,7 @@ class nsZenGlanceManager extends nsZenDOMOperatedFeature {
this.#animateSidebarButtons(sidebarButtons);
this.#animateParentBackgroundClose(browserSidebarContainer);
this.#executeClosingAnimation(setNewID, onTabClose);
return this.#executeClosingAnimation(setNewID, onTabClose);
}
/**

View File

@@ -2,6 +2,8 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* eslint-disable consistent-return */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
@@ -422,6 +424,10 @@ class nsZenWindowSync {
const isGroup = gBrowser.isTabGroup(aOriginalItem);
const isTab = !isGroup;
if (aOriginalItem.hasAttribute("zen-glance-tab")) {
return;
}
if (isTab) {
if (originalIsEssential !== targetIsEssential) {
if (originalIsEssential) {
@@ -964,7 +970,7 @@ class nsZenWindowSync {
// No need to sync icon changes for tabs that aren't active in this window.
return;
}
this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_ICON);
return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_ICON);
}
on_ZenTabLabelChanged(aEvent) {
@@ -972,7 +978,7 @@ class nsZenWindowSync {
// No need to sync label changes for tabs that aren't active in this window.
return;
}
this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_LABEL);
return this.#delegateGenericSyncEvent(aEvent, SYNC_FLAG_LABEL);
}
on_TabMove(aEvent) {

View File

@@ -2,6 +2,8 @@
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
/* eslint-disable consistent-return */
import { nsZenDOMOperatedFeature } from "chrome://browser/content/zen-components/ZenCommonUtils.mjs";
class nsSplitLeafNode {
@@ -365,7 +367,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
gBrowser.tabbox.appendChild(this.fakeBrowser);
this.fakeBrowser.setAttribute("side", side);
this._finishAllAnimatingPromise = Promise.all([
gZenUIManager.elementAnimate(
gZenUIManager.motion.animate(
gBrowser.tabbox,
side === "left"
? {
@@ -377,12 +379,11 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
paddingLeft: 0,
},
{
duration: 110,
duration: 0.1,
easing: "ease-out",
fill: "forwards",
}
),
gZenUIManager.elementAnimate(
gZenUIManager.motion.animate(
this.fakeBrowser,
{
width: [0, `${halfWidth - padding}px`],
@@ -393,9 +394,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
: {}),
},
{
duration: 110,
duration: 0.1,
easing: "ease-out",
fill: "forwards",
}
),
]);
@@ -454,7 +454,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
);
this._canDrop = false;
Promise.all([
gZenUIManager.elementAnimate(
gZenUIManager.motion.animate(
gBrowser.tabbox,
side === "left"
? {
@@ -464,12 +464,11 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
paddingRight: [`${halfWidth}px`, 0],
},
{
duration: 110,
duration: 0.1,
easing: "ease-out",
fill: "forwards",
}
),
gZenUIManager.elementAnimate(
gZenUIManager.motion.animate(
this.fakeBrowser,
{
width: [`${halfWidth - padding * 2}px`, 0],
@@ -480,9 +479,8 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
: {}),
},
{
duration: 110,
duration: 0.1,
easing: "ease-out",
fill: "forwards",
}
),
]).finally(() => {
@@ -755,6 +753,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
dragImageOffset = dragImageOffset * scale;
}
event.dataTransfer.setDragImage(toDrag, dragImageOffset, dragImageOffset);
return true;
};
onBrowserDragOver = (event) => {
@@ -1761,17 +1760,13 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
};
_maybeRemoveFakeBrowser(select = true) {
gBrowser.tabbox.removeAttribute("style");
this.tabBrowserPanel.removeAttribute("dragging-split");
if (this._dndElement) {
this._dndElement.remove();
delete this._dndElement;
}
if (this.fakeBrowser) {
gBrowser.tabbox.removeAttribute("style");
this.tabBrowserPanel.removeAttribute("dragging-split");
const tabboxAnimations = document.getElementById("tabbrowser-tabbox").getAnimations();
if (tabboxAnimations.length) {
tabboxAnimations.forEach((a) => a.cancel());
}
delete this._hasAnimated;
this.fakeBrowser.remove();
this.fakeBrowser = null;

View File

@@ -171,7 +171,6 @@
cursor: pointer;
appearance: none;
outline: none;
color: var(--button-primary-bgcolor);
border-top-left-radius: 0;
border-top-right-radius: 0;

View File

@@ -0,0 +1,71 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const TAG_NAME = "zen-essentials-promo";
// Even though its costly, we need to update the pinned height
// whenever the promo is added or removed, to avoid any flickering.
function updatePinnedHeight() {
gZenWorkspaces.updateTabsContainers();
}
class nsZenEssentialsPromo extends MozXULElement {
#hasConnected = false;
static markup = `
<image src="${gZenEmojiPicker.getSVGURL("heart.svg")}" />
<label data-l10n-id="zen-essentials-promo-label" class="zen-essentials-promo-title"></label>
<label data-l10n-id="zen-essentials-promo-sublabel" class="zen-essentials-promo-sublabel"></label>
`;
connectedCallback() {
if (this.delayConnectedCallback() || this.#hasConnected) {
return;
}
this.appendChild(this.constructor.fragment);
this.classList.add("zen-drop-target");
this.#hasConnected = true;
}
remove() {
const section = this.parentElement;
if (section) {
delete section.essentialsPromo;
}
super.remove();
updatePinnedHeight();
}
}
/**
* Create and append the Zen Essentials promo element to the given container.
*
* @param {number|undefined} container - The container to append the promo to.
* If undefined, appends to the current workspace's tab strip.
* @returns {"created"|"shown"|false} - "created" if the promo was created and appended,
* "exists" if the promo already exists, or false if the section is not empty.
*/
export function createZenEssentialsPromo(container = undefined) {
if (!Services.prefs.getBoolPref("zen.tabs.essentials.dnd-promo-enabled", true)) {
return false;
}
if (container === undefined) {
container = gZenWorkspaces.getCurrentSpaceContainerId();
}
const section = gZenWorkspaces.getEssentialsSection(container);
if (!section || section.essentialsPromo) {
return "shown";
}
if (section.children.length) {
return false;
}
const element = document.createXULElement(TAG_NAME);
section.appendChild(element);
section.essentialsPromo = element;
updatePinnedHeight();
return "created";
}
customElements.define(TAG_NAME, nsZenEssentialsPromo);

View File

@@ -3,5 +3,6 @@
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
content/browser/zen-components/ZenPinnedTabManager.mjs (../../zen/tabs/ZenPinnedTabManager.mjs)
content/browser/zen-components/ZenEssentialsPromo.mjs (../../zen/tabs/ZenEssentialsPromo.mjs)
* content/browser/zen-styles/zen-tabs.css (../../zen/tabs/zen-tabs.css)
content/browser/zen-styles/zen-tabs/vertical-tabs.css (../../zen/tabs/zen-tabs/vertical-tabs.css)

View File

@@ -0,0 +1,46 @@
/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
zen-essentials-promo {
background: color-mix(in srgb, var(--zen-primary-color) 40%, transparent);
border: 1px solid var(--zen-primary-color);
border-radius: var(--border-radius-medium);
justify-content: center;
align-items: center;
padding: 1.1rem 0;
flex-direction: column;
margin: 2px;
text-align: center;
&:not([dragover="true"]) {
background: color-mix(in srgb, var(--zen-primary-color) 60%, transparent);
outline: 1px dashed var(--zen-primary-color);
}
:root:not([zen-sidebar-expanded="true"]) & {
display: none !important;
}
& image {
-moz-context-properties: fill;
fill: currentColor;
width: 18px;
}
& > * {
pointer-events: none;
max-width: 80%;
}
& .zen-essentials-promo-title {
font-weight: 600;
}
& .zen-essentials-promo-sublabel {
opacity: 0.8;
font-size: smaller;
}
}

View File

@@ -3,7 +3,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
/* Styles for both vertical and horizontal tabs */
@import url('chrome://browser/content/zen-styles/zen-tabs/vertical-tabs.css');
#zen-tabbox-wrapper {
@@ -125,4 +125,6 @@
}
}
}
}
}
%include zen-essentials-promo.css

View File

@@ -1102,6 +1102,7 @@
.zen-essentials-container {
overflow: hidden;
min-height: 2px;
gap: 4px;
transition:
max-height 0.3s ease-out,

1
src/zen/vendor/jar.inc.mn generated vendored
View File

@@ -2,5 +2,4 @@
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
content/browser/zen-vendor/tsparticles.confetti.bundle.min.js (../../zen/vendor/tsparticles.confetti.bundle.min.js)
content/browser/zen-vendor/motion.min.mjs (../../zen/vendor/motion.min.mjs)

File diff suppressed because one or more lines are too long

View File

@@ -5,10 +5,6 @@
class nsZenWorkspaceIcons extends MozXULElement {
#hasConnected = false;
constructor() {
super();
}
connectedCallback() {
if (this.delayConnectedCallback() || this.#hasConnected) {
return;

View File

@@ -2954,15 +2954,6 @@ class nsZenWorkspaces {
return this.pinnedTabsContainer.children.length - 1;
}
get allWorkspaceTabs() {
const currentWorkspace = this.activeWorkspace;
return this.allStoredTabs.filter(
(tab) =>
tab.hasAttribute("zen-essential") ||
tab.getAttribute("zen-workspace-id") === currentWorkspace
);
}
reorganizeTabsAfterWelcome() {
const children = gBrowser.tabContainer.arrowScrollbox.children;
const remainingTabs = Array.from(children).filter((child) => gBrowser.isTab(child));

View File

@@ -156,7 +156,6 @@
/* Mark workspaces indicator */
.zen-current-workspace-indicator {
--indicator-gap: 10px;
margin-top: 1px;
padding: calc(2px + var(--tab-inline-padding) + var(--zen-toolbox-padding));
font-weight: 500;
position: relative;
@@ -248,6 +247,10 @@
border-radius: 99px;
font-size: 10px;
}
:root:not([zen-unsynced-window]) & {
flex: 1;
}
}
.zen-workspaces-actions {