collabmd 0.1.31 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/assets/{drawio-editor-B4sHHwWJ.js → drawio-editor-BMTwP5mD.js} +1 -1
- package/dist/client/assets/drawioEditor-D6clB5OH.js +2 -0
- package/dist/client/assets/{editor-session-BKHEYkbY.js → editor-session-Dzy_85Wp.js} +2 -2
- package/dist/client/assets/embedded-editor-base-CgEkjNYn.css +1 -0
- package/dist/client/assets/{excalidraw-editor-CHbeNZu6.js → excalidraw-editor-CAP9F8CB.js} +3 -3
- package/dist/client/assets/{excalidrawEditor-BhaTX2Ko.js → excalidrawEditor-DD4AvzYP.js} +2 -2
- package/dist/client/assets/exportDocument-BbkN85c4.css +1 -0
- package/dist/client/assets/index-BIt5EZXz.css +1 -0
- package/dist/client/assets/{index-kPgEYvEX.js → index-Czg_KL-i.js} +2 -2
- package/dist/client/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/client/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/client/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/client/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/client/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/client/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/client/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/client/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/dist/client/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/client/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/client/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/client/assets/main-DeowJOSp.js +1368 -0
- package/dist/client/assets/vault-api-client-D7Ck0CvR.css +1 -0
- package/dist/client/assets/vault-api-client-p7h7cXKM.js +1 -0
- package/dist/client/drawio-editor.html +1 -1
- package/dist/client/excalidraw-editor.html +1 -1
- package/dist/client/export-document.html +2 -2
- package/dist/client/index.html +2 -2
- package/package.json +8 -5
- package/src/client/app/main-entry.js +2 -0
- package/src/client/application/app-shell/git-feature.js +28 -36
- package/src/client/application/app-shell/presence-feature.js +3 -1
- package/src/client/application/diagram-preview-hydrator.js +24 -15
- package/src/client/application/mermaid-preview-hydrator.js +121 -4
- package/src/client/application/plantuml-preview-hydrator.js +6 -16
- package/src/client/application/preview-renderer.js +11 -2
- package/src/client/bootstrap/collabmd-app-shell.js +182 -50
- package/src/client/domain/excalidraw-document-switch.js +3 -0
- package/src/client/excalidraw-editor.js +187 -84
- package/src/client/infrastructure/auth-client.js +4 -4
- package/src/client/infrastructure/backlinks-api-client.js +18 -0
- package/src/client/infrastructure/comment-thread-store.js +4 -0
- package/src/client/infrastructure/editor-session.js +6 -0
- package/src/client/infrastructure/editor-view-adapter.js +23 -0
- package/src/client/infrastructure/git-api-client.js +134 -0
- package/src/client/infrastructure/plantuml-api-client.js +21 -0
- package/src/client/infrastructure/vault-api-client.js +115 -0
- package/src/client/presentation/backlinks-panel.js +6 -9
- package/src/client/presentation/bases-preview-controller.js +1413 -34
- package/src/client/presentation/comments-panel.js +50 -5
- package/src/client/presentation/excalidraw-embed-controller.js +251 -15
- package/src/client/presentation/excalidraw-embed-reconciler.js +0 -1
- package/src/client/presentation/file-action-controller.js +97 -0
- package/src/client/presentation/file-explorer-controller.js +2 -0
- package/src/client/presentation/file-explorer-view.js +368 -0
- package/src/client/presentation/file-history-view-controller.js +41 -40
- package/src/client/presentation/git-diff-view-controller.js +70 -53
- package/src/client/presentation/git-panel-controller.js +12 -30
- package/src/client/styles/components/scrollbars.css +5 -0
- package/src/client/styles/export-document.css +45 -2
- package/src/client/styles/features/auth-gate.css +13 -24
- package/src/client/styles/features/comment-card.css +6 -0
- package/src/client/styles/features/comments-drawer.css +38 -18
- package/src/client/styles/features/diagram-preview.css +33 -0
- package/src/client/styles/features/embedded-preview.css +37 -11
- package/src/client/styles/features/preview-markdown.css +559 -39
- package/src/client/styles/foundation/themes.css +4 -0
- package/src/client/styles/foundation/tokens.css +4 -3
- package/src/client/styles/layout/sidebar.css +72 -0
- package/src/server/auth/auth-common.js +224 -0
- package/src/server/auth/auth-constants.js +30 -0
- package/src/server/auth/auth-strategies.js +320 -0
- package/src/server/auth/create-auth-service.js +24 -545
- package/src/server/domain/backlink-index.js +177 -5
- package/src/server/domain/bases/base-definition.js +382 -0
- package/src/server/domain/bases/base-expression-runtime.js +1196 -0
- package/src/server/domain/bases/base-index-snapshot-store.js +565 -0
- package/src/server/domain/bases/base-query-metadata.js +242 -0
- package/src/server/domain/bases/base-query-results.js +215 -0
- package/src/server/domain/bases/base-query-service.js +406 -1992
- package/src/server/domain/bases/base-transform.js +116 -0
- package/src/server/domain/collaboration/collaboration-room.js +36 -1
- package/src/server/domain/collaboration/room-client-state-store.js +1 -0
- package/src/server/domain/collaboration/room-persistence-controller.js +1 -1
- package/src/server/infrastructure/http/create-vault-api-query-handler.js +161 -1
- package/src/server/infrastructure/http/http-response.js +56 -0
- package/src/server/infrastructure/persistence/vault-file-store.js +110 -0
- package/dist/client/assets/drawioEditor-DGpo2hcx.js +0 -2
- package/dist/client/assets/embedded-editor-base-DgmRcsR8.css +0 -1
- package/dist/client/assets/exportDocument-DmV7Gngb.css +0 -1
- package/dist/client/assets/index-D69HOZHr.css +0 -1
- package/dist/client/assets/main-DSxNTtD-.js +0 -1243
- package/dist/client/assets/vault-api-client-BzHQseMN.js +0 -1
- package/dist/client/assets/vault-api-client-De7kMvrE.css +0 -1
- /package/dist/client/assets/{exportDocument-CtZKWE2c.js → exportDocument-Zfwq1XVx.js} +0 -0
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import yaml from 'js-yaml';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
findView,
|
|
5
|
+
normalizeBaseDefinition,
|
|
6
|
+
normalizeRawDefinitionForWrite,
|
|
7
|
+
} from './base-definition.js';
|
|
8
|
+
|
|
9
|
+
function isPlainObject(value) {
|
|
10
|
+
return Object.prototype.toString.call(value) === '[object Object]';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function cloneRawSource(source = '') {
|
|
14
|
+
const parsed = yaml.load(String(source ?? '')) ?? {};
|
|
15
|
+
return isPlainObject(parsed) ? parsed : {};
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function ensureViews(rawDefinition) {
|
|
19
|
+
if (!Array.isArray(rawDefinition.views) || rawDefinition.views.length === 0) {
|
|
20
|
+
rawDefinition.views = [{ name: 'Table', type: 'table' }];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return rawDefinition.views;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function findViewIndex(definition, requestedView = '') {
|
|
27
|
+
const activeView = findView(definition, requestedView);
|
|
28
|
+
return definition.views.findIndex((view) => view.id === activeView.id);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function serializeGroupBy(groupBy) {
|
|
32
|
+
if (!groupBy?.property) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
direction: String(groupBy.direction ?? 'asc').toUpperCase(),
|
|
38
|
+
property: groupBy.property,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function serializeSort(sort = []) {
|
|
43
|
+
return sort
|
|
44
|
+
.filter((entry) => entry?.property)
|
|
45
|
+
.map((entry) => ({
|
|
46
|
+
direction: String(entry.direction ?? 'asc').toUpperCase(),
|
|
47
|
+
property: entry.property,
|
|
48
|
+
}));
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function applySetViewConfig(rawDefinition, definition, mutation = {}) {
|
|
52
|
+
const viewIndex = findViewIndex(definition, mutation.view ?? '');
|
|
53
|
+
if (viewIndex < 0) {
|
|
54
|
+
throw new Error('Base view not found');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const rawViews = ensureViews(rawDefinition);
|
|
58
|
+
const nextView = isPlainObject(rawViews[viewIndex])
|
|
59
|
+
? { ...rawViews[viewIndex] }
|
|
60
|
+
: {};
|
|
61
|
+
const nextConfig = isPlainObject(mutation.config) ? mutation.config : {};
|
|
62
|
+
|
|
63
|
+
if (Object.hasOwn(nextConfig, 'order')) {
|
|
64
|
+
nextView.order = Array.isArray(nextConfig.order)
|
|
65
|
+
? nextConfig.order.filter((entry) => typeof entry === 'string' && entry.trim())
|
|
66
|
+
: [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (Object.hasOwn(nextConfig, 'sort')) {
|
|
70
|
+
const sort = serializeSort(nextConfig.sort);
|
|
71
|
+
if (sort.length > 0) {
|
|
72
|
+
nextView.sort = sort;
|
|
73
|
+
} else {
|
|
74
|
+
delete nextView.sort;
|
|
75
|
+
delete nextView.sorts;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (Object.hasOwn(nextConfig, 'groupBy')) {
|
|
80
|
+
const groupBy = serializeGroupBy(nextConfig.groupBy);
|
|
81
|
+
if (groupBy) {
|
|
82
|
+
nextView.groupBy = groupBy;
|
|
83
|
+
} else {
|
|
84
|
+
delete nextView.groupBy;
|
|
85
|
+
delete nextView.group_by;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (Object.hasOwn(nextConfig, 'filters')) {
|
|
90
|
+
if (nextConfig.filters == null || nextConfig.filters === '') {
|
|
91
|
+
delete nextView.filters;
|
|
92
|
+
} else {
|
|
93
|
+
nextView.filters = nextConfig.filters;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
rawViews[viewIndex] = nextView;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export function transformBaseSource(source = '', mutation = {}) {
|
|
101
|
+
const definition = normalizeBaseDefinition(source);
|
|
102
|
+
const rawDefinition = normalizeRawDefinitionForWrite({
|
|
103
|
+
raw: cloneRawSource(source),
|
|
104
|
+
});
|
|
105
|
+
const mutationType = String(mutation?.type ?? '');
|
|
106
|
+
|
|
107
|
+
switch (mutationType) {
|
|
108
|
+
case 'set-view-config':
|
|
109
|
+
applySetViewConfig(rawDefinition, definition, mutation);
|
|
110
|
+
break;
|
|
111
|
+
default:
|
|
112
|
+
throw new Error(`Unsupported base mutation: ${mutationType || 'unknown'}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return `${yaml.dump(normalizeRawDefinitionForWrite(rawDefinition), { lineWidth: -1, noRefs: true }).trim()}\n`;
|
|
116
|
+
}
|
|
@@ -45,10 +45,39 @@ function closeSlowClient(ws, clientState, { maxBufferedAmountBytes, name }) {
|
|
|
45
45
|
return false;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
+
function isExpectedSocketSendError(error) {
|
|
49
|
+
const code = String(error?.code || '').toUpperCase();
|
|
50
|
+
const message = String(error?.message || '');
|
|
51
|
+
|
|
52
|
+
return code === 'EPIPE'
|
|
53
|
+
|| code === 'ECONNRESET'
|
|
54
|
+
|| /EPIPE|ECONNRESET|WebSocket is not open|readyState/i.test(message);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function cleanupDisconnectedClient(room, ws, clientState) {
|
|
58
|
+
if (clientState?.transportCloseIssued) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (clientState) {
|
|
63
|
+
clientState.transportCloseIssued = true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
room?.removeClient?.(ws);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
ws.terminate?.();
|
|
70
|
+
} catch {
|
|
71
|
+
// Ignore termination errors while cleaning up disconnected clients.
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
48
77
|
function sendMessage(ws, payload, { maxBufferedAmountBytes, name }) {
|
|
49
78
|
const clientState = this?.getClientState?.(ws) ?? null;
|
|
50
79
|
if (ws.readyState !== ws.OPEN) {
|
|
51
|
-
return
|
|
80
|
+
return cleanupDisconnectedClient(this, ws, clientState);
|
|
52
81
|
}
|
|
53
82
|
|
|
54
83
|
const bufferedAmountBeforeSend = ws.bufferedAmount;
|
|
@@ -59,6 +88,11 @@ function sendMessage(ws, payload, { maxBufferedAmountBytes, name }) {
|
|
|
59
88
|
|
|
60
89
|
ws.send(payload, (error) => {
|
|
61
90
|
if (error) {
|
|
91
|
+
if (isExpectedSocketSendError(error)) {
|
|
92
|
+
cleanupDisconnectedClient(this, ws, clientState);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
62
96
|
console.error(`[room:${name}] Failed to send websocket frame:`, error.message);
|
|
63
97
|
}
|
|
64
98
|
});
|
|
@@ -343,6 +377,7 @@ export class CollaborationRoom {
|
|
|
343
377
|
const existingState = this.getClientState(ws);
|
|
344
378
|
if (existingState) {
|
|
345
379
|
existingState.backpressureCloseIssued = false;
|
|
380
|
+
existingState.transportCloseIssued = false;
|
|
346
381
|
return existingState;
|
|
347
382
|
}
|
|
348
383
|
|
|
@@ -46,10 +46,10 @@ export class RoomPersistenceController {
|
|
|
46
46
|
return;
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
const generation = ++this.shutdownGeneration;
|
|
50
49
|
if (this.finalizePromise) {
|
|
51
50
|
return;
|
|
52
51
|
}
|
|
52
|
+
const generation = ++this.shutdownGeneration;
|
|
53
53
|
|
|
54
54
|
this.finalizePromise = (async () => {
|
|
55
55
|
clearTimeout(this.persistTimer);
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import archiver from 'archiver';
|
|
1
2
|
import { basename } from 'node:path';
|
|
2
3
|
|
|
3
4
|
import {
|
|
@@ -9,7 +10,7 @@ import {
|
|
|
9
10
|
isPlantUmlFilePath,
|
|
10
11
|
} from '../../../domain/file-kind.js';
|
|
11
12
|
import { parseJsonBody } from './request-body.js';
|
|
12
|
-
import { jsonResponse, sendResponse } from './http-response.js';
|
|
13
|
+
import { jsonResponse, sendResponse, sendStreamResponse } from './http-response.js';
|
|
13
14
|
|
|
14
15
|
const SVG_MIME_TYPE = 'image/svg+xml';
|
|
15
16
|
const SVG_ATTACHMENT_CSP = "default-src 'none'; img-src 'self' data: blob:; style-src 'unsafe-inline'; sandbox";
|
|
@@ -43,6 +44,15 @@ function createAttachmentHeaders(attachment) {
|
|
|
43
44
|
return headers;
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
function createDownloadHeaders(fileName, contentType) {
|
|
48
|
+
return {
|
|
49
|
+
'Cache-Control': 'no-store',
|
|
50
|
+
'Content-Disposition': `attachment; filename="${createSafeAsciiFilename(fileName)}"; filename*=UTF-8''${encodeContentDispositionFilename(fileName)}`,
|
|
51
|
+
'Content-Type': contentType,
|
|
52
|
+
'X-Content-Type-Options': 'nosniff',
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
46
56
|
function selectReadOperation(vaultFileStore, filePath) {
|
|
47
57
|
if (isExcalidrawFilePath(filePath)) {
|
|
48
58
|
return vaultFileStore.readExcalidrawFile(filePath);
|
|
@@ -67,6 +77,46 @@ function selectReadOperation(vaultFileStore, filePath) {
|
|
|
67
77
|
return vaultFileStore.readMarkdownFile(filePath);
|
|
68
78
|
}
|
|
69
79
|
|
|
80
|
+
async function streamDirectoryArchive(req, res, {
|
|
81
|
+
entries = [],
|
|
82
|
+
rootName = 'archive',
|
|
83
|
+
} = {}) {
|
|
84
|
+
const archive = archiver('zip', {
|
|
85
|
+
zlib: {
|
|
86
|
+
level: 6,
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
archive.on('warning', (error) => {
|
|
91
|
+
if (error?.code !== 'ENOENT') {
|
|
92
|
+
archive.emit('error', error);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
const responsePromise = sendStreamResponse(req, res, {
|
|
97
|
+
headers: createDownloadHeaders(`${rootName}.zip`, 'application/zip'),
|
|
98
|
+
statusCode: 200,
|
|
99
|
+
stream: archive,
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (entry.type === 'directory') {
|
|
104
|
+
const directoryName = entry.path
|
|
105
|
+
? `${rootName}/${entry.path}/`
|
|
106
|
+
: `${rootName}/`;
|
|
107
|
+
archive.append('', { name: directoryName });
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
archive.file(entry.absolutePath, {
|
|
112
|
+
name: `${rootName}/${entry.path}`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await archive.finalize();
|
|
117
|
+
await responsePromise;
|
|
118
|
+
}
|
|
119
|
+
|
|
70
120
|
export function createVaultApiQueryHandler({
|
|
71
121
|
baseQueryService = null,
|
|
72
122
|
backlinkIndex,
|
|
@@ -99,6 +149,57 @@ export function createVaultApiQueryHandler({
|
|
|
99
149
|
return true;
|
|
100
150
|
}
|
|
101
151
|
|
|
152
|
+
if (requestUrl.pathname === '/api/base/property-values' && req.method === 'POST') {
|
|
153
|
+
try {
|
|
154
|
+
if (!baseQueryService?.propertyValues) {
|
|
155
|
+
jsonResponse(req, res, 503, { error: 'Bases query service is unavailable' });
|
|
156
|
+
return true;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const body = await parseJsonBody(req);
|
|
160
|
+
const result = await baseQueryService.propertyValues({
|
|
161
|
+
activeFilePath: body?.activeFilePath ?? '',
|
|
162
|
+
basePath: body?.path ?? '',
|
|
163
|
+
propertyId: body?.propertyId ?? '',
|
|
164
|
+
query: body?.query ?? '',
|
|
165
|
+
source: typeof body?.source === 'string' ? body.source : null,
|
|
166
|
+
sourcePath: body?.sourcePath ?? '',
|
|
167
|
+
view: body?.view ?? '',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
jsonResponse(req, res, 200, { ok: true, result });
|
|
171
|
+
} catch (error) {
|
|
172
|
+
console.error('[api] Failed to read base property values:', error.message);
|
|
173
|
+
jsonResponse(req, res, 400, { error: error.message || 'Failed to read base property values' });
|
|
174
|
+
}
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (requestUrl.pathname === '/api/base/transform' && req.method === 'POST') {
|
|
179
|
+
try {
|
|
180
|
+
if (!baseQueryService?.transform) {
|
|
181
|
+
jsonResponse(req, res, 503, { error: 'Bases query service is unavailable' });
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const body = await parseJsonBody(req);
|
|
186
|
+
const result = await baseQueryService.transform({
|
|
187
|
+
activeFilePath: body?.activeFilePath ?? '',
|
|
188
|
+
basePath: body?.path ?? '',
|
|
189
|
+
mutation: body?.mutation ?? null,
|
|
190
|
+
source: typeof body?.source === 'string' ? body.source : null,
|
|
191
|
+
sourcePath: body?.sourcePath ?? '',
|
|
192
|
+
view: body?.view ?? '',
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
jsonResponse(req, res, 200, { ok: true, result });
|
|
196
|
+
} catch (error) {
|
|
197
|
+
console.error('[api] Failed to transform base:', error.message);
|
|
198
|
+
jsonResponse(req, res, 400, { error: error.message || 'Failed to transform base' });
|
|
199
|
+
}
|
|
200
|
+
return true;
|
|
201
|
+
}
|
|
202
|
+
|
|
102
203
|
if (requestUrl.pathname === '/api/base/export' && req.method === 'POST') {
|
|
103
204
|
try {
|
|
104
205
|
if (!baseQueryService?.query) {
|
|
@@ -110,6 +211,7 @@ export function createVaultApiQueryHandler({
|
|
|
110
211
|
const result = await baseQueryService.query({
|
|
111
212
|
activeFilePath: body?.activeFilePath ?? '',
|
|
112
213
|
basePath: body?.path ?? '',
|
|
214
|
+
includeCsv: true,
|
|
113
215
|
search: body?.search ?? '',
|
|
114
216
|
source: typeof body?.source === 'string' ? body.source : null,
|
|
115
217
|
sourcePath: body?.sourcePath ?? '',
|
|
@@ -166,6 +268,64 @@ export function createVaultApiQueryHandler({
|
|
|
166
268
|
return true;
|
|
167
269
|
}
|
|
168
270
|
|
|
271
|
+
if (requestUrl.pathname === '/api/download/file' && req.method === 'GET') {
|
|
272
|
+
const filePath = requestUrl.searchParams.get('path');
|
|
273
|
+
if (!filePath) {
|
|
274
|
+
jsonResponse(req, res, 400, { error: 'Missing path parameter' });
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
try {
|
|
279
|
+
const download = await vaultFileStore.readDownloadFile(filePath);
|
|
280
|
+
if (!download) {
|
|
281
|
+
jsonResponse(req, res, 404, { error: 'File not found' });
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
sendResponse(req, res, {
|
|
286
|
+
body: download.content,
|
|
287
|
+
headers: createDownloadHeaders(
|
|
288
|
+
basename(String(download.path ?? 'download')),
|
|
289
|
+
download.mimeType || 'application/octet-stream',
|
|
290
|
+
),
|
|
291
|
+
statusCode: 200,
|
|
292
|
+
});
|
|
293
|
+
} catch (error) {
|
|
294
|
+
console.error('[api] Failed to download file:', error.message);
|
|
295
|
+
jsonResponse(req, res, 500, { error: 'Failed to download file' });
|
|
296
|
+
}
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
if (requestUrl.pathname === '/api/download/directory' && req.method === 'GET') {
|
|
301
|
+
const directoryPath = requestUrl.searchParams.get('path');
|
|
302
|
+
if (!directoryPath) {
|
|
303
|
+
jsonResponse(req, res, 400, { error: 'Missing path parameter' });
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
try {
|
|
308
|
+
const result = await vaultFileStore.listDirectoryEntriesForDownload(directoryPath);
|
|
309
|
+
if (!result.ok) {
|
|
310
|
+
jsonResponse(req, res, result.error === 'Directory not found' ? 404 : 400, { error: result.error });
|
|
311
|
+
return true;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
await streamDirectoryArchive(req, res, {
|
|
315
|
+
entries: result.entries,
|
|
316
|
+
rootName: result.rootName || 'archive',
|
|
317
|
+
});
|
|
318
|
+
} catch (error) {
|
|
319
|
+
console.error('[api] Failed to download directory:', error.message);
|
|
320
|
+
if (!res.headersSent) {
|
|
321
|
+
jsonResponse(req, res, 500, { error: 'Failed to download directory' });
|
|
322
|
+
} else {
|
|
323
|
+
res.destroy(error);
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
|
|
169
329
|
if (requestUrl.pathname === '/api/attachment' && req.method === 'GET') {
|
|
170
330
|
const filePath = requestUrl.searchParams.get('path');
|
|
171
331
|
if (!filePath) {
|
|
@@ -179,6 +179,62 @@ export function sendResponse(req, res, {
|
|
|
179
179
|
});
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
export function sendStreamResponse(req, res, {
|
|
183
|
+
headers = {},
|
|
184
|
+
statusCode = 200,
|
|
185
|
+
stream,
|
|
186
|
+
} = {}) {
|
|
187
|
+
writeResponseHead(res, statusCode, headers, null);
|
|
188
|
+
|
|
189
|
+
if (!stream) {
|
|
190
|
+
res.end();
|
|
191
|
+
return Promise.resolve();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (req.method === 'HEAD' || statusCode === 204 || statusCode === 304) {
|
|
195
|
+
stream.destroy?.();
|
|
196
|
+
res.end();
|
|
197
|
+
return Promise.resolve();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return new Promise((resolve, reject) => {
|
|
201
|
+
let settled = false;
|
|
202
|
+
|
|
203
|
+
const finish = (error = null) => {
|
|
204
|
+
if (settled) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
settled = true;
|
|
209
|
+
res.off('finish', handleFinish);
|
|
210
|
+
res.off('error', handleError);
|
|
211
|
+
res.off('close', handleClose);
|
|
212
|
+
stream.off('error', handleError);
|
|
213
|
+
|
|
214
|
+
if (error) {
|
|
215
|
+
reject(error);
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
resolve();
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
const handleFinish = () => finish();
|
|
223
|
+
const handleError = (error) => finish(error);
|
|
224
|
+
const handleClose = () => {
|
|
225
|
+
if (!res.writableEnded) {
|
|
226
|
+
finish(new Error('Response stream closed before completion'));
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
res.on('finish', handleFinish);
|
|
231
|
+
res.on('error', handleError);
|
|
232
|
+
res.on('close', handleClose);
|
|
233
|
+
stream.on('error', handleError);
|
|
234
|
+
stream.pipe(res);
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
|
|
182
238
|
export function jsonResponse(req, res, statusCode, data) {
|
|
183
239
|
const body = JSON.stringify(data);
|
|
184
240
|
sendResponse(req, res, {
|
|
@@ -2,6 +2,7 @@ import { mkdir, readFile, readdir, rename, rm, rmdir, stat, writeFile } from 'fs
|
|
|
2
2
|
import { basename, dirname, extname, join, relative, resolve } from 'path';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
getVaultFileKind,
|
|
5
6
|
getVaultTreeNodeType,
|
|
6
7
|
isImageAttachmentFilePath,
|
|
7
8
|
isMarkdownFilePath,
|
|
@@ -37,6 +38,14 @@ const MIME_TYPE_TO_IMAGE_EXTENSION = Object.freeze({
|
|
|
37
38
|
'image/svg+xml': '.svg',
|
|
38
39
|
'image/webp': '.webp',
|
|
39
40
|
});
|
|
41
|
+
const TEXT_FILE_MIME_TYPES = Object.freeze({
|
|
42
|
+
base: 'text/yaml; charset=utf-8',
|
|
43
|
+
drawio: 'application/xml; charset=utf-8',
|
|
44
|
+
excalidraw: 'application/json; charset=utf-8',
|
|
45
|
+
markdown: 'text/markdown; charset=utf-8',
|
|
46
|
+
mermaid: 'text/plain; charset=utf-8',
|
|
47
|
+
plantuml: 'text/plain; charset=utf-8',
|
|
48
|
+
});
|
|
40
49
|
const WORKSPACE_SCAN_CONCURRENCY = 8;
|
|
41
50
|
|
|
42
51
|
function createTransactionalPath(targetPath, label) {
|
|
@@ -201,6 +210,15 @@ function createAttachmentMarkdownSnippet({ altText, documentPath, storedPath })
|
|
|
201
210
|
return ``;
|
|
202
211
|
}
|
|
203
212
|
|
|
213
|
+
function getDownloadMimeType(filePath) {
|
|
214
|
+
const fileKind = getVaultFileKind(filePath);
|
|
215
|
+
if (fileKind === 'image') {
|
|
216
|
+
return IMAGE_EXTENSION_TO_MIME_TYPE[extname(String(filePath ?? '')).toLowerCase()] || 'application/octet-stream';
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return TEXT_FILE_MIME_TYPES[fileKind] || 'application/octet-stream';
|
|
220
|
+
}
|
|
221
|
+
|
|
204
222
|
export class VaultFileStore {
|
|
205
223
|
constructor({ vaultDir }) {
|
|
206
224
|
this.vaultDir = resolve(vaultDir);
|
|
@@ -463,6 +481,36 @@ export class VaultFileStore {
|
|
|
463
481
|
};
|
|
464
482
|
}
|
|
465
483
|
|
|
484
|
+
async readDownloadFile(filePath) {
|
|
485
|
+
const normalizedPath = String(filePath ?? '').replace(/\\/g, '/').trim();
|
|
486
|
+
if (!normalizedPath || !isVaultFilePath(normalizedPath)) {
|
|
487
|
+
return null;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (isImageAttachmentFilePath(normalizedPath)) {
|
|
491
|
+
return this.readImageAttachmentFile(normalizedPath);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
const absolute = this.resolveContentPath(normalizedPath, { requireVaultFile: false });
|
|
495
|
+
if (!absolute) {
|
|
496
|
+
return null;
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
try {
|
|
500
|
+
return {
|
|
501
|
+
content: await readFile(absolute),
|
|
502
|
+
mimeType: getDownloadMimeType(normalizedPath),
|
|
503
|
+
path: normalizedPath,
|
|
504
|
+
};
|
|
505
|
+
} catch (error) {
|
|
506
|
+
if (error.code === 'ENOENT') {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
throw error;
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
466
514
|
async persistCollaborationState(filePath, {
|
|
467
515
|
commentThreads = [],
|
|
468
516
|
content = '',
|
|
@@ -660,6 +708,68 @@ export class VaultFileStore {
|
|
|
660
708
|
return sortWorkspacePaths(paths);
|
|
661
709
|
}
|
|
662
710
|
|
|
711
|
+
async listDirectoryEntriesForDownload(dirPath) {
|
|
712
|
+
const normalizedPath = String(dirPath ?? '').replace(/\\/g, '/').trim();
|
|
713
|
+
const { absolute, error } = resolveVaultDirectoryPath(this.vaultDir, normalizedPath);
|
|
714
|
+
if (!absolute) {
|
|
715
|
+
return { ok: false, error };
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
try {
|
|
719
|
+
const info = await stat(absolute);
|
|
720
|
+
if (!info.isDirectory()) {
|
|
721
|
+
return { ok: false, error: 'Directory not found' };
|
|
722
|
+
}
|
|
723
|
+
} catch (statError) {
|
|
724
|
+
if (statError.code === 'ENOENT') {
|
|
725
|
+
return { ok: false, error: 'Directory not found' };
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
throw statError;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
const entries = [];
|
|
732
|
+
const visitDirectory = async (directoryAbsolutePath, relativeDirectoryPath = '') => {
|
|
733
|
+
const dirEntries = sortDirectoryEntries(await readdir(directoryAbsolutePath, { withFileTypes: true }))
|
|
734
|
+
.filter((entry) => !isIgnoredVaultEntry(entry.name));
|
|
735
|
+
|
|
736
|
+
if (dirEntries.length === 0) {
|
|
737
|
+
entries.push({
|
|
738
|
+
path: relativeDirectoryPath,
|
|
739
|
+
type: 'directory',
|
|
740
|
+
});
|
|
741
|
+
return;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
for (const entry of dirEntries) {
|
|
745
|
+
const childAbsolutePath = join(directoryAbsolutePath, entry.name);
|
|
746
|
+
const childRelativePath = relativeDirectoryPath
|
|
747
|
+
? `${relativeDirectoryPath}/${entry.name}`
|
|
748
|
+
: entry.name;
|
|
749
|
+
|
|
750
|
+
if (entry.isDirectory()) {
|
|
751
|
+
await visitDirectory(childAbsolutePath, childRelativePath);
|
|
752
|
+
continue;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
if (isVaultFilePath(entry.name)) {
|
|
756
|
+
entries.push({
|
|
757
|
+
absolutePath: childAbsolutePath,
|
|
758
|
+
path: childRelativePath,
|
|
759
|
+
type: 'file',
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
};
|
|
764
|
+
|
|
765
|
+
await visitDirectory(absolute);
|
|
766
|
+
return {
|
|
767
|
+
entries,
|
|
768
|
+
ok: true,
|
|
769
|
+
rootName: basename(normalizedPath),
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
|
|
663
773
|
async deleteFile(filePath) {
|
|
664
774
|
const { absolute, error } = resolveVaultFilePath(this.vaultDir, filePath);
|
|
665
775
|
if (!absolute) {
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
const __vite__mapDeps=(i,m=__vite__mapDeps,d=(m.f||(m.f=["./drawio-editor-B4sHHwWJ.js","./yjs-provider-reset-guard-CtERLBID.js","./runtime-config-CqhchMC4.js","./vault-api-client-BzHQseMN.js","./vault-api-client-De7kMvrE.css","./drawio-room-DEivFyNV.js","./drawio-editor-UQQ8XHou.css","./embedded-editor-base-DgmRcsR8.css"])))=>i.map(i=>d[i]);
|
|
2
|
-
import{t as e}from"./runtime-config-loader-_D2KZKn-.js";import{t}from"./preload-helper-Bo9GmnuQ.js";await e(),await t(()=>import(`./drawio-editor-B4sHHwWJ.js`),__vite__mapDeps([0,1,2,3,4,5,6,7]),import.meta.url);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
:root{--text-xs:clamp(.75rem, .7rem + .25vw, .875rem);--text-sm:clamp(.875rem, .8rem + .35vw, 1rem);--text-base:clamp(1rem, .9rem + .5vw, 1.125rem);--text-lg:clamp(1.125rem, .95rem + .85vw, 1.5rem);--text-xl:clamp(1.5rem, 1rem + 1.5vw, 2.25rem);--text-2xl:clamp(2rem, 1.2rem + 2.5vw, 3.5rem);--text-label:12px;--text-meta:11px;--text-caption:10px;--text-micro:9px;--text-code-sm:11.5px;--text-ui-sm:13px;--text-ui-md:14px;--text-title-sm:15px;--text-title-md:18px;--font-body:"Inter", system-ui, sans-serif;--font-display:"Geist", "Cal Sans", "Inter", system-ui, sans-serif;--font-mono:"JetBrains Mono", "Fira Code", monospace;--space-1:.25rem;--space-2:.5rem;--space-3:.75rem;--space-4:1rem;--space-5:1.25rem;--space-6:1.5rem;--space-8:2rem;--space-10:2.5rem;--space-12:3rem;--space-16:4rem;--space-20:5rem;--space-24:6rem;--space-32:8rem;--radius-sm:.375rem;--radius-md:.5rem;--radius-lg:.75rem;--radius-xl:1rem;--radius-2xs:2px;--radius-xs:4px;--radius-lg-plus:10px;--radius-xl-plus:18px;--radius-full:9999px;--transition-interactive:.18s cubic-bezier(.16, 1, .3, 1);--content-narrow:640px;--content-default:960px;--content-wide:1200px;--preview-content-width:720px;--panel-comments-width:min(300px, 34vw);--panel-comments-min-width:240px;--panel-outline-width:220px;--toolbar-h:48px;--control-h-xs:28px;--control-h-sm:30px;--control-h-md:36px;--control-h-lg:44px;--control-icon-sm:14px;--control-icon-md:16px;--panel-border-width:1px}:root,[data-theme=light]{--color-bg:#f5f5f7;--color-surface:#fff;--color-surface-2:#fafafe;--color-surface-offset:#eeeef2;--color-surface-offset-2:#e6e6eb;--color-surface-dynamic:#dddde3;--color-surface-raised:#fff;--color-surface-ink:#0f1117;--color-divider:#d1d1d8;--color-border:#c4c4ce;--color-text:#1a1a2e;--color-text-muted:#6b6b80;--color-text-faint:#a0a0b0;--color-text-inverse:#f5f5f7;--color-primary:#4f46e5;--color-primary-hover:#4338ca;--color-primary-active:#3730a3;--color-primary-highlight:#e0e0ff;--color-comment:#4f46e5;--color-comment-soft:#4f46e514;--color-comment-strong:#4f46e524;--color-comment-text:#3730a3;--color-error:#dc2626;--color-error-highlight:#fce8e8;--color-success:#16a34a;--color-success-highlight:#dcfce7;--color-warning:#d97706;--color-warning-highlight:#fef3c7;--diff-add-bg:#16a34a0f;--diff-add-line-bg:#16a34a24;--diff-add-text:#16a34a;--diff-del-bg:#dc26260f;--diff-del-line-bg:#dc262624;--diff-del-text:#dc2626;--diff-hunk-bg:#4f46e514;--diff-hunk-text:#4f46e5;--diff-gutter:#a0a0b0;--surface-fill-subtle:color-mix(in oklab, var(--color-surface) 92%, var(--color-surface-offset) 8%);--surface-fill-strong:color-mix(in oklab, var(--color-surface-offset) 72%, var(--color-surface-offset-2) 28%);--surface-overlay:color-mix(in srgb, var(--color-surface) 84%, var(--color-surface-offset) 16%);--surface-overlay-soft:color-mix(in srgb, var(--color-surface) 88%, var(--color-surface-offset) 12%);--surface-overlay-strong:color-mix(in srgb, var(--color-surface) 90%, var(--color-surface-offset) 10%);--surface-overlay-raised:color-mix(in oklab, var(--color-surface) 94%, transparent);--surface-overlay-header:color-mix(in srgb, var(--color-surface-offset) 76%, var(--color-bg) 24%);--surface-overlay-offset:color-mix(in oklab, var(--color-surface-offset) 82%, transparent);--surface-scrim:oklch(10% 0 0/.42);--surface-scrim-strong:oklch(8% .01 260/.82);--surface-inverse-glass:oklch(18% .02 260/.68);--surface-inverse-glass-hover:oklch(24% .02 260/.82);--surface-media-canvas:#000;--surface-embed-canvas:#fff;--surface-avatar-muted:#ffffff38;--surface-avatar-label:#0f1117d6;--surface-accent-subtle:color-mix(in oklab, var(--color-primary) 8%, var(--color-surface));--surface-accent-soft:color-mix(in oklab, var(--color-primary) 16%, transparent);--surface-accent-strong:color-mix(in oklab, var(--color-primary) 22%, transparent);--surface-success-soft:color-mix(in oklab, var(--color-success) 14%, transparent);--surface-warning-soft:color-mix(in oklab, var(--color-warning) 14%, transparent);--surface-danger-soft:color-mix(in oklab, var(--color-error) 14%, transparent);--surface-comment-shell:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .035), transparent 120px), color-mix(in srgb, var(--color-surface) 96%, var(--color-comment) 4%);--surface-comment-shell-header:color-mix(in srgb, var(--color-surface) 97%, var(--color-comment) 3%);--surface-comment-record:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .028), transparent 70%), color-mix(in srgb, var(--color-surface-2) 95%, var(--color-comment) 5%);--surface-comment-record-hover:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .042), transparent 70%), color-mix(in srgb, var(--color-surface-2) 92%, var(--color-comment) 8%);--surface-comment-record-active:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .055), transparent 72%), color-mix(in srgb, var(--color-surface-2) 90%, var(--color-comment) 10%);--surface-comment-subtle:color-mix(in srgb, var(--color-surface-2) 95%, var(--color-comment) 5%);--surface-comment-subtle-strong:color-mix(in srgb, var(--color-surface-2) 92%, var(--color-comment) 8%);--surface-comment-card:color-mix(in srgb, var(--color-surface) 94%, var(--color-comment) 6%);--surface-comment-elevated:color-mix(in srgb, var(--color-surface) 96%, var(--color-comment) 4%);--surface-comment-offset:color-mix(in srgb, var(--color-surface-offset) 90%, var(--color-comment) 10%);--surface-comment-offset-soft:color-mix(in srgb, var(--color-surface-offset) 88%, var(--color-comment) 12%);--surface-comment-offset-strong:color-mix(in srgb, var(--color-surface-offset) 84%, var(--color-comment) 16%);--surface-comment-offset-active:color-mix(in srgb, var(--color-surface-offset) 72%, var(--color-comment) 28%);--surface-comment-highlight-hover:color-mix(in srgb, var(--color-comment-soft) 74%, var(--color-comment) 26%);--surface-comment-highlight-active:color-mix(in srgb, var(--color-comment-soft) 62%, var(--color-comment) 38%);--surface-comment-selection:color-mix(in srgb, var(--color-surface) 25%, var(--color-comment) 75%);--surface-comment-preview-fade:linear-gradient(180deg, oklch(0% 0 0/0), color-mix(in srgb, var(--color-surface-2) 95%, var(--color-comment) 5%));--border-comment-soft:color-mix(in srgb, var(--color-divider) 90%, var(--color-comment) 10%);--border-comment-muted:color-mix(in srgb, var(--color-divider) 88%, var(--color-comment) 12%);--border-comment-strong:color-mix(in srgb, var(--color-border) 82%, var(--color-comment) 18%);--border-comment-active:color-mix(in srgb, var(--color-border) 78%, var(--color-comment) 22%);--shadow-comment-outline:0 0 0 1px oklch(from var(--color-comment) l c h / .14);--surface-warning-toast:linear-gradient(135deg, oklab(94.9166% -.0018052 .0644112), oklab(99.8777% .00026585 .000908241) 42%), #fff8e7;--border-warning-toast:oklab(73.4461% .062398 .147608);--text-warning-toast:#422006;--shadow-warning-toast:0 16px 30px #542e0a2e, 0 0 0 1px #f59e0b1f;--border-accent-soft:color-mix(in oklab, var(--color-primary) 22%, transparent);--border-accent-muted:color-mix(in oklab, var(--color-primary) 24%, var(--color-border));--border-accent-strong:color-mix(in oklab, var(--color-primary) 34%, var(--color-border));--border-accent-stronger:color-mix(in oklab, var(--color-primary) 40%, var(--color-border));--shadow-accent-inset:inset 0 0 0 1px color-mix(in oklab, var(--color-primary) 14%, transparent);--shadow-accent-outline:0 0 0 1px color-mix(in oklab, var(--color-primary) 16%, transparent);--shadow-accent-halo:0 0 0 3px oklch(from var(--color-primary) l c h / .16);--shadow-accent-ring:0 0 0 2px var(--color-surface), 0 0 0 4px oklch(from var(--color-primary) l c h / .18);--shadow-accent-ring-strong:0 0 0 2px var(--color-bg), 0 0 0 4px oklch(from var(--color-primary) l c h / .45);--shadow-accent-ring-solid:0 0 0 2px var(--color-primary), 0 0 0 4px oklch(from var(--color-primary) l c h / .2);--shadow-elevated-soft:0 10px 24px color-mix(in oklab, var(--color-surface-ink) 10%, transparent);--shadow-floating-soft:0 14px 34px oklch(0% 0 0/.18);--shadow-floating-hover:0 18px 40px oklch(0% 0 0/.2);--shadow-media-soft:0 16px 40px oklch(0% 0 0/.24);--shadow-media-stage:0 28px 80px oklch(0% 0 0/.28);--shadow-media-modal:0 10px 40px #00000059;--shadow-card-strong:0 20px 56px oklch(0% 0 0/.24);--shadow-sheet-inset:inset 0 1px 0 color-mix(in oklab, var(--color-text) 4%, transparent);--shadow-focus:0 0 0 3px oklch(from var(--color-primary) l c h / .15);--shadow-focus-compact:0 0 0 2px oklch(from var(--color-primary) l c h / .45);--interactive-border:color-mix(in oklab, var(--color-primary) 22%, transparent);--interactive-surface:color-mix(in oklab, var(--color-primary) 14%, transparent);--color-text-on-dark:#fff;--shadow-sm:0 1px 2px oklch(20% .02 260/.06);--shadow-md:0 4px 12px oklch(20% .02 260/.08);--shadow-lg:0 12px 32px oklch(20% .02 260/.12)}[data-theme=dark]{--color-bg:#0f1117;--color-surface:#161822;--color-surface-2:#1c1e2c;--color-surface-offset:#1a1c28;--color-surface-offset-2:#22243a;--color-surface-dynamic:#2a2c3e;--color-surface-raised:#202330;--color-surface-ink:#050608;--color-divider:#2e3044;--color-border:#383a50;--color-text:#e2e2ea;--color-text-muted:#8b8ba0;--color-text-faint:#5a5a70;--color-text-inverse:#0f1117;--color-primary:#818cf8;--color-primary-hover:#6366f1;--color-primary-active:#4f46e5;--color-primary-highlight:#1e1e3a;--color-comment:#818cf8;--color-comment-soft:#818cf81a;--color-comment-strong:#818cf829;--color-comment-text:#c7d2fe;--color-error:#f87171;--color-error-highlight:#2a1a1a;--color-success:#4ade80;--color-success-highlight:#1a2a1a;--color-warning:#fbbf24;--color-warning-highlight:#2a261a;--diff-add-bg:#4ade8014;--diff-add-line-bg:#4ade8026;--diff-add-text:#4ade80;--diff-del-bg:#f8717114;--diff-del-line-bg:#f8717126;--diff-del-text:#f87171;--diff-hunk-bg:#818cf81a;--diff-hunk-text:#818cf8;--diff-gutter:#5a5a70;--surface-fill-subtle:color-mix(in oklab, var(--color-surface) 88%, var(--color-surface-offset) 12%);--surface-fill-strong:color-mix(in oklab, var(--color-surface-offset) 60%, var(--color-surface-offset-2) 40%);--surface-overlay:color-mix(in srgb, var(--color-surface) 84%, var(--color-surface-offset) 16%);--surface-overlay-soft:color-mix(in srgb, var(--color-surface) 88%, var(--color-surface-offset) 12%);--surface-overlay-strong:color-mix(in srgb, var(--color-surface) 90%, var(--color-surface-offset) 10%);--surface-overlay-raised:color-mix(in oklab, var(--color-surface) 94%, transparent);--surface-overlay-header:color-mix(in srgb, var(--color-surface-offset) 76%, var(--color-bg) 24%);--surface-overlay-offset:color-mix(in oklab, var(--color-surface-offset) 82%, transparent);--surface-scrim:oklch(10% 0 0/.42);--surface-scrim-strong:oklch(8% .01 260/.82);--surface-inverse-glass:oklch(18% .02 260/.68);--surface-inverse-glass-hover:oklch(24% .02 260/.82);--surface-media-canvas:#000;--surface-embed-canvas:#121212;--surface-avatar-muted:#ffffff38;--surface-avatar-label:#0f1117d6;--surface-accent-subtle:color-mix(in oklab, var(--color-primary) 8%, var(--color-surface));--surface-accent-soft:color-mix(in oklab, var(--color-primary) 16%, transparent);--surface-accent-strong:color-mix(in oklab, var(--color-primary) 22%, transparent);--surface-success-soft:color-mix(in oklab, var(--color-success) 14%, transparent);--surface-warning-soft:color-mix(in oklab, var(--color-warning) 14%, transparent);--surface-danger-soft:color-mix(in oklab, var(--color-error) 14%, transparent);--surface-comment-shell:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .035), transparent 120px), color-mix(in srgb, var(--color-surface) 96%, var(--color-comment) 4%);--surface-comment-shell-header:color-mix(in srgb, var(--color-surface) 97%, var(--color-comment) 3%);--surface-comment-record:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .028), transparent 70%), color-mix(in srgb, var(--color-surface-2) 95%, var(--color-comment) 5%);--surface-comment-record-hover:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .042), transparent 70%), color-mix(in srgb, var(--color-surface-2) 92%, var(--color-comment) 8%);--surface-comment-record-active:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .055), transparent 72%), color-mix(in srgb, var(--color-surface-2) 90%, var(--color-comment) 10%);--surface-comment-subtle:color-mix(in srgb, var(--color-surface-2) 95%, var(--color-comment) 5%);--surface-comment-subtle-strong:color-mix(in srgb, var(--color-surface-2) 92%, var(--color-comment) 8%);--surface-comment-card:color-mix(in srgb, var(--color-surface) 94%, var(--color-comment) 6%);--surface-comment-elevated:color-mix(in srgb, var(--color-surface) 96%, var(--color-comment) 4%);--surface-comment-offset:color-mix(in srgb, var(--color-surface-offset) 90%, var(--color-comment) 10%);--surface-comment-offset-soft:color-mix(in srgb, var(--color-surface-offset) 88%, var(--color-comment) 12%);--surface-comment-offset-strong:color-mix(in srgb, var(--color-surface-offset) 84%, var(--color-comment) 16%);--surface-comment-offset-active:color-mix(in srgb, var(--color-surface-offset) 72%, var(--color-comment) 28%);--surface-comment-highlight-hover:color-mix(in srgb, var(--color-comment-soft) 74%, var(--color-comment) 26%);--surface-comment-highlight-active:color-mix(in srgb, var(--color-comment-soft) 62%, var(--color-comment) 38%);--surface-comment-selection:color-mix(in srgb, var(--color-surface) 25%, var(--color-comment) 75%);--surface-comment-preview-fade:linear-gradient(180deg, oklch(0% 0 0/0), color-mix(in srgb, var(--color-surface-2) 95%, var(--color-comment) 5%));--border-comment-soft:color-mix(in srgb, var(--color-divider) 90%, var(--color-comment) 10%);--border-comment-muted:color-mix(in srgb, var(--color-divider) 88%, var(--color-comment) 12%);--border-comment-strong:color-mix(in srgb, var(--color-border) 82%, var(--color-comment) 18%);--border-comment-active:color-mix(in srgb, var(--color-border) 78%, var(--color-comment) 22%);--shadow-comment-outline:0 0 0 1px oklch(from var(--color-comment) l c h / .14);--surface-warning-toast:linear-gradient(135deg, oklab(76.8591% .0560995 .154808/.32), color-mix(in oklab, var(--color-surface) 86%, #0b1022 14%) 42%), color-mix(in oklab, var(--color-surface) 74%, #1b1320 26%);--border-warning-toast:oklab(79.4887% .0497246 .137216/.88);--text-warning-toast:oklab(99.6952% -.000454402 .00461993);--shadow-warning-toast:0 18px 34px #00000061, 0 0 0 1px oklab(76.8591% .0560995 .154808/.18);--border-accent-soft:color-mix(in oklab, var(--color-primary) 22%, transparent);--border-accent-muted:color-mix(in oklab, var(--color-primary) 24%, var(--color-border));--border-accent-strong:color-mix(in oklab, var(--color-primary) 34%, var(--color-border));--border-accent-stronger:color-mix(in oklab, var(--color-primary) 40%, var(--color-border));--shadow-accent-inset:inset 0 0 0 1px color-mix(in oklab, var(--color-primary) 14%, transparent);--shadow-accent-outline:0 0 0 1px color-mix(in oklab, var(--color-primary) 16%, transparent);--shadow-accent-halo:0 0 0 3px oklch(from var(--color-primary) l c h / .16);--shadow-accent-ring:0 0 0 2px var(--color-surface), 0 0 0 4px oklch(from var(--color-primary) l c h / .18);--shadow-accent-ring-strong:0 0 0 2px var(--color-bg), 0 0 0 4px oklch(from var(--color-primary) l c h / .45);--shadow-accent-ring-solid:0 0 0 2px var(--color-primary), 0 0 0 4px oklch(from var(--color-primary) l c h / .2);--shadow-elevated-soft:0 10px 24px color-mix(in oklab, var(--color-surface-ink) 10%, transparent);--shadow-floating-soft:0 14px 34px oklch(0% 0 0/.18);--shadow-floating-hover:0 18px 40px oklch(0% 0 0/.2);--shadow-media-soft:0 16px 40px oklch(0% 0 0/.24);--shadow-media-stage:0 28px 80px oklch(0% 0 0/.28);--shadow-media-modal:0 16px 48px #0009;--shadow-card-strong:0 20px 56px oklch(0% 0 0/.24);--shadow-sheet-inset:inset 0 1px 0 color-mix(in oklab, var(--color-text) 4%, transparent);--shadow-focus:0 0 0 3px oklch(from var(--color-primary) l c h / .15);--shadow-focus-compact:0 0 0 2px oklch(from var(--color-primary) l c h / .45);--interactive-border:color-mix(in oklab, var(--color-primary) 32%, transparent);--interactive-surface:color-mix(in oklab, var(--color-primary) 18%, transparent);--color-text-on-dark:#fff;--shadow-sm:0 1px 2px oklch(0% 0 0/.25);--shadow-md:0 4px 12px oklch(0% 0 0/.35);--shadow-lg:0 12px 32px oklch(0% 0 0/.5)}@media (prefers-color-scheme:dark){:root:not([data-theme]){--color-bg:#0f1117;--color-surface:#161822;--color-surface-2:#1c1e2c;--color-surface-offset:#1a1c28;--color-surface-offset-2:#22243a;--color-surface-dynamic:#2a2c3e;--color-surface-raised:#202330;--color-surface-ink:#050608;--color-divider:#2e3044;--color-border:#383a50;--color-text:#e2e2ea;--color-text-muted:#8b8ba0;--color-text-faint:#5a5a70;--color-text-inverse:#0f1117;--color-primary:#818cf8;--color-primary-hover:#6366f1;--color-primary-active:#4f46e5;--color-primary-highlight:#1e1e3a;--color-comment:#818cf8;--color-comment-soft:#818cf81a;--color-comment-strong:#818cf829;--color-comment-text:#c7d2fe;--color-error:#f87171;--color-error-highlight:#2a1a1a;--color-success:#4ade80;--color-success-highlight:#1a2a1a;--color-warning:#fbbf24;--color-warning-highlight:#2a261a;--surface-fill-subtle:color-mix(in oklab, var(--color-surface) 88%, var(--color-surface-offset) 12%);--surface-fill-strong:color-mix(in oklab, var(--color-surface-offset) 60%, var(--color-surface-offset-2) 40%);--surface-overlay:color-mix(in srgb, var(--color-surface) 84%, var(--color-surface-offset) 16%);--surface-overlay-soft:color-mix(in srgb, var(--color-surface) 88%, var(--color-surface-offset) 12%);--surface-overlay-strong:color-mix(in srgb, var(--color-surface) 90%, var(--color-surface-offset) 10%);--surface-overlay-raised:color-mix(in oklab, var(--color-surface) 94%, transparent);--surface-overlay-header:color-mix(in srgb, var(--color-surface-offset) 76%, var(--color-bg) 24%);--surface-overlay-offset:color-mix(in oklab, var(--color-surface-offset) 82%, transparent);--surface-scrim:oklch(10% 0 0/.42);--surface-scrim-strong:oklch(8% .01 260/.82);--surface-inverse-glass:oklch(18% .02 260/.68);--surface-inverse-glass-hover:oklch(24% .02 260/.82);--surface-media-canvas:#000;--surface-embed-canvas:#121212;--surface-avatar-muted:#ffffff38;--surface-avatar-label:#0f1117d6;--surface-accent-subtle:color-mix(in oklab, var(--color-primary) 8%, var(--color-surface));--surface-accent-soft:color-mix(in oklab, var(--color-primary) 16%, transparent);--surface-accent-strong:color-mix(in oklab, var(--color-primary) 22%, transparent);--surface-success-soft:color-mix(in oklab, var(--color-success) 14%, transparent);--surface-warning-soft:color-mix(in oklab, var(--color-warning) 14%, transparent);--surface-danger-soft:color-mix(in oklab, var(--color-error) 14%, transparent);--surface-comment-shell:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .035), transparent 120px), color-mix(in srgb, var(--color-surface) 96%, var(--color-comment) 4%);--surface-comment-shell-header:color-mix(in srgb, var(--color-surface) 97%, var(--color-comment) 3%);--surface-comment-record:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .028), transparent 70%), color-mix(in srgb, var(--color-surface-2) 95%, var(--color-comment) 5%);--surface-comment-record-hover:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .042), transparent 70%), color-mix(in srgb, var(--color-surface-2) 92%, var(--color-comment) 8%);--surface-comment-record-active:linear-gradient(180deg, oklch(from var(--color-comment) l c h / .055), transparent 72%), color-mix(in srgb, var(--color-surface-2) 90%, var(--color-comment) 10%);--surface-comment-subtle:color-mix(in srgb, var(--color-surface-2) 95%, var(--color-comment) 5%);--surface-comment-subtle-strong:color-mix(in srgb, var(--color-surface-2) 92%, var(--color-comment) 8%);--surface-comment-card:color-mix(in srgb, var(--color-surface) 94%, var(--color-comment) 6%);--surface-comment-elevated:color-mix(in srgb, var(--color-surface) 96%, var(--color-comment) 4%);--surface-comment-offset:color-mix(in srgb, var(--color-surface-offset) 90%, var(--color-comment) 10%);--surface-comment-offset-soft:color-mix(in srgb, var(--color-surface-offset) 88%, var(--color-comment) 12%);--surface-comment-offset-strong:color-mix(in srgb, var(--color-surface-offset) 84%, var(--color-comment) 16%);--surface-comment-offset-active:color-mix(in srgb, var(--color-surface-offset) 72%, var(--color-comment) 28%);--surface-comment-highlight-hover:color-mix(in srgb, var(--color-comment-soft) 74%, var(--color-comment) 26%);--surface-comment-highlight-active:color-mix(in srgb, var(--color-comment-soft) 62%, var(--color-comment) 38%);--surface-comment-selection:color-mix(in srgb, var(--color-surface) 25%, var(--color-comment) 75%);--surface-comment-preview-fade:linear-gradient(180deg, oklch(0% 0 0/0), color-mix(in srgb, var(--color-surface-2) 95%, var(--color-comment) 5%));--border-comment-soft:color-mix(in srgb, var(--color-divider) 90%, var(--color-comment) 10%);--border-comment-muted:color-mix(in srgb, var(--color-divider) 88%, var(--color-comment) 12%);--border-comment-strong:color-mix(in srgb, var(--color-border) 82%, var(--color-comment) 18%);--border-comment-active:color-mix(in srgb, var(--color-border) 78%, var(--color-comment) 22%);--shadow-comment-outline:0 0 0 1px oklch(from var(--color-comment) l c h / .14);--surface-warning-toast:linear-gradient(135deg, oklab(76.8591% .0560995 .154808/.32), color-mix(in oklab, var(--color-surface) 86%, #0b1022 14%) 42%), color-mix(in oklab, var(--color-surface) 74%, #1b1320 26%);--border-warning-toast:oklab(79.4887% .0497246 .137216/.88);--text-warning-toast:oklab(99.6952% -.000454402 .00461993);--shadow-warning-toast:0 18px 34px #00000061, 0 0 0 1px oklab(76.8591% .0560995 .154808/.18);--border-accent-soft:color-mix(in oklab, var(--color-primary) 22%, transparent);--border-accent-muted:color-mix(in oklab, var(--color-primary) 24%, var(--color-border));--border-accent-strong:color-mix(in oklab, var(--color-primary) 34%, var(--color-border));--border-accent-stronger:color-mix(in oklab, var(--color-primary) 40%, var(--color-border));--shadow-accent-inset:inset 0 0 0 1px color-mix(in oklab, var(--color-primary) 14%, transparent);--shadow-accent-outline:0 0 0 1px color-mix(in oklab, var(--color-primary) 16%, transparent);--shadow-accent-halo:0 0 0 3px oklch(from var(--color-primary) l c h / .16);--shadow-accent-ring:0 0 0 2px var(--color-surface), 0 0 0 4px oklch(from var(--color-primary) l c h / .18);--shadow-accent-ring-strong:0 0 0 2px var(--color-bg), 0 0 0 4px oklch(from var(--color-primary) l c h / .45);--shadow-accent-ring-solid:0 0 0 2px var(--color-primary), 0 0 0 4px oklch(from var(--color-primary) l c h / .2);--shadow-elevated-soft:0 10px 24px color-mix(in oklab, var(--color-surface-ink) 10%, transparent);--shadow-floating-soft:0 14px 34px oklch(0% 0 0/.18);--shadow-floating-hover:0 18px 40px oklch(0% 0 0/.2);--shadow-media-soft:0 16px 40px oklch(0% 0 0/.24);--shadow-media-stage:0 28px 80px oklch(0% 0 0/.28);--shadow-media-modal:0 16px 48px #0009;--shadow-card-strong:0 20px 56px oklch(0% 0 0/.24);--shadow-sheet-inset:inset 0 1px 0 color-mix(in oklab, var(--color-text) 4%, transparent);--shadow-focus:0 0 0 3px oklch(from var(--color-primary) l c h / .15);--shadow-focus-compact:0 0 0 2px oklch(from var(--color-primary) l c h / .45);--interactive-border:color-mix(in oklab, var(--color-primary) 32%, transparent);--interactive-surface:color-mix(in oklab, var(--color-primary) 18%, transparent);--color-text-on-dark:#fff;--shadow-sm:0 1px 2px oklch(0% 0 0/.25);--shadow-md:0 4px 12px oklch(0% 0 0/.35);--shadow-lg:0 12px 32px oklch(0% 0 0/.5)}}*,:before,:after{box-sizing:border-box;margin:0;padding:0}html{-webkit-text-size-adjust:none;-moz-text-size-adjust:none;text-size-adjust:none;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-rendering:optimizelegibility;scroll-behavior:smooth;hanging-punctuation:first last;scroll-padding-top:var(--space-16);overscroll-behavior:none;height:100%;overflow:hidden}body{height:100%;min-height:100dvh;line-height:1.6;font-family:var(--font-body,sans-serif);font-size:var(--text-sm);color:var(--color-text);background-color:var(--color-bg);overflow:hidden}img,picture,video,canvas,svg{max-width:100%;height:auto;display:block}ul[role=list],ol[role=list]{list-style:none}input,button,textarea,select{font:inherit;color:inherit}h1,h2,h3,h4,h5,h6{font-family:var(--font-display,inherit);text-wrap:balance;line-height:1.15}p,li,figcaption{text-wrap:pretty;max-width:72ch}::selection{background:oklch(from var(--color-primary) l c h / .25);color:var(--color-text)}:focus-visible{outline:2px solid var(--color-primary);outline-offset:3px;border-radius:var(--radius-sm)}@media (prefers-reduced-motion:reduce){*,:before,:after{scroll-behavior:auto!important;transition-duration:.01ms!important;animation-duration:.01ms!important;animation-iteration-count:1!important}}button{cursor:pointer;background:0 0;border:none}table{border-collapse:collapse;width:100%}.sr-only{clip:rect(0, 0, 0, 0);white-space:nowrap;border-width:0;width:1px;height:1px;margin:-1px;padding:0;position:absolute;overflow:hidden}html,body{width:100%;height:100%;margin:0;overflow:hidden}body[data-surface]{font-family:var(--font-body);font-size:var(--text-ui-sm);color:var(--color-text);background:var(--color-bg)}#root{width:100%;height:100%}
|