From 5206ac7188c9eefd6f1d18050e2831b0f10be8ef Mon Sep 17 00:00:00 2001 From: "(quasar) nebula" Date: Sun, 27 Nov 2022 22:49:16 -0400 Subject: redesign & refinements for sticky layouting --- package-lock.json | 11 ++++++ package.json | 1 + src/misc-templates.js | 20 ++++++++-- src/page/album.js | 34 ++++++++++++----- src/page/flash.js | 21 +++++++---- src/page/group.js | 4 ++ src/page/homepage.js | 1 + src/page/track.js | 13 +++++-- src/static/client.js | 8 +++- src/static/site2.css | 102 ++++++++++++++++++++++++++++++++++++++++++++++---- src/upd8.js | 81 +++++++++++++++++++++++++++++++++------ src/util/colors.js | 62 +++++++++++++----------------- src/util/html.js | 4 ++ src/util/link.js | 11 +++++- 14 files changed, 291 insertions(+), 82 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4195eecf..e12b9c7e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.1.0", "license": "GPL-3.0", "dependencies": { + "chroma-js": "^2.4.2", "command-exists": "^1.2.9", "he": "^1.2.0", "js-yaml": "^4.1.0" @@ -211,6 +212,11 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -1875,6 +1881,11 @@ "supports-color": "^7.1.0" } }, + "chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, "color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", diff --git a/package.json b/package.json index c4f1ce1f..052aae22 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "dev": "eslint src && node src/upd8.js" }, "dependencies": { + "chroma-js": "^2.4.2", "command-exists": "^1.2.9", "he": "^1.2.0", "js-yaml": "^4.1.0" diff --git a/src/misc-templates.js b/src/misc-templates.js index 2614aac9..22752692 100644 --- a/src/misc-templates.js +++ b/src/misc-templates.js @@ -4,8 +4,6 @@ import {Track, Album} from './data/things.js'; -import {getColors} from './util/colors.js'; - import { empty, unique, @@ -297,15 +295,29 @@ function unbound_generateCoverLink({ // CSS & color shenanigans -function unbound_getThemeString(color, additionalVariables = []) { +function unbound_getThemeString(color, { + getColors, + + additionalVariables = [], +} = {}) { if (!color) return ''; - const {primary, dim, bg} = getColors(color); + const { + primary, + dark, + dim, + bg, + bgBlack, + shadow, + } = getColors(color); const variables = [ `--primary-color: ${primary}`, + `--dark-color: ${dark}`, `--dim-color: ${dim}`, `--bg-color: ${bg}`, + `--bg-black-color: ${bgBlack}`, + `--shadow-color: ${shadow}`, ...additionalVariables, ].filter(Boolean); diff --git a/src/page/album.js b/src/page/album.js index e7658cda..741fcaba 100644 --- a/src/page/album.js +++ b/src/page/album.js @@ -131,9 +131,14 @@ export function write(album, {wikiData}) { return { title: language.$('albumPage.title', {album: album.name}), stylesheet: getAlbumStylesheet(album), - theme: getThemeString(album.color, [ - `--album-directory: ${album.directory}`, - ]), + + themeColor: album.color, + theme: + getThemeString(album.color, { + additionalVariables: [ + `--album-directory: ${album.directory}`, + ], + }), banner: !empty(album.bannerArtistContribs) && { dimensions: album.bannerDimensions, @@ -463,15 +468,26 @@ export function generateAlbumSidebar(album, currentTrack, { ]); if (empty(groupParts)) { - return {content: trackListPart}; + return { + stickyMode: 'column', + content: trackListPart, + }; } else if (isTrackPage) { - const combinedGroupPart = - groupParts + const combinedGroupPart = { + classes: ['no-sticky-header'], + content: groupParts .map(groupPart => groupPart.filter(Boolean).join('\n')) - .join('\n
\n'); - return {multiple: [trackListPart, combinedGroupPart]}; + .join('\n
\n'), + }; + return { + stickyMode: 'column', + multiple: [trackListPart, combinedGroupPart], + }; } else { - return {multiple: [...groupParts, trackListPart]}; + return { + stickyMode: 'last', + multiple: [...groupParts, trackListPart], + }; } } diff --git a/src/page/flash.js b/src/page/flash.js index be07039b..1e818ae9 100644 --- a/src/page/flash.js +++ b/src/page/flash.js @@ -28,22 +28,27 @@ export function write(flash, {wikiData}) { language, }) => ({ title: language.$('flashPage.title', {flash: flash.name}), - theme: getThemeString(flash.color, [ - `--flash-directory: ${flash.directory}`, - ]), + + themeColor: flash.color, + theme: + getThemeString(flash.color, { + additionalVariables: [ + `--flash-directory: ${flash.directory}`, + ], + }), main: { content: [ - html.tag('h1', - language.$('flashPage.title', { - flash: flash.name, - })), - generateCoverLink({ src: getFlashCover(flash), alt: language.$('misc.alt.flashArt'), }), + html.tag('h1', + language.$('flashPage.title', { + flash: flash.name, + })), + html.tag('p', language.$('releaseInfo.released', { date: language.formatDate(flash.date), diff --git a/src/page/group.js b/src/page/group.js index b181bcb2..1d586cf5 100644 --- a/src/page/group.js +++ b/src/page/group.js @@ -40,6 +40,8 @@ export function write(group, {wikiData}) { transformMultiline, }) => ({ title: language.$('groupInfoPage.title', {group: group.name}), + + themeColor: group.color, theme: getThemeString(group.color), main: { @@ -132,6 +134,8 @@ export function write(group, {wikiData}) { link, }) => ({ title: language.$('groupGalleryPage.title', {group: group.name}), + + themeColor: group.color, theme: getThemeString(group.color), main: { diff --git a/src/page/homepage.js b/src/page/homepage.js index 7295ba09..105c402f 100644 --- a/src/page/homepage.js +++ b/src/page/homepage.js @@ -107,6 +107,7 @@ export function writeTargetless({wikiData}) { sidebarLeft: homepageLayout.sidebarContent && { wide: true, collapse: false, + stickyMode: 'none', // This is a pretty filthy hack! 8ut otherwise, the [[news]] part // gets treated like it's a reference to the track named "news", // which o8viously isn't what we're going for. Gotta catch that diff --git a/src/page/track.js b/src/page/track.js index 4095f75a..18fd7262 100644 --- a/src/page/track.js +++ b/src/page/track.js @@ -187,10 +187,15 @@ export function write(track, {wikiData}) { return { title: language.$('trackPage.title', {track: track.name}), stylesheet: getAlbumStylesheet(album, {to}), - theme: getThemeString(track.color, [ - `--album-directory: ${album.directory}`, - `--track-directory: ${track.directory}`, - ]), + + themeColor: track.color, + theme: + getThemeString(track.color, { + additionalVariables: [ + `--album-directory: ${album.directory}`, + `--track-directory: ${track.directory}`, + ] + }), socialEmbed: { heading: language.$('trackPage.socialEmbed.heading', { diff --git a/src/static/client.js b/src/static/client.js index 32fb2abe..e9286ab0 100644 --- a/src/static/client.js +++ b/src/static/client.js @@ -222,8 +222,14 @@ let fastHover = false; let endFastHoverTimeout = null; function colorLink(a, color) { + console.warn('Info card link colors temporarily disabled: chroma.js required, no dependency linking for client.js yet'); + return; + + // eslint-disable-next-line no-unreachable + const chroma = {}; + if (color) { - const {primary, dim} = getColors(color); + const {primary, dim} = getColors(color, {chroma}); a.style.setProperty('--primary-color', primary); a.style.setProperty('--dim-color', dim); } diff --git a/src/static/site2.css b/src/static/site2.css index 49c3ab83..33553e84 100644 --- a/src/static/site2.css +++ b/src/static/site2.css @@ -104,6 +104,7 @@ a:hover { .layout-columns { display: flex; + align-items: stretch; } #header, @@ -274,6 +275,17 @@ 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-column { @@ -281,7 +293,7 @@ footer { min-width: 150px; max-width: 250px; flex-basis: 250px; - height: 100%; + align-self: flex-start; } .sidebar-multiple { @@ -290,11 +302,12 @@ footer { } .sidebar-multiple .sidebar:not(:first-child) { - margin-top: 10px; + margin-top: 15px; } .sidebar { - padding: 5px; + --content-padding: 5px; + padding: var(--content-padding); font-size: 0.85em; } @@ -314,8 +327,9 @@ footer { } #content { + --content-padding: 20px; box-sizing: border-box; - padding: 20px; + padding: var(--content-padding); flex-grow: 1; flex-shrink: 3; overflow-wrap: anywhere; @@ -454,8 +468,9 @@ footer { float: right; width: 40%; max-width: 400px; - margin: 0 0 10px 10px; + margin: -5px 0 10px 10px; font-size: 0.8em; + box-shadow: 0 0 3px 3px rgba(0, 0, 0, 0.25); } #cover-art img { @@ -966,6 +981,76 @@ li > ul { margin-bottom: 0; } +/* sticky headers */ + +#content:not(.no-sticky-heading) h1:first-of-type, +.sidebar:not(.no-sticky-heading) h1:first-of-type { + position: sticky; + top: 0; + + margin: calc(-1 * var(--content-padding)); + margin-bottom: calc(0.5 * var(--content-padding)); + padding: + calc(1.25 * var(--content-padding)) + 20px + calc(0.75 * var(--content-padding)) + 20px; + + background: var(--bg-black-color); + border-bottom: 1px dotted rgba(180, 180, 180, 0.4); + + -webkit-backdrop-filter: blur(6px); + backdrop-filter: blur(6px); +} + +#content:not(.no-sticky-heading) h1:first-of-type { + z-index: 1; + box-shadow: + inset 0 10px 10px -5px var(--shadow-color), + 0 4px 4px rgba(0, 0, 0, 0.8); +} + +#content:not(.no-sticky-heading) .long-content h1:first-of-type { + margin-left: -40%; + margin-right: -40%; + padding-left: 40%; +} + +#cover-art-container { + z-index: 2; + position: relative; +} + +.sidebar:not(.no-sticky-heading) h1:first-of-type { + box-shadow: + inset 0 8px 8px -6px var(--shadow-color), + 0 4px 4px rgba(0, 0, 0, 0.8); +} + +#content, .sidebar { + contain: paint; +} + +/* sticky sidebar */ + +.sidebar-column.sidebar.sticky-column, +.sidebar-column.sidebar.sticky-last, +.sidebar-multiple.sticky-last > .sidebar:last-child, +.sidebar-multiple.sticky-column { + position: sticky; + top: 10px; +} + +.sidebar-multiple.sticky-last { + align-self: stretch; +} + +.sidebar-multiple.sticky-column { + align-self: flex-start; +} + +/* responsive layout */ + @media (max-width: 900px) { .sidebar-column:not(.no-hide) { display: none; @@ -1002,8 +1087,7 @@ li > ul { #cover-art-container { float: none; - margin: 0 10px 10px 10px; - margin: 0; + margin: 0 0 40px 0; width: 100%; max-width: unset; } @@ -1015,6 +1099,10 @@ li > ul { #header > div:not(:first-child) { margin-top: 0.5em; } + + #content { + border-top-style: solid; + } } /* important easter egg mode */ diff --git a/src/upd8.js b/src/upd8.js index 8e3e0920..6d63b1b1 100755 --- a/src/upd8.js +++ b/src/upd8.js @@ -37,6 +37,8 @@ import {fileURLToPath} from 'url'; // It stands for "HTML Entities", apparently. Cursed. import he from 'he'; +import chroma from 'chroma-js'; + import { copyFile, mkdir, @@ -56,7 +58,7 @@ import * as pageSpecs from './page/index.js'; import find, {bindFind} from './util/find.js'; import * as html from './util/html.js'; -import unbound_link, {getLinkThemeString} from './util/link.js'; +import {getColors} from './util/colors.js'; import {findFiles} from './util/io.js'; import CacheableObject from './data/cacheable-object.js'; @@ -92,10 +94,14 @@ import { getGridHTML, getRevealStringFromTags, getRevealStringFromWarnings, - getThemeString, + getThemeString as unbound_getThemeString, iconifyURL, } from './misc-templates.js'; +import unbound_link, { + getLinkThemeString as unbound_getLinkThemeString, +} from './util/link.js'; + import { color, decorateTime, @@ -866,6 +872,7 @@ writePage.to = writePage.html = (pageInfo, { defaultLanguage, + getThemeString, language, languages, localizedPaths, @@ -884,6 +891,7 @@ writePage.html = (pageInfo, { stylesheet = '', showWikiNameInTitle = true, + themeColor = '', // missing properties are auto-filled, see below! body = {}, @@ -934,6 +942,10 @@ writePage.html = (pageInfo, { ? transformMultiline(wikiInfo.footerContent) : ''; + const colors = themeColor + ? getColors(themeColor, {chroma}) + : null; + const canonical = wikiInfo.canonicalBase ? wikiInfo.canonicalBase + (paths.pathname === '/' ? '' : paths.pathname) : ''; @@ -988,6 +1000,11 @@ writePage.html = (pageInfo, { classes, collapse = true, wide = false, + + // 'last' - last or only sidebar box is sticky + // 'column' - entire column, incl. multiple boxes from top, is sticky + // 'none' - sidebar not sticky at all, stays at top of page + stickyMode = 'last', }) => content ? html.tag('div', @@ -998,6 +1015,7 @@ writePage.html = (pageInfo, { 'sidebar', wide && 'wide', !collapse && 'no-hide', + stickyMode !== 'none' && 'sticky-' + stickyMode, ...classes, ], }, @@ -1011,10 +1029,24 @@ writePage.html = (pageInfo, { 'sidebar-multiple', wide && 'wide', !collapse && 'no-hide', + stickyMode !== 'none' && 'sticky-' + stickyMode, ], }, - multiple.map((content) => - html.tag('div', {class: ['sidebar', ...classes]}, content))) + multiple + .map((infoOrContent) => + (typeof infoOrContent === 'object' && !Array.isArray(infoOrContent)) + ? infoOrContent + : {content: infoOrContent}) + .filter(({content}) => content) + .map(({ + content, + classes: classes2 = [], + }) => + html.tag('div', + { + class: ['sidebar', ...classes, ...classes2], + }, + html.fragment(content)))) : ''; const sidebarLeftHTML = generateSidebarHTML('sidebar-left', sidebarLeft); @@ -1201,8 +1233,21 @@ writePage.html = (pageInfo, { socialEmbed.image && html.tag('meta', {property: 'og:image', content: socialEmbed.image}), - socialEmbed.color && - html.tag('meta', {name: 'theme-color', content: socialEmbed.color}), + ...html.fragment( + colors && [ + // Safari only respects the first media-matching meta tag here, + // so position the dark-specific entry first + html.tag('meta', { + name: 'theme-color', + content: colors.dark, + media: '(prefers-color-scheme: dark)' + }), + + html.tag('meta', { + name: 'theme-color', + content: colors.primary, + }), + ]), oEmbedJSONHref && html.tag('link', { @@ -2304,9 +2349,24 @@ async function main() { bound.html = html; + bound.getColors = bindOpts(getColors, { + chroma, + }); + + bound.getLinkThemeString = bindOpts(unbound_getLinkThemeString, { + getColors: bound.getColors, + }); + + bound.getThemeString = bindOpts(unbound_getThemeString, { + getColors: bound.getColors, + }); + bound.link = withEntries(unbound_link, (entries) => - entries.map(([key, fn]) => [key, bindOpts(fn, {to})]) - ); + entries + .map(([key, fn]) => [key, bindOpts(fn, { + getLinkThemeString: bound.getLinkThemeString, + to, + })])); bound.parseAttributes = bindOpts(parseAttributes, { to, @@ -2363,10 +2423,6 @@ async function main() { getRevealStringFromWarnings: bound.getRevealStringFromWarnings, }); - bound.getLinkThemeString = getLinkThemeString; - - bound.getThemeString = getThemeString; - bound.getArtistString = bindOpts(getArtistString, { html, link: bound.link, @@ -2497,6 +2553,7 @@ async function main() { const pageHTML = writePage.html(pageInfo, { defaultLanguage: finalDefaultLanguage, + getThemeString: bound.getThemeString, language, languages, localizedPaths, diff --git a/src/util/colors.js b/src/util/colors.js index a0cc7486..dea67123 100644 --- a/src/util/colors.js +++ b/src/util/colors.js @@ -1,44 +1,36 @@ // Color and theming utility functions! Handy. -// Graciously stolen from https://stackoverflow.com/a/54071699! ::::) -// in: r,g,b in [0,1], out: h in [0,360) and s,l in [0,1] -export function rgb2hsl(r, g, b) { - let a = Math.max(r, g, b), - n = a - Math.min(r, g, b), - f = 1 - Math.abs(a + a - n - 1); - - let h = - n && - a == r - ? (g - b) / n - : a == g - ? 2 + (b - r) / n - : 4 + (r - g) / n; - - return [ - 60 * (h < 0 ? h + 6 : h), - f ? n / f : 0, - (a + a - n) / 2 - ]; -} +export function getColors(themeColor, { + // chroma.js external dependency (https://gka.github.io/chroma.js/) + chroma, +} = {}) { + if (!chroma) { + throw new Error('chroma.js library must be passed or bound for color manipulation'); + } + + const primary = chroma(themeColor); -export function getColors(primary) { - const [r, g, b] = primary - .slice(1) - .match(/[0-9a-fA-F]{2,2}/g) - .slice(0, 3) - .map((val) => parseInt(val, 16) / 255); + const dark = primary.luminance(0.02); + const dim = primary.desaturate(2).darken(1.5); - const [h, s, l] = rgb2hsl(r, g, b); + const bg = primary.luminance(0.008).desaturate(3.5).alpha(0.8); + const bgBlack = primary.saturate(1).luminance(0.0025).alpha(0.8); + const shadow = primary.desaturate(4).set('hsl.l', 0.05).alpha(0.8); - const dim = `hsl(${Math.round(h)}deg, ${Math.round(s * 50)}%, ${Math.round(l * 80)}%)`; - const bg = `hsla(${Math.round(h)}deg, ${Math.round(s * 15)}%, 12%, 0.80)`; + const hsl = primary.hsl(); + if (isNaN(hsl[0])) hsl[0] = 0; return { - primary, - dim, - bg, - rgb: [r, g, b], - hsl: [h, s, l], + primary: primary.hex(), + + dark: dark.hex(), + dim: dim.hex(), + + bg: bg.hex(), + bgBlack: bgBlack.hex(), + shadow: shadow.hex(), + + rgb: primary.rgb(), + hsl, }; } diff --git a/src/util/html.js b/src/util/html.js index 0a586223..6c429b92 100644 --- a/src/util/html.js +++ b/src/util/html.js @@ -72,6 +72,10 @@ export function tag(tagName, ...args) { } if (Array.isArray(content)) { + if (content.some(item => Array.isArray(item))) { + throw new Error(`Found array instead of string (tag) or null/falsey, did you forget to \`...\` spread an array or fragment?`); + } + const joiner = attrs?.[joinChildren]; content = content.filter(Boolean).join( (joiner diff --git a/src/util/link.js b/src/util/link.js index 9de4c95a..41ac9131 100644 --- a/src/util/link.js +++ b/src/util/link.js @@ -10,7 +10,6 @@ // gener8ting just a8out any link on the site. import * as html from './html.js'; -import {getColors} from './colors.js'; import { Album, @@ -23,7 +22,9 @@ import { Track, } from '../data/things.js'; -export function getLinkThemeString(color) { +export function unbound_getLinkThemeString(color, { + getColors, +}) { if (!color) return ''; const {primary, dim} = getColors(color); @@ -38,7 +39,9 @@ const linkHelper = attr = null, } = {}) => (thing, { + getLinkThemeString, to, + text = '', attributes = null, class: className = '', @@ -187,4 +190,8 @@ const link = { }, }; +export { + unbound_getLinkThemeString as getLinkThemeString, +}; + export default link; -- cgit 1.3.0-6-gf8a5