@alexgorbatchev/shadcn 0.0.21 → 0.0.22

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.
@@ -4,13 +4,12 @@ import type { IDensityProps } from "../../density/types.js";
4
4
  type TreeProps<T> = IDensityProps & {
5
5
  tree: TreeInstance<T>;
6
6
  label: string;
7
+ variant?: "default" | "sidebar";
7
8
  renderIcon?: (item: ItemInstance<T>) => ReactNode;
8
9
  renderLabel?: (item: ItemInstance<T>) => ReactNode;
10
+ renderItem?: (item: ItemInstance<T>) => ReactNode;
9
11
  };
10
- /** Render a native Headless Tree instance; configure loading, selection, and actions with useTree. */
11
- declare function Tree<T>({ tree, label, renderIcon, renderLabel, density }: TreeProps<T>): ReactElement;
12
+ /** Headless Tree owns navigation, selection, expansion, and optional pointer/keyboard drag and drop. */
13
+ declare function Tree<T>({ tree, label, variant, renderIcon, renderLabel, renderItem, density, }: TreeProps<T>): ReactElement;
12
14
  export { Tree };
13
15
  export type { TreeProps };
14
- export { useTree, AssistiveTreeDescription } from "@headless-tree/react";
15
- export { syncDataLoaderFeature, asyncDataLoaderFeature, hotkeysCoreFeature, selectionFeature, checkboxesFeature, searchFeature, renamingFeature, expandAllFeature, } from "@headless-tree/core";
16
- export type { TreeInstance, ItemInstance, TreeConfig } from "@headless-tree/core";
@@ -0,0 +1,22 @@
1
+ import { useRender } from "@base-ui/react/use-render";
2
+ import type { ItemInstance } from "@headless-tree/core";
3
+ import type { ReactElement, ReactNode } from "react";
4
+ import type { IDensityProps } from "../../density/types.js";
5
+ type TreeItemState = {
6
+ slot: "tree-item";
7
+ selected: boolean;
8
+ expanded: boolean;
9
+ dropTarget: boolean;
10
+ density: IDensityProps["density"];
11
+ };
12
+ type TreeItemProps<T> = useRender.ComponentProps<"div", TreeItemState> & IDensityProps & {
13
+ item: ItemInstance<T>;
14
+ startEnhancer?: ReactNode;
15
+ endEnhancer?: ReactNode;
16
+ expandOn?: "click" | "doubleClick";
17
+ isEditing?: boolean;
18
+ isDropTarget?: boolean;
19
+ };
20
+ /** Registered tree row; compose menus with render and place counts/actions in endEnhancer. */
21
+ export declare function TreeItem<T>({ item, children, startEnhancer, endEnhancer, expandOn, isEditing, isDropTarget, density, render, ...props }: TreeItemProps<T>): ReactElement;
22
+ export type { TreeItemProps };
@@ -1,2 +1,7 @@
1
- export { Tree, useTree, AssistiveTreeDescription, syncDataLoaderFeature, asyncDataLoaderFeature, hotkeysCoreFeature, selectionFeature, checkboxesFeature, searchFeature, renamingFeature, expandAllFeature, } from "./Tree.js";
2
- export type { TreeProps, TreeInstance, ItemInstance, TreeConfig } from "./Tree.js";
1
+ export { Tree } from "./Tree.js";
2
+ export { TreeItem } from "./TreeItem.js";
3
+ export type { TreeProps } from "./Tree.js";
4
+ export type { TreeItemProps } from "./TreeItem.js";
5
+ export { useTree, AssistiveTreeDescription } from "@headless-tree/react";
6
+ export { syncDataLoaderFeature, asyncDataLoaderFeature, hotkeysCoreFeature, selectionFeature, checkboxesFeature, searchFeature, renamingFeature, expandAllFeature, dragAndDropFeature, keyboardDragAndDropFeature, isOrderedDragTarget, } from "@headless-tree/core";
7
+ export type { TreeInstance, ItemInstance, TreeConfig, DragTarget } from "@headless-tree/core";
@@ -2,8 +2,161 @@ import {
2
2
  cn2
3
3
  } from "../../chunk-cp2m7y7v.js";
4
4
 
5
- // src/components/tree/Tree.tsx
5
+ // src/components/tree/TreeItem.tsx
6
+ import { mergeProps } from "@base-ui/react/merge-props";
7
+ import { useRender } from "@base-ui/react/use-render";
6
8
  import { ChevronRightIcon, FileIcon, FolderIcon, FolderOpenIcon } from "lucide-react";
9
+ import { jsx, jsxs, Fragment } from "react/jsx-runtime";
10
+ function isRowControl(event) {
11
+ return event.target !== event.currentTarget && event.target instanceof Element && Boolean(event.target.closest("button,input,textarea,select,form"));
12
+ }
13
+ function TreeItem({
14
+ item,
15
+ children,
16
+ startEnhancer,
17
+ endEnhancer,
18
+ expandOn = "click",
19
+ isEditing = false,
20
+ isDropTarget = false,
21
+ density,
22
+ render,
23
+ ...props
24
+ }) {
25
+ const nativeProps = item.getProps();
26
+ const isExpanded = item.isExpanded();
27
+ const isSelected = item.isSelected?.() ?? false;
28
+ const isInsideTarget = item.isUnorderedDragTarget?.() ?? false;
29
+ const isAbove = item.isDragTargetAbove?.() ?? false;
30
+ const isBelow = item.isDragTargetBelow?.() ?? false;
31
+ const toggle = () => {
32
+ if (isExpanded)
33
+ item.collapse();
34
+ else
35
+ item.expand();
36
+ };
37
+ const select = (event) => {
38
+ if (isRowControl(event))
39
+ return;
40
+ item.setFocused();
41
+ if (event.shiftKey)
42
+ item.selectUpTo?.(event.ctrlKey || event.metaKey);
43
+ else if (event.ctrlKey || event.metaKey)
44
+ item.toggleSelect?.();
45
+ else
46
+ item.getTree().setSelectedItems?.([item.getId()]);
47
+ if (!event.shiftKey)
48
+ item.getTree().getDataRef().current.selectUpToAnchorId = item.getId();
49
+ item.primaryAction();
50
+ };
51
+ return useRender({
52
+ defaultTagName: "div",
53
+ render,
54
+ props: mergeProps({
55
+ ...nativeProps,
56
+ onKeyDown: (event) => {
57
+ if (event.target !== event.currentTarget || event.defaultPrevented || event.key !== "Enter" || event.repeat || item.getTree().getState().dnd)
58
+ return;
59
+ event.preventDefault();
60
+ item.getTree().setSelectedItems?.([item.getId()]);
61
+ item.primaryAction();
62
+ if (expandOn === "doubleClick" && item.isFolder())
63
+ toggle();
64
+ },
65
+ ...expandOn === "doubleClick" ? {
66
+ onClick: select,
67
+ onDoubleClick: (event) => {
68
+ if (!isRowControl(event) && item.isFolder())
69
+ toggle();
70
+ }
71
+ } : {},
72
+ ...isEditing ? {
73
+ draggable: false,
74
+ onClick: undefined,
75
+ onDoubleClick: undefined,
76
+ onKeyDown: undefined,
77
+ onDragStart: undefined
78
+ } : {},
79
+ style: { paddingInlineStart: `${item.getItemMeta().level * 20 + 8}px` },
80
+ className: cn2("relative flex min-h-control min-w-0 cursor-default items-center gap-2 rounded-sm py-1 pr-2 text-sm outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring", (isSelected || isDropTarget || isInsideTarget) && "bg-accent text-accent-foreground"),
81
+ children: /* @__PURE__ */ jsxs(Fragment, {
82
+ children: [
83
+ item.isFolder() ? /* @__PURE__ */ jsx(ChevronRightIcon, {
84
+ "aria-hidden": "true",
85
+ className: cn2("size-3.5 shrink-0", isExpanded && "rotate-90")
86
+ }) : /* @__PURE__ */ jsx("span", {
87
+ className: "size-3.5 shrink-0"
88
+ }),
89
+ /* @__PURE__ */ jsx("span", {
90
+ "aria-hidden": "true",
91
+ className: "flex shrink-0 [&>svg]:size-4",
92
+ children: startEnhancer ?? (item.isFolder() ? isExpanded ? /* @__PURE__ */ jsx(FolderOpenIcon, {}) : /* @__PURE__ */ jsx(FolderIcon, {}) : /* @__PURE__ */ jsx(FileIcon, {}))
93
+ }),
94
+ /* @__PURE__ */ jsx("div", {
95
+ className: "min-w-0 flex-1 truncate",
96
+ children: children ?? item.getItemName()
97
+ }),
98
+ endEnhancer && /* @__PURE__ */ jsx("div", {
99
+ className: "flex shrink-0 items-center gap-1 text-xs tabular-nums",
100
+ onClick: (event) => event.stopPropagation(),
101
+ onDoubleClick: (event) => event.stopPropagation(),
102
+ onKeyDown: (event) => event.stopPropagation(),
103
+ children: endEnhancer
104
+ }),
105
+ isAbove && /* @__PURE__ */ jsx("span", {
106
+ "aria-hidden": "true",
107
+ className: "pointer-events-none absolute inset-x-0 top-0 h-0.5 bg-primary"
108
+ }),
109
+ isBelow && /* @__PURE__ */ jsx("span", {
110
+ "aria-hidden": "true",
111
+ className: "pointer-events-none absolute inset-x-0 bottom-0 h-0.5 bg-primary"
112
+ })
113
+ ]
114
+ })
115
+ }, props),
116
+ state: {
117
+ slot: "tree-item",
118
+ selected: isSelected,
119
+ expanded: isExpanded,
120
+ dropTarget: isInsideTarget || isDropTarget,
121
+ density
122
+ }
123
+ });
124
+ }
125
+
126
+ // src/components/tree/Tree.tsx
127
+ import { jsx as jsx2 } from "react/jsx-runtime";
128
+ function Tree({
129
+ tree,
130
+ label,
131
+ variant = "default",
132
+ renderIcon,
133
+ renderLabel,
134
+ renderItem,
135
+ density
136
+ }) {
137
+ const container = tree.getContainerProps(label);
138
+ return /* @__PURE__ */ jsx2("div", {
139
+ ...container,
140
+ "data-density": density,
141
+ className: cn2("grid min-w-0 gap-1", variant === "default" && "rounded-sm border border-border bg-card p-2"),
142
+ onKeyDown: (event) => {
143
+ if (event.target instanceof Element && event.target.closest("input,textarea,select,form,button,[role=menu]"))
144
+ return;
145
+ container.onKeyDown?.(event);
146
+ },
147
+ children: tree.getItems().map((item) => renderItem ? /* @__PURE__ */ jsx2("div", {
148
+ role: "none",
149
+ children: renderItem(item)
150
+ }, item.getKey()) : /* @__PURE__ */ jsx2(TreeItem, {
151
+ item,
152
+ startEnhancer: renderIcon?.(item),
153
+ density,
154
+ children: renderLabel?.(item)
155
+ }, item.getKey()))
156
+ });
157
+ }
158
+
159
+ // src/components/tree/index.ts
7
160
  import { useTree, AssistiveTreeDescription } from "@headless-tree/react";
8
161
  import {
9
162
  syncDataLoaderFeature,
@@ -13,42 +166,22 @@ import {
13
166
  checkboxesFeature,
14
167
  searchFeature,
15
168
  renamingFeature,
16
- expandAllFeature
169
+ expandAllFeature,
170
+ dragAndDropFeature,
171
+ keyboardDragAndDropFeature,
172
+ isOrderedDragTarget
17
173
  } from "@headless-tree/core";
18
- import { jsx } from "react/jsx-runtime";
19
- import { createElement } from "react";
20
- function Tree({ tree, label, renderIcon, renderLabel, density }) {
21
- return /* @__PURE__ */ jsx("div", {
22
- ...tree.getContainerProps(label),
23
- "data-density": density,
24
- className: "grid gap-1 rounded-sm border border-border bg-card p-2",
25
- children: tree.getItems().map((item) => /* @__PURE__ */ createElement("div", {
26
- ...item.getProps(),
27
- key: item.getKey(),
28
- style: { paddingInlineStart: `${item.getItemMeta().level * 20 + 8}px` },
29
- className: cn2("flex min-h-control cursor-default items-center gap-2 rounded-sm py-1 pr-2 text-sm outline-none hover:bg-accent focus-visible:ring-2 focus-visible:ring-ring", tree.getState().selectedItems?.includes(item.getId()) && "bg-accent text-accent-foreground")
30
- }, item.isFolder() ? /* @__PURE__ */ jsx(ChevronRightIcon, {
31
- "aria-hidden": "true",
32
- className: cn2("size-3.5 shrink-0", item.isExpanded() && "rotate-90")
33
- }) : /* @__PURE__ */ jsx("span", {
34
- className: "size-3.5 shrink-0"
35
- }), /* @__PURE__ */ jsx("span", {
36
- "aria-hidden": "true",
37
- className: "flex shrink-0 [&>svg]:size-4",
38
- children: renderIcon ? renderIcon(item) : item.isFolder() ? item.isExpanded() ? /* @__PURE__ */ jsx(FolderOpenIcon, {}) : /* @__PURE__ */ jsx(FolderIcon, {}) : /* @__PURE__ */ jsx(FileIcon, {})
39
- }), /* @__PURE__ */ jsx("span", {
40
- className: "min-w-0 truncate",
41
- children: renderLabel ? renderLabel(item) : item.getItemName()
42
- })))
43
- });
44
- }
45
174
  export {
46
175
  AssistiveTreeDescription,
47
176
  Tree,
177
+ TreeItem,
48
178
  asyncDataLoaderFeature,
49
179
  checkboxesFeature,
180
+ dragAndDropFeature,
50
181
  expandAllFeature,
51
182
  hotkeysCoreFeature,
183
+ isOrderedDragTarget,
184
+ keyboardDragAndDropFeature,
52
185
  renamingFeature,
53
186
  searchFeature,
54
187
  selectionFeature,
package/dist/index.js CHANGED
@@ -2673,6 +2673,7 @@ function InlineEdit({
2673
2673
  }) {
2674
2674
  const input = useRef3(null);
2675
2675
  const isFinished = useRef3(false);
2676
+ const shouldCancel = useRef3(false);
2676
2677
  const [isSaving, setSaving] = useState2(false);
2677
2678
  const [error, setError] = useState2("");
2678
2679
  useEffect2(() => {
@@ -2712,14 +2713,21 @@ function InlineEdit({
2712
2713
  disabled: isDisabled || isSaving,
2713
2714
  onChange: (event) => onValueChange(event.target.value),
2714
2715
  onBlur: () => {
2715
- save();
2716
+ if (shouldCancel.current && !isFinished.current) {
2717
+ isFinished.current = true;
2718
+ onCancel();
2719
+ } else
2720
+ save();
2716
2721
  },
2717
2722
  onKeyDown: (event) => {
2718
2723
  if (event.key === "Escape") {
2719
2724
  event.preventDefault();
2720
2725
  event.stopPropagation();
2721
- isFinished.current = true;
2722
- onCancel();
2726
+ shouldCancel.current = true;
2727
+ event.currentTarget.blur();
2728
+ } else if (event.key === "Enter") {
2729
+ event.preventDefault();
2730
+ event.currentTarget.blur();
2723
2731
  }
2724
2732
  }
2725
2733
  }),
@@ -8,7 +8,14 @@ Source: src/components/stories/navigation/Tree.stories.tsx
8
8
  ## Public imports
9
9
 
10
10
  ```tsx
11
- import { Tree, useTree, syncDataLoaderFeature, hotkeysCoreFeature, selectionFeature } from "@alexgorbatchev/shadcn/tree";
11
+ import { Tree, TreeItem, useTree, syncDataLoaderFeature, hotkeysCoreFeature, selectionFeature } from "@alexgorbatchev/shadcn/tree";
12
+ import { Button, ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuGroup, ContextMenuItem, InlineEdit } from "@alexgorbatchev/shadcn";
13
+ ```
14
+
15
+ ## Supporting imports
16
+
17
+ ```tsx
18
+ import { useState } from "react";
12
19
  ```
13
20
 
14
21
  ## Default render
@@ -26,3 +33,19 @@ Storybook CSF excerpts. Default args and render apply unless overridden. Local h
26
33
  ```tsx
27
34
  {}
28
35
  ```
36
+
37
+ ### ComposedRows
38
+
39
+ ```tsx
40
+ {
41
+ render: () => <Example isComposed/>
42
+ }
43
+ ```
44
+
45
+ ### EditableRows
46
+
47
+ ```tsx
48
+ {
49
+ render: () => <EditableExample />
50
+ }
51
+ ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@alexgorbatchev/shadcn",
3
- "version": "0.0.21",
3
+ "version": "0.0.22",
4
4
  "description": "Sharp, monospace React components with compact and comfortable densities.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1043,7 +1043,7 @@
1043
1043
  "build": "bun scripts/build.ts",
1044
1044
  "typecheck": "tsc --noEmit",
1045
1045
  "lint": "oxlint --deny-warnings src .storybook scripts && oxfmt --check . && bun run lint:unused",
1046
- "lint:unused": "NODE_OPTIONS=--max-old-space-size=12288 ts-unused check tsconfig.json",
1046
+ "lint:unused": "ts-unused check tsconfig.json",
1047
1047
  "storybook": "bun scripts/buildLlmDocs.ts && storybook dev --host 127.0.0.1 -p 6006 --no-open",
1048
1048
  "build-storybook": "bun scripts/buildLlmDocs.ts && storybook build",
1049
1049
  "prepack": "bun run build",
@@ -1139,7 +1139,7 @@
1139
1139
  "storybook": "^10.6.1",
1140
1140
  "strip-ansi": "7.2.0",
1141
1141
  "tailwindcss": "^4.3.3",
1142
- "ts-unused": "3.7.0",
1142
+ "ts-unused": "3.7.1",
1143
1143
  "typescript": "^6.0.3",
1144
1144
  "vite": "^8.3.4",
1145
1145
  "vitest": "^4.1.6",