Files
lightcraft/docs/parity.md
T
Nathaniel LedfordandClaude Opus 5.5 9b78f573ab LR-BEHAV-TEXTFIELD: document the shared text field
A parity row (🟡: three fields use it so far, the rest still use
egui's own) with the summary refreshed, and What's New for the menu,
Esc and Ctrl+C / Ctrl+V on Windows and Linux.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-09 13:23:10 -04:00

143 KiB
Raw Blame History

Lightroom parity tracker

A living checklist of every Lightroom feature LightCraft aims to match, what we have, and what is missing. Agents and humans pick work from Top gaps; whoever lands a feature updates its row in the same commit.

  • Ids come from the (local, gitignored) reference in plan/lightroom/: LR-… = feature catalog (03), MENU-… = menu items (04), KEY-… / KEYC-… = keyboard shortcuts (06, desktop / Classic), LRC-… = Classic extras (08). Ids are stable; never renumber.
  • Tier as in the catalog: P0 core, P1 important parity, P2 later / AI / niche, OOS out of scope.
  • Status: ✅ done (usable end to end; polish may be noted) · 🟡 partial (note says what is missing, or the status is unverified) · ⬜ missing · 🚫 out of scope / not applicable.
  • Evidence: cmd:<id> = command id (engine command_specs() or UI UI_COMMANDS), ctl:<id> = develop control id (lightcraft-cli controls), plus source files. A trailing * matches a prefix (ctl:mixer.*).
  • Feature names and notes are our own words. No Adobe text, screenshots or assets belong here.

cargo xtask parity checks that every cmd:/ctl: id and file path below still exists (part of cargo xtask ci) and prints the summary; cargo xtask parity --write refreshes the summary table below.

Summary

Section ✅ 🟡 ⬜ 🚫 P0 done P1 done
A. Import (IMP) 11 3 1 1 3/5 (60%) 5/5 (100%)
B. Library management (LIB) 22 2 1 2 9/9 (100%) 9/9 (100%)
C. Views & navigation (VIEW) 15 1 1 0 8/9 (89%) 4/4 (100%)
D. Search & filter (FILT) 11 1 1 0 4/4 (100%) 4/4 (100%)
E. Metadata (META) 5 1 0 0 2/2 (100%) 2/2 (100%)
F. Edit panel — global adjustments (EDIT) 42 2 4 1 28/28 (100%) 13/14 (93%)
G. Profiles (PROF) 7 1 3 0 3/4 (75%) 3/3 (100%)
H. Crop & rotate (CROP) 9 0 0 1 6/6 (100%) 3/3 (100%)
I. Remove / healing (REM) 7 1 2 2 4/4 (100%) 2/3 (67%)
J. Red eye (EYE) 2 0 0 0 — 1/1 (100%)
K. Masking (MASK) 14 4 5 0 8/8 (100%) 5/5 (100%)
L. Presets (PRE) 6 0 1 1 2/2 (100%) 2/2 (100%)
M. Versions & history (VER) 5 0 0 0 1/1 (100%) 3/3 (100%)
N. Copy / paste / sync (SYNC) 5 0 0 0 3/3 (100%) 1/1 (100%)
O. Merge (MERGE) 4 0 0 0 — —
P. Enhance (ENH) 0 0 2 0 — —
Q. HDR (HDR) 0 0 5 0 — —
R. Video (VID) 0 0 5 1 — 0/2 (0%)
S. Export (EXP) 15 1 2 0 7/7 (100%) 7/7 (100%)
T. Share (SHARE) 0 0 0 4 — —
U. Map & location (MAP) 0 1 1 0 — 0/1 (0%)
V. Preferences (PREF) 6 1 2 2 1/1 (100%) 4/4 (100%)
W. Cloud & AI infrastructure (CLOUD / AI) 1 0 1 2 — —
X. Cross-cutting behaviours (BEHAV) 18 6 1 1 8/8 (100%) 6/9 (67%)
Y. Menus 82 0 4 8 47/47 (100%) 23/23 (100%)
Z. Keyboard shortcuts (desktop) 75 3 3 1 49/52 (94%) 23/24 (96%)
Lightroom Classic extras 32 11 39 9 — 22/24 (92%)
Total 394 39 84 36 193/200 (97%) 142/153 (93%)

Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): 80.0% of 517 in-scope rows — P0 98.2% of 200 · P1 95.4% of 153 · P2 42.9% of 163.

Top gaps

Ordered by user impact, then tier, then effort. The checklist above counts features that exist; these are the gaps that decide whether a photographer can switch (see the honest assessment in ROADMAP.md). Take the first one nobody is working on.

  1. LR-PROF-CAMERACOLOR (P0): our own camera colour calibration. Sony ARW, Nikon NEF and Panasonic RW2 now get a guarded file-local fit to their own embedded JPEG (colour matrix + hue/saturation/value table + tone and chroma curves, relative WB; per-model profiles pooled from many photos via lightcraft-cli calibrate for ARW and NEF; ARW: 7 of 10 public samples accepted, mean ΔE vs the camera JPEG 17–26 → 3–10; NEF: 13 of 13 decodable samples from 6 bodies accepted, ΔE 13–46 → 3–8, one mixed-light scene 13 → 12; RW2: 140 of 174 public samples accepted, from 93 of the 114 bodies with a preview, median ΔE 5.0); Nikon CropArea and untagged-preview ColorSpace metadata are respected; measured calibration and fidelity remain missing. Other non-DNG raws and rejected fits still use a neutral matrix. Expand validated preview fitting and use matrices the files carry themselves; never Adobe data.
  2. LR-IMP-FORMATS (P0): CR3 first (every Canon body since ~2018), then compressed RAF / ORF, Nikon
  3. LR-PROF-CAMERACOLOR (P0): our own camera colour calibration. Sony ARW, Nikon NEF, Panasonic RW2 and Fujifilm RAF now get a guarded file-local fit to their own embedded JPEG (colour matrix + hue/saturation/value table + tone and chroma curves, relative WB; per-model profiles pooled from many photos via lightcraft-cli calibrate for ARW, NEF and RAF; ARW: 7 of 10 public samples accepted, mean ΔE vs the camera JPEG 17–26 → 3–10; NEF: 13 of 13 decodable samples from 6 bodies accepted, ΔE 13–46 → 3–8, one mixed-light scene 13 → 12; RW2: 140 of 174 public samples accepted, from 93 of the 114 bodies with a preview, median ΔE 5.0; RAF: 23 of 24 references accepted with bundled X-H2S/X-T4 profiles; 150 withheld-file tests improve mean ΔE slightly over per-file fitting); measured calibration and fidelity remain missing. Other non-DNG raws and rejected fits still use a neutral matrix. Expand validated preview fitting and use matrices the files carry themselves; never Adobe data.
  4. LR-IMP-FORMATS (P0): CR3 first (every Canon body since ~2018), then compressed ORF, Nikon
  5. LR-PROF-CAMERACOLOR (P0): our own camera colour calibration. Sony ARW, Nikon NEF and Panasonic RW2 now get a guarded file-local fit to their own embedded JPEG (colour matrix + hue/saturation/value table + tone and chroma curves, relative WB; per-model profiles pooled from many photos via lightcraft-cli calibrate for ARW and NEF; ARW: 7 of 10 public samples accepted, mean ΔE vs the camera JPEG 17–26 → 3–10; NEF: 13 of 13 decodable samples from 6 bodies accepted, ΔE 13–46 → 3–8, one mixed-light scene 13 → 12; RW2: 140 of 174 public samples accepted, from 93 of the 114 bodies with a preview, median ΔE 5.0); measured calibration and fidelity remain missing. Other non-DNG raws and rejected fits still use a neutral matrix. Expand validated preview fitting and use matrices the files carry themselves; never Adobe data.
  6. LR-IMP-FORMATS (P0): CR3 lossless Bayer and version 0x100/0x200 C-RAW now decodes (M50/R100/R8 full sensor exact). Remaining unverified CRX variants, compressed RAF / ORF, Nikon "lossy after split" NEF, Canon sRAW; AVIF decode (HEIC decodes behind the codecs' non-default heif feature: the optional lightcraft-heif crate on heic-rs, the same decoder and policy as PhotoCraft). Clean-room, from prose descriptions only (see crates/raw/src/vendor/nefc.rs for how compressed NEF was done). Until decoded, such photos are preview_only.
  7. LR-IMP-CAMERA-COVERAGE (P0): per-model verification; grow the CC0 corpus and fix per-model bugs (like the CR2 colour-filter layout, fixed in #85 by reading the file's own tag).
  8. LR-BEHAV-RENDER-FIDELITY (P1): a side-by-side fidelity suite against Lightroom renders (kept local in plan/), then tune tone, highlights, texture/clarity/dehaze, NR and sharpening against it. 4b. LR-VIEW-ZOOM (P0): finish the sharp zoomed views of issue #323: windows for compare, overlays and soft proofing (Before/After has them); check sharpness and the wide-stage margin on real raw photos (only synthetic scenes so far).
  9. LR-EDIT-OPTICS-PROFILE (P1): a lens-profile database of our own (corrections embedded in DNG files and the distortion correction Panasonic / Leica cameras record in RW2 / RWL files work today; issue #256).
  10. AI masks and Enhance (LR-MASK-SUBJECT / SKY / PEOPLE / OBJECTS, LR-EDIT-DETAIL-DENOISE, SUPERRES, LENSBLUR): Object and Describe masks run SAM 3 in pure Rust (crates/segment); the weights (SAM License) are never bundled: the app offers a consented, verified download, but LightCraft's CDN mirrors are not configured yet (the built-in list is empty, so today users need their own mirror or a manual install). Subject / Sky / People could use the same model with fixed prompts; denoise / super-resolution models remain a maintainer decision.
  11. HDR (Q. HDR, LR-EXP-HDR), video (R. Video), Classic output modules (Map view, Book, Slideshow module, Print, publish): large, well understood, lower priority than 1–5.
  12. LR-VIEW-ZOOM (P0): pinch zoom and two-finger pan now work, with stable image cursors between gesture events. The whole-image preview is still capped by Settings → Performance → Preview size (2,560 px by default). Render visible regions at native resolution so 100% can reliably show original-pixel detail on larger photos.

Recognition runtime gap closed (LR-PREF-PEOPLE): the pinned SFace and AuraFace models now run on the shared pure-Rust CPU engine and agree with an independent reference runtime on public crops. Live-face threshold calibration, small-face coverage and separated clusters remain open; see faces.md.

Shortcuts: conflicts and missing bindings

Compared plan/lightroom/06-shortcuts.md (desktop part) with our bindings: command specs in crates/engine/src/cmd/, UI_COMMANDS in crates/ui-egui/src/menus.rs and secondary bindings (ALIASES) in crates/ui-egui/src/shortcuts.rs. no_conflicting_bindings (same file) fails when one key fires two actions.

Fixed (M16.1): added Lightroom-desktop keys as secondary bindings for existing commands — ⌘D Select None, ⇧E Export dialog, Space Toggle zoom, ⇧M Create Version, ⇧X Reject + advance, ⇧U Unflag + advance. ⇧Y fired both Before/After Split and the History panel; History lost the binding. W and ⇧⌘I also fired the engine command under the UI command that wraps it (a no-op error); the UI command now wins.

Deliberate differences (our key → Lightroom desktop key) — each is a conflict with another binding we have:

Action Ours Lightroom desktop Why
Pick flag P Z Z = toggle zoom (Classic convention); P is the Classic pick key
Photos panel (left) ⌘⇧L P P = pick
Expand/collapse edit sections ⌘⌥1–5 ⌘1–6 ⌘0 = Zoom to Fit, ⌘1 = Zoom 100 %; Geometry lives in the Crop panel
Histogram ⌘⇧H ⌘0 ⌘0 = Zoom to Fit
Square Grid ⇧G (none; ⇧G = Guided Upright) Guided Upright is a button in the Crop panel
Export dialog ⌘⇧E (+ ⇧E) ⇧E ⌘⇧E is "Edit in Photoshop" there; no external-editor command yet
Crop overlay cycle ⇧O O O = mask overlay; ⇧O (mask colour / overlay orientation) unused otherwise
Create Version ⌘⇧S (+ ⇧M) ⇧M (Windows: Ctrl+⇧S) —
Select None ⌘⇧A (+ ⌘D) ⌘D —

Library culling (KEYC-RATING): 0–5, 6–9 and P/X/U reach egui on macOS even when displayed in the native menu (adapted from PR #261; issue #283). Shift+0–9 applies and advances once; Shift+P picks and advances in Photo Grid / Square Grid and retains Presets elsewhere. See library shortcuts for the verified Classic mapping and regression coverage. Remaining Classic gaps: rating [ / ], flag cycling and filter-bar keys.

Still missing / broken:

  • No command yet: F1 help (verify the rest of the old list: full screen, settings, stacks, visualize spots and merges have commands now).
  • H opens Remove; Lightroom also uses it (Classic) to hide pins — pins toggle from View → Show Mask Pins.
  • ⌘M / ⌘H / ⌘Q / ⌘W rely on the platform window defaults (unverified).

A. Import (IMP)

Id Feature Tier Status Evidence Missing / notes
LR-IMP-ADD-DIALOG Add photos/folders P0 ✅ cmd:file.addPhotos, cmd:library.importPreview, cmd:library.import (mode, album, albumName, preset, keywords), crates/ui-egui/src/import.rs review dialog (Import Photos): the scanned source (a scanned folder is not added to Local), candidate grid with thumbnails and checkboxes (duplicates by path/content marked and unchecked; with several file types, a toggle per type checks or unchecks all of its files, e.g. the raws without the JPEGs beside them, issue #344) filling a resizable window (issue #337), transfer mode (add in place / copy / move, each explained), album (existing/new), preset, keywords; the folder scan runs on a background thread with a progress window and Cancel (a NAS folder no longer freezes the window); the import itself (probing, sidecars, copying / moving files) runs on a worker thread and its batches join the library between frames, with a progress window and Cancel; one undo step. Drag-and-drop goes through the same background import. File dialogs list the importable extensions (lightcraft_engine::import::EXTENSIONS) in any letter case on Linux, where GTK and the desktop portal match globs case-sensitively (apps/lightcraft/src/dialog_filter.rs, issue #342)
LR-IMP-DRAGDROP Drop files/folders to import P0 ✅ crates/ui-egui/src/lib.rs (dropped files → cmd:library.import) dropping onto a specific album not supported
LR-IMP-DUPES Skip duplicates by content P1 ✅ crates/engine/src/import.rs, crates/engine/src/tests_import.rs
LR-IMP-DEVICE Import from camera/card P1 ✅ cmd:library.devices, cmd:file.addFromDevice, crates/engine/src/devices.rs mounted volumes with a DCIM folder (macOS /Volumes, Linux /media and /run/media, Windows drive letters); File → Import from Device → the import review, copying into the library by default; menus serve the last scan and rescan in the background (never blocking a frame; hot-plugs repaint); copies are verified (a new file, synced, checked against the content hash the scan computed — byte compare with the source when there is none, issue #134; a bad copy is removed and reported as failed, never counted as imported; taken names get -1, -2…, never replaced — issue #96, crates/engine/src/import_move.rs); no PTP/MTP (cameras that don't mount as a disk)
LR-IMP-AUTO Watched-folder auto import P2 ✅ cmd:library.autoImport, cmd:library.autoImportScan, crates/ui-egui/src/panels/settings.rs Settings ▸ Import ▸ Auto Import: a watched folder whose new photos are added (in place or copied) once complete, into an optional album; scanned every 3 s
LR-IMP-PRESET Preset on import P2 ✅ cmd:library.import (preset) chosen in the import review; one History entry
LR-IMP-RAWDEFAULT Raw defaults P1 ✅ cmd:library.preferences, crates/engine/src/import.rs, crates/ui-egui/src/panels/settings.rs LightCraft default / a preset / per camera (make + model); non-raw default too; the preset look counts as unedited and Reset returns to it
LR-IMP-MIGRATE Migrate other catalogs OOS 🚫
LR-IMP-PROFILES Import profiles & presets P1 ✅ cmd:file.importPresets, cmd:preset.import, cmd:profile.import, crates/engine/src/preset_import.rs, crates/engine/src/preset_luminar.rs presets: .lcpreset, XMP, classic .lrtemplate, photos carrying edits ("DNG presets"), Luminar looks (.lmp, .mplumpack collections; sliders with a counterpart), .zip bundles, folders (folder → group), drag & drop; masks carried over; unmapped settings reported (fields that only describe the preset or photo, e.g. Cluster, SortName, SupportsAmount2, the as-shot white, empty Point Color slots, are not); profiles: .cube 3D LUTs; Adobe profile formats deliberately unsupported
LR-IMP-LOCAL Work on files in place P0 ✅ cmd:library.browse, cmd:photo.addToLibrary, cmd:library.import (mode add), cmd:local.addRoot, cmd:local.hide, cmd:local.restoreHidden, crates/engine/src/cmd/browse.rs, crates/ui-egui/src/panels/left.rs (local_section), crates/ui-egui/src/panels/grid.rs (folder_header) Local: browse Pictures / Desktop / Downloads / Home or any folder without adding it (breadcrumb, Include subfolders, Add N to My Photos); browsed photos stay out of All Photos, albums and counts; folders picked with Browse Folder… (or Keep in Local / Add to Local on a folder row) stay listed across restarts, and browsing a folder inside a listed one highlights it inside that tree (opened down to it, siblings reachable) instead of adding a root; edits go to XMP sidecars; importing promotes them; right-click a top-level location → Remove from Local hides the shortcut only (nothing on disk or in the catalog changes), saved with the UI state, and “Show N hidden locations” (or Browse Folder… on it) brings it back; a folder is one location however its path is spelled (/ or \, trailing separator, ./.., drive-letter case; folder_key in crates/catalog/src/query.rs); browsed photos nobody changed are forgotten once their folder has not been browsed for 30 days (Settings → Performance → Local folders, 0 = never; checked when the library opens, or cmd:library.forgetLocal with dryRun; files and sidecars stay, browsing again brings them back; see crates/catalog/src/local.rs for what counts as a change)
LR-IMP-SIDECAR-SPLIT Separate XMP sidecar variants P2 ⬜ cmd:library.xmpPreferences sidecar naming option exists (stem/full), no split sidecars
LR-IMP-FORMATS Supported formats P0 🟡 crates/codecs/src/lib.rs, crates/raw/src/lib.rs, crates/raw/src/jxl.rs JPEG, PNG, TIFF (1- to 64-bit integer, 16- to 64-bit float, incl. bilevel and CCITT fax), WebP, JXL, PSD, GIF, BMP; DNG (incl. lossy / Smart Preview DNG and DNG 1.7 JPEG XL tiles/strips: lossless ones decode bit-exactly on synthetic CFA and LinearRaw files, lossy ones are restored to the declared linear primaries without clipping or gamut mapping, checked on one real file with 108 lossy tiles, and any tile that fails to decode makes the file preview-only; a single-chunk JPEG XL preview IFD is offered as the embedded preview), CR2 (colour-filter layout from the file's CR2CFAPattern tag, issue #85), ARW (pre-2017 bodies: white balance and black level from enciphered maker-note / SR2SubIFD data, issue #148; downsized Sony lossless YCbCr 4:2:0 / 4:2:2 tiles decoded to linear RGB, verified on private ILCE-7M4 3:2 & 4:3 examples including 16-bit full-resolution export), NEF (uncompressed, lossless and lossy compressed 12/14-bit, crates/raw/src/vendor/nefc.rs), RAF (uncompressed, lossless/lossy compressed Bayer and X-Trans; crates/raw/src/vendor/rafc.rs, full sensor verification on 13 bodies, docs/raf-compression.md), RW2 / Leica RWL / Panasonic RAW in every raw format (compressed 4 and 6, the prefix-coded strips of format 8 from the GH6 on, packed 2/5/7, the 16-bit words of the 2005–2007 bodies; black levels, in-camera aspect crops and mapped-out defect pixels; 178 CC0 files from 118 bodies checked, crates/raw/src/vendor/rw2.rs), PEF, ORF (uncompressed; colour-filter layout from the file's Exif CFAPattern), Hasselblad 3FR with uncompressed raw data (X1D, X1D II, X2D, CFV 100C: a TIFF without a DNG version tag that describes its CFA IFD, levels, crop and white balance with DNG's tags; RawFormat::CfaTiff, read by dng::decode_as, RGGB when the file states no CFAPattern; the lossless-JPEG 3FR and FFF files use scan selection value 8 and stay preview-only), Samsung SRW without compression (NX5, NX10, NX11 and the NX20-generation bodies NX20, NX200, NX210, NX1000 and NX1100 as 12-bit packed samples in either bit order, EX1 and WB2000 as 16-bit words; layout, camera-JPEG framing, levels and white balance from the maker note, crates/raw/src/vendor/srw.rs; median ΔE vs the camera JPEG 2.2–7.6 on 11 of the 12 files from 10 bodies checked, 10.7 (5.7 registered) on the backlit WB2000 sample). 12-bit NEF black level: maker note 0x003d is in 14-bit units (D750, D780, D850, D7500, Z 50 checked). Lossy type 2 NEF curves of version 0x44 0x40 (D780, D6, Z 5, Z 6, Z 7, Z 7_2) span a quarter of the sample range, read from the table's own point spacing. CR3 lossless CRX Bayer and version 0x100/0x200 C-RAW decodes to sensor pixels with timed white balance, black/white levels and file crop (the shot's aspect ratio from the maker note's AspectInfo); full sensor samples match the reference on M50/R100/R8 (crates/raw/tests/cr3_corpus.rs, docs/cr3.md). Unsupported coding variants retain metadata and use the embedded JPEG when available; HEVC preview decoding remains unsupported. Missing: CR3 unverified CRX variants, compressed ORF, compressed Samsung SRW (NX1, NX30, NX300, NX500, NX2000, NX3000, NX3300, NX mini: preview only with the reason), NEFs labelled compressed (34713) but stored uncompressed without a 0x0096 table (Z 6 packed 14-bit, D850 12-bit uncompressed), NEF "lossy after split" (preview only: imported with preview_only = the decoder's reason, rendered as a rendered JPEG, flagged by a grid badge and loupe/Edit/Info notices, previewOnly in catalog.query; Reload clears it once the file decodes), TIFF-based raws whose raw data sits in a private block (Phase One / Leaf IIQ, Canon EOS-1D / 1Ds and Kodak DCS TIFFs: IFD0 is only a reduced RGB image next to a much larger declared picture) are recognised from that structure and import as preview_only with the reason, instead of opening as a thumbnail-sized ordinary image (thumbnail_shell in crates/raw/src/lib.rs), Recognised raws LightCraft cannot decode open as preview_only with any usable embedded JPEG, also Canon CRW, Minolta MRW and Sigma X3F (containers recognised by magic, preview found by a structural JPEG scan), Kodak KDC (IFD0 without ImageWidth), Leaf MOS and Epson ERF (scan), and any decode failure of a recognised raw, not only an unsupported variant; ones with no JPEG still fail with "without an embedded preview", AVIF decode (HEIC decodes only behind the non-default heif feature: crates/heif, heic-rs, same policy as PhotoCraft; 8/10-bit, alpha, irot/imir/clap, ICC/EXIF/XMP), palette (indexed-colour) TIFF (refused by the tiff crate)

| LR-IMP-CAMERA-COVERAGE | Camera coverage, verified per model | P0 | 🟡 | crates/raw/tests/corpus.rs, xtask/src/main.rs | a supported container is not the same as every camera that writes it decoding correctly: ~55 CC0 corpus files plus 6 exact-sensor CR3 cases (plus ~60 NEFs checked by hand and 178 Panasonic / Leica RW2, RWL and RAW files from 118 bodies, checked against each file's own JPEG) against the >1,000 models Lightroom lists; per-model differences still surface (e.g. the CR2 colour-filter layout differed by model until it was read from the file's own tag, issue #85, checked on 35 CR2 bodies; pre-2017 Sony ARWs opened green until their scrambled white balance / black level were read, issue #148, checked on 16 ARWs from 15 Sony bodies; Canon PowerShot and EOS M CR2s whose ColorData is stored as bytes opened green until the bytes were read as 16-bit words, 25 files from 25 bodies checked). Compressed RAF sensor arrays verified on 13 Fujifilm bodies (docs/raf-compression.md). Grow the CC0 corpus per model and test each sample decodes with plausible colour. Nikon CropArea and untagged-preview colour-space regression tests; private Z 8 crop verified. | | LR-IMP-CULL-AT-IMPORT | Culling analysis at import | P2 | 🟡 | cmd:photo.analyze | run Assisted Culling on the imported photos (they're selected after an import); not automatic | | LR-IMP-MOVE | Move on import [Classic] | P1 | ✅ | cmd:library.import (mode move), crates/engine/src/import_move.rs, crates/engine/src/tests_import_move.rs, crates/ui-egui/src/import.rs | Transfer ▸ Move with Copy's destination, folders (day / month / one folder / custom template), rename template and example destination; XMP sidecars (both namings) move along; each source is removed only after its destination is written (hard link on the same volume, else copied, synced and compared byte for byte) and its catalog record is saved; failed, duplicate, unchecked files keep their sources, taken names get -1, -2…, never overwritten; files already in the destination/library are added in place; sources that can't be removed (read-only card) are kept and reported (kept). Undo removes the photos from the library but leaves the moved files at the destination. No Copy as DNG while moving | | LR-IMP-DNG-CONVERT | Convert to DNG on import [Classic] | P2 | ✅ | cmd:library.import (dng), crates/ui-egui/src/import.rs | copy imports: Raw files ▸ Copy as DNG (lossless; the card is untouched); the raw copy is removed only once its DNG is verified and on disk (issue #106) |

B. Library management (LIB)

Id Feature Tier Status Evidence Missing / notes
LR-LIB-ALLPHOTOS All photos P0 ✅ cmd:library.source, crates/ui-egui/src/panels/left.rs
LR-LIB-RECENT-ADDED Recently added P1 ✅ cmd:library.source (recentlyAdded), crates/engine/src/view.rs (RECENT_DAYS) photos imported in the 30 days up to the latest import, newest import first, grouped by import day
LR-LIB-BYDATE Browse by date P1 ✅ cmd:library.filter (date), crates/ui-egui/src/panels/left.rs (date_row), crates/catalog/src/query.rs (date_groups) year → month → day tree with library-wide counts; a row filters the current source by that date. Active filters show as removable chips under the grid header (crates/ui-egui/src/panels/chips.rs, with “+N more” and Clear all), the header counts “N of M photos” (matching of the source's total), a badge on the top-bar filter icon counts them, and an empty result says no photos match the active filters
LR-LIB-ALBUM Albums P0 ✅ cmd:album.create, cmd:album.rename, cmd:album.delete, cmd:album.addPhotos, cmd:album.removePhotos, cmd:dialog.newAlbum photos dragged from the grid onto an album row are added to it (LR-BEHAV-DRAGDROP)
LR-LIB-FOLDER Folders of albums P0 ✅ cmd:album.create (folder, parent), cmd:album.move, cmd:album.reorder, cmd:album.sort, cmd:dialog.newAlbum / cmd:dialog.newFolder / cmd:dialog.smartAlbum / cmd:dialog.newSmartAlbum (parent), crates/ui-egui/src/panels/left.rs (albums_tree, folder_menu, disclosure_triangle), crates/ui-egui/src/tests_panels.rs folders nest and fold with the same disclosure triangle as Local / By Date / Folders / Keywords (albumToggle:<id>; the row folds them too; open by default); right-click a folder ▸ New ▸ Create Album… / Create Smart Album… / Create Smart Album from Filter… / Create Folder… creates inside it (the dialog.new* commands take parent), opening the folders down to the new row; creating is only allowed inside folders; moving: drag an album or folder row onto a folder (outlined when allowed; a closed folder opens after 0.6 s under the pointer; the "Albums" header takes an item that sits in a folder back to the top level while it is dragged), the Move to menu or album.move — one undo step each; not onto a plain album, itself or something inside it; drop on the top / bottom half of an album or the edge of a folder to place it there by hand (cmd:album.reorder, a line shows the place; ordered per folder, folders stay first, new albums go after the ordered ones; right-click ▸ Sort Contents A–Z or + ▸ Sort Albums A–Z = cmd:album.sort)
LR-LIB-SMARTALBUM Smart albums P1 ✅ cmd:album.createSmart, cmd:album.setRules, crates/catalog/src/query.rs, crates/ui-egui/src/panels/filterbar.rs, crates/ui-egui/src/lib.rs (album_counts) saved filters (rating/flag/label/kind/edited/keyword/camera/lens/date range/text/album), live; match-all only (no any/none rule groups, no rule editor dialog — rules come from the filter bar or album.setRules); sidebar counts are cached per catalog revision (and per minute while an “in the last…” rule is used), counted without building id lists
LR-LIB-SHARED-ALBUM Shared albums P2 ⬜ needs a sharing service
LR-LIB-OFFLINE Keep album offline P2 🚫 not applicable: local-first library
LR-LIB-TARGET Target album P2 ✅ cmd:album.setTarget, cmd:album.toggleTarget see LRC-LIB-COLLECTIONS
LR-LIB-RATING Star ratings P0 ✅ cmd:photo.rate (advance), crates/ui-egui/src/shortcuts.rs 0 clears; 1–5 set; Shift+0–5 applies and advances once, including with Auto Advance. Batch grid and undo/redo regressions; macOS native ownership corrected (PR #261)
LR-LIB-FLAG Pick / reject flags P0 ✅ cmd:photo.pick, cmd:photo.reject, cmd:photo.unflag, cmd:photo.flag P pick, X reject, U clear; Shift+P picks and advances in both grids (Presets elsewhere); Shift+X/U and legacy Shift+Z advance; no flag cycle; rejection does not delete the file
LR-LIB-LABEL Colour labels P1 ✅ cmd:photo.label, cmd:label.setNames, cmd:label.names, cmd:dialog.labelNames, keys 6–9 in crates/ui-egui/src/shortcuts.rs Photo menu and grid context menu (coloured, named), Info-panel swatches, label dot plus translucent colour on square-grid/filmstrip surrounds and photo-grid footers (photo pixels unchanged); colour-label toasts have pale matching backgrounds; setting/clearing through keys (including Shift), menus or Info swatches shows the same bottom toast as ratings, with the colour/custom name; editable label names (undoable, journaled); no purple key (as in Lightroom), no custom extra labels
LR-LIB-KEYWORD Keywords P0 ✅ cmd:panel.keywords, cmd:photo.setMeta (addKeywords/removeKeywords), cmd:keyword.list, cmd:keyword.rename, cmd:keyword.delete, cmd:keyword.merge, cmd:keyword.suggest, crates/catalog/src/keywords.rs, crates/ui-egui/src/panels/left.rs left-panel keyword tree with counts (click filters, children included; context menu rename / merge / delete / add / remove); library-wide ops are one undo step and replay from the op log; suggestions (co-occurring / most used / completions) in the Keywords panel; no keyword drag-and-drop
LR-LIB-PEOPLE People / faces P2 🟡 cmd:faces.detect, cmd:view.people, cmd:view.faceBoxes, cmd:photo.removeRegion, cmd:photo.setRegion, cmd:library.filter, crates/ui-egui/src/panels/people.rs, crates/catalog/src/query.rs (people), crates/engine/src/media.rs (face_job), crates/meta/src/xmp.rs MVP: named face regions read from XMP (MWG-RS, read-only, see docs/xmp-interop.md; clipped to the photo; Lightroom's Rotation of ±π/2 and π handled, mirrored orientations taken as written; boxes follow Rotate Left/Right and flips; a sidecar stating mwg-rs:Regions, even empty, replaces the photo's on re-read, one without leaves them) become cards in the People view (View ▸ People, or the People button in the toolbar): a close-up of the person's largest face, name and photo count; a click filters the grid (person in library.filter, person: search token, filter chip). Face boxes in the loupe can be switched off (View ▸ Face Boxes, toolbar) resized with eight drag handles and removed with the × shown on hover (catalog-only and undoable, one undo step per drag; the sidecar is never rewritten for either); no moving a box by dragging it yet. Photo ▸ Detect Faces (cmd:faces.detect) finds faces with the YuNet detector (pure Rust, about 200 ms a photo, matches OpenCV's own YuNet on a 45-photo test set; the 232 KB model is a one-time download offered in the same step, never part of LightCraft) and adds them as unnamed boxes; names are suggested from the faces already named (recognition models are opt-in downloads, run on the CPU in a background thread; a face is named only when you confirm it); a person's page shows their cropped faces and the unnamed faces that look like them (LRC-LIB-PEOPLE); with recognition on, a background scan finds faces in photos that have none and embeds every face (one to three worker threads, raws through their embedded preview); an Unnamed faces section orders faces by likeness and names a selected group at once; no separated clusters, no manual tagging, no writing regions back
LR-LIB-STACK Stacks P1 ✅ cmd:stack.group, cmd:stack.ungroup, cmd:stack.toggle, cmd:stack.setTop, cmd:stack.remove, cmd:stack.auto, crates/catalog/src/stacks.rs grid/filmstrip count badges, expand/collapse, auto-stack by capture time; no visual-similarity auto-stack
LR-LIB-VERSIONS Versions P1 ✅ cmd:version.create see section M
LR-LIB-DELETE Delete / Recently Deleted P0 ✅ cmd:photo.delete, cmd:photo.restore, cmd:photo.deletePermanently Deletion retains surviving selected photos; if none remain, selects the next visible survivor (previous at the end) in the current source/filter/sort instead of jumping to the first photo; Restore and Delete Permanently in the Photo menu and the photo context menus for photos in Recently Deleted (before, the UI had no way to restore); adding a file that is in Recently Deleted again selects it there (side panel opened) and says how to restore it or import it afresh; Photo ▸ Empty Recently Deleted (cmd:library.emptyRecentlyDeleted, one undo step, shown while the trash is the source); import takes onDeleted skip / restore / fresh for files in the trash (cmd:library.import; the report flags existingDeleted and lists restored; the import review dialog offers Leave them / Restore them / Import as new for files that are in the trash; dropped files and Add Photos without a review still skip and point at Restore); no confirmation dialog, no auto-purge after N days (needs a deleted-at date in the catalog)
LR-LIB-REMOVE-ALBUM Remove from album P0 ✅ cmd:album.removePhotos
LR-LIB-DUPLICATE Duplicate a photo P2 ✅ cmd:photo.duplicate Photo ▸ Duplicate: a real -copy file with the same settings, metadata and albums (virtual copies share the file)
LR-LIB-RENAME Batch rename P1 ✅ cmd:photo.rename, cmd:photo.renamePreview, cmd:photo.renameTokens, cmd:dialog.rename, crates/engine/src/rename.rs (TOKENS) template tokens {name} {num} {seq:N} {date:%Y%m%d} {folder} {camera} {lens} {iso} {rating} {title} {creator} {ext} (shared with export naming and import rename); a Tags picker beside every template field (Rename Photos, import Rename, export File name) lists each tag with meaning + live example, the date directives and the rules, and inserts the clicked tag at the text cursor; unknown tags are flagged; preview; renames files on disk with their XMP sidecars (a stem sidecar another file still shares, e.g. raw + JPEG, is copied rather than taken), never overwriting (-1, -2… suffixes; a change of letter case only is checked by file identity, so on case-sensitive volumes img_1.jpg next to IMG_1.JPG is a collision), rolls back on failure (a file that can't be moved back is reported old → new and the library follows it as an undoable partial rename; the same for undo/redo); undo/redo move the files; virtual copies follow
LR-LIB-CAPTURETIME Edit capture time P1 ✅ cmd:photo.setCaptureTime, cmd:dialog.captureTime, crates/catalog/src/dates.rs set (the other selected photos shift by the same amount, or each), shift by days/hours/minutes, time-zone shift; one undo step, journaled; Info panel button; no “revert to original capture time”
LR-LIB-SHOWFINDER Reveal original in file manager P0 ✅ cmd:app.showInFinder ⌘R (see MENU-FILE-SHOWFINDER)
LR-LIB-COVER Album cover P2 ✅ cmd:album.setCover
LR-LIB-CULL Assisted culling P2 🟡 cmd:photo.analyze, cmd:dialog.cull, crates/pipeline/src/cull.rs Photo ▸ Assisted Culling…: focus score (0–100), clipping, burst grouping (look-alike shots within 10 s) with the sharpest marked; reject below a focus score, pick each burst's best; classical measures — no eyes-closed / expression detection
LR-LIB-ACTIVITY Comments & likes OOS 🚫
LR-LIB-QUICKCOLL Quick collection [Classic] P2 ✅ cmd:album.toggleTarget, cmd:album.clearQuick B in the grids
LR-LIB-VIRTUALCOPY Virtual copies [Classic] P1 ✅ cmd:photo.virtualCopy, crates/engine/src/cmd/organize.rs “Copy N” badge, stacked with the original, same albums, no XMP writes; no “Set Copy as Master”

C. Views & navigation (VIEW)

Id Feature Tier Status Evidence Missing / notes
LR-VIEW-PHOTOGRID Justified photo grid P0 ✅ cmd:view.photoGrid, cmd:library.groups, cmd:library.sort (group), crates/ui-egui/src/panels/grid.rs, crates/catalog/src/dates.rs grouped by capture / import / edit date (day headers “Wednesday, 30 September 2026 · 12 photos”, months or years when zoomed out, or a fixed day/month/year/none choice in the sort menu); sticky header; clicking a header selects its photos; stacks never split; scrolls to the active photo only when it changes (or the grid comes back on screen), never on its own
LR-VIEW-SQUAREGRID Square grid P0 ✅ cmd:view.squareGrid same date headers as the photo grid
LR-VIEW-DETAIL Single-photo view P0 ✅ cmd:view.detail, crates/ui-egui/src/panels/detail.rs
LR-VIEW-EDIT Edit view P0 ✅ cmd:panel.edit
LR-VIEW-FULLSCREEN Full-screen preview P1 ✅ cmd:view.fullScreenPreview, cmd:view.enterFullScreen photo on black, arrows step, Esc exits; ⇧⌘F window full screen
LR-VIEW-FILMSTRIP Filmstrip P0 ✅ cmd:view.filmstrip, crates/ui-egui/src/panels/detail.rs the photo context menu on right-click; the mouse wheel scrolls it sideways; it follows the active photo only when that changes (centred if off screen), so a scrolled strip stays put; every selected photo is highlighted, the active one framed in white, as in the grid
LR-VIEW-ZOOM Zoom & pan P0 🟡 cmd:view.zoomFit, cmd:view.zoom100, cmd:view.zoomIn, cmd:view.zoomOut, cmd:view.zoomToggle, cmd:view.clickZoom, cmd:view.navigate, crates/ui-egui/src/panels/detail.rs, crates/ui-egui/src/headless.rs continuous native pinch / modifier-wheel zoom (Fit–800%, anchored under the pointer), two-finger scroll / touch pan with edge limits; Detail (including tools, Before/After and full-screen preview), Compare and Reference; fractional percentages preserve small gestures; image, picker and targeted-adjustment cursors stay steady between gesture events; keyboard steps 25–800 % (not 6–1600 %); Fill in the bottom bar; click / Z / Space eases to the chosen ratio (1:1 default, 2:1, 3:1, 4:1, 8:1). The loupe's cost follows the canvas, not the zoom (issue #323): the whole frame is rendered at about canvas size (at most the 2560 px preview source level unless a larger Preview size is chosen, and never above the GPU's texture side), and once the view is zoomed well past that (more than 25 %) the window on screen is rendered too, cut from a frame of at most the photo's own pixels, so beyond 100 % the GPU magnifies it (region::plan, RenderRequest::window, Session::region_job, crates/ui-egui/src/region.rs); 1:1 and 2:1–8:1 are sharp at any photo size. A slider tick takes 7–12 ms on a 1400 × 900 canvas and 11–25 ms on a 2800 × 1700 one at 100 % and 400 % of a 24 MP photo (M1 Max, GPU, profile_slider_drag); about 20–25 ms at 100 % on the big canvas is over a 16 ms frame. A continuous pinch holds the render sizes (and the windows there are) until it is quiet. A fit view on a canvas up to about 3200 px (a Retina Mac) never decodes the original; larger displays get a window at fit, which does. Modes that draw another picture over the loupe (mask overlays, soft proofing) and photos whose spots or Auto Mask strokes read too far for one window (only photos longer than 8192 px) render the whole frame at the size it is drawn, as before windows; while a preset or profile is hovered, or over a picture that is not the frame (an unsupported raw's embedded JPEG), the whole frame stays canvas-sized. Before/After (side by side, stacked, \\ and both wipes) gets the same windows on both sides, clipped to their pane or side of the line. Windows render on the GPU like whole frames (equivalence-tested against the CPU), in the browser build too (windows no wider than its GPU's texture side, at most 3072 px), and are not made while hovering a look, soft-proofing or showing an overlay, nor over a picture that is not the frame (an unsupported raw's embedded JPEG with another crop). No texture is larger than the GPU allows. Spots and Auto Mask brushes work while what a window reads fits one render (8192 px along an axis, 4096 in the browser): the window grows to hold the spot and its source patch, or the dabs near it, and if that would take more (a clone from far away at 8:1) the loupe keeps the whole-frame render instead, and spots with an automatic source pick it from a fixed-size render of the whole frame (so a window's choice can differ from a whole-frame render's, which picks at its own resolution). The wide stages (dehaze, clarity, highlights/shadows base) and Refine Edges on masks read a window margin capped at 768 px; measured on hard-edged blocks that costs at most a few levels at 8:1. the histogram and Navigator come from the whole-frame render
LR-VIEW-NAVIGATOR Navigator mini map P1 ✅ cmd:view.navigator, crates/ui-egui/src/panels/detail.rs shown while zoomed (bottom right); click/drag pans
LR-VIEW-BEFOREAFTER Before / after P0 ✅ cmd:view.showOriginal, cmd:view.beforeAfter, cmd:view.beforeAfterSplit, cmd:view.beforeAfterTopBottom, cmd:view.beforeAfterSplitTopBottom, cmd:beforeAfter.setBefore, cmd:beforeAfter.copyAfterToBefore, cmd:beforeAfter.copyBeforeToAfter, cmd:beforeAfter.swap, cmd:beforeAfter.resetBefore all four layouts; before = the import state (defaults + import preset) or a chosen history step / version / the current settings; copy and swap (View → Before/After Settings, History row menu). The chosen before lasts for the session
LR-VIEW-COMPARE Compare two photos P1 ✅ cmd:view.compare, cmd:compare.swap, cmd:compare.makeSelect, crates/ui-egui/src/panels/compare.rs select / candidate, synced zoom + pan, arrows move the candidate; no zoom-link toggle
LR-VIEW-SURVEY Survey view [Classic] P2 ✅ cmd:view.survey, crates/ui-egui/src/panels/compare.rs selection tiled (≤ 48), keys act on the active photo, hover × removes
LR-VIEW-INFOOVERLAY Info overlay on the photo P1 ✅ cmd:view.infoOverlay off / file + date + size / exposure + camera; ⌘I cycles (I in full screen; elsewhere I stays the Info panel)
LR-VIEW-SLIDESHOW Slideshow P2 ✅ cmd:view.slideshow View ▸ Slideshow (⌥⌘↩): the photos in view full screen, every 4 s (interval), wrapping; Space pauses, ←/→ step, Esc ends
LR-VIEW-SECONDWINDOW Second display window [Classic] P2 ✅ cmd:view.secondWindow, crates/ui-egui/src/panels/second.rs Window ▸ Second Window (⌘F11): the active photo fitted in its own native window with its own render (a floating panel where there are no native windows); loupe view only (no grid / compare / survey there)
LR-VIEW-CLIPPING Clipping indicators P0 ✅ cmd:view.clipping
LR-VIEW-HISTOGRAM Histogram P0 ✅ cmd:view.histogram, crates/ui-egui/src/panels/edit.rs no drag-to-adjust on the histogram
LR-VIEW-HDR-DISPLAY HDR display output P2 ⬜

D. Search & filter (FILT)

Id Feature Tier Status Evidence Missing / notes
LR-FILT-SEARCH-META Text search P0 ✅ cmd:library.filter (text), crates/ui-egui/src/panels/topbar.rs, crates/catalog/src/query.rs fielded tokens (rating:3, iso:>800, camera:…); no suggestions dropdown
LR-FILT-SEARCH-AI Natural-language search P2 ⬜
LR-FILT-RATING Rating filter P0 ✅ cmd:library.filter (rating, ratingOp), crates/ui-egui/src/panels/filterbar.rs ≥ / = / ≤ stars in the filter bar (cmd:view.filterBar)
LR-FILT-FLAG Flag filter P0 ✅ cmd:library.filter (flag), crates/ui-egui/src/panels/filterbar.rs picked / rejected / unflagged (one at a time)
LR-FILT-LABEL Colour-label filter P1 ✅ cmd:library.filter (label), crates/ui-egui/src/panels/filterbar.rs one label at a time; no “no label” choice
LR-FILT-TYPE Type / edited filter P1 ✅ cmd:library.filter (kind, merged, edited), crates/ui-egui/src/panels/filterbar.rs, crates/catalog/src/query.rs (merged_kind) photos / raw / videos / HDR / panoramas / HDR panoramas (merge results by name), edited / unedited; no depth kind
LR-FILT-KEYWORD Keyword filter P1 ✅ cmd:library.filter (keyword), crates/ui-egui/src/panels/filterbar.rs keyword picker
LR-FILT-CAMERA Camera / lens filter P1 ✅ cmd:library.filter (camera, lens), crates/ui-egui/src/panels/filterbar.rs camera and lens pickers
LR-FILT-LOCATION Location filter P2 ✅ cmd:library.filter (text, ruleSet field location) free text, or a rule on location / city / state / country (smart albums, library.filter)
LR-FILT-PEOPLE People filter P2 🟡 cmd:library.filter, crates/catalog/src/query.rs filter by a person's name (case-insensitive, named Face regions only) from the People view, a filter chip or person:; no unnamed / suggested people
LR-FILT-CULL Culling-score filters P2 ✅ cmd:library.filter (ruleSet fields sharpness, bestOfGroup) Focus and Best of Similar Shots in the rule editor / smart albums
LR-FILT-SORT Sort P0 ✅ cmd:library.sort, cmd:library.shuffle, crates/ui-egui/src/panels/bottombar.rs Random sort (seeded, stable under edits; choosing Random or Reshuffle picks a new seed); the shuffle is part of the saved view state; no effect in Recently Added (always newest import first); no colour-label or custom (manual) order
LR-FILT-SAVED Filter presets [Classic] P2 ✅ cmd:filter.savePreset, cmd:filter.applyPreset, cmd:filter.presets, cmd:filter.deletePreset, crates/ui-egui/src/panels/filterbar.rs filter bar → Presets: apply, save current filter, delete (right-click); saved with the library

E. Metadata (META)

Id Feature Tier Status Evidence Missing / notes
LR-META-INFO Info panel P0 ✅ cmd:panel.info, cmd:photo.setMeta, crates/ui-egui/src/panels/right.rs (info, camera_card) camera card (camera, lens, size, format, focal length / shutter / aperture / ISO); title, caption, alt text, extended description, copyright, copyright status, rights usage terms, copyright info URL, creator; file name (rename), file path (reveal), capture time (edit); location, city, state, country; GPS. No flash, map snippet or people
LR-META-COPYRIGHT-DEFAULT Default copyright on import P1 ✅ cmd:library.preferences (import.copyright, import.creator), crates/ui-egui/src/panels/settings.rs Settings → Import → Metadata; fills only photos without their own
LR-META-LOCATION Location editing P2 🟡 cmd:photo.setMeta (location, city, state, country, gps) place fields and GPS (decimal or degrees / minutes / seconds) edited in Info, read/written as IPTC Core / Photoshop / EXIF XMP; Show on Map opens OpenStreetMap; no embedded map, no geocoding
LR-META-COPYPASTE Copy / paste metadata P2 ✅ cmd:photo.copyMetadata, cmd:photo.pasteMetadata title, caption, alt text, extended description, copyright (notice, status, usage terms, info URL), creator, place fields, keywords; fields picks a subset
LR-META-XMP XMP read/write P0 ✅ cmd:photo.saveMetadataToFile, cmd:photo.readMetadataFromFile, cmd:library.xmpPreferences, crates/engine/src/sidecar.rs, docs/xmp-interop.md sidecar wins for metadata; a sidecar capture time (exif:DateTimeOriginal / photoshop:DateCreated / xmp:CreateDate) fills in only when the file has none; saving merges into an existing sidecar (crates/meta/src/xmp_merge.rs): another app's crs:, xmpMM:History and unknown namespaces are kept byte for byte, an unreadable sidecar is backed up first; with stem naming, files sharing a stem (raw + JPEG) get separate sidecars (<file>.xmp for all but the raw)
LR-META-EXIF-FULL Full EXIF/IPTC [Classic] P1 ✅ cmd:photo.allMetadata, cmd:dialog.allMetadata, crates/meta/src/tags.rs, crates/meta/src/exif.rs, crates/meta/src/iptc.rs read and written on export; Info ▸ All Metadata…: every TIFF / EXIF / GPS / interop tag (named, common values spelled out) and the XMP fields, searchable; metadata presets

F. Edit panel — global adjustments (EDIT)

Id Feature Tier Status Evidence Missing / notes
LR-EDIT-AUTO Auto settings P0 ✅ cmd:develop.auto, crates/pipeline/src/auto.rs
LR-EDIT-BW Black & white P0 ✅ cmd:develop.treatment
LR-EDIT-HDR-MODE HDR editing P2 ⬜
LR-EDIT-LIGHT-EXPOSURE Exposure P0 ✅ ctl:light.exposure
LR-EDIT-LIGHT-CONTRAST Contrast P0 ✅ ctl:light.contrast
LR-EDIT-LIGHT-HIGHLIGHTS Highlights P0 ✅ ctl:light.highlights
LR-EDIT-LIGHT-SHADOWS Shadows P0 ✅ ctl:light.shadows
LR-EDIT-LIGHT-WHITES Whites P0 ✅ ctl:light.whites
LR-EDIT-LIGHT-BLACKS Blacks P0 ✅ ctl:light.blacks
LR-EDIT-LIGHT-CURVE-PARAM Parametric curve P0 ✅ ctl:curve.highlights, ctl:curve.lights, ctl:curve.darks, ctl:curve.shadows, ctl:curve.split*
LR-EDIT-LIGHT-CURVE-POINT Point curve P0 ✅ cmd:develop.curve, cmd:curve.reset, cmd:curve.presets, cmd:curve.applyPreset, cmd:curve.savePreset, cmd:curve.deletePreset, cmd:curve.importPresets, cmd:curve.exportPresets, cmd:file.importCurvePresets, cmd:file.exportCurvePresets, crates/engine/src/cmd/curves.rs, crates/ui-egui/src/panels/edit.rs click to add a point; drag a point in both axes (between its neighbours, input / output readout, one undo step per drag); drag empty space to add and drag; double-click removes; reset: double-click a channel selector (that channel), the Reset button under the graph (every curve incl. parametric) or right-click the graph (channel / all); Point Curve preset dropdown: own Linear / Medium Contrast / Strong Contrast, user presets (save the current point curves, delete by right-click; saved with the library), import / export as .lccurve JSON
LR-EDIT-LIGHT-CURVE-RGB Per-channel curves P0 ✅ cmd:develop.curve (channel)
LR-EDIT-LIGHT-CURVE-REFINESAT Curve saturation compensation P1 ✅ ctl:curve.refineSaturation
LR-EDIT-LIGHT-CURVE-TAT Drag-on-image curve adjust P1 ✅ cmd:develop.targeted (target: curve)
LR-EDIT-COLOR-WB-PRESET White-balance presets P0 ✅ cmd:develop.wb
LR-EDIT-COLOR-WB-PICKER White-balance eyedropper P0 ✅ cmd:tool.wbPicker, cmd:develop.wbPick no magnified loupe while picking
LR-EDIT-COLOR-TEMP Temperature P0 ✅ ctl:wb.temp, crates/pipeline/src/local.rs (wb_matrix_for), crates/raw/src/color.rs (rebalance) relative scale for non-raw in the UI; raws with a colour matrix (DNG and other raws without a file-local look) are re-developed for the chosen white through the camera's own matrices (camera-space white balance, as Lightroom), and the eyedropper / Auto invert the same model; other photos are adapted (Bradford)
LR-EDIT-COLOR-TINT Tint P0 ✅ ctl:wb.tint, docs/tint-direction.md DNG / Lightroom sign: negative (left) adds green, positive (right) magenta; XMP crs:Tint values render as in Lightroom (issue #188); CPU/GPU regression tests
LR-EDIT-COLOR-VIBRANCE Vibrance P0 ✅ ctl:color.vibrance
LR-EDIT-COLOR-SATURATION Saturation P0 ✅ ctl:color.saturation
LR-EDIT-COLOR-MIXER-HSL 8-band colour mixer P0 ✅ ctl:mixer.* no targeted (drag-on-image) mode
LR-EDIT-COLOR-MIXER-BW B&W mix P1 ✅ ctl:bw.*, cmd:develop.autoBwMix eight bands; Auto pushes each hue band's colourful pixels away from the mean lightness (own rule)
LR-EDIT-COLOR-POINTCOLOR Point colour P1 ✅ cmd:pointColor.pick, cmd:pointColor.delete
LR-EDIT-COLOR-GRADING Colour grading wheels P0 ✅ ctl:grading.*
LR-EDIT-EFFECTS-TEXTURE Texture P0 ✅ ctl:effects.texture
LR-EDIT-EFFECTS-CLARITY Clarity P0 ✅ ctl:effects.clarity
LR-EDIT-EFFECTS-DEHAZE Dehaze P0 ✅ ctl:effects.dehaze
LR-EDIT-EFFECTS-VIGNETTE Post-crop vignette P0 ✅ ctl:vignette.*, crates/pipeline/src/finish.rs, crates/ui-egui/src/panels/edit.rs style picker (Highlight / Color / Paint) in the Effects section
LR-EDIT-EFFECTS-GRAIN Grain P1 ✅ ctl:grain.*
LR-EDIT-DETAIL-SHARPEN Sharpening P0 ✅ ctl:detail.sharpenAmount, ctl:detail.sharpenRadius, ctl:detail.sharpenDetail, ctl:detail.sharpenMasking no Alt-drag mask preview
LR-EDIT-DETAIL-NR Luminance noise reduction P0 ✅ ctl:detail.nrLuminance, ctl:detail.nrDetail, ctl:detail.nrContrast
LR-EDIT-DETAIL-CNR Colour noise reduction P0 ✅ ctl:detail.nrColor, ctl:detail.nrColorDetail, ctl:detail.nrColorSmoothness
LR-EDIT-DETAIL-DENOISE AI denoise P2 🟡 cmd:denoise.toggle, cmd:denoise.models.install, cmd:denoise.queue, ctl:enhance.denoise, crates/denoise/, docs/denoise.md non-destructive Bayer RAW, opt-in weights, pure-Rust CPU/GPU; X-Trans/linear RGB, model policy and Lightroom fidelity remain
LR-EDIT-DETAIL-RAWDETAILS Improved demosaic toggle P2 ⬜
LR-EDIT-DETAIL-SUPERRES Super resolution P2 ⬜
LR-EDIT-DETAIL-AISHARPEN AI sharpen OOS 🚫
LR-EDIT-OPTICS-CA Remove chromatic aberration P1 ✅ crates/ui-egui/src/panels/edit.rs (checkbox), ctl:optics.caRed, ctl:optics.caBlue
LR-EDIT-OPTICS-PROFILE Lens profile corrections P1 🟡 ctl:optics.profileDistortion, ctl:optics.profileVignetting, crates/pipeline/src/optics.rs, crates/raw/src/vendor/rw2.rs, crates/engine/src/crs.rs uses corrections embedded in the file: DNG WarpRectilinear / FixVignetteRadial, and the camera's own distortion correction in Panasonic / Leica RW2 / RWL files (tag 0x0119, read as the equivalent WarpRectilinear; on 79 photos from 13 bodies and 11 lenses the corrected render lines up with the camera JPEG within 0.75 px at 1440 px at the corners, from up to 13 % of the half diagonal uncorrected; issue #256); an imported preset's LensProfileEnable=0 leaves them on, as Lightroom applies built-in corrections regardless (inferred); vignetting and lateral CA data of other raw formats (Olympus, Fujifilm, Sony) not read yet; no lens-profile database
)
LR-EDIT-OPTICS-DEFRINGE Defringe P1 ✅ ctl:optics.defringe* no fringe eyedropper
LR-EDIT-OPTICS-MANUAL Manual distortion / vignetting P1 ✅ ctl:optics.distortion, ctl:optics.vignetting, ctl:optics.vignettingMidpoint
LR-EDIT-GEOM-UPRIGHT Upright P1 ✅ cmd:geometry.upright, cmd:geometry.guides
LR-EDIT-GEOM-MANUAL Manual transform P1 ✅ ctl:geometry.*
LR-EDIT-GEOM-CONSTRAIN Constrain crop P1 ✅ crates/ui-egui/src/panels/right.rs (checkbox → cmd:develop.merge)
LR-EDIT-GEOM-GRID Grid while transforming P2 ✅ crates/ui-egui/src/panels/detail.rs a fine grid over the photo while a geometry slider is dragged
LR-EDIT-LENSBLUR Lens blur P2 ⬜ settings field reserved, not rendered
LR-EDIT-CALIB Calibration [Classic] P1 ✅ ctl:calibration.* shadows tint, red/green/blue primary hue and saturation; read/written in XMP
LR-EDIT-SECTION-TOGGLE Section on/off P1 ✅ cmd:develop.sectionEnabled, crates/develop/src/lib.rs (effective) the eye on a section header hides its adjustments: Light (with the curve), Color (white balance, presence, mixers, grading, point colour), Effects, Detail, Optics, Geometry, Calibration; masks stay on (issue #316 fixed Light, Color and Detail, which had no effect)
LR-EDIT-RESET Reset all / section / slider P0 ✅ cmd:develop.reset, cmd:develop.resetSection, cmd:develop.resetControl, crates/ui-egui/src/widgets.rs (double-click) no "reset to open"
LR-EDIT-SHOWORIG Show original P0 ✅ cmd:view.showOriginal

G. Profiles (PROF)

Id Feature Tier Status Evidence Missing / notes
LR-PROF-DROPDOWN Profile menu P0 ✅ cmd:develop.profile, cmd:profiles.list, cmd:profiles.menu, cmd:profile.favorite, crates/ui-egui/src/panels/edit.rs Favorites, Recent (last 5), group submenus, favourite toggle; favourites/recent persist with the library; Amount slider under the menu for non-default profiles; Browse… opens the profile browser; resting on an entry previews it in the loupe
LR-PROF-BROWSER Profile browser P1 ✅ cmd:panel.profiles, crates/ui-egui/src/panels/profiles.rs, crates/engine/src/media.rs grid of live variant thumbnails of the current photo per group (Favorites first), star toggles favourites, amount slider, hover = loupe preview (no history), click applies
LR-PROF-ADOBE Standard raw looks (own equivalents) P0 ✅ crates/engine/src/presets.rs (PROFILES), crates/pipeline/src/profiles.rs six own looks: Color, Neutral, Vivid, Landscape, Portrait, Monochrome
LR-PROF-ADAPTIVE Adaptive profiles P2 ⬜
LR-PROF-CAMERA Camera-matching looks P2 ⬜
LR-PROF-CAMERACOLOR Camera colour calibration (own) P0 🟡 crates/raw/src/color.rs, crates/raw/src/profile.rs, crates/raw/src/gaintable.rs, crates/engine/src/camera_preview.rs DNG files use the colour matrices and the camera-profile look they carry (ProfileHueSatMap, ProfileLookTable, ProfileToneCurve, read from the file at run time per DNG spec ch. 6 — Lightroom-converted DNGs rendered flat and muted without them, issue #138). A DNG's ProfileGainTableMap / ProfileGainTableMap2 (Apple ProRAW's local tone map; both versions, DNG 1.7 precedence, the file's byte order with a big-endian fallback) is read and written back by DNG export and not rendered by default, as Lightroom Classic renders ProRAW without it (measured on one CC0 iPhone 12 Pro ProRAW); the Profile option "Camera local tone mapping" (profile.camera_local_tone, per photo, off by default, crates/pipeline/src/local_tone.rs) renders it as the camera does, per output pixel in the scene-linear stage (CPU, also under the GPU path); Sony ARW, Nikon NEF, Panasonic RW2, Fujifilm RAF, Canon CR3 and CR2, Pentax PEF and Samsung SRW get a guarded file-local fit to their own embedded JPEG — chromaticity matrix + hue/saturation/value table + tone and chroma curves, relative WB — or, with a bundled or local per-model profile pooled from many photos (lightcraft-cli calibrate, ARW, NEF and RAF so far, crates/engine/src/camera_profiles.rs), the profile's colour and only their own tone/chroma curves (on 29 held-out ILCE-7M4 photos mean ΔE vs the camera JPEG 3.54 → 3.10), kept with smart previews (see docs/camera-preview-colour.md); ARW: on 10 public raw.pixls.us samples from 8 bodies 7 fits were accepted, mean ΔE vs the camera JPEG 17–26 → 3–10; NEF: 13 of 13 decodable samples (D750, D780, D850, D7500, Z 50 and local D7500 shots) accepted, ΔE 13–46 → 3–8 (issue #150); RW2 / RWL: 140 of 174 public samples accepted, from 93 of the 114 bodies with a preview, median ΔE 5.0 with matching lightness (13 of the 14 corpus files with a preview: ΔE 9.6–27 → 2.8–8.9; most rejections are compacts and kit zooms whose camera JPEG is distortion-corrected); a fit that fails the gates on all pixels is retried away from edges, where a lens-corrected camera JPEG still lines up (issue #232: the six distortion-corrected public ILCE-7RM2 samples ΔE 16.6–16.9 → 3.7–3.8, the corpus GX80 19.1 → 7.9); RAF: bundled X-H2S/X-T4 profiles from 201/545 photos; on 150 withheld files mean ΔE improves slightly over per-file fitting (X-H2S 1.70 → 1.62, X-T4 2.43 → 2.34); 23 of 24 references from 13 bodies accepted, including the dark X-H2S samples (8.74 → 2.25) (docs/camera-preview-colour.md); this is not measured camera calibration; other raws and rejected fits use a neutral fallback (camera RGB ≈ linear sRGB, matrix_is_fallback) with as-shot white balance, so colours are muted and not accurate. Needs our own per-camera calibration: matrices the files carry themselves (e.g. Olympus ColorMatrix), fitting each camera to its own embedded JPEG, then chart shots. Adobe matrices / DCPs are never used. Biggest image-quality gap today
LR-PROF-CREATIVE Creative profiles (own) P2 ✅ cmd:develop.profile, crates/pipeline/src/profiles.rs 16 own looks in Film / Cinematic / Muted / B&W (tone + point-curve fades, colour grading, mixer / B&W mix); scale with ctl:profile.amount; sliders untouched
LR-PROF-LEGACY Legacy profiles P2 ⬜
LR-PROF-NONRAW Profiles for non-raw files P0 ✅ cmd:develop.profile same looks apply to JPEG/TIFF
LR-PROF-AMOUNT Profile amount P1 ✅ ctl:profile.amount
LR-PROF-IMPORT Import profiles P1 ✅ cmd:profile.import, cmd:profile.deleteImported, crates/pipeline/src/lut.rs, crates/engine/src/cmd/lut_profiles.rs, crates/ui-egui/src/panels/edit.rs, crates/ui-egui/src/panels/profiles.rs .cube 3D LUTs (files, folders, zips) become creative profiles with Amount, grouped in the profile browser, kept with the library; rendered on the CPU; Adobe profile formats deliberately unsupported

H. Crop & rotate (CROP)

Id Feature Tier Status Evidence Missing / notes
LR-CROP-RECT Crop rectangle P0 ✅ cmd:crop.set, cmd:crop.drag, crates/geom/src/crop_drag.rs, crates/ui-egui/src/panels/detail.rs double-click inside the box applies the crop (like Return)
LR-CROP-ASPECT Aspect ratios P0 ✅ cmd:crop.aspect, cmd:crop.rotateAspect, crates/ui-egui/src/panels/right.rs menu with presets + custom W × H, visible Lock toggle (also A), button names the active ratio; every handle honours the lock and the drag stops at the image edge instead of shifting the frame (issue #295); "Original" is the photo's own shape (no separate As Shot)
LR-CROP-STRAIGHTEN Straighten tool P0 ✅ cmd:crop.straighten, cmd:crop.autoStraighten Straighten Tool button: drag along a horizon/vertical; double-click or Auto levels automatically
LR-CROP-AUTO Auto straighten P1 ✅ cmd:crop.autoStraighten crop-angle leveling from detected horizon/plumb lines (consensus required)
LR-CROP-ANGLE Angle slider P0 ✅ ctl:crop.angle
LR-CROP-ROTATE90 Rotate 90° P0 ✅ cmd:photo.rotateLeft, cmd:photo.rotateRight
LR-CROP-FLIP Flip P0 ✅ cmd:photo.flipHorizontal, cmd:photo.flipVertical
LR-CROP-OVERLAY Crop overlays P1 ✅ cmd:view.cropOverlay, cmd:view.cropOverlayOrientation, crates/ui-egui/src/panels/crop_overlay.rs thirds, grid, golden ratio, diagonal, triangle, golden spiral (mirrored with ⇧O while cropping); no aspect-ratio overlays
LR-CROP-ZOOM Zoom while cropping P1 ✅ cmd:view.zoom100, cmd:view.zoomIn, crates/ui-egui/src/panels/detail.rs zoom levels apply with the crop tool open (verified: 100% shows the photo at native size with the crop frame)
LR-CROP-GENEXPAND Generative expand OOS 🚫

I. Remove / healing (REM)

Id Feature Tier Status Evidence Missing / notes
LR-REM-CONTENTAWARE Content-aware remove P1 🟡 cmd:spot.add (mode: remove), crates/pipeline/src/spots.rs heal with automatic source; no patch synthesis (M8.4)
LR-REM-HEAL Heal P0 ✅ cmd:spot.add (mode: heal)
LR-REM-CLONE Clone P0 ✅ cmd:spot.add (mode: clone)
LR-REM-GEN Generative remove OOS 🚫
LR-REM-DETECT Object detection for remove P2 ⬜
LR-REM-BRUSH-PARAMS Brush size / feather / opacity P0 ✅ cmd:spot.add (size, feather, opacity), cmd:brush.smaller, cmd:brush.larger, cmd:brush.featherLess, cmd:brush.featherMore, crates/ui-egui/src/panels/right.rs Size / Feather / Opacity sliders (for new spots and the selected one); [ ] size, ⇧[ ⇧] feather (also the Masking brush)
LR-REM-SPOT-EDIT Edit existing spots P0 ✅ cmd:spot.select, cmd:spot.update, cmd:spot.refreshSource, cmd:spot.delete click a pin to select, drag target or source, ⌫ deletes the selected spot, / picks another source; automatic sources are resolved when the spot is added
LR-REM-VISUALIZE Visualize spots P1 ✅ cmd:view.visualizeSpots
LR-REM-PEOPLE Remove people (generative) OOS 🚫
LR-REM-REFLECT Remove reflections P2 ⬜
LR-REM-DUST Dust detection P2 ✅ cmd:spot.findDust, crates/pipeline/src/dust.rs Remove panel ▸ Find Dust Spots: small, soft, round dark spots on smooth areas become heal spots (sensitivity; one undo step); Visualize Spots for checking by eye
LR-REM-SYNC Sync spots P1 ✅ cmd:develop.copy (groups), crates/develop/src/presets.rs

J. Red eye (EYE)

Id Feature Tier Status Evidence Missing / notes
LR-EYE-RED Red-eye correction P1 ✅ cmd:panel.redeye, cmd:redeye.add, cmd:redeye.delete, cmd:redeye.catchlight red eye and pet eye (with catchlight)
LR-EYE-PET Pet eye P2 ✅ cmd:redeye.add (pet), cmd:redeye.catchlight

K. Masking (MASK)

Id Feature Tier Status Evidence Missing / notes
LR-MASK-PANEL Masks panel P0 ✅ cmd:panel.masking, cmd:mask.add, cmd:mask.select, cmd:mask.rename, cmd:mask.duplicate (invert), cmd:mask.move, cmd:mask.visible, cmd:mask.delete, crates/ui-egui/src/panels/masking.rs list with per-mask show/hide eye, double-click rename, right-click menu (duplicate, duplicate and invert, invert, hide, move up/down, rename, delete); fits any panel width (create tiles go to three columns, mask actions wrap, long component names are cut short); no drag-to-reorder
LR-MASK-SUBJECT Select subject P2 🟡 cmd:mask.add (subject), crates/pipeline/src/masks.rs saliency heuristic, no segmentation model; a photo that carries person mattes (DNG semantic masks: iPhone ProRAW portrait / skin / hair / teeth / glasses mattes) uses them instead (synthetic files only: no corpus file carries person mattes)
LR-MASK-SKY Select sky P2 🟡 cmd:mask.add (sky), crates/pipeline/src/masks.rs (Mattes), crates/raw/src/semantic.rs heuristic; a photo that carries a sky matte (DNG semantic mask, e.g. iPhone ProRAW) uses it instead, through orientation / crop / lens corrections, preview = export (checked on one CC0 iPhone 12 Pro ProRAW and synthetic files)
LR-MASK-BACKGROUND Select background P2 🟡 cmd:mask.add (background) inverse of the subject mask (heuristic, or the photo's person mattes)
LR-MASK-OBJECTS Object selection P2 🟡 cmd:mask.add (object, prompt), cmd:mask.objectPoint, cmd:mask.refineDetail, cmd:segment.prepare, cmd:segment.model.status, cmd:segment.model.download, cmd:segment.model.cancel, crates/segment, crates/engine/src/segment/mod.rs SAM 3 in pure Rust (candle; Metal on macOS, CPU elsewhere): Object tile → click to include, ⌥-click to leave out; Describe tile → a text prompt selects every instance ("sky", "the red car"); both also as Add/Subtract/Intersect components; + / − under the selected mask; comma lists (car, road); a zoomed-in detail pass for 5–10× finer edges on small objects; per-selection Edge (hard ↔ soft). The model runs on its own worker thread (the UI never waits; panics become errors), is unloaded after 10 min idle, and is optional: the segmentation is stored with the mask (288² logits), so renders and exports never need it. Weights are never bundled (SAM License): without them the app offers a consented background download (mirrors, resume, timeouts, SHA-256) — but no default download location is configured yet (users set LIGHTCRAFT_SAM3_MIRRORS or install by hand: the dialog names the three files, shows the model folder and links docs/ai-masks.md). Not verified against Lightroom's Select Object; no brush/box object mode; the first click on a photo waits for its analysis (~4 s on an M4 Pro, much longer on CPU)
LR-MASK-PEOPLE People parts P2 ⬜ shape exists (no UI): uses the photo's person / skin / hair / teeth / glasses mattes when it carries them (DNG semantic masks), else the subject heuristic
LR-MASK-LANDSCAPE Landscape classes P2 ⬜ shape exists, evaluates empty
LR-MASK-BRUSH Brush mask P0 ✅ cmd:tool.brush, cmd:mask.brushStroke (autoMask), crates/pipeline/src/masks.rs size/feather/flow/density/erase; Auto Mask: dabs weighted by similarity to the colour under the dab centre, refined by a guided filter on luminance (CPU + GPU); no A/B brushes, no pressure
LR-MASK-LINEAR Linear gradient P0 ✅ cmd:tool.linear, cmd:mask.update
LR-MASK-RADIAL Radial gradient P0 ✅ cmd:tool.radial, cmd:mask.update, crates/ui-egui/src/panels/detail.rs drag the selected ellipse's body or centre pin to move; edge grips resize, outer grip rotates; rotated and compound radial components, hidden pins, outside hit rejection and one-step undo tested
LR-MASK-COLORRANGE Colour range P1 ✅ cmd:mask.add (colorRange), cmd:mask.sampleColor, cmd:mask.update, crates/pipeline/src/lib.rs (color_range_sample) Color tile → click the photo to sample (⇧-click adds, up to 5; samples taken in the space the mask compares in), Pick button, Refine slider
LR-MASK-LUMRANGE Luminance range P1 ✅ cmd:mask.add (luminanceRange), cmd:mask.update, crates/ui-egui/src/panels/masking.rs (range_controls) range bar with two handles (one undo step per drag), Smoothness, Show Luminance Map (B&W photo with the range tinted)
LR-MASK-DEPTHRANGE Depth range P2 ⬜ shape exists, needs depth data
LR-MASK-COMBINE Add / subtract / intersect P0 ✅ cmd:mask.addComponent
LR-MASK-INVERT Invert P0 ✅ cmd:mask.invert
LR-MASK-AMOUNT Mask amount P1 ✅ cmd:mask.adjust (amount), crates/ui-egui/src/panels/masking.rs
LR-MASK-FEATHER-EDGE Refine mask edges P2 ✅ cmd:mask.refine, crates/pipeline/src/masks.rs (evaluate_one) Refine Edges slider per mask: guided filter on the photo's luminance snaps soft mask edges to the photo's edges (rendered on the CPU)
LR-MASK-SLIDERS Local adjustment sliders P0 ✅ cmd:mask.adjust, crates/pipeline/src/finish.rs every slider renders (CPU + GPU), incl. Noise, Moiré, Defringe (negative Defringe has no effect); no local curve or effect presets
LR-MASK-OVERLAY Mask overlay P0 ✅ cmd:view.maskOverlay, cmd:view.maskOverlayMode, cmd:view.maskOverlayColor, crates/pipeline/src/visualize.rs rendered alpha of the selected mask (CPU + GPU): colour, colour on B&W, image on black/white, white on black; ⇧O cycles while masking; no auto-show on hover
LR-MASK-PINS Pins P1 ✅ cmd:view.maskPins, crates/ui-egui/src/panels/detail.rs a pin per radial/linear/brush component: click selects its mask, drag moves it; no pins for range/AI components, no Auto mode
LR-MASK-UPDATE Recompute AI masks P2 ⬜
LR-MASK-SYNC Copy masks to other photos P1 ✅ cmd:develop.copy (groups)
LR-MASK-ADAPTIVE Adaptive presets P2 ⬜

L. Presets (PRE)

Id Feature Tier Status Evidence Missing / notes
LR-PRE-PANEL Presets panel P0 ✅ cmd:panel.presets, cmd:preset.apply, crates/ui-egui/src/panels/presets.rs grouped list with amount; resting on a preset previews it in the loupe (no history entry); optional live thumbnails (⋯ → Show Thumbnails)
LR-PRE-CREATE Create preset P0 ✅ cmd:dialog.createPreset, cmd:preset.create (groups) name, group and a checklist of settings groups (crop, masks, remove, red eye off by default; All / None)
LR-PRE-MANAGE Manage presets P1 ✅ cmd:preset.delete, cmd:preset.favorite, cmd:preset.import, cmd:preset.export, cmd:preset.rename, cmd:preset.update, cmd:preset.move rename, update with current settings, move to a group (existing or new); no hiding of groups
LR-PRE-AMOUNT Preset amount P1 ✅ cmd:preset.apply (amount 0–200)
LR-PRE-ADAPTIVE Adaptive presets P2 ⬜
LR-PRE-PREMIUM Built-in presets (own) P2 ✅ crates/engine/src/presets.rs 41 own-authored presets in 10 groups (Color, Film, B&W incl. toners, Portrait, Landscape, Urban, Food, Seasons, Vintage, Style)
LR-PRE-RECOMMENDED Community recommendations OOS 🚫
LR-PRE-ONIMPORT Apply during import P2 ✅ cmd:library.import (preset) chosen in the import review; raw / per-camera defaults in Settings

M. Versions & history (VER)

Id Feature Tier Status Evidence Missing / notes
LR-VER-CREATE Create version P1 ✅ cmd:version.create
LR-VER-PANEL Versions panel P1 ✅ cmd:panel.versions, cmd:version.create, cmd:version.restore, cmd:version.delete, cmd:version.rename, cmd:version.update, crates/ui-egui/src/panels/right.rs (versions) Named / Auto tabs, a thumbnail of each version's look, name and date, current marker; resting on a version previews it in the loupe; click restores, double-click renames; row menu (update, rename, set as before, delete). No automatic versions are created yet (LR-VER-AUTO)
LR-VER-AUTO Automatic versions P2 ✅ crates/engine/src/lib.rs (auto_version) leaving an edited photo keeps its settings as an Auto version (when they differ from the latest version; 20 kept; not an undo step); Versions panel ▸ Auto
LR-VER-HISTORY Edit history P1 ✅ cmd:panel.activity, cmd:history.list, cmd:history.restore
LR-VER-UNDO Undo / redo P0 ✅ cmd:edit.undo, cmd:edit.redo

N. Copy / paste / sync (SYNC)

Id Feature Tier Status Evidence Missing / notes
LR-SYNC-COPY Copy edit settings P0 ✅ cmd:develop.copy
LR-SYNC-CHOOSE Choose settings to copy P0 ✅ cmd:dialog.copySettings groups are coarser than per-slider
LR-SYNC-PASTE Paste to selection P0 ✅ cmd:develop.paste no separate "paste selected" (choose at copy time instead)
LR-SYNC-SYNCBTN Sync active → selected P1 ✅ cmd:develop.sync
LR-SYNC-PREVIOUS Paste from previous / auto sync [Classic] P2 ✅ cmd:develop.pastePrevious, cmd:develop.autoSync ⌥⌘V pastes the previously active photo's settings (copy groups); Auto Sync (⌥⇧⌘A)

O. Merge (MERGE)

Id Feature Tier Status Evidence Missing / notes
LR-MERGE-HDR HDR merge P2 ✅ cmd:merge.hdr, cmd:dialog.mergeHdr auto align, deghost None–High + overlay, auto settings, float DNG, Create Stack; JPEG brackets treated as linear
LR-MERGE-PANO Panorama P2 ✅ cmd:merge.panorama, cmd:dialog.mergePanorama spherical/cylindrical/perspective + auto, boundary warp, auto crop, fill edges (diffusion), DNG; no lens model / 360° wrap
LR-MERGE-HDRPANO HDR panorama P2 ✅ cmd:merge.hdrPanorama, cmd:dialog.mergeHdrPanorama brackets grouped by EXIF
LR-MERGE-HEADLESS Merge with last settings P2 ✅ cmd:merge.hdrLast, cmd:merge.panoramaLast, cmd:merge.hdrPanoramaLast no dialog; the options of the last merge of that kind (defaults the first time)

P. Enhance (ENH)

Id Feature Tier Status Evidence Missing / notes
LR-ENH-DIALOG Enhance dialog P2 ⬜
LR-ENH-INPLACE In-place enhance P2 ⬜

Q. HDR (HDR)

Id Feature Tier Status Evidence Missing / notes
LR-HDR-EDIT HDR editing P2 ⬜
LR-HDR-SDRPREVIEW SDR preview of HDR P2 ⬜
LR-HDR-VISUALIZE Visualize HDR range P2 ⬜
LR-HDR-LIMIT HDR headroom limit P2 ⬜
LR-HDR-EXPORT HDR export P2 ⬜

R. Video (VID)

Id Feature Tier Status Evidence Missing / notes
LR-VID-PLAY Video playback P1 ⬜ crates/catalog/src/model.rs videos can be catalogued (kind, duration); no player
LR-VID-TRIM Trim P1 ⬜
LR-VID-EDIT Edits on video P2 ⬜
LR-VID-COVER Cover frame P2 ⬜
LR-VID-EXPORT Video export P2 ⬜
LR-VID-PHOTO2VIDEO Photo to video (generative) OOS 🚫

S. Export (EXP)

Id Feature Tier Status Evidence Missing / notes
LR-EXP-DIALOG Export dialog P0 ✅ cmd:dialog.export, cmd:app.export (preset), cmd:export.presets, cmd:export.savePreset, cmd:export.deletePreset, crates/ui-egui/src/panels/dialogs.rs, crates/engine/src/export.rs batch export; built-in presets (JPEG Small 2048 px / Large full size, Original + Settings, DNG) and saved user presets (library prefs) load into the dialog; deleting a user preset is command-only
LR-EXP-TYPE File types P0 ✅ cmd:app.export (format), crates/engine/src/export.rs (ExportFormat), crates/engine/src/tests_export.rs JPEG, PNG, TIFF, WebP, AVIF, DNG (raw photos: lossless re-encode with the edits in the embedded XMP), Original (+ XMP sidecar). No JXL encoder; non-raw → DNG not supported
LR-EXP-DIM Output size P0 ✅ cmd:app.export (longEdge, shortEdge, width, height, megapixels, percent, dontEnlarge, ppi), crates/engine/src/export.rs (Resize), crates/ui-egui/src/panels/dialogs.rs full size = the cropped native size (no longer upscaled); W × H fits either orientation; ppi written to JFIF / pHYs / TIFF tags
LR-EXP-QUALITY JPEG quality P0 ✅ cmd:app.export (quality, limitKb)
LR-EXP-BITDEPTH Bit depth P1 ✅ cmd:app.export (bitDepth), crates/pipeline/src/output.rs (OutputDepth), crates/engine/src/export.rs 16-bit PNG/TIFF rendered at 16 bits (TIFF defaults to 16), 32-bit float linear TIFF with a linear profile, 10-bit AVIF; high-bit-depth renders run on the CPU
LR-EXP-COMPRESSION TIFF compression P1 ✅ cmd:app.export (tiffCompression: none / lzw / zip)
LR-EXP-COLORSPACE Output colour space P0 ✅ cmd:app.export (colorSpace), crates/pipeline/src/output.rs, crates/engine/src/export.rs sRGB, Display P3, Adobe RGB (1998) compatible, ProPhoto RGB, Rec. 2020: rendered from the working space with gamut mapping into the target gamut (CPU + GPU), own ICC profile embedded; AVIF stays sRGB (muxer has no ICC)
LR-EXP-HDR HDR output P2 ⬜
LR-EXP-SHARPEN Output sharpening P1 ✅ cmd:app.export (sharpen, sharpenAmount)
LR-EXP-METADATA Metadata policy P1 ✅ cmd:app.export (metadata, removeLocation)
LR-EXP-WATERMARK Watermark P1 ✅ cmd:app.export (watermark: text, or {text, vertical, size, opacity, anchor, inset, color, shadow, image, imageWidth}), crates/engine/src/export.rs (Watermark, WATERMARK_PARAMS) text (size = height as a fraction of the short edge, 0.005..0.5, default 0.035; color sRGB [r,g,b]; shadow; Japanese vertical glyph forms and origins, tested by biz_ud_vertical_watermarks_place_punctuation_at_the_top_right; extended grapheme cells keep decomposed dakuten/handakuten and Latin accents together, tested by vertical_watermarks_keep_combining_marks_in_one_cell and upright_watermark_cell_retains_multiple_glyphs (see docs/vertical-watermarks.md)) or a graphic with transparency (image path, imageWidth = fraction of the photo's width, 0.01..1, converted to the output colour space); anchor, inset (fraction of the short edge, 0..0.4), opacity 0..1. Unknown keys and out-of-range values are errors (issue #183)
LR-EXP-NAMING File naming P1 ✅ cmd:app.export (naming, startNumber), crates/engine/src/rename.rs (expand_tokens) one template language for export, Rename Photos and import renaming: {name}, {num} (number at the end of the name), {seq} / {seq:N}, {date} / {date:%Y-%m-%d}, {folder}, {camera}, {lens}, {iso}, {rating}, {title}, {creator}, {ext}; free-form template rather than a list of named schemes
LR-EXP-LOCATION Destination folder P0 ✅ cmd:app.export (dir, subfolder, conflict: unique / overwrite / skip), cmd:export.checkTarget, crates/ui-egui/src/panels/dialogs.rs (Choose… folder picker), crates/engine/src/originals.rs, crates/catalog/src/safe_file.rs folder picker on the desktop; existing files get -2, -3… by default; the policy covers a file and its sidecars together; a catalogued original (or its sidecar) is never written over, whatever the policy or an exact path (export, ui.render, MCP render_photo, merge previewPath, lightcraft-cli render); files are written to a temp file and renamed into place (issue #93; not synced file by file — exports can be made again, issue #134)
LR-EXP-PREVIOUS Export with previous settings P0 ✅ cmd:app.exportPrevious, cmd:dialog.export last options persist in prefs.json; dialog prefilled; no named export presets yet
LR-EXP-DNGOPT DNG options P2 🟡 cmd:app.export (dngCompression), crates/engine/src/export.rs compression: lossless JPEG (default), ZIP or none; no embedded JPEG preview size, no lossy DNG output
LR-EXP-ORIGINAL Original + XMP P1 ✅ cmd:app.export (format: original), crates/engine/src/tests_export.rs file copied byte for byte, sidecar named after the output and subject to the conflict policy
LR-EXP-PHOTOS Export to the system photo library P2 ⬜
LR-EXP-PSD Round trip to an external editor P2 ✅ cmd:photo.editExternal, cmd:photo.editInExternal, crates/engine/src/cmd/convert.rs a 16-bit TIFF -Edit copy with the edits (Adobe RGB / ProPhoto / P3 / sRGB) next to the original, added stacked on top of it and opened in the editor set in Settings ▸ General (or the system default)

T. Share (SHARE)

Id Feature Tier Status Evidence Missing / notes
LR-SHARE-LINK Web share link OOS 🚫
LR-SHARE-INVITE Invite collaborators OOS 🚫
LR-SHARE-WEBGALLERY Web galleries OOS 🚫
LR-SHARE-COMMUNITY Community edits OOS 🚫

U. Map & location (MAP)

Id Feature Tier Status Evidence Missing / notes
LR-MAP-INFO Location in the info panel P1 🟡 cmd:photo.setMeta (location, city, state, country, gps), crates/ui-egui/src/panels/right.rs location, city, state/province, country and GPS editable; Show on Map (OpenStreetMap in the browser); no map in the panel
LR-MAP-MODULE Map module [Classic] P2 ⬜

V. Preferences (PREF)

Id Feature Tier Status Evidence Missing / notes
LR-PREF-GENERAL General settings P0 ✅ cmd:app.settings, cmd:app.openLibrary, crates/ui-egui/src/panels/settings.rs, crates/ui-egui/src/panels/notices.rs General / Import / Performance / Interface tabs; app settings in ui.json (written atomically, saved when the library changes and every few seconds), library settings in prefs.json; a damaged settings file is kept as .corrupt-<time> and reported, an unreadable one is not overwritten that session (library.info → settingsWarnings); quitting with unsaved changes retries, then asks
LR-PREF-LOCALSTORAGE Storage & cache P1 ✅ cmd:library.preferences, cmd:library.clearPreviews, cmd:library.compact thumbnail cache size + clear in Settings → Performance; library location + Open Library… in General
LR-PREF-ACCOUNT Account OOS 🚫
LR-PREF-INTERFACE Interface options P1 ✅ cmd:app.settings filmstrip names/badges, grid badges (auto/always/never), square-grid names, navigator, info overlay
LR-PREF-PERFORMANCE GPU / performance P1 ✅ cmd:app.gpu, cmd:app.memoryBudget, cmd:app.settings GPU on/off, preview size (Automatic = the whole-frame loupe render at canvas size, up to 2560 px; or a 1600–5120 px size for it; zoomed views add a window at up to 100 %, issue #323), memory budget (a quarter of it is the idle per-view stage caches' share; the open photo's views are counted and reported, and cut only past four times that, the before side's and the zoom window's first — a Before/After at 1:1 on a 2800 px canvas holds about 1 GB of GPU stages, plus the one 288 MB copy of the original, which ui.inspect reports as sharedSourceBytes), thumbnail cache size in Settings
LR-PREF-PEOPLE Face recognition P2 🟡 cmd:faces.enable, cmd:faces.models.list, cmd:faces.models.inspect, cmd:faces.models.install, cmd:faces.models.download, cmd:faces.models.downloads, cmd:faces.models.downloadCancel, cmd:faces.models.remove, cmd:faces.models.select, cmd:dialog.faceModel, crates/faces/src/lib.rs, crates/ui-egui/src/panels/faces.rs Settings ▸ Faces: an off-by-default toggle and the face model list. Models are opt-in and never bundled, the 232 KB YuNet detector included. Press Download (terms first; then a pinned address, size and SHA-256 checked, fetched over pure-Rust https with lightcraft-fetch), or add any .onnx by picking or dropping it: it is recognised by hash or described from its shape, shows its licence terms and training-data note, runs a self-test, and installs only after an "I accept" box; an installed model is chosen and recognition switched on at once (the newest wins, each model keeps its own embedding cache). Non-commercial models are add-by-file only, never offered for download. Suggestions of names run on the shared pure-Rust CPU interpreter (crates/faces/src/net.rs, crates/faces/src/runtime.rs; bounded im2col/GEMM, checked ONNX shapes/operators; no external inference runtime; 40 public crops agree within 2.1e-6, one-thread SFace 34–36 ms / AuraFace 233–252 ms, see docs/faces.md) in a background worker and are never applied automatically. Thresholds are starting points calibrated on busts, not yet on live faces
LR-PREF-WATERMARK Watermark settings P1 ✅ cmd:export.savePreset, crates/ui-egui/src/panels/dialogs.rs set in the Export dialog; kept with Export with Previous and in saved export presets
LR-PREF-SHORTCUTS Shortcut customisation — ✅ cmd:app.setShortcut, cmd:app.resetShortcuts, crates/ui-egui/src/panels/keymap.rs an extra (the reference app has no keymap editor): Help ▸ Keyboard Shortcuts (⌘/) lists every command; click a shortcut and press the new keys (Esc cancels), × removes, ↺ restores, Reset All; a key another command had moves over; saved in ui.json; native menu accelerators follow. Rating/label digits and secondary keys are listed read-only and yield to a key given to a command
LR-PREF-TECHPREVIEW Early-access toggles P2 ⬜
LR-PREF-NOTIFICATIONS Notifications OOS 🚫
LR-PREF-DEVICE Device settings P2 ⬜

W. Cloud & AI infrastructure (CLOUD / AI)

Id Feature Tier Status Evidence Missing / notes
LR-CLOUD-SYNC Cloud sync OOS 🚫
LR-CLOUD-SMARTPREVIEW Editable proxies P2 ✅ cmd:library.smartPreviews, cmd:library.smartPreviewsLocation, cmd:photo.smartPreview, crates/engine/src/smart.rs, crates/ui-egui/src/panels/settings.rs File ▸ Previews ▸ Build / Discard Smart Previews (on a worker thread, progress and Stop like Build Previews): ~1 MB proxies in the library; with the original offline the photo renders, edits and exports (at proxy size) from its proxy; Info shows the status; the proxy folder is chosen per library (Settings → Performance → Smart previews, saved in the library): the effective path, count and size are shown, changing it needs a choice for the proxies already built (move / leave / delete), and an unavailable or unwritable folder is an error, never a fallback to the library drive; proxies are written atomically and a damaged one (cut short by a crash or a full drive) doesn't count as built, is rebuilt (repaired) and is never moved over a good one (issue #106). The thumbnail cache stays in the library
LR-AI-UPDATE-INDICATOR AI-settings update indicator P2 ⬜
LR-AI-CREDITS Generative credits OOS 🚫

X. Cross-cutting behaviours (BEHAV)

Id Feature Tier Status Evidence Missing / notes
LR-BEHAV-AUTOSAVE Instant autosave P0 ✅ crates/catalog/src/journal.rs, crates/engine/src/library.rs every change fsynced before the command returns; a failed write fails the command ("saved in memory but not written to disk"), keeps the change queued and retries; top-bar warning until saved; a write that fails part-way is cut back to the last whole record so the retry never reads back as damage (logs from older builds with such a fragment load in full); quitting saves queued changes in the closing snapshot even when the log can't be appended to; versioned catalog format (journal::VERSION): older libraries are upgraded on open, a library from a newer LightCraft is refused untouched ("written by a newer version")
LR-BEHAV-UNDO Global undo P0 ✅ cmd:edit.undo, crates/engine/src/tests.rs (rating_flag_undo_redo) covers ratings, albums, deletes, edits
LR-BEHAV-MULTISELECT Multi-selection P0 ✅ cmd:library.select (replace/add/toggle/range), cmd:library.selectAll
LR-BEHAV-BATCH Batch apply to selection P0 ✅ cmd:photo.rate, cmd:develop.paste, cmd:preset.apply, cmd:app.export
LR-BEHAV-PREVIEW-HOVER Hover previews P1 ✅ crates/ui-egui/src/panels/presets.rs, crates/ui-egui/src/panels/profiles.rs, crates/ui-egui/src/panels/right.rs (versions), crates/ui-egui/src/panels/detail.rs presets, profile menu, profile browser and versions preview in the loupe
LR-BEHAV-PROGRESSIVE Progressive rendering P0 ✅ crates/engine/src/media.rs, crates/preview/src/lib.rs
LR-BEHAV-BG-TASKS Background tasks P0 ✅ crates/preview/src/lib.rs (JobPool), crates/ui-egui/src/export_task.rs, crates/ui-egui/src/import.rs renders off the UI thread; exports started from the UI run on a worker thread with a progress panel and Cancel (ui.inspect → export); imports show a progress window. No combined activity centre
LR-BEHAV-OFFLINE Offline editing P1 ✅ local-first: everything works offline
LR-BEHAV-WEB-SAFETY Library safety in the browser build P1 🟡 cmd:file.backupLibrary, cmd:file.restoreLibrary, apps/lightcraft-web/src/safety.rs, apps/lightcraft-web/src/backup.rs, apps/lightcraft-web/src/files.rs web only (experimental): Back Up Library (zip of the catalog and every stored original) and a non-destructive Restore; failed saves show as unsaved and are retried; a photo that can't be stored isn't added; one tab per library (Web Locks); notices for storage that isn't persistent and for a library that can't open; ?reset asks first; a panic shows a message. Missing: ?safe start, deleting originals of removed photos, zip64 (backups > 4 GB)
LR-BEHAV-GPU GPU acceleration P0 ✅ crates/gpu/src/render.rs, cmd:app.gpu, docs/gpu-pipeline.md CPU fallback per render on device limits, out of memory, device errors / loss, incomplete or black results (perf.gpuReason, perf.gpuFallback); short submissions for slow iGPUs; backend choice (DX12 only on Windows, LIGHTCRAFT_GPU_BACKEND / WGPU_BACKEND), device created after the window opens, crash sentinel (issue #136); no tiling beyond the buffer limit (CPU)
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
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 (import); 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/mod.rs
LR-BEHAV-SIDEBAR-COLLAPSE Collapsible left-sidebar sections P2 ✅ crates/ui-egui/src/panels/left.rs (sidebar_section_header), crates/ui-egui/src/state.rs (collapsed_sidebar), crates/ui-egui/src/tests_panels.rs click the Albums, Local, By Date or Keywords header (or drive ui.clickWidget sidebarSection:<albums|local|byDate|keywords>) to fold or unfold the section; a chevron after the title shows the state; the choice is kept across restarts (collapsedSidebar in the UI state). UI-only (no command), like the By Date / Keywords disclosure triangles; photos dropped on a folded Albums section have no target
LR-BEHAV-PANEL-RESIZE Resizable side panels P1 ✅ crates/ui-egui/src/panels/mod.rs (resizable_side), crates/ui-egui/src/state.rs (LEFT_WIDTH, RIGHT_WIDTH), crates/ui-egui/src/tests_panels.rs drag the left sidebar's right edge (200–480 pt) or the right panel's left edge (250–520 pt); the photo area keeps ≥ 360 pt; widths are kept across panels, views and restarts (leftWidth / rightWidth in the UI state); the Presets column stays fixed
LR-BEHAV-EMPTY-STATES Empty states P1 ✅ crates/ui-egui/src/panels/mod.rs (empty_message)
LR-BEHAV-TOOLTIPS Tooltips with shortcuts P0 ✅ crates/ui-egui/src/panels/bottombar.rs
LR-BEHAV-TEXTFIELD Text fields behave alike P1 🟡 crates/ui-egui/src/text_field.rs, crates/ui-egui/src/panels/topbar.rs, crates/ui-egui/src/widgets.rs, crates/ui-egui/src/panels/unnamed.rs one shared field: right-click Cut / Copy / Paste / Select All (no Paste in the browser), Esc gives back the text from before the edit, Return / leaving / Esc reported to the caller, optional select-on-focus; agents use ui.clipboard (MCP clipboard) and ui.inspect → copied. Used by Search Photos, slider values and the unnamed faces' name field so far; the other text fields (dialogs, import, settings, masking, keywords …) still use egui's own and have no menu
LR-BEHAV-HEADLESS Headless UI snapshots P2 ✅ apps/lightcraft-cli/src/main.rs, crates/ui-egui/src/headless.rs, docs/control-protocol.md snapshot rasterizes the UI and defaults photo development to the CPU before the first frame, without adapter discovery; scaled dimensions are validated before allocation, and UI zoom preserves the requested physical viewport; JSON-lines control scripts are supported
LR-BEHAV-ACCESS Accessibility P2 🟡 crates/ui-egui/src/widgets.rs, crates/ui-egui/src/panels/grid.rs AccessKit (VoiceOver / Narrator / AT-SPI): sliders announce control and value, buttons / icon buttons / dropdowns / section headers / sources their labels and state, grid thumbnails file name, rating, flag and label; the canvas tools (crop, masks) are pointer-only; not audited with a screen reader
LR-BEHAV-LOCALE Language options P2 ✅ cmd:app.language.english, cmd:app.language.simplifiedChinese, cmd:app.language.traditionalChinese, cmd:app.language.japanese, cmd:app.language.portuguese, cmd:app.language.spanish, cmd:app.language.german, cmd:app.language.russian Edit > Language lists every language in the table and the active one is checked; shared with Settings > General (docs/localization.md)
LR-BEHAV-LOCALIZE Localisation P2 🟡 crates/ui-egui/src/i18n.rs, crates/ui-egui/locales/, docs/localization.md, docs/localization-zh-hans.md, docs/localization-zh-hant.md, docs/localization-ja.md, docs/localization-pt-br.md, docs/localization-es.md, docs/localization-de.md, docs/localization-ru.md One table entry per language (code, endonym, ISO 15924 script, catalog): English, Simplified Chinese, Traditional Chinese (Taiwan), Japanese, Brazilian Portuguese, Spanish, German and Russian UI with a persisted language preference and craft-fonts regular/bold faces per script; menus, edit, crop/masks, settings, import/export and primary progress messages. Adding a language is a table entry plus two catalogs. Stock presets/profiles, history steps, library headings, date headings and capture times are localised (user-named items stay verbatim); the German catalog covers every existing catalog key, command/control label and rule-editor label; the other catalogs fall back to English for messages they lack. Technical errors remain English; release notes are translated in German; Traditional Chinese borrows the Simplified Chinese face (no TC face in craft-fonts yet), and the web build embeds the Japanese face in WASM while loading the Simplified Chinese face from a separate font file.
LR-BEHAV-LEARN Tutorials OOS 🚫
LR-BEHAV-WHATSNEW What's new P2 ✅ cmd:app.whatsNew, docs/whats-new.md Help ▸ What's New: release highlights
LR-BEHAV-AI-EA Early-access badges P2 ⬜
LR-BEHAV-TITLEBAR Title bar drag and double-click zoom (macOS) P2 🟡 crates/ui-egui/src/titlebar.rs, crates/ui-egui/src/panels/topbar.rs, crates/ui-egui/src/tests_titlebar.rs the native title bar is hidden on macOS, so the top bar's empty space (including the traffic-light margin) moves the window and a double-click zooms or restores it; buttons and the search field keep their clicks; ignored in full screen. Zoom is winit's Maximized: it does not follow System Settings › “Double-click a window's title bar to” (Fill / Minimize). a press that starts on a button or the search field never moves the window. Known gap: egui-winit reads the zoomed state only at start-up and after our own Maximized command, so a window zoomed another way (green button, Window ▸ Zoom, snapping) makes the first double-click a no-op before the toggle catches up. Checked by hand on a real macOS window (drag, zoom, restore)

Y. Menus

From 04-menu-tree.md. ✅ = a command exists and is reachable from the UI (button, panel or shortcut). There is no visible menu bar yet: the menu model is only exposed through the control channel (ui.menu.list).

Id Feature Tier Status Evidence Missing / notes
MENU-BAR Menu bar rendering P1 ✅ crates/ui-egui/src/menubar.rs, apps/lightcraft/src/native_menu.rs native macOS menu bar (muda) with live labels/enabled/checked; in-window menus on web/Windows/Linux (with one menu open, the pointer on another title switches to it, as in native menu bars); ⌫ and X stay egui-handled (contextual), so they show no key in the native menu
MENU-APP-ABOUT About P2 ✅ cmd:app.about
MENU-APP-SETTINGS Settings… P0 ✅ cmd:app.settings app menu on macOS, Edit menu elsewhere
MENU-APP-UPDATES Check for updates P2 ⬜
MENU-APP-SYNC Sync status / pause OOS 🚫
MENU-APP-SIGNOUT Sign out OOS 🚫
MENU-APP-HIDE Hide / hide others / show all P1 ✅ apps/lightcraft/src/native_menu.rs the system's own items in the app menu (⌘H, ⌥⌘H)
MENU-APP-QUIT Quit P0 ✅ cmd:app.quit, apps/lightcraft/src/native_menu.rs macOS: app menu (native); elsewhere: File → Quit LightCraft, always the last item in its own group (after commands the File layout does not list)
MENU-FILE-ADDPHOTOS Import Photos… (was Add Photos…) P0 ✅ cmd:file.addPhotos, crates/ui-egui/src/menus.rs first item of File, ⇧⌘I; file picker → the import review
MENU-FILE-ADDFOLDER Import from Folder… (was Add Folder…) P0 ✅ cmd:file.addFolder, cmd:library.importPreview folder picker (desktop) → the import review, subfolders included; path param for agents. Worded as importing: it doesn't save a Local location (that is Local → Browse Folder…)
MENU-FILE-MIGRATE Migrate photos OOS 🚫
MENU-FILE-NEWALBUM New Album… P0 ✅ cmd:dialog.newAlbum
MENU-FILE-NEWFOLDER New Folder… P0 ✅ cmd:dialog.newFolder ⇧⌘N
MENU-FILE-NEWSMART New Smart Album… P1 ✅ cmd:dialog.newSmartAlbum saves the current view (source + filter)
MENU-FILE-IMPORTPROFILES Import Profiles & Presets… P1 ✅ cmd:file.importPresets presets in every common format and .cube LUT profiles (see LR-IMP-PROFILES)
MENU-FILE-EXPORT Export… P0 ✅ cmd:dialog.export
MENU-FILE-EXPORTPREV Export with Previous P0 ✅ cmd:app.exportPrevious ⌥⇧⌘E
MENU-FILE-EXPORTPRESETS Export preset submenu P0 ✅ cmd:app.export (preset), crates/ui-egui/src/menubar.rs (Export with Preset) built-ins, then user presets, then Custom… (the dialog); exports to the last folder
MENU-FILE-SHARE Share / get link / invite OOS 🚫
MENU-FILE-PHOTOSHOP Edit in external editor P2 ✅ cmd:photo.editInExternal Photo ▸ Edit in External Editor
MENU-FILE-SHOWFINDER Show in Finder P0 ✅ cmd:app.showInFinder ⌘R; Explorer on Windows, the folder on Linux; disabled for demo scenes and on the web
MENU-FILE-OFFLINE Store album locally P2 🚫 not applicable: local-first
MENU-FILE-CLOSE Close Window P1 ✅ apps/lightcraft/src/native_menu.rs the system's Close Window item at the end of File (⌘W)
MENU-EDIT-UNDO Undo P0 ✅ cmd:edit.undo label does not name the step
MENU-EDIT-REDO Redo P0 ✅ cmd:edit.redo
MENU-EDIT-COPYPASTE Copy / paste (edit settings) P0 ✅ cmd:develop.copy, cmd:develop.paste
MENU-EDIT-CHOOSECOPY Choose Edit Settings to Copy… P0 ✅ cmd:dialog.copySettings
MENU-EDIT-PASTESELECTED Paste Selected Settings P0 ✅ cmd:dialog.pasteSettings, cmd:develop.paste (groups) checklist prefilled with the copied groups; only copied groups can be pasted
MENU-EDIT-SELECTALL Select All P0 ✅ cmd:library.selectAll
MENU-EDIT-SELECTNONE Select None P0 ✅ cmd:library.selectNone
MENU-EDIT-SELECTBY Select by flag / rating P1 ✅ cmd:library.selectBy Edit → Select by: picks, rejects, unflagged, ★…★★★★★ and higher, unrated, colour labels; add extends the selection
MENU-EDIT-FIND Find… P0 ✅ cmd:view.focusSearch, crates/ui-egui/src/panels/topbar.rs ⌘F focuses the search field
MENU-VIEW-PHOTOGRID Photo Grid P0 ✅ cmd:view.photoGrid
MENU-VIEW-SQUAREGRID Square Grid P0 ✅ cmd:view.squareGrid
MENU-VIEW-DETAIL Detail P0 ✅ cmd:view.detail
MENU-VIEW-EDIT Edit P0 ✅ cmd:panel.edit
MENU-VIEW-FULLSCREENPREVIEW Full Screen Preview P1 ✅ cmd:view.fullScreenPreview
MENU-VIEW-ENTERFULLSCREEN Enter Full Screen P1 ✅ cmd:view.enterFullScreen
MENU-VIEW-PHOTOSPANEL Show/Hide photos panel P0 ✅ cmd:view.leftPanel
MENU-VIEW-FILMSTRIP Show/Hide filmstrip P0 ✅ cmd:view.filmstrip
MENU-VIEW-INFO Show/Hide info P0 ✅ cmd:panel.info
MENU-VIEW-KEYWORDS Show/Hide keywords P0 ✅ cmd:panel.keywords
MENU-VIEW-ACTIVITY Show/Hide activity (comments) OOS 🚫 our History panel is panel.activity
MENU-VIEW-VERSIONS Show/Hide versions P1 ✅ cmd:panel.versions
MENU-VIEW-HISTOGRAM Show/Hide histogram P0 ✅ cmd:view.histogram
MENU-VIEW-INFOOVERLAY Show info overlay P1 ✅ cmd:view.infoOverlay cycles
MENU-VIEW-SHOWORIGINAL Show Original P0 ✅ cmd:view.showOriginal
MENU-VIEW-BEFOREAFTER Before/After submenu P0 ✅ cmd:view.beforeAfter, cmd:view.beforeAfterSplit, cmd:view.beforeAfterTopBottom, cmd:view.beforeAfterSplitTopBottom
MENU-VIEW-ZOOM Zoom in / out / toggle / fit / 1:1 P0 ✅ cmd:view.zoomIn, cmd:view.zoomOut, cmd:view.zoomToggle, cmd:view.zoomFit, cmd:view.zoom100
MENU-VIEW-CLIPPING Show Clipping P0 ✅ cmd:view.clipping
MENU-VIEW-MASKOVERLAY Mask overlay / cycle colour P0 ✅ cmd:view.maskOverlay, cmd:view.maskOverlayMode, cmd:view.maskOverlayColor colour cycles through the panel's swatches (no params) or takes color / opacity
MENU-VIEW-INCLUDESUBFOLDERS Include subfolders P1 ✅ cmd:library.browse (subfolders) toggle in the folder header; imports of folders are recursive
MENU-VIEW-SORT Sort submenu P0 ✅ cmd:library.sort, cmd:library.shuffle Random + Reshuffle; no colour-label key
MENU-VIEW-STACKS Expand/collapse stacks P1 ✅ cmd:stack.expandAll, cmd:stack.collapseAll
MENU-VIEW-PHOTOCOUNT Show photo counts P2 ✅ cmd:view.photoCounts View ▸ Show Photo Counts toggles the left panel's counts
MENU-VIEW-HDR HDR display options P2 ⬜
MENU-PHOTO-ADDTOALBUM Add to album P0 ✅ cmd:album.addPhotos
MENU-PHOTO-REMOVEFROMALBUM Remove from album P0 ✅ cmd:album.removePhotos
MENU-PHOTO-RATE Rate submenu P0 ✅ cmd:photo.rate
MENU-PHOTO-FLAG Flag submenu P0 ✅ cmd:photo.flag
MENU-PHOTO-LABEL Colour label submenu P1 ✅ cmd:photo.label, cmd:dialog.labelNames shows custom names; Edit Label Names…
MENU-PHOTO-ROTATE Rotate left / right P0 ✅ cmd:photo.rotateLeft, cmd:photo.rotateRight
MENU-PHOTO-FLIP Flip horizontal / vertical P0 ✅ cmd:photo.flipHorizontal, cmd:photo.flipVertical
MENU-PHOTO-CREATEVERSION Create Version… P1 ✅ cmd:version.create no name prompt
MENU-PHOTO-STACK Stack submenu P1 ✅ cmd:stack.group, cmd:stack.ungroup, cmd:dialog.autoStack
MENU-PHOTO-MERGE Photo merge submenu P2 ✅ cmd:dialog.mergeHdr, cmd:dialog.mergePanorama, cmd:dialog.mergeHdrPanorama, cmd:merge.hdrLast
MENU-PHOTO-ENHANCE Enhance… P2 ⬜
MENU-PHOTO-AUTO Auto settings P0 ✅ cmd:develop.auto
MENU-PHOTO-BW Convert to B&W P0 ✅ cmd:develop.treatment
MENU-PHOTO-RESET Reset edits / crop P0 ✅ cmd:develop.reset, cmd:crop.reset
MENU-PHOTO-UPDATEAI Update AI settings P2 ⬜
MENU-PHOTO-RENAME Rename N photos… P1 ✅ cmd:dialog.rename live “Rename N Photos…” label
MENU-PHOTO-CAPTURETIME Edit capture time… P1 ✅ cmd:dialog.captureTime
MENU-PHOTO-COVER Set as album cover P2 ✅ cmd:album.setCover
MENU-PHOTO-DELETE Delete N photos… P0 ✅ cmd:photo.delete no confirmation; static label
MENU-PHOTO-MOVETOCLOUD Move/copy to cloud OOS 🚫
MENU-WINDOW-MINIMIZE Minimize / zoom P1 ✅ apps/lightcraft/src/native_menu.rs the system's Minimize (⌘M) and Zoom items at the top of Window
MENU-WINDOW-PANELS Panel switches P0 ✅ cmd:panel.edit, cmd:panel.crop, cmd:panel.remove, cmd:panel.masking, cmd:panel.presets, cmd:panel.versions
MENU-WINDOW-BRINGFRONT Bring all to front P2 ✅ apps/lightcraft/src/native_menu.rs the system's item at the end of Window
MENU-HELP-HELP Help P2 ✅ cmd:app.help opens the documentation
MENU-HELP-TUTORIALS Tutorials OOS 🚫
MENU-HELP-WHATSNEW What's new P2 ✅ cmd:app.whatsNew
MENU-HELP-SHORTCUTS Keyboard shortcuts P1 ✅ cmd:app.shortcuts
MENU-HELP-FEEDBACK Send feedback P2 ✅ cmd:app.feedback opens a new issue on the project's GitHub
MENU-HELP-LOGFOLDER Open log folder P2 ✅ cmd:app.openLogFolder an extra (the reference app has no such item): Help ▸ Open Log Folder reveals logs/lightcraft.log in the system file manager (Finder, Explorer, or the folder on Linux); off on the web and in --memory sessions, which keep no log (issue #260)
MENU-HELP-SYSINFO System info P2 ✅ cmd:app.systemInfo, cmd:library.info Help ▸ System Info…: version, OS, CPU threads, GPU, memory budget, preview size, library, timings; Copy to Clipboard; JSON for agents (open: false)
MENU-CTX-GRID Photo context menu P0 ✅ crates/ui-egui/src/panels/grid.rs (context_menu) rate, flag, label, add to / remove from album, rename, virtual copy, version, stack, copy / paste / paste selected, reset, merge, rotate, show in Finder, export / export with preset, set as album cover (in an album), delete (restore / delete permanently in Recently Deleted)
MENU-CTX-DETAIL Loupe context menu P1 ✅ crates/ui-egui/src/panels/detail.rs Zoom submenu (fit, 100%, in, out), then the photo menu
MENU-CTX-ALBUM Album / folder row menu P0 ✅ crates/ui-egui/src/panels/left.rs (folder_menu), cmd:album.move, cmd:dialog.export add selected, export album (dialog / preset), New ▸ Create Album / Smart Album / Smart Album from Filter / Folder (on a folder: made inside it), move to a folder or the top level, rename, delete; smart albums: update rules
MENU-CTX-MASK Mask / component menu P0 ✅ crates/ui-egui/src/panels/masking.rs (mask_menu, component_row_menu), cmd:mask.component mask rows: duplicate (and invert), invert, show/hide, move, rename, delete; component rows (right-click or "…"): invert, duplicate, mode (add / subtract / intersect), intersect with / subtract a new component, rename (also double-click), delete (the last one deletes the mask)
MENU-CTX-PRESET Preset menu P1 ✅ crates/ui-egui/src/panels/presets.rs favourite, update with current settings, rename, move to group, delete; group: export
MENU-CTX-PROFILE Profile favourites P2 ✅ cmd:profile.favorite, crates/ui-egui/src/panels/profiles.rs star in the profile browser; Favorites group first
MENU-CTX-VERSION Version menu P1 ✅ crates/ui-egui/src/panels/right.rs (versions) restore, update with current settings, rename, set as before, delete
MENU-CTX-KEYWORD Keyword chip menu P1 ✅ crates/ui-egui/src/panels/right.rs, cmd:keyword.delete remove from photo, show photos with keyword, rename keyword, delete keyword

Z. Keyboard shortcuts (desktop)

From 06-shortcuts.md part 1. Evidence is our binding; conflicts are explained in Shortcuts: conflicts and missing bindings.

Id Feature Tier Status Evidence Missing / notes
KEY-CROP Crop & rotate — C P0 ✅ cmd:panel.crop
KEY-DETAIL Detail — D P0 ✅ cmd:view.detail
KEY-EDIT Edit — E P0 ✅ cmd:panel.edit
KEY-FULLSCREEN Full-screen preview — F P1 ✅ cmd:view.fullScreenPreview
KEY-GRID Grid — G P0 ✅ cmd:view.gridToggle, cmd:view.photoGrid G toggles Photo Grid ↔ Square Grid
KEY-INFO Info — I P0 ✅ cmd:panel.info
KEY-KEYWORDS Keywords — K P0 ✅ cmd:panel.keywords
KEY-CLIPBOARD Copy / paste edit settings — ⌘C / ⌘V P0 ✅ cmd:develop.copy, cmd:develop.paste, crates/ui-egui/src/shortcuts.rs (matches) ⌘X has nothing to cut outside text fields; on Windows and Linux, where the windowing layer sends Ctrl+C / Ctrl+V as clipboard events rather than key presses, those run the shortcuts too
KEY-UNDOREDO Undo / redo — ⌘Z / ⇧⌘Z P0 ✅ cmd:edit.undo, cmd:edit.redo
KEY-MINIMIZE Minimize — ⌘M P1 ✅ apps/lightcraft/src/native_menu.rs native Window ▸ Minimize
KEY-AUTO Auto — ⇧A P0 ✅ cmd:develop.auto
KEY-PHOTOSHOP External editor — ⇧⌘E P2 ✅ cmd:photo.editInExternal ⇧⌘E as observed; the export dialog is ⇧E
KEY-ROTATE Rotate — ⌘[ / ⌘] P0 ✅ cmd:photo.rotateLeft, cmd:photo.rotateRight
KEY-ZOOM Zoom in / out — ⌘= / ⌘− P0 ✅ cmd:view.zoomIn, cmd:view.zoomOut
KEY-SELECTALL Select all — ⌘A P0 ✅ cmd:library.selectAll
KEY-SELECTNONE Select none — ⌘D P0 ✅ cmd:library.selectNone secondary binding (primary ⌘⇧A)
KEY-PASTESELECTED Paste selected — ⇧⌘V P0 ✅ cmd:dialog.pasteSettings
KEY-PREFS Settings — ⌘, P0 ✅ cmd:app.settings
KEY-SEARCH Search — ⌘F P0 ✅ cmd:view.focusSearch
KEY-VISUALIZESPOTS Visualize spots — A P1 ✅ cmd:view.visualizeSpots
KEY-CYCLEOVERLAY Cycle overlay — O P0 ✅ cmd:view.maskOverlay, cmd:view.maskOverlayMode, cmd:view.cropOverlay O toggles the mask overlay; ⇧O cycles mask overlay modes while masking, crop overlays elsewhere
KEY-PHOTOSPANEL Photos panel — P P0 🟡 cmd:view.leftPanel bound to ⌘⇧L; P = pick
KEY-LINEAR Linear gradient — L P0 ✅ cmd:tool.linear
KEY-RADIAL Radial gradient — R P0 ✅ cmd:tool.radial
KEY-CLIPPING Clipping — J P0 ✅ cmd:view.clipping
KEY-WB White-balance selector — W P0 ✅ cmd:tool.wbPicker
KEY-FILMSTRIP Filmstrip — / P0 ✅ cmd:view.filmstrip
KEY-SHOWORIGINAL Show original — \ P0 ✅ cmd:view.showOriginal
KEY-TOGGLEZOOM Toggle zoom — Space P0 ✅ cmd:view.zoomToggle Space is a secondary binding (primary Z)
KEY-MASKCOLOR Cycle mask colour — ⇧O P1 ✅ cmd:view.maskOverlayColor, crates/ui-egui/src/shortcuts.rs ⇧O while masking cycles the overlay colour (while cropping: the guides' orientation); the overlay mode is in View ▸ Cycle Mask Overlay Mode
KEY-EXPORTPREV Export with previous — ⌘E P0 ✅ cmd:app.exportPrevious ⌘E (alias) and ⌥⇧⌘E (Classic)
KEY-EXPORTDIALOG Export dialog — ⇧E P0 ✅ cmd:dialog.export secondary binding (primary ⌘⇧E)
KEY-ENTERFULLSCREEN Window full screen — ⇧⌘F P1 ✅ cmd:view.enterFullScreen
KEY-STACK Group / ungroup stack — ⌘G / ⇧⌘G P1 ✅ cmd:stack.group, cmd:stack.ungroup also S expand/collapse, ⇧S top of stack
KEY-GUIDEDUPRIGHT Guided Upright — ⇧G P1 ✅ cmd:tool.guidedUpright, cmd:geometry.upright opens Crop & Geometry with Guided Upright on and the guide tool active; G toggles Photo Grid ↔ Square Grid (cmd:view.gridToggle) as observed
KEY-HIDE Hide / hide others — ⌘H / ⌥⌘H P1 ✅ apps/lightcraft/src/native_menu.rs native app-menu items
KEY-QUIT Quit — ⌘Q P0 ✅ cmd:app.quit ⌘Q (Ctrl+Q off macOS)
KEY-CREATEVERSION Create version — ⇧M P1 ✅ cmd:version.create secondary binding (primary ⌘⇧S)
KEY-CLOSEWINDOW Close window — ⌘W P1 ✅ apps/lightcraft/src/native_menu.rs native File ▸ Close Window
KEY-DELETE Delete photo — ⌫ P0 ✅ cmd:photo.delete
KEY-ADDPHOTOS Import photos — ⇧⌘I P0 ✅ cmd:file.addPhotos
KEY-VERSIONS Versions panel — ⇧V P1 ✅ cmd:panel.versions
KEY-SECTIONS Expand/collapse edit sections — ⌘1…⌘6 P1 ✅ cmd:section.light, cmd:section.color, cmd:section.effects, cmd:section.detail, cmd:section.optics ⌘1–⌘5 (Light, Color, Effects, Detail, Optics) as observed; our Edit panel has no Lens Blur / Geometry section for ⌘6 / ⌘7; Zoom 100% moved to ⌥⌘0
KEY-PRESETS Presets panel — ⇧P P0 ✅ cmd:panel.presets Shift+P opens Presets outside the two library grids; in grids it picks and advances (Classic convention). Menu hides the Presets key there; shortcut help follows the view
KEY-HISTOGRAM Histogram — ⌘0 P0 🟡 cmd:view.histogram bound to ⌘⇧H; ⌘0 = zoom to fit
KEY-BRUSHSIZE Brush size — [ / ] P0 ✅ cmd:brush.smaller, cmd:brush.larger Masking brush and Remove tool (and its selected spot)
KEY-BRUSHFEATHER Brush feather — ⇧[ / ⇧] P0 ✅ cmd:brush.featherLess, cmd:brush.featherMore
KEY-BRUSH Brush — B P0 ✅ cmd:tool.brush
KEY-HEAL Remove / heal — H P0 ✅ cmd:panel.remove
KEY-MERGE HDR / panorama merges — ⌃H ⇧⌃H ⌃M ⇧⌃M P2 ✅ cmd:dialog.mergeHdr, cmd:dialog.mergePanorama, cmd:merge.hdrLast, cmd:merge.panoramaLast
KEY-PICK Pick — Z P0 🟡 cmd:photo.pick bound to P; Z toggles zoom
KEY-UNFLAG Unflag — U P0 ✅ cmd:photo.unflag
KEY-REJECT Reject — X P0 ✅ cmd:photo.reject swaps crop aspect while cropping
KEY-RATING Ratings — 0…5 P0 ✅ crates/ui-egui/src/shortcuts.rs → cmd:photo.rate 0 clears, 1–5 set; Shift applies and advances; native macOS plain keys stay with egui; grid multi-selection and undo/redo tested
KEY-LABELS Labels — 6…9 P1 ✅ crates/ui-egui/src/shortcuts.rs → cmd:photo.label 6 red, 7 yellow, 8 green, 9 blue; Shift+6–9 labels and advances; purple/none available from menus
KEY-MASKING Masking — M P0 ✅ cmd:panel.masking
KEY-ERASE Erase while held — ⌥ P0 ✅ crates/ui-egui/src/panels/detail.rs, crates/ui-egui/src/panels/masking.rs ⌥ while painting flips Add ↔ Erase for that stroke; Add/Erase buttons too
KEY-RATEADVANCE Rate and advance — ⇧0…5 P1 ✅ crates/ui-egui/src/shortcuts.rs → cmd:photo.rate (advance)
KEY-FLAGADVANCE Flag and advance — ⇧Z / ⇧X / ⇧U P1 ✅ cmd:photo.flag (advance), crates/ui-egui/src/shortcuts.rs Shift+Z pick, Shift+X reject, Shift+U clear; Shift+P also picks and advances in the library grids; exactly one step with Auto Advance enabled
KEY-NEXTPREV Next / previous — → / ← P0 ✅ cmd:library.next, cmd:library.previous
KEY-BA-CYCLE Before/after — Y P0 ✅ cmd:view.beforeAfter toggles side by side (no cycling)
KEY-BA-TOPBOTTOM Before/after top/bottom — ⌥Y P1 ✅ cmd:view.beforeAfterTopBottom, cmd:view.beforeAfterSplitTopBottom ⌥Y, ⇧⌥Y
KEY-BA-SPLIT Split before/after — ⇧Y P0 ✅ cmd:view.beforeAfterSplit
KEY-CHOOSECOPY Choose settings to copy — ⇧⌘C P0 ✅ cmd:dialog.copySettings
KEY-RESETALL Reset all — ⇧⌘R P0 ✅ cmd:develop.reset
KEY-GENAI Generative toggles / variations — ⌥⇧G, ⌥←/→ OOS 🚫
KEY-DETECT Detect objects — ⌥⇧O P2 ⬜
KEY-CROP-CONSTRAIN Lock crop aspect — A P1 ✅ cmd:crop.aspect (toggle, current) A while cropping locks the current shape / unlocks
KEY-CROP-SWAP Swap crop orientation — X P0 ✅ cmd:crop.rotateAspect
KEY-CROP-OVERLAYORIENT Crop overlay orientation — ⇧O P1 ✅ cmd:view.cropOverlayOrientation while cropping: O cycles the overlay, ⇧O its orientation
KEY-CROP-RESET Reset crop — ⌥⌘R P0 ✅ cmd:crop.reset
KEY-STRAIGHTEN Straighten while held — ⌘ drag P1 ✅ cmd:crop.straighten, crates/ui-egui/src/panels/detail.rs (straighten_overlay) ⌘-drag in Crop draws a level line; the crop tool stays active
KEY-SLIDER-RESET Reset slider — double-click P0 ✅ crates/ui-egui/src/widgets.rs
KEY-SLIDER-NUDGE Nudge slider — ↑/↓ P1 ✅ crates/ui-egui/src/widgets.rs (nudged), cmd:develop.adjust ↑/↓ over any slider: ≈ 1/200 of its range (exposure 0.05, most sliders 1, temperature 50 K); ⇧ ×5; one undo step each
KEY-SLIDER-TYPE Type a slider value P1 ✅ crates/ui-egui/src/widgets.rs (typed_value) click the value next to any slider's name and type one (1.5, +0,25, -20, 5600 K): Return or clicking away applies it on the control's steps and range as one undo step, Esc keeps the old value; issue #322
KEY-SHORTCUTS Shortcut list — ⌘/ P1 ✅ cmd:app.shortcuts
KEY-HELP Help — F1 P2 ✅ cmd:app.help
KEY-VIDEO-PLAY Play/pause video — Space P1 ⬜
KEY-ESC Leave tool / view — Esc P0 ✅ cmd:view.back, crates/ui-egui/src/icons.rs (navigation_arrowheads_have_both_halves) top-bar navigation arrowheads render both halves at 1× and 2× scale
KEY-COMMIT Commit tool — Return P1 ✅ cmd:tool.done Return closes Crop / Remove / Red Eye / Masking back to Edit (edits apply live, as in Lightroom)
KEY-DELETE-PIN Delete selected pin — ⌫ P0 ✅ cmd:mask.delete, cmd:spot.delete ⌫ deletes the active mask (Masking) or the selected spot (Remove), never the photo while retouching
KEY-HIDEPINS Hide pins — H P2 ⬜ H = Remove panel

Lightroom Classic extras

From 08-lightroom-classic-extras.md (Classic-only features) and part 2 of 06-shortcuts.md (Classic keys, grouped).

Id Feature Tier Status Evidence Missing / notes
LRC-LIB-IMPORT Full import dialog P1 ✅ cmd:library.import (mode, destination, organize, rename, metadataPreset, preset, keywords, album), crates/ui-egui/src/import.rs review grid with duplicates skipped; add in place or copy (library Originals or any folder; by day / by month / one folder / custom folder template such as {date:%Y}/{date:%Y%m%d} → 2026/20260114, kept inside the destination; example destination shown; rename template with live example, Tags picker (insert at cursor) and unknown-tag warning, numbered across the import); develop preset, metadata preset, keywords, album on import
LRC-LIB-AUTOIMPORT Watched-folder import P2 ✅ cmd:library.autoImport see LR-IMP-AUTO
LRC-LIB-TETHER Tethered capture P2 ⬜
LRC-LIB-VIEWS Grid / loupe / compare / survey / people P1 🟡 cmd:view.photoGrid, cmd:view.squareGrid, cmd:view.detail, cmd:view.compare, cmd:view.survey, cmd:view.gridInfo grid, square grid, loupe, compare, survey, second window; square-grid captions: file name, exposure or capture date (View ▸ Grid Info); no people view
LRC-LIB-COMPARE Compare view P1 ✅ cmd:view.compare ⇧C (C is Crop here); swap, make select; zoom always linked
LRC-LIB-SURVEY Survey view P2 ✅ cmd:view.survey N
LRC-LIB-REFVIEW Reference view P2 ✅ cmd:view.reference, cmd:photo.setReference, crates/ui-egui/src/panels/compare.rs (show_reference) View ▸ Reference View (⇧R): the reference photo beside the active one (edited with the Edit panel); photo menu ▸ Set as Reference Photo
LRC-LIB-CATALOG-PANEL Catalog sets P1 ✅ cmd:library.source (missing) all, recently added, picks, recently deleted, missing photos (when any)
LRC-LIB-CATALOG-IMPORT Native Lightroom Classic catalog import P1 🟡 cmd:library.inspectLightroom, cmd:library.importLightroom, crates/engine/src/lightroom_catalog.rs, docs/lightroom-catalog-import.md read-only .lrcat plus committed WAL pages, in-tree read-only Rust SQLite B-tree reader; background inspection/import with progress/cancellation, per-open library identity checks; originals in place, ratings/flags/labels, XMP, hierarchical keywords, collection sets/regular collections, virtual copies, mapped develop settings; stable reimport identities, undo, missing-file placeholders, existing edits preserved. Best-effort deduplicated recovery archives retain settings/history/snapshots/collection rules (32 MiB each, eight archives/128 MiB aggregate); archive limits/write failures and optional-table limits warn without blocking photo import; unrelated unsupported tables and opaque SQL BLOBs skipped. Adobe rendering, AI data and native history replay remain incomplete; no writes to Lightroom catalogs
LRC-LIB-CATALOG-LOCK Catalog open in one program at a time P1 ✅ cmd:app.openLibrary, crates/catalog/src/lock.rs, crates/engine/src/library.rs (open_library) exclusive OS lock on catalog.lock for the session (app and lightcraft-cli --library); a second opener is refused with who has it (catalog.lock.owner: program, process, computer); released by the OS on a crash, so never stale; file systems without locks open unprotected with a warning (the app's notices, library.info → settingsWarnings, and the CLI's stderr; issue #171); no read-only mode; the browser build (OPFS) has no guard across tabs yet. A library that can't be opened at launch (locked, unreadable, newer format, drive missing) shows a window — Try Again, Choose Another Library…, Continue Without Saving (then a banner; nothing written), Quit — instead of a silent demo session (crates/ui-egui/src/panels/library_problem.rs)
LRC-LIB-FOLDERS Disk folder tree P1 ✅ cmd:library.browse, cmd:folder.rename, cmd:folder.move, crates/ui-egui/src/panels/left.rs (local_section, folder_tree) standard places + the browsed folder + Browse Folder…, each an expandable tree of subfolders, breadcrumb navigation; context menu: Rename Folder…, Move Folder To…, Show in Finder (on disk, photos relinked; one undo step each: undo / redo move the folder back and forth, refused when the destination is taken)
LRC-LIB-LIBFOLDERS Library folders P1 ✅ cmd:library.folders, cmd:library.source (libraryFolder), cmd:library.filter (libraryFolder), cmd:library.removeFolder, cmd:folder.rename, cmd:folder.move, crates/catalog/src/folders.rs, crates/ui-egui/src/panels/left.rs (folders_section, folder_menu_for_library, row_menu), crates/ui-egui/src/tests_panels.rs sidebar Folders: the folders the library's photos were imported from, with photo counts (subfolders included), as another way to explore the library next to By Date and Keywords; Local stays the way to browse any folder on disk. Each disk is a row of its own and starts open: a drive letter, a Windows share (\\server\share, \\?\UNC\…, \\wsl$\…), a macOS share or disk (/Volumes/tokyo), a Linux mount (/media, /run/media, /mnt, gvfs smb-share:…, /net/<host>), NFS (/Network/Servers/<host>), else “This Computer” — told apart from the stored paths alone, so an offline share lists like a mounted one and the sidebar never waits on a network. Only folders that hold imported photos are listed (browsed, deleted and demo photos don't count); every folder is a row of its own (Users → me → Pictures), and a folder that holds nothing itself and leads to one folder starts open; every level indents one step more than its parent however deep it goes: the whole sidebar (Albums, Local, By Date, Folders, Keywords) scrolls sideways when its widest row is wider than the panel (rows are measured one frame behind; Create Album and the counts stay at the visible edge); a name that does not fit ends in an ellipsis at the count (the full name on hover; a row asks for room for at most 140 pt of its name, so depth, not one long name, makes the sidebar scroll), as does the grid title; selection and hover bars end at the panel's edge; click makes the folder the source, like an album or a Local folder (its photos and those of the folders inside it fill the grid, titled “photos/travel”; the filter bar narrows it further; library.source libraryFolder + path; kept with the library's view), and its row is always opened into view; the shown folder follows a rename or move and their undo (engine-tested; the sidebar test covers rename), is dropped back to All Photos when it is removed or no longer holds a photo, and a smart album made from the view keeps it, also when its rules are edited (the editor has no folder field). Only a folder that shows exactly its own photos can be shown (not the startup disk, not /Volumes-like folders above disks, not an empty one). A row that would cover other disks' photos too (the startup disk, /Volumes, /mnt…) only opens. Right-click (row or triangle): Rename Folder…, Move Folder To…, Show in Finder (on disk, photos relinked, as in Local) and Remove from Library… (asks; moves the folder's photos to Recently Deleted, one undo step, no file touched); a disk row offers Remove Disk from Library… (the command needs disk: true for a whole disk or a folder spanning several). Limits: names are compared as written (macOS /Volumes/tokyo vs /Volumes/TOKYO are two disks; /Volumes/Macintosh HD/… and /… two rows); no disk capacity / online status; no “include subfolders” switch (a folder always includes the ones inside it); creating a folder and moving photos between folders are not here
LRC-LIB-COLLECTIONS Collections & sets P1 ✅ cmd:album.create, cmd:album.createSmart, cmd:album.toggleTarget, cmd:album.setTarget, cmd:album.clearQuick albums + folders (sets), smart albums (rule editor; rules incl. File Path — the whole string, either separator, case-insensitive), Quick Collection and target album (B in the grids adds / removes the selection; album menu ▸ Set as Target Album; the target is marked +)
LRC-LIB-SMARTCOLL Smart-collection rules P1 ✅ cmd:album.createSmart, cmd:album.setRules, cmd:album.ruleFields, cmd:dialog.smartAlbum, crates/catalog/src/rules.rs, crates/ui-egui/src/panels/rules_editor.rs rule editor: match all / any / none, nested groups (⌥+ or + Group), 30 fields (rating, flag, label, type, edits, keywords, any text, filename, file path, format, title, caption, camera, lens, location, creator, copyright, copyright status, capture / import / edit date, ISO, aperture, focal length, megapixels, GPS, virtual copy, album) with text / number / date / in-the-last operators; live match count; also usable as a library filter (ruleSet)
LRC-LIB-PUBLISH Publish services P2 ⬜
LRC-LIB-FILTERBAR Library filter bar P1 ✅ cmd:view.filterBar, cmd:filter.applyPreset, cmd:library.filter (labels, ruleSet), crates/ui-egui/src/panels/filterbar.rs rating/flag/label (several labels at once = any of them)/kind/edited/camera/lens/keyword, clear, save as smart album, filter presets; the filter stays as you change albums (always locked, as in the desktop app); arbitrary conditions via the smart-album rule editor
LRC-LIB-STACKS Stacks (full) P1 ✅ crates/catalog/src/stacks.rs, cmd:stack.group, cmd:stack.split, cmd:stack.moveUp, cmd:stack.moveDown group / ungroup / toggle / set top / remove / auto by time / split / move up and down
LRC-LIB-VC Virtual copies P1 ✅ cmd:photo.virtualCopy ⌘'
LRC-LIB-LABELS Colour-label sets P1 ✅ cmd:label.sets, cmd:label.applySet, cmd:label.saveSet, cmd:label.deleteSet, cmd:label.setNames, crates/engine/src/cmd/manage.rs built-in Colors / Review sets + user sets (Photo ▸ Set Color Label, Edit Label Names… dialog); names written to and read from xmp:Label
LRC-LIB-KEYWORDS Hierarchical keywords, sets, painter P1 ✅ crates/catalog/src/keywords.rs, cmd:keyword.list, cmd:keyword.sets, cmd:keyword.toggleFromSet, cmd:keyword.saveSet, cmd:tool.keywordPainter hierarchical a|b|c keywords (tree, parent filters include children, rename moves children); keyword sets + Recent Keywords (⌥1–⌥9); keyword painter (Keywords panel ▸ Paint: click photos in the grid to toggle a keyword, Esc stops)
LRC-LIB-METADATA Metadata panel & presets P1 ✅ cmd:photo.setMeta, cmd:metadata.savePreset, cmd:metadata.applyPreset, cmd:metadata.presets, cmd:metadata.deletePreset, cmd:dialog.captureTime IPTC core, accessibility, place, capture-time edit; copyright status (unknown / copyrighted / public domain), rights usage terms and copyright info URL (photo.setMeta copyrightStatus, usageTerms, copyrightUrl; XMP Rights Management fields, kept by "copyright only" exports, a smart-album rule); metadata presets (Photo → Metadata Preset, Save Metadata Preset… from the active photo; applied on import from Settings)
LRC-LIB-QUICKDEV Quick develop P2 ✅ cmd:develop.quickAdjust, crates/ui-egui/src/panels/edit.rs (quick_develop) grid with several photos selected: Edit panel ▸ Quick Develop steps (exposure ⅓ / 1 stop, contrast, highlights, shadows, whites, blacks, clarity, vibrance, temperature) added to each photo's own value, one undo step
LRC-LIB-PEOPLE People view P2 🟡 cmd:view.people, cmd:view.person, cmd:faces.person, cmd:faces.unnamed, cmd:faces.nameFaces, crates/ui-egui/src/panels/people.rs, crates/ui-egui/src/panels/person.rs, crates/ui-egui/src/panels/unnamed.rs Named People cards (face close-up, the photo count on the picture, name) for the photos the active filters let through, with the filter chips shown above them, sized by the thumbnail slider. Below them the Unnamed faces: every face nobody has named as a cropped picture, look-alikes next to each other once recognition has looked at them, a suggested name on a face the named ones recognise; click or Shift-click to select, type a name, Enter names the whole group in one undo step. A click on a person opens their page: only cropped faces, then More (unnamed faces that look like them, best first; click to confirm, × to hide). Every face is shown by the detector's box where the scan has one, so faces are equally close. Until recognition is set up a line at the top offers the next step (Settings ▸ Faces, or Turn on), as does the loupe's name box. No clusters with separators between groups, no persistent reject, no merge of two people
LRC-LIB-COMMENTS Comments panel P2 ⬜
LRC-LIB-VISUALSEARCH Find similar photos P2 ✅ cmd:library.findSimilar, crates/pipeline/src/cull.rs (signature) photo menu / Photo ▸ Find Similar Photos: look-alike photos (composition and tones), most similar first, as a filter (only)
LRC-LIB-MISSING Missing files & relink P1 ✅ cmd:library.missing, cmd:photo.relink, cmd:library.findMissing, cmd:file.findMissing, cmd:photo.locate, crates/engine/src/cmd/missing.rs (checked_path) File → Find Missing Photos… (anywhere in a folder: same name, size and content hash; renamed files by size + content hash; same name and size alone only when unambiguous — look-alikes are skipped and reported; searched on a worker thread, one undo step), photo menu → Locate Missing File…; unreadable files show "!" in the grid and a reason in the loupe; undo never moves files. Missing Photos in the sidebar (counted on a worker thread; the view, the Info panel, the grid's context menu and thumbnails read file availability from a cache a worker thread fills, so an offline NAS never stalls a frame); it covers library photos only — Local browse records are never checked, for the count, the view, library.missing and Find Missing alike
LRC-LIB-CONVERT Convert to DNG P2 ✅ cmd:photo.convertToDng, crates/engine/src/cmd/convert.rs Photo ▸ Convert to DNG: lossless DNG next to the raw with the settings embedded, photo (and its virtual copies) relinked; originals kept; undoable; the DNG is decoded and compared with the raw data, written to a temp file, synced, read back and only then given a free name (never replacing a file) — a failed write leaves no DNG and the photo on its raw (issue #106)
LRC-LIB-PREVIEWS Build / discard previews P1 ✅ cmd:library.buildPreviews, cmd:library.previewProgress, cmd:library.cancelPreviews, cmd:library.clearPreviews, crates/engine/src/cmd/previews.rs, crates/preview/src/lib.rs File ▸ Previews: build standard-sized (Settings → Performance size) or 1:1 previews of the selected / visible photos in the background (progress toasts, stop), discard the cache; disk thumbnail + view cache; no smart previews (offline proxies)
LRC-LIB-SLIDESHOW-IMPROMPTU Impromptu slideshow P2 ✅ cmd:view.slideshow see LR-VIEW-SLIDESHOW
LRC-DEV-SNAPSHOTS Named snapshots P1 ✅ cmd:version.create, cmd:version.restore = versions
LRC-DEV-HISTORY Full history panel P1 ✅ cmd:history.list, cmd:history.restore, cmd:history.clear row menu: copy step to before, create version from step, clear history
LRC-DEV-SOFTPROOF Soft proofing P2 🟡 cmd:view.softProof, crates/pipeline/src/output.rs (Proof), crates/ui-egui/src/panels/edit.rs (soft_proofing) S in the loupe (in grids S stays Expand/Collapse Stack): paper-white surround and "Proof Preview", proof profile (sRGB / Display P3 / Adobe RGB / ProPhoto / Rec. 2020), destination (red) and display (blue) gamut warnings, Create Proof Copy (a named virtual copy). Missing: printer ICC profiles, rendering intent, Simulate Paper & Ink
LRC-DEV-AUTOSYNC Sync / auto sync / paste previous P1 ✅ cmd:develop.sync, cmd:develop.autoSync, cmd:develop.pastePrevious Edit ▸ Sync Settings / Auto Sync (⌥⇧⌘A): only the changed settings carry over, one undo step, slider drags sync on release, spots / red eye stay per photo; Edit panel banner
LRC-DEV-MATCHEXP Match total exposures P2 ✅ cmd:develop.matchExposure Photo ▸ Match Total Exposures: the selected photos' Exposure set so shutter × ISO ÷ aperture² plus the slider matches the active photo's
LRC-DEV-CALIB Calibration panel P1 ✅ ctl:calibration.*
LRC-DEV-TAT Targeted adjustment tools P1 ✅ cmd:develop.targeted (target: curve / hue / sat / lum)
LRC-DEV-DEFAULTS Per-camera raw defaults P1 ✅ cmd:library.preferences (camera, import.perCamera), crates/ui-egui/src/panels/settings.rs Settings → Import: raw default and per-camera presets
LRC-DEV-VIEWOPTIONS Develop view options P2 ⬜
LRC-DEV-VIDEO Video frame capture P2 ⬜
LRC-MAP-VIEW Map view P2 ⬜
LRC-MAP-GEOTAG Drag photos onto the map P2 ⬜
LRC-MAP-LOCATIONS Saved locations P2 ⬜
LRC-MAP-TRACKLOG GPS track logs P2 ✅ cmd:photo.autoTagTracklog, cmd:photo.tagFromTracklog, crates/meta/src/gpx.rs Photo ▸ Auto-Tag from Tracklog…: a GPX 1.0 / 1.1 track log sets the GPS of the selected photos by capture time — interpolated between the points of a track segment, else the nearest point within maxGap (10 min); never across segment breaks. The camera's time zone comes from the photo (Exif offset) or is asked for; photos that already have a location keep it unless replace; dryRun previews; one undo step. No track drawn on a map (no Map module)
LRC-MAP-FILTER Location filter bar P2 ⬜
LRC-MAP-REVGEO Reverse geocoding OOS 🚫
LRC-BOOK-SETTINGS Book settings P2 ⬜
LRC-BOOK-AUTOLAYOUT Book auto layout P2 ⬜
LRC-BOOK-PAGE Book pages & templates P2 ⬜
LRC-BOOK-GUIDES Book guides P2 ⬜
LRC-BOOK-CELL Book cell padding P2 ⬜
LRC-BOOK-TEXT Book photo/page text P2 ⬜
LRC-BOOK-TYPE Book typography P2 ⬜
LRC-BOOK-BG Book backgrounds P2 ⬜
LRC-BOOK-VIEWS Book views P2 ⬜
LRC-BOOK-EXPORT Book export (PDF/JPEG) P2 ⬜
LRC-SS-TEMPLATES Slideshow templates P2 ⬜
LRC-SS-OPTIONS Slideshow options P2 ⬜
LRC-SS-LAYOUT Slideshow layout P2 ⬜
LRC-SS-OVERLAYS Slideshow overlays P2 ⬜
LRC-SS-BACKDROP Slideshow backdrop P2 ⬜
LRC-SS-TITLES Slideshow titles P2 ⬜
LRC-SS-MUSIC Slideshow music P2 ⬜
LRC-SS-PLAYBACK Slideshow playback P2 ⬜
LRC-SS-EXPORT Slideshow export P2 ⬜
LRC-PRINT-LAYOUTSTYLE Print layout styles P2 ⬜
LRC-PRINT-IMAGESETTINGS Print image settings P2 ⬜
LRC-PRINT-LAYOUT Print layout P2 ⬜
LRC-PRINT-GUIDES Print guides P2 ⬜
LRC-PRINT-CELLS Picture-package cells P2 ⬜
LRC-PRINT-PAGE Print page options P2 ⬜
LRC-PRINT-JOB Print job & colour management P2 ⬜
LRC-PRINT-TEMPLATES Print templates P2 ⬜
LRC-WEB-LAYOUT Web gallery layouts OOS 🚫
LRC-WEB-SITEINFO Web gallery site info OOS 🚫
LRC-WEB-COLOR Web gallery colours OOS 🚫
LRC-WEB-APPEARANCE Web gallery appearance OOS 🚫
LRC-WEB-IMAGEINFO Web gallery image info OOS 🚫
LRC-WEB-OUTPUT Web gallery output OOS 🚫
LRC-WEB-UPLOAD Web gallery upload OOS 🚫
KEYC-PANELS Classic panel keys (Tab, ⇧Tab, T, F5–F8, solo) P2 ⬜
KEYC-MODULES Classic module switching (⌘⌥1–7) P2 🚫 no modules in LightCraft
KEYC-VIEWS Classic view keys (E, G, C, N, L, F, I, ⇧R, ⌘⌥0) P2 🟡 cmd:view.photoGrid, cmd:view.zoom100 G works; E opens Edit (not loupe); no compare/survey/lights-out/screen modes
KEYC-SECONDWINDOW Classic secondary-window keys P2 ⬜
KEYC-CATALOG Classic photo/catalog keys (⇧⌘I, ⌘', ⌘R, F2, ⌫, ⇧⌘E…) P2 ✅ cmd:library.import, cmd:photo.delete, cmd:photo.virtualCopy, cmd:app.showInFinder, cmd:dialog.rename, cmd:photo.editInExternal ⇧⌘I add, ⌘' virtual copy, ⌘R show in Finder, F2 rename, ⌫ delete, ⇧⌘E external editor
KEYC-COMPARE Classic grid/compare keys (Z, Home/End, =/−, ⌘⇧D, S…) P2 🟡 cmd:view.zoomToggle Z toggles zoom; no compare, stacks, thumbnail-size keys
KEYC-RATING Classic rating/flag keys (1–5, ⇧1–5, 6–9, P, X, U, ⇧X, ⇧U, [ ], `) P2 🟡 cmd:photo.rate, cmd:photo.pick, cmd:photo.flag 0–5 / Shift+0–5 and 6–9 / Shift+6–9; P/X/U, Shift+X/U and grid Shift+P work; tests cover batch/undo, active Compare candidate and text focus. Missing: Shift+P outside grids (Presets), rating [ / ], flag cycle and filter-bar keys
KEYC-COLLECTIONS Classic collection keys (⌘N, B…) P2 🟡 cmd:dialog.newAlbum ⌘N new album; no quick collection
KEYC-METADATA Classic keyword/metadata keys (⌘K, ⌘S, ⌘⌥⇧C/V…) P2 🟡 cmd:photo.saveMetadataToFile ⌘S saves metadata; no keyword sets, metadata copy/paste
KEYC-DEVELOP Classic develop keys (V, ⌘U, ⇧⌘U, R, Q, K, M, ⇧M, ⇧W, ⇧J, ⇧Q…) P2 🟡 cmd:develop.treatment, cmd:develop.reset, cmd:crop.reset V, ⇧⌘R, ⌥⌘R, W, J, Y, ⇧Y, \ match; R/K/M/⇧M differ; no Classic keymap layer
KEYC-MODULE-OUTPUT Book / slideshow / print / map / web keys P2 ⬜ modules not implemented
KEYC-HELP Classic help keys (⌘/, F1) P2 🟡 cmd:app.shortcuts ⌘/ only

Japanese text watermarks

app.export watermark objects accept vertical (boolean, defaults to false). The export dialog offers Horizontal/Vertical and multiline text. Japanese glyphs fall back to the craft-fonts Mincho faces (BIZ UDMincho; on the web build, BIZ UDPGothic) when built with CRAFT_FONTS_DIR. Vertical lettering uses upright em cells and right-to-left newline columns; Latin stays upright. Advanced Japanese composition remains open. Regression coverage: export::tests::japanese_watermarks_support_vertical_columns, japanese_watermark_options_and_legacy_defaults, watermarks_work_without_craft_fonts.