Connect a wallet in
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 @@