@dsh-workbench/shared 0.3.6 → 0.3.7
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/api/context-ref.d.ts +0 -12
- package/dist/api/context-ref.js +0 -40
- package/dist/api/facts.d.ts +0 -6
- package/dist/api/facts.js +0 -8
- package/dist/components/ConfirmBar.css +26 -0
- package/dist/components/ConfirmBar.d.ts +36 -0
- package/dist/components/ConfirmBar.js +26 -0
- package/dist/components/Form.d.ts +7 -5
- package/dist/components/Form.js +5 -3
- package/dist/components/FormNodes.d.ts +17 -13
- package/dist/components/FormNodes.js +13 -5
- package/dist/components/FormTypes.d.ts +39 -14
- package/dist/components/MockStrip.css +11 -1
- package/dist/components/StateDot.d.ts +5 -0
- package/dist/components/StateDot.js +10 -0
- package/dist/config/constants.d.ts +2 -0
- package/dist/config/constants.js +2 -0
- package/dist/host/__tests__/fakes.d.ts +41 -0
- package/dist/host/__tests__/fakes.js +76 -0
- package/dist/host/__tests__/preflight-fixtures.d.ts +19 -0
- package/dist/host/__tests__/preflight-fixtures.js +47 -0
- package/dist/host/board-fact.d.ts +8 -0
- package/dist/host/board-fact.js +16 -0
- package/dist/host/browser-read.js +2 -1
- package/dist/host/context-inject.d.ts +24 -0
- package/dist/host/context-inject.js +74 -0
- package/dist/host/context-ref.d.ts +13 -0
- package/dist/host/context-ref.js +41 -0
- package/dist/host/diy-debug.d.ts +80 -0
- package/dist/host/diy-debug.js +88 -0
- package/dist/host/diy-record.d.ts +135 -0
- package/dist/host/diy-record.js +162 -0
- package/dist/host/events.d.ts +9 -0
- package/dist/host/events.js +70 -0
- package/dist/host/facts.d.ts +6 -0
- package/dist/host/facts.js +9 -0
- package/dist/host/git.js +1 -1
- package/dist/host/http.d.ts +2 -2
- package/dist/host/http.js +3 -3
- package/dist/host/long-task.d.ts +5 -0
- package/dist/host/long-task.js +3 -0
- package/dist/host/mount-units.d.ts +52 -0
- package/dist/host/mount-units.js +186 -0
- package/dist/host/opencli-runtime.js +1 -1
- package/dist/host/opencli-window.js +1 -1
- package/dist/host/opencli.js +2 -1
- package/dist/host/panel-state.d.ts +120 -0
- package/dist/host/panel-state.js +176 -0
- package/dist/host/plugin-source.js +1 -1
- package/dist/host/preflight.d.ts +58 -0
- package/dist/host/preflight.js +233 -0
- package/dist/host/probe-cache.d.ts +6 -0
- package/dist/host/probe-cache.js +19 -0
- package/dist/host/probe-libs.d.ts +45 -0
- package/dist/host/probe-libs.js +61 -0
- package/dist/host/probe-units.d.ts +17 -0
- package/dist/host/probe-units.js +102 -0
- package/dist/host/probe-verdict.d.ts +7 -0
- package/dist/host/probe-verdict.js +143 -0
- package/dist/host/shell-text.d.ts +13 -0
- package/dist/host/shell-text.js +20 -0
- package/dist/host/tool-scope.d.ts +32 -0
- package/dist/host/tool-scope.js +84 -0
- package/dist/host/unit-declaration.d.ts +65 -0
- package/dist/host/unit-declaration.js +213 -0
- package/dist/host/unit-readiness.d.ts +7 -0
- package/dist/host/unit-readiness.js +5 -0
- package/dist/host/unit-registry.d.ts +9 -0
- package/dist/host/unit-registry.js +33 -0
- package/dist/host/version-range.d.ts +2 -0
- package/dist/host/version-range.js +73 -0
- package/dist/host.d.ts +7 -4
- package/dist/host.js +7 -4
- package/dist/i18n/en.js +4 -0
- package/dist/i18n/index.d.ts +5 -1
- package/dist/i18n/zh.d.ts +4 -0
- package/dist/i18n/zh.js +4 -0
- package/dist/index.d.ts +1 -4
- package/dist/index.js +1 -4
- package/dist/lib/board-fact.d.ts +0 -7
- package/dist/lib/board-fact.js +0 -15
- package/dist/lib/diy-record.d.ts +38 -1
- package/dist/lib/diy-record.js +66 -0
- package/dist/lib/text.d.ts +0 -13
- package/dist/lib/text.js +0 -20
- package/dist/testing/declaration.js +1 -1
- package/package.json +1 -1
- package/src/api/__tests__/facts.test.ts +0 -13
- package/src/api/context-ref.ts +0 -39
- package/src/api/facts.ts +0 -9
- package/src/components/ConfirmBar.css +26 -0
- package/src/components/ConfirmBar.test.tsx +130 -0
- package/src/components/ConfirmBar.tsx +63 -0
- package/src/components/Form.test.tsx +15 -0
- package/src/components/Form.tsx +7 -5
- package/src/components/FormNodes.tsx +50 -26
- package/src/components/FormTypes.test.ts +31 -0
- package/src/components/FormTypes.ts +46 -15
- package/src/components/FormWiring.test.tsx +1 -0
- package/src/config/constants.ts +3 -0
- package/src/host/__tests__/board-fact.test.ts +47 -0
- package/src/{lib → host/__tests__}/diy-debug.test.ts +1 -1
- package/src/{lib → host/__tests__}/diy-record.test.ts +137 -1
- package/src/host/__tests__/facts.test.ts +13 -0
- package/src/host/__tests__/long-task.test.ts +42 -0
- package/src/{lib → host/__tests__}/panel-state.test.ts +1 -1
- package/src/host/__tests__/preflight-route.test.ts +33 -0
- package/src/host/__tests__/shell-text.test.ts +44 -0
- package/src/host/board-fact.ts +14 -0
- package/src/host/browser-read.ts +2 -1
- package/src/host/context-ref.ts +40 -0
- package/src/{lib → host}/diy-record.ts +95 -1
- package/src/host/facts.ts +10 -0
- package/src/host/git.ts +1 -1
- package/src/host/http.ts +4 -4
- package/src/host/long-task.ts +9 -0
- package/src/host/opencli-runtime.ts +1 -1
- package/src/host/opencli-window.ts +1 -1
- package/src/host/opencli.ts +2 -1
- package/src/host/plugin-source.ts +1 -1
- package/src/host/shell-text.ts +21 -0
- package/src/{api → host}/unit-declaration.ts +1 -1
- package/src/host.ts +7 -4
- package/src/i18n/en.ts +4 -0
- package/src/i18n/zh.ts +4 -0
- package/src/index.ts +1 -4
- package/src/lib/board-fact.test.ts +1 -33
- package/src/lib/board-fact.ts +0 -13
- package/src/lib/text.test.ts +1 -43
- package/src/lib/text.ts +0 -22
- package/src/testing/declaration.ts +1 -1
- package/dist/lib/lint-probe.d.ts +0 -1
- package/dist/lib/lint-probe.js +0 -6
- /package/src/{api → host}/__tests__/unit-declaration.test.ts +0 -0
- /package/src/{lib → host}/diy-debug.ts +0 -0
- /package/src/{lib → host}/panel-state.ts +0 -0
package/dist/index.js
CHANGED
|
@@ -7,7 +7,6 @@ export * from './api/facts.js';
|
|
|
7
7
|
export * from './api/host-write.js';
|
|
8
8
|
export * from './api/long-task.js';
|
|
9
9
|
export * from './api/unit-board.js';
|
|
10
|
-
export * from './api/unit-declaration.js';
|
|
11
10
|
export * from './api/unit.js';
|
|
12
11
|
export * from './api/workbench-client.js';
|
|
13
12
|
export * from './api/workbench-contract.js';
|
|
@@ -19,6 +18,7 @@ export * from './components/CardAnchor.js';
|
|
|
19
18
|
export * from './components/CardBoundary.js';
|
|
20
19
|
export * from './components/CardHead.js';
|
|
21
20
|
export * from './components/ChoiceGroup.js';
|
|
21
|
+
export * from './components/ConfirmBar.js';
|
|
22
22
|
export * from './components/Field.js';
|
|
23
23
|
export * from './components/Form.js';
|
|
24
24
|
export * from './components/FormActions.js';
|
|
@@ -49,10 +49,7 @@ export * from './i18n/zh.js';
|
|
|
49
49
|
export * from './lib/blocker-action.js';
|
|
50
50
|
export * from './lib/board-fact.js';
|
|
51
51
|
export * from './lib/busy.js';
|
|
52
|
-
export * from './lib/diy-debug.js';
|
|
53
|
-
export * from './lib/diy-record.js';
|
|
54
52
|
export * from './lib/form-problem.js';
|
|
55
|
-
export * from './lib/panel-state.js';
|
|
56
53
|
export * from './lib/react.js';
|
|
57
54
|
export * from './lib/text.js';
|
|
58
55
|
export * from './lib/ui.js';
|
package/dist/lib/board-fact.d.ts
CHANGED
|
@@ -24,13 +24,6 @@ export interface BoardProbe {
|
|
|
24
24
|
* @returns 同一个路径或在其下给 `true`;`root` 末尾带不带斜杠都认(`/a` 不匹配 `/ab`)。
|
|
25
25
|
*/
|
|
26
26
|
export declare function insideWorkshop(root: string, target: string): boolean;
|
|
27
|
-
/**
|
|
28
|
-
* 把一次探针读数归成椅面归属。
|
|
29
|
-
* @param probe - 探针读数。
|
|
30
|
-
* @param workshop - 当前工作区根路径;`null` 表示还不知道当前是哪个工作区。
|
|
31
|
-
* @returns 外壳不在、或板路径与工作区缺一 → `unknown`;右栏没开或没画出来 → `hidden`;板在本工作区 → `workshop`,否则 `other`。
|
|
32
|
-
*/
|
|
33
|
-
export declare function boardFactOf(probe: BoardProbe, workshop: string | null): BoardFactState;
|
|
34
27
|
/**
|
|
35
28
|
* 把探针读数归成左栏那句人话的档位。
|
|
36
29
|
* @param probe - 探针读数。
|
package/dist/lib/board-fact.js
CHANGED
|
@@ -10,21 +10,6 @@ export function insideWorkshop(root, target) {
|
|
|
10
10
|
const base = root.endsWith('/') ? root : `${root}/`;
|
|
11
11
|
return target.startsWith(base);
|
|
12
12
|
}
|
|
13
|
-
/**
|
|
14
|
-
* 把一次探针读数归成椅面归属。
|
|
15
|
-
* @param probe - 探针读数。
|
|
16
|
-
* @param workshop - 当前工作区根路径;`null` 表示还不知道当前是哪个工作区。
|
|
17
|
-
* @returns 外壳不在、或板路径与工作区缺一 → `unknown`;右栏没开或没画出来 → `hidden`;板在本工作区 → `workshop`,否则 `other`。
|
|
18
|
-
*/
|
|
19
|
-
export function boardFactOf(probe, workshop) {
|
|
20
|
-
if (!probe.workbench)
|
|
21
|
-
return 'unknown';
|
|
22
|
-
if (!probe.bar || !probe.open || !probe.drawn)
|
|
23
|
-
return 'hidden';
|
|
24
|
-
if (probe.board === '' || workshop === null)
|
|
25
|
-
return 'unknown';
|
|
26
|
-
return insideWorkshop(workshop, probe.board) ? 'workshop' : 'other';
|
|
27
|
-
}
|
|
28
13
|
/**
|
|
29
14
|
* 把探针读数归成左栏那句人话的档位。
|
|
30
15
|
* @param probe - 探针读数。
|
package/dist/lib/diy-record.d.ts
CHANGED
|
@@ -14,7 +14,29 @@ export interface RecordExpect {
|
|
|
14
14
|
readonly urlContains: string;
|
|
15
15
|
}
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* 一次「截图」在宿主台账里的那一行:页面上的计数、落图那一刻、落盘路径;没落下来时带 `error` 原文(落成的那条没有它)。
|
|
18
|
+
*/
|
|
19
|
+
export interface RecordShot {
|
|
20
|
+
readonly n: number;
|
|
21
|
+
readonly at: number;
|
|
22
|
+
readonly path: string;
|
|
23
|
+
readonly error?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 某个页面计数已经试过几次:初拍算一次,失败再试一次;计数一换就从头数。
|
|
27
|
+
*/
|
|
28
|
+
export interface RecordShotTries {
|
|
29
|
+
readonly n: number;
|
|
30
|
+
readonly tries: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 一张图最多试几次(初拍 + 一次重试):失败如实报过就停,不每拍重试到底。
|
|
34
|
+
*/
|
|
35
|
+
export declare const RECORD_SHOT_TRIES = 2;
|
|
36
|
+
/**
|
|
37
|
+
* 一份活着的录制约定:相位 + 谁的会话 + 哪个工作区 / 单元 + 页面 URL 与标题 + 要找的那一次写 +
|
|
38
|
+
* 落盘文件 + 三个时间戳,以及两份台账:按计数落图的 `shots`(`shotsSeen` 只记落成的那个计数)
|
|
39
|
+
* 与门铃的 `beats` / `beatMs`(命中几次、最近一次等了多久)。
|
|
18
40
|
*/
|
|
19
41
|
export interface RecordState {
|
|
20
42
|
readonly phase: RecordPhase;
|
|
@@ -30,6 +52,11 @@ export interface RecordState {
|
|
|
30
52
|
readonly startedAt: number;
|
|
31
53
|
readonly t0: number | null;
|
|
32
54
|
readonly t1: number | null;
|
|
55
|
+
readonly shots: readonly RecordShot[];
|
|
56
|
+
readonly shotsSeen: number;
|
|
57
|
+
readonly shotTries: RecordShotTries | null;
|
|
58
|
+
readonly beats: number;
|
|
59
|
+
readonly beatMs: number;
|
|
33
60
|
readonly at: number;
|
|
34
61
|
}
|
|
35
62
|
/**
|
|
@@ -83,8 +110,18 @@ export interface RecordStore {
|
|
|
83
110
|
begin(pact: RecordPact): RecordMove;
|
|
84
111
|
started(sessionId: string, at: number, now: number): RecordMove;
|
|
85
112
|
finished(sessionId: string, at: number, now: number): RecordMove;
|
|
113
|
+
shotTaken(sessionId: string, shot: RecordShot, now: number): RecordMove;
|
|
114
|
+
shotFailed(sessionId: string, shot: RecordShot, now: number): RecordMove;
|
|
115
|
+
beat(sessionId: string, ms: number, now: number): RecordMove;
|
|
86
116
|
stop(sessionId: string, now: number): RecordMove;
|
|
87
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* 这一拍该不该落图:页面计数涨过 `shotsSeen` 才算;同一个计数已经试满就先放下,等他自己再按。
|
|
120
|
+
* @param state - 当前有效的那份约定。
|
|
121
|
+
* @param pageShots - 页面浮条上那个「已拍」计数。
|
|
122
|
+
* @returns 该落的那个计数;不该落给 `null`(没涨 / 试满了 / 不是个正经数)。
|
|
123
|
+
*/
|
|
124
|
+
export declare function shotDueOf(state: RecordState, pageShots: number): number | null;
|
|
88
125
|
/**
|
|
89
126
|
* 造一份新的录制约定存储(进程内存,同一时刻只可能有一份有效约定)。
|
|
90
127
|
* @returns 存取口;动作成不成都在返回的 `RecordMove` 里,不抛异常。
|
package/dist/lib/diy-record.js
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
* 录制约定的有效期(15 分钟,毫秒):超过它没动静的约定算过期、当场让人。
|
|
3
3
|
*/
|
|
4
4
|
export const RECORD_STALE_MS = 15 * 60 * 1000;
|
|
5
|
+
/**
|
|
6
|
+
* 一张图最多试几次(初拍 + 一次重试):失败如实报过就停,不每拍重试到底。
|
|
7
|
+
*/
|
|
8
|
+
export const RECORD_SHOT_TRIES = 2;
|
|
5
9
|
/**
|
|
6
10
|
* 把工具入参收成人话动作。
|
|
7
11
|
* @param raw - 任意入参值。
|
|
@@ -10,6 +14,22 @@ export const RECORD_STALE_MS = 15 * 60 * 1000;
|
|
|
10
14
|
export function parseRecordIntent(raw) {
|
|
11
15
|
return raw === 'peek' || raw === 'halt' || raw === 'stop' ? raw : null;
|
|
12
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* 这一拍该不该落图:页面计数涨过 `shotsSeen` 才算;同一个计数已经试满就先放下,等他自己再按。
|
|
19
|
+
* @param state - 当前有效的那份约定。
|
|
20
|
+
* @param pageShots - 页面浮条上那个「已拍」计数。
|
|
21
|
+
* @returns 该落的那个计数;不该落给 `null`(没涨 / 试满了 / 不是个正经数)。
|
|
22
|
+
*/
|
|
23
|
+
export function shotDueOf(state, pageShots) {
|
|
24
|
+
if (!Number.isFinite(pageShots))
|
|
25
|
+
return null;
|
|
26
|
+
const n = Math.floor(pageShots);
|
|
27
|
+
if (n <= state.shotsSeen)
|
|
28
|
+
return null;
|
|
29
|
+
if (state.shotTries !== null && state.shotTries.n === n && state.shotTries.tries >= RECORD_SHOT_TRIES)
|
|
30
|
+
return null;
|
|
31
|
+
return n;
|
|
32
|
+
}
|
|
13
33
|
function alive(state, now) {
|
|
14
34
|
if (state === null)
|
|
15
35
|
return null;
|
|
@@ -28,6 +48,18 @@ function ownerOf(state, sessionId, now) {
|
|
|
28
48
|
export function createRecordStore() {
|
|
29
49
|
let held = null;
|
|
30
50
|
const live = (now) => alive(held, now);
|
|
51
|
+
const withShot = (found, shot, now, error) => {
|
|
52
|
+
const row = error === undefined ? shot : { n: shot.n, at: shot.at, path: shot.path, error };
|
|
53
|
+
const kept = found.shots.filter((one) => one.n !== shot.n);
|
|
54
|
+
const tries = found.shotTries !== null && found.shotTries.n === shot.n ? found.shotTries.tries : 0;
|
|
55
|
+
return {
|
|
56
|
+
...found,
|
|
57
|
+
shots: [...kept, row].sort((one, two) => one.n - two.n),
|
|
58
|
+
shotsSeen: error === undefined ? Math.max(found.shotsSeen, shot.n) : found.shotsSeen,
|
|
59
|
+
shotTries: error === undefined ? null : { n: shot.n, tries: tries + 1 },
|
|
60
|
+
at: now,
|
|
61
|
+
};
|
|
62
|
+
};
|
|
31
63
|
return {
|
|
32
64
|
live,
|
|
33
65
|
begin(pact) {
|
|
@@ -49,6 +81,11 @@ export function createRecordStore() {
|
|
|
49
81
|
startedAt: pact.now,
|
|
50
82
|
t0: null,
|
|
51
83
|
t1: null,
|
|
84
|
+
shots: [],
|
|
85
|
+
shotsSeen: 0,
|
|
86
|
+
shotTries: null,
|
|
87
|
+
beats: 0,
|
|
88
|
+
beatMs: 0,
|
|
52
89
|
at: pact.now,
|
|
53
90
|
};
|
|
54
91
|
held = state;
|
|
@@ -76,6 +113,35 @@ export function createRecordStore() {
|
|
|
76
113
|
held = state;
|
|
77
114
|
return { ok: true, state };
|
|
78
115
|
},
|
|
116
|
+
shotTaken(sessionId, shot, now) {
|
|
117
|
+
const found = ownerOf(held, sessionId, now);
|
|
118
|
+
if (typeof found === 'string')
|
|
119
|
+
return { ok: false, code: found };
|
|
120
|
+
const state = withShot(found, shot, now, undefined);
|
|
121
|
+
held = state;
|
|
122
|
+
return { ok: true, state };
|
|
123
|
+
},
|
|
124
|
+
shotFailed(sessionId, shot, now) {
|
|
125
|
+
const found = ownerOf(held, sessionId, now);
|
|
126
|
+
if (typeof found === 'string')
|
|
127
|
+
return { ok: false, code: found };
|
|
128
|
+
const state = withShot(found, shot, now, shot.error === undefined ? '落图失败' : shot.error);
|
|
129
|
+
held = state;
|
|
130
|
+
return { ok: true, state };
|
|
131
|
+
},
|
|
132
|
+
beat(sessionId, ms, now) {
|
|
133
|
+
const found = ownerOf(held, sessionId, now);
|
|
134
|
+
if (typeof found === 'string')
|
|
135
|
+
return { ok: false, code: found };
|
|
136
|
+
const state = {
|
|
137
|
+
...found,
|
|
138
|
+
beats: found.beats + 1,
|
|
139
|
+
beatMs: Math.max(0, Math.floor(ms)),
|
|
140
|
+
at: now,
|
|
141
|
+
};
|
|
142
|
+
held = state;
|
|
143
|
+
return { ok: true, state };
|
|
144
|
+
},
|
|
79
145
|
stop(sessionId, now) {
|
|
80
146
|
const found = ownerOf(held, sessionId, now);
|
|
81
147
|
if (typeof found === 'string')
|
package/dist/lib/text.d.ts
CHANGED
|
@@ -1,16 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
|
|
3
|
-
* @param value - 任意值;不是字符串的先 `String()`。
|
|
4
|
-
* @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
|
|
5
|
-
*/
|
|
6
|
-
export declare function quoteShellArg(value: unknown): string;
|
|
7
|
-
/**
|
|
8
|
-
* 取一段文本的末尾若干行(如命令输出),顺带去掉首尾空白。
|
|
9
|
-
* @param text - 原文;`null` / `undefined` 当空串。
|
|
10
|
-
* @param lines - 最多留几行,默认 6;0 给全篇、负数从开头砍掉那么多行(都是 `slice` 语义),调用方传正数。
|
|
11
|
-
* @returns 末尾若干行、去首尾空行后的文本;原文行数不够就全给,空输入给空串。
|
|
12
|
-
*/
|
|
13
|
-
export declare function tailLines(text: string | null | undefined, lines?: number): string;
|
|
14
1
|
/**
|
|
15
2
|
* 宽松解析一段前后可能夹着人话的 JSON:整段不通就从行首的 `[` / `{` 起、截到最后一个闭合符再试。
|
|
16
3
|
* @param text - 原文;`null` / `undefined` 当空串。
|
package/dist/lib/text.js
CHANGED
|
@@ -1,23 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 把一个值裹成 shell 里的单引号字面量(值里的单引号一并转义),保证元字符按字面量传递。
|
|
3
|
-
* @param value - 任意值;不是字符串的先 `String()`。
|
|
4
|
-
* @returns 可安全拼进 `/bin/sh -c` 的一段文本 —— 含空格 / `$()` / `;` / 通配符的值都不会被当成语法。
|
|
5
|
-
*/
|
|
6
|
-
export function quoteShellArg(value) {
|
|
7
|
-
return `'${String(value).replace(/'/g, `'\\''`)}'`;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* 取一段文本的末尾若干行(如命令输出),顺带去掉首尾空白。
|
|
11
|
-
* @param text - 原文;`null` / `undefined` 当空串。
|
|
12
|
-
* @param lines - 最多留几行,默认 6;0 给全篇、负数从开头砍掉那么多行(都是 `slice` 语义),调用方传正数。
|
|
13
|
-
* @returns 末尾若干行、去首尾空行后的文本;原文行数不够就全给,空输入给空串。
|
|
14
|
-
*/
|
|
15
|
-
export function tailLines(text, lines = 6) {
|
|
16
|
-
const all = String(text ?? '')
|
|
17
|
-
.trim()
|
|
18
|
-
.split('\n');
|
|
19
|
-
return all.slice(-lines).join('\n');
|
|
20
|
-
}
|
|
21
1
|
function candidateStarts(raw) {
|
|
22
2
|
const starts = [];
|
|
23
3
|
for (let index = 0; index < raw.length; index += 1) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import { fileURLToPath } from 'node:url';
|
|
3
|
-
import { readDeclaration } from '../
|
|
3
|
+
import { readDeclaration } from '../host/unit-declaration.js';
|
|
4
4
|
export function declarationOfPackage(pkg, packageJson) {
|
|
5
5
|
const at = typeof packageJson === 'string' ? packageJson : fileURLToPath(packageJson);
|
|
6
6
|
let parsed;
|
package/package.json
CHANGED
|
@@ -2,10 +2,7 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import {
|
|
3
3
|
createFactChannel,
|
|
4
4
|
factOfRoute,
|
|
5
|
-
isFactScope,
|
|
6
5
|
parseFactFrame,
|
|
7
|
-
scopePrefixOf,
|
|
8
|
-
unitFactScope,
|
|
9
6
|
type FactFrame,
|
|
10
7
|
type FactTransport,
|
|
11
8
|
} from '../facts.js';
|
|
@@ -262,13 +259,3 @@ describe('factOfRoute', () => {
|
|
|
262
259
|
expect(factOfRoute('capability')).toBe('capability');
|
|
263
260
|
});
|
|
264
261
|
});
|
|
265
|
-
|
|
266
|
-
describe('unitFactScope', () => {
|
|
267
|
-
it('命名空间 = 模块 id + `*`("这个模块的数据全作废"只写一个字符串)', () => {
|
|
268
|
-
expect(unitFactScope('cmdb')).toBe('cmdb*');
|
|
269
|
-
expect(isFactScope('cmdb*')).toBe(true);
|
|
270
|
-
expect(isFactScope('*')).toBe(false);
|
|
271
|
-
expect(isFactScope('cmdb/summary')).toBe(false);
|
|
272
|
-
expect(scopePrefixOf('cmdb*')).toBe('cmdb');
|
|
273
|
-
});
|
|
274
|
-
});
|
package/src/api/context-ref.ts
CHANGED
|
@@ -11,10 +11,6 @@ export interface ContextRef {
|
|
|
11
11
|
readonly parts: readonly string[];
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const MARKER = new RegExp(`@${CONTEXT_MARKER_NAME}:([a-z][a-z0-9-]*:[A-Za-z0-9._~%#/-]+)`, 'g');
|
|
15
|
-
|
|
16
|
-
const TRAILING_PUNCTUATION = /[.,;:!?]+$/;
|
|
17
|
-
|
|
18
14
|
/**
|
|
19
15
|
* 把引用拼回标记体里的那一段文本(不含 @ 与标记名)。
|
|
20
16
|
* @param ref 要拼的引用。
|
|
@@ -24,21 +20,6 @@ export function encodeRef(ref: ContextRef): string {
|
|
|
24
20
|
return `${ref.kind}:${ref.parts.join('/')}`;
|
|
25
21
|
}
|
|
26
22
|
|
|
27
|
-
/**
|
|
28
|
-
* 把 kind:part/part 文本读回引用。
|
|
29
|
-
* @param text 待解析文本。
|
|
30
|
-
* @returns 没冒号、冒号在首尾、种类不是小写字母开头、或冒号后为空时给 undefined;不抛异常。
|
|
31
|
-
*/
|
|
32
|
-
export function decodeRef(text: string): ContextRef | undefined {
|
|
33
|
-
const at = text.indexOf(':');
|
|
34
|
-
if (at <= 0 || at === text.length - 1) return undefined;
|
|
35
|
-
const kind = text.slice(0, at);
|
|
36
|
-
if (!/^[a-z][a-z0-9-]*$/.test(kind)) return undefined;
|
|
37
|
-
const parts = text.slice(at + 1).split('/').filter((part) => part !== '');
|
|
38
|
-
if (parts.length === 0) return undefined;
|
|
39
|
-
return { kind, parts };
|
|
40
|
-
}
|
|
41
|
-
|
|
42
23
|
/**
|
|
43
24
|
* 拼出 @ 后面那半截(标记名加已编码文本),比较与拼接都用它。
|
|
44
25
|
* @param encoded encodeRef 的结果。
|
|
@@ -56,23 +37,3 @@ export function markerBody(encoded: string): string {
|
|
|
56
37
|
export function buildMarker(ref: ContextRef): string {
|
|
57
38
|
return `@${markerBody(encodeRef(ref))}`;
|
|
58
39
|
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* 从一段文本里按出现顺序捞出所有标记引用,同一个引用只留第一次。
|
|
62
|
-
* @param text 任意文本;null / undefined 当空串。
|
|
63
|
-
* @returns 解析不出来的标记会被丢掉,所以结果只含合法引用,可能为空数组。
|
|
64
|
-
*/
|
|
65
|
-
export function parseMarkers(text: string): ContextRef[] {
|
|
66
|
-
const seen = new Set<string>();
|
|
67
|
-
const refs: ContextRef[] = [];
|
|
68
|
-
for (const match of String(text ?? '').matchAll(MARKER)) {
|
|
69
|
-
const raw = match[1].replace(TRAILING_PUNCTUATION, '');
|
|
70
|
-
const ref = decodeRef(raw);
|
|
71
|
-
if (ref === undefined) continue;
|
|
72
|
-
const key = encodeRef(ref);
|
|
73
|
-
if (seen.has(key)) continue;
|
|
74
|
-
seen.add(key);
|
|
75
|
-
refs.push(ref);
|
|
76
|
-
}
|
|
77
|
-
return refs;
|
|
78
|
-
}
|
package/src/api/facts.ts
CHANGED
|
@@ -12,15 +12,6 @@ export interface FactFrame {
|
|
|
12
12
|
*/
|
|
13
13
|
export const FACT_SCOPE_SUFFIX = '*';
|
|
14
14
|
|
|
15
|
-
/**
|
|
16
|
-
* 由单元 id 拼出它那一族事实的范围名。
|
|
17
|
-
* @param unit 单元 id,不带后缀。
|
|
18
|
-
* @returns <unit>* —— 永远是范围名,不是某一条事实。
|
|
19
|
-
*/
|
|
20
|
-
export function unitFactScope(unit: string): string {
|
|
21
|
-
return `${unit}${FACT_SCOPE_SUFFIX}`;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
15
|
/**
|
|
25
16
|
* 判一个名字是不是范围查法,不是问这条事实存不存在。
|
|
26
17
|
* @param name 事实名或范围名。
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
.wb-confirmBar {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--wb-len-8);
|
|
5
|
+
width: 100%;
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
padding: var(--wb-len-10) var(--wb-len-12);
|
|
8
|
+
border: 0.5px solid var(--dsw-alias-border-l3);
|
|
9
|
+
border-radius: var(--dsw-radius-md);
|
|
10
|
+
background: var(--dsw-alias-bg-layer-1);
|
|
11
|
+
}
|
|
12
|
+
.wb-confirmBarTitle {
|
|
13
|
+
margin: 0;
|
|
14
|
+
font-size: var(--dsw-font-xxs-12-font-size);
|
|
15
|
+
font-weight: 600;
|
|
16
|
+
line-height: var(--wb-line-18);
|
|
17
|
+
color: var(--dsw-alias-label-primary);
|
|
18
|
+
overflow-wrap: anywhere;
|
|
19
|
+
}
|
|
20
|
+
.wb-confirmBarHint {
|
|
21
|
+
margin: 0;
|
|
22
|
+
font-size: var(--dsw-font-xxxs-11-font-size);
|
|
23
|
+
line-height: var(--wb-line-16);
|
|
24
|
+
color: var(--dsw-alias-label-secondary);
|
|
25
|
+
overflow-wrap: anywhere;
|
|
26
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { describe, expect, it } from 'vitest';
|
|
3
|
+
import { ConfirmBar, type ConfirmRow } from './ConfirmBar.js';
|
|
4
|
+
import { FormActions, type FormAction } from './FormActions.js';
|
|
5
|
+
import { Note } from './Note.js';
|
|
6
|
+
import { Rows } from './Rows.js';
|
|
7
|
+
|
|
8
|
+
interface AnyElement {
|
|
9
|
+
readonly type: unknown;
|
|
10
|
+
readonly props: Record<string, unknown>;
|
|
11
|
+
readonly key?: unknown;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function action(key: string, kind?: FormAction['kind']): FormAction {
|
|
15
|
+
return { key, label: key, kind, onClick: () => {} };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function row(key: string): ConfirmRow {
|
|
19
|
+
return { key, label: `名字-${key}`, value: `值-${key}` };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function bar(extra: Record<string, unknown> = {}): AnyElement {
|
|
23
|
+
return ConfirmBar({
|
|
24
|
+
title: '发布到 prod?',
|
|
25
|
+
actions: [action('publish', 'primary')],
|
|
26
|
+
...extra,
|
|
27
|
+
}) as unknown as AnyElement;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function childrenOf(node: AnyElement): AnyElement[] {
|
|
31
|
+
return ((node.props['children'] as unknown[]) ?? []).filter(
|
|
32
|
+
(one): one is AnyElement => typeof one === 'object' && one !== null,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
describe('确认条(契约类 wb-confirmBar)', () => {
|
|
37
|
+
it('★ 是一个 role="group" 的条,读屏名默认取字符串标题', () => {
|
|
38
|
+
const root = bar();
|
|
39
|
+
|
|
40
|
+
expect(root.props['role']).toBe('group');
|
|
41
|
+
expect(root.props['aria-label']).toBe('发布到 prod?');
|
|
42
|
+
expect(childrenOf(root)[0]?.props['className']).toBe('wb-confirmBarTitle');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('显式给了 label 就用它;标题不是字符串又没给 label 就不设读屏名', () => {
|
|
46
|
+
expect(bar({ label: '发布确认' }).props['aria-label']).toBe('发布确认');
|
|
47
|
+
expect(bar({ title: 42 }).props['aria-label']).toBeUndefined();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('busy 只喂 aria-busy,不改动作那一块', () => {
|
|
51
|
+
const root = bar({ busy: true, actions: [action('publish', 'primary'), action('cancel')] });
|
|
52
|
+
|
|
53
|
+
expect(root.props['aria-busy']).toBe(true);
|
|
54
|
+
expect(childrenOf(root)[1]?.props['actions']).toHaveLength(2);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('★ 三件都借现成的:键值走 Rows、失败那句走 Note(tone=problem, role=alert)、动作走 FormActions', () => {
|
|
58
|
+
const actions = [action('publish', 'primary'), action('cancel')];
|
|
59
|
+
const root = bar({ rows: [row('env')], problem: '源上没有这个包', actions });
|
|
60
|
+
const children = childrenOf(root);
|
|
61
|
+
|
|
62
|
+
expect(children[0]?.props['className']).toBe('wb-confirmBarTitle');
|
|
63
|
+
expect(children[1]?.type).toBe(Rows);
|
|
64
|
+
expect(children[2]?.type).toBe(Note);
|
|
65
|
+
expect(children[2]?.props['tone']).toBe('problem');
|
|
66
|
+
expect(children[2]?.props['role']).toBe('alert');
|
|
67
|
+
expect(children[3]?.type).toBe(FormActions);
|
|
68
|
+
expect(children[3]?.props['actions']).toBe(actions);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('★ 没给的那几块不占位:只要标题与动作区', () => {
|
|
72
|
+
expect(childrenOf(bar())).toHaveLength(2);
|
|
73
|
+
expect(childrenOf(bar({ rows: [] }))).toHaveLength(2);
|
|
74
|
+
expect(childrenOf(bar({ hint: '这一趟会重启一次' }))).toHaveLength(3);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('★ 键值那几格走 Row 的 label / value 两格(key 交给 React 认列表),不是自己画两栏', () => {
|
|
78
|
+
const root = bar({ rows: [row('env'), row('branch')] });
|
|
79
|
+
const rowNodes = childrenOf(childrenOf(root)[1] as AnyElement);
|
|
80
|
+
|
|
81
|
+
expect(rowNodes.map((one) => one.props['label'])).toEqual(['名字-env', '名字-branch']);
|
|
82
|
+
expect(rowNodes.map((one) => one.props['value'])).toEqual(['值-env', '值-branch']);
|
|
83
|
+
expect(rowNodes.map((one) => one.key)).toEqual(['env', 'branch']);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const CSS = readFileSync(new URL('./ConfirmBar.css', import.meta.url), 'utf8');
|
|
88
|
+
|
|
89
|
+
function declarations(selector: string): string[] {
|
|
90
|
+
for (const block of CSS.split('}')) {
|
|
91
|
+
const [head, ...rest] = block.split('{');
|
|
92
|
+
if (rest.length === 0 || head === undefined) continue;
|
|
93
|
+
if (head.trim() !== selector) continue;
|
|
94
|
+
return rest
|
|
95
|
+
.join('{')
|
|
96
|
+
.split(';')
|
|
97
|
+
.map((one) => one.replace(/\s+/g, ' ').trim())
|
|
98
|
+
.filter((one) => one !== '');
|
|
99
|
+
}
|
|
100
|
+
throw new Error(`ConfirmBar.css 里没有这条规则:${selector}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
describe('确认条的版式(ADR-0073 §2.2)', () => {
|
|
104
|
+
it('★ 占满自己那一格(有内边距与描边 ⇒ 配 border-box),别在横向父容器里退回内容宽', () => {
|
|
105
|
+
const rule = declarations('.wb-confirmBar');
|
|
106
|
+
|
|
107
|
+
expect(rule).toContain('width: 100%');
|
|
108
|
+
expect(rule).toContain('box-sizing: border-box');
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('★ 一档间距、描边与圆角都走令牌', () => {
|
|
112
|
+
const rule = declarations('.wb-confirmBar');
|
|
113
|
+
|
|
114
|
+
expect(rule).toContain('gap: var(--wb-len-8)');
|
|
115
|
+
expect(rule).toContain('padding: var(--wb-len-10) var(--wb-len-12)');
|
|
116
|
+
expect(rule).toContain('border: 0.5px solid var(--dsw-alias-border-l3)');
|
|
117
|
+
expect(rule).toContain('border-radius: var(--dsw-radius-md)');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('★ 标题与说明的大小走令牌,标题比说明重一档', () => {
|
|
121
|
+
const title = declarations('.wb-confirmBarTitle');
|
|
122
|
+
const hint = declarations('.wb-confirmBarHint');
|
|
123
|
+
|
|
124
|
+
expect(title).toContain('font-size: var(--dsw-font-xxs-12-font-size)');
|
|
125
|
+
expect(title).toContain('line-height: var(--wb-line-18)');
|
|
126
|
+
expect(title).toContain('font-weight: 600');
|
|
127
|
+
expect(hint).toContain('font-size: var(--dsw-font-xxxs-11-font-size)');
|
|
128
|
+
expect(hint).toContain('line-height: var(--wb-line-16)');
|
|
129
|
+
});
|
|
130
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ReactNode } from '../lib/react.js';
|
|
2
|
+
import { h } from '../lib/react.js';
|
|
3
|
+
import { FormActions, type FormAction } from './FormActions.js';
|
|
4
|
+
import { Note } from './Note.js';
|
|
5
|
+
import { Row, Rows } from './Rows.js';
|
|
6
|
+
import './ConfirmBar.css';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* `ConfirmBar` 的一格键值:`label` 在左、`value` 在右(走 `Rows` 的标签列)。
|
|
10
|
+
*/
|
|
11
|
+
export interface ConfirmRow {
|
|
12
|
+
readonly key: string;
|
|
13
|
+
readonly label: ReactNode;
|
|
14
|
+
readonly value: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* `ConfirmBar` 的契约:标题、说明、键值、失败那句与动作。
|
|
19
|
+
*/
|
|
20
|
+
export interface ConfirmBarProps {
|
|
21
|
+
readonly title: ReactNode;
|
|
22
|
+
readonly hint?: ReactNode | undefined;
|
|
23
|
+
readonly rows?: readonly ConfirmRow[] | undefined;
|
|
24
|
+
readonly actions: readonly FormAction[];
|
|
25
|
+
readonly problem?: ReactNode | undefined;
|
|
26
|
+
readonly busy?: boolean | undefined;
|
|
27
|
+
readonly label?: string | undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* 「先问一句再动手」那一条:标题 + 说明 + 键值 + 失败那句 + 动作区,形状一次收口。
|
|
32
|
+
* **要不要问**归调用点(ADR-0054 §2 规则三),这一件只回答"问的时候长什么样"。
|
|
33
|
+
* @param props.title - 那一句问话("发布到 prod?")。
|
|
34
|
+
* @param props.hint - 标题下面那句说明或正文;不给不占位。
|
|
35
|
+
* @param props.rows - 键值两格(键值列宽与分隔线归 `Rows` / `Row`);不给或空数组不渲染。值那一格可以放控件。
|
|
36
|
+
* @param props.actions - 动作清单(归 `FormActions`:一个区一颗 `primary`、永远在右、窄了折行)。
|
|
37
|
+
* @param props.problem - 失败那句(走 `Note tone="problem"`,读屏是 `alert`);不给不占位。
|
|
38
|
+
* @param props.busy - 整条在忙(只喂 `aria-busy`;按钮自己的转圈与禁用归 `actions` 里那一颗)。
|
|
39
|
+
* @param props.label - 读屏的组名;不给时 `title` 是字符串就拿它顶上,两者都没有就不设。
|
|
40
|
+
* @returns 一个 `role="group"` 的条;`actions` 空数组时最后那一块不渲染。
|
|
41
|
+
*/
|
|
42
|
+
export function ConfirmBar({ title, hint, rows, actions, problem, busy, label }: ConfirmBarProps) {
|
|
43
|
+
const name = label ?? (typeof title === 'string' ? title : undefined);
|
|
44
|
+
return (
|
|
45
|
+
<div className="wb-confirmBar" role="group" aria-label={name} aria-busy={busy}>
|
|
46
|
+
<p className="wb-confirmBarTitle">{title}</p>
|
|
47
|
+
{hint === undefined ? null : <p className="wb-confirmBarHint">{hint}</p>}
|
|
48
|
+
{rows === undefined || rows.length === 0 ? null : (
|
|
49
|
+
<Rows>
|
|
50
|
+
{rows.map((row) => (
|
|
51
|
+
<Row key={row.key} label={row.label} value={row.value} />
|
|
52
|
+
))}
|
|
53
|
+
</Rows>
|
|
54
|
+
)}
|
|
55
|
+
{problem === undefined ? null : (
|
|
56
|
+
<Note tone="problem" role="alert">
|
|
57
|
+
{problem}
|
|
58
|
+
</Note>
|
|
59
|
+
)}
|
|
60
|
+
<FormActions actions={actions} />
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -20,6 +20,7 @@ function field(key: string, value: FormValue, validate?: FormField['validate']):
|
|
|
20
20
|
key,
|
|
21
21
|
label: key,
|
|
22
22
|
value,
|
|
23
|
+
onChange: () => {},
|
|
23
24
|
control: (binding) => `${key}:${binding.id}:${String(binding.invalid)}:${String(binding.disabled)}`,
|
|
24
25
|
...(validate === undefined ? {} : { validate }),
|
|
25
26
|
};
|
|
@@ -402,6 +403,20 @@ describe('formRowsOf', () => {
|
|
|
402
403
|
expect(propsOf(rows[0])['children']).toBe('env:wb-form-env:true:false');
|
|
403
404
|
});
|
|
404
405
|
|
|
406
|
+
it('★ 这一格的 value / onChange 从字段配置原样进绑定(值只写一处)', () => {
|
|
407
|
+
const onChange = (): void => {};
|
|
408
|
+
const probe: FormField = {
|
|
409
|
+
key: 'env',
|
|
410
|
+
label: 'env',
|
|
411
|
+
value: 'prod',
|
|
412
|
+
onChange,
|
|
413
|
+
control: (binding) => `${String(binding.value)}:${String(binding.onChange === onChange)}`,
|
|
414
|
+
};
|
|
415
|
+
const rows = formRowsOf({ fields: [probe], problems: {}, showProblem: false, busy: false });
|
|
416
|
+
|
|
417
|
+
expect(propsOf(rows[0])['children']).toBe('prod:true');
|
|
418
|
+
});
|
|
419
|
+
|
|
405
420
|
it('★ 还没提交过(showProblem: false):文案不画、invalid 也不给(done 之前一律 false)', () => {
|
|
406
421
|
const rows = formRowsOf({ fields: [field('env', 'prod')], problems, showProblem: false, busy: false });
|
|
407
422
|
|