x-ipe 1.0.23__py3-none-any.whl → 1.0.25__py3-none-any.whl

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 (146) hide show
  1. x_ipe/app.py +32 -1
  2. x_ipe/handlers/terminal_handlers.py +6 -0
  3. x_ipe/handlers/voice_handlers.py +5 -0
  4. x_ipe/resources/copilot-instructions.md +19 -6
  5. x_ipe/resources/skills/lesson-learned/SKILL.md +208 -0
  6. x_ipe/resources/skills/lesson-learned/references/examples.md +238 -0
  7. x_ipe/resources/skills/project-quality-board-management/SKILL.md +135 -298
  8. x_ipe/resources/skills/project-quality-board-management/references/evaluation-principles.md +213 -0
  9. x_ipe/resources/skills/project-quality-board-management/references/evaluation-procedures.md +214 -0
  10. x_ipe/resources/skills/project-quality-board-management/templates/quality-report.md +70 -18
  11. x_ipe/resources/skills/task-execution-guideline/SKILL.md +2 -2
  12. x_ipe/resources/skills/task-execution-guideline/templates/task-record.yaml +1 -1
  13. x_ipe/resources/skills/task-type-code-implementation/SKILL.md +72 -270
  14. x_ipe/resources/skills/task-type-code-implementation/references/implementation-guidelines.md +432 -0
  15. x_ipe/resources/skills/task-type-code-refactor-v2/SKILL.md +127 -353
  16. x_ipe/resources/skills/task-type-code-refactor-v2/references/refactoring-techniques.md +373 -0
  17. x_ipe/resources/skills/task-type-feature-breakdown/SKILL.md +31 -243
  18. x_ipe/resources/skills/task-type-feature-breakdown/references/breakdown-guidelines.md +330 -0
  19. x_ipe/resources/skills/task-type-feature-refinement/SKILL.md +27 -180
  20. x_ipe/resources/skills/task-type-feature-refinement/references/specification-writing-guide.md +267 -0
  21. x_ipe/resources/skills/task-type-idea-mockup/SKILL.md +38 -276
  22. x_ipe/resources/skills/task-type-idea-mockup/references/mockup-guidelines.md +299 -0
  23. x_ipe/resources/skills/task-type-idea-to-architecture/SKILL.md +20 -218
  24. x_ipe/resources/skills/task-type-idea-to-architecture/references/architecture-patterns.md +342 -0
  25. x_ipe/resources/skills/task-type-ideation/SKILL.md +10 -266
  26. x_ipe/resources/skills/task-type-ideation/references/folder-naming-guide.md +55 -0
  27. x_ipe/resources/skills/task-type-ideation/references/tool-usage-guide.md +236 -0
  28. x_ipe/resources/skills/task-type-ideation-v2/SKILL.md +488 -0
  29. x_ipe/resources/skills/task-type-ideation-v2/references/examples.md +377 -0
  30. x_ipe/resources/skills/task-type-ideation-v2/references/folder-naming-guide.md +74 -0
  31. x_ipe/resources/skills/task-type-ideation-v2/references/tool-usage-guide.md +145 -0
  32. x_ipe/resources/skills/task-type-ideation-v2/references/visualization-guide.md +160 -0
  33. x_ipe/resources/skills/task-type-ideation-v2/templates/idea-summary.md +86 -0
  34. x_ipe/resources/skills/task-type-refactoring-analysis/SKILL.md +83 -145
  35. x_ipe/resources/skills/task-type-refactoring-analysis/references/output-schema.md +172 -0
  36. x_ipe/resources/skills/task-type-technical-design/SKILL.md +28 -214
  37. x_ipe/resources/skills/task-type-technical-design/references/design-templates.md +422 -0
  38. x_ipe/resources/skills/task-type-test-generation/SKILL.md +47 -332
  39. x_ipe/resources/skills/task-type-test-generation/references/test-patterns.md +368 -0
  40. x_ipe/resources/skills/tool-tracing-creator/SKILL.md +312 -0
  41. x_ipe/resources/skills/tool-tracing-creator/references/examples.md +324 -0
  42. x_ipe/resources/skills/tool-tracing-instrumentation/SKILL.md +373 -0
  43. x_ipe/resources/skills/tool-tracing-instrumentation/references/examples.md +264 -0
  44. x_ipe/resources/skills/x-ipe-skill-creator-v3/SKILL.md +486 -0
  45. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/10. example-gate-conditions.md +73 -0
  46. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/11. reference-quality-standards.md +127 -0
  47. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/2. reference-section-order.md +127 -0
  48. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/3. example-step-based-code-review.md +84 -0
  49. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/4. example-step-based-feature-implementation.md +113 -0
  50. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/5. example-function-based-validation.md +73 -0
  51. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/6. example-function-based-analysis.md +94 -0
  52. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/7. example-task-io-code-implementation.md +36 -0
  53. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/8. example-structured-summary.md +43 -0
  54. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/9. example-dor-dod.md +77 -0
  55. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/examples.md +429 -0
  56. x_ipe/resources/skills/x-ipe-skill-creator-v3/references/skill-general-guidelines-v2.md +611 -0
  57. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-meta.md +153 -0
  58. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-based.md +324 -0
  59. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-task-category.md +109 -0
  60. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/skill-meta-x-ipe-tool.md +205 -0
  61. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-meta.md +334 -0
  62. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-task-based.md +279 -0
  63. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-tool.md +175 -0
  64. x_ipe/resources/skills/x-ipe-skill-creator-v3/templates/x-ipe-workflow-orchestration.md +329 -0
  65. x_ipe/resources/skills/x-ipe-task-based-ideation/SKILL.md +487 -0
  66. x_ipe/resources/skills/x-ipe-task-based-ideation/references/examples.md +377 -0
  67. x_ipe/resources/skills/x-ipe-task-based-ideation/references/folder-naming-guide.md +74 -0
  68. x_ipe/resources/skills/x-ipe-task-based-ideation/references/tool-usage-guide.md +145 -0
  69. x_ipe/resources/skills/x-ipe-task-based-ideation/references/visualization-guide.md +160 -0
  70. x_ipe/resources/skills/x-ipe-task-based-ideation/templates/idea-summary.md +86 -0
  71. x_ipe/routes/__init__.py +2 -0
  72. x_ipe/routes/ideas_routes.py +289 -0
  73. x_ipe/routes/kb_routes.py +80 -0
  74. x_ipe/routes/main_routes.py +18 -0
  75. x_ipe/routes/project_routes.py +7 -0
  76. x_ipe/routes/proxy_routes.py +10 -2
  77. x_ipe/routes/quality_evaluation_routes.py +193 -0
  78. x_ipe/routes/settings_routes.py +6 -0
  79. x_ipe/routes/tools_routes.py +6 -0
  80. x_ipe/routes/tracing_routes.py +232 -0
  81. x_ipe/routes/uiux_feedback_routes.py +50 -0
  82. x_ipe/services/__init__.py +5 -0
  83. x_ipe/services/config_service.py +6 -0
  84. x_ipe/services/file_service.py +20 -0
  85. x_ipe/services/homepage_service.py +160 -0
  86. x_ipe/services/ideas_service.py +535 -2
  87. x_ipe/services/kb_service.py +378 -0
  88. x_ipe/services/proxy_service.py +37 -7
  89. x_ipe/services/settings_service.py +13 -0
  90. x_ipe/services/skills_service.py +4 -0
  91. x_ipe/services/terminal_service.py +24 -0
  92. x_ipe/services/themes_service.py +4 -0
  93. x_ipe/services/tools_config_service.py +4 -0
  94. x_ipe/services/tracing_service.py +333 -0
  95. x_ipe/services/uiux_feedback_service.py +148 -1
  96. x_ipe/services/voice_input_service_v2.py +11 -0
  97. x_ipe/static/css/base.css +7 -0
  98. x_ipe/static/css/homepage-infinity.css +330 -0
  99. x_ipe/static/css/kb-core.css +301 -0
  100. x_ipe/static/css/quality-evaluation.css +345 -0
  101. x_ipe/static/css/sidebar.css +14 -4
  102. x_ipe/static/css/terminal.css +23 -0
  103. x_ipe/static/css/tracing-dashboard.css +796 -0
  104. x_ipe/static/css/uiux-feedback.css +7 -1
  105. x_ipe/static/css/workplace.css +636 -0
  106. x_ipe/static/img/homepage-infinity-loop.png +0 -0
  107. x_ipe/static/js/features/confirm-dialog.js +169 -0
  108. x_ipe/static/js/features/folder-view.js +742 -0
  109. x_ipe/static/js/features/homepage-infinity.js +314 -0
  110. x_ipe/static/js/features/kb-core.js +371 -0
  111. x_ipe/static/js/features/quality-evaluation.js +387 -0
  112. x_ipe/static/js/features/sidebar.js +255 -12
  113. x_ipe/static/js/features/tracing-dashboard.js +855 -0
  114. x_ipe/static/js/features/tracing-graph.js +1031 -0
  115. x_ipe/static/js/features/tree-drag.js +227 -0
  116. x_ipe/static/js/features/tree-search.js +228 -0
  117. x_ipe/static/js/features/workplace.js +661 -33
  118. x_ipe/static/js/init.js +76 -0
  119. x_ipe/static/js/terminal-v2.js +45 -14
  120. x_ipe/static/js/terminal.js +50 -49
  121. x_ipe/static/js/uiux-feedback.js +75 -16
  122. x_ipe/templates/base.html +24 -0
  123. x_ipe/templates/index.html +10 -1
  124. x_ipe/templates/knowledge-base.html +110 -0
  125. x_ipe/templates/workplace.html +4 -0
  126. x_ipe/tracing/__init__.py +37 -0
  127. x_ipe/tracing/buffer.py +135 -0
  128. x_ipe/tracing/context.py +125 -0
  129. x_ipe/tracing/decorator.py +288 -0
  130. x_ipe/tracing/middleware.py +197 -0
  131. x_ipe/tracing/parser.py +235 -0
  132. x_ipe/tracing/redactor.py +111 -0
  133. x_ipe/tracing/writer.py +122 -0
  134. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/METADATA +2 -2
  135. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/RECORD +138 -65
  136. x_ipe/app.py.bak +0 -1333
  137. x_ipe/resources/skills/x-ipe-skill-creator/SKILL.md +0 -329
  138. x_ipe/resources/skills/x-ipe-skill-creator/references/output-patterns.md +0 -169
  139. x_ipe/resources/skills/x-ipe-skill-creator/references/skill-structure.md +0 -162
  140. x_ipe/resources/skills/x-ipe-skill-creator/references/workflows.md +0 -110
  141. x_ipe/resources/skills/x-ipe-skill-creator/templates/references/examples.md +0 -113
  142. x_ipe/resources/skills/x-ipe-skill-creator/templates/skill-category-skill.md +0 -296
  143. x_ipe/resources/skills/x-ipe-skill-creator/templates/task-type-skill.md +0 -269
  144. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/WHEEL +0 -0
  145. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/entry_points.txt +0 -0
  146. {x_ipe-1.0.23.dist-info → x_ipe-1.0.25.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,742 @@
1
+ /**
2
+ * FolderViewManager - Detailed folder view panel
3
+ * FEATURE-008 CR-006: Folder Tree UX Enhancement
4
+ *
5
+ * Provides:
6
+ * - Folder contents view replacing preview panel
7
+ * - Breadcrumb navigation
8
+ * - Action bar (Add File, Add Folder, Rename, Delete)
9
+ * - File/folder items with hover actions
10
+ * - Subfolder expansion in place
11
+ */
12
+ class FolderViewManager {
13
+ constructor(options) {
14
+ this.container = options.container;
15
+ this.onAction = options.onAction; // Callback: (action, path, data) => Promise<boolean>
16
+ this.onNavigate = options.onNavigate; // Callback: (path) => void (when clicking file)
17
+ this.onClose = options.onClose; // Callback: () => void (when closing folder view)
18
+ this.currentPath = null;
19
+ this.expandedFolders = new Set();
20
+ this.confirmDialog = null;
21
+ }
22
+
23
+ /**
24
+ * Initialize folder view
25
+ */
26
+ async init() {
27
+ // Lazy load ConfirmDialog
28
+ if (typeof ConfirmDialog !== 'undefined') {
29
+ this.confirmDialog = new ConfirmDialog();
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Render folder view for given path
35
+ * @param {string} folderPath - Folder path to display
36
+ */
37
+ async render(folderPath) {
38
+ this.currentPath = folderPath;
39
+
40
+ try {
41
+ const contents = await this._loadContents(folderPath);
42
+
43
+ this.container.innerHTML = `
44
+ <div class="folder-view">
45
+ <header class="folder-view-header">
46
+ <div class="folder-view-header-row">
47
+ ${this._renderPathBar(folderPath)}
48
+ <button class="folder-view-close" title="Close folder view">
49
+ <i class="bi bi-x-lg"></i>
50
+ </button>
51
+ </div>
52
+ ${this._renderActionBar()}
53
+ </header>
54
+ <div class="folder-view-content">
55
+ ${this._renderContents(contents)}
56
+ </div>
57
+ </div>
58
+ `;
59
+
60
+ this._bindEvents();
61
+ } catch (error) {
62
+ console.error('Failed to load folder contents:', error);
63
+ this.container.innerHTML = `
64
+ <div class="folder-view folder-view-error">
65
+ <i class="bi bi-exclamation-triangle"></i>
66
+ <p>Failed to load folder contents</p>
67
+ <button class="btn btn-sm btn-outline-primary" onclick="location.reload()">
68
+ Retry
69
+ </button>
70
+ </div>
71
+ `;
72
+ }
73
+ }
74
+
75
+ /**
76
+ * Load folder contents from API
77
+ * @param {string} folderPath
78
+ * @returns {Promise<Array>}
79
+ */
80
+ async _loadContents(folderPath) {
81
+ const response = await fetch(`/api/ideas/folder-contents?path=${encodeURIComponent(folderPath)}`);
82
+ const data = await response.json();
83
+
84
+ if (!data.success) {
85
+ throw new Error(data.error || 'Failed to load folder');
86
+ }
87
+
88
+ return data.items || [];
89
+ }
90
+
91
+ /**
92
+ * Render breadcrumb path bar
93
+ * @param {string} path
94
+ * @returns {string} HTML
95
+ */
96
+ _renderPathBar(path) {
97
+ const parts = path.split('/').filter(Boolean);
98
+
99
+ const breadcrumbs = parts.map((part, i) => {
100
+ const fullPath = parts.slice(0, i + 1).join('/');
101
+ const isLast = i === parts.length - 1;
102
+ return `<span class="breadcrumb-item ${isLast ? 'current' : 'clickable'}"
103
+ data-path="${fullPath}">${this._escapeHtml(part)}</span>`;
104
+ }).join('<span class="breadcrumb-sep"><i class="bi bi-chevron-right"></i></span>');
105
+
106
+ return `<nav class="folder-view-breadcrumb" aria-label="Folder path">
107
+ <span class="breadcrumb-item clickable" data-path="">
108
+ <i class="bi bi-house"></i> Ideas
109
+ </span>
110
+ ${breadcrumbs ? '<span class="breadcrumb-sep"><i class="bi bi-chevron-right"></i></span>' + breadcrumbs : ''}
111
+ </nav>`;
112
+ }
113
+
114
+ /**
115
+ * Render action bar with buttons
116
+ * @returns {string} HTML
117
+ */
118
+ _renderActionBar() {
119
+ return `<div class="folder-view-actions">
120
+ <button class="folder-view-action-btn" data-action="add-file" title="Add new file">
121
+ <i class="bi bi-file-earmark-plus"></i>
122
+ <span>Add File</span>
123
+ </button>
124
+ <button class="folder-view-action-btn" data-action="add-folder" title="Create new folder">
125
+ <i class="bi bi-folder-plus"></i>
126
+ <span>Add Folder</span>
127
+ </button>
128
+ <button class="folder-view-action-btn" data-action="rename" title="Rename folder">
129
+ <i class="bi bi-pencil"></i>
130
+ <span>Rename</span>
131
+ </button>
132
+ <button class="folder-view-action-btn danger" data-action="delete" title="Delete folder">
133
+ <i class="bi bi-trash"></i>
134
+ <span>Delete</span>
135
+ </button>
136
+ </div>`;
137
+ }
138
+
139
+ /**
140
+ * Render folder contents
141
+ * @param {Array} items
142
+ * @returns {string} HTML
143
+ */
144
+ _renderContents(items) {
145
+ if (!items || items.length === 0) {
146
+ return `<div class="folder-view-empty">
147
+ <i class="bi bi-folder2-open"></i>
148
+ <p>This folder is empty</p>
149
+ <p class="text-muted">Use the buttons above to add files or folders</p>
150
+ </div>`;
151
+ }
152
+
153
+ // Sort: folders first, then files, alphabetically
154
+ const sorted = [...items].sort((a, b) => {
155
+ if (a.type !== b.type) {
156
+ return a.type === 'folder' ? -1 : 1;
157
+ }
158
+ return a.name.localeCompare(b.name);
159
+ });
160
+
161
+ return `<div class="folder-view-list">
162
+ ${sorted.map(item => this._renderItem(item)).join('')}
163
+ </div>`;
164
+ }
165
+
166
+ /**
167
+ * Render a single item (file or folder)
168
+ * @param {Object} item
169
+ * @returns {string} HTML
170
+ */
171
+ _renderItem(item) {
172
+ const isFolder = item.type === 'folder';
173
+ const icon = isFolder ? 'bi-folder-fill folder-icon' : this._getFileIcon(item.name);
174
+ const isExpanded = this.expandedFolders.has(item.path);
175
+
176
+ // TASK-240: Add 'into' action for folders (enter folder view)
177
+ const actions = isFolder
178
+ ? ['into', 'rename', 'delete', 'duplicate']
179
+ : ['rename', 'delete', 'duplicate', 'download'];
180
+
181
+ // TASK-241: Add draggable support
182
+ return `
183
+ <div class="folder-view-item ${isFolder ? 'is-folder' : 'is-file'} ${isExpanded ? 'expanded' : ''}"
184
+ data-path="${item.path}"
185
+ data-type="${item.type}"
186
+ data-name="${this._escapeHtml(item.name)}"
187
+ draggable="true">
188
+ <div class="folder-view-item-main">
189
+ ${isFolder ? `
190
+ <button class="folder-view-item-toggle" title="Expand folder">
191
+ <i class="bi bi-chevron-right"></i>
192
+ </button>
193
+ ` : '<span class="folder-view-item-spacer"></span>'}
194
+ <i class="folder-view-item-icon bi ${icon}"></i>
195
+ <span class="folder-view-item-name">${this._escapeHtml(item.name)}</span>
196
+ <div class="folder-view-item-actions">
197
+ ${actions.map(action => `
198
+ <button class="folder-view-item-action ${action === 'delete' ? 'danger' : ''} ${action === 'into' ? 'into-btn' : ''}"
199
+ data-action="${action}"
200
+ title="${this._getActionTitle(action)}">
201
+ <i class="bi bi-${this._getActionIcon(action)}"></i>
202
+ </button>
203
+ `).join('')}
204
+ </div>
205
+ </div>
206
+ ${isFolder ? '<div class="folder-view-item-children"></div>' : ''}
207
+ </div>
208
+ `;
209
+ }
210
+
211
+ /**
212
+ * Get file icon based on extension
213
+ * @param {string} filename
214
+ * @returns {string} Bootstrap icon class
215
+ */
216
+ _getFileIcon(filename) {
217
+ const ext = filename.split('.').pop()?.toLowerCase();
218
+ const iconMap = {
219
+ 'md': 'bi-markdown',
220
+ 'txt': 'bi-file-text',
221
+ 'json': 'bi-braces',
222
+ 'html': 'bi-filetype-html',
223
+ 'css': 'bi-filetype-css',
224
+ 'js': 'bi-filetype-js',
225
+ 'py': 'bi-filetype-py',
226
+ 'pdf': 'bi-file-pdf',
227
+ 'png': 'bi-file-image',
228
+ 'jpg': 'bi-file-image',
229
+ 'jpeg': 'bi-file-image',
230
+ 'gif': 'bi-file-image',
231
+ 'svg': 'bi-file-image'
232
+ };
233
+ return iconMap[ext] || 'bi-file-earmark';
234
+ }
235
+
236
+ /**
237
+ * Get action icon
238
+ * @param {string} action
239
+ * @returns {string}
240
+ */
241
+ _getActionIcon(action) {
242
+ const icons = {
243
+ 'into': 'box-arrow-in-right', // TASK-240: Enter folder icon
244
+ 'rename': 'pencil',
245
+ 'delete': 'trash',
246
+ 'duplicate': 'copy',
247
+ 'download': 'download'
248
+ };
249
+ return icons[action] || action;
250
+ }
251
+
252
+ /**
253
+ * Get action title
254
+ * @param {string} action
255
+ * @returns {string}
256
+ */
257
+ _getActionTitle(action) {
258
+ const titles = {
259
+ 'into': 'Enter folder', // TASK-240: Enter folder tooltip
260
+ 'rename': 'Rename',
261
+ 'delete': 'Delete',
262
+ 'duplicate': 'Duplicate',
263
+ 'download': 'Download'
264
+ };
265
+ return titles[action] || action;
266
+ }
267
+
268
+ /**
269
+ * Bind event listeners
270
+ */
271
+ _bindEvents() {
272
+ const container = this.container.querySelector('.folder-view');
273
+ if (!container) return;
274
+
275
+ // Close button
276
+ const closeBtn = container.querySelector('.folder-view-close');
277
+ if (closeBtn) {
278
+ closeBtn.addEventListener('click', () => {
279
+ if (this.onClose) this.onClose();
280
+ });
281
+ }
282
+
283
+ // Breadcrumb navigation
284
+ container.querySelectorAll('.breadcrumb-item.clickable').forEach(item => {
285
+ item.addEventListener('click', () => {
286
+ const path = item.dataset.path;
287
+ if (path === '') {
288
+ // Navigate to root - close folder view
289
+ if (this.onClose) this.onClose();
290
+ } else {
291
+ this.render(path);
292
+ }
293
+ });
294
+ });
295
+
296
+ // Action bar buttons
297
+ container.querySelectorAll('.folder-view-action-btn').forEach(btn => {
298
+ btn.addEventListener('click', () => {
299
+ const action = btn.dataset.action;
300
+ this._handleFolderAction(action);
301
+ });
302
+ });
303
+
304
+ // Item clicks (file navigation, folder expansion)
305
+ container.querySelectorAll('.folder-view-item').forEach(item => {
306
+ // Main area click (not action buttons)
307
+ const mainArea = item.querySelector('.folder-view-item-main');
308
+ mainArea.addEventListener('click', (e) => {
309
+ // Ignore if clicking on action buttons or toggle
310
+ if (e.target.closest('.folder-view-item-actions') ||
311
+ e.target.closest('.folder-view-item-toggle')) {
312
+ return;
313
+ }
314
+
315
+ const path = item.dataset.path;
316
+ const type = item.dataset.type;
317
+
318
+ if (type === 'file' && this.onNavigate) {
319
+ this.onNavigate(path);
320
+ } else if (type === 'folder') {
321
+ this._toggleFolder(item);
322
+ }
323
+ });
324
+
325
+ // Toggle button for folders
326
+ const toggle = item.querySelector('.folder-view-item-toggle');
327
+ if (toggle) {
328
+ toggle.addEventListener('click', (e) => {
329
+ e.stopPropagation();
330
+ this._toggleFolder(item);
331
+ });
332
+ }
333
+
334
+ // Item action buttons
335
+ item.querySelectorAll('.folder-view-item-action').forEach(btn => {
336
+ btn.addEventListener('click', (e) => {
337
+ e.stopPropagation();
338
+ const action = btn.dataset.action;
339
+ const path = item.dataset.path;
340
+ const name = item.dataset.name;
341
+ const type = item.dataset.type;
342
+ this._handleItemAction(action, path, name, type);
343
+ });
344
+ });
345
+
346
+ // TASK-241: Drag and drop events
347
+ this._bindDragEvents(item);
348
+ });
349
+
350
+ // TASK-241: Allow dropping on the folder view content area (move to current folder)
351
+ const contentArea = container.querySelector('.folder-view-content');
352
+ if (contentArea) {
353
+ this._bindDropZone(contentArea);
354
+ }
355
+ }
356
+
357
+ /**
358
+ * TASK-241: Bind drag events to an item
359
+ * @param {HTMLElement} item
360
+ */
361
+ _bindDragEvents(item) {
362
+ item.addEventListener('dragstart', (e) => {
363
+ e.stopPropagation();
364
+ const path = item.dataset.path;
365
+ const type = item.dataset.type;
366
+ const name = item.dataset.name;
367
+
368
+ e.dataTransfer.setData('text/plain', JSON.stringify({ path, type, name }));
369
+ e.dataTransfer.effectAllowed = 'move';
370
+ item.classList.add('dragging');
371
+
372
+ // Store reference for drop validation
373
+ this._draggingItem = { path, type, name };
374
+ });
375
+
376
+ item.addEventListener('dragend', (e) => {
377
+ item.classList.remove('dragging');
378
+ this._draggingItem = null;
379
+
380
+ // Clean up any drop-target classes
381
+ this.container.querySelectorAll('.drop-target').forEach(el => {
382
+ el.classList.remove('drop-target');
383
+ });
384
+ });
385
+
386
+ // Only folders can be drop targets
387
+ if (item.dataset.type === 'folder') {
388
+ item.addEventListener('dragover', (e) => {
389
+ e.preventDefault();
390
+ e.stopPropagation();
391
+
392
+ // Validate drop is allowed
393
+ if (this._canDropOn(item.dataset.path)) {
394
+ e.dataTransfer.dropEffect = 'move';
395
+ item.classList.add('drop-target');
396
+ } else {
397
+ e.dataTransfer.dropEffect = 'none';
398
+ }
399
+ });
400
+
401
+ item.addEventListener('dragleave', (e) => {
402
+ // Only remove if actually leaving this element
403
+ if (!item.contains(e.relatedTarget)) {
404
+ item.classList.remove('drop-target');
405
+ }
406
+ });
407
+
408
+ item.addEventListener('drop', async (e) => {
409
+ e.preventDefault();
410
+ e.stopPropagation();
411
+ item.classList.remove('drop-target');
412
+
413
+ const targetPath = item.dataset.path;
414
+ await this._handleDrop(e, targetPath);
415
+ });
416
+ }
417
+ }
418
+
419
+ /**
420
+ * TASK-241: Bind drop zone events (for dropping into current folder)
421
+ * @param {HTMLElement} zone
422
+ */
423
+ _bindDropZone(zone) {
424
+ zone.addEventListener('dragover', (e) => {
425
+ // Only handle if not over a folder item
426
+ if (!e.target.closest('.folder-view-item.is-folder')) {
427
+ e.preventDefault();
428
+ if (this._canDropOn(this.currentPath)) {
429
+ e.dataTransfer.dropEffect = 'move';
430
+ zone.classList.add('drop-target-zone');
431
+ }
432
+ }
433
+ });
434
+
435
+ zone.addEventListener('dragleave', (e) => {
436
+ if (!zone.contains(e.relatedTarget)) {
437
+ zone.classList.remove('drop-target-zone');
438
+ }
439
+ });
440
+
441
+ zone.addEventListener('drop', async (e) => {
442
+ // Only handle if not over a folder item
443
+ if (!e.target.closest('.folder-view-item.is-folder')) {
444
+ e.preventDefault();
445
+ zone.classList.remove('drop-target-zone');
446
+ await this._handleDrop(e, this.currentPath);
447
+ }
448
+ });
449
+ }
450
+
451
+ /**
452
+ * TASK-241: Check if item can be dropped on target
453
+ * @param {string} targetPath
454
+ * @returns {boolean}
455
+ */
456
+ _canDropOn(targetPath) {
457
+ if (!this._draggingItem) return false;
458
+
459
+ const sourcePath = this._draggingItem.path;
460
+
461
+ // Can't drop on self
462
+ if (sourcePath === targetPath) return false;
463
+
464
+ // Can't drop parent into child
465
+ if (targetPath.startsWith(sourcePath + '/')) return false;
466
+
467
+ // Can't drop into same parent (already there)
468
+ const sourceParent = sourcePath.substring(0, sourcePath.lastIndexOf('/'));
469
+ if (sourceParent === targetPath) return false;
470
+
471
+ return true;
472
+ }
473
+
474
+ /**
475
+ * TASK-241: Handle drop event
476
+ * @param {DragEvent} e
477
+ * @param {string} targetPath
478
+ */
479
+ async _handleDrop(e, targetPath) {
480
+ try {
481
+ const data = JSON.parse(e.dataTransfer.getData('text/plain'));
482
+ const sourcePath = data.path;
483
+
484
+ if (!this._canDropOn(targetPath)) {
485
+ return;
486
+ }
487
+
488
+ // Call move action through onAction callback
489
+ if (this.onAction) {
490
+ const success = await this.onAction('move', sourcePath, {
491
+ targetPath,
492
+ name: data.name,
493
+ type: data.type
494
+ });
495
+
496
+ if (success) {
497
+ await this.refresh();
498
+ }
499
+ }
500
+ } catch (error) {
501
+ console.error('Drop failed:', error);
502
+ }
503
+ }
504
+
505
+ /**
506
+ * Handle folder-level actions (from action bar)
507
+ * @param {string} action
508
+ */
509
+ async _handleFolderAction(action) {
510
+ switch (action) {
511
+ case 'add-file':
512
+ if (this.onAction) {
513
+ await this.onAction('add-file', this.currentPath, {});
514
+ }
515
+ break;
516
+ case 'add-folder':
517
+ if (this.onAction) {
518
+ await this.onAction('add-folder', this.currentPath, {});
519
+ }
520
+ break;
521
+ case 'rename':
522
+ if (this.onAction) {
523
+ await this.onAction('rename', this.currentPath, { type: 'folder' });
524
+ }
525
+ break;
526
+ case 'delete':
527
+ await this._confirmAndDelete(this.currentPath, 'folder');
528
+ break;
529
+ }
530
+ }
531
+
532
+ /**
533
+ * Handle item-level actions
534
+ * @param {string} action
535
+ * @param {string} path
536
+ * @param {string} name
537
+ * @param {string} type
538
+ */
539
+ async _handleItemAction(action, path, name, type) {
540
+ switch (action) {
541
+ case 'into': // TASK-240: Navigate into folder
542
+ if (type === 'folder') {
543
+ await this.render(path);
544
+ }
545
+ break;
546
+ case 'rename':
547
+ if (this.onAction) {
548
+ await this.onAction('rename', path, { name, type });
549
+ }
550
+ break;
551
+ case 'delete':
552
+ await this._confirmAndDelete(path, type, name);
553
+ break;
554
+ case 'duplicate':
555
+ if (this.onAction) {
556
+ const success = await this.onAction('duplicate', path, { name, type });
557
+ if (success) {
558
+ await this.refresh();
559
+ }
560
+ }
561
+ break;
562
+ case 'download':
563
+ this._downloadFile(path);
564
+ break;
565
+ }
566
+ }
567
+
568
+ /**
569
+ * Confirm and delete an item
570
+ * @param {string} path
571
+ * @param {string} type
572
+ * @param {string} name
573
+ */
574
+ async _confirmAndDelete(path, type, name = null) {
575
+ // Get delete info for confirmation
576
+ let itemCount = 0;
577
+ const itemName = name || path.split('/').pop();
578
+
579
+ if (type === 'folder') {
580
+ try {
581
+ const response = await fetch(`/api/ideas/delete-info?path=${encodeURIComponent(path)}`);
582
+ const data = await response.json();
583
+ if (data.success) {
584
+ itemCount = data.item_count || 0;
585
+ }
586
+ } catch (e) {
587
+ console.warn('Failed to get delete info:', e);
588
+ }
589
+ }
590
+
591
+ // Show confirmation
592
+ let confirmed = true;
593
+ if (this.confirmDialog) {
594
+ confirmed = await this.confirmDialog.confirmDelete(itemName, type, itemCount);
595
+ } else {
596
+ const msg = type === 'folder' && itemCount > 0
597
+ ? `Delete "${itemName}" and all ${itemCount} items inside?`
598
+ : `Delete "${itemName}"?`;
599
+ confirmed = confirm(msg);
600
+ }
601
+
602
+ if (confirmed && this.onAction) {
603
+ const success = await this.onAction('delete', path, { name: itemName, type });
604
+ if (success) {
605
+ if (path === this.currentPath) {
606
+ // Deleted current folder - close view
607
+ if (this.onClose) this.onClose();
608
+ } else {
609
+ await this.refresh();
610
+ }
611
+ }
612
+ }
613
+ }
614
+
615
+ /**
616
+ * Download a file
617
+ * @param {string} path
618
+ */
619
+ _downloadFile(path) {
620
+ const url = `/api/ideas/download?path=${encodeURIComponent(path)}`;
621
+ const link = document.createElement('a');
622
+ link.href = url;
623
+ link.download = path.split('/').pop();
624
+ document.body.appendChild(link);
625
+ link.click();
626
+ document.body.removeChild(link);
627
+ }
628
+
629
+ /**
630
+ * Toggle folder expansion
631
+ * @param {HTMLElement} folderItem
632
+ */
633
+ async _toggleFolder(folderItem) {
634
+ const path = folderItem.dataset.path;
635
+ const childrenContainer = folderItem.querySelector('.folder-view-item-children');
636
+
637
+ if (!childrenContainer) return;
638
+
639
+ const isExpanded = folderItem.classList.contains('expanded');
640
+
641
+ if (isExpanded) {
642
+ // Collapse
643
+ folderItem.classList.remove('expanded');
644
+ childrenContainer.innerHTML = '';
645
+ this.expandedFolders.delete(path);
646
+ } else {
647
+ // Expand - load children
648
+ try {
649
+ const contents = await this._loadContents(path);
650
+ childrenContainer.innerHTML = this._renderContents(contents);
651
+ folderItem.classList.add('expanded');
652
+ this.expandedFolders.add(path);
653
+
654
+ // Rebind events for new items
655
+ this._bindChildEvents(childrenContainer);
656
+ } catch (error) {
657
+ console.error('Failed to expand folder:', error);
658
+ }
659
+ }
660
+ }
661
+
662
+ /**
663
+ * Bind events for dynamically loaded children
664
+ * @param {HTMLElement} container
665
+ */
666
+ _bindChildEvents(container) {
667
+ container.querySelectorAll('.folder-view-item').forEach(item => {
668
+ const mainArea = item.querySelector('.folder-view-item-main');
669
+ mainArea.addEventListener('click', (e) => {
670
+ if (e.target.closest('.folder-view-item-actions') ||
671
+ e.target.closest('.folder-view-item-toggle')) {
672
+ return;
673
+ }
674
+
675
+ const path = item.dataset.path;
676
+ const type = item.dataset.type;
677
+
678
+ if (type === 'file' && this.onNavigate) {
679
+ this.onNavigate(path);
680
+ } else if (type === 'folder') {
681
+ this._toggleFolder(item);
682
+ }
683
+ });
684
+
685
+ const toggle = item.querySelector('.folder-view-item-toggle');
686
+ if (toggle) {
687
+ toggle.addEventListener('click', (e) => {
688
+ e.stopPropagation();
689
+ this._toggleFolder(item);
690
+ });
691
+ }
692
+
693
+ item.querySelectorAll('.folder-view-item-action').forEach(btn => {
694
+ btn.addEventListener('click', (e) => {
695
+ e.stopPropagation();
696
+ const action = btn.dataset.action;
697
+ const path = item.dataset.path;
698
+ const name = item.dataset.name;
699
+ const type = item.dataset.type;
700
+ this._handleItemAction(action, path, name, type);
701
+ });
702
+ });
703
+
704
+ // TASK-241: Bind drag events for dynamically loaded children
705
+ this._bindDragEvents(item);
706
+ });
707
+ }
708
+
709
+ /**
710
+ * Refresh current folder view
711
+ */
712
+ async refresh() {
713
+ if (this.currentPath) {
714
+ await this.render(this.currentPath);
715
+ }
716
+ }
717
+
718
+ /**
719
+ * Close the folder view
720
+ */
721
+ close() {
722
+ this.currentPath = null;
723
+ this.expandedFolders.clear();
724
+ this.container.innerHTML = '';
725
+ }
726
+
727
+ /**
728
+ * Escape HTML to prevent XSS
729
+ * @param {string} str
730
+ * @returns {string}
731
+ */
732
+ _escapeHtml(str) {
733
+ const div = document.createElement('div');
734
+ div.textContent = str;
735
+ return div.innerHTML;
736
+ }
737
+ }
738
+
739
+ // Export for module usage
740
+ if (typeof module !== 'undefined' && module.exports) {
741
+ module.exports = FolderViewManager;
742
+ }