new functions

This commit is contained in:
Mateusz Gruszczyński
2026-07-29 09:49:37 +02:00
parent bbcd1fe987
commit 904f59146b
23 changed files with 469 additions and 53 deletions
Generated
+1 -1
View File
@@ -2581,7 +2581,7 @@ dependencies = [
[[package]] [[package]]
name = "rustpad" name = "rustpad"
version = "0.1.22" version = "0.1.24"
dependencies = [ dependencies = [
"argon2", "argon2",
"aws-config", "aws-config",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rustpad" name = "rustpad"
version = "0.1.22" version = "0.1.24"
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"
@@ -0,0 +1,2 @@
ALTER TABLE pads ADD COLUMN public_page_disabled BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE notes ADD COLUMN public_page_disabled BOOLEAN NOT NULL DEFAULT FALSE;
@@ -0,0 +1,8 @@
CREATE TABLE resource_editor_settings (
resource_kind VARCHAR(32) NOT NULL,
resource_slug VARCHAR(512) NOT NULL,
authorship_mode VARCHAR(16) NOT NULL DEFAULT 'simple',
colors_enabled BOOLEAN NOT NULL DEFAULT TRUE,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (resource_kind, resource_slug)
);
@@ -0,0 +1,2 @@
ALTER TABLE pads ADD COLUMN public_page_disabled BOOLEAN NOT NULL DEFAULT FALSE;
ALTER TABLE notes ADD COLUMN public_page_disabled BOOLEAN NOT NULL DEFAULT FALSE;
@@ -0,0 +1,8 @@
CREATE TABLE resource_editor_settings (
resource_kind TEXT NOT NULL,
resource_slug TEXT NOT NULL,
authorship_mode TEXT NOT NULL DEFAULT 'simple',
colors_enabled BOOLEAN NOT NULL DEFAULT TRUE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY (resource_kind, resource_slug)
);
@@ -0,0 +1,2 @@
ALTER TABLE pads ADD COLUMN public_page_disabled INTEGER NOT NULL DEFAULT 0;
ALTER TABLE notes ADD COLUMN public_page_disabled INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,8 @@
CREATE TABLE resource_editor_settings (
resource_kind TEXT NOT NULL,
resource_slug TEXT NOT NULL,
authorship_mode TEXT NOT NULL DEFAULT 'simple',
colors_enabled INTEGER NOT NULL DEFAULT 1,
updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (resource_kind, resource_slug)
);
+65 -1
View File
@@ -41,7 +41,8 @@ fn bearer_token(headers: &HeaderMap) -> Option<&str> {
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
pub struct PublishResponse { pub struct PublishResponse {
url: String, url: Option<String>,
enabled: bool,
} }
#[derive(Debug, Serialize)] #[derive(Debug, Serialize)]
@@ -83,6 +84,7 @@ pub struct PublishRequest {
allow_task_updates: bool, allow_task_updates: bool,
#[serde(default)] #[serde(default)]
unprotect_page: bool, unprotect_page: bool,
enabled: Option<bool>,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -153,11 +155,16 @@ pub struct NoteInfo {
note_protected: bool, note_protected: bool,
allow_public_task_updates: bool, allow_public_task_updates: bool,
public_page_unprotected: bool, public_page_unprotected: bool,
public_page_enabled: bool,
private: bool,
created_at: String, created_at: String,
updated_at: String, updated_at: String,
can_delete_files: bool, can_delete_files: bool,
global_color: Option<String>, global_color: Option<String>,
note_color: Option<String>, note_color: Option<String>,
authorship_mode: String,
colors_enabled: bool,
can_save_editor_settings: bool,
} }
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
@@ -165,6 +172,44 @@ pub struct EditorColorRequest {
color: Option<String>, color: Option<String>,
} }
#[derive(Debug, Deserialize)]
pub struct EditorSettingsRequest {
authorship_mode: String,
colors_enabled: bool,
}
async fn editor_settings(state: &SharedState, kind: &str, slug: &str) -> Result<(String, bool), ApiError> {
let row: Option<(String, i64)> = sqlx::query_as(queries::get(
state.db.kind(),
queries::RESOURCE_EDITOR_SETTINGS_SELECT,
))
.bind(kind)
.bind(slug)
.fetch_optional(state.db.pool())
.await?;
Ok(row.map(|(mode, colors)| (if mode == "full" { "full".into() } else { "simple".into() }, colors != 0))
.unwrap_or_else(|| ("simple".into(), true)))
}
async fn save_editor_settings(
state: &SharedState, headers: &HeaderMap, permission_kind: &str, permission_slug: &str,
settings_kind: &str, settings_slug: &str, payload: EditorSettingsRequest,
) -> Result<Json<serde_json::Value>, ApiError> {
let permission = crate::auth::resource_permission(state, permission_kind, permission_slug, bearer_token(headers))
.await.map_err(|e| ApiError::forbidden(&e.message))?;
if permission.as_deref() != Some("rw") {
return Err(ApiError::forbidden("Read and write access is required to save editor settings"));
}
let mode = match payload.authorship_mode.as_str() { "simple" => "simple", "full" | "advanced" => "full", _ => return Err(ApiError::bad_request("Invalid authorship mode")) };
let mut tx = state.db.pool().begin().await?;
sqlx::query(queries::get(state.db.kind(), queries::RESOURCE_EDITOR_SETTINGS_DELETE))
.bind(settings_kind).bind(settings_slug).execute(&mut *tx).await?;
sqlx::query(queries::get(state.db.kind(), queries::RESOURCE_EDITOR_SETTINGS_INSERT))
.bind(settings_kind).bind(settings_slug).bind(mode).bind(payload.colors_enabled).execute(&mut *tx).await?;
tx.commit().await?;
Ok(Json(serde_json::json!({"authorship_mode": mode, "colors_enabled": payload.colors_enabled})))
}
pub async fn create_workspace( pub async fn create_workspace(
State(state): State<SharedState>, State(state): State<SharedState>,
headers: HeaderMap, headers: HeaderMap,
@@ -439,7 +484,12 @@ pub async fn note_info(
.ok_or_else(ApiError::not_found_note)?; .ok_or_else(ApiError::not_found_note)?;
let color_slug = format!("{}/{}", workspace_slug, note_slug); let color_slug = format!("{}/{}", workspace_slug, note_slug);
let (global_color, note_color) = editor_colors(&state, &headers, "note", &color_slug).await?; let (global_color, note_color) = editor_colors(&state, &headers, "note", &color_slug).await?;
let (authorship_mode, colors_enabled) = editor_settings(&state, "note", &color_slug).await?;
let can_save_editor_settings = crate::auth::resource_permission(&state, "workspace", &workspace_slug, bearer_token(&headers)).await.ok().flatten().as_deref() == Some("rw");
if workspace.is_private == 0 && !db::note_public_page_disabled(&state.db, note.id).await? && !db::note_public_page_enabled(&state.db, note.id).await? {
db::publish_note(&state.db, note.id).await?;
}
Ok(Json(NoteInfo { Ok(Json(NoteInfo {
workspace_slug: workspace.slug, workspace_slug: workspace.slug,
workspace_title: workspace.title, workspace_title: workspace.title,
@@ -449,6 +499,8 @@ pub async fn note_info(
note_protected: note.protected, note_protected: note.protected,
allow_public_task_updates: db::note_public_task_updates(&state.db, note.id).await?, allow_public_task_updates: db::note_public_task_updates(&state.db, note.id).await?,
public_page_unprotected: db::note_public_page_unprotected(&state.db, note.id).await?, public_page_unprotected: db::note_public_page_unprotected(&state.db, note.id).await?,
public_page_enabled: db::note_public_page_enabled(&state.db, note.id).await?,
private: workspace.is_private != 0,
created_at: db::normalize_timestamp(&note.created_at), created_at: db::normalize_timestamp(&note.created_at),
updated_at: db::normalize_timestamp(&note.updated_at), updated_at: db::normalize_timestamp(&note.updated_at),
can_delete_files: { can_delete_files: {
@@ -474,9 +526,21 @@ pub async fn note_info(
}, },
global_color, global_color,
note_color, note_color,
authorship_mode,
colors_enabled,
can_save_editor_settings,
})) }))
} }
pub async fn set_note_editor_settings(
State(state): State<SharedState>, headers: HeaderMap,
Path((workspace_slug, note_slug)): Path<(String, String)>,
Json(payload): Json<EditorSettingsRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
let settings_slug = format!("{}/{}", workspace_slug, note_slug);
save_editor_settings(&state, &headers, "workspace", &workspace_slug, "note", &settings_slug, payload).await
}
pub async fn history( pub async fn history(
State(state): State<SharedState>, State(state): State<SharedState>,
headers: HeaderMap, headers: HeaderMap,
+38 -2
View File
@@ -20,11 +20,16 @@ pub struct PadInfo {
protected: bool, protected: bool,
allow_public_task_updates: bool, allow_public_task_updates: bool,
public_page_unprotected: bool, public_page_unprotected: bool,
public_page_enabled: bool,
private: bool,
created_at: String, created_at: String,
updated_at: String, updated_at: String,
can_delete_files: bool, can_delete_files: bool,
global_color: Option<String>, global_color: Option<String>,
note_color: Option<String>, note_color: Option<String>,
authorship_mode: String,
colors_enabled: bool,
can_save_editor_settings: bool,
} }
pub async fn create_pad( pub async fn create_pad(
@@ -78,12 +83,19 @@ pub async fn pad_info(
) )
.await?; .await?;
let (global_color, note_color) = editor_colors(&state, &headers, "pad", &slug).await?; let (global_color, note_color) = editor_colors(&state, &headers, "pad", &slug).await?;
let (authorship_mode, colors_enabled) = editor_settings(&state, "pad", &slug).await?;
let can_save_editor_settings = crate::auth::resource_permission(&state, "pad", &slug, bearer_token(&headers)).await.ok().flatten().as_deref() == Some("rw");
if pad.is_private == 0 && !db::pad_public_page_disabled(&state.db, pad.id).await? && !db::pad_public_page_enabled(&state.db, pad.id).await? {
db::publish_pad(&state.db, pad.id).await?;
}
Ok(Json(PadInfo { Ok(Json(PadInfo {
slug: pad.slug, slug: pad.slug,
title: pad.title, title: pad.title,
protected: pad.password_hash.is_some(), protected: pad.password_hash.is_some(),
allow_public_task_updates: db::pad_public_task_updates(&state.db, pad.id).await?, allow_public_task_updates: db::pad_public_task_updates(&state.db, pad.id).await?,
public_page_unprotected: db::pad_public_page_unprotected(&state.db, pad.id).await?, public_page_unprotected: db::pad_public_page_unprotected(&state.db, pad.id).await?,
public_page_enabled: db::pad_public_page_enabled(&state.db, pad.id).await?,
private: pad.is_private != 0,
created_at: db::normalize_timestamp(&pad.created_at), created_at: db::normalize_timestamp(&pad.created_at),
updated_at: db::normalize_timestamp(&pad.updated_at), updated_at: db::normalize_timestamp(&pad.updated_at),
can_delete_files: crate::auth::is_resource_owner( can_delete_files: crate::auth::is_resource_owner(
@@ -96,9 +108,19 @@ pub async fn pad_info(
.unwrap_or(false), .unwrap_or(false),
global_color, global_color,
note_color, note_color,
authorship_mode,
colors_enabled,
can_save_editor_settings,
})) }))
} }
pub async fn set_pad_editor_settings(
State(state): State<SharedState>, headers: HeaderMap, Path(slug): Path<String>,
Json(payload): Json<EditorSettingsRequest>,
) -> Result<Json<serde_json::Value>, ApiError> {
save_editor_settings(&state, &headers, "pad", &slug, "pad", &slug, payload).await
}
pub async fn pad_editor_color( pub async fn pad_editor_color(
State(state): State<SharedState>, State(state): State<SharedState>,
headers: HeaderMap, headers: HeaderMap,
@@ -181,11 +203,18 @@ pub async fn publish_pad_page(
.await? .await?
}; };
require_write(level)?; require_write(level)?;
let enabled = payload.enabled.unwrap_or(true);
if !enabled {
db::unpublish_pad(&state.db, pad.id).await?;
db::set_pad_public_page_disabled(&state.db, pad.id, true).await?;
return Ok(Json(PublishResponse { url: None, enabled: false }));
}
db::set_pad_public_page_disabled(&state.db, pad.id, false).await?;
let token = db::publish_pad(&state.db, pad.id).await?; let token = db::publish_pad(&state.db, pad.id).await?;
db::set_pad_public_task_updates(&state.db, pad.id, payload.allow_task_updates).await?; db::set_pad_public_task_updates(&state.db, pad.id, payload.allow_task_updates).await?;
db::set_pad_public_page_unprotected(&state.db, pad.id, payload.unprotect_page).await?; db::set_pad_public_page_unprotected(&state.db, pad.id, payload.unprotect_page).await?;
Ok(Json(PublishResponse { Ok(Json(PublishResponse {
url: format!("/s/{token}"), url: Some(format!("/s/{token}")), enabled: true,
})) }))
} }
@@ -219,11 +248,18 @@ pub async fn publish_note_page(
.await? .await?
}; };
require_write(level)?; require_write(level)?;
let enabled = payload.enabled.unwrap_or(true);
if !enabled {
db::unpublish_note(&state.db, note.id).await?;
db::set_note_public_page_disabled(&state.db, note.id, true).await?;
return Ok(Json(PublishResponse { url: None, enabled: false }));
}
db::set_note_public_page_disabled(&state.db, note.id, false).await?;
let token = db::publish_note(&state.db, note.id).await?; let token = db::publish_note(&state.db, note.id).await?;
db::set_note_public_task_updates(&state.db, note.id, payload.allow_task_updates).await?; db::set_note_public_task_updates(&state.db, note.id, payload.allow_task_updates).await?;
db::set_note_public_page_unprotected(&state.db, note.id, payload.unprotect_page).await?; db::set_note_public_page_unprotected(&state.db, note.id, payload.unprotect_page).await?;
Ok(Json(PublishResponse { Ok(Json(PublishResponse {
url: format!("/s/{token}"), url: Some(format!("/s/{token}")), enabled: true,
})) }))
} }
+2
View File
@@ -118,6 +118,7 @@ pub fn router(
"/api/pads/{slug}/editor-color", "/api/pads/{slug}/editor-color",
get(api::pad_editor_color).post(api::set_pad_editor_color), get(api::pad_editor_color).post(api::set_pad_editor_color),
) )
.route("/api/pads/{slug}/editor-settings", post(api::set_pad_editor_settings))
.route("/api/pads/{slug}/publish", post(api::publish_pad_page)) .route("/api/pads/{slug}/publish", post(api::publish_pad_page))
.route("/api/pads/{slug}/restore", post(api::pad_restore)) .route("/api/pads/{slug}/restore", post(api::pad_restore))
.route( .route(
@@ -146,6 +147,7 @@ pub fn router(
"/api/workspaces/{workspace_slug}/notes/{note_slug}/editor-color", "/api/workspaces/{workspace_slug}/notes/{note_slug}/editor-color",
get(api::note_editor_color).post(api::set_note_editor_color), get(api::note_editor_color).post(api::set_note_editor_color),
) )
.route("/api/workspaces/{workspace_slug}/notes/{note_slug}/editor-settings", post(api::set_note_editor_settings))
.route( .route(
"/api/workspaces/{workspace_slug}/notes/{note_slug}/publish", "/api/workspaces/{workspace_slug}/notes/{note_slug}/publish",
post(api::publish_note_page), post(api::publish_note_page),
+48
View File
@@ -94,6 +94,28 @@ pub async fn publish_note(pool: &Database, note_id: i64) -> Result<String, sqlx:
Ok(token) Ok(token)
} }
pub async fn pad_public_page_enabled(pool: &Database, pad_id: i64) -> Result<bool, sqlx::Error> {
Ok(sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q017))
.bind(pad_id).fetch_optional(pool.pool()).await?.is_some())
}
pub async fn note_public_page_enabled(pool: &Database, note_id: i64) -> Result<bool, sqlx::Error> {
Ok(sqlx::query_scalar::<_, String>(queries::get(pool.kind(), queries::Q019))
.bind(note_id).fetch_optional(pool.pool()).await?.is_some())
}
pub async fn unpublish_pad(pool: &Database, pad_id: i64) -> Result<(), sqlx::Error> {
let sql = match pool.kind() { DatabaseKind::Postgres => "DELETE FROM published_pages WHERE pad_id = $1", _ => "DELETE FROM published_pages WHERE pad_id = ?" };
sqlx::query(sql).bind(pad_id).execute(pool.pool()).await?;
Ok(())
}
pub async fn unpublish_note(pool: &Database, note_id: i64) -> Result<(), sqlx::Error> {
let sql = match pool.kind() { DatabaseKind::Postgres => "DELETE FROM published_pages WHERE note_id = $1", _ => "DELETE FROM published_pages WHERE note_id = ?" };
sqlx::query(sql).bind(note_id).execute(pool.pool()).await?;
Ok(())
}
pub async fn find_published_page( pub async fn find_published_page(
pool: &Database, pool: &Database,
token: &str, token: &str,
@@ -342,3 +364,29 @@ impl<'r> sqlx::FromRow<'r, AnyRow> for PostgresPublishedPageRow {
}) })
} }
} }
pub async fn pad_public_page_disabled(pool: &Database, pad_id: i64) -> Result<bool, sqlx::Error> {
let sql = match pool.kind() { DatabaseKind::Postgres => "SELECT public_page_disabled FROM pads WHERE id = $1", _ => "SELECT public_page_disabled FROM pads WHERE id = ?" };
if pool.kind() == DatabaseKind::Postgres { return Ok(sqlx::query_scalar::<_, bool>(sql).bind(pad_id).fetch_one(pool.pool()).await?); }
Ok(sqlx::query_scalar::<_, i64>(sql).bind(pad_id).fetch_one(pool.pool()).await? != 0)
}
pub async fn note_public_page_disabled(pool: &Database, note_id: i64) -> Result<bool, sqlx::Error> {
let sql = match pool.kind() { DatabaseKind::Postgres => "SELECT public_page_disabled FROM notes WHERE id = $1", _ => "SELECT public_page_disabled FROM notes WHERE id = ?" };
if pool.kind() == DatabaseKind::Postgres { return Ok(sqlx::query_scalar::<_, bool>(sql).bind(note_id).fetch_one(pool.pool()).await?); }
Ok(sqlx::query_scalar::<_, i64>(sql).bind(note_id).fetch_one(pool.pool()).await? != 0)
}
pub async fn set_pad_public_page_disabled(pool: &Database, pad_id: i64, disabled: bool) -> Result<(), sqlx::Error> {
let sql = match pool.kind() { DatabaseKind::Postgres => "UPDATE pads SET public_page_disabled = $1 WHERE id = $2", _ => "UPDATE pads SET public_page_disabled = ? WHERE id = ?" };
let mut query = sqlx::query(sql);
query = if pool.kind() == DatabaseKind::Postgres { query.bind(disabled) } else { query.bind(if disabled { 1i64 } else { 0i64 }) };
query.bind(pad_id).execute(pool.pool()).await?; Ok(())
}
pub async fn set_note_public_page_disabled(pool: &Database, note_id: i64, disabled: bool) -> Result<(), sqlx::Error> {
let sql = match pool.kind() { DatabaseKind::Postgres => "UPDATE notes SET public_page_disabled = $1 WHERE id = $2", _ => "UPDATE notes SET public_page_disabled = ? WHERE id = ?" };
let mut query = sqlx::query(sql);
query = if pool.kind() == DatabaseKind::Postgres { query.bind(disabled) } else { query.bind(if disabled { 1i64 } else { 0i64 }) };
query.bind(note_id).execute(pool.pool()).await?; Ok(())
}
+6
View File
@@ -21,6 +21,9 @@ pub enum Query {
RESOURCE_COLOR_BY_USER, RESOURCE_COLOR_BY_USER,
RESOURCE_COLOR_DELETE, RESOURCE_COLOR_DELETE,
RESOURCE_COLOR_INSERT, RESOURCE_COLOR_INSERT,
RESOURCE_EDITOR_SETTINGS_SELECT,
RESOURCE_EDITOR_SETTINGS_DELETE,
RESOURCE_EDITOR_SETTINGS_INSERT,
AUTH_ACCOUNT_ACTION_BY_TOKEN, AUTH_ACCOUNT_ACTION_BY_TOKEN,
AUTH_CONSUME_ACCOUNT_ACTION, AUTH_CONSUME_ACCOUNT_ACTION,
AUTH_UPDATE_EMAIL, AUTH_UPDATE_EMAIL,
@@ -160,6 +163,9 @@ pub const AUTH_EDITOR_COLOR_BY_USER: Query = Query::AUTH_EDITOR_COLOR_BY_USER;
pub const RESOURCE_COLOR_BY_USER: Query = Query::RESOURCE_COLOR_BY_USER; pub const RESOURCE_COLOR_BY_USER: Query = Query::RESOURCE_COLOR_BY_USER;
pub const RESOURCE_COLOR_DELETE: Query = Query::RESOURCE_COLOR_DELETE; pub const RESOURCE_COLOR_DELETE: Query = Query::RESOURCE_COLOR_DELETE;
pub const RESOURCE_COLOR_INSERT: Query = Query::RESOURCE_COLOR_INSERT; pub const RESOURCE_COLOR_INSERT: Query = Query::RESOURCE_COLOR_INSERT;
pub const RESOURCE_EDITOR_SETTINGS_SELECT: Query = Query::RESOURCE_EDITOR_SETTINGS_SELECT;
pub const RESOURCE_EDITOR_SETTINGS_DELETE: Query = Query::RESOURCE_EDITOR_SETTINGS_DELETE;
pub const RESOURCE_EDITOR_SETTINGS_INSERT: Query = Query::RESOURCE_EDITOR_SETTINGS_INSERT;
pub const AUTH_ACCOUNT_ACTION_BY_TOKEN: Query = Query::AUTH_ACCOUNT_ACTION_BY_TOKEN; pub const AUTH_ACCOUNT_ACTION_BY_TOKEN: Query = Query::AUTH_ACCOUNT_ACTION_BY_TOKEN;
pub const AUTH_CONSUME_ACCOUNT_ACTION: Query = Query::AUTH_CONSUME_ACCOUNT_ACTION; pub const AUTH_CONSUME_ACCOUNT_ACTION: Query = Query::AUTH_CONSUME_ACCOUNT_ACTION;
pub const AUTH_UPDATE_EMAIL: Query = Query::AUTH_UPDATE_EMAIL; pub const AUTH_UPDATE_EMAIL: Query = Query::AUTH_UPDATE_EMAIL;
+3
View File
@@ -15,6 +15,9 @@ pub fn get(query: Query) -> &'static str {
Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = ?"#, Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = ?"#,
Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#,
Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#,
Query::RESOURCE_EDITOR_SETTINGS_SELECT => r#"SELECT authorship_mode, CASE WHEN colors_enabled THEN 1 ELSE 0 END FROM resource_editor_settings WHERE resource_kind = ? AND resource_slug = ?"#,
Query::RESOURCE_EDITOR_SETTINGS_DELETE => r#"DELETE FROM resource_editor_settings WHERE resource_kind = ? AND resource_slug = ?"#,
Query::RESOURCE_EDITOR_SETTINGS_INSERT => r#"INSERT INTO resource_editor_settings (resource_kind, resource_slug, authorship_mode, colors_enabled) VALUES (?, ?, ?, ?)"#,
Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)"#, Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)"#,
Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, CAST(payload AS CHAR CHARACTER SET utf8mb4) AS payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"#, Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, CAST(payload AS CHAR CHARACTER SET utf8mb4) AS payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"#,
Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL"#, Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL"#,
+3
View File
@@ -15,6 +15,9 @@ pub fn get(query: Query) -> &'static str {
Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = $1"#, Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = $1"#,
Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = $1 AND resource_kind = $2 AND resource_slug = $3"#, Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = $1 AND resource_kind = $2 AND resource_slug = $3"#,
Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = $1 AND resource_kind = $2 AND resource_slug = $3"#, Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = $1 AND resource_kind = $2 AND resource_slug = $3"#,
Query::RESOURCE_EDITOR_SETTINGS_SELECT => r#"SELECT authorship_mode, (CASE WHEN colors_enabled THEN 1 ELSE 0 END)::BIGINT FROM resource_editor_settings WHERE resource_kind = $1 AND resource_slug = $2"#,
Query::RESOURCE_EDITOR_SETTINGS_DELETE => r#"DELETE FROM resource_editor_settings WHERE resource_kind = $1 AND resource_slug = $2"#,
Query::RESOURCE_EDITOR_SETTINGS_INSERT => r#"INSERT INTO resource_editor_settings (resource_kind, resource_slug, authorship_mode, colors_enabled) VALUES ($1, $2, $3, $4)"#,
Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES ($1, $2, $3, $4)"#, Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES ($1, $2, $3, $4)"#,
Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = $1"#, Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = $1"#,
Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = $1 WHERE token = $2 AND used_at IS NULL"#, Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = $1 WHERE token = $2 AND used_at IS NULL"#,
+3
View File
@@ -15,6 +15,9 @@ pub fn get(query: Query) -> &'static str {
Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = ?"#, Query::AUTH_EDITOR_COLOR_BY_USER => r#"SELECT editor_color FROM users WHERE id = ?"#,
Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, Query::RESOURCE_COLOR_BY_USER => r#"SELECT color FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#,
Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#, Query::RESOURCE_COLOR_DELETE => r#"DELETE FROM user_resource_colors WHERE user_id = ? AND resource_kind = ? AND resource_slug = ?"#,
Query::RESOURCE_EDITOR_SETTINGS_SELECT => r#"SELECT authorship_mode, CASE WHEN colors_enabled THEN 1 ELSE 0 END FROM resource_editor_settings WHERE resource_kind = ? AND resource_slug = ?"#,
Query::RESOURCE_EDITOR_SETTINGS_DELETE => r#"DELETE FROM resource_editor_settings WHERE resource_kind = ? AND resource_slug = ?"#,
Query::RESOURCE_EDITOR_SETTINGS_INSERT => r#"INSERT INTO resource_editor_settings (resource_kind, resource_slug, authorship_mode, colors_enabled) VALUES (?, ?, ?, ?)"#,
Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)"#, Query::RESOURCE_COLOR_INSERT => r#"INSERT INTO user_resource_colors (user_id, resource_kind, resource_slug, color) VALUES (?, ?, ?, ?)"#,
Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"#, Query::AUTH_ACCOUNT_ACTION_BY_TOKEN => r#"SELECT user_id, action, payload, expires_at, used_at FROM account_action_tokens WHERE token = ?"#,
Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL"#, Query::AUTH_CONSUME_ACCOUNT_ACTION => r#"UPDATE account_action_tokens SET used_at = ? WHERE token = ? AND used_at IS NULL"#,
+99
View File
@@ -4698,3 +4698,102 @@ dialog::backdrop {
width: 100%; width: 100%;
min-height: 0; min-height: 0;
} }
/* Compact Page settings dropdown. */
.page-settings { position: relative; }
.page-settings > summary { list-style: none; cursor: pointer; }
.page-settings > summary::-webkit-details-marker { display: none; }
.page-settings-menu { position: absolute; z-index: 30; top: calc(100% + 6px); right: 0; display: grid; gap: 4px; min-width: 220px; padding: 8px; border: 1px solid var(--border); border-radius: 9px; background: var(--panel); box-shadow: 0 12px 30px rgba(0,0,0,.28); }
.page-settings-menu .public-task-toggle { min-height: 30px; padding: 5px 7px; border-radius: 6px; }
.page-settings-menu .public-task-toggle:hover { background: color-mix(in srgb, var(--surface-strong, #262b35) 72%, transparent); }
#publish-page:disabled { opacity: .45; cursor: not-allowed; }
/* Keep the Simple/Full switch inside the editor label frame. */
.authorship-mode-control { padding: 1px; border-radius: 7px; }
.authorship-mode-control button { min-height: 20px; height: 20px; padding: 0 7px; border-radius: 5px; line-height: 20px; }
/* Make secondary editor actions read clearly as buttons. */
.markdown-more > summary,
.pad-page .toolbar-action { display: inline-flex; align-items: center; justify-content: center; min-height: 30px; padding: 5px 10px; border: 1px solid var(--border); border-radius: 7px; background: var(--surface-strong, #262b35); color: var(--text); font-weight: 600; cursor: pointer; box-shadow: inset 0 1px 0 rgba(255,255,255,.04); }
.markdown-more > summary:hover,
.pad-page .toolbar-action:hover { border-color: color-mix(in srgb, var(--accent) 55%, var(--border)); filter: brightness(1.08); }
.markdown-more > summary { list-style: none; }
.markdown-more > summary::-webkit-details-marker { display: none; }
/* Final UI fixes: opaque Page settings and per-note authorship controls. */
.page-settings-menu {
background: #171c24;
opacity: 1;
backdrop-filter: none;
}
.authorship-controls { display: inline-flex; align-items: center; gap: 7px; }
.authorship-color-toggle { display: inline-flex; align-items: center; gap: 5px; min-height: 22px; color: var(--muted); font-size: 11px; cursor: pointer; }
.authorship-color-toggle input { width: 28px; height: 16px; margin: 0; accent-color: var(--accent); }
.authorship-layer { overflow: hidden; padding: 0; }
.authorship-canvas { position: absolute; top: 0; left: 0; box-sizing: border-box; color: transparent; white-space: pre; tab-size: 4; will-change: transform; }
.share-link-row .share-link-info { justify-self: end; width: min(100%, 520px); text-align: right; }
.share-link-row .share-link-inline { width: 100%; text-align: left; }
@media (max-width: 800px) {
.share-link-row .share-link-info { justify-self: stretch; width: 100%; text-align: left; }
.authorship-controls { max-width: 100%; gap: 5px; }
}
.page-settings-menu .public-task-toggle { background: #171c24; }
.page-settings-menu .public-task-toggle:hover { background: #242b36; }
/* Shared editor display settings */
.authorship-controls { display: flex; align-items: center; gap: 7px; flex-wrap: wrap; }
.switch-control { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-size: 11px; color: var(--muted); user-select: none; }
.switch-control input { position: absolute; opacity: 0; pointer-events: none; }
.switch-control__track { position: relative; width: 30px; height: 16px; border: 1px solid var(--border); border-radius: 999px; background: var(--surface-strong, #262b35); transition: .15s ease; }
.switch-control__track::after { content: ""; position: absolute; width: 10px; height: 10px; left: 2px; top: 2px; border-radius: 50%; background: var(--muted); transition: .15s ease; }
.switch-control input:checked + .switch-control__track { border-color: var(--accent); background: color-mix(in srgb, var(--accent) 28%, var(--surface-strong, #262b35)); }
.switch-control input:checked + .switch-control__track::after { transform: translateX(14px); background: var(--accent); }
.editor-settings-save { min-height: 22px; height: 22px; padding: 0 8px; font-size: 11px; }
.editor-settings-save:disabled { opacity: .45; cursor: not-allowed; }
.share-link-row .share-link-info { margin-top: 8px; justify-self: start; text-align: left; }
.preview-editable--source {
font-family: var(--font-mono, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace);
white-space: pre-wrap;
}
/* Keep generated individual links below the new-link form. */
.share-link-list-wrap {
display: grid;
gap: 8px;
margin-top: 12px;
padding-top: 12px;
border-top: 1px solid var(--border);
}
.share-link-list-wrap h5 {
margin: 0;
font-size: .86rem;
}
.share-link-list {
width: 100%;
}
.share-link-row {
grid-template-columns: minmax(130px, auto) 100px auto auto;
align-items: end;
}
.share-link-row .share-link-info {
grid-column: 1 / -1;
width: 100%;
justify-self: stretch;
text-align: left;
margin-top: 0;
}
.share-link-row .share-link-inline {
width: 100%;
}
@media (max-width: 800px) {
.share-link-row {
grid-template-columns: 1fr;
}
.share-link-row .share-link-info {
grid-column: 1;
}
}
+19 -17
View File
@@ -175,31 +175,33 @@ export function renderAuthorshipLayer(layer, editor, model, colorFor) {
if (!layer) return; if (!layer) return;
const style = getComputedStyle(editor); const style = getComputedStyle(editor);
layer.style.left = `${editor.offsetLeft}px`; layer.style.left = `${editor.offsetLeft}px`;
layer.style.paddingTop = style.paddingTop; const canvas = document.createElement("div");
layer.style.paddingRight = style.paddingRight; canvas.className = "authorship-canvas";
layer.style.paddingBottom = style.paddingBottom; canvas.style.paddingTop = style.paddingTop;
layer.style.paddingLeft = style.paddingLeft; canvas.style.paddingRight = style.paddingRight;
layer.style.fontFamily = style.fontFamily; canvas.style.paddingBottom = style.paddingBottom;
layer.style.fontSize = style.fontSize; canvas.style.paddingLeft = style.paddingLeft;
layer.style.fontWeight = style.fontWeight; canvas.style.fontFamily = style.fontFamily;
layer.style.lineHeight = style.lineHeight; canvas.style.fontSize = style.fontSize;
layer.style.letterSpacing = style.letterSpacing; canvas.style.fontWeight = style.fontWeight;
canvas.style.lineHeight = style.lineHeight;
canvas.style.letterSpacing = style.letterSpacing;
canvas.style.minWidth = `${editor.scrollWidth}px`;
canvas.style.minHeight = `${editor.scrollHeight}px`;
const text = editor.value; const text = editor.value;
const fragment = document.createDocumentFragment();
let cursor = 0; let cursor = 0;
for (const span of normalize(model?.spans, text.length)) { for (const span of normalize(model?.spans, text.length)) {
if (span.start > cursor) fragment.append(document.createTextNode(text.slice(cursor, span.start))); if (span.start > cursor) canvas.append(document.createTextNode(text.slice(cursor, span.start)));
const mark = document.createElement("span"); const mark = document.createElement("span");
mark.className = "authorship-fragment"; mark.className = "authorship-fragment";
mark.style.setProperty("--owner", colorFor(span.owner)); mark.style.setProperty("--owner", colorFor(span.owner));
mark.textContent = text.slice(span.start, span.end); mark.textContent = text.slice(span.start, span.end);
mark.title = span.owner.split("\u001f", 1)[0]; mark.title = span.owner.split("\u001f", 1)[0];
fragment.append(mark); canvas.append(mark);
cursor = span.end; cursor = span.end;
} }
if (cursor < text.length) fragment.append(document.createTextNode(text.slice(cursor))); if (cursor < text.length) canvas.append(document.createTextNode(text.slice(cursor)));
if (!text.endsWith("\n")) fragment.append(document.createTextNode("\n")); if (!text.endsWith("\n")) canvas.append(document.createTextNode("\n"));
layer.replaceChildren(fragment); canvas.style.transform = `translate3d(${-editor.scrollLeft}px, ${-editor.scrollTop}px, 0)`;
layer.scrollTop = editor.scrollTop; layer.replaceChildren(canvas);
layer.scrollLeft = editor.scrollLeft;
} }
+1 -1
View File
@@ -158,7 +158,7 @@ async function loadResources() {
<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>
</form><div data-link-list class="share-list"></div></section> </form><div class="share-link-list-wrap"><h5>Individual links</h5><div data-link-list class="share-list share-link-list"></div></div></section>
</div> </div>
<div class="share-dialog-footer"><p class="form-message resource-inline-message" data-inline-message role="status"></p><button class="secondary-button" type="button" data-done>Done</button></div> <div class="share-dialog-footer"><p class="form-message resource-inline-message" data-inline-message role="status"></p><button class="secondary-button" type="button" data-done>Done</button></div>
</div>`; </div>`;
+6 -4
View File
@@ -17,6 +17,7 @@ export function createPadAdapter() {
loadInfo: headers => api(base, { headers }), loadInfo: headers => api(base, { headers }),
loadColor: headers => api(`${base}/editor-color`, { headers }), loadColor: headers => api(`${base}/editor-color`, { headers }),
saveColor: (headers, color) => api(`${base}/editor-color`, { method: "POST", headers, body: JSON.stringify({ color }) }), saveColor: (headers, color) => api(`${base}/editor-color`, { method: "POST", headers, body: JSON.stringify({ color }) }),
saveEditorSettings: (headers, settings) => api(`${base}/editor-settings`, { method: "POST", headers, body: JSON.stringify(settings) }),
fileEndpoints: { fileEndpoints: {
list: `${base}/files`, list: `${base}/files`,
upload: `${base}/files`, upload: `${base}/files`,
@@ -27,9 +28,9 @@ export function createPadAdapter() {
method: "POST", method: "POST",
body: JSON.stringify({ kind: "pad", slug, password }), body: JSON.stringify({ kind: "pad", slug, password }),
}), }),
publish: (accessToken, allowTaskUpdates, unprotectPage) => api(`${base}/publish`, { publish: (accessToken, allowTaskUpdates, unprotectPage, enabled = true) => api(`${base}/publish`, {
method: "POST", method: "POST",
body: JSON.stringify({ access_token: accessToken || null, allow_task_updates: allowTaskUpdates, unprotect_page: unprotectPage }), body: JSON.stringify({ access_token: accessToken || null, allow_task_updates: allowTaskUpdates, unprotect_page: unprotectPage, enabled }),
}), }),
loadHistory: accessToken => api(`${base}/history`, { loadHistory: accessToken => api(`${base}/history`, {
method: "POST", method: "POST",
@@ -57,6 +58,7 @@ export function createWorkspaceNoteAdapter() {
loadInfo: headers => api(base, { headers }), loadInfo: headers => api(base, { headers }),
loadColor: headers => api(`${base}/editor-color`, { headers }), loadColor: headers => api(`${base}/editor-color`, { headers }),
saveColor: (headers, color) => api(`${base}/editor-color`, { method: "POST", headers, body: JSON.stringify({ color }) }), saveColor: (headers, color) => api(`${base}/editor-color`, { method: "POST", headers, body: JSON.stringify({ color }) }),
saveEditorSettings: (headers, settings) => api(`${base}/editor-settings`, { method: "POST", headers, body: JSON.stringify(settings) }),
fileEndpoints: { fileEndpoints: {
list: `${base}/files`, list: `${base}/files`,
upload: `${base}/files`, upload: `${base}/files`,
@@ -67,9 +69,9 @@ export function createWorkspaceNoteAdapter() {
method: "POST", method: "POST",
body: JSON.stringify({ kind: "workspace", slug: workspaceSlug, password }), body: JSON.stringify({ kind: "workspace", slug: workspaceSlug, password }),
}), }),
publish: (accessToken, allowTaskUpdates, unprotectPage) => api(`${base}/publish`, { publish: (accessToken, allowTaskUpdates, unprotectPage, enabled = true) => api(`${base}/publish`, {
method: "POST", method: "POST",
body: JSON.stringify({ access_token: accessToken || null, allow_task_updates: allowTaskUpdates, unprotect_page: unprotectPage }), body: JSON.stringify({ access_token: accessToken || null, allow_task_updates: allowTaskUpdates, unprotect_page: unprotectPage, enabled }),
}), }),
loadHistory: accessToken => api(`${base}/history`, { loadHistory: accessToken => api(`${base}/history`, {
method: "POST", method: "POST",
+139 -15
View File
@@ -17,9 +17,10 @@ export function startNoteEditor(adapter) {
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"), mobileChatUnread = document.querySelector("#mobile-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"), unprotectPublicPage = document.querySelector("#unprotect-public-page"), participantBadges = document.querySelector("#participant-badges"), 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"), authorshipColorsToggle = document.querySelector("#authorship-colors-toggle"), authorshipColorsLabel = document.querySelector("#authorship-colors-label"), saveEditorSettingsButton = document.querySelector("#save-editor-settings"), publicPageEnabled = document.querySelector("#public-page-enabled"), publicTaskUpdates = document.querySelector("#public-task-updates"), unprotectPublicPage = document.querySelector("#unprotect-public-page"), participantBadges = document.querySelector("#participant-badges"), 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);
let accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = ["full", "advanced"].includes(localStorage.getItem("rustpad:authorship-mode")) ? "full" : "simple"; const notePreferenceKey = name => `rustpad:${name}:${adapter.access.kind}:${adapter.access.key}`;
let accessToken = shareToken || getAccessToken(adapter.access.kind, adapter.access.key), password = "", nickname = getNickname(), info, socket, saveTimer, applyingRemote = false, resourceUnlocked = false, uiState = readEditorState(), authorship = parseAuthorship("", "[]"), previousContent = "", globalColor = "", noteColor = "", presenceUsers = [], authorshipMode = "simple", authorshipColorsEnabled = true;
const compactLayoutQuery = window.matchMedia("(max-width: 1499px)"); const compactLayoutQuery = window.matchMedia("(max-width: 1499px)");
let compactView = uiState.view === "preview" ? "preview" : "edit"; let compactView = uiState.view === "preview" ? "preview" : "edit";
const lineToggle = document.querySelector("#line-numbers-toggle"), previewLineToggle = document.querySelector("#preview-line-numbers-toggle"); lineToggle.checked = localStorage.getItem("rustpad:line-numbers") !== "off"; const lineToggle = document.querySelector("#line-numbers-toggle"), previewLineToggle = document.querySelector("#preview-line-numbers-toggle"); lineToggle.checked = localStorage.getItem("rustpad:line-numbers") !== "off";
@@ -27,6 +28,12 @@ export function startNoteEditor(adapter) {
compactToggle.checked = localStorage.getItem("rustpad:compact") !== "off"; compactToggle.checked = localStorage.getItem("rustpad:compact") !== "off";
fontFamily.value = localStorage.getItem("rustpad:font-family") || "mono"; fontFamily.value = localStorage.getItem("rustpad:font-family") || "mono";
fontSize.value = localStorage.getItem("rustpad:font-size") || "14"; fontSize.value = localStorage.getItem("rustpad:font-size") || "14";
authorshipColorsToggle.checked = authorshipColorsEnabled;
function updateAuthorshipControls() {
authorshipColorsToggle.checked = authorshipColorsEnabled;
authorshipColorsLabel.textContent = authorshipColorsEnabled ? "Colors on" : "Colors off";
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.classList.toggle("active", button.dataset.authorshipMode === authorshipMode));
}
function defaultColorFor(name) { let h = 0; for (const c of name || "?") h = (h * 31 + c.charCodeAt(0)) % 360; return `hsl(${h} 70% 62%)`; } function defaultColorFor(name) { let h = 0; for (const c of name || "?") h = (h * 31 + c.charCodeAt(0)) % 360; return `hsl(${h} 70% 62%)`; }
function ownerParts(owner) { const raw = String(owner || ""); const split = raw.lastIndexOf("\u001f"); return split < 0 ? { name: raw, color: "" } : { name: raw.slice(0, split), color: raw.slice(split + 1) }; } function ownerParts(owner) { const raw = String(owner || ""); const split = raw.lastIndexOf("\u001f"); return split < 0 ? { name: raw, color: "" } : { name: raw.slice(0, split), color: raw.slice(split + 1) }; }
function ownerName(owner) { return ownerParts(owner).name; } function ownerName(owner) { return ownerParts(owner).name; }
@@ -59,6 +66,10 @@ export function startNoteEditor(adapter) {
} else { } else {
noteColor = readGuestColor(); noteColor = readGuestColor();
} }
authorshipMode = info.authorship_mode === "full" ? "full" : "simple";
authorshipColorsEnabled = info.colors_enabled !== false;
updateAuthorshipControls();
if (saveEditorSettingsButton) saveEditorSettingsButton.disabled = !info.can_save_editor_settings;
updateCurrentUser(); return info; updateCurrentUser(); return info;
} }
function updatePresence(users) { const entries = Array.isArray(users) ? users : []; presenceUsers = entries.map(entry => typeof entry === "string" ? { name: entry, color: "" } : entry || {}); roomCount.textContent = `${entries.length} ${entries.length === 1 ? "user" : "users"}`; roomUsers.replaceChildren(...presenceUsers.map(user => { 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); } renderGutter(); } function updatePresence(users) { const entries = Array.isArray(users) ? users : []; presenceUsers = entries.map(entry => typeof entry === "string" ? { name: entry, color: "" } : entry || {}); roomCount.textContent = `${entries.length} ${entries.length === 1 ? "user" : "users"}`; roomUsers.replaceChildren(...presenceUsers.map(user => { 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); } renderGutter(); }
@@ -95,12 +106,12 @@ export function startNoteEditor(adapter) {
const lineCount = Math.max(1, (editor.value.match(/\n/g) || []).length + 1); const lineCount = Math.max(1, (editor.value.match(/\n/g) || []).length + 1);
const lines = Array.from({ length: lineCount }); const lines = Array.from({ length: lineCount });
const owners = authorshipOwners(authorship); const owners = authorshipOwners(authorship);
const showAuthorship = owners.length > 0; const showAuthorship = authorshipColorsEnabled && owners.length > 0;
const authorsByLine = showAuthorship ? lineAuthors(editor.value, authorship) : []; const authorsByLine = showAuthorship ? lineAuthors(editor.value, authorship) : [];
const full = authorshipMode === "full"; const full = authorshipMode === "full";
authorshipLayer.hidden = !showAuthorship; authorshipLayer.hidden = !showAuthorship;
ownerLabels.hidden = !full || !showAuthorship; ownerLabels.hidden = !full || !showAuthorship;
renderParticipantBadges(owners); renderParticipantBadges(authorshipColorsEnabled ? owners : []);
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.classList.toggle("active", button.dataset.authorshipMode === authorshipMode)); document.querySelectorAll("[data-authorship-mode]").forEach(button => button.classList.toggle("active", button.dataset.authorshipMode === authorshipMode));
editorWorkspace.dataset.authorshipMode = authorshipMode; editorWorkspace.dataset.authorshipMode = authorshipMode;
const style = getComputedStyle(editor), lineHeight = parseFloat(style.lineHeight) || 29, paddingTop = parseFloat(style.paddingTop) || 24, paddingBottom = parseFloat(style.paddingBottom) || 24; const style = getComputedStyle(editor), lineHeight = parseFloat(style.lineHeight) || 29, paddingTop = parseFloat(style.paddingTop) || 24, paddingBottom = parseFloat(style.paddingBottom) || 24;
@@ -146,10 +157,10 @@ export function startNoteEditor(adapter) {
const title = current.getAttribute("title"); const title = current.getAttribute("title");
return `![${alt}](${src}${title ? ` "${title.replace(/"/g, "&quot;")}"` : ""})`; return `![${alt}](${src}${title ? ` "${title.replace(/"/g, "&quot;")}"` : ""})`;
} }
if (tag === "br") return " "; if (tag === "br") return "\n";
return body; return body;
}; };
return [...node.childNodes].map(walk).join("").replace(/\n/g, " ").trim(); return [...node.childNodes].map(walk).join("").replace(/\u00a0/g, " ");
} }
function previewCaretOffset(target) { function previewCaretOffset(target) {
@@ -219,11 +230,6 @@ export function startNoteEditor(adapter) {
editorWorkspace.style.setProperty("--editor-font-size", `${fontSize.value}px`); editorWorkspace.style.setProperty("--editor-font-size", `${fontSize.value}px`);
document.body.classList.toggle("compact-editor", compactToggle.checked); document.body.classList.toggle("compact-editor", compactToggle.checked);
document.body.classList.toggle("compact-note-layout", compactLayoutQuery.matches); document.body.classList.toggle("compact-note-layout", compactLayoutQuery.matches);
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.addEventListener("click", () => {
authorshipMode = button.dataset.authorshipMode === "full" ? "full" : "simple";
localStorage.setItem("rustpad:authorship-mode", authorshipMode);
renderGutter();
}));
document.querySelectorAll("[data-view]").forEach(button => { document.querySelectorAll("[data-view]").forEach(button => {
const active = button.dataset.view === view; const active = button.dataset.view === view;
button.classList.toggle("active", active); button.classList.toggle("active", active);
@@ -274,7 +280,7 @@ export function startNoteEditor(adapter) {
accessToken = shareToken || getAuthToken() || getAccessToken(adapter.access.kind, adapter.access.key); accessToken = shareToken || getAuthToken() || getAccessToken(adapter.access.kind, adapter.access.key);
await loadNoteInfo(); await loadNoteInfo();
document.title = adapter.title(info); document.title = adapter.title(info);
publicTaskUpdates.checked = Boolean(info.allow_public_task_updates); unprotectPublicPage.checked = Boolean(info.public_page_unprotected); publicPageEnabled.checked = Boolean(info.public_page_enabled); publicTaskUpdates.checked = Boolean(info.allow_public_task_updates); unprotectPublicPage.checked = Boolean(info.public_page_unprotected); updatePageControls();
adapter.configureView?.(info); adapter.configureView?.(info);
applyUi({ write: true, replace: true }); applyUi({ write: true, replace: true });
updateCurrentUser(); updateCurrentUser();
@@ -394,9 +400,127 @@ export function startNoteEditor(adapter) {
mobileBubbleDrag.addEventListener("pointercancel", end); mobileBubbleDrag.addEventListener("pointercancel", end);
}); });
window.addEventListener("resize", () => { if (mobileBubble?.style.left) placeMobileBubble(mobileBubble.getBoundingClientRect()); }); 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 }); const cancelledPreviewEdits = new WeakSet();
const savePublicOptions = async () => adapter.publish(accessToken, publicTaskUpdates.checked, unprotectPublicPage.checked); function commitPreviewEdit(target, { focusNextLine = false } = {}) {
publicTaskUpdates.addEventListener("change", async () => { publicTaskUpdates.disabled = true; try { await savePublicOptions(); toast(publicTaskUpdates.checked ? "Public task updates enabled" : "Public task updates disabled"); } catch (error) { publicTaskUpdates.checked = !publicTaskUpdates.checked; toast(error.message); } finally { publicTaskUpdates.disabled = false; } }); unprotectPublicPage.addEventListener("change", async () => { unprotectPublicPage.disabled = true; try { await savePublicOptions(); toast(unprotectPublicPage.checked ? "Published page is now unprotected" : "Published page protection enabled"); } catch (error) { unprotectPublicPage.checked = !unprotectPublicPage.checked; toast(error.message); } finally { unprotectPublicPage.disabled = false; } }); document.querySelector("#publish-page").addEventListener("click", async () => { try { const result = await savePublicOptions(); 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); } }); const lineIndex = Number(target.dataset.sourceLine) - 1;
if (lineIndex < 0) return;
const value = markdownFromPreview(target);
const lines = editor.value.split("\n");
if (target.dataset.rawSourceEdit === "true") {
if (value === lines[lineIndex]) return;
lines[lineIndex] = value.replace(/\n/g, "");
editor.value = lines.join("\n");
editor.dispatchEvent(new Event("input", { bubbles: true }));
return;
}
if (!focusNextLine && value === target.dataset.originalValue) return;
if (focusNextLine) cancelledPreviewEdits.add(target);
if (target.dataset.tableCell !== undefined) {
lines[lineIndex] = replaceTableCell(lines[lineIndex], Number(target.dataset.tableCell), value.replace(/\n/g, " "));
if (focusNextLine) lines.splice(lineIndex + 1, 0, "");
} else {
const prefix = target.dataset.sourcePrefix || "", suffix = target.dataset.sourceSuffix || "";
const editedLines = value.split("\n");
const replacements = editedLines.map((part, index) => `${index === 0 ? prefix : ""}${part}${index === editedLines.length - 1 ? suffix : ""}`);
lines.splice(lineIndex, 1, ...replacements);
}
editor.value = lines.join("\n");
editor.dispatchEvent(new Event("input", { bubbles: true }));
if (focusNextLine) {
const nextLine = lineIndex + Math.max(2, value.split("\n").length);
const next = preview.querySelector(`[data-source-line="${nextLine}"].preview-editable`);
next?.focus();
if (next) placePreviewCaret(next, 0);
}
}
function editRawPreviewLine(target) {
const lineIndex = Number(target.dataset.sourceLine) - 1;
const lines = editor.value.split("\n");
if (lineIndex < 0 || lineIndex >= lines.length) return;
const caretOffset = Math.min(previewCaretOffset(target), lines[lineIndex].length);
target.dataset.rawSourceEdit = "true";
target.dataset.originalValue = lines[lineIndex];
target.textContent = lines[lineIndex];
target.classList.add("preview-editable--source");
target.focus({ preventScroll: true });
placePreviewCaret(target, caretOffset);
}
function insertPreviewLineBreak(target) {
const lineIndex = Number(target.dataset.sourceLine) - 1;
if (lineIndex < 0) return;
const lines = editor.value.split("\n");
const value = markdownFromPreview(target);
let insertedLineIndex;
if (target.dataset.tableCell !== undefined) {
lines[lineIndex] = replaceTableCell(lines[lineIndex], Number(target.dataset.tableCell), value.replace(/\n/g, " "));
insertedLineIndex = lineIndex + 1;
lines.splice(insertedLineIndex, 0, "");
} else {
const prefix = target.dataset.sourcePrefix || "", suffix = target.dataset.sourceSuffix || "";
const editedLines = value.split("\n");
const replacements = editedLines.map((part, index) => `${index === 0 ? prefix : ""}${part}${index === editedLines.length - 1 ? suffix : ""}`);
insertedLineIndex = lineIndex + replacements.length;
lines.splice(lineIndex, 1, ...replacements, "");
}
cancelledPreviewEdits.add(target);
editor.value = lines.join("\n");
editor.dispatchEvent(new Event("input", { bubbles: true }));
const sourceLine = insertedLineIndex + 1;
const restoreFocus = () => {
const next = preview.querySelector(`[data-source-line="${sourceLine}"].preview-editable`);
if (!next) return;
next.focus({ preventScroll: true });
placePreviewCaret(next, 0);
};
restoreFocus();
queueMicrotask(() => {
const active = document.activeElement;
if (!active || active === document.body || !preview.contains(active)) restoreFocus();
});
}
document.querySelectorAll("[data-authorship-mode]").forEach(button => button.addEventListener("click", () => {
authorshipMode = button.dataset.authorshipMode === "full" ? "full" : "simple";
updateAuthorshipControls();
renderGutter();
}));
authorshipColorsToggle?.addEventListener("change", () => {
authorshipColorsEnabled = authorshipColorsToggle.checked;
updateAuthorshipControls();
renderGutter();
});
saveEditorSettingsButton?.addEventListener("click", async () => {
if (!info?.can_save_editor_settings) return;
saveEditorSettingsButton.disabled = true;
try {
await adapter.saveEditorSettings(sessionHeaders(), { authorship_mode: authorshipMode, colors_enabled: authorshipColorsEnabled });
toast("Editor settings saved for everyone");
} catch (error) {
toast(error.message);
} finally {
saveEditorSettingsButton.disabled = !info?.can_save_editor_settings;
}
});
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("focusin", event => { const target = event.target.closest(".preview-editable"); if (!target) return; target.dataset.originalHtml = target.innerHTML; target.dataset.originalValue = markdownFromPreview(target); }); preview.addEventListener("beforeinput", event => { if (!event.target.closest(".preview-editable")) return; if (event.inputType === "insertParagraph" || event.inputType === "insertLineBreak") event.preventDefault(); }); preview.addEventListener("keydown", event => { const target = event.target.closest(".preview-editable"); if (!target) return; if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); if (target.dataset.rawSourceEdit === "true") { cancelledPreviewEdits.add(target); render(); } else editRawPreviewLine(target); return; } if (event.key === "Enter") { event.preventDefault(); event.stopPropagation(); if (event.altKey) insertPreviewLineBreak(target); else 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; if (cancelledPreviewEdits.has(target)) { cancelledPreviewEdits.delete(target); return; } commitPreviewEdit(target); }, { capture: true });
const publishPageButton = document.querySelector("#publish-page");
function updatePageControls() {
const enabled = publicPageEnabled.checked;
publishPageButton.disabled = !enabled;
publicTaskUpdates.disabled = !enabled;
unprotectPublicPage.disabled = !enabled;
}
const savePublicOptions = async () => adapter.publish(accessToken, publicTaskUpdates.checked, unprotectPublicPage.checked, publicPageEnabled.checked);
publicPageEnabled.addEventListener("change", async () => {
const previous = !publicPageEnabled.checked;
updatePageControls();
publicPageEnabled.disabled = true;
try { await savePublicOptions(); toast(publicPageEnabled.checked ? "Page enabled" : "Page disabled"); }
catch (error) { publicPageEnabled.checked = previous; updatePageControls(); toast(error.message); }
finally { publicPageEnabled.disabled = false; }
});
publicTaskUpdates.addEventListener("change", async () => { publicTaskUpdates.disabled = true; try { await savePublicOptions(); toast(publicTaskUpdates.checked ? "Public task updates enabled" : "Public task updates disabled"); } catch (error) { publicTaskUpdates.checked = !publicTaskUpdates.checked; toast(error.message); } finally { updatePageControls(); } });
unprotectPublicPage.addEventListener("change", async () => { unprotectPublicPage.disabled = true; try { await savePublicOptions(); toast(unprotectPublicPage.checked ? "Published page is now unprotected" : "Published page protection enabled"); } catch (error) { unprotectPublicPage.checked = !unprotectPublicPage.checked; toast(error.message); } finally { updatePageControls(); } });
publishPageButton.addEventListener("click", async () => { if (!publicPageEnabled.checked) return; try { const result = await savePublicOptions(); if (!result.url) throw new Error("Page is disabled"); 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(() => { }); } else { roomDetails.classList.remove("is-mobile-open"); } }); 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(); });
+2 -5
View File
@@ -30,10 +30,7 @@
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span> <span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
</button> </button>
<div id="header-actions" class="header-actions"><button id="copy-link" <div id="header-actions" class="header-actions"><button id="copy-link"
class="secondary-button">Copy link</button><button id="publish-page" class="secondary-button">Copy link</button><button id="publish-page" class="secondary-button">Page</button><details class="page-settings"><summary class="secondary-button">Page settings</summary><div class="page-settings-menu"><label class="public-task-toggle" title="Enable or disable the published page"><input id="public-page-enabled" type="checkbox"> Enable Page</label><label class="public-task-toggle" title="Allow visitors to update task checkboxes on the published page"><input id="public-task-updates" type="checkbox"> Editable tasks</label><label class="public-task-toggle" title="Allow the published page to open without the resource password or private access"><input id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div></details><button id="files-button"
class="secondary-button">Page</button><div class="public-page-options"><label class="public-task-toggle"
title="Allow visitors to update task checkboxes on the published page"><input id="public-task-updates"
type="checkbox"> Editable tasks on Page</label><label class="public-task-toggle" title="Allow the published page to open without the note password or private access"><input id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div><button id="files-button"
class="secondary-button">Files</button><button id="delete-note" class="secondary-button danger-button" class="secondary-button">Files</button><button id="delete-note" class="secondary-button danger-button"
hidden>Delete</button><button id="history-button" class="secondary-button">History</button></div> hidden>Delete</button><button id="history-button" class="secondary-button">History</button></div>
</div> </div>
@@ -104,7 +101,7 @@
</div> </div>
<div id="editor-workspace" class="workspace view-split"> <div id="editor-workspace" class="workspace view-split">
<div class="editor-column"> <div class="editor-column">
<div class="column-label editor-column-label"><span>Editor</span><div class="authorship-mode-control" role="group" aria-label="Authorship display"><button type="button" data-authorship-mode="simple" class="active">Simple</button><button type="button" data-authorship-mode="full">Full</button></div></div><div id="participant-badges" class="participant-badges" aria-label="Participants"></div> <div class="column-label editor-column-label"><span>Editor</span><div class="authorship-controls"><label class="switch-control authorship-colors-switch" title="Show or hide author coloring"><input id="authorship-colors-toggle" type="checkbox" checked><span class="switch-control__track" aria-hidden="true"></span><span id="authorship-colors-label">Colors on</span></label><div class="authorship-mode-control" role="group" aria-label="Authorship display"><button type="button" data-authorship-mode="simple" class="active">Simple</button><button type="button" data-authorship-mode="full">Full</button></div><button id="save-editor-settings" class="secondary-button compact-button editor-settings-save" type="button">Save</button></div></div><div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
<div class="editor-shell"> <div class="editor-shell">
<div id="line-gutter" class="line-gutter" aria-hidden="true"></div> <div id="line-gutter" class="line-gutter" aria-hidden="true"></div>
<div id="authorship-layer" class="authorship-layer" aria-hidden="true"></div><div id="owner-labels" class="owner-labels" aria-hidden="true"></div><textarea id="editor" <div id="authorship-layer" class="authorship-layer" aria-hidden="true"></div><div id="owner-labels" class="owner-labels" aria-hidden="true"></div><textarea id="editor"
+3 -6
View File
@@ -32,10 +32,7 @@
<span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span> <span aria-hidden="true"></span><span aria-hidden="true"></span><span aria-hidden="true"></span>
</button> </button>
<div id="header-actions" class="header-actions"><button id="copy-link" class="secondary-button">Copy <div id="header-actions" class="header-actions"><button id="copy-link" class="secondary-button">Copy
link</button><button id="publish-page" class="secondary-button">Page</button><div class="public-page-options"><label link</button><button id="publish-page" class="secondary-button">Page</button><details class="page-settings"><summary class="secondary-button">Page settings</summary><div class="page-settings-menu"><label class="public-task-toggle" title="Enable or disable the published page"><input id="public-page-enabled" type="checkbox"> Enable Page</label><label class="public-task-toggle" title="Allow visitors to update task checkboxes on the published page"><input id="public-task-updates" type="checkbox"> Editable tasks</label><label class="public-task-toggle" title="Allow the published page to open without the resource password or private access"><input id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div></details><button
class="public-task-toggle"
title="Allow visitors to update task checkboxes on the published page"><input
id="public-task-updates" type="checkbox"> Editable tasks on Page</label><label class="public-task-toggle" title="Allow the published page to open without the note password or private access"><input id="unprotect-public-page" type="checkbox"> Unprotect Page</label></div><button
id="files-button" class="secondary-button">Files</button><button id="history-button" id="files-button" class="secondary-button">Files</button><button id="history-button"
class="secondary-button">History</button></div> class="secondary-button">History</button></div>
</div> </div>
@@ -107,7 +104,7 @@
</div> </div>
<div id="editor-workspace" class="workspace view-split"> <div id="editor-workspace" class="workspace view-split">
<div class="editor-column"> <div class="editor-column">
<div class="column-label editor-column-label"><span>Editor</span><div class="authorship-mode-control" role="group" aria-label="Authorship display"><button type="button" data-authorship-mode="simple" class="active">Simple</button><button type="button" data-authorship-mode="full">Full</button></div></div><div id="participant-badges" class="participant-badges" aria-label="Participants"></div> <div class="column-label editor-column-label"><span>Editor</span><div class="authorship-controls"><label class="switch-control authorship-colors-switch" title="Show or hide author coloring"><input id="authorship-colors-toggle" type="checkbox" checked><span class="switch-control__track" aria-hidden="true"></span><span id="authorship-colors-label">Colors on</span></label><div class="authorship-mode-control" role="group" aria-label="Authorship display"><button type="button" data-authorship-mode="simple" class="active">Simple</button><button type="button" data-authorship-mode="full">Full</button></div><button id="save-editor-settings" class="secondary-button compact-button editor-settings-save" type="button">Save</button></div></div><div id="participant-badges" class="participant-badges" aria-label="Participants"></div>
<div class="editor-shell"> <div class="editor-shell">
<div id="line-gutter" class="line-gutter" aria-hidden="true"></div> <div id="line-gutter" class="line-gutter" aria-hidden="true"></div>
<div id="authorship-layer" class="authorship-layer" aria-hidden="true"></div> <div id="authorship-layer" class="authorship-layer" aria-hidden="true"></div>
@@ -171,7 +168,7 @@
<div class="shortcut-grid"> <div class="shortcut-grid">
<kbd>Ctrl/Cmd+Z</kbd><span>Undo</span><kbd>Ctrl/Cmd+B</kbd><span>Bold</span><kbd>Ctrl/Cmd+I</kbd><span>Italic</span><kbd>Ctrl/Cmd+Shift+X</kbd><span>Strikethrough</span><kbd>Ctrl/Cmd+K</kbd><span>Link</span><kbd>Ctrl/Cmd+Shift+7</kbd><span>Numbered <kbd>Ctrl/Cmd+Z</kbd><span>Undo</span><kbd>Ctrl/Cmd+B</kbd><span>Bold</span><kbd>Ctrl/Cmd+I</kbd><span>Italic</span><kbd>Ctrl/Cmd+Shift+X</kbd><span>Strikethrough</span><kbd>Ctrl/Cmd+K</kbd><span>Link</span><kbd>Ctrl/Cmd+Shift+7</kbd><span>Numbered
list</span><kbd>Ctrl/Cmd+Shift+8</kbd><span>Bullet list</span><kbd>Ctrl/Cmd+Shift+9</kbd><span>Task list</span><kbd>Ctrl/Cmd+Shift+8</kbd><span>Bullet list</span><kbd>Ctrl/Cmd+Shift+9</kbd><span>Task
list</span><kbd>Alt+1…4</kbd><span>Headings H1H4</span> list</span><kbd>Alt+1…4</kbd><span>Headings H1H4</span><kbd>Alt+Enter</kbd><span>New line while editing Preview</span><kbd>Esc</kbd><span>Edit raw Markdown of current Preview line</span>
</div> </div>
</div> </div>
</dialog> </dialog>