Rewrite mobile explorer

This commit is contained in:
saberzero1 2024-10-01 22:12:17 +02:00
parent b0c079f24a
commit a190cba737
No known key found for this signature in database
GPG Key ID: 41AEE99107640F10
5 changed files with 445 additions and 232 deletions

View File

@ -27,7 +27,7 @@ export const defaultContentPageLayout: PageLayout = {
Component.MobileOnly(Component.Spacer()), Component.MobileOnly(Component.Spacer()),
Component.Search(), Component.Search(),
Component.Darkmode(), Component.Darkmode(),
Component.DesktopOnly(Component.Explorer()), Component.Explorer(),
], ],
right: [ right: [
Component.Graph(), Component.Graph(),

View File

@ -5,7 +5,6 @@ import explorerStyle from "./styles/explorer.scss"
import script from "./scripts/explorer.inline" import script from "./scripts/explorer.inline"
import { ExplorerNode, FileNode, Options } from "./ExplorerNode" import { ExplorerNode, FileNode, Options } from "./ExplorerNode"
import { QuartzPluginData } from "../plugins/vfile" import { QuartzPluginData } from "../plugins/vfile"
import { classNames } from "../util/lang"
import { i18n } from "../i18n" import { i18n } from "../i18n"
// Options interface defined in `ExplorerNode` to avoid circular dependency // Options interface defined in `ExplorerNode` to avoid circular dependency
@ -13,6 +12,7 @@ const defaultOptions = {
folderClickBehavior: "collapse", folderClickBehavior: "collapse",
folderDefaultState: "collapsed", folderDefaultState: "collapsed",
useSavedState: true, useSavedState: true,
usePagePath: false,
mapFn: (node) => { mapFn: (node) => {
return node return node
}, },
@ -46,7 +46,7 @@ export default ((userOpts?: Partial<Options>) => {
let jsonTree: string let jsonTree: string
let lastBuildId: string = "" let lastBuildId: string = ""
function constructFileTree(allFiles: QuartzPluginData[]) { function constructFileTree(allFiles: QuartzPluginData[], currentFilePath: string) {
// Construct tree from allFiles // Construct tree from allFiles
fileTree = new FileNode("") fileTree = new FileNode("")
allFiles.forEach((file) => fileTree.add(file)) allFiles.forEach((file) => fileTree.add(file))
@ -68,7 +68,10 @@ export default ((userOpts?: Partial<Options>) => {
// Get all folders of tree. Initialize with collapsed state // Get all folders of tree. Initialize with collapsed state
// Stringify to pass json tree as data attribute ([data-tree]) // Stringify to pass json tree as data attribute ([data-tree])
const folders = fileTree.getFolderPaths(opts.folderDefaultState === "collapsed") const folders = fileTree.getFolderPaths(
opts.folderDefaultState === "collapsed",
currentFilePath,
)
jsonTree = JSON.stringify(folders) jsonTree = JSON.stringify(folders)
} }
@ -81,42 +84,78 @@ export default ((userOpts?: Partial<Options>) => {
}: QuartzComponentProps) => { }: QuartzComponentProps) => {
if (ctx.buildId !== lastBuildId) { if (ctx.buildId !== lastBuildId) {
lastBuildId = ctx.buildId lastBuildId = ctx.buildId
constructFileTree(allFiles) constructFileTree(allFiles, (fileData.filePath ?? "").replaceAll(" ", "-"))
} }
return ( return (
<div class={classNames(displayClass, "explorer")}> <div class="explorer-container">
<button <div class={`mobile-explorer explorer ${displayClass ?? ""}`}>
type="button" <button
id="explorer" type="button"
data-behavior={opts.folderClickBehavior} id="mobile-explorer"
data-collapsed={opts.folderDefaultState} class="collapsed"
data-savestate={opts.useSavedState} data-behavior={opts.folderClickBehavior}
data-tree={jsonTree} data-collapsed={opts.folderDefaultState}
aria-controls="explorer-content" data-savestate={opts.useSavedState}
aria-expanded={opts.folderDefaultState === "open"} data-pagepathstate={opts.usePagePath}
> data-tree={jsonTree}
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2> data-mobile={true}
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="5 8 14 8"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="fold"
> >
<polyline points="6 9 12 15 18 9"></polyline> <svg
</svg> xmlns="http://www.w3.org/2000/svg"
</button> width="24"
<div id="explorer-content"> height="24"
<ul class="overflow" id="explorer-ul"> viewBox="0 0 24 24"
<ExplorerNode node={fileTree} opts={opts} fileData={fileData} /> stroke-width="2"
<li id="explorer-end" /> stroke-linecap="round"
</ul> stroke-linejoin="round"
class="lucide lucide-menu"
>
<line x1="4" x2="20" y1="12" y2="12" />
<line x1="4" x2="20" y1="6" y2="6" />
<line x1="4" x2="20" y1="18" y2="18" />
</svg>
</button>
<div id="explorer-content" class="collapsed">
<ul class="overflow" id="explorer-ul">
<ExplorerNode node={fileTree} opts={opts} fileData={fileData} />
<li id="explorer-end" />
</ul>
</div>
</div>
<div class={`desktop-explorer explorer ${displayClass ?? ""}`}>
<button
type="button"
id="desktop-explorer"
class="title-button"
data-behavior={opts.folderClickBehavior}
data-collapsed={opts.folderDefaultState}
data-savestate={opts.useSavedState}
data-pagepathstate={opts.usePagePath}
data-tree={jsonTree}
data-mobile={false}
>
<h2>{opts.title ?? i18n(cfg.locale).components.explorer.title}</h2>
<svg
xmlns="http://www.w3.org/2000/svg"
width="14"
height="14"
viewBox="5 8 14 8"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="fold"
>
<polyline points="6 9 12 15 18 9"></polyline>
</svg>
</button>
<div id="explorer-content" class="">
<ul class="overflow" id="explorer-ul">
<ExplorerNode node={fileTree} opts={opts} fileData={fileData} />
<li id="explorer-end" />
</ul>
</div>
</div> </div>
</div> </div>
) )

View File

@ -16,6 +16,7 @@ export interface Options {
folderDefaultState: "collapsed" | "open" folderDefaultState: "collapsed" | "open"
folderClickBehavior: "collapse" | "link" folderClickBehavior: "collapse" | "link"
useSavedState: boolean useSavedState: boolean
usePagePath: boolean
sortFn: (a: FileNode, b: FileNode) => number sortFn: (a: FileNode, b: FileNode) => number
filterFn: (node: FileNode) => boolean filterFn: (node: FileNode) => boolean
mapFn: (node: FileNode) => void mapFn: (node: FileNode) => void
@ -124,9 +125,10 @@ export class FileNode {
* Get folder representation with state of tree. * Get folder representation with state of tree.
* Intended to only be called on root node before changes to the tree are made * Intended to only be called on root node before changes to the tree are made
* @param collapsed default state of folders (collapsed by default or not) * @param collapsed default state of folders (collapsed by default or not)
* @param currentFile current file
* @returns array containing folder state for tree * @returns array containing folder state for tree
*/ */
getFolderPaths(collapsed: boolean): FolderState[] { getFolderPaths(collapsed: boolean, currentFile: string): FolderState[] {
const folderPaths: FolderState[] = [] const folderPaths: FolderState[] = []
const traverse = (node: FileNode, currentPath: string) => { const traverse = (node: FileNode, currentPath: string) => {

View File

@ -1,7 +1,13 @@
import { FolderState } from "../ExplorerNode" import { FolderState } from "../ExplorerNode"
// Current state of folders
type MaybeHTMLElement = HTMLElement | undefined type MaybeHTMLElement = HTMLElement | undefined
let currentExplorerState: FolderState[] let currentExplorerState: FolderState[]
function escapeCharacters(str: string) {
return str.replace(/'/g, "\\'").replace(/"/g, '\\"')
}
const observer = new IntersectionObserver((entries) => { const observer = new IntersectionObserver((entries) => {
// If last element is observed, remove gradient of "overflow" class so element is visible // If last element is observed, remove gradient of "overflow" class so element is visible
const explorerUl = document.getElementById("explorer-ul") const explorerUl = document.getElementById("explorer-ul")
@ -15,24 +21,17 @@ const observer = new IntersectionObserver((entries) => {
} }
}) })
function toggleExplorer(this: HTMLElement) {
this.classList.toggle("collapsed")
this.setAttribute(
"aria-expanded",
this.getAttribute("aria-expanded") === "true" ? "false" : "true",
)
const content = this.nextElementSibling as MaybeHTMLElement
if (!content) return
content.classList.toggle("collapsed")
}
function toggleFolder(evt: MouseEvent) { function toggleFolder(evt: MouseEvent) {
evt.stopPropagation() evt.stopPropagation()
// Element that was clicked
const target = evt.target as MaybeHTMLElement const target = evt.target as MaybeHTMLElement
if (!target) return if (!target) return
// Check if target was svg icon or button
const isSvg = target.nodeName === "svg" const isSvg = target.nodeName === "svg"
// corresponding <ul> element relative to clicked button/folder
const childFolderContainer = ( const childFolderContainer = (
isSvg isSvg
? target.parentElement?.nextSibling ? target.parentElement?.nextSibling
@ -42,68 +41,150 @@ function toggleFolder(evt: MouseEvent) {
isSvg ? target.nextElementSibling : target.parentElement isSvg ? target.nextElementSibling : target.parentElement
) as MaybeHTMLElement ) as MaybeHTMLElement
if (!(childFolderContainer && currentFolderParent)) return if (!(childFolderContainer && currentFolderParent)) return
// <li> element of folder (stores folder-path dataset)
childFolderContainer.classList.toggle("open") childFolderContainer.classList.toggle("open")
// Collapse folder container
const isCollapsed = childFolderContainer.classList.contains("open") const isCollapsed = childFolderContainer.classList.contains("open")
setFolderState(childFolderContainer, !isCollapsed) setFolderState(childFolderContainer, !isCollapsed)
// Save folder state to localStorage
const fullFolderPath = currentFolderParent.dataset.folderpath as string const fullFolderPath = currentFolderParent.dataset.folderpath as string
toggleCollapsedByPath(currentExplorerState, fullFolderPath) toggleCollapsedByPath(currentExplorerState, fullFolderPath)
const stringifiedFileTree = JSON.stringify(currentExplorerState) const stringifiedFileTree = JSON.stringify(currentExplorerState)
localStorage.setItem("fileTree", stringifiedFileTree) localStorage.setItem("fileTree", stringifiedFileTree)
} }
function setupExplorer() { function toggleExplorer(this: HTMLElement) {
const explorer = document.getElementById("explorer") // Toggle collapsed state of entire explorer
if (!explorer) return this.classList.toggle("collapsed")
const content = this.nextElementSibling as MaybeHTMLElement
if (!content) return
content.classList.toggle("collapsed")
//content.style.maxHeight = content.style.maxHeight === "0px" ? content.scrollHeight + "px" : "0px"
content.style.maxHeight = content.style.maxHeight === "0px" ? "calc(100vh - 8rem)" : "0px"
if (explorer.dataset.behavior === "collapse") { //prevent scroll under
if (this.dataset.mobile === "true" && document.querySelector("#mobile-explorer")) {
const article = document.querySelectorAll(
".popover-hint, footer, .backlinks, .graph, .toc, #progress",
)
const header = document.querySelector(".page .page-header")
if (article)
article.forEach((element) => {
element.classList.toggle("no-scroll")
})
if (header) header.classList.toggle("fixed")
}
}
function setupExplorer() {
// Set click handler for collapsing entire explorer
const allExplorers = document.querySelectorAll(".explorer > button") as NodeListOf<HTMLElement>
for (const explorer of allExplorers) {
// Get folder state from local storage
const storageTree = localStorage.getItem("fileTree")
// Convert to bool
const useSavedFolderState = explorer?.dataset.savestate === "true"
if (explorer) {
// Get config
const collapseBehavior = explorer.dataset.behavior
// Add click handlers for all folders (click handler on folder "label")
if (collapseBehavior === "collapse") {
for (const item of document.getElementsByClassName(
"folder-button",
) as HTMLCollectionOf<HTMLElement>) {
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
item.addEventListener("click", toggleFolder)
}
}
// Add click handler to main explorer
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer))
explorer.addEventListener("click", toggleExplorer)
}
// Set up click handlers for each folder (click handler on folder "icon")
for (const item of document.getElementsByClassName( for (const item of document.getElementsByClassName(
"folder-button", "folder-icon",
) as HTMLCollectionOf<HTMLElement>) { ) as HTMLCollectionOf<HTMLElement>) {
item.addEventListener("click", toggleFolder) item.addEventListener("click", toggleFolder)
window.addCleanup(() => item.removeEventListener("click", toggleFolder)) window.addCleanup(() => item.removeEventListener("click", toggleFolder))
} }
// Get folder state from local storage
const oldExplorerState: FolderState[] =
storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
//console.log(explorer.dataset.tree)
//console.log(explorer.dataset.tree ? JSON.parse(explorer.dataset.tree) : [])
const newExplorerState: FolderState[] = explorer.dataset.tree
? JSON.parse(explorer.dataset.tree)
: []
currentExplorerState = []
for (const { path, collapsed } of newExplorerState) {
currentExplorerState.push({ path, collapsed: /*oldIndex.get(path) ?? */ collapsed })
}
currentExplorerState.map((folderState) => {
const folderLi = document.querySelector(
`[data-folderpath='${folderState.path.replace("'", "-")}']`,
) as MaybeHTMLElement
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
if (folderUl) {
setFolderState(folderUl, folderState.collapsed)
}
})
} }
}
explorer.addEventListener("click", toggleExplorer) function toggleExplorerFolders() {
window.addCleanup(() => explorer.removeEventListener("click", toggleExplorer)) const currentFile = (document.querySelector("body")?.getAttribute("data-slug") ?? "").replace(
/\/index$/g,
"",
)
const listToReplace = document.querySelectorAll(".folder-outer:has(> ul[data-folderul]")
// Set up click handlers for each folder (click handler on folder "icon") listToReplace.forEach((element) => {
for (const item of document.getElementsByClassName( if (element.children.length > 0) {
"folder-icon", if (currentFile.includes(element.firstElementChild?.getAttribute("data-folderul") ?? "")) {
) as HTMLCollectionOf<HTMLElement>) { if (!element.classList.contains("open")) {
item.addEventListener("click", toggleFolder) element.classList.add("open")
window.addCleanup(() => item.removeEventListener("click", toggleFolder)) }
} }
// Get folder state from local storage
const storageTree = localStorage.getItem("fileTree")
const useSavedFolderState = explorer?.dataset.savestate === "true"
const oldExplorerState: FolderState[] =
storageTree && useSavedFolderState ? JSON.parse(storageTree) : []
const oldIndex = new Map(oldExplorerState.map((entry) => [entry.path, entry.collapsed]))
const newExplorerState: FolderState[] = explorer.dataset.tree
? JSON.parse(explorer.dataset.tree)
: []
currentExplorerState = []
for (const { path, collapsed } of newExplorerState) {
currentExplorerState.push({ path, collapsed: oldIndex.get(path) ?? collapsed })
}
currentExplorerState.map((folderState) => {
const folderLi = document.querySelector(
`[data-folderpath='${folderState.path}']`,
) as MaybeHTMLElement
const folderUl = folderLi?.parentElement?.nextElementSibling as MaybeHTMLElement
if (folderUl) {
setFolderState(folderUl, folderState.collapsed)
} }
}) })
} }
window.addEventListener("resize", setupExplorer) window.addEventListener("resize", setupExplorer)
document.addEventListener("DOMContentLoaded", () => {
const explorer = document.querySelector("#mobile-explorer")
if (explorer) {
explorer.classList.add("collapsed")
const content = explorer.nextElementSibling as HTMLElement
content.classList.add("collapsed")
content.style.maxHeight = "0px"
}
toggleExplorerFolders()
})
document.addEventListener("nav", () => { document.addEventListener("nav", () => {
const explorer = document.querySelector("#mobile-explorer")
if (explorer) {
explorer.classList.add("collapsed")
const content = explorer.nextElementSibling as HTMLElement
content.classList.add("collapsed")
content.style.maxHeight = "0px"
}
setupExplorer() setupExplorer()
//add collapsed class to all folders
observer.disconnect() observer.disconnect()
// select pseudo element at end of list // select pseudo element at end of list
@ -111,6 +192,8 @@ document.addEventListener("nav", () => {
if (lastItem) { if (lastItem) {
observer.observe(lastItem) observer.observe(lastItem)
} }
toggleExplorerFolders()
}) })
/** /**

View File

@ -1,15 +1,31 @@
@use "../../styles/variables.scss" as *; @use "../../styles/variables.scss" as *;
.explorer { .explorer-container {
display: flex; .mobile-explorer {
flex-direction: column; display: none;
overflow-y: hidden; }
&.desktop-only { .desktop-explorer {
@media all and not ($mobile) { display: flex;
}
@media all and ($mobile) {
.mobile-explorer {
display: flex; display: flex;
} }
.desktop-explorer {
display: none;
}
} }
/*&:after {
.mobile-explorer,
.desktop-explorer {
flex-direction: column;
overflow-y: hidden;
&.desktop-only {
@media all and not ($mobile) {
display: flex;
}
}
/*&:after {
pointer-events: none; pointer-events: none;
content: ""; content: "";
width: 100%; width: 100%;
@ -21,161 +37,234 @@
transition: opacity 0.3s ease; transition: opacity 0.3s ease;
background: linear-gradient(transparent 0px, var(--light)); background: linear-gradient(transparent 0px, var(--light));
}*/ }*/
}
button#explorer {
background-color: transparent;
border: none;
text-align: left;
cursor: pointer;
padding: 0;
color: var(--dark);
display: flex;
align-items: center;
& h2 {
font-size: 1rem;
display: inline-block;
margin: 0;
} }
& .fold { button#mobile-explorer,
margin-left: 0.5rem; button#desktop-explorer {
transition: transform 0.3s ease;
opacity: 0.8;
}
&.collapsed .fold {
transform: rotateZ(-90deg);
}
}
.folder-outer {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease-in-out;
}
.folder-outer.open {
grid-template-rows: 1fr;
}
.folder-outer > ul {
overflow: hidden;
}
#explorer-content {
list-style: none;
overflow: hidden;
overflow-y: auto;
max-height: 100%;
transition:
max-height 0.35s ease,
visibility 0s linear 0s;
margin-top: 0.5rem;
visibility: visible;
&.collapsed {
max-height: 0;
transition:
max-height 0.35s ease,
visibility 0s linear 0.35s;
visibility: hidden;
}
& ul {
list-style: none;
margin: 0.08rem 0;
padding: 0;
transition:
max-height 0.35s ease,
transform 0.35s ease,
opacity 0.2s ease;
& li > a {
color: var(--dark);
opacity: 0.75;
pointer-events: all;
}
}
> #explorer-ul {
max-height: none;
}
}
svg {
pointer-events: all;
& > polyline {
pointer-events: none;
}
}
.folder-container {
flex-direction: row;
display: flex;
align-items: center;
user-select: none;
& div > a {
color: var(--secondary);
font-family: var(--headerFont);
font-size: 0.95rem;
font-weight: $semiBoldWeight;
line-height: 1.5rem;
display: inline-block;
}
& div > a:hover {
color: var(--tertiary);
}
& div > button {
color: var(--dark);
background-color: transparent; background-color: transparent;
border: none; border: none;
text-align: left; text-align: left;
cursor: pointer; cursor: pointer;
padding-left: 0; padding: 0;
padding-right: 0; color: var(--dark);
display: flex; display: flex;
align-items: center; align-items: center;
font-family: var(--headerFont);
& span { & h2 {
font-size: 0.95rem; font-size: 1rem;
display: inline-block; display: inline-block;
color: var(--secondary);
font-weight: $semiBoldWeight;
margin: 0; margin: 0;
line-height: 1.5rem; }
& .fold {
margin-left: 0.5rem;
transition: transform 0.3s ease;
opacity: 0.8;
}
&.collapsed .fold {
transform: rotateZ(-90deg);
}
}
.folder-outer {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s ease-in-out;
}
.folder-outer.open {
grid-template-rows: 1fr;
}
.folder-outer > ul {
overflow: hidden;
}
#explorer-content {
list-style: none;
overflow: hidden;
overflow-y: auto;
max-height: 100%;
transition:
max-height 0.35s ease,
visibility 0s linear 0s;
margin-top: 0.5rem;
visibility: visible;
&.collapsed {
max-height: 0;
transition:
max-height 0.35s ease,
visibility 0s linear 0.35s;
visibility: hidden;
}
& ul {
list-style: none;
margin: 0.08rem 0;
padding: 0;
transition:
max-height 0.35s ease,
transform 0.35s ease,
opacity 0.2s ease;
& li > a {
color: var(--dark);
opacity: 0.75;
pointer-events: all;
}
}
> #explorer-ul {
max-height: none;
}
}
svg {
pointer-events: all;
& > polyline {
pointer-events: none; pointer-events: none;
} }
} }
}
.folder-icon { .folder-container {
margin-right: 5px; flex-direction: row;
color: var(--secondary); display: flex;
cursor: pointer; align-items: center;
transition: transform 0.3s ease; user-select: none;
backface-visibility: visible;
}
li:has(> .folder-outer:not(.open)) > .folder-container > svg { & div > a {
transform: rotate(-90deg); color: var(--secondary);
} font-family: var(--headerFont);
font-size: 0.95rem;
font-weight: $semiBoldWeight;
line-height: 1.5rem;
display: inline-block;
}
.folder-icon:hover { & div > a:hover {
color: var(--tertiary); color: var(--tertiary);
} }
.no-background::after { & div > button {
background: none !important; color: var(--dark);
} background-color: transparent;
border: none;
text-align: left;
cursor: pointer;
padding-left: 0;
padding-right: 0;
display: flex;
align-items: center;
font-family: var(--headerFont);
#explorer-end { & span {
// needs height so IntersectionObserver gets triggered font-size: 0.95rem;
height: 4px; display: inline-block;
// remove default margin from li color: var(--secondary);
margin: 0; font-weight: $semiBoldWeight;
margin: 0;
line-height: 1.5rem;
pointer-events: none;
}
}
}
.folder-icon {
margin-right: 5px;
color: var(--secondary);
cursor: pointer;
transition: transform 0.3s ease;
backface-visibility: visible;
}
li:has(> .folder-outer:not(.open)) > .folder-container > svg {
transform: rotate(-90deg);
}
.folder-icon:hover {
color: var(--tertiary);
}
.no-background::after {
background: none !important;
}
#explorer-end {
// needs height so IntersectionObserver gets triggered
height: 4px;
// remove default margin from li
margin: 0;
}
.mobile-explorer,
.desktop-explorer {
#explorer-content {
//left: 0;
z-index: 100;
position: absolute;
//max-height: max-content !important;
background-color: var(--light);
//max-width: calc(100% - 36px);
max-width: 100%;
width: calc(100% - 36px + 4px);
//margin-right: 36px;
transition: all 300ms ease-in-out;
overflow: hidden;
padding: 0 16px;
height: calc(100dvh - 8rem);
//border-top-left-radius: 23px;
//border-top-right-radius: 23px;
border-radius: 23px;
margin-top: 36px;
&:not(.collapsed) {
height: calc(100dvh - 8rem);
//top: 10dvh;
}
ul.overflow {
max-height: calc(100dvh - 8rem);
width: 100%;
}
&.collapsed {
height: 0;
}
}
#mobile-explorer,
#desktop-explorer {
&:not(.collapsed) .lucide-menu {
transform: rotate(90deg);
transition: transform 200ms ease-in-out;
}
.lucide-menu {
stroke: var(--darkgray);
transition: transform 200ms ease;
&:hover {
stroke: var(--dark);
}
}
}
}
.no-scroll {
opacity: 0;
overflow: hidden;
}
html:has(.no-scroll) {
overflow: hidden;
}
@media all and not ($mobile) {
.no-scroll {
opacity: 1 !important;
overflow: auto !important;
}
html:has(.no-scroll) {
overflow: auto !important;
}
}
} }