diff --git a/packages/layout-engine/painters/dom/src/index.test.ts b/packages/layout-engine/painters/dom/src/index.test.ts index b8614cccf5..0b92c9735f 100644 --- a/packages/layout-engine/painters/dom/src/index.test.ts +++ b/packages/layout-engine/painters/dom/src/index.test.ts @@ -2706,7 +2706,8 @@ describe('DomPainter', () => { const span = mount.querySelector('.superdoc-comment-highlight') as HTMLElement; expect(span).toBeTruthy(); expect(span.dataset.commentIds).toBe('comment-1'); - expect(span.style.backgroundColor).toBe(''); + // Track-change styling still applies; comment highlight should not override it. + expect(span.style.backgroundColor).toBe('var(--sd-track-insert-bg, #399c7222)'); }); it('applies comment highlight styles for non-tracked-change comments', () => { @@ -2737,6 +2738,112 @@ describe('DomPainter', () => { expect(span.style.backgroundColor).not.toBe(''); }); + it('uses configured comment highlight colors and opacity', () => { + const commentBlock: FlowBlock = { + kind: 'paragraph', + id: 'comment-config-block', + runs: [ + { + text: 'Configured highlight', + fontFamily: 'Arial', + fontSize: 16, + comments: [{ commentId: 'comment-3', internal: false, trackedChange: false }], + }, + ], + }; + + const { paragraphMeasure, paragraphLayout } = buildSingleParagraphData( + commentBlock.id, + commentBlock.runs[0].text.length, + ); + + const painter = createDomPainter({ + blocks: [commentBlock], + measures: [paragraphMeasure], + comments: { + highlightColors: { external: '#112233' }, + highlightOpacity: { inactive: 1 }, + }, + }); + painter.paint(paragraphLayout, mount); + + const span = mount.querySelector('.superdoc-comment-highlight') as HTMLElement; + expect(span).toBeTruthy(); + expect(span.style.backgroundColor).toBe('var(--sd-comment-highlight-color, #112233ff)'); + }); + + it('uses active comment highlight overrides when active', () => { + const commentBlock: FlowBlock = { + kind: 'paragraph', + id: 'comment-active-block', + runs: [ + { + text: 'Active highlight', + fontFamily: 'Arial', + fontSize: 16, + comments: [{ commentId: 'comment-4', internal: false, trackedChange: false }], + }, + ], + }; + + const { paragraphMeasure, paragraphLayout } = buildSingleParagraphData( + commentBlock.id, + commentBlock.runs[0].text.length, + ); + + const painter = createDomPainter({ + blocks: [commentBlock], + measures: [paragraphMeasure], + comments: { + highlightColors: { activeExternal: '#ff0000' }, + activeThreadId: 'comment-4', + }, + }); + painter.paint(paragraphLayout, mount); + + const span = mount.querySelector('.superdoc-comment-highlight') as HTMLElement; + expect(span).toBeTruthy(); + expect(span.style.backgroundColor).toBe( + 'var(--sd-comment-highlight-color-active, var(--sd-comment-highlight-color, #ff0000))', + ); + }); + + it('applies tracked-change highlight styles with CSS var fallbacks', () => { + const trackedBlock: FlowBlock = { + kind: 'paragraph', + id: 'tracked-highlight-block', + runs: [ + { + text: 'Inserted', + fontFamily: 'Arial', + fontSize: 16, + trackedChange: { + kind: 'insert', + id: 'change-highlight', + }, + }, + ], + attrs: { + trackedChangesMode: 'review', + trackedChangesEnabled: true, + }, + }; + + const { paragraphMeasure, paragraphLayout } = buildSingleParagraphData( + trackedBlock.id, + trackedBlock.runs[0].text.length, + ); + + const painter = createDomPainter({ blocks: [trackedBlock], measures: [paragraphMeasure] }); + painter.paint(paragraphLayout, mount); + + const span = mount.querySelector('.track-insert-dec') as HTMLElement; + expect(span).toBeTruthy(); + expect(span.style.backgroundColor).toBe('var(--sd-track-insert-bg, #399c7222)'); + expect(span.style.borderTop).toBe('1px dashed var(--sd-track-insert-border, #00853d)'); + expect(span.style.borderBottom).toBe('1px dashed var(--sd-track-insert-border, #00853d)'); + }); + it('respects trackedChangesMode modifiers for insertions', () => { const finalBlock: FlowBlock = { kind: 'paragraph', diff --git a/packages/layout-engine/painters/dom/src/index.ts b/packages/layout-engine/painters/dom/src/index.ts index cc1de409a4..d8469a54fc 100644 --- a/packages/layout-engine/painters/dom/src/index.ts +++ b/packages/layout-engine/painters/dom/src/index.ts @@ -10,7 +10,12 @@ import type { } from '@superdoc/contracts'; import { DomPainter } from './renderer.js'; import type { PageStyles } from './styles.js'; -import type { RulerOptions } from './renderer.js'; +import type { + CommentHighlightColors, + CommentHighlightOpacity, + CommentHighlightOptions, + RulerOptions, +} from './renderer.js'; // Re-export constants export { DOM_CLASS_NAMES } from './constants.js'; @@ -33,7 +38,12 @@ export type { RulerTick, CreateRulerElementOptions, } from './ruler/index.js'; -export type { RulerOptions } from './renderer.js'; +export type { + CommentHighlightColors, + CommentHighlightOpacity, + CommentHighlightOptions, + RulerOptions, +} from './renderer.js'; // Re-export utility functions for testing export { sanitizeUrl, linkMetrics, applyRunDataAttributes } from './renderer.js'; @@ -84,6 +94,8 @@ export type DomPainterOptions = { pageGap?: number; headerProvider?: PageDecorationProvider; footerProvider?: PageDecorationProvider; + /** Comment highlight configuration */ + comments?: CommentHighlightOptions; /** * Feature-flagged page virtualization. * When enabled (vertical mode only), the painter renders only a sliding window of pages @@ -115,6 +127,7 @@ export const createDomPainter = ( options: DomPainterOptions, ): PainterDOM & { setProviders?: (header?: PageDecorationProvider, footer?: PageDecorationProvider) => void; + setCommentHighlightConfig?: (config?: CommentHighlightOptions) => void; setVirtualizationPins?: (pageIndices: number[] | null | undefined) => void; } => { const painter = new DomPainter(options.blocks, options.measures, { @@ -123,6 +136,7 @@ export const createDomPainter = ( pageGap: options.pageGap, headerProvider: options.headerProvider, footerProvider: options.footerProvider, + comments: options.comments, virtualization: options.virtualization, ruler: options.ruler, }); @@ -145,6 +159,9 @@ export const createDomPainter = ( setProviders(header?: PageDecorationProvider, footer?: PageDecorationProvider) { painter.setProviders(header, footer); }, + setCommentHighlightConfig(config?: CommentHighlightOptions) { + painter.setCommentHighlightConfig(config); + }, setVirtualizationPins(pageIndices: number[] | null | undefined) { painter.setVirtualizationPins(pageIndices); }, diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 5636bb8450..899d810611 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -302,6 +302,30 @@ export type RulerOptions = { onMarginChange?: (side: 'left' | 'right', marginInches: number) => void; }; +export type CommentHighlightColors = { + /** Base highlight color for internal comments */ + internal?: string; + /** Base highlight color for external comments */ + external?: string; + /** Active highlight color override for internal comments */ + activeInternal?: string; + /** Active highlight color override for external comments */ + activeExternal?: string; +}; + +export type CommentHighlightOpacity = { + /** Opacity for active comment highlight (0-1) */ + active?: number; + /** Opacity for inactive comment highlight (0-1) */ + inactive?: number; +}; + +export type CommentHighlightOptions = { + highlightColors?: CommentHighlightColors; + highlightOpacity?: CommentHighlightOpacity; + activeThreadId?: string | null; +}; + type PainterOptions = { pageStyles?: PageStyles; layoutMode?: LayoutMode; @@ -319,6 +343,7 @@ type PainterOptions = { }; /** Per-page ruler options */ ruler?: RulerOptions; + comments?: CommentHighlightOptions; }; type BlockLookupEntry = { @@ -382,7 +407,10 @@ const DEFAULT_PAGE_HEIGHT_PX = 1056; const DEFAULT_VIRTUALIZED_PAGE_GAP = 72; const COMMENT_EXTERNAL_COLOR = '#B1124B'; const COMMENT_INTERNAL_COLOR = '#078383'; -const COMMENT_INACTIVE_ALPHA = '22'; +/** Default opacity for active comment highlights (0x44/0xff ≈ 0.267). */ +const DEFAULT_ACTIVE_ALPHA = 0x44 / 0xff; +/** Default opacity for inactive comment highlights (0x22/0xff ≈ 0.133). */ +const DEFAULT_INACTIVE_ALPHA = 0x22 / 0xff; type LinkRenderData = { href?: string; @@ -478,6 +506,20 @@ const TRACK_CHANGE_BASE_CLASS: Record = { format: 'track-format-dec', }; +const TRACK_CHANGE_DEFAULTS = { + insert: { + border: '#00853d', + background: '#399c7222', + }, + delete: { + border: '#cb0e47', + background: '#cb0e4722', + }, + format: { + border: 'gold', + }, +} as const; + const TRACK_CHANGE_MODIFIER_CLASS: Record> = { insert: { review: 'highlighted', @@ -775,7 +817,7 @@ const applyLinkDataset = (element: HTMLElement, dataset?: Record */ export class DomPainter { private blockLookup: BlockLookup; - private readonly options: PainterOptions; + private options: PainterOptions; private mount: HTMLElement | null = null; private doc: Document | null = null; private pageStates: PageDomState[] = []; @@ -858,6 +900,42 @@ export class DomPainter { this.footerProvider = footer; } + public setCommentHighlightConfig(config?: CommentHighlightOptions): void { + this.options.comments = config; + this.refreshCommentHighlights(); + } + + private refreshCommentHighlights(): void { + if (!this.mount) return; + const elements = this.mount.querySelectorAll('.superdoc-comment-highlight'); + elements.forEach((element) => { + const el = element as HTMLElement; + // Skip tracked-change comments that intentionally do not have a highlight background. + if (!el.style.backgroundColor) return; + const commentIds = el.dataset.commentIds + ? el.dataset.commentIds + .split(',') + .map((id) => id.trim()) + .filter(Boolean) + : []; + if (commentIds.length === 0) return; + + const isInternal = el.dataset.commentInternal === 'true'; + const comments = commentIds.map((commentId) => ({ + commentId, + internal: isInternal, + trackedChange: false, + })); + const highlight = computeCommentHighlightColors({ comments } as TextRun, this.options.comments); + if (!highlight) return; + if (highlight.isActive) { + el.style.backgroundColor = `var(--sd-comment-highlight-color-active, var(--sd-comment-highlight-color, ${highlight.activeColor}))`; + } else { + el.style.backgroundColor = `var(--sd-comment-highlight-color, ${highlight.inactiveColor})`; + } + }); + } + /** * Pins specific page indices so they remain mounted when virtualization is enabled. * @@ -3916,10 +3994,16 @@ export class DomPainter { const commentAnnotations = textRun.comments; const hasAnyComment = !!commentAnnotations?.length; const hasHighlightableComment = !!commentAnnotations?.some((c) => !c.trackedChange); - const commentColor = getCommentHighlight(textRun); + const commentHighlight = computeCommentHighlightColors(textRun, this.options.comments); - if (commentColor && !textRun.highlight && hasHighlightableComment) { - (elem as HTMLElement).style.backgroundColor = commentColor; + if (commentHighlight && !textRun.highlight && hasHighlightableComment) { + const { inactiveColor, activeColor, isActive } = commentHighlight; + const element = elem as HTMLElement; + if (isActive) { + element.style.backgroundColor = `var(--sd-comment-highlight-color-active, var(--sd-comment-highlight-color, ${activeColor}))`; + } else { + element.style.backgroundColor = `var(--sd-comment-highlight-color, ${inactiveColor})`; + } } // We still need to preserve the comment ids if (hasAnyComment) { @@ -5051,6 +5135,20 @@ export class DomPainter { elem.classList.add(modifier); } + if (modifier === 'highlighted') { + if (meta.kind === 'insert') { + elem.style.borderTop = `1px dashed var(--sd-track-insert-border, ${TRACK_CHANGE_DEFAULTS.insert.border})`; + elem.style.borderBottom = `1px dashed var(--sd-track-insert-border, ${TRACK_CHANGE_DEFAULTS.insert.border})`; + elem.style.backgroundColor = `var(--sd-track-insert-bg, ${TRACK_CHANGE_DEFAULTS.insert.background})`; + } else if (meta.kind === 'delete') { + elem.style.borderTop = `1px dashed var(--sd-track-delete-border, ${TRACK_CHANGE_DEFAULTS.delete.border})`; + elem.style.borderBottom = `1px dashed var(--sd-track-delete-border, ${TRACK_CHANGE_DEFAULTS.delete.border})`; + elem.style.backgroundColor = `var(--sd-track-delete-bg, ${TRACK_CHANGE_DEFAULTS.delete.background})`; + } else if (meta.kind === 'format') { + elem.style.borderBottom = `2px solid var(--sd-track-format-border, ${TRACK_CHANGE_DEFAULTS.format.border})`; + } + } + elem.dataset.trackChangeId = meta.id; elem.dataset.trackChangeKind = meta.kind; if (meta.author) { @@ -5928,12 +6026,73 @@ const applyRunStyles = (element: HTMLElement, run: Run, _isLink = false): void = } }; -const getCommentHighlight = (run: TextRun): string | undefined => { +const clampOpacity = (value: number | undefined): number | null => { + if (typeof value !== 'number' || !Number.isFinite(value)) return null; + return Math.max(0, Math.min(1, value)); +}; + +const applyAlphaToHex = (color: string, opacity: number): string => { + 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}`; +}; + +type CommentMeta = NonNullable[number]; + +const findActiveComment = ( + comments: CommentMeta[] | undefined, + activeThreadId?: string | null, +): CommentMeta | undefined => { + if (!comments?.length || !activeThreadId) return undefined; + return comments.find((comment) => comment.commentId === activeThreadId || comment.importedId === activeThreadId); +}; + +const computeCommentHighlightColors = ( + run: TextRun, + config?: CommentHighlightOptions, +): { inactiveColor: string; activeColor: string; isActive: boolean } | undefined => { const comments = run.comments; if (!comments || comments.length === 0) return undefined; + const activeComment = findActiveComment(comments, config?.activeThreadId); const primary = comments[0]; - const base = primary.internal ? COMMENT_INTERNAL_COLOR : COMMENT_EXTERNAL_COLOR; - return `${base}${COMMENT_INACTIVE_ALPHA}`; + const highlightColors = config?.highlightColors ?? {}; + const highlightOpacity = config?.highlightOpacity ?? {}; + const isActive = Boolean(activeComment); + + const baseColor = primary.internal + ? (highlightColors.internal ?? COMMENT_INTERNAL_COLOR) + : (highlightColors.external ?? COMMENT_EXTERNAL_COLOR); + + const inactiveOpacity = clampOpacity(highlightOpacity.inactive) ?? DEFAULT_INACTIVE_ALPHA; + const inactiveColor = applyAlphaToHex(baseColor, inactiveOpacity); + + const activeInternal = (activeComment ?? primary)?.internal === true; + const activeBaseColor = activeInternal + ? (highlightColors.internal ?? COMMENT_INTERNAL_COLOR) + : (highlightColors.external ?? COMMENT_EXTERNAL_COLOR); + const activeOverride = activeInternal ? highlightColors.activeInternal : highlightColors.activeExternal; + const activeOpacity = clampOpacity(highlightOpacity.active) ?? DEFAULT_ACTIVE_ALPHA; + const activeColor = activeOverride ?? applyAlphaToHex(activeBaseColor, activeOpacity); + + return { inactiveColor, activeColor, isActive }; +}; + +const getCommentHighlight = (run: TextRun, config?: CommentHighlightOptions): string | undefined => { + const resolved = computeCommentHighlightColors(run, config); + if (!resolved) return undefined; + return resolved.isActive ? resolved.activeColor : resolved.inactiveColor; }; /** diff --git a/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts b/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts index 69815ff6ed..1a5eebd9f8 100644 --- a/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts +++ b/packages/super-editor/src/core/presentation-editor/PresentationEditor.ts @@ -81,8 +81,13 @@ import type { MultiSectionHeaderFooterIdentifier, TableHitResult, } from '@superdoc/layout-bridge'; -import { createDomPainter } from '@superdoc/painter-dom'; -import type { LayoutMode, PageDecorationProvider, RulerOptions } from '@superdoc/painter-dom'; +import { + createDomPainter, + type CommentHighlightOptions, + LayoutMode, + PageDecorationProvider, + RulerOptions, +} from '@superdoc/painter-dom'; import { measureBlock } from '@superdoc/measuring-dom'; import type { ColumnLayout, @@ -157,6 +162,7 @@ export type { // Mark name constants import { CommentMarkName } from '@extensions/comment/comments-constants.js'; +import { CommentsPluginKey } from '@extensions/comment/comments-plugin.js'; import { TrackInsertMarkName, TrackDeleteMarkName, TrackFormatMarkName } from '@extensions/track-changes/constants.js'; /** @@ -254,6 +260,7 @@ export class PresentationEditor extends EventEmitter { #hiddenHost: HTMLElement; #layoutOptions: LayoutEngineOptions; #layoutState: LayoutState = { blocks: [], measures: [], layout: null, bookmarks: new Map() }; + #activeCommentId: string | null = null; #domPainter: ReturnType | null = null; #pageGeometryHelper: PageGeometryHelper | null = null; #dragDropManager: DragDropManager | null = null; @@ -2185,6 +2192,24 @@ export class PresentationEditor extends EventEmitter { } } + #syncActiveCommentState(): boolean { + const pluginState = CommentsPluginKey.getState(this.#editor.state); + const nextActive = pluginState?.activeThreadId ?? null; + if (nextActive === this.#activeCommentId) return false; + this.#activeCommentId = nextActive; + return true; + } + + #buildCommentHighlightConfig(): CommentHighlightOptions | undefined { + const comments = this.#options.comments; + if (!comments && !this.#activeCommentId) return undefined; + return { + highlightColors: comments?.highlightColors, + highlightOpacity: comments?.highlightOpacity, + activeThreadId: this.#activeCommentId, + }; + } + #setupEditorListeners() { const handleUpdate = ({ transaction }: { transaction?: Transaction }) => { const trackedChangesChanged = this.#syncTrackedChangesPreferences(); @@ -2220,6 +2245,11 @@ export class PresentationEditor extends EventEmitter { this.#editorInputManager?.clearCellAnchor(); } }; + const handleTransaction = ({ transaction }: { transaction?: Transaction }) => { + if (!transaction) return; + if (!this.#syncActiveCommentState()) return; + this.#domPainter?.setCommentHighlightConfig?.(this.#buildCommentHighlightConfig()); + }; const handleSelection = () => { this.#scheduleSelectionUpdate(); // Update local cursor in awareness for collaboration @@ -2229,8 +2259,10 @@ export class PresentationEditor extends EventEmitter { }; this.#editor.on('update', handleUpdate); this.#editor.on('selectionUpdate', handleSelection); + this.#editor.on('transaction', handleTransaction); this.#editorListeners.push({ event: 'update', handler: handleUpdate as (...args: unknown[]) => void }); this.#editorListeners.push({ event: 'selectionUpdate', handler: handleSelection as (...args: unknown[]) => void }); + this.#editorListeners.push({ event: 'transaction', handler: handleTransaction as (...args: unknown[]) => void }); // Listen for page style changes (e.g., margin adjustments via ruler). // These changes don't modify document content (docChanged === false), @@ -2960,6 +2992,7 @@ export class PresentationEditor extends EventEmitter { footerBlocks.length > 0 ? footerBlocks : undefined, footerMeasures.length > 0 ? footerMeasures : undefined, ); + painter.setCommentHighlightConfig?.(this.#buildCommentHighlightConfig()); // Avoid MutationObserver overhead while repainting large DOM trees. this.#domIndexObserverManager?.pause(); // Pass the transaction mapping for efficient position attribute updates. @@ -3031,6 +3064,7 @@ export class PresentationEditor extends EventEmitter { footerProvider: this.#headerFooterSession?.footerDecorationProvider, ruler: this.#layoutOptions.ruler, pageGap: this.#layoutState.layout?.pageGap ?? this.#getEffectivePageGap(), + comments: this.#buildCommentHighlightConfig(), }); } return this.#domPainter; diff --git a/packages/superdoc/README.md b/packages/superdoc/README.md index 2d65954979..85279c925b 100644 --- a/packages/superdoc/README.md +++ b/packages/superdoc/README.md @@ -45,6 +45,50 @@ const superdoc = new SuperDoc({ }); ``` +### Comment Highlight Customization + +```javascript +const superdoc = new SuperDoc({ + selector: '#superdoc', + documents: [{ id: 'my-doc-id', type: 'docx', data: fileObject }], + modules: { + comments: { + highlightColors: { + internal: '#078383', + external: '#B1124B', + activeInternal: '#1355ff', + activeExternal: '#1355ff', + }, + highlightOpacity: { + inactive: 0.13, + active: 0.27, + }, + overlayHighlightColors: { + internal: '#078383', + external: '#B1124B', + activeInternal: '#1355ff', + activeExternal: '#1355ff', + }, + overlayHighlightOpacity: { + inactive: 0.2, + active: 0.4, + }, + }, + }, +}); +``` + +CSS theming can override these values via: +- `--sd-comment-highlight-color` +- `--sd-comment-highlight-color-active` +- `--sd-comment-overlay-color` +- `--sd-comment-overlay-color-active` +- `--sd-track-insert-border` +- `--sd-track-insert-bg` +- `--sd-track-delete-border` +- `--sd-track-delete-bg` +- `--sd-track-format-border` + ## 🛠️ Development Setup 1. **Clone the Repository** diff --git a/packages/superdoc/src/components/CommentsLayer/CommentsLayer.vue b/packages/superdoc/src/components/CommentsLayer/CommentsLayer.vue index 599c1560e6..34e9a92133 100644 --- a/packages/superdoc/src/components/CommentsLayer/CommentsLayer.vue +++ b/packages/superdoc/src/components/CommentsLayer/CommentsLayer.vue @@ -62,13 +62,50 @@ const getStyle = (conversation) => { const placement = conversation.selection.selectionBounds; const top = (parseFloat(placement.top) + containerBounds.top) * activeZoom.value; - const internalHighlightColor = '#078383'; - const externalHighlightColor = '#B1124B'; + const commentsConfig = proxy?.$superdoc?.config?.modules?.comments ?? {}; + const overlayColors = commentsConfig.overlayHighlightColors ?? {}; + const baseColors = commentsConfig.highlightColors ?? {}; + const overlayOpacity = commentsConfig.overlayHighlightOpacity ?? commentsConfig.highlightOpacity ?? {}; + + const defaultInactiveOpacity = 0x33 / 0xff; + const defaultActiveOpacity = 0x66 / 0xff; + + const isActive = activeComment.value === commentId; + const internalBase = overlayColors.internal ?? baseColors.internal ?? '#078383'; + const externalBase = overlayColors.external ?? baseColors.external ?? '#B1124B'; + const baseColor = conversation.isInternal ? internalBase : externalBase; + + const activeOverride = conversation.isInternal + ? (overlayColors.activeInternal ?? baseColors.activeInternal) + : (overlayColors.activeExternal ?? baseColors.activeExternal); + + const activeOpacity = Number.isFinite(overlayOpacity.active) ? Math.max(0, Math.min(overlayOpacity.active, 1)) : null; + const inactiveOpacity = Number.isFinite(overlayOpacity.inactive) + ? Math.max(0, Math.min(overlayOpacity.inactive, 1)) + : null; + + 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}`; + }; - let opacity = '33'; - activeComment.value === commentId ? (opacity = '66') : '33'; - let fillColor = conversation.isInternal ? internalHighlightColor : externalHighlightColor; - fillColor += opacity; + const inactiveColor = applyAlphaToHex(baseColor, inactiveOpacity ?? defaultInactiveOpacity); + const activeColor = activeOverride ?? applyAlphaToHex(baseColor, activeOpacity ?? defaultActiveOpacity); + const fillColor = isActive + ? `var(--sd-comment-overlay-color-active, var(--sd-comment-overlay-color, ${activeColor}))` + : `var(--sd-comment-overlay-color, ${inactiveColor})`; return { position: 'absolute', diff --git a/packages/superdoc/src/core/types/index.js b/packages/superdoc/src/core/types/index.js index 0fb542db5f..947af53179 100644 --- a/packages/superdoc/src/core/types/index.js +++ b/packages/superdoc/src/core/types/index.js @@ -59,6 +59,14 @@ * @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 {Object} [comments.overlayHighlightColors] Overlay highlight colors for comment blocks + * @property {string} [comments.overlayHighlightColors.internal] Overlay color for internal comments + * @property {string} [comments.overlayHighlightColors.external] Overlay color for external comments + * @property {string} [comments.overlayHighlightColors.activeInternal] Active overlay color for internal comments + * @property {string} [comments.overlayHighlightColors.activeExternal] Active overlay color for external comments + * @property {Object} [comments.overlayHighlightOpacity] Overlay highlight opacity values (0-1) + * @property {number} [comments.overlayHighlightOpacity.active] Opacity for active overlay highlight + * @property {number} [comments.overlayHighlightOpacity.inactive] Opacity for inactive overlay 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