Add title pages, standard manuscript format and collapsible panels

Five changes to the export and the window chrome:

* Manuscript details gains a contact field and a "Begin exports with a
  title page" option: the title, the author beneath it, and the contact
  details beneath that, laid out line for line as typed. A chapters +
  master export puts the title page on the master, not on each chapter.

* "Standard manuscript format" lays an export out the way an agent or an
  editor expects a submission: 12pt Courier, double-spaced, half-inch
  first-line indents, a Surname / Title / page header on every page but
  the title page, chapters opening a third of the way down, and `---`
  rendered as the conventional centred `#` scene break. The title page
  becomes the submission kind, with contact top-left and an approximate
  word count top-right. Off by default; margins were already the
  standard 1in on US Letter and are unchanged either way.

* A folder button beside Export ODT opens the project folder in the file
  manager -- the enclosing git work tree, including one still awaiting
  confirmation, since showing a folder is a smaller question than
  choosing which repository to commit to.

* The toolbar and the file list each collapse, from the pair of buttons
  at the right of the menu bar or from the View menu, and Ctrl+D folds
  both away together for distraction-free writing. Both choices persist.

* The editor sizes to its viewport instead of a fixed 30 rows, so the
  height a folded panel gives back reaches the page rather than leaving
  grey space below the text box that did not even take focus.

Two traps worth recording. ODF's style:master-page-name is inherited,
and any paragraph style carrying one forces a page break before every
paragraph that uses it -- a four-line contact block came out as four
pages until Contact_20_Line stopped inheriting from Contact_20_Block;
a test now pins which styles may carry one. And the status bar shares
a function with the toolbar, so guarding that function's top rather
than the top panel alone silently took the word counts away with it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GYSGPDwkSzhm4qLqjCbqxU
This commit is contained in:
2026-08-25 17:58:59 -05:00
parent 66d3f9817e
commit f35397b49b
9 changed files with 1038 additions and 58 deletions
+88 -1
View File
@@ -6,8 +6,15 @@ use super::*;
impl App {
// ---- UI ----------------------------------------------------------------
/// The toolbar — the Workspace, Export, Grammar and Spelling rows — and the
/// status bar along the bottom, which is rendered here too.
///
/// The toolbar collapses through `show_animated`, which reserves no height
/// when hidden and slides rather than snapping. Note that it guards only the
/// top panel: the status bar shares this function and must keep drawing.
pub(super) fn top_bar(&mut self, ctx: &egui::Context) {
egui::TopBottomPanel::top("top").show(ctx, |ui| {
let show_toolbar = self.config.show_toolbar;
egui::TopBottomPanel::top("top").show_animated(ctx, show_toolbar, |ui| {
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("Workspace:");
@@ -56,6 +63,16 @@ impl App {
if ui.button("Export ODT").clicked() {
self.export_odt();
}
// Resolved first: the hover text borrows the path, and the click
// handler needs `self` mutably.
let project = self.project_root().display().to_string();
if ui
.button("📁")
.on_hover_text(format!("Open the project folder {project}"))
.clicked()
{
self.open_project_folder();
}
ui.separator();
if ui
.checkbox(&mut self.config.show_preview, "Preview")
@@ -310,6 +327,35 @@ impl App {
}
});
ui.menu_button("View", |ui| {
if ui
.checkbox(&mut self.config.show_toolbar, "Toolbar")
.on_hover_text(
"The Workspace, Export, Grammar and Spelling rows \
under the menu bar",
)
.clicked()
{
self.config.save();
}
if ui
.checkbox(&mut self.config.show_file_panel, "File list")
.on_hover_text("The file tree down the left-hand side")
.clicked()
{
self.config.save();
}
if ui
.button("🖹 Distraction-free Ctrl+D")
.on_hover_text(
"Fold away the toolbar and the file list together, \
leaving nothing but the page. Again to bring them back.",
)
.clicked()
{
ui.close_menu();
self.toggle_distraction_free();
}
ui.separator();
if ui
.checkbox(&mut self.config.show_preview, "Preview pane")
.clicked()
@@ -375,6 +421,30 @@ impl App {
self.show_cheatsheet = true;
}
});
// Right-aligned, and in the menu bar rather than the toolbar:
// a control that hides the toolbar cannot live inside it.
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let (icon, hint) = if self.config.show_toolbar {
("⬆", "Hide the toolbar (Workspace, Export, Grammar, Spelling)")
} else {
("⬇", "Show the toolbar (Workspace, Export, Grammar, Spelling)")
};
if ui.button(icon).on_hover_text(hint).clicked() {
self.config.show_toolbar = !self.config.show_toolbar;
self.config.save();
}
// Added second, so in a right-to-left layout it sits to the
// left of the toolbar toggle -- the side its panel is on.
let (icon, hint) = if self.config.show_file_panel {
("⬅", "Hide the file list")
} else {
("➡", "Show the file list")
};
if ui.button(icon).on_hover_text(hint).clicked() {
self.config.show_file_panel = !self.config.show_file_panel;
self.config.save();
}
});
});
});
}
@@ -535,6 +605,23 @@ impl App {
/// It exists so a hidden header can never be mistaken for a file that has
/// none: the editor is showing less than the file holds, and that has to be
/// visible without opening a menu.
/// Fold away everything that is not the page — the toolbar and the file
/// list — and put both back on the next invocation.
///
/// One command in and out, rather than a mode with a state of its own: if
/// any chrome is showing, hide it all; otherwise restore it all.
pub(super) fn toggle_distraction_free(&mut self) {
let any_chrome = self.config.show_toolbar || self.config.show_file_panel;
self.config.show_toolbar = !any_chrome;
self.config.show_file_panel = !any_chrome;
self.config.save();
self.status = if any_chrome {
"Distraction-free — Ctrl+D to bring the panels back".to_string()
} else {
"Panels restored".to_string()
};
}
pub(super) fn header_toggle(&mut self, ui: &mut egui::Ui) {
let collapsed = self.header_stash.is_some();
// Nothing to say about a file with no marker to fold at.