improvement(copilot): ui/ux; refactor: store dimensions (#2636)

This commit is contained in:
Emir Karabeg
2025-12-29 20:42:42 -08:00
committed by GitHub
parent 97a9295230
commit a7a7c8601c
21 changed files with 178 additions and 158 deletions

View File

@@ -5,13 +5,16 @@
/** /**
* CSS-based sidebar and panel widths to prevent SSR hydration mismatches. * CSS-based sidebar and panel widths to prevent SSR hydration mismatches.
* Default widths are set here and updated via blocking script before React hydrates. * Default widths are set here and updated via blocking script before React hydrates.
*
* @important These values must stay in sync with stores/constants.ts
* @see stores/constants.ts for the source of truth
*/ */
:root { :root {
--sidebar-width: 232px; --sidebar-width: 232px; /* SIDEBAR_WIDTH.DEFAULT */
--panel-width: 260px; --panel-width: 290px; /* PANEL_WIDTH.DEFAULT */
--toolbar-triggers-height: 300px; --toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */
--editor-connections-height: 200px; --editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
--terminal-height: 155px; --terminal-height: 155px; /* TERMINAL_HEIGHT.DEFAULT */
} }
.sidebar-container { .sidebar-container {

View File

@@ -41,7 +41,12 @@ export default function RootLayout({ children }: { children: React.ReactNode })
}} }}
/> />
{/* Workspace layout dimensions: set CSS vars before hydration to avoid layout jump */} {/*
Workspace layout dimensions: set CSS vars before hydration to avoid layout jump.
IMPORTANT: These hardcoded values must stay in sync with stores/constants.ts
We cannot use imports here since this is a blocking script that runs before React.
*/}
<script <script
id='workspace-layout-dimensions' id='workspace-layout-dimensions'
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
@@ -84,7 +89,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
var panelWidth = panelState && panelState.panelWidth; var panelWidth = panelState && panelState.panelWidth;
var maxPanelWidth = window.innerWidth * 0.4; var maxPanelWidth = window.innerWidth * 0.4;
if (panelWidth >= 260 && panelWidth <= maxPanelWidth) { if (panelWidth >= 290 && panelWidth <= maxPanelWidth) {
document.documentElement.style.setProperty('--panel-width', panelWidth + 'px'); document.documentElement.style.setProperty('--panel-width', panelWidth + 'px');
} else if (panelWidth > maxPanelWidth) { } else if (panelWidth > maxPanelWidth) {
document.documentElement.style.setProperty('--panel-width', maxPanelWidth + 'px'); document.documentElement.style.setProperty('--panel-width', maxPanelWidth + 'px');

View File

@@ -107,7 +107,7 @@ export const FileAttachmentDisplay = memo(({ fileAttachments }: FileAttachmentDi
{fileAttachments.map((file) => ( {fileAttachments.map((file) => (
<div <div
key={file.id} key={file.id}
className='group relative h-16 w-16 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 cursor-pointer overflow-hidden rounded-md border border-[var(--border-1)] bg-muted/20 transition-all hover:bg-muted/40'
onClick={() => handleFileClick(file)} onClick={() => handleFileClick(file)}
title={`${file.filename} (${formatFileSize(file.size)})`} title={`${file.filename} (${formatFileSize(file.size)})`}
> >

View File

@@ -319,7 +319,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
onClick={handleMessageClick} onClick={handleMessageClick}
onMouseEnter={() => setIsHoveringMessage(true)} onMouseEnter={() => setIsHoveringMessage(true)}
onMouseLeave={() => setIsHoveringMessage(false)} onMouseLeave={() => setIsHoveringMessage(false)}
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)]' className='group relative w-full cursor-pointer rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-4)] px-[6px] py-[6px] transition-all duration-200 hover:border-[var(--surface-7)] hover:bg-[var(--surface-4)] dark:bg-[var(--surface-4)] dark:hover:border-[var(--surface-7)] dark:hover:bg-[var(--border-1)]'
> >
<div <div
ref={messageContentRef} ref={messageContentRef}
@@ -350,7 +350,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
nodes.push( nodes.push(
<span <span
key={`mention-${i}-${lastIndex}`} key={`mention-${i}-${lastIndex}`}
className='rounded-[6px] bg-[rgba(142,76,251,0.65)]' className='rounded-[4px] bg-[rgba(50,189,126,0.65)] py-[1px]'
> >
{mention} {mention}
</span> </span>
@@ -365,7 +365,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
{/* Gradient fade when truncated - applies to entire message box */} {/* Gradient fade when truncated - applies to entire message box */}
{!isExpanded && needsExpansion && ( {!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-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' /> <div className='pointer-events-none absolute right-0 bottom-0 left-0 h-6 bg-gradient-to-t from-0% from-[var(--surface-4)] via-25% via-[var(--surface-4)] to-100% to-transparent opacity-40 group-hover:opacity-30 dark:from-[var(--surface-4)] dark:via-[var(--surface-4)] dark:group-hover:from-[var(--border-1)] dark:group-hover:via-[var(--border-1)]' />
)} )}
{/* Abort button when hovering and response is generating (only on last user message) */} {/* Abort button when hovering and response is generating (only on last user message) */}
@@ -376,13 +376,12 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
e.stopPropagation() e.stopPropagation()
abortMessage() abortMessage()
}} }}
className='h-[20px] w-[20px] rounded-full bg-[var(--c-C0C0C0)] p-0 transition-colors hover:bg-[var(--c-D0D0D0)]' className='h-[20px] w-[20px] rounded-full border-0 bg-[var(--c-383838)] p-0 transition-colors hover:bg-[var(--c-575757)] dark:bg-[var(--c-E0E0E0)] dark:hover:bg-[var(--c-CFCFCF)]'
title='Stop generation' title='Stop generation'
> >
<svg <svg
className='block h-[13px] w-[13px]' className='block h-[13px] w-[13px] fill-white dark:fill-black'
viewBox='0 0 24 24' viewBox='0 0 24 24'
fill='black'
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
> >
<rect x='4' y='4' width='16' height='16' rx='3' ry='3' /> <rect x='4' y='4' width='16' height='16' rx='3' ry='3' />

View File

@@ -140,7 +140,7 @@ export function ModelSelector({ selectedModel, isNearTop, onModelSelect }: Model
{getModelIconComponent(option.value)} {getModelIconComponent(option.value)}
<span>{option.label}</span> <span>{option.label}</span>
{isMaxModel(option.value) && ( {isMaxModel(option.value) && (
<Badge variant='default' className='ml-auto px-[6px] py-[1px] text-[10px]'> <Badge size='sm' className='ml-auto'>
MAX MAX
</Badge> </Badge>
)} )}

View File

@@ -590,7 +590,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
elements.push( elements.push(
<span <span
key={`mention-${i}-${range.start}-${range.end}`} key={`mention-${i}-${range.start}-${range.end}`}
className='rounded-[6px] bg-[rgba(142,76,251,0.65)]' className='rounded-[4px] bg-[rgba(50,189,126,0.65)] py-[1px]'
> >
{mentionText} {mentionText}
</span> </span>
@@ -619,7 +619,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
<div <div
ref={setInputContainerRef} ref={setInputContainerRef}
className={cn( className={cn(
'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)]', 'relative w-full rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-4)] px-[6px] py-[6px] transition-colors dark:bg-[var(--surface-4)]',
fileAttachments.isDragging && 'ring-[1.75px] ring-[var(--brand-secondary)]' fileAttachments.isDragging && 'ring-[1.75px] ring-[var(--brand-secondary)]'
)} )}
onDragEnter={fileAttachments.handleDragEnter} onDragEnter={fileAttachments.handleDragEnter}
@@ -746,7 +746,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
onClick={fileAttachments.handleFileSelect} onClick={fileAttachments.handleFileSelect}
title='Attach file' title='Attach file'
className={cn( className={cn(
'cursor-pointer rounded-[6px] bg-transparent p-[0px] dark:bg-transparent', 'cursor-pointer rounded-[6px] border-0 bg-transparent p-[0px] dark:bg-transparent',
(disabled || isLoading) && 'cursor-not-allowed opacity-50' (disabled || isLoading) && 'cursor-not-allowed opacity-50'
)} )}
> >
@@ -758,20 +758,19 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
onClick={handleAbort} onClick={handleAbort}
disabled={isAborting} disabled={isAborting}
className={cn( className={cn(
'h-[20px] w-[20px] rounded-full p-0 transition-colors', 'h-[20px] w-[20px] rounded-full border-0 p-0 transition-colors',
!isAborting !isAborting
? 'bg-[var(--c-C0C0C0)] hover:bg-[var(--c-D0D0D0)]' ? 'bg-[var(--c-383838)] hover:bg-[var(--c-575757)] dark:bg-[var(--c-E0E0E0)] dark:hover:bg-[var(--c-CFCFCF)]'
: 'bg-[var(--c-C0C0C0)]' : 'bg-[var(--c-383838)] dark:bg-[var(--c-E0E0E0)]'
)} )}
title='Stop generation' title='Stop generation'
> >
{isAborting ? ( {isAborting ? (
<Loader2 className='block h-[13px] w-[13px] animate-spin text-black' /> <Loader2 className='block h-[13px] w-[13px] animate-spin text-white dark:text-black' />
) : ( ) : (
<svg <svg
className='block h-[13px] w-[13px]' className='block h-[13px] w-[13px] fill-white dark:fill-black'
viewBox='0 0 24 24' viewBox='0 0 24 24'
fill='black'
xmlns='http://www.w3.org/2000/svg' xmlns='http://www.w3.org/2000/svg'
> >
<rect x='4' y='4' width='16' height='16' rx='3' ry='3' /> <rect x='4' y='4' width='16' height='16' rx='3' ry='3' />
@@ -785,16 +784,19 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
}} }}
disabled={!canSubmit} disabled={!canSubmit}
className={cn( className={cn(
'h-[22px] w-[22px] rounded-full p-0 transition-colors', 'h-[22px] w-[22px] rounded-full border-0 p-0 transition-colors',
canSubmit canSubmit
? 'bg-[var(--c-C0C0C0)] hover:bg-[var(--c-D0D0D0)]' ? 'bg-[var(--c-383838)] hover:bg-[var(--c-575757)] dark:bg-[var(--c-E0E0E0)] dark:hover:bg-[var(--c-CFCFCF)]'
: 'bg-[var(--c-C0C0C0)]' : 'bg-[var(--c-808080)] dark:bg-[var(--c-808080)]'
)} )}
> >
{isLoading ? ( {isLoading ? (
<Loader2 className='block h-3.5 w-3.5 animate-spin text-black' /> <Loader2 className='block h-3.5 w-3.5 animate-spin text-white dark:text-black' />
) : ( ) : (
<ArrowUp className='block h-3.5 w-3.5 text-black' strokeWidth={2.25} /> <ArrowUp
className='block h-3.5 w-3.5 text-white dark:text-black'
strokeWidth={2.25}
/>
)} )}
</Button> </Button>
)} )}

View File

@@ -1,12 +1,7 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { PANEL_WIDTH } from '@/stores/constants'
import { usePanelStore } from '@/stores/panel/store' import { usePanelStore } from '@/stores/panel/store'
/**
* Constants for panel sizing
*/
const MIN_WIDTH = 244
const MAX_WIDTH_PERCENTAGE = 0.4 // 40% of viewport width
/** /**
* Custom hook to handle panel resize functionality. * Custom hook to handle panel resize functionality.
* Manages mouse events for resizing and enforces min/max width constraints. * Manages mouse events for resizing and enforces min/max width constraints.
@@ -35,9 +30,9 @@ export function usePanelResize() {
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
// Calculate width from the right edge of the viewport // Calculate width from the right edge of the viewport
const newWidth = window.innerWidth - e.clientX const newWidth = window.innerWidth - e.clientX
const maxWidth = window.innerWidth * MAX_WIDTH_PERCENTAGE const maxWidth = window.innerWidth * PANEL_WIDTH.MAX_PERCENTAGE
if (newWidth >= MIN_WIDTH && newWidth <= maxWidth) { if (newWidth >= PANEL_WIDTH.MIN && newWidth <= maxWidth) {
setPanelWidth(newWidth) setPanelWidth(newWidth)
} }
} }

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { OUTPUT_PANEL_WIDTH } from '@/stores/constants'
import { useTerminalStore } from '@/stores/terminal' import { useTerminalStore } from '@/stores/terminal'
const MIN_WIDTH = 440
const BLOCK_COLUMN_WIDTH = 240 const BLOCK_COLUMN_WIDTH = 240
export function useOutputPanelResize() { export function useOutputPanelResize() {
@@ -26,7 +26,7 @@ export function useOutputPanelResize() {
const newWidth = window.innerWidth - e.clientX - panelWidth const newWidth = window.innerWidth - e.clientX - panelWidth
const terminalWidth = window.innerWidth - sidebarWidth - panelWidth const terminalWidth = window.innerWidth - sidebarWidth - panelWidth
const maxWidth = terminalWidth - BLOCK_COLUMN_WIDTH const maxWidth = terminalWidth - BLOCK_COLUMN_WIDTH
const clampedWidth = Math.max(MIN_WIDTH, Math.min(newWidth, maxWidth)) const clampedWidth = Math.max(OUTPUT_PANEL_WIDTH.MIN, Math.min(newWidth, maxWidth))
setOutputPanelWidth(clampedWidth) setOutputPanelWidth(clampedWidth)
} }

View File

@@ -44,6 +44,7 @@ import {
useTerminalResize, useTerminalResize,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks' } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/terminal/hooks'
import { getBlock } from '@/blocks' import { getBlock } from '@/blocks'
import { OUTPUT_PANEL_WIDTH, TERMINAL_HEIGHT } from '@/stores/constants'
import { useCopilotTrainingStore } from '@/stores/copilot-training/store' import { useCopilotTrainingStore } from '@/stores/copilot-training/store'
import { useGeneralStore } from '@/stores/settings/general/store' import { useGeneralStore } from '@/stores/settings/general/store'
import type { ConsoleEntry } from '@/stores/terminal' import type { ConsoleEntry } from '@/stores/terminal'
@@ -53,15 +54,15 @@ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
/** /**
* Terminal height configuration constants * Terminal height configuration constants
*/ */
const MIN_HEIGHT = 30 const MIN_HEIGHT = TERMINAL_HEIGHT.MIN
const NEAR_MIN_THRESHOLD = 40 const NEAR_MIN_THRESHOLD = 40
const DEFAULT_EXPANDED_HEIGHT = 155 const DEFAULT_EXPANDED_HEIGHT = TERMINAL_HEIGHT.DEFAULT
/** /**
* Column width constants - numeric values for calculations * Column width constants - numeric values for calculations
*/ */
const BLOCK_COLUMN_WIDTH_PX = 240 const BLOCK_COLUMN_WIDTH_PX = 240
const MIN_OUTPUT_PANEL_WIDTH_PX = 440 const MIN_OUTPUT_PANEL_WIDTH_PX = OUTPUT_PANEL_WIDTH.MIN
/** /**
* Column width constants - Tailwind classes for styling * Column width constants - Tailwind classes for styling

View File

@@ -14,7 +14,6 @@ import {
ModalContent, ModalContent,
ModalFooter, ModalFooter,
ModalHeader, ModalHeader,
Slider,
Switch, Switch,
} from '@/components/emcn' } from '@/components/emcn'
import { Input, Skeleton } from '@/components/ui' import { Input, Skeleton } from '@/components/ui'
@@ -70,7 +69,7 @@ function GeneralSkeleton() {
</div> </div>
{/* Auto-connect row */} {/* Auto-connect row */}
<div className='flex items-center justify-between pt-[12px]'> <div className='flex items-center justify-between'>
<Skeleton className='h-4 w-36' /> <Skeleton className='h-4 w-36' />
<Skeleton className='h-[17px] w-[30px] rounded-full' /> <Skeleton className='h-[17px] w-[30px] rounded-full' />
</div> </div>
@@ -82,7 +81,7 @@ function GeneralSkeleton() {
</div> </div>
{/* Telemetry row */} {/* Telemetry row */}
<div className='flex items-center justify-between border-t pt-[12px]'> <div className='flex items-center justify-between border-t pt-[16px]'>
<Skeleton className='h-4 w-44' /> <Skeleton className='h-4 w-44' />
<Skeleton className='h-[17px] w-[30px] rounded-full' /> <Skeleton className='h-[17px] w-[30px] rounded-full' />
</div> </div>
@@ -134,8 +133,7 @@ export function General({ onOpenChange }: GeneralProps) {
const [uploadError, setUploadError] = useState<string | null>(null) const [uploadError, setUploadError] = useState<string | null>(null)
const [localSnapValue, setLocalSnapValue] = useState<number | null>(null) const snapToGridValue = settings?.snapToGridSize ?? 0
const snapToGridValue = localSnapValue ?? settings?.snapToGridSize ?? 0
useEffect(() => { useEffect(() => {
if (profile?.name) { if (profile?.name) {
@@ -295,16 +293,11 @@ export function General({ onOpenChange }: GeneralProps) {
} }
} }
const handleSnapToGridChange = (value: number[]) => { const handleSnapToGridChange = async (value: string) => {
setLocalSnapValue(value[0]) const newValue = Number.parseInt(value, 10)
}
const handleSnapToGridCommit = async (value: number[]) => {
const newValue = value[0]
if (newValue !== settings?.snapToGridSize && !updateSetting.isPending) { if (newValue !== settings?.snapToGridSize && !updateSetting.isPending) {
await updateSetting.mutateAsync({ key: 'snapToGridSize', value: newValue }) await updateSetting.mutateAsync({ key: 'snapToGridSize', value: newValue })
} }
setLocalSnapValue(null)
} }
const handleTrainingControlsChange = async (checked: boolean) => { const handleTrainingControlsChange = async (checked: boolean) => {
@@ -476,7 +469,7 @@ export function General({ onOpenChange }: GeneralProps) {
</div> </div>
</div> </div>
<div className='flex items-center justify-between pt-[12px]'> <div className='flex items-center justify-between'>
<Label htmlFor='auto-connect'>Auto-connect on drop</Label> <Label htmlFor='auto-connect'>Auto-connect on drop</Label>
<Switch <Switch
id='auto-connect' id='auto-connect'
@@ -486,26 +479,7 @@ export function General({ onOpenChange }: GeneralProps) {
</div> </div>
<div className='flex items-center justify-between'> <div className='flex items-center justify-between'>
<Label htmlFor='snap-to-grid'>Snap to grid</Label> <Label htmlFor='error-notifications'>Workflow error notifications</Label>
<div className='flex items-center gap-[12px]'>
<span className='w-[32px] text-right text-[12px] text-[var(--text-tertiary)]'>
{snapToGridValue === 0 ? 'Off' : `${snapToGridValue}px`}
</span>
<Slider
id='snap-to-grid'
value={[snapToGridValue]}
onValueChange={handleSnapToGridChange}
onValueCommit={handleSnapToGridCommit}
min={0}
max={50}
step={1}
className='w-[100px]'
/>
</div>
</div>
<div className='flex items-center justify-between'>
<Label htmlFor='error-notifications'>Run error notifications</Label>
<Switch <Switch
id='error-notifications' id='error-notifications'
checked={settings?.errorNotificationsEnabled ?? true} checked={settings?.errorNotificationsEnabled ?? true}
@@ -513,7 +487,29 @@ export function General({ onOpenChange }: GeneralProps) {
/> />
</div> </div>
<div className='flex items-center justify-between border-t pt-[12px]'> <div className='flex items-center justify-between'>
<Label htmlFor='snap-to-grid'>Snap to grid</Label>
<div className='w-[100px]'>
<Combobox
size='sm'
align='end'
dropdownWidth={140}
value={String(snapToGridValue)}
onChange={handleSnapToGridChange}
placeholder='Select size'
options={[
{ label: 'Off', value: '0' },
{ label: '10px', value: '10' },
{ label: '20px', value: '20' },
{ label: '30px', value: '30' },
{ label: '40px', value: '40' },
{ label: '50px', value: '50' },
]}
/>
</div>
</div>
<div className='flex items-center justify-between border-t pt-[16px]'>
<Label htmlFor='telemetry'>Allow anonymous telemetry</Label> <Label htmlFor='telemetry'>Allow anonymous telemetry</Label>
<Switch <Switch
id='telemetry' id='telemetry'
@@ -522,9 +518,9 @@ export function General({ onOpenChange }: GeneralProps) {
/> />
</div> </div>
<p className='text-[12px] text-[var(--text-muted)]'> <p className='-mt-[8px] text-[12px] text-[var(--text-muted)]'>
We use OpenTelemetry to collect anonymous usage data to improve Sim. All data is collected We use OpenTelemetry to collect anonymous usage data to improve Sim. You can opt-out at any
in accordance with our privacy policy, and you can opt-out at any time. time.
</p> </p>
{isTrainingEnabled && ( {isTrainingEnabled && (
@@ -561,11 +557,11 @@ export function General({ onOpenChange }: GeneralProps) {
</> </>
)} )}
<Button <Button
onClick={() => window.open('/', '_blank', 'noopener,noreferrer')} onClick={() => window.open('/?from=settings', '_blank', 'noopener,noreferrer')}
variant='active' variant='active'
className='ml-auto' className='ml-auto'
> >
Landing Home Page
</Button> </Button>
</div> </div>

View File

@@ -13,7 +13,8 @@ import {
import { getBillingStatus, getSubscriptionStatus, getUsage } from '@/lib/billing/client/utils' import { getBillingStatus, getSubscriptionStatus, getUsage } from '@/lib/billing/client/utils'
import { useSocket } from '@/app/workspace/providers/socket-provider' import { useSocket } from '@/app/workspace/providers/socket-provider'
import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription' import { subscriptionKeys, useSubscriptionData } from '@/hooks/queries/subscription'
import { MIN_SIDEBAR_WIDTH, useSidebarStore } from '@/stores/sidebar/store' import { SIDEBAR_WIDTH } from '@/stores/constants'
import { useSidebarStore } from '@/stores/sidebar/store'
const logger = createLogger('UsageIndicator') const logger = createLogger('UsageIndicator')
@@ -172,7 +173,7 @@ function shouldShowPlanText(
const countDigits = (value: number): number => value.toFixed(2).replace(/\D/g, '').length const countDigits = (value: number): number => value.toFixed(2).replace(/\D/g, '').length
const usageDigits = countDigits(usage.current) + countDigits(usage.limit) const usageDigits = countDigits(usage.current) + countDigits(usage.limit)
const extraWidth = Math.max(0, sidebarWidth - MIN_SIDEBAR_WIDTH) const extraWidth = Math.max(0, sidebarWidth - SIDEBAR_WIDTH.MIN)
const bonusDigits = Math.floor(extraWidth / WIDTH_COSTS.WIDTH_PER_EXTRA_DIGIT) const bonusDigits = Math.floor(extraWidth / WIDTH_COSTS.WIDTH_PER_EXTRA_DIGIT)
let totalCost = usageDigits let totalCost = usageDigits
@@ -256,7 +257,7 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
) )
const pillCount = useMemo(() => { const pillCount = useMemo(() => {
const widthDelta = sidebarWidth - MIN_SIDEBAR_WIDTH const widthDelta = sidebarWidth - SIDEBAR_WIDTH.MIN
const additionalPills = Math.floor(widthDelta / PILL_CONFIG.WIDTH_PER_PILL) const additionalPills = Math.floor(widthDelta / PILL_CONFIG.WIDTH_PER_PILL)
const calculatedCount = PILL_CONFIG.MIN_COUNT + additionalPills const calculatedCount = PILL_CONFIG.MIN_COUNT + additionalPills
return Math.max(PILL_CONFIG.MIN_COUNT, Math.min(PILL_CONFIG.MAX_COUNT, calculatedCount)) return Math.max(PILL_CONFIG.MIN_COUNT, Math.min(PILL_CONFIG.MAX_COUNT, calculatedCount))

View File

@@ -718,7 +718,7 @@ export function InviteModal({ open, onOpenChange, workspaceName }: InviteModalPr
tabIndex={-1} tabIndex={-1}
readOnly readOnly
/> />
<div className='scrollbar-hide flex max-h-32 min-h-9 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-[6px] py-[4px] focus-within:outline-none dark:bg-[var(--surface-5)]'> <div className='scrollbar-hide flex max-h-32 min-h-9 flex-wrap items-center gap-x-[8px] gap-y-[4px] overflow-y-auto rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-4)] px-[6px] py-[4px] focus-within:outline-none'>
{invalidEmails.map((email, index) => ( {invalidEmails.map((email, index) => (
<EmailTag <EmailTag
key={`invalid-${index}`} key={`invalid-${index}`}

View File

@@ -1,10 +1,6 @@
import { useCallback, useEffect, useState } from 'react' import { useCallback, useEffect, useState } from 'react'
import { MIN_SIDEBAR_WIDTH, useSidebarStore } from '@/stores/sidebar/store' import { SIDEBAR_WIDTH } from '@/stores/constants'
import { useSidebarStore } from '@/stores/sidebar/store'
/**
* Constants for sidebar sizing
*/
const MAX_WIDTH_PERCENTAGE = 0.3 // 30% of viewport width
/** /**
* Custom hook to handle sidebar resize functionality. * Custom hook to handle sidebar resize functionality.
@@ -33,9 +29,9 @@ export function useSidebarResize() {
const handleMouseMove = (e: MouseEvent) => { const handleMouseMove = (e: MouseEvent) => {
const newWidth = e.clientX const newWidth = e.clientX
const maxWidth = window.innerWidth * MAX_WIDTH_PERCENTAGE const maxWidth = window.innerWidth * SIDEBAR_WIDTH.MAX_PERCENTAGE
if (newWidth >= MIN_SIDEBAR_WIDTH && newWidth <= maxWidth) { if (newWidth >= SIDEBAR_WIDTH.MIN && newWidth <= maxWidth) {
setSidebarWidth(newWidth) setSidebarWidth(newWidth)
} }
} }

View File

@@ -30,10 +30,11 @@ import {
useExportWorkspace, useExportWorkspace,
useImportWorkspace, useImportWorkspace,
} from '@/app/workspace/[workspaceId]/w/hooks' } from '@/app/workspace/[workspaceId]/w/hooks'
import { SIDEBAR_WIDTH } from '@/stores/constants'
import { useFolderStore } from '@/stores/folders/store' import { useFolderStore } from '@/stores/folders/store'
import { useSearchModalStore } from '@/stores/search-modal/store' import { useSearchModalStore } from '@/stores/search-modal/store'
import { useSettingsModalStore } from '@/stores/settings-modal/store' import { useSettingsModalStore } from '@/stores/settings-modal/store'
import { MIN_SIDEBAR_WIDTH, useSidebarStore } from '@/stores/sidebar/store' import { useSidebarStore } from '@/stores/sidebar/store'
const logger = createLogger('Sidebar') const logger = createLogger('Sidebar')
@@ -250,7 +251,7 @@ export function Sidebar() {
if (isCollapsed) { if (isCollapsed) {
setIsCollapsed(false) setIsCollapsed(false)
} }
setSidebarWidth(MIN_SIDEBAR_WIDTH) setSidebarWidth(SIDEBAR_WIDTH.MIN)
} }
}, [isOnWorkflowPage, isCollapsed, setIsCollapsed, setSidebarWidth]) }, [isOnWorkflowPage, isCollapsed, setIsCollapsed, setSidebarWidth])

View File

@@ -59,3 +59,58 @@ export const COPILOT_TOOL_ERROR_NAMES: Record<string, string> = {
} as const } as const
export type CopilotToolId = keyof typeof COPILOT_TOOL_DISPLAY_NAMES export type CopilotToolId = keyof typeof COPILOT_TOOL_DISPLAY_NAMES
/**
* Layout dimension constants.
*
* These values must stay in sync with:
* - `globals.css` (CSS variable defaults)
* - `layout.tsx` (blocking script validations)
*
* @see globals.css for CSS variable definitions
* @see layout.tsx for pre-hydration script that reads localStorage
*/
/** Sidebar width constraints */
export const SIDEBAR_WIDTH = {
DEFAULT: 232,
MIN: 232,
/** Maximum is 30% of viewport, enforced dynamically */
MAX_PERCENTAGE: 0.3,
} as const
/** Right panel width constraints */
export const PANEL_WIDTH = {
DEFAULT: 290,
MIN: 290,
/** Maximum is 40% of viewport, enforced dynamically */
MAX_PERCENTAGE: 0.4,
} as const
/** Terminal height constraints */
export const TERMINAL_HEIGHT = {
DEFAULT: 155,
MIN: 30,
/** Maximum is 70% of viewport, enforced dynamically */
MAX_PERCENTAGE: 0.7,
} as const
/** Toolbar triggers section height constraints */
export const TOOLBAR_TRIGGERS_HEIGHT = {
DEFAULT: 300,
MIN: 30,
MAX: 800,
} as const
/** Editor connections section height constraints */
export const EDITOR_CONNECTIONS_HEIGHT = {
DEFAULT: 172,
MIN: 30,
MAX: 300,
} as const
/** Output panel (terminal execution results) width constraints */
export const OUTPUT_PANEL_WIDTH = {
DEFAULT: 440,
MIN: 440,
} as const

View File

@@ -2,15 +2,9 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import { EDITOR_CONNECTIONS_HEIGHT } from '@/stores/constants'
import { usePanelStore } from '../store' import { usePanelStore } from '../store'
/**
* Connections height constraints
*/
const DEFAULT_CONNECTIONS_HEIGHT = 172
const MIN_CONNECTIONS_HEIGHT = 30
const MAX_CONNECTIONS_HEIGHT = 300
/** /**
* State for the Editor panel. * State for the Editor panel.
* Tracks the currently selected block to edit its subblocks/values and connections panel height. * Tracks the currently selected block to edit its subblocks/values and connections panel height.
@@ -38,7 +32,7 @@ export const usePanelEditorStore = create<PanelEditorState>()(
persist( persist(
(set, get) => ({ (set, get) => ({
currentBlockId: null, currentBlockId: null,
connectionsHeight: DEFAULT_CONNECTIONS_HEIGHT, connectionsHeight: EDITOR_CONNECTIONS_HEIGHT.DEFAULT,
setCurrentBlockId: (blockId) => { setCurrentBlockId: (blockId) => {
set({ currentBlockId: blockId }) set({ currentBlockId: blockId })
@@ -53,8 +47,8 @@ export const usePanelEditorStore = create<PanelEditorState>()(
}, },
setConnectionsHeight: (height) => { setConnectionsHeight: (height) => {
const clampedHeight = Math.max( const clampedHeight = Math.max(
MIN_CONNECTIONS_HEIGHT, EDITOR_CONNECTIONS_HEIGHT.MIN,
Math.min(MAX_CONNECTIONS_HEIGHT, height) Math.min(EDITOR_CONNECTIONS_HEIGHT.MAX, height)
) )
set({ connectionsHeight: clampedHeight }) set({ connectionsHeight: clampedHeight })
// Update CSS variable for immediate visual feedback // Update CSS variable for immediate visual feedback
@@ -68,7 +62,9 @@ export const usePanelEditorStore = create<PanelEditorState>()(
toggleConnectionsCollapsed: () => { toggleConnectionsCollapsed: () => {
const currentState = get() const currentState = get()
const isAtMinHeight = currentState.connectionsHeight <= 35 const isAtMinHeight = currentState.connectionsHeight <= 35
const newHeight = isAtMinHeight ? DEFAULT_CONNECTIONS_HEIGHT : MIN_CONNECTIONS_HEIGHT const newHeight = isAtMinHeight
? EDITOR_CONNECTIONS_HEIGHT.DEFAULT
: EDITOR_CONNECTIONS_HEIGHT.MIN
set({ connectionsHeight: newHeight }) set({ connectionsHeight: newHeight })
@@ -88,7 +84,7 @@ export const usePanelEditorStore = create<PanelEditorState>()(
if (state && typeof window !== 'undefined') { if (state && typeof window !== 'undefined') {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
'--editor-connections-height', '--editor-connections-height',
`${state.connectionsHeight || DEFAULT_CONNECTIONS_HEIGHT}px` `${state.connectionsHeight || EDITOR_CONNECTIONS_HEIGHT.DEFAULT}px`
) )
} }
}, },

View File

@@ -1,13 +1,8 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import { PANEL_WIDTH } from '@/stores/constants'
import type { PanelState, PanelTab } from '@/stores/panel/types' import type { PanelState, PanelTab } from '@/stores/panel/types'
/**
* Panel width constraints
* Note: Maximum width is enforced dynamically at 40% of viewport width in the resize hook
*/
const MIN_PANEL_WIDTH = 260
/** /**
* Default panel tab * Default panel tab
*/ */
@@ -16,10 +11,10 @@ const DEFAULT_TAB: PanelTab = 'copilot'
export const usePanelStore = create<PanelState>()( export const usePanelStore = create<PanelState>()(
persist( persist(
(set) => ({ (set) => ({
panelWidth: MIN_PANEL_WIDTH, panelWidth: PANEL_WIDTH.DEFAULT,
setPanelWidth: (width) => { setPanelWidth: (width) => {
// Only enforce minimum - maximum is enforced dynamically by the resize hook // Only enforce minimum - maximum is enforced dynamically by the resize hook
const clampedWidth = Math.max(MIN_PANEL_WIDTH, width) const clampedWidth = Math.max(PANEL_WIDTH.MIN, width)
set({ panelWidth: clampedWidth }) set({ panelWidth: clampedWidth })
// Update CSS variable for immediate visual feedback // Update CSS variable for immediate visual feedback
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {

View File

@@ -1,13 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import { TOOLBAR_TRIGGERS_HEIGHT } from '@/stores/constants'
/**
* Toolbar triggers height constraints
* Minimum is set low to allow collapsing to just the header height (~30-40px)
*/
const DEFAULT_TOOLBAR_TRIGGERS_HEIGHT = 300
const MIN_TOOLBAR_HEIGHT = 30
const MAX_TOOLBAR_HEIGHT = 800
/** /**
* Toolbar state interface * Toolbar state interface
@@ -22,9 +15,12 @@ interface ToolbarState {
export const useToolbarStore = create<ToolbarState>()( export const useToolbarStore = create<ToolbarState>()(
persist( persist(
(set) => ({ (set) => ({
toolbarTriggersHeight: DEFAULT_TOOLBAR_TRIGGERS_HEIGHT, toolbarTriggersHeight: TOOLBAR_TRIGGERS_HEIGHT.DEFAULT,
setToolbarTriggersHeight: (height) => { setToolbarTriggersHeight: (height) => {
const clampedHeight = Math.max(MIN_TOOLBAR_HEIGHT, Math.min(MAX_TOOLBAR_HEIGHT, height)) const clampedHeight = Math.max(
TOOLBAR_TRIGGERS_HEIGHT.MIN,
Math.min(TOOLBAR_TRIGGERS_HEIGHT.MAX, height)
)
set({ toolbarTriggersHeight: clampedHeight }) set({ toolbarTriggersHeight: clampedHeight })
// Update CSS variable for immediate visual feedback // Update CSS variable for immediate visual feedback
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {
@@ -44,7 +40,7 @@ export const useToolbarStore = create<ToolbarState>()(
if (state && typeof window !== 'undefined') { if (state && typeof window !== 'undefined') {
document.documentElement.style.setProperty( document.documentElement.style.setProperty(
'--toolbar-triggers-height', '--toolbar-triggers-height',
`${state.toolbarTriggersHeight || DEFAULT_TOOLBAR_TRIGGERS_HEIGHT}px` `${state.toolbarTriggersHeight || TOOLBAR_TRIGGERS_HEIGHT.DEFAULT}px`
) )
} }
}, },

View File

@@ -1,5 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import { SIDEBAR_WIDTH } from '@/stores/constants'
/** /**
* Sidebar state interface * Sidebar state interface
@@ -15,24 +16,17 @@ interface SidebarState {
setHasHydrated: (hasHydrated: boolean) => void setHasHydrated: (hasHydrated: boolean) => void
} }
/**
* Sidebar width constraints
* Note: Maximum width is enforced dynamically at 30% of viewport width in the resize hook
*/
export const DEFAULT_SIDEBAR_WIDTH = 232
export const MIN_SIDEBAR_WIDTH = 232
export const useSidebarStore = create<SidebarState>()( export const useSidebarStore = create<SidebarState>()(
persist( persist(
(set, get) => ({ (set, get) => ({
workspaceDropdownOpen: false, workspaceDropdownOpen: false,
sidebarWidth: DEFAULT_SIDEBAR_WIDTH, sidebarWidth: SIDEBAR_WIDTH.DEFAULT,
isCollapsed: false, isCollapsed: false,
_hasHydrated: false, _hasHydrated: false,
setWorkspaceDropdownOpen: (isOpen) => set({ workspaceDropdownOpen: isOpen }), setWorkspaceDropdownOpen: (isOpen) => set({ workspaceDropdownOpen: isOpen }),
setSidebarWidth: (width) => { setSidebarWidth: (width) => {
// Only enforce minimum - maximum is enforced dynamically by the resize hook // Only enforce minimum - maximum is enforced dynamically by the resize hook
const clampedWidth = Math.max(MIN_SIDEBAR_WIDTH, width) const clampedWidth = Math.max(SIDEBAR_WIDTH.MIN, width)
set({ sidebarWidth: clampedWidth }) set({ sidebarWidth: clampedWidth })
// Update CSS variable for immediate visual feedback // Update CSS variable for immediate visual feedback
if (typeof window !== 'undefined') { if (typeof window !== 'undefined') {

View File

@@ -1,3 +1,3 @@
export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './console' export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } from './console'
export { useTerminalConsoleStore } from './console' export { useTerminalConsoleStore } from './console'
export { DEFAULT_TERMINAL_HEIGHT, MIN_TERMINAL_HEIGHT, useTerminalStore } from './store' export { useTerminalStore } from './store'

View File

@@ -1,5 +1,6 @@
import { create } from 'zustand' import { create } from 'zustand'
import { persist } from 'zustand/middleware' import { persist } from 'zustand/middleware'
import { OUTPUT_PANEL_WIDTH, TERMINAL_HEIGHT } from '@/stores/constants'
/** /**
* Display mode type for terminal output. * Display mode type for terminal output.
@@ -41,22 +42,6 @@ interface TerminalState {
setHasHydrated: (hasHydrated: boolean) => void setHasHydrated: (hasHydrated: boolean) => void
} }
/**
* Terminal height constraints.
*
* @remarks
* The maximum height is enforced dynamically at 70% of the viewport height
* inside the resize hook to keep the workflow canvas visible.
*/
export const MIN_TERMINAL_HEIGHT = 30
export const DEFAULT_TERMINAL_HEIGHT = 155
/**
* Output panel width constraints.
*/
const MIN_OUTPUT_PANEL_WIDTH = 440
const DEFAULT_OUTPUT_PANEL_WIDTH = 440
/** /**
* Default display mode for terminal output. * Default display mode for terminal output.
*/ */
@@ -65,8 +50,8 @@ const DEFAULT_OUTPUT_PANEL_WIDTH = 440
export const useTerminalStore = create<TerminalState>()( export const useTerminalStore = create<TerminalState>()(
persist( persist(
(set) => ({ (set) => ({
terminalHeight: DEFAULT_TERMINAL_HEIGHT, terminalHeight: TERMINAL_HEIGHT.DEFAULT,
lastExpandedHeight: DEFAULT_TERMINAL_HEIGHT, lastExpandedHeight: TERMINAL_HEIGHT.DEFAULT,
isResizing: false, isResizing: false,
/** /**
* Updates the terminal height and synchronizes the CSS custom property. * Updates the terminal height and synchronizes the CSS custom property.
@@ -79,12 +64,12 @@ export const useTerminalStore = create<TerminalState>()(
* @param height - Desired terminal height in pixels. * @param height - Desired terminal height in pixels.
*/ */
setTerminalHeight: (height) => { setTerminalHeight: (height) => {
const clampedHeight = Math.max(MIN_TERMINAL_HEIGHT, height) const clampedHeight = Math.max(TERMINAL_HEIGHT.MIN, height)
set((state) => ({ set((state) => ({
terminalHeight: clampedHeight, terminalHeight: clampedHeight,
lastExpandedHeight: lastExpandedHeight:
clampedHeight > MIN_TERMINAL_HEIGHT ? clampedHeight : state.lastExpandedHeight, clampedHeight > TERMINAL_HEIGHT.MIN ? clampedHeight : state.lastExpandedHeight,
})) }))
// Update CSS variable for immediate visual feedback // Update CSS variable for immediate visual feedback
@@ -100,14 +85,14 @@ export const useTerminalStore = create<TerminalState>()(
setIsResizing: (isResizing) => { setIsResizing: (isResizing) => {
set({ isResizing }) set({ isResizing })
}, },
outputPanelWidth: DEFAULT_OUTPUT_PANEL_WIDTH, outputPanelWidth: OUTPUT_PANEL_WIDTH.DEFAULT,
/** /**
* Updates the output panel width, enforcing the minimum constraint. * Updates the output panel width, enforcing the minimum constraint.
* *
* @param width - Desired width in pixels for the output panel. * @param width - Desired width in pixels for the output panel.
*/ */
setOutputPanelWidth: (width) => { setOutputPanelWidth: (width) => {
const clampedWidth = Math.max(MIN_OUTPUT_PANEL_WIDTH, width) const clampedWidth = Math.max(OUTPUT_PANEL_WIDTH.MIN, width)
set({ outputPanelWidth: clampedWidth }) set({ outputPanelWidth: clampedWidth })
}, },
openOnRun: true, openOnRun: true,