« get me outta code hell

content: generateAlbumSidebar + misc fixes - hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
author(quasar) nebula <qznebula@protonmail.com>2023-04-15 12:22:48 -0300
committer(quasar) nebula <qznebula@protonmail.com>2023-04-15 12:24:22 -0300
commit6b35077eb1542eaf9a89534d6920c35fee86cc04 (patch)
tree3ff4e6bb1860b67b9b2a4f2e39cdcd76cbeaa4b9
parent3a5b49cf3a10702c0dae1190c9baabd8a2c2ef3b (diff)
content: generateAlbumSidebar + misc fixes
This restores the CSS for sticky sidebars, but removes the
specific lines applying that effect (for the album sidebar).
There's also an experimental new splitter for the joined
group info box but we might go back on that or do something
different.

No tests for the new stuff here yet!
-rw-r--r--src/content/dependencies/generateAlbumInfoPage.js4
-rw-r--r--src/content/dependencies/generateAlbumSidebar.js241
-rw-r--r--src/content/dependencies/generatePageLayout.js128
-rw-r--r--src/content/dependencies/generateTrackInfoPage.js4
-rw-r--r--src/content/dependencies/generateTrackInfoPageContent.js23
-rw-r--r--src/content/dependencies/linkGroup.js8
-rw-r--r--src/static/site3.css18
-rw-r--r--src/write/build-modes/live-dev-server.js2
8 files changed, 410 insertions, 18 deletions
diff --git a/src/content/dependencies/generateAlbumInfoPage.js b/src/content/dependencies/generateAlbumInfoPage.js
index e5ce193c..21d5ec87 100644
--- a/src/content/dependencies/generateAlbumInfoPage.js
+++ b/src/content/dependencies/generateAlbumInfoPage.js
@@ -1,6 +1,7 @@
 export default {
   contentDependencies: [
     'generateAlbumInfoPageContent',
+    'generateAlbumSidebar',
     'generateAlbumSocialEmbed',
     'generateAlbumStyleRules',
     'generateColorStyleRules',
@@ -14,6 +15,7 @@ export default {
       layout: relation('generatePageLayout'),
 
       content: relation('generateAlbumInfoPageContent', album),
+      sidebar: relation('generateAlbumSidebar', album, null),
       socialEmbed: relation('generateAlbumSocialEmbed', album),
       albumStyleRules: relation('generateAlbumStyleRules', album),
       colorStyleRules: relation('generateColorStyleRules', album.color),
@@ -38,6 +40,8 @@ export default {
         cover: relations.content.cover,
         mainContent: relations.content.main.content,
 
+        ...relations.sidebar,
+
         // socialEmbed: relations.socialEmbed,
       });
   },
diff --git a/src/content/dependencies/generateAlbumSidebar.js b/src/content/dependencies/generateAlbumSidebar.js
new file mode 100644
index 00000000..223b1b7c
--- /dev/null
+++ b/src/content/dependencies/generateAlbumSidebar.js
@@ -0,0 +1,241 @@
+import {empty} from '../../util/sugar.js';
+
+function groupRelationships(album) {
+  return album.groups.map(group => {
+    const albums = group.albums.filter(album => album.date);
+    const index = albums.indexOf(album);
+
+    const previousAlbum = (index > 0) && albums[index - 1];
+    const nextAlbum = (index < albums.length - 1) && albums[index + 1];
+
+    return {group, previousAlbum, nextAlbum};
+  });
+}
+
+export default {
+  contentDependencies: [
+    'linkAlbum',
+    'linkExternal',
+    'linkGroup',
+    'linkTrack',
+  ],
+
+  extraDependencies: [
+    'getColors',
+    'html',
+    'language',
+    'transformMultiline',
+  ],
+
+  relations(relation, album, track) {
+    const relations = {};
+
+    relations.albumLink =
+      relation('linkAlbum', album);
+
+    relations.trackLinks =
+      album.trackSections.map(trackSection =>
+        trackSection.tracks.map(track =>
+          relation('linkTrack', track)));
+
+    relations.groupLinks =
+      groupRelationships(album)
+        .map(({group, previousAlbum, nextAlbum}) => ({
+          groupLink:
+            relation('linkGroup', group),
+
+          externalLinks:
+            group.urls.map(url =>
+              relation('linkExternal', url)),
+
+          previousAlbumLink:
+            previousAlbum &&
+              relation('linkAlbum', previousAlbum),
+
+          nextAlbumLink:
+            nextAlbum &&
+              relation('linkAlbum', nextAlbum),
+        }))
+
+    return relations;
+  },
+
+  data(album, track) {
+    const data = {};
+
+    data.isAlbumPage = !track;
+    data.isTrackPage = !!track;
+
+    data.hasTrackNumbers = album.hasTrackNumbers;
+
+    data.trackSectionInfo =
+      album.trackSections.map(trackSection => ({
+        name: trackSection.name,
+        color: trackSection.color,
+        isDefaultTrackSection: trackSection.isDefaultTrackSection,
+
+        firstTrackNumber: trackSection.startIndex + 1,
+        lastTrackNumber: trackSection.startIndex + trackSection.tracks.length,
+
+        includesCurrentTrack: track && trackSection.tracks.includes(track),
+        currentTrackIndex: trackSection.tracks.indexOf(track),
+      }));
+
+    data.groupInfo =
+      album.groups.map(group => ({
+        description: group.descriptionShort,
+      }));
+
+    return data;
+  },
+
+  generate(data, relations, {
+    getColors,
+    html,
+    language,
+    transformMultiline,
+  }) {
+    const {isTrackPage, isAlbumPage} = data;
+
+    const trackListPart = html.tags([
+      html.tag('h1', relations.albumLink),
+      data.trackSectionInfo.map(
+        ({
+          name,
+          color,
+          isDefaultTrackSection,
+
+          firstTrackNumber,
+          lastTrackNumber,
+
+          includesCurrentTrack,
+          currentTrackIndex,
+        }, index) => {
+          const trackLinks = relations.trackLinks[index];
+
+          const sectionName =
+            html.tag('span', {class: 'group-name'},
+              (isDefaultTrackSection
+                ? language.$('albumSidebar.trackList.fallbackSectionName')
+                : name));
+
+          let style;
+          if (color) {
+            const {primary} = getColors(color);
+            style = `--primary-color: ${primary}`;
+          }
+
+          const trackListItems =
+            trackLinks.map((trackLink, index) =>
+              html.tag('li',
+                {
+                  class:
+                    includesCurrentTrack &&
+                    index === currentTrackIndex &&
+                    'current',
+                },
+                language.$('albumSidebar.trackList.item', {
+                  track: trackLink,
+                })));
+
+          return html.tag('details',
+            {
+              class: includesCurrentTrack && 'current',
+
+              open: (
+                // Leave sidebar track sections collapsed on album info page,
+                // since there's already a view of the full track listing
+                // in the main content area.
+                isTrackPage &&
+
+                // Only expand the track section which includes the track
+                // currently being viewed by default.
+                includesCurrentTrack),
+            },
+            [
+              html.tag('summary', {style},
+                html.tag('span',
+                  (data.hasTrackNumbers
+                    ? language.$('albumSidebar.trackList.group.withRange', {
+                        group: sectionName,
+                        range: `${firstTrackNumber}&ndash;${lastTrackNumber}`
+                      })
+                    : language.$('albumSidebar.trackList.group', {
+                        group: sectionName,
+                      })))),
+
+              (data.hasTrackNumbers
+                ? html.tag('ol',
+                    {start: firstTrackNumber},
+                    trackListItems)
+                : html.tag('ul', trackListItems)),
+            ]);
+        }),
+    ]);
+
+    const groupParts = data.groupInfo.map(
+      ({description}, index) => {
+        const links = relations.groupLinks[index];
+
+        return html.tags([
+          html.tag('h1',
+            language.$('albumSidebar.groupBox.title', {
+              group: links.groupLink,
+            })),
+
+          isAlbumPage &&
+            transformMultiline(description),
+
+          !empty(links.externalLinks) &&
+            html.tag('p',
+              language.$('releaseInfo.visitOn', {
+                links: language.formatDisjunctionList(links.externalLinks),
+              })),
+
+          isAlbumPage &&
+          links.nextAlbumLink &&
+            html.tag('p', {class: 'group-chronology-link'},
+              language.$('albumSidebar.groupBox.next', {
+                album: links.nextAlbumLink,
+              })),
+
+          isAlbumPage &&
+          links.previousAlbumLink &&
+            html.tag('p', {class: 'group-chronology-link'},
+              language.$('albumSidebar.groupBox.previous', {
+                album: links.previousAlbumLink,
+              })),
+        ]);
+      });
+
+    if (isAlbumPage) {
+      return {
+        // leftSidebarStickyMode: 'last',
+        leftSidebarMultiple: [
+          ...groupParts.map(groupPart => ({content: groupPart})),
+          {content: trackListPart},
+        ],
+      };
+    } else {
+      return {
+        // leftSidebarStickyMode: 'column',
+        leftSidebarMultiple: [
+          {content: trackListPart},
+          // ...groupParts.map(groupPart => ({content: groupPart})),
+          {
+            content:
+              groupParts
+                .flatMap((part, i) => [
+                  part,
+                  i < groupParts.length - 1 &&
+                    html.tag('hr', {
+                      style: `border-color: var(--primary-color); border-style: none none dotted none`
+                    })
+                ])
+                .filter(Boolean),
+          },
+        ],
+      };
+    }
+  },
+};
diff --git a/src/content/dependencies/generatePageLayout.js b/src/content/dependencies/generatePageLayout.js
index 98b2d350..1ea5ce24 100644
--- a/src/content/dependencies/generatePageLayout.js
+++ b/src/content/dependencies/generatePageLayout.js
@@ -29,6 +29,47 @@ export default {
     transformMultiline,
     wikiInfo,
   }) {
+    const sidebarSlots = side => ({
+      // Content is a flat HTML array. It'll generate one sidebar section
+      // if specified.
+      [side + 'Content']: {type: 'html'},
+
+      // Multiple is an array of {content: (HTML)} objects. Each of these
+      // will generate one sidebar section.
+      [side + 'Multiple']: {
+        validate: v =>
+          v.arrayOf(
+            v.validateProperties({
+              content: v.isHTML,
+            })),
+      },
+
+      // Sticky mode controls which sidebar section(s), if any, follow the
+      // scroll position, "sticking" to the top of the browser viewport.
+      //
+      // '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
+      //
+      // Note: This doesn't affect the content of any sidebar section, only
+      // the whole section's containing box (or the sidebar column as a whole).
+      [side + 'StickyMode']: {
+        validate: v => v.is('last', 'column', 'static'),
+      },
+
+      // Collapsing sidebars disappear when the viewport is sufficiently
+      // thin. (This is the default.) Override as false to make the sidebar
+      // stay visible in thinner viewports, where the page layout will be
+      // reflowed so the sidebar is as wide as the screen and appears below
+      // nav, above the main content.
+      [side + 'Collapse']: {type: 'boolean', default: true},
+
+      // Wide sidebars generally take up more horizontal space in the normal
+      // page layout, and should be used if the content of the sidebar has
+      // a greater than typical focus compared to main content.
+      [side + 'Wide']: {type: 'boolean', defualt: false},
+    });
+
     return html.template({
       annotation: 'generatePageLayout',
 
@@ -36,15 +77,8 @@ export default {
         title: {type: 'html'},
         cover: {type: 'html'},
 
-        mainContent: {type: 'html'},
-        footerContent: {type: 'html'},
         socialEmbed: {type: 'html'},
 
-        headingMode: {
-          validate: v => v.is('sticky', 'static'),
-          default: 'static',
-        },
-
         styleRules: {
           validate: v => v.arrayOf(v.isString),
           default: [],
@@ -54,6 +88,24 @@ export default {
           validate: v => v.arrayOf(v.isString),
           default: [],
         },
+
+        // Main
+
+        mainContent: {type: 'html'},
+
+        headingMode: {
+          validate: v => v.is('sticky', 'static'),
+          default: 'static',
+        },
+
+        // Sidebars
+
+        ...sidebarSlots('leftSidebar'),
+        ...sidebarSlots('rightSidebar'),
+
+        // Nav & Footer
+
+        footerContent: {type: 'html'},
       },
 
       content(slots) {
@@ -114,6 +166,52 @@ export default {
               relations.footerLocalizationLinks,
             ]);
 
+        const generateSidebarHTML = (side, id) => {
+          const content = slots[side + 'Content'];
+          const multiple = slots[side + 'Multiple'];
+          const stickyMode = slots[side + 'StickyMode'];
+          const wide = slots[side + 'Wide'];
+          const collapse = slots[side + 'Collapse'];
+
+          let sidebarClasses = [];
+          let sidebarContent = html.blank();
+
+          if (!html.isBlank(content)) {
+            sidebarClasses = ['sidebar'];
+            sidebarContent = content;
+          } else if (multiple) {
+            sidebarClasses = ['sidebar-multiple'];
+            sidebarContent =
+              multiple
+                .filter(Boolean)
+                .map(({content}) =>
+                  html.tag('div',
+                    {
+                      [html.onlyIfContent]: true,
+                      class: 'sidebar',
+                    },
+                    content));
+          }
+
+          return html.tag('div',
+            {
+              [html.onlyIfContent]: true,
+              id,
+              class: [
+                'sidebar-column',
+                wide && 'wide',
+                !collapse && 'no-hide',
+                stickyMode !== 'static' && `sticky-${stickyMode}`,
+                ...sidebarClasses,
+              ],
+            },
+            sidebarContent);
+        }
+
+        const sidebarLeftHTML = generateSidebarHTML('leftSidebar', 'sidebar-left');
+        const sidebarRightHTML = generateSidebarHTML('rightSidebar', 'sidebar-right');
+        const collapseSidebars = slots.leftSidebarCollapse && slots.rightSidebarCollapse;
+
         const layoutHTML = [
           // navHTML,
           // banner.position === 'top' && bannerHTML,
@@ -122,18 +220,18 @@ export default {
             {
               class: [
                 'layout-columns',
-                // !collapseSidebars && 'vertical-when-thin',
-                // (sidebarLeftHTML || sidebarRightHTML) && 'has-one-sidebar',
-                // (sidebarLeftHTML && sidebarRightHTML) && 'has-two-sidebars',
-                // !(sidebarLeftHTML || sidebarRightHTML) && 'has-zero-sidebars',
-                // sidebarLeftHTML && 'has-sidebar-left',
-                // sidebarRightHTML && 'has-sidebar-right',
+                !collapseSidebars && 'vertical-when-thin',
+                (sidebarLeftHTML || sidebarRightHTML) && 'has-one-sidebar',
+                (sidebarLeftHTML && sidebarRightHTML) && 'has-two-sidebars',
+                !(sidebarLeftHTML || sidebarRightHTML) && 'has-zero-sidebars',
+                sidebarLeftHTML && 'has-sidebar-left',
+                sidebarRightHTML && 'has-sidebar-right',
               ],
             },
             [
-              // sidebarLeftHTML,
+              sidebarLeftHTML,
               mainHTML,
-              // sidebarRightHTML,
+              sidebarRightHTML,
             ]),
           // banner.position === 'bottom' && bannerHTML,
           footerHTML,
diff --git a/src/content/dependencies/generateTrackInfoPage.js b/src/content/dependencies/generateTrackInfoPage.js
index f7f14573..0519b7e8 100644
--- a/src/content/dependencies/generateTrackInfoPage.js
+++ b/src/content/dependencies/generateTrackInfoPage.js
@@ -1,6 +1,7 @@
 export default {
   contentDependencies: [
     'generateTrackInfoPageContent',
+    'generateAlbumSidebar',
     'generateAlbumStyleRules',
     'generateColorStyleRules',
     'generatePageLayout',
@@ -13,6 +14,7 @@ export default {
       layout: relation('generatePageLayout'),
 
       content: relation('generateTrackInfoPageContent', track),
+      sidebar: relation('generateAlbumSidebar', track.album, track),
       albumStyleRules: relation('generateAlbumStyleRules', track.album),
       colorStyleRules: relation('generateColorStyleRules', track.color),
     };
@@ -35,6 +37,8 @@ export default {
 
         cover: relations.content.cover,
         mainContent: relations.content.main.content,
+
+        ...relations.sidebar,
       });
   },
 }
diff --git a/src/content/dependencies/generateTrackInfoPageContent.js b/src/content/dependencies/generateTrackInfoPageContent.js
index 0ebb4121..3f37a0c9 100644
--- a/src/content/dependencies/generateTrackInfoPageContent.js
+++ b/src/content/dependencies/generateTrackInfoPageContent.js
@@ -33,6 +33,10 @@ export default {
     } else if (album.hasCoverArt) {
       relations.cover =
         relation('generateCoverArtwork', album.artTags);
+      relations.coverArtistLinks = null;
+    } else {
+      relations.cover = null;
+      relations.coverArtistLinks = null;
     }
 
     relations.artistLinks =
@@ -95,6 +99,15 @@ export default {
   }) {
     const content = {};
 
+    const formatContributions = contributionLinks =>
+      language.formatConjunctionList(
+        contributionLinks.map(link =>
+          link
+            .slots({
+              showContribution: true,
+              showIcons: true,
+            })));
+
     if (data.hasUniqueCoverArt) {
       content.cover = relations.cover
         .slots({
@@ -114,6 +127,8 @@ export default {
             data.coverArtFileExtension,
           ],
         });
+    } else {
+      content.cover = null;
     }
 
     content.main = {
@@ -125,10 +140,14 @@ export default {
           [html.joinChildren]: html.tag('br'),
         }, [
           !empty(relations.artistLinks) &&
-            language.$('releaseInfo.by', {artists: relations.artistLinks}),
+            language.$('releaseInfo.by', {
+              artists: formatContributions(relations.artistLinks),
+            }),
 
           !empty(relations.coverArtistLinks) &&
-            language.$('releaseInfo.coverArtBy', {artists: relations.coverArtistLinks}),
+            language.$('releaseInfo.coverArtBy', {
+              artists: formatContributions(relations.coverArtistLinks),
+            }),
 
           data.date &&
             language.$('releaseInfo.released', {
diff --git a/src/content/dependencies/linkGroup.js b/src/content/dependencies/linkGroup.js
new file mode 100644
index 00000000..ebab1b5b
--- /dev/null
+++ b/src/content/dependencies/linkGroup.js
@@ -0,0 +1,8 @@
+export default {
+  contentDependencies: ['linkThing'],
+
+  relations: (relation, group) =>
+    ({link: relation('linkThing', 'localized.groupInfo', group)}),
+
+  generate: (relations) => relations.link,
+};
diff --git a/src/static/site3.css b/src/static/site3.css
index 80624758..684b6d7d 100644
--- a/src/static/site3.css
+++ b/src/static/site3.css
@@ -1358,6 +1358,24 @@ main.long-content .content-sticky-heading-container .content-sticky-subheading-r
   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;
+}
+
 /* Image overlay */
 
 #image-overlay-container {
diff --git a/src/write/build-modes/live-dev-server.js b/src/write/build-modes/live-dev-server.js
index 1e72e5a8..50caafe9 100644
--- a/src/write/build-modes/live-dev-server.js
+++ b/src/write/build-modes/live-dev-server.js
@@ -359,7 +359,7 @@ export async function go({
         ...bound,
 
         appendIndexHTML: false,
-        transformMultiline: text => text,
+        transformMultiline: text => `<p>${text}</p>`,
       };
 
       // NOTE: ALL THIS STUFF IS PASTED, REVIEW AND INTEGRATE SOON(TM)