Add progressive web app companion for cross-platform access

Vite + TypeScript PWA that mirrors the Android app's core features:
- Pre-processed shelter data (build-time UTM33N→WGS84 conversion)
- Leaflet map with shelter markers, user location, and offline tiles
- Canvas compass arrow (ported from DirectionArrowView.kt)
- IndexedDB shelter cache with 7-day staleness check
- Service worker with CacheFirst tiles and precached app shell
- i18n for en, nb, nn (ported from Android strings.xml)
- iOS/Android compass handling with low-pass filter
- Respects user map interaction (no auto-snap on pan/zoom)
- Build revision cache-breaker for reliable SW updates

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2026-03-08 17:41:38 +01:00
commit e8428de775
12051 changed files with 1799735 additions and 0 deletions

View file

@ -0,0 +1,22 @@
interface ParsedStack {
method: string;
file: string;
line: number;
column: number;
}
interface SnapshotEnvironment {
getVersion: () => string;
getHeader: () => string;
resolvePath: (filepath: string) => Promise<string>;
resolveRawPath: (testPath: string, rawPath: string) => Promise<string>;
saveSnapshotFile: (filepath: string, snapshot: string) => Promise<void>;
readSnapshotFile: (filepath: string) => Promise<string | null>;
removeSnapshotFile: (filepath: string) => Promise<void>;
processStackTrace?: (stack: ParsedStack) => ParsedStack;
}
interface SnapshotEnvironmentOptions {
snapshotsDirName?: string;
}
export type { SnapshotEnvironment as S, SnapshotEnvironmentOptions as a };

View file

@ -0,0 +1,16 @@
import { S as SnapshotEnvironment, a as SnapshotEnvironmentOptions } from './environment-Ddx0EDtY.js';
declare class NodeSnapshotEnvironment implements SnapshotEnvironment {
private options;
constructor(options?: SnapshotEnvironmentOptions);
getVersion(): string;
getHeader(): string;
resolveRawPath(testPath: string, rawPath: string): Promise<string>;
resolvePath(filepath: string): Promise<string>;
prepareDirectory(dirPath: string): Promise<void>;
saveSnapshotFile(filepath: string, snapshot: string): Promise<void>;
readSnapshotFile(filepath: string): Promise<string | null>;
removeSnapshotFile(filepath: string): Promise<void>;
}
export { NodeSnapshotEnvironment, SnapshotEnvironment };

43
pwa/node_modules/@vitest/snapshot/dist/environment.js generated vendored Normal file
View file

@ -0,0 +1,43 @@
import { promises, existsSync } from 'node:fs';
import { isAbsolute, resolve, dirname, join, basename } from 'pathe';
class NodeSnapshotEnvironment {
constructor(options = {}) {
this.options = options;
}
getVersion() {
return "1";
}
getHeader() {
return `// Snapshot v${this.getVersion()}`;
}
async resolveRawPath(testPath, rawPath) {
return isAbsolute(rawPath) ? rawPath : resolve(dirname(testPath), rawPath);
}
async resolvePath(filepath) {
return join(
join(dirname(filepath), this.options.snapshotsDirName ?? "__snapshots__"),
`${basename(filepath)}.snap`
);
}
async prepareDirectory(dirPath) {
await promises.mkdir(dirPath, { recursive: true });
}
async saveSnapshotFile(filepath, snapshot) {
await promises.mkdir(dirname(filepath), { recursive: true });
await promises.writeFile(filepath, snapshot, "utf-8");
}
async readSnapshotFile(filepath) {
if (!existsSync(filepath)) {
return null;
}
return promises.readFile(filepath, "utf-8");
}
async removeSnapshotFile(filepath) {
if (existsSync(filepath)) {
await promises.unlink(filepath);
}
}
}
export { NodeSnapshotEnvironment };

110
pwa/node_modules/@vitest/snapshot/dist/index.d.ts generated vendored Normal file
View file

@ -0,0 +1,110 @@
import { S as SnapshotStateOptions, a as SnapshotMatchOptions, b as SnapshotResult, R as RawSnapshotInfo } from './rawSnapshot-CPNkto81.js';
export { c as SnapshotData, d as SnapshotSerializer, e as SnapshotSummary, f as SnapshotUpdateState, U as UncheckedSnapshot } from './rawSnapshot-CPNkto81.js';
import { S as SnapshotEnvironment } from './environment-Ddx0EDtY.js';
import { Plugin, Plugins } from '@vitest/pretty-format';
interface ParsedStack {
method: string;
file: string;
line: number;
column: number;
}
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
interface SnapshotReturnOptions {
actual: string;
count: number;
expected?: string;
key: string;
pass: boolean;
}
interface SaveStatus {
deleted: boolean;
saved: boolean;
}
declare class SnapshotState {
testFilePath: string;
snapshotPath: string;
private _counters;
private _dirty;
private _updateSnapshot;
private _snapshotData;
private _initialData;
private _inlineSnapshots;
private _inlineSnapshotStacks;
private _rawSnapshots;
private _uncheckedKeys;
private _snapshotFormat;
private _environment;
private _fileExists;
added: number;
expand: boolean;
matched: number;
unmatched: number;
updated: number;
private constructor();
static create(testFilePath: string, options: SnapshotStateOptions): Promise<SnapshotState>;
get environment(): SnapshotEnvironment;
markSnapshotsAsCheckedForTest(testName: string): void;
protected _inferInlineSnapshotStack(stacks: ParsedStack[]): ParsedStack | null;
private _addSnapshot;
clear(): void;
save(): Promise<SaveStatus>;
getUncheckedCount(): number;
getUncheckedKeys(): Array<string>;
removeUncheckedKeys(): void;
match({ testName, received, key, inlineSnapshot, isInline, error, rawSnapshot, }: SnapshotMatchOptions): SnapshotReturnOptions;
pack(): Promise<SnapshotResult>;
}
interface AssertOptions {
received: unknown;
filepath?: string;
name?: string;
message?: string;
isInline?: boolean;
properties?: object;
inlineSnapshot?: string;
error?: Error;
errorMessage?: string;
rawSnapshot?: RawSnapshotInfo;
}
interface SnapshotClientOptions {
isEqual?: (received: unknown, expected: unknown) => boolean;
}
declare class SnapshotClient {
private options;
filepath?: string;
name?: string;
snapshotState: SnapshotState | undefined;
snapshotStateMap: Map<string, SnapshotState>;
constructor(options?: SnapshotClientOptions);
startCurrentRun(filepath: string, name: string, options: SnapshotStateOptions): Promise<void>;
getSnapshotState(filepath: string): SnapshotState;
clearTest(): void;
skipTestSnapshots(name: string): void;
assert(options: AssertOptions): void;
assertRaw(options: AssertOptions): Promise<void>;
finishCurrentRun(): Promise<SnapshotResult | null>;
clear(): void;
}
declare function stripSnapshotIndentation(inlineSnapshot: string): string;
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
declare function addSerializer(plugin: Plugin): void;
declare function getSerializers(): Plugins;
export { SnapshotClient, SnapshotMatchOptions, SnapshotResult, SnapshotState, SnapshotStateOptions, addSerializer, getSerializers, stripSnapshotIndentation };

2213
pwa/node_modules/@vitest/snapshot/dist/index.js generated vendored Normal file

File diff suppressed because it is too large Load diff

18
pwa/node_modules/@vitest/snapshot/dist/manager.d.ts generated vendored Normal file
View file

@ -0,0 +1,18 @@
import { S as SnapshotStateOptions, e as SnapshotSummary, b as SnapshotResult } from './rawSnapshot-CPNkto81.js';
import '@vitest/pretty-format';
import './environment-Ddx0EDtY.js';
declare class SnapshotManager {
options: Omit<SnapshotStateOptions, 'snapshotEnvironment'>;
summary: SnapshotSummary;
extension: string;
constructor(options: Omit<SnapshotStateOptions, 'snapshotEnvironment'>);
clear(): void;
add(result: SnapshotResult): void;
resolvePath(testPath: string): string;
resolveRawPath(testPath: string, rawPath: string): string;
}
declare function emptySummary(options: Omit<SnapshotStateOptions, 'snapshotEnvironment'>): SnapshotSummary;
declare function addSnapshotResult(summary: SnapshotSummary, result: SnapshotResult): void;
export { SnapshotManager, addSnapshotResult, emptySummary };

76
pwa/node_modules/@vitest/snapshot/dist/manager.js generated vendored Normal file
View file

@ -0,0 +1,76 @@
import { join, dirname, basename, isAbsolute, resolve } from 'pathe';
class SnapshotManager {
constructor(options) {
this.options = options;
this.clear();
}
summary = void 0;
extension = ".snap";
clear() {
this.summary = emptySummary(this.options);
}
add(result) {
addSnapshotResult(this.summary, result);
}
resolvePath(testPath) {
const resolver = this.options.resolveSnapshotPath || (() => {
return join(
join(dirname(testPath), "__snapshots__"),
`${basename(testPath)}${this.extension}`
);
});
const path = resolver(testPath, this.extension);
return path;
}
resolveRawPath(testPath, rawPath) {
return isAbsolute(rawPath) ? rawPath : resolve(dirname(testPath), rawPath);
}
}
function emptySummary(options) {
const summary = {
added: 0,
failure: false,
filesAdded: 0,
filesRemoved: 0,
filesRemovedList: [],
filesUnmatched: 0,
filesUpdated: 0,
matched: 0,
total: 0,
unchecked: 0,
uncheckedKeysByFile: [],
unmatched: 0,
updated: 0,
didUpdate: options.updateSnapshot === "all"
};
return summary;
}
function addSnapshotResult(summary, result) {
if (result.added) {
summary.filesAdded++;
}
if (result.fileDeleted) {
summary.filesRemoved++;
}
if (result.unmatched) {
summary.filesUnmatched++;
}
if (result.updated) {
summary.filesUpdated++;
}
summary.added += result.added;
summary.matched += result.matched;
summary.unchecked += result.unchecked;
if (result.uncheckedKeys && result.uncheckedKeys.length > 0) {
summary.uncheckedKeysByFile.push({
filePath: result.filepath,
keys: result.uncheckedKeys
});
}
summary.unmatched += result.unmatched;
summary.updated += result.updated;
summary.total += result.added + result.matched + result.unmatched + result.updated;
}
export { SnapshotManager, addSnapshotResult, emptySummary };

View file

@ -0,0 +1,60 @@
import { Plugin, OptionsReceived } from '@vitest/pretty-format';
import { S as SnapshotEnvironment } from './environment-Ddx0EDtY.js';
type SnapshotData = Record<string, string>;
type SnapshotUpdateState = 'all' | 'new' | 'none';
type SnapshotSerializer = Plugin;
interface SnapshotStateOptions {
updateSnapshot: SnapshotUpdateState;
snapshotEnvironment: SnapshotEnvironment;
expand?: boolean;
snapshotFormat?: OptionsReceived;
resolveSnapshotPath?: (path: string, extension: string) => string;
}
interface SnapshotMatchOptions {
testName: string;
received: unknown;
key?: string;
inlineSnapshot?: string;
isInline: boolean;
error?: Error;
rawSnapshot?: RawSnapshotInfo;
}
interface SnapshotResult {
filepath: string;
added: number;
fileDeleted: boolean;
matched: number;
unchecked: number;
uncheckedKeys: Array<string>;
unmatched: number;
updated: number;
}
interface UncheckedSnapshot {
filePath: string;
keys: Array<string>;
}
interface SnapshotSummary {
added: number;
didUpdate: boolean;
failure: boolean;
filesAdded: number;
filesRemoved: number;
filesRemovedList: Array<string>;
filesUnmatched: number;
filesUpdated: number;
matched: number;
total: number;
unchecked: number;
uncheckedKeysByFile: Array<UncheckedSnapshot>;
unmatched: number;
updated: number;
}
interface RawSnapshotInfo {
file: string;
readonly?: boolean;
content?: string;
}
export type { RawSnapshotInfo as R, SnapshotStateOptions as S, UncheckedSnapshot as U, SnapshotMatchOptions as a, SnapshotResult as b, SnapshotData as c, SnapshotSerializer as d, SnapshotSummary as e, SnapshotUpdateState as f };