diff --git a/scripts/vendor-dependencies.mjs b/scripts/vendor-dependencies.mjs index ae3e603..7c23af6 100644 --- a/scripts/vendor-dependencies.mjs +++ b/scripts/vendor-dependencies.mjs @@ -9,10 +9,41 @@ import { existsSync } from "node:fs"; import { resolve } from "node:path"; import { fileURLToPath } from "node:url"; +const esmVersion = "135", + esTarget = "es2022", + esmBundle = ({ name, version, path = "", exports = [] }) => { + const scopedName = name; + if (name.startsWith("@")) name = name.split("/")[1]; + path ||= `${name}.bundle.mjs`; + let bundleSrc = `https://esm.sh/v${esmVersion}/${scopedName}@${version}/${esTarget}/${path}`; + if (exports.length) bundleSrc += `?bundle&exports=${exports.join()}`; + return { [`${scopedName.replace(/\//g, "-")}.mjs`]: bundleSrc }; + }; + const dependencies = { - "htm.min.js": "https://unpkg.com/htm@3.1.1/mini/index.js", - "twind.min.js": "https://unpkg.com/@twind/cdn@1.0.8/cdn.global.js", - "lucide.min.js": "https://unpkg.com/lucide@0.319.0/dist/umd/lucide.min.js", + ...esmBundle({ name: "htm", version: "3.1.1" }), + ...esmBundle({ + name: "lucide", + version: "0.319.0", + path: "dist/umd/lucide.mjs", + }), + ...esmBundle({ + name: "@unocss/core", + version: "0.58.5", + exports: ["createGenerator", "expandVariantGroup"], + }), + ...esmBundle({ + name: "@unocss/preset-uno", + version: "0.58.5", + exports: ["presetUno"], + }), + ...esmBundle({ + name: "@unocss/preset-icons", + version: "0.58.5", + exports: ["presetIcons"], + }), + "@unocss-preflight-tailwind.css": + "https://esm.sh/@unocss/reset@0.58.5/tailwind.css", "coloris.min.js": "https://cdn.jsdelivr.net/gh/mdbassit/Coloris@v0.22.0/dist/coloris.min.js", "coloris.min.css": @@ -29,22 +60,3 @@ for (const file in dependencies) { res = await (await fetch(source)).text(); await write(file, res); } - -// expose vendored twind cdn -await append("twind.min.js", `\n;globalThis.twind = twind;`); - -// build content type lookup script from mime-db to avoid -// re-processing entire the database every time a file is -// requested via notion://www.notion.so/__notion-enhancer/ -let contentTypes = []; -for (const [type, { extensions, charset }] of Object.entries( - await (await fetch("https://unpkg.com/mime-db@1.52.0/db.json")).json() -)) { - if (!extensions) continue; - const contentType = charset - ? `${type}; charset=${charset.toLowerCase()}` - : type; - for (const ext of extensions) contentTypes.push([ext, contentType]); -} -contentTypes = `module.exports=new Map(${JSON.stringify(contentTypes)});`; -await write("content-types.min.js", contentTypes); diff --git a/src/common/events.js b/src/common/events.js index 81aa5a6..1707fc8 100644 --- a/src/common/events.js +++ b/src/common/events.js @@ -61,56 +61,6 @@ const _state = {}, }, dumpState = () => _state; -let documentObserver, - mutationListeners = []; -const mutationQueue = [], - addMutationListener = (selector, callback, subtree = true) => { - mutationListeners.push([selector, callback, subtree]); - }, - removeMutationListener = (callback) => { - mutationListeners = mutationListeners.filter(([, c]) => c !== callback); - }, - selectorMutated = (mutation, selector, subtree) => { - const target = - mutation.type === "characterData" - ? mutation.target.parentElement - : mutation.target, - matchesTarget = target?.matches(selector); - if (!subtree) return matchesTarget; - const descendsFromTarget = target?.matches(`${selector} *`), - addedToTarget = [...(mutation.addedNodes || [])].some( - (node) => - node instanceof HTMLElement && - (node?.matches(`${selector}, ${selector} *`) || - node?.querySelector(selector)) - ); - return matchesTarget || descendsFromTarget || addedToTarget; - }, - handleMutations = () => { - while (mutationQueue.length) { - const mutation = mutationQueue.shift(); - for (const [selector, callback, subtree] of mutationListeners) { - const matches = selectorMutated(mutation, selector, subtree); - if (matches) callback(mutation); - } - } - }, - attachObserver = () => { - if (document.readyState !== "complete") return; - documentObserver ??= new MutationObserver((mutations, _observer) => { - if (!mutationQueue.length) requestIdleCallback(handleMutations); - mutationQueue.push(...mutations); - }); - documentObserver.observe(document.body, { - attributes: true, - characterData: true, - childList: true, - subtree: true, - }); - }; -document.addEventListener("readystatechange", attachObserver); -attachObserver(); - let keyListeners = []; // accelerators approximately match electron accelerators. // logic used when recording hotkeys in menu matches logic used @@ -163,17 +113,21 @@ const modifierAliases = [ }); if (combinationTriggered) callback(event); } + }, + onKeyup = (event) => { + const keyupListeners = keyListeners // + .filter(([, , waitForKeyup]) => waitForKeyup); + handleKeypress(event, keyupListeners); + }, + onKeydown = (event) => { + const keydownListeners = keyListeners // + .filter(([, , waitForKeyup]) => !waitForKeyup); + handleKeypress(event, keydownListeners); }; -document.addEventListener("keyup", (event) => { - const keyupListeners = keyListeners // - .filter(([, , waitForKeyup]) => waitForKeyup); - handleKeypress(event, keyupListeners); -}); -document.addEventListener("keydown", (event) => { - const keydownListeners = keyListeners // - .filter(([, , waitForKeyup]) => !waitForKeyup); - handleKeypress(event, keydownListeners); -}); +document.removeEventListener("keyup", onKeyup); +document.removeEventListener("keydown", onKeydown); +document.addEventListener("keyup", onKeyup); +document.addEventListener("keydown", onKeydown); Object.assign((globalThis.__enhancerApi ??= {}), { sleep, @@ -181,8 +135,6 @@ Object.assign((globalThis.__enhancerApi ??= {}), { setState, useState, dumpState, - addMutationListener, - removeMutationListener, addKeyListener, removeKeyListener, }); diff --git a/src/common/markup.js b/src/common/markup.js deleted file mode 100644 index 2b6d4d2..0000000 --- a/src/common/markup.js +++ /dev/null @@ -1,598 +0,0 @@ -/** - * notion-enhancer - * (c) 2023 dragonwocky (https://dragonwocky.me/) - * (https://notion-enhancer.github.io/) under the MIT license - */ - -"use strict"; - -const { twind, htm, lucide } = globalThis, - { iconColour, iconMonochrome } = globalThis.__enhancerApi; - -const kebabToPascalCase = (string) => - string[0].toUpperCase() + - string.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase()).slice(1), - hToString = (type, props, ...children) => - `<${type}${Object.entries(props) - .map(([attr, value]) => ` ${attr}="${value}"`) - .join("")}>${children - .map((child) => (Array.isArray(child) ? hToString(...child) : child)) - .join("")}`; - -const encodeSvg = (svg) => - // https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0 - svg - .replace( - "/g, "%3E") - .replace(/\s+/g, " "), - presetIcons = ([, icon, mode]) => { - let svg; - // manually register i-notion-enhancer: renders the colour - // version by default, renders the monochrome version when - // mask mode is requested via i-notion-enhancer?mask - if (icon === "notion-enhancer") { - svg = mode === "mask" ? iconMonochrome : iconColour; - } else { - icon = kebabToPascalCase(icon); - if (!lucide[icon]) return; - const [type, props, children] = lucide[icon]; - svg = hToString(type, props, ...children); - } - // https://antfu.me/posts/icons-in-pure-css - const dataUri = `url("data:image/svg+xml;utf8,${encodeSvg(svg)}")`; - if (mode === "auto") mode = undefined; - mode ??= svg.includes("currentColor") ? "mask" : "bg"; - return { - display: "inline-block", - height: "1em", - width: "1em", - ...(mode === "mask" - ? { - mask: `${dataUri} no-repeat`, - "mask-size": "100% 100%", - "background-color": "currentColor", - color: "inherit", - } - : { - background: `${dataUri} no-repeat`, - "background-size": "100% 100%", - "background-color": "transparent", - }), - }; - }; - -// 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. `` -twind.install({ - darkMode: "class", - rules: [ - ["text-(wrap|nowrap|balance|pretty)", "textWrap"], - [/^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/, presetIcons], - [/^size-\[([^\]]+)\]$/, ({ 1: $1 }) => ({ height: $1, width: $1 })], - ], - variants: [ - ["children", "&>*"], - ["siblings", "&~*"], - ["sibling", "&+*"], - [/^&/, (match) => match.input], - [/^has-\[([^\]]+)\]/, (match) => `&:has(${match[1]})`], - [ - /^not-([a-z-]+|\[.+\])/, - ({ 1: $1 }) => `&:not(${($1[0] == "[" ? "" : ":") + $1})`, - ], - ], -}); - -// https://developer.mozilla.org/en-US/docs/Web/SVG/Element -const svgElements = [ - "animate", - "animateMotion", - "animateTransform", - "circle", - "clipPath", - "defs", - "desc", - "discard", - "ellipse", - "feBlend", - "feColorMatrix", - "feComponentTransfer", - "feComposite", - "feConvolveMatrix", - "feDiffuseLighting", - "feDisplacementMap", - "feDistantLight", - "feDropShadow", - "feFlood", - "feFuncA", - "feFuncB", - "feFuncG", - "feFuncR", - "feGaussianBlur", - "feImage", - "feMerge", - "feMergeNode", - "feMorphology", - "feOffset", - "fePointLight", - "feSpecularLighting", - "feSpotLight", - "feTile", - "feTurbulence", - "filter", - "foreignObject", - "g", - "hatch", - "hatchpath", - "image", - "line", - "linearGradient", - "marker", - "mask", - "metadata", - "mpath", - "path", - "pattern", - "polygon", - "polyline", - "radialGradient", - "rect", - "script", - "set", - "stop", - "style", - "svg", - "switch", - "symbol", - "text", - "textPath", - "title", - "tspan", - "use", - "view", - ], - htmlAttributes = [ - // https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes - "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", - // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute[ - "accent-height", - "accumulate", - "additive", - "alignment-baseline", - "alphabetic", - "amplitude", - "arabic-form", - "ascent", - "attributeName", - "attributeType", - "azimuth", - "baseFrequency", - "baseline-shift", - "baseProfile", - "bbox", - "begin", - "bias", - "by", - "calcMode", - "cap-height", - "clip", - "clipPathUnits", - "clip-path", - "clip-rule", - "color-interpolation", - "color-interpolation-filters", - "color-profile", - "color-rendering", - "contentScriptType", - "contentStyleType", - "cursor", - "cx", - "cy", - "d", - "decelerate", - "descent", - "diffuseConstant", - "direction", - "display", - "divisor", - "dominant-baseline", - "dur", - "dx", - "dy", - "edgeMode", - "elevation", - "enable-background", - "end", - "exponent", - "fill", - "fill-opacity", - "fill-rule", - "filter", - "filterRes", - "filterUnits", - "flood-color", - "flood-opacity", - "font-family", - "font-size", - "font-size-adjust", - "font-stretch", - "font-style", - "font-variant", - "font-weight", - "format", - "from", - "fr", - "fx", - "fy", - "g1", - "g2", - "glyph-name", - "glyph-orientation-horizontal", - "glyph-orientation-vertical", - "glyphRef", - "gradientTransform", - "gradientUnits", - "hanging", - "horiz-adv-x", - "horiz-origin-x", - "ideographic", - "image-rendering", - "in", - "in2", - "intercept", - "k", - "k1", - "k2", - "k3", - "k4", - "kernelMatrix", - "kernelUnitLength", - "kerning", - "keyPoints", - "keySplines", - "keyTimes", - "lengthAdjust", - "letter-spacing", - "lighting-color", - "limitingConeAngle", - "local", - "marker-end", - "marker-mid", - "marker-start", - "markerHeight", - "markerUnits", - "markerWidth", - "mask", - "maskContentUnits", - "maskUnits", - "mathematical", - "mode", - "numOctaves", - "offset", - "opacity", - "operator", - "order", - "orient", - "orientation", - "origin", - "overflow", - "overline-position", - "overline-thickness", - "panose-1", - "paint-order", - "path", - "pathLength", - "patternContentUnits", - "patternTransform", - "patternUnits", - "pointer-events", - "points", - "pointsAtX", - "pointsAtY", - "pointsAtZ", - "preserveAlpha", - "preserveAspectRatio", - "primitiveUnits", - "r", - "radius", - "referrerPolicy", - "refX", - "refY", - "rendering-intent", - "repeatCount", - "repeatDur", - "requiredExtensions", - "requiredFeatures", - "restart", - "result", - "rotate", - "rx", - "ry", - "scale", - "seed", - "shape-rendering", - "slope", - "spacing", - "specularConstant", - "specularExponent", - "speed", - "spreadMethod", - "startOffset", - "stdDeviation", - "stemh", - "stemv", - "stitchTiles", - "stop-color", - "stop-opacity", - "strikethrough-position", - "strikethrough-thickness", - "string", - "stroke", - "stroke-dasharray", - "stroke-dashoffset", - "stroke-linecap", - "stroke-linejoin", - "stroke-miterlimit", - "stroke-opacity", - "stroke-width", - "surfaceScale", - "systemLanguage", - "tableValues", - "targetX", - "targetY", - "text-anchor", - "text-decoration", - "text-rendering", - "textLength", - "to", - "transform", - "transform-origin", - "u1", - "u2", - "underline-position", - "underline-thickness", - "unicode", - "unicode-bidi", - "unicode-range", - "units-per-em", - "v-alphabetic", - "v-hanging", - "v-ideographic", - "v-mathematical", - "values", - "vector-effect", - "version", - "vert-adv-y", - "vert-origin-x", - "vert-origin-y", - "viewBox", - "viewTarget", - "visibility", - "widths", - "word-spacing", - "writing-mode", - "x", - "x-height", - "x1", - "x2", - "xChannelSelector", - "xlink:actuate", - "xlink:arcrole", - "xlink:href", - "xlink:role", - "xlink:show", - "xlink:title", - "xlink:type", - "xml:base", - "xml:lang", - "xml:space", - "y", - "y1", - "y2", - "yChannelSelector", - "z", - "zoomAndPan", - ]; - -// 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`
` -const h = (type, props, ...children) => { - children = children.flat(Infinity); - // html`<${Component} attr="value">Click Me` - if (typeof type === "function") { - return type(props ?? {}, ...children); - } - const elem = svgElements.includes(type) - ? document.createElementNS("http://www.w3.org/2000/svg", type) - : document.createElement(type); - for (const prop in props ?? {}) { - if (typeof props[prop] === "undefined") continue; - const isAttr = - htmlAttributes.includes(prop) || - prop.startsWith("data-") || - prop.startsWith("aria-"); - if (isAttr) { - if (typeof props[prop] === "boolean") { - if (!props[prop]) continue; - elem.setAttribute(prop, ""); - } else elem.setAttribute(prop, props[prop]); - } else elem[prop] = props[prop]; - } - if (type === "style") { - elem.append(children.join("").replace(/\s+/g, " ")); - } else elem.append(...children); - 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); - -Object.assign((globalThis.__enhancerApi ??= {}), { - html, - extendProps, -}); diff --git a/src/common/markup.mjs b/src/common/markup.mjs new file mode 100644 index 0000000..9c5ac5f --- /dev/null +++ b/src/common/markup.mjs @@ -0,0 +1,234 @@ +/** + * notion-enhancer + * (c) 2023 dragonwocky (https://dragonwocky.me/) + * (https://notion-enhancer.github.io/) under the MIT license + */ + +"use strict"; + +import htm from "../vendor/htm.mjs"; +import lucide from "../vendor/lucide.mjs"; +import { + createGenerator, + expandVariantGroup, +} from "../vendor/@unocss-core.mjs"; +import { presetUno } from "../vendor/@unocss-preset-uno.mjs"; + +// prettier-ignore +// https://developer.mozilla.org/en-US/docs/Web/SVG/Element +// https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute +// https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes +const svgElements = ["animate","animateMotion","animateTransform","circle","clipPath","defs","desc","discard","ellipse","feBlend","feColorMatrix","feComponentTransfer","feComposite","feConvolveMatrix","feDiffuseLighting","feDisplacementMap","feDistantLight","feDropShadow","feFlood","feFuncA","feFuncB","feFuncG","feFuncR","feGaussianBlur","feImage","feMerge","feMergeNode","feMorphology","feOffset","fePointLight","feSpecularLighting","feSpotLight","feTile","feTurbulence","filter","foreignObject","g","hatch","hatchpath","image","line","linearGradient","marker","mask","metadata","mpath","path","pattern","polygon","polyline","radialGradient","rect","script","set","stop","style","svg","switch","symbol","text","textPath","title","tspan","use","view"], + 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","accent-height","accumulate","additive","alignment-baseline","alphabetic","amplitude","arabic-form","ascent","attributeName","attributeType","azimuth","baseFrequency","baseline-shift","baseProfile","bbox","begin","bias","by","calcMode","cap-height","clip","clipPathUnits","clip-path","clip-rule","color-interpolation","color-interpolation-filters","color-profile","color-rendering","contentScriptType","contentStyleType","cursor","cx","cy","d","decelerate","descent","diffuseConstant","direction","display","divisor","dominant-baseline","dur","dx","dy","edgeMode","elevation","enable-background","end","exponent","fill","fill-opacity","fill-rule","filter","filterRes","filterUnits","flood-color","flood-opacity","font-family","font-size","font-size-adjust","font-stretch","font-style","font-variant","font-weight","format","from","fr","fx","fy","g1","g2","glyph-name","glyph-orientation-horizontal","glyph-orientation-vertical","glyphRef","gradientTransform","gradientUnits","hanging","horiz-adv-x","horiz-origin-x","ideographic","image-rendering","in","in2","intercept","k","k1","k2","k3","k4","kernelMatrix","kernelUnitLength","kerning","keyPoints","keySplines","keyTimes","lengthAdjust","letter-spacing","lighting-color","limitingConeAngle","local","marker-end","marker-mid","marker-start","markerHeight","markerUnits","markerWidth","mask","maskContentUnits","maskUnits","mathematical","mode","numOctaves","offset","opacity","operator","order","orient","orientation","origin","overflow","overline-position","overline-thickness","panose-1","paint-order","path","pathLength","patternContentUnits","patternTransform","patternUnits","pointer-events","points","pointsAtX","pointsAtY","pointsAtZ","preserveAlpha","preserveAspectRatio","primitiveUnits","r","radius","referrerPolicy","refX","refY","rendering-intent","repeatCount","repeatDur","requiredExtensions","requiredFeatures","restart","result","rotate","rx","ry","scale","seed","shape-rendering","slope","spacing","specularConstant","specularExponent","speed","spreadMethod","startOffset","stdDeviation","stemh","stemv","stitchTiles","stop-color","stop-opacity","strikethrough-position","strikethrough-thickness","string","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","surfaceScale","systemLanguage","tableValues","targetX","targetY","text-anchor","text-decoration","text-rendering","textLength","to","transform","transform-origin","u1","u2","underline-position","underline-thickness","unicode","unicode-bidi","unicode-range","units-per-em","v-alphabetic","v-hanging","v-ideographic","v-mathematical","values","vector-effect","version","vert-adv-y","vert-origin-x","vert-origin-y","viewBox","viewTarget","visibility","widths","word-spacing","writing-mode","x","x-height","x1","x2","xChannelSelector","xlink:actuate","xlink:arcrole","xlink:href","xlink:role","xlink:show","xlink:title","xlink:type","xml:base","xml:lang","xml:space","y","y1","y2","yChannelSelector","z","zoomAndPan"]; + +const kebabToPascalCase = (string) => + string[0].toUpperCase() + + string.replace(/-[a-z]/g, (match) => match.slice(1).toUpperCase()).slice(1), + hToString = (type, props, ...children) => + `<${type}${Object.entries(props) + .map(([attr, value]) => ` ${attr}="${value}"`) + .join("")}>${children + .map((child) => (Array.isArray(child) ? hToString(...child) : child)) + .join("")}`, + svgToUri = (svg) => { + // https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0 + const xlmns = ~svg.indexOf("xmlns") + ? "/g, "%3E") + .replace(/\s+/g, " ") + .trim()}")`; + }; + +const iconPattern = /^i-((?:\w|-)+)(?:\?(mask|bg|auto))?$/, + presetIcons = ([, icon, mode]) => { + let svg; + if (!["bg", "mask"].includes(mode)) mode = undefined; + if (icon === "notion-enhancer") { + const { iconColour, iconMonochrome } = globalThis.__enhancerApi; + svg = mode === "mask" ? iconMonochrome : iconColour; + } else { + icon = kebabToPascalCase(icon); + if (!lucide[icon]) return; + const [type, props, children] = lucide[icon]; + svg = hToString(type, props, ...children); + } + mode ??= svg.includes("currentColor") ? "mask" : "bg"; + return { + // https://antfu.me/posts/icons-in-pure-css + display: "inline-block", + height: "1em", + width: "1em", + ...(mode === "mask" + ? { + mask: `${svgToUri(svg)} no-repeat`, + "mask-size": "100% 100%", + "background-color": "currentColor", + } + : { + background: `${svgToUri(svg)} no-repeat`, + "background-size": "100% 100%", + "background-color": "transparent", + }), + }; + }; + +let documentObserver, + observerDefaults = { + attributes: true, + characterData: true, + childList: true, + subtree: true, + }, + mutationListeners = []; +const _mutations = [], + addMutationListener = (selector, callback, opts) => { + opts = { ...observerDefaults, ...opts }; + mutationListeners.push([selector, callback, opts]); + }, + removeMutationListener = (callback) => { + mutationListeners = mutationListeners.filter(([, c]) => c !== callback); + }, + selectorMutated = (mutation, selector, opts) => { + if (!opts.attributes && mutation.type === "attributes") return false; + if (!opts.characterData && mutation.type === "characterData") return false; + const target = + mutation.type === "characterData" + ? mutation.target.parentElement + : mutation.target; + if (!target) return false; + const matchesTarget = target.matches(selector), + matchesParent = opts.subtree && target.matches(`${selector} *`), + matchesChild = opts.subtree && target.querySelector(selector), + matchesAdded = + // was matching element added? + opts.childList && + [...(mutation.addedNodes || [])].some((node) => { + if (!(node instanceof HTMLElement)) node = node.parentElement; + return node?.querySelector(selector); + }); + return matchesTarget || matchesParent || matchesChild || matchesAdded; + }, + handleMutations = () => { + let mutation; + while ((mutation = _mutations.shift())) { + for (const [selector, callback, subtree] of mutationListeners) + if (selectorMutated(mutation, selector, subtree)) callback(mutation); + } + }, + attachObserver = () => { + if (document.readyState !== "complete") return; + document.removeEventListener("readystatechange", attachObserver); + (documentObserver ??= new MutationObserver((mutations, _observer) => { + if (!_mutations.length) requestIdleCallback(handleMutations); + _mutations.push(...mutations); + })).disconnect(); + documentObserver.observe(document.body, observerDefaults); + }; +document.addEventListener("readystatechange", attachObserver); +attachObserver(); + +// 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 +const extendProps = (props, extend) => { + for (const key in extend) { + const { [key]: value } = props; + if (typeof extend[key] === "function") { + props[key] = (...args) => { + extend[key](...args); + if (typeof value === "function") value(...args); + }; + } else if (key === "class") { + props[key] = value ? `${value} ${extend[key]}` : extend[key]; + } else props[key] = extend[key] ?? value; + } + return props; + }, + // 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`
` + h = function (type, props, ...children) { + // disables element caching + this[0] = 3; + children = children.flat(Infinity); + if (typeof type === "function") { + // html`<${Component} attr="value">Click Me` + return type(props ?? {}, ...children); + } + const elem = svgElements.includes(type) + ? document.createElementNS("http://www.w3.org/2000/svg", type) + : document.createElement(type); + for (const prop in props ?? {}) { + if (typeof props[prop] === "undefined") continue; + if (["class", "className"].includes(prop)) { + // collapse multiline classes & + // expand utility variant class groups + props[prop] = props[prop].replace(/\s+/g, " "); + props[prop] = expandVariantGroup(props[prop]).trim(); + elem.setAttribute("un-cloak", ""); + } + if (htmlAttributes.includes(prop) || prop.includes("-")) { + if (typeof props[prop] === "boolean") { + if (!props[prop]) continue; + elem.setAttribute(prop, ""); + } else elem.setAttribute(prop, props[prop]); + } else elem[prop] = props[prop]; + } + if (type === "style") { + elem.append(children.join("").replace(/\s+/g, " ")); + } else elem.append(...children); + return elem; + }, + html = htm.bind(h); + +let _renderedTokens = -1; +const _tokens = new Set(), + _stylesheet = html``, + uno = createGenerator({ + presets: [presetUno()], + preflights: [{ getCSS: () => `[un-cloak]{display:none!important}` }], + rules: [[iconPattern, presetIcons, { layer: "icons" }]], + layers: { preflights: -2, icons: -1, default: 1 }, + }), + extractTokens = ($root) => { + if (!$root?.classList) return; + for (const t of $root.classList) _tokens.add(t); + for (const $ of $root.children) extractTokens($); + $root.removeAttribute("un-cloak"); + }, + renderStylesheet = async () => { + if (_renderedTokens === _tokens.size) return; + _renderedTokens = _tokens.size; + const res = await uno.generate(_tokens); + if (!document.contains(_stylesheet)) document.head.append(_stylesheet); + if (_stylesheet.innerHTML !== res.css) _stylesheet.innerHTML = res.css; + }; +addMutationListener("*", (mutation) => { + const targets = []; + if (mutation.type === "childList") { + for (const node of mutation.addedNodes) extractTokens(node); + } else if (mutation.type === "attributes") extractTokens(mutation.target); + else return; + renderStylesheet(); +}); +renderStylesheet(); + +Object.assign((globalThis.__enhancerApi ??= {}), { + html, + extendProps, + addMutationListener, + removeMutationListener, +}); diff --git a/src/core/client.mjs b/src/core/client.mjs index 75f3d37..de9197b 100644 --- a/src/core/client.mjs +++ b/src/core/client.mjs @@ -72,7 +72,7 @@ const insertMenu = async (api, db) => { // pass notion-enhancer api to electron menu process if (["linux", "win32", "darwin"].includes(platform)) { const apiKey = "__enhancerApi"; - this.contentWindow[apiKey] = globalThis[apiKey]; + this.contentWindow[apiKey] = { ...globalThis[apiKey] }; } _contentWindow = this.contentWindow; updateMenuTheme(); @@ -99,7 +99,7 @@ const insertMenu = async (api, db) => { Configure the notion-enhancer and its mods `.attach($button, "right"); addMutationListener(notionSidebar, appendToDom); - addMutationListener(".notion-app-inner", updateMenuTheme, false); + addMutationListener(".notion-app-inner", updateMenuTheme, { subtree: false }); appendToDom(); addKeyListener(openMenuHotkey, (event) => { diff --git a/src/core/islands/FloatingButton.mjs b/src/core/islands/FloatingButton.mjs index 93c413e..a065e05 100644 --- a/src/core/islands/FloatingButton.mjs +++ b/src/core/islands/FloatingButton.mjs @@ -18,7 +18,7 @@ const setupWrapper = () => { const $wrapper = html`
.notion-help-button]:static" style="right:${$help.style.right}" >
`; removeMutationListener(addToDom); diff --git a/src/core/islands/Panel.mjs b/src/core/islands/Panel.mjs index 8a98bed..627ead1 100644 --- a/src/core/islands/Panel.mjs +++ b/src/core/islands/Panel.mjs @@ -99,12 +99,12 @@ function Panel({ `, $panel = html`
diff --git a/src/core/islands/TopbarButton.mjs b/src/core/islands/TopbarButton.mjs index 67523b6..83cb080 100644 --- a/src/core/islands/TopbarButton.mjs +++ b/src/core/islands/TopbarButton.mjs @@ -15,9 +15,9 @@ function TopbarButton({ icon, ...props }, ...children) { select-none h-[28px] w-[33px] duration-[20ms] transition inline-flex items-center justify-center rounded-[3px] hover:bg-[color:var(--theme--bg-hover)] - has-[span]:w-auto &>span:(text-[14px] leading-[1.2] px-[8px]) - &[data-active]:bg-[color:var(--theme--bg-hover)] - &>i:size-[20px]`, + has-[span]:w-auto [&>span]:(text-[14px] leading-[1.2] px-[8px]) + [&[data-active]]:bg-[color:var(--theme--bg-hover)] + [&>i]:size-[20px]`, }); return html`