diff options
Diffstat (limited to 'src/static')
42 files changed, 7874 insertions, 3404 deletions
diff --git a/src/static/css/features.css b/src/static/css/features.css new file mode 100644 index 00000000..33a7b9c9 --- /dev/null +++ b/src/static/css/features.css @@ -0,0 +1,2365 @@ +/* Additional files lists */ + +@layer layout { + .additional-files-list { + padding-left: 0; + } + + .additional-files-list > li { + list-style-type: none; + } + + .additional-files-list summary { + /* Sorry, Safari! + * https://bugs.webkit.org/show_bug.cgi?id=157323 + */ + list-style-position: outside; + margin-left: 40px; + } + + .additional-files-list details ul { + margin-left: 40px; + margin-top: 2px; + margin-bottom: 10px; + } + + .additional-files-list .entry-description { + list-style-type: none; + max-width: 540px; + + /* This should be margin-bottom, but cascading rules + * cause some awkwardness - `#content li` takes precedence. + */ + padding-bottom: 3px; + } +} + +/* Additional names (heading and box) */ + +@layer layout { + #additional-names-box { + --custom-scroll-offset: calc(0.5em - 2px); + + margin: 1em 0 1em -10px; + max-width: min(60vw, 600px); + + padding: 15px 20px 10px 20px; + } + + #additional-names-box > :first-child { margin-top: 0; } + #additional-names-box > :last-child { margin-bottom: 0; } + + #additional-names-box p { + padding-left: 10px; + padding-right: 10px; + margin-bottom: 0; + } + + #additional-names-box ul { + padding-left: 10px; + margin-top: 0.5em; + } + + #additional-names-box li .additional-name { + margin-right: 0.25em; + } + + #additional-names-box li .additional-name .content-image { + margin-bottom: 0.25em; + margin-top: 0.5em; + } + + #content.top-index #additional-names-box { + margin-left: auto; + margin-right: auto; + + /* So like, these were in two separate, right-nearby + * rules with the same selector, lol + */ + margin-bottom: 2em; + margin-bottom: 0.75em; + } +} + +@layer print { + #additional-names-box p { + font-style: oblique; + } + + #additional-names-box li .accent { + opacity: 0.8; + } + + #additional-names-box li .additional-name > img { + vertical-align: text-bottom; + } + + #content.top-index #additional-names-box { + text-align: center; + } +} + +@layer interaction { + h1 a[href="#additional-names-box"] { + color: inherit; + text-decoration: underline; + text-decoration-style: dotted; + } + + h1 a[href="#additional-names-box"]:hover { + text-decoration-style: solid; + } + + #additional-names-box:not(.always-visible) { + display: none; + } +} + +/* Carousels */ + +@layer layout { + .carousel-container { + --carousel-tile-min-width: 120px; + --carousel-row-count: 3; + --carousel-column-count: 6; + + position: relative; + overflow: hidden; + margin: 20px 0 5px 0; + padding: 8px 0; + } + + .carousel-container + .quick-info, + .carousel-container + .quick-description { + margin-top: 25px; + } + + html[data-url-key="localized.home"] .carousel-container { + /* This is currently unused... */ + --carousel-tile-size: 140px; + } + + .carousel-container[data-carousel-rows="1"] { --carousel-row-count: 1; } + .carousel-container[data-carousel-rows="2"] { --carousel-row-count: 2; } + .carousel-container[data-carousel-rows="3"] { --carousel-row-count: 3; } + .carousel-container[data-carousel-columns="4"] { --carousel-column-count: 4; } + .carousel-container[data-carousel-columns="5"] { --carousel-column-count: 5; } + .carousel-container[data-carousel-columns="6"] { --carousel-column-count: 6; } + + .carousel-grid { + /* Thanks to: https://css-tricks.com/an-auto-filling-css-grid-with-max-columns/ */ + --carousel-gap-count: calc(var(--carousel-column-count) - 1); + --carousel-total-gap-width: calc(var(--carousel-gap-count) * 10px); + --carousel-calculated-tile-max-width: calc((100% - var(--carousel-total-gap-width)) / var(--carousel-column-count)); + + position: relative; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(max(var(--carousel-tile-min-width), var(--carousel-calculated-tile-max-width)), 1fr)); + grid-template-rows: repeat(var(--carousel-row-count), auto); + grid-auto-flow: dense; + grid-auto-rows: 0; + overflow: hidden; + margin: auto; + + z-index: 5; + } + + .carousel-grid:nth-child(2), + .carousel-grid:nth-child(3) { + position: absolute; + top: 8px; + left: 0; + right: 0; + } + + .carousel-item { + display: inline-block; + margin: 0; + flex: 1 1 150px; + padding: 3px; + } + + .carousel-item .image-outer-area { + padding: 0; + } + + .carousel-item .image { + width: 100%; + height: 100%; + margin-top: auto; + margin-bottom: auto; + } +} + +@layer material { + .carousel-container { + border-radius: 4px; + } + + .carousel-container::before, + .carousel-container::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + + .carousel-container::before { + background-color: var(--dim-color); + filter: brightness(0.6); + } + + .carousel-container::after { + z-index: 40; + pointer-events: none; + + border: 1px solid var(--primary-color); + border-radius: 4px; + + box-shadow: + inset 20px 2px 40px var(--shadow-color), + inset -20px -2px 40px var(--shadow-color); + } + + .carousel-item { + border-radius: 7px; + } + + .carousel-item .image-container { + border: none; + border-radius: 6px; + } + + .carousel-item .image-inner-area::after { + box-shadow: none; + } +} + +@layer interaction { + .carousel-container:hover .carousel-grid { + animation-play-state: running; + } + + .carousel-grid { + transform: translateX(0); + animation: carousel-marquee1 40s linear infinite; + animation-play-state: paused; + } + + .carousel-grid:nth-child(2) { + animation-name: carousel-marquee2; + } + + .carousel-grid:nth-child(3) { + animation-name: carousel-marquee3; + } + + @keyframes carousel-marquee1 { + 0% { + transform: translateX(-100%) translateX(70px); + } + + 100% { + transform: translateX(-200%) translateX(70px); + } + } + + @keyframes carousel-marquee2 { + 0% { + transform: translateX(0%) translateX(70px); + } + + 100% { + transform: translateX(-100%) translateX(70px); + } + } + + @keyframes carousel-marquee3 { + 0% { + transform: translateX(100%) translateX(70px); + } + + 100% { + transform: translateX(0%) translateX(70px); + } + } + + .carousel-item { + filter: brightness(0.8); + } + + .carousel-item:hover { + filter: brightness(1); + background: var(--dim-color); + } +} + +/* Content entries (at large) */ + +@layer layout { + #content blockquote { + margin-left: 40px; + margin-right: min(8vw, 75px); + width: auto; + } + + #content blockquote blockquote { + margin-left: 10px; + padding-left: 10px; + margin-right: 20px; + padding-top: 6px; + padding-bottom: 6px; + } + + #content blockquote blockquote > :first-child { + margin-top: 0; + } + + #content blockquote blockquote > :last-child { + margin-bottom: 0; + } +} + +@layer material { + #content blockquote blockquote { + border-left: dotted 1px; + } +} + +@layer print { + #content blockquote h2 { + font-size: 1em; + font-weight: 800; + } + + #content blockquote h3 { + font-size: 1em; + font-weight: normal; + font-style: oblique; + } +} + +/* Commentary-style content entries */ + +@layer layout { + .inherited-commentary-section { + clear: right; + margin-top: 1em; + margin-bottom: 1.5em; + margin-right: min(4vw, 60px); + } + + .commentary-art { + float: right; + width: 30%; + max-width: 250px; + margin: 15px 0 10px 20px; + } +} + +@layer material { + .inherited-commentary-section { + border: 2px solid var(--deep-color); + border-radius: 4px; + background: #ffffff07; + } + + .cover-artwork.commentary-art { + box-shadow: 0 0 4px 5px rgba(0, 0, 0, 0.25) !important; + } +} + +@layer print { + .content-entry-heading { + margin-left: 15px; + padding-left: calc(5px + 1.25ch); + text-indent: -1.25ch; + margin-right: min(calc(8vw - 35px), 45px); + padding-bottom: 0.2em; + + border-bottom: 1px solid var(--dim-color); + } + + .content-entry-accent { + margin-left: 0.25ch; + } + + .content-entry-heading.dated .content-entry-accent { + margin-right: 0.75ch; + } + + .content-entry-annotation { + font-style: oblique; + } + + .content-entry-heading .commentary-date { + display: inline-block; + text-indent: 0; + } + + .content-entry-heading .hoverable { + box-shadow: 1px 2px 6px 5px #04040460; + } + + .content-entry-body summary { + list-style-position: outside; + } + + .content-entry-body summary > span { + color: var(--primary-color); + } + + .wiki-commentary s:not(.spoiler) { + text-decoration-color: #fff9; + text-decoration-thickness: 1.4px; + color: #fffb; + } +} + +/* Content headings */ + +@layer layout { + .content-heading { + border-bottom: 3px double transparent; + margin-bottom: -3px; + } + + dl dt.content-heading { + /* Basic margin-bottom for dt is 2px, + * so just subtract 3px from that. + */ + margin-bottom: -1px; + } + + h3.content-heading { + clear: both; + } +} + +@layer construction { + summary.content-heading { + list-style-type: none; + } + + summary.content-heading .cue { + display: inline-flex; + } + + summary.content-heading .cue::after { + content: ""; + padding-left: 0.5ch; + display: list-item; + list-style-position: inside; + } +} + +@layer interaction { + .content-heading.highlight-hash-link { + animation: highlight-hash-link 4s; + animation-delay: 125ms; + } + + /* This animation's name is referenced in JavaScript */ + @keyframes highlight-hash-link { + 0% { + border-bottom-color: transparent; + } + + 10% { + border-bottom-color: white; + } + + 25% { + border-bottom-color: white; + } + + 100% { + border-bottom-color: transparent; + } + } + + summary.content-heading .cue { + color: var(--primary-color); + } + + summary.content-heading .cue::after { + list-style-type: disclosure-closed; + } + + details[open] > summary.content-heading .cue::after { + list-style-type: disclosure-open; + } + + summary.content-heading > span:hover { + text-decoration: none !important; + } + + summary.content-heading > span:hover .cue { + text-decoration: underline; + text-decoration-style: wavy; + } + + summary.content-heading .when-open { + display: none; + } + + details[open] > summary.content-heading .when-open { + display: unset; + } + + details[open] > summary.content-heading .when-collapsed { + display: none; + } +} + +/* Cover artworks, music videos, and album art info */ + +@layer layout { + .cover-artwork .image { + display: block; + width: 100%; + height: 100%; + } + + .music-video .image { + display: block; + aspect-ratio: 16 / 9; + width: 100%; + height: 100%; + } + + #artwork-column .album-art-info { + margin: 10px min(15px, 1vw) 15px; + } + + #artwork-column .cover-artwork:not(:first-child), + #artwork-column .cover-artwork-joiner { + margin-left: 30px; + margin-right: 5px; + } + + #artwork-column .cover-artwork:first-child + .cover-artwork-joiner, + #artwork-column .cover-artwork.attached-artwork-is-main-artwork, + #artwork-column .cover-artwork.attached-artwork-is-main-artwork + .cover-artwork-joiner { + margin-left: 17.5px; + margin-right: 17.5px; + } + + #artwork-column *:where(.cover-artwork, .music-video):where(:not(:first-child)) { + margin-top: 20px; + } + + #artwork-column *:is(.cover-artwork, .music-video):last-child:not(:first-child) { + margin-bottom: 25px; + } + + .cover-artwork-joiner { + z-index: -2; + } + + .cover-artwork-joiner + .cover-artwork { + margin-top: 0 !important; + } + + #artwork-column .music-video { + width: 88.888888%; + margin-left: auto; + } + + #artwork-column .cover-artwork + .music-video { + margin-top: 25px; + } + + #artwork-column .cover-artwork + .cover-artwork + .music-video { + margin-top: 30px; + } +} + +@layer material { + .cover-artwork, + .music-video, + .album-art-info { + border: 2px solid var(--primary-color); + background: var(--bg-black-color); + + -webkit-backdrop-filter: blur(3px); + backdrop-filter: blur(3px); + } + + .cover-artwork { + border-radius: 0 0 4px 4px; + } + + .cover-artwork:has(.image-details), + .cover-artwork.has-image-details { + border-radius: 0 0 6px 6px; + } + + .music-video { + border-radius: 4px; + padding: 0 4px; + } + + .album-art-info { + border-color: var(--deep-color); + border-radius: 5px; + + background: var(--bg-black-color); + padding: 6px; + } + + .album-art-info p { + margin: 0; + } + + .cover-artwork .image-container, + .music-video .image-container { + border: none; + border-radius: 0 !important; + } + + .music-video .image-container { + background: transparent; + border-style: dashed none; + border-width: 2px; + border-color: var(--dim-color); + } + + .cover-artwork:not(:has(.image-details)), + .cover-artwork:not(.has-image-details) { + /* Hacky: `overflow: hidden` hides tag tooltips, so it can't be applied + * if we've got tags/details visible. But it's okay, because we only + * need to apply it if it *doesn't* - that's when the rounded border + * of the .cover-artwork needs to cut off its child .image-container + * (which has a background that otherwise causes sharp corners). + */ + overflow: hidden; + } + + #artwork-column .cover-artwork, + #artwork-column .music-video { + --normal-shadow: 0 0 12px 12px #00000080; + + box-shadow: + 0 2px 14px -6px var(--primary-color), + var(--normal-shadow); + } + + #artwork-column .cover-artwork:not(:first-child) { + --normal-shadow: 0 0 9px 9px #00000068; + } + + .cover-artwork .image-details { + border-top-color: var(--deep-color); + } + + .cover-artwork .image-details + .image-details { + border-top-color: var(--primary-color); + } + + .image-details { + display: block; + margin-top: 0; + margin-bottom: 0; + } + + .image-details:not(.image-details + .image-details) { + margin-left: 0; + margin-right: 0; + padding-left: 9px; + padding-right: 9px; + + padding-top: 6px; + padding-bottom: 4px; + + border-top: 1px dashed var(--deep-color); + } + + .image-details + .image-details { + display: block; + + margin-left: 6px; + margin-right: 6px; + padding-left: 3px; + padding-right: 3px; + + padding-top: 4px; + padding-bottom: 4px; + + border-top: 1px dotted var(--primary-color); + } + + .image-details:last-child { + margin-bottom: 2px; + } +} + +@layer print { + .cover-artwork, + .music-video, + .album-art-info { + font-size: 0.8em; + } + + p.image-details.illustrator-details { + text-align: center; + font-style: oblique; + } + + p.image-details.origin-details { + margin-bottom: 2px; + } + + p.image-details.origin-details .origin-details-line { + display: block; + margin-top: 0.25em; + } + + p.image-details.origin-details .filename-line { + display: block; + margin-top: 0.25em; + } + + .music-video .music-video-label { + margin: 6px 12px 3px; + text-align: center; + } + + .music-video .music-video-label.title-style { + margin-bottom: 4px; + font-style: oblique; + } + + .music-video .image-container ~ p { + margin: 3px 5px; + } + + .music-video .image-container + p { + margin-top: 5px; + } + + .music-video .image-container ~ p:last-child { + margin-bottom: 6px; + } + + .music-video .artists-line { + display: block; + padding-left: 1.2ch; + text-indent: -1.2ch; + } + + .music-video .artists-line > * { + text-indent: 0; + } + + .music-video .artists-line + br { + display: none; + } +} + +@layer construction { + ul.image-details.art-tag-details { + padding-bottom: 0; + } + + ul.image-details.art-tag-details li { + display: inline-block; + } + + ul.image-details.art-tag-details li:not(:last-child)::after { + content: " \00b7 "; + } + + .cover-artwork-joiner::after { + content: ""; + display: block; + width: 0; + height: 15px; + margin-left: auto; + margin-right: auto; + border-right: 3px solid var(--primary-color); + } +} + +@layer interaction { + .music-video .image-link::after { + content: "▶︎"; + background: #0008; + color: white; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + display: flex; + justify-content: center; + align-items: center; + } + + .music-video .image-link:hover::after { + font-size: 1.4em; + background: #0006; + } + + .music-video .image-inner-area::after { + /* We're overriding normal image-inner-area stuff. */ + opacity: 0.8; + box-shadow: 0 0 4px inset black; + } + + .music-video .image-link:hover .image { + transform: scale(1.02); + } +} + +/* Dot switchers */ + +@layer print { + .dot-switcher > span { + color: #ffffffcc; + } + + .dot-switcher > span.current { + font-weight: normal; + color: #ffffffff; + } + + .dot-switcher > span.current a { + font-weight: 800; + } + + .group-view-switcher { + margin-left: 1ch; + } +} + +@layer construction { + .dot-switcher > span:not(:first-child) { + display: inline-block; + white-space: nowrap; + } + + .dot-switcher > span:not(:first-child)::before { + content: "\0020\00b7\0020"; + white-space: pre; + font-weight: 800; + color: white; + } + + /* Yeah, all this stuff only applies to elements of the dot switcher + * besides the first, which will necessarily have a bullet point at left. + */ + .dot-switcher *:where(.dot-switcher > span:not(:first-child) > *) { + display: inline-block; + white-space: wrap; + text-align: left; + vertical-align: top; + } +} + +@layer interaction { + .dot-switcher.intrapage > span:not(.current) a { + text-decoration: underline; + text-decoration-style: dotted; + } + + .dot-switcher.intrapage > span.current a { + /* Keeping cursor: pointer (the default) is intentional here. */ + text-decoration: none !important; + } +} + +/* External icons */ + +@layer print { + .external-icon { + display: inline-block; + padding: 0 3px; + width: 24px; + height: 1em; + position: relative; + } + + .external-icon svg { + width: 24px; + height: 24px; + top: -0.25em; + position: absolute; + fill: var(--primary-color); + } +} + +/* Gallery switchers (of various sorts) */ + +@layer layout { + .gallery-set-switcher { + text-align: center; + } + + .gallery-view-switcher, + .gallery-style-selector { + margin-left: auto; + margin-right: auto; + text-align: center; + line-height: 1.4; + } + + .gallery-style-selector .styles { + display: inline-flex; + justify-content: center; + } + + .gallery-style-selector .styles label:not(:last-child) { + margin-right: 1.25ch; + } + + .gallery-style-selector .count { + position: relative; + bottom: -0.25em; + } +} + +@layer print { + .gallery-view-switcher, + .gallery-style-selector { + line-height: 1.4; + } + + .gallery-style-selector .count { + font-size: 0.85em; + opacity: 0.9; + } +} + +/* Grid listings and grid items */ + +@layer layout { + .grid-listing { + display: flex; + flex-wrap: wrap; + justify-content: center; + align-items: flex-start; + padding: 5px 15px; + box-sizing: border-box; + margin-top: 1em; + } + + .grid-listing .reveal-all-container { + flex-basis: 100%; + } + + .grid-listing .reveal-all-container.has-nearby-tab { + margin-bottom: 0.6em; + } + + .grid-listing .reveal-all { + max-width: 400px; + margin: 0.20em auto 0; + text-align: center; + } + + .grid-listing .reveal-all a { + display: inline-block; + margin-bottom: 0.15em; + } + + .grid-listing .reveal-all b { + white-space: nowrap; + } + + .grid-item { + --tab-lift: 0px; + --tabnt-offset: 0px; + + margin: 10px; + margin-top: + calc( + 10px + - var(--tab-lift) + + var(--tabnt-offset)); + + padding: 5px; + } + + .grid-item.has-tab { + border-radius: 8px 8px 3px 3px; + } + + .grid-item:not(.has-tab) { + --tabnt-offset: calc(1.2em - 4px); + } + + .grid-item .image-container { + width: 100%; + } + + .grid-item .image { + width: 100%; + height: 100% !important; + margin-top: auto; + margin-bottom: auto; + object-fit: contain; + } + + .grid-listing > .grid-item { + flex: 1 25%; + max-width: 200px; + } + + .grid-actions { + margin: 15px; + align-self: center; + + display: flex; + flex-direction: row; + flex-wrap: wrap; + justify-content: center; + } + + .grid-actions > .grid-item { + flex-basis: unset !important; + margin: 5px; + width: 120px; + --primary-color: inherit !important; + --dim-color: inherit !important; + } + + .grid-listing > .grid-caption, + .grid-listing > .grid-expando { + flex-basis: 100%; + } + + .grid-expando { + margin-top: 1em; + margin-bottom: 2em; + + display: flex; + flex-direction: row; + justify-content: space-around; + } + + .grid-expando-content { + margin: 0; + } +} + +@layer material { + .grid-listing:not(:has(.grid-item:not([class*="hidden-by-"]))) { + padding-bottom: 140px; + background: #cccccc07; + border-radius: 10px; + border: 1px dashed #fff3; + } + + .grid-listing:not(:has(.grid-item:not([class*="hidden-by-"]))) .reveal-all-container { + display: none; + } + + .grid-item { + background-color: #111111; + border: 1px dotted var(--primary-color); + border-radius: 2px; + } + + .grid-item .image-inner-area { + border-radius: 0; + box-shadow: none; + } + + .grid-item .image-inner-area::after { + box-shadow: none; + } + + .grid-item .image { + --shadow-filter: + drop-shadow(0 3px 2px #0004) + drop-shadow(0 1px 5px #0001) + drop-shadow(0 3px 4px #0001); + } +} + +@layer print { + .grid-item { + text-align: center; + line-height: 1.2; + font-size: 0.9em; + } + + .grid-item > span { + display: block; + overflow-wrap: break-word; + hyphens: auto; + } + + /* tab */ + .grid-item > span:first-child { + margin-bottom: calc(3px + var(--tab-lift)); + + font-style: oblique; + } + + /* title */ + .grid-item > .image-container + span { + margin-top: 6px; + } + + .grid-name-marker { + color: white; + } + + /* info */ + .grid-item > .image-container + span ~ span { + margin-top: 2px; + + font-size: 0.9em; + opacity: 0.8; + } + + .grid-caption { + text-align: center; + line-height: 1.5; + } + + .grid-expando-content { + text-align: center; + line-height: 1.5; + } +} + +@layer interaction { + .grid-listing .reveal-all a { + text-decoration: underline; + text-decoration-style: dotted; + } + + .grid-listing .reveal-all .warnings { + opacity: 0.4; + } + + .grid-listing .reveal-all:hover .warnings { + opacity: 1; + } + + .grid-item.has-tab:hover { + --tab-lift: 3px; + } + + .grid-item[class*="hidden-by-"] { + display: none; + } + + .grid-item:hover { + text-decoration: none; + } + + .grid-actions .grid-item:hover { + text-decoration: underline; + } + + /* title */ + .grid-item:hover > .image-container + span { + text-decoration: underline; + } + + .grid-expando-toggle { + text-decoration: underline; + text-decoration-style: dotted; + } + + .grid-item.shown-by-expandable-cut { + animation: expand-cover-grid 0.8s forwards; + } + + @keyframes expand-cover-grid { + from { + opacity: 0; + } + + to { + opacity: 1; + } + } +} + +/* Group-contributions tables */ + +@layer layout { + .group-contributions-table { + display: inline-block; + } + + .group-contributions-table .group-contributions-row { + display: flex; + justify-content: space-between; + } + + .group-contributions-table .group-contributions-metrics { + margin-left: 1.5ch; + white-space: nowrap; + } +} + +@layer interaction { + .group-contributions-sorted-by-count:not(.visible), + .group-contributions-sorted-by-duration:not(.visible) { + display: none; + } + + .group-contributions-sort-button { + text-decoration: underline; + text-decoration-style: dotted; + } +} + +/* Image and media containers */ + +@layer layout { + .image-container { + display: block; + box-sizing: border-box; + position: relative; + height: 100%; + overflow: hidden; + } + + .content-image-container, + .content-video-container, + .content-audio-container { + margin-top: 1em; + margin-bottom: 1em; + } + + .content-video-container, + .content-audio-container { + width: fit-content; + max-width: 100%; + } + + .content-video-container video, + .content-audio-container audio { + display: block; + max-width: 100%; + } + + .image-text-area { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + + display: flex; + align-items: center; + justify-content: center; + text-align: center; + padding: 5px 15px; + } + + .image-outer-area { + width: 100%; + height: 100%; + padding: 5px; + box-sizing: border-box; + } + + .image-link { + display: block; + overflow: hidden; + } + + .square .image-link { + width: 100%; + height: 100%; + } + + .image-inner-area { + position: relative; + width: 100%; + height: 100%; + } + + .image-link .image-inner-area { + /* Jankily fix a rendering issue with border-radius on Safari. + * The `-webkit-` prefix is only to keep this from applying on + * other browsers (well, Firefox), where it doesn't *break* + * anything, but also isn't necessary. + */ + -webkit-transform: translateZ(0); + } + + img { + object-fit: cover; + } + + .square .image { + width: 100%; + height: 100%; + } + + .image-link .image { + display: block; + max-width: 100%; + height: auto; + } + + .reveal-text-container { + position: absolute; + top: 15px; + left: 10px; + right: 10px; + bottom: 10px; + display: flex; + flex-direction: column; + justify-content: center; + } + + .reveal-text { + padding-bottom: 0.5em; + } + + .reveal .image.reveal-thumbnail { + position: absolute; + top: 0; + left: 0; + } + + .reveal.has-reveal-thumbnail:not(.revealed) .image:not(.reveal-thumbnail) { + /* Keep the main image as part of the box model. + * It's what actually defines the dimensions of the + * image-container, so those dimensions never shift + * once the image is actually revealed. + */ + visibility: hidden; + } +} + +@layer material { + .image-container { + background-color: var(--dim-color); + border: 2px solid var(--primary-color); + border-radius: 0; + box-shadow: 0 2px 4px -2px var(--bg-black-color) inset; + } + + .content-video-container, + .content-audio-container { + background-color: var(--dark-color); + border: 2px solid var(--primary-color); + border-radius: 2.5px 2.5px 3px 3px; + padding: 5px; + } + + .image-text-area { + background: rgba(0, 0, 0, 0.65); + box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) inset; + + line-height: 1.35em; + color: var(--primary-color); + font-style: oblique; + text-shadow: 0 2px 5px rgba(0, 0, 0, 0.75); + } + + .image-link { + border-bottom: 1px solid #ffffff03; + border-radius: 2.5px 2.5px 3px 3px; + box-shadow: + 0 1px 8px -3px var(--bg-black-color); + } + + .image-inner-area::after { + content: ""; + display: block; + position: absolute; + pointer-events: none; + top: 0; + left: 0; + right: 0; + bottom: 0; + border-bottom-left-radius: 0.5px; + opacity: 0.035; + box-shadow: + 6px -6px 2px -4px white inset; + } + + .image { + --reveal-filter: ; + --shadow-filter: ; + + backdrop-filter: blur(0); + filter: + var(--reveal-filter) + var(--shadow-filter); + } + + .reveal .image.reveal-thumbnail { + image-rendering: pixelated; + } +} + +@layer print { + .image-container { + text-align: left; + color: white; + } + + .content-image-container.align-center { + text-align: center; + margin-top: 1.5em; + margin-bottom: 1.5em; + } + + .content-video-container.align-center, + .content-audio-container.align-center { + margin-left: auto; + margin-right: auto; + } + + .content-image-container.align-full, + .content-video-container.align-full, + .content-audio-container.align-full { + width: 100%; + } + + .content-audio-container .filename { + color: white; + font-family: monospace; + display: block; + font-size: 0.9em; + padding-left: 1ch; + padding-right: 1ch; + padding-bottom: 0.25em; + margin-bottom: 0.5em; + border-bottom: 1px solid #fff4; + } + + .reveal-text { + color: white; + text-align: center; + font-weight: bold; + font-size: 0.8rem; + } + + .grid-item .reveal-text { + font-size: 0.9em; + } + + .reveal-symbol { + display: inline-block; + width: 1em; + height: 1em; + margin-bottom: 0.1em; + + font-size: 1.6em; + opacity: 0.8; + } + + .sidebar .image-container { + max-width: 350px; + } +} + +@layer interaction { + .image-link:focus { + outline: 3px double white; + } + + .image-link:focus:not(:focus-visible) { + outline: none; + } + + .image-container.has-link:not(.no-image-preview) { + background: var(--deep-color); + box-shadow: none; + border-radius: 0 0 4px 4px; + } + + .reveal-interaction { + opacity: 0.8; + text-decoration: underline; + text-decoration-style: dotted; + } + + .reveal:not(.revealed) .image { + opacity: 0.7; + --reveal-filter: blur(20px) brightness(0.7); + } + + .reveal:not(.revealed) .image-outer-area > * { + position: relative; + border-radius: 6px; + } + + .reveal:not(.revealed) .image-outer-area > *::after { + content: ""; + position: absolute; + box-sizing: border-box; + top: 0; + left: 0; + bottom: 0; + right: 0; + border: 1px dotted var(--primary-color); + border-radius: 6px; + pointer-events: none; + + /* By an awkward DOM intersection, this element might be + * .image-inner-area::after, which is already styled with + * a slight visual effect. Guarantee that the properties + * set to that end are overwritten, and fully co-opt it + * to serve as the interaction cue instead. + */ + box-shadow: none; + opacity: 1; + } + + .reveal:not(.revealed) .image-inner-area { + background: var(--deep-color); + } + + .reveal:not(.revealed) .image-outer-area > *:hover::after { + border-style: solid; + box-shadow: 0 0 0 1.5px #00000099 inset; + } + + .reveal:not(.revealed) .image-outer-area > *:hover .image { + --reveal-filter: blur(20px) brightness(0.6); + opacity: 0.6; + } + + .reveal:not(.revealed) .image-outer-area > *:hover .reveal-interaction { + text-decoration-style: solid; + } + + .reveal.revealed.has-reveal-thumbnail .image.reveal-thumbnail { + display: none !important; + } + + .reveal.revealed .image { + opacity: 1; + } + + .reveal.revealed .reveal-text-container { + display: none; + } +} + +/* Image overlay */ + +@layer layout { + #image-overlay-container { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 4000; + + padding: 20px 40px; + box-sizing: border-box; + + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + } + + #image-overlay-content-container { + display: flex; + flex-direction: column; + + overflow: hidden; + } + + #image-overlay-image-area { + display: block; + overflow: hidden; + width: 80vmin; + margin-left: auto; + margin-right: auto; + } + + #image-overlay-image-layout { + display: block; + position: relative; + max-height: 100%; + margin: 4px 3px; + } + + #image-overlay-image, + #image-overlay-image-thumb { + display: block; + width: 100%; + height: auto; + } + + #image-overlay-image { + position: absolute; + max-height: 100%; + object-fit: contain; + } + + #image-overlay-container.no-thumb #image-overlay-image { + position: static; + } + + #image-overlay-action-container { + padding: 7px 4px 7px 4px; + text-align: center; + } +} + +@layer material { + #image-overlay-container { + background: rgba(0, 0, 0, 0.8); + color: white; + } + + #image-overlay-container::before { + content: ''; + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + + background: var(--deep-color); + opacity: 0.20; + } + + #image-overlay-content-container { + border-radius: 0 0 8px 8px; + border: 2px solid var(--primary-color); + background: var(--deep-ghost-color); + + box-shadow: + 0 0 90px 30px #00000060, + 0 0 20px 10px #00000040, + 0 0 10px 3px #00000080; + + -webkit-backdrop-filter: blur(3px); + backdrop-filter: blur(3px); + } + + #image-overlay-image-layout { + background: rgba(0, 0, 0, 0.65); + } + + #image-overlay-image-thumb { + filter: blur(16px); + transform: scale(1.5); + } + + #image-overlay-action-container { + border-radius: 0 0 5px 5px; + background: var(--bg-black-color); + box-shadow: + 0 3px 8px -5px var(--primary-color) inset; + } +} + +@layer print { + #image-overlay-action-container { + color: white; + font-style: oblique; + text-align: center; + } + + #image-overlay-file-size-warning { + opacity: 0.8; + font-size: 0.9em; + } +} + +@layer construction { + #image-overlay-image-area::after { + content: ""; + display: block; + position: absolute; + bottom: 0; + left: 0; + height: 4px; + width: var(--download-progress); + background: var(--primary-color); + box-shadow: 0 -3px 12px 4px var(--primary-color); + transition: 0.25s; + } + + #image-overlay-container.loaded #image-overlay-image-area::after { + width: 100%; + background: white; + opacity: 0; + } + + #image-overlay-container.errored #image-overlay-image-area::after { + width: 100%; + background: red; + } + + #image-overlay-container:not(.visible) #image-overlay-image-area::after { + width: 0 !important; + } + + #image-overlay-container #image-overlay-action-content-without-size:not(.visible), + #image-overlay-container #image-overlay-action-content-with-size:not(.visible), + #image-overlay-container #image-overlay-file-size-warning:not(.visible), + #image-overlay-container #image-overlay-file-size-kilobytes:not(.visible), + #image-overlay-container #image-overlay-file-size-megabytes:not(.visible) { + display: none; + } +} + +@layer interaction { + #image-overlay-container { + opacity: 0; + pointer-events: none; + transition: opacity 0.4s; + } + + #image-overlay-container.visible { + opacity: 1; + pointer-events: auto; + } + + #image-overlay-container.loaded #image-overlay-image-thumb { + opacity: 0; + pointer-events: none; + transition: opacity 0.25s; + } +} + +/* Info card */ + +@layer interactivity { + #info-card-container { + pointer-events: none; /* Padding area shouldn't 8e interactive. */ + } + + .info-card { + pointer-events: none; + } + + #info-card-container.show .info-card { + animation: 0.01s linear 0.2s forwards info-card-become-interactive; + } + + @keyframes info-card-become-interactive { + to { + pointer-events: auto; + } + } +} + +@layer layout { + #info-card-container { + position: absolute; + left: 0; + right: 10px; + + display: none; + } + + #info-card-container.show, + #info-card-container.hide { + display: flex; + } + + #info-card-container > * { + flex-basis: 400px; + } + + .info-card { + padding: 5px; + } + + .info-card-art-container { + float: right; + width: 40%; + margin: 5px; + + /* Dynamically shown. */ + display: none; + } + + .info-card-art-container .image-container { + padding: 2px; + } + + .info-card-art { + display: block; + width: 100%; + height: 100%; + } + + .info-card::after { + content: ""; + display: block; + clear: both; + } +} + +@layer material { + .info-card-decor { + padding-left: 3ch; + border-top: 1px solid white; + } + + .info-card { + background-color: black; + color: white; + + border: 1px dotted var(--primary-color); + border-radius: 3px; + box-shadow: 0 5px 5px black; + } + + .info-card-name { + font-size: 1em; + border-bottom: 1px dotted; + margin: 0; + } + + .info-card p { + margin-top: 0.25em; + margin-bottom: 0.25em; + } + + .info-card p:last-child { + margin-bottom: 0; + } +} + +@layer print { + .info-card { + font-size: 0.9em; + } + + .info-card-art-container { + font-size: 0.8em; + } +} + +@layer interaction { + #info-card-container.show { + animation: 0.2s linear forwards info-card-show; + transition: top 0.1s, left 0.1s; + } + + #info-card-container.hide { + animation: 0.2s linear forwards info-card-hide; + } + + @keyframes info-card-show { + 0% { + opacity: 0; + margin-top: -5px; + } + + 100% { + opacity: 1; + margin-top: 0; + } + } + + @keyframes info-card-hide { + 0% { + opacity: 1; + margin-top: 0; + } + + 100% { + opacity: 0; + margin-top: 5px; + display: none !important; + } + } +} + +/* Lyrics */ + +@layer layout { + .lyrics-switcher { + padding-left: 20px; + } +} + +@layer print { + .lyrics-entry .lyrics-details, + .lyrics-entry .origin-details { + font-size: 0.9em; + font-style: oblique; + } + + .lyrics-entry .lyrics-details { + margin-bottom: 0; + } + + .lyrics-entry .origin-details { + margin-top: 0.25em; + } + + .lyrics-entry sup { + vertical-align: text-top; + opacity: 0.8; + cursor: default; + } +} + +@layer interaction { + .lyrics-entry.long-lyrics { + clip-path: inset(-15px -20px); + } + + .lyrics-entry.long-lyrics::after { + content: ""; + pointer-events: none; + display: block; + + /* Slight stretching past the bottom of the screen seems + * to make resizing the window (and "revealing" that area) + * a bit smoother. + */ + position: fixed; + bottom: -20px; + left: 0; + right: 0; + + height: calc(20px + min(90px, 13.5vh)); + background: linear-gradient(to bottom, transparent, black 70%, black); + opacity: 0.6; + } +} + +/* Quick description */ + +@layer layout { + .quick-description:not(.has-external-links-only) { + --clamped-padding-ratio: max(var(--responsive-padding-ratio), 0.06); + margin-left: auto; + margin-right: auto; + padding-left: calc(0.40 * var(--clamped-padding-ratio) * 100%); + padding-right: calc(0.40 * var(--clamped-padding-ratio) * 100%); + padding-top: 0.25em; + padding-bottom: 0.75em; + max-width: 500px; + } + + .quick-description.has-external-links-only { + padding-left: 12%; + padding-right: 12%; + } + + .quick-description.has-content-only { + padding-bottom: 0.5em; + } +} + +@layer material { + .quick-description:not(.has-external-links-only) { + border-left: 1px solid var(--dim-color); + border-right: 1px solid var(--dim-color); + } +} + +@layer print { + .quick-description:not(.has-external-links-only) { + line-height: 1.25em; + } + + .quick-description p { + text-align: center; + } + + .quick-description > blockquote { + margin-left: 0 !important; + margin-right: 0 !important; + } + + .quick-description .description-content.long hr ~ p { + text-align: left; + } + + .quick-description .description-content :first-child { + margin-top: 0; + } + + .quick-description .quick-description-actions, + .quick-description.has-content-only .description-content :last-child { + margin-bottom: 0; + } +} + +@layer interaction { + .quick-description:not(.collapsed) .description-content.short, + .quick-description:not(.collapsed) .quick-description-actions.when-collapsed, + .quick-description:not(.expanded) .description-content.long, + .quick-description:not(.expanded) .quick-description-actions.when-expanded { + display: none; + } + + .quick-description .quick-description-actions .expand-link, + .quick-description .quick-description-actions .collapse-link { + text-decoration: underline; + text-decoration-style: dotted; + } +} + +/* Quick info */ + +@layer layout { + .quick-info { + padding-left: calc(var(--responsive-padding-ratio) * 100%); + padding-right: calc(var(--responsive-padding-ratio) * 100%); + } +} + +@layer print { + .quick-info { + text-align: center; + line-height: 1.25em; + } +} + +@layer construction { + ul.quick-info { + list-style: none; + padding-left: 0; + padding-right: 0; + } + + ul.quick-info li { + display: inline-block; + } + + ul.quick-info li:not(:last-child)::after { + content: " \00b7 "; + font-weight: 800; + } +} + +/* Sticky heading */ + +@layer layout { + :where([id]) { + --custom-scroll-offset: 0px; + } + + :where(#content [id]) { + /* Adjust scroll margin. */ + scroll-margin-top: calc( + 74px /* Sticky heading */ + + 33px /* Sticky subheading */ + - 1em /* One line of text (align bottom) */ + - 12px /* Padding for hanging letters & focus ring */ + + var(--custom-scroll-offset) /* Customizable offset */ + ) !important; + } + + .content-sticky-heading-root { + width: calc(100% + 2 * var(--content-padding)); + margin: calc(-1 * var(--content-padding)); + margin-bottom: 0; + } + + .content-sticky-heading-anchor, + .content-sticky-heading-container { + width: 100%; + } + + .content-sticky-heading-root:not([inert]) { + position: sticky; + top: 0; + } + + .content-sticky-heading-anchor:not(:where(.content-sticky-heading-root[inert]) *) { + position: relative; + } + + .content-sticky-heading-container:not(:where(.content-sticky-heading-root[inert]) *) { + position: absolute; + } + + .content-sticky-heading-root[inert] { + visibility: hidden; + } + + main.long-content .content-sticky-heading-container { + padding-left: 0; + padding-right: 0; + } + + main.long-content .content-sticky-heading-container .content-sticky-heading-row, + main.long-content .content-sticky-heading-container .content-sticky-subheading-row { + padding-left: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding)); + padding-right: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding)); + } + + .content-sticky-heading-row { + box-sizing: border-box; + padding: + calc(1.25 * var(--content-padding) + 5px) + 20px + calc(0.75 * var(--content-padding)) + 20px; + + width: 100%; + margin: 0; + } + + .content-sticky-heading-container.has-cover .content-sticky-heading-row, + .content-sticky-heading-container.has-cover .content-sticky-subheading-row { + display: grid; + grid-template-areas: + "title cover"; + grid-template-columns: 1fr min(40%, 400px); + } + + .content-sticky-heading-container.cover-visible .content-sticky-heading-row { + grid-template-columns: 1fr min(40%, 90px); + } + + .content-sticky-heading-row h1 { + position: relative; + margin: 0; + padding-right: 20px; + overflow-x: hidden; + } + + .content-sticky-heading-row h1 .reference-collapsed-heading { + position: absolute; + white-space: nowrap; + visibility: hidden; + } + + .content-sticky-heading-cover-container { + position: relative; + height: 0; + margin: -15px 0px -5px -5px; + } + + .content-sticky-heading-cover { + position: absolute; + top: 0; + width: 80px; + right: 10px; + } + + .content-sticky-heading-container .image-outer-area { + padding: 3px; + } + + .content-sticky-heading-cover .image { + display: block; + width: 100%; + height: 100%; + } + + .content-sticky-subheading-row { + position: absolute; + width: 100%; + box-sizing: border-box; + padding: 10px 20px 5px 20px; + margin-top: 0; + z-index: -1; + } + + .content-sticky-subheading-row h2 { + margin: 0; + } + + .content-sticky-subheading { + padding-right: 20px; + } +} + +@layer material { + .content-sticky-heading-row { + background: var(--bg-black-color); + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); + + box-shadow: + inset 0 10px 10px -5px var(--shadow-color), + 0 4px 8px -4px #000000b0; + + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); + } + + .content-sticky-heading-cover { + box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.25); + } + + .content-sticky-heading-cover .cover-artwork { + border-width: 1px; + border-radius: 1.25px; + box-shadow: none; + } + + .content-sticky-heading-container .image-inner-area { + border-radius: 1.75px; + + /* Editor's note: I don't know what this overflow: hidden is for. */ + overflow: hidden; + } + + .content-sticky-subheading-row { + background: var(--bg-black-color); + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); + + box-shadow: + 0 2px 2px -1px #00000060, + 0 4px 12px -4px #00000090; + + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + } +} + +@layer print { + .content-sticky-subheading-row h2 { + font-size: 0.9em !important; + font-weight: normal; + font-style: oblique; + color: #eee; + } +} + +@layer interaction { + .content-sticky-heading-container.collapse h1 { + white-space: nowrap; + overflow-wrap: normal; + + animation: collapse-sticky-heading 0.35s forwards; + text-overflow: ellipsis; + overflow-x: hidden; + } + + @keyframes collapse-sticky-heading { + from { + height: var(--uncollapsed-heading-height); + } + + 99.9% { + height: var(--collapsed-heading-height); + } + + to { + height: auto; + } + } + + .content-sticky-heading-container h1 a { + transition: text-decoration-color 0.35s; + } + + .content-sticky-heading-container h1 a:not([href]) { + color: inherit; + cursor: text; + text-decoration: underline; + text-decoration-style: dotted; + text-decoration-color: transparent; + } + + .content-sticky-heading-cover-needs-reveal { + display: none; + } + + .content-sticky-heading-cover { + transition: transform 0.35s, opacity 0.25s; + } + + .content-sticky-heading-cover-container:not(.visible) .content-sticky-heading-cover { + opacity: 0; + transform: translateY(15px); + transition: transform 0.35s, opacity 0.30s; + } + + .content-sticky-subheading-row { + transition: margin-top 0.35s, opacity 0.25s; + } + + .content-sticky-subheading-row:not(.visible) { + margin-top: -20px; + opacity: 0; + } +} + +/* Sticky sidebar */ + +@layer layout { + .sidebar-column:not(.sticky-column) { + align-self: stretch; + } + + .sidebar-column.sticky-column { + align-self: flex-start; + + position: sticky; + top: 10px; + max-height: calc(100vh - 20px); + + display: flex; + flex-direction: column; + } + + .sidebar-multiple.sticky-column .sidebar:last-child { + flex-shrink: 1; + overflow-y: scroll; + } + + .wiki-search-sidebar-box .wiki-search-results-container { + overflow-y: scroll; + } + + .sidebar-column.sidebar.sticky-column > h1 { + position: sticky; + top: 0; + z-index: 2; + + margin: 0 calc(-1 * var(--content-padding)); + margin-bottom: 10px; + + padding: 10px 5px; + } +} + +@layer material { + .sidebar-multiple.sticky-column .sidebar:last-child { + scrollbar-width: thin; + scrollbar-color: var(--dim-color) var(--dark-color); + } + + .wiki-search-sidebar-box .wiki-search-results-container { + overflow-y: scroll; + scrollbar-width: thin; + scrollbar-color: var(--dim-color) var(--dark-color); + } + + .sidebar-column.sticky-column .sidebar:last-child::-webkit-scrollbar, + .wiki-search-sidebar-box .wiki-search-results-container::-webkit-scrollbar { + background: var(--dark-color); + width: 12px; + } + + .sidebar-column.sticky-column .sidebar:last-child::-webkit-scrollbar-thumb, + .wiki-search-sidebar-box .wiki-search-results-container::-webkit-scrollbar-thumb { + transition: background 0.2s; + background: rgba(255, 255, 255, 0.2); + border: 3px solid transparent; + border-radius: 10px; + background-clip: content-box; + } + + .sidebar-column.sidebar.sticky-column > h1 { + border-bottom: 1px dotted rgba(220, 220, 220, 0.4); + + background: var(--bg-black-color); + + -webkit-backdrop-filter: blur(4px); + backdrop-filter: blur(4px); + + box-shadow: + 0 2px 3px -1px #0006, + 0 4px 8px -2px #0009; + } +} diff --git a/src/static/css/miscellany.css b/src/static/css/miscellany.css new file mode 100644 index 00000000..a9c24732 --- /dev/null +++ b/src/static/css/miscellany.css @@ -0,0 +1,600 @@ +/* Squares */ + +@layer layout { + .square { + position: relative; + width: 100%; + } + + .square::after { + content: ""; + display: block; + padding-bottom: 100%; + } + + .square-content { + position: absolute; + width: 100%; + height: 100%; + } +} + +/* Utility spans */ + +@layer print { + .nowrap { + white-space: nowrap; + } + + .blockwrap, .chunkwrap { + display: inline-block; + } + + p .current { + font-weight: 800; + } + + .js-hide, + .js-show-once-data, + .js-hide-once-data { + display: none; + } +} + +/* Totally miscellaneous typography */ + +@layer layout { + dl dt { + padding-left: 40px; + } + + dl dt { + /* Heads up, this affects the measurement + * for dl dt which are .content-heading! + */ + margin-bottom: 2px; + } + + dl dt[id]:not(.content-heading) { + --custom-scroll-offset: calc(2.5em - 2px); + } + + dl dd { + margin-bottom: 1em; + } + + dl ul, + dl ol { + margin-top: 0; + margin-bottom: 0; + } + + li .origin-details { + display: block; + margin-left: 2ch; + } + + li > ul { + margin-top: 5px; + } +} + +@layer print { + center { + margin-top: 1em; + margin-bottom: 1em; + } + + .flash-act-title { + display: inline-block; + text-decoration: inherit; + } + + a .flash-act-title { + color: var(--primary-color); + } + + .other-group-accent, + .rerelease-line { + opacity: 0.7; + font-style: oblique; + } + + .other-group-accent { + white-space: nowrap; + } + + .other-group-accent a { + color: var(--page-primary-color); + } + + dt .by, li .by { + font-style: oblique; + } + + dt .by a, li .by a { + display: inline-block; + } + + p code { + font-size: 0.95em; + font-family: "courier new", monospace; + font-weight: 800; + line-height: 1.1; + } + + /* "has-details" means a "has a <details> element" here. */ + ul > li.has-details { + list-style-type: none; + margin-left: -17px; + } + + li .origin-details { + font-size: 0.9em; + font-style: oblique; + } +} + +/* Ordinary links */ + +@layer print { + a { + color: var(--primary-color); + text-decoration: none; + } + + a.current { + font-weight: 800; + } + + a.series { + font-style: oblique; + } + + a:not([href]) { + cursor: default; + } + + a:not([href]):hover { + text-decoration: none; + } +} + +@Layer interaction { + /* This is an !important rule because it's really applicable + * pretty much anywhere, but the selector specificity is lower + * than most selectors which, say, add a dotted underline to + * begin with - those would take cascading priority. + * + * Similarly, this rule has to live in the interaction layer, + * because that's where other underline hover cues are set. + * If we set set it any higher (e.g. in print) the !important + * takes universal priority over !important's set in deeper + * layers - like interaction. + */ + a:hover { + text-decoration: underline; + text-decoration-style: solid !important; + } +} + +/* Links with symbols */ + +@layer print { + a .normal-content { + color: white; + } + + .external-link:not(.from-content) { + white-space: nowrap; + } + + .external-link.indicate-external::after { + content: '\00a0➚'; + font-style: normal; + } + + .external-link.indicate-external:hover::after { + color: white; + } + + .image-media-link::after { + /* Thanks to Jay Freestone for being awesome: + * https://www.jayfreestone.com/writing/wrapping-and-inline-pseudo-elements/ + */ + + pointer-events: none; + content: '\200b'; + padding-left: 22px; + + background-color: var(--primary-color); + + /* mask-image is set in content JavaScript, + * because we can't identify the correct nor + * absolute path to the file from CSS. + */ + + mask-repeat: no-repeat; + mask-position: calc(100% - 2px); + } + + .image-media-link:hover::after { + background-color: white; + } +} + +/* Label elements */ + +@layer interaction { + label:hover span { + text-decoration: underline; + text-decoration-style: solid; + } + + label > input[type=checkbox]:not(:checked) + span { + opacity: 0.8; + } +} + +/* Progress elements */ + +@layer material { + progress { + accent-color: var(--primary-color); + } +} + +/* Code blocks */ + +@layer print { + pre.content-code { + position: relative; + white-space: nowrap; + + max-width: calc(100vw - 180px); + + /* Welcome to heck. */ + font-family: inherit; + + border: 1px dashed var(--primary-color); + } + + pre.content-code span { + display: block; + overflow-x: scroll; + padding: 5px 20px 5px 5px; + background: black; + color: white; + } + + pre.content-code::before { + content: ""; + display: block; + position: absolute; + top: 0; + right: 0; + width: 100%; + height: 100%; + box-shadow: -15px 0 24px -8px black inset; + pointer-events: none; + } + + pre.content-code code { + font-family: "courier new", monospace; + font-weight: 800; + font-size: 0.8em; + } +} + +/* HR elements, of various flavors */ + +@layer print { + #content hr { + border: 1px inset #808080; + } + + #content hr.split { + color: #808080; + } + + #content hr.split::before { + content: "(split)"; + } + + #content hr.main-separator { + color: var(--dim-color); + clear: none; + margin-top: -0.25em; + margin-bottom: 1.75em; + } + + #content hr.main-separator::before { + content: "♦"; + font-size: 1.2em; + } + + #content hr.split, + #content hr.main-separator { + position: relative; + overflow: hidden; + border: none; + } + + #content hr.split::after, + #content hr.main-separator::after { + display: inline-block; + content: ""; + width: calc(100% - min(calc(8vw - 35px), 45px)); + position: absolute; + top: 50%; + margin-left: 10px; + } + + #content hr.split::after { + border: 1px inset currentColor; + margin-top: -2px; + } + + #content hr.main-separator::after { + border-bottom: 1px solid currentColor; + } + + hr.cute, + #content hr.cute, + .sidebar hr.cute { + border-color: var(--primary-color); + border-style: none none dotted none; + } +} + +/* Images and media embedded right into content, effects */ + +@layer layout { + a.align-center, img.align-center, + audio.align-center, video.align-center { + display: block; + margin-left: auto; + margin-right: auto; + } + + a.align-full, a.align-full img, + img.align-full, video.align-full { + width: 100%; + } + + .content-image { + display: inline-block !important; + } +} + +@layer material { + img.pixelate, .pixelate img, + video.pixelate, .pixelate video { + image-rendering: crisp-edges; + } +} + +@layer print { + p > img, li > img { + max-width: 100%; + object-fit: contain; + height: auto; + vertical-align: text-bottom; + } +} + +/* Paths and filenames */ + +@layer print { + span.path, code.filename { + font-size: 0.95em; + font-family: "courier new", monospace; + font-weight: 800; + background: #ccc3; + + padding: 0.05em 0.5ch; + border: 1px solid #ccce; + border-radius: 2px; + line-height: 1.4; + } + + blockquote :is(span.path, code.filename) { + font-size: 0.9em; + } + + .image-details code.filename { + margin-left: -0.4ch; + opacity: 0.8; + } + + .image-details code.filename:hover { + opacity: 1; + cursor: text; + } + + span.path i { + display: inline-block; + font-style: normal; + } + + span.path i::before { + content: "\0020/\0020"; + color: #ccc; + } +} + +/* Spoiler elements */ + +@layer print { + s.spoiler { + display: inline-block; + color: transparent; + text-decoration: underline; + text-decoration-color: white; + text-decoration-style: dashed; + text-decoration-skip: none; + text-decoration-skip-ink: none; + } + + s.spoiler::selection { + color: black; + background: white; + } + + s.spoiler::-moz-selection { + color: black; + background: white; + } +} + +/* Summary elements */ + +@layer print { + summary > span b { + font-weight: normal; + color: var(--primary-color); + } + + summary > span:hover { + cursor: pointer; + text-decoration: underline; + text-decoration-color: var(--primary-color); + } + + summary > span:hover a { + text-decoration: none !important; + } + + summary > span:hover:has(a:hover), + summary > span:hover:has(a.nested-hover), + summary.has-nested-hover > span { + text-decoration: none !important; + } + + summary > span:hover:has(a:hover) a, + summary > span:hover:has(a.nested-hover) a, + summary.has-nested-hover > span a { + text-decoration: underline !important; + } + + summary.underline-white > span:hover { + text-decoration-color: white; + } + + /* This link isn't supposed to be underlined *at all* + * when the summary (and not link) is hovered, but + * for some reason Safari is still applying its colored + * and dotted(!) underline. Get around the apparent + * effect by just making it white. + */ + summary.underline-white > span:hover a:not(:hover) { + text-decoration-color: white; + } +} + +/* "Drops" */ + +@layer layout { + .drop { + padding: 15px 20px; + width: max-content; + max-width: min(60vw, 600px); + } + + .commentary-drop { + margin-top: 25px; + margin-bottom: 15px; + margin-left: 20px; + padding: 10px 20px; + max-width: min(60vw, 300px); + } +} + +@layer material { + .drop { + border: 1px dotted var(--primary-color); + border-radius: 6px; + + background: + linear-gradient(var(--bg-color), var(--bg-color)), + linear-gradient(#000000bb, #000000bb), + var(--primary-color); + + --drop-shadow: 0 -2px 6px -1px var(--dim-color) inset; + box-shadow: var(--drop-shadow); + } +} + +@layer interaction { + .drop.shiny { + cursor: default; + } + + @supports (box-shadow: 1px 1px 1px color-mix(in srgb, blue, 40% red)) { + @property --drop-shine { + syntax: '<percentage>'; + initial-value: 0%; + inherits: false; + } + + .drop.shiny { + cursor: default; + transition: --drop-shine 0.2s; + } + + .drop.shiny:hover { + --drop-shine: 100%; + + box-shadow: + var(--drop-shadow), + 0 2px 4px -0.5px color-mix(in srgb, var(--primary-color), calc(100% - var(--drop-shine)) transparent); + } + } +} + +/* Track sections, and similar */ + +@layer layout { + .album-group-list dt, + .group-series-list dt { + padding-left: 0; + } + + .album-group-list dd, + .group-series-list dd { + margin-left: 0; + } + + .album-group-list li { + padding-left: 1.5ch; + text-indent: -1.5ch; + } + + .album-group-list li > * { + text-indent: 0; + } + + .album-group-list blockquote, + .group-series-list blockquote { + max-width: 540px; + margin-bottom: 9px; + margin-top: 3px; + } + + .album-group-list blockquote p:first-child, + .group-series-list blockquote p:first-child { + margin-top: 0; + } + + .album-group-list blockquote p:last-child, + .group-series-list blockquote p:last-child { + margin-bottom: 0; + } +} + +@layer print { + .album-group-list dt, + .group-series-list dt { + font-style: oblique; + } +} diff --git a/src/static/css/page.css b/src/static/css/page.css new file mode 100644 index 00000000..fdd15358 --- /dev/null +++ b/src/static/css/page.css @@ -0,0 +1,915 @@ +/* Document root */ + +@property --box-opacity { + /* This property rule doesn't actually do anything in practice. + * It just lets us see the effective value of --box-opacity + * in browser CSS inspectors. + */ + syntax: "<number>"; + inherits: true; + initial-value: 0.6; +} + +:root { + color-scheme: dark; + + --initial-wallpaper-opacity: 0.5; + --wallpaper-brightness: var(--initial-wallpaper-opacity); + --box-opacity: calc(0.6 + 0.3 * (clamp(0.75, var(--wallpaper-brightness), 0.95) - 0.75) / (0.95 - 0.75)); +} + +/* Body */ + +@layer layout { + body { + position: relative; + margin: 0; + padding: 10px; + overflow-y: scroll; + } + + body::before { + content: ""; + } +} + +/* Wallpaper */ + +@layer layout { + body::before, .wallpaper-part { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + z-index: -1; + + /* NB: these are 100 LVW, "largest view width", etc. + * Stabilizes background on viewports with modal dimensions, + * e.g. expanding/shrinking tab bar or collapsible find bar. + * 100% dimensions are kept above for browser compatibility. + */ + width: 100lvw; + height: 100lvh; + } +} + +@layer material { + body { + background: black; + } + + body::before { + /* This is where the basic background-image rule + * gets applied... but the path *to* that media file + * isn't part of the CSS itself anymore! + */ + } + + body::before, .wallpaper-part { + background-position: center; + background-size: cover; + opacity: var(--initial-wallpaper-opacity); + } +} + +/* Page container */ + +@layer layout { + #page-container { + max-width: 1100px; + + margin: 0 auto 40px; + padding: calc(15px - var(--page-border-width)) 0; + + --page-border-width: 0; + border: var(--page-border-width) solid transparent; + box-sizing: border-box; + } + + #page-container > * { + margin-left: calc(15px - var(--page-border-width)); + margin-right: calc(15px - var(--page-border-width)); + } + + .layout-columns { + display: flex; + align-items: stretch; + } +} + +@layer material { + #page-container { + background-color: var(--bg-color, rgba(35, 35, 35, 0.8)); + + --adjust-page-opacity: calc(alpha * (1.0 + 0.55 * (var(--box-opacity, 0.6) - 0.6))); + + background-color: + color-mix(in oklab, + rgb(from var(--bg-color, rgba(35, 35, 35, 0.8)) + r g b / var(--adjust-page-opacity)), + rgb(from var(--bg-color, rgba(35, 35, 35, 0.8)) + 0 0 0 / var(--adjust-page-opacity)) + calc((var(--box-opacity, 0.6) - 0.6) * 40%)); + + color: #ffffff; + + --page-border-width: 0.5px; + border-bottom-width: 1px; + border-radius: 4px 4px 5px 5px; + + --page-border-color: rgba( + 144, 144, 144, + calc(0.4 + 0.5 * (var(--box-opacity) - 0.6) / 0.4)); + border-color: var(--page-border-color); + + box-shadow: + 0 0 40px #0008, + 0 20px 15px -4px #0002, + 0 6px 15px -3px #2221, + 0 4px 6px 2px #1113, + 0 1px 4px 1px #1114; + } +} + +/* Skippers */ + +@layer layout { + #skippers > * { + display: inline-block; + } +} + +@layer construction { + #skippers > .skipper-list:not(:last-child)::after { + display: inline-block; + content: "\00a0"; + margin-left: 2px; + margin-right: -2px; + border-left: 1px dotted; + } + + #skippers .skipper-list > .skipper:not(:last-child)::after { + content: " \00b7 "; + font-weight: 800; + } +} + +@layer interaction { + #skippers { + position: absolute; + left: -10000px; + top: auto; + width: 1px; + height: 1px; + } + + #skippers:focus-within { + position: static; + width: unset; + height: unset; + } + + #page-container:not(.showing-sidebar-left) #skippers .skipper[data-for=sidebar-left], + #page-container:not(.showing-sidebar-right) #skippers .skipper[data-for=sidebar-right] { + display: none; + } +} + +/* Banner */ + +@property --banner-shine { + syntax: '<percentage>'; + initial-value: 0%; + inherits: false; +} + +@layer interactivity { + #banner::before, #banner::after { + pointer-events: none; + } +} + +@layer layout { + #banner { + position: relative; + } + + /* Used for top border */ + #banner::before { + content: ""; + position: absolute; + top: -1px; + left: var(--page-border-width); + right: var(--page-border-width); + bottom: 0; + } + + /* Used for inset shadow */ + #banner::after { + content: ""; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + } + + #banner { + margin: 10px calc(-1 * var(--page-border-width)); + } + + #banner img { + display: block; + width: 100%; + height: auto; + } + + #banner.short, #banner.short img { + /* 1100px -> 72.5px, 900px -> 60px + * 12.5px height per 200px width + * Viewport maxes at 1124px, so: + * Start from 72.5px + * Get width below maximum: + * (1124px - min(1124px, 100vw)) + * Multiply by rate of change: + * (12.5 / 200) + */ + max-height: calc(72.5px - (1124px - min(1124px, 100vw)) * (12.5 / 200)); + } + + #banner.short img { + object-position: 0 var(--short-banner-alignment, 50%); + } +} + +@layer material { + #banner { + background-color: var(--abyss-color); + border-bottom: 1px solid var(--primary-color); + } + + #banner::before { + border-top: 1px dashed var(--page-border-color); + } + + #banner::after { + box-shadow: + inset 0 -2px 3px rgba(0, 0, 0, 0.35), + inset 0 1px 4px 2px rgba(0, 0, 0, 0.15); + } + + #banner.dim img { + opacity: 0.32; + } +} + +/* Banner - shine effect */ + +@supports (-webkit-box-reflect: below) { + @layer material { + #banner { + -webkit-box-reflect: below -12px linear-gradient(transparent, color-mix(in srgb, transparent, var(--banner-shine, 0%) white)); + } + } + + @layer interaction { + #banner { + transition: --banner-shine 0.8s; + } + + #banner:not(.dim, .collapsed) { + --banner-shine: 4%; + } + + #banner:not(.dim, .collapsed):hover { + --banner-shine: 35%; + transition-delay: 0.3s; + } + } +} + +/* Boxes (in general) */ + +@layer layout { + #skippers, + #header, + #secondary-nav, + #footer { + padding: 5px; + } + + #skippers, + #header, + #secondary-nav { + margin-bottom: 10px; + } + + #footer { + margin-top: 10px; + } +} + +@layer material { + #skippers, + #header, + #secondary-nav, + #content, + #footer, + .sidebar { + background-color: rgba(0, 0, 0, var(--box-opacity, 0.6)); + border: 1px dotted var(--primary-color); + border-radius: 3px; + } +} + +@layer print { + #skippers, + #header, + #secondary-nav, + #footer, + .sidebar { + font-size: 0.85em; + } +} + +/* Header and navigation links */ + +@layer layout { + #header { display: grid; } + + .nav-main-links { grid-area: main-links; } + .nav-content { grid-area: content; } + .nav-bottom-row { grid-area: bottom-row; } + + #header.nav-has-main-links.nav-has-content { + grid-template-columns: 2.5fr 3fr; + grid-template-rows: min-content 1fr; + grid-template-areas: + "main-links content" + "bottom-row content"; + } + + #header.nav-has-main-links:not(.nav-has-content) { + grid-template-columns: 1fr; + grid-template-areas: + "main-links" + "bottom-row"; + } + + .nav-main-links { + margin-right: 20px; + } + + .nav-bottom-row { + align-self: start; + } +} + +@layer print { + .nav-link { + display: inline-block; + } + + .nav-main-links .nav-link.current > span.nav-link-content > a { + font-weight: 800; + } + + .nav-main-links .nav-link-accent { + display: inline-block; + } + + .series-nav-links { + display: inline-block; + } +} + +@layer construction { + .nav-links-index .nav-link:not(:first-child)::before, + .nav-links-groups .nav-link:not(:first-child)::before { + content: "\0020\00b7\0020"; + font-weight: 800; + } + + .nav-links-hierarchical .nav-link + .nav-link::before, + .nav-links-hierarchical .nav-link + .blockwrap .nav-link::before { + content: "\0020/\0020"; + } + + .group-with-series .group-nav-links + .series-nav-links::before { + content: "\00a0»\00a0"; + font-weight: normal; + } + + .group-with-series .series-nav-links:not(:last-child)::after { + content: ",\00a0"; + font-weight: normal; + } + + .group-with-series .series-nav-links + .series-nav-links::before { + content: ""; + } +} + +@layer interaction { + .inert-previous-next-link { + opacity: 0.7; + } +} + +/* Secondary nav */ + +@layer layout { + #secondary-nav { + text-align: center; + } + + #secondary-nav.album-secondary-nav { + display: flex; + justify-content: space-around; + padding-left: 7.5% !important; + padding-right: 7.5% !important; + flex-wrap: wrap; + } + + #secondary-nav.album-secondary-nav.with-previous-next .group-with-series { + width: 100%; + } + + #secondary-nav.album-secondary-nav.with-previous-next > * { + margin-left: 5px; + margin-right: 5px; + } +} + +@layer print { + #secondary-nav.album-secondary-nav { + line-height: 1.4; + } + + #secondary-nav.album-secondary-nav .group-nav-links .dot-switcher, + #secondary-nav.album-secondary-nav .series-nav-links .dot-switcher { + white-space: nowrap; + } +} + +/* Sidebar column */ + +@layer layout { + .sidebar-column { + flex: 1 1 35%; + min-width: 150px; + max-width: 250px; + align-self: flex-start; + } + + .sidebar-column.wide { + max-width: 350px; + flex-basis: 300px; + flex-shrink: 0; + flex-grow: 1; + } + + .sidebar-column.always-content-column { + /* duplicated in thin & medium media query */ + position: static !important; + max-width: unset !important; + flex-basis: unset !important; + margin-right: 0 !important; + margin-left: 0 !important; + width: 100%; + } + + .sidebar-multiple { + display: flex; + flex-direction: column; + } + + .sidebar-multiple .sidebar:not(:first-child) { + margin-top: 15px; + } +} + +@layer interaction { + .sidebar-column.initially-hidden { + display: none; + } + + .sidebar-column.search-showing-results { + position: sticky; + top: 5px; + align-self: flex-start !important; /* pls */ + } +} + +/* Sidebar boxes */ + +@layer layout { + .sidebar { + --content-padding: 5px; + padding: var(--content-padding); + } + + #sidebar-left { + margin-right: 10px; + } + + #sidebar-right { + margin-left: 10px; + } + + .sidebar-box-joiner { + height: 10px; + } + + .sidebar-box-joiner + .sidebar { + margin-top: 0 !important; + } +} + +@layer material { + .sidebar-box-joiner { + width: 0; + margin-left: auto; + margin-right: auto; + border-right: 1px dashed var(--primary-color); + } +} + +@layer print { + .sidebar > h1, + .sidebar > h2, + .sidebar > h3, + .sidebar > p { + text-align: center; + padding-left: 4px; + padding-right: 4px; + } + + .sidebar h1 { + font-size: 1.25em; + } + + .sidebar h2 { + font-size: 1.1em; + margin: 0; + } + + .sidebar h2:first-child { + margin-top: 0.5em; + margin-bottom: 0.5em; + } + + .sidebar h3 { + font-size: 1.1em; + font-style: oblique; + font-variant: small-caps; + margin-top: 0.3em; + margin-bottom: 0em; + } + + .sidebar > p { + margin: 0.5em 0; + padding: 0 5px; + } + + .sidebar hr { + color: #555; + margin: 10px 5px; + } + + .sidebar > ol, + .sidebar > ul { + padding-left: 30px; + padding-right: 15px; + } + + .sidebar > dl { + padding-right: 15px; + padding-left: 0; + } + + .sidebar > dl dt { + padding-left: 10px; + margin-top: 0.5em; + } + + .sidebar > dl dt.current { + font-weight: 800; + } + + .sidebar > dl dd { + margin-left: 0; + } + + .sidebar > dl dd ul { + padding-left: 30px; + margin-left: 0; + } + + .sidebar > dl .side { + padding-left: 10px; + } + + .sidebar details.has-tree-list[open] summary { + font-weight: 800; + } + + .sidebar dl.tree-list { + margin-top: 0.25em; + line-height: 1.25em; + padding-left: 15px; + } + + .sidebar dl.tree-list dt { + display: list-item; + list-style-type: disc; + padding-left: 0; + margin-left: 20px; + } + + .sidebar dl.tree-list dl { + padding-left: 15px; + } + + .sidebar dl.tree-list dd { + margin-left: 0; + } + + .sidebar dl.tree-list dt.current a { + font-weight: 800; + border-bottom: 1px solid; + } + + .sidebar .times-used { + opacity: 0.7; + font-size: 0.9em; + cursor: default; + } + + .sidebar li.current { + font-weight: 800; + } + + .sidebar li { + overflow-wrap: break-word; + padding-right: 3px; + } + + .sidebar li.structured { + margin-bottom: 3px; + } + + .sidebar > details.current summary { + font-weight: 800; + } + + .sidebar > details summary { + margin-top: 0.5em; + padding-left: 5px; + } + + .sidebar > details.current summary span b { + font-weight: 800; + } + + .sidebar > details ul, + .sidebar > details ol { + margin-top: 0; + margin-bottom: 0; + } + + .sidebar > details:last-child { + margin-bottom: 10px; + } + + .sidebar > details[open] { + margin-bottom: 1em; + } + + .sidebar article { + text-align: left; + margin: 5px 5px 15px 5px; + } + + .sidebar article:last-child { + margin-bottom: 5px; + } + + .sidebar article h2, + .news-index h2 { + border-bottom: 1px dotted; + } + + .sidebar article h2 time, + .news-index time { + float: right; + font-weight: normal; + } + + .group-chronology-link, + .series-chronology-link { + font-style: oblique; + } + + .group-chronology-link a, + .series-chronology-link a { + font-style: normal; + } +} + +/* Track release sidebar box */ + +@layer layout { + .track-release-sidebar-box { + --content-padding: 3px; + } + + .track-release-sidebar-box h1 { + margin: 0; + } + + .track-release-sidebar-box + .track-release-sidebar-box, + .track-release-sidebar-box + .track-list-sidebar-box, + .track-list-sidebar-box + .track-release-sidebar-box { + margin-top: 5px !important; + border-top-left-radius: 0 !important; + border-top-right-radius: 0 !important; + } + + .track-release-sidebar-box:has(+ .track-list-sidebar-box), + .track-list-sidebar-box:has(+ .track-release-sidebar-box) { + border-bottom-right-radius: 0 !important; + border-bottom-left-radius: 0 !important; + } +} + +@layer print { + .track-release-sidebar-box h1 { + font-weight: normal; + font-size: 0.9em; + font-style: oblique; + } +} + +/* Track list sidebar box */ + +@layer print { + .track-list-sidebar-box summary { + padding-left: 20px !important; + text-indent: -15px !important; + } + + .track-list-sidebar-box .track-section-range { + white-space: nowrap; + } +} + +/* Content area */ + +@layer layout { + #content { + flex-grow: 1; + flex-shrink: 3; + + position: relative; + box-sizing: border-box; + + --content-padding: 20px; + padding: var(--content-padding); + } +} + +@layer print { + #content { + overflow-wrap: anywhere; + } + + #content h1 { + font-size: 1.5em; + } + + #content li { + margin-bottom: 4px; + } + + #content li i { + white-space: nowrap; + } + + #content li.divider { + list-style-type: none; + max-width: 220px; + margin-top: 0.6em; + margin-bottom: 0.6em; + } + + #content li.divider hr { + color: #888; + border: none; + border-bottom: 1px solid; + } + + #content details { + margin-top: 0.25em; + margin-bottom: 0.25em; + } + + #content.top-index h1, + #content.flash-index h1 { + text-align: center; + font-size: 2em; + } + + #content.flash-index h2 { + text-align: center; + font-size: 2.5em; + font-variant: small-caps; + font-style: oblique; + margin-bottom: 0; + text-align: center; + width: 100%; + } + + #content.flash-index h2 .flash-act-title, + #content.flash-index h1 .flash-act-title { + display: block; + } + + #content.flash-index h2 .flash-act-title { + text-transform: uppercase; + } + + #content.top-index h2 { + text-align: center; + font-size: 2em; + font-weight: normal; + margin-bottom: 0.25em; + } + + #content.top-index.has-subtitle h1 { + margin-bottom: 0.35em; + } + + #content.top-index h2.page-subtitle { + font-size: 1.8em; + margin-top: 0.35em; + margin-bottom: 0.5em; + } +} + +/* Footer */ + +@layer layout { + .footer-content { + margin: 5px 12%; + } + + .footer-content > :first-child { + margin-top: 0; + } + + .footer-content > :last-child { + margin-bottom: 0; + } + + .footer-localization-links { + margin: 5px 12%; + } +} + +@layer print { + footer { + text-align: center; + font-style: oblique; + } +} + +@layer construction { + .footer-localization-links > span:not(:last-child)::after { + content: " \00b7 "; + font-weight: 800; + } +} + +/* Content (main box) */ + +@layer layout { + main { + --responsive-padding-ratio: 0.10; + } + + main.long-content { + --long-content-padding-ratio: var(--responsive-padding-ratio); + } + + main.long-content .main-content-container, + main.long-content > h1 { + padding-left: calc(var(--long-content-padding-ratio) * 100%); + padding-right: calc(var(--long-content-padding-ratio) * 100%); + } + + #content.top-index section { + margin-bottom: 1.5em; + } +} diff --git a/src/static/css/responsive.css b/src/static/css/responsive.css new file mode 100644 index 00000000..86cd7eb6 --- /dev/null +++ b/src/static/css/responsive.css @@ -0,0 +1,223 @@ +/* Layout - Wide (most computers) */ + +@media (min-width: 850px) { + #page-container.showing-sidebar-left:not(.sidebars-in-content-column) #secondary-nav:not(.always-visible), + #page-container.showing-sidebar-right:not(.sidebars-in-content-column) #secondary-nav:not(.always-visible) { + display: none; + } +} + +/* Layout - Medium (tablets, some landscape mobiles) + * + * Note: Rules defined here are exclusive to "medium" width, i.e. they don't + * additionally apply to "thin". Use the later section which applies to both + * if so desired. + */ + +@media (min-width: 600px) and (max-width: 849.98px) { + /* Medium layout is mainly defined (to the user) by hiding the sidebar, so + * don't apply the similar layout change of widening the long-content area + * if this page doesn't have a sidebar to hide in the first place. + */ + #page-container.showing-sidebar-left main, + #page-container.showing-sidebar-right main { + --responsive-padding-ratio: 0.06; + } +} + +/* Layout - Wide or Medium */ + +@media (min-width: 600px) { + .content-sticky-heading-root { + /* Safari doesn't always play nicely with position: sticky, + * this seems to fix images sometimes displaying above the + * position: absolute subheading (h2) child + * + * See also: https://stackoverflow.com/questions/50224855/ + */ + transform: translate3d(0, 0, 0); + z-index: 1; + } + + /* Cover art floats to the right. It's positioned in HTML beneath the + * heading, so pull it up a little to "float" on top. + */ + #artwork-column { + float: right; + width: 40%; + min-width: 220px; + max-width: 280px; + margin: -60px 0 10px 20px; + + position: relative; + z-index: 2; + } + + /* ...Except on top-indexes, where cover art is displayed prominently + * between the heading and subheading. + */ + #content.top-index #artwork-column { + float: none; + margin: 2em auto 2.5em auto; + max-width: 375px; + } + + html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:not(:nth-child(n+7)) { + flex-basis: 23%; + margin: 15px; + } + + html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:nth-child(n+7) { + flex-basis: 18%; + margin: 10px; + } +} + +/* Layout - Medium or Thin */ + +@media (max-width: 849.98px) { + .sidebar.collapsible, + .sidebar-box-joiner.collapsible, + .sidebar-column.all-boxes-collapsible { + display: none; + } + + /* Duplicated for "sidebars in content column" */ + + .layout-columns { + flex-direction: column; + } + + .layout-columns > *:not(:last-child) { + margin-bottom: 10px; + } + + .sidebar-column { + position: static !important; + max-width: unset !important; + flex-basis: unset !important; + margin-right: 0 !important; + margin-left: 0 !important; + width: 100%; + } + + .sidebar .news-entry:not(.first-news-entry) { + display: none; + } + + .wiki-search-sidebar-box { + max-height: max(245px, 60vh, calc(100vh - 205px)); + } + + /* End duplicated for "sidebars in content column" */ + + .grid-listing > .grid-item { + flex-basis: 40%; + } +} + +/* Layout - "sidebars in content column" + * This is the same code as immediately above, for medium and + * thin layouts, but can be opted into by the page itself + * instead of through a media query. + */ + +#page-container.sidebars-in-content-column +.layout-columns { + flex-direction: column; +} + +#page-container.sidebars-in-content-column +.layout-columns > *:not(:last-child) { + margin-bottom: 10px; +} + +#page-container.sidebars-in-content-column +.sidebar-column { + position: static !important; + max-width: unset !important; + flex-basis: unset !important; + margin-right: 0 !important; + margin-left: 0 !important; + width: 100%; +} + +#page-container.sidebars-in-content-column +.sidebar .news-entry:not(.first-news-entry) { + display: none; +} + +#page-container.sidebars-in-content-column +.wiki-search-sidebar-box { + max-height: max(245px, 60vh, calc(100vh - 205px)); +} + +/* Layout - Thin (phones) */ + +@media (max-width: 600px) { + main { + --responsive-padding-ratio: 0.02; + } + + #artwork-column { + margin: 25px 0 5px 0; + width: 100%; + max-width: unset; + } + + #artwork-column .cover-artwork { + --normal-shadow: 0 0 transparent; + } + + #artwork-column .cover-artwork:not(:first-child), + #artwork-column .cover-artwork-joiner { + margin-left: 30px; + margin-right: 30px; + } + + .music-video { + width: 70%; + margin-left: auto; + margin-right: auto; + } + + .music-video .image { + aspect-ratio: 32 / 9; + } + + #additional-names-box { + width: unset; + max-width: unset; + } + + .nav-has-content .nav-main-links .nav-link-accent { + display: block; + } + + /* Show sticky heading above cover art */ + + .content-sticky-heading-root { + z-index: 2; + } + + .content-sticky-heading-row h1 { + padding-right: 10px; + } + + /* Let sticky heading text span past lower-index cover art */ + + .content-sticky-heading-container.has-cover .content-sticky-heading-row, + .content-sticky-heading-container.has-cover .content-sticky-subheading-row { + grid-template-columns: 1fr 90px; + } + + /* Disable grid features, just line header children up vertically */ + + #header { + display: block; + } + + #header > div:not(:first-child) { + margin-top: 0.5em; + } +} diff --git a/src/static/css/search.css b/src/static/css/search.css new file mode 100644 index 00000000..f421803b --- /dev/null +++ b/src/static/css/search.css @@ -0,0 +1,490 @@ +/* Sidebar box */ + +@layer layout { + .wiki-search-sidebar-box { + padding: 1px 0 0 0; + + z-index: 100; + max-height: calc(100vh - 20px); + + display: flex; + flex-direction: column; + } +} + +@layer material { + .wiki-search-sidebar-box { + background-color: #000000c0; + + -webkit-backdrop-filter: + brightness(1.2) blur(4px); + backdrop-filter: + brightness(1.2) blur(4px); + } + + .wiki-search-sidebar-box.showing-results { + box-shadow: + 0 4px 16px -8px var(--primary-color), + 0 10px 6px var(--bg-black-color), + 0 6px 4px #00000040; + } +} + +/* Interactions with other sidebar boxes */ + +@layer interaction { + /* This is to say, any sidebar that's *not* + * the first sidebar after the search box. + */ + .wiki-search-sidebar-box.showing-results + .sidebar ~ .sidebar { + margin-top: 5px; + } + + .wiki-search-sidebar-box.showing-results ~ .sidebar:not(:hover) { + opacity: 0.8; + filter: brightness(0.7); + } +} + +/* Label and input elements */ + +@layer layout { + .wiki-search-label { + width: calc(100% - 4px); + padding: 2px 4px; + margin: 2px 2px 3px 2px; + box-sizing: border-box; + + display: flex; + flex-direction: row; + } +} + +@layer material { + .wiki-search-label { + background: transparent; + border: 1px solid var(--dim-color); + border-radius: 3px; + } + + .wiki-search-label::before { + display: inline-block; + padding-left: 3px; + padding-right: 3px; + margin-right: 3px; + width: 1.8em; + text-align: center; + content: '\1f50d\fe0e'; + } + + .wiki-search-input { + background: transparent; + border: transparent; + color: inherit; + flex-grow: 1; + } + + .wiki-search-input::-webkit-search-cancel-button { + filter: grayscale(1) invert(1); + } + + .wiki-search-input::placeholder { + color: var(--primary-color); + font-style: oblique; + } +} + +@layer interaction { + .wiki-search-label.disabled { + opacity: 0.6; + } + + .wiki-search-label.disabled, + .wiki-search-input[disabled] { + cursor: not-allowed; + } + + .wiki-search-label:not(.disabled):hover, + .wiki-search-label:focus-within { + background: var(--light-ghost-color); + } + + .wiki-search-label:focus-within { + border-color: var(--primary-color); + } + + .wiki-search-label:focus-within::before { + opacity: 0.7; + } + + .wiki-search-input:focus { + border: none; + outline: none; + } + + /* This is always the "Search for anything" text, + * if any placeholder is visible while focused. + */ + .wiki-search-input:focus::placeholder { + color: var(--dim-color); + } +} + +/* Progress area ("Loading data..." etc) */ + +@layer layout { + .wiki-search-progress-container { + padding: 2px 6px 4px 6px; + display: flex; + flex-direction: row; + } + + .wiki-search-progress-label { + margin-right: 1ch; + } + + .wiki-search-progress-bar { + flex-grow: 1; + } +} + +@layer print { + .wiki-search-progress-label { + font-size: 0.9em; + font-style: oblique; + } +} + +@layer interaction { + .wiki-search-progress-label { + cursor: default; + } +} + +/* "Failed to search" and "no results" info */ + +@layer layout { + .wiki-search-failed-container, + .wiki-search-no-results { + padding: 2px 3px 4px 6px; + } + + .wiki-search-failed-container p { + margin: 0; + } +} + +@layer print { + .wiki-search-no-results { + font-size: 0.9em; + } +} + +@layer interaction { + .wiki-search-no-results { + cursor: default; + } +} + +/* Context area ("Return to...") */ + +@layer layout { + .wiki-search-context-container { + padding: 2px 12px 4px; + } +} + +@layer material { + .wiki-search-context-container { + border-bottom: 1px solid var(--dim-color); + } +} + +@layer print { + .wiki-search-context-container { + font-size: 0.9em; + } +} + +/* Filters */ + +@layer layout { + .wiki-search-filter-container { + padding: 4px; + } + + .wiki-search-filter-link { + display: inline-block; + margin: 2px; + padding: 2px 4px; + border: 2px solid transparent; + border-radius: 4px; + } +} + +@layer interaction { + .wiki-search-filter-link:where(.active.shown) { + animation: + 0.15s ease 0.00s forwards normal show-filter, + 0.60s linear 0.15s infinite alternate blink-filter; + } + + .wiki-search-filter-link:where(.active:not(.shown)) { + animation: + 0.00s linear 0.00s forwards normal show-filter, + 0.60s linear 0.00s infinite alternate blink-filter; + } + + .wiki-search-filter-link:where(:not(.active).hidden) { + /* We can't just reverse the show-filter animation, + * because that won't actually start it over again. + */ + animation: + 0.15s ease 0.00s forwards reverse show-filter-the-sequel; + } + + .wiki-search-filter-link.active-from-query { + background: var(--primary-color); + border-color: var(--primary-color); + color: #000a; + animation: none; + } + + .wiki-search-filter-link.active-from-query::after { + content: "I"; + color: black; + font-family: monospace; + font-weight: 800; + font-size: 1.2em; + margin-left: 0.5ch; + vertical-align: middle; + animation: 1s steps(2, jump-none) 0.6s infinite blink-caret; + } + + @keyframes show-filter { + from { + background: transparent; + border-color: transparent; + color: var(--primary-color); + } + + to { + background: var(--primary-color); + border-color: var(--primary-color); + color: black; + } + } + + /* Exactly the same as show-filter above. */ + @keyframes show-filter-the-sequel { + from { + background: transparent; + border-color: transparent; + color: var(--primary-color); + } + + to { + background: var(--primary-color); + border-color: var(--primary-color); + color: black; + } + } + + @keyframes blink-filter { + to { + background: color-mix(in srgb, var(--primary-color) 90%, transparent); + } + } + + @keyframes blink-caret { + from { opacity: 0; } + to { opacity: 1; } + } +} + +/* Divider above search result list */ + +@layer material { + .wiki-search-sidebar-box hr { + margin-top: 3px !important; + margin-bottom: 3px !important; + } +} + +@layer layout { + .wiki-search-sidebar-box hr { + border-color: var(--primary-color); + border-style: none none dotted none; + } +} + +/* Result list */ + +@layer layout { + .wiki-search-results-container { + margin-bottom: 0; + padding: 2px; + } +} + +/* Basic result styling, including interactions */ + +@layer layout { + .wiki-search-result { + position: relative; + display: flex; + padding: 4px 3px 4px 6px; + } + + .wiki-search-result::before { + content: ''; + position: absolute; + top: -2px; + bottom: -2px; + left: 0; + right: 0; + } +} + +@layer material { + .wiki-search-result::before { + border: 1.5px solid var(--primary-color); + border-radius: 4px; + } +} + +@layer interaction { + .wiki-search-result:hover { + text-decoration: none !important; + } + + .wiki-search-result::before { + display: none; + } + + .wiki-search-result:hover::before, + .wiki-search-result:focus::before { + display: block; + background: var(--light-ghost-color); + } + + .wiki-search-result.current-result { + background: var(--light-ghost-color); + border-top: 1px solid var(--dim-color); + border-bottom: 1px solid var(--dim-color); + } + + .wiki-search-result.current-result:hover { + background: none; + border-color: transparent; + } +} + +/* Stuff inside results */ + +@layer layout { + .wiki-search-result-text-area { + align-self: center; + flex-grow: 1; + min-width: 0; + padding-bottom: 2px; + } + + .wiki-search-result-name { + margin-right: 0.25em; + } + + .wiki-search-result-image-container { + align-self: flex-start; + flex-shrink: 0; + margin-right: 6px; + border-radius: 2px; + overflow: hidden; + } + + .wiki-search-result-image, + .wiki-search-result-image-placeholder { + display: block; + width: 1.8em; + height: 1.8em; + aspect-ratio: 1 / 1; + border-radius: 1.5px; + } +} + +@layer material { + .wiki-search-result-image-container { + background-color: var(--deep-color); + border: 2px solid var(--deep-color); + } + + .wiki-search-result-image-placeholder { + background-color: #0004; + box-shadow: 0 1px 3px -1px #0008 inset; + } + + .wiki-search-result-image.has-warning { + filter: blur(2px) brightness(0.8); + } +} + +@layer print { + .wiki-search-result-text-area { + overflow-wrap: break-word; + } + + .wiki-search-result:hover .wiki-search-result-name { + text-decoration: underline; + } + + .wiki-search-current-result-text, + .wiki-search-result-kind, + .wiki-search-result-disambiguator { + opacity: 0.9; + display: inline-block; + } + + .wiki-search-current-result-text, + .wiki-search-result-kind { + font-style: oblique; + } +} + +@layer interaction { + .wiki-search-result.current-result:hover .wiki-search-current-result-text { + filter: saturate(0.8) brightness(1.4); + } + + .wiki-search-results:not(:has(.wiki-search-result-image)) .wiki-search-result-image-container { + display: none; + } +} + +/* "I'm done searching" line */ + +@layer layout { + .wiki-search-end-search-line { + text-align: center; + margin-top: 6px; + margin-bottom: 2px; + } +} + +@layer material { + .wiki-search-end-search-line a { + display: inline-block; + font-style: oblique; + opacity: 0.9; + padding: 3px 6px 4px 6px; + border: 1.5px solid transparent; + border-radius: 4px; + } +} + +@layer interaction { + .wiki-search-end-search-line a:hover { + opacity: 1; + background: var(--light-ghost-color); + border-color: var(--deep-color); + } +} diff --git a/src/static/css/site.css b/src/static/css/site.css index 6c853161..b3fff34c 100644 --- a/src/static/css/site.css +++ b/src/static/css/site.css @@ -1,3147 +1,14 @@ -/* A frontend file! Wow. - * This file is just loaded statically 8y <link>s in the HTML files, so there's - * no need to re-run upd8.js when tweaking values here. Handy! - */ - -/* Squares */ - -/* This styling is kind of awkwardly placed at the very top. Sorry! - * We need to rework what order sets of styles get applied in to be - * much more explicit (so that overriding isn't a headache), and - * hopefully that can be done through @imports, but it'll take some - * reworking and cleaning up. - */ - -.square { - position: relative; - width: 100%; -} - -.square::after { - content: ""; - display: block; - padding-bottom: 100%; -} - -.square-content { - position: absolute; - width: 100%; - height: 100%; -} - -/* Layout - Common */ - -body { - position: relative; - margin: 0; - padding: 10px; - overflow-y: scroll; -} - -body::before { - content: ""; -} - -body::before, .wallpaper-part { - position: fixed; - top: 0; - left: 0; - width: 100vw; - height: 100vh; - z-index: -1; - - /* NB: these are 100 LVW, "largest view width", etc. - * Stabilizes background on viewports with modal dimensions, - * e.g. expanding/shrinking tab bar or collapsible find bar. - * 100% dimensions are kept above for browser compatibility. - */ - width: 100lvw; - height: 100lvh; -} - -#page-container { - max-width: 1100px; - margin: 0 auto 40px; - padding: 15px 0; -} - -#page-container > * { - margin-left: 15px; - margin-right: 15px; -} - -#skippers:focus-within { - position: static; - width: unset; - height: unset; -} - -#banner { - margin: 10px 0; - width: 100%; - position: relative; -} - -#banner::after { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; -} - -#banner img { - display: block; - width: 100%; - height: auto; -} - -#skippers { - position: absolute; - left: -10000px; - top: auto; - width: 1px; - height: 1px; -} - -.layout-columns { - display: flex; - align-items: stretch; -} - -#header, -#secondary-nav, -#skippers, -#footer { - padding: 5px; -} - -#header, -#secondary-nav, -#skippers { - margin-bottom: 10px; -} - -#footer { - margin-top: 10px; -} - -#header { - display: grid; -} - -#header.nav-has-main-links.nav-has-content { - grid-template-columns: 2.5fr 3fr; - grid-template-rows: min-content 1fr; - grid-template-areas: - "main-links content" - "bottom-row content"; -} - -#header.nav-has-main-links:not(.nav-has-content) { - grid-template-columns: 1fr; - grid-template-areas: - "main-links" - "bottom-row"; -} - -.nav-main-links { - grid-area: main-links; - margin-right: 20px; -} - -.nav-content { - grid-area: content; -} - -.nav-bottom-row { - grid-area: bottom-row; - align-self: start; -} - -.sidebar-column { - flex: 1 1 20%; - min-width: 150px; - max-width: 250px; - flex-basis: 250px; - align-self: flex-start; -} - -.sidebar-column.wide { - max-width: 350px; - flex-basis: 300px; - flex-shrink: 0; - flex-grow: 1; -} - -.sidebar-column.initially-hidden { - display: none; -} - -.sidebar-multiple { - display: flex; - flex-direction: column; -} - -.sidebar-multiple .sidebar:not(:first-child) { - margin-top: 15px; -} - -.sidebar { - --content-padding: 5px; - padding: var(--content-padding); -} - -#sidebar-left { - margin-right: 10px; -} - -#sidebar-right { - margin-left: 10px; -} - -#content { - position: relative; - --content-padding: 20px; - box-sizing: border-box; - padding: var(--content-padding); - flex-grow: 1; - flex-shrink: 3; -} - -.footer-content { - margin: 5px 12%; -} - -.footer-content > :first-child { - margin-top: 0; -} - -.footer-content > :last-child { - margin-bottom: 0; -} - -.footer-localization-links { - margin: 5px 12%; -} - -/* Design & Appearance - Layout elements */ - -body { - background: black; -} - -body::before { - background-image: url("../../media/bg.jpg"); -} - -body::before, .wallpaper-part { - background-position: center; - background-size: cover; - opacity: 0.5; -} - -#page-container { - background-color: var(--bg-color, rgba(35, 35, 35, 0.8)); - color: #ffffff; - box-shadow: 0 0 40px rgba(0, 0, 0, 0.5); -} - -#skippers > * { - display: inline-block; -} - -#skippers > .skipper-list:not(:last-child)::after { - display: inline-block; - content: "\00a0"; - margin-left: 2px; - margin-right: -2px; - border-left: 1px dotted; -} - -#skippers .skipper-list > .skipper:not(:last-child)::after { - content: " \00b7 "; - font-weight: 800; -} - -#page-container:not(.showing-sidebar-left) #skippers .skipper[data-for=sidebar-left], -#page-container:not(.showing-sidebar-right) #skippers .skipper[data-for=sidebar-right] { - display: none; -} - -#banner { - background: black; - background-color: var(--dim-color); - border-bottom: 1px solid var(--primary-color); -} - -#banner::after { - box-shadow: inset 0 -2px 3px rgba(0, 0, 0, 0.35); - pointer-events: none; -} - -#banner.dim img { - opacity: 0.8; -} - -#header, -#secondary-nav, -#skippers, -#footer, -.sidebar { - font-size: 0.85em; -} - -.sidebar, -#content, -#header, -#secondary-nav, -#skippers, -#footer { - background-color: rgba(0, 0, 0, 0.6); - border: 1px dotted var(--primary-color); - border-radius: 3px; - transition: background-color 0.2s; -} - -/* -.sidebar:focus-within, -#content:focus-within, -#header:focus-within, -#secondary-nav:focus-within, -#skippers:focus-within, -#footer:focus-within { - background-color: rgba(0, 0, 0, 0.85); - border-style: solid; -} -*/ - -.sidebar > h1, -.sidebar > h2, -.sidebar > h3, -.sidebar > p { - text-align: center; - padding-left: 4px; - padding-right: 4px; -} - -.sidebar h1 { - font-size: 1.25em; -} - -.sidebar h2 { - font-size: 1.1em; - margin: 0; -} - -.sidebar h3 { - font-size: 1.1em; - font-style: oblique; - font-variant: small-caps; - margin-top: 0.3em; - margin-bottom: 0em; -} - -.sidebar > p { - margin: 0.5em 0; - padding: 0 5px; -} - -.sidebar hr { - color: #555; - margin: 10px 5px; -} - -.sidebar > ol, -.sidebar > ul { - padding-left: 30px; - padding-right: 15px; -} - -.sidebar > dl { - padding-right: 15px; - padding-left: 0; -} - -.sidebar > dl dt { - padding-left: 10px; - margin-top: 0.5em; -} - -.sidebar > dl dt.current { - font-weight: 800; -} - -.sidebar > dl dd { - margin-left: 0; -} - -.sidebar > dl dd ul { - padding-left: 30px; - margin-left: 0; -} - -.sidebar > dl .side { - padding-left: 10px; -} - -.sidebar li.current { - font-weight: 800; -} - -.sidebar li { - overflow-wrap: break-word; -} - -.sidebar > details.current summary { - font-weight: 800; -} - -.sidebar > details summary { - margin-top: 0.5em; - padding-left: 5px; -} - -.sidebar > details.current summary span b { - font-weight: 800; -} - -summary > span b { - font-weight: normal; - color: var(--primary-color); -} - -summary > span:hover { - cursor: pointer; - text-decoration: underline; - text-decoration-color: var(--primary-color); -} - -summary > span:hover a { - text-decoration: none !important; -} - -summary > span:hover:has(a:hover), -summary > span:hover:has(a.nested-hover), -summary.has-nested-hover > span { - text-decoration: none !important; -} - -summary > span:hover:has(a:hover) a, -summary > span:hover:has(a.nested-hover) a, -summary.has-nested-hover > span a { - text-decoration: underline !important; -} - -summary.underline-white > span:hover { - text-decoration-color: white; -} - -/* This link isn't supposed to be underlined *at all* - * when the summary (and not link) is hovered, but - * for some reason Safari is still applying its colored - * and dotted(!) underline. Get around the apparent - * effect by just making it white. - */ -summary.underline-white > span:hover a:not(:hover) { - text-decoration-color: white; -} - -.sidebar > details ul, -.sidebar > details ol { - margin-top: 0; - margin-bottom: 0; -} - -.sidebar > details:last-child { - margin-bottom: 10px; -} - -.sidebar > details[open] { - margin-bottom: 1em; -} - -.sidebar article { - text-align: left; - margin: 5px 5px 15px 5px; -} - -.sidebar article:last-child { - margin-bottom: 5px; -} - -.sidebar article h2, -.news-index h2 { - border-bottom: 1px dotted; -} - -.sidebar article h2 time, -.news-index time { - float: right; - font-weight: normal; -} - -.sidebar-column.search-showing-results { - position: sticky; - top: 5px; - align-self: flex-start !important; /* pls */ -} - -.sidebar-box-joiner { - width: 0; - margin-left: auto; - margin-right: auto; - border-right: 1px dashed var(--primary-color); - height: 10px; -} - -.sidebar-box-joiner + .sidebar { - margin-top: 0 !important; -} - -.wiki-search-sidebar-box { - padding: 1px 0 0 0; - - z-index: 100; - max-height: calc(100vh - 20px); - - display: flex; - flex-direction: column; - - background-color: #000000c0; - - -webkit-backdrop-filter: - brightness(1.2) blur(4px); - - backdrop-filter: - brightness(1.2) blur(4px); -} - -.wiki-search-sidebar-box.showing-results { - box-shadow: - 0 4px 16px -8px var(--primary-color), - 0 10px 6px var(--bg-black-color), - 0 6px 4px #00000040; -} - -/* This is to say, any sidebar that's *not* - * the first sidebar after the search box. - */ -.wiki-search-sidebar-box.showing-results + .sidebar ~ .sidebar { - margin-top: 5px; -} - -.wiki-search-sidebar-box.showing-results ~ .sidebar:not(:hover) { - opacity: 0.8; - filter: brightness(0.7); -} - -.wiki-search-label { - width: calc(100% - 4px); - padding: 2px 4px; - margin: 2px 2px 3px 2px; - box-sizing: border-box; - - display: flex; - flex-direction: row; - - background: transparent; - border: 1px solid var(--dim-color); - border-radius: 3px; -} - -.wiki-search-label::before { - display: inline-block; - padding-left: 3px; - padding-right: 3px; - margin-right: 3px; - width: 1.8em; - text-align: center; - content: '\1f50d\fe0e'; -} - -.wiki-search-input { - background: transparent; - border: transparent; - color: inherit; - flex-grow: 1; -} - -.wiki-search-input::-webkit-search-cancel-button { - filter: grayscale(1) invert(1); -} - -.wiki-search-label.disabled { - opacity: 0.6; -} - -.wiki-search-label.disabled, -.wiki-search-input[disabled] { - cursor: not-allowed; -} - -.wiki-search-label:not(.disabled):hover, -.wiki-search-label:focus-within { - background: var(--light-ghost-color); -} - -.wiki-search-label:focus-within { - border-color: var(--primary-color); -} - -.wiki-search-label:focus-within::before { - opacity: 0.7; -} - -.wiki-search-input:focus { - border: none; - outline: none; -} - -.wiki-search-input::placeholder { - color: var(--primary-color); - font-style: oblique; -} - -.wiki-search-input:focus::placeholder { - color: var(--dim-color); -} - -.wiki-search-sidebar-box hr { - border-color: var(--primary-color); - border-style: none none dotted none; - margin-top: 3px; - margin-bottom: 3px; -} - -.wiki-search-progress-container { - padding: 2px 6px 4px 6px; - display: flex; - flex-direction: row; -} - -.wiki-search-progress-label { - font-size: 0.9em; - font-style: oblique; - cursor: default; - margin-right: 1ch; -} - -.wiki-search-progress-bar { - flex-grow: 1; -} - -.wiki-search-failed-container { - padding: 2px 3px 4px 6px; -} - -.wiki-search-failed-container p { - margin: 0; -} - -.wiki-search-results-container { - margin-bottom: 0; - padding: 2px; -} - -.wiki-search-no-results { - font-size: 0.9em; - padding: 2px 3px 4px 6px; - cursor: default; -} - -.wiki-search-result { - position: relative; - display: flex; - padding: 4px 3px 4px 6px; -} - -.wiki-search-result:hover { - text-decoration: none !important; -} - -.wiki-search-result::before { - content: ''; - position: absolute; - top: -2px; - bottom: -2px; - left: 0; - right: 0; - - border: 1.5px solid var(--primary-color); - border-radius: 4px; - display: none; -} - -.wiki-search-result.current-result { - background: var(--light-ghost-color); - border-top: 1px solid var(--dim-color); - border-bottom: 1px solid var(--dim-color); -} - -.wiki-search-result:hover::before, -.wiki-search-result:focus::before { - display: block; - background: var(--light-ghost-color); -} - -.wiki-search-result.current-result:hover { - background: none; - border-color: transparent; -} - -.wiki-search-result.current-result:hover .wiki-search-current-result-text { - filter: saturate(0.8) brightness(1.4); -} - -.wiki-search-result-text-area { - align-self: center; - flex-grow: 1; - min-width: 0; - overflow-wrap: break-word; - padding-bottom: 2px; -} - -.wiki-search-result-name { - margin-right: 0.25em; -} - -.wiki-search-result:hover .wiki-search-result-name { - text-decoration: underline; -} - -.wiki-search-current-result-text, -.wiki-search-result-kind { - font-style: oblique; - opacity: 0.9; - display: inline-block; -} - -.wiki-search-result-image-container { - align-self: flex-start; - flex-shrink: 0; - margin-right: 6px; - border-radius: 2px; - overflow: hidden; - - background-color: var(--deep-color); - border: 2px solid var(--deep-color); -} - -.wiki-search-results:not(:has(.wiki-search-result-image)) .wiki-search-result-image-container { - display: none; -} - -.wiki-search-result-image, -.wiki-search-result-image-placeholder { - display: block; - width: 1.8em; - height: 1.8em; - aspect-ratio: 1 / 1; - border-radius: 1.5px; -} - -.wiki-search-result-image-placeholder { - background-color: #0004; - box-shadow: 0 1px 3px -1px #0008 inset; -} - -.wiki-search-result-image.has-warning { - filter: blur(2px) brightness(0.8); -} - -.wiki-search-end-search-line { - text-align: center; - margin-top: 6px; - margin-bottom: 2px; -} - -.wiki-search-end-search-line a { - display: inline-block; - font-style: oblique; - opacity: 0.9; - padding: 3px 6px 4px 6px; - border-radius: 4px; - border: 1.5px solid transparent; -} - -.wiki-search-end-search-line a:hover { - opacity: 1; - background: var(--light-ghost-color); - border-color: var(--deep-color); -} - -#content { - overflow-wrap: anywhere; -} - -footer { - text-align: center; - font-style: oblique; -} - -.footer-localization-links > span:not(:last-child)::after { - content: " \00b7 "; - font-weight: 800; -} - -/* Design & Appearance - Content elements */ - -a { - color: var(--primary-color); - text-decoration: none; -} - -a:hover { - text-decoration: underline; - text-decoration-style: solid !important; -} - -a.current { - font-weight: 800; -} - -a.series { - font-style: oblique; -} - -a:not([href]) { - cursor: default; -} - -a:not([href]):hover { - text-decoration: none; -} - -.external-link:not(.from-content) { - white-space: nowrap; -} - -.external-link.indicate-external::after { - content: '\00a0➚'; - font-style: normal; -} - -.external-link.indicate-external:hover::after { - color: white; -} - -.external-link .normal-content { - color: white; -} - -.nav-link { - display: inline-block; -} - -.nav-main-links .nav-link.current > span.nav-link-content > a { - font-weight: 800; -} - -.nav-main-links .nav-link-accent { - display: inline-block; -} - -.nav-links-index .nav-link:not(:first-child)::before, -.nav-links-groups .nav-link:not(:first-child)::before { - content: "\0020\00b7\0020"; - font-weight: 800; -} - -.nav-links-hierarchical .nav-link:not(:first-child)::before { - content: "\0020/\0020"; -} - -.series-nav-link { - display: inline-block; -} - -.series-nav-link:not(:first-child)::before { - content: "\00a0»\00a0"; - font-weight: normal; -} - -.series-nav-link:not(:last-child)::after { - content: ",\00a0"; -} - -.series-nav-link + .series-nav-link::before { - content: ""; -} - -.dot-switcher > span:not(:first-child)::before { - content: "\0020\00b7\0020"; - font-weight: 800; -} - -.dot-switcher > span.current { - font-weight: 800; -} - -.dot-switcher.intrapage > span:not(.current) a { - text-decoration: underline; - text-decoration-style: dotted; -} - -.dot-switcher.intrapage > span.current a { - /* Keeping cursor: pointer (the default) is intentional here. */ - text-decoration: none !important; -} - -#secondary-nav { - text-align: center; - - /* Default to visible. It'll automatically be hidden - * in layouts where the sidebar is visible. - */ - display: block; -} - -#secondary-nav.album-secondary-nav.with-previous-next { - display: flex; - justify-content: space-around; - padding-left: 7.5% !important; - padding-right: 7.5% !important; - flex-wrap: wrap; - line-height: 1.4; -} - -#secondary-nav.album-secondary-nav.with-previous-next .group-with-series { - width: 100%; -} - -#secondary-nav.album-secondary-nav.with-previous-next > * { - margin-left: 5px; - margin-right: 5px; -} - -#secondary-nav.album-secondary-nav .dot-switcher { - white-space: nowrap; -} - -.inert-previous-next-link { - opacity: 0.7; -} - -.nowrap { - white-space: nowrap; -} - -.blockwrap, .chunkwrap { - display: inline-block; -} - -.text-with-tooltip { - position: relative; -} - -.text-with-tooltip .text-with-tooltip-interaction-cue { - text-decoration: underline; - text-decoration-style: dotted; -} - -.text-with-tooltip > .hoverable:hover .text-with-tooltip-interaction-cue, -.text-with-tooltip > .hoverable.has-visible-tooltip .text-with-tooltip-interaction-cue { - text-decoration-style: wavy !important; -} - -.text-with-tooltip.datetimestamp .text-with-tooltip-interaction-cue, -.text-with-tooltip.missing-duration .text-with-tooltip-interaction-cue, -.text-with-tooltip.commentary-date .text-with-tooltip-interaction-cue, -.text-with-tooltip.wiki-edits .text-with-tooltip-interaction-cue { - cursor: default; -} - -.text-with-tooltip.missing-duration > .hoverable { - opacity: 0.5; -} - -.text-with-tooltip.missing-duration > .hoverable:hover, -.text-with-tooltip.missing-duration > .hoverable.has-visible-tooltip { - opacity: 1; -} - -.text-with-tooltip.missing-duration .text-with-tooltip-interaction-cue { - text-decoration: none !important; -} - -.tooltip { - position: absolute; - z-index: 3; - left: -10px; - top: calc(1em + 1px); - display: none; -} - -li:not(:first-child:last-child) .tooltip, -.offset-tooltips > :not(:first-child:last-child) .tooltip { - left: 14px; -} - -.tooltip-content { - display: block; - - background: var(--bg-black-color); - border: 1px dotted var(--primary-color); - border-radius: 6px; - - -webkit-backdrop-filter: - brightness(1.5) saturate(1.4) blur(4px); - - backdrop-filter: - brightness(1.5) saturate(1.4) blur(4px); - - box-shadow: - 0 3px 4px 4px #000000aa, - 0 -2px 4px -2px var(--primary-color) inset; - - text-indent: 0; -} - -.contribution-tooltip { - padding: 3px 6px 6px 6px; - left: -34px; -} - -.datetimestamp-tooltip, -.missing-duration-tooltip, -.commentary-date-tooltip { - padding: 3px 4px 2px 2px; - left: -10px; -} - -.thing-name-tooltip, -.wiki-edits-tooltip { - padding: 3px 4px 2px 2px; - left: -6px !important; -} - -.wiki-edits-tooltip { - font-size: 0.85em; -} - -/* Terrifying? - * https://stackoverflow.com/a/64424759/4633828 - */ -.thing-name-tooltip { margin-right: -120px; } -.wiki-edits-tooltip { margin-right: -200px; } - -.contribution-tooltip .tooltip-content { - padding: 6px 2px 2px 2px; - - -webkit-user-select: none; - user-select: none; - - cursor: default; - - display: grid; - - grid-template-columns: - [icon-start] 26px [icon-end handle-start] auto [handle-end platform-start] auto [platform-end]; -} - -.contribution-tooltip .external-link { - display: grid; - grid-column-start: icon-start; - grid-column-end: handle-end; - grid-template-columns: subgrid; - - height: 1.4em; -} - -.contribution-tooltip .chronology-link { - display: grid; - grid-column-start: icon-start; - grid-column-end: handle-end; - grid-template-columns: subgrid; - - height: 1.2em; -} - -.contribution-tooltip .external-icon, -.contribution-tooltip .chronology-symbol { - grid-column-start: icon-start; - grid-column-end: icon-end; -} - -.contribution-tooltip .external-icon svg { - width: 18px; - height: 18px; - top: -0.1em; -} - -.contribution-tooltip .chronology-symbol { - text-align: center; -} - -.contribution-tooltip .external-handle, -.contribution-tooltip .chronology-text { - grid-column-start: handle-start; - grid-column-end: handle-end; - - width: max-content; - max-width: 200px; - - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - -.contribution-tooltip .external-handle { - padding-right: 8px; -} - -.contribution-tooltip .chronology-text { - padding-right: 6px; -} - -.contribution-tooltip .chronology-text, -.contribution-tooltip .chronology-info { - font-size: 0.85em; -} - -.contribution-tooltip .tooltip-divider { - grid-column-start: icon-start; - grid-column-end: platform-end; - - border-top: 1px dotted var(--primary-color); - margin-top: 3px; - margin-bottom: 4px; -} - -.contribution-tooltip .external-platform, -.contribution-tooltip .chronology-info { - display: none; - - grid-column-start: platform-start; - grid-column-end: platform-end; - - --external-platform-opacity: 0.8; - opacity: 0.8; - padding-right: 4px; - - white-space: nowrap; -} - -.contribution-tooltip.show-info .external-platform, -.contribution-tooltip.show-info .chronology-info { - display: inline; - animation: external-platform 0.2s forwards linear; -} - -@keyframes external-platform { - from { - opacity: 0; - } - - to { - opacity: var(--external-platform-opacity); - } -} - -.contribution-tooltip .external-link:hover, -.contribution-tooltip .chronology-link:hover { - filter: brightness(1.4); - text-decoration: none; -} - -.contribution-tooltip .external-link:hover .external-handle, -.contribution-tooltip .chronology-link:hover .chronology-text { - text-decoration: underline; -} - -.contribution-tooltip .external-link:hover + .external-platform, -.contribution-tooltip .chronology-link:hover + .chronology-info { - --external-platform-opacity: 1; - text-decoration: underline; - text-decoration-color: #ffffffaa; -} - -.datetimestamp-tooltip .tooltip-content, -.missing-duration-tooltip .tooltip-content, -.commentary-date-tooltip .tooltip-content { - padding: 5px 6px; - white-space: nowrap; - font-size: 0.9em; -} - -.thing-name-tooltip .tooltip-content, -.wiki-edits-tooltip .tooltip-content { - padding: 3px 4.5px; -} - -.external-icon { - display: inline-block; - padding: 0 3px; - width: 24px; - height: 1em; - position: relative; -} - -.external-icon svg { - width: 24px; - height: 24px; - top: -0.25em; - position: absolute; - fill: var(--primary-color); -} - -.rerelease, -.other-group-accent { - opacity: 0.7; - font-style: oblique; -} - -.other-group-accent { - white-space: nowrap; -} - -.other-group-accent a { - color: var(--page-primary-color); -} - -s.spoiler { - display: inline-block; - color: transparent; - text-decoration: underline; - text-decoration-color: white; - text-decoration-style: dashed; - text-decoration-skip: none; - text-decoration-skip-ink: none; -} - -s.spoiler::selection { - color: black; - background: white; -} - -s.spoiler::-moz-selection { - color: black; - background: white; -} - -progress { - accent-color: var(--primary-color); -} - -.content-columns { - columns: 2; -} - -.content-columns .column { - break-inside: avoid; -} - -.content-columns .column h2 { - margin-top: 0; - font-size: 1em; -} - -p .current { - font-weight: 800; -} - -#cover-art-container { - font-size: 0.8em; - border: 2px solid var(--primary-color); - box-shadow: - 0 2px 14px -6px var(--primary-color), - 0 0 12px 12px #00000080; - - border-radius: 0 0 4px 4px; - background: var(--bg-black-color); - - -webkit-backdrop-filter: blur(3px); - backdrop-filter: blur(3px); -} - -#cover-art-container:has(.image-details), -#cover-art-container.has-image-details { - border-radius: 0 0 6px 6px; -} - -#cover-art-container:not(:has(.image-details)), -#cover-art-container:not(.has-image-details) { - /* Hacky: `overflow: hidden` hides tag tooltips, so it can't be applied - * if we've got tags/details visible. But it's okay, because we only - * need to apply it if it *doesn't* - that's when the rounded border - * of #cover-art-container needs to cut off its child image-container - * (which has a background that otherwise causes sharp corners). - */ - overflow: hidden; -} - -#cover-art-container .image-container { - /* Border is handled on the cover-art-container. */ - border: none; - border-radius: 0; -} - -#cover-art-container .image-details { - border-top-color: var(--deep-color); -} - -#cover-art-container .image-details + .image-details { - border-top-color: var(--primary-color); -} - -#cover-art-container .image { - display: block; - width: 100%; - height: 100%; -} - -.image-details { - display: block; - - margin-top: 0; - margin-bottom: 0; - - /* Styles below only apply for first image-details. */ - - margin-left: 0; - margin-right: 0; - padding-left: 9px; - padding-right: 9px; - - padding-top: 6px; - padding-bottom: 4px; - - border-top: 1px dashed var(--dim-color); -} - -.image-details + .image-details { - display: block; - - margin-left: 6px; - margin-right: 6px; - padding-left: 3px; - padding-right: 3px; - - padding-top: 4px; - padding-bottom: 4px; - - border-top: 1px dotted var(--primary-color); -} - -.image-details:last-child { - margin-bottom: 2px; -} - -ul.image-details.art-tag-details li { - display: inline-block; -} - -ul.image-details.art-tag-details li:not(:last-child)::after { - content: " \00b7 "; -} - -.image-details.non-unique-details { - font-style: oblique; -} - -p.image-details.illustrator-details { - text-align: center; - font-style: oblique; -} - -#artist-commentary.first-entry-is-dated { - clear: right; -} - -.commentary-entry-heading { - display: flex; - flex-direction: row; - - margin-left: 15px; - padding-left: 5px; - max-width: 625px; - padding-bottom: 0.2em; - - border-bottom: 1px solid var(--dim-color); -} - -.commentary-entry-heading-text { - flex-grow: 1; - padding-left: 1.25ch; - text-indent: -1.25ch; -} - -.commentary-entry-accent { - font-style: oblique; -} - -.commentary-entry-heading .commentary-date { - flex-shrink: 0; - - margin-left: 0.75ch; - align-self: flex-end; - - padding-left: 0.5ch; - padding-right: 0.25ch; -} - -.commentary-entry-heading .hoverable { - box-shadow: 1px 2px 6px 5px #04040460; -} - -.commentary-entry-body summary { - list-style-position: outside; -} - -.commentary-entry-body summary > span { - color: var(--primary-color); -} - -.commentary-art { - float: right; - width: 30%; - max-width: 250px; - margin: 15px 0 10px 20px; - - /* This !important is unfortunate, but it's necessary - * even if the rule itself is placed lower, because this - * is a relatively low-priority selector compared to - * others that alter image shadows. - */ - box-shadow: 0 0 4px 5px rgba(0, 0, 0, 0.25) !important; -} - -.js-hide, -.js-show-once-data, -.js-hide-once-data { - display: none; -} - -.content-image-container, -.content-video-container { - margin-top: 1em; - margin-bottom: 1em; -} - -.content-image-container.align-center, -.content-video-container.align-center { - text-align: center; - margin-top: 1.5em; - margin-bottom: 1.5em; -} - -a.align-center, img.align-center { - display: block; - margin-left: auto; - margin-right: auto; -} - -center { - margin-top: 1em; - margin-bottom: 1em; -} - -.content-image { - display: inline-block !important; -} - -.image-link { - display: block; - overflow: hidden; -} - -.image-link:focus { - outline: 3px double white; -} - -.image-link:focus:not(:focus-visible) { - outline: none; -} - -.image-link .image { - display: block; - max-width: 100%; - height: auto; -} - -.square .image-link { - width: 100%; - height: 100%; -} - -.square .image { - width: 100%; - height: 100%; -} - -h1 { - font-size: 1.5em; -} - -#content li { - margin-bottom: 4px; -} - -#content li i { - white-space: nowrap; -} - -#content.top-index h1, -#content.flash-index h1 { - text-align: center; - font-size: 2em; -} - -html[data-url-key="localized.home"] #content h1 { - text-align: center; - font-size: 2.5em; -} - -#content.flash-index h2 { - text-align: center; - font-size: 2.5em; - font-variant: small-caps; - font-style: oblique; - margin-bottom: 0; - text-align: center; - width: 100%; -} - -#content.top-index h2 { - text-align: center; - font-size: 2em; - font-weight: normal; - margin-bottom: 0.25em; -} - -#content.top-index.has-subtitle h1 { - margin-bottom: 0.35em; -} - -#content.top-index h2.page-subtitle { - font-size: 1.8em; - margin-top: 0.35em; - margin-bottom: 0.5em; -} - -.quick-info { - text-align: center; - padding-left: calc(var(--responsive-padding-ratio) * 100%); - padding-right: calc(var(--responsive-padding-ratio) * 100%); - line-height: 1.25em; -} - -ul.quick-info { - list-style: none; - padding-left: 0; - padding-right: 0; -} - -ul.quick-info li { - display: inline-block; -} - -ul.quick-info li:not(:last-child)::after { - content: " \00b7 "; - font-weight: 800; -} - -.carousel-container + .quick-info, -.carousel-container + .quick-description { - margin-top: 25px; -} - -.quick-description:not(.has-external-links-only) { - --clamped-padding-ratio: max(var(--responsive-padding-ratio), 0.06); - margin-left: auto; - margin-right: auto; - padding-left: calc(0.40 * var(--clamped-padding-ratio) * 100%); - padding-right: calc(0.40 * var(--clamped-padding-ratio) * 100%); - max-width: 500px; - - padding-top: 0.25em; - padding-bottom: 0.75em; - border-left: 1px solid var(--dim-color); - border-right: 1px solid var(--dim-color); - line-height: 1.25em; -} - -.quick-description.has-external-links-only { - padding-left: 12%; - padding-right: 12%; -} - -.quick-description.has-content-only { - padding-bottom: 0.5em; -} - -.quick-description p { - text-align: center; -} - -.quick-description > blockquote { - margin-left: 0 !important; -} - -.quick-description .description-content.long hr ~ p { - text-align: left; -} - -.quick-description > .description-content :first-child { - margin-top: 0; -} - -.quick-description > .quick-description-actions, -.quick-description.has-content-only .description-content :last-child { - margin-bottom: 0; -} - -.quick-description:not(.collapsed) .description-content.short, -.quick-description:not(.collapsed) .quick-description-actions.when-collapsed, -.quick-description:not(.expanded) .description-content.long, -.quick-description:not(.expanded) .quick-description-actions.when-expanded { - display: none; -} - -.quick-description .quick-description-actions .expand-link, -.quick-description .quick-description-actions .collapse-link { - text-decoration: underline; - text-decoration-style: dotted; -} - -#intro-menu { - margin: 24px 0; - padding: 10px; - background-color: #222222; - text-align: center; - border: 1px dotted var(--primary-color); - border-radius: 2px; -} - -#intro-menu p { - margin: 12px 0; -} - -#intro-menu a { - margin: 0 6px; -} - -li .by { - font-style: oblique; - max-width: 600px; -} - -li .by a { - display: inline-block; -} - -p code { - font-size: 0.95em; - font-family: "courier new", monospace; - font-weight: 800; - line-height: 1.1; -} - -#content blockquote { - margin-left: 40px; - max-width: 600px; - margin-right: 0; -} - -#content blockquote blockquote { - margin-left: 10px; - padding-left: 10px; - margin-right: 20px; - border-left: dotted 1px; - padding-top: 6px; - padding-bottom: 6px; -} - -#content blockquote blockquote > :first-child { - margin-top: 0; -} - -#content blockquote blockquote > :last-child { - margin-bottom: 0; -} - -#content blockquote h2 { - font-size: 1em; - font-weight: 800; -} - -#content blockquote h3 { - font-size: 1em; - font-weight: normal; - font-style: oblique; -} - -main { - --responsive-padding-ratio: 0.10; -} - -main.long-content { - --long-content-padding-ratio: var(--responsive-padding-ratio); -} - -main.long-content .main-content-container, -main.long-content > h1 { - padding-left: calc(var(--long-content-padding-ratio) * 100%); - padding-right: calc(var(--long-content-padding-ratio) * 100%); -} - -dl dt { - padding-left: 40px; - max-width: 600px; -} - -dl dt { - /* Heads up, this affects the measurement - * for dl dt which are .content-heading! - */ - margin-bottom: 2px; -} - -dl dd { - margin-bottom: 1em; -} - -dl ul, -dl ol { - margin-top: 0; - margin-bottom: 0; -} - -ul > li.has-details { - list-style-type: none; - margin-left: -17px; -} - -.album-group-list dt, -.group-series-list dt { - font-style: oblique; - padding-left: 0; -} - -.album-group-list dd, -.group-series-list dd { - margin-left: 0; -} - -.album-group-list blockquote { - max-width: 540px; - margin-bottom: 9px; - margin-top: 3px; -} - -.album-group-list blockquote p:first-child { - margin-top: 0; -} - -.album-group-list blockquote p:last-child { - margin-bottom: 0; -} - -.group-chronology-link, -.series-chronology-link { - font-style: oblique; -} - -.group-chronology-link a, -.series-chronology-link a { - font-style: normal; -} - -.group-view-switcher { - margin-left: 1ch; -} - -#content hr { - border: 1px inset #808080; - width: 100%; -} - -#content hr.split::before { - content: "(split)"; - color: #808080; -} - -#content hr.split { - position: relative; - overflow: hidden; - border: none; -} - -#content hr.split::after { - display: inline-block; - content: ""; - border: 1px inset #808080; - width: 100%; - position: absolute; - top: 50%; - margin-top: -2px; - margin-left: 10px; -} - -li > ul { - margin-top: 5px; -} - -.additional-files-list { - padding-left: 0; -} - -.additional-files-list > li { - list-style-type: none; -} - -.additional-files-list summary { - /* Sorry, Safari! - * https://bugs.webkit.org/show_bug.cgi?id=157323 - */ - list-style-position: outside; - margin-left: 40px; -} - -.additional-files-list details ul { - margin-left: 40px; - margin-top: 2px; - margin-bottom: 10px; -} - -.additional-files-list .entry-description { - list-style-type: none; - max-width: 540px; - - /* This should be margin-bottom, but cascading rules - * cause some awkwardness - `#content li` takes precedence. - */ - padding-bottom: 3px; -} - -.group-contributions-table { - display: inline-block; -} - -.group-contributions-table .group-contributions-row { - display: flex; - justify-content: space-between; -} - -.group-contributions-table .group-contributions-metrics { - margin-left: 1.5ch; - white-space: nowrap; -} - -.group-contributions-sorted-by-count:not(.visible), -.group-contributions-sorted-by-duration:not(.visible) { - display: none; -} - -.group-contributions-sort-button { - text-decoration: underline; - text-decoration-style: dotted; -} - -html[data-url-key="localized.albumCommentary"] li.no-commentary { - opacity: 0.7; -} - -html[data-url-key="localized.albumCommentary"] .content-heading-main-title { - margin-right: 0.25em; -} - -html[data-url-key="localized.albumCommentary"] .content-heading-accent { - font-weight: normal; - font-style: oblique; - font-size: 0.9rem; - display: inline-block; -} - -html[data-url-key="localized.albumCommentary"] p.track-info { - margin-left: 20px; -} - -html[data-url-key="localized.groupInfo"] .by a { - color: var(--page-primary-color); -} - -html[data-url-key="localized.listing"][data-url-value0="random"] #data-loading-line, -html[data-url-key="localized.listing"][data-url-value0="random"] #data-loaded-line, -html[data-url-key="localized.listing"][data-url-value0="random"] #data-error-line { - display: none; -} - -html[data-url-key="localized.listing"][data-url-value0="random"] #content a:not([href]) { - opacity: 0.7; -} - -html[data-url-key="localized.newsEntry"] .read-another-links { - font-style: oblique; - font-size: 0.9em; -} - -/* Additional names (heading and box) */ - -h1 a[href="#additional-names-box"] { - color: inherit; - text-decoration: underline; - text-decoration-style: dotted; -} - -h1 a[href="#additional-names-box"]:hover { - text-decoration-style: solid; -} - -#additional-names-box { - --custom-scroll-offset: calc(0.5em - 2px); - - margin: 1em 0 1em -10px; - padding: 15px 20px 10px 20px; - width: max-content; - max-width: min(60vw, 600px); - - border: 1px dotted var(--primary-color); - border-radius: 6px; - - background: - linear-gradient(var(--bg-color), var(--bg-color)), - linear-gradient(#000000bb, #000000bb), - var(--primary-color); - - box-shadow: 0 -2px 6px -1px var(--dim-color) inset; - - display: none; -} - -#additional-names-box > :first-child { margin-top: 0; } -#additional-names-box > :last-child { margin-bottom: 0; } - -#additional-names-box p { - padding-left: 10px; - padding-right: 10px; - margin-bottom: 0; - font-style: oblique; -} - -#additional-names-box ul { - padding-left: 10px; - margin-top: 0.5em; -} - -#additional-names-box li .additional-name { - margin-right: 0.25em; -} - -#additional-names-box li .additional-name .content-image { - margin-bottom: 0.25em; - margin-top: 0.5em; -} - -#additional-names-box li .accent { - opacity: 0.8; -} - -#additional-names-box li .additional-name > img { - vertical-align: text-bottom; -} - -/* Images */ - -.image-container { - display: block; - box-sizing: border-box; - position: relative; - height: 100%; - overflow: hidden; - - background-color: var(--dim-color); - border: 2px solid var(--primary-color); - border-radius: 0; - box-shadow: 0 2px 4px -2px var(--bg-black-color) inset; - - text-align: left; - color: white; -} - -/* Videos (in content) get a lite version of image-container. */ -.content-video-container { - width: min-content; - background-color: var(--dark-color); - border: 2px solid var(--primary-color); - border-radius: 2.5px 2.5px 3px 3px; - padding: 5px; -} - -.image-text-area { - position: absolute; - top: 0; - left: 0; - bottom: 0; - right: 0; - display: flex; - align-items: center; - justify-content: center; - text-align: center; - padding: 5px 15px; - - background: rgba(0, 0, 0, 0.65); - box-shadow: 0 0 5px rgba(0, 0, 0, 0.5) inset; - - line-height: 1.35em; - color: var(--primary-color); - font-style: oblique; - text-shadow: 0 2px 5px rgba(0, 0, 0, 0.75); -} - -.image-outer-area { - width: 100%; - height: 100%; - padding: 5px; - box-sizing: border-box; -} - -.image-link { - border-bottom: 1px solid #ffffff03; - border-radius: 2.5px 2.5px 3px 3px; - box-shadow: - 0 1px 8px -3px var(--bg-black-color); -} - -.image-inner-area { - position: relative; - width: 100%; - height: 100%; -} - -.image-link .image-inner-area { - /* Jankily fix a rendering issue with border-radius on Safari. - * The `-webkit-` prefix is only to keep this from applying on - * other browsers (well, Firefox), where it doesn't *break* - * anything, but also isn't necessary. - */ - -webkit-transform: translateZ(0); -} - -img { - object-fit: cover; -} - -.image-inner-area::after { - content: ""; - display: block; - position: absolute; - pointer-events: none; - top: 0; - left: 0; - right: 0; - bottom: 0; - border-bottom-left-radius: 0.5px; - opacity: 0.035; - box-shadow: - 6px -6px 2px -4px white inset; -} - -img.pixelate, .pixelate img, -video.pixelate, .pixelate video { - image-rendering: crisp-edges; -} - -.reveal-text-container { - position: absolute; - top: 15px; - left: 10px; - right: 10px; - bottom: 10px; - display: flex; - flex-direction: column; - justify-content: center; -} - -.grid-item .reveal-text { - font-size: 0.9em; -} - -.reveal-text { - color: white; - text-align: center; - font-weight: bold; - padding-bottom: 0.5em; - font-size: 0.8rem; -} - -.reveal-symbol { - display: inline-block; - width: 1em; - height: 1em; - margin-bottom: 0.1em; - - font-size: 1.6em; - opacity: 0.8; -} - -.reveal-interaction { - opacity: 0.8; - text-decoration: underline; - text-decoration-style: dotted; -} - -.reveal .image { - opacity: 0.7; - filter: blur(20px) brightness(0.7); -} - -.reveal .image.reveal-thumbnail { - position: absolute; - top: 0; - left: 0; - image-rendering: pixelated; -} - -.reveal.has-reveal-thumbnail:not(.revealed) .image:not(.reveal-thumbnail) { - /* Keep the main image as part of the box model. - * It's what actually defines the dimensions of the - * image-container, so those dimensions never shift - * once the image is actually revealed. - */ - visibility: hidden; -} - -.reveal.revealed.has-reveal-thumbnail .image.reveal-thumbnail { - display: none !important; -} - -.reveal.revealed .image { - filter: none; - opacity: 1; -} - -.reveal.revealed .reveal-text-container { - display: none; -} - -.reveal:not(.revealed) .image-outer-area > * { - --reveal-border-radius: 6px; - position: relative; - overflow: hidden; - border-radius: var(--reveal-border-radius); -} - -.reveal:not(.revealed) .image-outer-area > *::after { - content: ""; - position: absolute; - box-sizing: border-box; - top: 0; - left: 0; - bottom: 0; - right: 0; - border: 1px dotted var(--primary-color); - border-radius: var(--reveal-border-radius); - pointer-events: none; - - /* By an awkward DOM intersection, this element might be - * .image-inner-area::after, which is already styled with - * a slight visual effect. Guarantee that the properties - * set to that end are overwritten, and fully co-opt it - * to serve as the interaction cue instead. - */ - box-shadow: none; - opacity: 1; -} - -.reveal:not(.revealed) .image-inner-area { - background: var(--deep-color); -} - -.reveal:not(.revealed) .image-outer-area > *:hover::after { - border-style: solid; - box-shadow: 0 0 0 1.5px #00000099 inset; -} - -.reveal:not(.revealed) .image-outer-area > *:hover .image { - filter: blur(20px) brightness(0.6); - opacity: 0.6; -} - -.reveal:not(.revealed) .image-outer-area > *:hover .reveal-interaction { - text-decoration-style: solid; -} - -.image-container.has-link:not(.no-image-preview) { - background: var(--deep-color); - box-shadow: none; - border-radius: 0 0 4px 4px; -} - -.sidebar .image-container { - max-width: 350px; -} - -/* Grid listings */ - -.grid-listing { - display: flex; - flex-wrap: wrap; - justify-content: center; - align-items: flex-start; - padding: 5px 15px; -} - -.grid-item { - font-size: 0.9em; -} - -.grid-item { - display: inline-block; - text-align: center; - background-color: #111111; - border: 1px dotted var(--primary-color); - border-radius: 2px; - padding: 5px; - margin: 10px; -} - -.grid-item .image-container { - width: 100%; -} - -.grid-item .image-inner-area { - border-radius: 0; - box-shadow: none; -} - -.grid-item .image-inner-area::after { - box-shadow: none; -} - -.grid-item .image { - width: 100%; - height: 100% !important; - margin-top: auto; - margin-bottom: auto; -} - -.grid-item:hover { - text-decoration: none; -} - -.grid-actions .grid-item:hover { - text-decoration: underline; -} - -.grid-item > span { - display: block; - overflow-wrap: break-word; - hyphens: auto; -} - -.grid-item > span:not(:first-child) { - margin-top: 2px; -} - -.grid-item > span:first-of-type { - margin-top: 6px; -} - -.grid-item > span:not(:first-of-type) { - font-size: 0.9em; - opacity: 0.8; -} - -.grid-item:hover > span:first-of-type { - text-decoration: underline; -} - -.grid-listing > .grid-item { - flex: 1 25%; - max-width: 200px; -} - -.grid-actions { - display: flex; - flex-direction: row; - margin: 15px; - align-self: center; - flex-wrap: wrap; - justify-content: center; -} - -.grid-actions > .grid-item { - flex-basis: unset !important; - margin: 5px; - width: 120px; - --primary-color: inherit !important; - --dim-color: inherit !important; -} - -/* Carousel */ - -.carousel-container { - --carousel-tile-min-width: 120px; - --carousel-row-count: 3; - --carousel-column-count: 6; - - position: relative; - overflow: hidden; - margin: 20px 0 5px 0; - padding: 8px 0; -} - -.carousel-container::before { - content: ""; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: -20; - background-color: var(--dim-color); - filter: brightness(0.6); -} - -.carousel-container::after { - content: ""; - pointer-events: none; - position: absolute; - top: 0; - left: 0; - right: 0; - bottom: 0; - border: 1px solid var(--primary-color); - border-radius: 4px; - z-index: 40; - box-shadow: - inset 20px 2px 40px var(--shadow-color), - inset -20px -2px 40px var(--shadow-color); -} - -.carousel-container:hover .carousel-grid { - animation-play-state: running; -} - -html[data-url-key="localized.home"] .carousel-container { - --carousel-tile-size: 140px; -} - -.carousel-container[data-carousel-rows="1"] { --carousel-row-count: 1; } -.carousel-container[data-carousel-rows="2"] { --carousel-row-count: 2; } -.carousel-container[data-carousel-rows="3"] { --carousel-row-count: 3; } -.carousel-container[data-carousel-columns="4"] { --carousel-column-count: 4; } -.carousel-container[data-carousel-columns="5"] { --carousel-column-count: 5; } -.carousel-container[data-carousel-columns="6"] { --carousel-column-count: 6; } - -.carousel-grid:nth-child(2), -.carousel-grid:nth-child(3) { - position: absolute; - top: 8px; - left: 0; - right: 0; -} - -.carousel-grid:nth-child(2) { - animation-name: carousel-marquee2; -} - -.carousel-grid:nth-child(3) { - animation-name: carousel-marquee3; -} - -@keyframes carousel-marquee1 { - 0% { - transform: translateX(-100%) translateX(70px); - } - - 100% { - transform: translateX(-200%) translateX(70px); - } -} - -@keyframes carousel-marquee2 { - 0% { - transform: translateX(0%) translateX(70px); - } - - 100% { - transform: translateX(-100%) translateX(70px); - } -} - -@keyframes carousel-marquee3 { - 0% { - transform: translateX(100%) translateX(70px); - } - - 100% { - transform: translateX(0%) translateX(70px); - } -} - -.carousel-grid { - /* Thanks to: https://css-tricks.com/an-auto-filling-css-grid-with-max-columns/ */ - --carousel-gap-count: calc(var(--carousel-column-count) - 1); - --carousel-total-gap-width: calc(var(--carousel-gap-count) * 10px); - --carousel-calculated-tile-max-width: calc((100% - var(--carousel-total-gap-width)) / var(--carousel-column-count)); - - display: grid; - grid-template-columns: repeat(auto-fill, minmax(max(var(--carousel-tile-min-width), var(--carousel-calculated-tile-max-width)), 1fr)); - grid-template-rows: repeat(var(--carousel-row-count), auto); - grid-auto-flow: dense; - grid-auto-rows: 0; - overflow: hidden; - margin: auto; - - transform: translateX(0); - animation: carousel-marquee1 40s linear infinite; - animation-play-state: paused; - z-index: 5; -} - -.carousel-item { - display: inline-block; - margin: 0; - flex: 1 1 150px; - padding: 3px; - border-radius: 10px; - filter: brightness(0.8); -} - -.carousel-item .image-container { - border: none; - border-radius: 5px; -} - -.carousel-item .image-outer-area { - padding: 0; -} - -.carousel-item .image-inner-area::after { - box-shadow: none; -} - -.carousel-item .image { - width: 100%; - height: 100%; - margin-top: auto; - margin-bottom: auto; -} - -.carousel-item:hover { - filter: brightness(1); - background: var(--dim-color); -} - -/* Info card */ - -#info-card-container { - position: absolute; - - left: 0; - right: 10px; - - pointer-events: none; /* Padding area shouldn't 8e interactive. */ - display: none; -} - -#info-card-container.show, -#info-card-container.hide { - display: flex; -} - -#info-card-container > * { - flex-basis: 400px; -} - -#info-card-container.show { - animation: 0.2s linear forwards info-card-show; - transition: top 0.1s, left 0.1s; -} - -#info-card-container.hide { - animation: 0.2s linear forwards info-card-hide; -} - -@keyframes info-card-show { - 0% { - opacity: 0; - margin-top: -5px; - } - - 100% { - opacity: 1; - margin-top: 0; - } -} - -@keyframes info-card-hide { - 0% { - opacity: 1; - margin-top: 0; - } - - 100% { - opacity: 0; - margin-top: 5px; - display: none !important; - } -} - -.info-card-decor { - padding-left: 3ch; - border-top: 1px solid white; -} - -.info-card { - background-color: black; - color: white; - - border: 1px dotted var(--primary-color); - border-radius: 3px; - box-shadow: 0 5px 5px black; - - padding: 5px; - font-size: 0.9em; - - pointer-events: none; -} - -.info-card::after { - content: ""; - display: block; - clear: both; -} - -#info-card-container.show .info-card { - animation: 0.01s linear 0.2s forwards info-card-become-interactive; -} - -@keyframes info-card-become-interactive { - to { - pointer-events: auto; - } -} - -.info-card-art-container { - float: right; - - width: 40%; - margin: 5px; - font-size: 0.8em; - - /* Dynamically shown. */ - display: none; -} - -.info-card-art-container .image-container { - padding: 2px; -} - -.info-card-art { - display: block; - width: 100%; - height: 100%; -} - -.info-card-name { - font-size: 1em; - border-bottom: 1px dotted; - margin: 0; -} - -.info-card p { - margin-top: 0.25em; - margin-bottom: 0.25em; -} - -.info-card p:last-child { - margin-bottom: 0; -} - -/* Custom hash links */ - -.content-heading { - border-bottom: 3px double transparent; - margin-bottom: -3px; -} - -.content-heading.highlight-hash-link { - animation: highlight-hash-link 4s; - animation-delay: 125ms; -} - -dl dt.content-heading { - /* Basic margin-bottom for dt is 2px, - * so just subtract 3px from that. - */ - margin-bottom: -1px; -} - -h3.content-heading { - clear: both; -} - -/* This animation's name is referenced in JavaScript */ -@keyframes highlight-hash-link { - 0% { - border-bottom-color: transparent; - } - - 10% { - border-bottom-color: white; - } - - 25% { - border-bottom-color: white; - } - - 100% { - border-bottom-color: transparent; - } -} - -/* Sticky heading */ - -[id] { - --custom-scroll-offset: 0px; -} - -#content [id] { - /* Adjust scroll margin. */ - scroll-margin-top: calc( - 74px /* Sticky heading */ - + 33px /* Sticky subheading */ - - 1em /* One line of text (align bottom) */ - - 12px /* Padding for hanging letters & focus ring */ - + var(--custom-scroll-offset) /* Customizable offset */ - ); -} - -.content-sticky-heading-container { - position: sticky; - top: 0; - - margin: calc(-1 * var(--content-padding)); - margin-bottom: calc(0.5 * var(--content-padding)); - - transform: translateY(-5px); -} - -main.long-content .content-sticky-heading-container { - padding-left: 0; - padding-right: 0; -} - -main.long-content .content-sticky-heading-container .content-sticky-heading-row, -main.long-content .content-sticky-heading-container .content-sticky-subheading-row { - padding-left: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding)); - padding-right: calc(var(--long-content-padding-ratio) * (100% - 2 * var(--content-padding)) + var(--content-padding)); -} - -.content-sticky-heading-row { - box-sizing: border-box; - padding: - calc(1.25 * var(--content-padding) + 5px) - 20px - calc(0.75 * var(--content-padding)) - 20px; - - width: 100%; - margin: 0; - - background: var(--bg-black-color); - border-bottom: 1px dotted rgba(220, 220, 220, 0.4); - - -webkit-backdrop-filter: blur(6px); - backdrop-filter: blur(6px); -} - -.content-sticky-heading-container.has-cover .content-sticky-heading-row, -.content-sticky-heading-container.has-cover .content-sticky-subheading-row { - display: grid; - grid-template-areas: - "title cover"; - grid-template-columns: 1fr min(40%, 400px); -} - -.content-sticky-heading-row h1 { - margin: 0; - padding-right: 20px; -} - -.content-sticky-heading-cover-container { - position: relative; - height: 0; - margin: -15px 0px -5px -5px; -} - -.content-sticky-heading-cover-needs-reveal { - display: none; -} - -.content-sticky-heading-cover { - position: absolute; - top: 0; - width: 80px; - right: 10px; - box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.25); - transition: transform 0.35s, opacity 0.25s; -} - -.content-sticky-heading-cover-container:not(.visible) .content-sticky-heading-cover { - opacity: 0; - transform: translateY(15px); - transition: transform 0.35s, opacity 0.30s; -} - -.content-sticky-heading-cover .image-container { - border-width: 1px; - border-radius: 1.25px; - box-shadow: none; -} - -.content-sticky-heading-container .image-outer-area { - padding: 3px; -} - -.content-sticky-heading-container .image-inner-area { - border-radius: 1.75px; - overflow: hidden; -} - -.content-sticky-heading-cover .image { - display: block; - width: 100%; - height: 100%; -} - -.content-sticky-subheading-row { - position: absolute; - width: 100%; - box-sizing: border-box; - padding: 10px 20px 5px 20px; - margin-top: 0; - z-index: -1; - - background: var(--bg-black-color); - border-bottom: 1px dotted rgba(220, 220, 220, 0.4); - box-shadow: - 0 2px 2px -1px #00000060, - 0 4px 12px -4px #00000090; - - -webkit-backdrop-filter: blur(4px); - backdrop-filter: blur(4px); - - transition: margin-top 0.35s, opacity 0.25s; -} - -.content-sticky-subheading-row h2 { - margin: 0; - - font-size: 0.9em !important; - font-weight: normal; - font-style: oblique; - color: #eee; -} - -.content-sticky-subheading-row:not(.visible) { - margin-top: -20px; - opacity: 0; -} - -.content-sticky-subheading { - padding-right: 20px; -} - -.content-sticky-heading-container h2.visible { - margin-top: 0; - opacity: 1; -} - -.content-sticky-heading-row { - box-shadow: - inset 0 10px 10px -5px var(--shadow-color), - 0 4px 8px -4px #000000b0; -} - -#content, .sidebar { - contain: paint; -} - -/* Sticky sidebar */ - -.sidebar-column:not(.sticky-column) { - align-self: stretch; -} - -.sidebar-column.sticky-column { - position: sticky; - top: 10px; - align-self: flex-start; - max-height: calc(100vh - 20px); - display: flex; - flex-direction: column; -} - -.sidebar-multiple.sticky-column .sidebar:last-child { - flex-shrink: 1; - overflow-y: scroll; - scrollbar-width: thin; - scrollbar-color: var(--dim-color) var(--dark-color); -} - -.wiki-search-sidebar-box .wiki-search-results-container { - overflow-y: scroll; - scrollbar-width: thin; - scrollbar-color: var(--dim-color) var(--dark-color); -} - -.sidebar-column.sticky-column .sidebar:last-child::-webkit-scrollbar, -.wiki-search-sidebar-box .wiki-search-results-container::-webkit-scrollbar { - background: var(--dark-color); - width: 12px; -} - -.sidebar-column.sticky-column .sidebar:last-child::-webkit-scrollbar-thumb, -.wiki-search-sidebar-box .wiki-search-results-container::-webkit-scrollbar-thumb { - transition: background 0.2s; - background: rgba(255, 255, 255, 0.2); - border: 3px solid transparent; - border-radius: 10px; - background-clip: content-box; -} - -.sidebar-column.sidebar.sticky-column > h1 { - position: sticky; - top: 0; - z-index: 2; - - margin: 0 calc(-1 * var(--content-padding)); - margin-bottom: 10px; - - border-bottom: 1px dotted rgba(220, 220, 220, 0.4); - padding: 10px 5px; - - background: var(--bg-black-color); - -webkit-backdrop-filter: blur(4px); - backdrop-filter: blur(4px); - - box-shadow: - 0 2px 3px -1px #0006, - 0 4px 8px -2px #0009; -} - -/* Image overlay */ - -#image-overlay-container { - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - z-index: 4000; - - background: rgba(0, 0, 0, 0.8); - color: white; - padding: 20px 40px; - box-sizing: border-box; - - opacity: 0; - pointer-events: none; - transition: opacity 0.4s; - - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; -} - -#image-overlay-container::before { - content: ''; - position: fixed; - top: 0; - left: 0; - right: 0; - bottom: 0; - - background: var(--deep-color); - opacity: 0.20; -} - -#image-overlay-container.visible { - opacity: 1; - pointer-events: auto; -} - -#image-overlay-content-container { - border-radius: 0 0 8px 8px; - border: 2px solid var(--primary-color); - background: var(--deep-ghost-color); - overflow: hidden; - - box-shadow: - 0 0 90px 30px #00000060, - 0 0 20px 10px #00000040, - 0 0 10px 3px #00000080; - - -webkit-backdrop-filter: blur(3px); - backdrop-filter: blur(3px); -} - -#image-overlay-image-container { - display: block; - position: relative; - overflow: hidden; - width: 80vmin; - height: 80vmin; - margin-left: auto; - margin-right: auto; -} - -#image-overlay-image, -#image-overlay-image-thumb { - display: inline-block; - object-fit: contain; - width: 100%; - height: 100%; - background: rgba(0, 0, 0, 0.65); -} - -#image-overlay-image { - position: absolute; - top: 3px; - left: 3px; - width: calc(100% - 6px); - height: calc(100% - 4px); -} - -#image-overlay-image-thumb { - filter: blur(16px); - transform: scale(1.5); -} - -#image-overlay-container.loaded #image-overlay-image-thumb { - opacity: 0; - pointer-events: none; - transition: opacity 0.25s; -} - -#image-overlay-image-container::after { - content: ""; - display: block; - position: absolute; - bottom: 0; - left: 0; - height: 4px; - width: var(--download-progress); - background: var(--primary-color); - box-shadow: 0 -3px 12px 4px var(--primary-color); - transition: 0.25s; -} - -#image-overlay-container.loaded #image-overlay-image-container::after { - width: 100%; - background: white; - opacity: 0; -} - -#image-overlay-container.errored #image-overlay-image-container::after { - width: 100%; - background: red; -} - -#image-overlay-container:not(.visible) #image-overlay-image-container::after { - width: 0 !important; -} - -#image-overlay-action-container { - padding: 7px 4px 7px 4px; - border-radius: 0 0 5px 5px; - background: var(--bg-black-color); - color: white; - font-style: oblique; - text-align: center; - box-shadow: - 0 3px 8px -5px var(--primary-color) inset; -} - -#image-overlay-container #image-overlay-action-content-without-size:not(.visible), -#image-overlay-container #image-overlay-action-content-with-size:not(.visible), -#image-overlay-container #image-overlay-file-size-warning:not(.visible), -#image-overlay-container #image-overlay-file-size-kilobytes:not(.visible), -#image-overlay-container #image-overlay-file-size-megabytes:not(.visible) { - display: none; -} - -#image-overlay-file-size-warning { - opacity: 0.8; - font-size: 0.9em; -} - -/* important easter egg mode */ - -html[data-language-code="preview-en"][data-url-key="localized.home"] #content - h1::after { - font-family: cursive; - display: block; - content: "(Preview Build)"; - font-size: 0.8em; -} - -/* Layout - Wide (most computers) */ - -@media (min-width: 900px) { - #page-container.showing-sidebar-left #secondary-nav:not(.always-visible), - #page-container.showing-sidebar-right #secondary-nav:not(.always-visible) { - display: none; - } -} - -/* Layout - Medium (tablets, some landscape mobiles) - * - * Note: Rules defined here are exclusive to "medium" width, i.e. they don't - * additionally apply to "thin". Use the later section which applies to both - * if so desired. - */ - -@media (min-width: 600px) and (max-width: 899.98px) { - /* Medium layout is mainly defined (to the user) by hiding the sidebar, so - * don't apply the similar layout change of widening the long-content area - * if this page doesn't have a sidebar to hide in the first place. - */ - #page-container.showing-sidebar-left main, - #page-container.showing-sidebar-right main { - --responsive-padding-ratio: 0.06; - } -} - -/* Layout - Wide or Medium */ - -@media (min-width: 600px) { - .content-sticky-heading-container { - /* Safari doesn't always play nicely with position: sticky, - * this seems to fix images sometimes displaying above the - * position: absolute subheading (h2) child - * - * See also: https://stackoverflow.com/questions/50224855/ - */ - transform: translate3d(0, 0, 0); - z-index: 1; - } - - /* Cover art floats to the right. It's positioned in HTML beneath the - * heading, so pull it up a little to "float" on top. - */ - #cover-art-container { - float: right; - width: 40%; - max-width: 400px; - margin: -60px 0 10px 20px; - - position: relative; - z-index: 2; - } - - /* ...Except on top-indexes, where cover art is displayed prominently - * between the heading and subheading. - */ - #content.top-index #cover-art-container { - float: none; - margin: 2em auto 2.5em auto; - max-width: 375px; - } - - html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:not(:nth-child(n+7)) { - flex-basis: 23%; - margin: 15px; - } - - html[data-url-key="localized.home"] #page-container.showing-sidebar-left .grid-listing > .grid-item:nth-child(n+7) { - flex-basis: 18%; - margin: 10px; - } -} - -/* Layout - Medium or Thin */ - -@media (max-width: 899.98px) { - .sidebar.collapsible, - .sidebar-box-joiner.collapsible, - .sidebar-column.all-boxes-collapsible { - display: none; - } - - .layout-columns { - flex-direction: column; - } - - .layout-columns > *:not(:last-child) { - margin-bottom: 10px; - } - - .sidebar-column { - position: static !important; - max-width: unset !important; - flex-basis: unset !important; - margin-right: 0 !important; - margin-left: 0 !important; - width: 100%; - } - - .sidebar .news-entry:not(.first-news-entry) { - display: none; - } - - .grid-listing > .grid-item { - flex-basis: 40%; - } -} - -/* Layout - Thin (phones) */ - -@media (max-width: 600px) { - .content-columns { - columns: 1; - } - - main { - --responsive-padding-ratio: 0.02; - } - - #cover-art-container { - margin: 25px 0 5px 0; - width: 100%; - max-width: unset; - } - - #additional-names-box { - width: unset; - max-width: unset; - } - - .nav-has-content .nav-main-links .nav-link-accent { - display: block; - } - - /* Show sticky heading above cover art */ - - .content-sticky-heading-container { - z-index: 2; - } - - .content-sticky-heading-row h1 { - padding-right: 10px; - } - - /* Let sticky heading text span past lower-index cover art */ - - .content-sticky-heading-container.has-cover .content-sticky-heading-row, - .content-sticky-heading-container.has-cover .content-sticky-subheading-row { - grid-template-columns: 1fr 90px; - } - - /* Disable grid features, just line header children up vertically */ - - #header { - display: block; - } - - #header > div:not(:first-child) { - margin-top: 0.5em; - } -} +@layer interactivity; +@layer layout; +@layer material, print; +@layer construction; +@layer interaction; + +@import url(miscellany.css); +@import url(page.css); +@import url(search.css); +@import url(tooltips.css); +@import url(features.css); +@import url(responsive.css); + +@import url(specific-pages.css); diff --git a/src/static/css/specific-pages.css b/src/static/css/specific-pages.css new file mode 100644 index 00000000..ef3a0c54 --- /dev/null +++ b/src/static/css/specific-pages.css @@ -0,0 +1,288 @@ +/* Obviously this file should be split up. + * Just porting from old CSS for now. + */ + +/* Album commentary page */ + +@layer layout { + html[data-url-key="localized.albumCommentary"] .content-heading-main-title { + margin-right: 0.25em; + } + + html[data-url-key="localized.albumCommentary"] .content-heading-accent { + display: inline-block; + } + + html[data-url-key="localized.albumCommentary"] p.track-info { + margin-left: 20px; + } +} + +@layer print { + html[data-url-key="localized.albumCommentary"] li.no-commentary { + opacity: 0.7; + } + + html[data-url-key="localized.albumCommentary"] .content-heading-accent { + font-weight: normal; + font-style: oblique; + font-size: 0.9rem; + } +} + +/* Art tag gallery page */ + +@layer layout { + html[data-url-key="localized.artTagGallery"] #descends-from-line { + margin-bottom: 0.25em; + } + + html[data-url-key="localized.artTagGallery"] #descendants-line { + margin-top: 0.25em; + } + + html[data-url-key="localized.artTagGallery"] #descends-from-line a, + html[data-url-key="localized.artTagGallery"] #descendants-line a { + display: inline-block; + } +} + +@layer interaction { + html[data-url-key="localized.artTagGallery"] #featured-direct-line, + html[data-url-key="localized.artTagGallery"] #featured-indirect-line, + html[data-url-key="localized.artTagGallery"] #showing-direct-line, + html[data-url-key="localized.artTagGallery"] #showing-indirect-line { + display: none; + } + + html[data-url-key="localized.artTagGallery"] #showing-all-line a, + html[data-url-key="localized.artTagGallery"] #showing-direct-line a, + html[data-url-key="localized.artTagGallery"] #showing-indirect-line a { + text-decoration: underline; + text-decoration-style: dotted; + } +} + +/* "Art Tag Network" listing */ + +@layer layout { + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:not(#network-top-dl > dd) { + margin-left: 20px; + margin-bottom: 0; + padding-left: 10px; + } + + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:not(#network-top-dl > dd):not(:last-child) { + padding-bottom: 20px; + } + + html[data-url-key="localized.listing"][data-url-value0="tags/network"] #network-stat-line { + padding-left: 10px; + margin-left: 20px; + } + + html[data-url-key="localized.listing"][data-url-value0="tags/network"] .network-tag-stat { + display: inline-block; + text-align: right; + min-width: 5ch; + margin-right: 2px; + } + + html[data-url-key="localized.listing"][data-url-value0="tags/network"] #network-top-dl > dt:has(.network-tag.with-stat:not([style*="display: none"])) { + padding-left: 20px; + } + + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dt + dt:has(+ dd) { + padding-top: 20px; + } + + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt { + padding-left: 10px; + margin-left: 20px; + margin-bottom: 0; + padding-bottom: 2px; + max-width: unset; + position: relative; + } +} + +@layer material { + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:not(#network-top-dl > dd).even, + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:not(#network-top-dl > dt).even { + border-left: 1px solid #eaeaea; + } + + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:not(#network-top-dl > dd).odd, + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:not(#network-top-dl > dt).odd { + border-left: 1px solid #7b7b7b; + } + + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:last-child:not(#network-top-dl > dd).odd::after, + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:last-child:not(#network-top-dl > dt).odd::after { + content: ""; + display: block; + width: 7px; + height: 7px; + background: #7b7b7b; + position: absolute; + bottom: -4px; + left: -4px; + } + + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dd:last-child:not(#network-top-dl > dd).even::after, + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dl dt:last-child:not(#network-top-dl > dt).even::after { + content: ""; + display: block; + width: 6px; + height: 6px; + background: #eaeaea; + position: absolute; + bottom: -3px; + left: -3px; + border-bottom-right-radius: 6px; + border-top-left-radius: 3px; + } +} + +@layer print { + html[data-url-key="localized.listing"][data-url-value0="tags/network"] .network-tag-stat { + text-align: right; + } + + html[data-url-key="localized.listing"][data-url-value0="tags/network"] dt:has(+ dd) .network-tag-stat { + text-align: center; + } +} + +@layer interaction { + html[data-url-key="localized.listing"][data-url-value0="tags/network"] #network-stat-line a { + text-decoration: underline; + text-decoration-style: dotted; + } +} + +/* Artist rolling window page */ + +@layer layout { + html[data-url-key="localized.artistRollingWindow"] #content p { + text-align: center; + } + + html[data-url-key="localized.artistRollingWindow"] #timeframe-selection-control a { + display: inline-block; + padding: 5px; + } + + + html[data-url-key="localized.artistRollingWindow"] #timeframe-source-area { + display: flex; + flex-direction: column; + justify-content: center; + min-height: calc(100vh - 260px); + } + + html[data-url-key="localized.artistRollingWindow"] #timeframe-source-area .grid-listing { + width: 100%; + } +} + +@layer material { + html[data-url-key="localized.artistRollingWindow"] #content input[type=number] { + width: 3em; + margin: 0 0.25em; + background: black; + color: white; + border: 1px dotted var(--primary-color); + padding: 4px; + border-radius: 3px; + } + + html[data-url-key="localized.artistRollingWindow"] #timeframe-source-area { + border: 1px dashed #ffffff42; + border-top-style: solid; + border-bottom-style: solid; + } +} + +@layer material { + html[data-url-key="localized.artistRollingWindow"] .grid-item.peeking { + opacity: 0.8; + background: #ffffff24; + } +} + +@layer construction { + html[data-url-key="localized.artistRollingWindow"] .grid-item > span:not(:first-of-type) { + display: flex; + flex-direction: row; + justify-content: center; + flex-wrap: wrap; + } +} + +@layer interaction { + html[data-url-key="localized.artistRollingWindow"] #timeframe-selection-control a { + text-decoration: underline; + text-decoration-style: dotted; + } + + html[data-url-key="localized.artistRollingWindow"] #timeframe-selection-control a:not([href]) { + text-decoration: none; + opacity: 0.7; + } +} + +@layer construction { + html[data-url-key="localized.artistRollingWindow"] .grid-item > span:not(:first-of-type) > *:not([style*="display: none"]) ~ *::before { + content: '\00b7'; + margin-left: 0.5ch; + margin-right: 0.5ch; + } +} + +/* Group info page */ + +@layer print { + html[data-url-key="localized.groupInfo"] .by a { + color: var(--page-primary-color); + } +} + +/* Homepage */ + +@layer print { + html[data-url-key="localized.home"] #content h1 { + text-align: center; + font-size: 2.5em; + } + + html[data-language-code="preview-en"][data-url-key="localized.home"] #content h1::after { + font-family: cursive; + display: block; + content: "(Preview Build)"; + font-size: 0.8em; + } +} + +/* "Random Pages" listing */ + +@layer interaction { + html[data-url-key="localized.listing"][data-url-value0="random"] #data-loading-line, + html[data-url-key="localized.listing"][data-url-value0="random"] #data-loaded-line, + html[data-url-key="localized.listing"][data-url-value0="random"] #data-error-line { + display: none; + } + + html[data-url-key="localized.listing"][data-url-value0="random"] #content a:not([href]) { + opacity: 0.7; + } +} + +/* News entries */ + +@layer print { + html[data-url-key="localized.newsEntry"] .read-another-links { + font-style: oblique; + font-size: 0.9em; + } +} diff --git a/src/static/css/tooltips.css b/src/static/css/tooltips.css new file mode 100644 index 00000000..f4f9416e --- /dev/null +++ b/src/static/css/tooltips.css @@ -0,0 +1,419 @@ +@layer layout { + .text-with-tooltip { + position: relative; + } + + .tooltip { + position: absolute; + z-index: 3; + left: -10px; + top: calc(1em + 1px); + + display: none; + } + + .tooltip-content { + display: block; + } + + :where(.isolate-tooltip-z-indexing) { + position: relative; + z-index: 1; + } + + :where(.isolate-tooltip-z-indexing > *) { + position: relative; + z-index: -1; + } +} + +@layer material { + .tooltip { + font-size: 1rem; + } + + .cover-artwork .tooltip, + #sidebar .tooltip { + font-size: 0.9rem; + } + + .offset-tooltips .tooltip { + left: 14px; + } + + .tooltip-content { + background: var(--bg-black-color); + border: 1px dotted var(--primary-color); + border-radius: 6px; + + -webkit-backdrop-filter: + brightness(1.5) saturate(1.4) blur(4px); + + backdrop-filter: + brightness(1.5) saturate(1.4) blur(4px); + + box-shadow: + 0 3px 4px 4px #000000aa, + 0 -2px 4px -2px var(--primary-color) inset; + + text-indent: 0; + } +} + +@layer print { + .tooltip-content { + font-size: 1rem; + } + + .cover-artwork .content-tooltip .tooltip-content { + font-size: 0.85rem; + } +} + +@layer interaction { + .tooltip { + display: none; + } + + .text-with-tooltip .text-with-tooltip-interaction-cue { + text-decoration-line: underline; + text-decoration-style: dotted; + } + + .text-with-tooltip > .hoverable:hover .text-with-tooltip-interaction-cue, + .text-with-tooltip > .hoverable.has-visible-tooltip .text-with-tooltip-interaction-cue { + text-decoration-style: wavy !important; + } +} + +/* "General" tooltips */ + +@layer layout { + .datetimestamp-tooltip, + .missing-duration-tooltip, + .commentary-date-tooltip, + .rerelease-tooltip, + .first-release-tooltip, + .other-release-tooltip, + .content-tooltip { + padding: 3px 4px 2px 2px; + left: -10px; + } + + .datetimestamp-tooltip .tooltip-content, + .missing-duration-tooltip .tooltip-content, + .commentary-date-tooltip .tooltip-content { + padding: 5px 6px; + white-space: nowrap; + font-size: 0.9em; + } +} + +@layer interaction { + .text-with-tooltip.datetimestamp .text-with-tooltip-interaction-cue, + .text-with-tooltip.missing-duration .text-with-tooltip-interaction-cue, + .text-with-tooltip.commentary-date .text-with-tooltip-interaction-cue, + .text-with-tooltip.wiki-edits .text-with-tooltip-interaction-cue, + .text-with-tooltip.rerelease .text-with-tooltip-interaction-cue, + .text-with-tooltip.first-release .text-with-tooltip-interaction-cue { + cursor: default; + } +} + +/* Contribution tooltip */ + +@layer interactivity { + .contribution-tooltip .tooltip-content { + -webkit-user-select: none; + user-select: none; + } +} + +@layer layout { + .contribution-tooltip { + padding: 3px 6px 6px 6px; + left: -34px; + } + + .contribution-tooltip .tooltip-content { + display: grid; + grid-template-columns: + [icon-start] 26px [icon-end handle-start] auto [handle-end platform-start] auto [platform-end]; + + padding: 6px 2px 2px 2px; + } + + .contribution-tooltip .external-link { + display: grid; + grid-template-columns: subgrid; + grid-column-start: icon-start; + grid-column-end: handle-end; + + height: 1.4em; + } + + .contribution-tooltip .chronology-heading { + grid-column-start: handle-start; + grid-column-end: platform-end; + + min-width: 30ch; + margin-bottom: 2px; + } + + .contribution-tooltip .chronology-link { + display: grid; + grid-column-start: icon-start; + grid-column-end: handle-end; + grid-template-columns: subgrid; + + height: 1.2em; + } + + .contribution-tooltip .external-icon, + .contribution-tooltip .chronology-symbol { + grid-column-start: icon-start; + grid-column-end: icon-end; + } + + .contribution-tooltip .external-icon svg { + width: 18px !important; + height: 18px !important; + top: -0.1em; + } + + .contribution-tooltip .chronology-symbol { + text-align: center; + } + + .contribution-tooltip .external-handle, + .contribution-tooltip .chronology-text { + grid-column-start: handle-start; + grid-column-end: handle-end; + + width: max-content; + max-width: 200px; + + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .contribution-tooltip .external-handle { + padding-right: 8px; + } + + .contribution-tooltip .chronology-text { + padding-right: 6px; + } + + .contribution-tooltip .external-platform, + .contribution-tooltip .chronology-info { + grid-column-start: platform-start; + grid-column-end: platform-end; + white-space: nowrap; + padding-right: 4px; + } +} + +@layer print { + .contribution-tooltip .tooltip-divider, + .tooltip-content hr.cute, + .tooltip-content span.cute-break { + display: block; + grid-column-start: icon-start; + grid-column-end: platform-end; + border-top: 1px dotted var(--primary-color); + } + + /* Don't mind me... */ + .tooltip-content .tooltip-divider, + .tooltip-content hr.cute { + margin-top: 3px; + margin-bottom: 4px; + } + + .contribution-tooltip .chronology-heading { + font-size: 0.85em; + font-style: oblique; + } + + .contribution-tooltip .chronology-text, + .contribution-tooltip .chronology-info { + font-size: 0.85em; + } +} + +@layer interaction { + .contribution-tooltip .tooltip-content { + cursor: default; + } + + .contribution-tooltip.show-info .external-platform, + .contribution-tooltip.show-info .chronology-info { + display: inline; + animation: external-platform 0.2s forwards linear; + } + + @keyframes external-platform { + from { + opacity: 0; + } + + to { + opacity: var(--external-platform-opacity); + } + } + + .contribution-tooltip .external-platform, + .contribution-tooltip .chronology-info { + display: none; + + --external-platform-opacity: 0.8; + opacity: 0.8; + } + + .contribution-tooltip .external-link:hover, + .contribution-tooltip .chronology-link:hover { + filter: brightness(1.4); + text-decoration: none; + } + + .contribution-tooltip .external-link:hover .external-handle, + .contribution-tooltip .chronology-link:hover .chronology-text { + text-decoration: underline; + } + + .contribution-tooltip .external-link:hover + .external-platform, + .contribution-tooltip .chronology-link:hover + .chronology-info { + --external-platform-opacity: 1; + text-decoration: underline; + text-decoration-color: #ffffffaa; + } +} + +/* Missing duration tooltip */ + +@layer interaction { + .text-with-tooltip.missing-duration > .hoverable { + opacity: 0.5; + } + + .text-with-tooltip.missing-duration > .hoverable:hover, + .text-with-tooltip.missing-duration > .hoverable.has-visible-tooltip { + opacity: 1; + } + + .text-with-tooltip.missing-duration .text-with-tooltip-interaction-cue { + text-decoration: none !important; + } +} + +/* Rerelease, first release, other release tooltips */ + +@layer layout { + .rerelease-tooltip .tooltip-content, + .first-release-tooltip .tooltip-content, + .other-release-tooltip .tooltip-content { + padding: 3px 4.5px; + } + + .rerelease-tooltip .tooltip-content, + .first-release-tooltip .tooltip-content { + width: 260px; + } + + .other-release-tooltip .tooltip-content { + width: max-content; + max-width: 250px; + } + + .other-release-tooltip .tooltip-content .when { + /* technically just putting this in a <span> was enough + * to keep it from wrapping all tight-like, for some + * reason, but im not taking any chances... + */ + white-space: nowrap; + } +} + +@layer print { + .rerelease-tooltip .tooltip-content, + .first-release-tooltip .tooltip-content, + .other-release-tooltip .tooltip-content { + font-size: 0.9rem; + } + + .rerelease-tooltip .not-credited-on-first-release { + opacity: 0.9; + } +} + +/* Thing name & wiki edits tooltips */ + +@layer layout { + .thing-name-tooltip, + .wiki-edits-tooltip { + padding: 3px 4px 2px 2px; + left: -6px; + } + + .thing-name-tooltip .tooltip-content, + .wiki-edits-tooltip .tooltip-content { + width: max-content; + padding: 3px 4.5px; + } + + .thing-name-tooltip .tooltip-content, + max-width: 120px; + } + + .wiki-edits-tooltip .tooltip-content { + max-width: 200px; + } +} + +@layer print { + .text-with-tooltip.wiki-edits > .hoverable { + white-space: nowrap; + } + + .thing-name-tooltip .tooltip-content, + .wiki-edits-tooltip .tooltip-content { + font-size: 0.85rem; + } +} + +/* Content tooltips */ + +@layer layout { + .content-tooltip-guy { + display: inline-block; + } + + .content-tooltip-guy:not(.has-link) .hoverable { + cursor: default; + } + + .content-tooltip-guy.has-link .text-with-tooltip-interaction-cue { + text-decoration-color: var(--primary-color); + } + + .content-tooltip .tooltip-content { + padding: 3px 4.5px; + width: max-content; + max-width: 240px; + } + + .cover-artwork .content-tooltip { + padding: 2px 3px; + width: max-content; + max-width: 220px; + } +} + +@layer interaction { + .content-tooltip-guy .hoverable a { + text-decoration-color: transparent; + text-decoration-style: dotted; + } +} diff --git a/src/static/js/client-util.js b/src/static/js/client-util.js index f06b707a..0c113758 100644 --- a/src/static/js/client-util.js +++ b/src/static/js/client-util.js @@ -1,12 +1,17 @@ -/* eslint-env browser */ - export function rebase(href, rebaseKey = 'rebaseLocalized') { - const relative = (document.documentElement.dataset[rebaseKey] || '.') + '/'; - if (relative) { - return relative + href; - } else { - return href; + let result = document.documentElement.dataset[rebaseKey] || './'; + + if (!result.endsWith('/')) { + result += '/'; } + + if (href.startsWith('/')) { + href = href.slice(1); + } + + result += href; + + return result; } export function cssProp(el, ...args) { @@ -30,7 +35,7 @@ export function cssProp(el, ...args) { } } -export function templateContent(el) { +export function templateContent(el, slots = {}) { if (el === null) { return null; } @@ -39,7 +44,33 @@ export function templateContent(el) { throw new Error(`Expected a <template> element`); } - return el.content.cloneNode(true); + const content = el.content.cloneNode(true); + + for (const [key, value] of Object.entries(slots)) { + const slot = content.querySelector(`slot[name="${key}"]`); + + if (!slot) { + console.warn(`Slot ${key} missing in template:`, el); + continue; + } + + if (value === null || value === undefined) { + console.warn(`Valueless slot ${key} in template:`, el); + continue; + } + + slot.replaceWith(value); + } + + return content; +} + +export function decodeEntities(string) { + if (!string) return string; + + const textarea = document.createElement('textarea'); + textarea.innerHTML = string; + return textarea.value; } // Curry-style, so multiple points can more conveniently be tested at once. @@ -74,12 +105,12 @@ export function getVisuallyContainingElement(child) { const getLinkHref = (type, directory) => rebase(`${type}/${directory}`); */ -export const openAlbum = d => rebase(`album/${d}`); -export const openArtTag = d => rebase(`tag/${d}`); -export const openArtist = d => rebase(`artist/${d}`); -export const openFlash = d => rebase(`flash/${d}`); -export const openGroup = d => rebase(`group/${d}`); -export const openTrack = d => rebase(`track/${d}`); +export const openAlbum = d => rebase(`album/${d}/`); +export const openArtTag = d => rebase(`tag/${d}/`); +export const openArtist = d => rebase(`artist/${d}/`); +export const openFlash = d => rebase(`flash/${d}/`); +export const openGroup = d => rebase(`group/${d}/`); +export const openTrack = d => rebase(`track/${d}/`); // TODO: This should also use urlSpec. @@ -120,3 +151,10 @@ export function dispatchInternalEvent(event, eventName, ...args) { return results; } + +const languageCode = document.documentElement.getAttribute('lang'); + +export function formatDate(inputDate) { + const date = new Date(inputDate); + return date.toLocaleDateString(languageCode); +} diff --git a/src/static/js/client/additional-names-box.js b/src/static/js/client/additional-names-box.js index 558ef06f..a6d9b098 100644 --- a/src/static/js/client/additional-names-box.js +++ b/src/static/js/client/additional-names-box.js @@ -1,14 +1,17 @@ -/* eslint-env browser */ - import {cssProp} from '../client-util.js'; import {info as hashLinkInfo} from './hash-link.js'; +import {info as stickyHeadingInfo} from './sticky-heading.js'; export const info = { id: 'additionalNamesBoxInfo', box: null, + links: null, + stickyHeadingLink: null, + + contentContainer: null, mainContentContainer: null, state: { @@ -23,6 +26,16 @@ export function getPageReferences() { info.links = document.querySelectorAll('a[href="#additional-names-box"]'); + info.stickyHeadingLink = + document.querySelector( + '.content-sticky-heading-container' + + ' ' + + 'a[href="#additional-names-box"]' + + ':not(:where([inert] *))'); + + info.contentContainer = + document.querySelector('#content'); + info.mainContentContainer = document.querySelector('#content .main-content-container'); } @@ -33,6 +46,34 @@ export function addInternalListeners() { return false; } }); + + stickyHeadingInfo.event.whenStuckStatusChanges.push((index, stuck) => { + const {state} = info; + + if (!info.stickyHeadingLink) return; + + const container = stickyHeadingInfo.contentContainers[index]; + if (container !== info.contentContainer) return; + + if (stuck) { + if (!state.visible) { + info.stickyHeadingLink.removeAttribute('href'); + + if (info.stickyHeadingLink.hasAttribute('title')) { + info.stickyHeadingLink.dataset.restoreTitle = info.stickyHeadingLink.getAttribute('title'); + info.stickyHeadingLink.removeAttribute('title'); + } + } + } else { + info.stickyHeadingLink.setAttribute('href', '#additional-names-box'); + + const {restoreTitle} = info.stickyHeadingLink.dataset; + if (restoreTitle) { + info.stickyHeadingLink.setAttribute('title', restoreTitle); + delete info.stickyHeadingLink.dataset.restoreTitle; + } + } + }); } export function addPageListeners() { @@ -48,6 +89,7 @@ function handleAdditionalNamesBoxLinkClicked(domEvent) { domEvent.preventDefault(); + if (!domEvent.target.hasAttribute('href')) return; if (!info.box || !info.mainContentContainer) return; const margin = @@ -58,7 +100,30 @@ function handleAdditionalNamesBoxLinkClicked(domEvent) { ? info.box.getBoundingClientRect() : info.mainContentContainer.getBoundingClientRect()); - if (top + 20 < margin || top > 0.4 * window.innerHeight) { + const {bottom, height} = + (state.visible + ? info.box.getBoundingClientRect() + : {bottom: null}); + + const boxFitsInFrame = + (height + ? height < window.innerHeight - margin - 60 + : null); + + const worthScrolling = + top + 20 < margin || + + (height && boxFitsInFrame + ? top > 0.7 * window.innerHeight + : height && !boxFitsInFrame + ? top > 0.4 * window.innerHeight + : top > 0.5 * window.innerHeight) || + + (bottom && boxFitsInFrame + ? bottom > window.innerHeight - 20 + : false); + + if (worthScrolling) { if (!state.visible) { toggleAdditionalNamesBox(); } diff --git a/src/static/js/client/album-commentary-sidebar.js b/src/static/js/client/album-commentary-sidebar.js index c5eaf81b..d7c4a591 100644 --- a/src/static/js/client/album-commentary-sidebar.js +++ b/src/static/js/client/album-commentary-sidebar.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {empty} from '../../shared-util/sugar.js'; import {info as hashLinkInfo} from './hash-link.js'; diff --git a/src/static/js/client/art-tag-gallery-filter.js b/src/static/js/client/art-tag-gallery-filter.js new file mode 100644 index 00000000..b7fff70d --- /dev/null +++ b/src/static/js/client/art-tag-gallery-filter.js @@ -0,0 +1,149 @@ +export const info = { + id: 'artTagGalleryFilterInfo', + + featuredAllLine: null, + showingAllLine: null, + showingAllLink: null, + + featuredDirectLine: null, + showingDirectLine: null, + showingDirectLink: null, + + featuredIndirectLine: null, + showingIndirectLine: null, + showingIndirectLink: null, + + gridItems: null, + gridItemsOnlyFeaturedIndirectly: null, + gridItemsFeaturedDirectly: null, +}; + +export function getPageReferences() { + if (document.documentElement.dataset.urlKey !== 'localized.artTagGallery') { + return; + } + + info.featuredAllLine = + document.getElementById('featured-all-line'); + + info.featuredDirectLine = + document.getElementById('featured-direct-line'); + + info.featuredIndirectLine = + document.getElementById('featured-indirect-line'); + + info.showingAllLine = + document.getElementById('showing-all-line'); + + info.showingDirectLine = + document.getElementById('showing-direct-line'); + + info.showingIndirectLine = + document.getElementById('showing-indirect-line'); + + info.showingAllLink = + info.showingAllLine?.querySelector('a') ?? null; + + info.showingDirectLink = + info.showingDirectLine?.querySelector('a') ?? null; + + info.showingIndirectLink = + info.showingIndirectLine?.querySelector('a') ?? null; + + info.gridItems = + Array.from( + document.querySelectorAll('#content .grid-listing .grid-item')); + + info.gridItemsOnlyFeaturedIndirectly = + info.gridItems + .filter(gridItem => gridItem.classList.contains('featured-indirectly')); + + info.gridItemsFeaturedDirectly = + info.gridItems + .filter(gridItem => !gridItem.classList.contains('featured-indirectly')); +} + +function filterArtTagGallery(showing) { + let gridItemsToShow; + + switch (showing) { + case 'all': + gridItemsToShow = info.gridItems; + break; + + case 'direct': + gridItemsToShow = info.gridItemsFeaturedDirectly; + break; + + case 'indirect': + gridItemsToShow = info.gridItemsOnlyFeaturedIndirectly; + break; + } + + for (const gridItem of info.gridItems) { + if (gridItemsToShow.includes(gridItem)) { + gridItem.style.removeProperty('display'); + } else { + gridItem.style.display = 'none'; + } + } +} + +export function addPageListeners() { + const orderShowing = [ + 'all', + 'direct', + 'indirect', + ]; + + const orderFeaturedLines = [ + info.featuredAllLine, + info.featuredDirectLine, + info.featuredIndirectLine, + ]; + + const orderShowingLines = [ + info.showingAllLine, + info.showingDirectLine, + info.showingIndirectLine, + ]; + + const orderShowingLinks = [ + info.showingAllLink, + info.showingDirectLink, + info.showingIndirectLink, + ]; + + for (let index = 0; index < orderShowing.length; index++) { + if (!orderShowingLines[index]) continue; + + let nextIndex = index; + do { + if (nextIndex === orderShowing.length) { + nextIndex = 0; + } else { + nextIndex++; + } + } while (!orderShowingLinks[nextIndex]); + + const currentFeaturedLine = orderFeaturedLines[index]; + const currentShowingLine = orderShowingLines[index]; + const currentShowingLink = orderShowingLinks[index]; + + const nextFeaturedLine = orderFeaturedLines[nextIndex]; + const nextShowingLine = orderShowingLines[nextIndex]; + const nextShowing = orderShowing[nextIndex]; + + currentShowingLink.addEventListener('click', event => { + event.preventDefault(); + + currentFeaturedLine.style.display = 'none'; + currentShowingLine.style.display = 'none'; + + nextFeaturedLine.style.display = 'inline'; + nextShowingLine.style.display = 'inline'; + + filterArtTagGallery(nextShowing); + }); + } +} diff --git a/src/static/js/client/art-tag-network.js b/src/static/js/client/art-tag-network.js new file mode 100644 index 00000000..d0576152 --- /dev/null +++ b/src/static/js/client/art-tag-network.js @@ -0,0 +1,145 @@ +import {cssProp} from '../client-util.js'; + +import {atOffset, stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'artTagNetworkInfo', + + noneStatLink: null, + totalUsesStatLink: null, + directUsesStatLink: null, + descendantsStatLink: null, + leavesStatLink: null, + + tagsWithoutStats: null, + tagsWithStats: null, + + totalUsesStats: null, + directUsesStats: null, + descendantsStats: null, + leavesStats: null, +}; + +export function getPageReferences() { + if ( + document.documentElement.dataset.urlKey !== 'localized.listing' || + document.documentElement.dataset.urlValue0 !== 'tags/network' + ) { + return; + } + + info.noneStatLink = + document.getElementById('network-stat-none'); + + info.totalUsesStatLink = + document.getElementById('network-stat-total-uses'); + + info.directUsesStatLink = + document.getElementById('network-stat-direct-uses'); + + info.descendantsStatLink = + document.getElementById('network-stat-descendants'); + + info.leavesStatLink = + document.getElementById('network-stat-leaves'); + + info.tagsWithoutStats = + document.querySelectorAll('.network-tag:not(.with-stat)'); + + info.tagsWithStats = + document.querySelectorAll('.network-tag.with-stat'); + + info.totalUsesStats = + Array.from(document.getElementsByClassName('network-tag-total-uses-stat')); + + info.directUsesStats = + Array.from(document.getElementsByClassName('network-tag-direct-uses-stat')); + + info.descendantsStats = + Array.from(document.getElementsByClassName('network-tag-descendants-stat')); + + info.leavesStats = + Array.from(document.getElementsByClassName('network-tag-leaves-stat')); +} + +export function addPageListeners() { + if (!info.noneStatLink) return; + + const linkOrder = [ + info.noneStatLink, + info.totalUsesStatLink, + info.directUsesStatLink, + info.descendantsStatLink, + info.leavesStatLink, + ]; + + const statsOrder = [ + null, + info.totalUsesStats, + info.directUsesStats, + info.descendantsStats, + info.leavesStats, + ]; + + const stitched = + stitchArrays({ + link: linkOrder, + stats: statsOrder, + }); + + for (const [index, {link}] of stitched.entries()) { + const next = atOffset(stitched, index, +1, {wrap: true}); + + link.addEventListener('click', domEvent => { + domEvent.preventDefault(); + + cssProp(link, 'display', 'none'); + cssProp(next.link, 'display', null); + + if (next.stats === null) { + hideArtTagNetworkStats(); + } else { + showArtTagNetworkStats(next.stats); + } + }); + } +} + +function showArtTagNetworkStats(stats) { + for (const tagElement of info.tagsWithoutStats) { + cssProp(tagElement, 'display', 'none'); + } + + for (const tagElement of info.tagsWithStats) { + cssProp(tagElement, 'display', null); + } + + const allStats = [ + ...info.totalUsesStats, + ...info.directUsesStats, + ...info.descendantsStats, + ...info.leavesStats, + ]; + + const otherStats = + allStats + .filter(stat => !stats.includes(stat)); + + for (const statElement of otherStats) { + cssProp(statElement, 'display', 'none'); + } + + for (const statElement of stats) { + cssProp(statElement, 'display', null); + } +} + +function hideArtTagNetworkStats() { + for (const tagElement of info.tagsWithoutStats) { + cssProp(tagElement, 'display', null); + } + + for (const tagElement of info.tagsWithStats) { + cssProp(tagElement, 'display', 'none'); + } +} diff --git a/src/static/js/client/artist-external-link-tooltip.js b/src/static/js/client/artist-external-link-tooltip.js index 21ddfb91..2eadf916 100644 --- a/src/static/js/client/artist-external-link-tooltip.js +++ b/src/static/js/client/artist-external-link-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {accumulateSum, empty} from '../../shared-util/sugar.js'; import {info as hoverableTooltipInfo, repositionCurrentTooltip} diff --git a/src/static/js/client/artist-rolling-window.js b/src/static/js/client/artist-rolling-window.js new file mode 100644 index 00000000..b8ff7354 --- /dev/null +++ b/src/static/js/client/artist-rolling-window.js @@ -0,0 +1,571 @@ +import {cssProp, formatDate} from '../client-util.js'; + +import {sortByDate} from '../../shared-util/sort.js'; +import {chunkByConditions, chunkByProperties, empty, stitchArrays} + from '../../shared-util/sugar.js'; + +export const info = { + id: 'artistRollingWindowInfo', + + timeframeMonthsBefore: null, + timeframeMonthsAfter: null, + timeframeMonthsPeek: null, + + contributionKind: null, + contributionGroup: null, + + timeframeSelectionSomeLine: null, + timeframeSelectionNoneLine: null, + + timeframeSelectionContributionCount: null, + timeframeSelectionTimeframeCount: null, + timeframeSelectionFirstDate: null, + timeframeSelectionLastDate: null, + + timeframeSelectionControl: null, + timeframeSelectionMenu: null, + timeframeSelectionPrevious: null, + timeframeSelectionNext: null, + + timeframeEmptyLine: null, + + sourceArea: null, + sourceGrid: null, + sources: null, +}; + +export function getPageReferences() { + if (document.documentElement.dataset.urlKey !== 'localized.artistRollingWindow') { + return; + } + + info.timeframeMonthsBefore = + document.getElementById('timeframe-months-before'); + + info.timeframeMonthsAfter = + document.getElementById('timeframe-months-after'); + + info.timeframeMonthsPeek = + document.getElementById('timeframe-months-peek'); + + info.contributionKind = + document.getElementById('contribution-kind'); + + info.contributionGroup = + document.getElementById('contribution-group'); + + info.timeframeSelectionSomeLine = + document.getElementById('timeframe-selection-some'); + + info.timeframeSelectionNoneLine = + document.getElementById('timeframe-selection-none'); + + info.timeframeSelectionContributionCount = + document.getElementById('timeframe-selection-contribution-count'); + + info.timeframeSelectionTimeframeCount = + document.getElementById('timeframe-selection-timeframe-count'); + + info.timeframeSelectionFirstDate = + document.getElementById('timeframe-selection-first-date'); + + info.timeframeSelectionLastDate = + document.getElementById('timeframe-selection-last-date'); + + info.timeframeSelectionControl = + document.getElementById('timeframe-selection-control'); + + info.timeframeSelectionMenu = + document.getElementById('timeframe-selection-menu'); + + info.timeframeSelectionPrevious = + document.getElementById('timeframe-selection-previous'); + + info.timeframeSelectionNext = + document.getElementById('timeframe-selection-next'); + + info.timeframeEmptyLine = + document.getElementById('timeframe-empty'); + + info.sourceArea = + document.getElementById('timeframe-source-area'); + + info.sourceGrid = + info.sourceArea.querySelector('.grid-listing'); + + info.sources = + info.sourceGrid.getElementsByClassName('grid-item'); +} + +export function addPageListeners() { + if (!info.sourceArea) { + return; + } + + for (const input of [ + info.timeframeMonthsBefore, + info.timeframeMonthsAfter, + info.timeframeMonthsPeek, + info.contributionKind, + info.contributionGroup, + ]) { + input.addEventListener('change', () => { + updateArtistRollingWindow() + }); + } + + info.timeframeSelectionMenu.addEventListener('change', () => { + updateRollingWindowTimeframeSelection(); + }); + + const eatClicks = (element, callback) => { + element.addEventListener('click', domEvent => { + domEvent.preventDefault(); + callback(); + }); + + element.addEventListener('mousedown', domEvent => { + if (domEvent.detail > 1) { + domEvent.preventDefault(); + } + }); + }; + + eatClicks(info.timeframeSelectionNext, nextRollingTimeframeSelection); + eatClicks(info.timeframeSelectionPrevious, previousRollingTimeframeSelection); +} + +export function mutatePageContent() { + if (!info.sourceArea) { + return; + } + + updateArtistRollingWindow(); +} + +function previousRollingTimeframeSelection() { + const menu = info.timeframeSelectionMenu; + + if (menu.selectedIndex > 0) { + menu.selectedIndex--; + } + + updateRollingWindowTimeframeSelection(); +} + +function nextRollingTimeframeSelection() { + const menu = info.timeframeSelectionMenu; + + if (menu.selectedIndex < menu.length - 1) { + menu.selectedIndex++; + } + + updateRollingWindowTimeframeSelection(); +} + +function getArtistRollingWindowSourceInfo() { + const sourceElements = + Array.from(info.sources); + + const sourceTimeElements = + sourceElements + .map(el => Array.from(el.getElementsByTagName('time'))); + + const sourceTimeClasses = + sourceTimeElements + .map(times => times + .map(time => Array.from(time.classList))); + + const sourceKinds = + sourceTimeClasses + .map(times => times + .map(classes => classes + .find(cl => cl.endsWith('-contribution-date')) + .slice(0, -'-contribution-date'.length))); + + const sourceGroups = + sourceElements + .map(el => + Array.from(el.querySelectorAll('.contribution-group')) + .map(data => data.value)); + + const sourceDates = + sourceTimeElements + .map(times => times + .map(time => new Date(time.getAttribute('datetime')))); + + return stitchArrays({ + element: sourceElements, + kinds: sourceKinds, + groups: sourceGroups, + dates: sourceDates, + }); +} + +function getArtistRollingWindowTimeframeInfo() { + const contributionKind = + info.contributionKind.value; + + const contributionGroup = + info.contributionGroup.value; + + const sourceInfo = + getArtistRollingWindowSourceInfo(); + + const principalSources = + sourceInfo.filter(source => { + if (!source.kinds.includes(contributionKind)) { + return false; + } + + if (contributionGroup !== '-') { + if (!source.groups.includes(contributionGroup)) { + return false; + } + } + + return true; + }); + + const principalSourceDates = + principalSources.map(source => + stitchArrays({ + kind: source.kinds, + date: source.dates, + }).find(({kind}) => kind === contributionKind) + .date); + + const getPeekDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + - parseInt(info.timeframeMonthsBefore.value) + - parseInt(info.timeframeMonthsPeek.value))); + + return date; + }; + + const getEntranceDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + - parseInt(info.timeframeMonthsBefore.value))); + + return date; + }; + + const getExitDate = inputDate => { + const date = new Date(inputDate); + + date.setMonth( + (date.getMonth() + + parseInt(info.timeframeMonthsAfter.value))); + + return date; + }; + + const principalSourceIndices = + Array.from({length: principalSources.length}, (_, i) => i); + + const timeframeSourceChunks = + chunkByConditions(principalSourceIndices, [ + (previous, next) => + +principalSourceDates[previous] !== + +principalSourceDates[next], + ]); + + const timeframeSourceChunkDates = + timeframeSourceChunks + .map(indices => indices[0]) + .map(index => principalSourceDates[index]); + + const timeframeSourceChunkPeekDates = + timeframeSourceChunkDates + .map(getPeekDate); + + const timeframeSourceChunkEntranceDates = + timeframeSourceChunkDates + .map(getEntranceDate); + + const timeframeSourceChunkExitDates = + timeframeSourceChunkDates + .map(getExitDate); + + const peekDateInfo = + stitchArrays({ + peek: timeframeSourceChunkPeekDates, + indices: timeframeSourceChunks, + }).map(({peek, indices}) => ({ + date: peek, + peek: indices, + })); + + const entranceDateInfo = + stitchArrays({ + entrance: timeframeSourceChunkEntranceDates, + indices: timeframeSourceChunks, + }).map(({entrance, indices}) => ({ + date: entrance, + entrance: indices, + })); + + const exitDateInfo = + stitchArrays({ + exit: timeframeSourceChunkExitDates, + indices: timeframeSourceChunks, + }).map(({exit, indices}) => ({ + date: exit, + exit: indices, + })); + + const dateInfoChunks = + chunkByProperties( + sortByDate([ + ...peekDateInfo, + ...entranceDateInfo, + ...exitDateInfo, + ]), + ['date']); + + const dateInfo = + dateInfoChunks + .map(({chunk}) => + Object.assign({ + peek: null, + entrance: null, + exit: null, + }, ...chunk)); + + const timeframeInfo = + dateInfo.reduce( + (accumulator, {date, peek, entrance, exit}) => { + const previous = accumulator.at(-1); + + // These mustn't be mutated! + let peeking = (previous ? previous.peeking : []); + let tracking = (previous ? previous.tracking : []); + + if (peek) { + peeking = + peeking.concat(peek); + } + + if (entrance) { + peeking = + peeking.filter(index => !entrance.includes(index)); + + tracking = + tracking.concat(entrance); + } + + if (exit) { + tracking = + tracking.filter(index => !exit.includes(index)); + } + + return [...accumulator, { + date, + peeking, + tracking, + peek, + entrance, + exit, + }]; + }, + []); + + const indicesToSources = indices => + (indices + ? indices.map(index => principalSources[index]) + : null); + + const finalizedTimeframeInfo = + timeframeInfo.map(({ + date, + peeking, + tracking, + peek, + entrance, + exit, + }) => ({ + date, + peeking: indicesToSources(peeking), + tracking: indicesToSources(tracking), + peek: indicesToSources(peek), + entrance: indicesToSources(entrance), + exit: indicesToSources(exit), + })); + + return finalizedTimeframeInfo; +} + +function updateArtistRollingWindow() { + const timeframeInfo = + getArtistRollingWindowTimeframeInfo(); + + if (empty(timeframeInfo)) { + cssProp(info.timeframeSelectionControl, 'display', 'none'); + cssProp(info.timeframeSelectionSomeLine, 'display', 'none'); + cssProp(info.timeframeSelectionNoneLine, 'display', null); + + updateRollingWindowTimeframeSelection(timeframeInfo); + + return; + } + + cssProp(info.timeframeSelectionControl, 'display', null); + cssProp(info.timeframeSelectionSomeLine, 'display', null); + cssProp(info.timeframeSelectionNoneLine, 'display', 'none'); + + // The last timeframe is just the exit of the final tracked sources, + // so we aren't going to display a menu option for it, and will just use + // it as the end of the final option's date range. + + const usedTimeframes = timeframeInfo.slice(0, -1); + const firstTimeframe = timeframeInfo.at(0); + const lastTimeframe = timeframeInfo.at(-1); + + const sourceCount = + timeframeInfo + .flatMap(({entrance}) => entrance ?? []) + .length; + + const timeframeCount = + usedTimeframes.length; + + info.timeframeSelectionContributionCount.innerText = sourceCount; + info.timeframeSelectionTimeframeCount.innerText = timeframeCount; + + const firstDate = firstTimeframe.date; + const lastDate = lastTimeframe.date; + + info.timeframeSelectionFirstDate.innerText = formatDate(firstDate); + info.timeframeSelectionLastDate.innerText = formatDate(lastDate); + + while (info.timeframeSelectionMenu.firstChild) { + info.timeframeSelectionMenu.firstChild.remove(); + } + + for (const [index, timeframe] of usedTimeframes.entries()) { + const nextTimeframe = timeframeInfo[index + 1]; + + const option = document.createElement('option'); + + option.appendChild(document.createTextNode( + `${formatDate(timeframe.date)} – ${formatDate(nextTimeframe.date)}`)); + + info.timeframeSelectionMenu.appendChild(option); + } + + updateRollingWindowTimeframeSelection(timeframeInfo); +} + +function updateRollingWindowTimeframeSelection(timeframeInfo) { + timeframeInfo ??= getArtistRollingWindowTimeframeInfo(); + + updateRollingWindowTimeframeSelectionControls(timeframeInfo); + updateRollingWindowTimeframeSelectionSources(timeframeInfo); +} + +function updateRollingWindowTimeframeSelectionControls(timeframeInfo) { + const currentIndex = + info.timeframeSelectionMenu.selectedIndex; + + const atFirstTimeframe = + currentIndex === 0; + + // The last actual timeframe is empty and not displayed as a menu option. + const atLastTimeframe = + currentIndex === timeframeInfo.length - 2; + + if (atFirstTimeframe) { + info.timeframeSelectionPrevious.removeAttribute('href'); + } else { + info.timeframeSelectionPrevious.setAttribute('href', '#'); + } + + if (atLastTimeframe) { + info.timeframeSelectionNext.removeAttribute('href'); + } else { + info.timeframeSelectionNext.setAttribute('href', '#'); + } +} + +function updateRollingWindowTimeframeSelectionSources(timeframeInfo) { + const currentIndex = + info.timeframeSelectionMenu.selectedIndex; + + const contributionGroup = + info.contributionGroup.value; + + cssProp(info.sourceGrid, 'display', null); + + const {peeking: peekingSources, tracking: trackingSources} = + (empty(timeframeInfo) + ? {peeking: [], tracking: []} + : timeframeInfo[currentIndex]); + + const peekingElements = + peekingSources.map(source => source.element); + + const trackingElements = + trackingSources.map(source => source.element); + + const showingElements = + [...trackingElements, ...peekingElements]; + + const hidingElements = + Array.from(info.sources) + .filter(element => + !peekingElements.includes(element) && + !trackingElements.includes(element)); + + for (const element of peekingElements) { + element.classList.add('peeking'); + element.classList.remove('tracking'); + } + + for (const element of trackingElements) { + element.classList.remove('peeking'); + element.classList.add('tracking'); + } + + for (const element of hidingElements) { + element.classList.remove('peeking'); + element.classList.remove('tracking'); + cssProp(element, 'display', 'none'); + } + + for (const element of showingElements) { + cssProp(element, 'display', null); + + for (const time of element.getElementsByTagName('time')) { + for (const className of time.classList) { + if (!className.endsWith('-contribution-date')) continue; + + const kind = className.slice(0, -'-contribution-date'.length); + if (kind === info.contributionKind.value) { + cssProp(time, 'display', null); + } else { + cssProp(time, 'display', 'none'); + } + } + } + + for (const data of element.getElementsByClassName('contribution-group')) { + if (contributionGroup === '-' || data.value !== contributionGroup) { + cssProp(data, 'display', null); + } else { + cssProp(data, 'display', 'none'); + } + } + } + + if (empty(peekingElements) && empty(trackingElements)) { + cssProp(info.timeframeEmptyLine, 'display', null); + } else { + cssProp(info.timeframeEmptyLine, 'display', 'none'); + } +} diff --git a/src/static/js/client/css-compatibility-assistant.js b/src/static/js/client/css-compatibility-assistant.js index 6e7b15b5..37b0645a 100644 --- a/src/static/js/client/css-compatibility-assistant.js +++ b/src/static/js/client/css-compatibility-assistant.js @@ -1,22 +1,28 @@ -/* eslint-env browser */ +import {stitchArrays} from '../../shared-util/sugar.js'; export const info = { id: 'cssCompatibilityAssistantInfo', - coverArtContainer: null, - coverArtImageDetails: null, + coverArtworks: null, + coverArtworkImageDetails: null, }; export function getPageReferences() { - info.coverArtContainer = - document.getElementById('cover-art-container'); + info.coverArtworks = + Array.from(document.querySelectorAll('.cover-artwork')); - info.coverArtImageDetails = - info.coverArtContainer?.querySelector('.image-details'); + info.coverArtworkImageDetails = + info.coverArtworks + .map(artwork => artwork.querySelector('.image-details')); } export function mutatePageContent() { - if (info.coverArtImageDetails) { - info.coverArtContainer.classList.add('has-image-details'); - } + stitchArrays({ + coverArtwork: info.coverArtworks, + imageDetails: info.coverArtworkImageDetails, + }).forEach(({coverArtwork, imageDetails}) => { + if (imageDetails) { + coverArtwork.classList.add('has-image-details'); + } + }); } diff --git a/src/static/js/client/datetimestamp-tooltip.js b/src/static/js/client/datetimestamp-tooltip.js index 46d1cd5b..00530484 100644 --- a/src/static/js/client/datetimestamp-tooltip.js +++ b/src/static/js/client/datetimestamp-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // TODO: Maybe datetimestamps can just be incorporated into text-with-tooltip? import {stitchArrays} from '../../shared-util/sugar.js'; diff --git a/src/static/js/client/dragged-link.js b/src/static/js/client/dragged-link.js index 56021e7f..3a4ee314 100644 --- a/src/static/js/client/dragged-link.js +++ b/src/static/js/client/dragged-link.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - export const info = { id: `draggedLinkInfo`, diff --git a/src/static/js/client/expandable-grid-section.js b/src/static/js/client/expandable-grid-section.js new file mode 100644 index 00000000..4d6e0058 --- /dev/null +++ b/src/static/js/client/expandable-grid-section.js @@ -0,0 +1,83 @@ +import {cssProp} from '../client-util.js'; + +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'expandableGallerySectionInfo', + + items: null, + toggles: null, + expandCues: null, + collapseCues: null, +}; + +export function getPageReferences() { + const expandos = + Array.from(document.querySelectorAll('.grid-expando')); + + const grids = + expandos + .map(expando => expando.closest('.grid-listing')); + + info.items = + grids + .map(grid => grid.querySelectorAll('.grid-item')) + .map(items => Array.from(items)); + + info.toggles = + expandos + .map(expando => expando.querySelector('.grid-expando-toggle')); + + info.expandCues = + info.toggles + .map(toggle => toggle.querySelector('.grid-expand-cue')); + + info.collapseCues = + info.toggles + .map(toggle => toggle.querySelector('.grid-collapse-cue')); +} + +export function addPageListeners() { + stitchArrays({ + items: info.items, + toggle: info.toggles, + expandCue: info.expandCues, + collapseCue: info.collapseCues, + }).forEach(({ + items, + toggle, + expandCue, + collapseCue, + }) => { + toggle.addEventListener('click', domEvent => { + domEvent.preventDefault(); + + const collapsed = + items.some(item => + item.classList.contains('hidden-by-expandable-cut')); + + for (const item of items) { + if ( + !item.classList.contains('hidden-by-expandable-cut') && + !item.classList.contains('shown-by-expandable-cut') + ) continue; + + if (collapsed) { + item.classList.remove('hidden-by-expandable-cut'); + item.classList.add('shown-by-expandable-cut'); + } else { + item.classList.add('hidden-by-expandable-cut'); + item.classList.remove('shown-by-expandable-cut'); + } + } + + if (collapsed) { + cssProp(expandCue, 'display', 'none'); + cssProp(collapseCue, 'display', null); + } else { + cssProp(expandCue, 'display', null); + cssProp(collapseCue, 'display', 'none'); + } + }); + }); +} diff --git a/src/static/js/client/gallery-style-selector.js b/src/static/js/client/gallery-style-selector.js new file mode 100644 index 00000000..44f98ac3 --- /dev/null +++ b/src/static/js/client/gallery-style-selector.js @@ -0,0 +1,121 @@ +import {cssProp} from '../client-util.js'; + +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'galleryStyleSelectorInfo', + + selectors: null, + sections: null, + + selectorStyleInputs: null, + selectorStyleInputStyles: null, + + selectorReleaseItems: null, + selectorReleaseItemStyles: null, + + selectorCountAll: null, + selectorCountFiltered: null, + selectorCountFilteredCount: null, + selectorCountNone: null, +}; + +export function getPageReferences() { + info.selectors = + Array.from(document.querySelectorAll('.gallery-style-selector')); + + info.sections = + info.selectors + .map(selector => selector.closest('section')); + + info.selectorStyleInputs = + info.selectors + .map(selector => selector.querySelectorAll('.styles input')) + .map(inputs => Array.from(inputs)); + + info.selectorStyleInputStyles = + info.selectorStyleInputs + .map(inputs => inputs + .map(input => input.closest('label').dataset.style)); + + info.selectorReleaseItems = + info.sections + .map(section => section.querySelectorAll('.grid-item')) + .map(items => Array.from(items)); + + info.selectorReleaseItemStyles = + info.selectorReleaseItems + .map(items => items + .map(item => item.dataset.style)); + + info.selectorCountAll = + info.selectors + .map(selector => selector.querySelector('.count.all')); + + info.selectorCountFiltered = + info.selectors + .map(selector => selector.querySelector('.count.filtered')); + + info.selectorCountFilteredCount = + info.selectorCountFiltered + .map(selector => selector.querySelector('span')); + + info.selectorCountNone = + info.selectors + .map(selector => selector.querySelector('.count.none')); +} + +export function addPageListeners() { + for (const index of info.selectors.keys()) { + for (const input of info.selectorStyleInputs[index]) { + input.addEventListener('input', () => updateVisibleReleases(index)); + } + } +} + +function updateVisibleReleases(index) { + const inputs = info.selectorStyleInputs[index]; + const inputStyles = info.selectorStyleInputStyles[index]; + + const selectedStyles = + stitchArrays({input: inputs, style: inputStyles}) + .filter(({input}) => input.checked) + .map(({style}) => style); + + const releases = info.selectorReleaseItems[index]; + const releaseStyles = info.selectorReleaseItemStyles[index]; + + let visible = 0; + + stitchArrays({ + release: releases, + style: releaseStyles, + }).forEach(({release, style}) => { + if (selectedStyles.includes(style)) { + release.classList.remove('hidden-by-style-mismatch'); + visible++; + } else { + release.classList.add('hidden-by-style-mismatch'); + } + }); + + const countAll = info.selectorCountAll[index]; + const countFiltered = info.selectorCountFiltered[index]; + const countFilteredCount = info.selectorCountFilteredCount[index]; + const countNone = info.selectorCountNone[index]; + + if (visible === releases.length) { + cssProp(countAll, 'display', null); + cssProp(countFiltered, 'display', 'none'); + cssProp(countNone, 'display', 'none'); + } else if (visible === 0) { + cssProp(countAll, 'display', 'none'); + cssProp(countFiltered, 'display', 'none'); + cssProp(countNone, 'display', null); + } else { + cssProp(countAll, 'display', 'none'); + cssProp(countFiltered, 'display', null); + cssProp(countNone, 'display', 'none'); + countFilteredCount.innerHTML = visible; + } +} diff --git a/src/static/js/client/hash-link.js b/src/static/js/client/hash-link.js index 27035e29..02ffdc23 100644 --- a/src/static/js/client/hash-link.js +++ b/src/static/js/client/hash-link.js @@ -1,6 +1,5 @@ -/* eslint-env browser */ - -import {filterMultipleArrays, stitchArrays} from '../../shared-util/sugar.js'; +import {filterMultipleArrays, stitchArrays, unique} + from '../../shared-util/sugar.js'; import {dispatchInternalEvent} from '../client-util.js'; @@ -11,6 +10,9 @@ export const info = { hrefs: null, targets: null, + details: null, + detailsIDs: null, + state: { highlightedTarget: null, scrollingAfterClick: false, @@ -40,6 +42,19 @@ export function getPageReferences() { info.hrefs, info.targets, (_link, _href, target) => target); + + info.details = + unique([ + ...document.querySelectorAll('details[id]'), + ... + Array.from(document.querySelectorAll('summary[id]')) + .map(summary => summary.closest('details')), + ]); + + info.detailsIDs = + info.details.map(details => + details.id || + details.querySelector('summary').id); } function processScrollingAfterHashLinkClicked() { @@ -60,6 +75,15 @@ function processScrollingAfterHashLinkClicked() { }, 200); } +export function mutatePageContent() { + if (location.hash.length > 1) { + const target = document.getElementById(location.hash.slice(1)); + if (target) { + expandDetails(target); + } + } +} + export function addPageListeners() { // Instead of defining a scroll offset (to account for the sticky heading) // in JavaScript, we interface with the CSS property 'scroll-margin-top'. @@ -94,6 +118,8 @@ export function addPageListeners() { return; } + expandDetails(target); + // Hide skipper box right away, so the layout is updated on time for the // math operations coming up next. const skipper = document.getElementById('skippers'); @@ -143,4 +169,32 @@ export function addPageListeners() { state.highlightedTarget = null; }); } + + stitchArrays({ + details: info.details, + id: info.detailsIDs, + }).forEach(({details, id}) => { + details.addEventListener('toggle', () => { + if (!details.open) { + detractHash(id); + } + }); + }); +} + +function expandDetails(target) { + if (target.nodeName === 'SUMMARY') { + const details = target.closest('details'); + if (details) { + details.open = true; + } + } else if (target.nodeName === 'DETAILS') { + target.open = true; + } +} + +function detractHash(id) { + if (location.hash === '#' + id) { + history.pushState({}, undefined, location.href.replace(/#.*$/, '')); + } } diff --git a/src/static/js/client/hoverable-tooltip.js b/src/static/js/client/hoverable-tooltip.js index 484f2ab0..22b9471c 100644 --- a/src/static/js/client/hoverable-tooltip.js +++ b/src/static/js/client/hoverable-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {empty, filterMultipleArrays} from '../../shared-util/sugar.js'; import {WikiRect} from '../rectangles.js'; @@ -118,17 +116,17 @@ export function registerTooltipElement(tooltip) { handleTooltipMouseLeft(tooltip); }); - tooltip.addEventListener('focusin', event => { - handleTooltipReceivedFocus(tooltip, event.relatedTarget); + tooltip.addEventListener('focusin', domEvent => { + handleTooltipReceivedFocus(tooltip, domEvent.relatedTarget); }); - tooltip.addEventListener('focusout', event => { + tooltip.addEventListener('focusout', domEvent => { // This event gets activated for tabbing *between* links inside the // tooltip, which is no good and certainly doesn't represent the focus // leaving the tooltip. - if (currentlyShownTooltipHasFocus(event.relatedTarget)) return; + if (currentlyShownTooltipHasFocus(domEvent.relatedTarget)) return; - handleTooltipLostFocus(tooltip, event.relatedTarget); + handleTooltipLostFocus(tooltip, domEvent.relatedTarget); }); } @@ -158,20 +156,20 @@ export function registerTooltipHoverableElement(hoverable, tooltip) { handleTooltipHoverableMouseLeft(hoverable); }); - hoverable.addEventListener('focusin', event => { - handleTooltipHoverableReceivedFocus(hoverable, event); + hoverable.addEventListener('focusin', domEvent => { + handleTooltipHoverableReceivedFocus(hoverable, domEvent); }); - hoverable.addEventListener('focusout', event => { - handleTooltipHoverableLostFocus(hoverable, event); + hoverable.addEventListener('focusout', domEvent => { + handleTooltipHoverableLostFocus(hoverable, domEvent); }); - hoverable.addEventListener('touchend', event => { - handleTooltipHoverableTouchEnded(hoverable, event); + hoverable.addEventListener('touchend', domEvent => { + handleTooltipHoverableTouchEnded(hoverable, domEvent); }); - hoverable.addEventListener('click', event => { - handleTooltipHoverableClicked(hoverable, event); + hoverable.addEventListener('click', domEvent => { + handleTooltipHoverableClicked(hoverable, domEvent); }); } @@ -416,7 +414,7 @@ function handleTooltipHoverableTouchEnded(hoverable, domEvent) { }, 1200); } -function handleTooltipHoverableClicked(hoverable) { +function handleTooltipHoverableClicked(hoverable, domEvent) { const {state} = info; // Don't navigate away from the page if the this hoverable was recently @@ -426,7 +424,7 @@ function handleTooltipHoverableClicked(hoverable) { state.currentlyActiveHoverable === hoverable && state.hoverableWasRecentlyTouched ) { - event.preventDefault(); + domEvent.preventDefault(); } } @@ -576,6 +574,17 @@ export function showTooltipFromHoverable(hoverable) { hoverable.classList.add('has-visible-tooltip'); + const isolator = + hoverable.closest('.isolate-tooltip-z-indexing > *'); + + if (isolator) { + for (const child of isolator.parentElement.children) { + cssProp(child, 'z-index', null); + } + + cssProp(isolator, 'z-index', '1'); + } + positionTooltipFromHoverableWithBrains(hoverable); cssProp(tooltip, 'display', 'block'); @@ -667,12 +676,12 @@ export function positionTooltipFromHoverableWithBrains(hoverable) { for (let i = 0; i < numBaselineRects; i++) { for (const [dir1, dir2] of [ + ['down', 'right'], + ['down', 'left'], ['right', 'down'], ['left', 'down'], ['right', 'up'], ['left', 'up'], - ['down', 'right'], - ['down', 'left'], ['up', 'right'], ['up', 'left'], ]) { @@ -995,6 +1004,14 @@ export function getTooltipBaselineOpportunityAreas(tooltip) { return results; } +export function mutatePageContent() { + for (const isolatorRoot of document.querySelectorAll('.isolate-tooltip-z-indexing')) { + if (isolatorRoot.firstElementChild) { + cssProp(isolatorRoot.firstElementChild, 'z-index', '1'); + } + } +} + export function addPageListeners() { const {state} = info; diff --git a/src/static/js/client/image-overlay.js b/src/static/js/client/image-overlay.js index b51d57a4..0595bff7 100644 --- a/src/static/js/client/image-overlay.js +++ b/src/static/js/client/image-overlay.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {getColors} from '../../shared-util/colors.js'; import {cssProp} from '../client-util.js'; @@ -66,8 +64,13 @@ export function getPageReferences() { info.fileSizeWarning = document.getElementById('image-overlay-file-size-warning'); + const linkQuery = [ + '.image-link', + '.image-media-link', + ].join(', '); + info.links = - Array.from(document.querySelectorAll('.image-link')) + Array.from(document.querySelectorAll(linkQuery)) .filter(link => !link.closest('.no-image-preview')); } @@ -88,10 +91,13 @@ function handleContainerClicked(evt) { return; } - // If you clicked anything close to or beneath the action bar, don't hide - // the image overlay. + // If you clicked anything near the action bar, don't hide the + // image overlay. const rect = info.actionContainer.getBoundingClientRect(); - if (evt.clientY >= rect.top - 40) { + if ( + evt.clientY >= rect.top - 40 && evt.clientY <= rect.bottom + 40 && + evt.clientX >= rect.left + 20 && evt.clientX <= rect.right - 20 + ) { return; } @@ -141,13 +147,24 @@ function getImageLinkDetails(imageLink) { a.href, embeddedSrc: - img.src, + img?.src || + img?.currentSrc || + a.dataset.embedSrc, originalFileSize: - img.dataset.originalSize, + img?.dataset.originalSize ?? + a.dataset.originalSize ?? + null, availableThumbList: - img.dataset.thumbs, + img?.dataset.thumbs ?? + a.dataset.thumbs ?? + null, + + dimensions: + img?.dataset.dimensions?.split('x') ?? + a.dataset.dimensions?.split('x') ?? + null, color: cssProp(imageLink, '--primary-color'), @@ -170,7 +187,7 @@ function getImageSources(details) { }; } else { return { - mainSrc: originalSrc, + mainSrc: details.originalSrc, thumbSrc: null, mainThumb: '', thumbThumb: '', @@ -211,15 +228,31 @@ async function loadOverlayImage(details) { if (details.thumbSrc) { info.thumbImage.src = details.thumbSrc; info.thumbImage.style.display = null; + info.container.classList.remove('no-thumb'); } else { info.thumbImage.src = ''; info.thumbImage.style.display = 'none'; + info.container.classList.add('no-thumb'); } // Show the thumbnail size on each <img> element's data attributes. // Y'know, just for debugging convenience. info.mainImage.dataset.displayingThumb = details.mainThumb; - info.thumbImage.dataset.displayingThumb = details.thumbThubm; + info.thumbImage.dataset.displayingThumb = details.thumbThumb; + + if (details.dimensions) { + info.mainImage.width = details.dimensions[0]; + info.mainImage.height = details.dimensions[1]; + info.thumbImage.width = details.dimensions[0]; + info.thumbImage.height = details.dimensions[1]; + cssProp(info.thumbImage, 'aspect-ratio', details.dimensions.join('/')); + } else { + info.mainImage.removeAttribute('width'); + info.mainImage.removeAttribute('height'); + info.thumbImage.removeAttribute('width'); + info.thumbImage.removeAttribute('height'); + cssProp(info.thumbImage, 'aspect-ratio', null); + } info.mainImage.addEventListener('load', handleMainImageLoaded); info.mainImage.addEventListener('error', handleMainImageErrored); diff --git a/src/static/js/client/index.js b/src/static/js/client/index.js index 52d2afd6..16ebe89f 100644 --- a/src/static/js/client/index.js +++ b/src/static/js/client/index.js @@ -1,19 +1,24 @@ -/* eslint-env browser */ - import '../group-contributions-table.js'; import * as additionalNamesBoxModule from './additional-names-box.js'; import * as albumCommentarySidebarModule from './album-commentary-sidebar.js'; +import * as artTagGalleryFilterModule from './art-tag-gallery-filter.js'; +import * as artTagNetworkModule from './art-tag-network.js'; import * as artistExternalLinkTooltipModule from './artist-external-link-tooltip.js'; +import * as artistRollingWindowModule from './artist-rolling-window.js'; import * as cssCompatibilityAssistantModule from './css-compatibility-assistant.js'; import * as datetimestampTooltipModule from './datetimestamp-tooltip.js'; import * as draggedLinkModule from './dragged-link.js'; +import * as expandableGridSectionModule from './expandable-grid-section.js'; +import * as galleryStyleSelectorModule from './gallery-style-selector.js'; import * as hashLinkModule from './hash-link.js'; import * as hoverableTooltipModule from './hoverable-tooltip.js'; import * as imageOverlayModule from './image-overlay.js'; import * as intrapageDotSwitcherModule from './intrapage-dot-switcher.js'; import * as liveMousePositionModule from './live-mouse-position.js'; +import * as memorableDetailsModule from './memorable-details.js'; import * as quickDescriptionModule from './quick-description.js'; +import * as revealAllGridControlModule from './reveal-all-grid-control.js'; import * as scriptedLinkModule from './scripted-link.js'; import * as sidebarSearchModule from './sidebar-search.js'; import * as stickyHeadingModule from './sticky-heading.js'; @@ -24,16 +29,23 @@ import * as wikiSearchModule from './wiki-search.js'; export const modules = [ additionalNamesBoxModule, albumCommentarySidebarModule, + artTagGalleryFilterModule, + artTagNetworkModule, artistExternalLinkTooltipModule, + artistRollingWindowModule, cssCompatibilityAssistantModule, datetimestampTooltipModule, draggedLinkModule, + expandableGridSectionModule, + galleryStyleSelectorModule, hashLinkModule, hoverableTooltipModule, imageOverlayModule, intrapageDotSwitcherModule, liveMousePositionModule, + memorableDetailsModule, quickDescriptionModule, + revealAllGridControlModule, scriptedLinkModule, sidebarSearchModule, stickyHeadingModule, @@ -44,7 +56,10 @@ export const modules = [ const clientInfo = window.hsmusicClientInfo = Object.create(null); -const clientSteps = { +// These steps are always run in the listed order, on page load. +// So for example, all modules' getPageReferences steps are evaluated, then +// all modules' addInternalListeners steps are evaluated, and so on. +const setupSteps = { getPageReferences: [], addInternalListeners: [], mutatePageContent: [], @@ -52,6 +67,18 @@ const clientSteps = { addPageListeners: [], }; +// These steps are run only on certain triggers. Those are global events, +// so all modules (which specify that step) respond in sequence. +const situationalSteps = { + /* There's none yet... sorry... */ +}; + +const stepInfoSymbol = Symbol(); + +const boundSessionStorage = + window.hsmusicBoundSessionStorage = + Object.create(null); + for (const module of modules) { const {info} = module; @@ -136,12 +163,47 @@ for (const module of modules) { const storageKey = `hsmusic.${infoKey}.${key}`; + // There are two storage systems besides actual session storage in play. + // + // "Fallback" is for if session storage is not available, which may + // suddenly become the case, i.e. access is temporarily revoked or fails. + // The fallback value is controlled completely internally i.e. in this + // infrastructure, in this lexical scope. + // + // "Bound" is for if the value kept in session storage was saved to + // the page when the page was initially loaded, rather than a living + // window on session storage (which may be affected by pages later in + // the history stack). Whether or not bound storage is in effect is + // controlled at page load (of course), by each module's own logic. + // + // Asterisk: Bound storage can't work miracles and if the page is + // actually deloaded with its JavaScript state discarded, the bound + // values are lost, even if the browser recreates on-page form state. + let fallbackValue = defaultValue; + let boundValue = undefined; + + const updateBoundValue = (givenValue = undefined) => { + if (givenValue) { + if ( + infoKey in boundSessionStorage && + key in boundSessionStorage[infoKey] + ) { + boundSessionStorage[infoKey][key] = givenValue; + } + } else { + boundValue = boundSessionStorage[infoKey]?.[key]; + } + }; Object.defineProperty(info.session, key, { get: () => { + updateBoundValue(); + let value; - try { + if (boundValue !== undefined) { + value = boundValue ?? defaultValue; + } else try { value = sessionStorage.getItem(storageKey) ?? defaultValue; } catch (error) { if (error instanceof DOMException) { @@ -177,21 +239,23 @@ for (const module of modules) { return; } - let operation; + let sessionOperation; if (value === '') { fallbackValue = null; - operation = () => { + updateBoundValue(null); + sessionOperation = () => { sessionStorage.removeItem(storageKey); }; } else { fallbackValue = value; - operation = () => { + updateBoundValue(value); + sessionOperation = () => { sessionStorage.setItem(storageKey, value); }; } try { - operation(); + sessionOperation(); } catch (error) { if (!(error instanceof DOMException)) { throw error; @@ -204,28 +268,68 @@ for (const module of modules) { Object.preventExtensions(info.session); } - for (const key of Object.keys(clientSteps)) { - if (Object.hasOwn(module, key)) { - const fn = module[key]; + for (const stepsObject of [setupSteps, situationalSteps]) { + for (const key of Object.keys(stepsObject)) { + if (Object.hasOwn(module, key)) { + const fn = module[key]; - Object.defineProperty(fn, 'name', { - value: `${infoKey}/${fn.name}`, - }); + fn[stepInfoSymbol] = info; + + Object.defineProperty(fn, 'name', { + value: `${infoKey}/${fn.name}`, + }); + + stepsObject[key].push(fn); + } + } + } +} + +function evaluateBindSessionStorageStep(bindSessionStorage) { + const {id: infoKey, session: moduleExposedSessionObject} = + bindSessionStorage[stepInfoSymbol]; + + const generator = bindSessionStorage(); - clientSteps[key].push(fn); + let lastBoundValue; + while (true) { + const {value: key, done} = generator.next(lastBoundValue); + const storageKey = `hsmusic.${infoKey}.${key}`; + + let value = undefined; + try { + value = sessionStorage.getItem(storageKey); + } catch (error) { + if (!(error instanceof DOMException)) { + throw error; + } } + + if (value === undefined) { + // This effectively gets the default value. + value = moduleExposedSessionObject[key]; + } + + boundSessionStorage[infoKey] ??= Object.create(null); + boundSessionStorage[infoKey][key] = value; + + lastBoundValue = value; + + if (done) break; } } -for (const [key, steps] of Object.entries(clientSteps)) { - for (const step of steps) { +function evaluateStep(stepsObject, key) { + for (const step of stepsObject[key]) { try { step(); } catch (error) { - // TODO: Be smarter about not running later steps for the same module! - // Or maybe not, since an error is liable to cause explosions anyway. console.error(`During ${key}, failed to run ${step.name}`); console.error(error); } } } + +for (const key of Object.keys(setupSteps)) { + evaluateStep(setupSteps, key); +} diff --git a/src/static/js/client/intrapage-dot-switcher.js b/src/static/js/client/intrapage-dot-switcher.js index d06bc5a6..b9a27a9b 100644 --- a/src/static/js/client/intrapage-dot-switcher.js +++ b/src/static/js/client/intrapage-dot-switcher.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {stitchArrays} from '../../shared-util/sugar.js'; import {cssProp} from '../client-util.js'; diff --git a/src/static/js/client/live-mouse-position.js b/src/static/js/client/live-mouse-position.js index 36a28429..32fc5bf4 100644 --- a/src/static/js/client/live-mouse-position.js +++ b/src/static/js/client/live-mouse-position.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - export const info = { id: 'liveMousePositionInfo', diff --git a/src/static/js/client/memorable-details.js b/src/static/js/client/memorable-details.js new file mode 100644 index 00000000..57d9fde8 --- /dev/null +++ b/src/static/js/client/memorable-details.js @@ -0,0 +1,62 @@ +import {stitchArrays} from '../../shared-util/sugar.js'; + +export const info = { + id: 'memorableDetailsInfo', + + details: null, + ids: null, + + session: { + openDetails: { + type: 'json', + maxLength: settings => settings.maxOpenDetailsStorage, + }, + }, + + settings: { + maxOpenDetailsStorage: 1000, + }, +}; + +export function getPageReferences() { + info.details = + Array.from(document.querySelectorAll('details.memorable')); + + info.ids = + info.details.map(details => details.getAttribute('data-memorable-id')); +} + +export function mutatePageContent() { + stitchArrays({ + details: info.details, + id: info.ids, + }).forEach(({details, id}) => { + if (info.session.openDetails?.includes(id)) { + details.open = true; + } + }); +} + +export function addPageListeners() { + for (const [index, details] of info.details.entries()) { + details.addEventListener('toggle', () => { + handleDetailsToggled(index); + }); + } +} + +function handleDetailsToggled(index) { + const details = info.details[index]; + const id = info.ids[index]; + + if (details.open) { + if (info.session.openDetails) { + info.session.openDetails = [...info.session.openDetails, id]; + } else { + info.session.openDetails = [id]; + } + } else if (info.session.openDetails?.includes(id)) { + info.session.openDetails = + info.session.openDetails.filter(item => item !== id); + } +} diff --git a/src/static/js/client/quick-description.js b/src/static/js/client/quick-description.js index cff82252..9117d48c 100644 --- a/src/static/js/client/quick-description.js +++ b/src/static/js/client/quick-description.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {stitchArrays} from '../../shared-util/sugar.js'; export const info = { diff --git a/src/static/js/client/reveal-all-grid-control.js b/src/static/js/client/reveal-all-grid-control.js new file mode 100644 index 00000000..0572a190 --- /dev/null +++ b/src/static/js/client/reveal-all-grid-control.js @@ -0,0 +1,70 @@ +import {cssProp} from '../client-util.js'; + +export const info = { + id: 'revealAllGridControlInfo', + + revealAllLinks: null, + revealables: null, + + revealLabels: null, + concealLabels: null, +}; + +export function getPageReferences() { + info.revealAllLinks = + Array.from(document.querySelectorAll('.reveal-all a')); + + info.revealables = + info.revealAllLinks + .map(link => link.closest('.grid-listing')) + .map(listing => listing.querySelectorAll('.reveal')); + + info.revealLabels = + info.revealAllLinks + .map(link => link.querySelector('.reveal-label')); + + info.concealLabels = + info.revealAllLinks + .map(link => link.querySelector('.conceal-label')); +} + +export function addPageListeners() { + for (const [index, link] of info.revealAllLinks.entries()) { + link.addEventListener('click', domEvent => { + domEvent.preventDefault(); + handleRevealAllLinkClicked(index); + }); + } +} + +export function addInternalListeners() { + // Don't even think about it. "Reveal all artworks" is a stable control, + // meaning it only changes because the user interacted with it directly. +} + +function handleRevealAllLinkClicked(index) { + const revealables = info.revealables[index]; + const revealLabel = info.revealLabels[index]; + const concealLabel = info.concealLabels[index]; + + const shouldReveal = + (cssProp(revealLabel, 'display') === 'none' + ? false + : true); + + for (const revealable of revealables) { + if (shouldReveal) { + revealable.classList.add('revealed'); + } else { + revealable.classList.remove('revealed'); + } + } + + if (shouldReveal) { + cssProp(revealLabel, 'display', 'none'); + cssProp(concealLabel, 'display', null); + } else { + cssProp(revealLabel, 'display', null); + cssProp(concealLabel, 'display', 'none'); + } +} diff --git a/src/static/js/client/scripted-link.js b/src/static/js/client/scripted-link.js index 8b8d8a13..badc6ccb 100644 --- a/src/static/js/client/scripted-link.js +++ b/src/static/js/client/scripted-link.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {pick, stitchArrays} from '../../shared-util/sugar.js'; import { diff --git a/src/static/js/client/sidebar-search.js b/src/static/js/client/sidebar-search.js index c79fb837..8b29cf63 100644 --- a/src/static/js/client/sidebar-search.js +++ b/src/static/js/client/sidebar-search.js @@ -1,10 +1,15 @@ -/* eslint-env browser */ - import {getColors} from '../../shared-util/colors.js'; -import {accumulateSum, empty} from '../../shared-util/sugar.js'; + +import { + accumulateSum, + compareArrays, + empty, + unique, +} from '../../shared-util/sugar.js'; import { cssProp, + decodeEntities, openAlbum, openArtist, openArtTag, @@ -41,6 +46,17 @@ export const info = { failedRule: null, failedContainer: null, + contextContainer: null, + contextBackLink: null, + + filterContainer: null, + albumFilterLink: null, + artistFilterLink: null, + flashFilterLink: null, + groupFilterLink: null, + tagFilterLink: null, + trackFilterLink: null, + resultsRule: null, resultsContainer: null, results: null, @@ -49,6 +65,8 @@ export const info = { endSearchLine: null, endSearchLink: null, + standbyInputPlaceholder: null, + preparingString: null, loadingDataString: null, searchingString: null, @@ -58,17 +76,36 @@ export const info = { currentResultString: null, endSearchString: null, + backString: null, + albumResultKindString: null, artistResultKindString: null, + flashResultKindString: null, groupResultKindString: null, + singleResultKindString: null, tagResultKindString: null, + groupResultDisambiguatorString: null, + flashResultDisambiguatorString: null, + trackResultDisambiguatorString1: null, + trackResultDisambiguatorString2: null, + + albumResultFilterString: null, + artistResultFilterString: null, + flashResultFilterString: null, + groupResultFilterString: null, + tagResultFilterString: null, + trackResultFilterString: null, + state: { sidebarColumnShownForSearch: null, tidiedSidebar: null, collapsedDetailsForTidiness: null, + recallingRecentSearch: null, + recallingRecentSearchFromMouse: null, + currentValue: null, workerStatus: null, @@ -87,11 +124,19 @@ export const info = { type: 'string', }, + activeQueryContextPageName: {type: 'string'}, + activeQueryContextPagePathname: {type: 'string'}, + activeQueryContextPageColor: {type: 'string'}, + activeQueryResults: { type: 'json', maxLength: settings => settings.maxActiveResultsStorage, }, + activeFilterType: { + type: 'string', + }, + repeatQueryOnReload: { type: 'boolean', default: false, @@ -113,6 +158,17 @@ export const info = { }, }; +export function* bindSessionStorage() { + if (yield 'activeQuery') { + yield 'activeQueryContextPageName'; + yield 'activeQueryContextPagePathname'; + yield 'activeQueryContextPageColor'; + yield 'activeQueryResults'; + yield 'activeFilterType'; + yield 'resultsScrollOffset'; + } +} + export function getPageReferences() { info.pageContainer = document.getElementById('page-container'); @@ -133,6 +189,9 @@ export function getPageReferences() { info.searchSidebarColumn = info.searchBox.closest('.sidebar-column'); + info.standbyInputPlaceholder = + info.searchInput.placeholder; + const findString = classPart => info.searchBox.querySelector(`.wiki-search-${classPart}-string`); @@ -151,6 +210,9 @@ export function getPageReferences() { info.noResultsString = findString('no-results'); + info.backString = + findString('back'); + info.currentResultString = findString('current-result'); @@ -163,11 +225,47 @@ export function getPageReferences() { info.artistResultKindString = findString('artist-result-kind'); + info.flashResultKindString = + findString('flash-result-kind'); + info.groupResultKindString = findString('group-result-kind'); + info.singleResultKindString = + findString('single-result-kind'); + info.tagResultKindString = findString('tag-result-kind'); + + info.groupResultDisambiguatorString = + findString('group-result-disambiguator'); + + info.flashResultDisambiguatorString = + findString('flash-result-disambiguator'); + + info.trackResultDisambiguatorString1 = + findString('track-result-album-disambiguator'); + + info.trackResultDisambiguatorString2 = + findString('track-result-artist-disambiguator'); + + info.albumResultFilterString = + findString('album-result-filter'); + + info.artistResultFilterString = + findString('artist-result-filter'); + + info.flashResultFilterString = + findString('flash-result-filter'); + + info.groupResultFilterString = + findString('group-result-filter'); + + info.tagResultFilterString = + findString('tag-result-filter'); + + info.trackResultFilterString = + findString('track-result-filter'); } export function addInternalListeners() { @@ -257,6 +355,57 @@ export function mutatePageContent() { info.searchBox.appendChild(info.failedRule); info.searchBox.appendChild(info.failedContainer); + // Context section + + info.contextContainer = + document.createElement('div'); + + info.contextContainer.classList.add('wiki-search-context-container'); + + info.contextBackLink = + document.createElement('a'); + + info.contextContainer.appendChild( + templateContent(info.backString, { + page: info.contextBackLink, + })); + + cssProp(info.contextContainer, 'display', 'none'); + + info.searchBox.appendChild(info.contextContainer); + + // Filter section + + info.filterContainer = + document.createElement('div'); + + info.filterContainer.classList.add('wiki-search-filter-container'); + + cssProp(info.filterContainer, 'display', 'none'); + + forEachFilter((type, _filterLink) => { + // TODO: It's probably a sin to access `session` during this step LOL + const {session} = info; + + const filterLink = document.createElement('a'); + + filterLink.href = '#'; + filterLink.classList.add('wiki-search-filter-link'); + + if (session.activeFilterType === type) { + filterLink.classList.add('active'); + } + + const string = info[type + 'ResultFilterString']; + filterLink.appendChild(templateContent(string)); + + info[type + 'FilterLink'] = filterLink; + + info.filterContainer.appendChild(filterLink); + }); + + info.searchBox.appendChild(info.filterContainer); + // Results section info.resultsRule = @@ -305,11 +454,54 @@ export function mutatePageContent() { info.searchBox.appendChild(info.endSearchRule); info.searchBox.appendChild(info.endSearchLine); + + // Accommodate the web browser reconstructing the search input with a value + // that was previously entered (or restored after recall), i.e. because + // the user is traversing very far back in history and yet the browser is + // trying to rebuild the page as-was anyway, by telling it "no don't". + info.searchInput.value = ''; } export function addPageListeners() { if (!info.searchInput) return; + info.searchInput.addEventListener('mousedown', _domEvent => { + const {state} = info; + + if (state.recallingRecentSearch) { + state.recallingRecentSearchFromMouse = true; + } + }); + + info.searchInput.addEventListener('focus', _domEvent => { + const {session, state} = info; + + if (state.recallingRecentSearch) { + info.searchInput.value = session.activeQuery; + info.searchInput.placeholder = info.standbyInputPlaceholder; + showSidebarSearchResults(session.activeQueryResults); + state.recallingRecentSearch = false; + } + }); + + info.searchLabel.addEventListener('click', domEvent => { + const {state} = info; + + if (state.recallingRecentSearchFromMouse) { + if (info.searchInput.selectionStart === info.searchInput.selectionEnd) { + info.searchInput.select(); + } + + state.recallingRecentSearchFromMouse = false; + return; + } + + const inputRect = info.searchInput.getBoundingClientRect(); + if (domEvent.clientX < inputRect.left - 3) { + info.searchInput.select(); + } + }); + info.searchInput.addEventListener('change', _domEvent => { const {state} = info; @@ -326,7 +518,7 @@ export function addPageListeners() { const {settings, state} = info; if (!info.searchInput.value) { - clearSidebarSearch(); + clearSidebarSearch(); // ...but don't clear filter return; } @@ -388,10 +580,18 @@ export function addPageListeners() { info.endSearchLink.addEventListener('click', domEvent => { domEvent.preventDefault(); clearSidebarSearch(); + clearSidebarFilter(); possiblyHideSearchSidebarColumn(); restoreSidebarSearchColumn(); }); + forEachFilter((type, filterLink) => { + filterLink.addEventListener('click', domEvent => { + domEvent.preventDefault(); + toggleSidebarSearchFilter(type); + }); + }); + info.resultsContainer.addEventListener('scroll', () => { const {settings, state} = info; @@ -404,6 +604,18 @@ export function addPageListeners() { saveSidebarSearchResultsScrollOffset(); }, settings.stoppedScrollingDelay); }); + + document.addEventListener('keypress', domEvent => { + const {tagName} = document.activeElement ?? {}; + if (tagName === 'INPUT' || tagName === 'TEXTAREA') { + return; + } + + if (event.shiftKey && event.code === 'Slash') { + domEvent.preventDefault(); + info.searchLabel.click(); + } + }); } export function initializeState() { @@ -412,11 +624,11 @@ export function initializeState() { if (!info.searchInput) return; if (session.activeQuery) { - info.searchInput.value = session.activeQuery; if (session.repeatQueryOnReload) { + info.searchInput.value = session.activeQuery; activateSidebarSearch(session.activeQuery); } else if (session.activeQueryResults) { - showSidebarSearchResults(session.activeQueryResults); + considerRecallingRecentSidebarSearch(); } } } @@ -473,9 +685,28 @@ function trackSidebarSearchDownloadEnds(event) { } } +function forEachFilter(callback) { + const filterOrder = [ + 'track', + 'album', + 'artist', + 'group', + 'flash', + 'tag', + ]; + + for (const type of filterOrder) { + callback(type, info[type + 'FilterLink']); + } +} + async function activateSidebarSearch(query) { const {session, state} = info; + if (!query) { + return; + } + if (state.stoppedTypingTimeout) { clearTimeout(state.stoppedTypingTimeout); state.stoppedTypingTimeout = null; @@ -500,6 +731,8 @@ async function activateSidebarSearch(query) { state.searchStage = 'complete'; updateSidebarSearchStatus(); + recordActiveQueryContext(); + session.activeQuery = query; session.activeQueryResults = results; session.resultsScrollOffset = 0; @@ -513,6 +746,25 @@ async function activateSidebarSearch(query) { } } +function recordActiveQueryContext() { + const {session} = info; + + if (document.documentElement.dataset.urlKey === 'localized.home') { + return; + } + + session.activeQueryContextPageName = + decodeEntities(document.querySelector('title').dataset.withoutWikiName) || + document.title; + + session.activeQueryContextPagePathname = + location.pathname; + + session.activeQueryContextPageColor = + document.querySelector('.color-style')?.dataset.color ?? + null; +} + function clearSidebarSearch() { const {session, state} = info; @@ -535,6 +787,16 @@ function clearSidebarSearch() { hideSidebarSearchResults(); } +function clearSidebarFilter() { + const {session} = info; + + toggleSidebarSearchFilter(session.activeFilterType); + + forEachFilter((_type, filterLink) => { + filterLink.classList.remove('shown', 'hidden'); + }); +} + function updateSidebarSearchStatus() { const {state} = info; @@ -621,63 +883,185 @@ function showSidebarSearchFailed() { } function showSidebarSearchResults(results) { - console.debug(`Showing search results:`, results); + const {session} = info; - showSearchSidebarColumn(); + console.debug(`Showing search results:`, tidyResults(results)); - const flatResults = - Object.entries(results) - .filter(([index]) => index === 'generic') - .flatMap(([index, results]) => results - .flatMap(({doc, id}) => ({ - index, - reference: id ?? null, - referenceType: (id ? id.split(':')[0] : null), - directory: (id ? id.split(':')[1] : null), - data: doc, - }))); + showSearchSidebarColumn(); info.searchBox.classList.add('showing-results'); info.searchSidebarColumn.classList.add('search-showing-results'); - while (info.results.firstChild) { - info.results.firstChild.remove(); + let filterType = session.activeFilterType; + let shownAnyResults = + fillResultElements(results, {filterType: session.activeFilterType}); + + showFilterElements(results); + + if (!shownAnyResults) { + shownAnyResults = toggleSidebarSearchFilter(filterType); + filterType = null; } - cssProp(info.resultsRule, 'display', 'block'); - cssProp(info.resultsContainer, 'display', 'block'); + if (shownAnyResults) { + showContextControls(); - if (empty(flatResults)) { + cssProp(info.endSearchRule, 'display', 'block'); + cssProp(info.endSearchLine, 'display', 'block'); + + tidySidebarSearchColumn(); + } else { const p = document.createElement('p'); p.classList.add('wiki-search-no-results'); p.appendChild(templateContent(info.noResultsString)); info.results.appendChild(p); } - for (const result of flatResults) { - const el = generateSidebarSearchResult(result); + restoreSidebarSearchResultsScrollOffset(); +} + +function tidyResults(results) { + const tidiedResults = + results.results.map(({doc, id}) => ({ + reference: id ?? null, + referenceType: (id ? id.split(':')[0] : null), + directory: (id ? id.split(':')[1] : null), + data: doc, + })); + + return tidiedResults; +} + +function fillResultElements(results, { + filterType = null, +} = {}) { + const tidiedResults = tidyResults(results); + + let filteredResults = tidiedResults; + + if (filterType) { + filteredResults = filteredResults + .filter(result => result.referenceType === filterType); + } + + if (!filterType) { + filteredResults = filteredResults + .filter(result => { + if (result.referenceType !== 'track') return true; + if (result.data.classification !== 'single') return true; + return !filteredResults.find(otherResult => { + if (otherResult.referenceType !== 'album') return false; + return otherResult.name === result.parentName; + }); + }); + } + + filteredResults = filteredResults + + while (info.results.firstChild) { + info.results.firstChild.remove(); + } + + cssProp(info.resultsRule, 'display', 'block'); + cssProp(info.resultsContainer, 'display', 'block'); + + if (empty(filteredResults)) { + return false; + } + + for (const result of filteredResults) { + let el; + try { + el = generateSidebarSearchResult(result, filteredResults); + } catch (error) { + console.error(`Error showing result:`, result); + console.error(error); + } + if (!el) continue; info.results.appendChild(el); } - if (!empty(flatResults)) { - cssProp(info.endSearchRule, 'display', 'block'); - cssProp(info.endSearchLine, 'display', 'block'); + return true; +} - tidySidebarSearchColumn(); +function showFilterElements(results) { + const {queriedKind} = results; + + const tidiedResults = tidyResults(results); + + const allReferenceTypes = + unique(tidiedResults.map(result => result.referenceType)); + + let shownAny = false; + + forEachFilter((type, filterLink) => { + filterLink.classList.remove('shown', 'hidden'); + + if (allReferenceTypes.includes(type)) { + shownAny = true; + cssProp(filterLink, 'display', null); + + if (queriedKind) { + filterLink.setAttribute('inert', 'inert'); + } else { + filterLink.removeAttribute('inert'); + } + + if (type === queriedKind) { + filterLink.classList.add('active-from-query'); + } else { + filterLink.classList.remove('active-from-query'); + } + } else { + cssProp(filterLink, 'display', 'none'); + } + }); + + if (shownAny) { + cssProp(info.filterContainer, 'display', null); + } else { + cssProp(info.filterContainer, 'display', 'none'); } +} - restoreSidebarSearchResultsScrollOffset(); +function showContextControls() { + const {session} = info; + + const shouldShow = + session.activeQueryContextPagePathname && + location.pathname !== session.activeQueryContextPagePathname; + + if (shouldShow) { + info.contextBackLink.href = + session.activeQueryContextPagePathname; + + cssProp(info.contextBackLink, + '--primary-color', + session.activeQueryContextPageColor); + + while (info.contextBackLink.firstChild) { + info.contextBackLink.firstChild.remove(); + } + + info.contextBackLink.appendChild( + document.createTextNode( + session.activeQueryContextPageName)); + + cssProp(info.contextContainer, 'display', 'block'); + } else { + cssProp(info.contextContainer, 'display', 'none'); + } } -function generateSidebarSearchResult(result) { +function generateSidebarSearchResult(result, results) { const preparedSlots = { color: result.data.color ?? null, name: - result.data.name ?? result.data.primaryName ?? null, + getSearchResultName(result), imageSource: getSearchResultImageSource(result), @@ -689,7 +1073,9 @@ function generateSidebarSearchResult(result) { openAlbum(result.directory); preparedSlots.kindString = - info.albumResultKindString; + (result.data.classification === 'single' + ? info.singleResultKindString + : info.albumResultKindString); break; } @@ -718,6 +1104,9 @@ function generateSidebarSearchResult(result) { preparedSlots.href = openFlash(result.directory); + preparedSlots.kindString = + info.flashResultKindString; + break; } @@ -742,9 +1131,95 @@ function generateSidebarSearchResult(result) { return null; } + const compareReferenceType = otherResult => + otherResult.referenceType === result.referenceType; + + const compareName = otherResult => + getSearchResultName(otherResult) === getSearchResultName(result); + + const ambiguousWith = + results.filter(otherResult => + otherResult !== result && + compareReferenceType(otherResult) && + compareName(otherResult)); + + if (!empty(ambiguousWith)) disambiguate: { + const allAmbiguous = [result, ...ambiguousWith]; + + // First search for an ideal disambiguation, which disambiguates + // all ambiguous results in the same way. + let disambiguation = null, i; + for (i = 0; i < result.data.disambiguators.length; i++) { + const disambiguations = + allAmbiguous.map(r => r.data.disambiguators[i]); + + if (unique(disambiguations).length === allAmbiguous.length) { + disambiguation = result.data.disambiguators[i]; + break; + } + } + + // Otherwise, search for a disambiguation which disambiguates + // *this result* with at least one other result which it is + // *otherwise* ambiguous with. + if (!disambiguation) { + for (i = 1; i < result.data.disambiguators.length; i++) { + const otherwiseAmbiguousWith = + ambiguousWith.filter(otherResult => + compareArrays( + otherResult.data.disambiguators.slice(0, i), + result.data.disambiguators.slice(0, i))); + + if ( + otherwiseAmbiguousWith.find(otherResult => + otherResult.data.disambiguators[i] !== + result.data.disambiguators[i]) + ) { + disambiguation = result.data.disambiguators[i]; + break; + } + } + } + + // Otherwise, search for a disambiguation which disambiguates + // this result at all. + if (!disambiguation) { + for (i = 0; i < result.data.disambiguators.length; i++) { + if ( + ambiguousWith.find(otherResult => + otherResult.data.disambiguators[i] !== + result.data.disambiguators[i]) + ) { + disambiguation = result.data.disambiguators[i]; + break; + } + } + } + + if (!disambiguation) { + break disambiguate; + } + + const string = + info[result.referenceType + 'ResultDisambiguatorString' + (i + 1)]; + + if (!string) break disambiguate; + + preparedSlots.disambiguate = disambiguation; + preparedSlots.disambiguatorString = string; + } + return generateSidebarSearchResultTemplate(preparedSlots); } +function getSearchResultName(result) { + return ( + result.data.name ?? + result.data.primaryName ?? + null + ); +} + function getSearchResultImageSource(result) { const {artwork} = result.data; if (!artwork) return null; @@ -820,6 +1295,15 @@ function generateSidebarSearchResultTemplate(slots) { } } + if (!accentSpan && slots.disambiguate) { + accentSpan = document.createElement('span'); + accentSpan.classList.add('wiki-search-result-disambiguator'); + accentSpan.appendChild( + templateContent(slots.disambiguatorString, { + disambiguator: slots.disambiguate, + })); + } + if (!accentSpan && slots.kindString) { accentSpan = document.createElement('span'); accentSpan.classList.add('wiki-search-result-kind'); @@ -859,6 +1343,9 @@ function generateSidebarSearchResultTemplate(slots) { } function hideSidebarSearchResults() { + cssProp(info.contextContainer, 'display', 'none'); + cssProp(info.filterContainer, 'display', 'none'); + cssProp(info.resultsRule, 'display', 'none'); cssProp(info.resultsContainer, 'display', 'none'); @@ -991,6 +1478,36 @@ function tidySidebarSearchColumn() { } } +function toggleSidebarSearchFilter(toggleType) { + const {session} = info; + + if (!toggleType) return null; + + let shownAnyResults = null; + + forEachFilter((type, filterLink) => { + if (type === toggleType) { + const filterActive = filterLink.classList.toggle('active'); + const filterType = (filterActive ? type : null); + + if (cssProp(filterLink, 'display') !== 'none') { + filterLink.classList.add(filterActive ? 'shown' : 'hidden'); + } + + if (session.activeQueryResults) { + shownAnyResults = + fillResultElements(session.activeQueryResults, {filterType}); + } + + session.activeFilterType = filterType; + } else { + filterLink.classList.remove('active'); + } + }); + + return shownAnyResults; +} + function restoreSidebarSearchColumn() { const {state} = info; @@ -1004,6 +1521,33 @@ function restoreSidebarSearchColumn() { state.collapsedDetailsForTidiness = []; state.tidiedSidebar = null; + + info.searchInput.placeholder = info.standbyInputPlaceholder; +} + +function considerRecallingRecentSidebarSearch() { + const {session, state} = info; + + if (document.documentElement.dataset.urlKey === 'localized.home') { + return forgetRecentSidebarSearch(); + } + + info.searchInput.placeholder = session.activeQuery; + state.recallingRecentSearch = true; +} + +function forgetRecentSidebarSearch() { + const {session} = info; + + session.activeQuery = null; + + session.activeQueryContextPageName = null; + session.activeQueryContextPagePathname = null; + session.activeQueryContextPageColor = null; + + session.activeQueryResults = null; + + clearSidebarFilter(); } async function handleDroppedIntoSearchInput(domEvent) { @@ -1032,7 +1576,7 @@ async function handleDroppedIntoSearchInput(domEvent) { let droppedURL; try { droppedURL = new URL(droppedText); - } catch (error) { + } catch { droppedURL = null; } diff --git a/src/static/js/client/sticky-heading.js b/src/static/js/client/sticky-heading.js index ae63eab5..c69e137f 100644 --- a/src/static/js/client/sticky-heading.js +++ b/src/static/js/client/sticky-heading.js @@ -1,13 +1,17 @@ -/* eslint-env browser */ - import {filterMultipleArrays, stitchArrays} from '../../shared-util/sugar.js'; -import {dispatchInternalEvent, templateContent} from '../client-util.js'; +import {cssProp, dispatchInternalEvent, templateContent} + from '../client-util.js'; export const info = { id: 'stickyHeadingInfo', + stickyRoots: null, + stickyContainers: null, + staticContainers: null, + stickyHeadingRows: null, + stickyHeadings: null, stickySubheadingRows: null, stickySubheadings: null, @@ -17,21 +21,33 @@ export const info = { contentContainers: null, contentHeadings: null, + contentCoverColumns: null, contentCovers: null, contentCoversReveal: null, + referenceCollapsedHeading: null, + state: { displayedHeading: null, }, event: { whenDisplayedHeadingChanges: [], + whenStuckStatusChanges: [], }, }; export function getPageReferences() { + info.stickyRoots = + Array.from(document.querySelectorAll('.content-sticky-heading-root:not([inert])')); + info.stickyContainers = - Array.from(document.getElementsByClassName('content-sticky-heading-container')); + info.stickyRoots + .map(el => el.querySelector('.content-sticky-heading-container')); + + info.staticContainers = + info.stickyRoots + .map(el => el.nextElementSibling); info.stickyCoverContainers = info.stickyContainers @@ -45,6 +61,14 @@ export function getPageReferences() { info.stickyCovers .map(el => el?.querySelector('.image-text-area')); + info.stickyHeadingRows = + info.stickyContainers + .map(el => el.querySelector('.content-sticky-heading-row')); + + info.stickyHeadings = + info.stickyHeadingRows + .map(el => el.querySelector('h1')); + info.stickySubheadingRows = info.stickyContainers .map(el => el.querySelector('.content-sticky-subheading-row')); @@ -55,11 +79,15 @@ export function getPageReferences() { info.contentContainers = info.stickyContainers - .map(el => el.parentElement); + .map(el => el.closest('.content-sticky-heading-root').parentElement); - info.contentCovers = + info.contentCoverColumns = info.contentContainers - .map(el => el.querySelector('#cover-art-container')); + .map(el => el.querySelector('#artwork-column')); + + info.contentCovers = + info.contentCoverColumns + .map(el => el ? el.querySelector('.cover-artwork') : null); info.contentCoversReveal = info.contentCovers @@ -68,6 +96,10 @@ export function getPageReferences() { info.contentHeadings = info.contentContainers .map(el => Array.from(el.querySelectorAll('.content-heading'))); + + info.referenceCollapsedHeading = + info.stickyHeadings + .map(el => el.querySelector('.reference-collapsed-heading')); } export function mutatePageContent() { @@ -137,15 +169,61 @@ function topOfViewInside(el, scroll = window.scrollY) { scroll < el.offsetTop + el.offsetHeight); } +function updateStuckStatus(index) { + const {event} = info; + + const contentContainer = info.contentContainers[index]; + const stickyContainer = info.stickyContainers[index]; + + const wasStuck = stickyContainer.classList.contains('stuck'); + const stuck = topOfViewInside(contentContainer); + + if (stuck === wasStuck) return; + + if (stuck) { + stickyContainer.classList.add('stuck'); + } else { + stickyContainer.classList.remove('stuck'); + } + + dispatchInternalEvent(event, 'whenStuckStatusChanges', index, stuck); +} + +function updateCollapseStatus(index) { + const stickyContainer = info.stickyContainers[index]; + const staticContainer = info.staticContainers[index]; + const stickyHeading = info.stickyHeadings[index]; + const referenceCollapsedHeading = info.referenceCollapsedHeading[index]; + + const {height: uncollapsedHeight} = stickyHeading.getBoundingClientRect(); + const {height: collapsedHeight} = referenceCollapsedHeading.getBoundingClientRect(); + + if ( + staticContainer.getBoundingClientRect().bottom < 4 || + staticContainer.getBoundingClientRect().top < -80 + ) { + if (!stickyContainer.classList.contains('collapse')) { + stickyContainer.classList.add('collapse'); + cssProp(stickyContainer, '--uncollapsed-heading-height', uncollapsedHeight + 'px'); + cssProp(stickyContainer, '--collapsed-heading-height', collapsedHeight + 'px'); + } + } else { + stickyContainer.classList.remove('collapse'); + } +} + function updateStickyCoverVisibility(index) { const stickyCoverContainer = info.stickyCoverContainers[index]; - const contentCover = info.contentCovers[index]; + const stickyContainer = info.stickyContainers[index]; + const contentCoverColumn = info.contentCoverColumns[index]; - if (contentCover && stickyCoverContainer) { - if (contentCover.getBoundingClientRect().bottom < 4) { + if (contentCoverColumn && stickyCoverContainer) { + if (contentCoverColumn.getBoundingClientRect().bottom < 4) { stickyCoverContainer.classList.add('visible'); + stickyContainer.classList.add('cover-visible'); } else { stickyCoverContainer.classList.remove('visible'); + stickyContainer.classList.remove('cover-visible'); } } } @@ -157,26 +235,31 @@ function getContentHeadingClosestToStickySubheading(index) { return null; } - const stickySubheading = info.stickySubheadings[index]; - - if (stickySubheading.childNodes.length === 0) { - // Supply a non-breaking space to ensure correct basic line height. - stickySubheading.appendChild(document.createTextNode('\xA0')); - } - - const stickyContainer = info.stickyContainers[index]; - const stickyRect = stickyContainer.getBoundingClientRect(); + const stickyHeadingRow = info.stickyHeadingRows[index]; + const stickyRect = stickyHeadingRow.getBoundingClientRect(); - // TODO: Should this compute with the subheading row instead of h2? - const subheadingRect = stickySubheading.getBoundingClientRect(); + // Subheadings only appear when the sticky heading is collapsed, + // so the used bottom edge should always be *as though* it's only + // displaying one line of text. Subtract the current discrepancy. + const stickyHeading = info.stickyHeadings[index]; + const referenceCollapsedHeading = info.referenceCollapsedHeading[index]; + const correctBottomEdge = + stickyHeading.getBoundingClientRect().height - + referenceCollapsedHeading.getBoundingClientRect().height; - const stickyBottom = stickyRect.bottom + subheadingRect.height; + const stickyBottom = + (stickyRect.bottom + - correctBottomEdge); // Iterate from bottom to top of the content area. const contentHeadings = info.contentHeadings[index]; - for (const heading of contentHeadings.slice().reverse()) { + for (const heading of contentHeadings.toReversed()) { + if (heading.nodeName === 'SUMMARY' && !heading.closest('details').open) { + continue; + } + const headingRect = heading.getBoundingClientRect(); - if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 20) { + if (headingRect.y + headingRect.height / 1.5 < stickyBottom + 40) { return heading; } } @@ -187,7 +270,12 @@ function getContentHeadingClosestToStickySubheading(index) { function updateStickySubheadingContent(index) { const {event, state} = info; - const closestHeading = getContentHeadingClosestToStickySubheading(index); + const stickyContainer = info.stickyContainers[index]; + + const closestHeading = + (stickyContainer.classList.contains('collapse') + ? getContentHeadingClosestToStickySubheading(index) + : null); if (state.displayedHeading === closestHeading) return; @@ -233,6 +321,8 @@ function updateStickySubheadingContent(index) { } export function updateStickyHeadings(index) { + updateStuckStatus(index); + updateCollapseStatus(index); updateStickyCoverVisibility(index); updateStickySubheadingContent(index); } diff --git a/src/static/js/client/summary-nested-link.js b/src/static/js/client/summary-nested-link.js index 23857fa5..1c4e7e4b 100644 --- a/src/static/js/client/summary-nested-link.js +++ b/src/static/js/client/summary-nested-link.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import { empty, filterMultipleArrays, diff --git a/src/static/js/client/text-with-tooltip.js b/src/static/js/client/text-with-tooltip.js index dd207e04..2b855756 100644 --- a/src/static/js/client/text-with-tooltip.js +++ b/src/static/js/client/text-with-tooltip.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {stitchArrays} from '../../shared-util/sugar.js'; import {registerTooltipElement, registerTooltipHoverableElement} diff --git a/src/static/js/client/wiki-search.js b/src/static/js/client/wiki-search.js index 2446c172..9a6e29c1 100644 --- a/src/static/js/client/wiki-search.js +++ b/src/static/js/client/wiki-search.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {promiseWithResolvers} from '../../shared-util/sugar.js'; import {dispatchInternalEvent} from '../client-util.js'; diff --git a/src/static/js/group-contributions-table.js b/src/static/js/group-contributions-table.js index 72ad2327..bef85fad 100644 --- a/src/static/js/group-contributions-table.js +++ b/src/static/js/group-contributions-table.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // TODO: Update to clientSteps style. const groupContributionsTableInfo = diff --git a/src/static/js/info-card.js b/src/static/js/info-card.js index 1d9f7c86..05d5d801 100644 --- a/src/static/js/info-card.js +++ b/src/static/js/info-card.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // Note: This is a super ancient chunk of code which isn't actually in use, // so it's just commented out here. diff --git a/src/static/js/lazy-loading.js b/src/static/js/lazy-loading.js index 1df56f08..0c8aef31 100644 --- a/src/static/js/lazy-loading.js +++ b/src/static/js/lazy-loading.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - // Lazy loading! Roll your own. Woot. // This file includes a 8unch of fall8acks and stuff like that, and is written // with fairly Olden JavaScript(TM), so as to work on pretty much any 8rowser diff --git a/src/static/js/rectangles.js b/src/static/js/rectangles.js index cdab2cb8..24382ef8 100644 --- a/src/static/js/rectangles.js +++ b/src/static/js/rectangles.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - import {info as liveMousePositionInfo} from './client/live-mouse-position.js'; export class WikiRect extends DOMRect { @@ -510,4 +508,46 @@ export class WikiRect extends DOMRect { height: this.height, }); } + + // Other utilities + + #display = null; + + display() { + if (!this.#display) { + this.#display = document.createElement('div'); + document.body.appendChild(this.#display); + } + + Object.assign(this.#display.style, { + position: 'fixed', + background: '#000c', + border: '3px solid var(--primary-color)', + borderRadius: '4px', + top: this.top + 'px', + left: this.left + 'px', + width: this.width + 'px', + height: this.height + 'px', + pointerEvents: 'none', + }); + + let i = 0; + const int = setInterval(() => { + i++; + if (i >= 3) clearInterval(int); + if (!this.#display) return; + + this.#display.style.display = 'none'; + setTimeout(() => { + this.#display.style.display = ''; + }, 200); + }, 600); + } + + hide() { + if (this.#display) { + this.#display.remove(); + this.#display = null; + } + } } diff --git a/src/static/js/search-worker.js b/src/static/js/search-worker.js index 1b4684ad..b79df3d4 100644 --- a/src/static/js/search-worker.js +++ b/src/static/js/search-worker.js @@ -1,8 +1,7 @@ -/* eslint-env worker */ - import FlexSearch from '../lib/flexsearch/flexsearch.bundle.module.min.js'; -import {makeSearchIndex, searchSpec} from '../shared-util/search-spec.js'; +import {default as searchSpec, makeSearchIndex} + from '../shared-util/search-shape.js'; import { empty, @@ -33,7 +32,7 @@ postStatus('alive'); Promise.all([ loadDependencies(), loadDatabase(), -]).then(main) +]).then(() => main()) .then( () => { postStatus('ready'); @@ -130,7 +129,7 @@ async function loadDatabase() { try { idb = await promisifyIDBRequest(request); - } catch (error) { + } catch { console.warn(`Couldn't load search IndexedDB - won't use an internal cache.`); console.warn(request.error); idb = null; @@ -183,7 +182,7 @@ function fetchIndexes(keysNeedingFetch) { }))); } -async function main() { +async function main(fromRetry = false) { const prepareIndexDataPromise = prepareIndexData(); indexes = @@ -195,17 +194,50 @@ async function main() { const {indexData, idbIndexData} = await prepareIndexDataPromise; + const understoodKeys = Object.keys(searchSpec); + const unexpectedKeysFromCache = + Object.keys(idbIndexData) + .filter(key => !understoodKeys.includes(key)); + + // This step is largely "unnecessary" because the rest of the code pays + // attention to which keys are understood anyway, but we delete unexpected + // keys from the index anyway, to trim stored data that isn't being used. + if (idb && !empty(unexpectedKeysFromCache)) { + for (const key of unexpectedKeysFromCache) { + console.warn(`Unexpected search index in cache, deleting: ${key}`); + } + + const transaction = + idb.transaction(['indexes'], 'readwrite'); + + const store = + transaction.objectStore('indexes'); + + for (const [key] of unexpectedKeysFromCache) { + try { + await promisifyIDBRequest(store.delete(key)); + } catch (error) { + console.warn(`Error deleting ${key} from internal search cache`); + console.warn(error); + continue; + } + } + } + const keysNeedingFetch = (idbIndexData ? Object.keys(indexData) + .filter(key => understoodKeys.includes(key)) .filter(key => indexData[key].md5 !== idbIndexData[key]?.md5) - : Object.keys(indexData)); + : Object.keys(indexData) + .filter(key => understoodKeys.includes(key))); const keysFromCache = Object.keys(indexData) - .filter(key => !keysNeedingFetch.includes(key)) + .filter(key => understoodKeys.includes(key)) + .filter(key => !keysNeedingFetch.includes(key)); const cacheArrayBufferPromises = keysFromCache @@ -234,10 +266,20 @@ async function main() { } function importIndexes(keys, jsons) { + const succeeded = []; + const failed = []; + stitchArrays({key: keys, json: jsons}) .forEach(({key, json}) => { - importIndex(key, json); + try { + importIndex(key, json); + succeeded.push([key, null]); + } catch (caughtError) { + failed.push([key, caughtError]); + } }); + + return {succeeded, failed}; } if (idb) { @@ -245,6 +287,8 @@ async function main() { console.debug(`Fetching indexes anew:`, keysNeedingFetch); } + let signalRetryNeeded = false; + await Promise.all([ async () => { const cacheArrayBuffers = @@ -254,7 +298,34 @@ async function main() { cacheArrayBuffers .map(arrayBufferToJSON); - importIndexes(keysFromCache, cacheJSONs); + const importResults = + importIndexes(keysFromCache, cacheJSONs); + + if (empty(importResults.failed)) return; + if (!idb) return; + + const transaction = + idb.transaction(['indexes'], 'readwrite'); + + const store = + transaction.objectStore('indexes'); + + for (const [key, error] of importResults.failed) { + console.warn(`Failed to import search index from cache: ${key}`); + console.warn(error); + } + + for (const [key] of importResults.failed) { + try { + await promisifyIDBRequest(store.delete(key)); + } catch (error) { + console.warn(`Error deleting ${key} from internal search cache`); + console.warn(error); + continue; + } + } + + signalRetryNeeded = true; }, async () => { @@ -265,7 +336,21 @@ async function main() { fetchArrayBuffers .map(arrayBufferToJSON); - importIndexes(keysNeedingFetch, fetchJSONs); + const importResults = + importIndexes(keysNeedingFetch, fetchJSONs); + + if (empty(importResults.failed)) return; + + for (const [key, error] of importResults.failed) { + console.warn(`Failed to import search index from fetch: ${key}`); + console.warn(error); + } + + console.warn( + `Trying again would just mean fetching this same data, ` + + `so this is needs outside intervention.`); + + throw new Error(`Failed to load search data from fresh fetch`); }, async () => { @@ -300,11 +385,19 @@ async function main() { } }, ].map(fn => fn())); + + if (signalRetryNeeded) { + if (fromRetry) { + console.error(`Already retried, this is probably a logic / code flow error.`); + throw new Error(`Failed to load good search data even on a retry`); + } else { + console.warn(`Trying to load search data again, hopefully from fresh conditions`); + return main(true); + } + } } function importIndex(indexKey, indexData) { - // If this fails, it's because an outdated index was cached. - // TODO: If this fails, try again once with a cache busting url. for (const [key, value] of Object.entries(indexData)) { indexes[indexKey].import(key, JSON.stringify(value)); } @@ -371,58 +464,78 @@ function postActionResult(id, status, value) { } function performSearchAction({query, options}) { - const {generic, ...otherIndexes} = indexes; + const {queriedKind} = processTerms(query); + const genericResults = queryGenericIndex(query, options); + const verbatimResults = queryVerbatimIndex(query, options); - const genericResults = - queryGenericIndex(generic, query, options); + const verbatimIDs = + new Set(verbatimResults?.map(result => result.id)); - const otherResults = - withEntries(otherIndexes, entries => entries - .map(([indexName, index]) => [ - indexName, - index.search(query, options), - ])); + const commonResults = + (verbatimResults && genericResults + ? genericResults + .filter(({id}) => verbatimIDs.has(id)) + : verbatimResults ?? genericResults); return { - generic: genericResults, - ...otherResults, + results: commonResults, + queriedKind, }; } -function queryGenericIndex(index, query, options) { - const interestingFieldCombinations = [ - ['primaryName', 'parentName', 'groups'], - ['primaryName', 'parentName'], - ['primaryName', 'groups', 'contributors'], - ['primaryName', 'groups', 'artTags'], - ['primaryName', 'groups'], - ['primaryName', 'contributors'], - ['primaryName', 'artTags'], - ['parentName', 'groups', 'artTags'], - ['parentName', 'artTags'], - ['groups', 'contributors'], - ['groups', 'artTags'], - - // This prevents just matching *everything* tagged "john" if you - // only search "john", but it actually supports matching more than - // *two* tags at once: "john rose lowas" works! This is thanks to - // flexsearch matching multiple field values in a single query. - ['artTags', 'artTags'], - - ['contributors', 'parentName'], - ['contributors', 'groups'], - ['primaryName', 'contributors'], - ['primaryName'], - ]; +const interestingFieldCombinations = [ + ['primaryName'], + ['additionalNames'], + + ['primaryName', 'parentName', 'groups'], + ['primaryName', 'parentName'], + ['primaryName', 'groups', 'contributors'], + ['primaryName', 'groups', 'artTags'], + ['primaryName', 'groups'], + ['additionalNames', 'groups'], + ['primaryName', 'contributors'], + ['primaryName', 'artTags'], + ['parentName', 'groups', 'artTags'], + ['parentName', 'artTags'], + ['groups', 'contributors'], + ['groups', 'artTags'], + + // This prevents just matching *everything* tagged "john" if you + // only search "john", but it actually supports matching more than + // *two* tags at once: "john rose lowas" works! This is thanks to + // flexsearch matching multiple field values in a single query. + ['artTags', 'artTags'], + + ['contributors', 'parentName'], + ['contributors', 'groups'], + ['primaryName', 'contributors'], +]; + +function queryGenericIndex(query, options) { + return queryIndex({ + indexKey: 'generic', + termsKey: 'genericTerms', + }, query, options); +} + +function queryVerbatimIndex(query, options) { + return queryIndex({ + indexKey: 'verbatim', + termsKey: 'verbatimTerms', + }, query, options); +} +function queryIndex({termsKey, indexKey}, query, options) { const interestingFields = unique(interestingFieldCombinations.flat()); - const {genericTerms, queriedKind} = + const {[termsKey]: terms, queriedKind} = processTerms(query); + if (empty(terms)) return null; + const particles = - particulate(genericTerms); + particulate(terms); const groupedParticles = groupArray(particles, ({length}) => length); @@ -437,7 +550,7 @@ function queryGenericIndex(index, query, options) { query: values, })); - const boilerplate = queryBoilerplate(index); + const boilerplate = queryBoilerplate(indexes[indexKey]); const particleResults = Object.fromEntries( @@ -459,62 +572,73 @@ function queryGenericIndex(index, query, options) { ])), ])); - const results = new Set(); + let matchedResults = new Set(); for (const interestingFieldCombination of interestingFieldCombinations) { for (const query of queriesBy(interestingFieldCombination)) { - const idToMatchingFieldsMap = new Map(); - for (const {field, query: fieldQuery} of query) { - for (const id of particleResults[field][fieldQuery]) { - if (idToMatchingFieldsMap.has(id)) { - idToMatchingFieldsMap.get(id).push(field); - } else { - idToMatchingFieldsMap.set(id, [field]); - } - } - } + const [firstQueryFieldLine, ...restQueryFieldLines] = query; const commonAcrossFields = - Array.from(idToMatchingFieldsMap.entries()) - .filter(([id, matchingFields]) => - matchingFields.length === interestingFieldCombination.length) - .map(([id]) => id); + new Set( + particleResults + [firstQueryFieldLine.field] + [firstQueryFieldLine.query]); + + for (const currQueryFieldLine of restQueryFieldLines) { + const tossResults = new Set(commonAcrossFields); + + const keepResults = + particleResults + [currQueryFieldLine.field] + [currQueryFieldLine.query]; + + for (const result of keepResults) { + tossResults.delete(result); + } + + for (const result of tossResults) { + commonAcrossFields.delete(result); + } + } for (const result of commonAcrossFields) { - results.add(result); + matchedResults.add(result); } } } - const constituted = - boilerplate.constitute(results); + matchedResults = Array.from(matchedResults); + + const filteredResults = + (queriedKind + ? matchedResults.filter(id => id.split(':')[0] === queriedKind) + : matchedResults); - const constitutedAndFiltered = - constituted - .filter(({id}) => - (queriedKind - ? id.split(':')[0] === queriedKind - : true)); + const constitutedResults = + boilerplate.constitute(filteredResults); - return constitutedAndFiltered; + return constitutedResults; } function processTerms(query) { const kindTermSpec = [ - {kind: 'album', terms: ['album']}, - {kind: 'artist', terms: ['artist']}, - {kind: 'flash', terms: ['flash']}, - {kind: 'group', terms: ['group']}, - {kind: 'tag', terms: ['art tag', 'tag']}, - {kind: 'track', terms: ['track']}, + {kind: 'album', terms: ['album', 'albums', 'release', 'releases']}, + {kind: 'artist', terms: ['artist', 'artists']}, + {kind: 'flash', terms: ['flash', 'flashes']}, + {kind: 'group', terms: ['group', 'groups']}, + {kind: 'tag', terms: ['art tag', 'art tags', 'tag', 'tags']}, + {kind: 'track', terms: ['track', 'tracks']}, ]; const genericTerms = []; + const verbatimTerms = []; let queriedKind = null; const termRegexp = new RegExp( - String.raw`(?<kind>${kindTermSpec.flatMap(spec => spec.terms).join('|')})` + + String.raw`(?<kind>(?<=^|\s)(?:${kindTermSpec.flatMap(spec => spec.terms).join('|')})(?=$|\s))` + + String.raw`|(?<=^|\s)(?<quote>["'])(?<regularVerbatim>.+?)\k<quote>(?=$|\s)` + + String.raw`|(?<=^|\s)[“”‘’](?<curlyVerbatim>.+?)[“”‘’](?=$|\s)` + String.raw`|[^\s\-]+`, 'gi'); @@ -530,10 +654,16 @@ function processTerms(query) { continue; } + const verbatim = groups.regularVerbatim || groups.curlyVerbatim; + if (verbatim) { + verbatimTerms.push(verbatim); + continue; + } + genericTerms.push(match[0]); } - return {genericTerms, queriedKind}; + return {genericTerms, verbatimTerms, queriedKind}; } function particulate(terms) { diff --git a/src/static/js/xhr-util.js b/src/static/js/xhr-util.js index 8a43072c..bc0698da 100644 --- a/src/static/js/xhr-util.js +++ b/src/static/js/xhr-util.js @@ -1,5 +1,3 @@ -/* eslint-env browser */ - /** * This fetch function is adapted from a `loadImage` function * credited to Parziphal, Feb 13, 2017. diff --git a/src/static/misc/image.svg b/src/static/misc/image.svg new file mode 100644 index 00000000..a251b373 --- /dev/null +++ b/src/static/misc/image.svg @@ -0,0 +1,11 @@ +<!-- Copyright © (c) 2019-2023 The Bootstrap authors + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +The Software is provided "as is", without warranty of any kind, express or implied, including but not limited to the warranties of merchantability, fitness for a particular purpose and noninfringement. In no event shall the authors or copyright holders be liable for any claim, damages or other liability, whether in an action of contract, tort or otherwise, arising from, out of or in connection with the Software or the use or other dealings in the Software. --> + +<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="bi bi-image-fill" viewBox="0 0 16 16"> + <path d="M.002 3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v10a2 2 0 0 1-2 2h-12a2 2 0 0 1-2-2V3zm1 9v1a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1V9.5l-3.777-1.947a.5.5 0 0 0-.577.093l-3.71 3.71-2.66-1.772a.5.5 0 0 0-.63.062L1.002 12zm5-6.5a1.5 1.5 0 1 0-3 0 1.5 1.5 0 0 0 3 0z"/> +</svg> |