Add prettier. Format.

This commit is contained in:
2026-07-22 02:12:05 +00:00
parent d28b09dc21
commit 18baa96848
45 changed files with 4520 additions and 2613 deletions

View File

@@ -1,6 +1,6 @@
<script setup lang="ts">
const requestedAt = Date.now()
await new Promise((resolve) => window.setTimeout(resolve, 1_000))
await new Promise((resolve) => window.setTimeout(resolve, 3_000))
const resolutionTime = Date.now() - requestedAt
</script>

View File

@@ -1,103 +1,207 @@
import { computed, reactive, readonly, ref } from 'vue'
import { computed, reactive, readonly, ref } from "vue";
export interface Person {
id: string
name: string
handle: string
color: string
online: boolean
bio: string
id: string;
name: string;
handle: string;
color: string;
online: boolean;
bio: string;
}
export interface Message {
id: string
personId: string
body: string
sentAt: number
mine: boolean
status: 'sent' | 'delivered' | 'failed'
id: string;
personId: string;
body: string;
sentAt: number;
mine: boolean;
status: "sent" | "delivered" | "failed";
}
export interface Conversation {
id: string
personId: string
unread: number
pinned?: boolean
messages: Message[]
id: string;
personId: string;
unread: number;
pinned?: boolean;
messages: Message[];
}
const people: Person[] = [
{ id: 'maya', name: 'Maya Chen', handle: '@mayac', color: '#ff7a8a', online: true, bio: 'Product designer · Sydney to everywhere.' },
{ id: 'noah', name: 'Noah Williams', handle: '@noahw', color: '#4f9cff', online: true, bio: 'Film, tiny cameras, and very long walks.' },
{ id: 'sofia', name: 'Sofia Rossi', handle: '@sofiar', color: '#9d67ff', online: false, bio: 'Making typefaces and better pasta.' },
{ id: 'liam', name: 'Liam Park', handle: '@liamp', color: '#35c7a0', online: true, bio: 'Engineer. Climber. Questionable DJ.' },
{ id: 'amara', name: 'Amara Okafor', handle: '@amarao', color: '#f4ad42', online: false, bio: 'Architecture and cities after dark.' },
]
{
id: "maya",
name: "Maya Chen",
handle: "@mayac",
color: "#ff7a8a",
online: true,
bio: "Product designer · Sydney to everywhere.",
},
{
id: "noah",
name: "Noah Williams",
handle: "@noahw",
color: "#4f9cff",
online: true,
bio: "Film, tiny cameras, and very long walks.",
},
{
id: "sofia",
name: "Sofia Rossi",
handle: "@sofiar",
color: "#9d67ff",
online: false,
bio: "Making typefaces and better pasta.",
},
{
id: "liam",
name: "Liam Park",
handle: "@liamp",
color: "#35c7a0",
online: true,
bio: "Engineer. Climber. Questionable DJ.",
},
{
id: "amara",
name: "Amara Okafor",
handle: "@amarao",
color: "#f4ad42",
online: false,
bio: "Architecture and cities after dark.",
},
];
const timestamp = Date.now()
const timestamp = Date.now();
const seedConversations: Conversation[] = [
{
id: 'maya', personId: 'maya', unread: 2, pinned: true,
id: "maya",
personId: "maya",
unread: 2,
pinned: true,
messages: [
{ id: 'm1', personId: 'maya', body: 'That transition feels ridiculously smooth ✨', sentAt: timestamp - 840_000, mine: false, status: 'delivered' },
{ id: 'm2', personId: 'maya', body: 'Try holding it halfway, then let go slowly.', sentAt: timestamp - 780_000, mine: false, status: 'delivered' },
{
id: "m1",
personId: "maya",
body: "That transition feels ridiculously smooth ✨",
sentAt: timestamp - 840_000,
mine: false,
status: "delivered",
},
{
id: "m2",
personId: "maya",
body: "Try holding it halfway, then let go slowly.",
sentAt: timestamp - 780_000,
mine: false,
status: "delivered",
},
],
},
{
id: 'noah', personId: 'noah', unread: 0,
id: "noah",
personId: "noah",
unread: 0,
messages: [
{ id: 'n1', personId: 'noah', body: 'Uploaded the photos from yesterday.', sentAt: timestamp - 4_200_000, mine: false, status: 'delivered' },
{ id: 'n2', personId: 'noah', body: 'The grain is perfect.', sentAt: timestamp - 4_000_000, mine: true, status: 'delivered' },
{
id: "n1",
personId: "noah",
body: "Uploaded the photos from yesterday.",
sentAt: timestamp - 4_200_000,
mine: false,
status: "delivered",
},
{
id: "n2",
personId: "noah",
body: "The grain is perfect.",
sentAt: timestamp - 4_000_000,
mine: true,
status: "delivered",
},
],
},
{
id: 'sofia', personId: 'sofia', unread: 1,
messages: [{ id: 's1', personId: 'sofia', body: 'Coffee at the new place tomorrow?', sentAt: timestamp - 18_000_000, mine: false, status: 'delivered' }],
id: "sofia",
personId: "sofia",
unread: 1,
messages: [
{
id: "s1",
personId: "sofia",
body: "Coffee at the new place tomorrow?",
sentAt: timestamp - 18_000_000,
mine: false,
status: "delivered",
},
],
},
{
id: 'liam', personId: 'liam', unread: 0,
messages: [{ id: 'l1', personId: 'liam', body: 'The build is green. Ship it.', sentAt: timestamp - 86_000_000, mine: false, status: 'delivered' }],
id: "liam",
personId: "liam",
unread: 0,
messages: [
{
id: "l1",
personId: "liam",
body: "The build is green. Ship it.",
sentAt: timestamp - 86_000_000,
mine: false,
status: "delivered",
},
],
},
{
id: 'amara', personId: 'amara', unread: 0,
messages: [{ id: 'a1', personId: 'amara', body: 'This city never really goes quiet.', sentAt: timestamp - 172_000_000, mine: false, status: 'delivered' }],
id: "amara",
personId: "amara",
unread: 0,
messages: [
{
id: "a1",
personId: "amara",
body: "This city never really goes quiet.",
sentAt: timestamp - 172_000_000,
mine: false,
status: "delivered",
},
],
},
]
];
const conversations = ref<Conversation[]>(structuredClone(seedConversations))
const ready = ref(false)
const offline = ref(!navigator.onLine)
const settings = reactive({ simulatedLatency: 180, simulateFailures: false })
const conversations = ref<Conversation[]>(structuredClone(seedConversations));
const ready = ref(false);
const offline = ref(!navigator.onLine);
const settings = reactive({ simulatedLatency: 180, simulateFailures: false });
function openDatabase() {
return new Promise<IDBDatabase>((resolve, reject) => {
const request = indexedDB.open('native-vue-messenger', 1)
request.onupgradeneeded = () => request.result.createObjectStore('state')
request.onsuccess = () => resolve(request.result)
request.onerror = () => reject(request.error)
})
const request = indexedDB.open("native-vue-messenger", 1);
request.onupgradeneeded = () => request.result.createObjectStore("state");
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
}
async function readStored() {
const database = await openDatabase()
const database = await openDatabase();
return await new Promise<Conversation[] | undefined>((resolve, reject) => {
const transaction = database.transaction('state', 'readonly')
const request = transaction.objectStore('state').get('conversations')
request.onsuccess = () => resolve(request.result as Conversation[] | undefined)
request.onerror = () => reject(request.error)
}).finally(() => database.close())
const transaction = database.transaction("state", "readonly");
const request = transaction.objectStore("state").get("conversations");
request.onsuccess = () =>
resolve(request.result as Conversation[] | undefined);
request.onerror = () => reject(request.error);
}).finally(() => database.close());
}
async function persist() {
try {
const database = await openDatabase()
const database = await openDatabase();
await new Promise<void>((resolve, reject) => {
const transaction = database.transaction('state', 'readwrite')
transaction.objectStore('state').put(structuredClone(conversations.value), 'conversations')
transaction.oncomplete = () => resolve()
transaction.onerror = () => reject(transaction.error)
})
database.close()
const transaction = database.transaction("state", "readwrite");
transaction
.objectStore("state")
.put(structuredClone(conversations.value), "conversations");
transaction.oncomplete = () => resolve();
transaction.onerror = () => reject(transaction.error);
});
database.close();
} catch {
// Private browsing and locked-down webviews can reject IndexedDB.
}
@@ -105,48 +209,58 @@ async function persist() {
async function initialize() {
try {
const stored = await readStored()
if (stored?.length) conversations.value = stored
else await persist()
const stored = await readStored();
if (stored?.length) conversations.value = stored;
else await persist();
} finally {
ready.value = true
ready.value = true;
}
}
window.addEventListener('online', () => { offline.value = false })
window.addEventListener('offline', () => { offline.value = true })
void initialize()
window.addEventListener("online", () => {
offline.value = false;
});
window.addEventListener("offline", () => {
offline.value = true;
});
void initialize();
export function useDemoStore() {
const conversationFor = (id: string) => computed(() => conversations.value.find((conversation) => conversation.id === id))
const personFor = (id: string) => people.find((person) => person.id === id)
const conversationFor = (id: string) =>
computed(() =>
conversations.value.find((conversation) => conversation.id === id),
);
const personFor = (id: string) => people.find((person) => person.id === id);
const markRead = (id: string) => {
const conversation = conversations.value.find((item) => item.id === id)
if (conversation) conversation.unread = 0
void persist()
}
const conversation = conversations.value.find((item) => item.id === id);
if (conversation) conversation.unread = 0;
void persist();
};
const sendMessage = async (id: string, body: string) => {
const conversation = conversations.value.find((item) => item.id === id)
if (!conversation || !body.trim()) return false
const conversation = conversations.value.find((item) => item.id === id);
if (!conversation || !body.trim()) return false;
const message: Message = {
id: crypto.randomUUID(),
personId: id,
body: body.trim(),
sentAt: Date.now(),
mine: true,
status: 'sent',
}
conversation.messages.push(message)
await persist()
await new Promise((resolve) => window.setTimeout(resolve, settings.simulatedLatency))
message.status = settings.simulateFailures || offline.value ? 'failed' : 'delivered'
await persist()
return message.status === 'delivered'
}
status: "sent",
};
conversation.messages.push(message);
await persist();
await new Promise((resolve) =>
window.setTimeout(resolve, settings.simulatedLatency),
);
message.status =
settings.simulateFailures || offline.value ? "failed" : "delivered";
await persist();
return message.status === "delivered";
};
const reset = async () => {
conversations.value = structuredClone(seedConversations)
await persist()
}
conversations.value = structuredClone(seedConversations);
await persist();
};
return {
people: readonly(ref(people)),
conversations: readonly(conversations),
@@ -158,14 +272,14 @@ export function useDemoStore() {
markRead,
sendMessage,
reset,
}
};
}
export function relativeTime(value: number) {
const minutes = Math.floor((Date.now() - value) / 60_000)
if (minutes < 1) return 'now'
if (minutes < 60) return `${minutes}m`
const hours = Math.floor(minutes / 60)
if (hours < 24) return `${hours}h`
return `${Math.floor(hours / 24)}d`
const minutes = Math.floor((Date.now() - value) / 60_000);
if (minutes < 1) return "now";
if (minutes < 60) return `${minutes}m`;
const hours = Math.floor(minutes / 60);
if (hours < 24) return `${hours}h`;
return `${Math.floor(hours / 24)}d`;
}

View File

@@ -1,50 +1,50 @@
import { readonly, reactive } from 'vue'
import { readonly, reactive } from "vue";
function createGuardState() {
return reactive({
blockEntry: false,
checks: 0,
status: 'idle' as 'idle' | 'checking' | 'allowed' | 'blocked',
})
status: "idle" as "idle" | "checking" | "allowed" | "blocked",
});
}
const storyState = createGuardState()
const labState = createGuardState()
const storyState = createGuardState();
const labState = createGuardState();
export const storyEntryGuard = readonly(storyState)
export const runtimeLabGuard = readonly(labState)
export const storyEntryGuard = readonly(storyState);
export const runtimeLabGuard = readonly(labState);
export function setStoryEntryBlocked(blocked: boolean) {
storyState.blockEntry = blocked
if (storyState.status !== 'checking') storyState.status = 'idle'
storyState.blockEntry = blocked;
if (storyState.status !== "checking") storyState.status = "idle";
}
async function evaluate(state: ReturnType<typeof createGuardState>) {
state.checks += 1
state.status = 'checking'
await new Promise((resolve) => window.setTimeout(resolve, 320))
const allowed = !state.blockEntry
state.status = allowed ? 'allowed' : 'blocked'
return allowed
state.checks += 1;
state.status = "checking";
await new Promise((resolve) => window.setTimeout(resolve, 320));
const allowed = !state.blockEntry;
state.status = allowed ? "allowed" : "blocked";
return allowed;
}
/** Dynamic guard used to reject a sibling that may already be cached. */
export function evaluateStoryEntry() {
storyState.checks += 1
storyState.checks += 1;
if (!storyState.blockEntry) {
storyState.status = 'allowed'
return true
storyState.status = "allowed";
return true;
}
storyState.status = 'checking'
storyState.status = "checking";
return new Promise<boolean>((resolve) => {
window.setTimeout(() => {
storyState.status = 'blocked'
resolve(false)
}, 320)
})
storyState.status = "blocked";
resolve(false);
}, 320);
});
}
/** Always-allowing asynchronous guard for the deeper stress-lab route. */
export function evaluateRuntimeLabEntry() {
return evaluate(labState)
return evaluate(labState);
}

View File

@@ -1,29 +1,32 @@
import { createApp } from 'vue'
import { createNativeRouter } from '@native-vue-router/core'
import { createCapacitorAdapter } from '@native-vue-router/capacitor'
import { createElectronRendererAdapter } from '@native-vue-router/electron'
import App from './App.vue'
import { createPwaAdapter } from './pwa'
import { router } from './router'
import './style.css'
import { createApp } from "vue";
import { createNativeRouter } from "@native-vue-router/core";
import { createCapacitorAdapter } from "@native-vue-router/capacitor";
import { createElectronRendererAdapter } from "@native-vue-router/electron";
import App from "./App.vue";
import { createPwaAdapter } from "./pwa";
import { router } from "./router";
import "./style.css";
const isElectron = navigator.userAgent.toLowerCase().includes('electron')
const capacitorPlatform = createCapacitorAdapter({ haptics: true, exitAtRoot: true })
const isElectron = navigator.userAgent.toLowerCase().includes("electron");
const capacitorPlatform = createCapacitorAdapter({
haptics: true,
exitAtRoot: true,
});
const platform = isElectron
? createElectronRendererAdapter()
: capacitorPlatform.name !== 'capacitor-web'
: capacitorPlatform.name !== "capacitor-web"
? capacitorPlatform
: createPwaAdapter()
: createPwaAdapter();
const nativeRouter = createNativeRouter({
router,
cache: { maxInactive: 4 },
platform,
})
});
const app = createApp(App)
app.use(router)
app.use(nativeRouter)
const app = createApp(App);
app.use(router);
app.use(nativeRouter);
await router.isReady()
app.mount('#app')
await router.isReady();
app.mount("#app");

View File

@@ -1,66 +1,74 @@
import { ref } from 'vue'
import { ref } from "vue";
import {
createNativeNavigationProfiler,
type NativeNavigationProfiler,
type NativeProfilerReport,
type NativeRouterRuntime,
} from '@native-vue-router/core'
import { pwaBuildId } from './pwa'
} from "@native-vue-router/core";
import { pwaBuildId } from "./pwa";
let profiler: NativeNavigationProfiler | undefined
export const profilerRecording = ref(false)
export const profilerHasCapture = ref(false)
let profiler: NativeNavigationProfiler | undefined;
export const profilerRecording = ref(false);
export const profilerHasCapture = ref(false);
function instance(runtime: NativeRouterRuntime) {
profiler ??= createNativeNavigationProfiler(runtime, {
metadata: { app: 'nvr-messenger-demo', build: pwaBuildId },
})
return profiler
metadata: { app: "nvr-messenger-demo", build: pwaBuildId },
});
return profiler;
}
export function startDemoProfile(runtime: NativeRouterRuntime) {
const active = instance(runtime)
active.start()
profilerRecording.value = true
profilerHasCapture.value = true
const active = instance(runtime);
active.start();
profilerRecording.value = true;
profilerHasCapture.value = true;
}
export function stopDemoProfile(runtime: NativeRouterRuntime) {
const report = instance(runtime).stop()
profilerRecording.value = false
return report
const report = instance(runtime).stop();
profilerRecording.value = false;
return report;
}
export function snapshotDemoProfile(runtime: NativeRouterRuntime) {
return instance(runtime).snapshot()
return instance(runtime).snapshot();
}
export async function shareDemoProfile(runtime: NativeRouterRuntime, report?: NativeProfilerReport) {
const active = instance(runtime)
const current = report ?? (profilerRecording.value ? stopDemoProfile(runtime) : active.snapshot())
const json = active.toJSON(current)
const stamp = new Date().toISOString().replaceAll(':', '-').replaceAll('.', '-')
const filename = `native-vue-router-profile-${stamp}.json`
const file = new File([json], filename, { type: 'application/json' })
export async function shareDemoProfile(
runtime: NativeRouterRuntime,
report?: NativeProfilerReport,
) {
const active = instance(runtime);
const current =
report ??
(profilerRecording.value ? stopDemoProfile(runtime) : active.snapshot());
const json = active.toJSON(current);
const stamp = new Date()
.toISOString()
.replaceAll(":", "-")
.replaceAll(".", "-");
const filename = `native-vue-router-profile-${stamp}.json`;
const file = new File([json], filename, { type: "application/json" });
const shareNavigator = navigator as Navigator & {
canShare?: (data: ShareData) => boolean
share?: (data: ShareData) => Promise<void>
}
canShare?: (data: ShareData) => boolean;
share?: (data: ShareData) => Promise<void>;
};
if (shareNavigator.share && shareNavigator.canShare?.({ files: [file] })) {
await shareNavigator.share({
title: 'Native Vue Router performance profile',
text: 'Frame pacing and navigation diagnostics. Route params and query values are omitted.',
title: "Native Vue Router performance profile",
text: "Frame pacing and navigation diagnostics. Route params and query values are omitted.",
files: [file],
})
return { report: current, method: 'shared' as const }
});
return { report: current, method: "shared" as const };
}
const url = URL.createObjectURL(file)
const link = document.createElement('a')
link.href = url
link.download = filename
link.click()
window.setTimeout(() => URL.revokeObjectURL(url), 1_000)
return { report: current, method: 'downloaded' as const }
const url = URL.createObjectURL(file);
const link = document.createElement("a");
link.href = url;
link.download = filename;
link.click();
window.setTimeout(() => URL.revokeObjectURL(url), 1_000);
return { report: current, method: "downloaded" as const };
}

View File

@@ -1,62 +1,77 @@
import { reactive, readonly } from 'vue'
import type { NativePlatformAdapter } from '@native-vue-router/core'
import { reactive, readonly } from "vue";
import type { NativePlatformAdapter } from "@native-vue-router/core";
interface StandaloneNavigator extends Navigator {
standalone?: boolean
standalone?: boolean;
}
type ServiceWorkerState = 'unsupported' | 'installing' | 'ready'
export type PwaUpdateState = 'idle' | 'checking' | 'current' | 'ready' | 'error'
type ServiceWorkerState = "unsupported" | "installing" | "ready";
export type PwaUpdateState =
"idle" | "checking" | "current" | "ready" | "error";
declare const __NVR_BUILD_ID__: string
declare const __NVR_BUILD_ID__: string;
export const pwaBuildId = __NVR_BUILD_ID__
export const pwaBuildId = __NVR_BUILD_ID__;
const state = reactive({
ios: false,
standalone: false,
edgeGuard: false,
edgeClaims: 0,
serviceWorker: 'installing' as ServiceWorkerState,
updateState: 'idle' as PwaUpdateState,
serviceWorker: "installing" as ServiceWorkerState,
updateState: "idle" as PwaUpdateState,
updateChecks: 0,
})
});
export const pwaEnvironment = readonly(state)
export const pwaEnvironment = readonly(state);
export function recordPwaUpdateState(updateState: PwaUpdateState, checked = false) {
state.updateState = updateState
if (checked) state.updateChecks += 1
document.documentElement.dataset.pwaUpdateState = state.updateState
document.documentElement.dataset.pwaUpdateChecks = String(state.updateChecks)
export function recordPwaUpdateState(
updateState: PwaUpdateState,
checked = false,
) {
state.updateState = updateState;
if (checked) state.updateChecks += 1;
document.documentElement.dataset.pwaUpdateState = state.updateState;
document.documentElement.dataset.pwaUpdateChecks = String(state.updateChecks);
}
export function isIOSWebKit() {
const navigatorWithTouch = navigator as Navigator & { maxTouchPoints?: number }
return /iPad|iPhone|iPod/.test(navigator.userAgent)
|| (/Macintosh/.test(navigator.userAgent) && (navigatorWithTouch.maxTouchPoints ?? 0) > 1)
const navigatorWithTouch = navigator as Navigator & {
maxTouchPoints?: number;
};
return (
/iPad|iPhone|iPod/.test(navigator.userAgent) ||
(/Macintosh/.test(navigator.userAgent) &&
(navigatorWithTouch.maxTouchPoints ?? 0) > 1)
);
}
export function isStandalonePwa() {
return Boolean((navigator as StandaloneNavigator).standalone)
|| window.matchMedia('(display-mode: standalone)').matches
|| window.matchMedia('(display-mode: fullscreen)').matches
return (
Boolean((navigator as StandaloneNavigator).standalone) ||
window.matchMedia("(display-mode: standalone)").matches ||
window.matchMedia("(display-mode: fullscreen)").matches
);
}
function updateEnvironment() {
state.ios = isIOSWebKit()
state.standalone = isStandalonePwa()
state.edgeGuard = state.ios && state.standalone
document.documentElement.dataset.pwaPlatform = state.ios ? 'ios' : 'other'
document.documentElement.dataset.pwaDisplayMode = state.standalone ? 'standalone' : 'browser'
document.documentElement.dataset.pwaEdgeGuard = state.edgeGuard ? 'active' : 'inactive'
document.documentElement.dataset.pwaEdgeClaims = String(state.edgeClaims)
document.documentElement.dataset.pwaUpdateState = state.updateState
document.documentElement.dataset.pwaUpdateChecks = String(state.updateChecks)
state.ios = isIOSWebKit();
state.standalone = isStandalonePwa();
state.edgeGuard = state.ios && state.standalone;
document.documentElement.dataset.pwaPlatform = state.ios ? "ios" : "other";
document.documentElement.dataset.pwaDisplayMode = state.standalone
? "standalone"
: "browser";
document.documentElement.dataset.pwaEdgeGuard = state.edgeGuard
? "active"
: "inactive";
document.documentElement.dataset.pwaEdgeClaims = String(state.edgeClaims);
document.documentElement.dataset.pwaUpdateState = state.updateState;
document.documentElement.dataset.pwaUpdateChecks = String(state.updateChecks);
}
export interface PwaAdapterOptions {
edgeWidth?: number
edgeWidth?: number;
}
/**
@@ -66,65 +81,104 @@ export interface PwaAdapterOptions {
* Safari does not expose WKWebView's native gesture switch to web content,
* so this is deliberately scoped to standalone mode and reinforced by CSS.
*/
export function createPwaAdapter(options: PwaAdapterOptions = {}): NativePlatformAdapter {
const edgeWidth = options.edgeWidth ?? 32
export function createPwaAdapter(
options: PwaAdapterOptions = {},
): NativePlatformAdapter {
const edgeWidth = options.edgeWidth ?? 32;
return {
name: 'pwa',
name: "pwa",
install() {
updateEnvironment()
const displayMode = window.matchMedia('(display-mode: standalone)')
const update = () => updateEnvironment()
displayMode.addEventListener('change', update)
document.addEventListener('visibilitychange', update)
updateEnvironment();
const displayMode = window.matchMedia("(display-mode: standalone)");
const update = () => updateEnvironment();
displayMode.addEventListener("change", update);
document.addEventListener("visibilitychange", update);
if (!('serviceWorker' in navigator)) state.serviceWorker = 'unsupported'
if (!("serviceWorker" in navigator)) state.serviceWorker = "unsupported";
else {
state.serviceWorker = navigator.serviceWorker.controller ? 'ready' : 'installing'
void navigator.serviceWorker.ready.then(() => { state.serviceWorker = 'ready' })
navigator.serviceWorker.addEventListener('controllerchange', updateServiceWorkerState)
state.serviceWorker = navigator.serviceWorker.controller
? "ready"
: "installing";
void navigator.serviceWorker.ready.then(() => {
state.serviceWorker = "ready";
});
navigator.serviceWorker.addEventListener(
"controllerchange",
updateServiceWorkerState,
);
}
let reservedTouch: number | null = null
let reservedTouch: number | null = null;
const touchAtLeadingEdge = (event: TouchEvent) => {
if (!state.edgeGuard || event.touches.length !== 1) return undefined
if (!(event.target instanceof Element) || !event.target.closest('.nvr-navigator')) return undefined
const touch = event.touches[0]
const rtl = getComputedStyle(document.documentElement).direction === 'rtl'
const atEdge = rtl ? window.innerWidth - touch.clientX <= edgeWidth : touch.clientX <= edgeWidth
return atEdge ? touch : undefined
}
if (!state.edgeGuard || event.touches.length !== 1) return undefined;
if (
!(event.target instanceof Element) ||
!event.target.closest(".nvr-navigator")
)
return undefined;
const touch = event.touches[0];
const rtl =
getComputedStyle(document.documentElement).direction === "rtl";
const atEdge = rtl
? window.innerWidth - touch.clientX <= edgeWidth
: touch.clientX <= edgeWidth;
return atEdge ? touch : undefined;
};
const reserveEdge = (event: TouchEvent) => {
const touch = touchAtLeadingEdge(event)
if (!touch) return
reservedTouch = touch.identifier
state.edgeClaims += 1
document.documentElement.dataset.pwaEdgeClaims = String(state.edgeClaims)
event.preventDefault()
}
const touch = touchAtLeadingEdge(event);
if (!touch) return;
reservedTouch = touch.identifier;
state.edgeClaims += 1;
document.documentElement.dataset.pwaEdgeClaims = String(
state.edgeClaims,
);
event.preventDefault();
};
const holdEdge = (event: TouchEvent) => {
if (reservedTouch === null) return
if ([...event.touches].some((touch) => touch.identifier === reservedTouch)) event.preventDefault()
}
const releaseEdge = () => { reservedTouch = null }
if (reservedTouch === null) return;
if (
[...event.touches].some((touch) => touch.identifier === reservedTouch)
)
event.preventDefault();
};
const releaseEdge = () => {
reservedTouch = null;
};
document.addEventListener('touchstart', reserveEdge, { capture: true, passive: false })
document.addEventListener('touchmove', holdEdge, { capture: true, passive: false })
document.addEventListener('touchend', releaseEdge, { capture: true, passive: true })
document.addEventListener('touchcancel', releaseEdge, { capture: true, passive: true })
document.addEventListener("touchstart", reserveEdge, {
capture: true,
passive: false,
});
document.addEventListener("touchmove", holdEdge, {
capture: true,
passive: false,
});
document.addEventListener("touchend", releaseEdge, {
capture: true,
passive: true,
});
document.addEventListener("touchcancel", releaseEdge, {
capture: true,
passive: true,
});
return () => {
displayMode.removeEventListener('change', update)
document.removeEventListener('visibilitychange', update)
if ('serviceWorker' in navigator) navigator.serviceWorker.removeEventListener('controllerchange', updateServiceWorkerState)
document.removeEventListener('touchstart', reserveEdge, true)
document.removeEventListener('touchmove', holdEdge, true)
document.removeEventListener('touchend', releaseEdge, true)
document.removeEventListener('touchcancel', releaseEdge, true)
}
displayMode.removeEventListener("change", update);
document.removeEventListener("visibilitychange", update);
if ("serviceWorker" in navigator)
navigator.serviceWorker.removeEventListener(
"controllerchange",
updateServiceWorkerState,
);
document.removeEventListener("touchstart", reserveEdge, true);
document.removeEventListener("touchmove", holdEdge, true);
document.removeEventListener("touchend", releaseEdge, true);
document.removeEventListener("touchcancel", releaseEdge, true);
};
},
}
};
}
function updateServiceWorkerState() {
state.serviceWorker = 'ready'
state.serviceWorker = "ready";
}

View File

@@ -1,46 +1,116 @@
import { createRouter, createWebHashHistory, createWebHistory, type RouteRecordRaw } from 'vue-router'
import { evaluateRuntimeLabEntry, evaluateStoryEntry } from './guard-state'
import {
createRouter,
createWebHashHistory,
createWebHistory,
type RouteRecordRaw,
} from "vue-router";
import { evaluateRuntimeLabEntry, evaluateStoryEntry } from "./guard-state";
const routes: RouteRecordRaw[] = [
{ path: '/', redirect: '/inbox' },
{ path: "/", redirect: "/inbox" },
{
path: '/inbox', name: 'inbox', component: () => import('./views/InboxView.vue'),
meta: { tab: true, native: { siblingGroup: 'primary', siblingOrder: 0, siblingHistory: 'replace', gesture: 'full' } },
path: "/inbox",
name: "inbox",
component: () => import("./views/InboxView.vue"),
meta: {
tab: true,
native: {
siblingGroup: "primary",
siblingOrder: 0,
siblingHistory: "replace",
gesture: "full",
},
},
},
{
path: '/stories', name: 'stories', component: () => import('./views/StoriesView.vue'),
path: "/stories",
name: "stories",
component: () => import("./views/StoriesView.vue"),
beforeEnter: evaluateStoryEntry,
meta: { tab: true, native: { siblingGroup: 'primary', siblingOrder: 1, siblingHistory: 'replace', gesture: 'full' } },
meta: {
tab: true,
native: {
siblingGroup: "primary",
siblingOrder: 1,
siblingHistory: "replace",
gesture: "full",
},
},
},
{
path: '/profile', name: 'profile', component: () => import('./views/ProfileView.vue'),
meta: { tab: true, native: { siblingGroup: 'primary', siblingOrder: 2, siblingHistory: 'replace', gesture: 'full' } },
path: "/profile",
name: "profile",
component: () => import("./views/ProfileView.vue"),
meta: {
tab: true,
native: {
siblingGroup: "primary",
siblingOrder: 2,
siblingHistory: "replace",
gesture: "full",
},
},
},
{
path: '/profile/runtime-lab', name: 'runtime-lab', component: () => import('./views/RuntimeLabView.vue'),
path: "/profile/runtime-lab",
name: "runtime-lab",
component: () => import("./views/RuntimeLabView.vue"),
beforeEnter: evaluateRuntimeLabEntry,
meta: { tab: true, native: { siblingGroup: 'primary', siblingOrder: 3, siblingHistory: 'push', presentation: 'slide', parent: '/profile', gesture: 'full' } },
meta: {
tab: true,
native: {
siblingGroup: "primary",
siblingOrder: 3,
siblingHistory: "push",
presentation: "slide",
parent: "/profile",
gesture: "full",
},
},
},
{
path: '/chat/:id', name: 'chat', component: () => import('./views/ChatView.vue'),
meta: { native: { presentation: 'push', parent: '/inbox', gesture: 'edge' } },
path: "/chat/:id",
name: "chat",
component: () => import("./views/ChatView.vue"),
meta: {
native: { presentation: "push", parent: "/inbox", gesture: "edge" },
},
},
{
path: '/chat/:id/details', name: 'chat-details', component: () => import('./views/ContactView.vue'),
meta: { native: { presentation: 'push', parent: (route) => `/chat/${String(route.params.id)}`, gesture: 'edge' } },
path: "/chat/:id/details",
name: "chat-details",
component: () => import("./views/ContactView.vue"),
meta: {
native: {
presentation: "push",
parent: (route) => `/chat/${String(route.params.id)}`,
gesture: "edge",
},
},
},
{
path: '/compose', name: 'compose', component: () => import('./views/ComposeView.vue'),
meta: { native: { presentation: 'sheet', parent: '/inbox', gesture: 'full' } },
path: "/compose",
name: "compose",
component: () => import("./views/ComposeView.vue"),
meta: {
native: { presentation: "sheet", parent: "/inbox", gesture: "full" },
},
},
{
path: '/settings', name: 'settings', component: () => import('./views/SettingsView.vue'),
meta: { native: { presentation: 'push', parent: '/profile', gesture: 'edge' } },
path: "/settings",
name: "settings",
component: () => import("./views/SettingsView.vue"),
meta: {
native: { presentation: "push", parent: "/profile", gesture: "edge" },
},
},
]
];
export const router = createRouter({
history: window.location.protocol === 'file:' ? createWebHashHistory() : createWebHistory(),
history:
window.location.protocol === "file:"
? createWebHashHistory()
: createWebHistory(),
routes,
scrollBehavior: () => ({ top: 0 }),
})
});

File diff suppressed because it is too large Load Diff