Skip to content
Open
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
Expand Up @@ -11,14 +11,17 @@ vi.mock('@sim/emcn', () => ({
{children}
</button>
),
Check: () => null,
Duplicate: () => null,
ShimmerText: ({ children }: { children?: React.ReactNode }) => <span>{children}</span>,
Tooltip: {
Provider: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
Root: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
Trigger: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
Content: ({ children }: { children?: React.ReactNode }) => <>{children}</>,
},
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
handleKeyboardActivation: vi.fn(),
}))

vi.mock('@/app/(interfaces)/chat/components/message/components/file-download', () => ({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* @vitest-environment jsdom
*/
import { act, type ReactNode, type SVGProps } from 'react'
import { act, type SVGProps } from 'react'
import { createRoot, type Root } from 'react-dom/client'
import { renderToStaticMarkup } from 'react-dom/server'
import { beforeEach, describe, expect, it, vi } from 'vitest'
Expand All @@ -11,10 +11,6 @@ import type { ToolCallData } from '@/app/workspace/[workspaceId]/home/types'
import { notifyBlockOverlayChanged } from '@/blocks/custom/client-overlay'
import { getBlock, getBlockByToolName } from '@/blocks/registry'

vi.mock('@/components/ui', () => ({
ShimmerText: ({ children }: { children: ReactNode }) => <span>{children}</span>,
}))

describe('ToolCallItem', () => {
beforeEach(() => {
vi.clearAllMocks()
Expand Down
11 changes: 4 additions & 7 deletions apps/sim/components/agent-stream/agent-stream-chrome.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,6 @@ import { afterEach, describe, expect, it, vi } from 'vitest'

vi.mock('@sim/emcn', () => ({
cn: (...args: unknown[]) => args.filter(Boolean).join(' '),
}))

vi.mock('@/lib/mothership/tools/tool-display', () => ({
humanizeToolName: (name: string) => name,
}))

vi.mock('@/components/ui', () => ({
ShimmerText: ({
as: Comp = 'span',
children,
Expand All @@ -34,6 +27,10 @@ vi.mock('@/components/ui', () => ({
},
}))

vi.mock('@/lib/mothership/tools/tool-display', () => ({
humanizeToolName: (name: string) => name,
}))

import {
AgentStreamThinkingChrome,
AgentStreamToolCallsChrome,
Expand Down
3 changes: 1 addition & 2 deletions apps/sim/components/agent-stream/agent-stream-chrome.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
'use client'

import { useEffect, useLayoutEffect, useRef, useState } from 'react'
import { cn } from '@sim/emcn'
import { cn, ShimmerText } from '@sim/emcn'
import { Check, ChevronDown, Circle, Square, X } from '@sim/emcn/icons'
import type {
AgentStreamToolCall,
AgentStreamToolStatus,
} from '@/components/agent-stream/tool-call-lifecycle'
import { ShimmerText } from '@/components/ui'
import { humanizeToolName } from '@/lib/mothership/tools/tool-display'

/** Distance from bottom (px) within which we keep following new thinking text. */
Expand Down
3 changes: 1 addition & 2 deletions apps/sim/components/ui/activity-status.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { ReactNode } from 'react'
import { cn, OverflowText } from '@sim/emcn'
import { ShimmerText } from '@/components/ui/shimmer-text'
import { cn, OverflowText, ShimmerText } from '@sim/emcn'

/** The icon column every activity row shares, sized for the default 14px icon. */
export const ACTIVITY_ICON_SLOT_CLASS = 'flex size-[14px] shrink-0 items-center justify-center'
Expand Down
1 change: 0 additions & 1 deletion apps/sim/components/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,4 @@ export {
SelectTrigger,
SelectValue,
} from './select'
export { ShimmerText } from './shimmer-text'
export { ThinkingLoader, type ThinkingLoaderVariant } from './thinking-loader'
29 changes: 0 additions & 29 deletions apps/sim/components/ui/shimmer-text.tsx

This file was deleted.

7 changes: 2 additions & 5 deletions apps/sim/components/ui/thinking-loader.module.css
Original file line number Diff line number Diff line change
Expand Up @@ -79,12 +79,9 @@
white-space: nowrap;
}

/* The sweeping-band treatment itself (gradient, timing, dark mode, reduced
motion) is owned by the shared shimmer-text module; this class only adds the
loader-scaled font sizing. Canonical normal weight per emcn rules: body text
is 400, never medium. */
/* ShimmerText owns the gradient, timing, dark mode, and reduced motion. This
class only adds loader-scaled font sizing and canonical normal weight. */
.label {
composes: shimmer from "./shimmer-text.module.css";
font-size: var(--tl-label-size, 14px);
font-weight: 400;
}
Expand Down
14 changes: 11 additions & 3 deletions apps/sim/components/ui/thinking-loader.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
'use client'

import { type CSSProperties, type ReactNode, useEffect, useId, useState } from 'react'
import { cn } from '@sim/emcn'
import { cn, ShimmerText } from '@sim/emcn'
import styles from '@/components/ui/thinking-loader.module.css'

const VARIANTS = [
Expand Down Expand Up @@ -532,11 +532,19 @@ export function ThinkingLoader({
<span className={styles.labelStack}>
{exitingLabel ? (
<span key={exitingLabel} className={cn(styles.labelLayer, styles.labelOut)}>
<span className={shimmer ? styles.label : styles.labelStatic}>{exitingLabel}</span>
{shimmer ? (
<ShimmerText className={styles.label}>{exitingLabel}</ShimmerText>
) : (
<span className={styles.labelStatic}>{exitingLabel}</span>
)}
</span>
) : null}
<span key={shownLabel} className={cn(styles.labelLayer, styles.labelIn)}>
<span className={shimmer ? styles.label : styles.labelStatic}>{shownLabel}</span>
{shimmer ? (
<ShimmerText className={styles.label}>{shownLabel}</ShimmerText>
) : (
<span className={styles.labelStatic}>{shownLabel}</span>
)}
</span>
</span>
</output>
Expand Down
1 change: 1 addition & 0 deletions packages/emcn/src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ export { ProgressItem } from './progress-item/progress-item'
export { RowActions, type RowActionsProps, rowActionsGroupClass } from './row-actions/row-actions'
export { SecretInput } from './secret-input/secret-input'
export { SecretReveal } from './secret-reveal/secret-reveal'
export { ShimmerText, type ShimmerTextProps } from './shimmer-text/shimmer-text'
export { WORDMARK_PATHS, WORDMARK_VIEW_BOX } from './sim-wordmark/paths'
export { SimWordmark, type SimWordmarkProps } from './sim-wordmark/sim-wordmark'
export { Skeleton } from './skeleton/skeleton'
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
/**
* Claude-style text shimmer: the text paints from a gradient with a light band
* that sweeps across the glyphs via background-clip. This is the single source
* of truth for the treatment — the ThinkingLoader label composes it. Under
* of truth for the treatment, including the ThinkingLoader label. Under
* reduced motion the sweep is replaced by a gentle opacity pulse in solid ink:
* the shimmer conveys essential in-progress state, so it needs a vestibular-safe
* fallback rather than none. Consumers whose resting text is not body ink set
Expand Down
19 changes: 19 additions & 0 deletions packages/emcn/src/components/shimmer-text/shimmer-text.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { renderToStaticMarkup } from 'react-dom/server'
import { describe, expect, it } from 'vitest'
import { ShimmerText } from './shimmer-text'
import styles from './shimmer-text.module.css'

describe('ShimmerText', () => {
it('preserves the requested element, consumer classes, and native attributes', () => {
const html = renderToStaticMarkup(
<ShimmerText as='div' className='text-sm' data-testid='streaming-body'>
Writing a reply
</ShimmerText>
)
expect(html).toMatch(/^<div /)
expect(html).toContain(styles.shimmer)
expect(html).toContain('text-sm')
expect(html).toContain('data-testid="streaming-body"')
expect(html).toContain('Writing a reply</div>')
Comment thread
greptile-apps[bot] marked this conversation as resolved.
})
})
30 changes: 30 additions & 0 deletions packages/emcn/src/components/shimmer-text/shimmer-text.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ComponentPropsWithoutRef, ElementType, ReactNode } from 'react'
import { cn } from '../../lib/cn'
import styles from './shimmer-text.module.css'

export type ShimmerTextProps<T extends ElementType = 'span'> = {
as?: T
children: ReactNode
className?: string
} & Omit<ComponentPropsWithoutRef<T>, 'as' | 'children' | 'className'>

/**
* Sweeping-highlight shimmer for active text. Size and weight come from the
* consumer; `--shimmer-rest` supplies resting ink under reduced motion.
*
* @example
* <ShimmerText className='text-sm'>Working…</ShimmerText>
*/
export function ShimmerText<T extends ElementType = 'span'>({
as,
children,
className,
...props
}: ShimmerTextProps<T>) {
const Comp = as ?? 'span'
return (
<Comp className={cn(styles.shimmer, className)} {...props}>
{children}
</Comp>
)
}
Loading