@tienne/gestalt 0.90.1 → 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.
- package/dist/package.json +1 -1
- package/dist/plugin/skills/architecture/SKILL.md +27 -2
- package/dist/schemas/architecture-ir.schema.json +20 -0
- package/dist/src/architecture/compare-layout.d.ts.map +1 -1
- package/dist/src/architecture/compare-layout.js +1 -0
- package/dist/src/architecture/compare-layout.js.map +1 -1
- package/dist/src/architecture/dataflow-layout.d.ts.map +1 -1
- package/dist/src/architecture/dataflow-layout.js +1 -0
- package/dist/src/architecture/dataflow-layout.js.map +1 -1
- package/dist/src/architecture/drilldown.d.ts.map +1 -1
- package/dist/src/architecture/drilldown.js +1 -0
- package/dist/src/architecture/drilldown.js.map +1 -1
- package/dist/src/architecture/html-client.d.ts +8 -0
- package/dist/src/architecture/html-client.d.ts.map +1 -1
- package/dist/src/architecture/html-client.js +312 -23
- package/dist/src/architecture/html-client.js.map +1 -1
- package/dist/src/architecture/html-renderer.d.ts.map +1 -1
- package/dist/src/architecture/html-renderer.js +240 -15
- package/dist/src/architecture/html-renderer.js.map +1 -1
- package/dist/src/architecture/html-theme.d.ts +2 -0
- package/dist/src/architecture/html-theme.d.ts.map +1 -1
- package/dist/src/architecture/html-theme.js +68 -1
- package/dist/src/architecture/html-theme.js.map +1 -1
- package/dist/src/architecture/ir-schema.d.ts +66 -26
- package/dist/src/architecture/ir-schema.d.ts.map +1 -1
- package/dist/src/architecture/ir-schema.js +9 -0
- package/dist/src/architecture/ir-schema.js.map +1 -1
- package/dist/src/architecture/kind-text.d.ts +10 -0
- package/dist/src/architecture/kind-text.d.ts.map +1 -1
- package/dist/src/architecture/kind-text.js +17 -0
- package/dist/src/architecture/kind-text.js.map +1 -1
- package/dist/src/architecture/layout.d.ts +5 -0
- package/dist/src/architecture/layout.d.ts.map +1 -1
- package/dist/src/architecture/layout.js +14 -3
- package/dist/src/architecture/layout.js.map +1 -1
- package/dist/src/architecture/merge.d.ts.map +1 -1
- package/dist/src/architecture/merge.js +9 -0
- package/dist/src/architecture/merge.js.map +1 -1
- package/dist/src/architecture/packs/data.d.ts +14 -0
- package/dist/src/architecture/packs/data.d.ts.map +1 -1
- package/dist/src/architecture/packs/data.js +11 -7
- package/dist/src/architecture/packs/data.js.map +1 -1
- package/dist/src/architecture/packs/generic.d.ts +7 -0
- package/dist/src/architecture/packs/generic.d.ts.map +1 -1
- package/dist/src/architecture/packs/generic.js +13 -6
- package/dist/src/architecture/packs/generic.js.map +1 -1
- package/dist/src/architecture/packs/harness.d.ts +10 -0
- package/dist/src/architecture/packs/harness.d.ts.map +1 -1
- package/dist/src/architecture/packs/harness.js +18 -6
- package/dist/src/architecture/packs/harness.js.map +1 -1
- package/dist/src/architecture/packs/index.d.ts +110 -0
- package/dist/src/architecture/packs/index.d.ts.map +1 -1
- package/dist/src/architecture/packs/infra.d.ts +10 -0
- package/dist/src/architecture/packs/infra.d.ts.map +1 -1
- package/dist/src/architecture/packs/infra.js +9 -4
- package/dist/src/architecture/packs/infra.js.map +1 -1
- package/dist/src/architecture/packs/knowledge.d.ts +8 -0
- package/dist/src/architecture/packs/knowledge.d.ts.map +1 -1
- package/dist/src/architecture/packs/knowledge.js +8 -4
- package/dist/src/architecture/packs/knowledge.js.map +1 -1
- package/dist/src/architecture/packs/process.d.ts +10 -0
- package/dist/src/architecture/packs/process.d.ts.map +1 -1
- package/dist/src/architecture/packs/process.js +14 -5
- package/dist/src/architecture/packs/process.js.map +1 -1
- package/dist/src/architecture/packs/types.d.ts +6 -0
- package/dist/src/architecture/packs/types.d.ts.map +1 -1
- package/dist/src/architecture/packs/web-product.d.ts +51 -0
- package/dist/src/architecture/packs/web-product.d.ts.map +1 -1
- package/dist/src/architecture/packs/web-product.js +94 -33
- package/dist/src/architecture/packs/web-product.js.map +1 -1
- package/dist/src/architecture/sequence-layout.d.ts.map +1 -1
- package/dist/src/architecture/sequence-layout.js +1 -0
- package/dist/src/architecture/sequence-layout.js.map +1 -1
- package/dist/src/architecture/sequence-views.d.ts +155 -0
- package/dist/src/architecture/sequence-views.d.ts.map +1 -0
- package/dist/src/architecture/sequence-views.js +407 -0
- package/dist/src/architecture/sequence-views.js.map +1 -0
- package/dist/src/architecture/types.d.ts +15 -0
- package/dist/src/architecture/types.d.ts.map +1 -1
- package/dist/src/architecture/types.js +4 -0
- package/dist/src/architecture/types.js.map +1 -1
- package/dist/src/architecture/validator.d.ts +9 -1
- package/dist/src/architecture/validator.d.ts.map +1 -1
- package/dist/src/architecture/validator.js +102 -1
- package/dist/src/architecture/validator.js.map +1 -1
- package/dist/src/mcp/tools/architecture-passthrough.d.ts.map +1 -1
- package/dist/src/mcp/tools/architecture-passthrough.js +3 -1
- package/dist/src/mcp/tools/architecture-passthrough.js.map +1 -1
- package/package.json +1 -1
- package/plugin/.codex-plugin/plugin.json +1 -1
- package/plugin/.mcp.json +1 -1
- package/plugin/mcp.json +1 -1
- package/plugin/skills/architecture/SKILL.md +27 -2
- package/schemas/architecture-ir.schema.json +20 -0
|
@@ -1,8 +1,8 @@
|
|
|
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';
|
|
5
|
-
import { LANE_TITLES, 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';
|
|
4
|
+
import { datastoreEngine, iconUse, PRODUCT_COLORS, renderCss, renderIconSprite, VIEW_CSS, DOC_CSS, SEQ_VIEW_CSS, } from './html-theme.js';
|
|
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';
|
|
8
8
|
import { ALL_PACKS_VOCABULARY, irVocabulary } from './packs/index.js';
|
|
@@ -10,8 +10,9 @@ 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
|
-
import { chipTextOverride, displayKindOf } from './types.js';
|
|
15
|
+
import { chipTextOverride, displayKindOf, hasCardDescription } from './types.js';
|
|
15
16
|
import { maskSharedText, redactForSharing } from './validator.js';
|
|
16
17
|
const DASHED_PATTERN = '6 4';
|
|
17
18
|
const INFERRED_BADGE = '추정';
|
|
@@ -60,8 +61,8 @@ const FLOW_TEXT = {
|
|
|
60
61
|
person: '사람',
|
|
61
62
|
system: '시스템',
|
|
62
63
|
};
|
|
63
|
-
const HINT_DRILL = '
|
|
64
|
-
const HINT_FLAT = '
|
|
64
|
+
const HINT_DRILL = '카드나 선, 칸 제목을 누르면 설명과 출처가 보여요. 더블클릭하거나 상세보기를 누르면 한 단계 안으로 들어가고 뒤로가기로 돌아와요. 전체 화면에서 Shift나 ⌘를 누른 채 두 항목을 고르면 그 사이 경로를 보여줘요.';
|
|
65
|
+
const HINT_FLAT = '카드나 선, 칸 제목을 누르면 설명과 출처가 보여요. 끌어서 옮기고 ⌘나 Ctrl을 누른 채 휠을 굴리면 크게 볼 수 있어요.';
|
|
65
66
|
function compareStr(a, b) {
|
|
66
67
|
return a < b ? -1 : a > b ? 1 : 0;
|
|
67
68
|
}
|
|
@@ -260,9 +261,14 @@ function docCoverBadge(cover) {
|
|
|
260
261
|
: '') +
|
|
261
262
|
owner);
|
|
262
263
|
}
|
|
264
|
+
/** 레인 제목의 aria-label. 클라이언트가 환경 필터로 수를 바꿀 때도 같은 꼴로 다시 단다 */
|
|
265
|
+
function laneAria(title, count) {
|
|
266
|
+
return `${title} 레인, 카드 ${count}개`;
|
|
267
|
+
}
|
|
263
268
|
function renderCard(node, box, enterable, focus, facts, band, micro, flows = 0, products = [], productIds = [], cover, where) {
|
|
264
269
|
const name = nodeName(node);
|
|
265
270
|
const guess = isGuess(node);
|
|
271
|
+
const desc = hasCardDescription(node.description);
|
|
266
272
|
const platforms = facts?.platforms.map((p) => platformName(p, facts.webHosting)) ?? [];
|
|
267
273
|
const dk = displayKindOf(node);
|
|
268
274
|
const cls = ['node', `k-${dk}`];
|
|
@@ -279,7 +285,8 @@ function renderCard(node, box, enterable, focus, facts, band, micro, flows = 0,
|
|
|
279
285
|
const tooltip = (node.displayName === undefined
|
|
280
286
|
? node.label
|
|
281
287
|
: `${node.displayName}${guess ? ` (${INFERRED_BADGE})` : ''}\n${node.label}`) +
|
|
282
|
-
(facts?.prodDomain !== undefined ? `\n${facts.prodDomain}` : '')
|
|
288
|
+
(facts?.prodDomain !== undefined ? `\n${facts.prodDomain}` : '') +
|
|
289
|
+
(desc ? `\n${node.description}` : '');
|
|
283
290
|
const subText = where !== undefined
|
|
284
291
|
? `<span class="tc">${escapeHtml(where)}</span>`
|
|
285
292
|
: facts?.prodDomain !== undefined
|
|
@@ -307,6 +314,7 @@ function renderCard(node, box, enterable, focus, facts, band, micro, flows = 0,
|
|
|
307
314
|
docCoverBadge(cover) +
|
|
308
315
|
`</span>` +
|
|
309
316
|
second +
|
|
317
|
+
(desc ? `<span class="ds">${escapeHtml(node.description)}</span>` : '') +
|
|
310
318
|
(enterable ? '<span class="go" aria-hidden="true">›</span>' : '') +
|
|
311
319
|
(products.length > 1 ? renderProductBricks(products, productIds, box.width) : '') +
|
|
312
320
|
`</div>`);
|
|
@@ -372,7 +380,7 @@ function renderLink(edge, route, labelOf, crossAccount) {
|
|
|
372
380
|
`${renderPill(edge.count, route.mid, edge.inferred !== undefined)}</g>`);
|
|
373
381
|
}
|
|
374
382
|
const name = `${EDGE_KIND_TEXT[edge.kind]}: ${labelOf(edge.from)} → ${labelOf(edge.to)}${xaText}`;
|
|
375
|
-
return (`<g class="link e-${edge.kind}${xa}" ${ends}><title>${escapeHtml(name)}</title>${hit}` +
|
|
383
|
+
return (`<g class="link e-${edge.kind}${xa}" ${ends} data-link-id="${escapeHtml(edge.id)}" tabindex="0" role="button" aria-label="${escapeHtml(name)}"><title>${escapeHtml(name)}</title>${hit}` +
|
|
376
384
|
`<path class="edge" data-edge-id="${escapeHtml(edge.id)}" d="${route.d}" stroke-width="${width}"${dash}/>${tip}</g>`);
|
|
377
385
|
}
|
|
378
386
|
const VIEW_SHAPE_NAME = {
|
|
@@ -469,7 +477,8 @@ function renderLevelSection(spec) {
|
|
|
469
477
|
`height="${round2(height - LANE_INSET_Y * 2)}" rx="14"/>`)
|
|
470
478
|
.join('');
|
|
471
479
|
const laneTitles = layout.lanes
|
|
472
|
-
.map((l) => `<div class="lane-title"
|
|
480
|
+
.map((l) => `<div class="lane-title" data-lane-id="${escapeHtml(l.id)}" role="button" tabindex="0" aria-label="${escapeHtml(laneAria(l.title, l.count))}" ` +
|
|
481
|
+
`style="left:${round2(l.x + CANVAS_PAD_X)}px;top:${LANE_INSET_Y + 10}px;width:${l.width}px">` +
|
|
473
482
|
`${escapeHtml(l.title)}<span class="n">${l.count}</span></div>`)
|
|
474
483
|
.join('');
|
|
475
484
|
const { regionRects, regionTitles } = renderBands(layout, width);
|
|
@@ -804,6 +813,206 @@ function stickyHeadBar(heads, width) {
|
|
|
804
813
|
const h = heads.reduce((m, b) => Math.max(m, b.height), 0);
|
|
805
814
|
return `<div class="seq-sticky" style="top:${CANVAS_PAD_TOP - STICKY_PAD}px;width:${width}px;height:${h + STICKY_PAD * 2}px"></div>`;
|
|
806
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
|
+
}
|
|
807
1016
|
/** 데이터가 어디서 어디로 옮겨 가는지. 선 라벨이 옮겨 가는 데이터 이름이다 */
|
|
808
1017
|
function renderDataflowSection(levelId, projection, layout, nodeById, services) {
|
|
809
1018
|
const px = (x) => round2(x + CANVAS_PAD_X);
|
|
@@ -1049,8 +1258,8 @@ function renderBar(title, drill, generatedAt, nodeCount, edgeCount, questionCoun
|
|
|
1049
1258
|
}
|
|
1050
1259
|
const clientScripts = new Map();
|
|
1051
1260
|
/** 칩 글자와 설명, 레인 제목은 IR이 쓰는 팩 것만 싣는다. 팩을 더해도 그 팩을 안 쓰는 그림의 바이트가 그대로다 */
|
|
1052
|
-
function clientScriptFor(vocab, rootFlows, views, mixedBundles) {
|
|
1053
|
-
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}`;
|
|
1054
1263
|
const hit = clientScripts.get(key);
|
|
1055
1264
|
if (hit !== undefined)
|
|
1056
1265
|
return hit;
|
|
@@ -1062,8 +1271,14 @@ function clientScriptFor(vocab, rootFlows, views, mixedBundles) {
|
|
|
1062
1271
|
...pick(NODE_KIND_TEXT, Object.keys(vocab.looks)),
|
|
1063
1272
|
...pick(EDGE_KIND_TEXT, [...Object.keys(vocab.edgeKinds), 'contains']),
|
|
1064
1273
|
},
|
|
1274
|
+
kindAbout: {
|
|
1275
|
+
...pick(NODE_KIND_ABOUT, Object.keys(vocab.looks)),
|
|
1276
|
+
...pick(EDGE_KIND_ABOUT, [...Object.keys(vocab.edgeKinds), 'contains']),
|
|
1277
|
+
},
|
|
1065
1278
|
evidenceText: EVIDENCE_TYPE_TEXT,
|
|
1066
1279
|
laneTitles: pick(LANE_TITLES, Object.keys(vocab.lanes)),
|
|
1280
|
+
laneAbout: pick(LANE_ABOUT, Object.keys(vocab.lanes)),
|
|
1281
|
+
stageLaneAbout: STAGE_LANE_ABOUT,
|
|
1067
1282
|
inferredBadge: INFERRED_BADGE,
|
|
1068
1283
|
platformChip: PLATFORM_CHIP_TEXT,
|
|
1069
1284
|
platformName: PLATFORM_NAME,
|
|
@@ -1076,6 +1291,7 @@ function clientScriptFor(vocab, rootFlows, views, mixedBundles) {
|
|
|
1076
1291
|
rootFlows,
|
|
1077
1292
|
views,
|
|
1078
1293
|
mixedBundles,
|
|
1294
|
+
sequences,
|
|
1079
1295
|
docs: vocab.packIds.includes('knowledge'),
|
|
1080
1296
|
});
|
|
1081
1297
|
clientScripts.set(key, script);
|
|
@@ -1089,12 +1305,13 @@ function renderViewsPop(views) {
|
|
|
1089
1305
|
.join('');
|
|
1090
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>`;
|
|
1091
1307
|
}
|
|
1092
|
-
function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles = false, }) {
|
|
1308
|
+
function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles = false, sequences = [], }) {
|
|
1093
1309
|
const title = viewTitle(ir);
|
|
1094
1310
|
const vocab = irVocabulary(ir);
|
|
1095
1311
|
const nodeById = new Map(payload.nodes.map((n) => [n.id, n]));
|
|
1096
1312
|
const hasFlows = flows !== undefined && flows.flows.length > 0;
|
|
1097
1313
|
const hasViews = views !== undefined && views.length > 0;
|
|
1314
|
+
const hasSequences = sequences.length > 0;
|
|
1098
1315
|
return [
|
|
1099
1316
|
'<!doctype html>',
|
|
1100
1317
|
'<html lang="ko">',
|
|
@@ -1103,7 +1320,7 @@ function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles =
|
|
|
1103
1320
|
'<meta name="viewport" content="width=device-width, initial-scale=1">',
|
|
1104
1321
|
`<title>${escapeHtml(title)}</title>`,
|
|
1105
1322
|
`<script>${THEME_BOOT_SCRIPT}</script>`,
|
|
1106
|
-
`<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>`,
|
|
1107
1324
|
'</head>',
|
|
1108
1325
|
'<body>',
|
|
1109
1326
|
renderIconSprite(vocab),
|
|
@@ -1117,7 +1334,8 @@ function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles =
|
|
|
1117
1334
|
'</div>',
|
|
1118
1335
|
'</div>',
|
|
1119
1336
|
`<p id="hint" class="hint"><span>${drill ? HINT_DRILL : HINT_FLAT}</span>` +
|
|
1120
|
-
`<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)}` : ''),
|
|
1121
1339
|
'<details id="sheet" class="sheet" hidden><summary id="sheet-title">세부 연결</summary><div id="sheet-body" class="scroll"></div></details>',
|
|
1122
1340
|
'<aside id="drawer" class="drawer" role="dialog" aria-modal="false" aria-labelledby="drawer-title" aria-hidden="true" inert>' +
|
|
1123
1341
|
`<button type="button" id="drawer-close" class="btn icon dr-close" aria-label="닫기">${iconUse('u-close')}</button>` +
|
|
@@ -1129,7 +1347,7 @@ function renderPage({ ir, payload, sections, drill, flows, views, mixedBundles =
|
|
|
1129
1347
|
? '<div id="flow-pop" class="pop" role="dialog" aria-label="흐름 고르기" tabindex="-1" hidden><h3>흐름</h3><ul class="flow-list"></ul></div>'
|
|
1130
1348
|
: '') + (hasViews ? `\n${renderViewsPop(views)}` : ''),
|
|
1131
1349
|
`<script id="ir" type="application/json">${embedJson(stableStringify(payload))}</script>`,
|
|
1132
|
-
`<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>`,
|
|
1133
1351
|
'</body>',
|
|
1134
1352
|
'</html>',
|
|
1135
1353
|
'',
|
|
@@ -1226,6 +1444,7 @@ export function renderDrilldownHtml(validated, drilldown, opts) {
|
|
|
1226
1444
|
messages: messagePayload(views, drawnMessages, base.unresolved, ir.edges),
|
|
1227
1445
|
});
|
|
1228
1446
|
}
|
|
1447
|
+
const sequenceSteps = [];
|
|
1229
1448
|
const rootFlowLinks = flowLevels
|
|
1230
1449
|
.filter(({ flow }) => flow.service === undefined)
|
|
1231
1450
|
.map(({ level, flow }) => ({ level: level.id, title: flow.title }))
|
|
@@ -1274,7 +1493,12 @@ export function renderDrilldownHtml(validated, drilldown, opts) {
|
|
|
1274
1493
|
return renderCompareSection(v.level, p, layout, nodeById, base.services);
|
|
1275
1494
|
}
|
|
1276
1495
|
const layout = computeSequenceLayout(p, drawnMessages, nodeById, whereOf);
|
|
1277
|
-
|
|
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}`;
|
|
1278
1502
|
}))
|
|
1279
1503
|
.join('\n');
|
|
1280
1504
|
return renderPage({
|
|
@@ -1285,6 +1509,7 @@ export function renderDrilldownHtml(validated, drilldown, opts) {
|
|
|
1285
1509
|
...(flowLevels.length > 0 ? { flows: { flows: drawnFlows, drawnSteps } } : {}),
|
|
1286
1510
|
...(views.length > 0 ? { views } : {}),
|
|
1287
1511
|
mixedBundles: levels.some((l) => l.edges.some((e) => e.inferred !== undefined)),
|
|
1512
|
+
...(sequenceSteps.length > 0 ? { sequences: sequenceSteps } : {}),
|
|
1288
1513
|
});
|
|
1289
1514
|
}
|
|
1290
1515
|
//# sourceMappingURL=html-renderer.js.map
|