diff --git a/src/app/components/updates-dialog/UpdatesDialog.css.ts b/src/app/components/updates-dialog/UpdatesDialog.css.ts
index af1f358..c313196 100644
--- a/src/app/components/updates-dialog/UpdatesDialog.css.ts
+++ b/src/app/components/updates-dialog/UpdatesDialog.css.ts
@@ -7,6 +7,7 @@ export const PortalLayer = style({
position: 'fixed',
inset: 0,
zIndex: config.zIndex.Max,
+ pointerEvents: 'auto',
});
/** Dialog + overlay padding must never exceed the viewport height. */
diff --git a/src/app/components/updates-dialog/UpdatesDialog.tsx b/src/app/components/updates-dialog/UpdatesDialog.tsx
index 873363c..feecb10 100644
--- a/src/app/components/updates-dialog/UpdatesDialog.tsx
+++ b/src/app/components/updates-dialog/UpdatesDialog.tsx
@@ -4,7 +4,6 @@ import FocusTrap from 'focus-trap-react';
import {
Box,
Button,
- config,
Dialog,
IconButton,
Overlay,
@@ -48,6 +47,9 @@ export function UpdatesDialog({
setShowOlderList(false);
};
+ const portalTarget =
+ document.getElementById('portalContainer') ?? document.body;
+
return createPortal(
}>
@@ -55,8 +57,7 @@ export function UpdatesDialog({
@@ -181,6 +182,6 @@ export function UpdatesDialog({
,
- document.body
+ portalTarget
);
}
diff --git a/src/app/components/updates-dialog/UpdatesDialogHost.tsx b/src/app/components/updates-dialog/UpdatesDialogHost.tsx
index 14a9df9..8db2c91 100644
--- a/src/app/components/updates-dialog/UpdatesDialogHost.tsx
+++ b/src/app/components/updates-dialog/UpdatesDialogHost.tsx
@@ -26,7 +26,10 @@ type UpdaterInfo = {
export function useOpenReleaseNotesDialog(): () => void {
const setDialogState = useSetAtom(releaseNotesDialogAtom);
return useCallback(() => {
- setDialogState({ open: true, manual: true });
+ // Defer so the opening tap does not trip FocusTrap click-outside on Android.
+ window.requestAnimationFrame(() => {
+ setDialogState({ open: true, manual: true });
+ });
}, [setDialogState]);
}
diff --git a/src/app/data/updateNotes.ts b/src/app/data/updateNotes.ts
index 497ab41..99b8f1e 100644
--- a/src/app/data/updateNotes.ts
+++ b/src/app/data/updateNotes.ts
@@ -1,5 +1,12 @@
+import bundledManifest from '../../../public/update/manifest.json';
import { trimTrailingSlash } from '../utils/common';
+const bundledMarkdownByFile = import.meta.glob('../../../public/update/*.md', {
+ query: '?raw',
+ import: 'default',
+ eager: true,
+}) as Record;
+
function getUpdateBaseUrl(): string {
const basePath = trimTrailingSlash(import.meta.env.BASE_URL || './');
const relative =
@@ -64,9 +71,26 @@ function parseUpdateMarkdown(file: string, source: string): ParsedUpdateDoc {
};
}
+function loadBundledManifest(): UpdateManifest {
+ const manifest = bundledManifest as UpdateManifest;
+ return {
+ current: manifest.current,
+ older: (manifest.older ?? []).filter((entry) => entry?.file),
+ };
+}
+
+function loadBundledDocument(file: string): ParsedUpdateDoc | null {
+ const safeFile = file.replace(/^\/+/, '');
+ const entry = Object.entries(bundledMarkdownByFile).find(([path]) =>
+ path.endsWith(`/${safeFile}`)
+ );
+ if (!entry) return null;
+ return parseUpdateMarkdown(safeFile, entry[1]);
+}
+
export function resolveUpdateAssetUrl(src: string | undefined): string | undefined {
if (!src) return undefined;
- if (/^(https?:|data:|blob:)/i.test(src)) return src;
+ if (/^(https?:|data:|blob:|capacitor:)/i.test(src)) return src;
if (src.startsWith('/')) return src;
const normalized = src.replace(/^\.\//, '');
@@ -76,15 +100,21 @@ export function resolveUpdateAssetUrl(src: string | undefined): string | undefin
export async function loadUpdateManifest(): Promise {
try {
const response = await fetch(getUpdateFileUrl('manifest.json'), { cache: 'no-cache' });
- if (!response.ok) return null;
+ if (response.ok) {
+ const data = (await response.json()) as UpdateManifest;
+ if (data?.current && Array.isArray(data.older)) {
+ return {
+ current: data.current,
+ older: data.older.filter((entry) => entry?.file),
+ };
+ }
+ }
+ } catch {
+ // fall through to bundled copy (Capacitor / offline)
+ }
- const data = (await response.json()) as UpdateManifest;
- if (!data?.current || !Array.isArray(data.older)) return null;
-
- return {
- current: data.current,
- older: data.older.filter((entry) => entry?.file),
- };
+ try {
+ return loadBundledManifest();
} catch {
return null;
}
@@ -96,13 +126,15 @@ export async function loadUpdateDocument(file: string): Promise {