diff --git a/packages/super-editor/src/assets/styles/elements/prosemirror.css b/packages/super-editor/src/assets/styles/elements/prosemirror.css index 5b806eed6d..b56f7f533f 100644 --- a/packages/super-editor/src/assets/styles/elements/prosemirror.css +++ b/packages/super-editor/src/assets/styles/elements/prosemirror.css @@ -226,21 +226,21 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html } .ProseMirror .track-insert-dec.highlighted { - border-top: 1px dashed #00853d; - border-bottom: 1px dashed #00853d; - background-color: #399c7222; + border-top: 1px dashed var(--sd-track-insert-border, #00853d); + border-bottom: 1px dashed var(--sd-track-insert-border, #00853d); + background-color: var(--sd-track-insert-bg, #399c7222); } .ProseMirror .track-delete-dec.highlighted { - border-top: 1px dashed #cb0e47; - border-bottom: 1px dashed #cb0e47; - background-color: #cb0e4722; + border-top: 1px dashed var(--sd-track-delete-border, #cb0e47); + border-bottom: 1px dashed var(--sd-track-delete-border, #cb0e47); + background-color: var(--sd-track-delete-bg, #cb0e4722); text-decoration: line-through !important; text-decoration-thickness: 2px !important; } .ProseMirror .track-format-dec.highlighted { - border-bottom: 2px solid gold; + border-bottom: 2px solid var(--sd-track-format-border, gold); } .ProseMirror .track-delete-widget { diff --git a/packages/super-editor/src/assets/styles/extensions/comments.css b/packages/super-editor/src/assets/styles/extensions/comments.css index 567b92fdea..9b4ab407e0 100644 --- a/packages/super-editor/src/assets/styles/extensions/comments.css +++ b/packages/super-editor/src/assets/styles/extensions/comments.css @@ -3,7 +3,7 @@ } .sd-editor-comment-highlight:hover { - background-color: #1354ff55; + background-color: var(--sd-comment-highlight-hover, #1354ff55); } .sd-editor-comment-highlight.sd-custom-selection { diff --git a/packages/super-editor/src/core/types/EditorConfig.ts b/packages/super-editor/src/core/types/EditorConfig.ts index 0ed724ed78..020450298f 100644 --- a/packages/super-editor/src/core/types/EditorConfig.ts +++ b/packages/super-editor/src/core/types/EditorConfig.ts @@ -185,6 +185,20 @@ export interface EditorOptions { /** Whether comments are enabled */ isCommentsEnabled?: boolean; + /** Comment highlight configuration */ + comments?: { + highlightColors?: { + internal?: string; + external?: string; + activeInternal?: string; + activeExternal?: string; + }; + highlightOpacity?: { + active?: number; + inactive?: number; + }; + }; + /** Whether this is a new file */ isNewFile?: boolean; diff --git a/packages/super-editor/src/extensions/comment/comments-helpers.js b/packages/super-editor/src/extensions/comment/comments-helpers.js index ff4969563e..feb062327c 100644 --- a/packages/super-editor/src/extensions/comment/comments-helpers.js +++ b/packages/super-editor/src/extensions/comment/comments-helpers.js @@ -718,10 +718,47 @@ export const translateFormatChangesToEnglish = (attrs = {}) => { * @param {EditorView} param0.editor The current editor view * @returns {String} The color to use for the highlight */ +const DEFAULT_ACTIVE_ALPHA = 0x44 / 0xff; +const DEFAULT_INACTIVE_ALPHA = 0x22 / 0xff; + +const clampOpacity = (value) => { + if (!Number.isFinite(value)) return null; + return Math.max(0, Math.min(1, value)); +}; + +const applyAlphaToHex = (color, opacity) => { + if (typeof color !== 'string') return color; + const match = color.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (!match) return color; + + const hex = + match[1].length === 3 + ? match[1] + .split('') + .map((c) => c + c) + .join('') + : match[1]; + const alpha = Math.round(opacity * 255) + .toString(16) + .padStart(2, '0'); + return `#${hex}${alpha}`; +}; + export const getHighlightColor = ({ activeThreadId, threadId, isInternal, editor }) => { if (!editor.options.isInternal && isInternal) return 'transparent'; const pluginState = CommentsPluginKey.getState(editor.state); - const color = isInternal ? pluginState.internalColor : pluginState.externalColor; - const alpha = activeThreadId == threadId ? '44' : '22'; - return `${color}${alpha}`; + const highlightColors = editor.options.comments?.highlightColors || {}; + const highlightOpacity = editor.options.comments?.highlightOpacity || {}; + const isActive = activeThreadId == threadId; + + const baseColor = isInternal + ? (highlightColors.internal ?? pluginState.internalColor) + : (highlightColors.external ?? pluginState.externalColor); + + const activeOverride = isInternal ? highlightColors.activeInternal : highlightColors.activeExternal; + if (isActive && activeOverride) return activeOverride; + + const resolvedOpacity = clampOpacity(isActive ? highlightOpacity.active : highlightOpacity.inactive); + const opacity = resolvedOpacity ?? (isActive ? DEFAULT_ACTIVE_ALPHA : DEFAULT_INACTIVE_ALPHA); + return applyAlphaToHex(baseColor, opacity); }; diff --git a/packages/super-editor/src/extensions/comment/comments-plugin.js b/packages/super-editor/src/extensions/comment/comments-plugin.js index 721e2ec2ed..024b7e6fda 100644 --- a/packages/super-editor/src/extensions/comment/comments-plugin.js +++ b/packages/super-editor/src/extensions/comment/comments-plugin.js @@ -314,10 +314,11 @@ export const CommentsPlugin = Extension.create({ state: { init() { + const highlightColors = editor.options.comments?.highlightColors || {}; return { activeThreadId: null, - externalColor: '#B1124B', - internalColor: '#078383', + externalColor: highlightColors.external ?? '#B1124B', + internalColor: highlightColors.internal ?? '#078383', decorations: DecorationSet.empty, allCommentPositions: {}, allCommentIds: [], diff --git a/packages/super-editor/src/extensions/comment/comments.test.js b/packages/super-editor/src/extensions/comment/comments.test.js index 73a43f0dbe..687717144d 100644 --- a/packages/super-editor/src/extensions/comment/comments.test.js +++ b/packages/super-editor/src/extensions/comment/comments.test.js @@ -350,6 +350,64 @@ describe('comment helpers', () => { const hidden = getHighlightColor({ activeThreadId: null, threadId: 'thread-3', isInternal: true, editor }); expect(hidden).toBe('transparent'); }); + + it('uses configured highlight colors and opacity for inactive comments', () => { + const editor = { + options: { + isInternal: false, + comments: { + highlightColors: { external: '#112233' }, + highlightOpacity: { inactive: 0.25 }, + }, + }, + state: {}, + }; + vi.spyOn(CommentsPluginKey, 'getState').mockReturnValue({ + internalColor: '#123456', + externalColor: '#abcdef', + }); + + const color = getHighlightColor({ activeThreadId: 'thread-2', threadId: 'thread-1', isInternal: false, editor }); + expect(color).toBe('#11223340'); + }); + + it('uses active highlight override color when provided', () => { + const editor = { + options: { + isInternal: false, + comments: { + highlightColors: { external: '#112233', activeExternal: '#ff0000' }, + }, + }, + state: {}, + }; + vi.spyOn(CommentsPluginKey, 'getState').mockReturnValue({ + internalColor: '#123456', + externalColor: '#abcdef', + }); + + const color = getHighlightColor({ activeThreadId: 'thread-1', threadId: 'thread-1', isInternal: false, editor }); + expect(color).toBe('#ff0000'); + }); + + it('falls back to plugin colors with custom opacity', () => { + const editor = { + options: { + isInternal: false, + comments: { + highlightOpacity: { active: 0.2 }, + }, + }, + state: {}, + }; + vi.spyOn(CommentsPluginKey, 'getState').mockReturnValue({ + internalColor: '#123456', + externalColor: '#abcdef', + }); + + const color = getHighlightColor({ activeThreadId: 'thread-1', threadId: 'thread-1', isInternal: false, editor }); + expect(color).toBe('#abcdef33'); + }); }); describe('comments plugin commands', () => { diff --git a/packages/superdoc/src/SuperDoc.vue b/packages/superdoc/src/SuperDoc.vue index 7b6988885f..16287c6329 100644 --- a/packages/superdoc/src/SuperDoc.vue +++ b/packages/superdoc/src/SuperDoc.vue @@ -105,9 +105,28 @@ const commentsModuleConfig = computed(() => { return config; }); -const superdocStyleVars = computed(() => ({ - '--sd-ui-font-family': uiFontFamily.value, -})); +const superdocStyleVars = computed(() => { + const vars = { + '--sd-ui-font-family': uiFontFamily.value, + }; + + const commentsConfig = proxy.$superdoc.config.modules?.comments; + if (!commentsConfig || commentsConfig === false) return vars; + + if (commentsConfig.highlightHoverColor) { + vars['--sd-comment-highlight-hover'] = commentsConfig.highlightHoverColor; + } + + const trackChangeColors = commentsConfig.trackChangeHighlightColors || {}; + const activeTrackChangeColors = commentsConfig.trackChangeActiveHighlightColors || trackChangeColors; + if (activeTrackChangeColors.insertBorder) vars['--sd-track-insert-border'] = activeTrackChangeColors.insertBorder; + if (activeTrackChangeColors.insertBackground) vars['--sd-track-insert-bg'] = activeTrackChangeColors.insertBackground; + if (activeTrackChangeColors.deleteBorder) vars['--sd-track-delete-border'] = activeTrackChangeColors.deleteBorder; + if (activeTrackChangeColors.deleteBackground) vars['--sd-track-delete-bg'] = activeTrackChangeColors.deleteBackground; + if (activeTrackChangeColors.formatBorder) vars['--sd-track-format-border'] = activeTrackChangeColors.formatBorder; + + return vars; +}); // Refs const layers = ref(null); @@ -448,6 +467,10 @@ const editorOptions = (doc) => { isCommentsEnabled: Boolean(commentsModuleConfig.value), isAiEnabled: proxy.$superdoc.config.modules?.ai, slashMenuConfig: proxy.$superdoc.config.modules?.slashMenu, + comments: { + highlightColors: commentsModuleConfig.value?.highlightColors, + highlightOpacity: commentsModuleConfig.value?.highlightOpacity, + }, editorCtor: useLayoutEngine ? PresentationEditor : undefined, onBeforeCreate: onEditorBeforeCreate, onCreate: onEditorCreate, diff --git a/packages/superdoc/src/core/types/index.js b/packages/superdoc/src/core/types/index.js index 79efaa263f..0fb542db5f 100644 --- a/packages/superdoc/src/core/types/index.js +++ b/packages/superdoc/src/core/types/index.js @@ -51,6 +51,27 @@ * currentUser?: User | null, * superdoc?: SuperDoc | null, * }) => boolean | undefined} [comments.permissionResolver] Custom permission resolver for comment actions + * @property {Object} [comments.highlightColors] Comment highlight colors (internal/external and active overrides) + * @property {string} [comments.highlightColors.internal] Base highlight color for internal comments + * @property {string} [comments.highlightColors.external] Base highlight color for external comments + * @property {string} [comments.highlightColors.activeInternal] Active highlight color override for internal comments + * @property {string} [comments.highlightColors.activeExternal] Active highlight color override for external comments + * @property {Object} [comments.highlightOpacity] Comment highlight opacity values (0-1) + * @property {number} [comments.highlightOpacity.active] Opacity for active comment highlight + * @property {number} [comments.highlightOpacity.inactive] Opacity for inactive comment highlight + * @property {string} [comments.highlightHoverColor] Hover highlight color for comment marks + * @property {Object} [comments.trackChangeHighlightColors] Track change highlight colors + * @property {string} [comments.trackChangeHighlightColors.insertBorder] Border color for inserted text highlight + * @property {string} [comments.trackChangeHighlightColors.insertBackground] Background color for inserted text highlight + * @property {string} [comments.trackChangeHighlightColors.deleteBorder] Border color for deleted text highlight + * @property {string} [comments.trackChangeHighlightColors.deleteBackground] Background color for deleted text highlight + * @property {string} [comments.trackChangeHighlightColors.formatBorder] Border color for format change highlight + * @property {Object} [comments.trackChangeActiveHighlightColors] Active track change highlight colors (defaults to trackChangeHighlightColors) + * @property {string} [comments.trackChangeActiveHighlightColors.insertBorder] Active border color for inserted text highlight + * @property {string} [comments.trackChangeActiveHighlightColors.insertBackground] Active background color for inserted text highlight + * @property {string} [comments.trackChangeActiveHighlightColors.deleteBorder] Active border color for deleted text highlight + * @property {string} [comments.trackChangeActiveHighlightColors.deleteBackground] Active background color for deleted text highlight + * @property {string} [comments.trackChangeActiveHighlightColors.formatBorder] Active border color for format change highlight * @property {Object} [ai] AI module configuration * @property {string} [ai.apiKey] Harbour API key for AI features * @property {string} [ai.endpoint] Custom endpoint URL for AI services