diff --git a/extension/api/components/panel.css b/extension/api/components/panel.css index 7458bc5..149ab3c 100644 --- a/extension/api/components/panel.css +++ b/extension/api/components/panel.css @@ -72,61 +72,81 @@ 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 { +.enhancer--panel-view-title { margin: 0; height: 1em; display: flex; align-items: center; + font-size: 1.1rem; + font-weight: 600; } -#enhancer--panel-header-title > p svg, -#enhancer--panel-header-title > p img { +.enhancer--panel-view-title svg, +.enhancer--panel-view-title img { height: 1em; width: 1em; } -#enhancer--panel-header-title > p span { +.enhancer--panel-view-title span { font-size: 0.9em; margin-left: 0.5em; } + +#enhancer--panel-header { + font-size: 1.2rem; + font-weight: 600; + display: flex; + align-items: center; + cursor: pointer; + user-select: none; +} +#enhancer--panel-header .enhancer--panel-view-title { + font-size: 1.2rem; +} +#enhancer--panel-content { + padding: 0.75rem 1rem; + font-size: 1rem; +} +#enhancer--panel-header-title { + padding: 0 0.5em 0.1em 1rem; +} +#enhancer--panel-header-switcher { + padding: 4px; + margin: 0.75rem 0; +} #enhancer--panel-header-toggle { margin-left: auto; + padding-right: 1rem; + height: 100%; + width: 2.5em; + opacity: 0; + display: flex; } -#enhancer--panel-header-toggle, -#enhancer--panel-header-switcher { +#enhancer--panel-header-toggle > div { + margin: auto 0 auto auto; +} +#enhancer--panel-header-switcher, +#enhancer--panel-header-toggle > div { + color: var(--theme--icon_secondary); height: 1em; width: 1em; cursor: pointer; - opacity: 0; - transition: 300ms ease-in-out; display: flex; flex-direction: column; + border-radius: 3px; + transition: 300ms ease-in-out; } -#enhancer--panel-header-switcher svg { - width: 0.5em; - height: 0.5em; - display: block; - margin: auto; +#enhancer--panel-header-switcher:hover, +#enhancer--panel-header-switcher:focus, +#enhancer--panel-header-toggle > div:hover, +#enhancer--panel-header-toggle > div:focus { + background: var(--theme--ui_interactive-hover); } - -#enhancer--panel:not([data-enhancer-panel-pinned]) #enhancer--panel-header-toggle { +#enhancer--panel #enhancer--panel-header-toggle svg { + transition: 300ms ease-in-out; +} +#enhancer--panel:not([data-enhancer-panel-pinned]) #enhancer--panel-header-toggle svg { transform: rotateZ(-180deg); } -#enhancer--panel:hover #enhancer--panel-header-toggle, -#enhancer--panel:hover #enhancer--panel-header-switcher { +#enhancer--panel:hover #enhancer--panel-header-toggle { opacity: 1; } @@ -149,3 +169,39 @@ #enhancer--panel[data-enhancer-panel-pinned] #enhancer--panel-resize:hover div { background: var(--theme--ui_divider); } + +#enhancer--panel-switcher-overlay-container { + position: fixed; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 999; + overflow: hidden; +} +#enhancer--panel-switcher { + max-width: 320px; + position: relative; + right: 14px; + border-radius: 3px; + padding: 8px 0; + background: var(--theme--bg_popup); + 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; + overflow: hidden; +} +.enhancer--panel-switcher-item { + display: flex; + align-items: center; + width: 100%; + padding: 8px 14px; + user-select: none; + cursor: pointer; + overflow: hidden; + transition: background 300ms ease; +} +.enhancer--panel-switcher-item:hover, +.enhancer--panel-switcher-item:focus { + background: var(--theme--ui_interactive-hover); +} diff --git a/extension/api/components/panel.mjs b/extension/api/components/panel.mjs index b9f44e1..f17e709 100644 --- a/extension/api/components/panel.mjs +++ b/extension/api/components/panel.mjs @@ -17,6 +17,15 @@ const db = await registry.db('36a2ffc9-27ff-480e-84a7-c7700a7d232d'); let $panel, _views = []; +const svgExpand = web.raw` + +`; + export const panel = async (icon, title, generator = () => {}) => { _views.push({ icon: web.html`${icon}`, @@ -31,44 +40,31 @@ export const panel = async (icon, title, generator = () => {}) => { await web.whenReady([notionRightSidebarSelector]); web.loadStylesheet('api/components/panel.css'); - const $title = web.html`
`, - $header = web.render(web.html`
`, $title), - $content = web.html`
`; - // opening/closing const $notionFrame = document.querySelector('.notion-frame'), $notionRightSidebar = document.querySelector(notionRightSidebarSelector), - $pinnedToggle = web.html`
+ $pinnedToggle = web.html`
${await components.feather('chevrons-right')} -
`, +
`, $hoverTrigger = web.html`
`, 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]; + const $elems = [$notionRightSidebar, $notionFrame, $hoverTrigger, $panel]; if (isPinned()) { - if (isRightSidebarOpen()) $elems.push($notionFrame); + closeSwitcher(); 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); + $pinnedToggle.addEventListener('click', (event) => { + event.stopPropagation(); + togglePanel(); + }); // resizing let dragStartX, @@ -120,19 +116,90 @@ export const panel = async (icon, title, generator = () => {}) => { }); // view selection - const $switcherTrigger = web.html`
- ${await components.feather('chevron-up')} - ${await components.feather('chevron-down')} + const $title = web.html`
`, + $header = web.render(web.html`
`, $title), + $content = web.html`
`, + $switcherTrigger = web.html`
+ ${svgExpand}
`, + $notionApp = document.querySelector('.notion-app-inner'), + $switcherOverlayContainer = web.html`
`, + $switcher = web.html`
`, + isSwitcherOpen = () => document.body.contains($switcher), renderView = (view) => { - web.render(web.empty($title), web.render(web.html`

`, view.icon, view.title)); + web.render( + web.empty($title), + web.render( + web.html`

`, + view.icon, + view.title + ) + ); web.render(web.empty($content), view.$elem); + }, + openSwitcher = () => { + if (!isPinned()) return togglePanel(); + web.render($notionApp, $switcherOverlayContainer); + web.empty($switcher); + for (const view of _views) { + const $item = web.render( + web.html`
`, + web.render( + web.html`

`, + view.icon.cloneNode(true), + view.title.cloneNode(true) + ) + ); + $item.addEventListener('click', () => renderView(view)); + web.render($switcher, $item); + } + const rect = $header.getBoundingClientRect(); + web.render( + web.empty($switcherOverlayContainer), + web.render( + web.html`
`, + web.render( + web.html`
`, + $switcher + ) + ) + ); + $switcher.firstElementChild.focus(); + $switcher.animate([{ opacity: 0 }, { opacity: 1 }], { duration: 200 }); }; + function closeSwitcher() { + $switcher.animate([{ opacity: 1 }, { opacity: 0 }], { duration: 200 }).onfinish = () => + $switcherOverlayContainer.remove(); + } + web.addHotkeyListener(['Escape'], () => { + if (isSwitcherOpen()) closeSwitcher(); + }); + web.addHotkeyListener(['Enter'], () => { + if (isSwitcherOpen()) document.activeElement.click(); + }); + document.addEventListener('keydown', (event) => { + if (isSwitcherOpen()) { + if (event.key === 'ArrowUp') { + const $prev = event.target.previousElementSibling; + ($prev || event.target.parentElement.lastElementChild).focus(); + event.stopPropagation(); + } + if (event.key === 'ArrowDown') { + const $next = event.target.nextElementSibling; + ($next || event.target.parentElement.firstElementChild).focus(); + event.stopPropagation(); + } + } + }); + $header.addEventListener('click', openSwitcher); + $switcherTrigger.addEventListener('click', openSwitcher); + $switcherOverlayContainer.addEventListener('click', closeSwitcher); renderView(_views[0]); web.render( $panel, - web.render($header, $switcherTrigger, $title, $pinnedToggle), + web.render($header, $title, $switcherTrigger, $pinnedToggle), $content, $resizeHandle ); diff --git a/extension/api/web.mjs b/extension/api/web.mjs index d215c49..d7ae5d1 100644 --- a/extension/api/web.mjs +++ b/extension/api/web.mjs @@ -11,7 +11,12 @@ * @module notion-enhancer/api/web */ -import { fs, fmt } from './_.mjs'; +import { fs } from './_.mjs'; + +let _hotkeyEventListeners = [], + _documentObserver, + _documentObserverListeners = [], + _documentObserverEvents = []; import '../dep/jscolor.min.js'; /** color picker with alpha channel using https://jscolor.com/ */ @@ -138,27 +143,26 @@ export const loadStylesheet = (path) => { return true; }; -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 = []; +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(event); + } +}); /** * register a hotkey listener to the page @@ -181,39 +185,40 @@ 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} [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 1a0d46c..8ff7f1a 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 @@ -34,4 +34,7 @@ export default async function (api, db) { components.panel(await components.feather('sidebar'), 'Test Panel', ($panel) => { return web.html`

test

`; }); + components.panel(await components.feather('users'), 'Other Panel', ($panel) => { + return web.html`

yay

`; + }); }