mirror of
https://github.com/notion-enhancer/notion-enhancer.git
synced 2025-04-10 15:39:01 +00:00
feat(menu): add searchbar + descriptions to mod lists, fix(menu): natural keyboard interactions w/ <Select> component
This commit is contained in:
parent
a8eb03ee67
commit
530be53e70
@ -87,9 +87,44 @@ function View({ id }, ...children) {
|
|||||||
return $el;
|
return $el;
|
||||||
}
|
}
|
||||||
|
|
||||||
function List({}, ...children) {
|
function List({ description }, ...children) {
|
||||||
const { html } = globalThis.__enhancerApi;
|
const { html } = globalThis.__enhancerApi;
|
||||||
return html`<div class="flex flex-col gap-y-[14px]">${children}</div>`;
|
return html`<div class="flex flex-col gap-y-[14px]">
|
||||||
|
<${Search} items=${children} />
|
||||||
|
<p
|
||||||
|
class="text-([12px] [color:var(--theme--fg-secondary)])"
|
||||||
|
innerHTML=${description}
|
||||||
|
></p>
|
||||||
|
${children}
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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({
|
function Mod({
|
||||||
@ -211,7 +246,8 @@ function Option({ type, value, description, _get, _set, ...props }) {
|
|||||||
return html`<${type === "toggle" ? "label" : "div"}
|
return html`<${type === "toggle" ? "label" : "div"}
|
||||||
class="notion-enhancer--menu-option flex items-center justify-between
|
class="notion-enhancer--menu-option flex items-center justify-between
|
||||||
mb-[18px] ${type === "toggle" ? "cursor-pointer" : ""}"
|
mb-[18px] ${type === "toggle" ? "cursor-pointer" : ""}"
|
||||||
><div class="flex flex-col ${type === "text" ? "w-full" : "mr-[10%]"}">
|
>
|
||||||
|
<div class="flex flex-col ${type === "text" ? "w-full" : "mr-[10%]"}">
|
||||||
<h5 class="text-[14px] mb-[2px] mt-0">${label}</h5>
|
<h5 class="text-[14px] mb-[2px] mt-0">${label}</h5>
|
||||||
${type === "text" ? $input : ""}
|
${type === "text" ? $input : ""}
|
||||||
<p
|
<p
|
||||||
@ -224,30 +260,34 @@ function Option({ type, value, description, _get, _set, ...props }) {
|
|||||||
<//>`;
|
<//>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Input({ wide, transparent, icon, onrerender, ...props }) {
|
function Input({ size, icon, transparent, onrerender, ...props }) {
|
||||||
const { html } = globalThis.__enhancerApi,
|
const { html } = globalThis.__enhancerApi,
|
||||||
$input = html`<input
|
$input = html`<input
|
||||||
class="appearance-none h-[28px] w-full bg-transparent
|
class="${size === "lg"
|
||||||
pl-[8px] pr-[32px] pb-px text-[14px] leading-[1.2]"
|
? "h-[36px] pl-[12px] pr-[40px]"
|
||||||
|
: "h-[28px] pl-[8px] pr-[32px]"}
|
||||||
|
w-full pb-px text-[14px] leading-[1.2]
|
||||||
|
appearance-none bg-transparent"
|
||||||
...${props}
|
...${props}
|
||||||
/>`,
|
/>`,
|
||||||
$icon = html`<i
|
$icon = html`<i
|
||||||
class="i-${icon} pointer-events-none
|
class="i-${icon} absolute w-[16px] h-[16px] pointer-events-none
|
||||||
absolute right-[8px] top-[6px] w-[16px] h-[16px]
|
${size === "lg" ? "right-[12px] top-[10px]" : "right-[8px] top-[6px]"}
|
||||||
text-[color:var(--theme--fg-secondary)]"
|
text-[color:var(--theme--fg-secondary)]"
|
||||||
></i>`;
|
></i>`;
|
||||||
useState(["rerender"], () => onrerender?.($input, $icon));
|
useState(["rerender"], () => onrerender?.($input, $icon));
|
||||||
return html`<label
|
return html`<label
|
||||||
|
focus=${() => $input.focus()}
|
||||||
class="notion-enhancer--menu-input
|
class="notion-enhancer--menu-input
|
||||||
relative overflow-hidden rounded-[4px]
|
relative overflow-hidden rounded-[4px]
|
||||||
${wide ? "block w-full mt-[4px] mb-[8px]" : "shrink-0 w-[192px]"}
|
focus-within:ring-(& [color:var(--theme--accent-primary)])
|
||||||
${transparent
|
${size === "lg" ? "block w-full" : ""}
|
||||||
? `bg-(
|
${size === "md" ? "block w-full mt-[4px] mb-[8px]" : ""}
|
||||||
[image:repeating-linear-gradient(45deg,#aaa_25%,transparent_25%,transparent_75%,#aaa_75%,#aaa),repeating-linear-gradient(45deg,#aaa_25%,#fff_25%,#fff_75%,#aaa_75%,#aaa)]
|
${size === "sm" ? "shrink-0 w-[192px]" : ""}
|
||||||
[position:0_0,4px_4px]
|
bg-${transparent
|
||||||
[size:8px_8px]
|
? `([image:repeating-linear-gradient(45deg,#aaa_25%,transparent_25%,transparent_75%,#aaa_75%,#aaa),repeating-linear-gradient(45deg,#aaa_25%,#fff_25%,#fff_75%,#aaa_75%,#aaa)]
|
||||||
)`
|
[position:0_0,4px_4px] [size:8px_8px])`
|
||||||
: "bg-[color:var(--theme--bg-hover)]"}"
|
: "[color:var(--theme--bg-hover)]"}"
|
||||||
>${$input}${$icon}
|
>${$input}${$icon}
|
||||||
</label>`;
|
</label>`;
|
||||||
}
|
}
|
||||||
@ -255,7 +295,7 @@ function Input({ wide, transparent, icon, onrerender, ...props }) {
|
|||||||
function TextInput({ _get, _set, onchange, ...props }) {
|
function TextInput({ _get, _set, onchange, ...props }) {
|
||||||
const { html } = globalThis.__enhancerApi;
|
const { html } = globalThis.__enhancerApi;
|
||||||
return html`<${Input}
|
return html`<${Input}
|
||||||
wide
|
size="md"
|
||||||
type="text"
|
type="text"
|
||||||
icon="text-cursor"
|
icon="text-cursor"
|
||||||
onchange=${(event) => {
|
onchange=${(event) => {
|
||||||
@ -272,6 +312,7 @@ function TextInput({ _get, _set, onchange, ...props }) {
|
|||||||
function NumberInput({ _get, _set, onchange, ...props }) {
|
function NumberInput({ _get, _set, onchange, ...props }) {
|
||||||
const { html } = globalThis.__enhancerApi;
|
const { html } = globalThis.__enhancerApi;
|
||||||
return html`<${Input}
|
return html`<${Input}
|
||||||
|
size="sm"
|
||||||
type="number"
|
type="number"
|
||||||
icon="hash"
|
icon="hash"
|
||||||
onchange=${(event) => {
|
onchange=${(event) => {
|
||||||
@ -314,6 +355,7 @@ function HotkeyInput({ _get, _set, onkeydown, ...props }) {
|
|||||||
event.target.dispatchEvent(new Event("change"));
|
event.target.dispatchEvent(new Event("change"));
|
||||||
};
|
};
|
||||||
return html`<${Input}
|
return html`<${Input}
|
||||||
|
size="sm"
|
||||||
type="text"
|
type="text"
|
||||||
icon="command"
|
icon="command"
|
||||||
onkeydown=${(event) => {
|
onkeydown=${(event) => {
|
||||||
@ -349,6 +391,7 @@ function ColorInput({ _get, _set, oninput, ...props }) {
|
|||||||
};
|
};
|
||||||
return html`<${Input}
|
return html`<${Input}
|
||||||
transparent
|
transparent
|
||||||
|
size="sm"
|
||||||
type="text"
|
type="text"
|
||||||
icon="pipette"
|
icon="pipette"
|
||||||
data-coloris
|
data-coloris
|
||||||
@ -439,6 +482,9 @@ function Select({ values, _get, _set, ...props }) {
|
|||||||
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
||||||
...${props}
|
...${props}
|
||||||
></div>`,
|
></div>`,
|
||||||
|
$options = values.map((value) => {
|
||||||
|
return html`<${SelectOption} ...${{ value, _get, _set }} />`;
|
||||||
|
}),
|
||||||
$popup = html`<div
|
$popup = html`<div
|
||||||
class="group absolute top-0 left-0
|
class="group absolute top-0 left-0
|
||||||
flex flex-col justify-center items-end
|
flex flex-col justify-center items-end
|
||||||
@ -452,38 +498,48 @@ function Select({ values, _get, _set, ...props }) {
|
|||||||
transition duration-[200ms] opacity-0 scale-95 rounded-[4px]
|
transition duration-[200ms] opacity-0 scale-95 rounded-[4px]
|
||||||
group-open:(pointer-events-auto opacity-100 scale-100)"
|
group-open:(pointer-events-auto opacity-100 scale-100)"
|
||||||
>
|
>
|
||||||
${values.map((value) => {
|
${$options}
|
||||||
return html`<${SelectOption} ...${{ value, _get, _set }} />`;
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const { onclick } = $select;
|
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) => {
|
$select.onclick = (event) => {
|
||||||
onclick?.(event);
|
onclick?.(event);
|
||||||
$popup.setAttribute("open", true);
|
openPopup();
|
||||||
setState({ popupOpen: true });
|
};
|
||||||
|
$select.onkeydown = (event) => {
|
||||||
|
onkeydown?.(event);
|
||||||
|
if (event.key === "Enter") openPopup();
|
||||||
};
|
};
|
||||||
useState(["rerender"], () => {
|
useState(["rerender"], () => {
|
||||||
_get?.().then((value) => {
|
_get?.().then((value) => {
|
||||||
if ($popup.hasAttribute("open")) {
|
if ($popup.hasAttribute("open")) {
|
||||||
$popup.removeAttribute("open");
|
closePopup(value);
|
||||||
$select.style.width = `${$select.offsetWidth}px`;
|
|
||||||
$select.style.background = "transparent";
|
|
||||||
$select.innerText = value;
|
|
||||||
setTimeout(() => {
|
|
||||||
$select.style.width = "";
|
|
||||||
$select.style.background = "";
|
|
||||||
setState({ popupOpen: false });
|
|
||||||
}, 200);
|
|
||||||
} else $select.innerText = value;
|
} else $select.innerText = value;
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
document.addEventListener("click", (event) => {
|
document.addEventListener("click", (event) => {
|
||||||
if (!$popup.hasAttribute("open")) return;
|
if (!$popup.hasAttribute("open")) return;
|
||||||
if ($popup.contains(event.target) || event.target === $select) return;
|
if ($popup.contains(event.target) || event.target === $select) return;
|
||||||
_set?.($select.innerText);
|
closePopup();
|
||||||
});
|
});
|
||||||
|
|
||||||
return html`<div class="notion-enhancer--menu-select relative">
|
return html`<div class="notion-enhancer--menu-select relative">
|
||||||
@ -501,7 +557,6 @@ function SelectOption({ value, _get, _set, ...props }) {
|
|||||||
$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
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
|
||||||
class="select-none cursor-pointer rounded-[3px]
|
class="select-none cursor-pointer rounded-[3px]
|
||||||
flex items-center w-full h-[28px] px-[12px] leading-[1.2]
|
flex items-center w-full h-[28px] px-[12px] leading-[1.2]
|
||||||
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
transition duration-[20ms] hover:bg-[color:var(--theme--bg-hover)]"
|
||||||
@ -512,11 +567,15 @@ function SelectOption({ value, _get, _set, ...props }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|
||||||
const { onclick } = $option;
|
const { onclick, onkeydown } = $option;
|
||||||
$option.onclick = (event) => {
|
$option.onclick = (event) => {
|
||||||
onclick?.(event);
|
onclick?.(event);
|
||||||
_set?.(value);
|
_set?.(value);
|
||||||
};
|
};
|
||||||
|
$option.onkeydown = (event) => {
|
||||||
|
onkeydown?.(event);
|
||||||
|
if (event.key === "Enter") _set?.(value);
|
||||||
|
};
|
||||||
useState(["rerender"], () => {
|
useState(["rerender"], () => {
|
||||||
_get?.().then((actualValue) => {
|
_get?.().then((actualValue) => {
|
||||||
if (actualValue === value) {
|
if (actualValue === value) {
|
||||||
|
@ -58,7 +58,7 @@ const compatibleMods = (mods) => {
|
|||||||
return required && compatible;
|
return required && compatible;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
renderList = async (mods) => {
|
renderList = async (mods, description) => {
|
||||||
const { html, getProfile, initDatabase } = globalThis.__enhancerApi,
|
const { html, getProfile, initDatabase } = globalThis.__enhancerApi,
|
||||||
enabledMods = initDatabase([await getProfile(), "enabledMods"]);
|
enabledMods = initDatabase([await getProfile(), "enabledMods"]);
|
||||||
mods = compatibleMods(mods).map(async (mod) => {
|
mods = compatibleMods(mods).map(async (mod) => {
|
||||||
@ -69,30 +69,31 @@ const compatibleMods = (mods) => {
|
|||||||
};
|
};
|
||||||
return html`<${Mod} ...${{ ...mod, _get, _set }} />`;
|
return html`<${Mod} ...${{ ...mod, _get, _set }} />`;
|
||||||
});
|
});
|
||||||
return html`<${List}>${await Promise.all(mods)}<//>`;
|
return html`<${List} description=${description}>
|
||||||
|
${await Promise.all(mods)}
|
||||||
|
<//>`;
|
||||||
|
},
|
||||||
|
renderOptionViews = async (parentView, mods) => {
|
||||||
|
const { html, getProfile, initDatabase } = globalThis.__enhancerApi,
|
||||||
|
enabledMods = initDatabase([await getProfile(), "enabledMods"]);
|
||||||
|
mods = compatibleMods(mods)
|
||||||
|
.filter((mod) => {
|
||||||
|
return mod.options?.filter((opt) => opt.type !== "heading").length;
|
||||||
|
})
|
||||||
|
.map(async (mod) => {
|
||||||
|
const _get = () => enabledMods.get(mod.id),
|
||||||
|
_set = async (enabled) => {
|
||||||
|
await enabledMods.set(mod.id, enabled);
|
||||||
|
setState({ rerender: true });
|
||||||
|
};
|
||||||
|
return html`<${View} id=${mod.id}>
|
||||||
|
<${Mod} ...${{ ...mod, options: [], _get, _set }} />
|
||||||
|
${await renderOptions(mod)}<//
|
||||||
|
>`;
|
||||||
|
});
|
||||||
|
return Promise.all(mods);
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderOptionViews = async (parentView, mods) => {
|
|
||||||
const { html, getProfile, initDatabase } = globalThis.__enhancerApi,
|
|
||||||
enabledMods = initDatabase([await getProfile(), "enabledMods"]);
|
|
||||||
mods = compatibleMods(mods)
|
|
||||||
.filter((mod) => {
|
|
||||||
return mod.options?.filter((opt) => opt.type !== "heading").length;
|
|
||||||
})
|
|
||||||
.map(async (mod) => {
|
|
||||||
const _get = () => enabledMods.get(mod.id),
|
|
||||||
_set = async (enabled) => {
|
|
||||||
await enabledMods.set(mod.id, enabled);
|
|
||||||
setState({ rerender: true });
|
|
||||||
};
|
|
||||||
return html`<${View} id=${mod.id}>
|
|
||||||
<${Mod} ...${{ ...mod, options: [], _get, _set }} />
|
|
||||||
${await renderOptions(mod)}<//
|
|
||||||
>`;
|
|
||||||
});
|
|
||||||
return Promise.all(mods);
|
|
||||||
};
|
|
||||||
|
|
||||||
const render = async () => {
|
const render = async () => {
|
||||||
const { html, getCore, getThemes } = globalThis.__enhancerApi,
|
const { html, getCore, getThemes } = globalThis.__enhancerApi,
|
||||||
{ getExtensions, getIntegrations } = globalThis.__enhancerApi,
|
{ getExtensions, getIntegrations } = globalThis.__enhancerApi,
|
||||||
@ -150,13 +151,32 @@ const render = async () => {
|
|||||||
html`<${View} id="welcome">welcome<//>`,
|
html`<${View} id="welcome">welcome<//>`,
|
||||||
html`<${View} id="core">${await renderOptions(await getCore())}<//>`
|
html`<${View} id="core">${await renderOptions(await getCore())}<//>`
|
||||||
);
|
);
|
||||||
for (const { id, mods } of [
|
for (const { id, mods, description } of [
|
||||||
{ id: "themes", mods: await getThemes() },
|
{
|
||||||
{ id: "extensions", mods: await getExtensions() },
|
id: "themes",
|
||||||
{ id: "integrations", mods: await getIntegrations() },
|
mods: await getThemes(),
|
||||||
|
description: `Themes override Notion's colour schemes. To switch between
|
||||||
|
dark mode and light mode, go to <span class="py-[2px] px-[4px] rounded-[3px]
|
||||||
|
bg-[color:var(--theme--bg-hover)]">Settings & members → My notifications &
|
||||||
|
settings → My settings → Appearance</span>.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "extensions",
|
||||||
|
mods: await getExtensions(),
|
||||||
|
description: `Extensions add to the functionality and layout of the Notion
|
||||||
|
client, interacting with and modifying existing interfaces.`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "integrations",
|
||||||
|
mods: await getIntegrations(),
|
||||||
|
description: `<span class="text-[color:var(--theme--fg-red)]">
|
||||||
|
Integrations access and modify Notion content. They interact directly with
|
||||||
|
<span class="py-[2px] px-[4px] rounded-[3px] bg-[color:var(--theme--bg-hover)]">
|
||||||
|
https://www.notion.so/api/v3</span>. Use at your own risk.</span>`,
|
||||||
|
},
|
||||||
]) {
|
]) {
|
||||||
document.body.append(
|
document.body.append(
|
||||||
html`<${View} id=${id}>${await renderList(mods)}<//>`,
|
html`<${View} id=${id}>${await renderList(mods, description)}<//>`,
|
||||||
...(await renderOptionViews(id, mods))
|
...(await renderOptionViews(id, mods))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user