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: 12 additions & 2 deletions apps/sim/app/_styles/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,6 @@
*/
--color-amber-50: #fffbeb;
--color-amber-200: #fde68a;
--color-amber-300: #fcd34d;
--color-amber-400: #fbbf24;
--color-amber-500: #f59e0b;
--color-amber-600: #d97706;
Expand All @@ -166,7 +165,6 @@
--color-emerald-400: #34d399;
--color-emerald-500: #10b981;
--color-green-500: #22c55e;
--color-orange-400: #fb923c;
--color-purple-500: #a855f7;
--color-red-50: #fef2f2;
--color-red-300: #fca5a5;
Expand Down Expand Up @@ -235,6 +233,18 @@
--animate-hero-edge-draw: hero-edge-draw 520ms ease-out forwards;
}

/** Runtime collaborator colours must exist even without a matching utility class. */
@theme static {
--color-black: #000000;
--color-white: #ffffff;
--color-amber-300: #fcd34d;
--color-orange-400: #fb923c;
--color-pink-400: #f472b6;
--color-purple-400: #c084fc;
--color-violet-500: #8b5cf6;
--color-cool-gray-500: #6b7280;
}

/**
* One period of the running block's hatch (26px horizontal). Shifting by
* exactly one period is what makes the loop seamless.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ export function PresenceAvatars({
style={{ zIndex: 0 }}
aria-label={`${overflowCount} more ${overflowCount === 1 ? 'user' : 'users'}`}
>
<AvatarFallback className='border-0 bg-[#404040] font-semibold text-[7px] text-white leading-none'>
<AvatarFallback className='border-0 bg-gray-700 font-semibold text-[7px] text-white leading-none'>
+{overflowCount}
</AvatarFallback>
</Avatar>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import type { Awareness } from 'y-protocols/awareness'
export const CARET_LABEL_HOLD_MS = 2000

/** Fallback caret color when a peer's awareness carries no `color`. */
export const DEFAULT_CARET_COLOR = '#000000'
export const DEFAULT_CARET_COLOR = 'var(--color-black)'

/**
* The active-state class {@link activateCaretLabel} toggles on the caret node to reveal the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ import {
} from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/raw-markdown-snippet'
import { SlashCommand } from '@/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/slash-command/slash-command'

const FileCollaborationCaret = CollaborationCaret.extend({
addProseMirrorPlugins() {
// Older peers need a resolved colour to apply their selection opacity.
// Resolve at editor mount, before the parent captures and publishes user.
const color = this.options.user.color
const token = typeof color === 'string' ? /^var\((--[\w-]+)\)$/.exec(color)?.[1] : undefined
if (token && typeof document !== 'undefined') {
const resolved = getComputedStyle(document.documentElement).getPropertyValue(token).trim()
if (resolved) this.options.user = { ...this.options.user, color: resolved }
}
return this.parent?.() ?? []
},
})

/** Live collaboration binding for the editor. When present, the editor's history
* is Yjs-backed and remote carets/selection render via CollaborationCaret. */
export interface EditorCollaboration {
Expand Down Expand Up @@ -87,15 +101,15 @@ export function createMarkdownEditorExtensions({
// relayed by the socket provider once connected). `render` tags each caret
// with the peer's client id and shows its name label; the selection tint is
// a translucent fill of the peer's identity color.
CollaborationCaret.configure({
FileCollaborationCaret.configure({
provider: { awareness: collaboration.awareness },
user: collaboration.user,
render: renderCaret,
selectionRender: (user) => {
const hex = typeof user.color === 'string' ? user.color : DEFAULT_CARET_COLOR
const color = typeof user.color === 'string' ? user.color : DEFAULT_CARET_COLOR
return {
class: 'collaboration-carets__selection',
style: `background-color: ${withAlpha(hex, 0.2)};`,
style: `background-color: ${withAlpha(color, 0.2)};`,
}
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -296,3 +296,24 @@ describe('list editing with delayed peer updates', () => {
)
})
})

it('publishes resolved global colours so older peers keep translucent selections', () => {
const doc = new Y.Doc()
const awareness = new Awareness(doc)
const user = { name: 'User', color: 'var(--color-pink-400)' }
const extensions = createMarkdownEditorExtensions({
placeholder: '',
collaboration: { doc, awareness, user },
})
document.documentElement.style.setProperty('--color-pink-400', '#f472b6')
const editor = new Editor({ extensions })
cleanups.push(() => {
editor.destroy()
awareness.destroy()
doc.destroy()
document.documentElement.style.removeProperty('--color-pink-400')
})

expect(awareness.getLocalState()?.user.color).toBe('#f472b6')
expect(user.color).toBe('var(--color-pink-400)')
})
Original file line number Diff line number Diff line change
Expand Up @@ -440,7 +440,7 @@
* text without ever shifting the text (or any following text) as the highlight is applied/removed.
*/
.rich-markdown-nodes mark {
background-color: rgba(255, 212, 0, 0.4);
background-color: color-mix(in srgb, var(--color-amber-400) 40%, transparent);
color: inherit;
border-radius: 2px;
padding: 0 0.1em;
Expand Down Expand Up @@ -604,5 +604,5 @@
* its solid fill with a fixed dark ink for exactly this reason. */
.rich-markdown-nodes .rich-find-match-active {
background-color: var(--brand-secondary);
color: #000;
color: var(--color-black);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/** Tailwind class applied to selected rows / columns / cells. */
export const SELECTION_TINT_BG = 'bg-[rgba(37,99,235,0.06)]'
export const SELECTION_TINT_BG = 'bg-[color-mix(in_srgb,var(--selection)_6%,transparent)]'

/**
* Fill marking every cell matching the active find query. Reuses the app's
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,14 @@ export const DiffControls = memo(function DiffControls() {
width: '2px',
transform: 'skewX(-18.4deg)',
background:
'linear-gradient(to right, var(--border) 50%, color-mix(in srgb, var(--brand-accent) 70%, black) 50%)',
'linear-gradient(to right, var(--border) 50%, color-mix(in srgb, var(--brand-accent) 70%, var(--color-black)) 50%)',
}}
/>
{/* Accept side */}
<button
onClick={handleAccept}
title='Accept changes (⇧⌘⏎)'
className='-ml-2.5 relative flex h-full items-center border border-[rgba(0,0,0,0.15)] bg-[var(--brand-accent)] pr-3 pl-5 text-[var(--text-inverse)] text-small transition-[background-color,border-color,fill,stroke] hover-hover:brightness-110 dark:border-[rgba(255,255,255,0.1)]'
className='-ml-2.5 relative flex h-full items-center border border-black/15 bg-[var(--brand-accent)] pr-3 pl-5 text-[var(--text-inverse)] text-small transition-[background-color,border-color,fill,stroke] hover-hover:brightness-110 dark:border-white/10'
style={{
clipPath: 'polygon(10px 0, 100% 0, 100% 100%, 0 100%)',
borderRadius: '0 4px 4px 0',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -434,7 +434,7 @@ function ConnectionsSection({
handleKeyboardActivation(event, () => setExpandedVariables(!expandedVariables))
}
>
<div className='relative flex size-[14px] shrink-0 items-center justify-center overflow-hidden rounded-sm bg-[#8B5CF6]'>
<div className='relative flex size-[14px] shrink-0 items-center justify-center overflow-hidden rounded-sm bg-violet-500'>
<span className='text-[9px] text-white'>V</span>
</div>
<OverflowText
Expand Down Expand Up @@ -483,7 +483,7 @@ function ConnectionsSection({
handleKeyboardActivation(event, () => setExpandedEnvVars(!expandedEnvVars))
}
>
<div className='relative flex size-[14px] shrink-0 items-center justify-center overflow-hidden rounded-sm bg-[#6B7280]'>
<div className='relative flex size-[14px] shrink-0 items-center justify-center overflow-hidden rounded-sm bg-cool-gray-500'>
<span className='text-[9px] text-white'>E</span>
</div>
<OverflowText
Expand Down
14 changes: 12 additions & 2 deletions apps/sim/components/ui/shimmer-text.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@
* `--shimmer-rest` to their resting color.
*/
.shimmer {
background-image: linear-gradient(90deg, #4a4a4a 40%, #b0b0b0 50%, #4a4a4a 60%);
background-image: linear-gradient(
90deg,
var(--text-body) 40%,
var(--color-gray-400) 50%,
var(--text-body) 60%
);
background-size: 200% 100%;
-webkit-background-clip: text;
background-clip: text;
Expand All @@ -17,7 +22,12 @@
}

:global(.dark) .shimmer {
background-image: linear-gradient(90deg, #b9b9b9 40%, #f8f8f8 50%, #b9b9b9 60%);
background-image: linear-gradient(
90deg,
var(--text-body) 40%,
var(--color-gray-50) 50%,
var(--text-body) 60%
);
}

@keyframes shimmer-sweep {
Expand Down
37 changes: 37 additions & 0 deletions apps/sim/lib/workspaces/colors.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { describe, expect, it } from 'vitest'
import { getUserColor, withAlpha } from '@/lib/workspaces/colors'

describe('collaborator colours', () => {
it('keeps the same identity slots when colours move to CSS variables', () => {
expect(['f', 'a', 'b', 'c', 'd', 'e'].map(getUserColor)).toEqual([
'var(--indicator-active)',
'var(--color-pink-400)',
'var(--brand-secondary)',
'var(--color-orange-400)',
'var(--color-purple-400)',
'var(--color-amber-300)',
])
})

it.each([0.08, 0.2, 0.22])('preserves selection opacity %s for CSS colours', (alpha) => {
expect(withAlpha('var(--color-pink-400)', alpha)).toBe(
`color-mix(in srgb, var(--color-pink-400) ${alpha * 100}%, transparent)`
)
})

it('accepts hex colours from peers running an older client', () => {
expect(withAlpha('#F472B6', 0.2)).toBe('rgba(244, 114, 182, 0.2)')
expect(withAlpha('#000', 0.2)).toBe('rgba(0, 0, 0, 0.2)')
})

it('clamps opacity for CSS and hex colours', () => {
expect(withAlpha('var(--color-black)', -1)).toBe(
'color-mix(in srgb, var(--color-black) 0%, transparent)'
)
expect(withAlpha('var(--color-black)', 2)).toBe(
'color-mix(in srgb, var(--color-black) 100%, transparent)'
)
expect(withAlpha('#000000', -1)).toBe('rgba(0, 0, 0, 0)')
expect(withAlpha('#000000', 2)).toBe('rgba(0, 0, 0, 1)')
})
})
34 changes: 19 additions & 15 deletions apps/sim/lib/workspaces/colors.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
import { hexToRgb } from '@/lib/colors'

/**
* User color palette matching terminal.tsx RUN_ID_COLORS
* These colors are used consistently across cursors, avatars, and terminal run IDs
* Shared identity palette for cursors, avatars, and collaborator selections.
* Colour values live in globals.css; keep the order stable for user ID hashing.
*/
export const USER_COLORS = [
'#4ADE80', // Green
'#F472B6', // Pink
'#60C5FF', // Blue
'#FF8533', // Orange
'#C084FC', // Purple
'#FCD34D', // Yellow
'var(--indicator-active)', // Green
'var(--color-pink-400)',
'var(--brand-secondary)', // Blue
'var(--color-orange-400)',
'var(--color-purple-400)',
'var(--color-amber-300)',
] as const

const HEX_COLOR_REGEX = /^#(?:[0-9a-fA-F]{3}){1,2}$/
Expand All @@ -27,21 +27,25 @@ function hashIdentifier(identifier: string | number): number {
return 0
}

export function withAlpha(hexColor: string, alpha: number): string {
if (!HEX_COLOR_REGEX.test(hexColor)) {
return hexColor
export function withAlpha(color: string, alpha: number): string {
const opacity = Math.min(Math.max(alpha, 0), 1)
if (color.startsWith('var(')) {
return `color-mix(in srgb, ${color} ${opacity * 100}%, transparent)`
}
if (!HEX_COLOR_REGEX.test(color)) {
return color
}

const { r, g, b } = hexToRgb(hexColor)
return `rgba(${r}, ${g}, ${b}, ${Math.min(Math.max(alpha, 0), 1)})`
const { r, g, b } = hexToRgb(color)
return `rgba(${r}, ${g}, ${b}, ${opacity})`
}

/**
* Gets a consistent color for a user based on their ID.
* The same user will always get the same color across cursors, avatars, and terminal.
* The same user will always get the same color across cursors, avatars, and selections.
*
* @param userId - The unique user identifier
* @returns A hex color string
* @returns A CSS colour variable reference
*/
export function getUserColor(userId: string): string {
const hash = hashIdentifier(userId)
Expand Down
Loading