diff --git a/.claude/rules/emcn-components.md b/.claude/rules/emcn-components.md index d4c697869d0..1d88722122e 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` border (`chip-chrome.ts` still spells it through the legacy alias `--border-1`; new code writes `--border`). @@ -24,15 +24,15 @@ The menu surface intentionally diverges from the pill: `dropdown-menu.tsx` items - **`Chip` / `ChipLink`** — the pill button (` + ), + AuthTextLink: (props: ButtonHTMLAttributes) => ( + - +
- +
) diff --git a/apps/sim/app/(interfaces)/chat/components/input/input.tsx b/apps/sim/app/(interfaces)/chat/components/input/input.tsx index 9bd9c9b8b60..95256482f3e 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/input/public-chat-action-button.tsx b/apps/sim/app/(interfaces)/chat/components/input/public-chat-action-button.tsx index 838051a1817..0ac57babb30 100644 --- a/apps/sim/app/(interfaces)/chat/components/input/public-chat-action-button.tsx +++ b/apps/sim/app/(interfaces)/chat/components/input/public-chat-action-button.tsx @@ -4,7 +4,7 @@ import { Button } from '@sim/emcn' interface PublicChatActionButtonProps extends Omit< ComponentProps, - 'variant' | 'size' | 'iconSize' | 'iconPadding' | 'className' + 'variant' | 'size' | 'iconSize' | 'iconPadding' | 'className' | 'shape' > { variant: 'primary' | 'quiet' 'aria-label': string @@ -12,6 +12,6 @@ interface PublicChatActionButtonProps /** Public chat's circular composer action, retaining its primary and quiet palettes. */ export const PublicChatActionButton = forwardRef( - (props, ref) =>
diff --git a/apps/sim/app/(interfaces)/chat/components/message-container/message-container.tsx b/apps/sim/app/(interfaces)/chat/components/message-container/message-container.tsx index 9a355614557..b9dd835af01 100644 --- a/apps/sim/app/(interfaces)/chat/components/message-container/message-container.tsx +++ b/apps/sim/app/(interfaces)/chat/components/message-container/message-container.tsx @@ -72,7 +72,8 @@ export function ChatMessageContainer({ +
Refresh
@@ -785,7 +788,7 @@ export default function ResumeExecutionPage({ {/* Main Layout */}
{/* Pause Points List */} -
+
@@ -817,17 +820,17 @@ export default function ResumeExecutionPage({ {/* Detail Panel */}
{loadingDetail && !selectedDetail ? ( -
+
Loading…
) : !selectedContextId ? ( -
+
Select a pause point
) : !selectedDetail ? ( -
+
Could not load details @@ -835,7 +838,7 @@ export default function ResumeExecutionPage({ ) : (
{/* Status Header */} -
+

@@ -853,7 +856,7 @@ export default function ResumeExecutionPage({

{selectedDetail.pausePoint.automaticResumeWaitingReason && ( -
+

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

+
@@ -917,7 +920,7 @@ export default function ResumeExecutionPage({ <> {/* Display Data */} {responseStructureRows.length > 0 ? ( -
+
@@ -943,7 +946,7 @@ export default function ResumeExecutionPage({
) : ( -
+
@@ -957,7 +960,7 @@ export default function ResumeExecutionPage({ {/* Resume Form */} {isHumanMode && hasInputFormat ? ( -
+
@@ -986,7 +989,7 @@ export default function ResumeExecutionPage({
) : ( -
+
@@ -1005,7 +1008,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/landing-cta-link/landing-cta-link.tsx b/apps/sim/app/(landing)/components/landing-cta-link/landing-cta-link.tsx index 4fbba3d3750..5787f2c8560 100644 --- a/apps/sim/app/(landing)/components/landing-cta-link/landing-cta-link.tsx +++ b/apps/sim/app/(landing)/components/landing-cta-link/landing-cta-link.tsx @@ -9,7 +9,7 @@ type LandingCtaSize = 'compact' | 'default' | 'display' export type LandingCtaSection = PostHogEventMap['landing_cta_clicked']['section'] -interface LandingCtaLinkProps extends Omit { +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 bf89ed90878..7066f483191 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 8839f44f4a7..fb84af1d8b4 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' @@ -27,8 +27,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 @@ -38,7 +42,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) @@ -47,7 +51,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'), + }) } } @@ -58,7 +65,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') }) } } @@ -70,7 +77,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'), + }) } } @@ -89,7 +99,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}
-
+
{ setSent(false) diff --git a/apps/sim/app/f/[token]/public-file-sso-auth.tsx b/apps/sim/app/f/[token]/public-file-sso-auth.tsx index 3e73581f715..20b38e83579 100644 --- a/apps/sim/app/f/[token]/public-file-sso-auth.tsx +++ b/apps/sim/app/f/[token]/public-file-sso-auth.tsx @@ -1,7 +1,7 @@ 'use client' import { useState } from 'react' -import { cn, Input, Label } from '@sim/emcn' +import { ChipInput, Label } from '@sim/emcn' import { getErrorMessage } from '@sim/utils/errors' import { normalizeEmail } from '@sim/utils/string' import { useRouter } from 'next/navigation' @@ -68,7 +68,7 @@ export function PublicFileSSOAuth({ token }: PublicFileSSOAuthProps) { >
- {error ?

{error}

: null}
diff --git a/apps/sim/app/invite/[id]/loading.tsx b/apps/sim/app/invite/[id]/loading.tsx index b26abbbaf59..31e3d619994 100644 --- a/apps/sim/app/invite/[id]/loading.tsx +++ b/apps/sim/app/invite/[id]/loading.tsx @@ -3,11 +3,11 @@ import { Skeleton } from '@sim/emcn' export default function InviteLoading() { return (
- - - - - + + + + +
) } diff --git a/apps/sim/app/invite/components/index.ts b/apps/sim/app/invite/components/index.ts index f72433bf8a8..817d145aaae 100644 --- a/apps/sim/app/invite/components/index.ts +++ b/apps/sim/app/invite/components/index.ts @@ -1,4 +1,5 @@ export { InvitationDisclosure } from '@/app/invite/components/invitation-disclosure' export { InvitationWorkspaceAccess } from '@/app/invite/components/invitation-workspace-access' +export { InviteHeading } from '@/app/invite/components/invite-heading' export { default as InviteLayout } from '@/app/invite/components/layout' export { InviteStatusCard } from '@/app/invite/components/status-card' diff --git a/apps/sim/app/invite/components/invite-heading.tsx b/apps/sim/app/invite/components/invite-heading.tsx new file mode 100644 index 00000000000..c2b54626498 --- /dev/null +++ b/apps/sim/app/invite/components/invite-heading.tsx @@ -0,0 +1,16 @@ +import type { ReactNode } from 'react' + +interface InviteHeadingProps { + title: ReactNode + children: ReactNode +} + +/** Heading shared by invitation and email-preference states; subcopy stays with each caller. */ +export function InviteHeading({ title, children }: InviteHeadingProps) { + return ( +
+

{title}

+ {children} +
+ ) +} diff --git a/apps/sim/app/invite/components/status-card.tsx b/apps/sim/app/invite/components/status-card.tsx index f137111f4c1..403e64fdb1e 100644 --- a/apps/sim/app/invite/components/status-card.tsx +++ b/apps/sim/app/invite/components/status-card.tsx @@ -1,7 +1,7 @@ 'use client' -import { Chip, cn, Loader } from '@sim/emcn' +import { Chip, Loader } from '@sim/emcn' import { AuthSubmitButton } from '@/app/(auth)/components' -import { AUTH_BUTTON_CLASS } from '@/app/(auth)/components/constants' +import { InviteHeading } from '@/app/invite/components/invite-heading' /** A document navigation, so the marketing surface initializes its own theme store. */ function returnHome(): void { @@ -37,10 +37,9 @@ export function InviteStatusCard({ if (type === 'loading') { return ( <> -
-

Loading

+

{description}

-
+
@@ -50,10 +49,9 @@ export function InviteStatusCard({ return ( <> -
-

{title}

+

{description}

-
+
{details} @@ -81,7 +79,9 @@ export function InviteStatusCard({ fullWidth onClick={action.onClick} disabled={action.disabled || action.loading} - className={cn(AUTH_BUTTON_CLASS, 'border border-[var(--border)]')} + variant='outline' + size='lg' + align='center' > {action.loading ? ( diff --git a/apps/sim/app/playground/page.tsx b/apps/sim/app/playground/page.tsx index fa5a295eacd..a1ece9e3835 100644 --- a/apps/sim/app/playground/page.tsx +++ b/apps/sim/app/playground/page.tsx @@ -172,7 +172,7 @@ export default function PlaygroundPage() { aria-label='Go back' variant='ghost' onClick={() => router.back()} - className='size-8 p-0' + iconSize='roomy' > @@ -187,7 +187,7 @@ export default function PlaygroundPage() { aria-label={isDarkMode ? 'Light mode' : 'Dark mode'} variant='default' onClick={toggleDarkMode} - className='size-8 p-0' + iconSize='roomy' > {isDarkMode ? : } @@ -307,12 +307,6 @@ export default function PlaygroundPage() { - - - - - - @@ -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 new file mode 100644 index 00000000000..b91cc7e471e --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.test.tsx @@ -0,0 +1,159 @@ +/** + * @vitest-environment jsdom + */ +import { act, createRef } from 'react' +import { createRoot, type Root } from 'react-dom/client' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { + CodeSearchOverlay, + type CodeSearchOverlayProps, +} from '@/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay' + +let host: HTMLDivElement +let root: Root +const inputRef = createRef() + +const callbacks = { + onQueryChange: vi.fn(), + onPrevious: vi.fn(), + onNext: vi.fn(), + onClose: vi.fn(), +} +const parentClick = vi.fn() + +function renderOverlay(props: Partial = {}) { + act(() => + root.render( +
+ +
+ ) + ) + const overlay = host.firstElementChild?.firstElementChild as HTMLDivElement + const input = overlay.querySelector('input') as HTMLInputElement + return { overlay, input } +} + +beforeEach(() => { + ;(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true + vi.clearAllMocks() + host = document.createElement('div') + document.body.appendChild(host) + root = createRoot(host) +}) + +afterEach(() => { + act(() => root.unmount()) + host.remove() +}) + +describe('CodeSearchOverlay', () => { + it('shares the floating chrome and routes query, navigation, and close actions', () => { + const { overlay, input } = renderOverlay() + expect(overlay.className).toContain('h-[34px]') + expect(overlay.className).toContain('rounded-sm bg-[var(--surface-1)]') + expect(overlay.className).toContain('top-0 right-0') + expect(overlay.getAttribute('role')).toBe('presentation') + expect(inputRef.current).toBe(input) + expect(input.getAttribute('aria-label')).toBe('Search code') + expect(input.value).toBe('error') + expect(overlay.textContent).toContain('2/3') + const tally = overlay.querySelector('[aria-live="polite"][aria-atomic="true"]') + expect(tally?.textContent).toBe('2/3') + + const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set + act(() => { + setter?.call(input, 'failed') + input.dispatchEvent(new Event('input', { bubbles: true })) + }) + expect(callbacks.onQueryChange).toHaveBeenCalledWith('failed') + act(() => { + overlay.querySelector('[aria-label="Previous match"]')?.click() + overlay.querySelector('[aria-label="Next match"]')?.click() + overlay.querySelector('[aria-label="Close search"]')?.click() + }) + expect(callbacks.onPrevious).toHaveBeenCalledTimes(1) + expect(callbacks.onNext).toHaveBeenCalledTimes(1) + expect(callbacks.onClose).toHaveBeenCalledTimes(1) + expect(parentClick).not.toHaveBeenCalled() + + renderOverlay({ matchCount: 0, currentMatchIndex: 0 }) + expect(tally?.textContent).toBe('0/0') + }) + + it('keeps preview search compact in a floating overlay with a usable input ref', () => { + const { overlay, input } = renderOverlay({ + inputKind: 'plain', + className: 'top-10 right-[8px]', + }) + expect(overlay.getAttribute('role')).toBe('presentation') + expect(overlay.className).toContain('top-10 right-[8px]') + expect(overlay.hasAttribute('data-toolbar-root')).toBe(false) + expect(input.parentElement?.className).toContain('h-[23px]') + expect(inputRef.current).toBe(input) + + const setter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, 'value')?.set + act(() => { + setter?.call(input, 'preview') + input.dispatchEvent(new Event('input', { bubbles: true })) + overlay.querySelector('[aria-label="Next match"]')?.click() + }) + expect(callbacks.onQueryChange).toHaveBeenCalledWith('preview') + expect(callbacks.onNext).toHaveBeenCalledTimes(1) + expect(parentClick).not.toHaveBeenCalled() + }) + + it('retains the attached terminal edge, marker, wider tally, and disabled navigation', () => { + const { overlay, input } = renderOverlay({ + appearance: 'attached', + inputKind: 'plain', + className: 'top-[30px] right-[8px]', + query: '', + matchCount: 0, + currentMatchIndex: 0, + }) + 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) + expect(input.parentElement?.className).toContain('h-[23px]') + expect(input.parentElement?.className).toContain('w-[94px]') + expect(input.className).toContain('text-caption') + expect(overlay.textContent).toContain('No results') + expect(overlay.querySelector('span.w-\\[58px\\]')).not.toBeNull() + const previous = overlay.querySelector('[aria-label="Previous match"]') + const next = overlay.querySelector('[aria-label="Next match"]') + const close = overlay.querySelector('[aria-label="Close search"]') + expect(previous?.disabled).toBe(true) + expect(next?.disabled).toBe(true) + expect(close?.disabled).toBe(false) + expect(previous?.className).toContain('-m-1.5') + expect(previous?.querySelector('svg')?.getAttribute('class')).toContain('size-[14px]') + act(() => { + previous?.click() + next?.click() + close?.click() + }) + expect(callbacks.onPrevious).not.toHaveBeenCalled() + expect(callbacks.onNext).not.toHaveBeenCalled() + expect(callbacks.onClose).toHaveBeenCalledTimes(1) + }) + + it('shows the compact no-results tally for other code panels', () => { + const { overlay } = renderOverlay({ matchCount: 0, currentMatchIndex: 0 }) + expect(overlay.textContent).toContain('0/0') + expect(overlay.getAttribute('data-toolbar-root')).toBeNull() + expect( + overlay.querySelector('[aria-label="Previous match"]')?.disabled + ).toBe(true) + }) +}) 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 new file mode 100644 index 00000000000..d8750794cf5 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay.tsx @@ -0,0 +1,100 @@ +import type { ChangeEvent, Ref } from 'react' +import { Button, ChipInput, cn } from '@sim/emcn' +import { ArrowDown, ArrowUp, X } from '@sim/emcn/icons' + +export interface CodeSearchOverlayProps { + /** The attached terminal panel has a joined lower edge and wider result tally. */ + appearance?: 'floating' | 'attached' + /** Position relative to the owning code panel. */ + className: string + /** Logs use the 30px chip field; previews and terminal output use compact search. */ + inputKind: 'chip' | 'plain' + inputRef: Ref + query: string + onQueryChange: (query: string) => void + matchCount: number + currentMatchIndex: number + onPrevious: () => void + onNext: () => void + onClose: () => void +} + +/** Shared controls for searching a Code.Viewer without owning its search state. */ +export function CodeSearchOverlay({ + appearance = 'floating', + className, + inputKind, + inputRef, + query, + onQueryChange, + matchCount, + currentMatchIndex, + onPrevious, + onNext, + onClose, +}: CodeSearchOverlayProps) { + const attached = appearance === 'attached' + const inputProps = { + ref: inputRef, + type: 'text', + value: query, + onChange: (event: ChangeEvent) => onQueryChange(event.target.value), + placeholder: 'Search...', + 'aria-label': 'Search code', + } as const + const actionProps = { + type: 'button' as const, + variant: 'ghost' as const, + iconPadding: attached ? ('md' as const) : ('sm' as const), + className: attached ? '-m-1.5' : undefined, + } + const iconClass = attached ? 'size-[14px]' : 'size-[12px]' + + return ( +
event.stopPropagation()} + data-toolbar-root={attached ? true : undefined} + data-search-active={attached ? true : undefined} + > + {inputKind === 'chip' ? ( + + ) : ( + + )} + 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]' + )} + > + {matchCount > 0 + ? `${currentMatchIndex + 1}/${matchCount}` + : attached + ? 'No results' + : '0/0'} + + + + +
+ ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/components/find-bar/find-bar.tsx b/apps/sim/app/workspace/[workspaceId]/components/find-bar/find-bar.tsx index ceb2b0b709b..6ec4de4377e 100644 --- a/apps/sim/app/workspace/[workspaceId]/components/find-bar/find-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/components/find-bar/find-bar.tsx @@ -129,7 +129,8 @@ export const FindBar = memo(function FindBar({ type='button' variant='ghost' size='icon' - className='size-6 shrink-0' + iconSize='compact' + className='shrink-0' aria-label={showReplace ? 'Hide replace' : 'Show replace'} aria-expanded={showReplace} onClick={() => setShowReplace((visible) => !visible)} @@ -179,7 +180,8 @@ export const FindBar = memo(function FindBar({ type='button' variant='ghost' size='icon' - className='size-6 shrink-0' + iconSize='compact' + className='shrink-0' aria-label='Previous match' title='Previous match (Shift+Enter)' disabled={!navEnabled} @@ -191,7 +193,8 @@ export const FindBar = memo(function FindBar({ type='button' variant='ghost' size='icon' - className='size-6 shrink-0' + iconSize='compact' + className='shrink-0' aria-label='Next match' title='Next match (Enter)' disabled={!navEnabled} @@ -203,7 +206,8 @@ export const FindBar = memo(function FindBar({ type='button' variant='ghost' size='icon' - className='size-6 shrink-0' + iconSize='compact' + className='shrink-0' aria-label='Close find' title='Close (Esc)' onClick={onClose} 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 57d83213e0e..3eea2a00448 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' @@ -43,7 +42,6 @@ export type { PaginationConfig, ResourceCell, ResourceCellEditing, - ResourceColumn, ResourceRow, ResourceTableHandle, RowDragDropConfig, 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 f2efc1446eb..d51c928c90f 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' @@ -160,7 +160,7 @@ export function InviteModal({ { enabled: open && isOrganizationInvite && !organizationOnly } ) - const workspaceOptions = useMemo(() => { + const workspaceOptions = useMemo(() => { if (!isOrganizationInvite) { return workspaceId ? [{ value: workspaceId, label: workspaceName ?? 'This workspace' }] : [] } @@ -316,12 +316,15 @@ export function InviteModal({ {!organizationOnly && ( <> - - + {copied ? 'Copied message' : 'Copy message'} @@ -188,27 +188,29 @@ export const MessageActions = memo(function MessageActions({ <> - + Good response - + Bad response @@ -217,15 +219,16 @@ export const MessageActions = memo(function MessageActions({ {canFork && ( - + Fork in new chat 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/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 95bf5999af8..b20af9cd3c0 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/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/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 9e8c3c634c4..cde4083d589 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 @@ -53,9 +53,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 d6623e648d6..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 @@ -282,11 +282,13 @@ export function ResizableImageView({ node, selected, editor, getPos }: ReactNode type='button' variant='ghost' size='icon' + iconSize={{ base: 'touch', sm: 'roomy' }} + iconPadding='sm' aria-label='Resize image' onPointerDown={startResize} - className='absolute right-0 bottom-0 flex size-10 cursor-nwse-resize touch-none items-end justify-end p-1 sm:size-8' + 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/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/menus/toolbar-button.tsx b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx index 4bdcb5b19d8..7d2aa9f6a58 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/components/file-viewer/rich-markdown-editor/menus/toolbar-button.tsx @@ -1,5 +1,5 @@ import type { ComponentType, SVGProps } from 'react' -import { Button, cn, Tooltip } from '@sim/emcn' +import { Button, Tooltip } from '@sim/emcn' interface ToolbarButtonProps { /** Any SVG icon component, e.g. from `@sim/emcn/icons`. */ @@ -28,17 +28,15 @@ export function ToolbarButton({ 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 902baad1c02..adcb3ea373e 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; } @@ -278,7 +278,7 @@ .rich-markdown-prose pre { background: var(--surface-5); - border-radius: 8px; + border-radius: var(--radius-lg); padding: 1rem; overflow-x: auto; } @@ -319,7 +319,7 @@ .rich-markdown-prose pre code { background: none; padding: 0; - font-size: 13px; + font-size: var(--text-small); line-height: 21px; } @@ -343,12 +343,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; } @@ -375,7 +375,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 @@ -457,7 +457,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; @@ -473,8 +473,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; } @@ -563,7 +563,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; } @@ -601,10 +601,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; @@ -626,12 +626,12 @@ .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); } .rich-markdown-nodes .collaboration-carets__selection { background-color: color-mix(in srgb, var(--caret-color) 20%, transparent); - border-radius: 2px; + border-radius: var(--radius-xs); pointer-events: none; } @@ -640,7 +640,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 9cb16f9a489..99c7d1981c6 100644 --- a/apps/sim/app/workspace/[workspaceId]/files/files.tsx +++ b/apps/sim/app/workspace/[workspaceId]/files/files.tsx @@ -3,7 +3,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { Avatar, - Button, + Chip, ChipCombobox, ChipConfirmModal, Columns2, @@ -2061,13 +2061,9 @@ function FilesContent() { )} {hasActiveFilters && ( - + )} ) diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/search-filters.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/search-filters.tsx index c1d28baf693..e4e51728732 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/search-filters.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/knowledge-search-results/search-filters.tsx @@ -1,4 +1,4 @@ -import { ChipDatePicker, ChipDropdown } from '@sim/emcn' +import { ChipDatePicker, ChipSelect } from '@sim/emcn' import { useQueryStates } from 'nuqs' import { connectorDisplayName } from '@/lib/sim-search/connectors' import { @@ -20,11 +20,12 @@ export function SearchFilters({ sourceTypes }: SearchFiltersProps) { aria-label='Search filters' className='flex flex-wrap items-center gap-2 px-2 py-2' > - void setFilters({ source: source || null })} /> - window.id === filters.updated)?.label ?? 'Any time'}`} - matchTriggerWidth={false} + dropdownWidth='content' + modal={false} + showSelectedCheck value={filters.updated} options={UPDATED_WINDOWS.map((window) => ({ value: window.id, label: window.label }))} onChange={(value) => { 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/message-sources/message-sources.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/components/message-sources/message-sources.tsx index 3de491dad48..3210aafabda 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 @@ -20,7 +20,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/message-content.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/message-content/message-content.tsx index e04ea68b9bd..c9fe3be0646 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 @@ -1227,7 +1227,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 f93063bd6be..a27c7f5703b 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 @@ -153,7 +153,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', @@ -165,7 +165,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-downloads.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-downloads.tsx index ed3ae9e2eb1..a694dc21271 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-downloads.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-downloads.tsx @@ -3,7 +3,6 @@ import { useEffect, useMemo, useRef, useState } from 'react' import type { BrowserDownloadInfo } from '@sim/desktop-bridge' import { - Button, cn, DropdownMenu, DropdownMenuContent, @@ -18,6 +17,7 @@ import { showBrowserDownloadInFolder, showBrowserDownloadsMenu, } from '@/lib/browser-agent/transport' +import { BrowserToolbarButton } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-toolbar-button' /** Aggregate byte progress for the toolbar rail; unknown-size downloads are ignored. */ export function aggregateDownloadPercent(downloads: BrowserDownloadInfo[]): number | null { @@ -127,14 +127,11 @@ export function BrowserDownloads({ scopeId, open, requestOpen, onClose }: Browse }} > - + Downloads diff --git a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx index 8a1f2bc6539..1170962715a 100644 --- a/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-find-bar.tsx @@ -106,7 +106,8 @@ export function BrowserFindBar({ inputRef, onClose, scopeId }: BrowserFindBarPro size='sm' aria-label='Previous match' disabled={!result?.matches} - className='size-[24px] shrink-0 p-0' + iconSize='compact-fixed' + className='shrink-0' onClick={() => step('back')} > @@ -117,7 +118,8 @@ export function BrowserFindBar({ inputRef, onClose, scopeId }: BrowserFindBarPro size='sm' aria-label='Next match' disabled={!result?.matches} - className='size-[24px] shrink-0 p-0' + iconSize='compact-fixed' + className='shrink-0' onClick={() => step('forward')} > @@ -127,7 +129,8 @@ export function BrowserFindBar({ inputRef, onClose, scopeId }: BrowserFindBarPro variant='ghost-secondary' size='sm' aria-label='Close find bar' - className='size-[24px] shrink-0 p-0' + iconSize='compact-fixed' + className='shrink-0' onClick={dismiss} > 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 d59a858afd0..e180b2508e9 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 @@ -160,7 +160,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 86b7174a037..e9704626fd1 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 @@ -12,7 +12,6 @@ import type { import { isBrowserTheme } from '@sim/browser-protocol' import type { BrowserAddToChatPayload, DesktopAppearanceTheme } from '@sim/desktop-bridge' import { - Button, ChipConfirmModal, ChipInput, chipVariants, @@ -80,6 +79,7 @@ import { useBrowserPanelOcclusion, } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-panel-occlusion' import { BrowserThemeNotice } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-theme-notice' +import { BrowserToolbarButton } from '@/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-content/components/browser-session/browser-toolbar-button' import { buildOmniboxSuggestions, googleSearchUrl, @@ -115,7 +115,7 @@ function BrowserSuggestionIcon({ suggestion }: { suggestion: UrlSuggestion }) { setFailed(true)} /> ) @@ -1016,38 +1016,26 @@ export function BrowserSession({
- - - + {/* URL bar: Enter navigates the agent browser. */} )} + , + 'variant' | 'size' | 'iconSize' | 'iconPadding' | 'type' + > { + 'aria-label': string +} + +/** Browser navigation and utility action; forwards menu-anchor refs and native events. */ +export const BrowserToolbarButton = forwardRef( + ({ className, ...props }, ref) => ( + -
-
+ /> {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 9de2b6c4cb5..6ad276ebe32 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, @@ -53,7 +60,6 @@ import { EMPTY_CELL_PLACEHOLDER, Resource, } from '@/app/workspace/[workspaceId]/components/resource/resource' -import { ResourceNotFound } from '@/app/workspace/[workspaceId]/components/resource/resource-not-found' import { SearchHighlight } from '@/app/workspace/[workspaceId]/components/search-highlight/search-highlight' import { ChunkContextMenu, @@ -1173,7 +1179,7 @@ export function Document({ */ if (documentError && !documentData) { return ( - )}
- { @@ -1308,7 +1311,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..e4b54cdfc22 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.displayName}`} + 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 fb1d1dda457..1a62e6009a6 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' @@ -171,12 +171,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/[id]/document-columns.ts b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/document-columns.ts index d2a3746c010..44fc70e6a91 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/document-columns.ts +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/[id]/document-columns.ts @@ -1,4 +1,4 @@ -import type { ResourceColumn } from '@/app/workspace/[workspaceId]/components' +import type { ResourceColumn } from '@/app/workspace/[workspaceId]/components/resource/resource' export const DOCUMENT_COLUMNS: ResourceColumn[] = [ { id: 'name', header: 'Name', widthMultiplier: 0.8 }, 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..b396e9bde9f --- /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/resource/components/resource-options' + +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..4f800581fb9 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/components/knowledge-tag-row.test.tsx @@ -0,0 +1,53 @@ +/** @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') + expect(buttons[1].getAttribute('aria-label')).toBe('Delete 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.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/knowledge.tsx index bc26441338b..d6b907ffedc 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 { Avatar, Button, ChipConfirmModal, ChipDropdown, Tooltip, toast } from '@sim/emcn' +import type { ChipSelectOption } from '@sim/emcn' +import { Avatar, 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' @@ -53,7 +53,6 @@ import type { SearchConfig, SortConfig, } from '@/app/workspace/[workspaceId]/components/resource/components/resource-options' -import { FILTER_SECTION_LABEL_CLASS } from '@/app/workspace/[workspaceId]/components/resource/components/resource-options' import { timeCell } from '@/app/workspace/[workspaceId]/components/resource/components/time-cell' import { resourceListState } from '@/app/workspace/[workspaceId]/components/resource/is-resource-list-empty' import type { @@ -73,6 +72,7 @@ import { DeleteKnowledgeBaseModal, EditKnowledgeBaseModal, KnowledgeBaseContextMenu, + KnowledgeFilterHeading, KnowledgeListContextMenu, } from '@/app/workspace/[workspaceId]/knowledge/components' import KnowledgeLoading from '@/app/workspace/[workspaceId]/knowledge/loading' @@ -130,13 +130,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' }, @@ -1326,7 +1326,7 @@ function KnowledgeContent() { [activeSort, setListSort, clearListSort] ) - const memberOptions: ChipDropdownOption[] = useMemo( + const memberOptions: ChipSelectOption[] = useMemo( () => (members ?? []).map((m) => ({ value: m.userId, @@ -1340,19 +1340,15 @@ function KnowledgeContent() { () => (
-
- Connectors - {connectorFilter.length > 0 && ( - - )} -
- 0} + onClear={() => setConnectorFilter([])} + /> + setConnectorFilter(value === 'all' ? [] : [value])} @@ -1361,19 +1357,15 @@ function KnowledgeContent() { />
-
- Content - {contentFilter.length > 0 && ( - - )} -
- 0} + onClear={() => setContentFilter([])} + /> + setContentFilter(value === 'all' ? [] : [value])} @@ -1383,24 +1375,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/components/log-details/components/trace-view/trace-view.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx index 6b5f0a54988..eab5ed975ac 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/components/trace-view/trace-view.tsx @@ -15,23 +15,16 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, Duplicate, + OverlayActionButton, Search as SearchIcon, Tooltip, useCopyToClipboard, } from '@sim/emcn' -import { - ArrowDown, - ArrowUp, - Check, - ChevronsDownUp, - ChevronsUpDown, - Clipboard, - Search, - X, -} from '@sim/emcn/icons' +import { Check, ChevronsDownUp, ChevronsUpDown, Clipboard, Search } from '@sim/emcn/icons' import { formatDuration } from '@sim/utils/formatting' import { createPortal } from 'react-dom' import type { TraceSpan } from '@/lib/logs/types' +import { CodeSearchOverlay } from '@/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay' import { adjustBgForContrast, formatCostAmount, @@ -494,7 +487,8 @@ function DetailCodeSection({ - + {copied ? 'Copied' : 'Copy'} - + Search @@ -544,54 +534,18 @@ function DetailCodeSection({ )}
{isSearchActive && ( -
e.stopPropagation()} - > - setSearchQuery(e.target.value)} - placeholder='Search...' - className='mr-0.5 w-[94px]' - /> - 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]' - )} - > - {matchCount > 0 ? `${currentMatchIndex + 1}/${matchCount}` : '0/0'} - - - - -
+ )} {typeof document !== 'undefined' && createPortal( diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index b3a28c53f71..d336e7a461f 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -15,7 +15,6 @@ import { Badge, Button, Chip, - ChipInput, ChipModalTabs, Code, cn, @@ -28,14 +27,13 @@ import { Duplicate, Eye, handleKeyboardActivation, + OverlayActionButton, Redo, Search as SearchIcon, Tooltip, useCopyToClipboard, } from '@sim/emcn' import { - ArrowDown, - ArrowUp, Check, ChevronUp, Clipboard, @@ -59,6 +57,7 @@ import { filterHiddenOutputKeys } from '@/lib/logs/execution/trace-spans/trace-s import type { TraceSpan } from '@/lib/logs/types' import { sendMothershipMessage } from '@/lib/mothership/events' import { DELETED_WORKFLOW_LABEL } from '@/lib/workflows/workflow-labels' +import { CodeSearchOverlay } from '@/app/workspace/[workspaceId]/components/code-search-overlay/code-search-overlay' /** * Deep imports on purpose: importing these back through the parent `logs/components` * barrel forms a parent->child cycle that would keep the barrel edge to the snapshot @@ -158,7 +157,8 @@ export const WorkflowOutputSection = memo( - + {copied ? 'Copied' : 'Copy'} - + Search @@ -211,54 +207,18 @@ export const WorkflowOutputSection = memo( {/* Search Overlay */} {isSearchActive && ( -
e.stopPropagation()} - > - setSearchQuery(e.target.value)} - placeholder='Search...' - className='mr-0.5 w-[94px]' - /> - 0 ? 'text-[var(--text-secondary)]' : 'text-[var(--text-tertiary)]' - )} - > - {matchCount > 0 ? `${currentMatchIndex + 1}/${matchCount}` : '0/0'} - - - - -
+ )} {/* Context Menu - rendered in portal to avoid transform/overflow clipping */} @@ -517,7 +477,7 @@ export function LogDetailsContent({ log, onActiveTabChange }: LogDetailsContentP target='_blank' rel='noopener noreferrer' prefetch={false} - className='-mx-1.5 -my-0.5 group flex w-fit min-w-0 max-w-[calc(100%+0.75rem)] items-center gap-1.5 rounded-[5px] px-1.5 py-0.5 transition-colors hover-hover:bg-[var(--surface-active)] focus-visible:bg-[var(--surface-active)] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[color-mix(in_srgb,var(--text-muted)_30%,transparent)]' + className='-mx-1.5 -my-0.5 group flex w-fit min-w-0 max-w-[calc(100%+0.75rem)] items-center gap-1.5 rounded-sm px-1.5 py-0.5 transition-colors hover-hover:bg-[var(--surface-active)] focus-visible:bg-[var(--surface-active)] focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-[color-mix(in_srgb,var(--text-muted)_30%,transparent)]' > diff --git a/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx b/apps/sim/app/workspace/[workspaceId]/logs/logs.tsx index ff435d7d94b..e39b5b58892 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, @@ -198,7 +199,7 @@ function getColorIcon(color: string): React.ComponentType<{ className?: string } const ColorIcon = ({ className }: { className?: string }) => (
{selectedStatusColor && (
)} @@ -1561,13 +1562,9 @@ function LogsFilterPanel({ searchQuery, onSearchQueryChange }: LogsFilterPanelPr
{filtersActive && ( - + )}
) 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 2ea78d2cc95..2f049a1de59 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 @@ -14,7 +14,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 c2efa65b787..2a76a5c7997 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, @@ -402,14 +402,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.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]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx b/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx index f712133d9b9..878e7165988 100644 --- a/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx +++ b/apps/sim/app/workspace/[workspaceId]/settings/components/workflow-mcp-servers/workflow-mcp-servers.tsx @@ -607,7 +607,7 @@ function ServerDetailView({ : 'json' } wrapText - className='min-h-0! rounded-sm border border-[var(--border-1)]' + className='min-h-0!' /> {activeConfigTab === 'cursor' && ( + {props.config && ( )} - + ) } @@ -263,21 +263,14 @@ function ColumnConfigBody({ return (
-
+

Configure column

- -
+ + -
+ {/* `disabled` on the fieldset reaches every native control inside, including the comboboxes' trigger buttons; `contents` keeps the existing layout. Values stay readable and selectable. */} @@ -386,7 +379,7 @@ function ColumnConfigBody({ )} -
+
+
- -
+ + -
+
{enrichment.inputs.length === 0 ? ( @@ -368,7 +363,7 @@ export function EnrichmentConfig({ /> )} -
+
-
+ +

This enrichment ("{editGroup.enrichmentId}") is no longer available. Delete the column @@ -118,19 +109,12 @@ function EnrichmentsSidebarBody({ return (

-
+

Enrichments

- -
+ +
{field.label} diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-options-editor.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-options-editor.tsx index 4319140413d..0fba77cbe81 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-options-editor.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/select-field/select-options-editor.tsx @@ -80,7 +80,8 @@ export function SelectOptionsEditor({ options, onChange }: SelectOptionsEditorPr size='sm' onClick={() => remove(option.id)} iconPadding='sm' - className='size-7 shrink-0' + iconSize='regular' + className='shrink-0' aria-label={`Remove ${option.name || 'option'}`} > 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 bc1e125b79d..a1d885c9b1b 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 ? ( onRemove(rule.id)} iconPadding='sm' - className='size-7 shrink-0' + iconSize='regular' + className='shrink-0' aria-label='Remove filter' > diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx index 6e9045f2bd4..bb468ac963d 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx @@ -412,7 +412,7 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle alt='' width={12} height={12} - className='shrink-0 rounded-[2px]' + className='shrink-0 rounded-xs' onError={(e) => { 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 5fb467f4d3f..dfb6c820d33 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 0cabf2f1666..554b011d854 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' @@ -4694,15 +4694,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]/tables/[tableId]/components/table-sidebar-header/table-sidebar-header.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-header/table-sidebar-header.tsx new file mode 100644 index 00000000000..99892eb15fb --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-sidebar-header/table-sidebar-header.tsx @@ -0,0 +1,38 @@ +import { type ButtonHTMLAttributes, forwardRef, type ReactNode } from 'react' +import { Button } from '@sim/emcn' + +interface TableSidebarHeaderProps { + children: ReactNode +} + +/** Shared header row for table configuration sidebars. */ +export function TableSidebarHeader({ children }: TableSidebarHeaderProps) { + return ( +
+ {children} +
+ ) +} + +interface TableSidebarHeaderActionProps extends ButtonHTMLAttributes { + 'aria-label': string +} + +/** Button treatment for actions in a table sidebar header. */ +export const TableSidebarHeaderAction = forwardRef< + HTMLButtonElement, + TableSidebarHeaderActionProps +>(({ className, ...props }, ref) => ( + + )}

- -
+ + -
+ {/* Single-output mode renames this column directly. */} {isEditOutputMode && ( <> @@ -717,26 +707,18 @@ export function WorkflowSidebarBody({ />
{!isEnrichment && ( - - - - - Open workflow - + + window.open( + `/workspace/${workspaceId}/w/${selectedWorkflowId}`, + '_blank', + 'noopener,noreferrer' + ) + } + > + + )} ) : ( @@ -837,23 +819,9 @@ export function WorkflowSidebarBody({ )} {selectedWorkflowId && ( <> -
- - - -
+ setShowAdvanced((v) => !v)}> + {showAdvanced ? 'Hide additional fields' : 'Show additional fields'} + {showAdvanced && ( <> )} -
+
+ 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 88478ea513c..5851d2bb03b 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/api/api.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx index e4b63ed76e0..2aa6c76129d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/api/api.tsx @@ -480,7 +480,7 @@ console.log(limits);` code={getSyncCommand()} language={LANGUAGE_SYNTAX[language]} wrapText - className='min-h-0! rounded-sm border border-[var(--border-1)]' + className='min-h-0!' />
@@ -518,7 +518,7 @@ console.log(limits);` code={getStreamCommand()} language={LANGUAGE_SYNTAX[language]} wrapText - className='min-h-0! rounded-sm border border-[var(--border-1)]' + className='min-h-0!' />
@@ -562,7 +562,7 @@ console.log(limits);` code={getAsyncCommand()} language={LANGUAGE_SYNTAX[language]} wrapText - className='min-h-0! rounded-sm border border-[var(--border-1)]' + className='min-h-0!' />
)} 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..3330346065d --- /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,84 @@ +import type { ComponentProps, PropsWithChildren } from 'react' +import { deploymentShapeMock } from '@sim/testing/mocks/deployment-shape.mock' +import { emcnMock } from '@sim/testing/mocks/emcn.mock' +import { emcnIconsMock } from '@sim/testing/mocks/emcn-icons.mock' +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', () => ({ + ...emcnMock, + Input: (props: ComponentProps<'input'>) => , + Label: (props: ComponentProps<'label'>) => ( + + ), + Tooltip: { + Root: ({ children }: PropsWithChildren) => <>{children}, + Trigger: ({ children }: PropsWithChildren) => <>{children}, + Content: ({ children }: PropsWithChildren) => <>{children}, + }, +})) +vi.mock('@sim/emcn/icons', () => emcnIconsMock) +vi.mock('@/components/ui', () => ({ GeneratedPasswordInput: () => null })) +vi.mock('@/lib/core/config/deployment-shape', () => deploymentShapeMock) +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 8ce604f967b..b90788f6f3b 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 existingChat: ExistingChat | null @@ -331,12 +344,7 @@ export function ChatDeploy({ return ( <> - + {errors.general && (
@@ -354,24 +362,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} + /> )} -
+ -
+
- {errors.outputBlocks && ( -

- {errors.outputBlocks} -

- )} + {errors.outputBlocks && {errors.outputBlocks}}
-
+
@@ -403,7 +404,7 @@ export function ChatDeploy({ />
-
+
@@ -430,23 +431,17 @@ export function ChatDeploy({ hasExistingPassword={existingPassword} error={errors.password || errors.emails} /> -
- -