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
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
/**
* @vitest-environment jsdom
*
* The chip label must never carry its own explicit text color — see the comment on `CHIP_CLASS` in
* `mention-chip.tsx`. An element's own explicit `color` always wins over an inherited one regardless
* of ancestor specificity, so hardcoding a color here would silently override any ambient color a
* mention's container legitimately sets (a link's blue, an `h6` heading's dimmer `--text-secondary`) —
* the same bug class already fixed for `strong`/`em`/`code` in `rich-markdown-editor.css`.
*/
import { act } from 'react'
import type { Editor } from '@tiptap/react'
import { createRoot, type Root } from 'react-dom/client'
import { afterEach, describe, expect, it, vi } from 'vitest'

vi.mock('next/navigation', () => ({
useRouter: () => ({ push: vi.fn() }),
useParams: () => ({}),
}))

// Override the global `getAllBlocks: () => ({})` stub — `getIconColorMap` iterates it as an array.
vi.mock('@/blocks/registry', () => ({
getAllBlocks: () => [],
}))

const { MentionChipView } = await import('./mention-chip')

function fakeNode(attrs: Record<string, unknown>) {
return { attrs } as unknown as Parameters<typeof MentionChipView>[0]['node']
}

function fakeEditor(): Editor {
return { storage: { mention: { navigable: false } } } as unknown as Editor
}

let container: HTMLDivElement | null = null
let root: Root | null = null

afterEach(() => {
if (root) act(() => root?.unmount())
container?.remove()
container = null
root = null
})

describe('MentionChipView', () => {
it('renders its wrapper with no explicit text-color utility class', async () => {
;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true
container = document.createElement('div')
document.body.appendChild(container)
root = createRoot(container)

await act(async () => {
root?.render(
MentionChipView({
node: fakeNode({ kind: 'file', id: 'f1', label: 'notes.md' }),
editor: fakeEditor(),
} as Parameters<typeof MentionChipView>[0])
)
})

const chip = container.querySelector('.mention-chip') as HTMLElement
expect(chip).not.toBeNull()

// Any `text-*` utility targeting the wrapper itself — bare, or Tailwind's self-targeting
// `[&]:text-*` arbitrary variant (as opposed to a descendant variant like `[&>svg]:text-*`,
// which the icon rule below legitimately uses) — would regress this fix, not just the specific
// old `text-[var(--text-primary)]` class. Rather than enumerate every Tailwind color-naming
// scheme (arbitrary value, shade-suffixed, semantic theme tokens like `text-primary`/
// `text-muted-foreground`, keywords), flag ANY such token: none is legitimate on this wrapper
// today, so this can only ever be a color utility slipping back in. A genuinely new, non-color
// `text-*` need (e.g. a font-size utility) should fail this test and force an explicit update,
// not be silently allowed through.
const ownTextUtilities = chip.className
.split(/\s+/)
.filter((cls) => cls.startsWith('text-') || cls.startsWith('[&]:text-'))
expect(ownTextUtilities).toEqual([])

// The icon's own monochrome fallback is unrelated and must be untouched by this fix.
expect(chip.className).toContain('[&>svg]:text-[var(--text-icon)]')
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,24 @@ import { simLinkPath } from './sim-link'
* color, and a 12px icon. Integration icons keep their brand color via
* {@link getBareIconStyle} (see {@link MentionChipView}); other kinds stay
* monochrome through the `--text-icon` fallback below.
*
* No explicit label color — an element's own explicit `color` always wins over an inherited one
* regardless of ancestor specificity, so hardcoding `--text-primary` here (redundant with the prose
* default anyway) would silently override any ambient color a ancestor legitimately sets — a link's
* blue, or `h6`'s dimmer `--text-secondary` — since a mention is inline content and can appear inside
* either. Omitting it lets the label inherit correctly in both cases, same fix as `strong`/`em`/`code`
* in rich-markdown-editor.css.
*/
const CHIP_CLASS =
'mention-chip mx-px inline-flex items-center gap-1 align-middle text-[var(--text-primary)] leading-[1.5] [&>svg]:size-[12px] [&>svg]:shrink-0 [&>svg]:text-[var(--text-icon)]'
'mention-chip mx-px inline-flex items-center gap-1 align-middle leading-[1.5] [&>svg]:size-[12px] [&>svg]:shrink-0 [&>svg]:text-[var(--text-icon)]'

/**
* Live chip: an entity icon + label matching the chat input's mention rendering. Where the host opted
* into navigation (the file viewer), Cmd/Ctrl-click routes to the resource; in a modal field it stays
* inert so a click can't navigate away from an unsaved edit. This view pulls the block registry (for
* integration brand icons), so it's kept out of the headless {@link MarkdownMention} module.
*/
function MentionChipView({ node, editor }: ReactNodeViewProps) {
export function MentionChipView({ node, editor }: ReactNodeViewProps) {
const router = useRouter()
const params = useParams()
const { kind, id, label } = node.attrs as MentionAttrs
Expand Down
Loading