@nextlyhq/ui 0.0.2-alpha.52 → 0.0.2-alpha.54

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/README.md CHANGED
@@ -73,6 +73,7 @@ as a Tailwind v3 preset from `@nextlyhq/ui/tailwind-preset`.
73
73
  **Toggles:** `Checkbox`, `RadioGroup`, `Switch`, `Collapsible`
74
74
  **Layout and disclosure:** `Accordion`, `Avatar`, `Tabs`, `Tooltip`, `Popover`
75
75
  **Resizable regions:** `ResizablePanelGroup`, `ResizablePanel`, `ResizableHandle`
76
+ **Hierarchies:** `TreeView` (virtualized, keyboard-operable)
76
77
  **Overlays:** `Dialog`, `AlertDialog`, `Sheet`
77
78
  **Menus and command palette:** `DropdownMenu`, `ContextMenu`, `Select`, `Command`
78
79
  **Feedback:** `Spinner`, `Toaster` (with `toast()` helper)
package/dist/index.cjs CHANGED
@@ -177,6 +177,7 @@ __export(index_exports, {
177
177
  TooltipContent: () => TooltipContent,
178
178
  TooltipProvider: () => TooltipProvider,
179
179
  TooltipTrigger: () => TooltipTrigger,
180
+ TreeView: () => TreeView,
180
181
  alertVariants: () => alertVariants,
181
182
  avatarVariants: () => avatarVariants,
182
183
  badgeVariants: () => badgeVariants,
@@ -2489,6 +2490,325 @@ var ResizableHandle = ({
2489
2490
  ]
2490
2491
  }
2491
2492
  );
2493
+
2494
+ // src/components/tree-view.tsx
2495
+ var import_react_virtual = require("@tanstack/react-virtual");
2496
+ var import_lucide_react14 = require("lucide-react");
2497
+ var React10 = __toESM(require("react"), 1);
2498
+ var import_jsx_runtime36 = require("react/jsx-runtime");
2499
+ var ROW_HEIGHT = 28;
2500
+ var INDENT_PER_LEVEL = 12;
2501
+ function textOf(node) {
2502
+ if (typeof node.textValue === "string") return node.textValue;
2503
+ return typeof node.label === "string" ? node.label : "";
2504
+ }
2505
+ function flatten(nodes, expanded) {
2506
+ const rows = [];
2507
+ const pending = [{ list: nodes, index: 0, level: 0 }];
2508
+ while (pending.length > 0) {
2509
+ const frame = pending[pending.length - 1];
2510
+ if (frame === void 0 || frame.index >= frame.list.length) {
2511
+ pending.pop();
2512
+ continue;
2513
+ }
2514
+ const node = frame.list[frame.index];
2515
+ const posInSet = frame.index;
2516
+ frame.index += 1;
2517
+ if (node === void 0) continue;
2518
+ const hasChildren = node.children !== void 0;
2519
+ rows.push({
2520
+ node,
2521
+ level: frame.level,
2522
+ setSize: frame.list.length,
2523
+ posInSet,
2524
+ parentId: frame.parentId,
2525
+ hasChildren
2526
+ });
2527
+ if (hasChildren && expanded.has(node.id)) {
2528
+ pending.push({
2529
+ list: node.children ?? [],
2530
+ index: 0,
2531
+ level: frame.level + 1,
2532
+ parentId: node.id
2533
+ });
2534
+ }
2535
+ }
2536
+ return rows;
2537
+ }
2538
+ function useControllable(controlled, fallback) {
2539
+ const [uncontrolled, setUncontrolled] = React10.useState(fallback);
2540
+ return [
2541
+ controlled === void 0 ? uncontrolled : controlled,
2542
+ setUncontrolled
2543
+ ];
2544
+ }
2545
+ var TreeView = React10.forwardRef(
2546
+ ({
2547
+ nodes,
2548
+ expandedIds,
2549
+ defaultExpandedIds,
2550
+ onExpandedChange,
2551
+ selectedId,
2552
+ defaultSelectedId,
2553
+ onSelectedChange,
2554
+ className,
2555
+ "aria-label": ariaLabel,
2556
+ "aria-labelledby": ariaLabelledBy,
2557
+ "aria-describedby": ariaDescribedBy,
2558
+ ...props
2559
+ }, forwardedRef) => {
2560
+ const scrollRef = React10.useRef(null);
2561
+ const attachScroll = React10.useCallback(
2562
+ (node) => {
2563
+ scrollRef.current = node;
2564
+ if (typeof forwardedRef === "function") forwardedRef(node);
2565
+ else if (forwardedRef !== null && forwardedRef !== void 0) {
2566
+ forwardedRef.current = node;
2567
+ }
2568
+ },
2569
+ [forwardedRef]
2570
+ );
2571
+ const [expandedState, setExpandedState] = useControllable(
2572
+ expandedIds === void 0 ? void 0 : [...expandedIds],
2573
+ [...defaultExpandedIds ?? []]
2574
+ );
2575
+ const expanded = React10.useMemo(
2576
+ () => new Set(expandedIds ?? expandedState),
2577
+ [expandedIds, expandedState]
2578
+ );
2579
+ const [selected, setSelected] = useControllable(
2580
+ selectedId === void 0 ? void 0 : selectedId,
2581
+ defaultSelectedId ?? null
2582
+ );
2583
+ const rows = React10.useMemo(
2584
+ () => flatten(nodes, expanded),
2585
+ [nodes, expanded]
2586
+ );
2587
+ const [activeId, setActiveId] = React10.useState(null);
2588
+ const activeIndex = Math.max(
2589
+ 0,
2590
+ rows.findIndex((row) => row.node.id === (activeId ?? selected))
2591
+ );
2592
+ const virtualizer = (0, import_react_virtual.useVirtualizer)({
2593
+ count: rows.length,
2594
+ getScrollElement: () => scrollRef.current,
2595
+ estimateSize: () => ROW_HEIGHT,
2596
+ overscan: 8
2597
+ });
2598
+ const commitExpanded = (next) => {
2599
+ const ids = [...next];
2600
+ if (expandedIds === void 0) setExpandedState(ids);
2601
+ onExpandedChange?.(ids);
2602
+ };
2603
+ const setExpansion = (id, open) => {
2604
+ const next = new Set(expanded);
2605
+ if (open) next.add(id);
2606
+ else next.delete(id);
2607
+ commitExpanded(next);
2608
+ };
2609
+ const choose = (id) => {
2610
+ if (selectedId === void 0) setSelected(id);
2611
+ onSelectedChange?.(id);
2612
+ };
2613
+ const focusRow = (index) => {
2614
+ const row = rows[index];
2615
+ if (row === void 0) return;
2616
+ setActiveId(row.node.id);
2617
+ virtualizer.scrollToIndex(index, { align: "auto" });
2618
+ requestAnimationFrame(() => {
2619
+ const element = scrollRef.current?.querySelector(
2620
+ `[data-tree-index="${index}"]`
2621
+ );
2622
+ element?.focus();
2623
+ });
2624
+ };
2625
+ const step = (from, delta) => {
2626
+ for (let index = from + delta; index >= 0 && index < rows.length; index += delta) {
2627
+ if (rows[index]?.node.disabled !== true) return index;
2628
+ }
2629
+ return from;
2630
+ };
2631
+ const typeahead = React10.useRef({ query: "", at: 0 });
2632
+ const onKeyDown = (event) => {
2633
+ const index = activeIndex;
2634
+ const row = rows[index];
2635
+ if (row === void 0) return;
2636
+ switch (event.key) {
2637
+ case "ArrowDown":
2638
+ event.preventDefault();
2639
+ focusRow(step(index, 1));
2640
+ return;
2641
+ case "ArrowUp":
2642
+ event.preventDefault();
2643
+ focusRow(step(index, -1));
2644
+ return;
2645
+ case "ArrowRight":
2646
+ event.preventDefault();
2647
+ if (row.hasChildren && !expanded.has(row.node.id)) {
2648
+ setExpansion(row.node.id, true);
2649
+ } else if (row.hasChildren) {
2650
+ for (let child = index + 1; child < rows.length && (rows[child]?.level ?? 0) > row.level; child += 1) {
2651
+ if (rows[child]?.parentId === row.node.id && rows[child]?.node.disabled !== true) {
2652
+ focusRow(child);
2653
+ break;
2654
+ }
2655
+ }
2656
+ }
2657
+ return;
2658
+ case "ArrowLeft":
2659
+ event.preventDefault();
2660
+ if (row.hasChildren && expanded.has(row.node.id)) {
2661
+ setExpansion(row.node.id, false);
2662
+ } else if (row.parentId !== void 0) {
2663
+ let ancestor = row.parentId;
2664
+ while (ancestor !== void 0) {
2665
+ const at = rows.findIndex(
2666
+ (candidate) => candidate.node.id === ancestor
2667
+ );
2668
+ if (at < 0) break;
2669
+ if (rows[at]?.node.disabled !== true) {
2670
+ focusRow(at);
2671
+ break;
2672
+ }
2673
+ ancestor = rows[at]?.parentId;
2674
+ }
2675
+ }
2676
+ return;
2677
+ case "Home":
2678
+ event.preventDefault();
2679
+ focusRow(rows[0]?.node.disabled === true ? step(0, 1) : 0);
2680
+ return;
2681
+ case "End": {
2682
+ event.preventDefault();
2683
+ const last = rows.length - 1;
2684
+ focusRow(rows[last]?.node.disabled === true ? step(last, -1) : last);
2685
+ return;
2686
+ }
2687
+ case "Enter":
2688
+ case " ":
2689
+ event.preventDefault();
2690
+ if (row.node.disabled !== true) choose(row.node.id);
2691
+ return;
2692
+ case "*": {
2693
+ event.preventDefault();
2694
+ const next = new Set(expanded);
2695
+ for (const sibling of rows) {
2696
+ if (sibling.parentId === row.parentId && sibling.hasChildren) {
2697
+ next.add(sibling.node.id);
2698
+ }
2699
+ }
2700
+ commitExpanded(next);
2701
+ return;
2702
+ }
2703
+ default:
2704
+ break;
2705
+ }
2706
+ if (event.key.length !== 1 || event.metaKey || event.ctrlKey || event.altKey) {
2707
+ return;
2708
+ }
2709
+ event.preventDefault();
2710
+ const now = Date.now();
2711
+ const state = typeahead.current;
2712
+ state.query = now - state.at > 500 ? event.key : state.query + event.key;
2713
+ state.at = now;
2714
+ const query = state.query.toLowerCase();
2715
+ for (let offset = 1; offset <= rows.length; offset += 1) {
2716
+ const candidate = rows[(index + offset) % rows.length];
2717
+ if (candidate === void 0 || candidate.node.disabled === true)
2718
+ continue;
2719
+ if (textOf(candidate.node).toLowerCase().startsWith(query)) {
2720
+ focusRow(rows.indexOf(candidate));
2721
+ return;
2722
+ }
2723
+ }
2724
+ };
2725
+ const virtualItems = virtualizer.getVirtualItems();
2726
+ const usable = (index) => rows[index]?.node.disabled !== true;
2727
+ const tabStopIndex = virtualItems.some((item) => item.index === activeIndex) && usable(activeIndex) ? activeIndex : virtualItems.find((item) => usable(item.index))?.index ?? -1;
2728
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2729
+ "div",
2730
+ {
2731
+ ref: attachScroll,
2732
+ className: cn("overflow-auto", className),
2733
+ ...props,
2734
+ children: /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2735
+ "div",
2736
+ {
2737
+ role: "tree",
2738
+ "aria-label": ariaLabel,
2739
+ "aria-labelledby": ariaLabelledBy,
2740
+ "aria-describedby": ariaDescribedBy,
2741
+ onKeyDown,
2742
+ style: { height: virtualizer.getTotalSize(), position: "relative" },
2743
+ children: virtualItems.map((item) => {
2744
+ const row = rows[item.index];
2745
+ if (row === void 0) return null;
2746
+ const isSelected = selected === row.node.id;
2747
+ return /* @__PURE__ */ (0, import_jsx_runtime36.jsxs)(
2748
+ "div",
2749
+ {
2750
+ "data-tree-index": item.index,
2751
+ role: "treeitem",
2752
+ "aria-level": row.level + 1,
2753
+ "aria-setsize": row.setSize,
2754
+ "aria-posinset": row.posInSet + 1,
2755
+ "aria-selected": isSelected,
2756
+ "aria-expanded": row.hasChildren ? expanded.has(row.node.id) : void 0,
2757
+ "aria-disabled": row.node.disabled === true ? true : void 0,
2758
+ tabIndex: item.index === tabStopIndex ? 0 : -1,
2759
+ onFocus: () => setActiveId(row.node.id),
2760
+ onClick: () => {
2761
+ if (row.node.disabled === true) return;
2762
+ setActiveId(row.node.id);
2763
+ choose(row.node.id);
2764
+ },
2765
+ className: cn(
2766
+ "absolute left-0 flex w-full select-none items-center gap-1 rounded-sm pr-2 text-sm outline-none",
2767
+ "focus-visible:ring-1 focus-visible:ring-ring",
2768
+ row.node.disabled === true ? "pointer-events-none opacity-50" : "cursor-pointer",
2769
+ isSelected ? "bg-muted text-foreground" : "hover:bg-muted/50"
2770
+ ),
2771
+ style: {
2772
+ height: item.size,
2773
+ transform: `translateY(${item.start}px)`,
2774
+ paddingLeft: 4 + row.level * INDENT_PER_LEVEL
2775
+ },
2776
+ children: [
2777
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2778
+ "span",
2779
+ {
2780
+ "aria-hidden": "true",
2781
+ className: "flex size-4 shrink-0 items-center justify-center",
2782
+ onClick: (event) => {
2783
+ if (!row.hasChildren) return;
2784
+ event.stopPropagation();
2785
+ setExpansion(row.node.id, !expanded.has(row.node.id));
2786
+ },
2787
+ children: row.hasChildren ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)(
2788
+ import_lucide_react14.ChevronRight,
2789
+ {
2790
+ className: cn(
2791
+ "size-3.5 text-muted-foreground transition-transform",
2792
+ expanded.has(row.node.id) && "rotate-90"
2793
+ )
2794
+ }
2795
+ ) : null
2796
+ }
2797
+ ),
2798
+ row.node.icon !== void 0 ? /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "flex size-4 shrink-0 items-center justify-center text-muted-foreground", children: row.node.icon }) : null,
2799
+ /* @__PURE__ */ (0, import_jsx_runtime36.jsx)("span", { className: "truncate", children: row.node.label })
2800
+ ]
2801
+ },
2802
+ row.node.id
2803
+ );
2804
+ })
2805
+ }
2806
+ )
2807
+ }
2808
+ );
2809
+ }
2810
+ );
2811
+ TreeView.displayName = "TreeView";
2492
2812
  // Annotate the CommonJS export names for ESM import in node:
2493
2813
  0 && (module.exports = {
2494
2814
  Accordion,
@@ -2637,6 +2957,7 @@ var ResizableHandle = ({
2637
2957
  TooltipContent,
2638
2958
  TooltipProvider,
2639
2959
  TooltipTrigger,
2960
+ TreeView,
2640
2961
  alertVariants,
2641
2962
  avatarVariants,
2642
2963
  badgeVariants,