@kontourai/survey 2.2.1 → 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 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 defaults are emitted in overridable form
173
- (`--k-brand: var(--k-brand, <default>)`), so a host brand is authoritative: set
174
- any `--k-*` token on an ancestor of the embed (or inline on the embed element,
175
- or the host element of the web component) and it propagates in — no need to
176
- out-specificity the embed's own selectors. Unset tokens keep their default, so
177
- you only declare what you re-brand.
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: var(--k-bg, #0a0e13);
10
- --k-panel: var(--k-panel, #111824);
11
- --k-panel-raised: var(--k-panel-raised, #16202d);
12
- --k-line: var(--k-line, rgba(150, 180, 210, 0.12));
13
- --k-line-strong: var(--k-line-strong, rgba(150, 180, 210, 0.22));
14
- --k-shadow: var(--k-shadow, 0 26px 60px -42px rgba(0, 0, 0, 0.95));
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: var(--k-text, #eef3f8);
18
- --k-text-muted: var(--k-text-muted, #aebccb);
19
- --k-text-faint: var(--k-text-faint, #72869b);
17
+ --k-text: #eef3f8;
18
+ --k-text-muted: #aebccb;
19
+ --k-text-faint: #72869b;
20
20
 
21
21
  /* brand */
22
- --k-brand: var(--k-brand, #5ce0c6);
23
- --k-brand-contrast: var(--k-brand-contrast, #06080b);
22
+ --k-brand: #5ce0c6;
23
+ --k-brand-contrast: #06080b;
24
24
 
25
25
  /* semantic status scale */
26
- --k-positive: var(--k-positive, #34d399);
27
- --k-caution: var(--k-caution, #f3b14b);
28
- --k-negative: var(--k-negative, #ff6f6f);
29
- --k-neutral: var(--k-neutral, #6f8095);
30
- --k-active: var(--k-active, #7aa2ff);
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: var(--k-space-1, 4px);
38
- --k-space-2: var(--k-space-2, 8px);
39
- --k-space-3: var(--k-space-3, 12px);
40
- --k-space-4: var(--k-space-4, 16px);
41
- --k-space-5: var(--k-space-5, 24px);
42
- --k-space-6: var(--k-space-6, 32px);
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: var(--k-radius-sm, 9px);
46
- --k-radius-md: var(--k-radius-md, 14px);
45
+ --k-radius-sm: 9px;
46
+ --k-radius-md: 14px;
47
47
 
48
48
  /* type */
49
- --k-text-xs: var(--k-text-xs, 11px);
50
- --k-text-sm: var(--k-text-sm, 12.5px);
51
- --k-text-md: var(--k-text-md, 14px);
52
- --k-text-lg: var(--k-text-lg, 18px);
53
- --k-text-xl: var(--k-text-xl, 22px);
54
- --k-text-2xl: var(--k-text-2xl, clamp(26px, 3.4vw, 38px));
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: var(--k-font-display, "Fraunces", Georgia, "Times New Roman", serif);
58
- --k-font-ui: var(--k-font-ui, "Hanken Grotesk", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif);
59
- --k-font-mono: var(--k-font-mono, "IBM Plex Mono", ui-monospace, SFMono-Regular, Menlo, monospace);
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: var(--k-ease, cubic-bezier(0.22, 1, 0.36, 1));
63
- --k-dur: var(--k-dur, 0.16s);
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: var(--k-bg, #f5f4ef);
69
- --k-panel: var(--k-panel, #ffffff);
70
- --k-panel-raised: var(--k-panel-raised, #fbfaf7);
71
- --k-line: var(--k-line, rgba(36, 40, 46, 0.12));
72
- --k-line-strong: var(--k-line-strong, rgba(36, 40, 46, 0.20));
73
- --k-shadow: var(--k-shadow, 0 18px 50px -30px rgba(0, 0, 0, 0.25));
74
- --k-text: var(--k-text, #202124);
75
- --k-text-muted: var(--k-text-muted, #5b626b);
76
- --k-text-faint: var(--k-text-faint, #707782);
77
- --k-brand-contrast: var(--k-brand-contrast, #ffffff);
78
- --k-positive: var(--k-positive, #168257);
79
- --k-caution: var(--k-caution, #8a5a00);
80
- --k-negative: var(--k-negative, #c83b3b);
81
- --k-neutral: var(--k-neutral, #5f6975);
82
- --k-active: var(--k-active, #3f6fd6);
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: var(--k-bg, #06080b);
88
- --k-brand: var(--k-brand, #5ce0c6);
89
- --k-text-faint: var(--k-text-faint, #6f8095);
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: var(--k-font-ui, "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif);
94
- --k-font-display: var(--k-font-display, "Aptos Narrow", "DIN Condensed", "IBM Plex Sans Condensed", "Gill Sans", sans-serif);
95
- --k-radius-sm: var(--k-radius-sm, 0);
96
- --k-radius-md: var(--k-radius-md, 0);
97
- --k-brand: var(--k-brand, #c9ff4a);
98
- --k-brand-contrast: var(--k-brand-contrast, #11120f);
99
- --k-bg: var(--k-bg, #11120f);
100
- --k-panel: var(--k-panel, #191b16);
101
- --k-panel-raised: var(--k-panel-raised, #20231e);
102
- --k-line: var(--k-line, #3a4035);
103
- --k-line-strong: var(--k-line-strong, #8ea36e);
104
- --k-text: var(--k-text, #f2f0e8);
105
- --k-text-muted: var(--k-text-muted, #a6ab9c);
106
- --k-text-faint: var(--k-text-faint, #747a6c);
107
- --k-positive: var(--k-positive, #a6d37b);
108
- --k-caution: var(--k-caution, #e8c15f);
109
- --k-negative: var(--k-negative, #ee776f);
110
- --k-active: var(--k-active, #84d8c8);
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: var(--k-brand, #2f88a6);
114
+ --k-brand: #2f88a6;
115
115
  }
116
116
 
117
117
  .survey-workbench-embed.theme-surface{
118
- --k-brand: var(--k-brand, #14a37a);
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: var(--k-brand, #16806f);
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: var(--k-brand, #6c9400);
129
- --k-brand-contrast: var(--k-brand-contrast, #ffffff);
130
- --k-bg: var(--k-bg, #f3f5eb);
131
- --k-panel: var(--k-panel, #fbfcf7);
132
- --k-panel-raised: var(--k-panel-raised, #eef2e6);
133
- --k-line: var(--k-line, #ccd5bf);
134
- --k-line-strong: var(--k-line-strong, #8fa36f);
135
- --k-text: var(--k-text, #1e2319);
136
- --k-text-muted: var(--k-text-muted, #596250);
137
- --k-text-faint: var(--k-text-faint, #77816d);
138
- --k-positive: var(--k-positive, #2f7d32);
139
- --k-caution: var(--k-caution, #8a6500);
140
- --k-negative: var(--k-negative, #b93a36);
141
- --k-active: var(--k-active, #247f75);
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: var(--k-brand, #1f6f88);
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: var(--k-brand, #0f6b52);
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);
@@ -423,7 +423,7 @@ export const facilityCredentialReviewItemExample = {
423
423
  selectedCandidateId: "facility-credential-review-operating-license:candidate:current",
424
424
  },
425
425
  };
426
- function queueFixture(name, target, currentValue, proposedValue, candidateSetStatus, feedbackTags) {
426
+ function queueFixture({ name, target, currentValue, proposedValue, candidateSetStatus, feedbackTags, proposalId, excerpts, }) {
427
427
  return {
428
428
  ...publicDirectoryReviewItemExample,
429
429
  metadata: {
@@ -450,6 +450,11 @@ function queueFixture(name, target, currentValue, proposedValue, candidateSetSta
450
450
  ...candidate.source,
451
451
  sourceId: `${name}:source:${role}`,
452
452
  },
453
+ locator: {
454
+ ...candidate.locator,
455
+ locator: `html:field=${target}`,
456
+ excerpt: role === "proposed" ? excerpts.proposed : excerpts.current,
457
+ },
453
458
  extraction: {
454
459
  ...candidate.extraction,
455
460
  extractionId: `${name}:extraction:${role}`,
@@ -471,6 +476,15 @@ function queueFixture(name, target, currentValue, proposedValue, candidateSetSta
471
476
  },
472
477
  producer: {
473
478
  ...candidate.producer,
479
+ ...(role === "proposed" ? { proposalId, oldValue: currentValue } : {}),
480
+ ...(candidate.producer?.sourceAuthority
481
+ ? {
482
+ sourceAuthority: {
483
+ ...candidate.producer.sourceAuthority,
484
+ scope: `${target} field on entity-123`,
485
+ },
486
+ }
487
+ : {}),
474
488
  },
475
489
  };
476
490
  }),
@@ -652,12 +666,60 @@ const dailyHoursNoSourceReviewItemExample = {
652
666
  status: { observedCandidateCount: 2 },
653
667
  };
654
668
  export const reviewWorkbenchQueueExamples = [
655
- queueFixture("public-directory-hours", "hours", "Weekdays 9am-5pm", "Weekdays 8am-6pm", "needs-review", ["hours-change", "crawler-suggested"]),
656
- queueFixture("public-directory-phone", "phoneNumber", "+1-555-0100", "+1-555-0199", "needs-review", ["contact-field", "source-conflict"]),
669
+ queueFixture({
670
+ name: "public-directory-hours",
671
+ target: "hours",
672
+ currentValue: "Weekdays 9am-5pm",
673
+ proposedValue: "Weekdays 8am-6pm",
674
+ candidateSetStatus: "needs-review",
675
+ feedbackTags: ["hours-change", "crawler-suggested"],
676
+ proposalId: "proposal-451",
677
+ excerpts: {
678
+ current: "Program hours: Weekdays 9am-5pm. Closed weekends and public holidays.",
679
+ proposed: "Extended schedule for the fall term — program hours are now Weekdays 8am-6pm.",
680
+ },
681
+ }),
682
+ queueFixture({
683
+ name: "public-directory-phone",
684
+ target: "phoneNumber",
685
+ currentValue: "+1-555-0100",
686
+ proposedValue: "+1-555-0199",
687
+ candidateSetStatus: "needs-review",
688
+ feedbackTags: ["contact-field", "source-conflict"],
689
+ proposalId: "proposal-452",
690
+ excerpts: {
691
+ current: "Questions? Call the main office at +1-555-0100.",
692
+ proposed: "Our enrollment line has moved. Call +1-555-0199 to reach the office.",
693
+ },
694
+ }),
657
695
  dropInPriceReviewItemExample,
658
696
  dailyHoursNoSourceReviewItemExample,
659
697
  publicDirectoryReviewItemExample,
660
- queueFixture("public-directory-address", "streetAddress", "100 Main Street", "102 Main Street", "needs-review", ["address-change", "producer-escalation-candidate"]),
661
- queueFixture("public-directory-license", "licenseStatus", "ACTIVE", "EXPIRED", "escalated", ["licensing", "manual-review-required"]),
698
+ queueFixture({
699
+ name: "public-directory-address",
700
+ target: "streetAddress",
701
+ currentValue: "100 Main Street",
702
+ proposedValue: "102 Main Street",
703
+ candidateSetStatus: "needs-review",
704
+ feedbackTags: ["address-change", "producer-escalation-candidate"],
705
+ proposalId: "proposal-453",
706
+ excerpts: {
707
+ current: "Find us at 100 Main Street, Example City.",
708
+ proposed: "We have moved one door down to 102 Main Street, Example City.",
709
+ },
710
+ }),
711
+ queueFixture({
712
+ name: "public-directory-license",
713
+ target: "licenseStatus",
714
+ currentValue: "ACTIVE",
715
+ proposedValue: "EXPIRED",
716
+ candidateSetStatus: "escalated",
717
+ feedbackTags: ["licensing", "manual-review-required"],
718
+ proposalId: "proposal-454",
719
+ excerpts: {
720
+ current: "Operating license status: ACTIVE (renewed 2025-06-01).",
721
+ proposed: "Operating license status: EXPIRED as of 2026-05-31. Renewal not yet filed.",
722
+ },
723
+ }),
662
724
  regulatedRuleConflictReviewItemExample,
663
725
  ];