ui: update to defaults, less assumptions about layout

This commit is contained in:
Lewis Wynne 2026-04-28 00:30:08 +01:00
parent 1d993fb6cc
commit 6bbdb50ab8
7 changed files with 113 additions and 102 deletions

View file

@ -69,23 +69,23 @@
# .entry-name, .entry-website, .entry-body # .entry-name, .entry-website, .entry-body
# BOOK_STYLE= # BOOK_STYLE=
# Text shown above the form. # Text shown above the form. Empty by default.
# BOOK_FORM_PROMPT=Thanks for visiting. Sign the guestbook! # BOOK_FORM_PROMPT=Thanks for visiting. Sign the guestbook!
# Submit button text. # Submit button text.
# BOOK_BUTTON_TEXT=sign # BOOK_BUTTON_TEXT=sign
# Label for the name field. # Label for the name field.
# BOOK_LABEL_NAME=Your name: # BOOK_LABEL_NAME=name
# Label for the website field. # Label for the website field.
# BOOK_LABEL_WEBSITE=Your website (optional): # BOOK_LABEL_WEBSITE=website (optional)
# Label for the message field. # Label for the message field.
# BOOK_LABEL_MESSAGE=Your message: # BOOK_LABEL_MESSAGE=message
# Message textarea width in pixels. # Message textarea width in pixels.
# BOOK_TEXTAREA_WIDTH=400 # BOOK_TEXTAREA_WIDTH=320
# Message textarea height in pixels. # Message textarea height in pixels.
# BOOK_TEXTAREA_HEIGHT=150 # BOOK_TEXTAREA_HEIGHT=150
@ -103,7 +103,7 @@
# BOOK_ENABLE_DRAWINGS=false # BOOK_ENABLE_DRAWINGS=false
# Drawing canvas width in pixels. # Drawing canvas width in pixels.
# BOOK_CANVAS_WIDTH=400 # BOOK_CANVAS_WIDTH=320
# Drawing canvas height in pixels. # Drawing canvas height in pixels.
# BOOK_CANVAS_HEIGHT=200 # BOOK_CANVAS_HEIGHT=200

View file

@ -156,27 +156,28 @@ Running `guestbook` with no env vars will give you a working guestbook on `local
# Custom CSS injected into a style tag. # Custom CSS injected into a style tag.
# Classes: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input, # Classes: .guestbook-form, .guestbook-prompt, .guestbook-label, .guestbook-input,
# .guestbook-textarea, .guestbook-button, .entry, .entry-header, .entry-date, # .guestbook-textarea, .guestbook-button, .entries, .entry-header, .entry-date,
# .entry-name, .entry-website, .entry-body # .entry-name, .entry-website, .entry-body, .entry-drawing-wrap, .entry-drawing,
# .entry-voice-note-wrap
# BOOK_STYLE= # BOOK_STYLE=
# Text shown above the form. # Text shown above the form. Empty by default.
# BOOK_FORM_PROMPT=Thanks for visiting. Sign the guestbook! # BOOK_FORM_PROMPT=Thanks for visiting. Sign the guestbook!
# Submit button text. # Submit button text.
# BOOK_BUTTON_TEXT=sign # BOOK_BUTTON_TEXT=sign
# Label for the name field. # Label for the name field.
# BOOK_LABEL_NAME=Your name: # BOOK_LABEL_NAME=name
# Label for the website field. # Label for the website field.
# BOOK_LABEL_WEBSITE=Your website (optional): # BOOK_LABEL_WEBSITE=website (optional)
# Label for the message field. # Label for the message field.
# BOOK_LABEL_MESSAGE=Your message: # BOOK_LABEL_MESSAGE=message
# Message textarea width in pixels. # Message textarea width in pixels.
# BOOK_TEXTAREA_WIDTH=400 # BOOK_TEXTAREA_WIDTH=320
# Message textarea height in pixels. # Message textarea height in pixels.
# BOOK_TEXTAREA_HEIGHT=150 # BOOK_TEXTAREA_HEIGHT=150
@ -194,7 +195,7 @@ Running `guestbook` with no env vars will give you a working guestbook on `local
# BOOK_ENABLE_DRAWINGS=false # BOOK_ENABLE_DRAWINGS=false
# Drawing canvas width in pixels. # Drawing canvas width in pixels.
# BOOK_CANVAS_WIDTH=400 # BOOK_CANVAS_WIDTH=320
# Drawing canvas height in pixels. # Drawing canvas height in pixels.
# BOOK_CANVAS_HEIGHT=200 # BOOK_CANVAS_HEIGHT=200
@ -234,7 +235,7 @@ services.guestbook = {
websites.enable = true; websites.enable = true;
drawing = { drawing = {
enable = false; enable = false;
canvasWidth = 400; canvasWidth = 320;
canvasHeight = 200; canvasHeight = 200;
}; };
voiceNote = { voiceNote = {
@ -275,15 +276,15 @@ services.guestbook = {
cssFile = null; cssFile = null;
templateFile = null; templateFile = null;
successTemplateFile = null; successTemplateFile = null;
greeting = "Thanks for visiting. Sign the guestbook!"; greeting = "";
labels = { labels = {
submit = "sign"; submit = "sign";
name = "Your name:"; name = "name";
website = "Your website (optional):"; website = "website (optional)";
message = "Your message:"; message = "message";
}; };
message = { message = {
width = 400; width = 320;
height = 150; height = 150;
}; };
}; };
@ -412,14 +413,12 @@ entered into the 'message' field.
</head> </head>
<body> <body>
<div class="page-container"> <div class="page-container">
{{title}} <h1>{{title}}</h1>
=========
{{prompt}} {{prompt}}
{{form}} {{form}}
entries <h1>entries</h1>
=======
{{entries}} {{entries}}
</div> </div>
</body> </body>
@ -462,9 +461,12 @@ entries
```css ```css
/* Page container */ /* Page container */
body {
margin: 0;
line-height: 1.5;
}
.page-container { .page-container {
max-width: 70ch; max-width: 70ch;
margin: 0 auto;
padding: 1rem; padding: 1rem;
word-wrap: break-word; word-wrap: break-word;
} }
@ -472,37 +474,37 @@ entries
/* Form */ /* Form */
.guestbook-prompt { display: block; margin-bottom: 1em; } .guestbook-prompt { display: block; margin-bottom: 1em; }
.guestbook-form {} .guestbook-form {}
.guestbook-label { display: block; } .guestbook-label { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; }
.guestbook-input { display: block; margin-bottom: 0.5em; } .guestbook-input { display: block; margin-bottom: 0.2em; }
.guestbook-textarea { display: block; box-sizing: border-box; max-width: 100%; margin-bottom: 0.5em; } .guestbook-textarea { display: block; box-sizing: border-box; max-width: 100%; margin-bottom: 0.2em; }
.guestbook-button { display: block; margin-top: 1em; margin-bottom: 1.5em; } .guestbook-button { display: block; }
/* Drawings */ /* Drawings */
.guestbook-canvas { border: 1px solid #000; cursor: crosshair; display: block; max-width: 100%; height: auto; } .guestbook-canvas { border: 1px solid #000; cursor: crosshair; display: block; max-width: 100%; height: auto; }
.guestbook-canvas-tools { display: block; } .guestbook-canvas-tools { display: block; }
.guestbook-canvas-tools a { cursor: pointer; } .guestbook-canvas-tools a { cursor: pointer; }
.guestbook-drawing-wrap { display: block; margin-bottom: 0.5em; } .guestbook-drawing-wrap { display: block; }
.guestbook-drawing-inline a { cursor: pointer; } .guestbook-drawing-inline a { cursor: pointer; }
.guestbook-drawing-content:empty { display: none; } .guestbook-drawing-content:empty { display: none; }
.guestbook-drawing-content { display: block; margin-bottom: 0.5em; } .guestbook-drawing-content { display: block; }
.guestbook-swatch { display: inline-block; width: 0.85em; height: 0.85em; border: 1px solid #000; cursor: pointer; vertical-align: middle; box-sizing: border-box; margin: 0 1px; } .guestbook-swatch { display: inline-block; width: 0.85em; height: 0.85em; border: 1px solid #000; cursor: pointer; vertical-align: middle; box-sizing: border-box; margin: 0 1px; }
.guestbook-swatch.active { border: 1px solid #000; outline: 1px solid #000; } .guestbook-swatch.active { border: 1px solid #000; outline: 1px solid #000; }
.guestbook-size-slider { width: 4em; vertical-align: middle; } .guestbook-size-slider { width: 4em; vertical-align: middle; }
.entry-drawing { max-width: 100%; } .entry-drawing { max-width: 100%; }
/* Voice notes */ /* Voice notes */
.guestbook-voice-wrap { display: block; margin-bottom: 0.5em; } .guestbook-voice-wrap { display: block; }
.guestbook-voice-record.recording { color: red; } .guestbook-voice-record.recording { color: red; }
.guestbook-voice-timer { font-variant-numeric: tabular-nums; } .guestbook-voice-timer { font-variant-numeric: tabular-nums; }
.guestbook-voice-playback:empty { display: none; } .guestbook-voice-playback:empty { display: none; }
.guestbook-voice-playback { display: block; white-space: normal; } .guestbook-voice-playback { display: block; white-space: normal; }
audio { display: block; margin-bottom: 0.3em; height: 2em; } audio { display: block; height: 2em; }
/* Entries */ /* Entries */
.entry { margin: 0.5em 0; } .entries { margin: 0; line-height: 1; }
.entry-header { margin-bottom: 0.2em; } .entries dt:not(:first-child) { margin-top: 0.5rem; }
.entry-date {} .entry-date {}
.entry-name {} .entry-name { font-weight: bold; }
.entry-website {} .entry-website {}
.entry-body { white-space: pre-wrap; } .entry-body { white-space: pre-wrap; }
``` ```

View file

@ -98,7 +98,7 @@ in
canvasWidth = mkOption { canvasWidth = mkOption {
type = types.int; type = types.int;
default = 400; default = 320;
description = "Drawing canvas width in pixels."; description = "Drawing canvas width in pixels.";
}; };
@ -251,7 +251,7 @@ in
greeting = mkOption { greeting = mkOption {
type = types.str; type = types.str;
default = "Thanks for visiting. Sign the guestbook!"; default = "";
description = "Text shown above the form."; description = "Text shown above the form.";
}; };
@ -264,27 +264,27 @@ in
name = mkOption { name = mkOption {
type = types.str; type = types.str;
default = "Your name:"; default = "name";
description = "Label for the name field."; description = "Label for the name field (used as both screen-reader label and placeholder).";
}; };
website = mkOption { website = mkOption {
type = types.str; type = types.str;
default = "Your website (optional):"; default = "website (optional)";
description = "Label for the website field."; description = "Label for the website field (used as both screen-reader label and placeholder).";
}; };
message = mkOption { message = mkOption {
type = types.str; type = types.str;
default = "Your message:"; default = "message";
description = "Label for the message field."; description = "Label for the message field (used as both screen-reader label and placeholder).";
}; };
}; };
message = { message = {
width = mkOption { width = mkOption {
type = types.int; type = types.int;
default = 400; default = 320;
description = "Message textarea width in pixels."; description = "Message textarea width in pixels.";
}; };

View file

@ -137,7 +137,7 @@ impl Config {
.map(|v| v != "false") .map(|v| v != "false")
.unwrap_or(false), .unwrap_or(false),
canvas_width: env::var("BOOK_CANVAS_WIDTH") canvas_width: env::var("BOOK_CANVAS_WIDTH")
.unwrap_or_else(|_| "400".into()) .unwrap_or_else(|_| "320".into())
.parse() .parse()
.map_err(|_| "BOOK_CANVAS_WIDTH must be a number")?, .map_err(|_| "BOOK_CANVAS_WIDTH must be a number")?,
canvas_height: env::var("BOOK_CANVAS_HEIGHT") canvas_height: env::var("BOOK_CANVAS_HEIGHT")
@ -167,18 +167,16 @@ impl Config {
}) })
.or_else(|| env::var("BOOK_STYLE").ok()) .or_else(|| env::var("BOOK_STYLE").ok())
.unwrap_or_default(), .unwrap_or_default(),
form_prompt: env::var("BOOK_FORM_PROMPT") form_prompt: env::var("BOOK_FORM_PROMPT").unwrap_or_default(),
.unwrap_or_else(|_| "Thanks for visiting. Sign the guestbook!".into()),
button_text: env::var("BOOK_BUTTON_TEXT") button_text: env::var("BOOK_BUTTON_TEXT")
.unwrap_or_else(|_| "sign".into()), .unwrap_or_else(|_| "sign".into()),
label_name: env::var("BOOK_LABEL_NAME") label_name: env::var("BOOK_LABEL_NAME").unwrap_or_else(|_| "name".into()),
.unwrap_or_else(|_| "Your name:".into()),
label_website: env::var("BOOK_LABEL_WEBSITE") label_website: env::var("BOOK_LABEL_WEBSITE")
.unwrap_or_else(|_| "Your website (optional):".into()), .unwrap_or_else(|_| "website (optional)".into()),
label_message: env::var("BOOK_LABEL_MESSAGE") label_message: env::var("BOOK_LABEL_MESSAGE")
.unwrap_or_else(|_| "Your message:".into()), .unwrap_or_else(|_| "message".into()),
textarea_width: env::var("BOOK_TEXTAREA_WIDTH") textarea_width: env::var("BOOK_TEXTAREA_WIDTH")
.unwrap_or_else(|_| "400".into()) .unwrap_or_else(|_| "320".into())
.parse() .parse()
.map_err(|_| "BOOK_TEXTAREA_WIDTH must be a number")?, .map_err(|_| "BOOK_TEXTAREA_WIDTH must be a number")?,
textarea_height: env::var("BOOK_TEXTAREA_HEIGHT") textarea_height: env::var("BOOK_TEXTAREA_HEIGHT")
@ -318,9 +316,9 @@ mod tests {
let config = Config::from_env().unwrap(); let config = Config::from_env().unwrap();
assert!(!config.enable_drawings); assert!(!config.enable_drawings);
assert_eq!(config.canvas_width, 400); assert_eq!(config.canvas_width, 320);
assert_eq!(config.canvas_height, 200); assert_eq!(config.canvas_height, 200);
assert_eq!(config.max_drawing_bytes(), 400 * 200 * 4); assert_eq!(config.max_drawing_bytes(), 320 * 200 * 4);
} }
#[test] #[test]

View file

@ -32,8 +32,8 @@ pub fn render_page(template: &str, config: &Config, entries: &[Entry], form_html
pub fn render_form(config: &Config) -> String { pub fn render_form(config: &Config) -> String {
let website_section = if config.enable_website_links { let website_section = if config.enable_website_links {
format!( format!(
"\n<label class=\"guestbook-label\" for=\"website\">{}</label>\n<input class=\"guestbook-input\" id=\"website\" name=\"website\">\n", "\n<label class=\"guestbook-label\" for=\"website\">{label}</label>\n<input class=\"guestbook-input\" id=\"website\" name=\"website\" placeholder=\"{label}\">\n",
config.label_website label = config.label_website
) )
} else { } else {
String::new() String::new()
@ -41,8 +41,8 @@ pub fn render_form(config: &Config) -> String {
let captcha_section = if config.enable_captcha { let captcha_section = if config.enable_captcha {
format!( format!(
"\n<label class=\"guestbook-label\" for=\"captcha\">{}</label>\n<input class=\"guestbook-input\" id=\"captcha\" name=\"captcha\" required>\n", "\n<label class=\"guestbook-label\" for=\"captcha\">{label}</label>\n<input class=\"guestbook-input\" id=\"captcha\" name=\"captcha\" placeholder=\"{label}\" required>\n",
config.captcha_question label = config.captcha_question
) )
} else { } else {
String::new() String::new()
@ -186,10 +186,10 @@ pub fn render_form(config: &Config) -> String {
format!( format!(
r#"<form class="guestbook-form" method="post" action="/submit" accept-charset="UTF-8"> r#"<form class="guestbook-form" method="post" action="/submit" accept-charset="UTF-8">
<label class="guestbook-label" for="name">{label_name}</label> <label class="guestbook-label" for="name">{label_name}</label>
<input class="guestbook-input" id="name" name="name" required> <input class="guestbook-input" id="name" name="name" placeholder="{label_name}" required>
{website_section} {website_section}
<label class="guestbook-label" for="message">{label_message}</label> <label class="guestbook-label" for="message">{label_message}</label>
<textarea class="guestbook-textarea" id="message" name="message" style="width:{tw}px;height:{th}px" required></textarea> <textarea class="guestbook-textarea" id="message" name="message" placeholder="{label_message}" style="width:{tw}px;height:{th}px" required></textarea>
{captcha_section} {captcha_section}
{drawing_section}{voice_note_section}<input name="url" aria-hidden="true" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)" tabindex="-1" autocomplete="off"><button class="guestbook-button" type="submit">{button}</button> {drawing_section}{voice_note_section}<input name="url" aria-hidden="true" style="position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0,0,0,0)" tabindex="-1" autocomplete="off"><button class="guestbook-button" type="submit">{button}</button>
</form>"#, </form>"#,
@ -256,13 +256,14 @@ fn escape_html(s: &str) -> String {
} }
fn render_entries(entries: &[Entry], config: &Config) -> String { fn render_entries(entries: &[Entry], config: &Config) -> String {
let mut html = String::new(); if entries.is_empty() {
for (i, entry) in entries.iter().enumerate() { return String::new();
if i > 0 {
html.push_str("<hr class=\"entry-separator\">");
} }
let mut html = String::from("<dl class=\"entries\">");
for entry in entries {
html.push_str(&render_entry(entry, config)); html.push_str(&render_entry(entry, config));
} }
html.push_str("</dl>");
html html
} }
@ -272,18 +273,15 @@ fn render_entry(entry: &Entry, config: &Config) -> String {
} else { } else {
escape_html(&entry.meta.name) escape_html(&entry.meta.name)
}; };
let mut header = format!( let name_html = if config.enable_website_links && !entry.meta.website.is_empty() {
"<header class=\"entry-header\"><span class=\"entry-date\">{}</span> - <span class=\"entry-name\">{}</span>", format!(
&entry.meta.date[..10], name "<a class=\"entry-website\" href=\"{}\">{}</a>",
); escape_html(&entry.meta.website),
if config.enable_website_links && !entry.meta.website.is_empty() { name
let website = escape_html(&entry.meta.website); )
header.push_str(&format!( } else {
" (<a class=\"entry-website\" href=\"{}\">{}</a>)", name
website, website };
));
}
header.push_str("</header>");
let body = if config.enable_html_injection { let body = if config.enable_html_injection {
entry.body.clone() entry.body.clone()
} else { } else {
@ -291,7 +289,7 @@ fn render_entry(entry: &Entry, config: &Config) -> String {
}; };
let drawing_html = if !entry.meta.drawing.is_empty() { let drawing_html = if !entry.meta.drawing.is_empty() {
format!( format!(
"<img class=\"entry-drawing\" src=\"/drawings/{}\" alt=\"Drawing by {}\">", "<dd class=\"entry-drawing-wrap\"><img class=\"entry-drawing\" src=\"/drawings/{}\" alt=\"Drawing by {}\"></dd>",
escape_html(&entry.meta.drawing), escape_html(&entry.meta.drawing),
escape_html(&entry.meta.name) escape_html(&entry.meta.name)
) )
@ -300,15 +298,21 @@ fn render_entry(entry: &Entry, config: &Config) -> String {
}; };
let voice_note_html = if !entry.meta.voice_note.is_empty() { let voice_note_html = if !entry.meta.voice_note.is_empty() {
format!( format!(
"<span class=\"entry-voice-note\"><audio controls preload=\"metadata\" src=\"/voice_notes/{}\"></audio></span>", "<dd class=\"entry-voice-note-wrap\"><audio controls preload=\"metadata\" src=\"/voice_notes/{}\"></audio></dd>",
escape_html(&entry.meta.voice_note) escape_html(&entry.meta.voice_note)
) )
} else { } else {
String::new() String::new()
}; };
let body_html = if body.is_empty() {
String::new()
} else {
format!("<dd class=\"entry-body\">{body}</dd>")
};
let date = &entry.meta.date[..10];
format!( format!(
"<article class=\"entry\" id=\"{id}\">{header}{drawing_html}{voice_note_html}<div class=\"entry-body\">{body}</div></article>", "<dt class=\"entry-header\" id=\"{id}\" title=\"{date}\"><span class=\"entry-date\">{date}&emsp;</span><span class=\"entry-name\">{name_html}</span></dt>{body_html}{drawing_html}{voice_note_html}",
id = escape_html(&entry.id) id = escape_html(&entry.id),
) )
} }
@ -404,7 +408,8 @@ mod tests {
assert!(html.contains("entry-header")); assert!(html.contains("entry-header"));
assert!(html.contains("entry-name")); assert!(html.contains("entry-name"));
assert!(html.contains("entry-body")); assert!(html.contains("entry-body"));
assert!(html.contains("<article class=\"entry\" id=\"test\">")); assert!(html.contains("id=\"test\""));
assert!(html.contains("<dl class=\"entries\">"));
} }
#[test] #[test]

View file

@ -1,7 +1,10 @@
/* Page container */ /* Page container */
body {
margin: 0;
line-height: 1.5;
}
.page-container { .page-container {
max-width: 70ch; max-width: 70ch;
margin: 0 auto;
padding: 1rem; padding: 1rem;
word-wrap: break-word; word-wrap: break-word;
} }
@ -13,22 +16,28 @@
} }
.guestbook-form {} .guestbook-form {}
.guestbook-label { .guestbook-label {
display: block; position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
} }
.guestbook-input { .guestbook-input {
display: block; display: block;
margin-bottom: 0.5em; margin-bottom: 0.2em;
} }
.guestbook-textarea { .guestbook-textarea {
display: block; display: block;
box-sizing: border-box; box-sizing: border-box;
max-width: 100%; max-width: 100%;
margin-bottom: 0.5em; margin-bottom: 0.2em;
} }
.guestbook-button { .guestbook-button {
display: block; display: block;
margin-top: 1em;
margin-bottom: 1.5em;
} }
/* Drawings */ /* Drawings */
@ -47,7 +56,6 @@
} }
.guestbook-drawing-wrap { .guestbook-drawing-wrap {
display: block; display: block;
margin-bottom: 0.5em;
} }
.guestbook-drawing-inline a { .guestbook-drawing-inline a {
cursor: pointer; cursor: pointer;
@ -57,7 +65,6 @@
} }
.guestbook-drawing-content { .guestbook-drawing-content {
display: block; display: block;
margin-bottom: 0.5em;
} }
.guestbook-swatch { .guestbook-swatch {
display: inline-block; display: inline-block;
@ -84,7 +91,6 @@
/* Voice notes */ /* Voice notes */
.guestbook-voice-wrap { .guestbook-voice-wrap {
display: block; display: block;
margin-bottom: 0.5em;
} }
.guestbook-voice-record.recording { .guestbook-voice-record.recording {
color: red; color: red;
@ -101,19 +107,21 @@
} }
audio { audio {
display: block; display: block;
margin-bottom: 0.3em;
height: 2em; height: 2em;
} }
/* Entries */ /* Entries */
.entry { .entries {
margin: 0.5em 0; margin: 0;
line-height: 1;
} }
.entry-header { .entries dt:not(:first-child) {
margin-bottom: 0.2em; margin-top: 0.5rem;
} }
.entry-date {} .entry-date {}
.entry-name {} .entry-name {
font-weight: bold;
}
.entry-website {} .entry-website {}
.entry-body { .entry-body {
white-space: pre-wrap; white-space: pre-wrap;

View file

@ -30,14 +30,12 @@
</head> </head>
<body> <body>
<div class="page-container"> <div class="page-container">
{{title}} <h1>{{title}}</h1>
=========
{{prompt}} {{prompt}}
{{form}} {{form}}
entries <h1>entries</h1>
=======
{{entries}} {{entries}}
</div> </div>
</body> </body>