diff --git a/apps/sim/app/_shell/providers/theme-provider.tsx b/apps/sim/app/_shell/providers/theme-provider.tsx index e9b724967e..6b3c7f315e 100644 --- a/apps/sim/app/_shell/providers/theme-provider.tsx +++ b/apps/sim/app/_shell/providers/theme-provider.tsx @@ -28,7 +28,7 @@ export function ThemeProvider({ children, ...props }: ThemeProviderProps) { return ( ([]) + const [emailItems, setEmailItems] = useState([]) const [formErrors, setFormErrors] = useState>({}) @@ -225,8 +225,7 @@ export function NotificationSettings({ }) setFormErrors({}) setEditingId(null) - setEmailInputValue('') - setInvalidEmails([]) + setEmailItems([]) }, []) const handleClose = useCallback(() => { @@ -243,81 +242,37 @@ export function NotificationSettings({ const normalized = email.trim().toLowerCase() const validation = quickValidateEmail(normalized) - if (formData.emailRecipients.includes(normalized) || invalidEmails.includes(normalized)) { + if (emailItems.some((item) => item.value === normalized)) { return false } - if (!validation.isValid) { - setInvalidEmails((prev) => [...prev, normalized]) - setEmailInputValue('') - return false + setEmailItems((prev) => [...prev, { value: normalized, isValid: validation.isValid }]) + + if (validation.isValid) { + setFormErrors((prev) => ({ ...prev, emailRecipients: '' })) + setFormData((prev) => ({ + ...prev, + emailRecipients: [...prev.emailRecipients, normalized], + })) } - setFormErrors((prev) => ({ ...prev, emailRecipients: '' })) - setFormData((prev) => ({ - ...prev, - emailRecipients: [...prev.emailRecipients, normalized], - })) - setEmailInputValue('') - return true + return validation.isValid }, - [formData.emailRecipients, invalidEmails] + [emailItems] ) - const handleRemoveEmail = useCallback((emailToRemove: string) => { - setFormData((prev) => ({ - ...prev, - emailRecipients: prev.emailRecipients.filter((e) => e !== emailToRemove), - })) - }, []) - - const handleRemoveInvalidEmail = useCallback((index: number) => { - setInvalidEmails((prev) => prev.filter((_, i) => i !== index)) - }, []) - - const handleEmailKeyDown = useCallback( - (e: React.KeyboardEvent) => { - if (['Enter', ',', ' '].includes(e.key) && emailInputValue.trim()) { - e.preventDefault() - addEmail(emailInputValue) - } - - if (e.key === 'Backspace' && !emailInputValue) { - if (invalidEmails.length > 0) { - handleRemoveInvalidEmail(invalidEmails.length - 1) - } else if (formData.emailRecipients.length > 0) { - handleRemoveEmail(formData.emailRecipients[formData.emailRecipients.length - 1]) - } + const handleRemoveEmailItem = useCallback( + (_value: string, index: number, isValid: boolean) => { + const itemToRemove = emailItems[index] + setEmailItems((prev) => prev.filter((_, i) => i !== index)) + if (isValid && itemToRemove) { + setFormData((prev) => ({ + ...prev, + emailRecipients: prev.emailRecipients.filter((e) => e !== itemToRemove.value), + })) } }, - [ - emailInputValue, - addEmail, - invalidEmails, - formData.emailRecipients, - handleRemoveInvalidEmail, - handleRemoveEmail, - ] - ) - - const handleEmailPaste = useCallback( - (e: React.ClipboardEvent) => { - e.preventDefault() - const pastedText = e.clipboardData.getData('text') - const pastedEmails = pastedText.split(/[\s,;]+/).filter(Boolean) - - let addedCount = 0 - pastedEmails.forEach((email) => { - if (addEmail(email)) { - addedCount++ - } - }) - - if (addedCount === 0 && pastedEmails.length === 1) { - setEmailInputValue(emailInputValue + pastedEmails[0]) - } - }, - [addEmail, emailInputValue] + [emailItems] ) const validateForm = (): boolean => { @@ -356,8 +311,11 @@ export function NotificationSettings({ } else if (formData.emailRecipients.length > 10) { errors.emailRecipients = 'Maximum 10 email recipients allowed' } - if (invalidEmails.length > 0) { - errors.emailRecipients = `Invalid email addresses: ${invalidEmails.join(', ')}` + const invalidEmailValues = emailItems + .filter((item) => !item.isValid) + .map((item) => item.value) + if (invalidEmailValues.length > 0) { + errors.emailRecipients = `Invalid email addresses: ${invalidEmailValues.join(', ')}` } } @@ -536,8 +494,9 @@ export function NotificationSettings({ inactivityHours: subscription.alertConfig?.inactivityHours || 24, errorCountThreshold: subscription.alertConfig?.errorCountThreshold || 10, }) - setEmailInputValue('') - setInvalidEmails([]) + setEmailItems( + (subscription.emailRecipients || []).map((email) => ({ value: email, isValid: true })) + ) setShowForm(true) } @@ -692,37 +651,13 @@ export function NotificationSettings({ {activeTab === 'email' && (
-
- {invalidEmails.map((email, index) => ( - handleRemoveInvalidEmail(index)} - isInvalid={true} - /> - ))} - {formData.emailRecipients.map((email, index) => ( - handleRemoveEmail(email)} - /> - ))} - setEmailInputValue(e.target.value)} - onKeyDown={handleEmailKeyDown} - onPaste={handleEmailPaste} - onBlur={() => emailInputValue.trim() && addEmail(emailInputValue)} - placeholder={ - formData.emailRecipients.length > 0 || invalidEmails.length > 0 - ? 'Add another email' - : 'Enter emails' - } - className='min-w-[180px] flex-1 border-none bg-transparent p-0 font-medium font-sans text-foreground text-sm outline-none placeholder:text-[var(--text-muted)] disabled:cursor-not-allowed disabled:opacity-50' - /> -
+ addEmail(value)} + onRemove={handleRemoveEmailItem} + placeholder='Enter emails' + placeholderWithTags='Add email' + /> {formErrors.emailRecipients && (

{formErrors.emailRecipients}

)} @@ -1351,37 +1286,3 @@ export function NotificationSettings({ ) } - -interface EmailTagProps { - email: string - onRemove: () => void - isInvalid?: boolean -} - -function EmailTag({ email, onRemove, isInvalid }: EmailTagProps) { - return ( -
- {email} - -
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/block-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/block-context-menu.tsx index 6ae1f22e09..3e74ecd72f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/block-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/block-context-menu.tsx @@ -80,7 +80,7 @@ export function BlockContextMenu({ }} > Copy - ⌘C + ⌘C Paste - ⌘V + ⌘V {!hasStarterBlock && ( Delete - + diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx index dde87e5a56..acca2b1ea5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/context-menu/pane-context-menu.tsx @@ -61,7 +61,7 @@ export function PaneContextMenu({ }} > Undo - ⌘Z + ⌘Z Redo - ⌘⇧Z + ⌘⇧Z {/* Edit and creation actions */} @@ -86,7 +86,7 @@ export function PaneContextMenu({ }} > Paste - ⌘V + ⌘V Add Block - ⌘K + ⌘K Auto-layout - ⇧L + ⇧L {/* Navigation actions */} @@ -121,7 +121,7 @@ export function PaneContextMenu({ }} > Open Logs - ⌘L + ⌘L { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx index 24936e8e47..c4ede34df9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/notifications/notifications.tsx @@ -2,7 +2,7 @@ import { memo, useCallback, useMemo } from 'react' import { createLogger } from '@sim/logger' import clsx from 'clsx' import { X } from 'lucide-react' -import { Button } from '@/components/emcn' +import { Button, Tooltip } 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' @@ -130,14 +130,21 @@ export const Notifications = memo(function Notifications() { hasAction ? 'line-clamp-2' : 'line-clamp-4' }`} > - + + + + + + Clear all + + {notification.level === 'error' && ( )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx index f4ad8ce127..3de83a3d8f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/chat/chat.tsx @@ -2,7 +2,7 @@ import { useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { AlertTriangle, Check, Clipboard, Eye, EyeOff, Loader2, RefreshCw, X } from 'lucide-react' +import { AlertTriangle, Check, Clipboard, Eye, EyeOff, Loader2, RefreshCw } from 'lucide-react' import { Button, ButtonGroup, @@ -14,6 +14,8 @@ import { ModalContent, ModalFooter, ModalHeader, + TagInput, + type TagItem, Textarea, Tooltip, } from '@/components/emcn' @@ -352,6 +354,7 @@ export function ChatDeploy({
([]) + const [emailItems, setEmailItems] = useState(() => + emails.map((email) => ({ value: email, isValid: true })) + ) const handleGeneratePassword = () => { const newPassword = generatePassword(24) @@ -607,59 +611,25 @@ function AuthSelector({ const validation = quickValidateEmail(normalized) const isValid = validation.isValid || isDomainPattern - if (emails.includes(normalized) || invalidEmails.includes(normalized)) { + if (emailItems.some((item) => item.value === normalized)) { return false } - if (!isValid) { - setInvalidEmails((prev) => [...prev, normalized]) - setEmailInputValue('') - return false + setEmailItems((prev) => [...prev, { value: normalized, isValid }]) + + if (isValid) { + setEmailError('') + onEmailsChange([...emails, normalized]) } - setEmailError('') - onEmailsChange([...emails, normalized]) - setEmailInputValue('') - return true + return isValid } - const handleRemoveEmail = (emailToRemove: string) => { - onEmailsChange(emails.filter((e) => e !== emailToRemove)) - } - - const handleRemoveInvalidEmail = (index: number) => { - setInvalidEmails((prev) => prev.filter((_, i) => i !== index)) - } - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (['Enter', ',', ' '].includes(e.key) && emailInputValue.trim()) { - e.preventDefault() - addEmail(emailInputValue) - } - - if (e.key === 'Backspace' && !emailInputValue) { - if (invalidEmails.length > 0) { - handleRemoveInvalidEmail(invalidEmails.length - 1) - } else if (emails.length > 0) { - handleRemoveEmail(emails[emails.length - 1]) - } - } - } - - const handlePaste = (e: React.ClipboardEvent) => { - e.preventDefault() - const pastedText = e.clipboardData.getData('text') - const pastedEmails = pastedText.split(/[\s,;]+/).filter(Boolean) - - let addedCount = 0 - pastedEmails.forEach((email) => { - if (addEmail(email)) { - addedCount++ - } - }) - - if (addedCount === 0 && pastedEmails.length === 1) { - setEmailInputValue(emailInputValue + pastedEmails[0]) + const handleRemoveEmailItem = (_value: string, index: number, isValid: boolean) => { + const itemToRemove = emailItems[index] + setEmailItems((prev) => prev.filter((_, i) => i !== index)) + if (isValid && itemToRemove) { + onEmailsChange(emails.filter((e) => e !== itemToRemove.value)) } } @@ -774,40 +744,14 @@ function AuthSelector({ -
- {invalidEmails.map((email, index) => ( - handleRemoveInvalidEmail(index)} - disabled={disabled} - isInvalid={true} - /> - ))} - {emails.map((email, index) => ( - handleRemoveEmail(email)} - disabled={disabled} - /> - ))} - setEmailInputValue(e.target.value)} - onKeyDown={handleKeyDown} - onPaste={handlePaste} - onBlur={() => emailInputValue.trim() && addEmail(emailInputValue)} - placeholder={ - emails.length > 0 || invalidEmails.length > 0 - ? 'Add another email' - : 'Enter emails or domains (@example.com)' - } - className='min-w-[180px] flex-1 border-none bg-transparent p-0 font-medium font-sans text-foreground text-sm outline-none placeholder:text-[var(--text-muted)] disabled:cursor-not-allowed disabled:opacity-50' - disabled={disabled} - /> -
+ addEmail(value)} + onRemove={handleRemoveEmailItem} + placeholder='Enter emails or domains (@example.com)' + placeholderWithTags='Add email' + disabled={disabled} + /> {emailError && (

{emailError}

)} @@ -823,40 +767,3 @@ function AuthSelector({ ) } - -interface EmailTagProps { - email: string - onRemove: () => void - disabled?: boolean - isInvalid?: boolean -} - -function EmailTag({ email, onRemove, disabled, isInvalid }: EmailTagProps) { - return ( -
- {email} - {!disabled && ( - - )} -
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx index 8ebfee9406..bafbfdb236 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/deploy/components/deploy-modal/components/form/form.tsx @@ -2,8 +2,17 @@ import { useCallback, useEffect, useState } from 'react' import { createLogger } from '@sim/logger' -import { ChevronDown, ChevronRight, Eye, EyeOff, Loader2, X } from 'lucide-react' -import { Badge, ButtonGroup, ButtonGroupItem, Input, Label, Textarea } from '@/components/emcn' +import { ChevronDown, ChevronRight, Eye, EyeOff, Loader2 } from 'lucide-react' +import { + Badge, + ButtonGroup, + ButtonGroupItem, + Input, + Label, + TagInput, + type TagItem, + Textarea, +} from '@/components/emcn' import { Skeleton } from '@/components/ui' import { getEnv } from '@/lib/core/config/env' import { isDev } from '@/lib/core/config/feature-flags' @@ -85,7 +94,7 @@ export function FormDeploy({ ) const [authType, setAuthType] = useState<'public' | 'password' | 'email'>('public') const [password, setPassword] = useState('') - const [allowedEmails, setAllowedEmails] = useState([]) + const [emailItems, setEmailItems] = useState([]) const [existingForm, setExistingForm] = useState(null) const [isLoading, setIsLoading] = useState(true) const [formUrl, setFormUrl] = useState('') @@ -143,7 +152,9 @@ export function FormDeploy({ 'Your response has been submitted successfully.' ) setAuthType(form.authType) - setAllowedEmails(form.allowedEmails || []) + setEmailItems( + (form.allowedEmails || []).map((email) => ({ value: email, isValid: true })) + ) if (form.customizations?.fieldConfigs) { setFieldConfigs(form.customizations.fieldConfigs) } @@ -210,6 +221,8 @@ export function FormDeploy({ } }, [workflowId, fieldConfigs.length]) + const allowedEmails = emailItems.filter((item) => item.isValid).map((item) => item.value) + // Validate form useEffect(() => { const isValid = @@ -225,7 +238,7 @@ export function FormDeploy({ title, authType, password, - allowedEmails, + allowedEmails.length, existingForm?.hasPassword, onValidationChange, inputFields.length, @@ -612,41 +625,27 @@ export function FormDeploy({ -
- {allowedEmails.map((email) => ( -
- {email} - -
- ))} - 0 ? 'Add another' : 'Enter emails or @domain.com' + { + const trimmed = value.trim().toLowerCase() + if (!trimmed || emailItems.some((item) => item.value === trimmed)) { + return false } - className='min-w-[150px] flex-1 border-none bg-transparent p-0 text-sm outline-none placeholder:text-[var(--text-muted)]' - onKeyDown={(e) => { - if (e.key === 'Enter' || e.key === ',') { - e.preventDefault() - const value = (e.target as HTMLInputElement).value.trim() - if (value && !allowedEmails.includes(value)) { - setAllowedEmails([...allowedEmails, value]) - clearError('emails') - ;(e.target as HTMLInputElement).value = '' - } - } - }} - /> -
+ const isDomainPattern = trimmed.startsWith('@') + const isValid = isDomainPattern || trimmed.includes('@') + setEmailItems((prev) => [...prev, { value: trimmed, isValid }]) + if (isValid) { + clearError('emails') + } + return isValid + }} + onRemove={(_value, index) => { + setEmailItems((prev) => prev.filter((_, i) => i !== index)) + }} + placeholder='Enter emails or @domain.com' + placeholderWithTags='Add another' + /> {errors.emails && (

{errors.emails}

)} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx index c2ca50fcaa..ceabafac3c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/document-tag-entry/document-tag-entry.tsx @@ -225,7 +225,7 @@ export function DocumentTagEntry({ */ const renderTagHeader = (tag: DocumentTag, index: number) => (
toggleCollapse(tag.id)} >
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx index 786ceba5f6..98f7df3ebb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/input-mapping/input-mapping.tsx @@ -373,7 +373,7 @@ function InputMappingField({ )} >
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx index 49125b125c..d669cedb59 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/knowledge-tag-filters/knowledge-tag-filters.tsx @@ -218,7 +218,7 @@ export function KnowledgeTagFilters({ */ const renderFilterHeader = (filter: TagFilter, index: number) => (
toggleCollapse(filter.id)} >
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx index a6874adacc..099dee906d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/starter/input-format.tsx @@ -163,7 +163,7 @@ export function FieldFormat({ */ const renderFieldHeader = (field: Field, index: number) => (
toggleCollapse(field.id)} >
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index caeaa12011..69169a6a6a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -2156,7 +2156,7 @@ export function ToolInput({ >
{ diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx index 6663ed0d4f..dba07e3c5f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/variables-input/variables-input.tsx @@ -318,7 +318,7 @@ export function VariablesInput({ )} >
toggleCollapse(assignment.id)} >
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 3e2428c593..3e9c048399 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger' import { ArrowUp, Square } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { + BubbleChatClose, BubbleChatPreview, Button, Copy, @@ -427,7 +428,7 @@ export function Panel() { variant={isChatOpen ? 'active' : 'default'} onClick={() => setIsChatOpen(!isChatOpen)} > - + {isChatOpen ? : }
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx index 98d037b6c5..ae1858f3df 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/terminal.tsx @@ -1240,7 +1240,7 @@ export function Terminal() { - Clear console + Clear console @@ -1605,7 +1605,7 @@ export function Terminal() { - Clear console + Clear console )} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/email-tag.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/email-tag.tsx deleted file mode 100644 index 2c63bb0a73..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/email-tag.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from 'react' -import { X } from 'lucide-react' -import { cn } from '@/lib/core/utils/cn' - -export interface EmailTagProps { - email: string - onRemove: () => void - disabled?: boolean - isInvalid?: boolean - isSent?: boolean -} - -export const EmailTag = React.memo( - ({ email, onRemove, disabled, isInvalid, isSent }) => ( -
- {email} - {isSent && sent} - {!disabled && !isSent && ( - - )} -
- ) -) - -EmailTag.displayName = 'EmailTag' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/index.ts index da6e909475..f58531afa1 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/components/index.ts @@ -1,4 +1,3 @@ -export * from './email-tag' export * from './permission-selector' export * from './permissions-table' export * from './permissions-table-skeleton' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx index 62450150f7..984db97263 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/invite-modal/invite-modal.tsx @@ -1,25 +1,25 @@ 'use client' -import React, { type KeyboardEvent, useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useRef, useState } from 'react' import { createLogger } from '@sim/logger' import { useParams } from 'next/navigation' import { Button, - Input, Label, Modal, ModalBody, ModalContent, ModalFooter, ModalHeader, + TagInput, + type TagItem, } from '@/components/emcn' import { useSession } from '@/lib/auth/auth-client' -import { cn } from '@/lib/core/utils/cn' import { quickValidateEmail } from '@/lib/messaging/email/validation' import { useWorkspacePermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider' import { API_ENDPOINTS } from '@/stores/constants' import type { PermissionType, UserPermissions } from './components' -import { EmailTag, PermissionsTable } from './components' +import { PermissionsTable } from './components' const logger = createLogger('InviteModal') @@ -40,9 +40,7 @@ interface PendingInvitation { export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalProps) { const formRef = useRef(null) - const [inputValue, setInputValue] = useState('') - const [emails, setEmails] = useState([]) - const [invalidEmails, setInvalidEmails] = useState([]) + const [emailItems, setEmailItems] = useState([]) const [userPermissions, setUserPermissions] = useState([]) const [pendingInvitations, setPendingInvitations] = useState([]) const [isPendingInvitationsLoading, setIsPendingInvitationsLoading] = useState(false) @@ -79,7 +77,8 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr } = useWorkspacePermissionsContext() const hasPendingChanges = Object.keys(existingUserPermissionChanges).length > 0 - const hasNewInvites = emails.length > 0 || inputValue.trim() + const validEmails = emailItems.filter((item) => item.isValid).map((item) => item.value) + const hasNewInvites = validEmails.length > 0 const fetchPendingInvitations = useCallback(async () => { if (!workspaceId) return @@ -134,14 +133,13 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr const validation = quickValidateEmail(normalized) const isValid = validation.isValid - if (emails.includes(normalized) || invalidEmails.includes(normalized)) { + if (emailItems.some((item) => item.value === normalized)) { return false } const hasPendingInvitation = pendingInvitations.some((inv) => inv.email === normalized) if (hasPendingInvitation) { setErrorMessage(`${normalized} already has a pending invitation`) - setInputValue('') return false } @@ -150,52 +148,43 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr ) if (isExistingMember) { setErrorMessage(`${normalized} is already a member of this workspace`) - setInputValue('') return false } if (session?.user?.email && session.user.email.toLowerCase() === normalized) { setErrorMessage('You cannot invite yourself') - setInputValue('') return false } - if (!isValid) { - setInvalidEmails((prev) => [...prev, normalized]) - setInputValue('') - return false + setEmailItems((prev) => [...prev, { value: normalized, isValid }]) + + if (isValid) { + setErrorMessage(null) + setUserPermissions((prev) => [ + ...prev, + { + email: normalized, + permissionType: 'read', + }, + ]) } - setErrorMessage(null) - setEmails((prev) => [...prev, normalized]) - - setUserPermissions((prev) => [ - ...prev, - { - email: normalized, - permissionType: 'read', - }, - ]) - - setInputValue('') - return true + return isValid }, - [emails, invalidEmails, pendingInvitations, workspacePermissions?.users, session?.user?.email] + [emailItems, pendingInvitations, workspacePermissions?.users, session?.user?.email] ) - const removeEmail = useCallback( - (index: number) => { - const emailToRemove = emails[index] - setEmails((prev) => prev.filter((_, i) => i !== index)) - setUserPermissions((prev) => prev.filter((user) => user.email !== emailToRemove)) + const removeEmailItem = useCallback( + (_value: string, index: number, isValid?: boolean) => { + const itemToRemove = emailItems[index] + setEmailItems((prev) => prev.filter((_, i) => i !== index)) + if (isValid ?? itemToRemove?.isValid) { + setUserPermissions((prev) => prev.filter((user) => user.email !== itemToRemove?.value)) + } }, - [emails] + [emailItems] ) - const removeInvalidEmail = useCallback((index: number) => { - setInvalidEmails((prev) => prev.filter((_, i) => i !== index)) - }, []) - const handlePermissionChange = useCallback( (identifier: string, permissionType: PermissionType) => { const existingUser = workspacePermissions?.users?.find((user) => user.userId === identifier) @@ -472,57 +461,15 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr [workspaceId, userPerms.canAdmin, resendCooldowns] ) - const handleKeyDown = useCallback( - (e: KeyboardEvent) => { - if (['Enter', ',', ' '].includes(e.key) && inputValue.trim()) { - e.preventDefault() - addEmail(inputValue) - } - - if (e.key === 'Backspace' && !inputValue) { - if (invalidEmails.length > 0) { - removeInvalidEmail(invalidEmails.length - 1) - } else if (emails.length > 0) { - removeEmail(emails.length - 1) - } - } - }, - [inputValue, addEmail, invalidEmails, emails, removeInvalidEmail, removeEmail] - ) - - const handlePaste = useCallback( - (e: React.ClipboardEvent) => { - e.preventDefault() - const pastedText = e.clipboardData.getData('text') - const pastedEmails = pastedText.split(/[\s,;]+/).filter(Boolean) - - let addedCount = 0 - pastedEmails.forEach((email) => { - if (addEmail(email)) { - addedCount++ - } - }) - - if (addedCount === 0 && pastedEmails.length === 1) { - setInputValue(inputValue + pastedEmails[0]) - } - }, - [addEmail, inputValue] - ) - const handleSubmit = useCallback( async (e: React.FormEvent) => { e.preventDefault() - if (inputValue.trim()) { - addEmail(inputValue) - } - // Clear messages at start of submission setErrorMessage(null) setSuccessMessage(null) - if (emails.length === 0 || !workspaceId) { + if (validEmails.length === 0 || !workspaceId) { return } @@ -532,7 +479,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr const failedInvites: string[] = [] const results = await Promise.all( - emails.map(async (email) => { + validEmails.map(async (email) => { try { const userPermission = userPermissions.find((up) => up.email === email) const permissionType = userPermission?.permissionType || 'read' @@ -553,9 +500,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr const data = await response.json() if (!response.ok) { - if (!invalidEmails.includes(email)) { - failedInvites.push(email) - } + failedInvites.push(email) if (data.error) { setErrorMessage(data.error) @@ -566,16 +511,14 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr return true } catch { - if (!invalidEmails.includes(email)) { - failedInvites.push(email) - } + failedInvites.push(email) return false } }) ) const successCount = results.filter(Boolean).length - const successfulEmails = emails.filter((_, index) => results[index]) + const successfulEmails = validEmails.filter((_, index) => results[index]) if (successCount > 0) { if (successfulEmails.length > 0) { @@ -605,17 +548,14 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr } fetchPendingInvitations() - setInputValue('') if (failedInvites.length > 0) { - setEmails(failedInvites) + setEmailItems(failedInvites.map((email) => ({ value: email, isValid: true }))) setUserPermissions((prev) => prev.filter((user) => failedInvites.includes(user.email))) } else { - setEmails([]) + setEmailItems([]) setUserPermissions([]) } - - setInvalidEmails([]) setShowSent(true) setTimeout(() => { @@ -631,23 +571,12 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr setIsSubmitting(false) } }, - [ - inputValue, - addEmail, - emails, - workspaceId, - userPermissions, - invalidEmails, - fetchPendingInvitations, - onOpenChange, - ] + [validEmails, workspaceId, userPermissions, fetchPendingInvitations] ) const resetState = useCallback(() => { // Batch state updates using React's automatic batching in React 18+ - setInputValue('') - setEmails([]) - setInvalidEmails([]) + setEmailItems([]) setUserPermissions([]) setPendingInvitations([]) setIsPendingInvitationsLoading(false) @@ -718,55 +647,21 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr tabIndex={-1} readOnly /> -
- {invalidEmails.map((email, index) => ( - removeInvalidEmail(index)} - disabled={isSubmitting || !userPerms.canAdmin} - isInvalid={true} - /> - ))} - {emails.map((email, index) => ( - removeEmail(index)} - disabled={isSubmitting || !userPerms.canAdmin} - /> - ))} - setInputValue(e.target.value)} - onKeyDown={handleKeyDown} - onPaste={handlePaste} - onBlur={() => inputValue.trim() && addEmail(inputValue)} - placeholder={ - !userPerms.canAdmin - ? 'Only administrators can invite new members' - : emails.length > 0 || invalidEmails.length > 0 - ? 'Add another email' - : 'Enter emails' - } - className={cn( - 'h-6 min-w-[180px] flex-1 border-none bg-transparent p-0 text-[13px] focus-visible:ring-0 focus-visible:ring-offset-0', - emails.length > 0 || invalidEmails.length > 0 ? 'pl-[4px]' : 'pl-[4px]' - )} - autoFocus={userPerms.canAdmin} - disabled={isSubmitting || !userPerms.canAdmin} - autoComplete='off' - autoCorrect='off' - autoCapitalize='off' - spellCheck={false} - data-lpignore='true' - data-form-type='other' - aria-autocomplete='none' - /> -
+ addEmail(value)} + onRemove={removeEmailItem} + placeholder={ + !userPerms.canAdmin + ? 'Only administrators can invite new members' + : 'Enter emails' + } + placeholderWithTags='Add email' + autoFocus={userPerms.canAdmin} + disabled={isSubmitting || !userPerms.canAdmin} + />
{errorMessage && (

{errorMessage}

diff --git a/apps/sim/components/emcn/components/index.ts b/apps/sim/components/emcn/components/index.ts index 3997e6af64..70eec79c06 100644 --- a/apps/sim/components/emcn/components/index.ts +++ b/apps/sim/components/emcn/components/index.ts @@ -107,6 +107,15 @@ export { TableHeader, TableRow, } from './table/table' +export { + Tag, + TagInput, + type TagInputProps, + type TagItem, + type TagProps, + tagInputVariants, + tagVariants, +} from './tag-input/tag-input' export { Textarea } from './textarea/textarea' export { TimePicker, type TimePickerProps, timePickerVariants } from './time-picker/time-picker' export { Tooltip } from './tooltip/tooltip' diff --git a/apps/sim/components/emcn/components/tag-input/tag-input.tsx b/apps/sim/components/emcn/components/tag-input/tag-input.tsx new file mode 100644 index 0000000000..33f6358767 --- /dev/null +++ b/apps/sim/components/emcn/components/tag-input/tag-input.tsx @@ -0,0 +1,340 @@ +/** + * A tag input component for managing a list of tags with validation support. + * + * @example + * ```tsx + * import { TagInput, type TagItem } from '@/components/emcn' + * + * const [items, setItems] = useState([]) + * + * { + * const isValid = isValidEmail(value) + * setItems(prev => [...prev, { value, isValid }]) + * return isValid + * }} + * onRemove={(value, index) => { + * setItems(prev => prev.filter((_, i) => i !== index)) + * }} + * placeholder="Enter emails" + * /> + * ``` + */ +'use client' + +import * as React from 'react' +import { cva, type VariantProps } from 'class-variance-authority' +import { Plus, X } from 'lucide-react' +import { cn } from '@/lib/core/utils/cn' + +/** + * Variant styles for the Tag component. + * No vertical padding to fit within the input's natural line height without causing expansion. + * Uses colored badge-style variants (blue for valid, red for invalid). + */ +const tagVariants = cva( + 'flex w-auto cursor-default items-center gap-[3px] rounded-[4px] px-[4px] font-medium font-sans text-[13px] leading-[20px] transition-colors', + { + variants: { + variant: { + default: 'bg-[#bfdbfe] text-[#1d4ed8] dark:bg-[rgba(59,130,246,0.2)] dark:text-[#93c5fd]', + invalid: + 'bg-[#fecaca] text-[var(--text-error)] dark:bg-[#551a1a] dark:text-[var(--text-error)]', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +) + +/** + * Props for the Tag component. + */ +export interface TagProps extends VariantProps { + /** The tag value to display */ + value: string + /** Callback when remove button is clicked */ + onRemove?: () => void + /** Whether the tag is disabled */ + disabled?: boolean + /** Additional class names */ + className?: string + /** Optional suffix content (e.g., "sent" label) */ + suffix?: React.ReactNode +} + +/** + * A single tag badge with optional remove button. + */ +const Tag = React.memo(function Tag({ + value, + onRemove, + disabled, + variant, + className, + suffix, +}: TagProps) { + return ( +
+ {value} + {suffix} + {!disabled && onRemove && ( + + )} +
+ ) +}) + +/** + * Variant styles for the TagInput container. + * Matches the Input component styling exactly for consistent height. + */ +const tagInputVariants = cva( + 'scrollbar-hide flex w-full cursor-text 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] transition-colors focus-within:outline-none dark:bg-[var(--surface-5)]', + { + variants: { + variant: { + default: '', + }, + }, + defaultVariants: { + variant: 'default', + }, + } +) + +/** + * Represents a tag item with its value and validity status. + */ +export interface TagItem { + value: string + isValid: boolean +} + +/** + * Props for the TagInput component. + */ +export interface TagInputProps extends VariantProps { + /** Array of tag items with value and validity status */ + items: TagItem[] + /** + * Callback when a new tag is added. + * Return true if the value was valid and added, false if invalid. + */ + onAdd: (value: string) => boolean + /** Callback when a tag is removed (receives value, index, and isValid) */ + onRemove: (value: string, index: number, isValid: boolean) => void + /** Placeholder text for the input */ + placeholder?: string + /** Placeholder text when there are existing tags */ + placeholderWithTags?: string + /** Whether the input is disabled */ + disabled?: boolean + /** Additional class names for the container */ + className?: string + /** Additional class names for the input */ + inputClassName?: string + /** Maximum height for the container (defaults to 128px / max-h-32) */ + maxHeight?: string + /** HTML id for the input element */ + id?: string + /** HTML name for the input element */ + name?: string + /** Whether to auto-focus the input */ + autoFocus?: boolean + /** Custom keys that trigger tag addition (defaults to Enter, comma, space) */ + triggerKeys?: string[] + /** Optional render function for tag suffix content */ + renderTagSuffix?: (value: string, index: number) => React.ReactNode +} + +/** + * An input component for managing a list of tags. + * + * @remarks + * - Maintains consistent height with standard Input component + * - Supports keyboard navigation (Enter/comma/space to add, Backspace to remove) + * - Handles paste with multiple values separated by whitespace, commas, or semicolons + * - Displays invalid values with error styling + */ +const TagInput = React.forwardRef( + ( + { + items, + onAdd, + onRemove, + placeholder = 'Enter values', + placeholderWithTags = 'Add another', + disabled = false, + className, + inputClassName, + maxHeight = 'max-h-32', + id, + name, + autoFocus = false, + triggerKeys = ['Enter', ',', ' '], + renderTagSuffix, + variant, + }, + ref + ) => { + const [inputValue, setInputValue] = React.useState('') + const internalRef = React.useRef(null) + const inputRef = (ref as React.RefObject) || internalRef + + const hasItems = items.length > 0 + + React.useEffect(() => { + if (autoFocus && inputRef.current) { + inputRef.current.focus() + } + }, [autoFocus, inputRef]) + + const handleKeyDown = React.useCallback( + (e: React.KeyboardEvent) => { + if (triggerKeys.includes(e.key) && inputValue.trim()) { + e.preventDefault() + onAdd(inputValue.trim()) + setInputValue('') + } + + if (e.key === 'Backspace' && !inputValue && items.length > 0) { + const lastItem = items[items.length - 1] + onRemove(lastItem.value, items.length - 1, lastItem.isValid) + } + }, + [inputValue, triggerKeys, onAdd, items, onRemove] + ) + + const handlePaste = React.useCallback( + (e: React.ClipboardEvent) => { + e.preventDefault() + const pastedText = e.clipboardData.getData('text') + const pastedValues = pastedText.split(/[\s,;]+/).filter(Boolean) + + let addedCount = 0 + pastedValues.forEach((value) => { + if (onAdd(value.trim())) { + addedCount++ + } + }) + + if (addedCount === 0 && pastedValues.length === 1) { + setInputValue(inputValue + pastedValues[0]) + } + }, + [onAdd, inputValue] + ) + + const handleBlur = React.useCallback(() => { + if (inputValue.trim()) { + onAdd(inputValue.trim()) + setInputValue('') + } + }, [inputValue, onAdd]) + + const handleContainerClick = React.useCallback(() => { + inputRef.current?.focus() + }, [inputRef]) + + return ( +
+ {items.map((item, index) => ( + onRemove(item.value, index, item.isValid)} + disabled={disabled} + suffix={item.isValid ? renderTagSuffix?.(item.value, index) : undefined} + /> + ))} +
+
+ {inputValue.trim() && ( + + )} + setInputValue(e.target.value)} + onKeyDown={handleKeyDown} + onPaste={handlePaste} + onBlur={handleBlur} + placeholder={hasItems ? placeholderWithTags : placeholder} + size={hasItems ? placeholderWithTags?.length || 10 : placeholder?.length || 12} + className={cn( + 'border-none bg-transparent font-medium font-sans outline-none placeholder:text-[var(--text-muted)] disabled:cursor-not-allowed disabled:opacity-50', + inputValue.trim() + ? 'absolute top-0 left-0 h-full w-full p-0 text-[13px] text-inherit leading-[20px]' + : 'w-auto min-w-0 p-0 text-foreground text-sm', + inputClassName + )} + disabled={disabled} + autoComplete='off' + autoCorrect='off' + autoCapitalize='off' + spellCheck={false} + data-lpignore='true' + data-form-type='other' + aria-autocomplete='none' + /> +
+ {inputValue.trim() && ( + + )} +
+
+ ) + } +) + +TagInput.displayName = 'TagInput' + +export { Tag, TagInput, tagInputVariants, tagVariants } diff --git a/apps/sim/components/emcn/components/tooltip/tooltip.tsx b/apps/sim/components/emcn/components/tooltip/tooltip.tsx index f3c42c0c99..6cb91b1eee 100644 --- a/apps/sim/components/emcn/components/tooltip/tooltip.tsx +++ b/apps/sim/components/emcn/components/tooltip/tooltip.tsx @@ -57,9 +57,37 @@ const Content = React.forwardRef< )) Content.displayName = TooltipPrimitive.Content.displayName +interface ShortcutProps { + /** The keyboard shortcut keys to display (e.g., "⌘D", "⌘K") */ + keys: string + /** Optional additional class names */ + className?: string + /** Optional children to display before the shortcut */ + children?: React.ReactNode +} + +/** + * Displays a keyboard shortcut within tooltip content. + * + * @example + * ```tsx + * + * Clear console + * + * ``` + */ +const Shortcut = ({ keys, className, children }: ShortcutProps) => ( + + {children && {children}} + {keys} + +) +Shortcut.displayName = 'Tooltip.Shortcut' + export const Tooltip = { Root, Trigger, Content, Provider, + Shortcut, } diff --git a/apps/sim/components/emcn/icons/bubble-chat-close.tsx b/apps/sim/components/emcn/icons/bubble-chat-close.tsx new file mode 100644 index 0000000000..a8792b2ab0 --- /dev/null +++ b/apps/sim/components/emcn/icons/bubble-chat-close.tsx @@ -0,0 +1,28 @@ +import type { SVGProps } from 'react' + +/** + * BubbleChatClose icon component - chat bubble with solid eye to indicate closing + * @param props - SVG properties including className, fill, etc. + */ +export function BubbleChatClose(props: SVGProps) { + return ( + + + + + + ) +} diff --git a/apps/sim/components/emcn/icons/index.ts b/apps/sim/components/emcn/icons/index.ts index 2ffee636a3..9dc0cfb306 100644 --- a/apps/sim/components/emcn/icons/index.ts +++ b/apps/sim/components/emcn/icons/index.ts @@ -1,3 +1,4 @@ +export { BubbleChatClose } from './bubble-chat-close' export { BubbleChatPreview } from './bubble-chat-preview' export { Card } from './card' export { ChevronDown } from './chevron-down' diff --git a/apps/sim/hooks/queries/general-settings.ts b/apps/sim/hooks/queries/general-settings.ts index d46ee57371..c34705b50d 100644 --- a/apps/sim/hooks/queries/general-settings.ts +++ b/apps/sim/hooks/queries/general-settings.ts @@ -43,7 +43,7 @@ async function fetchGeneralSettings(): Promise { autoConnect: data.autoConnect ?? true, showTrainingControls: data.showTrainingControls ?? false, superUserModeEnabled: data.superUserModeEnabled ?? true, - theme: data.theme || 'system', + theme: data.theme || 'dark', telemetryEnabled: data.telemetryEnabled ?? true, billingUsageNotificationsEnabled: data.billingUsageNotificationsEnabled ?? true, errorNotificationsEnabled: data.errorNotificationsEnabled ?? true, diff --git a/apps/sim/lib/core/utils/theme.ts b/apps/sim/lib/core/utils/theme.ts index 5d7101ca7e..272a1ff1b6 100644 --- a/apps/sim/lib/core/utils/theme.ts +++ b/apps/sim/lib/core/utils/theme.ts @@ -38,6 +38,6 @@ export function syncThemeToNextThemes(theme: 'system' | 'light' | 'dark') { * Gets the current theme from next-themes localStorage */ export function getThemeFromNextThemes(): 'system' | 'light' | 'dark' { - if (typeof window === 'undefined') return 'system' - return (localStorage.getItem('sim-theme') as 'system' | 'light' | 'dark') || 'system' + if (typeof window === 'undefined') return 'dark' + return (localStorage.getItem('sim-theme') as 'system' | 'light' | 'dark') || 'dark' }