select styled
This commit is contained in:
Generated
+1
-1
@@ -2581,7 +2581,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.65"
|
version = "0.2.66"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.2.65"
|
version = "0.2.66"
|
||||||
edition = "2024"
|
edition = "2024"
|
||||||
rust-version = "1.94"
|
rust-version = "1.94"
|
||||||
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
||||||
|
|||||||
+2
-1
@@ -42,6 +42,7 @@ const MODULES: &[&str] = &[
|
|||||||
"url-state",
|
"url-state",
|
||||||
"vendor-libs",
|
"vendor-libs",
|
||||||
"security",
|
"security",
|
||||||
|
"select",
|
||||||
];
|
];
|
||||||
|
|
||||||
pub fn render_html(
|
pub fn render_html(
|
||||||
@@ -152,7 +153,7 @@ impl<'a> AssetUrls<'a> {
|
|||||||
fn entrypoint(&self, name: &str) -> String {
|
fn entrypoint(&self, name: &str) -> String {
|
||||||
let entrypoint = self.url(&format!("js/{name}.js"));
|
let entrypoint = self.url(&format!("js/{name}.js"));
|
||||||
format!(
|
format!(
|
||||||
r#"<script type="module">import {{ initI18n }} from "@rustpad/i18n";import {{ initHeaderPreferences }} from "@rustpad/preferences";await initI18n();initHeaderPreferences();await import("{}");</script>"#,
|
r#"<script type="module">import {{ initI18n }} from "@rustpad/i18n";import {{ initHeaderPreferences }} from "@rustpad/preferences";import {{ initSelectControls }} from "@rustpad/select";await initI18n();initHeaderPreferences();await import("{}");initSelectControls();</script>"#,
|
||||||
escape_js_string(&entrypoint)
|
escape_js_string(&entrypoint)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
+158
-205
@@ -797,8 +797,7 @@ textarea:focus {
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-toolbar button,
|
.editor-toolbar button {
|
||||||
.editor-toolbar select {
|
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
@@ -808,18 +807,12 @@ textarea:focus {
|
|||||||
font-size: .78rem;
|
font-size: .78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-toolbar button:hover,
|
.editor-toolbar button:hover {
|
||||||
.editor-toolbar select:hover {
|
|
||||||
border-color: var(--border);
|
border-color: var(--border);
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-toolbar select {
|
|
||||||
border-color: var(--border);
|
|
||||||
background: var(--surface-raised);
|
|
||||||
}
|
|
||||||
|
|
||||||
.view-switch {
|
.view-switch {
|
||||||
padding: 3px;
|
padding: 3px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
@@ -832,7 +825,6 @@ textarea:focus {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Focus mode removes the toolbar row and leaves only compact floating controls. */
|
|
||||||
.pad-page .toolbar-collapse-toggle {
|
.pad-page .toolbar-collapse-toggle {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -1271,8 +1263,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.toast-region--modal {
|
.toast-region--modal {
|
||||||
/* A manual popover enters the browser top layer above the active dialog.
|
|
||||||
Reset popover defaults while keeping the regular toast viewport geometry. */
|
|
||||||
top: auto;
|
top: auto;
|
||||||
left: auto;
|
left: auto;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -1284,7 +1274,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.app-dialog.has-modal-toast-region {
|
.app-dialog.has-modal-toast-region {
|
||||||
/* Fallback for browsers without the Popover API. */
|
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1399,8 +1388,13 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes toast-countdown {
|
@keyframes toast-countdown {
|
||||||
from { transform: scaleX(1); }
|
from {
|
||||||
to { transform: scaleX(0); }
|
transform: scaleX(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
to {
|
||||||
|
transform: scaleX(0);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
@@ -1697,7 +1691,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Home: standalone note and workspace are separate choices. */
|
|
||||||
.home-layout--wide {
|
.home-layout--wide {
|
||||||
width: min(1040px, calc(100% - 32px));
|
width: min(1040px, calc(100% - 32px));
|
||||||
}
|
}
|
||||||
@@ -1743,7 +1736,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Collaborative editor additions */
|
|
||||||
.editor-shell {
|
.editor-shell {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: auto minmax(0, 1fr);
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
@@ -2008,7 +2000,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Layout safeguards and home footer */
|
|
||||||
.app-header__main,
|
.app-header__main,
|
||||||
.document-heading {
|
.document-heading {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -2074,7 +2065,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact line numbers; author labels sit over the edited text, not in the gutter. */
|
|
||||||
.line-gutter {
|
.line-gutter {
|
||||||
width: 48px;
|
width: 48px;
|
||||||
}
|
}
|
||||||
@@ -2124,7 +2114,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Editor display preferences and author overlay */
|
|
||||||
.editor-shell {
|
.editor-shell {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -2155,10 +2144,10 @@ dialog::backdrop {
|
|||||||
font-size: .72rem;
|
font-size: .72rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.editor-controls select {
|
.editor-controls .app-select--field .app-select-trigger {
|
||||||
width: auto;
|
|
||||||
min-height: 30px;
|
min-height: 30px;
|
||||||
padding: 3px 24px 3px 7px;
|
height: 30px;
|
||||||
|
padding: 3px 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
font-size: .74rem;
|
font-size: .74rem;
|
||||||
}
|
}
|
||||||
@@ -2230,7 +2219,6 @@ dialog::backdrop {
|
|||||||
word-break: normal;
|
word-break: normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep line numbers exactly aligned with the logical text lines. */
|
|
||||||
.line-gutter {
|
.line-gutter {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
font-size: var(--editor-font-size, 18px);
|
font-size: var(--editor-font-size, 18px);
|
||||||
@@ -2247,7 +2235,6 @@ dialog::backdrop {
|
|||||||
font-size: calc(var(--editor-font-size, 18px) - 2px);
|
font-size: calc(var(--editor-font-size, 18px) - 2px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Image crop/resize dialog. */
|
|
||||||
.image-editor-dialog {
|
.image-editor-dialog {
|
||||||
width: min(920px, calc(100% - 28px));
|
width: min(920px, calc(100% - 28px));
|
||||||
max-width: none;
|
max-width: none;
|
||||||
@@ -2329,42 +2316,17 @@ dialog::backdrop {
|
|||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-editor-controls select,
|
.image-editor-controls .app-select--field,
|
||||||
.image-editor-controls input {
|
.image-editor-controls input {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-editor-controls select {
|
.image-editor-controls .app-select--field .app-select-trigger {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 0 36px 0 12px;
|
height: 40px;
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
border-radius: 9px;
|
border-radius: 9px;
|
||||||
background-color: var(--control-bg);
|
|
||||||
background-image: linear-gradient(45deg, transparent 50%, var(--select-arrow) 50%),
|
|
||||||
linear-gradient(135deg, var(--select-arrow) 50%, transparent 50%);
|
|
||||||
background-position: calc(100% - 16px) 17px, calc(100% - 11px) 17px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 5px 5px, 5px 5px;
|
|
||||||
box-shadow: inset 0 1px 0 var(--wash-soft);
|
|
||||||
color: var(--text);
|
|
||||||
font-size: .84rem;
|
font-size: .84rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-editor-controls select:hover {
|
|
||||||
border-color: var(--select-border);
|
|
||||||
background-color: var(--surface-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.image-editor-controls select:focus,
|
|
||||||
.image-editor-controls select:focus-visible {
|
|
||||||
border-color: var(--focus);
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 3px var(--focus-ring), inset 0 1px 0 var(--wash-soft);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.image-editor-controls input[type="range"] {
|
.image-editor-controls input[type="range"] {
|
||||||
@@ -2470,7 +2432,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Wider published page workspace. */
|
|
||||||
.public-header {
|
.public-header {
|
||||||
padding-inline: max(20px, calc((100vw - 1180px) / 2));
|
padding-inline: max(20px, calc((100vw - 1180px) / 2));
|
||||||
}
|
}
|
||||||
@@ -2492,7 +2453,6 @@ dialog::backdrop {
|
|||||||
padding: clamp(24px, 4vw, 52px);
|
padding: clamp(24px, 4vw, 52px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Preview and attachment management */
|
|
||||||
.preview {
|
.preview {
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
@@ -2919,7 +2879,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Choice shown when a video file is uploaded. */
|
|
||||||
.video-insert-dialog {
|
.video-insert-dialog {
|
||||||
width: min(560px, calc(100vw - 24px));
|
width: min(560px, calc(100vw - 24px));
|
||||||
max-width: 560px;
|
max-width: 560px;
|
||||||
@@ -3238,7 +3197,6 @@ dialog::backdrop {
|
|||||||
cursor: default;
|
cursor: default;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Preview editing and source line numbers. */
|
|
||||||
.preview {
|
.preview {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding: 12px 24px 12px 62px;
|
padding: 12px 24px 12px 62px;
|
||||||
@@ -3361,7 +3319,6 @@ dialog::backdrop {
|
|||||||
color: var(--text-subtle);
|
color: var(--text-subtle);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Task-list layout. Keep the checkbox in the same marker gutter as a bullet. */
|
|
||||||
.markdown-body .task-list-item {
|
.markdown-body .task-list-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
@@ -3755,7 +3712,6 @@ dialog::backdrop {
|
|||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Account footer and authentication dialog. */
|
|
||||||
.home-footer {
|
.home-footer {
|
||||||
width: min(1040px, calc(100% - 32px));
|
width: min(1040px, calc(100% - 32px));
|
||||||
margin: auto auto 24px;
|
margin: auto auto 24px;
|
||||||
@@ -4142,7 +4098,6 @@ dialog::backdrop {
|
|||||||
background: var(--danger-hover);
|
background: var(--danger-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Native hidden must win over component display declarations. */
|
|
||||||
[hidden] {
|
[hidden] {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -4364,7 +4319,6 @@ dialog::backdrop {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stable scrolling and collapsible Markdown sections. */
|
|
||||||
.editor-shell {
|
.editor-shell {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
@@ -4419,7 +4373,6 @@ dialog::backdrop {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Full-height editor: keep header/footer visible and scroll only the work area. */
|
|
||||||
.pad-page {
|
.pad-page {
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
@@ -4475,7 +4428,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* A collapsible section has one source-line marker; its rendered Markdown does not repeat line numbers inside. */
|
|
||||||
.markdown-details__content .preview-source-line::before {
|
.markdown-details__content .preview-source-line::before {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -4488,7 +4440,6 @@ dialog::backdrop {
|
|||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ephemeral room presence and chat */
|
|
||||||
.room-details {
|
.room-details {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
@@ -4685,7 +4636,6 @@ dialog::backdrop {
|
|||||||
filter: brightness(1.15);
|
filter: brightness(1.15);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Sharing panel */
|
|
||||||
.share-panel {
|
.share-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 18px;
|
gap: 18px;
|
||||||
@@ -4783,7 +4733,7 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.share-link-row input,
|
.share-link-row input,
|
||||||
.share-link-row select {
|
.share-link-row .app-select--field {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4811,7 +4761,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Share dialog */
|
|
||||||
.share-dialog {
|
.share-dialog {
|
||||||
width: min(900px, calc(100% - 28px));
|
width: min(900px, calc(100% - 28px));
|
||||||
max-width: 900px;
|
max-width: 900px;
|
||||||
@@ -4904,30 +4853,13 @@ dialog::backdrop {
|
|||||||
font-size: .76rem;
|
font-size: .76rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-dialog select {
|
.share-dialog .app-select--field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.share-dialog .app-select--field .app-select-trigger {
|
||||||
min-height: 42px;
|
min-height: 42px;
|
||||||
padding: 0 38px 0 12px;
|
height: 42px;
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
border-radius: 8px;
|
|
||||||
background-color: var(--surface-toolbar);
|
|
||||||
color: var(--text);
|
|
||||||
appearance: none;
|
|
||||||
background-image: linear-gradient(45deg, transparent 50%, var(--select-arrow) 50%), linear-gradient(135deg, var(--select-arrow) 50%, transparent 50%);
|
|
||||||
background-position: calc(100% - 16px) 17px, calc(100% - 11px) 17px;
|
|
||||||
background-size: 5px 5px, 5px 5px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-dialog select:hover {
|
|
||||||
border-color: var(--select-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.share-dialog select:focus {
|
|
||||||
border-color: var(--focus);
|
|
||||||
box-shadow: 0 0 0 3px var(--focus-ring);
|
|
||||||
outline: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-dialog input[type="text"],
|
.share-dialog input[type="text"],
|
||||||
@@ -5043,7 +4975,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Refined sharing dialog and shared-resource attribution. */
|
|
||||||
.share-dialog {
|
.share-dialog {
|
||||||
border: 1px solid var(--border-strong);
|
border: 1px solid var(--border-strong);
|
||||||
border-radius: 18px;
|
border-radius: 18px;
|
||||||
@@ -5202,7 +5133,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Markdown alerts. */
|
|
||||||
.markdown-body .markdown-alert {
|
.markdown-body .markdown-alert {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
padding: .8em 1em;
|
padding: .8em 1em;
|
||||||
@@ -5239,7 +5169,6 @@ dialog::backdrop {
|
|||||||
background: var(--status-danger-bg);
|
background: var(--status-danger-bg);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fenced code line numbers for every language: ```lang=, ```lang=101, ```= or ```=101. */
|
|
||||||
.markdown-body pre.code-with-lines code {
|
.markdown-body pre.code-with-lines code {
|
||||||
counter-reset: none;
|
counter-reset: none;
|
||||||
}
|
}
|
||||||
@@ -5264,7 +5193,6 @@ dialog::backdrop {
|
|||||||
user-select: none;
|
user-select: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Generated table of contents. */
|
|
||||||
.markdown-body .markdown-toc {
|
.markdown-body .markdown-toc {
|
||||||
margin: 1em 0;
|
margin: 1em 0;
|
||||||
padding: .8em 1em;
|
padding: .8em 1em;
|
||||||
@@ -5278,7 +5206,6 @@ dialog::backdrop {
|
|||||||
padding-left: 1.35em;
|
padding-left: 1.35em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Top-level entries are already numbered by their headings, so omit redundant bullets. */
|
|
||||||
.markdown-body .markdown-toc>ul {
|
.markdown-body .markdown-toc>ul {
|
||||||
padding-left: 0;
|
padding-left: 0;
|
||||||
}
|
}
|
||||||
@@ -5303,7 +5230,6 @@ dialog::backdrop {
|
|||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Nested Markdown lists keep markers and source-line numbers in separate gutters. */
|
|
||||||
.markdown-body ul,
|
.markdown-body ul,
|
||||||
.markdown-body ol {
|
.markdown-body ol {
|
||||||
padding-left: 1.75em;
|
padding-left: 1.75em;
|
||||||
@@ -5323,7 +5249,6 @@ dialog::backdrop {
|
|||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the editor gutter inside the mobile workspace. */
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.pad-page .editor-shell {
|
.pad-page .editor-shell {
|
||||||
grid-template-columns: 38px minmax(0, 1fr);
|
grid-template-columns: 38px minmax(0, 1fr);
|
||||||
@@ -5518,7 +5443,6 @@ dialog::backdrop {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Per-character authorship overlay. The textarea remains the editable surface. */
|
|
||||||
.authorship-layer {
|
.authorship-layer {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
@@ -5560,7 +5484,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Multiple authors can contribute to one line. */
|
|
||||||
.owner-label-group {
|
.owner-label-group {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
right: calc(var(--editor-rendered-font-size, 14px) * .57);
|
right: calc(var(--editor-rendered-font-size, 14px) * .57);
|
||||||
@@ -5581,12 +5504,10 @@ dialog::backdrop {
|
|||||||
transform: none;
|
transform: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep public TOC targets visible below the sticky page header. */
|
|
||||||
.public-content :is(h1, h2, h3, h4, h5, h6)[id] {
|
.public-content :is(h1, h2, h3, h4, h5, h6)[id] {
|
||||||
scroll-margin-top: 88px;
|
scroll-margin-top: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shared semantic action buttons. Keep legacy class names as aliases. */
|
|
||||||
.action-button,
|
.action-button,
|
||||||
.primary-button,
|
.primary-button,
|
||||||
.secondary-button,
|
.secondary-button,
|
||||||
@@ -5964,8 +5885,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the view controls visible on Full HD displays using 125% scaling.
|
|
||||||
Only the formatting group becomes horizontally scrollable. */
|
|
||||||
@media (min-width: 1500px) and (max-width: 1699px) {
|
@media (min-width: 1500px) and (max-width: 1699px) {
|
||||||
.pad-page .editor-toolbar {
|
.pad-page .editor-toolbar {
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
@@ -5995,7 +5914,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Responsive note editor -------------------------------------------------- */
|
|
||||||
@media (max-width: 1499px) {
|
@media (max-width: 1499px) {
|
||||||
.pad-page {
|
.pad-page {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -6329,8 +6247,7 @@ dialog::backdrop {
|
|||||||
padding: 6px;
|
padding: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.pad-page .editor-toolbar button,
|
.pad-page .editor-toolbar button {
|
||||||
.pad-page .editor-toolbar select {
|
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
padding-inline: 8px;
|
padding-inline: 8px;
|
||||||
}
|
}
|
||||||
@@ -6357,7 +6274,6 @@ dialog::backdrop {
|
|||||||
cursor: not-allowed;
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Per-note author color override */
|
|
||||||
.use-global-color {
|
.use-global-color {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
width: 1.75rem;
|
width: 1.75rem;
|
||||||
@@ -6376,7 +6292,6 @@ dialog::backdrop {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Quick actions appear when the editor header switches to its compact menu. */
|
|
||||||
.mobile-editor-bubble {
|
.mobile-editor-bubble {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -6501,7 +6416,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Movable quick actions for compact editor screens. */
|
|
||||||
@media (max-width: 1499px) {
|
@media (max-width: 1499px) {
|
||||||
.pad-page .mobile-editor-bubble {
|
.pad-page .mobile-editor-bubble {
|
||||||
user-select: none;
|
user-select: none;
|
||||||
@@ -6563,7 +6477,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Authorship display modes */
|
|
||||||
.editor-column-label {
|
.editor-column-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -6643,7 +6556,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the whole editor surface consistent in Simple and Full modes. */
|
|
||||||
.editor-shell {
|
.editor-shell {
|
||||||
background: var(--surface-inset);
|
background: var(--surface-inset);
|
||||||
}
|
}
|
||||||
@@ -6666,7 +6578,6 @@ dialog::backdrop {
|
|||||||
z-index: 5;
|
z-index: 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Stable editor canvas in both authorship modes. */
|
|
||||||
.pad-page .editor-column,
|
.pad-page .editor-column,
|
||||||
.pad-page .editor-shell {
|
.pad-page .editor-shell {
|
||||||
background: var(--surface-inset);
|
background: var(--surface-inset);
|
||||||
@@ -6690,7 +6601,6 @@ dialog::backdrop {
|
|||||||
background: transparent !important;
|
background: transparent !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* The authorship canvas must paint the whole editable area, not only text rows. */
|
|
||||||
.pad-page .authorship-layer {
|
.pad-page .authorship-layer {
|
||||||
width: auto;
|
width: auto;
|
||||||
height: auto;
|
height: auto;
|
||||||
@@ -6714,7 +6624,6 @@ dialog::backdrop {
|
|||||||
line-height: 1.45;
|
line-height: 1.45;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Full-height editor/gutter separator, independent of the number of text lines. */
|
|
||||||
.pad-page .editor-shell::after {
|
.pad-page .editor-shell::after {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 6;
|
z-index: 6;
|
||||||
@@ -6744,9 +6653,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fill the editor column with the actual editor shell.
|
|
||||||
The grid version could size the shell to its content in Full authorship mode,
|
|
||||||
so the gutter separator stopped after the last rendered line. */
|
|
||||||
.pad-page .editor-column {
|
.pad-page .editor-column {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -6765,7 +6671,6 @@ dialog::backdrop {
|
|||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Published page action and settings in one dropdown. */
|
|
||||||
.page-settings {
|
.page-settings {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@@ -6880,7 +6785,6 @@ dialog::backdrop {
|
|||||||
background: var(--surface-hover);
|
background: var(--surface-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep the Simple/Full switch inside the editor label frame. */
|
|
||||||
.authorship-mode-control {
|
.authorship-mode-control {
|
||||||
padding: 1px;
|
padding: 1px;
|
||||||
border-radius: 7px;
|
border-radius: 7px;
|
||||||
@@ -6894,7 +6798,6 @@ dialog::backdrop {
|
|||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Final UI fixes: opaque Page settings and per-note authorship controls. */
|
|
||||||
.page-settings-menu {
|
.page-settings-menu {
|
||||||
background: var(--surface-card);
|
background: var(--surface-card);
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
@@ -6947,7 +6850,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Shared editor display settings */
|
|
||||||
.authorship-controls {
|
.authorship-controls {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -7020,7 +6922,6 @@ dialog::backdrop {
|
|||||||
white-space: pre-wrap;
|
white-space: pre-wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep generated individual links below the new-link form. */
|
|
||||||
.share-link-list-wrap {
|
.share-link-list-wrap {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
@@ -7080,7 +6981,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Upload progress toast --------------------------------------------------- */
|
|
||||||
.toast-card--upload {
|
.toast-card--upload {
|
||||||
grid-template-columns: minmax(0, 1fr) 28px;
|
grid-template-columns: minmax(0, 1fr) 28px;
|
||||||
}
|
}
|
||||||
@@ -7133,9 +7033,17 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@keyframes upload-toast-indeterminate {
|
@keyframes upload-toast-indeterminate {
|
||||||
0% { transform: translateX(-120%); }
|
0% {
|
||||||
50% { transform: translateX(180%); }
|
transform: translateX(-120%);
|
||||||
100% { transform: translateX(420%); }
|
}
|
||||||
|
|
||||||
|
50% {
|
||||||
|
transform: translateX(180%);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
transform: translateX(420%);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.upload-toast__meta {
|
.upload-toast__meta {
|
||||||
@@ -7195,7 +7103,6 @@ dialog::backdrop {
|
|||||||
border-color: color-mix(in srgb, var(--accent) 55%, var(--border-strong)) !important;
|
border-color: color-mix(in srgb, var(--accent) 55%, var(--border-strong)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Optional links to exact editor lines. */
|
|
||||||
.line-number-button {
|
.line-number-button {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -7233,7 +7140,6 @@ dialog::backdrop {
|
|||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Personal editor preferences in the compact mobile action bar. */
|
|
||||||
@media (max-width: 1499px) {
|
@media (max-width: 1499px) {
|
||||||
.mobile-editor-options {
|
.mobile-editor-options {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -7288,14 +7194,17 @@ dialog::backdrop {
|
|||||||
font-size: .78rem;
|
font-size: .78rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mobile-editor-options__panel select {
|
.mobile-editor-options__panel .app-select--field {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-editor-options__panel .app-select--field .app-select-trigger {
|
||||||
min-height: 34px;
|
min-height: 34px;
|
||||||
|
height: 34px;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Note editor polish: clearer actions, lighter canvas, and aligned split columns. */
|
|
||||||
.pad-page .header-actions>.secondary-button:not(.danger-button),
|
.pad-page .header-actions>.secondary-button:not(.danger-button),
|
||||||
.pad-page .page-settings>summary.secondary-button {
|
.pad-page .page-settings>summary.secondary-button {
|
||||||
border-color: var(--note-action-border);
|
border-color: var(--note-action-border);
|
||||||
@@ -7395,7 +7304,6 @@ dialog::backdrop {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Unified RustPad resource identity used by notes and workspaces. */
|
|
||||||
.resource-brand {
|
.resource-brand {
|
||||||
display: grid;
|
display: grid;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@@ -7464,7 +7372,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Search, pagination and theme-aware scrollbars. */
|
|
||||||
* {
|
* {
|
||||||
scrollbar-width: thin;
|
scrollbar-width: thin;
|
||||||
scrollbar-color: var(--border-strong) var(--surface-inset);
|
scrollbar-color: var(--border-strong) var(--surface-inset);
|
||||||
@@ -7520,36 +7427,14 @@ dialog::backdrop {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-size-label select {
|
.page-size-label .app-select--field {
|
||||||
min-width: 78px;
|
min-width: 78px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-size-label .app-select--field .app-select-trigger {
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
padding: 0 34px 0 12px;
|
height: 40px;
|
||||||
border: 1px solid var(--border-strong);
|
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
background-color: var(--surface-toolbar);
|
|
||||||
background-image: linear-gradient(45deg, transparent 50%, var(--select-arrow) 50%),
|
|
||||||
linear-gradient(135deg, var(--select-arrow) 50%, transparent 50%);
|
|
||||||
background-position: calc(100% - 16px) 16px, calc(100% - 11px) 16px;
|
|
||||||
background-repeat: no-repeat;
|
|
||||||
background-size: 5px 5px, 5px 5px;
|
|
||||||
box-shadow: inset 0 1px 0 var(--wash-soft);
|
|
||||||
color: var(--text);
|
|
||||||
font-weight: 700;
|
|
||||||
appearance: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
cursor: pointer;
|
|
||||||
transition: border-color .15s ease, background-color .15s ease, box-shadow .15s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-size-label select:hover {
|
|
||||||
border-color: var(--select-border);
|
|
||||||
background-color: var(--surface-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.page-size-label select:focus-visible {
|
|
||||||
border-color: var(--focus);
|
|
||||||
outline: none;
|
|
||||||
box-shadow: 0 0 0 3px var(--focus-ring), inset 0 1px 0 var(--wash-soft);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.page-size-label:focus-within {
|
.page-size-label:focus-within {
|
||||||
@@ -7591,8 +7476,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Share links: keep one-time URLs readable without storing plaintext tokens. */
|
|
||||||
|
|
||||||
.share-link-once {
|
.share-link-once {
|
||||||
color: var(--muted-2) !important;
|
color: var(--muted-2) !important;
|
||||||
}
|
}
|
||||||
@@ -7601,7 +7484,6 @@ dialog::backdrop {
|
|||||||
color: var(--warning);
|
color: var(--warning);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Keep editor mode/view controls inside the viewport at browser zoom. */
|
|
||||||
.control-label-short {
|
.control-label-short {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -7674,7 +7556,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* History content wraps vertically and never creates a horizontal scrollbar. */
|
|
||||||
.history-panel {
|
.history-panel {
|
||||||
max-width: 100vw;
|
max-width: 100vw;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
@@ -7838,7 +7719,6 @@ dialog::backdrop {
|
|||||||
font-size: .68rem;
|
font-size: .68rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Mobile upload action shown directly below the formatting controls. */
|
|
||||||
.mobile-upload-button {
|
.mobile-upload-button {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
@@ -7906,16 +7786,10 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Published-note permalinks ------------------------------------------------ */
|
|
||||||
.public-content .preview-source-line {
|
.public-content .preview-source-line {
|
||||||
scroll-margin-top: 88px;
|
scroll-margin-top: 88px;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Line linking is opt-in on published notes. When regular line numbers are
|
|
||||||
* hidden, enabling links restores the same gutter so link targets stay inside
|
|
||||||
* the document instead of protruding past its left edge.
|
|
||||||
*/
|
|
||||||
.public-page .public-content {
|
.public-page .public-content {
|
||||||
transition: padding-left .18s ease;
|
transition: padding-left .18s ease;
|
||||||
}
|
}
|
||||||
@@ -7924,11 +7798,6 @@ dialog::backdrop {
|
|||||||
padding-left: clamp(24px, 4vw, 52px);
|
padding-left: clamp(24px, 4vw, 52px);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* The permalink occupies the same gutter slot as the regular source number.
|
|
||||||
* It stays invisible until the row is approached, then behaves like the
|
|
||||||
* clickable editor line number and remains visible briefly after copying.
|
|
||||||
*/
|
|
||||||
.public-content .public-permalink {
|
.public-content .public-permalink {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
@@ -7992,7 +7861,8 @@ dialog::backdrop {
|
|||||||
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 24%, transparent);
|
box-shadow: 0 0 0 2px color-mix(in srgb, var(--accent) 24%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: none), (pointer: coarse) {
|
@media (hover: none),
|
||||||
|
(pointer: coarse) {
|
||||||
.public-page.line-links-enabled .public-content .public-permalink {
|
.public-page.line-links-enabled .public-content .public-permalink {
|
||||||
opacity: .62;
|
opacity: .62;
|
||||||
transform: none;
|
transform: none;
|
||||||
@@ -8009,7 +7879,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Per-user, per-note navigation-bar focus mode ---------------------------- */
|
|
||||||
.pad-page .navbar-collapse-toggle {
|
.pad-page .navbar-collapse-toggle {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
top: max(3px, env(safe-area-inset-top));
|
top: max(3px, env(safe-area-inset-top));
|
||||||
@@ -8094,7 +7963,8 @@ dialog::backdrop {
|
|||||||
flex: 1 1 0;
|
flex: 1 1 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (hover: none), (pointer: coarse) {
|
@media (hover: none),
|
||||||
|
(pointer: coarse) {
|
||||||
.pad-page .navbar-collapse-toggle {
|
.pad-page .navbar-collapse-toggle {
|
||||||
width: 32px;
|
width: 32px;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -8103,7 +7973,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Compact interface preferences in application headers. */
|
|
||||||
.header-preferences {
|
.header-preferences {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -8111,20 +7980,20 @@ dialog::backdrop {
|
|||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-picker {
|
.app-select {
|
||||||
position: relative;
|
position: relative;
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-picker > summary {
|
.app-select>summary {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-picker > summary::-webkit-details-marker {
|
.app-select>summary::-webkit-details-marker {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-trigger {
|
.app-select-trigger {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
min-width: 38px;
|
min-width: 38px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
@@ -8141,19 +8010,25 @@ dialog::backdrop {
|
|||||||
transition: background-color .14s ease, border-color .14s ease, color .14s ease, box-shadow .14s ease;
|
transition: background-color .14s ease, border-color .14s ease, color .14s ease, box-shadow .14s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-trigger:hover,
|
.app-select-trigger:hover,
|
||||||
.header-preference-picker[open] > .header-preference-trigger {
|
.app-select[open]>.app-select-trigger,
|
||||||
|
.app-select.is-open>.app-select-trigger {
|
||||||
border-color: var(--border-strong);
|
border-color: var(--border-strong);
|
||||||
background: var(--surface-3);
|
background: var(--surface-3);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-trigger:focus-visible {
|
.app-select-trigger:focus-visible {
|
||||||
outline: 2px solid var(--focus);
|
outline: 2px solid var(--focus);
|
||||||
outline-offset: 2px;
|
outline-offset: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-trigger__icon {
|
.app-select-trigger:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: .6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-select-trigger__icon {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 17px;
|
width: 17px;
|
||||||
height: 17px;
|
height: 17px;
|
||||||
@@ -8161,7 +8036,7 @@ dialog::backdrop {
|
|||||||
place-items: center;
|
place-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-trigger__icon svg {
|
.app-select-trigger__icon svg {
|
||||||
width: 17px;
|
width: 17px;
|
||||||
height: 17px;
|
height: 17px;
|
||||||
fill: none;
|
fill: none;
|
||||||
@@ -8171,7 +8046,7 @@ dialog::backdrop {
|
|||||||
stroke-width: 1.7;
|
stroke-width: 1.7;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-trigger__value {
|
.app-select-trigger__value {
|
||||||
max-width: 72px;
|
max-width: 72px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
@@ -8182,24 +8057,25 @@ dialog::backdrop {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-picker--language .header-preference-trigger__value {
|
.app-select--language .app-select-trigger__value {
|
||||||
min-width: 18px;
|
min-width: 18px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
letter-spacing: .04em;
|
letter-spacing: .04em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-trigger__chevron {
|
.app-select-trigger__chevron {
|
||||||
color: var(--muted-2);
|
color: var(--muted-2);
|
||||||
font-size: .68rem;
|
font-size: .68rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
transition: transform .14s ease;
|
transition: transform .14s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-picker[open] .header-preference-trigger__chevron {
|
.app-select[open] .app-select-trigger__chevron,
|
||||||
|
.app-select.is-open .app-select-trigger__chevron {
|
||||||
transform: rotate(180deg);
|
transform: rotate(180deg);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-menu {
|
.app-select-menu {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: calc(100% + 7px);
|
top: calc(100% + 7px);
|
||||||
right: 0;
|
right: 0;
|
||||||
@@ -8216,11 +8092,15 @@ dialog::backdrop {
|
|||||||
box-shadow: 0 14px 34px var(--shadow-38), inset 0 1px 0 var(--wash-soft);
|
box-shadow: 0 14px 34px var(--shadow-38), inset 0 1px 0 var(--wash-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-picker--language .header-preference-menu {
|
.app-select-menu[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-select--language .app-select-menu {
|
||||||
min-width: 190px;
|
min-width: 190px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-option {
|
.app-select-option {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 18px minmax(0, 1fr);
|
grid-template-columns: 18px minmax(0, 1fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -8235,19 +8115,24 @@ dialog::backdrop {
|
|||||||
text-align: left;
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-option:hover,
|
.app-select-option:hover,
|
||||||
.header-preference-option:focus-visible {
|
.app-select-option:focus-visible {
|
||||||
border-color: var(--border-strong);
|
border-color: var(--border-strong);
|
||||||
outline: 0;
|
outline: 0;
|
||||||
background: var(--surface-hover);
|
background: var(--surface-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-option[aria-selected="true"] {
|
.app-select-option[aria-selected="true"] {
|
||||||
border-color: var(--accent-a35);
|
border-color: var(--accent-a35);
|
||||||
background: var(--accent-soft);
|
background: var(--accent-soft);
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-option__check {
|
.app-select-option:disabled {
|
||||||
|
cursor: default;
|
||||||
|
opacity: .48;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-select-option__check {
|
||||||
display: grid;
|
display: grid;
|
||||||
width: 18px;
|
width: 18px;
|
||||||
height: 18px;
|
height: 18px;
|
||||||
@@ -8257,7 +8142,7 @@ dialog::backdrop {
|
|||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-option__copy {
|
.app-select-option__copy {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
font-size: .78rem;
|
font-size: .78rem;
|
||||||
font-weight: 650;
|
font-weight: 650;
|
||||||
@@ -8265,19 +8150,87 @@ dialog::backdrop {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.app-select-option__group {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 1px;
|
||||||
|
color: var(--muted-2);
|
||||||
|
font-size: .65rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-select-native {
|
||||||
|
position: absolute !important;
|
||||||
|
width: 1px !important;
|
||||||
|
height: 1px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin: -1px !important;
|
||||||
|
overflow: hidden !important;
|
||||||
|
clip: rect(0 0 0 0) !important;
|
||||||
|
clip-path: inset(50%) !important;
|
||||||
|
border: 0 !important;
|
||||||
|
white-space: nowrap !important;
|
||||||
|
opacity: 0 !important;
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-select--field {
|
||||||
|
display: inline-block;
|
||||||
|
max-width: 100%;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-select--field .app-select-trigger {
|
||||||
|
width: 100%;
|
||||||
|
min-width: 0;
|
||||||
|
min-height: 40px;
|
||||||
|
height: 40px;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 0 11px 0 12px;
|
||||||
|
border-color: var(--border-strong);
|
||||||
|
background: var(--surface-toolbar);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: .82rem;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-select--field .app-select-trigger__value {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
max-width: none;
|
||||||
|
color: inherit;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-select-menu--portal {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
right: auto;
|
||||||
|
left: 0;
|
||||||
|
z-index: 10000;
|
||||||
|
min-width: 0;
|
||||||
|
max-height: min(320px, calc(100dvh - 16px));
|
||||||
|
overflow: auto;
|
||||||
|
overscroll-behavior: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-select-menu--portal .app-select-option__copy {
|
||||||
|
max-width: min(320px, calc(100vw - 72px));
|
||||||
|
}
|
||||||
|
|
||||||
.home-header {
|
.home-header {
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 720px) {
|
@media (max-width: 720px) {
|
||||||
.header-preference-trigger {
|
.app-select--header>.app-select-trigger {
|
||||||
min-width: 36px;
|
min-width: 36px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
padding-inline: 8px;
|
padding-inline: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-picker--theme .header-preference-trigger__value,
|
.app-select--header.app-select--theme .app-select-trigger__value,
|
||||||
.header-preference-trigger__chevron {
|
.app-select--header .app-select-trigger__chevron {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -8298,18 +8251,18 @@ dialog::backdrop {
|
|||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-trigger {
|
.app-select--header>.app-select-trigger {
|
||||||
width: 34px;
|
width: 34px;
|
||||||
min-width: 34px;
|
min-width: 34px;
|
||||||
height: 34px;
|
height: 34px;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-trigger__value {
|
.app-select--header .app-select-trigger__value {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-preference-menu {
|
.app-select--header .app-select-menu {
|
||||||
max-width: calc(100vw - 20px);
|
max-width: calc(100vw - 20px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+4
-4
@@ -120,13 +120,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<button id="mobile-upload-button" class="mobile-upload-button" type="button"
|
<button id="mobile-upload-button" class="mobile-upload-button" type="button"
|
||||||
title="Upload a file"><span aria-hidden="true"></span> Upload</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="mono">Mono</option>
|
||||||
<option value="system">System</option>
|
<option value="system">System</option>
|
||||||
<option value="serif">Serif</option>
|
<option value="serif">Serif</option>
|
||||||
<option value="arial">Arial</option>
|
<option value="arial">Arial</option>
|
||||||
<option value="georgia">Georgia</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="14" selected>14</option>
|
||||||
<option value="16">16</option>
|
<option value="16">16</option>
|
||||||
<option value="18">18</option>
|
<option value="18">18</option>
|
||||||
@@ -338,14 +338,14 @@
|
|||||||
<details id="mobile-editor-options" class="mobile-editor-options">
|
<details id="mobile-editor-options" class="mobile-editor-options">
|
||||||
<summary title="Editor options" aria-label="Open editor options">⚙</summary>
|
<summary title="Editor options" aria-label="Open editor options">⚙</summary>
|
||||||
<div class="mobile-editor-options__panel">
|
<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="mono">Mono</option>
|
||||||
<option value="system">System</option>
|
<option value="system">System</option>
|
||||||
<option value="serif">Serif</option>
|
<option value="serif">Serif</option>
|
||||||
<option value="arial">Arial</option>
|
<option value="arial">Arial</option>
|
||||||
<option value="georgia">Georgia</option>
|
<option value="georgia">Georgia</option>
|
||||||
</select></label>
|
</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="14">14</option>
|
||||||
<option value="16">16</option>
|
<option value="16">16</option>
|
||||||
<option value="18">18</option>
|
<option value="18">18</option>
|
||||||
|
|||||||
+1
-1
@@ -152,7 +152,7 @@
|
|||||||
<div class="list-controls resources-controls">
|
<div class="list-controls resources-controls">
|
||||||
<label class="list-search"><span class="sr-only">Search notes and workspaces</span><input id="resources-search"
|
<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>
|
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="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
|
|||||||
+3
-3
@@ -233,12 +233,12 @@ async function loadResources() {
|
|||||||
<div class="share-dialog-body">
|
<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">
|
<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 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>
|
<button class="primary-button" type="submit">Grant access</button>
|
||||||
</form><div data-user-list class="share-list"></div></section>
|
</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">
|
<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 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><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>
|
<label class="share-forever"><input name="forever" type="checkbox"><span>Never expires</span></label>
|
||||||
<button class="primary-button" type="submit">Create link</button>
|
<button class="primary-button" type="submit">Create link</button>
|
||||||
@@ -281,7 +281,7 @@ async function loadResources() {
|
|||||||
const linkPreview = visibleLink
|
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>`
|
? `<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>`;
|
: `<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>';
|
}).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; } }));
|
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 => {
|
linkList.querySelectorAll("[data-link-token-hash]").forEach(linkRow => {
|
||||||
|
|||||||
@@ -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-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-crop-stage"><canvas></canvas></div>
|
||||||
<div class="image-editor-controls">
|
<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>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>
|
||||||
<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>
|
<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>`;
|
</form>`;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import { toast } from "@rustpad/toast";
|
|||||||
import { formatDateTime, formatNumber, formatTime, t, translateSource } from "@rustpad/i18n";
|
import { formatDateTime, formatNumber, formatTime, t, translateSource } from "@rustpad/i18n";
|
||||||
import { getTheme } from "@rustpad/theme";
|
import { getTheme } from "@rustpad/theme";
|
||||||
import { isResourceAccessError } from "@rustpad/security";
|
import { isResourceAccessError } from "@rustpad/security";
|
||||||
|
import { syncSelectControl } from "@rustpad/select";
|
||||||
import { loadHighlight, loadMediaPlayer, loadMermaid } from "@rustpad/vendor-libs";
|
import { loadHighlight, loadMediaPlayer, loadMermaid } from "@rustpad/vendor-libs";
|
||||||
|
|
||||||
export function startNoteEditor(adapter) {
|
export function startNoteEditor(adapter) {
|
||||||
@@ -169,6 +170,10 @@ export function startNoteEditor(adapter) {
|
|||||||
if (mobilePreviewLineToggle) mobilePreviewLineToggle.checked = previewLineToggle.checked;
|
if (mobilePreviewLineToggle) mobilePreviewLineToggle.checked = previewLineToggle.checked;
|
||||||
if (mobileCompactToggle) mobileCompactToggle.checked = compactToggle.checked;
|
if (mobileCompactToggle) mobileCompactToggle.checked = compactToggle.checked;
|
||||||
if (mobileLineLinksToggle) mobileLineLinksToggle.checked = lineLinksToggle.checked;
|
if (mobileLineLinksToggle) mobileLineLinksToggle.checked = lineLinksToggle.checked;
|
||||||
|
syncSelectControl(fontFamily);
|
||||||
|
syncSelectControl(fontSize);
|
||||||
|
if (mobileFontFamily) syncSelectControl(mobileFontFamily);
|
||||||
|
if (mobileFontSize) syncSelectControl(mobileFontSize);
|
||||||
}
|
}
|
||||||
syncMobileEditorControls();
|
syncMobileEditorControls();
|
||||||
function updateAuthorshipControls() {
|
function updateAuthorshipControls() {
|
||||||
|
|||||||
+25
-21
@@ -1,6 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
|
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
|
||||||
* Source-Available Code / Dual-Licensed.
|
* 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";
|
import { api } from "@rustpad/api";
|
||||||
@@ -28,7 +32,7 @@ function icon(name) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function closeOtherPickers(except = null) {
|
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;
|
if (details !== except) details.open = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -36,19 +40,19 @@ function closeOtherPickers(except = null) {
|
|||||||
function preferenceButton({ value, label, selected, languageCode }) {
|
function preferenceButton({ value, label, selected, languageCode }) {
|
||||||
const button = document.createElement("button");
|
const button = document.createElement("button");
|
||||||
button.type = "button";
|
button.type = "button";
|
||||||
button.className = "header-preference-option";
|
button.className = "app-select-option";
|
||||||
button.dataset.value = value;
|
button.dataset.value = value;
|
||||||
button.setAttribute("role", "option");
|
button.setAttribute("role", "option");
|
||||||
button.setAttribute("aria-selected", selected ? "true" : "false");
|
button.setAttribute("aria-selected", selected ? "true" : "false");
|
||||||
if (languageCode) button.lang = languageCode;
|
if (languageCode) button.lang = languageCode;
|
||||||
|
|
||||||
const check = document.createElement("span");
|
const check = document.createElement("span");
|
||||||
check.className = "header-preference-option__check";
|
check.className = "app-select-option__check";
|
||||||
check.setAttribute("aria-hidden", "true");
|
check.setAttribute("aria-hidden", "true");
|
||||||
check.textContent = selected ? "✓" : "";
|
check.textContent = selected ? "✓" : "";
|
||||||
|
|
||||||
const copy = document.createElement("span");
|
const copy = document.createElement("span");
|
||||||
copy.className = "header-preference-option__copy";
|
copy.className = "app-select-option__copy";
|
||||||
copy.textContent = label;
|
copy.textContent = label;
|
||||||
|
|
||||||
button.append(check, copy);
|
button.append(check, copy);
|
||||||
@@ -57,29 +61,29 @@ function preferenceButton({ value, label, selected, languageCode }) {
|
|||||||
|
|
||||||
function createPicker(kind) {
|
function createPicker(kind) {
|
||||||
const details = document.createElement("details");
|
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;
|
details.dataset.preferenceKind = kind;
|
||||||
|
|
||||||
const summary = document.createElement("summary");
|
const summary = document.createElement("summary");
|
||||||
summary.className = "header-preference-trigger";
|
summary.className = "app-select-trigger";
|
||||||
|
|
||||||
const iconWrap = document.createElement("span");
|
const iconWrap = document.createElement("span");
|
||||||
iconWrap.className = "header-preference-trigger__icon";
|
iconWrap.className = "app-select-trigger__icon";
|
||||||
iconWrap.innerHTML = icon(kind === "theme" ? getTheme() : "language");
|
iconWrap.innerHTML = icon(kind === "theme" ? getTheme() : "language");
|
||||||
|
|
||||||
const compactValue = document.createElement("span");
|
const compactValue = document.createElement("span");
|
||||||
compactValue.className = "header-preference-trigger__value";
|
compactValue.className = "app-select-trigger__value";
|
||||||
compactValue.setAttribute("aria-hidden", "true");
|
compactValue.setAttribute("aria-hidden", "true");
|
||||||
|
|
||||||
const chevron = document.createElement("span");
|
const chevron = document.createElement("span");
|
||||||
chevron.className = "header-preference-trigger__chevron";
|
chevron.className = "app-select-trigger__chevron";
|
||||||
chevron.setAttribute("aria-hidden", "true");
|
chevron.setAttribute("aria-hidden", "true");
|
||||||
chevron.textContent = "▾";
|
chevron.textContent = "▾";
|
||||||
|
|
||||||
summary.append(iconWrap, compactValue, chevron);
|
summary.append(iconWrap, compactValue, chevron);
|
||||||
|
|
||||||
const menu = document.createElement("div");
|
const menu = document.createElement("div");
|
||||||
menu.className = "header-preference-menu";
|
menu.className = "app-select-menu";
|
||||||
menu.setAttribute("role", "listbox");
|
menu.setAttribute("role", "listbox");
|
||||||
|
|
||||||
details.append(summary, menu);
|
details.append(summary, menu);
|
||||||
@@ -102,9 +106,9 @@ function renderLanguagePicker(details) {
|
|||||||
const languages = getAvailableLanguages();
|
const languages = getAvailableLanguages();
|
||||||
const current = getLanguage();
|
const current = getLanguage();
|
||||||
const selected = languages.find(language => language.code === current) || languages[0];
|
const selected = languages.find(language => language.code === current) || languages[0];
|
||||||
const summary = details.querySelector(".header-preference-trigger");
|
const summary = details.querySelector(".app-select-trigger");
|
||||||
const value = details.querySelector(".header-preference-trigger__value");
|
const value = details.querySelector(".app-select-trigger__value");
|
||||||
const menu = details.querySelector(".header-preference-menu");
|
const menu = details.querySelector(".app-select-menu");
|
||||||
const label = t("common.language", {}, "Language");
|
const label = t("common.language", {}, "Language");
|
||||||
|
|
||||||
summary.setAttribute("aria-label", label);
|
summary.setAttribute("aria-label", label);
|
||||||
@@ -121,10 +125,10 @@ function renderLanguagePicker(details) {
|
|||||||
|
|
||||||
function renderThemePicker(details) {
|
function renderThemePicker(details) {
|
||||||
const current = getTheme();
|
const current = getTheme();
|
||||||
const summary = details.querySelector(".header-preference-trigger");
|
const summary = details.querySelector(".app-select-trigger");
|
||||||
const iconWrap = details.querySelector(".header-preference-trigger__icon");
|
const iconWrap = details.querySelector(".app-select-trigger__icon");
|
||||||
const value = details.querySelector(".header-preference-trigger__value");
|
const value = details.querySelector(".app-select-trigger__value");
|
||||||
const menu = details.querySelector(".header-preference-menu");
|
const menu = details.querySelector(".app-select-menu");
|
||||||
const label = t("profile.theme", {}, "Interface theme");
|
const label = t("profile.theme", {}, "Interface theme");
|
||||||
|
|
||||||
summary.setAttribute("aria-label", label);
|
summary.setAttribute("aria-label", label);
|
||||||
@@ -145,12 +149,12 @@ function renderPicker(details) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderAll() {
|
function renderAll() {
|
||||||
document.querySelectorAll(".header-preference-picker").forEach(renderPicker);
|
document.querySelectorAll(".app-select--header").forEach(renderPicker);
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindPicker(details) {
|
function bindPicker(details) {
|
||||||
details.querySelector(".header-preference-menu")?.addEventListener("click", async event => {
|
details.querySelector(".app-select-menu")?.addEventListener("click", async event => {
|
||||||
const button = event.target.closest(".header-preference-option");
|
const button = event.target.closest(".app-select-option");
|
||||||
if (!(button instanceof HTMLButtonElement)) return;
|
if (!(button instanceof HTMLButtonElement)) return;
|
||||||
const next = button.dataset.value || "";
|
const next = button.dataset.value || "";
|
||||||
const kind = details.dataset.preferenceKind;
|
const kind = details.dataset.preferenceKind;
|
||||||
@@ -209,7 +213,7 @@ export function initHeaderPreferences() {
|
|||||||
|
|
||||||
document.addEventListener("pointerdown", event => {
|
document.addEventListener("pointerdown", event => {
|
||||||
const target = event.target instanceof Element ? event.target : null;
|
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 });
|
}, { passive: true });
|
||||||
document.addEventListener("keydown", event => {
|
document.addEventListener("keydown", event => {
|
||||||
if (event.key === "Escape") closeOtherPickers();
|
if (event.key === "Escape") closeOtherPickers();
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@
|
|||||||
* Commercial or production use requires a valid paid license.
|
* Commercial or production use requires a valid paid license.
|
||||||
* See LICENSE file in repository root for details.
|
* See LICENSE file in repository root for details.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const STORAGE_KEY = "rustpad:theme";
|
const STORAGE_KEY = "rustpad:theme";
|
||||||
const GUEST_STORAGE_KEY = "rustpad:guest-theme";
|
const GUEST_STORAGE_KEY = "rustpad:guest-theme";
|
||||||
const AUTH_STATE_KEY = "rustpad:auth-state";
|
const AUTH_STATE_KEY = "rustpad:auth-state";
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright (C) 2026 Mateusz Gruszczyński @linuxiarz.pl
|
* Copyright (C) 2026 Mateusz Gruszczynski @linuxiarz.pl
|
||||||
* Source-Available Code / Dual-Licensed.
|
* 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";
|
import { t } from "@rustpad/i18n";
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
<div class="notes-toolbar">
|
<div class="notes-toolbar">
|
||||||
<label class="list-search"><span class="sr-only">Search notes</span><input id="notes-search" type="search"
|
<label class="list-search"><span class="sr-only">Search notes</span><input id="notes-search" type="search"
|
||||||
placeholder="Search notes…" autocomplete="off"></label>
|
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="25">25</option>
|
||||||
<option value="50">50</option>
|
<option value="50">50</option>
|
||||||
<option value="100">100</option>
|
<option value="100">100</option>
|
||||||
|
|||||||
Reference in New Issue
Block a user