@wcstack/devtools 4.0.0-rc.6 → 4.0.0-rc.8

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/index.d.ts CHANGED
@@ -341,7 +341,12 @@ interface IWiringEntry {
341
341
  */
342
342
  readonly stateElementRef: WeakRef<object> | null;
343
343
  }
344
- type CoreChangeKind = "sources" | "roster" | "wiring" | "timeline" | "coverage";
344
+ /**
345
+ * Core の変化の種類。`values` は「状態の値が変わったかもしれない」の合図で、更新バッチ
346
+ * (`state:update-batch`)ごとに流れる。timeline の一時停止(paused)とは独立 —
347
+ * 一時停止が止めるのは記録であって観測ではないので、State ペインは追従し続ける。
348
+ */
349
+ type CoreChangeKind = "sources" | "roster" | "wiring" | "timeline" | "coverage" | "values";
345
350
  type CoreChangeListener = (kind: CoreChangeKind) => void;
346
351
  /**
347
352
  * 配線カバレッジ 1 行(static-wiring-dx-design.md §4 — 宣言 × 実測の突合)。
@@ -512,6 +517,10 @@ declare class WcsDevtools extends HTMLElement {
512
517
  private _expanded;
513
518
  private _hotkeyHandler;
514
519
  private _pickHandler;
520
+ /** State ペインで開いているインライン編集の入力欄(無ければ null)。 */
521
+ private _editInput;
522
+ /** State ペインの中でポインタが押されている間 true(離すまで再描画を待つ)。 */
523
+ private _statePointerDown;
515
524
  get core(): DevtoolsCore | null;
516
525
  connectedCallback(): void;
517
526
  disconnectedCallback(): void;
@@ -530,9 +539,20 @@ declare class WcsDevtools extends HTMLElement {
530
539
  private _exitPickMode;
531
540
  private _markDirty;
532
541
  private _renderDirty;
542
+ /**
543
+ * State ペインの描き直しを待つべきか: インライン編集の入力欄にフォーカスがあるか、
544
+ * ペインの中でポインタが押されている。描き直すと入力欄(打ちかけの値)や押した行が消える。
545
+ * フォーカスで見るのは、パネルを閉じる・ノードを外すなど blur の来ない抜け方でも
546
+ * 待ち続けないため。
547
+ */
548
+ private _stateHeld;
549
+ /** 待たせていた State ペインの描き直しを流す(保留が無ければ何もしない)。 */
550
+ private _releaseState;
551
+ private _holdStateWhilePressed;
533
552
  private _rosterKey;
534
553
  private _selectedRoster;
535
554
  private _renderStatePane;
555
+ private _renderStateBody;
536
556
  /**
537
557
  * 選択ツリーの鍵付き購読セクション(keyedSubscriptions — protocol v2 追補・要件 D17)。
538
558
  * `$eq` / `$eqPath` / `$eqIndex` の購読は依存グラフの動的な変化で、上の状態ツリーにも
package/dist/index.esm.js CHANGED
@@ -775,6 +775,9 @@ class DevtoolsCore {
775
775
  if (total === 0) {
776
776
  return;
777
777
  }
778
+ // 値の変化の合図(State ペインの丸ごとの再読 — devtools-tag-design.md §10 G-U1)。
779
+ // timeline の行より先に、一時停止中でも流す
780
+ this._notify("values");
778
781
  const rest = total > labels.length ? `, …(${total})` : "";
779
782
  this._appendTimeline({
780
783
  sourceId,
@@ -1123,7 +1126,29 @@ header {
1123
1126
  border-bottom: 1px solid #2b4f78;
1124
1127
  background: #10263f;
1125
1128
  }
1126
- header .title { font-weight: 700; color: #9fd0ff; margin-right: 4px; }
1129
+ /* The header never grows wider than the panel: the title and the middle group shrink (the
1130
+ group scrolls sideways when even that is not enough), so dock and × always stay in view —
1131
+ the right dock is only 420px wide. */
1132
+ header .title {
1133
+ flex: 0 1 auto;
1134
+ min-width: 0;
1135
+ overflow: hidden;
1136
+ text-overflow: ellipsis;
1137
+ white-space: nowrap;
1138
+ font-weight: 700;
1139
+ color: #9fd0ff;
1140
+ margin-right: 4px;
1141
+ }
1142
+ header .tools {
1143
+ flex: 1 1 auto;
1144
+ min-width: 0;
1145
+ display: flex;
1146
+ gap: 6px;
1147
+ align-items: center;
1148
+ overflow-x: auto;
1149
+ }
1150
+ header .tools select { flex: 0 1 auto; min-width: 6em; text-overflow: ellipsis; }
1151
+ header .tools button, header > button { flex: none; white-space: nowrap; }
1127
1152
  header select, header button {
1128
1153
  font: inherit;
1129
1154
  background: #16324f;
@@ -1134,7 +1159,6 @@ header select, header button {
1134
1159
  cursor: pointer;
1135
1160
  }
1136
1161
  header button[aria-pressed="true"] { background: #2b5d8f; }
1137
- header .spacer { flex: 1; }
1138
1162
  .panes { display: flex; flex: 1; min-height: 0; }
1139
1163
  .pane { flex: 1; min-width: 0; overflow: auto; padding: 6px 8px; border-right: 1px solid #1d3a5c; }
1140
1164
  .pane:last-child { border-right: none; }
@@ -1238,6 +1262,10 @@ class WcsDevtools extends HTMLElement {
1238
1262
  _expanded = new Set();
1239
1263
  _hotkeyHandler = null;
1240
1264
  _pickHandler = null;
1265
+ /** State ペインで開いているインライン編集の入力欄(無ければ null)。 */
1266
+ _editInput = null;
1267
+ /** State ペインの中でポインタが押されている間 true(離すまで再描画を待つ)。 */
1268
+ _statePointerDown = false;
1241
1269
  get core() {
1242
1270
  return this._core;
1243
1271
  }
@@ -1267,6 +1295,13 @@ class WcsDevtools extends HTMLElement {
1267
1295
  this._markDirty("wiring");
1268
1296
  }
1269
1297
  }
1298
+ else if (kind === "values") {
1299
+ // 値が変わった: State ペインを丸ごと読み直す(devtools-tag-design.md §10 G-U1)。
1300
+ // 閉じている間は読まない — 開いたときに _applyOpen が描き直す
1301
+ if (this.open) {
1302
+ this._markDirty("state");
1303
+ }
1304
+ }
1270
1305
  else {
1271
1306
  this._markDirty("timeline");
1272
1307
  }
@@ -1327,6 +1362,11 @@ class WcsDevtools extends HTMLElement {
1327
1362
  title.className = "title";
1328
1363
  title.textContent = "wcstack devtools";
1329
1364
  header.append(title);
1365
+ // 中間のグループ(ツリー選択・pick・⏸・🗑)。幅が足りないときはここが縮んで横に
1366
+ // スクロールし、dock と × はヘッダーの右端に残る
1367
+ const tools = document.createElement("div");
1368
+ tools.className = "tools";
1369
+ header.append(tools);
1330
1370
  const stateSelect = document.createElement("select");
1331
1371
  stateSelect.title = "state element";
1332
1372
  stateSelect.addEventListener("change", () => {
@@ -1336,9 +1376,9 @@ class WcsDevtools extends HTMLElement {
1336
1376
  this._markDirty("state");
1337
1377
  this._markDirty("wiring");
1338
1378
  });
1339
- header.append(stateSelect);
1379
+ tools.append(stateSelect);
1340
1380
  this._stateSelect = stateSelect;
1341
- const pickButton = this._headerButton(header, "⌖ pick", "pick a page element");
1381
+ const pickButton = this._headerButton(tools, "⌖ pick", "pick a page element");
1342
1382
  pickButton.addEventListener("click", () => {
1343
1383
  if (this._pickMode) {
1344
1384
  this._exitPickMode();
@@ -1349,7 +1389,7 @@ class WcsDevtools extends HTMLElement {
1349
1389
  pickButton.setAttribute("aria-pressed", String(this._pickMode));
1350
1390
  });
1351
1391
  pickButton.dataset["role"] = "pick";
1352
- const pauseButton = this._headerButton(header, "⏸", "pause timeline");
1392
+ const pauseButton = this._headerButton(tools, "⏸", "pause timeline");
1353
1393
  pauseButton.addEventListener("click", () => {
1354
1394
  const core = this._core;
1355
1395
  if (core === null) {
@@ -1359,14 +1399,11 @@ class WcsDevtools extends HTMLElement {
1359
1399
  pauseButton.setAttribute("aria-pressed", String(core.paused));
1360
1400
  });
1361
1401
  pauseButton.dataset["role"] = "pause";
1362
- const clearButton = this._headerButton(header, "🗑", "clear timeline");
1402
+ const clearButton = this._headerButton(tools, "🗑", "clear timeline");
1363
1403
  clearButton.addEventListener("click", () => {
1364
1404
  this._core?.clearTimeline();
1365
1405
  });
1366
1406
  clearButton.dataset["role"] = "clear";
1367
- const spacer = document.createElement("span");
1368
- spacer.className = "spacer";
1369
- header.append(spacer);
1370
1407
  const dockButton = this._headerButton(header, "dock", "toggle dock position");
1371
1408
  dockButton.addEventListener("click", () => {
1372
1409
  const next = (this.getAttribute("dock") ?? "bottom") === "bottom" ? "right" : "bottom";
@@ -1394,6 +1431,11 @@ class WcsDevtools extends HTMLElement {
1394
1431
  panes.append(pane);
1395
1432
  this._paneElements[name] = body;
1396
1433
  }
1434
+ // 押している間に State ペインを描き直すと、押した行が消えてクリックが成立しない
1435
+ // (値が毎フレーム変わるページ)。離すまで待つ
1436
+ this._paneElements["state"].parentElement.addEventListener("pointerdown", () => {
1437
+ this._holdStateWhilePressed();
1438
+ });
1397
1439
  panel.append(panes);
1398
1440
  shadow.append(panel);
1399
1441
  this._panel = panel;
@@ -1402,12 +1444,12 @@ class WcsDevtools extends HTMLElement {
1402
1444
  shadow.append(highlightLayer);
1403
1445
  this._highlightLayer = highlightLayer;
1404
1446
  }
1405
- _headerButton(header, label, title) {
1447
+ _headerButton(container, label, title) {
1406
1448
  const button = document.createElement("button");
1407
1449
  button.textContent = label;
1408
1450
  button.title = title;
1409
1451
  button.setAttribute("aria-pressed", "false");
1410
- header.append(button);
1452
+ container.append(button);
1411
1453
  return button;
1412
1454
  }
1413
1455
  // --- open/close/dock/hotkey ---
@@ -1518,7 +1560,13 @@ class WcsDevtools extends HTMLElement {
1518
1560
  const dirty = this._dirtyPanes;
1519
1561
  this._dirtyPanes = new Set();
1520
1562
  if (dirty.has("state")) {
1521
- this._renderStatePane();
1563
+ if (this._stateHeld()) {
1564
+ // 編集中・押下中は今の DOM のまま。印だけ残し、終わったときに描く(_releaseState)
1565
+ this._dirtyPanes.add("state");
1566
+ }
1567
+ else {
1568
+ this._renderStatePane();
1569
+ }
1522
1570
  }
1523
1571
  if (dirty.has("wiring")) {
1524
1572
  this._renderWiringPane();
@@ -1527,6 +1575,37 @@ class WcsDevtools extends HTMLElement {
1527
1575
  this._renderTimelinePane();
1528
1576
  }
1529
1577
  }
1578
+ /**
1579
+ * State ペインの描き直しを待つべきか: インライン編集の入力欄にフォーカスがあるか、
1580
+ * ペインの中でポインタが押されている。描き直すと入力欄(打ちかけの値)や押した行が消える。
1581
+ * フォーカスで見るのは、パネルを閉じる・ノードを外すなど blur の来ない抜け方でも
1582
+ * 待ち続けないため。
1583
+ */
1584
+ _stateHeld() {
1585
+ if (this._statePointerDown) {
1586
+ return true;
1587
+ }
1588
+ // 外れた(描き直しで消えた)入力欄はフォーカスを持てないので、これだけで足りる
1589
+ return this._editInput !== null && this.shadowRoot.activeElement === this._editInput;
1590
+ }
1591
+ /** 待たせていた State ペインの描き直しを流す(保留が無ければ何もしない)。 */
1592
+ _releaseState() {
1593
+ if (this._dirtyPanes.has("state")) {
1594
+ this._markDirty("state");
1595
+ }
1596
+ }
1597
+ _holdStateWhilePressed() {
1598
+ this._statePointerDown = true;
1599
+ // 離した場所はペインの外かもしれない(ドラッグ)。window で受けて、受けたら外す
1600
+ const release = () => {
1601
+ window.removeEventListener("pointerup", release, true);
1602
+ window.removeEventListener("pointercancel", release, true);
1603
+ this._statePointerDown = false;
1604
+ this._releaseState();
1605
+ };
1606
+ window.addEventListener("pointerup", release, true);
1607
+ window.addEventListener("pointercancel", release, true);
1608
+ }
1530
1609
  _rosterKey(entry) {
1531
1610
  return `${entry.sourceId}:${entry.label}`;
1532
1611
  }
@@ -1540,8 +1619,18 @@ class WcsDevtools extends HTMLElement {
1540
1619
  return found ?? roster[0];
1541
1620
  }
1542
1621
  _renderStatePane() {
1543
- const core = this._core;
1622
+ // 描き直しで今の入力欄は消える(編集中ならここへは来ない — _stateHeld)
1623
+ this._editInput = null;
1544
1624
  const body = this._paneElements["state"];
1625
+ // スクロール位置は描き直しの前後で保つ。中身を空にした瞬間にレイアウトが走ると
1626
+ // (値の読み出しがページの getter を通る)スクロールが 0 に詰められうる
1627
+ const scroller = body.parentElement;
1628
+ const scrollTop = scroller.scrollTop;
1629
+ this._renderStateBody(body);
1630
+ scroller.scrollTop = scrollTop;
1631
+ }
1632
+ _renderStateBody(body) {
1633
+ const core = this._core;
1545
1634
  const select = this._stateSelect;
1546
1635
  const roster = core.getRoster();
1547
1636
  const selected = this._selectedRoster();
@@ -1773,6 +1862,7 @@ class WcsDevtools extends HTMLElement {
1773
1862
  const input = document.createElement("input");
1774
1863
  input.value = typeof current === "string" ? current : String(current);
1775
1864
  const commit = () => {
1865
+ this._editInput = null;
1776
1866
  // 編集は通常のリアクティブパイプラインを通る(devtools-tag-design.md §3.1)
1777
1867
  this._core?.writeValue(entry, ref.path, coerceInput(input.value), ref.indexes);
1778
1868
  this._markDirty("state");
@@ -1782,10 +1872,17 @@ class WcsDevtools extends HTMLElement {
1782
1872
  commit();
1783
1873
  }
1784
1874
  else if (event.key === "Escape") {
1875
+ this._editInput = null;
1785
1876
  this._markDirty("state");
1786
1877
  }
1787
1878
  });
1879
+ // フォーカスが外れたら、待たせていた描き直しを流す(入力欄はそれで消える = 取り消し)。
1880
+ // 待たせていなければ入力欄はそのまま残る(従来どおり)
1881
+ input.addEventListener("blur", () => {
1882
+ this._releaseState();
1883
+ });
1788
1884
  row.replaceChild(input, valueSpan);
1885
+ this._editInput = input;
1789
1886
  input.focus();
1790
1887
  }
1791
1888
  _renderWiringPane() {