@takagaki/cortex-decisions-viewer 0.12.114 → 0.12.116
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/build.js +82 -1
- package/dist/evidence-names.js +30 -1
- package/dist/render.js +309 -34
- package/package.json +1 -1
package/dist/build.js
CHANGED
|
@@ -36,7 +36,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
36
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
-
exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
|
|
39
|
+
exports.DECISION_FIRMNESS = exports.PROJECT_ICON_EXTS = exports.MIRROR_RELS = void 0;
|
|
40
40
|
exports.sanitizeBody = sanitizeBody;
|
|
41
41
|
exports.findProjectIcon = findProjectIcon;
|
|
42
42
|
exports.readFleetSources = readFleetSources;
|
|
@@ -55,6 +55,9 @@ exports.bodyFileSlug = bodyFileSlug;
|
|
|
55
55
|
exports.bodyStampId = bodyStampId;
|
|
56
56
|
exports.bodyStampComment = bodyStampComment;
|
|
57
57
|
exports.build = build;
|
|
58
|
+
exports.decisionFirmnessOf = decisionFirmnessOf;
|
|
59
|
+
exports.decisionTopicOf = decisionTopicOf;
|
|
60
|
+
exports.decidedByOf = decidedByOf;
|
|
58
61
|
exports.createRefResolver = createRefResolver;
|
|
59
62
|
const node_crypto_1 = require("node:crypto");
|
|
60
63
|
const node_fs_1 = require("node:fs");
|
|
@@ -1523,6 +1526,16 @@ async function buildEvidenceBook(repoRoot, targetUrls, externalSources = []) {
|
|
|
1523
1526
|
if (key && name && !book.names.has(key))
|
|
1524
1527
|
book.names.set(key, name);
|
|
1525
1528
|
};
|
|
1529
|
+
// 片付けで移した議事録のリンク(URL の鍵 → 「定例名 日付」・cortex-engine #968 R-6・A-R6-3)。表が無い・読めない案件は今どおり
|
|
1530
|
+
// (種類の既定の名前)。資料より先に載せる(同じ鍵に資料の題名が先に付かないように)
|
|
1531
|
+
try {
|
|
1532
|
+
const table = JSON.parse(await node_fs_1.promises.readFile(path.join(repoRoot, evidence_names_1.MOVED_MEETINGS_FILE), "utf8"));
|
|
1533
|
+
for (const [url, name] of (0, evidence_names_1.movedMeetingNamesOf)(table))
|
|
1534
|
+
putName(foldKeyOf(url), name);
|
|
1535
|
+
}
|
|
1536
|
+
catch {
|
|
1537
|
+
// 表が無い(片付けていない案件)・壊れている
|
|
1538
|
+
}
|
|
1526
1539
|
// 課題(キー → 「キー 題名」)・Backlog のドキュメントと Wiki(URL の鍵 → 題名)
|
|
1527
1540
|
const backlogDir = await findBacklogDir(repoRoot, "課題管理");
|
|
1528
1541
|
for (const sub of BACKLOG_SECTIONS) {
|
|
@@ -2024,6 +2037,17 @@ function buildGraph(decisions, home, sections, targetUrls, repoBaseUrl, book = (
|
|
|
2024
2037
|
addReferenceEdges(d.id, d.references);
|
|
2025
2038
|
addLinkEdges(d.id, d.bodyLinks);
|
|
2026
2039
|
}
|
|
2040
|
+
// 決めた人の線(決定 → 関係者・#1094)。relations には書かれないので `decided_by[].member` から引く。
|
|
2041
|
+
// 🔴 相手の関係者がグラフに無ければ引かない(宛先の無い点を作らない=classifyTarget を通さない)
|
|
2042
|
+
for (const d of decisions) {
|
|
2043
|
+
for (const who of d.decidedBy ?? []) {
|
|
2044
|
+
if (!("member" in who))
|
|
2045
|
+
continue;
|
|
2046
|
+
const node = nodes.get(who.member);
|
|
2047
|
+
if (node && node.group === "member")
|
|
2048
|
+
edges.push({ from: d.id, to: who.member, rel: "decided_by" });
|
|
2049
|
+
}
|
|
2050
|
+
}
|
|
2027
2051
|
if (home) {
|
|
2028
2052
|
addRelationEdges(home.id, home.relations);
|
|
2029
2053
|
addSourceEdge(home.id, home.source);
|
|
@@ -2705,6 +2729,10 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
|
|
|
2705
2729
|
const references = referencesRaw.map((r) => refs.resolve(r));
|
|
2706
2730
|
const relations = toRelations(data.relations);
|
|
2707
2731
|
const followupHuman = (0, rederivation_1.humanFollowupOf)(data.followup);
|
|
2732
|
+
// 記憶 v2 の任意の欄(#1094)。**欄が無い・語彙外の記録ではキーごと載せない**(埋め込みの形を今と同じに保つ)
|
|
2733
|
+
const firmness = decisionFirmnessOf(data.firmness);
|
|
2734
|
+
const topic = decisionTopicOf(data.topic);
|
|
2735
|
+
const decidedBy = decidedByOf(data.decided_by);
|
|
2708
2736
|
// 本文先頭の H1 はタイトルと重複するため除去(詳細画面ではタイトルを別途表示する)
|
|
2709
2737
|
const bodyHtml = await renderRecordBody(content);
|
|
2710
2738
|
const filePath = repoRelDir ? `${repoRelDir}/${fileName}` : fileName;
|
|
@@ -2735,8 +2763,61 @@ async function parseDecision(raw, fileName, repoRelDir, repoBaseUrl, branch, ref
|
|
|
2735
2763
|
fmErrors: validateFm(data),
|
|
2736
2764
|
// 担当者の答え(#705)。**語彙外・形が違うものはキーごと載せない**
|
|
2737
2765
|
...(followupHuman ? { followupHuman: followupHuman } : {}),
|
|
2766
|
+
...(firmness ? { firmness } : {}),
|
|
2767
|
+
...(topic ? { topic } : {}),
|
|
2768
|
+
...(decidedBy ? { decidedBy } : {}),
|
|
2738
2769
|
};
|
|
2739
2770
|
}
|
|
2771
|
+
/**
|
|
2772
|
+
* 記憶 v2 の決定の新しい欄の読み手(#1094)。
|
|
2773
|
+
*
|
|
2774
|
+
* 🔴 **写し。正本は cortex-engine `scripts/gold/decision-v2-fields.mjs` の `decisionV2FieldsOf`**。
|
|
2775
|
+
* 同じバイト列の表 `test/fixtures/decision-v2-fields-contract.json` を両リポに置き、
|
|
2776
|
+
* `test/supersedes-view.test.mjs` の「[契約] …」が全ケースを流す。規則を変えるときは engine の正本・この写し・表を同じ日に直す
|
|
2777
|
+
*/
|
|
2778
|
+
/** 決定の固さの語彙(並びも正本と同じ) */
|
|
2779
|
+
exports.DECISION_FIRMNESS = ["確定", "仮", "検討中"];
|
|
2780
|
+
/** `decided_by` の要素が持ってよい鍵 */
|
|
2781
|
+
const DECIDED_BY_KEYS = ["member", "name", "url"];
|
|
2782
|
+
const MEMBER_ID_RE = /^member:\S/;
|
|
2783
|
+
const DECIDED_BY_URL_RE = /^https?:\/\/\S+$/;
|
|
2784
|
+
const isText = (v) => typeof v === "string" && v.trim() !== "";
|
|
2785
|
+
/** frontmatter の `firmness` → 語彙の 1 語。語彙外・文字列でないものは undefined(綴りはそのまま比べる) */
|
|
2786
|
+
function decisionFirmnessOf(value) {
|
|
2787
|
+
return exports.DECISION_FIRMNESS.includes(value) ? value : undefined;
|
|
2788
|
+
}
|
|
2789
|
+
/** frontmatter の `topic` → 論点の札の名前(前後の空白を落とす)。空・文字列でないものは undefined */
|
|
2790
|
+
function decisionTopicOf(value) {
|
|
2791
|
+
return isText(value) ? value.trim() : undefined;
|
|
2792
|
+
}
|
|
2793
|
+
/** `decided_by` の要素 1 つ。読めなければ null(文字列・知らない鍵・member と name/url の同居・member: で始まらない・空の name・http(s) でない url) */
|
|
2794
|
+
function decidedByEntryOf(entry) {
|
|
2795
|
+
if (entry == null || typeof entry !== "object" || Array.isArray(entry))
|
|
2796
|
+
return null;
|
|
2797
|
+
const o = entry;
|
|
2798
|
+
if (Object.keys(o).some((k) => !DECIDED_BY_KEYS.includes(k)))
|
|
2799
|
+
return null;
|
|
2800
|
+
const has = (k) => Object.prototype.hasOwnProperty.call(o, k);
|
|
2801
|
+
if (has("member")) {
|
|
2802
|
+
if (has("name") || has("url"))
|
|
2803
|
+
return null;
|
|
2804
|
+
return typeof o.member === "string" && MEMBER_ID_RE.test(o.member) ? { member: o.member } : null;
|
|
2805
|
+
}
|
|
2806
|
+
if (!has("name") || !isText(o.name))
|
|
2807
|
+
return null;
|
|
2808
|
+
if (o.url == null)
|
|
2809
|
+
return { name: o.name };
|
|
2810
|
+
return typeof o.url === "string" && DECIDED_BY_URL_RE.test(o.url) ? { name: o.name, url: o.url } : null;
|
|
2811
|
+
}
|
|
2812
|
+
/**
|
|
2813
|
+
* frontmatter の `decided_by` → 決めた人の並び。🔴 形の崩れた要素は**飛ばす**。
|
|
2814
|
+
* 欄がリストでなければ undefined(無いものとして扱い、旧い `deciders` を出す)。空のリストは []
|
|
2815
|
+
*/
|
|
2816
|
+
function decidedByOf(value) {
|
|
2817
|
+
if (!Array.isArray(value))
|
|
2818
|
+
return undefined;
|
|
2819
|
+
return value.map(decidedByEntryOf).filter((e) => e !== null);
|
|
2820
|
+
}
|
|
2740
2821
|
/**
|
|
2741
2822
|
* 解決した参照 → 書かれていた値(#968 tools 1)。つながり画面は点の鍵をこの値から決める
|
|
2742
2823
|
* (`site-data` に載せない——機械の語を画面の材料に増やさない)
|
package/dist/evidence-names.js
CHANGED
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
* 対応表で URL に引けるものは同じ鍵に寄せる——今の ID と新しい URL の根拠が混ざっても 1 つの点になる。
|
|
14
14
|
*/
|
|
15
15
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
-
exports.emptyEvidenceBook = void 0;
|
|
16
|
+
exports.MOVED_MEETINGS_FILE = exports.emptyEvidenceBook = void 0;
|
|
17
17
|
exports.minuteFoldKey = minuteFoldKey;
|
|
18
18
|
exports.minuteLabel = minuteLabel;
|
|
19
|
+
exports.movedMeetingNamesOf = movedMeetingNamesOf;
|
|
19
20
|
exports.minuteRefOfPath = minuteRefOfPath;
|
|
20
21
|
exports.slackChannelOfLabel = slackChannelOfLabel;
|
|
21
22
|
exports.backlogTailOf = backlogTailOf;
|
|
@@ -55,6 +56,34 @@ function minuteLabel(id) {
|
|
|
55
56
|
const d = m[2];
|
|
56
57
|
return `${lastSegment(m[1])} ${d.slice(0, 4)}-${d.slice(4, 6)}-${d.slice(6, 8)}`;
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* 片付けで移した会議の表(engine が目次と同じコミットで置く・cortex-engine #968 R-6・A-R6-3)。
|
|
61
|
+
* 写しを片付けた案件だけが持つ。無ければ今どおり(移した議事録のリンクは種類の既定の名前で出る)
|
|
62
|
+
*/
|
|
63
|
+
exports.MOVED_MEETINGS_FILE = ".cortex/mirror-moved-meetings.json";
|
|
64
|
+
/**
|
|
65
|
+
* 移した会議の表 → `[移した URL, 会議の名前(「定例名 日付」)]` の並び。形が違う表・行は読まない(名前を当て推量しない)。
|
|
66
|
+
* 名前は `minuteLabel`(`minute:` の根拠と同じ 1 本)で作る——同じ会議が `minute:` で書かれても URL で書かれても同じ名前になる
|
|
67
|
+
*/
|
|
68
|
+
function movedMeetingNamesOf(table) {
|
|
69
|
+
if (!table || typeof table !== "object")
|
|
70
|
+
return [];
|
|
71
|
+
const t = table;
|
|
72
|
+
if (t.version !== 1 || !Array.isArray(t.meetings))
|
|
73
|
+
return [];
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const row of t.meetings) {
|
|
76
|
+
if (!row || typeof row !== "object")
|
|
77
|
+
continue;
|
|
78
|
+
const { url, ref } = row;
|
|
79
|
+
if (typeof url !== "string" || !/^https:\/\/\S+$/.test(url) || typeof ref !== "string")
|
|
80
|
+
continue;
|
|
81
|
+
const name = minuteLabel(ref);
|
|
82
|
+
if (name)
|
|
83
|
+
out.push([url, name]);
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
58
87
|
/**
|
|
59
88
|
* 議事録の写しのパス(`会議/{…}/{定例名}/{YYYYMMDD}/{…}minutes{…}.md`)→ `minute:{定例名}:{YYYYMMDD}`。
|
|
60
89
|
* `buildTargetUrlMap` の `minute:` の組み方と同じ規約(定例名は日付の 1 つ上の段)
|
package/dist/render.js
CHANGED
|
@@ -5314,6 +5314,15 @@ a.pager-btn:hover { text-decoration: none; }
|
|
|
5314
5314
|
.followup-section { margin: 14px 0 4px; }
|
|
5315
5315
|
/* 置き換え(#699 F3)。印は「その後」の置き換え済みと同じ色(同じ意味なので) */
|
|
5316
5316
|
.badge.retired { background: #f5f5f4; color: #57534e; border: 1px solid #d6d3d1; }
|
|
5317
|
+
/* 論点・固さ・「今」(#1094)。欄の無い記録には付かない */
|
|
5318
|
+
.badge.now { background: #1d4ed8; color: #fff; }
|
|
5319
|
+
.card.is-now { border-left: 3px solid #1d4ed8; }
|
|
5320
|
+
.card.is-faded { opacity: .55; }
|
|
5321
|
+
.badge.topic { background: #f5f3ff; color: #5b21b6; border: 1px solid #ddd6fe; font-weight: 500; }
|
|
5322
|
+
.badge.firmness-fixed { background: #ecfdf5; color: #047857; border: 1px solid #a7f3d0; }
|
|
5323
|
+
.badge.firmness-tentative { background: #fffbeb; color: #92400e; border: 1px solid #fde68a; }
|
|
5324
|
+
.badge.firmness-open { background: #f8fafc; color: #475569; border: 1px dashed #cbd5e1; }
|
|
5325
|
+
.decision-topic { margin-bottom: 4px; }
|
|
5317
5326
|
.supersede-section { margin: 14px 0 4px; }
|
|
5318
5327
|
.supersede-list { list-style: none; margin: 0 0 8px; padding: 0; }
|
|
5319
5328
|
.supersede-row { display: flex; flex-wrap: wrap; gap: 8px; align-items: baseline; font-size: 13.5px; line-height: 1.7; padding: 2px 0; }
|
|
@@ -7360,6 +7369,110 @@ ${calendarAddUrl.toString()}
|
|
|
7360
7369
|
return box;
|
|
7361
7370
|
}
|
|
7362
7371
|
|
|
7372
|
+
/* -------------------------------------------------------------------------
|
|
7373
|
+
* 論点・固さ・決めた人(記憶 v2・cortex-engine #1094)
|
|
7374
|
+
* -------------------------------------------------------------------------
|
|
7375
|
+
* 欄(topic / firmness / decidedBy)は**ビルド側で語彙・形を確かめてある**。欄の無い記録では
|
|
7376
|
+
* キーごと無いので、画面は有無だけを見る。🔴 **欄の無い記録だけの案件では要素を 1 つも作らない**
|
|
7377
|
+
* (絞り込み・「今」・固さの札・決めた人の節が出ない=今と同じ DOM)。
|
|
7378
|
+
* 「置き換えられた」の判定は isRetired(ビルドが supersedes.ts で組んだ答え)をそのまま使う。
|
|
7379
|
+
*/
|
|
7380
|
+
function hasTopic(rec) {
|
|
7381
|
+
return !!(rec && typeof rec.topic === "string" && rec.topic);
|
|
7382
|
+
}
|
|
7383
|
+
/** 論点の札の一覧(札を持つ決定が無ければ空) */
|
|
7384
|
+
function topicOptions(items) {
|
|
7385
|
+
var seen = {};
|
|
7386
|
+
var out = [];
|
|
7387
|
+
(items || []).forEach(function (d) {
|
|
7388
|
+
if (!hasTopic(d) || Object.prototype.hasOwnProperty.call(seen, d.topic)) return;
|
|
7389
|
+
seen[d.topic] = true;
|
|
7390
|
+
out.push(d.topic);
|
|
7391
|
+
});
|
|
7392
|
+
return out.sort(function (a, b) { return a.localeCompare(b, "ja"); });
|
|
7393
|
+
}
|
|
7394
|
+
/** 新しい順(決定日の降順 → ID の降順)。日付の無いものは最後 */
|
|
7395
|
+
function newestDecisionFirst(a, b) {
|
|
7396
|
+
if (a.date !== b.date) {
|
|
7397
|
+
if (!a.date) return 1;
|
|
7398
|
+
if (!b.date) return -1;
|
|
7399
|
+
return a.date < b.date ? 1 : -1;
|
|
7400
|
+
}
|
|
7401
|
+
return a.id < b.id ? 1 : a.id > b.id ? -1 : 0;
|
|
7402
|
+
}
|
|
7403
|
+
/**
|
|
7404
|
+
* 札ごとの「今」の決定(一番新しく、置き換えられていない 1 件)の ID の集合。
|
|
7405
|
+
* 削除済みは数えない(一覧と同じ材料)
|
|
7406
|
+
*/
|
|
7407
|
+
function nowDecisionIds() {
|
|
7408
|
+
var best = {};
|
|
7409
|
+
visibleRecords(SITE.decisions).forEach(function (d) {
|
|
7410
|
+
if (!hasTopic(d) || isRetired(d)) return;
|
|
7411
|
+
var cur = best[d.topic];
|
|
7412
|
+
if (!cur || newestDecisionFirst(d, cur) < 0) best[d.topic] = d;
|
|
7413
|
+
});
|
|
7414
|
+
var ids = {};
|
|
7415
|
+
Object.keys(best).forEach(function (t) { ids[best[t].id] = true; });
|
|
7416
|
+
return ids;
|
|
7417
|
+
}
|
|
7418
|
+
function isNowDecision(rec) {
|
|
7419
|
+
return !!(rec && rec._kind === "decision" && hasTopic(rec) && nowDecisionIds()[rec.id]);
|
|
7420
|
+
}
|
|
7421
|
+
function nowBadge(rec) {
|
|
7422
|
+
return isNowDecision(rec) ? el("span", { class: "badge now", title: "この論点で今有効な決定", text: "今" }) : null;
|
|
7423
|
+
}
|
|
7424
|
+
/** 論点の札(一覧の行)。札が無ければ null */
|
|
7425
|
+
function topicBadge(rec) {
|
|
7426
|
+
return hasTopic(rec) ? el("span", { class: "badge topic", title: "論点", text: rec.topic }) : null;
|
|
7427
|
+
}
|
|
7428
|
+
var FIRMNESS_CLASS = { "確定": "fixed", "仮": "tentative", "検討中": "open" };
|
|
7429
|
+
/** 固さの小札。語彙外は出さない(ビルド側でも捨てている・二重の網) */
|
|
7430
|
+
function firmnessBadge(rec) {
|
|
7431
|
+
var f = rec && rec.firmness;
|
|
7432
|
+
if (typeof f !== "string" || !Object.prototype.hasOwnProperty.call(FIRMNESS_CLASS, f)) return null;
|
|
7433
|
+
return el("span", { class: "badge firmness firmness-" + FIRMNESS_CLASS[f], title: "固さ", text: f });
|
|
7434
|
+
}
|
|
7435
|
+
/** 置き換えられた決定を薄く出すか(論点の札を持つ決定だけ。欄の無い記録の見た目は変えない) */
|
|
7436
|
+
function fadedClass(rec) {
|
|
7437
|
+
return hasTopic(rec) && isRetired(rec) ? " is-faded" : "";
|
|
7438
|
+
}
|
|
7439
|
+
/** 決めた人 1 人の札。関係者が名簿にあればそのページへ、{name, url} は url があれば元の URL へ */
|
|
7440
|
+
function decidedByChip(who) {
|
|
7441
|
+
if (!who || typeof who !== "object") return null;
|
|
7442
|
+
if (typeof who.member === "string") {
|
|
7443
|
+
var rec = byId[who.member];
|
|
7444
|
+
var fallback = who.member.slice("member:".length);
|
|
7445
|
+
if (rec && rec._kind === "generic") {
|
|
7446
|
+
var a = el("a", { class: "chip decided-by-member", href: "?id=" + encodeURIComponent(who.member), text: humanTitle(rec.title) || fallback });
|
|
7447
|
+
a.addEventListener("click", function (ev) {
|
|
7448
|
+
if (ev.defaultPrevented || ev.button !== 0 || ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return;
|
|
7449
|
+
ev.preventDefault();
|
|
7450
|
+
go(who.member);
|
|
7451
|
+
});
|
|
7452
|
+
return a;
|
|
7453
|
+
}
|
|
7454
|
+
return el("span", { class: "chip", text: fallback });
|
|
7455
|
+
}
|
|
7456
|
+
if (typeof who.name !== "string" || !who.name) return null;
|
|
7457
|
+
var raw = typeof who.url === "string" ? who.url.toLowerCase() : "";
|
|
7458
|
+
var url = raw.indexOf("http://") === 0 || raw.indexOf("https://") === 0 ? who.url : "";
|
|
7459
|
+
if (url) return el("a", { class: "chip decided-by-link", href: url, target: "_blank", rel: "noopener", text: who.name });
|
|
7460
|
+
return el("span", { class: "chip", text: who.name });
|
|
7461
|
+
}
|
|
7462
|
+
/** 検索に使う決めた人の表記(decidedBy があればそちら、無ければ deciders) */
|
|
7463
|
+
function deciderWords(d) {
|
|
7464
|
+
if (Array.isArray(d.decidedBy) && d.decidedBy.length) {
|
|
7465
|
+
return d.decidedBy.map(function (w) {
|
|
7466
|
+
if (w && typeof w.member === "string") {
|
|
7467
|
+
var r = byId[w.member];
|
|
7468
|
+
return r && r.title ? r.title : w.member.slice("member:".length);
|
|
7469
|
+
}
|
|
7470
|
+
return w && typeof w.name === "string" ? w.name : "";
|
|
7471
|
+
}).join(" ");
|
|
7472
|
+
}
|
|
7473
|
+
return (d.deciders || []).join(" ");
|
|
7474
|
+
}
|
|
7475
|
+
|
|
7363
7476
|
// 未確認が1件も無い一覧では絞り込みを出さない
|
|
7364
7477
|
function hasUnverified(items) {
|
|
7365
7478
|
return (items || []).some(function (r) { return machineOwned(confirmPathOf(r), r.rstatus); });
|
|
@@ -8680,7 +8793,7 @@ ${calendarAddUrl.toString()}
|
|
|
8680
8793
|
}
|
|
8681
8794
|
}
|
|
8682
8795
|
|
|
8683
|
-
var listState = { tab: DEFAULT_TAB, q: "", category: "", month: "", status: "", page: 1 };
|
|
8796
|
+
var listState = { tab: DEFAULT_TAB, q: "", category: "", month: "", status: "", topic: "", page: 1 };
|
|
8684
8797
|
|
|
8685
8798
|
// 一覧のページネーション(描画コスト対策。1ページあたりの件数)
|
|
8686
8799
|
var PAGE_SIZE = 30;
|
|
@@ -19304,6 +19417,13 @@ ${calendarAddUrl.toString()}
|
|
|
19304
19417
|
if (statusOptsD.length) {
|
|
19305
19418
|
filters.appendChild(buildSelect("すべての状態", statusOptsD, listState.status, function (v) { listState.status = v; listState.page = 1; drawCards(); }, statusLabel));
|
|
19306
19419
|
}
|
|
19420
|
+
// 論点の札(#1094)。札を持つ決定が 1 件も無い案件では出さない
|
|
19421
|
+
var topicOpts = topicOptions(visibleRecords(SITE.decisions));
|
|
19422
|
+
if (topicOpts.length) {
|
|
19423
|
+
var topicSel = buildSelect("すべての論点", topicOpts, listState.topic, function (v) { listState.topic = v; listState.page = 1; drawCards(); });
|
|
19424
|
+
topicSel.classList.add("topic-filter");
|
|
19425
|
+
filters.appendChild(topicSel);
|
|
19426
|
+
}
|
|
19307
19427
|
app.appendChild(filters);
|
|
19308
19428
|
|
|
19309
19429
|
var addBarD = aiAddBar("Decisions");
|
|
@@ -19335,16 +19455,25 @@ ${calendarAddUrl.toString()}
|
|
|
19335
19455
|
var q = listState.q.trim().toLowerCase();
|
|
19336
19456
|
// 削除済みは絞り込みの手前で落とす(分母も一緒に減るので「12/13件」が残らない)
|
|
19337
19457
|
var all = visibleRecords(SITE.decisions);
|
|
19338
|
-
|
|
19458
|
+
var out = all.filter(function (d) {
|
|
19339
19459
|
if (listState.month && d.date.slice(0, 7) !== listState.month) return false;
|
|
19340
19460
|
if (listState.category && d.category !== listState.category) return false;
|
|
19341
19461
|
if (!matchesStatusFilter(d, !!SITE.decisionFollowup)) return false;
|
|
19462
|
+
if (!matchesTopicFilter(d)) return false;
|
|
19342
19463
|
if (q) {
|
|
19343
|
-
var hay = (d.title + " " + d.summary + " " + d.searchText + " " + d
|
|
19464
|
+
var hay = (d.title + " " + d.summary + " " + d.searchText + " " + deciderWords(d)).toLowerCase();
|
|
19344
19465
|
if (hay.indexOf(q) === -1) return false;
|
|
19345
19466
|
}
|
|
19346
19467
|
return true;
|
|
19347
19468
|
});
|
|
19469
|
+
// 同じ論点の中は新しい順(決定日の降順 → ID の降順)
|
|
19470
|
+
if (listState.topic) out.sort(newestDecisionFirst);
|
|
19471
|
+
return out;
|
|
19472
|
+
}
|
|
19473
|
+
/** 論点の絞り込み(#1094)。選んでいなければ全件、選んだら同じ札の決定だけ */
|
|
19474
|
+
function matchesTopicFilter(d) {
|
|
19475
|
+
if (!listState.topic) return true;
|
|
19476
|
+
return hasTopic(d) && d.topic === listState.topic;
|
|
19348
19477
|
}
|
|
19349
19478
|
|
|
19350
19479
|
function drawCards() {
|
|
@@ -19367,6 +19496,7 @@ ${calendarAddUrl.toString()}
|
|
|
19367
19496
|
|
|
19368
19497
|
pg.slice.forEach(function (d) {
|
|
19369
19498
|
var meta = el("div", { class: "card-meta" }, [
|
|
19499
|
+
nowBadge(d),
|
|
19370
19500
|
draftBadge(d),
|
|
19371
19501
|
proposalBadge(d),
|
|
19372
19502
|
followupBadge(d),
|
|
@@ -19374,13 +19504,15 @@ ${calendarAddUrl.toString()}
|
|
|
19374
19504
|
latestBadge(d),
|
|
19375
19505
|
el("span", { class: "mono", text: d.date || "----" }),
|
|
19376
19506
|
catBadge(d.category),
|
|
19507
|
+
firmnessBadge(d),
|
|
19508
|
+
topicBadge(d),
|
|
19377
19509
|
]);
|
|
19378
19510
|
var eb = fmErrorBadge(d);
|
|
19379
19511
|
if (eb) meta.appendChild(eb);
|
|
19380
19512
|
var traceD = el("a", { class: "mc-trace", href: "?tab=graph", text: "🕸 つながり" });
|
|
19381
19513
|
traceD.addEventListener("click", function (ev) { ev.preventDefault(); ev.stopPropagation(); openExplorerFor(d); });
|
|
19382
19514
|
meta.appendChild(traceD);
|
|
19383
|
-
var card = el("div", { class: "card", title: d.id, "data-record-id": d.id }, [
|
|
19515
|
+
var card = el("div", { class: "card" + fadedClass(d) + (isNowDecision(d) ? " is-now" : ""), title: d.id, "data-record-id": d.id }, [
|
|
19384
19516
|
meta,
|
|
19385
19517
|
// 消したレコードは一覧でも取り消し線(次のビルドまで一覧には残る)
|
|
19386
19518
|
cardTitleLink(d, d.deleted ? " is-deleted" : ""),
|
|
@@ -20404,6 +20536,9 @@ ${calendarAddUrl.toString()}
|
|
|
20404
20536
|
// 話題の線(決定・ルール・関係者 → 用語・engine #551 の about)。名前の一致で機械が付けた手がかりなので、
|
|
20405
20537
|
// 根拠の線と同じく細く薄い「ゆるいつながり」として描き、凡例は日本語の 1 行(栓 about_relations の on の条件)
|
|
20406
20538
|
about: { color: "#ddd6fe", label: "話題", kind: "topic" },
|
|
20539
|
+
// 決めた人の線(決定 → 関係者・#1094)。relations ではなく decided_by から引く。
|
|
20540
|
+
// 🔴 凡例には**この線がグラフにあるときだけ**出す(欄の無い案件の凡例を変えない)
|
|
20541
|
+
decided_by: { color: "#f472b6", label: "決めた人", kind: "gold", onlyWhenPresent: true },
|
|
20407
20542
|
};
|
|
20408
20543
|
var WEAK_RELS = {};
|
|
20409
20544
|
var REL_COLOR = {};
|
|
@@ -20471,7 +20606,10 @@ ${calendarAddUrl.toString()}
|
|
|
20471
20606
|
// 線(関係)の色凡例。グラフ内の有無に関わらず全種別を、Goldどうし/根拠の2群で表示する
|
|
20472
20607
|
[["gold", "線(Goldどうし):"], ["evidence", "線(根拠):"], ["topic", "線(話題):"]].forEach(function (pair) {
|
|
20473
20608
|
var kind = pair[0], heading = pair[1];
|
|
20474
|
-
var keys = Object.keys(REL_META).filter(function (r) {
|
|
20609
|
+
var keys = Object.keys(REL_META).filter(function (r) {
|
|
20610
|
+
if (REL_META[r].kind !== kind) return false;
|
|
20611
|
+
return !REL_META[r].onlyWhenPresent || graph.edges.some(function (e) { return e.rel === r; });
|
|
20612
|
+
});
|
|
20475
20613
|
if (!keys.length) return;
|
|
20476
20614
|
legend.appendChild(el("span", { class: "sep", text: heading }));
|
|
20477
20615
|
keys.forEach(function (r) {
|
|
@@ -20603,6 +20741,12 @@ ${calendarAddUrl.toString()}
|
|
|
20603
20741
|
(inAdj[e.b.id] = inAdj[e.b.id] || []).push(e.a.id);
|
|
20604
20742
|
});
|
|
20605
20743
|
|
|
20744
|
+
// 描き直しの間引き(engine #551 のカクつき対策)。操作・検索・期間の切り替え・ホバーの波の間は毎フレーム描き、
|
|
20745
|
+
// それが済んで止まっている間は、星の瞬き(ゆっくり)のためだけに IDLE_FRAME_MS ごとに描く
|
|
20746
|
+
var IDLE_FRAME_MS = 100;
|
|
20747
|
+
var activeUntil = 0;
|
|
20748
|
+
function poke(ms) { activeUntil = Math.max(activeUntil, performance.now() + ms); }
|
|
20749
|
+
|
|
20606
20750
|
// 検索: 一致ノード(matched)が光り、その隣接(lit)は残し、他は減光する
|
|
20607
20751
|
var matched = {}, lit = {}, searching = false;
|
|
20608
20752
|
// 期間(engine #551)を選んでいるときは、その期間に生まれた・変わった記録だけが一致になる(キーワードと両方なら両方を満たす記録)
|
|
@@ -20611,6 +20755,7 @@ ${calendarAddUrl.toString()}
|
|
|
20611
20755
|
var range = graphPeriodRange(graphState.period.unit === "all" ? null : graphState.period);
|
|
20612
20756
|
searching = q.length > 0 || !!range;
|
|
20613
20757
|
matched = {}; lit = {};
|
|
20758
|
+
poke(250);
|
|
20614
20759
|
if (!searching) {
|
|
20615
20760
|
countEl.textContent = "点 " + nodes.length + " / つながり " + edges.length;
|
|
20616
20761
|
return;
|
|
@@ -20659,6 +20804,8 @@ ${calendarAddUrl.toString()}
|
|
|
20659
20804
|
var UPSTREAM_HOPS = 1; // 参照元(入辺)を辿る深さ
|
|
20660
20805
|
var SWEEP_STEP_MS = 120; // 波が1段(1ホップ)進むごとの間隔
|
|
20661
20806
|
var SWEEP_GROW_MS = 200; // 辺・点が灯り切るまでの時間
|
|
20807
|
+
// ホバーの波が流れ切るまで毎フレーム描く時間(参照元 1 段+参照先 2 段=順番 3 まで+灯り切る時間に余裕を見る)
|
|
20808
|
+
var HOVER_SWEEP_MS = (DOWNSTREAM_HOPS + UPSTREAM_HOPS + 2) * SWEEP_STEP_MS + SWEEP_GROW_MS + 300;
|
|
20662
20809
|
function computeFitTarget() {
|
|
20663
20810
|
if (!nodes.length) return null;
|
|
20664
20811
|
var minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
|
|
@@ -20686,7 +20833,10 @@ ${calendarAddUrl.toString()}
|
|
|
20686
20833
|
view.x += (t.x - view.x) * 0.18;
|
|
20687
20834
|
view.y += (t.y - view.y) * 0.18;
|
|
20688
20835
|
}
|
|
20836
|
+
// まだ目に見えて動いているか(止まっていれば描き直しを間引ける)
|
|
20837
|
+
return Math.abs(t.scale - view.scale) > view.scale * 0.0005 || Math.abs(t.x - view.x) > 0.1 || Math.abs(t.y - view.y) > 0.1;
|
|
20689
20838
|
}
|
|
20839
|
+
var fitMoving = true;
|
|
20690
20840
|
|
|
20691
20841
|
// ロード時に1回だけ: Goldの中心から外へ「ふわっ」と光が広がるイントロ。
|
|
20692
20842
|
// 金色(#fcd34d)はホバー連鎖・検索の専用色なので、イントロは寒色で別物として見せる。
|
|
@@ -20700,6 +20850,7 @@ ${calendarAddUrl.toString()}
|
|
|
20700
20850
|
resetBtn.addEventListener("click", function () {
|
|
20701
20851
|
autoFit = true; // 全ノードが収まる表示へ滑らかに戻す
|
|
20702
20852
|
resize();
|
|
20853
|
+
poke(250);
|
|
20703
20854
|
if (!reduceMotion) introStart = performance.now(); // 中心から光が広がるイントロを再生
|
|
20704
20855
|
});
|
|
20705
20856
|
canvas.parentNode.appendChild(resetBtn);
|
|
@@ -20834,6 +20985,96 @@ ${calendarAddUrl.toString()}
|
|
|
20834
20985
|
return n.recordId && n.glow !== null ? 0.4 + 0.6 * n.glow : 1;
|
|
20835
20986
|
}
|
|
20836
20987
|
|
|
20988
|
+
// 点の光(にじみ・shadowBlur)は、点ごとに毎フレーム描くと 700 点で 1 フレーム 30ms を超える(engine #551 のカクつき)。
|
|
20989
|
+
// にじみだけを小さな絵に前もって描いて使い回し、本体の円はその上に正確な半径で描く。
|
|
20990
|
+
// 絵は画面の画素(device px)で作る(shadowBlur は座標変換に影響されない=元の描き方と同じ幅)。
|
|
20991
|
+
// 半径は 0.25px・にじみの幅は小さいうちは 0.5 刻み、大きいと約 4% 刻みに丸めて、絵の数を抑える
|
|
20992
|
+
// (小さい点のにじみの明るさは円の面積に比例するので、1px 刻みでは引いた表示で明るさが ±数十% ずれる)
|
|
20993
|
+
var TAU = Math.PI * 2;
|
|
20994
|
+
// 使い回しの置き場は「最近使った順」の上限つき(上限を超えたら一番古いものから捨てる=ズームし続けても一度に作り直さない)
|
|
20995
|
+
function spriteStore(max) {
|
|
20996
|
+
var m = new Map();
|
|
20997
|
+
return {
|
|
20998
|
+
get: function (k) { var v = m.get(k); if (v) { m.delete(k); m.set(k, v); } return v; },
|
|
20999
|
+
set: function (k, v) { m.set(k, v); if (m.size > max) m.delete(m.keys().next().value); },
|
|
21000
|
+
};
|
|
21001
|
+
}
|
|
21002
|
+
var haloCache = spriteStore(800);
|
|
21003
|
+
// これより大きいにじみ(大きく寄ったとき・画面にわずかな点しか無い)は絵にせず直接描く(大きな絵を溜めない)
|
|
21004
|
+
var HALO_SPRITE_MAX = 512;
|
|
21005
|
+
function haloBlurStep(blur) {
|
|
21006
|
+
if (blur < 0.25) return 0;
|
|
21007
|
+
if (blur <= 12) return Math.round(blur * 2) / 2;
|
|
21008
|
+
return Math.round(12 * Math.pow(1.04, Math.round(Math.log(blur / 12) / Math.log(1.04))) * 2) / 2;
|
|
21009
|
+
}
|
|
21010
|
+
function haloSprite(color, R, blur) {
|
|
21011
|
+
var bq = haloBlurStep(blur);
|
|
21012
|
+
if (!bq) return null;
|
|
21013
|
+
var rq = Math.max(0.25, Math.round(R * 4) / 4);
|
|
21014
|
+
var half = Math.ceil(rq + bq * 1.5 + 2); // にじみ(σ=blur/2)が見える範囲 ≒ 3σ
|
|
21015
|
+
if (half * 2 > HALO_SPRITE_MAX) return false;
|
|
21016
|
+
var key = color + "|" + rq + "|" + bq;
|
|
21017
|
+
var sp = haloCache.get(key);
|
|
21018
|
+
if (sp) return sp;
|
|
21019
|
+
var size = half * 2;
|
|
21020
|
+
var c = document.createElement("canvas");
|
|
21021
|
+
c.width = size; c.height = size;
|
|
21022
|
+
var g = c.getContext("2d");
|
|
21023
|
+
if (!g) return null;
|
|
21024
|
+
// 円は絵の外(左へ size ずらす)に描き、影だけを絵の中央へ落とす=にじみだけが残る
|
|
21025
|
+
g.shadowColor = color;
|
|
21026
|
+
g.shadowBlur = bq;
|
|
21027
|
+
g.shadowOffsetX = size;
|
|
21028
|
+
g.fillStyle = color;
|
|
21029
|
+
g.beginPath();
|
|
21030
|
+
g.arc(half - size, half, rq, 0, TAU);
|
|
21031
|
+
g.fill();
|
|
21032
|
+
sp = { c: c, h: half };
|
|
21033
|
+
haloCache.set(key, sp);
|
|
21034
|
+
return sp;
|
|
21035
|
+
}
|
|
21036
|
+
// X・Y は画素の境目に揃えた値を渡す(半端な位置だと絵の引き伸ばしが入り、1 枚ごとに何倍も重くなる)
|
|
21037
|
+
function drawHalo(color, R, blur, X, Y) {
|
|
21038
|
+
var sp = haloSprite(color, R, blur);
|
|
21039
|
+
if (sp) { ctx.drawImage(sp.c, X - sp.h, Y - sp.h); return; }
|
|
21040
|
+
if (sp === false) {
|
|
21041
|
+
ctx.save();
|
|
21042
|
+
ctx.shadowColor = color;
|
|
21043
|
+
ctx.shadowBlur = blur;
|
|
21044
|
+
ctx.fillStyle = color;
|
|
21045
|
+
ctx.beginPath();
|
|
21046
|
+
ctx.arc(X, Y, R, 0, TAU);
|
|
21047
|
+
ctx.fill();
|
|
21048
|
+
ctx.restore();
|
|
21049
|
+
}
|
|
21050
|
+
}
|
|
21051
|
+
// ラベルも文字を絵にして使い回す(検索で数百のラベルを毎フレーム文字から描くと 1 フレーム 100ms を超える)。
|
|
21052
|
+
// 文字の大きさは画面の画素で一定(11px)なので、ズームしても同じ絵が使える
|
|
21053
|
+
var labelCache = spriteStore(3000);
|
|
21054
|
+
function labelFont(bold) { return (bold ? "600 " : "") + (11 * dpr) + "px 'Noto Sans JP', sans-serif"; }
|
|
21055
|
+
function drawLabel(text, color, bold, X, baseY) {
|
|
21056
|
+
var key = color + "|" + (bold ? 1 : 0) + "|" + text;
|
|
21057
|
+
var sp = labelCache.get(key);
|
|
21058
|
+
if (!sp) {
|
|
21059
|
+
var font = labelFont(bold);
|
|
21060
|
+
var c = document.createElement("canvas");
|
|
21061
|
+
var g = c.getContext("2d");
|
|
21062
|
+
if (!g) return;
|
|
21063
|
+
g.font = font;
|
|
21064
|
+
var asc = Math.ceil(13 * dpr), desc = Math.ceil(4 * dpr); // 11px の文字の上下+少しの余白
|
|
21065
|
+
c.width = Math.max(1, Math.ceil(g.measureText(text).width) + 2);
|
|
21066
|
+
c.height = asc + desc;
|
|
21067
|
+
g.font = font; // 大きさを変えると描き方の設定が戻るので、もう一度
|
|
21068
|
+
g.fillStyle = color;
|
|
21069
|
+
g.fillText(text, 0, asc);
|
|
21070
|
+
sp = { c: c, asc: asc };
|
|
21071
|
+
labelCache.set(key, sp);
|
|
21072
|
+
}
|
|
21073
|
+
ctx.drawImage(sp.c, Math.round(X), Math.round(baseY - sp.asc));
|
|
21074
|
+
}
|
|
21075
|
+
// ホバーの連鎖(参照先・参照元)は触った点が変わったときだけ辿り直す
|
|
21076
|
+
var linFor = null, linCache = null;
|
|
21077
|
+
|
|
20837
21078
|
function draw() {
|
|
20838
21079
|
var rect = canvas.getBoundingClientRect();
|
|
20839
21080
|
var now = performance.now();
|
|
@@ -20862,7 +21103,7 @@ ${calendarAddUrl.toString()}
|
|
|
20862
21103
|
ctx.fill();
|
|
20863
21104
|
});
|
|
20864
21105
|
ctx.globalAlpha = 1;
|
|
20865
|
-
|
|
21106
|
+
fitMoving = autoFit ? !!applyFit(false) : false; // 全ノードが収まる位置・倍率へ滑らかに追従
|
|
20866
21107
|
ctx.translate(view.x, view.y);
|
|
20867
21108
|
ctx.scale(view.scale, view.scale);
|
|
20868
21109
|
|
|
@@ -20882,7 +21123,8 @@ ${calendarAddUrl.toString()}
|
|
|
20882
21123
|
}
|
|
20883
21124
|
return hop;
|
|
20884
21125
|
}
|
|
20885
|
-
if (hovered)
|
|
21126
|
+
if (hovered && linFor === hovered) lin = linCache;
|
|
21127
|
+
else if (hovered) {
|
|
20886
21128
|
var downHop = bfsDir(outAdj, DOWNSTREAM_HOPS); // 参照先
|
|
20887
21129
|
var upHop = bfsDir(inAdj, UPSTREAM_HOPS); // 参照元
|
|
20888
21130
|
var maxUp = 0;
|
|
@@ -20897,6 +21139,7 @@ ${calendarAddUrl.toString()}
|
|
|
20897
21139
|
if (id === hovered.id || lin[id] !== undefined) return;
|
|
20898
21140
|
lin[id] = { order: maxUp + downHop[id], hop: downHop[id], dir: "down" };
|
|
20899
21141
|
});
|
|
21142
|
+
linFor = hovered; linCache = lin;
|
|
20900
21143
|
}
|
|
20901
21144
|
function hopAlpha(d) {
|
|
20902
21145
|
return d <= 1 ? 1 : Math.max(0.6, 1 - d * 0.1);
|
|
@@ -20914,8 +21157,15 @@ ${calendarAddUrl.toString()}
|
|
|
20914
21157
|
relates_to: "rgba(148,163,184,0.6)", supersedes: "rgba(248,113,113,0.9)",
|
|
20915
21158
|
reference: "rgba(180,196,230,0.32)", source: "rgba(180,196,230,0.32)", link: "rgba(180,196,230,0.22)",
|
|
20916
21159
|
about: "rgba(196,181,253,0.22)",
|
|
21160
|
+
decided_by: "rgba(244,114,182,0.75)",
|
|
20917
21161
|
};
|
|
21162
|
+
// 画面に入っている範囲(世界座標)。両端が同じ側の外にある線は描かない
|
|
21163
|
+
var em = 4 / view.scale; // 線の太さの分の余白(画面で 4px)
|
|
21164
|
+
var wx0 = -view.x / view.scale - em, wy0 = -view.y / view.scale - em;
|
|
21165
|
+
var wx1 = (rect.width - view.x) / view.scale + em, wy1 = (rect.height - view.y) / view.scale + em;
|
|
20918
21166
|
edges.forEach(function (e) {
|
|
21167
|
+
if ((e.a.x < wx0 && e.b.x < wx0) || (e.a.x > wx1 && e.b.x > wx1) ||
|
|
21168
|
+
(e.a.y < wy0 && e.b.y < wy0) || (e.a.y > wy1 && e.b.y > wy1)) return;
|
|
20919
21169
|
// ホバー中: リネージに入った辺だけ、波の順に「order の低い側→高い側」へ伸びながら金色に。
|
|
20920
21170
|
if (hovered) {
|
|
20921
21171
|
var la = lin[e.a.id], lb = lin[e.b.id];
|
|
@@ -20955,6 +21205,10 @@ ${calendarAddUrl.toString()}
|
|
|
20955
21205
|
ctx.stroke();
|
|
20956
21206
|
});
|
|
20957
21207
|
|
|
21208
|
+
// 点は画面の画素の座標で描く(にじみの絵をそのまま置くため)
|
|
21209
|
+
var k = view.scale * dpr, ox = view.x * dpr, oy = view.y * dpr;
|
|
21210
|
+
var cw = canvas.width, ch = canvas.height;
|
|
21211
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
20958
21212
|
nodes.forEach(function (n) {
|
|
20959
21213
|
var style = GROUP_STYLE[n.group] || GROUP_STYLE.phantom;
|
|
20960
21214
|
var alphaN = 1;
|
|
@@ -20977,46 +21231,45 @@ ${calendarAddUrl.toString()}
|
|
|
20977
21231
|
}
|
|
20978
21232
|
var dim = alphaN < 0.3;
|
|
20979
21233
|
var isGold = !!n.recordId;
|
|
21234
|
+
var r0 = nodeRadius(n);
|
|
21235
|
+
// 画面の画素での位置と半径(ラベル・輪・にじみはすべて画素で描く)
|
|
21236
|
+
// 中心は画素の境目に揃える(にじみの絵と本体の円の中心を合わせる。dpr 2 なら 0.25px 以内の差)
|
|
21237
|
+
var X = Math.round(n.x * k + ox), Y = Math.round(n.y * k + oy), R = r0 * k;
|
|
21238
|
+
var inChain = hovered && n !== hovered && lin[n.id] !== undefined;
|
|
21239
|
+
var blur =
|
|
21240
|
+
(isGold ? (n.glow === null || matched[n.id] ? Math.max(16, n.glow === null ? 16 : 6 + 22 * n.glow) : 6 + 22 * n.glow) : 7) * (dim ? 0.3 : 1) * (inChain ? 1.7 : 1) * (1 + introFlash * 1.8);
|
|
21241
|
+
// ラベル: Gold層レコードはズームイン時のみ表示(引いて全体表示中は出さない)。
|
|
21242
|
+
// それ以外はホバーの連鎖に入ったときか検索ヒット時のみ(ズームに関係なく表示)。
|
|
21243
|
+
var showLabel = (n.recordId && view.scale >= LABEL_MIN_SCALE) || (hovered && lin[n.id] !== undefined && sweepRamp(lin[n.id]) > 0.15) || (searching && lit[n.id]);
|
|
21244
|
+
// 画面の外の点は描かない(にじみの届く幅・輪・右に出るラベルの幅を見込む)
|
|
21245
|
+
var reach = R + Math.max(blur, 18) * 1.5 + 6 * dpr;
|
|
21246
|
+
if (X + reach + (showLabel ? 300 * dpr : 0) < 0 || X - reach > cw || Y + reach < 0 || Y - reach > ch) return;
|
|
20980
21247
|
// Gold以外の星はゆっくり瞬く
|
|
20981
21248
|
var tw = isGold ? 1 : 0.72 + 0.28 * (0.5 + 0.5 * Math.sin(now * 0.0017 + n.phase));
|
|
20982
21249
|
ctx.globalAlpha = alphaN * tw * glowAlpha(n);
|
|
20983
|
-
var r0 = nodeRadius(n);
|
|
20984
21250
|
var fill = isGold ? style.color : "#c9d6f2";
|
|
20985
21251
|
// 検索一致ノードとホバー中のノードは金色のリングで発光させる
|
|
20986
21252
|
if ((matched[n.id] || n === hovered) && !dim) {
|
|
20987
|
-
|
|
20988
|
-
ctx.shadowColor = "#fcd34d";
|
|
20989
|
-
ctx.shadowBlur = 18;
|
|
21253
|
+
drawHalo("#fcd34d", R + 2 * dpr, 18, X, Y);
|
|
20990
21254
|
ctx.beginPath();
|
|
20991
|
-
ctx.arc(
|
|
21255
|
+
ctx.arc(X, Y, R + 2 * dpr, 0, TAU);
|
|
20992
21256
|
ctx.fillStyle = fill;
|
|
20993
21257
|
ctx.fill();
|
|
20994
|
-
ctx.restore();
|
|
20995
21258
|
ctx.strokeStyle = "#fcd34d";
|
|
20996
|
-
ctx.lineWidth = 2.5
|
|
21259
|
+
ctx.lineWidth = 2.5 * dpr;
|
|
20997
21260
|
ctx.beginPath();
|
|
20998
|
-
ctx.arc(
|
|
21261
|
+
ctx.arc(X, Y, R + 3.5 * dpr, 0, TAU);
|
|
20999
21262
|
ctx.stroke();
|
|
21000
21263
|
}
|
|
21001
21264
|
// 星本体(色のにじみ=グロー付き)
|
|
21002
|
-
|
|
21003
|
-
ctx.save();
|
|
21004
|
-
ctx.shadowColor =
|
|
21005
|
-
introFlash > 0.01 ? "#bcd0ff" : isGold ? style.color : "#9fb4e8";
|
|
21006
|
-
ctx.shadowBlur =
|
|
21007
|
-
(isGold ? (n.glow === null || matched[n.id] ? Math.max(16, n.glow === null ? 16 : 6 + 22 * n.glow) : 6 + 22 * n.glow) : 7) * (dim ? 0.3 : 1) * (inChain ? 1.7 : 1) * (1 + introFlash * 1.8);
|
|
21265
|
+
drawHalo(introFlash > 0.01 ? "#bcd0ff" : isGold ? style.color : "#9fb4e8", R, blur, X, Y);
|
|
21008
21266
|
ctx.beginPath();
|
|
21009
|
-
ctx.arc(
|
|
21267
|
+
ctx.arc(X, Y, R, 0, TAU);
|
|
21010
21268
|
ctx.fillStyle = fill;
|
|
21011
21269
|
ctx.fill();
|
|
21012
|
-
|
|
21013
|
-
// ラベル: Gold層レコードはズームイン時のみ表示(引いて全体表示中は出さない)。
|
|
21014
|
-
// それ以外はホバーの連鎖に入ったときか検索ヒット時のみ(ズームに関係なく表示)。
|
|
21015
|
-
if ((n.recordId && view.scale >= LABEL_MIN_SCALE) || (hovered && lin[n.id] !== undefined && sweepRamp(lin[n.id]) > 0.15) || (searching && lit[n.id])) {
|
|
21016
|
-
ctx.fillStyle = dim ? "#3d4763" : matched[n.id] ? "#fde68a" : isGold ? "#e7edfb" : "#9fb0d6";
|
|
21017
|
-
ctx.font = (matched[n.id] ? "600 " : "") + (11 / view.scale) + "px 'Noto Sans JP', sans-serif";
|
|
21270
|
+
if (showLabel) {
|
|
21018
21271
|
var label = n.label.length > 24 ? n.label.slice(0, 23) + "…" : n.label;
|
|
21019
|
-
|
|
21272
|
+
drawLabel(label, dim ? "#3d4763" : matched[n.id] ? "#fde68a" : isGold ? "#e7edfb" : "#9fb0d6", !!matched[n.id], X + R + 4 * dpr, Y + 4 * dpr);
|
|
21020
21273
|
}
|
|
21021
21274
|
});
|
|
21022
21275
|
ctx.globalAlpha = 1;
|
|
@@ -21054,7 +21307,7 @@ ${calendarAddUrl.toString()}
|
|
|
21054
21307
|
return;
|
|
21055
21308
|
}
|
|
21056
21309
|
var nh = nodeAt(ev.clientX, ev.clientY);
|
|
21057
|
-
if (nh !== hovered) { hovered = nh; hoverStart = nh ? performance.now() : 0; } // 別ノードでスイープ再生
|
|
21310
|
+
if (nh !== hovered) { hovered = nh; hoverStart = nh ? performance.now() : 0; poke(HOVER_SWEEP_MS); } // 別ノードでスイープ再生
|
|
21058
21311
|
canvas.style.cursor = hovered ? "pointer" : "grab";
|
|
21059
21312
|
});
|
|
21060
21313
|
canvas.addEventListener("mousedown", function (ev) {
|
|
@@ -21086,6 +21339,7 @@ ${calendarAddUrl.toString()}
|
|
|
21086
21339
|
view.x = mx - (mx - view.x) * factor;
|
|
21087
21340
|
view.y = my - (my - view.y) * factor;
|
|
21088
21341
|
view.scale *= factor;
|
|
21342
|
+
poke(250);
|
|
21089
21343
|
}, { passive: false });
|
|
21090
21344
|
|
|
21091
21345
|
// ビルド時に座標計算済みなら、ウォームアップ(重い同期計算)を完全に省略する=開いた瞬間ゼロ待ち。
|
|
@@ -21104,10 +21358,17 @@ ${calendarAddUrl.toString()}
|
|
|
21104
21358
|
if (!running || !document.body.contains(canvas)) { running = false; return; }
|
|
21105
21359
|
// 収束後(alpha減衰しきり・無操作)は O(n²) の物理計算 tick を止める。これが「固まり続ける」主因。
|
|
21106
21360
|
// ドラッグ/パン中は動かす必要があるので回す。描画(draw)はホバー演出・イントロのため毎フレーム続ける。
|
|
21107
|
-
|
|
21108
|
-
|
|
21361
|
+
var moving = alpha > 0.004 || draggingNode || panning;
|
|
21362
|
+
if (moving) tick();
|
|
21363
|
+
// 動いている・操作の直後・イントロ・自動フレーミングの途中は毎フレーム描く。止まっている間は瞬きのためだけに間引いて描く(engine #551)
|
|
21364
|
+
var now = performance.now();
|
|
21365
|
+
if (moving || introStart || fitMoving || now < activeUntil || now - lastDrawAt >= IDLE_FRAME_MS) {
|
|
21366
|
+
lastDrawAt = now;
|
|
21367
|
+
draw();
|
|
21368
|
+
}
|
|
21109
21369
|
requestAnimationFrame(loop);
|
|
21110
21370
|
}
|
|
21371
|
+
var lastDrawAt = 0;
|
|
21111
21372
|
loop();
|
|
21112
21373
|
}
|
|
21113
21374
|
|
|
@@ -21148,12 +21409,14 @@ ${calendarAddUrl.toString()}
|
|
|
21148
21409
|
app.appendChild(head);
|
|
21149
21410
|
|
|
21150
21411
|
var meta = el("div", { class: "detail-meta" }, [
|
|
21412
|
+
nowBadge(d),
|
|
21151
21413
|
draftBadge(d),
|
|
21152
21414
|
proposalBadge(d),
|
|
21153
21415
|
retiredBadge(d),
|
|
21154
21416
|
latestBadge(d),
|
|
21155
21417
|
el("span", { class: "mono", text: d.date || "----" }),
|
|
21156
21418
|
catBadge(d.category),
|
|
21419
|
+
firmnessBadge(d),
|
|
21157
21420
|
]);
|
|
21158
21421
|
meta.title = d.id;
|
|
21159
21422
|
meta.setAttribute("data-record-id", d.id);
|
|
@@ -21173,7 +21436,18 @@ ${calendarAddUrl.toString()}
|
|
|
21173
21436
|
app.appendChild(el("div", { class: "summary-box", text: d.summary }));
|
|
21174
21437
|
}
|
|
21175
21438
|
|
|
21176
|
-
|
|
21439
|
+
// 論点(#1094)。札の無い決定では何も作らない
|
|
21440
|
+
if (hasTopic(d)) {
|
|
21441
|
+
app.appendChild(el("div", { class: "section-label", text: "論点" }));
|
|
21442
|
+
app.appendChild(el("div", { class: "decision-topic" }, [el("span", { class: "chip", text: d.topic })]));
|
|
21443
|
+
}
|
|
21444
|
+
|
|
21445
|
+
// 決めた人(#1094)。decided_by に読めた人がいればそちらを出し、いなければ(欄が無い・空・全部崩れ)今どおり deciders
|
|
21446
|
+
var whoChips = Array.isArray(d.decidedBy) ? d.decidedBy.map(decidedByChip).filter(Boolean) : [];
|
|
21447
|
+
if (whoChips.length) {
|
|
21448
|
+
app.appendChild(el("div", { class: "section-label", text: "決めた人" }));
|
|
21449
|
+
app.appendChild(el("div", { class: "deciders decided-by" }, whoChips));
|
|
21450
|
+
} else if (d.deciders && d.deciders.length) {
|
|
21177
21451
|
app.appendChild(el("div", { class: "section-label", text: "決定者" }));
|
|
21178
21452
|
app.appendChild(el("div", { class: "deciders" }, d.deciders.map(function (p) {
|
|
21179
21453
|
return el("span", { class: "chip", text: p });
|
|
@@ -21222,6 +21496,7 @@ ${calendarAddUrl.toString()}
|
|
|
21222
21496
|
based_on: "根拠", derived_from: "由来", relates_to: "関連",
|
|
21223
21497
|
supersedes: "置換", superseded_by: "置換先", reference: "参照", source: "出典", link: "リンク",
|
|
21224
21498
|
about: "話題",
|
|
21499
|
+
decided_by: "決めた人",
|
|
21225
21500
|
};
|
|
21226
21501
|
var explorerState = { folder: null, path: [], q: "", sx: 0, autoRight: false };
|
|
21227
21502
|
// 索引もグラフ本体と同じ材料(削除済みを除いたもの)から作る。別々に間引くと、
|
|
@@ -21234,7 +21509,7 @@ ${calendarAddUrl.toString()}
|
|
|
21234
21509
|
(adjDir[e.from] = adjDir[e.from] || []).push({ id: e.to, rel: e.rel, dir: "out" });
|
|
21235
21510
|
(adjDir[e.to] = adjDir[e.to] || []).push({ id: e.from, rel: e.rel, dir: "in" });
|
|
21236
21511
|
});
|
|
21237
|
-
var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "source", "reference", "link", "about"];
|
|
21512
|
+
var REL_ORDER = ["supersedes", "based_on", "derived_from", "relates_to", "decided_by", "source", "reference", "link", "about"];
|
|
21238
21513
|
|
|
21239
21514
|
function recordText(rec) {
|
|
21240
21515
|
if (!rec) return "";
|
package/package.json
CHANGED