@lionrapid/core 0.2.1 → 0.3.0-beta.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.
Files changed (95) hide show
  1. package/dist/browser/auto-translator.d.ts.map +1 -1
  2. package/dist/browser/auto-translator.js +15 -4
  3. package/dist/browser/auto-translator.js.map +1 -1
  4. package/dist/browser/block-extractor.d.ts.map +1 -1
  5. package/dist/browser/block-extractor.js +6 -0
  6. package/dist/browser/block-extractor.js.map +1 -1
  7. package/dist/browser/editor/editor-api.d.ts +31 -1
  8. package/dist/browser/editor/editor-api.d.ts.map +1 -1
  9. package/dist/browser/editor/editor-api.js +43 -1
  10. package/dist/browser/editor/editor-api.js.map +1 -1
  11. package/dist/browser/editor/editor-init.js +25 -0
  12. package/dist/browser/editor/editor-init.js.map +1 -1
  13. package/dist/browser/editor/editor-styles.d.ts +1 -1
  14. package/dist/browser/editor/editor-styles.d.ts.map +1 -1
  15. package/dist/browser/editor/editor-styles.js +55 -0
  16. package/dist/browser/editor/editor-styles.js.map +1 -1
  17. package/dist/browser/editor/live-preview.d.ts.map +1 -1
  18. package/dist/browser/editor/live-preview.js +20 -3
  19. package/dist/browser/editor/live-preview.js.map +1 -1
  20. package/dist/browser/editor/sidebar.d.ts +20 -1
  21. package/dist/browser/editor/sidebar.d.ts.map +1 -1
  22. package/dist/browser/editor/sidebar.js +147 -9
  23. package/dist/browser/editor/sidebar.js.map +1 -1
  24. package/dist/browser/lionrapid.auto.dev.js +125 -311
  25. package/dist/browser/lionrapid.auto.dev.js.map +3 -3
  26. package/dist/browser/lionrapid.auto.js +125 -311
  27. package/dist/browser/lionrapid.auto.js.map +3 -3
  28. package/dist/browser/lionrapid.auto.min.js +3 -3
  29. package/dist/browser/lionrapid.auto.min.js.map +4 -4
  30. package/dist/browser/lionrapid.editor.js +274 -181
  31. package/dist/browser/lionrapid.editor.js.map +3 -3
  32. package/dist/browser/lionrapid.editor.min.js +58 -3
  33. package/dist/browser/lionrapid.editor.min.js.map +4 -4
  34. package/dist/browser/lionrapid.esm.js +108 -294
  35. package/dist/browser/lionrapid.esm.js.map +3 -3
  36. package/dist/browser/lionrapid.esm.min.js +2 -2
  37. package/dist/browser/lionrapid.esm.min.js.map +4 -4
  38. package/dist/browser/lionrapid.iife.dev.js +108 -294
  39. package/dist/browser/lionrapid.iife.dev.js.map +3 -3
  40. package/dist/browser/lionrapid.iife.js +108 -294
  41. package/dist/browser/lionrapid.iife.js.map +3 -3
  42. package/dist/browser/lionrapid.iife.min.js +2 -2
  43. package/dist/browser/lionrapid.iife.min.js.map +4 -4
  44. package/dist/browser/lionrapid.umd.js +108 -294
  45. package/dist/browser/lionrapid.umd.js.map +3 -3
  46. package/dist/browser/lionrapid.umd.min.js +2 -2
  47. package/dist/browser/lionrapid.umd.min.js.map +4 -4
  48. package/dist/browser/markup-parser.d.ts +9 -5
  49. package/dist/browser/markup-parser.d.ts.map +1 -1
  50. package/dist/browser/markup-parser.js +19 -23
  51. package/dist/browser/markup-parser.js.map +1 -1
  52. package/dist/content/canonical-tag.d.ts +34 -0
  53. package/dist/content/canonical-tag.d.ts.map +1 -0
  54. package/dist/content/canonical-tag.js +57 -0
  55. package/dist/content/canonical-tag.js.map +1 -0
  56. package/dist/content/index.d.ts +1 -4
  57. package/dist/content/index.d.ts.map +1 -1
  58. package/dist/content/index.js +3 -1
  59. package/dist/content/index.js.map +1 -1
  60. package/dist/content/parser/MarkupParser.d.ts +9 -49
  61. package/dist/content/parser/MarkupParser.d.ts.map +1 -1
  62. package/dist/content/parser/MarkupParser.js +37 -157
  63. package/dist/content/parser/MarkupParser.js.map +1 -1
  64. package/dist/content/serializer/HTMLSerializer.d.ts +10 -43
  65. package/dist/content/serializer/HTMLSerializer.d.ts.map +1 -1
  66. package/dist/content/serializer/HTMLSerializer.js +29 -69
  67. package/dist/content/serializer/HTMLSerializer.js.map +1 -1
  68. package/dist/content/types.d.ts +21 -12
  69. package/dist/content/types.d.ts.map +1 -1
  70. package/dist/content/validator/ContentValidator.d.ts +2 -2
  71. package/dist/content/validator/ContentValidator.js +2 -2
  72. package/dist/index.d.ts +2 -3
  73. package/dist/index.d.ts.map +1 -1
  74. package/dist/index.js +4 -1
  75. package/dist/index.js.map +1 -1
  76. package/dist/lionRapid.d.ts +1 -1
  77. package/dist/lionRapid.js +4 -4
  78. package/dist/lionRapid.js.map +1 -1
  79. package/dist/repositories/network-repository.d.ts +14 -0
  80. package/dist/repositories/network-repository.d.ts.map +1 -1
  81. package/dist/repositories/network-repository.js +26 -4
  82. package/dist/repositories/network-repository.js.map +1 -1
  83. package/package.json +1 -1
  84. package/dist/content/safe-href.d.ts +0 -22
  85. package/dist/content/safe-href.d.ts.map +0 -1
  86. package/dist/content/safe-href.js +0 -48
  87. package/dist/content/safe-href.js.map +0 -1
  88. package/dist/content/style-mapper/DefaultStyleMapper.d.ts +0 -59
  89. package/dist/content/style-mapper/DefaultStyleMapper.d.ts.map +0 -1
  90. package/dist/content/style-mapper/DefaultStyleMapper.js +0 -132
  91. package/dist/content/style-mapper/DefaultStyleMapper.js.map +0 -1
  92. package/dist/content/style-mapper/IStyleMapper.d.ts +0 -50
  93. package/dist/content/style-mapper/IStyleMapper.d.ts.map +0 -1
  94. package/dist/content/style-mapper/IStyleMapper.js +0 -6
  95. package/dist/content/style-mapper/IStyleMapper.js.map +0 -1
@@ -226,6 +226,60 @@ var LionRapidEditor = (() => {
226
226
  background: #fef2f2;
227
227
  }
228
228
 
229
+ .lr-wf-note {
230
+ font-size: 11px;
231
+ color: #6b7280;
232
+ font-style: italic;
233
+ padding: 4px 0;
234
+ }
235
+
236
+ /* Workflow context strip (per-row 'you are here' indicator at the top of
237
+ the workflow actions block). Width-flexible: stages wrap on narrow sidebars. */
238
+ .lr-wf-strip-host {
239
+ width: 100%;
240
+ }
241
+ .lr-wf-strip {
242
+ display: flex;
243
+ flex-direction: column;
244
+ gap: 4px;
245
+ margin-bottom: 8px;
246
+ padding: 6px 8px;
247
+ background: #f9fafb;
248
+ border: 1px solid #e5e7eb;
249
+ border-radius: 4px;
250
+ }
251
+ .lr-wf-strip-stages {
252
+ display: flex;
253
+ flex-wrap: wrap;
254
+ align-items: center;
255
+ gap: 4px;
256
+ font-size: 11px;
257
+ font-weight: 500;
258
+ }
259
+ .lr-wf-strip-stage {
260
+ padding: 2px 8px;
261
+ border-radius: 3px;
262
+ white-space: nowrap;
263
+ }
264
+ .lr-wf-strip-stage.is-active {
265
+ background: #2563eb;
266
+ color: white;
267
+ }
268
+ .lr-wf-strip-stage.is-muted {
269
+ background: #e5e7eb;
270
+ color: #9ca3af;
271
+ }
272
+ .lr-wf-strip-sep {
273
+ color: #d1d5db;
274
+ font-size: 12px;
275
+ user-select: none;
276
+ }
277
+ .lr-wf-strip-caption {
278
+ font-size: 11px;
279
+ color: #4b5563;
280
+ font-weight: 500;
281
+ }
282
+
229
283
  /* Snapshot viewer sandbox wrapper */
230
284
  #__lr_snapshot_root {
231
285
  isolation: isolate;
@@ -470,6 +524,7 @@ var LionRapidEditor = (() => {
470
524
  .lr-status-dot.ready { background: #22c55e; }
471
525
  .lr-status-dot.locked { background: #a855f7; }
472
526
  .lr-status-dot.unsaved { background: #f59e0b; }
527
+ .lr-status-dot.readonly { background: #9ca3af; opacity: 0.6; } /* grey + faded \u2014 past my stage */
473
528
 
474
529
  /* \u2500\u2500 Locked textarea \u2500\u2500 */
475
530
  .lr-textarea-locked {
@@ -501,131 +556,47 @@ var LionRapidEditor = (() => {
501
556
  }
502
557
  `;
503
558
 
504
- // src/content/safe-href.ts
505
- var SAFE_SCHEME = /^[a-z][a-z0-9+.-]*:/i;
506
- var ALLOWED_SCHEMES = /* @__PURE__ */ new Set(["http:", "https:", "mailto:", "tel:", "sms:"]);
507
- function safeHref(href) {
508
- if (!href) return "#";
509
- const trimmed = href.trim();
510
- if (!trimmed) return "#";
511
- if (trimmed.startsWith("//")) {
512
- return "#";
513
- }
514
- if (trimmed.startsWith("/") || trimmed.startsWith("?") || trimmed.startsWith("#") || trimmed.startsWith("./") || trimmed.startsWith("../")) {
515
- return trimmed;
516
- }
517
- const schemeMatch = SAFE_SCHEME.exec(trimmed);
518
- if (schemeMatch) {
519
- const scheme = schemeMatch[0].toLowerCase();
520
- return ALLOWED_SCHEMES.has(scheme) ? trimmed : "#";
521
- }
522
- return trimmed;
559
+ // src/content/canonical-tag.ts
560
+ var VOID_TAGS = /* @__PURE__ */ new Set([
561
+ "area",
562
+ "base",
563
+ "br",
564
+ "col",
565
+ "embed",
566
+ "hr",
567
+ "img",
568
+ "input",
569
+ "link",
570
+ "meta",
571
+ "param",
572
+ "source",
573
+ "track",
574
+ "wbr"
575
+ ]);
576
+ function escapeAttrValue(value) {
577
+ return value.replace(/"/g, """);
523
578
  }
524
-
525
- // src/content/style-mapper/DefaultStyleMapper.ts
526
- var DefaultStyleMapper = class {
527
- constructor() {
528
- /**
529
- * Element → Style mapping
530
- * Maps HTML element names to their style property
531
- */
532
- __publicField(this, "elementToStyles", {
533
- b: "bold",
534
- strong: "bold",
535
- i: "italic",
536
- em: "italic",
537
- u: "underline",
538
- s: "strikethrough",
539
- del: "strikethrough",
540
- strike: "strikethrough",
541
- code: "code",
542
- mark: "highlight",
543
- sub: "subscript",
544
- sup: "superscript"
545
- });
546
- /**
547
- * Style → Element mapping (reverse)
548
- * Uses simple, familiar elements for developer convenience
549
- * Also accepts element names directly (strong, em, etc.)
550
- */
551
- __publicField(this, "styleToElement", {
552
- // Semantic style names → simple elements
553
- bold: "b",
554
- italic: "i",
555
- underline: "u",
556
- strikethrough: "s",
557
- code: "code",
558
- highlight: "mark",
559
- subscript: "sub",
560
- superscript: "sup",
561
- // Element names (for servers that store styles as element names)
562
- strong: "b",
563
- b: "b",
564
- em: "i",
565
- i: "i",
566
- u: "u",
567
- s: "s",
568
- del: "s",
569
- strike: "s",
570
- mark: "mark",
571
- sub: "sub",
572
- sup: "sup"
573
- });
574
- }
575
- /**
576
- * Convert HTML element to style properties
577
- *
578
- * @param element - Tag name (e.g., "b", "a", "strong")
579
- * @param attrs - Element attributes (e.g., { href: "/url" })
580
- * @returns Array of style strings
581
- */
582
- mapElementToStyles(element, attrs) {
583
- const styles = [];
584
- const tag = element.toLowerCase();
585
- const mappedStyle = this.elementToStyles[tag];
586
- if (mappedStyle) {
587
- styles.push(mappedStyle);
588
- }
589
- if (tag === "a" && attrs.href) {
590
- styles.push(`hyperlink:${attrs.href}`);
591
- }
592
- return styles;
593
- }
594
- /**
595
- * Convert style properties to HTML element
596
- *
597
- * Priority order:
598
- * 1. Hyperlink (becomes wrapper element with href)
599
- * 2. First recognized style property
600
- * 3. Fallback to span
601
- *
602
- * @param styles - Array of style strings
603
- * @returns Element info for serialization
604
- */
605
- mapStylesToElement(styles) {
606
- for (const style of styles) {
607
- if (style.startsWith("hyperlink:")) {
608
- return {
609
- element: "a",
610
- attrs: { href: safeHref(style.slice("hyperlink:".length)) }
611
- };
612
- }
613
- }
614
- for (const style of styles) {
615
- const element = this.styleToElement[style];
616
- if (element) {
617
- return {
618
- element,
619
- attrs: {}
620
- };
621
- }
622
- }
623
- return { element: "span", attrs: {} };
579
+ function serializeOpeningTag(tagName, attrs) {
580
+ const tag = tagName.toLowerCase();
581
+ let result = `<${tag}`;
582
+ for (const [name, value] of Object.entries(attrs)) {
583
+ result += ` ${name.toLowerCase()}="${escapeAttrValue(value)}"`;
584
+ }
585
+ result += ">";
586
+ return result;
587
+ }
588
+ function extractTagName(rawOpenTag) {
589
+ const end = rawOpenTag.search(/[\s>]/);
590
+ return rawOpenTag.slice(1, end === -1 ? rawOpenTag.length : end).toLowerCase();
591
+ }
592
+ function readRawStyle(styles) {
593
+ for (const s of styles) {
594
+ if (s.startsWith("raw:")) return s.slice(4);
624
595
  }
625
- };
596
+ return null;
597
+ }
626
598
 
627
599
  // src/browser/markup-parser.ts
628
- var styleMapper = new DefaultStyleMapper();
629
600
  function parseBlockElement(element) {
630
601
  const styles = {};
631
602
  let tagIndex = 0;
@@ -643,22 +614,17 @@ var LionRapidEditor = (() => {
643
614
  const attr = el.attributes[i];
644
615
  attrs[attr.name.toLowerCase()] = attr.value;
645
616
  }
646
- const styleList = styleMapper.mapElementToStyles(tag, attrs);
647
- if (styleList.length > 0) {
648
- tagIndex++;
649
- const idx = tagIndex;
650
- styles[String(idx)] = styleList;
651
- let inner = "";
652
- for (let i = 0; i < node.childNodes.length; i++) {
653
- inner += walk(node.childNodes[i]);
654
- }
655
- return `<${idx}>${inner}</${idx}>`;
617
+ tagIndex++;
618
+ const idx = tagIndex;
619
+ styles[String(idx)] = [`raw:${serializeOpeningTag(tag, attrs)}`];
620
+ if (VOID_TAGS.has(tag)) {
621
+ return `<${idx}/>`;
656
622
  }
657
- let result = "";
623
+ let inner = "";
658
624
  for (let i = 0; i < node.childNodes.length; i++) {
659
- result += walk(node.childNodes[i]);
625
+ inner += walk(node.childNodes[i]);
660
626
  }
661
- return result;
627
+ return `<${idx}>${inner}</${idx}>`;
662
628
  }
663
629
  let text = "";
664
630
  for (let i = 0; i < element.childNodes.length; i++) {
@@ -1088,16 +1054,6 @@ var LionRapidEditor = (() => {
1088
1054
 
1089
1055
  // src/content/serializer/HTMLSerializer.ts
1090
1056
  var HTMLSerializer = class {
1091
- constructor(options = {}) {
1092
- __publicField(this, "styleMapper");
1093
- this.styleMapper = options.styleMapper ?? new DefaultStyleMapper();
1094
- }
1095
- /**
1096
- * Serialize ContentUnit to HTML string
1097
- *
1098
- * @param unit - ContentUnit with indexed tags and styles
1099
- * @returns HTML string with semantic elements
1100
- */
1101
1057
  serialize(unit) {
1102
1058
  const { text, styles } = unit;
1103
1059
  const tagPattern = /<(\/?)?(\d+)(\/)?>/g;
@@ -1106,40 +1062,25 @@ var LionRapidEditor = (() => {
1106
1062
  let match;
1107
1063
  while ((match = tagPattern.exec(text)) !== null) {
1108
1064
  result += text.slice(lastIndex, match.index);
1109
- const [fullMatch, closingSlash, indexStr, selfClosingSlash] = match;
1110
- const tagIndex = indexStr;
1111
- const styleList = styles[tagIndex] || [];
1112
- if (closingSlash) {
1113
- const mapping = this.styleMapper.mapStylesToElement(styleList);
1114
- result += `</${mapping.element}>`;
1115
- } else if (selfClosingSlash) {
1116
- const mapping = this.styleMapper.mapStylesToElement(styleList);
1117
- const attrs = this.formatAttributes(mapping.attrs);
1118
- result += `<${mapping.element}${attrs} />`;
1065
+ const [fullMatch, closingSlash, indexStr] = match;
1066
+ const styleList = styles[indexStr] ?? [];
1067
+ const rawOpen = readRawStyle(styleList);
1068
+ if (rawOpen) {
1069
+ const tagName = extractTagName(rawOpen);
1070
+ const isVoid = VOID_TAGS.has(tagName);
1071
+ if (closingSlash) {
1072
+ if (!isVoid) result += `</${tagName}>`;
1073
+ } else {
1074
+ result += rawOpen;
1075
+ }
1119
1076
  } else {
1120
- const mapping = this.styleMapper.mapStylesToElement(styleList);
1121
- const attrs = this.formatAttributes(mapping.attrs);
1122
- result += `<${mapping.element}${attrs}>`;
1077
+ result += fullMatch;
1123
1078
  }
1124
1079
  lastIndex = match.index + fullMatch.length;
1125
1080
  }
1126
1081
  result += text.slice(lastIndex);
1127
1082
  return result;
1128
1083
  }
1129
- /**
1130
- * Format attributes object into HTML attribute string
1131
- */
1132
- formatAttributes(attrs) {
1133
- const entries = Object.entries(attrs);
1134
- if (entries.length === 0) return "";
1135
- return " " + entries.map(([key, value]) => `${key}="${this.escapeAttributeValue(value)}"`).join(" ");
1136
- }
1137
- /**
1138
- * Escape special characters in attribute values
1139
- */
1140
- escapeAttributeValue(value) {
1141
- return value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1142
- }
1143
1084
  };
1144
1085
 
1145
1086
  // src/browser/editor/live-preview.ts
@@ -1154,10 +1095,12 @@ var LionRapidEditor = (() => {
1154
1095
  const fullKey = `${locale}:${namespace}:${key}`;
1155
1096
  cache.set(fullKey, text, styles);
1156
1097
  }
1157
- if (styles && Object.keys(styles).length > 0) {
1098
+ const hasIndexedMarkers = /<\/?[0-9]+\/?>/.test(text);
1099
+ const sourceHasStyles = styles && Object.keys(styles).length > 0;
1100
+ if (hasIndexedMarkers) {
1158
1101
  const unit = { text, styles };
1159
1102
  element.innerHTML = htmlSerializer.serialize(unit);
1160
- } else {
1103
+ } else if (!sourceHasStyles) {
1161
1104
  element.textContent = text;
1162
1105
  }
1163
1106
  element.setAttribute("data-lr-block", key);
@@ -1239,8 +1182,14 @@ var LionRapidEditor = (() => {
1239
1182
  // Server already translated
1240
1183
  "data-lionrapid-client",
1241
1184
  // Client already translated
1242
- "data-no-translate"
1185
+ "data-no-translate",
1243
1186
  // Explicitly marked to skip
1187
+ // Edge proxy already rendered this block with the correct translation
1188
+ // and styles. Skip in normal extraction so the client-side auto-translator
1189
+ // can't overwrite the styled DOM with a stale (or plain-text) cache entry.
1190
+ // The editor passes includeTranslated=true to bypass this and re-extract
1191
+ // edge-rendered blocks for inspection.
1192
+ "data-lr-block"
1244
1193
  ];
1245
1194
  var BlockExtractor = class {
1246
1195
  constructor(excludePatterns = {}, excludeSelectors = [], minTextLength = 2) {
@@ -1364,6 +1313,10 @@ var LionRapidEditor = (() => {
1364
1313
  __publicField(this, "lastFetchTime", 0);
1365
1314
  __publicField(this, "lastFlushTime", 0);
1366
1315
  __publicField(this, "userRole");
1316
+ // Project workflow context — fetched once on first render, cached for the
1317
+ // sidebar's lifetime. Workflow shape is stable per editor session.
1318
+ __publicField(this, "workflowInfo", null);
1319
+ __publicField(this, "workflowInfoFetched", false);
1367
1320
  this.api = api;
1368
1321
  this.locale = locale;
1369
1322
  this.namespace = namespace;
@@ -1565,9 +1518,14 @@ var LionRapidEditor = (() => {
1565
1518
  const isReady = !!status?.isReady;
1566
1519
  const isLocked = !!status?.isLocked;
1567
1520
  const isConfirmed = !!status?.isConfirmed;
1521
+ const canEdit = status?.canEdit !== false;
1568
1522
  const composite = `${this.locale}:${this.namespace}:${key}`;
1569
1523
  const wrap = document.createElement("div");
1570
1524
  wrap.className = "lr-workflow-actions";
1525
+ const stripHost = document.createElement("div");
1526
+ stripHost.className = "lr-wf-strip-host";
1527
+ wrap.appendChild(stripHost);
1528
+ this.renderWorkflowStrip(stripHost, status);
1571
1529
  const addBtn = (label, action, variant = "secondary") => {
1572
1530
  const btn = document.createElement("button");
1573
1531
  btn.className = `lr-wf-btn lr-wf-${variant}`;
@@ -1582,12 +1540,17 @@ var LionRapidEditor = (() => {
1582
1540
  });
1583
1541
  wrap.appendChild(btn);
1584
1542
  };
1585
- if (!isLocked && !isReady) {
1586
- addBtn("Confirm", "confirm", "primary");
1587
- addBtn("Reject", "reject", "danger");
1588
- }
1589
- if (isConfirmed && !isLocked && !isReady) {
1590
- addBtn("Unconfirm", "unconfirm");
1543
+ if (canEdit && !isLocked && !isReady) {
1544
+ if (!isConfirmed) {
1545
+ addBtn("Confirm", "confirm", "primary");
1546
+ addBtn("Reject", "reject", "danger");
1547
+ } else {
1548
+ const isPerFile = this.workflowInfo?.workflowMode === "per_file";
1549
+ if (!isPerFile) {
1550
+ addBtn("Advance", "advance", "primary");
1551
+ }
1552
+ addBtn("Unconfirm", "unconfirm");
1553
+ }
1591
1554
  }
1592
1555
  if (isPM) {
1593
1556
  if (!isReady) {
@@ -1596,10 +1559,88 @@ var LionRapidEditor = (() => {
1596
1559
  addBtn("Unmark Ready", "unmark-ready", "danger");
1597
1560
  }
1598
1561
  }
1562
+ if (!canEdit && !isPM && !isReady && !isLocked) {
1563
+ const note = document.createElement("span");
1564
+ note.className = "lr-wf-note";
1565
+ note.textContent = typeof status?.stageIndex === "number" && status.stageIndex > 0 ? `Row already advanced (stage ${status.stageIndex + 1}) \u2014 out of your scope.` : "You are not assigned to this row at the current stage.";
1566
+ wrap.appendChild(note);
1567
+ }
1599
1568
  if (wrap.children.length > 0) {
1600
1569
  this.bodyEl.appendChild(wrap);
1601
1570
  }
1602
1571
  }
1572
+ /**
1573
+ * Render the workflow context strip ('Workflow: A → B → C — you are at stage X').
1574
+ * Highlights the user's POV stage and greys the rest.
1575
+ *
1576
+ * Highlight rule:
1577
+ * - PM/admin: highlight = row's current stage_index (deep-dive view)
1578
+ * - Non-PM: highlight = first userAssignedStages entry (fixed POV)
1579
+ *
1580
+ * Workflow info is fetched lazily on first call and cached for the sidebar's
1581
+ * lifetime. While the fetch is in flight, the host stays empty (no flash).
1582
+ */
1583
+ renderWorkflowStrip(host, status) {
1584
+ if (this.workflowInfo) {
1585
+ const node = this.buildWorkflowStripNode(this.workflowInfo, status);
1586
+ host.replaceChildren(...node ? [node] : []);
1587
+ return;
1588
+ }
1589
+ if (this.workflowInfoFetched) return;
1590
+ this.workflowInfoFetched = true;
1591
+ void this.api.fetchWorkflowInfo().then((info) => {
1592
+ this.workflowInfo = info;
1593
+ if (!this.current) return;
1594
+ const fresh = this.statusMap.get(this.current.key) ?? status ?? null;
1595
+ const node = info ? this.buildWorkflowStripNode(info, fresh) : null;
1596
+ if (host.isConnected) host.replaceChildren(...node ? [node] : []);
1597
+ });
1598
+ }
1599
+ buildWorkflowStripNode(info, status) {
1600
+ if (!info.stages || info.stages.length === 0) return null;
1601
+ let highlightIndex = null;
1602
+ if (info.isPmAdmin) {
1603
+ highlightIndex = typeof status?.stageIndex === "number" ? status.stageIndex : null;
1604
+ } else if (info.userAssignedStages.length > 0) {
1605
+ highlightIndex = info.userAssignedStages[0];
1606
+ }
1607
+ const strip = document.createElement("div");
1608
+ strip.className = "lr-wf-strip";
1609
+ const stagesRow = document.createElement("div");
1610
+ stagesRow.className = "lr-wf-strip-stages";
1611
+ info.stages.forEach((stage, i) => {
1612
+ const item = document.createElement("span");
1613
+ item.className = "lr-wf-strip-stage";
1614
+ if (highlightIndex !== null && stage.index === highlightIndex) {
1615
+ item.classList.add("is-active");
1616
+ } else {
1617
+ item.classList.add("is-muted");
1618
+ }
1619
+ item.textContent = stage.name;
1620
+ stagesRow.appendChild(item);
1621
+ if (i < info.stages.length - 1) {
1622
+ const sep = document.createElement("span");
1623
+ sep.className = "lr-wf-strip-sep";
1624
+ sep.textContent = "\u2192";
1625
+ stagesRow.appendChild(sep);
1626
+ }
1627
+ });
1628
+ strip.appendChild(stagesRow);
1629
+ const caption = document.createElement("div");
1630
+ caption.className = "lr-wf-strip-caption";
1631
+ if (highlightIndex !== null) {
1632
+ const stage = info.stages.find((s) => s.index === highlightIndex);
1633
+ const name = stage?.name ?? `Stage ${highlightIndex + 1}`;
1634
+ const label = info.isPmAdmin ? "Row currently at" : "You are here";
1635
+ caption.textContent = `${label}: stage ${highlightIndex + 1} of ${info.stages.length} \u2014 ${name}`;
1636
+ } else if (!info.isPmAdmin && info.userAssignedStages.length === 0) {
1637
+ caption.textContent = "You have no stage assignment on this project.";
1638
+ } else {
1639
+ caption.textContent = `${info.stages.length} stages`;
1640
+ }
1641
+ strip.appendChild(caption);
1642
+ return strip;
1643
+ }
1603
1644
  /** Re-fetch status for one key and re-render the current view. */
1604
1645
  async refreshStatusForKey(key) {
1605
1646
  const fresh = await this.api.fetchStatus([key], this.locale, this.namespace);
@@ -1622,13 +1663,18 @@ var LionRapidEditor = (() => {
1622
1663
  this.bodyEl.appendChild(field);
1623
1664
  }
1624
1665
  /**
1625
- * Get CSS class for the status dot based on actual row state.
1666
+ * Get CSS class for the status dot based on actual row state, with one
1667
+ * viewer-relative override: rows that have advanced past the viewer's
1668
+ * stage assignment render as 'readonly' (greyed) — same data for everyone,
1669
+ * different signal per viewer. Avoids the new-color-per-stage explosion the
1670
+ * user was originally considering; reuses existing semantics.
1626
1671
  */
1627
1672
  getDotClass(key) {
1628
1673
  if (this.unsavedKeys.has(key)) return "unsaved";
1629
1674
  const s = this.statusMap.get(key);
1630
1675
  if (!s) return "missing";
1631
1676
  if (s.isLocked) return "locked";
1677
+ if (s.canEdit === false && typeof s.stageIndex === "number" && s.stageIndex > 0) return "readonly";
1632
1678
  if (s.isReady) return "ready";
1633
1679
  if (s.isConfirmed) return "confirmed";
1634
1680
  if (s.rowState === "edited") return "edited";
@@ -1735,6 +1781,9 @@ var LionRapidEditor = (() => {
1735
1781
  __publicField(this, "onFlushed", () => {
1736
1782
  });
1737
1783
  __publicField(this, "onSessionExpired", null);
1784
+ // Workflow info is project-scoped and stable for the editor session lifetime.
1785
+ // Cached as a Promise so concurrent callers share one fetch.
1786
+ __publicField(this, "workflowInfoPromise", null);
1738
1787
  __publicField(this, "statusAbort", null);
1739
1788
  __publicField(this, "handleVisibility", () => {
1740
1789
  if (document.visibilityState === "hidden") {
@@ -1873,6 +1922,37 @@ var LionRapidEditor = (() => {
1873
1922
  get pendingCount() {
1874
1923
  return this.queue.length;
1875
1924
  }
1925
+ /**
1926
+ * Fetch workflow context for the project (stages, user's stage assignments,
1927
+ * PM/admin flag). Cached for the editor session lifetime — workflow shape
1928
+ * doesn't change mid-session and the sidebar re-uses it on every row click.
1929
+ * Returns null if the fetch fails; callers should hide the strip in that case.
1930
+ */
1931
+ async fetchWorkflowInfo() {
1932
+ if (this.workflowInfoPromise) return this.workflowInfoPromise;
1933
+ this.workflowInfoPromise = (async () => {
1934
+ try {
1935
+ const response = await fetch(`${this.apiBase}/api/integrations/editor/workflow`, {
1936
+ headers: { "Authorization": `Bearer ${this.sessionToken}` }
1937
+ });
1938
+ if (response.status === 401) {
1939
+ this.handleSessionExpired();
1940
+ return null;
1941
+ }
1942
+ if (!response.ok) return null;
1943
+ const data = await response.json();
1944
+ return {
1945
+ stages: Array.isArray(data.stages) ? data.stages : [],
1946
+ userAssignedStages: Array.isArray(data.user_assigned_stages) ? data.user_assigned_stages : [],
1947
+ isPmAdmin: Boolean(data.is_pm_admin),
1948
+ workflowMode: data.workflow_mode === "per_file" ? "per_file" : "per_row"
1949
+ };
1950
+ } catch {
1951
+ return null;
1952
+ }
1953
+ })();
1954
+ return this.workflowInfoPromise;
1955
+ }
1876
1956
  // ============================================
1877
1957
  // Session renewal
1878
1958
  // ============================================
@@ -1910,6 +1990,7 @@ var LionRapidEditor = (() => {
1910
1990
  token: this.sessionToken,
1911
1991
  exp: payload.exp || 0
1912
1992
  }));
1993
+ document.cookie = "__lr_editor_active=1; path=/; SameSite=Lax";
1913
1994
  }
1914
1995
  } catch {
1915
1996
  }
@@ -1928,6 +2009,7 @@ var LionRapidEditor = (() => {
1928
2009
  }
1929
2010
  }
1930
2011
  handleSessionExpired() {
2012
+ document.cookie = "__lr_editor_active=; path=/; SameSite=Lax; Max-Age=0";
1931
2013
  this.onStatus("error", "Session expired. Please reopen the editor from the dashboard.");
1932
2014
  if (this.onSessionExpired) {
1933
2015
  this.onSessionExpired();
@@ -2369,6 +2451,13 @@ var LionRapidEditor = (() => {
2369
2451
 
2370
2452
  // src/browser/editor/editor-init.ts
2371
2453
  var SESSION_KEY = "__lr_editor_session";
2454
+ var PREVIEW_COOKIE = "__lr_editor_active";
2455
+ function setPreviewCookie() {
2456
+ document.cookie = `${PREVIEW_COOKIE}=1; path=/; SameSite=Lax`;
2457
+ }
2458
+ function clearPreviewCookie() {
2459
+ document.cookie = `${PREVIEW_COOKIE}=; path=/; SameSite=Lax; Max-Age=0`;
2460
+ }
2372
2461
  function getStoredSession() {
2373
2462
  try {
2374
2463
  const raw = sessionStorage.getItem(SESSION_KEY);
@@ -2376,8 +2465,10 @@ var LionRapidEditor = (() => {
2376
2465
  const parsed = JSON.parse(raw);
2377
2466
  if (parsed.exp * 1e3 < Date.now() + 3e4) {
2378
2467
  sessionStorage.removeItem(SESSION_KEY);
2468
+ clearPreviewCookie();
2379
2469
  return null;
2380
2470
  }
2471
+ setPreviewCookie();
2381
2472
  return parsed.token;
2382
2473
  } catch {
2383
2474
  return null;
@@ -2391,11 +2482,13 @@ var LionRapidEditor = (() => {
2391
2482
  const payload = JSON.parse(atob(parts[1]));
2392
2483
  const session = { token, exp: payload.exp || 0 };
2393
2484
  sessionStorage.setItem(SESSION_KEY, JSON.stringify(session));
2485
+ setPreviewCookie();
2394
2486
  } catch {
2395
2487
  }
2396
2488
  }
2397
2489
  function clearSession() {
2398
2490
  sessionStorage.removeItem(SESSION_KEY);
2491
+ clearPreviewCookie();
2399
2492
  }
2400
2493
  function extractSessionRole(token) {
2401
2494
  try {