mirror of
https://github.com/vrtmrz/obsidian-livesync.git
synced 2026-07-17 10:06:00 +00:00
@@ -6,7 +6,7 @@ Note: (I vrtmrz have not tested this so much yet).
|
||||
|
||||
- 🌐 Runs entirely in the browser
|
||||
- 📁 Uses FileSystem API to access your local vault
|
||||
- 🔄 Syncs with CouchDB, Object Storage server (compatible with Self-hosted LiveSync plugin)
|
||||
- 🔄 Syncs with CouchDB, Object Storage server (compatible with Self-hosted LiveSync plug-in)
|
||||
- 🚫 No server-side code required!!
|
||||
- 💾 Settings stored in `.livesync/settings.json` within your vault
|
||||
- 👁️ Real-time file watching (Chrome 124+ with FileSystemObserver)
|
||||
@@ -127,7 +127,7 @@ webapp/
|
||||
1. **Adapters**: Implement `IFileSystemAdapter` interface using FileSystem API
|
||||
2. **Managers**: Handle storage events and file watching
|
||||
3. **Service Modules**: Integrate with LiveSyncBaseCore
|
||||
4. **Main**: Application initialization and lifecycle management
|
||||
4. **Main**: Application initialisation and lifecycle management
|
||||
|
||||
### Service Hub
|
||||
|
||||
@@ -154,11 +154,11 @@ Uses `BrowserServiceHub` which provides:
|
||||
- Settings stored in `.livesync/settings.json` in vault
|
||||
- Real-time file watching only with FileSystemObserver (Chrome 124+)
|
||||
|
||||
## Differences from Obsidian Plugin
|
||||
## Differences from Obsidian Plug-in
|
||||
|
||||
- No Obsidian-specific modules (UI, settings dialog, etc.)
|
||||
- No Obsidian-specific modules (UI, settings dialogue, etc.)
|
||||
- Simplified configuration
|
||||
- No plugin/theme sync features
|
||||
- No plug-in/theme sync features
|
||||
- No internal file handling (`.obsidian` folder)
|
||||
|
||||
## Development Notes
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
import type { FilePath, UXFileInfoStub, UXFolderInfo } from "@lib/common/types";
|
||||
import type { IConversionAdapter } from "@lib/serviceModules/adapters";
|
||||
import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
||||
|
||||
@@ -28,7 +28,7 @@ export class FSAPIConversionAdapter implements IConversionAdapter<FSAPIFile, FSA
|
||||
path: folder.path,
|
||||
isFolder: true,
|
||||
children: [],
|
||||
parent: parentPath as any,
|
||||
parent: parentPath as FilePath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,11 @@ export class FSAPIFileSystemAdapter implements IFileSystemAdapter<FSAPIFile, FSA
|
||||
}
|
||||
|
||||
// Use AsyncIterator instead of .values() for better compatibility
|
||||
for await (const [name, entry] of (currentHandle as any).entries()) {
|
||||
for await (const [name, entry] of (
|
||||
currentHandle as unknown as {
|
||||
entries(): AsyncIterable<[string, FileSystemHandle]>;
|
||||
}
|
||||
).entries()) {
|
||||
const entryPath = relativePath ? `${relativePath}/${name}` : name;
|
||||
|
||||
if (entry.kind === "directory") {
|
||||
|
||||
@@ -195,7 +195,11 @@ export class FSAPIStorageAdapter implements IStorageAdapter<FSAPIStat> {
|
||||
const folders: string[] = [];
|
||||
|
||||
// Use AsyncIterator instead of .values() for better compatibility
|
||||
for await (const [name, entry] of (dirHandle as any).entries()) {
|
||||
for await (const [name, entry] of (
|
||||
dirHandle as unknown as {
|
||||
entries(): AsyncIterable<[string, FileSystemHandle]>;
|
||||
}
|
||||
).entries()) {
|
||||
const entryPath = basePath ? `${basePath}/${name}` : name;
|
||||
|
||||
if (entry.kind === "directory") {
|
||||
|
||||
@@ -5,13 +5,24 @@ import type { FSAPIFile, FSAPIFolder } from "./FSAPITypes";
|
||||
* Type guard adapter implementation for FileSystem API
|
||||
*/
|
||||
export class FSAPITypeGuardAdapter implements ITypeGuardAdapter<FSAPIFile, FSAPIFolder> {
|
||||
isFile(file: any): file is FSAPIFile {
|
||||
return (
|
||||
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
|
||||
isFile(file: unknown): file is FSAPIFile {
|
||||
return !!(
|
||||
file &&
|
||||
typeof file === "object" &&
|
||||
"path" in file &&
|
||||
"stat" in file &&
|
||||
"handle" in file &&
|
||||
!(file as { isFolder?: boolean }).isFolder
|
||||
);
|
||||
}
|
||||
|
||||
isFolder(item: any): item is FSAPIFolder {
|
||||
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
|
||||
isFolder(item: unknown): item is FSAPIFolder {
|
||||
return !!(
|
||||
item &&
|
||||
typeof item === "object" &&
|
||||
"path" in item &&
|
||||
(item as { isFolder?: boolean }).isFolder === true &&
|
||||
"handle" in item
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,8 +116,7 @@ export class FSAPIVaultAdapter implements IVaultAdapter<FSAPIFile> {
|
||||
await this.delete(file, force);
|
||||
}
|
||||
|
||||
trigger(name: string, ...data: any[]): any {
|
||||
trigger(name: string, ...data: unknown[]): void {
|
||||
// No-op in webapp version (no event system yet)
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,19 +121,17 @@ async function initializeVaultSelector(): Promise<void> {
|
||||
await renderHistoryList();
|
||||
}
|
||||
|
||||
compatGlobal.addEventListener("load", async () => {
|
||||
try {
|
||||
await initializeVaultSelector();
|
||||
} catch (error) {
|
||||
compatGlobal.addEventListener("load", () => {
|
||||
initializeVaultSelector().catch((error) => {
|
||||
console.error("Failed to initialize vault selector:", error);
|
||||
setStatus("error", `Initialization failed: ${String(error)}`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
compatGlobal.addEventListener("beforeunload", () => {
|
||||
void app?.shutdown();
|
||||
});
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- patching
|
||||
(compatGlobal as any).livesyncApp = {
|
||||
getApp: () => app,
|
||||
historyStore,
|
||||
|
||||
+14
-12
@@ -23,7 +23,6 @@ import { compatGlobal, _activeDocument } from "@lib/common/coreEnvFunctions.ts";
|
||||
|
||||
const SETTINGS_DIR = ".livesync";
|
||||
const SETTINGS_FILE = "settings.json";
|
||||
const DB_NAME = "livesync-webapp";
|
||||
|
||||
/**
|
||||
* Default settings for the webapp
|
||||
@@ -51,7 +50,7 @@ const DEFAULT_SETTINGS: Partial<ObsidianLiveSyncSettings> = {
|
||||
|
||||
class LiveSyncWebApp {
|
||||
private rootHandle: FileSystemDirectoryHandle;
|
||||
private core: LiveSyncBaseCore<ServiceContext, any> | null = null;
|
||||
private core: LiveSyncBaseCore<ServiceContext, never> | null = null;
|
||||
private serviceHub: BrowserServiceHub<ServiceContext> | null = null;
|
||||
|
||||
constructor(rootHandle: FileSystemDirectoryHandle) {
|
||||
@@ -65,7 +64,6 @@ class LiveSyncWebApp {
|
||||
console.log(`Vault directory: ${this.rootHandle.name}`);
|
||||
|
||||
// Create service context and hub
|
||||
const context = new ServiceContext();
|
||||
this.serviceHub = new BrowserServiceHub<ServiceContext>();
|
||||
|
||||
// Setup API service
|
||||
@@ -99,17 +97,19 @@ class LiveSyncWebApp {
|
||||
});
|
||||
|
||||
// App lifecycle handlers
|
||||
this.serviceHub.appLifecycle.scheduleRestart.setHandler(async () => {
|
||||
console.log("[AppLifecycle] Restart requested");
|
||||
await this.shutdown();
|
||||
await this.initialize();
|
||||
compatGlobal.setTimeout(() => {
|
||||
compatGlobal.location.reload();
|
||||
}, 1000);
|
||||
this.serviceHub.appLifecycle.scheduleRestart.setHandler(() => {
|
||||
void (async () => {
|
||||
console.log("[AppLifecycle] Restart requested");
|
||||
await this.shutdown();
|
||||
await this.initialize();
|
||||
compatGlobal.setTimeout(() => {
|
||||
compatGlobal.location.reload();
|
||||
}, 1000);
|
||||
})();
|
||||
});
|
||||
|
||||
// Create LiveSync core
|
||||
this.core = new LiveSyncBaseCore(
|
||||
this.core = new LiveSyncBaseCore<ServiceContext, never>(
|
||||
this.serviceHub,
|
||||
(core, serviceHub) => {
|
||||
return initialiseServiceModulesFSAPI(this.rootHandle, core, serviceHub);
|
||||
@@ -129,7 +129,7 @@ class LiveSyncWebApp {
|
||||
// new ModuleReplicatorP2P(core), // Register P2P replicator for CLI (useP2PReplicator is not used here)
|
||||
new SetupManager(core),
|
||||
],
|
||||
() => [], // No add-ons
|
||||
() => [] as never[], // No add-ons
|
||||
(core) => {
|
||||
useOfflineScanner(core);
|
||||
useRedFlagFeatures(core);
|
||||
@@ -207,6 +207,7 @@ class LiveSyncWebApp {
|
||||
}
|
||||
|
||||
// Scan the directory to populate file cache
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
|
||||
const fileAccess = (this.core as any)._serviceModules?.storageAccess?.vaultAccess;
|
||||
if (fileAccess?.fsapiAdapter) {
|
||||
console.log("[Scanning] Scanning vault directory...");
|
||||
@@ -225,6 +226,7 @@ class LiveSyncWebApp {
|
||||
console.log("[Shutdown] Shutting down...");
|
||||
|
||||
// Stop file watching
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
|
||||
const storageEventManager = (this.core as any)._serviceModules?.storageAccess?.storageEventManager;
|
||||
if (storageEventManager?.cleanup) {
|
||||
await storageEventManager.cleanup();
|
||||
|
||||
@@ -17,14 +17,25 @@ import { compatGlobal } from "@lib/common/coreEnvFunctions.ts";
|
||||
* FileSystem API-specific type guard adapter
|
||||
*/
|
||||
class FSAPITypeGuardAdapter implements IStorageEventTypeGuardAdapter<FSAPIFile, FSAPIFolder> {
|
||||
isFile(file: any): file is FSAPIFile {
|
||||
return (
|
||||
file && typeof file === "object" && "path" in file && "stat" in file && "handle" in file && !file.isFolder
|
||||
isFile(file: unknown): file is FSAPIFile {
|
||||
return !!(
|
||||
file &&
|
||||
typeof file === "object" &&
|
||||
"path" in file &&
|
||||
"stat" in file &&
|
||||
"handle" in file &&
|
||||
!(file as { isFolder?: boolean }).isFolder
|
||||
);
|
||||
}
|
||||
|
||||
isFolder(item: any): item is FSAPIFolder {
|
||||
return item && typeof item === "object" && "path" in item && item.isFolder === true && "handle" in item;
|
||||
isFolder(item: unknown): item is FSAPIFolder {
|
||||
return !!(
|
||||
item &&
|
||||
typeof item === "object" &&
|
||||
"path" in item &&
|
||||
(item as { isFolder?: boolean }).isFolder === true &&
|
||||
"handle" in item
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,12 +155,14 @@ class FSAPIConverterAdapter implements IStorageEventConverterAdapter<FSAPIFile>
|
||||
* FileSystem API-specific watch adapter using FileSystemObserver (Chrome only)
|
||||
*/
|
||||
class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
|
||||
private observer: any = null; // FileSystemObserver type
|
||||
|
||||
constructor(private rootHandle: FileSystemDirectoryHandle) {}
|
||||
|
||||
async beginWatch(handlers: IStorageEventWatchHandlers): Promise<void> {
|
||||
// Use FileSystemObserver if available (Chrome 124+)
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing global FileSystemObserver
|
||||
if (typeof (compatGlobal as any).FileSystemObserver === "undefined") {
|
||||
console.log("[FSAPIWatchAdapter] FileSystemObserver not available, file watching disabled");
|
||||
console.log("[FSAPIWatchAdapter] Consider using Chrome 124+ for real-time file watching");
|
||||
@@ -157,11 +170,12 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
|
||||
const FileSystemObserver = (compatGlobal as any).FileSystemObserver;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- Accessing private service modules
|
||||
this.observer = new FileSystemObserver(async (records: any[]) => {
|
||||
for (const record of records) {
|
||||
const handle = record.root;
|
||||
const changedHandle = record.changedHandle;
|
||||
const relativePathComponents = record.relativePathComponents;
|
||||
const type = record.type; // "appeared", "disappeared", "modified", "moved", "unknown", "errored"
|
||||
@@ -193,9 +207,9 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
};
|
||||
|
||||
if (type === "appeared") {
|
||||
await handlers.onCreate(fileInfo, undefined);
|
||||
handlers.onCreate(fileInfo, undefined);
|
||||
} else {
|
||||
await handlers.onChange(fileInfo, undefined);
|
||||
handlers.onChange(fileInfo, undefined);
|
||||
}
|
||||
}
|
||||
} else if (type === "disappeared") {
|
||||
@@ -207,9 +221,9 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
ctime: Date.now(),
|
||||
type: "file" as const,
|
||||
},
|
||||
handle: null as any,
|
||||
handle: null as unknown as FileSystemFileHandle, // No handle available for disappeared files
|
||||
};
|
||||
await handlers.onDelete(fileInfo, undefined);
|
||||
handlers.onDelete(fileInfo, undefined);
|
||||
} else if (type === "moved") {
|
||||
// Handle as delete + create
|
||||
// Note: FileSystemObserver provides both old and new paths in some cases
|
||||
@@ -226,7 +240,7 @@ class FSAPIWatchAdapter implements IStorageEventWatchAdapter {
|
||||
},
|
||||
handle: changedHandle,
|
||||
};
|
||||
await handlers.onChange(fileInfo, undefined);
|
||||
handlers.onChange(fileInfo, undefined);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
|
||||
@@ -30,7 +30,7 @@ export class StorageEventManagerFSAPI extends StorageEventManagerBase<FSAPIStora
|
||||
async cleanup() {
|
||||
// Stop file watching
|
||||
if (this.fsapiAdapter?.watch) {
|
||||
await (this.fsapiAdapter.watch as any).stopWatch?.();
|
||||
await this.fsapiAdapter.watch.stopWatch?.();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "livesync-webapp",
|
||||
"private": true,
|
||||
"version": "0.25.76-webapp",
|
||||
"version": "0.25.77-webapp",
|
||||
"type": "module",
|
||||
"description": "Browser-based Self-hosted LiveSync using FileSystem API",
|
||||
"scripts": {
|
||||
|
||||
@@ -22,7 +22,7 @@ import { ServiceFileHandler } from "@/serviceModules/FileHandler";
|
||||
*/
|
||||
export function initialiseServiceModulesFSAPI(
|
||||
rootHandle: FileSystemDirectoryHandle,
|
||||
core: LiveSyncBaseCore<ServiceContext, any>,
|
||||
core: LiveSyncBaseCore<ServiceContext, never>,
|
||||
services: InjectableServiceHub<ServiceContext>
|
||||
): ServiceModules {
|
||||
const storageAccessManager = new StorageAccessManager();
|
||||
@@ -67,7 +67,7 @@ export function initialiseServiceModulesFSAPI(
|
||||
});
|
||||
|
||||
// File handler (platform-independent)
|
||||
const fileHandler = new (ServiceFileHandler as any)({
|
||||
const fileHandler = new ServiceFileHandler({
|
||||
API: services.API,
|
||||
databaseFileAccess: databaseFileAccess,
|
||||
conflict: services.conflict,
|
||||
|
||||
@@ -28,11 +28,38 @@ function stripPrefix(raw: string): string {
|
||||
return raw.replace(/^[^:]+:/, "");
|
||||
}
|
||||
|
||||
interface TestCore {
|
||||
services?: {
|
||||
replication?: {
|
||||
databaseQueueCount?: { value: number };
|
||||
storageApplyingCount?: { value: number };
|
||||
};
|
||||
fileProcessing?: {
|
||||
totalQueued?: { value: number };
|
||||
batched?: { value: number };
|
||||
processing?: { value: number };
|
||||
};
|
||||
database?: {
|
||||
localDatabase: {
|
||||
findAllNormalDocs(options?: { conflicts?: boolean }): AsyncIterable<{
|
||||
_deleted?: boolean;
|
||||
deleted?: boolean;
|
||||
path?: string;
|
||||
_rev?: string;
|
||||
_conflicts?: string[];
|
||||
size?: number;
|
||||
mtime?: number;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Poll every 300 ms until all known processing queues are drained, or until
|
||||
* the timeout elapses. Mirrors `waitForIdle` in the existing vitest harness.
|
||||
*/
|
||||
async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
|
||||
async function waitForIdle(core: TestCore, timeoutMs = 60_000): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs;
|
||||
while (Date.now() < deadline) {
|
||||
const q =
|
||||
@@ -47,8 +74,8 @@ async function waitForIdle(core: any, timeoutMs = 60_000): Promise<void> {
|
||||
throw new Error(`waitForIdle timed out after ${timeoutMs} ms`);
|
||||
}
|
||||
|
||||
function getCore(): any {
|
||||
const core = (app as any)?.core;
|
||||
function getCore(): TestCore {
|
||||
const core = (app as unknown as { core: TestCore | null })?.core;
|
||||
if (!core) throw new Error("Vault not initialised – call livesyncTest.init() first");
|
||||
return core;
|
||||
}
|
||||
@@ -178,10 +205,10 @@ const livesyncTest: LiveSyncTestAPI = {
|
||||
if (docPath !== vaultPath) continue;
|
||||
return {
|
||||
path: docPath,
|
||||
revision: (doc._rev as string) ?? "",
|
||||
conflicts: (doc._conflicts as string[]) ?? [],
|
||||
size: (doc.size as number) ?? 0,
|
||||
mtime: (doc.mtime as number) ?? 0,
|
||||
revision: doc._rev ?? "",
|
||||
conflicts: doc._conflicts ?? [],
|
||||
size: doc.size ?? 0,
|
||||
mtime: doc.mtime ?? 0,
|
||||
};
|
||||
}
|
||||
return null;
|
||||
@@ -201,4 +228,4 @@ const livesyncTest: LiveSyncTestAPI = {
|
||||
};
|
||||
|
||||
// Expose on window for Playwright page.evaluate() calls.
|
||||
(compatGlobal as any).livesyncTest = livesyncTest;
|
||||
(compatGlobal as unknown as Record<string, unknown>).livesyncTest = livesyncTest;
|
||||
|
||||
@@ -24,9 +24,9 @@
|
||||
// "baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["../../*"],
|
||||
"@lib/*": ["../../lib/src/*"]
|
||||
"@lib/*": ["../../lib/src/*", "../../../_types/src/lib/src/*"]
|
||||
}
|
||||
},
|
||||
"include": ["*.ts", "**/*.ts", "**/*.tsx"],
|
||||
"include": ["*.ts", "**/*.ts", "**/*.tsx", "**/*.svelte"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
|
||||
@@ -32,7 +32,10 @@ function randomId(): string {
|
||||
}
|
||||
|
||||
async function hasReadWritePermission(handle: FileSystemDirectoryHandle, requestIfNeeded: boolean): Promise<boolean> {
|
||||
const h = handle as any;
|
||||
const h = handle as unknown as {
|
||||
queryPermission?: (options: { mode: "readwrite" }) => Promise<PermissionState>;
|
||||
requestPermission?: (options: { mode: "readwrite" }) => Promise<PermissionState>;
|
||||
};
|
||||
if (typeof h.queryPermission === "function") {
|
||||
const queried = await h.queryPermission({ mode: "readwrite" });
|
||||
if (queried === "granted") {
|
||||
@@ -91,7 +94,7 @@ export class VaultHistoryStore {
|
||||
|
||||
async getVaultHistory(): Promise<VaultHistoryItem[]> {
|
||||
return this.withStore("readonly", async (store) => {
|
||||
const keys = (await this.requestAsPromise(store.getAllKeys()));
|
||||
const keys = await this.requestAsPromise(store.getAllKeys());
|
||||
const values = (await this.requestAsPromise(store.getAll())) as unknown[];
|
||||
const items: VaultHistoryItem[] = [];
|
||||
for (let i = 0; i < keys.length; i++) {
|
||||
@@ -172,15 +175,17 @@ export class VaultHistoryStore {
|
||||
}
|
||||
|
||||
async pickNewVault(): Promise<FileSystemDirectoryHandle> {
|
||||
const picker = (compatGlobal as any).showDirectoryPicker;
|
||||
const picker = (compatGlobal as unknown as Record<string, unknown>).showDirectoryPicker as
|
||||
| ((options?: { mode?: "readwrite" | "read"; startIn?: string }) => Promise<FileSystemDirectoryHandle>)
|
||||
| undefined;
|
||||
if (typeof picker !== "function") {
|
||||
throw new Error("FileSystem API showDirectoryPicker is not supported in this browser");
|
||||
}
|
||||
|
||||
const handle = (await picker({
|
||||
const handle = await picker({
|
||||
mode: "readwrite",
|
||||
startIn: "documents",
|
||||
})) as FileSystemDirectoryHandle;
|
||||
});
|
||||
|
||||
const granted = await hasReadWritePermission(handle, true);
|
||||
if (!granted) {
|
||||
|
||||
@@ -3,10 +3,12 @@ import { svelte } from "@sveltejs/vite-plugin-svelte";
|
||||
import istanbul from "vite-plugin-istanbul";
|
||||
import path from "node:path";
|
||||
import { readFileSync } from "node:fs";
|
||||
const packageJson = JSON.parse(readFileSync("../../../package.json", "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync("../../../manifest.json", "utf-8"));
|
||||
const enableCoverage = process.env.PW_COVERAGE === "1";
|
||||
import { fileURLToPath } from "node:url";
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(__dirname, "../../..");
|
||||
const packageJson = JSON.parse(readFileSync(path.resolve(repoRoot, "package.json"), "utf-8"));
|
||||
const manifestJson = JSON.parse(readFileSync(path.resolve(repoRoot, "manifest.json"), "utf-8"));
|
||||
const enableCoverage = process.env.PW_COVERAGE === "1";
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
font-family: system-ui, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
@@ -254,7 +254,7 @@ body.livesync-log-visible {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
padding: 8px 12px;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-family: "SFMono-Regular", Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: #e2e8f0;
|
||||
@@ -397,6 +397,4 @@ popup {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
backdrop-filter: blur(15px);
|
||||
border-radius: 10px;
|
||||
z-index: 10;
|
||||
}
|
||||
Reference in New Issue
Block a user