diff --git a/ROADMAP.md b/ROADMAP.md index 46a87add..d3216412 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~20 points and ~120–210 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (six original starter decks; previous: minor, hours to ~95% per audience added; ready 56% → 55%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere @@ -128,6 +128,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-14 | File › New starter decks: Briefing, Lesson, Pitch, Field Notes, Quarterly and Evening Talk, original text on our themes (`file.new {template}`, `file.templates`). The large template gallery is still open | | 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless | | 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline | | 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting | @@ -150,6 +151,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | Change | Summary | |---|---|---| +| 2026-10-14 | trivial | Progress log: six original starter decks | | 2026-10-13 | trivial | Progress log: wild text cases | | 2026-10-12 | trivial | Progress log: File tab (Backstage, #76) | | 2026-10-12 | trivial | Progress log: lines in scaled groups (#233) | diff --git a/crates/engine/src/cmd/file.rs b/crates/engine/src/cmd/file.rs index f6424f7c..7490dae8 100644 --- a/crates/engine/src/cmd/file.rs +++ b/crates/engine/src/cmd/file.rs @@ -8,7 +8,16 @@ use crate::{DocState, EngineError, Result, Session}; pub fn specs() -> Vec { vec![ - cmd!(noundo "file.new", "New Presentation", ["File"], Some("Cmd+N"), "{theme?: name, size?: [w,h] pt, blank?: bool}", always, new), + cmd!( + noundo "file.new", + "New Presentation", + ["File"], + Some("Cmd+N"), + "{theme?: name, template?: starter name, size?: [w,h] pt, blank?: bool}", + always, + new + ), + cmd!(query "file.templates", "Starter Templates", [], None, "{} → [{name, summary, theme, slides}]", always, templates), cmd!(noundo "file.open", "Open…", ["File"], Some("Cmd+O"), "{path}", always, open), cmd!(noundo "file.openBytes", "Open Data", [], None, "{name, data: base64}", always, open_bytes), cmd!(noundo "file.save", "Save", ["File"], Some("Cmd+S"), "{path?, format?: deckcraft|pptx}", has_doc, save), @@ -49,18 +58,51 @@ fn theme_named(name: &str) -> Option { deckcraft_model::theme::builtin_themes().into_iter().find(|t| t.name.eq_ignore_ascii_case(name)) } -fn new(s: &mut Session, p: &Value) -> Result { - let theme = str_param(p, "theme").and_then(theme_named).unwrap_or_default(); - let size = p - .get("size") +fn slide_size(p: &Value) -> deckcraft_geom::Size { + p.get("size") .and_then(Value::as_array) .and_then(|a| Some(deckcraft_geom::Size::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?))) .filter(|s| s.width >= 1.0 && s.height >= 1.0 && s.width < 5000.0 && s.height < 5000.0) - .unwrap_or(defaults::WIDE); - let doc = defaults::blank_presentation(size, theme, !bool_or(p, "blank", false)); + .unwrap_or(defaults::WIDE) +} + +fn templates(_s: &mut Session, _p: &Value) -> Result { + Ok(json!({ + "templates": crate::templates::starters().iter().map(|t| json!({ + "name": t.name, + "summary": t.summary, + "theme": t.theme, + "slides": t.slides.len(), + })).collect::>(), + })) +} + +fn new(s: &mut Session, p: &Value) -> Result { + let template = str_param(p, "template").filter(|t| !t.is_empty()); + let theme_name = str_param(p, "theme").filter(|t| !t.is_empty()); + if template.is_some() && theme_name.is_some() { + return Err(bad("file.new", "pass `theme` or `template`, not both")); + } + if template.is_some() && bool_or(p, "blank", false) { + return Err(bad("file.new", "a template is not a blank presentation")); + } + let size = slide_size(p); + let (doc, template_name) = if let Some(name) = template { + let doc = crate::templates::build(name, size)?; + let chosen = crate::templates::find(name).map(|t| t.name); + (doc, chosen) + } else { + let theme = theme_name.and_then(theme_named).unwrap_or_default(); + (defaults::blank_presentation(size, theme, !bool_or(p, "blank", false)), None) + }; + let slides = doc.slides.len(); let title = s.next_untitled(); let i = s.add_document(DocState::new(doc, None, title)); - Ok(json!({"document": i})) + let mut out = json!({"document": i, "slides": slides}); + if let Some(name) = template_name { + out["template"] = json!(name); + } + Ok(out) } /// Recognise and read presentation bytes (native, PPTX). diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index c2b56c7f..f0c83183 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -15,6 +15,7 @@ pub mod keys; pub mod links; pub mod recovery; pub mod sample; +pub mod templates; pub mod tools; use std::sync::Arc; diff --git a/crates/engine/src/templates.rs b/crates/engine/src/templates.rs new file mode 100644 index 00000000..eda9aa15 --- /dev/null +++ b/crates/engine/src/templates.rs @@ -0,0 +1,337 @@ +//! Original starter presentations for File › New. +//! +//! Each deck is a few slides of our own text on one of our themes. Nothing here is copied from +//! another product's templates. + +use std::sync::Arc; + +use deckcraft_geom::Size; +use deckcraft_model::text::TextBody; +use deckcraft_model::{Presentation, defaults}; +use serde_json::Value; + +use crate::Result; +use crate::cmd::{bad, slide::find_layout}; + +/// One slide of a starter: a layout, a title, text for each non-title placeholder in order, and +/// optional speaker notes. +pub struct SlideSeed { + pub layout: &'static str, + pub title: &'static str, + pub bodies: &'static [&'static str], + pub notes: &'static str, +} + +/// A starter deck shown in the template gallery. +pub struct Starter { + pub name: &'static str, + pub summary: &'static str, + pub theme: &'static str, + pub slides: &'static [SlideSeed], +} + +const fn seed(layout: &'static str, title: &'static str, bodies: &'static [&'static str], notes: &'static str) -> SlideSeed { + SlideSeed { layout, title, bodies, notes } +} + +const STARTERS: &[Starter] = &[ + Starter { + name: "Briefing", + summary: "A weekly check-in: what changed, what's stuck, and who does the next thing", + theme: "Harbor", + slides: &[ + seed("title", "Weekly briefing", &["What changed, what's stuck, and what we do next"], "Open with what changed since last week."), + seed( + "titleAndContent", + "This week's agenda", + &["Shipped since last time\n\tOne concrete change\nStill open\n\tThe decision we need\nNext\n\tWho does it, and by when"], + "", + ), + seed("sectionHeader", "Where we are", &["A short picture before the details"], ""), + seed( + "twoContent", + "Done and next", + &[ + "Done\n\tThe change is in people's hands\n\tWe wrote down what we learned", + "Next\n\tPick the one risk to retire\n\tSay who owns it", + ], + "", + ), + seed("title", "See you next week", &["Bring one number and one question"], ""), + ], + }, + Starter { + name: "Lesson", + summary: "A short class that ends with the learner explaining the idea", + theme: "Meadow", + slides: &[ + seed("title", "How a lock works", &["A short lesson for people new to the idea"], ""), + seed( + "titleAndContent", + "By the end", + &["You can say what a lock protects\n\tAnd what it does not\nYou can sketch one in three steps"], + "", + ), + seed("sectionHeader", "The idea", &["One metaphor, then the real thing"], ""), + seed( + "titleAndContent", + "Three steps", + &["Name the thing being shared\nDecide who may change it\nLeave it so the next person can tell"], + "", + ), + seed("title", "Try it", &["Explain a lock to someone who has never seen one"], ""), + ], + }, + Starter { + name: "Pitch", + summary: "A ten-minute conversation about one promise, not a tour of features", + theme: "Ember", + slides: &[ + seed("title", "A quieter inbox", &["Ten minutes. One promise."], ""), + seed( + "titleAndContent", + "The mess", + &[ + "People miss the one message that mattered\n\tIt arrived between newsletters\nThey spend the morning sorting\n\tAnd still feel behind", + ], + "", + ), + seed("sectionHeader", "A smaller promise", &["Show the one message. Hide the rest until asked."], ""), + seed( + "comparison", + "Today and after", + &[ + "Today", + "The important note is buried\nFinding it takes a search", + "After", + "It sits at the top until you deal with it\nEverything else waits", + ], + "", + ), + seed("title", "The ask", &["Twenty people, two weeks, one question: did the right note surface?"], ""), + ], + }, + Starter { + name: "Field Notes", + summary: "What an outing set out to see, and what it actually found", + theme: "Coral Reef", + slides: &[ + seed("title", "Saturday on the ridge", &["What we went to see, and what we found"], ""), + seed("titleAndContent", "Plan", &["Leave at dawn\n\tCold, clear, little wind\nWalk the west path\nBe back before dusk"], ""), + seed("sectionHeader", "What we saw", &["Notes taken on the spot"], ""), + seed( + "twoContent", + "Expected and found", + &[ + "Expected\n\tA clear view from the ridge\n\tThe creek on the map", + "Found\n\tLow cloud on the ridge\n\tThe creek, and a better pencil", + ], + "", + ), + seed("title", "Pack list for next time", &["Warmer gloves. A second pencil. The map that shows the creek."], ""), + ], + }, + Starter { + name: "Quarterly", + summary: "A report meant to be read aloud in half an hour", + theme: "Paper", + slides: &[ + seed("title", "Spring report", &["Read aloud in thirty minutes"], ""), + seed( + "titleAndContent", + "Headlines", + &["The waiting list got shorter\n\tBy the amount we promised\nTwo things slipped\n\tBoth have a new date"], + "", + ), + seed("sectionHeader", "The numbers", &["Only the ones that change a decision"], ""), + seed("titleAndContent", "What we will stop", &["The report nobody reads\nThe meeting that repeats the report"], ""), + seed("title", "Questions we want", &["Where should the next quarter refuse work?"], ""), + ], + }, + Starter { + name: "Evening Talk", + summary: "A public talk the audience can check on the way home", + theme: "Nocturne", + slides: &[ + seed("title", "Lights out", &["A talk about how cities sleep"], ""), + seed("titleAndContent", "Road map", &["Why the sky looks orange\nWhat a dark street is for\nOne change a block can make"], ""), + seed("sectionHeader", "Look up", &["The part you can check on the way home"], ""), + seed( + "titleAndContent", + "One change", + &["Shield the lamp so light goes down\n\tNot into bedroom windows\nLeave a path you can still walk"], + "", + ), + seed("title", "Walk home", &["Count the lamps you can see from your pillow"], ""), + ], + }, +]; + +/// The starter gallery, in the order File › New shows it. +pub fn starters() -> &'static [Starter] { + STARTERS +} + +/// Starters whose name, summary, theme or a slide title contains `query` (empty matches all). +pub fn matching(query: &str) -> Vec<&'static Starter> { + let q = query.trim().to_lowercase(); + let hit = |text: &str| text.to_lowercase().contains(&q); + starters() + .iter() + .filter(|t| q.is_empty() || hit(t.name) || hit(t.summary) || hit(t.theme) || t.slides.iter().any(|slide| hit(slide.title))) + .collect() +} + +pub fn find(name: &str) -> Option<&'static Starter> { + let name = name.trim(); + starters().iter().find(|t| t.name.eq_ignore_ascii_case(name)) +} + +fn text_with_levels(text: &str) -> TextBody { + let mut body = TextBody::from_text(text); + for para in &mut body.paragraphs { + let tabs = para.runs.first().map(|r| r.text.chars().take_while(|c| *c == '\t').count()).unwrap_or(0); + if tabs > 0 + && let Some(run) = para.runs.first_mut() + { + run.text = run.text.trim_start_matches('\t').to_string(); + para.level = tabs.min(8) as u8; + } + } + body +} + +/// A new presentation filled with `name`'s slides. `size` is the slide size in points. +pub fn build(name: &str, size: Size) -> Result { + let starter = find(name).ok_or_else(|| { + let names = starters().iter().map(|t| t.name).collect::>().join(", "); + bad("file.new", format!("no template `{name}`; use {names}")) + })?; + let theme = deckcraft_model::theme::builtin_themes() + .into_iter() + .find(|t| t.name.eq_ignore_ascii_case(starter.theme)) + .ok_or_else(|| bad("file.new", format!("template `{}` names a missing theme `{}`", starter.name, starter.theme)))?; + let mut doc = defaults::blank_presentation(size, theme, false); + for seed in starter.slides { + let key = Value::String(seed.layout.to_string()); + let layout = find_layout(&doc, &key, 0) + .ok_or_else(|| bad("file.new", format!("template `{}` uses an unknown layout `{}`", starter.name, seed.layout)))?; + let mut slide = defaults::new_slide(&mut doc, layout); + if !seed.title.is_empty() + && let Some(sh) = slide.shapes.iter_mut().find(|sh| sh.ph_type().is_some_and(|k| k.is_title())) + { + sh.text = Some(text_with_levels(seed.title)); + } + let mut bodies = seed.bodies.iter(); + for sh in slide.shapes.iter_mut().filter(|sh| sh.ph_type().is_some_and(|k| !k.is_title())) { + let Some(text) = bodies.next() else { break }; + if !text.is_empty() { + sh.text = Some(text_with_levels(text)); + } + } + if bodies.next().is_some() { + return Err(bad("file.new", format!("template `{}` has more text than the `{}` layout holds", starter.name, seed.layout))); + } + if !seed.notes.is_empty() { + slide.notes = text_with_levels(seed.notes); + } + doc.slides.push(Arc::new(slide)); + } + Ok(doc) +} + +#[cfg(test)] +mod tests { + use serde_json::json; + + use super::*; + use crate::Session; + + fn body(doc: &Presentation, slide: usize, n: usize) -> (String, u8) { + let para_slide = &doc.slides[slide]; + let text = para_slide + .shapes + .iter() + .filter(|sh| sh.ph_type().is_some_and(|k| !k.is_title())) + .nth(n) + .and_then(|sh| sh.text.as_ref()) + .cloned() + .unwrap_or_default(); + let level = text.paragraphs.get(1).map(|p| p.level).unwrap_or(0); + (text.text(), level) + } + + #[test] + fn every_starter_opens_on_its_theme_and_is_a_clean_document() { + let mut s = Session::new(); + for starter in starters() { + s.execute("file.new", &json!({"template": starter.name.to_lowercase()})).unwrap(); + let st = s.active().unwrap(); + assert_eq!(st.doc.masters[0].theme.name, starter.theme, "{}", starter.name); + assert_eq!(st.doc.slides.len(), starter.slides.len(), "{}", starter.name); + assert_eq!(st.doc.slides[0].title(), starter.slides[0].title, "{}", starter.name); + assert!(!st.is_dirty(), "{}", starter.name); + assert!(st.history.undo.is_empty(), "{}", starter.name); + assert_eq!(st.selection.slide, 0); + } + let listed = s.execute("file.templates", &json!({})).unwrap(); + assert_eq!(listed["templates"].as_array().map(Vec::len), Some(starters().len())); + } + + #[test] + fn briefing_keeps_notes_columns_and_outline_levels() { + let mut s = Session::new(); + s.execute("file.new", &json!({"template": "Briefing", "size": [800.0, 450.0]})).unwrap(); + let doc = &s.active().unwrap().doc; + assert_eq!(doc.slide_size.width, 800.0); + assert_eq!(doc.slides[0].notes_text(), "Open with what changed since last week."); + let (agenda, level) = body(doc, 1, 0); + assert!(agenda.contains("One concrete change"), "{agenda}"); + assert_eq!(level, 1); + let (done, _) = body(doc, 3, 0); + let (next, _) = body(doc, 3, 1); + assert!(done.starts_with("Done"), "{done}"); + assert!(next.starts_with("Next"), "{next}"); + } + + #[test] + fn pitch_comparison_fills_both_columns() { + let doc = build("Pitch", defaults::WIDE).unwrap(); + assert_eq!(body(&doc, 3, 0).0, "Today"); + assert!(body(&doc, 3, 1).0.contains("buried")); + assert_eq!(body(&doc, 3, 2).0, "After"); + assert!(body(&doc, 3, 3).0.contains("top")); + } + + #[test] + fn search_matches_name_summary_and_theme() { + assert_eq!(matching("ridge").iter().map(|t| t.name).collect::>(), vec!["Field Notes"]); + assert_eq!(matching("nocturne").iter().map(|t| t.name).collect::>(), vec!["Evening Talk"]); + assert_eq!(matching("").len(), starters().len()); + assert!(matching("facet").is_empty()); + } + + #[test] + fn unknown_blank_and_theme_are_refused() { + let mut s = Session::new(); + let unknown = s.execute("file.new", &json!({"template": "Ion"})).unwrap_err(); + assert!(unknown.to_string().contains("Briefing"), "{unknown}"); + let both = s.execute("file.new", &json!({"template": "Lesson", "theme": "Harbor"})).unwrap_err(); + assert!(both.to_string().contains("not both"), "{both}"); + let blank = s.execute("file.new", &json!({"template": "Lesson", "blank": true})).unwrap_err(); + assert!(blank.to_string().contains("blank"), "{blank}"); + assert!(s.active().is_none()); + } + + #[test] + fn a_starter_round_trips_through_pptx() { + let doc = build("Evening Talk", defaults::WIDE).unwrap(); + let bytes = deckcraft_pptx::export(&doc).unwrap(); + let back = deckcraft_pptx::import(&bytes).unwrap(); + assert_eq!(back.slides.len(), doc.slides.len()); + for (before, after) in doc.slides.iter().zip(back.slides.iter()) { + assert_eq!(after.title(), before.title()); + } + assert!(back.slides[3].shapes.iter().any(|sh| sh.text.as_ref().is_some_and(|t| t.text().contains("bedroom")))); + } +} diff --git a/crates/ui-egui/src/backstage.rs b/crates/ui-egui/src/backstage.rs index f1d0fde3..bc8a1f03 100644 --- a/crates/ui-egui/src/backstage.rs +++ b/crates/ui-egui/src/backstage.rs @@ -497,15 +497,14 @@ fn tile(ui: &mut Ui, label: &str, theme: Option<&deckcraft_model::Theme>, big: b } /// The built-in themes and the sample deck, filtered by `query` (after a blank presentation when -/// `blank`); at most `limit` theme tiles. -fn new_tiles(app: &mut SlideApp, ui: &mut Ui, query: &str, limit: usize, blank: bool) { +/// `blank`); at most `limit` theme tiles. False when nothing matched. +fn new_tiles(app: &mut SlideApp, ui: &mut Ui, query: &str, limit: usize, blank: bool) -> bool { let q = query.trim().to_lowercase(); let themes: Vec = deckcraft_model::theme::builtin_themes().into_iter().filter(|th| q.is_empty() || th.name.to_lowercase().contains(&q)).collect(); let sample = q.is_empty() || "sample deck northern lights".contains(&q); if themes.is_empty() && !sample { - dim(ui, &format!("No themes match “{}”.", query.trim())); - return; + return false; } ui.horizontal_wrapped(|ui| { ui.spacing_mut().item_spacing = vec2(10.0, 10.0); @@ -531,6 +530,25 @@ fn new_tiles(app: &mut SlideApp, ui: &mut Ui, query: &str, limit: usize, blank: } } }); + true +} + +/// Starter decks whose name, summary, theme or a slide title matches `query`, at most `limit` tiles. +fn template_tiles(app: &mut SlideApp, ui: &mut Ui, query: &str, limit: usize) -> bool { + let list = deckcraft_engine::templates::matching(query); + if list.is_empty() { + return false; + } + ui.horizontal_wrapped(|ui| { + ui.spacing_mut().item_spacing = vec2(10.0, 10.0); + for starter in list.into_iter().take(limit) { + let theme = deckcraft_model::theme::builtin_themes().into_iter().find(|th| th.name == starter.theme); + if tile(ui, starter.name, theme.as_ref(), false).on_hover_text(starter.summary).clicked() { + let _ = app.run("file.new", json!({"template": starter.name})); + } + } + }); + true } /// File name and folder of a recent path, split as text (nothing on disk is read). @@ -648,6 +666,17 @@ fn home(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage, out: &mut Outcome) ui.add_space(6.0); new_tiles(app, ui, "", 5, true); ui.add_space(22.0); + ui.horizontal(|ui| { + ui.label(RichText::new("Templates").font(theme::bold(16.0)).color(t.text)); + ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { + if arrow_link(ui, "More templates") { + out.page = Some(Page::New); + } + }); + }); + ui.add_space(6.0); + template_tiles(app, ui, "", 3); + ui.add_space(22.0); ui.horizontal(|ui| { ui.label(RichText::new("Recent").font(theme::bold(16.0)).color(t.text)); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { @@ -668,7 +697,7 @@ fn home(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage, out: &mut Outcome) ui.hyperlink_to("Join the ArtCraft community on Discord", deckcraft_engine::links::DISCORD); } -/// New: a blank presentation, then every theme with a search box. +/// New: a blank presentation, then every theme and starter template, with a search box. fn new_page(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage) { let t = Tokens::get(ui.ctx()); if tile(ui, "Blank Presentation", None, true).clicked() { @@ -679,10 +708,20 @@ fn new_page(app: &mut SlideApp, ui: &mut Ui, bs: &mut Backstage) { let (r, _) = ui.allocate_exact_size(vec2(w, 1.0), Sense::hover()); ui.painter().line_segment([r.left_center(), r.right_center()], Stroke::new(1.0, t.separator)); ui.add_space(14.0); - search(ui, &mut bs.query, "Search for themes", 380.0); + search(ui, &mut bs.query, "Search for themes and templates", 380.0); ui.add_space(14.0); let q = bs.query.clone(); - new_tiles(app, ui, &q, usize::MAX, false); + let themes = new_tiles(app, ui, &q, usize::MAX, false); + let templates = if q.trim().is_empty() || !deckcraft_engine::templates::matching(&q).is_empty() { + ui.add_space(18.0); + heading(ui, "Templates"); + template_tiles(app, ui, &q, usize::MAX) + } else { + false + }; + if !themes && !templates { + dim(ui, &format!("No themes or templates match “{}”.", q.trim())); + } } /// Open: Recent and Browse on the left; the recent presentations (with a search box) on the right. diff --git a/crates/ui-egui/src/backstage_tests.rs b/crates/ui-egui/src/backstage_tests.rs index 115d782f..5fb6a2e3 100644 --- a/crates/ui-egui/src/backstage_tests.rs +++ b/crates/ui-egui/src/backstage_tests.rs @@ -179,6 +179,21 @@ fn every_page_renders() { assert!(h.state_mut().run("app.backstage", json!({"page": "share"})).is_err(), "Share (cloud) isn't offered"); } +/// A starter template from File › New opens its deck and leaves the File tab. +#[test] +fn new_template_opens_a_starter_deck() { + let mut h = harness(new_app()); + h.state_mut().run("app.backstage", json!({"page": "new"})).unwrap(); + h.run_steps(3); + h.get_by_label("Briefing").click(); + h.run_steps(3); + let doc = &h.state().session.active().unwrap().doc; + assert_eq!(doc.masters[0].theme.name, "Harbor"); + assert_eq!(doc.slides[0].title(), "Weekly briefing"); + assert_eq!(doc.slides.len(), 5); + assert!(h.state().backstage.is_none(), "the starter deck shows"); +} + /// New and Home create presentations, which leaves the File tab. #[test] fn new_creates_a_presentation() { diff --git a/docs/gaps.md b/docs/gaps.md index a2894b42..bb95531e 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where DeckCraft falls short of PowerPoint -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (3-D bevel, camera and face lighting rendered; previous: minor, IME composition and CJK interface text landed, #101; previous: minor, core-workflow alpha gate added; stage stays alpha; before that: major, first itemized gap list; full re-measure against PowerPoint for Mac 16.113.4 and the open GitHub issues) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (six original starter decks on File › New; previous: minor, 3-D bevel, camera and face lighting rendered; previous: minor, IME composition and CJK interface text landed, #101) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Every known shortfall, one entry each, ranked by how much it stops someone who presents with PowerPoint from switching. This is the work list: pick from the top. Numbers are summarized in @@ -242,8 +242,11 @@ Detail in [localization-parity.md](localization-parity.md). ## Ecosystem -- **8 original themes, no template gallery.** PowerPoint offers dozens of designs online and - offline. 8–14 h for a larger original set (asset policy: our own designs only). +- **Six original starter decks; PowerPoint's large template gallery remains.** File › New and the + start screen offer Briefing, Lesson, Pitch, Field Notes, Quarterly and Evening Talk + (`file.new {template}`), each a few slides on one of our themes. Dozens of further offline + designs, and any online gallery, are still absent. 4–8 h for a larger original set (asset policy: + our own designs only). - **No add-ins or VBA.** Out of scope by policy; MCP/CLI automation is the alternative. ## AI features @@ -256,6 +259,7 @@ Detail in [localization-parity.md](localization-parity.md). | Date | Change | Summary | |---|---|---| +| 2026-10-14 | minor | File › New has six original starter decks; a large template gallery is still open | | 2026-10-13 | minor | Shape, picture and table rendering checked against PowerPoint on ~150 synthetic edge-case slides; 3-D bevels, cameras and face lighting now render approximately (gap narrowed to extrusion, rigs and the 3-D pane) | | 2026-10-12 | minor | IME composition in slide text and notes, and CJK interface text with real glyphs (#101): the IME gap is now Korean on the web and table-cell underlines | | 2026-10-12 | minor | EMF/WMF text, pattern and hatch brushes drawn; legacy OLE and ActiveX previews shown; the SVG gap keeps only EMF clip regions | diff --git a/docs/roadmap.md b/docs/roadmap.md index 97953034..c7e7b66e 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # DeckCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (core-workflow alpha gate added; stage stays alpha; previous: major, created; milestones carried from the local execution plan, the beta plan built from gaps.md) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (six original starter decks; the large template gallery stays after beta; previous: minor, core-workflow alpha gate added; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Forward-looking: where DeckCraft is going and in what order. The one-page summary is [ROADMAP.md](../ROADMAP.md); every shortfall is in [gaps.md](gaps.md); numbers are in @@ -61,8 +61,8 @@ reference renders, a Windows tester with the affected GPUs, native speakers to r SmartArt editing, chartex chart types, the remaining animation effects and motion paths, Notes and Handout masters, edit points, text warp and 3-D, proofing dictionaries, Zoom, recording, video and GIF export, legacy `.ppt` import and `.odp` (owner decision), GPU rasterization and hardware video -decode, the other 21 languages with RTL mirroring, a larger original template gallery, and local AI -features (owner decision on models). Itemized with hours in [gaps.md](gaps.md). +decode, the other 21 languages with RTL mirroring, a larger template gallery (six original starters +ship), and local AI features (owner decision on models). Itemized with hours in [gaps.md](gaps.md). ## Milestones @@ -109,5 +109,6 @@ plan above. | Date | Change | Summary | |---|---|---| +| 2026-10-14 | minor | After beta: six original starter decks exist; a larger template gallery remains | | 2026-10-10 | minor | Added the alpha gate (six core workflows): all pass, two with partial depth; stage stays alpha | | 2026-10-10 | major | Created: current focus, ranked beta plan with hours, milestones M0–M14 with status, the old alpha checklist as history | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 99653dc7..fc02dacd 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # DeckCraft vs Microsoft PowerPoint: parity assessment -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready recomputed from written weights, 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; replaces the parity sections of ROADMAP.md) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-14 · **Change:** minor (six original starter decks; ecosystem stays ~25% until the gallery is large; previous: minor, hours to ~95% per audience added; ready 56% → 55%) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) The authoritative answer to "how close is DeckCraft to PowerPoint, and how much work is left?". [ROADMAP.md](../ROADMAP.md) summarizes it; [gaps.md](gaps.md) itemizes every shortfall; @@ -72,7 +72,7 @@ The weights are what those users actually depend on, written down so the total c | Hardware | 5% | ~40% | 25–45 | [hardware-parity.md](hardware-parity.md) | GPU-composited window (wgpu), but CPU slide raster, software-only video decode, presenter view not placed on the second display, no pen pressure, no pinch zoom | | Platforms | 5% | ~80% | 5–10 | [ROADMAP.md](../ROADMAP.md) | Ahead in reach: macOS, Windows x64/x86/arm64, Linux (5 formats), FreeBSD and the web, all from one release workflow. Held back by the Windows GPU crashes and Linux/Windows clipboard gaps | | Localization | 5% | ~8% | 55–90 + native review | [localization-parity.md](localization-parity.md) | English only; no string catalog. PowerPoint for Mac ships 26 languages. Bidi and Arabic shaping in slide text exist | -| Ecosystem (templates, add-ins) | 3% | ~25% | 10–20 | [gaps.md](gaps.md) | 8 original themes vs PowerPoint's large template gallery; no add-ins or VBA (out of scope by policy) | +| Ecosystem (templates, add-ins) | 3% | ~25% | 10–20 | [gaps.md](gaps.md) | 8 original themes and 6 original starter decks; PowerPoint's large template gallery remains; no add-ins or VBA (out of scope by policy) | | AI features | 2% | ~10% | 20–40 (owner decision on models) | [gaps.md](gaps.md) | No Designer, Copilot, Speaker Coach, live subtitles or Translate (all Microsoft cloud services). Agents drive every command over MCP instead | | **Total** | 100% | **~55%** (Σ weight × ready = 55.3) | **~365–650** | | | | Automation (✱, not weighted) | | ahead | 0 | [mcp.md](mcp.md), [control-protocol.md](control-protocol.md) | Every command is scriptable from the CLI, a JSON control channel and MCP, headless or against the running app; PowerPoint for Mac has AppleScript and VBA only | @@ -211,7 +211,8 @@ Carried from the 2026-10-07 ROADMAP status and updated with what has landed sinc inheritance (slide → layout → master → theme), sections, notes, comments, custom shows, header/footer fields, embedded media, unlimited undo on shared slide snapshots. - **Themes:** 8 original themes with colour and font schemes, custom colours and fonts, - backgrounds (solid, gradient, picture, pattern), slide size presets. + backgrounds (solid, gradient, picture, pattern), slide size presets. File › New also offers 6 + original starter decks (Briefing, Lesson, Pitch, Field Notes, Quarterly, Evening Talk). - **Shapes:** ~180 preset geometries with adjustment handles, text boxes, pictures (crop, corrections, animated GIFs that play and pause), tables with styles, charts (column, bar, line, area, pie, doughnut, scatter and more), basic SmartArt, groups, connectors glued to sites, @@ -276,6 +277,7 @@ scored done (16 open issues). | Date | Change | Summary | |---|---|---| +| 2026-10-14 | minor | Ecosystem evidence: six original starter decks. The dimension stays ~25% because PowerPoint's large gallery is still absent | | 2026-10-10 | minor | Added the readiness-by-audience table: each number gets its own hours to ~95% (full 340–600, mainstream 150–250, essentials 50–90). The full number stays an additive weighted sum; no change to the percentages | | 2026-10-10 | minor | Added the mainstream practitioner (~53%) and essentials user (~62%) numbers with written weights, discounts and user evidence. Features dimension recomputed from written area weights (66% judgement → 62.4% weighted mean), which moves ready for real work from ~56% to ~55%; stage unchanged | | 2026-10-10 | minor | Stage checked against the core-workflow gate (all six pass); stays alpha | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index c403f05e..898396b0 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -2,7 +2,7 @@ > **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (File tab / Backstage, #76; previous: off-slide objects in Normal view, #270) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) -> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (text editing keys observed in PowerPoint for Windows, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-14 · **Change:** minor (File › New starter decks; previous: minor, text editing keys observed in PowerPoint for Windows, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) How DeckCraft looks and feels next to PowerPoint: the shell, the ribbon, menus, panes, dialogs, direct manipulation, keyboard and precision. Observations of PowerPoint are kept locally in @@ -41,8 +41,8 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`). | Page | PowerPoint | DeckCraft | |---|---|---| -| Home | Prompt box (Copilot), New row of themes with More themes, Recent / Favorites / Shared with Me with a file search, More presentations | DeckCraft's mark, Blank Presentation and the first themes with More themes →, Recent with a file search and More presentations → (to Open). No Copilot prompt, Favorites or Shared with Me | -| New | Blank Presentation, template search with suggested searches, gallery of online templates and themes | Blank Presentation, search box, the built-in themes and the sample deck (`file.new {theme}`). No online templates | +| Home | Prompt box (Copilot), New row of themes with More themes, Recent / Favorites / Shared with Me with a file search, More presentations | DeckCraft's mark, Blank Presentation and the first themes with More themes →, the first starter decks with More templates →, Recent with a file search and More presentations → (to Open). No Copilot prompt, Favorites or Shared with Me | +| New | Blank Presentation, template search with suggested searches, gallery of online templates and themes | Blank Presentation, search box, the built-in themes, six original starter decks (`file.new {template}`) and the sample deck (`file.new {theme}`). No online templates | | Open | Recent, Shared with Me, OneDrive, This PC, Add a Place, Browse; Presentations / Folders; Recover Unsaved Presentations | Recent and Browse (`app.openDialog`), recent presentations with a search box (`file.open`), Recover Unsaved Presentations (`file.recovery.open`). No cloud places or Folders tab | | Info | Name and path, Upload / Share / Copy path / Open file location; Protect, Inspect, Version History, Manage Presentation; Properties, Related Dates, Related People | Name and path, Copy path, Open file location; Inspect Presentation (Accessibility Checker), Manage Presentation (Revert); Size, Slides, Hidden slides and editable Title, Subject, Tags, Categories, Company, Comments, Author (`file.info`, `file.properties`). No Protect, Version History or dates | | Save | Saves; a presentation never saved goes to Save As | Same (`file.save`) | @@ -148,6 +148,7 @@ imitate Microsoft's. No pixel comparison against PowerPoint screenshots has been | Date | Change | Summary | |---|---|---| +| 2026-10-14 | minor | File › Home and New show six original starter decks; online templates are still absent | | 2026-10-12 | minor | File tab (Backstage) observed in PowerPoint for Windows and matched (#76); the start screen is File › Home | | 2026-10-12 | minor | Off-slide objects drawn and editable around the slide in Normal view (#270) | | 2026-10-12 | minor | Selection Pane matched to PowerPoint (#133): drag and drop, arrows, groups, eye, Lock |