conversation-replay 0.1.13 → 0.1.15

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.
Files changed (4) hide show
  1. package/README.md +43 -19
  2. package/dist/cli.js +755 -84
  3. package/package.json +2 -1
  4. package/schema.json +292 -0
package/dist/cli.js CHANGED
@@ -1,5 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from "node:module";
3
+ var __create = Object.create;
4
+ var __getProtoOf = Object.getPrototypeOf;
5
+ var __defProp = Object.defineProperty;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __toESM = (mod, isNodeMode, target) => {
9
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
10
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
11
+ for (let key of __getOwnPropNames(mod))
12
+ if (!__hasOwnProp.call(to, key))
13
+ __defProp(to, key, {
14
+ get: () => mod[key],
15
+ enumerable: true
16
+ });
17
+ return to;
18
+ };
3
19
  var __commonJS = (cb, mod) => () => (mod || cb((mod = { exports: {} }).exports, mod), mod.exports);
4
20
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
5
21
 
@@ -7270,7 +7286,7 @@ import { writeFile } from "node:fs/promises";
7270
7286
  // package.json
7271
7287
  var package_default = {
7272
7288
  name: "conversation-replay",
7273
- version: "0.1.13",
7289
+ version: "0.1.15",
7274
7290
  description: "Create annotated replays of text conversations",
7275
7291
  main: "dist/cli.js",
7276
7292
  type: "module",
@@ -7279,6 +7295,7 @@ var package_default = {
7279
7295
  },
7280
7296
  files: [
7281
7297
  "dist",
7298
+ "schema.json",
7282
7299
  "README.md"
7283
7300
  ],
7284
7301
  scripts: {
@@ -7431,21 +7448,107 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
7431
7448
  const radiusLg = cornerStyle === "straight" ? "0" : "12px";
7432
7449
  const blurAmount = initialBlur ?? 1;
7433
7450
  const tabCss = hasMultipleScenarios ? `
7434
- /* Tabs - browser-style integrated with canvas */
7451
+ /* Tabs - horizontal scrolling with arrow navigation */
7452
+ .tabs-wrapper {
7453
+ position: relative;
7454
+ margin-left: 12px;
7455
+ margin-right: 12px;
7456
+ margin-bottom: -1px;
7457
+ overflow: hidden;
7458
+ }
7459
+
7460
+ .tab-scroll-btn {
7461
+ position: absolute;
7462
+ top: 0;
7463
+ bottom: 1px;
7464
+ width: 32px;
7465
+ border: none;
7466
+ background: transparent;
7467
+ cursor: pointer;
7468
+ z-index: 4;
7469
+ display: none;
7470
+ align-items: center;
7471
+ justify-content: center;
7472
+ padding: 0;
7473
+ color: var(--text-primary);
7474
+ transition: color 0.2s ease;
7475
+ }
7476
+
7477
+ .tab-scroll-btn:hover {
7478
+ color: var(--accent);
7479
+ }
7480
+
7481
+ .tab-scroll-btn svg {
7482
+ width: 16px;
7483
+ height: 16px;
7484
+ fill: currentColor;
7485
+ /* Glow effect makes icon visible on any background */
7486
+ filter: drop-shadow(0 0 2px rgba(255, 255, 255, 1))
7487
+ drop-shadow(0 0 4px rgba(255, 255, 255, 0.8))
7488
+ drop-shadow(0 0 6px rgba(255, 255, 255, 0.6));
7489
+ }
7490
+
7491
+ :root[data-theme="dark"] .tab-scroll-btn svg {
7492
+ filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
7493
+ drop-shadow(0 0 4px rgba(0, 0, 0, 0.6));
7494
+ }
7495
+
7496
+ .tab-scroll-btn.left {
7497
+ left: 0;
7498
+ }
7499
+
7500
+ .tab-scroll-btn.right {
7501
+ right: 0;
7502
+ }
7503
+
7504
+ /* Use mask-image to fade tabs at edges - works on any background */
7505
+ /* Fade zone is larger so text is fully transparent before reaching the chevron */
7506
+ .tabs-wrapper.can-scroll-right .tabs {
7507
+ -webkit-mask-image: linear-gradient(to right, black 0%, black calc(100% - 60px), transparent calc(100% - 20px), transparent 100%);
7508
+ mask-image: linear-gradient(to right, black 0%, black calc(100% - 60px), transparent calc(100% - 20px), transparent 100%);
7509
+ }
7510
+
7511
+ .tabs-wrapper.can-scroll-left .tabs {
7512
+ -webkit-mask-image: linear-gradient(to left, black 0%, black calc(100% - 60px), transparent calc(100% - 20px), transparent 100%);
7513
+ mask-image: linear-gradient(to left, black 0%, black calc(100% - 60px), transparent calc(100% - 20px), transparent 100%);
7514
+ }
7515
+
7516
+ .tabs-wrapper.can-scroll-left.can-scroll-right .tabs {
7517
+ -webkit-mask-image: linear-gradient(to right, transparent 0%, transparent 20px, black 60px, black calc(100% - 60px), transparent calc(100% - 20px), transparent 100%);
7518
+ mask-image: linear-gradient(to right, transparent 0%, transparent 20px, black 60px, black calc(100% - 60px), transparent calc(100% - 20px), transparent 100%);
7519
+ }
7520
+
7521
+ .tabs-wrapper.can-scroll-left .tab-scroll-btn.left {
7522
+ display: flex;
7523
+ }
7524
+
7525
+ .tabs-wrapper.can-scroll-right .tab-scroll-btn.right {
7526
+ display: flex;
7527
+ }
7528
+
7435
7529
  .tabs {
7436
7530
  display: flex;
7437
7531
  gap: 0;
7438
- margin-left: 12px;
7439
- margin-bottom: -1px;
7440
- flex-wrap: wrap;
7532
+ flex-wrap: nowrap;
7533
+ overflow-x: auto;
7534
+ overflow-y: hidden;
7535
+ -webkit-overflow-scrolling: touch;
7536
+ scroll-snap-type: x mandatory;
7537
+ scroll-behavior: smooth;
7538
+ scrollbar-width: none;
7539
+ -ms-overflow-style: none;
7441
7540
  position: relative;
7442
7541
  z-index: 2;
7443
7542
  }
7444
7543
 
7544
+ .tabs::-webkit-scrollbar {
7545
+ display: none;
7546
+ }
7547
+
7445
7548
  .tab {
7446
7549
  padding: 10px 20px;
7447
7550
  border: 1px solid transparent;
7448
- border-bottom: none;
7551
+ border-bottom: 1px solid var(--border-color);
7449
7552
  background: transparent;
7450
7553
  color: var(--tab-inactive-color, var(--text-secondary));
7451
7554
  border-radius: var(--radius) var(--radius) 0 0;
@@ -7453,6 +7556,9 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
7453
7556
  font-size: 13px;
7454
7557
  font-weight: 500;
7455
7558
  transition: all 0.2s;
7559
+ flex-shrink: 0;
7560
+ scroll-snap-align: start;
7561
+ white-space: nowrap;
7456
7562
  }
7457
7563
 
7458
7564
  .tab:hover {
@@ -7463,50 +7569,32 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
7463
7569
  background: var(--bg-chat);
7464
7570
  color: var(--text-primary);
7465
7571
  border-color: var(--border-color);
7572
+ border-bottom-color: var(--bg-chat);
7466
7573
  position: relative;
7467
7574
  }
7468
7575
 
7469
- .tab.active::after {
7470
- content: '';
7471
- position: absolute;
7472
- bottom: -1px;
7473
- left: 0;
7474
- right: 0;
7475
- height: 2px;
7476
- background: var(--bg-chat);
7477
- }
7478
-
7479
7576
  @media (max-width: 600px) {
7577
+ .tabs-wrapper {
7578
+ margin-left: 12px;
7579
+ margin-right: 12px;
7580
+ }
7581
+
7480
7582
  .tabs {
7481
- flex-wrap: nowrap;
7482
- overflow: hidden; /* No scrolling - tabs will truncate */
7483
- padding: 0 12px; /* Align with canvas border-radius */
7484
- margin-left: 0;
7583
+ padding: 0;
7485
7584
  }
7486
7585
 
7487
7586
  .tab {
7488
- padding: 10px 12px;
7587
+ padding: 10px 16px;
7489
7588
  font-size: 12px;
7490
- flex: 1 1 0; /* Shrink equally */
7491
- text-align: center;
7492
- min-width: 60px; /* Don't get too small */
7493
- white-space: nowrap;
7494
- overflow: hidden;
7495
- text-overflow: ellipsis;
7589
+ min-width: 100px;
7590
+ max-width: 150px;
7591
+ }
7592
+
7593
+ .tab-scroll-btn {
7594
+ width: 28px;
7496
7595
  }
7497
7596
  }
7498
-
7499
- /* In iframe, ensure non-active tabs are visible against potentially dark/complex backgrounds */
7500
- body.in-iframe .tab:not(.active) {
7501
- background: rgba(255, 255, 255, 0.5);
7502
- backdrop-filter: blur(4px);
7503
- -webkit-backdrop-filter: blur(4px);
7504
- }
7505
-
7506
- :root[data-theme="dark"] body.in-iframe .tab:not(.active) {
7507
- background: rgba(30, 41, 59, 0.5);
7508
- color: rgba(248, 250, 252, 0.7);
7509
- }
7597
+
7510
7598
  ` : "";
7511
7599
  return `
7512
7600
  *, *::before, *::after {
@@ -7601,6 +7689,11 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
7601
7689
  --shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.3);
7602
7690
  --shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.4);
7603
7691
  }
7692
+
7693
+ .tab-scroll-btn svg {
7694
+ filter: drop-shadow(0 0 2px rgba(0, 0, 0, 0.8))
7695
+ drop-shadow(0 0 4px rgba(0, 0, 0, 0.6));
7696
+ }
7604
7697
  }
7605
7698
 
7606
7699
  /* Dark mode via data-theme attribute (for iframe sync with parent) */
@@ -7792,7 +7885,7 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
7792
7885
  display: flex;
7793
7886
  flex-direction: column;
7794
7887
  background: transparent !important;
7795
- padding: 0 !important;
7888
+ padding: 0;
7796
7889
  margin: 0 !important;
7797
7890
  border: none !important;
7798
7891
  box-shadow: none !important;
@@ -7813,17 +7906,14 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
7813
7906
  max-height: none !important; /* Remove height constraint in iframe */
7814
7907
  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05) !important; /* Subtle shadow in iframe */
7815
7908
  background: var(--bg-chat) !important; /* Ensure opaque background in iframe */
7816
- margin-bottom: clamp(70px, 15vh, 120px); /* Responsive space for fixed controls */
7817
7909
  }
7818
7910
 
7819
7911
  body.in-iframe .controls-wrapper {
7820
- position: fixed;
7821
- bottom: 12px;
7822
- left: 0;
7823
- right: 0;
7912
+ position: relative;
7913
+ flex-shrink: 0;
7824
7914
  display: flex;
7825
7915
  justify-content: center;
7826
- pointer-events: none; /* Let clicks pass through around buttons */
7916
+ padding: 12px 0;
7827
7917
  z-index: 20;
7828
7918
  }
7829
7919
 
@@ -7833,6 +7923,8 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
7833
7923
  flex-direction: column;
7834
7924
  gap: 20px;
7835
7925
  padding-bottom: 80px; /* Space for floating controls */
7926
+ padding-left: 32px; /* Symmetric with right */
7927
+ padding-right: 32px; /* Clearance for timer circle */
7836
7928
  }
7837
7929
 
7838
7930
  .play-overlay {
@@ -7920,8 +8012,7 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
7920
8012
  font-weight: 600;
7921
8013
  letter-spacing: 0.02em;
7922
8014
  margin-bottom: 6px;
7923
- color: var(--text-muted);
7924
- opacity: 0.8;
8015
+ color: var(--text-secondary);
7925
8016
  display: flex;
7926
8017
  align-items: center;
7927
8018
  gap: 6px;
@@ -7999,7 +8090,7 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
7999
8090
  width: 100%;
8000
8091
  margin: 24px 0;
8001
8092
  padding: 0 0 0 20px;
8002
- font-size: 15px;
8093
+ font-size: 16px;
8003
8094
  font-style: italic;
8004
8095
  color: var(--annotation-text);
8005
8096
  opacity: 0;
@@ -8012,10 +8103,9 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
8012
8103
  left: 0;
8013
8104
  top: 2px;
8014
8105
  bottom: 2px;
8015
- width: 3px;
8106
+ width: 4px;
8016
8107
  background: var(--annotation-border);
8017
- border-radius: 1.5px;
8018
- opacity: 0.5;
8108
+ border-radius: 2px;
8019
8109
  }
8020
8110
 
8021
8111
  .annotation.visible {
@@ -8025,7 +8115,7 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
8025
8115
  }
8026
8116
 
8027
8117
  .annotation-label {
8028
- font-size: 10px;
8118
+ font-size: 11px;
8029
8119
  font-weight: 700;
8030
8120
  text-transform: uppercase;
8031
8121
  letter-spacing: 0.05em;
@@ -8035,7 +8125,6 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
8035
8125
  display: flex;
8036
8126
  align-items: center;
8037
8127
  gap: 6px;
8038
- opacity: 0.8;
8039
8128
  }
8040
8129
 
8041
8130
  .annotation-content {
@@ -8354,10 +8443,6 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
8354
8443
  min-height: 250px;
8355
8444
  }
8356
8445
 
8357
- body.in-iframe .chat-container {
8358
- max-height: 400px;
8359
- }
8360
-
8361
8446
  .controls {
8362
8447
  gap: 8px;
8363
8448
  padding-top: 12px;
@@ -8416,20 +8501,37 @@ function generateCss(theme, hasMultipleScenarios, colors, cornerStyle, initialBl
8416
8501
  }
8417
8502
 
8418
8503
  .controls {
8419
- flex-wrap: wrap;
8420
- justify-content: center;
8504
+ flex-wrap: nowrap;
8505
+ gap: 4px;
8506
+ padding: 6px 8px;
8507
+ }
8508
+
8509
+ .controls .divider {
8510
+ display: none; /* Hide dividers to save space */
8421
8511
  }
8422
8512
 
8423
8513
  .control-btn {
8424
- flex: 0 0 auto;
8514
+ width: 36px;
8515
+ height: 36px;
8516
+ min-width: 36px;
8517
+ min-height: 36px;
8518
+ padding: 0;
8425
8519
  }
8426
8520
 
8427
8521
  .control-btn.primary {
8428
- order: -1;
8429
- flex: 0 0 auto;
8430
- width: 44px;
8431
- height: 44px;
8432
- margin-bottom: 4px;
8522
+ width: 40px;
8523
+ height: 40px;
8524
+ }
8525
+
8526
+ .speed-select {
8527
+ padding: 6px 8px;
8528
+ min-width: 32px;
8529
+ font-size: 12px;
8530
+ }
8531
+
8532
+ .progress {
8533
+ font-size: 11px;
8534
+ min-width: 36px;
8433
8535
  }
8434
8536
  }
8435
8537
 
@@ -8606,6 +8708,7 @@ function generateJs(demo, timerStyle) {
8606
8708
  var isPaused = false;
8607
8709
  var animationTimeout = null;
8608
8710
  var speed = 1;
8711
+ var currentStepBaseDelay = 0; // Base delay before speed adjustment (for mid-playback speed changes)
8609
8712
  var hasStarted = false;
8610
8713
 
8611
8714
  // DOM elements
@@ -8620,6 +8723,8 @@ function generateJs(demo, timerStyle) {
8620
8723
  var timerElement = document.getElementById('timer-element');
8621
8724
  var chatContainer = document.getElementById('chat-container');
8622
8725
  var tabs = document.querySelectorAll('.tab');
8726
+ var tabsContainer = document.querySelector('.tabs');
8727
+ var tabsWrapper = document.querySelector('.tabs-wrapper');
8623
8728
  var infoBtn = document.getElementById('info-btn');
8624
8729
  var infoPopover = document.getElementById('info-popover');
8625
8730
 
@@ -8788,7 +8893,7 @@ function generateJs(demo, timerStyle) {
8788
8893
 
8789
8894
  if (step.footnote) {
8790
8895
  var footnote = document.createElement('p');
8791
- footnote.style.cssText = 'margin: 8px 0 0 0; font-style: italic; font-size: 12px; color: var(--text-secondary);';
8896
+ footnote.style.cssText = 'margin: 8px 0 0 0; font-style: italic; font-size: 15px; color: var(--text-secondary);';
8792
8897
  footnote.textContent = step.footnote;
8793
8898
  contentWrapper.appendChild(footnote);
8794
8899
  }
@@ -8946,6 +9051,9 @@ function generateJs(demo, timerStyle) {
8946
9051
  playOverlay.classList.add('hidden');
8947
9052
  chatContainer.classList.remove('fading');
8948
9053
 
9054
+ // Ensure new active tab is visible (not in fade zone)
9055
+ scrollActiveTabIntoView();
9056
+
8949
9057
  // Auto-play immediately
8950
9058
  play();
8951
9059
  }, 300);
@@ -8989,6 +9097,7 @@ function generateJs(demo, timerStyle) {
8989
9097
 
8990
9098
  if (currentStepIndex < steps.length) {
8991
9099
  var delay = calculateDelay(step);
9100
+ currentStepBaseDelay = delay * speed; // Store base delay for mid-playback speed changes
8992
9101
  startTimer(delay);
8993
9102
  animationTimeout = setTimeout(showNextStep, delay);
8994
9103
  } else {
@@ -9013,7 +9122,16 @@ function generateJs(demo, timerStyle) {
9013
9122
 
9014
9123
  var steps = getSteps();
9015
9124
  var firstStep = steps[0];
9125
+
9126
+ // Show first step if not already visible (e.g., during auto-advance)
9127
+ if (chatMessages.children.length === 0) {
9128
+ var element = createStepElement(firstStep);
9129
+ element.classList.add('visible');
9130
+ chatMessages.appendChild(element);
9131
+ }
9132
+
9016
9133
  var delay = calculateDelay(firstStep);
9134
+ currentStepBaseDelay = delay * speed; // Store base delay for mid-playback speed changes
9017
9135
  currentStepIndex = 1;
9018
9136
 
9019
9137
  startTimer(delay);
@@ -9044,6 +9162,7 @@ function generateJs(demo, timerStyle) {
9044
9162
  updateButtonStates();
9045
9163
 
9046
9164
  var delay = calculateDelay(steps[0]);
9165
+ currentStepBaseDelay = delay * speed; // Store base delay for mid-playback speed changes
9047
9166
  startTimer(delay);
9048
9167
  animationTimeout = setTimeout(showNextStep, delay);
9049
9168
  return;
@@ -9143,23 +9262,26 @@ function generateJs(demo, timerStyle) {
9143
9262
  } else {
9144
9263
  // Always show initial preview to render the first message
9145
9264
  showInitialPreview();
9146
-
9265
+
9147
9266
  if (wasPlaying) {
9148
9267
  // If we were playing, continue playing (this will hide the overlay again)
9149
9268
  play();
9150
9269
  } else {
9151
- // If we weren't playing, ensure the overlay is hidden?
9270
+ // If we weren't playing, ensure the overlay is hidden?
9152
9271
  // The original comment said "keep overlay hidden" but showInitialPreview shows it.
9153
- // If we pause, we usually show the play button?
9272
+ // If we pause, we usually show the play button?
9154
9273
  // Actually, standard behavior is usually to show the play button on a fresh tab.
9155
9274
  // But strictly following the intention of "keep overlay hidden" would mean adding it back?
9156
9275
  // Let's stick to standard behavior: new tab = reset state = show play button.
9157
- // showInitialPreview() handles that.
9158
-
9276
+ // showInitialPreview() handles that.
9277
+
9159
9278
  // However, check if we want to suppress it:
9160
9279
  // "User can click play button or canvas to start"
9161
9280
  }
9162
9281
  }
9282
+
9283
+ // Ensure active tab is fully visible (not obscured by scroll buttons)
9284
+ scrollActiveTabIntoView();
9163
9285
  }, 300);
9164
9286
  }
9165
9287
 
@@ -9236,7 +9358,29 @@ function generateJs(demo, timerStyle) {
9236
9358
  }
9237
9359
 
9238
9360
  speedSelect.addEventListener('change', function(e) {
9361
+ var oldSpeed = speed;
9239
9362
  speed = parseFloat(e.target.value);
9363
+
9364
+ // If playing, recalculate and reschedule with new speed
9365
+ if (isPlaying && !isPaused && animationTimeout && currentStepBaseDelay > 0) {
9366
+ var elapsed = Date.now() - timerStartTime;
9367
+ var elapsedBase = elapsed * oldSpeed; // Content time consumed
9368
+ var remainingBase = currentStepBaseDelay - elapsedBase;
9369
+
9370
+ if (remainingBase > 0) {
9371
+ // Cancel current timeout (but keep timer animation running)
9372
+ clearTimeout(animationTimeout);
9373
+
9374
+ // Adjust timer variables so animation continues from current position
9375
+ // progress = elapsed / timerDuration should remain the same
9376
+ timerDuration = currentStepBaseDelay / speed;
9377
+ timerStartTime = Date.now() - (elapsedBase / speed);
9378
+
9379
+ // Reschedule with remaining time at new speed
9380
+ var remainingTime = remainingBase / speed;
9381
+ animationTimeout = setTimeout(showNextStep, remainingTime);
9382
+ }
9383
+ }
9240
9384
  });
9241
9385
 
9242
9386
  // Handle page visibility changes (pause timer animation when tab is hidden)
@@ -9249,8 +9393,78 @@ function generateJs(demo, timerStyle) {
9249
9393
  });
9250
9394
  }
9251
9395
 
9396
+ // Scroll indicators for mobile tabs
9397
+ function updateTabScrollIndicators() {
9398
+ if (!tabsContainer || !tabsWrapper) return;
9399
+
9400
+ var scrollLeft = tabsContainer.scrollLeft;
9401
+ var maxScroll = tabsContainer.scrollWidth - tabsContainer.clientWidth;
9402
+ var threshold = 2; // Account for sub-pixel rendering
9403
+
9404
+ if (scrollLeft > threshold) {
9405
+ tabsWrapper.classList.add('can-scroll-left');
9406
+ } else {
9407
+ tabsWrapper.classList.remove('can-scroll-left');
9408
+ }
9409
+
9410
+ if (scrollLeft < maxScroll - threshold) {
9411
+ tabsWrapper.classList.add('can-scroll-right');
9412
+ } else {
9413
+ tabsWrapper.classList.remove('can-scroll-right');
9414
+ }
9415
+ }
9416
+
9417
+ // Auto-scroll to ensure active tab is fully visible (outside the fade zone)
9418
+ function scrollActiveTabIntoView() {
9419
+ if (!tabsContainer) return;
9420
+ var activeTab = tabsContainer.querySelector('.tab.active');
9421
+ if (!activeTab) return;
9422
+
9423
+ var containerRect = tabsContainer.getBoundingClientRect();
9424
+ var tabRect = activeTab.getBoundingClientRect();
9425
+ var fadeZone = 70; // Must be larger than CSS mask fade zone (60px) to keep active tab fully opaque
9426
+
9427
+ // Check if tab is cut off on the right
9428
+ if (tabRect.right > containerRect.right - fadeZone) {
9429
+ tabsContainer.scrollBy({
9430
+ left: tabRect.right - containerRect.right + fadeZone + 8,
9431
+ behavior: 'smooth'
9432
+ });
9433
+ }
9434
+ // Check if tab is cut off on the left
9435
+ else if (tabRect.left < containerRect.left + fadeZone) {
9436
+ tabsContainer.scrollBy({
9437
+ left: tabRect.left - containerRect.left - fadeZone - 8,
9438
+ behavior: 'smooth'
9439
+ });
9440
+ }
9441
+ }
9442
+
9443
+ if (tabsContainer && tabsWrapper) {
9444
+ tabsContainer.addEventListener('scroll', updateTabScrollIndicators, { passive: true });
9445
+ window.addEventListener('resize', updateTabScrollIndicators, { passive: true });
9446
+
9447
+ // Arrow button click handlers
9448
+ var scrollLeftBtn = tabsWrapper.querySelector('.tab-scroll-btn.left');
9449
+ var scrollRightBtn = tabsWrapper.querySelector('.tab-scroll-btn.right');
9450
+ var scrollAmount = 200; // pixels to scroll per click
9451
+
9452
+ if (scrollLeftBtn) {
9453
+ scrollLeftBtn.addEventListener('click', function() {
9454
+ tabsContainer.scrollBy({ left: -scrollAmount, behavior: 'smooth' });
9455
+ });
9456
+ }
9457
+
9458
+ if (scrollRightBtn) {
9459
+ scrollRightBtn.addEventListener('click', function() {
9460
+ tabsContainer.scrollBy({ left: scrollAmount, behavior: 'smooth' });
9461
+ });
9462
+ }
9463
+ }
9464
+
9252
9465
  // Initialize
9253
9466
  updateTabStates();
9467
+ updateTabScrollIndicators();
9254
9468
  showInitialPreview();
9255
9469
 
9256
9470
  if (prefersReducedMotion) {
@@ -9271,9 +9485,17 @@ function generateTabsHtml(demo) {
9271
9485
  }).join(`
9272
9486
  `);
9273
9487
  return `
9274
- <nav class="tabs" role="tablist">
9275
- ${tabButtons}
9276
- </nav>
9488
+ <div class="tabs-wrapper">
9489
+ <button class="tab-scroll-btn left" aria-label="Scroll tabs left">
9490
+ <svg viewBox="0 0 24 24"><path d="M18.41 7.41L17 6l-6 6 6 6 1.41-1.41L13.83 12z"/><path d="M12.41 7.41L11 6l-6 6 6 6 1.41-1.41L7.83 12z"/></svg>
9491
+ </button>
9492
+ <nav class="tabs" role="tablist">
9493
+ ${tabButtons}
9494
+ </nav>
9495
+ <button class="tab-scroll-btn right" aria-label="Scroll tabs right">
9496
+ <svg viewBox="0 0 24 24"><path d="M5.59 7.41L7 6l6 6-6 6-1.41-1.41L10.17 12z"/><path d="M11.59 7.41L13 6l6 6-6 6-1.41-1.41L16.17 12z"/></svg>
9497
+ </button>
9498
+ </div>
9277
9499
  `;
9278
9500
  }
9279
9501
  function generateHtml(demo, options = { outputPath: "" }) {
@@ -9391,6 +9613,394 @@ async function buildDemo(demo, outputPath, options = {}) {
9391
9613
  await writeFile(outputPath, html, "utf-8");
9392
9614
  }
9393
9615
 
9616
+ // src/schema.ts
9617
+ function generateTemplate(theme) {
9618
+ const selectedTheme = theme || "chat";
9619
+ return `# Conversation Replay - Demo Configuration
9620
+ # Documentation: https://github.com/lennyzeltser/conversation-replay
9621
+ #
9622
+ # Run: conversation-replay build this-file.yaml -o output.html
9623
+ # Validate: conversation-replay validate this-file.yaml
9624
+
9625
+ meta:
9626
+ title: "My Demo Title" # Required: shown in header
9627
+ description: "A brief description" # Optional: shown below title
9628
+ theme: ${selectedTheme} # chat | email | slack | terminal | generic
9629
+ # articleUrl: "/related-article" # Optional: "View Article" link
9630
+ # annotationLabel: "Behind the Scenes" # Label for annotation steps
9631
+ # autoAdvance: true # Auto-play next scenario when current ends
9632
+
9633
+ # Timer and visual style
9634
+ # timerStyle: circle # circle | bar
9635
+ # cornerStyle: rounded # rounded | straight
9636
+ # initialBlur: 1 # Blur amount for play overlay (px)
9637
+
9638
+ # Custom colors (all optional)
9639
+ # colors:
9640
+ # accent: "#4f46e5" # Buttons, links (Indigo 600)
9641
+ # pageBg: "#f8fafc" # Page background (Slate 50)
9642
+ # canvasBg: "#ffffff" # Chat container background
9643
+ # leftBg: "#eef2ff" # Left bubble background (Indigo 50)
9644
+ # leftBorder: "transparent" # Left bubble border
9645
+ # rightBg: "#f1f5f9" # Right bubble background (Slate 100)
9646
+ # rightBorder: "transparent" # Right bubble border
9647
+ # annotationText: "#475569" # Annotation text (Slate 600)
9648
+ # annotationBorder: "#cbd5e1" # Annotation accent bar (Slate 300)
9649
+
9650
+ # Timing configuration (all in milliseconds)
9651
+ # speed:
9652
+ # minDelay: 3000 # Minimum delay between steps
9653
+ # maxDelay: 8000 # Maximum delay between steps
9654
+ # msPerWord: 200 # Reading time per word
9655
+ # annotationMultiplier: 1.15 # Extra time for annotations
9656
+ # upNextDelay: 2500 # "Up Next" display time
9657
+
9658
+ scenarios:
9659
+ - id: main
9660
+ title: "Scenario Title" # Shown in tab (if multiple scenarios)
9661
+
9662
+ participants:
9663
+ - id: user1
9664
+ label: "Person A"
9665
+ role: left # Messages appear on left
9666
+ - id: user2
9667
+ label: "Person B"
9668
+ role: right # Messages appear on right
9669
+
9670
+ steps:
9671
+ # Message from a participant
9672
+ - type: message
9673
+ from: user1 # Must match a participant id
9674
+ content: "Hello! This is a sample message."
9675
+
9676
+ - type: message
9677
+ from: user2
9678
+ content: "Hi there! Here's a response."
9679
+ # footnote: "Optional italic footnote below the message"
9680
+
9681
+ # Message with code block
9682
+ # - type: message
9683
+ # from: user2
9684
+ # content: "Here's some code:"
9685
+ # codeBlock: |
9686
+ # function example() {
9687
+ # return "hello";
9688
+ # }
9689
+
9690
+ # Annotation - educational callout with accent bar
9691
+ - type: annotation
9692
+ content: "This annotation explains something to the viewer."
9693
+
9694
+ # Transition - centered scene break
9695
+ # - type: transition
9696
+ # content: "Later that day..."
9697
+
9698
+ # Additional scenarios appear as tabs
9699
+ # - id: part2
9700
+ # title: "Part Two"
9701
+ # participants:
9702
+ # - id: analyst
9703
+ # label: "Analyst"
9704
+ # role: left
9705
+ # steps:
9706
+ # - type: message
9707
+ # from: analyst
9708
+ # content: "This is another scenario."
9709
+ `;
9710
+ }
9711
+ var jsonSchema = {
9712
+ $schema: "http://json-schema.org/draft-07/schema#",
9713
+ $id: "https://github.com/lennyzeltser/conversation-replay/schema.json",
9714
+ title: "Conversation Replay Demo",
9715
+ description: "Schema for conversation-replay YAML configuration files",
9716
+ type: "object",
9717
+ required: ["meta", "scenarios"],
9718
+ properties: {
9719
+ meta: {
9720
+ type: "object",
9721
+ description: "Demo metadata and configuration",
9722
+ required: ["title"],
9723
+ properties: {
9724
+ title: {
9725
+ type: "string",
9726
+ description: "Demo title shown in header",
9727
+ minLength: 1
9728
+ },
9729
+ description: {
9730
+ type: "string",
9731
+ description: "Brief description shown below title"
9732
+ },
9733
+ theme: {
9734
+ type: "string",
9735
+ enum: ["chat", "email", "slack", "terminal", "generic"],
9736
+ description: "Visual theme for message styling",
9737
+ default: "chat"
9738
+ },
9739
+ articleUrl: {
9740
+ type: "string",
9741
+ description: "URL for 'View Article' link in header"
9742
+ },
9743
+ hideHeaderInIframe: {
9744
+ type: "boolean",
9745
+ description: "Auto-hide header when embedded in iframe",
9746
+ default: true
9747
+ },
9748
+ autoAdvance: {
9749
+ type: "boolean",
9750
+ description: "Auto-play next scenario when current one completes"
9751
+ },
9752
+ annotationLabel: {
9753
+ type: "string",
9754
+ description: "Label shown above annotation content",
9755
+ default: "Behind the Scenes"
9756
+ },
9757
+ timerStyle: {
9758
+ type: "string",
9759
+ enum: ["bar", "circle"],
9760
+ description: "Countdown timer display style",
9761
+ default: "circle"
9762
+ },
9763
+ cornerStyle: {
9764
+ type: "string",
9765
+ enum: ["rounded", "straight"],
9766
+ description: "Border radius style for containers and bubbles",
9767
+ default: "rounded"
9768
+ },
9769
+ initialBlur: {
9770
+ type: "number",
9771
+ description: "Blur amount in pixels for play overlay",
9772
+ default: 1,
9773
+ minimum: 0
9774
+ },
9775
+ colors: {
9776
+ type: "object",
9777
+ description: "Custom color overrides",
9778
+ properties: {
9779
+ accent: { type: "string", description: "Primary accent color (buttons, links)" },
9780
+ pageBg: { type: "string", description: "Page background color" },
9781
+ canvasBg: { type: "string", description: "Chat container background" },
9782
+ leftBg: { type: "string", description: "Left participant bubble background" },
9783
+ leftBorder: { type: "string", description: "Left participant bubble border" },
9784
+ rightBg: { type: "string", description: "Right participant bubble background" },
9785
+ rightBorder: { type: "string", description: "Right participant bubble border" },
9786
+ tabInactiveColor: { type: "string", description: "Inactive tab text color" },
9787
+ annotationText: { type: "string", description: "Annotation text color" },
9788
+ annotationBorder: { type: "string", description: "Annotation accent bar color" }
9789
+ }
9790
+ },
9791
+ speed: {
9792
+ type: "object",
9793
+ description: "Timing configuration for playback",
9794
+ properties: {
9795
+ minDelay: { type: "number", description: "Minimum delay between steps (ms)", default: 3000 },
9796
+ maxDelay: { type: "number", description: "Maximum delay between steps (ms)", default: 8000 },
9797
+ msPerWord: { type: "number", description: "Milliseconds per word for reading time", default: 200 },
9798
+ annotationMultiplier: { type: "number", description: "Time multiplier for annotations", default: 1.15 },
9799
+ upNextDelay: { type: "number", description: "'Up Next' display duration (ms)", default: 2500 }
9800
+ }
9801
+ }
9802
+ }
9803
+ },
9804
+ scenarios: {
9805
+ type: "array",
9806
+ description: "One or more conversation scenarios (shown as tabs if multiple)",
9807
+ minItems: 1,
9808
+ items: {
9809
+ type: "object",
9810
+ required: ["id", "title", "participants", "steps"],
9811
+ properties: {
9812
+ id: {
9813
+ type: "string",
9814
+ description: "Unique identifier for this scenario",
9815
+ minLength: 1
9816
+ },
9817
+ title: {
9818
+ type: "string",
9819
+ description: "Display title (shown in tab)",
9820
+ minLength: 1
9821
+ },
9822
+ participants: {
9823
+ type: "array",
9824
+ description: "People/entities in this conversation",
9825
+ minItems: 1,
9826
+ items: {
9827
+ type: "object",
9828
+ required: ["id", "label"],
9829
+ properties: {
9830
+ id: {
9831
+ type: "string",
9832
+ description: "Unique identifier (referenced in message 'from' field)"
9833
+ },
9834
+ label: {
9835
+ type: "string",
9836
+ description: "Display name shown above messages"
9837
+ },
9838
+ role: {
9839
+ type: "string",
9840
+ enum: ["left", "right"],
9841
+ description: "Which side messages appear on",
9842
+ default: "left"
9843
+ }
9844
+ }
9845
+ }
9846
+ },
9847
+ steps: {
9848
+ type: "array",
9849
+ description: "Sequence of messages, annotations, and transitions",
9850
+ minItems: 1,
9851
+ items: {
9852
+ oneOf: [
9853
+ {
9854
+ type: "object",
9855
+ description: "A message from a participant",
9856
+ required: ["type", "from", "content"],
9857
+ properties: {
9858
+ type: { const: "message" },
9859
+ from: { type: "string", description: "Participant ID" },
9860
+ content: { type: "string", description: "Message text" },
9861
+ codeBlock: { type: "string", description: "Optional code/preformatted text" },
9862
+ footnote: { type: "string", description: "Optional italic footnote" }
9863
+ }
9864
+ },
9865
+ {
9866
+ type: "object",
9867
+ description: "An educational annotation/callout",
9868
+ required: ["type", "content"],
9869
+ properties: {
9870
+ type: { const: "annotation" },
9871
+ content: { type: "string", description: "Annotation text" }
9872
+ }
9873
+ },
9874
+ {
9875
+ type: "object",
9876
+ description: "A scene break/transition",
9877
+ required: ["type", "content"],
9878
+ properties: {
9879
+ type: { const: "transition" },
9880
+ content: { type: "string", description: "Transition text (e.g., 'Later that day...')" }
9881
+ }
9882
+ }
9883
+ ]
9884
+ }
9885
+ }
9886
+ }
9887
+ }
9888
+ }
9889
+ }
9890
+ };
9891
+ function getSchemaReference(section) {
9892
+ if (section === "meta") {
9893
+ return `
9894
+ META OPTIONS
9895
+ ============
9896
+
9897
+ Required:
9898
+ title: string Demo title shown in header
9899
+
9900
+ Optional:
9901
+ description: string Brief description below title
9902
+ theme: string chat | email | slack | terminal | generic (default: chat)
9903
+ articleUrl: string URL for "View Article" link
9904
+ annotationLabel: string Label for annotations (default: "Behind the Scenes")
9905
+ autoAdvance: boolean Auto-play next scenario when current ends
9906
+ hideHeaderInIframe: bool Auto-hide header when embedded (default: true)
9907
+ timerStyle: string circle | bar (default: circle)
9908
+ cornerStyle: string rounded | straight (default: rounded)
9909
+ initialBlur: number Play overlay blur in px (default: 1)
9910
+ colors: object Custom color overrides (see: schema colors)
9911
+ speed: object Timing configuration (see: schema speed)
9912
+ `;
9913
+ }
9914
+ if (section === "colors") {
9915
+ return `
9916
+ COLOR OPTIONS (meta.colors)
9917
+ ===========================
9918
+
9919
+ All colors are optional CSS color values (hex, rgb, named colors, etc.)
9920
+
9921
+ accent: string Buttons, links (default: #4f46e5)
9922
+ pageBg: string Page background (default: #f8fafc)
9923
+ canvasBg: string Chat container background (default: #ffffff)
9924
+ leftBg: string Left bubble background (default: #eef2ff)
9925
+ leftBorder: string Left bubble border (default: transparent)
9926
+ rightBg: string Right bubble background (default: #f1f5f9)
9927
+ rightBorder: string Right bubble border (default: transparent)
9928
+ tabInactiveColor: string Inactive tab text (default: #94a3b8)
9929
+ annotationText: string Annotation text (default: #475569)
9930
+ annotationBorder: string Annotation accent bar (default: #cbd5e1)
9931
+ `;
9932
+ }
9933
+ if (section === "speed") {
9934
+ return `
9935
+ SPEED OPTIONS (meta.speed)
9936
+ ==========================
9937
+
9938
+ All values are in milliseconds.
9939
+
9940
+ minDelay: number Minimum delay between steps (default: 3000)
9941
+ maxDelay: number Maximum delay between steps (default: 8000)
9942
+ msPerWord: number Reading time per word (default: 200)
9943
+ annotationMultiplier: num Extra time multiplier for annotations (default: 1.15)
9944
+ upNextDelay: number "Up Next" indicator duration (default: 2500)
9945
+ `;
9946
+ }
9947
+ if (section === "steps") {
9948
+ return `
9949
+ STEP TYPES
9950
+ ==========
9951
+
9952
+ MESSAGE - A chat bubble from a participant
9953
+ type: message (required)
9954
+ from: string Participant ID (required)
9955
+ content: string Message text (required)
9956
+ codeBlock: string Optional preformatted code block
9957
+ footnote: string Optional italic footnote
9958
+
9959
+ ANNOTATION - Educational callout with accent bar
9960
+ type: annotation (required)
9961
+ content: string Annotation text (required)
9962
+
9963
+ TRANSITION - Centered scene break
9964
+ type: transition (required)
9965
+ content: string Transition text (required)
9966
+ `;
9967
+ }
9968
+ return `
9969
+ CONVERSATION REPLAY SCHEMA
9970
+ ==========================
9971
+
9972
+ STRUCTURE
9973
+ ---------
9974
+ meta: Demo configuration (see: schema meta)
9975
+ scenarios: Array of conversation scenarios
9976
+
9977
+ SCENARIO
9978
+ --------
9979
+ id: string Unique identifier (required)
9980
+ title: string Display title for tab (required)
9981
+ participants: array People in conversation (required)
9982
+ steps: array Message/annotation sequence (required)
9983
+
9984
+ PARTICIPANT
9985
+ -----------
9986
+ id: string Unique ID, used in message 'from' field (required)
9987
+ label: string Display name shown above messages (required)
9988
+ role: string left | right (default: left)
9989
+
9990
+ STEP TYPES
9991
+ ----------
9992
+ message Chat bubble (requires: from, content)
9993
+ annotation Educational callout (requires: content)
9994
+ transition Scene break (requires: content)
9995
+
9996
+ Use 'schema <section>' for details:
9997
+ schema meta Meta/configuration options
9998
+ schema colors Color customization
9999
+ schema speed Timing configuration
10000
+ schema steps Step type details
10001
+ `;
10002
+ }
10003
+
9394
10004
  // src/cli.ts
9395
10005
  var HELP = `
9396
10006
  conversation-replay - Create annotated replays of text conversations from YAML
@@ -9398,22 +10008,31 @@ conversation-replay - Create annotated replays of text conversations from YAML
9398
10008
  Usage:
9399
10009
  conversation-replay build <scenario.yaml> -o <output.html> [options]
9400
10010
  conversation-replay validate <scenario.yaml>
9401
- conversation-replay --help
10011
+ conversation-replay init <output.yaml> [--theme <theme>]
10012
+ conversation-replay schema [section] [--json]
10013
+ conversation-replay info
10014
+ conversation-replay --help | --version
9402
10015
 
9403
10016
  Commands:
9404
10017
  build Generate HTML from a scenario file
9405
10018
  validate Check a scenario file for errors
10019
+ init Create a starter YAML template with documentation
10020
+ schema Show schema reference (sections: meta, colors, speed, steps)
10021
+ info Show tool name, version, author, and links
9406
10022
 
9407
10023
  Options:
9408
10024
  -o, --output <path> Output HTML file path (required for build)
9409
10025
  --theme <theme> Override theme (chat, email, slack, terminal, generic)
9410
10026
  --no-header Exclude the demo header
10027
+ --json Output JSON schema (for schema command)
9411
10028
  -h, --help Show this help message
10029
+ -v, --version Show version number
9412
10030
 
9413
10031
  Examples:
9414
10032
  conversation-replay build demo.yaml -o demo.html
9415
- conversation-replay build demo.yaml -o demo.html --theme email
9416
- conversation-replay validate demo.yaml
10033
+ conversation-replay init my-demo.yaml
10034
+ conversation-replay schema meta
10035
+ conversation-replay info
9417
10036
  `;
9418
10037
  var VALID_THEMES2 = ["chat", "email", "slack", "terminal", "generic"];
9419
10038
  async function main() {
@@ -9423,18 +10042,32 @@ async function main() {
9423
10042
  output: { type: "string", short: "o" },
9424
10043
  theme: { type: "string" },
9425
10044
  "no-header": { type: "boolean" },
9426
- help: { type: "boolean", short: "h" }
10045
+ json: { type: "boolean" },
10046
+ help: { type: "boolean", short: "h" },
10047
+ version: { type: "boolean", short: "v" }
9427
10048
  },
9428
10049
  allowPositionals: true
9429
10050
  });
10051
+ if (values.version) {
10052
+ console.log(`conversation-replay v${package_default.version}`);
10053
+ process.exit(0);
10054
+ }
9430
10055
  if (values.help || positionals.length === 0) {
9431
10056
  console.log(HELP);
9432
10057
  process.exit(0);
9433
10058
  }
9434
10059
  const command = positionals[0];
9435
- const inputFile = positionals[1];
9436
- if (!inputFile) {
9437
- console.error(`Error: No input file specified
10060
+ const arg1 = positionals[1];
10061
+ if (command === "schema") {
10062
+ handleSchema(arg1, values.json);
10063
+ process.exit(0);
10064
+ }
10065
+ if (command === "info") {
10066
+ handleInfo();
10067
+ process.exit(0);
10068
+ }
10069
+ if (!arg1) {
10070
+ console.error(`Error: No file specified
9438
10071
  `);
9439
10072
  console.log(HELP);
9440
10073
  process.exit(1);
@@ -9447,10 +10080,13 @@ async function main() {
9447
10080
  try {
9448
10081
  switch (command) {
9449
10082
  case "build":
9450
- await handleBuild(inputFile, values);
10083
+ await handleBuild(arg1, values);
9451
10084
  break;
9452
10085
  case "validate":
9453
- await handleValidate(inputFile);
10086
+ await handleValidate(arg1);
10087
+ break;
10088
+ case "init":
10089
+ await handleInit(arg1, values.theme);
9454
10090
  break;
9455
10091
  default:
9456
10092
  console.error(`Error: Unknown command "${command}"
@@ -9523,6 +10159,41 @@ async function handleValidate(inputFile) {
9523
10159
  console.log(` - Transitions: ${counts.transition}`);
9524
10160
  }
9525
10161
  }
10162
+ async function handleInit(outputFile, theme) {
10163
+ const fs = await import("fs/promises");
10164
+ try {
10165
+ await fs.access(outputFile);
10166
+ console.error(`Error: File already exists: ${outputFile}`);
10167
+ console.error("Use a different filename or delete the existing file.");
10168
+ process.exit(1);
10169
+ } catch {}
10170
+ const template = generateTemplate(theme);
10171
+ await fs.writeFile(outputFile, template, "utf-8");
10172
+ console.log(`Created ${outputFile}`);
10173
+ console.log("");
10174
+ console.log("Next steps:");
10175
+ console.log(` 1. Edit ${outputFile} to define your conversation`);
10176
+ console.log(` 2. Validate: conversation-replay validate ${outputFile}`);
10177
+ console.log(` 3. Build: conversation-replay build ${outputFile} -o output.html`);
10178
+ console.log("");
10179
+ console.log("For schema reference: conversation-replay schema");
10180
+ }
10181
+ function handleInfo() {
10182
+ console.log(`
10183
+ Conversation Replay v${package_default.version}
10184
+ https://github.com/lennyzeltser/conversation-replay
10185
+
10186
+ Created by Lenny Zeltser
10187
+ https://zeltser.com
10188
+ `.trim());
10189
+ }
10190
+ function handleSchema(section, outputJson) {
10191
+ if (outputJson) {
10192
+ console.log(JSON.stringify(jsonSchema, null, 2));
10193
+ return;
10194
+ }
10195
+ console.log(getSchemaReference(section));
10196
+ }
9526
10197
  main().catch((error) => {
9527
10198
  console.error("Unexpected error:", error);
9528
10199
  process.exit(1);