Files
lightcraft/apps/lightcraft-web
dcc8a62910 ui: first-run onboarding tour and What's New after an update (#2206)
* ui: first-run onboarding tour and What's New after an update

The very first launch now opens a short Welcome tour (six pages over the
real views: import, the Photo Grid, Detail, Assisted Culling, export and
print, and driving the app with agents over MCP), and after the app has
been updated the existing What's New dialog opens once on its own. Both
are skipped once the running version has been seen.

- UiState.seen_version: the last version whose startup dialog was shown
  (serde default/skip, so older ui.json files read as None = first run).
- new startup module: parse_version, startup_dialog (FirstRun only when
  nothing was ever seen; WhatsNew when the version changed, comparing
  through parse_version and falling back to string difference when either
  side is unparseable), and open_at_startup — headless hosts (snapshots,
  scripted runs, tests: session-local state) never gain a modal.
- UiState::record_startup_dialog_seen records on show, so a crash
  mid-dialog does not re-trigger it on every launch; ui.json saves every
  few seconds and at exit, as before.
- Dialog::Welcome renders the tour with Back / Skip / Next (Get Started on
  the last page); every string goes through tr(), every menu path names a
  real command (file.addPhotos, view.photoGrid, view.detail, dialog.cull,
  dialog.export, dialog.print).
- the decision runs once on the first frame (ui-egui, so desktop and web
  both get it); headless hosts and already-seen versions open nothing.

* ui: the integration harnesses are headless hosts

The accessibility and docking tests build the app directly and drive it
through logic() and ui(). On a fresh UI state the first frame therefore
opened the Welcome tour: its modal took the Tab focus the accessibility
tests walk (canvas_tool_sliders_are_reachable_with_tab,
tab_walks_the_right_panels_top_to_bottom) and the pointer the docking
drag aims at (real_drag_float_close_reopen_and_state_reload_keep_photo_tool_mode).

Mark both harnesses as headless hosts, as the Headless harness already
does: a headless host never gains a first-run modal.

* ui: translate the first-run tour into the 13 catalogs

The 21 strings of the first-run tour (its six pages, its window title and
its Next and Get Started labels) were in no catalog, so every language
showed the tour in English. They are now in each of the 13 catalogs under
crates/ui-egui/locales/, with the placeholders and punctuation of the
English kept: the ellipses, the menu separators, the middle dots, the
parentheses around shortcuts and the final full stop. ka.json stays
key-sorted; the other catalogs get the keys appended.

WELCOME_TITLE, WELCOME_NEXT and WELCOME_START name the three labels that
are not part of WELCOME_PAGES, so one test can check all 21 keys:
the_welcome_tour_is_translated_in_every_catalog. It fails with the catalogs
as they were.

The translations were written without a native speaker's review and need
one for each language before release.

* ui: translate the first-run tour into the four new catalogs

main added sv, fi, id and vi after this branch was cut, and
the_welcome_tour_is_translated_in_every_catalog iterates Locale::ALL, so
the tour's 21 keys were missing there. Same block, same place (after
"Visualize HDR"), in each catalog's own terms.

* ui: the lifecycle gesture test is a headless host

main's egui-037 lifecycle tests (added after this branch was cut) build
the app directly and drive it through logic() and ui(), so the first
frame opened the Welcome tour there: its modal took the dialog the
delivered gesture under test opens
(a_delivered_gesture_may_change_the_state_it_acted_on saw the tour where
it expected View Options).

Mark that one test's app as a headless host, as the accessibility and
docking harnesses already are: a headless host never gains a first-run
modal. The flag stays scoped to this test because the file's screenshot
tests exercise the headless-capture fallback, which a headless host
short-circuits.

* ui: build the tour's shortcut chords from the platform bindings

WELCOME_PAGES hard-coded Cmd+Shift+I, D and Cmd+P in its prose, and the
renderer translated those sentences verbatim, so Windows, Linux and
FreeBSD users were shown keys the app does not bind. The three pages
that name a shortcut now take the command's live binding at render time
(shortcuts::shortcut_of) and paint it the way the menus paint theirs
(menubar::shortcut_text: a symbol chord on macOS, Ctrl+... elsewhere);
no key is written in the source strings any more. A command whose
binding was removed shows the menu path alone.

The chorded sentences move from the message catalogs into tr_format!
messages (placeholders are format!-checked in every language), translated
in all 17 catalogs' -formats.json; the catalog keys stay, chordless, as
the no-binding fallback, and the_tour_is_translated_in_every_catalog
pins both per language.

* ui: the tour's Develop page names the Before/After command, not the Y key

The page's prose still hard-coded Y as the before/after key: remap or remove
that binding and the tour sends users to a command that no longer runs. The
body now names View › Before/After Side by Side the way the other pages name
their commands, and every catalog translates the new sentence with its own
menu label.

* ui: record the startup dialog once its input is not blocked

* Keep headless UI tests independent of onboarding

Assisted-by: OpenAI Codex

* Tests: integration harnesses mark the app as a headless host

Services has no headless field; the app does.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tests: Find Similar waits for the worker to set the row total

It lost that race twice on the Windows gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: eliets2 <287273517+eliets2@users.noreply.github.com>
Co-authored-by: FredApps <FredApps@users.noreply.github.com>
Co-authored-by: FredApps <153173854+FredApps@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-05 23:40:00 +01:00
..