@humbdb/ui 0.0.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/dist/index.js ADDED
@@ -0,0 +1,1221 @@
1
+ // src/components/console-log.tsx
2
+ import { Trash2 } from "lucide-react";
3
+ import { jsx, jsxs } from "react/jsx-runtime";
4
+ var LEVEL_COLOR = {
5
+ info: void 0,
6
+ warn: "var(--c-amber)",
7
+ error: "var(--c-red)"
8
+ };
9
+ function formatTime(timestamp) {
10
+ return new Date(timestamp).toLocaleTimeString("en-US", { hour12: false });
11
+ }
12
+ function ConsoleLog({ events, onClear }) {
13
+ return /* @__PURE__ */ jsxs(
14
+ "div",
15
+ {
16
+ "data-testid": "console-log",
17
+ className: "flex h-full flex-col overflow-hidden rounded-[3px] border border-border",
18
+ children: [
19
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-2 border-b border-border px-3 py-2", children: [
20
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[9px] uppercase tracking-widest text-muted-foreground", children: "Console" }),
21
+ /* @__PURE__ */ jsxs(
22
+ "button",
23
+ {
24
+ type: "button",
25
+ onClick: onClear,
26
+ className: "ml-auto flex items-center gap-1 rounded-[3px] px-2 py-1 text-[11px] text-muted-foreground hover:bg-accent",
27
+ children: [
28
+ /* @__PURE__ */ jsx(Trash2, { className: "h-2.5 w-2.5" }),
29
+ "Clear"
30
+ ]
31
+ }
32
+ )
33
+ ] }),
34
+ /* @__PURE__ */ jsxs("div", { className: "flex-1 space-y-0.5 overflow-auto p-3", children: [
35
+ events.length === 0 ? /* @__PURE__ */ jsx("p", { className: "font-mono text-[11px] text-muted-foreground/50", children: "No events yet." }) : events.map((event) => /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3 py-0.5 font-mono text-[11px]", children: [
36
+ /* @__PURE__ */ jsx("span", { className: "shrink-0 tabular-nums text-muted-foreground/30", children: formatTime(event.timestamp) }),
37
+ /* @__PURE__ */ jsx(
38
+ "span",
39
+ {
40
+ className: "w-9 shrink-0 text-right text-[10px]",
41
+ style: { color: LEVEL_COLOR[event.level] },
42
+ children: event.level === "info" ? /* @__PURE__ */ jsx("span", { className: "text-muted-foreground/40", children: event.level }) : event.level
43
+ }
44
+ ),
45
+ /* @__PURE__ */ jsx(
46
+ "span",
47
+ {
48
+ className: event.level === "info" ? "text-foreground/65" : "",
49
+ style: { color: LEVEL_COLOR[event.level] },
50
+ children: event.message
51
+ }
52
+ )
53
+ ] }, event.id)),
54
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 pt-2", children: [
55
+ /* @__PURE__ */ jsx("span", { className: "font-mono text-[11px]", style: { color: "var(--c-green)" }, children: "\u276F" }),
56
+ /* @__PURE__ */ jsx("span", { className: "animate-pulse font-mono text-[11px] text-muted-foreground/25", children: "\u2588" })
57
+ ] })
58
+ ] })
59
+ ]
60
+ }
61
+ );
62
+ }
63
+
64
+ // src/components/files-browser.tsx
65
+ import { File, FileCode2, FolderOpen } from "lucide-react";
66
+ import { useState } from "react";
67
+
68
+ // src/cn.ts
69
+ import { clsx } from "clsx";
70
+ import { twMerge } from "tailwind-merge";
71
+ function cn(...inputs) {
72
+ return twMerge(clsx(inputs));
73
+ }
74
+
75
+ // src/components/spinner.tsx
76
+ import { Loader2 } from "lucide-react";
77
+ import { jsx as jsx2 } from "react/jsx-runtime";
78
+ function Spinner({ className }) {
79
+ return /* @__PURE__ */ jsx2(
80
+ Loader2,
81
+ {
82
+ className: cn("h-3 w-3 animate-spin text-muted-foreground", className),
83
+ "aria-hidden": "true"
84
+ }
85
+ );
86
+ }
87
+
88
+ // src/components/files-browser.tsx
89
+ import { jsx as jsx3, jsxs as jsxs2 } from "react/jsx-runtime";
90
+ function TreeRow({
91
+ node,
92
+ depth,
93
+ selectedPath,
94
+ onSelectFile
95
+ }) {
96
+ const [open, setOpen] = useState(depth < 1);
97
+ const isSelected = node.type === "file" && node.path === selectedPath;
98
+ return /* @__PURE__ */ jsxs2("div", { children: [
99
+ /* @__PURE__ */ jsxs2(
100
+ "div",
101
+ {
102
+ role: node.type === "file" ? "button" : void 0,
103
+ "aria-pressed": node.type === "file" ? isSelected : void 0,
104
+ className: cn(
105
+ "mx-1 flex cursor-pointer select-none items-center gap-1.5 rounded-[2px] py-[3px] pr-2 hover:bg-sidebar-accent",
106
+ isSelected && "bg-primary/10"
107
+ ),
108
+ style: { paddingLeft: `${8 + depth * 14}px` },
109
+ onClick: () => {
110
+ if (node.type === "directory") setOpen((current) => !current);
111
+ else onSelectFile(node.path);
112
+ },
113
+ children: [
114
+ node.type === "directory" ? /* @__PURE__ */ jsx3(
115
+ "svg",
116
+ {
117
+ viewBox: "0 0 24 24",
118
+ className: cn(
119
+ "h-2.5 w-2.5 shrink-0 text-muted-foreground/60 transition-transform",
120
+ open && "rotate-90"
121
+ ),
122
+ fill: "none",
123
+ stroke: "currentColor",
124
+ strokeWidth: 2,
125
+ children: /* @__PURE__ */ jsx3("path", { d: "m9 18 6-6-6-6", strokeLinecap: "round", strokeLinejoin: "round" })
126
+ }
127
+ ) : /* @__PURE__ */ jsx3("span", { className: "w-2.5 shrink-0" }),
128
+ node.type === "directory" ? /* @__PURE__ */ jsx3(FolderOpen, { className: "h-3 w-3 shrink-0", style: { color: "var(--c-amber)" } }) : /* @__PURE__ */ jsx3(File, { className: "h-3 w-3 shrink-0", style: { color: "var(--c-blue)" } }),
129
+ /* @__PURE__ */ jsx3(
130
+ "span",
131
+ {
132
+ className: cn(
133
+ "truncate font-mono text-[11px] text-foreground/70",
134
+ isSelected ? "text-foreground" : "hover:text-foreground"
135
+ ),
136
+ children: node.name
137
+ }
138
+ )
139
+ ]
140
+ }
141
+ ),
142
+ node.type === "directory" && open && /* @__PURE__ */ jsx3("div", { children: node.children?.map((child) => /* @__PURE__ */ jsx3(
143
+ TreeRow,
144
+ {
145
+ node: child,
146
+ depth: depth + 1,
147
+ selectedPath,
148
+ onSelectFile
149
+ },
150
+ child.path
151
+ )) })
152
+ ] });
153
+ }
154
+ function FilesBrowser({
155
+ tree,
156
+ selectedPath,
157
+ onSelectFile,
158
+ content,
159
+ isContentLoading,
160
+ contentError
161
+ }) {
162
+ const lines = content?.split("\n") ?? [];
163
+ return /* @__PURE__ */ jsxs2(
164
+ "div",
165
+ {
166
+ "data-testid": "files-browser",
167
+ className: "flex h-full overflow-hidden rounded-[3px] border border-border",
168
+ children: [
169
+ /* @__PURE__ */ jsx3("nav", { className: "w-48 shrink-0 overflow-y-auto border-r border-border bg-sidebar py-1", children: tree.map((node) => /* @__PURE__ */ jsx3(
170
+ TreeRow,
171
+ {
172
+ node,
173
+ depth: 0,
174
+ selectedPath,
175
+ onSelectFile
176
+ },
177
+ node.path
178
+ )) }),
179
+ /* @__PURE__ */ jsxs2("div", { className: "flex min-w-0 flex-1 flex-col overflow-hidden", children: [
180
+ /* @__PURE__ */ jsxs2("div", { className: "flex shrink-0 items-center gap-2 border-b border-border bg-card px-3 py-2", children: [
181
+ /* @__PURE__ */ jsx3(FileCode2, { className: "h-3 w-3 shrink-0", style: { color: "var(--c-blue)" } }),
182
+ /* @__PURE__ */ jsx3("span", { className: "truncate font-mono text-[11px] text-foreground", children: selectedPath ?? "Select a file" })
183
+ ] }),
184
+ isContentLoading ? /* @__PURE__ */ jsxs2("p", { className: "flex items-center gap-1.5 p-3 text-[13px] text-muted-foreground", children: [
185
+ /* @__PURE__ */ jsx3(Spinner, {}),
186
+ " Loading file..."
187
+ ] }) : contentError ? /* @__PURE__ */ jsx3("p", { className: "p-3 text-[13px] text-muted-foreground", children: contentError }) : content === void 0 ? /* @__PURE__ */ jsx3("p", { className: "p-3 font-mono text-[11px] text-muted-foreground", children: "No file selected." }) : /* @__PURE__ */ jsxs2("div", { className: "flex flex-1 overflow-auto", children: [
188
+ /* @__PURE__ */ jsx3(
189
+ "div",
190
+ {
191
+ "aria-hidden": "true",
192
+ className: "shrink-0 select-none border-r border-border bg-background pr-3 pt-3 text-right font-mono text-[11px] text-muted-foreground/30",
193
+ style: { minWidth: "44px" },
194
+ children: lines.map((_, index) => /* @__PURE__ */ jsx3("div", { style: { lineHeight: "20px" }, children: index + 1 }, index))
195
+ }
196
+ ),
197
+ /* @__PURE__ */ jsx3("pre", { className: "flex-1 whitespace-pre p-3 font-mono text-[12px] leading-5 text-foreground/80", children: content })
198
+ ] })
199
+ ] })
200
+ ]
201
+ }
202
+ );
203
+ }
204
+
205
+ // src/components/table-detail.tsx
206
+ import { Link, Table2 } from "lucide-react";
207
+
208
+ // src/components/type-icon.tsx
209
+ import { Calendar, Hash, ToggleLeft, Type } from "lucide-react";
210
+ import { jsx as jsx4 } from "react/jsx-runtime";
211
+ function classify(dataType) {
212
+ const type = dataType.toLowerCase();
213
+ if (type.startsWith("int") || type.startsWith("numeric") || type.startsWith("decimal") || type.startsWith("float") || type.startsWith("double") || type.startsWith("real") || type.startsWith("serial")) {
214
+ return "numeric";
215
+ }
216
+ if (type.startsWith("bool")) return "boolean";
217
+ if (type.startsWith("timestamp") || type.startsWith("date") || type.startsWith("time")) {
218
+ return "datetime";
219
+ }
220
+ if (type.startsWith("char") || type.startsWith("varchar") || type.startsWith("text") || type.startsWith("uuid") || type.startsWith("json")) {
221
+ return "text";
222
+ }
223
+ return "other";
224
+ }
225
+ function TypeIcon({ dataType }) {
226
+ switch (classify(dataType)) {
227
+ case "numeric":
228
+ return /* @__PURE__ */ jsx4(Hash, { className: "h-2.5 w-2.5 shrink-0", style: { color: "var(--c-amber)" } });
229
+ case "text":
230
+ return /* @__PURE__ */ jsx4(Type, { className: "h-2.5 w-2.5 shrink-0", style: { color: "var(--c-blue)" } });
231
+ case "boolean":
232
+ return /* @__PURE__ */ jsx4(ToggleLeft, { className: "h-2.5 w-2.5 shrink-0", style: { color: "var(--c-green)" } });
233
+ case "datetime":
234
+ return /* @__PURE__ */ jsx4(Calendar, { className: "h-2.5 w-2.5 shrink-0", style: { color: "var(--c-purple)" } });
235
+ default:
236
+ return /* @__PURE__ */ jsx4(Hash, { className: "h-2.5 w-2.5 shrink-0 text-muted-foreground" });
237
+ }
238
+ }
239
+
240
+ // src/components/table-detail.tsx
241
+ import { jsx as jsx5, jsxs as jsxs3 } from "react/jsx-runtime";
242
+ function TableDetail({ table }) {
243
+ return /* @__PURE__ */ jsxs3(
244
+ "div",
245
+ {
246
+ "data-testid": "table-detail",
247
+ className: "max-w-xl overflow-hidden rounded-[3px] border border-border",
248
+ children: [
249
+ /* @__PURE__ */ jsxs3("div", { className: "flex items-center gap-2 border-b border-border bg-card px-3 py-2", children: [
250
+ /* @__PURE__ */ jsx5(Table2, { className: "h-3 w-3 shrink-0", style: { color: "var(--c-blue)" } }),
251
+ /* @__PURE__ */ jsx5("span", { className: "font-mono text-[12px] font-medium text-foreground", children: table.name }),
252
+ table.rowCount !== void 0 && /* @__PURE__ */ jsxs3("span", { className: "ml-auto font-mono text-[10px] text-muted-foreground", children: [
253
+ "~",
254
+ table.rowCount.toLocaleString(),
255
+ " row",
256
+ table.rowCount === 1 ? "" : "s"
257
+ ] })
258
+ ] }),
259
+ /* @__PURE__ */ jsx5("div", { className: "bg-background", children: table.columns.map((column, index) => /* @__PURE__ */ jsxs3(
260
+ "div",
261
+ {
262
+ className: "flex items-center gap-2 px-3 py-1.5 font-mono text-[11px] hover:bg-accent/50" + (index !== 0 ? " border-t border-border-subtle" : ""),
263
+ children: [
264
+ /* @__PURE__ */ jsx5(TypeIcon, { dataType: column.dataType }),
265
+ /* @__PURE__ */ jsx5(
266
+ "span",
267
+ {
268
+ className: column.isPrimaryKey || column.isForeignKey ? "" : "text-foreground/80",
269
+ style: {
270
+ color: column.isPrimaryKey ? "var(--c-amber)" : column.isForeignKey ? "var(--c-blue)" : void 0
271
+ },
272
+ children: column.name
273
+ }
274
+ ),
275
+ column.isPrimaryKey && /* @__PURE__ */ jsx5(
276
+ "span",
277
+ {
278
+ className: "rounded-[2px] border px-1 text-[8px]",
279
+ style: {
280
+ color: "var(--c-amber)",
281
+ borderColor: "color-mix(in srgb, var(--c-amber) 30%, transparent)"
282
+ },
283
+ children: "PK"
284
+ }
285
+ ),
286
+ column.isForeignKey && /* @__PURE__ */ jsxs3(
287
+ "span",
288
+ {
289
+ className: "flex items-center gap-0.5 rounded-[2px] border px-1 text-[8px]",
290
+ style: {
291
+ color: "var(--c-blue)",
292
+ borderColor: "color-mix(in srgb, var(--c-blue) 30%, transparent)"
293
+ },
294
+ children: [
295
+ /* @__PURE__ */ jsx5(Link, { className: "h-2 w-2" }),
296
+ "FK"
297
+ ]
298
+ }
299
+ ),
300
+ /* @__PURE__ */ jsx5("span", { className: "ml-auto text-[9px] text-muted-foreground/40", children: column.dataType }),
301
+ column.nullable && /* @__PURE__ */ jsx5("span", { className: "text-[9px] text-muted-foreground/30", children: "null" })
302
+ ]
303
+ },
304
+ column.name
305
+ )) }),
306
+ table.indexes && table.indexes.length > 0 && /* @__PURE__ */ jsx5("div", { className: "border-t border-border bg-card px-3 py-2", children: /* @__PURE__ */ jsx5("ul", { className: "space-y-0.5 font-mono text-[10px] text-muted-foreground", children: table.indexes.map((index) => /* @__PURE__ */ jsxs3("li", { children: [
307
+ index.name,
308
+ " (",
309
+ index.columns.join(", "),
310
+ ")",
311
+ index.primary ? " \xB7 primary key" : index.unique ? " \xB7 unique" : ""
312
+ ] }, index.name)) }) })
313
+ ]
314
+ }
315
+ );
316
+ }
317
+
318
+ // src/components/schema-grid.tsx
319
+ import { jsx as jsx6 } from "react/jsx-runtime";
320
+ function SchemaGrid({ tables }) {
321
+ return /* @__PURE__ */ jsx6(
322
+ "div",
323
+ {
324
+ "data-testid": "schema-grid",
325
+ className: "grid gap-3",
326
+ style: { gridTemplateColumns: "repeat(auto-fill, minmax(260px, 1fr))" },
327
+ children: tables.map((table) => /* @__PURE__ */ jsx6(TableDetail, { table }, `${table.schema}.${table.name}`))
328
+ }
329
+ );
330
+ }
331
+
332
+ // src/components/schema-tree.tsx
333
+ import { Circle, FolderOpen as FolderOpen2, Table2 as Table22 } from "lucide-react";
334
+ import { useMemo, useState as useState2 } from "react";
335
+ import { Fragment, jsx as jsx7, jsxs as jsxs4 } from "react/jsx-runtime";
336
+ var STATUS_DOT_COLOR = {
337
+ connected: "var(--c-green)",
338
+ disconnected: "var(--c-red)",
339
+ unconfigured: "rgb(var(--muted-foreground))"
340
+ };
341
+ function buildTree(target, schemas) {
342
+ return {
343
+ id: "connection",
344
+ name: target ?? "not connected",
345
+ type: "connection",
346
+ children: schemas.map((schema) => ({
347
+ id: `schema:${schema.name}`,
348
+ name: schema.name,
349
+ type: "schema",
350
+ children: schema.tables.map((table) => ({
351
+ id: `table:${schema.name}:${table}`,
352
+ name: table,
353
+ type: "table",
354
+ schema: schema.name
355
+ }))
356
+ }))
357
+ };
358
+ }
359
+ function collectMatchIds(node, query, ancestors = []) {
360
+ const result = /* @__PURE__ */ new Set();
361
+ const path = [...ancestors, node.id];
362
+ if (node.name.toLowerCase().includes(query.toLowerCase())) {
363
+ for (const id of path) result.add(id);
364
+ }
365
+ for (const child of node.children ?? []) {
366
+ for (const id of collectMatchIds(child, query, path)) result.add(id);
367
+ }
368
+ return result;
369
+ }
370
+ function highlight(text, query) {
371
+ if (!query) return text;
372
+ const index = text.toLowerCase().indexOf(query.toLowerCase());
373
+ if (index === -1) return text;
374
+ return /* @__PURE__ */ jsxs4(Fragment, { children: [
375
+ text.slice(0, index),
376
+ /* @__PURE__ */ jsx7("mark", { className: "rounded-[1px] bg-[color-mix(in_srgb,var(--c-blue)_25%,transparent)] text-[var(--c-blue)]", children: text.slice(index, index + query.length) }),
377
+ text.slice(index + query.length)
378
+ ] });
379
+ }
380
+ function TreeRow2({
381
+ node,
382
+ depth,
383
+ query,
384
+ matchIds,
385
+ selected,
386
+ onSelect,
387
+ status
388
+ }) {
389
+ const [manualOpen, setManualOpen] = useState2(depth < 2);
390
+ const forceOpen = query.length > 0 && matchIds.has(node.id);
391
+ const open = query.length > 0 ? forceOpen : manualOpen;
392
+ if (query.length > 0 && !matchIds.has(node.id)) return null;
393
+ const hasChildren = Boolean(node.children && node.children.length > 0);
394
+ const isSelected = node.type === "table" && selected?.schema === node.schema && selected?.table === node.name;
395
+ return /* @__PURE__ */ jsxs4("div", { children: [
396
+ /* @__PURE__ */ jsxs4(
397
+ "div",
398
+ {
399
+ role: node.type === "table" ? "button" : void 0,
400
+ "aria-pressed": node.type === "table" ? isSelected : void 0,
401
+ className: cn(
402
+ "mx-1 flex cursor-pointer select-none items-center gap-1.5 rounded-[2px] py-[3px] pr-2 hover:bg-sidebar-accent",
403
+ isSelected && "bg-primary/10"
404
+ ),
405
+ style: { paddingLeft: `${8 + depth * 14}px` },
406
+ onClick: () => {
407
+ if (hasChildren) setManualOpen((current) => !current);
408
+ if (node.type === "table" && node.schema) onSelect(node.schema, node.name);
409
+ },
410
+ children: [
411
+ hasChildren ? /* @__PURE__ */ jsx7(
412
+ "svg",
413
+ {
414
+ viewBox: "0 0 24 24",
415
+ className: cn(
416
+ "h-2.5 w-2.5 shrink-0 text-muted-foreground/60 transition-transform",
417
+ open && "rotate-90"
418
+ ),
419
+ fill: "none",
420
+ stroke: "currentColor",
421
+ strokeWidth: 2,
422
+ children: /* @__PURE__ */ jsx7("path", { d: "m9 18 6-6-6-6", strokeLinecap: "round", strokeLinejoin: "round" })
423
+ }
424
+ ) : /* @__PURE__ */ jsx7("span", { className: "w-2.5 shrink-0" }),
425
+ node.type === "connection" && /* @__PURE__ */ jsx7(
426
+ Circle,
427
+ {
428
+ className: "h-1.5 w-1.5 shrink-0",
429
+ fill: STATUS_DOT_COLOR[status],
430
+ style: { color: STATUS_DOT_COLOR[status] }
431
+ }
432
+ ),
433
+ node.type === "schema" && /* @__PURE__ */ jsx7(FolderOpen2, { className: "h-3 w-3 shrink-0", style: { color: "var(--c-amber)" } }),
434
+ node.type === "table" && /* @__PURE__ */ jsx7(Table22, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
435
+ /* @__PURE__ */ jsx7(
436
+ "span",
437
+ {
438
+ className: cn(
439
+ "truncate font-mono text-[11px] text-foreground/70",
440
+ isSelected ? "text-foreground" : "hover:text-foreground"
441
+ ),
442
+ children: highlight(node.name, query)
443
+ }
444
+ )
445
+ ]
446
+ }
447
+ ),
448
+ hasChildren && open && /* @__PURE__ */ jsx7("div", { children: node.children?.map((child) => /* @__PURE__ */ jsx7(
449
+ TreeRow2,
450
+ {
451
+ node: child,
452
+ depth: depth + 1,
453
+ query,
454
+ matchIds,
455
+ selected,
456
+ onSelect,
457
+ status
458
+ },
459
+ child.id
460
+ )) })
461
+ ] });
462
+ }
463
+ function SchemaTree({
464
+ target,
465
+ status,
466
+ schemas,
467
+ selected,
468
+ onSelect
469
+ }) {
470
+ const [query, setQuery] = useState2("");
471
+ const tree = useMemo(() => buildTree(target, schemas), [target, schemas]);
472
+ const matchIds = useMemo(
473
+ () => query.trim().length > 1 ? collectMatchIds(tree, query.trim()) : /* @__PURE__ */ new Set(),
474
+ [tree, query]
475
+ );
476
+ const trimmedQuery = query.trim();
477
+ return /* @__PURE__ */ jsxs4("div", { "data-testid": "schema-tree", className: "flex h-full flex-col", children: [
478
+ /* @__PURE__ */ jsx7("div", { className: "shrink-0 border-b border-border px-2 py-2", children: /* @__PURE__ */ jsxs4("div", { className: "flex items-center gap-1.5 rounded-[3px] border border-border bg-background px-2 py-1.5", children: [
479
+ /* @__PURE__ */ jsxs4(
480
+ "svg",
481
+ {
482
+ viewBox: "0 0 24 24",
483
+ className: "h-2.5 w-2.5 shrink-0 text-muted-foreground/50",
484
+ fill: "none",
485
+ stroke: "currentColor",
486
+ strokeWidth: 2,
487
+ children: [
488
+ /* @__PURE__ */ jsx7("circle", { cx: "11", cy: "11", r: "8" }),
489
+ /* @__PURE__ */ jsx7("path", { d: "m21 21-4.35-4.35", strokeLinecap: "round", strokeLinejoin: "round" })
490
+ ]
491
+ }
492
+ ),
493
+ /* @__PURE__ */ jsx7(
494
+ "input",
495
+ {
496
+ type: "text",
497
+ value: query,
498
+ onChange: (event) => setQuery(event.target.value),
499
+ placeholder: "Search tables, schemas...",
500
+ "aria-label": "Search tables",
501
+ className: "w-full min-w-0 bg-transparent font-mono text-[11px] text-foreground outline-none placeholder:text-muted-foreground/40"
502
+ }
503
+ )
504
+ ] }) }),
505
+ /* @__PURE__ */ jsx7("nav", { className: "flex-1 overflow-y-auto py-1", children: trimmedQuery.length > 1 && matchIds.size === 0 ? /* @__PURE__ */ jsx7("div", { className: "px-3 py-4 text-center font-mono text-[11px] text-muted-foreground/40", children: "no results" }) : /* @__PURE__ */ jsx7(
506
+ TreeRow2,
507
+ {
508
+ node: tree,
509
+ depth: 0,
510
+ query: trimmedQuery.length > 1 ? trimmedQuery : "",
511
+ matchIds,
512
+ selected,
513
+ onSelect,
514
+ status
515
+ }
516
+ ) })
517
+ ] });
518
+ }
519
+
520
+ // src/components/sidebar.tsx
521
+ import { PanelLeftClose, PanelLeftOpen } from "lucide-react";
522
+ import { Fragment as Fragment2, jsx as jsx8, jsxs as jsxs5 } from "react/jsx-runtime";
523
+ function Sidebar({
524
+ target,
525
+ status,
526
+ schemas,
527
+ selected,
528
+ onSelect,
529
+ isLoading,
530
+ isError,
531
+ onRetry,
532
+ open,
533
+ onOpenChange
534
+ }) {
535
+ return /* @__PURE__ */ jsxs5(Fragment2, { children: [
536
+ open && /* @__PURE__ */ jsx8(
537
+ "div",
538
+ {
539
+ className: "fixed inset-0 z-40 bg-black/50 md:hidden",
540
+ onClick: () => onOpenChange(false),
541
+ "aria-hidden": "true"
542
+ }
543
+ ),
544
+ /* @__PURE__ */ jsx8(
545
+ "aside",
546
+ {
547
+ className: cn(
548
+ "fixed inset-y-0 left-0 z-50 flex w-64 flex-col border-r border-sidebar-border bg-sidebar transition-transform duration-150",
549
+ "md:static md:z-auto md:translate-x-0 md:transition-none",
550
+ open ? "translate-x-0" : "-translate-x-full",
551
+ !open && "md:w-8"
552
+ ),
553
+ children: open ? /* @__PURE__ */ jsxs5(Fragment2, { children: [
554
+ /* @__PURE__ */ jsxs5("div", { className: "flex shrink-0 items-center gap-2 border-b border-border px-3 py-2", children: [
555
+ /* @__PURE__ */ jsx8("span", { className: "font-mono text-[9px] uppercase tracking-widest text-muted-foreground/60", children: "Explorer" }),
556
+ /* @__PURE__ */ jsx8(
557
+ "button",
558
+ {
559
+ type: "button",
560
+ "aria-label": "Collapse sidebar",
561
+ onClick: () => onOpenChange(false),
562
+ className: "ml-auto rounded p-0.5 text-muted-foreground hover:bg-sidebar-accent hover:text-foreground",
563
+ children: /* @__PURE__ */ jsx8(PanelLeftClose, { className: "h-3 w-3" })
564
+ }
565
+ )
566
+ ] }),
567
+ /* @__PURE__ */ jsx8("div", { className: "min-h-0 flex-1", children: isLoading ? /* @__PURE__ */ jsxs5("p", { className: "flex items-center gap-1.5 px-3 py-2 font-mono text-[11px] text-muted-foreground", children: [
568
+ /* @__PURE__ */ jsx8(Spinner, {}),
569
+ " Loading schemas..."
570
+ ] }) : isError ? /* @__PURE__ */ jsxs5("div", { className: "px-3 py-2 font-mono text-[11px] text-muted-foreground", children: [
571
+ "Failed to load schemas.",
572
+ " ",
573
+ /* @__PURE__ */ jsx8("button", { type: "button", onClick: onRetry, className: "text-primary underline", children: "Retry" })
574
+ ] }) : /* @__PURE__ */ jsx8(
575
+ SchemaTree,
576
+ {
577
+ target,
578
+ status,
579
+ schemas,
580
+ selected,
581
+ onSelect: (schema, table) => {
582
+ onSelect(schema, table);
583
+ if (window.innerWidth < 768) onOpenChange(false);
584
+ }
585
+ }
586
+ ) })
587
+ ] }) : /* @__PURE__ */ jsx8("div", { className: "hidden flex-col items-center gap-2 px-1.5 py-2 md:flex", children: /* @__PURE__ */ jsx8(
588
+ "button",
589
+ {
590
+ type: "button",
591
+ "aria-label": "Expand sidebar",
592
+ onClick: () => onOpenChange(true),
593
+ className: "rounded-md p-1 text-muted-foreground hover:bg-sidebar-accent hover:text-foreground",
594
+ children: /* @__PURE__ */ jsx8(PanelLeftOpen, { className: "h-3.5 w-3.5" })
595
+ }
596
+ ) })
597
+ }
598
+ )
599
+ ] });
600
+ }
601
+
602
+ // src/components/status-bar.tsx
603
+ import { Clock } from "lucide-react";
604
+ import { Fragment as Fragment3, jsx as jsx9, jsxs as jsxs6 } from "react/jsx-runtime";
605
+ var STATUS_LABEL = {
606
+ connected: "connected",
607
+ disconnected: "disconnected",
608
+ unconfigured: "no database"
609
+ };
610
+ var STATUS_COLOR = {
611
+ connected: "var(--c-green)",
612
+ disconnected: "var(--c-red)",
613
+ unconfigured: "rgb(var(--muted-foreground))"
614
+ };
615
+ function Separator() {
616
+ return /* @__PURE__ */ jsx9("span", { className: "text-border", children: "\xB7" });
617
+ }
618
+ function StatusBar({
619
+ status,
620
+ engine,
621
+ engineVersion,
622
+ schema,
623
+ lastQueryMs
624
+ }) {
625
+ const engineLabel = engineVersion ?? engine;
626
+ return /* @__PURE__ */ jsxs6("footer", { className: "flex h-6 shrink-0 items-center justify-between gap-3 border-t border-border bg-sidebar px-3 sm:px-4 font-mono text-[10px]", children: [
627
+ /* @__PURE__ */ jsxs6("div", { className: "flex min-w-0 items-center gap-3 text-muted-foreground/60", children: [
628
+ /* @__PURE__ */ jsx9("span", { style: { color: STATUS_COLOR[status] }, children: STATUS_LABEL[status] }),
629
+ engineLabel && /* @__PURE__ */ jsxs6(Fragment3, { children: [
630
+ /* @__PURE__ */ jsx9(Separator, {}),
631
+ /* @__PURE__ */ jsx9("span", { className: "hidden sm:inline", children: engineLabel })
632
+ ] }),
633
+ schema && /* @__PURE__ */ jsxs6(Fragment3, { children: [
634
+ /* @__PURE__ */ jsx9(Separator, {}),
635
+ /* @__PURE__ */ jsx9("span", { className: "hidden truncate sm:inline", children: schema })
636
+ ] })
637
+ ] }),
638
+ /* @__PURE__ */ jsxs6("div", { className: "hidden shrink-0 items-center gap-3 text-muted-foreground/40 sm:flex", children: [
639
+ /* @__PURE__ */ jsx9("span", { children: "UTF-8" }),
640
+ lastQueryMs !== void 0 && /* @__PURE__ */ jsxs6(Fragment3, { children: [
641
+ /* @__PURE__ */ jsx9(Separator, {}),
642
+ /* @__PURE__ */ jsxs6("span", { className: "flex items-center gap-1", children: [
643
+ /* @__PURE__ */ jsx9(Clock, { className: "h-2.5 w-2.5" }),
644
+ lastQueryMs,
645
+ "ms"
646
+ ] })
647
+ ] })
648
+ ] })
649
+ ] });
650
+ }
651
+
652
+ // src/components/tab-bar.tsx
653
+ import { FileCode2 as FileCode22, FolderOpen as FolderOpen3, Network, Table2 as Table23, Terminal } from "lucide-react";
654
+ import { jsx as jsx10, jsxs as jsxs7 } from "react/jsx-runtime";
655
+ var TABS = [
656
+ { id: "sql-editor", label: "SQL Editor", icon: FileCode22 },
657
+ { id: "tables", label: "Tables", icon: Table23 },
658
+ { id: "schema", label: "Schema", icon: Network },
659
+ { id: "files", label: "Files", icon: FolderOpen3 },
660
+ { id: "console", label: "Console", icon: Terminal }
661
+ ];
662
+ function TabBar({ active, onChange }) {
663
+ return /* @__PURE__ */ jsx10(
664
+ "div",
665
+ {
666
+ role: "tablist",
667
+ className: "flex shrink-0 items-end gap-0.5 overflow-x-auto overflow-y-hidden border-b border-border bg-card px-2 pt-1",
668
+ children: TABS.map(({ id, label, icon: Icon }) => {
669
+ const isActive = id === active;
670
+ return /* @__PURE__ */ jsxs7(
671
+ "button",
672
+ {
673
+ type: "button",
674
+ role: "tab",
675
+ "aria-selected": isActive,
676
+ onClick: () => onChange(id),
677
+ className: cn(
678
+ "flex shrink-0 items-center gap-1.5 whitespace-nowrap rounded-t-[3px] px-3 py-1.5 text-[11px] font-medium transition-colors",
679
+ isActive ? "-mb-px border border-border border-b-background bg-background text-foreground" : "text-muted-foreground hover:bg-accent/50 hover:text-foreground"
680
+ ),
681
+ children: [
682
+ /* @__PURE__ */ jsx10(Icon, { className: "h-3 w-3" }),
683
+ label
684
+ ]
685
+ },
686
+ id
687
+ );
688
+ })
689
+ }
690
+ );
691
+ }
692
+
693
+ // src/components/title-bar.tsx
694
+ import { ChevronRight, Menu, Moon, RefreshCw, Settings, Sun } from "lucide-react";
695
+ import { Fragment as Fragment4, jsx as jsx11, jsxs as jsxs8 } from "react/jsx-runtime";
696
+ var STATUS_DOT_COLOR2 = {
697
+ connected: "bg-[var(--c-green)]",
698
+ disconnected: "bg-[var(--c-red)]",
699
+ unconfigured: "bg-muted-foreground"
700
+ };
701
+ var STATUS_LABEL2 = {
702
+ connected: "Connected",
703
+ disconnected: "Disconnected",
704
+ unconfigured: "No database"
705
+ };
706
+ function splitTarget(target) {
707
+ const lastSlash = target.lastIndexOf("/");
708
+ if (lastSlash === -1) return { name: target };
709
+ return { prefix: target.slice(0, lastSlash), name: target.slice(lastSlash + 1) };
710
+ }
711
+ function TitleBar({
712
+ status,
713
+ target,
714
+ theme,
715
+ onToggleTheme,
716
+ onRefresh,
717
+ isRefreshing,
718
+ onToggleSidebar
719
+ }) {
720
+ const breadcrumb = target ? splitTarget(target) : null;
721
+ return /* @__PURE__ */ jsxs8("header", { className: "flex h-9 shrink-0 items-center border-b border-border bg-card px-3 sm:px-4", children: [
722
+ /* @__PURE__ */ jsx11(
723
+ "button",
724
+ {
725
+ type: "button",
726
+ onClick: onToggleSidebar,
727
+ "aria-label": "Toggle sidebar",
728
+ className: "mr-2 rounded-md p-1 text-muted-foreground hover:bg-accent hover:text-foreground md:hidden",
729
+ children: /* @__PURE__ */ jsx11(Menu, { className: "h-4 w-4" })
730
+ }
731
+ ),
732
+ /* @__PURE__ */ jsx11("h1", { className: "m-0 mr-4 shrink-0 font-mono text-[13px] font-semibold tracking-tight text-foreground", children: "Humb" }),
733
+ /* @__PURE__ */ jsx11("div", { className: "mr-4 hidden h-3.5 w-px shrink-0 bg-border sm:block" }),
734
+ /* @__PURE__ */ jsx11("div", { className: "flex min-w-0 items-center gap-0 truncate font-mono text-[11px]", children: breadcrumb ? /* @__PURE__ */ jsxs8(Fragment4, { children: [
735
+ breadcrumb.prefix && /* @__PURE__ */ jsx11("span", { className: "hidden truncate text-muted-foreground/60 sm:inline", children: breadcrumb.prefix }),
736
+ breadcrumb.prefix && /* @__PURE__ */ jsx11(ChevronRight, { className: "mx-1 hidden h-3 w-3 shrink-0 text-border sm:inline" }),
737
+ /* @__PURE__ */ jsx11("span", { className: "truncate font-medium text-foreground/80", children: breadcrumb.name })
738
+ ] }) : /* @__PURE__ */ jsx11("span", { className: "text-muted-foreground/60", children: "not connected" }) }),
739
+ /* @__PURE__ */ jsxs8(
740
+ "span",
741
+ {
742
+ "data-testid": "status-badge",
743
+ "data-status": status,
744
+ title: STATUS_LABEL2[status],
745
+ className: "ml-3 flex shrink-0 items-center gap-1.5",
746
+ children: [
747
+ /* @__PURE__ */ jsx11("span", { className: cn("h-1.5 w-1.5 rounded-full", STATUS_DOT_COLOR2[status]) }),
748
+ /* @__PURE__ */ jsx11(
749
+ "span",
750
+ {
751
+ "data-testid": "connection-summary",
752
+ className: "font-mono text-[11px] text-muted-foreground",
753
+ children: STATUS_LABEL2[status]
754
+ }
755
+ )
756
+ ]
757
+ }
758
+ ),
759
+ /* @__PURE__ */ jsxs8("div", { className: "ml-auto flex shrink-0 items-center gap-0.5", children: [
760
+ /* @__PURE__ */ jsx11(
761
+ "button",
762
+ {
763
+ type: "button",
764
+ onClick: onToggleTheme,
765
+ title: theme === "dark" ? "Switch to light" : "Switch to dark",
766
+ "aria-label": "Toggle theme",
767
+ className: "rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground",
768
+ children: theme === "dark" ? /* @__PURE__ */ jsx11(Sun, { className: "h-3.5 w-3.5" }) : /* @__PURE__ */ jsx11(Moon, { className: "h-3.5 w-3.5" })
769
+ }
770
+ ),
771
+ /* @__PURE__ */ jsx11(
772
+ "button",
773
+ {
774
+ type: "button",
775
+ onClick: onRefresh,
776
+ disabled: isRefreshing,
777
+ "aria-label": "Refresh",
778
+ className: "rounded-md p-1.5 text-muted-foreground hover:bg-accent hover:text-foreground disabled:opacity-50",
779
+ children: /* @__PURE__ */ jsx11(RefreshCw, { className: cn("h-3.5 w-3.5", isRefreshing && "animate-spin") })
780
+ }
781
+ ),
782
+ /* @__PURE__ */ jsx11(
783
+ "button",
784
+ {
785
+ type: "button",
786
+ "aria-label": "Settings",
787
+ disabled: true,
788
+ className: "cursor-not-allowed rounded-md p-1.5 text-muted-foreground opacity-50",
789
+ children: /* @__PURE__ */ jsx11(Settings, { className: "h-3.5 w-3.5" })
790
+ }
791
+ )
792
+ ] })
793
+ ] });
794
+ }
795
+
796
+ // src/components/rows-table.tsx
797
+ import {
798
+ ArrowUpDown,
799
+ ChevronLeft,
800
+ ChevronRight as ChevronRight2,
801
+ Copy,
802
+ Download,
803
+ RefreshCw as RefreshCw2,
804
+ Search,
805
+ X
806
+ } from "lucide-react";
807
+ import { useMemo as useMemo2, useState as useState3 } from "react";
808
+
809
+ // src/format-cell.ts
810
+ function formatCell(value) {
811
+ if (value === null || value === void 0) {
812
+ return "";
813
+ }
814
+ return typeof value === "string" ? value : JSON.stringify(value);
815
+ }
816
+
817
+ // src/components/rows-table.tsx
818
+ import { Fragment as Fragment5, jsx as jsx12, jsxs as jsxs9 } from "react/jsx-runtime";
819
+ function toCsv(columns, rows) {
820
+ const escape = (value) => {
821
+ const text = formatCell(value);
822
+ return /[",\n]/.test(text) ? `"${text.replace(/"/g, '""')}"` : text;
823
+ };
824
+ const lines = [columns.map(escape).join(",")];
825
+ for (const row of rows) lines.push(columns.map((column) => escape(row[column])).join(","));
826
+ return lines.join("\n");
827
+ }
828
+ function downloadCsv(filename, csv) {
829
+ const blob = new Blob([csv], { type: "text/csv;charset=utf-8" });
830
+ const url = URL.createObjectURL(blob);
831
+ const link = document.createElement("a");
832
+ link.href = url;
833
+ link.download = filename;
834
+ link.click();
835
+ URL.revokeObjectURL(url);
836
+ }
837
+ function RowsTable({
838
+ rowPage,
839
+ columns = [],
840
+ tableName,
841
+ approxRowCount,
842
+ page,
843
+ canGoPrevious,
844
+ canGoNext,
845
+ onPrevious,
846
+ onNext,
847
+ onRefresh
848
+ }) {
849
+ const [search, setSearch] = useState3("");
850
+ const [sortCol, setSortCol] = useState3(null);
851
+ const [sortDir, setSortDir] = useState3(null);
852
+ const [selected, setSelected] = useState3(/* @__PURE__ */ new Set());
853
+ const columnByName = useMemo2(
854
+ () => new Map(columns.map((column) => [column.name, column])),
855
+ [columns]
856
+ );
857
+ const indexed = useMemo2(() => rowPage.rows.map((row, index) => ({ row, index })), [rowPage.rows]);
858
+ const filtered = useMemo2(() => {
859
+ const query = search.trim().toLowerCase();
860
+ if (!query) return indexed;
861
+ return indexed.filter(
862
+ ({ row }) => Object.values(row).some((value) => formatCell(value).toLowerCase().includes(query))
863
+ );
864
+ }, [indexed, search]);
865
+ const sorted = useMemo2(() => {
866
+ if (!sortCol || !sortDir) return filtered;
867
+ const copy = [...filtered];
868
+ copy.sort((a, b) => {
869
+ const left = formatCell(a.row[sortCol]);
870
+ const right = formatCell(b.row[sortCol]);
871
+ const cmp = left < right ? -1 : left > right ? 1 : 0;
872
+ return sortDir === "asc" ? cmp : -cmp;
873
+ });
874
+ return copy;
875
+ }, [filtered, sortCol, sortDir]);
876
+ function handleSort(column) {
877
+ if (sortCol !== column) {
878
+ setSortCol(column);
879
+ setSortDir("asc");
880
+ } else if (sortDir === "asc") {
881
+ setSortDir("desc");
882
+ } else {
883
+ setSortCol(null);
884
+ setSortDir(null);
885
+ }
886
+ }
887
+ function toggleRow(index) {
888
+ setSelected((current) => {
889
+ const next = new Set(current);
890
+ if (next.has(index)) next.delete(index);
891
+ else next.add(index);
892
+ return next;
893
+ });
894
+ }
895
+ async function copySelected() {
896
+ const rows = sorted.filter(({ index }) => selected.has(index)).map(({ row }) => row);
897
+ await navigator.clipboard.writeText(toCsv(rowPage.columns, rows));
898
+ }
899
+ function exportCsv() {
900
+ downloadCsv(
901
+ `${tableName ?? "rows"}.csv`,
902
+ toCsv(
903
+ rowPage.columns,
904
+ sorted.map(({ row }) => row)
905
+ )
906
+ );
907
+ }
908
+ return /* @__PURE__ */ jsxs9("div", { className: "flex h-full flex-col", children: [
909
+ /* @__PURE__ */ jsxs9("div", { className: "flex shrink-0 flex-wrap items-center gap-2 border-b border-border px-3 py-2", children: [
910
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1.5 rounded-[3px] bg-accent px-2 py-1 font-mono text-[11px] text-muted-foreground", children: [
911
+ /* @__PURE__ */ jsx12(Search, { className: "h-2.5 w-2.5" }),
912
+ /* @__PURE__ */ jsx12(
913
+ "input",
914
+ {
915
+ value: search,
916
+ onChange: (event) => setSearch(event.target.value),
917
+ placeholder: "Filter this page...",
918
+ "aria-label": "Filter this page",
919
+ title: "Filters only the rows currently loaded on this page, not the whole table",
920
+ className: "w-28 min-w-0 bg-transparent text-foreground outline-none placeholder:text-muted-foreground sm:w-36"
921
+ }
922
+ ),
923
+ search && /* @__PURE__ */ jsx12("button", { type: "button", onClick: () => setSearch(""), "aria-label": "Clear filter", children: /* @__PURE__ */ jsx12(X, { className: "h-2.5 w-2.5" }) })
924
+ ] }),
925
+ /* @__PURE__ */ jsxs9("div", { className: "ml-auto flex items-center gap-2", children: [
926
+ selected.size > 0 && /* @__PURE__ */ jsxs9(Fragment5, { children: [
927
+ /* @__PURE__ */ jsxs9("span", { className: "font-mono text-[10px]", style: { color: "var(--c-blue)" }, children: [
928
+ selected.size,
929
+ " selected"
930
+ ] }),
931
+ /* @__PURE__ */ jsxs9(
932
+ "button",
933
+ {
934
+ type: "button",
935
+ onClick: () => void copySelected(),
936
+ className: "flex items-center gap-1 rounded-[3px] px-2 py-1 text-[11px] text-muted-foreground hover:bg-accent hover:text-foreground",
937
+ children: [
938
+ /* @__PURE__ */ jsx12(Copy, { className: "h-3 w-3" }),
939
+ " Copy as CSV"
940
+ ]
941
+ }
942
+ )
943
+ ] }),
944
+ /* @__PURE__ */ jsx12(
945
+ "button",
946
+ {
947
+ type: "button",
948
+ onClick: exportCsv,
949
+ "aria-label": "Export this page as CSV",
950
+ title: "Exports the rows currently loaded on this page, not the whole table",
951
+ className: "rounded-[3px] p-1 text-muted-foreground hover:bg-accent hover:text-foreground",
952
+ children: /* @__PURE__ */ jsx12(Download, { className: "h-3 w-3" })
953
+ }
954
+ ),
955
+ onRefresh && /* @__PURE__ */ jsx12(
956
+ "button",
957
+ {
958
+ type: "button",
959
+ onClick: onRefresh,
960
+ "aria-label": "Refresh rows",
961
+ className: "rounded-[3px] p-1 text-muted-foreground hover:bg-accent hover:text-foreground",
962
+ children: /* @__PURE__ */ jsx12(RefreshCw2, { className: "h-3 w-3" })
963
+ }
964
+ )
965
+ ] })
966
+ ] }),
967
+ columns.length > 0 && /* @__PURE__ */ jsxs9("div", { className: "flex shrink-0 overflow-x-auto overflow-y-hidden border-b border-border bg-card", children: [
968
+ /* @__PURE__ */ jsx12("div", { className: "w-8 shrink-0 border-r border-border" }),
969
+ /* @__PURE__ */ jsx12("div", { className: "w-8 shrink-0 border-r border-border" }),
970
+ rowPage.columns.map((columnName) => {
971
+ const meta = columnByName.get(columnName);
972
+ return /* @__PURE__ */ jsxs9(
973
+ "div",
974
+ {
975
+ className: "flex min-w-[120px] shrink-0 items-center gap-1 whitespace-nowrap border-r border-border px-3 py-1 font-mono text-[9px] text-muted-foreground",
976
+ children: [
977
+ meta && /* @__PURE__ */ jsx12(TypeIcon, { dataType: meta.dataType }),
978
+ /* @__PURE__ */ jsx12("span", { children: meta?.dataType ?? "unknown" }),
979
+ meta?.isPrimaryKey && /* @__PURE__ */ jsx12("span", { className: "ml-1 font-bold", style: { color: "var(--c-amber)" }, children: "PK" })
980
+ ]
981
+ },
982
+ columnName
983
+ );
984
+ })
985
+ ] }),
986
+ rowPage.rows.length === 0 ? /* @__PURE__ */ jsx12("div", { "data-testid": "rows-table", className: "flex-1 p-3", children: /* @__PURE__ */ jsx12("p", { className: "font-mono text-[11px] text-muted-foreground", children: "No rows in this table." }) }) : /* @__PURE__ */ jsx12("div", { "data-testid": "rows-table", className: "flex-1 overflow-auto", children: /* @__PURE__ */ jsxs9("table", { className: "w-full border-collapse font-mono text-[11px]", children: [
987
+ /* @__PURE__ */ jsx12("thead", { className: "sticky top-0 z-10 bg-card", children: /* @__PURE__ */ jsxs9("tr", { children: [
988
+ /* @__PURE__ */ jsx12("th", { className: "w-8 border-b border-r border-border px-2 py-2" }),
989
+ /* @__PURE__ */ jsx12("th", { className: "w-8 border-b border-r border-border px-2 py-2 text-right font-normal text-muted-foreground/40", children: "#" }),
990
+ rowPage.columns.map((columnName) => /* @__PURE__ */ jsx12(
991
+ "th",
992
+ {
993
+ onClick: () => handleSort(columnName),
994
+ className: "group cursor-pointer whitespace-nowrap border-b border-r border-border px-3 py-2 text-left font-medium text-muted-foreground hover:text-foreground",
995
+ children: /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1.5", children: [
996
+ columnName,
997
+ /* @__PURE__ */ jsx12(
998
+ ArrowUpDown,
999
+ {
1000
+ className: cn(
1001
+ "h-2.5 w-2.5 transition-opacity",
1002
+ sortCol === columnName ? "text-primary opacity-100" : "opacity-0 group-hover:opacity-40"
1003
+ )
1004
+ }
1005
+ )
1006
+ ] })
1007
+ },
1008
+ columnName
1009
+ ))
1010
+ ] }) }),
1011
+ /* @__PURE__ */ jsx12("tbody", { children: sorted.map(({ row, index }, displayIndex) => /* @__PURE__ */ jsxs9(
1012
+ "tr",
1013
+ {
1014
+ onClick: () => toggleRow(index),
1015
+ className: cn(
1016
+ "cursor-pointer border-b border-border-subtle hover:bg-accent/40",
1017
+ selected.has(index) && "bg-primary/5"
1018
+ ),
1019
+ children: [
1020
+ /* @__PURE__ */ jsx12("td", { className: "w-8 border-r border-border-subtle px-2 py-1.5 text-center", children: /* @__PURE__ */ jsx12(
1021
+ "input",
1022
+ {
1023
+ type: "checkbox",
1024
+ checked: selected.has(index),
1025
+ onChange: () => toggleRow(index),
1026
+ onClick: (event) => event.stopPropagation(),
1027
+ className: "h-3 w-3 accent-primary",
1028
+ "aria-label": `Select row ${displayIndex + 1}`
1029
+ }
1030
+ ) }),
1031
+ /* @__PURE__ */ jsx12("td", { className: "w-8 border-r border-border-subtle px-2 py-1.5 text-right text-muted-foreground/30", children: displayIndex + 1 }),
1032
+ rowPage.columns.map((columnName) => /* @__PURE__ */ jsx12(
1033
+ "td",
1034
+ {
1035
+ className: "whitespace-nowrap border-r border-border-subtle px-3 py-1.5 text-foreground/80",
1036
+ children: row[columnName] === null || row[columnName] === void 0 ? /* @__PURE__ */ jsx12("span", { className: "italic text-muted-foreground/30", children: "null" }) : formatCell(row[columnName])
1037
+ },
1038
+ columnName
1039
+ ))
1040
+ ]
1041
+ },
1042
+ index
1043
+ )) })
1044
+ ] }) }),
1045
+ /* @__PURE__ */ jsxs9("div", { className: "flex shrink-0 items-center justify-between border-t border-border bg-card px-3 py-1.5", children: [
1046
+ /* @__PURE__ */ jsxs9("span", { className: "font-mono text-[10px] text-muted-foreground", children: [
1047
+ sorted.length.toLocaleString(),
1048
+ " of",
1049
+ " ",
1050
+ approxRowCount !== void 0 ? `~${approxRowCount.toLocaleString()}` : sorted.length.toLocaleString(),
1051
+ " ",
1052
+ "rows",
1053
+ tableName && /* @__PURE__ */ jsxs9(Fragment5, { children: [
1054
+ " \xB7 ",
1055
+ tableName
1056
+ ] })
1057
+ ] }),
1058
+ /* @__PURE__ */ jsxs9("div", { className: "flex items-center gap-1", children: [
1059
+ /* @__PURE__ */ jsx12(
1060
+ "button",
1061
+ {
1062
+ type: "button",
1063
+ disabled: !canGoPrevious,
1064
+ onClick: onPrevious,
1065
+ "aria-label": "Previous page",
1066
+ className: "rounded-[2px] p-0.5 text-muted-foreground hover:bg-accent disabled:opacity-30",
1067
+ children: /* @__PURE__ */ jsx12(ChevronLeft, { className: "h-3.5 w-3.5" })
1068
+ }
1069
+ ),
1070
+ /* @__PURE__ */ jsxs9("span", { className: "px-1 font-mono text-[10px] text-muted-foreground", children: [
1071
+ "Page ",
1072
+ page + 1
1073
+ ] }),
1074
+ /* @__PURE__ */ jsx12(
1075
+ "button",
1076
+ {
1077
+ type: "button",
1078
+ disabled: !canGoNext,
1079
+ onClick: onNext,
1080
+ "aria-label": "Next page",
1081
+ className: "rounded-[2px] p-0.5 text-muted-foreground hover:bg-accent disabled:opacity-30",
1082
+ children: /* @__PURE__ */ jsx12(ChevronRight2, { className: "h-3.5 w-3.5" })
1083
+ }
1084
+ )
1085
+ ] })
1086
+ ] })
1087
+ ] });
1088
+ }
1089
+
1090
+ // src/components/query-runner.tsx
1091
+ import { Play } from "lucide-react";
1092
+ import { useRef } from "react";
1093
+ import { jsx as jsx13, jsxs as jsxs10 } from "react/jsx-runtime";
1094
+ function QueryRunner({
1095
+ sql,
1096
+ onSqlChange,
1097
+ onRun,
1098
+ isRunning,
1099
+ result,
1100
+ error
1101
+ }) {
1102
+ const canRun = !isRunning && sql.trim().length > 0;
1103
+ const lineCount = sql.split("\n").length;
1104
+ const gutterRef = useRef(null);
1105
+ function handleKeyDown(event) {
1106
+ if ((event.ctrlKey || event.metaKey) && event.key === "Enter") {
1107
+ event.preventDefault();
1108
+ if (canRun) onRun();
1109
+ }
1110
+ }
1111
+ function handleScroll(event) {
1112
+ if (gutterRef.current) {
1113
+ gutterRef.current.scrollTop = event.currentTarget.scrollTop;
1114
+ }
1115
+ }
1116
+ return /* @__PURE__ */ jsxs10(
1117
+ "div",
1118
+ {
1119
+ "data-testid": "query-runner",
1120
+ className: "flex h-full flex-col overflow-hidden rounded-[3px] border border-border",
1121
+ children: [
1122
+ /* @__PURE__ */ jsxs10("div", { className: "flex shrink-0 items-center gap-2 border-b border-border px-3 py-2", children: [
1123
+ /* @__PURE__ */ jsxs10(
1124
+ "button",
1125
+ {
1126
+ type: "button",
1127
+ onClick: onRun,
1128
+ disabled: !canRun,
1129
+ className: "flex items-center gap-1.5 rounded-[3px] bg-primary px-3 py-1 text-[11px] font-medium text-primary-foreground transition-opacity hover:opacity-90 disabled:opacity-50",
1130
+ children: [
1131
+ isRunning ? /* @__PURE__ */ jsx13(Spinner, { className: "h-2.5 w-2.5 text-primary-foreground" }) : /* @__PURE__ */ jsx13(Play, { className: "h-2.5 w-2.5" }),
1132
+ isRunning ? "Running..." : "Run"
1133
+ ]
1134
+ }
1135
+ ),
1136
+ /* @__PURE__ */ jsx13("span", { className: "hidden font-mono text-[10px] text-muted-foreground sm:inline", children: "\u2318 Enter" }),
1137
+ /* @__PURE__ */ jsxs10("span", { className: "ml-auto font-mono text-[10px] text-muted-foreground", children: [
1138
+ lineCount,
1139
+ " line",
1140
+ lineCount === 1 ? "" : "s"
1141
+ ] })
1142
+ ] }),
1143
+ /* @__PURE__ */ jsxs10("div", { className: "flex min-h-[8rem] flex-1 overflow-hidden", children: [
1144
+ /* @__PURE__ */ jsx13(
1145
+ "div",
1146
+ {
1147
+ ref: gutterRef,
1148
+ "aria-hidden": "true",
1149
+ className: "shrink-0 select-none overflow-hidden border-r border-border bg-background pr-3 pt-3 text-right font-mono text-[11px] text-muted-foreground/30",
1150
+ style: { minWidth: "44px" },
1151
+ children: Array.from({ length: lineCount }, (_, index) => /* @__PURE__ */ jsx13("div", { style: { lineHeight: "20px" }, children: index + 1 }, index))
1152
+ }
1153
+ ),
1154
+ /* @__PURE__ */ jsx13(
1155
+ "textarea",
1156
+ {
1157
+ value: sql,
1158
+ onChange: (event) => onSqlChange(event.target.value),
1159
+ onKeyDown: handleKeyDown,
1160
+ onScroll: handleScroll,
1161
+ placeholder: "SELECT * FROM my_table LIMIT 10",
1162
+ spellCheck: false,
1163
+ className: "flex-1 resize-none overflow-auto bg-background p-3 font-mono text-[12px] leading-5 text-foreground outline-none",
1164
+ style: { caretColor: "var(--c-blue)" }
1165
+ }
1166
+ )
1167
+ ] }),
1168
+ error && /* @__PURE__ */ jsx13(
1169
+ "p",
1170
+ {
1171
+ "data-testid": "query-error",
1172
+ className: "border-t border-border px-3 py-2 font-mono text-[11px]",
1173
+ style: { color: "var(--c-red)" },
1174
+ children: error
1175
+ }
1176
+ ),
1177
+ result && !error && /* @__PURE__ */ jsx13(
1178
+ "div",
1179
+ {
1180
+ "data-testid": "query-result",
1181
+ className: "max-h-64 shrink-0 overflow-auto border-t border-border",
1182
+ children: result.rows.length === 0 ? /* @__PURE__ */ jsx13("p", { className: "p-3 font-mono text-[11px] text-muted-foreground", children: "Query returned no rows." }) : /* @__PURE__ */ jsxs10("table", { className: "w-full border-collapse font-mono text-[11px]", children: [
1183
+ /* @__PURE__ */ jsx13("thead", { className: "sticky top-0 bg-card", children: /* @__PURE__ */ jsx13("tr", { children: result.columns.map((column) => /* @__PURE__ */ jsx13(
1184
+ "th",
1185
+ {
1186
+ className: "whitespace-nowrap border-b border-r border-border px-3 py-1.5 text-left font-medium text-muted-foreground",
1187
+ children: column
1188
+ },
1189
+ column
1190
+ )) }) }),
1191
+ /* @__PURE__ */ jsx13("tbody", { children: result.rows.map((row, rowIndex) => /* @__PURE__ */ jsx13("tr", { className: "border-b border-border-subtle", children: result.columns.map((column) => /* @__PURE__ */ jsx13(
1192
+ "td",
1193
+ {
1194
+ className: "whitespace-nowrap border-r border-border-subtle px-3 py-1.5 text-foreground/80",
1195
+ children: formatCell(row[column])
1196
+ },
1197
+ column
1198
+ )) }, rowIndex)) })
1199
+ ] })
1200
+ }
1201
+ )
1202
+ ]
1203
+ }
1204
+ );
1205
+ }
1206
+ export {
1207
+ ConsoleLog,
1208
+ FilesBrowser,
1209
+ QueryRunner,
1210
+ RowsTable,
1211
+ SchemaGrid,
1212
+ SchemaTree,
1213
+ Sidebar,
1214
+ Spinner,
1215
+ StatusBar,
1216
+ TabBar,
1217
+ TableDetail,
1218
+ TitleBar,
1219
+ TypeIcon
1220
+ };
1221
+ //# sourceMappingURL=index.js.map