diff --git a/locales/en-US/browser/browser/zen-library.ftl b/locales/en-US/browser/browser/zen-library.ftl
index 53b517f66..d44155d64 100644
--- a/locales/en-US/browser/browser/zen-library.ftl
+++ b/locales/en-US/browser/browser/zen-library.ftl
@@ -23,6 +23,52 @@ library-search-no-results = No results
library-filter-button = Filter
+library-history-filter-all =
+ .label = All time
+library-history-filter-today =
+ .label = Today
+library-history-filter-yesterday =
+ .label = Yesterday
+library-history-filter-last-7-days =
+ .label = Last 7 days
+library-history-filter-last-30-days =
+ .label = Last 30 days
+
+library-downloads-filter-all =
+ .label = All
+library-downloads-filter-completed =
+ .label = Completed
+library-downloads-filter-in-progress =
+ .label = In progress
+library-downloads-filter-failed =
+ .label = Failed
+library-downloads-filter-paused =
+ .label = Paused
+
+library-item-context-open =
+ .label = Open
+library-item-context-open-glance =
+ .label = Open in Glance
+library-item-context-open-new-tab =
+ .label = Open in New Tab
+library-item-context-open-new-window =
+ .label = Open in New Window
+library-item-context-copy-url =
+ .label = Copy URL
+library-item-context-delete-history =
+ .label = Forget About This Page
+library-item-context-show-in-folder =
+ .label = Show in Folder
+library-item-context-open-source =
+ .label = Open Source URL
+library-item-context-remove =
+ .label = Remove from History
+
+library-history-action-remove =
+ .tooltiptext = Forget About This Page
+library-history-action-open-tab =
+ .tooltiptext = Open in New Tab
+
library-downloads-state-downloading = Downloading…
library-downloads-state-canceled = Canceled
library-downloads-state-failed = Failed
diff --git a/src/browser/base/content/zen-locales.inc.xhtml b/src/browser/base/content/zen-locales.inc.xhtml
index ed52e6fca..490dd1d8a 100644
--- a/src/browser/base/content/zen-locales.inc.xhtml
+++ b/src/browser/base/content/zen-locales.inc.xhtml
@@ -9,6 +9,7 @@
+
diff --git a/src/zen/library/ZenLibrarySections.mjs b/src/zen/library/ZenLibrarySections.mjs
index 333c5f042..289613c84 100644
--- a/src/zen/library/ZenLibrarySections.mjs
+++ b/src/zen/library/ZenLibrarySections.mjs
@@ -10,11 +10,13 @@ import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
+ BrowserUtils: "resource://gre/modules/BrowserUtils.sys.mjs",
DownloadsCommon:
"moz-src:///browser/components/downloads/DownloadsCommon.sys.mjs",
DownloadsViewUI:
"moz-src:///browser/components/downloads/DownloadsViewUI.sys.mjs",
DownloadUtils: "resource://gre/modules/DownloadUtils.sys.mjs",
+ FileUtils: "resource://gre/modules/FileUtils.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});
@@ -24,7 +26,32 @@ ChromeUtils.defineLazyGetter(
() => new Localization(["browser/zen-library.ftl"], true)
);
+ChromeUtils.defineLazyGetter(lazy, "clipboardHelper", () =>
+ Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper)
+);
+
const SEARCH_DEBOUNCE_MS = 300;
+const DAY_MS = 86_400_000;
+
+const HISTORY_FILTERS = [
+ { id: "all", label: "library-history-filter-all", days: null },
+ { id: "today", label: "library-history-filter-today", days: 0 },
+ { id: "yesterday", label: "library-history-filter-yesterday", days: 1 },
+ { id: "last-7-days", label: "library-history-filter-last-7-days", days: 7 },
+ {
+ id: "last-30-days",
+ label: "library-history-filter-last-30-days",
+ days: 30,
+ },
+];
+
+const DOWNLOAD_FILTERS = [
+ { id: "all", label: "library-downloads-filter-all" },
+ { id: "completed", label: "library-downloads-filter-completed" },
+ { id: "in-progress", label: "library-downloads-filter-in-progress" },
+ { id: "failed", label: "library-downloads-filter-failed" },
+ { id: "paused", label: "library-downloads-filter-paused" },
+];
// Intl singletons (construction is expensive in SpiderMonkey)
const TIME_FORMATTER = new Intl.DateTimeFormat(undefined, {
@@ -67,6 +94,7 @@ class SearchSection extends LibrarySection {
inputValue: { type: String },
isEmpty: { type: Boolean },
isScrolledToTop: { type: Boolean },
+ activeFilter: { type: String },
};
#canDebug = false;
@@ -80,6 +108,9 @@ class SearchSection extends LibrarySection {
if (this.inputValue === undefined) {
this.inputValue = this.searchTerm;
}
+ if (!this.activeFilter) {
+ this.activeFilter = this._filters()[0]?.id ?? "";
+ }
this.isScrolledToTop = true;
this.isEmpty = false;
super.connectedCallback();
@@ -156,17 +187,321 @@ class SearchSection extends LibrarySection {
return TIME_FORMATTER.format(d);
}
+ /** Subclasses override to expose filter options. Empty array hides the button. */
+ _filters() {
+ return [];
+ }
+
+ /**
+ * Called when the user picks a filter; subclasses re-query or re-filter.
+ *
+ * @param _id
+ */
+ _onFilterChange(_id) {}
+
+ _onFilterButtonClick(event) {
+ event.stopPropagation();
+ const filters = this._filters();
+ if (filters.length === 0) {
+ return;
+ }
+ const items = filters.map(f => ({
+ l10nId: f.label,
+ type: "radio",
+ checked: this.activeFilter === f.id,
+ onClick: () => this._onSelectFilter(f.id),
+ }));
+ this._openNativeContextMenu(items, {
+ type: "anchor",
+ el: event.currentTarget,
+ });
+ }
+
+ _onSelectFilter(id) {
+ if (id === this.activeFilter) {
+ return;
+ }
+ this.activeFilter = id;
+ this._onFilterChange(id);
+ }
+
+ _activeFilterLabel() {
+ const filters = this._filters();
+ const active = filters.find(f => f.id === this.activeFilter) ?? filters[0];
+ return active ? this._l10n(active.label) : "";
+ }
+
+ /**
+ * Renders one item row. `sideTop`/`sideBottom` are the stacked right-side
+ * info slots (e.g. domain over time for downloads, just time for history).
+ *
+ * @param root0
+ * @param root0.key
+ * @param root0.iconSrc
+ * @param root0.label
+ * @param root0.sublabel
+ * @param root0.sideTop
+ * @param root0.sideBottom
+ * @param root0.payload
+ */
+ _renderItem({ key, iconSrc, label, sublabel, sideTop, sideBottom, payload }) {
+ return html`
+
{
- const el = this.renderRoot.querySelector(".search-results");
- if (!el) {
- return;
- }
- this.isScrolledToTop = el.scrollTop === 0;
- }}
+ @scroll=${e => {
+ const el = e.currentTarget;
+ this.isScrolledToTop = el.scrollTop === 0;
+ this._onScroll?.(el);
+ }}
?scrolled-to-top=${this.isScrolledToTop}
>
${this.renderSearchResults()}
@@ -223,10 +568,14 @@ class SearchSection extends LibrarySection {
* so the main thread stays responsive and the scrollbar tracks correctly.
*/
class ProgressiveSearchSection extends SearchSection {
+ static INITIAL_BATCH = 50;
+ static SCROLL_BATCH = 50;
+ static SCROLL_THRESHOLD_PX = 400;
+
_allItems = [];
#renderedItemCount = 0;
- #renderTask = null;
+ #fillScheduled = false;
_getRenderedSlice() {
return this._allItems.slice(0, this.#renderedItemCount);
@@ -234,47 +583,83 @@ class ProgressiveSearchSection extends SearchSection {
/** Call when _allItems is fully replaced (new query / search term). */
_resetProgressiveRender() {
- this.#cancelTask();
- this.#renderedItemCount = Math.min(50, this._allItems.length);
- this.isEmpty = this._allItems.length === 0;
- this.#scheduleNextBatch();
- }
-
- /** Call when _allItems grows incrementally to avoid resetting scroll position. */
- _maintainProgressiveRender() {
- this.#renderedItemCount = Math.max(
- this.#renderedItemCount,
- Math.min(50, this._allItems.length)
+ this.#renderedItemCount = Math.min(
+ ProgressiveSearchSection.INITIAL_BATCH,
+ this._allItems.length
);
this.isEmpty = this._allItems.length === 0;
- this.#scheduleNextBatch();
+ this.#scheduleViewportFill();
}
- #scheduleNextBatch() {
- if (this.#renderedItemCount >= this._allItems.length) {
+ /** Call when _allItems grows/shrinks but the rendered prefix should stick. */
+ _maintainProgressiveRender() {
+ this.#renderedItemCount = Math.min(
+ Math.max(
+ this.#renderedItemCount,
+ Math.min(ProgressiveSearchSection.INITIAL_BATCH, this._allItems.length)
+ ),
+ this._allItems.length
+ );
+ this.isEmpty = this._allItems.length === 0;
+ this.#scheduleViewportFill();
+ }
+
+ /**
+ * Top up the rendered slice until the list is at least one viewport tall;
+ * a list shorter than the viewport never fires a scroll event, so without
+ * this the user could never trigger more loads.
+ */
+ #scheduleViewportFill() {
+ if (this.#fillScheduled) {
return;
}
- this.#renderTask = requestIdleCallback(() => {
- this.#renderTask = null;
- this.#renderedItemCount = Math.min(
- this.#renderedItemCount + 100,
- this._allItems.length
- );
- this.requestUpdate();
- this.#scheduleNextBatch();
+ this.#fillScheduled = true;
+ this.updateComplete.then(() => {
+ this.#fillScheduled = false;
+ if (!this.isConnected) {
+ return;
+ }
+ const el = this.renderRoot.querySelector(".search-results");
+ if (!el) {
+ return;
+ }
+ if (
+ el.scrollHeight <= el.clientHeight &&
+ this.#renderedItemCount < this._allItems.length
+ ) {
+ this.#growBy(ProgressiveSearchSection.SCROLL_BATCH);
+ this.#scheduleViewportFill();
+ }
});
}
- #cancelTask() {
- if (this.#renderTask) {
- cancelIdleCallback(this.#renderTask);
- this.#renderTask = null;
+ #growBy(n) {
+ if (this.#renderedItemCount >= this._allItems.length) {
+ return;
+ }
+ this.#renderedItemCount = Math.min(
+ this.#renderedItemCount + n,
+ this._allItems.length
+ );
+ this.requestUpdate();
+ }
+
+ /**
+ * Hook called from SearchSection's results scroll handler.
+ *
+ * @param el
+ */
+ _onScroll(el) {
+ if (this.#renderedItemCount >= this._allItems.length) {
+ return;
+ }
+ const distanceFromBottom = el.scrollHeight - el.scrollTop - el.clientHeight;
+ if (distanceFromBottom <= ProgressiveSearchSection.SCROLL_THRESHOLD_PX) {
+ this.#growBy(ProgressiveSearchSection.SCROLL_BATCH);
}
}
disconnectedCallback() {
- // Cancel before super so no stale idle callback fires against cleared base state.
- this.#cancelTask();
this._allItems = [];
super.disconnectedCallback();
}
@@ -378,13 +763,25 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
return;
}
+ const cutoff = this.#filterCutoff();
+ const yesterdayWindow = this.#yesterdayWindow();
+
// RESULTS_AS_VISIT produces one node per visit; de-duplicate per day-bucket.
const seenNodes = new Set();
for (let i = 0; i < root.childCount; i++) {
const node = root.getChild(i);
const date = new Date(node.time / 1000);
- const dayStart = new Date(date).setHours(0, 0, 0, 0);
+ const ts = date.getTime();
+ if (yesterdayWindow) {
+ if (ts < yesterdayWindow.start || ts >= yesterdayWindow.end) {
+ continue;
+ }
+ } else if (cutoff !== null && ts < cutoff) {
+ continue;
+ }
+
+ const dayStart = new Date(date).setHours(0, 0, 0, 0);
const key = `${dayStart}|${node.uri}`;
if (seenNodes.has(key)) {
continue;
@@ -399,13 +796,47 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
}
this.log(
- `#walkAndPopulate — ${this._allItems.length} items, search=${!!this.searchTerm}`
+ `#walkAndPopulate — ${this._allItems.length} items, search=${!!this.searchTerm}, filter=${this.activeFilter}`
);
} finally {
this.#walking = false;
}
}
+ /** Lower bound (timestamp ms) for the active time-range filter, or null for "all". */
+ #filterCutoff() {
+ const filter = HISTORY_FILTERS.find(f => f.id === this.activeFilter);
+ if (!filter || filter.days === null) {
+ return null;
+ }
+ if (filter.id === "today") {
+ return new Date().setHours(0, 0, 0, 0);
+ }
+ if (filter.id === "yesterday") {
+ return null; // handled by #yesterdayWindow
+ }
+ return new Date().setHours(0, 0, 0, 0) - filter.days * DAY_MS;
+ }
+
+ /** Yesterday is a single-day window, not just a lower bound. */
+ #yesterdayWindow() {
+ if (this.activeFilter !== "yesterday") {
+ return null;
+ }
+ const start = new Date().setHours(0, 0, 0, 0) - DAY_MS;
+ return { start, end: start + DAY_MS };
+ }
+
+ _filters() {
+ return HISTORY_FILTERS;
+ }
+
+ _onFilterChange(_id) {
+ this.#walkAndPopulate();
+ this._resetProgressiveRender();
+ this.requestUpdate();
+ }
+
#createObserver() {
const rebuild = () => this.#scheduleRebuild();
return {
@@ -494,42 +925,128 @@ class ZenLibraryHistorySection extends ProgressiveSearchSection {
if (dayLabel !== lastLabel) {
lastLabel = dayLabel;
rows.push(html`
-
+
`);
}
-
- rows.push(html`
-
-
-
-
-
-

-
-
-
-
-
-
-
-
-
- `);
+ const domain =
+ v.url?.replace(/^[\w-]+:\/\//, "").replace(/\/$/, "") || "";
+ const time = this._formatTime(v.date);
+ const sublabel = [domain, time].filter(Boolean).join(" · ");
+ rows.push(
+ this._renderItem({
+ key: `${v.date.getTime()}|${v.url}`,
+ iconSrc: `page-icon:${v.url || ""}`,
+ label: v.title || v.url,
+ sublabel,
+ payload: v,
+ })
+ );
}
return html`${rows}`;
}
+
+ /**
+ * 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.
+ *
+ * @param event
+ * @param item
+ */
+ _onItemClick(event, item) {
+ if (event.button === 2) {
+ return;
+ }
+ if (!item?.url) {
+ return;
+ }
+ 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)
+ ) {
+ this._openInGlance(event, item.url);
+ this._closeLibrary();
+ return;
+ }
+
+ const where = lazy.BrowserUtils.whereToOpenLink(event, false, true);
+ window.openTrustedLinkIn(item.url, where === "current" ? "tab" : where);
+ this._closeLibrary();
+ }
+
+ _renderItemActions(item) {
+ if (!item?.url) {
+ return [];
+ }
+ return [
+ {
+ icon: "chrome://browser/skin/zen-icons/edit-delete.svg",
+ l10nId: "library-history-action-remove",
+ onClick: () => {
+ lazy.PlacesUtils.history.remove(item.url).catch(console.error);
+ },
+ },
+ {
+ icon: "chrome://browser/skin/zen-icons/arrow-right.svg",
+ l10nId: "library-history-action-open-tab",
+ onClick: () => {
+ window.openTrustedLinkIn(item.url, "tab");
+ this._closeLibrary();
+ },
+ },
+ ];
+ }
+
+ _buildContextMenu(item) {
+ if (!item?.url) {
+ return null;
+ }
+ return [
+ {
+ l10nId: "library-item-context-open",
+ onClick: () => {
+ window.openTrustedLinkIn(item.url, "tab");
+ 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);
+ this._closeLibrary();
+ },
+ },
+ {
+ l10nId: "library-item-context-open-new-window",
+ onClick: () => {
+ window.openTrustedLinkIn(item.url, "window");
+ this._closeLibrary();
+ },
+ },
+ { separator: true },
+ {
+ l10nId: "library-item-context-copy-url",
+ onClick: () => lazy.clipboardHelper.copyString(item.url),
+ },
+ {
+ l10nId: "library-item-context-delete-history",
+ onClick: () => {
+ lazy.PlacesUtils.history.remove(item.url).catch(console.error);
+ },
+ },
+ ];
+ }
}
/**
@@ -633,6 +1150,16 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
this.requestUpdate();
}
+ _filters() {
+ return DOWNLOAD_FILTERS;
+ }
+
+ _onFilterChange(_id) {
+ this.#rebuildItems();
+ this._resetProgressiveRender();
+ this.requestUpdate();
+ }
+
renderSearchResults() {
const slice = this._getRenderedSlice();
@@ -667,32 +1194,121 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
? `moz-icon://${dl.target.path}?size=16`
: "moz-icon://.unknown?size=16";
- return html`
-
-
-
-
-
-

-
-
-
-
-
-
-
-
-
- `;
+ const sublabelParts = [
+ this.#getStatusLabel(dl),
+ this.#sourceDomain(dl),
+ this._formatTime(this.#downloadTime(dl)),
+ ].filter(Boolean);
+
+ return this._renderItem({
+ key: `${this.#downloadTime(dl)}|${dl.source.url}`,
+ iconSrc: iconPath,
+ label: nameStr,
+ sublabel: sublabelParts.join(" · "),
+ payload: dl,
+ });
+ }
+
+ #sourceDomain(dl) {
+ const url = dl.source.originalUrl || dl.source.url || "";
+ try {
+ return new URL(url).hostname.replace(/^www\./, "");
+ } catch {
+ return "";
+ }
+ }
+
+ _onItemClick(event, dl) {
+ if (event.button === 2) {
+ return;
+ }
+ if (!dl) {
+ return;
+ }
+ event.preventDefault();
+
+ const state = lazy.DownloadsCommon.stateOfDownload(dl);
+ const isFinished = state === lazy.DownloadsCommon.DOWNLOAD_FINISHED;
+
+ // Glance modifier — preview the source URL, regardless of file state.
+ if (event.button !== 1 && this._isGlanceActivation(event)) {
+ const previewUrl =
+ dl.source.referrerInfo?.originalReferrer?.spec || dl.source.url;
+ if (previewUrl) {
+ this._openInGlance(event, previewUrl);
+ this._closeLibrary();
+ }
+ return;
+ }
+
+ // Middle-click or finished file with no path: fall back to opening source URL.
+ if (event.button === 1 || !isFinished || !dl.target.path) {
+ const url = dl.source.originalUrl || dl.source.url;
+ if (url) {
+ const where = lazy.BrowserUtils.whereToOpenLink(event, false, true);
+ window.openTrustedLinkIn(url, where === "current" ? "tab" : where);
+ this._closeLibrary();
+ }
+ return;
+ }
+
+ // Default: launch the downloaded file with the system handler.
+ if (dl.target.exists !== false && !dl.deleted) {
+ lazy.DownloadsCommon.openDownload(dl).catch(console.error);
+ }
+ }
+
+ _buildContextMenu(dl) {
+ if (!dl) {
+ return null;
+ }
+ const state = lazy.DownloadsCommon.stateOfDownload(dl);
+ const isFinished = state === lazy.DownloadsCommon.DOWNLOAD_FINISHED;
+ const fileExists = isFinished && dl.target.exists !== false && !dl.deleted;
+ const sourceUrl = dl.source.originalUrl || dl.source.url;
+
+ const items = [];
+ if (fileExists) {
+ items.push({
+ l10nId: "library-item-context-open",
+ onClick: () => {
+ lazy.DownloadsCommon.openDownload(dl).catch(console.error);
+ },
+ });
+ items.push({
+ l10nId: "library-item-context-show-in-folder",
+ onClick: () => {
+ try {
+ const file = new lazy.FileUtils.File(dl.target.path);
+ lazy.DownloadsCommon.showDownloadedFile(file);
+ } catch (ex) {
+ console.error(ex);
+ }
+ },
+ });
+ items.push({ separator: true });
+ }
+ if (sourceUrl) {
+ items.push({
+ l10nId: "library-item-context-open-source",
+ onClick: () => {
+ window.openTrustedLinkIn(sourceUrl, "tab");
+ this._closeLibrary();
+ },
+ });
+ items.push({
+ l10nId: "library-item-context-copy-url",
+ onClick: () => lazy.clipboardHelper.copyString(sourceUrl),
+ });
+ items.push({ separator: true });
+ }
+ items.push({
+ l10nId: "library-item-context-remove",
+ onClick: () => {
+ lazy.DownloadsCommon.deleteDownload(dl).catch(console.error);
+ },
+ });
+ return items;
}
#downloadTime(dl) {
@@ -723,12 +1339,40 @@ class ZenLibraryDownloadsSection extends ProgressiveSearchSection {
#rebuildItems() {
const term = this.searchTerm?.trim().toLowerCase();
- if (term) {
- this._allItems = this.#allDownloads.filter(dl =>
- this.#matchesSearchTerm(dl, term)
- );
- } else {
- this._allItems = [...this.#allDownloads];
+ const filter = this.activeFilter;
+ this._allItems = this.#allDownloads.filter(dl => {
+ if (term && !this.#matchesSearchTerm(dl, term)) {
+ return false;
+ }
+ return this.#matchesStatusFilter(dl, filter);
+ });
+ }
+
+ #matchesStatusFilter(dl, filter) {
+ if (!filter || filter === "all") {
+ return true;
+ }
+ const state = lazy.DownloadsCommon.stateOfDownload(dl);
+ const C = lazy.DownloadsCommon;
+ switch (filter) {
+ case "completed":
+ return state === C.DOWNLOAD_FINISHED;
+ case "in-progress":
+ return (
+ state === C.DOWNLOAD_DOWNLOADING || state === C.DOWNLOAD_NOTSTARTED
+ );
+ case "failed":
+ return (
+ state === C.DOWNLOAD_FAILED ||
+ state === C.DOWNLOAD_CANCELED ||
+ state === C.DOWNLOAD_DIRTY ||
+ state === C.DOWNLOAD_BLOCKED_PARENTAL ||
+ state === C.DOWNLOAD_BLOCKED_CONTENT_ANALYSIS
+ );
+ case "paused":
+ return state === C.DOWNLOAD_PAUSED;
+ default:
+ return true;
}
}
diff --git a/src/zen/library/zen-library.css b/src/zen/library/zen-library.css
index bb32a95fb..2cf4c373c 100644
--- a/src/zen/library/zen-library.css
+++ b/src/zen/library/zen-library.css
@@ -114,7 +114,6 @@
gap: 4px;
margin: 8px 6px;
padding: 0;
- box-sizing: border-box;
@media (-moz-platform: macos) {
font-size: 1rem; /* Slightly larger font on macOS */
@@ -131,7 +130,6 @@
margin-inline: 2px;
background: transparent;
border-radius: var(--border-radius-medium);
- box-sizing: border-box;
}
.search-urlbar-background {
@@ -155,7 +153,6 @@
gap: 0;
padding-inline: var(--urlbar-icon-padding, 9px);
padding-block: var(--urlbar-padding-block, 4px);
- box-sizing: border-box;
overflow: hidden;
& .search-icon {
@@ -243,7 +240,12 @@
fill-opacity: 0.5;
-moz-context-properties: fill, fill-opacity;
- &:hover {
+ & label {
+ opacity: .8;
+ }
+
+ &:hover,
+ &[open] {
background: var(--zen-toolbar-element-bg-hover);
}
@@ -264,6 +266,7 @@
overflow-y: auto;
scrollbar-width: auto;
scrollbar-color: color-mix(in srgb, currentColor 35%, transparent 65%) transparent;
+ -moz-window-dragging: drag;
@media (-moz-platform: macos) {
font-size: 1.25rem; /* Slightly larger font on macOS */
@@ -308,6 +311,7 @@
border-radius: 0;
color: inherit;
color-scheme: unset;
+ cursor: pointer;
overflow: clip;
}
@@ -315,6 +319,7 @@
display: grid;
position: relative;
width: 100%;
+ -moz-window-dragging: no-drag;
}
.library-item-background {
@@ -338,8 +343,9 @@
position: relative;
align-items: center;
min-width: 0;
- padding: 12px 10px;
+ padding: 0 10px;
overflow: clip;
+ height: 56px;
}
.library-item-icon-stack {
@@ -389,23 +395,76 @@
overflow: hidden;
}
-/* unfinished */
-.library-item-time {
+.library-item-side {
+ display: flex;
+ flex-direction: column;
flex-shrink: 0;
- margin-inline-start: calc(var(--toolbarbutton-inner-padding) - 0.5em); /* idk why this calc makes sense to me
- but it is inconsistant so please fix it if you have an idea */
- padding-inline-end: calc(var(--toolbarbutton-inner-padding) / 3);
- font-size: x-small;
+ align-items: flex-end;
+ justify-content: center;
+ gap: 2px;
+ margin-inline-start: 8px;
+ padding-inline-end: 4px;
+ text-align: end;
white-space: nowrap;
opacity: 0.65;
- display: none;
+ pointer-events: none;
+}
- .library-item-content:hover & {
- display: flex;
+.library-item-side-top {
+ font-size: 0.85em;
+}
+
+.library-item-side-bottom {
+ font-size: x-small;
+ opacity: 0.85;
+}
+
+.library-item-actions {
+ display: none;
+ flex-shrink: 0;
+ align-items: center;
+ gap: 4px;
+ margin-inline-start: 8px;
+}
+
+.library-item:hover .library-item-actions,
+.library-item:focus-within .library-item-actions {
+ display: flex;
+}
+
+.library-item-action-button {
+ display: flex;
+ flex-shrink: 0;
+ align-items: center;
+ justify-content: center;
+ width: 32px;
+ height: 32px;
+ padding: 6px;
+ appearance: none;
+ background: transparent;
+ border: none;
+ border-radius: calc(var(--border-radius-medium) - 6px);
+ color: inherit;
+ cursor: pointer;
+ fill: currentColor;
+ -moz-context-properties: fill, fill-opacity;
+
+ & img {
+ width: 18px;
+ height: 18px;
+ opacity: 0.75;
+ pointer-events: none;
+ }
+
+ &:hover {
+ background: color-mix(in srgb, currentColor 12%, transparent);
+ }
+
+ &:hover img {
+ opacity: 1;
}
}
-/* unfinished */
.empty-state {
display: flex;
flex: 1;