@takagaki/cortex-decisions-viewer 0.4.84 → 0.4.85

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 (2) hide show
  1. package/dist/render.js +217 -92
  2. package/package.json +1 -1
package/dist/render.js CHANGED
@@ -1402,7 +1402,7 @@ a.ds-name:hover { text-decoration: underline; }
1402
1402
  button.ops-card-btn { cursor: pointer; }
1403
1403
  button.ops-card-btn:disabled { opacity: .6; cursor: default; }
1404
1404
  /* ---- 投入モーダル ---- */
1405
- .modal-overlay {
1405
+ .modal-overlay, .confirm-overlay {
1406
1406
  position: fixed; inset: 0; z-index: 100; display: flex; align-items: center; justify-content: center;
1407
1407
  background: rgba(16,24,40,0.45); padding: 20px;
1408
1408
  -webkit-backdrop-filter: blur(2px); backdrop-filter: blur(2px);
@@ -1434,6 +1434,12 @@ button.ops-card-btn:disabled { opacity: .6; cursor: default; }
1434
1434
  background: var(--surface); color: var(--muted); font-size: 13px; font-weight: 600; cursor: pointer;
1435
1435
  }
1436
1436
  .modal-cancel:hover { background: var(--bg); }
1437
+ /* 確認モーダル。能力モーダルの上に重なるので z-index は上(下地は inert で塞ぐ) */
1438
+ .confirm-overlay { z-index: 110; }
1439
+ .confirm-box { max-width: 420px; gap: 10px; }
1440
+ /* 貼ったURLを1行で出すので、はみ出させない(箱は420px・モバイルは実質335px) */
1441
+ .confirm-line { font-size: 14px; color: var(--text); line-height: 1.75; margin: 0; white-space: pre-wrap; overflow-wrap: anywhere; }
1442
+ .confirm-line:first-child { font-weight: 700; }
1437
1443
  .modal-ok { color: #047857; font-weight: 600; }
1438
1444
  .modal-status-error { color: #dc2626; font-weight: 600; }
1439
1445
  .modal-paths { margin-top: 8px; display: flex; flex-direction: column; gap: 4px; }
@@ -2283,7 +2289,7 @@ ${capabilityState.toString()}
2283
2289
  // **何が読み取られるのかを必ず書く。** 一覧に並ぶものと読み取られるものの関係は、
2284
2290
  // 書いていないと分からない(チャットは「載っていれば全部」、開発はリポジトリ単位でOFFできる)
2285
2291
  if (cap === "チャット") {
2286
- m.modal.appendChild(el("p", { class: "modal-desc", text: "ここに並んでいるチャンネルは、例外なく全て毎晩の Gold 昇格が読み取り、顧客も見る Decision・用語集の材料になります。読み取りを止めるには「登録を解除」してください。" }));
2292
+ m.modal.appendChild(el("p", { class: "modal-desc", text: "ここに並んでいるチャンネルは、例外なく全て毎晩の Gold 昇格が読み取り、Decision・用語集の材料になります。読み取りを止めるには「登録を解除」してください。" }));
2287
2293
  } else if (cap === "開発") {
2288
2294
  m.modal.appendChild(el("p", { class: "modal-desc", text: "ここに並んでいるリポジトリの Issues を毎晩の Gold 昇格が読み取ります。リポジトリ単位で読み取りを止められます(止めたものは読み取られません)。" }));
2289
2295
  }
@@ -2979,7 +2985,7 @@ ${capabilityState.toString()}
2979
2985
 
2980
2986
  // 注記は全能力の後ろに1つ。**何が Gold の材料になるのかを1文で言う。**
2981
2987
  if (anyCap) {
2982
- sec.appendChild(el("p", { class: "ds-note", text: "登録されている Slack チャンネルと GitHub リポジトリは、毎晩の Gold 昇格が読み取り、顧客も見る Decision・用語集の材料になります。チャンネルは登録を解除すると、リポジトリは読み取りを止めると、以後は読み取られません。" }));
2988
+ sec.appendChild(el("p", { class: "ds-note", text: "登録されている Slack チャンネルと GitHub リポジトリは、毎晩の Gold 昇格が読み取り、Decision・用語集の材料になります。チャンネルは登録を解除すると、リポジトリは読み取りを止めると、以後は読み取られません。" }));
2983
2989
  }
2984
2990
 
2985
2991
  if (pipes.length) {
@@ -3427,11 +3433,11 @@ ${capabilityState.toString()}
3427
3433
  ]);
3428
3434
  if (!editable) return row;
3429
3435
  var btn = el("button", { class: "ds-toggle-btn ds-danger", type: "button", text: "この接続先を外す" });
3430
- btn.addEventListener("click", function () {
3436
+ btn.addEventListener("click", async function () {
3431
3437
  // **最後の1件かどうかは「押した時点」で数える。** 描画時の件数で決めると、
3432
3438
  // 2件を1件ずつ外したとき最後の1件で確認が出ない(Figma で踏んだのと同じ)
3433
3439
  var last = targets.length === 1;
3434
- if (!confirmDanger(removeTargetConfirmLines(t, last))) { msg.textContent = ""; return; }
3440
+ if (!(await confirmDanger(removeTargetConfirmLines(t, last), { returnTo: btn }))) { msg.textContent = ""; return; }
3435
3441
  var payload = {
3436
3442
  op: "backlog-target-remove",
3437
3443
  // **projectKey では送らない。** body の projectKey は案件キー(cortex の
@@ -3487,25 +3493,7 @@ ${capabilityState.toString()}
3487
3493
  title: "+ 接続先を追加",
3488
3494
  placeholder: "https://<スペース>.backlog.jp/projects/<プロジェクトキー>",
3489
3495
  requiredNote: "BacklogのURLを入力してください(プロジェクトキーが入った形。数字だけのURLは受け付けません)。",
3490
- hint: "Backlog でこの案件のプロジェクトを開き、ブラウザのURLをそのまま貼ってください(課題の画面のURLでも構いません)。APIキーは下の「Backlog のAPIキー」から別に登録します。",
3491
- confirmLines: function (v) {
3492
- // **解析した結果を見せる。** 貼ったURLをそのまま出すと、どのプロジェクトを
3493
- // 指すことになるかが確定前に分からない。実際 /projects/PJ-CORTEX は
3494
- // ハイフンで切れて PJ と解釈される(黙って別プロジェクトを指す)
3495
- var t = backlogTargetOf(v);
3496
- return [
3497
- "この案件が同期する Backlog プロジェクトを追加します。",
3498
- "",
3499
- t ? t.domain + " / " + t.projectKey : v,
3500
- "",
3501
- "このプロジェクトの課題・ドキュメント・Wiki が、次の同期から取り込まれます。",
3502
- "貼ったURLがこの案件のものか、もう一度確認してください。",
3503
- "",
3504
- "※ 増えるのは同期先だけです。ビューアからの起票先は変わりません。リアルタイム同期を使う場合は、追加したプロジェクトの Backlog 側にも案件専用URLの登録が要ります。",
3505
- "",
3506
- "追加しますか?",
3507
- ];
3508
- },
3496
+ hint: "Backlog でこの案件のプロジェクトを開き、ブラウザのURLをそのまま貼ってください(課題の画面のURLでも構いません)。APIキーは下の「Backlog のAPIキー」から別に登録します。増えるのは同期先だけで、ビューアからの起票先は変わりません。リアルタイム同期を使う場合は、追加したプロジェクトの Backlog 側にも案件専用URLの登録が要ります。",
3509
3497
  build: function (url) {
3510
3498
  return { op: "backlog-target-add", url: url, baseGeneration: targetsGenerationOf(targets) };
3511
3499
  },
@@ -3646,17 +3634,6 @@ ${capabilityState.toString()}
3646
3634
  namePlaceholder: "メモ(任意)",
3647
3635
  requiredNote: "フォルダのURLを入力してください。",
3648
3636
  hint: "このフォルダの中身が 共有資料 にコピーされ、Markdown 化されます。取り込み用アカウントをそのフォルダの閲覧者に招待しておいてください(招待が無いと読めません)。",
3649
- confirmLines: function (v) {
3650
- return [
3651
- "このフォルダの中身を、顧客も見るリポジトリにコピーします。",
3652
- "",
3653
- "貼ったURLがこの案件のものか、もう一度確認してください。",
3654
- "",
3655
- v,
3656
- "",
3657
- "追加しますか?",
3658
- ];
3659
- },
3660
3637
  build: function (url, name) { return { op: "add-drive-folder", url: url, name: name }; },
3661
3638
  }));
3662
3639
  // **ON/OFFのトグルは出さない。** フォルダを追加した時点で取り込みは始まる
@@ -3755,17 +3732,6 @@ ${capabilityState.toString()}
3755
3732
  namePlaceholder: "メモ(任意)",
3756
3733
  requiredNote: "FigmaのURLを入力してください。",
3757
3734
  hint: "このファイルの画面インベントリ(画面名・Figmaへのリンク)が毎晩同期されます。画像は取り込まれません。",
3758
- confirmLines: function (v) {
3759
- return [
3760
- "このFigmaファイルの画面名とFigmaへのリンクを、顧客も見るリポジトリに取り込みます(画像は取り込みません)。",
3761
- "",
3762
- "貼ったURLがこの案件のものか、もう一度確認してください。",
3763
- "",
3764
- v,
3765
- "",
3766
- "追加しますか?",
3767
- ];
3768
- },
3769
3735
  build: function (url, name) { return { op: "add-figma-file", url: url, name: name }; },
3770
3736
  }));
3771
3737
  used = true;
@@ -3959,30 +3925,8 @@ ${capabilityState.toString()}
3959
3925
  // **招待の話を先頭に置く。** Slack の仕様で、ボットは**公開チャンネルでも**招待されるまで
3960
3926
  // 中身を1件も読めない(conversations.history が not_in_channel を返す・2026-08-16 実測+公式ドキュメント)。
3961
3927
  // そのあとに「追加=Gold 昇格の対象」を置く(手順と、その手順の重さ)。
3962
- hint: "先に Slack でそのチャンネルを開き、下のコマンドを貼って Cortex を招待してください。公開チャンネルでも招待が必要で、招待していないと中身を読めません。そのあと、チャンネル名を右クリック →「リンクをコピー」で出るURLを下に貼ります。追加したチャンネルは毎晩の Gold 昇格の対象になり、顧客も見る Decision・用語集の材料になります(読み取りを止めるには、追加した行の「登録を解除」を押します)。招待前に登録すると名前は「名前未取得」になるので、招待してから追加するのがおすすめです。",
3928
+ hint: "先に Slack でそのチャンネルを開き、下のコマンドを貼って Cortex を招待してください。公開チャンネルでも招待が必要で、招待していないと中身を読めません。そのあと、チャンネル名を右クリック →「リンクをコピー」で出るURLを下に貼ります。追加したチャンネルは毎晩の Gold 昇格の対象になり、Decision・用語集の材料になります(読み取りを止めるには、追加した行の「登録を解除」を押します)。招待前に登録すると名前は「名前未取得」になるので、招待してから追加するのがおすすめです。",
3963
3929
  copyText: "/invite @Cortex",
3964
- // 撤去した Gold トグルの確認文の趣旨をここへ移した。**追加が昇格の意思決定そのもの**
3965
- // なので、社内限定の会話が顧客可視の層に出てしまう話はここで伝えるしかない
3966
- // (実際にその事故が起きている)。あわせて他案件の取り違えも確認させる。
3967
- confirmLines: function (v) {
3968
- return [
3969
- "このチャンネルの内容を、顧客も見る Decision・用語集に蒸留するようになります。",
3970
- "",
3971
- v,
3972
- "",
3973
- "社内限定の会話が含まれるチャンネルでは、顧客に見せられない内容が出てしまう可能性があります。",
3974
- "顧客がこのチャンネルにいる(=すでに会話を見ている)場合にだけ追加してください。",
3975
- "",
3976
- "貼ったURLがこの案件のものか、もう一度確認してください。",
3977
- "",
3978
- // Slack の仕様(公開チャンネルでも招待が要る)を、最後にもう一度置く。
3979
- // 「追加したのに何も取り込まれない」の原因のほとんどがこれ。
3980
- "※ Slack でこのチャンネルに Cortex を招待していないと、中身は読めません。",
3981
- "※ 今夜の蒸留(22時ごろ)までに登録を解除すれば取り込まれません。一度生成された記録は自動では消えません。",
3982
- "",
3983
- "追加しますか?",
3984
- ];
3985
- },
3986
3930
  // **選んだワークスペースを送る(#104)。** 空文字=社内ワークスペースなのでキーごと落とす
3987
3931
  // (サーバ側は「未宣言・null・空」を区別しない約束で、無いことで既定を表す)
3988
3932
  build: function (url, name, workspace) {
@@ -3991,6 +3935,29 @@ ${capabilityState.toString()}
3991
3935
  return payload;
3992
3936
  },
3993
3937
  workspacePicker: true,
3938
+ // **別案件でも使われていたときに出す確認(2026-08-20)。**
3939
+ //
3940
+ // **追加のときに出る唯一のモーダル。** 同日に「追加のたびの確認」を廃止した
3941
+ // (全部取り込む方針にし、顧客に見せるかは別途フラグで判定するため)ので、
3942
+ // ここで止まるのは「他の案件と重複したとき」だけ。
3943
+ // だから**何が起きるかもここで言う**(どちらの案件の顧客にも出ること)。
3944
+ //
3945
+ // **他案件の名前は出さない。** この画面の操作はその案件の Basic 認証を通れる人
3946
+ // (顧客を含む)なら誰でもできるので、案件名の列挙に使わせない。
3947
+ // **貼ったURLを本文に出す。** 「URLを確かめてください」と言うのに出ていなかった
3948
+ duplicateConfirmLines: function (value) {
3949
+ return [
3950
+ "このチャンネルは、別の案件でも追加されています。",
3951
+ "",
3952
+ "このまま追加すると、同じ会話が両方の案件の Decision・用語集に出ます。",
3953
+ "",
3954
+ "全体定例のように複数の案件に関係するチャンネルなら、これは正しい状態です。",
3955
+ "別のチャンネルと取り違えていないか、貼ったURLを確かめてください:",
3956
+ String(value || ""),
3957
+ "",
3958
+ "追加しますか?",
3959
+ ];
3960
+ },
3994
3961
  adder: "slack",
3995
3962
  }));
3996
3963
 
@@ -4001,18 +3968,6 @@ ${capabilityState.toString()}
4001
3968
  namePlaceholder: "メモ(任意)",
4002
3969
  requiredNote: "リポジトリを入力してください。",
4003
3970
  hint: "このリポジトリの Issues が、毎晩の Gold 昇格の材料になります。",
4004
- confirmLines: function (v) {
4005
- return [
4006
- "このリポジトリの Issues を、Gold 昇格の材料に追加します。",
4007
- "",
4008
- "追加した時点で読み取りの対象になります。",
4009
- "Issues の内容から、顧客も見る Decision・用語集が作られます。",
4010
- "",
4011
- v,
4012
- "",
4013
- "追加しますか?",
4014
- ];
4015
- },
4016
3971
  build: function (repo, name) { return { op: "add-github-source", repo: repo, name: name }; },
4017
3972
  adder: "github",
4018
3973
  }));
@@ -4054,6 +4009,49 @@ ${capabilityState.toString()}
4054
4009
  // 一覧が添えられていれば、断られていても先に画面を追いつかせる(#127)
4055
4010
  var raced = hooks && hooks.onTargets && Array.isArray(j.targets) ? hooks.onTargets(j.targets) : false;
4056
4011
  if (!res.ok) {
4012
+ // **確認すれば覆せる断り方(2026-08-20・Slack の重複だけ)。**
4013
+ //
4014
+ // 全体定例のように複数案件に関係するチャンネルは実在するのに、
4015
+ // 「取り違えていないか確認してください」で終わっていて**確認しても進めなかった**。
4016
+ //
4017
+ // **完全一致で見る。** j.confirmable を truthy で見ると、次に触る人が他の能力にも
4018
+ // 印を足したときに Drive でも確認が出て押せてしまう(サーバは通さないので実害は
4019
+ // 「押しても409」だが、「確認すれば通る」というUIの約束が壊れる)。
4020
+ //
4021
+ // **一度「確認済み」で送って、それでも断られたら二度目は出さない。**
4022
+ //
4023
+ // **理由を正確に書く(実装レビューの指摘)。** 「画面が先に新しくなった時間帯に
4024
+ // 押し続けることになる」は成立しない——古いサーバは印を返さないので、そもそも
4025
+ // 確認が出ない(canConfirm が最初から false)。このガードが本当に守るのは
4026
+ // **サーバを切り戻したとき**と**デプロイ中の版ズレ**。どちらも確認済みの再送だけが
4027
+ // 断られる状態で、そこで出し続けると「はい」を押し続けることになる。
4028
+ // 配る順番はサーバ→画面(これは順序の話とは別に必要)。
4029
+ var canConfirm =
4030
+ res.status === 409 &&
4031
+ j.confirmable === "duplicate" &&
4032
+ j.confirmableKind === "slack" &&
4033
+ hooks &&
4034
+ typeof hooks.onDuplicate === "function" &&
4035
+ payload.confirmDuplicate !== true;
4036
+ if (canConfirm) {
4037
+ // **ボタンは必ず先に戻す。** 確認の前に戻しておかないと、「いいえ」を押した人の
4038
+ // フォームが押せないまま残り、再読み込みするまで追加できなくなる
4039
+ btn.disabled = false;
4040
+ msg.className = "ds-toggle-msg";
4041
+ msg.textContent = "";
4042
+ if (await hooks.onDuplicate()) {
4043
+ // 「はい」——同じ内容に確認の印を付けて送り直す
4044
+ var again = {};
4045
+ for (var k in payload) if (Object.prototype.hasOwnProperty.call(payload, k)) again[k] = payload[k];
4046
+ again.confirmDuplicate = true;
4047
+ await sendConfig(again, msg, btn, onDone, hooks);
4048
+ return;
4049
+ }
4050
+ // 「いいえ」——サーバの文言を出して終わる(ボタンは戻してある)
4051
+ msg.className = "ds-toggle-msg is-error";
4052
+ msg.textContent = "✗ " + (j.message || "保存できませんでした。");
4053
+ return;
4054
+ }
4057
4055
  // サーバの文言をそのまま出す(貼り方の案内・取り違えの警告を含んでいる)。
4058
4056
  // **例外は「他の人が先に変えていた」だけ**——サーバは再読み込みを案内するが、
4059
4057
  // 画面は既に追いついているので、済んでいることをやらせる案内になる(#127)
@@ -4099,9 +4097,116 @@ ${capabilityState.toString()}
4099
4097
  * 書いていたが、取り違えはサーバ側が409で実際に弾く(findRefInOtherProjects)。
4100
4098
  * 実在する守りがあるのに脅し文を重ねると、操作者が萎縮するだけで安全は増えない。
4101
4099
  * 「貼ったURLがこの案件のものか確認してください」で足りる。
4100
+ *
4101
+ * **ただし Slack のチャンネルだけは、この前提が成り立たなくなった(2026-08-20)。**
4102
+ * 重複は 409 で断られたあと**確認すれば覆せる**ので、「サーバが実際に弾く」とは
4103
+ * 言えない。だから2枚目の確認(duplicateConfirmLines)では、重複そのものの事実と
4104
+ * 「両方の案件の顧客が見る場所に出る」ことをはっきり書いている——**そこは脅し文ではなく、
4105
+ * 押す前に知っておくべき事実**。1枚目(confirmLines)は変えていない。
4106
+ * ※ この region はテンプレート文字列の中なので、コメントにもバッククォートを書かない。
4107
+ */
4108
+ function confirmDanger(lines, opts) {
4109
+ return openConfirm(lines, opts);
4110
+ }
4111
+
4112
+ var confirmSeq = 0;
4113
+
4114
+ /**
4115
+ * 画面内の確認モーダル。**window.confirm の置き換え(2026-08-20)。**
4116
+ * 返すのは Promise で、中身は「進めてよいか」。
4117
+ *
4118
+ * ## なぜ makeModal を使わないのか(一番大事)
4119
+ *
4120
+ * **makeModal は開いているモーダルを畳む**(existing.__close(true))。
4121
+ * ところが確認が要る5か所は**すべて能力モーダルの中にいる**——
4122
+ * openCapabilityModal の items.forEach(m.modal.appendChild(n)) は
4123
+ * **ノードの移動**なので、利用者が「追加」を押した時点でフォームはモーダルの子。
4124
+ * makeModal で確認を開くと**親ごと消え、フォームが無くなる**。
4125
+ * しかも sendConfig の再送は掴んだままの msg/btn に書くので、
4126
+ * **追加は成功するのに画面には何も起きない**(利用者はもう一度押す)。
4127
+ *
4128
+ * makeModal の close() は refreshDataSources と能力モーダル専用の戻り先処理に
4129
+ * 絡んでいるため、フラグで分岐させると**あちらを壊すリスクをこちらの都合で背負う**。
4130
+ * 確認は「短い文と2つのボタン」しかないので、小さく別に持つ。
4131
+ *
4132
+ * ## なぜクラスを .modal-overlay にしないのか
4133
+ *
4134
+ * isModalOpen()・expandAllCaps・openCapModal がいずれも .modal-overlay を
4135
+ * 見ている。名前を共有すると「モーダルが開いている間は一覧を差し替えない」等の
4136
+ * 判定に確認が混ざる。見た目だけ CSS のカンマセレクタで揃える。
4137
+ *
4138
+ * ## なぜ Esc を capture で受けて止めるのか
4139
+ *
4140
+ * **makeModal は開いている間ずっと document に keydown を張ったままにする。**
4141
+ * inert は document レベルの keydown を止めない。素直に足すと
4142
+ * **親のリスナが先に登録済みなので先に発火し、Esc 一発で親モーダルが閉じる**
4143
+ * (=上と同じ破壊が Esc 経由で起きる)。capture で受けて stopPropagation すれば
4144
+ * 同一ノードの bubble リスナ=親の onKey には届かない。
4145
+ * オーバーレイ要素だけにリスナを張るのでは足りない(フォーカスが body にあると経路を通らない)。
4146
+ *
4147
+ * @param lines 表示する行(1行1段落)。先頭行が主文=読み上げの名前になる
4148
+ * @param opts { okLabel, returnTo }
4102
4149
  */
4103
- function confirmDanger(lines) {
4104
- return window.confirm(lines.join(String.fromCharCode(10)));
4150
+ function openConfirm(lines, opts) {
4151
+ return new Promise(function (resolve) {
4152
+ // **多重オープンはキャンセル扱いで無音。** 下地を inert にするので通常は起きない。
4153
+ // ここで警告を出しても操作者にできることが無いし、2枚重なって
4154
+ // 「下の1枚が residual で操作を受け付けない」状態のほうが害が大きい
4155
+ if (app.querySelector(".confirm-overlay")) { resolve(false); return; }
4156
+ var o = opts || {};
4157
+ var overlay = el("div", { class: "confirm-overlay" });
4158
+ // 確認は dialog ではなく alertdialog(「注意を促して答えを待つ」もの)
4159
+ var box = el("div", { class: "modal confirm-box", role: "alertdialog", "aria-modal": "true" });
4160
+ confirmSeq += 1;
4161
+ var textId = "confirm-line-" + confirmSeq;
4162
+ (lines || []).forEach(function (line, i) {
4163
+ var pnode = el("p", { class: "confirm-line", text: line });
4164
+ // 先頭行を読み上げの名前にする。見出しは付けない(主文を2回言うことになる)
4165
+ if (i === 0) { pnode.id = textId; box.setAttribute("aria-labelledby", textId); }
4166
+ box.appendChild(pnode);
4167
+ });
4168
+ var cancel = el("button", { class: "modal-cancel", type: "button", text: "キャンセル" });
4169
+ var ok = el("button", { class: "ds-toggle-btn ds-danger", type: "button", text: o.okLabel || "はい" });
4170
+ box.appendChild(el("div", { class: "modal-actions" }, [cancel, ok]));
4171
+ overlay.appendChild(box);
4172
+
4173
+ function finish(answer) {
4174
+ document.removeEventListener("keydown", onKey, true);
4175
+ // **順序が仕様。** ①自分をDOMから外す → ②inertを全部外す →
4176
+ // ③親が残っていれば掛け直す → ④それからフォーカスを戻す
4177
+ if (overlay.parentNode) overlay.parentNode.removeChild(overlay);
4178
+ setBackdropInert(overlay, false);
4179
+ // **親モーダルはまだ開いている。** 全部外したままにすると、その後ろの画面が
4180
+ // 触れるようになり Tab でタブ行へ抜けられる(別タブに移ると close() を
4181
+ // 通らないので keydown が1本残る)
4182
+ var rest = app.querySelector(".modal-overlay");
4183
+ if (rest) setBackdropInert(rest, true);
4184
+ // **戻し先は呼び出し側からもらう。** activeElement を記録する方式は破綻する——
4185
+ // sendConfig は冒頭で btn.disabled = true にしており、実ブラウザでは
4186
+ // フォーカス中の要素を disabled にした時点でフォーカスが body に落ちる。
4187
+ // disabled を戻してもフォーカスは戻らない。**jsdom は落とさないのでテストから見えない。**
4188
+ // Safari はクリックでボタンにフォーカスを当てないので、なおさら明示が要る
4189
+ var back = o.returnTo;
4190
+ if (back && typeof back.focus === "function" && app.contains(back)) back.focus();
4191
+ resolve(answer);
4192
+ }
4193
+ function onKey(e) {
4194
+ if (e.key !== "Escape") return;
4195
+ e.stopPropagation();
4196
+ e.preventDefault();
4197
+ finish(false);
4198
+ }
4199
+ document.addEventListener("keydown", onKey, true);
4200
+ // 外側クリックとキャンセルは「いいえ」。**確認は安全側に倒す**
4201
+ overlay.addEventListener("click", function (e) { if (e.target === overlay) finish(false); });
4202
+ cancel.addEventListener("click", function () { finish(false); });
4203
+ ok.addEventListener("click", function () { finish(true); });
4204
+
4205
+ app.appendChild(overlay);
4206
+ setBackdropInert(overlay, true);
4207
+ // **キャンセルに載せる**(showModal と同じ「押しても何も壊れないものへ」の原則)
4208
+ cancel.focus();
4209
+ });
4105
4210
  }
4106
4211
 
4107
4212
  /**
@@ -4119,10 +4224,10 @@ ${capabilityState.toString()}
4119
4224
  };
4120
4225
  paint();
4121
4226
 
4122
- btn.addEventListener("click", function () {
4227
+ btn.addEventListener("click", async function () {
4123
4228
  var next = !on;
4124
4229
  // 読み始める方向(危険)だけ確認する。止める方向は素通し
4125
- if (next && opts.confirmOn && !confirmDanger(opts.confirmOn)) { msg.textContent = ""; return; }
4230
+ if (next && opts.confirmOn && !(await confirmDanger(opts.confirmOn, { returnTo: btn }))) { msg.textContent = ""; return; }
4126
4231
  sendConfig(opts.build(next), msg, btn, function () {
4127
4232
  // **押した内容をボタンに反映し、逆向きの操作を残す。**
4128
4233
  // **カードごと作り直さない。** 上の msg に出した結果表示が消えてしまう
@@ -4149,8 +4254,8 @@ ${capabilityState.toString()}
4149
4254
  var btn = el("button", { class: "ds-toggle-btn ds-danger", type: "button", text: opts.label || "削除" });
4150
4255
  var msg = el("div", { class: "ds-toggle-msg" });
4151
4256
 
4152
- btn.addEventListener("click", function () {
4153
- if (!confirmDanger(opts.confirmLines())) { msg.textContent = ""; return; }
4257
+ btn.addEventListener("click", async function () {
4258
+ if (!(await confirmDanger(opts.confirmLines(), { returnTo: btn }))) { msg.textContent = ""; return; }
4154
4259
  sendConfig(opts.build(), msg, btn, function () {
4155
4260
  btn.disabled = true;
4156
4261
  if (opts.onDone) opts.onDone();
@@ -4169,7 +4274,7 @@ ${capabilityState.toString()}
4169
4274
  * 2つ目を足せると読めるため、そういう口だけ submitLabel で語を変える。押した結果の
4170
4275
  * 表示(+/追加済み)も同じ理由で語を揃える。
4171
4276
  *
4172
- * @param opts { title, placeholder, hint, needsName, confirmLines, build(values), submitLabel, hooks }
4277
+ * @param opts { title, placeholder, hint, needsName, build(values), submitLabel, hooks }
4173
4278
  * hooks … sendConfig へ渡す追い付き処理を返す関数(接続先の一覧・#127)。
4174
4279
  * **これがある口では、その場の控え(「追加済み・反映中」)を出さない**——
4175
4280
  * 応答で本物の一覧が増えるので、控えを併記すると同じものが2回出る
@@ -4201,7 +4306,7 @@ ${capabilityState.toString()}
4201
4306
  var btn = el("button", { class: "ds-toggle-btn", type: "button", text: submitLabel });
4202
4307
  var msg = el("div", { class: "ds-toggle-msg" });
4203
4308
 
4204
- btn.addEventListener("click", function () {
4309
+ btn.addEventListener("click", async function () {
4205
4310
  if (!main.value.trim()) {
4206
4311
  msg.className = "ds-toggle-msg is-error";
4207
4312
  msg.textContent = "⚠ " + (opts.requiredNote || "入力してください。");
@@ -4212,7 +4317,6 @@ ${capabilityState.toString()}
4212
4317
  msg.textContent = "⚠ 表示名を入力してください。";
4213
4318
  return;
4214
4319
  }
4215
- if (opts.confirmLines && !confirmDanger(opts.confirmLines(main.value.trim()))) { msg.textContent = ""; return; }
4216
4320
  var added = main.value.trim();
4217
4321
  var addedName = name ? name.value.trim() : "";
4218
4322
  var payload = opts.build(added, addedName, wsSelect ? wsSelect.value : "");
@@ -4233,7 +4337,28 @@ ${capabilityState.toString()}
4233
4337
  el("span", { class: "ds-pending-note", text: submitLabel + "済み・反映中" }),
4234
4338
  ]),
4235
4339
  );
4236
- }, opts.hooks ? opts.hooks() : null);
4340
+ }, (function () {
4341
+ // **既存の hooks(接続先の一覧が持つ onTargets)と、重複の確認を1つにまとめる。**
4342
+ // Slack の追加フォームは onTargets を持たないが、重複の確認は要る。
4343
+ // 逆に接続先の一覧(Backlog)は onTargets を持つが確認は要らない——
4344
+ // 共通に足すと、あちらの「他の人が先に変えていた」の判定と混ざる。
4345
+ var h = opts.hooks ? opts.hooks() : null;
4346
+ if (!opts.duplicateConfirmLines) return h;
4347
+ h = h || {};
4348
+ // **返り値が「進めてよいか」(Promise)。** 呼び出し側は必ず await すること——
4349
+ // await を落とすと Promise は truthy なので**確認なしで「はい」扱いになる**。
4350
+ // 2026-08-20 に window.confirm から画面内のモーダルに変えた(openConfirm)。
4351
+ // **戻し先に btn を渡す。** sendConfig が冒頭で btn.disabled = true にしており、
4352
+ // 実ブラウザではその時点でフォーカスが body に落ちるので、
4353
+ // activeElement を記録する方式では戻し先が消える
4354
+ h.onDuplicate = function () {
4355
+ return confirmDanger(opts.duplicateConfirmLines(main.value.trim()), {
4356
+ okLabel: "追加する",
4357
+ returnTo: btn,
4358
+ });
4359
+ };
4360
+ return h;
4361
+ })());
4237
4362
  });
4238
4363
 
4239
4364
  // このセッションで追加したものを並べる場所(リロードすると消える=正式な一覧は上に出る)
@@ -4372,7 +4497,7 @@ ${capabilityState.toString()}
4372
4497
  confirmOn: [
4373
4498
  "このリポジトリの Issues を、Gold 昇格の材料に戻します。",
4374
4499
  "",
4375
- "Issues の内容から、顧客も見る Decision・用語集が作られます。",
4500
+ "Issues の内容から、Decision・用語集が作られます。",
4376
4501
  "",
4377
4502
  s.ref,
4378
4503
  "",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@takagaki/cortex-decisions-viewer",
3
- "version": "0.4.84",
3
+ "version": "0.4.85",
4
4
  "description": "Cortexの意思決定記録(Decisions/*.md)を静的サイトにビルドして閲覧する。各レコードに「Edit on GitHub」リンクを付与する。",
5
5
  "license": "UNLICENSED",
6
6
  "private": false,