diff --git a/css/tailwind.source.css b/css/tailwind.source.css index 81d31079..54a56e3e 100644 --- a/css/tailwind.source.css +++ b/css/tailwind.source.css @@ -448,6 +448,11 @@ .popover-panel, .popover__panel { @apply focus-ring pointer-events-none z-overlay-popover min-w-menu max-w-popover-safe scale-95 rounded-overlay border border-border border-opacity-40 bg-overlay-panel px-overlay-panel-inline py-overlay-panel-block text-sm text-text opacity-0 shadow-overlay-panel transition-opacity transition-transform duration-base ease-out; + position: var(--popover-strategy, fixed); + left: var(--popover-left, 0px); + top: var(--popover-top, 0px); + max-width: var(--popover-max-width, none); + max-height: var(--popover-max-height, none); } .popover-panel[data-state="open"], @@ -460,6 +465,13 @@ @apply pointer-events-none scale-95 opacity-0; } + [data-popover-arrow="true"] { + left: var(--popover-arrow-left, auto); + right: var(--popover-arrow-right, auto); + top: var(--popover-arrow-top, auto); + bottom: var(--popover-arrow-bottom, auto); + } + .p-overlay-panel { @apply px-overlay-panel-inline py-overlay-panel-block; } @@ -808,6 +820,10 @@ @apply inline-flex flex-col items-start gap-sm rounded-xl border px-3 py-1.5 text-xs font-semibold normal-case tracking-tight shadow-sm; } + .moderation-badge--interactive { + pointer-events: auto; + } + .moderation-badge__actions { @apply inline-flex flex-nowrap items-center gap-sm; } @@ -2104,6 +2120,9 @@ @apply relative block shrink-0 overflow-hidden rounded-lg bg-surface-alt; width: 168px; aspect-ratio: 16 / 9; + background-image: var(--similar-card-thumb-url, none); + background-position: center; + background-size: cover; } @media (max-width: theme("screens.sm")) { @@ -2126,6 +2145,7 @@ .player-modal__similar-card-title { @apply text-sm font-semibold leading-snug text-text transition-colors duration-base ease-out; + min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; @@ -2160,6 +2180,7 @@ .player-modal__similar-card-meta { @apply flex flex-wrap items-center gap-1 text-2xs text-muted-strong; + min-width: 0; } .player-modal__similar-card-timestamp { @@ -2442,6 +2463,8 @@ inset-block-start: 0; block-size: 100%; max-inline-size: 100%; + inline-size: var(--video-modal-reaction-fill, 0%); + width: var(--video-modal-reaction-fill, 0%); background-image: linear-gradient( 90deg, var(--color-accent) 0%, @@ -3338,6 +3361,7 @@ .qr-code__table { border: 0; border-collapse: collapse; + margin: var(--qr-code-table-margin, 0); } .qr-code__cell { @@ -3354,6 +3378,22 @@ background-color: var(--qr-code-color-dark); } + .qr-code__container[data-qr-code-mode="image"] .qr-code__canvas { + display: none; + } + + .qr-code__container[data-qr-code-mode="image"] .qr-code__image { + display: block; + } + + .qr-code__container[data-qr-code-mode="canvas"] .qr-code__canvas { + display: block; + } + + .qr-code__container[data-qr-code-mode="canvas"] .qr-code__image { + display: none; + } + .fade-in { opacity: 0; animation: interface-fade-in 280ms ease-out forwards; @@ -3568,18 +3608,26 @@ z-index: var(--z-modal-content); } - #modalContainer > #profileModal { + #modalContainer > #profileModal, + #profileModal[data-modal-stack="top"] { z-index: var(--z-modal-top-root); } - #modalContainer > #profileModal > .bv-modal-backdrop { + #modalContainer > #profileModal > .bv-modal-backdrop, + #profileModal[data-modal-stack="top"] > .bv-modal-backdrop { z-index: var(--z-modal-top-overlay); } - #modalContainer > #profileModal .bv-modal__panel { + #modalContainer > #profileModal .bv-modal__panel, + #profileModal[data-modal-stack="top"] .bv-modal__panel { z-index: var(--z-modal-top-content); } + .bv-modal[data-nested-modal-active="true"] { + pointer-events: none; + visibility: hidden; + } + #modalContainer > #loginModal { z-index: var(--z-modal-peak-root); } @@ -4389,6 +4437,26 @@ scrollbar-gutter: stable both-edges; } + .sidebar-dropup-panel__inner.is-measuring { + position: absolute; + width: max-content; + max-width: none; + visibility: hidden; + pointer-events: none; + left: -9999px; + right: auto; + top: auto; + bottom: auto; + } + + .sidebar-dropup-measure { + position: absolute; + visibility: hidden; + pointer-events: none; + height: 0; + overflow: hidden; + } + .sidebar-dropup-panel__inner > * { width: 100%; min-width: 0; diff --git a/docs/moderation/fixtures/index.html b/docs/moderation/fixtures/index.html index 967da4f3..372e3936 100644 --- a/docs/moderation/fixtures/index.html +++ b/docs/moderation/fixtures/index.html @@ -34,6 +34,16 @@ color: #cbd5f5; margin-bottom: 1rem; } + .fixture-toggle { + margin-top: 0.75rem; + font-size: 0.875rem; + color: #cbd5f5; + } + .fixture-toggle__label { + display: inline-flex; + align-items: center; + gap: 0.5rem; + }
@@ -581,14 +591,10 @@ if (fixture.blockToggle && fixture.blockToggle.pubkey) { const toggleWrapper = document.createElement("div"); - toggleWrapper.style.marginTop = "0.75rem"; - toggleWrapper.style.fontSize = "0.875rem"; - toggleWrapper.style.color = "#cbd5f5"; + toggleWrapper.classList.add("fixture-toggle"); const label = document.createElement("label"); - label.style.display = "inline-flex"; - label.style.alignItems = "center"; - label.style.gap = "0.5rem"; + label.classList.add("fixture-toggle__label"); const input = document.createElement("input"); input.type = "checkbox"; diff --git a/js/channelProfile.js b/js/channelProfile.js index 0d33fb1f..d6221adf 100644 --- a/js/channelProfile.js +++ b/js/channelProfile.js @@ -90,6 +90,53 @@ const channelModerationBadgeState = { }; const channelVideoCardsById = new Map(); +let channelBannerStyleCounter = 0; +const channelBannerStyleRegistry = new WeakMap(); + +function getChannelBannerStyleState(doc) { + if (!doc || typeof doc.createElement !== "function") { + return null; + } + const existing = channelBannerStyleRegistry.get(doc); + if (existing) { + return existing; + } + let styleNode = doc.getElementById?.("channelBannerStyles") || null; + if (!(styleNode instanceof HTMLStyleElement)) { + styleNode = doc.createElement("style"); + styleNode.id = "channelBannerStyles"; + doc.head?.appendChild(styleNode); + } + const state = { + styleNode, + rules: new Map(), + }; + channelBannerStyleRegistry.set(doc, state); + return state; +} + +function updateChannelBannerBackground(el, url) { + const doc = el?.ownerDocument || (typeof document !== "undefined" ? document : null); + const state = getChannelBannerStyleState(doc); + if (!state || !el) { + return; + } + + if (!el.dataset.channelBannerStyleId) { + channelBannerStyleCounter += 1; + el.dataset.channelBannerStyleId = `channel-banner-${channelBannerStyleCounter}`; + } + const selector = `[data-channel-banner-style-id="${el.dataset.channelBannerStyleId}"]`; + + if (url) { + const escaped = url.replace(/(["\\])/g, "\\$1"); + state.rules.set(selector, `${selector} { background-image: url("${escaped}"); }`); + } else { + state.rules.set(selector, `${selector} { background-image: none; }`); + } + + state.styleNode.textContent = [...state.rules.values()].join("\n"); +} export function clearChannelVideoCardRegistry() { channelVideoCardsById.clear(); @@ -1432,10 +1479,7 @@ function setBannerVisual(el, url, { referrerPolicy } = {}) { el.src = resolvedUrl; } } else { - const value = resolvedUrl ? `url("${resolvedUrl}")` : ""; - if (el.style.backgroundImage !== value) { - el.style.backgroundImage = value; - } + updateChannelBannerBackground(el, resolvedUrl || ""); } if (el.dataset.bannerSrc !== resolvedUrl) { diff --git a/js/index.js b/js/index.js index f74e90b6..9284bd82 100644 --- a/js/index.js +++ b/js/index.js @@ -885,22 +885,67 @@ async function bootstrapInterface() { }; }; + const sidebarDropupStyleState = (() => { + let styleNode = null; + let rootWidthRule = ""; + const measureRules = new Map(); + + const ensureStyleNode = () => { + if (styleNode && styleNode.isConnected) { + return styleNode; + } + styleNode = document.getElementById("sidebarDropupStyles"); + if (!(styleNode instanceof HTMLStyleElement)) { + styleNode = document.createElement("style"); + styleNode.id = "sidebarDropupStyles"; + document.head.appendChild(styleNode); + } + return styleNode; + }; + + const updateStyles = () => { + const node = ensureStyleNode(); + const rules = [rootWidthRule, ...measureRules.values()].filter(Boolean); + node.textContent = rules.join("\n"); + }; + + return { + setRootWidth: (value) => { + rootWidthRule = `:root { --sidebar-dropup-content-width: ${value}; }`; + updateStyles(); + }, + setMeasureWidth: (id, value) => { + measureRules.set( + id, + `.sidebar-dropup-measure[data-measure-id="${id}"] { width: ${value}; }`, + ); + updateStyles(); + }, + clearMeasure: (id) => { + measureRules.delete(id); + updateStyles(); + }, + }; + })(); + + let sidebarDropupMeasureId = 0; + const resolveCssLengthToPixels = (value, container) => { if (!(container instanceof HTMLElement) || typeof value !== "string") { return 0; } const measurementNode = container.ownerDocument.createElement("div"); - measurementNode.style.position = "absolute"; - measurementNode.style.visibility = "hidden"; - measurementNode.style.pointerEvents = "none"; - measurementNode.style.height = "0"; - measurementNode.style.width = value; - measurementNode.style.overflow = "hidden"; + sidebarDropupMeasureId += 1; + const measureId = `sidebar-dropup-measure-${sidebarDropupMeasureId}`; + measurementNode.classList.add("sidebar-dropup-measure"); + measurementNode.dataset.measureId = measureId; + sidebarDropupStyleState.setMeasureWidth(measureId, value); container.appendChild(measurementNode); const pixels = measurementNode.getBoundingClientRect().width; container.removeChild(measurementNode); + sidebarDropupStyleState.clearMeasure(measureId); return Number.isFinite(pixels) ? pixels : 0; }; @@ -910,46 +955,13 @@ async function bootstrapInterface() { const panel = panelInner?.closest(".sidebar-dropup-panel"); if (!(panelInner instanceof HTMLElement) || !(panel instanceof HTMLElement)) { - document.documentElement.style.setProperty( - "--sidebar-dropup-content-width", - "0px", - ); + sidebarDropupStyleState.setRootWidth("0px"); return; } - const originalStyles = { - position: panelInner.style.position, - width: panelInner.style.width, - maxWidth: panelInner.style.maxWidth, - visibility: panelInner.style.visibility, - pointerEvents: panelInner.style.pointerEvents, - left: panelInner.style.left, - right: panelInner.style.right, - top: panelInner.style.top, - bottom: panelInner.style.bottom, - }; - - panelInner.style.position = "absolute"; - panelInner.style.width = "max-content"; - panelInner.style.maxWidth = "none"; - panelInner.style.visibility = "hidden"; - panelInner.style.pointerEvents = "none"; - panelInner.style.left = "-9999px"; - panelInner.style.right = "auto"; - panelInner.style.top = "auto"; - panelInner.style.bottom = "auto"; - + panelInner.classList.add("is-measuring"); const measuredInnerWidth = Math.ceil(panelInner.scrollWidth); - - panelInner.style.position = originalStyles.position; - panelInner.style.width = originalStyles.width; - panelInner.style.maxWidth = originalStyles.maxWidth; - panelInner.style.visibility = originalStyles.visibility; - panelInner.style.pointerEvents = originalStyles.pointerEvents; - panelInner.style.left = originalStyles.left; - panelInner.style.right = originalStyles.right; - panelInner.style.top = originalStyles.top; - panelInner.style.bottom = originalStyles.bottom; + panelInner.classList.remove("is-measuring"); const panelStyles = window.getComputedStyle(panel); const paddingInlineStart = Number.parseFloat(panelStyles.paddingInlineStart) || 0; @@ -965,10 +977,7 @@ async function bootstrapInterface() { measuredInnerWidth + paddingInlineStart + paddingInlineEnd + scrollReserve, ); - document.documentElement.style.setProperty( - "--sidebar-dropup-content-width", - `${Math.ceil(totalWidth)}px`, - ); + sidebarDropupStyleState.setRootWidth(`${Math.ceil(totalWidth)}px`); }; const debouncedSidebarDropupResize = debounce(updateSidebarDropupContentWidth, 150); diff --git a/js/ui/components/SimilarContentCard.js b/js/ui/components/SimilarContentCard.js index dcb2a9ca..f844e4bd 100644 --- a/js/ui/components/SimilarContentCard.js +++ b/js/ui/components/SimilarContentCard.js @@ -6,6 +6,48 @@ import { buildModerationBadgeText } from "../moderationCopy.js"; const SVG_NAMESPACE = "http://www.w3.org/2000/svg"; const DEFAULT_PROFILE_AVATAR = "assets/svg/default-profile.svg"; +let similarCardIdCounter = 0; +const similarCardStyleRegistry = new WeakMap(); + +function getSimilarCardStyleState(doc) { + if (!doc || typeof doc.createElement !== "function") { + return null; + } + const existing = similarCardStyleRegistry.get(doc); + if (existing) { + return existing; + } + let styleNode = doc.getElementById?.("similarContentCardStyles") || null; + if (!(styleNode instanceof HTMLStyleElement)) { + styleNode = doc.createElement("style"); + styleNode.id = "similarContentCardStyles"; + doc.head?.appendChild(styleNode); + } + const state = { + styleNode, + rules: new Map(), + }; + similarCardStyleRegistry.set(doc, state); + return state; +} + +function updateSimilarCardBackdrop(doc, cardId, url) { + const state = getSimilarCardStyleState(doc); + if (!state || !cardId) { + return; + } + + if (url) { + state.rules.set( + cardId, + `[data-similar-card-id="${cardId}"] { --similar-card-thumb-url: url("${url}"); }`, + ); + } else { + state.rules.delete(cardId); + } + + state.styleNode.textContent = [...state.rules.values()].join("\n"); +} export class SimilarContentCard { constructor({ @@ -60,6 +102,7 @@ export class SimilarContentCard { this.callbacks = { onPlay: null }; this.root = null; + this.cardStyleId = ""; this.mediaLinkEl = null; this.thumbnailEl = null; this.contentEl = null; @@ -330,6 +373,9 @@ export class SimilarContentCard { if (this.video.id) { root.dataset.videoId = this.video.id; } + similarCardIdCounter += 1; + this.cardStyleId = `similar-card-${similarCardIdCounter}`; + root.dataset.similarCardId = this.cardStyleId; const dsMode = this.designSystem?.getMode?.(); if (dsMode) { root.setAttribute("data-ds", dsMode); @@ -378,14 +424,11 @@ export class SimilarContentCard { } const badge = this.document.createElement("div"); - badge.className = "moderation-badge opacity-95"; + badge.className = "moderation-badge moderation-badge--interactive opacity-95"; badge.dataset.variant = "warning"; badge.dataset.moderationBadge = "true"; badge.dataset.moderationState = "trusted-mute"; - // Position it centrally over the thumbnail like in VideoCard - badge.style.pointerEvents = "auto"; - const label = this.document.createElement("span"); label.className = "moderation-badge__label inline-flex items-center gap-xs"; @@ -556,14 +599,12 @@ export class SimilarContentCard { buildContentSection() { const content = this.document.createElement("div"); content.classList.add("player-modal__similar-card-content"); - content.style.minWidth = "0"; const titleLink = this.document.createElement("a"); titleLink.classList.add("player-modal__similar-card-title"); titleLink.href = this.shareUrl; titleLink.textContent = this.video.title || "Untitled"; titleLink.title = this.video.title || "Untitled"; - titleLink.style.minWidth = "0"; const authorStack = this.buildAuthorStack(); const metaRow = this.buildMetaRow(); @@ -588,11 +629,9 @@ export class SimilarContentCard { buildAuthorStack() { const wrapper = this.document.createElement("div"); wrapper.classList.add("player-modal__similar-card-author"); - wrapper.style.minWidth = "0"; const textWrapper = this.document.createElement("span"); textWrapper.classList.add("player-modal__similar-card-author-meta"); - textWrapper.style.minWidth = "0"; const nameEl = this.document.createElement("span"); nameEl.classList.add("author-name", "player-modal__similar-card-author-name"); @@ -653,7 +692,6 @@ export class SimilarContentCard { buildMetaRow() { const row = this.document.createElement("div"); row.classList.add("player-modal__similar-card-meta"); - row.style.minWidth = "0"; const timeEl = this.document.createElement("time"); timeEl.classList.add("player-modal__similar-card-timestamp"); @@ -870,12 +908,10 @@ export class SimilarContentCard { } setCardBackdropImage(src) { - if (!this.root || !this.root.style) { + if (!this.root || !this.cardStyleId) { return; } - const style = this.root.style; - const normalizeSource = (raw) => { if (typeof raw !== "string") { return ""; @@ -919,12 +955,9 @@ export class SimilarContentCard { const sanitized = normalizeSource(src); if (sanitized) { const escaped = sanitized.replace(/(["\\])/g, "\\$1"); - style.setProperty( - "--similar-card-thumb-url", - `url("${escaped}")` - ); + updateSimilarCardBackdrop(this.document, this.cardStyleId, escaped); } else { - style.removeProperty("--similar-card-thumb-url"); + updateSimilarCardBackdrop(this.document, this.cardStyleId, ""); } } } diff --git a/js/ui/components/VideoModal.js b/js/ui/components/VideoModal.js index 4a80168b..955748a8 100644 --- a/js/ui/components/VideoModal.js +++ b/js/ui/components/VideoModal.js @@ -153,6 +153,7 @@ export class VideoModal { "+": null, "-": null, }; + this.reactionMeterStyleNode = null; this.reactionState = { counts: { "+": 0, "-": 0 }, total: 0, @@ -3116,9 +3117,32 @@ export class VideoModal { this.reactionMeterAssistive.textContent = assistiveLabel; } - if (this.reactionMeterFill) { - this.reactionMeterFill.style.inlineSize = `${likeRatio}%`; - this.reactionMeterFill.style.width = `${likeRatio}%`; + this.updateReactionMeterFill(likeRatio); + } + + updateReactionMeterFill(likeRatio) { + const doc = this.playerModal?.ownerDocument || this.document; + if (!doc) { + return; + } + + const clamped = Math.max(0, Math.min(100, Math.round(likeRatio))); + const widthValue = `${clamped}%`; + if (!this.reactionMeterStyleNode || !this.reactionMeterStyleNode.isConnected) { + const existing = doc.getElementById?.("videoModalReactionStyles"); + if (existing instanceof HTMLStyleElement) { + this.reactionMeterStyleNode = existing; + } else { + const styleNode = doc.createElement("style"); + styleNode.id = "videoModalReactionStyles"; + doc.head?.appendChild(styleNode); + this.reactionMeterStyleNode = styleNode; + } + } + + if (this.reactionMeterStyleNode) { + this.reactionMeterStyleNode.textContent = + `#playerModal { --video-modal-reaction-fill: ${widthValue}; }`; } } @@ -4021,9 +4045,6 @@ export class VideoModal { this.videoTitle.textContent = titleText; // Ensure element is visible this.videoTitle.hidden = false; - if (this.videoTitle.style) { - this.videoTitle.style.display = ""; - } } else { // Log for debugging if element cannot be found this.logger?.log?.( diff --git a/js/ui/overlay/popoverEngine.js b/js/ui/overlay/popoverEngine.js index 7eb50c65..ef0e9ea2 100644 --- a/js/ui/overlay/popoverEngine.js +++ b/js/ui/overlay/popoverEngine.js @@ -20,6 +20,7 @@ const DEFAULT_MAX_WIDTH_TOKEN = "--popover-inline-safe-max"; const MENU_TYPEAHEAD_RESET_MS = 500; let popoverPanelIdCounter = 0; +let popoverArrowIdCounter = 0; let activePopoverInstance = null; @@ -115,22 +116,39 @@ function resolveArrow(option, panel) { return null; } -function applyArrowStyles({ arrowElement, placement, middlewareData }) { +function ensureArrowId(arrowElement) { + if (!arrowElement) { + return ""; + } + if (arrowElement.id) { + return arrowElement.id; + } + popoverArrowIdCounter += 1; + const arrowId = `popover-arrow-${popoverArrowIdCounter}`; + arrowElement.id = arrowId; + return arrowId; +} + +function buildArrowStyleState({ arrowElement, placement, middlewareData }) { + const state = { + basePlacement: "", + values: { + left: "auto", + right: "auto", + top: "auto", + bottom: "auto", + }, + }; + if (!arrowElement || typeof placement !== "string") { - return; + return state; } const basePlacement = placement.split("-")[0]; const arrowData = middlewareData?.arrow || null; - arrowElement.style.left = ""; - arrowElement.style.right = ""; - arrowElement.style.top = ""; - arrowElement.style.bottom = ""; - if (!arrowData || !basePlacement) { - delete arrowElement.dataset.popoverArrowSide; - return; + return state; } const staticSideMap = { @@ -141,11 +159,11 @@ function applyArrowStyles({ arrowElement, placement, middlewareData }) { }; if (Number.isFinite(arrowData.x)) { - arrowElement.style.left = `${Math.round(arrowData.x)}px`; + state.values.left = `${Math.round(arrowData.x)}px`; } if (Number.isFinite(arrowData.y)) { - arrowElement.style.top = `${Math.round(arrowData.y)}px`; + state.values.top = `${Math.round(arrowData.y)}px`; } const staticSide = staticSideMap[basePlacement] || null; @@ -161,10 +179,11 @@ function applyArrowStyles({ arrowElement, placement, middlewareData }) { basePlacement === "top" || basePlacement === "bottom" ? arrowHeight : arrowWidth; - arrowElement.style[staticSide] = `${-Math.round(offsetValue)}px`; + state.values[staticSide] = `${-Math.round(offsetValue)}px`; } - arrowElement.dataset.popoverArrowSide = basePlacement; + state.basePlacement = basePlacement; + return state; } function containsTarget(root, target) { @@ -238,6 +257,67 @@ export function createPopover(trigger, render, options = {}) { typeaheadBuffer: "", typeaheadTimeout: null, }; + let popoverStyleNode = null; + let arrowStyleState = { + left: "auto", + right: "auto", + top: "auto", + bottom: "auto", + }; + let panelStyleState = { + left: 0, + top: 0, + strategy, + maxWidth: "none", + maxHeight: "none", + }; + let arrowId = ""; + + function ensurePopoverStyleNode(panelId) { + if (popoverStyleNode && popoverStyleNode.isConnected) { + return popoverStyleNode; + } + const styleId = `popover-style-${panelId}`; + const existing = documentRef?.getElementById?.(styleId) || null; + if (existing instanceof HTMLStyleElement) { + popoverStyleNode = existing; + return popoverStyleNode; + } + if (!documentRef?.createElement) { + return null; + } + const styleNode = documentRef.createElement("style"); + styleNode.id = styleId; + documentRef.head?.appendChild(styleNode); + popoverStyleNode = styleNode; + return popoverStyleNode; + } + + function syncPopoverStyles() { + if (!panel || !panel.id) { + return; + } + const styleNode = ensurePopoverStyleNode(panel.id); + if (!styleNode) { + return; + } + const panelRule = `#${panel.id}{` + + `--popover-left:${Math.round(panelStyleState.left)}px;` + + `--popover-top:${Math.round(panelStyleState.top)}px;` + + `--popover-strategy:${panelStyleState.strategy || strategy};` + + `--popover-max-width:${panelStyleState.maxWidth || "none"};` + + `--popover-max-height:${panelStyleState.maxHeight || "none"};` + + `}`; + const arrowRule = arrowId + ? `#${arrowId}{` + + `--popover-arrow-left:${arrowStyleState.left};` + + `--popover-arrow-right:${arrowStyleState.right};` + + `--popover-arrow-top:${arrowStyleState.top};` + + `--popover-arrow-bottom:${arrowStyleState.bottom};` + + `}` + : ""; + styleNode.textContent = [panelRule, arrowRule].filter(Boolean).join("\n"); + } function getView() { return documentRef?.defaultView || globalThis; @@ -610,13 +690,21 @@ export function createPopover(trigger, render, options = {}) { if (maxWidthToken) { const widthValue = resolveTokenValue(maxWidthToken, documentRef); - panel.style.maxWidth = widthValue || ""; + panelStyleState = { + ...panelStyleState, + maxWidth: widthValue || "none", + }; } if (maxHeightToken) { const heightValue = resolveTokenValue(maxHeightToken, documentRef); - panel.style.maxHeight = heightValue || ""; + panelStyleState = { + ...panelStyleState, + maxHeight: heightValue || "none", + }; } + + syncPopoverStyles(); } function ensurePanel() { @@ -672,6 +760,12 @@ export function createPopover(trigger, render, options = {}) { applyPanelTokens(); + if (arrowElement) { + arrowElement.dataset.popoverArrow = "true"; + arrowId = ensureArrowId(arrowElement); + syncPopoverStyles(); + } + return panel; } @@ -720,18 +814,39 @@ export function createPopover(trigger, render, options = {}) { ? resolvedPlacement : placement; - panel.style.position = resolvedStrategy || strategy; - panel.style.left = `${Math.round(nextX)}px`; - panel.style.top = `${Math.round(nextY)}px`; + panelStyleState = { + ...panelStyleState, + left: nextX, + top: nextY, + strategy: resolvedStrategy || strategy, + }; panel.dataset.popoverPlacement = finalPlacement; if (arrowElement) { - applyArrowStyles({ + const arrowState = buildArrowStyleState({ arrowElement, placement: finalPlacement, middlewareData, }); + arrowStyleState = { ...arrowState.values }; + if (arrowState.basePlacement) { + arrowElement.dataset.popoverArrowSide = arrowState.basePlacement; + } else { + delete arrowElement.dataset.popoverArrowSide; + } + arrowElement.dataset.popoverArrow = "true"; + arrowId = ensureArrowId(arrowElement); + } else { + arrowStyleState = { + left: "auto", + right: "auto", + top: "auto", + bottom: "auto", + }; + arrowId = ""; } + + syncPopoverStyles(); } catch (error) { logger.user.error("[popover] positioning failed", error); } @@ -908,8 +1023,14 @@ export function createPopover(trigger, render, options = {}) { portal.parentNode.removeChild(portal); } + if (popoverStyleNode?.parentNode) { + popoverStyleNode.parentNode.removeChild(popoverStyleNode); + } + panel = null; arrowElement = null; + popoverStyleNode = null; + arrowId = ""; if (anchor) { anchor.removeAttribute("aria-controls"); diff --git a/js/ui/profileModalController.js b/js/ui/profileModalController.js index aa6e962e..cfa9f271 100644 --- a/js/ui/profileModalController.js +++ b/js/ui/profileModalController.js @@ -1135,8 +1135,7 @@ export class ProfileModalController { this.focusTrapSuspended = false; this.focusTrapSuspendCount = 0; this.focusTrapAriaHiddenBeforeSuspend = null; - this.focusTrapPointerEventsBeforeSuspend = null; - this.focusTrapVisibilityBeforeSuspend = null; + this.focusTrapNestedModalActiveBeforeSuspend = null; this.profileSwitcherSelectionPubkey = null; this.previouslyFocusedElement = null; this.largeLayoutQuery = null; @@ -8332,25 +8331,16 @@ export class ProfileModalController { this.focusTrapAriaHiddenBeforeSuspend = modalRoot.getAttribute( "aria-hidden", ); - this.focusTrapPointerEventsBeforeSuspend = - typeof modalRoot.style.pointerEvents === "string" && - modalRoot.style.pointerEvents - ? modalRoot.style.pointerEvents - : null; - this.focusTrapVisibilityBeforeSuspend = - typeof modalRoot.style.visibility === "string" && - modalRoot.style.visibility - ? modalRoot.style.visibility + this.focusTrapNestedModalActiveBeforeSuspend = + typeof modalRoot.dataset.nestedModalActive === "string" + ? modalRoot.dataset.nestedModalActive : null; modalRoot.dataset.nestedModalActive = "true"; modalRoot.setAttribute("aria-hidden", "true"); - modalRoot.style.setProperty("pointer-events", "none"); - modalRoot.style.setProperty("visibility", "hidden"); } else { this.focusTrapAriaHiddenBeforeSuspend = null; - this.focusTrapPointerEventsBeforeSuspend = null; - this.focusTrapVisibilityBeforeSuspend = null; + this.focusTrapNestedModalActiveBeforeSuspend = null; } const panel = this.getModalPanelElement(); @@ -8391,30 +8381,11 @@ export class ProfileModalController { } } - if ( - this.focusTrapPointerEventsBeforeSuspend === null || - this.focusTrapPointerEventsBeforeSuspend === undefined || - this.focusTrapPointerEventsBeforeSuspend === "" - ) { - modalRoot.style.removeProperty("pointer-events"); + if (this.focusTrapNestedModalActiveBeforeSuspend === null) { + delete modalRoot.dataset.nestedModalActive; } else { - modalRoot.style.setProperty( - "pointer-events", - this.focusTrapPointerEventsBeforeSuspend, - ); - } - - if ( - this.focusTrapVisibilityBeforeSuspend === null || - this.focusTrapVisibilityBeforeSuspend === undefined || - this.focusTrapVisibilityBeforeSuspend === "" - ) { - modalRoot.style.removeProperty("visibility"); - } else { - modalRoot.style.setProperty( - "visibility", - this.focusTrapVisibilityBeforeSuspend, - ); + modalRoot.dataset.nestedModalActive = + this.focusTrapNestedModalActiveBeforeSuspend; } } @@ -8424,8 +8395,7 @@ export class ProfileModalController { } this.focusTrapAriaHiddenBeforeSuspend = null; - this.focusTrapPointerEventsBeforeSuspend = null; - this.focusTrapVisibilityBeforeSuspend = null; + this.focusTrapNestedModalActiveBeforeSuspend = null; this.updateFocusTrap(); @@ -8503,21 +8473,7 @@ export class ProfileModalController { : null; if (modalRoot) { - modalRoot.style.setProperty("z-index", "var(--z-modal-top-root)"); - } - - if (this.profileModalBackdrop instanceof HTMLElement) { - this.profileModalBackdrop.style.setProperty( - "z-index", - "var(--z-modal-top-overlay)", - ); - } - - if (this.profileModalPanel instanceof HTMLElement) { - this.profileModalPanel.style.setProperty( - "z-index", - "var(--z-modal-top-content)", - ); + modalRoot.dataset.modalStack = "top"; } } @@ -8685,8 +8641,7 @@ export class ProfileModalController { modalElement.classList.add("hidden"); modalElement.setAttribute("aria-hidden", "true"); delete modalElement.dataset.nestedModalActive; - modalElement.style.removeProperty("pointer-events"); - modalElement.style.removeProperty("visibility"); + delete modalElement.dataset.modalStack; this.setGlobalModalState("profile", false); this.setMobileView("menu", { skipFocusTrap: true }); @@ -8712,8 +8667,7 @@ export class ProfileModalController { this.focusTrapSuspendCount = 0; this.focusTrapSuspended = false; this.focusTrapAriaHiddenBeforeSuspend = null; - this.focusTrapPointerEventsBeforeSuspend = null; - this.focusTrapVisibilityBeforeSuspend = null; + this.focusTrapNestedModalActiveBeforeSuspend = null; if ( this.boundProfileHistoryVisibility && diff --git a/js/utils/qrcode.js b/js/utils/qrcode.js index 001aa787..20bb2932 100644 --- a/js/utils/qrcode.js +++ b/js/utils/qrcode.js @@ -235,6 +235,88 @@ if (hasDocument) { })(); var useSVG = document.documentElement.tagName.toLowerCase() === "svg"; + var qrCodeStyleRegistry = new WeakMap(); + var qrCodeStyleCounter = 0; + + function ensureQrCodeStyleState(doc) { + if (!doc || typeof doc.createElement !== "function") { + return null; + } + var existing = qrCodeStyleRegistry.get(doc); + if (existing) { + return existing; + } + var styleNode = doc.getElementById("qrCodeStyles"); + if (!(styleNode instanceof HTMLStyleElement)) { + styleNode = doc.createElement("style"); + styleNode.id = "qrCodeStyles"; + doc.head && doc.head.appendChild(styleNode); + } + var state = { + styleNode: styleNode, + values: new Map() + }; + qrCodeStyleRegistry.set(doc, state); + return state; + } + + function buildQrCodeRule(selector, values) { + var declarations = []; + if (values.colorDark) { + declarations.push("--qr-code-color-dark: " + values.colorDark + ";"); + } + if (values.colorLight) { + declarations.push("--qr-code-color-light: " + values.colorLight + ";"); + } + if (values.cellWidth) { + declarations.push("--qr-code-cell-width: " + values.cellWidth + ";"); + } + if (values.cellHeight) { + declarations.push("--qr-code-cell-height: " + values.cellHeight + ";"); + } + if (values.tableMargin) { + declarations.push("--qr-code-table-margin: " + values.tableMargin + ";"); + } + return selector + " { " + declarations.join(" ") + " }"; + } + + function updateQrCodeContainerStyles(el, values) { + if (!el) { + return; + } + var doc = el.ownerDocument || document; + var state = ensureQrCodeStyleState(doc); + if (!state) { + return; + } + if (!el.dataset.qrCodeStyleId) { + qrCodeStyleCounter += 1; + el.dataset.qrCodeStyleId = "qr-code-" + qrCodeStyleCounter; + } + var selector = "[data-qr-code-style-id=\"" + el.dataset.qrCodeStyleId + "\"]"; + var currentValues = state.values.get(selector) || {}; + var nextValues = Object.assign({}, currentValues, values || {}); + state.values.set(selector, nextValues); + var rules = []; + state.values.forEach(function (entryValues, entrySelector) { + rules.push(buildQrCodeRule(entrySelector, entryValues)); + }); + state.styleNode.textContent = rules.join("\n"); + } + + function setQrCodeMode(el, mode) { + if (!el) { + return; + } + if (el.classList) { + el.classList.add("qr-code__container"); + } + if (mode) { + el.dataset.qrCodeMode = mode; + } else { + delete el.dataset.qrCodeMode; + } + } // Drawing in DOM by using Table tag var Drawing = useSVG ? svgDrawer : !_isSupportCanvas() ? (function () { @@ -256,10 +338,13 @@ if (hasDocument) { var nHeight = Math.floor(_htOption.height / nCount); var aHTML = ['