From 086c8abaf3563cdc09cc2e32fa4d9987eaeebefb Mon Sep 17 00:00:00 2001 From: Ramachandra Avuthu Date: Tue, 21 Jul 2026 22:42:09 -0400 Subject: [PATCH 1/6] feat(visual diff) diff view to support LOC and MSM file changes in EW --- blocks/canvas/canvas.js | 5 ++ .../editor-utils/prose-merge-conflicts.js | 20 +++++++ .../ew-canvas-header/ew-canvas-header.js | 9 ++++ blocks/canvas/ew-editor-doc/ew-editor-doc.js | 9 ++++ blocks/canvas/ew-editor-doc/prose.js | 21 +++++++- .../ew-editor-wysiwyg/ew-editor-wysiwyg.js | 21 ++++++++ blocks/edit/prose/diff/diff-utils.js | 28 ++++++---- .../prose-merge-conflicts.test.js | 54 +++++++++++++++++++ 8 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 blocks/canvas/editor-utils/prose-merge-conflicts.js create mode 100644 test/unit/blocks/canvas/editor-utils/prose-merge-conflicts.test.js diff --git a/blocks/canvas/canvas.js b/blocks/canvas/canvas.js index 0e7414c53..fc5f5862c 100644 --- a/blocks/canvas/canvas.js +++ b/blocks/canvas/canvas.js @@ -255,6 +255,11 @@ export default async function decorate(block) { syncEditorSplitLayout({ mountRoot, view: header.editorView }); installEditorSplitDrag(mountRoot); + mountRoot.addEventListener('nx-canvas-merge-conflicts', (e) => { + header.hasUnresolvedMergeConflicts = e.detail?.hasMergeConflicts ?? false; + if (header.hasUnresolvedMergeConflicts) header.setEditorView('content'); + }); + mountRoot.addEventListener('nx-editor-undo-state', (e) => { header.undoAvailable = e.detail?.canUndo ?? false; header.redoAvailable = e.detail?.canRedo ?? false; diff --git a/blocks/canvas/editor-utils/prose-merge-conflicts.js b/blocks/canvas/editor-utils/prose-merge-conflicts.js new file mode 100644 index 000000000..b8359fe46 --- /dev/null +++ b/blocks/canvas/editor-utils/prose-merge-conflicts.js @@ -0,0 +1,20 @@ +import { Plugin } from 'da-y-wrapper'; +import { checkForLocNodes } from '../../edit/prose/diff/diff-utils.js'; +import { debounce } from '../../edit/utils/helpers.js'; + +export function createMergeConflictsPlugin(onMergeConflictsChange) { + const debouncedCheck = debounce((view) => { + onMergeConflictsChange?.(checkForLocNodes(view)); + }, 500); + + return new Plugin({ + view(editorView) { + debouncedCheck(editorView); + return { + update(view, prevState) { + if (view.state.doc !== prevState.doc) debouncedCheck(view); + }, + }; + }, + }); +} diff --git a/blocks/canvas/ew-canvas-header/ew-canvas-header.js b/blocks/canvas/ew-canvas-header/ew-canvas-header.js index e45d851d7..0f2f73b5d 100644 --- a/blocks/canvas/ew-canvas-header/ew-canvas-header.js +++ b/blocks/canvas/ew-canvas-header/ew-canvas-header.js @@ -23,6 +23,7 @@ class EWCanvasHeader extends LitElement { undoAvailable: { type: Boolean }, redoAvailable: { type: Boolean }, authorized: { type: Boolean }, + hasUnresolvedMergeConflicts: { type: Boolean }, _chatDisabled: { state: true }, }; @@ -32,6 +33,7 @@ class EWCanvasHeader extends LitElement { this.undoAvailable = false; this.redoAvailable = false; this.authorized = true; + this.hasUnresolvedMergeConflicts = false; } connectedCallback() { @@ -82,7 +84,12 @@ class EWCanvasHeader extends LitElement { ); } + setEditorView(view) { + this._setEditorView(view); + } + _setEditorView(view) { + if (this.hasUnresolvedMergeConflicts && view !== 'content') return; if (!EDITOR_VIEWS.includes(view) || view === this.editorView) return; this.editorView = view; this.dispatchEvent( @@ -130,6 +137,7 @@ class EWCanvasHeader extends LitElement { type="button" class="segment ${this.editorView === 'layout' ? 'is-selected' : ''}" aria-pressed=${this.editorView === 'layout'} + ?disabled=${this.hasUnresolvedMergeConflicts} @click=${() => this._setEditorView('layout')} >Layout diff --git a/blocks/canvas/ew-editor-doc/ew-editor-doc.js b/blocks/canvas/ew-editor-doc/ew-editor-doc.js index 8e9e2ffa2..5eea64fd4 100644 --- a/blocks/canvas/ew-editor-doc/ew-editor-doc.js +++ b/blocks/canvas/ew-editor-doc/ew-editor-doc.js @@ -87,6 +87,14 @@ export class EwEditorDoc extends LitElement { })); } + _emitMergeConflictsState(hasMergeConflicts) { + this.dispatchEvent(new CustomEvent('nx-canvas-merge-conflicts', { + bubbles: true, + composed: true, + detail: { hasMergeConflicts }, + })); + } + _observeUndoManager(mgr) { this._stopObservingUndoManager(); if (!mgr) return; @@ -215,6 +223,7 @@ export class EwEditorDoc extends LitElement { permissions, setEditable: (editable) => this._setEditable(editable), getToken: () => token, + onMergeConflictsChange: (hasConflicts) => this._emitMergeConflictsState(hasConflicts), extraPlugins: [ createExtensionsBridgePlugin(), createTrackingPlugin( diff --git a/blocks/canvas/ew-editor-doc/prose.js b/blocks/canvas/ew-editor-doc/prose.js index f642b6d83..16c29f5f8 100644 --- a/blocks/canvas/ew-editor-doc/prose.js +++ b/blocks/canvas/ew-editor-doc/prose.js @@ -41,6 +41,9 @@ import { getNx } from '../../../scripts/utils.js'; import { getAuthToken } from '../../shared/utils.js'; import { generateColor, getCollabIdentity } from './utils/collab.js'; import { checkBlockLibraryConfigured } from '../editor-utils/block-slash.js'; +import { getDiffClass } from '../../edit/prose/diff/diff-utils.js'; +import { initDaMetadata } from '../../edit/utils/helpers.js'; +import { createMergeConflictsPlugin } from '../editor-utils/prose-merge-conflicts.js'; const { DA_ADMIN, DA_COLLAB, hashChange } = await import(`${getNx()}/utils/utils.js`); @@ -82,7 +85,7 @@ function checkLibraryConfiguredOnSync(wsProvider, canWrite) { export default async function initProse({ path, permissions, setEditable, getToken, - extraPlugins = [], + extraPlugins = [], onMergeConflictsChange, }) { const editor = document.createElement('div'); editor.className = 'da-prose-mirror'; @@ -211,7 +214,7 @@ export default async function initProse({ if (canWrite) { plugins.unshift(createSlashMenuPlugin(), createSelectionToolbarPlugin()); - plugins.push(imageFocalPoint()); + plugins.push(imageFocalPoint(), createMergeConflictsPlugin(onMergeConflictsChange)); } let state = EditorState.create({ schema, plugins }); @@ -219,11 +222,25 @@ export default async function initProse({ const fix = fixTables(state); if (fix) state = state.apply(fix.setMeta('addToHistory', false)); + const diffNodeViews = canWrite ? { + diff_added(node, view, getPos) { + const MergeConflictsAddedView = getDiffClass('da-diff-added', getSchema, () => {}, { isUpstream: false }); + return new MergeConflictsAddedView(node, view, getPos); + }, + diff_deleted(node, view, getPos) { + const MergeConflictsDeletedView = getDiffClass('da-diff-deleted', getSchema, () => {}, { isUpstream: true }); + return new MergeConflictsDeletedView(node, view, getPos); + }, + } : undefined; + viewRef = new EditorView(editor, { state, editable() { return canWrite; }, + nodeViews: diffNodeViews, }); + initDaMetadata(ydoc.getMap('daMetadata')); + const undoManager = yUndoPluginKey.getState(viewRef.state)?.undoManager ?? null; return { proseEl: editor, wsProvider, view: viewRef, ydoc, undoManager }; diff --git a/blocks/canvas/ew-editor-wysiwyg/ew-editor-wysiwyg.js b/blocks/canvas/ew-editor-wysiwyg/ew-editor-wysiwyg.js index 930c942d2..45fccaf64 100644 --- a/blocks/canvas/ew-editor-wysiwyg/ew-editor-wysiwyg.js +++ b/blocks/canvas/ew-editor-wysiwyg/ew-editor-wysiwyg.js @@ -56,15 +56,35 @@ export class EwEditorWysiwyg extends LitElement { this._syncCanvasVisibility(); }; this.parentElement?.addEventListener('nx-canvas-editor-active', this._onCanvasEditorActive); + this._onMergeConflictsChange = (e) => { + const hasMergeConflicts = e.detail?.hasMergeConflicts ?? false; + if (hasMergeConflicts) { + this._hadMergeConflicts = true; + } else if (this._hadMergeConflicts) { + this._hadMergeConflicts = false; + this._reloadIframe(); + } + }; + this.parentElement?.addEventListener('nx-canvas-merge-conflicts', this._onMergeConflictsChange); this._syncCanvasVisibility(); } disconnectedCallback() { this.parentElement?.removeEventListener('nx-canvas-editor-active', this._onCanvasEditorActive); + this.parentElement?.removeEventListener('nx-canvas-merge-conflicts', this._onMergeConflictsChange); this._clearQuickEditRetry(); super.disconnectedCallback(); } + _reloadIframe() { + const iframe = this.shadowRoot?.querySelector('iframe'); + try { + iframe?.contentWindow?.location.reload(); + } catch { + /* ignore — cross-origin reload should be allowed, but don't let this throw */ + } + } + get _iframeSrc() { const { org, repo, path } = this.ctx ?? {}; if (!org || !repo || !path || !this._cookieReady) return null; @@ -105,6 +125,7 @@ export class EwEditorWysiwyg extends LitElement { _resetCookieStateForCtxChange() { this._clearQuickEditRetry(); this._cookieReady = false; + this._hadMergeConflicts = false; } updated(changed) { diff --git a/blocks/edit/prose/diff/diff-utils.js b/blocks/edit/prose/diff/diff-utils.js index 603002ce2..70a425ed2 100644 --- a/blocks/edit/prose/diff/diff-utils.js +++ b/blocks/edit/prose/diff/diff-utils.js @@ -78,23 +78,29 @@ function setDiffLabelCssVars(daEditor) { host.style.setProperty('--diff-label-upstream', `'${labels.upstream}'`); } +function getDiffHost(view) { + const classicHost = document.querySelector('da-content')?.shadowRoot + ?.querySelector('da-editor'); + if (classicHost) return classicHost; + + const root = view?.dom?.getRootNode?.(); + return root instanceof ShadowRoot ? root.host : null; +} + let locCssLoading = false; -async function loadLocCss() { +async function loadLocCss(hostEl) { if (locCssLoading) return; locCssLoading = true; try { const locSheet = await getSheet('/blocks/edit/prose/diff/diff-utils.css'); - const daEditor = document.querySelector('da-content')?.shadowRoot - ?.querySelector('da-editor'); - - if (daEditor?.shadowRoot) { - const existingSheets = daEditor.shadowRoot.adoptedStyleSheets || []; - daEditor.shadowRoot.adoptedStyleSheets = [...existingSheets, locSheet]; + if (hostEl?.shadowRoot) { + const existingSheets = hostEl.shadowRoot.adoptedStyleSheets || []; + hostEl.shadowRoot.adoptedStyleSheets = [...existingSheets, locSheet]; // Set CSS custom properties for diff labels - setDiffLabelCssVars(daEditor); + setDiffLabelCssVars(hostEl); } } catch (error) { // eslint-disable-next-line no-console @@ -192,7 +198,7 @@ export function checkForLocNodes(view) { .some((node) => isLocNode(node) || hasListLocNode(node)); if (hasLocNodes) { - loadLocCss(); + loadLocCss(getDiffHost(view)); showGlobalDialog(view); } else { hideGlobalDialog(); @@ -323,7 +329,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea } renderTabbedInterface(nodeA, view, posA, nodeB) { - loadLocCss(); + loadLocCss(getDiffHost(view)); this.dom = createElement('div', 'loc-tabbed-container', { contentEditable: 'false' }); this.contentDOM = null; // Don't let ProseMirror manage content @@ -430,7 +436,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea } renderSingleNode(node, view, pos, upstream) { - loadLocCss(); + loadLocCss(getDiffHost(view)); const isDeleted = node.type.name === 'diff_deleted'; const viewClass = isDeleted ? 'loc-deleted-view' : 'loc-added-view'; diff --git a/test/unit/blocks/canvas/editor-utils/prose-merge-conflicts.test.js b/test/unit/blocks/canvas/editor-utils/prose-merge-conflicts.test.js new file mode 100644 index 000000000..a462fb877 --- /dev/null +++ b/test/unit/blocks/canvas/editor-utils/prose-merge-conflicts.test.js @@ -0,0 +1,54 @@ +import { expect } from '@esm-bundle/chai'; +import { createMergeConflictsPlugin } from '../../../../../blocks/canvas/editor-utils/prose-merge-conflicts.js'; +import { createTestEditor, destroyEditor } from '../../edit/prose/test-helpers.js'; + +const waitFor = (ms) => new Promise((resolve) => { setTimeout(resolve, ms); }); + +describe('createMergeConflictsPlugin', () => { + let editor; + + afterEach(() => { + if (editor) destroyEditor(editor); + editor = undefined; + }); + + it('reports false shortly after mount when the doc has no merge-conflict nodes', async () => { + const calls = []; + const plugin = createMergeConflictsPlugin((hasMergeConflicts) => calls.push(hasMergeConflicts)); + editor = await createTestEditor({ additionalPlugins: [plugin] }); + + await waitFor(600); + + expect(calls).to.deep.equal([false]); + }); + + it('reports true once a diff_added node is inserted', async () => { + const calls = []; + const plugin = createMergeConflictsPlugin((hasMergeConflicts) => calls.push(hasMergeConflicts)); + editor = await createTestEditor({ additionalPlugins: [plugin] }); + + const { schema } = editor.view.state; + const para = schema.nodes.paragraph.create(null, schema.text('new')); + const diffAdded = schema.nodes.diff_added.create({}, para); + const tr = editor.view.state.tr.insert(editor.view.state.doc.content.size, diffAdded); + editor.view.dispatch(tr); + + await waitFor(600); + + expect(calls[calls.length - 1]).to.equal(true); + }); + + it('does not re-check on transactions that leave the doc unchanged', async () => { + const calls = []; + const plugin = createMergeConflictsPlugin((hasMergeConflicts) => calls.push(hasMergeConflicts)); + editor = await createTestEditor({ additionalPlugins: [plugin] }); + + await waitFor(600); // let the initial mount check land + calls.length = 0; + + editor.view.dispatch(editor.view.state.tr); // no doc change + await waitFor(600); + + expect(calls).to.deep.equal([]); + }); +}); From 95463642ea77a852937d40928216ca9b96b8d0b3 Mon Sep 17 00:00:00 2001 From: Ramachandra Avuthu Date: Thu, 23 Jul 2026 12:19:23 -0400 Subject: [PATCH 2/6] match with ew visuals --- .../ew-editor-doc/ew-editor-doc-diff.css | 172 ++++++++++++++++++ blocks/canvas/ew-editor-doc/prose.js | 4 +- blocks/canvas/ew-editor-doc/utils/teardown.js | 3 + blocks/edit/prose/diff/diff-utils.js | 14 +- 4 files changed, 190 insertions(+), 3 deletions(-) create mode 100644 blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css diff --git a/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css b/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css new file mode 100644 index 000000000..8438c96a5 --- /dev/null +++ b/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css @@ -0,0 +1,172 @@ +/* stylelint-disable selector-class-pattern */ + +/* + * Canvas-only visual overrides for the shared regional-edits diff UI + * (blocks/edit/prose/diff/*). Adopted in addition to diff-utils.css, only + * when the diff host is canvas's — classic editor keeps its + * original look untouched. Reuses EW's --s2-* tokens. + * + * EW's own chrome is mostly monotone gray, with color reserved for + * content-level meaning rather than button fills — so button chrome here is + * neutral gray throughout, and the green=local(added)/red=upstream(removed) + * split (matching ew-canvas-versions/ew-canvas-compare.css's ins/del + * treatment) is used only for the text/overlay rules further down, where it + * actually conveys which content is which. + */ + +.loc-tabbed-container { + background: var(--s2-gray-25); + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); + border-radius: var(--s2-corner-radius-400); +} + +.diff-tab-content { + background: var(--s2-gray-25); +} + +.da-diff-btn-base { + border-radius: var(--s2-corner-radius-400); + height: 32px; +} + +.da-diff-btn-base-element { + font-size: var(--s2-component-s-regular-font-size, 0.875rem); + font-weight: 600; + + &:focus-visible { + outline: 2px solid var(--s2-blue-800); + outline-offset: 2px; + } +} + +/* Checkmark / close icons, replacing the "✓" / "✕" text glyphs */ +.da-diff-btn .confirm-btn::before, +.da-diff-btn-3part .diff-accept::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; + background-color: currentcolor; + mask: no-repeat center / 16px url('/img/icons/s2-icon-checkmark-20-n.svg'); +} + +.da-diff-btn-3part .diff-delete::before { + content: ''; + display: inline-block; + width: 16px; + height: 16px; + background-color: currentcolor; + mask: no-repeat center / 16px url('/img/icons/s2-icon-close-20-n.svg'); +} + +/* Neutral button chrome — EW's own chrome (headers, panels, toolbars) is + almost entirely gray, with color reserved for content-level meaning (see + the ins/del + overlay rules below), not button fills. So Local/Upstream/ + Difference all get the same treatment; the label text + icon already say + what each button does. + The base stylesheet's per-variant rules (e.g. .da-diff-btn.is-local + .switch-btn) are 3 classes deep — :is(...) here matches that specificity + so this actually wins the cascade instead of silently losing to it. */ +.da-diff-btn:is(.is-local, .is-upstream, .is-diff) .switch-btn, +.da-diff-btn-3part:is(.is-local, .is-upstream) .diff-label { + background: var(--s2-gray-100); + color: var(--s2-gray-800); +} + +.da-diff-btn:is(.is-local, .is-upstream, .is-diff) .confirm-btn, +.da-diff-btn-3part:is(.is-local, .is-upstream) .diff-accept, +.da-diff-btn-3part:is(.is-local, .is-upstream) .diff-delete { + background: var(--s2-gray-800); + color: var(--s2-gray-25); +} + +.da-diff-btn .confirm-btn:hover, +.da-diff-btn-3part .diff-accept:hover, +.da-diff-btn-3part .diff-delete:hover { + color: var(--s2-gray-25); + filter: brightness(1.1); +} + +.da-diff-btn.is-active { + border-color: var(--s2-gray-900); +} + +.da-diff-btn.is-active .switch-btn, +.da-diff-btn.is-active .confirm-btn { + color: var(--s2-gray-25); +} + +/* Flatter tooltip, matching EW's chrome instead of the dark-gradient default */ +.diff-tooltip { + background: var(--s2-gray-1000); + color: var(--s2-gray-25); + font-weight: 500; + border: none; + border-radius: var(--s2-corner-radius-400); + box-shadow: 0 2px 6px rgb(0 0 0 / 15%); + backdrop-filter: none; +} + +.diff-tooltip::before { + display: none; +} + +.diff-tooltip::after { + border-top-color: var(--s2-gray-1000); + filter: none; +} + +/* "Difference" tab text-level highlighting, matching ew-canvas-compare's ins/del treatment */ +.diff-tab-pane del.diffdel, +.diff-tab-pane del.diffmod { + background-color: var(--s2-red-200); + color: var(--s2-red-900); + border: none; +} + +.diff-tab-pane ins.diffins, +.diff-tab-pane ins.diffmod { + background-color: var(--s2-green-200); + color: var(--s2-green-900); + border: none; +} + +/* Single-node floating overlay: colors are set inline by diff-utils.js + (renderSingleNode), so !important is required to win over that. The + distinguishing class (loc-regional/loc-langstore) is only added once the + real overlay finishes its async load; until then the inline color briefly + shows through, unchanged. + These sit on top of the actual diff content (position: absolute, covering + the node), so the fill must stay translucent — same reason the original + inline colors were hex+alpha (#902ade33 etc.) rather than solid. */ +.loc-color-overlay.loc-regional { + background-color: color-mix(in srgb, var(--s2-green-200) 60%, transparent) !important; +} + +.loc-color-overlay.loc-langstore { + background-color: color-mix(in srgb, var(--s2-red-200) 60%, transparent) !important; +} + +.diff-bg-local { + background-color: color-mix(in srgb, var(--s2-green-200) 60%, transparent); +} + +.diff-bg-upstream { + background-color: color-mix(in srgb, var(--s2-red-200) 60%, transparent); +} + +.diff-bg-diff { + background-color: color-mix(in srgb, var(--s2-gray-100) 60%, transparent); +} + +/* Global "Keep All Local / Keep All Upstream" bar */ +.da-regional-edits-overlay { + padding: var(--s2-spacing-200); + border-radius: var(--s2-corner-radius-400); + background: var(--s2-gray-25); + box-shadow: 0 2px 8px rgb(0 0 0 / 8%); +} + +.da-regional-edits-actions { + gap: var(--s2-spacing-200); +} diff --git a/blocks/canvas/ew-editor-doc/prose.js b/blocks/canvas/ew-editor-doc/prose.js index 16c29f5f8..bdc5d3daa 100644 --- a/blocks/canvas/ew-editor-doc/prose.js +++ b/blocks/canvas/ew-editor-doc/prose.js @@ -41,7 +41,7 @@ import { getNx } from '../../../scripts/utils.js'; import { getAuthToken } from '../../shared/utils.js'; import { generateColor, getCollabIdentity } from './utils/collab.js'; import { checkBlockLibraryConfigured } from '../editor-utils/block-slash.js'; -import { getDiffClass } from '../../edit/prose/diff/diff-utils.js'; +import { getDiffClass, addActiveView } from '../../edit/prose/diff/diff-utils.js'; import { initDaMetadata } from '../../edit/utils/helpers.js'; import { createMergeConflictsPlugin } from '../editor-utils/prose-merge-conflicts.js'; @@ -239,6 +239,8 @@ export default async function initProse({ nodeViews: diffNodeViews, }); + if (canWrite) addActiveView(viewRef); + initDaMetadata(ydoc.getMap('daMetadata')); const undoManager = yUndoPluginKey.getState(viewRef.state)?.undoManager ?? null; diff --git a/blocks/canvas/ew-editor-doc/utils/teardown.js b/blocks/canvas/ew-editor-doc/utils/teardown.js index a066b17ca..98c59279e 100644 --- a/blocks/canvas/ew-editor-doc/utils/teardown.js +++ b/blocks/canvas/ew-editor-doc/utils/teardown.js @@ -1,3 +1,5 @@ +import { removeActiveView } from '../../../edit/prose/diff/diff-utils.js'; + export function teardownEditorDocResources({ clearPortHandler, awarenessOff, @@ -14,6 +16,7 @@ export function teardownEditorDocResources({ wsProvider.disconnect({ data: 'unmount' }); } if (view) { + removeActiveView(view); view.destroy(); } if (proseEl?.parentNode) { diff --git a/blocks/edit/prose/diff/diff-utils.js b/blocks/edit/prose/diff/diff-utils.js index 70a425ed2..eccabcf89 100644 --- a/blocks/edit/prose/diff/diff-utils.js +++ b/blocks/edit/prose/diff/diff-utils.js @@ -96,8 +96,14 @@ async function loadLocCss(hostEl) { const locSheet = await getSheet('/blocks/edit/prose/diff/diff-utils.css'); if (hostEl?.shadowRoot) { - const existingSheets = hostEl.shadowRoot.adoptedStyleSheets || []; - hostEl.shadowRoot.adoptedStyleSheets = [...existingSheets, locSheet]; + const sheets = [...(hostEl.shadowRoot.adoptedStyleSheets || []), locSheet]; + + if (hostEl.tagName === 'EW-EDITOR-DOC') { + const ewSheet = await getSheet('/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css'); + sheets.push(ewSheet); + } + + hostEl.shadowRoot.adoptedStyleSheets = sheets; // Set CSS custom properties for diff labels setDiffLabelCssVars(hostEl); @@ -282,6 +288,10 @@ export function addActiveView(view) { activeViews.add(view); } +export function removeActiveView(view) { + activeViews.delete(view); +} + export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstream } = {}) { return class { constructor(node, view, getPos) { From 5964f710966e26a85f343a32e32c63c0fbc8541a Mon Sep 17 00:00:00 2001 From: Ramachandra Avuthu Date: Fri, 24 Jul 2026 15:33:18 -0400 Subject: [PATCH 3/6] ew diff styles updated --- blocks/canvas/canvas.css | 5 ++ .../ew-canvas-header/ew-canvas-header.css | 14 ++++++ .../ew-canvas-header/ew-canvas-header.js | 5 +- .../ew-editor-doc/ew-editor-doc-diff.css | 48 ++++++++++++------- blocks/edit/prose/diff/diff-global-dialog.js | 5 +- blocks/edit/prose/diff/diff-utils.css | 25 +++++----- blocks/edit/prose/diff/diff-utils.js | 1 + 7 files changed, 68 insertions(+), 35 deletions(-) diff --git a/blocks/canvas/canvas.css b/blocks/canvas/canvas.css index ced573f10..04765afe7 100644 --- a/blocks/canvas/canvas.css +++ b/blocks/canvas/canvas.css @@ -14,6 +14,11 @@ html:has(aside.panel[data-position="after"]:not([hidden])) ew-canvas-header::par display: none; } +html:has(ew-canvas-header[has-conflicts]) aside.panel[data-position="before"] { + pointer-events: none; + opacity: 0.45; +} + .fragment-content:has(nx-chat) { height: 100%; diff --git a/blocks/canvas/ew-canvas-header/ew-canvas-header.css b/blocks/canvas/ew-canvas-header/ew-canvas-header.css index fd3e6b5c7..db4459723 100644 --- a/blocks/canvas/ew-canvas-header/ew-canvas-header.css +++ b/blocks/canvas/ew-canvas-header/ew-canvas-header.css @@ -92,6 +92,16 @@ outline: none; } + &:disabled { + color: var(--s2-gray-400); + cursor: not-allowed; + + &:hover { + color: var(--s2-gray-400); + background-color: transparent; + } + } + &:hover:not(.is-selected) { color: var(--s2-gray-900); background-color: var(--s2-gray-200); @@ -118,6 +128,10 @@ flex-shrink: 0; overflow: hidden; } + + &:disabled.segment-icon svg.icon { + opacity: 0.45; + } } .bar { diff --git a/blocks/canvas/ew-canvas-header/ew-canvas-header.js b/blocks/canvas/ew-canvas-header/ew-canvas-header.js index 8b4f13e9d..a64a1e510 100644 --- a/blocks/canvas/ew-canvas-header/ew-canvas-header.js +++ b/blocks/canvas/ew-canvas-header/ew-canvas-header.js @@ -24,7 +24,7 @@ class EWCanvasHeader extends LitElement { undoAvailable: { type: Boolean }, redoAvailable: { type: Boolean }, authorized: { type: Boolean }, - hasUnresolvedMergeConflicts: { type: Boolean }, + hasUnresolvedMergeConflicts: { type: Boolean, reflect: true, attribute: 'has-conflicts' }, _chatDisabled: { state: true }, }; @@ -134,6 +134,7 @@ class EWCanvasHeader extends LitElement { type="button" class="segment ${this.editorView === 'layout' ? 'is-selected' : ''}" aria-pressed=${this.editorView === 'layout'} + title=${this.hasUnresolvedMergeConflicts ? 'Resolve conflicts to switch views' : nothing} ?disabled=${this.hasUnresolvedMergeConflicts} @click=${() => this._setEditorView('layout')} >Layout @@ -148,7 +149,7 @@ class EWCanvasHeader extends LitElement { class="segment segment-icon ${this.editorView === 'split' ? 'is-selected' : ''}" aria-pressed=${this.editorView === 'split'} aria-label="Split view" - title="Split view" + title=${this.hasUnresolvedMergeConflicts ? 'Resolve conflicts to switch views' : 'Split view'} ?disabled=${this.hasUnresolvedMergeConflicts} @click=${() => this._setEditorView('split')} >${this._renderIcon('gridCompare')} diff --git a/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css b/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css index e6ca182b2..11e6f83f8 100644 --- a/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css +++ b/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css @@ -12,7 +12,13 @@ .da-diff-btn-base { border-radius: var(--s2-corner-radius-400); - height: 32px; + height: 28px; + background: var(--s2-gray-100); +} + +.da-diff-btn .switch-btn, +.da-diff-btn-3part .diff-label { + padding: 4px 10px; } .da-diff-btn-base-element { @@ -46,22 +52,16 @@ .da-diff-btn:is(.is-local, .is-upstream, .is-diff) .switch-btn, .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-label { - background: var(--s2-gray-100); + background: transparent; color: var(--s2-gray-800); } .da-diff-btn:is(.is-local, .is-upstream, .is-diff) .confirm-btn, .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-accept, .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-delete { - background: var(--s2-gray-800); - color: var(--s2-gray-25); -} - -.da-diff-btn .confirm-btn:hover, -.da-diff-btn-3part .diff-accept:hover, -.da-diff-btn-3part .diff-delete:hover { - color: var(--s2-gray-25); - filter: brightness(1.1); + background: var(--s2-gray-200); + color: var(--s2-gray-800); + border-left: 1px solid var(--s2-gray-300); } .da-diff-btn.is-active { @@ -70,7 +70,20 @@ .da-diff-btn.is-active .switch-btn, .da-diff-btn.is-active .confirm-btn { - color: var(--s2-gray-25); + background: var(--s2-gray-300); + color: var(--s2-gray-900); +} + + +.da-diff-btn .switch-btn { + cursor: default; +} + +.da-diff-btn .confirm-btn:hover, +.da-diff-btn-3part .diff-accept:hover, +.da-diff-btn-3part .diff-delete:hover { + background: var(--s2-gray-300); + color: var(--s2-gray-900); } .diff-tooltip { @@ -107,28 +120,27 @@ } .loc-color-overlay.loc-regional { - background-color: color-mix(in srgb, var(--s2-green-200) 60%, transparent) !important; + background-color: color-mix(in srgb, var(--s2-green-200) 25%, transparent) !important; } .loc-color-overlay.loc-langstore { - background-color: color-mix(in srgb, var(--s2-red-200) 60%, transparent) !important; + background-color: color-mix(in srgb, var(--s2-red-200) 25%, transparent) !important; } .diff-bg-local { - background-color: color-mix(in srgb, var(--s2-green-200) 60%, transparent); + background-color: color-mix(in srgb, var(--s2-green-200) 25%, transparent); } .diff-bg-upstream { - background-color: color-mix(in srgb, var(--s2-red-200) 60%, transparent); + background-color: color-mix(in srgb, var(--s2-red-200) 25%, transparent); } .diff-bg-diff { - background-color: color-mix(in srgb, var(--s2-gray-100) 60%, transparent); + background-color: color-mix(in srgb, var(--s2-gray-100) 25%, transparent); } .da-regional-edits-overlay { padding: var(--s2-spacing-200); - border-radius: var(--s2-corner-radius-400); background: var(--s2-gray-25); box-shadow: 0 2px 8px rgb(0 0 0 / 8%); } diff --git a/blocks/edit/prose/diff/diff-global-dialog.js b/blocks/edit/prose/diff/diff-global-dialog.js index d30b66ed5..4b265b795 100644 --- a/blocks/edit/prose/diff/diff-global-dialog.js +++ b/blocks/edit/prose/diff/diff-global-dialog.js @@ -149,15 +149,14 @@ function createGlobalAction( function createGlobalOverlay(activeViews, isLocNode) { const dialog = createElement('div', 'da-regional-edits-overlay'); - const actionsContainer = createElement('div', 'da-regional-edits-actions'); - const labels = getDiffLabels(); + const actionsContainer = createElement('div', 'da-regional-edits-actions'); const localButton = createGlobalAction('local', `Keep All ${labels.local}`, KEEP_LOCAL, `Accept All ${labels.local}`, activeViews, isLocNode); const upstreamButton = createGlobalAction('upstream', `Keep All ${labels.upstream}`, KEEP_UPSTREAM, `Accept All ${labels.upstream}`, activeViews, isLocNode); - actionsContainer.appendChild(localButton); actionsContainer.appendChild(upstreamButton); + dialog.appendChild(actionsContainer); return dialog; diff --git a/blocks/edit/prose/diff/diff-utils.css b/blocks/edit/prose/diff/diff-utils.css index 43fd34733..778bb9e8a 100644 --- a/blocks/edit/prose/diff/diff-utils.css +++ b/blocks/edit/prose/diff/diff-utils.css @@ -84,6 +84,10 @@ da-loc-added, pointer-events: auto; } +.da-prose-mirror.has-regional-edits .loc-sticky-buttons { + top: 64px; +} + /* When the container is short, the sticky buttons should be moved down to not block content */ .loc-single-container.is-short .loc-sticky-buttons, .loc-tabbed-container.is-short .loc-sticky-buttons, @@ -404,27 +408,24 @@ da-loc-added, .da-regional-edits-overlay { display: none; - position: absolute; - right: -48px; - z-index: 4; - padding: 16px 20px; - margin: 0 48px; + position: sticky; + top: 0; + z-index: 1111; + padding: 12px 20px; box-sizing: border-box; + width: 100%; + background: #fff; + box-shadow: 0 2px 4px rgb(0 0 0 / 10%); } .da-regional-edits-overlay.show { display: flex; - align-items: center; - justify-content: space-between; - gap: 20px; + justify-content: flex-end; } .da-regional-edits-actions { display: flex; gap: 12px; align-items: center; -} - -.da-prose-mirror.has-regional-edits .ProseMirror { - padding-top: 60px; + justify-content: flex-end; } diff --git a/blocks/edit/prose/diff/diff-utils.js b/blocks/edit/prose/diff/diff-utils.js index eccabcf89..026c45ece 100644 --- a/blocks/edit/prose/diff/diff-utils.js +++ b/blocks/edit/prose/diff/diff-utils.js @@ -384,6 +384,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea colorOverlay.style.display = 'block'; colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-upstream'; } else if (targetTab === 'diff') { + colorOverlay.style.display = 'none'; colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-diff'; const diffTab = tabContent.querySelector('[data-tab="diff"]'); From dffb74d831ea5ef0232f4e821c792c34bf062add Mon Sep 17 00:00:00 2001 From: Ramachandra Avuthu Date: Thu, 30 Jul 2026 09:36:48 -0400 Subject: [PATCH 4/6] undo global dialog changes --- blocks/edit/prose/diff/diff-global-dialog.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/blocks/edit/prose/diff/diff-global-dialog.js b/blocks/edit/prose/diff/diff-global-dialog.js index 4b265b795..d30b66ed5 100644 --- a/blocks/edit/prose/diff/diff-global-dialog.js +++ b/blocks/edit/prose/diff/diff-global-dialog.js @@ -149,14 +149,15 @@ function createGlobalAction( function createGlobalOverlay(activeViews, isLocNode) { const dialog = createElement('div', 'da-regional-edits-overlay'); + const actionsContainer = createElement('div', 'da-regional-edits-actions'); + const labels = getDiffLabels(); - const actionsContainer = createElement('div', 'da-regional-edits-actions'); const localButton = createGlobalAction('local', `Keep All ${labels.local}`, KEEP_LOCAL, `Accept All ${labels.local}`, activeViews, isLocNode); const upstreamButton = createGlobalAction('upstream', `Keep All ${labels.upstream}`, KEEP_UPSTREAM, `Accept All ${labels.upstream}`, activeViews, isLocNode); + actionsContainer.appendChild(localButton); actionsContainer.appendChild(upstreamButton); - dialog.appendChild(actionsContainer); return dialog; From b7512d77f5727fe134b070bac3a1d58d39c84aa2 Mon Sep 17 00:00:00 2001 From: Ramachandra Avuthu Date: Thu, 6 Aug 2026 15:15:16 -0400 Subject: [PATCH 5/6] review comments addressed --- .../ew-canvas-header/ew-canvas-header.js | 10 +-- .../ew-editor-doc/ew-editor-doc-diff.css | 87 ++++++++++--------- blocks/canvas/ew-editor-doc/ew-editor-doc.js | 7 +- blocks/edit/prose/diff/diff-actions.js | 2 +- blocks/edit/prose/diff/diff-global-dialog.js | 2 +- blocks/edit/prose/diff/diff-overlay-ui.js | 4 +- blocks/edit/prose/diff/diff-utils.css | 8 ++ blocks/edit/prose/diff/diff-utils.js | 18 ++-- 8 files changed, 71 insertions(+), 67 deletions(-) diff --git a/blocks/canvas/ew-canvas-header/ew-canvas-header.js b/blocks/canvas/ew-canvas-header/ew-canvas-header.js index 4222b9e62..2715f6ef0 100644 --- a/blocks/canvas/ew-canvas-header/ew-canvas-header.js +++ b/blocks/canvas/ew-canvas-header/ew-canvas-header.js @@ -84,10 +84,6 @@ class EWCanvasHeader extends LitElement { } setEditorView(view) { - this._setEditorView(view); - } - - _setEditorView(view) { if (this.hasUnresolvedMergeConflicts && view !== 'content') return; if (!EDITOR_VIEWS.includes(view) || view === this.editorView) return; this.editorView = view; @@ -143,13 +139,13 @@ class EWCanvasHeader extends LitElement { aria-pressed=${this.editorView === 'layout'} title=${this.hasUnresolvedMergeConflicts ? 'Resolve conflicts to switch views' : nothing} ?disabled=${this.hasUnresolvedMergeConflicts} - @click=${() => this._setEditorView('layout')} + @click=${() => this.setEditorView('layout')} >Layout ` : nothing} diff --git a/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css b/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css index 11e6f83f8..e1d98e060 100644 --- a/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css +++ b/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css @@ -1,27 +1,31 @@ /* stylelint-disable selector-class-pattern */ -.loc-tabbed-container { +.ew-editor-doc-mount .loc-tabbed-container { background: var(--s2-gray-25); box-shadow: 0 2px 8px rgb(0 0 0 / 8%); border-radius: var(--s2-corner-radius-400); } -.diff-tab-content { +.ew-editor-doc-mount .diff-tab-content { background: var(--s2-gray-25); } -.da-diff-btn-base { +.ew-editor-doc-mount .da-diff-btn-base { border-radius: var(--s2-corner-radius-400); height: 28px; background: var(--s2-gray-100); } -.da-diff-btn .switch-btn, -.da-diff-btn-3part .diff-label { +.ew-editor-doc-mount .da-diff-btn .switch-btn, +.ew-editor-doc-mount .da-diff-btn-3part .diff-label { padding: 4px 10px; } -.da-diff-btn-base-element { +.ew-editor-doc-mount .da-diff-btn .switch-btn { + cursor: default; +} + +.ew-editor-doc-mount .da-diff-btn-base-element { font-size: var(--s2-component-s-regular-font-size, 0.875rem); font-weight: 600; @@ -31,8 +35,14 @@ } } -.da-diff-btn .confirm-btn::before, -.da-diff-btn-3part .diff-accept::before { +.ew-editor-doc-mount .da-diff-btn .confirm-btn, +.ew-editor-doc-mount .da-diff-btn-3part .diff-accept, +.ew-editor-doc-mount .da-diff-btn-3part .diff-delete { + padding: 0; +} + +.ew-editor-doc-mount .da-diff-btn .confirm-btn::before, +.ew-editor-doc-mount .da-diff-btn-3part .diff-accept::before { content: ''; display: inline-block; width: 16px; @@ -41,7 +51,7 @@ mask: no-repeat center / 16px url('/img/icons/s2-icon-checkmark-20-n.svg'); } -.da-diff-btn-3part .diff-delete::before { +.ew-editor-doc-mount .da-diff-btn-3part .diff-delete::before { content: ''; display: inline-block; width: 16px; @@ -50,43 +60,38 @@ mask: no-repeat center / 16px url('/img/icons/s2-icon-close-20-n.svg'); } -.da-diff-btn:is(.is-local, .is-upstream, .is-diff) .switch-btn, -.da-diff-btn-3part:is(.is-local, .is-upstream) .diff-label { +.ew-editor-doc-mount .da-diff-btn:is(.is-local, .is-upstream, .is-diff) .switch-btn, +.ew-editor-doc-mount .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-label { background: transparent; color: var(--s2-gray-800); } -.da-diff-btn:is(.is-local, .is-upstream, .is-diff) .confirm-btn, -.da-diff-btn-3part:is(.is-local, .is-upstream) .diff-accept, -.da-diff-btn-3part:is(.is-local, .is-upstream) .diff-delete { +.ew-editor-doc-mount .da-diff-btn:is(.is-local, .is-upstream, .is-diff) .confirm-btn, +.ew-editor-doc-mount .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-accept, +.ew-editor-doc-mount .da-diff-btn-3part:is(.is-local, .is-upstream) .diff-delete { background: var(--s2-gray-200); color: var(--s2-gray-800); border-left: 1px solid var(--s2-gray-300); } -.da-diff-btn.is-active { +.ew-editor-doc-mount .da-diff-btn.is-active { border-color: var(--s2-gray-900); } -.da-diff-btn.is-active .switch-btn, -.da-diff-btn.is-active .confirm-btn { +.ew-editor-doc-mount .da-diff-btn.is-active .switch-btn, +.ew-editor-doc-mount .da-diff-btn.is-active .confirm-btn { background: var(--s2-gray-300); color: var(--s2-gray-900); } - -.da-diff-btn .switch-btn { - cursor: default; -} - -.da-diff-btn .confirm-btn:hover, -.da-diff-btn-3part .diff-accept:hover, -.da-diff-btn-3part .diff-delete:hover { +.ew-editor-doc-mount .da-diff-btn .confirm-btn:hover, +.ew-editor-doc-mount .da-diff-btn-3part .diff-accept:hover, +.ew-editor-doc-mount .da-diff-btn-3part .diff-delete:hover { background: var(--s2-gray-300); color: var(--s2-gray-900); } -.diff-tooltip { +.ew-editor-doc-mount .diff-tooltip { background: var(--s2-gray-1000); color: var(--s2-gray-25); font-weight: 500; @@ -96,55 +101,55 @@ backdrop-filter: none; } -.diff-tooltip::before { +.ew-editor-doc-mount .diff-tooltip::before { display: none; } -.diff-tooltip::after { +.ew-editor-doc-mount .diff-tooltip::after { border-top-color: var(--s2-gray-1000); filter: none; } -.diff-tab-pane del.diffdel, -.diff-tab-pane del.diffmod { +.ew-editor-doc-mount .diff-tab-pane del.diffdel, +.ew-editor-doc-mount .diff-tab-pane del.diffmod { background-color: var(--s2-red-200); color: var(--s2-red-900); border: none; } -.diff-tab-pane ins.diffins, -.diff-tab-pane ins.diffmod { +.ew-editor-doc-mount .diff-tab-pane ins.diffins, +.ew-editor-doc-mount .diff-tab-pane ins.diffmod { background-color: var(--s2-green-200); color: var(--s2-green-900); border: none; } -.loc-color-overlay.loc-regional { - background-color: color-mix(in srgb, var(--s2-green-200) 25%, transparent) !important; +.ew-editor-doc-mount .loc-color-overlay.loc-regional { + background-color: color-mix(in srgb, var(--s2-green-200) 25%, transparent); } -.loc-color-overlay.loc-langstore { - background-color: color-mix(in srgb, var(--s2-red-200) 25%, transparent) !important; +.ew-editor-doc-mount .loc-color-overlay.loc-langstore { + background-color: color-mix(in srgb, var(--s2-red-200) 25%, transparent); } -.diff-bg-local { +.ew-editor-doc-mount .diff-bg-local { background-color: color-mix(in srgb, var(--s2-green-200) 25%, transparent); } -.diff-bg-upstream { +.ew-editor-doc-mount .diff-bg-upstream { background-color: color-mix(in srgb, var(--s2-red-200) 25%, transparent); } -.diff-bg-diff { +.ew-editor-doc-mount .diff-bg-diff { background-color: color-mix(in srgb, var(--s2-gray-100) 25%, transparent); } -.da-regional-edits-overlay { +.ew-editor-doc-mount .da-regional-edits-overlay { padding: var(--s2-spacing-200); background: var(--s2-gray-25); box-shadow: 0 2px 8px rgb(0 0 0 / 8%); } -.da-regional-edits-actions { +.ew-editor-doc-mount .da-regional-edits-actions { gap: var(--s2-spacing-200); } diff --git a/blocks/canvas/ew-editor-doc/ew-editor-doc.js b/blocks/canvas/ew-editor-doc/ew-editor-doc.js index bf67accee..908139d29 100644 --- a/blocks/canvas/ew-editor-doc/ew-editor-doc.js +++ b/blocks/canvas/ew-editor-doc/ew-editor-doc.js @@ -1,6 +1,7 @@ import { LitElement, html, nothing } from 'da-lit'; import { yUndo, yRedo, NodeSelection, TextSelection } from 'da-y-wrapper'; -import { getNx } from '../../../scripts/utils.js'; +import { getNx, getNx2 } from '../../../scripts/utils.js'; +import getSheet from '../../shared/sheet.js'; import { updateDocument, updateCursors, getInstrumentedHTML, getEditor } from '../editor-utils/editor-utils.js'; import { getActiveBlockIndex, getBlockPositions } from '../editor-utils/blocks.js'; import { @@ -41,6 +42,8 @@ const { loadStyle } = await import(`${getNx()}/utils/utils.js`); const { CHAT_EVENT } = await import(`${getNx()}/blocks/chat/constants.js`); const style = await loadStyle(import.meta.url); +const buttons = await getSheet(`${getNx2()}/styles/buttons.css`); +const diffStyle = await getSheet('/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css'); export class EwEditorDoc extends LitElement { static properties = { @@ -341,7 +344,7 @@ export class EwEditorDoc extends LitElement { connectedCallback() { super.connectedCallback(); - this.shadowRoot.adoptedStyleSheets = [style]; + this.shadowRoot.adoptedStyleSheets = [style, buttons, diffStyle]; this._unsubscribeEditorActive = canvasBus.editorViewState.subscribe(({ view }) => { this.hidden = view === 'layout'; hideSelectionToolbar(); diff --git a/blocks/edit/prose/diff/diff-actions.js b/blocks/edit/prose/diff/diff-actions.js index 33210242b..00b377189 100644 --- a/blocks/edit/prose/diff/diff-actions.js +++ b/blocks/edit/prose/diff/diff-actions.js @@ -117,7 +117,7 @@ function createCompositeButton({ switchBtn.appendChild(createTooltip(switchTooltip, 'diff-tooltip')); } - const confirmBtn = createButton('confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': `${tooltip}` }); + const confirmBtn = createButton('nx-action-btn-icon confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': `${tooltip}` }); confirmBtn.addEventListener('click', handler); if (tooltip) { confirmBtn.appendChild(createTooltip(tooltip, 'diff-tooltip')); diff --git a/blocks/edit/prose/diff/diff-global-dialog.js b/blocks/edit/prose/diff/diff-global-dialog.js index d30b66ed5..b4c7b153d 100644 --- a/blocks/edit/prose/diff/diff-global-dialog.js +++ b/blocks/edit/prose/diff/diff-global-dialog.js @@ -138,7 +138,7 @@ function createGlobalAction( const label = createElement('span', 'switch-btn da-diff-btn-base-element'); label.textContent = text; - const confirm = createButton('confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': text }); + const confirm = createButton('nx-action-btn-icon confirm-btn da-diff-btn-base-element', 'button', { 'aria-label': text }); confirm.addEventListener('click', () => handleGlobalAction(action, activeViews, isLocNode)); confirm.appendChild(createTooltip(tooltipText, 'diff-tooltip')); diff --git a/blocks/edit/prose/diff/diff-overlay-ui.js b/blocks/edit/prose/diff/diff-overlay-ui.js index 6b21914fa..f4192de5e 100644 --- a/blocks/edit/prose/diff/diff-overlay-ui.js +++ b/blocks/edit/prose/diff/diff-overlay-ui.js @@ -20,10 +20,10 @@ export function getLangOverlay(upstream) { const labelBtn = createElement('span', 'diff-label da-diff-btn-base-element'); labelBtn.textContent = text; - const acceptBtn = createButton('diff-accept da-diff-btn-base-element', 'button', { 'aria-label': `Accept ${text}` }); + const acceptBtn = createButton('nx-action-btn-icon diff-accept da-diff-btn-base-element', 'button', { 'aria-label': `Accept ${text}` }); acceptBtn.appendChild(createTooltip(`Accept ${text}`, 'diff-tooltip')); - const deleteBtn = createButton('diff-delete da-diff-btn-base-element', 'button', { 'aria-label': `Delete ${text}` }); + const deleteBtn = createButton('nx-action-btn-icon diff-delete da-diff-btn-base-element', 'button', { 'aria-label': `Delete ${text}` }); deleteBtn.appendChild(createTooltip(`Delete ${text}`, 'diff-tooltip')); compositeBtn.appendChild(labelBtn); diff --git a/blocks/edit/prose/diff/diff-utils.css b/blocks/edit/prose/diff/diff-utils.css index 778bb9e8a..71f979807 100644 --- a/blocks/edit/prose/diff/diff-utils.css +++ b/blocks/edit/prose/diff/diff-utils.css @@ -17,6 +17,14 @@ da-loc-added, border-radius: 6px; } +.loc-color-overlay.loc-regional { + background-color: rgb(144 42 222 / 20%); +} + +.loc-color-overlay.loc-langstore { + background-color: rgb(70 130 180 / 20%); +} + .loc-lang-overlay { z-index: 1001; } diff --git a/blocks/edit/prose/diff/diff-utils.js b/blocks/edit/prose/diff/diff-utils.js index 026c45ece..292bbb1be 100644 --- a/blocks/edit/prose/diff/diff-utils.js +++ b/blocks/edit/prose/diff/diff-utils.js @@ -96,14 +96,10 @@ async function loadLocCss(hostEl) { const locSheet = await getSheet('/blocks/edit/prose/diff/diff-utils.css'); if (hostEl?.shadowRoot) { - const sheets = [...(hostEl.shadowRoot.adoptedStyleSheets || []), locSheet]; - - if (hostEl.tagName === 'EW-EDITOR-DOC') { - const ewSheet = await getSheet('/blocks/canvas/ew-editor-doc/ew-editor-doc-diff.css'); - sheets.push(ewSheet); - } - - hostEl.shadowRoot.adoptedStyleSheets = sheets; + hostEl.shadowRoot.adoptedStyleSheets = [ + ...(hostEl.shadowRoot.adoptedStyleSheets || []), + locSheet, + ]; // Set CSS custom properties for diff labels setDiffLabelCssVars(hostEl); @@ -464,8 +460,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea this.dom.appendChild(nodeDOM); // Create placeholder cover div immediately - const coverDiv = createElement('div', 'loc-color-overlay', { 'loc-temp-dom': '' }); - coverDiv.style.backgroundColor = upstream ? '#4682b433' : '#902ade33'; + const coverDiv = createElement('div', `loc-color-overlay ${upstream ? 'loc-langstore' : 'loc-regional'}`, { 'loc-temp-dom': '' }); this.dom.appendChild(coverDiv); // Create placeholder overlay @@ -500,9 +495,6 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea coverDiv.removeChild(this.langOverlay); this.langOverlay = overlay; coverDiv.appendChild(this.langOverlay); - - const className = `loc-color-overlay ${upstream ? 'loc-langstore' : 'loc-regional'}`; - coverDiv.className = className; } catch (error) { // eslint-disable-next-line no-console console.warn('Failed to load enhanced overlays:', error); From 117265886a22d6c906ea34522442434ded54af32 Mon Sep 17 00:00:00 2001 From: Ramachandra Avuthu Date: Wed, 12 Aug 2026 16:38:51 -0400 Subject: [PATCH 6/6] review comments addressed --- blocks/canvas/canvas.js | 5 +++- .../editor-utils/prose-merge-conflicts.js | 4 +-- .../ew-canvas-header/ew-canvas-header.js | 1 - blocks/canvas/ew-editor-doc/ew-editor-doc.js | 1 + blocks/canvas/ew-editor-doc/prose.js | 8 +++--- .../ew-editor-wysiwyg/ew-editor-wysiwyg.js | 21 ---------------- blocks/edit/prose/diff/diff-utils.css | 4 +++ blocks/edit/prose/diff/diff-utils.js | 25 ++++++------------- blocks/edit/prose/index.js | 10 +++++--- 9 files changed, 29 insertions(+), 50 deletions(-) diff --git a/blocks/canvas/canvas.js b/blocks/canvas/canvas.js index 64e337833..c62ed8353 100644 --- a/blocks/canvas/canvas.js +++ b/blocks/canvas/canvas.js @@ -161,6 +161,7 @@ async function installCanvasHeader(block, { org, site }) { header.editorView = await readInitialCanvasEditorView({ org, site }); canvasBus.editorViewRequest.subscribe(({ view: rawView }) => { const view = normalizeCanvasEditorView(rawView); + header.editorView = view; persistCanvasEditorView(view); notifyCanvasEditorActive(view); syncEditorSplitLayout({ mountRoot: canvasEditorMountRoot(block), view }); @@ -253,7 +254,9 @@ export default async function decorate(block) { canvasBus.mergeConflictsState.subscribe((detail) => { header.hasUnresolvedMergeConflicts = detail?.hasMergeConflicts ?? false; - if (header.hasUnresolvedMergeConflicts) header.setEditorView('content'); + if (header.hasUnresolvedMergeConflicts && header.editorView !== 'content') { + canvasBus.editorViewRequest.emit({ view: 'content' }); + } }); canvasBus.undoState.subscribe((detail) => { diff --git a/blocks/canvas/editor-utils/prose-merge-conflicts.js b/blocks/canvas/editor-utils/prose-merge-conflicts.js index b8359fe46..ab83d9e89 100644 --- a/blocks/canvas/editor-utils/prose-merge-conflicts.js +++ b/blocks/canvas/editor-utils/prose-merge-conflicts.js @@ -2,9 +2,9 @@ import { Plugin } from 'da-y-wrapper'; import { checkForLocNodes } from '../../edit/prose/diff/diff-utils.js'; import { debounce } from '../../edit/utils/helpers.js'; -export function createMergeConflictsPlugin(onMergeConflictsChange) { +export function createMergeConflictsPlugin(onMergeConflictsChange, hostEl) { const debouncedCheck = debounce((view) => { - onMergeConflictsChange?.(checkForLocNodes(view)); + onMergeConflictsChange?.(checkForLocNodes(view, hostEl)); }, 500); return new Plugin({ diff --git a/blocks/canvas/ew-canvas-header/ew-canvas-header.js b/blocks/canvas/ew-canvas-header/ew-canvas-header.js index 2715f6ef0..0ad347e00 100644 --- a/blocks/canvas/ew-canvas-header/ew-canvas-header.js +++ b/blocks/canvas/ew-canvas-header/ew-canvas-header.js @@ -86,7 +86,6 @@ class EWCanvasHeader extends LitElement { setEditorView(view) { if (this.hasUnresolvedMergeConflicts && view !== 'content') return; if (!EDITOR_VIEWS.includes(view) || view === this.editorView) return; - this.editorView = view; canvasBus.editorViewRequest.emit({ view }); } diff --git a/blocks/canvas/ew-editor-doc/ew-editor-doc.js b/blocks/canvas/ew-editor-doc/ew-editor-doc.js index 908139d29..686aa91be 100644 --- a/blocks/canvas/ew-editor-doc/ew-editor-doc.js +++ b/blocks/canvas/ew-editor-doc/ew-editor-doc.js @@ -289,6 +289,7 @@ export class EwEditorDoc extends LitElement { setEditable: (editable) => this._setEditable(editable), getToken: () => token, onMergeConflictsChange: (hasConflicts) => this._emitMergeConflictsState(hasConflicts), + hostEl: this, extraPlugins: [ createExtensionsBridgePlugin(), createTrackingPlugin( diff --git a/blocks/canvas/ew-editor-doc/prose.js b/blocks/canvas/ew-editor-doc/prose.js index 6d709e048..17daf918b 100644 --- a/blocks/canvas/ew-editor-doc/prose.js +++ b/blocks/canvas/ew-editor-doc/prose.js @@ -86,7 +86,7 @@ function checkLibraryConfiguredOnSync(wsProvider, canWrite) { export default async function initProse({ path, permissions, setEditable, getToken, - extraPlugins = [], onMergeConflictsChange, + extraPlugins = [], onMergeConflictsChange, hostEl, }) { const editor = document.createElement('div'); editor.className = 'da-prose-mirror'; @@ -215,7 +215,7 @@ export default async function initProse({ if (canWrite) { plugins.unshift(createSlashMenuPlugin(), createSelectionToolbarPlugin()); - plugins.push(imageFocalPoint(), createMergeConflictsPlugin(onMergeConflictsChange)); + plugins.push(imageFocalPoint(), createMergeConflictsPlugin(onMergeConflictsChange, hostEl)); } let state = EditorState.create({ schema, plugins }); @@ -225,11 +225,11 @@ export default async function initProse({ const diffNodeViews = canWrite ? { diff_added(node, view, getPos) { - const MergeConflictsAddedView = getDiffClass('da-diff-added', getSchema, () => {}, { isUpstream: false }); + const MergeConflictsAddedView = getDiffClass('da-diff-added', getSchema, () => {}, { isUpstream: false, hostEl }); return new MergeConflictsAddedView(node, view, getPos); }, diff_deleted(node, view, getPos) { - const MergeConflictsDeletedView = getDiffClass('da-diff-deleted', getSchema, () => {}, { isUpstream: true }); + const MergeConflictsDeletedView = getDiffClass('da-diff-deleted', getSchema, () => {}, { isUpstream: true, hostEl }); return new MergeConflictsDeletedView(node, view, getPos); }, } : undefined; diff --git a/blocks/canvas/ew-editor-wysiwyg/ew-editor-wysiwyg.js b/blocks/canvas/ew-editor-wysiwyg/ew-editor-wysiwyg.js index ba66a81ee..c54b00eca 100644 --- a/blocks/canvas/ew-editor-wysiwyg/ew-editor-wysiwyg.js +++ b/blocks/canvas/ew-editor-wysiwyg/ew-editor-wysiwyg.js @@ -62,34 +62,14 @@ export class EwEditorWysiwyg extends LitElement { this._syncCanvasVisibility(); }); this._syncCanvasVisibility(); - - this._unsubscribeMergeConflicts = canvasBus.mergeConflictsState.subscribe((detail) => { - const hasMergeConflicts = detail?.hasMergeConflicts ?? false; - if (hasMergeConflicts) { - this._hadMergeConflicts = true; - } else if (this._hadMergeConflicts) { - this._hadMergeConflicts = false; - this._reloadIframe(); - } - }); } disconnectedCallback() { - this._unsubscribeMergeConflicts?.(); this._unsubscribeEditorActive?.(); this._clearQuickEditRetry(); super.disconnectedCallback(); } - _reloadIframe() { - const iframe = this.shadowRoot?.querySelector('iframe'); - try { - iframe?.contentWindow?.location.reload(); - } catch { - /* ignore — cross-origin reload should be allowed, but don't let this throw */ - } - } - get _iframeSrc() { const { org, repo, path } = this.ctx ?? {}; if (!org || !repo || !path || !this._cookieReady) return null; @@ -130,7 +110,6 @@ export class EwEditorWysiwyg extends LitElement { _resetCookieStateForCtxChange() { this._clearQuickEditRetry(); this._cookieReady = false; - this._hadMergeConflicts = false; } updated(changed) { diff --git a/blocks/edit/prose/diff/diff-utils.css b/blocks/edit/prose/diff/diff-utils.css index 71f979807..004632ae3 100644 --- a/blocks/edit/prose/diff/diff-utils.css +++ b/blocks/edit/prose/diff/diff-utils.css @@ -337,6 +337,10 @@ da-loc-added, z-index: 2; } +.loc-tabbed-color-overlay.is-hidden { + display: none; +} + @media (max-width: 600px) { .loc-tab-button { font-size: 12px; diff --git a/blocks/edit/prose/diff/diff-utils.js b/blocks/edit/prose/diff/diff-utils.js index 292bbb1be..2e24639a8 100644 --- a/blocks/edit/prose/diff/diff-utils.js +++ b/blocks/edit/prose/diff/diff-utils.js @@ -78,15 +78,6 @@ function setDiffLabelCssVars(daEditor) { host.style.setProperty('--diff-label-upstream', `'${labels.upstream}'`); } -function getDiffHost(view) { - const classicHost = document.querySelector('da-content')?.shadowRoot - ?.querySelector('da-editor'); - if (classicHost) return classicHost; - - const root = view?.dom?.getRootNode?.(); - return root instanceof ShadowRoot ? root.host : null; -} - let locCssLoading = false; async function loadLocCss(hostEl) { if (locCssLoading) return; @@ -189,7 +180,7 @@ function hasMatchingContent(nodeA, nodeB) { return true; } -export function checkForLocNodes(view) { +export function checkForLocNodes(view, hostEl) { const { doc } = view.state; const hasListLocNode = (node) => (node.type.name === 'bullet_list' || node.type.name === 'ordered_list') @@ -200,7 +191,7 @@ export function checkForLocNodes(view) { .some((node) => isLocNode(node) || hasListLocNode(node)); if (hasLocNodes) { - loadLocCss(getDiffHost(view)); + loadLocCss(hostEl); showGlobalDialog(view); } else { hideGlobalDialog(); @@ -288,13 +279,14 @@ export function removeActiveView(view) { activeViews.delete(view); } -export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstream } = {}) { +export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstream, hostEl } = {}) { return class { constructor(node, view, getPos) { this.node = node; this.view = view; this.getPos = getPos; this.schema = getSchema(); + this.hostEl = hostEl; const pos = getPos(); const { doc } = view.state; @@ -335,7 +327,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea } renderTabbedInterface(nodeA, view, posA, nodeB) { - loadLocCss(getDiffHost(view)); + loadLocCss(this.hostEl); this.dom = createElement('div', 'loc-tabbed-container', { contentEditable: 'false' }); this.contentDOM = null; // Don't let ProseMirror manage content @@ -374,14 +366,11 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea }); if (targetTab === 'added') { - colorOverlay.style.display = 'block'; colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-local'; } else if (targetTab === 'deleted') { - colorOverlay.style.display = 'block'; colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-upstream'; } else if (targetTab === 'diff') { - colorOverlay.style.display = 'none'; - colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-diff'; + colorOverlay.className = 'loc-tabbed-color-overlay diff-bg-diff is-hidden'; const diffTab = tabContent.querySelector('[data-tab="diff"]'); if (diffTab && !diffTab.loaded) { @@ -443,7 +432,7 @@ export function getDiffClass(elName, getSchema, dispatchTransaction, { isUpstrea } renderSingleNode(node, view, pos, upstream) { - loadLocCss(getDiffHost(view)); + loadLocCss(this.hostEl); const isDeleted = node.type.name === 'diff_deleted'; const viewClass = isDeleted ? 'loc-deleted-view' : 'loc-added-view'; diff --git a/blocks/edit/prose/index.js b/blocks/edit/prose/index.js index 54e841610..ab2df8aa5 100644 --- a/blocks/edit/prose/index.js +++ b/blocks/edit/prose/index.js @@ -195,6 +195,8 @@ async function loadCustomPlugins() { let lastCursorPosition = null; +let diffHostEl = null; + function dispatchTransaction(transaction) { if (!window.view) return; @@ -202,7 +204,7 @@ function dispatchTransaction(transaction) { window.view.updateState(newState); if (transaction.docChanged) { - debounce(checkForLocNodes, 500)(window.view); + debounce(checkForLocNodes, 500)(window.view, diffHostEl); } } @@ -520,6 +522,8 @@ export default async function initProse({ path, permissions, doc, daContent, wsP delete window.view; } + diffHostEl = daContent?.shadowRoot?.querySelector('da-editor') ?? null; + const connectionPromise = wsPromise || createConnection(path); const editor = document.createElement('div'); @@ -565,11 +569,11 @@ export default async function initProse({ path, permissions, doc, daContent, wsP dispatchTransaction, nodeViews: { diff_added(node, view, getPos) { - const LocAddedView = getDiffClass('da-diff-added', getSchema, dispatchTransaction, { isUpstream: false }); + const LocAddedView = getDiffClass('da-diff-added', getSchema, dispatchTransaction, { isUpstream: false, hostEl: diffHostEl }); return new LocAddedView(node, view, getPos); }, diff_deleted(node, view, getPos) { - const LocDeletedView = getDiffClass('da-diff-deleted', getSchema, dispatchTransaction, { isUpstream: true }); + const LocDeletedView = getDiffClass('da-diff-deleted', getSchema, dispatchTransaction, { isUpstream: true, hostEl: diffHostEl }); return new LocDeletedView(node, view, getPos); }, },