@peers-app/peers-ui 0.15.5 → 0.16.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.
Files changed (29) hide show
  1. package/dist/components/markdown-editor/editor.js +2 -1
  2. package/dist/components/markdown-editor/move-line-plugin.d.ts +11 -1
  3. package/dist/components/markdown-editor/move-line-plugin.js +83 -18
  4. package/dist/components/markdown-editor/select-line-boundary-plugin.d.ts +8 -0
  5. package/dist/components/markdown-editor/select-line-boundary-plugin.js +71 -0
  6. package/dist/components/router.js +4 -0
  7. package/dist/screens/account/account-screen.d.ts +1 -0
  8. package/dist/screens/account/account-screen.js +60 -0
  9. package/dist/screens/data-explorer/data-explorer.js +64 -16
  10. package/dist/screens/setup-user.js +4 -0
  11. package/dist/system-apps/account.app.d.ts +2 -0
  12. package/dist/system-apps/account.app.js +8 -0
  13. package/dist/system-apps/index.d.ts +1 -0
  14. package/dist/system-apps/index.js +5 -1
  15. package/package.json +3 -3
  16. package/src/components/markdown-editor/editor.tsx +2 -0
  17. package/src/components/markdown-editor/move-line-plugin.tsx +89 -19
  18. package/src/components/markdown-editor/select-line-boundary-plugin.tsx +95 -0
  19. package/src/components/router.tsx +4 -0
  20. package/src/screens/account/account-screen.tsx +141 -0
  21. package/src/screens/data-explorer/data-explorer.tsx +271 -76
  22. package/src/screens/setup-user.tsx +5 -0
  23. package/src/system-apps/account.app.ts +7 -0
  24. package/src/system-apps/index.ts +3 -0
  25. package/docs/conversation-tab.md +0 -201
  26. package/docs/getting-started.md +0 -284
  27. package/docs/knowledge.md +0 -187
  28. package/docs/tabs-ui.md +0 -681
  29. package/docs/user-contacts-ui.md +0 -384
@@ -13,11 +13,19 @@ interface ITableInfo {
13
13
  bytes?: number;
14
14
  }
15
15
 
16
+ interface IDeletedTableInfo {
17
+ tableName: string;
18
+ tableId: string;
19
+ deletedAt: string;
20
+ }
21
+
16
22
  type DataExplorerElectronApi = {
17
23
  getCurrentContext: () => Promise<{ contextName: string }>;
18
24
  getAllTables: () => Promise<ITableInfo[]>;
19
25
  compactDatabase: (beforeTimestamp?: number) => Promise<void>;
20
26
  resetChangeTracking: () => Promise<void>;
27
+ deleteTable: (tableName: string) => Promise<void>;
28
+ getDeletedTables: () => Promise<IDeletedTableInfo[]>;
21
29
  };
22
30
 
23
31
  type WindowWithDataExplorer = Window & {
@@ -28,11 +36,112 @@ function getDataExplorerApi(): DataExplorerElectronApi | undefined {
28
36
  return (window as WindowWithDataExplorer).electronAPI?.dataExplorer;
29
37
  }
30
38
 
39
+ function formatRelativeTime(isoString: string): string {
40
+ const date = new Date(isoString);
41
+ const now = Date.now();
42
+ const diffMs = now - date.getTime();
43
+ const diffMin = Math.floor(diffMs / 60_000);
44
+ if (diffMin < 1) return "just now";
45
+ if (diffMin < 60) return `${diffMin}m ago`;
46
+ const diffHrs = Math.floor(diffMin / 60);
47
+ if (diffHrs < 24) return `${diffHrs}h ago`;
48
+ const diffDays = Math.floor(diffHrs / 24);
49
+ if (diffDays < 30) return `${diffDays}d ago`;
50
+ return date.toLocaleDateString();
51
+ }
52
+
53
+ function DeleteConfirmModal({
54
+ tableName,
55
+ onConfirm,
56
+ onCancel,
57
+ }: {
58
+ tableName: string;
59
+ onConfirm: () => void;
60
+ onCancel: () => void;
61
+ }) {
62
+ const [confirmText, setConfirmText] = useState("");
63
+ const [deleting, setDeleting] = useState(false);
64
+
65
+ const handleConfirm = async () => {
66
+ setDeleting(true);
67
+ try {
68
+ await onConfirm();
69
+ } finally {
70
+ setDeleting(false);
71
+ }
72
+ };
73
+
74
+ return (
75
+ <div
76
+ className="position-fixed top-0 start-0 w-100 h-100 d-flex align-items-center justify-content-center"
77
+ style={{ zIndex: 1050, backgroundColor: "rgba(0,0,0,0.5)" }}
78
+ onClick={onCancel}
79
+ >
80
+ <div
81
+ className="card shadow-lg"
82
+ style={{ maxWidth: 480, width: "100%" }}
83
+ onClick={(e) => e.stopPropagation()}
84
+ >
85
+ <div className="card-header bg-danger text-white d-flex justify-content-between align-items-center">
86
+ <strong>Delete Table</strong>
87
+ <button type="button" className="btn-close btn-close-white" onClick={onCancel} />
88
+ </div>
89
+ <div className="card-body">
90
+ <div className="alert alert-danger mb-3">
91
+ <i className="bi bi-exclamation-triangle-fill me-2" />
92
+ This will permanently delete all data for <strong>{tableName}</strong> on this device
93
+ and all synced devices once synced.
94
+ </div>
95
+ <label className="form-label">
96
+ Type <strong>{tableName}</strong> to confirm:
97
+ </label>
98
+ <input
99
+ type="text"
100
+ className="form-control"
101
+ value={confirmText}
102
+ onChange={(e) => setConfirmText(e.target.value)}
103
+ placeholder={tableName}
104
+ // biome-ignore lint/a11y/noAutofocus: confirmation input needs immediate focus
105
+ autoFocus
106
+ />
107
+ </div>
108
+ <div className="card-footer d-flex justify-content-end gap-2">
109
+ <button
110
+ type="button"
111
+ className="btn btn-secondary"
112
+ onClick={onCancel}
113
+ disabled={deleting}
114
+ >
115
+ Cancel
116
+ </button>
117
+ <button
118
+ type="button"
119
+ className="btn btn-danger"
120
+ disabled={confirmText !== tableName || deleting}
121
+ onClick={handleConfirm}
122
+ >
123
+ {deleting ? (
124
+ <>
125
+ <span className="spinner-border spinner-border-sm me-1" />
126
+ Deleting...
127
+ </>
128
+ ) : (
129
+ "Delete Table"
130
+ )}
131
+ </button>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ );
136
+ }
137
+
31
138
  export function DataExplorerList() {
32
139
  const [tables, setTables] = useState<ITableInfo[]>([]);
140
+ const [deletedTables, setDeletedTables] = useState<IDeletedTableInfo[]>([]);
33
141
  const [loading, setLoading] = useState(true);
34
142
  const [currentContext, setCurrentContext] = useState<string>("");
35
143
  const [compacting, setCompacting] = useState(false);
144
+ const [deleteTarget, setDeleteTarget] = useState<string | null>(null);
36
145
 
37
146
  const formatBytes = (bytes: number | undefined) => {
38
147
  if (bytes === undefined) return "-";
@@ -48,10 +157,10 @@ export function DataExplorerList() {
48
157
  const kb = 1024;
49
158
  const mb = kb * 1024;
50
159
 
51
- if (bytes < 10 * kb) return "text-secondary"; // 0 to 10KB: secondary (gray)
52
- if (bytes < 10 * mb) return "text-primary"; // 10KB to 10MB: primary (blue)
53
- if (bytes < 100 * mb) return "text-warning"; // 10MB to 100MB: warning (orange)
54
- return "text-danger"; // >100MB: danger (red)
160
+ if (bytes < 10 * kb) return "text-secondary";
161
+ if (bytes < 10 * mb) return "text-primary";
162
+ if (bytes < 100 * mb) return "text-warning";
163
+ return "text-danger";
55
164
  };
56
165
 
57
166
  const loadTables = useCallback(async () => {
@@ -64,22 +173,21 @@ export function DataExplorerList() {
64
173
  return;
65
174
  }
66
175
 
67
- // Get current context info
68
176
  const contextInfo = await api.getCurrentContext();
69
177
  setCurrentContext(contextInfo.contextName);
70
178
 
71
- // Get all tables from IPC
72
179
  const allTablesInfo = await api.getAllTables();
73
-
74
- // Sort by registered status first, then by table name
75
180
  allTablesInfo.sort((a: ITableInfo, b: ITableInfo) => {
76
181
  if (a.isRegistered !== b.isRegistered) {
77
182
  return a.isRegistered ? -1 : 1;
78
183
  }
79
184
  return a.tableName.localeCompare(b.tableName);
80
185
  });
81
-
82
186
  setTables(allTablesInfo);
187
+
188
+ const deleted = await api.getDeletedTables();
189
+ deleted.sort((a, b) => a.tableName.localeCompare(b.tableName));
190
+ setDeletedTables(deleted);
83
191
  } catch (error) {
84
192
  console.error("Error loading tables:", error);
85
193
  } finally {
@@ -87,6 +195,22 @@ export function DataExplorerList() {
87
195
  }
88
196
  }, []);
89
197
 
198
+ const handleDeleteTable = async () => {
199
+ if (!deleteTarget) return;
200
+ const api = getDataExplorerApi();
201
+ if (!api) return;
202
+
203
+ try {
204
+ await api.deleteTable(deleteTarget);
205
+ setDeleteTarget(null);
206
+ setLoading(true);
207
+ await loadTables();
208
+ } catch (error: unknown) {
209
+ console.error("Error deleting table:", error);
210
+ alert(`Error deleting table: ${error instanceof Error ? error.message : String(error)}`);
211
+ }
212
+ };
213
+
90
214
  const compactDatabase = async (beforeTimestamp?: number) => {
91
215
  try {
92
216
  setCompacting(true);
@@ -110,7 +234,6 @@ export function DataExplorerList() {
110
234
  await api.compactDatabase(beforeTimestamp);
111
235
  alert(`Database compacted successfully!${isExtreme ? " (Extreme mode)" : ""}`);
112
236
 
113
- // Reload tables to show updated sizes
114
237
  setLoading(true);
115
238
  await loadTables();
116
239
  } catch (error: unknown) {
@@ -141,7 +264,6 @@ export function DataExplorerList() {
141
264
  await api.resetChangeTracking();
142
265
  alert("Change tracking reset successfully!");
143
266
 
144
- // Reload tables to show updated sizes
145
267
  setLoading(true);
146
268
  await loadTables();
147
269
  } catch (error: unknown) {
@@ -180,8 +302,19 @@ export function DataExplorerList() {
180
302
  );
181
303
  }
182
304
 
305
+ const registeredTables = tables.filter((t) => t.isRegistered);
306
+ const unregisteredTables = tables.filter((t) => !t.isRegistered);
307
+
183
308
  return (
184
309
  <div className="container-fluid p-4">
310
+ {deleteTarget && (
311
+ <DeleteConfirmModal
312
+ tableName={deleteTarget}
313
+ onConfirm={handleDeleteTable}
314
+ onCancel={() => setDeleteTarget(null)}
315
+ />
316
+ )}
317
+
185
318
  {/* Navigation Tabs */}
186
319
  <ul className="nav nav-tabs mb-4">
187
320
  <li className="nav-item">
@@ -303,14 +436,14 @@ export function DataExplorerList() {
303
436
  <div className="card mb-4">
304
437
  <div className="card-body">
305
438
  <h5 className="card-title">
306
- Registered Tables ({tables.filter((t) => t.isRegistered).length})
439
+ Registered Tables ({registeredTables.length})
307
440
  <span className="badge bg-success ms-2">Active</span>
308
441
  </h5>
309
442
  <p className="text-muted small">
310
443
  These tables are registered with the TableContainer and actively used by the
311
444
  application.
312
445
  </p>
313
- {tables.filter((t) => t.isRegistered).length === 0 ? (
446
+ {registeredTables.length === 0 ? (
314
447
  <p className="text-muted">No registered tables found</p>
315
448
  ) : (
316
449
  <div className="table-responsive">
@@ -321,27 +454,36 @@ export function DataExplorerList() {
321
454
  <th>Table ID</th>
322
455
  <th className="text-end">Row Count</th>
323
456
  <th className="text-end">Bytes</th>
457
+ <th style={{ width: "1%" }}></th>
324
458
  </tr>
325
459
  </thead>
326
460
  <tbody>
327
- {tables
328
- .filter((t) => t.isRegistered)
329
- .map((table) => (
330
- <tr key={table.tableName}>
331
- <td>
332
- <strong>{table.tableName}</strong>
333
- </td>
334
- <td>
335
- <code className="text-muted small">{table.tableId || "system"}</code>
336
- </td>
337
- <td className="text-end">
338
- {table.rowCount !== undefined ? table.rowCount.toLocaleString() : "-"}
339
- </td>
340
- <td className={`text-end fw-bold ${getBytesColorClass(table.bytes)}`}>
341
- {formatBytes(table.bytes)}
342
- </td>
343
- </tr>
344
- ))}
461
+ {registeredTables.map((table) => (
462
+ <tr key={table.tableName}>
463
+ <td>
464
+ <strong>{table.tableName}</strong>
465
+ </td>
466
+ <td>
467
+ <code className="text-muted small">{table.tableId || "system"}</code>
468
+ </td>
469
+ <td className="text-end">
470
+ {table.rowCount !== undefined ? table.rowCount.toLocaleString() : "-"}
471
+ </td>
472
+ <td className={`text-end fw-bold ${getBytesColorClass(table.bytes)}`}>
473
+ {formatBytes(table.bytes)}
474
+ </td>
475
+ <td>
476
+ <button
477
+ type="button"
478
+ className="btn btn-sm btn-outline-danger border-0"
479
+ title={`Delete ${table.tableName}`}
480
+ onClick={() => setDeleteTarget(table.tableName)}
481
+ >
482
+ <i className="bi bi-trash" />
483
+ </button>
484
+ </td>
485
+ </tr>
486
+ ))}
345
487
  </tbody>
346
488
  </table>
347
489
  </div>
@@ -350,11 +492,11 @@ export function DataExplorerList() {
350
492
  </div>
351
493
 
352
494
  {/* Unregistered Tables (Raw SQLite) */}
353
- {tables.some((t) => !t.isRegistered) && (
354
- <div className="card">
495
+ {unregisteredTables.length > 0 && (
496
+ <div className="card mb-4">
355
497
  <div className="card-body">
356
498
  <h5 className="card-title">
357
- Unregistered Tables ({tables.filter((t) => !t.isRegistered).length})
499
+ Unregistered Tables ({unregisteredTables.length})
358
500
  <span className="badge bg-warning text-dark ms-2">Raw SQLite</span>
359
501
  </h5>
360
502
  <p className="text-muted small">
@@ -370,51 +512,104 @@ export function DataExplorerList() {
370
512
  <th className="text-end">Row Count</th>
371
513
  <th className="text-end">Bytes</th>
372
514
  <th>SQL Definition</th>
515
+ <th style={{ width: "1%" }}></th>
516
+ </tr>
517
+ </thead>
518
+ <tbody>
519
+ {unregisteredTables.map((table) => (
520
+ <tr key={table.tableName}>
521
+ <td>
522
+ <strong>{table.tableName}</strong>
523
+ </td>
524
+ <td>
525
+ <span className="badge bg-secondary">{table.type || "table"}</span>
526
+ </td>
527
+ <td className="text-end">
528
+ {table.rowCount !== undefined ? table.rowCount.toLocaleString() : "-"}
529
+ </td>
530
+ <td className={`text-end fw-bold ${getBytesColorClass(table.bytes)}`}>
531
+ {formatBytes(table.bytes)}
532
+ </td>
533
+ <td>
534
+ {table.sql ? (
535
+ <details>
536
+ <summary
537
+ className="cursor-pointer text-primary"
538
+ style={{ cursor: "pointer" }}
539
+ >
540
+ <small>View SQL</small>
541
+ </summary>
542
+ <pre
543
+ className="mt-2 p-2 bg-body-secondary border rounded"
544
+ style={{
545
+ fontSize: "0.75rem",
546
+ maxHeight: "300px",
547
+ overflow: "auto",
548
+ }}
549
+ >
550
+ <code className="text-body">{table.sql}</code>
551
+ </pre>
552
+ </details>
553
+ ) : (
554
+ <span className="text-muted small">-</span>
555
+ )}
556
+ </td>
557
+ <td>
558
+ <button
559
+ type="button"
560
+ className="btn btn-sm btn-outline-danger border-0"
561
+ title={`Delete ${table.tableName}`}
562
+ onClick={() => setDeleteTarget(table.tableName)}
563
+ >
564
+ <i className="bi bi-trash" />
565
+ </button>
566
+ </td>
567
+ </tr>
568
+ ))}
569
+ </tbody>
570
+ </table>
571
+ </div>
572
+ </div>
573
+ </div>
574
+ )}
575
+
576
+ {/* Deleted Tables */}
577
+ {deletedTables.length > 0 && (
578
+ <div className="card mb-4">
579
+ <div className="card-body">
580
+ <h5 className="card-title">
581
+ Deleted Tables ({deletedTables.length})
582
+ <span className="badge bg-danger ms-2">Deleted</span>
583
+ </h5>
584
+ <p className="text-muted small">
585
+ These tables have been deleted. Their definitions are retained so other devices know
586
+ to stop syncing data for them.
587
+ </p>
588
+ <div className="table-responsive">
589
+ <table className="table table-hover table-sm">
590
+ <thead>
591
+ <tr>
592
+ <th>Table Name</th>
593
+ <th>Table ID</th>
594
+ <th>Deleted At</th>
373
595
  </tr>
374
596
  </thead>
375
597
  <tbody>
376
- {tables
377
- .filter((t) => !t.isRegistered)
378
- .map((table) => (
379
- <tr key={table.tableName}>
380
- <td>
381
- <strong>{table.tableName}</strong>
382
- </td>
383
- <td>
384
- <span className="badge bg-secondary">{table.type || "table"}</span>
385
- </td>
386
- <td className="text-end">
387
- {table.rowCount !== undefined ? table.rowCount.toLocaleString() : "-"}
388
- </td>
389
- <td className={`text-end fw-bold ${getBytesColorClass(table.bytes)}`}>
390
- {formatBytes(table.bytes)}
391
- </td>
392
- <td>
393
- {table.sql ? (
394
- <details>
395
- <summary
396
- className="cursor-pointer text-primary"
397
- style={{ cursor: "pointer" }}
398
- >
399
- <small>View SQL</small>
400
- </summary>
401
- <pre
402
- className="mt-2 p-2 bg-body-secondary border rounded"
403
- style={{
404
- fontSize: "0.75rem",
405
- maxHeight: "300px",
406
- overflow: "auto",
407
- }}
408
- >
409
- <code className="text-body">{table.sql}</code>
410
- </pre>
411
- </details>
412
- ) : (
413
- <span className="text-muted small">-</span>
414
- )}
415
- </td>
416
- </tr>
417
- ))}
598
+ {deletedTables.map((table) => (
599
+ <tr key={table.tableId}>
600
+ <td>
601
+ <strong className="text-muted">{table.tableName}</strong>
602
+ </td>
603
+ <td>
604
+ <code className="text-muted small">{table.tableId}</code>
605
+ </td>
606
+ <td>
607
+ <span title={new Date(table.deletedAt).toLocaleString()}>
608
+ {formatRelativeTime(table.deletedAt)}
609
+ </span>
610
+ </td>
611
+ </tr>
612
+ ))}
418
613
  </tbody>
419
614
  </table>
420
615
  </div>
@@ -46,6 +46,11 @@ export const SetupUser = () => {
46
46
  console.error("Error auto-installing peers-core:", err);
47
47
  });
48
48
 
49
+ // Register with peers-services
50
+ await rpcServerCalls.registerWithPeersServices().catch((err: unknown) => {
51
+ console.warn("Auto-registration with peers-services deferred:", err);
52
+ });
53
+
49
54
  // Success - reload the app
50
55
  window.location.reload();
51
56
  } catch (err) {
@@ -0,0 +1,7 @@
1
+ import type { IAppNav } from "@peers-app/peers-sdk";
2
+
3
+ export const accountApp: IAppNav = {
4
+ name: "Account",
5
+ iconClassName: "bi-person-circle",
6
+ navigationPath: "account",
7
+ };
@@ -1,5 +1,6 @@
1
1
  import type { IAppNav, IPackage } from "@peers-app/peers-sdk";
2
2
 
3
+ export { accountApp } from "./account.app";
3
4
  export { assistantsApp } from "./assistants.app";
4
5
  export { consoleLogsApp } from "./console-logs.app";
5
6
  export { contactsApp } from "./contacts.app";
@@ -18,6 +19,7 @@ export { typesApp } from "./types.app";
18
19
  export { variablesApp } from "./variables.app";
19
20
  export { workflowsApp } from "./workflows.app";
20
21
 
22
+ import { accountApp } from "./account.app";
21
23
  import { assistantsApp } from "./assistants.app";
22
24
  import { consoleLogsApp } from "./console-logs.app";
23
25
  import { contactsApp } from "./contacts.app";
@@ -64,6 +66,7 @@ export const systemApps: IAppNav[] = [
64
66
  joinGroupApp,
65
67
 
66
68
  // User & Settings Apps
69
+ accountApp,
67
70
  settingsApp,
68
71
  // Mobile Settings (only in React Native)
69
72
  ...(isReactNative() ? [mobileSettingsApp] : []),