« get me outta code hell

hsmusic-wiki - HSMusic - static wiki software cataloguing collaborative creation
about summary refs log tree commit diff
diff options
context:
space:
mode:
-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)