@alexgorbatchev/shadcn 0.0.21 → 0.0.23

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, ref, ...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,164 @@ 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
+ ref,
24
+ ...props
25
+ }) {
26
+ const nativeProps = item.getProps();
27
+ const isExpanded = item.isExpanded();
28
+ const isSelected = item.isSelected?.() ?? false;
29
+ const isInsideTarget = item.isUnorderedDragTarget?.() ?? false;
30
+ const toggle = () => {
31
+ if (isExpanded)
32
+ item.collapse();
33
+ else
34
+ item.expand();
35
+ };
36
+ const select = (event) => {
37
+ if (isRowControl(event))
38
+ return;
39
+ item.setFocused();
40
+ if (event.shiftKey)
41
+ item.selectUpTo?.(event.ctrlKey || event.metaKey);
42
+ else if (event.ctrlKey || event.metaKey)
43
+ item.toggleSelect?.();
44
+ else
45
+ item.getTree().setSelectedItems?.([item.getId()]);
46
+ if (!event.shiftKey)
47
+ item.getTree().getDataRef().current.selectUpToAnchorId = item.getId();
48
+ item.primaryAction();
49
+ };
50
+ return useRender({
51
+ defaultTagName: "div",
52
+ render,
53
+ ref: [item.registerElement, ref ?? null],
54
+ props: mergeProps({
55
+ ...nativeProps,
56
+ ref: undefined,
57
+ onKeyDown: (event) => {
58
+ if (event.target !== event.currentTarget || event.defaultPrevented || event.key !== "Enter" || event.repeat || item.getTree().getState().dnd)
59
+ return;
60
+ event.preventDefault();
61
+ item.getTree().setSelectedItems?.([item.getId()]);
62
+ item.primaryAction();
63
+ if (expandOn === "doubleClick" && item.isFolder())
64
+ toggle();
65
+ },
66
+ ...expandOn === "doubleClick" ? {
67
+ onClick: select,
68
+ onDoubleClick: (event) => {
69
+ if (!isRowControl(event) && item.isFolder())
70
+ toggle();
71
+ }
72
+ } : {},
73
+ ...isEditing ? {
74
+ draggable: false,
75
+ onClick: undefined,
76
+ onDoubleClick: undefined,
77
+ onKeyDown: undefined,
78
+ onDragStart: undefined
79
+ } : {},
80
+ style: { paddingInlineStart: `${item.getItemMeta().level * (item.getTree().getConfig().indent ?? 20) + 8}px` },
81
+ 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"),
82
+ children: /* @__PURE__ */ jsxs(Fragment, {
83
+ children: [
84
+ item.isFolder() ? /* @__PURE__ */ jsx(ChevronRightIcon, {
85
+ "aria-hidden": "true",
86
+ className: cn2("size-3.5 shrink-0", isExpanded && "rotate-90")
87
+ }) : /* @__PURE__ */ jsx("span", {
88
+ className: "size-3.5 shrink-0"
89
+ }),
90
+ /* @__PURE__ */ jsx("span", {
91
+ "aria-hidden": "true",
92
+ className: "flex shrink-0 [&>svg]:size-4",
93
+ children: startEnhancer ?? (item.isFolder() ? isExpanded ? /* @__PURE__ */ jsx(FolderOpenIcon, {}) : /* @__PURE__ */ jsx(FolderIcon, {}) : /* @__PURE__ */ jsx(FileIcon, {}))
94
+ }),
95
+ /* @__PURE__ */ jsx("div", {
96
+ className: "min-w-0 flex-1 truncate",
97
+ children: children ?? item.getItemName()
98
+ }),
99
+ endEnhancer && /* @__PURE__ */ jsx("div", {
100
+ className: "flex shrink-0 items-center gap-1 text-xs tabular-nums",
101
+ onClick: (event) => event.stopPropagation(),
102
+ onDoubleClick: (event) => event.stopPropagation(),
103
+ onKeyDown: (event) => event.stopPropagation(),
104
+ children: endEnhancer
105
+ })
106
+ ]
107
+ })
108
+ }, props),
109
+ state: {
110
+ slot: "tree-item",
111
+ selected: isSelected,
112
+ expanded: isExpanded,
113
+ dropTarget: isInsideTarget || isDropTarget,
114
+ density
115
+ }
116
+ });
117
+ }
118
+
119
+ // src/components/tree/Tree.tsx
120
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
121
+ function Tree({
122
+ tree,
123
+ label,
124
+ variant = "default",
125
+ renderIcon,
126
+ renderLabel,
127
+ renderItem,
128
+ density
129
+ }) {
130
+ const container = tree.getContainerProps(label);
131
+ const dragLine = tree.getDragLineData?.();
132
+ const element = tree.getElement();
133
+ return /* @__PURE__ */ jsxs2("div", {
134
+ ...container,
135
+ "data-density": density,
136
+ className: cn2("grid min-w-0 gap-1", variant === "default" && "rounded-sm border border-border bg-card p-2"),
137
+ onKeyDown: (event) => {
138
+ if (event.target instanceof Element && event.target.closest("input,textarea,select,form,button,[role=menu]"))
139
+ return;
140
+ container.onKeyDown?.(event);
141
+ },
142
+ children: [
143
+ tree.getItems().map((item) => renderItem ? /* @__PURE__ */ jsx2("div", {
144
+ role: "none",
145
+ children: renderItem(item)
146
+ }, item.getKey()) : /* @__PURE__ */ jsx2(TreeItem, {
147
+ item,
148
+ startEnhancer: renderIcon?.(item),
149
+ density,
150
+ children: renderLabel?.(item)
151
+ }, item.getKey())),
152
+ dragLine && /* @__PURE__ */ jsx2("span", {
153
+ "data-slot": "tree-drop-indicator",
154
+ "aria-hidden": "true",
155
+ className: "pointer-events-none absolute h-0.5 bg-primary",
156
+ style: tree.getDragLineStyle(-1 - (element?.clientTop ?? 0), -(element?.clientLeft ?? 0))
157
+ })
158
+ ]
159
+ });
160
+ }
161
+
162
+ // src/components/tree/index.ts
7
163
  import { useTree, AssistiveTreeDescription } from "@headless-tree/react";
8
164
  import {
9
165
  syncDataLoaderFeature,
@@ -13,42 +169,22 @@ import {
13
169
  checkboxesFeature,
14
170
  searchFeature,
15
171
  renamingFeature,
16
- expandAllFeature
172
+ expandAllFeature,
173
+ dragAndDropFeature,
174
+ keyboardDragAndDropFeature,
175
+ isOrderedDragTarget
17
176
  } 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
177
  export {
46
178
  AssistiveTreeDescription,
47
179
  Tree,
180
+ TreeItem,
48
181
  asyncDataLoaderFeature,
49
182
  checkboxesFeature,
183
+ dragAndDropFeature,
50
184
  expandAllFeature,
51
185
  hotkeysCoreFeature,
186
+ isOrderedDragTarget,
187
+ keyboardDragAndDropFeature,
52
188
  renamingFeature,
53
189
  searchFeature,
54
190
  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.23",
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",