dbdiff-app 0.1.0

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 (69) hide show
  1. package/README.md +73 -0
  2. package/bin/cli.js +83 -0
  3. package/bin/install-local.js +57 -0
  4. package/electron/generate-icon.mjs +54 -0
  5. package/electron/icon.icns +0 -0
  6. package/electron/icon.png +0 -0
  7. package/electron/icon.svg +21 -0
  8. package/electron/main.js +169 -0
  9. package/electron/patch-dev-plist.js +31 -0
  10. package/electron/preload.cjs +18 -0
  11. package/electron/wait-for-vite.js +43 -0
  12. package/index.html +13 -0
  13. package/package.json +91 -0
  14. package/public/favicon.svg +15 -0
  15. package/public/vite.svg +1 -0
  16. package/server/export.ts +57 -0
  17. package/server/index.ts +392 -0
  18. package/src/App.css +1 -0
  19. package/src/App.tsx +543 -0
  20. package/src/assets/react.svg +1 -0
  21. package/src/components/CommandPalette.tsx +243 -0
  22. package/src/components/ConnectedView.tsx +78 -0
  23. package/src/components/ConnectionPicker.tsx +381 -0
  24. package/src/components/ConsoleView.tsx +360 -0
  25. package/src/components/CsvExportModal.tsx +144 -0
  26. package/src/components/DataGrid/DataGrid.tsx +262 -0
  27. package/src/components/DataGrid/DataGridCell.tsx +73 -0
  28. package/src/components/DataGrid/DataGridHeader.tsx +89 -0
  29. package/src/components/DataGrid/index.ts +20 -0
  30. package/src/components/DataGrid/types.ts +63 -0
  31. package/src/components/DataGrid/useColumnResize.ts +153 -0
  32. package/src/components/DataGrid/useDataGridSelection.ts +340 -0
  33. package/src/components/DataGrid/utils.ts +184 -0
  34. package/src/components/DatabaseMenu.tsx +93 -0
  35. package/src/components/DatabaseSwitcher.tsx +208 -0
  36. package/src/components/DiffView.tsx +215 -0
  37. package/src/components/EditConnectionModal.tsx +417 -0
  38. package/src/components/ErrorBoundary.tsx +69 -0
  39. package/src/components/GlobalShortcuts.tsx +201 -0
  40. package/src/components/InnerTabBar.tsx +129 -0
  41. package/src/components/JsonTreeViewer.tsx +387 -0
  42. package/src/components/MemberAccessEditor.tsx +443 -0
  43. package/src/components/MembersModal.tsx +446 -0
  44. package/src/components/NewConnectionModal.tsx +274 -0
  45. package/src/components/Resizer.tsx +66 -0
  46. package/src/components/ScanSuccessModal.tsx +113 -0
  47. package/src/components/ShortcutSettingsModal.tsx +318 -0
  48. package/src/components/Sidebar.tsx +532 -0
  49. package/src/components/TabBar.tsx +188 -0
  50. package/src/components/TableView.tsx +2147 -0
  51. package/src/components/ThemeToggle.tsx +44 -0
  52. package/src/components/index.ts +17 -0
  53. package/src/constants.ts +12 -0
  54. package/src/electron.d.ts +12 -0
  55. package/src/index.css +44 -0
  56. package/src/main.tsx +13 -0
  57. package/src/stores/hooks.ts +1146 -0
  58. package/src/stores/index.ts +12 -0
  59. package/src/stores/store.ts +1514 -0
  60. package/src/stores/useCloudSync.ts +274 -0
  61. package/src/stores/useSyncDatabase.ts +422 -0
  62. package/src/types.ts +277 -0
  63. package/src/utils/csv.ts +27 -0
  64. package/src/vite-env.d.ts +2 -0
  65. package/tsconfig.app.json +28 -0
  66. package/tsconfig.json +7 -0
  67. package/tsconfig.node.json +26 -0
  68. package/tsconfig.server.json +14 -0
  69. package/vite.config.ts +14 -0
@@ -0,0 +1,318 @@
1
+ import { useState, useEffect, useRef } from "react";
2
+ import type { ShortcutAction } from "../types";
3
+ import { useHotkey, formatShortcutDisplay } from "../stores/hooks";
4
+ import { useStore } from "../stores";
5
+
6
+ // Human-readable labels for each shortcut action
7
+ const SHORTCUT_LABELS: Record<ShortcutAction, string> = {
8
+ newConsole: "New Console",
9
+ closeInnerTab: "Close Tab",
10
+ nextInnerTab: "Next Tab",
11
+ prevInnerTab: "Previous Tab",
12
+ newConnectionTab: "New Connection Tab",
13
+ closeConnectionTab: "Close Connection Tab",
14
+ nextConnectionTab: "Next Connection",
15
+ prevConnectionTab: "Previous Connection",
16
+ runQuery: "Run Query",
17
+ closeModal: "Close Modal",
18
+ openTableSwitcher: "Switch Table",
19
+ openDatabaseSwitcher: "Switch Database",
20
+ deleteRows: "Delete Rows",
21
+ selectAll: "Select All",
22
+ refreshTable: "Refresh Table",
23
+ };
24
+
25
+ // Default shortcuts (keep in sync with store.ts BROWSER_SHORTCUTS)
26
+ const DEFAULT_SHORTCUTS: Record<ShortcutAction, string> = {
27
+ newConsole: "alt+t",
28
+ closeInnerTab: "alt+w",
29
+ nextInnerTab: "alt+tab",
30
+ prevInnerTab: "alt+shift+tab",
31
+ newConnectionTab: "mod+alt+n",
32
+ closeConnectionTab: "mod+alt+w",
33
+ prevConnectionTab: "mod+alt+j",
34
+ nextConnectionTab: "mod+alt+k",
35
+ runQuery: "ctrl+enter",
36
+ closeModal: "escape",
37
+ openTableSwitcher: "mod+o",
38
+ openDatabaseSwitcher: "mod+p",
39
+ deleteRows: "delete",
40
+ selectAll: "mod+a",
41
+ refreshTable: "mod+r",
42
+ };
43
+
44
+ const ALL_ACTIONS: ShortcutAction[] = [
45
+ "newConsole",
46
+ "closeInnerTab",
47
+ "nextInnerTab",
48
+ "prevInnerTab",
49
+ "newConnectionTab",
50
+ "closeConnectionTab",
51
+ "nextConnectionTab",
52
+ "prevConnectionTab",
53
+ "runQuery",
54
+ "closeModal",
55
+ "openTableSwitcher",
56
+ "openDatabaseSwitcher",
57
+ "deleteRows",
58
+ "selectAll",
59
+ "refreshTable",
60
+ ];
61
+
62
+ interface ShortcutSettingsModalProps {
63
+ onClose: () => void;
64
+ }
65
+
66
+ interface ShortcutRowProps {
67
+ action: ShortcutAction;
68
+ currentShortcut: string;
69
+ isCustom: boolean;
70
+ onEdit: (action: ShortcutAction) => void;
71
+ onReset: (action: ShortcutAction) => void;
72
+ }
73
+
74
+ function ShortcutRow({
75
+ action,
76
+ currentShortcut,
77
+ isCustom,
78
+ onEdit,
79
+ onReset,
80
+ }: ShortcutRowProps) {
81
+ const defaultShortcut = DEFAULT_SHORTCUTS[action];
82
+
83
+ return (
84
+ <div className="flex items-center justify-between py-2.5 border-b border-stone-100 dark:border-white/5 last:border-b-0">
85
+ <div className="flex-1">
86
+ <div className="text-[14px] text-primary font-medium">
87
+ {SHORTCUT_LABELS[action]}
88
+ </div>
89
+ {isCustom && (
90
+ <div className="text-[11px] text-tertiary mt-0.5">
91
+ Default: {formatShortcutDisplay(defaultShortcut)}
92
+ </div>
93
+ )}
94
+ </div>
95
+ <div className="flex items-center gap-2">
96
+ <button
97
+ onClick={() => onEdit(action)}
98
+ className={`px-3 py-1.5 text-[13px] font-mono rounded-md transition-colors ${
99
+ isCustom
100
+ ? "bg-blue-500/10 text-blue-600 dark:text-blue-400 hover:bg-blue-500/20"
101
+ : "bg-stone-100 dark:bg-white/5 text-secondary hover:bg-stone-200 dark:hover:bg-white/10"
102
+ }`}
103
+ >
104
+ {formatShortcutDisplay(currentShortcut)}
105
+ </button>
106
+ {isCustom && (
107
+ <button
108
+ onClick={() => onReset(action)}
109
+ className="p-1.5 text-tertiary hover:text-secondary transition-colors"
110
+ title="Reset to default"
111
+ >
112
+ <svg
113
+ className="w-4 h-4"
114
+ fill="none"
115
+ viewBox="0 0 24 24"
116
+ stroke="currentColor"
117
+ strokeWidth={2}
118
+ >
119
+ <path
120
+ strokeLinecap="round"
121
+ strokeLinejoin="round"
122
+ d="M6 18L18 6M6 6l12 12"
123
+ />
124
+ </svg>
125
+ </button>
126
+ )}
127
+ </div>
128
+ </div>
129
+ );
130
+ }
131
+
132
+ interface RecordingOverlayProps {
133
+ action: ShortcutAction;
134
+ onCancel: () => void;
135
+ onSave: (keys: string) => void;
136
+ }
137
+
138
+ function RecordingOverlay({ action, onCancel, onSave }: RecordingOverlayProps) {
139
+ const [recordedKeys, setRecordedKeys] = useState<string | null>(null);
140
+ const overlayRef = useRef<HTMLDivElement>(null);
141
+
142
+ useEffect(() => {
143
+ const handleKeyDown = (e: KeyboardEvent) => {
144
+ e.preventDefault();
145
+ e.stopPropagation();
146
+
147
+ // Ignore lone modifier keys
148
+ if (["Control", "Alt", "Shift", "Meta"].includes(e.key)) {
149
+ return;
150
+ }
151
+
152
+ // Build the shortcut string
153
+ const parts: string[] = [];
154
+ if (e.ctrlKey || e.metaKey) parts.push("ctrl");
155
+ if (e.altKey) parts.push("alt");
156
+ if (e.shiftKey) parts.push("shift");
157
+
158
+ // Handle special keys — use e.code for Alt+letter on macOS
159
+ let key = e.key.toLowerCase();
160
+ if (e.altKey && e.code.startsWith("Key")) {
161
+ key = e.code.slice(3).toLowerCase();
162
+ }
163
+ if (key === " ") key = "space";
164
+ if (key === "escape") {
165
+ // If pressing escape with no modifiers, cancel
166
+ if (parts.length === 0) {
167
+ onCancel();
168
+ return;
169
+ }
170
+ }
171
+
172
+ parts.push(key);
173
+ setRecordedKeys(parts.join("+"));
174
+ };
175
+
176
+ window.addEventListener("keydown", handleKeyDown, true);
177
+ return () => window.removeEventListener("keydown", handleKeyDown, true);
178
+ }, [onCancel]);
179
+
180
+ return (
181
+ <div
182
+ ref={overlayRef}
183
+ className="fixed inset-0 z-[60] flex items-center justify-center bg-black/60 backdrop-blur-sm"
184
+ >
185
+ <div className="bg-white dark:bg-[#1a1a1a] rounded-xl shadow-2xl p-6 w-full max-w-sm mx-4 border border-stone-200 dark:border-white/10">
186
+ <h3 className="text-[16px] font-semibold text-primary mb-2">
187
+ Set shortcut for "{SHORTCUT_LABELS[action]}"
188
+ </h3>
189
+ <p className="text-[13px] text-secondary mb-6">
190
+ Press the key combination you want to use, or Esc to cancel.
191
+ </p>
192
+
193
+ <div className="flex items-center justify-center py-8 mb-6 bg-stone-50 dark:bg-white/5 rounded-lg border border-stone-200 dark:border-white/10">
194
+ {recordedKeys ? (
195
+ <span className="text-[20px] font-mono text-primary">
196
+ {formatShortcutDisplay(recordedKeys)}
197
+ </span>
198
+ ) : (
199
+ <span className="text-[14px] text-tertiary">
200
+ Waiting for input...
201
+ </span>
202
+ )}
203
+ </div>
204
+
205
+ <div className="flex gap-3">
206
+ <button
207
+ onClick={onCancel}
208
+ className="flex-1 px-4 py-2.5 text-[14px] font-medium text-secondary bg-stone-100 dark:bg-white/5 hover:bg-stone-200 dark:hover:bg-white/10 rounded-lg transition-colors"
209
+ >
210
+ Cancel
211
+ </button>
212
+ <button
213
+ onClick={() => recordedKeys && onSave(recordedKeys)}
214
+ disabled={!recordedKeys}
215
+ className="flex-1 px-4 py-2.5 text-[14px] font-medium text-white bg-blue-600 hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed rounded-lg transition-colors"
216
+ >
217
+ Save
218
+ </button>
219
+ </div>
220
+ </div>
221
+ </div>
222
+ );
223
+ }
224
+
225
+ export function ShortcutSettingsModal({ onClose }: ShortcutSettingsModalProps) {
226
+ const shortcutOverrides = useStore((s) => s.shortcutOverrides);
227
+ const setShortcut = useStore((s) => s.setShortcut);
228
+ const resetShortcut = useStore((s) => s.resetShortcut);
229
+ const resetAllShortcuts = useStore((s) => s.resetAllShortcuts);
230
+
231
+ const [editingAction, setEditingAction] = useState<ShortcutAction | null>(
232
+ null,
233
+ );
234
+
235
+ // Only use escape to close when not recording a shortcut
236
+ useHotkey("closeModal", () => {
237
+ if (!editingAction) {
238
+ onClose();
239
+ }
240
+ });
241
+
242
+ function getCurrentShortcut(action: ShortcutAction): string {
243
+ return shortcutOverrides[action] ?? DEFAULT_SHORTCUTS[action];
244
+ }
245
+
246
+ function isCustom(action: ShortcutAction): boolean {
247
+ return action in shortcutOverrides;
248
+ }
249
+
250
+ function handleSaveShortcut(keys: string) {
251
+ if (editingAction) {
252
+ setShortcut(editingAction, keys);
253
+ setEditingAction(null);
254
+ }
255
+ }
256
+
257
+ const hasAnyCustom = ALL_ACTIONS.some((action) => isCustom(action));
258
+
259
+ return (
260
+ <>
261
+ <div className="fixed inset-0 z-50 flex items-center justify-center">
262
+ <div
263
+ className="absolute inset-0 bg-black/50 backdrop-blur-sm"
264
+ onClick={onClose}
265
+ />
266
+ <div className="relative bg-white dark:bg-[#1a1a1a] rounded-xl shadow-2xl w-full max-w-md mx-4 border border-stone-200 dark:border-white/10 max-h-[80vh] flex flex-col">
267
+ <div className="p-6 pb-0 shrink-0">
268
+ <div className="flex items-center justify-between mb-6">
269
+ <h2 className="text-[18px] font-semibold text-primary">
270
+ Keyboard Shortcuts
271
+ </h2>
272
+ {hasAnyCustom && (
273
+ <button
274
+ onClick={resetAllShortcuts}
275
+ className="text-[12px] text-tertiary hover:text-secondary transition-colors"
276
+ >
277
+ Reset All
278
+ </button>
279
+ )}
280
+ </div>
281
+ </div>
282
+
283
+ <div className="px-6 pb-4 overflow-y-auto min-h-0">
284
+ <div className="divide-y divide-stone-100 dark:divide-white/5">
285
+ {ALL_ACTIONS.map((action) => (
286
+ <ShortcutRow
287
+ key={action}
288
+ action={action}
289
+ currentShortcut={getCurrentShortcut(action)}
290
+ isCustom={isCustom(action)}
291
+ onEdit={setEditingAction}
292
+ onReset={resetShortcut}
293
+ />
294
+ ))}
295
+ </div>
296
+ </div>
297
+
298
+ <div className="p-6 pt-4 border-t border-stone-200 dark:border-white/10 shrink-0">
299
+ <button
300
+ onClick={onClose}
301
+ className="w-full px-4 py-2.5 text-[14px] font-medium text-secondary bg-stone-100 dark:bg-white/5 hover:bg-stone-200 dark:hover:bg-white/10 rounded-lg transition-colors"
302
+ >
303
+ Done
304
+ </button>
305
+ </div>
306
+ </div>
307
+ </div>
308
+
309
+ {editingAction && (
310
+ <RecordingOverlay
311
+ action={editingAction}
312
+ onCancel={() => setEditingAction(null)}
313
+ onSave={handleSaveShortcut}
314
+ />
315
+ )}
316
+ </>
317
+ );
318
+ }