From 250d0c641cd7ccd0f9925272926e0a774fe79c37 Mon Sep 17 00:00:00 2001 From: "mr. m" Date: Tue, 24 Feb 2026 00:56:01 +0100 Subject: [PATCH] feat: Add feature callout for live folders, b=no-bug, c=folders, common --- .../browser/browser/zen-live-folders.ftl | 3 + .../firefox/native_macos_popovers.patch | 76 +++++++++++++--- src/zen/common/styles/zen-animations.css | 9 ++ .../common/styles/zen-single-components.css | 12 +++ src/zen/folders/zen-folders.css | 8 +- .../ZenLiveFoldersManager.sys.mjs | 91 ++++++++++++++++++- 6 files changed, 184 insertions(+), 15 deletions(-) diff --git a/locales/en-US/browser/browser/zen-live-folders.ftl b/locales/en-US/browser/browser/zen-live-folders.ftl index 12ba2a14f..79988c1dc 100644 --- a/locales/en-US/browser/browser/zen-live-folders.ftl +++ b/locales/en-US/browser/browser/zen-live-folders.ftl @@ -96,3 +96,6 @@ zen-live-folder-github-issues = zen-live-folder-github-option-repo-list-note = .label = This list is generated based on your currently active pull requests. + +zen-live-folders-promotion-title = Live Folder Created! +zen-live-folders-promotion-description = Latest content from your RSS feeds or GitHub pull requests will appear here automatically. diff --git a/src/external-patches/firefox/native_macos_popovers.patch b/src/external-patches/firefox/native_macos_popovers.patch index 3d5c21c27..b1023a68d 100644 --- a/src/external-patches/firefox/native_macos_popovers.patch +++ b/src/external-patches/firefox/native_macos_popovers.patch @@ -1,3 +1,54 @@ +diff --git a/browser/components/aboutwelcome/content/aboutwelcome.css b/browser/components/aboutwelcome/content/aboutwelcome.css +--- a/browser/components/aboutwelcome/content/aboutwelcome.css ++++ b/browser/components/aboutwelcome/content/aboutwelcome.css +@@ -329,10 +329,15 @@ + --panel-color: var(--fc-color); + --panel-shadow: none; + --panel-shadow-margin: 6px; + --panel-arrow-space: calc(var(--panel-shadow-margin) + var(--arrow-visible-height) - 1.5px); + --panel-margin-offset: calc(-1 * (var(--panel-shadow-margin) + var(--arrow-corner-distance) + (var(--arrow-width) / 2))); ++ ++ @media -moz-pref("widget.macos.native-popovers") and (-moz-platform: macos) { ++ --panel-shadow-margin: 0; ++ --panel-arrow-space: 0; ++ } + } + + panel#feature-callout::part(content) { + width: initial; + border: 0; +@@ -515,10 +520,16 @@ + border-radius: 4px; + padding: var(--callout-padding, 16px); + width: 25em; + gap: 16px; + background: var(--fc-background); ++ ++ @media -moz-pref("widget.macos.native-popovers") and (-moz-platform: macos) { ++ --fc-background: transparent; ++ box-shadow: none; ++ border-width: 0; ++ } + } + #feature-callout .screen[pos=callout] .section-main .main-content .main-content-inner { + gap: 12px; + } + #feature-callout .screen[pos=callout] .section-main .main-content .main-content-inner .legal-paragraph .text-link { +@@ -828,10 +839,14 @@ + #feature-callout .arrow-box { + position: absolute; + overflow: visible; + transform: rotate(45deg); + transform-style: preserve-3d; ++ ++ @media -moz-pref("widget.macos.native-popovers") and (-moz-platform: macos) { ++ display: none; ++ } + } + #feature-callout:not([arrow-position]) .arrow-box, #feature-callout[hide-arrow] .arrow-box { + display: none; + } + #feature-callout .arrow { diff --git a/browser/components/customizableui/content/panelUI.inc.xhtml b/browser/components/customizableui/content/panelUI.inc.xhtml --- a/browser/components/customizableui/content/panelUI.inc.xhtml +++ b/browser/components/customizableui/content/panelUI.inc.xhtml @@ -80,7 +131,7 @@ diff --git a/modules/libpref/init/StaticPrefList.yaml b/modules/libpref/init/Sta diff --git a/toolkit/themes/shared/global-shared.css b/toolkit/themes/shared/global-shared.css --- a/toolkit/themes/shared/global-shared.css +++ b/toolkit/themes/shared/global-shared.css -@@ -100,10 +100,19 @@ +@@ -100,10 +100,20 @@ --panel-padding: var(--arrowpanel-padding); --panel-shadow-margin: var(--arrowpanel-shadow-margin); --menuitem-border-radius: var(--arrowpanel-menuitem-border-radius); @@ -94,6 +145,7 @@ diff --git a/toolkit/themes/shared/global-shared.css b/toolkit/themes/shared/glo + --panel-shadow: none; + --panel-border-color: transparent; + --panel-shadow-margin: 0px; ++ --panel-padding: 0px; + } } @@ -216,7 +268,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm + +static void SyncPopoverBounds(NSPopover* aPopover, + nsMenuPopupFrame* aPopupFrame) { -+ if (!aPopover || !aPopover.shown) { ++ if (!aPopover || !aPopover.shown || !aPopupFrame) { + return; + } + NSWindow* popoverWindow = aPopover.contentViewController.view.window; @@ -246,7 +298,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; if (!mWindow) { -@@ -5227,10 +5280,58 @@ +@@ -5227,10 +5280,56 @@ mWindow.contentView.needsDisplay = YES; if (!nativeParentWindow || mPopupLevel != PopupLevel::Parent) { [mWindow orderFront:nil]; @@ -286,11 +338,9 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm + NSRect positioningRect = [parentView convertRect:windowRect + fromView:nil]; + BOOL shouldHideAnchor = NO; -+ if (auto popoverElement = GetPopupFrame()) { -+ auto& element = popoverElement->PopupElement(); -+ if (element.GetBoolAttr(nsGkAtoms::hidepopovertail)) { -+ shouldHideAnchor = YES; -+ } ++ auto& element = popupFrame->PopupElement(); ++ if (element.GetBoolAttr(nsGkAtoms::hidepopovertail)) { ++ shouldHideAnchor = YES; + } + [(PopupWindow*)mWindow showPopoverRelativeToRect:positioningRect + ofView:parentView @@ -305,7 +355,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm // close other programs' context menus when ours open. if ([mWindow isKindOfClass:[PopupWindow class]] && [(PopupWindow*)mWindow isContextMenu]) { -@@ -5301,10 +5402,15 @@ +@@ -5301,10 +5400,15 @@ // of a window it hides the parent window. if (mWindowType == WindowType::Popup && nativeParentWindow) { [nativeParentWindow removeChildWindow:mWindow]; @@ -321,7 +371,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm // other programs) that a menu has closed. if ([mWindow isKindOfClass:[PopupWindow class]] && [(PopupWindow*)mWindow isContextMenu]) { -@@ -5351,10 +5457,17 @@ +@@ -5351,10 +5455,17 @@ return false; } return nsIWidget::ShouldUseOffMainThreadCompositing(); @@ -339,7 +389,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm return mWindow.isOpaque ? TransparencyMode::Opaque : TransparencyMode::Transparent; -@@ -6313,10 +6426,20 @@ +@@ -6313,10 +6424,20 @@ // We ignore aRepaint -- we have to call display:YES, otherwise the // title bar doesn't immediately get repainted and is displayed in // the wrong place, leading to a visual jump. @@ -360,7 +410,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm void nsCocoaWindow::Resize(const DesktopRect& aRect, bool aRepaint) { DoResize(aRect.x, aRect.y, aRect.width, aRect.height, aRepaint, false); -@@ -8277,18 +8400,27 @@ +@@ -8277,18 +8398,27 @@ backing:(NSBackingStoreType)bufferingType defer:(BOOL)deferCreation { NS_OBJC_BEGIN_TRY_BLOCK_RETURN; @@ -388,7 +438,7 @@ diff --git a/widget/cocoa/nsCocoaWindow.mm b/widget/cocoa/nsCocoaWindow.mm // Return 0 in order to match what the system does for sheet windows and // _NSPopoverWindows. - (CGFloat)_backdropBleedAmount { -@@ -8342,10 +8474,120 @@ +@@ -8342,10 +8472,120 @@ - (void)setIsContextMenu:(BOOL)flag { mIsContextMenu = flag; diff --git a/src/zen/common/styles/zen-animations.css b/src/zen/common/styles/zen-animations.css index 3b8a176b2..591b52d72 100644 --- a/src/zen/common/styles/zen-animations.css +++ b/src/zen/common/styles/zen-animations.css @@ -82,3 +82,12 @@ transform: translateY(0); } } + +@keyframes zen-text-gradient { + 0% { + background-position: 0% 50%; + } + 100% { + background-position: -400% 50%; + } +} diff --git a/src/zen/common/styles/zen-single-components.css b/src/zen/common/styles/zen-single-components.css index 47f3f7e75..2c019910b 100644 --- a/src/zen/common/styles/zen-single-components.css +++ b/src/zen/common/styles/zen-single-components.css @@ -614,3 +614,15 @@ :root:not([zen-sidebar-expanded="true"]) zen-sidebar-notification { display: none; } + +/* Feature allouts */ +#feature-callout #mainContentHeader { + --special-color: color-mix(in srgb, light-dark(white, black), currentColor 50%); + --specia-color-2: color-mix(in srgb, light-dark(white, black), currentColor 90%); + background: linear-gradient(135deg, var(--specia-color-2), var(--special-color), var(--specia-color-2)); + background-size: 400%; + background-clip: text; + /* Still works on firefox */ + -webkit-text-fill-color: transparent; + animation: zen-text-gradient 5s linear infinite; +} diff --git a/src/zen/folders/zen-folders.css b/src/zen/folders/zen-folders.css index 5bbedc5df..5ab525230 100644 --- a/src/zen/folders/zen-folders.css +++ b/src/zen/folders/zen-folders.css @@ -177,7 +177,6 @@ zen-folder { & > label { width: 100% !important; - background: transparent !important; border: none !important; color: var(--sidebar-text-color) !important; margin: 0 !important; @@ -191,6 +190,13 @@ zen-folder { padding: 0 !important; max-width: 100% !important; } + + &[live-folder-animation] { + background: linear-gradient(90deg, color-mix(in srgb, var(--sidebar-text-color) 50%, transparent), var(--sidebar-text-color)); + background-size: 400%; + background-clip: text; + -webkit-text-fill-color: transparent; + } } .tab-reset-button[live-folder-action] { diff --git a/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs b/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs index 148ef3d0d..d8834b0ce 100644 --- a/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs +++ b/src/zen/live-folders/ZenLiveFoldersManager.sys.mjs @@ -8,6 +8,7 @@ ChromeUtils.defineESModuleGetters(lazy, { setTimeout: "resource://gre/modules/Timer.sys.mjs", TabStateCache: "resource:///modules/sessionstore/TabStateCache.sys.mjs", ZenWindowSync: "resource:///modules/zen/ZenWindowSync.sys.mjs", + FeatureCallout: "resource:///modules/asrouter/FeatureCallout.sys.mjs", }); ChromeUtils.defineLazyGetter( @@ -192,8 +193,11 @@ class nsZenLiveFoldersManager { const folder = this.window.gZenFolders.createFolder([], { label, isLiveFolder: true, + collapsed: true, }); + this.#maybeShowPromotion(folder, icon); + if (icon) { this.window.gZenFolders.setFolderUserIcon(folder, icon); } @@ -220,6 +224,88 @@ class nsZenLiveFoldersManager { return folder.id; } + #maybeShowPromotion(folder, icon) { + let labelElement = folder.labelElement; + labelElement.setAttribute("live-folder-animation", "true"); + labelElement.style.backgroundPositionX = "0%"; + let animation = folder.ownerGlobal.gZenUIManager.motion + .animate( + labelElement, + { + backgroundPositionX: ["0%", "-100%"], + }, + { + duration: 2, + } + ) + .then(() => { + labelElement.style.backgroundPositionX = ""; + labelElement.removeAttribute("live-folder-animation"); + }); + + if (Services.prefs.getBoolPref("zen.live-folders.promotion.shown", false)) { + return; + } + Services.prefs.setBoolPref("zen.live-folders.promotion.shown", true); + let window = this.window; + let gBrowser = window.gBrowser; + const callout = new lazy.FeatureCallout({ + win: this.window, + location: "chrome", + context: "chrome", + browser: gBrowser.selectedBrowser, + theme: { preset: "chrome" }, + }); + animation.then(() => { + callout.showFeatureCallout({ + id: "ZEN_LIVE_FOLDERS_CALLOUT", + template: "feature_callout", + groups: ["cfr"], + content: { + id: "ZEN_LIVE_FOLDERS_CALLOUT", + template: "spotlight", + backdrop: "transparent", + transitions: true, + autohide: true, + screens: [ + { + id: "ZEN_LIVE_FOLDERS_CALLOUT", + anchors: [ + { + selector: `[id="${folder.id}"]`, + panel_position: { + anchor_attachment: "rightcenter", + callout_attachment: "topleft", + }, + }, + ], + content: { + position: "callout", + width: "355px", + title_logo: { + imageURL: icon, + width: "18px", + height: "18px", + marginInline: "0 8px", + }, + title: { + string_id: "zen-live-folders-promotion-title", + }, + subtitle: { + string_id: "zen-live-folders-promotion-description", + }, + }, + }, + ], + }, + }); + + lazy.setTimeout(() => { + callout.endTour(); + }, 8000); + }); + } + deleteFolder(id, deleteFolder = true) { const liveFolder = this.liveFolders.get(id); if (!liveFolder) { @@ -286,7 +372,10 @@ class nsZenLiveFoldersManager { existingItemIds.add(itemId); } - this.window.gBrowser.removeTabs(outdatedTabs); + this.window.gBrowser.removeTabs(outdatedTabs, { + skipSessionStore: true, + animate: !folder.collapsed, + }); // Remove the dismissed items that are no longer in the given list for (const dismissedItemId of this.dismissedItems) {