Browse / github.com/storytold/lightcraft / patch
LR-BEHAV-DIALOGS: a one-line prompt that's refused stays open and says why
LR-BEHAV-DIALOGS: a one-line prompt that's refused stays open and says why Off `main`. ## Problem In Folders, right-click a folder, choose **New Folder on Disk…**, type a name that is already taken, and press Create: the dialog closes and nothing says why. The same happens with every one-line prompt, because a refused `Dialog::TextPrompt` falls to the `_ => close = true` arm of the dialog's confirm handling, so the person loses what they typed and never sees the engine's reason. A New Folder on Disk that does work says nothing either, so the person can't tell the folder was made (it holds no photos yet, so Folders doesn't list it). ## Change - A one-line prompt w
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- nledford (imported from pull request #3167, open)
- Base
3a3e49af53aeff55248419234a05d1a262094092- Size
- +160 −4 in 25 files
- Review
- pending
- Shared
- 2026-10-28
- Id
68e32944b5ffbfe428f6dfb7
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/locales/cs-formats.json crates/ui-egui/locales/de-formats.json crates/ui-egui/locales/es-formats.json crates/ui-egui/locales/fi-formats.json crates/ui-egui/locales/fr-formats.json crates/ui-egui/locales/id-formats.json crates/ui-egui/locales/it-formats.json crates/ui-egui/locales/ja-formats.json crates/ui-egui/locales/ka-formats.json crates/ui-egui/locales/pl-formats.json crates/ui-egui/locales/pt-br-formats.json crates/ui-egui/locales/ru-formats.json crates/ui-egui/locales/sv-formats.json crates/ui-egui/locales/uk-formats.json crates/ui-egui/locales/vi-formats.json crates/ui-egui/locales/zh-hans-formats.json crates/ui-egui/locales/zh-hant-formats.json crates/ui-egui/src/control.rs crates/ui-egui/src/dialogs.rs crates/ui-egui/src/panels/left.rs crates/ui-egui/src/panels/left/folders.rs crates/ui-egui/src/tests.rs crates/ui-egui/src/tests/text_prompt.rs docs/parity-checklist.md docs/whats-new.md
@@ -108,6 +108,7 @@ "Moved; {} photo(s) relinked": "Přesunuto; znovu propojeno fotografií: {}", "Named ({named_n})": "Pojmenované ({named_n})", "New Folder in “{name}”": "Nová složka v „{name}“",+ "Created folder “{name}” in “{parent}”": "Vytvořena složka „{name}“ v „{parent}“", "On {have} of {of} selected photos": "Na {have} z {of} vybraných fotografií", "Painting “{k}”: click photos to add or remove it · Esc stops": "Nanášení „{k}“: kliknutím na fotografie přidáte nebo odeberete slovo · Esc ukončí režim", "Paste into {n} photo{} — only settings that were copied are pasted": "Vložit do fotografií: {n}{:.0} — vloží se pouze zkopírovaná nastavení",@@ -248,6 +248,7 @@ "{what} merge cancelled": "{what}-Zusammenfügung abgebrochen", "{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Übersprungen: {n} lokale Fotos{0:.0} – Alben enthalten Bibliotheksfotos (zuerst zu „Meine Fotos“ hinzufügen)", "New Folder in “{name}”": "Neuer Ordner in „{name}“",+ "Created folder “{name}” in “{parent}”": "Ordner „{name}“ in „{parent}“ erstellt", "Moved {} photo(s)": "{} Foto(s) verschoben", "not added to Apple Photos: {e}": "nicht zu Apple Fotos hinzugefügt: {e}", "{n} added to Apple Photos album “{album}”": "{n} zum Apple-Fotos-Album „{album}“ hinzugefügt",@@ -248,6 +248,7 @@ "{what} merge cancelled": "Combinación {what} cancelada", "{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Fotos locales omitidas: {n}{0:.0} — los álbumes contienen fotos de la biblioteca (añádelas primero a Mis fotos)", "New Folder in “{name}”": "Nueva carpeta en «{name}»",+ "Created folder “{name}” in “{parent}”": "Carpeta «{name}» creada en «{parent}»", "Moved {} photo(s)": "{} foto(s) movida(s)", "not added to Apple Photos: {e}": "no se añadieron a Fotos de Apple: {e}", "{n} added to Apple Photos album “{album}”": "{n} añadidas al álbum “{album}” de Fotos de Apple",@@ -134,6 +134,7 @@ "Moved; {} photo(s) relinked": "Siirretty; yhdistetty uudelleen kuvia: {}", "Named ({named_n})": "Nimetyt ({named_n})", "New Folder in “{name}”": "Uusi kansio kohteessa ”{name}”",+ "Created folder “{name}” in “{parent}”": "Kansio ”{name}” luotu kohteeseen ”{parent}”", "On {have} of {of} selected photos": "{have} / {of} valitulla kuvalla", "Painting “{k}”: click photos to add or remove it · Esc stops": "Maalataan ”{k}”: napsauta kuvia lisätäksesi tai poistaaksesi sen · Esc lopettaa", "Paste into {n} photo{} — only settings that were copied are pasted": "Liitä kuviin: {n}{:.0} — vain kopioidut asetukset liitetään",@@ -248,6 +248,7 @@ "{what} merge cancelled": "Fusion {what} annulée", "{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Photos locales ignorées : {n}{0:.0} — les albums contiennent des photos de la bibliothèque (ajoutez-les d’abord à Mes photos)", "New Folder in “{name}”": "Nouveau dossier dans « {name} »",+ "Created folder “{name}” in “{parent}”": "Dossier « {name} » créé dans « {parent} »", "Moved {} photo(s)": "{} photo(s) déplacée(s)", "not added to Apple Photos: {e}": "non ajouté à Apple Photos : {e}", "{n} added to Apple Photos album “{album}”": "{n} ajouté(s) à l'album Apple Photos « {album} »",@@ -251,6 +251,7 @@ "{what} merge cancelled": "Penggabungan {what} dibatalkan", "{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "{n} foto Lokal{:.0} dilewati: album hanya dapat berisi foto pustaka (tambahkan ke Foto saya terlebih dahulu)", "New Folder in “{name}”": "Folder baru di “{name}”",+ "Created folder “{name}” in “{parent}”": "Folder “{name}” dibuat di “{parent}”", "Moved {} photo(s)": "{} foto dipindahkan", "not added to Apple Photos: {e}": "tidak ditambahkan ke Apple Photos: {e}", "{n} added to Apple Photos album “{album}”": "{n} ditambahkan ke album Apple Photos “{album}”",@@ -240,6 +240,7 @@ "{what} merge cancelled": "Unione {what} annullata", "{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Foto locali saltate: {n}{0:.0}; gli album contengono foto della libreria (prima aggiungile a Le mie foto)", "New Folder in “{name}”": "Nuova cartella in “{name}”",+ "Created folder “{name}” in “{parent}”": "Cartella “{name}” creata in “{parent}”", "Moved {} photo(s)": "Foto spostate: {}", "not added to Apple Photos: {e}": "non aggiunte a Foto di Apple: {e}", "{n} added to Apple Photos album “{album}”": "Aggiunte all'album “{album}” di Foto di Apple: {n}",@@ -252,6 +252,7 @@ "{what} merge cancelled": "{what}合成をキャンセルしました", "{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "ローカル写真{n}枚をスキップしました{0:.0}:アルバムに入れられるのはライブラリの写真です(先にマイフォトに追加してください)", "New Folder in “{name}”": "「{name}」に新規フォルダ",+ "Created folder “{name}” in “{parent}”": "「{parent}」にフォルダ「{name}」を作成しました", "Moved {} photo(s)": "{} 枚の写真を移動しました", "not added to Apple Photos: {e}": "Appleの写真に追加されませんでした:{e}", "{n} added to Apple Photos album “{album}”": "{n}件をAppleの写真のアルバム「{album}」に追加しました",@@ -108,6 +108,7 @@ "Moved; {} photo(s) relinked": "გადატანილია; ხელახლა დაკავშირდა ფოტო: {}", "Named ({named_n})": "სახელიანი ({named_n})", "New Folder in “{name}”": "ახალი საქაღალდე „{name}“-ში",+ "Created folder “{name}” in “{parent}”": "საქაღალდე „{name}“ შეიქმნა „{parent}“-ში", "On {have} of {of} selected photos": "{of} მონიშნული ფოტოდან {have}-ზე", "Painting “{k}”: click photos to add or remove it · Esc stops": "„{k}“-ის მინიჭება: დაწკაპეთ ფოტოებზე მის დასამატებლად ან მოსახსნელად · Esc ასრულებს", "Paste into {n} photo{} — only settings that were copied are pasted": "ჩასმა ფოტოებში: {n}{:.0} — ჩაისმება მხოლოდ კოპირებული პარამეტრები",@@ -236,6 +236,7 @@ "Lightroom geometry was not preserved for {path}; {mode} will be recalculated.": "Geometria z Lightrooma dla {path} nie została zachowana; tryb {mode} zostanie obliczony ponownie.", "Moved {} photo(s)": "Przeniesione zdjęcia: {}", "New Folder in “{name}”": "Nowy folder w „{name}”",+ "Created folder “{name}” in “{parent}”": "Utworzono folder „{name}” w „{parent}”", "Restored {} photo{} from Recently Deleted": "Przywrócone z Ostatnio usuniętych zdjęcia: {}{:.0}", "XMP adjustments were not imported for {path}: {fields}.": "Korekty XMP dla {path} nie zostały zaimportowane: {fields}.", "XMP profile \"{profile}\" was not applied for {path}; colors may differ from Lightroom.": "Profil XMP „{profile}” nie został zastosowany do {path}; kolory mogą się różnić od Lightrooma.",@@ -252,6 +252,7 @@ "{what} merge cancelled": "{what} mesclagem cancelada", "{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Fotos locais ignoradas: {n}{0:.0} — álbuns contêm fotos da biblioteca (adicione-as às Minhas fotos primeiro)", "New Folder in “{name}”": "Nova pasta em “{name}”",+ "Created folder “{name}” in “{parent}”": "Pasta “{name}” criada em “{parent}”", "Moved {} photo(s)": "{} foto(s) movida(s)", "not added to Apple Photos: {e}": "não adicionadas ao Fotos da Apple: {e}", "{n} added to Apple Photos album “{album}”": "{n} adicionadas ao álbum “{album}” do Fotos da Apple",@@ -248,6 +248,7 @@ "{what} merge cancelled": "{what}: слияние отменено", "{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Пропущено локальных фото: {n}{0:.0} — в альбомах только фото из библиотеки (сначала добавьте их в «Мои фото»)", "New Folder in “{name}”": "Новая папка в «{name}»",+ "Created folder “{name}” in “{parent}”": "Папка «{name}» создана в «{parent}»", "Moved {} photo(s)": "Перемещено фото: {}", "not added to Apple Photos: {e}": "не добавлено в Apple Фото: {e}", "{n} added to Apple Photos album “{album}”": "Добавлено в альбом «{album}» Apple Фото: {n}",@@ -127,6 +127,7 @@ "Moved; {} photo(s) relinked": "Flyttat; {} foto(n) har länkats om", "Named ({named_n})": "Namngivna ({named_n})", "New Folder in “{name}”": "Ny mapp i ”{name}”",+ "Created folder “{name}” in “{parent}”": "Mappen ”{name}” skapad i ”{parent}”", "new, inside {parent}": "ny, inuti {parent}", "none (CPU rendering): {why}": "ingen (CPU-rendering): {why}", "not added to Apple Photos: {e}": "inte tillagt i Apple Photos: {e}",@@ -248,6 +248,7 @@ "{what} merge cancelled": "Об’єднання {what} скасовано", "{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "Пропущено локальних фото: {n}{0:.0} — в альбомах лише фото з бібліотеки (спершу додайте їх до моїх фото)", "New Folder in “{name}”": "Нова тека в «{name}»",+ "Created folder “{name}” in “{parent}”": "Теку «{name}» створено в «{parent}»", "Moved {} photo(s)": "Переміщено фото: {}", "not added to Apple Photos: {e}": "не додано до Apple Фото: {e}", "{n} added to Apple Photos album “{album}”": "Додано до альбому Apple Фото «{album}»: {n}",@@ -134,6 +134,7 @@ "Moved; {} photo(s) relinked": "Đã di chuyển; liên kết lại {} ảnh", "Named ({named_n})": "Có tên ({named_n})", "New Folder in “{name}”": "Thư mục mới trong «{name}»",+ "Created folder “{name}” in “{parent}”": "Đã tạo thư mục «{name}» trong «{parent}»", "On {have} of {of} selected photos": "Trên {have} / {of} ảnh đã chọn", "Painting “{k}”: click photos to add or remove it · Esc stops": "Gán «{k}»: nhấp ảnh để thêm hoặc gỡ · Esc dừng", "Paste into {n} photo{} — only settings that were copied are pasted": "Dán vào {n} ảnh{:.0} — chỉ dán các thiết lập đã sao chép",@@ -252,6 +252,7 @@ "{what} merge cancelled": "已取消 {what} 合并", "{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "已跳过 {n} 张本地照片{0:.0}:相册只能收录图库中的照片(请先添加到我的照片)", "New Folder in “{name}”": "在“{name}”中新建文件夹",+ "Created folder “{name}” in “{parent}”": "已在“{parent}”中创建文件夹“{name}”", "Moved {} photo(s)": "已移动 {} 张照片", "not added to Apple Photos: {e}": "未添加到 Apple 照片:{e}", "{n} added to Apple Photos album “{album}”": "已将 {n} 个添加到 Apple 照片相册“{album}”",@@ -252,6 +252,7 @@ "{what} merge cancelled": "已取消{what}合併", "{n} Local photo{} skipped: albums hold library photos (Add to My Photos first)": "已略過 {n} 張本機照片{0:.0}:相簿只能收錄照片庫中的照片(請先加入我的照片)", "New Folder in “{name}”": "在「{name}」中新增資料夾",+ "Created folder “{name}” in “{parent}”": "已在「{parent}」中建立資料夾「{name}」", "Moved {} photo(s)": "已移動 {} 張照片", "not added to Apple Photos: {e}": "未加入 Apple 照片:{e}", "{n} added to Apple Photos album “{album}”": "已將 {n} 個加入 Apple 照片相簿「{album}」",@@ -414,7 +414,7 @@ pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context, req: &ControlRequest "ui.dialog.confirm" => match app.ui.dialog.take() { Some(d) => { let r = crate::panels::dialogs::confirm_dialog(app, &d);- // the import review and the rule editor stay open on an error, as with their buttons+ // the import review, the rule editor and the one-line prompts stay open on an error, as with their buttons if (r.is_err() && matches!( d,@@ -423,6 +423,7 @@ pub fn handle(app: &mut LightcraftApp, ctx: &egui::Context, req: &ControlRequest | crate::state::Dialog::SmartRules { .. } | crate::state::Dialog::SaveEffect { .. } | crate::state::Dialog::CaptureTime { .. }+ | crate::state::Dialog::TextPrompt { .. } )) || (r.is_ok() && crate::panels::dialogs::keeps_open(app, &d)) || (r.is_err() && crate::lightroom_destination::keeps_open_on_error(app, &d))@@ -1440,6 +1440,16 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) { *error = Some(e); } }+ // a one-line prompt that is refused stays open with its text, so the name can be fixed+ Err(e) if matches!(dlg, Dialog::TextPrompt { .. }) => app.toast_error(ctx, e),+ // a folder made on disk says so (the new folder holds no photos yet)+ Ok(r) if matches!(&dlg, Dialog::TextPrompt { command, .. } if command == "folder.create") => {+ if let Dialog::TextPrompt { params, .. } = &dlg {+ let (path, parent) = (r["path"].as_str().unwrap_or_default(), params["parent"].as_str().unwrap_or_default());+ app.toast(ctx, crate::panels::left::created_text(path, parent));+ }+ close = true;+ } Ok(r) if matches!(dlg, Dialog::NewAlbum { .. }) && r.get("skipped").and_then(serde_json::Value::as_u64).is_some_and(|n| n > 0) => { // albums hold library photos (issue #684): say which part of the selection stayed out crate::panels::grid::toast_skipped(app, ctx, &r);@@ -23,7 +23,7 @@ pub use albums::{album_drag_feedback, auto_scroll_speed}; use albums::{albums_rows, drag_auto_scroll}; pub(crate) use dates::browse_all_photos; use dates::date_rows;-pub(crate) use folders::{folder_row_of, move_photos, moved_text};+pub(crate) use folders::{created_text, folder_row_of, move_photos, moved_text}; use folders::{folder_rows, reveal_chosen}; use keywords::keyword_rows; pub(crate) use local::{fs_cached, fs_cached_running, same_folder};@@ -482,6 +482,13 @@ pub(crate) fn moved_text(n: u64, name: &str) -> String { } } +/// What `folder.create` of the folder at `path` inside `parent` says when it succeeds: the folder+/// is on disk now, but it has no photos, so the Folders list may not show it yet.+pub(crate) fn created_text(path: &str, parent: &str) -> String {+ let name = std::path::Path::new(path).file_name().map_or_else(|| path.to_string(), |f| f.to_string_lossy().to_string());+ crate::i18n::tr_format!("Created folder “{name}” in “{parent}”", name = name, parent = lightcraft_catalog::folders::folder_label(parent))+}+ /// [`move_photos`], saying how it went in a toast. fn move_photos_here(app: &mut LightcraftApp, ctx: &egui::Context, ids: Option<&[u64]>, path: &str, name: &str) { match move_photos(app, ids, path) {@@ -75,6 +75,7 @@ mod switch_library; mod sync; mod tallies; mod temp_slider;+mod text_prompt; mod titlebar; mod tool_overlay; mod unsaved;new file mode 100644@@ -0,0 +1,111 @@+//! A one-line prompt that the engine refuses stays open with its text and says why, so the name can+//! be fixed; a folder made on disk says it was made.++use std::path::PathBuf;+use std::time::Duration;++use serde_json::json;++use crate::headless::Headless;+use crate::state::Dialog;+use crate::{LightcraftApp, Services};++const T: Duration = Duration::from_secs(20);+const SETTLE: Duration = Duration::from_secs(60);+const CREATED: &str = "Created folder “{name}” in “{parent}”";++/// A fresh `Shoot` folder holding `Taken` and `Other`, in a temporary folder of its own.+fn shoot(tag: &str) -> PathBuf {+ let root = std::env::temp_dir().join(format!("lc-text-prompt-{tag}-{}", std::process::id()));+ let _ = std::fs::remove_dir_all(&root);+ let shoot = root.join("Shoot");+ for name in ["Taken", "Other"] {+ std::fs::create_dir_all(shoot.join(name)).unwrap();+ }+ shoot+}++/// Remove the temporary folder `shoot` made, which holds `dir`.+fn clean(dir: &std::path::Path) {+ let _ = std::fs::remove_dir_all(dir.parent().unwrap());+}++fn app() -> Headless {+ let session = lightcraft_engine::Session::new().with_fs();+ let mut h = Headless::new(LightcraftApp::new(session, Services { png: None, ..Default::default() }), [1300.0, 900.0], 1.0);+ h.settle(SETTLE);+ h+}++fn prompt(command: &str, value: &str, params: serde_json::Value, key: &str) -> Dialog {+ Dialog::TextPrompt { title: "Title".into(), hint: "Name".into(), value: value.into(), command: command.into(), params, key: key.into() }+}++/// Open `dialog`, press its OK button, and let the frame settle.+fn confirm(h: &mut Headless, dialog: Dialog) {+ h.app.ui.dialog = Some(dialog);+ for _ in 0..4 {+ h.step();+ }+ let r = h.request("ui.clickWidget", json!({"id": "button:dialogOk"}), T);+ assert_eq!(r["ok"], true, "{r}");+ h.settle(SETTLE);+}++fn toast(h: &Headless) -> String {+ h.app.ui.toast.as_ref().map(|(t, _, _)| t.clone()).unwrap_or_default()+}++fn assert_stays_open(h: &Headless, value: &str, reason: &str) {+ match &h.app.ui.dialog {+ Some(Dialog::TextPrompt { value: kept, .. }) => assert_eq!(kept, value, "the typed text is kept"),+ other => panic!("the prompt stays open: {other:?}"),+ }+ assert!(toast(h).contains(reason), "the reason is shown: {:?}", toast(h));+}++#[test]+fn a_new_folder_whose_name_is_taken_keeps_the_prompt_open_and_says_why() {+ let dir = shoot("create-taken");+ let mut h = app();+ confirm(&mut h, prompt("folder.create", "Taken", json!({"parent": dir.to_string_lossy()}), "name"));+ assert_stays_open(&h, "Taken", "already exists");+ clean(&dir);+}++#[test]+fn a_rename_to_a_taken_name_keeps_the_prompt_open_and_says_why() {+ let dir = shoot("rename-taken");+ let mut h = app();+ confirm(&mut h, prompt("folder.rename", "Taken", json!({"path": dir.join("Other").to_string_lossy()}), "name"));+ assert_stays_open(&h, "Taken", "already exists");+ assert!(dir.join("Other").is_dir(), "nothing was renamed");+ clean(&dir);+}++#[test]+fn a_new_folder_closes_the_prompt_and_says_where_it_is() {+ let dir = shoot("create-ok");+ let mut h = app();+ confirm(&mut h, prompt("folder.create", " Selects ", json!({"parent": dir.to_string_lossy()}), "name"));+ assert!(dir.join("Selects").is_dir(), "made on disk");+ assert_eq!(h.app.ui.dialog, None);+ let parent = lightcraft_catalog::folders::folder_label(&dir.to_string_lossy());+ assert_eq!(toast(&h), CREATED.replace("{name}", "Selects").replace("{parent}", &parent));+ clean(&dir);+}++#[test]+fn the_created_folder_toast_comes_through_the_catalog() {+ let dir = shoot("create-de");+ let path = dir.join("Selects");+ let parent = lightcraft_catalog::folders::folder_label(&dir.to_string_lossy());+ let catalog: std::collections::BTreeMap<String, String> = serde_json::from_str(include_str!("../../locales/de-formats.json")).unwrap();+ let want = catalog[CREATED].replace("{name}", "Selects").replace("{parent}", &parent);+ crate::i18n::set_language(crate::i18n::Locale::De);+ let text = crate::panels::left::created_text(&path.to_string_lossy(), &dir.to_string_lossy());+ crate::i18n::set_language(crate::i18n::Locale::En);+ assert_ne!(want, CREATED.replace("{name}", "Selects").replace("{parent}", &parent), "German has its own wording");+ assert_eq!(text, want);+ clean(&dir);+}@@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-25 · **Change:** minor (LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6+> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (LR-BEHAV-DIALOGS: a one-line prompt that is refused stays open and says why, and a New Folder on Disk says it was made; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good@@ -468,7 +468,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | LR-BEHAV-RENDER-FIDELITY | Rendering matches Lightroom | P1 | 🟡 | `crates/pipeline/src/lib.rs`, `crates/pipeline/src/tone.rs` | every slider exists and works, but the character of the result (tone curve shape, highlight recovery, texture / clarity / dehaze, noise reduction, sharpening, default look) is tuned by eye; there is no systematic side-by-side comparison against Lightroom on the same CC0 raws. Needs a fidelity suite: Lightroom reference renders kept only in the local `plan/` (never committed), compared per slider and preset with a perceptual metric. Tuning that changes how existing settings render ships as a new process version (LRC-DEV-PROCESS, `docs/process-versions.md`) | | LR-BEHAV-DRAGDROP | Drag and drop | P1 | ✅ | `crates/ui-egui/src/lib.rs`, `crates/ui-egui/src/panels/grid.rs` (`drag_feedback`), `crates/ui-egui/src/panels/left.rs` (`drop_target`, `album_drag_over`, `top_level_drop_target`, `drag_auto_scroll`) | files → app (a drop-target card while they hover, then the import review); grid photos → an album row (adds the selection, with a count badge while dragging); sidebar album / folder rows → a folder (moves it; its name follows the pointer); a drag of albums or grid photos near the top or bottom edge of the sidebar scrolls it (faster nearer the edge, only while the pointer is over the sidebar) | | LR-BEHAV-TOAST | Toast notifications | P1 | ✅ | `crates/ui-egui/src/panels.rs` | |-| LR-BEHAV-DIALOGS | Dialogs fit the window | P1 | ✅ | `crates/ui-egui/src/dialogs.rs` (`dialog_body`, `list_height`), `crates/ui-egui/src/tests/dialog_fit.rs`, `crates/ui-egui/src/tests/dialogs.rs` (`a_click_outside_closes_the_dialog_when_settings_allow_it`) | a dialog taller than the window (Export on a 1366×768 screen, issue #781) never runs off it: its options scroll while the title bar and the Cancel / OK row stay on screen; lists that scroll on their own (What's New, All Metadata, shortcuts, smart-album rules, credits) get shorter first; dialogs that fit look as before; Import keeps its own resizable, scrolling window; Settings → Dialogs → Close a dialog by clicking outside it (off by default, so a stray click never throws away a dialog's choices): a click pressed and released on the dimmed backdrop closes the dialog like Esc and doesn't reach the panels below, while a drag that ends outside (a slider pulled past the edge) or a click that closes a dropdown keeps it; the import review never closes this way (`clicked_backdrop`) |+| LR-BEHAV-DIALOGS | Dialogs fit the window | P1 | ✅ | `crates/ui-egui/src/dialogs.rs` (`dialog_body`, `list_height`), `crates/ui-egui/src/tests/dialog_fit.rs`, `crates/ui-egui/src/tests/dialogs.rs` (`a_click_outside_closes_the_dialog_when_settings_allow_it`) | a dialog taller than the window (Export on a 1366×768 screen, issue #781) never runs off it: its options scroll while the title bar and the Cancel / OK row stay on screen; lists that scroll on their own (What's New, All Metadata, shortcuts, smart-album rules, credits) get shorter first; dialogs that fit look as before; Import keeps its own resizable, scrolling window; Settings → Dialogs → Close a dialog by clicking outside it (off by default, so a stray click never throws away a dialog's choices): a click pressed and released on the dimmed backdrop closes the dialog like Esc and doesn't reach the panels below, while a drag that ends outside (a slider pulled past the edge) or a click that closes a dropdown keeps it; the import review never closes this way (`clicked_backdrop`); a one-line prompt (Rename Folder, New Folder on Disk, the preset and version names, New Library when its name can't be used, Auto-Tag from Tracklog) the engine refuses stays open with its text and shows the reason in a toast, so the name can be corrected, where it used to close without a word; a New Folder on Disk that works says so (“Created folder “Selects” in “2026/Shoot””, naming the parent by its last two folders as dialogs do) | | LR-BEHAV-SIDEBAR-COLLAPSE | Collapsible left-sidebar sections | P2 | ✅ | `crates/ui-egui/src/panels/left.rs` (`section`, `section_header`), `crates/ui-egui/src/sidebar.rs` (`SidebarLayout`), `crates/ui-egui/src/tests/panels.rs` | click the Albums, Local, By Date, Folders or Keywords header (or drive `ui.clickWidget` `sidebarSection:<albums\|local\|byDate\|folders\|keywords>`) to fold or unfold the section; a chevron before the title shows the state; the choice is kept across restarts (`sidebar` in the UI state; the `collapsedSidebar` list of older settings files is still read). the click itself is UI-only, like the By Date / Keywords disclosure triangles, and `cmd:view.sidebarSection` `{section, collapsed}` folds and unfolds for agents; photos dropped on a folded Albums section have no target | | LR-BEHAV-SIDEBAR-SECTIONS | Left-sidebar sections share one frame and one arrangement | P2 | ✅ | `crates/ui-egui/src/sidebar.rs` (`SidebarSection`, `SidebarLayout`), `crates/ui-egui/src/panels/left.rs` (`section`, `section_header`, `tree_toggle`), `crates/ui-egui/src/tests/panels.rs` | Albums, Local, By Date, Folders and Keywords are drawn by one `section` frame (the same gap above, header, fold and accessibility name) from one arrangement, `sidebar` in the UI state: a list of `{id, hidden, collapsed}` top to bottom, also settable with `ui.set`. Whatever is read, every section is in it exactly once (unknown and repeated entries are dropped, the rest follow in the usual order), so a damaged or newer settings file never loses the other settings. Rows that open (album folders, Local folders, dates, library folders, keywords) share `tree_toggle`: the same triangle, the same remembered open state, announced as Expand / Collapse. A section with nothing to list (Folders before anything is imported, Keywords before any is added) stays, open, with a one-line note in place of rows (`sidebarEmpty:<id>`) instead of disappearing. `ui.set` reads `sidebar` strictly (`SidebarLayout::parse`): an unknown or repeated section, or a flag that is not true / false, is refused and changes nothing; sections left out follow in the usual order, shown and open; the older `collapsedSidebar` list still folds and unfolds. A section may have one button at the end of its header, declared in one place (`header_button`: an icon, a tooltip, and either an action or a menu): the frame draws it at the visible edge in the header and its pinned copy, keeps its click from folding the section and leaves it room beside a folded section's count. Albums has + (`icon:albumNew`: Create Album / Smart Album / Folder…), Keywords has + (`icon:keywordNew`: Create Keyword Tag, for a keyword inside the default parent, not inside what the Keyword List has picked; once made, Keywords is shown and opened down to it). My Photos (All Photos, Recently Added, Picks, Missing Photos, Recently Deleted) is not a section: it stays on top | | LR-BEHAV-SIDEBAR-HIDE | Show and hide left-sidebar sections | P2 | ✅ | `cmd:view.sidebarSection`, `cmd:view.sidebarReset`, `crates/ui-egui/src/sidebar.rs`, `crates/ui-egui/src/panels/left.rs` (`sections_menu`), `crates/ui-egui/src/menus/menubar.rs`, `crates/ui-egui/src/tests/panels.rs` | View ▸ Sidebar Sections lists Albums, Local, By Date, Folders and Keywords in the sidebar's order, each checked while shown; unchecking one takes its header and rows off the sidebar, checking it puts it back where and how it was (still folded if it was). The same list opens with a right-click on any section header (with Hide This Section first; widgets `sidebarMenu:hide`, `sidebarMenu:section:<id>`, `sidebarMenu:reset`) and on the My Photos title (`sidebarTitle`), which is always there, so hidden sections can always come back. Reset Sidebar Sections (enabled once something changed) shows and opens every section in the usual order. `view.sidebarSection` `{section, show?, collapsed?}` flips shown / hidden when neither flag is given and refuses an unknown section or a flag that is not true / false. Hiding the section of what the grid shows (an album, a keyword filter) leaves the grid as it is; an album made while Albums is hidden or folded brings Albums back, open. Kept with the UI state (`sidebar`). My Photos itself can't be hidden |@@ -959,3 +959,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | 2026-10-23 | patch | LR-BEHAV-ACCESS: the name field of New Album, New Smart Album, Rename and the other one-field dialogs takes the keyboard focus once when the dialog opens instead of every frame, so Tab and Shift+Tab reach the check box, Cancel and OK; status unchanged (🟡) | | 2026-10-22 | minor | LR-CROP-AUTO: Auto straighten turns by at most 10° and only on at least 3 agreeing lines, and says when it finds nothing to level by; measured on 68 photos straightened by hand and 300 left as shot, unwanted turns of more than 1° fell from 79 to 19 of the 300. Downgraded ✅ → 🟡: it still leaves most tilted photos alone | | 2026-10-23 | patch | LRC-LIB-FILTERBAR: showing an album (`library.source`) while the filter holds an album drops the filter's album, as showing a library folder already dropped its folder filter; before, the old album stayed in the filter unseen (the filter bar has no album chip) and narrowed the next source such as All Photos; the row's note says so; status unchanged (✅) |+| 2026-10-27 | patch | LR-BEHAV-DIALOGS: a one-line prompt the engine refuses (a name already taken, a bad time zone) stays open with the typed text and shows the reason, instead of closing silently; a New Folder on Disk that works shows “Created folder “…” in “…””; status unchanged (✅) |@@ -1,5 +1,13 @@ # What's new in LightCraft +## 2026-10-27++### A refused name prompt stays open and says why+- A one-line prompt (Rename Folder, New Folder on Disk, the preset and version names, New Library for a name it can't+ use, Auto-Tag from Tracklog) that the app refuses, for example a folder name that is taken, now stays open with what+ you typed and shows the reason, so you can change the name. Before, it closed without a word. A New Folder on Disk+ that works now says so: “Created folder “Selects” in “2026/Shoot””. Cancel and Esc still close the prompt.+ ## 2026-10-24 ### Relative-date filters and smart albums follow the clock