@gmickel/gno 0.26.0 → 0.27.1
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/assets/skill/SKILL.md +5 -0
- package/package.json +1 -1
- package/src/cli/commands/get.ts +21 -0
- package/src/core/document-capabilities.ts +113 -0
- package/src/mcp/tools/get.ts +10 -0
- package/src/sdk/documents.ts +12 -0
- package/src/serve/doc-events.ts +69 -0
- package/src/serve/public/app.tsx +81 -24
- package/src/serve/public/components/CaptureModal.tsx +138 -3
- package/src/serve/public/components/QuickSwitcher.tsx +248 -0
- package/src/serve/public/components/ShortcutHelpModal.tsx +1 -0
- package/src/serve/public/components/ai-elements/code-block.tsx +74 -26
- package/src/serve/public/components/editor/CodeMirrorEditor.tsx +51 -0
- package/src/serve/public/components/ui/command.tsx +2 -2
- package/src/serve/public/hooks/use-doc-events.ts +34 -0
- package/src/serve/public/hooks/useCaptureModal.tsx +12 -3
- package/src/serve/public/hooks/useKeyboardShortcuts.ts +2 -2
- package/src/serve/public/lib/deep-links.ts +68 -0
- package/src/serve/public/lib/document-availability.ts +22 -0
- package/src/serve/public/lib/local-history.ts +44 -0
- package/src/serve/public/lib/wiki-link.ts +36 -0
- package/src/serve/public/pages/Browse.tsx +11 -0
- package/src/serve/public/pages/Dashboard.tsx +2 -2
- package/src/serve/public/pages/DocView.tsx +241 -18
- package/src/serve/public/pages/DocumentEditor.tsx +399 -9
- package/src/serve/public/pages/Search.tsx +20 -1
- package/src/serve/routes/api.ts +361 -29
- package/src/serve/server.ts +48 -1
- package/src/serve/watch-service.ts +149 -0
package/assets/skill/SKILL.md
CHANGED
|
@@ -110,10 +110,15 @@ gno get gno://work/report.md --from 100 -l 20
|
|
|
110
110
|
# With line numbers
|
|
111
111
|
gno get gno://work/report.md --line-numbers
|
|
112
112
|
|
|
113
|
+
# JSON output with capabilities metadata
|
|
114
|
+
gno get gno://work/report.md --json
|
|
115
|
+
|
|
113
116
|
# Multiple documents
|
|
114
117
|
gno multi-get gno://work/doc1.md gno://work/doc2.md
|
|
115
118
|
```
|
|
116
119
|
|
|
120
|
+
**Editable vs read-only**: `gno get --json` returns a `capabilities` field showing whether a document is editable at its source. Markdown and plain text files are editable in place. Converted documents (PDF, DOCX, XLSX) are read-only -- to edit their content, create a new markdown note instead of overwriting the binary source.
|
|
121
|
+
|
|
117
122
|
## Search Then Get (common pipeline)
|
|
118
123
|
|
|
119
124
|
```bash
|
package/package.json
CHANGED
package/src/cli/commands/get.ts
CHANGED
|
@@ -8,6 +8,10 @@
|
|
|
8
8
|
import type { DocumentRow, StorePort, StoreResult } from "../../store/types";
|
|
9
9
|
import type { ParsedRef } from "./ref-parser";
|
|
10
10
|
|
|
11
|
+
import {
|
|
12
|
+
getDocumentCapabilities,
|
|
13
|
+
type DocumentCapabilities,
|
|
14
|
+
} from "../../core/document-capabilities";
|
|
11
15
|
import { parseRef } from "./ref-parser";
|
|
12
16
|
import { initStore } from "./shared";
|
|
13
17
|
|
|
@@ -58,6 +62,7 @@ export interface GetResponse {
|
|
|
58
62
|
converterVersion?: string;
|
|
59
63
|
mirrorHash?: string;
|
|
60
64
|
};
|
|
65
|
+
capabilities: DocumentCapabilities;
|
|
61
66
|
}
|
|
62
67
|
|
|
63
68
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -203,6 +208,7 @@ function buildResponse(ctx: BuildResponseContext): GetResult {
|
|
|
203
208
|
language: doc.languageHint ?? undefined,
|
|
204
209
|
source: buildSourceMeta(doc, config),
|
|
205
210
|
conversion: buildConversionMeta(doc),
|
|
211
|
+
capabilities: buildCapabilities(doc),
|
|
206
212
|
},
|
|
207
213
|
};
|
|
208
214
|
}
|
|
@@ -233,6 +239,7 @@ function buildResponse(ctx: BuildResponseContext): GetResult {
|
|
|
233
239
|
language: doc.languageHint ?? undefined,
|
|
234
240
|
source: buildSourceMeta(doc, config),
|
|
235
241
|
conversion: buildConversionMeta(doc),
|
|
242
|
+
capabilities: buildCapabilities(doc),
|
|
236
243
|
},
|
|
237
244
|
};
|
|
238
245
|
}
|
|
@@ -247,6 +254,7 @@ interface DocRow {
|
|
|
247
254
|
sourceMime: string;
|
|
248
255
|
sourceExt: string;
|
|
249
256
|
sourceSize: number;
|
|
257
|
+
sourceMtime?: string;
|
|
250
258
|
sourceHash: string;
|
|
251
259
|
}
|
|
252
260
|
|
|
@@ -262,11 +270,24 @@ function buildSourceMeta(
|
|
|
262
270
|
relPath: doc.relPath,
|
|
263
271
|
mime: doc.sourceMime,
|
|
264
272
|
ext: doc.sourceExt,
|
|
273
|
+
modifiedAt: doc.sourceMtime ?? undefined,
|
|
265
274
|
sizeBytes: doc.sourceSize,
|
|
266
275
|
sourceHash: doc.sourceHash,
|
|
267
276
|
};
|
|
268
277
|
}
|
|
269
278
|
|
|
279
|
+
function buildCapabilities(doc: {
|
|
280
|
+
sourceExt: string;
|
|
281
|
+
sourceMime: string;
|
|
282
|
+
mirrorHash?: string | null;
|
|
283
|
+
}): DocumentCapabilities {
|
|
284
|
+
return getDocumentCapabilities({
|
|
285
|
+
sourceExt: doc.sourceExt,
|
|
286
|
+
sourceMime: doc.sourceMime,
|
|
287
|
+
contentAvailable: doc.mirrorHash !== null,
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
|
|
270
291
|
interface ConversionDoc {
|
|
271
292
|
converterId?: string | null;
|
|
272
293
|
converterVersion?: string | null;
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// node:path has no Bun equivalent
|
|
2
|
+
import { posix as pathPosix } from "node:path";
|
|
3
|
+
|
|
4
|
+
export type DocumentCapabilityMode = "editable" | "read_only";
|
|
5
|
+
|
|
6
|
+
export interface DocumentCapabilities {
|
|
7
|
+
editable: boolean;
|
|
8
|
+
tagsEditable: boolean;
|
|
9
|
+
tagsWriteback: boolean;
|
|
10
|
+
canCreateEditableCopy: boolean;
|
|
11
|
+
mode: DocumentCapabilityMode;
|
|
12
|
+
reason?: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const EDITABLE_EXTENSIONS = new Set([
|
|
16
|
+
".md",
|
|
17
|
+
".markdown",
|
|
18
|
+
".mdx",
|
|
19
|
+
".txt",
|
|
20
|
+
".text",
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
function isTextLikeMime(mime: string): boolean {
|
|
24
|
+
return mime.startsWith("text/");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function getDocumentCapabilities(input: {
|
|
28
|
+
sourceExt: string;
|
|
29
|
+
sourceMime: string;
|
|
30
|
+
contentAvailable: boolean;
|
|
31
|
+
}): DocumentCapabilities {
|
|
32
|
+
const ext = input.sourceExt.toLowerCase();
|
|
33
|
+
const editable =
|
|
34
|
+
EDITABLE_EXTENSIONS.has(ext) || isTextLikeMime(input.sourceMime);
|
|
35
|
+
const tagsWriteback = ext === ".md" || ext === ".markdown" || ext === ".mdx";
|
|
36
|
+
|
|
37
|
+
if (editable) {
|
|
38
|
+
return {
|
|
39
|
+
editable: true,
|
|
40
|
+
tagsEditable: true,
|
|
41
|
+
tagsWriteback,
|
|
42
|
+
canCreateEditableCopy: false,
|
|
43
|
+
mode: "editable",
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
editable: false,
|
|
49
|
+
tagsEditable: true,
|
|
50
|
+
tagsWriteback: false,
|
|
51
|
+
canCreateEditableCopy: input.contentAvailable,
|
|
52
|
+
mode: "read_only",
|
|
53
|
+
reason:
|
|
54
|
+
"This document is derived from a source format that GNO cannot safely write back in place.",
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function deriveEditableCopyRelPath(
|
|
59
|
+
relPath: string,
|
|
60
|
+
existingRelPaths: Iterable<string> = []
|
|
61
|
+
): string {
|
|
62
|
+
const parsed = pathPosix.parse(relPath);
|
|
63
|
+
const prefix = parsed.dir ? `${parsed.dir}/` : "";
|
|
64
|
+
const baseName = parsed.name || "copy";
|
|
65
|
+
const existing = new Set(existingRelPaths);
|
|
66
|
+
|
|
67
|
+
const baseCandidate =
|
|
68
|
+
parsed.ext.toLowerCase() === ".md" ||
|
|
69
|
+
parsed.ext.toLowerCase() === ".markdown" ||
|
|
70
|
+
parsed.ext.toLowerCase() === ".mdx"
|
|
71
|
+
? `${prefix}${baseName}.copy.md`
|
|
72
|
+
: `${prefix}${baseName}.md`;
|
|
73
|
+
|
|
74
|
+
if (!existing.has(baseCandidate)) {
|
|
75
|
+
return baseCandidate;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let counter = 2;
|
|
79
|
+
while (true) {
|
|
80
|
+
const candidate = `${prefix}${baseName}.copy-${counter}.md`;
|
|
81
|
+
if (!existing.has(candidate)) {
|
|
82
|
+
return candidate;
|
|
83
|
+
}
|
|
84
|
+
counter += 1;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function buildEditableCopyContent(input: {
|
|
89
|
+
title: string;
|
|
90
|
+
sourceDocid: string;
|
|
91
|
+
sourceUri: string;
|
|
92
|
+
sourceMime: string;
|
|
93
|
+
sourceExt: string;
|
|
94
|
+
content: string;
|
|
95
|
+
tags?: string[];
|
|
96
|
+
}): string {
|
|
97
|
+
const frontmatterLines = [
|
|
98
|
+
`title: ${JSON.stringify(input.title)}`,
|
|
99
|
+
`gno_source_docid: ${JSON.stringify(input.sourceDocid)}`,
|
|
100
|
+
`gno_source_uri: ${JSON.stringify(input.sourceUri)}`,
|
|
101
|
+
`gno_source_mime: ${JSON.stringify(input.sourceMime)}`,
|
|
102
|
+
`gno_source_ext: ${JSON.stringify(input.sourceExt)}`,
|
|
103
|
+
];
|
|
104
|
+
|
|
105
|
+
if (input.tags && input.tags.length > 0) {
|
|
106
|
+
frontmatterLines.push("tags:");
|
|
107
|
+
for (const tag of input.tags) {
|
|
108
|
+
frontmatterLines.push(` - ${JSON.stringify(tag)}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return `---\n${frontmatterLines.join("\n")}\n---\n\n${input.content}`;
|
|
113
|
+
}
|
package/src/mcp/tools/get.ts
CHANGED
|
@@ -11,6 +11,10 @@ import type { ToolContext } from "../server";
|
|
|
11
11
|
|
|
12
12
|
import { parseUri } from "../../app/constants";
|
|
13
13
|
import { parseRef } from "../../cli/commands/ref-parser";
|
|
14
|
+
import {
|
|
15
|
+
getDocumentCapabilities,
|
|
16
|
+
type DocumentCapabilities,
|
|
17
|
+
} from "../../core/document-capabilities";
|
|
14
18
|
import { runTool, type ToolResult } from "./index";
|
|
15
19
|
|
|
16
20
|
interface GetInput {
|
|
@@ -42,6 +46,7 @@ interface GetResponse {
|
|
|
42
46
|
converterVersion?: string;
|
|
43
47
|
mirrorHash?: string;
|
|
44
48
|
};
|
|
49
|
+
capabilities: DocumentCapabilities;
|
|
45
50
|
}
|
|
46
51
|
|
|
47
52
|
/**
|
|
@@ -213,6 +218,11 @@ export function handleGet(
|
|
|
213
218
|
mirrorHash: doc.mirrorHash,
|
|
214
219
|
}
|
|
215
220
|
: undefined,
|
|
221
|
+
capabilities: getDocumentCapabilities({
|
|
222
|
+
sourceExt: doc.sourceExt,
|
|
223
|
+
sourceMime: doc.sourceMime,
|
|
224
|
+
contentAvailable: doc.mirrorHash !== null,
|
|
225
|
+
}),
|
|
216
226
|
};
|
|
217
227
|
|
|
218
228
|
return response;
|
package/src/sdk/documents.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
} from "./types";
|
|
24
24
|
|
|
25
25
|
import { isGlobPattern, parseRef, splitRefs } from "../cli/commands/ref-parser";
|
|
26
|
+
import { getDocumentCapabilities } from "../core/document-capabilities";
|
|
26
27
|
import { sdkError } from "./errors";
|
|
27
28
|
|
|
28
29
|
const URI_PREFIX_PATTERN = /^gno:\/\/[^/]+\//;
|
|
@@ -53,6 +54,7 @@ function buildSourceMeta(
|
|
|
53
54
|
mime: doc.sourceMime,
|
|
54
55
|
ext: doc.sourceExt,
|
|
55
56
|
sizeBytes: doc.sourceSize,
|
|
57
|
+
modifiedAt: doc.sourceMtime ?? undefined,
|
|
56
58
|
sourceHash: doc.sourceHash,
|
|
57
59
|
};
|
|
58
60
|
}
|
|
@@ -122,6 +124,11 @@ export async function getDocumentByRef(
|
|
|
122
124
|
language: doc.languageHint ?? undefined,
|
|
123
125
|
source: buildSourceMeta(doc, config),
|
|
124
126
|
conversion: buildConversionMeta(doc),
|
|
127
|
+
capabilities: getDocumentCapabilities({
|
|
128
|
+
sourceExt: doc.sourceExt,
|
|
129
|
+
sourceMime: doc.sourceMime,
|
|
130
|
+
contentAvailable: doc.mirrorHash !== null,
|
|
131
|
+
}),
|
|
125
132
|
};
|
|
126
133
|
}
|
|
127
134
|
|
|
@@ -144,6 +151,11 @@ export async function getDocumentByRef(
|
|
|
144
151
|
language: doc.languageHint ?? undefined,
|
|
145
152
|
source: buildSourceMeta(doc, config),
|
|
146
153
|
conversion: buildConversionMeta(doc),
|
|
154
|
+
capabilities: getDocumentCapabilities({
|
|
155
|
+
sourceExt: doc.sourceExt,
|
|
156
|
+
sourceMime: doc.sourceMime,
|
|
157
|
+
contentAvailable: doc.mirrorHash !== null,
|
|
158
|
+
}),
|
|
147
159
|
};
|
|
148
160
|
}
|
|
149
161
|
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type DocumentEventOrigin = "watcher" | "save" | "create";
|
|
2
|
+
|
|
3
|
+
export interface DocumentEvent {
|
|
4
|
+
type: "document-changed";
|
|
5
|
+
uri: string;
|
|
6
|
+
collection: string;
|
|
7
|
+
relPath: string;
|
|
8
|
+
origin: DocumentEventOrigin;
|
|
9
|
+
changedAt: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const encoder = new TextEncoder();
|
|
13
|
+
|
|
14
|
+
export class DocumentEventBus {
|
|
15
|
+
readonly #controllers = new Set<
|
|
16
|
+
ReadableStreamDefaultController<Uint8Array>
|
|
17
|
+
>();
|
|
18
|
+
|
|
19
|
+
createResponse(): Response {
|
|
20
|
+
const controllers = this.#controllers;
|
|
21
|
+
let streamController: ReadableStreamDefaultController<Uint8Array> | null =
|
|
22
|
+
null;
|
|
23
|
+
const stream = new ReadableStream<Uint8Array>({
|
|
24
|
+
start(controller) {
|
|
25
|
+
streamController = controller;
|
|
26
|
+
controllers.add(controller);
|
|
27
|
+
controller.enqueue(encoder.encode(": connected\n\n"));
|
|
28
|
+
},
|
|
29
|
+
cancel() {
|
|
30
|
+
if (streamController) {
|
|
31
|
+
controllers.delete(streamController);
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
return new Response(stream, {
|
|
37
|
+
headers: {
|
|
38
|
+
"Content-Type": "text/event-stream",
|
|
39
|
+
"Cache-Control": "no-cache",
|
|
40
|
+
Connection: "keep-alive",
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
emit(event: DocumentEvent): void {
|
|
46
|
+
const payload = encoder.encode(
|
|
47
|
+
`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
for (const controller of this.#controllers) {
|
|
51
|
+
try {
|
|
52
|
+
controller.enqueue(payload);
|
|
53
|
+
} catch {
|
|
54
|
+
this.#controllers.delete(controller);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
close(): void {
|
|
60
|
+
for (const controller of this.#controllers) {
|
|
61
|
+
try {
|
|
62
|
+
controller.close();
|
|
63
|
+
} catch {
|
|
64
|
+
// Best-effort shutdown.
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
this.#controllers.clear();
|
|
68
|
+
}
|
|
69
|
+
}
|
package/src/serve/public/app.tsx
CHANGED
|
@@ -2,9 +2,11 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
2
2
|
import { createRoot } from "react-dom/client";
|
|
3
3
|
|
|
4
4
|
import { HelpButton } from "./components/HelpButton";
|
|
5
|
+
import { QuickSwitcher, saveRecentDocument } from "./components/QuickSwitcher";
|
|
5
6
|
import { ShortcutHelpModal } from "./components/ShortcutHelpModal";
|
|
6
|
-
import { CaptureModalProvider } from "./hooks/useCaptureModal";
|
|
7
|
+
import { CaptureModalProvider, useCaptureModal } from "./hooks/useCaptureModal";
|
|
7
8
|
import { useKeyboardShortcuts } from "./hooks/useKeyboardShortcuts";
|
|
9
|
+
import { parseDocumentDeepLink } from "./lib/deep-links";
|
|
8
10
|
import Ask from "./pages/Ask";
|
|
9
11
|
import Browse from "./pages/Browse";
|
|
10
12
|
import Collections from "./pages/Collections";
|
|
@@ -36,37 +38,48 @@ const routes: Record<Route, React.ComponentType<{ navigate: Navigate }>> = {
|
|
|
36
38
|
"/graph": GraphView,
|
|
37
39
|
};
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
);
|
|
44
|
-
|
|
41
|
+
interface AppContentProps {
|
|
42
|
+
location: string;
|
|
43
|
+
navigate: Navigate;
|
|
44
|
+
shortcutHelpOpen: boolean;
|
|
45
|
+
setShortcutHelpOpen: (open: boolean) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function AppContent({
|
|
49
|
+
location,
|
|
50
|
+
navigate,
|
|
51
|
+
shortcutHelpOpen,
|
|
52
|
+
setShortcutHelpOpen,
|
|
53
|
+
}: AppContentProps) {
|
|
54
|
+
const { openCapture } = useCaptureModal();
|
|
55
|
+
const [quickSwitcherOpen, setQuickSwitcherOpen] = useState(false);
|
|
45
56
|
|
|
46
57
|
useEffect(() => {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}, []);
|
|
58
|
+
const basePath = location.split("?")[0];
|
|
59
|
+
if (basePath !== "/doc" && basePath !== "/edit") {
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
63
|
+
const search = location.includes("?")
|
|
64
|
+
? `?${location.split("?")[1] ?? ""}`
|
|
65
|
+
: "";
|
|
66
|
+
const target = parseDocumentDeepLink(search);
|
|
67
|
+
if (!target.uri) {
|
|
57
68
|
return;
|
|
58
69
|
}
|
|
59
|
-
window.history.pushState({}, "", to);
|
|
60
|
-
setLocation(to);
|
|
61
|
-
}, []);
|
|
62
70
|
|
|
63
|
-
|
|
71
|
+
saveRecentDocument({
|
|
72
|
+
uri: target.uri,
|
|
73
|
+
href: location,
|
|
74
|
+
label: decodeURIComponent(target.uri.split("/").pop() ?? target.uri),
|
|
75
|
+
});
|
|
76
|
+
}, [location]);
|
|
77
|
+
|
|
64
78
|
const shortcuts = useMemo(
|
|
65
79
|
() => [
|
|
66
80
|
{
|
|
67
81
|
key: "/",
|
|
68
82
|
action: () => {
|
|
69
|
-
// Focus search input on current page or navigate to search
|
|
70
83
|
const searchInput = document.querySelector<HTMLInputElement>(
|
|
71
84
|
'input[type="search"], input[placeholder*="Search"], input[id*="search"]'
|
|
72
85
|
);
|
|
@@ -82,18 +95,22 @@ function App() {
|
|
|
82
95
|
key: "?",
|
|
83
96
|
action: () => setShortcutHelpOpen(true),
|
|
84
97
|
},
|
|
98
|
+
{
|
|
99
|
+
key: "k",
|
|
100
|
+
meta: true,
|
|
101
|
+
action: () => setQuickSwitcherOpen(true),
|
|
102
|
+
},
|
|
85
103
|
],
|
|
86
|
-
[navigate]
|
|
104
|
+
[navigate, setShortcutHelpOpen]
|
|
87
105
|
);
|
|
88
106
|
|
|
89
107
|
useKeyboardShortcuts(shortcuts);
|
|
90
108
|
|
|
91
|
-
// Extract base path for routing (ignore query params)
|
|
92
109
|
const basePath = location.split("?")[0] as Route;
|
|
93
110
|
const Page = routes[basePath] || Dashboard;
|
|
94
111
|
|
|
95
112
|
return (
|
|
96
|
-
|
|
113
|
+
<>
|
|
97
114
|
<div className="flex min-h-screen flex-col">
|
|
98
115
|
<div className="flex-1">
|
|
99
116
|
<Page key={location} navigate={navigate} />
|
|
@@ -147,10 +164,50 @@ function App() {
|
|
|
147
164
|
</footer>
|
|
148
165
|
</div>
|
|
149
166
|
<HelpButton onClick={() => setShortcutHelpOpen(true)} />
|
|
167
|
+
<QuickSwitcher
|
|
168
|
+
navigate={(to) => navigate(to)}
|
|
169
|
+
onCreateNote={openCapture}
|
|
170
|
+
onOpenChange={setQuickSwitcherOpen}
|
|
171
|
+
open={quickSwitcherOpen}
|
|
172
|
+
/>
|
|
150
173
|
<ShortcutHelpModal
|
|
151
174
|
onOpenChange={setShortcutHelpOpen}
|
|
152
175
|
open={shortcutHelpOpen}
|
|
153
176
|
/>
|
|
177
|
+
</>
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function App() {
|
|
182
|
+
const [location, setLocation] = useState<string>(
|
|
183
|
+
window.location.pathname + window.location.search
|
|
184
|
+
);
|
|
185
|
+
const [shortcutHelpOpen, setShortcutHelpOpen] = useState(false);
|
|
186
|
+
|
|
187
|
+
useEffect(() => {
|
|
188
|
+
const handlePopState = () =>
|
|
189
|
+
setLocation(window.location.pathname + window.location.search);
|
|
190
|
+
window.addEventListener("popstate", handlePopState);
|
|
191
|
+
return () => window.removeEventListener("popstate", handlePopState);
|
|
192
|
+
}, []);
|
|
193
|
+
|
|
194
|
+
const navigate = useCallback((to: string | number) => {
|
|
195
|
+
if (typeof to === "number") {
|
|
196
|
+
window.history.go(to);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
window.history.pushState({}, "", to);
|
|
200
|
+
setLocation(to);
|
|
201
|
+
}, []);
|
|
202
|
+
|
|
203
|
+
return (
|
|
204
|
+
<CaptureModalProvider>
|
|
205
|
+
<AppContent
|
|
206
|
+
location={location}
|
|
207
|
+
navigate={navigate}
|
|
208
|
+
setShortcutHelpOpen={setShortcutHelpOpen}
|
|
209
|
+
shortcutHelpOpen={shortcutHelpOpen}
|
|
210
|
+
/>
|
|
154
211
|
</CaptureModalProvider>
|
|
155
212
|
);
|
|
156
213
|
}
|