diff --git a/src/zen/library/ZenLibrary.mjs b/src/zen/library/ZenLibrary.mjs
index 6bd903f07..c694c932d 100644
--- a/src/zen/library/ZenLibrary.mjs
+++ b/src/zen/library/ZenLibrary.mjs
@@ -86,11 +86,18 @@ export class ZenLibrary extends MozLitElement {
this.addEventListener("animationend", this);
// Add connected call back and make `appContentWrapper` transform translate the oposite of this element
this.#resizeObserver = new ResizeObserver(() => {
+ if (this._swipeActive) {
+ this._swipeWrapperTargetPx = this.#computeWrapperTargetPx();
+ ZenLibrary.updateSwipeProgress(this._swipeProgress ?? 0);
+ return;
+ }
if (gZenWorkspaces._swipeManager._swipeState.librarySwiping) {
return;
}
let translateX = this.#computeWrapperTargetPx();
- lazy.appContentWrapper.style.transform = `translateX(${translateX}px)`;
+ requestAnimationFrame(() => {
+ lazy.appContentWrapper.style.transform = `translateX(${translateX}px)`;
+ });
});
this.#resizeObserver.observe(this);
for (const Section of Object.values(lazy.ZenLibrarySections)) {
@@ -221,7 +228,7 @@ export class ZenLibrary extends MozLitElement {
break;
}
case "animationend": {
- if (event.animationName === "zen-library-tab-icon-play") {
+ if (event.animationName === "zen-library-sprite-play") {
event.target.closest(".zen-library-tab")?.removeAttribute("animate");
}
break;
@@ -313,9 +320,12 @@ export class ZenLibrary extends MozLitElement {
*/
#computeWrapperTargetPx() {
const isRightSide = gZenVerticalTabsManager._prefsRightSide;
- let translateX = this.getBoundingClientRect()[
- isRightSide ? "left" : "right"
- ];
+ const selfTransform = getComputedStyle(this).transform;
+ const selfOffset =
+ selfTransform === "none" ? 0 : new DOMMatrix(selfTransform).m41;
+ let translateX =
+ this.getBoundingClientRect()[isRightSide ? "left" : "right"] -
+ selfOffset;
const contentPosition = window.windowUtils.getBoundsWithoutFlushing(
lazy.appContentWrapper
)[isRightSide ? "right" : "left"];
@@ -396,21 +406,11 @@ export class ZenLibrary extends MozLitElement {
instance.#cancelActiveAnimations();
const wasOpen = instance.hasAttribute("open");
if (wasOpen) {
- // Library is already open; the wrapper's current inline transform IS
- // the target — no remeasure needed.
instance._swipeWrapperTargetPx =
new DOMMatrix(lazy.appContentWrapper.style.transform).m41 ||
instance.#computeWrapperTargetPx();
} else {
- // Measure the open-state wrapper target without flashing: temporarily
- // mark [open] so layout reflects the open position, then revert.
- instance.setAttribute("open", "true");
- instance.style.visibility = "hidden";
- await new Promise(r => requestAnimationFrame(r));
instance._swipeWrapperTargetPx = instance.#computeWrapperTargetPx();
- instance.style.visibility = "";
- instance.removeAttribute("open");
- lazy.appContentWrapper.style.transform = "";
}
instance._swipeActive = true;
// Initialize visual state to match the current attribute.
diff --git a/src/zen/library/ZenLibrarySections.mjs b/src/zen/library/ZenLibrarySections.mjs
index 5cd47c611..5453df1a5 100644
--- a/src/zen/library/ZenLibrarySections.mjs
+++ b/src/zen/library/ZenLibrarySections.mjs
@@ -475,21 +475,18 @@ class SearchSection extends LibrarySection {
}
/**
- * Open `url` in a Glance overlay anchored to the clicked item.
+ * Open `url` in a Glance overlay.
*
- * @param {Event} event
* @param {string} url
*/
- _openInGlance(event, url) {
- const itemEl = event.currentTarget;
- const rect = window.windowUtils.getBoundsWithoutFlushing(itemEl);
+ _openInGlance(url) {
const tabPanelRect = window.windowUtils.getBoundsWithoutFlushing(
window.gBrowser.tabpanels
);
window.gZenGlanceManager.openGlance({
url,
- clientX: rect.left - tabPanelRect.left,
- clientY: rect.top - tabPanelRect.top,
+ clientX: window.innerWidth / 2 - tabPanelRect.left,
+ clientY: window.innerHeight / 2 - tabPanelRect.top,
width: 0,
height: 0,
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
@@ -515,6 +512,26 @@ class SearchSection extends LibrarySection {
return html``;
}
+ /**
+ * Shared empty-state markup: the section's sprite icon above the localized
+ * message. The [data-section] attribute picks the sprite strip, see
+ * zen-library.css.
+ *
+ * @param {string} emptyL10nId - message shown when the section has no data.
+ */
+ _renderEmptyState(emptyL10nId) {
+ return html`
+
+ `;
+ }
+
render() {
const hasFilters = !!this._filters().length;
return html`
@@ -917,12 +934,7 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
return html``;
}
if (this.isEmpty) {
- return html`
-
- `;
+ return this._renderEmptyState("library-history-empty");
}
const slice = this._getRenderedSlice();
@@ -960,9 +972,7 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
}
/**
- * Plain click on a history row opens the page in a Glance overlay; holding
- * Ctrl (or any other modifier / middle-click) falls back to the standard
- * "where to open" routing.
+ * Clicking a history row opens the page in a new tab.
*
* @param {Event} event
* @param {object} item
@@ -976,21 +986,6 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
}
event.preventDefault();
- const hasModifier =
- event.ctrlKey || event.metaKey || event.shiftKey || event.altKey;
- const isMiddleClick = event.button === 1;
-
- if (
- !hasModifier &&
- !isMiddleClick &&
- Services.prefs.getBoolPref("zen.glance.enabled", true)
- ) {
- // Glance overlays on top of the library; keep the library open so the
- // user can fire another glance without re-opening it.
- this._openInGlance(event, item.url);
- return;
- }
-
const where = lazy.BrowserUtils.whereToOpenLink(event, false, true);
window.openTrustedLinkIn(item.url, where === "current" ? "tab" : where);
this._closeLibrary();
@@ -1031,15 +1026,6 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
this._closeLibrary();
},
},
- {
- l10nId: "library-item-context-open-glance",
- onClick: () => {
- // Use the section element as the anchor — context menu opens at
- // the cursor and the original item element isn't tracked here.
- const fakeEvent = { currentTarget: this };
- this._openInGlance(fakeEvent, item.url);
- },
- },
{
l10nId: "library-item-context-open-new-window",
onClick: () => {
@@ -1177,12 +1163,7 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
const slice = this._getRenderedSlice();
if (this.isEmpty) {
- return html`
-
- `;
+ return this._renderEmptyState("library-downloads-empty");
}
const groups = this.#groupByDate(slice);
@@ -1313,7 +1294,7 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
const previewUrl =
dl.source.referrerInfo?.originalReferrer?.spec || dl.source.url;
if (previewUrl) {
- this._openInGlance(event, previewUrl);
+ this._openInGlance(previewUrl);
}
return;
}
@@ -1631,12 +1612,7 @@ class ZenLibraryBoostsSection extends SearchSection {
renderSearchResults() {
const boosts = this.#getBoosts();
if (boosts.length === 0) {
- return html`
-
- `;
+ return this._renderEmptyState("library-boosts-empty");
}
return html`${boosts.map(b => this.#renderBoost(b))}`;
}
@@ -1647,7 +1623,7 @@ class ZenLibraryBoostsSection extends SearchSection {
class="library-item library-boost-item"
data-key=${`${boost.domain}|${boost.id}`}
?active=${boost.isActive}
- @click=${e => this.#openBoost(e, boost)}
+ @click=${() => this.#openBoost(boost)}
@contextmenu=${e => this._onItemContextMenu(e, boost)}
>
@@ -1693,12 +1669,11 @@ class ZenLibraryBoostsSection extends SearchSection {
* Open the boost's domain in a Glance overlay and pop the boost editor
* window next to it so the user can tweak the boost while previewing.
*
- * @param {Event} event
* @param {object} boost
*/
- #openBoost(event, boost) {
+ #openBoost(boost) {
const url = `https://${boost.domain}/`;
- this._openInGlance(event, url);
+ this._openInGlance(url);
try {
const stored = lazy.gZenBoostsManager.loadBoostFromStore(
boost.domain,
diff --git a/src/zen/library/zen-library.css b/src/zen/library/zen-library.css
index f13d7ec0c..c54f18a88 100644
--- a/src/zen/library/zen-library.css
+++ b/src/zen/library/zen-library.css
@@ -83,6 +83,8 @@ zen-library[open] {
}
.zen-library-tab {
+ --zen-library-sprite-size: 28px;
+
display: flex;
position: relative;
justify-content: center;
@@ -114,54 +116,56 @@ zen-library[open] {
}
& .zen-library-tab-icon {
- width: 28px;
- height: 28px;
+ width: var(--zen-library-sprite-size);
+ height: var(--zen-library-sprite-size);
overflow: clip;
}
- & .zen-library-tab-icon-image {
- width: calc(36 * 28px);
- height: 28px;
- fill: rgba(255, 255, 255, 0.8);
- stroke: var(--zen-colors-primary);
- -moz-context-properties: fill, stroke;
- transform: translateX(0);
- }
-
- &[data-section="history"] .zen-library-tab-icon-image {
- background-image: url("chrome://browser/skin/zen-icons/library/library-history-sprite.svg");
- }
-
- &[data-section="downloads"] .zen-library-tab-icon-image {
- background-image: url("chrome://browser/skin/zen-icons/library/library-downloads-sprite.svg");
- }
-
- &[data-section="boosts"] .zen-library-tab-icon-image {
- background-image: url("chrome://browser/skin/zen-icons/library/library-boosts-sprite.svg");
- }
-
- &[data-section="spaces"] .zen-library-tab-icon-image {
- background-image: url("chrome://browser/skin/zen-icons/library/library-spaces-sprite.svg");
- }
-
- &[data-section="media"] .zen-library-tab-icon-image {
- background-image: url("chrome://browser/skin/zen-icons/library/library-media-sprite.svg");
- }
-
@media (prefers-reduced-motion: no-preference) {
&[animate] .zen-library-tab-icon-image {
- animation: zen-library-tab-icon-play 0.583s steps(36, jump-none);
+ animation: zen-library-sprite-play 0.583s steps(36, jump-none);
}
}
}
-@keyframes zen-library-tab-icon-play {
+.zen-library-tab-icon-image,
+.empty-state-icon-image {
+ width: calc(36 * var(--zen-library-sprite-size));
+ height: var(--zen-library-sprite-size);
+ background-size: 100% 100%;
+ fill: rgba(255, 255, 255, 0.8);
+ stroke: var(--zen-colors-primary);
+ -moz-context-properties: fill, stroke;
+ transform: translateX(0);
+}
+
+[data-section="history"] :is(.zen-library-tab-icon-image, .empty-state-icon-image) {
+ background-image: url("chrome://browser/skin/zen-icons/library/library-history-sprite.svg");
+}
+
+[data-section="downloads"] :is(.zen-library-tab-icon-image, .empty-state-icon-image) {
+ background-image: url("chrome://browser/skin/zen-icons/library/library-downloads-sprite.svg");
+}
+
+[data-section="boosts"] :is(.zen-library-tab-icon-image, .empty-state-icon-image) {
+ background-image: url("chrome://browser/skin/zen-icons/library/library-boosts-sprite.svg");
+}
+
+[data-section="spaces"] :is(.zen-library-tab-icon-image, .empty-state-icon-image) {
+ background-image: url("chrome://browser/skin/zen-icons/library/library-spaces-sprite.svg");
+}
+
+[data-section="media"] :is(.zen-library-tab-icon-image, .empty-state-icon-image) {
+ background-image: url("chrome://browser/skin/zen-icons/library/library-media-sprite.svg");
+}
+
+@keyframes zen-library-sprite-play {
from {
transform: translateX(0);
}
to {
- transform: translateX(calc(35 * -28px));
+ transform: translateX(calc(35 * -1 * var(--zen-library-sprite-size)));
}
}
@@ -361,7 +365,7 @@ zen-library[open] {
}
.library-item:hover .library-item-background {
- background-color: var(--tab-hover-background-color);
+ background-color: var(--tab-background-color-hover);
outline-color: var(--tab-hover-outline-color);
}
@@ -573,11 +577,36 @@ zen-library[open] {
.empty-state {
display: flex;
flex: 1;
+ flex-direction: column;
align-items: center;
justify-content: center;
+ gap: 16px;
font-size: 2em;
font-weight: 600;
opacity: 0.5;
+
+ @media (prefers-reduced-motion: no-preference) {
+ animation: zen-library-empty-state-enter 0.3s cubic-bezier(0.32, 0.72, 0, 1);
+
+ & .empty-state-icon-image {
+ animation: zen-library-sprite-play 0.583s steps(36, jump-none);
+ }
+ }
+}
+
+@keyframes zen-library-empty-state-enter {
+ from {
+ opacity: 0;
+ transform: translateY(12px) scale(0.98);
+ }
+}
+
+.empty-state-icon {
+ --zen-library-sprite-size: 56px;
+
+ width: var(--zen-library-sprite-size);
+ height: var(--zen-library-sprite-size);
+ overflow: clip;
}
/* Boosts section */