@keenmate/svelte-treeview 4.8.0 → 5.0.0-rc02

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.
Files changed (51) hide show
  1. package/README.md +106 -117
  2. package/ai/INDEX.txt +310 -0
  3. package/ai/advanced-patterns.txt +506 -0
  4. package/ai/basic-setup.txt +336 -0
  5. package/ai/context-menu.txt +349 -0
  6. package/ai/data-handling.txt +390 -0
  7. package/ai/drag-drop.txt +397 -0
  8. package/ai/events-callbacks.txt +382 -0
  9. package/ai/import-patterns.txt +271 -0
  10. package/ai/performance.txt +349 -0
  11. package/ai/search-features.txt +359 -0
  12. package/ai/styling-theming.txt +354 -0
  13. package/ai/tree-editing.txt +423 -0
  14. package/ai/typescript-types.txt +357 -0
  15. package/dist/components/Node.svelte +47 -40
  16. package/dist/components/Node.svelte.d.ts +1 -1
  17. package/dist/components/Tree.svelte +384 -1479
  18. package/dist/components/Tree.svelte.d.ts +30 -28
  19. package/dist/components/TreeProvider.svelte +28 -0
  20. package/dist/components/TreeProvider.svelte.d.ts +28 -0
  21. package/dist/constants.generated.d.ts +1 -1
  22. package/dist/constants.generated.js +1 -1
  23. package/dist/core/TreeController.svelte.d.ts +353 -0
  24. package/dist/core/TreeController.svelte.js +1503 -0
  25. package/dist/core/createTreeController.d.ts +9 -0
  26. package/dist/core/createTreeController.js +11 -0
  27. package/dist/global-api.d.ts +1 -1
  28. package/dist/global-api.js +5 -5
  29. package/dist/index.d.ts +10 -6
  30. package/dist/index.js +7 -3
  31. package/dist/logger.d.ts +7 -6
  32. package/dist/logger.js +0 -2
  33. package/dist/ltree/indexer.js +2 -4
  34. package/dist/ltree/ltree-node.svelte.d.ts +2 -1
  35. package/dist/ltree/ltree-node.svelte.js +1 -0
  36. package/dist/ltree/ltree.svelte.d.ts +1 -1
  37. package/dist/ltree/ltree.svelte.js +168 -175
  38. package/dist/ltree/types.d.ts +12 -8
  39. package/dist/perf-logger.d.ts +2 -1
  40. package/dist/perf-logger.js +0 -2
  41. package/dist/styles/main.scss +78 -78
  42. package/dist/styles.css +41 -41
  43. package/dist/styles.css.map +1 -1
  44. package/dist/vendor/loglevel/index.d.ts +55 -2
  45. package/dist/vendor/loglevel/prefix.d.ts +23 -2
  46. package/package.json +96 -95
  47. package/dist/ltree/ltree-demo.d.ts +0 -2
  48. package/dist/ltree/ltree-demo.js +0 -90
  49. package/dist/vendor/loglevel/loglevel-esm.d.ts +0 -2
  50. package/dist/vendor/loglevel/loglevel-plugin-prefix-esm.d.ts +0 -7
  51. package/dist/vendor/loglevel/loglevel-plugin-prefix.d.ts +0 -2
@@ -0,0 +1,336 @@
1
+ BASIC SETUP
2
+ ===========
3
+
4
+ CRITICAL: This is a Svelte 5 component using runes
5
+ - Requires Svelte 5.0.0 or higher
6
+ - Uses $state, $derived, $effect runes
7
+ - NOT compatible with Svelte 4 or earlier
8
+ - Works with SvelteKit, Vite, or any Svelte 5 setup
9
+
10
+ INSTALLATION
11
+ ------------
12
+ npm install @keenmate/svelte-treeview
13
+
14
+ # Or yarn
15
+ yarn add @keenmate/svelte-treeview
16
+
17
+ # Or pnpm
18
+ pnpm add @keenmate/svelte-treeview
19
+
20
+ IMPORT STYLES
21
+ -------------
22
+ The component requires CSS to display correctly:
23
+
24
+ // In your main.js/main.ts or +layout.svelte
25
+ import '@keenmate/svelte-treeview/styles.css';
26
+
27
+ // Or SCSS (if you have sass configured)
28
+ import '@keenmate/svelte-treeview/styles.scss';
29
+
30
+ MINIMAL EXAMPLE
31
+ ---------------
32
+ <script lang="ts">
33
+ import { Tree } from '@keenmate/svelte-treeview';
34
+
35
+ const data = [
36
+ { id: '1', path: '1', name: 'Documents' },
37
+ { id: '1.1', path: '1.1', name: 'Projects' },
38
+ { id: '1.1.1', path: '1.1.1', name: 'Project A' },
39
+ { id: '2', path: '2', name: 'Pictures' }
40
+ ];
41
+ </script>
42
+
43
+ <Tree
44
+ {data}
45
+ idMember="id"
46
+ pathMember="path"
47
+ displayValueMember="name"
48
+ />
49
+
50
+ REQUIRED PROPS
51
+ --------------
52
+ These props are required for the tree to work:
53
+
54
+ | Prop | Type | Description |
55
+ |------|------|-------------|
56
+ | data | T[] | Array of data objects |
57
+ | idMember | string | Property name for unique ID |
58
+ | pathMember | string | Property name for hierarchical path |
59
+
60
+ RECOMMENDED PROPS
61
+ -----------------
62
+ | Prop | Type | Default | Description |
63
+ |------|------|---------|-------------|
64
+ | displayValueMember | string | null | Property for display text |
65
+ | sortCallback | function | built-in | Custom sort function |
66
+ | expandLevel | number | 2 | Auto-expand nodes to this depth |
67
+
68
+ PATH STRUCTURE
69
+ --------------
70
+ CRITICAL: Data must use path-based hierarchy
71
+
72
+ The path defines the tree structure:
73
+ - Root level: "1", "2", "3"
74
+ - Second level: "1.1", "1.2", "2.1"
75
+ - Third level: "1.1.1", "1.2.1"
76
+
77
+ Parent path is derived from child path:
78
+ - "1.2.3" → parent is "1.2"
79
+ - "1.2" → parent is "1"
80
+ - "1" → no parent (root)
81
+
82
+ Custom separators:
83
+ <Tree treePathSeparator="/" ... />
84
+ // Paths: "1/2/3", "files/docs/readme"
85
+
86
+ BINDING SELECTED NODE
87
+ ---------------------
88
+ <script>
89
+ let selectedNode = $state(null);
90
+ </script>
91
+
92
+ <Tree
93
+ {data}
94
+ idMember="id"
95
+ pathMember="path"
96
+ bind:selectedNode
97
+ />
98
+
99
+ {#if selectedNode}
100
+ <p>Selected: {selectedNode.data.name}</p>
101
+ {/if}
102
+
103
+ BINDING SEARCH TEXT
104
+ -------------------
105
+ <script>
106
+ let searchText = $state('');
107
+ </script>
108
+
109
+ <input bind:value={searchText} placeholder="Search..." />
110
+
111
+ <Tree
112
+ {data}
113
+ idMember="id"
114
+ pathMember="path"
115
+ bind:searchText
116
+ shouldUseInternalSearchIndex={true}
117
+ searchValueMember="name"
118
+ />
119
+
120
+ CUSTOM NODE TEMPLATE
121
+ --------------------
122
+ <Tree
123
+ {data}
124
+ idMember="id"
125
+ pathMember="path"
126
+ >
127
+ {#snippet nodeTemplate(node)}
128
+ <div class="custom-node">
129
+ <span class="icon">{node.data.icon}</span>
130
+ <span class="name">{node.data.name}</span>
131
+ {#if node.data.count}
132
+ <span class="badge">{node.data.count}</span>
133
+ {/if}
134
+ </div>
135
+ {/snippet}
136
+ </Tree>
137
+
138
+ IMPORTANT: The snippet name is nodeTemplate (not nodeContent)
139
+
140
+ EXPAND/COLLAPSE CONTROL
141
+ -----------------------
142
+ Control initial expansion:
143
+
144
+ <Tree
145
+ {data}
146
+ expandLevel={3} <!-- Expand first 3 levels -->
147
+ />
148
+
149
+ Programmatic control:
150
+
151
+ <script>
152
+ let treeRef;
153
+ </script>
154
+
155
+ <Tree bind:this={treeRef} {data} ... />
156
+
157
+ <button onclick={() => treeRef.expandAll()}>Expand All</button>
158
+ <button onclick={() => treeRef.collapseAll()}>Collapse All</button>
159
+ <button onclick={() => treeRef.expandNodes('1.2')}>Expand 1.2</button>
160
+
161
+ CLICK HANDLING
162
+ --------------
163
+ <Tree
164
+ {data}
165
+ idMember="id"
166
+ pathMember="path"
167
+ onNodeClicked={(node) => {
168
+ console.log('Clicked:', node.data.name);
169
+ console.log('Path:', node.path);
170
+ }}
171
+ />
172
+
173
+ Toggle on click (default true):
174
+ <Tree shouldToggleOnNodeClick={false} ... />
175
+
176
+ SCROLL TO NODE
177
+ --------------
178
+ <script>
179
+ let treeRef;
180
+
181
+ async function goToNode() {
182
+ await treeRef.scrollToPath('1.2.3', {
183
+ expand: true, // Expand parents first
184
+ highlight: true, // Highlight the node
185
+ containerScroll: true // Scroll only within container
186
+ });
187
+ }
188
+ </script>
189
+
190
+ <Tree bind:this={treeRef} {data} ... />
191
+ <button onclick={goToNode}>Go to 1.2.3</button>
192
+
193
+ TREE STATISTICS
194
+ ---------------
195
+ <script>
196
+ let treeRef;
197
+
198
+ function showStats() {
199
+ const stats = treeRef.statistics;
200
+ console.log('Total nodes:', stats.nodeCount);
201
+ console.log('Max depth:', stats.maxLevel);
202
+ console.log('Filtered:', stats.filteredNodeCount);
203
+ console.log('Indexing:', stats.isIndexing);
204
+ }
205
+ </script>
206
+
207
+ DEBUG MODE
208
+ ----------
209
+ Enable debug panel and console logging:
210
+
211
+ <Tree
212
+ {data}
213
+ shouldDisplayDebugInformation={true}
214
+ />
215
+
216
+ Shows:
217
+ - Tree ID
218
+ - Node count
219
+ - Max depth
220
+ - Filtered count
221
+ - Indexing status
222
+ - Currently dragged node
223
+
224
+ EMPTY STATE
225
+ -----------
226
+ Custom message when tree is empty:
227
+
228
+ <Tree {data} ...>
229
+ {#snippet noDataFound()}
230
+ <div class="empty-state">
231
+ <p>No items found</p>
232
+ <button>Add First Item</button>
233
+ </div>
234
+ {/snippet}
235
+ </Tree>
236
+
237
+ LOADING STATE
238
+ -------------
239
+ Show a loading placeholder while data is being fetched:
240
+
241
+ <Tree {data} isLoading={isLoading}>
242
+ {#snippet loadingPlaceholder()}
243
+ <div class="loading">Loading tree data...</div>
244
+ {/snippet}
245
+ </Tree>
246
+
247
+ DROP PLACEHOLDER
248
+ ----------------
249
+ Custom placeholder for empty trees that accept drops:
250
+
251
+ <Tree {data} dragDropMode="both">
252
+ {#snippet dropPlaceholder()}
253
+ <div class="drop-here">Drop items here to start</div>
254
+ {/snippet}
255
+ </Tree>
256
+
257
+ FRAMEWORK INTEGRATION
258
+ ---------------------
259
+ SvelteKit:
260
+ // +page.svelte
261
+ <script>
262
+ import { Tree } from '@keenmate/svelte-treeview';
263
+ import '@keenmate/svelte-treeview/styles.css';
264
+ </script>
265
+
266
+ Vite + Svelte:
267
+ // App.svelte
268
+ <script>
269
+ import { Tree } from '@keenmate/svelte-treeview';
270
+ </script>
271
+
272
+ // main.js
273
+ import '@keenmate/svelte-treeview/styles.css';
274
+
275
+ COMMON SETUP PATTERNS
276
+ ---------------------
277
+ File explorer:
278
+ <Tree
279
+ {data}
280
+ idMember="id"
281
+ pathMember="path"
282
+ displayValueMember="name"
283
+ expandLevel={1}
284
+ treePathSeparator="/"
285
+ />
286
+
287
+ Organization tree:
288
+ <Tree
289
+ {data}
290
+ idMember="employeeId"
291
+ pathMember="orgPath"
292
+ displayValueMember="fullName"
293
+ expandLevel={3}
294
+ />
295
+
296
+ With search and drag-drop:
297
+ <Tree
298
+ {data}
299
+ idMember="id"
300
+ pathMember="path"
301
+ bind:searchText
302
+ shouldUseInternalSearchIndex={true}
303
+ searchValueMember="name"
304
+ dragOverNodeClass="ltree-dragover-glow"
305
+ onNodeDrop={handleDrop}
306
+ />
307
+
308
+ BROWSER SUPPORT
309
+ ---------------
310
+ Requires modern browsers:
311
+ - Chrome/Edge 88+
312
+ - Firefox 78+
313
+ - Safari 14+
314
+
315
+ No IE11 support (Svelte 5 requirement)
316
+
317
+ CRITICAL REMINDERS
318
+ ------------------
319
+ ✅ Import styles (CSS or SCSS)
320
+ ✅ Use Svelte 5.0.0 or higher
321
+ ✅ Use path-based hierarchical data
322
+ ✅ Set idMember and pathMember
323
+ ✅ Use $state.raw() for large datasets (1000+ items)
324
+
325
+ ❌ Don't use with Svelte 4 or earlier
326
+ ❌ Don't forget to import styles
327
+ ❌ Don't use non-path-based data structures
328
+ ❌ Don't use $state() for large datasets (causes slowdown)
329
+
330
+ NEXT STEPS
331
+ ----------
332
+ - Custom data structure → data-handling.txt
333
+ - Add drag and drop → drag-drop.txt
334
+ - Add search → search-features.txt
335
+ - Add context menu → context-menu.txt
336
+ - Optimize performance → performance.txt
@@ -0,0 +1,349 @@
1
+ CONTEXT MENU
2
+ ============
3
+
4
+ CRITICAL: Two approaches for context menus
5
+ - Callback-based (recommended): contextMenuCallback prop
6
+ - Snippet-based: contextMenu snippet
7
+ - Both support dynamic items, icons, disabled states
8
+
9
+ CALLBACK-BASED CONTEXT MENU
10
+ ---------------------------
11
+ <script>
12
+ import type { ContextMenuItem, LTreeNode } from '@keenmate/svelte-treeview';
13
+
14
+ function createContextMenu(node: LTreeNode<MyItem>, closeMenuCallback: () => void): ContextMenuItem[] {
15
+ const items: ContextMenuItem[] = [];
16
+
17
+ // Basic item
18
+ items.push({
19
+ icon: '📂',
20
+ title: 'Open',
21
+ callback: () => alert(`Opening ${node.data.name}`)
22
+ });
23
+
24
+ // Conditional item
25
+ if (node.data.canEdit) {
26
+ items.push({
27
+ icon: '✏️',
28
+ title: 'Edit',
29
+ callback: () => editItem(node.data)
30
+ });
31
+ }
32
+
33
+ // Divider
34
+ items.push({ isDivider: true });
35
+
36
+ // Disabled item
37
+ items.push({
38
+ icon: '🗑️',
39
+ title: 'Delete',
40
+ isDisabled: !node.data.canDelete,
41
+ callback: () => deleteItem(node.data)
42
+ });
43
+
44
+ return items;
45
+ }
46
+ </script>
47
+
48
+ <Tree
49
+ {data}
50
+ contextMenuCallback={createContextMenu}
51
+ />
52
+
53
+ CONTEXT MENU ITEM INTERFACE
54
+ ---------------------------
55
+ interface ContextMenuItem {
56
+ icon?: string; // Optional icon (emoji or text)
57
+ title: string; // Menu item text
58
+ isDisabled?: boolean; // Disable the item
59
+ callback: () => void | Promise<void>; // Action
60
+ isDivider?: boolean; // Render as divider
61
+ className?: string; // Custom CSS class
62
+ }
63
+
64
+ SNIPPET-BASED CONTEXT MENU
65
+ --------------------------
66
+ <Tree {data}>
67
+ {#snippet contextMenu(node, closeMenu)}
68
+ <div class="ltree-context-menu-item" onclick={() => {
69
+ openItem(node.data);
70
+ closeMenu();
71
+ }}>
72
+ 📂 Open
73
+ </div>
74
+
75
+ <div class="ltree-context-menu-divider"></div>
76
+
77
+ <div
78
+ class="ltree-context-menu-item"
79
+ class:ltree-context-menu-item-disabled={!node.data.canDelete}
80
+ onclick={() => {
81
+ if (node.data.canDelete) {
82
+ deleteItem(node.data);
83
+ closeMenu();
84
+ }
85
+ }}
86
+ >
87
+ 🗑️ Delete
88
+ </div>
89
+ {/snippet}
90
+ </Tree>
91
+
92
+ POSITION OFFSET
93
+ ---------------
94
+ Adjust menu position relative to cursor:
95
+
96
+ <Tree
97
+ contextMenuCallback={createMenu}
98
+ contextMenuXOffset={8} <!-- Horizontal offset (default: 8px) -->
99
+ contextMenuYOffset={0} <!-- Vertical offset (default: 0px) -->
100
+ />
101
+
102
+ CLOSE MENU CALLBACK
103
+ -------------------
104
+ Callback receives closeMenu function for programmatic control:
105
+
106
+ function createContextMenu(node, closeMenuCallback) {
107
+ return [
108
+ {
109
+ title: 'Action',
110
+ callback: async () => {
111
+ const success = await doSomething();
112
+ if (success) {
113
+ closeMenuCallback(); // Close only on success
114
+ }
115
+ }
116
+ }
117
+ ];
118
+ }
119
+
120
+ <Tree contextMenuCallback={createContextMenu} />
121
+
122
+ ASYNC CALLBACKS
123
+ ---------------
124
+ Menu item callbacks can be async:
125
+
126
+ {
127
+ title: 'Save to Server',
128
+ callback: async () => {
129
+ try {
130
+ await saveToServer(node.data);
131
+ showSuccess('Saved!');
132
+ } catch (error) {
133
+ showError('Failed to save');
134
+ // Menu stays open on error
135
+ }
136
+ }
137
+ }
138
+
139
+ CLOSE CONTEXT MENU PROGRAMMATICALLY
140
+ -----------------------------------
141
+ <script>
142
+ let treeRef;
143
+
144
+ function closeMenuExternally() {
145
+ treeRef.closeContextMenu();
146
+ }
147
+ </script>
148
+
149
+ <Tree bind:this={treeRef} ... />
150
+
151
+ AUTO-CLOSE BEHAVIOR
152
+ -------------------
153
+ Context menu automatically closes on:
154
+ - Click outside the menu
155
+ - Scroll events (mouse wheel, scrollbar, touch)
156
+ - Another right-click
157
+ - Pressing Escape
158
+ - Window resize
159
+
160
+ CUSTOM ITEM STYLING
161
+ -------------------
162
+ Add custom classes to items:
163
+
164
+ {
165
+ title: 'Delete',
166
+ className: 'text-danger', // Your CSS class
167
+ callback: () => deleteItem()
168
+ }
169
+
170
+ Built-in CSS classes:
171
+ - .ltree-context-menu - Menu container
172
+ - .ltree-context-menu-item - Menu item
173
+ - .ltree-context-menu-item-disabled - Disabled item
174
+ - .ltree-context-menu-icon - Icon container
175
+ - .ltree-context-menu-divider - Divider line
176
+
177
+ CONDITIONAL MENUS
178
+ -----------------
179
+ Show different menus based on node type:
180
+
181
+ function createContextMenu(node, closeMenuCallback) {
182
+ if (node.data?.type === 'folder') {
183
+ return [
184
+ { title: 'New File', callback: () => newFile(node) },
185
+ { title: 'New Folder', callback: () => newFolder(node) },
186
+ { isDivider: true },
187
+ { title: 'Rename', callback: () => rename(node) },
188
+ { title: 'Delete', callback: () => deleteFolder(node) }
189
+ ];
190
+ }
191
+
192
+ if (node.data?.type === 'file') {
193
+ return [
194
+ { title: 'Open', callback: () => openFile(node) },
195
+ { title: 'Download', callback: () => downloadFile(node) },
196
+ { isDivider: true },
197
+ { title: 'Rename', callback: () => rename(node) },
198
+ { title: 'Delete', callback: () => deleteFile(node) }
199
+ ];
200
+ }
201
+
202
+ return []; // No menu
203
+ }
204
+
205
+ ICONS
206
+ -----
207
+ Icons can be:
208
+ - Emoji: '📁', '✏️', '🗑️'
209
+ - Text: 'Edit', '[X]'
210
+ - Unicode: '\u{1F4C1}'
211
+
212
+ Example with emojis:
213
+ {
214
+ icon: '📂',
215
+ title: 'Open Folder',
216
+ callback: () => openFolder()
217
+ }
218
+
219
+ DIVIDERS
220
+ --------
221
+ Add visual separation:
222
+
223
+ [
224
+ { title: 'Cut', callback: () => cut() },
225
+ { title: 'Copy', callback: () => copy() },
226
+ { title: 'Paste', callback: () => paste() },
227
+ { isDivider: true }, // <-- Divider here
228
+ { title: 'Delete', callback: () => del() }
229
+ ]
230
+
231
+ DISABLED ITEMS
232
+ --------------
233
+ Disable based on conditions:
234
+
235
+ {
236
+ title: 'Paste',
237
+ isDisabled: clipboard.isEmpty(),
238
+ callback: () => paste()
239
+ }
240
+
241
+ {
242
+ title: 'Delete',
243
+ isDisabled: node.data.isReadOnly,
244
+ callback: () => deleteItem()
245
+ }
246
+
247
+ DEBUG MODE
248
+ ----------
249
+ For CSS development, show menu persistently:
250
+
251
+ <Tree
252
+ contextMenuCallback={createMenu}
253
+ shouldDisplayContextMenuInDebugMode={true}
254
+ shouldDisplayDebugInformation={true}
255
+ />
256
+
257
+ Debug mode:
258
+ - Shows menu at fixed position (200px right, 100px down from tree)
259
+ - Uses second node (or first if only one)
260
+ - Menu stays visible for styling
261
+
262
+ COMMON PATTERNS
263
+ ---------------
264
+ File explorer menu:
265
+ function createMenu(node, closeMenuCallback) {
266
+ const items = [];
267
+
268
+ if (node.data?.type === 'folder') {
269
+ items.push(
270
+ { icon: '📄', title: 'New File', callback: () => newFile(node) },
271
+ { icon: '📁', title: 'New Folder', callback: () => newFolder(node) }
272
+ );
273
+ }
274
+
275
+ items.push(
276
+ { isDivider: true },
277
+ { icon: '✏️', title: 'Rename', callback: () => rename(node) },
278
+ { icon: '📋', title: 'Copy Path', callback: () => copyPath(node) },
279
+ { isDivider: true },
280
+ {
281
+ icon: '🗑️',
282
+ title: 'Delete',
283
+ isDisabled: node.data.isSystem,
284
+ callback: () => deleteItem(node)
285
+ }
286
+ );
287
+
288
+ return items;
289
+ }
290
+
291
+ User management menu:
292
+ function createMenu(node, closeMenuCallback) {
293
+ const user = node.data;
294
+ return [
295
+ { icon: '👤', title: 'View Profile', callback: () => viewProfile(user) },
296
+ { icon: '✉️', title: 'Send Email', callback: () => sendEmail(user) },
297
+ { isDivider: true },
298
+ {
299
+ icon: '🔒',
300
+ title: user.isActive ? 'Deactivate' : 'Activate',
301
+ callback: () => toggleActive(user)
302
+ },
303
+ {
304
+ icon: '🗑️',
305
+ title: 'Remove',
306
+ isDisabled: user.isAdmin,
307
+ callback: () => removeUser(user)
308
+ }
309
+ ];
310
+ }
311
+
312
+ STYLING CONTEXT MENU
313
+ --------------------
314
+ Override default styles:
315
+
316
+ .ltree-context-menu {
317
+ background: #1a1a1a;
318
+ border: 1px solid #333;
319
+ border-radius: 8px;
320
+ padding: 4px;
321
+ }
322
+
323
+ .ltree-context-menu-item {
324
+ padding: 8px 12px;
325
+ border-radius: 4px;
326
+ }
327
+
328
+ .ltree-context-menu-item:hover {
329
+ background: #2a2a2a;
330
+ }
331
+
332
+ .ltree-context-menu-item-disabled {
333
+ opacity: 0.5;
334
+ cursor: not-allowed;
335
+ }
336
+
337
+ BEST PRACTICES
338
+ --------------
339
+ ✅ Use callback-based approach for complex menus
340
+ ✅ Show icons for quick recognition
341
+ ✅ Use dividers to group related actions
342
+ ✅ Disable dangerous actions when not safe
343
+ ✅ Close menu after successful action
344
+ ✅ Handle async operations gracefully
345
+
346
+ ❌ Don't create menus with too many items (>10)
347
+ ❌ Don't forget to close menu after action
348
+ ❌ Don't use blocking operations in callbacks
349
+ ❌ Don't make all items look the same (use icons)