visual changes
This commit is contained in:
Generated
+1
-1
@@ -2581,7 +2581,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.1.11"
|
version = "0.1.12"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"argon2",
|
"argon2",
|
||||||
"aws-config",
|
"aws-config",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "rustpad"
|
name = "rustpad"
|
||||||
version = "0.1.11"
|
version = "0.1.12"
|
||||||
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"
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const MODULES: &[&str] = &[
|
|||||||
"note-files",
|
"note-files",
|
||||||
"session",
|
"session",
|
||||||
"socket",
|
"socket",
|
||||||
|
"toast",
|
||||||
"url-state",
|
"url-state",
|
||||||
"security",
|
"security",
|
||||||
];
|
];
|
||||||
|
|||||||
+14
-4
@@ -650,10 +650,20 @@ pub async fn update_profile(
|
|||||||
"E-mail and password are managed by LDAP/AD.",
|
"E-mail and password are managed by LDAP/AD.",
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
} else if !verify_password(&user.password_hash, &req.password) {
|
} else {
|
||||||
return Err(AuthError::unauthorized(
|
let changes_credentials = req
|
||||||
"The current password is incorrect.",
|
.new_email
|
||||||
));
|
.as_deref()
|
||||||
|
.is_some_and(|value| !value.trim().is_empty())
|
||||||
|
|| req
|
||||||
|
.new_password
|
||||||
|
.as_deref()
|
||||||
|
.is_some_and(|value| !value.is_empty());
|
||||||
|
if changes_credentials && !verify_password(&user.password_hash, &req.password) {
|
||||||
|
return Err(AuthError::unauthorized(
|
||||||
|
"The current password is incorrect.",
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut nickname = user.nickname.clone();
|
let mut nickname = user.nickname.clone();
|
||||||
|
|||||||
+172
-87
@@ -209,10 +209,6 @@ textarea:focus {
|
|||||||
padding: 7px;
|
padding: 7px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[hidden] {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.form-message {
|
.form-message {
|
||||||
min-height: 1.2em;
|
min-height: 1.2em;
|
||||||
margin: -5px 0 0;
|
margin: -5px 0 0;
|
||||||
@@ -256,7 +252,7 @@ textarea:focus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.secondary-button:hover {
|
.secondary-button:hover {
|
||||||
border-color: #4b5565;
|
|
||||||
background: var(--surface-3);
|
background: var(--surface-3);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -501,15 +497,15 @@ textarea::selection {
|
|||||||
.preview {
|
.preview {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
padding: 24px;
|
|
||||||
color: #dce2eb;
|
color: #dce2eb;
|
||||||
line-height: 1.72;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body h1,
|
.markdown-body h1,
|
||||||
.markdown-body h2,
|
.markdown-body h2,
|
||||||
.markdown-body h3 {
|
.markdown-body h3 {
|
||||||
margin: 1.25em 0 .5em;
|
|
||||||
letter-spacing: -.03em;
|
letter-spacing: -.03em;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -527,10 +523,6 @@ textarea::selection {
|
|||||||
font-size: 1.5rem;
|
font-size: 1.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body p {
|
|
||||||
margin: .72em 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.markdown-body code {
|
.markdown-body code {
|
||||||
padding: .16em .36em;
|
padding: .16em .36em;
|
||||||
border: 1px solid #303745;
|
border: 1px solid #303745;
|
||||||
@@ -553,7 +545,7 @@ textarea::selection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body blockquote {
|
.markdown-body blockquote {
|
||||||
margin: 1em 0;
|
|
||||||
padding: .2em 1em;
|
padding: .2em 1em;
|
||||||
border-left: 3px solid var(--accent);
|
border-left: 3px solid var(--accent);
|
||||||
color: #abb5c3;
|
color: #abb5c3;
|
||||||
@@ -664,7 +656,7 @@ textarea::selection {
|
|||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
border: 2px solid #8b7af4;
|
border: 2px solid #8b7af4;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
background: #101319;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.revision time {
|
.revision time {
|
||||||
@@ -923,13 +915,6 @@ dialog::backdrop {
|
|||||||
font-size: .75rem;
|
font-size: .75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dialog-actions {
|
|
||||||
display: flex;
|
|
||||||
justify-content: flex-end;
|
|
||||||
gap: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.error-card {
|
.error-card {
|
||||||
width: min(560px, 100%);
|
width: min(560px, 100%);
|
||||||
padding: 32px;
|
padding: 32px;
|
||||||
@@ -1032,7 +1017,7 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.line-gutter {
|
.line-gutter {
|
||||||
width: 62px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 24px 8px 24px 0;
|
padding: 24px 8px 24px 0;
|
||||||
border-right: 1px solid var(--border);
|
border-right: 1px solid var(--border);
|
||||||
@@ -1044,7 +1029,7 @@ dialog::backdrop {
|
|||||||
|
|
||||||
.line-gutter div {
|
.line-gutter div {
|
||||||
height: 1.72em;
|
height: 1.72em;
|
||||||
padding-right: 8px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.hide-editor-line-numbers .editor-shell {
|
.hide-editor-line-numbers .editor-shell {
|
||||||
@@ -1207,11 +1192,9 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body img {
|
.markdown-body img {
|
||||||
display: block;
|
|
||||||
max-width: 100%;
|
|
||||||
height: auto;
|
|
||||||
margin: 16px auto;
|
margin: 16px auto;
|
||||||
border-radius: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdown-body a {
|
.markdown-body a {
|
||||||
@@ -1244,7 +1227,7 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.public-document {
|
.public-document {
|
||||||
width: min(900px, calc(100% - 32px));
|
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
padding: 56px 0 96px;
|
padding: 56px 0 96px;
|
||||||
}
|
}
|
||||||
@@ -1263,7 +1246,7 @@ dialog::backdrop {
|
|||||||
|
|
||||||
.public-content {
|
.public-content {
|
||||||
min-height: 240px;
|
min-height: 240px;
|
||||||
padding: 32px 32px 32px 62px;
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
@@ -1291,7 +1274,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Layout safeguards and home footer */
|
/* Layout safeguards and home footer */
|
||||||
.app-header__main,
|
.app-header__main,
|
||||||
.document-heading {
|
.document-heading {
|
||||||
@@ -1314,19 +1296,12 @@ dialog::backdrop {
|
|||||||
|
|
||||||
.home-footer {
|
.home-footer {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
width: min(1040px, calc(100% - 32px));
|
|
||||||
margin: auto auto 28px;
|
|
||||||
padding-top: 24px;
|
|
||||||
color: var(--muted-2);
|
|
||||||
font-size: .76rem;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.home-footer__inner {
|
.home-footer__inner {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1369,12 +1344,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Show an author once at the start of each contiguous ownership block. */
|
|
||||||
.line-gutter div {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.line-owner-label {
|
.line-owner-label {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 7px;
|
left: 7px;
|
||||||
@@ -1415,11 +1384,10 @@ dialog::backdrop {
|
|||||||
|
|
||||||
.owner-labels {
|
.owner-labels {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 2;
|
|
||||||
top: 0;
|
|
||||||
right: 12px;
|
right: 12px;
|
||||||
left: 48px;
|
left: 48px;
|
||||||
height: 100%;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
@@ -1465,7 +1433,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Editor display preferences and author overlay */
|
/* Editor display preferences and author overlay */
|
||||||
.editor-shell {
|
.editor-shell {
|
||||||
position: relative;
|
position: relative;
|
||||||
@@ -1738,13 +1705,6 @@ dialog::backdrop {
|
|||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.note-card-title {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.protect-badge,
|
.protect-badge,
|
||||||
.file-flag {
|
.file-flag {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@@ -1770,10 +1730,6 @@ dialog::backdrop {
|
|||||||
color: var(--danger);
|
color: var(--danger);
|
||||||
}
|
}
|
||||||
|
|
||||||
.danger-button {
|
|
||||||
color: var(--danger);
|
|
||||||
}
|
|
||||||
|
|
||||||
.dialog-check {
|
.dialog-check {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -1803,12 +1759,12 @@ dialog::backdrop {
|
|||||||
|
|
||||||
.files-panel {
|
.files-panel {
|
||||||
grid-template-rows: auto minmax(0, 1fr);
|
grid-template-rows: auto minmax(0, 1fr);
|
||||||
min-height: min(590px, calc(100vh - 28px));
|
|
||||||
max-height: calc(100vh - 28px);
|
max-height: calc(100vh - 28px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.files-list {
|
.files-list {
|
||||||
min-height: 0;
|
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
overscroll-behavior: contain;
|
overscroll-behavior: contain;
|
||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
@@ -1864,13 +1820,13 @@ dialog::backdrop {
|
|||||||
|
|
||||||
.file-actions {
|
.file-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
|
||||||
justify-content: flex-end;
|
justify-content: flex-end;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-actions button {
|
.file-actions button {
|
||||||
min-height: 30px;
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
background: var(--surface-2);
|
background: var(--surface-2);
|
||||||
@@ -2217,8 +2173,6 @@ dialog::backdrop {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.markdown-body .table-wrap {
|
.markdown-body .table-wrap {
|
||||||
overflow-x: auto;
|
overflow-x: auto;
|
||||||
margin: .7em 0;
|
margin: .7em 0;
|
||||||
@@ -2535,24 +2489,12 @@ dialog::backdrop {
|
|||||||
flex: 1 0 100%;
|
flex: 1 0 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.identity-panel {
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
|
|
||||||
.identity-panel__close {
|
.identity-panel__close {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 14px;
|
top: 14px;
|
||||||
right: 14px;
|
right: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.identity-links {
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
|
|
||||||
.identity-links .text-button {
|
|
||||||
padding: 3px 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.auth-panel {
|
.auth-panel {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
@@ -2832,7 +2774,7 @@ dialog::backdrop {
|
|||||||
min-width: 0;
|
min-width: 0;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
align-items: center;
|
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
@@ -2997,7 +2939,6 @@ dialog::backdrop {
|
|||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Full-height editor: keep header/footer visible and scroll only the work area. */
|
/* Full-height editor: keep header/footer visible and scroll only the work area. */
|
||||||
.pad-page {
|
.pad-page {
|
||||||
height: 100dvh;
|
height: 100dvh;
|
||||||
@@ -3290,10 +3231,9 @@ dialog::backdrop {
|
|||||||
.share-section {
|
.share-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 10px;
|
gap: 10px;
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid var(--border);
|
border: 1px solid var(--border);
|
||||||
border-radius: 10px;
|
|
||||||
background: var(--surface);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.share-form-grid {
|
.share-form-grid {
|
||||||
@@ -3334,7 +3274,7 @@ dialog::backdrop {
|
|||||||
grid-template-columns: minmax(180px, 1fr) auto auto;
|
grid-template-columns: minmax(180px, 1fr) auto auto;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 9px 0;
|
|
||||||
border-top: 1px solid var(--border);
|
border-top: 1px solid var(--border);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3588,7 +3528,6 @@ dialog::backdrop {
|
|||||||
padding: 7px 11px;
|
padding: 7px 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@media (max-width: 800px) {
|
@media (max-width: 800px) {
|
||||||
.share-dialog {
|
.share-dialog {
|
||||||
width: calc(100% - 16px);
|
width: calc(100% - 16px);
|
||||||
@@ -4009,7 +3948,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.editor-column-label {
|
.editor-column-label {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@@ -4404,6 +4342,12 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (max-width: 720px) {
|
||||||
|
.pad-page #save-state {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 520px) {
|
@media (max-width: 520px) {
|
||||||
.pad-page .app-header {
|
.pad-page .app-header {
|
||||||
padding-inline: 10px;
|
padding-inline: 10px;
|
||||||
@@ -4450,7 +4394,6 @@ dialog::backdrop {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/* Per-note author color override */
|
/* Per-note author color override */
|
||||||
.use-global-color {
|
.use-global-color {
|
||||||
display: inline-grid;
|
display: inline-grid;
|
||||||
@@ -4469,3 +4412,145 @@ dialog::backdrop {
|
|||||||
border-color: var(--accent);
|
border-color: var(--accent);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Quick actions appear when the editor header switches to its compact menu. */
|
||||||
|
.mobile-editor-bubble { display: none; }
|
||||||
|
@media (max-width: 1499px) {
|
||||||
|
.pad-page .mobile-editor-bubble {
|
||||||
|
position: fixed;
|
||||||
|
right: 10px;
|
||||||
|
bottom: calc(10px + env(safe-area-inset-bottom, 0px));
|
||||||
|
z-index: 45;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
max-width: calc(100vw - 20px);
|
||||||
|
padding: 5px;
|
||||||
|
border: 1px solid rgba(255,255,255,.12);
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(17,21,28,.88);
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,.28);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
.mobile-editor-bubble > button {
|
||||||
|
display: inline-grid;
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
padding: 0;
|
||||||
|
place-items: center;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 50%;
|
||||||
|
background: rgba(255,255,255,.07);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
.mobile-color-dot {
|
||||||
|
width: 15px;
|
||||||
|
height: 15px;
|
||||||
|
border: 2px solid rgba(255,255,255,.75);
|
||||||
|
border-radius: 50%;
|
||||||
|
background: var(--owner, var(--accent));
|
||||||
|
}
|
||||||
|
.mobile-connection-status {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0 8px 0 4px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: .72rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
.mobile-connection-status #mobile-status-text {
|
||||||
|
max-width: 92px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
.pad-page .toast { bottom: calc(62px + env(safe-area-inset-bottom, 0px)); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Movable quick actions for compact editor screens. */
|
||||||
|
@media (max-width: 1499px) {
|
||||||
|
.pad-page .mobile-editor-bubble {
|
||||||
|
touch-action: none;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-page .footer-connection-block {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-page .room-details.is-mobile-open .room-popover {
|
||||||
|
position: fixed;
|
||||||
|
right: 12px;
|
||||||
|
bottom: 72px;
|
||||||
|
left: auto;
|
||||||
|
z-index: 60;
|
||||||
|
width: min(560px, calc(100vw - 24px));
|
||||||
|
max-height: min(560px, calc(100dvh - 96px));
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-time-button {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-time-preview {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
bottom: calc(100% + 8px);
|
||||||
|
padding: 6px 9px;
|
||||||
|
border: 1px solid var(--border-strong);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: var(--surface);
|
||||||
|
box-shadow: 0 8px 24px rgb(0 0 0 / 32%);
|
||||||
|
color: var(--text);
|
||||||
|
font-size: .78rem;
|
||||||
|
font-variant-numeric: tabular-nums;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-time-preview[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pad-page .mobile-editor-bubble.is-dragging {
|
||||||
|
transition: none;
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-editor-bubble > .mobile-bubble-drag {
|
||||||
|
width: 24px;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--muted-2);
|
||||||
|
cursor: grab;
|
||||||
|
font-size: .85rem;
|
||||||
|
letter-spacing: -3px;
|
||||||
|
touch-action: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-chat-button {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-chat-unread {
|
||||||
|
position: absolute;
|
||||||
|
top: -4px;
|
||||||
|
right: -4px;
|
||||||
|
display: inline-grid;
|
||||||
|
min-width: 17px;
|
||||||
|
height: 17px;
|
||||||
|
padding: 0 4px;
|
||||||
|
place-items: center;
|
||||||
|
border: 2px solid #11151c;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--danger);
|
||||||
|
color: white;
|
||||||
|
font-size: .58rem;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobile-chat-unread[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+1
-1
@@ -137,7 +137,7 @@
|
|||||||
<label data-local-profile-field>Current e-mail<input id="profile-current-email" type="email" readonly></label>
|
<label data-local-profile-field>Current e-mail<input id="profile-current-email" type="email" readonly></label>
|
||||||
<label data-local-profile-field>New e-mail<input id="profile-email" type="email" maxlength="320" placeholder="Leave empty to keep current"></label>
|
<label data-local-profile-field>New e-mail<input id="profile-email" type="email" maxlength="320" placeholder="Leave empty to keep current"></label>
|
||||||
<label data-local-profile-field>New password<input id="profile-new-password" type="password" minlength="8" maxlength="128" placeholder="Leave empty to keep current"></label>
|
<label data-local-profile-field>New password<input id="profile-new-password" type="password" minlength="8" maxlength="128" placeholder="Leave empty to keep current"></label>
|
||||||
<label data-local-profile-field>Current password<input id="profile-password" type="password" minlength="8" maxlength="128" required></label>
|
<label data-local-profile-field>Current password<input id="profile-password" type="password" minlength="8" maxlength="128" placeholder="Required for e-mail or password changes"></label>
|
||||||
</div>
|
</div>
|
||||||
<button class="primary-button" type="submit">Save profile</button>
|
<button class="primary-button" type="submit">Save profile</button>
|
||||||
<button id="profile-delete" data-local-profile-field class="danger-button" type="button">Delete account</button>
|
<button id="profile-delete" data-local-profile-field class="danger-button" type="button">Delete account</button>
|
||||||
|
|||||||
+5
-11
@@ -6,6 +6,7 @@ import { getAuthToken, setAccessToken } from "@rustpad/session";
|
|||||||
import { api } from "@rustpad/api";
|
import { api } from "@rustpad/api";
|
||||||
import { copyText } from "@rustpad/clipboard";
|
import { copyText } from "@rustpad/clipboard";
|
||||||
import { safeAppUrl } from "@rustpad/security";
|
import { safeAppUrl } from "@rustpad/security";
|
||||||
|
import { toast } from "@rustpad/toast";
|
||||||
|
|
||||||
function slugify(value, fallback) {
|
function slugify(value, fallback) {
|
||||||
return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback;
|
return value.toLowerCase().normalize("NFKD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "") || fallback;
|
||||||
@@ -84,14 +85,6 @@ document.querySelector("#workspace-form").addEventListener("submit", async (even
|
|||||||
|
|
||||||
handleAccountConfirmationToken();
|
handleAccountConfirmationToken();
|
||||||
handleResetToken();
|
handleResetToken();
|
||||||
function toast(text) {
|
|
||||||
const element = document.querySelector("#toast");
|
|
||||||
if (!element) return;
|
|
||||||
element.textContent = text;
|
|
||||||
element.classList.add("visible");
|
|
||||||
clearTimeout(toast.timer);
|
|
||||||
toast.timer = setTimeout(() => element.classList.remove("visible"), 3000);
|
|
||||||
}
|
|
||||||
|
|
||||||
{ const params=new URLSearchParams(location.search); const token=params.get("account_action_token"); if(token){ api("/api/auth/account-action/confirm",{method:"POST",body:JSON.stringify({token})}).then(r=>toast(r.message)).catch(e=>toast(e.message)).finally(()=>{params.delete("account_action_token");history.replaceState({},"",`${location.pathname}${params.size?`?${params}`:""}${location.hash}`);}); } }
|
{ const params=new URLSearchParams(location.search); const token=params.get("account_action_token"); if(token){ api("/api/auth/account-action/confirm",{method:"POST",body:JSON.stringify({token})}).then(r=>toast(r.message)).catch(e=>toast(e.message)).finally(()=>{params.delete("account_action_token");history.replaceState({},"",`${location.pathname}${params.size?`?${params}`:""}${location.hash}`);}); } }
|
||||||
|
|
||||||
@@ -256,7 +249,7 @@ function renderAccount(session) {
|
|||||||
if (identityDialog) {
|
if (identityDialog) {
|
||||||
const authDialog = bindIdentityDialog({
|
const authDialog = bindIdentityDialog({
|
||||||
dialog: identityDialog,
|
dialog: identityDialog,
|
||||||
onIdentity: async (_nickname, session) => renderAccount(session),
|
onIdentity: async (_nickname, session) => { renderAccount(session); if (session) toast(`Logged in as ${session.nickname}.`); },
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector("#footer-login")?.addEventListener("click", () => {
|
document.querySelector("#footer-login")?.addEventListener("click", () => {
|
||||||
@@ -290,14 +283,14 @@ if (identityDialog) {
|
|||||||
const suggestion = currentSession?.suggested_nickname;
|
const suggestion = currentSession?.suggested_nickname;
|
||||||
const suggestionElement = document.querySelector("#profile-nickname-suggestion");
|
const suggestionElement = document.querySelector("#profile-nickname-suggestion");
|
||||||
suggestionElement.textContent = suggestion ? `Suggested nickname: ${suggestion}` : "No automatic nickname suggestion is available.";
|
suggestionElement.textContent = suggestion ? `Suggested nickname: ${suggestion}` : "No automatic nickname suggestion is available.";
|
||||||
document.querySelector("#profile-password").required = !directoryManaged;
|
document.querySelector("#profile-password").required = false;
|
||||||
profileDialog.showModal();
|
profileDialog.showModal();
|
||||||
});
|
});
|
||||||
document.querySelector("#close-profile")?.addEventListener("click", () => profileDialog.close());
|
document.querySelector("#close-profile")?.addEventListener("click", () => profileDialog.close());
|
||||||
profileDialog?.addEventListener("click", event => { if (event.target === profileDialog) profileDialog.close(); });
|
profileDialog?.addEventListener("click", event => { if (event.target === profileDialog) profileDialog.close(); });
|
||||||
profileForm?.addEventListener("submit", async event => {
|
profileForm?.addEventListener("submit", async event => {
|
||||||
event.preventDefault(); const message=document.querySelector("#profile-message"); message.textContent=""; message.classList.remove("success", "error");
|
event.preventDefault(); const message=document.querySelector("#profile-message"); message.textContent=""; message.classList.remove("success", "error");
|
||||||
try { const selectedColor=document.querySelector("#profile-color").value; const result=await api("/api/auth/profile",{method:"POST",headers:authHeaders(),body:JSON.stringify({nickname:document.querySelector("#profile-nickname").value.trim(),editor_color:selectedColor,new_email:currentSession?.directory_managed?null:(document.querySelector("#profile-email").value.trim()||null),new_password:currentSession?.directory_managed?null:(document.querySelector("#profile-new-password").value||null),password:currentSession?.directory_managed?"":document.querySelector("#profile-password").value})}); message.textContent=result.message; message.classList.add("success"); currentSession.nickname=result.nickname; currentSession.editor_color=result.editor_color; renderAccount(currentSession); } catch(e){ message.textContent=e.message; message.classList.add("error"); }
|
try { const selectedColor=document.querySelector("#profile-color").value; const newEmail=currentSession?.directory_managed?null:(document.querySelector("#profile-email").value.trim()||null); const newPassword=currentSession?.directory_managed?null:(document.querySelector("#profile-new-password").value||null); const password=currentSession?.directory_managed?"":document.querySelector("#profile-password").value; if ((newEmail || newPassword) && !password) throw new Error("Enter the current password to change e-mail or password."); const result=await api("/api/auth/profile",{method:"POST",headers:authHeaders(),body:JSON.stringify({nickname:document.querySelector("#profile-nickname").value.trim(),editor_color:selectedColor,new_email:newEmail,new_password:newPassword,password})}); message.textContent=result.message; message.classList.add("success"); currentSession.nickname=result.nickname; currentSession.editor_color=result.editor_color; renderAccount(currentSession); } catch(e){ message.textContent=e.message; message.classList.add("error"); }
|
||||||
});
|
});
|
||||||
document.querySelector("#profile-delete")?.addEventListener("click", async () => {
|
document.querySelector("#profile-delete")?.addEventListener("click", async () => {
|
||||||
const message=document.querySelector("#profile-message"); const password=document.querySelector("#profile-password").value;
|
const message=document.querySelector("#profile-message"); const password=document.querySelector("#profile-password").value;
|
||||||
@@ -313,6 +306,7 @@ if (identityDialog) {
|
|||||||
document.querySelector("#footer-logout")?.addEventListener("click", async () => {
|
document.querySelector("#footer-logout")?.addEventListener("click", async () => {
|
||||||
await logoutCurrentSession();
|
await logoutCurrentSession();
|
||||||
renderAccount(null);
|
renderAccount(null);
|
||||||
|
toast("Logged out.");
|
||||||
});
|
});
|
||||||
|
|
||||||
window.addEventListener("rustpad:session-expired", () => renderAccount(null));
|
window.addEventListener("rustpad:session-expired", () => renderAccount(null));
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import { getNickname, getGuestId, getAuthToken, getAccessToken, setAccessToken }
|
|||||||
import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
|
import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
|
||||||
import { bindNoteFiles } from "@rustpad/note-files";
|
import { bindNoteFiles } from "@rustpad/note-files";
|
||||||
import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url-state";
|
import { currentShareUrl, readEditorState, writeEditorState } from "@rustpad/url-state";
|
||||||
|
import { toast } from "@rustpad/toast";
|
||||||
|
|
||||||
export function startNoteEditor(adapter) {
|
export function startNoteEditor(adapter) {
|
||||||
const editor = document.querySelector("#editor"), preview = document.querySelector("#preview"), editorWorkspace = document.querySelector("#editor-workspace"), gutter = document.querySelector("#line-gutter"), ownerLabels = document.querySelector("#owner-labels"), authorshipLayer = document.querySelector("#authorship-layer");
|
const editor = document.querySelector("#editor"), preview = document.querySelector("#preview"), editorWorkspace = document.querySelector("#editor-workspace"), gutter = document.querySelector("#line-gutter"), ownerLabels = document.querySelector("#owner-labels"), authorshipLayer = document.querySelector("#authorship-layer");
|
||||||
const modeToggle = document.querySelector("#mode-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog");
|
const modeToggle = document.querySelector("#mode-toggle"), passwordDialog = document.querySelector("#password-dialog"), identityDialog = document.querySelector("#identity-dialog");
|
||||||
const accessLevel = document.querySelector("#access-level"), roomDetails = document.querySelector("#room-details"), roomUsers = document.querySelector("#room-users"), roomCount = document.querySelector("#room-count"), socketLatency = document.querySelector("#socket-latency"), chatMessages = document.querySelector("#chat-messages"), chatForm = document.querySelector("#chat-form"), chatInput = document.querySelector("#chat-input"), chatUnread = document.querySelector("#chat-unread");
|
const accessLevel = document.querySelector("#access-level"), roomDetails = document.querySelector("#room-details"), roomUsers = document.querySelector("#room-users"), roomCount = document.querySelector("#room-count"), socketLatency = document.querySelector("#socket-latency"), chatMessages = document.querySelector("#chat-messages"), chatForm = document.querySelector("#chat-form"), chatInput = document.querySelector("#chat-input"), chatUnread = document.querySelector("#chat-unread"), mobileChatUnread = document.querySelector("#mobile-chat-unread");
|
||||||
let unreadChat = 0;
|
let unreadChat = 0;
|
||||||
const compactToggle = document.querySelector("#compact-toggle"), publicTaskUpdates = document.querySelector("#public-task-updates"), fontFamily = document.querySelector("#font-family"), fontSize = document.querySelector("#font-size"), currentUser = document.querySelector("#current-user"), userColorPicker = document.querySelector("#user-color-picker"), useGlobalColorButton = document.querySelector("#use-global-color");
|
const compactToggle = document.querySelector("#compact-toggle"), publicTaskUpdates = document.querySelector("#public-task-updates"), fontFamily = document.querySelector("#font-family"), fontSize = document.querySelector("#font-size"), currentUser = document.querySelector("#current-user"), userColorPicker = document.querySelector("#user-color-picker"), useGlobalColorButton = document.querySelector("#use-global-color");
|
||||||
const shareToken = new URLSearchParams(location.search).get("share"); if (shareToken) setAccessToken(adapter.access.kind, adapter.access.key, shareToken);
|
const shareToken = new URLSearchParams(location.search).get("share"); if (shareToken) setAccessToken(adapter.access.kind, adapter.access.key, shareToken);
|
||||||
@@ -46,8 +47,7 @@ export function startNoteEditor(adapter) {
|
|||||||
return /^#[0-9a-f]{6}$/i.test(parts.color) && parts.color.toLowerCase() === color.toLowerCase();
|
return /^#[0-9a-f]{6}$/i.test(parts.color) && parts.color.toLowerCase() === color.toLowerCase();
|
||||||
}, replacement, contentLength);
|
}, replacement, contentLength);
|
||||||
}
|
}
|
||||||
function updateCurrentUser() { const color = currentUserColor() || defaultColorFor(nickname); const overridden = Boolean(noteUserColor()); currentUser.querySelector(".user-chip__name").textContent = nickname; currentUser.style.setProperty("--owner", color); currentUser.title = overridden ? "Note color override" : "Global profile color"; userColorPicker.value = /^#[0-9a-f]{6}$/i.test(color) ? color : "#7c6cff"; useGlobalColorButton.hidden = !overridden; }
|
function updateCurrentUser() { const color = currentUserColor() || defaultColorFor(nickname); const overridden = Boolean(noteUserColor()); currentUser.querySelector(".user-chip__name").textContent = nickname; currentUser.style.setProperty("--owner", color); currentUser.title = overridden ? "Note color override" : "Global profile color"; userColorPicker.value = /^#[0-9a-f]{6}$/i.test(color) ? color : "#7c6cff"; useGlobalColorButton.hidden = !overridden; document.querySelector(".mobile-editor-bubble")?.style.setProperty("--owner", color); }
|
||||||
function toast(text) { const el = document.querySelector("#toast"); el.textContent = text; el.classList.add("visible"); setTimeout(() => el.classList.remove("visible"), 1800); }
|
|
||||||
function sessionHeaders() { const token = accessToken || getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
|
function sessionHeaders() { const token = accessToken || getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
|
||||||
function accountHeaders() { const token = getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
|
function accountHeaders() { const token = getAuthToken(); return token ? { Authorization: `Bearer ${token}` } : {}; }
|
||||||
async function loadNoteInfo() {
|
async function loadNoteInfo() {
|
||||||
@@ -64,10 +64,10 @@ export function startNoteEditor(adapter) {
|
|||||||
function updatePresence(users) { const entries = Array.isArray(users) ? users : []; roomCount.textContent = `${entries.length} ${entries.length === 1 ? "user" : "users"}`; roomUsers.replaceChildren(...entries.map(entry => { const user = typeof entry === "string" ? { name: entry, color: "" } : entry || {}; const li = document.createElement("li"), dot = document.createElement("span"), label = document.createElement("span"); li.className = "room-user"; dot.className = "room-user__dot"; dot.style.setProperty("--owner", /^#[0-9a-f]{6}$/i.test(user.color || "") ? user.color : defaultColorFor(user.name)); label.textContent = user.name || "Guest"; li.title = label.textContent; li.append(dot, label); return li; })); if (!entries.length) { const li = document.createElement("li"); li.textContent = "No active users"; roomUsers.append(li); } }
|
function updatePresence(users) { const entries = Array.isArray(users) ? users : []; roomCount.textContent = `${entries.length} ${entries.length === 1 ? "user" : "users"}`; roomUsers.replaceChildren(...entries.map(entry => { const user = typeof entry === "string" ? { name: entry, color: "" } : entry || {}; const li = document.createElement("li"), dot = document.createElement("span"), label = document.createElement("span"); li.className = "room-user"; dot.className = "room-user__dot"; dot.style.setProperty("--owner", /^#[0-9a-f]{6}$/i.test(user.color || "") ? user.color : defaultColorFor(user.name)); label.textContent = user.name || "Guest"; li.title = label.textContent; li.append(dot, label); return li; })); if (!entries.length) { const li = document.createElement("li"); li.textContent = "No active users"; roomUsers.append(li); } }
|
||||||
function updateLatency(ms) { socketLatency.textContent = Number.isFinite(ms) ? `${ms} ms` : "— ms"; }
|
function updateLatency(ms) { socketLatency.textContent = Number.isFinite(ms) ? `${ms} ms` : "— ms"; }
|
||||||
function appendLinkifiedText(container, value) { const text = String(value || ""); const urlPattern = /https?:\/\/[^\s<>{}\[\]"'`]+/gi; let index = 0; for (const match of text.matchAll(urlPattern)) { const start = match.index ?? 0; if (start > index) container.append(document.createTextNode(text.slice(index, start))); let raw = match[0], trail = ""; while (/[),.!?:;]$/.test(raw)) { trail = raw.slice(-1) + trail; raw = raw.slice(0, -1); } try { const url = new URL(raw); if (url.protocol === "http:" || url.protocol === "https:") { const link = document.createElement("a"); link.href = url.href; link.textContent = raw; link.target = "_blank"; link.rel = "noopener noreferrer"; container.append(link); } else container.append(document.createTextNode(raw)); } catch { container.append(document.createTextNode(raw)); } if (trail) container.append(document.createTextNode(trail)); index = start + match[0].length; } if (index < text.length) container.append(document.createTextNode(text.slice(index))); }
|
function appendLinkifiedText(container, value) { const text = String(value || ""); const urlPattern = /https?:\/\/[^\s<>{}\[\]"'`]+/gi; let index = 0; for (const match of text.matchAll(urlPattern)) { const start = match.index ?? 0; if (start > index) container.append(document.createTextNode(text.slice(index, start))); let raw = match[0], trail = ""; while (/[),.!?:;]$/.test(raw)) { trail = raw.slice(-1) + trail; raw = raw.slice(0, -1); } try { const url = new URL(raw); if (url.protocol === "http:" || url.protocol === "https:") { const link = document.createElement("a"); link.href = url.href; link.textContent = raw; link.target = "_blank"; link.rel = "noopener noreferrer"; container.append(link); } else container.append(document.createTextNode(raw)); } catch { container.append(document.createTextNode(raw)); } if (trail) container.append(document.createTextNode(trail)); index = start + match[0].length; } if (index < text.length) container.append(document.createTextNode(text.slice(index))); }
|
||||||
function appendChatMessage(message) { const empty = chatMessages.querySelector(".chat-empty"); empty?.remove(); const row = document.createElement("p"); row.className = "chat-message"; const author = document.createElement("strong"); author.textContent = message.sender; const text = document.createElement("span"); appendLinkifiedText(text, message.text); row.append(author, text); chatMessages.append(row); while (chatMessages.children.length > 100) chatMessages.firstElementChild.remove(); chatMessages.scrollTop = chatMessages.scrollHeight; if (message.sender !== nickname && !roomDetails.open) { unreadChat++; chatUnread.hidden = false; chatUnread.textContent = unreadChat > 99 ? "99+" : String(unreadChat); const oldTitle = document.title; if (!document.title.startsWith("● ")) document.title = `● ${oldTitle}`; if (document.hidden && Notification.permission === "granted") new Notification(`${message.sender} wrote in RustPad`, { body: message.text.slice(0, 160), tag: "rustpad-room-chat" }); } }
|
function appendChatMessage(message) { const empty = chatMessages.querySelector(".chat-empty"); empty?.remove(); const row = document.createElement("p"); row.className = "chat-message"; const author = document.createElement("strong"); author.textContent = message.sender; const text = document.createElement("span"); appendLinkifiedText(text, message.text); row.append(author, text); chatMessages.append(row); while (chatMessages.children.length > 100) chatMessages.firstElementChild.remove(); chatMessages.scrollTop = chatMessages.scrollHeight; if (message.sender !== nickname && !roomDetails.open) { unreadChat++; chatUnread.hidden = false; chatUnread.textContent = unreadChat > 99 ? "99+" : String(unreadChat); if (mobileChatUnread) { mobileChatUnread.hidden = false; mobileChatUnread.textContent = chatUnread.textContent; } const oldTitle = document.title; if (!document.title.startsWith("● ")) document.title = `● ${oldTitle}`; if (document.hidden && Notification.permission === "granted") new Notification(`${message.sender} wrote in RustPad`, { body: message.text.slice(0, 160), tag: "rustpad-room-chat" }); } }
|
||||||
function clearUnread() { unreadChat = 0; chatUnread.hidden = true; chatUnread.textContent = ""; document.title = document.title.replace(/^● /, ""); }
|
function clearUnread() { unreadChat = 0; chatUnread.hidden = true; chatUnread.textContent = ""; if (mobileChatUnread) { mobileChatUnread.hidden = true; mobileChatUnread.textContent = ""; } document.title = document.title.replace(/^● /, ""); }
|
||||||
|
|
||||||
function setStatus(kind, text) { document.querySelector("#status-dot").className = `status__dot${kind ? ` is-${kind}` : ""}`; document.querySelector("#status-text").textContent = text; }
|
function setStatus(kind, text) { const className = `status__dot${kind ? ` is-${kind}` : ""}`; document.querySelector("#status-dot").className = className; document.querySelector("#status-text").textContent = text; const mobileDot = document.querySelector("#mobile-status-dot"); const mobileText = document.querySelector("#mobile-status-text"); if (mobileDot) mobileDot.className = className; if (mobileText) mobileText.textContent = text; }
|
||||||
function updateAddressLabel() { document.querySelector(adapter.addressSelector).textContent = `${location.pathname}${location.search}`; }
|
function updateAddressLabel() { document.querySelector(adapter.addressSelector).textContent = `${location.pathname}${location.search}`; }
|
||||||
async function renderMermaid() { const nodes = preview.querySelectorAll(".mermaid"); if (!nodes.length) return; try { const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"); mermaid.initialize({ startOnLoad: false, theme: "dark", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", '<p class="error">Failed to load Mermaid.</p>')); } }
|
async function renderMermaid() { const nodes = preview.querySelectorAll(".mermaid"); if (!nodes.length) return; try { const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"); mermaid.initialize({ startOnLoad: false, theme: "dark", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", '<p class="error">Failed to load Mermaid.</p>')); } }
|
||||||
async function renderCodeHighlight() { const nodes = preview.querySelectorAll('pre code[class^="language-"]:not(.language-mermaid)'); if (!nodes.length) return; try { const hljs = await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm"); nodes.forEach(node => { const lines = node.querySelectorAll(".code-line"); if (!lines.length) { hljs.default.highlightElement(node); return; } const language = [...node.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.default.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.default.highlightAuto(line.textContent).value; } }); node.classList.add("hljs"); }); } catch { } }
|
async function renderCodeHighlight() { const nodes = preview.querySelectorAll('pre code[class^="language-"]:not(.language-mermaid)'); if (!nodes.length) return; try { const hljs = await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm"); nodes.forEach(node => { const lines = node.querySelectorAll(".code-line"); if (!lines.length) { hljs.default.highlightElement(node); return; } const language = [...node.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.default.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.default.highlightAuto(line.textContent).value; } }); node.classList.add("hljs"); }); } catch { } }
|
||||||
@@ -285,9 +285,80 @@ export function startNoteEditor(adapter) {
|
|||||||
if (!event.matches) setHeaderMenuOpen(false);
|
if (!event.matches) setHeaderMenuOpen(false);
|
||||||
});
|
});
|
||||||
modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUi({ write: true }); }); lineToggle.addEventListener("change", () => { localStorage.setItem("rustpad:line-numbers", lineToggle.checked ? "on" : "off"); renderGutter(); }); previewLineToggle.addEventListener("change", () => { localStorage.setItem("rustpad:preview-line-numbers", previewLineToggle.checked ? "on" : "off"); renderGutter(); alignPreviewLineNumbers(preview); }); compactToggle.addEventListener("change", () => { localStorage.setItem("rustpad:compact", compactToggle.checked ? "on" : "off"); applyUi(); }); fontFamily.addEventListener("change", () => { localStorage.setItem("rustpad:font-family", fontFamily.value); applyUi(); }); fontSize.addEventListener("change", () => { localStorage.setItem("rustpad:font-size", fontSize.value); applyUi(); });
|
modeToggle.addEventListener("click", () => { uiState = { ...uiState, mode: uiState.mode === "markdown" ? "text" : "markdown" }; applyUi({ write: true }); }); lineToggle.addEventListener("change", () => { localStorage.setItem("rustpad:line-numbers", lineToggle.checked ? "on" : "off"); renderGutter(); }); previewLineToggle.addEventListener("change", () => { localStorage.setItem("rustpad:preview-line-numbers", previewLineToggle.checked ? "on" : "off"); renderGutter(); alignPreviewLineNumbers(preview); }); compactToggle.addEventListener("change", () => { localStorage.setItem("rustpad:compact", compactToggle.checked ? "on" : "off"); applyUi(); }); fontFamily.addEventListener("change", () => { localStorage.setItem("rustpad:font-family", fontFamily.value); applyUi(); }); fontSize.addEventListener("change", () => { localStorage.setItem("rustpad:font-size", fontSize.value); applyUi(); });
|
||||||
|
document.querySelector("#mobile-files-button")?.addEventListener("click", () => document.querySelector("#files-button")?.click());
|
||||||
|
document.querySelector("#mobile-color-button")?.addEventListener("click", () => userColorPicker.click());
|
||||||
|
const compactBubbleQuery = matchMedia("(max-width: 1499px)");
|
||||||
|
document.querySelector("#mobile-chat-button")?.addEventListener("click", event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
roomDetails.classList.add("is-mobile-open");
|
||||||
|
roomDetails.open = true;
|
||||||
|
clearUnread();
|
||||||
|
requestAnimationFrame(() => chatInput.focus());
|
||||||
|
});
|
||||||
|
const mobileTimeButton = document.querySelector("#mobile-time-button");
|
||||||
|
const mobileTimePreview = document.querySelector("#mobile-time-preview");
|
||||||
|
let mobileTimeTimer = null;
|
||||||
|
function updateMobileTime() {
|
||||||
|
if (!mobileTimePreview) return;
|
||||||
|
mobileTimePreview.textContent = new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", second: "2-digit" }).format(new Date());
|
||||||
|
}
|
||||||
|
mobileTimeButton?.addEventListener("click", event => {
|
||||||
|
event.stopPropagation();
|
||||||
|
const willOpen = mobileTimePreview.hidden;
|
||||||
|
mobileTimePreview.hidden = !willOpen;
|
||||||
|
clearInterval(mobileTimeTimer);
|
||||||
|
mobileTimeTimer = null;
|
||||||
|
if (willOpen) { updateMobileTime(); mobileTimeTimer = setInterval(updateMobileTime, 1000); }
|
||||||
|
});
|
||||||
|
document.addEventListener("click", event => {
|
||||||
|
if (!event.target.closest("#mobile-time-button, #mobile-time-preview") && mobileTimePreview) { mobileTimePreview.hidden = true; clearInterval(mobileTimeTimer); mobileTimeTimer = null; }
|
||||||
|
if (roomDetails.classList.contains("is-mobile-open") && !event.target.closest("#mobile-chat-button, #room-details")) { roomDetails.open = false; roomDetails.classList.remove("is-mobile-open"); }
|
||||||
|
});
|
||||||
|
const mobileBubble = document.querySelector("#mobile-editor-bubble");
|
||||||
|
const mobileBubbleDrag = document.querySelector("#mobile-bubble-drag");
|
||||||
|
const bubblePositionKey = `rustpad:mobile-bubble:${adapter.access.kind}`;
|
||||||
|
function clampBubblePosition(left, top) {
|
||||||
|
const rect = mobileBubble.getBoundingClientRect();
|
||||||
|
const margin = 8;
|
||||||
|
return {
|
||||||
|
left: Math.min(Math.max(margin, left), Math.max(margin, innerWidth - rect.width - margin)),
|
||||||
|
top: Math.min(Math.max(margin, top), Math.max(margin, innerHeight - rect.height - margin)),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
function placeMobileBubble(position) {
|
||||||
|
if (!mobileBubble || !position) return;
|
||||||
|
const next = clampBubblePosition(Number(position.left), Number(position.top));
|
||||||
|
if (!Number.isFinite(next.left) || !Number.isFinite(next.top)) return;
|
||||||
|
mobileBubble.style.left = `${next.left}px`;
|
||||||
|
mobileBubble.style.top = `${next.top}px`;
|
||||||
|
mobileBubble.style.right = "auto";
|
||||||
|
mobileBubble.style.bottom = "auto";
|
||||||
|
}
|
||||||
|
try { placeMobileBubble(JSON.parse(localStorage.getItem(bubblePositionKey) || "null")); } catch {}
|
||||||
|
mobileBubbleDrag?.addEventListener("pointerdown", event => {
|
||||||
|
if (!mobileBubble || !compactBubbleQuery.matches) return;
|
||||||
|
event.preventDefault();
|
||||||
|
const rect = mobileBubble.getBoundingClientRect();
|
||||||
|
const offsetX = event.clientX - rect.left, offsetY = event.clientY - rect.top;
|
||||||
|
mobileBubble.classList.add("is-dragging");
|
||||||
|
mobileBubbleDrag.setPointerCapture(event.pointerId);
|
||||||
|
const move = moveEvent => placeMobileBubble({ left: moveEvent.clientX - offsetX, top: moveEvent.clientY - offsetY });
|
||||||
|
const end = () => {
|
||||||
|
mobileBubble.classList.remove("is-dragging");
|
||||||
|
mobileBubbleDrag.removeEventListener("pointermove", move);
|
||||||
|
mobileBubbleDrag.removeEventListener("pointerup", end);
|
||||||
|
mobileBubbleDrag.removeEventListener("pointercancel", end);
|
||||||
|
const finalRect = mobileBubble.getBoundingClientRect();
|
||||||
|
localStorage.setItem(bubblePositionKey, JSON.stringify({ left: finalRect.left, top: finalRect.top }));
|
||||||
|
};
|
||||||
|
mobileBubbleDrag.addEventListener("pointermove", move);
|
||||||
|
mobileBubbleDrag.addEventListener("pointerup", end);
|
||||||
|
mobileBubbleDrag.addEventListener("pointercancel", end);
|
||||||
|
});
|
||||||
|
window.addEventListener("resize", () => { if (mobileBubble?.style.left) placeMobileBubble(mobileBubble.getBoundingClientRect()); });
|
||||||
window.addEventListener("popstate", () => { uiState = readEditorState(); applyUi(); }); window.addEventListener("rustpad:urlchange", updateAddressLabel); document.querySelector("#copy-link").addEventListener("click", async () => { try { await copyText(currentShareUrl(uiState)); toast("Link copied"); } catch (e) { toast(e.message); } }); document.querySelectorAll("[data-format]").forEach(b => b.addEventListener("click", () => { applyFormat(editor, b.dataset.format); b.closest("details")?.removeAttribute("open"); })); bindFormatShortcuts(editor); bindEmojiPicker({ editor, details: document.querySelector("#emoji-picker"), search: document.querySelector("#emoji-search"), categories: document.querySelector("#emoji-categories"), grid: document.querySelector("#emoji-grid"), empty: document.querySelector("#emoji-empty") }); document.querySelector("#shortcuts-button").addEventListener("click", () => document.querySelector("#shortcuts-dialog").showModal()); document.querySelector("#close-shortcuts").addEventListener("click", () => document.querySelector("#shortcuts-dialog").close()); preview.addEventListener("change", event => { const checkbox = event.target.closest(".task-checkbox"); if (!checkbox) return; const lineIndex = Number(checkbox.dataset.sourceLine) - 1; const lines = editor.value.split("\n"); if (lineIndex < 0 || lineIndex >= lines.length) return; lines[lineIndex] = lines[lineIndex].replace(/^(\s*[-*+]\s+\[)[ xX](\])/, `$1${checkbox.checked ? "x" : " "}$2`); editor.value = lines.join("\n"); editor.dispatchEvent(new Event("input", { bubbles: true })); }); preview.addEventListener("keydown", event => { const target = event.target.closest(".preview-editable"); if (!target) return; if (event.key === "Enter") { event.preventDefault(); target.blur(); return; } if (event.key === "ArrowUp" || event.key === "ArrowDown") { if (movePreviewCaret(target, event.key === "ArrowUp" ? -1 : 1)) event.preventDefault(); } }); preview.addEventListener("blur", event => { const target = event.target.closest(".preview-editable"); if (!target) return; const lineIndex = Number(target.dataset.sourceLine) - 1; if (lineIndex < 0) return; const lines = editor.value.split("\n"); const value = markdownFromPreview(target); let next; if (target.dataset.tableCell !== undefined) next = replaceTableCell(lines[lineIndex], Number(target.dataset.tableCell), value); else { const prefix = target.dataset.sourcePrefix || "", suffix = target.dataset.sourceSuffix || ""; next = prefix + value + suffix; } if (lines[lineIndex] === next) return; lines[lineIndex] = next; editor.value = lines.join("\n"); editor.setSelectionRange(editor.value.length, editor.value.length); editor.dispatchEvent(new Event("input", { bubbles: true })); }, { capture: true });
|
window.addEventListener("popstate", () => { uiState = readEditorState(); applyUi(); }); window.addEventListener("rustpad:urlchange", updateAddressLabel); document.querySelector("#copy-link").addEventListener("click", async () => { try { await copyText(currentShareUrl(uiState)); toast("Link copied"); } catch (e) { toast(e.message); } }); document.querySelectorAll("[data-format]").forEach(b => b.addEventListener("click", () => { applyFormat(editor, b.dataset.format); b.closest("details")?.removeAttribute("open"); })); bindFormatShortcuts(editor); bindEmojiPicker({ editor, details: document.querySelector("#emoji-picker"), search: document.querySelector("#emoji-search"), categories: document.querySelector("#emoji-categories"), grid: document.querySelector("#emoji-grid"), empty: document.querySelector("#emoji-empty") }); document.querySelector("#shortcuts-button").addEventListener("click", () => document.querySelector("#shortcuts-dialog").showModal()); document.querySelector("#close-shortcuts").addEventListener("click", () => document.querySelector("#shortcuts-dialog").close()); preview.addEventListener("change", event => { const checkbox = event.target.closest(".task-checkbox"); if (!checkbox) return; const lineIndex = Number(checkbox.dataset.sourceLine) - 1; const lines = editor.value.split("\n"); if (lineIndex < 0 || lineIndex >= lines.length) return; lines[lineIndex] = lines[lineIndex].replace(/^(\s*[-*+]\s+\[)[ xX](\])/, `$1${checkbox.checked ? "x" : " "}$2`); editor.value = lines.join("\n"); editor.dispatchEvent(new Event("input", { bubbles: true })); }); preview.addEventListener("keydown", event => { const target = event.target.closest(".preview-editable"); if (!target) return; if (event.key === "Enter") { event.preventDefault(); target.blur(); return; } if (event.key === "ArrowUp" || event.key === "ArrowDown") { if (movePreviewCaret(target, event.key === "ArrowUp" ? -1 : 1)) event.preventDefault(); } }); preview.addEventListener("blur", event => { const target = event.target.closest(".preview-editable"); if (!target) return; const lineIndex = Number(target.dataset.sourceLine) - 1; if (lineIndex < 0) return; const lines = editor.value.split("\n"); const value = markdownFromPreview(target); let next; if (target.dataset.tableCell !== undefined) next = replaceTableCell(lines[lineIndex], Number(target.dataset.tableCell), value); else { const prefix = target.dataset.sourcePrefix || "", suffix = target.dataset.sourceSuffix || ""; next = prefix + value + suffix; } if (lines[lineIndex] === next) return; lines[lineIndex] = next; editor.value = lines.join("\n"); editor.setSelectionRange(editor.value.length, editor.value.length); editor.dispatchEvent(new Event("input", { bubbles: true })); }, { capture: true });
|
||||||
publicTaskUpdates.addEventListener("change", async () => { publicTaskUpdates.disabled = true; try { await adapter.publish(accessToken, publicTaskUpdates.checked); toast(publicTaskUpdates.checked ? "Public task updates enabled" : "Public task updates disabled"); } catch (error) { publicTaskUpdates.checked = !publicTaskUpdates.checked; toast(error.message); } finally { publicTaskUpdates.disabled = false; } }); document.querySelector("#publish-page").addEventListener("click", async () => { try { const result = await adapter.publish(accessToken, publicTaskUpdates.checked); const url = new URL(result.url, location.origin).href; await copyText(url); toast("Page link copied"); window.open(url, "_blank", "noopener"); } catch (error) { toast(error.message); } });
|
publicTaskUpdates.addEventListener("change", async () => { publicTaskUpdates.disabled = true; try { await adapter.publish(accessToken, publicTaskUpdates.checked); toast(publicTaskUpdates.checked ? "Public task updates enabled" : "Public task updates disabled"); } catch (error) { publicTaskUpdates.checked = !publicTaskUpdates.checked; toast(error.message); } finally { publicTaskUpdates.disabled = false; } }); document.querySelector("#publish-page").addEventListener("click", async () => { try { const result = await adapter.publish(accessToken, publicTaskUpdates.checked); const url = new URL(result.url, location.origin).href; await copyText(url); toast("Page link copied"); window.open(url, "_blank", "noopener"); } catch (error) { toast(error.message); } });
|
||||||
roomDetails.addEventListener("toggle", () => { if (roomDetails.open) { clearUnread(); chatInput.focus(); if ("Notification" in window && Notification.permission === "default") Notification.requestPermission().catch(() => { }); } });
|
roomDetails.addEventListener("toggle", () => { if (roomDetails.open) { clearUnread(); chatInput.focus(); if ("Notification" in window && Notification.permission === "default") Notification.requestPermission().catch(() => { }); } else { roomDetails.classList.remove("is-mobile-open"); } });
|
||||||
document.addEventListener("visibilitychange", () => { if (!document.hidden && roomDetails.open) clearUnread(); });
|
document.addEventListener("visibilitychange", () => { if (!document.hidden && roomDetails.open) clearUnread(); });
|
||||||
chatForm.addEventListener("submit", event => { event.preventDefault(); const text = chatInput.value.trim(); if (!text || !socket) return; socket.chat(text); chatInput.value = ""; chatInput.focus(); });
|
chatForm.addEventListener("submit", event => { event.preventDefault(); const text = chatInput.value.trim(); if (!text || !socket) return; socket.chat(text); chatInput.value = ""; chatInput.focus(); });
|
||||||
if (!chatMessages.children.length) { const empty = document.createElement("p"); empty.className = "chat-empty"; empty.textContent = "No messages yet"; chatMessages.append(empty); }
|
if (!chatMessages.children.length) { const empty = document.createElement("p"); empty.className = "chat-empty"; empty.textContent = "No messages yet"; chatMessages.append(empty); }
|
||||||
|
|||||||
+1
-1
@@ -4,11 +4,11 @@ installGlobalDiagnostics();
|
|||||||
import { api } from "@rustpad/api";
|
import { api } from "@rustpad/api";
|
||||||
import { copyText } from "@rustpad/clipboard";
|
import { copyText } from "@rustpad/clipboard";
|
||||||
import { alignPreviewLineNumbers, renderMarkdown } from "@rustpad/markdown";
|
import { alignPreviewLineNumbers, renderMarkdown } from "@rustpad/markdown";
|
||||||
|
import { toast } from "@rustpad/toast";
|
||||||
|
|
||||||
const token = location.pathname.split("/").filter(Boolean)[1];
|
const token = location.pathname.split("/").filter(Boolean)[1];
|
||||||
const content = document.querySelector("#public-content");
|
const content = document.querySelector("#public-content");
|
||||||
const lineNumbersToggle = document.querySelector("#public-line-numbers-toggle");
|
const lineNumbersToggle = document.querySelector("#public-line-numbers-toggle");
|
||||||
function toast(text) { const el = document.querySelector("#toast"); el.textContent = text; el.classList.add("visible"); setTimeout(() => el.classList.remove("visible"), 1800); }
|
|
||||||
async function renderMermaid() { const nodes = content.querySelectorAll(".mermaid"); if (!nodes.length) return; try { const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"); mermaid.initialize({ startOnLoad: false, theme: "dark", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", '<p class="error">Failed to load Mermaid.</p>')); } }
|
async function renderMermaid() { const nodes = content.querySelectorAll(".mermaid"); if (!nodes.length) return; try { const { default: mermaid } = await import("https://cdn.jsdelivr.net/npm/mermaid@11/dist/mermaid.esm.min.mjs"); mermaid.initialize({ startOnLoad: false, theme: "dark", securityLevel: "strict" }); await mermaid.run({ nodes: [...nodes] }); } catch { nodes.forEach(n => n.insertAdjacentHTML("beforebegin", '<p class="error">Failed to load Mermaid.</p>')); } }
|
||||||
async function renderCodeHighlight() { const blocks = content.querySelectorAll('pre code[class^="language-"]'); if (!blocks.length) return; try { const hljs = await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm"); blocks.forEach(block => { const lines = block.querySelectorAll(".code-line"); if (!lines.length) { hljs.default.highlightElement(block); return; } const language = [...block.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.default.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.default.highlightAuto(line.textContent).value; } }); block.classList.add("hljs"); }); } catch { } }
|
async function renderCodeHighlight() { const blocks = content.querySelectorAll('pre code[class^="language-"]'); if (!blocks.length) return; try { const hljs = await import("https://cdn.jsdelivr.net/npm/highlight.js@11.11.1/+esm"); blocks.forEach(block => { const lines = block.querySelectorAll(".code-line"); if (!lines.length) { hljs.default.highlightElement(block); return; } const language = [...block.classList].find(name => name.startsWith("language-"))?.slice(9); lines.forEach(line => { try { line.innerHTML = hljs.default.highlight(line.textContent, { language, ignoreIllegals: true }).value; } catch { line.innerHTML = hljs.default.highlightAuto(line.textContent).value; } }); block.classList.add("hljs"); }); } catch { } }
|
||||||
function lockPublicContent(allowTaskUpdates) {
|
function lockPublicContent(allowTaskUpdates) {
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
let hideTimer;
|
||||||
|
|
||||||
|
export function toast(message, options = {}) {
|
||||||
|
const element = document.querySelector(options.selector || "#toast");
|
||||||
|
if (!element) return;
|
||||||
|
element.textContent = String(message ?? "");
|
||||||
|
element.classList.add("visible");
|
||||||
|
clearTimeout(hideTimer);
|
||||||
|
hideTimer = setTimeout(() => element.classList.remove("visible"), options.duration ?? 1800);
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@ import { getNickname, getAccessToken, getAuthToken, setAccessToken } from "@rust
|
|||||||
import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
|
import { bindIdentityDialog, validateCurrentSession } from "@rustpad/auth-ui";
|
||||||
import { askConfirm } from "@rustpad/modal";
|
import { askConfirm } from "@rustpad/modal";
|
||||||
import { safeAppUrl } from "@rustpad/security";
|
import { safeAppUrl } from "@rustpad/security";
|
||||||
|
import { toast } from "@rustpad/toast";
|
||||||
|
|
||||||
const parts = location.pathname.split("/").filter(Boolean);
|
const parts = location.pathname.split("/").filter(Boolean);
|
||||||
const slug = parts[1];
|
const slug = parts[1];
|
||||||
@@ -23,12 +24,6 @@ const notesViewKey = `rustpad:workspace:${slug}:notes-view`;
|
|||||||
let notesView = localStorage.getItem(notesViewKey) === "table" ? "table" : "grid";
|
let notesView = localStorage.getItem(notesViewKey) === "table" ? "table" : "grid";
|
||||||
let notesCache = [];
|
let notesCache = [];
|
||||||
|
|
||||||
function toast(text) {
|
|
||||||
const el = document.querySelector("#toast");
|
|
||||||
el.textContent = text;
|
|
||||||
el.classList.add("visible");
|
|
||||||
setTimeout(() => el.classList.remove("visible"), 1600);
|
|
||||||
}
|
|
||||||
function escapeHtml(v) { const e = document.createElement("div"); e.textContent = v; return e.innerHTML; }
|
function escapeHtml(v) { const e = document.createElement("div"); e.textContent = v; return e.innerHTML; }
|
||||||
function formatDate(value) {
|
function formatDate(value) {
|
||||||
if (value == null || value === "") return "—";
|
if (value == null || value === "") return "—";
|
||||||
|
|||||||
+12
-4
@@ -117,10 +117,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="editor-footer">
|
<footer class="editor-footer">
|
||||||
<div class="footer-left"><span id="characters">0 characters</span> · <span id="words">0 words</span> ·
|
<div class="footer-left"><span id="characters">0 characters</span> · <span id="words">0 words</span><span class="footer-connection-block"> · <span class="footer-status status"><span id="status-dot" class="status__dot"></span><span
|
||||||
<span class="footer-status status"><span id="status-dot" class="status__dot"></span><span
|
id="status-text">Connecting…</span></span> · <span id="socket-latency" class="footer-socket-latency"
|
||||||
id="status-text">Connecting…</span></span> · <span id="socket-latency"
|
title="WebSocket round-trip time">— ms</span></span> · <details id="room-details" class="room-details">
|
||||||
title="WebSocket round-trip time">— ms</span> · <details id="room-details" class="room-details">
|
|
||||||
<summary><span id="room-count">0 users</span><span id="chat-unread" class="chat-unread"
|
<summary><span id="room-count">0 users</span><span id="chat-unread" class="chat-unread"
|
||||||
hidden></span></summary>
|
hidden></span></summary>
|
||||||
<div class="room-popover">
|
<div class="room-popover">
|
||||||
@@ -210,6 +209,15 @@
|
|||||||
class="dialog-link" href="/">Cancel</a>
|
class="dialog-link" href="/">Cancel</a>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<div id="mobile-editor-bubble" class="mobile-editor-bubble" aria-label="Quick editor actions">
|
||||||
|
<button id="mobile-bubble-drag" class="mobile-bubble-drag" type="button" title="Move quick actions" aria-label="Move quick actions">⋮⋮</button>
|
||||||
|
<button id="mobile-files-button" type="button" title="Files" aria-label="Open files">📎</button>
|
||||||
|
<button id="mobile-color-button" type="button" title="Editor color" aria-label="Change editor color"><span class="mobile-color-dot" aria-hidden="true"></span></button>
|
||||||
|
<button id="mobile-chat-button" class="mobile-chat-button" type="button" title="Chat" aria-label="Open chat">💬<span id="mobile-chat-unread" class="mobile-chat-unread" hidden></span></button>
|
||||||
|
<button id="mobile-time-button" class="mobile-time-button" type="button" title="Current time" aria-label="Show current time">🕒</button>
|
||||||
|
<span id="mobile-time-preview" class="mobile-time-preview" role="status" hidden></span>
|
||||||
|
<span id="mobile-connection-status" class="mobile-connection-status" title="WebSocket status"><span id="mobile-status-dot" class="status__dot"></span><span id="mobile-status-text">Connecting…</span></span>
|
||||||
|
</div>
|
||||||
<div id="toast" class="toast"></div>
|
<div id="toast" class="toast"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
+12
-4
@@ -115,10 +115,9 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<footer class="editor-footer">
|
<footer class="editor-footer">
|
||||||
<div class="footer-left"><span id="characters">0 characters</span> · <span id="words">0 words</span> ·
|
<div class="footer-left"><span id="characters">0 characters</span> · <span id="words">0 words</span><span class="footer-connection-block"> · <span class="footer-status status"><span id="status-dot" class="status__dot"></span><span
|
||||||
<span class="footer-status status"><span id="status-dot" class="status__dot"></span><span
|
id="status-text">Connecting…</span></span> · <span id="socket-latency" class="footer-socket-latency"
|
||||||
id="status-text">Connecting…</span></span> · <span id="socket-latency"
|
title="WebSocket round-trip time">— ms</span></span> · <details id="room-details" class="room-details">
|
||||||
title="WebSocket round-trip time">— ms</span> · <details id="room-details" class="room-details">
|
|
||||||
<summary><span id="room-count">0 users</span><span id="chat-unread" class="chat-unread"
|
<summary><span id="room-count">0 users</span><span id="chat-unread" class="chat-unread"
|
||||||
hidden></span></summary>
|
hidden></span></summary>
|
||||||
<div class="room-popover">
|
<div class="room-popover">
|
||||||
@@ -208,6 +207,15 @@
|
|||||||
class="dialog-link" href="/">Cancel</a>
|
class="dialog-link" href="/">Cancel</a>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
</dialog>
|
||||||
|
<div id="mobile-editor-bubble" class="mobile-editor-bubble" aria-label="Quick editor actions">
|
||||||
|
<button id="mobile-bubble-drag" class="mobile-bubble-drag" type="button" title="Move quick actions" aria-label="Move quick actions">⋮⋮</button>
|
||||||
|
<button id="mobile-files-button" type="button" title="Files" aria-label="Open files">📎</button>
|
||||||
|
<button id="mobile-color-button" type="button" title="Editor color" aria-label="Change editor color"><span class="mobile-color-dot" aria-hidden="true"></span></button>
|
||||||
|
<button id="mobile-chat-button" class="mobile-chat-button" type="button" title="Chat" aria-label="Open chat">💬<span id="mobile-chat-unread" class="mobile-chat-unread" hidden></span></button>
|
||||||
|
<button id="mobile-time-button" class="mobile-time-button" type="button" title="Current time" aria-label="Show current time">🕒</button>
|
||||||
|
<span id="mobile-time-preview" class="mobile-time-preview" role="status" hidden></span>
|
||||||
|
<span id="mobile-connection-status" class="mobile-connection-status" title="WebSocket status"><span id="mobile-status-dot" class="status__dot"></span><span id="mobile-status-text">Connecting…</span></span>
|
||||||
|
</div>
|
||||||
<div id="toast" class="toast"></div>
|
<div id="toast" class="toast"></div>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user