@tienne/gestalt 0.91.0 → 0.92.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/dist/package.json +1 -1
  2. package/dist/plugin/skills/architecture/SKILL.md +15 -1
  3. package/dist/schemas/architecture-ir.schema.json +20 -0
  4. package/dist/src/architecture/html-client.d.ts +3 -0
  5. package/dist/src/architecture/html-client.d.ts.map +1 -1
  6. package/dist/src/architecture/html-client.js +192 -11
  7. package/dist/src/architecture/html-client.js.map +1 -1
  8. package/dist/src/architecture/html-renderer.d.ts.map +1 -1
  9. package/dist/src/architecture/html-renderer.js +219 -8
  10. package/dist/src/architecture/html-renderer.js.map +1 -1
  11. package/dist/src/architecture/html-theme.d.ts +2 -0
  12. package/dist/src/architecture/html-theme.d.ts.map +1 -1
  13. package/dist/src/architecture/html-theme.js +60 -0
  14. package/dist/src/architecture/html-theme.js.map +1 -1
  15. package/dist/src/architecture/ir-schema.d.ts +66 -26
  16. package/dist/src/architecture/ir-schema.d.ts.map +1 -1
  17. package/dist/src/architecture/ir-schema.js +9 -0
  18. package/dist/src/architecture/ir-schema.js.map +1 -1
  19. package/dist/src/architecture/merge.d.ts.map +1 -1
  20. package/dist/src/architecture/merge.js +9 -0
  21. package/dist/src/architecture/merge.js.map +1 -1
  22. package/dist/src/architecture/sequence-views.d.ts +155 -0
  23. package/dist/src/architecture/sequence-views.d.ts.map +1 -0
  24. package/dist/src/architecture/sequence-views.js +407 -0
  25. package/dist/src/architecture/sequence-views.js.map +1 -0
  26. package/dist/src/architecture/types.d.ts +13 -0
  27. package/dist/src/architecture/types.d.ts.map +1 -1
  28. package/dist/src/architecture/validator.d.ts +2 -1
  29. package/dist/src/architecture/validator.d.ts.map +1 -1
  30. package/dist/src/architecture/validator.js +84 -0
  31. package/dist/src/architecture/validator.js.map +1 -1
  32. package/package.json +1 -1
  33. package/plugin/.codex-plugin/plugin.json +1 -1
  34. package/plugin/.mcp.json +1 -1
  35. package/plugin/mcp.json +1 -1
  36. package/plugin/skills/architecture/SKILL.md +15 -1
  37. package/schemas/architecture-ir.schema.json +20 -0
@@ -1,7 +1,7 @@
1
1
  import { BAND_LINE_X, BAND_LINE_Y, BAND_TITLE_HALF, BAND_TITLE_X, CANVAS_PAD_TOP, CANVAS_PAD_X, LANE_INSET_Y, routeCanvas, } from './canvas-geometry.js';
2
2
  import { flowCountByService, ROOT_LEVEL_ID } from './drilldown.js';
3
3
  import { renderClientScript, THEME_BOOT_SCRIPT } from './html-client.js';
4
- import { datastoreEngine, iconUse, PRODUCT_COLORS, renderCss, renderIconSprite, VIEW_CSS, DOC_CSS, } from './html-theme.js';
4
+ import { datastoreEngine, iconUse, PRODUCT_COLORS, renderCss, renderIconSprite, VIEW_CSS, DOC_CSS, SEQ_VIEW_CSS, } from './html-theme.js';
5
5
  import { EDGE_KIND_ABOUT, LANE_ABOUT, LANE_TITLES, NODE_KIND_ABOUT, STAGE_LANE_ABOUT, flowBadgeText, MICRO_HOST_SHORT, NODE_KIND_SHORT, PLATFORM_CHIP_TEXT, PLATFORM_NAME, WEB_HOSTING_CHIP_TEXT, WEB_HOSTING_NAME, platformChipText, platformName, } from './kind-text.js';
6
6
  import { compareEnvironment, FLAT_BACKWARD_EDGE_KINDS, textUnits, } from './layout.js';
7
7
  import { indexMicroApps } from './micro-app.js';
@@ -10,6 +10,7 @@ import { KNOWLEDGE_COVERAGE_KINDS, KNOWLEDGE_DOC_KINDS } from './packs/knowledge
10
10
  import { computeCompareLayout } from './compare-layout.js';
11
11
  import { computeDataflowLayout } from './dataflow-layout.js';
12
12
  import { computeSequenceLayout } from './sequence-layout.js';
13
+ import { clipText, computePhaseBoard, computeWalkLayout, phaseMetaText, resolveSequencePhases, sequenceCallDepths, } from './sequence-views.js';
13
14
  import { computeServiceFacts } from './service-facts.js';
14
15
  import { chipTextOverride, displayKindOf, hasCardDescription } from './types.js';
15
16
  import { maskSharedText, redactForSharing } from './validator.js';
@@ -812,6 +813,206 @@ function stickyHeadBar(heads, width) {
812
813
  const h = heads.reduce((m, b) => Math.max(m, b.height), 0);
813
814
  return `<div class="seq-sticky" style="top:${CANVAS_PAD_TOP - STICKY_PAD}px;width:${width}px;height:${h + STICKY_PAD * 2}px"></div>`;
814
815
  }
816
+ /** 순서도 메시지의 읽기 이름. 순서도 보기와 같은 꼴이다 */
817
+ function sequenceMessageName(n, m, nameOf) {
818
+ return (`${n}. ${nameOf(m.from)} → ${nameOf(m.to)}: ${m.label}` +
819
+ (m.reply ? ', 응답' : '') +
820
+ (m.lineStyle === 'dashed' ? ', 문서로만 확인' : ''));
821
+ }
822
+ /**
823
+ * 순서도 옆에 두는 두 보기, 단계별 카드와 따라가기. 순서도 섹션은 그대로 두고 형제 섹션으로 붙인다.
824
+ * data-level-id가 없어 레벨 목록에 안 잡히고 보기 단추로만 바뀐다
825
+ */
826
+ function renderSequenceAlts(levelId, projection, layout, drawn, nodeById, services, whereOf, edges) {
827
+ const nameOf = (id) => {
828
+ const n = nodeById.get(id);
829
+ return n ? nodeName(n) : id;
830
+ };
831
+ const order = layout.heads.map((h) => h.id);
832
+ const phases = resolveSequencePhases(projection, drawn, order, nameOf);
833
+ if (phases.length === 0)
834
+ return undefined;
835
+ const messages = projection.messages.filter((m) => drawn.has(m.id));
836
+ const numberById = new Map(layout.messages.map((b) => [b.id, b.n]));
837
+ const numberOf = (id) => numberById.get(id) ?? 0;
838
+ const blockText = new Map((projection.blocks ?? []).map((b) => [b.id, `${SEQUENCE_BLOCK_TEXT[b.kind]} [${b.label}]`]));
839
+ const of = escapeHtml(levelId);
840
+ const cards = renderPhaseCards(of, projection, phases, numberOf, nodeById, nameOf);
841
+ const walk = renderWalk(of, projection, phases, messages, layout, nodeById, services, whereOf, edges, numberOf);
842
+ const phaseOf = new Map(phases.flatMap((ph) => ph.messageIds.map((id) => [id, ph])));
843
+ const messageById = new Map(messages.map((m) => [m.id, m]));
844
+ const steps = phases
845
+ .map((ph) => {
846
+ const items = ph.messageIds
847
+ .map((id) => {
848
+ const m = messageById.get(id);
849
+ const say = `${m.reply ? '↩ ' : ''}${m.label}`;
850
+ const block = m.block !== undefined
851
+ ? `${blockText.get(m.block) ?? ''}${m.branch !== undefined ? `, ${m.branch}` : ''}`
852
+ : '';
853
+ return (`<li><button type="button" data-n="${numberOf(id)}" data-phase="${escapeHtml(phaseOf.get(id).label)}" ` +
854
+ `data-say="${escapeHtml(say)}" data-block="${escapeHtml(block)}">${numberOf(id)}. ${escapeHtml(say)}</button></li>`);
855
+ })
856
+ .join('');
857
+ return `<li class="ss-phase"><h4>${escapeHtml(ph.label)}</h4><ol>${items}</ol></li>`;
858
+ })
859
+ .join('');
860
+ return {
861
+ sections: `${cards}\n${walk}`,
862
+ steps: `<ol class="ss-list" data-seq-of="${of}" hidden>${steps}</ol>`,
863
+ };
864
+ }
865
+ function seqAltSection(levelIdAttr, mode, name, projection, width, height, body) {
866
+ return (`<section class="level view-level seq-alt seq-${mode}" data-seq-of="${levelIdAttr}" data-seq-mode="${mode}" ` +
867
+ `aria-label="${escapeHtml(`${projection.title} ${name}`)}" data-w="${width}" data-h="${height}" ` +
868
+ `style="width:${width}px;height:${height}px" hidden>` +
869
+ `<p class="view-q" style="left:${CANVAS_PAD_X}px">${escapeHtml(projection.question)}</p>${body}</section>`);
870
+ }
871
+ const SEQ_ALT_BOTTOM = 32;
872
+ const MINI_ARROW = 6;
873
+ /** 단계별 카드. 카드마다 그 구간 참여자만 가진 작은 순서도를 그리고 카드 사이에 화살표를 둔다 */
874
+ function renderPhaseCards(levelIdAttr, projection, phases, numberOf, nodeById, nameOf) {
875
+ const board = computePhaseBoard(phases, projection, numberOf);
876
+ const px = (x) => round2(x + CANVAS_PAD_X);
877
+ const py = (y) => round2(y + CANVAS_PAD_TOP);
878
+ const width = round2(board.width + CANVAS_PAD_X * 2);
879
+ const height = round2(board.height + CANVAS_PAD_TOP + SEQ_ALT_BOTTOM);
880
+ const phaseById = new Map(phases.map((ph) => [ph.id, ph]));
881
+ const messageById = new Map(projection.messages.map((m) => [m.id, m]));
882
+ const cards = board.cards
883
+ .map((card) => {
884
+ const ph = phaseById.get(card.phaseId);
885
+ const title = `${card.index}. ${ph.label}`;
886
+ return (`<div class="seq-phase" role="group" aria-label="${escapeHtml(title)}" ` +
887
+ `style="left:${px(card.x)}px;top:${py(card.y)}px;width:${card.width}px;height:${card.height}px">` +
888
+ `<h3 title="${escapeHtml(title)}">${escapeHtml(title)}</h3><p class="sp-meta">${phaseMetaText(ph)}</p>` +
889
+ miniSequenceSvg(card, projection, messageById, nodeById, nameOf) +
890
+ `</div>`);
891
+ })
892
+ .join('');
893
+ const arrows = board.arrows
894
+ .map((a) => `<span class="sp-arrow" aria-hidden="true" style="left:${round2(px(a.x) - 18)}px;top:${round2(py(a.y) - 14)}px">→</span>`)
895
+ .join('');
896
+ return seqAltSection(levelIdAttr, 'cards', '단계별 카드', projection, width, height, cards + arrows);
897
+ }
898
+ function miniSequenceSvg(card, projection, messageById, nodeById, nameOf) {
899
+ const s = card.svg;
900
+ const blocks = s.blocks
901
+ .map((b) => {
902
+ const tag = SEQUENCE_BLOCK_TEXT[b.kind];
903
+ const tagW = textUnits(tag) * 6 + 12;
904
+ const room = Math.max(0, Math.floor((b.width - tagW - 20) / 6) - 2);
905
+ const branches = b.branches
906
+ .map((br) => `<line class="b-branch" x1="${b.x}" x2="${round2(b.x + b.width)}" y1="${br.y}" y2="${br.y}"/>` +
907
+ (br.label !== ''
908
+ ? `<text class="b-label" x="${b.x + 8}" y="${round2(br.y + 11)}">[${escapeHtml(clipText(br.label, room))}]</text>`
909
+ : ''))
910
+ .join('');
911
+ return (`<g class="seq-block b-${b.kind}"><rect x="${b.x}" y="${b.y}" width="${b.width}" height="${b.height}" rx="6"/>` +
912
+ `<path class="b-tab" d="M${b.x} ${round2(b.y + 14)}H${b.x + tagW}L${b.x + tagW + 6} ${round2(b.y + 8)}V${b.y}"/>` +
913
+ `<text class="b-kind" x="${b.x + 6}" y="${round2(b.y + 11)}">${tag}</text>` +
914
+ `<text class="b-label" x="${b.x + tagW + 12}" y="${round2(b.y + 11)}"><title>[${escapeHtml(b.label)}]</title>[${escapeHtml(clipText(b.label, room))}]</text>` +
915
+ `${branches}</g>`);
916
+ })
917
+ .join('');
918
+ const heads = s.heads
919
+ .map((h) => {
920
+ const n = nodeById.get(h.id);
921
+ const name = nameOf(h.id);
922
+ return (`<line class="sp-life" x1="${h.cx}" x2="${h.cx}" y1="${s.lifeline.y1}" y2="${s.lifeline.y2}"/>` +
923
+ `<g class="sp-head${n ? ` k-${displayKindOf(n)}` : ''}"><title>${escapeHtml(name)}</title>` +
924
+ `<rect x="${h.x}" y="${h.y}" width="${h.width}" height="${h.height}" rx="7"/>` +
925
+ `<text x="${h.cx}" y="${round2(h.y + h.height / 2 + 4)}" text-anchor="middle">${escapeHtml(clipText(name, 17))}</text></g>`);
926
+ })
927
+ .join('');
928
+ const links = s.messages
929
+ .map((box) => {
930
+ const m = messageById.get(box.id);
931
+ const dash = m.lineStyle === 'dashed' ? ` stroke-dasharray="${DASHED_PATTERN}"` : '';
932
+ const y = box.y;
933
+ let d;
934
+ let tip;
935
+ let labelX;
936
+ let labelY;
937
+ if (box.self) {
938
+ const x = box.x1;
939
+ const yb = round2(y + 4);
940
+ d = `M${x} ${round2(y - 6)}H${round2(x + 18)}V${yb}H${round2(x + 2)}`;
941
+ tip = [
942
+ `${round2(x + 2)},${yb}`,
943
+ `${round2(x + 2 + MINI_ARROW)},${round2(yb - MINI_ARROW / 2)}`,
944
+ `${round2(x + 2 + MINI_ARROW)},${round2(yb + MINI_ARROW / 2)}`,
945
+ ];
946
+ labelX = round2(x + 24);
947
+ labelY = round2(y + 2);
948
+ }
949
+ else {
950
+ const back = round2(box.x2 - (box.x2 > box.x1 ? 1 : -1) * MINI_ARROW);
951
+ d = `M${box.x1} ${y}H${box.x2}`;
952
+ tip = [
953
+ `${box.x2},${y}`,
954
+ `${back},${round2(y - MINI_ARROW / 2)}`,
955
+ `${back},${round2(y + MINI_ARROW / 2)}`,
956
+ ];
957
+ labelX = round2(Math.min(box.x1, box.x2) + 6);
958
+ labelY = round2(y - 4);
959
+ }
960
+ const name = sequenceMessageName(box.n, m, nameOf);
961
+ const head = m.reply
962
+ ? `<polyline class="tip open" points="${tip[1]} ${tip[0]} ${tip[2]}"/>`
963
+ : `<polygon class="tip" points="${tip.join(' ')}"/>`;
964
+ return (`<g class="link seq-m${m.reply ? ' reply' : ''}" data-from="${escapeHtml(m.from)}" data-to="${escapeHtml(m.to)}" ` +
965
+ `data-message-id="${escapeHtml(m.id)}" tabindex="0" role="button" aria-label="${escapeHtml(name)}">` +
966
+ `<title>${escapeHtml(name)}</title><path class="hit" d="${d}" stroke-width="10"/>` +
967
+ `<path class="edge" d="${d}" stroke-width="1.2"${dash}/>${head}` +
968
+ `<text class="m-label" x="${labelX}" y="${labelY}"><tspan class="m-n">${box.n}.</tspan> ${escapeHtml(clipText(m.label, box.labelUnits))}</text></g>`);
969
+ })
970
+ .join('');
971
+ return (`<svg class="sp-seq" xmlns="http://www.w3.org/2000/svg" width="${s.width}" height="${s.height}" viewBox="0 0 ${s.width} ${s.height}" ` +
972
+ `role="group" aria-label="${escapeHtml(`${projection.title} 메시지`)}">${blocks}${heads}${links}</svg>`);
973
+ }
974
+ /** 따라가기. 순서도 참여자만으로 만든 작은 지도 위에 단계마다 화살표 하나를 켠다. 어느 화살표를 켤지는 클라이언트가 정한다 */
975
+ function renderWalk(levelIdAttr, projection, phases, messages, layout, nodeById, services, whereOf, edges, numberOf) {
976
+ const sizes = new Map(layout.heads.map((h) => [h.id, { width: h.width, height: h.height }]));
977
+ const depths = sequenceCallDepths(messages, layout.heads[0]?.id);
978
+ const walk = computeWalkLayout(phases, messages, depths, (id) => sizes.get(id), new Map(edges.map((e) => [e.id, e])), numberOf);
979
+ const width = round2(walk.width + CANVAS_PAD_X * 2);
980
+ const height = round2(walk.height + CANVAS_PAD_TOP + SEQ_ALT_BOTTOM);
981
+ const tipId = escapeHtml(`walk-tip-${projection.id}`);
982
+ const bands = walk.bands
983
+ .map((b) => `<text class="w-band" x="${b.x}" y="${b.y}">${escapeHtml(b.label)}</text>`)
984
+ .join('');
985
+ const lines = walk.edges.map((e) => `<path class="w-edge" d="${e.d}"/>`).join('');
986
+ const steps = walk.steps
987
+ .map((st) => `<path class="w-step" data-n="${st.n}" data-from="${escapeHtml(st.from)}" data-to="${escapeHtml(st.to)}" ` +
988
+ `data-message-id="${escapeHtml(st.messageId)}" d="${st.d}" marker-end="url(#${tipId})"/>`)
989
+ .join('');
990
+ const svg = `<svg class="links" xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="0 0 ${width} ${height}" ` +
991
+ `role="group" aria-label="${escapeHtml(`${projection.title} 따라가기 지도`)}">` +
992
+ `<defs><marker id="${tipId}" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">` +
993
+ `<path class="w-tip" d="M0 0L10 5L0 10z"/></marker></defs>` +
994
+ `<g transform="translate(${CANVAS_PAD_X} ${CANVAS_PAD_TOP})"><g class="w-bands">${bands}</g><g class="w-edges">${lines}</g>` +
995
+ `<g class="w-steps">${steps}</g></g></svg>`;
996
+ const cards = viewCards(walk.nodes, nodeById, services, whereOf);
997
+ return seqAltSection(levelIdAttr, 'walk', '따라가기', projection, width, height, svg + cards);
998
+ }
999
+ /** 순서도 보기 전환 단추와 따라가기 자막. 순서도가 있는 그림에만 싣고 순서도 레벨에서만 보인다 */
1000
+ function renderSequenceUi(stepLists) {
1001
+ const mode = (id, text, on) => `<button type="button" class="btn" data-seq-mode="${id}" aria-pressed="${on}">${text}</button>`;
1002
+ return (`<div id="seq-bar" class="seq-bar" hidden>` +
1003
+ `<div class="seq-modes" role="group" aria-label="순서도 보기">` +
1004
+ mode('seq', '순서도', true) +
1005
+ mode('cards', '단계별 카드', false) +
1006
+ mode('walk', '따라가기', false) +
1007
+ `</div>` +
1008
+ `<div class="seq-cap" hidden><span class="sw-say" aria-live="polite"><span class="sw-n"></span><span class="sw-p"></span>` +
1009
+ `<span class="sw-t"></span><span class="sw-b" hidden></span></span>` +
1010
+ `<span class="sw-ctl"><button type="button" class="btn icon sw-prev" aria-label="이전 단계" title="이전 단계 (←)">◀</button>` +
1011
+ `<button type="button" class="btn sw-play" aria-pressed="false">재생</button>` +
1012
+ `<button type="button" class="btn icon sw-next" aria-label="다음 단계" title="다음 단계 (→)">▶</button></span></div>` +
1013
+ `</div>\n` +
1014
+ `<nav id="seq-steps" class="seq-steps" aria-label="단계 목록" hidden>${stepLists.join('')}</nav>`);
1015
+ }
815
1016
  /** 데이터가 어디서 어디로 옮겨 가는지. 선 라벨이 옮겨 가는 데이터 이름이다 */
816
1017
  function renderDataflowSection(levelId, projection, layout, nodeById, services) {
817
1018
  const px = (x) => round2(x + CANVAS_PAD_X);
@@ -1057,8 +1258,8 @@ function renderBar(title, drill, generatedAt, nodeCount, edgeCount, questionCoun
1057
1258
  }
1058
1259
  const clientScripts = new Map();
1059
1260
  /** 칩 글자와 설명, 레인 제목은 IR이 쓰는 팩 것만 싣는다. 팩을 더해도 그 팩을 안 쓰는 그림의 바이트가 그대로다 */
1060
- function clientScriptFor(vocab, rootFlows, views, mixedBundles) {
1061
- const key = `${vocab.packIds.join(',')}|${rootFlows}|${views}|${mixedBundles}`;
1261
+ function clientScriptFor(vocab, rootFlows, views, mixedBundles, sequences) {
1262
+ const key = `${vocab.packIds.join(',')}|${rootFlows}|${views}|${mixedBundles}|${sequences}`;
1062
1263
  const hit = clientScripts.get(key);
1063
1264
  if (hit !== undefined)
1064
1265
  return hit;
@@ -1090,6 +1291,7 @@ function clientScriptFor(vocab, rootFlows, views, mixedBundles) {
1090
1291
  rootFlows,
1091
1292
  views,
1092
1293
  mixedBundles,
1294
+ sequences,
1093
1295
  docs: vocab.packIds.includes('knowledge'),
1094
1296
  });
1095
1297
  clientScripts.set(key, script);
@@ -1103,12 +1305,13 @@ function renderViewsPop(views) {
1103
1305
  .join('');
1104
1306
  return `<div id="views" class="pop" role="dialog" aria-label="질문별 그림" tabindex="-1" hidden><h3>질문별 그림</h3><ul class="flow-list view-list">${items}</ul></div>`;
1105
1307
  }
1106
- function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles = false, }) {
1308
+ function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles = false, sequences = [], }) {
1107
1309
  const title = viewTitle(ir);
1108
1310
  const vocab = irVocabulary(ir);
1109
1311
  const nodeById = new Map(payload.nodes.map((n) => [n.id, n]));
1110
1312
  const hasFlows = flows !== undefined && flows.flows.length > 0;
1111
1313
  const hasViews = views !== undefined && views.length > 0;
1314
+ const hasSequences = sequences.length > 0;
1112
1315
  return [
1113
1316
  '<!doctype html>',
1114
1317
  '<html lang="ko">',
@@ -1117,7 +1320,7 @@ function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles =
1117
1320
  '<meta name="viewport" content="width=device-width, initial-scale=1">',
1118
1321
  `<title>${escapeHtml(title)}</title>`,
1119
1322
  `<script>${THEME_BOOT_SCRIPT}</script>`,
1120
- `<style>${renderCss(vocab)}${hasViews ? `\n${VIEW_CSS}` : ''}${vocab.packIds.includes('knowledge') ? `\n${DOC_CSS}` : ''}</style>`,
1323
+ `<style>${renderCss(vocab)}${hasViews ? `\n${VIEW_CSS}` : ''}${hasSequences ? `\n${SEQ_VIEW_CSS}` : ''}${vocab.packIds.includes('knowledge') ? `\n${DOC_CSS}` : ''}</style>`,
1121
1324
  '</head>',
1122
1325
  '<body>',
1123
1326
  renderIconSprite(vocab),
@@ -1131,7 +1334,8 @@ function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles =
1131
1334
  '</div>',
1132
1335
  '</div>',
1133
1336
  `<p id="hint" class="hint"><span>${drill ? HINT_DRILL : HINT_FLAT}</span>` +
1134
- `<button type="button" id="hint-close" aria-label="안내 닫기">${iconUse('u-close')}</button></p>`,
1337
+ `<button type="button" id="hint-close" aria-label="안내 닫기">${iconUse('u-close')}</button></p>` +
1338
+ (hasSequences ? `\n${renderSequenceUi(sequences)}` : ''),
1135
1339
  '<details id="sheet" class="sheet" hidden><summary id="sheet-title">세부 연결</summary><div id="sheet-body" class="scroll"></div></details>',
1136
1340
  '<aside id="drawer" class="drawer" role="dialog" aria-modal="false" aria-labelledby="drawer-title" aria-hidden="true" inert>' +
1137
1341
  `<button type="button" id="drawer-close" class="btn icon dr-close" aria-label="닫기">${iconUse('u-close')}</button>` +
@@ -1143,7 +1347,7 @@ function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles =
1143
1347
  ? '<div id="flow-pop" class="pop" role="dialog" aria-label="흐름 고르기" tabindex="-1" hidden><h3>흐름</h3><ul class="flow-list"></ul></div>'
1144
1348
  : '') + (hasViews ? `\n${renderViewsPop(views)}` : ''),
1145
1349
  `<script id="ir" type="application/json">${embedJson(stableStringify(payload))}</script>`,
1146
- `<script>${clientScriptFor(vocab, hasFlows && flows.flows.some((f) => f.service === undefined), hasViews, mixedBundles)}</script>`,
1350
+ `<script>${clientScriptFor(vocab, hasFlows && flows.flows.some((f) => f.service === undefined), hasViews, mixedBundles, hasSequences)}</script>`,
1147
1351
  '</body>',
1148
1352
  '</html>',
1149
1353
  '',
@@ -1240,6 +1444,7 @@ export function renderDrilldownHtml(validated, drilldown, opts) {
1240
1444
  messages: messagePayload(views, drawnMessages, base.unresolved, ir.edges),
1241
1445
  });
1242
1446
  }
1447
+ const sequenceSteps = [];
1243
1448
  const rootFlowLinks = flowLevels
1244
1449
  .filter(({ flow }) => flow.service === undefined)
1245
1450
  .map(({ level, flow }) => ({ level: level.id, title: flow.title }))
@@ -1288,7 +1493,12 @@ export function renderDrilldownHtml(validated, drilldown, opts) {
1288
1493
  return renderCompareSection(v.level, p, layout, nodeById, base.services);
1289
1494
  }
1290
1495
  const layout = computeSequenceLayout(p, drawnMessages, nodeById, whereOf);
1291
- return renderSequenceSection(v.level, p, layout, nodeById, base.services, whereOf);
1496
+ const section = renderSequenceSection(v.level, p, layout, nodeById, base.services, whereOf);
1497
+ const alts = renderSequenceAlts(v.level, p, layout, drawnMessages, nodeById, base.services, whereOf, ir.edges);
1498
+ if (!alts)
1499
+ return section;
1500
+ sequenceSteps.push(alts.steps);
1501
+ return `${section}\n${alts.sections}`;
1292
1502
  }))
1293
1503
  .join('\n');
1294
1504
  return renderPage({
@@ -1299,6 +1509,7 @@ export function renderDrilldownHtml(validated, drilldown, opts) {
1299
1509
  ...(flowLevels.length > 0 ? { flows: { flows: drawnFlows, drawnSteps } } : {}),
1300
1510
  ...(views.length > 0 ? { views } : {}),
1301
1511
  mixedBundles: levels.some((l) => l.edges.some((e) => e.inferred !== undefined)),
1512
+ ...(sequenceSteps.length > 0 ? { sequences: sequenceSteps } : {}),
1302
1513
  });
1303
1514
  }
1304
1515
  //# sourceMappingURL=html-renderer.js.map