@parqui/react 1.1.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.cjs ADDED
@@ -0,0 +1,3177 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ FileDropZone: () => FileDropZone,
24
+ HEADER_HEIGHT: () => HEADER_HEIGHT,
25
+ ParquetExplorer: () => ParquetExplorer,
26
+ ParquetViewer: () => ParquetViewer,
27
+ ParquiLogo: () => ParquiLogo,
28
+ ROW_HEIGHT: () => ROW_HEIGHT,
29
+ useColumnManager: () => useColumnManager,
30
+ useContainerSize: () => useContainerSize,
31
+ useKeyboardNavigation: () => useKeyboardNavigation,
32
+ useParquetData: () => useParquetData,
33
+ useVirtualScroll: () => useVirtualScroll
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // src/ParquetViewer.tsx
38
+ var import_react9 = require("react");
39
+
40
+ // src/hooks/useContainerSize.ts
41
+ var import_react = require("react");
42
+ function useContainerSize() {
43
+ const containerRef = (0, import_react.useRef)(null);
44
+ const [size, setSize] = (0, import_react.useState)({ width: 0, height: 0 });
45
+ const rafRef = (0, import_react.useRef)(0);
46
+ const handleResize = (0, import_react.useCallback)((entries) => {
47
+ cancelAnimationFrame(rafRef.current);
48
+ rafRef.current = requestAnimationFrame(() => {
49
+ const entry = entries[0];
50
+ if (entry) {
51
+ const { width, height } = entry.contentRect;
52
+ setSize((prev) => {
53
+ if (Math.abs(prev.width - width) < 1 && Math.abs(prev.height - height) < 1) {
54
+ return prev;
55
+ }
56
+ return { width, height };
57
+ });
58
+ }
59
+ });
60
+ }, []);
61
+ (0, import_react.useEffect)(() => {
62
+ const el = containerRef.current;
63
+ if (!el) return;
64
+ const observer = new ResizeObserver(handleResize);
65
+ observer.observe(el);
66
+ return () => {
67
+ cancelAnimationFrame(rafRef.current);
68
+ observer.disconnect();
69
+ };
70
+ }, [handleResize]);
71
+ return { containerRef, size };
72
+ }
73
+
74
+ // src/hooks/useParquetData.ts
75
+ var import_react2 = require("react");
76
+ var import_core = require("@parqui/core");
77
+
78
+ // src/source.ts
79
+ function makeSliceablePromise(input) {
80
+ const promise = Promise.resolve(input);
81
+ promise.slice = (start, end) => promise.then((buffer) => buffer.slice(start, end));
82
+ return promise;
83
+ }
84
+ var sourceCache = /* @__PURE__ */ new WeakMap();
85
+ function normalizeParquetSource(source) {
86
+ const cached = sourceCache.get(source);
87
+ if (cached) return cached;
88
+ const wrapped = {
89
+ byteLength: source.byteLength,
90
+ slice(start, end) {
91
+ return makeSliceablePromise(
92
+ Promise.resolve(source.slice(start, end))
93
+ );
94
+ }
95
+ };
96
+ sourceCache.set(source, wrapped);
97
+ return wrapped;
98
+ }
99
+ function isFileSource(value) {
100
+ return typeof File !== "undefined" && value instanceof File;
101
+ }
102
+ function isArrayBufferSource(value) {
103
+ return value instanceof ArrayBuffer;
104
+ }
105
+ function isParquetSource(value) {
106
+ if (!value || typeof value !== "object") return false;
107
+ const candidate = value;
108
+ return typeof candidate.byteLength === "number" && typeof candidate.slice === "function";
109
+ }
110
+ function resolveSourceInput(source) {
111
+ if (source === void 0) return {};
112
+ if (isFileSource(source)) return { file: source };
113
+ if (isArrayBufferSource(source)) return { data: source };
114
+ if (typeof source === "string") return { url: source };
115
+ if (isParquetSource(source)) return { source: normalizeParquetSource(source) };
116
+ return {};
117
+ }
118
+ function sourceToDisplayName(source) {
119
+ if (isFileSource(source)) return source.name;
120
+ if (typeof source === "string") return urlToName(source);
121
+ return "";
122
+ }
123
+ function sourceToDisplaySize(source) {
124
+ if (isFileSource(source)) return source.size;
125
+ return void 0;
126
+ }
127
+ function urlToName(url) {
128
+ try {
129
+ const pathname = new URL(url).pathname;
130
+ return pathname.split("/").pop() ?? url;
131
+ } catch {
132
+ return url;
133
+ }
134
+ }
135
+
136
+ // src/hooks/useParquetData.ts
137
+ var PARQUI_DEBUG_NS = "[parqui/react/useParquetData]";
138
+ function debugEnabled() {
139
+ const g = globalThis;
140
+ return g?.__PARQUI_DEBUG ?? false;
141
+ }
142
+ function dbg(message, details) {
143
+ if (!debugEnabled()) return;
144
+ if (details !== void 0) {
145
+ console.log(`${PARQUI_DEBUG_NS} ${message}`, details);
146
+ } else {
147
+ console.log(`${PARQUI_DEBUG_NS} ${message}`);
148
+ }
149
+ }
150
+ function dbgError(message, error) {
151
+ if (!debugEnabled()) return;
152
+ console.error(`${PARQUI_DEBUG_NS} ${message}`, error);
153
+ }
154
+ var CHUNK_SIZE = 500;
155
+ var MAX_DISPLAY_CHUNKS = 10;
156
+ var SOURCE_CACHE_MAX = 5e3;
157
+ var MAX_CONCURRENT_READS = 4;
158
+ var GAP_TOLERANCE = 200;
159
+ function useParquetData(inputSource, columns) {
160
+ dbg("hook:init", {
161
+ hasInputSource: inputSource !== void 0,
162
+ columns: columns ?? null
163
+ });
164
+ const [sourceState, setSourceState] = (0, import_react2.useState)({ metadata: null, source: null, loading: false, error: null, totalRows: 0 });
165
+ const [pipeline, setPipeline] = (0, import_react2.useState)(import_core.createEmptyPipeline);
166
+ const [computing, setComputing] = (0, import_react2.useState)(false);
167
+ const [mapping, setMapping] = (0, import_react2.useState)(null);
168
+ const [groups, setGroups] = (0, import_react2.useState)([]);
169
+ const [renderTick, setRenderTick] = (0, import_react2.useState)(0);
170
+ const sourceRef = (0, import_react2.useRef)(null);
171
+ const columnsRef = (0, import_react2.useRef)(columns);
172
+ const mountedRef = (0, import_react2.useRef)(true);
173
+ const metadataRef = (0, import_react2.useRef)(null);
174
+ const mappingRef = (0, import_react2.useRef)(null);
175
+ const genRef = (0, import_react2.useRef)(0);
176
+ const chunksRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
177
+ const loadingChunksRef = (0, import_react2.useRef)(/* @__PURE__ */ new Set());
178
+ const sourceRowCacheRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
179
+ const columnCacheRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
180
+ const uniqueCacheRef = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
181
+ columnsRef.current = columns;
182
+ const filteredRowCount = mapping ? mapping.length : sourceState.totalRows;
183
+ const tick = (0, import_react2.useCallback)(() => setRenderTick((v) => v + 1), []);
184
+ const clearCaches = (0, import_react2.useCallback)(() => {
185
+ chunksRef.current.clear();
186
+ loadingChunksRef.current.clear();
187
+ sourceRowCacheRef.current.clear();
188
+ }, []);
189
+ (0, import_react2.useEffect)(() => {
190
+ dbg("source:init:start", {
191
+ inputType: inputSource === void 0 ? "undefined" : inputSource instanceof File ? "File" : inputSource instanceof ArrayBuffer ? "ArrayBuffer" : typeof inputSource
192
+ });
193
+ mountedRef.current = true;
194
+ clearCaches();
195
+ sourceRef.current = null;
196
+ metadataRef.current = null;
197
+ mappingRef.current = null;
198
+ columnCacheRef.current.clear();
199
+ uniqueCacheRef.current.clear();
200
+ genRef.current++;
201
+ setSourceState({
202
+ metadata: null,
203
+ source: null,
204
+ loading: true,
205
+ error: null,
206
+ totalRows: 0
207
+ });
208
+ setPipeline((0, import_core.createEmptyPipeline)());
209
+ setMapping(null);
210
+ setGroups([]);
211
+ setComputing(false);
212
+ let cancelled = false;
213
+ async function init() {
214
+ try {
215
+ let source;
216
+ const resolved = resolveSourceInput(inputSource);
217
+ dbg("source:init:resolved", {
218
+ hasFile: !!resolved.file,
219
+ hasData: !!resolved.data,
220
+ hasUrl: !!resolved.url,
221
+ hasSource: !!resolved.source
222
+ });
223
+ if (resolved.source) source = resolved.source;
224
+ else if (resolved.data) source = (0, import_core.sourceFromBuffer)(resolved.data);
225
+ else if (resolved.file) source = (0, import_core.sourceFromFile)(resolved.file);
226
+ else if (resolved.url) source = await (0, import_core.sourceFromUrl)(resolved.url);
227
+ else {
228
+ dbg("source:init:none");
229
+ setSourceState({
230
+ metadata: null,
231
+ source: null,
232
+ loading: false,
233
+ error: null,
234
+ totalRows: 0
235
+ });
236
+ return;
237
+ }
238
+ if (cancelled) return;
239
+ sourceRef.current = source;
240
+ const metadata = await (0, import_core.readParquetMetadata)(source);
241
+ if (cancelled) return;
242
+ dbg("source:init:metadata-loaded", {
243
+ rowCount: metadata.rowCount,
244
+ columns: metadata.columns.length,
245
+ rowGroups: metadata.rowGroups
246
+ });
247
+ metadataRef.current = metadata;
248
+ setSourceState({
249
+ metadata,
250
+ source,
251
+ loading: false,
252
+ error: null,
253
+ totalRows: metadata.rowCount
254
+ });
255
+ } catch (err) {
256
+ dbgError("source:init:error", err);
257
+ if (!cancelled) {
258
+ setSourceState({
259
+ metadata: null,
260
+ source: null,
261
+ loading: false,
262
+ error: err instanceof Error ? err.message : "Failed to load file",
263
+ totalRows: 0
264
+ });
265
+ }
266
+ }
267
+ }
268
+ init();
269
+ return () => {
270
+ cancelled = true;
271
+ mountedRef.current = false;
272
+ };
273
+ }, [inputSource, clearCaches]);
274
+ (0, import_react2.useEffect)(() => {
275
+ const source = sourceRef.current;
276
+ const metadata = metadataRef.current;
277
+ const totalRows = metadata?.rowCount ?? 0;
278
+ if (!source || !metadata || totalRows === 0) {
279
+ dbg("pipeline:skip-no-source-or-metadata", {
280
+ hasSource: !!source,
281
+ hasMetadata: !!metadata,
282
+ totalRows
283
+ });
284
+ mappingRef.current = null;
285
+ setMapping(null);
286
+ setGroups([]);
287
+ setComputing(false);
288
+ genRef.current++;
289
+ return;
290
+ }
291
+ const { sorts, filters, groups: groupDefs } = pipeline;
292
+ dbg("pipeline:run", {
293
+ sorts,
294
+ filtersCount: filters.length,
295
+ groups: groupDefs,
296
+ totalRows
297
+ });
298
+ if (sorts.length === 0 && filters.length === 0 && groupDefs.length === 0) {
299
+ const hadMapping = mappingRef.current !== null;
300
+ mappingRef.current = null;
301
+ genRef.current++;
302
+ setMapping(null);
303
+ setGroups([]);
304
+ setComputing(false);
305
+ if (hadMapping) {
306
+ clearCaches();
307
+ }
308
+ dbg("pipeline:fast-path");
309
+ tick();
310
+ return;
311
+ }
312
+ const gen = ++genRef.current;
313
+ const isStale = () => gen !== genRef.current;
314
+ clearCaches();
315
+ setComputing(true);
316
+ tick();
317
+ async function compute() {
318
+ try {
319
+ dbg("pipeline:compute:start", { gen });
320
+ const neededCols = /* @__PURE__ */ new Set();
321
+ for (const s of sorts) neededCols.add(s.column);
322
+ for (const f of filters) neededCols.add(f.column);
323
+ for (const g of groupDefs) neededCols.add(g.column);
324
+ const toRead = [...neededCols].filter(
325
+ (c) => !columnCacheRef.current.has(c)
326
+ );
327
+ if (toRead.length > 0) {
328
+ dbg("pipeline:compute:read-columns", { toRead });
329
+ const newCols = await (0, import_core.readColumnValues)(source, toRead, isStale);
330
+ if (isStale()) return;
331
+ for (const [key, vals] of newCols) {
332
+ columnCacheRef.current.set(key, vals);
333
+ }
334
+ }
335
+ if (isStale()) return;
336
+ await yieldToUI();
337
+ let indices = (0, import_core.buildFilterIndex)(totalRows, filters, columnCacheRef.current);
338
+ if (indices === null) {
339
+ indices = Array.from({ length: totalRows }, (_, i) => i);
340
+ }
341
+ if (isStale()) return;
342
+ await yieldToUI();
343
+ if (sorts.length > 0) {
344
+ dbg("pipeline:compute:sort:start", { sortsCount: sorts.length, indices: indices.length });
345
+ const colValues = columnCacheRef.current;
346
+ const compareFn = (a, b) => {
347
+ for (const sort of sorts) {
348
+ const col = colValues.get(sort.column);
349
+ if (!col) continue;
350
+ const va = col[a];
351
+ const vb = col[b];
352
+ const cmp = compareValues(va, vb);
353
+ if (cmp !== 0) return sort.direction === "asc" ? cmp : -cmp;
354
+ }
355
+ return a - b;
356
+ };
357
+ if (indices.length > 1e5) {
358
+ indices = await asyncSort(indices, compareFn, isStale);
359
+ } else {
360
+ indices.sort(compareFn);
361
+ }
362
+ }
363
+ if (isStale()) return;
364
+ let groupNodes = [];
365
+ if (groupDefs.length > 0) {
366
+ dbg("pipeline:compute:groups:start", { groupDefs });
367
+ groupNodes = (0, import_core.buildGroups)(indices, groupDefs, columnCacheRef.current);
368
+ }
369
+ mappingRef.current = indices;
370
+ setMapping(indices);
371
+ setGroups(groupNodes);
372
+ dbg("pipeline:compute:done", {
373
+ mappedRows: indices.length,
374
+ groups: groupNodes.length
375
+ });
376
+ tick();
377
+ } catch (error) {
378
+ dbgError("pipeline:compute:error", error);
379
+ } finally {
380
+ if (!isStale()) {
381
+ setComputing(false);
382
+ }
383
+ }
384
+ }
385
+ compute();
386
+ }, [sourceState.source, sourceState.metadata, sourceState.totalRows, pipeline, clearCaches, tick]);
387
+ const fetchSourceRows = (0, import_react2.useCallback)(
388
+ async (neededIndices, gen) => {
389
+ dbg("fetchSourceRows:start", {
390
+ needed: neededIndices.length,
391
+ gen
392
+ });
393
+ if (!sourceRef.current || !metadataRef.current || neededIndices.length === 0) return;
394
+ const totalRows = metadataRef.current.rowCount;
395
+ const unique = [...new Set(neededIndices)].filter(
396
+ (idx) => !sourceRowCacheRef.current.has(idx)
397
+ );
398
+ if (unique.length === 0) return;
399
+ unique.sort((a, b) => a - b);
400
+ const clusters = [];
401
+ let curStart = unique[0];
402
+ let curEnd = unique[0] + 1;
403
+ let curNeeded = /* @__PURE__ */ new Set([unique[0]]);
404
+ for (let i = 1; i < unique.length; i++) {
405
+ if (unique[i] <= curEnd + GAP_TOLERANCE) {
406
+ curEnd = unique[i] + 1;
407
+ curNeeded.add(unique[i]);
408
+ } else {
409
+ clusters.push({ start: curStart, end: curEnd, needed: curNeeded });
410
+ curStart = unique[i];
411
+ curEnd = unique[i] + 1;
412
+ curNeeded = /* @__PURE__ */ new Set([unique[i]]);
413
+ }
414
+ }
415
+ clusters.push({ start: curStart, end: curEnd, needed: curNeeded });
416
+ dbg("fetchSourceRows:clusters", {
417
+ clusters: clusters.length,
418
+ uniqueIndices: unique.length
419
+ });
420
+ await runWithConcurrency(clusters, MAX_CONCURRENT_READS, async (cluster) => {
421
+ if (gen !== genRef.current) return;
422
+ try {
423
+ const readStart = Math.max(0, cluster.start);
424
+ const readEnd = Math.min(cluster.end, totalRows);
425
+ const result = await (0, import_core.readParquetData)(sourceRef.current, {
426
+ columns: columnsRef.current,
427
+ offset: readStart,
428
+ limit: readEnd - readStart
429
+ });
430
+ if (!mountedRef.current || gen !== genRef.current) return;
431
+ for (let i = 0; i < result.rows.length; i++) {
432
+ const sourceIdx = readStart + i;
433
+ if (cluster.needed.has(sourceIdx)) {
434
+ sourceRowCacheRef.current.set(sourceIdx, result.rows[i]);
435
+ }
436
+ }
437
+ dbg("fetchSourceRows:cluster:done", {
438
+ readStart,
439
+ readEnd,
440
+ rows: result.rows.length
441
+ });
442
+ } catch (error) {
443
+ dbgError("fetchSourceRows:cluster:error", error);
444
+ }
445
+ });
446
+ const cache = sourceRowCacheRef.current;
447
+ if (cache.size > SOURCE_CACHE_MAX) {
448
+ const toDelete = cache.size - SOURCE_CACHE_MAX;
449
+ let count = 0;
450
+ for (const key of cache.keys()) {
451
+ if (count >= toDelete) break;
452
+ cache.delete(key);
453
+ count++;
454
+ }
455
+ }
456
+ },
457
+ []
458
+ );
459
+ const loadChunk = (0, import_react2.useCallback)(
460
+ async (chunkIndex) => {
461
+ dbg("loadChunk:start", { chunkIndex });
462
+ if (loadingChunksRef.current.has(chunkIndex)) return;
463
+ if (chunksRef.current.has(chunkIndex)) return;
464
+ if (!sourceRef.current) return;
465
+ loadingChunksRef.current.add(chunkIndex);
466
+ const gen = genRef.current;
467
+ try {
468
+ const curMapping = mappingRef.current;
469
+ const displayStart = chunkIndex * CHUNK_SIZE;
470
+ if (curMapping) {
471
+ const displayEnd = Math.min(displayStart + CHUNK_SIZE, curMapping.length);
472
+ const sourceIndices = curMapping.slice(displayStart, displayEnd);
473
+ dbg("loadChunk:mapped", {
474
+ chunkIndex,
475
+ displayStart,
476
+ displayEnd,
477
+ sourceIndices: sourceIndices.length
478
+ });
479
+ const uncached = sourceIndices.filter(
480
+ (idx) => !sourceRowCacheRef.current.has(idx)
481
+ );
482
+ if (uncached.length > 0) {
483
+ dbg("loadChunk:mapped:fetch-uncached", {
484
+ chunkIndex,
485
+ uncached: uncached.length
486
+ });
487
+ await fetchSourceRows(uncached, gen);
488
+ if (!mountedRef.current || gen !== genRef.current) return;
489
+ }
490
+ const rows = [];
491
+ for (const sourceIdx of sourceIndices) {
492
+ rows.push(sourceRowCacheRef.current.get(sourceIdx) ?? {});
493
+ }
494
+ if (mountedRef.current && gen === genRef.current) {
495
+ chunksRef.current.set(chunkIndex, { rows, loadedAt: Date.now() });
496
+ evictOldChunks(chunksRef.current);
497
+ dbg("loadChunk:mapped:done", {
498
+ chunkIndex,
499
+ rows: rows.length
500
+ });
501
+ tick();
502
+ }
503
+ } else {
504
+ dbg("loadChunk:sequential", {
505
+ chunkIndex,
506
+ displayStart,
507
+ limit: CHUNK_SIZE
508
+ });
509
+ const result = await (0, import_core.readParquetData)(sourceRef.current, {
510
+ columns: columnsRef.current,
511
+ offset: displayStart,
512
+ limit: CHUNK_SIZE
513
+ });
514
+ if (mountedRef.current && gen === genRef.current) {
515
+ chunksRef.current.set(chunkIndex, {
516
+ rows: result.rows,
517
+ loadedAt: Date.now()
518
+ });
519
+ evictOldChunks(chunksRef.current);
520
+ dbg("loadChunk:sequential:done", {
521
+ chunkIndex,
522
+ rows: result.rows.length
523
+ });
524
+ tick();
525
+ }
526
+ }
527
+ } catch (error) {
528
+ dbgError("loadChunk:error", error);
529
+ } finally {
530
+ loadingChunksRef.current.delete(chunkIndex);
531
+ }
532
+ },
533
+ [fetchSourceRows, tick]
534
+ );
535
+ const getRow = (0, import_react2.useCallback)(
536
+ (displayIndex) => {
537
+ const chunkIndex = Math.floor(displayIndex / CHUNK_SIZE);
538
+ const chunk = chunksRef.current.get(chunkIndex);
539
+ if (!chunk) return void 0;
540
+ return chunk.rows[displayIndex - chunkIndex * CHUNK_SIZE];
541
+ },
542
+ []
543
+ );
544
+ const ensureRange = (0, import_react2.useCallback)(
545
+ (start, end) => {
546
+ dbg("ensureRange", { start, end, computing });
547
+ if (computing) return;
548
+ const startChunk = Math.floor(start / CHUNK_SIZE);
549
+ const endChunk = Math.floor(Math.max(0, end - 1) / CHUNK_SIZE);
550
+ for (let i = startChunk; i <= endChunk; i++) {
551
+ if (!chunksRef.current.has(i) && !loadingChunksRef.current.has(i)) {
552
+ loadChunk(i);
553
+ }
554
+ }
555
+ },
556
+ [loadChunk, computing]
557
+ );
558
+ const ensureIndices = (0, import_react2.useCallback)(
559
+ (displayIndices) => {
560
+ dbg("ensureIndices", {
561
+ displayIndices: displayIndices.length,
562
+ computing
563
+ });
564
+ if (computing) return;
565
+ const chunkSet = /* @__PURE__ */ new Set();
566
+ for (const idx of displayIndices) {
567
+ chunkSet.add(Math.floor(idx / CHUNK_SIZE));
568
+ }
569
+ for (const chunkIdx of chunkSet) {
570
+ if (!chunksRef.current.has(chunkIdx) && !loadingChunksRef.current.has(chunkIdx)) {
571
+ loadChunk(chunkIdx);
572
+ }
573
+ }
574
+ },
575
+ [loadChunk, computing]
576
+ );
577
+ const toggleSort = (0, import_react2.useCallback)((column) => {
578
+ setPipeline((prev) => {
579
+ const existing = prev.sorts.find((s) => s.column === column);
580
+ let newSorts;
581
+ if (!existing) {
582
+ newSorts = [...prev.sorts, { column, direction: "asc" }];
583
+ } else if (existing.direction === "asc") {
584
+ newSorts = prev.sorts.map(
585
+ (s) => s.column === column ? { ...s, direction: "desc" } : s
586
+ );
587
+ } else {
588
+ newSorts = prev.sorts.filter((s) => s.column !== column);
589
+ }
590
+ return { ...prev, sorts: newSorts };
591
+ });
592
+ }, []);
593
+ const setSort = (0, import_react2.useCallback)((sorts) => {
594
+ setPipeline((prev) => ({ ...prev, sorts }));
595
+ }, []);
596
+ const clearSort = (0, import_react2.useCallback)(() => {
597
+ setPipeline((prev) => ({ ...prev, sorts: [] }));
598
+ }, []);
599
+ const getSortDirection = (0, import_react2.useCallback)(
600
+ (column) => {
601
+ return pipeline.sorts.find((s) => s.column === column)?.direction ?? null;
602
+ },
603
+ [pipeline.sorts]
604
+ );
605
+ const getSortPriority = (0, import_react2.useCallback)(
606
+ (column) => {
607
+ const idx = pipeline.sorts.findIndex((s) => s.column === column);
608
+ return idx >= 0 ? idx + 1 : null;
609
+ },
610
+ [pipeline.sorts]
611
+ );
612
+ const setFilter = (0, import_react2.useCallback)((filter) => {
613
+ setPipeline((prev) => ({
614
+ ...prev,
615
+ filters: [
616
+ ...prev.filters.filter((f) => f.column !== filter.column),
617
+ filter
618
+ ]
619
+ }));
620
+ }, []);
621
+ const removeFilter = (0, import_react2.useCallback)((column) => {
622
+ setPipeline((prev) => ({
623
+ ...prev,
624
+ filters: prev.filters.filter((f) => f.column !== column)
625
+ }));
626
+ }, []);
627
+ const clearFilters = (0, import_react2.useCallback)(() => {
628
+ setPipeline((prev) => ({ ...prev, filters: [] }));
629
+ }, []);
630
+ const getFilter = (0, import_react2.useCallback)(
631
+ (column) => {
632
+ return pipeline.filters.find((f) => f.column === column);
633
+ },
634
+ [pipeline.filters]
635
+ );
636
+ const getUniqueValues = (0, import_react2.useCallback)(
637
+ async (column) => {
638
+ if (uniqueCacheRef.current.has(column)) {
639
+ return uniqueCacheRef.current.get(column);
640
+ }
641
+ const source = sourceRef.current;
642
+ if (!source) return [];
643
+ let values = columnCacheRef.current.get(column);
644
+ if (!values) {
645
+ const cols = await (0, import_core.readColumnValues)(source, [column]);
646
+ values = cols.get(column) ?? [];
647
+ columnCacheRef.current.set(column, values);
648
+ }
649
+ const unique = (0, import_core.collectUniqueValues)(values, 500);
650
+ uniqueCacheRef.current.set(column, unique);
651
+ return unique;
652
+ },
653
+ []
654
+ );
655
+ const setGroupDefs = (0, import_react2.useCallback)((groupDefs) => {
656
+ setPipeline((prev) => ({ ...prev, groups: groupDefs }));
657
+ }, []);
658
+ const toggleGroup = (0, import_react2.useCallback)((column) => {
659
+ setPipeline((prev) => {
660
+ const exists = prev.groups.some((g) => g.column === column);
661
+ const newGroups = exists ? prev.groups.filter((g) => g.column !== column) : [...prev.groups, { column }];
662
+ return { ...prev, groups: newGroups };
663
+ });
664
+ }, []);
665
+ const clearGroups = (0, import_react2.useCallback)(() => {
666
+ setPipeline((prev) => ({ ...prev, groups: [] }));
667
+ }, []);
668
+ const toggleGroupExpanded = (0, import_react2.useCallback)((groupIndex) => {
669
+ setGroups(
670
+ (prev) => prev.map(
671
+ (g, i) => i === groupIndex ? { ...g, expanded: !g.expanded } : g
672
+ )
673
+ );
674
+ }, []);
675
+ const resetAll = (0, import_react2.useCallback)(() => {
676
+ setPipeline((0, import_core.createEmptyPipeline)());
677
+ mappingRef.current = null;
678
+ genRef.current++;
679
+ clearCaches();
680
+ setMapping(null);
681
+ setGroups([]);
682
+ setComputing(false);
683
+ columnCacheRef.current.clear();
684
+ uniqueCacheRef.current.clear();
685
+ tick();
686
+ }, [clearCaches, tick]);
687
+ return {
688
+ metadata: sourceState.metadata,
689
+ loading: sourceState.loading,
690
+ error: sourceState.error,
691
+ totalRows: sourceState.totalRows,
692
+ pipeline,
693
+ computing,
694
+ filteredRowCount,
695
+ groups,
696
+ getRow,
697
+ ensureRange,
698
+ ensureIndices,
699
+ renderTick,
700
+ toggleSort,
701
+ setSort,
702
+ clearSort,
703
+ getSortDirection,
704
+ getSortPriority,
705
+ setFilter,
706
+ removeFilter,
707
+ clearFilters,
708
+ getFilter,
709
+ getUniqueValues,
710
+ setGroups: setGroupDefs,
711
+ toggleGroup,
712
+ clearGroups,
713
+ toggleGroupExpanded,
714
+ resetAll
715
+ };
716
+ }
717
+ function evictOldChunks(chunks) {
718
+ if (chunks.size <= MAX_DISPLAY_CHUNKS) return;
719
+ const entries = [...chunks.entries()].sort(
720
+ (a, b) => a[1].loadedAt - b[1].loadedAt
721
+ );
722
+ const toRemove = entries.length - MAX_DISPLAY_CHUNKS;
723
+ for (let i = 0; i < toRemove; i++) {
724
+ chunks.delete(entries[i][0]);
725
+ }
726
+ }
727
+ function compareValues(a, b) {
728
+ if (a === null || a === void 0)
729
+ return b === null || b === void 0 ? 0 : 1;
730
+ if (b === null || b === void 0) return -1;
731
+ if (typeof a === "number" && typeof b === "number") return a - b;
732
+ if (typeof a === "bigint" && typeof b === "bigint")
733
+ return a < b ? -1 : a > b ? 1 : 0;
734
+ if (typeof a === "boolean" && typeof b === "boolean")
735
+ return Number(a) - Number(b);
736
+ if (a instanceof Date && b instanceof Date)
737
+ return a.getTime() - b.getTime();
738
+ return String(a).localeCompare(String(b));
739
+ }
740
+ function yieldToUI() {
741
+ return new Promise((resolve) => setTimeout(resolve, 0));
742
+ }
743
+ async function asyncSort(arr, compareFn, isCancelled) {
744
+ const CHUNK = 5e4;
745
+ const chunks = [];
746
+ for (let i = 0; i < arr.length; i += CHUNK) {
747
+ if (isCancelled()) return arr;
748
+ const chunk = arr.slice(i, i + CHUNK);
749
+ chunk.sort(compareFn);
750
+ chunks.push(chunk);
751
+ await yieldToUI();
752
+ }
753
+ while (chunks.length > 1) {
754
+ if (isCancelled()) return arr;
755
+ const merged = [];
756
+ for (let i = 0; i < chunks.length; i += 2) {
757
+ if (i + 1 < chunks.length) {
758
+ merged.push(mergeSorted(chunks[i], chunks[i + 1], compareFn));
759
+ } else {
760
+ merged.push(chunks[i]);
761
+ }
762
+ }
763
+ chunks.length = 0;
764
+ chunks.push(...merged);
765
+ await yieldToUI();
766
+ }
767
+ return chunks[0] ?? [];
768
+ }
769
+ function mergeSorted(a, b, compareFn) {
770
+ const result = new Array(a.length + b.length);
771
+ let i = 0, j = 0, k = 0;
772
+ while (i < a.length && j < b.length) {
773
+ if (compareFn(a[i], b[j]) <= 0) result[k++] = a[i++];
774
+ else result[k++] = b[j++];
775
+ }
776
+ while (i < a.length) result[k++] = a[i++];
777
+ while (j < b.length) result[k++] = b[j++];
778
+ return result;
779
+ }
780
+ async function runWithConcurrency(items, maxConcurrency, fn) {
781
+ let index = 0;
782
+ const workers = Array.from(
783
+ { length: Math.min(maxConcurrency, items.length) },
784
+ async () => {
785
+ while (index < items.length) {
786
+ const i = index++;
787
+ await fn(items[i]);
788
+ }
789
+ }
790
+ );
791
+ await Promise.all(workers);
792
+ }
793
+
794
+ // src/hooks/useVirtualScroll.ts
795
+ var import_react3 = require("react");
796
+ var ROW_HEIGHT = 32;
797
+ var HEADER_HEIGHT = 36;
798
+ var OVERSCAN = 10;
799
+ var MAX_SCROLL_HEIGHT = 15e5;
800
+ function useVirtualScroll(containerHeight, totalRows, resetKey) {
801
+ const elRef = (0, import_react3.useRef)(null);
802
+ const roRef = (0, import_react3.useRef)(null);
803
+ const [scrollTop, setScrollTop] = (0, import_react3.useState)(0);
804
+ const [measuredHeight, setMeasuredHeight] = (0, import_react3.useState)(0);
805
+ const paramsRef = (0, import_react3.useRef)({
806
+ rawStart: 0,
807
+ scaled: false,
808
+ totalHeight: 0,
809
+ totalRows: 0
810
+ });
811
+ const scrollRef = (0, import_react3.useCallback)((el) => {
812
+ if (roRef.current) {
813
+ roRef.current.disconnect();
814
+ roRef.current = null;
815
+ }
816
+ elRef.current = el;
817
+ if (!el) return;
818
+ setMeasuredHeight(el.clientHeight);
819
+ setScrollTop(el.scrollTop);
820
+ roRef.current = new ResizeObserver(() => {
821
+ const newClientHeight = el.clientHeight;
822
+ const p = paramsRef.current;
823
+ if (p.scaled && p.totalRows > 0) {
824
+ const newBodyHeight = Math.max(1, newClientHeight - HEADER_HEIGHT);
825
+ const newMaxScroll = Math.max(1, p.totalHeight - newBodyHeight);
826
+ const newFullyVisible = Math.floor(newBodyHeight / ROW_HEIGHT);
827
+ const newMaxFirstRow = Math.max(1, p.totalRows - newFullyVisible);
828
+ const targetRaw = Math.min(p.rawStart, newMaxFirstRow);
829
+ const newScrollTop = targetRaw / newMaxFirstRow * newMaxScroll;
830
+ el.scrollTop = newScrollTop;
831
+ setScrollTop(el.scrollTop);
832
+ } else {
833
+ setScrollTop(el.scrollTop);
834
+ }
835
+ setMeasuredHeight(newClientHeight);
836
+ });
837
+ roRef.current.observe(el);
838
+ }, []);
839
+ const liveHeight = elRef.current?.clientHeight ?? 0;
840
+ const effectiveHeight = liveHeight > 0 ? liveHeight : measuredHeight > 0 ? measuredHeight : containerHeight;
841
+ (0, import_react3.useEffect)(() => {
842
+ setScrollTop(0);
843
+ if (elRef.current) {
844
+ elRef.current.scrollTop = 0;
845
+ }
846
+ }, [resetKey]);
847
+ const bodyHeight = Math.max(1, effectiveHeight - HEADER_HEIGHT);
848
+ const visibleCount = Math.ceil(bodyHeight / ROW_HEIGHT);
849
+ const naturalHeight = totalRows * ROW_HEIGHT;
850
+ const scaled = naturalHeight > MAX_SCROLL_HEIGHT;
851
+ const totalHeight = scaled ? MAX_SCROLL_HEIGHT : naturalHeight + ROW_HEIGHT;
852
+ let startIndex;
853
+ let endIndex;
854
+ let offsetY;
855
+ let rawStart;
856
+ if (scaled) {
857
+ const maxScroll = Math.max(1, totalHeight - bodyHeight);
858
+ const scrollFraction = Math.min(1, Math.max(0, scrollTop / maxScroll));
859
+ const fullyVisibleRows = Math.floor(bodyHeight / ROW_HEIGHT);
860
+ const maxFirstRow = Math.max(0, totalRows - fullyVisibleRows);
861
+ rawStart = Math.round(scrollFraction * maxFirstRow);
862
+ startIndex = Math.max(0, rawStart - OVERSCAN);
863
+ endIndex = Math.min(totalRows, rawStart + visibleCount + OVERSCAN);
864
+ const overscanAbove = rawStart - startIndex;
865
+ offsetY = scrollTop - overscanAbove * ROW_HEIGHT;
866
+ } else {
867
+ rawStart = Math.floor(scrollTop / ROW_HEIGHT);
868
+ startIndex = Math.max(0, rawStart - OVERSCAN);
869
+ endIndex = Math.min(totalRows, rawStart + visibleCount + OVERSCAN);
870
+ offsetY = startIndex * ROW_HEIGHT;
871
+ }
872
+ paramsRef.current = { rawStart, scaled, totalHeight, totalRows };
873
+ const onScroll = (0, import_react3.useCallback)((e) => {
874
+ const el = e.currentTarget;
875
+ setScrollTop(el.scrollTop);
876
+ setMeasuredHeight((prev) => {
877
+ const h = el.clientHeight;
878
+ return h === prev ? prev : h;
879
+ });
880
+ }, []);
881
+ const scrollToRow = (0, import_react3.useCallback)(
882
+ (rowIndex) => {
883
+ const el = elRef.current;
884
+ if (!el) return;
885
+ const idx = Math.max(0, Math.min(totalRows - 1, rowIndex));
886
+ const actualBodyHeight = el.clientHeight - HEADER_HEIGHT;
887
+ if (scaled) {
888
+ const maxScroll = Math.max(1, totalHeight - actualBodyHeight);
889
+ const fullyVisible = Math.floor(actualBodyHeight / ROW_HEIGHT);
890
+ const maxFirstRow = Math.max(1, totalRows - fullyVisible);
891
+ const frac = Math.min(1, Math.max(0, el.scrollTop / maxScroll));
892
+ const renderRawStart = Math.round(frac * maxFirstRow);
893
+ if (idx < renderRawStart) {
894
+ el.scrollTop = idx / maxFirstRow * maxScroll;
895
+ } else if (idx >= renderRawStart + fullyVisible) {
896
+ el.scrollTop = (idx - fullyVisible + 1) / maxFirstRow * maxScroll;
897
+ const checkFrac = Math.min(1, Math.max(0, el.scrollTop / maxScroll));
898
+ const checkRaw = Math.round(checkFrac * maxFirstRow);
899
+ if (idx >= checkRaw + fullyVisible) {
900
+ el.scrollTop += 1;
901
+ }
902
+ }
903
+ } else {
904
+ const rowTop = idx * ROW_HEIGHT;
905
+ const rowBottom = rowTop + ROW_HEIGHT;
906
+ const viewTop = el.scrollTop;
907
+ const viewBottom = viewTop + actualBodyHeight;
908
+ if (rowTop < viewTop) {
909
+ el.scrollTop = rowTop;
910
+ } else if (rowBottom > viewBottom) {
911
+ el.scrollTop = rowBottom - actualBodyHeight;
912
+ }
913
+ }
914
+ setScrollTop(el.scrollTop);
915
+ setMeasuredHeight(el.clientHeight);
916
+ },
917
+ [scaled, totalHeight, totalRows]
918
+ );
919
+ return {
920
+ scrollTop,
921
+ startIndex,
922
+ endIndex,
923
+ visibleCount,
924
+ totalHeight,
925
+ offsetY,
926
+ onScroll,
927
+ scrollToRow,
928
+ scrollRef,
929
+ scrollEl: elRef
930
+ };
931
+ }
932
+
933
+ // src/hooks/useColumnManager.ts
934
+ var import_react4 = require("react");
935
+ var DEFAULT_COL_WIDTH = 150;
936
+ var MIN_COL_WIDTH = 50;
937
+ function useColumnManager(columnNames, initialWidths) {
938
+ const [columns, setColumns] = (0, import_react4.useState)(
939
+ () => columnNames.map((key) => ({
940
+ key,
941
+ width: initialWidths?.[key] ?? DEFAULT_COL_WIDTH,
942
+ visible: true
943
+ }))
944
+ );
945
+ const prevNamesRef = (0, import_react4.useRef)(columnNames);
946
+ if (columnNames.length !== prevNamesRef.current.length || columnNames.some((n, i) => n !== prevNamesRef.current[i])) {
947
+ prevNamesRef.current = columnNames;
948
+ const newCols = columnNames.map((key) => ({
949
+ key,
950
+ width: initialWidths?.[key] ?? DEFAULT_COL_WIDTH,
951
+ visible: true
952
+ }));
953
+ setColumns(newCols);
954
+ }
955
+ const [dragState, setDragState] = (0, import_react4.useState)({
956
+ dragKey: null,
957
+ insertIndex: null
958
+ });
959
+ const dragRef = (0, import_react4.useRef)(dragState);
960
+ dragRef.current = dragState;
961
+ const [resizeState, setResizeState] = (0, import_react4.useState)({
962
+ columnKey: null,
963
+ startX: 0,
964
+ startWidth: 0
965
+ });
966
+ const visibleColumns = columns.filter((c) => c.visible);
967
+ const totalWidth = visibleColumns.reduce((sum, c) => sum + c.width, 0);
968
+ const startDrag = (0, import_react4.useCallback)((key) => {
969
+ setDragState({ dragKey: key, insertIndex: null });
970
+ }, []);
971
+ const updateDragOver = (0, import_react4.useCallback)(
972
+ (overKey, mouseXInColumn, columnWidth) => {
973
+ setDragState((prev) => {
974
+ if (!prev.dragKey || prev.dragKey === overKey) {
975
+ return { ...prev, insertIndex: null };
976
+ }
977
+ const visibleCols = columns.filter((c) => c.visible);
978
+ const overIdx = visibleCols.findIndex((c) => c.key === overKey);
979
+ if (overIdx === -1) return prev;
980
+ const isLeftHalf = mouseXInColumn < columnWidth / 2;
981
+ const insertIndex = isLeftHalf ? overIdx : overIdx + 1;
982
+ return { ...prev, insertIndex };
983
+ });
984
+ },
985
+ [columns]
986
+ );
987
+ const updateDragOverEnd = (0, import_react4.useCallback)(() => {
988
+ setDragState((prev) => {
989
+ if (!prev.dragKey) return prev;
990
+ const visibleCols = columns.filter((c) => c.visible);
991
+ return { ...prev, insertIndex: visibleCols.length };
992
+ });
993
+ }, [columns]);
994
+ const endDrag = (0, import_react4.useCallback)(() => {
995
+ const { dragKey, insertIndex } = dragRef.current;
996
+ setDragState({ dragKey: null, insertIndex: null });
997
+ if (!dragKey || insertIndex === null) return;
998
+ setColumns((cols) => {
999
+ const visibleCols = cols.filter((c) => c.visible);
1000
+ const fromIdx = visibleCols.findIndex((c) => c.key === dragKey);
1001
+ if (fromIdx === -1) return cols;
1002
+ if (insertIndex === fromIdx || insertIndex === fromIdx + 1) return cols;
1003
+ const visibleToFullIdx = [];
1004
+ cols.forEach((c, fullIdx) => {
1005
+ if (c.visible) visibleToFullIdx.push(fullIdx);
1006
+ });
1007
+ const fullFromIdx = visibleToFullIdx[fromIdx];
1008
+ const moved = cols[fullFromIdx];
1009
+ const newCols = cols.filter((_, i) => i !== fullFromIdx);
1010
+ let targetFullIdx;
1011
+ if (insertIndex >= visibleCols.length) {
1012
+ const lastVisibleFullIdx = visibleToFullIdx[visibleCols.length - 1];
1013
+ targetFullIdx = lastVisibleFullIdx > fullFromIdx ? lastVisibleFullIdx : lastVisibleFullIdx + 1;
1014
+ } else {
1015
+ const adjustedInsertIdx = insertIndex > fromIdx ? insertIndex - 1 : insertIndex;
1016
+ const remainingVisible = newCols.filter((c) => c.visible);
1017
+ const targetKey = remainingVisible[adjustedInsertIdx]?.key;
1018
+ targetFullIdx = targetKey ? newCols.findIndex((c) => c.key === targetKey) : newCols.length;
1019
+ }
1020
+ newCols.splice(targetFullIdx, 0, moved);
1021
+ return newCols;
1022
+ });
1023
+ }, []);
1024
+ const startResize = (0, import_react4.useCallback)(
1025
+ (key, clientX) => {
1026
+ const col = columns.find((c) => c.key === key);
1027
+ if (col) {
1028
+ setResizeState({
1029
+ columnKey: key,
1030
+ startX: clientX,
1031
+ startWidth: col.width
1032
+ });
1033
+ }
1034
+ },
1035
+ [columns]
1036
+ );
1037
+ const updateResize = (0, import_react4.useCallback)(
1038
+ (clientX) => {
1039
+ if (!resizeState.columnKey) return;
1040
+ const delta = clientX - resizeState.startX;
1041
+ const newWidth = Math.max(MIN_COL_WIDTH, resizeState.startWidth + delta);
1042
+ setColumns(
1043
+ (cols) => cols.map(
1044
+ (c) => c.key === resizeState.columnKey ? { ...c, width: newWidth } : c
1045
+ )
1046
+ );
1047
+ },
1048
+ [resizeState]
1049
+ );
1050
+ const endResize = (0, import_react4.useCallback)(() => {
1051
+ setResizeState({ columnKey: null, startX: 0, startWidth: 0 });
1052
+ }, []);
1053
+ const toggleColumn = (0, import_react4.useCallback)((key) => {
1054
+ setColumns(
1055
+ (cols) => cols.map((c) => c.key === key ? { ...c, visible: !c.visible } : c)
1056
+ );
1057
+ }, []);
1058
+ const showAllColumns = (0, import_react4.useCallback)(() => {
1059
+ setColumns((cols) => cols.map((c) => ({ ...c, visible: true })));
1060
+ }, []);
1061
+ const setColumnWidth = (0, import_react4.useCallback)((key, width) => {
1062
+ setColumns(
1063
+ (cols) => cols.map(
1064
+ (c) => c.key === key ? { ...c, width: Math.max(MIN_COL_WIDTH, width) } : c
1065
+ )
1066
+ );
1067
+ }, []);
1068
+ return {
1069
+ columns,
1070
+ visibleColumns,
1071
+ totalWidth,
1072
+ startDrag,
1073
+ updateDragOver,
1074
+ updateDragOverEnd,
1075
+ endDrag,
1076
+ dragState,
1077
+ startResize,
1078
+ updateResize,
1079
+ endResize,
1080
+ resizeState,
1081
+ toggleColumn,
1082
+ showAllColumns,
1083
+ setColumnWidth
1084
+ };
1085
+ }
1086
+
1087
+ // src/hooks/useKeyboardNavigation.ts
1088
+ var import_react5 = require("react");
1089
+ function useKeyboardNavigation(onScrollToCell, isNavigable) {
1090
+ const [activeCell, setActiveCell] = (0, import_react5.useState)(null);
1091
+ const moveTo = (0, import_react5.useCallback)(
1092
+ (row, col, totalRows, totalCols) => {
1093
+ let r = Math.max(0, Math.min(totalRows - 1, row));
1094
+ const clampedCol = Math.max(0, Math.min(totalCols - 1, col));
1095
+ if (isNavigable) {
1096
+ if (!isNavigable(r)) {
1097
+ const currentRow = activeCell?.row ?? 0;
1098
+ const direction = row >= currentRow ? 1 : -1;
1099
+ let candidate = r;
1100
+ while (candidate >= 0 && candidate < totalRows && !isNavigable(candidate)) {
1101
+ candidate += direction;
1102
+ }
1103
+ if (candidate >= 0 && candidate < totalRows) {
1104
+ r = candidate;
1105
+ } else {
1106
+ candidate = r;
1107
+ while (candidate >= 0 && candidate < totalRows && !isNavigable(candidate)) {
1108
+ candidate -= direction;
1109
+ }
1110
+ if (candidate >= 0 && candidate < totalRows) {
1111
+ r = candidate;
1112
+ } else {
1113
+ return;
1114
+ }
1115
+ }
1116
+ }
1117
+ }
1118
+ setActiveCell({ row: r, col: clampedCol });
1119
+ onScrollToCell?.(r, clampedCol);
1120
+ },
1121
+ [onScrollToCell, isNavigable, activeCell?.row]
1122
+ );
1123
+ const handleKeyDown = (0, import_react5.useCallback)(
1124
+ (e, totalRows, totalCols) => {
1125
+ if (!activeCell) {
1126
+ if (["ArrowDown", "ArrowUp", "ArrowLeft", "ArrowRight"].includes(e.key)) {
1127
+ e.preventDefault();
1128
+ moveTo(0, 0, totalRows, totalCols);
1129
+ }
1130
+ return;
1131
+ }
1132
+ const { row, col } = activeCell;
1133
+ const PAGE_SIZE = 20;
1134
+ switch (e.key) {
1135
+ case "ArrowDown":
1136
+ e.preventDefault();
1137
+ moveTo(row + 1, col, totalRows, totalCols);
1138
+ break;
1139
+ case "ArrowUp":
1140
+ e.preventDefault();
1141
+ moveTo(row - 1, col, totalRows, totalCols);
1142
+ break;
1143
+ case "ArrowRight":
1144
+ e.preventDefault();
1145
+ moveTo(row, col + 1, totalRows, totalCols);
1146
+ break;
1147
+ case "ArrowLeft":
1148
+ e.preventDefault();
1149
+ moveTo(row, col - 1, totalRows, totalCols);
1150
+ break;
1151
+ case "Tab":
1152
+ e.preventDefault();
1153
+ if (e.shiftKey) {
1154
+ if (col > 0) {
1155
+ moveTo(row, col - 1, totalRows, totalCols);
1156
+ } else if (row > 0) {
1157
+ moveTo(row - 1, totalCols - 1, totalRows, totalCols);
1158
+ }
1159
+ } else {
1160
+ if (col < totalCols - 1) {
1161
+ moveTo(row, col + 1, totalRows, totalCols);
1162
+ } else if (row < totalRows - 1) {
1163
+ moveTo(row + 1, 0, totalRows, totalCols);
1164
+ }
1165
+ }
1166
+ break;
1167
+ case "Home":
1168
+ e.preventDefault();
1169
+ if (e.ctrlKey) {
1170
+ moveTo(0, 0, totalRows, totalCols);
1171
+ } else {
1172
+ moveTo(row, 0, totalRows, totalCols);
1173
+ }
1174
+ break;
1175
+ case "End":
1176
+ e.preventDefault();
1177
+ if (e.ctrlKey) {
1178
+ moveTo(totalRows - 1, totalCols - 1, totalRows, totalCols);
1179
+ } else {
1180
+ moveTo(row, totalCols - 1, totalRows, totalCols);
1181
+ }
1182
+ break;
1183
+ case "PageDown":
1184
+ e.preventDefault();
1185
+ moveTo(row + PAGE_SIZE, col, totalRows, totalCols);
1186
+ break;
1187
+ case "PageUp":
1188
+ e.preventDefault();
1189
+ moveTo(row - PAGE_SIZE, col, totalRows, totalCols);
1190
+ break;
1191
+ case "Escape":
1192
+ e.preventDefault();
1193
+ setActiveCell(null);
1194
+ break;
1195
+ }
1196
+ },
1197
+ [activeCell, moveTo]
1198
+ );
1199
+ const handleCellClick = (0, import_react5.useCallback)((row, col) => {
1200
+ setActiveCell({ row, col });
1201
+ }, []);
1202
+ return {
1203
+ activeCell,
1204
+ setActiveCell,
1205
+ handleKeyDown,
1206
+ handleCellClick
1207
+ };
1208
+ }
1209
+
1210
+ // src/components/ColumnHeader.tsx
1211
+ var import_react6 = require("react");
1212
+ var import_jsx_runtime = require("react/jsx-runtime");
1213
+ function ColumnHeader({
1214
+ column,
1215
+ visibleIndex,
1216
+ dragState,
1217
+ sortDirection,
1218
+ sortPriority,
1219
+ hasFilter,
1220
+ isGrouped,
1221
+ onDragStart,
1222
+ onDragOver,
1223
+ onDragEnd,
1224
+ onResizeStart,
1225
+ onSortClick,
1226
+ onContextMenu
1227
+ }) {
1228
+ const cellRef = (0, import_react6.useRef)(null);
1229
+ const isDragging = dragState.dragKey === column.key;
1230
+ const showLeftIndicator = dragState.dragKey !== null && dragState.dragKey !== column.key && dragState.insertIndex === visibleIndex;
1231
+ const showRightIndicator = dragState.dragKey !== null && dragState.dragKey !== column.key && dragState.insertIndex === visibleIndex + 1;
1232
+ const handleDragStart = (0, import_react6.useCallback)(
1233
+ (e) => {
1234
+ e.dataTransfer.effectAllowed = "move";
1235
+ e.dataTransfer.setData("text/plain", column.key);
1236
+ onDragStart(column.key);
1237
+ },
1238
+ [column.key, onDragStart]
1239
+ );
1240
+ const handleDragOver = (0, import_react6.useCallback)(
1241
+ (e) => {
1242
+ e.preventDefault();
1243
+ e.dataTransfer.dropEffect = "move";
1244
+ const rect = cellRef.current?.getBoundingClientRect();
1245
+ if (rect) {
1246
+ const mouseXInColumn = e.clientX - rect.left;
1247
+ onDragOver(column.key, mouseXInColumn, rect.width);
1248
+ }
1249
+ },
1250
+ [column.key, onDragOver]
1251
+ );
1252
+ const handleDrop = (0, import_react6.useCallback)(
1253
+ (e) => {
1254
+ e.preventDefault();
1255
+ onDragEnd();
1256
+ },
1257
+ [onDragEnd]
1258
+ );
1259
+ const handleNativeDragEnd = (0, import_react6.useCallback)(() => {
1260
+ onDragEnd();
1261
+ }, [onDragEnd]);
1262
+ const handleResizeMouseDown = (0, import_react6.useCallback)(
1263
+ (e) => {
1264
+ e.preventDefault();
1265
+ e.stopPropagation();
1266
+ onResizeStart(column.key, e.clientX);
1267
+ },
1268
+ [column.key, onResizeStart]
1269
+ );
1270
+ const handleClick = (0, import_react6.useCallback)(
1271
+ (e) => {
1272
+ if (e.target.dataset.resize) return;
1273
+ onSortClick(column.key);
1274
+ },
1275
+ [column.key, onSortClick]
1276
+ );
1277
+ const handleContextMenu = (0, import_react6.useCallback)(
1278
+ (e) => {
1279
+ e.preventDefault();
1280
+ onContextMenu(column.key, e.clientX, e.clientY);
1281
+ },
1282
+ [column.key, onContextMenu]
1283
+ );
1284
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
1285
+ "div",
1286
+ {
1287
+ ref: cellRef,
1288
+ draggable: true,
1289
+ onDragStart: handleDragStart,
1290
+ onDragOver: handleDragOver,
1291
+ onDrop: handleDrop,
1292
+ onDragEnd: handleNativeDragEnd,
1293
+ onClick: handleClick,
1294
+ onContextMenu: handleContextMenu,
1295
+ style: {
1296
+ ...headerStyles.cell,
1297
+ width: column.width,
1298
+ minWidth: column.width,
1299
+ maxWidth: column.width,
1300
+ height: HEADER_HEIGHT,
1301
+ opacity: isDragging ? 0.4 : 1
1302
+ },
1303
+ title: column.key,
1304
+ children: [
1305
+ showLeftIndicator && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: headerStyles.indicatorLeft }),
1306
+ showRightIndicator && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: headerStyles.indicatorRight }),
1307
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: headerStyles.label, children: column.key }),
1308
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: headerStyles.indicators, children: [
1309
+ hasFilter && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: headerStyles.filterDot }),
1310
+ isGrouped && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: headerStyles.groupIcon, children: "\u25A6" }),
1311
+ sortDirection && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { style: headerStyles.sortArrow, children: [
1312
+ sortDirection === "asc" ? "\u2191" : "\u2193",
1313
+ sortPriority !== null && sortPriority > 1 && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: headerStyles.sortPriority, children: sortPriority })
1314
+ ] })
1315
+ ] }),
1316
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1317
+ "div",
1318
+ {
1319
+ "data-resize": "true",
1320
+ onMouseDown: handleResizeMouseDown,
1321
+ style: headerStyles.resizeHandle,
1322
+ role: "separator",
1323
+ "aria-orientation": "vertical"
1324
+ }
1325
+ )
1326
+ ]
1327
+ }
1328
+ );
1329
+ }
1330
+ function ColumnDropEndZone({
1331
+ dragState,
1332
+ totalColumns,
1333
+ onDragOverEnd,
1334
+ onDragEnd
1335
+ }) {
1336
+ const showIndicator = dragState.dragKey !== null && dragState.insertIndex === totalColumns;
1337
+ const handleDragOver = (0, import_react6.useCallback)(
1338
+ (e) => {
1339
+ e.preventDefault();
1340
+ e.dataTransfer.dropEffect = "move";
1341
+ onDragOverEnd();
1342
+ },
1343
+ [onDragOverEnd]
1344
+ );
1345
+ const handleDrop = (0, import_react6.useCallback)(
1346
+ (e) => {
1347
+ e.preventDefault();
1348
+ onDragEnd();
1349
+ },
1350
+ [onDragEnd]
1351
+ );
1352
+ if (!dragState.dragKey) return null;
1353
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1354
+ "div",
1355
+ {
1356
+ onDragOver: handleDragOver,
1357
+ onDrop: handleDrop,
1358
+ style: headerStyles.endZone,
1359
+ children: showIndicator && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: headerStyles.indicatorLeft })
1360
+ }
1361
+ );
1362
+ }
1363
+ var headerStyles = {
1364
+ cell: {
1365
+ position: "relative",
1366
+ display: "flex",
1367
+ alignItems: "center",
1368
+ padding: "0 12px",
1369
+ backgroundColor: "#f6f9fc",
1370
+ borderBottom: "2px solid rgba(0,0,0,0.06)",
1371
+ borderRight: "1px solid rgba(0,0,0,0.06)",
1372
+ fontWeight: 600,
1373
+ fontSize: 13,
1374
+ color: "#0a2540",
1375
+ cursor: "pointer",
1376
+ userSelect: "none",
1377
+ overflow: "hidden",
1378
+ flexShrink: 0,
1379
+ boxSizing: "border-box",
1380
+ gap: 4
1381
+ },
1382
+ label: {
1383
+ overflow: "hidden",
1384
+ textOverflow: "ellipsis",
1385
+ whiteSpace: "nowrap",
1386
+ flex: 1
1387
+ },
1388
+ indicators: {
1389
+ display: "flex",
1390
+ alignItems: "center",
1391
+ gap: 4,
1392
+ flexShrink: 0
1393
+ },
1394
+ sortArrow: {
1395
+ display: "flex",
1396
+ alignItems: "center",
1397
+ fontSize: 12,
1398
+ color: "#6366f1",
1399
+ fontWeight: 700
1400
+ },
1401
+ sortPriority: {
1402
+ fontSize: 9,
1403
+ marginLeft: 1
1404
+ },
1405
+ filterDot: {
1406
+ width: 5,
1407
+ height: 5,
1408
+ borderRadius: "50%",
1409
+ backgroundColor: "#f59e0b"
1410
+ },
1411
+ groupIcon: {
1412
+ fontSize: 11,
1413
+ color: "#8b5cf6"
1414
+ },
1415
+ resizeHandle: {
1416
+ position: "absolute",
1417
+ right: 0,
1418
+ top: 0,
1419
+ bottom: 0,
1420
+ width: 6,
1421
+ cursor: "col-resize",
1422
+ zIndex: 2,
1423
+ background: "transparent"
1424
+ },
1425
+ indicatorLeft: {
1426
+ position: "absolute",
1427
+ left: 0,
1428
+ top: 0,
1429
+ bottom: 0,
1430
+ width: 3,
1431
+ backgroundColor: "#6366f1",
1432
+ zIndex: 3,
1433
+ borderRadius: "2px 0 0 2px"
1434
+ },
1435
+ indicatorRight: {
1436
+ position: "absolute",
1437
+ right: 0,
1438
+ top: 0,
1439
+ bottom: 0,
1440
+ width: 3,
1441
+ backgroundColor: "#6366f1",
1442
+ zIndex: 3,
1443
+ borderRadius: "0 2px 2px 0"
1444
+ },
1445
+ endZone: {
1446
+ position: "relative",
1447
+ width: 32,
1448
+ minWidth: 32,
1449
+ flexShrink: 0,
1450
+ backgroundColor: "#f6f9fc",
1451
+ borderBottom: "2px solid rgba(0,0,0,0.06)"
1452
+ }
1453
+ };
1454
+
1455
+ // src/components/ColumnContextMenu.tsx
1456
+ var import_react7 = require("react");
1457
+ var import_jsx_runtime2 = require("react/jsx-runtime");
1458
+ function ColumnContextMenu({
1459
+ column,
1460
+ x,
1461
+ y,
1462
+ sortDirection,
1463
+ sortPriority,
1464
+ activeFilter,
1465
+ isGrouped,
1466
+ onClose,
1467
+ onSortAsc,
1468
+ onSortDesc,
1469
+ onSortClear,
1470
+ onFilterSet,
1471
+ onFilterClear,
1472
+ onGroupToggle,
1473
+ loadUniqueValues
1474
+ }) {
1475
+ const [showFilterPanel, setShowFilterPanel] = (0, import_react7.useState)(false);
1476
+ const [uniqueValues, setUniqueValues] = (0, import_react7.useState)(null);
1477
+ const [selectedValues, setSelectedValues] = (0, import_react7.useState)(/* @__PURE__ */ new Set());
1478
+ const [filterSearch, setFilterSearch] = (0, import_react7.useState)("");
1479
+ const [loadingValues, setLoadingValues] = (0, import_react7.useState)(false);
1480
+ const menuRef = (0, import_react7.useRef)(null);
1481
+ (0, import_react7.useEffect)(() => {
1482
+ const handle = (e) => {
1483
+ if (menuRef.current && !menuRef.current.contains(e.target)) {
1484
+ onClose();
1485
+ }
1486
+ };
1487
+ document.addEventListener("mousedown", handle);
1488
+ return () => document.removeEventListener("mousedown", handle);
1489
+ }, [onClose]);
1490
+ (0, import_react7.useEffect)(() => {
1491
+ const handle = (e) => {
1492
+ if (e.key === "Escape") onClose();
1493
+ };
1494
+ document.addEventListener("keydown", handle);
1495
+ return () => document.removeEventListener("keydown", handle);
1496
+ }, [onClose]);
1497
+ (0, import_react7.useEffect)(() => {
1498
+ if (showFilterPanel && uniqueValues === null && !loadingValues) {
1499
+ setLoadingValues(true);
1500
+ loadUniqueValues().then((values) => {
1501
+ setUniqueValues(values);
1502
+ if (activeFilter?.operator === "in" && Array.isArray(activeFilter.value)) {
1503
+ setSelectedValues(new Set(activeFilter.value.map(String)));
1504
+ } else {
1505
+ setSelectedValues(new Set(values.map((v) => String(v))));
1506
+ }
1507
+ setLoadingValues(false);
1508
+ });
1509
+ }
1510
+ }, [showFilterPanel, uniqueValues, loadingValues, loadUniqueValues, activeFilter]);
1511
+ const handleFilterApply = (0, import_react7.useCallback)(() => {
1512
+ if (!uniqueValues) return;
1513
+ const allSelected = selectedValues.size === uniqueValues.length;
1514
+ if (allSelected) {
1515
+ onFilterClear();
1516
+ } else {
1517
+ onFilterSet({
1518
+ column,
1519
+ operator: "in",
1520
+ value: [...selectedValues]
1521
+ });
1522
+ }
1523
+ onClose();
1524
+ }, [column, selectedValues, uniqueValues, onFilterSet, onFilterClear, onClose]);
1525
+ const toggleValue = (0, import_react7.useCallback)((val) => {
1526
+ setSelectedValues((prev) => {
1527
+ const next = new Set(prev);
1528
+ if (next.has(val)) next.delete(val);
1529
+ else next.add(val);
1530
+ return next;
1531
+ });
1532
+ }, []);
1533
+ const selectAll = (0, import_react7.useCallback)(() => {
1534
+ if (uniqueValues) {
1535
+ setSelectedValues(new Set(uniqueValues.map((v) => String(v))));
1536
+ }
1537
+ }, [uniqueValues]);
1538
+ const selectNone = (0, import_react7.useCallback)(() => {
1539
+ setSelectedValues(/* @__PURE__ */ new Set());
1540
+ }, []);
1541
+ const adjustedX = Math.min(x, window.innerWidth - 260);
1542
+ const adjustedY = Math.min(y, window.innerHeight - 400);
1543
+ const filteredUniqueValues = uniqueValues?.filter(
1544
+ (v) => filterSearch ? String(v).toLowerCase().includes(filterSearch.toLowerCase()) : true
1545
+ );
1546
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1547
+ "div",
1548
+ {
1549
+ ref: menuRef,
1550
+ style: { ...menuStyles.overlay, left: adjustedX, top: adjustedY },
1551
+ onContextMenu: (e) => e.preventDefault(),
1552
+ children: /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: menuStyles.container, children: [
1553
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: menuStyles.sectionLabel, children: "Sort" }),
1554
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1555
+ "button",
1556
+ {
1557
+ style: {
1558
+ ...menuStyles.item,
1559
+ ...sortDirection === "asc" ? menuStyles.itemActive : {}
1560
+ },
1561
+ onClick: () => {
1562
+ onSortAsc();
1563
+ onClose();
1564
+ },
1565
+ children: [
1566
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: menuStyles.itemIcon, children: "\u2191" }),
1567
+ "Sort ascending",
1568
+ sortPriority && sortPriority > 0 && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: menuStyles.badge, children: sortPriority })
1569
+ ]
1570
+ }
1571
+ ),
1572
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1573
+ "button",
1574
+ {
1575
+ style: {
1576
+ ...menuStyles.item,
1577
+ ...sortDirection === "desc" ? menuStyles.itemActive : {}
1578
+ },
1579
+ onClick: () => {
1580
+ onSortDesc();
1581
+ onClose();
1582
+ },
1583
+ children: [
1584
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: menuStyles.itemIcon, children: "\u2193" }),
1585
+ "Sort descending"
1586
+ ]
1587
+ }
1588
+ ),
1589
+ sortDirection && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1590
+ "button",
1591
+ {
1592
+ style: menuStyles.item,
1593
+ onClick: () => {
1594
+ onSortClear();
1595
+ onClose();
1596
+ },
1597
+ children: [
1598
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: menuStyles.itemIcon, children: "\u2715" }),
1599
+ "Clear sort"
1600
+ ]
1601
+ }
1602
+ ),
1603
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: menuStyles.divider }),
1604
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: menuStyles.sectionLabel, children: "Filter" }),
1605
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1606
+ "button",
1607
+ {
1608
+ style: {
1609
+ ...menuStyles.item,
1610
+ ...showFilterPanel ? menuStyles.itemActive : {}
1611
+ },
1612
+ onClick: () => setShowFilterPanel(!showFilterPanel),
1613
+ children: [
1614
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: menuStyles.itemIcon, children: "\u229E" }),
1615
+ "Filter by values...",
1616
+ activeFilter && /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: menuStyles.activeDot })
1617
+ ]
1618
+ }
1619
+ ),
1620
+ activeFilter && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1621
+ "button",
1622
+ {
1623
+ style: menuStyles.item,
1624
+ onClick: () => {
1625
+ onFilterClear();
1626
+ onClose();
1627
+ },
1628
+ children: [
1629
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: menuStyles.itemIcon, children: "\u2715" }),
1630
+ "Clear filter"
1631
+ ]
1632
+ }
1633
+ ),
1634
+ showFilterPanel && /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: menuStyles.filterPanel, children: [
1635
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1636
+ "input",
1637
+ {
1638
+ type: "text",
1639
+ placeholder: "Search values...",
1640
+ value: filterSearch,
1641
+ onChange: (e) => setFilterSearch(e.target.value),
1642
+ style: menuStyles.filterSearch,
1643
+ autoFocus: true
1644
+ }
1645
+ ),
1646
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("div", { style: menuStyles.filterActions, children: [
1647
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: menuStyles.linkButton, onClick: selectAll, children: "Select all" }),
1648
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: menuStyles.linkButton, onClick: selectNone, children: "Select none" })
1649
+ ] }),
1650
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: menuStyles.filterList, children: loadingValues ? /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: menuStyles.filterLoading, children: "Loading values..." }) : filteredUniqueValues?.map((val) => {
1651
+ const key = String(val);
1652
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)("label", { style: menuStyles.filterItem, children: [
1653
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
1654
+ "input",
1655
+ {
1656
+ type: "checkbox",
1657
+ checked: selectedValues.has(key),
1658
+ onChange: () => toggleValue(key),
1659
+ style: menuStyles.checkbox
1660
+ }
1661
+ ),
1662
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: menuStyles.filterValue, children: val === null || val === void 0 ? "(null)" : String(val) })
1663
+ ] }, key);
1664
+ }) }),
1665
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("button", { style: menuStyles.applyButton, onClick: handleFilterApply, children: "Apply filter" })
1666
+ ] }),
1667
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: menuStyles.divider }),
1668
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { style: menuStyles.sectionLabel, children: "Group" }),
1669
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(
1670
+ "button",
1671
+ {
1672
+ style: {
1673
+ ...menuStyles.item,
1674
+ ...isGrouped ? menuStyles.itemActive : {}
1675
+ },
1676
+ onClick: () => {
1677
+ onGroupToggle();
1678
+ onClose();
1679
+ },
1680
+ children: [
1681
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("span", { style: menuStyles.itemIcon, children: isGrouped ? "\u25A4" : "\u25A6" }),
1682
+ isGrouped ? "Remove grouping" : "Group by this column"
1683
+ ]
1684
+ }
1685
+ )
1686
+ ] })
1687
+ }
1688
+ );
1689
+ }
1690
+ var menuStyles = {
1691
+ overlay: {
1692
+ position: "fixed",
1693
+ zIndex: 1e4
1694
+ },
1695
+ container: {
1696
+ backgroundColor: "white",
1697
+ border: "1px solid rgba(0,0,0,0.06)",
1698
+ borderRadius: 10,
1699
+ boxShadow: "0 8px 30px rgba(0,0,0,0.12), 0 2px 8px rgba(0,0,0,0.06)",
1700
+ minWidth: 220,
1701
+ maxWidth: 320,
1702
+ padding: "4px 0",
1703
+ fontFamily: "'Plus Jakarta Sans', 'Inter', system-ui, sans-serif",
1704
+ fontSize: 13
1705
+ },
1706
+ sectionLabel: {
1707
+ padding: "6px 12px 2px",
1708
+ fontSize: 11,
1709
+ fontWeight: 600,
1710
+ color: "rgba(66,84,102,0.5)",
1711
+ textTransform: "uppercase",
1712
+ letterSpacing: "0.05em"
1713
+ },
1714
+ item: {
1715
+ display: "flex",
1716
+ alignItems: "center",
1717
+ gap: 8,
1718
+ width: "100%",
1719
+ padding: "7px 12px",
1720
+ border: "none",
1721
+ background: "none",
1722
+ textAlign: "left",
1723
+ fontSize: 13,
1724
+ color: "#0a2540",
1725
+ cursor: "pointer",
1726
+ fontFamily: "inherit"
1727
+ },
1728
+ itemActive: {
1729
+ backgroundColor: "#eef2ff",
1730
+ color: "#6366f1"
1731
+ },
1732
+ itemIcon: {
1733
+ width: 16,
1734
+ textAlign: "center",
1735
+ flexShrink: 0,
1736
+ fontSize: 14
1737
+ },
1738
+ badge: {
1739
+ marginLeft: "auto",
1740
+ backgroundColor: "rgba(0,0,0,0.04)",
1741
+ color: "#425466",
1742
+ fontSize: 10,
1743
+ fontWeight: 700,
1744
+ padding: "1px 5px",
1745
+ borderRadius: 4
1746
+ },
1747
+ activeDot: {
1748
+ marginLeft: "auto",
1749
+ width: 6,
1750
+ height: 6,
1751
+ borderRadius: "50%",
1752
+ backgroundColor: "#6366f1"
1753
+ },
1754
+ divider: {
1755
+ height: 1,
1756
+ backgroundColor: "rgba(0,0,0,0.03)",
1757
+ margin: "4px 0"
1758
+ },
1759
+ filterPanel: {
1760
+ padding: "4px 12px 8px"
1761
+ },
1762
+ filterSearch: {
1763
+ width: "100%",
1764
+ padding: "6px 8px",
1765
+ border: "1px solid rgba(0,0,0,0.06)",
1766
+ borderRadius: 6,
1767
+ fontSize: 12,
1768
+ fontFamily: "inherit",
1769
+ outline: "none",
1770
+ boxSizing: "border-box"
1771
+ },
1772
+ filterActions: {
1773
+ display: "flex",
1774
+ gap: 12,
1775
+ padding: "4px 0"
1776
+ },
1777
+ linkButton: {
1778
+ background: "none",
1779
+ border: "none",
1780
+ color: "#6366f1",
1781
+ fontSize: 11,
1782
+ cursor: "pointer",
1783
+ padding: 0,
1784
+ fontFamily: "inherit"
1785
+ },
1786
+ filterList: {
1787
+ maxHeight: 200,
1788
+ overflowY: "auto",
1789
+ border: "1px solid #f1f5f9",
1790
+ borderRadius: 6,
1791
+ marginBottom: 6
1792
+ },
1793
+ filterLoading: {
1794
+ padding: 12,
1795
+ textAlign: "center",
1796
+ color: "rgba(66,84,102,0.5)",
1797
+ fontSize: 12
1798
+ },
1799
+ filterItem: {
1800
+ display: "flex",
1801
+ alignItems: "center",
1802
+ gap: 6,
1803
+ padding: "3px 8px",
1804
+ cursor: "pointer",
1805
+ fontSize: 12
1806
+ },
1807
+ checkbox: {
1808
+ margin: 0,
1809
+ flexShrink: 0
1810
+ },
1811
+ filterValue: {
1812
+ overflow: "hidden",
1813
+ textOverflow: "ellipsis",
1814
+ whiteSpace: "nowrap"
1815
+ },
1816
+ applyButton: {
1817
+ width: "100%",
1818
+ padding: "6px 12px",
1819
+ backgroundColor: "#6366f1",
1820
+ color: "white",
1821
+ border: "none",
1822
+ borderRadius: 6,
1823
+ fontSize: 12,
1824
+ fontWeight: 500,
1825
+ cursor: "pointer",
1826
+ fontFamily: "inherit"
1827
+ }
1828
+ };
1829
+
1830
+ // src/components/ParquiLogo.tsx
1831
+ var import_react8 = require("react");
1832
+ var import_jsx_runtime3 = require("react/jsx-runtime");
1833
+ function ParquiLogo({
1834
+ size = 28,
1835
+ color = "#50ABF1",
1836
+ style
1837
+ }) {
1838
+ const clipId = (0, import_react8.useId)();
1839
+ const r = size / 28 * 3;
1840
+ return /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)(
1841
+ "svg",
1842
+ {
1843
+ width: size,
1844
+ height: size,
1845
+ viewBox: "0 0 28 28",
1846
+ fill: "none",
1847
+ xmlns: "http://www.w3.org/2000/svg",
1848
+ style: { flexShrink: 0, ...style },
1849
+ children: [
1850
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("defs", { children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("clipPath", { id: clipId, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { width: "28", height: "28", rx: r }) }) }),
1851
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("g", { clipPath: `url(#${clipId})`, children: /* @__PURE__ */ (0, import_jsx_runtime3.jsxs)("g", { transform: "rotate(45 14 14)", children: [
1852
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { x: "-20", y: "-5", width: "68", height: "10", fill: color }),
1853
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { x: "-20", y: "9", width: "32", height: "10", fill: color }),
1854
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { x: "16", y: "9", width: "32", height: "10", fill: color }),
1855
+ /* @__PURE__ */ (0, import_jsx_runtime3.jsx)("rect", { x: "-20", y: "23", width: "68", height: "10", fill: color })
1856
+ ] }) })
1857
+ ]
1858
+ }
1859
+ );
1860
+ }
1861
+
1862
+ // src/ParquetViewer.tsx
1863
+ var import_jsx_runtime4 = require("react/jsx-runtime");
1864
+ function buildFlatItems(groups) {
1865
+ const items = [];
1866
+ for (let gi = 0; gi < groups.length; gi++) {
1867
+ items.push({ type: "header", groupIndex: gi });
1868
+ if (groups[gi].expanded) {
1869
+ for (const displayIdx of groups[gi].rowIndices) {
1870
+ items.push({ type: "row", displayIndex: displayIdx });
1871
+ }
1872
+ }
1873
+ }
1874
+ return items;
1875
+ }
1876
+ function ParquetViewer({
1877
+ source,
1878
+ columns,
1879
+ onMetadataLoad,
1880
+ className,
1881
+ style
1882
+ }) {
1883
+ const { containerRef, size } = useContainerSize();
1884
+ const pq = useParquetData(source, columns);
1885
+ const onMetadataLoadRef = (0, import_react9.useRef)(onMetadataLoad);
1886
+ onMetadataLoadRef.current = onMetadataLoad;
1887
+ (0, import_react9.useEffect)(() => {
1888
+ if (pq.metadata) onMetadataLoadRef.current?.(pq.metadata);
1889
+ }, [pq.metadata]);
1890
+ const columnNames = (0, import_react9.useMemo)(() => {
1891
+ if (columns) return columns;
1892
+ return pq.metadata?.columns.map((c) => c.name) ?? [];
1893
+ }, [columns, pq.metadata]);
1894
+ const colManager = useColumnManager(columnNames);
1895
+ const [contextMenu, setContextMenu] = (0, import_react9.useState)(
1896
+ null
1897
+ );
1898
+ const isGrouped = pq.pipeline.groups.length > 0 && pq.groups.length > 0;
1899
+ const flatItems = (0, import_react9.useMemo)(() => {
1900
+ if (!isGrouped) return null;
1901
+ return buildFlatItems(pq.groups);
1902
+ }, [isGrouped, pq.groups]);
1903
+ const scrollRowCount = flatItems ? flatItems.length : pq.filteredRowCount;
1904
+ const fallbackHeight = size.height;
1905
+ const vScroll = useVirtualScroll(fallbackHeight, scrollRowCount, pq.metadata);
1906
+ const isNavigable = (0, import_react9.useMemo)(() => {
1907
+ if (!flatItems) return void 0;
1908
+ return (flatIdx) => {
1909
+ const item = flatItems[flatIdx];
1910
+ return item !== void 0 && item.type === "row";
1911
+ };
1912
+ }, [flatItems]);
1913
+ const colLayoutRef = (0, import_react9.useRef)({
1914
+ rowNumWidth: 50,
1915
+ widths: []
1916
+ });
1917
+ const scrollToCell = (0, import_react9.useCallback)(
1918
+ (row, col) => {
1919
+ vScroll.scrollToRow(row);
1920
+ const el = vScroll.scrollEl.current;
1921
+ if (!el) return;
1922
+ const { rowNumWidth: rowNumWidth2, widths } = colLayoutRef.current;
1923
+ let colLeft = rowNumWidth2;
1924
+ for (let i = 0; i < col; i++) colLeft += widths[i] ?? 0;
1925
+ const colRight = colLeft + (widths[col] ?? 0);
1926
+ const viewLeft = el.scrollLeft;
1927
+ const viewRight = viewLeft + el.clientWidth;
1928
+ if (colRight > viewRight) {
1929
+ el.scrollLeft = colRight - el.clientWidth;
1930
+ } else if (colLeft < viewLeft) {
1931
+ el.scrollLeft = colLeft;
1932
+ }
1933
+ },
1934
+ [vScroll.scrollToRow]
1935
+ );
1936
+ const keyboard = useKeyboardNavigation(scrollToCell, isNavigable);
1937
+ (0, import_react9.useEffect)(() => {
1938
+ if (scrollRowCount === 0) return;
1939
+ if (flatItems) {
1940
+ const displayIndices = [];
1941
+ for (let i = vScroll.startIndex; i < vScroll.endIndex && i < flatItems.length; i++) {
1942
+ const item = flatItems[i];
1943
+ if (item.type === "row") displayIndices.push(item.displayIndex);
1944
+ }
1945
+ if (displayIndices.length > 0) pq.ensureIndices(displayIndices);
1946
+ } else {
1947
+ pq.ensureRange(vScroll.startIndex, vScroll.endIndex);
1948
+ }
1949
+ }, [
1950
+ vScroll.startIndex,
1951
+ vScroll.endIndex,
1952
+ scrollRowCount,
1953
+ pq.renderTick,
1954
+ flatItems
1955
+ ]);
1956
+ (0, import_react9.useEffect)(() => {
1957
+ if (!colManager.resizeState.columnKey) return;
1958
+ const handleMouseMove = (e) => {
1959
+ colManager.updateResize(e.clientX);
1960
+ };
1961
+ const handleMouseUp = () => {
1962
+ colManager.endResize();
1963
+ };
1964
+ document.addEventListener("mousemove", handleMouseMove);
1965
+ document.addEventListener("mouseup", handleMouseUp);
1966
+ return () => {
1967
+ document.removeEventListener("mousemove", handleMouseMove);
1968
+ document.removeEventListener("mouseup", handleMouseUp);
1969
+ };
1970
+ }, [
1971
+ colManager.resizeState.columnKey,
1972
+ colManager.updateResize,
1973
+ colManager.endResize
1974
+ ]);
1975
+ const handleKeyDown = (0, import_react9.useCallback)(
1976
+ (e) => {
1977
+ keyboard.handleKeyDown(
1978
+ e,
1979
+ scrollRowCount,
1980
+ colManager.visibleColumns.length
1981
+ );
1982
+ },
1983
+ [keyboard, scrollRowCount, colManager.visibleColumns.length]
1984
+ );
1985
+ const handleSortClick = (0, import_react9.useCallback)(
1986
+ (column) => {
1987
+ pq.toggleSort(column);
1988
+ },
1989
+ [pq.toggleSort]
1990
+ );
1991
+ const handleContextMenu = (0, import_react9.useCallback)(
1992
+ (column, x, y) => {
1993
+ setContextMenu({ column, x, y });
1994
+ },
1995
+ []
1996
+ );
1997
+ const handleCloseContextMenu = (0, import_react9.useCallback)(() => {
1998
+ setContextMenu(null);
1999
+ }, []);
2000
+ if (!source) {
2001
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2002
+ "div",
2003
+ {
2004
+ ref: containerRef,
2005
+ className,
2006
+ style: { ...viewerStyles.container, ...style },
2007
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: viewerStyles.placeholder, children: "No parquet source provided" })
2008
+ }
2009
+ );
2010
+ }
2011
+ if (pq.loading) {
2012
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2013
+ "div",
2014
+ {
2015
+ ref: containerRef,
2016
+ className,
2017
+ style: { ...viewerStyles.container, ...style },
2018
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: viewerStyles.placeholder, children: "Loading..." })
2019
+ }
2020
+ );
2021
+ }
2022
+ if (pq.error) {
2023
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2024
+ "div",
2025
+ {
2026
+ ref: containerRef,
2027
+ className,
2028
+ style: { ...viewerStyles.container, ...style },
2029
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: viewerStyles.error, children: pq.error })
2030
+ }
2031
+ );
2032
+ }
2033
+ const visibleCols = colManager.visibleColumns;
2034
+ const displayRowCount = pq.filteredRowCount;
2035
+ const rowNumWidth = Math.max(50, String(displayRowCount).length * 9 + 24);
2036
+ colLayoutRef.current = {
2037
+ rowNumWidth,
2038
+ widths: visibleCols.map((c) => c.width)
2039
+ };
2040
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2041
+ "div",
2042
+ {
2043
+ ref: containerRef,
2044
+ className,
2045
+ style: { ...viewerStyles.container, ...style },
2046
+ tabIndex: 0,
2047
+ onKeyDown: handleKeyDown,
2048
+ role: "grid",
2049
+ "aria-rowcount": displayRowCount,
2050
+ "aria-colcount": visibleCols.length,
2051
+ children: [
2052
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("style", { children: `
2053
+ @keyframes parqui-shimmer {
2054
+ 0% { background-position: 200% 0; }
2055
+ 100% { background-position: -200% 0; }
2056
+ }
2057
+ ` }),
2058
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2059
+ "div",
2060
+ {
2061
+ ref: vScroll.scrollRef,
2062
+ onScroll: vScroll.onScroll,
2063
+ style: viewerStyles.scrollContainer,
2064
+ children: [
2065
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2066
+ "div",
2067
+ {
2068
+ style: {
2069
+ ...viewerStyles.headerRow,
2070
+ width: colManager.totalWidth + rowNumWidth,
2071
+ minWidth: "100%"
2072
+ },
2073
+ children: [
2074
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2075
+ "div",
2076
+ {
2077
+ style: {
2078
+ ...viewerStyles.rowNumHeader,
2079
+ width: rowNumWidth,
2080
+ minWidth: rowNumWidth,
2081
+ height: HEADER_HEIGHT
2082
+ },
2083
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(ParquiLogo, { size: 18, color: "#94a3b8" })
2084
+ }
2085
+ ),
2086
+ visibleCols.map((col, idx) => /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2087
+ ColumnHeader,
2088
+ {
2089
+ column: col,
2090
+ visibleIndex: idx,
2091
+ dragState: colManager.dragState,
2092
+ sortDirection: pq.getSortDirection(col.key),
2093
+ sortPriority: pq.getSortPriority(col.key),
2094
+ hasFilter: !!pq.getFilter(col.key),
2095
+ isGrouped: pq.pipeline.groups.some(
2096
+ (g) => g.column === col.key
2097
+ ),
2098
+ onDragStart: colManager.startDrag,
2099
+ onDragOver: colManager.updateDragOver,
2100
+ onDragEnd: colManager.endDrag,
2101
+ onResizeStart: colManager.startResize,
2102
+ onSortClick: handleSortClick,
2103
+ onContextMenu: handleContextMenu
2104
+ },
2105
+ col.key
2106
+ )),
2107
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2108
+ ColumnDropEndZone,
2109
+ {
2110
+ dragState: colManager.dragState,
2111
+ totalColumns: visibleCols.length,
2112
+ onDragOverEnd: colManager.updateDragOverEnd,
2113
+ onDragEnd: colManager.endDrag
2114
+ }
2115
+ )
2116
+ ]
2117
+ }
2118
+ ),
2119
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2120
+ "div",
2121
+ {
2122
+ style: {
2123
+ height: vScroll.totalHeight,
2124
+ position: "relative",
2125
+ width: colManager.totalWidth + rowNumWidth,
2126
+ minWidth: "100%"
2127
+ },
2128
+ children: /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2129
+ "div",
2130
+ {
2131
+ style: {
2132
+ position: "absolute",
2133
+ top: vScroll.offsetY,
2134
+ left: 0,
2135
+ right: 0
2136
+ },
2137
+ children: flatItems ? renderFlatItems(
2138
+ flatItems,
2139
+ pq.groups,
2140
+ vScroll.startIndex,
2141
+ vScroll.endIndex,
2142
+ visibleCols,
2143
+ pq.getRow,
2144
+ rowNumWidth,
2145
+ keyboard.activeCell,
2146
+ keyboard.handleCellClick,
2147
+ pq.toggleGroupExpanded,
2148
+ colManager.totalWidth
2149
+ ) : renderRows(
2150
+ vScroll.startIndex,
2151
+ vScroll.endIndex,
2152
+ visibleCols,
2153
+ pq.getRow,
2154
+ rowNumWidth,
2155
+ keyboard.activeCell,
2156
+ keyboard.handleCellClick
2157
+ )
2158
+ }
2159
+ )
2160
+ }
2161
+ )
2162
+ ]
2163
+ }
2164
+ ),
2165
+ contextMenu && /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2166
+ ColumnContextMenu,
2167
+ {
2168
+ column: contextMenu.column,
2169
+ x: contextMenu.x,
2170
+ y: contextMenu.y,
2171
+ sortDirection: pq.getSortDirection(contextMenu.column),
2172
+ sortPriority: pq.getSortPriority(contextMenu.column),
2173
+ activeFilter: pq.getFilter(contextMenu.column),
2174
+ isGrouped: pq.pipeline.groups.some(
2175
+ (g) => g.column === contextMenu.column
2176
+ ),
2177
+ onClose: handleCloseContextMenu,
2178
+ onSortAsc: () => {
2179
+ pq.setSort([
2180
+ ...pq.pipeline.sorts.filter(
2181
+ (s) => s.column !== contextMenu.column
2182
+ ),
2183
+ { column: contextMenu.column, direction: "asc" }
2184
+ ]);
2185
+ },
2186
+ onSortDesc: () => {
2187
+ pq.setSort([
2188
+ ...pq.pipeline.sorts.filter(
2189
+ (s) => s.column !== contextMenu.column
2190
+ ),
2191
+ { column: contextMenu.column, direction: "desc" }
2192
+ ]);
2193
+ },
2194
+ onSortClear: () => {
2195
+ pq.setSort(
2196
+ pq.pipeline.sorts.filter(
2197
+ (s) => s.column !== contextMenu.column
2198
+ )
2199
+ );
2200
+ },
2201
+ onFilterSet: (filter) => pq.setFilter(filter),
2202
+ onFilterClear: () => pq.removeFilter(contextMenu.column),
2203
+ onGroupToggle: () => pq.toggleGroup(contextMenu.column),
2204
+ loadUniqueValues: () => pq.getUniqueValues(contextMenu.column)
2205
+ }
2206
+ )
2207
+ ]
2208
+ }
2209
+ );
2210
+ }
2211
+ function renderRows(start, end, columns, getRow, rowNumWidth, activeCell, onCellClick) {
2212
+ const rows = [];
2213
+ for (let i = start; i < end; i++) {
2214
+ const row = getRow(i);
2215
+ rows.push(
2216
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2217
+ VirtualRow,
2218
+ {
2219
+ flatIndex: i,
2220
+ displayIndex: i,
2221
+ columns,
2222
+ row,
2223
+ rowNumWidth,
2224
+ isActiveRow: activeCell?.row === i,
2225
+ activeCol: activeCell?.row === i ? activeCell.col : -1,
2226
+ onCellClick
2227
+ },
2228
+ i
2229
+ )
2230
+ );
2231
+ }
2232
+ return rows;
2233
+ }
2234
+ function renderFlatItems(flatItems, groups, start, end, columns, getRow, rowNumWidth, activeCell, onCellClick, onToggleGroup, totalColWidth) {
2235
+ const items = [];
2236
+ for (let i = start; i < end && i < flatItems.length; i++) {
2237
+ const item = flatItems[i];
2238
+ if (item.type === "header") {
2239
+ const group = groups[item.groupIndex];
2240
+ items.push(
2241
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2242
+ GroupHeaderRow,
2243
+ {
2244
+ group,
2245
+ groupIndex: item.groupIndex,
2246
+ onToggle: onToggleGroup,
2247
+ totalWidth: totalColWidth + rowNumWidth
2248
+ },
2249
+ `gh-${item.groupIndex}`
2250
+ )
2251
+ );
2252
+ } else {
2253
+ const row = getRow(item.displayIndex);
2254
+ items.push(
2255
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2256
+ VirtualRow,
2257
+ {
2258
+ flatIndex: i,
2259
+ displayIndex: item.displayIndex,
2260
+ columns,
2261
+ row,
2262
+ rowNumWidth,
2263
+ isActiveRow: activeCell?.row === i,
2264
+ activeCol: activeCell?.row === i ? activeCell.col : -1,
2265
+ onCellClick
2266
+ },
2267
+ `r-${item.displayIndex}`
2268
+ )
2269
+ );
2270
+ }
2271
+ }
2272
+ return items;
2273
+ }
2274
+ function GroupHeaderRow({
2275
+ group,
2276
+ groupIndex,
2277
+ onToggle,
2278
+ totalWidth
2279
+ }) {
2280
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2281
+ "div",
2282
+ {
2283
+ style: { ...viewerStyles.groupHeader, height: ROW_HEIGHT, width: totalWidth },
2284
+ onClick: () => onToggle(groupIndex),
2285
+ role: "row",
2286
+ children: [
2287
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: viewerStyles.groupChevron, children: group.expanded ? "\u25BE" : "\u25B8" }),
2288
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("span", { style: viewerStyles.groupLabel, children: group.label }),
2289
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)("span", { style: viewerStyles.groupCount, children: [
2290
+ "(",
2291
+ group.count.toLocaleString(),
2292
+ ")"
2293
+ ] })
2294
+ ]
2295
+ }
2296
+ );
2297
+ }
2298
+ function VirtualRow({
2299
+ flatIndex,
2300
+ displayIndex,
2301
+ columns,
2302
+ row,
2303
+ rowNumWidth,
2304
+ isActiveRow,
2305
+ activeCol,
2306
+ onCellClick
2307
+ }) {
2308
+ const isAlt = displayIndex % 2 === 1;
2309
+ const isLoading = !row;
2310
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsxs)(
2311
+ "div",
2312
+ {
2313
+ style: {
2314
+ ...viewerStyles.row,
2315
+ backgroundColor: isAlt ? "#fafbfc" : "#ffffff",
2316
+ height: ROW_HEIGHT
2317
+ },
2318
+ role: "row",
2319
+ "aria-rowindex": displayIndex + 1,
2320
+ children: [
2321
+ /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2322
+ "div",
2323
+ {
2324
+ style: {
2325
+ ...viewerStyles.rowNum,
2326
+ width: rowNumWidth,
2327
+ minWidth: rowNumWidth,
2328
+ height: ROW_HEIGHT
2329
+ },
2330
+ children: displayIndex + 1
2331
+ }
2332
+ ),
2333
+ columns.map((col, colIdx) => {
2334
+ const isActive = isActiveRow && activeCol === colIdx;
2335
+ return /* @__PURE__ */ (0, import_jsx_runtime4.jsx)(
2336
+ "div",
2337
+ {
2338
+ onClick: () => onCellClick(flatIndex, colIdx),
2339
+ style: {
2340
+ ...viewerStyles.cell,
2341
+ width: col.width,
2342
+ minWidth: col.width,
2343
+ maxWidth: col.width,
2344
+ height: ROW_HEIGHT,
2345
+ ...isActive ? viewerStyles.cellActive : void 0
2346
+ },
2347
+ role: "gridcell",
2348
+ "aria-colindex": colIdx + 1,
2349
+ "aria-selected": isActive,
2350
+ children: isLoading ? /* @__PURE__ */ (0, import_jsx_runtime4.jsx)("div", { style: viewerStyles.cellSkeleton }) : formatCell(row[col.key])
2351
+ },
2352
+ col.key
2353
+ );
2354
+ })
2355
+ ]
2356
+ }
2357
+ );
2358
+ }
2359
+ function formatCell(value) {
2360
+ if (value === null || value === void 0) return "\u2205";
2361
+ if (typeof value === "object") return JSON.stringify(value);
2362
+ return String(value);
2363
+ }
2364
+ var viewerStyles = {
2365
+ container: {
2366
+ fontFamily: "'Plus Jakarta Sans', 'Inter', system-ui, sans-serif",
2367
+ fontSize: 13,
2368
+ color: "#0a2540",
2369
+ overflow: "hidden",
2370
+ display: "flex",
2371
+ flexDirection: "column",
2372
+ height: "100%",
2373
+ minHeight: 200,
2374
+ outline: "none",
2375
+ position: "relative"
2376
+ },
2377
+ placeholder: {
2378
+ padding: 40,
2379
+ textAlign: "center",
2380
+ color: "#94a3b8",
2381
+ display: "flex",
2382
+ alignItems: "center",
2383
+ justifyContent: "center",
2384
+ flex: 1
2385
+ },
2386
+ error: {
2387
+ padding: 20,
2388
+ color: "#ef4444",
2389
+ textAlign: "center",
2390
+ display: "flex",
2391
+ alignItems: "center",
2392
+ justifyContent: "center",
2393
+ flex: 1
2394
+ },
2395
+ scrollContainer: {
2396
+ overflow: "auto",
2397
+ flex: 1,
2398
+ minHeight: 0,
2399
+ position: "relative"
2400
+ },
2401
+ headerRow: {
2402
+ display: "flex",
2403
+ position: "sticky",
2404
+ top: 0,
2405
+ zIndex: 2,
2406
+ backgroundColor: "#f6f9fc"
2407
+ },
2408
+ rowNumHeader: {
2409
+ display: "flex",
2410
+ alignItems: "center",
2411
+ justifyContent: "center",
2412
+ backgroundColor: "#edf2f7",
2413
+ borderBottom: "2px solid rgba(0,0,0,0.06)",
2414
+ borderRight: "1px solid rgba(0,0,0,0.06)",
2415
+ fontWeight: 600,
2416
+ fontSize: 12,
2417
+ color: "#94a3b8",
2418
+ flexShrink: 0,
2419
+ boxSizing: "border-box"
2420
+ },
2421
+ groupHeader: {
2422
+ display: "flex",
2423
+ alignItems: "center",
2424
+ gap: 8,
2425
+ padding: "0 12px",
2426
+ backgroundColor: "#f6f9fc",
2427
+ borderBottom: "1px solid rgba(0,0,0,0.06)",
2428
+ cursor: "pointer",
2429
+ userSelect: "none",
2430
+ fontSize: 13,
2431
+ fontWeight: 500,
2432
+ boxSizing: "border-box"
2433
+ },
2434
+ groupChevron: {
2435
+ fontSize: 12,
2436
+ color: "#425466",
2437
+ width: 12
2438
+ },
2439
+ groupLabel: {
2440
+ color: "#0a2540"
2441
+ },
2442
+ groupCount: {
2443
+ color: "rgba(66,84,102,0.5)",
2444
+ fontSize: 12
2445
+ },
2446
+ row: {
2447
+ display: "flex",
2448
+ borderBottom: "1px solid rgba(0,0,0,0.03)",
2449
+ boxSizing: "border-box"
2450
+ },
2451
+ rowNum: {
2452
+ display: "flex",
2453
+ alignItems: "center",
2454
+ justifyContent: "flex-end",
2455
+ padding: "0 8px",
2456
+ backgroundColor: "#f6f9fc",
2457
+ borderRight: "1px solid rgba(0,0,0,0.06)",
2458
+ fontSize: 11,
2459
+ color: "rgba(66,84,102,0.5)",
2460
+ flexShrink: 0,
2461
+ boxSizing: "border-box",
2462
+ userSelect: "none"
2463
+ },
2464
+ cell: {
2465
+ display: "flex",
2466
+ alignItems: "center",
2467
+ padding: "0 12px",
2468
+ borderRight: "1px solid rgba(0,0,0,0.03)",
2469
+ overflow: "hidden",
2470
+ whiteSpace: "nowrap",
2471
+ textOverflow: "ellipsis",
2472
+ cursor: "default",
2473
+ flexShrink: 0,
2474
+ boxSizing: "border-box",
2475
+ color: "#0a2540"
2476
+ },
2477
+ cellActive: {
2478
+ outline: "2px solid #6366f1",
2479
+ outlineOffset: -2,
2480
+ backgroundColor: "#eef2ff",
2481
+ zIndex: 1
2482
+ },
2483
+ cellSkeleton: {
2484
+ width: "60%",
2485
+ height: 8,
2486
+ borderRadius: 3,
2487
+ background: "linear-gradient(90deg, #e9ecf0 25%, #f3f5f7 50%, #e9ecf0 75%)",
2488
+ backgroundSize: "200% 100%",
2489
+ animation: "parqui-shimmer 1.5s ease-in-out infinite",
2490
+ opacity: 0.7
2491
+ }
2492
+ };
2493
+
2494
+ // src/FileDropZone.tsx
2495
+ var import_react10 = require("react");
2496
+ var import_jsx_runtime5 = require("react/jsx-runtime");
2497
+ function FileDropZone({
2498
+ onFileSelect,
2499
+ onCustomOpen,
2500
+ accept = [".parquet"],
2501
+ compact = false,
2502
+ className,
2503
+ label,
2504
+ buttonLabel
2505
+ }) {
2506
+ const [isDragOver, setIsDragOver] = (0, import_react10.useState)(false);
2507
+ const inputRef = (0, import_react10.useRef)(null);
2508
+ const acceptString = accept.join(",");
2509
+ const handleDrop = (0, import_react10.useCallback)(
2510
+ (e) => {
2511
+ e.preventDefault();
2512
+ e.stopPropagation();
2513
+ setIsDragOver(false);
2514
+ const droppedFile = e.dataTransfer.files[0];
2515
+ if (droppedFile && accept.some((ext) => droppedFile.name.endsWith(ext))) {
2516
+ onFileSelect(droppedFile);
2517
+ }
2518
+ },
2519
+ [onFileSelect, accept]
2520
+ );
2521
+ const handleDragOver = (0, import_react10.useCallback)((e) => {
2522
+ e.preventDefault();
2523
+ e.stopPropagation();
2524
+ setIsDragOver(true);
2525
+ }, []);
2526
+ const handleDragLeave = (0, import_react10.useCallback)((e) => {
2527
+ e.preventDefault();
2528
+ e.stopPropagation();
2529
+ setIsDragOver(false);
2530
+ }, []);
2531
+ const handleFileInput = (0, import_react10.useCallback)(
2532
+ (e) => {
2533
+ const selectedFile = e.target.files?.[0];
2534
+ if (selectedFile) {
2535
+ onFileSelect(selectedFile);
2536
+ }
2537
+ if (inputRef.current) {
2538
+ inputRef.current.value = "";
2539
+ }
2540
+ },
2541
+ [onFileSelect]
2542
+ );
2543
+ const handleClick = (0, import_react10.useCallback)(() => {
2544
+ if (onCustomOpen) {
2545
+ onCustomOpen();
2546
+ } else {
2547
+ inputRef.current?.click();
2548
+ }
2549
+ }, [onCustomOpen]);
2550
+ const handleKeyDown = (0, import_react10.useCallback)(
2551
+ (e) => {
2552
+ if (e.key === "Enter" || e.key === " ") {
2553
+ e.preventDefault();
2554
+ handleClick();
2555
+ }
2556
+ },
2557
+ [handleClick]
2558
+ );
2559
+ if (compact) {
2560
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2561
+ "div",
2562
+ {
2563
+ className,
2564
+ role: "button",
2565
+ tabIndex: 0,
2566
+ "aria-label": label ?? "Drop a file here or click to select",
2567
+ onDrop: handleDrop,
2568
+ onDragOver: handleDragOver,
2569
+ onDragLeave: handleDragLeave,
2570
+ onClick: handleClick,
2571
+ onKeyDown: handleKeyDown,
2572
+ style: {
2573
+ ...compactStyles.zone,
2574
+ ...isDragOver ? compactStyles.zoneDragOver : {}
2575
+ },
2576
+ children: [
2577
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: compactStyles.icon, children: "+" }),
2578
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("span", { style: compactStyles.text, children: buttonLabel ?? "Open file" }),
2579
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2580
+ "input",
2581
+ {
2582
+ ref: inputRef,
2583
+ type: "file",
2584
+ accept: acceptString,
2585
+ onChange: handleFileInput,
2586
+ style: { display: "none" },
2587
+ tabIndex: -1,
2588
+ "aria-hidden": "true"
2589
+ }
2590
+ )
2591
+ ]
2592
+ }
2593
+ );
2594
+ }
2595
+ return /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2596
+ "div",
2597
+ {
2598
+ className,
2599
+ role: "button",
2600
+ tabIndex: 0,
2601
+ "aria-label": label ?? "Drop a parquet file here or click to select",
2602
+ onDrop: handleDrop,
2603
+ onDragOver: handleDragOver,
2604
+ onDragLeave: handleDragLeave,
2605
+ onClick: handleClick,
2606
+ onKeyDown: handleKeyDown,
2607
+ style: {
2608
+ ...fullStyles.zone,
2609
+ ...isDragOver ? fullStyles.zoneDragOver : {}
2610
+ },
2611
+ children: [
2612
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("div", { style: fullStyles.iconCircle, children: /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)(
2613
+ "svg",
2614
+ {
2615
+ width: "32",
2616
+ height: "32",
2617
+ viewBox: "0 0 24 24",
2618
+ fill: "none",
2619
+ stroke: "currentColor",
2620
+ strokeWidth: "1.5",
2621
+ strokeLinecap: "round",
2622
+ strokeLinejoin: "round",
2623
+ children: [
2624
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
2625
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("polyline", { points: "14 2 14 8 20 8" })
2626
+ ]
2627
+ }
2628
+ ) }),
2629
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsxs)("div", { style: fullStyles.textGroup, children: [
2630
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: fullStyles.title, children: label ?? "Open a Parquet file" }),
2631
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: fullStyles.hint, children: "Drag & drop a .parquet file here, or click to browse" })
2632
+ ] }),
2633
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2634
+ "button",
2635
+ {
2636
+ type: "button",
2637
+ onClick: (e) => {
2638
+ e.stopPropagation();
2639
+ handleClick();
2640
+ },
2641
+ style: fullStyles.button,
2642
+ children: buttonLabel ?? "Choose file"
2643
+ }
2644
+ ),
2645
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)("p", { style: fullStyles.supportedLabel, children: ".parquet" }),
2646
+ /* @__PURE__ */ (0, import_jsx_runtime5.jsx)(
2647
+ "input",
2648
+ {
2649
+ ref: inputRef,
2650
+ type: "file",
2651
+ accept: acceptString,
2652
+ onChange: handleFileInput,
2653
+ style: { display: "none" },
2654
+ tabIndex: -1,
2655
+ "aria-hidden": "true"
2656
+ }
2657
+ )
2658
+ ]
2659
+ }
2660
+ );
2661
+ }
2662
+ var fullStyles = {
2663
+ zone: {
2664
+ display: "flex",
2665
+ flexDirection: "column",
2666
+ alignItems: "center",
2667
+ justifyContent: "center",
2668
+ gap: 20,
2669
+ padding: "56px 48px",
2670
+ border: "2px dashed rgba(0,0,0,0.08)",
2671
+ borderRadius: 16,
2672
+ cursor: "pointer",
2673
+ transition: "all 0.2s ease",
2674
+ backgroundColor: "#f6f9fc",
2675
+ fontFamily: "'Plus Jakarta Sans', 'Inter', system-ui, sans-serif",
2676
+ outline: "none",
2677
+ maxWidth: 420,
2678
+ width: "100%"
2679
+ },
2680
+ zoneDragOver: {
2681
+ borderColor: "#6366f1",
2682
+ backgroundColor: "#eef2ff",
2683
+ transform: "scale(1.01)"
2684
+ },
2685
+ iconCircle: {
2686
+ width: 72,
2687
+ height: 72,
2688
+ borderRadius: "50%",
2689
+ backgroundColor: "rgba(0,0,0,0.04)",
2690
+ display: "flex",
2691
+ alignItems: "center",
2692
+ justifyContent: "center",
2693
+ color: "#425466",
2694
+ transition: "background-color 0.2s, color 0.2s"
2695
+ },
2696
+ textGroup: {
2697
+ textAlign: "center"
2698
+ },
2699
+ title: {
2700
+ fontSize: 17,
2701
+ fontWeight: 600,
2702
+ color: "#0a2540",
2703
+ margin: 0
2704
+ },
2705
+ hint: {
2706
+ fontSize: 13,
2707
+ color: "#425466",
2708
+ margin: "6px 0 0"
2709
+ },
2710
+ button: {
2711
+ padding: "10px 32px",
2712
+ backgroundColor: "#0a2540",
2713
+ color: "#ffffff",
2714
+ border: "none",
2715
+ borderRadius: 8,
2716
+ fontSize: 14,
2717
+ fontWeight: 600,
2718
+ cursor: "pointer",
2719
+ fontFamily: "'Plus Jakarta Sans', 'Inter', system-ui, sans-serif",
2720
+ transition: "background-color 0.15s"
2721
+ },
2722
+ supportedLabel: {
2723
+ fontSize: 11,
2724
+ color: "rgba(66,84,102,0.5)",
2725
+ margin: 0,
2726
+ letterSpacing: "0.03em"
2727
+ }
2728
+ };
2729
+ var compactStyles = {
2730
+ zone: {
2731
+ display: "flex",
2732
+ alignItems: "center",
2733
+ gap: 8,
2734
+ padding: "6px 14px",
2735
+ border: "1px dashed rgba(0,0,0,0.08)",
2736
+ borderRadius: 8,
2737
+ cursor: "pointer",
2738
+ transition: "all 0.15s ease",
2739
+ backgroundColor: "#f6f9fc",
2740
+ fontFamily: "'Plus Jakarta Sans', 'Inter', system-ui, sans-serif",
2741
+ fontSize: 13,
2742
+ color: "#425466",
2743
+ whiteSpace: "nowrap",
2744
+ outline: "none"
2745
+ },
2746
+ zoneDragOver: {
2747
+ borderColor: "#6366f1",
2748
+ backgroundColor: "#eef2ff",
2749
+ color: "#6366f1"
2750
+ },
2751
+ icon: {
2752
+ fontSize: 16,
2753
+ fontWeight: 700,
2754
+ lineHeight: 1
2755
+ },
2756
+ text: {
2757
+ lineHeight: 1
2758
+ }
2759
+ };
2760
+
2761
+ // src/ParquetExplorer.tsx
2762
+ var import_react11 = require("react");
2763
+ var import_jsx_runtime6 = require("react/jsx-runtime");
2764
+ function ParquetExplorer({
2765
+ source,
2766
+ onRequestOpen,
2767
+ className,
2768
+ style,
2769
+ ...viewerProps
2770
+ }) {
2771
+ const [currentSource, setCurrentSource] = (0, import_react11.useState)(source);
2772
+ const [fileName, setFileName] = (0, import_react11.useState)(
2773
+ source ? sourceToDisplayName(source) : ""
2774
+ );
2775
+ const [fileSize, setFileSize] = (0, import_react11.useState)(
2776
+ source ? sourceToDisplaySize(source) : void 0
2777
+ );
2778
+ const [metadata, setMetadata] = (0, import_react11.useState)(null);
2779
+ (0, import_react11.useEffect)(() => {
2780
+ setCurrentSource(source);
2781
+ setFileName(source ? sourceToDisplayName(source) : "");
2782
+ setFileSize(source ? sourceToDisplaySize(source) : void 0);
2783
+ setMetadata(null);
2784
+ }, [source]);
2785
+ const hasSource = !!currentSource;
2786
+ const handleFileSelect = (0, import_react11.useCallback)((newFile) => {
2787
+ setCurrentSource(newFile);
2788
+ setFileName(newFile.name);
2789
+ setFileSize(newFile.size);
2790
+ setMetadata(null);
2791
+ }, []);
2792
+ const handleClose = (0, import_react11.useCallback)(() => {
2793
+ setCurrentSource(void 0);
2794
+ setFileName("");
2795
+ setFileSize(void 0);
2796
+ setMetadata(null);
2797
+ }, []);
2798
+ const handleCustomOpen = (0, import_react11.useCallback)(async () => {
2799
+ if (!onRequestOpen) return;
2800
+ const result = await onRequestOpen();
2801
+ if (result) {
2802
+ setCurrentSource(result.source);
2803
+ setFileName(result.name);
2804
+ setFileSize(void 0);
2805
+ setMetadata(null);
2806
+ }
2807
+ }, [onRequestOpen]);
2808
+ const handleMetadataLoad = (0, import_react11.useCallback)((meta) => {
2809
+ setMetadata(meta);
2810
+ }, []);
2811
+ const dropZoneProps = onRequestOpen ? { onFileSelect: handleFileSelect, onCustomOpen: handleCustomOpen } : { onFileSelect: handleFileSelect };
2812
+ const fileInputRef = (0, import_react11.useRef)(null);
2813
+ const handleOpenClick = (0, import_react11.useCallback)(() => {
2814
+ if (onRequestOpen) {
2815
+ handleCustomOpen();
2816
+ } else {
2817
+ fileInputRef.current?.click();
2818
+ }
2819
+ }, [onRequestOpen, handleCustomOpen]);
2820
+ const handleInputChange = (0, import_react11.useCallback)(
2821
+ (e) => {
2822
+ const f = e.target.files?.[0];
2823
+ if (f) handleFileSelect(f);
2824
+ e.target.value = "";
2825
+ },
2826
+ [handleFileSelect]
2827
+ );
2828
+ const [showAbout, setShowAbout] = (0, import_react11.useState)(false);
2829
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2830
+ "div",
2831
+ {
2832
+ className,
2833
+ style: { ...styles.root, ...style },
2834
+ children: [
2835
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: styles.toolbar, children: [
2836
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: styles.toolbarLeft, children: [
2837
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(
2838
+ "button",
2839
+ {
2840
+ type: "button",
2841
+ onClick: handleOpenClick,
2842
+ style: styles.openButton,
2843
+ title: "Open file (Ctrl+O)",
2844
+ "aria-label": "Open file",
2845
+ children: [
2846
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FolderOpenIcon, {}),
2847
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { children: "Open" })
2848
+ ]
2849
+ }
2850
+ ),
2851
+ !onRequestOpen && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2852
+ "input",
2853
+ {
2854
+ ref: fileInputRef,
2855
+ type: "file",
2856
+ accept: ".parquet",
2857
+ style: { display: "none" },
2858
+ onChange: handleInputChange
2859
+ }
2860
+ ),
2861
+ hasSource && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)(import_jsx_runtime6.Fragment, { children: [
2862
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: styles.separator }),
2863
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: styles.fileInfoGroup, children: [
2864
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: styles.fileIcon, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FileIcon, {}) }),
2865
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: styles.fileName, children: fileName }),
2866
+ fileSize != null && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: styles.fileSizeBadge, children: formatBytes(fileSize) }),
2867
+ metadata && /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: styles.statsGroup, children: [
2868
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: styles.statDot, children: "\xB7" }),
2869
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StatChip, { value: formatNumber(metadata.rowCount), label: "rows" }),
2870
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: styles.statDot, children: "\xB7" }),
2871
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(StatChip, { value: String(metadata.columns.length), label: "cols" })
2872
+ ] })
2873
+ ] }),
2874
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2875
+ "button",
2876
+ {
2877
+ type: "button",
2878
+ onClick: handleClose,
2879
+ style: styles.iconButton,
2880
+ title: "Close file",
2881
+ "aria-label": "Close file",
2882
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(CloseIcon, {})
2883
+ }
2884
+ )
2885
+ ] })
2886
+ ] }),
2887
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: styles.toolbarRight, children: [
2888
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2889
+ "button",
2890
+ {
2891
+ type: "button",
2892
+ style: styles.iconButton,
2893
+ title: "Settings",
2894
+ "aria-label": "Settings",
2895
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(SettingsIcon, {})
2896
+ }
2897
+ ),
2898
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { style: { position: "relative" }, children: [
2899
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2900
+ "button",
2901
+ {
2902
+ type: "button",
2903
+ style: styles.iconButton,
2904
+ title: "About Parqui",
2905
+ "aria-label": "About Parqui",
2906
+ onClick: () => setShowAbout(!showAbout),
2907
+ children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(InfoIcon, {})
2908
+ }
2909
+ ),
2910
+ showAbout && /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(AboutPopover, { onClose: () => setShowAbout(false) })
2911
+ ] })
2912
+ ] })
2913
+ ] }),
2914
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: styles.content, children: hasSource ? /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(
2915
+ ParquetViewer,
2916
+ {
2917
+ source: currentSource,
2918
+ onMetadataLoad: handleMetadataLoad,
2919
+ ...viewerProps,
2920
+ style: { height: "100%", width: "100%" }
2921
+ }
2922
+ ) : /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: styles.dropZoneWrapper, children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)(FileDropZone, { ...dropZoneProps }) }) })
2923
+ ]
2924
+ }
2925
+ );
2926
+ }
2927
+ function StatChip({ value, label }) {
2928
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("span", { style: styles.stat, children: [
2929
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: styles.statValue, children: value }),
2930
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("span", { style: styles.statLabel, children: label })
2931
+ ] });
2932
+ }
2933
+ function AboutPopover({ onClose }) {
2934
+ (0, import_react11.useEffect)(() => {
2935
+ const handler = (e) => {
2936
+ if (!e.target.closest("[data-about-popover]")) {
2937
+ onClose();
2938
+ }
2939
+ };
2940
+ document.addEventListener("mousedown", handler);
2941
+ return () => document.removeEventListener("mousedown", handler);
2942
+ }, [onClose]);
2943
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("div", { "data-about-popover": true, style: styles.aboutPopover, children: [
2944
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: styles.aboutTitle, children: "Parqui" }),
2945
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: styles.aboutText, children: "A fast, lightweight Parquet file viewer." }),
2946
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("div", { style: styles.aboutVersion, children: "v0.1.0" })
2947
+ ] });
2948
+ }
2949
+ function FolderOpenIcon() {
2950
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M22 19a2 2 0 0 1-2 2H4a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h5l2 3h9a2 2 0 0 1 2 2z" }) });
2951
+ }
2952
+ function FileIcon() {
2953
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("svg", { width: "13", height: "13", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2954
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z" }),
2955
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("polyline", { points: "14 2 14 8 20 8" })
2956
+ ] });
2957
+ }
2958
+ function CloseIcon() {
2959
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("svg", { width: "14", height: "14", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2960
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "18", y1: "6", x2: "6", y2: "18" }),
2961
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "6", y1: "6", x2: "18", y2: "18" })
2962
+ ] });
2963
+ }
2964
+ function SettingsIcon() {
2965
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2966
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("circle", { cx: "12", cy: "12", r: "3" }),
2967
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("path", { d: "M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" })
2968
+ ] });
2969
+ }
2970
+ function InfoIcon() {
2971
+ return /* @__PURE__ */ (0, import_jsx_runtime6.jsxs)("svg", { width: "15", height: "15", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
2972
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("circle", { cx: "12", cy: "12", r: "10" }),
2973
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "12", y1: "16", x2: "12", y2: "12" }),
2974
+ /* @__PURE__ */ (0, import_jsx_runtime6.jsx)("line", { x1: "12", y1: "8", x2: "12.01", y2: "8" })
2975
+ ] });
2976
+ }
2977
+ function formatBytes(bytes) {
2978
+ if (bytes < 1024) return `${bytes} B`;
2979
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
2980
+ if (bytes < 1024 * 1024 * 1024) return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
2981
+ return `${(bytes / (1024 * 1024 * 1024)).toFixed(1)} GB`;
2982
+ }
2983
+ function formatNumber(n) {
2984
+ return n.toLocaleString("en-US");
2985
+ }
2986
+ var styles = {
2987
+ root: {
2988
+ fontFamily: "'Plus Jakarta Sans', 'Inter', system-ui, sans-serif",
2989
+ display: "flex",
2990
+ flexDirection: "column",
2991
+ backgroundColor: "#ffffff",
2992
+ color: "#0a2540",
2993
+ overflow: "hidden"
2994
+ },
2995
+ toolbar: {
2996
+ display: "flex",
2997
+ alignItems: "center",
2998
+ justifyContent: "space-between",
2999
+ padding: "0 6px",
3000
+ backgroundColor: "#f6f9fc",
3001
+ borderBottom: "1px solid rgba(0,0,0,0.06)",
3002
+ flexShrink: 0,
3003
+ height: 40,
3004
+ userSelect: "none"
3005
+ },
3006
+ toolbarLeft: {
3007
+ display: "flex",
3008
+ alignItems: "center",
3009
+ gap: 0,
3010
+ minWidth: 0,
3011
+ flex: 1,
3012
+ overflow: "hidden"
3013
+ },
3014
+ toolbarRight: {
3015
+ display: "flex",
3016
+ alignItems: "center",
3017
+ gap: 2,
3018
+ flexShrink: 0,
3019
+ marginLeft: 8
3020
+ },
3021
+ openButton: {
3022
+ display: "flex",
3023
+ alignItems: "center",
3024
+ gap: 5,
3025
+ padding: "4px 10px",
3026
+ fontSize: 12,
3027
+ fontWeight: 600,
3028
+ fontFamily: "inherit",
3029
+ color: "#0a2540",
3030
+ backgroundColor: "transparent",
3031
+ border: "1px solid rgba(0,0,0,0.08)",
3032
+ borderRadius: 6,
3033
+ cursor: "pointer",
3034
+ whiteSpace: "nowrap",
3035
+ flexShrink: 0,
3036
+ height: 28,
3037
+ transition: "background-color 0.15s, border-color 0.15s"
3038
+ },
3039
+ separator: {
3040
+ width: 1,
3041
+ height: 18,
3042
+ backgroundColor: "rgba(0,0,0,0.06)",
3043
+ margin: "0 10px",
3044
+ flexShrink: 0
3045
+ },
3046
+ fileInfoGroup: {
3047
+ display: "flex",
3048
+ alignItems: "center",
3049
+ gap: 6,
3050
+ minWidth: 0,
3051
+ flex: 1,
3052
+ overflow: "hidden"
3053
+ },
3054
+ fileIcon: {
3055
+ flexShrink: 0,
3056
+ color: "#425466",
3057
+ display: "flex",
3058
+ alignItems: "center"
3059
+ },
3060
+ fileName: {
3061
+ fontSize: 13,
3062
+ fontWeight: 600,
3063
+ color: "#0a2540",
3064
+ overflow: "hidden",
3065
+ textOverflow: "ellipsis",
3066
+ whiteSpace: "nowrap"
3067
+ },
3068
+ fileSizeBadge: {
3069
+ fontSize: 11,
3070
+ fontWeight: 500,
3071
+ color: "#425466",
3072
+ backgroundColor: "rgba(0,0,0,0.04)",
3073
+ padding: "1px 6px",
3074
+ borderRadius: 4,
3075
+ whiteSpace: "nowrap",
3076
+ flexShrink: 0
3077
+ },
3078
+ statsGroup: {
3079
+ display: "flex",
3080
+ alignItems: "center",
3081
+ gap: 6,
3082
+ flexShrink: 99999,
3083
+ minWidth: 0,
3084
+ overflow: "hidden",
3085
+ whiteSpace: "nowrap"
3086
+ },
3087
+ stat: {
3088
+ display: "flex",
3089
+ alignItems: "center",
3090
+ gap: 3,
3091
+ flexShrink: 0
3092
+ },
3093
+ statValue: {
3094
+ fontSize: 12,
3095
+ fontWeight: 600,
3096
+ color: "#0a2540"
3097
+ },
3098
+ statLabel: {
3099
+ fontSize: 11,
3100
+ color: "#425466"
3101
+ },
3102
+ statDot: {
3103
+ color: "rgba(0,0,0,0.15)",
3104
+ fontSize: 14,
3105
+ lineHeight: 1,
3106
+ flexShrink: 0
3107
+ },
3108
+ iconButton: {
3109
+ display: "flex",
3110
+ alignItems: "center",
3111
+ justifyContent: "center",
3112
+ width: 28,
3113
+ height: 28,
3114
+ border: "none",
3115
+ borderRadius: 6,
3116
+ backgroundColor: "transparent",
3117
+ color: "#425466",
3118
+ cursor: "pointer",
3119
+ flexShrink: 0,
3120
+ transition: "background-color 0.15s, color 0.15s"
3121
+ },
3122
+ aboutPopover: {
3123
+ position: "absolute",
3124
+ top: "calc(100% + 6px)",
3125
+ right: 0,
3126
+ width: 200,
3127
+ padding: "12px 14px",
3128
+ backgroundColor: "#ffffff",
3129
+ border: "1px solid rgba(0,0,0,0.06)",
3130
+ borderRadius: 8,
3131
+ boxShadow: "0 8px 24px rgba(0,0,0,0.08), 0 2px 6px rgba(0,0,0,0.04)",
3132
+ zIndex: 100
3133
+ },
3134
+ aboutTitle: {
3135
+ fontSize: 13,
3136
+ fontWeight: 700,
3137
+ color: "#0a2540",
3138
+ marginBottom: 4
3139
+ },
3140
+ aboutText: {
3141
+ fontSize: 12,
3142
+ color: "#425466",
3143
+ lineHeight: 1.4,
3144
+ marginBottom: 6
3145
+ },
3146
+ aboutVersion: {
3147
+ fontSize: 11,
3148
+ color: "rgba(66,84,102,0.5)"
3149
+ },
3150
+ content: {
3151
+ flex: 1,
3152
+ minHeight: 0,
3153
+ display: "flex",
3154
+ flexDirection: "column"
3155
+ },
3156
+ dropZoneWrapper: {
3157
+ flex: 1,
3158
+ display: "flex",
3159
+ alignItems: "center",
3160
+ justifyContent: "center"
3161
+ }
3162
+ };
3163
+ // Annotate the CommonJS export names for ESM import in node:
3164
+ 0 && (module.exports = {
3165
+ FileDropZone,
3166
+ HEADER_HEIGHT,
3167
+ ParquetExplorer,
3168
+ ParquetViewer,
3169
+ ParquiLogo,
3170
+ ROW_HEIGHT,
3171
+ useColumnManager,
3172
+ useContainerSize,
3173
+ useKeyboardNavigation,
3174
+ useParquetData,
3175
+ useVirtualScroll
3176
+ });
3177
+ //# sourceMappingURL=index.cjs.map