Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
5.9 KiB
Crate: crates/ui-egui (notescraft-ui-egui, layer L5) plus a tiny apps/notescraft/src/main.rs. Part 1 of 2 of the first UI: the app SHELL. Another agent builds note_view.rs, palette.rs and backlinks_panel.rs in the same crate at the same time: do NOT create or edit those files and do not touch their modules; you own: src/shell.rs, src/layout.rs, src/tree.rs, src/tabs.rs, src/statusbar.rs, src/style.rs, src/state.rs, src/app.rs, and apps/notescraft/src/main.rs. lib.rs is shared: only add your own pub mod ...; lines (one per line, alphabetical block of your modules) and never remove others' lines.
READ FIRST: SPEC.md sections 8, 8.1, 8.2, 11; docs/ARCHITECTURE.md; crates/ui-egui/src/theme.rs (the Tokens/color/PANEL_RADIUS/PANEL_GAP contract: use it, do not redefine); crates/engine/src (Session, Registry, core commands); ~/.cargo/git/checkouts/craft-libs-*/7fcee66/crates/ui/README.md and src (craft-ui: tabs, layout, docking, icons, buttons: reuse them where they fit, do not reinvent). Reference for style: /Volumes/Storage/Projects/photocraft/crates/ui-egui/src (read-only). Design reference: PaperCraft project NotesCraft screens "Workspace / macOS" (rounded floating panels on the canvas colour: sidebar 260 pt, editor card fluid, right panel 300 pt; 16 pt radius, 8 pt gaps, tone-separated panels, no borders; tab bar 44 pt tall; status bar with branch, words, chars and approx tokens).
IMPORTANT build rules: edit only the files above. Use cargo test -p notescraft-ui-egui and cargo clippy -p notescraft-ui-egui --all-targets -- -D warnings. Do NOT build the notescraft app crate or eframe (a full eframe build needs gigabytes of disk and would break the machine): the app crate is compiled later by the orchestrator. Write apps/notescraft/src/main.rs carefully without compiling it (see item 7). Build in SMALL steps: one file per tool call (<=250 lines), build + clippy after each file, every module declared in lib.rs. Never use unwrap/expect/panic outside tests; no indexing; user strings in one src/strings.rs-style constants block per module (they move to the i18n crate later).
Implement (all pure egui, no eframe, no wgpu):
style.rs:apply(ctx: &egui::Context, tokens: &Tokens)setsVisuals(dark/light from the theme variant), widget colours, selection, hyperlink colour, window/panel fills, rounding from tokens (no borders: strokesStroke::NONE), spacing (8 pt scale), and text styles on the 8-step scale (12, 14, 16, 20, 24, 32). Fonts: egui defaults for now.layout.rs:shell(ctx, tokens, |ui_parts| ...): paints the windowcanvascolour and lays out three rounded cards (left 260, centre fluid, right 300, widths clamped for narrow windows: below 900 pt the right panel hides, below 640 the sidebar hides) withPANEL_GAPgaps andPANEL_RADIUScorners, each filled with its token colour (surface / background / surface). Expose aPanelsclosure struct{ sidebar, editor, right }ofFnOnce(&mut egui::Ui)so callers fill them. Pure function ofctx, tokens and the closures.tree.rs: file-tree widget over a plainTreeModelbuilt fromSession::list-style data (from_paths(&[String]) -> TreeModel, nested folders, folders first then notes alphabetically case-insensitive,.mdshown without extension, collapse state in aBTreeSet<String>), rows 32 pt tall with 16 pt indentation, hover/selected states from tokens, returnsTreeResponse { opened: Option<String>, toggled: Option<String>, context: Option<String> }.tabs.rs: tab bar stateTabs { open: Vec<String>, active: usize }withopen(path),close(i),activate(i), dedupe, cap 32 tabs; rendering with craft-ui tabs if it fits else egui; 44 pt tall, active tab onsurface_2; middle/click-close glyph; returnsTabsResponse.statusbar.rs:StatusInfo { branch: Option<String>, words, chars, tokens }andshow(ui, tokens, &StatusInfo);count(text) -> StatusInfo(words by Unicode whitespace, chars bychar, tokens = chars/4 rounded up).state.rs+app.rs:UiState(serde: sidebar/right widths, collapsed folders, open tabs, active tab, theme id) withto_json/from_json(corrupt JSON gives defaults, never panics);NotesAppholdingSession,UiState,Tokens,Tabs,TreeModel:NotesApp::new(session, theme),ui(&mut self, ctx: &egui::Context)drawing the shell with sidebar tree + tab bar + a placeholder editor area (aui.labelof the active note's raw text; the other agent'snote_viewplugs in later via a single function pointer fieldnote_view: Option<fn(&mut egui::Ui, &Tokens, &str)>), and the status bar; clicking a tree row opens a tab and loads text withSession::read_note; errors from the session shown as a dismissible toast text, never panics.apps/notescraft/src/main.rs: parse one CLI arg (vault path; if missing, use theNOTESCRAFT_VAULTenv var, else print usage to stderr and exit code 2),Session::open, themecatppuccin-mochavianotescraft_themes::builtin, theneframe::run_nativewith a smalleframe::Appadapter callingNotesApp::ui(title "NotesCraft", initial size 1440x900, min 640x480). Keep it under 80 lines, no panics (return aResultfrommainviastd::process::ExitCode).
Tests (headless: create egui::Context::default() with default_fonts (dev-dependency feature already enabled), call ctx.run(RawInput { screen_rect: Some(...) , ..Default::default() }, |ctx| ...) for 2 frames and assert no panic and that shapes were produced): style apply for both theme variants; layout at 1440x900, 800x600 (right panel hidden) and 500x400 (only the editor); tree model ordering, nesting, collapse; tabs open/dedupe/close/cap; status counts (unicode, empty); state JSON round trip and corrupt JSON defaults; NotesApp frame run over a temp vault with 5 notes, clicking is simulated by calling the app's open_note(path) method; hostile inputs (10 000 paths, 100-level deep folder path, path with control characters).