select styled

This commit is contained in:
Mateusz Gruszczyński
2026-09-07 15:01:58 +02:00
parent 5734fcfe01
commit 87b61d6088
14 changed files with 551 additions and 269 deletions
+186 -233
View File
File diff suppressed because it is too large Load Diff
+4 -4
View File
@@ -120,13 +120,13 @@
</div>
<button id="mobile-upload-button" class="mobile-upload-button" type="button"
title="Upload a file"><span aria-hidden="true"></span> Upload</button>
<div class="editor-controls"><label>Font<select id="font-family">
<div class="editor-controls"><label>Font<select class="app-select-input" id="font-family">
<option value="mono">Mono</option>
<option value="system">System</option>
<option value="serif">Serif</option>
<option value="arial">Arial</option>
<option value="georgia">Georgia</option>
</select></label><label>Size<select id="font-size">
</select></label><label>Size<select class="app-select-input" id="font-size">
<option value="14" selected>14</option>
<option value="16">16</option>
<option value="18">18</option>
@@ -338,14 +338,14 @@
<details id="mobile-editor-options" class="mobile-editor-options">
<summary title="Editor options" aria-label="Open editor options"></summary>
<div class="mobile-editor-options__panel">
<label>Font<select id="mobile-font-family">
<label>Font<select class="app-select-input" id="mobile-font-family">
<option value="mono">Mono</option>
<option value="system">System</option>
<option value="serif">Serif</option>
<option value="arial">Arial</option>
<option value="georgia">Georgia</option>
</select></label>
<label>Size<select id="mobile-font-size">
<label>Size<select class="app-select-input" id="mobile-font-size">
<option value="14">14</option>
<option value="16">16</option>
<option value="18">18</option>
+1 -1
View File
@@ -152,7 +152,7 @@
<div class="list-controls resources-controls">
<label class="list-search"><span class="sr-only">Search notes and workspaces</span><input id="resources-search"
type="search" placeholder="Search notes and workspaces…" autocomplete="off"></label>
<label class="page-size-label">Per page<select id="resources-per-page">
<label class="page-size-label">Per page<select class="app-select-input" id="resources-per-page">
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>
+3 -3
View File
@@ -233,12 +233,12 @@ async function loadResources() {
<div class="share-dialog-body">
<section class="share-section"><div class="share-section-head"><div><h4>People with access</h4><p>Invite existing RustPad users by e-mail or account/directory username.</p></div></div><form data-user-share-form class="share-form-grid">
<label class="share-emails"><span>E-mail or username</span><input name="recipients" type="text" placeholder="alice@example.com, jsmith" autocomplete="off"></label>
<label><span>Permission</span><select name="permission"><option value="ro">Read only</option><option value="rw">Read and write</option></select></label>
<label><span>Permission</span><select class="app-select-input" name="permission"><option value="ro">Read only</option><option value="rw">Read and write</option></select></label>
<button class="primary-button" type="submit">Grant access</button>
</form><div data-user-list class="share-list"></div></section>
<section class="share-section" data-direct-links-section><div class="share-section-head"><div><h4>Direct links</h4><p>Create links for people without an account or manage existing links.</p></div></div><div data-direct-links-enabled ${initialShareLinksEnabled ? "" : "hidden"}><form data-link-form class="share-form-grid share-link-form">
<label class="share-link-label"><span>Label</span><input name="label" type="text" maxlength="120" placeholder="Client ABC" autocomplete="off"></label>
<label><span>Permission</span><select name="permission"><option value="ro">Read only</option><option value="rw">Read and write</option></select></label>
<label><span>Permission</span><select class="app-select-input" name="permission"><option value="ro">Read only</option><option value="rw">Read and write</option></select></label>
<label><span>Valid for</span><div class="share-hours-field"><input name="hours" type="number" min="1" max="87600" value="24" inputmode="numeric"><span>hours</span></div></label>
<label class="share-forever"><input name="forever" type="checkbox"><span>Never expires</span></label>
<button class="primary-button" type="submit">Create link</button>
@@ -281,7 +281,7 @@ async function loadResources() {
const linkPreview = visibleLink
? `<div class="share-link-inline"><input type="text" readonly value="${escapeHtml(visibleLink)}" aria-label="New share link"><button class="secondary-button compact-button" type="button" data-copy-link>Copy</button></div><small class="share-link-once">The full address remains visible until this dialog is closed.</small>`
: `<small class="share-link-legacy">The full address is not stored. Use the label or link ID #${escapeHtml(identifier)} to identify it.</small>`;
return `<form class="share-list-row share-link-row" data-link-token-hash="${escapeHtml(link.token_hash)}"><div class="share-link-info"><label class="share-link-label-edit"><span>Label</span><input name="label" type="text" maxlength="120" value="${escapeHtml(link.label || "")}" placeholder="Unlabeled link" autocomplete="off"></label><small>Link ID #${escapeHtml(identifier)} · ${escapeHtml(formatShareCreated(link.created_at))} · ${escapeHtml(formatShareExpiry(link.expires_at))}</small>${linkPreview}</div><label><span class="sr-only">Permission</span><select name="permission" aria-label="Link permission"><option value="ro" ${link.permission === "ro" ? "selected" : ""}>Read only</option><option value="rw" ${link.permission === "rw" ? "selected" : ""}>Read and write</option></select></label><label><span class="sr-only">Validity in hours</span><div class="share-hours-field"><input name="hours" type="number" min="1" max="87600" value="24" aria-label="New validity in hours"><span>h</span></div></label><label class="share-forever"><input name="forever" type="checkbox" ${link.expires_at ? "" : "checked"}><span>Never</span></label><div class="share-row-actions"><button class="secondary-button compact-button" type="submit">Update</button><button class="danger-button compact-button" type="button" data-revoke-link>Revoke</button></div></form>`;
return `<form class="share-list-row share-link-row" data-link-token-hash="${escapeHtml(link.token_hash)}"><div class="share-link-info"><label class="share-link-label-edit"><span>Label</span><input name="label" type="text" maxlength="120" value="${escapeHtml(link.label || "")}" placeholder="Unlabeled link" autocomplete="off"></label><small>Link ID #${escapeHtml(identifier)} · ${escapeHtml(formatShareCreated(link.created_at))} · ${escapeHtml(formatShareExpiry(link.expires_at))}</small>${linkPreview}</div><label><span class="sr-only">Permission</span><select class="app-select-input" name="permission" aria-label="Link permission"><option value="ro" ${link.permission === "ro" ? "selected" : ""}>Read only</option><option value="rw" ${link.permission === "rw" ? "selected" : ""}>Read and write</option></select></label><label><span class="sr-only">Validity in hours</span><div class="share-hours-field"><input name="hours" type="number" min="1" max="87600" value="24" aria-label="New validity in hours"><span>h</span></div></label><label class="share-forever"><input name="forever" type="checkbox" ${link.expires_at ? "" : "checked"}><span>Never</span></label><div class="share-row-actions"><button class="secondary-button compact-button" type="submit">Update</button><button class="danger-button compact-button" type="button" data-revoke-link>Revoke</button></div></form>`;
}).join("") : '<p class="share-empty">No active links.</p>';
userList.querySelectorAll("[data-remove-user]").forEach(button => button.addEventListener("click", async () => { try { button.disabled = true; await api("/api/auth/resources/sharing", { method: "DELETE", headers: authHeaders(), body: JSON.stringify({ kind: item.kind, slug: item.slug, email: button.dataset.removeUser }) }); setDialogMessage("Access removed.", "success"); await refresh(); } catch (err) { setDialogMessage(err.message, "error"); button.disabled = false; } }));
linkList.querySelectorAll("[data-link-token-hash]").forEach(linkRow => {
+2 -2
View File
@@ -26,9 +26,9 @@ export async function prepareImageFile(file) {
<div class="image-editor-head"><div><h2>Adjust image</h2><p>Keep the whole image or choose a crop, then select output size.</p></div><button class="icon-button" value="cancel" aria-label="Close">×</button></div>
<div class="image-crop-stage"><canvas></canvas></div>
<div class="image-editor-controls">
<label>Crop<select data-aspect><option value="original" selected>Whole image</option><option value="free">Free</option><option value="1">Square</option><option value="1.333333">4:3</option><option value="1.777778">16:9</option></select></label>
<label>Crop<select class="app-select-input" data-aspect><option value="original" selected>Whole image</option><option value="free">Free</option><option value="1">Square</option><option value="1.333333">4:3</option><option value="1.777778">16:9</option></select></label>
<label>Zoom<input data-zoom type="range" min="1" max="3" value="1" step="0.01"></label>
<label>Max size<select data-size><option value="320">320 px</option><option value="480">480 px</option><option value="640">640 px</option><option value="800">800 px</option><option value="1000">1000 px</option><option value="1200">1200 px</option><option value="1600">1600 px</option><option value="2000">2000 px</option><option value="0" selected>Original</option></select></label>
<label>Max size<select class="app-select-input" data-size><option value="320">320 px</option><option value="480">480 px</option><option value="640">640 px</option><option value="800">800 px</option><option value="1000">1000 px</option><option value="1200">1200 px</option><option value="1600">1600 px</option><option value="2000">2000 px</option><option value="0" selected>Original</option></select></label>
</div>
<div class="image-editor-actions"><button class="secondary-button" value="cancel">Cancel</button><button type="button" class="primary-button" data-apply>Use image</button></div>
</form>`;
+5
View File
@@ -29,6 +29,7 @@ import { toast } from "@rustpad/toast";
import { formatDateTime, formatNumber, formatTime, t, translateSource } from "@rustpad/i18n";
import { getTheme } from "@rustpad/theme";
import { isResourceAccessError } from "@rustpad/security";
import { syncSelectControl } from "@rustpad/select";
import { loadHighlight, loadMediaPlayer, loadMermaid } from "@rustpad/vendor-libs";
export function startNoteEditor(adapter) {
@@ -169,6 +170,10 @@ export function startNoteEditor(adapter) {
if (mobilePreviewLineToggle) mobilePreviewLineToggle.checked = previewLineToggle.checked;
if (mobileCompactToggle) mobileCompactToggle.checked = compactToggle.checked;
if (mobileLineLinksToggle) mobileLineLinksToggle.checked = lineLinksToggle.checked;
syncSelectControl(fontFamily);
syncSelectControl(fontSize);
if (mobileFontFamily) syncSelectControl(mobileFontFamily);
if (mobileFontSize) syncSelectControl(mobileFontSize);
}
syncMobileEditorControls();
function updateAuthorshipControls() {
+25 -21
View File
@@ -1,6 +1,10 @@
/*
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
import { api } from "@rustpad/api";
@@ -28,7 +32,7 @@ function icon(name) {
}
function closeOtherPickers(except = null) {
document.querySelectorAll(".header-preference-picker[open]").forEach(details => {
document.querySelectorAll(".app-select--header[open]").forEach(details => {
if (details !== except) details.open = false;
});
}
@@ -36,19 +40,19 @@ function closeOtherPickers(except = null) {
function preferenceButton({ value, label, selected, languageCode }) {
const button = document.createElement("button");
button.type = "button";
button.className = "header-preference-option";
button.className = "app-select-option";
button.dataset.value = value;
button.setAttribute("role", "option");
button.setAttribute("aria-selected", selected ? "true" : "false");
if (languageCode) button.lang = languageCode;
const check = document.createElement("span");
check.className = "header-preference-option__check";
check.className = "app-select-option__check";
check.setAttribute("aria-hidden", "true");
check.textContent = selected ? "✓" : "";
const copy = document.createElement("span");
copy.className = "header-preference-option__copy";
copy.className = "app-select-option__copy";
copy.textContent = label;
button.append(check, copy);
@@ -57,29 +61,29 @@ function preferenceButton({ value, label, selected, languageCode }) {
function createPicker(kind) {
const details = document.createElement("details");
details.className = `header-preference-picker header-preference-picker--${kind}`;
details.className = `app-select app-select--header app-select--${kind}`;
details.dataset.preferenceKind = kind;
const summary = document.createElement("summary");
summary.className = "header-preference-trigger";
summary.className = "app-select-trigger";
const iconWrap = document.createElement("span");
iconWrap.className = "header-preference-trigger__icon";
iconWrap.className = "app-select-trigger__icon";
iconWrap.innerHTML = icon(kind === "theme" ? getTheme() : "language");
const compactValue = document.createElement("span");
compactValue.className = "header-preference-trigger__value";
compactValue.className = "app-select-trigger__value";
compactValue.setAttribute("aria-hidden", "true");
const chevron = document.createElement("span");
chevron.className = "header-preference-trigger__chevron";
chevron.className = "app-select-trigger__chevron";
chevron.setAttribute("aria-hidden", "true");
chevron.textContent = "▾";
summary.append(iconWrap, compactValue, chevron);
const menu = document.createElement("div");
menu.className = "header-preference-menu";
menu.className = "app-select-menu";
menu.setAttribute("role", "listbox");
details.append(summary, menu);
@@ -102,9 +106,9 @@ function renderLanguagePicker(details) {
const languages = getAvailableLanguages();
const current = getLanguage();
const selected = languages.find(language => language.code === current) || languages[0];
const summary = details.querySelector(".header-preference-trigger");
const value = details.querySelector(".header-preference-trigger__value");
const menu = details.querySelector(".header-preference-menu");
const summary = details.querySelector(".app-select-trigger");
const value = details.querySelector(".app-select-trigger__value");
const menu = details.querySelector(".app-select-menu");
const label = t("common.language", {}, "Language");
summary.setAttribute("aria-label", label);
@@ -121,10 +125,10 @@ function renderLanguagePicker(details) {
function renderThemePicker(details) {
const current = getTheme();
const summary = details.querySelector(".header-preference-trigger");
const iconWrap = details.querySelector(".header-preference-trigger__icon");
const value = details.querySelector(".header-preference-trigger__value");
const menu = details.querySelector(".header-preference-menu");
const summary = details.querySelector(".app-select-trigger");
const iconWrap = details.querySelector(".app-select-trigger__icon");
const value = details.querySelector(".app-select-trigger__value");
const menu = details.querySelector(".app-select-menu");
const label = t("profile.theme", {}, "Interface theme");
summary.setAttribute("aria-label", label);
@@ -145,12 +149,12 @@ function renderPicker(details) {
}
function renderAll() {
document.querySelectorAll(".header-preference-picker").forEach(renderPicker);
document.querySelectorAll(".app-select--header").forEach(renderPicker);
}
function bindPicker(details) {
details.querySelector(".header-preference-menu")?.addEventListener("click", async event => {
const button = event.target.closest(".header-preference-option");
details.querySelector(".app-select-menu")?.addEventListener("click", async event => {
const button = event.target.closest(".app-select-option");
if (!(button instanceof HTMLButtonElement)) return;
const next = button.dataset.value || "";
const kind = details.dataset.preferenceKind;
@@ -209,7 +213,7 @@ export function initHeaderPreferences() {
document.addEventListener("pointerdown", event => {
const target = event.target instanceof Element ? event.target : null;
if (!target?.closest(".header-preference-picker")) closeOtherPickers();
if (!target?.closest(".app-select--header")) closeOtherPickers();
}, { passive: true });
document.addEventListener("keydown", event => {
if (event.key === "Escape") closeOtherPickers();
+314
View File
@@ -0,0 +1,314 @@
/*
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
const states = new WeakMap();
let initialized = false;
let openState = null;
let observer = null;
function selectLabel(select) {
const direct = select.getAttribute("aria-label");
if (direct) return direct.trim();
if (select.id) {
const label = document.querySelector(`label[for="${CSS.escape(select.id)}"]`);
const text = label?.textContent?.trim();
if (text) return text;
}
const label = select.closest("label");
if (label) {
const clone = label.cloneNode(true);
clone.querySelectorAll("select, .app-select").forEach(node => node.remove());
const text = clone.textContent?.trim();
if (text) return text;
}
return select.name || select.id || "Select option";
}
function optionEntries(select) {
return [...select.options].map((option, index) => ({ option, index }));
}
function closeSelect(state, { focusTrigger = false } = {}) {
if (!state?.open) return;
state.open = false;
state.wrapper.classList.remove("is-open");
state.trigger.setAttribute("aria-expanded", "false");
state.menu.hidden = true;
if (openState === state) openState = null;
if (focusTrigger) state.trigger.focus();
}
function closeOtherSelects(except = null) {
if (openState && openState !== except) closeSelect(openState);
document.querySelectorAll(".app-select--header[open]").forEach(details => {
if (details !== except?.wrapper) details.open = false;
});
}
function menuRoot(select) {
return select.closest("dialog") || document.body;
}
function positionMenu(state) {
if (!state.open || state.menu.hidden || !state.trigger.isConnected) return;
const rect = state.trigger.getBoundingClientRect();
const viewportWidth = document.documentElement.clientWidth;
const viewportHeight = document.documentElement.clientHeight;
const gap = 6;
const edge = 8;
state.menu.style.minWidth = `${Math.ceil(rect.width)}px`;
state.menu.style.maxWidth = `${Math.max(180, viewportWidth - edge * 2)}px`;
state.menu.style.left = `${Math.max(edge, Math.min(rect.left, viewportWidth - state.menu.offsetWidth - edge))}px`;
const menuHeight = state.menu.offsetHeight;
const roomBelow = viewportHeight - rect.bottom - edge;
const roomAbove = rect.top - edge;
const openAbove = roomBelow < Math.min(menuHeight, 220) && roomAbove > roomBelow;
const top = openAbove
? Math.max(edge, rect.top - menuHeight - gap)
: Math.min(viewportHeight - menuHeight - edge, rect.bottom + gap);
state.menu.style.top = `${Math.max(edge, top)}px`;
}
function focusOption(state, direction = 1) {
const buttons = [...state.menu.querySelectorAll(".app-select-option:not(:disabled)")];
if (!buttons.length) return;
const current = document.activeElement;
const currentIndex = buttons.indexOf(current);
const selectedIndex = buttons.findIndex(button => button.getAttribute("aria-selected") === "true");
const base = currentIndex >= 0 ? currentIndex : selectedIndex;
const next = base < 0
? (direction > 0 ? 0 : buttons.length - 1)
: (base + direction + buttons.length) % buttons.length;
buttons[next].focus();
}
function renderOptions(state) {
const { select, menu } = state;
const fragment = document.createDocumentFragment();
for (const { option, index } of optionEntries(select)) {
const button = document.createElement("button");
button.type = "button";
button.className = "app-select-option";
button.dataset.optionIndex = String(index);
button.setAttribute("role", "option");
button.setAttribute("aria-selected", option.selected ? "true" : "false");
button.disabled = option.disabled || Boolean(option.parentElement?.disabled);
const check = document.createElement("span");
check.className = "app-select-option__check";
check.setAttribute("aria-hidden", "true");
check.textContent = option.selected ? "✓" : "";
const copy = document.createElement("span");
copy.className = "app-select-option__copy";
copy.textContent = option.label || option.textContent || option.value;
if (option.parentElement instanceof HTMLOptGroupElement) {
const group = document.createElement("span");
group.className = "app-select-option__group";
group.textContent = option.parentElement.label;
copy.prepend(group);
}
button.append(check, copy);
fragment.append(button);
}
menu.replaceChildren(fragment);
}
function syncSelect(state) {
const { select, trigger, value, menu, wrapper } = state;
const selected = select.selectedOptions[0] || select.options[select.selectedIndex] || select.options[0];
value.textContent = selected?.label || selected?.textContent || selected?.value || "";
trigger.disabled = select.disabled;
trigger.setAttribute("aria-disabled", select.disabled ? "true" : "false");
trigger.setAttribute("aria-label", `${selectLabel(select)}: ${value.textContent}`);
wrapper.classList.toggle("is-disabled", select.disabled);
renderOptions(state);
if (state.open) requestAnimationFrame(() => positionMenu(state));
}
function openSelect(state, { focusSelected = false } = {}) {
if (state.select.disabled || state.open) return;
closeOtherSelects(state);
state.open = true;
openState = state;
state.wrapper.classList.add("is-open");
state.trigger.setAttribute("aria-expanded", "true");
state.menu.hidden = false;
syncSelect(state);
positionMenu(state);
if (focusSelected) {
const selected = state.menu.querySelector('.app-select-option[aria-selected="true"]:not(:disabled)')
|| state.menu.querySelector(".app-select-option:not(:disabled)");
selected?.focus();
}
}
function toggleSelect(state) {
if (state.open) closeSelect(state);
else openSelect(state);
}
function enhanceSelect(select) {
if (!(select instanceof HTMLSelectElement)) return;
if (select.dataset.appSelectEnhanced === "true") return;
if (select.multiple || Number(select.size || 0) > 1 || select.dataset.appSelect === "native") return;
const wrapper = document.createElement("span");
wrapper.className = "app-select app-select--field";
const trigger = document.createElement("button");
trigger.type = "button";
trigger.className = "app-select-trigger";
trigger.setAttribute("aria-haspopup", "listbox");
trigger.setAttribute("aria-expanded", "false");
const value = document.createElement("span");
value.className = "app-select-trigger__value";
const chevron = document.createElement("span");
chevron.className = "app-select-trigger__chevron";
chevron.setAttribute("aria-hidden", "true");
chevron.textContent = "▾";
const menu = document.createElement("div");
menu.className = "app-select-menu app-select-menu--portal";
menu.setAttribute("role", "listbox");
menu.hidden = true;
trigger.append(value, chevron);
wrapper.append(trigger);
const rect = select.getBoundingClientRect();
if (rect.width) wrapper.style.minWidth = `${Math.ceil(rect.width)}px`;
select.insertAdjacentElement("afterend", wrapper);
menuRoot(select).append(menu);
select.dataset.appSelectEnhanced = "true";
select.classList.add("app-select-native");
select.tabIndex = -1;
const state = { select, wrapper, trigger, value, menu, open: false };
states.set(select, state);
trigger.addEventListener("click", event => {
event.preventDefault();
event.stopPropagation();
toggleSelect(state);
});
trigger.addEventListener("keydown", event => {
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
openSelect(state, { focusSelected: true });
if (event.key === "ArrowUp") focusOption(state, -1);
}
});
menu.addEventListener("click", event => {
const button = event.target.closest(".app-select-option");
if (!(button instanceof HTMLButtonElement) || button.disabled) return;
event.preventDefault();
event.stopPropagation();
const index = Number(button.dataset.optionIndex);
if (!Number.isInteger(index) || !select.options[index]) return;
const changed = select.selectedIndex !== index;
select.selectedIndex = index;
syncSelect(state);
closeSelect(state, { focusTrigger: true });
if (changed) {
select.dispatchEvent(new Event("input", { bubbles: true }));
select.dispatchEvent(new Event("change", { bubbles: true }));
}
});
menu.addEventListener("keydown", event => {
if (event.key === "ArrowDown" || event.key === "ArrowUp") {
event.preventDefault();
focusOption(state, event.key === "ArrowDown" ? 1 : -1);
return;
}
if (event.key === "Home" || event.key === "End") {
event.preventDefault();
const buttons = [...menu.querySelectorAll(".app-select-option:not(:disabled)")];
(event.key === "Home" ? buttons[0] : buttons.at(-1))?.focus();
return;
}
if (event.key === "Escape" || event.key === "Tab") {
closeSelect(state, { focusTrigger: event.key === "Escape" });
}
});
select.addEventListener("change", () => syncSelect(state));
select.addEventListener("rustpad:select-sync", () => syncSelect(state));
syncSelect(state);
}
function scan(root = document) {
if (root instanceof HTMLSelectElement) enhanceSelect(root);
root.querySelectorAll?.("select").forEach(enhanceSelect);
}
function syncMutation(mutation) {
const target = mutation.target instanceof Element ? mutation.target : mutation.target?.parentElement;
const select = target?.closest?.("select");
const state = select ? states.get(select) : null;
if (state) syncSelect(state);
}
export function syncSelectControl(select) {
const state = states.get(select);
if (state) syncSelect(state);
}
export function initSelectControls() {
scan(document);
if (initialized) return;
initialized = true;
observer = new MutationObserver(mutations => {
for (const mutation of mutations) {
mutation.addedNodes.forEach(node => {
if (node instanceof Element) scan(node);
});
if (mutation.type === "attributes" || mutation.target instanceof HTMLOptionElement || mutation.target instanceof HTMLOptGroupElement) syncMutation(mutation);
}
});
observer.observe(document.documentElement, {
subtree: true,
childList: true,
attributes: true,
attributeFilter: ["disabled", "aria-label", "label"],
});
document.addEventListener("pointerdown", event => {
const target = event.target instanceof Element ? event.target : null;
if (openState && !openState.wrapper.contains(target) && !openState.menu.contains(target)) closeSelect(openState);
}, { passive: true });
document.addEventListener("keydown", event => {
if (event.key === "Escape" && openState) closeSelect(openState, { focusTrigger: true });
});
window.addEventListener("resize", () => {
if (openState) positionMenu(openState);
}, { passive: true });
document.addEventListener("scroll", () => {
if (openState) positionMenu(openState);
}, { capture: true, passive: true });
}
+1
View File
@@ -6,6 +6,7 @@
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
const STORAGE_KEY = "rustpad:theme";
const GUEST_STORAGE_KEY = "rustpad:guest-theme";
const AUTH_STATE_KEY = "rustpad:auth-state";
+5 -1
View File
@@ -1,6 +1,10 @@
/*
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
* Source-Available Code / Dual-Licensed.
*
* Free for non-commercial and evaluation use under terms of BSL/GPLv3.
* Commercial or production use requires a valid paid license.
* See LICENSE file in repository root for details.
*/
import { t } from "@rustpad/i18n";
+1 -1
View File
@@ -45,7 +45,7 @@
<div class="notes-toolbar">
<label class="list-search"><span class="sr-only">Search notes</span><input id="notes-search" type="search"
placeholder="Search notes…" autocomplete="off"></label>
<label class="page-size-label">Per page<select id="notes-per-page">
<label class="page-size-label">Per page<select class="app-select-input" id="notes-per-page">
<option value="25">25</option>
<option value="50">50</option>
<option value="100">100</option>