Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
32d93a3
fix(overlay/hotkeys): wave 1 UX bugfixes
Mosmain Jun 1, 2026
ade1862
fix(hotkeys): suspend global shortcuts while recording
Mosmain Jun 1, 2026
cbc7603
fix(hotkeys): serialize register ops to stop resume/watch race
Mosmain Jun 1, 2026
cd8a3e5
fix(hotkeys): map right Alt (AltGr) to Ctrl+Alt; drop applied flash
Mosmain Jun 1, 2026
ee0a5b4
fix(hotkeys): record right Alt (AltGr) as plain Alt, not Ctrl+Alt
Mosmain Jun 1, 2026
e5b3ec5
fix(hotkeys): reject right Alt (AltGr), don't try to bind it
Mosmain Jun 1, 2026
1e94d0d
feat(overlay/settings): wave 2 UX polish
Mosmain Jun 1, 2026
659ac29
fix(overlay): seamless dashed border via pathLength normalization
Mosmain Jun 1, 2026
371de72
feat(overlay): hover-reveal drag bar instead of corner handle
Mosmain Jun 1, 2026
59cc55b
refactor(overlay): full-width drag bar that persists during drag
Mosmain Jun 1, 2026
d73934f
refactor(overlay): airy floating drag pill + obvious drawer close
Mosmain Jun 2, 2026
3150404
fix(overlay): drawer double-close; add dev flag to preview overlay ch…
Mosmain Jun 2, 2026
aac70d3
refactor(overlay): full-width unfurling drag bar; default drawer close
Mosmain Jun 2, 2026
9c5875a
refactor(overlay): separated drag/controls zones per UX review
Mosmain Jun 2, 2026
b2cf687
refactor(overlay): update drag pill icon for consistency
Mosmain Jun 2, 2026
097b54b
feat(map/overlay): wave 3 — follow-no-zoom, looser pan, lock-on-top, …
Mosmain Jun 2, 2026
00103dc
feat(overlay): #3 Tarkov in-game clock chip
Mosmain Jun 2, 2026
fcbb55b
feat(hotkeys): #8 in-browser keyboard shortcuts
Mosmain Jun 2, 2026
9b2901f
fix(overlay): move Tarkov clock below the header band
Mosmain Jun 2, 2026
b781ed2
feat(overlay): float the Tarkov clock out of the header's way
Mosmain Jun 2, 2026
2770be8
fix(overlay): sync clock slide with header reveal via transform
Mosmain Jun 2, 2026
1a0fdca
feat(desktop): headless backend binary (server without the Tauri window)
Mosmain Jun 2, 2026
f6c63e5
fix(hotkeys): show the hotkeys settings section in the desktop browser
Mosmain Jun 2, 2026
dd8940f
feat(hotkeys): backend-owned global hotkeys forwarded to every client
Mosmain Jun 2, 2026
2ad31d8
docs: refresh README/CLAUDE for backend hotkeys; drop dead Node/Fasti…
Mosmain Jun 2, 2026
5762a47
feat(extracts): edge arrows for off-screen extracts (#18)
Mosmain Jun 2, 2026
77dd099
feat(ui): safe-area insets for floating controls (#24)
Mosmain Jun 2, 2026
ba68aeb
feat(map): right-click the map name to switch maps (#21)
Mosmain Jun 2, 2026
f5445d1
fix(extracts): bolder, larger edge arrows so they read on the map (#18)
Mosmain Jun 2, 2026
9271d3c
fix(ui): align clock with the status cluster; dvh height for iOS Safari
Mosmain Jun 2, 2026
3712e64
feat(settings): tabbed drawer with collapsible accordion sections (#13)
Mosmain Jun 4, 2026
e51d191
feat(settings): non-modal live-preview drawer + mobile bottom-sheet (…
Mosmain Jun 4, 2026
6c82ba0
feat(overlay): minimize-to-tray option for the close button (#9)
Mosmain Jun 4, 2026
dcb1345
feat(overlay): on-map layer rail + glanceable lock read-outs
Mosmain Jun 5, 2026
d0fcff4
docs: sync CLAUDE.md + READMEs with the on-map rail architecture
Mosmain Jun 5, 2026
3a3e96e
fix(overlay): review fixes - visibility persistence, floor direction,…
Mosmain Jun 5, 2026
4a6167b
refactor(overlay): consolidate layers into one registry (arch review #1)
Mosmain Jun 5, 2026
636a182
refactor(overlay): decouple edge indicators from rail DOM + overlay s…
Mosmain Jun 5, 2026
1e602da
refactor(overlay): single useOverlayLock source for the lock policy (…
Mosmain Jun 5, 2026
b18cd31
test(overlay): cover layer visibility, floor stepping, and the layer …
Mosmain Jun 5, 2026
a9f76c2
docs: sync registries section after the layer-registry consolidation
Mosmain Jun 5, 2026
bdad8c5
feat(client): mobile fullscreen + keep-awake, PWA manifest
Mosmain Jun 6, 2026
55143b2
fix(client): stable viewport height + iOS-safe rail/flyout centering
Mosmain Jun 6, 2026
eab1b24
refactor(client): consolidate mobile top bar; drop keep-awake; clock …
Mosmain Jun 7, 2026
81afb76
feat(hotkeys): read-only on mobile + live cross-client config re-sync
Mosmain Jun 7, 2026
0ffa21f
fix(desktop): comctl32 v6 manifest for test exes + rustfmt of committ…
Mosmain Jun 12, 2026
e129bd7
feat(desktop): tray/LAN groundwork -- toast hint, icon set, delete-sc…
Mosmain Jun 12, 2026
a863dc3
refactor(client): clean up formatting and improve readability in vari…
Mosmain Jun 12, 2026
3cd8fe6
docs: update CLAUDE.md for improved layer structure clarity
Mosmain Jun 12, 2026
4feb4a5
Add .prettierignore file to exclude build outputs, lockfiles, and gen…
Mosmain Jun 12, 2026
ee59334
refactor(i18n): update paths section headings for clarity
Mosmain Jun 12, 2026
1435f24
chore: enhance package.json scripts and CI configurations
Mosmain Jun 12, 2026
1fb4bc3
feat(updater): implement self-update functionality and UI components
Mosmain Jun 12, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .claude/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"name": "tarkov-checker-dev",
"runtimeExecutable": "pnpm",
"runtimeArgs": ["dev"],
"port": 5173
"port": 5173,
"autoPort": false
}
]
}
2 changes: 1 addition & 1 deletion .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ jobs:
# eslint-config-prettier disables format rules in ESLint, so
# Prettier itself isn't verified by `pnpm lint`. Run it first —
# a formatting drift fails fast before the heavier eslint pass.
- run: pnpm format:check
- run: pnpm format

- run: pnpm lint

Expand Down
4 changes: 4 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ apps/client/.eslintrc-auto-import.json

# Claude Code local tooling (launch config, vendored skills)
.claude/**

# Agent instructions — hand-formatted (tables, aligned lists), owned by
# whoever edits them, not by prettier
CLAUDE.md
488 changes: 364 additions & 124 deletions CLAUDE.md

Large diffs are not rendered by default.

40 changes: 22 additions & 18 deletions README.md

Large diffs are not rendered by default.

28 changes: 15 additions & 13 deletions apps/client/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @tarkov-checker/client

Vue + Leaflet map. Runs both inside the Tauri overlay (`apps/desktop`) and as a plain browser page on phones over LAN (the same prod build Fastify serves at `:3000/`). Same code path, different transport (Tauri events vs SSE / `EventSource`).
Vue + Leaflet map. Runs both inside the Tauri overlay (`apps/desktop`) and as a plain browser page on phones over LAN (the same prod build the in-process Rust helper in `apps/desktop` serves at `:47474/`; dev uses Vite on `:5173`, which proxies `/api` + `/events` to the helper). Same code path, different transport (Tauri events vs SSE / `EventSource`).

Specifics about Tauri internals, Windows build quirks, and dev workflow live in [the repo CLAUDE.md](../../CLAUDE.md). This README is for contributors working **inside the client**.

Expand All @@ -21,15 +21,15 @@ Specifics about Tauri internals, Windows build quirks, and dev workflow live in
app/ Composition root — router config.
pages/ File-based routes. Add a *.vue here → it becomes a route. typed-router.d.ts is regenerated on dev/build.
features/ One folder per business feature, each fully owns its slice:
map/ Leaflet map framework (useLeafletMap, useFloorSwitcher), layers registry + built-in layers (extracts/player/airdrop), MapView component, static extracts dataset (data/extracts/<code>.json — one file per canonical map)
map/ Leaflet map framework (useLeafletMap, useLayerVisibility), layers registry + built-in layers (extracts/player/airdrop), MapView + LayerRail components, static extracts dataset (data/extracts/<code>.json — one file per canonical map)
airdrop/ Airdrop triangulation state machine, screenshot tracker, settings section
overlay/ Tauri overlay window controls, opacity/zoom/mapOpacity sync, tray icon, overlay-specific components
hotkeys/ Global shortcuts, HotkeyRecorder, accelerator parser
server/ SSE/HTTP transport, typed IPC contract, server event bus, /api/config client
hotkeys/ Backend-owned combos synced via thin store (fetch/PUT), HotkeyRecorder, accelerator parser; only the overlay lock stays client-registered
server/ SSE/HTTP transport, typed IPC contract, server event bus, /api/config + /api/hotkeys clients
i18n/ createI18n instance, store (apiLang persisted), locales/<code>.json files
settings/ Settings registry, SettingsPanel.vue, section sub-components
shared/ Cross-feature utilities (persisted-store, config) — no business logic
App.vue Tiny: <RouterView/> + ConfirmDialog + MapQuickMenu + side effects (transport, tray, lock hotkey)
App.vue Tiny: <RouterView/> + ConfirmDialog + MapQuickMenu + side effects (transport, tray, lock hotkey, hotkeys sync — backend load + recorder suspend/resume bridge)
main.ts createApp → pinia + router + i18n + PrimeVue
theme.ts PrimeVue Aura preset override (primary = purple)
styles.css Tailwind imports, font-faces, dark mode token tweaks
Expand Down Expand Up @@ -86,23 +86,25 @@ For nested or dynamic routes (`/raids/[id]`), see [Vue Router file-based docs](h
2. Inside the feature: `./X` and `../X` imports.
3. Other features depend on yours via `@/features/<name>/<thing>`.
4. If the feature has persisted user settings — create `store.ts` using [`persistedRef`](./src/shared/persisted-store.ts) from `@/shared/persisted-store`. Each setting gets its own key like `tc.<name>.<field>`.
5. If the feature has its own settings UI:
5. If the feature has its own SYSTEM/app settings UI (the gear drawer). Map-layer
settings live on the layer registration instead — see step 6.
- Create a section component `XxxSection.vue` under [`features/settings/sections/`](./src/features/settings/sections/).
- Create `src/features/<name>/settings.ts` that calls `registerSettingsSection()`:
```ts
import { registerSettingsSection } from '@/features/settings/registry';
registerSettingsSection({
id: '<name>',
group: 'main', // or 'system'
order: 30, // multiples of 10
titleKey: 'xxx.heading', // i18n key for the section title
visible: 'always', // or 'tauri', 'desktop-or-tauri'
component: XxxSection,
});
```
- That's it — `main.ts` auto-discovers the file via `import.meta.glob('@/features/*/settings.ts', { eager: true })`.
6. If the feature adds a map layer (new extract/player marker/etc):
- Create `src/features/map/layers/<name>/` with `useXxxLayer.ts` and `index.ts`.
- Call `registerMapLayer({ id, mount: useXxxLayer })` in `index.ts`.
- Call `registerMapLayer({ id, mount: useXxxLayer, category, order, titleKey, settingsComponent })` in `index.ts` — the rail category, order, display name, and optional inline settings component all live on the layer registration (no separate `settings.ts` for layers).
- Inside your composable, read `ctx.visible` (wired to the on-map LayerRail toggle via `useLayerVisibility(id)`) and `watch` it to add/remove your Leaflet root when toggled.
- `main.ts` auto-discovers via `import.meta.glob('@/features/map/layers/*/index.ts', { eager: true })`.

### Add a new locale
Expand Down Expand Up @@ -182,15 +184,15 @@ pnpm lint # eslint --max-warnings=0
## Where things connect

- **Tauri detection** — centralized in [`shared/tauri.ts`](./src/shared/tauri.ts) as `isTauri` const. Checked at module load time, safe for non-DOM contexts. Used by transport layer, router, overlay composables, and settings visibility logic.
- **Server-pushed messages** — schema in [`@shared/ws-messages`](../../packages/shared/src/ws-messages.ts). Position + map-change events; Node server pushes over SSE, Rust port emits as Tauri events. Client uses the same Zod schema either way.
- **Server-pushed messages** — schema in [`@shared/sse-messages`](../../packages/shared/src/sse-messages.ts). Position, map-change, command (backend-owned hotkey press), and hotkeys (config re-sync after a rebind) events; the in-process Rust helper pushes over SSE to browsers, and emits as Tauri events to the overlay webview. Client uses the same Zod schema either way.
- **Tarkov map calibration** (CRS, bounds, rotation) — [`@shared/maps`](../../packages/shared/src/maps.ts). Modifying calibration affects both desktop and browser.
- **Map localization** — `useMapI18n()` composable in [`features/map/composables/useMapI18n.ts`](./src/features/map/composables/useMapI18n.ts) provides `localizedMapName(code)` with `te → t → displayName` fallback chain. Used in MapView + MapSection to stay in sync.
- **HTTP / IPC** — single dispatch in [`features/server/api/transport.ts`](./src/features/server/api/transport.ts).
- **Settings registry** — `registerSettingsSection()` in [`features/settings/registry.ts`](./src/features/settings/registry.ts). Each feature calls this at module load via `features/<name>/settings.ts`. Auto-discovered by `main.ts`.
- **Map layers registry** — `registerMapLayer()` in [`features/map/layers/registry.ts`](./src/features/map/layers/registry.ts). Each layer calls this via `features/map/layers/<name>/index.ts`. Auto-discovered by `main.ts`; mounted by `MapView.vue` in setup().
- **Settings registry** — `registerSettingsSection()` in [`features/settings/registry.ts`](./src/features/settings/registry.ts) — **system/app settings only** (gear-drawer accordion). Each feature calls it at module load via `features/<name>/settings.ts`. Map-LAYER settings live on the map-layer registration instead (below).
- **Map layers registry** — `registerMapLayer()` in [`features/map/layers/registry.ts`](./src/features/map/layers/registry.ts) — the single source of truth for layers, carrying each layer's rail `category`/`order`/`titleKey`/`settingsComponent`. Each layer calls it via `features/map/layers/<name>/index.ts`; auto-discovered by `main.ts`, mounted by `MapView.vue`, rendered by the on-map `LayerRail`. Per-layer show/hide is `useLayerVisibility`; edge-indicator layers read the rail rect from `useRailObstacle`.

## See also

- [`CLAUDE.md`](../../CLAUDE.md) — repo-wide context: Tauri internals, Windows build quirks (HVCI/Defender), path resolution dual implementation (Node + Rust), why ASCII-only repo path matters.
- [`CLAUDE.md`](../../CLAUDE.md) — repo-wide context: Tauri internals, Windows build quirks (HVCI/Defender), Rust path resolution, why ASCII-only repo path matters.
- [`apps/desktop/README.md`](../desktop/README.md) — Tauri overlay specifics.
- [`packages/shared/`](../../packages/shared/) — Zod schemas + map calibration tables shared between client, Node server, and Rust port.
- [`packages/shared/`](../../packages/shared/) — Zod schemas + map calibration tables shared between the client and the Rust helper.
8 changes: 8 additions & 0 deletions apps/client/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,14 @@
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
<meta name="theme-color" content="#0b0b0b" />
<!-- PWA: "Add to Home Screen" gives iPhone an app-like chrome-free view
(iOS has no Fullscreen API); Android Chrome installs as standalone. -->
<link rel="manifest" href="/manifest.webmanifest" />
<link rel="apple-touch-icon" href="/favicon.svg" />
<meta name="mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<meta name="apple-mobile-web-app-title" content="tarkov-checker" />
<title>tarkov-checker</title>
</head>
<body class="text-neutral-100">
Expand Down
3 changes: 2 additions & 1 deletion apps/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
"test": "vitest run",
"test:watch": "vitest",
"typecheck": "vue-tsc -b --noEmit",
"lint": "eslint src"
"lint": "eslint src",
"lint:fix": "eslint src --fix"
},
"dependencies": {
"@primeuix/themes": "^2.0.3",
Expand Down
19 changes: 19 additions & 0 deletions apps/client/public/manifest.webmanifest
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "tarkov-checker",
"short_name": "tarkov-checker",
"description": "Live Escape from Tarkov map overlay — second-screen map for your phone.",
"start_url": "/",
"scope": "/",
"display": "standalone",
"orientation": "any",
"background_color": "#0b0b0b",
"theme_color": "#0b0b0b",
"icons": [
{
"src": "/favicon.svg",
"sizes": "any",
"type": "image/svg+xml",
"purpose": "any"
}
]
}
27 changes: 24 additions & 3 deletions apps/client/src/App.vue
Original file line number Diff line number Diff line change
@@ -1,16 +1,19 @@
<script setup lang="ts">
import MapQuickMenu from '@/features/overlay/components/MapQuickMenu.vue';
import PairingModal from '@/features/overlay/components/PairingModal.vue';
import UpdateBanner from '@/features/updater/components/UpdateBanner.vue';
import { useServerTransport } from '@/features/server/composables/useServerTransport';
import { provideTransportStatus } from '@/features/server/composables/useTransportStatus';
import { useOverlayStore } from '@/features/overlay/store';
import { useHotkeysStore } from '@/features/hotkeys/store';
import { useTauriOverlay } from '@/features/overlay/composables/useTauriOverlay';
import { useGlobalShortcut } from '@/features/hotkeys/composables/useGlobalShortcut';
import { useHotkeysSync } from '@/features/hotkeys/composables/useHotkeysSync';
import { useTrayIcon } from '@/features/overlay/composables/useTrayIcon';
import { useOverlayBootstrap } from '@/features/overlay/composables/useOverlayBootstrap';
import { useAutoMapSwitch } from '@/features/map/composables/useAutoMapSwitch';
import { eventsUrl } from '@/shared/config';
import { useConfirm } from 'primevue/useconfirm';

const { clickThrough: overlayClickThrough } = storeToRefs(useOverlayStore());
const { lockHotkey } = storeToRefs(useHotkeysStore());
Expand All @@ -26,17 +29,32 @@ provideTransportStatus(status);
// regardless of which route is currently mounted.
useAutoMapSwitch();

// Lock toggle is the only truly global hotkey — every other map-specific
// shortcut lives inside OverlayView, scoped to that route.
// Lock toggle stays a client-side global shortcut: it's an overlay window op
// (click-through) with no browser meaning, and keeping it on the proven plugin
// path preserves the recovery route out of a click-through lockout. Every other
// hotkey is backend-owned and arrives as a `command` event (see index.vue).
useGlobalShortcut(isTauri, lockHotkey, () => {
overlayClickThrough.value = !overlayClickThrough.value;
});

// Load backend-owned combos into the store + bridge the recorder's
// suspend/resume to the backend (so a combo can be re-recorded).
useHotkeysSync();

useOverlayBootstrap(overlayClickThrough);
useTrayIcon(isTauri, overlayClickThrough);

const confirm = useConfirm();
const quickMenu = ref<InstanceType<typeof MapQuickMenu> | null>(null);

// Close transient UI when the overlay locks so click-through can't strand them.
watch(overlayClickThrough, (locked) => {
if (locked) {
confirm.close();
quickMenu.value?.close();
}
});

// Right-click anywhere over the Leaflet canvas opens the transparency panel.
// Lives at the app root so the same gesture works regardless of which route
// is mounted underneath.
Expand All @@ -59,8 +77,11 @@ function onMapContextMenu(event: MouseEvent): void {
mount it at all there.
-->
<PairingModal v-if="isTauri" />
<!-- Self-update is a Tauri-only concern: browsers/phones always load the
SPA the helper serves, they have nothing to update themselves. -->
<UpdateBanner v-if="isTauri" />
<div
class="relative h-screen w-screen text-surface-0"
class="relative h-full w-screen text-surface-0"
:class="isTauri ? '' : 'bg-surface-950'"
@contextmenu="onMapContextMenu"
>
Expand Down
Loading
Loading