@djangocfg/ui-tools 2.1.429 → 2.1.431

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.
@@ -1,5 +1,6 @@
1
1
  'use client';
2
2
 
3
+ import { useState } from 'react';
3
4
  import {
4
5
  ContextMenu,
5
6
  ContextMenuContent,
@@ -14,6 +15,60 @@ import type {
14
15
  TreeRowRenderProps,
15
16
  } from '../../types';
16
17
 
18
+ interface RowContextMenuProps<T> {
19
+ rowProps: TreeRowRenderProps<T>;
20
+ items: TreeContextMenuItem<T>[];
21
+ trigger: React.ReactNode;
22
+ }
23
+
24
+ /**
25
+ * Per-row context menu.
26
+ *
27
+ * PERF: every visible row wraps its trigger in a `<ContextMenu>`, so a
28
+ * large tree mounts hundreds of these. Building the full
29
+ * `<ContextMenuContent>` item subtree for each of them on every render is
30
+ * what made right-click feel laggy (the selection change on right-click
31
+ * re-renders all rows, each rebuilding its menu body). We now render the
32
+ * content body ONLY while this row's menu is actually open — closed rows
33
+ * cost just the lightweight `ContextMenu` + `Trigger` shell.
34
+ */
35
+ function RowContextMenu<T>({
36
+ rowProps,
37
+ items,
38
+ trigger,
39
+ }: RowContextMenuProps<T>) {
40
+ const [open, setOpen] = useState(false);
41
+ return (
42
+ <ContextMenu onOpenChange={setOpen}>
43
+ <ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
44
+ {open ? (
45
+ <ContextMenuContent>
46
+ {items.map((item, idx) => {
47
+ if (item === 'separator') {
48
+ return <ContextMenuSeparator key={`sep-${idx}`} />;
49
+ }
50
+ const Icon = item.icon;
51
+ return (
52
+ <ContextMenuItem
53
+ key={item.id}
54
+ disabled={item.disabled}
55
+ variant={item.destructive ? 'destructive' : undefined}
56
+ onSelect={() => item.onSelect(rowProps)}
57
+ >
58
+ {Icon ? <Icon /> : null}
59
+ {item.label}
60
+ {item.shortcut ? (
61
+ <ContextMenuShortcut>{item.shortcut}</ContextMenuShortcut>
62
+ ) : null}
63
+ </ContextMenuItem>
64
+ );
65
+ })}
66
+ </ContextMenuContent>
67
+ ) : null}
68
+ </ContextMenu>
69
+ );
70
+ }
71
+
17
72
  /**
18
73
  * Render an array of declarative menu items as a themed `<ContextMenu>`
19
74
  * wrapped around the supplied trigger element. Pure presentational layer
@@ -24,33 +79,7 @@ export function renderItemsAsContextMenu<T>(
24
79
  items: TreeContextMenuItem<T>[],
25
80
  trigger: React.ReactNode,
26
81
  ): React.ReactNode {
27
- return (
28
- <ContextMenu>
29
- <ContextMenuTrigger asChild>{trigger}</ContextMenuTrigger>
30
- <ContextMenuContent>
31
- {items.map((item, idx) => {
32
- if (item === 'separator') {
33
- return <ContextMenuSeparator key={`sep-${idx}`} />;
34
- }
35
- const Icon = item.icon;
36
- return (
37
- <ContextMenuItem
38
- key={item.id}
39
- disabled={item.disabled}
40
- variant={item.destructive ? 'destructive' : undefined}
41
- onSelect={() => item.onSelect(rowProps)}
42
- >
43
- {Icon ? <Icon /> : null}
44
- {item.label}
45
- {item.shortcut ? (
46
- <ContextMenuShortcut>{item.shortcut}</ContextMenuShortcut>
47
- ) : null}
48
- </ContextMenuItem>
49
- );
50
- })}
51
- </ContextMenuContent>
52
- </ContextMenu>
53
- );
82
+ return <RowContextMenu rowProps={rowProps} items={items} trigger={trigger} />;
54
83
  }
55
84
 
56
85
  /**
@@ -6,6 +6,20 @@ export type TreeDensity = 'compact' | 'cozy' | 'comfortable';
6
6
  export type TreeAccentIntensity = 'subtle' | 'default' | 'strong';
7
7
  export type TreeRadius = 'none' | 'sm' | 'md';
8
8
 
9
+ /**
10
+ * High-level look. Sets sensible defaults for icons / row sizing / the
11
+ * active indicator; any individual `TreeAppearance` field still overrides.
12
+ *
13
+ * - `'explorer'` (default) — classic file/folder tree: leaf + folder
14
+ * icons, fixed single-line rows, VSCode active-bar.
15
+ * - `'list'` — macOS-sidebar / chat-history look: no icons (chevron
16
+ * only on groups), auto-height rows (multi-line label friendly), quiet
17
+ * selection (no active-bar).
18
+ */
19
+ export type TreeVariant = 'explorer' | 'list';
20
+
21
+ export type TreeRowSizing = 'fixed' | 'auto';
22
+
9
23
  /**
10
24
  * Cosmetic configuration. Every field is optional; missing values fall
11
25
  * back to the `cozy` preset (a comfortable VSCode-Explorer-like density).
@@ -38,6 +52,21 @@ export interface TreeAppearance {
38
52
  * Mimics the VSCode active-tab indicator. Default: `true`.
39
53
  */
40
54
  showActiveIndicator?: boolean;
55
+ /**
56
+ * High-level look. Default: `'explorer'`. Sets defaults for the three
57
+ * fields below; pass any of them explicitly to override the variant.
58
+ */
59
+ variant?: TreeVariant;
60
+ /** Hide per-leaf icons (chevron + label only). Variant default. */
61
+ hideLeafIcons?: boolean;
62
+ /** Hide folder icons too (chevron stays). Variant default. */
63
+ hideFolderIcons?: boolean;
64
+ /**
65
+ * `'fixed'` — every row is exactly `rowHeight` (single-line explorer).
66
+ * `'auto'` — `rowHeight` is a *minimum*; rows grow to fit a multi-line
67
+ * label (title + meta). Variant default.
68
+ */
69
+ rowSizing?: TreeRowSizing;
41
70
  }
42
71
 
43
72
  export interface ResolvedAppearance {
@@ -52,6 +81,9 @@ export interface ResolvedAppearance {
52
81
  radius: TreeRadius;
53
82
  indentGuideOpacity: number;
54
83
  showActiveIndicator: boolean;
84
+ hideLeafIcons: boolean;
85
+ hideFolderIcons: boolean;
86
+ rowSizing: TreeRowSizing;
55
87
  }
56
88
 
57
89
  const DENSITY_PRESETS: Record<
@@ -63,6 +95,31 @@ const DENSITY_PRESETS: Record<
63
95
  comfortable: { rowHeight: 32, iconSize: 16, fontSize: 14, gap: 8 },
64
96
  };
65
97
 
98
+ /**
99
+ * Per-variant defaults for the icon / sizing / indicator fields. The
100
+ * `'list'` variant is the macOS-sidebar / chat-history look.
101
+ */
102
+ const VARIANT_DEFAULTS: Record<
103
+ TreeVariant,
104
+ Pick<
105
+ ResolvedAppearance,
106
+ 'hideLeafIcons' | 'hideFolderIcons' | 'rowSizing' | 'showActiveIndicator'
107
+ >
108
+ > = {
109
+ explorer: {
110
+ hideLeafIcons: false,
111
+ hideFolderIcons: false,
112
+ rowSizing: 'fixed',
113
+ showActiveIndicator: true,
114
+ },
115
+ list: {
116
+ hideLeafIcons: true,
117
+ hideFolderIcons: true,
118
+ rowSizing: 'auto',
119
+ showActiveIndicator: false,
120
+ },
121
+ };
122
+
66
123
  export const DEFAULT_TREE_APPEARANCE: ResolvedAppearance = {
67
124
  density: 'cozy',
68
125
  ...DENSITY_PRESETS.cozy,
@@ -71,7 +128,7 @@ export const DEFAULT_TREE_APPEARANCE: ResolvedAppearance = {
71
128
  accent: 'default',
72
129
  radius: 'sm',
73
130
  indentGuideOpacity: 0.4,
74
- showActiveIndicator: true,
131
+ ...VARIANT_DEFAULTS.explorer,
75
132
  };
76
133
 
77
134
  /**
@@ -88,6 +145,9 @@ export function resolveAppearance(
88
145
 
89
146
  const density: TreeDensity = input?.density ?? 'cozy';
90
147
  const preset = DENSITY_PRESETS[density];
148
+ // Variant sets the icon/sizing/indicator defaults; explicit fields win.
149
+ const variant: TreeVariant = input?.variant ?? 'explorer';
150
+ const v = VARIANT_DEFAULTS[variant];
91
151
 
92
152
  return {
93
153
  density,
@@ -101,8 +161,10 @@ export function resolveAppearance(
101
161
  radius: input?.radius ?? DEFAULT_TREE_APPEARANCE.radius,
102
162
  indentGuideOpacity:
103
163
  input?.indentGuideOpacity ?? DEFAULT_TREE_APPEARANCE.indentGuideOpacity,
104
- showActiveIndicator:
105
- input?.showActiveIndicator ?? DEFAULT_TREE_APPEARANCE.showActiveIndicator,
164
+ showActiveIndicator: input?.showActiveIndicator ?? v.showActiveIndicator,
165
+ hideLeafIcons: input?.hideLeafIcons ?? v.hideLeafIcons,
166
+ hideFolderIcons: input?.hideFolderIcons ?? v.hideFolderIcons,
167
+ rowSizing: input?.rowSizing ?? v.rowSizing,
106
168
  };
107
169
  }
108
170
 
@@ -40,4 +40,6 @@ export type {
40
40
  TreeDensity,
41
41
  TreeAccentIntensity,
42
42
  TreeRadius,
43
+ TreeVariant,
44
+ TreeRowSizing,
43
45
  } from './appearance';
@@ -101,6 +101,8 @@ export type {
101
101
  TreeDensity,
102
102
  TreeAccentIntensity,
103
103
  TreeRadius,
104
+ TreeVariant,
105
+ TreeRowSizing,
104
106
  } from './data';
105
107
 
106
108
  export { DEFAULT_TREE_LABELS } from './types';