@kontourai/survey 0.5.2 → 0.7.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.
@@ -449,9 +449,12 @@ function renderReviewQueueSessionHtml(session, events, presentationAdapter) {
449
449
  return `
450
450
  <section class="workbench-shell" aria-label="Survey review workbench">
451
451
  ${renderWorkbenchHeader(state, presentationAdapter)}
452
+ ${renderMobileQueueBar(session, state, presentationAdapter)}
452
453
  ${renderActiveReviewStrip(session, state, presentationAdapter)}
453
454
  <div class="queue-layout">
454
- <aside class="queue-panel" aria-label="Review queue">
455
+ <div class="queue-drawer-backdrop" data-testid="queue-drawer-backdrop" aria-hidden="true"></div>
456
+ <aside class="queue-panel" aria-label="Review queue" role="dialog" aria-modal="false" id="queue-panel">
457
+ <button class="queue-drawer-close" type="button" data-testid="queue-drawer-close" aria-label="Close queue panel">&#x2715;</button>
455
458
  ${renderQueueRows(session, presentationAdapter)}
456
459
  ${renderSessionSummary(session)}
457
460
  ${renderSessionAudit(session, sessionExport)}
@@ -465,6 +468,28 @@ function renderReviewQueueSessionHtml(session, events, presentationAdapter) {
465
468
  </section>
466
469
  `;
467
470
  }
471
+ function renderMobileQueueBar(session, state, presentationAdapter) {
472
+ const activeIndex = session.items.findIndex((item) => item.metadata.name === session.activeItemName);
473
+ const position = activeIndex >= 0 ? activeIndex + 1 : 1;
474
+ const resolved = Object.keys(session.decisionsByItemName).length;
475
+ const presentation = buildReviewItemPresentation(state.item, presentationAdapter);
476
+ return `
477
+ <div class="mobile-queue-bar" data-testid="mobile-queue-bar" aria-label="Queue progress">
478
+ <div class="mobile-queue-progress">
479
+ <span class="field-label">Queue · ${resolved} of ${session.items.length} resolved</span>
480
+ <span class="mobile-queue-item-label">${escapeHtml(presentation.targetLabel)}</span>
481
+ </div>
482
+ <button
483
+ class="mobile-queue-open"
484
+ type="button"
485
+ data-testid="mobile-queue-open"
486
+ aria-controls="queue-panel"
487
+ aria-expanded="false"
488
+ aria-label="Open review queue"
489
+ >Queue</button>
490
+ </div>
491
+ `;
492
+ }
468
493
  function renderSessionAudit(session, sessionExport) {
469
494
  const lastEvents = sessionExport.events.slice(-6);
470
495
  const replayed = replayReviewSessionEvents({
@@ -585,17 +610,13 @@ function renderActiveReviewStrip(session, state, presentationAdapter) {
585
610
  `;
586
611
  }
587
612
  function renderWorkbenchHeader(state, presentationAdapter) {
588
- const current = candidateByRole(state.item, "current");
589
- const proposed = candidateByRole(state.item, "proposed");
590
613
  const presentation = buildReviewItemPresentation(state.item, presentationAdapter);
591
- const currentPresentation = buildReviewCandidatePresentation(state.item, current, presentationAdapter, presentation.targetLabel);
592
- const proposedPresentation = buildReviewCandidatePresentation(state.item, proposed, presentationAdapter, presentation.targetLabel);
593
614
  return `
594
615
  <header class="topbar">
595
616
  <div>
596
617
  <p class="eyebrow">${escapeHtml(String(state.item.metadata.producer?.displayName ?? "Survey"))}</p>
597
618
  <h1>Review candidate update</h1>
598
- <p class="review-question">For <strong>${escapeHtml(presentation.targetLabel)}</strong>, decide whether <strong>${escapeHtml(proposedPresentation.valueText)}</strong> should replace <strong>${escapeHtml(currentPresentation.valueText)}</strong>.</p>
619
+ <p class="review-target-label" data-testid="review-target-label"><strong>${escapeHtml(presentation.targetLabel)}</strong></p>
599
620
  </div>
600
621
  <dl class="meta-grid">
601
622
  ${metaItem("Item", state.item.metadata.name)}
@@ -719,32 +740,31 @@ function renderSurfacePreview(state, presentationAdapter) {
719
740
  }
720
741
  function renderPendingSurfacePreview() {
721
742
  return `
722
- <section class="surface-preview" data-testid="surface-preview" aria-label="Surface preview">
723
- <div class="surface-head">
724
- <h2>Surface preview</h2>
743
+ <details class="surface-preview" data-testid="surface-preview" aria-label="Surface preview">
744
+ <summary class="surface-summary">
745
+ <span class="surface-summary-label">Surface preview</span>
725
746
  <span class="state-label">pending</span>
726
- </div>
727
- <p class="preview-disclaimer">Choose a review decision to preview the source and review posture that would project toward Surface. Survey does not validate real-world truth.</p>
728
- </section>
747
+ </summary>
748
+ <p class="preview-disclaimer">Choose a review decision to preview the source and review posture that would project toward Surface.</p>
749
+ </details>
729
750
  `;
730
751
  }
731
752
  function renderPopulatedSurfacePreview(preview) {
732
753
  return `
733
- <section class="surface-preview" data-testid="surface-preview" aria-label="Surface preview">
734
- <div class="surface-head">
735
- <h2>Surface preview</h2>
754
+ <details class="surface-preview" data-testid="surface-preview" aria-label="Surface preview">
755
+ <summary class="surface-summary">
756
+ <span class="surface-summary-label">Surface preview</span>
736
757
  <span class="state-label">local preview</span>
737
- </div>
738
- <p class="preview-disclaimer" data-testid="surface-preview-disclaimer">${escapeHtml(preview.postureDisclaimer)}</p>
758
+ </summary>
739
759
  <div class="preview-section-grid">
740
760
  ${renderSurfacePreviewSections(preview)}
741
761
  </div>
742
- </section>
762
+ <p class="preview-disclaimer preview-disclaimer-footer" data-testid="surface-preview-disclaimer">${escapeHtml(preview.postureDisclaimer)}</p>
763
+ </details>
743
764
  `;
744
765
  }
745
766
  function renderSurfacePreviewSections(preview) {
746
767
  return [
747
- renderCanonicalClaim(preview),
748
768
  renderCandidateHistory(preview),
749
769
  renderSourceEvidence(preview),
750
770
  renderReviewEvent(preview),
@@ -942,11 +962,13 @@ function renderCurrentState(root, session, controller, presentationAdapter) {
942
962
  bindQueueRows(root, (itemName) => {
943
963
  controller.selectQueueItem(itemName);
944
964
  controller.renderCurrentState();
965
+ closeQueueDrawer(root);
945
966
  });
946
967
  bindNextUnresolved(root, () => {
947
968
  controller.goToNextUnresolved();
948
969
  controller.renderCurrentState();
949
970
  });
971
+ bindQueueDrawer(root);
950
972
  }
951
973
  function queueSessionFromStartState(startState) {
952
974
  if ("items" in startState) {
@@ -993,6 +1015,48 @@ function bindNextUnresolved(root, goToNextUnresolved) {
993
1015
  });
994
1016
  });
995
1017
  }
1018
+ function bindQueueDrawer(root) {
1019
+ const openButton = root.querySelector("[data-testid='mobile-queue-open']");
1020
+ const closeButton = root.querySelector("[data-testid='queue-drawer-close']");
1021
+ const backdrop = root.querySelector("[data-testid='queue-drawer-backdrop']");
1022
+ const panel = root.querySelector("#queue-panel");
1023
+ openButton?.addEventListener("click", () => openQueueDrawer(root));
1024
+ closeButton?.addEventListener("click", () => closeQueueDrawer(root));
1025
+ backdrop?.addEventListener("click", () => closeQueueDrawer(root));
1026
+ if (typeof root.addEventListener === "function") {
1027
+ root.addEventListener("keydown", (event) => {
1028
+ if (event.key === "Escape" && panel?.classList.contains("is-open")) {
1029
+ closeQueueDrawer(root);
1030
+ }
1031
+ });
1032
+ }
1033
+ }
1034
+ function openQueueDrawer(root) {
1035
+ const panel = root.querySelector("#queue-panel");
1036
+ const backdrop = root.querySelector("[data-testid='queue-drawer-backdrop']");
1037
+ const openButton = root.querySelector("[data-testid='mobile-queue-open']");
1038
+ if (!panel) {
1039
+ return;
1040
+ }
1041
+ panel.classList.add("is-open");
1042
+ panel.setAttribute("aria-modal", "true");
1043
+ backdrop?.classList.add("is-visible");
1044
+ openButton?.setAttribute("aria-expanded", "true");
1045
+ panel.querySelector("button, [tabindex]")?.focus();
1046
+ }
1047
+ function closeQueueDrawer(root) {
1048
+ const panel = root.querySelector("#queue-panel");
1049
+ const backdrop = root.querySelector("[data-testid='queue-drawer-backdrop']");
1050
+ const openButton = root.querySelector("[data-testid='mobile-queue-open']");
1051
+ if (!panel) {
1052
+ return;
1053
+ }
1054
+ panel.classList.remove("is-open");
1055
+ panel.setAttribute("aria-modal", "false");
1056
+ backdrop?.classList.remove("is-visible");
1057
+ openButton?.setAttribute("aria-expanded", "false");
1058
+ openButton?.focus();
1059
+ }
996
1060
  function scrollActiveReviewIntoView(root) {
997
1061
  if (typeof window === "undefined" || !window.matchMedia("(max-width: 980px)").matches) {
998
1062
  return;
@@ -169,6 +169,17 @@ h1 {
169
169
  font-size: 0.95em;
170
170
  }
171
171
 
172
+ .review-target-label {
173
+ margin-top: 4px;
174
+ }
175
+
176
+ .review-target-label strong {
177
+ color: var(--paper);
178
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
179
+ font-size: 13px;
180
+ font-weight: 600;
181
+ }
182
+
172
183
  .meta-grid {
173
184
  display: none;
174
185
  grid-template-columns: repeat(4, minmax(110px, 1fr));
@@ -223,6 +234,52 @@ h1 {
223
234
  display: none;
224
235
  }
225
236
 
237
+ /* ---- Mobile queue progress bar (mobile only) -------------------------- */
238
+
239
+ .mobile-queue-bar {
240
+ display: none;
241
+ }
242
+
243
+ /* ---- Queue drawer backdrop -------------------------------------------- */
244
+
245
+ .queue-drawer-backdrop {
246
+ display: none;
247
+ position: fixed;
248
+ inset: 0;
249
+ z-index: 40;
250
+ background: rgba(0, 0, 0, 0.55);
251
+ backdrop-filter: blur(2px);
252
+ opacity: 0;
253
+ transition: opacity 0.22s ease;
254
+ pointer-events: none;
255
+ }
256
+
257
+ .queue-drawer-backdrop.is-visible {
258
+ opacity: 1;
259
+ pointer-events: auto;
260
+ }
261
+
262
+ /* ---- Queue drawer close button (mobile only) -------------------------- */
263
+
264
+ .queue-drawer-close {
265
+ display: none;
266
+ position: absolute;
267
+ top: 12px;
268
+ right: 12px;
269
+ width: 30px;
270
+ height: 30px;
271
+ border: 1px solid var(--line-strong);
272
+ border-radius: 999px;
273
+ background: rgba(8, 12, 18, 0.8);
274
+ color: var(--paper-faint);
275
+ cursor: pointer;
276
+ font-size: 14px;
277
+ line-height: 1;
278
+ align-items: center;
279
+ justify-content: center;
280
+ z-index: 2;
281
+ }
282
+
226
283
  .queue-list,
227
284
  .session-summary,
228
285
  .session-audit {
@@ -894,6 +951,53 @@ textarea:focus {
894
951
  gap: 11px;
895
952
  }
896
953
 
954
+ /* When surface-preview is a <details> element */
955
+ details.surface-preview {
956
+ display: block;
957
+ }
958
+
959
+ details.surface-preview[open] {
960
+ display: grid;
961
+ gap: 11px;
962
+ }
963
+
964
+ .surface-summary {
965
+ display: flex;
966
+ gap: 12px;
967
+ justify-content: space-between;
968
+ align-items: center;
969
+ cursor: pointer;
970
+ list-style: none;
971
+ padding: 0;
972
+ }
973
+
974
+ .surface-summary::-webkit-details-marker {
975
+ display: none;
976
+ }
977
+
978
+ .surface-summary::before {
979
+ content: none;
980
+ }
981
+
982
+ .surface-summary-label {
983
+ font-family: "Fraunces", Georgia, serif;
984
+ font-weight: 600;
985
+ font-size: 16px;
986
+ letter-spacing: -0.01em;
987
+ color: var(--paper);
988
+ }
989
+
990
+ .surface-summary-label::before {
991
+ content: "› ";
992
+ color: var(--accent);
993
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
994
+ font-size: 13px;
995
+ }
996
+
997
+ details.surface-preview[open] .surface-summary-label::before {
998
+ content: "⌄ ";
999
+ }
1000
+
897
1001
  .surface-head {
898
1002
  display: flex;
899
1003
  gap: 12px;
@@ -916,6 +1020,13 @@ textarea:focus {
916
1020
  line-height: 1.5;
917
1021
  }
918
1022
 
1023
+ .preview-disclaimer-footer {
1024
+ margin-top: 4px;
1025
+ border-left-color: var(--line);
1026
+ font-size: 11.5px;
1027
+ color: var(--paper-faint);
1028
+ }
1029
+
919
1030
  .preview-section-grid {
920
1031
  display: grid;
921
1032
  grid-template-columns: repeat(2, minmax(0, 1fr));
@@ -1058,7 +1169,6 @@ pre {
1058
1169
 
1059
1170
  @media (max-width: 980px) {
1060
1171
  .topbar,
1061
- .queue-layout,
1062
1172
  .focus-evidence,
1063
1173
  .candidate-grid {
1064
1174
  grid-template-columns: 1fr;
@@ -1073,9 +1183,8 @@ pre {
1073
1183
  }
1074
1184
 
1075
1185
  .queue-layout {
1076
- grid-template-areas:
1077
- "content"
1078
- "queue";
1186
+ display: block;
1187
+ position: relative;
1079
1188
  }
1080
1189
 
1081
1190
  .active-review-strip {
@@ -1136,8 +1245,85 @@ pre {
1136
1245
  font-size: 11px;
1137
1246
  }
1138
1247
 
1139
- .queue-panel {
1140
- grid-area: queue;
1248
+ .queue-drawer-backdrop {
1249
+ display: block;
1250
+ }
1251
+
1252
+ .queue-drawer-close {
1253
+ display: flex;
1254
+ }
1255
+
1256
+ .mobile-queue-bar {
1257
+ display: grid;
1258
+ grid-template-columns: minmax(0, 1fr) auto;
1259
+ gap: 12px;
1260
+ align-items: center;
1261
+ border: 1px solid var(--line-strong);
1262
+ border-left: 3px solid var(--accent);
1263
+ border-radius: var(--radius);
1264
+ padding: 11px 14px;
1265
+ margin-bottom: 14px;
1266
+ background:
1267
+ linear-gradient(90deg, rgba(92, 224, 198, 0.08), transparent 46%),
1268
+ linear-gradient(180deg, var(--ink-800), var(--ink-850));
1269
+ animation: rise 0.6s cubic-bezier(0.22, 1, 0.36, 1) both;
1270
+ }
1271
+
1272
+ .mobile-queue-progress {
1273
+ display: grid;
1274
+ gap: 3px;
1275
+ min-width: 0;
1276
+ }
1277
+
1278
+ .mobile-queue-item-label {
1279
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
1280
+ font-size: 13px;
1281
+ font-weight: 600;
1282
+ color: var(--paper);
1283
+ overflow-wrap: anywhere;
1284
+ word-break: break-word;
1285
+ }
1286
+
1287
+ .mobile-queue-open {
1288
+ flex-shrink: 0;
1289
+ min-height: 34px;
1290
+ padding: 0 14px;
1291
+ border: 1px solid var(--line-strong);
1292
+ border-radius: 999px;
1293
+ background: rgba(92, 224, 198, 0.08);
1294
+ color: var(--accent);
1295
+ cursor: pointer;
1296
+ font-family: "IBM Plex Mono", ui-monospace, monospace;
1297
+ font-size: 11px;
1298
+ font-weight: 600;
1299
+ letter-spacing: 0.06em;
1300
+ transition: background 0.16s ease, border-color 0.16s ease;
1301
+ }
1302
+
1303
+ .mobile-queue-open:hover {
1304
+ background: rgba(92, 224, 198, 0.16);
1305
+ border-color: var(--accent);
1306
+ }
1307
+
1308
+ /* Higher specificity than the @container stacked-layout fallback below:
1309
+ on a narrow viewport the drawer must win even inside a narrow container. */
1310
+ .queue-layout .queue-panel {
1311
+ position: fixed;
1312
+ top: 0;
1313
+ left: 0;
1314
+ bottom: 0;
1315
+ width: min(320px, 90vw);
1316
+ z-index: 50;
1317
+ overflow-y: auto;
1318
+ overflow-x: hidden;
1319
+ transform: translateX(-105%);
1320
+ transition: transform 0.26s cubic-bezier(0.22, 1, 0.36, 1);
1321
+ border-radius: 0 var(--radius) var(--radius) 0;
1322
+ padding-top: 50px;
1323
+ }
1324
+
1325
+ .queue-layout .queue-panel.is-open {
1326
+ transform: translateX(0);
1141
1327
  }
1142
1328
 
1143
1329
  .content-grid {
@@ -1148,10 +1334,6 @@ pre {
1148
1334
  display: none;
1149
1335
  }
1150
1336
 
1151
- .queue-panel {
1152
- position: static;
1153
- }
1154
-
1155
1337
  .content-grid,
1156
1338
  .review-main,
1157
1339
  .candidate-grid,
@@ -1331,11 +1513,11 @@ pre {
1331
1513
  }
1332
1514
 
1333
1515
  .focus-value strong {
1334
- font-size: clamp(22px, 10vw, 32px);
1516
+ font-size: clamp(16px, 5cqi, 22px);
1335
1517
  }
1336
1518
 
1337
1519
  .candidate-value .field-value {
1338
- font-size: clamp(21px, 9vw, 30px);
1520
+ font-size: clamp(15px, 4.5cqi, 20px);
1339
1521
  line-height: 1.12;
1340
1522
  }
1341
1523
 
@@ -1440,7 +1622,7 @@ pre {
1440
1622
 
1441
1623
  .focus-value strong {
1442
1624
  margin-top: 6px;
1443
- font-size: 24px;
1625
+ font-size: 18px;
1444
1626
  line-height: 1.05;
1445
1627
  }
1446
1628
 
@@ -1449,6 +1631,6 @@ pre {
1449
1631
  }
1450
1632
 
1451
1633
  .candidate-value .field-value {
1452
- font-size: 20px;
1634
+ font-size: 17px;
1453
1635
  }
1454
1636
  }