/** * notion-enhancer * (c) 2023 dragonwocky (https://dragonwocky.me/) * (https://notion-enhancer.github.io/) under the MIT license */ import { setState, useState, getState } from "./state.mjs"; function Sidebar({}, ...children) { const { html } = globalThis.__enhancerApi; return html``; } function SidebarSection({}, ...children) { const { html } = globalThis.__enhancerApi; return html`

${children}

`; } function SidebarButton({ icon, ...props }, ...children) { const { html } = globalThis.__enhancerApi, iconSize = icon.startsWith("notion-enhancer") ? "w-[17px] h-[17px] ml-[1.5px] mr-[9.5px]" : "w-[18px] h-[18px] ml-px mr-[9px]", $el = html`<${props.href ? "a" : "button"} class="flex select-none cursor-pointer w-full items-center py-[5px] px-[15px] text-[14px] last:mb-[12px] transition hover:bg-[color:var(--theme--bg-hover)]" ...${props} > ${children} `; if (!props.href) { const id = $el.innerText; $el.onclick ??= () => setState({ transition: "fade", view: id }); useState(["view"], ([view = "welcome"]) => { const active = view.toLowerCase() === id.toLowerCase(); $el.style.background = active ? "var(--theme--bg-hover)" : ""; $el.style.fontWeight = active ? "600" : ""; }); } return $el; } function View({ id }, ...children) { const { html } = globalThis.__enhancerApi, duration = 100, $el = html`
${children}
`; useState(["view"], ([view = "welcome"]) => { const [transition] = getState(["transition"]), isVisible = $el.style.display !== "none", nowActive = view.toLowerCase() === id.toLowerCase(); if (transition === "fade") { $el.style.opacity = "0"; $el.style.transition = `opacity ${duration}ms`; if (isVisible && !nowActive) { setTimeout(() => ($el.style.display = "none"), duration); } else if (!isVisible && nowActive) { setTimeout(() => { $el.style.display = ""; requestIdleCallback(() => ($el.style.opacity = "1")); }, duration); } } else { $el.style.transition = ""; $el.style.opacity = nowActive ? "1" : "0"; $el.style.display = nowActive ? "" : "none"; } }); return $el; } function List({ description }, ...children) { const { html } = globalThis.__enhancerApi; return html`
<${Search} items=${children} />

${children}
`; } function Search({ items, oninput, ...props }) { const { html, addKeyListener } = globalThis.__enhancerApi, $search = html`<${Input} size="lg" type="text" icon="search" placeholder="Search ('/' to focus)" oninput=${(event) => { oninput?.(event); const query = event.target.value.toLowerCase(); for (const $item of items) { const matches = $item.innerText.toLowerCase().includes(query); $item.style.display = matches ? "" : "none"; } }} ...${props} />`; addKeyListener("/", (event) => { if (document.activeElement?.nodeName === "INPUT") return; // offsetParent == null if parent has "display: none;" if ($search.offsetParent) { event.preventDefault(); $search.focus(); } }); return $search; } function Mod({ id, name, version, description, thumbnail, tags = [], authors, options = [], _get, _set, _src, }) { const { html, enhancerUrl } = globalThis.__enhancerApi, toggleId = Math.random().toString(36).slice(2, 5), $thumbnail = thumbnail ? html`` : "", $options = options.length ? html`` : ""; return html``; } function Option({ type, value, description, _get, _set, ...props }) { const { html } = globalThis.__enhancerApi, camelToSentenceCase = (string) => string[0].toUpperCase() + string.replace(/[A-Z]/g, (match) => ` ${match.toLowerCase()}`).slice(1); let $input; const label = props.label ?? camelToSentenceCase(props.key); switch (type) { case "heading": return html`

${label}

`; case "text": $input = html`<${TextInput} ...${{ _get, _set }} />`; break; case "number": $input = html`<${NumberInput} ...${{ _get, _set }} />`; break; case "hotkey": $input = html`<${HotkeyInput} ...${{ _get, _set }} />`; break; case "color": $input = html`<${ColorInput} ...${{ _get, _set }} />`; break; case "file": $input = html`<${FileInput} extensions="${props.extensions}" ...${{ _get, _set }} />`; break; case "select": $input = html`<${Select} values=${props.values} ...${{ _get, _set }} />`; break; case "toggle": $input = html`<${Toggle} ...${{ _get, _set }} />`; } return html`<${type === "toggle" ? "label" : "div"} class="notion-enhancer--menu-option flex items-center justify-between mb-[18px] ${type === "toggle" ? "cursor-pointer" : ""}" >
${label}
${type === "text" ? $input : ""}

${type === "text" ? "" : $input} `; } function Input({ size, icon, transparent, onrerender, ...props }) { const { html } = globalThis.__enhancerApi, $input = html``, $icon = html``; useState(["rerender"], () => onrerender?.($input, $icon)); return html``; } function TextInput({ _get, _set, onchange, ...props }) { const { html } = globalThis.__enhancerApi; return html`<${Input} size="md" type="text" icon="text-cursor" onchange=${(event) => { onchange?.(event); _set?.(event.target.value); }} onrerender=${($input) => { _get?.().then((value) => ($input.value = value)); }} ...${props} />`; } function NumberInput({ _get, _set, onchange, ...props }) { const { html } = globalThis.__enhancerApi; return html`<${Input} size="sm" type="number" icon="hash" onchange=${(event) => { onchange?.(event); _set?.(event.target.value); }} onrerender=${($input) => { _get?.().then((value) => ($input.value = value)); }} ...${props} />`; } function HotkeyInput({ _get, _set, onkeydown, ...props }) { const { html } = globalThis.__enhancerApi, updateHotkey = (event) => { event.preventDefault(); const keys = []; for (const modifier of ["metaKey", "ctrlKey", "altKey", "shiftKey"]) { if (!event[modifier]) continue; const alias = modifier[0].toUpperCase() + modifier.slice(1, -3); keys.push(alias); } if (!keys.length && ["Backspace", "Delete"].includes(event.key)) { event.target.value = ""; } else if (event.key) { let key = event.key; if (key === " ") key = "Space"; if (["+", "="].includes(key)) key = "Plus"; if (key === "-") key = "Minus"; if (event.code === "Comma") key = ","; if (event.code === "Period") key = "."; if (key === "Control") key = "Ctrl"; // avoid e.g. Shift+Shift, force inclusion of non-modifier if (keys.includes(key)) return; keys.push(key.length === 1 ? key.toUpperCase() : key); event.target.value = keys.join("+"); } event.target.dispatchEvent(new Event("input")); event.target.dispatchEvent(new Event("change")); }; return html`<${Input} size="sm" type="text" icon="command" onkeydown=${(event) => { updateHotkey(event); onkeydown?.(event); _set?.(event.target.value); }} onrerender=${($input) => { _get?.().then((value) => ($input.value = value)); }} ...${props} />`; } function ColorInput({ _get, _set, oninput, ...props }) { Coloris({ format: "rgb" }); const { html } = globalThis.__enhancerApi, updateContrast = ($input, $icon) => { $input.style.background = $input.value; const [r, g, b, a = 1] = $input.value .replace(/^rgba?\(/, "") .replace(/\)$/, "") .split(",") .map((n) => parseFloat(n)); if (a > 0.5) { // pick a contrasting foreground for an rgb background // using the percieved brightness constants from http://alienryderflex.com/hsp.html const brightness = 0.299 * (r * r) + 0.587 * (g * g) + 0.114 * (b * b); $input.style.color = Math.sqrt(brightness) > 165.75 ? "#000" : "#fff"; } else $input.style.color = "#000"; $icon.style.color = $input.style.color; $icon.style.opacity = "0.7"; }; return html`<${Input} transparent size="sm" type="text" icon="pipette" data-coloris oninput=${(event) => { oninput?.(event); _set?.(event.target.value); }} onrerender=${($input, $icon) => { _get?.().then((value) => { $input.value = value; updateContrast($input, $icon); }); }} ...${props} />`; } function FileInput({ extensions, _get, _set, ...props }) { const { html } = globalThis.__enhancerApi, $filename = html`Upload a file`, $clear = html``; const { onchange } = props; props.onchange = (event) => { const file = event.target.files[0], reader = new FileReader(); reader.onload = async (progress) => { const content = progress.currentTarget.result, upload = { filename: file.name, content }; $filename.innerText = file.name; $clear.style.display = ""; _set?.(upload); }; reader.readAsText(file); onchange?.(event); }; useState(["rerender"], () => { _get?.().then((file) => { $filename.innerText = file?.filename || "Upload a file"; $clear.style.display = file?.filename ? "" : "none"; }); }); return html`
${$clear}
`; } function Select({ values, _get, _set, ...props }) { const { html } = globalThis.__enhancerApi, $select = html`
`, $options = values.map((value) => { return html`<${SelectOption} ...${{ value, _get, _set }} />`; }), $popup = html`
${$options}
`; const { onclick, onkeydown } = $select, openPopup = () => { $popup.setAttribute("open", true); $options.forEach(($opt) => ($opt.tabIndex = 0)); setState({ popupOpen: true }); }, closePopup = (value) => { $popup.removeAttribute("open"); $options.forEach(($opt) => ($opt.tabIndex = -1)); $select.style.width = `${$select.offsetWidth}px`; $select.style.background = "transparent"; if (value) $select.innerText = value; setTimeout(() => { $select.style.width = ""; $select.style.background = ""; setState({ popupOpen: false }); }, 200); }; $select.onclick = (event) => { onclick?.(event); openPopup(); }; $select.onkeydown = (event) => { onkeydown?.(event); if (event.key === "Enter") openPopup(); }; useState(["rerender"], () => { _get?.().then((value) => { if ($popup.hasAttribute("open")) { closePopup(value); } else $select.innerText = value; }); }); document.addEventListener("click", (event) => { if (!$popup.hasAttribute("open")) return; if ($popup.contains(event.target) || event.target === $select) return; closePopup(); }); return html`
${$select}${$popup}
`; } function SelectOption({ value, _get, _set, ...props }) { const { html } = globalThis.__enhancerApi, $selected = html``, $option = html`
${value}
`; const { onclick, onkeydown } = $option; $option.onclick = (event) => { onclick?.(event); _set?.(value); }; $option.onkeydown = (event) => { onkeydown?.(event); if (event.key === "Enter") _set?.(value); }; useState(["rerender"], () => { _get?.().then((actualValue) => { if (actualValue === value) { $option.append($selected); } else $selected.remove(); }); }); return $option; } function Toggle({ _get, _set, ...props }) { const { html } = globalThis.__enhancerApi, $input = html``; const { onchange } = $input; $input.onchange = (event) => { onchange?.(event); _set?.($input.checked); }; useState(["rerender"], () => { _get?.().then((checked) => ($input.checked = checked)); }); return html`
${$input}
`; } export { Sidebar, SidebarSection, SidebarButton, View, List, Mod, Option };