From badcfc694437b7f950cffd2133ff3f147fd865c1 Mon Sep 17 00:00:00 2001 From: thePR0M3TH3AN <53631862+PR0M3TH3AN@users.noreply.github.com> Date: Fri, 10 Oct 2025 22:52:58 -0400 Subject: [PATCH] Refactor zap popovers and more menus --- components/video-modal.html | 159 +++++++++++++++++++-------------- css/style.css | 29 ++---- docs/design-system.md | 32 +++++++ js/channelProfile.js | 38 +++++--- js/ui/components/VideoCard.js | 82 +++++++---------- js/ui/components/VideoModal.js | 46 ++++++++-- js/ui/moreMenuController.js | 22 ++++- js/ui/views/VideoListView.js | 4 +- views/channel-profile.html | 46 ++++++---- 9 files changed, 282 insertions(+), 176 deletions(-) diff --git a/components/video-modal.html b/components/video-modal.html index f86a28fc..9418318a 100644 --- a/components/video-modal.html +++ b/components/video-modal.html @@ -47,7 +47,7 @@ class="h-5 w-5" /> -
+
- -
- Boost on Nostr… + - - - -
- - -
diff --git a/css/style.css b/css/style.css index d3d1df39..1efc7a77 100644 --- a/css/style.css +++ b/css/style.css @@ -510,26 +510,6 @@ body.modal-open #sidebarOverlay { gap: 0.75rem; } -[data-modal-zap-wrapper] .zap-popover, -.zap-popover { - position: absolute; - top: calc(100% + 0.5rem); - right: 0; - z-index: 80; - width: max-content; -} - -.zap-popover__content { - width: 18rem; - max-width: calc(100vw - 2rem); - background: var(--color-overlay-veil); - color: var(--color-subtle); - border: 1px solid var(--color-border-translucent-stronger); - box-shadow: 0 22px 48px -18px var(--color-shadow-intense); - -webkit-backdrop-filter: blur(12px); - backdrop-filter: blur(12px); -} - .modal-actions__overflow { position: relative; } @@ -543,6 +523,15 @@ body.modal-open #sidebarOverlay { z-index: 70; } +.popover__panel[data-variant="zap"] { + background: var(--color-overlay-veil); + color: var(--color-subtle); + border: 1px solid var(--color-border-translucent-stronger); + box-shadow: 0 22px 48px -18px var(--color-shadow-intense); + -webkit-backdrop-filter: blur(12px); + backdrop-filter: blur(12px); +} + /* Mobile-specific styles */ @media (max-width: 767px) { #playerModal { diff --git a/docs/design-system.md b/docs/design-system.md index b4907bc0..b947eff3 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -157,6 +157,38 @@ Mix utilities such as `right-0` or `translate-x-1/2` on `.popover__panel` when y The helpers rely on layout utilities (`flex`, `gap-*`) so you can safely inject icons, shortcuts, or nested badges without restyling the base tokens. +### Zap Popover + +Compose `.popover` with a zap-specific panel variant when you need to surface the tipping form inline. Toggle `data-state="open"` on the `.popover__panel` and remove its `hidden` attribute when the dialog is visible. The `data-variant="zap"` hook reuses the frosted-glass styling defined in `css/style.css`. + +```html +
+ + +
+``` + ### Grid Stack `.grid-stack` offers gap tokens for dense layouts. Add `data-variant="cards"` for responsive card grids (`grid-cols-1` → `sm:grid-cols-2` → `xl:grid-cols-3`). Switch to `data-orientation="vertical"` for stacked flex columns that reuse the same spacing tokens. diff --git a/js/channelProfile.js b/js/channelProfile.js index 05727cdf..3d65a477 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -154,7 +154,7 @@ function setChannelZapVisibility(visible) { ? app.isUserLoggedIn() : Boolean(app?.normalizeHexPubkey?.(app?.pubkey)); const shouldShow = !!visible && isLoggedIn; - zapButton.classList.toggle("hidden", !shouldShow); + zapButton.toggleAttribute("hidden", !shouldShow); zapButton.disabled = !shouldShow; zapButton.setAttribute("aria-disabled", (!shouldShow).toString()); zapButton.setAttribute("aria-hidden", (!shouldShow).toString()); @@ -171,6 +171,7 @@ function setChannelZapVisibility(visible) { closeZapControls(); if (controls) { controls.setAttribute("aria-hidden", "true"); + controls.hidden = true; } if (amountInput) { amountInput.disabled = !shouldShow; @@ -233,6 +234,18 @@ function getZapControlsContainer() { if (!cachedZapControls) { cachedZapControls = document.getElementById("zapControls"); } + if (cachedZapControls) { + if (!cachedZapControls.dataset.state) { + const isHidden = + cachedZapControls.hasAttribute("hidden") || + cachedZapControls.getAttribute("aria-hidden") === "true"; + cachedZapControls.dataset.state = isHidden ? "closed" : "open"; + } + if (cachedZapControls.dataset.state !== "open") { + cachedZapControls.hidden = true; + cachedZapControls.setAttribute("aria-hidden", "true"); + } + } return cachedZapControls; } @@ -332,7 +345,7 @@ function setZapWalletPromptVisible(visible) { return; } const shouldShow = !!visible; - prompt.classList.toggle("hidden", !shouldShow); + prompt.toggleAttribute("hidden", !shouldShow); prompt.setAttribute("aria-hidden", (!shouldShow).toString()); } @@ -376,7 +389,8 @@ function openZapControls({ focus = false } = {}) { return false; } if (!zapControlsOpen) { - controls.classList.remove("hidden"); + controls.hidden = false; + controls.dataset.state = "open"; controls.setAttribute("aria-hidden", "false"); zapButton.setAttribute("aria-expanded", "true"); zapControlsOpen = true; @@ -391,8 +405,9 @@ function closeZapControls({ focusButton = false } = {}) { const controls = getZapControlsContainer(); const zapButton = getChannelZapButton(); if (controls) { - controls.classList.add("hidden"); + controls.dataset.state = "closed"; controls.setAttribute("aria-hidden", "true"); + controls.hidden = true; } if (zapButton) { zapButton.setAttribute("aria-expanded", "false"); @@ -1274,12 +1289,12 @@ async function updateChannelMenuState() { if (action === "copy-npub") { if (currentChannelNpub) { button.dataset.npub = currentChannelNpub; - button.classList.remove("hidden"); + button.removeAttribute("hidden"); button.setAttribute("aria-hidden", "false"); } else { delete button.dataset.npub; - button.classList.add("hidden"); button.setAttribute("aria-hidden", "true"); + button.setAttribute("hidden", ""); } return; } @@ -1287,12 +1302,12 @@ async function updateChannelMenuState() { if (action === "block-author") { if (currentChannelHex) { button.dataset.author = currentChannelHex; - button.classList.remove("hidden"); + button.removeAttribute("hidden"); button.setAttribute("aria-hidden", "false"); } else { delete button.dataset.author; - button.classList.add("hidden"); button.setAttribute("aria-hidden", "true"); + button.setAttribute("hidden", ""); } return; } @@ -1305,13 +1320,13 @@ async function updateChannelMenuState() { } else { delete button.dataset.npub; } - button.classList.remove("hidden"); + button.removeAttribute("hidden"); button.setAttribute("aria-hidden", "false"); } else { delete button.dataset.author; delete button.dataset.npub; - button.classList.add("hidden"); button.setAttribute("aria-hidden", "true"); + button.setAttribute("hidden", ""); } } }); @@ -1421,7 +1436,8 @@ function setupZapButton() { } setChannelZapVisibility(false); - controls.classList.add("hidden"); + controls.dataset.state = "closed"; + controls.hidden = true; controls.setAttribute("aria-hidden", "true"); zapButton.setAttribute("aria-expanded", "false"); setupZapWalletLink(); diff --git a/js/ui/components/VideoCard.js b/js/ui/components/VideoCard.js index 1b28ac5d..549a0c4c 100644 --- a/js/ui/components/VideoCard.js +++ b/js/ui/components/VideoCard.js @@ -194,7 +194,9 @@ export class VideoCard { closeMoreMenu() { if (this.moreMenu) { - this.moreMenu.classList.add("hidden"); + this.moreMenu.dataset.state = "closed"; + this.moreMenu.setAttribute("aria-hidden", "true"); + this.moreMenu.hidden = true; } if (this.moreMenuButton) { this.moreMenuButton.setAttribute("aria-expanded", "false"); @@ -661,7 +663,7 @@ export class VideoCard { buildMoreMenu() { const wrapper = this.createElement("div", { - classNames: ["relative", "inline-block", "ml-1", "overflow-visible"], + classNames: ["popover", "ml-1"], }); wrapper.dataset.moreMenuWrapper = "true"; @@ -698,43 +700,38 @@ export class VideoCard { button.appendChild(icon); const dropdown = this.createElement("div", { - classNames: [ - "hidden", - "absolute", - "right-0", - "bottom-full", - "mb-2", - "w-40", - "rounded-md", - "shadow-lg", - "bg-gray-800", - "ring-1", - "ring-black", - "ring-opacity-5", - "z-50", - ], + classNames: ["popover__panel", "w-40", "p-0"], + attrs: { + hidden: "", + "data-state": "closed", + "aria-hidden": "true", + }, }); dropdown.id = `moreDropdown-${this.index}`; dropdown.dataset.moreMenu = "true"; dropdown.setAttribute("role", "menu"); const list = this.createElement("div", { - classNames: ["py-1"], + classNames: ["menu"], + attrs: { role: "none" }, }); - const addActionButton = (text, action, extraDataset = {}, classes = []) => { + const addActionButton = (text, action, extraDataset = {}, options = {}) => { + const { variant = null } = options || {}; const btn = this.createElement("button", { classNames: [ - "block", - "w-full", - "text-left", - "px-4", - "py-2", - "text-sm", - ...classes, + "menu__item", + "justify-start", ], textContent: text, + attrs: { + type: "button", + role: "menuitem", + }, }); + if (variant) { + btn.dataset.variant = variant; + } btn.dataset.action = action; Object.entries(extraDataset || {}).forEach(([key, value]) => { if (value === undefined || value === null) { @@ -748,11 +745,11 @@ export class VideoCard { addActionButton("Open channel", "open-channel", { author: this.video.pubkey || "", - }, ["text-gray-100", "hover:bg-gray-700"]); + }); addActionButton("Copy link", "copy-link", { eventId: this.video.id || "", - }, ["text-gray-100", "hover:bg-gray-700"]); + }); const pointer = Array.isArray(this.pointerInfo?.pointer) ? this.pointerInfo.pointer @@ -782,16 +779,7 @@ export class VideoCard { } const boostLabel = this.createElement("div", { - classNames: [ - "px-4", - "pt-2", - "pb-1", - "text-xs", - "font-semibold", - "uppercase", - "tracking-wide", - "text-gray-400", - ], + classNames: ["menu__heading"], textContent: "Boost on Nostr…", }); list.appendChild(boostLabel); @@ -800,7 +788,6 @@ export class VideoCard { "Repost (kind 6)", "repost-event", baseBoostDataset, - ["text-gray-100", "hover:bg-gray-700"], ); if (this.playbackUrl && this.video.isPrivate !== true) { @@ -819,7 +806,6 @@ export class VideoCard { "Mirror (kind 1063)", "mirror-video", mirrorDataset, - ["text-gray-100", "hover:bg-gray-700"], ); } @@ -832,12 +818,12 @@ export class VideoCard { "Rebroadcast", "ensure-presence", ensureDataset, - ["text-gray-100", "hover:bg-gray-700"], ); list.appendChild( this.createElement("div", { - classNames: ["my-1", "border-t", "border-gray-700", "opacity-70"], + classNames: ["menu__separator"], + attrs: { role: "separator" }, }) ); @@ -855,7 +841,6 @@ export class VideoCard { pointerRelay: historyPointerRelay || "", reason: "remove-item", }, - ["text-gray-100", "hover:bg-gray-700"] ); removeButton.setAttribute( "title", @@ -873,7 +858,7 @@ export class VideoCard { "Blacklist creator", "blacklist-author", { author: this.video.pubkey || "" }, - ["text-red-400", "hover:bg-red-700", "hover:text-white"] + { variant: "critical" } ); } @@ -881,14 +866,13 @@ export class VideoCard { "Block creator", "block-author", { author: this.video.pubkey || "" }, - ["text-red-400", "hover:bg-red-700", "hover:text-white"] + { variant: "critical" } ); addActionButton( "Report", "report", { eventId: this.video.id || "" }, - ["text-gray-100", "hover:bg-gray-700"] ); dropdown.appendChild(list); @@ -1563,12 +1547,14 @@ export class VideoCard { this.moreMenuButton.addEventListener("click", (event) => { event.preventDefault(); event.stopPropagation(); - const willOpen = this.moreMenu.classList.contains("hidden"); + const willOpen = this.moreMenu.dataset.state !== "open"; if (this.onRequestCloseAllMenus) { this.onRequestCloseAllMenus(this); } if (willOpen) { - this.moreMenu.classList.remove("hidden"); + this.moreMenu.hidden = false; + this.moreMenu.dataset.state = "open"; + this.moreMenu.setAttribute("aria-hidden", "false"); this.moreMenuButton.setAttribute("aria-expanded", "true"); } }); diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index 210b5a82..56f72fcf 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -264,6 +264,32 @@ export class VideoModal { this.modalZapWalletLink = playerModal.querySelector("#modalZapWalletLink") || null; + if (this.modalZapDialog) { + if (!this.modalZapDialog.dataset.state) { + const isHidden = + this.modalZapDialog.hasAttribute("hidden") || + this.modalZapDialog.getAttribute("aria-hidden") === "true"; + this.modalZapDialog.dataset.state = isHidden ? "closed" : "open"; + } + if (this.modalZapDialog.dataset.state !== "open") { + this.modalZapDialog.hidden = true; + this.modalZapDialog.setAttribute("aria-hidden", "true"); + } + } + + if (this.modalMoreMenu) { + if (!this.modalMoreMenu.dataset.state) { + const isHidden = + this.modalMoreMenu.hasAttribute("hidden") || + this.modalMoreMenu.getAttribute("aria-hidden") === "true"; + this.modalMoreMenu.dataset.state = isHidden ? "closed" : "open"; + } + if (this.modalMoreMenu.dataset.state !== "open") { + this.modalMoreMenu.hidden = true; + this.modalMoreMenu.setAttribute("aria-hidden", "true"); + } + } + const closeButton = playerModal.querySelector("#closeModal"); if (closeButton) { closeButton.addEventListener("click", () => { @@ -626,7 +652,7 @@ export class VideoModal { setZapVisibility(visible) { const shouldShow = !!visible; if (this.modalZapBtn) { - this.modalZapBtn.classList.toggle("hidden", !shouldShow); + this.modalZapBtn.toggleAttribute("hidden", !shouldShow); const disableButton = !shouldShow || this.modalZapPending; this.modalZapBtn.disabled = disableButton; this.modalZapBtn.setAttribute("aria-disabled", (!shouldShow).toString()); @@ -656,7 +682,7 @@ export class VideoModal { return; } const shouldShow = !!visible; - this.modalZapWalletPrompt.classList.toggle("hidden", !shouldShow); + this.modalZapWalletPrompt.toggleAttribute("hidden", !shouldShow); this.modalZapWalletPrompt.setAttribute( "aria-hidden", (!shouldShow).toString() @@ -667,7 +693,8 @@ export class VideoModal { if (!this.modalZapDialog) { return; } - this.modalZapDialog.classList.remove("hidden"); + this.modalZapDialog.hidden = false; + this.modalZapDialog.dataset.state = "open"; this.modalZapDialogOpen = true; this.modalZapDialog.setAttribute("aria-hidden", "false"); if (this.modalZapBtn) { @@ -681,8 +708,9 @@ export class VideoModal { return; } if (this.modalZapDialogOpen) { - this.modalZapDialog.classList.add("hidden"); + this.modalZapDialog.dataset.state = "closed"; this.modalZapDialog.setAttribute("aria-hidden", "true"); + this.modalZapDialog.hidden = true; this.modalZapDialogOpen = false; if (this.modalZapBtn) { this.modalZapBtn.setAttribute("aria-expanded", "false"); @@ -918,7 +946,7 @@ export class VideoModal { this.modalZapBtn.disabled = true; this.modalZapBtn.setAttribute("aria-busy", "true"); this.modalZapBtn.classList.add("opacity-50", "pointer-events-none"); - } else if (!this.modalZapBtn.classList.contains("hidden")) { + } else if (!this.modalZapBtn.hasAttribute("hidden")) { this.modalZapBtn.disabled = false; this.modalZapBtn.removeAttribute("aria-busy"); this.modalZapBtn.classList.remove("opacity-50", "pointer-events-none"); @@ -1106,12 +1134,12 @@ export class VideoModal { if (action === "blacklist-author") { if (canManageBlacklist && currentVideo?.pubkey) { button.dataset.author = currentVideo.pubkey; - button.classList.remove("hidden"); + button.removeAttribute("hidden"); button.setAttribute("aria-hidden", "false"); } else { delete button.dataset.author; - button.classList.add("hidden"); button.setAttribute("aria-hidden", "true"); + button.setAttribute("hidden", ""); } return; } @@ -1161,7 +1189,7 @@ export class VideoModal { const isPrivate = currentVideo?.isPrivate === true; if (hasUrl && !isPrivate) { - button.classList.remove("hidden"); + button.removeAttribute("hidden"); button.setAttribute("aria-hidden", "false"); button.dataset.eventId = currentVideo.id || ""; button.dataset.author = currentVideo.pubkey || ""; @@ -1181,8 +1209,8 @@ export class VideoModal { delete button.dataset.description; delete button.dataset.title; button.dataset.isPrivate = "true"; - button.classList.add("hidden"); button.setAttribute("aria-hidden", "true"); + button.setAttribute("hidden", ""); } return; } diff --git a/js/ui/moreMenuController.js b/js/ui/moreMenuController.js index aac09fe8..197986e9 100644 --- a/js/ui/moreMenuController.js +++ b/js/ui/moreMenuController.js @@ -226,7 +226,9 @@ export default class MoreMenuController { const menus = this.document.querySelectorAll("[data-more-menu]"); menus.forEach((menu) => { if (!HTMLElementCtor || menu instanceof HTMLElementCtor) { - menu.classList.add("hidden"); + menu.dataset.state = "closed"; + menu.setAttribute("aria-hidden", "true"); + menu.hidden = true; } }); @@ -278,11 +280,25 @@ export default class MoreMenuController { return; } - const willOpen = dropdown.classList.contains("hidden"); + if (dropdown && !dropdown.dataset.state) { + const isHidden = + dropdown.hasAttribute("hidden") || + dropdown.getAttribute("aria-hidden") === "true"; + dropdown.dataset.state = isHidden ? "closed" : "open"; + } + + if (dropdown && dropdown.dataset.state !== "open") { + dropdown.hidden = true; + dropdown.setAttribute("aria-hidden", "true"); + } + + const willOpen = dropdown?.dataset.state !== "open"; this.closeAllMoreMenus(); if (willOpen) { - dropdown.classList.remove("hidden"); + dropdown.hidden = false; + dropdown.dataset.state = "open"; + dropdown.setAttribute("aria-hidden", "false"); button.setAttribute("aria-expanded", "true"); } }); diff --git a/js/ui/views/VideoListView.js b/js/ui/views/VideoListView.js index ae45d45d..df38d4df 100644 --- a/js/ui/views/VideoListView.js +++ b/js/ui/views/VideoListView.js @@ -1001,7 +1001,9 @@ export class VideoListView { const menus = this.document.querySelectorAll("[data-more-menu]"); menus.forEach((menu) => { if (menu instanceof HTMLElement) { - menu.classList.add("hidden"); + menu.dataset.state = "closed"; + menu.setAttribute("aria-hidden", "true"); + menu.hidden = true; } }); diff --git a/views/channel-profile.html b/views/channel-profile.html index b8cfa62e..9fda3ded 100644 --- a/views/channel-profile.html +++ b/views/channel-profile.html @@ -29,7 +29,7 @@
-
+
-
+