@kontourai/survey 0.5.2 → 0.6.0

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.
@@ -0,0 +1,45 @@
1
+ /**
2
+ * <survey-review-workbench> — Custom element wrapper for the Survey Review Workbench.
3
+ *
4
+ * Works like <surface-trust-panel>: data via the `.session` property OR a `src`
5
+ * attribute that fetches a JSON-serialised ReviewQueueSessionState. Shadow DOM
6
+ * is used throughout so CSS custom properties (--k-*) inherit from the host
7
+ * with no separate stylesheet required — a single module import is sufficient.
8
+ *
9
+ * Usage (single import, self-contained):
10
+ * import "@kontourai/survey/review-workbench/element";
11
+ * <survey-review-workbench theme="survey" color-scheme="dark"
12
+ * src="/api/sessions/my-session.json">
13
+ * </survey-review-workbench>
14
+ *
15
+ * // or via property:
16
+ * const el = document.querySelector("survey-review-workbench");
17
+ * el.session = reviewQueueSession;
18
+ * el.presentationAdapter = myPresentationAdapter;
19
+ *
20
+ * Theming:
21
+ * CSS custom properties (--k-*) inherit through the shadow boundary.
22
+ * Setting any --k-* token on the element or an ancestor overrides the
23
+ * shadow :host defaults. Token defaults are declared on :host so that
24
+ * host-page rules always win over the shadow defaults.
25
+ *
26
+ * Attributes:
27
+ * theme — "survey" | "console" | "flow" | "surface" (maps to theme-* class)
28
+ * color-scheme — "dark" | "light" (default: "dark")
29
+ * src — URL to fetch a JSON-serialised ReviewQueueSessionState
30
+ */
31
+ import { type ReviewQueueSessionState, type ReviewWorkbenchState } from "./review-workbench.js";
32
+ import type { ReviewPresentationAdapter } from "./review-presentation.js";
33
+ export declare class SurveyReviewWorkbenchElement extends HTMLElement {
34
+ #private;
35
+ static readonly observedAttributes: string[];
36
+ constructor();
37
+ /** The review queue session to display. Setting this property re-mounts the workbench. */
38
+ get session(): ReviewQueueSessionState | ReviewWorkbenchState | null;
39
+ set session(value: ReviewQueueSessionState | ReviewWorkbenchState | null | undefined);
40
+ /** Optional presentation adapter for custom labels, value summaries, and links. */
41
+ get presentationAdapter(): ReviewPresentationAdapter | undefined;
42
+ set presentationAdapter(value: ReviewPresentationAdapter | undefined);
43
+ connectedCallback(): void;
44
+ attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null): void;
45
+ }
@@ -0,0 +1,215 @@
1
+ /**
2
+ * <survey-review-workbench> — Custom element wrapper for the Survey Review Workbench.
3
+ *
4
+ * Works like <surface-trust-panel>: data via the `.session` property OR a `src`
5
+ * attribute that fetches a JSON-serialised ReviewQueueSessionState. Shadow DOM
6
+ * is used throughout so CSS custom properties (--k-*) inherit from the host
7
+ * with no separate stylesheet required — a single module import is sufficient.
8
+ *
9
+ * Usage (single import, self-contained):
10
+ * import "@kontourai/survey/review-workbench/element";
11
+ * <survey-review-workbench theme="survey" color-scheme="dark"
12
+ * src="/api/sessions/my-session.json">
13
+ * </survey-review-workbench>
14
+ *
15
+ * // or via property:
16
+ * const el = document.querySelector("survey-review-workbench");
17
+ * el.session = reviewQueueSession;
18
+ * el.presentationAdapter = myPresentationAdapter;
19
+ *
20
+ * Theming:
21
+ * CSS custom properties (--k-*) inherit through the shadow boundary.
22
+ * Setting any --k-* token on the element or an ancestor overrides the
23
+ * shadow :host defaults. Token defaults are declared on :host so that
24
+ * host-page rules always win over the shadow defaults.
25
+ *
26
+ * Attributes:
27
+ * theme — "survey" | "console" | "flow" | "surface" (maps to theme-* class)
28
+ * color-scheme — "dark" | "light" (default: "dark")
29
+ * src — URL to fetch a JSON-serialised ReviewQueueSessionState
30
+ */
31
+ import { mountReviewWorkbench, } from "./review-workbench.js";
32
+ import { REVIEW_WORKBENCH_CSS } from "./review-workbench-css.generated.js";
33
+ export class SurveyReviewWorkbenchElement extends HTMLElement {
34
+ static observedAttributes = ["theme", "color-scheme", "src"];
35
+ #session = null;
36
+ #presentationAdapter = undefined;
37
+ #root;
38
+ #mountRoot;
39
+ #mounted = false;
40
+ constructor() {
41
+ super();
42
+ this.#root = this.attachShadow({ mode: "open" });
43
+ // Inject the workbench CSS directly from the generated module.
44
+ // This makes the element fully self-contained: a single
45
+ // `import "@kontourai/survey/review-workbench/element"` is all that's needed.
46
+ const sheet = this.#adoptCss();
47
+ if (!sheet) {
48
+ const styleEl = document.createElement("style");
49
+ styleEl.textContent = REVIEW_WORKBENCH_CSS;
50
+ this.#root.appendChild(styleEl);
51
+ }
52
+ // Host token defaults: CSS custom properties inherit through the shadow
53
+ // boundary so host overrides win automatically. These are the defaults
54
+ // that apply when no ancestor sets the tokens.
55
+ const hostStyle = document.createElement("style");
56
+ hostStyle.textContent = `
57
+ :host {
58
+ display: block;
59
+ container-type: inline-size;
60
+ /* --k-bg, --k-panel, etc. are set by the Console Kit theme stylesheet.
61
+ Declare fallback token values here so shadow content has something
62
+ to work with even if the host doesn't load Console Kit. */
63
+ --k-bg: var(--k-bg, #060a10);
64
+ --k-panel: var(--k-panel, #101822);
65
+ --k-panel-raised: var(--k-panel-raised, #161e2b);
66
+ --k-text: var(--k-text, #e8eaf0);
67
+ --k-text-muted: var(--k-text-muted, #8b93a8);
68
+ --k-text-faint: var(--k-text-faint, #4e5870);
69
+ --k-line: var(--k-line, rgba(255,255,255,0.08));
70
+ --k-line-strong: var(--k-line-strong, rgba(255,255,255,0.14));
71
+ --k-brand: var(--k-brand, #5ce0c6);
72
+ --k-active: var(--k-active, #7aa2ff);
73
+ --k-positive: var(--k-positive, #34d399);
74
+ --k-caution: var(--k-caution, #f3b14b);
75
+ --k-negative: var(--k-negative, #ff6f6f);
76
+ --k-radius-md: var(--k-radius-md, 10px);
77
+ --k-radius-sm: var(--k-radius-sm, 6px);
78
+ --k-font-ui: var(--k-font-ui, "Hanken Grotesk", system-ui, sans-serif);
79
+ }
80
+ .workbench-empty, .workbench-error {
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ min-height: 6rem;
85
+ padding: 1.5rem;
86
+ font-family: var(--k-font-ui, system-ui, sans-serif);
87
+ font-size: 0.9rem;
88
+ color: var(--k-text-muted, #8b93a8);
89
+ background: var(--k-panel, #101822);
90
+ border-radius: var(--k-radius-md, 10px);
91
+ }
92
+ .workbench-error {
93
+ color: var(--k-negative, #ff6f6f);
94
+ }
95
+ `;
96
+ this.#root.appendChild(hostStyle);
97
+ this.#mountRoot = document.createElement("div");
98
+ this.#mountRoot.className = "workbench survey-workbench-embed";
99
+ this.#root.appendChild(this.#mountRoot);
100
+ }
101
+ /** The review queue session to display. Setting this property re-mounts the workbench. */
102
+ get session() {
103
+ return this.#session;
104
+ }
105
+ set session(value) {
106
+ this.#session = value ?? null;
107
+ this.#remount();
108
+ }
109
+ /** Optional presentation adapter for custom labels, value summaries, and links. */
110
+ get presentationAdapter() {
111
+ return this.#presentationAdapter;
112
+ }
113
+ set presentationAdapter(value) {
114
+ this.#presentationAdapter = value;
115
+ this.#remount();
116
+ }
117
+ connectedCallback() {
118
+ // Rescue a `session` set before the element was upgraded so the property
119
+ // assignment reaches the class accessor instead of being shadowed by an
120
+ // own property. Mirrors the same pattern in <surface-trust-panel>.
121
+ if (Object.prototype.hasOwnProperty.call(this, "session")) {
122
+ const pending = this.session;
123
+ delete this.session;
124
+ this.session = pending;
125
+ return;
126
+ }
127
+ this.#applyThemeClasses();
128
+ const src = this.getAttribute("src");
129
+ if (this.#session === null && src) {
130
+ void this.#load(src);
131
+ }
132
+ else {
133
+ this.#remount();
134
+ }
135
+ }
136
+ attributeChangedCallback(name, oldValue, newValue) {
137
+ if (name === "src" && newValue && newValue !== oldValue) {
138
+ void this.#load(newValue);
139
+ return;
140
+ }
141
+ this.#applyThemeClasses();
142
+ }
143
+ async #load(src) {
144
+ try {
145
+ const response = await fetch(src);
146
+ if (!response.ok)
147
+ throw new Error(`Failed to load session: HTTP ${response.status}`);
148
+ this.session = await response.json();
149
+ }
150
+ catch (error) {
151
+ this.#renderError(error instanceof Error ? error.message : String(error));
152
+ }
153
+ }
154
+ #applyThemeClasses() {
155
+ const theme = this.getAttribute("theme") ?? "survey";
156
+ const colorScheme = this.getAttribute("color-scheme") ?? "dark";
157
+ this.#mountRoot.className = `workbench survey-workbench-embed theme-${theme}`;
158
+ this.#mountRoot.setAttribute("data-color-scheme", colorScheme);
159
+ if (colorScheme === "light") {
160
+ this.#mountRoot.removeAttribute("data-dark");
161
+ }
162
+ else {
163
+ this.#mountRoot.setAttribute("data-dark", "");
164
+ }
165
+ }
166
+ #remount() {
167
+ if (!this.isConnected) {
168
+ return;
169
+ }
170
+ if (this.#session === null) {
171
+ this.#renderEmpty();
172
+ return;
173
+ }
174
+ const options = this.#presentationAdapter
175
+ ? { presentationAdapter: this.#presentationAdapter }
176
+ : {};
177
+ this.#applyThemeClasses();
178
+ mountReviewWorkbench(this.#mountRoot, this.#session, options);
179
+ this.#mounted = true;
180
+ }
181
+ #renderEmpty() {
182
+ this.#mountRoot.innerHTML = '<div class="workbench-empty">No review session loaded yet.</div>';
183
+ this.#mounted = false;
184
+ }
185
+ #renderError(message) {
186
+ this.#mountRoot.innerHTML = `<div class="workbench-error">${escapeHtml(message)}</div>`;
187
+ this.#mounted = false;
188
+ }
189
+ /** Attempt to inject the workbench CSS via constructable CSSStyleSheet. */
190
+ #adoptCss() {
191
+ try {
192
+ if (typeof CSSStyleSheet === "undefined" || !this.#root.adoptedStyleSheets) {
193
+ return false;
194
+ }
195
+ const sheet = new CSSStyleSheet();
196
+ sheet.replaceSync(REVIEW_WORKBENCH_CSS);
197
+ this.#root.adoptedStyleSheets = [sheet];
198
+ return true;
199
+ }
200
+ catch {
201
+ return false;
202
+ }
203
+ }
204
+ }
205
+ function escapeHtml(text) {
206
+ return text
207
+ .replaceAll("&", "&amp;")
208
+ .replaceAll("<", "&lt;")
209
+ .replaceAll(">", "&gt;")
210
+ .replaceAll('"', "&quot;");
211
+ }
212
+ // Register the element unless it's already defined (supports HMR scenarios).
213
+ if (typeof customElements !== "undefined" && !customElements.get("survey-review-workbench")) {
214
+ customElements.define("survey-review-workbench", SurveyReviewWorkbenchElement);
215
+ }
@@ -319,6 +319,17 @@
319
319
  font-size: 0.95em;
320
320
  }
321
321
 
322
+ .survey-workbench-embed .review-target-label{
323
+ margin-top: 4px;
324
+ }
325
+
326
+ .survey-workbench-embed .review-target-label strong{
327
+ color: var(--paper);
328
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
329
+ font-size: 13px;
330
+ font-weight: 600;
331
+ }
332
+
322
333
  .survey-workbench-embed .meta-grid{
323
334
  display: none;
324
335
  grid-template-columns: repeat(4, minmax(110px, 1fr));
@@ -373,6 +384,52 @@
373
384
  display: none;
374
385
  }
375
386
 
387
+ /* ---- Mobile queue progress bar (mobile only) -------------------------- */
388
+
389
+ .survey-workbench-embed .mobile-queue-bar{
390
+ display: none;
391
+ }
392
+
393
+ /* ---- Queue drawer backdrop -------------------------------------------- */
394
+
395
+ .survey-workbench-embed .queue-drawer-backdrop{
396
+ display: none;
397
+ position: absolute;
398
+ inset: 0;
399
+ z-index: 40;
400
+ background: rgba(0, 0, 0, 0.55);
401
+ backdrop-filter: blur(2px);
402
+ opacity: 0;
403
+ transition: opacity 0.22s ease;
404
+ pointer-events: none;
405
+ }
406
+
407
+ .survey-workbench-embed .queue-drawer-backdrop.is-visible{
408
+ opacity: 1;
409
+ pointer-events: auto;
410
+ }
411
+
412
+ /* ---- Queue drawer close button (mobile only) -------------------------- */
413
+
414
+ .survey-workbench-embed .queue-drawer-close{
415
+ display: none;
416
+ position: absolute;
417
+ top: 12px;
418
+ right: 12px;
419
+ width: 30px;
420
+ height: 30px;
421
+ border: 1px solid var(--line-strong);
422
+ border-radius: 999px;
423
+ background: rgba(8, 12, 18, 0.8);
424
+ color: var(--paper-faint);
425
+ cursor: pointer;
426
+ font-size: 14px;
427
+ line-height: 1;
428
+ align-items: center;
429
+ justify-content: center;
430
+ z-index: 2;
431
+ }
432
+
376
433
  .survey-workbench-embed .queue-list,
377
434
  .survey-workbench-embed .session-summary,
378
435
  .survey-workbench-embed .session-audit{
@@ -1044,6 +1101,53 @@
1044
1101
  gap: 11px;
1045
1102
  }
1046
1103
 
1104
+ /* When surface-preview is a <details> element */
1105
+ .survey-workbench-embed details.surface-preview{
1106
+ display: block;
1107
+ }
1108
+
1109
+ .survey-workbench-embed details.surface-preview[open]{
1110
+ display: grid;
1111
+ gap: 11px;
1112
+ }
1113
+
1114
+ .survey-workbench-embed .surface-summary{
1115
+ display: flex;
1116
+ gap: 12px;
1117
+ justify-content: space-between;
1118
+ align-items: center;
1119
+ cursor: pointer;
1120
+ list-style: none;
1121
+ padding: 0;
1122
+ }
1123
+
1124
+ .survey-workbench-embed .surface-summary::-webkit-details-marker{
1125
+ display: none;
1126
+ }
1127
+
1128
+ .survey-workbench-embed .surface-summary::before{
1129
+ content: none;
1130
+ }
1131
+
1132
+ .survey-workbench-embed .surface-summary-label{
1133
+ font-family: "Fraunces", Georgia, serif;
1134
+ font-weight: 600;
1135
+ font-size: 16px;
1136
+ letter-spacing: -0.01em;
1137
+ color: var(--paper);
1138
+ }
1139
+
1140
+ .survey-workbench-embed .surface-summary-label::before{
1141
+ content: "› ";
1142
+ color: var(--accent);
1143
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
1144
+ font-size: 13px;
1145
+ }
1146
+
1147
+ .survey-workbench-embed details.surface-preview[open] .surface-summary-label::before{
1148
+ content: "⌄ ";
1149
+ }
1150
+
1047
1151
  .survey-workbench-embed .surface-head{
1048
1152
  display: flex;
1049
1153
  gap: 12px;
@@ -1066,6 +1170,13 @@
1066
1170
  line-height: 1.5;
1067
1171
  }
1068
1172
 
1173
+ .survey-workbench-embed .preview-disclaimer-footer{
1174
+ margin-top: 4px;
1175
+ border-left-color: var(--line);
1176
+ font-size: 11.5px;
1177
+ color: var(--paper-faint);
1178
+ }
1179
+
1069
1180
  .survey-workbench-embed .preview-section-grid{
1070
1181
  display: grid;
1071
1182
  grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1208,7 +1319,6 @@
1208
1319
 
1209
1320
  @media (max-width: 980px) {
1210
1321
  .survey-workbench-embed .topbar,
1211
- .survey-workbench-embed .queue-layout,
1212
1322
  .survey-workbench-embed .focus-evidence,
1213
1323
  .survey-workbench-embed .candidate-grid{
1214
1324
  grid-template-columns: 1fr;
@@ -1223,9 +1333,8 @@
1223
1333
  }
1224
1334
 
1225
1335
  .survey-workbench-embed .queue-layout{
1226
- grid-template-areas:
1227
- "content"
1228
- "queue";
1336
+ display: block;
1337
+ position: relative;
1229
1338
  }
1230
1339
 
1231
1340
  .survey-workbench-embed .active-review-strip{
@@ -1286,8 +1395,85 @@
1286
1395
  font-size: 11px;
1287
1396
  }
1288
1397
 
1289
- .survey-workbench-embed .queue-panel{
1290
- grid-area: queue;
1398
+ .survey-workbench-embed .queue-drawer-backdrop{
1399
+ display: block;
1400
+ }
1401
+
1402
+ .survey-workbench-embed .queue-drawer-close{
1403
+ display: flex;
1404
+ }
1405
+
1406
+ .survey-workbench-embed .mobile-queue-bar{
1407
+ display: grid;
1408
+ grid-template-columns: minmax(0, 1fr) auto;
1409
+ gap: 12px;
1410
+ align-items: center;
1411
+ border: 1px solid var(--line-strong);
1412
+ border-left: 3px solid var(--accent);
1413
+ border-radius: var(--radius);
1414
+ padding: 11px 14px;
1415
+ margin-bottom: 14px;
1416
+ background:
1417
+ linear-gradient(90deg, rgba(92, 224, 198, 0.08), transparent 46%),
1418
+ linear-gradient(180deg, var(--ink-800), var(--ink-850));
1419
+ animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
1420
+ }
1421
+
1422
+ .survey-workbench-embed .mobile-queue-progress{
1423
+ display: grid;
1424
+ gap: 3px;
1425
+ min-width: 0;
1426
+ }
1427
+
1428
+ .survey-workbench-embed .mobile-queue-item-label{
1429
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
1430
+ font-size: 13px;
1431
+ font-weight: 600;
1432
+ color: var(--paper);
1433
+ overflow-wrap: anywhere;
1434
+ word-break: break-word;
1435
+ }
1436
+
1437
+ .survey-workbench-embed .mobile-queue-open{
1438
+ flex-shrink: 0;
1439
+ min-height: 34px;
1440
+ padding: 0 14px;
1441
+ border: 1px solid var(--line-strong);
1442
+ border-radius: 999px;
1443
+ background: rgba(92, 224, 198, 0.08);
1444
+ color: var(--accent);
1445
+ cursor: pointer;
1446
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
1447
+ font-size: 11px;
1448
+ font-weight: 600;
1449
+ letter-spacing: 0.06em;
1450
+ transition: background 0.16s ease, border-color 0.16s ease;
1451
+ }
1452
+
1453
+ .survey-workbench-embed .mobile-queue-open:hover{
1454
+ background: rgba(92, 224, 198, 0.16);
1455
+ border-color: var(--accent);
1456
+ }
1457
+
1458
+ /* Higher specificity than the @container stacked-layout fallback below:
1459
+ on a narrow viewport the drawer must win even inside a narrow container. */
1460
+ .survey-workbench-embed .queue-layout .queue-panel{
1461
+ position: absolute;
1462
+ top: 0;
1463
+ left: 0;
1464
+ bottom: 0;
1465
+ width: min(320px, 90vw);
1466
+ z-index: 50;
1467
+ overflow-y: auto;
1468
+ overflow-x: hidden;
1469
+ transform: translateX(-105%);
1470
+ transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
1471
+ border-radius: 0 var(--radius) var(--radius) 0;
1472
+ padding-top: 50px;
1473
+ }
1474
+
1475
+ .survey-workbench-embed .queue-layout .queue-panel.is-open{
1476
+ transform: translateX(0);
1291
1477
  }
1292
1478
 
1293
1479
  .survey-workbench-embed .content-grid{
@@ -1298,10 +1484,6 @@
1298
1484
  display: none;
1299
1485
  }
1300
1486
 
1301
- .survey-workbench-embed .queue-panel{
1302
- position: static;
1303
- }
1304
-
1305
1487
  .survey-workbench-embed .content-grid,
1306
1488
  .survey-workbench-embed .review-main,
1307
1489
  .survey-workbench-embed .candidate-grid,
@@ -1481,11 +1663,11 @@
1481
1663
  }
1482
1664
 
1483
1665
  .survey-workbench-embed .focus-value strong{
1484
- font-size: clamp(22px, 10vw, 32px);
1666
+ font-size: clamp(16px, 5cqi, 22px);
1485
1667
  }
1486
1668
 
1487
1669
  .survey-workbench-embed .candidate-value .field-value{
1488
- font-size: clamp(21px, 9vw, 30px);
1670
+ font-size: clamp(15px, 4.5cqi, 20px);
1489
1671
  line-height: 1.12;
1490
1672
  }
1491
1673
 
@@ -1590,7 +1772,7 @@
1590
1772
 
1591
1773
  .survey-workbench-embed .focus-value strong{
1592
1774
  margin-top: 6px;
1593
- font-size: 24px;
1775
+ font-size: 18px;
1594
1776
  line-height: 1.05;
1595
1777
  }
1596
1778
 
@@ -1599,7 +1781,7 @@
1599
1781
  }
1600
1782
 
1601
1783
  .survey-workbench-embed .candidate-value .field-value{
1602
- font-size: 20px;
1784
+ font-size: 17px;
1603
1785
  }
1604
1786
  }
1605
1787