Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions packages/super-editor/src/assets/styles/elements/prosemirror.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
37 changes: 37 additions & 0 deletions packages/super-editor/src/core/types/EditorConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,40 @@ export interface PermissionParams {
trackedChange?: unknown | null;
}

/**
* Comment highlight color configuration
*/
export interface 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;
}

/**
* Comment highlight opacity configuration
*/
export interface CommentHighlightOpacity {
/** Opacity for active comment highlight (0-1) */
active?: number;
/** Opacity for inactive comment highlight (0-1) */
inactive?: number;
}

/**
* Comment configuration options
*/
export interface CommentConfig {
/** Comment highlight colors */
highlightColors?: CommentHighlightColors;
/** Comment highlight opacity values */
highlightOpacity?: CommentHighlightOpacity;
}

/**
* Editor configuration options
*/
Expand Down Expand Up @@ -185,6 +219,9 @@ export interface EditorOptions {
/** Whether comments are enabled */
isCommentsEnabled?: boolean;

/** Comment highlight configuration */
comments?: CommentConfig;

/** Whether this is a new file */
isNewFile?: boolean;

Expand Down
58 changes: 55 additions & 3 deletions packages/super-editor/src/extensions/comment/comments-helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -718,10 +718,62 @@ export const translateFormatChangesToEnglish = (attrs = {}) => {
* @param {EditorView} param0.editor The current editor view
* @returns {String} The color to use for the highlight
*/

/** 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;

/**
* Clamps an opacity value to the valid range [0, 1].
* @param {number} value - The opacity value to clamp
* @returns {number|null} The clamped value, or null if input is not a finite number
*/
export const clampOpacity = (value) => {
if (!Number.isFinite(value)) return null;
return Math.max(0, Math.min(1, value));
};

/**
* Applies an alpha/opacity value to a hex color string.
* @param {string} color - Hex color in 3-digit (#abc) or 6-digit (#aabbcc) format
* @param {number} opacity - Opacity value between 0 and 1
* @returns {string} The color with alpha appended (e.g., #aabbcc44), or original color if invalid format
*/
export 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;

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve loose ID match for active highlight detection

The new strict comparison in getHighlightColor makes active detection depend on activeThreadId and threadId having the same type. Elsewhere IDs are explicitly matched with loose equality (e.g., commentId == id in packages/superdoc/src/stores/comments-store.js), which implies numeric IDs can flow in from external data/imports. If a mark stores a numeric commentId but the active thread is set from a string ID (or vice versa), isActive will never be true and the active highlight/opacity will never show. Consider normalizing IDs to a consistent type or using a loose comparison here.

Useful? React with 👍 / 👎.


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);
};
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
Expand Down
118 changes: 118 additions & 0 deletions packages/super-editor/src/extensions/comment/comments.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const {
prepareCommentsForImport,
translateFormatChangesToEnglish,
getHighlightColor,
clampOpacity,
applyAlphaToHex,
} = CommentHelpers;

afterEach(() => {
Expand Down Expand Up @@ -350,6 +352,122 @@ 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('clampOpacity', () => {
it('returns the value when within valid range', () => {
expect(clampOpacity(0.5)).toBe(0.5);
expect(clampOpacity(0)).toBe(0);
expect(clampOpacity(1)).toBe(1);
});

it('clamps values below 0 to 0', () => {
expect(clampOpacity(-0.5)).toBe(0);
expect(clampOpacity(-100)).toBe(0);
});

it('clamps values above 1 to 1', () => {
expect(clampOpacity(1.5)).toBe(1);
expect(clampOpacity(100)).toBe(1);
});

it('returns null for non-finite values', () => {
expect(clampOpacity(NaN)).toBeNull();
expect(clampOpacity(Infinity)).toBeNull();
expect(clampOpacity(-Infinity)).toBeNull();
expect(clampOpacity(undefined)).toBeNull();
expect(clampOpacity(null)).toBeNull();
});
});

describe('applyAlphaToHex', () => {
it('applies alpha to 6-digit hex colors', () => {
expect(applyAlphaToHex('#aabbcc', 0.5)).toBe('#aabbcc80');
expect(applyAlphaToHex('#000000', 1)).toBe('#000000ff');
expect(applyAlphaToHex('#ffffff', 0)).toBe('#ffffff00');
});

it('expands and applies alpha to 3-digit hex colors', () => {
expect(applyAlphaToHex('#abc', 0.5)).toBe('#aabbcc80');
expect(applyAlphaToHex('#000', 1)).toBe('#000000ff');
expect(applyAlphaToHex('#fff', 0.25)).toBe('#ffffff40');
});

it('returns original color for invalid hex formats', () => {
expect(applyAlphaToHex('rgb(255,0,0)', 0.5)).toBe('rgb(255,0,0)');
expect(applyAlphaToHex('#gg0000', 0.5)).toBe('#gg0000');
expect(applyAlphaToHex('red', 0.5)).toBe('red');
expect(applyAlphaToHex('#aabbccdd', 0.5)).toBe('#aabbccdd');
});

it('returns original value for non-string input', () => {
expect(applyAlphaToHex(null, 0.5)).toBeNull();
expect(applyAlphaToHex(undefined, 0.5)).toBeUndefined();
expect(applyAlphaToHex(123, 0.5)).toBe(123);
});

it('handles case-insensitive hex colors', () => {
expect(applyAlphaToHex('#AABBCC', 0.5)).toBe('#AABBCC80');
expect(applyAlphaToHex('#AbCdEf', 0.5)).toBe('#AbCdEf80');
});
});

describe('comments plugin commands', () => {
Expand Down
56 changes: 56 additions & 0 deletions packages/superdoc/src/SuperDoc.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -832,4 +832,60 @@ describe('SuperDoc.vue', () => {
expect(setupState.toolsMenuPosition.top).toBeNull();
expect(wrapper.vm.showToolsFloatingMenu).toBeFalsy();
});

it('merges partial trackChangeActiveHighlightColors with base colors', async () => {
const superdocStub = createSuperdocStub();
superdocStub.config.modules.comments = {
trackChangeHighlightColors: {
insertBorder: '#00ff00',
deleteBackground: '#0000ff',
},
trackChangeActiveHighlightColors: {
insertBorder: '#ff0000', // only override this one
},
};

const wrapper = await mountComponent(superdocStub);
await nextTick();

const styleVars = wrapper.vm.superdocStyleVars;

// Active insertBorder should be overridden
expect(styleVars['--sd-track-insert-border']).toBe('#ff0000');
// deleteBackground should be inherited from base config
expect(styleVars['--sd-track-delete-bg']).toBe('#0000ff');
});

it('sets track change CSS vars from base config when no active config provided', async () => {
const superdocStub = createSuperdocStub();
superdocStub.config.modules.comments = {
trackChangeHighlightColors: {
insertBorder: '#11ff11',
deleteBorder: '#ff1111',
formatBorder: '#1111ff',
},
};

const wrapper = await mountComponent(superdocStub);
await nextTick();

const styleVars = wrapper.vm.superdocStyleVars;

expect(styleVars['--sd-track-insert-border']).toBe('#11ff11');
expect(styleVars['--sd-track-delete-border']).toBe('#ff1111');
expect(styleVars['--sd-track-format-border']).toBe('#1111ff');
});

it('sets comment highlight hover color CSS var', async () => {
const superdocStub = createSuperdocStub();
superdocStub.config.modules.comments = {
highlightHoverColor: '#abcdef88',
};

const wrapper = await mountComponent(superdocStub);
await nextTick();

const styleVars = wrapper.vm.superdocStyleVars;
expect(styleVars['--sd-comment-highlight-hover']).toBe('#abcdef88');
});
});
Loading