feat: light mode (#2457)

* feat(light): restored light theme

* feat: styling consolidation, note block upgrades

* improvement(zoom-prevention): moved downstream

* fix(notifications): mismatching workflow ID

* feat(light): button variant updates and controls consolidation

* improvement: UI consolidation

* feat: badges, usage limit; fix(note): iframe security; improvement(s-modal): sizing

* improvement: oauth modal, subscription

* improvement(team): ui/ux

* feat: emcn, subscription, tool input

* improvement(copilot): styling consolidation

* feat: colors consolidation

* improvement(ui): light styling

* fix(build): unused billing component

* improvement: addressed comments
This commit is contained in:
Emir Karabeg
2025-12-26 12:45:06 -08:00
committed by GitHub
parent 88cda3a9ce
commit 1f0e3f2be6
184 changed files with 4002 additions and 4472 deletions

View File

@@ -2,7 +2,7 @@
import { useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Loader2, X } from 'lucide-react'
import { X } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
@@ -499,16 +499,11 @@ export default function CareersPage() {
className='min-w-[200px] rounded-[10px] border border-[#6F3DFA] bg-gradient-to-b from-[#8357FF] to-[#6F3DFA] text-white shadow-[inset_0_2px_4px_0_#9B77FF] transition-all duration-300 hover:opacity-90 disabled:opacity-50'
size='lg'
>
{isSubmitting ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Submitting...
</>
) : submitStatus === 'success' ? (
'Submitted'
) : (
'Submit Application'
)}
{isSubmitting
? 'Submitting...'
: submitStatus === 'success'
? 'Submitted'
: 'Submit Application'}
</Button>
</div>
</form>

View File

@@ -1,5 +1,6 @@
'use client'
import type { ComponentType, SVGProps } from 'react'
import { useState } from 'react'
import { createLogger } from '@sim/logger'
import type { LucideIcon } from 'lucide-react'
@@ -24,7 +25,7 @@ import {
const logger = createLogger('LandingPricing')
interface PricingFeature {
icon: LucideIcon
icon: LucideIcon | ComponentType<SVGProps<SVGSVGElement>>
text: string
}

View File

@@ -7,7 +7,7 @@ import { ThemeProvider as NextThemesProvider } from 'next-themes'
export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
const pathname = usePathname()
// Force light mode on public/marketing pages, dark mode everywhere else
// Force light mode on public/marketing pages, allow user preference elsewhere
const isLightModePage =
pathname === '/' ||
pathname.startsWith('/login') ||
@@ -27,10 +27,10 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) {
<NextThemesProvider
attribute='class'
defaultTheme='dark'
enableSystem={false}
enableSystem
disableTransitionOnChange
storageKey='sim-theme'
forcedTheme={isLightModePage ? 'light' : 'dark'}
forcedTheme={isLightModePage ? 'light' : undefined}
{...props}
>
{children}

View File

@@ -1,33 +0,0 @@
'use client'
import { useEffect } from 'react'
export function ZoomPrevention() {
useEffect(() => {
const preventZoom = (e: KeyboardEvent | WheelEvent) => {
// Prevent zoom on ctrl/cmd + wheel
if (e instanceof WheelEvent && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
}
// Prevent zoom on ctrl/cmd + plus/minus/zero
if (e instanceof KeyboardEvent && (e.ctrlKey || e.metaKey)) {
if (e.key === '=' || e.key === '-' || e.key === '0') {
e.preventDefault()
}
}
}
// Add event listeners
document.addEventListener('wheel', preventZoom, { passive: false })
document.addEventListener('keydown', preventZoom)
// Cleanup
return () => {
document.removeEventListener('wheel', preventZoom)
document.removeEventListener('keydown', preventZoom)
}
}, [])
return null
}

View File

@@ -8,7 +8,7 @@
*/
:root {
--sidebar-width: 232px;
--panel-width: 244px;
--panel-width: 260px;
--toolbar-triggers-height: 300px;
--editor-connections-height: 200px;
--terminal-height: 196px;
@@ -26,41 +26,6 @@
height: var(--terminal-height);
}
/**
* Workflow component z-index fixes and background colors
*/
.workflow-container .react-flow__edges {
z-index: 0 !important;
}
.workflow-container .react-flow__node {
z-index: 21 !important;
}
.workflow-container .react-flow__handle {
z-index: 30 !important;
}
.workflow-container .react-flow__edge [data-testid="workflow-edge"] {
z-index: 0 !important;
}
.workflow-container .react-flow__edge-labels {
z-index: 60 !important;
}
.workflow-container,
.workflow-container .react-flow__pane,
.workflow-container .react-flow__renderer {
background-color: var(--bg) !important;
}
.dark .workflow-container,
.dark .workflow-container .react-flow__pane,
.dark .workflow-container .react-flow__renderer {
background-color: var(--bg) !important;
}
/**
* Landing loop animation styles (keyframes defined in tailwind.config.ts)
*/
@@ -75,101 +40,87 @@
}
/**
* Dark color tokens - single source of truth for all colors (dark-only)
* Color tokens - single source of truth for all colors
* Light mode: Warm theme
* Dark mode: Dark neutral theme
*/
@layer base {
:root,
.light {
/* Neutrals (surfaces) - shadcn stone palette */
--bg: #ffffff; /* pure white for landing/auth pages */
--surface-1: #fafaf9; /* stone-50 */
--surface-2: #ffffff; /* white */
--surface-3: #f5f5f4; /* stone-100 */
--surface-4: #f5f5f4; /* stone-100 */
--surface-5: #eeedec; /* stone-150 */
--surface-6: #f5f5f4; /* stone-100 */
--surface-9: #f5f5f4; /* stone-100 */
--surface-11: #e7e5e4; /* stone-200 */
--surface-12: #d6d3d1; /* stone-300 */
--surface-13: #a8a29e; /* stone-400 */
--surface-14: #78716c; /* stone-500 */
--surface-15: #57534e; /* stone-600 */
--surface-elevated: #ffffff; /* white */
--bg-strong: #e7e5e4; /* stone-200 */
--bg: #f9faf8; /* main canvas - near white */
--surface-1: #f9faf8; /* sidebar, panels - light warm gray */
--surface-2: #fdfdfb; /* blocks, cards, modals - soft warm white */
--surface-3: #f4f5f1; /* popovers, headers - more contrast */
--surface-4: #f2f3ef; /* buttons base */
--border: #d7dcda; /* primary border */
--surface-5: #f0f1ed; /* inputs, form elements - subtle */
--border-1: #d7dcda; /* stronger border - sage gray */
--surface-6: #eceee9; /* popovers, elevated surfaces */
--surface-7: #e8e9e4;
/* Text - shadcn stone palette for proper contrast */
--text-primary: #1c1917; /* stone-900 */
--text-secondary: #292524; /* stone-800 */
--text-tertiary: #57534e; /* stone-600 */
--text-muted: #78716c; /* stone-500 */
--text-subtle: #a8a29e; /* stone-400 */
--text-inverse: #fafaf9; /* stone-50 */
--text-error: #dc2626;
--workflow-edge: #d7dcda; /* workflow handles/edges - matches border-1 */
/* Borders / dividers - shadcn stone palette */
--border: #d6d3d1; /* stone-300 */
--border-strong: #d6d3d1; /* stone-300 */
--divider: #e7e5e4; /* stone-200 */
--border-muted: #e7e5e4; /* stone-200 */
--border-success: #d6d3d1; /* stone-300 */
/* Text - warm neutrals */
--text-primary: #2d2d2d;
--text-secondary: #404040;
--text-tertiary: #5c5c5c;
--text-muted: #737373;
--text-subtle: #8c8c8c;
--text-inverse: #f0fff6;
--text-error: #ef4444;
/* Borders / dividers */
--divider: #e8e9e4;
--border-muted: #dfe0db;
--border-success: #d7dcda;
/* Brand & state */
--brand-400: #8e4cfb;
--brand-500: #6f3dfa;
--brand-secondary: #33b4ff;
--brand-tertiary: #22c55e;
--brand-tertiary-2: #33c481;
--brand-tertiary-2: #32bd7e;
--warning: #ea580c;
/* Utility */
--white: #ffffff;
/* Font weights - lighter for light mode (-20 from dark) */
/* Font weights - lighter for light mode */
--font-weight-base: 430;
--font-weight-medium: 450;
--font-weight-semibold: 500;
/* RGB for opacity usage - stone palette */
--surface-4-rgb: 245 245 244; /* stone-100 */
--surface-5-rgb: 238 237 236; /* stone-150 */
--surface-7-rgb: 245 245 244; /* stone-100 */
--surface-9-rgb: 245 245 244; /* stone-100 */
--divider-rgb: 231 229 228; /* stone-200 */
--white-rgb: 255 255 255;
--black-rgb: 0 0 0;
/* Extended palette - mapped to shadcn stone palette */
--c-0D0D0D: #0c0a09; /* stone-950 */
--c-1A1A1A: #1c1917; /* stone-900 */
--c-1F1F1F: #1c1917; /* stone-900 */
--c-2A2A2A: #292524; /* stone-800 */
--c-383838: #44403c; /* stone-700 */
--c-414141: #57534e; /* stone-600 */
/* Extended palette */
--c-0D0D0D: #0d0d0d;
--c-1A1A1A: #1a1a1a;
--c-1F1F1F: #1f1f1f;
--c-2A2A2A: #2a2a2a;
--c-383838: #383838;
--c-414141: #414141;
--c-442929: #442929;
--c-491515: #491515;
--c-575757: #78716c; /* stone-500 */
--c-686868: #78716c; /* stone-500 */
--c-707070: #78716c; /* stone-500 */
--c-727272: #78716c; /* stone-500 */
--c-737373: #78716c; /* stone-500 */
--c-808080: #a8a29e; /* stone-400 */
--c-858585: #a8a29e; /* stone-400 */
--c-868686: #a8a29e; /* stone-400 */
--c-8D8D8D: #a8a29e; /* stone-400 */
--c-939393: #a8a29e; /* stone-400 */
--c-A8A8A8: #a8a29e; /* stone-400 */
--c-B8B8B8: #d6d3d1; /* stone-300 */
--c-C0C0C0: #d6d3d1; /* stone-300 */
--c-CDCDCD: #d6d3d1; /* stone-300 */
--c-D0D0D0: #d6d3d1; /* stone-300 */
--c-D2D2D2: #d6d3d1; /* stone-300 */
--c-E0E0E0: #e7e5e4; /* stone-200 */
--c-E5E5E5: #e7e5e4; /* stone-200 */
--c-E8E8E8: #e7e5e4; /* stone-200 */
--c-EEEEEE: #f5f5f4; /* stone-100 */
--c-F0F0F0: #f5f5f4; /* stone-100 */
--c-F4F4F4: #fafaf9; /* stone-50 */
--c-F5F5F5: #fafaf9; /* stone-50 */
--c-575757: #575757;
--c-686868: #686868;
--c-707070: #707070;
--c-727272: #727272;
--c-737373: #737373;
--c-808080: #808080;
--c-858585: #858585;
--c-868686: #868686;
--c-8D8D8D: #8d8d8d;
--c-939393: #939393;
--c-A8A8A8: #a8a8a8;
--c-B8B8B8: #b8b8b8;
--c-C0C0C0: #c0c0c0;
--c-CDCDCD: #cdcdcd;
--c-D0D0D0: #d0d0d0;
--c-D2D2D2: #d2d2d2;
--c-E0E0E0: #e0e0e0;
--c-E5E5E5: #e5e5e5;
--c-E8E8E8: #e8e8e8;
--c-EEEEEE: #eeeeee;
--c-F0F0F0: #f0f0f0;
--c-F4F4F4: #f4f4f4;
--c-F5F5F5: #f5f5f5;
/* Blues and cyans */
--c-00B0B0: #00b0b0;
@@ -203,30 +154,27 @@
/* Terminal status badges */
--terminal-status-error-bg: #feeeee;
--terminal-status-error-border: #f87171;
--terminal-status-info-bg: #f5f5f4; /* stone-100 */
--terminal-status-info-border: #a8a29e; /* stone-400 */
--terminal-status-info-color: #57534e; /* stone-600 */
--terminal-status-info-bg: #f5f5f4;
--terminal-status-info-border: #a8a29e;
--terminal-status-info-color: #57534e;
--terminal-status-warning-bg: #fef9e7;
--terminal-status-warning-border: #f5c842;
--terminal-status-warning-color: #a16207;
}
.dark {
/* Neutrals (surfaces) */
/* Surface */
--bg: #1b1b1b;
--surface-1: #1e1e1e;
--surface-2: #232323;
--surface-3: #242424;
--surface-4: #252525;
--surface-5: #272727;
--surface-6: #282828;
--surface-9: #363636;
--surface-11: #3d3d3d;
--surface-12: #434343;
--surface-13: #454545;
--surface-14: #4a4a4a;
--surface-15: #5a5a5a;
--surface-elevated: #202020;
--bg-strong: #0c0c0c;
--surface-4: #292929;
--border: #2c2c2c;
--surface-5: #363636;
--border-1: #3d3d3d;
--surface-6: #454545;
--surface-7: #454545;
--workflow-edge: #454545; /* workflow handles/edges - same as surface-6 in dark */
/* Text */
--text-primary: #e6e6e6;
@@ -237,9 +185,7 @@
--text-inverse: #1b1b1b;
--text-error: #ef4444;
/* Borders / dividers */
--border: #2c2c2c;
--border-strong: #303030;
/* --border-strong: #303030; */
--divider: #393939;
--border-muted: #424242;
--border-success: #575757;
@@ -248,7 +194,7 @@
--brand-400: #8e4cfb;
--brand-secondary: #33b4ff;
--brand-tertiary: #22c55e;
--brand-tertiary-2: #33c481;
--brand-tertiary-2: #32bd7e;
--warning: #ff6600;
/* Utility */
@@ -259,15 +205,6 @@
--font-weight-medium: 480;
--font-weight-semibold: 550;
/* RGB for opacity usage */
--surface-4-rgb: 37 37 37;
--surface-5-rgb: 39 39 39;
--surface-7-rgb: 44 44 44;
--surface-9-rgb: 54 54 54;
--divider-rgb: 57 57 57;
--white-rgb: 255 255 255;
--black-rgb: 0 0 0;
/* Extended palette (exhaustive from code usage via -[#...]) */
/* Neutral deep shades */
--c-0D0D0D: #0d0d0d;
@@ -395,34 +332,34 @@
}
::-webkit-scrollbar-thumb {
background-color: var(--surface-12);
background-color: var(--surface-7);
border-radius: var(--radius);
}
::-webkit-scrollbar-thumb:hover {
background-color: var(--surface-13);
background-color: var(--surface-7);
}
/* Dark Mode Global Scrollbar */
.dark ::-webkit-scrollbar-track {
background: var(--surface-5);
background: var(--surface-4);
}
.dark ::-webkit-scrollbar-thumb {
background-color: var(--surface-12);
background-color: var(--surface-7);
}
.dark ::-webkit-scrollbar-thumb:hover {
background-color: var(--surface-13);
background-color: var(--surface-7);
}
* {
scrollbar-width: thin;
scrollbar-color: var(--surface-12) var(--surface-1);
scrollbar-color: var(--surface-7) var(--surface-1);
}
.dark * {
scrollbar-color: var(--surface-12) var(--surface-5);
scrollbar-color: var(--surface-7) var(--surface-4);
}
.copilot-scrollable {
@@ -438,8 +375,8 @@
}
.panel-tab-active {
background-color: var(--white);
color: var(--text-inverse);
background-color: var(--surface-5);
color: var(--text-primary);
border-color: var(--border-muted);
}
@@ -450,7 +387,7 @@
}
.panel-tab-inactive:hover {
background-color: var(--surface-9);
background-color: var(--surface-5);
color: var(--text-primary);
}
@@ -642,7 +579,7 @@ input[type="search"]::-ms-clear {
}
html[data-panel-active-tab="copilot"] .panel-container [data-tab-button="copilot"] {
background-color: var(--surface-11) !important;
background-color: var(--border-1) !important;
color: var(--text-primary) !important;
}
html[data-panel-active-tab="copilot"] .panel-container [data-tab-button="toolbar"],
@@ -652,7 +589,7 @@ input[type="search"]::-ms-clear {
}
html[data-panel-active-tab="toolbar"] .panel-container [data-tab-button="toolbar"] {
background-color: var(--surface-11) !important;
background-color: var(--border-1) !important;
color: var(--text-primary) !important;
}
html[data-panel-active-tab="toolbar"] .panel-container [data-tab-button="copilot"],
@@ -662,7 +599,7 @@ input[type="search"]::-ms-clear {
}
html[data-panel-active-tab="editor"] .panel-container [data-tab-button="editor"] {
background-color: var(--surface-11) !important;
background-color: var(--border-1) !important;
color: var(--text-primary) !important;
}
html[data-panel-active-tab="editor"] .panel-container [data-tab-button="copilot"],

View File

@@ -93,6 +93,7 @@ export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const context = searchParams.get('context') || 'user'
const contextId = searchParams.get('id')
const includeOrg = searchParams.get('includeOrg') === 'true'
// Validate context parameter
if (!['user', 'organization'].includes(context)) {
@@ -115,14 +116,38 @@ export async function GET(request: NextRequest) {
if (context === 'user') {
// Get user billing (may include organization if they're part of one)
billingData = await getSimplifiedBillingSummary(session.user.id, contextId || undefined)
// Attach effective billing blocked status (includes org owner check)
const billingStatus = await getEffectiveBillingStatus(session.user.id)
billingData = {
...billingData,
billingBlocked: billingStatus.billingBlocked,
billingBlockedReason: billingStatus.billingBlockedReason,
blockedByOrgOwner: billingStatus.blockedByOrgOwner,
}
// Optionally include organization membership and role
if (includeOrg) {
const userMembership = await db
.select({
organizationId: member.organizationId,
role: member.role,
})
.from(member)
.where(eq(member.userId, session.user.id))
.limit(1)
if (userMembership.length > 0) {
billingData = {
...billingData,
organization: {
id: userMembership[0].organizationId,
role: userMembership[0].role as 'owner' | 'admin' | 'member',
},
}
}
}
} else {
// Get user role in organization for permission checks first
const memberRecord = await db

View File

@@ -2,7 +2,6 @@
import { type KeyboardEvent, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Loader2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { InputOTP, InputOTPGroup, InputOTPSlot } from '@/components/ui/input-otp'
@@ -299,14 +298,7 @@ export default function EmailAuth({
className={`${buttonClass} flex w-full items-center justify-center gap-2 rounded-[10px] border font-medium text-[15px] text-white transition-all duration-200`}
disabled={isSendingOtp}
>
{isSendingOtp ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Sending Code...
</>
) : (
'Continue'
)}
{isSendingOtp ? 'Sending Code...' : 'Continue'}
</Button>
</form>
) : (

View File

@@ -162,14 +162,7 @@ export function InviteStatusCard({
onClick={action.onClick}
disabled={action.disabled || action.loading}
>
{action.loading ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
{action.label}...
</>
) : (
action.label
)}
{action.loading ? `${action.label}...` : action.label}
</Button>
))}
</div>

View File

@@ -11,7 +11,6 @@ import { HydrationErrorHandler } from '@/app/_shell/hydration-error-handler'
import { QueryProvider } from '@/app/_shell/providers/query-provider'
import { SessionProvider } from '@/app/_shell/providers/session-provider'
import { ThemeProvider } from '@/app/_shell/providers/theme-provider'
import { ZoomPrevention } from '@/app/_shell/zoom-prevention'
import { season } from '@/app/_styles/fonts/season/season'
export const viewport: Viewport = {
@@ -85,7 +84,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
var panelWidth = panelState && panelState.panelWidth;
var maxPanelWidth = window.innerWidth * 0.4;
if (panelWidth >= 244 && panelWidth <= maxPanelWidth) {
if (panelWidth >= 260 && panelWidth <= maxPanelWidth) {
document.documentElement.style.setProperty('--panel-width', panelWidth + 'px');
} else if (panelWidth > maxPanelWidth) {
document.documentElement.style.setProperty('--panel-width', maxPanelWidth + 'px');
@@ -190,10 +189,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<ThemeProvider>
<QueryProvider>
<SessionProvider>
<BrandedLayout>
<ZoomPrevention />
{children}
</BrandedLayout>
<BrandedLayout>{children}</BrandedLayout>
</SessionProvider>
</QueryProvider>
</ThemeProvider>

View File

@@ -317,10 +317,10 @@ export default function PlaygroundPage() {
</PopoverContent>
</Popover>
</VariantRow>
<VariantRow label='primary variant'>
<Popover variant='primary'>
<VariantRow label='secondary variant'>
<Popover variant='secondary'>
<PopoverTrigger asChild>
<Button variant='primary'>Primary Popover</Button>
<Button variant='secondary'>Secondary Popover</Button>
</PopoverTrigger>
<PopoverContent>
<PopoverItem>Item 1</PopoverItem>
@@ -550,7 +550,7 @@ export default function PlaygroundPage() {
].map(({ Icon, name }) => (
<Tooltip.Root key={name}>
<Tooltip.Trigger asChild>
<div className='flex h-10 w-10 cursor-pointer items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] transition-colors hover:bg-[var(--surface-5)]'>
<div className='flex h-10 w-10 cursor-pointer items-center justify-center rounded-md border border-[var(--border)] bg-[var(--surface-2)] transition-colors hover:bg-[var(--surface-4)]'>
<Icon className='h-5 w-5 text-[var(--text-secondary)]' />
</div>
</Tooltip.Trigger>

View File

@@ -732,7 +732,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
<>
{!currentUserId ? (
<Button
variant='primary'
variant='tertiary'
onClick={() => {
const callbackUrl =
isWorkspaceContext && workspaceId
@@ -748,7 +748,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
</Button>
) : isWorkspaceContext ? (
<Button
variant='primary'
variant='tertiary'
onClick={handleUseTemplate}
disabled={isUsing}
className='!text-[#FFFFFF] h-[32px] rounded-[6px] px-[12px] text-[14px]'
@@ -832,7 +832,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
/>
</div>
) : (
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-elevated)]'>
<div className='flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-2)]'>
<User className='h-[14px] w-[14px] text-[var(--text-muted)]' />
</div>
)}
@@ -1001,7 +1001,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
/>
</div>
) : (
<div className='flex h-[48px] w-[48px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-elevated)]'>
<div className='flex h-[48px] w-[48px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-2)]'>
<User className='h-[24px] w-[24px] text-[var(--text-muted)]' />
</div>
)}

View File

@@ -27,15 +27,15 @@ interface TemplateCardProps {
export function TemplateCardSkeleton({ className }: { className?: string }) {
return (
<div className={cn('h-[268px] w-full rounded-[8px] bg-[#202020] p-[8px]', className)}>
<div className='h-[180px] w-full animate-pulse rounded-[6px] bg-gray-700' />
<div className={cn('h-[268px] w-full rounded-[8px] bg-[var(--surface-3)] p-[8px]', className)}>
<div className='h-[180px] w-full animate-pulse rounded-[6px] bg-[var(--surface-5)]' />
<div className='mt-[14px] flex items-center justify-between'>
<div className='h-4 w-32 animate-pulse rounded bg-gray-700' />
<div className='h-4 w-32 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='flex items-center gap-[-4px]'>
{Array.from({ length: 3 }).map((_, index) => (
<div
key={index}
className='h-[18px] w-[18px] animate-pulse rounded-[4px] bg-gray-700'
className='h-[18px] w-[18px] animate-pulse rounded-[4px] bg-[var(--surface-5)]'
/>
))}
</div>
@@ -43,14 +43,14 @@ export function TemplateCardSkeleton({ className }: { className?: string }) {
<div className='mt-[14px] flex items-center justify-between'>
<div className='flex items-center gap-[6px]'>
<div className='h-[20px] w-[20px] animate-pulse rounded-full bg-gray-700' />
<div className='h-3 w-20 animate-pulse rounded bg-gray-700' />
<div className='h-[20px] w-[20px] animate-pulse rounded-full bg-[var(--surface-5)]' />
<div className='h-3 w-20 animate-pulse rounded bg-[var(--surface-5)]' />
</div>
<div className='flex items-center gap-[6px]'>
<div className='h-3 w-3 animate-pulse rounded bg-gray-700' />
<div className='h-3 w-6 animate-pulse rounded bg-gray-700' />
<div className='h-3 w-3 animate-pulse rounded bg-gray-700' />
<div className='h-3 w-6 animate-pulse rounded bg-gray-700' />
<div className='h-3 w-3 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-6 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-3 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-6 animate-pulse rounded bg-[var(--surface-5)]' />
</div>
</div>
</div>
@@ -195,7 +195,7 @@ function TemplateCardInner({
return (
<div
onClick={handleCardClick}
className={cn('w-full cursor-pointer rounded-[8px] bg-[#202020] p-[8px]', className)}
className={cn('w-full cursor-pointer rounded-[8px] bg-[var(--surface-3)] p-[8px]', className)}
>
<div
ref={previewRef}
@@ -213,12 +213,14 @@ function TemplateCardInner({
lightweight
/>
) : (
<div className='h-full w-full bg-[#2A2A2A]' />
<div className='h-full w-full bg-[var(--surface-4)]' />
)}
</div>
<div className='mt-[10px] flex items-center justify-between'>
<h3 className='truncate pr-[8px] pl-[2px] font-medium text-[16px] text-white'>{title}</h3>
<h3 className='truncate pr-[8px] pl-[2px] font-medium text-[16px] text-[var(--text-primary)]'>
{title}
</h3>
<div className='flex flex-shrink-0'>
{blockTypes.length > 4 ? (
@@ -241,10 +243,12 @@ function TemplateCardInner({
)
})}
<div
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[#4A4A4A]'
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[var(--surface-7)]'
style={{ marginLeft: '-4px' }}
>
<span className='font-medium text-[10px] text-white'>+{blockTypes.length - 3}</span>
<span className='font-medium text-[10px] text-[var(--text-primary)]'>
+{blockTypes.length - 3}
</span>
</div>
</>
) : (
@@ -276,24 +280,26 @@ function TemplateCardInner({
<img src={authorImageUrl} alt={author} className='h-full w-full object-cover' />
</div>
) : (
<div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-full bg-[#4A4A4A]'>
<User className='h-[12px] w-[12px] text-[#888888]' />
<div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-7)]'>
<User className='h-[12px] w-[12px] text-[var(--text-muted)]' />
</div>
)}
<div className='flex min-w-0 items-center gap-[4px]'>
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span>
<span className='truncate font-medium text-[12px] text-[var(--text-muted)]'>
{author}
</span>
{isVerified && <VerifiedBadge size='sm' />}
</div>
</div>
<div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[#888888] text-[12px]'>
<div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[12px] text-[var(--text-muted)]'>
<User className='h-[12px] w-[12px]' />
<span>{usageCount}</span>
<Star
onClick={handleStarClick}
className={cn(
'h-[12px] w-[12px] cursor-pointer transition-colors',
isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[#888888]',
isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[var(--text-muted)]',
isStarLoading && 'opacity-50'
)}
/>

View File

@@ -149,7 +149,7 @@ export default function Templates({
</div>
<div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input
placeholder='Search'

View File

@@ -294,14 +294,14 @@ function UnsubscribeContent() {
variant='destructive'
className='w-full'
>
{processing ? (
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
) : data?.currentPreferences.unsubscribeAll ? (
{data?.currentPreferences.unsubscribeAll ? (
<CheckCircle className='mr-2 h-4 w-4' />
) : null}
{data?.currentPreferences.unsubscribeAll
? 'Unsubscribed from All Emails'
: 'Unsubscribe from All Marketing Emails'}
{processing
? 'Unsubscribing...'
: data?.currentPreferences.unsubscribeAll
? 'Unsubscribed from All Emails'
: 'Unsubscribe from All Marketing Emails'}
</Button>
<div className='text-center text-muted-foreground text-sm'>

View File

@@ -2,7 +2,7 @@
import { useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { AlertCircle, Loader2 } from 'lucide-react'
import { AlertCircle } from 'lucide-react'
import {
Button,
Label,
@@ -157,19 +157,12 @@ export function CreateChunkModal({
Cancel
</Button>
<Button
variant='primary'
variant='tertiary'
onClick={handleCreateChunk}
type='button'
disabled={!isFormValid || isCreating}
>
{isCreating ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Creating...
</>
) : (
'Create Chunk'
)}
{isCreating ? 'Creating...' : 'Create Chunk'}
</Button>
</ModalFooter>
</form>
@@ -181,7 +174,7 @@ export function CreateChunkModal({
<ModalContent size='sm'>
<ModalHeader>Discard Changes</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
You have unsaved changes. Are you sure you want to close without saving?
</p>
</ModalBody>
@@ -193,12 +186,7 @@ export function CreateChunkModal({
>
Keep Editing
</Button>
<Button
variant='primary'
onClick={handleConfirmDiscard}
type='button'
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
<Button variant='destructive' onClick={handleConfirmDiscard} type='button'>
Discard Changes
</Button>
</ModalFooter>

View File

@@ -69,7 +69,7 @@ export function DeleteChunkModal({
<ModalContent size='sm'>
<ModalHeader>Delete Chunk</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete this chunk?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
@@ -78,12 +78,7 @@ export function DeleteChunkModal({
<Button variant='active' disabled={isDeleting} onClick={onClose}>
Cancel
</Button>
<Button
variant='primary'
onClick={handleDeleteChunk}
disabled={isDeleting}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
<Button variant='destructive' onClick={handleDeleteChunk} disabled={isDeleting}>
{isDeleting ? <>Deleting...</> : <>Delete</>}
</Button>
</ModalFooter>

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Loader2 } from 'lucide-react'
import {
Button,
Combobox,
@@ -575,7 +574,7 @@ export function DocumentTagsModal({
Cancel
</Button>
<Button
variant='primary'
variant='tertiary'
onClick={saveDocumentTag}
className='flex-1'
disabled={!canSaveTag}
@@ -741,7 +740,7 @@ export function DocumentTagsModal({
</Button>
)}
<Button
variant='primary'
variant='tertiary'
onClick={saveDocumentTag}
className='flex-1'
disabled={
@@ -755,14 +754,7 @@ export function DocumentTagsModal({
))
}
>
{isSavingTag ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Creating...
</>
) : (
'Create Tag'
)}
{isSavingTag ? 'Creating...' : 'Create Tag'}
</Button>
</div>
</div>

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from 'react'
import * as DialogPrimitive from '@radix-ui/react-dialog'
import { createLogger } from '@sim/logger'
import { AlertCircle, ChevronDown, ChevronUp, Loader2, X } from 'lucide-react'
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react'
import {
Button,
Label,
@@ -275,19 +275,12 @@ export function EditChunkModal({
</Button>
{userPermissions.canEdit && (
<Button
variant='primary'
variant='tertiary'
onClick={handleSaveContent}
type='button'
disabled={!isFormValid || isSaving || !hasUnsavedChanges || isNavigating}
>
{isSaving ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Saving...
</>
) : (
'Save'
)}
{isSaving ? 'Saving...' : 'Save'}
</Button>
)}
</ModalFooter>
@@ -300,7 +293,7 @@ export function EditChunkModal({
<ModalContent size='sm'>
<ModalHeader>Unsaved Changes</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
You have unsaved changes to this chunk content.
{pendingNavigation
? ' Do you want to discard your changes and navigate to the next chunk?'
@@ -318,12 +311,7 @@ export function EditChunkModal({
>
Keep Editing
</Button>
<Button
variant='primary'
onClick={handleConfirmDiscard}
type='button'
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
<Button variant='destructive' onClick={handleConfirmDiscard} type='button'>
Discard Changes
</Button>
</ModalFooter>

View File

@@ -234,7 +234,7 @@ function DocumentLoading({
</div>
<div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input
placeholder='Search chunks...'
@@ -242,7 +242,7 @@ function DocumentLoading({
className='flex-1 border-0 bg-transparent px-0 font-medium text-[var(--text-secondary)] text-small leading-none placeholder:text-[var(--text-subtle)] focus-visible:ring-0 focus-visible:ring-offset-0'
/>
</div>
<Button disabled variant='primary' className='h-[32px] rounded-[6px]'>
<Button disabled variant='tertiary' className='h-[32px] rounded-[6px]'>
Create Chunk
</Button>
</div>
@@ -851,7 +851,7 @@ export function Document({
</div>
<div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input
placeholder={
@@ -882,7 +882,7 @@ export function Document({
<Button
onClick={() => setIsCreateChunkModalOpen(true)}
disabled={documentData?.processingStatus === 'failed' || !userPermissions.canEdit}
variant='primary'
variant='tertiary'
className='h-[32px] rounded-[6px]'
>
Create Chunk
@@ -1092,18 +1092,13 @@ export function Document({
)}
{documentData?.processingStatus === 'completed' && totalPages > 1 && (
<div className='flex items-center justify-center border-t bg-background px-6 py-4'>
<div className='flex items-center justify-center border-t bg-background px-4 pt-[10px]'>
<div className='flex items-center gap-1'>
<Button
variant='ghost'
onClick={prevPage}
disabled={!hasPrevPage}
className='h-8 w-8 p-0'
>
<ChevronLeft className='h-4 w-4' />
<Button variant='ghost' onClick={prevPage} disabled={!hasPrevPage}>
<ChevronLeft className='h-3.5 w-3.5' />
</Button>
<div className='mx-4 flex items-center gap-6'>
<div className='mx-[12px] flex items-center gap-[16px]'>
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
let page: number
if (totalPages <= 5) {
@@ -1133,13 +1128,8 @@ export function Document({
})}
</div>
<Button
variant='ghost'
onClick={nextPage}
disabled={!hasNextPage}
className='h-8 w-8 p-0'
>
<ChevronRight className='h-4 w-4' />
<Button variant='ghost' onClick={nextPage} disabled={!hasNextPage}>
<ChevronRight className='h-3.5 w-3.5' />
</Button>
</div>
</div>
@@ -1229,7 +1219,7 @@ export function Document({
<ModalContent size='sm'>
<ModalHeader>Delete Document</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete "{effectiveDocumentName}"? This will permanently
delete the document and all {documentData?.chunkCount ?? 0} chunk
{documentData?.chunkCount === 1 ? '' : 's'} within it.{' '}
@@ -1245,10 +1235,9 @@ export function Document({
Cancel
</Button>
<Button
variant='primary'
variant='destructive'
onClick={handleDeleteDocument}
disabled={isDeletingDocument}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
{isDeletingDocument ? 'Deleting...' : 'Delete Document'}
</Button>

View File

@@ -185,7 +185,7 @@ function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoadingProps)
</div>
<div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input
placeholder='Search documents...'
@@ -193,7 +193,7 @@ function KnowledgeBaseLoading({ knowledgeBaseName }: KnowledgeBaseLoadingProps)
className='flex-1 border-0 bg-transparent px-0 font-medium text-[var(--text-secondary)] text-small leading-none placeholder:text-[var(--text-subtle)] focus-visible:ring-0 focus-visible:ring-offset-0'
/>
</div>
<Button disabled variant='primary' className='h-[32px] rounded-[6px]'>
<Button disabled variant='tertiary' className='h-[32px] rounded-[6px]'>
Add Documents
</Button>
</div>
@@ -973,7 +973,7 @@ export function KnowledgeBase({
</div>
<div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input
placeholder='Search documents...'
@@ -999,7 +999,7 @@ export function KnowledgeBase({
<Button
onClick={handleAddDocuments}
disabled={userPermissions.canEdit !== true}
variant='primary'
variant='tertiary'
className='h-[32px] rounded-[6px]'
>
Add Documents
@@ -1253,18 +1253,17 @@ export function KnowledgeBase({
)}
{totalPages > 1 && (
<div className='flex items-center justify-center border-t bg-background px-6 py-4'>
<div className='flex items-center justify-center border-t bg-background px-4 pt-[10px]'>
<div className='flex items-center gap-1'>
<Button
variant='ghost'
onClick={prevPage}
disabled={!hasPrevPage || isLoadingDocuments}
className='h-8 w-8 p-0'
>
<ChevronLeft className='h-4 w-4' />
<ChevronLeft className='h-3.5 w-3.5' />
</Button>
<div className='mx-4 flex items-center gap-6'>
<div className='mx-[12px] flex items-center gap-[16px]'>
{Array.from({ length: Math.min(totalPages, 5) }, (_, i) => {
let page: number
if (totalPages <= 5) {
@@ -1298,9 +1297,8 @@ export function KnowledgeBase({
variant='ghost'
onClick={nextPage}
disabled={!hasNextPage || isLoadingDocuments}
className='h-8 w-8 p-0'
>
<ChevronRight className='h-4 w-4' />
<ChevronRight className='h-3.5 w-3.5' />
</Button>
</div>
</div>
@@ -1315,7 +1313,7 @@ export function KnowledgeBase({
<ModalContent size='sm'>
<ModalHeader>Delete Knowledge Base</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete "{knowledgeBaseName}"? This will permanently delete
the knowledge base and all {pagination.total} document
{pagination.total === 1 ? '' : 's'} within it.{' '}
@@ -1330,12 +1328,7 @@ export function KnowledgeBase({
>
Cancel
</Button>
<Button
variant='primary'
onClick={handleDeleteKnowledgeBase}
disabled={isDeleting}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
<Button variant='destructive' onClick={handleDeleteKnowledgeBase} disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete Knowledge Base'}
</Button>
</ModalFooter>
@@ -1346,7 +1339,7 @@ export function KnowledgeBase({
<ModalContent size='sm'>
<ModalHeader>Delete Document</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete "
{documents.find((doc) => doc.id === documentToDelete)?.filename ?? 'this document'}"?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
@@ -1362,11 +1355,7 @@ export function KnowledgeBase({
>
Cancel
</Button>
<Button
variant='primary'
onClick={confirmDeleteDocument}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
<Button variant='destructive' onClick={confirmDeleteDocument}>
Delete Document
</Button>
</ModalFooter>
@@ -1377,7 +1366,7 @@ export function KnowledgeBase({
<ModalContent size='sm'>
<ModalHeader>Delete Documents</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete {selectedDocuments.size} document
{selectedDocuments.size === 1 ? '' : 's'}?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
@@ -1387,12 +1376,7 @@ export function KnowledgeBase({
<Button variant='active' onClick={() => setShowBulkDeleteModal(false)}>
Cancel
</Button>
<Button
variant='primary'
onClick={confirmBulkDelete}
disabled={isBulkOperating}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
<Button variant='destructive' onClick={confirmBulkDelete} disabled={isBulkOperating}>
{isBulkOperating
? 'Deleting...'
: `Delete ${selectedDocuments.size} Document${selectedDocuments.size === 1 ? '' : 's'}`}

View File

@@ -41,8 +41,8 @@ export function ActionBar({
transition={{ duration: 0.2 }}
className={cn('-translate-x-1/2 fixed bottom-6 left-1/2 z-50 transform', className)}
>
<div className='flex items-center gap-[8px] rounded-[10px] border border-[var(--border-strong)] bg-[var(--surface-1)] p-[8px]'>
<span className='px-[4px] text-[13px] text-[var(--text-muted)]'>
<div className='flex items-center gap-[8px] rounded-[10px] border border-[var(--border)] bg-[var(--surface-2)] px-[8px] py-[6px]'>
<span className='px-[4px] text-[13px] text-[var(--text-secondary)]'>
{selectedCount} selected
</span>
@@ -54,14 +54,12 @@ export function ActionBar({
variant='ghost'
onClick={onEnable}
disabled={isLoading}
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-9)] p-0 text-[#868686] hover:bg-[var(--brand-secondary)]'
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]'
>
<Circle className='h-[12px] w-[12px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
Enable {disabledCount > 1 ? `${disabledCount} items` : 'item'}
</Tooltip.Content>
<Tooltip.Content side='top'>Enable</Tooltip.Content>
</Tooltip.Root>
)}
@@ -72,14 +70,12 @@ export function ActionBar({
variant='ghost'
onClick={onDisable}
disabled={isLoading}
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-9)] p-0 text-[#868686] hover:bg-[var(--brand-secondary)]'
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]'
>
<CircleOff className='h-[12px] w-[12px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>
Disable {enabledCount > 1 ? `${enabledCount} items` : 'item'}
</Tooltip.Content>
<Tooltip.Content side='top'>Disable</Tooltip.Content>
</Tooltip.Root>
)}
@@ -90,12 +86,12 @@ export function ActionBar({
variant='ghost'
onClick={onDelete}
disabled={isLoading}
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-9)] p-0 text-[#868686] hover:bg-[var(--brand-secondary)]'
className='hover:!text-[var(--text-inverse)] h-[28px] w-[28px] rounded-[8px] bg-[var(--surface-5)] p-0 text-[var(--text-secondary)] hover:bg-[var(--brand-secondary)]'
>
<Trash2 className='h-[12px] w-[12px]' />
</Button>
</Tooltip.Trigger>
<Tooltip.Content side='top'>Delete items</Tooltip.Content>
<Tooltip.Content side='top'>Delete</Tooltip.Content>
</Tooltip.Root>
)}
</div>

View File

@@ -352,7 +352,7 @@ export function AddDocumentsModal({
Cancel
</Button>
<Button
variant='primary'
variant='tertiary'
type='button'
onClick={handleUpload}
disabled={files.length === 0 || isUploading}

View File

@@ -2,7 +2,6 @@
import { useCallback, useEffect, useMemo, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Loader2 } from 'lucide-react'
import {
Button,
Combobox,
@@ -429,7 +428,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
Cancel
</Button>
<Button
variant='primary'
variant='tertiary'
onClick={saveTagDefinition}
className='flex-1'
disabled={
@@ -438,14 +437,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
!hasAvailableSlots(createTagForm.fieldType)
}
>
{isSavingTag ? (
<>
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
Creating...
</>
) : (
'Create Tag'
)}
{isSavingTag ? 'Creating...' : 'Create Tag'}
</Button>
</div>
</div>
@@ -468,7 +460,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
<ModalHeader>Delete Tag</ModalHeader>
<ModalBody className='!pb-[16px]'>
<div className='space-y-[8px]'>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete the "{selectedTag?.displayName}" tag? This will
remove this tag from {selectedTagUsage?.documentCount || 0} document
{selectedTagUsage?.documentCount !== 1 ? 's' : ''}.{' '}
@@ -494,12 +486,7 @@ export function BaseTagsModal({ open, onOpenChange, knowledgeBaseId }: BaseTagsM
>
Cancel
</Button>
<Button
variant='primary'
onClick={confirmDeleteTag}
disabled={isDeletingTag}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
<Button variant='destructive' onClick={confirmDeleteTag} disabled={isDeletingTag}>
{isDeletingTag ? <>Deleting...</> : 'Delete Tag'}
</Button>
</ModalFooter>

View File

@@ -67,26 +67,26 @@ function formatAbsoluteDate(dateString: string): string {
*/
export function BaseCardSkeleton() {
return (
<div className='group flex h-full cursor-pointer flex-col gap-[12px] rounded-[4px] bg-[var(--surface-elevated)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-5)]'>
<div className='group flex h-full cursor-pointer flex-col gap-[12px] rounded-[4px] bg-[var(--surface-4)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-5)]'>
<div className='flex items-center justify-between gap-[8px]'>
<div className='h-[17px] w-[120px] animate-pulse rounded-[4px] bg-[var(--surface-9)]' />
<div className='h-[22px] w-[90px] animate-pulse rounded-[4px] bg-[var(--surface-5)]' />
<div className='h-[17px] w-[120px] animate-pulse rounded-[4px] bg-[var(--surface-5)]' />
<div className='h-[22px] w-[90px] animate-pulse rounded-[4px] bg-[var(--surface-4)]' />
</div>
<div className='flex flex-1 flex-col gap-[8px]'>
<div className='flex items-center justify-between'>
<div className='flex items-center gap-[6px]'>
<div className='h-[12px] w-[12px] animate-pulse rounded-[2px] bg-[var(--surface-9)]' />
<div className='h-[15px] w-[45px] animate-pulse rounded-[4px] bg-[var(--surface-9)]' />
<div className='h-[12px] w-[12px] animate-pulse rounded-[2px] bg-[var(--surface-5)]' />
<div className='h-[15px] w-[45px] animate-pulse rounded-[4px] bg-[var(--surface-5)]' />
</div>
<div className='h-[15px] w-[120px] animate-pulse rounded-[4px] bg-[var(--surface-5)]' />
<div className='h-[15px] w-[120px] animate-pulse rounded-[4px] bg-[var(--surface-4)]' />
</div>
<div className='h-0 w-full border-[var(--divider)] border-t' />
<div className='flex h-[36px] flex-col gap-[6px]'>
<div className='h-[15px] w-full animate-pulse rounded-[4px] bg-[var(--surface-5)]' />
<div className='h-[15px] w-[75%] animate-pulse rounded-[4px] bg-[var(--surface-5)]' />
<div className='h-[15px] w-full animate-pulse rounded-[4px] bg-[var(--surface-4)]' />
<div className='h-[15px] w-[75%] animate-pulse rounded-[4px] bg-[var(--surface-4)]' />
</div>
</div>
</div>
@@ -122,9 +122,9 @@ export function BaseCard({ id, title, docCount, description, updatedAt }: BaseCa
return (
<Link href={href} prefetch={true} className='h-full'>
<div className='group flex h-full cursor-pointer flex-col gap-[12px] rounded-[4px] bg-[var(--surface-elevated)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-5)]'>
<div className='group flex h-full cursor-pointer flex-col gap-[12px] rounded-[4px] bg-[var(--surface-4)] px-[8px] py-[6px] transition-colors hover:bg-[var(--surface-5)]'>
<div className='flex items-center justify-between gap-[8px]'>
<h3 className='min-w-0 flex-1 truncate text-[14px] text-[var(--text-primary)]'>
<h3 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
{title}
</h3>
{shortId && <Badge className='flex-shrink-0 rounded-[4px] text-[12px]'>{shortId}</Badge>}
@@ -139,7 +139,7 @@ export function BaseCard({ id, title, docCount, description, updatedAt }: BaseCa
{updatedAt && (
<Tooltip.Root>
<Tooltip.Trigger asChild>
<span className='text-[12px] text-[var(--text-muted)]'>
<span className='text-[12px] text-[var(--text-tertiary)]'>
last updated: {formatRelativeTime(updatedAt)}
</span>
</Tooltip.Trigger>

View File

@@ -1,8 +1,8 @@
export const filterButtonClass =
'w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[var(--white)] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
'w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[var(--white)] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-2)]'
export const dropdownContentClass =
'w-[220px] rounded-lg border-[#E5E5E5] bg-[var(--white)] p-0 shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
'w-[220px] rounded-lg border-[#E5E5E5] bg-[var(--white)] p-0 shadow-xs dark:border-[#414141] dark:bg-[var(--surface-2)]'
export const commandListClass = 'overflow-y-auto overflow-x-hidden'

View File

@@ -388,7 +388,7 @@ export function CreateBaseModal({
/>
</div>
<div className='space-y-[12px] rounded-[6px] bg-[var(--surface-6)] px-[12px] py-[14px]'>
<div className='space-y-[12px] rounded-[6px] bg-[var(--surface-5)] px-[12px] py-[14px]'>
<div className='grid grid-cols-2 gap-[12px]'>
<div className='flex flex-col gap-[8px]'>
<Label htmlFor='minChunkSize'>Min Chunk Size (characters)</Label>
@@ -562,7 +562,7 @@ export function CreateBaseModal({
Cancel
</Button>
<Button
variant='primary'
variant='tertiary'
type='submit'
disabled={isSubmitting || !nameValue?.trim()}
>

View File

@@ -145,7 +145,7 @@ export function Knowledge() {
</div>
<div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input
placeholder='Search'
@@ -184,7 +184,7 @@ export function Knowledge() {
<Button
onClick={() => setIsCreateModalOpen(true)}
disabled={userPermissions.canEdit !== true}
variant='primary'
variant='tertiary'
className='h-[32px] rounded-[6px]'
>
Create

View File

@@ -671,7 +671,7 @@ function LineChartComponent({
const top = Math.min(Math.max(anchorY - 26, padding.top), height - padding.bottom - 18)
return (
<div
className='pointer-events-none absolute rounded-[8px] border border-[var(--border-strong)] bg-[var(--surface-1)] px-[8px] py-[6px] font-medium text-[11px] shadow-lg'
className='pointer-events-none absolute rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-1)] px-[8px] py-[6px] font-medium text-[11px] shadow-lg'
style={{ left, top }}
>
{currentHoverDate && (

View File

@@ -97,7 +97,7 @@ export function StatusBar({
{hoverIndex !== null && segments[hoverIndex] && (
<div
className={`-translate-x-1/2 pointer-events-none absolute z-20 w-max whitespace-nowrap rounded-[8px] border border-[var(--border-strong)] bg-[var(--surface-1)] px-[8px] py-[6px] text-center text-[11px] shadow-lg ${
className={`-translate-x-1/2 pointer-events-none absolute z-20 w-max whitespace-nowrap rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-1)] px-[8px] py-[6px] text-center text-[11px] shadow-lg ${
preferBelow ? '' : '-translate-y-full'
}`}
style={{

View File

@@ -66,8 +66,8 @@ export function WorkflowsList({
<div
key={workflow.workflowId}
className={cn(
'flex h-[44px] cursor-pointer items-center gap-[16px] px-[24px] hover:bg-[var(--c-2A2A2A)]',
isSelected && 'bg-[var(--c-2A2A2A)]'
'flex h-[44px] cursor-pointer items-center gap-[16px] px-[24px] hover:bg-[var(--surface-6)] dark:hover:bg-[var(--surface-4)]',
isSelected && 'bg-[var(--surface-6)] dark:bg-[var(--surface-4)]'
)}
onClick={() => onToggleWorkflow(workflow.workflowId)}
>

View File

@@ -36,7 +36,7 @@ const SKELETON_BAR_HEIGHTS = [
function GraphCardSkeleton({ title }: { title: string }) {
return (
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'>
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-2)]'>
<div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'>
{title}
@@ -570,7 +570,7 @@ export default function Dashboard({ logs, isLoading, error }: DashboardProps) {
<div className='mt-[24px] flex min-h-0 flex-1 flex-col pb-[24px]'>
<div className='mb-[16px] flex-shrink-0'>
<div className='grid grid-cols-1 gap-[16px] md:grid-cols-3'>
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'>
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-2)]'>
<div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'>
Runs
@@ -597,7 +597,7 @@ export default function Dashboard({ logs, isLoading, error }: DashboardProps) {
</div>
</div>
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'>
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-2)]'>
<div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'>
Errors
@@ -624,7 +624,7 @@ export default function Dashboard({ logs, isLoading, error }: DashboardProps) {
</div>
</div>
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-elevated)]'>
<div className='flex flex-col overflow-hidden rounded-[6px] bg-[var(--surface-2)]'>
<div className='flex min-w-0 items-center justify-between gap-[8px] bg-[var(--surface-3)] px-[16px] py-[9px]'>
<span className='min-w-0 truncate font-medium text-[var(--text-primary)] text-sm'>
Latency

View File

@@ -4,9 +4,4 @@ export { FileCards } from './log-details/components/file-download'
export { FrozenCanvas } from './log-details/components/frozen-canvas'
export { TraceSpans } from './log-details/components/trace-spans'
export { LogsList } from './logs-list'
export {
AutocompleteSearch,
Controls,
LogsToolbar,
NotificationSettings,
} from './logs-toolbar'
export { AutocompleteSearch, LogsToolbar, NotificationSettings } from './logs-toolbar'

View File

@@ -277,10 +277,10 @@ function ExpandableRowHeader({
aria-expanded={hasChildren ? isExpanded : undefined}
aria-label={hasChildren ? (isExpanded ? 'Collapse' : 'Expand') : undefined}
>
<div className='flex items-center gap-[6px]'>
<div className='flex items-center gap-[8px]'>
{hasChildren && (
<ChevronDown
className='h-[10px] w-[10px] text-[var(--text-subtle)] transition-transform group-hover:text-[var(--text-tertiary)]'
className='h-[10px] w-[10px] flex-shrink-0 text-[var(--text-tertiary)] transition-transform duration-100 group-hover:text-[var(--text-primary)]'
style={{ transform: isExpanded ? 'rotate(0deg)' : 'rotate(-90deg)' }}
/>
)}
@@ -418,14 +418,14 @@ function InputOutputSection({
'font-medium text-[12px] transition-colors',
isError
? 'text-[var(--text-error)]'
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-secondary)]'
: 'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
)}
>
{label}
</span>
<ChevronDown
className={clsx(
'h-[10px] w-[10px] text-[var(--text-subtle)] transition-colors transition-transform group-hover:text-[var(--text-tertiary)]'
'h-[10px] w-[10px] text-[var(--text-tertiary)] transition-colors transition-transform group-hover:text-[var(--text-primary)]'
)}
style={{
transform: isExpanded ? 'rotate(180deg)' : 'rotate(0deg)',
@@ -502,7 +502,7 @@ function NestedBlockItem({
{/* Nested children */}
{hasChildren && isChildrenExpanded && (
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'>
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l pl-[10px]'>
{span.children!.map((child, childIndex) => (
<NestedBlockItem
key={child.id || `${spanId}-child-${childIndex}`}
@@ -643,7 +643,7 @@ const TraceSpanItem = memo(function TraceSpanItem({
{/* For workflow blocks, keep children nested within the card (not as separate cards) */}
{!isFirstSpan && isWorkflowBlock && inlineChildren.length > 0 && isCardExpanded && (
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'>
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l pl-[10px]'>
{inlineChildren.map((childSpan, index) => (
<NestedBlockItem
key={childSpan.id || `${spanId}-nested-${index}`}
@@ -663,7 +663,7 @@ const TraceSpanItem = memo(function TraceSpanItem({
{/* For non-workflow blocks, render inline children/tool calls */}
{!isFirstSpan && !isWorkflowBlock && isCardExpanded && (
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'>
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l pl-[10px]'>
{[...toolCallSpans, ...inlineChildren].map((childSpan, index) => {
const childId = childSpan.id || `${spanId}-inline-${index}`
const childIsError = childSpan.status === 'error'
@@ -731,7 +731,7 @@ const TraceSpanItem = memo(function TraceSpanItem({
{/* Nested children */}
{showChildrenInProgressBar && hasNestedChildren && isNestedExpanded && (
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l-2 pl-[10px]'>
<div className='mt-[2px] flex min-w-0 flex-col gap-[10px] overflow-hidden border-[var(--border)] border-l pl-[10px]'>
{childSpan.children!.map((nestedChild, nestedIndex) => (
<NestedBlockItem
key={nestedChild.id || `${childId}-nested-${nestedIndex}`}

View File

@@ -267,7 +267,7 @@ export const LogDetails = memo(function LogDetails({
</span>
<button
onClick={() => setIsFrozenCanvasOpen(true)}
className='flex items-center justify-between rounded-[6px] bg-[var(--surface-1)] px-[10px] py-[8px] transition-colors hover:bg-[var(--c-2A2A2A)]'
className='flex items-center justify-between rounded-[6px] bg-[var(--surface-1)] px-[10px] py-[8px] transition-colors hover:bg-[var(--surface-4)]'
>
<span className='font-medium text-[12px] text-[var(--text-secondary)]'>
View Snapshot

View File

@@ -38,8 +38,8 @@ const LogRow = memo(
<div
ref={isSelected ? selectedRowRef : null}
className={cn(
'relative flex h-[44px] cursor-pointer items-center px-[24px] hover:bg-[var(--c-2A2A2A)]',
isSelected && 'bg-[var(--c-2A2A2A)]'
'relative flex h-[44px] cursor-pointer items-center px-[24px] hover:bg-[var(--surface-4)]',
isSelected && 'bg-[var(--surface-4)]'
)}
onClick={handleClick}
>

View File

@@ -1,263 +0,0 @@
import { type ReactNode, useState } from 'react'
import { ArrowUp, Bell, ChevronDown, Loader2, RefreshCw, Search } from 'lucide-react'
import {
Button,
Popover,
PopoverContent,
PopoverItem,
PopoverScrollArea,
PopoverTrigger,
Tooltip,
} from '@/components/emcn'
import { MoreHorizontal } from '@/components/emcn/icons'
import { Input } from '@/components/ui/input'
import { cn } from '@/lib/core/utils/cn'
import { soehne } from '@/app/_styles/fonts/soehne/soehne'
import { useFilterStore } from '@/stores/logs/filters/store'
import type { TimeRange } from '@/stores/logs/filters/types'
const FILTER_BUTTON_CLASS =
'w-full justify-between rounded-[10px] border-[#E5E5E5] bg-[var(--white)] font-normal text-sm dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
type TimelineProps = {
variant?: 'default' | 'header'
}
/**
* Timeline component for time range selection.
* Displays a dropdown with predefined time ranges.
* @param props - The component props
* @returns Time range selector dropdown
*/
function Timeline({ variant = 'default' }: TimelineProps = {}) {
const { timeRange, setTimeRange } = useFilterStore()
const [isPopoverOpen, setIsPopoverOpen] = useState(false)
const specificTimeRanges: TimeRange[] = [
'Past 30 minutes',
'Past hour',
'Past 6 hours',
'Past 12 hours',
'Past 24 hours',
'Past 3 days',
'Past 7 days',
'Past 14 days',
'Past 30 days',
]
const handleTimeRangeSelect = (range: TimeRange) => {
setTimeRange(range)
setIsPopoverOpen(false)
}
return (
<Popover open={isPopoverOpen} onOpenChange={setIsPopoverOpen}>
<PopoverTrigger asChild>
<Button variant='outline' className={FILTER_BUTTON_CLASS}>
{timeRange}
<ChevronDown className='ml-2 h-4 w-4 text-muted-foreground' />
</Button>
</PopoverTrigger>
<PopoverContent
align={variant === 'header' ? 'end' : 'start'}
side='bottom'
sideOffset={4}
maxHeight={144}
>
<PopoverScrollArea>
<PopoverItem
active={timeRange === 'All time'}
showCheck
onClick={() => handleTimeRangeSelect('All time')}
>
All time
</PopoverItem>
<div className='my-[2px] h-px bg-[var(--surface-11)]' />
{specificTimeRanges.map((range) => (
<PopoverItem
key={range}
active={timeRange === range}
showCheck
onClick={() => handleTimeRangeSelect(range)}
>
{range}
</PopoverItem>
))}
</PopoverScrollArea>
</PopoverContent>
</Popover>
)
}
interface ControlsProps {
searchQuery?: string
setSearchQuery?: (v: string) => void
isRefetching: boolean
resetToNow: () => void
live: boolean
setLive: (v: (prev: boolean) => boolean) => void
viewMode: string
setViewMode: (mode: 'logs' | 'dashboard') => void
searchComponent?: ReactNode
showExport?: boolean
onExport?: () => void
canConfigureNotifications?: boolean
onConfigureNotifications?: () => void
}
export function Controls({
searchQuery,
setSearchQuery,
isRefetching,
resetToNow,
live,
setLive,
viewMode,
setViewMode,
searchComponent,
onExport,
canConfigureNotifications,
onConfigureNotifications,
}: ControlsProps) {
return (
<div
className={cn(
'mb-8 flex flex-col items-stretch justify-between gap-4 sm:flex-row sm:items-start',
soehne.className
)}
>
{searchComponent ? (
searchComponent
) : (
<div className='relative w-full max-w-md'>
<Search className='-translate-y-1/2 absolute top-1/2 left-3 h-[18px] w-[18px] text-muted-foreground' />
<Input
type='text'
placeholder='Search workflows...'
value={searchQuery}
onChange={(e) => setSearchQuery?.(e.target.value)}
className='h-9 w-full border-[#E5E5E5] bg-[var(--white)] pr-10 pl-9 dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
/>
{searchQuery && (
<button
onClick={() => setSearchQuery?.('')}
className='-translate-y-1/2 absolute top-1/2 right-3 text-muted-foreground hover:text-foreground'
>
<svg
width='14'
height='14'
viewBox='0 0 16 16'
fill='none'
stroke='currentColor'
strokeWidth='2'
strokeLinecap='round'
>
<path d='M12 4L4 12M4 4l8 8' />
</svg>
</button>
)}
</div>
)}
<div className='ml-auto flex flex-shrink-0 items-center gap-3'>
{viewMode !== 'dashboard' && (
<Popover>
<PopoverTrigger asChild>
<Button variant='ghost' className='h-9 w-9 p-0 hover:bg-secondary'>
<MoreHorizontal className='h-4 w-4' />
<span className='sr-only'>More options</span>
</Button>
</PopoverTrigger>
<PopoverContent align='end' sideOffset={4}>
<PopoverScrollArea>
<PopoverItem onClick={onExport}>
<ArrowUp className='h-3 w-3' />
<span>Export as CSV</span>
</PopoverItem>
<PopoverItem
onClick={canConfigureNotifications ? onConfigureNotifications : undefined}
disabled={!canConfigureNotifications}
>
<Bell className='h-3 w-3' />
<span>Configure Notifications</span>
</PopoverItem>
</PopoverScrollArea>
</PopoverContent>
</Popover>
)}
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button
variant='ghost'
onClick={resetToNow}
className='h-9 w-9 p-0 hover:bg-secondary'
disabled={isRefetching}
>
{isRefetching ? (
<Loader2 className='h-4 w-4 animate-spin' />
) : (
<RefreshCw className='h-4 w-4' />
)}
<span className='sr-only'>Refresh</span>
</Button>
</Tooltip.Trigger>
<Tooltip.Content>{isRefetching ? 'Refreshing...' : 'Refresh'}</Tooltip.Content>
</Tooltip.Root>
<div className='inline-flex h-9 items-center rounded-[11px] border bg-card p-1 shadow-sm'>
<Button
variant='ghost'
onClick={() => setLive((v) => !v)}
className={cn(
'h-7 rounded-[8px] px-3 font-normal text-xs',
live
? 'bg-[var(--brand-primary-hex)] text-white shadow-[0_0_0_0_var(--brand-primary-hex)] hover:bg-[var(--brand-primary-hover-hex)] hover:text-white hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]'
: 'text-muted-foreground hover:text-foreground'
)}
aria-pressed={live}
>
Live
</Button>
</div>
<div className='inline-flex h-9 items-center rounded-[11px] border bg-card p-1 shadow-sm'>
<Button
variant='ghost'
onClick={() => setViewMode('logs')}
className={cn(
'h-7 rounded-[8px] px-3 font-normal text-xs',
(viewMode as string) !== 'dashboard'
? 'bg-muted text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
aria-pressed={(viewMode as string) !== 'dashboard'}
>
Logs
</Button>
<Button
variant='ghost'
onClick={() => setViewMode('dashboard')}
className={cn(
'h-7 rounded-[8px] px-3 font-normal text-xs',
(viewMode as string) === 'dashboard'
? 'bg-muted text-foreground'
: 'text-muted-foreground hover:text-foreground'
)}
aria-pressed={(viewMode as string) === 'dashboard'}
>
Dashboard
</Button>
</div>
</div>
<div className='sm:hidden'>
<Timeline />
</div>
</div>
)
}
export default Controls

View File

@@ -1 +0,0 @@
export { Controls, default } from './controls'

View File

@@ -39,9 +39,6 @@ import { WorkflowSelector } from './components/workflow-selector'
const logger = createLogger('NotificationSettings')
const PRIMARY_BUTTON_STYLES =
'!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
type NotificationType = 'webhook' | 'email' | 'slack'
type LogLevel = 'info' | 'error'
type AlertRule =
@@ -618,10 +615,9 @@ export function NotificationSettings({
<div className='flex flex-shrink-0 items-center gap-[8px]'>
<Button
variant='primary'
variant='tertiary'
onClick={() => handleTest(subscription.id)}
disabled={testNotification.isPending && testStatus?.id !== subscription.id}
className={PRIMARY_BUTTON_STYLES}
>
{testStatus?.id === subscription.id
? testStatus.success
@@ -703,7 +699,7 @@ export function NotificationSettings({
{activeTab === 'email' && (
<div className='flex flex-col gap-[8px]'>
<Label className='text-[var(--text-secondary)]'>Email Recipients</Label>
<div className='scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-9)]'>
<div className='scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-5)]'>
{invalidEmails.map((email, index) => (
<EmailTag
key={`invalid-${index}`}
@@ -1302,10 +1298,9 @@ export function NotificationSettings({
</Button>
)}
<Button
variant='primary'
variant='tertiary'
onClick={handleSave}
disabled={createNotification.isPending || updateNotification.isPending}
className={PRIMARY_BUTTON_STYLES}
>
{createNotification.isPending || updateNotification.isPending
? editingId
@@ -1322,9 +1317,8 @@ export function NotificationSettings({
resetForm()
setShowForm(true)
}}
variant='primary'
variant='tertiary'
disabled={isLoading}
className={PRIMARY_BUTTON_STYLES}
>
<Plus className='mr-[6px] h-[13px] w-[13px]' />
Add
@@ -1338,7 +1332,7 @@ export function NotificationSettings({
<ModalContent className='w-[400px]'>
<ModalHeader>Delete Notification</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
This will permanently remove the notification and stop all deliveries.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
@@ -1352,10 +1346,9 @@ export function NotificationSettings({
Cancel
</Button>
<Button
variant='primary'
variant='destructive'
onClick={handleDelete}
disabled={deleteNotification.isPending}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
{deleteNotification.isPending ? 'Deleting...' : 'Delete'}
</Button>
@@ -1379,7 +1372,7 @@ function EmailTag({ email, onRemove, isInvalid }: EmailTagProps) {
'flex w-auto items-center gap-[4px] rounded-[4px] border px-[6px] py-[2px] text-[12px]',
isInvalid
? 'border-[var(--text-error)] bg-[color-mix(in_srgb,var(--text-error)_10%,transparent)] text-[var(--text-error)] dark:bg-[color-mix(in_srgb,var(--text-error)_16%,transparent)]'
: 'border-[var(--surface-11)] bg-[var(--surface-5)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
: 'border-[var(--border-1)] bg-[var(--surface-4)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
)}
>
<span className='max-w-[200px] truncate'>{email}</span>

View File

@@ -163,7 +163,7 @@ export function AutocompleteSearch({
}}
>
<PopoverAnchor asChild>
<div className='relative flex h-[32px] w-full items-center rounded-[8px] bg-[var(--surface-5)]'>
<div className='relative flex h-[32px] w-[400px] items-center rounded-[8px] bg-[var(--surface-4)]'>
{/* Search Icon */}
<Search className='mr-[6px] ml-[8px] h-[14px] w-[14px] flex-shrink-0 text-[var(--text-subtle)]' />
@@ -266,8 +266,8 @@ export function AutocompleteSearch({
data-index={0}
className={cn(
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
'hover:bg-[var(--surface-9)]',
highlightedIndex === 0 && 'bg-[var(--surface-9)]'
'hover:bg-[var(--surface-5)]',
highlightedIndex === 0 && 'bg-[var(--surface-5)]'
)}
onMouseEnter={() => setHighlightedIndex(0)}
onMouseDown={(e) => {
@@ -296,8 +296,8 @@ export function AutocompleteSearch({
data-index={index}
className={cn(
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
'hover:bg-[var(--surface-9)]',
isHighlighted && 'bg-[var(--surface-9)]'
'hover:bg-[var(--surface-5)]',
isHighlighted && 'bg-[var(--surface-5)]'
)}
onMouseEnter={() => setHighlightedIndex(index)}
onMouseDown={(e) => {
@@ -339,8 +339,8 @@ export function AutocompleteSearch({
data-index={index}
className={cn(
'w-full rounded-[6px] px-3 py-2 text-left transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-[var(--border-focus)]',
'hover:bg-[var(--surface-9)]',
index === highlightedIndex && 'bg-[var(--surface-9)]'
'hover:bg-[var(--surface-5)]',
index === highlightedIndex && 'bg-[var(--surface-5)]'
)}
onMouseEnter={() => setHighlightedIndex(index)}
onMouseDown={(e) => {

View File

@@ -1,4 +1,3 @@
export { Controls } from './components/controls'
export { NotificationSettings } from './components/notifications'
export { AutocompleteSearch } from './components/search'
export { LogsToolbar } from './logs-toolbar'

View File

@@ -302,7 +302,7 @@ export function LogsToolbar({
</div>
<div className='flex items-center gap-[8px]'>
{/* More options popover */}
<Popover>
<Popover size='sm'>
<PopoverTrigger asChild>
<Button variant='default' className='h-[32px] w-[32px] rounded-[6px] p-0'>
<MoreHorizontal className='h-[14px] w-[14px]' />
@@ -339,27 +339,36 @@ export function LogsToolbar({
{/* Live button */}
<Button
variant={isLive ? 'primary' : 'default'}
variant={isLive ? 'tertiary' : 'default'}
onClick={onToggleLive}
className='h-[32px] rounded-[6px] px-[10px]'
className={cn(
'h-[32px] rounded-[6px] px-[10px]',
isLive && 'border border-[var(--brand-tertiary-2)]'
)}
>
Live
</Button>
{/* View mode toggle */}
<div
className='flex h-[32px] cursor-pointer items-center rounded-[6px] border border-[var(--border)] bg-[var(--surface-elevated)] p-[2px]'
className='flex h-[32px] cursor-pointer items-center rounded-[6px] border border-[var(--border)] bg-[var(--surface-2)] p-[2px]'
onClick={() => onViewModeChange(isDashboardView ? 'logs' : 'dashboard')}
>
<Button
variant={!isDashboardView ? 'active' : 'ghost'}
className='h-[26px] rounded-[4px] px-[10px]'
className={cn(
'h-[26px] rounded-[4px] px-[10px]',
isDashboardView && 'border border-transparent'
)}
>
Logs
</Button>
<Button
variant={isDashboardView ? 'active' : 'ghost'}
className='h-[26px] rounded-[4px] px-[10px]'
className={cn(
'h-[26px] rounded-[4px] px-[10px]',
!isDashboardView && 'border border-transparent'
)}
>
Dashboard
</Button>

View File

@@ -5,10 +5,15 @@ import { getIntegrationMetadata } from '@/lib/logs/get-trigger-options'
import { getBlock } from '@/blocks/registry'
const CORE_TRIGGER_TYPES = ['manual', 'api', 'schedule', 'chat', 'webhook'] as const
const RUNNING_COLOR = '#22c55e' as const
const PENDING_COLOR = '#f59e0b' as const
/** Possible execution status values for workflow logs */
export type LogStatus = 'error' | 'pending' | 'running' | 'info' | 'cancelled'
/**
* Maps raw status string to LogStatus for display.
* @param status - Raw status from API
* @returns Normalized LogStatus value
*/
export function getDisplayStatus(status: string | null | undefined): LogStatus {
switch (status) {
case 'running':
@@ -24,108 +29,54 @@ export function getDisplayStatus(status: string | null | undefined): LogStatus {
}
}
/**
* Checks if a hex color is gray/neutral (low saturation) or too light/dark
*/
export function isGrayOrNeutral(hex: string): boolean {
const r = Number.parseInt(hex.slice(1, 3), 16)
const g = Number.parseInt(hex.slice(3, 5), 16)
const b = Number.parseInt(hex.slice(5, 7), 16)
const max = Math.max(r, g, b)
const min = Math.min(r, g, b)
const lightness = (max + min) / 2 / 255
const delta = max - min
const saturation = delta === 0 ? 0 : delta / (1 - Math.abs(2 * lightness - 1)) / 255
return saturation < 0.2 || lightness > 0.8 || lightness < 0.25
/** Configuration mapping log status to Badge variant and display label */
const STATUS_VARIANT_MAP: Record<
LogStatus,
{ variant: React.ComponentProps<typeof Badge>['variant']; label: string }
> = {
error: { variant: 'red', label: 'Error' },
pending: { variant: 'amber', label: 'Pending' },
running: { variant: 'green', label: 'Running' },
cancelled: { variant: 'gray', label: 'Cancelled' },
info: { variant: 'gray', label: 'Info' },
}
/**
* Converts a hex color to a background variant with appropriate opacity
*/
export function hexToBackground(hex: string): string {
const r = Number.parseInt(hex.slice(1, 3), 16)
const g = Number.parseInt(hex.slice(3, 5), 16)
const b = Number.parseInt(hex.slice(5, 7), 16)
return `rgba(${r}, ${g}, ${b}, 0.2)`
}
/**
* Lightens a hex color to make it more vibrant for text
*/
export function lightenColor(hex: string, percent = 30): string {
const r = Number.parseInt(hex.slice(1, 3), 16)
const g = Number.parseInt(hex.slice(3, 5), 16)
const b = Number.parseInt(hex.slice(5, 7), 16)
const newR = Math.min(255, Math.round(r + (255 - r) * (percent / 100)))
const newG = Math.min(255, Math.round(g + (255 - g) * (percent / 100)))
const newB = Math.min(255, Math.round(b + (255 - b) * (percent / 100)))
return `#${newR.toString(16).padStart(2, '0')}${newG.toString(16).padStart(2, '0')}${newB.toString(16).padStart(2, '0')}`
/** Configuration mapping core trigger types to Badge color variants */
const TRIGGER_VARIANT_MAP: Record<string, React.ComponentProps<typeof Badge>['variant']> = {
manual: 'gray-secondary',
api: 'blue',
schedule: 'teal',
chat: 'purple',
webhook: 'orange',
}
interface StatusBadgeProps {
/** The execution status to display */
status: LogStatus
}
/**
* Displays a styled badge for a log execution status
* Renders a colored badge indicating log execution status.
* @param props - Component props containing the status
* @returns A Badge with dot indicator and status label
*/
export const StatusBadge = React.memo(({ status }: StatusBadgeProps) => {
const config = {
error: {
bg: 'var(--terminal-status-error-bg)',
color: 'var(--text-error)',
label: 'Error',
},
pending: {
bg: hexToBackground(PENDING_COLOR),
color: lightenColor(PENDING_COLOR, 65),
label: 'Pending',
},
running: {
bg: hexToBackground(RUNNING_COLOR),
color: lightenColor(RUNNING_COLOR, 65),
label: 'Running',
},
cancelled: {
bg: 'var(--terminal-status-info-bg)',
color: 'var(--terminal-status-info-color)',
label: 'Cancelled',
},
info: {
bg: 'var(--terminal-status-info-bg)',
color: 'var(--terminal-status-info-color)',
label: 'Info',
},
}[status]
return React.createElement(
'div',
{
className:
'inline-flex items-center gap-[6px] rounded-[6px] px-[9px] py-[2px] font-medium text-[12px]',
style: { backgroundColor: config.bg, color: config.color },
},
React.createElement('div', {
className: 'h-[6px] w-[6px] rounded-[2px]',
style: { backgroundColor: config.color },
}),
config.label
)
const config = STATUS_VARIANT_MAP[status]
return React.createElement(Badge, { variant: config.variant, dot: true }, config.label)
})
StatusBadge.displayName = 'StatusBadge'
interface TriggerBadgeProps {
/** The trigger type identifier (e.g., 'manual', 'api', or integration block type) */
trigger: string
}
/**
* Displays a styled badge for a workflow trigger type
* Renders a colored badge indicating the workflow trigger type.
* Core triggers display with their designated colors; integrations show with icons.
* @param props - Component props containing the trigger type
* @returns A Badge with appropriate styling for the trigger type
*/
export const TriggerBadge = React.memo(({ trigger }: TriggerBadgeProps) => {
const metadata = getIntegrationMetadata(trigger)
@@ -133,37 +84,20 @@ export const TriggerBadge = React.memo(({ trigger }: TriggerBadgeProps) => {
const block = isIntegration ? getBlock(trigger) : null
const IconComponent = block?.icon
const isUnknownIntegration = isIntegration && trigger !== 'generic' && !block
if (
trigger === 'manual' ||
trigger === 'generic' ||
isUnknownIntegration ||
isGrayOrNeutral(metadata.color)
) {
const coreVariant = TRIGGER_VARIANT_MAP[trigger]
if (coreVariant) {
return React.createElement(Badge, { variant: coreVariant }, metadata.label)
}
if (IconComponent) {
return React.createElement(
Badge,
{
variant: 'default',
className:
'inline-flex items-center gap-[6px] rounded-[6px] px-[9px] py-[2px] font-medium text-[12px]',
},
IconComponent && React.createElement(IconComponent, { className: 'h-[12px] w-[12px]' }),
{ variant: 'gray-secondary', icon: IconComponent },
metadata.label
)
}
const textColor = lightenColor(metadata.color, 65)
return React.createElement(
'div',
{
className:
'inline-flex items-center gap-[6px] rounded-[6px] px-[9px] py-[2px] font-medium text-[12px]',
style: { backgroundColor: hexToBackground(metadata.color), color: textColor },
},
IconComponent && React.createElement(IconComponent, { className: 'h-[12px] w-[12px]' }),
metadata.label
)
return React.createElement(Badge, { variant: 'gray-secondary' }, metadata.label)
})
TriggerBadge.displayName = 'TriggerBadge'

View File

@@ -29,19 +29,19 @@ export function TemplateCardSkeleton({ className }: { className?: string }) {
return (
<div
className={cn(
'h-[268px] w-full rounded-[8px] bg-[var(--surface-elevated)] p-[8px] transition-colors hover:bg-[var(--surface-5)]',
'h-[268px] w-full rounded-[8px] bg-[var(--surface-4)] p-[8px] transition-colors hover:bg-[var(--surface-5)]',
className
)}
>
<div className='h-[180px] w-full animate-pulse rounded-[6px] bg-gray-700' />
<div className='h-[180px] w-full animate-pulse rounded-[6px] bg-[var(--surface-5)]' />
<div className='mt-[14px] flex items-center justify-between'>
<div className='h-4 w-32 animate-pulse rounded bg-gray-700' />
<div className='h-4 w-32 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='flex items-center gap-[-4px]'>
{Array.from({ length: 3 }).map((_, index) => (
<div
key={index}
className='h-[18px] w-[18px] animate-pulse rounded-[4px] bg-gray-700'
className='h-[18px] w-[18px] animate-pulse rounded-[4px] bg-[var(--surface-5)]'
/>
))}
</div>
@@ -49,14 +49,14 @@ export function TemplateCardSkeleton({ className }: { className?: string }) {
<div className='mt-[14px] flex items-center justify-between'>
<div className='flex items-center gap-[6px]'>
<div className='h-[20px] w-[20px] animate-pulse rounded-full bg-gray-700' />
<div className='h-3 w-20 animate-pulse rounded bg-gray-700' />
<div className='h-[20px] w-[20px] animate-pulse rounded-full bg-[var(--surface-5)]' />
<div className='h-3 w-20 animate-pulse rounded bg-[var(--surface-5)]' />
</div>
<div className='flex items-center gap-[6px]'>
<div className='h-3 w-3 animate-pulse rounded bg-gray-700' />
<div className='h-3 w-6 animate-pulse rounded bg-gray-700' />
<div className='h-3 w-3 animate-pulse rounded bg-gray-700' />
<div className='h-3 w-6 animate-pulse rounded bg-gray-700' />
<div className='h-3 w-3 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-6 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-3 animate-pulse rounded bg-[var(--surface-5)]' />
<div className='h-3 w-6 animate-pulse rounded bg-[var(--surface-5)]' />
</div>
</div>
</div>
@@ -202,7 +202,7 @@ function TemplateCardInner({
<div
onClick={handleCardClick}
className={cn(
'w-full cursor-pointer rounded-[8px] bg-[var(--surface-elevated)] p-[8px] transition-colors hover:bg-[var(--surface-5)]',
'w-full cursor-pointer rounded-[8px] bg-[var(--surface-4)] p-[8px] transition-colors hover:bg-[var(--surface-5)]',
className
)}
>
@@ -223,12 +223,14 @@ function TemplateCardInner({
cursorStyle='pointer'
/>
) : (
<div className='h-full w-full bg-[#2A2A2A]' />
<div className='h-full w-full bg-[var(--surface-4)]' />
)}
</div>
<div className='mt-[10px] flex items-center justify-between'>
<h3 className='truncate pr-[8px] pl-[2px] font-medium text-[16px] text-white'>{title}</h3>
<h3 className='truncate pr-[8px] pl-[2px] font-medium text-[16px] text-[var(--text-primary)]'>
{title}
</h3>
<div className='flex flex-shrink-0'>
{blockTypes.length > 4 ? (
@@ -251,10 +253,12 @@ function TemplateCardInner({
)
})}
<div
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[var(--surface-14)]'
className='flex h-[18px] w-[18px] flex-shrink-0 items-center justify-center rounded-[4px] bg-[var(--surface-7)]'
style={{ marginLeft: '-4px' }}
>
<span className='font-medium text-[10px] text-white'>+{blockTypes.length - 3}</span>
<span className='font-medium text-[10px] text-[var(--text-primary)]'>
+{blockTypes.length - 3}
</span>
</div>
</>
) : (
@@ -286,24 +290,26 @@ function TemplateCardInner({
<img src={authorImageUrl} alt={author} className='h-full w-full object-cover' />
</div>
) : (
<div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-14)]'>
<User className='h-[12px] w-[12px] text-[#888888]' />
<div className='flex h-[20px] w-[20px] flex-shrink-0 items-center justify-center rounded-full bg-[var(--surface-7)]'>
<User className='h-[12px] w-[12px] text-[var(--text-tertiary)]' />
</div>
)}
<div className='flex min-w-0 items-center gap-[4px]'>
<span className='truncate font-medium text-[#888888] text-[12px]'>{author}</span>
<span className='truncate font-medium text-[12px] text-[var(--text-tertiary)]'>
{author}
</span>
{isVerified && <VerifiedBadge size='sm' />}
</div>
</div>
<div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[#888888] text-[12px]'>
<div className='flex flex-shrink-0 items-center gap-[6px] font-medium text-[12px] text-[var(--text-tertiary)]'>
<User className='h-[12px] w-[12px]' />
<span>{usageCount}</span>
<Star
onClick={handleStarClick}
className={cn(
'h-[12px] w-[12px] cursor-pointer transition-colors',
isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[#888888]',
isStarred ? 'fill-yellow-500 text-yellow-500' : 'text-[var(--text-tertiary)]',
isStarLoading && 'opacity-50'
)}
/>

View File

@@ -186,7 +186,7 @@ export default function Templates({
</div>
<div className='mt-[14px] flex items-center justify-between'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-5)] px-[8px]'>
<div className='flex h-[32px] w-[400px] items-center gap-[6px] rounded-[8px] bg-[var(--surface-4)] px-[8px]'>
<Search className='h-[14px] w-[14px] text-[var(--text-subtle)]' />
<Input
placeholder='Search'

View File

@@ -37,7 +37,10 @@ import {
OutputSelect,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/components'
import { useChatFileUpload } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/chat/hooks'
import { useScrollManagement } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import {
usePreventZoom,
useScrollManagement,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import {
useFloatBoundarySync,
useFloatDrag,
@@ -233,6 +236,7 @@ export function Chat() {
const inputRef = useRef<HTMLInputElement>(null)
const timeoutRef = useRef<NodeJS.Timeout | null>(null)
const streamReaderRef = useRef<ReadableStreamDefaultReader<Uint8Array> | null>(null)
const preventZoomRef = usePreventZoom()
// File upload hook
const {
@@ -814,7 +818,8 @@ export function Chat() {
return (
<div
className='fixed z-30 flex flex-col overflow-hidden rounded-[6px] border border-[var(--border)] bg-[var(--surface-1)] px-[10px] pt-[2px] pb-[8px]'
ref={preventZoomRef}
className='fixed z-30 flex flex-col overflow-hidden rounded-[8px] border border-[var(--border)] bg-[var(--surface-1)] px-[10px] pt-[2px] pb-[8px]'
style={{
left: `${actualPosition.x}px`,
top: `${actualPosition.y}px`,
@@ -968,8 +973,8 @@ export function Chat() {
{/* Combined input container */}
<div
className={`rounded-[4px] border bg-[var(--surface-9)] py-0 pr-[6px] pl-[4px] transition-colors ${
isDragOver ? 'border-[var(--brand-secondary)]' : 'border-[var(--surface-11)]'
className={`rounded-[4px] border bg-[var(--surface-5)] py-0 pr-[6px] pl-[4px] transition-colors ${
isDragOver ? 'border-[var(--brand-secondary)]' : 'border-[var(--border-1)]'
}`}
>
{/* File thumbnails */}

View File

@@ -156,7 +156,7 @@ export function ChatMessage({ message }: ChatMessageProps) {
)}
{formattedContent && !formattedContent.startsWith('Uploaded') && (
<div className='rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-9)] px-[8px] py-[6px] transition-all duration-200'>
<div className='rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] transition-all duration-200'>
<div className='whitespace-pre-wrap break-words font-medium font-sans text-gray-100 text-sm leading-[1.25rem]'>
<WordWrap text={formattedContent} />
</div>

View File

@@ -8,6 +8,7 @@ import { useParams, useRouter } from 'next/navigation'
import { Button } from '@/components/emcn'
import { AgentIcon } from '@/components/icons'
import { cn } from '@/lib/core/utils/cn'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { useSearchModalStore } from '@/stores/search-modal/store'
const logger = createLogger('WorkflowCommandList')
@@ -58,6 +59,7 @@ export function CommandList() {
const params = useParams()
const router = useRouter()
const { open: openSearchModal } = useSearchModalStore()
const preventZoomRef = usePreventZoom()
const workspaceId = params.workspaceId as string | undefined
@@ -171,6 +173,7 @@ export function CommandList() {
return (
<div
ref={preventZoomRef}
className={cn(
'pointer-events-none absolute inset-0 mb-[50px] flex items-center justify-center'
)}

View File

@@ -3,6 +3,7 @@
import { memo, useMemo } from 'react'
import { useViewport } from 'reactflow'
import { useSession } from '@/lib/auth/auth-client'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { getUserColor } from '@/app/workspace/[workspaceId]/w/utils/get-user-color'
import { useSocket } from '@/app/workspace/providers/socket-provider'
@@ -23,6 +24,7 @@ const CursorsComponent = () => {
const viewport = useViewport()
const session = useSession()
const currentUserId = session.data?.user?.id
const preventZoomRef = usePreventZoom()
const cursors = useMemo<CursorRenderData[]>(() => {
return presenceUsers
@@ -41,7 +43,7 @@ const CursorsComponent = () => {
}
return (
<div className='pointer-events-none absolute inset-0 z-30 select-none'>
<div ref={preventZoomRef} className='pointer-events-none absolute inset-0 z-30 select-none'>
{cursors.map(({ id, name, cursor, color }) => {
const x = cursor.x * viewport.zoom + viewport.x
const y = cursor.y * viewport.zoom + viewport.y

View File

@@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
import clsx from 'clsx'
import { Eye, EyeOff } from 'lucide-react'
import { Button } from '@/components/emcn'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { useCopilotStore } from '@/stores/panel/copilot/store'
import { useTerminalStore } from '@/stores/terminal'
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
@@ -295,6 +296,8 @@ export const DiffControls = memo(function DiffControls() {
})
}, [updatePreviewToolCallState, rejectChanges])
const preventZoomRef = usePreventZoom()
// Don't show anything if no diff is available or diff is not ready
if (!hasActiveDiff || !isDiffReady) {
return null
@@ -302,6 +305,7 @@ export const DiffControls = memo(function DiffControls() {
return (
<div
ref={preventZoomRef}
className={clsx(
'-translate-x-1/2 fixed left-1/2 z-30',
!isTerminalResizing && 'transition-[bottom] duration-100 ease-out'
@@ -335,9 +339,9 @@ export const DiffControls = memo(function DiffControls() {
{/* Accept */}
<Button
variant='ghost'
variant='tertiary'
onClick={handleAccept}
className='!text-[var(--bg)] h-[30px] rounded-[8px] bg-[var(--brand-tertiary)] px-3'
className='h-[30px] rounded-[8px] px-3'
title='Accept changes'
>
Accept

View File

@@ -4,6 +4,7 @@ import { Component, type ReactNode, useEffect } from 'react'
import { createLogger } from '@sim/logger'
import { ReactFlowProvider } from 'reactflow'
import { Panel } from '@/app/workspace/[workspaceId]/w/[workflowId]/components'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import { Sidebar } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
const logger = createLogger('ErrorBoundary')
@@ -24,12 +25,13 @@ export function ErrorUI({
onReset,
fullScreen = false,
}: ErrorUIProps) {
const preventZoomRef = usePreventZoom()
const containerClass = fullScreen
? 'flex flex-col w-full h-screen bg-[var(--surface-1)]'
: 'flex flex-col w-full h-full bg-[var(--surface-1)]'
return (
<div className={containerClass}>
<div ref={preventZoomRef} className={containerClass}>
{/* Sidebar */}
<Sidebar />

View File

@@ -4,7 +4,6 @@ export { DiffControls } from './diff-controls/diff-controls'
export { ErrorBoundary } from './error/index'
export { Notifications } from './notifications/notifications'
export { Panel } from './panel/panel'
export { SkeletonLoading } from './skeleton-loading/skeleton-loading'
export { SubflowNodeComponent } from './subflows/subflow-node'
export { Terminal } from './terminal/terminal'
export { WandPromptBar } from './wand-prompt-bar/wand-prompt-bar'

View File

@@ -27,6 +27,21 @@ function extractFieldValue(rawValue: unknown): string | undefined {
return undefined
}
/**
* Extract YouTube video ID from various YouTube URL formats
*/
function getYouTubeVideoId(url: string): string | null {
const patterns = [
/(?:youtube\.com\/watch\?v=|youtu\.be\/|youtube\.com\/embed\/)([a-zA-Z0-9_-]{11})/,
/youtube\.com\/watch\?.*v=([a-zA-Z0-9_-]{11})/,
]
for (const pattern of patterns) {
const match = url.match(pattern)
if (match) return match[1]
}
return null
}
/**
* Compact markdown renderer for note blocks with tight spacing
*/
@@ -36,39 +51,41 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
remarkPlugins={[remarkGfm]}
components={{
p: ({ children }: any) => (
<p className='mb-2 break-words text-[#E5E5E5] text-sm'>{children}</p>
<p className='mb-1 break-words text-[#E5E5E5] text-sm leading-[1.25rem] last:mb-0'>
{children}
</p>
),
h1: ({ children }: any) => (
<h1 className='mt-3 mb-1 break-words font-semibold text-[#E5E5E5] text-lg first:mt-0'>
<h1 className='mt-3 mb-3 break-words font-semibold text-[#E5E5E5] text-lg first:mt-0'>
{children}
</h1>
),
h2: ({ children }: any) => (
<h2 className='mt-3 mb-1 break-words font-semibold text-[#E5E5E5] text-base first:mt-0'>
<h2 className='mt-2.5 mb-2.5 break-words font-semibold text-[#E5E5E5] text-base first:mt-0'>
{children}
</h2>
),
h3: ({ children }: any) => (
<h3 className='mt-3 mb-1 break-words font-semibold text-[#E5E5E5] text-sm first:mt-0'>
<h3 className='mt-2 mb-2 break-words font-semibold text-[#E5E5E5] text-sm first:mt-0'>
{children}
</h3>
),
h4: ({ children }: any) => (
<h4 className='mt-3 mb-1 break-words font-semibold text-[#E5E5E5] text-xs first:mt-0'>
<h4 className='mt-2 mb-2 break-words font-semibold text-[#E5E5E5] text-xs first:mt-0'>
{children}
</h4>
),
ul: ({ children }: any) => (
<ul className='mt-1 mb-2 list-disc break-words pl-4 text-[#E5E5E5] text-sm'>
<ul className='mt-1 mb-1 list-disc space-y-1 break-words pl-6 text-[#E5E5E5] text-sm'>
{children}
</ul>
),
ol: ({ children }: any) => (
<ol className='mt-1 mb-2 list-decimal break-words pl-4 text-[#E5E5E5] text-sm'>
<ol className='mt-1 mb-1 list-decimal space-y-1 break-words pl-6 text-[#E5E5E5] text-sm'>
{children}
</ol>
),
li: ({ children }: any) => <li className='mb-0 break-words'>{children}</li>,
li: ({ children }: any) => <li className='break-words'>{children}</li>,
code: ({ inline, className, children, ...props }: any) => {
const isInline = inline || !className?.includes('language-')
@@ -76,7 +93,7 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
return (
<code
{...props}
className='whitespace-normal rounded bg-gray-200 px-1 py-0.5 font-mono text-[#F59E0B] text-xs dark:bg-[var(--surface-11)]'
className='whitespace-normal rounded bg-gray-200 px-1 py-0.5 font-mono text-[#F59E0B] text-xs dark:bg-[var(--border-1)]'
>
{children}
</code>
@@ -92,22 +109,51 @@ const NoteMarkdown = memo(function NoteMarkdown({ content }: { content: string }
</code>
)
},
a: ({ href, children }: any) => (
<a
href={href}
target='_blank'
rel='noopener noreferrer'
className='text-[var(--brand-secondary)] underline-offset-2 hover:underline'
>
{children}
</a>
),
a: ({ href, children }: any) => {
const videoId = href ? getYouTubeVideoId(href) : null
if (videoId) {
return (
<span className='inline'>
<a
href={href}
target='_blank'
rel='noopener noreferrer'
className='text-[var(--brand-secondary)] underline-offset-2 hover:underline'
>
{children}
</a>
<span className='mt-1.5 block overflow-hidden rounded-md'>
<iframe
src={`https://www.youtube.com/embed/${videoId}`}
title='YouTube video'
allow='accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture; web-share'
allowFullScreen
loading='lazy'
referrerPolicy='strict-origin-when-cross-origin'
sandbox='allow-scripts allow-same-origin allow-presentation allow-popups'
className='aspect-video w-full'
/>
</span>
</span>
)
}
return (
<a
href={href}
target='_blank'
rel='noopener noreferrer'
className='text-[var(--brand-secondary)] underline-offset-2 hover:underline'
>
{children}
</a>
)
},
strong: ({ children }: any) => (
<strong className='break-words font-semibold text-white'>{children}</strong>
),
em: ({ children }: any) => <em className='break-words text-[#B8B8B8]'>{children}</em>,
blockquote: ({ children }: any) => (
<blockquote className='mt-1 mb-2 break-words border-[#F59E0B] border-l-2 pl-3 text-[#B8B8B8] italic'>
<blockquote className='my-4 break-words border-[var(--border-1)] border-l-4 py-1 pl-4 text-[#B8B8B8] italic'>
{children}
</blockquote>
),
@@ -135,28 +181,16 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlo
)
)
const noteValues = useMemo(() => {
const content = useMemo(() => {
if (data.isPreview && data.subBlockValues) {
const extractedPreviewFormat = extractFieldValue(data.subBlockValues.format)
const extractedPreviewContent = extractFieldValue(data.subBlockValues.content)
return {
format: typeof extractedPreviewFormat === 'string' ? extractedPreviewFormat : 'plain',
content: typeof extractedPreviewContent === 'string' ? extractedPreviewContent : '',
}
}
const format = extractFieldValue(storedValues?.format)
const content = extractFieldValue(storedValues?.content)
return {
format: typeof format === 'string' ? format : 'plain',
content: typeof content === 'string' ? content : '',
const extractedContent = extractFieldValue(data.subBlockValues.content)
return typeof extractedContent === 'string' ? extractedContent : ''
}
const storedContent = extractFieldValue(storedValues?.content)
return typeof storedContent === 'string' ? storedContent : ''
}, [data.isPreview, data.subBlockValues, storedValues])
const content = noteValues.content ?? ''
const isEmpty = content.trim().length === 0
const showMarkdown = noteValues.format === 'markdown' && !isEmpty
const userPermissions = useUserPermissionsContext()
@@ -182,7 +216,7 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlo
<div className='group relative'>
<div
className={cn(
'relative z-[20] w-[250px] cursor-default select-none rounded-[8px] bg-[var(--surface-2)]'
'relative z-[20] w-[250px] cursor-default select-none rounded-[8px] border border-[var(--border)] bg-[var(--surface-2)]'
)}
onClick={handleClick}
>
@@ -194,15 +228,12 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlo
event.stopPropagation()
}}
>
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
<div
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
style={{ background: isEnabled ? config.bgColor : 'gray' }}
>
<config.icon className='h-[16px] w-[16px] text-white' />
</div>
<div className='flex min-w-0 flex-1 items-center'>
<span
className={cn('font-medium text-[16px]', !isEnabled && 'truncate text-[#808080]')}
className={cn(
'truncate font-medium text-[16px]',
!isEnabled && 'text-[var(--text-muted)]'
)}
title={name}
>
{name}
@@ -210,14 +241,12 @@ export const NoteBlock = memo(function NoteBlock({ id, data }: NodeProps<NoteBlo
</div>
</div>
<div className='relative px-[12px] pt-[6px] pb-[8px]'>
<div className='relative p-[8px]'>
<div className='relative break-words'>
{isEmpty ? (
<p className='text-[#868686] text-sm italic'>Add a note...</p>
) : showMarkdown ? (
<NoteMarkdown content={content} />
<p className='text-[#868686] text-sm'>Add note...</p>
) : (
<p className='whitespace-pre-wrap text-[#E5E5E5] text-sm leading-snug'>{content}</p>
<NoteMarkdown content={content} />
)}
</div>
</div>

View File

@@ -1,17 +1,18 @@
import { memo, useCallback } from 'react'
import { memo, useCallback, useMemo } from 'react'
import { createLogger } from '@sim/logger'
import clsx from 'clsx'
import { X } from 'lucide-react'
import { useParams } from 'next/navigation'
import { Button } from '@/components/emcn'
import { useRegisterGlobalCommands } from '@/app/workspace/[workspaceId]/providers/global-commands-provider'
import { createCommands } from '@/app/workspace/[workspaceId]/utils/commands-utils'
import { usePreventZoom } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks'
import {
type NotificationAction,
openCopilotWithMessage,
useNotificationStore,
} from '@/stores/notifications'
import { useTerminalStore } from '@/stores/terminal'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
const logger = createLogger('Notifications')
const MAX_VISIBLE_NOTIFICATIONS = 4
@@ -22,15 +23,18 @@ const MAX_VISIBLE_NOTIFICATIONS = 4
* Shows both global notifications and workflow-specific notifications
*/
export const Notifications = memo(function Notifications() {
const params = useParams()
const workflowId = params.workflowId as string
const activeWorkflowId = useWorkflowRegistry((state) => state.activeWorkflowId)
const notifications = useNotificationStore((state) =>
state.notifications.filter((n) => !n.workflowId || n.workflowId === workflowId)
)
const allNotifications = useNotificationStore((state) => state.notifications)
const removeNotification = useNotificationStore((state) => state.removeNotification)
const clearNotifications = useNotificationStore((state) => state.clearNotifications)
const visibleNotifications = notifications.slice(0, MAX_VISIBLE_NOTIFICATIONS)
const visibleNotifications = useMemo(() => {
if (!activeWorkflowId) return []
return allNotifications
.filter((n) => !n.workflowId || n.workflowId === activeWorkflowId)
.slice(0, MAX_VISIBLE_NOTIFICATIONS)
}, [allNotifications, activeWorkflowId])
const isTerminalResizing = useTerminalStore((state) => state.isResizing)
/**
@@ -84,7 +88,7 @@ export const Notifications = memo(function Notifications() {
{
id: 'clear-notifications',
handler: () => {
clearNotifications(workflowId)
clearNotifications(activeWorkflowId ?? undefined)
},
overrides: {
allowInEditable: false,
@@ -93,12 +97,15 @@ export const Notifications = memo(function Notifications() {
])
)
const preventZoomRef = usePreventZoom()
if (visibleNotifications.length === 0) {
return null
}
return (
<div
ref={preventZoomRef}
className={clsx(
'fixed right-[calc(var(--panel-width)+16px)] bottom-[calc(var(--terminal-height)+16px)] z-30 flex flex-col items-end',
!isTerminalResizing && 'transition-[bottom] duration-100 ease-out'
@@ -113,8 +120,8 @@ export const Notifications = memo(function Notifications() {
<div
key={notification.id}
style={{ transform: `translateX(${xOffset}px)` }}
className={`relative h-[78px] w-[240px] overflow-hidden rounded-[4px] border bg-[var(--surface-2)] transition-transform duration-200 ${
index > 0 ? '-mt-[78px]' : ''
className={`relative h-[80px] w-[240px] overflow-hidden rounded-[4px] border bg-[var(--surface-2)] transition-transform duration-200 ${
index > 0 ? '-mt-[80px]' : ''
}`}
>
<div className='flex h-full flex-col justify-between px-[8px] pt-[6px] pb-[8px]'>
@@ -137,19 +144,17 @@ export const Notifications = memo(function Notifications() {
{notification.message}
</div>
{hasAction && (
<div className='mt-[4px]'>
<Button
variant='active'
onClick={() => executeAction(notification.id, notification.action!)}
className='w-full px-[8px] py-[4px] font-medium text-[12px]'
>
{notification.action!.type === 'copilot'
? 'Fix in Copilot'
: notification.action!.type === 'refresh'
? 'Refresh'
: 'Take action'}
</Button>
</div>
<Button
variant='active'
onClick={() => executeAction(notification.id, notification.action!)}
className='w-full px-[8px] py-[4px] font-medium text-[12px]'
>
{notification.action!.type === 'copilot'
? 'Fix in Copilot'
: notification.action!.type === 'refresh'
? 'Refresh'
: 'Take action'}
</Button>
)}
</div>
</div>

View File

@@ -137,29 +137,29 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
() => ({
// Paragraph
p: ({ children }: React.HTMLAttributes<HTMLParagraphElement>) => (
<p className='mb-1 font-base font-season text-[#1f2124] text-sm leading-[1.25rem] last:mb-0 dark:font-[470] dark:text-[#E8E8E8]'>
<p className='mb-1 font-base font-season text-[var(--text-primary)] text-sm leading-[1.25rem] last:mb-0 dark:font-[470]'>
{children}
</p>
),
// Headings
h1: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h1 className='mt-3 mb-3 font-season font-semibold text-2xl text-[var(--text-primary)] dark:text-[#F0F0F0]'>
<h1 className='mt-3 mb-3 font-season font-semibold text-2xl text-[var(--text-primary)]'>
{children}
</h1>
),
h2: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h2 className='mt-2.5 mb-2.5 font-season font-semibold text-[var(--text-primary)] text-xl dark:text-[#F0F0F0]'>
<h2 className='mt-2.5 mb-2.5 font-season font-semibold text-[var(--text-primary)] text-xl'>
{children}
</h2>
),
h3: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h3 className='mt-2 mb-2 font-season font-semibold text-[var(--text-primary)] text-lg dark:text-[#F0F0F0]'>
<h3 className='mt-2 mb-2 font-season font-semibold text-[var(--text-primary)] text-lg'>
{children}
</h3>
),
h4: ({ children }: React.HTMLAttributes<HTMLHeadingElement>) => (
<h4 className='mt-5 mb-2 font-season font-semibold text-[var(--text-primary)] text-base dark:text-[#F0F0F0]'>
<h4 className='mt-5 mb-2 font-season font-semibold text-[var(--text-primary)] text-base'>
{children}
</h4>
),
@@ -167,7 +167,7 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
// Lists
ul: ({ children }: React.HTMLAttributes<HTMLUListElement>) => (
<ul
className='mt-1 mb-1 space-y-1 pl-6 font-base font-season text-[#1f2124] dark:font-[470] dark:text-[#E8E8E8]'
className='mt-1 mb-1 space-y-1 pl-6 font-base font-season text-[var(--text-primary)] dark:font-[470]'
style={{ listStyleType: 'disc' }}
>
{children}
@@ -175,7 +175,7 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
),
ol: ({ children }: React.HTMLAttributes<HTMLOListElement>) => (
<ol
className='mt-1 mb-1 space-y-1 pl-6 font-base font-season text-[#1f2124] dark:font-[470] dark:text-[#E8E8E8]'
className='mt-1 mb-1 space-y-1 pl-6 font-base font-season text-[var(--text-primary)] dark:font-[470]'
style={{ listStyleType: 'decimal' }}
>
{children}
@@ -186,7 +186,7 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
ordered,
}: React.LiHTMLAttributes<HTMLLIElement> & { ordered?: boolean }) => (
<li
className='font-base font-season text-[#1f2124] dark:font-[470] dark:text-[#E8E8E8]'
className='font-base font-season text-[var(--text-primary)] dark:font-[470]'
style={{ display: 'list-item' }}
>
{children}
@@ -256,14 +256,14 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
: 'javascript'
return (
<div className='my-6 w-0 min-w-full overflow-hidden rounded-md border border-[var(--border-strong)] bg-[#1F1F1F] text-sm'>
<div className='flex items-center justify-between border-[var(--border-strong)] border-b px-4 py-1.5'>
<span className='font-season text-[#A3A3A3] text-xs'>
<div className='my-6 w-0 min-w-full overflow-hidden rounded-md border border-[var(--border-1)] bg-[var(--surface-1)] text-sm'>
<div className='flex items-center justify-between border-[var(--border-1)] border-b px-4 py-1.5'>
<span className='font-season text-[var(--text-muted)] text-xs'>
{language === 'code' ? viewerLanguage : language}
</span>
<button
onClick={handleCopy}
className='text-[#A3A3A3] transition-colors hover:text-gray-300'
className='text-[var(--text-muted)] transition-colors hover:text-[var(--text-tertiary)]'
title='Copy'
>
{showCopySuccess ? (
@@ -293,7 +293,7 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
if (inline) {
return (
<code
className='whitespace-normal break-all rounded border border-[var(--border-strong)] bg-[#1F1F1F] px-1 py-0.5 font-mono text-[#eeeeee] text-[0.9em]'
className='whitespace-normal break-all rounded border border-[var(--border-1)] bg-[var(--surface-1)] px-1 py-0.5 font-mono text-[0.9em] text-[var(--text-primary)]'
{...props}
>
{children}
@@ -309,35 +309,33 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
// Bold text
strong: ({ children }: React.HTMLAttributes<HTMLElement>) => (
<strong className='font-semibold text-[var(--text-primary)] dark:text-[#F0F0F0]'>
{children}
</strong>
<strong className='font-semibold text-[var(--text-primary)]'>{children}</strong>
),
// Bold text (alternative)
b: ({ children }: React.HTMLAttributes<HTMLElement>) => (
<b className='font-semibold text-[var(--text-primary)] dark:text-[#F0F0F0]'>{children}</b>
<b className='font-semibold text-[var(--text-primary)]'>{children}</b>
),
// Italic text
em: ({ children }: React.HTMLAttributes<HTMLElement>) => (
<em className='text-[#1f2124] italic dark:text-[#E8E8E8]'>{children}</em>
<em className='text-[var(--text-primary)] italic'>{children}</em>
),
// Italic text (alternative)
i: ({ children }: React.HTMLAttributes<HTMLElement>) => (
<i className='text-[#1f2124] italic dark:text-[#E8E8E8]'>{children}</i>
<i className='text-[var(--text-primary)] italic'>{children}</i>
),
// Blockquotes
blockquote: ({ children }: React.HTMLAttributes<HTMLQuoteElement>) => (
<blockquote className='my-4 border-[var(--border-strong)] border-l-4 py-1 pl-4 font-season text-[#3a3d41] italic dark:border-gray-600 dark:text-[#E0E0E0]'>
<blockquote className='my-4 border-[var(--border-1)] border-l-4 py-1 pl-4 font-season text-[var(--text-secondary)] italic'>
{children}
</blockquote>
),
// Horizontal rule
hr: () => <hr className='my-8 border-[var(--divider)] border-t dark:border-gray-400/[.07]' />,
hr: () => <hr className='my-8 border-[var(--divider)] border-t' />,
// Links
a: ({ href, children, ...props }: React.AnchorHTMLAttributes<HTMLAnchorElement>) => (
@@ -349,29 +347,31 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
// Tables
table: ({ children }: React.TableHTMLAttributes<HTMLTableElement>) => (
<div className='my-4 max-w-full overflow-x-auto'>
<table className='min-w-full table-auto border border-[var(--border)] font-season text-sm dark:border-gray-600'>
<table className='min-w-full table-auto border border-[var(--border-1)] font-season text-sm'>
{children}
</table>
</div>
),
thead: ({ children }: React.HTMLAttributes<HTMLTableSectionElement>) => (
<thead className='bg-[var(--surface-9)] text-left dark:bg-[#2A2A2A]'>{children}</thead>
<thead className='bg-[var(--surface-5)] text-left dark:bg-[var(--surface-4)]'>
{children}
</thead>
),
tbody: ({ children }: React.HTMLAttributes<HTMLTableSectionElement>) => (
<tbody className='divide-y divide-[var(--border)] dark:divide-gray-600'>{children}</tbody>
<tbody className='divide-y divide-[var(--border-1)]'>{children}</tbody>
),
tr: ({ children }: React.HTMLAttributes<HTMLTableRowElement>) => (
<tr className='border-[var(--border)] border-b transition-colors hover:bg-[var(--surface-9)] dark:border-gray-600 dark:hover:bg-[#2A2A2A]/60'>
<tr className='border-[var(--border-1)] border-b transition-colors hover:bg-[var(--surface-5)] dark:hover:bg-[var(--surface-4)]/60'>
{children}
</tr>
),
th: ({ children }: React.ThHTMLAttributes<HTMLTableCellElement>) => (
<th className='border-[var(--border)] border-r px-4 py-2 align-top font-base text-[#3a3d41] last:border-r-0 dark:border-gray-600 dark:font-[470] dark:text-[#E0E0E0]'>
<th className='border-[var(--border-1)] border-r px-4 py-2 align-top font-base text-[var(--text-secondary)] last:border-r-0 dark:font-[470]'>
{children}
</th>
),
td: ({ children }: React.TdHTMLAttributes<HTMLTableCellElement>) => (
<td className='break-words border-[var(--border)] border-r px-4 py-2 align-top font-base text-[#1f2124] last:border-r-0 dark:border-gray-600 dark:font-[470] dark:text-[#E8E8E8]'>
<td className='break-words border-[var(--border-1)] border-r px-4 py-2 align-top font-base text-[var(--text-primary)] last:border-r-0 dark:font-[470]'>
{children}
</td>
),
@@ -390,7 +390,7 @@ export default function CopilotMarkdownRenderer({ content }: CopilotMarkdownRend
)
return (
<div className='copilot-markdown-wrapper max-w-full space-y-3 break-words font-base font-season text-[#1f2124] text-sm leading-[1.25rem] dark:font-[470] dark:text-[#E8E8E8]'>
<div className='copilot-markdown-wrapper max-w-full space-y-3 break-words font-base font-season text-[var(--text-primary)] text-sm leading-[1.25rem] dark:font-[470]'>
<ReactMarkdown remarkPlugins={[remarkGfm]} components={markdownComponents}>
{content}
</ReactMarkdown>

View File

@@ -36,8 +36,8 @@ interface ShimmerOverlayTextProps {
function ShimmerOverlayText({ label, value, active = false }: ShimmerOverlayTextProps) {
return (
<span className='relative inline-block'>
<span style={{ color: '#B8B8B8' }}>{label}</span>
<span style={{ color: 'var(--text-muted)' }}>{value}</span>
<span className='text-[var(--text-tertiary)]'>{label}</span>
<span className='text-[var(--text-muted)]'>{value}</span>
{active ? (
<span
aria-hidden='true'
@@ -194,14 +194,11 @@ export function ThinkingBlock({
</button>
{isExpanded && (
<div className='ml-1 border-[var(--border-strong)] border-l-2 pl-2'>
<pre
className='whitespace-pre-wrap font-[470] font-season text-[12px] leading-[1.15rem]'
style={{ color: '#B8B8B8' }}
>
<div className='ml-1 border-[var(--border-1)] border-l-2 pl-2'>
<pre className='whitespace-pre-wrap font-[470] font-season text-[12px] text-[var(--text-tertiary)] leading-[1.15rem]'>
{content}
{isStreaming && (
<span className='ml-1 inline-block h-2 w-1 animate-pulse bg-[#B8B8B8]' />
<span className='ml-1 inline-block h-2 w-1 animate-pulse bg-[var(--text-tertiary)]' />
)}
</pre>
</div>

View File

@@ -108,6 +108,8 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
const {
showRestoreConfirmation,
showCheckpointDiscardModal,
isReverting,
isProcessingDiscard,
pendingEditRef,
setShowCheckpointDiscardModal,
handleRevertToCheckpoint,
@@ -265,30 +267,35 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
{/* Inline Checkpoint Discard Confirmation - shown below input in edit mode */}
{showCheckpointDiscardModal && (
<div className='mt-[8px] rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] p-[10px] dark:bg-[var(--surface-9)]'>
<p className='mb-[8px] text-[var(--text-primary)] text-sm'>
<div className='mt-[8px] rounded-[4px] border border-[var(--border)] bg-[var(--surface-4)] p-[10px]'>
<p className='mb-[8px] text-[12px] text-[var(--text-primary)]'>
Continue from a previous message?
</p>
<div className='flex gap-[6px]'>
<div className='flex gap-[8px]'>
<Button
onClick={handleCancelCheckpointDiscard}
variant='default'
className='flex flex-1 items-center justify-center gap-[6px] px-[8px] py-[4px] text-xs'
variant='active'
size='sm'
className='flex-1'
disabled={isProcessingDiscard}
>
<span>Cancel</span>
<span className='text-[10px] text-[var(--text-muted)]'>(Esc)</span>
Cancel
</Button>
<Button
onClick={handleContinueAndRevert}
variant='outline'
className='flex-1 px-[8px] py-[4px] text-xs'
variant='destructive'
size='sm'
className='flex-1'
disabled={isProcessingDiscard}
>
Revert
{isProcessingDiscard ? 'Reverting...' : 'Revert'}
</Button>
<Button
onClick={handleContinueWithoutRevert}
variant='outline'
className='flex-1 px-[8px] py-[4px] text-xs'
variant='tertiary'
size='sm'
className='flex-1'
disabled={isProcessingDiscard}
>
Continue
</Button>
@@ -312,11 +319,11 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
onClick={handleMessageClick}
onMouseEnter={() => setIsHoveringMessage(true)}
onMouseLeave={() => setIsHoveringMessage(false)}
className='group relative w-full cursor-pointer rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[6px] py-[6px] transition-all duration-200 hover:border-[var(--surface-14)] hover:bg-[var(--surface-9)] dark:bg-[var(--surface-9)] dark:hover:border-[var(--surface-13)] dark:hover:bg-[var(--surface-11)]'
className='group relative w-full cursor-pointer rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[6px] py-[6px] transition-all duration-200 hover:border-[var(--surface-7)] hover:bg-[var(--surface-5)] dark:bg-[var(--surface-5)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]'
>
<div
ref={messageContentRef}
className={`relative whitespace-pre-wrap break-words px-[2px] py-1 font-medium font-sans text-[#0D0D0D] text-sm leading-[1.25rem] dark:text-gray-100 ${isSendingMessage && isLastUserMessage && isHoveringMessage ? 'pr-7' : ''} ${!isExpanded && needsExpansion ? 'max-h-[60px] overflow-hidden' : 'overflow-visible'}`}
className={`relative whitespace-pre-wrap break-words px-[2px] py-1 font-medium font-sans text-[var(--text-primary)] text-sm leading-[1.25rem] ${isSendingMessage && isLastUserMessage && isHoveringMessage ? 'pr-7' : ''} ${!isExpanded && needsExpansion ? 'max-h-[60px] overflow-hidden' : 'overflow-visible'}`}
>
{(() => {
const text = message.content || ''
@@ -358,7 +365,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
{/* Gradient fade when truncated - applies to entire message box */}
{!isExpanded && needsExpansion && (
<div className='pointer-events-none absolute right-0 bottom-0 left-0 h-6 bg-gradient-to-t from-0% from-[var(--surface-6)] via-40% via-[var(--surface-6)]/70 to-100% to-transparent group-hover:from-[var(--surface-9)] group-hover:via-[var(--surface-9)]/70 dark:from-[var(--surface-9)] dark:via-[var(--surface-9)]/70 dark:group-hover:from-[var(--surface-11)] dark:group-hover:via-[var(--surface-11)]/70' />
<div className='pointer-events-none absolute right-0 bottom-0 left-0 h-6 bg-gradient-to-t from-0% from-[var(--surface-5)] via-40% via-[var(--surface-5)]/70 to-100% to-transparent group-hover:from-[var(--surface-5)] group-hover:via-[var(--surface-5)]/70 dark:from-[var(--surface-5)] dark:via-[var(--surface-5)]/70 dark:group-hover:from-[var(--border-1)] dark:group-hover:via-[var(--border-1)]/70' />
)}
{/* Abort button when hovering and response is generating (only on last user message) */}
@@ -369,7 +376,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
e.stopPropagation()
abortMessage()
}}
className='h-[20px] w-[20px] rounded-full bg-[#C0C0C0] p-0 transition-colors hover:bg-[#D0D0D0] dark:bg-[#C0C0C0] dark:hover:bg-[#D0D0D0]'
className='h-[20px] w-[20px] rounded-full bg-[var(--c-C0C0C0)] p-0 transition-colors hover:bg-[var(--c-D0D0D0)]'
title='Stop generation'
>
<svg
@@ -406,29 +413,30 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
{/* Inline Restore Checkpoint Confirmation */}
{showRestoreConfirmation && (
<div className='mt-[8px] rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] p-[10px] dark:bg-[var(--surface-9)]'>
<p className='mb-[8px] text-[var(--text-primary)] text-sm'>
<div className='mt-[8px] rounded-[4px] border border-[var(--border)] bg-[var(--surface-4)] p-[10px]'>
<p className='mb-[8px] text-[12px] text-[var(--text-primary)]'>
Revert to checkpoint? This will restore your workflow to the state saved at this
checkpoint.{' '}
<span className='font-medium text-[var(--text-error)]'>
This action cannot be undone.
</span>
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
<div className='flex gap-[6px]'>
<div className='flex gap-[8px]'>
<Button
onClick={handleCancelRevert}
variant='default'
className='flex flex-1 items-center justify-center gap-[6px] px-[8px] py-[4px] text-xs'
variant='active'
size='sm'
className='flex-1'
disabled={isReverting}
>
<span>Cancel</span>
<span className='text-[10px] text-[var(--text-muted)]'>(Esc)</span>
Cancel
</Button>
<Button
onClick={handleConfirmRevert}
variant='outline'
className='flex-1 px-[8px] py-[4px] text-xs'
variant='destructive'
size='sm'
className='flex-1'
disabled={isReverting}
>
Revert
{isReverting ? 'Reverting...' : 'Revert'}
</Button>
</div>
</div>

View File

@@ -26,6 +26,8 @@ export function useCheckpointManagement(
) {
const [showRestoreConfirmation, setShowRestoreConfirmation] = useState(false)
const [showCheckpointDiscardModal, setShowCheckpointDiscardModal] = useState(false)
const [isReverting, setIsReverting] = useState(false)
const [isProcessingDiscard, setIsProcessingDiscard] = useState(false)
const pendingEditRef = useRef<{
message: string
fileAttachments?: any[]
@@ -48,6 +50,7 @@ export function useCheckpointManagement(
const handleConfirmRevert = useCallback(async () => {
if (messageCheckpoints.length > 0) {
const latestCheckpoint = messageCheckpoints[0]
setIsReverting(true)
try {
await revertToCheckpoint(latestCheckpoint.id)
@@ -100,6 +103,8 @@ export function useCheckpointManagement(
logger.error('Failed to revert to checkpoint:', error)
setShowRestoreConfirmation(false)
onRevertModeChange?.(false)
} finally {
setIsReverting(false)
}
}
}, [
@@ -125,50 +130,55 @@ export function useCheckpointManagement(
* Reverts to checkpoint then proceeds with pending edit
*/
const handleContinueAndRevert = useCallback(async () => {
if (messageCheckpoints.length > 0) {
const latestCheckpoint = messageCheckpoints[0]
try {
await revertToCheckpoint(latestCheckpoint.id)
setIsProcessingDiscard(true)
try {
if (messageCheckpoints.length > 0) {
const latestCheckpoint = messageCheckpoints[0]
try {
await revertToCheckpoint(latestCheckpoint.id)
const { messageCheckpoints: currentCheckpoints } = useCopilotStore.getState()
const updatedCheckpoints = {
...currentCheckpoints,
[message.id]: messageCheckpoints.slice(1),
const { messageCheckpoints: currentCheckpoints } = useCopilotStore.getState()
const updatedCheckpoints = {
...currentCheckpoints,
[message.id]: messageCheckpoints.slice(1),
}
useCopilotStore.setState({ messageCheckpoints: updatedCheckpoints })
logger.info('Reverted to checkpoint before editing message', {
messageId: message.id,
checkpointId: latestCheckpoint.id,
})
} catch (error) {
logger.error('Failed to revert to checkpoint:', error)
}
useCopilotStore.setState({ messageCheckpoints: updatedCheckpoints })
logger.info('Reverted to checkpoint before editing message', {
messageId: message.id,
checkpointId: latestCheckpoint.id,
})
} catch (error) {
logger.error('Failed to revert to checkpoint:', error)
}
}
setShowCheckpointDiscardModal(false)
setShowCheckpointDiscardModal(false)
const { sendMessage } = useCopilotStore.getState()
if (pendingEditRef.current) {
const { message: msg, fileAttachments, contexts } = pendingEditRef.current
const editIndex = messages.findIndex((m) => m.id === message.id)
if (editIndex !== -1) {
const truncatedMessages = messages.slice(0, editIndex)
const updatedMessage = {
...message,
content: msg,
fileAttachments: fileAttachments || message.fileAttachments,
contexts: contexts || (message as any).contexts,
const { sendMessage } = useCopilotStore.getState()
if (pendingEditRef.current) {
const { message: msg, fileAttachments, contexts } = pendingEditRef.current
const editIndex = messages.findIndex((m) => m.id === message.id)
if (editIndex !== -1) {
const truncatedMessages = messages.slice(0, editIndex)
const updatedMessage = {
...message,
content: msg,
fileAttachments: fileAttachments || message.fileAttachments,
contexts: contexts || (message as any).contexts,
}
useCopilotStore.setState({ messages: [...truncatedMessages, updatedMessage] })
await sendMessage(msg, {
fileAttachments: fileAttachments || message.fileAttachments,
contexts: contexts || (message as any).contexts,
messageId: message.id,
})
}
useCopilotStore.setState({ messages: [...truncatedMessages, updatedMessage] })
await sendMessage(msg, {
fileAttachments: fileAttachments || message.fileAttachments,
contexts: contexts || (message as any).contexts,
messageId: message.id,
})
pendingEditRef.current = null
}
pendingEditRef.current = null
} finally {
setIsProcessingDiscard(false)
}
}, [messageCheckpoints, revertToCheckpoint, message, messages])
@@ -252,6 +262,8 @@ export function useCheckpointManagement(
// State
showRestoreConfirmation,
showCheckpointDiscardModal,
isReverting,
isProcessingDiscard,
pendingEditRef,
// Operations

View File

@@ -35,9 +35,9 @@ import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId
/**
* Shared border and background styles
*/
const SURFACE_5 = 'bg-[var(--surface-5)]'
const SURFACE_9 = 'bg-[var(--surface-9)]'
const BORDER_STRONG = 'border-[var(--border-strong)]'
const SURFACE_5 = 'bg-[var(--surface-4)]'
const SURFACE_9 = 'bg-[var(--surface-5)]'
const BORDER_STRONG = 'border-[var(--border-1)]'
export interface PlanModeSectionProps {
/**
@@ -184,7 +184,7 @@ const PlanModeSection: React.FC<PlanModeSectionProps> = ({
style={{ height: `${height}px` }}
>
{/* Header with build/edit/save/clear buttons */}
<div className='flex flex-shrink-0 items-center justify-between border-[var(--border-strong)] border-b py-[6px] pr-[2px] pl-[12px]'>
<div className='flex flex-shrink-0 items-center justify-between border-[var(--border-1)] border-b py-[6px] pr-[2px] pl-[12px]'>
<span className='font-[500] text-[11px] text-[var(--text-secondary)] uppercase tracking-wide'>
Workflow Plan
</span>
@@ -265,7 +265,7 @@ const PlanModeSection: React.FC<PlanModeSectionProps> = ({
className={cn(
'group flex h-[20px] w-full cursor-ns-resize items-center justify-center border-t',
BORDER_STRONG,
'transition-colors hover:bg-[var(--surface-9)]',
'transition-colors hover:bg-[var(--surface-5)]',
isResizing && SURFACE_9
)}
onMouseDown={handleResizeStart}

View File

@@ -66,7 +66,7 @@ export const TodoList = memo(function TodoList({
return (
<div
className={cn(
'w-full rounded-t-[4px] rounded-b-none border-[var(--surface-11)] border-x border-t bg-[var(--surface-6)] dark:bg-[var(--surface-9)]',
'w-full rounded-t-[4px] rounded-b-none border-[var(--border-1)] border-x border-t bg-[var(--surface-5)] dark:bg-[var(--surface-5)]',
className
)}
>
@@ -92,7 +92,7 @@ export const TodoList = memo(function TodoList({
<div className='flex flex-1 items-center gap-[8px] pl-[10px]'>
{/* Progress bar */}
<div className='h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--surface-11)]'>
<div className='h-1.5 flex-1 overflow-hidden rounded-full bg-[var(--border-1)]'>
<div
className='h-full bg-[var(--brand-400)] transition-all duration-300 ease-out'
style={{ width: `${progress}%` }}
@@ -119,8 +119,8 @@ export const TodoList = memo(function TodoList({
<div
key={todo.id}
className={cn(
'flex items-start gap-2 px-3 py-1.5 transition-colors hover:bg-[var(--surface-9)]/50 dark:hover:bg-[var(--surface-11)]/50',
index !== todos.length - 1 && 'border-[var(--surface-11)] border-b'
'flex items-start gap-2 px-3 py-1.5 transition-colors hover:bg-[var(--surface-5)]/50 dark:hover:bg-[var(--border-1)]/50',
index !== todos.length - 1 && 'border-[var(--border-1)] border-b'
)}
>
{todo.executing ? (
@@ -133,7 +133,7 @@ export const TodoList = memo(function TodoList({
'mt-0.5 flex h-4 w-4 flex-shrink-0 items-center justify-center rounded border transition-all',
todo.completed
? 'border-[var(--brand-400)] bg-[var(--brand-400)]'
: 'border-[#707070]'
: 'border-[var(--text-muted)]'
)}
>
{todo.completed ? <Check className='h-3 w-3 text-white' strokeWidth={3} /> : null}
@@ -143,7 +143,9 @@ export const TodoList = memo(function TodoList({
<span
className={cn(
'flex-1 font-base text-[12px] leading-relaxed',
todo.completed ? 'text-[var(--text-muted)] line-through' : 'text-[var(--white)]'
todo.completed
? 'text-[var(--text-muted)] line-through'
: 'text-[var(--text-primary)]'
)}
>
{todo.content}

View File

@@ -1,7 +1,6 @@
'use client'
import { useEffect, useRef, useState } from 'react'
import { Loader2 } from 'lucide-react'
import { Button, Code } from '@/components/emcn'
import { ClientToolCallState } from '@/lib/copilot/tools/client/base-tool'
import { getClientTool } from '@/lib/copilot/tools/client/manager'
@@ -141,13 +140,11 @@ function ShimmerOverlayText({
}: ShimmerOverlayTextProps) {
const [actionVerb, remainder] = splitActionVerb(text)
// Special tools: use gradient for entire text
// Special tools: use tertiary-2 color for entire text with shimmer
if (isSpecial) {
return (
<span className={`relative inline-block ${className || ''}`}>
<span className='bg-gradient-to-r from-[#7c3aed] to-[#9061f9] bg-clip-text text-transparent dark:from-[#B99FFD] dark:to-[#D1BFFF]'>
{text}
</span>
<span className='text-[var(--brand-tertiary-2)]'>{text}</span>
{active ? (
<span
aria-hidden='true'
@@ -157,7 +154,7 @@ function ShimmerOverlayText({
className='block text-transparent'
style={{
backgroundImage:
'linear-gradient(90deg, rgba(142,76,251,0) 0%, rgba(255,255,255,0.6) 50%, rgba(142,76,251,0) 100%)',
'linear-gradient(90deg, rgba(51,196,129,0) 0%, rgba(255,255,255,0.6) 50%, rgba(51,196,129,0) 100%)',
backgroundSize: '200% 100%',
backgroundRepeat: 'no-repeat',
WebkitBackgroundClip: 'text',
@@ -186,8 +183,12 @@ function ShimmerOverlayText({
<span className={`relative inline-block ${className || ''}`}>
{actionVerb ? (
<>
<span className='text-[#1f2124] dark:text-[#B8B8B8]'>{actionVerb}</span>
<span className='text-[#6b7075] dark:text-[var(--text-muted)]'>{remainder}</span>
<span className='text-[var(--text-primary)] dark:text-[var(--text-tertiary)]'>
{actionVerb}
</span>
<span className='text-[var(--text-secondary)] dark:text-[var(--text-muted)]'>
{remainder}
</span>
</>
) : (
<span>{text}</span>
@@ -504,13 +505,11 @@ function RunSkipButtons({
// Standardized buttons for all interrupt tools: Allow, Always Allow, Skip
return (
<div className='mt-[12px] flex gap-[6px]'>
<Button onClick={onRun} disabled={isProcessing} variant='primary'>
{isProcessing ? <Loader2 className='mr-1 h-3 w-3 animate-spin' /> : null}
Allow
<Button onClick={onRun} disabled={isProcessing} variant='tertiary'>
{isProcessing ? 'Allowing...' : 'Allow'}
</Button>
<Button onClick={onAlwaysAllow} disabled={isProcessing} variant='default'>
{isProcessing ? <Loader2 className='mr-1 h-3 w-3 animate-spin' /> : null}
Always Allow
{isProcessing ? 'Allowing...' : 'Always Allow'}
</Button>
<Button onClick={onSkip} disabled={isProcessing} variant='default'>
Skip
@@ -606,11 +605,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
const url = editedParams.url || ''
const method = (editedParams.method || '').toUpperCase()
return (
<div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F]'>
<div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
<table className='w-full table-fixed bg-transparent'>
<thead className='bg-transparent'>
<tr className='border-[var(--border-strong)] border-b bg-transparent'>
<th className='w-[26%] border-[var(--border-strong)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
<tr className='border-[var(--border-1)] border-b bg-transparent'>
<th className='w-[26%] border-[var(--border-1)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
Method
</th>
<th className='w-[74%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
@@ -619,8 +618,8 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
</tr>
</thead>
<tbody className='bg-transparent'>
<tr className='group relative border-[var(--border-strong)] border-t bg-transparent'>
<td className='relative w-[26%] border-[var(--border-strong)] border-r bg-transparent p-0'>
<tr className='group relative border-[var(--border-1)] border-t bg-transparent'>
<td className='relative w-[26%] border-[var(--border-1)] border-r bg-transparent p-0'>
<div className='px-[10px] py-[8px]'>
<input
type='text'
@@ -668,11 +667,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
})
return (
<div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F]'>
<div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
<table className='w-full table-fixed bg-transparent'>
<thead className='bg-transparent'>
<tr className='border-[var(--border-strong)] border-b bg-transparent'>
<th className='w-[36%] border-[var(--border-strong)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
<tr className='border-[var(--border-1)] border-b bg-transparent'>
<th className='w-[36%] border-[var(--border-1)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
Name
</th>
<th className='w-[64%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
@@ -682,8 +681,8 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
</thead>
<tbody className='bg-transparent'>
{normalizedEntries.length === 0 ? (
<tr className='border-[var(--border-strong)] border-t bg-transparent'>
<td colSpan={2} className='px-[10px] py-[8px] text-muted-foreground text-xs'>
<tr className='border-[var(--border-1)] border-t bg-transparent'>
<td colSpan={2} className='px-[10px] py-[8px] text-[var(--text-muted)] text-xs'>
No variables provided
</td>
</tr>
@@ -691,9 +690,9 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
normalizedEntries.map(([originalKey, name, value]) => (
<tr
key={originalKey}
className='group relative border-[var(--border-strong)] border-t bg-transparent'
className='group relative border-[var(--border-1)] border-t bg-transparent'
>
<td className='relative w-[36%] border-[var(--border-strong)] border-r bg-transparent p-0'>
<td className='relative w-[36%] border-[var(--border-1)] border-r bg-transparent p-0'>
<div className='px-[10px] py-[8px]'>
<input
type='text'
@@ -720,7 +719,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
}
setEditedParams({ ...editedParams, variables: newVariables })
}}
className='w-full bg-transparent font-medium text-foreground text-xs outline-none'
className='w-full bg-transparent font-medium text-[var(--text-primary)] text-xs outline-none'
/>
</div>
</td>
@@ -755,7 +754,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
}
setEditedParams({ ...editedParams, variables: newVariables })
}}
className='w-full bg-transparent font-mono text-muted-foreground text-xs outline-none focus:text-foreground'
className='w-full bg-transparent font-mono text-[var(--text-muted)] text-xs outline-none focus:text-[var(--text-primary)]'
/>
</div>
</td>
@@ -771,24 +770,24 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
if (toolCall.name === 'set_global_workflow_variables') {
const ops = Array.isArray(editedParams.operations) ? (editedParams.operations as any[]) : []
return (
<div className='w-full overflow-hidden rounded border border-muted bg-card'>
<div className='grid grid-cols-3 gap-0 border-muted/60 border-b bg-muted/40 py-1.5'>
<div className='self-start px-2 font-medium font-season text-[#3a3d41] text-[10px] uppercase tracking-wide dark:text-[#E0E0E0]'>
<div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
<div className='grid grid-cols-3 gap-0 border-[var(--border-1)] border-b bg-[var(--surface-4)] py-1.5'>
<div className='self-start px-2 font-medium font-season text-[10px] text-[var(--text-secondary)] uppercase tracking-wide'>
Name
</div>
<div className='self-start px-2 font-medium font-season text-[#3a3d41] text-[10px] uppercase tracking-wide dark:text-[#E0E0E0]'>
<div className='self-start px-2 font-medium font-season text-[10px] text-[var(--text-secondary)] uppercase tracking-wide'>
Type
</div>
<div className='self-start px-2 font-medium font-season text-[#3a3d41] text-[10px] uppercase tracking-wide dark:text-[#E0E0E0]'>
<div className='self-start px-2 font-medium font-season text-[10px] text-[var(--text-secondary)] uppercase tracking-wide'>
Value
</div>
</div>
{ops.length === 0 ? (
<div className='px-2 py-2 font-[470] font-season text-[#1f2124] text-xs dark:text-[#E8E8E8]'>
<div className='px-2 py-2 font-[470] font-season text-[var(--text-primary)] text-xs'>
No operations provided
</div>
) : (
<div className='divide-y divide-amber-200 dark:divide-amber-800'>
<div className='divide-y divide-[var(--border-1)]'>
{ops.map((op, idx) => (
<div key={idx} className='grid grid-cols-3 gap-0 py-1.5'>
<div className='min-w-0 self-start px-2'>
@@ -800,11 +799,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
newOps[idx] = { ...op, name: e.target.value }
setEditedParams({ ...editedParams, operations: newOps })
}}
className='w-full bg-transparent font-season text-amber-800 text-xs outline-none dark:text-amber-200'
className='w-full bg-transparent font-season text-[var(--text-primary)] text-xs outline-none'
/>
</div>
<div className='self-start px-2'>
<span className='rounded border px-1 py-0.5 font-[470] font-season text-[#1f2124] text-[10px] dark:text-[#E8E8E8]'>
<span className='rounded border border-[var(--border-1)] px-1 py-0.5 font-[470] font-season text-[10px] text-[var(--text-primary)]'>
{String(op.type || '')}
</span>
</div>
@@ -818,10 +817,10 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
newOps[idx] = { ...op, value: e.target.value }
setEditedParams({ ...editedParams, operations: newOps })
}}
className='w-full bg-transparent font-[470] font-mono text-amber-700 text-xs outline-none focus:text-amber-800 dark:text-amber-300 dark:focus:text-amber-200'
className='w-full bg-transparent font-[470] font-mono text-[var(--text-muted)] text-xs outline-none focus:text-[var(--text-primary)]'
/>
) : (
<span className='font-[470] font-season text-[#1f2124] text-xs dark:text-[#E8E8E8]'>
<span className='font-[470] font-season text-[var(--text-primary)] text-xs'>
</span>
)}
@@ -864,11 +863,11 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
const inputEntries = Object.entries(safeInputs)
return (
<div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F]'>
<div className='w-full overflow-hidden rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-1)]'>
<table className='w-full table-fixed bg-transparent'>
<thead className='bg-transparent'>
<tr className='border-[var(--border-strong)] border-b bg-transparent'>
<th className='w-[36%] border-[var(--border-strong)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
<tr className='border-[var(--border-1)] border-b bg-transparent'>
<th className='w-[36%] border-[var(--border-1)] border-r bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
Input
</th>
<th className='w-[64%] bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]'>
@@ -878,8 +877,8 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
</thead>
<tbody className='bg-transparent'>
{inputEntries.length === 0 ? (
<tr className='border-[var(--border-strong)] border-t bg-transparent'>
<td colSpan={2} className='px-[10px] py-[8px] text-muted-foreground text-xs'>
<tr className='border-[var(--border-1)] border-t bg-transparent'>
<td colSpan={2} className='px-[10px] py-[8px] text-[var(--text-muted)] text-xs'>
No inputs provided
</td>
</tr>
@@ -887,11 +886,13 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
inputEntries.map(([key, value]) => (
<tr
key={key}
className='group relative border-[var(--border-strong)] border-t bg-transparent'
className='group relative border-[var(--border-1)] border-t bg-transparent'
>
<td className='relative w-[36%] border-[var(--border-strong)] border-r bg-transparent p-0'>
<td className='relative w-[36%] border-[var(--border-1)] border-r bg-transparent p-0'>
<div className='px-[10px] py-[8px]'>
<span className='truncate font-medium text-foreground text-xs'>{key}</span>
<span className='truncate font-medium text-[var(--text-primary)] text-xs'>
{key}
</span>
</div>
</td>
<td className='relative w-[64%] bg-transparent p-0'>
@@ -926,7 +927,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
setEditedParams({ ...editedParams, [key]: e.target.value })
}
}}
className='w-full bg-transparent font-mono text-muted-foreground text-xs outline-none focus:text-foreground'
className='w-full bg-transparent font-mono text-[var(--text-muted)] text-xs outline-none focus:text-[var(--text-primary)]'
/>
</div>
</td>
@@ -959,7 +960,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
text={displayName}
active={isLoadingState}
isSpecial={isSpecial}
className='font-[470] font-season text-[#3a3d41] text-sm dark:text-[#939393]'
className='font-[470] font-season text-[var(--text-secondary)] text-sm dark:text-[var(--text-muted)]'
/>
</div>
<div className='mt-[8px]'>{renderPendingDetails()}</div>
@@ -1010,7 +1011,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
text={displayName}
active={isLoadingState}
isSpecial={false}
className='font-[470] font-season text-[#939393] text-sm dark:text-[#939393]'
className='font-[470] font-season text-[var(--text-muted)] text-sm'
/>
</div>
{code && (
@@ -1062,7 +1063,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
text={displayName}
active={isLoadingState}
isSpecial={isSpecial}
className='font-[470] font-season text-[#3a3d41] text-sm dark:text-[#939393]'
className='font-[470] font-season text-[var(--text-secondary)] text-sm dark:text-[var(--text-muted)]'
/>
</div>
{isExpandableTool && expanded && <div>{renderPendingDetails()}</div>}
@@ -1104,7 +1105,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
onStateChange?.('background')
} catch {}
}}
variant='primary'
variant='tertiary'
title='Move to Background'
>
Move to Background
@@ -1135,7 +1136,7 @@ export function ToolCall({ toolCall: toolCallProp, toolCallId, onStateChange }:
onStateChange?.('background')
} catch {}
}}
variant='primary'
variant='tertiary'
title='Wake'
>
Wake

View File

@@ -71,7 +71,7 @@ export function AttachedFilesDisplay({
{files.map((file) => (
<div
key={file.id}
className='group relative h-16 w-16 flex-shrink-0 cursor-pointer overflow-hidden rounded-md border border-border/50 bg-muted/20 transition-all hover:bg-muted/40'
className='group relative h-16 w-16 flex-shrink-0 cursor-pointer overflow-hidden rounded-md border bg-muted/20 transition-all hover:bg-muted/40'
title={`${file.name} (${formatFileSize(file.size)})`}
onClick={() => onFileClick(file)}
>

View File

@@ -29,9 +29,9 @@ export function ContextUsageIndicator({
const offset = circumference - (percentage / 100) * circumference
const color = useMemo(() => {
if (percentage >= 90) return '#dc2626'
if (percentage >= 75) return '#d97706'
return '#6b7280'
if (percentage >= 90) return 'var(--text-error)'
if (percentage >= 75) return 'var(--warning)'
return 'var(--text-muted)'
}, [percentage])
const displayPercentage = useMemo(() => {

View File

@@ -29,7 +29,7 @@ function formatTimestamp(iso: string): string {
/**
* Common text styling for loading and empty states
*/
const STATE_TEXT_CLASSES = 'px-[8px] py-[8px] text-[#868686] text-[12px] dark:text-[#868686]'
const STATE_TEXT_CLASSES = 'px-[8px] py-[8px] text-[12px] text-[var(--text-muted)]'
/**
* Loading state component for mention folders
@@ -541,9 +541,7 @@ export function MentionMenu({
active={index === submenuActiveIndex}
>
<span className='flex-1 truncate'>{tpl.name}</span>
<span className='text-[#868686] text-[10px] dark:text-[#868686]'>
{tpl.stars}
</span>
<span className='text-[10px] text-[var(--text-muted)]'>{tpl.stars}</span>
</PopoverItem>
))
)}
@@ -745,9 +743,7 @@ export function MentionMenu({
mentionData.templatesList.map((tpl) => (
<PopoverItem key={tpl.id} onClick={() => insertTemplateMention(tpl)}>
<span className='flex-1 truncate'>{tpl.name}</span>
<span className='text-[#868686] text-[10px] dark:text-[#868686]'>
{tpl.stars}
</span>
<span className='text-[10px] text-[var(--text-muted)]'>{tpl.stars}</span>
</PopoverItem>
))
)}

View File

@@ -619,7 +619,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
<div
ref={setInputContainerRef}
className={cn(
'relative w-full rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[6px] py-[6px] transition-colors dark:bg-[var(--surface-9)]',
'relative w-full rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[6px] py-[6px] transition-colors dark:bg-[var(--surface-5)]',
fileAttachments.isDragging && 'ring-[1.75px] ring-[var(--brand-secondary)]'
)}
onDragEnter={fileAttachments.handleDragEnter}
@@ -679,7 +679,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
{/* Highlight overlay - must have identical flow as textarea */}
<div
ref={overlayRef}
className='pointer-events-none absolute top-0 left-0 z-[1] m-0 box-border h-auto max-h-[120px] min-h-[48px] w-full resize-none overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words border-0 bg-transparent px-[2px] py-1 font-medium font-sans text-[#0D0D0D] text-sm leading-[1.25rem] outline-none [-ms-overflow-style:none] [scrollbar-width:none] [text-rendering:optimizeLegibility] dark:text-gray-100 [&::-webkit-scrollbar]:hidden'
className='pointer-events-none absolute top-0 left-0 z-[1] m-0 box-border h-auto max-h-[120px] min-h-[48px] w-full resize-none overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words border-0 bg-transparent px-[2px] py-1 font-medium font-sans text-[var(--text-primary)] text-sm leading-[1.25rem] outline-none [-ms-overflow-style:none] [scrollbar-width:none] [text-rendering:optimizeLegibility] [&::-webkit-scrollbar]:hidden'
aria-hidden='true'
>
{renderOverlayContent()}
@@ -760,8 +760,8 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
className={cn(
'h-[20px] w-[20px] rounded-full p-0 transition-colors',
!isAborting
? 'bg-[#C0C0C0] hover:bg-[#D0D0D0] dark:bg-[#C0C0C0] dark:hover:bg-[#D0D0D0]'
: 'bg-[#C0C0C0] dark:bg-[#C0C0C0]'
? 'bg-[var(--c-C0C0C0)] hover:bg-[var(--c-D0D0D0)]'
: 'bg-[var(--c-C0C0C0)]'
)}
title='Stop generation'
>
@@ -787,8 +787,8 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
className={cn(
'h-[22px] w-[22px] rounded-full p-0 transition-colors',
canSubmit
? 'bg-[#C0C0C0] hover:bg-[#D0D0D0] dark:bg-[#C0C0C0] dark:hover:bg-[#D0D0D0]'
: 'bg-[#C0C0C0] dark:bg-[#C0C0C0]'
? 'bg-[var(--c-C0C0C0)] hover:bg-[var(--c-D0D0D0)]'
: 'bg-[var(--c-C0C0C0)]'
)}
>
{isLoading ? (

View File

@@ -398,7 +398,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
className='flex h-full flex-col overflow-hidden'
>
{/* Header */}
<div className='flex flex-shrink-0 items-center justify-between gap-[8px] rounded-[4px] bg-[var(--surface-5)] px-[12px] py-[8px]'>
<div className='mx-[-1px] flex flex-shrink-0 items-center justify-between gap-[8px] rounded-[4px] border border-[var(--border)] bg-[var(--surface-4)] px-[12px] py-[6px]'>
<h2 className='min-w-0 flex-1 truncate font-medium text-[14px] text-[var(--text-primary)]'>
{currentChat?.title || 'New Chat'}
</h2>
@@ -418,7 +418,7 @@ export const Copilot = forwardRef<CopilotRef, CopilotProps>(({ panelWidth }, ref
<ChatHistorySkeleton />
</PopoverScrollArea>
) : groupedChats.length === 0 ? (
<div className='px-[6px] py-[16px] text-center text-[12px] text-[var(--white)]'>
<div className='px-[6px] py-[16px] text-center text-[12px] text-muted-foreground'>
No chats yet
</div>
) : (

View File

@@ -478,7 +478,7 @@ console.log(limits);`
code={info.endpoint}
language='javascript'
wrapText
className='!min-h-0 rounded-[4px] border border-[var(--border-strong)]'
className='!min-h-0 rounded-[4px] border border-[var(--border-1)]'
/>
</div> */}
@@ -534,7 +534,7 @@ console.log(limits);`
code={getSyncCommand()}
language={LANGUAGE_SYNTAX[language]}
wrapText
className='!min-h-0 rounded-[4px] border border-[var(--border-strong)]'
className='!min-h-0 rounded-[4px] border border-[var(--border-1)]'
/>
</div>
@@ -577,7 +577,7 @@ console.log(limits);`
code={getStreamCommand()}
language={LANGUAGE_SYNTAX[language]}
wrapText
className='!min-h-0 rounded-[4px] border border-[var(--border-strong)]'
className='!min-h-0 rounded-[4px] border border-[var(--border-1)]'
/>
</div>
@@ -655,7 +655,7 @@ console.log(limits);`
code={getAsyncCommand()}
language={LANGUAGE_SYNTAX[language]}
wrapText
className='!min-h-0 rounded-[4px] border border-[var(--border-strong)]'
className='!min-h-0 rounded-[4px] border border-[var(--border-1)]'
/>
</div>
)}

View File

@@ -394,7 +394,7 @@ export function ChatDeploy({
<ModalContent size='sm'>
<ModalHeader>Delete Chat</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete this chat?{' '}
<span className='text-[var(--text-error)]'>
This will remove the chat at "{getEmailDomain()}/chat/{existingChat?.identifier}"
@@ -410,13 +410,7 @@ export function ChatDeploy({
>
Cancel
</Button>
<Button
variant='primary'
onClick={handleDelete}
disabled={isDeleting}
className='gap-[8px] bg-[var(--text-error)] text-[13px] text-white hover:bg-[var(--text-error)]'
>
{isDeleting && <Loader2 className='mr-1 h-4 w-4 animate-spin' />}
<Button variant='destructive' onClick={handleDelete} disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
</ModalFooter>
@@ -507,11 +501,11 @@ function IdentifierInput({
</Label>
<div
className={cn(
'relative flex items-stretch overflow-hidden rounded-[4px] border border-[var(--surface-11)]',
'relative flex items-stretch overflow-hidden rounded-[4px] border border-[var(--border-1)]',
error && 'border-[var(--text-error)]'
)}
>
<div className='flex items-center whitespace-nowrap bg-[var(--surface-6)] px-[8px] font-medium text-[var(--text-secondary)] text-sm dark:bg-[var(--surface-9)]'>
<div className='flex items-center whitespace-nowrap bg-[var(--surface-5)] px-[8px] font-medium text-[var(--text-secondary)] text-sm dark:bg-[var(--surface-5)]'>
{getDomainPrefix()}
</div>
<div className='relative flex-1'>
@@ -787,7 +781,7 @@ function AuthSelector({
<Label className='mb-[6.5px] block pl-[2px] font-medium text-[13px] text-[var(--text-primary)]'>
{authType === 'email' ? 'Allowed emails' : 'Allowed SSO emails'}
</Label>
<div className='scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-9)]'>
<div className='scrollbar-hide flex max-h-32 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] focus-within:outline-none dark:bg-[var(--surface-5)]'>
{invalidEmails.map((email, index) => (
<EmailTag
key={`invalid-${index}`}
@@ -851,7 +845,7 @@ function EmailTag({ email, onRemove, disabled, isInvalid }: EmailTagProps) {
'flex w-auto items-center gap-[4px] rounded-[4px] border px-[6px] py-[2px] text-[12px]',
isInvalid
? 'border-[var(--text-error)] bg-[color-mix(in_srgb,var(--text-error)_10%,transparent)] text-[var(--text-error)] dark:bg-[color-mix(in_srgb,var(--text-error)_16%,transparent)]'
: 'border-[var(--surface-11)] bg-[var(--surface-5)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
: 'border-[var(--border-1)] bg-[var(--surface-4)] text-[var(--text-secondary)] hover:text-[var(--text-primary)]'
)}
>
<span className='max-w-[200px] truncate'>{email}</span>

View File

@@ -12,7 +12,7 @@ const logger = createLogger('Versions')
/** Shared styling constants aligned with terminal component */
const HEADER_TEXT_CLASS = 'font-medium text-[var(--text-tertiary)] text-[12px]'
const ROW_TEXT_CLASS = 'font-medium text-[#D2D2D2] text-[12px]'
const ROW_TEXT_CLASS = 'font-medium text-[var(--text-primary)] text-[12px]'
const COLUMN_BASE_CLASS = 'flex-shrink-0'
/** Column width configuration */
@@ -220,10 +220,10 @@ export function Versions({
<div
key={v.id}
className={clsx(
'flex h-[36px] cursor-pointer items-center px-[16px] transition-colors',
'flex h-[36px] cursor-pointer items-center px-[16px] transition-colors duration-100',
isSelected
? 'bg-[var(--accent)]/10 hover:bg-[var(--accent)]/15'
: 'hover:bg-[var(--border)]'
: 'hover:bg-[var(--surface-6)] dark:hover:bg-[var(--border)]'
)}
onClick={() => handleRowClick(v.version)}
>

View File

@@ -257,7 +257,7 @@ export function GeneralDeploy({
<ModalContent size='sm'>
<ModalHeader>Load Deployment</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to load{' '}
<span className='font-medium text-[var(--text-primary)]'>
{versionToLoadInfo?.name || `v${versionToLoad}`}
@@ -272,11 +272,7 @@ export function GeneralDeploy({
<Button variant='default' onClick={() => setShowLoadDialog(false)}>
Cancel
</Button>
<Button
variant='primary'
onClick={confirmLoadDeployment}
className='bg-[var(--text-error)] text-white hover:bg-[var(--text-error)]'
>
<Button variant='destructive' onClick={confirmLoadDeployment}>
Load deployment
</Button>
</ModalFooter>
@@ -302,7 +298,7 @@ export function GeneralDeploy({
<Button variant='default' onClick={() => setShowPromoteDialog(false)}>
Cancel
</Button>
<Button variant='primary' onClick={confirmPromoteToLive}>
<Button variant='tertiary' onClick={confirmPromoteToLive}>
Promote to live
</Button>
</ModalFooter>

View File

@@ -1,8 +1,7 @@
'use client'
import React, { useEffect, useRef, useState } from 'react'
import { forwardRef, useEffect, useRef, useState } from 'react'
import { createLogger } from '@sim/logger'
import { Loader2 } from 'lucide-react'
import {
Button,
Combobox,
@@ -364,7 +363,7 @@ export function TemplateDeploy({
</p>
<Button
type='button'
variant='primary'
variant='tertiary'
onClick={() => {
try {
const event = new CustomEvent('open-settings', {
@@ -425,7 +424,7 @@ export function TemplateDeploy({
<ModalContent size='sm'>
<ModalHeader>Delete Template</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete this template?{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
@@ -435,19 +434,11 @@ export function TemplateDeploy({
Cancel
</Button>
<Button
variant='primary'
variant='destructive'
onClick={handleDelete}
disabled={deleteMutation.isPending}
className='bg-[var(--text-error)] text-[13px] text-white hover:bg-[var(--text-error)]'
>
{deleteMutation.isPending ? (
<>
<Loader2 className='mr-1.5 h-3.5 w-3.5 animate-spin' />
Deleting...
</>
) : (
'Delete'
)}
{deleteMutation.isPending ? 'Deleting...' : 'Delete'}
</Button>
</ModalFooter>
</ModalContent>
@@ -463,7 +454,7 @@ export function TemplateDeploy({
* Hidden container for OG image capture.
* Lazy-rendered only when capturing - gets workflow state from store on mount.
*/
const OGCaptureContainer = React.forwardRef<HTMLDivElement>((_, ref) => {
const OGCaptureContainer = forwardRef<HTMLDivElement>((_, ref) => {
const blocks = useWorkflowStore((state) => state.blocks)
const edges = useWorkflowStore((state) => state.edges)
const loops = useWorkflowStore((state) => state.loops)

View File

@@ -2,8 +2,8 @@
import { useCallback, useEffect, useState } from 'react'
import { createLogger } from '@sim/logger'
import clsx from 'clsx'
import {
Badge,
Button,
Modal,
ModalBody,
@@ -628,7 +628,7 @@ export function DeployModal({
{chatExists && (
<Button
type='button'
variant='default'
variant='destructive'
onClick={handleChatDelete}
disabled={chatSubmitting}
>
@@ -637,7 +637,7 @@ export function DeployModal({
)}
<Button
type='button'
variant='primary'
variant='tertiary'
onClick={handleChatFormSubmit}
disabled={chatSubmitting || !isChatFormValid}
>
@@ -667,7 +667,7 @@ export function DeployModal({
{hasExistingTemplate && (
<Button
type='button'
variant='default'
variant='destructive'
onClick={handleTemplateDelete}
disabled={templateSubmitting}
>
@@ -676,7 +676,7 @@ export function DeployModal({
)}
<Button
type='button'
variant='primary'
variant='tertiary'
onClick={handleTemplateFormSubmit}
disabled={templateSubmitting || !templateFormValid}
>
@@ -698,7 +698,7 @@ export function DeployModal({
<ModalContent size='sm'>
<ModalHeader>Undeploy API</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to undeploy this workflow?{' '}
<span className='text-[var(--text-error)]'>
This will remove the API endpoint and make it unavailable to external users.
@@ -713,12 +713,7 @@ export function DeployModal({
>
Cancel
</Button>
<Button
variant='primary'
onClick={handleUndeploy}
disabled={isUndeploying}
className='bg-[var(--text-error)] text-[13px] text-white hover:bg-[var(--text-error)]'
>
<Button variant='destructive' onClick={handleUndeploy} disabled={isUndeploying}>
{isUndeploying ? 'Undeploying...' : 'Undeploy'}
</Button>
</ModalFooter>
@@ -734,29 +729,10 @@ interface StatusBadgeProps {
function StatusBadge({ isWarning }: StatusBadgeProps) {
const label = isWarning ? 'Update deployment' : 'Live'
return (
<div
className={clsx(
'flex h-[24px] items-center justify-start gap-[8px] rounded-[6px] border px-[9px]',
isWarning ? 'border-[#A16207] bg-[#452C0F]' : 'border-[#22703D] bg-[#14291B]'
)}
>
<div
className='h-[6px] w-[6px] rounded-[2px]'
style={{
backgroundColor: isWarning ? '#EAB308' : '#4ADE80',
}}
/>
<span
className='font-medium text-[11.5px]'
style={{
color: isWarning ? '#EAB308' : '#86EFAC',
}}
>
{label}
</span>
</div>
<Badge variant={isWarning ? 'amber' : 'green'} size='lg' dot>
{label}
</Badge>
)
}
@@ -776,35 +752,10 @@ function TemplateStatusBadge({ status, views, stars }: TemplateStatusBadgeProps)
: null
return (
<div
className={clsx(
'flex h-[24px] items-center justify-start gap-[8px] rounded-[6px] border px-[9px]',
isPending ? 'border-[#A16207] bg-[#452C0F]' : 'border-[#22703D] bg-[#14291B]'
)}
>
<div
className='h-[6px] w-[6px] rounded-[2px]'
style={{
backgroundColor: isPending ? '#EAB308' : '#4ADE80',
}}
/>
<span
className='font-medium text-[11.5px]'
style={{
color: isPending ? '#EAB308' : '#86EFAC',
}}
>
{label}
</span>
{statsText && (
<span
className='font-medium text-[11.5px]'
style={{ color: isPending ? '#EAB308' : '#86EFAC' }}
>
{statsText}
</span>
)}
</div>
<Badge variant={isPending ? 'amber' : 'green'} size='lg' dot>
{label}
{statsText && <span> {statsText}</span>}
</Badge>
)
}
@@ -830,8 +781,8 @@ function GeneralFooter({
if (!isDeployed) {
return (
<ModalFooter>
<Button variant='primary' onClick={onDeploy} disabled={isSubmitting}>
{isSubmitting ? 'Deploying...' : 'Deploy API'}
<Button variant='tertiary' onClick={onDeploy} disabled={isSubmitting}>
{isSubmitting ? 'Deploying...' : 'Deploy'}
</Button>
</ModalFooter>
)
@@ -845,7 +796,7 @@ function GeneralFooter({
{isUndeploying ? 'Undeploying...' : 'Undeploy'}
</Button>
{needsRedeployment && (
<Button variant='primary' onClick={onRedeploy} disabled={isSubmitting || isUndeploying}>
<Button variant='tertiary' onClick={onRedeploy} disabled={isSubmitting || isUndeploying}>
{isSubmitting ? 'Updating...' : 'Update'}
</Button>
)}

View File

@@ -2,7 +2,7 @@
import { useCallback, useState } from 'react'
import { Loader2 } from 'lucide-react'
import { Button, Rocket, Tooltip } from '@/components/emcn'
import { Button, Tooltip } from '@/components/emcn'
import { DeployModal } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/deploy-modal'
import {
useChangeDetection,
@@ -112,17 +112,15 @@ export function Deploy({ activeWorkflowId, userPermissions, className }: DeployP
<Tooltip.Trigger asChild>
<span>
<Button
className='h-[32px] gap-[8px] px-[10px]'
variant='active'
className='h-[30px] gap-[6px] px-[10px]'
variant={
isRegistryLoading ? 'active' : changeDetected || !isDeployed ? 'tertiary' : 'active'
}
onClick={onDeployClick}
disabled={isDisabled}
disabled={isRegistryLoading || isDisabled}
>
{isDeploying ? (
<Loader2 className='h-[13px] w-[13px] animate-spin' />
) : (
<Rocket className='h-[13px] w-[13px]' />
)}
{changeDetected ? 'Update' : isDeployed ? 'Active' : 'Deploy'}
{isDeploying && <Loader2 className='h-[13px] w-[13px] animate-spin' />}
{changeDetected ? 'Update' : isDeployed ? 'Live' : 'Deploy'}
</Button>
</span>
</Tooltip.Trigger>

View File

@@ -29,17 +29,6 @@ interface FieldItemProps {
onToggleExpand?: (path: string) => void
}
/**
* Tree layout constants shared with parent component
*/
export const TREE_SPACING = {
INDENT_PER_LEVEL: 20,
BASE_INDENT: 20,
VERTICAL_LINE_LEFT_OFFSET: 4,
ITEM_GAP: 0,
ITEM_HEIGHT: 25,
} as const
/**
* Individual field item component with drag functionality
*/
@@ -52,8 +41,6 @@ export function FieldItem({
isExpanded,
onToggleExpand,
}: FieldItemProps) {
const indent = TREE_SPACING.BASE_INDENT + level * TREE_SPACING.INDENT_PER_LEVEL
const handleDragStart = useCallback(
(e: React.DragEvent) => {
const normalizedBlockName = connection.name.replace(/\s+/g, '').toLowerCase()
@@ -91,25 +78,26 @@ export function FieldItem({
onDragStart={handleDragStart}
onClick={handleClick}
className={clsx(
'group flex h-[25px] cursor-grab items-center gap-[8px] rounded-[8px] px-[8px] text-[14px] hover:bg-[var(--border)] active:cursor-grabbing',
'group flex h-[26px] cursor-grab items-center gap-[8px] rounded-[8px] px-[6px] text-[14px] hover:bg-[var(--surface-6)] active:cursor-grabbing dark:hover:bg-[var(--surface-5)]',
hasChildren && 'cursor-pointer'
)}
style={{ marginLeft: `${indent}px` }}
>
<span
className={clsx(
'flex-1 truncate font-medium',
'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
'min-w-0 flex-1 truncate font-medium',
'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]'
)}
>
{field.name}
</span>
<Badge className='rounded-[2px] px-[4px] py-[1px] font-mono text-[10px]'>{field.type}</Badge>
<Badge className='flex-shrink-0 rounded-[4px] px-[6px] py-[1px] font-mono text-[11px]'>
{field.type}
</Badge>
{hasChildren && (
<ChevronDown
className={clsx(
'h-3.5 w-3.5 flex-shrink-0 transition-transform',
'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]',
'h-3.5 w-3.5 flex-shrink-0 transition-transform duration-100',
'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]',
isExpanded && 'rotate-180'
)}
/>

View File

@@ -8,7 +8,6 @@ import { useShallow } from 'zustand/react/shallow'
import {
FieldItem,
type SchemaField,
TREE_SPACING,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/connection-blocks/components/field-item/field-item'
import type { ConnectedBlock } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/hooks/use-block-connections'
import { useBlockOutputFields } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-block-output-fields'
@@ -24,43 +23,6 @@ interface ConnectionBlocksProps {
currentBlockId: string
}
const TREE_STYLES = {
LINE_COLOR: 'var(--border)',
LINE_OFFSET: 4,
} as const
/**
* Calculates total height of visible nested fields recursively
*/
const calculateFieldsHeight = (
fields: SchemaField[] | undefined,
parentPath: string,
connectionId: string,
isExpanded: (connectionId: string, path: string) => boolean
): number => {
if (!fields || fields.length === 0) return 0
let totalHeight = 0
fields.forEach((field, index) => {
const fieldPath = parentPath ? `${parentPath}.${field.name}` : field.name
const expanded = isExpanded(connectionId, fieldPath)
totalHeight += TREE_SPACING.ITEM_HEIGHT
if (index < fields.length - 1) {
totalHeight += TREE_SPACING.ITEM_GAP
}
if (expanded && field.children && field.children.length > 0) {
totalHeight += TREE_SPACING.ITEM_GAP
totalHeight += calculateFieldsHeight(field.children, fieldPath, connectionId, isExpanded)
}
})
return totalHeight
}
interface ConnectionItemProps {
connection: ConnectedBlock
isExpanded: boolean
@@ -123,13 +85,13 @@ function ConnectionItem({
draggable
onDragStart={(e) => onConnectionDragStart(e, connection)}
className={clsx(
'group flex h-[25px] cursor-grab items-center gap-[8px] rounded-[8px] px-[5.5px] text-[14px] hover:bg-[var(--border)] active:cursor-grabbing',
'group flex h-[26px] cursor-grab items-center gap-[8px] rounded-[8px] px-[6px] text-[14px] hover:bg-[var(--surface-6)] active:cursor-grabbing dark:hover:bg-[var(--surface-5)]',
hasFields && 'cursor-pointer'
)}
onClick={() => hasFields && onToggleExpand(connection.id)}
>
<div
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
className='relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ background: bgColor }}
>
{Icon && (
@@ -137,7 +99,7 @@ function ConnectionItem({
className={clsx(
'text-white transition-transform duration-200',
hasFields && 'group-hover:scale-110',
'!h-[10px] !w-[10px]'
'!h-[9px] !w-[9px]'
)}
/>
)}
@@ -145,7 +107,7 @@ function ConnectionItem({
<span
className={clsx(
'truncate font-medium',
'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]'
'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]'
)}
>
{connection.name}
@@ -153,8 +115,8 @@ function ConnectionItem({
{hasFields && (
<ChevronDown
className={clsx(
'h-3.5 w-3.5 flex-shrink-0 transition-transform',
'text-[var(--text-tertiary)] group-hover:text-[var(--text-primary)]',
'h-3.5 w-3.5 flex-shrink-0 transition-transform duration-100',
'text-[var(--text-secondary)] group-hover:text-[var(--text-primary)]',
isExpanded && 'rotate-180'
)}
/>
@@ -162,17 +124,8 @@ function ConnectionItem({
</div>
{isExpanded && hasFields && (
<div className='relative'>
<div
className='pointer-events-none absolute'
style={{
left: `${TREE_SPACING.VERTICAL_LINE_LEFT_OFFSET}px`,
top: `${TREE_STYLES.LINE_OFFSET}px`,
width: '1px',
height: `${calculateFieldsHeight(fields, '', connection.id, isFieldExpanded) - TREE_STYLES.LINE_OFFSET * 2}px`,
background: TREE_STYLES.LINE_COLOR,
}}
/>
<div className='relative mt-[2px] ml-[12px] space-y-[2px] pl-[10px]'>
<div className='pointer-events-none absolute top-[4px] bottom-[4px] left-0 w-px bg-[var(--border)]' />
{renderFieldTree(fields, '', 0, connection)}
</div>
)}
@@ -311,18 +264,9 @@ export function ConnectionBlocks({ connections, currentBlockId }: ConnectionBloc
onToggleExpand={(p) => toggleFieldExpansion(connection.id, p)}
/>
{hasChildren && expanded && (
<div className='relative'>
<div
className='pointer-events-none absolute'
style={{
left: `${TREE_SPACING.BASE_INDENT + level * TREE_SPACING.INDENT_PER_LEVEL + TREE_SPACING.VERTICAL_LINE_LEFT_OFFSET}px`,
top: `${TREE_STYLES.LINE_OFFSET}px`,
width: '1px',
height: `${calculateFieldsHeight(field.children, fieldPath, connection.id, isFieldExpanded) - TREE_STYLES.LINE_OFFSET * 2}px`,
background: TREE_STYLES.LINE_COLOR,
}}
/>
<div>{renderFieldTree(field.children!, fieldPath, level + 1, connection)}</div>
<div className='relative mt-[2px] ml-[6px] space-y-[2px] pl-[10px]'>
<div className='pointer-events-none absolute top-[4px] bottom-[4px] left-0 w-px bg-[var(--border)]' />
{renderFieldTree(field.children!, fieldPath, level + 1, connection)}
</div>
)}
</div>

View File

@@ -285,6 +285,7 @@ export function ComboBox({
overlayContent={overlayContent}
inputRef={ref as React.RefObject<HTMLInputElement>}
filterOptions
searchable={config.searchable}
className={cn('allow-scroll overflow-x-auto', selectedOptionIcon && 'pl-[28px]')}
inputProps={{
onDrop: onDrop as (e: React.DragEvent<HTMLInputElement>) => void,

View File

@@ -709,14 +709,14 @@ export function ConditionInput({
{conditionalBlocks.map((block, index) => (
<div
key={block.id}
className='group relative overflow-visible rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]'
className='group relative overflow-visible rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]'
>
<div
className={cn(
'flex items-center justify-between overflow-hidden bg-transparent px-[10px] py-[5px]',
block.title === 'else'
? 'rounded-[4px] border-0'
: 'rounded-t-[4px] border-[var(--border-strong)] border-b'
: 'rounded-t-[4px] border-[var(--border-1)] border-b'
)}
>
<span className='font-medium text-[14px] text-[var(--text-tertiary)]'>

View File

@@ -25,15 +25,15 @@ export interface OAuthRequiredModalProps {
}
const SCOPE_DESCRIPTIONS: Record<string, string> = {
'https://www.googleapis.com/auth/gmail.send': 'Send emails on your behalf',
'https://www.googleapis.com/auth/gmail.labels': 'View and manage your email labels',
'https://www.googleapis.com/auth/gmail.modify': 'View and manage your email messages',
'https://www.googleapis.com/auth/drive.file': 'View and manage your Google Drive files',
'https://www.googleapis.com/auth/drive': 'Full access to all your Google Drive files',
'https://www.googleapis.com/auth/calendar': 'View and manage your calendar',
'https://www.googleapis.com/auth/userinfo.email': 'View your email address',
'https://www.googleapis.com/auth/userinfo.profile': 'View your basic profile info',
'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to your Google Forms',
'https://www.googleapis.com/auth/gmail.send': 'Send emails',
'https://www.googleapis.com/auth/gmail.labels': 'View and manage email labels',
'https://www.googleapis.com/auth/gmail.modify': 'View and manage email messages',
'https://www.googleapis.com/auth/drive.file': 'View and manage Google Drive files',
'https://www.googleapis.com/auth/drive': 'Access all Google Drive files',
'https://www.googleapis.com/auth/calendar': 'View and manage calendar',
'https://www.googleapis.com/auth/userinfo.email': 'View email address',
'https://www.googleapis.com/auth/userinfo.profile': 'View basic profile info',
'https://www.googleapis.com/auth/forms.responses.readonly': 'View responses to Google Forms',
'https://www.googleapis.com/auth/ediscovery': 'Access Google Vault for eDiscovery',
'https://www.googleapis.com/auth/devstorage.read_only': 'Read files from Google Cloud Storage',
'https://www.googleapis.com/auth/admin.directory.group': 'Manage Google Workspace groups',
@@ -65,50 +65,50 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'write:label:confluence': 'Add and remove labels',
'search:confluence': 'Search Confluence content',
'readonly:content.attachment:confluence': 'View attachments',
'read:me': 'Read your profile information',
'database.read': 'Read your database',
'database.write': 'Write to your database',
'projects.read': 'Read your projects',
offline_access: 'Access your account when you are not using the application',
repo: 'Access your repositories',
'read:me': 'Read profile information',
'database.read': 'Read database',
'database.write': 'Write to database',
'projects.read': 'Read projects',
offline_access: 'Access account when not using the application',
repo: 'Access repositories',
workflow: 'Manage repository workflows',
'read:user': 'Read your public user information',
'user:email': 'Access your email address',
'tweet.read': 'Read your tweets and timeline',
'tweet.write': 'Post tweets on your behalf',
'users.read': 'Read your profile information',
'offline.access': 'Access your account when you are not using the application',
'data.records:read': 'Read your records',
'data.records:write': 'Write to your records',
'webhook:manage': 'Manage your webhooks',
'page.read': 'Read your Notion pages',
'page.write': 'Write to your Notion pages',
'workspace.content': 'Read your Notion content',
'workspace.name': 'Read your Notion workspace name',
'workspace.read': 'Read your Notion workspace',
'workspace.write': 'Write to your Notion workspace',
'user.email:read': 'Read your email address',
'read:jira-user': 'Read your Jira user',
'read:jira-work': 'Read your Jira work',
'write:jira-work': 'Write to your Jira work',
'read:user': 'Read public user information',
'user:email': 'Access email address',
'tweet.read': 'Read tweets and timeline',
'tweet.write': 'Post tweets',
'users.read': 'Read profile information',
'offline.access': 'Access account when not using the application',
'data.records:read': 'Read records',
'data.records:write': 'Write to records',
'webhook:manage': 'Manage webhooks',
'page.read': 'Read Notion pages',
'page.write': 'Write to Notion pages',
'workspace.content': 'Read Notion content',
'workspace.name': 'Read Notion workspace name',
'workspace.read': 'Read Notion workspace',
'workspace.write': 'Write to Notion workspace',
'user.email:read': 'Read email address',
'read:jira-user': 'Read Jira user',
'read:jira-work': 'Read Jira work',
'write:jira-work': 'Write to Jira work',
'manage:jira-webhook': 'Register and manage Jira webhooks',
'read:webhook:jira': 'View Jira webhooks',
'write:webhook:jira': 'Create and update Jira webhooks',
'delete:webhook:jira': 'Delete Jira webhooks',
'read:issue-event:jira': 'Read your Jira issue events',
'write:issue:jira': 'Write to your Jira issues',
'read:project:jira': 'Read your Jira projects',
'read:issue-type:jira': 'Read your Jira issue types',
'read:issue-meta:jira': 'Read your Jira issue meta',
'read:issue-security-level:jira': 'Read your Jira issue security level',
'read:issue.vote:jira': 'Read your Jira issue votes',
'read:issue.changelog:jira': 'Read your Jira issue changelog',
'read:avatar:jira': 'Read your Jira avatar',
'read:issue:jira': 'Read your Jira issues',
'read:status:jira': 'Read your Jira status',
'read:user:jira': 'Read your Jira user',
'read:field-configuration:jira': 'Read your Jira field configuration',
'read:issue-details:jira': 'Read your Jira issue details',
'read:issue-event:jira': 'Read Jira issue events',
'write:issue:jira': 'Write to Jira issues',
'read:project:jira': 'Read Jira projects',
'read:issue-type:jira': 'Read Jira issue types',
'read:issue-meta:jira': 'Read Jira issue meta',
'read:issue-security-level:jira': 'Read Jira issue security level',
'read:issue.vote:jira': 'Read Jira issue votes',
'read:issue.changelog:jira': 'Read Jira issue changelog',
'read:avatar:jira': 'Read Jira avatar',
'read:issue:jira': 'Read Jira issues',
'read:status:jira': 'Read Jira status',
'read:user:jira': 'Read Jira user',
'read:field-configuration:jira': 'Read Jira field configuration',
'read:issue-details:jira': 'Read Jira issue details',
'read:field:jira': 'Read Jira field configurations',
'read:jql:jira': 'Use JQL to filter Jira issues',
'read:comment.property:jira': 'Read Jira comment properties',
@@ -124,57 +124,57 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'delete:issue-worklog:jira': 'Delete worklog entries from Jira issues',
'write:issue-link:jira': 'Create links between Jira issues',
'delete:issue-link:jira': 'Delete links between Jira issues',
'User.Read': 'Read your Microsoft user',
'Chat.Read': 'Read your Microsoft chats',
'Chat.ReadWrite': 'Write to your Microsoft chats',
'Chat.ReadBasic': 'Read your Microsoft chats',
'ChatMessage.Send': 'Send chat messages on your behalf',
'Channel.ReadBasic.All': 'Read your Microsoft channels',
'ChannelMessage.Send': 'Write to your Microsoft channels',
'ChannelMessage.Read.All': 'Read your Microsoft channels',
'ChannelMessage.ReadWrite': 'Read and write to your Microsoft channels',
'User.Read': 'Read Microsoft user',
'Chat.Read': 'Read Microsoft chats',
'Chat.ReadWrite': 'Write to Microsoft chats',
'Chat.ReadBasic': 'Read Microsoft chats',
'ChatMessage.Send': 'Send chat messages',
'Channel.ReadBasic.All': 'Read Microsoft channels',
'ChannelMessage.Send': 'Write to Microsoft channels',
'ChannelMessage.Read.All': 'Read Microsoft channels',
'ChannelMessage.ReadWrite': 'Read and write to Microsoft channels',
'ChannelMember.Read.All': 'Read team channel members',
'Group.Read.All': 'Read your Microsoft groups',
'Group.ReadWrite.All': 'Write to your Microsoft groups',
'Team.ReadBasic.All': 'Read your Microsoft teams',
'Group.Read.All': 'Read Microsoft groups',
'Group.ReadWrite.All': 'Write to Microsoft groups',
'Team.ReadBasic.All': 'Read Microsoft teams',
'TeamMember.Read.All': 'Read team members',
'Mail.ReadWrite': 'Write to your Microsoft emails',
'Mail.ReadBasic': 'Read your Microsoft emails',
'Mail.Read': 'Read your Microsoft emails',
'Mail.Send': 'Send emails on your behalf',
'Files.Read': 'Read your OneDrive files',
'Files.ReadWrite': 'Read and write your OneDrive files',
'Tasks.ReadWrite': 'Read and manage your Planner tasks',
'Mail.ReadWrite': 'Write to Microsoft emails',
'Mail.ReadBasic': 'Read Microsoft emails',
'Mail.Read': 'Read Microsoft emails',
'Mail.Send': 'Send emails',
'Files.Read': 'Read OneDrive files',
'Files.ReadWrite': 'Read and write OneDrive files',
'Tasks.ReadWrite': 'Read and manage Planner tasks',
'Sites.Read.All': 'Read Sharepoint sites',
'Sites.ReadWrite.All': 'Read and write Sharepoint sites',
'Sites.Manage.All': 'Manage Sharepoint sites',
openid: 'Standard authentication',
profile: 'Access your profile information',
email: 'Access your email address',
identify: 'Read your Discord user',
bot: 'Read your Discord bot',
'messages.read': 'Read your Discord messages',
guilds: 'Read your Discord guilds',
'guilds.members.read': 'Read your Discord guild members',
identity: 'Access your Reddit identity',
submit: 'Submit posts and comments on your behalf',
profile: 'Access profile information',
email: 'Access email address',
identify: 'Read Discord user',
bot: 'Read Discord bot',
'messages.read': 'Read Discord messages',
guilds: 'Read Discord guilds',
'guilds.members.read': 'Read Discord guild members',
identity: 'Access Reddit identity',
submit: 'Submit posts and comments',
vote: 'Vote on posts and comments',
save: 'Save and unsave posts and comments',
edit: 'Edit your posts and comments',
edit: 'Edit posts and comments',
subscribe: 'Subscribe and unsubscribe from subreddits',
history: 'Access your Reddit history',
privatemessages: 'Access your inbox and send private messages',
account: 'Update your account preferences and settings',
mysubreddits: 'Access your subscribed and moderated subreddits',
history: 'Access Reddit history',
privatemessages: 'Access inbox and send private messages',
account: 'Update account preferences and settings',
mysubreddits: 'Access subscribed and moderated subreddits',
flair: 'Manage user and post flair',
report: 'Report posts and comments for rule violations',
modposts: 'Approve, remove, and moderate posts in subreddits you moderate',
modflair: 'Manage flair in subreddits you moderate',
modposts: 'Approve, remove, and moderate posts in moderated subreddits',
modflair: 'Manage flair in moderated subreddits',
modmail: 'Access and respond to moderator mail',
login: 'Access your Wealthbox account',
data: 'Access your Wealthbox data',
read: 'Read access to your workspace',
write: 'Write access to your Linear workspace',
login: 'Access Wealthbox account',
data: 'Access Wealthbox data',
read: 'Read access to workspace',
write: 'Write access to Linear workspace',
'channels:read': 'View public channels',
'channels:history': 'Read channel messages',
'groups:read': 'View private channels',
@@ -189,15 +189,15 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'files:read': 'Download and read files',
'canvases:write': 'Create canvas documents',
'reactions:write': 'Add emoji reactions to messages',
'sites:read': 'View your Webflow sites',
'sites:read': 'View Webflow sites',
'sites:write': 'Manage webhooks and site settings',
'cms:read': 'View your CMS content',
'cms:write': 'Manage your CMS content',
'crm.objects.contacts.read': 'Read your HubSpot contacts',
'cms:read': 'View CMS content',
'cms:write': 'Manage CMS content',
'crm.objects.contacts.read': 'Read HubSpot contacts',
'crm.objects.contacts.write': 'Create and update HubSpot contacts',
'crm.objects.companies.read': 'Read your HubSpot companies',
'crm.objects.companies.read': 'Read HubSpot companies',
'crm.objects.companies.write': 'Create and update HubSpot companies',
'crm.objects.deals.read': 'Read your HubSpot deals',
'crm.objects.deals.read': 'Read HubSpot deals',
'crm.objects.deals.write': 'Create and update HubSpot deals',
'crm.objects.owners.read': 'Read HubSpot object owners',
'crm.objects.users.read': 'Read HubSpot users',
@@ -217,74 +217,74 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'crm.lists.write': 'Create and update HubSpot lists',
tickets: 'Manage HubSpot tickets',
api: 'Access Salesforce API',
refresh_token: 'Maintain long-term access to your Salesforce account',
default: 'Access your Asana workspace',
base: 'Basic access to your Pipedrive account',
'deals:read': 'Read your Pipedrive deals',
'deals:full': 'Full access to manage your Pipedrive deals',
'contacts:read': 'Read your Pipedrive contacts',
'contacts:full': 'Full access to manage your Pipedrive contacts',
'leads:read': 'Read your Pipedrive leads',
'leads:full': 'Full access to manage your Pipedrive leads',
'activities:read': 'Read your Pipedrive activities',
'activities:full': 'Full access to manage your Pipedrive activities',
'mail:read': 'Read your Pipedrive emails',
'mail:full': 'Full access to manage your Pipedrive emails',
'projects:read': 'Read your Pipedrive projects',
'projects:full': 'Full access to manage your Pipedrive projects',
'webhooks:read': 'Read your Pipedrive webhooks',
'webhooks:full': 'Full access to manage your Pipedrive webhooks',
w_member_social: 'Access your LinkedIn profile',
refresh_token: 'Maintain long-term access to Salesforce account',
default: 'Access Asana workspace',
base: 'Basic access to Pipedrive account',
'deals:read': 'Read Pipedrive deals',
'deals:full': 'Full access to manage Pipedrive deals',
'contacts:read': 'Read Pipedrive contacts',
'contacts:full': 'Full access to manage Pipedrive contacts',
'leads:read': 'Read Pipedrive leads',
'leads:full': 'Full access to manage Pipedrive leads',
'activities:read': 'Read Pipedrive activities',
'activities:full': 'Full access to manage Pipedrive activities',
'mail:read': 'Read Pipedrive emails',
'mail:full': 'Full access to manage Pipedrive emails',
'projects:read': 'Read Pipedrive projects',
'projects:full': 'Full access to manage Pipedrive projects',
'webhooks:read': 'Read Pipedrive webhooks',
'webhooks:full': 'Full access to manage Pipedrive webhooks',
w_member_social: 'Access LinkedIn profile',
// Box scopes
root_readwrite: 'Read and write all files and folders in your Box account',
root_readonly: 'Read all files and folders in your Box account',
root_readwrite: 'Read and write all files and folders in Box account',
root_readonly: 'Read all files and folders in Box account',
// Shopify scopes (write_* implicitly includes read access)
write_products: 'Read and manage your Shopify products',
write_orders: 'Read and manage your Shopify orders',
write_customers: 'Read and manage your Shopify customers',
write_inventory: 'Read and manage your Shopify inventory levels',
read_locations: 'View your store locations',
write_products: 'Read and manage Shopify products',
write_orders: 'Read and manage Shopify orders',
write_customers: 'Read and manage Shopify customers',
write_inventory: 'Read and manage Shopify inventory levels',
read_locations: 'View store locations',
write_merchant_managed_fulfillment_orders: 'Create fulfillments for orders',
// Zoom scopes
'user:read:user': 'View your Zoom profile information',
'user:read:user': 'View Zoom profile information',
'meeting:write:meeting': 'Create Zoom meetings',
'meeting:read:meeting': 'View Zoom meeting details',
'meeting:read:list_meetings': 'List your Zoom meetings',
'meeting:read:list_meetings': 'List Zoom meetings',
'meeting:update:meeting': 'Update Zoom meetings',
'meeting:delete:meeting': 'Delete Zoom meetings',
'meeting:read:invitation': 'View Zoom meeting invitations',
'meeting:read:list_past_participants': 'View past meeting participants',
'cloud_recording:read:list_user_recordings': 'List your Zoom cloud recordings',
'cloud_recording:read:list_user_recordings': 'List Zoom cloud recordings',
'cloud_recording:read:list_recording_files': 'View recording files',
'cloud_recording:delete:recording_file': 'Delete cloud recordings',
// Dropbox scopes
'account_info.read': 'View your Dropbox account information',
'account_info.read': 'View Dropbox account information',
'files.metadata.read': 'View file and folder names, sizes, and dates',
'files.metadata.write': 'Modify file and folder metadata',
'files.content.read': 'Download and read your Dropbox files',
'files.content.write': 'Upload, copy, move, and delete files in your Dropbox',
'sharing.read': 'View your shared files and folders',
'files.content.read': 'Download and read Dropbox files',
'files.content.write': 'Upload, copy, move, and delete files in Dropbox',
'sharing.read': 'View shared files and folders',
'sharing.write': 'Share files and folders with others',
// WordPress.com scopes
global: 'Full access to manage your WordPress.com sites, posts, pages, media, and settings',
global: 'Full access to manage WordPress.com sites, posts, pages, media, and settings',
// Spotify scopes
'user-read-private': 'View your Spotify account details',
'user-read-email': 'View your email address on Spotify',
'user-library-read': 'View your saved tracks and albums',
'user-library-modify': 'Save and remove tracks and albums from your library',
'playlist-read-private': 'View your private playlists',
'playlist-read-collaborative': 'View collaborative playlists you have access to',
'playlist-modify-public': 'Create and manage your public playlists',
'playlist-modify-private': 'Create and manage your private playlists',
'user-read-playback-state': 'View your current playback state',
'user-modify-playback-state': 'Control playback on your Spotify devices',
'user-read-currently-playing': 'View your currently playing track',
'user-read-recently-played': 'View your recently played tracks',
'user-top-read': 'View your top artists and tracks',
'user-follow-read': 'View artists and users you follow',
'user-read-private': 'View Spotify account details',
'user-read-email': 'View email address on Spotify',
'user-library-read': 'View saved tracks and albums',
'user-library-modify': 'Save and remove tracks and albums from library',
'playlist-read-private': 'View private playlists',
'playlist-read-collaborative': 'View collaborative playlists',
'playlist-modify-public': 'Create and manage public playlists',
'playlist-modify-private': 'Create and manage private playlists',
'user-read-playback-state': 'View current playback state',
'user-modify-playback-state': 'Control playback on Spotify devices',
'user-read-currently-playing': 'View currently playing track',
'user-read-recently-played': 'View recently played tracks',
'user-top-read': 'View top artists and tracks',
'user-follow-read': 'View followed artists and users',
'user-follow-modify': 'Follow and unfollow artists and users',
'user-read-playback-position': 'View your playback position in podcasts',
'ugc-image-upload': 'Upload images to your Spotify playlists',
'user-read-playback-position': 'View playback position in podcasts',
'ugc-image-upload': 'Upload images to Spotify playlists',
}
function getScopeDescription(scope: string): string {
@@ -378,7 +378,7 @@ export function OAuthRequiredModal({
<ModalBody>
<div className='flex flex-col gap-[16px]'>
<div className='flex items-center gap-[14px]'>
<div className='flex h-[40px] w-[40px] flex-shrink-0 items-center justify-center rounded-[8px] bg-[var(--surface-6)]'>
<div className='flex h-[40px] w-[40px] flex-shrink-0 items-center justify-center rounded-[8px] bg-[var(--surface-5)]'>
<ProviderIcon className='h-[18px] w-[18px]' />
</div>
<div className='flex-1'>
@@ -392,22 +392,22 @@ export function OAuthRequiredModal({
</div>
{displayScopes.length > 0 && (
<div className='rounded-[8px] border bg-[var(--surface-6)]'>
<div className='border-b px-[14px] py-[10px]'>
<h4 className='font-medium text-[13px] text-[var(--text-primary)]'>
<div className='rounded-[8px] border border-[var(--border-1)] bg-[var(--surface-5)]'>
<div className='border-[var(--border-1)] border-b px-[14px] py-[10px]'>
<h4 className='font-medium text-[12px] text-[var(--text-primary)]'>
Permissions requested
</h4>
</div>
<ul className='max-h-[330px] space-y-[10px] overflow-y-auto px-[14px] py-[12px]'>
{displayScopes.map((scope) => (
<li key={scope} className='flex items-start gap-[10px]'>
<div className='mt-[3px] flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
<div className='mt-[2px] flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center'>
<Check className='h-[10px] w-[10px] text-[var(--text-primary)]' />
</div>
<div className='flex-1 text-[12px] text-[var(--text-primary)]'>
<div className='flex flex-1 items-center gap-[8px] text-[12px] text-[var(--text-primary)]'>
<span>{getScopeDescription(scope)}</span>
{newScopesSet.has(scope) && (
<span className='ml-[8px] rounded-[4px] border border-amber-500/30 bg-amber-500/10 px-[6px] py-[2px] text-[10px] text-amber-300'>
<span className='inline-flex items-center gap-[6px] rounded-[6px] bg-[rgba(245,158,11,0.2)] px-[7px] py-[1px] font-medium text-[#fcd34d] text-[11px]'>
New
</span>
)}
@@ -423,12 +423,7 @@ export function OAuthRequiredModal({
<Button variant='active' onClick={onClose}>
Cancel
</Button>
<Button
variant='primary'
type='button'
onClick={handleConnectDirectly}
className='!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
>
<Button variant='tertiary' type='button' onClick={handleConnectDirectly}>
Connect
</Button>
</ModalFooter>

View File

@@ -127,7 +127,7 @@ export function EvalInput({
}
const renderMetricHeader = (metric: EvalMetric, index: number) => (
<div className='flex items-center justify-between overflow-hidden rounded-t-[4px] border-[var(--border-strong)] border-b bg-transparent px-[10px] py-[5px]'>
<div className='flex items-center justify-between overflow-hidden rounded-t-[4px] border-[var(--border-1)] border-b bg-transparent px-[10px] py-[5px]'>
<span className='font-medium text-[14px] text-[var(--text-tertiary)]'>
Metric {index + 1}
</span>
@@ -171,11 +171,11 @@ export function EvalInput({
<div
key={metric.id}
data-metric-id={metric.id}
className='group relative overflow-visible rounded-[4px] border border-[var(--border-strong)] bg-[#1F1F1F]'
className='group relative overflow-visible rounded-[4px] border border-[var(--border-1)] bg-[#1F1F1F]'
>
{renderMetricHeader(metric, index)}
<div className='flex flex-col gap-[6px] border-[var(--border-strong)] px-[10px] pt-[6px] pb-[10px]'>
<div className='flex flex-col gap-[6px] border-[var(--border-1)] px-[10px] pt-[6px] pb-[10px]'>
<div key={`name-${metric.id}`} className='space-y-[4px]'>
<Label className='text-[13px]'>Name</Label>
<Input

View File

@@ -420,7 +420,7 @@ export function FileUpload({
return (
<div
key={fileKey}
className='flex items-center justify-between rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] hover:border-[var(--surface-14)] hover:bg-[var(--surface-9)] dark:bg-[var(--surface-9)] dark:hover:bg-[var(--surface-11)]'
className='flex items-center justify-between rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] hover:border-[var(--surface-7)] hover:bg-[var(--surface-5)] dark:bg-[var(--surface-5)] dark:hover:bg-[var(--border-1)]'
>
<div className='flex-1 truncate pr-2 text-sm' title={file.name}>
<span className='text-[var(--text-primary)]'>{truncateMiddle(file.name)}</span>
@@ -447,7 +447,7 @@ export function FileUpload({
return (
<div
key={file.id}
className='flex items-center justify-between rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] dark:bg-[var(--surface-9)]'
className='flex items-center justify-between rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] dark:bg-[var(--surface-5)]'
>
<div className='flex-1 truncate pr-2 text-sm'>
<span className='text-[var(--text-primary)]'>{file.name}</span>

View File

@@ -107,8 +107,8 @@ export function GroupedCheckboxList({
variant='ghost'
disabled={disabled}
className={cn(
'flex w-full cursor-pointer items-center justify-between rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] px-[8px] py-[6px] font-medium font-sans text-[var(--text-primary)] text-sm outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-[var(--surface-9)]',
'hover:border-[var(--surface-14)] hover:bg-[var(--surface-9)] dark:hover:border-[var(--surface-13)] dark:hover:bg-[var(--surface-11)]'
'flex w-full cursor-pointer items-center justify-between rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] px-[8px] py-[6px] font-medium font-sans text-[var(--text-primary)] text-sm outline-none focus:outline-none focus-visible:outline-none focus-visible:ring-0 focus-visible:ring-offset-0 dark:bg-[var(--surface-5)]',
'hover:border-[var(--surface-7)] hover:bg-[var(--surface-5)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]'
)}
>
<span className='flex flex-1 items-center gap-2 truncate text-[var(--text-muted)]'>

View File

@@ -256,7 +256,7 @@ export function InputMapping({
if (!selectedWorkflowId) {
return (
<div className='flex flex-col items-center justify-center rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-3)] p-8 text-center dark:bg-[#1F1F1F]'>
<div className='flex flex-col items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-3)] p-8 text-center dark:bg-[#1F1F1F]'>
<svg
className='mb-3 h-10 w-10 text-[var(--text-tertiary)]'
fill='none'
@@ -368,7 +368,7 @@ function InputMappingField({
return (
<div
className={cn(
'rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]',
'rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]',
collapsed ? 'overflow-hidden' : 'overflow-visible'
)}
>
@@ -385,7 +385,7 @@ function InputMappingField({
</div>
{!collapsed && (
<div className='flex flex-col gap-[6px] border-[var(--border-strong)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='flex flex-col gap-[6px] border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='space-y-[4px]'>
<Label className='text-[13px]'>Value</Label>
<div className='relative'>

View File

@@ -369,7 +369,7 @@ export function LongInput({
{/* Custom resize handle */}
{!wandHook.isStreaming && (
<div
className='absolute right-1 bottom-1 flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] dark:bg-[var(--surface-9)]'
className='absolute right-1 bottom-1 flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] dark:bg-[var(--surface-5)]'
onMouseDown={startResize}
onDragStart={(e) => {
e.preventDefault()

View File

@@ -313,7 +313,7 @@ export function MessagesInput({
<div
key={`message-${index}`}
className={cn(
'relative flex w-full flex-col rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] transition-colors dark:bg-[var(--surface-9)]',
'relative flex w-full flex-col rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] transition-colors dark:bg-[var(--surface-5)]',
disabled && 'opacity-50'
)}
>
@@ -364,7 +364,7 @@ export function MessagesInput({
type='button'
disabled={isPreview || disabled}
className={cn(
'-ml-1.5 -my-1 rounded px-1.5 py-1 font-medium text-[13px] text-[var(--text-primary)] leading-none transition-colors hover:bg-[var(--surface-8)] hover:text-[var(--text-secondary)]',
'-ml-1.5 -my-1 rounded px-1.5 py-1 font-medium text-[13px] text-[var(--text-primary)] leading-none transition-colors hover:bg-[var(--surface-5)] hover:text-[var(--text-secondary)]',
(isPreview || disabled) &&
'cursor-default hover:bg-transparent hover:text-[var(--text-primary)]'
)}
@@ -534,7 +534,7 @@ export function MessagesInput({
{!isPreview && !disabled && (
<div
className='absolute right-1 bottom-1 flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--surface-11)] bg-[var(--surface-6)] dark:bg-[var(--surface-9)]'
className='absolute right-1 bottom-1 flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] dark:bg-[var(--surface-5)]'
onMouseDown={(e) => handleResizeStart(fieldId, e)}
onDragStart={(e) => {
e.preventDefault()

View File

@@ -1,5 +1,5 @@
import { useEffect } from 'react'
import { Slider } from '@/components/ui/slider'
import { Slider } from '@/components/emcn/components/slider/slider'
import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/hooks/use-sub-block-value'
interface SliderInputProps {
@@ -67,7 +67,6 @@ export function SliderInput({
step={integer ? 1 : step}
onValueChange={handleValueChange}
disabled={isPreview || disabled}
className='[&_[class*=SliderTrack]]:h-1 [&_[role=slider]]:h-4 [&_[role=slider]]:w-4 [&_[role=slider]]:cursor-pointer'
/>
<div
className='absolute top-6 text-muted-foreground text-sm'

View File

@@ -407,14 +407,14 @@ export function FieldFormat({
key={field.id}
data-field-id={field.id}
className={cn(
'rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]',
'rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]',
field.collapsed ? 'overflow-hidden' : 'overflow-visible'
)}
>
{renderFieldHeader(field, index)}
{!field.collapsed && (
<div className='flex flex-col gap-[6px] border-[var(--border-strong)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='flex flex-col gap-[6px] border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='flex flex-col gap-[4px]'>
<Label className='text-[13px]'>Name</Label>
<Input

View File

@@ -148,13 +148,13 @@ export function Table({
const renderHeader = () => (
<thead className='bg-transparent'>
<tr className='border-[var(--border-strong)] border-b bg-transparent'>
<tr className='border-[var(--border-1)] border-b bg-transparent'>
{columns.map((column, index) => (
<th
key={column}
className={cn(
'bg-transparent px-[10px] py-[5px] text-left font-medium text-[14px] text-[var(--text-tertiary)]',
index < columns.length - 1 && 'border-[var(--border-strong)] border-r'
index < columns.length - 1 && 'border-[var(--border-1)] border-r'
)}
>
{column}
@@ -223,7 +223,7 @@ export function Table({
key={`${row.id}-${column}`}
className={cn(
'relative bg-transparent p-0',
cellIndex < columns.length - 1 && 'border-[var(--border-strong)] border-r'
cellIndex < columns.length - 1 && 'border-[var(--border-1)] border-r'
)}
>
<div className='relative w-full'>
@@ -310,14 +310,14 @@ export function Table({
return (
<div className='relative'>
<div className='overflow-visible rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-2)] dark:bg-[#1F1F1F]'>
<div className='overflow-visible rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-2)] dark:bg-[#1F1F1F]'>
<table className='w-full bg-transparent'>
{renderHeader()}
<tbody className='bg-transparent'>
{rows.map((row, rowIndex) => (
<tr
key={row.id}
className='group relative border-[var(--border-strong)] border-t bg-transparent'
className='group relative border-[var(--border-1)] border-t bg-transparent'
>
{columns.map((column, cellIndex) => renderCell(row, rowIndex, column, cellIndex))}
{renderDeleteButton(rowIndex)}

View File

@@ -68,10 +68,15 @@ interface TagDropdownProps {
}
/**
* Checks if the tag trigger (<) should show the tag dropdown
* @param text - The full text content
* @param cursorPosition - Current cursor position
* @returns Object indicating whether to show the dropdown
* Checks if the tag trigger (`<`) should show the tag dropdown.
*
* @remarks
* The dropdown appears when there's an unclosed `<` bracket before the cursor.
* A closing `>` bracket after the last `<` will prevent the dropdown from showing.
*
* @param text - The full text content of the input field
* @param cursorPosition - Current cursor position in the text
* @returns Object with `show` property indicating whether to display the dropdown
*/
export const checkTagTrigger = (text: string, cursorPosition: number): { show: boolean } => {
if (cursorPosition >= 1) {
@@ -86,6 +91,17 @@ export const checkTagTrigger = (text: string, cursorPosition: number): { show: b
return { show: false }
}
/**
* Extracts the search term for tag filtering from the current input.
*
* @remarks
* Returns the text between the last unclosed `<` and the cursor position,
* converted to lowercase for case-insensitive matching.
*
* @param text - The full text content of the input field
* @param cursorPosition - Current cursor position in the text
* @returns The search term for filtering tags, or empty string if not in tag context
*/
export const getTagSearchTerm = (text: string, cursorPosition: number): string => {
if (cursorPosition <= 0) {
return ''
@@ -107,6 +123,9 @@ export const getTagSearchTerm = (text: string, cursorPosition: number): string =
return textBeforeCursor.slice(lastOpenBracket + 1).toLowerCase()
}
/**
* Color constants for block type icons in the tag dropdown.
*/
const BLOCK_COLORS = {
VARIABLE: '#2F8BFF',
DEFAULT: '#2F55FF',
@@ -114,6 +133,9 @@ const BLOCK_COLORS = {
PARALLEL: '#FEE12B',
} as const
/**
* Prefix constants for special tag types.
*/
const TAG_PREFIXES = {
VARIABLE: 'variable.',
} as const
@@ -128,14 +150,29 @@ const ensureRootTag = (tags: string[], rootTag: string): string[] => {
}
/**
* Gets a subblock value from the store
* Gets a subblock value from the store.
*
* @param blockId - The block identifier
* @param property - The property name to retrieve
* @returns The value from the subblock store
*/
const getSubBlockValue = (blockId: string, property: string): any => {
return useSubBlockStore.getState().getValue(blockId, property)
}
/**
* Gets the output type for a specific path in a block's outputs
* Gets the output type for a specific path in a block's outputs.
*
* @remarks
* Handles special cases for trigger blocks, starter blocks with chat mode,
* and tool-based operations.
*
* @param block - The block state
* @param blockConfig - The block configuration, or null
* @param blockId - The block identifier
* @param outputPath - The dot-separated path to the output field
* @param mergedSubBlocksOverride - Optional override for subblock values
* @returns The type of the output field (e.g., 'string', 'array', 'any')
*/
const getOutputTypeForPath = (
block: BlockState,
@@ -181,7 +218,15 @@ const getOutputTypeForPath = (
}
/**
* Recursively generates all output paths from an outputs schema
* Recursively generates all output paths from an outputs schema.
*
* @remarks
* Traverses nested objects and arrays to build dot-separated paths
* for all leaf values in the schema.
*
* @param outputs - The outputs schema object
* @param prefix - Current path prefix for recursion
* @returns Array of dot-separated paths to all output fields
*/
const generateOutputPaths = (outputs: Record<string, any>, prefix = ''): string[] => {
const paths: string[] = []
@@ -230,7 +275,15 @@ const generateOutputPaths = (outputs: Record<string, any>, prefix = ''): string[
}
/**
* Recursively generates all output paths with their types from an outputs schema
* Recursively generates all output paths with their types from an outputs schema.
*
* @remarks
* Similar to generateOutputPaths but also captures the type information
* for each path, useful for displaying type hints in the UI.
*
* @param outputs - The outputs schema object
* @param prefix - Current path prefix for recursion
* @returns Array of objects containing path and type for each output field
*/
const generateOutputPathsWithTypes = (
outputs: Record<string, any>,
@@ -269,7 +322,11 @@ const generateOutputPathsWithTypes = (
}
/**
* Generates output paths for a tool-based block
* Generates output paths for a tool-based block.
*
* @param blockConfig - The block configuration containing tools config
* @param operation - The selected operation for the tool
* @returns Array of output paths for the tool, or empty array on error
*/
const generateToolOutputPaths = (blockConfig: BlockConfig, operation: string): string[] => {
if (!blockConfig?.tools?.config?.tool) return []
@@ -289,7 +346,12 @@ const generateToolOutputPaths = (blockConfig: BlockConfig, operation: string): s
}
/**
* Gets the output type for a specific path in a tool's outputs
* Gets the output type for a specific path in a tool's outputs.
*
* @param blockConfig - The block configuration containing tools config
* @param operation - The selected operation for the tool
* @param path - The dot-separated path to the output field
* @returns The type of the output field, or 'any' if not found
*/
const getToolOutputType = (blockConfig: BlockConfig, operation: string, path: string): string => {
if (!blockConfig?.tools?.config?.tool) return 'any'
@@ -311,7 +373,16 @@ const getToolOutputType = (blockConfig: BlockConfig, operation: string, path: st
}
/**
* Calculates the viewport position of the caret in a textarea/input
* Calculates the viewport position of the caret in a textarea/input.
*
* @remarks
* Creates a hidden mirror div with identical styling to measure the
* precise position of the caret for popover anchoring.
*
* @param element - The textarea or input element
* @param caretPosition - The character position of the caret
* @param text - The text content of the element
* @returns Object with `left` and `top` viewport coordinates
*/
const getCaretViewportPosition = (
element: HTMLTextAreaElement | HTMLInputElement,
@@ -361,7 +432,15 @@ const getCaretViewportPosition = (
}
/**
* Renders a tag icon with background color - can use either a React icon component or a letter
* Renders a tag icon with background color.
*
* @remarks
* Supports either a React icon component or a single letter string
* for flexible icon display in the tag dropdown.
*
* @param icon - Either a letter string or a Lucide icon component
* @param color - Background color for the icon container
* @returns A styled icon element
*/
const TagIcon: React.FC<{
icon: string | React.ComponentType<{ className?: string }>
@@ -372,18 +451,29 @@ const TagIcon: React.FC<{
style={{ background: color }}
>
{typeof icon === 'string' ? (
<span className='font-bold text-[10px] text-white'>{icon}</span>
<span className='!text-white font-bold text-[10px]'>{icon}</span>
) : (
(() => {
const IconComponent = icon
return <IconComponent className='h-[9px] w-[9px] text-white' />
return <IconComponent className='!text-white size-[9px]' />
})()
)}
</div>
)
/**
* Wrapper for PopoverBackButton that handles parent tag navigation
* Wrapper for PopoverBackButton that handles parent tag navigation.
*
* @remarks
* Extends the base PopoverBackButton to support selecting the parent tag
* when navigating back from a nested folder view.
*
* @param selectedIndex - Currently selected item index
* @param setSelectedIndex - Callback to update selection
* @param flatTagList - Flat list of all available tags
* @param nestedBlockTagGroups - Groups of nested block tags
* @param itemRefs - Refs to item DOM elements for scrolling
* @returns The back button component with parent tag support
*/
const TagDropdownBackButton: React.FC<{
selectedIndex: number
@@ -432,8 +522,26 @@ const TagDropdownBackButton: React.FC<{
}
/**
* TagDropdown component that displays available tags (variables and block outputs)
* for selection in input fields. Uses the Popover component system for consistent styling.
* TagDropdown component that displays available tags for selection in input fields.
*
* @remarks
* Displays variables and block outputs that can be referenced in workflow inputs.
* Uses the Popover component system for consistent styling and positioning.
* Supports keyboard navigation, search filtering, and nested folder views.
*
* @example
* ```tsx
* <TagDropdown
* visible={showDropdown}
* onSelect={handleTagSelect}
* blockId={currentBlockId}
* activeSourceBlockId={null}
* inputValue={inputText}
* cursorPosition={cursor}
* onClose={() => setShowDropdown(false)}
* inputRef={textareaRef}
* />
* ```
*/
export const TagDropdown: React.FC<TagDropdownProps> = ({
visible,
@@ -1382,13 +1490,13 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}}
>
<TagIcon icon='V' color={BLOCK_COLORS.VARIABLE} />
<span className='flex-1 truncate'>
<span className='flex-1 truncate text-[var(--text-primary)]'>
{tag.startsWith(TAG_PREFIXES.VARIABLE)
? tag.substring(TAG_PREFIXES.VARIABLE.length)
: tag}
</span>
{variableInfo && (
<span className='ml-auto text-[10px] text-[var(--white)]/60'>
<span className='ml-auto text-[10px] text-[var(--text-secondary)]'>
{variableInfo.type}
</span>
)}
@@ -1502,9 +1610,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}}
>
<TagIcon icon={tagIcon} color={blockColor} />
<span className='flex-1 truncate'>{child.display}</span>
<span className='flex-1 truncate text-[var(--text-primary)]'>
{child.display}
</span>
{childType && childType !== 'any' && (
<span className='ml-auto text-[10px] text-[var(--white)]/60'>
<span className='ml-auto text-[10px] text-[var(--text-secondary)]'>
{childType}
</span>
)}
@@ -1578,9 +1688,11 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
}}
>
<TagIcon icon={displayIcon} color={blockColor} />
<span className='flex-1 truncate'>{nestedTag.display}</span>
<span className='flex-1 truncate text-[var(--text-primary)]'>
{nestedTag.display}
</span>
{tagDescription && tagDescription !== 'any' && (
<span className='ml-auto text-[10px] text-[var(--white)]/60'>
<span className='ml-auto text-[10px] text-[var(--text-secondary)]'>
{tagDescription}
</span>
)}

View File

@@ -1131,11 +1131,7 @@ try {
{activeSection === 'schema' && (
<ModalFooter className='items-center justify-between'>
{isEditing ? (
<Button
variant='default'
onClick={() => setShowDeleteConfirm(true)}
className='bg-[var(--text-error)] text-white hover:bg-[var(--text-error)]'
>
<Button variant='destructive' onClick={() => setShowDeleteConfirm(true)}>
Delete
</Button>
) : (
@@ -1146,7 +1142,7 @@ try {
Cancel
</Button>
<Button
variant='primary'
variant='tertiary'
onClick={() => setActiveSection('code')}
disabled={!isSchemaValid || !!schemaError}
>
@@ -1159,11 +1155,7 @@ try {
{activeSection === 'code' && (
<ModalFooter className='items-center justify-between'>
{isEditing ? (
<Button
variant='default'
onClick={() => setShowDeleteConfirm(true)}
className='bg-[var(--text-error)] text-white hover:bg-[var(--text-error)]'
>
<Button variant='destructive' onClick={() => setShowDeleteConfirm(true)}>
Delete
</Button>
) : (
@@ -1176,7 +1168,7 @@ try {
Cancel
</Button>
<Button
variant='primary'
variant='tertiary'
onClick={handleSave}
disabled={!isSchemaValid || !!schemaError}
>
@@ -1192,7 +1184,7 @@ try {
<ModalContent size='sm'>
<ModalHeader>Delete Custom Tool</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
This will permanently delete the tool and remove it from any workflows that are using
it. <span className='text-[var(--text-error)]'>This action cannot be undone.</span>
</p>
@@ -1206,10 +1198,9 @@ try {
Cancel
</Button>
<Button
variant='primary'
variant='destructive'
onClick={handleDelete}
disabled={deleteToolMutation.isPending}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
{deleteToolMutation.isPending ? 'Deleting...' : 'Delete'}
</Button>

View File

@@ -98,10 +98,10 @@ export function McpToolsList({
}}
>
<div
className='flex h-[15px] w-[15px] flex-shrink-0 items-center justify-center rounded'
className='flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded-[4px]'
style={{ background: mcpTool.bgColor }}
>
<IconComponent icon={mcpTool.icon} className='h-[11px] w-[11px] text-white' />
<IconComponent icon={mcpTool.icon} className='h-[9px] w-[9px] text-white' />
</div>
<span className='truncate' title={`${mcpTool.name} (${mcpTool.serverName})`}>
{mcpTool.name}

View File

@@ -232,8 +232,8 @@ export function CommandItem({
<button
id={value}
className={cn(
'flex h-[25px] w-full cursor-pointer select-none items-center gap-[8px] rounded-[6px] px-[6px] font-base text-[12px] text-[var(--text-primary)] outline-none transition-colors hover:bg-[var(--surface-9)] hover:text-[var(--text-primary)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:pointer-events-none data-[selected=true]:bg-[var(--surface-9)] data-[selected=true]:text-[var(--text-primary)] data-[disabled=true]:opacity-50',
(isActive || isHovered) && 'bg-[var(--surface-9)] text-[var(--text-primary)]',
'flex h-[25px] w-full cursor-pointer select-none items-center gap-[8px] rounded-[6px] px-[6px] font-base text-[12px] text-[var(--text-primary)] outline-none transition-colors hover:bg-[var(--surface-5)] hover:text-[var(--text-primary)] disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 data-[disabled=true]:pointer-events-none data-[selected=true]:bg-[var(--surface-5)] data-[selected=true]:text-[var(--text-primary)] data-[disabled=true]:opacity-50',
(isActive || isHovered) && 'bg-[var(--surface-5)] text-[var(--text-primary)]',
className
)}
onClick={() => !disabled && onSelect?.()}

View File

@@ -379,11 +379,7 @@ export function TriggerSave({
disabled={disabled || isProcessing}
className='h-[32px] rounded-[8px] px-[12px]'
>
{deleteStatus === 'deleting' ? (
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
) : (
<Trash className='h-[14px] w-[14px]' />
)}
<Trash className='h-[14px] w-[14px]' />
</Button>
)}
</div>
@@ -439,7 +435,7 @@ export function TriggerSave({
<ModalContent size='sm'>
<ModalHeader>Delete Trigger</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Are you sure you want to delete this trigger configuration? This will remove the
webhook and stop all incoming triggers.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
@@ -449,11 +445,7 @@ export function TriggerSave({
<Button variant='active' onClick={() => setShowDeleteDialog(false)}>
Cancel
</Button>
<Button
variant='primary'
onClick={handleDeleteConfirm}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
<Button variant='destructive' onClick={handleDeleteConfirm}>
Delete
</Button>
</ModalFooter>

View File

@@ -294,7 +294,7 @@ export function VariablesInput({
key={assignment.id}
data-assignment-id={assignment.id}
className={cn(
'rounded-[4px] border border-[var(--border-strong)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]',
'rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-3)] dark:bg-[#1F1F1F]',
collapsed ? 'overflow-hidden' : 'overflow-visible'
)}
>
@@ -336,7 +336,7 @@ export function VariablesInput({
</div>
{!collapsed && (
<div className='flex flex-col gap-[6px] border-[var(--border-strong)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='flex flex-col gap-[6px] border-[var(--border-1)] border-t px-[10px] pt-[6px] pb-[10px]'>
<div className='flex flex-col gap-[4px]'>
<Label className='text-[13px]'>Variable</Label>
<Combobox

View File

@@ -54,7 +54,11 @@ export interface WandControlHandlers {
}
/**
* Props for the `SubBlock` UI element. Renders a single configurable input within a workflow block.
* Props for the SubBlock component.
*
* @remarks
* SubBlock renders a single configurable input within a workflow block,
* supporting various input types, preview mode, and conditional requirements.
*/
interface SubBlockProps {
blockId: string
@@ -68,10 +72,14 @@ interface SubBlockProps {
/**
* Returns whether the field is required for validation.
*
* @remarks
* Evaluates conditional requirements based on current field values.
* @param config - The sub-block configuration
* @param subBlockValues - Current values of all subblocks
* @returns True if the field is required
* Supports boolean, condition objects, and functions that return conditions.
*
* @param config - The sub-block configuration containing requirement rules
* @param subBlockValues - Current values of all subblocks for condition evaluation
* @returns `true` if the field is required based on current context
*/
const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string, any>): boolean => {
if (!config.required) return false
@@ -126,10 +134,15 @@ const isFieldRequired = (config: SubBlockConfig, subBlockValues?: Record<string,
/**
* Retrieves the preview value for a specific sub-block.
*
* @remarks
* Only returns a value when in preview mode and subBlockValues are provided.
* Returns `null` if the value is not found in the subblock values.
*
* @param config - The sub-block configuration
* @param isPreview - Whether the component is in preview mode
* @param subBlockValues - Optional record of sub-block values
* @returns The preview value or undefined
* @returns The preview value, `null` if not found, or `undefined` if not in preview
*/
const getPreviewValue = (
config: SubBlockConfig,
@@ -142,11 +155,16 @@ const getPreviewValue = (
/**
* Renders the label with optional validation, description tooltips, and inline wand control.
* @param config - The sub-block configuration
* @param isValidJson - Whether the JSON is valid
* @param wandState - Wand interaction state
*
* @remarks
* Handles JSON validation indicators for code blocks, required field markers,
* and AI generation (wand) input interface.
*
* @param config - The sub-block configuration defining the label content
* @param isValidJson - Whether the JSON content is valid (for code blocks)
* @param wandState - State and handlers for the AI wand feature
* @param subBlockValues - Current values of all subblocks for evaluating conditional requirements
* @returns The label JSX element or null if no title or for switch types
* @returns The label JSX element, or `null` for switch types or when no title is defined
*/
const renderLabel = (
config: SubBlockConfig,
@@ -251,9 +269,14 @@ const renderLabel = (
/**
* Compares props to prevent unnecessary re-renders.
*
* @remarks
* Used with React.memo to optimize performance by skipping re-renders
* when props haven't meaningfully changed.
*
* @param prevProps - Previous component props
* @param nextProps - Next component props
* @returns True if props are equal (skip re-render)
* @returns `true` if props are equal and re-render should be skipped
*/
const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): boolean => {
return (
@@ -268,7 +291,21 @@ const arePropsEqual = (prevProps: SubBlockProps, nextProps: SubBlockProps): bool
}
/**
* Renders a single workflow sub-block input based on `config.type`, supporting preview and disabled states.
* Renders a single workflow sub-block input based on config.type.
*
* @remarks
* Supports multiple input types including short-input, long-input, dropdown,
* combobox, slider, table, code, switch, tool-input, and many more.
* Handles preview mode, disabled states, and AI wand generation.
*
* @param blockId - The parent block identifier
* @param config - Configuration defining the input type and properties
* @param isPreview - Whether to render in preview mode
* @param subBlockValues - Current values of all subblocks
* @param disabled - Whether the input is disabled
* @param fieldDiffStatus - Optional diff status for visual indicators
* @param allowExpandInPreview - Whether to allow expanding in preview mode
* @returns The rendered sub-block input component
*/
function SubBlockComponent({
blockId,
@@ -297,7 +334,8 @@ function SubBlockComponent({
const isWandEnabled = config.wandConfig?.enabled ?? false
/**
* Handle wand icon click to activate inline prompt mode
* Handles wand icon click to activate inline prompt mode.
* Focuses the input after a brief delay to ensure DOM is ready.
*/
const handleSearchClick = (): void => {
setIsSearchActive(true)
@@ -307,7 +345,8 @@ function SubBlockComponent({
}
/**
* Handle search input blur - deactivate if empty and not streaming
* Handles search input blur event.
* Deactivates the search mode if the query is empty and not currently streaming.
*/
const handleSearchBlur = (): void => {
if (!searchQuery.trim() && !wandControlRef.current?.isWandStreaming) {
@@ -316,14 +355,17 @@ function SubBlockComponent({
}
/**
* Handle search query change
* Handles search query change.
*
* @param value - The new search query value
*/
const handleSearchChange = (value: string): void => {
setSearchQuery(value)
}
/**
* Handle search submit - trigger generation
* Handles search submit to trigger AI generation.
* Clears the query and deactivates search mode after submission.
*/
const handleSearchSubmit = (): void => {
if (searchQuery.trim() && wandControlRef.current) {
@@ -334,7 +376,8 @@ function SubBlockComponent({
}
/**
* Handle search cancel
* Handles search cancel to exit AI prompt mode.
* Clears the query and deactivates search mode.
*/
const handleSearchCancel = (): void => {
setSearchQuery('')
@@ -358,7 +401,13 @@ function SubBlockComponent({
const isDisabled = gatedDisabled
/**
* Selects and renders the appropriate input component for the current sub-block `config.type`.
* Selects and renders the appropriate input component based on config.type.
*
* @remarks
* Maps the config type to the corresponding input component with all
* necessary props. Falls back to an error message for unknown types.
*
* @returns The appropriate input component JSX element
*/
const renderInput = (): JSX.Element => {
switch (config.type) {

View File

@@ -172,9 +172,9 @@ export function Editor() {
return (
<div className='flex h-full flex-col'>
{/* Header */}
<div className='flex flex-shrink-0 items-center justify-between rounded-[4px] bg-[var(--surface-5)] px-[12px] py-[8px]'>
<div className='mx-[-1px] flex flex-shrink-0 items-center justify-between rounded-[4px] border border-[var(--border)] bg-[var(--surface-4)] px-[12px] py-[6px]'>
<div className='flex min-w-0 flex-1 items-center gap-[8px]'>
{(blockConfig || isSubflow) && (
{(blockConfig || isSubflow) && currentBlock?.type !== 'note' && (
<div
className='flex h-[18px] w-[18px] items-center justify-center rounded-[4px]'
style={{ background: isSubflow ? subflowBgColor : blockConfig?.bgColor }}

View File

@@ -488,7 +488,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
>
{/* Header */}
<div
className='flex flex-shrink-0 cursor-pointer items-center justify-between rounded-[4px] bg-[var(--surface-5)] px-[12px] py-[8px]'
className='mx-[-1px] flex flex-shrink-0 cursor-pointer items-center justify-between rounded-[4px] border border-[var(--border)] bg-[var(--surface-4)] px-[12px] py-[6px]'
onClick={handleSearchClick}
>
<h2 className='font-medium text-[14px] text-[var(--text-primary)]'>Toolbar</h2>
@@ -532,7 +532,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
Triggers
</div>
<div className='flex-1 overflow-y-auto overflow-x-hidden px-[6px]'>
<div ref={triggersContentRef} className='space-y-[4px] pb-[8px]'>
<div ref={triggersContentRef} className='space-y-[2px] pb-[8px]'>
{filteredTriggers.map((trigger, index) => {
const Icon = trigger.icon
const isTriggerCapable = hasTriggerCapability(trigger)
@@ -554,9 +554,9 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
}}
onClick={() => handleItemClick(trigger.type, isTriggerCapable)}
className={clsx(
'group flex h-[25px] items-center gap-[8px] rounded-[8px] px-[5.5px] text-[14px]',
'cursor-pointer hover:bg-[var(--surface-9)] active:cursor-grabbing',
'focus-visible:bg-[var(--surface-9)] focus-visible:outline-none'
'group flex h-[28px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px]',
'cursor-pointer hover:bg-[var(--surface-6)] active:cursor-grabbing dark:hover:bg-[var(--surface-5)]',
'focus-visible:bg-[var(--surface-6)] focus-visible:outline-none dark:focus-visible:bg-[var(--surface-5)]'
)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
@@ -567,7 +567,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
}}
>
<div
className='relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ background: trigger.bgColor }}
>
{Icon && (
@@ -575,7 +575,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
className={clsx(
'toolbar-item-icon text-white transition-transform duration-200',
'group-hover:scale-110',
'!h-[9px] !w-[9px]'
'!h-[10px] !w-[10px]'
)}
/>
)}
@@ -614,7 +614,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
Blocks
</div>
<div className='flex-1 overflow-y-auto overflow-x-hidden px-[6px]'>
<div className='space-y-[4px] pb-[8px]'>
<div className='space-y-[2px] pb-[8px]'>
{filteredBlocks.map((block, index) => {
const Icon = block.icon
return (
@@ -643,9 +643,9 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
}}
onClick={() => handleItemClick(block.type, false)}
className={clsx(
'group flex h-[25px] items-center gap-[8px] rounded-[8px] px-[5.5px] text-[14px]',
'cursor-pointer hover:bg-[var(--surface-9)] active:cursor-grabbing',
'focus-visible:bg-[var(--surface-9)] focus-visible:outline-none'
'group flex h-[28px] items-center gap-[8px] rounded-[8px] px-[6px] text-[14px]',
'cursor-pointer hover:bg-[var(--surface-6)] active:cursor-grabbing dark:hover:bg-[var(--surface-5)]',
'focus-visible:bg-[var(--surface-6)] focus-visible:outline-none dark:focus-visible:bg-[var(--surface-5)]'
)}
onKeyDown={(event) => {
if (event.key === 'Enter') {
@@ -656,7 +656,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
}}
>
<div
className='relative flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
className='relative flex h-[16px] w-[16px] flex-shrink-0 items-center justify-center overflow-hidden rounded-[4px]'
style={{ background: block.bgColor }}
>
{Icon && (
@@ -664,7 +664,7 @@ export const Toolbar = forwardRef<ToolbarRef, ToolbarProps>(function Toolbar(
className={clsx(
'toolbar-item-icon text-white transition-transform duration-200',
'group-hover:scale-110',
'!h-[9px] !w-[9px]'
'!h-[10px] !w-[10px]'
)}
/>
)}

View File

@@ -1,10 +1,5 @@
/**
* @deprecated This component is deprecated and kept as reference only.
*/
'use client'
import { useStore } from 'reactflow'
import { Button, Redo, Undo } from '@/components/emcn'
import { useSession } from '@/lib/auth/auth-client'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
@@ -12,15 +7,10 @@ import { useUndoRedoStore } from '@/stores/undo-redo'
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
/**
* Workflow controls component that provides undo/redo and zoom functionality
* Integrates directly into the panel header for easy access
* Workflow controls component that provides undo/redo functionality.
* Styled to align with the panel tab buttons.
*/
export function WorkflowControls() {
// Subscribe to React Flow store so zoom % live-updates while zooming
const zoom = useStore((s: any) =>
Array.isArray(s.transform) ? s.transform[2] : s.viewport?.zoom
)
const { undo, redo } = useCollaborativeWorkflow()
const { activeWorkflowId } = useWorkflowRegistry()
const { data: session } = useSession()
@@ -33,34 +23,28 @@ export function WorkflowControls() {
return { undoSize: stack.undo.length, redoSize: stack.redo.length }
})()
const currentZoom = Math.round(((zoom as number) || 1) * 100)
const canUndo = undoRedoSizes.undoSize > 0
const canRedo = undoRedoSizes.redoSize > 0
return (
<div className='flex items-center gap-[4px]'>
{/* Undo/Redo Controls - Connected Two-Sided Button */}
<div className='flex gap-[1px]'>
<Button
className='h-[28px] rounded-r-none px-[8px] py-[5px] text-[12.5px]'
onClick={undo}
variant={undoRedoSizes.undoSize === 0 ? 'default' : 'active'}
title='Undo (Cmd+Z)'
>
<Undo className='h-[12px] w-[12px]' />
</Button>
<Button
className='h-[28px] rounded-l-none px-[8px] py-[5px] text-[12.5px]'
onClick={redo}
variant={undoRedoSizes.redoSize === 0 ? 'default' : 'active'}
title='Redo (Cmd+Shift+Z)'
>
<Redo className='h-[12px] w-[12px]' />
</Button>
</div>
{/* Zoom Badge */}
<Button className='flex h-[28px] w-[40px] items-center justify-center rounded-[4px] px-[8px] py-[5px] font-medium text-[12.5px]'>
{currentZoom}%
<div className='flex gap-[2px]'>
<Button
className='h-[28px] rounded-[6px] rounded-r-none border border-transparent px-[6px] py-[5px] hover:border-[var(--border-1)] hover:bg-[var(--surface-5)]'
onClick={undo}
variant={canUndo ? 'active' : 'ghost'}
disabled={!canUndo}
title='Undo (Cmd+Z)'
>
<Undo className='h-[12px] w-[12px]' />
</Button>
<Button
className='h-[28px] rounded-[6px] rounded-l-none border border-transparent px-[6px] py-[5px] hover:border-[var(--border-1)] hover:bg-[var(--surface-5)]'
onClick={redo}
variant={canRedo ? 'active' : 'ghost'}
disabled={!canRedo}
title='Redo (Cmd+Shift+Z)'
>
<Redo className='h-[12px] w-[12px]' />
</Button>
</div>
)

View File

@@ -352,10 +352,10 @@ export function Panel() {
{/* Header */}
<div className='flex flex-shrink-0 items-center justify-between px-[8px]'>
{/* More and Chat */}
<div className='flex gap-[4px]'>
<div className='flex gap-[6px]'>
<Popover open={isMenuOpen} onOpenChange={setIsMenuOpen}>
<PopoverTrigger asChild>
<Button className='h-[32px] w-[32px]'>
<Button className='h-[30px] w-[30px] rounded-[5px]'>
<MoreHorizontal />
</Button>
</PopoverTrigger>
@@ -408,7 +408,7 @@ export function Panel() {
</PopoverContent>
</Popover>
<Button
className='h-[32px] w-[32px]'
className='h-[30px] w-[30px] rounded-[5px]'
variant={isChatOpen ? 'active' : 'default'}
onClick={() => setIsChatOpen(!isChatOpen)}
>
@@ -417,11 +417,11 @@ export function Panel() {
</div>
{/* Deploy and Run */}
<div className='flex gap-[4px]'>
<div className='flex gap-[6px]'>
<Deploy activeWorkflowId={activeWorkflowId} userPermissions={userPermissions} />
<Button
className='h-[32px] w-[61.5px] gap-[8px]'
variant={isExecuting ? 'active' : 'primary'}
className='h-[30px] gap-[8px] px-[10px]'
variant={isExecuting ? 'active' : 'tertiary'}
onClick={isExecuting ? cancelWorkflow : () => runWorkflow()}
disabled={!isExecuting && isButtonDisabled}
>
@@ -430,7 +430,7 @@ export function Panel() {
) : (
<Play className='h-[11.5px] w-[11.5px]' />
)}
Run
{isExecuting ? 'Stop' : 'Run'}
</Button>
</div>
</div>
@@ -439,7 +439,11 @@ export function Panel() {
<div className='flex flex-shrink-0 items-center justify-between px-[8px] pt-[14px]'>
<div className='flex gap-[4px]'>
<Button
className='h-[28px] truncate px-[8px] py-[5px] text-[12.5px] hover:bg-[var(--surface-9)] hover:text-[var(--text-primary)]'
className={`h-[28px] truncate rounded-[6px] border px-[8px] py-[5px] text-[12.5px] ${
_hasHydrated && activeTab === 'copilot'
? 'border-[var(--border-1)]'
: 'border-transparent hover:border-[var(--border-1)] hover:bg-[var(--surface-5)] hover:text-[var(--text-primary)]'
}`}
variant={_hasHydrated && activeTab === 'copilot' ? 'active' : 'ghost'}
onClick={() => handleTabClick('copilot')}
data-tab-button='copilot'
@@ -447,7 +451,11 @@ export function Panel() {
Copilot
</Button>
<Button
className='h-[28px] px-[8px] py-[5px] text-[12.5px] hover:bg-[var(--surface-9)] hover:text-[var(--text-primary)]'
className={`h-[28px] rounded-[6px] border px-[8px] py-[5px] text-[12.5px] ${
_hasHydrated && activeTab === 'toolbar'
? 'border-[var(--border-1)]'
: 'border-transparent hover:border-[var(--border-1)] hover:bg-[var(--surface-5)] hover:text-[var(--text-primary)]'
}`}
variant={_hasHydrated && activeTab === 'toolbar' ? 'active' : 'ghost'}
onClick={() => handleTabClick('toolbar')}
data-tab-button='toolbar'
@@ -455,7 +463,11 @@ export function Panel() {
Toolbar
</Button>
<Button
className='h-[28px] px-[8px] py-[5px] text-[12.5px] hover:bg-[var(--surface-9)] hover:text-[var(--text-primary)]'
className={`h-[28px] rounded-[6px] border px-[8px] py-[5px] text-[12.5px] ${
_hasHydrated && activeTab === 'editor'
? 'border-[var(--border-1)]'
: 'border-transparent hover:border-[var(--border-1)] hover:bg-[var(--surface-5)] hover:text-[var(--text-primary)]'
}`}
variant={_hasHydrated && activeTab === 'editor' ? 'active' : 'ghost'}
onClick={() => handleTabClick('editor')}
data-tab-button='editor'
@@ -464,7 +476,7 @@ export function Panel() {
</Button>
</div>
{/* Workflow Controls (Undo/Redo and Zoom) */}
{/* Workflow Controls (Undo/Redo) */}
{/* <WorkflowControls /> */}
</div>
@@ -524,7 +536,7 @@ export function Panel() {
<ModalContent size='sm'>
<ModalHeader>Delete Workflow</ModalHeader>
<ModalBody>
<p className='text-[12px] text-[var(--text-tertiary)]'>
<p className='text-[12px] text-[var(--text-secondary)]'>
Deleting this workflow will permanently remove all associated blocks, executions, and
configuration.{' '}
<span className='text-[var(--text-error)]'>This action cannot be undone.</span>
@@ -538,12 +550,7 @@ export function Panel() {
>
Cancel
</Button>
<Button
variant='primary'
onClick={handleDeleteWorkflow}
disabled={isDeleting}
className='!bg-[var(--text-error)] !text-white hover:!bg-[var(--text-error)]/90'
>
<Button variant='destructive' onClick={handleDeleteWorkflow} disabled={isDeleting}>
{isDeleting ? 'Deleting...' : 'Delete'}
</Button>
</ModalFooter>

View File

@@ -1,183 +0,0 @@
'use client'
import { Bug, Copy, Layers, Play, Rocket, Trash2 } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { Skeleton } from '@/components/ui/skeleton'
const SkeletonControlBar = () => {
return (
<div className='fixed top-4 right-4 z-20 flex items-center gap-1'>
{/* Delete Button */}
<Button
variant='outline'
className='h-12 w-12 cursor-not-allowed rounded-[11px] border-[hsl(var(--card-border))] bg-[hsl(var(--card-background))] text-[hsl(var(--card-text))] opacity-50 shadow-xs hover:border-[hsl(var(--card-border))] hover:bg-[hsl(var(--card-background))]'
disabled
>
<Trash2 className='h-5 w-5' />
</Button>
{/* Duplicate Button */}
<Button
variant='outline'
className='h-12 w-12 cursor-not-allowed rounded-[11px] border-[hsl(var(--card-border))] bg-[hsl(var(--card-background))] text-[hsl(var(--card-text))] opacity-50 shadow-xs hover:border-[hsl(var(--card-border))] hover:bg-[hsl(var(--card-background))] hover:bg-gray-100'
disabled
>
<Copy className='h-5 w-5' />
</Button>
{/* Auto Layout Button */}
<Button
variant='outline'
className='h-12 w-12 cursor-not-allowed rounded-[11px] border-[hsl(var(--card-border))] bg-[hsl(var(--card-background))] text-[hsl(var(--card-text))] opacity-50 shadow-xs hover:border-[hsl(var(--card-border))] hover:bg-[hsl(var(--card-background))] hover:bg-gray-100'
disabled
>
<Layers className='h-5 w-5' />
</Button>
{/* Debug Mode Button */}
<Button
variant='outline'
className='h-12 w-12 cursor-not-allowed rounded-[11px] border-[hsl(var(--card-border))] bg-[hsl(var(--card-background))] text-[hsl(var(--card-text))] opacity-50 shadow-xs hover:border-[hsl(var(--card-border))] hover:bg-[hsl(var(--card-background))] hover:bg-gray-100'
disabled
>
<Bug className='h-5 w-5' />
</Button>
{/* Deploy Button */}
<Button
variant='outline'
className='h-12 w-12 cursor-not-allowed rounded-[11px] border-[hsl(var(--card-border))] bg-[hsl(var(--card-background))] text-[hsl(var(--card-text))] opacity-50 shadow-xs hover:border-[hsl(var(--card-border))] hover:bg-[hsl(var(--card-background))] hover:bg-gray-100'
disabled
>
<Rocket className='h-5 w-5' />
</Button>
{/* Run Button */}
<Button
className='h-12 cursor-not-allowed gap-2 rounded-[11px] bg-[var(--brand-primary-hex)] px-4 py-2 font-medium text-white shadow-[0_0_0_0_var(--brand-primary-hex)] transition-all duration-200 hover:bg-[var(--brand-primary-hover-hex)] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)] disabled:opacity-50 disabled:hover:bg-[var(--brand-primary-hex)] disabled:hover:shadow-none'
disabled
>
<Play className='h-3.5 w-3.5 fill-current stroke-current' />
</Button>
</div>
)
}
const SkeletonPanelComponent = () => {
return (
<div className='fixed top-0 right-0 z-10'>
{/* Panel skeleton */}
<div className='h-96 w-80 space-y-4 rounded-bl-lg border-b border-l bg-background p-4'>
{/* Tab headers skeleton */}
<div className='flex gap-2 border-b pb-2'>
{Array.from({ length: 3 }).map((_, i) => (
<Skeleton key={i} className='h-6 w-16' />
))}
</div>
{/* Content skeleton */}
<div className='space-y-3'>
{Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className='h-4' style={{ width: `${Math.random() * 40 + 60}%` }} />
))}
</div>
</div>
</div>
)
}
const SkeletonNodes = () => {
return [
// Starter node skeleton
{
id: 'skeleton-starter',
type: 'workflowBlock',
position: { x: 100, y: 100 },
data: {
type: 'skeleton',
config: { name: '', description: '', bgColor: '#9CA3AF' },
name: '',
isActive: false,
isPending: false,
isSkeleton: true,
},
dragHandle: '.workflow-drag-handle',
},
// Additional skeleton nodes
{
id: 'skeleton-node-1',
type: 'workflowBlock',
position: { x: 500, y: 100 },
data: {
type: 'skeleton',
config: { name: '', description: '', bgColor: '#9CA3AF' },
name: '',
isActive: false,
isPending: false,
isSkeleton: true,
},
dragHandle: '.workflow-drag-handle',
},
{
id: 'skeleton-node-2',
type: 'workflowBlock',
position: { x: 300, y: 300 },
data: {
type: 'skeleton',
config: { name: '', description: '', bgColor: '#9CA3AF' },
name: '',
isActive: false,
isPending: false,
isSkeleton: true,
},
dragHandle: '.workflow-drag-handle',
},
]
}
interface SkeletonLoadingProps {
showSkeleton: boolean
isSidebarCollapsed: boolean
children: React.ReactNode
}
export function SkeletonLoading({
showSkeleton,
isSidebarCollapsed,
children,
}: SkeletonLoadingProps) {
return (
<div className='flex h-full w-full flex-1 flex-col overflow-hidden'>
{/* Skeleton Control Bar */}
<div
className={`transition-opacity duration-500 ${showSkeleton ? 'opacity-100' : 'pointer-events-none absolute opacity-0'}`}
style={{ zIndex: showSkeleton ? 10 : -1 }}
>
<SkeletonControlBar />
</div>
{/* Real Control Bar */}
<div
className={`transition-opacity duration-500 ${showSkeleton ? 'pointer-events-none absolute opacity-0' : 'opacity-100'}`}
style={{ zIndex: showSkeleton ? -1 : 10 }}
>
{children}
</div>
{/* Real content will be rendered by children - sidebar will show its own loading state */}
</div>
)
}
export function SkeletonPanelWrapper({ showSkeleton }: { showSkeleton: boolean }) {
return (
<div
className={`transition-opacity duration-500 ${showSkeleton ? 'opacity-100' : 'pointer-events-none absolute opacity-0'}`}
style={{ zIndex: showSkeleton ? 10 : -1 }}
>
<SkeletonPanelComponent />
</div>
)
}
export { SkeletonNodes, SkeletonPanelComponent }

View File

@@ -108,7 +108,7 @@ export const SubflowNodeComponent = memo(({ data, id }: NodeProps<SubflowNodeDat
*/
const getHandleClasses = (position: 'left' | 'right') => {
const baseClasses = '!z-[10] !cursor-crosshair !border-none !transition-[colors] !duration-150'
const colorClasses = '!bg-[var(--surface-12)]'
const colorClasses = '!bg-[var(--surface-7)]'
const positionClasses = {
left: '!left-[-7px] !h-5 !w-[7px] !rounded-l-[2px] !rounded-r-none hover:!left-[-10px] hover:!w-[10px] hover:!rounded-l-full',

Some files were not shown because too many files have changed in this diff Show More