@hachej/boring-data-explorer 0.1.40 → 0.1.41

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.
@@ -1,5 +1,5 @@
1
1
  import * as react_jsx_runtime from 'react/jsx-runtime';
2
- import { ReactNode } from 'react';
2
+ import { ReactNode, ComponentType } from 'react';
3
3
  import { ExplorerDataSource, FacetConfig, ExplorerItem, DragPayload, Facets } from '../shared/index.js';
4
4
  export { Badge, FacetValue, FacetsArgs, SearchArgs, SearchResult } from '../shared/index.js';
5
5
 
@@ -16,6 +16,12 @@ type DataExplorerProps = {
16
16
  /** Empty state shown when the top-level result has no rows and no query/filters. */
17
17
  emptyState?: ReactNode;
18
18
  searchPlaceholder?: string;
19
+ /** Optional title rendered inside the explorer toolbar (for chromeless left tabs). */
20
+ toolbarTitle?: ReactNode;
21
+ /** Optional icon rendered before toolbarTitle. */
22
+ toolbarIcon?: ComponentType<{
23
+ className?: string;
24
+ }>;
19
25
  /** Hide the search input. Default true. */
20
26
  searchable?: boolean;
21
27
  /**
@@ -26,12 +32,14 @@ type DataExplorerProps = {
26
32
  query?: string;
27
33
  /** Called when the toolbar search changes. Use with `query` for controlled per-tab search. */
28
34
  onQueryChange?: (query: string) => void;
35
+ /** Optional external chrome target for toolbar-only actions such as filters. */
36
+ toolbarPortalElement?: Element | null;
29
37
  /** Page size and debounce — passed through to useExplorerState. */
30
38
  pageSize?: number;
31
39
  debounceMs?: number;
32
40
  className?: string;
33
41
  };
34
- declare function DataExplorer({ adapter, facets: facetConfigs, groupBy, onActivate, getDragPayload, emptyState, searchPlaceholder, searchable, query, onQueryChange, pageSize, debounceMs, className, }: DataExplorerProps): react_jsx_runtime.JSX.Element;
42
+ declare function DataExplorer({ adapter, facets: facetConfigs, groupBy, onActivate, getDragPayload, emptyState, searchPlaceholder, toolbarTitle, toolbarIcon, searchable, query, onQueryChange, toolbarPortalElement, pageSize, debounceMs, className, }: DataExplorerProps): react_jsx_runtime.JSX.Element;
35
43
 
36
44
  type UseExplorerStateOptions = {
37
45
  adapter: ExplorerDataSource;
@@ -1,5 +1,6 @@
1
1
  // src/front/DataExplorer.tsx
2
2
  import { useMemo } from "react";
3
+ import { createPortal } from "react-dom";
3
4
  import { ChevronRightIcon, ChevronDownIcon, FilterIcon, SearchIcon, XIcon } from "lucide-react";
4
5
 
5
6
  // src/front/utils.ts
@@ -353,9 +354,12 @@ function DataExplorer({
353
354
  getDragPayload,
354
355
  emptyState = "No results",
355
356
  searchPlaceholder = "Search\u2026",
357
+ toolbarTitle,
358
+ toolbarIcon,
356
359
  searchable = true,
357
360
  query,
358
361
  onQueryChange,
362
+ toolbarPortalElement,
359
363
  pageSize,
360
364
  debounceMs,
361
365
  className
@@ -375,6 +379,9 @@ function DataExplorer({
375
379
  const hasFilters = Object.values(state.filters).some((v) => v.length > 0);
376
380
  const treeMode = !!groupBy && !hasQuery && !hasFilters;
377
381
  const filterCount = Object.values(state.filters).reduce((n, v) => n + v.length, 0);
382
+ const useExternalToolbarActions = Boolean(
383
+ toolbarPortalElement && !showSearch && facetConfigs?.length && !toolbarTitle
384
+ );
378
385
  const groupEntries = useMemo(() => {
379
386
  if (!treeMode || !groupBy) return [];
380
387
  const config = facetConfigs?.find((f) => f.key === groupBy);
@@ -395,11 +402,27 @@ function DataExplorer({
395
402
  }, [treeMode, groupBy, facetConfigs, state.facets]);
396
403
  const showEmpty = !state.loading && !treeMode && state.topItems.length === 0 && state.query.length === 0 && !hasFilters;
397
404
  return /* @__PURE__ */ jsxs("div", { className: cn("flex h-full flex-col", className), "data-slot": "data-explorer", children: [
398
- showSearch || facetConfigs?.length ? /* @__PURE__ */ jsx(
405
+ useExternalToolbarActions && toolbarPortalElement ? createPortal(
406
+ /* @__PURE__ */ jsx(
407
+ ExternalToolbarActions,
408
+ {
409
+ facetConfigs,
410
+ facets: state.facets,
411
+ filters: state.filters,
412
+ filterCount,
413
+ onToggleFilter: state.toggleFilter,
414
+ onClearFilters: state.clearFilters
415
+ }
416
+ ),
417
+ toolbarPortalElement
418
+ ) : null,
419
+ (showSearch || facetConfigs?.length) && !useExternalToolbarActions ? /* @__PURE__ */ jsx(
399
420
  Toolbar,
400
421
  {
401
422
  searchable: showSearch,
402
423
  searchPlaceholder,
424
+ title: toolbarTitle,
425
+ icon: toolbarIcon,
403
426
  query: state.query,
404
427
  onQueryChange: setToolbarQuery,
405
428
  facetConfigs,
@@ -439,6 +462,8 @@ function DataExplorer({
439
462
  function Toolbar({
440
463
  searchable,
441
464
  searchPlaceholder,
465
+ title,
466
+ icon: Icon,
442
467
  query,
443
468
  onQueryChange,
444
469
  facetConfigs,
@@ -450,8 +475,12 @@ function Toolbar({
450
475
  total
451
476
  }) {
452
477
  return /* @__PURE__ */ jsxs(UiToolbar, { className: "border-b border-border/60 px-2 py-1.5", children: [
478
+ title ? /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 items-center gap-1.5 px-1", children: [
479
+ Icon ? /* @__PURE__ */ jsx(Icon, { className: "h-4 w-4 shrink-0 text-foreground/80" }) : null,
480
+ /* @__PURE__ */ jsx("span", { className: "truncate text-[14px] font-medium tracking-tight text-foreground", children: title })
481
+ ] }) : null,
453
482
  total != null ? /* @__PURE__ */ jsx("span", { className: "px-1 font-mono text-[10.5px] uppercase tracking-[0.05em] text-muted-foreground/80", children: total.toLocaleString() }) : null,
454
- /* @__PURE__ */ jsx("div", { className: "flex-1" }),
483
+ /* @__PURE__ */ jsx("div", { className: title ? "w-1 shrink-0" : "flex-1" }),
455
484
  searchable ? /* @__PURE__ */ jsxs(Popover, { children: [
456
485
  /* @__PURE__ */ jsxs(
457
486
  PopoverTrigger,
@@ -485,47 +514,71 @@ function Toolbar({
485
514
  ] }) : null
486
515
  ] })
487
516
  ] }) : null,
488
- facetConfigs?.length ? /* @__PURE__ */ jsxs(Popover, { children: [
489
- /* @__PURE__ */ jsxs(
490
- PopoverTrigger,
491
- {
492
- "aria-label": "Filters",
493
- className: cn(
494
- "inline-flex h-7 items-center gap-1 rounded-sm px-1.5 text-[11px] text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground",
495
- filterCount > 0 && "bg-muted text-foreground"
496
- ),
497
- children: [
498
- /* @__PURE__ */ jsx(FilterIcon, { size: 12 }),
499
- filterCount > 0 ? /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px]", children: filterCount }) : null
500
- ]
501
- }
502
- ),
503
- /* @__PURE__ */ jsxs(
504
- PopoverContent,
505
- {
506
- side: "right",
507
- align: "start",
508
- sideOffset: 8,
509
- className: "w-64 space-y-3 p-3 text-[12px]",
510
- children: [
511
- facetConfigs.map((config) => /* @__PURE__ */ jsx(
512
- FacetSection,
513
- {
514
- config,
515
- values: facets?.[config.key] ?? [],
516
- selected: filters[config.key] ?? [],
517
- onToggle: onToggleFilter
518
- },
519
- config.key
520
- )),
521
- filterCount > 0 ? /* @__PURE__ */ jsxs(Button, { type: "button", variant: "ghost", size: "xs", onClick: onClearFilters, className: "gap-1 text-[11px] text-muted-foreground hover:text-foreground", children: [
522
- /* @__PURE__ */ jsx(XIcon, { size: 11 }),
523
- " Clear all"
524
- ] }) : null
525
- ]
526
- }
527
- )
528
- ] }) : null
517
+ /* @__PURE__ */ jsx(
518
+ FilterControl,
519
+ {
520
+ facetConfigs,
521
+ facets,
522
+ filters,
523
+ filterCount,
524
+ onToggleFilter,
525
+ onClearFilters
526
+ }
527
+ )
528
+ ] });
529
+ }
530
+ function ExternalToolbarActions(props) {
531
+ return /* @__PURE__ */ jsx(FilterControl, { ...props });
532
+ }
533
+ function FilterControl({
534
+ facetConfigs,
535
+ facets,
536
+ filters,
537
+ filterCount,
538
+ onToggleFilter,
539
+ onClearFilters
540
+ }) {
541
+ if (!facetConfigs?.length) return null;
542
+ return /* @__PURE__ */ jsxs(Popover, { children: [
543
+ /* @__PURE__ */ jsxs(
544
+ PopoverTrigger,
545
+ {
546
+ "aria-label": "Filters",
547
+ className: cn(
548
+ "inline-flex h-7 items-center gap-1 rounded-sm px-1.5 text-[11px] text-muted-foreground transition-colors hover:bg-muted/60 hover:text-foreground",
549
+ filterCount > 0 && "bg-muted text-foreground"
550
+ ),
551
+ children: [
552
+ /* @__PURE__ */ jsx(FilterIcon, { size: 12 }),
553
+ filterCount > 0 ? /* @__PURE__ */ jsx("span", { className: "font-mono text-[10px]", children: filterCount }) : null
554
+ ]
555
+ }
556
+ ),
557
+ /* @__PURE__ */ jsxs(
558
+ PopoverContent,
559
+ {
560
+ side: "right",
561
+ align: "start",
562
+ sideOffset: 8,
563
+ className: "w-64 space-y-3 p-3 text-[12px]",
564
+ children: [
565
+ facetConfigs.map((config) => /* @__PURE__ */ jsx(
566
+ FacetSection,
567
+ {
568
+ config,
569
+ values: facets?.[config.key] ?? [],
570
+ selected: filters[config.key] ?? [],
571
+ onToggle: onToggleFilter
572
+ },
573
+ config.key
574
+ )),
575
+ filterCount > 0 ? /* @__PURE__ */ jsxs(Button, { type: "button", variant: "ghost", size: "xs", onClick: onClearFilters, className: "gap-1 text-[11px] text-muted-foreground hover:text-foreground", children: [
576
+ /* @__PURE__ */ jsx(XIcon, { size: 11 }),
577
+ " Clear all"
578
+ ] }) : null
579
+ ]
580
+ }
581
+ )
529
582
  ] });
530
583
  }
531
584
  function FacetSection({
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hachej/boring-data-explorer",
3
- "version": "0.1.40",
3
+ "version": "0.1.41",
4
4
  "type": "module",
5
5
  "private": false,
6
6
  "license": "MIT",
@@ -41,7 +41,7 @@
41
41
  "clsx": "^2.1.1",
42
42
  "lucide-react": "^1.8.0",
43
43
  "tailwind-merge": "^3.5.0",
44
- "@hachej/boring-ui-kit": "0.1.40"
44
+ "@hachej/boring-ui-kit": "0.1.41"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@testing-library/jest-dom": "^6.9.1",