@chrysb/alphaclaw 0.3.4 → 0.3.5-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.
@@ -8,7 +8,11 @@ import {
8
8
  } from "https://esm.sh/preact/hooks";
9
9
  import htm from "https://esm.sh/htm";
10
10
  import { marked } from "https://esm.sh/marked";
11
- import { fetchFileContent, saveFileContent } from "../lib/api.js";
11
+ import {
12
+ fetchBrowseFileDiff,
13
+ fetchFileContent,
14
+ saveFileContent,
15
+ } from "../lib/api.js";
12
16
  import {
13
17
  formatFrontmatterValue,
14
18
  getFileSyntaxKind,
@@ -24,7 +28,7 @@ import {
24
28
  import { ActionButton } from "./action-button.js";
25
29
  import { LoadingSpinner } from "./loading-spinner.js";
26
30
  import { SegmentedControl } from "./segmented-control.js";
27
- import { SaveFillIcon } from "./icons.js";
31
+ import { LockLineIcon, SaveFillIcon } from "./icons.js";
28
32
  import { showToast } from "./toast.js";
29
33
 
30
34
  const html = htm.bind(h);
@@ -32,6 +36,12 @@ const kFileViewerModeStorageKey = "alphaclaw.browse.fileViewerMode";
32
36
  const kLegacyFileViewerModeStorageKey = "alphaclawBrowseFileViewerMode";
33
37
  const kEditorSelectionStorageKey = "alphaclaw.browse.editorSelectionByPath";
34
38
  const kProtectedBrowsePaths = new Set(["openclaw.json", "devices/paired.json"]);
39
+ const kLockedBrowsePaths = new Set([
40
+ "hooks/bootstrap/agents.md",
41
+ "hooks/bootstrap/tools.md",
42
+ ".alphaclaw/hourly-git-sync.sh",
43
+ ".alphaclaw/.cli-device-auto-approved",
44
+ ]);
35
45
  const kLoadingIndicatorDelayMs = 1000;
36
46
  const kFileRefreshIntervalMs = 5000;
37
47
 
@@ -49,6 +59,20 @@ const normalizePolicyPath = (inputPath) =>
49
59
  .trim()
50
60
  .toLowerCase();
51
61
 
62
+ const matchesPolicyPath = (policyPathSet, normalizedPath) => {
63
+ const safeNormalizedPath = String(normalizedPath || "").trim();
64
+ if (!safeNormalizedPath) return false;
65
+ for (const policyPath of policyPathSet) {
66
+ if (
67
+ safeNormalizedPath === policyPath ||
68
+ safeNormalizedPath.endsWith(`/${policyPath}`)
69
+ ) {
70
+ return true;
71
+ }
72
+ }
73
+ return false;
74
+ };
75
+
52
76
  const readStoredFileViewerMode = () => {
53
77
  try {
54
78
  const storedMode = String(
@@ -68,35 +92,38 @@ const clampSelectionIndex = (value, maxValue) => {
68
92
  return Math.max(0, Math.min(maxValue, numericValue));
69
93
  };
70
94
 
71
- const readStoredEditorSelection = (filePath) => {
72
- const safePath = String(filePath || "").trim();
73
- if (!safePath) return null;
95
+ const readEditorSelectionStorageMap = () => {
74
96
  try {
75
- const rawStorageValue = window.localStorage.getItem(kEditorSelectionStorageKey);
76
- if (!rawStorageValue) return null;
97
+ const rawStorageValue = window.localStorage.getItem(
98
+ kEditorSelectionStorageKey,
99
+ );
100
+ if (!rawStorageValue) return {};
77
101
  const parsedStorageValue = JSON.parse(rawStorageValue);
78
- if (!parsedStorageValue || typeof parsedStorageValue !== "object") return null;
79
- const selection = parsedStorageValue[safePath];
80
- if (!selection || typeof selection !== "object") return null;
81
- return {
82
- start: selection.start,
83
- end: selection.end,
84
- };
102
+ if (!parsedStorageValue || typeof parsedStorageValue !== "object")
103
+ return {};
104
+ return parsedStorageValue;
85
105
  } catch {
86
- return null;
106
+ return {};
87
107
  }
88
108
  };
89
109
 
110
+ const readStoredEditorSelection = (filePath) => {
111
+ const safePath = String(filePath || "").trim();
112
+ if (!safePath) return null;
113
+ const storageMap = readEditorSelectionStorageMap();
114
+ const selection = storageMap[safePath];
115
+ if (!selection || typeof selection !== "object") return null;
116
+ return {
117
+ start: selection.start,
118
+ end: selection.end,
119
+ };
120
+ };
121
+
90
122
  const writeStoredEditorSelection = (filePath, selection) => {
91
123
  const safePath = String(filePath || "").trim();
92
124
  if (!safePath || !selection || typeof selection !== "object") return;
93
125
  try {
94
- const rawStorageValue = window.localStorage.getItem(kEditorSelectionStorageKey);
95
- const parsedStorageValue = rawStorageValue ? JSON.parse(rawStorageValue) : {};
96
- const nextStorageValue =
97
- parsedStorageValue && typeof parsedStorageValue === "object"
98
- ? parsedStorageValue
99
- : {};
126
+ const nextStorageValue = readEditorSelectionStorageMap();
100
127
  nextStorageValue[safePath] = {
101
128
  start: selection.start,
102
129
  end: selection.end,
@@ -108,10 +135,11 @@ const writeStoredEditorSelection = (filePath, selection) => {
108
135
  } catch {}
109
136
  };
110
137
 
111
-
112
138
  export const FileViewer = ({
113
139
  filePath = "",
114
140
  isPreviewOnly = false,
141
+ browseView = "edit",
142
+ onRequestEdit = () => {},
115
143
  }) => {
116
144
  const normalizedPath = String(filePath || "").trim();
117
145
  const normalizedPolicyPath = normalizePolicyPath(normalizedPath);
@@ -121,11 +149,15 @@ export const FileViewer = ({
121
149
  const [loading, setLoading] = useState(false);
122
150
  const [showDelayedLoadingSpinner, setShowDelayedLoadingSpinner] =
123
151
  useState(false);
152
+ const [diffLoading, setDiffLoading] = useState(false);
153
+ const [diffError, setDiffError] = useState("");
154
+ const [diffContent, setDiffContent] = useState("");
124
155
  const [saving, setSaving] = useState(false);
125
156
  const [error, setError] = useState("");
126
157
  const [isFolderPath, setIsFolderPath] = useState(false);
127
158
  const [frontmatterCollapsed, setFrontmatterCollapsed] = useState(false);
128
- const [externalChangeNoticeShown, setExternalChangeNoticeShown] = useState(false);
159
+ const [externalChangeNoticeShown, setExternalChangeNoticeShown] =
160
+ useState(false);
129
161
  const [protectedEditBypassPaths, setProtectedEditBypassPaths] = useState(
130
162
  () => new Set(),
131
163
  );
@@ -147,11 +179,17 @@ export const FileViewer = ({
147
179
  );
148
180
  const hasSelectedPath = normalizedPath.length > 0;
149
181
  const canEditFile = hasSelectedPath && !isFolderPath && !isPreviewOnly;
182
+ const isDiffView = String(browseView || "edit") === "diff";
150
183
  const isDirty = canEditFile && content !== initialContent;
184
+ const isLockedFile =
185
+ canEditFile && matchesPolicyPath(kLockedBrowsePaths, normalizedPolicyPath);
151
186
  const isProtectedFile =
152
- canEditFile && kProtectedBrowsePaths.has(normalizedPolicyPath);
187
+ canEditFile &&
188
+ !isLockedFile &&
189
+ matchesPolicyPath(kProtectedBrowsePaths, normalizedPolicyPath);
153
190
  const isProtectedLocked =
154
191
  isProtectedFile && !protectedEditBypassPaths.has(normalizedPolicyPath);
192
+ const isEditBlocked = isLockedFile || isProtectedLocked;
155
193
  const syntaxKind = useMemo(
156
194
  () => getFileSyntaxKind(normalizedPath),
157
195
  [normalizedPath],
@@ -315,7 +353,9 @@ export const FileViewer = ({
315
353
  dispatchEvent: (event) => window.dispatchEvent(event),
316
354
  });
317
355
  setExternalChangeNoticeShown(false);
318
- window.dispatchEvent(new CustomEvent("alphaclaw:browse-tree-refresh"));
356
+ window.dispatchEvent(
357
+ new CustomEvent("alphaclaw:browse-tree-refresh"),
358
+ );
319
359
  return;
320
360
  }
321
361
  if (!externalChangeNoticeShown) {
@@ -331,7 +371,10 @@ export const FileViewer = ({
331
371
  fileRefreshInFlightRef.current = false;
332
372
  }
333
373
  };
334
- const intervalId = window.setInterval(refreshFromDisk, kFileRefreshIntervalMs);
374
+ const intervalId = window.setInterval(
375
+ refreshFromDisk,
376
+ kFileRefreshIntervalMs,
377
+ );
335
378
  return () => {
336
379
  window.clearInterval(intervalId);
337
380
  };
@@ -347,6 +390,36 @@ export const FileViewer = ({
347
390
  externalChangeNoticeShown,
348
391
  ]);
349
392
 
393
+ useEffect(() => {
394
+ let active = true;
395
+ if (!hasSelectedPath || !isDiffView || isPreviewOnly) {
396
+ setDiffLoading(false);
397
+ setDiffError("");
398
+ setDiffContent("");
399
+ return () => {
400
+ active = false;
401
+ };
402
+ }
403
+ const loadDiff = async () => {
404
+ setDiffLoading(true);
405
+ setDiffError("");
406
+ try {
407
+ const data = await fetchBrowseFileDiff(normalizedPath);
408
+ if (!active) return;
409
+ setDiffContent(String(data?.content || ""));
410
+ } catch (nextError) {
411
+ if (!active) return;
412
+ setDiffError(nextError.message || "Could not load diff");
413
+ } finally {
414
+ if (active) setDiffLoading(false);
415
+ }
416
+ };
417
+ loadDiff();
418
+ return () => {
419
+ active = false;
420
+ };
421
+ }, [hasSelectedPath, isDiffView, isPreviewOnly, normalizedPath]);
422
+
350
423
  useEffect(() => {
351
424
  if (loadedFilePathRef.current !== normalizedPath) return;
352
425
  if (!canEditFile || !hasSelectedPath || loading) return;
@@ -371,26 +444,79 @@ export const FileViewer = ({
371
444
  ]);
372
445
 
373
446
  useEffect(() => {
374
- if (!canEditFile || loading || !hasSelectedPath) return;
375
- if (loadedFilePathRef.current !== normalizedPath) return;
376
- if (restoredSelectionPathRef.current === normalizedPath) return;
377
- const textareaElement = editorTextareaRef.current;
378
- if (!textareaElement) return;
447
+ if (!canEditFile || loading || !hasSelectedPath) return () => {};
448
+ if (loadedFilePathRef.current !== normalizedPath) return () => {};
449
+ if (restoredSelectionPathRef.current === normalizedPath) return () => {};
450
+ if (viewMode !== "edit") return () => {};
379
451
  const storedSelection = readStoredEditorSelection(normalizedPath);
380
- restoredSelectionPathRef.current = normalizedPath;
381
- if (!storedSelection) return;
382
- const maxIndex = String(content || "").length;
383
- const start = clampSelectionIndex(storedSelection.start, maxIndex);
384
- const end = clampSelectionIndex(storedSelection.end, maxIndex);
385
- textareaElement.setSelectionRange(start, Math.max(start, end));
386
- }, [canEditFile, loading, hasSelectedPath, normalizedPath, content]);
387
-
388
- const handleSave = async () => {
389
- if (!canEditFile || saving || !isDirty || isProtectedLocked) return;
452
+ if (!storedSelection) {
453
+ restoredSelectionPathRef.current = normalizedPath;
454
+ return () => {};
455
+ }
456
+ let frameId = 0;
457
+ let attempts = 0;
458
+ const restoreSelection = () => {
459
+ const textareaElement = editorTextareaRef.current;
460
+ if (!textareaElement) {
461
+ attempts += 1;
462
+ if (attempts < 6)
463
+ frameId = window.requestAnimationFrame(restoreSelection);
464
+ return;
465
+ }
466
+ const maxIndex = String(content || "").length;
467
+ const start = clampSelectionIndex(storedSelection.start, maxIndex);
468
+ const end = clampSelectionIndex(storedSelection.end, maxIndex);
469
+ textareaElement.focus();
470
+ textareaElement.setSelectionRange(start, Math.max(start, end));
471
+ window.requestAnimationFrame(() => {
472
+ const nextTextareaElement = editorTextareaRef.current;
473
+ if (!nextTextareaElement) return;
474
+ const safeContent = String(content || "");
475
+ const safeStart = clampSelectionIndex(start, safeContent.length);
476
+ const lineIndex =
477
+ safeContent.slice(0, safeStart).split("\n").length - 1;
478
+ const computedStyle = window.getComputedStyle(nextTextareaElement);
479
+ const parsedLineHeight = Number.parseFloat(
480
+ computedStyle.lineHeight || "",
481
+ );
482
+ const lineHeight =
483
+ Number.isFinite(parsedLineHeight) && parsedLineHeight > 0
484
+ ? parsedLineHeight
485
+ : 20;
486
+ const nextScrollTop = Math.max(
487
+ 0,
488
+ lineIndex * lineHeight - nextTextareaElement.clientHeight * 0.4,
489
+ );
490
+ nextTextareaElement.scrollTop = nextScrollTop;
491
+ if (editorLineNumbersRef.current) {
492
+ editorLineNumbersRef.current.scrollTop = nextScrollTop;
493
+ }
494
+ if (editorHighlightRef.current) {
495
+ editorHighlightRef.current.scrollTop = nextScrollTop;
496
+ }
497
+ viewScrollRatioRef.current = getScrollRatio(nextTextareaElement);
498
+ });
499
+ restoredSelectionPathRef.current = normalizedPath;
500
+ };
501
+ frameId = window.requestAnimationFrame(restoreSelection);
502
+ return () => {
503
+ if (frameId) window.cancelAnimationFrame(frameId);
504
+ };
505
+ }, [
506
+ canEditFile,
507
+ loading,
508
+ hasSelectedPath,
509
+ normalizedPath,
510
+ content,
511
+ viewMode,
512
+ ]);
513
+
514
+ const handleSave = useCallback(async () => {
515
+ if (!canEditFile || saving || !isDirty || isEditBlocked) return;
390
516
  setSaving(true);
391
517
  setError("");
392
518
  try {
393
- const data = await saveFileContent(normalizedPath, content);
519
+ await saveFileContent(normalizedPath, content);
394
520
  setInitialContent(content);
395
521
  setExternalChangeNoticeShown(false);
396
522
  clearStoredFileDraft(normalizedPath);
@@ -402,11 +528,7 @@ export const FileViewer = ({
402
528
  detail: { path: normalizedPath },
403
529
  }),
404
530
  );
405
- if (data.synced === false) {
406
- showToast("Saved, but git sync failed", "error");
407
- } else {
408
- showToast("Saved and synced", "success");
409
- }
531
+ showToast("Saved", "success");
410
532
  } catch (saveError) {
411
533
  const message = saveError.message || "Could not save file";
412
534
  setError(message);
@@ -414,7 +536,31 @@ export const FileViewer = ({
414
536
  } finally {
415
537
  setSaving(false);
416
538
  }
417
- };
539
+ }, [
540
+ canEditFile,
541
+ saving,
542
+ isDirty,
543
+ isEditBlocked,
544
+ normalizedPath,
545
+ content,
546
+ initialContent,
547
+ ]);
548
+
549
+ useEffect(() => {
550
+ const handleKeyDown = (event) => {
551
+ const isSaveShortcut =
552
+ (event.metaKey || event.ctrlKey) &&
553
+ !event.shiftKey &&
554
+ !event.altKey &&
555
+ String(event.key || "").toLowerCase() === "s";
556
+ if (!isSaveShortcut) return;
557
+ if (!canEditFile || isPreviewOnly || isDiffView || viewMode !== "edit") return;
558
+ event.preventDefault();
559
+ void handleSave();
560
+ };
561
+ window.addEventListener("keydown", handleKeyDown);
562
+ return () => window.removeEventListener("keydown", handleKeyDown);
563
+ }, [canEditFile, isPreviewOnly, isDiffView, viewMode, handleSave]);
418
564
 
419
565
  const handleEditProtectedFile = () => {
420
566
  if (!normalizedPolicyPath) return;
@@ -426,7 +572,7 @@ export const FileViewer = ({
426
572
  };
427
573
 
428
574
  const handleContentInput = (event) => {
429
- if (isProtectedLocked || isPreviewOnly) return;
575
+ if (isEditBlocked || isPreviewOnly) return;
430
576
  const nextContent = event.target.value;
431
577
  setContent(nextContent);
432
578
  if (hasSelectedPath && canEditFile) {
@@ -571,7 +717,8 @@ export const FileViewer = ({
571
717
  ${isPreviewOnly
572
718
  ? html`<div class="file-viewer-preview-pill">Preview</div>`
573
719
  : null}
574
- ${isMarkdownFile &&
720
+ ${!isDiffView &&
721
+ isMarkdownFile &&
575
722
  html`
576
723
  <${SegmentedControl}
577
724
  className="mr-2.5"
@@ -583,20 +730,49 @@ export const FileViewer = ({
583
730
  onChange=${handleChangeViewMode}
584
731
  />
585
732
  `}
586
- <${ActionButton}
587
- onClick=${handleSave}
588
- disabled=${loading || !isDirty || !canEditFile || isProtectedLocked}
589
- loading=${saving}
590
- tone=${isDirty ? "primary" : "secondary"}
591
- size="sm"
592
- idleLabel="Save"
593
- loadingLabel="Saving..."
594
- idleIcon=${SaveFillIcon}
595
- idleIconClassName="file-viewer-save-icon"
596
- className="file-viewer-save-action"
597
- />
733
+ ${!isDiffView
734
+ ? html`
735
+ <${ActionButton}
736
+ onClick=${handleSave}
737
+ disabled=${loading || !isDirty || !canEditFile || isEditBlocked}
738
+ loading=${saving}
739
+ tone=${isDirty ? "primary" : "secondary"}
740
+ size="sm"
741
+ idleLabel="Save"
742
+ loadingLabel="Saving..."
743
+ idleIcon=${SaveFillIcon}
744
+ idleIconClassName="file-viewer-save-icon"
745
+ className="file-viewer-save-action"
746
+ />
747
+ `
748
+ : null}
598
749
  </div>
599
- ${isProtectedFile
750
+ ${isDiffView
751
+ ? html`
752
+ <div class="file-viewer-protected-banner file-viewer-diff-banner">
753
+ <div class="file-viewer-protected-banner-text">
754
+ Viewing unsynced changes
755
+ </div>
756
+ <${ActionButton}
757
+ onClick=${() => onRequestEdit(normalizedPath)}
758
+ tone="secondary"
759
+ size="sm"
760
+ idleLabel="View file"
761
+ />
762
+ </div>
763
+ `
764
+ : null}
765
+ ${!isDiffView && isLockedFile
766
+ ? html`
767
+ <div class="file-viewer-protected-banner is-locked">
768
+ <${LockLineIcon} className="file-viewer-protected-banner-icon" />
769
+ <div class="file-viewer-protected-banner-text">
770
+ This file is managed by Alpha Claw and cannot be edited.
771
+ </div>
772
+ </div>
773
+ `
774
+ : null}
775
+ ${!isDiffView && isProtectedFile
600
776
  ? html`
601
777
  <div class="file-viewer-protected-banner">
602
778
  <div class="file-viewer-protected-banner-text">
@@ -683,139 +859,215 @@ ${formattedValue}</pre
683
859
  Folder selected. Choose a file from this folder in the tree.
684
860
  </div>
685
861
  `
686
- : html`
687
- ${isMarkdownFile
688
- ? html`
689
- <div
690
- class=${`file-viewer-preview ${viewMode === "preview" ? "" : "file-viewer-pane-hidden"}`}
691
- ref=${previewRef}
692
- onscroll=${handlePreviewScroll}
693
- aria-hidden=${viewMode === "preview" ? "false" : "true"}
694
- dangerouslySetInnerHTML=${{ __html: previewHtml }}
695
- ></div>
696
- <div
697
- class=${`file-viewer-editor-shell ${viewMode === "edit" ? "" : "file-viewer-pane-hidden"}`}
698
- aria-hidden=${viewMode === "edit" ? "false" : "true"}
699
- >
862
+ : isDiffView
863
+ ? html`
864
+ <div class="file-viewer-diff-shell">
865
+ ${diffLoading
866
+ ? html`
867
+ <div class="file-viewer-loading-shell">
868
+ <${LoadingSpinner} className="h-4 w-4" />
869
+ </div>
870
+ `
871
+ : diffError
872
+ ? html`
873
+ <div
874
+ class="file-viewer-state file-viewer-state-error"
875
+ >
876
+ ${diffError}
877
+ </div>
878
+ `
879
+ : html`
880
+ <pre class="file-viewer-diff-pre">
881
+ ${(diffContent || "").split("\n").map((line, lineIndex) => {
882
+ const lineClass =
883
+ line.startsWith("+") &&
884
+ !line.startsWith("+++")
885
+ ? "is-added"
886
+ : line.startsWith("-") &&
887
+ !line.startsWith("---")
888
+ ? "is-removed"
889
+ : line.startsWith("@@")
890
+ ? "is-hunk"
891
+ : line.startsWith("diff ") ||
892
+ line.startsWith("index ") ||
893
+ line.startsWith("--- ") ||
894
+ line.startsWith("+++ ")
895
+ ? "is-header"
896
+ : "";
897
+ return html`
898
+ <div
899
+ key=${`${lineIndex}:${line.slice(0, 20)}`}
900
+ class=${`file-viewer-diff-line ${lineClass}`.trim()}
901
+ >
902
+ ${line || " "}
903
+ </div>
904
+ `;
905
+ })}
906
+ </pre
907
+ >
908
+ `}
909
+ </div>
910
+ `
911
+ : html`
912
+ ${isMarkdownFile
913
+ ? html`
700
914
  <div
701
- class="file-viewer-editor-line-num-col"
702
- ref=${editorLineNumbersRef}
915
+ class=${`file-viewer-preview ${viewMode === "preview" ? "" : "file-viewer-pane-hidden"}`}
916
+ ref=${previewRef}
917
+ onscroll=${handlePreviewScroll}
918
+ aria-hidden=${viewMode === "preview"
919
+ ? "false"
920
+ : "true"}
921
+ dangerouslySetInnerHTML=${{ __html: previewHtml }}
922
+ ></div>
923
+ <div
924
+ class=${`file-viewer-editor-shell ${viewMode === "edit" ? "" : "file-viewer-pane-hidden"}`}
925
+ aria-hidden=${viewMode === "edit" ? "false" : "true"}
703
926
  >
704
- ${editorLineNumbers.map(
705
- (lineNumber) => html`
706
- <div
707
- class="file-viewer-editor-line-num"
708
- key=${lineNumber}
709
- ref=${(element) => {
710
- editorLineNumberRowRefs.current[
711
- lineNumber - 1
712
- ] = element;
713
- }}
714
- >
715
- ${lineNumber}
716
- </div>
717
- `,
718
- )}
719
- </div>
720
- <div class="file-viewer-editor-stack">
721
927
  <div
722
- class="file-viewer-editor-highlight"
723
- ref=${editorHighlightRef}
928
+ class="file-viewer-editor-line-num-col"
929
+ ref=${editorLineNumbersRef}
724
930
  >
725
- ${highlightedEditorLines.map(
726
- (line) => html`
931
+ ${editorLineNumbers.map(
932
+ (lineNumber) => html`
727
933
  <div
728
- class="file-viewer-editor-highlight-line"
729
- key=${line.lineNumber}
934
+ class="file-viewer-editor-line-num"
935
+ key=${lineNumber}
730
936
  ref=${(element) => {
731
- editorHighlightLineRefs.current[
732
- line.lineNumber - 1
937
+ editorLineNumberRowRefs.current[
938
+ lineNumber - 1
733
939
  ] = element;
734
940
  }}
735
941
  >
736
- <span
737
- class="file-viewer-editor-highlight-line-content"
738
- dangerouslySetInnerHTML=${{
739
- __html: line.html,
740
- }}
741
- ></span>
942
+ ${lineNumber}
742
943
  </div>
743
944
  `,
744
945
  )}
745
946
  </div>
746
- <textarea
747
- class="file-viewer-editor file-viewer-editor-overlay"
748
- ref=${editorTextareaRef}
749
- value=${content}
750
- onInput=${handleContentInput}
751
- onScroll=${handleEditorScroll}
752
- onSelect=${handleEditorSelectionChange}
753
- onKeyUp=${handleEditorSelectionChange}
754
- onClick=${handleEditorSelectionChange}
755
- spellcheck=${false}
756
- autocorrect="off"
757
- autocapitalize="off"
758
- autocomplete="off"
759
- data-gramm="false"
760
- data-gramm_editor="false"
761
- data-enable-grammarly="false"
762
- wrap="soft"
763
- ></textarea>
764
- </div>
765
- </div>
766
- `
767
- : html`
768
- <div class="file-viewer-editor-shell">
769
- <div
770
- class="file-viewer-editor-line-num-col"
771
- ref=${editorLineNumbersRef}
772
- >
773
- ${editorLineNumbers.map(
774
- (lineNumber) => html`
775
- <div
776
- class="file-viewer-editor-line-num"
777
- key=${lineNumber}
778
- ref=${(element) => {
779
- editorLineNumberRowRefs.current[
780
- lineNumber - 1
781
- ] = element;
782
- }}
783
- >
784
- ${lineNumber}
785
- </div>
786
- `,
787
- )}
947
+ <div class="file-viewer-editor-stack">
948
+ <div
949
+ class="file-viewer-editor-highlight"
950
+ ref=${editorHighlightRef}
951
+ >
952
+ ${highlightedEditorLines.map(
953
+ (line) => html`
954
+ <div
955
+ class="file-viewer-editor-highlight-line"
956
+ key=${line.lineNumber}
957
+ ref=${(element) => {
958
+ editorHighlightLineRefs.current[
959
+ line.lineNumber - 1
960
+ ] = element;
961
+ }}
962
+ >
963
+ <span
964
+ class="file-viewer-editor-highlight-line-content"
965
+ dangerouslySetInnerHTML=${{
966
+ __html: line.html,
967
+ }}
968
+ ></span>
969
+ </div>
970
+ `,
971
+ )}
972
+ </div>
973
+ <textarea
974
+ class="file-viewer-editor file-viewer-editor-overlay"
975
+ ref=${editorTextareaRef}
976
+ value=${content}
977
+ onInput=${handleContentInput}
978
+ onScroll=${handleEditorScroll}
979
+ onSelect=${handleEditorSelectionChange}
980
+ onKeyUp=${handleEditorSelectionChange}
981
+ onClick=${handleEditorSelectionChange}
982
+ disabled=${isEditBlocked || isPreviewOnly}
983
+ readonly=${isEditBlocked || isPreviewOnly}
984
+ spellcheck=${false}
985
+ autocorrect="off"
986
+ autocapitalize="off"
987
+ autocomplete="off"
988
+ data-gramm="false"
989
+ data-gramm_editor="false"
990
+ data-enable-grammarly="false"
991
+ wrap="soft"
992
+ ></textarea>
993
+ </div>
788
994
  </div>
789
- ${shouldUseHighlightedEditor
790
- ? html`
791
- <div class="file-viewer-editor-stack">
995
+ `
996
+ : html`
997
+ <div class="file-viewer-editor-shell">
998
+ <div
999
+ class="file-viewer-editor-line-num-col"
1000
+ ref=${editorLineNumbersRef}
1001
+ >
1002
+ ${editorLineNumbers.map(
1003
+ (lineNumber) => html`
792
1004
  <div
793
- class="file-viewer-editor-highlight"
794
- ref=${editorHighlightRef}
1005
+ class="file-viewer-editor-line-num"
1006
+ key=${lineNumber}
1007
+ ref=${(element) => {
1008
+ editorLineNumberRowRefs.current[
1009
+ lineNumber - 1
1010
+ ] = element;
1011
+ }}
795
1012
  >
796
- ${highlightedEditorLines.map(
797
- (line) => html`
798
- <div
799
- class="file-viewer-editor-highlight-line"
800
- key=${line.lineNumber}
801
- ref=${(element) => {
802
- editorHighlightLineRefs.current[
803
- line.lineNumber - 1
804
- ] = element;
805
- }}
806
- >
807
- <span
808
- class="file-viewer-editor-highlight-line-content"
809
- dangerouslySetInnerHTML=${{
810
- __html: line.html,
1013
+ ${lineNumber}
1014
+ </div>
1015
+ `,
1016
+ )}
1017
+ </div>
1018
+ ${shouldUseHighlightedEditor
1019
+ ? html`
1020
+ <div class="file-viewer-editor-stack">
1021
+ <div
1022
+ class="file-viewer-editor-highlight"
1023
+ ref=${editorHighlightRef}
1024
+ >
1025
+ ${highlightedEditorLines.map(
1026
+ (line) => html`
1027
+ <div
1028
+ class="file-viewer-editor-highlight-line"
1029
+ key=${line.lineNumber}
1030
+ ref=${(element) => {
1031
+ editorHighlightLineRefs.current[
1032
+ line.lineNumber - 1
1033
+ ] = element;
811
1034
  }}
812
- ></span>
813
- </div>
814
- `,
815
- )}
1035
+ >
1036
+ <span
1037
+ class="file-viewer-editor-highlight-line-content"
1038
+ dangerouslySetInnerHTML=${{
1039
+ __html: line.html,
1040
+ }}
1041
+ ></span>
1042
+ </div>
1043
+ `,
1044
+ )}
1045
+ </div>
1046
+ <textarea
1047
+ class="file-viewer-editor file-viewer-editor-overlay"
1048
+ ref=${editorTextareaRef}
1049
+ value=${content}
1050
+ onInput=${handleContentInput}
1051
+ onScroll=${handleEditorScroll}
1052
+ onSelect=${handleEditorSelectionChange}
1053
+ onKeyUp=${handleEditorSelectionChange}
1054
+ onClick=${handleEditorSelectionChange}
1055
+ disabled=${isEditBlocked || isPreviewOnly}
1056
+ readonly=${isEditBlocked || isPreviewOnly}
1057
+ spellcheck=${false}
1058
+ autocorrect="off"
1059
+ autocapitalize="off"
1060
+ autocomplete="off"
1061
+ data-gramm="false"
1062
+ data-gramm_editor="false"
1063
+ data-enable-grammarly="false"
1064
+ wrap="soft"
1065
+ ></textarea>
816
1066
  </div>
1067
+ `
1068
+ : html`
817
1069
  <textarea
818
- class="file-viewer-editor file-viewer-editor-overlay"
1070
+ class="file-viewer-editor"
819
1071
  ref=${editorTextareaRef}
820
1072
  value=${content}
821
1073
  onInput=${handleContentInput}
@@ -823,7 +1075,8 @@ ${formattedValue}</pre
823
1075
  onSelect=${handleEditorSelectionChange}
824
1076
  onKeyUp=${handleEditorSelectionChange}
825
1077
  onClick=${handleEditorSelectionChange}
826
- readonly=${isProtectedLocked || isPreviewOnly}
1078
+ disabled=${isEditBlocked || isPreviewOnly}
1079
+ readonly=${isEditBlocked || isPreviewOnly}
827
1080
  spellcheck=${false}
828
1081
  autocorrect="off"
829
1082
  autocapitalize="off"
@@ -833,32 +1086,10 @@ ${formattedValue}</pre
833
1086
  data-enable-grammarly="false"
834
1087
  wrap="soft"
835
1088
  ></textarea>
836
- </div>
837
- `
838
- : html`
839
- <textarea
840
- class="file-viewer-editor"
841
- ref=${editorTextareaRef}
842
- value=${content}
843
- onInput=${handleContentInput}
844
- onScroll=${handleEditorScroll}
845
- onSelect=${handleEditorSelectionChange}
846
- onKeyUp=${handleEditorSelectionChange}
847
- onClick=${handleEditorSelectionChange}
848
- readonly=${isProtectedLocked || isPreviewOnly}
849
- spellcheck=${false}
850
- autocorrect="off"
851
- autocapitalize="off"
852
- autocomplete="off"
853
- data-gramm="false"
854
- data-gramm_editor="false"
855
- data-enable-grammarly="false"
856
- wrap="soft"
857
- ></textarea>
858
- `}
859
- </div>
860
- `}
861
- `}
1089
+ `}
1090
+ </div>
1091
+ `}
1092
+ `}
862
1093
  </div>
863
1094
  `;
864
1095
  };