share invitation
This commit is contained in:
+3
-1
@@ -64,10 +64,12 @@ MYSQL_USER=rustpad
|
||||
MYSQL_PASSWORD=rustpad
|
||||
MYSQL_ROOT_PASSWORD=rustpad_root
|
||||
|
||||
# Optional account password reset via SMTP
|
||||
# Optional settings
|
||||
REGISTRATION_ENABLED=false
|
||||
ACCOUNT_CONFIRMATION_REQUIRED=false
|
||||
SHARE_CONFIRMATION_REQUIRED=true
|
||||
|
||||
# smtp mailing
|
||||
PUBLIC_URL=https://pad.example.com
|
||||
# SMTP_HOST=smtp.example.com
|
||||
SMTP_PORT=587
|
||||
|
||||
Generated
+1271
-59
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "rustpad"
|
||||
version = "0.0.10"
|
||||
version = "0.0.11"
|
||||
edition = "2024"
|
||||
rust-version = "1.94"
|
||||
description = "Collaborative Markdown notepad built with Axum, WebSockets and SQLite, PostgreSQL and MySQL"
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
CREATE TABLE resource_share_invitations (
|
||||
token_hash VARCHAR(64) PRIMARY KEY,
|
||||
resource_kind VARCHAR(16) NOT NULL,
|
||||
resource_slug VARCHAR(255) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
permission VARCHAR(2) NOT NULL,
|
||||
created_by BIGINT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
accepted_at TEXT NULL,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP),
|
||||
UNIQUE KEY uq_resource_share_invitation (resource_kind, resource_slug, user_id),
|
||||
CONSTRAINT fk_share_invitation_user FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE,
|
||||
CONSTRAINT fk_share_invitation_creator FOREIGN KEY(created_by) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX idx_resource_share_invitations_user ON resource_share_invitations(user_id);
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE resource_share_invitations (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
resource_kind TEXT NOT NULL,
|
||||
resource_slug TEXT NOT NULL,
|
||||
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
permission TEXT NOT NULL CHECK(permission IN ('ro','rw')),
|
||||
created_by BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at TEXT NOT NULL,
|
||||
accepted_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT (CURRENT_TIMESTAMP::text),
|
||||
UNIQUE(resource_kind, resource_slug, user_id)
|
||||
);
|
||||
CREATE INDEX idx_resource_share_invitations_user ON resource_share_invitations(user_id);
|
||||
@@ -0,0 +1,13 @@
|
||||
CREATE TABLE resource_share_invitations (
|
||||
token_hash TEXT PRIMARY KEY,
|
||||
resource_kind TEXT NOT NULL,
|
||||
resource_slug TEXT NOT NULL,
|
||||
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
permission TEXT NOT NULL CHECK(permission IN ('ro','rw')),
|
||||
created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
expires_at TEXT NOT NULL,
|
||||
accepted_at TEXT,
|
||||
created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(resource_kind, resource_slug, user_id)
|
||||
);
|
||||
CREATE INDEX idx_resource_share_invitations_user ON resource_share_invitations(user_id);
|
||||
@@ -52,6 +52,7 @@ pub fn router(state: SharedState, static_dir: &str, upload_max_size_bytes: usize
|
||||
.route("/api/auth/resources/privacy", post(auth::set_resource_privacy))
|
||||
.route("/api/auth/resources/sharing", get(auth::resource_sharing).post(auth::share_resource_users).delete(auth::remove_resource_user))
|
||||
.route("/api/auth/resources/share-links", post(auth::create_share_link).put(auth::update_share_link).delete(auth::revoke_share_link))
|
||||
.route("/share-invitations/{token}/accept", get(auth::accept_share_invitation))
|
||||
.route("/api/auth/password-reset", post(auth::request_reset))
|
||||
.route("/api/auth/password-reset/confirm", post(auth::confirm_reset))
|
||||
.route("/api/public/{token}", get(api::public_page))
|
||||
|
||||
+80
-3
@@ -1,5 +1,5 @@
|
||||
use argon2::{password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString}, Argon2};
|
||||
use axum::{extract::State, http::{HeaderMap, StatusCode}, Json};
|
||||
use axum::{extract::{Path as AxumPath, State}, http::{HeaderMap, StatusCode}, response::Redirect, Json};
|
||||
use chrono::{Duration, Utc};
|
||||
use lettre::{
|
||||
message::{header::ContentType, Mailbox, MultiPart, SinglePart},
|
||||
@@ -226,6 +226,9 @@ pub async fn share_resource_users(State(state): State<SharedState>, headers: Hea
|
||||
let owner = require_user(&state, &headers).await?;
|
||||
ensure_owner(&state, owner.id, &req.kind, &req.slug).await?;
|
||||
let permission = validate_permission(&req.permission)?;
|
||||
if state.share_confirmation_required && state.smtp.is_none() {
|
||||
return Err(AuthError::service_unavailable("Share confirmation requires SMTP configuration."));
|
||||
}
|
||||
let emails: Vec<String> = req.emails.split(',').map(|v| normalize(v)).filter(|v| !v.is_empty()).collect();
|
||||
if emails.is_empty() || emails.len() > 100 { return Err(AuthError::bad_request("Enter between 1 and 100 registered e-mail addresses.")); }
|
||||
let mut missing = Vec::new();
|
||||
@@ -233,13 +236,52 @@ pub async fn share_resource_users(State(state): State<SharedState>, headers: Hea
|
||||
let user = find_user_by_email(&state, &email).await?;
|
||||
let Some(user) = user else { missing.push(email); continue; };
|
||||
if user.id == owner.id { continue; }
|
||||
|
||||
sqlx::query(queries::get(state.db.kind(), "DELETE FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"))
|
||||
.bind(&req.kind).bind(req.slug.trim()).bind(user.id).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
sqlx::query(queries::get(state.db.kind(), "DELETE FROM resource_share_invitations WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"))
|
||||
.bind(&req.kind).bind(req.slug.trim()).bind(user.id).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
|
||||
if state.share_confirmation_required {
|
||||
let token = random_token();
|
||||
let token_hash = hash_token(&token);
|
||||
let expires_at = (Utc::now() + Duration::days(7)).to_rfc3339();
|
||||
sqlx::query(queries::get(state.db.kind(), "INSERT INTO resource_share_invitations (token_hash, resource_kind, resource_slug, user_id, permission, created_by, expires_at) VALUES (?, ?, ?, ?, ?, ?, ?)"))
|
||||
.bind(&token_hash).bind(&req.kind).bind(req.slug.trim()).bind(user.id).bind(permission).bind(owner.id).bind(&expires_at)
|
||||
.execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
if let Err(error) = send_share_invitation(state.smtp.as_ref().unwrap(), &owner, &user, &req.kind, req.slug.trim(), permission, &token).await {
|
||||
let _ = sqlx::query(queries::get(state.db.kind(), "DELETE FROM resource_share_invitations WHERE token_hash = ?"))
|
||||
.bind(&token_hash).execute(state.db.pool()).await;
|
||||
return Err(error);
|
||||
}
|
||||
} else {
|
||||
sqlx::query(queries::get(state.db.kind(), "INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES (?, ?, ?, ?)"))
|
||||
.bind(&req.kind).bind(req.slug.trim()).bind(user.id).bind(permission).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
}
|
||||
}
|
||||
if !missing.is_empty() { return Err(AuthError::bad_request(&format!("No registered account for: {}", missing.join(", ")))); }
|
||||
Ok(Json(serde_json::json!({"ok":true})))
|
||||
Ok(Json(serde_json::json!({"ok":true,"confirmation_required":state.share_confirmation_required})))
|
||||
}
|
||||
|
||||
pub async fn accept_share_invitation(State(state): State<SharedState>, AxumPath(token): AxumPath<String>) -> Result<Redirect, AuthError> {
|
||||
let token_hash = hash_token(token.trim());
|
||||
let row: Option<(String, String, i64, String, String, Option<String>)> = sqlx::query_as(queries::get(state.db.kind(), "SELECT resource_kind, resource_slug, user_id, permission, expires_at, accepted_at FROM resource_share_invitations WHERE token_hash = ?"))
|
||||
.bind(&token_hash).fetch_optional(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
let (kind, slug, user_id, permission, expires_at, accepted_at) = row.ok_or_else(|| AuthError::bad_request("The sharing invitation is invalid or has expired."))?;
|
||||
let expires = chrono::DateTime::parse_from_rfc3339(&expires_at).map_err(|_| AuthError::bad_request("The sharing invitation is invalid or has expired."))?.with_timezone(&Utc);
|
||||
if accepted_at.is_none() {
|
||||
if expires <= Utc::now() { return Err(AuthError::bad_request("The sharing invitation is invalid or has expired.")); }
|
||||
let mut tx = state.db.pool().begin().await.map_err(AuthError::database)?;
|
||||
sqlx::query(queries::get(state.db.kind(), "DELETE FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"))
|
||||
.bind(&kind).bind(&slug).bind(user_id).execute(&mut *tx).await.map_err(AuthError::database)?;
|
||||
sqlx::query(queries::get(state.db.kind(), "INSERT INTO resource_permissions (resource_kind, resource_slug, user_id, permission) VALUES (?, ?, ?, ?)"))
|
||||
.bind(&kind).bind(&slug).bind(user_id).bind(&permission).execute(&mut *tx).await.map_err(AuthError::database)?;
|
||||
sqlx::query(queries::get(state.db.kind(), "UPDATE resource_share_invitations SET accepted_at = ? WHERE token_hash = ?"))
|
||||
.bind(Utc::now().to_rfc3339()).bind(&token_hash).execute(&mut *tx).await.map_err(AuthError::database)?;
|
||||
tx.commit().await.map_err(AuthError::database)?;
|
||||
}
|
||||
let target = if kind == "workspace" { format!("/w/{slug}") } else { format!("/p/{slug}") };
|
||||
Ok(Redirect::to(&target))
|
||||
}
|
||||
|
||||
pub async fn remove_resource_user(State(state): State<SharedState>, headers: HeaderMap, Json(req): Json<RemoveShareRequest>) -> Result<Json<serde_json::Value>, AuthError> {
|
||||
@@ -249,6 +291,8 @@ pub async fn remove_resource_user(State(state): State<SharedState>, headers: Hea
|
||||
if let Some(user) = find_user_by_email(&state, &email).await? {
|
||||
sqlx::query(queries::get(state.db.kind(), "DELETE FROM resource_permissions WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"))
|
||||
.bind(&req.kind).bind(req.slug.trim()).bind(user.id).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
sqlx::query(queries::get(state.db.kind(), "DELETE FROM resource_share_invitations WHERE resource_kind = ? AND resource_slug = ? AND user_id = ?"))
|
||||
.bind(&req.kind).bind(req.slug.trim()).bind(user.id).execute(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
}
|
||||
Ok(Json(serde_json::json!({"ok":true})))
|
||||
}
|
||||
@@ -262,7 +306,9 @@ pub async fn resource_sharing(State(state): State<SharedState>, headers: HeaderM
|
||||
.bind(kind).bind(slug).fetch_all(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
let links: Vec<(String,String,Option<String>,String)> = sqlx::query_as(queries::get(state.db.kind(), "SELECT token_hash, permission, expires_at, created_at FROM resource_share_links WHERE resource_kind = ? AND resource_slug = ? AND revoked_at IS NULL ORDER BY created_at DESC"))
|
||||
.bind(kind).bind(slug).fetch_all(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
Ok(Json(serde_json::json!({"users":users.into_iter().map(|(email,nickname,permission)|serde_json::json!({"email":email,"nickname":nickname,"permission":permission})).collect::<Vec<_>>(), "links":links.into_iter().map(|(token,permission,expires_at,created_at)|serde_json::json!({"token":token,"permission":permission,"expires_at":expires_at,"created_at":created_at})).collect::<Vec<_>>() })))
|
||||
let pending: Vec<(String,String,String,String)> = sqlx::query_as(queries::get(state.db.kind(), "SELECT u.email, u.nickname, i.permission, i.expires_at FROM resource_share_invitations i JOIN users u ON u.id = i.user_id WHERE i.resource_kind = ? AND i.resource_slug = ? AND i.accepted_at IS NULL ORDER BY u.email"))
|
||||
.bind(kind).bind(slug).fetch_all(state.db.pool()).await.map_err(AuthError::database)?;
|
||||
Ok(Json(serde_json::json!({"users":users.into_iter().map(|(email,nickname,permission)|serde_json::json!({"email":email,"nickname":nickname,"permission":permission})).collect::<Vec<_>>(), "pending":pending.into_iter().map(|(email,nickname,permission,expires_at)|serde_json::json!({"email":email,"nickname":nickname,"permission":permission,"expires_at":expires_at})).collect::<Vec<_>>(), "links":links.into_iter().map(|(token,permission,expires_at,created_at)|serde_json::json!({"token":token,"permission":permission,"expires_at":expires_at,"created_at":created_at})).collect::<Vec<_>>() })))
|
||||
}
|
||||
|
||||
pub async fn create_share_link(State(state): State<SharedState>, headers: HeaderMap, Json(req): Json<CreateShareLinkRequest>) -> Result<Json<serde_json::Value>, AuthError> {
|
||||
@@ -609,6 +655,37 @@ async fn send_registration_email(
|
||||
send_message(smtp, message, "registration e-mail").await
|
||||
}
|
||||
|
||||
async fn send_share_invitation(
|
||||
smtp: &SmtpConfig,
|
||||
owner: &User,
|
||||
recipient_user: &User,
|
||||
kind: &str,
|
||||
slug: &str,
|
||||
permission: &str,
|
||||
token: &str,
|
||||
) -> Result<(), AuthError> {
|
||||
let site = smtp.public_url.trim_end_matches('/');
|
||||
let accept_url = format!("{site}/share-invitations/{token}/accept");
|
||||
let resource_label = if kind == "workspace" { "workspace" } else { "note" };
|
||||
let access_label = if permission == "rw" { "view and edit" } else { "view" };
|
||||
let sender = smtp.from.parse::<Mailbox>().map_err(|_| AuthError::internal("Invalid SMTP_FROM."))?;
|
||||
let recipient = recipient_user.email.parse::<Mailbox>().map_err(|_| AuthError::internal("Invalid recipient address."))?;
|
||||
let subject = format!("{} shared a RustPad {} with you", owner.nickname, resource_label);
|
||||
let text_body = format!(
|
||||
"Hello {},\n\n{} shared the {} '{}' with you ({access_label}).\nAccept the invitation within 7 days:\n{}\n\nIf you were not expecting this invitation, ignore this message.",
|
||||
recipient_user.nickname, owner.nickname, resource_label, slug, accept_url
|
||||
);
|
||||
let html_body = format!(r#"<!doctype html><html lang="en"><body style="margin:0;padding:24px;background:#f4f4f5;font-family:Arial,sans-serif;color:#18181b"><div style="max-width:560px;margin:0 auto;padding:24px;background:#fff;border-radius:10px"><h1 style="margin-top:0;font-size:22px">A RustPad {resource_label} was shared with you</h1><p>Hello {},</p><p><strong>{}</strong> shared <strong>{}</strong> with you. Permission: <strong>{access_label}</strong>.</p><p><a href="{}" style="display:inline-block;padding:11px 18px;background:#2563eb;color:#fff;text-decoration:none;border-radius:6px">Accept invitation</a></p><p style="font-size:13px;color:#52525b">This link expires in 7 days.</p></div></body></html>"#,
|
||||
recipient_user.nickname, owner.nickname, slug, accept_url
|
||||
);
|
||||
let message = Message::builder().from(sender).to(recipient).subject(subject)
|
||||
.multipart(MultiPart::alternative()
|
||||
.singlepart(SinglePart::builder().header(ContentType::TEXT_PLAIN).body(text_body))
|
||||
.singlepart(SinglePart::builder().header(ContentType::TEXT_HTML).body(html_body)))
|
||||
.map_err(|_| AuthError::internal("Failed to build sharing invitation e-mail."))?;
|
||||
send_message(smtp, message, "sharing invitation e-mail").await
|
||||
}
|
||||
|
||||
async fn send_message(
|
||||
smtp: &SmtpConfig,
|
||||
message: Message,
|
||||
|
||||
@@ -16,6 +16,7 @@ pub struct Config {
|
||||
pub smtp: Option<crate::state::SmtpConfig>,
|
||||
pub registration_enabled: bool,
|
||||
pub account_confirmation_required: bool,
|
||||
pub share_confirmation_required: bool,
|
||||
pub frontend_log_level: String,
|
||||
pub anonymous_access_token_ttl_days: i64,
|
||||
pub user_session_ttl_days: i64,
|
||||
@@ -82,6 +83,7 @@ impl Config {
|
||||
smtp,
|
||||
registration_enabled: env_bool("REGISTRATION_ENABLED", false)?,
|
||||
account_confirmation_required: env_bool("ACCOUNT_CONFIRMATION_REQUIRED", false)?,
|
||||
share_confirmation_required: env_bool("SHARE_CONFIRMATION_REQUIRED", false)?,
|
||||
frontend_log_level: env_log_level("FRONTEND_LOG_LEVEL", "warn")?,
|
||||
anonymous_access_token_ttl_days,
|
||||
user_session_ttl_days,
|
||||
|
||||
@@ -37,6 +37,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
file_cache_max_age_seconds = config.file_cache_max_age_seconds,
|
||||
registration_enabled = config.registration_enabled,
|
||||
account_confirmation_required = config.account_confirmation_required,
|
||||
share_confirmation_required = config.share_confirmation_required,
|
||||
frontend_log_level = %config.frontend_log_level,
|
||||
anonymous_access_token_ttl_days = config.anonymous_access_token_ttl_days,
|
||||
user_session_ttl_days = config.user_session_ttl_days,
|
||||
@@ -64,6 +65,7 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
config.smtp.clone(),
|
||||
config.registration_enabled,
|
||||
config.account_confirmation_required,
|
||||
config.share_confirmation_required,
|
||||
config.frontend_log_level.clone(),
|
||||
config.anonymous_access_token_ttl_days,
|
||||
config.user_session_ttl_days,
|
||||
|
||||
+3
-2
@@ -42,6 +42,7 @@ pub struct AppState {
|
||||
pub smtp: Option<SmtpConfig>,
|
||||
pub registration_enabled: bool,
|
||||
pub account_confirmation_required: bool,
|
||||
pub share_confirmation_required: bool,
|
||||
pub frontend_log_level: String,
|
||||
pub anonymous_access_token_ttl_days: i64,
|
||||
pub user_session_ttl_days: i64,
|
||||
@@ -51,8 +52,8 @@ pub struct AppState {
|
||||
}
|
||||
|
||||
impl AppState {
|
||||
pub fn new(db: Database, asset_version: String, storage: crate::storage::Storage, upload_max_size_bytes: usize, file_cache_max_age_seconds: u64, smtp: Option<SmtpConfig>, registration_enabled: bool, account_confirmation_required: bool, frontend_log_level: String, anonymous_access_token_ttl_days: i64, user_session_ttl_days: i64) -> Self {
|
||||
Self { db, asset_version, storage, upload_max_size_bytes, file_cache_max_age_seconds, smtp, registration_enabled, account_confirmation_required, frontend_log_level, anonymous_access_token_ttl_days, user_session_ttl_days, channels: RwLock::new(HashMap::new()), presence: RwLock::new(HashMap::new()), next_connection_id: AtomicU64::new(1) }
|
||||
pub fn new(db: Database, asset_version: String, storage: crate::storage::Storage, upload_max_size_bytes: usize, file_cache_max_age_seconds: u64, smtp: Option<SmtpConfig>, registration_enabled: bool, account_confirmation_required: bool, share_confirmation_required: bool, frontend_log_level: String, anonymous_access_token_ttl_days: i64, user_session_ttl_days: i64) -> Self {
|
||||
Self { db, asset_version, storage, upload_max_size_bytes, file_cache_max_age_seconds, smtp, registration_enabled, account_confirmation_required, share_confirmation_required, frontend_log_level, anonymous_access_token_ttl_days, user_session_ttl_days, channels: RwLock::new(HashMap::new()), presence: RwLock::new(HashMap::new()), next_connection_id: AtomicU64::new(1) }
|
||||
}
|
||||
async fn channel_for_key(&self, key: String) -> broadcast::Sender<RoomEvent> {
|
||||
if let Some(sender) = self.channels.read().await.get(&key) { return sender.clone(); }
|
||||
|
||||
+3618
-584
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,6 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
@@ -8,6 +9,7 @@
|
||||
<title>__ERROR_TITLE__ · RustPad</title>
|
||||
<link rel="stylesheet" href="/assets/css/styles.css?v=__ASSET_VERSION__">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<main class="error-page">
|
||||
<section class="error-card" aria-labelledby="error-title">
|
||||
@@ -21,4 +23,5 @@
|
||||
</section>
|
||||
</main>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+30
-12
@@ -1,13 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>RustPad</title>
|
||||
<link rel="stylesheet" href="/assets/css/styles.css?v=__ASSET_VERSION__">
|
||||
<script type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script><script type="module" src="/assets/js/home.js?v=__ASSET_VERSION__"></script>
|
||||
<script
|
||||
type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script>
|
||||
<script type="module" src="/assets/js/home.js?v=__ASSET_VERSION__"></script>
|
||||
</head>
|
||||
|
||||
<body class="home-page" data-registration-enabled="__REGISTRATION_ENABLED__">
|
||||
<header class="site-header home-header"><a class="brand home-brand" href="/">RustPad</a></header>
|
||||
<main class="home-layout home-layout--wide">
|
||||
@@ -26,11 +30,15 @@
|
||||
<div class="field">
|
||||
<label for="pad-name">Note name</label>
|
||||
<input id="pad-name" maxlength="80" required autocomplete="off" placeholder="Meeting notes">
|
||||
<div class="field-meta"><span id="pad-slug-preview">/p/meeting-notes</span><span id="pad-name-count">0/80</span></div>
|
||||
<div class="field-meta"><span id="pad-slug-preview">/p/meeting-notes</span><span
|
||||
id="pad-name-count">0/80</span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="label-row"><label for="pad-password">Password</label><span>optional, min. 8 characters</span></div>
|
||||
<div class="password-input"><input id="pad-password" type="password" maxlength="128" autocomplete="new-password" placeholder="Note password"><button class="text-button password-toggle" type="button" data-target="pad-password">Show</button></div>
|
||||
<div class="label-row"><label for="pad-password">Password</label><span>optional, min. 8 characters</span>
|
||||
</div>
|
||||
<div class="password-input"><input id="pad-password" type="password" maxlength="128"
|
||||
autocomplete="new-password" placeholder="Note password"><button class="text-button password-toggle"
|
||||
type="button" data-target="pad-password">Show</button></div>
|
||||
</div>
|
||||
<p id="pad-error" class="form-message error" role="alert"></p>
|
||||
<button id="pad-button" class="primary-button" type="submit">Create note</button>
|
||||
@@ -46,11 +54,15 @@
|
||||
<div class="field">
|
||||
<label for="workspace-name">Workspace name</label>
|
||||
<input id="workspace-name" maxlength="80" required autocomplete="off" placeholder="My project">
|
||||
<div class="field-meta"><span id="workspace-slug-preview">/w/my-project</span><span id="workspace-name-count">0/80</span></div>
|
||||
<div class="field-meta"><span id="workspace-slug-preview">/w/my-project</span><span
|
||||
id="workspace-name-count">0/80</span></div>
|
||||
</div>
|
||||
<div class="field">
|
||||
<div class="label-row"><label for="workspace-password">Password</label><span>optional, min. 8 characters</span></div>
|
||||
<div class="password-input"><input id="workspace-password" type="password" maxlength="128" autocomplete="new-password" placeholder="Workspace password"><button class="text-button password-toggle" type="button" data-target="workspace-password">Show</button></div>
|
||||
<div class="label-row"><label for="workspace-password">Password</label><span>optional, min. 8
|
||||
characters</span></div>
|
||||
<div class="password-input"><input id="workspace-password" type="password" maxlength="128"
|
||||
autocomplete="new-password" placeholder="Workspace password"><button class="text-button password-toggle"
|
||||
type="button" data-target="workspace-password">Show</button></div>
|
||||
</div>
|
||||
<p id="workspace-error" class="form-message error" role="alert"></p>
|
||||
<button id="workspace-button" class="primary-button" type="submit">Create workspace</button>
|
||||
@@ -63,14 +75,16 @@
|
||||
<div class="home-footer__inner">
|
||||
<div id="footer-account-guest" class="home-footer__account">
|
||||
<button id="footer-login" class="footer-action" type="button">Log in</button>
|
||||
<button id="footer-register" class="footer-action footer-action--primary" type="button">Register nickname</button>
|
||||
<button id="footer-register" class="footer-action footer-action--primary" type="button">Register
|
||||
nickname</button>
|
||||
</div>
|
||||
<div id="footer-account-user" class="home-footer__account" hidden>
|
||||
<span id="footer-user-label" class="home-footer__user"></span>
|
||||
<button id="footer-resources" class="footer-action footer-action--primary" type="button">My notes</button>
|
||||
<button id="footer-logout" class="footer-action" type="button">Log out</button>
|
||||
</div>
|
||||
<span class="home-footer__author">Author: <a href="https://www.linuxiarz.pl" rel="author noopener">@linuxiarz.pl</a></span>
|
||||
<span class="home-footer__author">Author: <a href="https://www.linuxiarz.pl"
|
||||
rel="author noopener">@linuxiarz.pl</a></span>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
@@ -82,9 +96,12 @@
|
||||
<p id="identity-copy" class="dialog-copy"></p>
|
||||
</header>
|
||||
<div class="identity-fields">
|
||||
<label>Nickname<input id="nickname" name="nickname" maxlength="40" autocomplete="off" data-bwignore="true" placeholder="Your nickname"></label>
|
||||
<label id="auth-email-field">E-mail<input id="auth-email" name="username" type="email" maxlength="320" autocomplete="username" required placeholder="you@example.com"></label>
|
||||
<label>Password<input id="auth-password" name="password" type="password" minlength="8" maxlength="128" autocomplete="current-password" required placeholder="At least 8 characters"></label>
|
||||
<label>Nickname<input id="nickname" name="nickname" maxlength="40" autocomplete="off" data-bwignore="true"
|
||||
placeholder="Your nickname"></label>
|
||||
<label id="auth-email-field">E-mail<input id="auth-email" name="username" type="email" maxlength="320"
|
||||
autocomplete="username" required placeholder="you@example.com"></label>
|
||||
<label>Password<input id="auth-password" name="password" type="password" minlength="8" maxlength="128"
|
||||
autocomplete="current-password" required placeholder="At least 8 characters"></label>
|
||||
</div>
|
||||
<button id="auth-submit" class="primary-button" type="submit">Log in</button>
|
||||
<div class="identity-links">
|
||||
@@ -107,4 +124,5 @@
|
||||
</div>
|
||||
</dialog>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+192
-5
@@ -1,5 +1,192 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><title>__NOTE_TITLE__ · RustPad</title><link rel="stylesheet" href="/assets/css/styles.css?v=__ASSET_VERSION__"><script type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script><script type="module" src="/assets/js/note.js?v=__ASSET_VERSION__"></script></head>
|
||||
<body class="pad-page" data-registration-enabled="__REGISTRATION_ENABLED__"><header class="app-header"><div class="app-header__main"><a id="workspace-link" class="brand" href="/w/__WORKSPACE_SLUG__">__WORKSPACE_TITLE__</a><span class="header-divider"></span><div class="document-heading"><h1 id="note-title">__NOTE_TITLE__</h1><p id="note-url" class="document-url"></p></div></div><div class="header-actions"><span class="user-color-control"><button id="current-user" class="user-chip" type="button" title="Change your color"><span class="user-chip__dot" aria-hidden="true"></span><span class="user-chip__name"></span></button><input id="user-color-picker" class="user-color-picker" type="color" aria-label="Choose your color"></span><button id="copy-link" class="secondary-button">Copy link</button><button id="publish-page" class="secondary-button">Page</button><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><button id="files-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></header>
|
||||
<main class="editor-layout"><section class="editor-panel"><div class="editor-toolbar"><div class="toolbar-group"><button data-format="bold" title="Bold"><strong>B</strong></button><button data-format="italic" title="Italic"><em>I</em></button><button data-format="strike" title="Strikethrough"><s>S</s></button><button data-format="heading1">H1</button><button data-format="heading2">H2</button><button data-format="heading3">H3</button><button data-format="heading4">H4</button><button data-format="bullet">• List</button><button data-format="number" title="Numbered list · Ctrl/Cmd+Shift+7">1. List</button><button data-format="task" title="Task list · Ctrl/Cmd+Shift+9">☑ Task</button><button data-format="quote">Quote</button><button data-format="link">Link</button><details class="markdown-more"><summary title="Extended Markdown">More</summary><div class="markdown-more-menu"><button type="button" data-format="details">Collapsible section</button><button type="button" data-format="inline-code">Inline code</button><button type="button" data-format="codeblock">Code block</button><button type="button" data-format="table">Table</button><button type="button" data-format="footnote">Footnote</button><button type="button" data-format="definition">Definition</button><button type="button" data-format="highlight">Highlight</button><button type="button" data-format="subscript">Subscript</button><button type="button" data-format="superscript">Superscript</button><button type="button" data-format="horizontal-rule">Horizontal rule</button></div></details></div><div class="editor-controls"><label>Font<select id="font-family"><option value="mono">Mono</option><option value="system">System</option><option value="serif">Serif</option><option value="arial">Arial</option><option value="georgia">Georgia</option></select></label><label>Size<select id="font-size"><option value="14" selected>14</option><option value="16">16</option><option value="18">18</option><option value="20">20</option><option value="22">22</option></select></label></div><button id="upload-button" class="toolbar-action">Image/file</button><input id="file-input" type="file" hidden><label class="line-toggle"><input id="line-numbers-toggle" type="checkbox" checked> Lines</label><label class="line-toggle"><input id="compact-toggle" type="checkbox" checked> Compact</label><div class="toolbar-fill"></div><button id="mode-toggle" class="markdown-toggle active" aria-pressed="true">Markdown</button><div class="view-switch"><button data-view="edit">Edit</button><button data-view="split" class="active">Split</button><button data-view="preview">Preview</button></div></div><div id="editor-workspace" class="workspace view-split"><div class="editor-column"><div class="column-label">Editor</div><div class="editor-shell"><div id="line-gutter" class="line-gutter" aria-hidden="true"></div><div id="owner-labels" class="owner-labels" aria-hidden="true"></div><textarea id="editor" wrap="off" placeholder="Start writing…" spellcheck="false"></textarea></div></div><div class="preview-column"><div id="preview-label" class="column-label">Markdown preview</div><article id="preview" class="preview markdown-body"></article></div></div><footer class="editor-footer"><div class="footer-left"><span id="characters">0 characters</span> · <span id="words">0 words</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" 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" hidden></span></summary><div class="room-popover"><section class="room-users"><strong>In this room</strong><ul id="room-users"></ul></section><section class="room-chat"><div class="room-chat__head"><strong>Room chat</strong><span>Messages disappear after disconnect</span></div><div id="chat-messages" class="chat-messages" aria-live="polite"></div><form id="chat-form" class="chat-form"><input id="chat-input" maxlength="1000" autocomplete="off" placeholder="Write a message…" aria-label="Chat message"><button type="submit">Send</button></form></section></div></details></div><span class="footer-right"><button id="shortcuts-button" class="footer-link" type="button" aria-haspopup="dialog">Shortcuts</button> · <button id="footer-files" class="footer-link" type="button">0 files</button> · <span id="save-state">Changes are saved automatically</span></span></footer></section><aside id="history-panel" class="history-panel" aria-hidden="true"><div class="history-header"><div><h2>Change history</h2><p>Author, time, and version preview</p></div><button id="close-history" class="icon-button">×</button></div><div id="history-list" class="history-list"></div></aside></main>
|
||||
<dialog id="shortcuts-dialog"><div class="dialog-panel shortcuts-panel"><div class="files-head"><div><h2>Keyboard shortcuts</h2><p>Use Ctrl on Windows/Linux or Cmd on macOS.</p></div><button id="close-shortcuts" class="icon-button" type="button">×</button></div><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 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 H1–H4</span></div></div></dialog><dialog id="files-dialog" class="image-editor-dialog files-dialog"><div class="image-editor-panel files-panel"><div class="files-head"><div><h2>Note files</h2><p>Copy a direct link or ready Markdown/HTML code.</p></div><button id="close-files" class="icon-button" type="button">×</button></div><div id="files-list" class="files-list"></div></div></dialog><dialog id="identity-dialog"><form id="identity-form" autocomplete="on" class="dialog-panel identity-panel"><button id="close-identity" class="modal-close" type="button" aria-label="Close dialog">×</button><h2>What should we call you?</h2><p class="dialog-copy">Use a free nickname without an account, or register it to reserve it.</p><input id="nickname" maxlength="40" autocomplete="off" data-bwignore="true" required placeholder="Name or nickname"><div class="identity-actions"><button id="guest-continue" class="primary-button" type="submit">Continue as guest</button><button id="show-register" class="text-button" type="button">Register</button><button id="show-login" class="text-button" type="button">Log in</button></div><section id="auth-panel" class="auth-panel" hidden><h3 id="auth-mode-title">Log in</h3><label id="auth-email-field">E-mail<input id="auth-email" name="username" type="email" maxlength="320" autocomplete="username" placeholder="you@example.com"></label><label>Password<input id="auth-password" name="password" type="password" minlength="8" maxlength="128" autocomplete="current-password"></label><button id="auth-submit" class="primary-button" type="submit">Log in and continue</button><div class="identity-links"><button id="show-reset" class="text-button" type="button">Forgot password?</button><button id="auth-back" class="text-button" type="button">Back to nickname</button><button id="logout-account" class="text-button" type="button">Log out saved account</button></div></section><p id="identity-error" class="form-message error" role="alert"></p></form></dialog>
|
||||
<dialog id="password-dialog"><form id="password-form" class="dialog-panel"><h2>Protected workspace</h2><input id="open-password" type="password" autocomplete="current-password" required placeholder="Password"><p id="password-error" class="form-message error"></p><button class="primary-button">Open</button><a id="back-workspace" class="dialog-link" href="/">Back</a></form></dialog><div id="toast" class="toast"></div></body></html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>__NOTE_TITLE__ · RustPad</title>
|
||||
<link rel="stylesheet" href="/assets/css/styles.css?v=__ASSET_VERSION__">
|
||||
<script
|
||||
type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script>
|
||||
<script type="module" src="/assets/js/note.js?v=__ASSET_VERSION__"></script>
|
||||
</head>
|
||||
|
||||
<body class="pad-page" data-registration-enabled="__REGISTRATION_ENABLED__">
|
||||
<header class="app-header">
|
||||
<div class="app-header__main"><a id="workspace-link" class="brand"
|
||||
href="/w/__WORKSPACE_SLUG__">__WORKSPACE_TITLE__</a><span class="header-divider"></span>
|
||||
<div class="document-heading">
|
||||
<h1 id="note-title">__NOTE_TITLE__</h1>
|
||||
<p id="note-url" class="document-url"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions"><span class="user-color-control"><button id="current-user" class="user-chip"
|
||||
type="button" title="Change your color"><span class="user-chip__dot" aria-hidden="true"></span><span
|
||||
class="user-chip__name"></span></button><input id="user-color-picker" class="user-color-picker"
|
||||
type="color" aria-label="Choose your color"></span><button id="copy-link"
|
||||
class="secondary-button">Copy link</button><button id="publish-page"
|
||||
class="secondary-button">Page</button><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><button id="files-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>
|
||||
</header>
|
||||
<main class="editor-layout">
|
||||
<section class="editor-panel">
|
||||
<div class="editor-toolbar">
|
||||
<div class="toolbar-group"><button data-format="bold" title="Bold"><strong>B</strong></button><button
|
||||
data-format="italic" title="Italic"><em>I</em></button><button data-format="strike"
|
||||
title="Strikethrough"><s>S</s></button><button data-format="heading1">H1</button><button
|
||||
data-format="heading2">H2</button><button data-format="heading3">H3</button><button
|
||||
data-format="heading4">H4</button><button data-format="bullet">• List</button><button
|
||||
data-format="number" title="Numbered list · Ctrl/Cmd+Shift+7">1. List</button><button
|
||||
data-format="task" title="Task list · Ctrl/Cmd+Shift+9">☑ Task</button><button
|
||||
data-format="quote">Quote</button><button data-format="link">Link</button>
|
||||
<details class="markdown-more">
|
||||
<summary title="Extended Markdown">More</summary>
|
||||
<div class="markdown-more-menu"><button type="button" data-format="details">Collapsible
|
||||
section</button><button type="button" data-format="inline-code">Inline
|
||||
code</button><button type="button" data-format="codeblock">Code block</button><button
|
||||
type="button" data-format="table">Table</button><button type="button"
|
||||
data-format="footnote">Footnote</button><button type="button"
|
||||
data-format="definition">Definition</button><button type="button"
|
||||
data-format="highlight">Highlight</button><button type="button"
|
||||
data-format="subscript">Subscript</button><button type="button"
|
||||
data-format="superscript">Superscript</button><button type="button"
|
||||
data-format="horizontal-rule">Horizontal rule</button></div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="editor-controls"><label>Font<select id="font-family">
|
||||
<option value="mono">Mono</option>
|
||||
<option value="system">System</option>
|
||||
<option value="serif">Serif</option>
|
||||
<option value="arial">Arial</option>
|
||||
<option value="georgia">Georgia</option>
|
||||
</select></label><label>Size<select id="font-size">
|
||||
<option value="14" selected>14</option>
|
||||
<option value="16">16</option>
|
||||
<option value="18">18</option>
|
||||
<option value="20">20</option>
|
||||
<option value="22">22</option>
|
||||
</select></label></div><button id="upload-button"
|
||||
class="toolbar-action">Image/file</button><input id="file-input" type="file" hidden><label
|
||||
class="line-toggle"><input id="line-numbers-toggle" type="checkbox" checked> Lines</label><label
|
||||
class="line-toggle"><input id="compact-toggle" type="checkbox" checked> Compact</label>
|
||||
<div class="toolbar-fill"></div><button id="mode-toggle" class="markdown-toggle active"
|
||||
aria-pressed="true">Markdown</button>
|
||||
<div class="view-switch"><button data-view="edit">Edit</button><button data-view="split"
|
||||
class="active">Split</button><button data-view="preview">Preview</button></div>
|
||||
</div>
|
||||
<div id="editor-workspace" class="workspace view-split">
|
||||
<div class="editor-column">
|
||||
<div class="column-label">Editor</div>
|
||||
<div class="editor-shell">
|
||||
<div id="line-gutter" class="line-gutter" aria-hidden="true"></div>
|
||||
<div id="owner-labels" class="owner-labels" aria-hidden="true"></div><textarea id="editor"
|
||||
wrap="off" placeholder="Start writing…" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-column">
|
||||
<div id="preview-label" class="column-label">Markdown preview</div>
|
||||
<article id="preview" class="preview markdown-body"></article>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="editor-footer">
|
||||
<div class="footer-left"><span id="characters">0 characters</span> · <span id="words">0 words</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"
|
||||
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"
|
||||
hidden></span></summary>
|
||||
<div class="room-popover">
|
||||
<section class="room-users"><strong>In this room</strong>
|
||||
<ul id="room-users"></ul>
|
||||
</section>
|
||||
<section class="room-chat">
|
||||
<div class="room-chat__head"><strong>Room chat</strong><span>Messages disappear after
|
||||
disconnect</span></div>
|
||||
<div id="chat-messages" class="chat-messages" aria-live="polite"></div>
|
||||
<form id="chat-form" class="chat-form"><input id="chat-input" maxlength="1000"
|
||||
autocomplete="off" placeholder="Write a message…"
|
||||
aria-label="Chat message"><button type="submit">Send</button></form>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
</div><span class="footer-right"><button id="shortcuts-button" class="footer-link" type="button"
|
||||
aria-haspopup="dialog">Shortcuts</button> · <button id="footer-files" class="footer-link"
|
||||
type="button">0 files</button> · <span id="save-state">Changes are saved
|
||||
automatically</span></span>
|
||||
</footer>
|
||||
</section>
|
||||
<aside id="history-panel" class="history-panel" aria-hidden="true">
|
||||
<div class="history-header">
|
||||
<div>
|
||||
<h2>Change history</h2>
|
||||
<p>Author, time, and version preview</p>
|
||||
</div><button id="close-history" class="icon-button">×</button>
|
||||
</div>
|
||||
<div id="history-list" class="history-list"></div>
|
||||
</aside>
|
||||
</main>
|
||||
<dialog id="shortcuts-dialog">
|
||||
<div class="dialog-panel shortcuts-panel">
|
||||
<div class="files-head">
|
||||
<div>
|
||||
<h2>Keyboard shortcuts</h2>
|
||||
<p>Use Ctrl on Windows/Linux or Cmd on macOS.</p>
|
||||
</div><button id="close-shortcuts" class="icon-button" type="button">×</button>
|
||||
</div>
|
||||
<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
|
||||
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 H1–H4</span></div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="files-dialog" class="image-editor-dialog files-dialog">
|
||||
<div class="image-editor-panel files-panel">
|
||||
<div class="files-head">
|
||||
<div>
|
||||
<h2>Note files</h2>
|
||||
<p>Copy a direct link or ready Markdown/HTML code.</p>
|
||||
</div><button id="close-files" class="icon-button" type="button">×</button>
|
||||
</div>
|
||||
<div id="files-list" class="files-list"></div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="identity-dialog">
|
||||
<form id="identity-form" autocomplete="on" class="dialog-panel identity-panel"><button id="close-identity"
|
||||
class="modal-close" type="button" aria-label="Close dialog">×</button>
|
||||
<h2>What should we call you?</h2>
|
||||
<p class="dialog-copy">Use a free nickname without an account, or register it to reserve it.</p><input
|
||||
id="nickname" maxlength="40" autocomplete="off" data-bwignore="true" required
|
||||
placeholder="Name or nickname">
|
||||
<div class="identity-actions"><button id="guest-continue" class="primary-button" type="submit">Continue as
|
||||
guest</button><button id="show-register" class="text-button" type="button">Register</button><button
|
||||
id="show-login" class="text-button" type="button">Log in</button></div>
|
||||
<section id="auth-panel" class="auth-panel" hidden>
|
||||
<h3 id="auth-mode-title">Log in</h3><label id="auth-email-field">E-mail<input id="auth-email"
|
||||
name="username" type="email" maxlength="320" autocomplete="username"
|
||||
placeholder="you@example.com"></label><label>Password<input id="auth-password" name="password"
|
||||
type="password" minlength="8" maxlength="128" autocomplete="current-password"></label><button
|
||||
id="auth-submit" class="primary-button" type="submit">Log in and continue</button>
|
||||
<div class="identity-links"><button id="show-reset" class="text-button" type="button">Forgot
|
||||
password?</button><button id="auth-back" class="text-button" type="button">Back to
|
||||
nickname</button><button id="logout-account" class="text-button" type="button">Log out saved
|
||||
account</button></div>
|
||||
</section>
|
||||
<p id="identity-error" class="form-message error" role="alert"></p>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="password-dialog">
|
||||
<form id="password-form" class="dialog-panel">
|
||||
<h2>Protected workspace</h2><input id="open-password" type="password" autocomplete="current-password"
|
||||
required placeholder="Password">
|
||||
<p id="password-error" class="form-message error"></p><button class="primary-button">Open</button><a
|
||||
id="back-workspace" class="dialog-link" href="/">Back</a>
|
||||
</form>
|
||||
</dialog>
|
||||
<div id="toast" class="toast"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+191
-5
@@ -1,5 +1,191 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><title>__PAD_TITLE__ · RustPad</title><link rel="stylesheet" href="/assets/css/styles.css?v=__ASSET_VERSION__"><script type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script><script type="module" src="/assets/js/pad.js?v=__ASSET_VERSION__"></script></head>
|
||||
<body class="pad-page" data-registration-enabled="__REGISTRATION_ENABLED__"><header class="app-header"><div class="app-header__main"><a class="brand" href="/">RustPad</a><span class="header-divider"></span><div class="document-heading"><h1 id="pad-title">__PAD_TITLE__</h1><p id="pad-url" class="document-url"></p></div></div><div class="header-actions"><span class="user-color-control"><button id="current-user" class="user-chip" type="button" title="Change your color"><span class="user-chip__dot" aria-hidden="true"></span><span class="user-chip__name"></span></button><input id="user-color-picker" class="user-color-picker" type="color" aria-label="Choose your color"></span><button id="copy-link" class="secondary-button">Copy link</button><button id="publish-page" class="secondary-button">Page</button><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><button id="files-button" class="secondary-button">Files</button><button id="history-button" class="secondary-button">History</button></div></header>
|
||||
<main class="editor-layout"><section class="editor-panel"><div class="editor-toolbar"><div class="toolbar-group"><button data-format="bold" title="Bold"><strong>B</strong></button><button data-format="italic" title="Italic"><em>I</em></button><button data-format="strike" title="Strikethrough"><s>S</s></button><button data-format="heading1">H1</button><button data-format="heading2">H2</button><button data-format="heading3">H3</button><button data-format="heading4">H4</button><button data-format="bullet">• List</button><button data-format="number" title="Numbered list · Ctrl/Cmd+Shift+7">1. List</button><button data-format="task" title="Task list · Ctrl/Cmd+Shift+9">☑ Task</button><button data-format="quote">Quote</button><button data-format="link">Link</button><details class="markdown-more"><summary title="Extended Markdown">More</summary><div class="markdown-more-menu"><button type="button" data-format="details">Collapsible section</button><button type="button" data-format="inline-code">Inline code</button><button type="button" data-format="codeblock">Code block</button><button type="button" data-format="table">Table</button><button type="button" data-format="footnote">Footnote</button><button type="button" data-format="definition">Definition</button><button type="button" data-format="highlight">Highlight</button><button type="button" data-format="subscript">Subscript</button><button type="button" data-format="superscript">Superscript</button><button type="button" data-format="horizontal-rule">Horizontal rule</button></div></details></div><div class="editor-controls"><label>Font<select id="font-family"><option value="mono">Mono</option><option value="system">System</option><option value="serif">Serif</option><option value="arial">Arial</option><option value="georgia">Georgia</option></select></label><label>Size<select id="font-size"><option value="14" selected>14</option><option value="16">16</option><option value="18">18</option><option value="20">20</option><option value="22">22</option></select></label></div><button id="upload-button" class="toolbar-action">Image/file</button><input id="file-input" type="file" hidden><label class="line-toggle"><input id="line-numbers-toggle" type="checkbox" checked> Lines</label><label class="line-toggle"><input id="compact-toggle" type="checkbox" checked> Compact</label><div class="toolbar-fill"></div><button id="mode-toggle" class="markdown-toggle active" aria-pressed="true">Markdown</button><div class="view-switch"><button data-view="edit">Edit</button><button data-view="split" class="active">Split</button><button data-view="preview">Preview</button></div></div><div id="editor-workspace" class="workspace view-split"><div class="editor-column"><div class="column-label">Editor</div><div class="editor-shell"><div id="line-gutter" class="line-gutter" aria-hidden="true"></div><div id="owner-labels" class="owner-labels" aria-hidden="true"></div><textarea id="editor" wrap="off" placeholder="Start writing…" spellcheck="false"></textarea></div></div><div class="preview-column"><div id="preview-label" class="column-label">Markdown preview</div><article id="preview" class="preview markdown-body"></article></div></div><footer class="editor-footer"><div class="footer-left"><span id="characters">0 characters</span> · <span id="words">0 words</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" 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" hidden></span></summary><div class="room-popover"><section class="room-users"><strong>In this room</strong><ul id="room-users"></ul></section><section class="room-chat"><div class="room-chat__head"><strong>Room chat</strong><span>Messages disappear after disconnect</span></div><div id="chat-messages" class="chat-messages" aria-live="polite"></div><form id="chat-form" class="chat-form"><input id="chat-input" maxlength="1000" autocomplete="off" placeholder="Write a message…" aria-label="Chat message"><button type="submit">Send</button></form></section></div></details></div><span class="footer-right"><button id="shortcuts-button" class="footer-link" type="button" aria-haspopup="dialog">Shortcuts</button> · <button id="footer-files" class="footer-link" type="button">0 files</button> · <span id="save-state">Changes are saved automatically</span></span></footer></section><aside id="history-panel" class="history-panel" aria-hidden="true"><div class="history-header"><div><h2>Change history</h2><p>Author, time, and version preview</p></div><button id="close-history" class="icon-button">×</button></div><div id="history-list" class="history-list"></div></aside></main>
|
||||
<dialog id="shortcuts-dialog"><div class="dialog-panel shortcuts-panel"><div class="files-head"><div><h2>Keyboard shortcuts</h2><p>Use Ctrl on Windows/Linux or Cmd on macOS.</p></div><button id="close-shortcuts" class="icon-button" type="button">×</button></div><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 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 H1–H4</span></div></div></dialog><dialog id="files-dialog" class="image-editor-dialog files-dialog"><div class="image-editor-panel files-panel"><div class="files-head"><div><h2>Note files</h2><p>Copy a direct link or ready Markdown/HTML code.</p></div><button id="close-files" class="icon-button" type="button">×</button></div><div id="files-list" class="files-list"></div></div></dialog><dialog id="identity-dialog"><form id="identity-form" autocomplete="on" class="dialog-panel identity-panel"><button id="close-identity" class="modal-close" type="button" aria-label="Close dialog">×</button><h2>What should we call you?</h2><p class="dialog-copy">Use a free nickname without an account, or register it to reserve it.</p><input id="nickname" maxlength="40" autocomplete="off" data-bwignore="true" required placeholder="Name or nickname"><div class="identity-actions"><button id="guest-continue" class="primary-button" type="submit">Continue as guest</button><button id="show-register" class="text-button" type="button">Register</button><button id="show-login" class="text-button" type="button">Log in</button></div><section id="auth-panel" class="auth-panel" hidden><h3 id="auth-mode-title">Log in</h3><label id="auth-email-field">E-mail<input id="auth-email" name="username" type="email" maxlength="320" autocomplete="username" placeholder="you@example.com"></label><label>Password<input id="auth-password" name="password" type="password" minlength="8" maxlength="128" autocomplete="current-password"></label><button id="auth-submit" class="primary-button" type="submit">Log in and continue</button><div class="identity-links"><button id="show-reset" class="text-button" type="button">Forgot password?</button><button id="auth-back" class="text-button" type="button">Back to nickname</button><button id="logout-account" class="text-button" type="button">Log out saved account</button></div></section><p id="identity-error" class="form-message error" role="alert"></p></form></dialog>
|
||||
<dialog id="password-dialog"><form id="password-form" class="dialog-panel"><h2>Protected note</h2><input id="open-password" type="password" autocomplete="current-password" required placeholder="Password"><p id="password-error" class="form-message error"></p><button class="primary-button">Open</button><a class="dialog-link" href="/">Back</a></form></dialog><div id="toast" class="toast"></div></body></html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>__PAD_TITLE__ · RustPad</title>
|
||||
<link rel="stylesheet" href="/assets/css/styles.css?v=__ASSET_VERSION__">
|
||||
<script
|
||||
type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script>
|
||||
<script type="module" src="/assets/js/pad.js?v=__ASSET_VERSION__"></script>
|
||||
</head>
|
||||
|
||||
<body class="pad-page" data-registration-enabled="__REGISTRATION_ENABLED__">
|
||||
<header class="app-header">
|
||||
<div class="app-header__main"><a class="brand" href="/">RustPad</a><span class="header-divider"></span>
|
||||
<div class="document-heading">
|
||||
<h1 id="pad-title">__PAD_TITLE__</h1>
|
||||
<p id="pad-url" class="document-url"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions"><span class="user-color-control"><button id="current-user" class="user-chip"
|
||||
type="button" title="Change your color"><span class="user-chip__dot" aria-hidden="true"></span><span
|
||||
class="user-chip__name"></span></button><input id="user-color-picker" class="user-color-picker"
|
||||
type="color" aria-label="Choose your color"></span><button id="copy-link"
|
||||
class="secondary-button">Copy link</button><button id="publish-page"
|
||||
class="secondary-button">Page</button><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><button id="files-button"
|
||||
class="secondary-button">Files</button><button id="history-button"
|
||||
class="secondary-button">History</button></div>
|
||||
</header>
|
||||
<main class="editor-layout">
|
||||
<section class="editor-panel">
|
||||
<div class="editor-toolbar">
|
||||
<div class="toolbar-group"><button data-format="bold" title="Bold"><strong>B</strong></button><button
|
||||
data-format="italic" title="Italic"><em>I</em></button><button data-format="strike"
|
||||
title="Strikethrough"><s>S</s></button><button data-format="heading1">H1</button><button
|
||||
data-format="heading2">H2</button><button data-format="heading3">H3</button><button
|
||||
data-format="heading4">H4</button><button data-format="bullet">• List</button><button
|
||||
data-format="number" title="Numbered list · Ctrl/Cmd+Shift+7">1. List</button><button
|
||||
data-format="task" title="Task list · Ctrl/Cmd+Shift+9">☑ Task</button><button
|
||||
data-format="quote">Quote</button><button data-format="link">Link</button>
|
||||
<details class="markdown-more">
|
||||
<summary title="Extended Markdown">More</summary>
|
||||
<div class="markdown-more-menu"><button type="button" data-format="details">Collapsible
|
||||
section</button><button type="button" data-format="inline-code">Inline
|
||||
code</button><button type="button" data-format="codeblock">Code block</button><button
|
||||
type="button" data-format="table">Table</button><button type="button"
|
||||
data-format="footnote">Footnote</button><button type="button"
|
||||
data-format="definition">Definition</button><button type="button"
|
||||
data-format="highlight">Highlight</button><button type="button"
|
||||
data-format="subscript">Subscript</button><button type="button"
|
||||
data-format="superscript">Superscript</button><button type="button"
|
||||
data-format="horizontal-rule">Horizontal rule</button></div>
|
||||
</details>
|
||||
</div>
|
||||
<div class="editor-controls"><label>Font<select id="font-family">
|
||||
<option value="mono">Mono</option>
|
||||
<option value="system">System</option>
|
||||
<option value="serif">Serif</option>
|
||||
<option value="arial">Arial</option>
|
||||
<option value="georgia">Georgia</option>
|
||||
</select></label><label>Size<select id="font-size">
|
||||
<option value="14" selected>14</option>
|
||||
<option value="16">16</option>
|
||||
<option value="18">18</option>
|
||||
<option value="20">20</option>
|
||||
<option value="22">22</option>
|
||||
</select></label></div><button id="upload-button"
|
||||
class="toolbar-action">Image/file</button><input id="file-input" type="file" hidden><label
|
||||
class="line-toggle"><input id="line-numbers-toggle" type="checkbox" checked> Lines</label><label
|
||||
class="line-toggle"><input id="compact-toggle" type="checkbox" checked> Compact</label>
|
||||
<div class="toolbar-fill"></div><button id="mode-toggle" class="markdown-toggle active"
|
||||
aria-pressed="true">Markdown</button>
|
||||
<div class="view-switch"><button data-view="edit">Edit</button><button data-view="split"
|
||||
class="active">Split</button><button data-view="preview">Preview</button></div>
|
||||
</div>
|
||||
<div id="editor-workspace" class="workspace view-split">
|
||||
<div class="editor-column">
|
||||
<div class="column-label">Editor</div>
|
||||
<div class="editor-shell">
|
||||
<div id="line-gutter" class="line-gutter" aria-hidden="true"></div>
|
||||
<div id="owner-labels" class="owner-labels" aria-hidden="true"></div><textarea id="editor"
|
||||
wrap="off" placeholder="Start writing…" spellcheck="false"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div class="preview-column">
|
||||
<div id="preview-label" class="column-label">Markdown preview</div>
|
||||
<article id="preview" class="preview markdown-body"></article>
|
||||
</div>
|
||||
</div>
|
||||
<footer class="editor-footer">
|
||||
<div class="footer-left"><span id="characters">0 characters</span> · <span id="words">0 words</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"
|
||||
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"
|
||||
hidden></span></summary>
|
||||
<div class="room-popover">
|
||||
<section class="room-users"><strong>In this room</strong>
|
||||
<ul id="room-users"></ul>
|
||||
</section>
|
||||
<section class="room-chat">
|
||||
<div class="room-chat__head"><strong>Room chat</strong><span>Messages disappear after
|
||||
disconnect</span></div>
|
||||
<div id="chat-messages" class="chat-messages" aria-live="polite"></div>
|
||||
<form id="chat-form" class="chat-form"><input id="chat-input" maxlength="1000"
|
||||
autocomplete="off" placeholder="Write a message…"
|
||||
aria-label="Chat message"><button type="submit">Send</button></form>
|
||||
</section>
|
||||
</div>
|
||||
</details>
|
||||
</div><span class="footer-right"><button id="shortcuts-button" class="footer-link" type="button"
|
||||
aria-haspopup="dialog">Shortcuts</button> · <button id="footer-files" class="footer-link"
|
||||
type="button">0 files</button> · <span id="save-state">Changes are saved
|
||||
automatically</span></span>
|
||||
</footer>
|
||||
</section>
|
||||
<aside id="history-panel" class="history-panel" aria-hidden="true">
|
||||
<div class="history-header">
|
||||
<div>
|
||||
<h2>Change history</h2>
|
||||
<p>Author, time, and version preview</p>
|
||||
</div><button id="close-history" class="icon-button">×</button>
|
||||
</div>
|
||||
<div id="history-list" class="history-list"></div>
|
||||
</aside>
|
||||
</main>
|
||||
<dialog id="shortcuts-dialog">
|
||||
<div class="dialog-panel shortcuts-panel">
|
||||
<div class="files-head">
|
||||
<div>
|
||||
<h2>Keyboard shortcuts</h2>
|
||||
<p>Use Ctrl on Windows/Linux or Cmd on macOS.</p>
|
||||
</div><button id="close-shortcuts" class="icon-button" type="button">×</button>
|
||||
</div>
|
||||
<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
|
||||
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 H1–H4</span></div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="files-dialog" class="image-editor-dialog files-dialog">
|
||||
<div class="image-editor-panel files-panel">
|
||||
<div class="files-head">
|
||||
<div>
|
||||
<h2>Note files</h2>
|
||||
<p>Copy a direct link or ready Markdown/HTML code.</p>
|
||||
</div><button id="close-files" class="icon-button" type="button">×</button>
|
||||
</div>
|
||||
<div id="files-list" class="files-list"></div>
|
||||
</div>
|
||||
</dialog>
|
||||
<dialog id="identity-dialog">
|
||||
<form id="identity-form" autocomplete="on" class="dialog-panel identity-panel"><button id="close-identity"
|
||||
class="modal-close" type="button" aria-label="Close dialog">×</button>
|
||||
<h2>What should we call you?</h2>
|
||||
<p class="dialog-copy">Use a free nickname without an account, or register it to reserve it.</p><input
|
||||
id="nickname" maxlength="40" autocomplete="off" data-bwignore="true" required
|
||||
placeholder="Name or nickname">
|
||||
<div class="identity-actions"><button id="guest-continue" class="primary-button" type="submit">Continue as
|
||||
guest</button><button id="show-register" class="text-button" type="button">Register</button><button
|
||||
id="show-login" class="text-button" type="button">Log in</button></div>
|
||||
<section id="auth-panel" class="auth-panel" hidden>
|
||||
<h3 id="auth-mode-title">Log in</h3><label id="auth-email-field">E-mail<input id="auth-email"
|
||||
name="username" type="email" maxlength="320" autocomplete="username"
|
||||
placeholder="you@example.com"></label><label>Password<input id="auth-password" name="password"
|
||||
type="password" minlength="8" maxlength="128" autocomplete="current-password"></label><button
|
||||
id="auth-submit" class="primary-button" type="submit">Log in and continue</button>
|
||||
<div class="identity-links"><button id="show-reset" class="text-button" type="button">Forgot
|
||||
password?</button><button id="auth-back" class="text-button" type="button">Back to
|
||||
nickname</button><button id="logout-account" class="text-button" type="button">Log out saved
|
||||
account</button></div>
|
||||
</section>
|
||||
<p id="identity-error" class="form-message error" role="alert"></p>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="password-dialog">
|
||||
<form id="password-form" class="dialog-panel">
|
||||
<h2>Protected note</h2><input id="open-password" type="password" autocomplete="current-password" required
|
||||
placeholder="Password">
|
||||
<p id="password-error" class="form-message error"></p><button class="primary-button">Open</button><a
|
||||
class="dialog-link" href="/">Back</a>
|
||||
</form>
|
||||
</dialog>
|
||||
<div id="toast" class="toast"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+6
-1
@@ -1,13 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>Published note · RustPad</title>
|
||||
<link rel="stylesheet" href="/assets/css/styles.css?v=__ASSET_VERSION__">
|
||||
<script type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script><script type="module" src="/assets/js/public.js?v=__ASSET_VERSION__"></script>
|
||||
<script
|
||||
type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script>
|
||||
<script type="module" src="/assets/js/public.js?v=__ASSET_VERSION__"></script>
|
||||
</head>
|
||||
|
||||
<body class="public-page">
|
||||
<header class="public-header">
|
||||
<a class="brand" href="/">RustPad</a>
|
||||
@@ -20,4 +24,5 @@
|
||||
</main>
|
||||
<div id="toast" class="toast"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+61
-5
@@ -1,5 +1,61 @@
|
||||
<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta name="color-scheme" content="dark"><title>__WORKSPACE_TITLE__ · RustPad</title><link rel="stylesheet" href="/assets/css/styles.css?v=__ASSET_VERSION__"><script type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script><script type="module" src="/assets/js/workspace.js?v=__ASSET_VERSION__"></script></head>
|
||||
<body><header class="app-header"><div class="app-header__main"><a class="brand" href="/">RustPad</a><span class="header-divider"></span><div class="document-heading"><h1 id="workspace-title">__WORKSPACE_TITLE__</h1><p id="workspace-url" class="document-url"></p></div></div><div class="header-actions"><button id="copy-workspace-link" class="secondary-button">Copy link</button></div></header>
|
||||
<main class="workspace-page"><section class="workspace-top"><div><h2>Notes</h2><p>Select a note or create a new one.</p></div><button id="new-note-button" class="primary-button inline-button">New note</button></section><div class="notes-toolbar"><span class="notes-toolbar__label">View</span><div class="notes-view-switch" role="group" aria-label="Notes view"><button type="button" data-notes-view="grid" class="active" aria-pressed="true">Cards</button><button type="button" data-notes-view="table" aria-pressed="false">Table</button></div></div><p id="workspace-error" class="form-message error"></p><section id="notes-list" class="notes-grid" aria-live="polite"></section></main>
|
||||
<dialog id="password-dialog"><form id="password-form" class="dialog-panel"><h2>Protected workspace</h2><input id="open-password" type="password" autocomplete="current-password" required placeholder="Password"><p id="password-error" class="form-message error"></p><button class="primary-button">Open</button><a href="/" class="dialog-link">Cancel</a></form></dialog>
|
||||
<dialog id="note-dialog"><form id="note-form" class="dialog-panel"><h2>New note</h2><input id="note-name" maxlength="80" required placeholder="Note name"><label class="dialog-check"><input id="note-protect" type="checkbox" checked> Protect this note from deletion</label><p id="note-error" class="form-message error"></p><div class="dialog-actions"><button type="button" id="cancel-note" class="secondary-button">Cancel</button><button class="primary-button">Create</button></div></form></dialog><div id="toast" class="toast"></div></body></html>
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<meta name="color-scheme" content="dark">
|
||||
<title>__WORKSPACE_TITLE__ · RustPad</title>
|
||||
<link rel="stylesheet" href="/assets/css/styles.css?v=__ASSET_VERSION__">
|
||||
<script
|
||||
type="importmap">{"imports":{"@rustpad/api":"/assets/js/api.js?v=__ASSET_VERSION__","@rustpad/clipboard":"/assets/js/clipboard.js?v=__ASSET_VERSION__","@rustpad/editor-format":"/assets/js/editor-format.js?v=__ASSET_VERSION__","@rustpad/markdown":"/assets/js/markdown.js?v=__ASSET_VERSION__","@rustpad/session":"/assets/js/session.js?v=__ASSET_VERSION__","@rustpad/socket":"/assets/js/socket.js?v=__ASSET_VERSION__","@rustpad/url-state":"/assets/js/url-state.js?v=__ASSET_VERSION__"}}</script>
|
||||
<script type="module" src="/assets/js/workspace.js?v=__ASSET_VERSION__"></script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header class="app-header">
|
||||
<div class="app-header__main"><a class="brand" href="/">RustPad</a><span class="header-divider"></span>
|
||||
<div class="document-heading">
|
||||
<h1 id="workspace-title">__WORKSPACE_TITLE__</h1>
|
||||
<p id="workspace-url" class="document-url"></p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions"><button id="copy-workspace-link" class="secondary-button">Copy link</button></div>
|
||||
</header>
|
||||
<main class="workspace-page">
|
||||
<section class="workspace-top">
|
||||
<div>
|
||||
<h2>Notes</h2>
|
||||
<p>Select a note or create a new one.</p>
|
||||
</div><button id="new-note-button" class="primary-button inline-button">New note</button>
|
||||
</section>
|
||||
<div class="notes-toolbar"><span class="notes-toolbar__label">View</span>
|
||||
<div class="notes-view-switch" role="group" aria-label="Notes view"><button type="button"
|
||||
data-notes-view="grid" class="active" aria-pressed="true">Cards</button><button type="button"
|
||||
data-notes-view="table" aria-pressed="false">Table</button></div>
|
||||
</div>
|
||||
<p id="workspace-error" class="form-message error"></p>
|
||||
<section id="notes-list" class="notes-grid" aria-live="polite"></section>
|
||||
</main>
|
||||
<dialog id="password-dialog">
|
||||
<form id="password-form" class="dialog-panel">
|
||||
<h2>Protected workspace</h2><input id="open-password" type="password" autocomplete="current-password"
|
||||
required placeholder="Password">
|
||||
<p id="password-error" class="form-message error"></p><button class="primary-button">Open</button><a
|
||||
href="/" class="dialog-link">Cancel</a>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="note-dialog">
|
||||
<form id="note-form" class="dialog-panel">
|
||||
<h2>New note</h2><input id="note-name" maxlength="80" required placeholder="Note name"><label
|
||||
class="dialog-check"><input id="note-protect" type="checkbox" checked> Protect this note from
|
||||
deletion</label>
|
||||
<p id="note-error" class="form-message error"></p>
|
||||
<div class="dialog-actions"><button type="button" id="cancel-note"
|
||||
class="secondary-button">Cancel</button><button class="primary-button">Create</button></div>
|
||||
</form>
|
||||
</dialog>
|
||||
<div id="toast" class="toast"></div>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
Reference in New Issue
Block a user