@djangocfg/ui-tools 2.1.314 → 2.1.315
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/dist/TreeRoot-DO33TIS5.mjs +4 -0
- package/dist/TreeRoot-DO33TIS5.mjs.map +1 -0
- package/dist/TreeRoot-NJOZ2DMV.cjs +19 -0
- package/dist/TreeRoot-NJOZ2DMV.cjs.map +1 -0
- package/dist/chunk-E5BP4IXF.mjs +1231 -0
- package/dist/chunk-E5BP4IXF.mjs.map +1 -0
- package/dist/chunk-MA552EWC.cjs +1282 -0
- package/dist/chunk-MA552EWC.cjs.map +1 -0
- package/dist/index.cjs +186 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +435 -2
- package/dist/index.d.ts +435 -2
- package/dist/index.mjs +60 -2
- package/dist/index.mjs.map +1 -1
- package/package.json +11 -6
- package/src/index.ts +4 -0
- package/src/tools/Tree/README.md +220 -0
- package/src/tools/Tree/Tree.story.tsx +536 -0
- package/src/tools/Tree/TreeRoot.tsx +157 -0
- package/src/tools/Tree/components/TreeChevron.tsx +39 -0
- package/src/tools/Tree/components/TreeContent.tsx +48 -0
- package/src/tools/Tree/components/TreeEmpty.tsx +21 -0
- package/src/tools/Tree/components/TreeError.tsx +24 -0
- package/src/tools/Tree/components/TreeIcon.tsx +29 -0
- package/src/tools/Tree/components/TreeIndentGuides.tsx +33 -0
- package/src/tools/Tree/components/TreeLabel.tsx +24 -0
- package/src/tools/Tree/components/TreeRow.tsx +173 -0
- package/src/tools/Tree/components/TreeSearchInput.tsx +50 -0
- package/src/tools/Tree/components/TreeSkeleton.tsx +22 -0
- package/src/tools/Tree/components/index.ts +22 -0
- package/src/tools/Tree/context/TreeContext.tsx +538 -0
- package/src/tools/Tree/context/hooks.ts +110 -0
- package/src/tools/Tree/context/index.ts +13 -0
- package/src/tools/Tree/data/appearance.ts +175 -0
- package/src/tools/Tree/data/childCache.ts +43 -0
- package/src/tools/Tree/data/createDemoTree.ts +42 -0
- package/src/tools/Tree/data/flatten.ts +51 -0
- package/src/tools/Tree/data/index.ts +24 -0
- package/src/tools/Tree/data/persist.ts +62 -0
- package/src/tools/Tree/hooks/index.ts +6 -0
- package/src/tools/Tree/hooks/useTreeKeyboard.ts +137 -0
- package/src/tools/Tree/hooks/useTreeTypeAhead.ts +100 -0
- package/src/tools/Tree/index.tsx +99 -0
- package/src/tools/Tree/lazy.tsx +14 -0
- package/src/tools/Tree/types.ts +136 -0
- package/src/tools/index.ts +75 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { ChevronDown, ChevronRight } from 'lucide-react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
import { useTreeContext } from '../context/TreeContext';
|
|
7
|
+
|
|
8
|
+
export interface TreeChevronProps {
|
|
9
|
+
isExpanded: boolean;
|
|
10
|
+
isFolder: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function TreeChevron({ isExpanded, isFolder, className }: TreeChevronProps) {
|
|
15
|
+
const { appearance } = useTreeContext();
|
|
16
|
+
const size = { width: 'var(--tree-icon-size)', height: 'var(--tree-icon-size)' };
|
|
17
|
+
|
|
18
|
+
if (!isFolder) {
|
|
19
|
+
return (
|
|
20
|
+
<span
|
|
21
|
+
aria-hidden
|
|
22
|
+
style={size}
|
|
23
|
+
className={cn('inline-block shrink-0', className)}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
const Icon = isExpanded ? ChevronDown : ChevronRight;
|
|
28
|
+
return (
|
|
29
|
+
<Icon
|
|
30
|
+
aria-hidden
|
|
31
|
+
strokeWidth={appearance.iconStrokeWidth}
|
|
32
|
+
style={size}
|
|
33
|
+
className={cn(
|
|
34
|
+
'shrink-0 text-muted-foreground/70 transition-transform',
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
/>
|
|
38
|
+
);
|
|
39
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Fragment, type ReactNode } from 'react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
import { useTreeContext } from '../context/TreeContext';
|
|
7
|
+
import type { FlatRow, TreeRowRenderProps, TreeRowSlot } from '../types';
|
|
8
|
+
import { TreeRow } from './TreeRow';
|
|
9
|
+
import { TreeEmpty } from './TreeEmpty';
|
|
10
|
+
|
|
11
|
+
export interface TreeContentProps<T> {
|
|
12
|
+
/** Custom row renderer; falls back to <TreeRow />. */
|
|
13
|
+
children?: TreeRowSlot<T>;
|
|
14
|
+
className?: string;
|
|
15
|
+
/** Override aria-label for the container. */
|
|
16
|
+
ariaLabel?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function TreeContent<T>({ children, className, ariaLabel }: TreeContentProps<T>) {
|
|
20
|
+
const { flatRows, labels, selected, focused, matchingIds } = useTreeContext<T>();
|
|
21
|
+
|
|
22
|
+
if (flatRows.length === 0) {
|
|
23
|
+
return <TreeEmpty>{labels.empty}</TreeEmpty>;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div
|
|
28
|
+
role="tree"
|
|
29
|
+
aria-label={ariaLabel ?? labels.ariaLabel}
|
|
30
|
+
className={cn('relative flex flex-col py-1', className)}
|
|
31
|
+
>
|
|
32
|
+
{flatRows.map((row: FlatRow<T>) => {
|
|
33
|
+
const slot: TreeRowRenderProps<T> = {
|
|
34
|
+
node: row.node,
|
|
35
|
+
level: row.level,
|
|
36
|
+
isSelected: selected.has(row.node.id),
|
|
37
|
+
isExpanded: row.isExpanded,
|
|
38
|
+
isFocused: focused === row.node.id,
|
|
39
|
+
isFolder: row.isFolder,
|
|
40
|
+
isLoading: row.isLoading,
|
|
41
|
+
isMatchingSearch: matchingIds.has(row.node.id),
|
|
42
|
+
};
|
|
43
|
+
const node: ReactNode = children ? children(slot) : <TreeRow row={row} />;
|
|
44
|
+
return <Fragment key={row.node.id}>{node}</Fragment>;
|
|
45
|
+
})}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
export interface TreeEmptyProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function TreeEmpty({ children, className }: TreeEmptyProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div
|
|
13
|
+
className={cn(
|
|
14
|
+
'flex h-full min-h-32 items-center justify-center px-4 py-6 text-sm text-muted-foreground',
|
|
15
|
+
className,
|
|
16
|
+
)}
|
|
17
|
+
>
|
|
18
|
+
{children}
|
|
19
|
+
</div>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { AlertCircle } from 'lucide-react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
export interface TreeErrorProps {
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function TreeError({ children, className }: TreeErrorProps) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
className={cn(
|
|
15
|
+
'flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-sm text-destructive',
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
role="alert"
|
|
19
|
+
>
|
|
20
|
+
<AlertCircle aria-hidden className="mt-0.5 size-4 shrink-0" />
|
|
21
|
+
<span>{children}</span>
|
|
22
|
+
</div>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { File, Folder, FolderOpen } from 'lucide-react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
import { useTreeContext } from '../context/TreeContext';
|
|
7
|
+
|
|
8
|
+
export interface TreeIconProps {
|
|
9
|
+
isFolder: boolean;
|
|
10
|
+
isExpanded: boolean;
|
|
11
|
+
className?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function TreeIcon({ isFolder, isExpanded, className }: TreeIconProps) {
|
|
15
|
+
const { appearance } = useTreeContext();
|
|
16
|
+
const Icon = isFolder ? (isExpanded ? FolderOpen : Folder) : File;
|
|
17
|
+
return (
|
|
18
|
+
<Icon
|
|
19
|
+
aria-hidden
|
|
20
|
+
strokeWidth={appearance.iconStrokeWidth}
|
|
21
|
+
style={{ width: 'var(--tree-icon-size)', height: 'var(--tree-icon-size)' }}
|
|
22
|
+
className={cn(
|
|
23
|
+
'shrink-0',
|
|
24
|
+
isFolder ? 'text-foreground/70' : 'text-muted-foreground/80',
|
|
25
|
+
className,
|
|
26
|
+
)}
|
|
27
|
+
/>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useTreeContext } from '../context/TreeContext';
|
|
4
|
+
|
|
5
|
+
export interface TreeIndentGuidesProps {
|
|
6
|
+
level: number;
|
|
7
|
+
indent: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Vertical guide lines under nested rows. Renders one absolute-positioned
|
|
12
|
+
* 1px column per ancestor level. Decorative — `aria-hidden` and
|
|
13
|
+
* pointer-events disabled. Opacity comes from the tree appearance.
|
|
14
|
+
*/
|
|
15
|
+
export function TreeIndentGuides({ level, indent }: TreeIndentGuidesProps) {
|
|
16
|
+
const { appearance } = useTreeContext();
|
|
17
|
+
if (level <= 0) return null;
|
|
18
|
+
return (
|
|
19
|
+
<span
|
|
20
|
+
aria-hidden
|
|
21
|
+
className="pointer-events-none absolute inset-y-0 left-0"
|
|
22
|
+
style={{ width: 8 + level * indent, opacity: appearance.indentGuideOpacity }}
|
|
23
|
+
>
|
|
24
|
+
{Array.from({ length: level }).map((_, i) => (
|
|
25
|
+
<span
|
|
26
|
+
key={i}
|
|
27
|
+
className="absolute inset-y-0 w-px bg-border"
|
|
28
|
+
style={{ left: 8 + i * indent + indent / 2 - 0.5 }}
|
|
29
|
+
/>
|
|
30
|
+
))}
|
|
31
|
+
</span>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
export interface TreeLabelProps {
|
|
6
|
+
children: React.ReactNode;
|
|
7
|
+
isMatchingSearch?: boolean;
|
|
8
|
+
className?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function TreeLabel({ children, isMatchingSearch, className }: TreeLabelProps) {
|
|
12
|
+
return (
|
|
13
|
+
<span
|
|
14
|
+
style={{ fontSize: 'var(--tree-font-size)' }}
|
|
15
|
+
className={cn(
|
|
16
|
+
'truncate leading-tight tracking-[-0.005em]',
|
|
17
|
+
isMatchingSearch && 'font-medium text-foreground',
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</span>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Loader2 } from 'lucide-react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
import { useTreeContext } from '../context/TreeContext';
|
|
7
|
+
import { radiusClass, rowStateClasses } from '../data/appearance';
|
|
8
|
+
import type { FlatRow, TreeRowRenderProps } from '../types';
|
|
9
|
+
import { TreeChevron } from './TreeChevron';
|
|
10
|
+
import { TreeIcon } from './TreeIcon';
|
|
11
|
+
import { TreeIndentGuides } from './TreeIndentGuides';
|
|
12
|
+
import { TreeLabel } from './TreeLabel';
|
|
13
|
+
|
|
14
|
+
export interface TreeRowProps<T> {
|
|
15
|
+
row: FlatRow<T>;
|
|
16
|
+
className?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function TreeRow<T>({ row, className }: TreeRowProps<T>) {
|
|
20
|
+
const ctx = useTreeContext<T>();
|
|
21
|
+
const {
|
|
22
|
+
appearance,
|
|
23
|
+
showIndentGuides,
|
|
24
|
+
selected,
|
|
25
|
+
focused,
|
|
26
|
+
matchingIds,
|
|
27
|
+
select,
|
|
28
|
+
toggle,
|
|
29
|
+
setFocus,
|
|
30
|
+
activate,
|
|
31
|
+
getItemName,
|
|
32
|
+
renderIcon,
|
|
33
|
+
renderLabel,
|
|
34
|
+
renderActions,
|
|
35
|
+
renderContextMenu,
|
|
36
|
+
} = ctx;
|
|
37
|
+
|
|
38
|
+
const { node, level, isFolder, isExpanded, isLoading } = row;
|
|
39
|
+
const isSelected = selected.has(node.id);
|
|
40
|
+
const isFocused = focused === node.id;
|
|
41
|
+
const isMatchingSearch = matchingIds.has(node.id);
|
|
42
|
+
|
|
43
|
+
const slot: TreeRowRenderProps<T> = {
|
|
44
|
+
node,
|
|
45
|
+
level,
|
|
46
|
+
isSelected,
|
|
47
|
+
isExpanded,
|
|
48
|
+
isFocused,
|
|
49
|
+
isFolder,
|
|
50
|
+
isLoading,
|
|
51
|
+
isMatchingSearch,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const handleActivate = () => {
|
|
55
|
+
if (node.disabled) return;
|
|
56
|
+
setFocus(node.id);
|
|
57
|
+
select(node.id);
|
|
58
|
+
if (isFolder) toggle(node.id);
|
|
59
|
+
else activate(node);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
|
63
|
+
handleActivate();
|
|
64
|
+
e.currentTarget.scrollIntoView?.({ block: 'nearest' });
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const handleDoubleClick = () => {
|
|
68
|
+
if (node.disabled) return;
|
|
69
|
+
if (!isFolder) activate(node);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const handleKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
73
|
+
// Match native button keyboard semantics on the row itself.
|
|
74
|
+
if (node.disabled) return;
|
|
75
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
76
|
+
e.preventDefault();
|
|
77
|
+
handleActivate();
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const trigger = (
|
|
82
|
+
<div
|
|
83
|
+
role="treeitem"
|
|
84
|
+
aria-level={level + 1}
|
|
85
|
+
aria-expanded={isFolder ? isExpanded : undefined}
|
|
86
|
+
aria-selected={isSelected || undefined}
|
|
87
|
+
aria-current={isSelected ? 'true' : undefined}
|
|
88
|
+
aria-disabled={node.disabled || undefined}
|
|
89
|
+
data-tree-row=""
|
|
90
|
+
data-id={node.id}
|
|
91
|
+
data-selected={isSelected ? 'true' : undefined}
|
|
92
|
+
data-focused={isFocused && !isSelected ? 'true' : undefined}
|
|
93
|
+
data-folder={isFolder || undefined}
|
|
94
|
+
data-expanded={isExpanded || undefined}
|
|
95
|
+
tabIndex={isFocused ? 0 : -1}
|
|
96
|
+
style={{
|
|
97
|
+
paddingLeft: 6 + level * appearance.indent,
|
|
98
|
+
height: 'var(--tree-row-height)',
|
|
99
|
+
gap: 'var(--tree-gap)',
|
|
100
|
+
}}
|
|
101
|
+
onClick={handleClick}
|
|
102
|
+
onDoubleClick={handleDoubleClick}
|
|
103
|
+
onKeyDown={handleKeyDown}
|
|
104
|
+
onFocus={() => setFocus(node.id)}
|
|
105
|
+
className={cn(
|
|
106
|
+
'group/row relative flex w-full select-none items-center pr-2 text-left',
|
|
107
|
+
'transition-colors outline-none',
|
|
108
|
+
node.disabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
|
109
|
+
radiusClass(appearance),
|
|
110
|
+
rowStateClasses(appearance),
|
|
111
|
+
'focus-visible:ring-1 focus-visible:ring-ring/50',
|
|
112
|
+
isMatchingSearch && 'ring-1 ring-primary/30',
|
|
113
|
+
node.disabled && 'opacity-50',
|
|
114
|
+
className,
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
{/* Active-row left indicator (VSCode style) */}
|
|
118
|
+
{appearance.showActiveIndicator && isSelected ? (
|
|
119
|
+
<span
|
|
120
|
+
aria-hidden
|
|
121
|
+
className={cn(
|
|
122
|
+
'absolute left-0 top-1 bottom-1 w-0.5 rounded-r-full',
|
|
123
|
+
'bg-foreground/30 group-focus-within/tree:bg-primary',
|
|
124
|
+
)}
|
|
125
|
+
/>
|
|
126
|
+
) : null}
|
|
127
|
+
|
|
128
|
+
{showIndentGuides && level > 0 ? (
|
|
129
|
+
<TreeIndentGuides level={level} indent={appearance.indent} />
|
|
130
|
+
) : null}
|
|
131
|
+
|
|
132
|
+
<TreeChevron isExpanded={isExpanded} isFolder={isFolder} />
|
|
133
|
+
|
|
134
|
+
{isLoading ? (
|
|
135
|
+
<Loader2
|
|
136
|
+
aria-hidden
|
|
137
|
+
strokeWidth={appearance.iconStrokeWidth}
|
|
138
|
+
style={{ width: 'var(--tree-icon-size)', height: 'var(--tree-icon-size)' }}
|
|
139
|
+
className="shrink-0 animate-spin text-muted-foreground/70"
|
|
140
|
+
/>
|
|
141
|
+
) : renderIcon ? (
|
|
142
|
+
renderIcon(slot)
|
|
143
|
+
) : (
|
|
144
|
+
<TreeIcon isFolder={isFolder} isExpanded={isExpanded} />
|
|
145
|
+
)}
|
|
146
|
+
|
|
147
|
+
<span
|
|
148
|
+
className="flex min-w-0 flex-1 items-center"
|
|
149
|
+
style={{ gap: 'var(--tree-gap)' }}
|
|
150
|
+
>
|
|
151
|
+
{renderLabel ? (
|
|
152
|
+
renderLabel(slot)
|
|
153
|
+
) : (
|
|
154
|
+
<TreeLabel isMatchingSearch={isMatchingSearch}>{getItemName(node)}</TreeLabel>
|
|
155
|
+
)}
|
|
156
|
+
</span>
|
|
157
|
+
|
|
158
|
+
{renderActions ? (
|
|
159
|
+
<span
|
|
160
|
+
className="ml-auto flex shrink-0 items-center gap-1 opacity-0 transition-opacity group-hover/row:opacity-100 group-focus-within/row:opacity-100"
|
|
161
|
+
onClick={(e) => e.stopPropagation()}
|
|
162
|
+
>
|
|
163
|
+
{renderActions(slot)}
|
|
164
|
+
</span>
|
|
165
|
+
) : null}
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
if (renderContextMenu) {
|
|
170
|
+
return <>{renderContextMenu(slot, trigger)}</>;
|
|
171
|
+
}
|
|
172
|
+
return trigger;
|
|
173
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Search, X } from 'lucide-react';
|
|
4
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
5
|
+
|
|
6
|
+
import { useTreeContext } from '../context/TreeContext';
|
|
7
|
+
import { useTreeSearch } from '../context/hooks';
|
|
8
|
+
|
|
9
|
+
export interface TreeSearchInputProps {
|
|
10
|
+
className?: string;
|
|
11
|
+
showMatches?: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function TreeSearchInput({ className, showMatches = true }: TreeSearchInputProps) {
|
|
15
|
+
const { labels } = useTreeContext();
|
|
16
|
+
const { query, setQuery, matchCount } = useTreeSearch();
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<div
|
|
20
|
+
className={cn(
|
|
21
|
+
'flex items-center gap-2 rounded-md border border-border bg-background px-2',
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
>
|
|
25
|
+
<Search aria-hidden className="size-3.5 shrink-0 text-muted-foreground" />
|
|
26
|
+
<input
|
|
27
|
+
type="search"
|
|
28
|
+
value={query}
|
|
29
|
+
onChange={(e) => setQuery(e.target.value)}
|
|
30
|
+
placeholder={labels.searchPlaceholder}
|
|
31
|
+
className="h-7 w-full bg-transparent text-sm outline-none placeholder:text-muted-foreground"
|
|
32
|
+
/>
|
|
33
|
+
{showMatches && query.trim().length > 0 ? (
|
|
34
|
+
<span className="shrink-0 text-xs text-muted-foreground">
|
|
35
|
+
{labels.searchMatches(matchCount)}
|
|
36
|
+
</span>
|
|
37
|
+
) : null}
|
|
38
|
+
{query.length > 0 ? (
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={() => setQuery('')}
|
|
42
|
+
aria-label="Clear search"
|
|
43
|
+
className="shrink-0 rounded p-1 text-muted-foreground hover:bg-accent hover:text-foreground"
|
|
44
|
+
>
|
|
45
|
+
<X aria-hidden className="size-3.5" />
|
|
46
|
+
</button>
|
|
47
|
+
) : null}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@djangocfg/ui-core/lib';
|
|
4
|
+
|
|
5
|
+
export interface TreeSkeletonProps {
|
|
6
|
+
rows?: number;
|
|
7
|
+
className?: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function TreeSkeleton({ rows = 6, className }: TreeSkeletonProps) {
|
|
11
|
+
return (
|
|
12
|
+
<div className={cn('flex flex-col gap-1 p-2', className)} aria-hidden>
|
|
13
|
+
{Array.from({ length: rows }).map((_, i) => (
|
|
14
|
+
<div key={i} className="flex items-center gap-2" style={{ paddingLeft: (i % 3) * 16 }}>
|
|
15
|
+
<span className="size-4 shrink-0 animate-pulse rounded bg-muted" />
|
|
16
|
+
<span className="size-4 shrink-0 animate-pulse rounded bg-muted" />
|
|
17
|
+
<span className="h-3 flex-1 animate-pulse rounded bg-muted" />
|
|
18
|
+
</div>
|
|
19
|
+
))}
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
export { TreeChevron } from './TreeChevron';
|
|
4
|
+
export type { TreeChevronProps } from './TreeChevron';
|
|
5
|
+
export { TreeIcon } from './TreeIcon';
|
|
6
|
+
export type { TreeIconProps } from './TreeIcon';
|
|
7
|
+
export { TreeLabel } from './TreeLabel';
|
|
8
|
+
export type { TreeLabelProps } from './TreeLabel';
|
|
9
|
+
export { TreeRow } from './TreeRow';
|
|
10
|
+
export type { TreeRowProps } from './TreeRow';
|
|
11
|
+
export { TreeContent } from './TreeContent';
|
|
12
|
+
export type { TreeContentProps } from './TreeContent';
|
|
13
|
+
export { TreeSearchInput } from './TreeSearchInput';
|
|
14
|
+
export type { TreeSearchInputProps } from './TreeSearchInput';
|
|
15
|
+
export { TreeEmpty } from './TreeEmpty';
|
|
16
|
+
export type { TreeEmptyProps } from './TreeEmpty';
|
|
17
|
+
export { TreeSkeleton } from './TreeSkeleton';
|
|
18
|
+
export type { TreeSkeletonProps } from './TreeSkeleton';
|
|
19
|
+
export { TreeError } from './TreeError';
|
|
20
|
+
export type { TreeErrorProps } from './TreeError';
|
|
21
|
+
export { TreeIndentGuides } from './TreeIndentGuides';
|
|
22
|
+
export type { TreeIndentGuidesProps } from './TreeIndentGuides';
|