pagedex 0.1.4 → 0.1.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pagedex",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Local-first Markdown knowledge viewer, editor, Git bridge, and MCP v2 server for any directory.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -29,6 +29,7 @@
29
29
  "start": "node --no-warnings src/server/index.js",
30
30
  "prepublishOnly": "vite build",
31
31
  "test": "node --no-warnings --test test/*.test.js",
32
+ "test:playwright": "node --no-warnings --test test/playwright/*.spec.js",
32
33
  "smoke": "node --no-warnings test/smoke.js all",
33
34
  "smoke:api": "node --no-warnings test/smoke.js api",
34
35
  "smoke:mcp": "node --no-warnings test/smoke.js mcp"
@@ -42,6 +43,7 @@
42
43
  "@tiptap/starter-kit": "^3.34.0",
43
44
  "highlight.js": "^11.12.0",
44
45
  "hono": "^4.7.0",
46
+ "lucide-react": "^1.73.0",
45
47
  "marked": "^15.0.0",
46
48
  "react": "^19.1.0",
47
49
  "react-dom": "^19.1.0",
@@ -50,6 +52,7 @@
50
52
  },
51
53
  "devDependencies": {
52
54
  "@vitejs/plugin-react": "^4.4.0",
55
+ "playwright-core": "^1.64.1",
53
56
  "vite": "^6.3.0"
54
57
  }
55
58
  }
@@ -12,6 +12,7 @@ import { registerEventRoutes, startVaultWatcher } from './routes/events.js';
12
12
 
13
13
  const HOST = process.env.HOST || '127.0.0.1';
14
14
  const PORT = Number(process.env.PORT || 4321);
15
+ const READ_ONLY = process.env.PAGEDEX_READ_ONLY === '1' || process.env.PAGEDEX_READ_ONLY === 'true';
15
16
 
16
17
  try {
17
18
  syncIndex();
@@ -27,6 +28,9 @@ const app = new Hono();
27
28
  app.use('/api/v1/*', async (c, next) => {
28
29
  const method = c.req.method.toUpperCase();
29
30
  if (method !== 'GET' && method !== 'HEAD' && method !== 'OPTIONS') {
31
+ if (READ_ONLY) {
32
+ return c.json({ error: 'Server is running in read-only mode' }, 403);
33
+ }
30
34
  if (c.req.header('origin') === 'null') {
31
35
  return c.json({ error: 'Forbidden origin' }, 403);
32
36
  }
package/src/server/mcp.js CHANGED
@@ -1,9 +1,57 @@
1
- import { randomUUID } from 'node:crypto';
1
+ import { randomUUID, timingSafeEqual } from 'node:crypto';
2
2
  import fs from 'node:fs';
3
3
  import { McpServer, createMcpHandler, parseJSONRPCMessage } from '@modelcontextprotocol/server';
4
4
  import { z } from 'zod';
5
- import { deleteNote, editNote, getCachedVault, resolveSafe, writeNote } from './vault.js';
6
- import { removeFileFromIndex, searchFts, upsertFileInIndex } from './search-index.js';
5
+ import {
6
+ deleteNote,
7
+ discardGitChange,
8
+ editNote,
9
+ getCachedVault,
10
+ getGitChanges,
11
+ getGitDiff,
12
+ gitCommitAndPush,
13
+ renameDir,
14
+ renameNote,
15
+ resolveSafe,
16
+ writeNote
17
+ } from './vault.js';
18
+ import { removeFileFromIndex, searchFts, syncIndex, upsertFileInIndex } from './search-index.js';
19
+
20
+ const VALID_MCP_MODES = new Set(['read', 'write', 'full', 'off']);
21
+
22
+ export function getMcpConfig() {
23
+ const readOnly = process.env.PAGEDEX_READ_ONLY === '1' || process.env.PAGEDEX_READ_ONLY === 'true';
24
+ const rawMode = (process.env.PAGEDEX_MCP_MODE || 'full').toLowerCase();
25
+ const normalized = VALID_MCP_MODES.has(rawMode) ? rawMode : 'full';
26
+ const mode = normalized === 'off' ? 'off' : readOnly ? 'read' : normalized;
27
+ const allowWrite = mode === 'write' || mode === 'full';
28
+ const allowDelete = mode === 'full';
29
+ const allowGit =
30
+ !readOnly &&
31
+ (process.env.PAGEDEX_MCP_GIT === '1' || process.env.PAGEDEX_MCP_GIT === 'true') &&
32
+ allowWrite;
33
+ return { mode, readOnly, allowWrite, allowDelete, allowGit };
34
+ }
35
+
36
+ function readMcpToken() {
37
+ const tokenFile = process.env.PAGEDEX_MCP_TOKEN_FILE;
38
+ if (tokenFile) {
39
+ return fs.readFileSync(tokenFile, 'utf8').trim();
40
+ }
41
+ return (process.env.PAGEDEX_MCP_TOKEN || '').trim();
42
+ }
43
+
44
+ function isAuthorizedMcpRequest(req) {
45
+ const expected = readMcpToken();
46
+ if (!expected) return true;
47
+ const auth = req.headers.get('authorization') || '';
48
+ const match = auth.match(/^Bearer\s+(.+)$/i);
49
+ if (!match) return false;
50
+ const provided = match[1].trim();
51
+ const a = Buffer.from(provided, 'utf8');
52
+ const b = Buffer.from(expected, 'utf8');
53
+ return a.length === b.length && timingSafeEqual(a, b);
54
+ }
7
55
 
8
56
  const sseSessions = new Map();
9
57
  const encoder = new TextEncoder();
@@ -88,7 +136,7 @@ class WebStandardSSEServerTransport {
88
136
  }
89
137
  }
90
138
 
91
- function createVaultMcpServer() {
139
+ function createVaultMcpServer(config = getMcpConfig()) {
92
140
  const server = new McpServer({
93
141
  name: 'vault-mcp',
94
142
  version: '0.1.0'
@@ -233,68 +281,142 @@ function createVaultMcpServer() {
233
281
  );
234
282
 
235
283
  server.registerTool(
236
- 'write_note',
284
+ 'get_vault_info',
237
285
  {
238
- title: 'Write or Edit Note',
239
- description:
240
- 'Create/overwrite/append a note (with content), or partially edit an existing note (with target + replacement).',
286
+ title: 'Get Vault Info',
287
+ description: 'Return vault summary: total indexed files and the most recently modified files.',
241
288
  inputSchema: z.object({
242
- path: z.string(),
243
- content: z.string().optional(),
244
- append: z.boolean().optional(),
245
- target: z.string().optional(),
246
- replacement: z.string().optional(),
247
- replaceAll: z.boolean().optional()
289
+ recentLimit: z.number().int().min(1).max(50).optional()
290
+ }),
291
+ outputSchema: z.object({
292
+ totalFiles: z.number(),
293
+ recentFiles: z.array(
294
+ z.object({
295
+ path: z.string(),
296
+ mtimeMs: z.number()
297
+ })
298
+ )
299
+ })
300
+ },
301
+ async ({ recentLimit }) => {
302
+ const { flatFiles, fileStats } = getCachedVault();
303
+ const limit = recentLimit ?? 15;
304
+ const recentFiles = [...(fileStats || [])]
305
+ .sort((a, b) => (b.mtimeMs || 0) - (a.mtimeMs || 0))
306
+ .slice(0, limit)
307
+ .map((f) => ({ path: f.path, mtimeMs: f.mtimeMs || 0 }));
308
+ const lines = recentFiles.map((f) => f.path);
309
+ return {
310
+ structuredContent: {
311
+ totalFiles: flatFiles.length,
312
+ recentFiles
313
+ },
314
+ content: [
315
+ {
316
+ type: 'text',
317
+ text: `Total files: ${flatFiles.length}\nRecent files:\n${lines.length ? lines.join('\n') : '(none)'}`
318
+ }
319
+ ]
320
+ };
321
+ }
322
+ );
323
+
324
+ server.registerTool(
325
+ 'get_note_links',
326
+ {
327
+ title: 'Get Note Links and Outline',
328
+ description: 'Return outgoing [[wikilinks]], incoming backlinks, and Markdown headings for a note.',
329
+ inputSchema: z.object({
330
+ path: z.string()
248
331
  }),
249
332
  outputSchema: z.object({
250
- ok: z.boolean(),
251
333
  path: z.string(),
252
- mode: z.enum(['write', 'append', 'edit']),
253
- bytes: z.number().optional(),
254
- replacements: z.number().optional()
334
+ outgoingLinks: z.array(z.string()),
335
+ backlinks: z.array(z.string()),
336
+ headings: z.array(
337
+ z.object({
338
+ level: z.number(),
339
+ text: z.string()
340
+ })
341
+ )
255
342
  })
256
343
  },
257
- async ({ path: relPath, content, append, target, replacement, replaceAll }) => {
258
- if (typeof target === 'string' && typeof replacement === 'string') {
259
- const result = editNote(relPath, target, replacement, { replaceAll: Boolean(replaceAll) });
260
- if (result.error) {
261
- return { isError: true, content: [{ type: 'text', text: result.error }] };
262
- }
263
- upsertFileInIndex(result.path);
264
- return {
265
- structuredContent: {
266
- ok: true,
267
- path: result.path,
268
- mode: 'edit',
269
- replacements: result.replacements
270
- },
271
- content: [
272
- {
273
- type: 'text',
274
- text: `Edited ${result.path} (${result.replacements} replacement${result.replacements === 1 ? '' : 's'})`
275
- }
276
- ]
277
- };
344
+ async ({ path: relPath }) => {
345
+ const full = resolveSafe(relPath);
346
+ if (!full || !fs.existsSync(full) || !fs.statSync(full).isFile()) {
347
+ return { isError: true, content: [{ type: 'text', text: `Not found: ${relPath}` }] };
278
348
  }
279
- if (typeof content !== 'string') {
280
- return { isError: true, content: [{ type: 'text', text: 'Provide either content or target + replacement' }] };
349
+ const raw = fs.readFileSync(full, 'utf8');
350
+ const outgoingSet = new Set();
351
+ for (const m of raw.matchAll(/\[\[([^\]|#]+)(?:[|#][^\]]*)?\]\]/g)) {
352
+ const target = m[1].trim();
353
+ if (target) outgoingSet.add(target);
281
354
  }
282
- const result = writeNote(relPath, content, { append: Boolean(append) });
283
- if (!result) {
284
- return { isError: true, content: [{ type: 'text', text: `Invalid or protected path: ${relPath}` }] };
355
+ const headings = [];
356
+ for (const line of raw.split('\n')) {
357
+ const hm = line.match(/^(#{1,6})\s+(.+?)\s*#*$/);
358
+ if (hm) headings.push({ level: hm[1].length, text: hm[2].trim() });
285
359
  }
286
- upsertFileInIndex(result.path);
360
+ const { backlinks } = getCachedVault();
361
+ const outgoingLinks = [...outgoingSet];
362
+ const incoming = backlinks[relPath] || [];
363
+ return {
364
+ structuredContent: {
365
+ path: relPath,
366
+ outgoingLinks,
367
+ backlinks: incoming,
368
+ headings
369
+ },
370
+ content: [
371
+ {
372
+ type: 'text',
373
+ text: [
374
+ `# ${relPath}`,
375
+ `Outgoing: ${outgoingLinks.length ? outgoingLinks.join(', ') : 'none'}`,
376
+ `Backlinks: ${incoming.length ? incoming.join(', ') : 'none'}`,
377
+ `Headings: ${headings.length ? headings.map((h) => `${'#'.repeat(h.level)} ${h.text}`).join(' | ') : 'none'}`
378
+ ].join('\n')
379
+ }
380
+ ]
381
+ };
382
+ }
383
+ );
384
+
385
+ server.registerTool(
386
+ 'git_status',
387
+ {
388
+ title: 'Git Status',
389
+ description: 'List uncommitted modified, added, or untracked files in the vault.',
390
+ inputSchema: z.object({}),
391
+ outputSchema: z.object({
392
+ isGit: z.boolean(),
393
+ count: z.number(),
394
+ changes: z.array(
395
+ z.object({
396
+ status: z.string(),
397
+ path: z.string(),
398
+ mtimeMs: z.number()
399
+ })
400
+ )
401
+ })
402
+ },
403
+ async () => {
404
+ const { isGit, changes } = getGitChanges();
405
+ const lines = changes.map((c) => `[${c.status}] ${c.path}`);
287
406
  return {
288
407
  structuredContent: {
289
- ok: true,
290
- path: result.path,
291
- mode: append ? 'append' : 'write',
292
- bytes: result.bytes
408
+ isGit,
409
+ count: changes.length,
410
+ changes
293
411
  },
294
412
  content: [
295
413
  {
296
414
  type: 'text',
297
- text: `${append ? 'Appended' : 'Wrote'} ${result.path} (${result.bytes} bytes)`
415
+ text: !isGit
416
+ ? 'Not a git repository'
417
+ : lines.length
418
+ ? lines.join('\n')
419
+ : 'Working tree clean'
298
420
  }
299
421
  ]
300
422
  };
@@ -302,40 +424,276 @@ function createVaultMcpServer() {
302
424
  );
303
425
 
304
426
  server.registerTool(
305
- 'delete_note',
427
+ 'git_diff',
306
428
  {
307
- title: 'Delete Note',
308
- description: 'Delete a note/file from the vault by relative path.',
429
+ title: 'Git Diff',
430
+ description: 'Get the unified git diff for a specific file in the vault.',
309
431
  inputSchema: z.object({
310
432
  path: z.string()
311
433
  }),
312
434
  outputSchema: z.object({
313
- ok: z.boolean(),
314
- path: z.string()
435
+ path: z.string(),
436
+ untracked: z.boolean(),
437
+ raw: z.string()
315
438
  })
316
439
  },
317
440
  async ({ path: relPath }) => {
318
- const result = deleteNote(relPath);
319
- if (!result) {
320
- return { isError: true, content: [{ type: 'text', text: `Not found or protected path: ${relPath}` }] };
441
+ const diff = getGitDiff(relPath);
442
+ if (diff.error) {
443
+ return { isError: true, content: [{ type: 'text', text: diff.error }] };
321
444
  }
322
- removeFileFromIndex(result.path);
323
445
  return {
324
446
  structuredContent: {
325
- ok: true,
326
- path: result.path
447
+ path: diff.path,
448
+ untracked: Boolean(diff.untracked),
449
+ raw: diff.raw || ''
327
450
  },
328
- content: [{ type: 'text', text: `Deleted ${result.path}` }]
451
+ content: [
452
+ {
453
+ type: 'text',
454
+ text: diff.raw ? diff.raw : `No changes in ${diff.path}`
455
+ }
456
+ ]
329
457
  };
330
458
  }
331
459
  );
332
460
 
461
+ if (config.allowWrite) {
462
+ server.registerTool(
463
+ 'write_note',
464
+ {
465
+ title: 'Write or Edit Note',
466
+ description:
467
+ 'Create/overwrite/append a note (with content), or partially edit an existing note (with target + replacement).',
468
+ inputSchema: z.object({
469
+ path: z.string(),
470
+ content: z.string().optional(),
471
+ append: z.boolean().optional(),
472
+ target: z.string().optional(),
473
+ replacement: z.string().optional(),
474
+ replaceAll: z.boolean().optional()
475
+ }),
476
+ outputSchema: z.object({
477
+ ok: z.boolean(),
478
+ path: z.string(),
479
+ mode: z.enum(['write', 'append', 'edit']),
480
+ bytes: z.number().optional(),
481
+ replacements: z.number().optional()
482
+ })
483
+ },
484
+ async ({ path: relPath, content, append, target, replacement, replaceAll }) => {
485
+ if (typeof target === 'string' && typeof replacement === 'string') {
486
+ const result = editNote(relPath, target, replacement, { replaceAll: Boolean(replaceAll) });
487
+ if (result.error) {
488
+ return { isError: true, content: [{ type: 'text', text: result.error }] };
489
+ }
490
+ upsertFileInIndex(result.path);
491
+ return {
492
+ structuredContent: {
493
+ ok: true,
494
+ path: result.path,
495
+ mode: 'edit',
496
+ replacements: result.replacements
497
+ },
498
+ content: [
499
+ {
500
+ type: 'text',
501
+ text: `Edited ${result.path} (${result.replacements} replacement${result.replacements === 1 ? '' : 's'})`
502
+ }
503
+ ]
504
+ };
505
+ }
506
+ if (typeof content !== 'string') {
507
+ return { isError: true, content: [{ type: 'text', text: 'Provide either content or target + replacement' }] };
508
+ }
509
+ const result = writeNote(relPath, content, { append: Boolean(append) });
510
+ if (!result) {
511
+ return { isError: true, content: [{ type: 'text', text: `Invalid or protected path: ${relPath}` }] };
512
+ }
513
+ upsertFileInIndex(result.path);
514
+ return {
515
+ structuredContent: {
516
+ ok: true,
517
+ path: result.path,
518
+ mode: append ? 'append' : 'write',
519
+ bytes: result.bytes
520
+ },
521
+ content: [
522
+ {
523
+ type: 'text',
524
+ text: `${append ? 'Appended' : 'Wrote'} ${result.path} (${result.bytes} bytes)`
525
+ }
526
+ ]
527
+ };
528
+ }
529
+ );
530
+
531
+ server.registerTool(
532
+ 'move_path',
533
+ {
534
+ title: 'Move or Rename File/Folder',
535
+ description: 'Rename or move a file or folder within the vault.',
536
+ inputSchema: z.object({
537
+ oldPath: z.string(),
538
+ newPath: z.string(),
539
+ type: z.enum(['auto', 'file', 'dir']).optional()
540
+ }),
541
+ outputSchema: z.object({
542
+ ok: z.boolean(),
543
+ type: z.enum(['file', 'dir']),
544
+ oldPath: z.string(),
545
+ newPath: z.string()
546
+ })
547
+ },
548
+ async ({ oldPath, newPath, type = 'auto' }) => {
549
+ const full = resolveSafe(oldPath);
550
+ if (!full || !fs.existsSync(full)) {
551
+ return { isError: true, content: [{ type: 'text', text: `Source not found: ${oldPath}` }] };
552
+ }
553
+ const isDir = type === 'dir' || (type === 'auto' && fs.statSync(full).isDirectory());
554
+ const result = isDir ? renameDir(oldPath, newPath) : renameNote(oldPath, newPath);
555
+ if (result.error) {
556
+ return { isError: true, content: [{ type: 'text', text: result.error }] };
557
+ }
558
+ if (isDir) {
559
+ syncIndex();
560
+ } else {
561
+ removeFileFromIndex(result.oldPath);
562
+ upsertFileInIndex(result.newPath);
563
+ }
564
+ return {
565
+ structuredContent: {
566
+ ok: true,
567
+ type: isDir ? 'dir' : 'file',
568
+ oldPath: result.oldPath,
569
+ newPath: result.newPath
570
+ },
571
+ content: [{ type: 'text', text: `Moved ${result.oldPath} -> ${result.newPath}` }]
572
+ };
573
+ }
574
+ );
575
+ }
576
+
577
+ if (config.allowDelete) {
578
+ server.registerTool(
579
+ 'delete_note',
580
+ {
581
+ title: 'Delete Note',
582
+ description: 'Delete a note/file from the vault by relative path.',
583
+ inputSchema: z.object({
584
+ path: z.string()
585
+ }),
586
+ outputSchema: z.object({
587
+ ok: z.boolean(),
588
+ path: z.string()
589
+ })
590
+ },
591
+ async ({ path: relPath }) => {
592
+ const result = deleteNote(relPath);
593
+ if (!result) {
594
+ return { isError: true, content: [{ type: 'text', text: `Not found or protected path: ${relPath}` }] };
595
+ }
596
+ removeFileFromIndex(result.path);
597
+ return {
598
+ structuredContent: {
599
+ ok: true,
600
+ path: result.path
601
+ },
602
+ content: [{ type: 'text', text: `Deleted ${result.path}` }]
603
+ };
604
+ }
605
+ );
606
+ }
607
+
608
+ if (config.allowGit) {
609
+ server.registerTool(
610
+ 'git_commit',
611
+ {
612
+ title: 'Git Commit',
613
+ description: 'Stage and commit vault changes locally (never pushes).',
614
+ inputSchema: z.object({
615
+ message: z.string(),
616
+ files: z.array(z.string()).optional()
617
+ }),
618
+ outputSchema: z.object({
619
+ ok: z.boolean(),
620
+ output: z.string()
621
+ })
622
+ },
623
+ async ({ message, files }) => {
624
+ const result = gitCommitAndPush({ message, files, push: false });
625
+ if (result.error) {
626
+ return { isError: true, content: [{ type: 'text', text: result.error }] };
627
+ }
628
+ return {
629
+ structuredContent: {
630
+ ok: true,
631
+ output: result.output || ''
632
+ },
633
+ content: [{ type: 'text', text: result.output || 'Committed' }]
634
+ };
635
+ }
636
+ );
637
+
638
+ server.registerTool(
639
+ 'git_discard',
640
+ {
641
+ title: 'Git Discard File Changes',
642
+ description: 'Discard all uncommitted changes to a file (or remove an untracked file).',
643
+ inputSchema: z.object({
644
+ path: z.string()
645
+ }),
646
+ outputSchema: z.object({
647
+ ok: z.boolean(),
648
+ path: z.string(),
649
+ discarded: z.string()
650
+ })
651
+ },
652
+ async ({ path: relPath }) => {
653
+ const result = discardGitChange({ path: relPath });
654
+ if (result.error) {
655
+ return { isError: true, content: [{ type: 'text', text: result.error }] };
656
+ }
657
+ syncIndex();
658
+ return {
659
+ structuredContent: {
660
+ ok: true,
661
+ path: result.path,
662
+ discarded: result.discarded || 'file'
663
+ },
664
+ content: [{ type: 'text', text: `Discarded changes in ${result.path} (${result.discarded})` }]
665
+ };
666
+ }
667
+ );
668
+ }
669
+
333
670
  return server;
334
671
  }
335
672
 
336
- const mcpHandler = createMcpHandler(createVaultMcpServer);
673
+ const mcpHandlers = new Map();
674
+
675
+ function getMcpHandler(config) {
676
+ const key = `${config.mode}:${config.allowGit}`;
677
+ let handler = mcpHandlers.get(key);
678
+ if (!handler) {
679
+ handler = createMcpHandler(() => createVaultMcpServer(config));
680
+ mcpHandlers.set(key, handler);
681
+ }
682
+ return handler;
683
+ }
337
684
 
338
685
  export async function handleMcpRequest(req) {
686
+ const config = getMcpConfig();
687
+ if (config.mode === 'off') {
688
+ return Response.json({ error: 'MCP endpoint is disabled' }, { status: 404 });
689
+ }
690
+ if (!isAuthorizedMcpRequest(req)) {
691
+ return Response.json(
692
+ { error: 'Unauthorized' },
693
+ { status: 401, headers: { 'WWW-Authenticate': 'Bearer' } }
694
+ );
695
+ }
696
+
339
697
  const url = new URL(req.url, 'http://localhost');
340
698
  const sessionId = url.searchParams.get('sessionId');
341
699
  const isStreamableHttpGet =
@@ -345,7 +703,7 @@ export async function handleMcpRequest(req) {
345
703
  if (req.method === 'GET' && !sessionId && !isStreamableHttpGet) {
346
704
  const transport = new WebStandardSSEServerTransport(url.pathname);
347
705
  sseSessions.set(transport.sessionId, transport);
348
- const server = createVaultMcpServer();
706
+ const server = createVaultMcpServer(config);
349
707
  await server.connect(transport);
350
708
  return transport.createResponse(req.signal);
351
709
  }
@@ -360,5 +718,5 @@ export async function handleMcpRequest(req) {
360
718
  }
361
719
 
362
720
  // 3. Official MCP v2 handler: supports BOTH Modern (2026-07-28 server/discover) and Legacy Streamable HTTP (2025-11-25 initialize)
363
- return mcpHandler.fetch(req);
721
+ return getMcpHandler(config).fetch(req);
364
722
  }
@@ -1 +0,0 @@
1
- import{a as d,j as e}from"./vendor-react-E4kvVwMA.js";import{u as p,E as b,s as h,M as x}from"./vendor-tiptap-ChgD7Piv.js";import"./vendor-markdown-C94ubdL7.js";function N({content:n,fileKey:u,fontSize:m,lineHeight:f,maxWidth:g,saving:c,dirty:o,onChange:l,onSave:v}){const t=p({extensions:[h,x],content:n});return d.useEffect(()=>{var s,i;if(!t)return;(((i=(s=t.storage.markdown)==null?void 0:s.getMarkdown)==null?void 0:i.call(s))??"")!==n&&!o&&t.commands.setContent(n)},[u,n,t,o]),d.useEffect(()=>{if(!t)return;const a=()=>{var i,r;const s=((r=(i=t.storage.markdown)==null?void 0:i.getMarkdown)==null?void 0:r.call(i))??"";l(s)};return t.on("update",a),()=>t.off("update",a)},[t,l]),t?e.jsxs("div",{className:"tiptap-wrap",style:{maxWidth:`${g}px`},children:[e.jsxs("div",{className:"tiptap-toolbar",children:[e.jsx("button",{className:`btn${t.isActive("bold")?" active":""}`,onClick:()=>t.chain().focus().toggleBold().run(),children:"B"}),e.jsx("button",{className:`btn${t.isActive("italic")?" active":""}`,onClick:()=>t.chain().focus().toggleItalic().run(),children:"I"}),e.jsx("button",{className:`btn${t.isActive("heading",{level:1})?" active":""}`,onClick:()=>t.chain().focus().toggleHeading({level:1}).run(),children:"H1"}),e.jsx("button",{className:`btn${t.isActive("heading",{level:2})?" active":""}`,onClick:()=>t.chain().focus().toggleHeading({level:2}).run(),children:"H2"}),e.jsx("button",{className:`btn${t.isActive("bulletList")?" active":""}`,onClick:()=>t.chain().focus().toggleBulletList().run(),children:"List"}),e.jsx("button",{className:`btn${t.isActive("codeBlock")?" active":""}`,onClick:()=>t.chain().focus().toggleCodeBlock().run(),children:"Code"}),e.jsx("button",{className:`btn${o?" primary":""}`,style:{marginLeft:"auto"},disabled:c,onClick:()=>{var a,s;return v(((s=(a=t.storage.markdown)==null?void 0:a.getMarkdown)==null?void 0:s.call(a))??"")},children:c?"Saving...":o?"Save *":"Saved"})]}),e.jsx("div",{className:"doc tiptap-body",style:{fontSize:`${m}px`,lineHeight:f},children:e.jsx(b,{editor:t})})]}):null}export{N as default};