@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@quolu/lattice",
3
- "version": "0.12.15",
3
+ "version": "0.12.16",
4
4
  "description": "Lattice — phase-aware TODO graph compiler and conflict-aware orchestration runtime",
5
5
  "type": "module",
6
6
  "license": "MIT",
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,
@@ -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.v12';
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
- : `<span class="fold-chip">完走済み ${foldedCount}件を非表示</span>`;
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
- : '<p class="fold-note">後続に作業中・未着手が残っていない完了工程は図から外しています。生きた工程とその直接の前提工程は必ず描きます。外した工程は右の「全工程」から辿れ、図に出すには <code>lattice todo gantt --scope all</code> を実行してください。総数・進捗・最長依存鎖は外す前の全工程で数えています。</p>';
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-weight:650}
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 svg=root.querySelector('[data-gantt-svg]');
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
- const baseWidth=Number(svg?.dataset.svgWidth??0);
405
- const baseHeight=Number(svg?.dataset.svgHeight??0);
406
- let zoom=1;let activeLaneKey=null;let selectedKey=null;let resizePointerId=null;
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="縦方向を主にスクロール可能な依存工程図">${svg}</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>`;
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', {