Skip to content

Fix/ux UI issues#21

Merged
Mosmain merged 53 commits into
masterfrom
fix/ux-ui-issues
Jun 12, 2026
Merged

Fix/ux UI issues#21
Mosmain merged 53 commits into
masterfrom
fix/ux-ui-issues

Conversation

@Mosmain

@Mosmain Mosmain commented Jun 12, 2026

Copy link
Copy Markdown
Owner

No description provided.

Mosmain and others added 30 commits June 2, 2026 01:29
- #7 numpad hotkeys: emit canonical W3C codes (NumpadSubtract etc.)
  the global-hotkey crate accepts; old short tokens (numsub/nummult/
  numdiv/numdec/NumpadEnter->Enter) failed to register and silently
  reverted. Right-Alt: detect held modifiers by event.code so AltGr
  (key AltGraph) no longer reads as a bad main key.
- #6 re-applying an identical hotkey: idempotent register + a re-apply
  window event so a same-value re-record re-claims the binding;
  recorder shows an applied confirmation.
- #17 close ConfirmDialog when the overlay locks.
- #20 dismiss the RMB transparency menu on lock and on window blur.
- #22 Enter key saves the Tarkov paths form.
- #23 clamp the overlay onto a visible monitor at startup when the
  window-state plugin restored off-screen coords (2K -> FHD).

Adds hotkey.spec.ts (18 cases) cross-checking emitted tokens against
the global-hotkey 0.7.0 parser table.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The OS keeps every registered shortcut live during capture, so pressing
an already-bound combo fired its action and the OS swallowed the
keystroke before the webview saw it. That made three things impossible:
binding a combo that is already taken (e.g. the lock combo), re-recording
the identical combo (the keypress never reached the recorder, so nothing
saved), and capturing right-Alt combos that collide with a default
Ctrl+Alt binding on AltGr layouts.

The recorder now brackets capture with suspend/resume window events:
suspend unregisters every live shortcut, resume re-claims them all (which
also re-applies an unchanged re-record). Replaces the earlier reapply-
event hack, which never ran because capture itself was being blocked.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The suspend/resume change introduced a regression: recording a new combo
updates the ref (firing the change-watch) AND triggers resume, and both
called tryRegister for the same combo concurrently. The second register
failed with an already-registered error, the watch took its failure path,
and reverted the value the user just set — so no hotkey could be saved.

Register/unregister now run through a per-instance promise queue, so the
watch's run sees the resume's registration and short-circuits idempotently
instead of double-registering. The revert is also gated on the combo not
having ended up registered, as a second guard.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Console logs from a Russian layout confirmed right Alt arrives as
key="AltGraph" (code "AltRight") plus a synthetic LeftControl, with the
altKey/ctrlKey flags unreliable on the event. Windows global shortcuts
have no distinct "right Alt" modifier — AltGr is delivered as Ctrl+Alt —
so capture now keys off getModifierState('AltGraph') and emits
CommandOrControl+Alt+<key>, which actually fires when right Alt is pressed.

Also removes the green-ring + check "applied" flash from the recorder
(stylistically excessive); the chip already updates to the new combo, and
the same-value re-record is re-claimed by the resume path regardless.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A Ctrl+Alt bind captured from AltGr on a Russian layout silently stops
firing if the player switches to an English layout mid-game, where the
same physical right-Alt key is a plain Alt with no Ctrl. Strip the
synthetic Ctrl that AltGr injects and record just Alt, so the bind is
layout-stable. Windows global shortcuts can't distinguish left/right Alt
anyway, so plain Alt is the honest, portable representation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Right Alt on AltGr layouts (Russian etc.) is delivered by the OS as
Ctrl+Alt, so any bind made with it fires inconsistently across layouts —
it simply doesn't work while on the Russian layout. Rather than guess a
mapping, refuse to bind it: when getModifierState('AltGraph') is set the
recorder shows a clear message and waits for a different combo.

AltGraph is set only by a real AltGr, not by a manual left-Ctrl+left-Alt,
so genuine Ctrl+Alt combos (incl. the lock/airdrop defaults) are
unaffected — covered by a test.

Adds the hotkeys.altgr message (en + ru); restart vite dev to pick up the
new locale string (the i18n plugin pre-compiles JSON, no HMR).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- #11 SelectButton fluid: swap class="w-full" for the PrimeVue `fluid`
  prop across Player/Overlay/Language/Extracts settings so the option
  buttons actually stretch to fill the row (w-full only stretched the
  wrapper). Sliders keep w-full.
- #16 first-launch language: default to the system/browser language
  (navigator.language ~ "ru" -> ru, else en) instead of hardcoded en.
  Only used when nothing is persisted; a manual choice still wins.
- #12 drag handle: a dedicated top-left pi-bars grab handle on the
  overlay so dragging isn't a hidden affordance on the status pill.
  Tauri + unlocked only; title/aria from new i18n overlay.move.
- #14 animated border: replace the static solid edge with a dashed
  marching-ants stroke (OverlayBorder.vue → SVG, teleported to body so
  the opacity slider can't dim it; stroke-dashoffset keyframe, seamless
  loop, prefers-reduced-motion guard, relative-color fallback). Removes
  the now-dead overlay-unlocked class toggling from useOverlayBootstrap.

Restart vite dev to pick up the new overlay.move locale string (the
i18n plugin pre-compiles JSON, no HMR).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The marching-ants border had a visible seam at the top-left (the path
start): the rounded-rect perimeter isn't an integer multiple of the
14px dash period, and it changes with the resizable window, so the last
dash never met the first. Give the rect pathLength="100" and express the
dash + offset in those normalized units (0.5+0.5 period → exactly 100
dashes, offset -1 = one period). The pattern now divides the perimeter
evenly and closes without a seam at any window size.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replaces the top-left pi-bars handle with a top-center grabber: a tiny
always-visible nub (when unlocked) that expands on hover into a bar
showing the map name, and which is itself the drag region. Keeps the map
view clean by default while making "draggable" discoverable, and gives a
large, obvious grab target. Pure CSS group-hover reveal; same
startDragging() call.

The map name moves out of the top-right pill into the drag bar; the
right cluster is now a compact status dot + gear + close. Browser mode
(no window drag) keeps the status+map-name pill as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two follow-ups to the hover-reveal drag bar:

- Make it full-width instead of a centered pill. The entire top strip is
  the drag region, so the window can still be grabbed back when it has
  been dragged mostly past a screen edge (a centered handle could end up
  off-screen and unreachable). A fixed-height top zone keeps the hover
  target comfortable; the visible bar is a faint idle line that expands
  to a labelled bar.
- Keep the bar expanded while dragging. The OS move loop steals pointer
  events the instant startDragging() runs, dropping :hover and collapsing
  the bar mid-drag. Track a `dragging` flag (set on mousedown, cleared on
  mouseup or the first button-less mousemove) so the bar stays open for
  the whole drag.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Drag affordance reworked from the full-width bar (which the right-hand
status/settings/close controls overlapped, and which fused with the
dashed border):
- The visible part is an airy pill floating below the top edge, centred
  so it never collides with the right controls, with a ring + shadow so
  it reads as floating rather than blending into the border. It grows
  from a grabber into a bigger labelled bar (h-8, text-sm).
- The drag region stays full-width (invisible) so the window is still
  grabbable when dragged mostly past a screen edge; hover/expand is
  scoped to the centre so reaching for the controls doesn't trigger it.
- Manual window listeners replaced with VueUse useEventListener (auto
  teardown); the bar still stays open for the whole drag via `dragging`.

Settings drawer: when the overlay is small the drawer opens full-screen,
so there's no outside to click-dismiss and Esc is non-obvious. Added a
prominent close button in a custom header (closable=false). Adds a
`close` locale string (en + ru) and uses it for the overlay close button
too. Restart vite dev to pick up the new strings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…rome

- The settings drawer showed two close buttons: PrimeVue 4's Drawer prop
  is `showCloseIcon`, not `closable`, so `:closable="false"` was ignored
  and the built-in icon rendered alongside the custom header button. Use
  `:show-close-icon="false"` so only the prominent custom close remains.

- Add `showOverlayChrome` (shared/tauri.ts): a DEV-only flag that renders
  the Tauri overlay chrome (drag bar, animated border, lock indicator) in
  a plain browser so the overlay UX can be previewed/iterated without a
  Tauri build. Enable with `?overlay` or localStorage. It only flips
  render gates — native window calls still guard on the real `isTauri`
  (OverlayHeader's startDrag), and the IPC transport is untouched, so
  nothing tries to reach a Tauri runtime that isn't there. OverlayHeader's
  render prop is renamed `isTauri` -> `tauriChrome` to make the split
  explicit; index.vue feeds it `showOverlayChrome`.

Verified in the browser preview: airy centred drag pill expands on hover
with no overlap of the right controls, animated border, lock indicator,
and a single drawer close button all render correctly.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- Drawer: drop the custom header + close button and go back to PrimeVue's
  built-in close (the duplicate close came from recreating it). The
  default X + Esc are enough.
- Drag bar: the expanded bar is now full-width (unfurls from a centred
  grabber to a near-full-width title bar), and the hover trigger spans
  the full width of the top, not just the centre. The idle grabber is
  shorter (h-1.5). It stays full-width-grabbable for off-screen recovery
  and open while dragging.
- No control overlap: the bar is tall enough (h-11) to fully contain the
  right-hand status/settings/close controls, which sit on top of it
  (z-1001) like title-bar controls instead of half-overlapping it.

Verified in the browser preview (?overlay): idle short grabber, full-
width unfurl on hover with controls cleanly seated on the bar, and a
single drawer close button.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Following a UX design review, stop treating the drag bar as a container
for the window controls (the root of every overlap iteration). The top
band is now a single hover/drag trigger that reveals two SEPARATE,
never-overlapping zones as flex siblings:

- left: a drag pill (flex-1) carrying the map name — visual only, the
  band owns the drag so it stays grabbable full-width for off-screen
  recovery;
- right: the status/settings/close control cluster (shrink-0, own
  pointer events, mousedown.stop so it never starts a drag).

Being flex siblings, they can't collide at any width. Idle shows only a
small centred grabber nub; hover/drag cross-fades to the revealed chrome
(controls auto-hide together with the bar, per the chosen direction);
locked shows just the connection dot; browser keeps its static cluster.

Verified in the browser preview (?overlay): clean idle nub, full-width
reveal with a clear gap between the drag pill and controls, no overlap.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Changed the drag pill icon from 'pi-bars' to 'pi-arrows-alt' to better represent the drag functionality. This update enhances the visual clarity of the overlay header while maintaining the existing layout and behavior.
…touch floors

- #4 Auto-follow without zoom: add an "On" mode that recenters on every
  position via panTo() and keeps the user's current zoom; S/M/L keep the
  recenter+zoom behaviour. Enum gains 'on' (superset — existing saved
  values stay valid), with the new option + clarified hint in en/ru.
- #5 Looser map panning: bump PAN_PAD 0.15 -> 0.5 and soften
  maxBoundsViscosity 1.0 -> 0.5 so dragging the map doesn't hit a rigid
  wall.
- #15 Lock pins on top: the effective always-on-top is now
  alwaysOnTop || clickThrough, so locking keeps the overlay visible out
  of raid and unlocking restores the user's setting.
- #10 Floor switcher on touch: on `pointer: coarse` devices the floor
  buttons drop the small size and get >=44px tap targets with more gap.

Restart vite dev to pick up the new playerFollowOptions.on locale string.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Show both in-game clocks (left + the +12h right) as a small, non-
interactive chip in the top-left corner — visible even when the overlay
is locked, since the time is glanceable raid info like the map itself.

Math is a pure function in @shared/tarkov-time (Tarkov runs at 7× real
time, anchored to Moscow UTC+3, two clocks 12h apart — the canonical
tarkov.dev formula), unit-tested with 4 cases. The chip ticks once a
second from Date.now().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Zoom, floor switching and the airdrop trigger now also respond to the
keyboard in a plain browser / LAN phone, not just the Tauri overlay.
useGlobalShortcut stays Tauri-only (OS global shortcut); the new
useBrowserShortcut is its page-level counterpart, firing the same store
combos via a window keydown listener — no-op under Tauri to avoid
double-firing, ignores keystrokes typed into form fields, and
preventDefaults a match so combos like Ctrl+= don't also zoom the page.

Matching is a pure `matchesAccelerator(event, combo)` in the hotkey lib
(CommandOrControl matches Ctrl or Meta; other modifiers exact), unit-
tested. index.vue binds each map action to both transports via a small
helper. Lock stays overlay-only.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clock chip overlapped the full-width drag bar when the top header
expanded on hover. Drop it from top-3 to top-14 (just below the h-12
header band) so it never collides — still upper-left and always visible.
Verified in the browser preview.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Instead of pinning the clock low, let it float in the top-left corner
(top-3) and slide down to top-14 only while the drag-bar header is
expanded, then back up when it collapses — so it uses the corner when
free and gets out of the way when the header appears.

Adds a shared app-singleton useOverlayHeaderActive (hovered/dragging/
active) owned by OverlayHeader (reset on lock/unmount so it can't stick),
read by the clock to animate its `top`. Verified in the preview: corner
at rest, smooth slide to below the header on hover, back on leave.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The clock animated `top` (layout) while the header reveals via
transform/opacity (composited), so the two motions drifted. Slide the
clock with translate-y (translate-y-11 = the 44px to top-14) instead, so
both are composited transforms on the same 200ms ease-out and move in
lockstep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Lets the in-process HTTP server + watchers run standalone on :47474 so a
browser / LAN phone can connect (SSE, config) without launching the
overlay — handy for testing the browser-side features, and the server
half of the tray/headless story.

Makes the Tauri AppHandle optional through the watcher chain
(screenshots/logs/watcher/http_server/commands/lib): events always go to
the broadcast channel (SSE), and `app.emit` to the webview only when a
handle is present. The GUI path is unchanged (passes Some). Adds
`run_headless()` + a second binary `tarkov-checker-headless` (no
windows_subsystem, keeps a console) and a `backend:dev` pnpm script.

Verified by running the binary: GET /api/ping and /api/config both 200
with the resolved paths — tauri::async_runtime works fine with no app.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The browser shortcuts (#8) worked, but the settings section to see/
configure them was tauri-only — so a browser user couldn't tell what the
combos were or change them. Make the section visible with
`desktop-or-tauri` (desktop browser + overlay, hidden on phones since
they have no keyboard); the browser uses the same combos via
useBrowserShortcut. The lock recorder stays overlay-only (no window to
lock in a browser).

Verified in the preview: at >=640px the Hotkeys section renders with the
5 keyboard-relevant bindings (zoom x2, floor x2, airdrop) and no lock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The backend registers the five forwarded combos (zoom-in/out,
floor-up/down, airdrop) as OS-global shortcuts and broadcasts a
`command` SSE event, so the overlay, any browser, and LAN phones all
react regardless of focus. Combos persist in hotkeys.json and are
editable from any client via /api/hotkeys. The overlay lock stays
client-registered (recovery path). Drops the focus-only
useBrowserShortcut.

- shared: commandMessage in the SSE union + hotkeys-api schema
- backend: HotkeyController trait (TauriHotkeys via the plugin,
  StandaloneHotkeys via its own Win32 message pump in headless),
  HotkeyStore, /api/hotkeys GET/PUT + suspend/resume, command event
- client: hotkeys store is a thin backend sync layer, command dispatch
  in index.vue, useHotkeysSync (load + recorder suspend/resume bridge)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…fy refs

- CLAUDE.md: new "Backend-owned hotkeys" section + route/store updates
- README (root EN+RU), client/desktop READMEs: the LAN/phone interface is
  served by the in-process Rust helper, not the removed Node/Fastify server
- desktop README: replace the obsolete "out of scope" list with current
  capabilities, fix beforeDevCommand, note the client-side tray

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Opt-in, off by default. When zoomed in, a small faction-coloured arrow
sits on the viewport edge for each off-screen extract, pointing at it.
Rendered in an overlay div over the Leaflet container (not a pane) so the
arrows stay pixel-fixed while panning; recomputed on move/zoom. Toggle in
Settings -> Extracts.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Apply env(safe-area-inset-*) via .sa-* helpers to the clock, top clusters,
lock indicator and floor switcher so the iPhone notch / Dynamic Island /
home indicator / rounded corners don't clip them. viewport-fit=cover was
already set in index.html. Insets are 0 on desktop, so positions are
unchanged there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A teleported context menu (same pattern as MapQuickMenu) listing all
visible maps with the current one checked. Wired on the overlay drag band
and the browser status pill; skipped on narrow phone viewports.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a thick dark outline (paint-order: stroke) and bump the arrow to 22px
with a stronger drop-shadow. The thin filled arrows blended into the busy
map; the outline makes them legible on any background.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- The clock now sits in a 40px row (= the settings button height) so its
  centre lines up with the status/settings cluster across the top.
- Root height uses dvh instead of vh so iOS Safari's dynamic bottom bar
  no longer clips the floor switcher in portrait (landscape was already fine).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Mosmain and others added 23 commits June 4, 2026 23:06
Reorganize the settings Drawer into two tabs — Layers (map/extracts/player/
airdrop) and Settings (overlay/hotkeys/language/paths/pairing) — each a
PrimeVue Accordion of collapsible sections instead of a flat Fieldset stack.

- registry: SectionGroup is now layers|settings; sections carry a titleKey
  for the accordion header (the per-section Fieldset legend is dropped).
- SettingsPanel: Tabs + per-tab Accordion (multiple). Expanded panels persist
  per tab; desktop opens all by default, mobile starts collapsed to keep the
  drawer short.
- sections: drop the Fieldset wrapper and render bare content.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
)

The settings drawer is now non-modal so the map stays bright and interactive
behind it — toggling a layer (extracts/labels/follow/edge arrows) previews on
the map in real time instead of being hidden behind a dimming mask.

On phones the drawer becomes a bottom-sheet: auto height that hugs its content
(capped at 85dvh with internal scroll, rounded top) so the map peeks above and
the live preview stays visible. Desktop keeps the right-side panel.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an opt-in "minimize to tray" overlay setting. When enabled, the ✕ button
hides the window to the system tray and keeps the app running (no confirm,
since it's reversible) instead of quitting; the tray's Show restores it and
Quit exits. Default off keeps the ✕ as a confirmed quit.

- overlay store: minimizeToTray persistedRef (tc.overlay.minimizeToTray).
- useCloseConfirm: branch to window.hide() when the flag is on.
- OverlaySection: toggle + hint; i18n keys (en/ru).
- capabilities: grant core:window:allow-hide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the settings-drawer Layers tab with an on-map LayerRail; the gear
drawer now holds system settings only. Make the overlay's lock state coherent:
controls hide, glanceable read-outs stay.

- LayerRail (left edge): category icons (Karta/Igrok + dimmed future Loot/
  Quests) open flyouts; per-layer visibility toggle + the layer's section
  inline via a gear. Flyout anchored to the rail (no fixed/Teleport) so it
  doesn't drift in the WebView2 overlay.
- Per-layer visibility primitive: useLayerVisibility (tc.layer.<id>.visible),
  watched by the extracts/player/airdrop composables; MapLayerContext gains
  visible.
- Settings registry: group settings -> system, add subgroup
  (player/loot/quests); SettingsPanel drops the Tabs/Accordion layers tab and
  renders system sections only.
- Floors: stepper moved into the rail bottom; Alt+wheel over the map steps
  floors (captured before Leaflet zoom, throttled); a read-only floor-by-wheel
  hotkey row + a locked-state floor read-out chip.
- Map section: dropdown -> inline Listbox behind an auto-switch disclosure (no
  overlay flip in the 360px flyout). Player follow simplified to on/off.
- Hotkey rows: combo chip + Change button on one line; register the PrimeVue
  Tooltip directive.
- Edge arrows wrap the rail and re-flow on lock; merged clock+location pill
  stays glanceable in every state.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Bring the docs current with the overlay redesign committed in dcb1345:
settings split into the on-map LayerRail (layers) + a system-only gear drawer;
per-layer visibility (tc.layer.<id>.visible); floors moved into the rail +
Alt+wheel; coherent lock state (controls hide, read-outs stay) with the merged
clock+location pill; player follow simplified to on/off; edge arrows wrap the
rail. Also folds in earlier audit fixes (0.0.0.0 bind, minimize-to-tray, close
behaviour).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… cleanup

From the architecture + code review of dcb1345:

- useLayerVisibility: create the per-layer persistedRefs in a DETACHED
  effectScope so each one's localStorage watch survives MapView's `:key`
  remount on map switch. Previously the watch was captured by MapView's setup
  scope and died after the first map switch, silently stopping visibility
  persistence (real regression in a user-facing setting).
- Floors: rename nextFloor/prevFloor -> floorUp/floorDown and CLAMP (no
  wrap-around). Up now means a higher floor everywhere - the floor-up hotkey
  and the rail's up button were moving in opposite directions, and the hotkey
  wrapped while the rail clamped.
- Remove dead FloorSwitcher.vue (replaced by the in-rail floor stepper).
- Drop the orphaned settingsGroups.system i18n key (en/ru).
- CLAUDE.md: restore useFloorSwitcher (+ useLayerVisibility) in the
  composables list.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Eliminate the dual-registration smell: a layer lived in BOTH the map-layer
registry and the settings-section registry (group 'layers'), linked only by a
matching string id. The map-layer registry is now the single source of truth -
each layer self-describes its rail category, order, title and inline settings:

  registerMapLayer({ id, mount, category, order, titleKey, settingsComponent })

- LayerRail reads useMapLayers() and derives categories from each layer's
  `category` (CATEGORY_META is now just icon/teaser metadata, not the catalog);
  the base map selector is a deliberate direct MapSection import.
- Settings registry is system-only now: dropped group/subgroup, and
  useSettingsSections() takes no argument.
- Removed features/map/settings.ts and features/airdrop/settings.ts - their
  registrations moved into the layer index.ts files.
- useLayerVisibility keys off the one registry's ids.

Verified in preview: rail categories/order/toggles, live show-hide (persisted),
base-map flyout, system drawer. typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…tore (arch review #2)

- New useRailObstacle: LayerRail publishes its rect (viewport coords; null when
  hidden) to a shared reactive singleton; edge-indicator layers consume it.
- useEdgeIndicators takes a getObstacle() callback instead of
  document.querySelector('.layer-rail').
- useExtractsLayer no longer imports the overlay store: it reads the rail rect
  and repaints on change, replacing the clickThrough-driven rAF loop + the magic
  260ms transition timer. The rail owns its lock/transition - it clears the
  obstacle immediately on lock and republishes after the enter transition.

Future edge-indicator layers (loot/quests) reuse useRailObstacle - no copy of
the DOM scrape or the overlay coupling.

Verified: left-edge arrows still wrap the rail (x=57 = railRight 41 + 16).
typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…arch review #3)

The controls-hide / read-outs-stay lock policy was re-derived (!clickThrough) in
every chrome component and threaded as a prop through OverlayHeader /
OverlayLockIndicator. Centralize it:

- New useOverlayLock(): locked / showControls (derived) + lock() action.
- LayerRail, OverlayHeader, OverlayLockIndicator and pages/index.vue read the
  derived state from it instead of the store directly; dropped the
  overlay-click-through prop and the lock emit (the button calls lock() itself).
  App.vue keeps the raw ref - it owns the hotkey toggle + boot reset.

One definition of the policy, no prop-drilling - cheaper to extend as chrome grows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…registry (arch review #4)

Pin the contracts the scaling refactor introduced:
- useLayerVisibility: default-visible, singleton-per-id, persists to
  tc.layer.<id>.visible, reads an existing value on first use.
- useFloorSwitcher: floorUp/floorDown direction + clamp (no wrap), default-floor
  start, single-floor no-op.
- map-layer registry: register/expose + HMR-safe replace-by-id.

110 tests pass; typecheck + lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Layers now live in ONE registry (registerMapLayer carries category/order/
titleKey/settingsComponent) and the settings registry is system-only. Update
CLAUDE.md (Settings & layer registries + Map rendering layers) and
apps/client/README (recipes + where-things-connect): drop the layers
group/subgroup from the settings registry, note useRailObstacle + useOverlayLock.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Browser/phone-only Display settings section plus an on-map fullscreen
control for the second-screen phone use case.

- Fullscreen button in the top-right cluster (mobile/touch browser only);
  iOS Safari has no Fullscreen API, so it shows an Add to Home Screen
  popover instead. Hidden once installed as a standalone PWA.
- Keep screen awake (default on): Screen Wake Lock API in secure contexts,
  with a self-contained muted-video fallback for insecure LAN HTTP (the
  phone case). No nosleep.js dependency -- the proven MIT clips are
  embedded in-repo and driven by useNoSleepVideo.
- PWA manifest + iOS meta tags for Add to Home Screen.
- New browser settings-section visibility (!isTauri); Display at order 25.

Reviewed by architect and code-review agents; wake-lock state-machine and
denied-status fixes folded in. Verified: typecheck, lint, 110 tests, live
mobile preview (fullscreen, keep-awake status, video playback).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On iOS 15 (Safari < 16.4) the map overlay layout broke on a real phone: the
page could scroll, which let Safari's bottom toolbar retract and swell dvh,
and the vertically-centred LayerRail + its flyout landed off-centre.

Two root causes, both iOS-specific:
- A scrollable body let the toolbar toggle, making 100dvh unstable. Lock the
  app shell (html/body/#app overflow:hidden + overscroll-behavior:none) and
  size the root with the stable 100%/svh chain (h-dvh -> h-full).
- Tailwind v4 translate utilities rely on @property-registered vars, which
  Safari < 16.4 ignores, so -translate-y-1/2 produced an invalid translate
  declaration and centring silently failed. Replace it with a literal
  transform: translateY(-50%) on the rail and flyout.

Also switch the rail flyout / settings drawer max-heights from dvh to svh so
they stay stable under the locked toolbar.

Diagnosed and verified on an iPod touch 7 (iOS 15.8) via a temporary
viewport-metrics overlay (since removed): the rail now centres at the
viewport midpoint with no page overflow or scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…seconds

Mobile/phone UX pass.

- Top bar: fold the connection-status dot, the map name (>=380px) and the two
  in-game clocks into the always-visible left chip (TarkovTimeChip). The
  top-right browser cluster is now just the settings gear, so the bar no longer
  overflows on a 320px phone (iPod touch). Drop the clock icon.
- Clock: show seconds, sampled via requestAnimationFrame so they tick smoothly
  (in-game time runs 7x, so a fixed interval visibly stutters them); add the
  seconds field to @shared/tarkov-time.
- Remove the keep-awake feature entirely. There is no reliable web way to keep a
  mobile screen awake over plain HTTP: the Screen Wake Lock API needs a secure
  context (absent on iOS < 16.4, unreachable for a LAN phone) and the
  muted/unmuted looping-video trick no longer keeps the screen on under current
  iOS or Android (verified on iPod touch iOS 15 + Android). Deletes useKeepAwake,
  useNoSleepVideo, the embedded video asset and the display store. The Display
  settings section now holds only the fullscreen toggle (iOS shows an
  Add-to-Home-Screen hint).
- Remove the on-map fullscreen button; fullscreen lives only in Display settings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Show the hotkeys settings section on phones (read-only) and keep every
client's view in sync when a combo is rebound.

- Section is now visible: 'always'. Recording needs a physical keyboard (the
  recorder captures window keydown), so the rows go read-only on a phone --
  HotkeyRecorder hides its Change button, the Alt+wheel mouse row is hidden,
  and a hint points at the overlay for rebinding. Editable on the overlay +
  desktop browser as before.
- Live re-sync: a rebind now broadcasts ServerEvent::Hotkeys { config } from
  BOTH PUT paths -- put_hotkeys_http (state.event_tx) and the update_hotkeys
  IPC command (slot.event_sender(), the same broadcast channel) -- so other
  clients refresh over SSE. New hotkeysMessage in the wire union; useHotkeysSync
  applies it via store.applyConfig. The originating client already has the
  effective config from its response.
- Rename packages/shared/src/ws-messages.ts -> sse-messages.ts (the transport
  has been SSE since the WS migration) + all importers, the package.json
  export, and docs. Fix a pre-existing extensionless relative import in
  hotkeys-api (node16 needs the .js).

Verified: shared + client typecheck, lint, 110 tests; Rust built via tauri:dev;
live re-sync confirmed on an iPod touch (rebind on the overlay reflects on the
phone instantly).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ed code

Every `cargo test` run died at load with STATUS_ENTRYPOINT_NOT_FOUND
(CI Build-overlay run 16): the lib statically imports ComCtl32
v6-only symbols (SetWindowSubclass etc., via tray-icon/muda), and the
test harness exes never get tauri-build's embedded manifest, so the
loader bound comctl32 v5.82. build.rs now disables tauri-build's app
manifest (its entire content is the same dependency block) and emits
the equivalent /MANIFEST:EMBED + /MANIFESTDEPENDENCY link args for all
linked targets, tests included. Verified the shipped exe manifest is
equivalent via mt.exe.

Also rustfmt over already-committed unformatted files (hotkeys.rs,
lan.rs, events.rs) -- `cargo fmt --check` is the first step of
build-overlay.yml and was what failed runs 17/18.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…reenshots opt-in

Work-in-progress slice of PLAN-LAN-AND-TRAY.md: minimize-to-tray close
behaviour with a one-time native toast hint (src/notify.rs registers
our own AppUserModelID so the toast is not attributed to PowerShell),
the regenerated icon set from favicon.svg, the opt-in recycle-bin
delete of parsed screenshots, and the client-side wiring (overlay
store, tray composable, paths settings section, locales).

Also carries the CI fixes that live inside feature-touched files:
rustfmt across the Rust tree, clippy useless_vec in the logs.rs tests,
and the CORS preflight test now sends
Access-Control-Request-Private-Network -- tower-http only emits the
allow header in response to it (mirrors real Chrome behaviour).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ous files

- Adjusted formatting in `hotkey.spec.ts`, `tarkov-time.spec.ts`, and `useServerTransport.spec.ts` for better readability.
- Enhanced code structure in `settings/registry.ts` and `settings/sections/MapSection.vue` by consolidating lines.
- Improved layout in `TarkovTimeChip.vue` for consistent HTML structure.
- Updated `useTrayIcon.ts` to streamline import statements.

These changes focus on code clarity and maintainability without altering functionality.
Reformatted the layers section in CLAUDE.md to enhance readability and maintain consistency in the documentation. The changes include indentation adjustments for better visual hierarchy, ensuring that the structure of layers and their respective files is clear and easy to follow.
Changed the heading in the English and Russian locale files from "Tarkov paths" to "Game integration" and "Пути Tarkov" to "Интеграция с игрой" respectively. Updated visibility settings in the settings section to 'always' for improved diagnostics. Adjusted layout in PathsSection.vue for better user experience, including a more accessible toggle switch for deleting screenshots.
- Added new scripts for frontend, backend, and Tauri development in package.json.
- Introduced lint:fix script for automated linting fixes.
- Updated CI workflow to run the format check using the updated format script.
- Added lint:fix command to shared and client package.json files for improved linting capabilities.
- Added `UpdateBanner` component to notify users of available updates in the client.
- Introduced updater logic in the Rust backend to check for and install updates from GitHub Releases.
- Updated English and Russian locale files to include new updater messages.
- Refactored package.json scripts for clarity and consistency.
- Enhanced the Tauri application to handle self-updating, including cleanup of old executables.

This commit lays the groundwork for a seamless update experience for users, ensuring they are notified of new versions and can easily install them.
@Mosmain Mosmain merged commit 5f1f4f2 into master Jun 12, 2026
5 checks passed
@Mosmain Mosmain deleted the fix/ux-ui-issues branch June 12, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant