Skip to content

Release v1.8.0#247

Merged
krissen merged 12 commits into
masterfrom
dev
Jul 18, 2026
Merged

Release v1.8.0#247
krissen merged 12 commits into
masterfrom
dev

Conversation

@krissen

@krissen krissen commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Release v1.8.0. Se CHANGELOG.md.

krissen added 12 commits July 17, 2026 15:24
…og (#235)

* (moduleRegistry) Turn module map into a declarative role catalog

Replace the {id -> component} map with MODULE_CATALOG, where each module
carries a placement role (main/side/bottom) plus metadata (singleton,
solo, step, keepMounted). MODULE_COMPONENTS and MODULE_TITLES are derived
from the catalog so existing callsites keep working. Add getModuleRole()
and isSingletonModule() (singleton defaults true).

* (placement) Dock new modules by role, not the active tabset

openModule() resolved its target via resolveTargetTabset, which prefers
the active tabset — so the same command placed a module differently
depending on where the user last clicked (the file queue could land in a
narrow 15% side column). Add a pure resolvePlacementTabset(model, role):
main -> largest tabset (weight fallback before first measure), side ->
existing narrow left column or a LEFT split, bottom -> existing bottom
bar or a BOTTOM split. openModule looks up the module role and applies
the placement descriptor, with an options.placement escape hatch for
callers that force a target. SINGLETON_MODULES is derived from the
registry instead of a hand-held list. resolveTargetTabset is unchanged
for the active-tabset (keyboard-ownership) choice.

* (docs) Note role catalog + deterministic placement

Record the module role/keepMounted foundation under the FileQueue
companion roadmap entry, and add an Unreleased Changed note for
role-based tab placement.

* (workspace) Update stale handleOpenCulling ordering comment

The open-before-close comment justified the order via the active tabset
(getActiveTabset → undefined), a dependency removed when openModule
switched to role-based placement. Reword it to the actual remaining
reason: opening culling first guarantees a host tabset before closing
Review could empty the layout.

* (placement) Require measured rects before reusing a bottom bar

FlexLayout's getRect() returns a zero-sized Rect (not null) before the
first render/measure pass, so the bottom-role branch's truthy guard let
threshold become 0 and matched every other unmeasured tabset at y === 0
(e.g. the left Review column) — stacking Log Viewer into the side panel
instead of splitting a bottom bar. Require non-zero width/height on both
the main area and the candidate (isMeasured); until measured, fall back
to a BOTTOM split. Add a headless test for the all-zero-rect case.

* (placement) Size a fresh split to the module's role weight

A new LEFT/BOTTOM split gets FlexLayout 0.8.17's default sizing, which
halves the target — so a side/bottom module opened from a solo workspace
landed at 50% despite its catalog weight (15/20), and could end up
weighing >= the main area, making later role resolution (main picked by
weight) ambiguous. Add applyPlacement() in tabsetUtils, which pins the
new tabset to the module's declared weight and gives the rest to main;
openModule now routes through it with getModuleWeight(moduleId). Give
log-viewer an explicit weight (20) so every split-producing role carries
one.

* (test) Add placement invariant matrix (role x layout state)

Consolidating matrix that enumerates role (main/side/bottom) x layout
state (measured/unmeasured, target present/absent, solo) and asserts both
the resolved placement descriptor and that a resulting split never weighs
the new side/bottom pane >= main — so role resolution stays unambiguous
after a split. Closes the placement-edge-case class.

* (placement) Gate side/bottom reuse on hosting a same-role module

The reuse predicates matched on size alone: any tabset narrower than main
counted as a side column, any measured tabset below main as a bottom bar.
With the Database layout (database-management weight 30 beside statistics
70) that hijacked the narrow Database column — file-queue/review docked
onto it, hiding Database — instead of splitting a fresh side pane. Gate
reuse on the tabset already HOSTING a module of the wanted role, looked up
via an injected roleOf(componentId) so tabsetUtils stays React-free; the
workspace passes getModuleRole. Mirror the same gate for bottom-bar reuse
(a panel below main hosting a non-bottom module is not a bottom bar). No
side/bottom-hosting tabset → fresh split, as before.

* (test) Cover role-hosting reuse gate in placement matrix

Extend the fixtures with occupant components and pass roleOf. New cells:
Database 30/70 narrow non-side column → split not hijack; a genuine
side-module tabset → reuse; a measured panel below main hosting a
non-bottom module → split; and reuse disabled when roleOf is omitted.
Also assert a freshly split side pane becomes reusable next time.
* (workflowSteps) Extract shared pipeline-step derivation from landing

Pull the workflow step list, tools list and continue-where-you-left-off
mapping out of StartupLanding into workspace/flexlayout/workflowSteps.js,
deriving each step's module id from the catalog's step metadata (add
getModuleStep to the registry). StartupLanding now consumes the shared
module so the WorkflowBar can render the same steps from one source.

* (workflow-bar) Add persistent WorkflowBar above the layout

Slim, always-visible pipeline row (Importera → Byt namn → Granska →
Räkna → Gallra) rendered by FlexLayoutWorkspace above the FlexLayout
host. Highlights the active step (tracked in activeStep, set by step
clicks and the existing hand-off events), shows a working-folder chip
with a Continue button, and a Verktyg menu for non-pipeline modules.
Steps/tools come from the shared workflowSteps catalog. Gated by the new
workspace.showWorkflowBar preference (default on), toggle in Layout
prefs; the bar re-reads on preferences-changed.

* (docs) Add ux-principles, document WorkflowBar

New docs/dev/ux-principles.md codifies the applied Nielsen heuristics,
navigation rules and style rules behind the pipeline navigation; linked
from CLAUDE.md Related Docs. Add a workspace-guide section on the
workflow bar, a CHANGELOG Added entry, and ROADMAP updates (nav-series
status + the resolvePlacementTabset/getMaximizedTabset known issue).

* (workflow-bar) Guard step switch against unsaved Review edits

A WorkflowBar step click runs openLandingStep, which replaces the layout
model and unmounts a live ReviewModule — dropping its unsaved
confirmations/ignores past the reviewDirtyRef guard the culling hand-off
honors. Reuse that same signal: openWorkflowStep now confirms via the
shared useConfirm primitive when Review is dirty and clears the dirty set
on a confirmed discard; cancel leaves the layout untouched. Expose
openWorkflowStep on window.workspace and pin the guard with tests.

* (WorkflowBar) Stop clipping the Verktyg dropdown

overflow:hidden on .workflow-bar clipped the absolutely-positioned tools
menu, making it invisible. Drop it; clip the step group itself instead so
the chip/tools stay visible when narrow, and give the bar a positioned,
lifted stacking context so the dropdown paints above the FlexLayout host
that follows it in the DOM.

* (workflow-bar) Mark Review active for the queue-files launch path

The bare `ansikten -q FILER` launch (handleQueueFilesIpc) brought up the
review pipeline without setting activeStep, so the WorkflowBar highlighted
no step for that entry. Set activeStep('review') at the head of the
handler, matching the other hand-off handlers.

* (WorkflowBar) Fix stale header comment on step opening path

onOpenStep is wired to openWorkflowStep (the dirty-guarded wrapper), not
openLandingStep directly.

* (workflow-bar) Treat active-step click as focus, not reload

Clicking the already-active step reached the dirty-prompt + openLandingStep
reload path, so a dirty Review would prompt and lose its edits to a
queue-review reload even though the user never left the step. Short-circuit
in openWorkflowStep: when the clicked step equals activeStep, focus its
surface via openModule and return before the guard. Test pins that an
active-step click while dirty neither confirms nor rebuilds the layout.

* (workflow-bar) Gate active-step focus no-op on module presence

The active-step focus fast-path fired even when activeStep had gone stale
— the user closed the step's tabs or loaded another layout from the menu
while activeStep lingered on 'review'. openModule then opened a bare
Review with no queue/viewer (a dead end). Gate the no-op on the step's
module still being in the model (hasModuleTab); when its tab is gone, fall
through to the full path so openLandingStep rebuilds the surface. Test
pins the stale-activeStep divergence; the genuine no-op regression test
stays green.

* (workflow-bar) Never discard-prompt on an already-mounted step

Starting in the default/reset Review layout activeStep is null, so a
click on Granska with unsaved edits missed the focus fast-path and ran
the dirty prompt + queue-review rebuild — discarding edits for the step
the user was already on. Broaden the fast-path: focus (no rebuild) when
the clicked step's module is mounted AND (it's the active step OR Review
is dirty). A clean click on a mounted-but-not-active step still falls
through to build the canonical surface (no queue-less dead end), and a
stale activeStep whose tab is gone still rebuilds. Tests pin both new
cases; the no-op and divergence tests stay green.

* (workflow-bar) Sync bar highlight when fast-path focuses via dirty clause

The focus fast-path didn't call setActiveStep, so when it fired through
the dirty clause with step !== activeStep (cold start: activeStep null,
dirty click on a mounted step), the bar marking stayed stale. Set
activeStep(step) alongside the focus so the highlight follows focus
regardless of dirty state.

* (workflowSteps) Normalize Windows separators in basename

The folder picker / backend can return C:\events\cupen; basename only
split on '/', so the whole path showed in the working-folder chip instead
of the folder name. Convert backslashes to '/' before deriving the last
segment. The app ships on Windows.

* (workflow-bar) Derive activeStep in openModule from step metadata

View-menu / Verktyg-menu commands route straight through openModule, which
never synced the bar highlight, so opening Räkna/Gallra/Import left the row
blank or pointing at the previous step. Sync at the root: after a successful
open/focus, openModule reads the module's step from the catalog and marks it
active; stepless modules (Inställningar, Statistik, Loggar) leave activeStep
untouched. This covers the menu, the Verktyg menu and window.workspace
callers in one place, so the three hand-off handlers that route through
openModule drop their now-redundant setActiveStep; the loadLayout-path
handlers (review-queue, queue-files) and the openWorkflowStep full path keep
theirs. Expose activeStep on window.workspace; tests pin step + stepless.
…he model (#237)

* (workspace-morph) Spike: verify moveNode preserves component state

flexlayout-react 0.8.17: Actions.moveNode retains the moved tab's node id,
so React keeps the same component instance across the move — state survives
both a move into an existing tabset and into a fresh split. This is the
load-bearing assumption for the morphing engine (no park-in-border fallback
needed).

* (workspace-morph) Add declarative step specs + morphing engine

workflows.js declares each pipeline step's pane spec (solo modules; review =
file-queue|review|image-viewer at 15|15|70) derived from the role catalog.
workspaceMorph.applyWorkspace reshapes the LIVE model into a spec over
FlexLayout Actions (moveNode/addNode/deleteTab/updateNodeAttributes) so mounted
modules keep their state; keepMounted (File Queue) and dirty (Review) modules
are parked in a collapsed bottom border instead of being closed. layouts.js
gains ensureBottomBorder so every model has a border to park into.

* (workspace-morph) Wire morphing engine into step navigation

enterStep(stepId) morphs the live model via applyWorkspace and is the sole
structural layout-change path; openWorkflowStep keeps PR 2's pinned
dirty/mounted fast-path but its rebuild branch now morphs instead of replacing
the model, so a step switch no longer discards a dirty Review — the discard
prompt is gone (kept only on Reset layout, the one destructive rebuild). CLI/
hand-off IPC (culling/import/rename/review/queue-files) and FileQueue.loadFile
route through enterStep; openModuleSolo/openLandingStep/ensureReviewSurface/
ensureQueueMounted are removed. Cmd+1..5 now drive the pipeline steps; the old
layout templates move to an accelerator-free Window submenu.

* (workspace-morph) Test step specs, Cmd routing and morph round-trip

workflows.test.js pins the spec shapes/order and primaryModuleOf;
menuCommandsSteps.test.js pins Cmd+1..5 → openWorkflowStep routing and the
dirty-guarded reset-layout; flexLayoutWorkspace gains a review→culling→review
round-trip asserting the File Queue node id survives the park/un-park.

* (workspace-morph) Document morphing navigation + Cmd+1-5 remap

ux-principles rule 3 now describes the realized morphing engine (enterStep /
applyWorkspace, background parking, no discard prompt on step switch); the
workspace guide documents Cmd+1-5 stepping and the moved layout templates;
CHANGELOG [Unreleased] and ROADMAP record the change including the Cmd+1-5
muscle-memory break. Drops two unused test imports flagged by eslint.

* (workspace-morph) Keep the active pane on idempotent re-entry

applyWorkspace no longer forces the primary pane active when the currently
active tabset is already one of the step's panes. Without this, FileQueue's
enterStep('review') on every file load would yank keyboard focus from the
Review panel to the Image Viewer. A cross-step morph (active pane gone) still
elects the primary.

* (workspace-morph) Label parked tabs by module for distinguishability

A parked tab is now named "Bakgrund: <module>" (e.g. "Bakgrund: Filkö") so
several modules parked at once stay distinct in the background border.

* (workspace-morph) Guard parking state-preservation at the React mount level

workspaceMorph.test.js proves parking via the node-id proxy; this adds a
mount-level round-trip (stateful probe in the review trio → count parks it →
review un-parks it) asserting mount-count stays 1 and internal state survives.
Pins the invariant directly, so a regression in FlexLayout's per-tab
render-on-demand honoring in a collapsed border would fail here even if the
node-id proxy still passed.

* (menu) Drop the mislabeled Helbild layout template

The Helbild item never mapped to a full-image layout — its action loaded the
review layout — and a full image is already reachable via Visa → Bildvisare
(Cmd+,), so it was a confusing duplicate among the moved secondary templates.
Removed; Jämförelse and Statistik remain with accurate labels.
…off polish (#238)

* (scanScope) Add display-only subscribeScanScope

* (queueStatus) Publish queue snapshot + label source folder in header

* (workflow-chain) Complete Fortsätt chain review to count to culling

* (workflow-bar) Working-set status dropdown on the folder chip

* (handoff) Standardize step hand-off buttons to "Nästa steg: <verb> →"

* (docs) Document working-set chip, queue labelling and hand-off chain

* (workflow-chain) Skip Räkna adopt-on-mount when open-count signals external load

* (workflow-bar) Use role=group for the chip status popover, not role=menu

* (workflow-chain) Consume external-load flag in count-load to close the already-mounted leak
…CLI fix (#239)

* (launch-command) Resolve CLI launch to one workspace command post-expansion

Pure, filesystem-free decision (expanders injected) turning a parsed CLI
intent into a single workspace command. The decision is made AFTER path
expansion, so a path that matches nothing still yields an explicit command
(open the step empty) instead of leaving the renderer to guess landing
suppression from raw argument counts and strand in the default layout.

* (workspaceCommands) Add the single workspace command router

Typed-intent router with an intent buffer: intents dispatched before the
workspace signals ready are queued and flushed in order on markReady (the
"router not up yet" race). Hand-off intents (queue-files, open-culling,
open-count, open-import, open-rename-nef, open-review-queue, load-image)
share one morph + waitForListeners + emit implementation, so the cold-mount
guard (the "target module not mounted yet" race) is written once and
behaves identically for every hand-off. signalExternalLoad for the count
hand-off is preserved via the HANDOFFS descriptor.

* (workspace-router) Route all opens through the router + ready handshake

Consolidate the three "open" mechanisms and the CLI launch bridge into the
single command router. The menu dispatch table, window.workspace, and the
in-app moduleAPI open-* events are now thin adapters that build a typed
intent and call dispatch; the router owns routing and the per-hand-off
waitForListeners guards.

Cold-start launch now uses a workspace-ready handshake instead of a timer:
the renderer signals workspace-ready once its router and listeners are live,
and the main process holds its resolved launch command until then, sent over
one unified workspace-command IPC. This replaces the did-finish-load + 1000ms
setTimeout timing lottery (removed). The renderer no longer guesses landing
suppression from raw argument counts — it reads the post-expansion willLaunch
boolean — and a CLI path that expands to nothing opens the target step empty
(e.g. `ansikten culling /typo`) rather than stranding in the default layout.

Guards removed and what replaces them:
- did-finish-load + 1000ms setTimeout (x3: culling/import/queue): replaced by
  the deterministic workspace-ready handshake (main sends only after the
  renderer's listeners are live).
- Renderer raw-arg landing guess (hasFiles/clear/verb): replaced by the main
  process's post-expansion willLaunch boolean.
Guards kept verbatim (relocated into the router): the per-hand-off
waitForListeners cold-mount guard (buffer does not cover lazy module mount),
the review-dirty/keepDirty morph semantics, and signalExternalLoad for count.

* (docs) Document the command router, handshake and CLI empty-expansion fix

Strike the resolved CLI empty-expansion roadmap item; add CHANGELOG entries
(Changed: router consolidation + workspace-ready handshake; Fixed: CLI path
that expands to nothing opens the step empty instead of stranding). Add
navigation rule 6 (one router opens everything; handshake not timer; launch
decided post-expansion) to ux-principles, and note the CLI empty-path
behaviour in the user workspace guide.

* (launch-queue) FIFO hold-until-ready queue for launch commands

Pure (Electron-free, DI delivery) queue that holds workspace commands until
the renderer completes the workspace-ready handshake. It is a FIFO queue, not
a single slot, so an initial CLI launch and a second-instance launch that both
arrive before ready deliver in order instead of the second clobbering the
first. markNotReady() re-arms the hold (queue preserved) so a caller can stash
commands across a renderer reload.

* (launch) Queue launch commands and re-arm the hold across renderer reloads

Replace the single launchCommand slot + workspaceReady flag with the FIFO
launch queue. Two robustness fixes:
- Renderer reload (Cmd+R / crash) previously left workspaceReady stuck true,
  so a second-instance command mid-reload was sent into a router-less page and
  dropped. markNotReady() on webContents 'did-start-loading' re-arms the hold;
  the command queues and delivers on the next workspace-ready (the renderer
  re-signals per mount).
- A second launch arriving before the first handshake overwrote the initial
  launch (single slot). The FIFO queue delivers both in order.
…doc sweep (#240)

* (startup-landing) Trim to a welcome card; bar owns navigation

The always-visible WorkflowBar already provides the pipeline steps, the
working-set chip, the Fortsatt continuation and the Verktyg tools menu at
all times — including on an empty workspace. Re-rendering the step grid,
tools grid and continue row on the landing only duplicated the bar (the
double-Fortsatt-buttons problem).

Reduce StartupLanding to a pure welcome/orientation card: title, subtitle
and a hint pointing at the bar. It no longer needs backend, moduleAPI or
working-folder wiring. The bar is the single source for navigation and
continuation.

* (step-layout-memory) Remember each step's layout across switches

Each pipeline step (import/rename/review/count/culling) now remembers the
shape the user leaves it in — an extra pane, changed pane weights — and
restores it on re-entry instead of the bare factory layout (Capture One
workspaces pattern, Nielsen N7).

stepLayoutMemory.js stores one pane spec per step under
ansikten-workspace-<step>. A spec ({ moduleId, weight }[]) — not a full
model — feeds the existing non-destructive morph, so restoring a
remembered layout preserves live module state and the parked-in-background
semantics. A snapshot reads only real tabsets, so border-parked Bakgrund
tabs never leak into a step's memory. resolveStepSpec merges saved with
factory so a step's essential modules can never go missing.

Persistence is scoped and settled: handleModelChange writes the active
step's spec on real user changes only (programmatic morphs are suppressed);
changes with no active step are not remembered. Mount always builds the
neutral default preset — memory surfaces only when a step is entered. The
legacy ansikten-flexlayout key is migrated once into review's memory, then
removed.

Reset layout now forgets the current step's memory and rebuilds it to
factory; new Reset all layouts (Window menu) forgets every step. Both keep
the dirty-Review confirm.

* (cleanup) Drop dead layout commands and unused fullImage key

The command-table entries layout-full-review and layout-template-full-image
are inert — no menu item dispatches them any more (Helbild was removed from
the Layoutmallar submenu when the workflow bar took over Cmd+1..5). Remove
them and the now-unused menu.window.fullImage i18n key.

* (docs) Sweep for per-step memory, landing role and reset semantics

- ux-principles.md: navigation rules 7 (per-step memory) and 8 (landing is
  orientation, not navigation); reset semantics in rule 3 updated to
  morph-to-factory for a step; N4/N7 reflect the bar as the single nav source.
- workspace-guide.md: per-step layout memory + Reset all layouts; startsida
  rewritten as a welcome card pointing at the bar.
- ROADMAP.md: drop the delivered Arbetsflodes-layoutpresets item; refresh the
  FileQueue n/p entry (companion foundation exists via the role catalog, the
  gate itself remains a future PR).
- CHANGELOG.md: Added (per-step memory), Changed (landing trim), Removed
  (dead menu commands + fullImage key).
…Alt (#241)

The document-level review keydown handler is active over the module's
companion panels (image viewer / original view). Its digit (1-5) and
letter (a/i/r/x/m) shortcuts matched on the bare key without checking
modifiers, so a Cmd+1..5 menu accelerator was consumed by preventDefault
before Electron could fire it (step switch never ran) and also wrongly
selected a face alternative. Require no Cmd/Ctrl/Alt for those bare-key
branches so modified presses fall through to the accelerator while plain
presses still act. Shift+Cmd+A (accept all) keeps its own branch.
…p fix (#242)

* (landing) Render welcome card below the WorkflowBar, not over it

The StartupLanding welcome card was a full-viewport overlay
(position: fixed; inset: 0; z-index: 1000, left over from before the
persistent WorkflowBar existed), so on a normal start it painted over the
bar and only the card was visible.

Move the card inside the layout host (.workspace-layout-host, the area
below the bar) and switch its CSS to an absolute fill with
z-index: var(--z-sticky) — above module content, below modals. The bar
now stays visible and clickable while the card is up; ShortcutsHelp and
confirm dialogs still paint above it as higher-z siblings of the shell.

* (welcome-first-run) Show the welcome card only on the first session

The welcome card appeared on every non-CLI start. It is now first-run
only: a persistent localStorage flag (ansikten-welcomed, welcomeFlag.js)
is set the first time the card is dismissed — opening a step, loading an
image, or closing it from the menu — routed through a single
dismissLanding() helper. Later launches drop straight into the workspace
with the WorkflowBar and no card; an emptied workspace no longer brings
the card back for a returning user.

The flag fails open toward showing (missing/corrupt counts as not yet
welcomed), and CLI launches (willLaunch) still never show it. A new
Help ▸ "Visa välkomstguiden" menu item (show-welcome) re-shows it on
demand, wired through the menu-command table.

Tests: welcomeFlag unit tests, plus workspace tests for visibility
structure, first-run set/skip, corrupt fail-open, CLI suppression, and
the show-welcome re-show.

* (welcome-first-run) Keep the empty-workspace card as an unconditional fallback

The first-run flag should govern only the at-start card shown over the
non-empty default layout. Closing every view is a separate trigger: the
empty model must always bring the card back so the workspace is never a
dead end, regardless of whether the user has been welcomed. Drop the
!hasBeenWelcomed() guard on the empty-workspace re-show; the card fills
the host under the always-visible WorkflowBar.

Add a test for the unconditional fallback and document the two-trigger
semantics in ux-principles and the workspace guide.

* (roadmap) Note Review arrow-key double-fire (Cmd+Arrow steps face and swaps panel)

* (shortcuts) Wire Help ▸ Keyboard Shortcuts to toggle the overlay

The show-keyboard-shortcuts menu command (and its Cmd+/ accelerator) had
no renderer handler and fell through to a no-op moduleAPI broadcast, so
the menu item did nothing — only the `?` key opened the overlay.

Add a toggleShortcutsHelp callback to the menu-command context and map
show-keyboard-shortcuts to it, matching the `?`-key toggle. The menu item
now opens (and closes) the ShortcutsHelp overlay.

* (welcome-first-run) Mark welcomed only when the card was actually shown

A CLI launch (willLaunch) renders no card, but its launch command opens a
step/module that still reaches dismissLanding — which unconditionally set
the welcomed flag, silently burning the first-run guide for a CLI-first
user on their later normal start.

Gate markWelcomed on the card being visible at dismissal: dismissLanding
now uses a functional setShowLanding updater to read the live value and
marks welcomed only when it was showing. markWelcomed stays idempotent so
a StrictMode double-invoke is harmless. Add a test asserting a CLI-launch
dismissal leaves the flag unset and a later normal start still shows the
card; normal dismissal still sets the flag.
…er in Review (#243)

* (workspace-morph) Support tab groups in step specs, morph and memory

Extend the pane-spec format so a step column can be a tab GROUP
({ tabs, active, weight }) rather than only a single module, and teach the
morph engine to build/diff grouped columns non-destructively (preserved node
ids, idempotent fast path, keepMounted tabs pinned to enableRenderOnDemand:false
so a hidden companion stays mounted). Per-step memory snapshots and restores
grouped tabs incl. the active tab, and a one-time migrateReviewMemoryShape folds
an old three-column review memory into the new companion-tab form.

* (workspace) Review file queue as companion tab: migrate memory + surface viewer on load

Wire migrateReviewMemoryShape into workspace init so existing installs adopt the
new review default, and surface the Image Viewer tab on image-loaded when it is
hidden behind the File Queue companion tab (programmatic selectTab, no DOM-focus
steal) so a queue-driven load is always shown.

* (FileQueueModule) Gate n/p on mounted-not-parked so navigation survives being the hidden companion tab

The queue's n/p (next/previous file) were gated on tab visibility, which breaks
the owner's primary review gesture the moment the Image Viewer tab is raised over
the hidden File Queue companion. Gate n/p on mounted-and-not-parked (parent is a
tabset, not a background border) instead; a queue parked under a different step
stays inert. Focus-dependent keys (Cmd+F, Cmd+A, /) remain visibility-gated.

* (test) Cover review companion-tab shape: tab groups, hidden-mount, n/p gate, memory migration

Update the review-shape assertions to the new review-module | [image-viewer,
file-queue] form, and add coverage for the pane-shape helpers, grouped-column
build/idempotence, the hidden File-Queue staying mounted (React mount level), the
mounted-not-parked n/p gate, grouped-tab snapshot/restore and
migrateReviewMemoryShape.

* (docs) Document review companion-tab layout, tab-group support and companion invariant

CHANGELOG Changed entry; ROADMAP marks the n/p companion-gate item done;
ux-principles gains navigation rule 9 (companion drivers stay mounted, keys gate
on mounted-not-parked, surface the partner on load) and a grouped-tabs memory
note; workspace-guide describes the new Granska layout, Cmd+Shift+U and n/p.

* (test) Cover image-loaded surfacing of the hidden Image Viewer tab

Extract the inline image-loaded effect body into a behavior-neutral pure helper
revealHiddenModuleTab(model, moduleId) in workspaceMorph.js (selects a module's
tab only when it is hidden behind another tab in a real tabset) and call it from
the workspace effect, so the behavior is unit-testable. Add two rendered-Layout
tests: (a) viewer hidden behind the File Queue tab is surfaced on image-loaded;
(b) an already-visible viewer is a no-op (no extra selectTab).
…hosphor (#244)

* (workflowbar-autohide) Autohide the workflow bar with retro slide + phosphor

Default-on autohide: the workflow bar slides out of flow and overlays the
top of the layout host (Lightroom module-row style), so the workspace
reclaims the height and FlexLayout never re-layouts as it shows/hides. It
returns on a step change, a top-edge hover-zone, or keyboard focus; an open
dropdown, focus-within, or the pointer resting on it pause the hide timer.

Timer logic lives in a headless hook (useWorkflowBarAutoHide); the slide and
accent phosphor pulse are CSS-only using existing theme tokens and are
neutralised under prefers-reduced-motion. A new independent preference
(workspace.workflowBarAutoHide, default true) offers the always-visible
opt-out, separate from showWorkflowBar.

* (workflowbar-autohide) Lift the autohide overlay above the welcome card

The autohide bar and its hover-zone overlay the layout host at z 6-7, but the
StartupLanding welcome card fills that host at --z-sticky (200) with default
pointer-events, so on the first-run / empty workspace it painted over the row
and swallowed its clicks — leaving a mouse user no way to reach the bar. Raise
the overlay + hover-zone to --z-sticky + 100 (still below --z-modal so modals
win). Update the StartupLanding comment to describe the new ordering.

* (workflowbar-autohide) Keep the hidden bar out of the tab order and reveal on focus

The hidden row was translateY(-100%) + aria-hidden, but its buttons stayed in
the tab order (focusable-inside-aria-hidden antipattern) and onFocus only
paused the timer without revealing. Replace aria-hidden with `inert` while
hidden — the whole row leaves the tab order and a11y tree and cannot take focus
or clicks; the catch-strip and Cmd+1..5 step changes remain the ways back in.
onFocus now also reveals, so a Tab into a still-visible, just-idle row surfaces
it before it slides away. Tests cover both: inert toggles with hidden state and
the hover-zone restores it, and focus reveals + pauses the hide timer.
…autohide gate (#245)

* (WorkflowBar) Float the revealed autohide bar as an inset pill

In autohide mode the revealed row now detaches from the edges — a horizontal
inset + small top gap (--space tokens), rounded corners (--radius-lg), an
elevated surface (--bg-elevated), a drop shadow (--shadow-lg) and a resting
accent phosphor glow — so it reads as a floating capsule rather than a docked
edge-to-edge strip. The phosphor keyframe settles onto the resting glow (no
snap), and the hidden transform clears the top gap while dropping the shadow so
nothing peeks past the top edge. The in-flow bar (autohide off) is unchanged.
No new theme keys; light + dark.

* (workflowbar-autohide) Suspend autohide on the welcome / empty workspace

The bar could slide away while only the welcome card (or an empty workspace)
was showing — hiding navigation with nothing behind it to uncover, and leaving
a mouse user no way in on first run. useAutoHide now takes a hasContent flag
that gates it exactly like the enabled opt-out: false keeps the bar revealed
with no timer, and a false→true flip (a view opens) starts it.
FlexLayoutWorkspace derives hasContent as !showLanding && tabCount > 0 (tabCount
tracked alongside the existing empty-workspace fallback) and passes it through.
Adds hook tests for the gate and updates the docs (this also subsumes the
Nagelfar first-run reachability residual).
…246)

* (roadmap) Log inherited micro-edge + navigation-series cleanup

* (docs) Document workspace router + workflow morph after nav overhaul

* (release) Prepare v1.8.0: changelog + version bump
@krissen krissen added the enhancement New feature or request label Jul 18, 2026
@krissen
krissen merged commit d5f16c6 into master Jul 18, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant