@particle-academy/react-fancy 1.7.4 → 1.8.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/README.md CHANGED
@@ -125,6 +125,7 @@ npx vite build # Build demo app (verifies imports work)
125
125
  | Breadcrumbs | Navigation breadcrumb trail with separator | [docs](docs/Breadcrumbs.md) |
126
126
  | Navbar | Responsive navigation bar with hamburger collapse | [docs](docs/Navbar.md) |
127
127
  | Pagination | Page navigation with prev/next and ellipsis | [docs](docs/Pagination.md) |
128
+ | TreeNav | Hierarchical file/folder tree with expand/collapse, selection, and extension-based icons | [docs](docs/TreeNav.md) |
128
129
 
129
130
  ### Rich Content
130
131
 
package/dist/index.cjs CHANGED
@@ -10028,7 +10028,7 @@ function DiagramEntity({
10028
10028
  }
10029
10029
  DiagramEntity.displayName = "DiagramEntity";
10030
10030
  var HEADER_HEIGHT = 36;
10031
- var FIELD_HEIGHT = 28;
10031
+ var FIELD_HEIGHT = 29;
10032
10032
  var SYMBOL_SIZE = 12;
10033
10033
  function oneSymbol(pt, direction) {
10034
10034
  const s = SYMBOL_SIZE * 0.6;
@@ -10125,82 +10125,32 @@ function DiagramRelation({
10125
10125
  const toFieldY = toFieldIdx >= 0 ? HEADER_HEIGHT + toFieldIdx * FIELD_HEIGHT + FIELD_HEIGHT / 2 : toRect.height / 2;
10126
10126
  const fromCx = fromRect.x + fromRect.width / 2;
10127
10127
  const toCx = toRect.x + toRect.width / 2;
10128
- const fromCy = fromRect.y + fromRect.height / 2;
10129
- const toCy = toRect.y + toRect.height / 2;
10130
- const dx = Math.abs(fromCx - toCx);
10131
- const dy = Math.abs(fromCy - toCy);
10132
10128
  let fromPt, toPt;
10133
10129
  let fromDir;
10134
10130
  let toDir;
10135
- if (dx > dy * 0.5) {
10136
- if (fromCx < toCx) {
10137
- fromPt = { x: fromRect.x + fromRect.width, y: fromRect.y + fromFieldY };
10138
- toPt = { x: toRect.x, y: toRect.y + toFieldY };
10139
- fromDir = "right";
10140
- toDir = "left";
10141
- } else {
10142
- fromPt = { x: fromRect.x, y: fromRect.y + fromFieldY };
10143
- toPt = { x: toRect.x + toRect.width, y: toRect.y + toFieldY };
10144
- fromDir = "left";
10145
- toDir = "right";
10146
- }
10131
+ if (fromCx <= toCx) {
10132
+ fromPt = { x: fromRect.x + fromRect.width, y: fromRect.y + fromFieldY };
10133
+ toPt = { x: toRect.x, y: toRect.y + toFieldY };
10134
+ fromDir = "right";
10135
+ toDir = "left";
10147
10136
  } else {
10148
- if (fromCy < toCy) {
10149
- fromPt = { x: fromRect.x + fromRect.width / 2, y: fromRect.y + fromRect.height };
10150
- toPt = { x: toRect.x + toRect.width / 2, y: toRect.y };
10151
- fromDir = "down";
10152
- toDir = "up";
10153
- } else {
10154
- fromPt = { x: fromRect.x + fromRect.width / 2, y: fromRect.y };
10155
- toPt = { x: toRect.x + toRect.width / 2, y: toRect.y + toRect.height };
10156
- fromDir = "up";
10157
- toDir = "down";
10158
- }
10137
+ fromPt = { x: fromRect.x, y: fromRect.y + fromFieldY };
10138
+ toPt = { x: toRect.x + toRect.width, y: toRect.y + toFieldY };
10139
+ fromDir = "left";
10140
+ toDir = "right";
10159
10141
  }
10160
10142
  const offsetFrom = { ...fromPt };
10161
10143
  const offsetTo = { ...toPt };
10162
- switch (fromDir) {
10163
- case "right":
10164
- offsetFrom.x += SYMBOL_SIZE;
10165
- break;
10166
- case "left":
10167
- offsetFrom.x -= SYMBOL_SIZE;
10168
- break;
10169
- case "down":
10170
- offsetFrom.y += SYMBOL_SIZE;
10171
- break;
10172
- case "up":
10173
- offsetFrom.y -= SYMBOL_SIZE;
10174
- break;
10175
- }
10176
- switch (toDir) {
10177
- case "right":
10178
- offsetTo.x += SYMBOL_SIZE;
10179
- break;
10180
- case "left":
10181
- offsetTo.x -= SYMBOL_SIZE;
10182
- break;
10183
- case "down":
10184
- offsetTo.y += SYMBOL_SIZE;
10185
- break;
10186
- case "up":
10187
- offsetTo.y -= SYMBOL_SIZE;
10188
- break;
10189
- }
10144
+ if (fromDir === "right") offsetFrom.x += SYMBOL_SIZE;
10145
+ else offsetFrom.x -= SYMBOL_SIZE;
10146
+ if (toDir === "left") offsetTo.x -= SYMBOL_SIZE;
10147
+ else offsetTo.x += SYMBOL_SIZE;
10190
10148
  const adx = Math.abs(offsetTo.x - offsetFrom.x);
10191
10149
  const ady = Math.abs(offsetTo.y - offsetFrom.y);
10192
- let linePath;
10193
- if (adx > ady) {
10194
- const off = adx * 0.4;
10195
- const cp1x = offsetFrom.x + (offsetTo.x > offsetFrom.x ? off : -off);
10196
- const cp2x = offsetTo.x + (offsetTo.x > offsetFrom.x ? -off : off);
10197
- linePath = `M${offsetFrom.x},${offsetFrom.y} C${cp1x},${offsetFrom.y} ${cp2x},${offsetTo.y} ${offsetTo.x},${offsetTo.y}`;
10198
- } else {
10199
- const off = Math.max(ady * 0.4, 20);
10200
- const cp1y = offsetFrom.y + (offsetTo.y > offsetFrom.y ? off : -off);
10201
- const cp2y = offsetTo.y + (offsetTo.y > offsetFrom.y ? -off : off);
10202
- linePath = `M${offsetFrom.x},${offsetFrom.y} C${offsetFrom.x},${cp1y} ${offsetTo.x},${cp2y} ${offsetTo.x},${offsetTo.y}`;
10203
- }
10150
+ const off = Math.max(adx * 0.4, ady * 0.25, 40);
10151
+ const cp1x = offsetFrom.x + (fromDir === "right" ? off : -off);
10152
+ const cp2x = offsetTo.x + (toDir === "left" ? -off : off);
10153
+ const linePath = `M${offsetFrom.x},${offsetFrom.y} C${cp1x},${offsetFrom.y} ${cp2x},${offsetTo.y} ${offsetTo.x},${offsetTo.y}`;
10204
10154
  const startSymbol = getSymbolPath(type, "start", fromPt, fromDir);
10205
10155
  const endSymbol = getSymbolPath(type, "end", toPt, toDir);
10206
10156
  return { linePath, startSymbol, endSymbol, midX: (offsetFrom.x + offsetTo.x) / 2, midY: (offsetFrom.y + offsetTo.y) / 2 };
@@ -10553,6 +10503,155 @@ var Diagram = Object.assign(DiagramRoot, {
10553
10503
  Relation: DiagramRelation,
10554
10504
  Toolbar: DiagramToolbar
10555
10505
  });
10506
+ var TreeNavContext = react.createContext(null);
10507
+ function useTreeNav() {
10508
+ const ctx = react.useContext(TreeNavContext);
10509
+ if (!ctx) {
10510
+ throw new Error("useTreeNav must be used within a <TreeNav> component");
10511
+ }
10512
+ return ctx;
10513
+ }
10514
+ var EXT_COLORS = {
10515
+ ts: "#3178c6",
10516
+ tsx: "#3178c6",
10517
+ js: "#f7df1e",
10518
+ jsx: "#f7df1e",
10519
+ php: "#777bb4",
10520
+ html: "#e34c26",
10521
+ htm: "#e34c26",
10522
+ css: "#264de4",
10523
+ json: "#a1a1aa",
10524
+ md: "#71717a",
10525
+ yaml: "#cb171e",
10526
+ yml: "#cb171e"
10527
+ };
10528
+ function FileIcon({ ext }) {
10529
+ const color = ext && EXT_COLORS[ext.toLowerCase()] || "#71717a";
10530
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", className: "shrink-0", children: [
10531
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M4 1h5.5L13 4.5V14a1 1 0 01-1 1H4a1 1 0 01-1-1V2a1 1 0 011-1z", stroke: color, strokeWidth: "1.2" }),
10532
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M9 1v4h4", stroke: color, strokeWidth: "1.2" })
10533
+ ] });
10534
+ }
10535
+ function FolderIcon({ open }) {
10536
+ if (open) {
10537
+ return /* @__PURE__ */ jsxRuntime.jsxs("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", className: "shrink-0", children: [
10538
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M1.5 3.5a1 1 0 011-1h3l1.5 1.5H13a1 1 0 011 1V5H2.5V3.5z", fill: "#fbbf24" }),
10539
+ /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M1 6h13l-1.5 7.5H2.5L1 6z", fill: "#fbbf24", opacity: "0.7" })
10540
+ ] });
10541
+ }
10542
+ return /* @__PURE__ */ jsxRuntime.jsx("svg", { width: "16", height: "16", viewBox: "0 0 16 16", fill: "none", className: "shrink-0", children: /* @__PURE__ */ jsxRuntime.jsx("path", { d: "M1.5 3a1 1 0 011-1h3l1.5 1.5H13a1 1 0 011 1v8a1 1 0 01-1 1H2.5a1 1 0 01-1-1V3z", fill: "#fbbf24" }) });
10543
+ }
10544
+ function ChevronIcon({ open }) {
10545
+ return /* @__PURE__ */ jsxRuntime.jsx(
10546
+ "svg",
10547
+ {
10548
+ width: "14",
10549
+ height: "14",
10550
+ viewBox: "0 0 24 24",
10551
+ fill: "none",
10552
+ stroke: "currentColor",
10553
+ strokeWidth: "2",
10554
+ strokeLinecap: "round",
10555
+ strokeLinejoin: "round",
10556
+ className: cn("shrink-0 transition-transform duration-150", open && "rotate-90"),
10557
+ children: /* @__PURE__ */ jsxRuntime.jsx("polyline", { points: "9 18 15 12 9 6" })
10558
+ }
10559
+ );
10560
+ }
10561
+ function TreeNode({ node, depth }) {
10562
+ const { selectedId, onSelect, expandedIds, toggle, indentSize, showIcons } = useTreeNav();
10563
+ const isFolder = node.type === "folder" || node.children && node.children.length > 0;
10564
+ const isExpanded = expandedIds.includes(node.id);
10565
+ const isSelected = selectedId === node.id;
10566
+ const paddingLeft = depth * indentSize + 4;
10567
+ const handleClick = () => {
10568
+ if (node.disabled) return;
10569
+ if (isFolder) {
10570
+ toggle(node.id);
10571
+ }
10572
+ onSelect?.(node.id, node);
10573
+ };
10574
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { "data-react-fancy-tree-node": "", children: [
10575
+ /* @__PURE__ */ jsxRuntime.jsxs(
10576
+ "button",
10577
+ {
10578
+ type: "button",
10579
+ onClick: handleClick,
10580
+ disabled: node.disabled,
10581
+ className: cn(
10582
+ "flex w-full items-center gap-1 rounded-md py-0.5 text-left text-[13px] transition-colors",
10583
+ isSelected ? "bg-blue-500/15 text-blue-600 dark:text-blue-400" : "text-zinc-700 hover:bg-zinc-100 dark:text-zinc-300 dark:hover:bg-zinc-800",
10584
+ node.disabled && "pointer-events-none opacity-40"
10585
+ ),
10586
+ style: { paddingLeft },
10587
+ children: [
10588
+ isFolder && /* @__PURE__ */ jsxRuntime.jsx(ChevronIcon, { open: isExpanded }),
10589
+ !isFolder && /* @__PURE__ */ jsxRuntime.jsx("span", { className: "w-3.5 shrink-0" }),
10590
+ showIcons && (node.icon ?? (isFolder ? /* @__PURE__ */ jsxRuntime.jsx(FolderIcon, { open: isExpanded }) : /* @__PURE__ */ jsxRuntime.jsx(FileIcon, { ext: node.ext ?? node.label.split(".").pop() }))),
10591
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate", children: node.label })
10592
+ ]
10593
+ }
10594
+ ),
10595
+ isFolder && isExpanded && node.children && /* @__PURE__ */ jsxRuntime.jsx("div", { "data-react-fancy-tree-node-children": "", children: node.children.map((child) => /* @__PURE__ */ jsxRuntime.jsx(TreeNode, { node: child, depth: depth + 1 }, child.id)) })
10596
+ ] });
10597
+ }
10598
+ TreeNode.displayName = "TreeNode";
10599
+ function collectFolderIds(nodes) {
10600
+ const ids = [];
10601
+ for (const node of nodes) {
10602
+ if (node.children && node.children.length > 0) {
10603
+ ids.push(node.id);
10604
+ ids.push(...collectFolderIds(node.children));
10605
+ }
10606
+ }
10607
+ return ids;
10608
+ }
10609
+ function TreeNavRoot({
10610
+ nodes,
10611
+ selectedId,
10612
+ onSelect,
10613
+ expandedIds: controlledExpanded,
10614
+ defaultExpandedIds,
10615
+ onExpandedChange,
10616
+ defaultExpandAll = false,
10617
+ indentSize = 16,
10618
+ showIcons = true,
10619
+ className
10620
+ }) {
10621
+ const [internalExpanded, setInternalExpanded] = react.useState(() => {
10622
+ if (defaultExpandedIds) return defaultExpandedIds;
10623
+ if (defaultExpandAll) return collectFolderIds(nodes);
10624
+ return [];
10625
+ });
10626
+ const isControlled = controlledExpanded !== void 0;
10627
+ const expandedIds = isControlled ? controlledExpanded : internalExpanded;
10628
+ const toggle = react.useCallback(
10629
+ (id) => {
10630
+ const next = expandedIds.includes(id) ? expandedIds.filter((v) => v !== id) : [...expandedIds, id];
10631
+ if (!isControlled) {
10632
+ setInternalExpanded(next);
10633
+ }
10634
+ onExpandedChange?.(next);
10635
+ },
10636
+ [expandedIds, isControlled, onExpandedChange]
10637
+ );
10638
+ const ctx = react.useMemo(
10639
+ () => ({ selectedId, onSelect, expandedIds, toggle, indentSize, showIcons }),
10640
+ [selectedId, onSelect, expandedIds, toggle, indentSize, showIcons]
10641
+ );
10642
+ return /* @__PURE__ */ jsxRuntime.jsx(TreeNavContext.Provider, { value: ctx, children: /* @__PURE__ */ jsxRuntime.jsx(
10643
+ "nav",
10644
+ {
10645
+ "data-react-fancy-tree-nav": "",
10646
+ className: cn("flex flex-col gap-0.5 py-1 text-sm", className),
10647
+ children: nodes.map((node) => /* @__PURE__ */ jsxRuntime.jsx(TreeNode, { node, depth: 0 }, node.id))
10648
+ }
10649
+ ) });
10650
+ }
10651
+ TreeNavRoot.displayName = "TreeNav";
10652
+ var TreeNav = Object.assign(TreeNavRoot, {
10653
+ Node: TreeNode
10654
+ });
10556
10655
 
10557
10656
  exports.Accordion = Accordion;
10558
10657
  exports.Action = Action;
@@ -10615,6 +10714,7 @@ exports.TimePicker = TimePicker;
10615
10714
  exports.Timeline = Timeline;
10616
10715
  exports.Toast = Toast;
10617
10716
  exports.Tooltip = Tooltip;
10717
+ exports.TreeNav = TreeNav;
10618
10718
  exports.cn = cn;
10619
10719
  exports.configureIcons = configureIcons;
10620
10720
  exports.find = find;
@@ -10650,5 +10750,6 @@ exports.usePopover = usePopover;
10650
10750
  exports.useSidebar = useSidebar;
10651
10751
  exports.useTabs = useTabs;
10652
10752
  exports.useToast = useToast;
10753
+ exports.useTreeNav = useTreeNav;
10653
10754
  //# sourceMappingURL=index.cjs.map
10654
10755
  //# sourceMappingURL=index.cjs.map