feat(menu): clone notion ui for core settings, add toggles

- reduce size of icons in sidebar
- keyboard focus outline
- improved distinguishing of attrs vs props
- register twind variants from preset-ext
This commit is contained in:
dragonwocky 2023-01-11 21:59:57 +11:00
parent ac5daf5b73
commit 881f69c47d
Signed by: dragonwocky
GPG Key ID: 7998D08F7D7BD7A8
8 changed files with 420 additions and 164 deletions

View File

@ -11,7 +11,7 @@ import { fileURLToPath } from "node:url";
const dependencies = { const dependencies = {
"htm.min.js": "https://unpkg.com/htm@3.1.1/mini/index.js", "htm.min.js": "https://unpkg.com/htm@3.1.1/mini/index.js",
"twind.min.js": "https://unpkg.com/@twind/cdn@1.0.4/cdn.global.js", "twind.min.js": "https://unpkg.com/@twind/cdn@1.0.7/cdn.global.js",
"lucide.min.js": "https://unpkg.com/lucide@0.104.0/dist/umd/lucide.min.js", "lucide.min.js": "https://unpkg.com/lucide@0.104.0/dist/umd/lucide.min.js",
"jscolor.min.js": "jscolor.min.js":
"https://cdnjs.cloudflare.com/ajax/libs/jscolor/2.5.1/jscolor.min.js", "https://cdnjs.cloudflare.com/ajax/libs/jscolor/2.5.1/jscolor.min.js",

View File

@ -79,76 +79,220 @@ const encodeSvg = (svg) =>
}; };
twind.install({ twind.install({
rules: [[/^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/, presetIcons]], rules: [[/^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/, presetIcons]],
variants: [["open", "&[open]"]], variants: [
// https://github.com/tw-in-js/twind/blob/main/packages/preset-ext/src/variants.ts
[
"not-([a-z-]+|\\[.+\\])",
({ 1: $1 }) => `&:not(${($1[0] == "[" ? "" : ":") + $1})`,
],
["children", "&>*"],
["siblings", "&~*"],
["sibling", "&+*"],
["override", "&&"],
["\\[.+]", (match) => "&" + match.input],
["([a-z-]+):", ({ 1: $1 }) => "&::" + $1],
],
}); });
// https://developer.mozilla.org/en-US/docs/Web/SVG/Element
const svgElements = [ const svgElements = [
"animate", "animate",
"animateMotion", "animateMotion",
"animateTransform", "animateTransform",
"circle", "circle",
"clipPath", "clipPath",
"defs", "defs",
"desc", "desc",
"discard", "discard",
"ellipse", "ellipse",
"feBlend", "feBlend",
"feColorMatrix", "feColorMatrix",
"feComponentTransfer", "feComponentTransfer",
"feComposite", "feComposite",
"feConvolveMatrix", "feConvolveMatrix",
"feDiffuseLighting", "feDiffuseLighting",
"feDisplacementMap", "feDisplacementMap",
"feDistantLight", "feDistantLight",
"feDropShadow", "feDropShadow",
"feFlood", "feFlood",
"feFuncA", "feFuncA",
"feFuncB", "feFuncB",
"feFuncG", "feFuncG",
"feFuncR", "feFuncR",
"feGaussianBlur", "feGaussianBlur",
"feImage", "feImage",
"feMerge", "feMerge",
"feMergeNode", "feMergeNode",
"feMorphology", "feMorphology",
"feOffset", "feOffset",
"fePointLight", "fePointLight",
"feSpecularLighting", "feSpecularLighting",
"feSpotLight", "feSpotLight",
"feTile", "feTile",
"feTurbulence", "feTurbulence",
"filter", "filter",
"foreignObject", "foreignObject",
"g", "g",
"hatch", "hatch",
"hatchpath", "hatchpath",
"image", "image",
"line", "line",
"linearGradient", "linearGradient",
"marker", "marker",
"mask", "mask",
"metadata", "metadata",
"mpath", "mpath",
"path", "path",
"pattern", "pattern",
"polygon", "polygon",
"polyline", "polyline",
"radialGradient", "radialGradient",
"rect", "rect",
"script", "script",
"set", "set",
"stop", "stop",
"style", "style",
"svg", "svg",
"switch", "switch",
"symbol", "symbol",
"text", "text",
"textPath", "textPath",
"title", "title",
"tspan", "tspan",
"use", "use",
"view", "view",
]; ],
// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes
htmlAttributes = [
"accept",
"accept-charset",
"accesskey",
"action",
"align",
"allow",
"alt",
"async",
"autocapitalize",
"autocomplete",
"autofocus",
"autoplay",
"background",
"bgcolor",
"border",
"buffered",
"capture",
"challenge",
"charset",
"checked",
"cite",
"class",
"code",
"codebase",
"color",
"cols",
"colspan",
"content",
"contenteditable",
"contextmenu",
"controls",
"coords",
"crossorigin",
"csp",
"data",
"data-*",
"datetime",
"decoding",
"default",
"defer",
"dir",
"dirname",
"disabled",
"download",
"draggable",
"enctype",
"enterkeyhint",
"for",
"form",
"formaction",
"formenctype",
"formmethod",
"formnovalidate",
"formtarget",
"headers",
"height",
"hidden",
"high",
"href",
"hreflang",
"http-equiv",
"icon",
"id",
"importance",
"integrity",
"inputmode",
"ismap",
"itemprop",
"keytype",
"kind",
"label",
"lang",
"loading",
"list",
"loop",
"low",
"max",
"maxlength",
"minlength",
"media",
"method",
"min",
"multiple",
"muted",
"name",
"novalidate",
"open",
"optimum",
"pattern",
"ping",
"placeholder",
"playsinline",
"poster",
"preload",
"radiogroup",
"readonly",
"referrerpolicy",
"rel",
"required",
"reversed",
"role",
"rows",
"rowspan",
"sandbox",
"scope",
"selected",
"shape",
"size",
"sizes",
"slot",
"span",
"spellcheck",
"src",
"srcdoc",
"srclang",
"srcset",
"start",
"step",
"style",
"tabindex",
"target",
"title",
"translate",
"type",
"usemap",
"value",
"width",
"wrap",
];
// html`<div class=${className}></div>` // html`<div class=${className}></div>`
const h = (type, props, ...children) => { const h = (type, props, ...children) => {
children = children.flat(Infinity); children = children.flat(Infinity);
@ -160,7 +304,7 @@ const h = (type, props, ...children) => {
? document.createElementNS("http://www.w3.org/2000/svg", type) ? document.createElementNS("http://www.w3.org/2000/svg", type)
: document.createElement(type); : document.createElement(type);
for (const prop in props ?? {}) { for (const prop in props ?? {}) {
if (["string", "number", "boolean"].includes(typeof props[prop])) { if (htmlAttributes.includes(prop)) {
elem.setAttribute(prop, props[prop]); elem.setAttribute(prop, props[prop]);
} else elem[prop] = props[prop]; } else elem[prop] = props[prop];
} }

View File

@ -60,6 +60,7 @@ export default async (api, db) => {
}; };
const openMenu = () => { const openMenu = () => {
updateTheme(true);
$menuModal?.setAttribute("open", true); $menuModal?.setAttribute("open", true);
}, },
closeMenu = () => $menuModal?.removeAttribute("open"); closeMenu = () => $menuModal?.removeAttribute("open");

View File

@ -9,8 +9,8 @@ import { setState, useState } from "./state.mjs";
const Sidebar = ({}, ...children) => { const Sidebar = ({}, ...children) => {
const { html } = globalThis.__enhancerApi; const { html } = globalThis.__enhancerApi;
return html`<aside return html`<aside
class="notion-enhancer--menu-sidebar h-full w-[250px] class="notion-enhancer--menu-sidebar min-w-[224.14px] max-w-[250px]
overflow-y-auto bg-[color:var(--theme--bg-secondary)]" h-full overflow-y-auto bg-[color:var(--theme--bg-secondary)]"
> >
${children} ${children}
</aside>`; </aside>`;
@ -31,8 +31,8 @@ const SidebarButton = ({ icon, ...props }, ...children) => {
const { html } = globalThis.__enhancerApi, const { html } = globalThis.__enhancerApi,
iconSize = iconSize =
icon === "notion-enhancer" icon === "notion-enhancer"
? "w-[18px] h-[18px] ml-px mr-[9px]" ? "w-[16px] h-[16px] ml-[2px] mr-[10px]"
: "w-[20px] h-[20px] mr-[8px]", : "w-[18px] h-[18px] ml-px mr-[9px]",
el = html`<${props.href ? "a" : "button"} el = html`<${props.href ? "a" : "button"}
class="flex select-none cursor-pointer w-full class="flex select-none cursor-pointer w-full
items-center py-[5px] px-[15px] text-[14px] last:mb-[12px] items-center py-[5px] px-[15px] text-[14px] last:mb-[12px]
@ -70,4 +70,98 @@ const View = ({ id }, ...children) => {
return el; return el;
}; };
export { Sidebar, SidebarSection, SidebarButton, View }; const TextInput = ({}, ...children) => {};
const NumberInput = ({}, ...children) => {};
const HotkeyInput = ({}, ...children) => {};
const ColorInput = ({}, ...children) => {};
const FileInput = ({}, ...children) => {};
const Select = ({}, ...children) => {};
const Toggle = (props, ..._children) => {
const { html } = globalThis.__enhancerApi;
return html`<div class="notion-enhancer--menu-toggle shrink-0">
<input
tabindex="-1"
type="checkbox"
class="appearance-none w-0 h-0 checked:sibling:children:(
bg-[color:var(--theme--accent-primary)] after:translate-x-[12px])"
...${props}
/>
<div
tabindex="0"
class="w-[30px] h-[18px] transition duration-200
rounded-[44px] bg-[color:var(--theme--bg-hover)]"
>
<div
class="w-full h-full rounded-[44px] p-[2px]
hover:bg-[color:var(--theme--bg-hover)]
transition duration-200 after:(
inline-block w-[14px] h-[14px] rounded-[44px]
bg-[color:var(--theme--accent-primary\\_contrast)]
transition duration-200
)"
></div>
</div>
</div>`;
};
const Option = ({ mod, type, ...props }, ..._children) => {
const { html } = globalThis.__enhancerApi,
camelToSentenceCase = (string) =>
string[0].toUpperCase() +
string.replace(/[A-Z]/g, (match) => ` ${match.toLowerCase()}`).slice(1);
const label = props.label ?? camelToSentenceCase(props.key),
description = props.description;
if (type === "heading") {
return html`<h2
class="notion-enhancer--menu-heading font-semibold
mb-[16px] mt-[48px] first:mt-0 pb-[12px] text-[16px]
border-b border-b-[color:var(--theme--fg-border)]"
>
${label}
</h2>`;
}
const id = `${mod}-${props.key}`;
switch (type) {
// case "text":
// break;
// case "number":
// break;
// case "hotkey":
// break;
// case "color":
// break;
// case "file":
// break;
// case "select":
// break;
// case "toggle":
default:
break;
}
return html`
<label
class="notion-enhancer--menu-option mb-[18px]
flex items-center justify-between cursor-pointer"
>
<div class="flex flex-col mr-[10%]" for=${id}>
<h3 class="text-[14px] mb-[2px] mt-0">${label}</h3>
<p
class="text-[12px] leading-[16px]
text-[color:var(--theme--fg-secondary)]"
innerHTML=${description}
></p>
</div>
<${Toggle} id=${id} />
</label>
`;
};
export { Sidebar, SidebarSection, SidebarButton, View, Toggle, Option };

View File

@ -7,6 +7,9 @@
::selection { ::selection {
background: var(--theme--accent-primary_transparent); background: var(--theme--accent-primary_transparent);
} }
*:focus-visible {
outline: 3px solid var(--theme--accent-primary);
}
::-webkit-scrollbar { ::-webkit-scrollbar {
width: 10px; width: 10px;
@ -24,5 +27,10 @@
background: var(--theme--scrollbar-thumb_hover) !important; background: var(--theme--scrollbar-thumb_hover) !important;
} }
/*! modern-normalize v1.1.0 | MIT License | https://github.com/sindresorhus/modern-normalize */ .notion-enhancer--menu-option a {
*,::after,::before{box-sizing:border-box}html{-moz-tab-size:4;tab-size:4}html{line-height:1.15;-webkit-text-size-adjust:100%}body{margin:0}body{font-family:system-ui,-apple-system,'Segoe UI',Roboto,Helvetica,Arial,sans-serif,'Apple Color Emoji','Segoe UI Emoji'}hr{height:0;color:inherit}abbr[title]{text-decoration:underline dotted}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Consolas,'Liberation Mono',Menlo,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;line-height:1.15;margin:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button}::-moz-focus-inner{border-style:none;padding:0}:-moz-focusring{outline:1px dotted ButtonText}:-moz-ui-invalid{box-shadow:none}legend{padding:0}progress{vertical-align:baseline}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item} text-decoration: underline;
transition: 100ms ease-in;
}
.notion-enhancer--menu-option a:hover {
color: var(--theme--accent-secondary);
}

View File

@ -5,96 +5,98 @@
*/ */
import { setState, useState } from "./state.mjs"; import { setState, useState } from "./state.mjs";
import { Sidebar, SidebarSection, SidebarButton, View } from "./components.mjs"; import {
Sidebar,
SidebarSection,
SidebarButton,
View,
Option,
} from "./components.mjs";
let stylesLoaded = false, let renderStarted;
interfacePopulated = false; const render = async () => {
const importApi = async () => { const { html, getCore } = globalThis.__enhancerApi;
// chrome extensions run in an isolated execution context if (!html || !getCore || renderStarted) return;
// but extension:// pages can access chrome apis renderStarted = true;
// ∴ notion-enhancer api is imported directly
if (typeof globalThis.__enhancerApi === "undefined") { const core = await getCore();
await import("../../api/browser.js");
} const $sidebar = html`<${Sidebar}>
// in electron this isn't necessary, as a) scripts are ${[
// not running in an isolated execution context and b) "notion-enhancer",
// the notion:// protocol csp bypass allows scripts to { icon: "notion-enhancer", title: "Welcome" },
// set iframe globals via $iframe.contentWindow {
}, icon: "message-circle",
importStyles = async () => { title: "Community",
if (stylesLoaded) return false; href: "https://discord.gg/sFWPXtA",
stylesLoaded = true; },
await import("../../load.mjs"); {
}, icon: "clock",
populateInterface = () => { title: "Changelog",
const { html } = globalThis.__enhancerApi; href: "https://notion-enhancer.github.io/about/changelog/",
if (!html || interfacePopulated) return; },
interfacePopulated = true; {
const $sidebar = html`<${Sidebar}> icon: "book",
${[ title: "Documentation",
"notion-enhancer", href: "https://notion-enhancer.github.io/",
{ icon: "notion-enhancer", title: "Welcome" }, },
{ {
icon: "message-circle", icon: "github",
title: "Community", title: "Source Code",
href: "https://discord.gg/sFWPXtA", href: "https://github.com/notion-enhancer",
}, },
{ {
icon: "clock", icon: "coffee",
title: "Changelog", title: "Sponsor",
href: "https://notion-enhancer.github.io/about/changelog/", href: "https://github.com/sponsors/dragonwocky",
}, },
{ "Settings",
icon: "book", { icon: "sliders-horizontal", title: "Core" },
title: "Documentation", { icon: "palette", title: "Themes" },
href: "https://notion-enhancer.github.io/", { icon: "zap", title: "Extensions" },
}, { icon: "plug", title: "Integrations" },
{ ].map((item) => {
icon: "github", if (typeof item === "string") {
title: "Source Code", return html`<${SidebarSection}>${item}<//>`;
href: "https://github.com/notion-enhancer", } else {
}, const { title, ...props } = item;
{ return html`<${SidebarButton} ...${props}>${title}<//>`;
icon: "coffee", }
title: "Sponsor", })}
href: "https://github.com/sponsors/dragonwocky", <//>`,
}, $views = [
"Settings", html`<${View} id="welcome">welcome<//>`,
{ icon: "sliders-horizontal", title: "Core" }, html`<${View} id="core">
{ icon: "palette", title: "Themes" }, ${core.options.map(
{ icon: "zap", title: "Extensions" }, (opt) => html`<${Option} mod=${core.id} ...${opt} />`
{ icon: "plug", title: "Integrations" }, )}
].map((item) => {
if (typeof item === "string") {
return html`<${SidebarSection}>${item}<//>`;
} else {
const { title, ...props } = item;
return html`<${SidebarButton} ...${props}>${title}<//>`;
}
})}
<//>`, <//>`,
$views = [ html`<${View} id="themes">themes<//>`,
html`<${View} id="welcome">welcome<//>`, html`<${View} id="extensions">extensions<//>`,
html`<${View} id="core">core<//>`, html`<${View} id="integrations">integrations<//>`,
html`<${View} id="themes">themes<//>`, ];
html`<${View} id="extensions">extensions<//>`, document.body.append($sidebar, ...$views);
html`<${View} id="integrations">integrations<//>`, };
];
document.body.append($sidebar, ...$views);
};
window.addEventListener("message", async (event) => { window.addEventListener("message", async (event) => {
if (event.data?.namespace !== "notion-enhancer") return; if (event.data?.namespace !== "notion-enhancer") return;
setState({ theme: event.data?.mode }); setState({ theme: event.data?.mode });
await importApi(); // chrome extensions run in an isolated execution context
await importStyles(); // but extension:// pages can access chrome apis
// ∴ notion-enhancer api is imported directly
if (typeof globalThis.__enhancerApi === "undefined") {
await import("../../api/browser.js");
// in electron this isn't necessary, as a) scripts are
// not running in an isolated execution context and b)
// the notion:// protocol csp bypass allows scripts to
// set iframe globals via $iframe.contentWindow
}
// load stylesheets from enabled themes
await import("../../load.mjs");
// wait for api globals to be available // wait for api globals to be available
requestIdleCallback(() => populateInterface()); requestIdleCallback(render);
}); });
useState(["theme"], ([mode]) => { useState(["theme"], ([mode]) => {
if (mode === "dark") { if (mode === "dark") document.body.classList.add("dark");
document.body.classList.add("dark"); if (mode === "light") document.body.classList.remove("dark");
} else if (mode === "light") {
document.body.classList.remove("dark");
}
}); });

View File

@ -7,11 +7,13 @@
"use strict"; "use strict";
(async () => { (async () => {
console.log("notion-enhancer: loading...");
// prettier-ignore // prettier-ignore
const { enhancerUrl } = globalThis.__enhancerApi, const { enhancerUrl } = globalThis.__enhancerApi,
isMenu = location.href.startsWith(enhancerUrl("/core/menu/index.html")), isMenu = location.href.startsWith(enhancerUrl("/core/menu/index.html")),
pageLoaded = /(^\/$)|((-|\/)[0-9a-f]{32}((\?.+)|$))/.test(location.pathname), pageLoaded = /(^\/$)|((-|\/)[0-9a-f]{32}((\?.+)|$))/.test(location.pathname),
signedIn = localStorage["LRU:KeyValueStore2:current-user-id"]; signedIn = localStorage["LRU:KeyValueStore2:current-user-id"];
if (!isMenu && !(signedIn && pageLoaded)) return; if (!isMenu && !(signedIn && pageLoaded)) return;
await import("./vendor/twind.min.js"); await import("./vendor/twind.min.js");
@ -25,6 +27,9 @@
for (const mod of await getMods()) { for (const mod of await getMods()) {
if (!(await isEnabled(mod.id))) continue; if (!(await isEnabled(mod.id))) continue;
const isTheme = mod._src.startsWith("themes/"),
isCore = mod._src === "core";
if (isMenu && !(isTheme || isCore)) continue;
// clientStyles // clientStyles
for (let stylesheet of mod.clientStyles ?? []) { for (let stylesheet of mod.clientStyles ?? []) {
@ -33,9 +38,9 @@
$stylesheet.href = enhancerUrl(`${mod._src}/${stylesheet}`); $stylesheet.href = enhancerUrl(`${mod._src}/${stylesheet}`);
document.head.append($stylesheet); document.head.append($stylesheet);
} }
if (isMenu) continue;
// clientScripts // clientScripts
if (isMenu) continue;
const options = await optionDefaults(mod.id), const options = await optionDefaults(mod.id),
db = initDatabase([await getProfile(), mod.id], options); db = initDatabase([await getProfile(), mod.id], options);
for (let script of mod.clientScripts ?? []) { for (let script of mod.clientScripts ?? []) {
@ -43,4 +48,6 @@
script.default(globalThis.__enhancerApi, db); script.default(globalThis.__enhancerApi, db);
} }
} }
console.log("notion-enhancer: ready");
})(); })();

File diff suppressed because one or more lines are too long