@quolu/lattice 0.12.15 → 0.12.16
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/package.json +1 -1
- package/src/todo-cli.mjs +5 -0
- package/src/todo-gantt-html.mjs +33 -10
package/package.json
CHANGED
package/src/todo-cli.mjs
CHANGED
|
@@ -653,6 +653,10 @@ export async function renderTodoGanttForProject({
|
|
|
653
653
|
const topology = mergedTopology(store);
|
|
654
654
|
const chain = projectTodoChainV1(topology);
|
|
655
655
|
const layout = layoutTodoGantt(store, chain, { scope });
|
|
656
|
+
// When the diagram hides history, the page also carries the full diagram so
|
|
657
|
+
// the reader can bring it back in place. Nothing is hidden under `all`.
|
|
658
|
+
const expandedLayout = layout.scope.folded_task_count === 0
|
|
659
|
+
? null : layoutTodoGantt(store, chain, { scope: 'all' });
|
|
656
660
|
const narrative = await loadNarratives(store, repoRoot);
|
|
657
661
|
const anchorOutcomes = verifyNarrativeAnchors({
|
|
658
662
|
readModel: store,
|
|
@@ -685,6 +689,7 @@ export async function renderTodoGanttForProject({
|
|
|
685
689
|
const rendered = renderTodoGanttHtml({
|
|
686
690
|
readModel: store,
|
|
687
691
|
layout,
|
|
692
|
+
expandedLayout,
|
|
688
693
|
narratives: narrative.narratives,
|
|
689
694
|
anchorOutcomes,
|
|
690
695
|
presentation,
|
package/src/todo-gantt-html.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from './todo-markdown-renderer.mjs';
|
|
7
7
|
import { renderTodoGanttSvg, TODO_GANTT_STATUS_PRESENTATION } from './todo-gantt-svg.mjs';
|
|
8
8
|
|
|
9
|
-
export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.
|
|
9
|
+
export const TODO_GANTT_RENDERER_VERSION = 'lattice.todo_gantt_renderer.v13';
|
|
10
10
|
export const TODO_GANTT_PROSE_MAX_BYTES = 8 * 1024 * 1024;
|
|
11
11
|
export const TODO_GANTT_HTML_MAX_BYTES = 24 * 1024 * 1024;
|
|
12
12
|
|
|
@@ -285,14 +285,21 @@ function renderRightPane(sections, layout, presentation, readModel) {
|
|
|
285
285
|
return `<div class="right-toolbar"><button type="button" data-show-overview>概要</button><button type="button" data-show-selected hidden>選択工程へ戻る</button><button type="button" data-show-task-index>元Markdown全文</button></div><div class="right-content">${overview}<div data-right-panel="details" hidden>${details}</div><section class="task-index" data-right-panel="task-index" hidden><h1>全工程</h1><p>Latticeに登録された全工程を、現在の状態とともに登録順で表示しています。</p>${taskIndex}</section></div>`;
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
-
function renderDiagramLegend(presentation, layout = null) {
|
|
288
|
+
function renderDiagramLegend(presentation, layout = null, expandable = false) {
|
|
289
289
|
const categories = (presentation?.lanes ?? []).map((lane) => `<div class="category-entry"><dt><code>${escapeHtmlText(lane.lane)}</code> — ${escapeHtmlText(lane.name)}</dt><dd>${escapeHtmlText(lane.description)}</dd></div>`).join('');
|
|
290
290
|
const categoryDetails = categories === '' ? '' : `<details class="category-legend"><summary>カテゴリ説明</summary><dl>${categories}</dl></details>`;
|
|
291
291
|
const foldedCount = layout?.scope?.folded_task_count ?? 0;
|
|
292
|
+
// The badge says what is missing from the diagram, so it is also the control
|
|
293
|
+
// that brings it back — a reader who notices the count is exactly the reader
|
|
294
|
+
// who wants to see it.
|
|
292
295
|
const foldChip = foldedCount === 0 ? ''
|
|
293
|
-
:
|
|
296
|
+
: expandable
|
|
297
|
+
? `<button type="button" class="fold-chip" data-toggle-expanded aria-expanded="false"><span data-toggle-label data-collapsed-label="完走済み ${foldedCount}件を非表示(押すと表示)" data-expanded-label="完走済み ${foldedCount}件を表示中(押すと非表示)">完走済み ${foldedCount}件を非表示(押すと表示)</span></button>`
|
|
298
|
+
: `<span class="fold-chip">完走済み ${foldedCount}件を非表示</span>`;
|
|
294
299
|
const foldNote = foldedCount === 0 ? ''
|
|
295
|
-
:
|
|
300
|
+
: expandable
|
|
301
|
+
? '<p class="fold-note">後続に作業中・未着手が残っていない完了工程は図から外しています。生きた工程とその直接の前提工程は必ず描きます。上のバッジを押すと外した工程も含めて描きます。総数・進捗・最長依存鎖は外す前の全工程で数えています。</p>'
|
|
302
|
+
: '<p class="fold-note">後続に作業中・未着手が残っていない完了工程は図から外しています。生きた工程とその直接の前提工程は必ず描きます。外した工程は右の「全工程」から辿れ、図に出すには <code>lattice todo gantt --scope all</code> を実行してください。総数・進捗・最長依存鎖は外す前の全工程で数えています。</p>';
|
|
296
303
|
return `<div class="diagram-legend" aria-label="工程図の凡例"><span>${statusMarkup('pending', ' 未着手')}</span><span>${statusMarkup('in-progress', ' 作業中')}</span><span>${statusMarkup('done', ' 完了')}</span><span>${statusMarkup('blocked', ' ブロック中')}</span><span>破線枠: ready frontier(同時dispatch推奨)</span><span>太線: 構造上の最長依存鎖</span><span>半円: 非接触の線交差</span><span>黒丸: 論理上の合流</span>${foldChip}${categoryDetails}${foldNote}<p>縦方向は時間ではなく、登録済み依存関係による工程段階です。ready frontierは全件同時dispatchが既定です。未登録の資源・host制約によりsubsetだけを選ぶ場合は理由を記録します。構造上の最長依存鎖は各工程を同じ重みとして数え、実時間・工数・納期を表しません。</p></div>`;
|
|
297
304
|
}
|
|
298
305
|
|
|
@@ -356,7 +363,10 @@ body{display:grid;grid-template-rows:minmax(0,1fr);height:100vh;margin:0;backgro
|
|
|
356
363
|
.todo-node .node-title{fill:var(--text-primary);font-size:13.5px;font-weight:400}
|
|
357
364
|
.todo-node .node-title-line{font-size:13.5px;font-weight:400}
|
|
358
365
|
.todo-node .status-mark{fill:var(--text-secondary);font-size:13.5px;font-weight:400}
|
|
359
|
-
.fold-chip{padding:2px 8px;border:1px solid var(--border);border-radius:9999px;background:var(--surface-2);color:var(--text-primary);font
|
|
366
|
+
.fold-chip{padding:2px 8px;border:1px solid var(--border);border-radius:9999px;background:var(--surface-2);color:var(--text-primary);font:650 13.5px/1.6 system-ui,-apple-system,"Hiragino Sans","Yu Gothic UI",sans-serif}
|
|
367
|
+
button.fold-chip{cursor:pointer}button.fold-chip:focus-visible{outline:2px solid var(--text-primary);outline-offset:2px}
|
|
368
|
+
button.fold-chip[aria-expanded="true"]{border-color:var(--text-primary)}
|
|
369
|
+
[data-diagram][hidden]{display:none}
|
|
360
370
|
.fold-note{flex:1 0 100%;margin:4px 0 0;color:var(--text-secondary);font-weight:400}
|
|
361
371
|
.task-index-folded{margin-top:8px}
|
|
362
372
|
.task-index-folded>summary,.phase-settled>summary{cursor:pointer;padding:6px 0;color:var(--text-secondary);font-weight:600}
|
|
@@ -398,18 +408,23 @@ const CONTROLLER = `
|
|
|
398
408
|
const nodes=[...root.querySelectorAll('[data-node-key]')];
|
|
399
409
|
const edges=[...root.querySelectorAll('[data-edge-id]')];
|
|
400
410
|
const laneChips=[...root.querySelectorAll('.summary-lane[data-lane-key]')];
|
|
401
|
-
const
|
|
411
|
+
const diagrams=[...root.querySelectorAll('[data-diagram]')];
|
|
412
|
+
const expandToggle=root.querySelector('[data-toggle-expanded]');
|
|
413
|
+
const toggleLabel=root.querySelector('[data-toggle-label]');
|
|
402
414
|
const scroller=root.querySelector('[data-diagram-scroll]');
|
|
403
415
|
const zoomOutput=root.querySelector('[data-zoom-output]');
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
let
|
|
416
|
+
let svg=root.querySelector('[data-diagram]:not([hidden]) [data-gantt-svg]')??root.querySelector('[data-gantt-svg]');
|
|
417
|
+
let baseWidth=Number(svg?.dataset.svgWidth??0);
|
|
418
|
+
let baseHeight=Number(svg?.dataset.svgHeight??0);
|
|
419
|
+
let zoom=1;let activeLaneKey=null;let selectedKey=null;let resizePointerId=null;let expanded=false;
|
|
407
420
|
const stacked=()=>window.matchMedia('(max-width:900px)').matches;
|
|
408
421
|
const setSplit=(clientX)=>{if(!shell||stacked())return;const bounds=shell.getBoundingClientRect();if(bounds.width<=0)return;const percent=Math.max(30,Math.min(75,(clientX-bounds.left)/bounds.width*100));shell.style.setProperty('--split',percent+'%');};
|
|
409
422
|
const finishResize=(event)=>{if(event.pointerId!==resizePointerId)return;if(divider?.hasPointerCapture(event.pointerId))divider.releasePointerCapture(event.pointerId);resizePointerId=null;};
|
|
410
423
|
const setZoom=(value,minimum=.2)=>{if(!svg||!Number.isFinite(value)||value<=0)return;zoom=Math.max(minimum,Math.min(2,value));svg.setAttribute('width',String(Math.max(1,Math.round(baseWidth*zoom))));svg.setAttribute('height',String(Math.max(1,Math.round(baseHeight*zoom))));if(zoomOutput){const percent=zoom>=.1?String(Math.round(zoom*100)):String(Number((zoom*100).toFixed(1)));zoomOutput.textContent=percent+'%';}};
|
|
411
424
|
const applyLane=(key)=>{activeLaneKey=key;for(const chip of laneChips)chip.setAttribute('aria-pressed',String(chip.dataset.laneKey===key));for(const node of nodes)node.classList.toggle('lane-dimmed',key!==null&&node.dataset.laneKey!==key);for(const edge of edges)edge.classList.toggle('lane-dimmed',key!==null&&edge.dataset.fromLaneKey!==key&&edge.dataset.toLaneKey!==key);};
|
|
412
425
|
const toggleLane=(key)=>applyLane(activeLaneKey===key?null:key);
|
|
426
|
+
// Both diagrams ship in the page; the badge picks which one is on screen.
|
|
427
|
+
const setExpanded=(next)=>{if(diagrams.length<2)return;expanded=next;for(const diagram of diagrams)diagram.hidden=(diagram.dataset.diagram==='expanded')!==expanded;svg=diagrams.find(diagram=>!diagram.hidden)?.querySelector('[data-gantt-svg]')??svg;baseWidth=Number(svg?.dataset.svgWidth??0);baseHeight=Number(svg?.dataset.svgHeight??0);if(expandToggle){expandToggle.setAttribute('aria-expanded',String(expanded));}if(toggleLabel){toggleLabel.textContent=expanded?toggleLabel.dataset.expandedLabel:toggleLabel.dataset.collapsedLabel;}setZoom(zoom);applyLane(activeLaneKey);syncSelection();scroller?.scrollTo(0,0);};
|
|
413
428
|
const showPanel=(name)=>{if(overviewPanel)overviewPanel.hidden=name!=='overview';if(detailsPanel)detailsPanel.hidden=name!=='details';if(taskIndexPanel)taskIndexPanel.hidden=name!=='task-index';if(selectedReturnButton)selectedReturnButton.hidden=name!=='task-index'||selectedKey===null;root.dataset.viewState=name;};
|
|
414
429
|
const syncSelection=()=>{for(const detail of detailPanels)detail.hidden=detail.dataset.detailKey!==selectedKey;for(const node of nodes){const selected=node.dataset.nodeKey===selectedKey;node.setAttribute('aria-selected',String(selected));node.classList.toggle('selected-node',selected);}for(const edge of edges){const selected=selectedKey!==null;edge.classList.toggle('selected-incident-edge',selected&&(edge.dataset.fromNodeKey===selectedKey||edge.dataset.toNodeKey===selectedKey));}};
|
|
415
430
|
const showOverview=()=>{selectedKey=null;syncSelection();showPanel('overview');};
|
|
@@ -420,6 +435,7 @@ const CONTROLLER = `
|
|
|
420
435
|
const overviewButton=event.target.closest('[data-show-overview]');if(overviewButton&&root.contains(overviewButton)){showOverview();return;}
|
|
421
436
|
const selectedButton=event.target.closest('[data-show-selected]');if(selectedButton&&root.contains(selectedButton)){showSelected();return;}
|
|
422
437
|
const taskIndexButton=event.target.closest('[data-show-task-index]');if(taskIndexButton&&root.contains(taskIndexButton)){showTaskIndex();return;}
|
|
438
|
+
const expandButton=event.target.closest('[data-toggle-expanded]');if(expandButton&&root.contains(expandButton)){setExpanded(!expanded);return;}
|
|
423
439
|
const selectButton=event.target.closest('[data-select-node-key]');if(selectButton&&root.contains(selectButton)){select(selectButton.dataset.selectNodeKey);return;}
|
|
424
440
|
const zoomButton=event.target.closest('[data-zoom-action]');if(zoomButton&&root.contains(zoomButton)){const action=zoomButton.dataset.zoomAction;if(action==='in')setZoom(zoom<1&&zoom*1.25>=1?1:zoom*1.25,.001);else if(action==='out')setZoom(zoom>1&&zoom/1.25<=1?1:zoom/1.25,.001);else if(action==='reset')setZoom(1);else if(action==='fit'&&scroller){setZoom(Math.min(1,(scroller.clientWidth-16)/baseWidth),.001);scroller.scrollTo(0,0);}return;}
|
|
425
441
|
const laneChip=event.target.closest('.summary-lane[data-lane-key]');if(laneChip&&root.contains(laneChip)){toggleLane(laneChip.dataset.laneKey);return;}
|
|
@@ -436,6 +452,7 @@ const CONTROLLER = `
|
|
|
436
452
|
|
|
437
453
|
export function renderTodoGanttHtml({
|
|
438
454
|
readModel, layout, narratives = [], anchorOutcomes = [], presentation = null, metadata = {},
|
|
455
|
+
expandedLayout = null,
|
|
439
456
|
}) {
|
|
440
457
|
if (readModel?.schema !== 'lattice.todo_store_read.v1' || !Array.isArray(readModel.members)) {
|
|
441
458
|
throw new TypeError('readModel must be lattice.todo_store_read.v1');
|
|
@@ -450,13 +467,19 @@ export function renderTodoGanttHtml({
|
|
|
450
467
|
const normalized = normalizeSections(readModel, narratives, anchorOutcomes);
|
|
451
468
|
const displayName = projectDisplayName(readModel, metadata);
|
|
452
469
|
const svg = renderTodoGanttSvg(layout, { presentation });
|
|
470
|
+
// The expanded diagram travels with the page so the badge can bring the
|
|
471
|
+
// history back without a round trip. A file:// artifact has nowhere to ask.
|
|
472
|
+
const expandedSvg = expandedLayout === null ? '' : renderTodoGanttSvg(expandedLayout, { presentation });
|
|
473
|
+
const diagrams = expandedSvg === ''
|
|
474
|
+
? `<div data-diagram="live">${svg}</div>`
|
|
475
|
+
: `<div data-diagram="live">${svg}</div><div data-diagram="expanded" hidden>${expandedSvg}</div>`;
|
|
453
476
|
const rightPane = renderRightPane(normalized.sections, layout, presentation, readModel);
|
|
454
477
|
const staticData = serializeJsonForScript({
|
|
455
478
|
renderer_version: TODO_GANTT_RENDERER_VERSION,
|
|
456
479
|
metadata,
|
|
457
480
|
presentation,
|
|
458
481
|
});
|
|
459
|
-
const html = `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lattice — ${escapeHtmlText(displayName)} 依存工程図</title><style>${CSS}</style></head><body data-gantt-root data-view-state="overview"><main class="shell"><section class="gantt-pane" aria-label="${escapeHtmlAttribute(displayName)} 依存工程図"><div class="diagram-toolbar" role="group" aria-label="図のズーム"><strong class="project-heading">${escapeHtmlText(displayName)} 依存工程図</strong><button type="button" data-zoom-action="out" aria-label="縮小">−</button><button type="button" data-zoom-action="reset">等倍</button><button type="button" data-zoom-action="in" aria-label="拡大">+</button><button type="button" data-zoom-action="fit">全体表示</button><output class="zoom-readout" data-zoom-output aria-live="polite">100%</output><span class="diagram-note">縦=依存段階(時間ではない)</span></div>${renderDiagramLegend(presentation, layout)}<div class="diagram-scroll" data-diagram-scroll tabindex="0" aria-label="縦方向を主にスクロール可能な依存工程図">${
|
|
482
|
+
const html = `<!doctype html><html lang="ja"><head><meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'"><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Lattice — ${escapeHtmlText(displayName)} 依存工程図</title><style>${CSS}</style></head><body data-gantt-root data-view-state="overview"><main class="shell"><section class="gantt-pane" aria-label="${escapeHtmlAttribute(displayName)} 依存工程図"><div class="diagram-toolbar" role="group" aria-label="図のズーム"><strong class="project-heading">${escapeHtmlText(displayName)} 依存工程図</strong><button type="button" data-zoom-action="out" aria-label="縮小">−</button><button type="button" data-zoom-action="reset">等倍</button><button type="button" data-zoom-action="in" aria-label="拡大">+</button><button type="button" data-zoom-action="fit">全体表示</button><output class="zoom-readout" data-zoom-output aria-live="polite">100%</output><span class="diagram-note">縦=依存段階(時間ではない)</span></div>${renderDiagramLegend(presentation, layout, expandedSvg !== '')}<div class="diagram-scroll" data-diagram-scroll tabindex="0" aria-label="縦方向を主にスクロール可能な依存工程図">${diagrams}</div></section><div class="pane-divider" data-pane-divider aria-hidden="true"></div><aside class="narrative-pane" aria-label="選択工程の詳細と全工程一覧">${rightPane}</aside></main><script type="application/json" id="todo-gantt-data">${staticData}</script><script>${CONTROLLER}</script></body></html>`;
|
|
460
483
|
const htmlBytes = Buffer.byteLength(html, 'utf8');
|
|
461
484
|
if (htmlBytes > TODO_GANTT_HTML_MAX_BYTES) {
|
|
462
485
|
throw new TodoGanttRenderError('TODO_SCALE_EXCEEDED', 'todo gantt HTML limit exceeded', {
|