diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index fd57ee5c00c..0342cadb8cb 100644 --- a/.claude/rules/emcn-components.md +++ b/.claude/rules/emcn-components.md @@ -6,14 +6,14 @@ paths: # EMCN Components -Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`/`ChipDropdown`, `Switch`→`ChipSwitch`, date field→`ChipDatePicker`). For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover). +Import components, `cn`, and tokens from the `@sim/emcn` barrel; icons come from the `@sim/emcn/icons` subpath, and CSS modules from their file path. Never deep-import other component subpaths. The **chip family** is the platform's primary chrome — always reach for it over the legacy primitives it is progressively replacing (`Input`→`ChipInput`, `Textarea`→`ChipTextarea`, `Modal`→`ChipModal`, `Select`/`Combobox`→`ChipSelect`/`ChipCombobox`, `Switch`→`ChipSwitch`, date field→`ChipDatePicker`). For context/action menus the canonical control is `DropdownMenu` — the standard menu (not a chip, and never a hand-rolled popover). ## Chip chrome — single source of truth Never hand-roll the chip pill from raw class strings (they go stale). Compose from the canonical sources: - **Surface, typography + content tokens:** `chip/chip-chrome.ts` — `chipFilledSurfaceTokens`, `chipFieldSurfaceClass`, `chipFieldTextClass` (text fields and the dropdown search box build on these), plus the chip-content chrome `chipContentGap`, `chipGeometryClass`, `chipContentIconClass`, `chipContentLabelClass`, `cellIconNodeClass` (non-chip surfaces that must visually match chip content, e.g. resource table cells), and the row-state pair `chipHoverSurfaceClass` / `chipActiveSurfaceClass` (hover vs. selected — mutually exclusive, so a selected row holds its surface through hover; every hand-rolled row imports these rather than restating the literals). All are re-exported from the `@sim/emcn` barrel — no subpath import needed. -- **Pill geometry:** `chip/chip.tsx` — `chipVariants` (30px tall, `rounded-lg`, `px-2`, icon↔text `gap-1.5`). Every pill-shaped trigger (`ChipDropdown`, `ChipSelect`, `ChipSwitch`) reuses it for visual parity. +- **Pill geometry:** `chip/chip.tsx` — `chipVariants` (30px tall, `rounded-lg`, `px-2`, icon↔text `gap-1.5`). Every pill-shaped trigger (`ChipSelect`, `ChipDatePicker`) reuses it for visual parity. Canonical look: normal font-weight (never `font-medium`/`font-semibold`), value text `--text-body`, icons `--text-icon` at `size-[14px]`, placeholder `--text-muted`, `transition-colors`, **no focus ring** (the caret marks focus). Filled surface is `--surface-5` light / `--surface-4` dark with a `--border-1` border. @@ -24,15 +24,15 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items - **`Chip` / `ChipLink`** — the pill button (` + ), +})) +vi.mock('@/app/(auth)/components/auth-button-classes', () => ({ AUTH_TEXT_LINK: '' })) +vi.mock('@/components/auth/public-auth-header', () => ({ + PublicAuthHeader: ({ title }: { title: string }) =>

{title}

, +})) +vi.mock('@/app/f/[token]/public-file-auth-shell', () => ({ + PublicFileAuthShell: ({ children }: { children: ReactNode }) =>
{children}
, +})) +vi.mock('next/navigation', () => ({ useRouter: () => ({ refresh: mocks.refresh }) })) +vi.mock('@/hooks/queries/chats', () => ({ + useChatEmailOtpRequest: () => ({ mutateAsync: mocks.chatRequest, isPending: false }), + useChatEmailOtpVerify: () => ({ mutateAsync: mocks.chatVerify, isPending: false }), +})) +vi.mock('@/hooks/queries/public-shares', () => ({ + usePublicFileOtpRequest: () => ({ mutateAsync: mocks.fileRequest, isPending: false }), + usePublicFileOtpVerify: () => ({ mutateAsync: mocks.fileVerify, isPending: false }), +})) + +import EmailAuth from '@/app/(interfaces)/chat/components/auth/email/email-auth' +import { PublicFileEmailAuth } from '@/app/f/[token]/public-file-email-auth' + +let root: Root +let container: HTMLDivElement + +function changeInput(input: HTMLInputElement, value: string) { + Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set?.call(input, value) + input.dispatchEvent(new Event('input', { bubbles: true })) +} + +function button(label: string) { + const found = Array.from(container.querySelectorAll('button')).find( + (candidate) => candidate.textContent?.trim() === label + ) + if (!found) throw new Error(`Missing button: ${label}`) + return found +} + +function expectOtpInvalid(invalid: boolean) { + expect(container.querySelector('[data-testid="otp-code"]')?.getAttribute('aria-invalid')).toBe( + String(invalid) + ) + const slots = container.querySelectorAll('[data-otp-slot]') + expect(slots).toHaveLength(6) + for (const slot of slots) expect(slot.getAttribute('data-invalid')).toBe(String(invalid)) +} + +beforeEach(() => { + vi.clearAllMocks() + vi.stubGlobal('IS_REACT_ACT_ENVIRONMENT', true) + mocks.chatRequest.mockResolvedValue({}) + mocks.chatVerify.mockResolvedValue({}) + mocks.fileRequest.mockResolvedValue({}) + mocks.fileVerify.mockResolvedValue({}) + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) +}) + +afterEach(() => { + act(() => root.unmount()) + container.remove() + vi.unstubAllGlobals() +}) + +describe('OTP error provenance', () => { + it('keeps the chat code valid on resend failure and marks only a failed verification invalid', async () => { + act(() => root.render()) + act(() => + changeInput(container.querySelector('#email')!, 'member@example.com') + ) + await act(async () => button('Continue').click()) + + mocks.chatRequest.mockRejectedValueOnce(new Error('Delivery failed')) + await act(async () => button('Resend').click()) + expect(container.textContent).toContain('Delivery failed') + expectOtpInvalid(false) + + mocks.chatVerify.mockRejectedValueOnce(new Error('Incorrect code')) + await act(async () => + changeInput(container.querySelector('[data-testid="otp-code"]')!, '123456') + ) + expect(container.textContent).toContain('Incorrect code') + expectOtpInvalid(true) + }) + + it('keeps the public-file code valid on resend failure and marks only a failed verification invalid', async () => { + act(() => root.render()) + act(() => + changeInput(container.querySelector('#email')!, 'member@example.com') + ) + await act(async () => button('Continue').click()) + + mocks.fileRequest.mockRejectedValueOnce(new Error('Delivery failed')) + await act(async () => button('Resend').click()) + expect(container.textContent).toContain('Delivery failed') + expectOtpInvalid(false) + + mocks.fileVerify.mockRejectedValueOnce(new Error('Incorrect code')) + await act(async () => + changeInput(container.querySelector('[data-testid="otp-code"]')!, '123456') + ) + expect(container.textContent).toContain('Incorrect code') + expectOtpInvalid(true) + }) +}) diff --git a/apps/sim/app/(auth)/verify/verify-content.tsx b/apps/sim/app/(auth)/verify/verify-content.tsx index 88d1c2e9d67..3e45a82f3fb 100644 --- a/apps/sim/app/(auth)/verify/verify-content.tsx +++ b/apps/sim/app/(auth)/verify/verify-content.tsx @@ -1,7 +1,7 @@ 'use client' import { Suspense, useEffect, useState } from 'react' -import { cn, InputOTP, InputOTPGroup, InputOTPSlot } from '@sim/emcn' +import { InputOTP, InputOTPGroup, InputOTPSlot } from '@sim/emcn' import { POST_AUTH_REDIRECT_STORAGE_KEY } from '@/app/(auth)/auth-redirect' import { AuthFormMessage, @@ -84,14 +84,16 @@ function VerificationForm({

- + {OTP_SLOTS.map((index) => ( - + ))} @@ -149,8 +151,8 @@ function VerificationFormFallback() { return (
-
-
+
+
) diff --git a/apps/sim/app/(interfaces)/chat/[identifier]/loading.tsx b/apps/sim/app/(interfaces)/chat/[identifier]/loading.tsx index 405a06bc0e7..8393900a280 100644 --- a/apps/sim/app/(interfaces)/chat/[identifier]/loading.tsx +++ b/apps/sim/app/(interfaces)/chat/[identifier]/loading.tsx @@ -8,10 +8,10 @@ export default function ChatLoading() {
- - + +
- +
@@ -24,16 +24,16 @@ export default function ChatLoading() {
- +
- +
- +
diff --git a/apps/sim/app/(interfaces)/chat/components/auth/email/email-auth.tsx b/apps/sim/app/(interfaces)/chat/components/auth/email/email-auth.tsx index 8710f405311..190dfbaf917 100644 --- a/apps/sim/app/(interfaces)/chat/components/auth/email/email-auth.tsx +++ b/apps/sim/app/(interfaces)/chat/components/auth/email/email-auth.tsx @@ -1,9 +1,10 @@ 'use client' import { useEffect, useState } from 'react' -import { cn, Input, InputOTP, InputOTPGroup, InputOTPSlot, Label } from '@sim/emcn' +import { ChipInput, cn, InputOTP, InputOTPGroup, InputOTPSlot, Label } from '@sim/emcn' import { createLogger } from '@sim/logger' import { toError } from '@sim/utils/errors' +import { PublicAuthHeader } from '@/components/auth/public-auth-header' import { quickValidateEmail } from '@/lib/messaging/email/validation' import { AuthSubmitButton } from '@/app/(auth)/components' import { AUTH_TEXT_LINK } from '@/app/(auth)/components/auth-button-classes' @@ -33,13 +34,17 @@ const validateEmailField = (emailValue: string): string[] => { export default function EmailAuth({ identifier }: EmailAuthProps) { const [email, setEmail] = useState('') - const [authError, setAuthError] = useState(null) + const [authError, setAuthError] = useState<{ + kind: 'verification' | 'request' + message: string + } | null>(null) const [emailErrors, setEmailErrors] = useState([]) const hasEmailError = emailErrors.length > 0 const [showOtpVerification, setShowOtpVerification] = useState(false) const [otpValue, setOtpValue] = useState('') const [countdown, setCountdown] = useState(0) + const isInvalidOtp = authError?.kind === 'verification' const requestOtp = useChatEmailOtpRequest(identifier) const verifyOtp = useChatEmailOtpVerify(identifier) @@ -88,7 +93,10 @@ export default function EmailAuth({ identifier }: EmailAuthProps) { await verifyOtp.mutateAsync({ email, otp: codeToVerify }) } catch (error) { logger.error('Error verifying OTP:', error) - setAuthError(toError(error).message || 'Invalid verification code') + setAuthError({ + kind: 'verification', + message: toError(error).message || 'Invalid verification code', + }) } } @@ -101,7 +109,10 @@ export default function EmailAuth({ identifier }: EmailAuthProps) { setOtpValue('') } catch (error) { logger.error('Error resending OTP:', error) - setAuthError(toError(error).message || 'Failed to resend verification code') + setAuthError({ + kind: 'request', + message: toError(error).message || 'Failed to resend verification code', + }) setCountdown(0) } } @@ -110,16 +121,14 @@ export default function EmailAuth({ identifier }: EmailAuthProps) {
-
-

- {showOtpVerification ? 'Verify Your Email' : 'Email Verification'} -

-

- {showOtpVerification + -

+ : 'This chat requires email verification' + } + />
{!showOtpVerification ? ( @@ -134,7 +143,7 @@ export default function EmailAuth({ identifier }: EmailAuthProps) {
- {hasEmailError && (
@@ -183,15 +191,12 @@ export default function EmailAuth({ identifier }: EmailAuthProps) { } }} disabled={verifyOtp.isPending} - className={cn('gap-2', authError && 'otp-error')} + className={cn('gap-2', isInvalidOtp && 'otp-error')} + aria-invalid={isInvalidOtp} > {[0, 1, 2, 3, 4, 5].map((index) => ( - + ))} @@ -199,7 +204,7 @@ export default function EmailAuth({ identifier }: EmailAuthProps) { {authError && (
-

{authError}

+

{authError.message}

)} @@ -231,7 +236,7 @@ export default function EmailAuth({ identifier }: EmailAuthProps) {

-
+
-
+
- +
) diff --git a/apps/sim/app/(interfaces)/chat/components/input/input.tsx b/apps/sim/app/(interfaces)/chat/components/input/input.tsx index b2c6c8b48f1..af48ca6908a 100644 --- a/apps/sim/app/(interfaces)/chat/components/input/input.tsx +++ b/apps/sim/app/(interfaces)/chat/components/input/input.tsx @@ -130,7 +130,7 @@ export const ChatInput: React.FC<{ {uploadErrors.length > 0 && (
{uploadErrors.map((error, idx) => ( - + {error} ))} @@ -172,7 +172,7 @@ export const ChatInput: React.FC<{ {attachedFiles.map((file) => ( -
+
{file.dataUrl ? ( - + {file.name.split('.').pop()}
@@ -215,7 +215,7 @@ export const ChatInput: React.FC<{ onKeyDown={handleKeyDown} placeholder={isDragOver ? 'Drop files here...' : 'Enter a message...'} rows={1} - className='m-0 h-auto min-h-[24px] w-full resize-none overflow-y-auto overflow-x-hidden border-0 bg-transparent p-1 text-[15px] text-[var(--text-primary)] leading-[24px] caret-[var(--text-primary)] outline-hidden [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0 [&::-webkit-scrollbar]:hidden' + className='m-0 h-auto min-h-[24px] w-full resize-none overflow-y-auto overflow-x-hidden border-0 bg-transparent p-1 text-[var(--text-primary)] text-base leading-[24px] caret-[var(--text-primary)] outline-hidden [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-[var(--text-muted)] focus-visible:ring-0 focus-visible:ring-offset-0 [&::-webkit-scrollbar]:hidden' />
diff --git a/apps/sim/app/(interfaces)/chat/components/loading-state/loading-state.tsx b/apps/sim/app/(interfaces)/chat/components/loading-state/loading-state.tsx index 5f7270578cd..9f4c2c0b0c6 100644 --- a/apps/sim/app/(interfaces)/chat/components/loading-state/loading-state.tsx +++ b/apps/sim/app/(interfaces)/chat/components/loading-state/loading-state.tsx @@ -18,9 +18,9 @@ export function ChatLoadingState() {
- +
- +
diff --git a/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx b/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx index dd6199e063e..616dfda617e 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/components/markdown-renderer.tsx @@ -101,7 +101,7 @@ const COMPONENTS = { }, inlineCode: ({ children }: { children?: React.ReactNode }) => ( - + {children} ), diff --git a/apps/sim/app/(interfaces)/chat/components/message/message.tsx b/apps/sim/app/(interfaces)/chat/components/message/message.tsx index 9458ed16be7..95cb39bf938 100644 --- a/apps/sim/app/(interfaces)/chat/components/message/message.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message/message.tsx @@ -192,7 +192,7 @@ export const ClientChatMessage = memo(function ClientChatMessage({ /> ) : ( <> -
+
{getFileIcon(attachment.type)}
diff --git a/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/loading.tsx b/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/loading.tsx index 37d6027a3cb..e725843d701 100644 --- a/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/loading.tsx +++ b/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/loading.tsx @@ -5,34 +5,34 @@ export default function ResumeLoading() {
- - + +
- + {Array.from({ length: 3 }).map((_, i) => ( - + ))}
-
- - +
+ +
- - + +
- - + +
- - + +
diff --git a/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-page-client.tsx b/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-page-client.tsx index bb09919dc63..b9783db6a80 100644 --- a/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-page-client.tsx +++ b/apps/sim/app/(interfaces)/resume/[workflowId]/[executionId]/resume-page-client.tsx @@ -146,7 +146,7 @@ function renderStructuredValuePreview(value: unknown) { className='max-h-[220px]' /> {truncated && ( -

+

Value truncated for preview ({DISPLAY_VALUE_PREVIEW_MAX_CHARS.toLocaleString()} of{' '} {prettyPrinted.length.toLocaleString()} characters shown).

@@ -158,11 +158,11 @@ function renderStructuredValuePreview(value: unknown) { const { text: stringValue, truncated } = truncateForPreview(String(value)) return (
-
+
{truncated ? `${stringValue}…` : stringValue}
{truncated && ( -

+

Value truncated for preview ({DISPLAY_VALUE_PREVIEW_MAX_CHARS.toLocaleString()} of{' '} {String(value).length.toLocaleString()} characters shown).

@@ -774,24 +774,27 @@ export default function ResumeExecutionPage({ {/* Header */}
-

Paused Execution

-

+

+ Paused Execution +

+

Select a pause point to review and resume

- + Refresh @@ -800,7 +803,7 @@ export default function ResumeExecutionPage({ {/* Main Layout */}
{/* Pause Points List */} -
+
@@ -832,17 +835,17 @@ export default function ResumeExecutionPage({ {/* Detail Panel */}
{loadingDetail && !selectedDetail ? ( -
+
Loading…
) : !selectedContextId ? ( -
+
Select a pause point
) : !selectedDetail ? ( -
+
Could not load details @@ -850,7 +853,7 @@ export default function ResumeExecutionPage({ ) : (
{/* Status Header */} -
+

@@ -868,7 +871,7 @@ export default function ResumeExecutionPage({

{selectedDetail.pausePoint.automaticResumeWaitingReason && ( -
+

{selectedDetail.pausePoint.automaticResumeWaitingReason} @@ -881,7 +884,7 @@ export default function ResumeExecutionPage({ {/* Already resolved - show form fields with submitted values */} {selectedStatus === 'resumed' || selectedStatus === 'failed' ? ( -

+
@@ -932,7 +935,7 @@ export default function ResumeExecutionPage({ <> {/* Display Data */} {responseStructureRows.length > 0 ? ( -
+
@@ -958,7 +961,7 @@ export default function ResumeExecutionPage({
) : ( -
+
@@ -972,7 +975,7 @@ export default function ResumeExecutionPage({ {/* Resume Form */} {isHumanMode && hasInputFormat ? ( -
+
@@ -1001,7 +1004,7 @@ export default function ResumeExecutionPage({
) : ( -
+
@@ -1020,7 +1023,8 @@ export default function ResumeExecutionPage({ placeholder='{"example": "value"}' rows={6} spellCheck={false} - className='min-h-[180px] font-mono' + monospace + className='min-h-[180px]' />
diff --git a/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/stage-block-card.tsx b/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/stage-block-card.tsx index a679aef3881..ad0262f57cd 100644 --- a/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/stage-block-card.tsx +++ b/apps/sim/app/(landing)/components/hero/components/hero-platform-loop/stage-block-card.tsx @@ -373,7 +373,7 @@ export function StageBlockCard({ hasContentBelowHeader ? 'h-[40px]' : 'h-[48px]' )} > - + {block.name} { +interface LandingCtaLinkProps extends Omit { size?: LandingCtaSize variant?: 'primary' | 'outline' /** Adds the animated chevron used by demo actions. */ diff --git a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/enterprise-menu-preview/enterprise-menu-preview.tsx b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/enterprise-menu-preview/enterprise-menu-preview.tsx index 5d187bb7345..0d3ca4fb9d1 100644 --- a/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/enterprise-menu-preview/enterprise-menu-preview.tsx +++ b/apps/sim/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/enterprise-menu-preview/enterprise-menu-preview.tsx @@ -1,4 +1,4 @@ -import { Chip, ChipDropdown, cn } from '@sim/emcn' +import { Chip, ChipSelect, cn } from '@sim/emcn' import { Building, MoreHorizontal, Plus } from '@sim/emcn/icons' import { MenuPreviewFrame } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/menu-preview-frame' import { MenuPreviewHeader } from '@/app/(landing)/components/navbar/components/nav-menu-chip/components/nav-menu-preview/components/menu-preview-header/menu-preview-header' @@ -47,11 +47,14 @@ export function EnterpriseMenuPreview({ layout = 'menu' }: EnterpriseMenuPreview image={null} status='' roleControl={ - } menu={} diff --git a/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx b/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx index 534fa557cdc..787a852a5e1 100644 --- a/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx +++ b/apps/sim/app/(landing)/contact/components/contact-form/contact-form.tsx @@ -2,7 +2,7 @@ import { type ReactNode, useId, useRef, useState } from 'react' import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile' -import { Chip, ChipDropdown, ChipInput, ChipTextarea, Label } from '@sim/emcn' +import { Chip, ChipInput, ChipSelect, ChipTextarea, Label } from '@sim/emcn' import { Check } from '@sim/emcn/icons' import { toError } from '@sim/utils/errors' import { @@ -286,7 +286,10 @@ export function ContactForm() { /> - - - option.value === (organizationRoles[member.email] ?? member.role))?.label ?? 'Owner'}`} value={organizationRoles[member.email] ?? member.role} options={ @@ -76,7 +79,7 @@ export function EnterpriseMembersPreview() { setOrganizationRoles((roles) => ({ ...roles, [member.email]: role })) } disabled={member.role === 'owner'} - matchTriggerWidth={false} + dropdownWidth='content' /> } /> @@ -95,7 +98,10 @@ export function EnterpriseMembersPreview() { image={null} status='' roleControl={ - option.value === (workspaceRoles[member.email] ?? (member.role === 'owner' ? 'admin' : 'write')))?.label}`} value={ workspaceRoles[member.email] ?? (member.role === 'owner' ? 'admin' : 'write') @@ -105,7 +111,7 @@ export function EnterpriseMembersPreview() { setWorkspaceRoles((roles) => ({ ...roles, [member.email]: role })) } disabled={member.role === 'owner'} - matchTriggerWidth={false} + dropdownWidth='content' /> } /> diff --git a/apps/sim/app/(landing)/logs/components/log-history-preview/log-history-preview.tsx b/apps/sim/app/(landing)/logs/components/log-history-preview/log-history-preview.tsx index 2e1dba49f5f..af9574dcb6f 100644 --- a/apps/sim/app/(landing)/logs/components/log-history-preview/log-history-preview.tsx +++ b/apps/sim/app/(landing)/logs/components/log-history-preview/log-history-preview.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useId, useRef, useState } from 'react' -import { Badge, Chip, ChipDropdown, ChipInput } from '@sim/emcn' +import { Badge, Chip, ChipInput, ChipSelect } from '@sim/emcn' import { Library, ListFilter, Search, X } from '@sim/emcn/icons' import { MenuPreviewHeader, @@ -109,13 +109,16 @@ export function LogHistoryPreview() { onChange={(event) => setQuery(event.target.value)} className='mr-auto min-w-0 flex-1' /> - option.value === filter)?.label ?? filter}`} value={filter} options={FILTERS} onChange={setFilter} - matchTriggerWidth={false} + dropdownWidth='content' />
diff --git a/apps/sim/app/(landing)/tables/components/tables-records-preview/components/lead-record-detail/lead-record-detail.tsx b/apps/sim/app/(landing)/tables/components/tables-records-preview/components/lead-record-detail/lead-record-detail.tsx index 9eb6378eb52..7236265ea4b 100644 --- a/apps/sim/app/(landing)/tables/components/tables-records-preview/components/lead-record-detail/lead-record-detail.tsx +++ b/apps/sim/app/(landing)/tables/components/tables-records-preview/components/lead-record-detail/lead-record-detail.tsx @@ -1,11 +1,11 @@ import { useState } from 'react' import { - ChipDropdown, ChipModal, ChipModalBody, ChipModalField, ChipModalFooter, ChipModalHeader, + ChipSelect, } from '@sim/emcn' import { TagIcon, TypeNumber, TypeText } from '@sim/emcn/icons' import type { LeadRecord } from '@/app/(landing)/tables/components/tables-records-preview/data' @@ -65,7 +65,11 @@ export function LeadRecordDetail({ record, onClose, onSave }: LeadRecordDetailPr } > -
-
+
{/* `pl` offsets the trailing letter-space `tracking` adds after the last glyph, which would otherwise pull the code left of optical center. */} - + {request.pairing}
diff --git a/apps/sim/app/cli/auth/loading.tsx b/apps/sim/app/cli/auth/loading.tsx index e6a96d20127..911ae79010e 100644 --- a/apps/sim/app/cli/auth/loading.tsx +++ b/apps/sim/app/cli/auth/loading.tsx @@ -11,11 +11,11 @@ import { AuthShell } from '@/app/(auth)/components' export function CliAuthLoading() { return (
- - - - - + + + + +
) } diff --git a/apps/sim/app/f/[token]/public-file-auth-shell.tsx b/apps/sim/app/f/[token]/public-file-auth-shell.tsx index 24da9e8d470..8953a5a5895 100644 --- a/apps/sim/app/f/[token]/public-file-auth-shell.tsx +++ b/apps/sim/app/f/[token]/public-file-auth-shell.tsx @@ -1,4 +1,5 @@ import type { ReactNode } from 'react' +import { PublicAuthHeader } from '@/components/auth/public-auth-header' import { SupportFooter } from '@/app/(auth)/components/support-footer' import { LogoShell } from '@/app/(landing)/components/logo-shell' @@ -17,14 +18,7 @@ export function PublicFileAuthShell({ title, subtitle, children }: PublicFileAut return ( }>
-
-

- {title} -

-

- {subtitle} -

-
+
{children}
diff --git a/apps/sim/app/f/[token]/public-file-auth.tsx b/apps/sim/app/f/[token]/public-file-auth.tsx index c0682b0d123..cf63af1b2fc 100644 --- a/apps/sim/app/f/[token]/public-file-auth.tsx +++ b/apps/sim/app/f/[token]/public-file-auth.tsx @@ -1,11 +1,10 @@ 'use client' import { useState } from 'react' -import { cn, Input, Label } from '@sim/emcn' -import { Eye, EyeOff } from '@sim/emcn/icons' +import { Label } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { useRouter } from 'next/navigation' -import { AuthSubmitButton } from '@/app/(auth)/components' +import { AuthSubmitButton, PasswordInput } from '@/app/(auth)/components' import { PublicFileAuthShell } from '@/app/f/[token]/public-file-auth-shell' import { usePublicFileAuth } from '@/hooks/queries/public-shares' @@ -21,7 +20,6 @@ export function PublicFileAuth({ token }: PublicFileAuthProps) { const router = useRouter() const authenticate = usePublicFileAuth(token) const [password, setPassword] = useState('') - const [showPassword, setShowPassword] = useState(false) const [error, setError] = useState(null) const handleAuthenticate = async () => { @@ -49,35 +47,21 @@ export function PublicFileAuth({ token }: PublicFileAuthProps) { >
-
- { - setPassword(e.target.value) - setError(null) - }} - className={cn( - 'pr-10', - error && 'border-[var(--text-error)] focus:border-[var(--text-error)]' - )} - /> - -
+ { + setPassword(e.target.value) + setError(null) + }} + error={Boolean(error)} + /> {error ?

{error}

: null}
diff --git a/apps/sim/app/f/[token]/public-file-email-auth.tsx b/apps/sim/app/f/[token]/public-file-email-auth.tsx index 71a5c564492..198fea32ae5 100644 --- a/apps/sim/app/f/[token]/public-file-email-auth.tsx +++ b/apps/sim/app/f/[token]/public-file-email-auth.tsx @@ -1,7 +1,7 @@ 'use client' import { useEffect, useState } from 'react' -import { cn, Input, InputOTP, InputOTPGroup, InputOTPSlot, Label } from '@sim/emcn' +import { ChipInput, cn, InputOTP, InputOTPGroup, InputOTPSlot, Label } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { normalizeEmail } from '@sim/utils/string' import { useRouter } from 'next/navigation' @@ -28,8 +28,12 @@ export function PublicFileEmailAuth({ token }: PublicFileEmailAuthProps) { const [email, setEmail] = useState('') const [otp, setOtp] = useState('') const [sent, setSent] = useState(false) - const [error, setError] = useState(null) + const [error, setError] = useState<{ + kind: 'verification' | 'request' + message: string + } | null>(null) const [countdown, setCountdown] = useState(0) + const isInvalidOtp = error?.kind === 'verification' useEffect(() => { if (countdown <= 0) return @@ -39,7 +43,7 @@ export function PublicFileEmailAuth({ token }: PublicFileEmailAuthProps) { const sendCode = async () => { if (!quickValidateEmail(normalizeEmail(email)).isValid) { - setError('Please enter a valid email address.') + setError({ kind: 'request', message: 'Please enter a valid email address.' }) return } setError(null) @@ -48,7 +52,10 @@ export function PublicFileEmailAuth({ token }: PublicFileEmailAuthProps) { setSent(true) setOtp('') } catch (err) { - setError(getErrorMessage(err, 'Failed to send verification code')) + setError({ + kind: 'request', + message: getErrorMessage(err, 'Failed to send verification code'), + }) } } @@ -59,7 +66,7 @@ export function PublicFileEmailAuth({ token }: PublicFileEmailAuthProps) { await verifyOtp.mutateAsync({ email: normalizeEmail(email), otp: code }) router.refresh() } catch (err) { - setError(getErrorMessage(err, 'Invalid verification code')) + setError({ kind: 'verification', message: getErrorMessage(err, 'Invalid verification code') }) } } @@ -71,7 +78,10 @@ export function PublicFileEmailAuth({ token }: PublicFileEmailAuthProps) { setError(null) } catch (err) { setCountdown(0) - setError(getErrorMessage(err, 'Failed to resend verification code')) + setError({ + kind: 'request', + message: getErrorMessage(err, 'Failed to resend verification code'), + }) } } @@ -90,7 +100,7 @@ export function PublicFileEmailAuth({ token }: PublicFileEmailAuthProps) { >
- - {error ?

{error}

: null} + {error ?

{error.message}

: null}
{[0, 1, 2, 3, 4, 5].map((i) => ( - + ))}
- {error ?

{error}

: null} + {error ? ( +

{error.message}

+ ) : null}
-
+
- - - - - - @@ -344,14 +338,8 @@ export default function PlaygroundPage() { JavaScript - - - Option 1 - Option 2 - - - - + + Option 1 Option 2 @@ -410,9 +398,6 @@ export default function PlaygroundPage() { Amber - - Teal - Cyan @@ -425,7 +410,6 @@ export default function PlaygroundPage() { Small Medium - Large @@ -525,10 +509,7 @@ export default function PlaygroundPage() { Medium (16px) - - - Large (20px) - + @@ -879,7 +860,7 @@ export default function PlaygroundPage() { - + Item 1 diff --git a/apps/sim/app/unsubscribe/loading.tsx b/apps/sim/app/unsubscribe/loading.tsx index 5f625c75bbd..e5a3aa10a8f 100644 --- a/apps/sim/app/unsubscribe/loading.tsx +++ b/apps/sim/app/unsubscribe/loading.tsx @@ -3,11 +3,11 @@ import { Skeleton } from '@sim/emcn' export default function UnsubscribeLoading() { return (
- - - - - + + + + +
) } diff --git a/apps/sim/app/unsubscribe/unsubscribe.tsx b/apps/sim/app/unsubscribe/unsubscribe.tsx index b5f265fd18e..34083d2b4ee 100644 --- a/apps/sim/app/unsubscribe/unsubscribe.tsx +++ b/apps/sim/app/unsubscribe/unsubscribe.tsx @@ -1,13 +1,12 @@ 'use client' import { Suspense } from 'react' -import { Chip, cn, Loader } from '@sim/emcn' +import { Chip, Loader } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { useSearchParams } from 'next/navigation' import type { UnsubscribeType } from '@/lib/api/contracts/user' import { AuthSubmitButton } from '@/app/(auth)/components' -import { AUTH_BUTTON_CLASS } from '@/app/(auth)/components/constants' -import { InviteLayout } from '@/app/invite/components' +import { InviteHeading, InviteLayout } from '@/app/invite/components' import { useUnsubscribe, useUnsubscribeMutation } from '@/hooks/queries/unsubscribe' function UnsubscribeContent() { @@ -39,10 +38,9 @@ function UnsubscribeContent() { if (loading) { return ( -
-

Loading

+

Validating your unsubscribe link…

-
+
@@ -53,12 +51,9 @@ function UnsubscribeContent() { if (error) { return ( -
-

- Invalid Unsubscribe Link -

+

{error}

-
+
window.history.back()} loadingLabel=''> @@ -72,15 +67,12 @@ function UnsubscribeContent() { if (data?.isTransactional) { return ( -
-

- Important Account Emails -

+

Transactional emails like password resets, account confirmations, and security alerts cannot be unsubscribed from as they contain essential information for your account.

-
+
window.close()} loadingLabel=''> @@ -94,15 +86,12 @@ function UnsubscribeContent() { if (unsubscribed) { return ( -
-

- Successfully Unsubscribed -

+

You have been unsubscribed from our emails. You will stop receiving emails within 48 hours.

-
+
window.close()} loadingLabel=''> @@ -117,15 +106,12 @@ function UnsubscribeContent() { return ( -
-

- Email Preferences -

+

Choose which emails you'd like to stop receiving.

{data?.email}

-
+
{data?.currentPreferences.unsubscribeMarketing ? 'Unsubscribed from Marketing' @@ -167,7 +155,9 @@ function UnsubscribeContent() { isAlreadyUnsubscribedFromAll || data?.currentPreferences.unsubscribeUpdates } - className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')} + variant='outline' + size='lg' + align='center' > {data?.currentPreferences.unsubscribeUpdates ? 'Unsubscribed from Updates' @@ -182,7 +172,9 @@ function UnsubscribeContent() { isAlreadyUnsubscribedFromAll || data?.currentPreferences.unsubscribeNotifications } - className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border-1)]')} + variant='outline' + size='lg' + align='center' > {data?.currentPreferences.unsubscribeNotifications ? 'Unsubscribed from Notifications' @@ -205,10 +197,9 @@ export default function Unsubscribe() { -
-

Loading

+

Validating your unsubscribe link…

-
+
diff --git a/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.test.tsx b/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.test.tsx index a3b16c21c7f..b91cc7e471e 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.test.tsx @@ -121,7 +121,7 @@ describe('CodeSearchOverlay', () => { matchCount: 0, currentMatchIndex: 0, }) - expect(overlay.className).toContain('rounded-b-[4px] border-t-0 bg-[var(--bg)]') + expect(overlay.className).toContain('rounded-b-sm border-t-0 bg-[var(--bg)]') expect(overlay.getAttribute('data-toolbar-root')).toBe('true') expect(overlay.getAttribute('data-search-active')).toBe('true') expect(overlay.hasAttribute('role')).toBe(false) diff --git a/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.tsx b/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.tsx index 19247f1dfc7..d8750794cf5 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.tsx @@ -55,7 +55,7 @@ export function CodeSearchOverlay({ role={attached ? undefined : 'presentation'} className={cn( 'absolute z-30 flex h-[34px] items-center gap-1.5 border border-[var(--border)] px-1.5 shadow-xs', - attached ? 'rounded-b-[4px] border-t-0 bg-[var(--bg)]' : 'rounded-sm bg-[var(--surface-1)]', + attached ? 'rounded-b-sm border-t-0 bg-[var(--bg)]' : 'rounded-sm bg-[var(--surface-1)]', className )} onClick={(event) => event.stopPropagation()} diff --git a/apps/sim/app/workspace/[workspaceId]/components/error/error.tsx b/apps/sim/app/workspace/[workspaceId]/components/error/error.tsx index 392c085baae..13ae488488e 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/error/error.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/error/error.tsx @@ -43,10 +43,10 @@ export function ErrorShell({ title, description, icon, children }: ErrorShellPro
-

+

{title}

-

+

{description}

diff --git a/apps/sim/app/workspace/[workspaceId]/components/folders/use-row-drag-ghost.ts b/apps/sim/app/workspace/[workspaceId]/components/folders/use-row-drag-ghost.ts index ee37ca45a8c..a7bc8d07fc3 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/folders/use-row-drag-ghost.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/folders/use-row-drag-ghost.ts @@ -11,7 +11,7 @@ import { useCallback, useEffect, useMemo, useRef } from 'react' * the label inherit the app font and match the row it was lifted from. */ const DRAG_GHOST_STYLE = - 'position:fixed;top:-500px;left:0;display:inline-flex;align-items:center;padding:4px 10px;background:var(--surface-active);border:1px solid var(--border);border-radius:8px;font-size:13px;color:var(--text-body);white-space:nowrap;pointer-events:none;box-shadow:var(--shadow-medium);z-index:var(--z-toast)' + 'position:fixed;top:-500px;left:0;display:inline-flex;align-items:center;padding:4px 10px;background:var(--surface-active);border:1px solid var(--border);border-radius:var(--radius-lg);font-size:13px;color:var(--text-body);white-space:nowrap;pointer-events:none;box-shadow:var(--shadow-medium);z-index:var(--z-toast)' const DRAG_GHOST_LABEL_STYLE = 'max-width:200px;overflow:hidden;text-overflow:ellipsis' diff --git a/apps/sim/app/workspace/[workspaceId]/components/index.ts b/apps/sim/app/workspace/[workspaceId]/components/index.ts index 9791199d107..17df9766567 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/components/index.ts @@ -2,7 +2,6 @@ export { isResourceListEmpty, resourceListState, } from '@/app/workspace/[workspaceId]/components/resource/is-resource-list-empty' -export { ResourceNotFound } from '@/app/workspace/[workspaceId]/components/resource/resource-not-found' export { ConversationListItem } from './conversation-list-item' export type { ErrorBoundaryProps, ErrorStateProps } from './error' export { ErrorShell, ErrorState } from './error' diff --git a/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.test.tsx b/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.test.tsx index f27e865a6a1..ac53da16d2f 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.test.tsx @@ -21,7 +21,7 @@ const { hostContext, mockUseOrganizationBilling, mockUseAdminWorkspaces, mockMut ) vi.mock('@sim/emcn', () => ({ - ChipDropdown: () =>
, + ChipSelect: () =>
, ChipModal: ({ children }: { children: ReactNode }) =>
{children}
, ChipModalBody: ({ children }: { children: ReactNode }) =>
{children}
, ChipModalError: ({ children }: { children: ReactNode }) =>
{children}
, diff --git a/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx b/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx index 090a9096847..2347899cd2f 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/invite-modal/invite-modal.tsx @@ -2,14 +2,14 @@ import { useCallback, useMemo, useState } from 'react' import { - ChipDropdown, - type ChipDropdownOption, ChipModal, ChipModalBody, ChipModalError, ChipModalField, ChipModalFooter, ChipModalHeader, + ChipSelect, + type ChipSelectOption, toast, } from '@sim/emcn' import { createLogger } from '@sim/logger' @@ -161,7 +161,7 @@ export function InviteModal({ { enabled: open && isOrganizationInvite && !organizationOnly } ) - const workspaceOptions = useMemo(() => { + const workspaceOptions = useMemo(() => { if (!isOrganizationInvite) { return workspaceId ? [{ value: workspaceId, label: workspaceName ?? 'This workspace' }] : [] } @@ -317,12 +317,15 @@ export function InviteModal({ {!organizationOnly && ( <> - {initials} @@ -98,7 +98,7 @@ export function PresenceAvatars({ style={{ zIndex: 0 }} aria-label={`${overflowCount} more ${overflowCount === 1 ? 'user' : 'users'}`} > - + +{overflowCount} diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/action-bar/action-bar.tsx index 7ded34f10d0..65442f93310 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/action-bar/action-bar.tsx @@ -2,6 +2,7 @@ import type { ComponentType } from 'react' import { + BulkActionBar, BulkActionButton, cn, DropdownMenu, @@ -91,57 +92,48 @@ export function ResourceActionBar({ className )} > -
- - {exceedsLimit - ? `${selectedCount} selected · select ${maxSelectable} or fewer` - : `${selectedCount} selected`} - -
- {onDownload && ( - - )} - {onMove && moveOptions && ( - - - - - - - - - - Move - - - {renderMoveOptions(moveOptions, onMove)} - - - )} - {onDelete && ( - - )} -
-
+ + {exceedsLimit + ? `${selectedCount} selected · select ${maxSelectable} or fewer` + : `${selectedCount} selected`} + + } + > + {onDownload && ( + + )} + {onMove && moveOptions && ( + + + + + + + + + + Move + + + {renderMoveOptions(moveOptions, onMove)} + + + )} + {onDelete && ( + + )} +
) } diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/components/owner-cell/owner-cell.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/components/owner-cell/owner-cell.tsx index ebbf3bb9e9b..ad14698ae52 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/components/owner-cell/owner-cell.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/components/owner-cell/owner-cell.tsx @@ -1,4 +1,5 @@ import { memo } from 'react' +import { Avatar, AvatarFallback, AvatarImage } from '@sim/emcn' import type { ResourceCell } from '@/app/workspace/[workspaceId]/components/resource/resource' import type { WorkspaceMember } from '@/hooks/queries/workspace' @@ -13,21 +14,11 @@ export interface OwnerAvatarProps { * owner/uploaded-by filter options on every list. */ export const OwnerAvatar = memo(function OwnerAvatar({ name, image }: OwnerAvatarProps) { - if (image) { - return ( - {name} - ) - } - return ( - - {name.charAt(0).toUpperCase()} - + + {image && } + {name.charAt(0).toUpperCase()} + ) }) diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/resource-not-found.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/resource-not-found.tsx deleted file mode 100644 index fa6007c8e02..00000000000 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/resource-not-found.tsx +++ /dev/null @@ -1,27 +0,0 @@ -import type { ComponentType } from 'react' - -interface ResourceNotFoundProps { - icon: ComponentType<{ className?: string }> - title: string - description: string -} - -/** - * Full-page screen for a resource that could not be loaded and has no shell left to - * draw — a knowledge base or a document that was deleted or moved. - * - * Distinct from the `emptyState` slot on {@link Resource.Table}: that one keeps the - * chrome and reports a failure *within* a page that still exists. This replaces the - * page, so it is only right when the thing the page is about is the thing that is gone. - */ -export function ResourceNotFound({ icon: Icon, title, description }: ResourceNotFoundProps) { - return ( -
- -
-

{title}

-

{description}

-
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx index 02cf2f87344..e6037517428 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx @@ -536,12 +536,10 @@ const Pagination = memo(function Pagination({ diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/chart-preview.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/chart-preview.tsx index c99164e58e0..762b82c0391 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/chart-preview.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/chart-preview.tsx @@ -24,8 +24,8 @@ function ChartErrorCard({ message, content }: { message: string; content: string return (
- chart - {message} + chart + {message}
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.css b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.css
index 43e6809375e..43c821a2ce0 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.css
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.css
@@ -54,14 +54,14 @@
   padding: 0.5rem 0.75rem;
   text-align: left;
   vertical-align: top;
-  font-size: 14px;
+  font-size: var(--text-sm);
   line-height: 1.5rem;
 }
 
 .rich-markdown-prose th,
 .document-table th {
   background: var(--surface-4);
-  font-weight: 600;
+  font-weight: var(--font-weight-semibold);
 }
 
 /* Cell editors are invisible until focused: they take the cell's own typography and color. */
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.test.ts
index fda1bbb36da..ff2c67485b5 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.test.ts
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/document-table.test.ts
@@ -141,8 +141,8 @@ describe('document-table chrome is shared with markdown tables', () => {
     const { th, td } = mountTable('document-table')
 
     expect(getComputedStyle(td).getPropertyValue('padding-left')).toBe('0.75rem')
-    expect(getComputedStyle(td).getPropertyValue('font-size')).toBe('14px')
-    expect(getComputedStyle(th).getPropertyValue('font-weight')).toBe('600')
+    expect(getComputedStyle(td).getPropertyValue('font-size')).toBe('var(--text-sm)')
+    expect(getComputedStyle(th).getPropertyValue('font-weight')).toBe('var(--font-weight-semibold)')
   })
 
   it('one rule draws the cell border for both roots', () => {
diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx
index 52407bbf750..c30a44d78d8 100644
--- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/file-viewer.tsx
@@ -427,7 +427,7 @@ const MediaPreview = memo(function MediaPreview({
       
-

{file.name}

+

{file.name}

{blobUrl && ( // biome-ignore lint/a11y/useMediaCaption: audio from workspace files diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx index 10019822f72..81d2f981af1 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/mermaid-diagram.tsx @@ -49,9 +49,9 @@ function MermaidSourcePreview({ return (
- mermaid + mermaid {(isRendering || status) && ( - + {isRendering ? 'Rendering…' : status} )} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/pdf-viewer.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/pdf-viewer.tsx index 598e330ca5a..3b255d082a1 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/pdf-viewer.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/pdf-viewer.tsx @@ -43,7 +43,7 @@ interface PdfViewerCoreProps { function PdfError({ error }: { error: string }) { return (
-

Failed to preview PDF

+

Failed to preview PDF

{error}

) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx index ed585e4c382..1b5ca5f1932 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/preview-shared.tsx @@ -18,7 +18,7 @@ export const UnsupportedPreview = memo(function UnsupportedPreview({ name }: { n return (
-

+

Preview not available{ext ? ` for .${ext} files` : ' for this file'}

@@ -31,7 +31,9 @@ export const UnsupportedPreview = memo(function UnsupportedPreview({ name }: { n export function PreviewError({ label, error }: { label: string; error: string }) { return (

-

Failed to preview {label}

+

+ Failed to preview {label} +

{error}

) diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx index 24897d1edfa..76f43919127 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/image.tsx @@ -288,7 +288,7 @@ export function ResizableImageView({ node, selected, editor, getPos }: ReactNode onPointerDown={startResize} className='absolute right-0 bottom-0 cursor-nwse-resize touch-none items-end justify-end' > - + )} diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/link-mark-precedence.test.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/link-mark-precedence.test.ts index a2edbbae3e5..f5fae887e8f 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/link-mark-precedence.test.ts +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/link-mark-precedence.test.ts @@ -102,11 +102,11 @@ describe('mark WRAPPING a link (`text`) — link color still }) describe('each mark keeps its own non-color styling even when link-colored', () => { - it('bold link keeps font-weight 600', () => { + it('bold link keeps the central semibold weight', () => { const root = mount('bold link') const strong = root.querySelector('strong') as HTMLElement expect(colorOf(strong)).toBe(LINK_COLOR) - expect(getComputedStyle(strong).fontWeight).toBe('600') + expect(getComputedStyle(strong).fontWeight).toBe('var(--font-weight-semibold)') }) it('italic link keeps font-style italic', () => { @@ -144,7 +144,7 @@ describe('multiple marks stacked together with a link', () => { expect(colorOf(em)).toBe(LINK_COLOR) expect(colorOf(strong)).toBe(LINK_COLOR) expect(getComputedStyle(em).fontStyle).toBe('italic') - expect(getComputedStyle(strong).fontWeight).toBe('600') + expect(getComputedStyle(strong).fontWeight).toBe('var(--font-weight-semibold)') }) it('bold + italic + strikethrough + link: link color wins at every nesting level', () => { diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/suggestion-menu-chrome.ts b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/suggestion-menu-chrome.ts index 918934aa0e1..353c615db0d 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/suggestion-menu-chrome.ts +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/suggestion-menu-chrome.ts @@ -17,7 +17,7 @@ export const SUGGESTION_SCROLL_CLASS = 'max-h-[240px] scroll-py-1.5 overflow-y-a /** A selectable row: icon + label, 14px icon in `--text-icon`, truncating label. The `img` rules * size custom-block image icons (rendered as ``, so the `svg` rules never reach them). */ export const SUGGESTION_ITEM_CLASS = - 'relative flex w-full min-w-0 cursor-pointer select-none items-center gap-2 rounded-[5px] px-2 py-1.5 text-left text-[var(--text-body)] text-caption outline-hidden transition-colors [&>span]:min-w-0 [&>span]:truncate [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)] [&_img]:pointer-events-none [&_img]:size-[14px] [&_img]:shrink-0' + 'relative flex w-full min-w-0 cursor-pointer select-none items-center gap-2 rounded-sm px-2 py-1.5 text-left text-[var(--text-body)] text-caption outline-hidden transition-colors [&>span]:min-w-0 [&>span]:truncate [&_svg]:pointer-events-none [&_svg]:size-[14px] [&_svg]:shrink-0 [&_svg]:text-[var(--text-icon)] [&_img]:pointer-events-none [&_img]:size-[14px] [&_img]:shrink-0' /** A group heading above a run of rows. */ export const SUGGESTION_GROUP_LABEL_CLASS = diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/raw-markdown-snippet.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/raw-markdown-snippet.tsx index ea4058895e7..0713cf189c4 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/raw-markdown-snippet.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/raw-markdown-snippet.tsx @@ -3,7 +3,7 @@ import { NodeViewContent, NodeViewWrapper, ReactNodeViewRenderer } from '@tiptap import { FootnoteDef, RawHtmlBlock } from './raw-markdown-snippet-schema' const BLOCK_CONTROL_CLASS = - 'pointer-events-none absolute top-1.5 right-2 select-none rounded-md bg-[var(--surface-4)] px-1.5 py-0.5 text-[10px] text-[var(--text-muted)] uppercase tracking-wide opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100' + 'pointer-events-none absolute top-1.5 right-2 select-none rounded-md bg-[var(--surface-4)] px-1.5 py-0.5 text-micro text-[var(--text-muted)] uppercase tracking-wide opacity-0 transition-opacity group-focus-within:opacity-100 group-hover:opacity-100' /** Badge text per block node type name — kept here rather than threaded through node options since * {@link NodeViewProps} exposes no options/extension reference to the rendering component. */ diff --git a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css index 3b6cc3f2b85..62621d0b2e6 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/rich-markdown-editor.css @@ -18,8 +18,8 @@ outline: none; color: var(--text-primary); font-family: var(--font-inter); - font-size: 15px; - font-weight: 400; + font-size: var(--text-base); + font-weight: var(--font-weight-normal); line-height: 25px; letter-spacing: 0; overflow-wrap: anywhere; @@ -28,7 +28,7 @@ .rich-markdown-nodes img { max-width: 100%; height: auto; - border-radius: 8px; + border-radius: var(--radius-lg); border: var(--border-width) solid var(--border); } @@ -37,7 +37,7 @@ .rich-markdown-nodes .ProseMirror-selectednode { outline: 2px solid var(--brand-secondary); outline-offset: 2px; - border-radius: 4px; + border-radius: var(--radius-sm); } /* An image is its own framed element; ring the image itself so the indicator hugs the picture @@ -73,7 +73,7 @@ .rich-markdown-prose h4, .rich-markdown-prose h5, .rich-markdown-prose h6 { - font-weight: 600; + font-weight: var(--font-weight-semibold); line-height: 1.3; color: var(--text-primary); } @@ -117,7 +117,7 @@ * the surrounding color, not reset it; omitting `color` here lets normal CSS inheritance do that * correctly in every context, including ones this file doesn't know about yet. */ .rich-markdown-prose strong { - font-weight: 600; + font-weight: var(--font-weight-semibold); } .rich-markdown-prose em { @@ -226,7 +226,7 @@ height: 16px; margin: 0; border: var(--border-width) solid var(--border-1); - border-radius: 3px; + border-radius: var(--radius-sm); background: transparent; cursor: pointer; } @@ -258,7 +258,7 @@ font-family: var(--font-martian-mono, ui-monospace, monospace); font-size: 0.875em; background: var(--surface-5); - border-radius: 4px; + border-radius: var(--radius-sm); padding: 0.125rem 0.375rem; } @@ -279,7 +279,7 @@ .rich-markdown-prose pre, .rich-markdown-prose .mermaid-diagram-frame { background: var(--surface-5); - border-radius: 8px; + border-radius: var(--radius-lg); padding: 1rem; overflow-x: auto; } @@ -317,7 +317,7 @@ .rich-markdown-prose pre code { background: none; padding: 0; - font-size: 13px; + font-size: var(--text-small); line-height: 21px; } @@ -341,12 +341,12 @@ } .rich-markdown-nodes .raw-markdown-block { - border-radius: 8px; + border-radius: var(--radius-lg); padding: 0.75rem 1rem; } .rich-markdown-nodes .raw-markdown-inline { - border-radius: 4px; + border-radius: var(--radius-sm); padding: 0.0625rem 0.3rem; } @@ -373,7 +373,7 @@ .rich-markdown-nodes img.rich-leaf-in-selection { outline: 2px solid var(--selection-bg); outline-offset: 2px; - border-radius: 4px; + border-radius: var(--radius-sm); } /* Borders, padding, typography, and header fill come from document-table.css — the chrome shared @@ -442,7 +442,7 @@ .rich-markdown-nodes mark { background-color: color-mix(in srgb, var(--color-amber-400) 40%, transparent); color: inherit; - border-radius: 2px; + border-radius: var(--radius-xs); padding: 0 0.1em; margin: 0 -0.1em; box-decoration-break: clone; @@ -458,8 +458,8 @@ * set their own 600, so only body text and the placeholder are affected. */ .rich-markdown-field-prose { - font-size: 14px; - font-weight: 400; + font-size: var(--text-sm); + font-weight: var(--font-weight-normal); line-height: 22px; } @@ -513,7 +513,7 @@ left: -1px; width: 8px; height: 5px; - border-radius: 2px 2px 2px 0; + border-radius: var(--radius-xs) var(--radius-xs) var(--radius-xs) 0; background-color: var(--caret-color); transition: opacity 0.2s ease; } @@ -551,10 +551,10 @@ max-width: 10rem; overflow: hidden; padding: 0.1rem 0.35rem; - border-radius: 2px 2px 2px 0; + border-radius: var(--radius-xs) var(--radius-xs) var(--radius-xs) 0; /* 11px = the `text-xs` the canvas/tables presence tags use, for pixel parity. */ - font-size: 11px; - font-weight: 500; + font-size: var(--text-xs); + font-weight: var(--font-weight-medium); line-height: 1.2; white-space: nowrap; text-overflow: ellipsis; @@ -576,13 +576,13 @@ .rich-markdown-nodes .collaboration-carets__caret--flip .collaboration-carets__label { left: auto; right: -1px; - border-radius: 2px 2px 0 2px; + border-radius: var(--radius-xs) var(--radius-xs) 0 var(--radius-xs); } /* Remote text selection: a rounded translucent tint of the collaborator's identity * color (the alpha fill is set inline by selectionRender). */ .rich-markdown-nodes .collaboration-carets__selection { - border-radius: 2px; + border-radius: var(--radius-xs); pointer-events: none; } @@ -591,7 +591,7 @@ .rich-markdown-nodes .rich-find-match { background-color: var(--highlight-match-bg); color: var(--highlight-match-text); - border-radius: 2px; + border-radius: var(--radius-xs); } /* The active hit is a stronger fill of the same hue, never a ring: an outline drawn diff --git a/apps/sim/app/workspace/[workspaceId]/files/files.tsx b/apps/sim/app/workspace/[workspaceId]/files/files.tsx index 5c98c2aaeba..bf75f05cdbd 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { - Button, + Chip, ChipCombobox, ChipConfirmModal, Columns2, @@ -2057,13 +2057,9 @@ function FilesContent() {
)} {hasActiveFilters && ( - + )}
) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx index 5bf1c53ac4d..925e6451401 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/agent-group/browser-agent-icon.tsx @@ -127,7 +127,7 @@ function BrowserAgentFavicon({ url, canLoad }: BrowserAgentFaviconProps) { referrerPolicy='no-referrer' alt='' className={cn( - 'size-full rounded-[3px]', + 'size-full rounded-sm', status !== 'loaded' && 'pointer-events-none absolute opacity-0' )} onLoad={() => setStatus('loaded')} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx index 958b6b893bf..0523a7648f0 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/chat-content.tsx @@ -58,7 +58,7 @@ const LANG_ALIASES: Record = { const PROSE_CLASSES = cn( 'prose prose-base dark:prose-invert max-w-none', - 'font-[family-name:var(--font-inter)] antialiased break-words tracking-[0]', + 'font-inter antialiased break-words tracking-[0]', 'prose-headings:font-semibold prose-headings:tracking-[0] prose-headings:text-[var(--text-primary)]', 'prose-headings:mb-3 prose-headings:mt-6 first:prose-headings:mt-0', 'prose-p:text-base prose-p:leading-[25px] prose-p:text-[var(--text-primary)]', @@ -334,7 +334,7 @@ const MARKDOWN_COMPONENTS = { className={cn( 'text-[var(--text-primary)]', kind - ? 'not-prose inline-flex items-baseline gap-1 rounded-[5px] bg-[var(--surface-5)] px-[5px] no-underline transition-colors hover-hover:bg-[var(--surface-6)]' + ? 'not-prose inline-flex items-baseline gap-1 rounded-sm bg-[var(--surface-5)] px-[5px] no-underline transition-colors hover-hover:bg-[var(--surface-6)]' : 'underline decoration-dashed underline-offset-4' )} onClick={(e) => { @@ -415,7 +415,7 @@ const MARKDOWN_COMPONENTS = { }, inlineCode({ children }: { children?: React.ReactNode }) { return ( - + {children} ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/external-link.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/external-link.tsx index fd9661d2151..11829e5bef2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/external-link.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/chat-content/external-link.tsx @@ -71,7 +71,7 @@ export function ExternalLink({ href, hostname, children }: ExternalLinkProps) { diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/message-sources/message-sources.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/message-sources/message-sources.tsx index 305a02b97fe..1f1d029d321 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/message-sources/message-sources.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/message-sources/message-sources.tsx @@ -7,7 +7,7 @@ import type { SourceTagData } from '@/app/workspace/[workspaceId]/home/component /** The action-row button, matching the copy and vote buttons beside it with room for a count. */ const BUTTON_CLASSES = - 'flex h-[26px] items-center gap-1 rounded-[6px] px-1.5 text-[var(--text-icon)] text-caption transition-colors hover-hover:bg-[var(--surface-hover)] focus-visible:outline-hidden data-[state=open]:bg-[var(--surface-active)] data-[state=open]:hover-hover:bg-[var(--surface-active)]' + 'flex h-[26px] items-center gap-1 rounded-md px-1.5 text-[var(--text-icon)] text-caption transition-colors hover-hover:bg-[var(--surface-hover)] focus-visible:outline-hidden data-[state=open]:bg-[var(--surface-active)] data-[state=open]:hover-hover:bg-[var(--surface-active)]' interface MessageSourcesProps { sources: readonly SourceTagData[] diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/options/options.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/options/options.tsx index 080edad2af7..33d543ace0e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/options/options.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/options/options.tsx @@ -15,7 +15,7 @@ export function Options({ items, onSelect }: OptionsProps) { key={item.id} type='button' onClick={() => onSelect?.(item.id)} - className='rounded-full border border-[var(--border)] bg-[var(--bg)] px-3.5 py-1.5 font-[family-name:var(--font-inter)] text-[var(--text-primary)] text-sm leading-5 transition-colors hover-hover:bg-[var(--surface-5)]' + className='rounded-full border border-[var(--border)] bg-[var(--bg)] px-3.5 py-1.5 font-inter text-[var(--text-primary)] text-sm leading-5 transition-colors hover-hover:bg-[var(--surface-5)]' > {item.label} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/resource-mention.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/resource-mention.tsx index a6f685cd35f..0e72032d065 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/resource-mention.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/resource-mention.tsx @@ -9,7 +9,7 @@ interface ResourceMentionProps { export function ResourceMention({ icon, title, onSelect }: ResourceMentionProps) { const classes = - 'inline-flex items-baseline gap-1 rounded-[5px] bg-[var(--surface-5)] px-[5px] align-baseline font-[inherit] text-[inherit] leading-[inherit]' + 'inline-flex items-baseline gap-1 rounded-sm bg-[var(--surface-5)] px-[5px] align-baseline font-[inherit] text-[inherit] leading-[inherit]' const content = ( <> {icon} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-card/source-card.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-card/source-card.tsx index b339c1a91fc..3729be3d831 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-card/source-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-card/source-card.tsx @@ -145,7 +145,7 @@ export function SourceCard({ source, query, onSummarize, dense = false }: Source ) : null diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip.tsx index 8d4540d4b9e..31332c215dd 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/source-chip/source-chip.tsx @@ -75,7 +75,7 @@ export function SourceChip({ source }: SourceChipProps) { ) : null} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index fdd9ed5c7ee..6f39702465d 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx @@ -1095,7 +1095,9 @@ function MessageContentInner({ <>
- Stopped by user + + Stopped by user +
{actions &&
{actionsRow}
} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/components/mothership-chat-skeleton/mothership-chat-skeleton.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/components/mothership-chat-skeleton/mothership-chat-skeleton.tsx index 9b32c21b1dd..8c4714a5c1e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/components/mothership-chat-skeleton/mothership-chat-skeleton.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/components/mothership-chat-skeleton/mothership-chat-skeleton.tsx @@ -27,24 +27,24 @@ export function MothershipChatSkeleton({ return (
- +
- - - - + + + +
- +
- - - + + +
) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx index 07254879b4f..e57b3c8f0f2 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-chat/mothership-chat.tsx @@ -142,7 +142,7 @@ const LAYOUT_STYLES = { rowGap: 'pb-6', userRow: 'flex flex-col items-end gap-[6px] pt-3', attachmentWidth: 'max-w-[70%]', - userBubble: 'max-w-[70%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3.5 py-2', + userBubble: 'max-w-[70%] overflow-hidden rounded-2xl bg-[var(--surface-5)] px-3.5 py-2', assistantRow: 'group/msg', footer: 'shrink-0 px-[24px] pb-[16px]', footerInner: 'mx-auto max-w-chat', @@ -154,7 +154,7 @@ const LAYOUT_STYLES = { rowGap: 'pb-4', userRow: 'flex flex-col items-end gap-[6px] pt-2', attachmentWidth: 'max-w-[85%]', - userBubble: 'max-w-[85%] overflow-hidden rounded-[16px] bg-[var(--surface-5)] px-3 py-2', + userBubble: 'max-w-[85%] overflow-hidden rounded-2xl bg-[var(--surface-5)] px-3 py-2', assistantRow: 'group/msg', footer: 'shrink-0 px-3 pb-3', footerInner: '', diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx index 199aa2b8be9..6c8299d27a0 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-page-issue.tsx @@ -148,7 +148,7 @@ export function BrowserPageIssueView({ issue, onReload, focusRecovery }: Browser

{copy.headline} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx index a7c82cf5069..26b198fe2ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-session.tsx @@ -117,7 +117,7 @@ function BrowserSuggestionIcon({ suggestion }: { suggestion: UrlSuggestion }) { setFailed(true)} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx index 5b6560b4eae..8c2d120dc3e 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/resource-content.tsx @@ -1,7 +1,15 @@ 'use client' import { lazy, memo, Suspense, useCallback, useEffect, useMemo, useRef, useState } from 'react' -import { OverflowText, PlayOutline, Skeleton, TabStripAction, Tooltip, toast } from '@sim/emcn' +import { + OverflowText, + PlayOutline, + ResourceEmptyState, + Skeleton, + TabStripAction, + Tooltip, + toast, +} from '@sim/emcn' import { Download, FileX, @@ -666,15 +674,11 @@ function EmbeddedWorkflow({ workspaceId, workflowId }: EmbeddedWorkflowProps) { if (!workflowExists || hasLoadError) { return ( -
- -
-

Workflow not found

-

- This workflow may have been deleted or moved -

-
-
+ ) } @@ -729,15 +733,11 @@ function EmbeddedFile({ if (!file) { return ( -
- -
-

File not found

-

- This file may have been deleted or moved -

-
-
+ ) } @@ -780,21 +780,17 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) { if (!folder) { return ( -
- -
-

Folder not found

-

- This folder may have been deleted or moved -

-
-
+ ) } return (
-

{folder.name}

+

{folder.name}

{folderWorkflows.length === 0 ? (

No workflows in this folder

) : ( @@ -804,7 +800,7 @@ function EmbeddedFolder({ workspaceId, folderId }: EmbeddedFolderProps) { key={w.id} type='button' onClick={() => openInternalLink(`/workspace/${workspaceId}/w/${w.id}`)} - className='flex items-center gap-2 rounded-[6px] px-3 py-2 text-left transition-colors hover:bg-[var(--surface-4)]' + className='flex items-center gap-2 rounded-md px-3 py-2 text-left transition-colors hover:bg-[var(--surface-4)]' > @@ -838,15 +834,11 @@ function EmbeddedLog({ workspaceId, logId, onNotFound }: EmbeddedLogProps) { if (!log) { return ( -
- -
-

Log not found

-

- This log may have been deleted or is no longer available -

-
-
+ ) } diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/browser-tab-icon.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/browser-tab-icon.tsx index cb0d6042c20..432ea7f74e9 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/browser-tab-icon.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-registry/browser-tab-icon.tsx @@ -64,7 +64,7 @@ export function BrowserTabIcon({ tabId, scopeId, className }: BrowserTabIconProp src={faviconUrl(hostname, 32)} alt='' className={cn( - 'size-[16px] rounded-[3px]', + 'size-[16px] rounded-sm', !faviconLoaded && 'pointer-events-none absolute opacity-0' )} onLoad={() => setFavicon({ hostname, status: 'loaded' })} diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/queued-messages/queued-messages.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/queued-messages/queued-messages.tsx index 466afe3e7de..57a8c63949b 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/queued-messages/queued-messages.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/queued-messages/queued-messages.tsx @@ -49,7 +49,7 @@ export function QueuedMessages({ return (
-
-
+ /> {editingTagIndex === index && (
@@ -701,7 +679,7 @@ export function DocumentTagsModal({ !kbTagDefinitions.find( (def) => def.displayName.toLowerCase() === editTagForm.displayName.toLowerCase() ) && ( - + Maximum tag definitions reached. You can still use existing tag definitions, but cannot create new ones. diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx index eb262824d69..b3202525bda 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/[documentId]/document.tsx @@ -1,7 +1,14 @@ 'use client' import { useCallback, useEffect, useEffectEvent, useMemo, useRef, useState } from 'react' -import { Badge, ChipCombobox, ChipConfirmModal, chipContentLabelClass, cn } from '@sim/emcn' +import { + Badge, + ChipCombobox, + ChipConfirmModal, + chipContentLabelClass, + cn, + ResourceEmptyState, +} from '@sim/emcn' import { ChevronDown, ChevronUp, @@ -38,7 +45,6 @@ import type { import { EMPTY_CELL_PLACEHOLDER, Resource, - ResourceNotFound, SearchHighlight, } from '@/app/workspace/[workspaceId]/components' import { @@ -1163,7 +1169,7 @@ export function Document({ */ if (documentError && !documentData) { return ( - )}
- { @@ -1307,7 +1310,7 @@ export function KnowledgeBase({ if (error && !knowledgeBase) { return ( - f.tagSlot && f.value.trim()).length - const tagOptions: ChipDropdownOption[] = tagDefinitions.map((t) => ({ + const tagOptions: ChipSelectOption[] = tagDefinitions.map((t) => ({ value: t.displayName, label: t.displayName, })) @@ -1801,7 +1804,7 @@ function TagFilterSection({ tagDefinitions, entries, onChange }: TagFilterSectio > {filtersToShow.map((entry, index) => { const operators = getOperatorsForFieldType(entry.fieldType) - const operatorOptions: ChipDropdownOption[] = operators.map((op) => ({ + const operatorOptions: ChipSelectOption[] = operators.map((op) => ({ value: op.value, label: op.label, })) @@ -1818,24 +1821,29 @@ function TagFilterSection({ tagDefinitions, entries, onChange }: TagFilterSectio )}
- handleTagChange(entry.id, value)} placeholder='Select tag' align='start' - matchTriggerWidth={false} + dropdownWidth='content' contentClassName='max-h-[240px] overflow-y-auto' - className='max-w-[150px]' + className='w-auto max-w-[150px]' /> {entry.tagSlot && ( - updateEntry(entry.id, { operator: value, valueTo: '' })} placeholder='Operator' align='start' - matchTriggerWidth={false} + dropdownWidth='content' /> )}
diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx index 01dbfaead48..3bd0cab96d4 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/action-bar/action-bar.tsx @@ -1,4 +1,4 @@ -import { BulkActionButton, cn, Tooltip } from '@sim/emcn' +import { BulkActionBar, BulkActionButton, cn, Tooltip } from '@sim/emcn' import { Ban, Circle, Trash } from '@sim/emcn/icons' import { domAnimation, LazyMotion, m } from 'framer-motion' import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' @@ -53,70 +53,70 @@ export function ActionBar({ transition={{ duration: 0.2 }} className={cn('-translate-x-1/2 fixed bottom-6 left-1/2 z-[var(--z-dropdown)]', className)} > -
- - {isAllSelected ? totalCount : selectedCount} selected - {showSelectAllOption && ( - <> - {' · '} - - - )} - {isAllSelected && onClearSelectAll && ( - <> - {' · '} - - - )} - + + {isAllSelected ? totalCount : selectedCount} selected + {showSelectAllOption && ( + <> + {' · '} + + + )} + {isAllSelected && onClearSelectAll && ( + <> + {' · '} + + + )} + + } + > + {showEnableButton && ( + + + + + + + Enable + + )} -
- {showEnableButton && ( - - - - - - - Enable - - )} + {showDisableButton && ( + + + + + + + Disable + + )} - {showDisableButton && ( - - - - - - - Disable - - )} - - {onDelete && canEdit && ( - - - - - - - Delete - - )} -
-
+ {onDelete && canEdit && ( + + + + + + + Delete + + )} + ) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx index 46163ac91ea..09ad32d5315 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal/base-tags-modal.tsx @@ -12,9 +12,7 @@ import { ChipModalFooter, ChipModalHeader, type ComboboxOption, - handleKeyboardActivation, } from '@sim/emcn' -import { Trash } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getDocumentIcon } from '@/components/icons/document-icons' import type { TagUsageData } from '@/lib/api/contracts/knowledge' @@ -24,6 +22,7 @@ import { SUPPORTED_FIELD_TYPES, TAG_SLOT_CONFIG, } from '@/lib/knowledge/constants' +import { KnowledgeTagRow } from '@/app/workspace/[workspaceId]/knowledge/components/knowledge-tag-row' import { type TagDefinition, useKnowledgeBaseTagDefinitions, @@ -270,41 +269,23 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM {kbTagDefinitions.map((tag) => { const usage = getTagUsage(tag.tagSlot) return ( -
handleViewDocuments(tag)} - onKeyDown={(event) => { - if (event.target !== event.currentTarget) return - handleKeyboardActivation(event, () => handleViewDocuments(tag)) + onActivate={() => handleViewDocuments(tag)} + activateLabel={`View documents for ${tag.displayName}`} + name={tag.displayName} + typeLabel={FIELD_TYPE_LABELS[tag.fieldType] || tag.fieldType} + detail={ + <> + {usage.documentCount} document{usage.documentCount !== 1 ? 's' : ''} + + } + removeLabel='Delete Tag' + onRemove={(e) => { + e.stopPropagation() + handleDeleteTagClick(tag) }} - > - - {tag.displayName} - - - {FIELD_TYPE_LABELS[tag.fieldType] || tag.fieldType} - -
- - {usage.documentCount} document{usage.documentCount !== 1 ? 's' : ''} - -
- -
-
+ /> ) })} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx index bd2cc556aa0..cefe21c6f32 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-access-field/connector-access-field.tsx @@ -5,9 +5,9 @@ import { ChipButtonGroup, ChipButtonGroupItem, ChipCombobox, - ChipDropdown, ChipLink, ChipModalField, + ChipSelect, type ComboboxOption, Tooltip, } from '@sim/emcn' @@ -170,12 +170,15 @@ export function ConnectorAccessField({ - ({ value: mode, label }))} disabled - className='pointer-events-none w-fit' + className='pointer-events-none w-fit max-w-none' /> diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx index 749b08d852c..6a5843c27f1 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/components/connector-documents/connector-document-status-filter.tsx @@ -1,6 +1,6 @@ 'use client' -import { ChipDropdown } from '@sim/emcn' +import { ChipSelect } from '@sim/emcn' import type { ConnectorDocumentFilter, ConnectorDocumentsData, @@ -20,14 +20,17 @@ export function ConnectorDocumentStatusFilter({ isLoading, }: ConnectorDocumentStatusFilterProps) { return ( - { if (value === 'active' || value === 'excluded' || value === 'failed' || value === 'skipped') onFilterChange(value) }} - matchTriggerWidth={false} + dropdownWidth='content' options={[ { value: 'active', label: isLoading ? 'Included' : `Included (${counts.active})` }, { value: 'excluded', label: isLoading ? 'Excluded' : `Excluded (${counts.excluded})` }, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/index.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/components/index.ts index f8a396f34b0..32d4f68c0d8 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/index.ts @@ -1,3 +1,4 @@ +export { KnowledgeFilterHeading } from '@/app/workspace/[workspaceId]/knowledge/components/knowledge-filter-heading' export { CreateBaseModal } from './create-base-modal' export { DeleteKnowledgeBaseModal } from './delete-knowledge-base-modal' export { EditKnowledgeBaseModal } from './edit-knowledge-base-modal' diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-filter-heading.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-filter-heading.tsx new file mode 100644 index 00000000000..34404d5485d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-filter-heading.tsx @@ -0,0 +1,22 @@ +import { Button } from '@sim/emcn' +import { FILTER_SECTION_LABEL_CLASS } from '@/app/workspace/[workspaceId]/components' + +interface KnowledgeFilterHeadingProps { + title: string + active: boolean + onClear: () => void +} + +/** Heading and conditional clear action for a knowledge-list filter. */ +export function KnowledgeFilterHeading({ title, active, onClear }: KnowledgeFilterHeadingProps) { + return ( +
+ {title} + {active && ( + + )} +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-tag-row.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-tag-row.test.tsx new file mode 100644 index 00000000000..8b9c4d379ef --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-tag-row.test.tsx @@ -0,0 +1,52 @@ +/** @vitest-environment jsdom */ +import { act } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { KnowledgeTagRow } from './knowledge-tag-row' + +let root: Root | undefined +let container: HTMLDivElement | undefined + +afterEach(() => { + act(() => root?.unmount()) + container?.remove() +}) + +describe('KnowledgeTagRow actions', () => { + it('keeps activation and removal as separate keyboard-accessible buttons', () => { + Object.assign(globalThis, { IS_REACT_ACT_ENVIRONMENT: true }) + container = document.createElement('div') + document.body.appendChild(container) + root = createRoot(container) + const onActivate = vi.fn() + const onRemove = vi.fn() + + act(() => + root?.render( + + ) + ) + + const buttons = container.querySelectorAll('button') + expect(buttons).toHaveLength(2) + expect(buttons[0].contains(buttons[1])).toBe(false) + expect(buttons[0].getAttribute('type')).toBe('button') + expect(buttons[0].getAttribute('aria-label')).toBe('View documents for Customer') + + act(() => buttons[0].click()) + expect(onActivate).toHaveBeenCalledTimes(1) + expect(onRemove).not.toHaveBeenCalled() + + act(() => buttons[1].click()) + expect(onActivate).toHaveBeenCalledTimes(1) + expect(onRemove).toHaveBeenCalledTimes(1) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-tag-row.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-tag-row.tsx new file mode 100644 index 00000000000..b8d1c86c726 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-tag-row.tsx @@ -0,0 +1,63 @@ +import type { MouseEventHandler, ReactNode } from 'react' +import { Button, cn } from '@sim/emcn' +import { Trash } from '@sim/emcn/icons' + +interface KnowledgeTagRowProps { + name: string + typeLabel: string + detail: ReactNode + truncateDetail?: boolean + activateLabel: string + removeLabel: string + onActivate: MouseEventHandler + onRemove: MouseEventHandler +} + +/** Shared tag summary; callers retain activation, keyboard and removal behavior. */ +export function KnowledgeTagRow({ + name, + typeLabel, + detail, + truncateDetail = false, + activateLabel, + removeLabel, + onActivate, + onRemove, +}: KnowledgeTagRowProps) { + return ( +
+ + +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.test.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.test.tsx index a03d3348d72..ab97f699aa9 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.test.tsx @@ -96,7 +96,8 @@ vi.mock('@/connectors/registry', () => ({ CONNECTOR_META_REGISTRY: {} })) vi.mock('@/app/workspace/[workspaceId]/knowledge/[id]/components/base-tags-modal', () => ({ BaseTagsModal: () => null, })) -vi.mock('@/app/workspace/[workspaceId]/knowledge/components', () => ({ +vi.mock('@/app/workspace/[workspaceId]/knowledge/components', async () => ({ + ...(await import('@/app/workspace/[workspaceId]/knowledge/components/knowledge-filter-heading')), CreateBaseModal: () => null, EditKnowledgeBaseModal: () => null, KnowledgeListContextMenu: () => null, diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index ccda6b899a9..3d813532aca 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx @@ -1,8 +1,8 @@ 'use client' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' -import type { ChipDropdownOption } from '@sim/emcn' -import { Button, ChipConfirmModal, ChipDropdown, Tooltip, toast } from '@sim/emcn' +import type { ChipSelectOption } from '@sim/emcn' +import { ChipConfirmModal, ChipSelect, Tooltip, toast } from '@sim/emcn' import { Database, FolderPlus, Pencil, Plus, Trash } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import { getErrorMessage } from '@sim/utils/errors' @@ -23,7 +23,6 @@ import type { } from '@/app/workspace/[workspaceId]/components' import { EMPTY_CELL_PLACEHOLDER, - FILTER_SECTION_LABEL_CLASS, OwnerAvatar, ownerCell, Resource, @@ -70,6 +69,7 @@ import { DeleteKnowledgeBaseModal, EditKnowledgeBaseModal, KnowledgeBaseContextMenu, + KnowledgeFilterHeading, KnowledgeListContextMenu, } from '@/app/workspace/[workspaceId]/knowledge/components' import KnowledgeLoading from '@/app/workspace/[workspaceId]/knowledge/loading' @@ -127,13 +127,13 @@ const SEARCH_COLUMNS: ResourceColumn[] = [...COLUMNS, FOLDER_LOCATION_COLUMN] const KNOWLEDGE_BASE_ICON = -const CONNECTOR_FILTER_OPTIONS: ChipDropdownOption[] = [ +const CONNECTOR_FILTER_OPTIONS: ChipSelectOption[] = [ { value: 'all', label: 'All' }, { value: 'connected', label: 'With connectors' }, { value: 'unconnected', label: 'Without connectors' }, ] -const CONTENT_FILTER_OPTIONS: ChipDropdownOption[] = [ +const CONTENT_FILTER_OPTIONS: ChipSelectOption[] = [ { value: 'all', label: 'All' }, { value: 'has-docs', label: 'Has documents' }, { value: 'empty', label: 'Empty' }, @@ -1320,7 +1320,7 @@ function KnowledgeContent() { [activeSort, setListSort, clearListSort] ) - const memberOptions: ChipDropdownOption[] = useMemo( + const memberOptions: ChipSelectOption[] = useMemo( () => (members ?? []).map((m) => ({ value: m.userId, @@ -1334,19 +1334,15 @@ function KnowledgeContent() { () => (
-
- Connectors - {connectorFilter.length > 0 && ( - - )} -
- 0} + onClear={() => setConnectorFilter([])} + /> + setConnectorFilter(value === 'all' ? [] : [value])} @@ -1355,19 +1351,15 @@ function KnowledgeContent() { />
-
- Content - {contentFilter.length > 0 && ( - - )} -
- 0} + onClear={() => setContentFilter([])} + /> + setContentFilter(value === 'all' ? [] : [value])} @@ -1377,24 +1369,21 @@ function KnowledgeContent() {
{memberOptions.length > 0 && (
-
- Owner - {ownerFilter.length > 0 && ( - - )} -
- 0} + onClear={() => setOwnerFilter([])} + /> + {/* Table header */} -
+
Workflow diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx index 9a009452d21..f816e380cfc 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/dashboard/dashboard.tsx @@ -408,7 +408,7 @@ function DashboardInner({ stats, isLoading, error, searchQuery }: DashboardProps )}
-
+
{globalDetails ? ( )}
-
+
{globalDetails ? ( )}
-
+
{globalDetails ? ( diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index a1dd5a25dfe..65c5da5acad 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx @@ -14,6 +14,7 @@ import { import { Button, Calendar, + Chip, ChipCombobox, type ComboboxOption, cn, @@ -195,7 +196,7 @@ function getColorIcon(color: string): React.ComponentType<{ className?: string } const ColorIcon = ({ className }: { className?: string }) => (
{selectedStatusColor && (
)} @@ -1584,13 +1585,9 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
{filtersActive && ( - + )}
) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx index 77303d43160..004b7ac7706 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/admin/admin.tsx @@ -443,9 +443,8 @@ export function Admin() { Page {currentPage} of {totalPages} ({usersData.total} users)
- - +
)} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx index d1d3ceb4b4c..155a417f5bc 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mcp/components/mcp-server-form-modal/mcp-server-form-modal.tsx @@ -763,7 +763,8 @@ export function McpServerFormModal({ type='button' variant='ghost' onClick={() => setShowAdvanced((v) => !v)} - className='gap-1 self-start py-0 text-small' + size='inline' + className='gap-1 self-start' > {showAdvanced ? ( diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx index 72ada4c1ef1..97c5be6db21 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/member-list/member-list.tsx @@ -1,7 +1,7 @@ 'use client' import type { ReactNode } from 'react' -import { OverflowText } from '@sim/emcn' +import { Avatar, AvatarFallback, AvatarImage, OverflowText } from '@sim/emcn' import { SettingsEmptyState } from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state' import { SettingsSection } from '@/app/workspace/[workspaceId]/settings/components/settings-section/settings-section' @@ -18,21 +18,11 @@ interface MemberAvatarProps { * the member's name when no image is available. */ export function MemberAvatar({ name, image }: MemberAvatarProps) { - if (image) { - return ( - {name} - ) - } - return ( - - {name.charAt(0).toUpperCase()} - + + {image && } + {name.charAt(0).toUpperCase()} + ) } @@ -42,7 +32,7 @@ interface MemberRowProps { image: string | null /** Muted trailing text, e.g. "Joined 6/3/2026" or "Invite pending". */ status: string - /** Role control rendered before the actions menu (e.g. a `ChipDropdown`). */ + /** Role control rendered before the actions menu (e.g. a `ChipSelect`). */ roleControl?: ReactNode /** Trailing actions menu (e.g. the `...` `DropdownMenu`). */ menu?: ReactNode diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/mothership/mothership.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/mothership/mothership.tsx index 8cf5573f53e..cf56b278328 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/mothership/mothership.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/mothership/mothership.tsx @@ -3,7 +3,7 @@ import { useCallback, useMemo, useState } from 'react' import { Badge, - Button, + Chip, ChipCopyInput, ChipInput, ChipModalTabs, @@ -434,14 +434,13 @@ function LicensesTab({ environment }: { environment: MothershipEnv }) { className='w-[160px]' />
- +
{generatedKey && ( diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-create-modal.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-create-modal.tsx index 6ccc9de9c41..436bbbdb33b 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-create-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-create-modal.tsx @@ -2,13 +2,13 @@ import { useState } from 'react' import { - ChipDropdown, ChipModal, ChipModalBody, ChipModalError, ChipModalField, ChipModalFooter, ChipModalHeader, + ChipSelect, } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { useParams } from 'next/navigation' @@ -152,7 +152,11 @@ export function SandboxCreateModal({ /> - setDraft((prev) => ({ ...prev, language: language as SandboxLanguage })) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.test.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.test.tsx index dd3249fab67..0fc1ce4fa3e 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.test.tsx @@ -33,7 +33,6 @@ const { recordChipSelectProps } = vi.hoisted(() => ({ vi.mock('@sim/emcn', () => ({ Chip: ({ children }: { children: ReactNode }) => , - ChipDropdown: () => null, ChipInput: () => null, ChipSelect: (props: MockChipSelectProps) => { recordChipSelectProps(props) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.tsx index 51e6ff33c52..0ae26090ab8 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/sandboxes/components/sandbox-editor.tsx @@ -1,7 +1,7 @@ 'use client' import { useMemo, useState } from 'react' -import { Chip, ChipDropdown, ChipInput, ChipTextarea, cn } from '@sim/emcn' +import { Chip, ChipInput, ChipSelect, ChipTextarea, cn } from '@sim/emcn' import type { SandboxDependencyIssue } from '@/lib/api/contracts/sandboxes' import { RowActionsMenu } from '@/app/workspace/[workspaceId]/settings/components/row-actions-menu' import { ManagedCliSelect } from '@/app/workspace/[workspaceId]/settings/components/sandboxes/components/managed-cli-select' @@ -67,7 +67,11 @@ export function SandboxEditor({ /> - onChange({ ...draft, language: language as SandboxLanguage })} options={LANGUAGE_OPTIONS.map((option) => ({ @@ -238,7 +242,7 @@ export function SandboxStatus({ {showLog ? 'Hide log' : 'Show log'} {showLog && ( -
+                
                   {sandbox.errorDetail}
                 
)} diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx index a843694bfe8..fa0e488c761 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/secrets/components/secret-value-field/secret-value-field.tsx @@ -11,7 +11,7 @@ const VIEWER_MASK_LENGTH = 10 type SecretValueFieldProps = Omit< ComponentProps<'input'>, - 'type' | 'value' | 'onChange' | 'readOnly' | 'style' + 'type' | 'value' | 'onChange' | 'readOnly' | 'style' | 'size' > & { /** The chip owns field styling; callers use className for layout. */ style?: never diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/no-organization-view/no-organization-view.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/no-organization-view/no-organization-view.tsx index 82c1009e63d..9e00fc9c38a 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/no-organization-view/no-organization-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/no-organization-view/no-organization-view.tsx @@ -83,18 +83,16 @@ export function NoOrganizationView({
-
-
- sim.ai/team/ -
- setOrgSlug(e.target.value)} - placeholder='my-team' - className='rounded-l-none' - /> -
+ setOrgSlug(e.target.value)} + placeholder='my-team' + className='mt-1' + startAdornment={ + sim.ai/team/ + } + />
diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx index 42ca096a093..43e5f9d25a5 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/team-management/components/organization-member-lists/organization-member-lists.tsx @@ -1,7 +1,7 @@ 'use client' import { useMemo, useState } from 'react' -import { ChipDropdown, ChipTag, toast } from '@sim/emcn' +import { ChipSelect, ChipTag, toast } from '@sim/emcn' import { createLogger } from '@sim/logger' import { isOrgAdminRole } from '@sim/platform-authz/predicates' import { getErrorMessage } from '@sim/utils/errors' @@ -138,7 +138,10 @@ export function OrganizationMemberLists({ } roleControl={ editable ? ( - updateMemberRole @@ -150,7 +153,7 @@ export function OrganizationMemberLists({ .catch((error) => logger.error('Failed to update member role', { error })) } options={ORG_ROLE_OPTIONS} - matchTriggerWidth={false} + dropdownWidth='content' disabled={updateMemberRole.isPending} /> ) : ( @@ -263,7 +266,10 @@ export function OrganizationMemberLists({ {isExternal ? 'External' : invitation.role === 'admin' ? 'Admin' : 'Member'} ) : ( - updateInvitation @@ -275,7 +281,7 @@ export function OrganizationMemberLists({ .catch((error) => logger.error('Failed to update invitation role', { error })) } options={ORG_ROLE_OPTIONS} - matchTriggerWidth={false} + dropdownWidth='content' disabled={updateInvitation.isPending} /> ) @@ -315,7 +321,10 @@ export function OrganizationMemberLists({ } roleControl={ - updatePermissions @@ -329,7 +338,7 @@ export function OrganizationMemberLists({ ) } options={WORKSPACE_ROLE_OPTIONS} - matchTriggerWidth={false} + dropdownWidth='content' disabled={disabled} /> @@ -364,7 +373,10 @@ export function OrganizationMemberLists({ access: RosterWorkspaceAccess ) => { const roleControl = ( - updateInvitation @@ -376,7 +388,7 @@ export function OrganizationMemberLists({ .catch((error) => logger.error('Failed to update invitation grant', { error })) } options={WORKSPACE_ROLE_OPTIONS} - matchTriggerWidth={false} + dropdownWidth='content' disabled={!canManage || updateInvitation.isPending} /> ) diff --git a/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx index de8294ee6c5..133c90c68b9 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/teammates/teammates.tsx @@ -1,7 +1,7 @@ 'use client' import { useCallback, useMemo, useState } from 'react' -import { ChipDropdown, Plus, toast } from '@sim/emcn' +import { ChipSelect, Plus, toast } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { formatDate } from '@sim/utils/formatting' import { useQueryClient } from '@tanstack/react-query' @@ -238,11 +238,14 @@ export function Teammates() { status={teammate.status} roleControl={ - handleRoleChange(teammate, role as WorkspacePermission)} options={ROLE_OPTIONS} - matchTriggerWidth={false} + dropdownWidth='content' disabled={ teammate.isPending || !canManage || diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/lock-settings-modal/lock-settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/lock-settings-modal/lock-settings-modal.tsx index e72cdce34ad..694361bc085 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/lock-settings-modal/lock-settings-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/lock-settings-modal/lock-settings-modal.tsx @@ -95,7 +95,7 @@ export function LockSettingsModal({ {field.label} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-value-editor.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-value-editor.tsx index cdd89e8e821..44cf3ae11bb 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-value-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-value-editor.tsx @@ -1,7 +1,7 @@ 'use client' import { useMemo } from 'react' -import { ChipDropdown } from '@sim/emcn' +import { ChipSelect } from '@sim/emcn' import type { ColumnDefinition } from '@/lib/table' import { SelectPill, selectedOptionIds } from './select-pill' @@ -18,7 +18,7 @@ const CLEAR_VALUE = '' /** * Option picker for `select`/`multiselect` cells in a form context (the row - * modal) — a `ChipDropdown` pill that lists each option as its colored pill and + * modal) — a `ChipSelect` pill that lists each option as its colored pill and * writes option ids back through `onChange`. Inline grid editing uses a bare * `DropdownMenu` instead (see `InlineSelectEditor`). */ @@ -35,31 +35,30 @@ export function SelectValueEditor({ (column.options ?? []).map((option) => ({ value: option.id, label: , + searchTerms: [option.name] as const, })), [column.options] ) if (isMulti) { return ( - { + onMultiSelectChange={(ids) => { if (column.required && ids.length === 0) return onChange(ids) }} options={options} showAllOption={false} - // In multiple mode ChipDropdown ignores `placeholder` and renders - // `allLabel` when nothing is selected — which would read as if every - // option were chosen. There is no "All" entry here, so this is the - // empty label. - allLabel='Select options' align={align} fullWidth={fullWidth} - matchTriggerWidth={false} + dropdownWidth='content' /> ) } @@ -69,19 +68,26 @@ export function SelectValueEditor({ const singleOptions = column.required ? options : [ - { value: CLEAR_VALUE, label: None }, + { + value: CLEAR_VALUE, + label: None, + searchTerms: ['None'] as const, + }, ...options, ] return ( - onChange(id === CLEAR_VALUE ? null : id)} options={singleOptions} placeholder='Select an option' align={align} fullWidth={fullWidth} - matchTriggerWidth={false} + dropdownWidth='content' /> ) } diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-action-bar/table-action-bar.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-action-bar/table-action-bar.tsx index b04257ae755..9922997c840 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-action-bar/table-action-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-action-bar/table-action-bar.tsx @@ -1,7 +1,7 @@ 'use client' import type React from 'react' -import { BulkActionButton, cn, Tooltip } from '@sim/emcn' +import { BulkActionBar, BulkActionButton, cn, Tooltip } from '@sim/emcn' import { Eye, PlayOutline, RefreshCw, Square } from '@sim/emcn/icons' import { AnimatePresence, domAnimation, LazyMotion, m } from 'framer-motion' @@ -89,47 +89,42 @@ export function TableActionBar({ className )} > -
- - {selectedCellCount === 1 + + : `Selected ${selectedCellCount} workflow cells` + } + > + {showPlay && ( + + + + )} -
- {showPlay && ( - - - - )} + {showRefresh && ( + + + + )} - {showRefresh && ( - - - - )} + {runningCount > 0 && ( + + + + )} - {runningCount > 0 && ( - - - - )} - - {onViewExecution && ( - - - - )} -
-
+ {onViewExecution && ( + + + + )} + )} @@ -152,12 +147,7 @@ function ActionIconButton({ label, onClick, disabled, children }: ActionIconButt return ( - + {children} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx index c005de9d781..0772217a108 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-filter/table-filter.tsx @@ -1,7 +1,7 @@ 'use client' import { memo, useCallback, useMemo, useRef, useState } from 'react' -import { Button, ChipDropdown, ChipInput, cn } from '@sim/emcn' +import { Button, Chip, ChipInput, ChipSelect, cn } from '@sim/emcn' import { Plus, X } from '@sim/emcn/icons' import { generateShortId } from '@sim/utils/id' import type { ColumnDefinition, FilterRule, TablePredicate } from '@/lib/table' @@ -243,20 +243,19 @@ export function TableFilter({ ))}
- + {!autoApply && (
{filter !== null && ( - + )} - +
)}
@@ -323,43 +322,49 @@ const FilterRuleRow = memo(function FilterRuleRow({ ) : ( )} - onColumnChange(rule.id, value)} placeholder='Column' align='start' - matchTriggerWidth={false} - className='min-w-[100px]' + dropdownWidth='content' + className='w-auto min-w-[100px] max-w-none' /> - onUpdate(rule.id, 'operator', value)} placeholder='Operator' align='start' - matchTriggerWidth={false} - className='min-w-[90px]' + dropdownWidth='content' + className='w-auto min-w-[90px] max-w-none' /> {VALUELESS_OPS.has(rule.operator) ? (
) : isSelect ? ( - onUpdate(rule.id, 'value', value)} placeholder='Select a value' align='start' - matchTriggerWidth={false} - className='min-w-[100px] flex-1' + dropdownWidth='content' + className='w-auto min-w-[100px] max-w-none flex-1' /> ) : autoApply ? ( { e.currentTarget.style.display = 'none' }} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx index 6ba6ebfa820..19ab7b1c0c3 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/column-header-menu.tsx @@ -178,7 +178,7 @@ export const ColumnHeaderMenu = React.memo(function ColumnHeaderMenu({ ghost.textContent = ghostLabel ghost.className = 'text-small' ghost.style.cssText = - 'position:absolute;top:-9999px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;white-space:nowrap;color:var(--text-primary)' + 'position:absolute;top:-9999px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);white-space:nowrap;color:var(--text-primary)' document.body.appendChild(ghost) e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2) requestAnimationFrame(() => ghost.parentNode?.removeChild(ghost)) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx index 69c159e585b..0c760235c2b 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/headers/workflow-group-meta-cell.tsx @@ -475,7 +475,7 @@ export function WorkflowGroupMetaCell({ ghost.textContent = name ghost.className = 'text-xs' ghost.style.cssText = - 'position:absolute;top:-9999px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:4px;white-space:nowrap;color:var(--text-primary)' + 'position:absolute;top:-9999px;padding:4px 8px;background:var(--bg);border:1px solid var(--border);border-radius:var(--radius-sm);white-space:nowrap;color:var(--text-primary)' document.body.appendChild(ghost) e.dataTransfer.setDragImage(ghost, ghost.offsetWidth / 2, ghost.offsetHeight / 2) requestAnimationFrame(() => ghost.parentNode?.removeChild(ghost)) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx index f040e922466..77e41daefa5 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/table-grid.tsx @@ -2,7 +2,7 @@ import type React from 'react' import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react' -import { cn, toast, useToast } from '@sim/emcn' +import { cn, ResourceEmptyState, toast, useToast } from '@sim/emcn' import { Loader, TableX } from '@sim/emcn/icons' import { createLogger } from '@sim/logger' import type { TableCellSelection } from '@sim/realtime-protocol/table-presence' @@ -4695,15 +4695,11 @@ export function TableGrid({ if (!isLoadingTable && !tableData) { return ( -
- -
-

Table not found

-

- This table may have been deleted or moved -

-
-
+ ) } diff --git a/apps/sim/app/workspace/[workspaceId]/upgrade/components/plan-card/plan-card.tsx b/apps/sim/app/workspace/[workspaceId]/upgrade/components/plan-card/plan-card.tsx index c14dfdeeda4..90e13df381c 100644 --- a/apps/sim/app/workspace/[workspaceId]/upgrade/components/plan-card/plan-card.tsx +++ b/apps/sim/app/workspace/[workspaceId]/upgrade/components/plan-card/plan-card.tsx @@ -78,13 +78,15 @@ export function UpgradePlanCard({ >
-

{name}

+

{name}

{bannerText && {bannerText}}
- {price} + + {price} + {discountLabel && {discountLabel}}

{priceSubtext ?? '\u00A0'}

diff --git a/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx b/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx index 7ec5cadb581..a69dd3eea4f 100644 --- a/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx +++ b/apps/sim/app/workspace/[workspaceId]/upgrade/upgrade.tsx @@ -192,7 +192,7 @@ export function Upgrade({ workspaceId }: UpgradeProps) {
-

+

{header}

{state.showUpgradePlans && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.test.tsx index a28d06bc942..34f2dd00680 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.test.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.test.tsx @@ -4,7 +4,9 @@ import { act, type ButtonHTMLAttributes, + cloneElement, type InputHTMLAttributes, + type ReactElement, type ReactNode, type Ref, } from 'react' @@ -21,6 +23,9 @@ const { mockHandleRunWorkflow, mockReadSSEEvents, mockRevokeObjectURL, + mockTooltipPointerEnter, + executionState, + registryState, WorkflowAttachmentUploadErrorMock, } = vi.hoisted(() => { class WorkflowAttachmentUploadErrorMock extends Error { @@ -61,16 +66,14 @@ const { mockHandleRunWorkflow: vi.fn(), mockReadSSEEvents: vi.fn(), mockRevokeObjectURL: vi.fn(), + mockTooltipPointerEnter: vi.fn(), + executionState: { isExecuting: false }, + registryState: { activeWorkflowId: 'workflow-1' }, WorkflowAttachmentUploadErrorMock, } }) vi.mock('@sim/emcn', () => ({ - Badge: ({ - children, - className: _className, - ...props - }: ButtonHTMLAttributes) => , Button: ({ children, className: _className, @@ -104,7 +107,10 @@ vi.mock('@sim/emcn', () => ({ PopoverTrigger: ({ children }: { children: ReactNode }) =>
{children}
, Tooltip: { Root: ({ children }: { children: ReactNode }) =>
{children}
, - Trigger: ({ children }: { children: ReactNode }) =>
{children}
, + Trigger: ({ children }: { children: ReactElement }) => + cloneElement(children as ReactElement<{ onPointerEnter?: () => void }>, { + onPointerEnter: mockTooltipPointerEnter, + }), Content: ({ children }: { children: ReactNode }) =>
{children}
, }, Trash: () => , @@ -199,7 +205,7 @@ vi.mock('@/stores/chat/store', () => ({ })) vi.mock('@/stores/execution', () => ({ - useIsCurrentWorkflowExecuting: () => false, + useIsCurrentWorkflowExecuting: () => executionState.isExecuting, })) vi.mock('@/stores/operation-queue/store', () => ({ @@ -214,7 +220,7 @@ vi.mock('@/stores/terminal', () => ({ vi.mock('@/stores/workflows/registry/store', () => ({ useWorkflowRegistry: (selector: (state: { activeWorkflowId: string }) => unknown) => - selector({ activeWorkflowId: 'workflow-1' }), + selector(registryState), })) vi.mock('@/stores/workflows/subblock/store', () => ({ @@ -234,6 +240,7 @@ vi.mock('@/stores/chat/utils', () => ({ })) import { Chat } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat' +import { MAX_CHAT_FILES } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks' let container: HTMLDivElement let root: Root @@ -280,6 +287,8 @@ describe('floating chat attachment uploads', () => { beforeEach(async () => { vi.useFakeTimers() vi.clearAllMocks() + executionState.isExecuting = false + registryState.activeWorkflowId = 'workflow-1' mockCreateObjectURL.mockReturnValue('blob:diagram-preview') mockReadSSEEvents.mockResolvedValue(undefined) vi.stubGlobal('FileReader', mockFileReader) @@ -308,6 +317,70 @@ describe('floating chat attachment uploads', () => { vi.unstubAllGlobals() }) + it('opens file selection through a named native action', () => { + const action = container.querySelector('button[aria-label="Attach file"]') + const fileInput = container.querySelector('#floating-chat-file-input') + expect(action?.type).toBe('button') + expect(action?.disabled).toBe(false) + expect(action?.querySelector('[data-icon="Paperclip"]')).not.toBeNull() + expect(fileInput).not.toBeNull() + + const openPicker = vi.fn() + if (fileInput) fileInput.click = openPicker + act(() => action?.click()) + expect(openPicker).toHaveBeenCalledTimes(1) + }) + + it.each(['executing', 'no workflow'] as const)('prevents file selection with %s', (condition) => { + executionState.isExecuting = condition === 'executing' + registryState.activeWorkflowId = condition === 'no workflow' ? '' : 'workflow-1' + act(() => root.render()) + + const action = container.querySelector('button[aria-label="Attach file"]') + const fileInput = container.querySelector('#floating-chat-file-input') + expect(action?.disabled).toBe(true) + expect(fileInput?.disabled).toBe(true) + + const openPicker = vi.fn() + if (fileInput) fileInput.click = openPicker + act(() => action?.click()) + expect(openPicker).not.toHaveBeenCalled() + }) + + it('keeps the tooltip hover target available when attachment is disabled', () => { + executionState.isExecuting = true + act(() => root.render()) + + const action = container.querySelector('button[aria-label="Attach file"]') + const trigger = action?.parentElement + expect(action?.disabled).toBe(true) + expect(trigger?.tagName).toBe('SPAN') + expect(trigger?.hasAttribute('disabled')).toBe(false) + + act(() => trigger?.dispatchEvent(new Event('pointerover', { bubbles: true }))) + expect(mockTooltipPointerEnter).toHaveBeenCalledTimes(1) + }) + + it('disables attachment selection at the file limit', () => { + const fileInput = container.querySelector('#floating-chat-file-input') + if (!fileInput) throw new Error('Expected file input') + Object.defineProperty(fileInput, 'files', { + configurable: true, + value: Array.from( + { length: MAX_CHAT_FILES }, + (_, index) => new File(['x'], `file-${index}.txt`) + ), + }) + act(() => fileInput.dispatchEvent(new Event('change', { bubbles: true }))) + + const action = container.querySelector('button[aria-label="Attach file"]') + expect(action?.disabled).toBe(true) + const openPicker = vi.fn() + fileInput.click = openPicker + act(() => action?.click()) + expect(openPicker).not.toHaveBeenCalled() + }) + it('uses uploaded URLs for message previews without base64 conversion', async () => { const file = new File(['diagram'], 'diagram.png', { type: 'image/png' }) mockHandleRunWorkflow.mockResolvedValue({ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx index d6a91f24477..e951f69a076 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/chat.tsx @@ -2,7 +2,6 @@ import { type KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { - Badge, Button, ComposerActionButton, cn, @@ -131,7 +130,7 @@ function ChatFilePreview({ file, onRemove }: ChatFilePreviewProps) { ) : (
{file.name}
-
{formatFileSize(file.size)}
+
{formatFileSize(file.size)}
)} @@ -1090,16 +1089,21 @@ export function Chat() {
- document.getElementById('floating-chat-file-input')?.click()} - className={cn( - 'cursor-pointer rounded-md border-0! bg-transparent! p-[0px]', - (!activeWorkflowId || isExecuting || chatFiles.length >= MAX_CHAT_FILES) && - 'cursor-not-allowed opacity-50' - )} - > - - + + + Attach file diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx index f0389e64ad4..65069778cbd 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/chat-message/chat-message.tsx @@ -79,7 +79,7 @@ export function ChatMessage({ message }: ChatMessageProps) { )} {formattedContent && !formattedContent.startsWith('Uploaded') && ( -
+
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx index 3d2e7e8b88a..f8cc36b1ef6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/command-list/command-list.tsx @@ -208,7 +208,7 @@ export function CommandList() { {/* Right side: Keyboard Shortcut */}
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat-field-error.test.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat-field-error.test.tsx new file mode 100644 index 00000000000..039844dfa4d --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat-field-error.test.tsx @@ -0,0 +1,86 @@ +import type { ComponentProps, PropsWithChildren } from 'react' +import { JSDOM } from 'jsdom' +import { renderToStaticMarkup } from 'react-dom/server' +import { describe, expect, it, vi } from 'vitest' + +const validation = vi.hoisted(() => ({ + current: { isChecking: false, error: 'Use lowercase letters', isValid: false } as { + isChecking: boolean + error: string | null + isValid: boolean + }, +})) + +vi.mock('@sim/emcn', () => ({ + Input: (props: ComponentProps<'input'>) => , + Label: (props: ComponentProps<'label'>) => ( + + ), + cn: (...values: unknown[]) => values.filter(Boolean).join(' '), + Tooltip: { + Root: ({ children }: PropsWithChildren) => <>{children}, + Trigger: ({ children }: PropsWithChildren) => <>{children}, + Content: ({ children }: PropsWithChildren) => <>{children}, + }, +})) +vi.mock('@sim/emcn/icons', () => ({ Check: () => null, TriangleAlert: () => null })) +vi.mock('@sim/logger', () => ({ createLogger: () => ({}) })) +vi.mock('@/components/ui', () => ({ GeneratedPasswordInput: () => null })) +vi.mock('@/lib/core/config/deployment-shape', () => ({ useDeploymentShape: () => ({}) })) +vi.mock('@/lib/core/utils/urls', () => ({ + getBaseUrl: () => 'https://sim.ai', + getEmailDomain: () => 'sim.ai', +})) +vi.mock('@/lib/messaging/email/validation', () => ({ validateAllowlistEntry: () => true })) +vi.mock('@/lib/workflows/streaming/output-selector', () => ({ + formatInternalOutputSelector: () => '', +})) +vi.mock( + '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components/output-select/output-select', + () => ({ + OutputSelect: () => null, + }) +) +vi.mock('@/hooks/queries/chats', () => ({ + useCreateChat: () => ({}), + useDeleteChat: () => ({}), + useRevealChatPassword: () => ({}), + useUpdateChat: () => ({}), +})) +vi.mock('@/hooks/use-permission-config', () => ({ usePermissionConfig: () => ({}) })) +vi.mock('./hooks', () => ({ useIdentifierValidation: () => validation.current })) + +import { IdentifierInput } from './chat' + +function renderIdentifier() { + return new JSDOM(renderToStaticMarkup()) + .window.document +} + +describe('deploy URL field error', () => { + it('announces and associates the URL validation error with its input', () => { + validation.current = { isChecking: false, error: 'Use lowercase letters', isValid: false } + const document = renderIdentifier() + const input = document.querySelector('#chat-url') + const alert = document.querySelector('[role="alert"]') + + expect(alert?.textContent).toBe('Use lowercase letters') + expect(alert?.className).toBe('mt-[6.5px] text-[var(--text-error)] text-caption') + expect(input?.getAttribute('aria-invalid')).toBe('true') + expect(input?.getAttribute('aria-describedby')).toBe(alert?.id) + expect(alert?.id).toBeTruthy() + expect(document.querySelector('label')?.htmlFor).toBe(input?.id) + }) + + it('omits the error relationship when the URL is valid', () => { + validation.current = { isChecking: false, error: null, isValid: true } + const document = renderIdentifier() + const input = document.querySelector('#chat-url') + + expect(document.querySelector('[role="alert"]')).toBeNull() + expect(input?.getAttribute('aria-invalid')).toBe('false') + expect(input?.hasAttribute('aria-describedby')).toBe(false) + }) +}) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx index 5b854ebeff6..3caefaad692 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx @@ -1,19 +1,19 @@ 'use client' -import { useEffect, useRef, useState } from 'react' +import { type ReactNode, useEffect, useId, useRef, useState } from 'react' import { ChipButtonGroup, ChipButtonGroupItem, ChipConfirmModal, ChipEmailsInput, ChipInput, + ChipModalField, cn, Input, Label, Loader, Skeleton, Switch, - Textarea, Tooltip, } from '@sim/emcn' import { Check, TriangleAlert } from '@sim/emcn/icons' @@ -49,6 +49,19 @@ const logger = createLogger('ChatDeploy') const IDENTIFIER_PATTERN = /^[a-z0-9-]+$/ +interface DeployFieldErrorProps { + children: ReactNode + id?: string +} + +function DeployFieldError({ children, id }: DeployFieldErrorProps) { + return ( + + ) +} + interface ChatDeployProps { workflowId: string deploymentInfo: { @@ -335,12 +348,7 @@ export function ChatDeploy({ return ( <> - + {errors.general && (
@@ -358,27 +366,21 @@ export function ChatDeploy({ isEditingExisting={!!existingChat} /> -
- - updateField('title', e.target.value)} - required - disabled={chatSubmitting} - /> - {errors.title && ( -

{errors.title}

+ + {(aria) => ( + updateField('title', e.target.value)} + required + disabled={chatSubmitting} + {...aria} + /> )} -
+ -
+
@@ -393,14 +395,10 @@ export function ChatDeploy({ className='w-full' disablePortal /> - {errors.outputBlocks && ( -

- {errors.outputBlocks} -

- )} + {errors.outputBlocks && {errors.outputBlocks}}
-
+
-
+