dsh-workflow-map 0.3.2 → 0.3.5
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/CHANGELOG.md +131 -6
- package/README.en.md +76 -28
- package/README.md +74 -27
- package/client.js +163 -19
- package/cordis.patch.yml +3 -3
- package/docs/OPEN-SOURCE-GUIDE.md +8 -8
- package/docs/host-compatibility.md +320 -0
- package/docs/screenshots/01-main-dark.png +0 -0
- package/docs/screenshots/02-main-light.png +0 -0
- package/docs/screenshots/03-running-step.png +0 -0
- package/docs/screenshots/04-step-detail.png +0 -0
- package/docs/screenshots/05-hot-inject.png +0 -0
- package/docs/screenshots/06-settings.png +0 -0
- package/package.json +20 -3
- package/src/client/ProgressUnder.js +1 -1
- package/src/client/StepColumn.js +4 -1
- package/src/client/panel.js +97 -4
- package/src/client/pulse.js +11 -1
- package/src/client/store.js +23 -1
- package/src/client/theme.js +27 -11
- package/src/fold.mjs +210 -19
- package/src/orchestrator.mjs +43 -1
package/client.js
CHANGED
|
@@ -198,7 +198,15 @@ window.__ModuleLoader__.load({
|
|
|
198
198
|
// the three branch hues live on the ROOT, so the dashed branches and their round
|
|
199
199
|
// icons share one palette with the rest of the panel (and one place to change it).
|
|
200
200
|
"--wf-chain-read:#5b8cff;--wf-chain-edit:#e0a13a;--wf-chain-tool:#8f7bea;" +
|
|
201
|
+
// `--dsw-alias-bg-layer-1` and `-2` carry the SAME value in the host's light theme (both
|
|
202
|
+
// neutral-bluish-00, i.e. #fff), so a card filled with layer-2 is invisible on the page. Every
|
|
203
|
+
// raised surface in the panel therefore paints one translucent lift over that fill. The lift goes
|
|
204
|
+
// towards ink on a light page and towards white on a dark one; the host marks dark by putting
|
|
205
|
+
// `data-ds-dark-theme` on <body>, so the light values are the defaults and the dark block below
|
|
206
|
+
// only wins when that attribute is present.
|
|
207
|
+
"--wf-lift:rgba(15,23,42,.045);--wf-lift-2:rgba(15,23,42,.085);" +
|
|
201
208
|
"font-family:ui-sans-serif,system-ui,-apple-system,'Segoe UI','Microsoft YaHei',sans-serif;font-size:12px;line-height:1.5}",
|
|
209
|
+
"body[data-ds-dark-theme] .wf-root{--wf-lift:rgba(255,255,255,.035);--wf-lift-2:rgba(255,255,255,.07)}",
|
|
202
210
|
|
|
203
211
|
/* card: fills the main area; the header and the settings card keep their own height,
|
|
204
212
|
the two-column body takes the rest. No drag grips: the column has no free edges.
|
|
@@ -224,6 +232,10 @@ window.__ModuleLoader__.load({
|
|
|
224
232
|
".wf-btn:hover{background:var(--dsw-alias-bg-layer-2,#2c3140);color:var(--dsw-alias-label-primary,#e9ecf3)}",
|
|
225
233
|
".wf-btn:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5b8cff);outline-offset:1px}",
|
|
226
234
|
".wf-btn[data-active='true']{color:var(--dsw-alias-brand-primary,#5b8cff);border-color:var(--dsw-alias-brand-primary,#5b8cff)}",
|
|
235
|
+
// The refresh button's one-second acknowledgement. Width is pinned to the glyph's own
|
|
236
|
+
// box so swapping "↻" for "已刷新" cannot shift the header row underneath the pointer.
|
|
237
|
+
".wf-btn-ok{color:var(--dsw-alias-state-success-primary,#3fb950);border-color:var(--dsw-alias-state-success-primary,#3fb950);" +
|
|
238
|
+
"width:auto;min-width:23px;padding:0 6px;font-size:11px}",
|
|
227
239
|
// Hot injection is the one header action that injects text into the RUNNING turn, so it is the
|
|
228
240
|
// one that must never be hunted for: a named, filled chip that keeps its name in Chinese.
|
|
229
241
|
// User feedback: "too small, still have to open it first" - so it grows to 96x30 (the other two in the row stay
|
|
@@ -281,13 +293,14 @@ window.__ModuleLoader__.load({
|
|
|
281
293
|
// `grid-area:1/1` is what makes the triangle and the card share the cell: without it the
|
|
282
294
|
// auto-placement cursor would drop the card into row 2, below the triangle.
|
|
283
295
|
".wf-step-card{grid-area:1/1;display:flex;gap:8px;align-items:flex-start;width:100%;padding:7px 8px;border-radius:8px;text-align:left;" +
|
|
284
|
-
"font:inherit;color:inherit;cursor:pointer;border:1px solid
|
|
285
|
-
"background:var(--dsw-alias-bg-layer-2,#282d3a);
|
|
296
|
+
"font:inherit;color:inherit;cursor:pointer;border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.22));" +
|
|
297
|
+
"background:var(--dsw-alias-bg-layer-2,#282d3a);background-image:linear-gradient(var(--wf-lift),var(--wf-lift));" +
|
|
298
|
+
"border-left:3px solid transparent}",
|
|
286
299
|
".wf-step-card:hover{border-color:var(--dsw-alias-border-l2,rgba(255,255,255,.22));" +
|
|
287
|
-
"background:var(--dsw-alias-bg-layer-1,#232733)}",
|
|
300
|
+
"background:var(--dsw-alias-bg-layer-1,#232733);background-image:linear-gradient(var(--wf-lift-2),var(--wf-lift-2))}",
|
|
288
301
|
".wf-step-card:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5b8cff);outline-offset:1px}",
|
|
289
302
|
".wf-step-card[data-selected='true']{border-color:var(--dsw-alias-brand-primary,#5b8cff);" +
|
|
290
|
-
"background:var(--dsw-alias-bg-layer-1,#232733)}",
|
|
303
|
+
"background:var(--dsw-alias-bg-layer-1,#232733);background-image:linear-gradient(var(--wf-lift-2),var(--wf-lift-2))}",
|
|
291
304
|
".wf-step-card[data-active='true']{border-left-color:var(--dsw-alias-brand-primary,#5b8cff)}",
|
|
292
305
|
".wf-step-ring{flex:0 0 auto;width:18px;height:18px;margin-top:1px;border-radius:50%;" +
|
|
293
306
|
"display:inline-flex;align-items:center;justify-content:center;font-size:11px;line-height:1;" +
|
|
@@ -441,7 +454,7 @@ window.__ModuleLoader__.load({
|
|
|
441
454
|
".wf-branch-icon{flex:0 0 auto;width:" + WF_CHAIN_ICON + "px;height:" + WF_CHAIN_ICON + "px;padding:0;" +
|
|
442
455
|
"border-radius:50%;display:inline-flex;align-items:center;justify-content:center;" +
|
|
443
456
|
"font:inherit;font-size:11px;line-height:1;cursor:pointer;position:relative;" +
|
|
444
|
-
"background:var(--dsw-alias-bg-layer-2,#282d3a);color:var(--dsw-alias-label-secondary,#9aa3b2);" +
|
|
457
|
+
"background:var(--dsw-alias-bg-layer-2,#282d3a);background-image:linear-gradient(var(--wf-lift),var(--wf-lift));color:var(--dsw-alias-label-secondary,#9aa3b2);" +
|
|
445
458
|
"border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.28))}",
|
|
446
459
|
".wf-branch-icon:hover{color:var(--dsw-alias-label-primary,#e9ecf3)}",
|
|
447
460
|
".wf-branch-icon:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5b8cff);outline-offset:2px}",
|
|
@@ -471,12 +484,14 @@ window.__ModuleLoader__.load({
|
|
|
471
484
|
/* node card: icon · title · meta · status dot */
|
|
472
485
|
".wf-node{display:flex;gap:7px;align-items:flex-start;width:100%;padding:6px 8px;border-radius:8px;" +
|
|
473
486
|
"text-align:left;font:inherit;color:inherit;cursor:pointer;" +
|
|
474
|
-
"background:var(--dsw-alias-bg-layer-2,#282d3a);
|
|
487
|
+
"background:var(--dsw-alias-bg-layer-2,#282d3a);background-image:linear-gradient(var(--wf-lift),var(--wf-lift));" +
|
|
488
|
+
"border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.22));" +
|
|
475
489
|
"border-left:3px solid var(--dsw-alias-border-l2,rgba(255,255,255,.2))}",
|
|
476
|
-
".wf-node:hover{border-color:var(--dsw-alias-border-l2,rgba(255,255,255,.24));
|
|
490
|
+
".wf-node:hover{border-color:var(--dsw-alias-border-l2,rgba(255,255,255,.24));" +
|
|
491
|
+
"background:var(--dsw-alias-bg-layer-1,#232733);background-image:linear-gradient(var(--wf-lift-2),var(--wf-lift-2))}",
|
|
477
492
|
".wf-node:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5b8cff);outline-offset:1px}",
|
|
478
493
|
".wf-node[data-selected='true']{border-color:var(--dsw-alias-brand-primary,#5b8cff);" +
|
|
479
|
-
"background:var(--dsw-alias-bg-layer-1,#232733)}",
|
|
494
|
+
"background:var(--dsw-alias-bg-layer-1,#232733);background-image:linear-gradient(var(--wf-lift-2),var(--wf-lift-2))}",
|
|
480
495
|
".wf-node[data-selected='true'] .wf-node-title{font-weight:600}",
|
|
481
496
|
".wf-node[data-kind='tool']{border-left-color:var(--dsw-alias-brand-primary,#5b8cff)}",
|
|
482
497
|
".wf-node[data-kind='report']{border-left-color:var(--dsw-alias-state-success-primary,#45c07a)}",
|
|
@@ -1053,11 +1068,12 @@ window.__ModuleLoader__.load({
|
|
|
1053
1068
|
|
|
1054
1069
|
/* ---- the unit card ---- */
|
|
1055
1070
|
".wf-unit-card{display:grid;grid-template-columns:auto auto minmax(0,1fr);align-items:flex-start;gap:8px;" +
|
|
1056
|
-
"width:100%;padding:10px 12px;border-radius:10px;text-align:left;border:1px solid
|
|
1057
|
-
"background:var(--dsw-alias-bg-layer-2,#282d3a);
|
|
1071
|
+
"width:100%;padding:10px 12px;border-radius:10px;text-align:left;border:1px solid var(--dsw-alias-border-l2,rgba(255,255,255,.22));" +
|
|
1072
|
+
"background:var(--dsw-alias-bg-layer-2,#282d3a);background-image:linear-gradient(var(--wf-lift),var(--wf-lift));" +
|
|
1073
|
+
"color:inherit;font:inherit;cursor:pointer;" +
|
|
1058
1074
|
"border-left:3px solid transparent;box-sizing:border-box}",
|
|
1059
1075
|
".wf-unit-card:hover{border-color:var(--dsw-alias-border-l2,rgba(255,255,255,.22));" +
|
|
1060
|
-
"background:var(--dsw-alias-bg-layer-1,#232733)}",
|
|
1076
|
+
"background:var(--dsw-alias-bg-layer-1,#232733);background-image:linear-gradient(var(--wf-lift-2),var(--wf-lift-2))}",
|
|
1061
1077
|
".wf-unit-card:focus-visible{outline:2px solid var(--dsw-alias-brand-primary,#5b8cff);outline-offset:1px}",
|
|
1062
1078
|
".wf-unit-card[data-selected='true']{border-color:var(--dsw-alias-brand-primary,#5b8cff)}",
|
|
1063
1079
|
".wf-unit-card[data-active='true']{border-left-color:var(--dsw-alias-brand-primary,#5b8cff)}",
|
|
@@ -1370,6 +1386,22 @@ window.__ModuleLoader__.load({
|
|
|
1370
1386
|
return "/dsh-workflow/state?sessionId=" + encodeURIComponent(String(sessionId));
|
|
1371
1387
|
}
|
|
1372
1388
|
|
|
1389
|
+
/**
|
|
1390
|
+
* May the poll loop re-arm after this result?
|
|
1391
|
+
*
|
|
1392
|
+
* A tick aborts the request it replaces, and an aborted request resolves as status -1.
|
|
1393
|
+
* That is the ORDINARY path whenever a poll outlives its interval, so -1 must not end
|
|
1394
|
+
* the loop: it only defers to the replacement that is already in the air. Returning
|
|
1395
|
+
* false there is what keeps two loops from running; returning false for anything else
|
|
1396
|
+
* (a real error, a 4xx) would stop the panel from ever updating again.
|
|
1397
|
+
*
|
|
1398
|
+
* Extracted from the hook so the rule is testable without a renderer.
|
|
1399
|
+
*/
|
|
1400
|
+
function wfPollContinues(result, inflight) {
|
|
1401
|
+
if (!result || result.status !== -1) return true;
|
|
1402
|
+
return wfFinite(inflight) === null ? true : inflight <= 0;
|
|
1403
|
+
}
|
|
1404
|
+
|
|
1373
1405
|
/**
|
|
1374
1406
|
* One conditional GET. Never throws: every outcome is a value, so the loop can
|
|
1375
1407
|
* always decide what to render and how long to wait.
|
|
@@ -1560,6 +1592,9 @@ window.__ModuleLoader__.load({
|
|
|
1560
1592
|
}
|
|
1561
1593
|
}
|
|
1562
1594
|
return {
|
|
1595
|
+
// A finished plan must not decide a card's status any more; the summary carries
|
|
1596
|
+
// the flag so the status policy can stand down without re-reading the phase.
|
|
1597
|
+
terminal: plan.terminal === true,
|
|
1563
1598
|
statuses: plan.statuses && typeof plan.statuses === "object" ? plan.statuses : {},
|
|
1564
1599
|
anchorSeqs: plan.anchorSeqs && typeof plan.anchorSeqs === "object" ? plan.anchorSeqs : {},
|
|
1565
1600
|
skipped: Array.isArray(plan.skipped) ? plan.skipped.slice() : [],
|
|
@@ -2073,7 +2108,10 @@ window.__ModuleLoader__.load({
|
|
|
2073
2108
|
errorText: typeof found.error === "string" ? found.error.slice(0, 200) : "",
|
|
2074
2109
|
seq: wfFinite(found.seq),
|
|
2075
2110
|
error: typeof found.error === "string" && found.error !== "",
|
|
2076
|
-
result:
|
|
2111
|
+
// The host's result boolean is the settled fact: a call that returned with an EMPTY body is
|
|
2112
|
+
// settled. Falling back to resultHead alone marked such a call unsettled, so the panel kept
|
|
2113
|
+
// it in the "calling right now" state and its dot pulsed after the call had already returned.
|
|
2114
|
+
result: found.result === true || (typeof found.resultHead === "string" && found.resultHead !== ""),
|
|
2077
2115
|
durationMs: wfFinite(found.durationMs),
|
|
2078
2116
|
at: wfFinite(found.at),
|
|
2079
2117
|
});
|
|
@@ -2554,7 +2592,17 @@ window.__ModuleLoader__.load({
|
|
|
2554
2592
|
* working session as idle (which would make the curve and the frame look broken).
|
|
2555
2593
|
*/
|
|
2556
2594
|
function wfSessionRunning(view, activeNodeId, nowMs) {
|
|
2557
|
-
|
|
2595
|
+
// The clock is the second witness. view.running is the turn flag, and a turn that never
|
|
2596
|
+
// received its turn/end event (interrupt, crash, a compacted session) leaves it true forever.
|
|
2597
|
+
// Once the last folded event is older than the idle window the session is not working,
|
|
2598
|
+
// whatever the flag says.
|
|
2599
|
+
if (view && typeof view.running === "boolean") {
|
|
2600
|
+
if (view.running !== true) return false;
|
|
2601
|
+
var flagAsOf = wfFinite(view.asOf);
|
|
2602
|
+
var flagNow = wfFinite(nowMs);
|
|
2603
|
+
if (flagAsOf !== null && flagAsOf > 0 && flagNow !== null && flagNow - flagAsOf >= WF_IDLE_AFTER_MS) return false;
|
|
2604
|
+
return true;
|
|
2605
|
+
}
|
|
2558
2606
|
if (typeof activeNodeId === "string" && activeNodeId !== "") return true;
|
|
2559
2607
|
var asOf = wfFinite(view && view.asOf);
|
|
2560
2608
|
if (asOf === null || asOf <= 0) return false;
|
|
@@ -2653,7 +2701,7 @@ window.__ModuleLoader__.load({
|
|
|
2653
2701
|
* "0 seconds ago" would be a fabricated number, which is exactly what this plugin
|
|
2654
2702
|
* refuses to print.
|
|
2655
2703
|
*
|
|
2656
|
-
*
|
|
2704
|
+
* The projected REMAINING half is no longer part of this rail. It used to be shipped
|
|
2657
2705
|
* by the host as a derived millisecond field (median of the finished steps, hard-bounded) and
|
|
2658
2706
|
* rendered as "remaining ~N minutes"; the host no longer computes it and this rail no longer renders it.
|
|
2659
2707
|
* One line, one fact, no consumer-less field — and a payload that still carries that field (an
|
|
@@ -3254,8 +3302,11 @@ window.__ModuleLoader__.load({
|
|
|
3254
3302
|
*/
|
|
3255
3303
|
function wfDeriveStepStatus(row, plan, todoIndex, currentPlanIndex) {
|
|
3256
3304
|
var planIndex = row && wfFinite(row.planIndex) !== null ? Math.round(row.planIndex) : null;
|
|
3305
|
+
// A finished plan (completed / aborted) is history: its statuses and anchor
|
|
3306
|
+
// cursor describe a turn that is over, so the todo table and the host decide.
|
|
3307
|
+
var live = plan && plan.terminal !== true;
|
|
3257
3308
|
|
|
3258
|
-
if (
|
|
3309
|
+
if (live && planIndex !== null) {
|
|
3259
3310
|
// 1. PAE's own statuses are the published truth.
|
|
3260
3311
|
var label = wfHasKey(plan.statuses, String(planIndex)) ? plan.statuses[String(planIndex)] : null;
|
|
3261
3312
|
var fromPae = wfCanonicalStatus(label);
|
|
@@ -7788,6 +7839,9 @@ window.__ModuleLoader__.load({
|
|
|
7788
7839
|
"panel.planning.note": "计划尚未提交,提交后会显示分步。",
|
|
7789
7840
|
"panel.collapse": "收起",
|
|
7790
7841
|
"panel.expand": "展开",
|
|
7842
|
+
"panel.refresh": "刷新",
|
|
7843
|
+
"panel.refresh.hint": "立即重新读取这一步的状态(自动刷新每 0.8 秒一次,这里是不等它的手动兜底)",
|
|
7844
|
+
"panel.refresh.done": "已刷新",
|
|
7791
7845
|
"panel.settings": "设置",
|
|
7792
7846
|
"panel.steps": "总任务分步",
|
|
7793
7847
|
"panel.flow": "分步流程",
|
|
@@ -7998,6 +8052,9 @@ window.__ModuleLoader__.load({
|
|
|
7998
8052
|
"panel.planning.note": "The plan has not been submitted yet; steps appear after it is.",
|
|
7999
8053
|
"panel.collapse": "Collapse",
|
|
8000
8054
|
"panel.expand": "Expand",
|
|
8055
|
+
"panel.refresh": "Refresh",
|
|
8056
|
+
"panel.refresh.hint": "Read this step's state again now (auto-refresh runs every 0.8 s; this is the manual fallback)",
|
|
8057
|
+
"panel.refresh.done": "Refreshed",
|
|
8001
8058
|
"panel.settings": "Settings",
|
|
8002
8059
|
"panel.steps": "Task steps",
|
|
8003
8060
|
"panel.flow": "Step flow",
|
|
@@ -8277,6 +8334,10 @@ window.__ModuleLoader__.load({
|
|
|
8277
8334
|
});
|
|
8278
8335
|
var snap = pair[0];
|
|
8279
8336
|
var setSnap = pair[1];
|
|
8337
|
+
// The running loop's own tick, so the header's refresh button can pull once on
|
|
8338
|
+
// demand without waiting for the interval. A ref, not state: swapping the function
|
|
8339
|
+
// must not re-render, and a click must never restart the effect.
|
|
8340
|
+
var tickRef = React.useRef(null);
|
|
8280
8341
|
|
|
8281
8342
|
React.useEffect(
|
|
8282
8343
|
function () {
|
|
@@ -8289,6 +8350,9 @@ window.__ModuleLoader__.load({
|
|
|
8289
8350
|
var timer = 0;
|
|
8290
8351
|
var controller = null;
|
|
8291
8352
|
var etag = "";
|
|
8353
|
+
// How many requests are in the air. A replaced request resolves as -1 and must
|
|
8354
|
+
// not re-arm while the newer one is still going, or two loops would run.
|
|
8355
|
+
var inflight = 0;
|
|
8292
8356
|
|
|
8293
8357
|
setSnap({ status: "loading", view: null, rev: null, error: "" });
|
|
8294
8358
|
|
|
@@ -8313,11 +8377,20 @@ window.__ModuleLoader__.load({
|
|
|
8313
8377
|
}
|
|
8314
8378
|
controller = typeof AbortController === "function" ? new AbortController() : null;
|
|
8315
8379
|
var signal = controller ? controller.signal : undefined;
|
|
8380
|
+
inflight += 1;
|
|
8316
8381
|
wfPollOnce(window.fetch, wfPollUrl(sessionId), etag, signal).then(function (result) {
|
|
8382
|
+
inflight -= 1;
|
|
8317
8383
|
if (stopped) return;
|
|
8318
|
-
// -1 = this request was aborted
|
|
8319
|
-
//
|
|
8320
|
-
|
|
8384
|
+
// -1 = this request was aborted. A tick aborts the request it replaces, so this
|
|
8385
|
+
// is the ORDINARY path on a slow link, not an exception: returning here without
|
|
8386
|
+
// re-arming stopped the loop for good the first time a poll outlived its tick —
|
|
8387
|
+
// the map then froze while the session kept working, which is the reported
|
|
8388
|
+
// "the map does not update by itself". Only the deliberate stop (stopped) ends it.
|
|
8389
|
+
if (result.status === -1) {
|
|
8390
|
+
// A replaced request: re-arm only when no replacement is still in the air.
|
|
8391
|
+
if (wfPollContinues(result, inflight)) schedule(delay);
|
|
8392
|
+
return;
|
|
8393
|
+
}
|
|
8321
8394
|
if (result.status === 304) {
|
|
8322
8395
|
setSnap(function (prev) {
|
|
8323
8396
|
return prev.view ? { status: "live", view: prev.view, rev: prev.rev, error: "" } : prev;
|
|
@@ -8350,10 +8423,12 @@ window.__ModuleLoader__.load({
|
|
|
8350
8423
|
tick();
|
|
8351
8424
|
}
|
|
8352
8425
|
|
|
8426
|
+
tickRef.current = tick;
|
|
8353
8427
|
tick();
|
|
8354
8428
|
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
8355
8429
|
return function () {
|
|
8356
8430
|
stopped = true;
|
|
8431
|
+
tickRef.current = null;
|
|
8357
8432
|
window.clearTimeout(timer);
|
|
8358
8433
|
if (controller && typeof controller.abort === "function") {
|
|
8359
8434
|
try {
|
|
@@ -8368,7 +8443,18 @@ window.__ModuleLoader__.load({
|
|
|
8368
8443
|
[sessionId]
|
|
8369
8444
|
);
|
|
8370
8445
|
|
|
8371
|
-
|
|
8446
|
+
// One object, not two returns: every caller already reads the snapshot field by field,
|
|
8447
|
+
// so the refresh hook rides along without changing the shape they destructure.
|
|
8448
|
+
return {
|
|
8449
|
+
status: snap.status,
|
|
8450
|
+
view: snap.view,
|
|
8451
|
+
rev: snap.rev,
|
|
8452
|
+
error: snap.error,
|
|
8453
|
+
refresh: function () {
|
|
8454
|
+
var pull = tickRef.current;
|
|
8455
|
+
if (typeof pull === "function") pull();
|
|
8456
|
+
},
|
|
8457
|
+
};
|
|
8372
8458
|
}
|
|
8373
8459
|
|
|
8374
8460
|
/** The stylesheet lives exactly as long as the mount. */
|
|
@@ -8388,6 +8474,25 @@ window.__ModuleLoader__.load({
|
|
|
8388
8474
|
}, []);
|
|
8389
8475
|
}
|
|
8390
8476
|
|
|
8477
|
+
/**
|
|
8478
|
+
* Clear a "just happened" stamp after a moment, so the button returns to its own
|
|
8479
|
+
* glyph. One timer, cleared on unmount and on a second click (the stamp changes).
|
|
8480
|
+
*/
|
|
8481
|
+
function wfUseFlashClear(stamp, clear) {
|
|
8482
|
+
React.useEffect(
|
|
8483
|
+
function () {
|
|
8484
|
+
if (!(stamp > 0)) return undefined;
|
|
8485
|
+
var timer = window.setTimeout(function () {
|
|
8486
|
+
clear();
|
|
8487
|
+
}, 1200);
|
|
8488
|
+
return function () {
|
|
8489
|
+
window.clearTimeout(timer);
|
|
8490
|
+
};
|
|
8491
|
+
},
|
|
8492
|
+
[stamp]
|
|
8493
|
+
);
|
|
8494
|
+
}
|
|
8495
|
+
|
|
8391
8496
|
/** Alt+W toggles the collapsed state; the listener is removed on unmount. */
|
|
8392
8497
|
function wfUseAltW(updatePrefs) {
|
|
8393
8498
|
React.useEffect(function () {
|
|
@@ -8780,6 +8885,15 @@ window.__ModuleLoader__.load({
|
|
|
8780
8885
|
var prefs = prefsHandle.prefs;
|
|
8781
8886
|
var updatePrefs = prefsHandle.update;
|
|
8782
8887
|
var snap = wfUseSnap(sessionId);
|
|
8888
|
+
// When the header's refresh was last clicked; drives the one-word acknowledgement
|
|
8889
|
+
// that clears itself. Deliberately NOT the poll's own state: a click must not look
|
|
8890
|
+
// like a data change.
|
|
8891
|
+
var refreshedPair = React.useState(0);
|
|
8892
|
+
var refreshed = refreshedPair[0];
|
|
8893
|
+
var setRefreshed = refreshedPair[1];
|
|
8894
|
+
wfUseFlashClear(refreshed, function () {
|
|
8895
|
+
setRefreshed(0);
|
|
8896
|
+
});
|
|
8783
8897
|
var settingsPair = React.useState(false);
|
|
8784
8898
|
var settingsOpen = settingsPair[0];
|
|
8785
8899
|
var setSettingsOpen = settingsPair[1];
|
|
@@ -8878,6 +8992,13 @@ window.__ModuleLoader__.load({
|
|
|
8878
8992
|
onToggleSettings: function () {
|
|
8879
8993
|
setSettingsOpen(!settingsOpen);
|
|
8880
8994
|
},
|
|
8995
|
+
// The manual fallback: one pull, now, without waiting for the interval and without
|
|
8996
|
+
// touching the loop. It reads the same endpoint with the same validator, so a click
|
|
8997
|
+
// is a normal poll and never a second data path.
|
|
8998
|
+
onRefresh: function () {
|
|
8999
|
+
setRefreshed(Date.now());
|
|
9000
|
+
if (typeof snap.refresh === "function") snap.refresh();
|
|
9001
|
+
},
|
|
8881
9002
|
// Clicking a main-flow unit: it becomes the SELECTED step (store.js builds its detail) and the
|
|
8882
9003
|
// right page opens on it. It deliberately does not expand anything in place.
|
|
8883
9004
|
// right page opens on it. It deliberately does not expand anything in place.
|
|
@@ -8956,6 +9077,7 @@ window.__ModuleLoader__.load({
|
|
|
8956
9077
|
|
|
8957
9078
|
return wfRenderPanel(model, prefs, say, actions, {
|
|
8958
9079
|
settingsOpen: settingsOpen,
|
|
9080
|
+
refreshed: refreshed,
|
|
8959
9081
|
status: snap.status,
|
|
8960
9082
|
error: snap.error,
|
|
8961
9083
|
rev: snap.rev,
|
|
@@ -8996,6 +9118,9 @@ window.__ModuleLoader__.load({
|
|
|
8996
9118
|
var pendingCount = wfPendingInjectCount(model.injects);
|
|
8997
9119
|
var extras = ui || {};
|
|
8998
9120
|
var fire = actions || {};
|
|
9121
|
+
// The refresh button's one-second acknowledgement, handed in by the component: the
|
|
9122
|
+
// renderer stays pure, so a self-test can drive it with a fixed stamp.
|
|
9123
|
+
var refreshed = wfFinite(extras.refreshed) === null ? 0 : extras.refreshed;
|
|
8999
9124
|
// the two attributes the living layer needs. `motion` comes from the system's
|
|
9000
9125
|
// prefers-reduced-motion preference (pulse.js), `idle` from the model's running flag.
|
|
9001
9126
|
// Both live on .wf-root so the capsule is covered as well as the panel.
|
|
@@ -9095,6 +9220,24 @@ window.__ModuleLoader__.load({
|
|
|
9095
9220
|
h(
|
|
9096
9221
|
"div",
|
|
9097
9222
|
{ className: "wf-btns", key: "btns" },
|
|
9223
|
+
// First in the row: it is the escape hatch when the automatic poll is not
|
|
9224
|
+
// delivering, so it must not be the hard one to find.
|
|
9225
|
+
h(
|
|
9226
|
+
"button",
|
|
9227
|
+
{
|
|
9228
|
+
type: "button",
|
|
9229
|
+
key: "refresh",
|
|
9230
|
+
className: refreshed > 0 ? "wf-btn wf-btn-ok" : "wf-btn",
|
|
9231
|
+
"data-dsh-workflow-map": "refresh",
|
|
9232
|
+
"data-refreshed": refreshed > 0 ? "true" : undefined,
|
|
9233
|
+
title: say("panel.refresh.hint"),
|
|
9234
|
+
"aria-label": say("panel.refresh"),
|
|
9235
|
+
onClick: function () {
|
|
9236
|
+
wfFire(fire.onRefresh);
|
|
9237
|
+
},
|
|
9238
|
+
},
|
|
9239
|
+
refreshed > 0 ? say("panel.refresh.done") : "↻"
|
|
9240
|
+
),
|
|
9098
9241
|
h(
|
|
9099
9242
|
"button",
|
|
9100
9243
|
{
|
|
@@ -9565,6 +9708,7 @@ window.__ModuleLoader__.load({
|
|
|
9565
9708
|
sessionIdFromSnapshot: wfSessionIdFromSnapshot,
|
|
9566
9709
|
pollUrl: wfPollUrl,
|
|
9567
9710
|
pollOnce: wfPollOnce,
|
|
9711
|
+
pollContinues: wfPollContinues,
|
|
9568
9712
|
parsePrefs: wfParsePrefs,
|
|
9569
9713
|
sanitizePrefs: wfSanitizePrefs,
|
|
9570
9714
|
applyPrefsPatch: wfApplyPrefsPatch,
|
package/cordis.patch.yml
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# dsh-workflow-map — bundle 挂载声明
|
|
2
2
|
#
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
3
|
+
# 作用:把这个包的宿主半插入 DSH 的插件行。
|
|
4
|
+
# 浏览器半不由本文件声明 —— package.json 的 `dsh.client`(platform: web)指向本包的
|
|
5
|
+
# ./client,因此这里没有 client 行。
|
|
6
6
|
#
|
|
7
7
|
# id 在整个 profile 的 patch 层里必须唯一。
|
|
8
8
|
|
|
@@ -23,8 +23,8 @@
|
|
|
23
23
|
| `locale/` / `icon.svg` | 词典与图标 |
|
|
24
24
|
| `tools/` | 自测、探针、浏览器量测(**不随 npm 包发布**,进 Git) |
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
26
|
+
**发布前核对**:`package.json` 里 `repository.url`、`homepage`、`bugs.url` 三处必须指向真实的 GitHub 用户名与仓库名;
|
|
27
|
+
仓库改名或 fork 后要同步改这三处。
|
|
28
28
|
|
|
29
29
|
---
|
|
30
30
|
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
|
|
37
37
|
在 <https://github.com/new> 建一个**空**仓库,仓库名与 `package.json` 的 `name` 一致。
|
|
38
38
|
|
|
39
|
-
⚠️ **不要**勾选 "Add a README / .gitignore / license"
|
|
39
|
+
⚠️ **不要**勾选 "Add a README / .gitignore / license":本地已有,勾选会导致首次推送冲突。
|
|
40
40
|
|
|
41
41
|
### 2.2 初始化并推送
|
|
42
42
|
|
|
@@ -87,8 +87,8 @@ pnpm pack --dry-run
|
|
|
87
87
|
|
|
88
88
|
- 第一行为 `package: dsh-workflow-map@<版本>`;
|
|
89
89
|
- 含 `client.js`、`lib/`、`src/`、`index.js`、`cordis.patch.yml`、`locale/*.json`、`icon.svg`、
|
|
90
|
-
`README.md`、`CHANGELOG.md`、`LICENSE`、`docs/OPEN-SOURCE-GUIDE.md`;
|
|
91
|
-
- **不含** `tools/`、`evidence/`、`*.bak
|
|
90
|
+
`README.md`、`CHANGELOG.md`、`LICENSE`、`docs/OPEN-SOURCE-GUIDE.md`、`docs/host-compatibility.md`;
|
|
91
|
+
- **不含** `tools/`、`evidence/`、`*.bak*` 与本地草稿。
|
|
92
92
|
|
|
93
93
|
`files[]` 是白名单:以**文件**(而非目录)列出,能避免整目录被连带发布。
|
|
94
94
|
|
|
@@ -168,7 +168,7 @@ node tools/probe-route.mjs # 真实 HTTP 路由
|
|
|
168
168
|
node tools/probe-steer.mjs # 注入 / 撤回通道
|
|
169
169
|
```
|
|
170
170
|
|
|
171
|
-
合计 **
|
|
171
|
+
合计 **1533 条离线断言**。也可一条命令:
|
|
172
172
|
|
|
173
173
|
```bash
|
|
174
174
|
npm test
|
|
@@ -196,7 +196,7 @@ pwsh -File tools/measure-settings.ps1 # 设置卡与导出
|
|
|
196
196
|
pwsh -File tools/measure-main.ps1 # 单页几何与交互
|
|
197
197
|
```
|
|
198
198
|
|
|
199
|
-
合计 **
|
|
199
|
+
合计 **323 条浏览器量测**(0.3.4 起 `measure-main` 另含 15 条浅色/深色主题对比检查)。
|
|
200
200
|
|
|
201
201
|
> `tools/retired/` 里是量「已移除界面」的历史脚本,**不属于验证链**,不期望通过。
|
|
202
202
|
|
|
@@ -248,7 +248,7 @@ dsh-workflow-map/
|
|
|
248
248
|
├─ client.js 生成物,禁止手改
|
|
249
249
|
├─ cordis.patch.yml 挂载声明
|
|
250
250
|
├─ locale/{zh,en}.json 词典
|
|
251
|
-
├─ docs/
|
|
251
|
+
├─ docs/ 发布指南与宿主兼容性核验
|
|
252
252
|
├─ tools/ 自测 · 探针 · 浏览器量测
|
|
253
253
|
├─ CHANGELOG.md
|
|
254
254
|
├─ LICENSE (MIT)
|