@kontourai/survey 2.2.2 → 2.2.3
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/README.md +14 -6
- package/dist/src/review-workbench/review-queue-session.d.ts +17 -0
- package/dist/src/review-workbench/review-queue-session.js +23 -0
- package/dist/src/review-workbench/review-workbench-css.generated.js +101 -90
- package/dist/src/review-workbench/review-workbench.css +101 -90
- package/dist/src/review-workbench/review-workbench.d.ts +1 -1
- package/dist/src/review-workbench/review-workbench.js +104 -80
- package/dist/src/review-workbench/review-workbench.standalone.css +11 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -169,12 +169,20 @@ The embedded stylesheet is scoped to `.survey-workbench-embed` and bundles Conso
|
|
|
169
169
|
|
|
170
170
|
### Theme it as your own brand
|
|
171
171
|
|
|
172
|
-
The bundled `--k-*` token
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
you
|
|
172
|
+
The bundled stylesheet carries a literal default for every `--k-*` token on the
|
|
173
|
+
embed element itself, so the workbench looks right in a host that declares no
|
|
174
|
+
tokens at all. To re-brand it, declare the tokens you want **on the embed
|
|
175
|
+
element** — a rule matching `.survey-workbench-embed`, or an inline `style`
|
|
176
|
+
attribute. Tokens you leave alone keep their default, so you only declare what
|
|
177
|
+
you re-brand.
|
|
178
|
+
|
|
179
|
+
Setting `--k-*` on an *ancestor* of the embed does not reach it: the embed
|
|
180
|
+
declares those tokens on its own root, and a declaration on an element always
|
|
181
|
+
beats a value inherited from an ancestor. If you want the embed to inherit a
|
|
182
|
+
token layer your page already publishes at `:root`, use the web component
|
|
183
|
+
(`<survey-review-workbench>`) — its defaults sit on the shadow `:host`, so host
|
|
184
|
+
tokens and inline styles propagate through — or restate the tokens on the embed
|
|
185
|
+
element as below.
|
|
178
186
|
|
|
179
187
|
```css
|
|
180
188
|
/* Your app's own palette drives the workbench — no Kontour branding. */
|
|
@@ -74,6 +74,23 @@ export declare function currentReviewItem(session: ReviewQueueSessionState): Rev
|
|
|
74
74
|
export declare function deriveQueueRowStatus(item: ReviewItem, session: ReviewQueueSessionState): ReviewQueueRowStatus;
|
|
75
75
|
export declare function nextUnresolvedItemName(session: ReviewQueueSessionState): string | undefined;
|
|
76
76
|
export declare function reviewSessionSummary(session: ReviewQueueSessionState): ReviewSessionSummary;
|
|
77
|
+
/**
|
|
78
|
+
* The decision the field card's "keep" control records for a ReviewItem, or
|
|
79
|
+
* `undefined` when the item cannot represent that action at all.
|
|
80
|
+
*
|
|
81
|
+
* Every workbench decision resolves to a candidate role (see
|
|
82
|
+
* {@link workbenchDecisionDefinitions}), so a decision whose role the item does
|
|
83
|
+
* not carry is not recordable — `keep-current` on an item that has no `current`
|
|
84
|
+
* candidate is the case that matters, because that is exactly how an
|
|
85
|
+
* envelope-imported item is modelled (one `proposed` candidate, nothing prior).
|
|
86
|
+
* The card already labels that control "Leave unset" rather than "Keep current";
|
|
87
|
+
* this returns the decision that means the same thing and IS representable:
|
|
88
|
+
* `reject-proposed` — the proposed value is not applied and nothing is set.
|
|
89
|
+
*
|
|
90
|
+
* Deliberately NOT solved by synthesising an empty `current` candidate: that
|
|
91
|
+
* would invent a prior value, with provenance, that the source never had.
|
|
92
|
+
*/
|
|
93
|
+
export declare function keepActionDecision(item: ReviewItem, flaggedWrong: boolean): ReviewWorkbenchDecision | undefined;
|
|
77
94
|
export declare function candidateForDecision(item: ReviewItem, decision: ReviewWorkbenchDecision): ReviewCandidate;
|
|
78
95
|
/**
|
|
79
96
|
* The value that should actually be applied for a decision: the reviewer's inline
|
|
@@ -125,6 +125,29 @@ export function reviewSessionSummary(session) {
|
|
|
125
125
|
unresolved: 0,
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* The decision the field card's "keep" control records for a ReviewItem, or
|
|
130
|
+
* `undefined` when the item cannot represent that action at all.
|
|
131
|
+
*
|
|
132
|
+
* Every workbench decision resolves to a candidate role (see
|
|
133
|
+
* {@link workbenchDecisionDefinitions}), so a decision whose role the item does
|
|
134
|
+
* not carry is not recordable — `keep-current` on an item that has no `current`
|
|
135
|
+
* candidate is the case that matters, because that is exactly how an
|
|
136
|
+
* envelope-imported item is modelled (one `proposed` candidate, nothing prior).
|
|
137
|
+
* The card already labels that control "Leave unset" rather than "Keep current";
|
|
138
|
+
* this returns the decision that means the same thing and IS representable:
|
|
139
|
+
* `reject-proposed` — the proposed value is not applied and nothing is set.
|
|
140
|
+
*
|
|
141
|
+
* Deliberately NOT solved by synthesising an empty `current` candidate: that
|
|
142
|
+
* would invent a prior value, with provenance, that the source never had.
|
|
143
|
+
*/
|
|
144
|
+
export function keepActionDecision(item, flaggedWrong) {
|
|
145
|
+
const hasRole = (role) => item.spec.candidates.some((candidate) => candidate.role === role);
|
|
146
|
+
if (flaggedWrong || !hasRole("current")) {
|
|
147
|
+
return hasRole("proposed") ? "reject-proposed" : undefined;
|
|
148
|
+
}
|
|
149
|
+
return "keep-current";
|
|
150
|
+
}
|
|
128
151
|
export function candidateForDecision(item, decision) {
|
|
129
152
|
const definition = workbenchDecisionDefinitions[decision];
|
|
130
153
|
const candidate = item.spec.candidates.find((entry) => entry.role === definition.candidateRole);
|
|
@@ -6,149 +6,149 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
|
|
|
6
6
|
color-scheme: dark;
|
|
7
7
|
|
|
8
8
|
/* structure */
|
|
9
|
-
--k-bg:
|
|
10
|
-
--k-panel:
|
|
11
|
-
--k-panel-raised:
|
|
12
|
-
--k-line:
|
|
13
|
-
--k-line-strong:
|
|
14
|
-
--k-shadow:
|
|
9
|
+
--k-bg: #0a0e13;
|
|
10
|
+
--k-panel: #111824;
|
|
11
|
+
--k-panel-raised: #16202d;
|
|
12
|
+
--k-line: rgba(150, 180, 210, 0.12);
|
|
13
|
+
--k-line-strong: rgba(150, 180, 210, 0.22);
|
|
14
|
+
--k-shadow: 0 26px 60px -42px rgba(0, 0, 0, 0.95);
|
|
15
15
|
|
|
16
16
|
/* text */
|
|
17
|
-
--k-text:
|
|
18
|
-
--k-text-muted:
|
|
19
|
-
--k-text-faint:
|
|
17
|
+
--k-text: #eef3f8;
|
|
18
|
+
--k-text-muted: #aebccb;
|
|
19
|
+
--k-text-faint: #72869b;
|
|
20
20
|
|
|
21
21
|
/* brand */
|
|
22
|
-
--k-brand:
|
|
23
|
-
--k-brand-contrast:
|
|
22
|
+
--k-brand: #5ce0c6;
|
|
23
|
+
--k-brand-contrast: #06080b;
|
|
24
24
|
|
|
25
25
|
/* semantic status scale */
|
|
26
|
-
--k-positive:
|
|
27
|
-
--k-caution:
|
|
28
|
-
--k-negative:
|
|
29
|
-
--k-neutral:
|
|
30
|
-
--k-active:
|
|
26
|
+
--k-positive: #34d399;
|
|
27
|
+
--k-caution: #f3b14b;
|
|
28
|
+
--k-negative: #ff6f6f;
|
|
29
|
+
--k-neutral: #6f8095;
|
|
30
|
+
--k-active: #7aa2ff;
|
|
31
31
|
--k-positive-soft: color-mix(in oklab, var(--k-positive) 14%, transparent);
|
|
32
32
|
--k-caution-soft: color-mix(in oklab, var(--k-caution) 14%, transparent);
|
|
33
33
|
--k-negative-soft: color-mix(in oklab, var(--k-negative) 14%, transparent);
|
|
34
34
|
--k-active-soft: color-mix(in oklab, var(--k-active) 14%, transparent);
|
|
35
35
|
|
|
36
36
|
/* spacing */
|
|
37
|
-
--k-space-1:
|
|
38
|
-
--k-space-2:
|
|
39
|
-
--k-space-3:
|
|
40
|
-
--k-space-4:
|
|
41
|
-
--k-space-5:
|
|
42
|
-
--k-space-6:
|
|
37
|
+
--k-space-1: 4px;
|
|
38
|
+
--k-space-2: 8px;
|
|
39
|
+
--k-space-3: 12px;
|
|
40
|
+
--k-space-4: 16px;
|
|
41
|
+
--k-space-5: 24px;
|
|
42
|
+
--k-space-6: 32px;
|
|
43
43
|
|
|
44
44
|
/* radius */
|
|
45
|
-
--k-radius-sm:
|
|
46
|
-
--k-radius-md:
|
|
45
|
+
--k-radius-sm: 9px;
|
|
46
|
+
--k-radius-md: 14px;
|
|
47
47
|
|
|
48
48
|
/* type */
|
|
49
|
-
--k-text-xs:
|
|
50
|
-
--k-text-sm:
|
|
51
|
-
--k-text-md:
|
|
52
|
-
--k-text-lg:
|
|
53
|
-
--k-text-xl:
|
|
54
|
-
--k-text-2xl:
|
|
49
|
+
--k-text-xs: 11px;
|
|
50
|
+
--k-text-sm: 12.5px;
|
|
51
|
+
--k-text-md: 14px;
|
|
52
|
+
--k-text-lg: 18px;
|
|
53
|
+
--k-text-xl: 22px;
|
|
54
|
+
--k-text-2xl: clamp(26px, 3.4vw, 38px);
|
|
55
55
|
|
|
56
56
|
/* fonts */
|
|
57
|
-
--k-font-display:
|
|
58
|
-
--k-font-ui:
|
|
59
|
-
--k-font-mono:
|
|
57
|
+
--k-font-display: "Fraunces", Georgia, "Times New Roman", serif;
|
|
58
|
+
--k-font-ui: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
59
|
+
--k-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
60
60
|
|
|
61
61
|
/* motion */
|
|
62
|
-
--k-ease:
|
|
63
|
-
--k-dur:
|
|
62
|
+
--k-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
63
|
+
--k-dur: 0.16s;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
66
|
.survey-workbench-embed[data-theme="light"]{
|
|
67
67
|
color-scheme: light;
|
|
68
|
-
--k-bg:
|
|
69
|
-
--k-panel:
|
|
70
|
-
--k-panel-raised:
|
|
71
|
-
--k-line:
|
|
72
|
-
--k-line-strong:
|
|
73
|
-
--k-shadow:
|
|
74
|
-
--k-text:
|
|
75
|
-
--k-text-muted:
|
|
76
|
-
--k-text-faint:
|
|
77
|
-
--k-brand-contrast:
|
|
78
|
-
--k-positive:
|
|
79
|
-
--k-caution:
|
|
80
|
-
--k-negative:
|
|
81
|
-
--k-neutral:
|
|
82
|
-
--k-active:
|
|
68
|
+
--k-bg: #f5f4ef;
|
|
69
|
+
--k-panel: #ffffff;
|
|
70
|
+
--k-panel-raised: #fbfaf7;
|
|
71
|
+
--k-line: rgba(36, 40, 46, 0.12);
|
|
72
|
+
--k-line-strong: rgba(36, 40, 46, 0.20);
|
|
73
|
+
--k-shadow: 0 18px 50px -30px rgba(0, 0, 0, 0.25);
|
|
74
|
+
--k-text: #202124;
|
|
75
|
+
--k-text-muted: #5b626b;
|
|
76
|
+
--k-text-faint: #707782;
|
|
77
|
+
--k-brand-contrast: #ffffff;
|
|
78
|
+
--k-positive: #168257;
|
|
79
|
+
--k-caution: #8a5a00;
|
|
80
|
+
--k-negative: #c83b3b;
|
|
81
|
+
--k-neutral: #5f6975;
|
|
82
|
+
--k-active: #3f6fd6;
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
|
|
86
86
|
.survey-workbench-embed.theme-survey{
|
|
87
|
-
--k-bg:
|
|
88
|
-
--k-brand:
|
|
89
|
-
--k-text-faint:
|
|
87
|
+
--k-bg: #06080b;
|
|
88
|
+
--k-brand: #5ce0c6;
|
|
89
|
+
--k-text-faint: #6f8095;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
92
|
.survey-workbench-embed.theme-console{
|
|
93
|
-
--k-font-ui:
|
|
94
|
-
--k-font-display:
|
|
95
|
-
--k-radius-sm:
|
|
96
|
-
--k-radius-md:
|
|
97
|
-
--k-brand:
|
|
98
|
-
--k-brand-contrast:
|
|
99
|
-
--k-bg:
|
|
100
|
-
--k-panel:
|
|
101
|
-
--k-panel-raised:
|
|
102
|
-
--k-line:
|
|
103
|
-
--k-line-strong:
|
|
104
|
-
--k-text:
|
|
105
|
-
--k-text-muted:
|
|
106
|
-
--k-text-faint:
|
|
107
|
-
--k-positive:
|
|
108
|
-
--k-caution:
|
|
109
|
-
--k-negative:
|
|
110
|
-
--k-active:
|
|
93
|
+
--k-font-ui: "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif;
|
|
94
|
+
--k-font-display: "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif;
|
|
95
|
+
--k-radius-sm: 0;
|
|
96
|
+
--k-radius-md: 0;
|
|
97
|
+
--k-brand: #c9ff4a;
|
|
98
|
+
--k-brand-contrast: #11120f;
|
|
99
|
+
--k-bg: #11120f;
|
|
100
|
+
--k-panel: #191b16;
|
|
101
|
+
--k-panel-raised: #20231e;
|
|
102
|
+
--k-line: #3a4035;
|
|
103
|
+
--k-line-strong: #8ea36e;
|
|
104
|
+
--k-text: #f2f0e8;
|
|
105
|
+
--k-text-muted: #a6ab9c;
|
|
106
|
+
--k-text-faint: #747a6c;
|
|
107
|
+
--k-positive: #a6d37b;
|
|
108
|
+
--k-caution: #e8c15f;
|
|
109
|
+
--k-negative: #ee776f;
|
|
110
|
+
--k-active: #84d8c8;
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
.survey-workbench-embed.theme-flow{
|
|
114
|
-
--k-brand:
|
|
114
|
+
--k-brand: #2f88a6;
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
.survey-workbench-embed.theme-surface{
|
|
118
|
-
--k-brand:
|
|
118
|
+
--k-brand: #14a37a;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
121
|
.survey-workbench-embed[data-theme="light"].theme-survey,
|
|
122
122
|
.survey-workbench-embed[data-theme="light"] .theme-survey{
|
|
123
|
-
--k-brand:
|
|
123
|
+
--k-brand: #16806f;
|
|
124
124
|
}
|
|
125
125
|
|
|
126
126
|
.survey-workbench-embed[data-theme="light"].theme-console,
|
|
127
127
|
.survey-workbench-embed[data-theme="light"] .theme-console{
|
|
128
|
-
--k-brand:
|
|
129
|
-
--k-brand-contrast:
|
|
130
|
-
--k-bg:
|
|
131
|
-
--k-panel:
|
|
132
|
-
--k-panel-raised:
|
|
133
|
-
--k-line:
|
|
134
|
-
--k-line-strong:
|
|
135
|
-
--k-text:
|
|
136
|
-
--k-text-muted:
|
|
137
|
-
--k-text-faint:
|
|
138
|
-
--k-positive:
|
|
139
|
-
--k-caution:
|
|
140
|
-
--k-negative:
|
|
141
|
-
--k-active:
|
|
128
|
+
--k-brand: #6c9400;
|
|
129
|
+
--k-brand-contrast: #ffffff;
|
|
130
|
+
--k-bg: #f3f5eb;
|
|
131
|
+
--k-panel: #fbfcf7;
|
|
132
|
+
--k-panel-raised: #eef2e6;
|
|
133
|
+
--k-line: #ccd5bf;
|
|
134
|
+
--k-line-strong: #8fa36f;
|
|
135
|
+
--k-text: #1e2319;
|
|
136
|
+
--k-text-muted: #596250;
|
|
137
|
+
--k-text-faint: #77816d;
|
|
138
|
+
--k-positive: #2f7d32;
|
|
139
|
+
--k-caution: #8a6500;
|
|
140
|
+
--k-negative: #b93a36;
|
|
141
|
+
--k-active: #247f75;
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
.survey-workbench-embed[data-theme="light"].theme-flow,
|
|
145
145
|
.survey-workbench-embed[data-theme="light"] .theme-flow{
|
|
146
|
-
--k-brand:
|
|
146
|
+
--k-brand: #1f6f88;
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
.survey-workbench-embed[data-theme="light"].theme-surface,
|
|
150
150
|
.survey-workbench-embed[data-theme="light"] .theme-surface{
|
|
151
|
-
--k-brand:
|
|
151
|
+
--k-brand: #0f6b52;
|
|
152
152
|
}
|
|
153
153
|
|
|
154
154
|
|
|
@@ -402,6 +402,17 @@ export const REVIEW_WORKBENCH_CSS = `/* Bundled, scoped Survey Review Workbench
|
|
|
402
402
|
transition: width 0.35s cubic-bezier(0.2, 0.7, 0.3, 1);
|
|
403
403
|
}
|
|
404
404
|
|
|
405
|
+
/* \`.progress\` is a generic class name, and a host page may well have its own —
|
|
406
|
+
@kontourai/ui ships an unscoped \`.progress span { background: linear-gradient(…) }\`
|
|
407
|
+
that painted a gradient straight over this element's text in an embed. The
|
|
408
|
+
embed cannot claim the name, but it must not leave its own descendants
|
|
409
|
+
unstyled and open to a host rule; state the presentation explicitly. */
|
|
410
|
+
.survey-workbench-embed .progress .ptext{
|
|
411
|
+
display: inline;
|
|
412
|
+
height: auto;
|
|
413
|
+
background: none;
|
|
414
|
+
}
|
|
415
|
+
|
|
405
416
|
.survey-workbench-embed .ptext{
|
|
406
417
|
font-size: 13px;
|
|
407
418
|
color: var(--k-muted);
|
|
@@ -3,149 +3,149 @@
|
|
|
3
3
|
color-scheme: dark;
|
|
4
4
|
|
|
5
5
|
/* structure */
|
|
6
|
-
--k-bg:
|
|
7
|
-
--k-panel:
|
|
8
|
-
--k-panel-raised:
|
|
9
|
-
--k-line:
|
|
10
|
-
--k-line-strong:
|
|
11
|
-
--k-shadow:
|
|
6
|
+
--k-bg: #0a0e13;
|
|
7
|
+
--k-panel: #111824;
|
|
8
|
+
--k-panel-raised: #16202d;
|
|
9
|
+
--k-line: rgba(150, 180, 210, 0.12);
|
|
10
|
+
--k-line-strong: rgba(150, 180, 210, 0.22);
|
|
11
|
+
--k-shadow: 0 26px 60px -42px rgba(0, 0, 0, 0.95);
|
|
12
12
|
|
|
13
13
|
/* text */
|
|
14
|
-
--k-text:
|
|
15
|
-
--k-text-muted:
|
|
16
|
-
--k-text-faint:
|
|
14
|
+
--k-text: #eef3f8;
|
|
15
|
+
--k-text-muted: #aebccb;
|
|
16
|
+
--k-text-faint: #72869b;
|
|
17
17
|
|
|
18
18
|
/* brand */
|
|
19
|
-
--k-brand:
|
|
20
|
-
--k-brand-contrast:
|
|
19
|
+
--k-brand: #5ce0c6;
|
|
20
|
+
--k-brand-contrast: #06080b;
|
|
21
21
|
|
|
22
22
|
/* semantic status scale */
|
|
23
|
-
--k-positive:
|
|
24
|
-
--k-caution:
|
|
25
|
-
--k-negative:
|
|
26
|
-
--k-neutral:
|
|
27
|
-
--k-active:
|
|
23
|
+
--k-positive: #34d399;
|
|
24
|
+
--k-caution: #f3b14b;
|
|
25
|
+
--k-negative: #ff6f6f;
|
|
26
|
+
--k-neutral: #6f8095;
|
|
27
|
+
--k-active: #7aa2ff;
|
|
28
28
|
--k-positive-soft: color-mix(in oklab, var(--k-positive) 14%, transparent);
|
|
29
29
|
--k-caution-soft: color-mix(in oklab, var(--k-caution) 14%, transparent);
|
|
30
30
|
--k-negative-soft: color-mix(in oklab, var(--k-negative) 14%, transparent);
|
|
31
31
|
--k-active-soft: color-mix(in oklab, var(--k-active) 14%, transparent);
|
|
32
32
|
|
|
33
33
|
/* spacing */
|
|
34
|
-
--k-space-1:
|
|
35
|
-
--k-space-2:
|
|
36
|
-
--k-space-3:
|
|
37
|
-
--k-space-4:
|
|
38
|
-
--k-space-5:
|
|
39
|
-
--k-space-6:
|
|
34
|
+
--k-space-1: 4px;
|
|
35
|
+
--k-space-2: 8px;
|
|
36
|
+
--k-space-3: 12px;
|
|
37
|
+
--k-space-4: 16px;
|
|
38
|
+
--k-space-5: 24px;
|
|
39
|
+
--k-space-6: 32px;
|
|
40
40
|
|
|
41
41
|
/* radius */
|
|
42
|
-
--k-radius-sm:
|
|
43
|
-
--k-radius-md:
|
|
42
|
+
--k-radius-sm: 9px;
|
|
43
|
+
--k-radius-md: 14px;
|
|
44
44
|
|
|
45
45
|
/* type */
|
|
46
|
-
--k-text-xs:
|
|
47
|
-
--k-text-sm:
|
|
48
|
-
--k-text-md:
|
|
49
|
-
--k-text-lg:
|
|
50
|
-
--k-text-xl:
|
|
51
|
-
--k-text-2xl:
|
|
46
|
+
--k-text-xs: 11px;
|
|
47
|
+
--k-text-sm: 12.5px;
|
|
48
|
+
--k-text-md: 14px;
|
|
49
|
+
--k-text-lg: 18px;
|
|
50
|
+
--k-text-xl: 22px;
|
|
51
|
+
--k-text-2xl: clamp(26px, 3.4vw, 38px);
|
|
52
52
|
|
|
53
53
|
/* fonts */
|
|
54
|
-
--k-font-display:
|
|
55
|
-
--k-font-ui:
|
|
56
|
-
--k-font-mono:
|
|
54
|
+
--k-font-display: "Fraunces", Georgia, "Times New Roman", serif;
|
|
55
|
+
--k-font-ui: "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
|
56
|
+
--k-font-mono: "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
57
57
|
|
|
58
58
|
/* motion */
|
|
59
|
-
--k-ease:
|
|
60
|
-
--k-dur:
|
|
59
|
+
--k-ease: cubic-bezier(0.22, 1, 0.36, 1);
|
|
60
|
+
--k-dur: 0.16s;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
.survey-workbench-embed[data-theme="light"]{
|
|
64
64
|
color-scheme: light;
|
|
65
|
-
--k-bg:
|
|
66
|
-
--k-panel:
|
|
67
|
-
--k-panel-raised:
|
|
68
|
-
--k-line:
|
|
69
|
-
--k-line-strong:
|
|
70
|
-
--k-shadow:
|
|
71
|
-
--k-text:
|
|
72
|
-
--k-text-muted:
|
|
73
|
-
--k-text-faint:
|
|
74
|
-
--k-brand-contrast:
|
|
75
|
-
--k-positive:
|
|
76
|
-
--k-caution:
|
|
77
|
-
--k-negative:
|
|
78
|
-
--k-neutral:
|
|
79
|
-
--k-active:
|
|
65
|
+
--k-bg: #f5f4ef;
|
|
66
|
+
--k-panel: #ffffff;
|
|
67
|
+
--k-panel-raised: #fbfaf7;
|
|
68
|
+
--k-line: rgba(36, 40, 46, 0.12);
|
|
69
|
+
--k-line-strong: rgba(36, 40, 46, 0.20);
|
|
70
|
+
--k-shadow: 0 18px 50px -30px rgba(0, 0, 0, 0.25);
|
|
71
|
+
--k-text: #202124;
|
|
72
|
+
--k-text-muted: #5b626b;
|
|
73
|
+
--k-text-faint: #707782;
|
|
74
|
+
--k-brand-contrast: #ffffff;
|
|
75
|
+
--k-positive: #168257;
|
|
76
|
+
--k-caution: #8a5a00;
|
|
77
|
+
--k-negative: #c83b3b;
|
|
78
|
+
--k-neutral: #5f6975;
|
|
79
|
+
--k-active: #3f6fd6;
|
|
80
80
|
}
|
|
81
81
|
|
|
82
82
|
|
|
83
83
|
.survey-workbench-embed.theme-survey{
|
|
84
|
-
--k-bg:
|
|
85
|
-
--k-brand:
|
|
86
|
-
--k-text-faint:
|
|
84
|
+
--k-bg: #06080b;
|
|
85
|
+
--k-brand: #5ce0c6;
|
|
86
|
+
--k-text-faint: #6f8095;
|
|
87
87
|
}
|
|
88
88
|
|
|
89
89
|
.survey-workbench-embed.theme-console{
|
|
90
|
-
--k-font-ui:
|
|
91
|
-
--k-font-display:
|
|
92
|
-
--k-radius-sm:
|
|
93
|
-
--k-radius-md:
|
|
94
|
-
--k-brand:
|
|
95
|
-
--k-brand-contrast:
|
|
96
|
-
--k-bg:
|
|
97
|
-
--k-panel:
|
|
98
|
-
--k-panel-raised:
|
|
99
|
-
--k-line:
|
|
100
|
-
--k-line-strong:
|
|
101
|
-
--k-text:
|
|
102
|
-
--k-text-muted:
|
|
103
|
-
--k-text-faint:
|
|
104
|
-
--k-positive:
|
|
105
|
-
--k-caution:
|
|
106
|
-
--k-negative:
|
|
107
|
-
--k-active:
|
|
90
|
+
--k-font-ui: "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif;
|
|
91
|
+
--k-font-display: "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif;
|
|
92
|
+
--k-radius-sm: 0;
|
|
93
|
+
--k-radius-md: 0;
|
|
94
|
+
--k-brand: #c9ff4a;
|
|
95
|
+
--k-brand-contrast: #11120f;
|
|
96
|
+
--k-bg: #11120f;
|
|
97
|
+
--k-panel: #191b16;
|
|
98
|
+
--k-panel-raised: #20231e;
|
|
99
|
+
--k-line: #3a4035;
|
|
100
|
+
--k-line-strong: #8ea36e;
|
|
101
|
+
--k-text: #f2f0e8;
|
|
102
|
+
--k-text-muted: #a6ab9c;
|
|
103
|
+
--k-text-faint: #747a6c;
|
|
104
|
+
--k-positive: #a6d37b;
|
|
105
|
+
--k-caution: #e8c15f;
|
|
106
|
+
--k-negative: #ee776f;
|
|
107
|
+
--k-active: #84d8c8;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
.survey-workbench-embed.theme-flow{
|
|
111
|
-
--k-brand:
|
|
111
|
+
--k-brand: #2f88a6;
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
.survey-workbench-embed.theme-surface{
|
|
115
|
-
--k-brand:
|
|
115
|
+
--k-brand: #14a37a;
|
|
116
116
|
}
|
|
117
117
|
|
|
118
118
|
.survey-workbench-embed[data-theme="light"].theme-survey,
|
|
119
119
|
.survey-workbench-embed[data-theme="light"] .theme-survey{
|
|
120
|
-
--k-brand:
|
|
120
|
+
--k-brand: #16806f;
|
|
121
121
|
}
|
|
122
122
|
|
|
123
123
|
.survey-workbench-embed[data-theme="light"].theme-console,
|
|
124
124
|
.survey-workbench-embed[data-theme="light"] .theme-console{
|
|
125
|
-
--k-brand:
|
|
126
|
-
--k-brand-contrast:
|
|
127
|
-
--k-bg:
|
|
128
|
-
--k-panel:
|
|
129
|
-
--k-panel-raised:
|
|
130
|
-
--k-line:
|
|
131
|
-
--k-line-strong:
|
|
132
|
-
--k-text:
|
|
133
|
-
--k-text-muted:
|
|
134
|
-
--k-text-faint:
|
|
135
|
-
--k-positive:
|
|
136
|
-
--k-caution:
|
|
137
|
-
--k-negative:
|
|
138
|
-
--k-active:
|
|
125
|
+
--k-brand: #6c9400;
|
|
126
|
+
--k-brand-contrast: #ffffff;
|
|
127
|
+
--k-bg: #f3f5eb;
|
|
128
|
+
--k-panel: #fbfcf7;
|
|
129
|
+
--k-panel-raised: #eef2e6;
|
|
130
|
+
--k-line: #ccd5bf;
|
|
131
|
+
--k-line-strong: #8fa36f;
|
|
132
|
+
--k-text: #1e2319;
|
|
133
|
+
--k-text-muted: #596250;
|
|
134
|
+
--k-text-faint: #77816d;
|
|
135
|
+
--k-positive: #2f7d32;
|
|
136
|
+
--k-caution: #8a6500;
|
|
137
|
+
--k-negative: #b93a36;
|
|
138
|
+
--k-active: #247f75;
|
|
139
139
|
}
|
|
140
140
|
|
|
141
141
|
.survey-workbench-embed[data-theme="light"].theme-flow,
|
|
142
142
|
.survey-workbench-embed[data-theme="light"] .theme-flow{
|
|
143
|
-
--k-brand:
|
|
143
|
+
--k-brand: #1f6f88;
|
|
144
144
|
}
|
|
145
145
|
|
|
146
146
|
.survey-workbench-embed[data-theme="light"].theme-surface,
|
|
147
147
|
.survey-workbench-embed[data-theme="light"] .theme-surface{
|
|
148
|
-
--k-brand:
|
|
148
|
+
--k-brand: #0f6b52;
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
|
|
@@ -399,6 +399,17 @@
|
|
|
399
399
|
transition: width 0.35s cubic-bezier(0.2, 0.7, 0.3, 1);
|
|
400
400
|
}
|
|
401
401
|
|
|
402
|
+
/* `.progress` is a generic class name, and a host page may well have its own —
|
|
403
|
+
@kontourai/ui ships an unscoped `.progress span { background: linear-gradient(…) }`
|
|
404
|
+
that painted a gradient straight over this element's text in an embed. The
|
|
405
|
+
embed cannot claim the name, but it must not leave its own descendants
|
|
406
|
+
unstyled and open to a host rule; state the presentation explicitly. */
|
|
407
|
+
.survey-workbench-embed .progress .ptext{
|
|
408
|
+
display: inline;
|
|
409
|
+
height: auto;
|
|
410
|
+
background: none;
|
|
411
|
+
}
|
|
412
|
+
|
|
402
413
|
.survey-workbench-embed .ptext{
|
|
403
414
|
font-size: 13px;
|
|
404
415
|
color: var(--k-muted);
|
|
@@ -3,7 +3,7 @@ import { type ReviewSessionReplayIssue } from "./review-session-replay.js";
|
|
|
3
3
|
import { type ReviewPresentationAdapter } from "./review-presentation.js";
|
|
4
4
|
import { type ReviewCandidate, type ReviewDecision, type ReviewItem, type ReviewSession, type ReviewSessionEvent, type ReviewValueDescriptor } from "../review-resource.js";
|
|
5
5
|
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, type ExtractionAlignmentState, type ArtifactUnavailableCode, type ExtractionInspectorCandidate, type ExtractionInspectorEntry, type ExtractionInspectorExportOptions, type ExtractionInspectorFilters, type ExtractionInspectorInput, type ExtractionInspectorMountOptions, type ExtractionInspectorModel, type ExtractionInspectorSource, type ResolvedExtractionArtifact, } from "./extraction-inspector.js";
|
|
6
|
-
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, type ReviewQueueRowStatus, type ReviewQueueSessionState, type ReviewSessionSummary, type ReviewWorkbenchDecision, type ReviewWorkbenchState, } from "./review-queue-session.js";
|
|
6
|
+
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, type ReviewQueueRowStatus, type ReviewQueueSessionState, type ReviewSessionSummary, type ReviewWorkbenchDecision, type ReviewWorkbenchState, } from "./review-queue-session.js";
|
|
7
7
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, type ReviewCandidatePresentation, type ReviewCandidatePresentationContext, type ReviewItemPresentation, type ReviewItemPresentationContext, type ReviewPresentationAdapter, type ReviewPresentationLink, type ReviewResultPresentation, type ReviewTracePresentationContext, type ReviewTraceRef, type ReviewValuePresentationContext, } from "./review-presentation.js";
|
|
8
8
|
export { buildSurfaceProjectionPreview, type PreviewAuthorityTrace, type PreviewCandidateHistory, type PreviewClaim, type PreviewIntegrityPosture, type PreviewReviewEvent, type PreviewSourceAuthority, type PreviewSourceEvidence, type SurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
9
9
|
export { validateReviewSessionEventsForSnapshot, type ReviewSessionReplayIssue, type ReviewSessionReplayIssueCode, } from "./review-session-replay.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { canonicalJson } from "./canonical.js";
|
|
2
2
|
import { assertReviewResolutionConsistency } from "../producer-discipline.js";
|
|
3
|
-
import { candidateForDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewWorkbenchState, defaultReviewSessionName, effectiveValueForDecision, initialReviewQueueSessionState, replayReviewSessionEvents, reviewWorkbenchSessionStorageKey, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
3
|
+
import { candidateForDecision, keepActionDecision, buildReviewSessionEvent, buildReviewSessionEvents, buildReviewSessionResource, currentReviewWorkbenchState, defaultReviewSessionName, effectiveValueForDecision, initialReviewQueueSessionState, replayReviewSessionEvents, reviewWorkbenchSessionStorageKey, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
4
4
|
import { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
5
5
|
import { buildSurfaceProjectionPreview, formatValue, } from "./review-surface-preview.js";
|
|
6
6
|
import { buildReviewCandidatePresentation, buildReviewItemPresentation, } from "./review-presentation.js";
|
|
@@ -8,7 +8,7 @@ import { reviewResourceApiVersion, } from "../review-resource.js";
|
|
|
8
8
|
import { validateAuthorizing, buildAuthorizedActionAuthorizing } from "../review-authorizing.js";
|
|
9
9
|
import { humanizeIdentifier } from "./review-presentation.js";
|
|
10
10
|
export { buildExtractionInspectorModel, exportExtractionInspector, filterExtractionInspectorCandidates, mountExtractionInspector, } from "./extraction-inspector.js";
|
|
11
|
-
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
11
|
+
export { buildReviewSessionEvents, buildReviewSessionEvent, buildReviewSessionResource, candidateForDecision, keepActionDecision, currentReviewItem, currentReviewWorkbenchState, defaultReviewSessionName, deriveQueueRowStatus, initialReviewQueueSessionState, initialReviewWorkbenchState, nextUnresolvedItemName, replayReviewSessionEvents, reviewSessionSummary, reviewWorkbenchSessionStorageKey, selectedCandidateRole, workbenchDecisionDefinitions, } from "./review-queue-session.js";
|
|
12
12
|
export { buildReviewCandidatePresentation, buildReviewItemPresentation, buildReviewResultPresentation, humanizeIdentifier, } from "./review-presentation.js";
|
|
13
13
|
export { buildSurfaceProjectionPreview, } from "./review-surface-preview.js";
|
|
14
14
|
export { validateReviewSessionEventsForSnapshot, } from "./review-session-replay.js";
|
|
@@ -475,11 +475,19 @@ function fieldCardState(item, decision) {
|
|
|
475
475
|
return "kept";
|
|
476
476
|
return "review";
|
|
477
477
|
}
|
|
478
|
-
|
|
478
|
+
/**
|
|
479
|
+
* The chip copy for a decided field. An item with no current value has nothing
|
|
480
|
+
* to "keep": both the kept and rejected outcomes leave it unset, and for such an
|
|
481
|
+
* item the recorded decision genuinely does not distinguish "leave unset" from
|
|
482
|
+
* "leave unset, and the suggestion was wrong" (both are `reject-proposed` — see
|
|
483
|
+
* {@link keepActionDecision}). Saying "Kept — flagged wrong" there would claim a
|
|
484
|
+
* distinction the record does not hold, so the chip states only what happened.
|
|
485
|
+
*/
|
|
486
|
+
function chipLabel(state, hasCurrentValue) {
|
|
479
487
|
switch (state) {
|
|
480
488
|
case "accepted": return "Accepted";
|
|
481
|
-
case "kept": return "Kept current";
|
|
482
|
-
case "rejected": return "Kept — flagged wrong";
|
|
489
|
+
case "kept": return hasCurrentValue ? "Kept current" : "Left unset";
|
|
490
|
+
case "rejected": return hasCurrentValue ? "Kept — flagged wrong" : "Left unset";
|
|
483
491
|
case "could-not-confirm": return "Could not confirm";
|
|
484
492
|
default: return "Needs review";
|
|
485
493
|
}
|
|
@@ -607,6 +615,11 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
607
615
|
const hasCurrentValue = current !== undefined && !isEmptyValue(current.value);
|
|
608
616
|
const kind = hasCurrentValue ? "Update" : "New";
|
|
609
617
|
const keepLabel = hasCurrentValue ? "Keep current" : "Leave unset";
|
|
618
|
+
// Only render controls whose decision this item can actually record. Every
|
|
619
|
+
// decision resolves to a candidate role, so a control routed at a role the
|
|
620
|
+
// item does not carry cannot emit anything — it used to throw mid-click and
|
|
621
|
+
// leave the queue unrenderable (kontourai/survey#203).
|
|
622
|
+
const keepDecision = keepActionDecision(item, false);
|
|
610
623
|
const editedValue = session.editedValuesByItemName?.[item.metadata.name];
|
|
611
624
|
const proposedPresentationText = proposed
|
|
612
625
|
? buildReviewCandidatePresentation(item, proposed, presentationAdapter, presentation.targetLabel).valueText
|
|
@@ -632,23 +645,23 @@ function renderFieldCard(item, session, presentationAdapter) {
|
|
|
632
645
|
<div class="frow1">
|
|
633
646
|
<span class="fname">${escapeHtml(presentation.targetLabel)}</span>
|
|
634
647
|
<span class="fkind">${kind}</span>
|
|
635
|
-
<span class="chip ${state} push" data-testid="field-chip">${chipLabel(state)}</span>
|
|
648
|
+
<span class="chip ${state} push" data-testid="field-chip">${chipLabel(state, hasCurrentValue)}</span>
|
|
636
649
|
</div>
|
|
637
650
|
${proposed
|
|
638
651
|
? renderDiffRow(item, current, proposed, presentation.targetLabel, decided, effectiveProposedText, currentPresentationText)
|
|
639
652
|
: "<p class=\"field-value\">No proposed value is available for this field.</p>"}
|
|
640
653
|
${proposed ? renderProvenanceRow(item, proposed, presentationAdapter) : ""}
|
|
641
654
|
<div class="decide">
|
|
642
|
-
|
|
643
|
-
|
|
655
|
+
${keepDecision === undefined ? "" : `<button class="btn keep" type="button" data-testid="keep-current" data-item-name="${escapeHtml(item.metadata.name)}">${keepLabel}</button>`}
|
|
656
|
+
${proposed ? `<button class="btn use" type="button" data-testid="use-proposed" data-item-name="${escapeHtml(item.metadata.name)}">Use proposed</button>
|
|
644
657
|
<label class="wrong">
|
|
645
658
|
<input type="checkbox" class="wrongbox" data-testid="wrong-toggle" data-item-name="${escapeHtml(item.metadata.name)}">
|
|
646
659
|
Suggestion was wrong
|
|
647
660
|
</label>
|
|
648
|
-
<button class="btn unconfirmed" type="button" data-testid="could-not-confirm" data-item-name="${escapeHtml(item.metadata.name)}">Could not confirm</button
|
|
661
|
+
<button class="btn unconfirmed" type="button" data-testid="could-not-confirm" data-item-name="${escapeHtml(item.metadata.name)}">Could not confirm</button>` : ""}
|
|
649
662
|
</div>
|
|
650
663
|
<div class="decided">
|
|
651
|
-
<span class="chip ${state}" data-testid="decided-chip">${chipLabel(state)}</span>
|
|
664
|
+
<span class="chip ${state}" data-testid="decided-chip">${chipLabel(state, hasCurrentValue)}</span>
|
|
652
665
|
<button class="undo" type="button" data-testid="undo-decision" data-item-name="${escapeHtml(item.metadata.name)}">Change</button>
|
|
653
666
|
</div>
|
|
654
667
|
${renderAuditDetails(item, current, proposed, session, presentationAdapter)}
|
|
@@ -1070,7 +1083,16 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1070
1083
|
const persistEvents = () => {
|
|
1071
1084
|
eventStore?.save(session, events);
|
|
1072
1085
|
};
|
|
1073
|
-
|
|
1086
|
+
/**
|
|
1087
|
+
* Builds the session event for a state WITHOUT committing anything. Callers
|
|
1088
|
+
* that also mutate the session build the event first and commit only once it
|
|
1089
|
+
* exists: a decision the item cannot represent must leave the workbench
|
|
1090
|
+
* exactly as it was, not half-applied and unrenderable (kontourai/survey#203).
|
|
1091
|
+
*/
|
|
1092
|
+
const buildEventForSession = (sessionForEvent, eventType, itemName, dataOverride) => {
|
|
1093
|
+
// Shadow the controller's mutable `session` so this builder can only read
|
|
1094
|
+
// the state it was handed.
|
|
1095
|
+
const session = sessionForEvent;
|
|
1074
1096
|
const item = itemName ? session.items.find((entry) => entry.metadata.name === itemName) : undefined;
|
|
1075
1097
|
const decision = itemName ? session.decisionsByItemName[itemName] : undefined;
|
|
1076
1098
|
const candidate = item && decision ? candidateForDecision(item, decision) : undefined;
|
|
@@ -1089,83 +1111,71 @@ function createReviewWorkbenchController(root, startState, options) {
|
|
|
1089
1111
|
? { workbenchDecision: decision, workbenchEditedValue: editedValue }
|
|
1090
1112
|
: { workbenchDecision: decision, ...(attemptEvidenceIds?.length ? { attemptEvidenceIds } : {}) })
|
|
1091
1113
|
: undefined;
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
data: dataOverride ?? defaultData,
|
|
1112
|
-
}),
|
|
1113
|
-
];
|
|
1114
|
+
return buildReviewSessionEvent(session, {
|
|
1115
|
+
sessionName: defaultReviewSessionName,
|
|
1116
|
+
sequence: events.length + 1,
|
|
1117
|
+
eventType,
|
|
1118
|
+
occurredAt: session.reviewedAt,
|
|
1119
|
+
reviewItemName: itemName,
|
|
1120
|
+
reviewDecisionName: item && decision ? `${item.metadata.name}-${decision}` : undefined,
|
|
1121
|
+
candidateId: candidate?.id,
|
|
1122
|
+
status: definition?.status,
|
|
1123
|
+
...(decision === "could-not-confirm"
|
|
1124
|
+
? {
|
|
1125
|
+
resolution: "could_not_confirm",
|
|
1126
|
+
resolutionReason: note?.trim(),
|
|
1127
|
+
...(attemptEvidenceIds ? { attemptEvidenceIds: [...attemptEvidenceIds] } : {}),
|
|
1128
|
+
}
|
|
1129
|
+
: {}),
|
|
1130
|
+
rationale: note,
|
|
1131
|
+
data: dataOverride ?? defaultData,
|
|
1132
|
+
});
|
|
1114
1133
|
};
|
|
1115
|
-
|
|
1116
|
-
|
|
1134
|
+
/** Commits a session update and its event together, or neither. */
|
|
1135
|
+
const commitSessionUpdate = (nextSession, eventType, itemName, dataOverride) => {
|
|
1136
|
+
const event = buildEventForSession(nextSession, eventType, itemName, dataOverride);
|
|
1137
|
+
session = nextSession;
|
|
1138
|
+
events = [...events, event];
|
|
1139
|
+
persistEvents();
|
|
1117
1140
|
};
|
|
1118
1141
|
const setDecision = (itemName, decision, rawEditedValue) => {
|
|
1119
1142
|
if (decision === "could-not-confirm" && !session.notesByItemName[itemName]?.trim()) {
|
|
1120
1143
|
throw new Error("Could not confirm requires a non-empty reason.");
|
|
1121
1144
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
};
|
|
1139
|
-
});
|
|
1140
|
-
appendEvent("decision-changed", itemName);
|
|
1141
|
-
persistEvents();
|
|
1145
|
+
const item = session.items.find((entry) => entry.metadata.name === itemName);
|
|
1146
|
+
const proposed = item?.spec.candidates.find((candidate) => candidate.role === "proposed");
|
|
1147
|
+
const originalText = proposed ? formatValue(proposed.value) : undefined;
|
|
1148
|
+
const nextEditedValuesByItemName = { ...session.editedValuesByItemName };
|
|
1149
|
+
if (decision === "accept-proposed" && rawEditedValue !== undefined && rawEditedValue !== originalText) {
|
|
1150
|
+
nextEditedValuesByItemName[itemName] = rawEditedValue;
|
|
1151
|
+
}
|
|
1152
|
+
else {
|
|
1153
|
+
delete nextEditedValuesByItemName[itemName];
|
|
1154
|
+
}
|
|
1155
|
+
commitSessionUpdate({
|
|
1156
|
+
...session,
|
|
1157
|
+
activeItemName: itemName,
|
|
1158
|
+
decisionsByItemName: { ...session.decisionsByItemName, [itemName]: decision },
|
|
1159
|
+
editedValuesByItemName: nextEditedValuesByItemName,
|
|
1160
|
+
}, "decision-changed", itemName);
|
|
1142
1161
|
};
|
|
1143
1162
|
const clearDecision = (itemName) => {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
};
|
|
1158
|
-
});
|
|
1159
|
-
appendEvent("decision-changed", itemName, { workbenchDecision: null });
|
|
1160
|
-
persistEvents();
|
|
1163
|
+
const remainingDecisions = { ...session.decisionsByItemName };
|
|
1164
|
+
delete remainingDecisions[itemName];
|
|
1165
|
+
const remainingEdits = { ...session.editedValuesByItemName };
|
|
1166
|
+
delete remainingEdits[itemName];
|
|
1167
|
+
const remainingAttemptEvidenceIds = { ...session.attemptEvidenceIdsByItemName };
|
|
1168
|
+
delete remainingAttemptEvidenceIds[itemName];
|
|
1169
|
+
commitSessionUpdate({
|
|
1170
|
+
...session,
|
|
1171
|
+
activeItemName: itemName,
|
|
1172
|
+
decisionsByItemName: remainingDecisions,
|
|
1173
|
+
editedValuesByItemName: remainingEdits,
|
|
1174
|
+
attemptEvidenceIdsByItemName: remainingAttemptEvidenceIds,
|
|
1175
|
+
}, "decision-changed", itemName, { workbenchDecision: null });
|
|
1161
1176
|
};
|
|
1162
1177
|
const updateReviewerNote = (itemName, note) => {
|
|
1163
|
-
|
|
1164
|
-
...current,
|
|
1165
|
-
notesByItemName: { ...current.notesByItemName, [itemName]: note },
|
|
1166
|
-
}));
|
|
1167
|
-
appendEvent("note-changed", itemName);
|
|
1168
|
-
persistEvents();
|
|
1178
|
+
commitSessionUpdate({ ...session, notesByItemName: { ...session.notesByItemName, [itemName]: note } }, "note-changed", itemName);
|
|
1169
1179
|
};
|
|
1170
1180
|
const controller = {
|
|
1171
1181
|
currentSession: () => session,
|
|
@@ -1252,7 +1262,13 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
1252
1262
|
const descriptor = controller
|
|
1253
1263
|
.currentSession()
|
|
1254
1264
|
.items.find((entry) => entry.metadata.name === itemName)?.spec.valueDescriptor;
|
|
1255
|
-
|
|
1265
|
+
// Validate the reviewer's edit only when there IS an editor to have edited.
|
|
1266
|
+
// A non-editable item (`spec.editable === false`, e.g. every envelope-imported
|
|
1267
|
+
// item) renders no editor, so there is no reviewer input to check — validating
|
|
1268
|
+
// the absent editor's "" value blocked the decision on every typed field, and
|
|
1269
|
+
// wrote the reason into an error slot inside the suppressed editor, so the
|
|
1270
|
+
// reviewer saw nothing at all (kontourai/survey#201).
|
|
1271
|
+
const error = input ? validateProposedValue(descriptor, input.value) : undefined;
|
|
1256
1272
|
const errorEl = field?.querySelector("[data-testid='value-error']");
|
|
1257
1273
|
if (error) {
|
|
1258
1274
|
// Block the decision and surface the reason inline; the transient DOM
|
|
@@ -1277,7 +1293,15 @@ function bindFieldCardInteractions(root, controller) {
|
|
|
1277
1293
|
const itemName = button.dataset.itemName ?? "";
|
|
1278
1294
|
const field = button.closest("[data-testid='review-field']");
|
|
1279
1295
|
const wrong = field?.querySelector("[data-testid='wrong-toggle']");
|
|
1280
|
-
const
|
|
1296
|
+
const item = controller.currentSession().items.find((entry) => entry.metadata.name === itemName);
|
|
1297
|
+
// Route by what this item can record, not by the button's identity: on an
|
|
1298
|
+
// item with no current candidate this control reads "Leave unset", and
|
|
1299
|
+
// "keep the current candidate" is not a decision it can carry
|
|
1300
|
+
// (kontourai/survey#203).
|
|
1301
|
+
const decision = item ? keepActionDecision(item, wrong?.checked ?? false) : undefined;
|
|
1302
|
+
if (!decision) {
|
|
1303
|
+
return;
|
|
1304
|
+
}
|
|
1281
1305
|
controller.setDecision(itemName, decision);
|
|
1282
1306
|
controller.renderCurrentState();
|
|
1283
1307
|
});
|
|
@@ -248,6 +248,17 @@ h1, h2, h3, p {
|
|
|
248
248
|
transition: width 0.35s cubic-bezier(0.2, 0.7, 0.3, 1);
|
|
249
249
|
}
|
|
250
250
|
|
|
251
|
+
/* `.progress` is a generic class name, and a host page may well have its own —
|
|
252
|
+
@kontourai/ui ships an unscoped `.progress span { background: linear-gradient(…) }`
|
|
253
|
+
that painted a gradient straight over this element's text in an embed. The
|
|
254
|
+
embed cannot claim the name, but it must not leave its own descendants
|
|
255
|
+
unstyled and open to a host rule; state the presentation explicitly. */
|
|
256
|
+
.progress .ptext {
|
|
257
|
+
display: inline;
|
|
258
|
+
height: auto;
|
|
259
|
+
background: none;
|
|
260
|
+
}
|
|
261
|
+
|
|
251
262
|
.ptext {
|
|
252
263
|
font-size: 13px;
|
|
253
264
|
color: var(--k-muted);
|
package/package.json
CHANGED