Merge PR #672 from NikitkaRa: Review: previous and next comment

This commit is contained in:
Brandon Thomas
2026-10-14 13:09:06 -04:00
9 changed files with 223 additions and 18 deletions
+3
View File
@@ -16,6 +16,7 @@
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: chart trendlines round-trip; previous: minor, hours to ~95% per audience added; ready ~55%; stage stays alpha; full re-measure against PowerPoint for Mac 16.113.4) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: Lucide icon gallery; previous: minor, hours to ~95% per audience added; ready 56% → 55%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Size and Position dialog; breadth stays 79%, ready stays ~55%; previous: minor, hours to ~95% per audience) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (progress log: previous and next comment; breadth stays 79%; previous: minor, hours to ~95%) · **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
@@ -165,6 +166,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-14 | Chart trendlines round-trip (type, polynomial order, moving-average period, forecast, intercept, equation and R²). An owned corpus slice also checks combo charts with data labels, merged tables with diagonal borders, and text columns, autofit, vertical text and a WordArt warp |
| 2026-10-14 | Insert › Icons: a gallery of Lucide stroke icons (release 1.56.0) inserted as custom shapes, no fill, round stroke scaled from the 24-unit design |
| 2026-10-14 | Size and Position dialog: height, width, rotation, scale and lock aspect ratio in one undo step, from Shape Format › Size and the shape menu. Breadth stays 79% |
| 2026-10-14 | Review: Previous and Next comment walk every comment in the deck; the Comments pane lists every slide and highlights the one you landed on |
| 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 |
@@ -205,6 +207,7 @@ app's JSON control channel (`deckcraft --control PORT`).
| 2026-10-14 | trivial | Progress log: chart trendlines |
| 2026-10-14 | trivial | Progress log: Lucide icon gallery |
| 2026-10-14 | trivial | Progress log: Size and Position dialog. Breadth stays 79%, ready stays ~55% |
| 2026-10-14 | trivial | Progress log: previous and next comment |
| 2026-10-13 | trivial | Progress log: wild text cases |
| 2026-10-13 | trivial | Progress log: slide show for interactive decks |
| 2026-10-12 | trivial | Progress log: File tab (Backstage, #76) |
+114 -1
View File
@@ -6,7 +6,7 @@ use deckcraft_model::Comment;
use serde_json::{Value, json};
use super::*;
use crate::{Result, Session, UiRequest};
use crate::{Result, Session, Target, UiRequest};
pub fn specs() -> Vec<CommandSpec> {
vec![
@@ -15,6 +15,24 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!("comment.delete", "Delete Comment", ["Review", "Comments"], None, "{index? | all?: bool}", has_slide, delete),
cmd!("comment.resolve", "Resolve Thread", ["Comments"], None, "{index, resolved?: bool}", has_slide, resolve),
cmd!(query "comment.list", "Comments", [], None, "{slide?} → comments", has_slide, list),
cmd!(
noundo "comment.next",
"Next Comment",
["Review", "Comments"],
None,
"{slide?, index?} → {slide, index, text}",
has_slide,
|s, p| neighbour(s, p, true)
),
cmd!(
noundo "comment.prev",
"Previous Comment",
["Review", "Comments"],
None,
"{slide?, index?} → {slide, index, text}",
has_slide,
|s, p| neighbour(s, p, false)
),
cmd!(query "review.accessibility", "Check Accessibility", ["Review", "Accessibility"], None, "{} → [{slide, shape, issue}]", has_doc, accessibility),
cmd!(query "review.spelling", "Spelling", ["Review", "Proofing"], Some("F7"), "{} → [{slide, shape, word}] (words not in the built-in list)", has_doc, spelling),
cmd!(noundo "show.fromStart", "Play from Start", ["Slide Show", "Start Slide Show"], Some("Cmd+Shift+Enter"), "{}", has_slide, |s, _| start(s, 0)),
@@ -123,6 +141,51 @@ fn list(s: &mut Session, p: &Value) -> Result<Value> {
Ok(serde_json::to_value(st.doc.slides.get(i).map(|x| x.comments.clone()).unwrap_or_default()).unwrap_or_default())
}
/// Flat `(slide, comment index)` list in deck order.
fn comment_threads(doc: &deckcraft_model::Presentation) -> Vec<(usize, usize)> {
let mut out = Vec::new();
for (si, sl) in doc.slides.iter().enumerate() {
for ci in 0..sl.comments.len() {
out.push((si, ci));
}
}
out
}
/// Step to the next or previous comment. With `{slide, index}` matching a thread, move one step
/// and stop at the ends. Without `index`, next is the first thread on or after `slide` (the
/// current slide when omitted) and previous is the last thread strictly before it.
fn neighbour(s: &mut Session, p: &Value, forward: bool) -> Result<Value> {
let cmd = if forward { "comment.next" } else { "comment.prev" };
let (threads, slide, index) = {
let st = s.doc()?;
(comment_threads(&st.doc), usize_param(p, "slide").unwrap_or(st.selection.slide), usize_param(p, "index"))
};
if threads.is_empty() {
return Err(bad(cmd, "no comments"));
}
let pos = index.and_then(|i| threads.iter().position(|&(sl, c)| sl == slide && c == i));
let target = match (pos, forward) {
(Some(i), true) => threads.get(i + 1).copied(),
(Some(i), false) => i.checked_sub(1).and_then(|j| threads.get(j).copied()),
(None, true) => threads.iter().copied().find(|(sl, _)| *sl >= slide),
(None, false) => threads.iter().copied().rev().find(|(sl, _)| *sl < slide),
};
let Some((slide, index)) = target else {
return Err(bad(cmd, "no more comments"));
};
s.select(|doc, sel| {
sel.slide = slide;
sel.slides = doc.slides.get(slide).map(|x| vec![x.id]).unwrap_or_default();
sel.shapes.clear();
sel.text = None;
sel.cells = None;
sel.target = Target::Slides;
})?;
let text = s.doc()?.doc.slides.get(slide).and_then(|sl| sl.comments.get(index)).map(|c| c.text.clone()).unwrap_or_default();
Ok(json!({"slide": slide, "index": index, "text": text}))
}
fn accessibility(s: &mut Session, _p: &Value) -> Result<Value> {
let st = s.doc()?;
let mut out = vec![];
@@ -264,3 +327,53 @@ fn custom_show(s: &mut Session, p: &Value) -> Result<Value> {
})?;
ok()
}
#[cfg(test)]
mod tests {
use serde_json::json;
use crate::Session;
#[test]
fn previous_and_next_comment_walk_the_deck() {
let mut s = Session::with_new();
let err = s.execute("comment.next", &json!({})).unwrap_err();
assert!(err.to_string().contains("no comments"), "{err}");
s.execute("comment.add", &json!({"text": "on first"})).unwrap();
s.execute("shape.insert", &json!({"preset": "rect", "rect": [10.0, 10.0, 40.0, 40.0]})).unwrap();
s.execute("slide.new", &json!({})).unwrap();
s.execute("comment.add", &json!({"text": "on second a"})).unwrap();
s.execute("comment.add", &json!({"text": "on second b"})).unwrap();
s.execute("slide.new", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().selection.slide, 2);
let err = s.execute("comment.next", &json!({})).unwrap_err();
assert!(err.to_string().contains("no more comments"), "{err}");
let undo_before = s.doc().unwrap().history.undo.len();
let back = s.execute("comment.prev", &json!({})).unwrap();
assert_eq!(back["slide"], 1);
assert_eq!(back["index"], 1);
assert_eq!(back["text"], "on second b");
assert_eq!(s.doc().unwrap().selection.slide, 1);
assert!(s.doc().unwrap().selection.shapes.is_empty());
assert_eq!(s.doc().unwrap().history.undo.len(), undo_before);
let prev = s.execute("comment.prev", &json!({"slide": 1, "index": 1})).unwrap();
assert_eq!(prev["index"], 0);
assert_eq!(prev["text"], "on second a");
let first = s.execute("comment.prev", &json!({"slide": prev["slide"], "index": prev["index"]})).unwrap();
assert_eq!(first["slide"], 0);
assert_eq!(first["text"], "on first");
let err = s.execute("comment.prev", &json!({"slide": 0, "index": 0})).unwrap_err();
assert!(err.to_string().contains("no more comments"), "{err}");
let next = s.execute("comment.next", &json!({"slide": 0, "index": 0})).unwrap();
assert_eq!(next["slide"], 1);
assert_eq!(next["text"], "on second a");
s.execute("slide.go", &json!({"index": 0})).unwrap();
let here = s.execute("comment.next", &json!({})).unwrap();
assert_eq!(here["text"], "on first");
}
}
+4
View File
@@ -103,6 +103,9 @@ pub struct UiState {
/// Format Shape pane: tab (`fill`, `effects`, `size`, `picture`, `text`).
pub format_tab: String,
pub grayscale: bool,
/// Comment the Review Previous/Next buttons last landed on: `(slide, index)`.
#[serde(skip_serializing_if = "Option::is_none")]
pub comment_focus: Option<(usize, usize)>,
}
impl Default for UiState {
@@ -128,6 +131,7 @@ impl Default for UiState {
ui_scale: 1.0,
format_tab: "fill".into(),
grayscale: false,
comment_focus: None,
}
}
}
+67 -15
View File
@@ -718,45 +718,97 @@ fn comments(app: &mut SlideApp, ui: &mut Ui) {
app.dialog = Some(crate::dialogs::Dialog::new("comment"));
}
ui.separator();
let Some(st) = app.session.active() else { return };
let list = st.current_slide().map(|s| s.comments.clone()).unwrap_or_default();
if list.is_empty() {
ui.label(egui::RichText::new("No comments on this slide.").color(t.text_dim));
let threads: Vec<(usize, Vec<deckcraft_model::Comment>)> = app
.session
.active()
.map(|st| st.doc.slides.iter().enumerate().filter(|(_, sl)| !sl.comments.is_empty()).map(|(i, sl)| (i, sl.comments.clone())).collect())
.unwrap_or_default();
if threads.is_empty() {
ui.label(egui::RichText::new("No comments.").color(t.text_dim));
return;
}
for (i, c) in list.iter().enumerate() {
egui::Frame::NONE.fill(t.chrome).corner_radius(CornerRadius::same(8)).inner_margin(egui::Margin::same(8)).show(ui, |ui| {
let focus = app.ui.comment_focus;
for (si, list) in threads {
ui.label(egui::RichText::new(format!("Slide {}", si + 1)).font(theme::bold(12.0)).color(t.text_dim));
ui.add_space(2.0);
for (i, c) in list.iter().enumerate() {
comment_card(app, ui, si, i, c, focus == Some((si, i)));
ui.add_space(6.0);
}
}
}
fn focus_comment(app: &mut SlideApp, slide: usize, index: usize) {
let _ = app.run("slide.go", json!({"index": slide}));
app.ui.comment_focus = Some((slide, index));
}
fn comment_card(app: &mut SlideApp, ui: &mut Ui, slide: usize, index: usize, c: &deckcraft_model::Comment, focused: bool) {
let t = Tokens::get(ui.ctx());
let stroke = if focused { Stroke::new(2.0, t.accent) } else { Stroke::NONE };
let shown = egui::Frame::NONE
.fill(if focused { t.selected } else { t.chrome })
.stroke(stroke)
.corner_radius(CornerRadius::same(8))
.inner_margin(egui::Margin::same(8))
.show(ui, |ui| {
ui.set_width(ui.available_width());
ui.horizontal(|ui| {
let head = ui.horizontal(|ui| {
let (r, _) = ui.allocate_exact_size(vec2(24.0, 24.0), Sense::hover());
ui.painter().circle_filled(r.center(), 12.0, t.accent);
ui.painter().text(r.center(), Align2::CENTER_CENTER, &c.initials, theme::bold(11.0), Color32::WHITE);
ui.vertical(|ui| {
ui.label(egui::RichText::new(&c.author).font(theme::bold(12.5)));
ui.label(egui::RichText::new(c.date.replace('T', " ").trim_end_matches('Z')).size(10.5).color(t.text_faint));
let name = ui.add(egui::Label::new(egui::RichText::new(&c.author).font(theme::bold(12.5))).sense(Sense::click()));
if name.clicked() {
focus_comment(app, slide, index);
}
let when = c.date.replace('T', " ").trim_end_matches('Z').to_string();
let when = if c.resolved { format!("{when} · Resolved") } else { when };
ui.label(egui::RichText::new(when).size(10.5).color(t.text_faint));
});
});
ui.label(&c.text);
if head.response.clicked() {
focus_comment(app, slide, index);
}
let body = ui.add(egui::Label::new(&c.text).sense(Sense::click()));
if body.clicked() {
focus_comment(app, slide, index);
}
for r in &c.replies {
ui.label(egui::RichText::new(format!("↳ {}: {}", r.author, r.text)).color(t.text_dim));
}
ui.horizontal(|ui| {
let id = ui.id().with(("reply", i));
let id = ui.id().with(("reply", slide, index));
let mut txt: String = ui.data_mut(|d| d.get_temp(id).unwrap_or_default());
let r = ui.add(egui::TextEdit::singleline(&mut txt).hint_text("Reply…").desired_width(ui.available_width() - 70.0));
if r.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) && !txt.trim().is_empty() {
run(app, "comment.reply", json!({"index": i, "text": txt}));
focus_comment(app, slide, index);
run(app, "comment.reply", json!({"index": index, "text": txt}));
txt.clear();
}
ui.data_mut(|d| d.insert_temp(id, txt));
if ui.small_button(if c.resolved { "Reopen" } else { "Resolve" }).clicked() {
run(app, "comment.resolve", json!({"index": i, "resolved": !c.resolved}));
focus_comment(app, slide, index);
run(app, "comment.resolve", json!({"index": index, "resolved": !c.resolved}));
}
if ui.small_button("🗑").clicked() {
run(app, "comment.delete", json!({"index": i}));
let _ = app.run("slide.go", json!({"index": slide}));
let _ = app.run("comment.delete", json!({"index": index}));
match app.ui.comment_focus {
Some((fs, fi)) if fs == slide && fi == index => app.ui.comment_focus = None,
Some((fs, fi)) if fs == slide && fi > index => app.ui.comment_focus = Some((fs, fi - 1)),
_ => {}
}
}
});
});
ui.add_space(6.0);
if focused {
let key = egui::Id::new("comment-focus-scrolled");
let seen = ui.data(|d| d.get_temp::<(usize, usize)>(key));
if seen != Some((slide, index)) {
ui.scroll_to_rect(shown.response.rect, Some(egui::Align::Center));
ui.data_mut(|d| d.insert_temp(key, (slide, index)));
}
}
}
+21
View File
@@ -1889,6 +1889,23 @@ fn record(app: &mut SlideApp, ui: &mut Ui) {
});
}
fn comment_nav(app: &mut SlideApp, ui: &mut Ui, forward: bool) {
let (icon, label, id) = if forward { (Icon::NextComment, "Next", "comment.next") } else { (Icon::PrevComment, "Previous", "comment.prev") };
if small_button(ui, icon, label, enabled(app, id), false).on_hover_text(tip(id, label)).clicked() {
let params = match app.ui.comment_focus {
Some((slide, index)) if app.session.active().is_some_and(|d| d.selection.slide == slide) => json!({"slide": slide, "index": index}),
_ => json!({}),
};
if let Ok(v) = app.run(id, params)
&& let (Some(slide), Some(index)) = (v.get("slide").and_then(Value::as_u64), v.get("index").and_then(Value::as_u64))
&& let (Ok(slide), Ok(index)) = (usize::try_from(slide), usize::try_from(index))
{
app.ui.comment_focus = Some((slide, index));
app.ui.pane = Some("comments".into());
}
}
}
fn review(app: &mut SlideApp, ui: &mut Ui) {
group(ui, |ui| {
big(app, ui, Icon::Spelling, "Spelling", "app.dialog", json!({"id": "spelling"}));
@@ -1905,6 +1922,10 @@ fn review(app: &mut SlideApp, ui: &mut Ui) {
});
group(ui, |ui| {
big(app, ui, Icon::NewComment, "New\nComment", "app.dialog", json!({"id": "comment"}));
rows(ui, |ui| {
comment_nav(app, ui, false);
comment_nav(app, ui, true);
});
big(app, ui, Icon::DeleteComment, "Delete", "comment.delete", json!({}));
let show = app.ui.pane.as_deref() == Some("comments");
if big_button_ex(ui, Icon::ShowComments, "Show\nComments", true, false, show).0.clicked() {
+4
View File
@@ -1,6 +1,7 @@
# DeckCraft architecture
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Size and Position dialog, 227 commands, 28 dialogs; previous: minor, transition frames) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (228 commands; previous: minor, transition frames) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
How DeckCraft is built today. The binding rules (layering, everything is a command, thin UI, never
break wasm, never crash) are in [AGENTS.md](../AGENTS.md) §3–§5; this document describes what
@@ -22,6 +23,8 @@ nothing below L6 uses egui, eframe, winit or rfd.
| L4 | `pdf` | PDF export through krilla |
| L5 | `engine` | Sessions, documents, selection, undo history on shared slide snapshots, the command registry (227 commands in `cmd/*`), pointer tools, keys, AutoRecover, the panic guard (`guard.rs`) |
| L6 | `ui-egui`, `mcp` | The PowerPoint-style interface (ribbon with 10 tabs + contextual tabs, canvas, thumbnails, panes, 28 dialogs, show and presenter windows, the JSON control channel); the MCP server (JSON-RPC 2.0 over stdio), headless or connected to a running app |
| L5 | `engine` | Sessions, documents, selection, undo history on shared slide snapshots, the command registry (228 commands in `cmd/*`), pointer tools, keys, AutoRecover, the panic guard (`guard.rs`) |
| L6 | `ui-egui`, `mcp` | The PowerPoint-style interface (ribbon with 10 tabs + contextual tabs, canvas, thumbnails, panes, 27 dialogs, show and presenter windows, the JSON control channel); the MCP server (JSON-RPC 2.0 over stdio), headless or connected to a running app |
| apps | `deckcraft` (desktop: eframe/wgpu, cpal audio, logging), `deckcraft-cli` (run, render, convert, mcp, app), `deckcraft-web` (trunk; WebGPU with WebGL2 fallback), `xtask` | Exempt from layering |
The media codecs and containers are clean-room pure Rust, 59k lines, shared in spirit with
@@ -79,5 +82,6 @@ macOS, Windows, Linux, FreeBSD and web artifacts.
| Date | Change | Summary |
|---|---|---|
| 2026-10-14 | minor | Size and Position dialog (`shape.sizePosition`): position, size, rotation, scale and lock aspect in one undo step. 227 commands, 28 dialogs |
| 2026-10-14 | trivial | Command registry: 228 (`comment.next`, `comment.prev`) |
| 2026-10-13 | minor | Transitions: perspective meshes, background/content parts, CPU compositor |
| 2026-10-10 | major | Created from the code on main (d556ff9) and AGENTS.md §4 |
+3
View File
@@ -7,6 +7,7 @@
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (owned PPTX corpus covers text effects, body layout, theme effects, fills, bevels and connectors; previous: minor, core-workflow alpha gate) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (`corpus_media.rs` keeps 3-D models and animation bounce end; real-deck corpus still open) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (chart trendlines round-trip; the real-deck corpus stays open; previous: minor, core-workflow alpha gate added; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Review previous/next comment; previous: minor, core-workflow alpha gate) · **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
@@ -27,6 +28,7 @@ points and lead the beta plan.
| Add business content: tables, charts with their data, simple diagrams | partial (not blocking) | Tables with styles, merge/split, all D or P. Charts insert with the common types and edit data in the Chart Data dialog (P0 partial); SmartArt is basic (pre-drawn shapes) | 12–20 |
| Share the deck: PDF of slides, notes or handouts; images | yes | `file.export` PDF with text layer, links, bookmarks, handouts 1–9; PNG/JPEG. Artwork is raster in the PDF; no direct print (print the PDF) | 10–16 |
| Review: comments with replies, resolve, spelling | partial (not blocking) | Comments, replies and resolve work and survive `.deckcraft` and `.pptx` (#70); spelling is a small built-in list | 4–8 |
| Review: comments with replies, resolve, spelling | partial (not blocking) | Comments, replies and resolve work and survive `.deckcraft`; Previous and Next walk every slide and the pane lists them; threads are flattened on `.pptx` save (#70); spelling is a small built-in list | 4–8 |
## Current focus (2026-10-10)
@@ -139,5 +141,6 @@ plan above.
| 2026-10-13 | trivial | Owned PPTX corpus covers text effects, body layout, theme effects, fills, bevels and connectors. Beta item 2 stays open for a larger corpus, embedded fonts and #70/#74/#77 |
| 2026-10-14 | trivial | Current focus: `corpus_media.rs` keeps 3-D models and animation bounce end; the real-deck corpus stays open |
| 2026-10-14 | trivial | Chart trendlines round-trip in an owned corpus slice; the real-deck corpus (item 2) stays open |
| 2026-10-14 | trivial | Review workflow: previous and next comment across the deck |
| 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 |
+4 -1
View File
@@ -3,6 +3,7 @@
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (text-warp shortfall is adjust handles; the Transform gallery applies a preset) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (media remainder no longer lists icons: Insert › Icons is a Lucide stroke set; percentages unchanged; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (227 commands; breadth stays 79%, ready stays ~55%; previous: minor, hours to ~95% per audience) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (228 commands; previous/next comment; breadth stays 79%; previous: minor, hours to ~95%) · **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;
@@ -234,7 +235,7 @@ Carried from the 2026-10-07 ROADMAP status and updated with what has landed sinc
actions followed on click.
- **Views:** Normal, Outline, Slide Sorter, Notes Page, Reading View, Slide Master; zoom;
grayscale.
- **Review:** comments with replies and resolve, accessibility checker, a basic spelling list.
- **Review:** comments with replies and resolve, previous and next across the deck, accessibility checker, a basic spelling list.
- **Media:** MP3, AAC/M4A, ALAC, WAV, AIFF, CAF, FLAC, Ogg Vorbis and Opus audio; H.264, HEVC,
VP9 and AV1 video in MP4/MOV/WebM/MKV with pure-Rust decoders; poster frames, trim, fades,
volume, loop, play across slides.
@@ -242,6 +243,7 @@ Carried from the 2026-10-07 ROADMAP status and updated with what has landed sinc
export (slides, notes, handouts 1–9 per page, text layer, links, bookmarks), PNG/JPEG, outline
text, AutoRecover.
- **Automation:** 227 commands, every one reachable from `deckcraft-cli`, the JSON control channel
- **Automation:** 228 commands, every one reachable from `deckcraft-cli`, the JSON control channel
and MCP.
- **Platforms:** signed releases for macOS (universal), Windows x64/x86/arm64, Linux
(AppImage/deb/rpm/tar.gz/Flatpak, x86_64 and aarch64), FreeBSD and the web (v0.4.0, 2026-10-09).
@@ -281,6 +283,7 @@ scored done (16 open issues).
| 2026-10-13 | minor | Shapes shortfall "text warp" narrowed to adjust handles: the Transform gallery applies a preset |
| 2026-10-14 | minor | Media remainder: Insert › Icons places Lucide stroke icons. 3-D models and stock content remain. Headline percentages unchanged; the checklist row stays missing |
| 2026-10-14 | trivial | Size and Position dialog. Command count 226 → 227. Breadth stays 79%, ready stays ~55%, UI stays ~60% |
| 2026-10-14 | trivial | 228 commands; Review lists previous and next comment. Breadth stays 79%; the comments checklist row stays D |
| 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 |
+3 -1
View File
@@ -6,6 +6,7 @@
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (WordArt Transform gallery; previous: 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-14 · **Last updated:** 2026-10-14 · **Change:** minor (Insert › Icons is a Lucide stroke gallery; ribbon icons stay original drawings; previous: File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (Size and Position dialog; previous: text editing keys, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** trivial (Comments pane lists every slide; Review Previous/Next; previous: File tab / Backstage, #76) · **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)
@@ -69,7 +70,7 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`).
| Format Background pane | yes | yes |
| Selection Pane | front-most first, groups fold, drag to restack, Bring Forward / Send Backward arrows, Show All / Hide All, eye, padlock (Lock), rename in place | same: drag restacks among siblings (a child stays in its group), Ctrl+click toggles, Shift+click selects a run, arrows / Home / End / F2 / Esc in the list, right-click menu |
| Animation Pane | list + advanced timeline | list, reorder, timing; no timeline |
| Comments pane (threads) | yes | yes |
| Comments pane (threads) | yes | yes: every slide, with Previous and Next on the Review tab |
| Design Ideas (Designer) | yes (cloud) | no |
| Dialogs | ~60 (Font, Paragraph, Bullets and Numbering, Find, Replace, Replace Fonts, Hyperlink, Action Settings, Header and Footer, Slide Size, Set Up Show, Custom Shows, Print, Page Setup, Insert Object, Symbol, Equation, Chart data sheet, SmartArt chooser, Preferences…) | 28 (`crates/ui-egui/src/dialogs.rs`): Table, Slide Size, Size and Position, Header and Footer, Hyperlink, Set Up Show, Zoom, Comment, Alt Text, Outline, Rename (section, shape, layout), Paragraph, About, Preferences, Export, Symbol, Equation (placeholder), Spelling, Accessibility, Language, Custom Shows, Chart Data, Trim Media, SmartArt chooser, Quit, Notes Pages |
@@ -164,6 +165,7 @@ on the slide as shapes. No pixel comparison against PowerPoint screenshots has b
| 2026-10-13 | minor | WordArt Transform gallery on Shape Format ▸ Text Effects; `format.textWarp` applies a preset or clears it. Adjust handles remain open |
| 2026-10-14 | minor | Insert › Icons is a searchable gallery of Lucide stroke icons inserted as shapes. Ribbon chrome stays original drawings |
| 2026-10-14 | minor | Size and Position dialog on Shape Format › Size and the shape context menu. Breadth stays 79%; UI ready stays ~60% |
| 2026-10-14 | trivial | Comments pane lists every slide; Review ▸ Previous and Next walk the deck |
| 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 |