refactor: move state.mjs into markup.mjs for use in notion interface

This commit is contained in:
dragonwocky 2023-08-06 23:45:56 +10:00
parent bb5853e866
commit 45e0be7d62
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
21 changed files with 284 additions and 248 deletions

View File

@ -8,6 +8,7 @@ import { checkForUpdate } from "./updateCheck.mjs";
import { sendTelemetryPing } from "./sendTelemetry.mjs"; import { sendTelemetryPing } from "./sendTelemetry.mjs";
import { Modal, Frame } from "./islands/Modal.mjs"; import { Modal, Frame } from "./islands/Modal.mjs";
import { MenuButton } from "./islands/MenuButton.mjs"; import { MenuButton } from "./islands/MenuButton.mjs";
import { TopbarButton } from "./islands/TopbarButton.mjs";
import { Panel } from "./islands/Panel.mjs"; import { Panel } from "./islands/Panel.mjs";
const shouldLoadThemeOverrides = async (db) => { const shouldLoadThemeOverrides = async (db) => {
@ -118,10 +119,11 @@ const insertMenu = async (db) => {
}); });
}; };
const insertPanel = async (db) => { const insertPanel = async (api, db) => {
const notionFrame = ".notion-frame", const notionFrame = ".notion-frame",
{ html, addKeyListener, addMutationListener } = globalThis.__enhancerApi, notionTopbarBtn = ".notion-topbar-more-button",
togglePanelHotkey = await db.get("togglePanelHotkey"); togglePanelHotkey = await db.get("togglePanelHotkey"),
{ html } = api;
const $panel = html`<${Panel} const $panel = html`<${Panel}
_getWidth=${() => db.get("sidePanelWidth")} _getWidth=${() => db.get("sidePanelWidth")}
@ -129,28 +131,45 @@ const insertPanel = async (db) => {
_getOpen=${() => db.get("sidePanelOpen")} _getOpen=${() => db.get("sidePanelOpen")}
_setOpen=${(open) => db.set("sidePanelOpen", open)} _setOpen=${(open) => db.set("sidePanelOpen", open)}
/>`, />`,
togglePanel = () => {
if ($panel.hasAttribute("open")) $panel.close();
else $panel.open();
};
const $panelTopbarBtn = html`<${TopbarButton}
aria-label="Open side panel"
icon="panel-right"
onclick=${togglePanel}
/>`,
appendToDom = () => { appendToDom = () => {
const $frame = document.querySelector(notionFrame); const $frame = document.querySelector(notionFrame);
if (!$frame) return; if (!$frame) return;
if (!$frame.contains($panel)) $frame.append($panel); if (!$frame.contains($panel)) $frame.append($panel);
if (!$frame.style.flexDirection !== "row") if (!$frame.style.flexDirection !== "row")
$frame.style.flexDirection = "row"; $frame.style.flexDirection = "row";
if (!document.contains($panelTopbarBtn)) {
const $notionTopbarBtn = document.querySelector(notionTopbarBtn);
$notionTopbarBtn?.before($panelTopbarBtn);
}
}; };
addMutationListener(notionFrame, appendToDom); api.addMutationListener(`${notionFrame}, ${notionTopbarBtn}`, appendToDom);
api.useState(["panelOpen"], ([panelOpen]) => {
if (panelOpen) $panelTopbarBtn.setAttribute("data-active", true);
else $panelTopbarBtn.removeAttribute("data-active");
});
appendToDom(); appendToDom();
addKeyListener(togglePanelHotkey, (event) => { api.addKeyListener(togglePanelHotkey, (event) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if ($panel.hasAttribute("open")) $panel.close(); togglePanel();
else $panel.open();
}); });
}; };
export default async (api, db) => export default async (api, db) =>
Promise.all([ Promise.all([
insertMenu(db), insertMenu(db),
insertPanel(db), insertPanel(api, db),
insertCustomStyles(db), insertCustomStyles(db),
loadThemeOverrides(db), loadThemeOverrides(db),
sendTelemetryPing(), sendTelemetryPing(),

View File

@ -4,17 +4,6 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
// <div class="absolute top-0">
// <svg
// role="graphics-symbol"
// viewBox="0 0 16 16"
// class="doubleChevronRight"
// style="width: 16px; height: 16px; display: block; fill: rgba(255, 255, 255, 0.443); flex-shrink: 0;"
// >
// <path d="M2.25781 14.1211C2.47656 14.1211 2.66797 14.0391 2.81836 13.8887L8.14355 8.67969C8.32812 8.49512 8.41699 8.29688 8.41699 8.06445C8.41699 7.8252 8.32812 7.62012 8.14355 7.44922L2.81836 2.24023C2.66797 2.08984 2.4834 2.00781 2.25781 2.00781C1.81348 2.00781 1.46484 2.35645 1.46484 2.80078C1.46484 3.0127 1.55371 3.21777 1.7041 3.375L6.50977 8.05762L1.7041 12.7539C1.55371 12.9043 1.46484 13.1094 1.46484 13.3281C1.46484 13.7725 1.81348 14.1211 2.25781 14.1211ZM8.36914 14.1211C8.58789 14.1211 8.77246 14.0391 8.92285 13.8887L14.2549 8.67969C14.4395 8.49512 14.5283 8.29688 14.5283 8.06445C14.5283 7.8252 14.4326 7.62012 14.2549 7.44922L8.92285 2.24023C8.77246 2.08984 8.58789 2.00781 8.36914 2.00781C7.9248 2.00781 7.56934 2.35645 7.56934 2.80078C7.56934 3.0127 7.66504 3.21777 7.81543 3.375L12.6211 8.05762L7.81543 12.7539C7.66504 12.9043 7.56934 13.1094 7.56934 13.3281C7.56934 13.7725 7.9248 14.1211 8.36914 14.1211Z"></path>
// </svg>
// </div>;
import { Tooltip } from "./Tooltip.mjs"; import { Tooltip } from "./Tooltip.mjs";
function PanelView(props) { function PanelView(props) {
@ -36,9 +25,8 @@ function Panel({
maxWidth = 640, maxWidth = 640,
...props ...props
}) { }) {
const { html, extendProps } = globalThis.__enhancerApi, const { html, ...api } = globalThis.__enhancerApi;
{ addMutationListener, removeMutationListener } = globalThis.__enhancerApi; api.extendProps(props, {
extendProps(props, {
class: `notion-enhancer--side-panel order-2 shrink-0 class: `notion-enhancer--side-panel order-2 shrink-0
transition-[width] open:w-[var(--side\\_panel--width)] transition-[width] open:w-[var(--side\\_panel--width)]
w-0 border-l-1 border-[color:var(--theme--fg-border)] w-0 border-l-1 border-[color:var(--theme--fg-border)]
@ -65,21 +53,22 @@ function Panel({
d="M2.25781 14.1211C2.47656 14.1211 2.66797 14.0391 2.81836 13.8887L8.14355 8.67969C8.32812 8.49512 8.41699 8.29688 8.41699 8.06445C8.41699 7.8252 8.32812 7.62012 8.14355 7.44922L2.81836 2.24023C2.66797 2.08984 2.4834 2.00781 2.25781 2.00781C1.81348 2.00781 1.46484 2.35645 1.46484 2.80078C1.46484 3.0127 1.55371 3.21777 1.7041 3.375L6.50977 8.05762L1.7041 12.7539C1.55371 12.9043 1.46484 13.1094 1.46484 13.3281C1.46484 13.7725 1.81348 14.1211 2.25781 14.1211ZM8.36914 14.1211C8.58789 14.1211 8.77246 14.0391 8.92285 13.8887L14.2549 8.67969C14.4395 8.49512 14.5283 8.29688 14.5283 8.06445C14.5283 7.8252 14.4326 7.62012 14.2549 7.44922L8.92285 2.24023C8.77246 2.08984 8.58789 2.00781 8.36914 2.00781C7.9248 2.00781 7.56934 2.35645 7.56934 2.80078C7.56934 3.0127 7.66504 3.21777 7.81543 3.375L12.6211 8.05762L7.81543 12.7539C7.66504 12.9043 7.56934 13.1094 7.56934 13.3281C7.56934 13.7725 7.9248 14.1211 8.36914 14.1211Z" d="M2.25781 14.1211C2.47656 14.1211 2.66797 14.0391 2.81836 13.8887L8.14355 8.67969C8.32812 8.49512 8.41699 8.29688 8.41699 8.06445C8.41699 7.8252 8.32812 7.62012 8.14355 7.44922L2.81836 2.24023C2.66797 2.08984 2.4834 2.00781 2.25781 2.00781C1.81348 2.00781 1.46484 2.35645 1.46484 2.80078C1.46484 3.0127 1.55371 3.21777 1.7041 3.375L6.50977 8.05762L1.7041 12.7539C1.55371 12.9043 1.46484 13.1094 1.46484 13.3281C1.46484 13.7725 1.81348 14.1211 2.25781 14.1211ZM8.36914 14.1211C8.58789 14.1211 8.77246 14.0391 8.92285 13.8887L14.2549 8.67969C14.4395 8.49512 14.5283 8.29688 14.5283 8.06445C14.5283 7.8252 14.4326 7.62012 14.2549 7.44922L8.92285 2.24023C8.77246 2.08984 8.58789 2.00781 8.36914 2.00781C7.9248 2.00781 7.56934 2.35645 7.56934 2.80078C7.56934 3.0127 7.66504 3.21777 7.81543 3.375L12.6211 8.05762L7.81543 12.7539C7.66504 12.9043 7.56934 13.1094 7.56934 13.3281C7.56934 13.7725 7.9248 14.1211 8.36914 14.1211Z"
></path> ></path>
</svg> </svg>
</div>`, </div>`;
$panel = html`<aside ...${props}>
${$resizeHandle} const $panel = html`<aside ...${props}>
<div ${$resizeHandle}
class="flex justify-between items-center <div
class="flex justify-between items-center
border-(b [color:var(--theme--fg-border)])" border-(b [color:var(--theme--fg-border)])"
>
<div
style="font-size: 14px; color: rgba(255, 255, 255, 0.81); font-weight: 500; display: flex; align-items: center; padding: 12px 12px 12px 16px;"
> >
<div Comments
style="font-size: 14px; color: rgba(255, 255, 255, 0.81); font-weight: 500; display: flex; align-items: center; padding: 12px 12px 12px 16px;"
>
Comments
</div>
${$chevronClose}
</div> </div>
</aside>`; ${$chevronClose}
</div>
</aside>`;
let preDragWidth, let preDragWidth,
userDragActive, userDragActive,
@ -139,33 +128,9 @@ function Panel({
options = { duration: 150, easing: "cubic-bezier(0.4, 0, 0.2, 1)" }; options = { duration: 150, easing: "cubic-bezier(0.4, 0, 0.2, 1)" };
$notionHelp.style.setProperty("right", destination); $notionHelp.style.setProperty("right", destination);
$notionHelp.animate(keyframes, options); $notionHelp.animate(keyframes, options);
removeMutationListener(repositionHelp); api.removeMutationListener(repositionHelp);
}; };
addMutationListener(notionHelp, repositionHelp); api.addMutationListener(notionHelp, repositionHelp);
const notionTopbarMore = ".notion-topbar-more-button",
$topbarPanelButton = html`<button
aria-label="Open side panel"
class="user-select-none h-[28px] w-[33px] duration-[20ms]
transition inline-flex items-center justify-center mr-[2px]
rounded-[3px] hover:bg-[color:var(--theme--bg-hover)]
[data-active]:bg-[color:var(--theme--bg-hover)]"
>
<i
class="i-panel-right w-[20px] h-[20px] fill-[color:var(--theme--fg-secondary)]"
/>
</button>`,
appendToDom = () => {
if (document.contains($topbarPanelButton)) return;
const $notionTopbarMore = document.querySelector(notionTopbarMore);
$notionTopbarMore?.before($topbarPanelButton);
};
$topbarPanelButton.addEventListener("click", () => {
if ($panel.hasAttribute("open")) $panel.close();
else $panel.open();
});
addMutationListener(notionTopbarMore, appendToDom);
appendToDom();
$panel.resize = async (width) => { $panel.resize = async (width) => {
$tooltip.hide(); $tooltip.hide();
@ -181,7 +146,7 @@ function Panel({
$panel.open = () => { $panel.open = () => {
$panel.setAttribute("open", true); $panel.setAttribute("open", true);
$panel.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = 0)); $panel.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = 0));
$topbarPanelButton.setAttribute("data-active", true); api.setState({ panelOpen: true });
$panel.onopen?.(); $panel.onopen?.();
_setOpen(true); _setOpen(true);
$panel.resize(); $panel.resize();
@ -191,8 +156,8 @@ function Panel({
$panel.onbeforeclose?.(); $panel.onbeforeclose?.();
$panel.removeAttribute("open"); $panel.removeAttribute("open");
$panel.style.pointerEvents = "auto"; $panel.style.pointerEvents = "auto";
$topbarPanelButton.removeAttribute("data-active");
$panel.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1)); $panel.querySelectorAll("[tabindex]").forEach(($el) => ($el.tabIndex = -1));
api.setState({ panelOpen: false });
repositionHelp(); repositionHelp();
_setOpen(false); _setOpen(false);
setTimeout(() => { setTimeout(() => {

View File

@ -0,0 +1,26 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
function TopbarButton({ icon, ...props }, ...children) {
const { html, extendProps } = globalThis.__enhancerApi;
extendProps(props, {
tabindex: 0,
role: "button",
class: `notion-enhancer--topbar-button
user-select-none h-[28px] w-[33px] duration-[20ms]
transition inline-flex items-center justify-center mr-[2px]
rounded-[3px] hover:bg-[color:var(--theme--bg-hover)]
[data-active]:bg-[color:var(--theme--bg-hover)]`,
});
return html`<button ...${props}>
<i
class="i-${icon} w-[20px] h-[20px]
fill-[color:var(--theme--fg-secondary)]"
/>
</button>`;
}
export { TopbarButton };

View File

@ -7,7 +7,6 @@
import { Popup } from "./Popup.mjs"; import { Popup } from "./Popup.mjs";
import { Button } from "./Button.mjs"; import { Button } from "./Button.mjs";
import { Description } from "./Description.mjs"; import { Description } from "./Description.mjs";
import { useState } from "../state.mjs";
const updateGuide = const updateGuide =
"https://notion-enhancer.github.io/getting-started/updating/", "https://notion-enhancer.github.io/getting-started/updating/",
@ -69,7 +68,7 @@ function Circle(rect) {
} }
function Banner({ updateAvailable, isDevelopmentBuild }) { function Banner({ updateAvailable, isDevelopmentBuild }) {
const { html, version, initDatabase } = globalThis.__enhancerApi, const { html, version, initDatabase, useState } = globalThis.__enhancerApi,
$version = html`<button $version = html`<button
class="text-[12px] py-[2px] px-[6px] mt-[2px] class="text-[12px] py-[2px] px-[6px] mt-[2px]
font-medium leading-tight tracking-wide rounded-[3px] font-medium leading-tight tracking-wide rounded-[3px]

View File

@ -4,11 +4,10 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "../state.mjs";
function Checkbox({ _get, _set, _requireReload = true, ...props }) { function Checkbox({ _get, _set, _requireReload = true, ...props }) {
let _initialValue; let _initialValue;
const { html, extendProps } = globalThis.__enhancerApi, const { html, extendProps } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
$input = html`<input $input = html`<input
type="checkbox" type="checkbox"
class="hidden checked:sibling:(px-px class="hidden checked:sibling:(px-px

View File

@ -4,11 +4,11 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "../state.mjs";
import { Button } from "./Button.mjs"; import { Button } from "./Button.mjs";
function Footer({ categories }) { function Footer({ categories }) {
const { html, reloadApp } = globalThis.__enhancerApi, const { html, reloadApp } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
$reload = html`<${Button} $reload = html`<${Button}
class="ml-auto" class="ml-auto"
variant="primary" variant="primary"

View File

@ -4,8 +4,6 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "../state.mjs";
const updateHotkey = (event) => { const updateHotkey = (event) => {
const keys = []; const keys = [];
for (const modifier of ["metaKey", "ctrlKey", "altKey", "shiftKey"]) { for (const modifier of ["metaKey", "ctrlKey", "altKey", "shiftKey"]) {
@ -76,7 +74,8 @@ function Input({
...props ...props
}) { }) {
let $filename, $clear; let $filename, $clear;
const { html, extendProps } = globalThis.__enhancerApi; const { html, extendProps } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi;
Coloris({ format: "rgb" }); Coloris({ format: "rgb" });
type ??= "text"; type ??= "text";

View File

@ -4,7 +4,6 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState } from "../state.mjs";
import { Description } from "./Description.mjs"; import { Description } from "./Description.mjs";
import { Input } from "./Input.mjs"; import { Input } from "./Input.mjs";
import { Mod } from "./Mod.mjs"; import { Mod } from "./Mod.mjs";
@ -38,7 +37,7 @@ function Search({ items, itemType }) {
} }
function List({ id, mods, description }) { function List({ id, mods, description }) {
const { html, isEnabled, setEnabled } = globalThis.__enhancerApi, const { html, isEnabled, setEnabled, setState } = globalThis.__enhancerApi,
$mods = mods.map((mod) => { $mods = mods.map((mod) => {
const _get = () => isEnabled(mod.id), const _get = () => isEnabled(mod.id),
_set = async (enabled) => { _set = async (enabled) => {

View File

@ -4,7 +4,6 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState } from "../state.mjs";
import { Description } from "./Description.mjs"; import { Description } from "./Description.mjs";
import { Toggle } from "./Toggle.mjs"; import { Toggle } from "./Toggle.mjs";
@ -21,7 +20,7 @@ function Mod({
_set, _set,
_src, _src,
}) { }) {
const { html, enhancerUrl } = globalThis.__enhancerApi, const { html, enhancerUrl, setState } = globalThis.__enhancerApi,
toggleId = Math.random().toString(36).slice(2, 5); toggleId = Math.random().toString(36).slice(2, 5);
return html`<label return html`<label

View File

@ -9,13 +9,13 @@ import { Description } from "./Description.mjs";
import { Checkbox } from "./Checkbox.mjs"; import { Checkbox } from "./Checkbox.mjs";
import { Button } from "./Button.mjs"; import { Button } from "./Button.mjs";
import { Tile } from "./Tile.mjs"; import { Tile } from "./Tile.mjs";
import { setState, useState } from "../state.mjs";
const privacyPolicy = "https://notion-enhancer.github.io/about/privacy-policy/", const privacyPolicy = "https://notion-enhancer.github.io/about/privacy-policy/",
tsAndCs = "https://notion-enhancer.github.io/about/terms-and-conditions/"; tsAndCs = "https://notion-enhancer.github.io/about/terms-and-conditions/";
function Onboarding() { function Onboarding() {
const { html, version, initDatabase } = globalThis.__enhancerApi, const { html, version, initDatabase } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
$submitAgreement = html`<${Button} $submitAgreement = html`<${Button}
icon="arrow-right" icon="arrow-right"
class="ml-auto" class="ml-auto"

View File

@ -4,7 +4,6 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState } from "../state.mjs";
import { Heading } from "./Heading.mjs"; import { Heading } from "./Heading.mjs";
import { Description } from "./Description.mjs"; import { Description } from "./Description.mjs";
import { Input } from "./Input.mjs"; import { Input } from "./Input.mjs";
@ -78,7 +77,7 @@ function Option({ _get, _set, ...opt }) {
} }
function Options({ mod }) { function Options({ mod }) {
const { html, modDatabase } = globalThis.__enhancerApi; const { html, modDatabase, setState } = globalThis.__enhancerApi;
return filterOptionsForRender(mod.options).map((opt) => { return filterOptionsForRender(mod.options).map((opt) => {
opt.label ??= camelToSentenceCase(opt.key); opt.label ??= camelToSentenceCase(opt.key);
if (opt.type === "heading") return html`<${Heading}>${opt.label}<//>`; if (opt.type === "heading") return html`<${Heading}>${opt.label}<//>`;

View File

@ -4,10 +4,9 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "../state.mjs";
function Popup({ trigger, ...props }, ...children) { function Popup({ trigger, ...props }, ...children) {
const { html, extendProps } = globalThis.__enhancerApi; const { html, extendProps } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi;
extendProps(props, { extendProps(props, {
class: `notion-enhancer--menu-popup class: `notion-enhancer--menu-popup
group absolute top-0 left-0 w-full h-full group absolute top-0 left-0 w-full h-full

View File

@ -4,7 +4,6 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "../state.mjs";
import { Heading } from "./Heading.mjs"; import { Heading } from "./Heading.mjs";
import { Description } from "./Description.mjs"; import { Description } from "./Description.mjs";
import { Checkbox } from "./Checkbox.mjs"; import { Checkbox } from "./Checkbox.mjs";
@ -14,6 +13,7 @@ import { Popup } from "./Popup.mjs";
function Profile({ id }) { function Profile({ id }) {
const { html, getProfile, initDatabase } = globalThis.__enhancerApi, const { html, getProfile, initDatabase } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
profile = initDatabase([id]), profile = initDatabase([id]),
db = initDatabase(); db = initDatabase();
@ -180,6 +180,7 @@ function Profile({ id }) {
function Profiles() { function Profiles() {
const { html, initDatabase } = globalThis.__enhancerApi, const { html, initDatabase } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
$input = html`<${Input} icon="file-cog" />`, $input = html`<${Input} icon="file-cog" />`,
$list = html`<ul></ul>`; $list = html`<ul></ul>`;

View File

@ -4,11 +4,11 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "../state.mjs";
import { Popup } from "./Popup.mjs"; import { Popup } from "./Popup.mjs";
function Option({ value, _get, _set }) { function Option({ value, _get, _set }) {
const { html } = globalThis.__enhancerApi, const { html } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
$selected = html`<i class="ml-auto i-check w-[16px] h-[16px]"></i>`, $selected = html`<i class="ml-auto i-check w-[16px] h-[16px]"></i>`,
$option = html`<div $option = html`<div
tabindex="0" tabindex="0"
@ -36,6 +36,7 @@ function Option({ value, _get, _set }) {
function Select({ values, _get, _set, _requireReload = true, ...props }) { function Select({ values, _get, _set, _requireReload = true, ...props }) {
let _initialValue; let _initialValue;
const { html } = globalThis.__enhancerApi, const { html } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
// dir="rtl" overflows to the left during transition // dir="rtl" overflows to the left during transition
$select = html`<div $select = html`<div
dir="rtl" dir="rtl"

View File

@ -4,11 +4,11 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "../state.mjs";
import { Description } from "./Description.mjs"; import { Description } from "./Description.mjs";
function SidebarHeading({}, ...children) { function SidebarHeading({}, ...children) {
const { html } = globalThis.__enhancerApi; const { html } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi;
return html`<h2 return html`<h2
class="flex items-center font-semibold leading-none class="flex items-center font-semibold leading-none
text-([12px] [color:var(--theme--fg-secondary)]) text-([12px] [color:var(--theme--fg-secondary)])
@ -20,6 +20,7 @@ function SidebarHeading({}, ...children) {
function SidebarButton({ id, icon, ...props }, ...children) { function SidebarButton({ id, icon, ...props }, ...children) {
const { html, extendProps } = globalThis.__enhancerApi, const { html, extendProps } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
$btn = html`<${props["href"] ? "a" : "button"} $btn = html`<${props["href"] ? "a" : "button"}
class="flex items-center select-none text-[14px] class="flex items-center select-none text-[14px]
min-h-[27px] px-[12px] my-px last:mb-[12px] w-full min-h-[27px] px-[12px] my-px last:mb-[12px] w-full
@ -53,6 +54,7 @@ function SidebarButton({ id, icon, ...props }, ...children) {
function Sidebar({ items, categories }) { function Sidebar({ items, categories }) {
const { html, version } = globalThis.__enhancerApi, const { html, version } = globalThis.__enhancerApi,
{ initDatabase, isEnabled } = globalThis.__enhancerApi, { initDatabase, isEnabled } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
$agreeToUnlock = html`<span $agreeToUnlock = html`<span
class="pt-[2px] pb-[5px] px-[15px] text-[12px] class="pt-[2px] pb-[5px] px-[15px] text-[12px]
inline-block text-[color:var(--theme--fg-red)]" inline-block text-[color:var(--theme--fg-red)]"

View File

@ -5,12 +5,12 @@
*/ */
import { collectTelemetryData } from "../../sendTelemetry.mjs"; import { collectTelemetryData } from "../../sendTelemetry.mjs";
import { useState, setState } from "../state.mjs";
import { Option } from "./Options.mjs"; import { Option } from "./Options.mjs";
const privacyPolicy = "https://notion-enhancer.github.io/about/privacy-policy/"; const privacyPolicy = "https://notion-enhancer.github.io/about/privacy-policy/";
function Telemetry() { function Telemetry() {
const { html, initDatabase } = globalThis.__enhancerApi, const { html, initDatabase } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
_get = async () => { _get = async () => {
// defaults to true, must be explicitly set to false to disable // defaults to true, must be explicitly set to false to disable
return (await initDatabase().get("telemetryEnabled")) ?? true; return (await initDatabase().get("telemetryEnabled")) ?? true;

View File

@ -4,11 +4,10 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "../state.mjs";
function Toggle({ _get, _set, _requireReload = true, ...props }) { function Toggle({ _get, _set, _requireReload = true, ...props }) {
let _initialValue; let _initialValue;
const { html, extendProps } = globalThis.__enhancerApi, const { html, extendProps } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
$input = html`<input $input = html`<input
type="checkbox" type="checkbox"
class="hidden checked:sibling:children:( class="hidden checked:sibling:children:(

View File

@ -4,10 +4,9 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "../state.mjs";
function View({ id }, ...children) { function View({ id }, ...children) {
const { html } = globalThis.__enhancerApi, const { html } = globalThis.__enhancerApi,
{ setState, useState } = globalThis.__enhancerApi,
// set padding on last child to maintain pad on overflow // set padding on last child to maintain pad on overflow
$view = html`<article $view = html`<article
id=${id} id=${id}

View File

@ -4,7 +4,6 @@
* (https://notion-enhancer.github.io/) under the MIT license * (https://notion-enhancer.github.io/) under the MIT license
*/ */
import { setState, useState } from "./state.mjs";
import { checkForUpdate, isDevelopmentBuild } from "../updateCheck.mjs"; import { checkForUpdate, isDevelopmentBuild } from "../updateCheck.mjs";
import { Sidebar } from "./islands/Sidebar.mjs"; import { Sidebar } from "./islands/Sidebar.mjs";
import { Footer } from "./islands/Footer.mjs"; import { Footer } from "./islands/Footer.mjs";
@ -17,6 +16,11 @@ import { Mod } from "./islands/Mod.mjs";
import { Options } from "./islands/Options.mjs"; import { Options } from "./islands/Options.mjs";
import { Profiles } from "./islands/Profiles.mjs"; import { Profiles } from "./islands/Profiles.mjs";
let _apiImported = false,
_renderStarted = false,
_stateHookedInto = false,
_hotkeyRegistered = false;
const categories = [ const categories = [
{ {
icon: "palette", icon: "palette",
@ -85,118 +89,126 @@ const categories = [
})), })),
]; ];
const render = async () => { const renderMenu = async () => {
const { html, getMods } = globalThis.__enhancerApi, const { html, ...api } = globalThis.__enhancerApi,
{ isEnabled, setEnabled } = globalThis.__enhancerApi, [theme, icon] = api.useState(["theme", "icon"]);
[icon, renderStarted] = useState(["icon", "renderStarted"]); if (!theme || !icon || _renderStarted) return;
if (!html || !getMods || !icon || renderStarted) return; if (icon === "Monochrome") sidebar[1].icon += "?mask";
if (icon === "Monochrome") sidebar[1].icon += "?mask"; _renderStarted = true;
setState({ renderStarted: true });
const mods = await getMods(); const mods = await api.getMods();
for (let i = 0; i < categories.length; i++) { for (let i = 0; i < categories.length; i++) {
const { id } = categories[i]; const { id } = categories[i];
categories[i].mods = mods.filter(({ _src }) => _src.startsWith(`${id}/`)); categories[i].mods = mods.filter(({ _src }) => _src.startsWith(`${id}/`));
categories[i].view = html`<${View} id=${id}> categories[i].view = html`<${View} id=${id}>
<${List} ...${categories[i]} /> <${List} ...${categories[i]} />
<//>`; <//>`;
} }
for (let i = 0; i < mods.length; i++) { for (let i = 0; i < mods.length; i++) {
const options = mods[i].options?.filter((opt) => opt.type !== "heading"); const options = mods[i].options?.filter((opt) => opt.type !== "heading");
if (mods[i]._src === "core" || !options?.length) continue; if (mods[i]._src === "core" || !options?.length) continue;
const _get = () => isEnabled(mods[i].id), const _get = () => api.isEnabled(mods[i].id),
_set = async (enabled) => { _set = async (enabled) => {
await setEnabled(mods[i].id, enabled); await api.setEnabled(mods[i].id, enabled);
setState({ rerender: true }); api.setState({ rerender: true });
}; };
mods[i].view = html`<${View} id=${mods[i].id}> mods[i].view = html`<${View} id=${mods[i].id}>
<!-- passing an empty options array hides the settings button --> <!-- passing an empty options array hides the settings button -->
<${Mod} ...${{ ...mods[i], options: [], _get, _set }} /> <${Mod} ...${{ ...mods[i], options: [], _get, _set }} />
<${Options} mod=${mods[i]} /> <${Options} mod=${mods[i]} />
<//>`; <//>`;
} }
const $sidebar = html`<${Sidebar} const $sidebar = html`<${Sidebar}
items=${sidebar} items=${sidebar}
categories=${categories} categories=${categories}
/>`, />`,
$main = html` $main = html`
<main class="flex-(& col) overflow-hidden transition-[height]"> <main class="flex-(& col) overflow-hidden transition-[height]">
<!-- wrappers necessary for transitions and breakpoints --> <!-- wrappers necessary for transitions and breakpoints -->
<div class="grow overflow-auto"> <div class="grow overflow-auto">
<div class="relative h-full w-full"> <div class="relative h-full w-full">
<${View} id="welcome"> <${View} id="welcome">
<${Banner} <${Banner}
updateAvailable=${await checkForUpdate()} updateAvailable=${await checkForUpdate()}
isDevelopmentBuild=${await isDevelopmentBuild()} isDevelopmentBuild=${await isDevelopmentBuild()}
/> />
<${Onboarding} /> <${Onboarding} />
<//> <//>
<${View} id="core"> <${View} id="core">
<${Options} mod=${mods.find(({ _src }) => _src === "core")} /> <${Options} mod=${mods.find(({ _src }) => _src === "core")} />
<${Telemetry} /> <${Telemetry} />
<${Profiles} /> <${Profiles} />
<//> <//>
${[...categories, ...mods] ${[...categories, ...mods]
.filter(({ view }) => view) .filter(({ view }) => view)
.map(({ view }) => view)} .map(({ view }) => view)}
</div>
</div> </div>
</div> <${Footer} categories=${categories} />
<${Footer} categories=${categories} /> </main>
</main> `;
`; api.useState(["footerOpen"], ([footerOpen]) => {
useState(["footerOpen"], ([footerOpen]) => { $main.style.height = footerOpen ? "100%" : "calc(100% + 33px)";
$main.style.height = footerOpen ? "100%" : "calc(100% + 33px)"; });
}); api.useState(["transitionInProgress"], ([transitionInProgress]) => {
useState(["transitionInProgress"], ([transitionInProgress]) => { $main.children[0].style.overflow = transitionInProgress ? "hidden" : "";
$main.children[0].style.overflow = transitionInProgress ? "hidden" : ""; });
});
const $skeleton = document.querySelector("#skeleton"); const $skeleton = document.querySelector("#skeleton");
$skeleton.replaceWith($sidebar, $main); $skeleton.replaceWith($sidebar, $main);
}; },
registerHotkey = ([hotkey]) => {
window.addEventListener("focus", () => { const api = globalThis.__enhancerApi;
setState({ focus: true, rerender: true }); if (!hotkey || _hotkeyRegistered) return;
}); _hotkeyRegistered = true;
window.addEventListener("message", (event) => { api.addKeyListener(hotkey, (event) => {
if (event.data?.channel !== "notion-enhancer") return; event.preventDefault();
const [hotkey, theme, icon] = useState(["hotkey", "theme", "icon"]); const msg = { channel: "notion-enhancer", action: "open-menu" };
setState({
rerender: true,
hotkey: event.data?.hotkey ?? hotkey,
theme: event.data?.theme ?? theme,
icon: event.data?.icon ?? icon,
});
});
useState(["hotkey"], ([hotkey]) => {
const { addKeyListener } = globalThis.__enhancerApi ?? {},
[hotkeyRegistered] = useState(["hotkeyRegistered"]),
[renderStarted] = useState(["renderStarted"]);
if (!hotkey || !addKeyListener || hotkeyRegistered || !renderStarted) return;
setState({ hotkeyRegistered: true });
addKeyListener(hotkey, (event) => {
event.preventDefault();
const msg = { channel: "notion-enhancer", action: "open-menu" };
parent?.postMessage(msg, "*");
});
addKeyListener("Escape", () => {
const [popupOpen] = useState(["popupOpen"]);
if (!popupOpen) {
const msg = { channel: "notion-enhancer", action: "close-menu" };
parent?.postMessage(msg, "*"); parent?.postMessage(msg, "*");
} else setState({ rerender: true }); });
api.addKeyListener("Escape", () => {
const [popupOpen] = api.useState(["popupOpen"]);
if (!popupOpen) {
const msg = { channel: "notion-enhancer", action: "close-menu" };
parent?.postMessage(msg, "*");
} else api.setState({ rerender: true });
});
},
updateTheme = ([theme]) => {
if (theme === "dark") document.body.classList.add("dark");
if (theme === "light") document.body.classList.remove("dark");
};
const importApi = async () => {
if (_apiImported) return;
_apiImported = true;
const api = globalThis.__enhancerApi;
if (typeof api === "undefined") await import("../../shared/system.js");
await import("../../load.mjs").then((i) => i.default);
},
hookIntoState = () => {
if (_stateHookedInto) return;
_stateHookedInto = true;
const api = globalThis.__enhancerApi;
api.useState(["rerender"], renderMenu);
api.useState(["hotkey"], registerHotkey);
api.useState(["theme"], updateTheme);
};
window.addEventListener("focus", async () => {
await importApi().then(hookIntoState);
const api = globalThis.__enhancerApi;
api.setState({ focus: true, rerender: true });
});
window.addEventListener("message", async (event) => {
if (event.data?.channel !== "notion-enhancer") return;
await importApi().then(hookIntoState);
const api = globalThis.__enhancerApi;
api.setState({
rerender: true,
hotkey: event.data?.hotkey ?? api.useState(["hotkey"]),
theme: event.data?.theme ?? api.useState(["theme"]),
icon: event.data?.icon ?? api.useState(["icon"]),
}); });
}); });
useState(["theme"], ([theme]) => {
if (theme === "dark") document.body.classList.add("dark");
if (theme === "light") document.body.classList.remove("dark");
});
useState(["rerender"], async () => {
const [theme, icon] = useState(["theme", "icon"]);
if (!theme || !icon) return;
if (typeof globalThis.__enhancerApi === "undefined")
await import("../../shared/system.js");
(await import("../../load.mjs")).default.then(render);
});

View File

@ -1,24 +0,0 @@
/**
* notion-enhancer
* (c) 2023 dragonwocky <thedragonring.bod@gmail.com> (https://dragonwocky.me/)
* (https://notion-enhancer.github.io/) under the MIT license
*/
const _state = {},
_subscribers = [];
const setState = (state) => {
Object.assign(_state, state);
const updates = Object.keys(state);
_subscribers
.filter(([keys]) => updates.some((key) => keys.includes(key)))
.forEach(([keys, callback]) => callback(keys.map((key) => _state[key])));
},
useState = (keys, callback) => {
const state = keys.map((key) => _state[key]);
if (callback) _subscribers.push([keys, callback]);
callback?.(state);
return state;
};
export { setState, useState };

View File

@ -6,6 +6,8 @@
"use strict"; "use strict";
// jump to
const { twind, htm, lucide } = globalThis, const { twind, htm, lucide } = globalThis,
{ readFile, iconColour, iconMonochrome } = globalThis.__enhancerApi; { readFile, iconColour, iconMonochrome } = globalThis.__enhancerApi;
@ -71,6 +73,14 @@ const encodeSvg = (svg) =>
}), }),
}; };
}; };
// at-runtime utility class evaluation w/ twind:
// - feature parity w/ tailwind v3
// - useful for building self-contained components
// (mods can extend interfaces w/out needing to
// import additional stylesheets)
// - integrated with lucide to render icons w/out
// complex markup, e.g. `<i class="i-bookmark" />`
twind.install({ twind.install({
darkMode: "class", darkMode: "class",
rules: [[/^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/, presetIcons]], rules: [[/^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/, presetIcons]],
@ -87,14 +97,6 @@ twind.install({
["\\[.+]", (match) => "&" + match.input], ["\\[.+]", (match) => "&" + match.input],
["([a-z-]+):", ({ 1: $1 }) => "&::" + $1], ["([a-z-]+):", ({ 1: $1 }) => "&::" + $1],
], ],
theme: {
extend: {
backgroundImage: {
"brand-gradient":
"linear-gradient(225deg, rgba(255,255,255,0.2) 0%, rgba(255,255,255,0) 100%)",
},
},
},
}); });
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element // https://developer.mozilla.org/en-US/docs/Web/SVG/Element
@ -535,7 +537,11 @@ const svgElements = [
"zoomAndPan", "zoomAndPan",
]; ];
// html`<div class=${className}></div>` // enables use of the jsx-like htm syntax
// for building components and interfaces
// with tagged templates. instantiates dom
// elements directly, does not use a vdom.
// e.g. html`<div class=${className}></div>`
const h = (type, props, ...children) => { const h = (type, props, ...children) => {
children = children.flat(Infinity); children = children.flat(Infinity);
// html`<${Component} attr="value">Click Me<//>` // html`<${Component} attr="value">Click Me<//>`
@ -557,24 +563,61 @@ const h = (type, props, ...children) => {
elem.append(...children); elem.append(...children);
return elem; return elem;
}, },
// combines instance-provided element props
// with a template of element props such that
// island/component/template props handlers
// and styles can be preserved and extended
// rather than overwritten
extendProps = (props, extend) => {
for (const key in extend) {
const { [key]: userProvided } = props;
if (typeof extend[key] === "function") {
props[key] = (...args) => {
extend[key](...args);
userProvided?.(...args);
};
} else if (key === "class") {
if (userProvided) props[key] += " ";
if (!userProvided) props[key] = "";
props[key] += extend[key];
} else props[key] = extend[key] ?? userProvided;
}
return props;
},
html = htm.bind(h); html = htm.bind(h);
const extendProps = (props, extend) => { // provides basic key/value reactivity:
for (const key in extend) { // this is shared between all active mods,
const { [key]: userProvided } = props; // i.e. mods can read and update other mods'
if (typeof extend[key] === "function") { // reactive states. this enables interop
props[key] = (...args) => { // between a mod's component islands and
extend[key](...args); // supports inter-mod communication if so
userProvided?.(...args); // required. caution should be used in
}; // naming keys to avoid conflicts
} else if (key === "class") { const _state = {},
if (userProvided) props[key] += " "; _subscribers = [],
if (!userProvided) props[key] = ""; setState = (state) => {
props[key] += extend[key]; Object.assign(_state, state);
} else props[key] = extend[key] ?? userProvided; const updates = Object.keys(state);
} _subscribers
return props; .filter(([keys]) => updates.some((key) => keys.includes(key)))
}; .forEach(([keys, callback]) => callback(keys.map((key) => _state[key])));
},
// useState(["keyA", "keyB"]) => returns [valueA, valueB]
// useState(["keyA", "keyB"], callback) => registers callback
// to be triggered after each update to either keyA or keyB,
// with [valueA, valueB] passed to the callback's first arg
useState = (keys, callback) => {
const state = keys.map((key) => _state[key]);
if (callback) _subscribers.push([keys, callback]);
callback?.(state);
return state;
};
globalThis.__enhancerApi ??= {}; globalThis.__enhancerApi ??= {};
Object.assign(globalThis.__enhancerApi, { html, extendProps }); Object.assign(globalThis.__enhancerApi, {
html,
extendProps,
setState,
useState,
});