diff --git a/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx b/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx index de314ad19f7..fafc45e6dda 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/message.test.tsx @@ -11,7 +11,9 @@ vi.mock('@sim/emcn', () => ({ {children} ), + Check: () => null, Duplicate: () => null, + ShimmerText: ({ children }: { children?: React.ReactNode }) => {children}, Tooltip: { Provider: ({ children }: { children?: React.ReactNode }) => <>{children}, Root: ({ children }: { children?: React.ReactNode }) => <>{children}, @@ -19,6 +21,7 @@ vi.mock('@sim/emcn', () => ({ 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', () => ({ diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx index 4bc151bce2a..21874c6c0aa 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/tool-call-item.test.tsx @@ -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' @@ -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 }) => {children}, -})) - describe('ToolCallItem', () => { beforeEach(() => { vi.clearAllMocks() diff --git a/apps/sim/components/agent-stream/agent-stream-chrome.test.tsx b/apps/sim/components/agent-stream/agent-stream-chrome.test.tsx index 66787de6fda..c9aa77228be 100644 --- a/apps/sim/components/agent-stream/agent-stream-chrome.test.tsx +++ b/apps/sim/components/agent-stream/agent-stream-chrome.test.tsx @@ -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, @@ -34,6 +27,10 @@ vi.mock('@/components/ui', () => ({ }, })) +vi.mock('@/lib/mothership/tools/tool-display', () => ({ + humanizeToolName: (name: string) => name, +})) + import { AgentStreamThinkingChrome, AgentStreamToolCallsChrome, diff --git a/apps/sim/components/agent-stream/agent-stream-chrome.tsx b/apps/sim/components/agent-stream/agent-stream-chrome.tsx index ed91d7d1ae1..fd6d86886cb 100644 --- a/apps/sim/components/agent-stream/agent-stream-chrome.tsx +++ b/apps/sim/components/agent-stream/agent-stream-chrome.tsx @@ -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. */ diff --git a/apps/sim/components/ui/activity-status.tsx b/apps/sim/components/ui/activity-status.tsx index 5c45b2b1b14..1e99f65b0a7 100644 --- a/apps/sim/components/ui/activity-status.tsx +++ b/apps/sim/components/ui/activity-status.tsx @@ -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' diff --git a/apps/sim/components/ui/index.ts b/apps/sim/components/ui/index.ts index f343ed3b309..55e7b018e90 100644 --- a/apps/sim/components/ui/index.ts +++ b/apps/sim/components/ui/index.ts @@ -13,5 +13,4 @@ export { SelectTrigger, SelectValue, } from './select' -export { ShimmerText } from './shimmer-text' export { ThinkingLoader, type ThinkingLoaderVariant } from './thinking-loader' diff --git a/apps/sim/components/ui/shimmer-text.tsx b/apps/sim/components/ui/shimmer-text.tsx deleted file mode 100644 index 0b6bc621a85..00000000000 --- a/apps/sim/components/ui/shimmer-text.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import type { ComponentPropsWithoutRef, ElementType } from 'react' -import { cn } from '@sim/emcn' -import styles from '@/components/ui/shimmer-text.module.css' - -type ShimmerTextProps = { - as?: T - children: React.ReactNode - className?: string -} & Omit, 'as' | 'children' | 'className'> - -/** - * Sweeping-highlight shimmer over a text phrase — the same treatment as the - * ThinkingLoader's "Thinking" label, reusable on any active/streaming row. - * Size and weight come from the consumer's className; the gradient replaces - * the text color, so color classes are ignored while shimmering. - */ -export function ShimmerText({ - as, - children, - className, - ...props -}: ShimmerTextProps) { - const Comp = as ?? 'span' - return ( - - {children} - - ) -} diff --git a/apps/sim/components/ui/thinking-loader.module.css b/apps/sim/components/ui/thinking-loader.module.css index 52703157b5e..c63b730fa36 100644 --- a/apps/sim/components/ui/thinking-loader.module.css +++ b/apps/sim/components/ui/thinking-loader.module.css @@ -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; } diff --git a/apps/sim/components/ui/thinking-loader.tsx b/apps/sim/components/ui/thinking-loader.tsx index 660a9d476ad..4d93f1c9c62 100644 --- a/apps/sim/components/ui/thinking-loader.tsx +++ b/apps/sim/components/ui/thinking-loader.tsx @@ -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 = [ @@ -532,11 +532,19 @@ export function ThinkingLoader({ {exitingLabel ? ( - {exitingLabel} + {shimmer ? ( + {exitingLabel} + ) : ( + {exitingLabel} + )} ) : null} - {shownLabel} + {shimmer ? ( + {shownLabel} + ) : ( + {shownLabel} + )} diff --git a/packages/emcn/src/components/index.ts b/packages/emcn/src/components/index.ts index 975d060b136..03802b0b6b2 100644 --- a/packages/emcn/src/components/index.ts +++ b/packages/emcn/src/components/index.ts @@ -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' diff --git a/apps/sim/components/ui/shimmer-text.module.css b/packages/emcn/src/components/shimmer-text/shimmer-text.module.css similarity index 94% rename from apps/sim/components/ui/shimmer-text.module.css rename to packages/emcn/src/components/shimmer-text/shimmer-text.module.css index fffd7322138..45ab55fbcb7 100644 --- a/apps/sim/components/ui/shimmer-text.module.css +++ b/packages/emcn/src/components/shimmer-text/shimmer-text.module.css @@ -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 diff --git a/packages/emcn/src/components/shimmer-text/shimmer-text.test.tsx b/packages/emcn/src/components/shimmer-text/shimmer-text.test.tsx new file mode 100644 index 00000000000..6a673535253 --- /dev/null +++ b/packages/emcn/src/components/shimmer-text/shimmer-text.test.tsx @@ -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( + + Writing a reply + + ) + expect(html).toMatch(/^
') + }) +}) diff --git a/packages/emcn/src/components/shimmer-text/shimmer-text.tsx b/packages/emcn/src/components/shimmer-text/shimmer-text.tsx new file mode 100644 index 00000000000..37495f79ee7 --- /dev/null +++ b/packages/emcn/src/components/shimmer-text/shimmer-text.tsx @@ -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 = { + as?: T + children: ReactNode + className?: string +} & Omit, '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 + * Working… + */ +export function ShimmerText({ + as, + children, + className, + ...props +}: ShimmerTextProps) { + const Comp = as ?? 'span' + return ( + + {children} + + ) +}