mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 16:02:26 +00:00
Merge pull request #690 from PR0M3TH3AN/codex/restructure-profile-modal-for-mobile-views
Improve profile modal mobile navigation
This commit is contained in:
6 files changed
+826
-105
No files matched your search
+119
-79
@@ -7,13 +7,43 @@
|
||||
aria-labelledby="profileModalTitle"
|
||||
tabindex="-1"
|
||||
data-ds="new"
|
||||
data-mobile-view="menu"
|
||||
>
|
||||
<div class="bv-modal-backdrop"></div>
|
||||
|
||||
<div class="bv-modal__panel modal-sheet profile-modal" tabindex="-1">
|
||||
<div
|
||||
class="bv-modal__panel modal-sheet profile-modal"
|
||||
tabindex="-1"
|
||||
data-mobile-view="menu"
|
||||
>
|
||||
<div class="profile-modal__content">
|
||||
<!-- Modal Header -->
|
||||
<div class="modal-header">
|
||||
<h2 id="profileModalTitle" class="text-lg font-bold text-text">Profile</h2>
|
||||
<div class="modal-header profile-modal__header">
|
||||
<div class="profile-modal__header-title">
|
||||
<button
|
||||
id="profileModalBack"
|
||||
type="button"
|
||||
class="modal-back btn-ghost focus-ring hidden"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
class="h-4 w-4"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke="currentColor"
|
||||
>
|
||||
<path
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
d="M15 19l-7-7 7-7"
|
||||
/>
|
||||
</svg>
|
||||
<span class="text-xs font-medium">Menu</span>
|
||||
</button>
|
||||
<h2 id="profileModalTitle" class="text-lg font-bold text-text">Profile</h2>
|
||||
</div>
|
||||
<button id="closeProfileModal" class="modal-close">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
@@ -34,68 +64,79 @@
|
||||
|
||||
<!-- Modal Body -->
|
||||
<div class="profile-modal__body">
|
||||
<div class="profile-modal__layout">
|
||||
<div
|
||||
class="profile-modal__layout"
|
||||
data-profile-layout
|
||||
data-mobile-view="menu"
|
||||
>
|
||||
<!-- Left navigation column -->
|
||||
<nav class="profile-modal__nav" aria-label="Profile sections">
|
||||
<button
|
||||
id="profileNavAccount"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneAccount"
|
||||
aria-selected="true"
|
||||
data-state="active"
|
||||
>
|
||||
Account
|
||||
</button>
|
||||
<button
|
||||
id="profileNavRelays"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneRelays"
|
||||
aria-selected="false"
|
||||
>
|
||||
Relays
|
||||
</button>
|
||||
<button
|
||||
id="profileNavWallet"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneWallet"
|
||||
aria-selected="false"
|
||||
>
|
||||
Wallet
|
||||
</button>
|
||||
<button
|
||||
id="profileNavBlocked"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneBlocked"
|
||||
aria-selected="false"
|
||||
>
|
||||
Blocked
|
||||
</button>
|
||||
<button
|
||||
id="profileNavHistory"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneHistory"
|
||||
aria-selected="false"
|
||||
>
|
||||
Watch history
|
||||
</button>
|
||||
<button
|
||||
id="profileNavAdmin"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring hidden w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneAdmin"
|
||||
aria-selected="false"
|
||||
>
|
||||
Admin (Moderation)
|
||||
</button>
|
||||
</nav>
|
||||
<div class="profile-modal__menu" data-profile-mobile-menu aria-hidden="false">
|
||||
<nav class="profile-modal__nav" aria-label="Profile sections">
|
||||
<button
|
||||
id="profileNavAccount"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneAccount"
|
||||
aria-selected="true"
|
||||
data-state="active"
|
||||
>
|
||||
Account
|
||||
</button>
|
||||
<button
|
||||
id="profileNavRelays"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneRelays"
|
||||
aria-selected="false"
|
||||
>
|
||||
Relays
|
||||
</button>
|
||||
<button
|
||||
id="profileNavWallet"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneWallet"
|
||||
aria-selected="false"
|
||||
>
|
||||
Wallet
|
||||
</button>
|
||||
<button
|
||||
id="profileNavBlocked"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneBlocked"
|
||||
aria-selected="false"
|
||||
>
|
||||
Blocked
|
||||
</button>
|
||||
<button
|
||||
id="profileNavHistory"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneHistory"
|
||||
aria-selected="false"
|
||||
>
|
||||
Watch history
|
||||
</button>
|
||||
<button
|
||||
id="profileNavAdmin"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring hidden w-full justify-start gap-2 px-3 py-2 text-xs font-medium"
|
||||
aria-controls="profilePaneAdmin"
|
||||
aria-selected="false"
|
||||
>
|
||||
Admin (Moderation)
|
||||
</button>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
<!-- Right content panes -->
|
||||
<div class="profile-modal__panes">
|
||||
<div
|
||||
class="profile-modal__pane-wrapper"
|
||||
data-profile-mobile-pane
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="profile-modal__panes">
|
||||
<section
|
||||
id="profilePaneAccount"
|
||||
class="profile-pane space-y-6"
|
||||
@@ -108,18 +149,17 @@
|
||||
classes that may be purged or overridden by other builds.
|
||||
-->
|
||||
<div id="profileSwitcherTray" class="card space-y-5 p-5">
|
||||
<div class="flex flex-wrap items-start justify-between gap-4">
|
||||
<div class="flex flex-wrap items-start gap-4">
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-base font-semibold text-text">Switch profiles</h3>
|
||||
<p class="text-sm text-muted">
|
||||
Save multiple Nostr profiles so you can swap identities without leaving bitvid.
|
||||
</p>
|
||||
</div>
|
||||
<span class="badge" data-variant="info">Preview</span>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="card focus-ring flex items-center gap-4 p-4"
|
||||
class="card profile-modal__active-profile"
|
||||
aria-label="Current Nostr profile"
|
||||
aria-pressed="true"
|
||||
role="button"
|
||||
@@ -136,22 +176,22 @@
|
||||
class="h-full w-full object-cover"
|
||||
/>
|
||||
</span>
|
||||
<div class="flex min-w-0 flex-1 flex-col gap-2">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<div class="profile-modal__active-details">
|
||||
<div class="profile-modal__active-meta">
|
||||
<span class="text-3xs font-semibold uppercase tracking-extra-wide text-info">
|
||||
Active profile
|
||||
</span>
|
||||
<a
|
||||
id="profileChannelLink"
|
||||
href="#"
|
||||
class="btn-ghost focus-ring hidden text-xs"
|
||||
>
|
||||
View channel
|
||||
</a>
|
||||
</div>
|
||||
<span id="profileModalName" class="truncate text-sm font-semibold text-text">Loading...</span>
|
||||
<span id="profileModalNpub" class="break-all font-mono text-xs text-muted">npub1...</span>
|
||||
</div>
|
||||
<a
|
||||
id="profileChannelLink"
|
||||
href="#"
|
||||
class="btn-ghost focus-ring profile-modal__channel-link hidden text-xs"
|
||||
>
|
||||
View channel
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div
|
||||
@@ -166,13 +206,10 @@
|
||||
<button
|
||||
type="button"
|
||||
id="profileAddAccountBtn"
|
||||
class="btn focus-ring w-full justify-start gap-4 p-4"
|
||||
class="profile-modal__add-profile"
|
||||
aria-label="Add a new Nostr profile"
|
||||
>
|
||||
<span
|
||||
class="flex h-10 w-10 items-center justify-center rounded-full bg-info/20 text-info"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span class="profile-modal__add-profile-icon" aria-hidden="true">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
@@ -369,6 +406,7 @@
|
||||
id="profileHistoryClear"
|
||||
type="button"
|
||||
class="btn-ghost focus-ring inline-flex items-center gap-2"
|
||||
data-size="sm"
|
||||
>
|
||||
Clear local cache
|
||||
</button>
|
||||
@@ -376,6 +414,7 @@
|
||||
id="profileHistoryRepublish"
|
||||
type="button"
|
||||
class="btn focus-ring inline-flex items-center gap-2"
|
||||
data-size="sm"
|
||||
>
|
||||
Republish now
|
||||
</button>
|
||||
@@ -606,4 +645,5 @@
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -5,4 +5,4 @@
|
||||
--player-modal-inline-padding,clamp(var(--space-md-plus),4vw,var(--space-2xl))
|
||||
)}#playerModal .video-modal__primary>.watch-container:first-child{padding-block-start:var(--player-modal-vertical-padding)}#playerModal .video-modal__secondary>.watch-container:last-child{padding-block-end:var(--player-modal-vertical-padding)}#playerModal .modal-nav .watch-container{max-width:none;margin-inline:0}#playerModal .modal-info-panel{display:flex;flex-direction:column;gap:var(--space-lg)}#playerModal h2#videoTitle{margin:0;font-size:clamp(var(--font-size-xl),calc(var(--font-size-modal-title-base) + .5vw),var(--font-size-xl-plus));font-weight:600;line-height:1.3;color:var(--color-text)}#playerModal .modal-meta{font-size:var(--font-size-sm);line-height:1.4;color:var(--color-text-muted)}#playerModal .modal-view-count{font-weight:600;color:var(--color-text)}#playerModal .channel-card{display:flex;align-items:center;gap:var(--space-md);padding:var(--space-md-plus) var(--space-lg);border-radius:var(--radius-xl);border:var(--border-width-hairline) solid var(--color-border-translucent);background-color:var(--color-surface);box-shadow:var(--shadow-sm)}@media (max-width:30rem){#playerModal .modal-nav .icon-button{width:var(--space-xl);height:var(--space-xl);min-width:var(--space-xl);min-height:var(--space-xl)}#playerModal .watch-container{padding-inline:var(--space-sm)}#playerModal .channel-card{gap:var(--space-sm);padding:var(--space-sm) var(--space-md)}}#playerModal .channel-card__avatar{flex-shrink:0;width:var(--avatar-size-lg);height:var(--avatar-size-lg);border-radius:var(--radius-full);overflow:hidden;background-color:var(--color-border-subtle)}#playerModal .channel-card__avatar img{display:block;width:100%;height:100%;-o-object-fit:cover;object-fit:cover}#playerModal .channel-card__name{margin:0;font-size:var(--font-size-lg);font-weight:600;color:var(--color-text)}#playerModal .channel-card__npub{margin:0;font-size:var(--font-size-sm);color:var(--color-text-muted)}#playerModal .description-panel{padding:var(--space-md-plus) var(--space-lg);border-radius:var(--radius-xl);border:var(--border-width-hairline) solid var(--color-border-translucent);background-color:var(--color-surface);box-shadow:var(--shadow-sm)}#playerModal .description-panel p{margin:0;font-size:var(--font-size-sm);line-height:1.6;color:var(--color-text)}.player-modal__content{position:relative;z-index:var(--z-modal-content);display:flex;flex:1 1 auto;width:100%;min-height:100%;flex-direction:column;overflow:visible;padding:0;margin:0;border:none;border-radius:0;background-color:var(--color-surface);box-shadow:none}@media (prefers-reduced-motion:reduce){.player-modal__content{transition-property:none}}#playerModal .modal-nav{position:sticky;top:0;z-index:var(--z-overlay-nav);--tw-translate-y:0px;transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;background-color:var(--color-overlay-darker);background-image:linear-gradient(180deg,var(--color-overlay-panel-gradient-start) 0,var(--color-overlay-panel-gradient-end) 100%);border-bottom:var(--border-width-hairline) solid var(--color-border-translucent);backdrop-filter:blur(var(--modal-nav-backdrop-blur));-webkit-backdrop-filter:blur(var(--modal-nav-backdrop-blur))}#playerModal#playerModal .modal-nav__bar{display:block}#playerModal .modal-nav__bar{display:flex;width:100%;align-items:center;gap:.75rem;padding:.5rem}@media (min-width:640px){#playerModal .modal-nav__bar{padding-left:1rem;padding-right:1rem}}#playerModal .modal-nav__bar{padding-inline:var(
|
||||
--player-modal-inline-padding,clamp(var(--space-md-plus),4vw,var(--space-2xl))
|
||||
)}#playerModal#playerModal .modal-nav .modal-actions{display:block}#playerModal .modal-nav .modal-actions{display:flex;align-items:center;gap:.5rem;margin-left:auto}#playerModal .modal-nav .icon-button{display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-full);color:var(--text-muted);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-timing-function:cubic-bezier(0,0,.2,1);width:var(--icon-size-lg);height:var(--icon-size-lg);background:var(--color-surface-tint-subtle);border:var(--border-width-hairline) solid var(--color-border-translucent-medium);color:var(--color-text-muted)}#playerModal .modal-nav .icon-button:focus-visible,#playerModal .modal-nav .icon-button:hover{background:var(--color-border-translucent-bright);border-color:var(--color-border-translucent-strong);color:var(--color-text)}.modal-nav--hidden{--tw-translate-y:-100%}.modal-nav--hidden,.modal-nav--visible{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal-nav--visible{--tw-translate-y:0px}.video-modal__layout{display:grid;gap:1.5rem}@media (min-width:1024px){.video-modal__layout{gap:2rem;grid-template-columns:minmax(0,var(--video-modal-primary-fr)) minmax(var(--video-modal-secondary-min),var(--video-modal-secondary-fr,1fr))}}.revert-modal__layout{display:grid;gap:1.5rem}@media (min-width:1024px){.revert-modal__layout{grid-template-columns:minmax(var(--revert-modal-sidebar-min),var(--revert-modal-sidebar-width)) minmax(0,1fr)}}#playerModal.video-modal__primary{display:block}.video-modal__primary{display:flex;flex-direction:column;gap:1.5rem}#playerModal.video-modal__secondary{display:block}.video-modal__secondary{display:flex;flex-direction:column;gap:1rem}.video-modal__video{position:relative;background-color:var(--color-black);padding-block:clamp(var(--space-xs),2.5vw,var(--space-md-plus))}@media (min-width:1024px){.video-modal__video{position:sticky;top:calc(var(--space-2xl) + var(--space-xl))}}#playerModal .video-modal__video video{border-radius:var(--radius-lg);box-shadow:var(--shadow-video-modal);max-height:min(80vh,56.25vw);-o-object-fit:contain;object-fit:contain}.video-modal__media{display:block;aspect-ratio:16/9;width:100%;-o-object-fit:contain;object-fit:contain}#playerModal.video-modal__details{display:block}.video-modal__details{display:flex;flex-direction:column;gap:1.5rem}.video-modal__meta{border-bottom-width:1px;border-color:var(--border-default);--tw-border-opacity:0.2;padding-bottom:1rem}#playerModal.video-modal__actions-row{display:block}.video-modal__actions-row{display:flex;flex-wrap:wrap;gap:.75rem}.video-modal__action-button,.video-modal__actions-row{align-items:center}.video-modal__action-button{display:inline-flex;height:2.75rem;justify-content:center;gap:.5rem;border-radius:var(--radius-full);font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:600;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);min-width:var(--video-modal-action-min-width);padding-inline:var(--space-md);border-width:1px;border-style:solid}.bv-modal.video-modal__action-label,.profile-pane.video-modal__action-label,.video-modal__action-label{display:none}@media (min-width:640px){.video-modal__action-label{display:inline}}.video-modal__action-icon{height:1.25rem;width:1.25rem}.video-modal__channel,.video-modal__description,.video-modal__stats{border-radius:1rem;border-width:1px;border-color:var(--border-default);--tw-border-opacity:0.2;background-color:var(--surface-base);padding:1.25rem 1.5rem;--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.video-modal__description-body{overflow-wrap:anywhere;word-break:break-word}.video-modal__description-body a{color:var(--color-accent-strong);text-decoration:underline;text-decoration-color:currentColor;word-break:break-word}.video-modal__description-body a:focus-visible,.video-modal__description-body a:hover{color:var(--color-accent-pressed)}.video-modal__progress{height:.5rem;width:100%;overflow:hidden;border-radius:var(--radius-full);background-color:var(--border-subtle)}#playerModal.video-modal__stat-metrics{display:block}.video-modal__stat-metrics{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-muted)}.profile-modal{display:flexLine truncated
|
||||
)}#playerModal#playerModal .modal-nav .modal-actions{display:block}#playerModal .modal-nav .modal-actions{display:flex;align-items:center;gap:.5rem;margin-left:auto}#playerModal .modal-nav .icon-button{display:inline-flex;align-items:center;justify-content:center;border-radius:var(--radius-full);color:var(--text-muted);transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-timing-function:cubic-bezier(0,0,.2,1);width:var(--icon-size-lg);height:var(--icon-size-lg);background:var(--color-surface-tint-subtle);border:var(--border-width-hairline) solid var(--color-border-translucent-medium);color:var(--color-text-muted)}#playerModal .modal-nav .icon-button:focus-visible,#playerModal .modal-nav .icon-button:hover{background:var(--color-border-translucent-bright);border-color:var(--color-border-translucent-strong);color:var(--color-text)}.modal-nav--hidden{--tw-translate-y:-100%}.modal-nav--hidden,.modal-nav--visible{transform:translate(var(--tw-translate-x),var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y))}.modal-nav--visible{--tw-translate-y:0px}.video-modal__layout{display:grid;gap:1.5rem}@media (min-width:1024px){.video-modal__layout{gap:2rem;grid-template-columns:minmax(0,var(--video-modal-primary-fr)) minmax(var(--video-modal-secondary-min),var(--video-modal-secondary-fr,1fr))}}.revert-modal__layout{display:grid;gap:1.5rem}@media (min-width:1024px){.revert-modal__layout{grid-template-columns:minmax(var(--revert-modal-sidebar-min),var(--revert-modal-sidebar-width)) minmax(0,1fr)}}#playerModal.video-modal__primary{display:block}.video-modal__primary{display:flex;flex-direction:column;gap:1.5rem}#playerModal.video-modal__secondary{display:block}.video-modal__secondary{display:flex;flex-direction:column;gap:1rem}.video-modal__video{position:relative;background-color:var(--color-black);padding-block:clamp(var(--space-xs),2.5vw,var(--space-md-plus))}@media (min-width:1024px){.video-modal__video{position:sticky;top:calc(var(--space-2xl) + var(--space-xl))}}#playerModal .video-modal__video video{border-radius:var(--radius-lg);box-shadow:var(--shadow-video-modal);max-height:min(80vh,56.25vw);-o-object-fit:contain;object-fit:contain}.video-modal__media{display:block;aspect-ratio:16/9;width:100%;-o-object-fit:contain;object-fit:contain}#playerModal.video-modal__details{display:block}.video-modal__details{display:flex;flex-direction:column;gap:1.5rem}.video-modal__meta{border-bottom-width:1px;border-color:var(--border-default);--tw-border-opacity:0.2;padding-bottom:1rem}#playerModal.video-modal__actions-row{display:block}.video-modal__actions-row{display:flex;flex-wrap:wrap;gap:.75rem}.video-modal__action-button,.video-modal__actions-row{align-items:center}.video-modal__action-button{display:inline-flex;height:2.75rem;justify-content:center;gap:.5rem;border-radius:var(--radius-full);font-size:var(--font-size-sm);line-height:var(--line-height-snug);font-weight:600;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s;transition-duration:var(--motion-duration-base);transition-timing-function:cubic-bezier(0,0,.2,1);min-width:var(--video-modal-action-min-width);padding-inline:var(--space-md);border-width:1px;border-style:solid}.bv-modal.video-modal__action-label,.profile-pane.video-modal__action-label,.video-modal__action-label{display:none}@media (min-width:640px){.video-modal__action-label{display:inline}}.video-modal__action-icon{height:1.25rem;width:1.25rem}.video-modal__channel,.video-modal__description,.video-modal__stats{border-radius:1rem;border-width:1px;border-color:var(--border-default);--tw-border-opacity:0.2;background-color:var(--surface-base);padding:1.25rem 1.5rem;--tw-shadow:var(--shadow-sm);--tw-shadow-colored:var(--shadow-sm);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.video-modal__description-body{overflow-wrap:anywhere;word-break:break-word}.video-modal__description-body a{color:var(--color-accent-strong);text-decoration:underline;text-decoration-color:currentColor;word-break:break-word}.video-modal__description-body a:focus-visible,.video-modal__description-body a:hover{color:var(--color-accent-pressed)}.video-modal__progress{height:.5rem;width:100%;overflow:hidden;border-radius:var(--radius-full);background-color:var(--border-subtle)}#playerModal.video-modal__stat-metrics{display:block}.video-modal__stat-metrics{display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:.75rem;font-size:var(--font-size-sm);line-height:var(--line-height-snug);color:var(--text-muted)}.profile-modal{display:flexLine truncated
|
||||
+146
-7
@@ -479,6 +479,16 @@
|
||||
@apply focus-ring inline-flex items-center justify-center gap-2 rounded-full border border-transparent bg-accent-strong px-5 py-2.5 text-base font-semibold text-white shadow-lg transition duration-base ease-in-out;
|
||||
}
|
||||
|
||||
.btn[data-size="sm"],
|
||||
.btn-ghost[data-size="sm"] {
|
||||
@apply px-3 py-1.5 text-sm;
|
||||
}
|
||||
|
||||
.btn[data-size="sm"],
|
||||
.btn-ghost[data-size="sm"] {
|
||||
@apply gap-1.5;
|
||||
}
|
||||
|
||||
.btn:hover {
|
||||
@apply bg-accent;
|
||||
}
|
||||
@@ -1071,6 +1081,16 @@
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.modal-back {
|
||||
@apply inline-flex items-center gap-2;
|
||||
}
|
||||
|
||||
@media (min-width: theme("screens.lg")) {
|
||||
.modal-back {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.modal-body {
|
||||
@apply space-y-5 px-8 pb-8 pt-2;
|
||||
overscroll-behavior: contain;
|
||||
@@ -1538,13 +1558,88 @@
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
inline-size: 100%;
|
||||
max-block-size: var(--modal-shell-max-height);
|
||||
block-size: 100%;
|
||||
min-block-size: 100%;
|
||||
max-block-size: none;
|
||||
overflow: hidden;
|
||||
--profile-modal-gap: calc(var(--space-lg) - var(--space-xs));
|
||||
--profile-modal-padding-inline: calc(var(--space-lg) - var(--space-xs));
|
||||
--profile-modal-padding-block-start: var(--space-md);
|
||||
--profile-modal-padding-block-end: calc(var(--space-lg) - var(--space-xs));
|
||||
--profile-modal-nav-gap: var(--space-xs);
|
||||
--profile-modal-scale: 0.9;
|
||||
--profile-modal-gap:
|
||||
calc((var(--space-md) - var(--space-3xs)) * var(--profile-modal-scale));
|
||||
--profile-modal-padding-inline:
|
||||
calc((var(--space-lg) - var(--space-md)) * var(--profile-modal-scale));
|
||||
--profile-modal-padding-block-start:
|
||||
calc((var(--space-md) - var(--space-2xs)) * var(--profile-modal-scale));
|
||||
--profile-modal-padding-block-end:
|
||||
calc((var(--space-lg) - var(--space-sm)) * var(--profile-modal-scale));
|
||||
--profile-modal-nav-gap: calc(var(--space-xs) * var(--profile-modal-scale));
|
||||
}
|
||||
|
||||
.profile-modal__content {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
block-size: 100%;
|
||||
min-block-size: 0;
|
||||
transform: scale(var(--profile-modal-scale));
|
||||
transform-origin: top center;
|
||||
}
|
||||
|
||||
.profile-modal__active-profile {
|
||||
@apply focus-ring flex flex-wrap items-center gap-4 p-4;
|
||||
}
|
||||
|
||||
.profile-modal__active-details {
|
||||
display: flex;
|
||||
flex: 1 1 12rem;
|
||||
flex-direction: column;
|
||||
gap: var(--space-2xs);
|
||||
min-inline-size: 0;
|
||||
}
|
||||
|
||||
.profile-modal__active-meta {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--space-xs);
|
||||
}
|
||||
|
||||
.profile-modal__channel-link {
|
||||
margin-inline-start: auto;
|
||||
align-self: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-modal__add-profile {
|
||||
--tw-ring-color: var(--color-ring-alert);
|
||||
@apply focus-ring relative flex w-full items-center justify-start gap-4 rounded-xl px-5 py-4 text-left text-text transition duration-base ease-in-out;
|
||||
border: var(--outline-thick-width) solid var(--color-ring-alert);
|
||||
background-color: var(--surface-panel-hover);
|
||||
}
|
||||
|
||||
.profile-modal__add-profile:hover {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--surface-panel-hover) 88%,
|
||||
var(--color-ring-alert) 12%
|
||||
);
|
||||
}
|
||||
|
||||
.profile-modal__add-profile:focus-visible {
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--surface-panel-hover) 80%,
|
||||
var(--color-ring-alert) 20%
|
||||
);
|
||||
}
|
||||
|
||||
.profile-modal__add-profile-icon {
|
||||
@apply flex h-10 w-10 items-center justify-center rounded-full;
|
||||
background-color: color-mix(
|
||||
in srgb,
|
||||
var(--color-ring-alert) 18%,
|
||||
var(--surface-panel)
|
||||
);
|
||||
color: var(--color-ring-alert);
|
||||
}
|
||||
|
||||
.profile-modal__body {
|
||||
@@ -1570,6 +1665,29 @@
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.profile-modal__header {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.profile-modal__header-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.profile-modal__menu {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.profile-modal__pane-wrapper {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-block-size: 0;
|
||||
}
|
||||
|
||||
.profile-modal__nav {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1609,7 +1727,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: calc(theme("screens.lg") - 0.02px)) {
|
||||
.profile-modal__layout[data-mobile-view="menu"]
|
||||
.profile-modal__pane-wrapper {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.profile-modal__layout[data-mobile-view="pane"]
|
||||
.profile-modal__menu {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: theme("screens.lg")) {
|
||||
.profile-modal {
|
||||
max-block-size: var(--modal-shell-max-height);
|
||||
}
|
||||
|
||||
.profile-modal__body {
|
||||
gap: calc(var(--profile-modal-gap) + var(--space-2xs));
|
||||
padding-inline: calc(var(--profile-modal-padding-inline) + var(--space-xs));
|
||||
@@ -1617,7 +1751,7 @@
|
||||
|
||||
.profile-modal__layout {
|
||||
display: grid;
|
||||
align-items: flex-start;
|
||||
align-items: stretch;
|
||||
block-size: 100%;
|
||||
gap: calc(var(--profile-modal-gap) + var(--space-2xs));
|
||||
grid-template-columns:
|
||||
@@ -1626,6 +1760,7 @@
|
||||
var(--profile-modal-nav-max-width)
|
||||
)
|
||||
minmax(0, 1fr);
|
||||
grid-auto-rows: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.profile-modal__nav {
|
||||
@@ -1636,6 +1771,10 @@
|
||||
inline-size: var(--profile-modal-nav-max-width);
|
||||
min-inline-size: var(--profile-modal-nav-min-width);
|
||||
}
|
||||
|
||||
.profile-modal__pane-wrapper {
|
||||
block-size: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
/* Torrent helper classes */
|
||||
|
||||
@@ -843,6 +843,11 @@ export function createWatchHistoryRenderer(config = {}) {
|
||||
"aria-checked",
|
||||
enabled ? "true" : "false"
|
||||
);
|
||||
elements.metadataToggle.setAttribute(
|
||||
"data-enabled",
|
||||
enabled ? "true" : "false"
|
||||
);
|
||||
elements.metadataToggle.dataset.state = enabled ? "on" : "off";
|
||||
elements.metadataToggle.classList.toggle("bg-info", enabled);
|
||||
elements.metadataToggle.classList.toggle("border-info", enabled);
|
||||
elements.metadataToggle.classList.toggle("bg-panel", !enabled);
|
||||
|
||||
+304
-15
@@ -670,6 +670,10 @@ export class ProfileModalController {
|
||||
this.profileModalRoot = null;
|
||||
this.profileModalPanel = null;
|
||||
this.profileModalBackdrop = null;
|
||||
this.profileModalLayout = null;
|
||||
this.profileModalMenu = null;
|
||||
this.profileModalPaneWrapper = null;
|
||||
this.profileModalBackButton = null;
|
||||
this.profileAvatar = null;
|
||||
this.profileName = null;
|
||||
this.profileNpub = null;
|
||||
@@ -763,6 +767,11 @@ export class ProfileModalController {
|
||||
this.focusTrapContainer = null;
|
||||
this.profileSwitcherSelectionPubkey = null;
|
||||
this.previouslyFocusedElement = null;
|
||||
this.largeLayoutQuery = null;
|
||||
this.largeLayoutQueryListener = null;
|
||||
this.isLargeLayoutActiveFlag = false;
|
||||
this.mobileViewState = "menu";
|
||||
this.lastMobileViewState = "menu";
|
||||
this.setActivePane(this.getActivePane());
|
||||
this.setWalletPaneBusy(this.isWalletBusy());
|
||||
this.adminEmptyMessages = new Map();
|
||||
@@ -797,9 +806,11 @@ export class ProfileModalController {
|
||||
modalRoot.querySelector(".bv-modal-backdrop") || null;
|
||||
|
||||
this.cacheDomReferences();
|
||||
this.setupLayoutBreakpointObserver();
|
||||
this.applyModalStackingOverrides();
|
||||
this.registerEventListeners();
|
||||
this.selectPane(this.getActivePane());
|
||||
const preserveMenu = this.isMobileLayoutActive();
|
||||
this.selectPane(this.getActivePane(), { keepMenuView: preserveMenu });
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -810,8 +821,16 @@ export class ProfileModalController {
|
||||
this.profileModalRoot?.querySelector(".bv-modal__panel") || null;
|
||||
this.profileModalBackdrop =
|
||||
this.profileModalRoot?.querySelector(".bv-modal-backdrop") || null;
|
||||
this.profileModalLayout =
|
||||
this.profileModalRoot?.querySelector("[data-profile-layout]") || null;
|
||||
this.profileModalMenu =
|
||||
this.profileModalRoot?.querySelector("[data-profile-mobile-menu]") || null;
|
||||
this.profileModalPaneWrapper =
|
||||
this.profileModalRoot?.querySelector("[data-profile-mobile-pane]") || null;
|
||||
this.profileModal = this.profileModalRoot;
|
||||
this.closeButton = document.getElementById("closeProfileModal") || null;
|
||||
this.profileModalBackButton =
|
||||
document.getElementById("profileModalBack") || null;
|
||||
this.logoutButton = document.getElementById("profileLogoutBtn") || null;
|
||||
this.channelLink = document.getElementById("profileChannelLink") || null;
|
||||
this.addAccountButton =
|
||||
@@ -1006,6 +1025,12 @@ export class ProfileModalController {
|
||||
});
|
||||
}
|
||||
|
||||
if (this.profileModalBackButton instanceof HTMLElement) {
|
||||
this.profileModalBackButton.addEventListener("click", () => {
|
||||
this.setMobileView("menu", { focusMenu: true });
|
||||
});
|
||||
}
|
||||
|
||||
if (this.logoutButton instanceof HTMLElement) {
|
||||
this.logoutButton.addEventListener("click", async () => {
|
||||
try {
|
||||
@@ -1469,7 +1494,211 @@ export class ProfileModalController {
|
||||
return container;
|
||||
}
|
||||
|
||||
selectPane(name = "account") {
|
||||
setupLayoutBreakpointObserver() {
|
||||
if (
|
||||
typeof window === "undefined" ||
|
||||
typeof window.matchMedia !== "function"
|
||||
) {
|
||||
this.largeLayoutQuery = null;
|
||||
this.largeLayoutQueryListener = null;
|
||||
this.isLargeLayoutActiveFlag = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.largeLayoutQuery && this.largeLayoutQueryListener) {
|
||||
this.teardownLayoutBreakpointObserver();
|
||||
}
|
||||
|
||||
try {
|
||||
const query = window.matchMedia("(min-width: 1024px)");
|
||||
const handler = (event) => {
|
||||
const matches =
|
||||
typeof event?.matches === "boolean" ? event.matches : query.matches;
|
||||
this.handleLayoutBreakpointChange(matches);
|
||||
};
|
||||
this.largeLayoutQuery = query;
|
||||
this.largeLayoutQueryListener = handler;
|
||||
|
||||
if (typeof query.addEventListener === "function") {
|
||||
query.addEventListener("change", handler);
|
||||
} else if (typeof query.addListener === "function") {
|
||||
query.addListener(handler);
|
||||
}
|
||||
|
||||
this.handleLayoutBreakpointChange(query.matches);
|
||||
} catch (error) {
|
||||
this.largeLayoutQuery = null;
|
||||
this.largeLayoutQueryListener = null;
|
||||
devLogger.warn(
|
||||
"[profileModal] Failed to initialize responsive breakpoint observer:",
|
||||
error,
|
||||
);
|
||||
this.handleLayoutBreakpointChange(false);
|
||||
}
|
||||
}
|
||||
|
||||
teardownLayoutBreakpointObserver() {
|
||||
const query = this.largeLayoutQuery;
|
||||
const handler = this.largeLayoutQueryListener;
|
||||
if (query && handler) {
|
||||
if (typeof query.removeEventListener === "function") {
|
||||
query.removeEventListener("change", handler);
|
||||
} else if (typeof query.removeListener === "function") {
|
||||
query.removeListener(handler);
|
||||
}
|
||||
}
|
||||
this.largeLayoutQuery = null;
|
||||
this.largeLayoutQueryListener = null;
|
||||
}
|
||||
|
||||
handleLayoutBreakpointChange(matches) {
|
||||
const isLarge = Boolean(matches);
|
||||
this.isLargeLayoutActiveFlag = isLarge;
|
||||
if (isLarge) {
|
||||
this.setMobileView("pane", { skipFocusTrap: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const targetView = this.lastMobileViewState || this.mobileViewState || "menu";
|
||||
this.setMobileView(targetView, { skipFocusTrap: false });
|
||||
}
|
||||
|
||||
isLargeLayoutActive() {
|
||||
return Boolean(this.isLargeLayoutActiveFlag);
|
||||
}
|
||||
|
||||
isMobileLayoutActive() {
|
||||
return !this.isLargeLayoutActive();
|
||||
}
|
||||
|
||||
focusActiveNavButton() {
|
||||
const active = this.getActivePane();
|
||||
const candidates = [];
|
||||
if (active && this.navButtons[active] instanceof HTMLElement) {
|
||||
candidates.push(this.navButtons[active]);
|
||||
}
|
||||
Object.values(this.navButtons).forEach((button) => {
|
||||
if (
|
||||
button instanceof HTMLElement &&
|
||||
!candidates.includes(button) &&
|
||||
!button.classList.contains("hidden")
|
||||
) {
|
||||
candidates.push(button);
|
||||
}
|
||||
});
|
||||
|
||||
const target = candidates.find((button) => button instanceof HTMLElement);
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
window.requestAnimationFrame(() => {
|
||||
try {
|
||||
target.focus();
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[profileModal] Failed to focus active navigation button:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setMobileView(view = "menu", options = {}) {
|
||||
const normalizedView = view === "pane" ? "pane" : "menu";
|
||||
const settings =
|
||||
options && typeof options === "object" ? options : { skipFocusTrap: false };
|
||||
const skipFocusTrap = Boolean(settings.skipFocusTrap);
|
||||
const focusMenu = Boolean(settings.focusMenu);
|
||||
|
||||
const layoutElement =
|
||||
this.profileModalLayout instanceof HTMLElement
|
||||
? this.profileModalLayout
|
||||
: null;
|
||||
const paneWrapper =
|
||||
this.profileModalPaneWrapper instanceof HTMLElement
|
||||
? this.profileModalPaneWrapper
|
||||
: null;
|
||||
const menuWrapper =
|
||||
this.profileModalMenu instanceof HTMLElement
|
||||
? this.profileModalMenu
|
||||
: null;
|
||||
const panelElement =
|
||||
this.profileModalPanel instanceof HTMLElement
|
||||
? this.profileModalPanel
|
||||
: null;
|
||||
const rootElement =
|
||||
this.profileModalRoot instanceof HTMLElement ? this.profileModalRoot : null;
|
||||
const backButton =
|
||||
this.profileModalBackButton instanceof HTMLElement
|
||||
? this.profileModalBackButton
|
||||
: null;
|
||||
|
||||
const isLarge = this.isLargeLayoutActive();
|
||||
this.mobileViewState = normalizedView;
|
||||
if (!isLarge) {
|
||||
this.lastMobileViewState = normalizedView;
|
||||
}
|
||||
|
||||
if (layoutElement) {
|
||||
layoutElement.dataset.mobileView = normalizedView;
|
||||
}
|
||||
if (panelElement) {
|
||||
panelElement.dataset.mobileView = normalizedView;
|
||||
}
|
||||
if (rootElement) {
|
||||
rootElement.dataset.mobileView = normalizedView;
|
||||
}
|
||||
|
||||
const menuHidden = !isLarge && normalizedView === "pane";
|
||||
const paneHidden = !isLarge && normalizedView === "menu";
|
||||
|
||||
if (menuWrapper) {
|
||||
menuWrapper.setAttribute("aria-hidden", menuHidden ? "true" : "false");
|
||||
if (menuHidden) {
|
||||
menuWrapper.classList.add("hidden");
|
||||
menuWrapper.setAttribute("hidden", "");
|
||||
} else {
|
||||
menuWrapper.classList.remove("hidden");
|
||||
menuWrapper.removeAttribute("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
if (paneWrapper) {
|
||||
paneWrapper.setAttribute("aria-hidden", paneHidden ? "true" : "false");
|
||||
if (paneHidden) {
|
||||
paneWrapper.classList.add("hidden");
|
||||
paneWrapper.setAttribute("hidden", "");
|
||||
} else {
|
||||
paneWrapper.classList.remove("hidden");
|
||||
paneWrapper.removeAttribute("hidden");
|
||||
}
|
||||
}
|
||||
|
||||
if (backButton) {
|
||||
if (isLarge || normalizedView === "menu") {
|
||||
backButton.classList.add("hidden");
|
||||
backButton.setAttribute("aria-hidden", "true");
|
||||
} else {
|
||||
backButton.classList.remove("hidden");
|
||||
backButton.setAttribute("aria-hidden", "false");
|
||||
}
|
||||
}
|
||||
|
||||
if (!skipFocusTrap) {
|
||||
this.updateFocusTrap();
|
||||
}
|
||||
|
||||
if (focusMenu && normalizedView === "menu") {
|
||||
this.focusActiveNavButton();
|
||||
}
|
||||
|
||||
return normalizedView;
|
||||
}
|
||||
|
||||
selectPane(name = "account", options = {}) {
|
||||
const { keepMenuView = false } =
|
||||
options && typeof options === "object" ? options : {};
|
||||
const normalized = typeof name === "string" ? name.toLowerCase() : "account";
|
||||
const previous = this.getActivePane();
|
||||
const availableKeys = Object.keys(this.panes).filter((key) => {
|
||||
@@ -1522,7 +1751,9 @@ export class ProfileModalController {
|
||||
});
|
||||
|
||||
this.setActivePane(target);
|
||||
this.updateFocusTrap();
|
||||
const isMobile = this.isMobileLayoutActive();
|
||||
const shouldStayInMenu = keepMenuView && isMobile;
|
||||
this.setMobileView(shouldStayInMenu ? "menu" : "pane");
|
||||
|
||||
if (target === "history") {
|
||||
void this.populateProfileWatchHistory();
|
||||
@@ -2824,6 +3055,44 @@ export class ProfileModalController {
|
||||
}
|
||||
}
|
||||
|
||||
normalizeAdminListEntries(entries) {
|
||||
const collected = [];
|
||||
const seen = new Set();
|
||||
|
||||
const append = (value) => {
|
||||
if (typeof value !== "string") {
|
||||
return;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed || seen.has(trimmed)) {
|
||||
return;
|
||||
}
|
||||
seen.add(trimmed);
|
||||
collected.push(trimmed);
|
||||
};
|
||||
|
||||
if (Array.isArray(entries)) {
|
||||
entries.forEach(append);
|
||||
} else if (entries && typeof entries?.[Symbol.iterator] === "function") {
|
||||
for (const entry of entries) {
|
||||
append(entry);
|
||||
}
|
||||
} else if (entries && typeof entries === "object") {
|
||||
Object.values(entries).forEach(append);
|
||||
}
|
||||
|
||||
try {
|
||||
collected.sort((a, b) => a.localeCompare(b));
|
||||
} catch (error) {
|
||||
devLogger.warn(
|
||||
"[profileModal] Failed to sort admin list entries, using fallback order.",
|
||||
error,
|
||||
);
|
||||
}
|
||||
|
||||
return collected;
|
||||
}
|
||||
|
||||
renderAdminList(listEl, emptyEl, entries, options = {}) {
|
||||
if (!(listEl instanceof HTMLElement) || !(emptyEl instanceof HTMLElement)) {
|
||||
return;
|
||||
@@ -2840,17 +3109,31 @@ export class ProfileModalController {
|
||||
const entriesNeedingFetch = new Set();
|
||||
|
||||
listEl.innerHTML = "";
|
||||
const values = Array.isArray(entries) ? [...entries] : [];
|
||||
values.sort((a, b) => a.localeCompare(b));
|
||||
|
||||
const values = this.normalizeAdminListEntries(entries);
|
||||
|
||||
const toggleHiddenState = (element, shouldHide) => {
|
||||
if (!(element instanceof HTMLElement)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldHide) {
|
||||
element.classList.add("hidden");
|
||||
element.setAttribute("hidden", "");
|
||||
} else {
|
||||
element.classList.remove("hidden");
|
||||
element.removeAttribute("hidden");
|
||||
}
|
||||
};
|
||||
|
||||
if (!values.length) {
|
||||
emptyEl.classList.remove("hidden");
|
||||
listEl.classList.add("hidden");
|
||||
toggleHiddenState(emptyEl, false);
|
||||
toggleHiddenState(listEl, true);
|
||||
return;
|
||||
}
|
||||
|
||||
emptyEl.classList.add("hidden");
|
||||
listEl.classList.remove("hidden");
|
||||
toggleHiddenState(emptyEl, true);
|
||||
toggleHiddenState(listEl, false);
|
||||
|
||||
values.forEach((npub) => {
|
||||
const item = document.createElement("li");
|
||||
@@ -2931,11 +3214,15 @@ export class ProfileModalController {
|
||||
}
|
||||
|
||||
const isSuperAdmin = this.services.accessControl.isSuperAdmin(actorNpub);
|
||||
const editors = this.services.accessControl
|
||||
.getEditors()
|
||||
.filter((npub) => npub && npub !== this.adminSuperNpub);
|
||||
const whitelist = this.services.accessControl.getWhitelist();
|
||||
const blacklist = this.services.accessControl.getBlacklist();
|
||||
const editors = this.normalizeAdminListEntries(
|
||||
this.services.accessControl.getEditors(),
|
||||
).filter((npub) => npub && npub !== this.adminSuperNpub);
|
||||
const whitelist = this.normalizeAdminListEntries(
|
||||
this.services.accessControl.getWhitelist(),
|
||||
);
|
||||
const blacklist = this.normalizeAdminListEntries(
|
||||
this.services.accessControl.getBlacklist(),
|
||||
);
|
||||
|
||||
this.renderAdminList(
|
||||
this.adminModeratorList,
|
||||
@@ -4197,7 +4484,8 @@ export class ProfileModalController {
|
||||
modalRoot.classList.remove("hidden");
|
||||
modalRoot.setAttribute("aria-hidden", "false");
|
||||
this.setGlobalModalState("profile", true);
|
||||
this.selectPane(pane);
|
||||
const preserveMenu = this.isMobileLayoutActive();
|
||||
this.selectPane(pane, { keepMenuView: preserveMenu });
|
||||
|
||||
const focusTarget =
|
||||
this.focusableElements[0] ||
|
||||
@@ -4225,6 +4513,7 @@ export class ProfileModalController {
|
||||
modalElement.classList.add("hidden");
|
||||
modalElement.setAttribute("aria-hidden", "true");
|
||||
this.setGlobalModalState("profile", false);
|
||||
this.setMobileView("menu", { skipFocusTrap: true });
|
||||
|
||||
if (this.boundKeydown && this.focusTrapContainer) {
|
||||
this.focusTrapContainer.removeEventListener(
|
||||
|
||||
@@ -3,6 +3,8 @@ import { strict as assert } from 'node:assert';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import ProfileModalController from '../js/ui/profileModalController.js';
|
||||
import { createWatchHistoryRenderer } from '../js/historyView.js';
|
||||
import watchHistoryService from '../js/watchHistoryService.js';
|
||||
import { formatShortNpub } from '../js/utils/formatters.js';
|
||||
import { resetRuntimeFlags } from '../js/constants.js';
|
||||
import { applyDesignSystemAttributes } from '../js/designSystem.js';
|
||||
@@ -24,6 +26,65 @@ async function waitForAnimationFrame(window, cycles = 1) {
|
||||
}
|
||||
}
|
||||
|
||||
function createMatchMediaList(query, initialMatches = false) {
|
||||
let matches = Boolean(initialMatches);
|
||||
const listeners = new Set();
|
||||
|
||||
const notify = () => {
|
||||
const event = { matches, media: query, type: 'change' };
|
||||
listeners.forEach((listener) => {
|
||||
try {
|
||||
listener(event);
|
||||
} catch {}
|
||||
});
|
||||
};
|
||||
|
||||
return {
|
||||
get matches() {
|
||||
return matches;
|
||||
},
|
||||
set matches(value) {
|
||||
matches = Boolean(value);
|
||||
},
|
||||
media: query,
|
||||
addEventListener(type, listener) {
|
||||
if (type === 'change' && typeof listener === 'function') {
|
||||
listeners.add(listener);
|
||||
}
|
||||
},
|
||||
removeEventListener(type, listener) {
|
||||
if (type === 'change' && typeof listener === 'function') {
|
||||
listeners.delete(listener);
|
||||
}
|
||||
},
|
||||
addListener(listener) {
|
||||
if (typeof listener === 'function') {
|
||||
listeners.add(listener);
|
||||
}
|
||||
},
|
||||
removeListener(listener) {
|
||||
if (typeof listener === 'function') {
|
||||
listeners.delete(listener);
|
||||
}
|
||||
},
|
||||
dispatchEvent(event) {
|
||||
if (!event || event.type !== 'change') {
|
||||
return true;
|
||||
}
|
||||
listeners.forEach((listener) => {
|
||||
try {
|
||||
listener(event);
|
||||
} catch {}
|
||||
});
|
||||
return true;
|
||||
},
|
||||
setMatches(value) {
|
||||
matches = Boolean(value);
|
||||
notify();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
dom = new JSDOM(
|
||||
'<!DOCTYPE html><html><body><div id="modalMount"></div></body></html>',
|
||||
@@ -53,6 +114,14 @@ beforeEach(() => {
|
||||
window.confirm = () => true;
|
||||
global.confirm = window.confirm;
|
||||
|
||||
window.__matchMediaMocks = [];
|
||||
window.matchMedia = (query) => {
|
||||
const mql = createMatchMediaList(query, false);
|
||||
window.__matchMediaMocks.push(mql);
|
||||
return mql;
|
||||
};
|
||||
global.matchMedia = window.matchMedia;
|
||||
|
||||
container = document.getElementById('modalMount');
|
||||
|
||||
global.fetch = async (resource) => {
|
||||
@@ -87,11 +156,16 @@ afterEach(() => {
|
||||
delete global.HTMLElement;
|
||||
delete global.document;
|
||||
delete global.window;
|
||||
delete global.matchMedia;
|
||||
|
||||
if (dom) {
|
||||
dom.window.close();
|
||||
dom = null;
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
delete window.matchMedia;
|
||||
delete window.__matchMediaMocks;
|
||||
}
|
||||
container = null;
|
||||
});
|
||||
|
||||
@@ -291,6 +365,74 @@ for (const _ of [0]) {
|
||||
},
|
||||
);
|
||||
|
||||
test('Profile modal toggles mobile menu and pane views', async (t) => {
|
||||
const controller = createController();
|
||||
await controller.load();
|
||||
applyDesignSystemAttributes(document);
|
||||
|
||||
let cleanupRan = false;
|
||||
const cleanup = () => {
|
||||
try {
|
||||
controller.hide({ silent: true });
|
||||
} catch {}
|
||||
cleanupRan = true;
|
||||
};
|
||||
|
||||
try {
|
||||
await controller.show('account');
|
||||
await waitForAnimationFrame(window, 2);
|
||||
|
||||
const layout = document.querySelector('[data-profile-layout]');
|
||||
const menuWrapper = document.querySelector('[data-profile-mobile-menu]');
|
||||
const paneWrapper = document.querySelector('[data-profile-mobile-pane]');
|
||||
const backButton = document.getElementById('profileModalBack');
|
||||
|
||||
assert.ok(layout && menuWrapper && paneWrapper && backButton);
|
||||
|
||||
assert.equal(layout?.dataset.mobileView, 'menu');
|
||||
assert.equal(menuWrapper?.getAttribute('aria-hidden'), 'false');
|
||||
assert.equal(menuWrapper?.classList.contains('hidden'), false);
|
||||
assert.equal(menuWrapper?.hasAttribute('hidden'), false);
|
||||
assert.equal(paneWrapper?.getAttribute('aria-hidden'), 'true');
|
||||
assert.equal(paneWrapper?.classList.contains('hidden'), true);
|
||||
assert.equal(paneWrapper?.hasAttribute('hidden'), true);
|
||||
assert.equal(backButton?.classList.contains('hidden'), true);
|
||||
|
||||
controller.selectPane('relays');
|
||||
await waitForAnimationFrame(window, 1);
|
||||
|
||||
assert.equal(layout?.dataset.mobileView, 'pane');
|
||||
assert.equal(menuWrapper?.getAttribute('aria-hidden'), 'true');
|
||||
assert.equal(menuWrapper?.classList.contains('hidden'), true);
|
||||
assert.equal(menuWrapper?.hasAttribute('hidden'), true);
|
||||
assert.equal(paneWrapper?.getAttribute('aria-hidden'), 'false');
|
||||
assert.equal(paneWrapper?.classList.contains('hidden'), false);
|
||||
assert.equal(paneWrapper?.hasAttribute('hidden'), false);
|
||||
assert.equal(backButton?.classList.contains('hidden'), false);
|
||||
|
||||
backButton?.click();
|
||||
await waitForAnimationFrame(window, 1);
|
||||
|
||||
assert.equal(layout?.dataset.mobileView, 'menu');
|
||||
assert.equal(menuWrapper?.getAttribute('aria-hidden'), 'false');
|
||||
assert.equal(menuWrapper?.classList.contains('hidden'), false);
|
||||
assert.equal(menuWrapper?.hasAttribute('hidden'), false);
|
||||
assert.equal(paneWrapper?.getAttribute('aria-hidden'), 'true');
|
||||
assert.equal(paneWrapper?.classList.contains('hidden'), true);
|
||||
assert.equal(paneWrapper?.hasAttribute('hidden'), true);
|
||||
assert.equal(backButton?.classList.contains('hidden'), true);
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
t.after(() => {
|
||||
if (!cleanupRan) {
|
||||
cleanup();
|
||||
}
|
||||
resetRuntimeFlags();
|
||||
});
|
||||
});
|
||||
|
||||
test('Profile modal uses abbreviated npub display', async () => {
|
||||
const sampleProfiles = [
|
||||
{
|
||||
@@ -454,9 +596,9 @@ test('admin mutations invoke accessControl stubs and update admin DOM', async ()
|
||||
},
|
||||
canEditAdminLists: () => true,
|
||||
isSuperAdmin: () => true,
|
||||
getEditors: () => ['npub1moderator'],
|
||||
getWhitelist: () => ['npub1allow'],
|
||||
getBlacklist: () => ['npub1block'],
|
||||
getEditors: () => new Set(['npub1moderator']),
|
||||
getWhitelist: () => new Set(['npub1allow']),
|
||||
getBlacklist: () => new Set(['npub1block']),
|
||||
addModerator: async (actor, target) => {
|
||||
accessEvents.push(['addModerator', actor, target]);
|
||||
return { ok: true };
|
||||
@@ -504,10 +646,13 @@ test('admin mutations invoke accessControl stubs and update admin DOM', async ()
|
||||
assert.equal(controller.navButtons.admin.classList.contains('hidden'), false);
|
||||
assert.equal(controller.moderatorSection.classList.contains('hidden'), false);
|
||||
assert.equal(controller.adminModeratorList.querySelectorAll('li').length, 1);
|
||||
assert.equal(controller.adminModeratorList.hasAttribute('hidden'), false);
|
||||
assert.equal(
|
||||
controller.whitelistList.querySelectorAll('button').length > 0,
|
||||
true,
|
||||
);
|
||||
assert.equal(controller.whitelistList.classList.contains('hidden'), false);
|
||||
assert.equal(controller.whitelistList.hasAttribute('hidden'), false);
|
||||
|
||||
controller.moderatorInput.value = 'npub1newmoderator';
|
||||
await controller.handleAddModerator();
|
||||
@@ -616,3 +761,106 @@ test('history pane lazily initializes the watch history renderer', async () => {
|
||||
assert.equal(controller.profileHistoryRenderer, null);
|
||||
assert.equal(controller.boundProfileHistoryVisibility, null);
|
||||
});
|
||||
|
||||
test('history metadata toggle updates stored preference', async (t) => {
|
||||
let storedPreference = true;
|
||||
let clearCalls = 0;
|
||||
|
||||
const originalSetPreference = watchHistoryService.setMetadataPreference;
|
||||
const originalShouldStore = watchHistoryService.shouldStoreMetadata;
|
||||
const originalClearMetadata = watchHistoryService.clearLocalMetadata;
|
||||
const originalSubscribe = watchHistoryService.subscribe;
|
||||
const originalIsEnabled = watchHistoryService.isEnabled;
|
||||
const originalSupportsLocal = watchHistoryService.supportsLocalHistory;
|
||||
const originalIsLocalOnly = watchHistoryService.isLocalOnly;
|
||||
|
||||
watchHistoryService.setMetadataPreference = (value) => {
|
||||
storedPreference = value !== false;
|
||||
};
|
||||
watchHistoryService.shouldStoreMetadata = () => storedPreference;
|
||||
watchHistoryService.clearLocalMetadata = () => {
|
||||
clearCalls += 1;
|
||||
};
|
||||
watchHistoryService.subscribe = () => () => {};
|
||||
watchHistoryService.isEnabled = () => true;
|
||||
watchHistoryService.supportsLocalHistory = () => true;
|
||||
watchHistoryService.isLocalOnly = () => false;
|
||||
|
||||
t.after(() => {
|
||||
watchHistoryService.setMetadataPreference = originalSetPreference;
|
||||
watchHistoryService.shouldStoreMetadata = originalShouldStore;
|
||||
watchHistoryService.clearLocalMetadata = originalClearMetadata;
|
||||
watchHistoryService.subscribe = originalSubscribe;
|
||||
watchHistoryService.isEnabled = originalIsEnabled;
|
||||
watchHistoryService.supportsLocalHistory = originalSupportsLocal;
|
||||
watchHistoryService.isLocalOnly = originalIsLocalOnly;
|
||||
});
|
||||
|
||||
const controller = createController({
|
||||
createWatchHistoryRenderer: (config) =>
|
||||
createWatchHistoryRenderer({
|
||||
...config,
|
||||
fetchHistory: async () => ({ items: [], metadata: {} }),
|
||||
snapshot: async () => ({}),
|
||||
}),
|
||||
services: {
|
||||
nostrClient: { sessionActor: { pubkey: defaultActorHex } },
|
||||
getCurrentUserNpub: () => defaultActorNpub,
|
||||
},
|
||||
});
|
||||
|
||||
await controller.load();
|
||||
applyDesignSystemAttributes(document);
|
||||
|
||||
controller.refreshAdminPaneState = async () => {};
|
||||
controller.populateProfileRelays = () => {};
|
||||
controller.populateBlockedList = () => {};
|
||||
controller.refreshWalletPaneState = () => {};
|
||||
|
||||
controller.setActivePubkey(defaultActorHex);
|
||||
|
||||
let cleanupRan = false;
|
||||
const cleanup = () => {
|
||||
try {
|
||||
controller.hide({ silent: true });
|
||||
} catch {}
|
||||
cleanupRan = true;
|
||||
};
|
||||
|
||||
try {
|
||||
await controller.show('history');
|
||||
await waitForAnimationFrame(window, 3);
|
||||
|
||||
const toggle = document.getElementById('profileHistoryMetadataToggle');
|
||||
assert.ok(toggle);
|
||||
assert.equal(toggle.getAttribute('aria-checked'), 'true');
|
||||
assert.equal(toggle.getAttribute('data-enabled'), 'true');
|
||||
|
||||
toggle.dispatchEvent(
|
||||
new window.MouseEvent('click', { bubbles: true, cancelable: true }),
|
||||
);
|
||||
await waitForAnimationFrame(window, 2);
|
||||
|
||||
assert.equal(storedPreference, false);
|
||||
assert.equal(toggle.getAttribute('aria-checked'), 'false');
|
||||
assert.equal(toggle.getAttribute('data-enabled'), 'false');
|
||||
assert.equal(clearCalls, 1);
|
||||
|
||||
toggle.dispatchEvent(
|
||||
new window.MouseEvent('click', { bubbles: true, cancelable: true }),
|
||||
);
|
||||
await waitForAnimationFrame(window, 2);
|
||||
|
||||
assert.equal(storedPreference, true);
|
||||
assert.equal(toggle.getAttribute('aria-checked'), 'true');
|
||||
assert.equal(toggle.getAttribute('data-enabled'), 'true');
|
||||
} finally {
|
||||
cleanup();
|
||||
}
|
||||
|
||||
t.after(() => {
|
||||
if (!cleanupRan) {
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user