« get me outta code hell

redesign & refinements for sticky layouting - 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>2022-11-27 22:49:16 -0400
committer(quasar) nebula <qznebula@protonmail.com>2022-11-27 22:49:16 -0400
commit5206ac7188c9eefd6f1d18050e2831b0f10be8ef (patch)
tree75c0f8ad55cd7771182ba64aa146104e5a5049ef
parentfb5859f083687b477b8e65e0e4de56baf4b35a98 (diff)
redesign & refinements for sticky layouting
-rw-r--r--package-lock.json11
-rw-r--r--package.json1
-rw-r--r--src/misc-templates.js20
-rw-r--r--src/page/album.js34
-rw-r--r--src/page/flash.js21
-rw-r--r--src/page/group.js4
-rw-r--r--src/page/homepage.js1
-rw-r--r--src/page/track.js13
-rw-r--r--src/static/client.js8
-rw-r--r--src/static/site2.css102
-rwxr-xr-xsrc/upd8.js81
-rw-r--r--src/util/colors.js62
-rw-r--r--src/util/html.js4
-rw-r--r--src/util/link.js11
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<hr>\n');
-    return {multiple: [trackListPart, combinedGroupPart]};
+        .join('\n<hr>\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;