@qnote/q-ai-note 1.0.3 → 1.0.5

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.
@@ -52,7 +52,16 @@ export function mountHtmlList(targetId, htmlItems) {
52
52
  export function mountDiaryTimeline(targetId, options) {
53
53
  const container = byId(targetId);
54
54
  if (!container) return;
55
- const { diaries = [], getSandboxName, onConfirm, onIgnore } = options || {};
55
+ const {
56
+ diaries = [],
57
+ getSandboxName,
58
+ getWorkItemName,
59
+ onConfirm,
60
+ onIgnore,
61
+ onEdit,
62
+ onOpenWorkItem,
63
+ renderContent,
64
+ } = options || {};
56
65
 
57
66
  if (!diaries.length) {
58
67
  container.innerHTML = '<div class="empty-state"><p>暂无日记</p></div>';
@@ -61,17 +70,29 @@ export function mountDiaryTimeline(targetId, options) {
61
70
 
62
71
  container.innerHTML = diaries.map((diary) => `
63
72
  <div class="diary-item ${diary.processed ? 'processed' : ''}">
64
- <div class="diary-meta">
65
- ${esc(new Date(diary.created_at).toLocaleString())}
66
- ${diary.sandbox_id ? ` · ${esc(getSandboxName?.(diary.sandbox_id) || diary.sandbox_id)}` : ''}
67
- </div>
68
- <div class="diary-content">${esc(diary.content)}</div>
69
- ${diary.processed ? '' : `
73
+ <div class="diary-head-row">
74
+ <div class="diary-meta">
75
+ ${esc(new Date(diary.created_at).toLocaleString())}
76
+ ${diary.sandbox_id ? ` · ${esc(getSandboxName?.(diary.sandbox_id) || diary.sandbox_id)}` : ''}
77
+ ${diary.work_item_id ? `
78
+ · 节点:
79
+ <button
80
+ class="diary-open-node-link"
81
+ type="button"
82
+ data-open-work-item="${esc(diary.work_item_id)}"
83
+ data-open-sandbox="${esc(diary.sandbox_id || '')}"
84
+ >${esc(getWorkItemName?.(diary.sandbox_id, diary.work_item_id) || diary.work_item_id)}</button>
85
+ ` : ''}
86
+ </div>
70
87
  <div class="diary-actions">
71
- <button class="confirm" data-confirm-id="${esc(diary.id)}">采纳</button>
72
- <button class="ignore" data-ignore-id="${esc(diary.id)}">忽略</button>
88
+ <button class="edit" data-edit-id="${esc(diary.id)}">编辑</button>
89
+ ${diary.processed ? '' : `
90
+ <button class="confirm" data-confirm-id="${esc(diary.id)}">采纳</button>
91
+ <button class="ignore" data-ignore-id="${esc(diary.id)}">忽略</button>
92
+ `}
73
93
  </div>
74
- `}
94
+ </div>
95
+ <div class="diary-content">${typeof renderContent === 'function' ? renderContent(diary.content) : esc(diary.content)}</div>
75
96
  </div>
76
97
  `).join('');
77
98
 
@@ -81,15 +102,28 @@ export function mountDiaryTimeline(targetId, options) {
81
102
  container.querySelectorAll('[data-ignore-id]').forEach((el) => {
82
103
  el.addEventListener('click', () => onIgnore?.(el.getAttribute('data-ignore-id')));
83
104
  });
105
+ container.querySelectorAll('[data-edit-id]').forEach((el) => {
106
+ el.addEventListener('click', () => onEdit?.(el.getAttribute('data-edit-id')));
107
+ });
108
+ container.querySelectorAll('[data-open-work-item]').forEach((el) => {
109
+ el.addEventListener('click', () => {
110
+ const workItemId = el.getAttribute('data-open-work-item');
111
+ const sandboxId = el.getAttribute('data-open-sandbox');
112
+ if (!workItemId || !sandboxId) return;
113
+ onOpenWorkItem?.(sandboxId, workItemId);
114
+ });
115
+ });
84
116
  }
85
117
 
86
118
  function renderEntityBadges(summary) {
87
119
  const issue = Number(summary?.issue || 0);
88
120
  const knowledge = Number(summary?.knowledge || 0);
89
121
  const capability = Number(summary?.capability || 0);
122
+ if (issue + knowledge + capability === 0) {
123
+ return '';
124
+ }
90
125
  const issueOpen = Number(summary?.issue_open || 0);
91
126
  const issueClosed = Number(summary?.issue_closed || 0);
92
- const isBlindSpot = issue + knowledge + capability === 0;
93
127
  const issueStateClass = issue === 0
94
128
  ? 'issue-none'
95
129
  : issueOpen > 0
@@ -99,20 +133,53 @@ function renderEntityBadges(summary) {
99
133
  : 'issue-none';
100
134
  return `
101
135
  <span
102
- class="node-entity-mini-badges ${isBlindSpot ? 'blind-spot' : ''} ${issueStateClass}"
136
+ class="node-entity-mini-badges ${issueStateClass}"
103
137
  title="节点摘要:${issue}/${knowledge}/${capability}(Issue/Knowledge/Capability)"
104
138
  >
105
- <span class="node-entity-mini-badge ${issue > 0 || knowledge > 0 || capability > 0 ? 'active' : ''}">${issue}/${knowledge}/${capability}</span>
139
+ <span class="node-entity-mini-badge active">${issue}/${knowledge}/${capability}</span>
106
140
  </span>
107
141
  `;
108
142
  }
109
143
 
110
- function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee = false) {
144
+ function pickEntityPreviewRows(nodeId, entityRowsByNodeId, mode) {
145
+ if (!entityRowsByNodeId || mode === 'none') return [];
146
+ const rows = entityRowsByNodeId[nodeId] || [];
147
+ if (mode === 'all') return rows;
148
+ return rows.filter((row) => row.entity_type === mode);
149
+ }
150
+
151
+ function renderEntityPreviewBoxes(nodeId, entityRowsByNodeId, mode) {
152
+ const rows = pickEntityPreviewRows(nodeId, entityRowsByNodeId, mode);
153
+ if (!rows.length) return '';
154
+ const shownRows = rows.slice(0, 4);
155
+ return `
156
+ <div class="node-entity-preview-strip" data-node-id="${esc(nodeId)}">
157
+ ${shownRows.map((row) => `
158
+ <button
159
+ class="entity-preview-box ${esc(row.entity_type)}"
160
+ type="button"
161
+ data-action="entity-preview"
162
+ data-node-id="${esc(nodeId)}"
163
+ data-entity-type="${esc(row.entity_type)}"
164
+ data-entity-id="${esc(row.id)}"
165
+ title="${esc(row.title || row.entity_type)}"
166
+ >
167
+ <span class="entity-preview-type">${esc(row.entity_type)}</span>
168
+ <span class="entity-preview-title">${esc(row.title || '-')}</span>
169
+ </button>
170
+ `).join('')}
171
+ ${rows.length > shownRows.length ? `<span class="entity-preview-more">+${rows.length - shownRows.length}</span>` : ''}
172
+ </div>
173
+ `;
174
+ }
175
+
176
+ function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee = false, entityRowsByNodeId = {}, elementPreviewMode = 'none') {
111
177
  const children = byParent.get(node.id) || [];
112
178
  const hasChildren = children.length > 0;
113
179
  const isExpanded = expandedIdSet.has(node.id);
114
180
  const isShort = String(node.name || '').trim().length <= 10;
115
181
  const nodeSummary = entitySummaryByNodeId?.[node.id] || { issue: 0, knowledge: 0, capability: 0 };
182
+ const previewHtml = renderEntityPreviewBoxes(node.id, entityRowsByNodeId, elementPreviewMode);
116
183
 
117
184
  if (!hasChildren) {
118
185
  return `
@@ -120,8 +187,10 @@ function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, sh
120
187
  <span class="node-status ${esc(node.status)}"></span>
121
188
  <span class="node-name ${isShort ? 'short-name' : ''}" data-select-id="${esc(node.id)}">${esc(node.name)}</span>
122
189
  ${renderEntityBadges(nodeSummary)}
190
+ ${previewHtml}
123
191
  <div class="node-actions">
124
192
  <button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(node.id)}" title="快捷提问">💬</button>
193
+ <button class="node-action-btn" data-action="add-diary" data-id="${esc(node.id)}" title="记录日记">📝</button>
125
194
  <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
126
195
  <button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
127
196
  <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
@@ -152,27 +221,32 @@ function renderTreeNode(node, byParent, expandedIdSet, entitySummaryByNodeId, sh
152
221
  ${showAssignee && node.assignee ? `<span class="node-meta">@${esc(node.assignee)}</span>` : ''}
153
222
  <div class="node-actions">
154
223
  <button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(node.id)}" title="快捷提问">💬</button>
224
+ <button class="node-action-btn" data-action="add-diary" data-id="${esc(node.id)}" title="记录日记">📝</button>
155
225
  <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(node.id)}" title="添加子任务">+</button>
156
226
  <button class="node-action-btn" data-action="edit" data-id="${esc(node.id)}" title="编辑">✎</button>
157
227
  <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
158
228
  </div>
159
229
  </div>
160
230
  ${isExpanded ? `
231
+ ${previewHtml ? `<div class="tree-node-preview-wrapper">${previewHtml}</div>` : ''}
161
232
  <div class="tree-parent-children">
162
- ${branchChildren.map((child) => renderTreeNode(child, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee)).join('')}
233
+ ${branchChildren.map((child) => renderTreeNode(child, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee, entityRowsByNodeId, elementPreviewMode)).join('')}
163
234
  ${leafChildren.length ? `
164
235
  <div class="tree-leaf-container">
165
236
  <div class="tree-leaf-grid">
166
237
  ${leafChildren.map((child) => {
167
238
  const shortName = String(child.name || '').trim().length <= 10;
168
239
  const childSummary = entitySummaryByNodeId?.[child.id] || { issue: 0, knowledge: 0, capability: 0 };
240
+ const childPreviewHtml = renderEntityPreviewBoxes(child.id, entityRowsByNodeId, elementPreviewMode);
169
241
  return `
170
242
  <div class="tree-leaf-node" data-id="${esc(child.id)}" data-select-id="${esc(child.id)}" tabindex="0">
171
243
  <span class="node-status ${esc(child.status)}"></span>
172
244
  <span class="node-name ${shortName ? 'short-name' : ''}" data-select-id="${esc(child.id)}">${esc(child.name)}</span>
173
245
  ${renderEntityBadges(childSummary)}
246
+ ${childPreviewHtml}
174
247
  <div class="node-actions">
175
248
  <button class="node-action-btn chat" data-action="quick-chat" data-id="${esc(child.id)}" title="快捷提问">💬</button>
249
+ <button class="node-action-btn" data-action="add-diary" data-id="${esc(child.id)}" title="记录日记">📝</button>
176
250
  <button class="node-action-btn add-child" data-action="add-child" data-id="${esc(child.id)}" title="添加子任务">+</button>
177
251
  <button class="node-action-btn" data-action="edit" data-id="${esc(child.id)}" title="编辑">✎</button>
178
252
  <button class="node-action-btn delete" data-action="delete" data-id="${esc(child.id)}" title="删除">✕</button>
@@ -232,18 +306,19 @@ function applyAdaptiveRootMixedGridLayout(container) {
232
306
  });
233
307
  }
234
308
 
235
- function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeId, depth = 0, showAssignee = false) {
309
+ function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeId, depth = 0, showAssignee = false, entityRowsByNodeId = {}, elementPreviewMode = 'none') {
236
310
  const children = byParent.get(node.id) || [];
237
311
  const hasChildren = children.length > 0;
238
312
  const isExpanded = expandedIdSet.has(node.id);
239
313
  const nodeSummary = entitySummaryByNodeId?.[node.id] || { issue: 0, knowledge: 0, capability: 0 };
240
314
  const useStackSummary = depth >= 3;
315
+ const previewHtml = renderEntityPreviewBoxes(node.id, entityRowsByNodeId, elementPreviewMode);
241
316
  const childrenHtml = hasChildren && isExpanded
242
- ? `<div class="lane-tree-children">${children.map((child) => renderDenseLaneNode(child, byParent, expandedIdSet, entitySummaryByNodeId, depth + 1, showAssignee)).join('')}</div>`
317
+ ? `<div class="lane-tree-children">${children.map((child) => renderDenseLaneNode(child, byParent, expandedIdSet, entitySummaryByNodeId, depth + 1, showAssignee, entityRowsByNodeId, elementPreviewMode)).join('')}</div>`
243
318
  : '';
244
319
  return `
245
320
  <div class="lane-tree-node" data-depth="${depth}">
246
- <div class="lane-tree-node-row" data-select-id="${esc(node.id)}" data-node-id="${esc(node.id)}" tabindex="0">
321
+ <div class="lane-tree-node-row" data-select-id="${esc(node.id)}" data-node-id="${esc(node.id)}" tabindex="0" draggable="true">
247
322
  ${hasChildren ? `
248
323
  <button
249
324
  class="node-expand-btn dense-expand-btn ${isExpanded ? 'expanded' : ''}"
@@ -277,6 +352,7 @@ function renderDenseLaneNode(node, byParent, expandedIdSet, entitySummaryByNodeI
277
352
  <button class="node-action-btn delete" data-action="delete" data-id="${esc(node.id)}" title="删除">✕</button>
278
353
  </div>
279
354
  </div>
355
+ ${previewHtml ? `<div class="lane-node-preview-wrapper">${previewHtml}</div>` : ''}
280
356
  ${childrenHtml}
281
357
  </div>
282
358
  `;
@@ -316,16 +392,17 @@ function getDenseLaneWidthPx(root, byParent, showAssignee = false) {
316
392
  return Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, preferred));
317
393
  }
318
394
 
319
- function renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee = false) {
395
+ function renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee = false, entityRowsByNodeId = {}, elementPreviewMode = 'none') {
320
396
  return `
321
397
  <div class="dense-tree dense-horizontal dense-lane-board">
322
398
  ${roots.map((root) => {
323
399
  const laneWidth = getDenseLaneWidthPx(root, byParent, showAssignee);
324
400
  const laneNameMax = Math.max(120, laneWidth - 86);
325
401
  return `
326
- <section class="dense-lane" style="--lane-width:${laneWidth}px;--lane-name-max:${laneNameMax}px;">
402
+ <section class="dense-lane" data-root-id="${esc(root.id)}" style="--lane-width:${laneWidth}px;--lane-name-max:${laneNameMax}px;">
403
+ <div class="dense-lane-drag-handle" data-lane-drag-id="${esc(root.id)}" draggable="true" title="拖拽调整泳道顺序">⋮⋮</div>
327
404
  <div class="dense-lane-body">
328
- ${renderDenseLaneNode(root, byParent, expandedIdSet, entitySummaryByNodeId, 0, showAssignee)}
405
+ ${renderDenseLaneNode(root, byParent, expandedIdSet, entitySummaryByNodeId, 0, showAssignee, entityRowsByNodeId, elementPreviewMode)}
329
406
  </div>
330
407
  </section>
331
408
  `;
@@ -342,11 +419,18 @@ export function mountWorkTree(targetId, options) {
342
419
  expandedIds = [],
343
420
  onToggleExpand,
344
421
  onAddChild,
422
+ onAddDiary,
345
423
  onEdit,
346
424
  onDelete,
347
425
  onQuickChat,
348
426
  onSelect,
427
+ onSelectEntity,
428
+ onMoveNode,
429
+ onReorderSiblings,
430
+ onReorderLanes,
349
431
  entitySummaryByNodeId = {},
432
+ entityRowsByNodeId = {},
433
+ elementPreviewMode = 'none',
350
434
  renderMode = 'card',
351
435
  showAssignee = false,
352
436
  } = options || {};
@@ -357,6 +441,33 @@ export function mountWorkTree(targetId, options) {
357
441
  if (!byParent.has(key)) byParent.set(key, []);
358
442
  byParent.get(key).push(item);
359
443
  }
444
+ const laneOrderOf = (item) => {
445
+ const value = Number(item?.extra_data?.lane_order);
446
+ return Number.isFinite(value) ? value : Number.POSITIVE_INFINITY;
447
+ };
448
+ const keyOf = (item, keyName) => String(item?.extra_data?.[keyName] || '').trim();
449
+ byParent.forEach((rows, parentKey) => {
450
+ rows.sort((a, b) => {
451
+ if (parentKey === '__root__') {
452
+ const laneKeyA = keyOf(a, 'lane_order_key');
453
+ const laneKeyB = keyOf(b, 'lane_order_key');
454
+ if (laneKeyA && laneKeyB && laneKeyA !== laneKeyB) return laneKeyA.localeCompare(laneKeyB);
455
+ if (laneKeyA && !laneKeyB) return -1;
456
+ if (!laneKeyA && laneKeyB) return 1;
457
+ const laneDiff = laneOrderOf(a) - laneOrderOf(b);
458
+ if (laneDiff !== 0) return laneDiff;
459
+ } else {
460
+ const orderKeyA = keyOf(a, 'order_key');
461
+ const orderKeyB = keyOf(b, 'order_key');
462
+ if (orderKeyA && orderKeyB && orderKeyA !== orderKeyB) return orderKeyA.localeCompare(orderKeyB);
463
+ if (orderKeyA && !orderKeyB) return -1;
464
+ if (!orderKeyA && orderKeyB) return 1;
465
+ }
466
+ const timeA = String(a.created_at || '');
467
+ const timeB = String(b.created_at || '');
468
+ return timeA.localeCompare(timeB);
469
+ });
470
+ });
360
471
  const roots = byParent.get('__root__') || [];
361
472
  const expandedIdSet = new Set(expandedIds);
362
473
 
@@ -365,13 +476,13 @@ export function mountWorkTree(targetId, options) {
365
476
  return;
366
477
  }
367
478
  if (renderMode === 'dense') {
368
- container.innerHTML = renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee);
479
+ container.innerHTML = renderDenseTree(roots, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee, entityRowsByNodeId, elementPreviewMode);
369
480
  } else {
370
481
  const htmlParts = roots.map((root) => {
371
482
  const hasChildren = ((byParent.get(root.id) || []).length > 0);
372
483
  return `
373
484
  <div class="root-grid-item ${hasChildren ? 'branch' : 'leaf'}" data-root-item-type="${hasChildren ? 'branch' : 'leaf'}">
374
- ${renderTreeNode(root, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee)}
485
+ ${renderTreeNode(root, byParent, expandedIdSet, entitySummaryByNodeId, showAssignee, entityRowsByNodeId, elementPreviewMode)}
375
486
  </div>
376
487
  `;
377
488
  });
@@ -382,10 +493,17 @@ export function mountWorkTree(targetId, options) {
382
493
  el.addEventListener('click', (e) => {
383
494
  e.stopPropagation();
384
495
  const action = el.getAttribute('data-action');
496
+ if (action === 'entity-preview') {
497
+ const nodeId = el.getAttribute('data-node-id');
498
+ const entityType = el.getAttribute('data-entity-type') || 'all';
499
+ if (nodeId) onSelectEntity?.(nodeId, entityType);
500
+ return;
501
+ }
385
502
  const id = el.getAttribute('data-id');
386
503
  if (!id) return;
387
504
  if (action === 'toggle') onToggleExpand?.(id);
388
505
  if (action === 'add-child') onAddChild?.(id);
506
+ if (action === 'add-diary') onAddDiary?.(id);
389
507
  if (action === 'edit') onEdit?.(id);
390
508
  if (action === 'delete') onDelete?.(id);
391
509
  if (action === 'quick-chat') onQuickChat?.(id, el);
@@ -402,5 +520,113 @@ export function mountWorkTree(targetId, options) {
402
520
  if (renderMode !== 'dense') {
403
521
  applyAdaptiveLeafGridLayout(container);
404
522
  applyAdaptiveRootMixedGridLayout(container);
523
+ return;
405
524
  }
525
+
526
+ let draggingNodeId = '';
527
+ let draggingLaneId = '';
528
+ const dragDropPositionByNodeId = new Map();
529
+ const rowEls = Array.from(container.querySelectorAll('.lane-tree-node-row[data-node-id]'));
530
+ rowEls.forEach((el) => {
531
+ const nodeId = el.getAttribute('data-node-id') || '';
532
+ el.addEventListener('dragstart', (event) => {
533
+ draggingNodeId = nodeId;
534
+ draggingLaneId = '';
535
+ el.classList.add('is-dragging');
536
+ event.stopPropagation();
537
+ event.dataTransfer?.setData('text/plain', nodeId);
538
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
539
+ });
540
+ el.addEventListener('dragend', () => {
541
+ draggingNodeId = '';
542
+ el.classList.remove('is-dragging');
543
+ rowEls.forEach((rowEl) => {
544
+ rowEl.classList.remove('drag-over-target');
545
+ rowEl.removeAttribute('data-drop-position');
546
+ });
547
+ dragDropPositionByNodeId.clear();
548
+ });
549
+ el.addEventListener('dragover', (event) => {
550
+ if (!draggingNodeId || draggingNodeId === nodeId) return;
551
+ event.preventDefault();
552
+ if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
553
+ const rect = el.getBoundingClientRect();
554
+ const offsetY = event.clientY - rect.top;
555
+ const ratio = rect.height > 0 ? offsetY / rect.height : 0.5;
556
+ let dropPosition = 'child';
557
+ if (ratio < 0.25) dropPosition = 'before';
558
+ else if (ratio > 0.75) dropPosition = 'after';
559
+ dragDropPositionByNodeId.set(nodeId, dropPosition);
560
+ el.setAttribute('data-drop-position', dropPosition);
561
+ el.classList.add('drag-over-target');
562
+ });
563
+ el.addEventListener('dragleave', () => {
564
+ el.classList.remove('drag-over-target');
565
+ el.removeAttribute('data-drop-position');
566
+ dragDropPositionByNodeId.delete(nodeId);
567
+ });
568
+ el.addEventListener('drop', (event) => {
569
+ event.preventDefault();
570
+ el.classList.remove('drag-over-target');
571
+ const dropPosition = dragDropPositionByNodeId.get(nodeId) || 'child';
572
+ el.removeAttribute('data-drop-position');
573
+ dragDropPositionByNodeId.delete(nodeId);
574
+ if (!draggingNodeId || draggingNodeId === nodeId) return;
575
+ if (dropPosition === 'before' || dropPosition === 'after') {
576
+ onReorderSiblings?.(draggingNodeId, nodeId, dropPosition);
577
+ } else {
578
+ onMoveNode?.(draggingNodeId, nodeId);
579
+ }
580
+ draggingNodeId = '';
581
+ });
582
+ });
583
+
584
+ const board = container.querySelector('.dense-lane-board');
585
+ board?.addEventListener('dragover', (event) => {
586
+ if (!draggingNodeId) return;
587
+ event.preventDefault();
588
+ });
589
+ board?.addEventListener('drop', (event) => {
590
+ if (!draggingNodeId) return;
591
+ event.preventDefault();
592
+ onMoveNode?.(draggingNodeId, null);
593
+ draggingNodeId = '';
594
+ });
595
+
596
+ const laneEls = Array.from(container.querySelectorAll('.dense-lane[data-root-id]'));
597
+ const laneHandleEls = Array.from(container.querySelectorAll('.dense-lane-drag-handle[data-lane-drag-id]'));
598
+ laneHandleEls.forEach((handle) => {
599
+ const rootId = handle.getAttribute('data-lane-drag-id') || '';
600
+ handle.addEventListener('dragstart', (event) => {
601
+ draggingLaneId = rootId;
602
+ draggingNodeId = '';
603
+ const lane = handle.closest('.dense-lane');
604
+ lane?.classList.add('is-lane-dragging');
605
+ event.dataTransfer?.setData('text/plain', rootId);
606
+ if (event.dataTransfer) event.dataTransfer.effectAllowed = 'move';
607
+ });
608
+ handle.addEventListener('dragend', () => {
609
+ const lane = handle.closest('.dense-lane');
610
+ lane?.classList.remove('is-lane-dragging');
611
+ draggingLaneId = '';
612
+ });
613
+ });
614
+ laneEls.forEach((el) => {
615
+ const rootId = el.getAttribute('data-root-id') || '';
616
+ el.addEventListener('dragover', (event) => {
617
+ if (!draggingLaneId || draggingLaneId === rootId) return;
618
+ event.preventDefault();
619
+ el.classList.add('lane-drag-over-target');
620
+ });
621
+ el.addEventListener('dragleave', () => {
622
+ el.classList.remove('lane-drag-over-target');
623
+ });
624
+ el.addEventListener('drop', (event) => {
625
+ event.preventDefault();
626
+ el.classList.remove('lane-drag-over-target');
627
+ if (!draggingLaneId || draggingLaneId === rootId) return;
628
+ onReorderLanes?.(draggingLaneId, rootId);
629
+ draggingLaneId = '';
630
+ });
631
+ });
406
632
  }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@qnote/q-ai-note",
3
- "version": "1.0.3",
3
+ "version": "1.0.5",
4
4
  "type": "module",
5
5
  "description": "AI-assisted personal work sandbox and diary system",
6
6
  "main": "dist/server/index.js",
7
7
  "bin": {
8
- "q-ai-note": "dist/cli.js"
8
+ "q-ai-note": "dist/cli.js",
9
+ "q-ai-note-server": "dist/cli-server.js"
9
10
  },
10
11
  "files": [
11
12
  "dist/**/*",
@@ -16,7 +17,8 @@
16
17
  "build": "tsc && node scripts/copy-web-assets.mjs",
17
18
  "start": "PORT=3000 node dist/server/index.js",
18
19
  "test": "vitest run",
19
- "test:e2e": "playwright test",
20
+ "test:e2e": "env -u CI playwright test --grep-invert @ai --workers=1",
21
+ "test:e2e:full": "playwright test",
20
22
  "test:watch": "vitest",
21
23
  "paths:check": "tsx scripts/print-runtime-paths.ts",
22
24
  "prepack": "npm run build"