From bc38f5d972a26abdfb917dee5c8eecc53e9228a3 Mon Sep 17 00:00:00 2001
From: dragonwocky <thedragonring.bod@gmail.com>
Date: Sun, 3 Oct 2021 00:13:50 +1000
Subject: [PATCH] side panel component: working except switcher

---
 extension/api/_.mjs                           |   2 +-
 extension/api/components/_.mjs                |  12 +-
 extension/api/components/feather.mjs          |  36 +++++
 extension/api/components/panel.css            | 151 ++++++++++++++++++
 extension/api/components/panel.mjs            | 141 ++++++++++++++++
 extension/api/components/sidebar.css          |   6 -
 extension/api/components/sidebar.mjs          |  24 ---
 extension/api/components/tooltip.mjs          |   4 +-
 extension/api/fmt.mjs                         |   4 +-
 extension/api/web.mjs                         | 119 ++++++--------
 .../client.mjs                                |   6 +-
 .../mod.json                                  |   8 +-
 .../blocks.mjs                                |  26 +--
 .../menu.mjs                                  |  26 +--
 .../notifications.mjs                         |  10 +-
 .../styles.mjs                                |   5 +-
 16 files changed, 434 insertions(+), 146 deletions(-)
 create mode 100644 extension/api/components/feather.mjs
 create mode 100644 extension/api/components/panel.css
 create mode 100644 extension/api/components/panel.mjs
 delete mode 100644 extension/api/components/sidebar.css
 delete mode 100644 extension/api/components/sidebar.mjs

diff --git a/extension/api/_.mjs b/extension/api/_.mjs
index c1a108d..334d08c 100644
--- a/extension/api/_.mjs
+++ b/extension/api/_.mjs
@@ -21,5 +21,5 @@ export * as fmt from './fmt.mjs';
 export * as registry from './registry.mjs';
 /** helpers for manipulation of a webpage */
 export * as web from './web.mjs';
-/** notion-style elements inc. the sidebar */
+/** shared notion-style elements */
 export * as components from './components/_.mjs';
diff --git a/extension/api/components/_.mjs b/extension/api/components/_.mjs
index 63e8daa..14544e3 100644
--- a/extension/api/components/_.mjs
+++ b/extension/api/components/_.mjs
@@ -7,7 +7,7 @@
 'use strict';
 
 /**
- * notion-style elements inc. the sidebar
+ * shared notion-style elements
  * @module notion-enhancer/api/components
  */
 
@@ -18,4 +18,12 @@
  */
 export { tooltip } from './tooltip.mjs';
 
-export { side } from './tooltip.mjs';
+/**
+ * generate an icon from the feather icons set
+ * @param {string} name - the name/id of the icon
+ * @param {object} attrs - an object of attributes to apply to the icon e.g. classes
+ * @returns {string} an svg string
+ */
+export { feather } from './feather.mjs';
+
+export { panel } from './panel.mjs';
diff --git a/extension/api/components/feather.mjs b/extension/api/components/feather.mjs
new file mode 100644
index 0000000..3734bda
--- /dev/null
+++ b/extension/api/components/feather.mjs
@@ -0,0 +1,36 @@
+/*
+ * notion-enhancer: api
+ * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (https://notion-enhancer.github.io/) under the MIT license
+ */
+
+'use strict';
+
+/**
+ * shared notion-style elements
+ * @module notion-enhancer/api/components/feather
+ */
+
+import { fs, web } from '../_.mjs';
+
+let _$iconSheet;
+
+/**
+ * generate an icon from the feather icons set
+ * @param {string} name - the name/id of the icon
+ * @param {object} attrs - an object of attributes to apply to the icon e.g. classes
+ * @returns {string} an svg string
+ */
+export const feather = async (name, attrs = {}) => {
+  if (!_$iconSheet) {
+    _$iconSheet = web.html`${await fs.getText('dep/feather-sprite.svg')}`;
+  }
+  attrs.style = (
+    (attrs.style ? attrs.style + ';' : '') +
+    'stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;fill:none;'
+  ).trim();
+  attrs.viewBox = '0 0 24 24';
+  return `<svg ${Object.entries(attrs)
+    .map(([key, val]) => `${web.escape(key)}="${web.escape(val)}"`)
+    .join(' ')}>${_$iconSheet.getElementById(name)?.innerHTML}</svg>`;
+};
diff --git a/extension/api/components/panel.css b/extension/api/components/panel.css
new file mode 100644
index 0000000..7458bc5
--- /dev/null
+++ b/extension/api/components/panel.css
@@ -0,0 +1,151 @@
+/*
+ * notion-enhancer core: components
+ * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (c) 2021 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
+ * (https://notion-enhancer.github.io/) under the MIT license
+ */
+
+:root {
+  --component--panel-width: 260px;
+}
+
+#enhancer--panel-hover-trigger {
+  height: 100vh;
+  width: 2.5rem;
+  max-height: 100%;
+  z-index: 999;
+  position: absolute;
+  top: 0;
+  right: 0;
+  flex-grow: 0;
+  flex-shrink: 0;
+  transition: width 300ms ease-in-out;
+}
+#enhancer--panel-hover-trigger[data-enhancer-panel-pinned] {
+  /* taking up the physical space of the panel to move topbar buttons */
+  position: relative;
+  width: var(--component--panel-width);
+}
+
+.notion-cursor-listener > div[style*='flex-end'] {
+  transition: margin-right 300ms ease-in-out;
+}
+.notion-cursor-listener > div[style*='flex-end'][data-enhancer-panel-pinned] {
+  margin-right: var(--component--panel-width);
+}
+.notion-frame {
+  transition: padding-right 300ms ease-in-out;
+}
+.notion-frame[data-enhancer-panel-pinned] {
+  padding-right: var(--component--panel-width);
+}
+
+#enhancer--panel {
+  z-index: 999;
+  position: absolute;
+  background: var(--theme--bg_secondary);
+  width: var(--component--panel-width);
+  right: calc(-1 * var(--component--panel-width));
+  opacity: 0;
+  height: 100vh;
+  flex-grow: 0;
+  flex-shrink: 0;
+  display: flex;
+  flex-direction: column;
+  transition: 300ms ease-in;
+
+  margin-top: 5rem;
+  max-height: calc(100vh - 10rem);
+}
+#enhancer--panel-hover-trigger:hover + #enhancer--panel:not([data-enhancer-panel-pinned]),
+#enhancer--panel:not([data-enhancer-panel-pinned]):hover {
+  opacity: 1;
+  transform: translateX(calc(-1 * var(--component--panel-width)));
+  box-shadow: var(--theme--ui_shadow, rgba(15, 15, 15, 0.05)) 0px 0px 0px 1px,
+    var(--theme--ui_shadow, rgba(15, 15, 15, 0.1)) 0px 3px 6px,
+    var(--theme--ui_shadow, rgba(15, 15, 15, 0.2)) 0px 9px 24px !important;
+}
+#enhancer--panel[data-enhancer-panel-pinned] {
+  opacity: 1;
+  max-height: 100%;
+  margin-top: 0;
+  transform: translateX(calc(-1 * var(--component--panel-width)));
+}
+
+#enhancer--panel-header {
+  font-size: 1.35rem;
+  font-weight: bold;
+  display: flex;
+  padding: 0.75rem 1rem;
+  align-items: center;
+}
+#enhancer--panel-content {
+  font-size: 1rem;
+  padding: 0.75rem 1rem;
+}
+#enhancer--panel-header-title {
+  padding-left: 0.5em;
+  padding-bottom: 0.1em;
+}
+#enhancer--panel-header-title > p {
+  margin: 0;
+  height: 1em;
+  display: flex;
+  align-items: center;
+}
+#enhancer--panel-header-title > p svg,
+#enhancer--panel-header-title > p img {
+  height: 1em;
+  width: 1em;
+}
+#enhancer--panel-header-title > p span {
+  font-size: 0.9em;
+  margin-left: 0.5em;
+}
+#enhancer--panel-header-toggle {
+  margin-left: auto;
+}
+#enhancer--panel-header-toggle,
+#enhancer--panel-header-switcher {
+  height: 1em;
+  width: 1em;
+  cursor: pointer;
+  opacity: 0;
+  transition: 300ms ease-in-out;
+  display: flex;
+  flex-direction: column;
+}
+#enhancer--panel-header-switcher svg {
+  width: 0.5em;
+  height: 0.5em;
+  display: block;
+  margin: auto;
+}
+
+#enhancer--panel:not([data-enhancer-panel-pinned]) #enhancer--panel-header-toggle {
+  transform: rotateZ(-180deg);
+}
+#enhancer--panel:hover #enhancer--panel-header-toggle,
+#enhancer--panel:hover #enhancer--panel-header-switcher {
+  opacity: 1;
+}
+
+#enhancer--panel-resize {
+  position: absolute;
+  left: -5px;
+  height: 100%;
+  width: 10px;
+}
+#enhancer--panel[data-enhancer-panel-pinned] #enhancer--panel-resize {
+  cursor: col-resize;
+}
+#enhancer--panel-resize div {
+  transition: background 150ms ease-in-out;
+  background: transparent;
+  width: 2px;
+  margin-left: 4px;
+  height: 100%;
+}
+#enhancer--panel[data-enhancer-panel-pinned] #enhancer--panel-resize:hover div {
+  background: var(--theme--ui_divider);
+}
diff --git a/extension/api/components/panel.mjs b/extension/api/components/panel.mjs
new file mode 100644
index 0000000..b9f44e1
--- /dev/null
+++ b/extension/api/components/panel.mjs
@@ -0,0 +1,141 @@
+/*
+ * notion-enhancer: api
+ * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
+ * (https://notion-enhancer.github.io/) under the MIT license
+ */
+
+'use strict';
+
+/**
+ * shared notion-style elements
+ * @module notion-enhancer/api/components/side-panel
+ */
+
+import { web, components, registry } from '../_.mjs';
+const db = await registry.db('36a2ffc9-27ff-480e-84a7-c7700a7d232d');
+
+let $panel,
+  _views = [];
+
+export const panel = async (icon, title, generator = () => {}) => {
+  _views.push({
+    icon: web.html`${icon}`,
+    title: web.html`<span>${web.escape(title)}</span>`,
+    $elem: generator(),
+  });
+
+  if (!$panel) {
+    $panel = web.html`<div id="enhancer--panel"></div>`;
+
+    const notionRightSidebarSelector = '.notion-cursor-listener > div[style*="flex-end"]';
+    await web.whenReady([notionRightSidebarSelector]);
+    web.loadStylesheet('api/components/panel.css');
+
+    const $title = web.html`<div id="enhancer--panel-header-title"></div>`,
+      $header = web.render(web.html`<div id="enhancer--panel-header"></div>`, $title),
+      $content = web.html`<div id="enhancer--panel-content"></div>`;
+
+    // opening/closing
+    const $notionFrame = document.querySelector('.notion-frame'),
+      $notionRightSidebar = document.querySelector(notionRightSidebarSelector),
+      $pinnedToggle = web.html`<div id="enhancer--panel-header-toggle">
+        ${await components.feather('chevrons-right')}
+      </div>`,
+      $hoverTrigger = web.html`<div id="enhancer--panel-hover-trigger"></div>`,
+      panelPinnedAttr = 'data-enhancer-panel-pinned',
+      isPinned = () => $panel.hasAttribute(panelPinnedAttr),
+      isRightSidebarOpen = () =>
+        $notionRightSidebar.matches('[style*="border-left: 1px solid rgba(0, 0, 0, 0)"]'),
+      togglePanel = () => {
+        const $elems = [$notionRightSidebar, $hoverTrigger, $panel];
+        if (isPinned()) {
+          if (isRightSidebarOpen()) $elems.push($notionFrame);
+          for (const $elem of $elems) $elem.removeAttribute(panelPinnedAttr);
+        } else {
+          $elems.push($notionFrame);
+          for (const $elem of $elems) $elem.setAttribute(panelPinnedAttr, 'true');
+        }
+        db.set(['panel.pinned'], isPinned());
+      };
+    web.addDocumentObserver(() => {
+      if (isPinned()) {
+        if (isRightSidebarOpen()) {
+          $notionFrame.removeAttribute(panelPinnedAttr);
+        } else {
+          $notionFrame.setAttribute(panelPinnedAttr, 'true');
+        }
+      }
+    }, [notionRightSidebarSelector]);
+    if (await db.get(['panel.pinned'])) togglePanel();
+    web.addHotkeyListener(await db.get(['panel.hotkey']), togglePanel);
+    $pinnedToggle.addEventListener('click', togglePanel);
+
+    // resizing
+    let dragStartX,
+      dragStartWidth,
+      dragEventsFired,
+      panelWidth = await db.get(['panel.width'], 240);
+    const $resizeHandle = web.html`<div id="enhancer--panel-resize"><div></div></div>`,
+      updateWidth = async () => {
+        document.documentElement.style.setProperty(
+          '--component--panel-width',
+          panelWidth + 'px'
+        );
+        db.set(['panel.width'], panelWidth);
+      },
+      resizeDrag = (event) => {
+        event.preventDefault();
+        dragEventsFired = true;
+        panelWidth = dragStartWidth + (dragStartX - event.clientX);
+        if (panelWidth < 190) panelWidth = 190;
+        if (panelWidth > 480) panelWidth = 480;
+        $panel.style.width = panelWidth + 'px';
+        $hoverTrigger.style.width = panelWidth + 'px';
+        $notionFrame.style.paddingRight = panelWidth + 'px';
+        $notionRightSidebar.style.right = panelWidth + 'px';
+      },
+      resizeEnd = (event) => {
+        $panel.style.width = '';
+        $hoverTrigger.style.width = '';
+        $notionFrame.style.paddingRight = '';
+        $notionRightSidebar.style.right = '';
+        updateWidth();
+        $resizeHandle.style.cursor = '';
+        document.body.removeEventListener('mousemove', resizeDrag);
+        document.body.removeEventListener('mouseup', resizeEnd);
+      },
+      resizeStart = (event) => {
+        dragStartX = event.clientX;
+        dragStartWidth = panelWidth;
+        $resizeHandle.style.cursor = 'auto';
+        document.body.addEventListener('mousemove', resizeDrag);
+        document.body.addEventListener('mouseup', resizeEnd);
+      };
+    updateWidth();
+    $resizeHandle.addEventListener('mousedown', resizeStart);
+    $resizeHandle.addEventListener('click', () => {
+      if (dragEventsFired) {
+        dragEventsFired = false;
+      } else togglePanel();
+    });
+
+    // view selection
+    const $switcherTrigger = web.html`<div id="enhancer--panel-header-switcher">
+        ${await components.feather('chevron-up')}
+        ${await components.feather('chevron-down')}
+      </div>`,
+      renderView = (view) => {
+        web.render(web.empty($title), web.render(web.html`<p></p>`, view.icon, view.title));
+        web.render(web.empty($content), view.$elem);
+      };
+    renderView(_views[0]);
+
+    web.render(
+      $panel,
+      web.render($header, $switcherTrigger, $title, $pinnedToggle),
+      $content,
+      $resizeHandle
+    );
+    $notionRightSidebar.after($hoverTrigger, $panel);
+  }
+};
diff --git a/extension/api/components/sidebar.css b/extension/api/components/sidebar.css
deleted file mode 100644
index b52afd8..0000000
--- a/extension/api/components/sidebar.css
+++ /dev/null
@@ -1,6 +0,0 @@
-/*
- * notion-enhancer core: components
- * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
- * (c) 2021 CloudHill <rl.cloudhill@gmail.com> (https://github.com/CloudHill)
- * (https://notion-enhancer.github.io/) under the MIT license
- */
diff --git a/extension/api/components/sidebar.mjs b/extension/api/components/sidebar.mjs
deleted file mode 100644
index 7593a46..0000000
--- a/extension/api/components/sidebar.mjs
+++ /dev/null
@@ -1,24 +0,0 @@
-/*
- * notion-enhancer: api
- * (c) 2021 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
- * (https://notion-enhancer.github.io/) under the MIT license
- */
-
-'use strict';
-
-/**
- * notion-style elements inc. the sidebar
- * @module notion-enhancer/api/components/side-panel
- */
-
-import { web } from '../_.mjs';
-
-let _$sidebar;
-
-export const sidebar = (icon, name, loader = ($panel) => {}) => {
-  if (!_$sidebar) {
-    web.loadStylesheet('api/components/sidebar.css');
-    _$sidebar = web.html`<div id="enhancer--sidebar"></div>`;
-    web.render(document.body, _$sidebar);
-  }
-};
diff --git a/extension/api/components/tooltip.mjs b/extension/api/components/tooltip.mjs
index 5d03bb8..4487ed7 100644
--- a/extension/api/components/tooltip.mjs
+++ b/extension/api/components/tooltip.mjs
@@ -7,7 +7,7 @@
 'use strict';
 
 /**
- * notion-style elements inc. the sidebar
+ * shared notion-style elements
  * @module notion-enhancer/api/components/tooltip
  */
 
@@ -22,8 +22,8 @@ let _$tooltip;
  */
 export const tooltip = ($ref, text) => {
   if (!_$tooltip) {
-    web.loadStylesheet('api/components/tooltip.css');
     _$tooltip = web.html`<div id="enhancer--tooltip"></div>`;
+    web.loadStylesheet('api/components/tooltip.css');
     web.render(document.body, _$tooltip);
   }
   text = fmt.md.render(text);
diff --git a/extension/api/fmt.mjs b/extension/api/fmt.mjs
index 0c75932..f905c01 100644
--- a/extension/api/fmt.mjs
+++ b/extension/api/fmt.mjs
@@ -11,7 +11,7 @@
  * @module notion-enhancer/api/fmt
  */
 
-import { web, fs } from './_.mjs';
+import { web, fs, components } from './_.mjs';
 
 import '../dep/prism.min.js';
 /** syntax highlighting using https://prismjs.com/ */
@@ -21,7 +21,7 @@ Prism.hooks.add('complete', async (event) => {
   event.element.parentElement.removeAttribute('tabindex');
   event.element.parentElement.parentElement
     .querySelector('.copy-to-clipboard-button')
-    .prepend(web.html`${await web.icon('clipboard')}`);
+    .prepend(web.html`${await components.feather('clipboard')}`);
 });
 
 import '../dep/markdown-it.min.js';
diff --git a/extension/api/web.mjs b/extension/api/web.mjs
index e116b44..d215c49 100644
--- a/extension/api/web.mjs
+++ b/extension/api/web.mjs
@@ -13,12 +13,6 @@
 
 import { fs, fmt } from './_.mjs';
 
-const _hotkeyEventListeners = [],
-  _documentObserverListeners = [],
-  _documentObserverEvents = [];
-
-let _hotkeyEvent, _documentObserver;
-
 import '../dep/jscolor.min.js';
 /** color picker with alpha channel using https://jscolor.com/ */
 export const jscolor = JSColor;
@@ -144,21 +138,27 @@ export const loadStylesheet = (path) => {
   return true;
 };
 
-/**
- * generate an icon from the feather icons set
- * @param {string} name - the name/id of the icon
- * @param {object} attrs - an object of attributes to apply to the icon e.g. classes
- * @returns {string} an svg string
- */
-export const icon = (name, attrs = {}) => {
-  attrs.style = (
-    (attrs.style || '') +
-    ';stroke:currentColor;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;fill:none;'
-  ).trim();
-  return `<svg ${Object.entries(attrs)
-    .map(([key, val]) => `${escape(key)}="${escape(val)}"`)
-    .join(' ')}><use xlink:href="${fs.localPath('dep/feather-sprite.svg')}#${name}" /></svg>`;
-};
+const _hotkeyEvent = document.addEventListener('keyup', (event) => {
+    if (document.activeElement.nodeName === 'INPUT') return;
+    for (const hotkey of _hotkeyEventListeners) {
+      const pressed = hotkey.keys.every((key) => {
+        key = key.toLowerCase();
+        const modifiers = {
+          metaKey: ['meta', 'os', 'win', 'cmd', 'command'],
+          ctrlKey: ['ctrl', 'control'],
+          shiftKey: ['shift'],
+          altKey: ['alt'],
+        };
+        for (const modifier in modifiers) {
+          const pressed = modifiers[modifier].includes(key) && event[modifier];
+          if (pressed) return true;
+        }
+        if (key === event.key.toLowerCase()) return true;
+      });
+      if (pressed) hotkey.callback();
+    }
+  }),
+  _hotkeyEventListeners = [];
 
 /**
  * register a hotkey listener to the page
@@ -169,28 +169,6 @@ export const icon = (name, attrs = {}) => {
  */
 export const addHotkeyListener = (keys, callback) => {
   if (typeof keys === 'string') keys = keys.split('+');
-  if (!_hotkeyEvent) {
-    _hotkeyEvent = document.addEventListener('keyup', (event) => {
-      if (document.activeElement.nodeName === 'INPUT') return;
-      for (const hotkey of _hotkeyEventListeners) {
-        const pressed = hotkey.keys.every((key) => {
-          key = key.toLowerCase();
-          const modifiers = {
-            metaKey: ['meta', 'os', 'win', 'cmd', 'command'],
-            ctrlKey: ['ctrl', 'control'],
-            shiftKey: ['shift'],
-            altKey: ['alt'],
-          };
-          for (const modifier in modifiers) {
-            const pressed = modifiers[modifier].includes(key) && event[modifier];
-            if (pressed) return true;
-          }
-          if (key === event.key.toLowerCase()) return true;
-        });
-        if (pressed) hotkey.callback();
-      }
-    });
-  }
   _hotkeyEventListeners.push({ keys, callback });
 };
 /**
@@ -203,40 +181,39 @@ export const removeHotkeyListener = (callback) => {
   );
 };
 
+const _documentObserver = new MutationObserver((list, observer) => {
+    if (!_documentObserverEvents.length)
+      requestIdleCallback(() => (queue) => {
+        while (queue.length) {
+          const event = queue.shift();
+          for (const listener of _documentObserverListeners) {
+            if (
+              !listener.selectors.length ||
+              listener.selectors.some(
+                (selector) =>
+                  event.target.matches(selector) || event.target.matches(`${selector} *`)
+              )
+            ) {
+              listener.callback(event);
+            }
+          }
+        }
+      });
+    _documentObserverEvents.push(...list);
+  }),
+  _documentObserverListeners = [],
+  _documentObserverEvents = [];
+_documentObserver.observe(document.body, {
+  childList: true,
+  subtree: true,
+  attributes: true,
+});
 /**
  * add a listener to watch for changes to the dom
  * @param {onDocumentObservedCallback} callback
  * @param {array<string>} [selectors]
  */
 export const addDocumentObserver = (callback, selectors = []) => {
-  if (!_documentObserver) {
-    const handle = (queue) => {
-      while (queue.length) {
-        const event = queue.shift();
-        for (const listener of _documentObserverListeners) {
-          if (
-            !listener.selectors.length ||
-            listener.selectors.some(
-              (selector) =>
-                event.target.matches(selector) || event.target.matches(`${selector} *`)
-            )
-          ) {
-            listener.callback(event);
-          }
-        }
-      }
-    };
-    _documentObserver = new MutationObserver((list, observer) => {
-      if (!_documentObserverEvents.length)
-        requestIdleCallback(() => handle(_documentObserverEvents));
-      _documentObserverEvents.push(...list);
-    });
-    _documentObserver.observe(document.body, {
-      childList: true,
-      subtree: true,
-      attributes: true,
-    });
-  }
   _documentObserverListeners.push({ callback, selectors });
 };
 
diff --git a/extension/repo/bypass-preview@cb6fd684-f113-4a7a-9423-8f0f0cff069f/client.mjs b/extension/repo/bypass-preview@cb6fd684-f113-4a7a-9423-8f0f0cff069f/client.mjs
index 4c640f6..1a0d46c 100644
--- a/extension/repo/bypass-preview@cb6fd684-f113-4a7a-9423-8f0f0cff069f/client.mjs
+++ b/extension/repo/bypass-preview@cb6fd684-f113-4a7a-9423-8f0f0cff069f/client.mjs
@@ -7,7 +7,7 @@
 'use strict';
 
 export default async function (api, db) {
-  const { web } = api;
+  const { web, components } = api;
   await web.whenReady();
 
   let _lastPage = {};
@@ -30,4 +30,8 @@ export default async function (api, db) {
       _lastPage = getCurrentPage();
     }
   });
+
+  components.panel(await components.feather('sidebar'), 'Test Panel', ($panel) => {
+    return web.html`<p>test</p>`;
+  });
 }
diff --git a/extension/repo/components@36a2ffc9-27ff-480e-84a7-c7700a7d232d/mod.json b/extension/repo/components@36a2ffc9-27ff-480e-84a7-c7700a7d232d/mod.json
index 5b43a62..bb0cc36 100644
--- a/extension/repo/components@36a2ffc9-27ff-480e-84a7-c7700a7d232d/mod.json
+++ b/extension/repo/components@36a2ffc9-27ff-480e-84a7-c7700a7d232d/mod.json
@@ -3,7 +3,7 @@
   "name": "components",
   "id": "36a2ffc9-27ff-480e-84a7-c7700a7d232d",
   "version": "0.2.0",
-  "description": "notion-style elements reused by other mods, inc. the sidebar.",
+  "description": "shared notion-style elements.",
   "tags": ["core"],
   "authors": [
     {
@@ -24,10 +24,10 @@
   "options": [
     {
       "type": "hotkey",
-      "key": "side-panel-hotkey",
-      "label": "toggle enhancer sidebar hotkey",
+      "key": "panel.hotkey",
+      "label": "toggle panel hotkey",
       "value": "Ctrl+Alt+\\",
-      "tooltip": "opens/closes the extra sidebar in notion - will only work if a mod is making use of it."
+      "tooltip": "opens/closes the side panel in notion - will only work if a mod is making use of it."
     }
   ]
 }
diff --git a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/blocks.mjs b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/blocks.mjs
index 41cf1cb..4f1a7e0 100644
--- a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/blocks.mjs
+++ b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/blocks.mjs
@@ -58,7 +58,7 @@ export const options = {
   toggle: async (mod, opt) => {
     const checked = await profileDB.get([mod.id, opt.key], opt.value),
       $toggle = blocks.toggle(opt.label, checked),
-      $tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
+      $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
       $label = $toggle.children[0],
       $input = $toggle.children[1];
     if (opt.tooltip) {
@@ -73,7 +73,7 @@ export const options = {
   },
   select: async (mod, opt) => {
     const value = await profileDB.get([mod.id, opt.key], opt.values[0]),
-      $tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
+      $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
       $label = web.render(
         web.html`<label class="input-label"></label>`,
         web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
@@ -88,7 +88,7 @@ export const options = {
       $select = web.html`<select class="input">
         ${$options.join('')}
       </select>`,
-      $icon = web.html`${web.icon('chevron-down', { class: 'input-icon' })}`;
+      $icon = web.html`${await components.feather('chevron-down', { class: 'input-icon' })}`;
     if (opt.tooltip) components.tooltip($tooltip, opt.tooltip);
     $select.addEventListener('change', async (event) => {
       await profileDB.set([mod.id, opt.key], $select.value);
@@ -98,13 +98,13 @@ export const options = {
   },
   text: async (mod, opt) => {
     const value = await profileDB.get([mod.id, opt.key], opt.value),
-      $tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
+      $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
       $label = web.render(
         web.html`<label class="input-label"></label>`,
         web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
       ),
       $input = web.html`<input type="text" class="input" value="${web.escape(value)}">`,
-      $icon = web.html`${web.icon('type', { class: 'input-icon' })}`;
+      $icon = web.html`${await components.feather('type', { class: 'input-icon' })}`;
     if (opt.tooltip) components.tooltip($tooltip, opt.tooltip);
     $input.addEventListener('change', async (event) => {
       await profileDB.set([mod.id, opt.key], $input.value);
@@ -114,13 +114,13 @@ export const options = {
   },
   number: async (mod, opt) => {
     const value = await profileDB.get([mod.id, opt.key], opt.value),
-      $tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
+      $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
       $label = web.render(
         web.html`<label class="input-label"></label>`,
         web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
       ),
       $input = web.html`<input type="number" class="input" value="${value}">`,
-      $icon = web.html`${web.icon('hash', { class: 'input-icon' })}`;
+      $icon = web.html`${await components.feather('hash', { class: 'input-icon' })}`;
     if (opt.tooltip) components.tooltip($tooltip, opt.tooltip);
     $input.addEventListener('change', async (event) => {
       await profileDB.set([mod.id, opt.key], $input.value);
@@ -130,13 +130,13 @@ export const options = {
   },
   color: async (mod, opt) => {
     const value = await profileDB.get([mod.id, opt.key], opt.value),
-      $tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
+      $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
       $label = web.render(
         web.html`<label class="input-label"></label>`,
         web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
       ),
       $input = web.html`<input type="text" class="input">`,
-      $icon = web.html`${web.icon('droplet', { class: 'input-icon' })}`,
+      $icon = web.html`${await components.feather('droplet', { class: 'input-icon' })}`,
       paint = () => {
         $input.style.background = $picker.toBackground();
         $input.style.color = $picker.isLight() ? '#000' : '#fff';
@@ -163,7 +163,7 @@ export const options = {
   },
   file: async (mod, opt) => {
     const { filename } = (await profileDB.get([mod.id, opt.key], {})) || {},
-      $tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
+      $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
       $label = web.render(
         web.html`<label class="input-label"></label>`,
         web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
@@ -172,7 +172,7 @@ export const options = {
       $input = web.html`<input type="file" class="hidden" accept=${web.escape(
         opt.extensions.join(',')
       )}>`,
-      $icon = web.html`${web.icon('file', { class: 'input-icon' })}`,
+      $icon = web.html`${await components.feather('file', { class: 'input-icon' })}`,
       $filename = web.html`<span>${web.escape(filename || 'none')}</span>`,
       $latest = web.render(web.html`<button class="file-latest">Latest: </button>`, $filename);
     if (opt.tooltip) components.tooltip($tooltip, opt.tooltip);
@@ -201,13 +201,13 @@ export const options = {
   },
   hotkey: async (mod, opt) => {
     const value = await profileDB.get([mod.id, opt.key], opt.value),
-      $tooltip = web.html`${web.icon('info', { class: 'input-tooltip' })}`,
+      $tooltip = web.html`${await components.feather('info', { class: 'input-tooltip' })}`,
       $label = web.render(
         web.html`<label class="input-label"></label>`,
         web.render(web.html`<p></p>`, opt.tooltip ? $tooltip : '', opt.label)
       ),
       $input = web.html`<input type="text" class="input" value="${web.escape(value)}">`,
-      $icon = web.html`${web.icon('command', { class: 'input-icon' })}`;
+      $icon = web.html`${await components.feather('command', { class: 'input-icon' })}`;
     if (opt.tooltip) components.tooltip($tooltip, opt.tooltip);
     $input.addEventListener('keydown', async (event) => {
       event.preventDefault();
diff --git a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/menu.mjs b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/menu.mjs
index 49c2611..6679759 100644
--- a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/menu.mjs
+++ b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/menu.mjs
@@ -6,7 +6,7 @@
 
 'use strict';
 
-import { env, fs, storage, registry, web } from '../../api/_.mjs';
+import { env, fs, storage, registry, web, components } from '../../api/_.mjs';
 import { notifications } from './notifications.mjs';
 import { blocks, options } from './blocks.mjs';
 import './styles.mjs';
@@ -74,17 +74,17 @@ $profile.addEventListener('click', async (event) => {
         pattern="/^[A-Za-z0-9_-]+$/"
       >`,
       $export = web.html`<button class="profile-export">
-        ${web.icon('download', { class: 'profile-icon-action' })}
+        ${await components.feather('download', { class: 'profile-icon-action' })}
       </button>`,
       $import = web.html`<label class="profile-import">
         <input type="file" class="hidden" accept="application/json">
-        ${web.icon('upload', { class: 'profile-icon-action' })}
+        ${await components.feather('upload', { class: 'profile-icon-action' })}
       </label>`,
       $save = web.html`<button class="profile-save">
-        ${web.icon('save', { class: 'profile-icon-text' })} Save
+        ${await components.feather('save', { class: 'profile-icon-text' })} Save
       </button>`,
       $delete = web.html`<button class="profile-delete">
-        ${web.icon('trash-2', { class: 'profile-icon-text' })} Delete
+        ${await components.feather('trash-2', { class: 'profile-icon-text' })} Delete
       </button>`,
       $error = web.html`<p class="profile-error"></p>`;
     $export.addEventListener('click', async (event) => {
@@ -175,12 +175,12 @@ $profile.addEventListener('click', async (event) => {
       web.render(
         web.html`<label class="input-label"></label>`,
         $select,
-        web.html`${web.icon('chevron-down', { class: 'input-icon' })}`
+        web.html`${await components.feather('chevron-down', { class: 'input-icon' })}`
       ),
       web.render(
         web.html`<label class="input-label"></label>`,
         $edit,
-        web.html`${web.icon('type', { class: 'input-icon' })}`
+        web.html`${await components.feather('type', { class: 'input-icon' })}`
       ),
       web.render(web.html`<p class="profile-actions"></p>`, $export, $import, $save, $delete),
       $error
@@ -288,7 +288,7 @@ const _$modListCache = {},
           web.render(
             web.html`<label class="search-container"></label>`,
             $search,
-            web.html`${web.icon('search', { class: 'input-icon' })}`
+            web.html`${await components.feather('search', { class: 'input-icon' })}`
           ),
           message ? web.html`<p class="main-message">${web.escape(message)}</p>` : '',
           $list
@@ -303,14 +303,13 @@ const $notionNavItem = web.html`<h1 class="nav-notion">
       /width="\d+" height="\d+"/,
       `class="nav-notion-icon"`
     )}
-    <a href="https://notion-enhancer.github.io/" target="_blank">notion-enhancer</a>
+    <span>notion-enhancer</span>
   </h1>`;
-$notionNavItem.children[0].addEventListener('click', env.focusNotion);
+$notionNavItem.addEventListener('click', env.focusNotion);
 
 const $coreNavItem = web.html`<a href="?view=core" class="nav-item">core</a>`,
   $extensionsNavItem = web.html`<a href="?view=extensions" class="nav-item">extensions</a>`,
-  $themesNavItem = web.html`<a href="?view=themes" class="nav-item">themes</a>`,
-  $communityNavItem = web.html`<a href="https://discord.gg/sFWPXtA" class="nav-item">community</a>`;
+  $themesNavItem = web.html`<a href="?view=themes" class="nav-item">themes</a>`;
 
 web.render(
   document.body,
@@ -324,7 +323,8 @@ web.render(
         $coreNavItem,
         $extensionsNavItem,
         $themesNavItem,
-        $communityNavItem
+        web.html`<a href="https://notion-enhancer.github.io" class="nav-item">docs</a>`,
+        web.html`<a href="https://discord.gg/sFWPXtA" class="nav-item">community</a>`
       ),
       $main
     ),
diff --git a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/notifications.mjs b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/notifications.mjs
index 64fcf64..97e0f93 100644
--- a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/notifications.mjs
+++ b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/notifications.mjs
@@ -6,7 +6,7 @@
 
 'use strict';
 
-import { env, fs, storage, fmt, registry, web } from '../../api/_.mjs';
+import { env, fs, storage, fmt, registry, web, components } from '../../api/_.mjs';
 import { tw } from './styles.mjs';
 
 export const notifications = {
@@ -16,7 +16,7 @@ export const notifications = {
     registry.welcomeNotification,
     ...(await fs.getJSON('https://notion-enhancer.github.io/notifications.json')),
   ],
-  add({ icon, message, id = undefined, color = undefined, link = undefined }) {
+  async add({ icon, message, id = undefined, color = undefined, link = undefined }) {
     const $notification = link
         ? web.html`<a
           href="${web.escape(link)}"
@@ -47,16 +47,16 @@ export const notifications = {
         web.html`<span class="notification-text markdown-inline">
           ${fmt.md.renderInline(message)}
         </span>`,
-        web.html`${web.icon(icon, { class: 'notification-icon' })}`
+        web.html`${await components.feather(icon, { class: 'notification-icon' })}`
       )
     );
     return $notification;
   },
   _onChange: false,
-  onChange() {
+  async onChange() {
     if (this._onChange) return;
     this._onChange = true;
-    const $notification = this.add({
+    const $notification = await this.add({
       icon: 'refresh-cw',
       message: 'Reload to apply changes.',
     });
diff --git a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/styles.mjs b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/styles.mjs
index de8337f..f9e15c1 100644
--- a/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/styles.mjs
+++ b/extension/repo/menu@a6621988-551d-495a-97d8-3c568bca2e9e/styles.mjs
@@ -36,13 +36,13 @@ const customClasses = {
   'notification-icon': apply`fill-current opacity-75 h-4 w-4 mx-2`,
   'body-container': apply`flex w-full h-full overflow-hidden`,
   'content-container': apply`h-full w-full-96`,
-  'nav': apply`px-4 py-3 flex flex-wrap items-center border-b border-divider h-48 sm:h-32 lg:h-16`,
+  'nav': apply`px-4 py-3 flex flex-wrap items-center border-b border-divider h-64 sm:h-48 md:h-32 lg:h-16`,
   'nav-notion': apply`flex items-center font-semibold text-xl cursor-pointer select-none mr-4
       ml-4 sm:mb-4 md:w-full lg:(w-auto ml-0 mb-0)`,
   'nav-notion-icon': apply`h-12 w-12 mr-5 sm:(h-6 w-6 mr-3)`,
   'nav-item': apply`ml-4 px-3 py-2 rounded-md text-sm font-medium hover:bg-interactive-hover focus:bg-interactive-focus`,
   'nav-item-selected': apply`ml-4 px-3 py-2 rounded-md text-sm font-medium ring-1 ring-divider bg-notion-secondary`,
-  'main': apply`transition px-4 py-3 overflow-y-auto max-h-full-48 sm:max-h-full-32 lg:max-h-full-16`,
+  'main': apply`transition px-4 py-3 overflow-y-auto max-h-full-64 sm:max-h-full-48 md:max-h-full-32 lg:max-h-full-16`,
   'main-message': apply`mx-2.5 my-2.5 px-px text-sm text-foreground-secondary text-justify`,
   'mods-list': apply`flex flex-wrap`,
   'mod-container': apply`w-full md:w-1/2 lg:w-1/3 xl:w-1/4 2xl:w-1/5 px-2.5 py-2.5 box-border`,
@@ -145,6 +145,7 @@ setup({
         'full-16': 'calc(100% - 4rem)',
         'full-32': 'calc(100% - 8rem)',
         'full-48': 'calc(100% - 12rem)',
+        'full-64': 'calc(100% - 16rem)',
       },
     },
   },