declare-cc 1.0.7 → 1.0.8
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/declare-tools.cjs +41 -30
- package/dist/public/app.js +16 -771
- package/dist/public/index.html +2 -355
- package/package.json +1 -1
package/dist/public/app.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Declare
|
|
2
|
+
* Declare — app.js
|
|
3
3
|
*
|
|
4
|
-
* Fetches /api/graph and /api/status, renders a
|
|
5
|
-
*
|
|
4
|
+
* Fetches /api/graph and /api/status, renders a drill browser with detail panels,
|
|
5
|
+
* and live-updates via SSE when .planning/ changes.
|
|
6
6
|
*
|
|
7
7
|
* Zero external dependencies. Vanilla JS, no build step.
|
|
8
8
|
*/
|
|
@@ -25,9 +25,6 @@ let statusData = null;
|
|
|
25
25
|
/** @type {string | null} */
|
|
26
26
|
let selectedNodeId = null;
|
|
27
27
|
|
|
28
|
-
/** @type {string | null} Focus node ID — the declaration or milestone being focused */
|
|
29
|
-
let focusNodeId = null;
|
|
30
|
-
|
|
31
28
|
/** @type {Set<string>} Tracks which action IDs are currently executing */
|
|
32
29
|
let runningActions = new Set();
|
|
33
30
|
|
|
@@ -66,10 +63,8 @@ const drillNavState = {
|
|
|
66
63
|
actions: { focusIndex: 0, scrollTop: 0 },
|
|
67
64
|
};
|
|
68
65
|
|
|
69
|
-
/** @type {'
|
|
70
|
-
let viewMode =
|
|
71
|
-
// Execution mode is only valid during active play; fall back to columns on reload
|
|
72
|
-
if (viewMode === 'execution') viewMode = 'columns';
|
|
66
|
+
/** @type {'columns'|'execution'} Current view mode */
|
|
67
|
+
let viewMode = 'columns';
|
|
73
68
|
|
|
74
69
|
/** @type {boolean} Whether the declaration input form is visible */
|
|
75
70
|
let declFormVisible = false;
|
|
@@ -186,17 +181,12 @@ if ($projectName) {
|
|
|
186
181
|
});
|
|
187
182
|
}
|
|
188
183
|
|
|
189
|
-
const $nodesDecls = document.getElementById('nodes-declarations');
|
|
190
|
-
const $nodesMiles = document.getElementById('nodes-milestones');
|
|
191
|
-
const $nodesActs = document.getElementById('nodes-actions');
|
|
192
|
-
const $edgesSvg = document.getElementById('edges-svg');
|
|
193
|
-
|
|
194
184
|
const $sidePanel = document.getElementById('side-panel');
|
|
195
185
|
const $panelBody = document.getElementById('panel-body');
|
|
196
186
|
const $panelEmpty = document.getElementById('panel-empty');
|
|
197
187
|
|
|
198
|
-
const $colBrowser = document.getElementById('column-browser');
|
|
199
188
|
const $readinessBanner = document.getElementById('readiness-banner');
|
|
189
|
+
const $colBrowser = document.getElementById('column-browser');
|
|
200
190
|
const $colDeclList = document.getElementById('col-decl-list');
|
|
201
191
|
const $colMileList = document.getElementById('col-mile-list');
|
|
202
192
|
const $colActList = document.getElementById('col-act-list');
|
|
@@ -215,9 +205,6 @@ const $drillPrompt = document.getElementById('drill-prompt');
|
|
|
215
205
|
if (drillBody && actFeed) drillBody.appendChild(actFeed);
|
|
216
206
|
})();
|
|
217
207
|
|
|
218
|
-
const $viewToggle = document.getElementById('view-toggle');
|
|
219
|
-
const $viewToggleLabel = document.getElementById('view-toggle-label');
|
|
220
|
-
const $canvasWrap = document.getElementById('canvas-wrap');
|
|
221
208
|
const $execView = document.getElementById('execution-view');
|
|
222
209
|
const $execOutputHeader = document.getElementById('exec-output-header');
|
|
223
210
|
const $execOutputLog = document.getElementById('exec-output-log');
|
|
@@ -378,7 +365,6 @@ async function loadData() {
|
|
|
378
365
|
|
|
379
366
|
hideOverlay();
|
|
380
367
|
renderStatusBar();
|
|
381
|
-
renderGraph();
|
|
382
368
|
parseDrillHash(); // Restore drill state from URL hash on load
|
|
383
369
|
renderDrillView();
|
|
384
370
|
updateLastUpdated();
|
|
@@ -548,164 +534,6 @@ function deriveDeclarationStatus(declaration, enrichedMilestones) {
|
|
|
548
534
|
return 'PENDING';
|
|
549
535
|
}
|
|
550
536
|
|
|
551
|
-
/**
|
|
552
|
-
* Build a node DOM element.
|
|
553
|
-
* @param {object} item
|
|
554
|
-
* @param {'declaration'|'milestone'|'action'} type
|
|
555
|
-
* @param {{ displayStatus?: string, doneCount?: number, totalCount?: number }} [derived]
|
|
556
|
-
* @returns {HTMLElement}
|
|
557
|
-
*/
|
|
558
|
-
function buildNodeEl(item, type, derived = {}) {
|
|
559
|
-
const displayStatus = derived.displayStatus || item.status || 'PENDING';
|
|
560
|
-
const el = document.createElement('div');
|
|
561
|
-
el.className = `node node-${type} status-${statusClass(displayStatus)}`;
|
|
562
|
-
el.dataset.nodeId = item.id;
|
|
563
|
-
el.dataset.nodeType = type;
|
|
564
|
-
|
|
565
|
-
// Wholeness left-border indicator
|
|
566
|
-
const wh = item.wholeness;
|
|
567
|
-
if (wh === 'whole' || wh === 'partial' || wh === 'broken') {
|
|
568
|
-
el.classList.add(`wholeness-${wh}`);
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
const title = item.title || item.statement || item.id;
|
|
572
|
-
|
|
573
|
-
// Progress bar for milestones with actions
|
|
574
|
-
let progressHtml = '';
|
|
575
|
-
if (type === 'milestone' && derived.totalCount > 0) {
|
|
576
|
-
const pct = Math.round((derived.doneCount / derived.totalCount) * 100);
|
|
577
|
-
const countLabel = `${derived.doneCount}/${derived.totalCount}`;
|
|
578
|
-
progressHtml = `
|
|
579
|
-
<div class="node-progress" title="${countLabel} actions done">
|
|
580
|
-
<div class="node-progress-fill" style="width:${pct}%"></div>
|
|
581
|
-
</div>`;
|
|
582
|
-
}
|
|
583
|
-
|
|
584
|
-
// Badge label — show progress count for executing milestones
|
|
585
|
-
let badgeLabel = displayStatus;
|
|
586
|
-
if (type === 'milestone' && displayStatus === 'EXECUTING' && derived.totalCount > 0) {
|
|
587
|
-
badgeLabel = `${derived.doneCount}/${derived.totalCount} DONE`;
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
// Integrity indicator — small colored dot next to status badge
|
|
591
|
-
// Skip for "broken" when node has no children (treat as pending/not-yet-computable)
|
|
592
|
-
let integrityDotHtml = '';
|
|
593
|
-
if (wh === 'whole' || wh === 'partial') {
|
|
594
|
-
integrityDotHtml = `<span class="integrity-dot integrity-${wh}" title="Integrity: ${wh}"></span>`;
|
|
595
|
-
} else if (wh === 'broken') {
|
|
596
|
-
// Only show broken dot if this node actually has children (not just "nothing to compute")
|
|
597
|
-
if (type === 'action') {
|
|
598
|
-
integrityDotHtml = `<span class="integrity-dot integrity-broken" title="Integrity: broken"></span>`;
|
|
599
|
-
} else if (type === 'milestone' && derived.totalCount > 0) {
|
|
600
|
-
integrityDotHtml = `<span class="integrity-dot integrity-broken" title="Integrity: broken"></span>`;
|
|
601
|
-
} else if (type === 'declaration') {
|
|
602
|
-
// Declarations: check if they have child milestones
|
|
603
|
-
const hasChildren = graphData && (graphData.milestones || []).some(m => (m.realizes || []).includes(item.id));
|
|
604
|
-
if (hasChildren) {
|
|
605
|
-
integrityDotHtml = `<span class="integrity-dot integrity-broken" title="Integrity: broken"></span>`;
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
// If none of the above matched, no dot shown (pending/not-computable)
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
// Classification icon for milestones (robot for agent, person for human)
|
|
612
|
-
let classIconHtml = '';
|
|
613
|
-
if (type === 'milestone' && item.classification) {
|
|
614
|
-
const isHuman = item.classification === 'human';
|
|
615
|
-
const icon = isHuman ? '\u{1F464}' : '\u{1F916}';
|
|
616
|
-
const label = isHuman ? 'Human' : 'Agent';
|
|
617
|
-
classIconHtml = `<span class="class-icon" title="${label}">${icon}</span>`;
|
|
618
|
-
}
|
|
619
|
-
|
|
620
|
-
// Dependency indicator for milestones
|
|
621
|
-
let depIndicatorHtml = '';
|
|
622
|
-
if (type === 'milestone' && item.dependsOn && item.dependsOn.length > 0) {
|
|
623
|
-
depIndicatorHtml = `<span class="dep-indicator" title="Blocked by: ${item.dependsOn.join(', ')}">← ${item.dependsOn.length}</span>`;
|
|
624
|
-
}
|
|
625
|
-
|
|
626
|
-
// Readiness badge for milestones
|
|
627
|
-
let readinessBadgeHtml = '';
|
|
628
|
-
if (type === 'milestone' && item.readiness) {
|
|
629
|
-
const rs = item.readiness.state;
|
|
630
|
-
if (rs === 'ready') {
|
|
631
|
-
readinessBadgeHtml = '<span class="readiness-badge readiness-ready">READY</span>';
|
|
632
|
-
} else if (rs === 'blocked') {
|
|
633
|
-
const blockers = (item.readiness.blockedBy || []).join(', ');
|
|
634
|
-
readinessBadgeHtml = `<span class="readiness-badge readiness-blocked" title="Blocked by: ${blockers}">BLOCKED</span>`;
|
|
635
|
-
} else if (rs === 'no-actions') {
|
|
636
|
-
readinessBadgeHtml = '<span class="readiness-badge readiness-no-actions">NO ACTIONS</span>';
|
|
637
|
-
}
|
|
638
|
-
// done state: no badge needed (status already shows DONE)
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
el.innerHTML = `
|
|
642
|
-
<div class="node-id">${classIconHtml}${item.id}${depIndicatorHtml}</div>
|
|
643
|
-
<div class="node-title">${truncate(title, 55)}</div>
|
|
644
|
-
<span class="status-badge">${badgeLabel}</span>${readinessBadgeHtml}${integrityDotHtml}${reviewBadgeHtml(item.id, item.reviewState)}
|
|
645
|
-
${progressHtml}
|
|
646
|
-
`;
|
|
647
|
-
|
|
648
|
-
el.addEventListener('click', () => selectNode(item.id, type));
|
|
649
|
-
return el;
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
// ─── Graph renderer ───────────────────────────────────────────────────────────
|
|
653
|
-
|
|
654
|
-
function renderGraph() {
|
|
655
|
-
if (!graphData) return;
|
|
656
|
-
|
|
657
|
-
const { declarations, milestones, actions } = graphData;
|
|
658
|
-
|
|
659
|
-
// ── Compute derived statuses from action data (always reflects reality) ──────
|
|
660
|
-
// Milestones
|
|
661
|
-
const enrichedMilestones = (milestones || []).map(m => ({
|
|
662
|
-
...m,
|
|
663
|
-
...deriveMilestoneStatus(m, actions || []),
|
|
664
|
-
}));
|
|
665
|
-
|
|
666
|
-
// Declarations
|
|
667
|
-
const enrichedDeclarations = (declarations || []).map(d => ({
|
|
668
|
-
...d,
|
|
669
|
-
displayStatus: deriveDeclarationStatus(d, enrichedMilestones),
|
|
670
|
-
}));
|
|
671
|
-
|
|
672
|
-
// Clear containers
|
|
673
|
-
$nodesDecls.innerHTML = '';
|
|
674
|
-
$nodesMiles.innerHTML = '';
|
|
675
|
-
$nodesActs.innerHTML = '';
|
|
676
|
-
|
|
677
|
-
// Render
|
|
678
|
-
enrichedDeclarations.forEach(d => {
|
|
679
|
-
$nodesDecls.appendChild(buildNodeEl(d, 'declaration', { displayStatus: d.displayStatus }));
|
|
680
|
-
});
|
|
681
|
-
|
|
682
|
-
// Sort milestones: ready first, then no-actions, then blocked, then done
|
|
683
|
-
const readinessOrder = { ready: 0, 'no-actions': 1, blocked: 2, done: 3 };
|
|
684
|
-
const sortedMilestones = [...enrichedMilestones].sort((a, b) => {
|
|
685
|
-
const aState = (a.readiness && a.readiness.state) || 'blocked';
|
|
686
|
-
const bState = (b.readiness && b.readiness.state) || 'blocked';
|
|
687
|
-
return (readinessOrder[aState] ?? 2) - (readinessOrder[bState] ?? 2);
|
|
688
|
-
});
|
|
689
|
-
|
|
690
|
-
sortedMilestones.forEach(m => {
|
|
691
|
-
$nodesMiles.appendChild(buildNodeEl(m, 'milestone', {
|
|
692
|
-
displayStatus: m.displayStatus,
|
|
693
|
-
doneCount: m.doneCount,
|
|
694
|
-
totalCount: m.totalCount,
|
|
695
|
-
}));
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
(actions || []).forEach(a => {
|
|
699
|
-
$nodesActs.appendChild(buildNodeEl(a, 'action'));
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
// Draw edges after layout settles
|
|
703
|
-
requestAnimationFrame(() => drawEdges());
|
|
704
|
-
|
|
705
|
-
// Mark running actions with pulsing indicator
|
|
706
|
-
updateRunningIndicators();
|
|
707
|
-
}
|
|
708
|
-
|
|
709
537
|
// ─── Declaration input form ───────────────────────────────────────────────────
|
|
710
538
|
|
|
711
539
|
/**
|
|
@@ -864,7 +692,7 @@ function renderColumnBrowser() {
|
|
|
864
692
|
|
|
865
693
|
const { declarations, milestones, actions } = graphData;
|
|
866
694
|
|
|
867
|
-
// Compute enriched milestones and declarations
|
|
695
|
+
// Compute enriched milestones and declarations
|
|
868
696
|
const enrichedMilestones = (milestones || []).map(m => ({
|
|
869
697
|
...m,
|
|
870
698
|
...deriveMilestoneStatus(m, actions || []),
|
|
@@ -3363,147 +3191,6 @@ document.addEventListener('click', async (e) => {
|
|
|
3363
3191
|
}
|
|
3364
3192
|
});
|
|
3365
3193
|
|
|
3366
|
-
// ─── Edge drawing ─────────────────────────────────────────────────────────────
|
|
3367
|
-
|
|
3368
|
-
/**
|
|
3369
|
-
* Get the center-bottom point of a DOM element relative to #canvas-container.
|
|
3370
|
-
* @param {Element} el
|
|
3371
|
-
* @returns {{ x: number, y: number }}
|
|
3372
|
-
*/
|
|
3373
|
-
function getBottomCenter(el) {
|
|
3374
|
-
const containerRect = document.getElementById('canvas-container').getBoundingClientRect();
|
|
3375
|
-
const scrollLeft = document.getElementById('canvas-wrap').scrollLeft;
|
|
3376
|
-
const scrollTop = document.getElementById('canvas-wrap').scrollTop;
|
|
3377
|
-
const r = el.getBoundingClientRect();
|
|
3378
|
-
return {
|
|
3379
|
-
x: r.left - containerRect.left + scrollLeft + r.width / 2,
|
|
3380
|
-
y: r.top - containerRect.top + scrollTop + r.height,
|
|
3381
|
-
};
|
|
3382
|
-
}
|
|
3383
|
-
|
|
3384
|
-
/**
|
|
3385
|
-
* Get the center-top point of a DOM element relative to #canvas-container.
|
|
3386
|
-
* @param {Element} el
|
|
3387
|
-
* @returns {{ x: number, y: number }}
|
|
3388
|
-
*/
|
|
3389
|
-
function getTopCenter(el) {
|
|
3390
|
-
const containerRect = document.getElementById('canvas-container').getBoundingClientRect();
|
|
3391
|
-
const scrollLeft = document.getElementById('canvas-wrap').scrollLeft;
|
|
3392
|
-
const scrollTop = document.getElementById('canvas-wrap').scrollTop;
|
|
3393
|
-
const r = el.getBoundingClientRect();
|
|
3394
|
-
return {
|
|
3395
|
-
x: r.left - containerRect.left + scrollLeft + r.width / 2,
|
|
3396
|
-
y: r.top - containerRect.top + scrollTop,
|
|
3397
|
-
};
|
|
3398
|
-
}
|
|
3399
|
-
|
|
3400
|
-
/**
|
|
3401
|
-
* Draw a cubic bezier SVG path from (x1,y1) to (x2,y2).
|
|
3402
|
-
* @param {number} x1
|
|
3403
|
-
* @param {number} y1
|
|
3404
|
-
* @param {number} x2
|
|
3405
|
-
* @param {number} y2
|
|
3406
|
-
* @returns {string}
|
|
3407
|
-
*/
|
|
3408
|
-
function curvePath(x1, y1, x2, y2) {
|
|
3409
|
-
const cy = (y1 + y2) / 2;
|
|
3410
|
-
return `M ${x1} ${y1} C ${x1} ${cy}, ${x2} ${cy}, ${x2} ${y2}`;
|
|
3411
|
-
}
|
|
3412
|
-
|
|
3413
|
-
/**
|
|
3414
|
-
* Build an SVG path element for an edge.
|
|
3415
|
-
* @param {string} d
|
|
3416
|
-
* @param {boolean} highlight
|
|
3417
|
-
* @returns {SVGPathElement}
|
|
3418
|
-
*/
|
|
3419
|
-
function makePath(d, highlight) {
|
|
3420
|
-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
3421
|
-
path.setAttribute('d', d);
|
|
3422
|
-
path.setAttribute('class', highlight ? 'edge highlight' : 'edge');
|
|
3423
|
-
return path;
|
|
3424
|
-
}
|
|
3425
|
-
|
|
3426
|
-
function drawEdges() {
|
|
3427
|
-
if (!graphData) return;
|
|
3428
|
-
|
|
3429
|
-
const { milestones, actions } = graphData;
|
|
3430
|
-
const container = document.getElementById('canvas-container');
|
|
3431
|
-
|
|
3432
|
-
// Resize SVG to container dimensions
|
|
3433
|
-
$edgesSvg.setAttribute('width', String(container.scrollWidth));
|
|
3434
|
-
$edgesSvg.setAttribute('height', String(container.scrollHeight));
|
|
3435
|
-
$edgesSvg.innerHTML = '';
|
|
3436
|
-
|
|
3437
|
-
const fragment = document.createDocumentFragment();
|
|
3438
|
-
|
|
3439
|
-
// Milestone → Declaration edges (realizes)
|
|
3440
|
-
(milestones || []).forEach(m => {
|
|
3441
|
-
const mEl = document.querySelector(`[data-node-id="${m.id}"]`);
|
|
3442
|
-
if (!mEl) return;
|
|
3443
|
-
const mTop = getTopCenter(mEl);
|
|
3444
|
-
|
|
3445
|
-
(m.realizes || []).forEach(dId => {
|
|
3446
|
-
const dEl = document.querySelector(`[data-node-id="${dId}"]`);
|
|
3447
|
-
if (!dEl) return;
|
|
3448
|
-
const dBot = getBottomCenter(dEl);
|
|
3449
|
-
const isHighlighted = selectedNodeId === m.id || selectedNodeId === dId;
|
|
3450
|
-
fragment.appendChild(makePath(curvePath(dBot.x, dBot.y, mTop.x, mTop.y), isHighlighted));
|
|
3451
|
-
});
|
|
3452
|
-
});
|
|
3453
|
-
|
|
3454
|
-
// Action → Milestone edges (causes)
|
|
3455
|
-
(actions || []).forEach(a => {
|
|
3456
|
-
const aEl = document.querySelector(`[data-node-id="${a.id}"]`);
|
|
3457
|
-
if (!aEl) return;
|
|
3458
|
-
const aTop = getTopCenter(aEl);
|
|
3459
|
-
|
|
3460
|
-
(a.causes || []).forEach(mId => {
|
|
3461
|
-
const mEl = document.querySelector(`[data-node-id="${mId}"]`);
|
|
3462
|
-
if (!mEl) return;
|
|
3463
|
-
const mBot = getBottomCenter(mEl);
|
|
3464
|
-
const isHighlighted = selectedNodeId === a.id || selectedNodeId === mId;
|
|
3465
|
-
fragment.appendChild(makePath(curvePath(mBot.x, mBot.y, aTop.x, aTop.y), isHighlighted));
|
|
3466
|
-
});
|
|
3467
|
-
});
|
|
3468
|
-
|
|
3469
|
-
// Milestone → Milestone dependency edges (dashed, horizontal)
|
|
3470
|
-
(milestones || []).forEach(m => {
|
|
3471
|
-
const deps = m.dependsOn || [];
|
|
3472
|
-
if (deps.length === 0) return;
|
|
3473
|
-
const mEl = document.querySelector(`[data-node-id="${m.id}"]`);
|
|
3474
|
-
if (!mEl) return;
|
|
3475
|
-
|
|
3476
|
-
deps.forEach(depId => {
|
|
3477
|
-
const depEl = document.querySelector(`[data-node-id="${depId}"]`);
|
|
3478
|
-
if (!depEl) return;
|
|
3479
|
-
|
|
3480
|
-
// Draw from right side of dependency to left side of dependent
|
|
3481
|
-
const containerRect = document.getElementById('canvas-container').getBoundingClientRect();
|
|
3482
|
-
const scrollLeft = document.getElementById('canvas-wrap').scrollLeft;
|
|
3483
|
-
const scrollTop = document.getElementById('canvas-wrap').scrollTop;
|
|
3484
|
-
|
|
3485
|
-
const depRect = depEl.getBoundingClientRect();
|
|
3486
|
-
const mRect = mEl.getBoundingClientRect();
|
|
3487
|
-
|
|
3488
|
-
const x1 = depRect.right - containerRect.left + scrollLeft;
|
|
3489
|
-
const y1 = depRect.top - containerRect.top + scrollTop + depRect.height / 2;
|
|
3490
|
-
const x2 = mRect.left - containerRect.left + scrollLeft;
|
|
3491
|
-
const y2 = mRect.top - containerRect.top + scrollTop + mRect.height / 2;
|
|
3492
|
-
|
|
3493
|
-
const cx = (x1 + x2) / 2;
|
|
3494
|
-
const d = `M ${x1} ${y1} C ${cx} ${y1}, ${cx} ${y2}, ${x2} ${y2}`;
|
|
3495
|
-
const isHighlighted = selectedNodeId === m.id || selectedNodeId === depId;
|
|
3496
|
-
|
|
3497
|
-
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
3498
|
-
path.setAttribute('d', d);
|
|
3499
|
-
path.setAttribute('class', isHighlighted ? 'edge dep-edge highlight' : 'edge dep-edge');
|
|
3500
|
-
fragment.appendChild(path);
|
|
3501
|
-
});
|
|
3502
|
-
});
|
|
3503
|
-
|
|
3504
|
-
$edgesSvg.appendChild(fragment);
|
|
3505
|
-
}
|
|
3506
|
-
|
|
3507
3194
|
// ─── Annotation panel ─────────────────────────────────────────────────────────
|
|
3508
3195
|
|
|
3509
3196
|
/**
|
|
@@ -4147,7 +3834,6 @@ function selectNode(nodeId, type) {
|
|
|
4147
3834
|
if (selectedNodeId === nodeId) {
|
|
4148
3835
|
// Toggle off
|
|
4149
3836
|
selectedNodeId = null;
|
|
4150
|
-
exitFocusMode();
|
|
4151
3837
|
if ($panelEmpty) $panelEmpty.style.display = '';
|
|
4152
3838
|
return;
|
|
4153
3839
|
}
|
|
@@ -4158,16 +3844,6 @@ function selectNode(nodeId, type) {
|
|
|
4158
3844
|
const el = document.querySelector(`[data-node-id="${nodeId}"]`);
|
|
4159
3845
|
if (el) el.classList.add('selected');
|
|
4160
3846
|
|
|
4161
|
-
// If clicking a node already visible in the current focused subtree, skip re-animation
|
|
4162
|
-
const alreadyInFocus = focusNodeId && getFocusSubtree(
|
|
4163
|
-
focusNodeId,
|
|
4164
|
-
document.querySelector(`[data-node-id="${focusNodeId}"]`)?.dataset.nodeType || 'declaration'
|
|
4165
|
-
).has(nodeId);
|
|
4166
|
-
|
|
4167
|
-
if (!alreadyInFocus) {
|
|
4168
|
-
enterFocusMode(nodeId, type);
|
|
4169
|
-
}
|
|
4170
|
-
|
|
4171
3847
|
// Populate panel
|
|
4172
3848
|
let item = null;
|
|
4173
3849
|
if (graphData) {
|
|
@@ -4182,13 +3858,11 @@ function selectNode(nodeId, type) {
|
|
|
4182
3858
|
renderPanelChain(item, type);
|
|
4183
3859
|
renderAnnotationPanel(nodeId, type);
|
|
4184
3860
|
|
|
4185
|
-
//
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
}, 100);
|
|
4191
|
-
}
|
|
3861
|
+
// Auto-scroll to review controls for review-mode flow
|
|
3862
|
+
setTimeout(() => {
|
|
3863
|
+
const reviewEl = document.getElementById('review-actions');
|
|
3864
|
+
if (reviewEl) reviewEl.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
|
3865
|
+
}, 100);
|
|
4192
3866
|
}
|
|
4193
3867
|
|
|
4194
3868
|
/**
|
|
@@ -6531,367 +6205,6 @@ async function cancelActionDerivation() {
|
|
|
6531
6205
|
if (btn) { btn.disabled = false; btn.textContent = 'Plan Actions'; }
|
|
6532
6206
|
}
|
|
6533
6207
|
|
|
6534
|
-
const $focusHint = document.getElementById('focus-hint');
|
|
6535
|
-
const FOCUS_DUR = 380;
|
|
6536
|
-
|
|
6537
|
-
/**
|
|
6538
|
-
* Compute the set of node IDs that belong to a focused subtree.
|
|
6539
|
-
* - declaration: itself + all its milestones + all their actions
|
|
6540
|
-
* - milestone: itself + its declarations + its actions
|
|
6541
|
-
* - action: itself + its parent milestone + that milestone's declaration + all sibling actions
|
|
6542
|
-
* @param {string} nodeId
|
|
6543
|
-
* @param {string} type
|
|
6544
|
-
* @returns {Set<string>}
|
|
6545
|
-
*/
|
|
6546
|
-
function getFocusSubtree(nodeId, type) {
|
|
6547
|
-
if (!graphData) return new Set();
|
|
6548
|
-
const { milestones, actions } = graphData;
|
|
6549
|
-
const visible = new Set();
|
|
6550
|
-
|
|
6551
|
-
if (type === 'declaration') {
|
|
6552
|
-
visible.add(nodeId);
|
|
6553
|
-
milestones.filter(m => (m.realizes || []).includes(nodeId)).forEach(m => {
|
|
6554
|
-
visible.add(m.id);
|
|
6555
|
-
actions.filter(a => (a.causes || []).includes(m.id)).forEach(a => visible.add(a.id));
|
|
6556
|
-
});
|
|
6557
|
-
} else if (type === 'milestone') {
|
|
6558
|
-
visible.add(nodeId);
|
|
6559
|
-
const m = milestones.find(x => x.id === nodeId);
|
|
6560
|
-
if (m) {
|
|
6561
|
-
(m.realizes || []).forEach(dId => visible.add(dId));
|
|
6562
|
-
actions.filter(a => (a.causes || []).includes(nodeId)).forEach(a => visible.add(a.id));
|
|
6563
|
-
}
|
|
6564
|
-
} else if (type === 'action') {
|
|
6565
|
-
visible.add(nodeId);
|
|
6566
|
-
const a = actions.find(x => x.id === nodeId);
|
|
6567
|
-
if (a) {
|
|
6568
|
-
(a.causes || []).forEach(mId => {
|
|
6569
|
-
visible.add(mId);
|
|
6570
|
-
const m = milestones.find(x => x.id === mId);
|
|
6571
|
-
if (m) {
|
|
6572
|
-
// parent declarations
|
|
6573
|
-
(m.realizes || []).forEach(dId => visible.add(dId));
|
|
6574
|
-
// sibling actions (all actions that cause the same milestone)
|
|
6575
|
-
actions.filter(sa => (sa.causes || []).includes(mId)).forEach(sa => visible.add(sa.id));
|
|
6576
|
-
}
|
|
6577
|
-
});
|
|
6578
|
-
}
|
|
6579
|
-
}
|
|
6580
|
-
|
|
6581
|
-
return visible;
|
|
6582
|
-
}
|
|
6583
|
-
|
|
6584
|
-
/** Clear all focus-mode inline styles from a node element. */
|
|
6585
|
-
function clearNodeFocusStyles(el) {
|
|
6586
|
-
el.style.cssText = ''; // wipe all inline styles at once
|
|
6587
|
-
el.classList.remove('focus-exiting', 'focus-active');
|
|
6588
|
-
el.dataset.focusDir = '';
|
|
6589
|
-
}
|
|
6590
|
-
|
|
6591
|
-
/** @type {Array<{el: HTMLElement, rect: DOMRect, dirLeft: boolean}>} */
|
|
6592
|
-
let exitedNodes = [];
|
|
6593
|
-
/** @type {ReturnType<typeof setTimeout> | null} Shared cleanup timer — covers both enter and exit cleanups so each cancels the other */
|
|
6594
|
-
let focusCleanupTimer = null;
|
|
6595
|
-
|
|
6596
|
-
/**
|
|
6597
|
-
* Snapshot getBoundingClientRect for a set of node IDs.
|
|
6598
|
-
* @param {Set<string>} ids
|
|
6599
|
-
* @returns {Map<string, DOMRect>}
|
|
6600
|
-
*/
|
|
6601
|
-
function snapshotRects(ids) {
|
|
6602
|
-
const map = new Map();
|
|
6603
|
-
ids.forEach(id => {
|
|
6604
|
-
const el = document.querySelector(`[data-node-id="${id}"]`);
|
|
6605
|
-
if (el) map.set(id, el.getBoundingClientRect());
|
|
6606
|
-
});
|
|
6607
|
-
return map;
|
|
6608
|
-
}
|
|
6609
|
-
|
|
6610
|
-
/**
|
|
6611
|
-
* Enter focus mode using FLIP:
|
|
6612
|
-
* 1. Snapshot subtree node positions (FIRST)
|
|
6613
|
-
* 2. Remove exiting nodes from flow + overlay them fixed at their original positions
|
|
6614
|
-
* 3. Flex re-centers remaining nodes instantly
|
|
6615
|
-
* 4. Snapshot new subtree positions (LAST)
|
|
6616
|
-
* 5. INVERT: push subtree nodes back to original positions via transform (no transition)
|
|
6617
|
-
* 6. PLAY: animate subtree to new center + animate fixed-overlay exits to slide out
|
|
6618
|
-
*/
|
|
6619
|
-
function enterFocusMode(nodeId, type) {
|
|
6620
|
-
if (!graphData) return;
|
|
6621
|
-
// Always cancel any pending cleanup (enter or exit) before starting a new animation
|
|
6622
|
-
if (focusCleanupTimer) { clearTimeout(focusCleanupTimer); focusCleanupTimer = null; }
|
|
6623
|
-
if (focusNodeId) {
|
|
6624
|
-
// Restore everything cleanly before re-entering
|
|
6625
|
-
exitedNodes.forEach(({ el }) => {
|
|
6626
|
-
el.style.cssText = '';
|
|
6627
|
-
el.classList.remove('focus-exiting', 'focus-active');
|
|
6628
|
-
});
|
|
6629
|
-
document.querySelectorAll('.node.focus-active').forEach(el => el.classList.remove('focus-active'));
|
|
6630
|
-
exitedNodes = [];
|
|
6631
|
-
}
|
|
6632
|
-
|
|
6633
|
-
focusNodeId = nodeId;
|
|
6634
|
-
const subtree = getFocusSubtree(nodeId, type);
|
|
6635
|
-
|
|
6636
|
-
// Determine focus center X for directional exits
|
|
6637
|
-
const focusEl = document.querySelector(`[data-node-id="${nodeId}"]`);
|
|
6638
|
-
if (!focusEl) return;
|
|
6639
|
-
const focusCenterX = focusEl.getBoundingClientRect().left + focusEl.getBoundingClientRect().width / 2;
|
|
6640
|
-
|
|
6641
|
-
// Classify all nodes
|
|
6642
|
-
const subtreeEls = new Map(); // id → el
|
|
6643
|
-
exitedNodes = [];
|
|
6644
|
-
document.querySelectorAll('.node').forEach(el => {
|
|
6645
|
-
const id = el.dataset.nodeId;
|
|
6646
|
-
if (!id) return;
|
|
6647
|
-
el.style.cssText = '';
|
|
6648
|
-
el.classList.remove('focus-exiting', 'focus-active');
|
|
6649
|
-
if (subtree.has(id)) {
|
|
6650
|
-
el.classList.add('focus-active');
|
|
6651
|
-
subtreeEls.set(id, el);
|
|
6652
|
-
} else {
|
|
6653
|
-
const rect = el.getBoundingClientRect();
|
|
6654
|
-
exitedNodes.push({ el, rect, dirLeft: (rect.left + rect.width / 2) < focusCenterX });
|
|
6655
|
-
}
|
|
6656
|
-
});
|
|
6657
|
-
|
|
6658
|
-
// FIRST: snapshot subtree positions before layout change
|
|
6659
|
-
const firstRects = snapshotRects(subtree);
|
|
6660
|
-
|
|
6661
|
-
// Remove exiting nodes from flow (instantly invisible — opacity handled separately)
|
|
6662
|
-
// Pin them fixed at their current viewport positions for the slide-out overlay
|
|
6663
|
-
exitedNodes.forEach(({ el, rect, dirLeft }) => {
|
|
6664
|
-
el.dataset.focusDir = dirLeft ? 'left' : 'right';
|
|
6665
|
-
el.classList.add('focus-exiting');
|
|
6666
|
-
el.style.position = 'fixed';
|
|
6667
|
-
el.style.left = rect.left + 'px';
|
|
6668
|
-
el.style.top = rect.top + 'px';
|
|
6669
|
-
el.style.width = rect.width + 'px';
|
|
6670
|
-
el.style.height = rect.height + 'px';
|
|
6671
|
-
el.style.margin = '0';
|
|
6672
|
-
el.style.zIndex = '15';
|
|
6673
|
-
el.style.pointerEvents = 'none';
|
|
6674
|
-
el.style.opacity = '1';
|
|
6675
|
-
el.style.transform = 'none';
|
|
6676
|
-
});
|
|
6677
|
-
|
|
6678
|
-
// Force reflow: flex now sees only subtree nodes → re-centers them
|
|
6679
|
-
void document.body.offsetWidth;
|
|
6680
|
-
|
|
6681
|
-
// LAST: snapshot new positions
|
|
6682
|
-
const lastRects = snapshotRects(subtree);
|
|
6683
|
-
|
|
6684
|
-
// INVERT: push subtree nodes to appear at their old positions (no transition)
|
|
6685
|
-
subtreeEls.forEach((el, id) => {
|
|
6686
|
-
const first = firstRects.get(id);
|
|
6687
|
-
const last = lastRects.get(id);
|
|
6688
|
-
if (!first || !last) return;
|
|
6689
|
-
const dx = first.left - last.left;
|
|
6690
|
-
const dy = first.top - last.top;
|
|
6691
|
-
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
|
|
6692
|
-
el.style.transition = 'none';
|
|
6693
|
-
el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
6694
|
-
}
|
|
6695
|
-
});
|
|
6696
|
-
|
|
6697
|
-
// Force reflow so invert transforms are painted before we play
|
|
6698
|
-
void document.body.offsetWidth;
|
|
6699
|
-
|
|
6700
|
-
const dur = FOCUS_DUR + 'ms';
|
|
6701
|
-
const easeIn = 'cubic-bezier(0,0,0.2,1)';
|
|
6702
|
-
const easeOut = 'cubic-bezier(0.4,0,1,1)';
|
|
6703
|
-
|
|
6704
|
-
// PLAY: animate subtree nodes to their new centered positions
|
|
6705
|
-
subtreeEls.forEach(el => {
|
|
6706
|
-
el.style.transition = `transform ${dur} ${easeIn}`;
|
|
6707
|
-
el.style.transform = '';
|
|
6708
|
-
});
|
|
6709
|
-
|
|
6710
|
-
// Clear edges immediately (no transition lag) — redraw only after animation settles
|
|
6711
|
-
$edgesSvg.innerHTML = '';
|
|
6712
|
-
$edgesSvg.style.opacity = '0';
|
|
6713
|
-
|
|
6714
|
-
// PLAY: slide + fade exiting nodes out from their fixed positions
|
|
6715
|
-
requestAnimationFrame(() => {
|
|
6716
|
-
exitedNodes.forEach(({ el, dirLeft }) => {
|
|
6717
|
-
el.style.transition = `opacity ${Math.round(FOCUS_DUR * 0.7)}ms ease, transform ${dur} ${easeOut}`;
|
|
6718
|
-
el.style.opacity = '0';
|
|
6719
|
-
el.style.transform = `translateX(${dirLeft ? -130 : 130}%)`;
|
|
6720
|
-
});
|
|
6721
|
-
});
|
|
6722
|
-
|
|
6723
|
-
// After animation: clean up + redraw edges at final positions, then fade back in
|
|
6724
|
-
// Stored in focusCleanupTimer so exitFocusMode can cancel it if user exits early
|
|
6725
|
-
focusCleanupTimer = setTimeout(() => {
|
|
6726
|
-
focusCleanupTimer = null;
|
|
6727
|
-
exitedNodes.forEach(({ el }) => {
|
|
6728
|
-
el.style.cssText = '';
|
|
6729
|
-
el.style.display = 'none';
|
|
6730
|
-
el.classList.remove('focus-exiting');
|
|
6731
|
-
});
|
|
6732
|
-
subtreeEls.forEach(el => { el.style.transition = ''; el.style.transform = ''; });
|
|
6733
|
-
drawEdgesForSubtree(subtree);
|
|
6734
|
-
$edgesSvg.style.opacity = '1';
|
|
6735
|
-
}, FOCUS_DUR + 50);
|
|
6736
|
-
|
|
6737
|
-
$focusHint.classList.add('visible');
|
|
6738
|
-
}
|
|
6739
|
-
|
|
6740
|
-
/**
|
|
6741
|
-
* Exit focus mode — proper reverse FLIP:
|
|
6742
|
-
* 1. Snapshot current visual positions of subtree nodes (may be centered)
|
|
6743
|
-
* 2. Restore ALL nodes to normal flow (clear all inline styles)
|
|
6744
|
-
* 3. Force layout → nodes at natural positions
|
|
6745
|
-
* 4. Snapshot natural positions (LAST)
|
|
6746
|
-
* 5. INVERT: push subtree nodes back to where they were + push returned nodes off-screen
|
|
6747
|
-
* 6. Force reflow
|
|
6748
|
-
* 7. PLAY: animate everything to natural (transform:'')
|
|
6749
|
-
*/
|
|
6750
|
-
function exitFocusMode() {
|
|
6751
|
-
if (!focusNodeId) return;
|
|
6752
|
-
const prevSubtree = getFocusSubtree(
|
|
6753
|
-
focusNodeId,
|
|
6754
|
-
document.querySelector(`[data-node-id="${focusNodeId}"]`)?.dataset.nodeType || 'declaration'
|
|
6755
|
-
);
|
|
6756
|
-
focusNodeId = null;
|
|
6757
|
-
if (focusCleanupTimer) { clearTimeout(focusCleanupTimer); focusCleanupTimer = null; }
|
|
6758
|
-
|
|
6759
|
-
const dur = FOCUS_DUR + 'ms';
|
|
6760
|
-
const easeIn = 'cubic-bezier(0,0,0.2,1)';
|
|
6761
|
-
|
|
6762
|
-
// FIRST: snapshot current visual positions of subtree nodes (before any style changes)
|
|
6763
|
-
const firstRects = snapshotRects(prevSubtree);
|
|
6764
|
-
|
|
6765
|
-
// Capture dirLeft for each exited node, then clear all inline styles on every node
|
|
6766
|
-
const capturedExits = exitedNodes.map(({ el, dirLeft }) => ({ el, dirLeft }));
|
|
6767
|
-
exitedNodes = [];
|
|
6768
|
-
|
|
6769
|
-
document.querySelectorAll('.node').forEach(el => {
|
|
6770
|
-
el.style.cssText = '';
|
|
6771
|
-
el.classList.remove('focus-exiting', 'focus-active');
|
|
6772
|
-
});
|
|
6773
|
-
|
|
6774
|
-
// Force layout: all nodes now at their natural flex positions
|
|
6775
|
-
void document.body.offsetWidth;
|
|
6776
|
-
|
|
6777
|
-
// LAST: snapshot natural positions of subtree nodes
|
|
6778
|
-
const lastRects = snapshotRects(prevSubtree);
|
|
6779
|
-
|
|
6780
|
-
// Build subtree element map
|
|
6781
|
-
const subtreeEls = new Map();
|
|
6782
|
-
prevSubtree.forEach(id => {
|
|
6783
|
-
const el = document.querySelector(`[data-node-id="${id}"]`);
|
|
6784
|
-
if (el) subtreeEls.set(id, el);
|
|
6785
|
-
});
|
|
6786
|
-
|
|
6787
|
-
// INVERT subtree: push nodes back to where they appeared (centered)
|
|
6788
|
-
subtreeEls.forEach((el, id) => {
|
|
6789
|
-
const first = firstRects.get(id);
|
|
6790
|
-
const last = lastRects.get(id);
|
|
6791
|
-
if (!first || !last) return;
|
|
6792
|
-
const dx = first.left - last.left;
|
|
6793
|
-
const dy = first.top - last.top;
|
|
6794
|
-
if (Math.abs(dx) > 0.5 || Math.abs(dy) > 0.5) {
|
|
6795
|
-
el.style.transition = 'none';
|
|
6796
|
-
el.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
6797
|
-
}
|
|
6798
|
-
});
|
|
6799
|
-
|
|
6800
|
-
// INVERT returned nodes: push off-screen so they slide in
|
|
6801
|
-
capturedExits.forEach(({ el, dirLeft }) => {
|
|
6802
|
-
el.style.transition = 'none';
|
|
6803
|
-
el.style.opacity = '0';
|
|
6804
|
-
el.style.transform = `translateX(${dirLeft ? -130 : 130}%)`;
|
|
6805
|
-
});
|
|
6806
|
-
|
|
6807
|
-
// Force reflow to paint inverted state before PLAY
|
|
6808
|
-
void document.body.offsetWidth;
|
|
6809
|
-
|
|
6810
|
-
// Clear edges immediately
|
|
6811
|
-
$edgesSvg.innerHTML = '';
|
|
6812
|
-
$edgesSvg.style.opacity = '0';
|
|
6813
|
-
|
|
6814
|
-
// PLAY: animate all nodes to natural positions (transform:'')
|
|
6815
|
-
requestAnimationFrame(() => {
|
|
6816
|
-
subtreeEls.forEach(el => {
|
|
6817
|
-
el.style.transition = `transform ${dur} ${easeIn}`;
|
|
6818
|
-
el.style.transform = '';
|
|
6819
|
-
});
|
|
6820
|
-
capturedExits.forEach(({ el }) => {
|
|
6821
|
-
el.style.transition = `opacity ${Math.round(FOCUS_DUR * 0.8)}ms ease ${Math.round(FOCUS_DUR * 0.1)}ms, transform ${dur} ${easeIn}`;
|
|
6822
|
-
el.style.opacity = '1';
|
|
6823
|
-
el.style.transform = '';
|
|
6824
|
-
});
|
|
6825
|
-
});
|
|
6826
|
-
|
|
6827
|
-
// Cleanup: remove inline styles, redraw edges at settled positions
|
|
6828
|
-
focusCleanupTimer = setTimeout(() => {
|
|
6829
|
-
document.querySelectorAll('.node').forEach(el => {
|
|
6830
|
-
el.style.transition = '';
|
|
6831
|
-
el.style.transform = '';
|
|
6832
|
-
el.style.opacity = '';
|
|
6833
|
-
});
|
|
6834
|
-
void document.body.offsetWidth;
|
|
6835
|
-
requestAnimationFrame(() => {
|
|
6836
|
-
drawEdges();
|
|
6837
|
-
$edgesSvg.style.opacity = '1';
|
|
6838
|
-
});
|
|
6839
|
-
focusCleanupTimer = null;
|
|
6840
|
-
}, FOCUS_DUR + 80);
|
|
6841
|
-
|
|
6842
|
-
$focusHint.classList.remove('visible');
|
|
6843
|
-
}
|
|
6844
|
-
|
|
6845
|
-
/**
|
|
6846
|
-
* Draw edges but dim those outside the given subtree.
|
|
6847
|
-
* @param {Set<string>} subtree
|
|
6848
|
-
*/
|
|
6849
|
-
function drawEdgesForSubtree(subtree) {
|
|
6850
|
-
if (!graphData) return;
|
|
6851
|
-
const { milestones, actions } = graphData;
|
|
6852
|
-
const container = document.getElementById('canvas-container');
|
|
6853
|
-
|
|
6854
|
-
$edgesSvg.setAttribute('width', String(container.scrollWidth));
|
|
6855
|
-
$edgesSvg.setAttribute('height', String(container.scrollHeight));
|
|
6856
|
-
$edgesSvg.innerHTML = '';
|
|
6857
|
-
|
|
6858
|
-
const fragment = document.createDocumentFragment();
|
|
6859
|
-
|
|
6860
|
-
(milestones || []).forEach(m => {
|
|
6861
|
-
const mEl = document.querySelector(`[data-node-id="${m.id}"]`);
|
|
6862
|
-
if (!mEl) return;
|
|
6863
|
-
const mTop = getTopCenter(mEl);
|
|
6864
|
-
|
|
6865
|
-
(m.realizes || []).forEach(dId => {
|
|
6866
|
-
const dEl = document.querySelector(`[data-node-id="${dId}"]`);
|
|
6867
|
-
if (!dEl) return;
|
|
6868
|
-
const dBot = getBottomCenter(dEl);
|
|
6869
|
-
const inSubtree = subtree.has(m.id) && subtree.has(dId);
|
|
6870
|
-
const path = makePath(curvePath(dBot.x, dBot.y, mTop.x, mTop.y), inSubtree);
|
|
6871
|
-
if (!inSubtree) path.classList.add('focus-dim');
|
|
6872
|
-
fragment.appendChild(path);
|
|
6873
|
-
});
|
|
6874
|
-
});
|
|
6875
|
-
|
|
6876
|
-
(actions || []).forEach(a => {
|
|
6877
|
-
const aEl = document.querySelector(`[data-node-id="${a.id}"]`);
|
|
6878
|
-
if (!aEl) return;
|
|
6879
|
-
const aTop = getTopCenter(aEl);
|
|
6880
|
-
|
|
6881
|
-
(a.causes || []).forEach(mId => {
|
|
6882
|
-
const mEl = document.querySelector(`[data-node-id="${mId}"]`);
|
|
6883
|
-
if (!mEl) return;
|
|
6884
|
-
const mBot = getBottomCenter(mEl);
|
|
6885
|
-
const inSubtree = subtree.has(a.id) && subtree.has(mId);
|
|
6886
|
-
const path = makePath(curvePath(mBot.x, mBot.y, aTop.x, aTop.y), inSubtree);
|
|
6887
|
-
if (!inSubtree) path.classList.add('focus-dim');
|
|
6888
|
-
fragment.appendChild(path);
|
|
6889
|
-
});
|
|
6890
|
-
});
|
|
6891
|
-
|
|
6892
|
-
$edgesSvg.appendChild(fragment);
|
|
6893
|
-
}
|
|
6894
|
-
|
|
6895
6208
|
// ─── Execution pipeline view ──────────────────────────────────────────────────
|
|
6896
6209
|
|
|
6897
6210
|
/**
|
|
@@ -7281,8 +6594,8 @@ function canEnterExecution() {
|
|
|
7281
6594
|
}
|
|
7282
6595
|
|
|
7283
6596
|
/**
|
|
7284
|
-
* Switch between
|
|
7285
|
-
* @param {'
|
|
6597
|
+
* Switch between column browser and execution views.
|
|
6598
|
+
* @param {'columns'|'execution'} mode
|
|
7286
6599
|
*/
|
|
7287
6600
|
function switchView(mode) {
|
|
7288
6601
|
if (mode === 'execution' && !canEnterExecution()) {
|
|
@@ -7290,35 +6603,20 @@ function switchView(mode) {
|
|
|
7290
6603
|
return;
|
|
7291
6604
|
}
|
|
7292
6605
|
viewMode = mode;
|
|
7293
|
-
localStorage.setItem('declare-view-mode', mode);
|
|
7294
6606
|
|
|
7295
6607
|
// Hide all views first
|
|
7296
|
-
$canvasWrap.style.display = 'none';
|
|
7297
6608
|
if ($drillBrowser) $drillBrowser.classList.remove('active');
|
|
7298
6609
|
if ($readinessBanner) $readinessBanner.classList.remove('active');
|
|
7299
6610
|
if ($execView) $execView.classList.remove('active');
|
|
7300
6611
|
document.body.classList.remove('exec-mode');
|
|
7301
|
-
|
|
7302
|
-
|
|
7303
|
-
if (mode === 'dag') {
|
|
7304
|
-
$canvasWrap.style.display = '';
|
|
7305
|
-
document.body.classList.add('dag-mode');
|
|
7306
|
-
clearColumnBrowserKbFocus();
|
|
7307
|
-
if ($viewToggle) $viewToggleLabel.innerHTML = '☰'; // hamburger = list view
|
|
7308
|
-
// Redraw edges since layout changed
|
|
7309
|
-
requestAnimationFrame(() => drawEdges());
|
|
7310
|
-
} else if (mode === 'columns') {
|
|
7311
|
-
// Exit focus mode before switching to columns
|
|
7312
|
-
if (focusNodeId) exitFocusMode();
|
|
6612
|
+
|
|
6613
|
+
if (mode === 'columns') {
|
|
7313
6614
|
if ($drillBrowser) $drillBrowser.classList.add('active');
|
|
7314
|
-
if ($viewToggle) $viewToggleLabel.innerHTML = '⬓'; // graph icon
|
|
7315
6615
|
// Refresh drill browser data
|
|
7316
6616
|
renderDrillView();
|
|
7317
6617
|
} else if (mode === 'execution') {
|
|
7318
|
-
if (focusNodeId) exitFocusMode();
|
|
7319
6618
|
if ($execView) $execView.classList.add('active');
|
|
7320
6619
|
document.body.classList.add('exec-mode');
|
|
7321
|
-
if ($viewToggle) $viewToggleLabel.innerHTML = '☰';
|
|
7322
6620
|
orderConfirmed = false;
|
|
7323
6621
|
preExecWaves = null;
|
|
7324
6622
|
renderPreExecutionView();
|
|
@@ -7339,17 +6637,6 @@ document.addEventListener('keydown', (e) => {
|
|
|
7339
6637
|
}
|
|
7340
6638
|
});
|
|
7341
6639
|
|
|
7342
|
-
// View toggle button — cycles: columns -> dag (execution excluded, entered only via play)
|
|
7343
|
-
if ($viewToggle) {
|
|
7344
|
-
$viewToggle.addEventListener('click', () => {
|
|
7345
|
-
if (viewMode === 'columns') {
|
|
7346
|
-
switchView('dag');
|
|
7347
|
-
} else {
|
|
7348
|
-
switchView('columns');
|
|
7349
|
-
}
|
|
7350
|
-
});
|
|
7351
|
-
}
|
|
7352
|
-
|
|
7353
6640
|
// Execution topbar buttons
|
|
7354
6641
|
if ($execExitBtn) {
|
|
7355
6642
|
$execExitBtn.addEventListener('click', () => switchView('columns'));
|
|
@@ -7487,53 +6774,12 @@ if ($playStopBtn) {
|
|
|
7487
6774
|
$playStopBtn.addEventListener('click', stopPlay);
|
|
7488
6775
|
}
|
|
7489
6776
|
|
|
7490
|
-
// ESC to exit focus mode; arrow keys to navigate between declarations (DAG view only)
|
|
7491
|
-
document.addEventListener('keydown', (e) => {
|
|
7492
|
-
// Skip DAG keyboard handling when column browser is active
|
|
7493
|
-
if (isColumnBrowserActive()) return;
|
|
7494
|
-
|
|
7495
|
-
if (e.key === 'Escape' && focusNodeId) {
|
|
7496
|
-
document.querySelectorAll('.node.selected').forEach(el => el.classList.remove('selected'));
|
|
7497
|
-
selectedNodeId = null;
|
|
7498
|
-
exitFocusMode();
|
|
7499
|
-
if ($panelEmpty) $panelEmpty.style.display = '';
|
|
7500
|
-
}
|
|
7501
|
-
|
|
7502
|
-
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && selectedNodeId && graphData) {
|
|
7503
|
-
const declarations = graphData.declarations;
|
|
7504
|
-
const idx = declarations.findIndex(d => d.id === selectedNodeId);
|
|
7505
|
-
if (idx === -1) return; // selected node is not a declaration
|
|
7506
|
-
const next = e.key === 'ArrowRight'
|
|
7507
|
-
? (idx + 1) % declarations.length
|
|
7508
|
-
: (idx - 1 + declarations.length) % declarations.length;
|
|
7509
|
-
selectNode(declarations[next].id, 'declaration');
|
|
7510
|
-
}
|
|
7511
|
-
});
|
|
7512
|
-
|
|
7513
|
-
// Click on canvas background to exit focus mode
|
|
7514
|
-
document.getElementById('canvas-wrap').addEventListener('click', (e) => {
|
|
7515
|
-
if (!focusNodeId) return;
|
|
7516
|
-
if (!e.target.closest('.node')) {
|
|
7517
|
-
document.querySelectorAll('.node.selected').forEach(el => el.classList.remove('selected'));
|
|
7518
|
-
selectedNodeId = null;
|
|
7519
|
-
exitFocusMode();
|
|
7520
|
-
if ($panelEmpty) $panelEmpty.style.display = '';
|
|
7521
|
-
}
|
|
7522
|
-
});
|
|
7523
6777
|
|
|
7524
6778
|
$overlayRetry.addEventListener('click', () => {
|
|
7525
6779
|
showLoading();
|
|
7526
6780
|
loadData();
|
|
7527
6781
|
});
|
|
7528
6782
|
|
|
7529
|
-
// Redraw edges on window resize or scroll (layout may shift)
|
|
7530
|
-
window.addEventListener('resize', () => {
|
|
7531
|
-
if (graphData) requestAnimationFrame(() => drawEdges());
|
|
7532
|
-
});
|
|
7533
|
-
|
|
7534
|
-
document.getElementById('canvas-wrap').addEventListener('scroll', () => {
|
|
7535
|
-
if (graphData) requestAnimationFrame(() => drawEdges());
|
|
7536
|
-
});
|
|
7537
6783
|
|
|
7538
6784
|
// ─── Confetti ────────────────────────────────────────────────────────────────
|
|
7539
6785
|
// Fires once when all declarations reach a completed state (DONE/KEPT/HONORED).
|
|
@@ -8155,7 +7401,6 @@ function connectSSE() {
|
|
|
8155
7401
|
});
|
|
8156
7402
|
let sseChangeTimer = null;
|
|
8157
7403
|
es.addEventListener('change', () => {
|
|
8158
|
-
if (focusNodeId || focusCleanupTimer) return; // skip during animation
|
|
8159
7404
|
// Debounce rapid SSE change events (e.g. approve writes multiple files)
|
|
8160
7405
|
clearTimeout(sseChangeTimer);
|
|
8161
7406
|
sseChangeTimer = setTimeout(() => {
|