@particle-academy/react-fancy 1.7.3 → 1.8.0

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 | — |
128
129
 
129
130
  ### Rich Content
130
131
 
package/dist/index.cjs CHANGED
@@ -8629,7 +8629,7 @@ function extensionEditorClasses(extensions) {
8629
8629
  ].join(" ");
8630
8630
  }).join(" ");
8631
8631
  }
8632
- function EditorContent({ className }) {
8632
+ function EditorContent({ className, maxHeight }) {
8633
8633
  const { contentRef, lineSpacing, placeholder, extensions, _initialHtml, _onInput } = useEditor();
8634
8634
  const initialized = react.useRef(false);
8635
8635
  react.useEffect(() => {
@@ -8652,10 +8652,14 @@ function EditorContent({ className }) {
8652
8652
  "data-react-fancy-editor-content": "",
8653
8653
  "data-placeholder": placeholder,
8654
8654
  onInput: _onInput,
8655
- style: { lineHeight: lineSpacing },
8655
+ style: {
8656
+ lineHeight: lineSpacing,
8657
+ maxHeight: maxHeight ? `${maxHeight}px` : void 0
8658
+ },
8656
8659
  className: cn(
8657
8660
  "min-h-[120px] px-4 py-3 text-sm outline-none",
8658
8661
  "focus:outline-none",
8662
+ maxHeight && "overflow-y-auto",
8659
8663
  proseClasses,
8660
8664
  extClasses,
8661
8665
  "empty:before:content-[attr(data-placeholder)] empty:before:text-zinc-400 empty:before:pointer-events-none",
@@ -10024,7 +10028,7 @@ function DiagramEntity({
10024
10028
  }
10025
10029
  DiagramEntity.displayName = "DiagramEntity";
10026
10030
  var HEADER_HEIGHT = 36;
10027
- var FIELD_HEIGHT = 28;
10031
+ var FIELD_HEIGHT = 29;
10028
10032
  var SYMBOL_SIZE = 12;
10029
10033
  function oneSymbol(pt, direction) {
10030
10034
  const s = SYMBOL_SIZE * 0.6;
@@ -10121,82 +10125,32 @@ function DiagramRelation({
10121
10125
  const toFieldY = toFieldIdx >= 0 ? HEADER_HEIGHT + toFieldIdx * FIELD_HEIGHT + FIELD_HEIGHT / 2 : toRect.height / 2;
10122
10126
  const fromCx = fromRect.x + fromRect.width / 2;
10123
10127
  const toCx = toRect.x + toRect.width / 2;
10124
- const fromCy = fromRect.y + fromRect.height / 2;
10125
- const toCy = toRect.y + toRect.height / 2;
10126
- const dx = Math.abs(fromCx - toCx);
10127
- const dy = Math.abs(fromCy - toCy);
10128
10128
  let fromPt, toPt;
10129
10129
  let fromDir;
10130
10130
  let toDir;
10131
- if (dx > dy * 0.5) {
10132
- if (fromCx < toCx) {
10133
- fromPt = { x: fromRect.x + fromRect.width, y: fromRect.y + fromFieldY };
10134
- toPt = { x: toRect.x, y: toRect.y + toFieldY };
10135
- fromDir = "right";
10136
- toDir = "left";
10137
- } else {
10138
- fromPt = { x: fromRect.x, y: fromRect.y + fromFieldY };
10139
- toPt = { x: toRect.x + toRect.width, y: toRect.y + toFieldY };
10140
- fromDir = "left";
10141
- toDir = "right";
10142
- }
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";
10143
10136
  } else {
10144
- if (fromCy < toCy) {
10145
- fromPt = { x: fromRect.x + fromRect.width / 2, y: fromRect.y + fromRect.height };
10146
- toPt = { x: toRect.x + toRect.width / 2, y: toRect.y };
10147
- fromDir = "down";
10148
- toDir = "up";
10149
- } else {
10150
- fromPt = { x: fromRect.x + fromRect.width / 2, y: fromRect.y };
10151
- toPt = { x: toRect.x + toRect.width / 2, y: toRect.y + toRect.height };
10152
- fromDir = "up";
10153
- toDir = "down";
10154
- }
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";
10155
10141
  }
10156
10142
  const offsetFrom = { ...fromPt };
10157
10143
  const offsetTo = { ...toPt };
10158
- switch (fromDir) {
10159
- case "right":
10160
- offsetFrom.x += SYMBOL_SIZE;
10161
- break;
10162
- case "left":
10163
- offsetFrom.x -= SYMBOL_SIZE;
10164
- break;
10165
- case "down":
10166
- offsetFrom.y += SYMBOL_SIZE;
10167
- break;
10168
- case "up":
10169
- offsetFrom.y -= SYMBOL_SIZE;
10170
- break;
10171
- }
10172
- switch (toDir) {
10173
- case "right":
10174
- offsetTo.x += SYMBOL_SIZE;
10175
- break;
10176
- case "left":
10177
- offsetTo.x -= SYMBOL_SIZE;
10178
- break;
10179
- case "down":
10180
- offsetTo.y += SYMBOL_SIZE;
10181
- break;
10182
- case "up":
10183
- offsetTo.y -= SYMBOL_SIZE;
10184
- break;
10185
- }
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;
10186
10148
  const adx = Math.abs(offsetTo.x - offsetFrom.x);
10187
10149
  const ady = Math.abs(offsetTo.y - offsetFrom.y);
10188
- let linePath;
10189
- if (adx > ady) {
10190
- const off = adx * 0.4;
10191
- const cp1x = offsetFrom.x + (offsetTo.x > offsetFrom.x ? off : -off);
10192
- const cp2x = offsetTo.x + (offsetTo.x > offsetFrom.x ? -off : off);
10193
- linePath = `M${offsetFrom.x},${offsetFrom.y} C${cp1x},${offsetFrom.y} ${cp2x},${offsetTo.y} ${offsetTo.x},${offsetTo.y}`;
10194
- } else {
10195
- const off = Math.max(ady * 0.4, 20);
10196
- const cp1y = offsetFrom.y + (offsetTo.y > offsetFrom.y ? off : -off);
10197
- const cp2y = offsetTo.y + (offsetTo.y > offsetFrom.y ? -off : off);
10198
- linePath = `M${offsetFrom.x},${offsetFrom.y} C${offsetFrom.x},${cp1y} ${offsetTo.x},${cp2y} ${offsetTo.x},${offsetTo.y}`;
10199
- }
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}`;
10200
10154
  const startSymbol = getSymbolPath(type, "start", fromPt, fromDir);
10201
10155
  const endSymbol = getSymbolPath(type, "end", toPt, toDir);
10202
10156
  return { linePath, startSymbol, endSymbol, midX: (offsetFrom.x + offsetTo.x) / 2, midY: (offsetFrom.y + offsetTo.y) / 2 };
@@ -10549,6 +10503,155 @@ var Diagram = Object.assign(DiagramRoot, {
10549
10503
  Relation: DiagramRelation,
10550
10504
  Toolbar: DiagramToolbar
10551
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
+ });
10552
10655
 
10553
10656
  exports.Accordion = Accordion;
10554
10657
  exports.Action = Action;
@@ -10611,6 +10714,7 @@ exports.TimePicker = TimePicker;
10611
10714
  exports.Timeline = Timeline;
10612
10715
  exports.Toast = Toast;
10613
10716
  exports.Tooltip = Tooltip;
10717
+ exports.TreeNav = TreeNav;
10614
10718
  exports.cn = cn;
10615
10719
  exports.configureIcons = configureIcons;
10616
10720
  exports.find = find;
@@ -10646,5 +10750,6 @@ exports.usePopover = usePopover;
10646
10750
  exports.useSidebar = useSidebar;
10647
10751
  exports.useTabs = useTabs;
10648
10752
  exports.useToast = useToast;
10753
+ exports.useTreeNav = useTreeNav;
10649
10754
  //# sourceMappingURL=index.cjs.map
10650
10755
  //# sourceMappingURL=index.cjs.map