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.
* 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 {
--sidebar-width: 232px;
--panel-width: 260px;
--toolbar-triggers-height: 300px;
--editor-connections-height: 200px;
--terminal-height: 155px;
--sidebar-width: 232px; /* SIDEBAR_WIDTH.DEFAULT */
--panel-width: 290px; /* PANEL_WIDTH.DEFAULT */
--toolbar-triggers-height: 300px; /* TOOLBAR_TRIGGERS_HEIGHT.DEFAULT */
--editor-connections-height: 172px; /* EDITOR_CONNECTIONS_HEIGHT.DEFAULT */
--terminal-height: 155px; /* TERMINAL_HEIGHT.DEFAULT */
}
.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
id='workspace-layout-dimensions'
dangerouslySetInnerHTML={{
@@ -84,7 +89,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
var panelWidth = panelState && panelState.panelWidth;
var maxPanelWidth = window.innerWidth * 0.4;
if (panelWidth >= 260 && panelWidth <= maxPanelWidth) {
if (panelWidth >= 290 && panelWidth <= maxPanelWidth) {
document.documentElement.style.setProperty('--panel-width', panelWidth + 'px');
} else if (panelWidth > maxPanelWidth) {
document.documentElement.style.setProperty('--panel-width', maxPanelWidth + 'px');

View File

@@ -107,7 +107,7 @@ export const FileAttachmentDisplay = memo(({ fileAttachments }: FileAttachmentDi
{fileAttachments.map((file) => (
<div
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)}
title={`${file.filename} (${formatFileSize(file.size)})`}
>

View File

@@ -319,7 +319,7 @@ 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(--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
ref={messageContentRef}
@@ -350,7 +350,7 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
nodes.push(
<span
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}
</span>
@@ -365,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-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) */}
@@ -376,13 +376,12 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
e.stopPropagation()
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'
>
<svg
className='block h-[13px] w-[13px]'
className='block h-[13px] w-[13px] fill-white dark:fill-black'
viewBox='0 0 24 24'
fill='black'
xmlns='http://www.w3.org/2000/svg'
>
<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)}
<span>{option.label}</span>
{isMaxModel(option.value) && (
<Badge variant='default' className='ml-auto px-[6px] py-[1px] text-[10px]'>
<Badge size='sm' className='ml-auto'>
MAX
</Badge>
)}

View File

@@ -590,7 +590,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
elements.push(
<span
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}
</span>
@@ -619,7 +619,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
<div
ref={setInputContainerRef}
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)]'
)}
onDragEnter={fileAttachments.handleDragEnter}
@@ -746,7 +746,7 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
onClick={fileAttachments.handleFileSelect}
title='Attach file'
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'
)}
>
@@ -758,20 +758,19 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
onClick={handleAbort}
disabled={isAborting}
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
? 'bg-[var(--c-C0C0C0)] hover:bg-[var(--c-D0D0D0)]'
: 'bg-[var(--c-C0C0C0)]'
? 'bg-[var(--c-383838)] hover:bg-[var(--c-575757)] dark:bg-[var(--c-E0E0E0)] dark:hover:bg-[var(--c-CFCFCF)]'
: 'bg-[var(--c-383838)] dark:bg-[var(--c-E0E0E0)]'
)}
title='Stop generation'
>
{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
className='block h-[13px] w-[13px]'
className='block h-[13px] w-[13px] fill-white dark:fill-black'
viewBox='0 0 24 24'
fill='black'
xmlns='http://www.w3.org/2000/svg'
>
<rect x='4' y='4' width='16' height='16' rx='3' ry='3' />
@@ -785,16 +784,19 @@ const UserInput = forwardRef<UserInputRef, UserInputProps>(
}}
disabled={!canSubmit}
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
? 'bg-[var(--c-C0C0C0)] hover:bg-[var(--c-D0D0D0)]'
: 'bg-[var(--c-C0C0C0)]'
? 'bg-[var(--c-383838)] hover:bg-[var(--c-575757)] dark:bg-[var(--c-E0E0E0)] dark:hover:bg-[var(--c-CFCFCF)]'
: 'bg-[var(--c-808080)] dark:bg-[var(--c-808080)]'
)}
>
{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>
)}

View File

@@ -1,12 +1,7 @@
import { useCallback, useEffect, useState } from 'react'
import { PANEL_WIDTH } from '@/stores/constants'
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.
* Manages mouse events for resizing and enforces min/max width constraints.
@@ -35,9 +30,9 @@ export function usePanelResize() {
const handleMouseMove = (e: MouseEvent) => {
// Calculate width from the right edge of the viewport
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)
}
}

View File

@@ -1,7 +1,7 @@
import { useCallback, useEffect, useState } from 'react'
import { OUTPUT_PANEL_WIDTH } from '@/stores/constants'
import { useTerminalStore } from '@/stores/terminal'
const MIN_WIDTH = 440
const BLOCK_COLUMN_WIDTH = 240
export function useOutputPanelResize() {
@@ -26,7 +26,7 @@ export function useOutputPanelResize() {
const newWidth = window.innerWidth - e.clientX - panelWidth
const terminalWidth = window.innerWidth - sidebarWidth - panelWidth
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)
}

View File

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

View File

@@ -14,7 +14,6 @@ import {
ModalContent,
ModalFooter,
ModalHeader,
Slider,
Switch,
} from '@/components/emcn'
import { Input, Skeleton } from '@/components/ui'
@@ -70,7 +69,7 @@ function GeneralSkeleton() {
</div>
{/* 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-[17px] w-[30px] rounded-full' />
</div>
@@ -82,7 +81,7 @@ function GeneralSkeleton() {
</div>
{/* 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-[17px] w-[30px] rounded-full' />
</div>
@@ -134,8 +133,7 @@ export function General({ onOpenChange }: GeneralProps) {
const [uploadError, setUploadError] = useState<string | null>(null)
const [localSnapValue, setLocalSnapValue] = useState<number | null>(null)
const snapToGridValue = localSnapValue ?? settings?.snapToGridSize ?? 0
const snapToGridValue = settings?.snapToGridSize ?? 0
useEffect(() => {
if (profile?.name) {
@@ -295,16 +293,11 @@ export function General({ onOpenChange }: GeneralProps) {
}
}
const handleSnapToGridChange = (value: number[]) => {
setLocalSnapValue(value[0])
}
const handleSnapToGridCommit = async (value: number[]) => {
const newValue = value[0]
const handleSnapToGridChange = async (value: string) => {
const newValue = Number.parseInt(value, 10)
if (newValue !== settings?.snapToGridSize && !updateSetting.isPending) {
await updateSetting.mutateAsync({ key: 'snapToGridSize', value: newValue })
}
setLocalSnapValue(null)
}
const handleTrainingControlsChange = async (checked: boolean) => {
@@ -476,7 +469,7 @@ export function General({ onOpenChange }: GeneralProps) {
</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>
<Switch
id='auto-connect'
@@ -486,26 +479,7 @@ export function General({ onOpenChange }: GeneralProps) {
</div>
<div className='flex items-center justify-between'>
<Label htmlFor='snap-to-grid'>Snap to grid</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>
<Label htmlFor='error-notifications'>Workflow error notifications</Label>
<Switch
id='error-notifications'
checked={settings?.errorNotificationsEnabled ?? true}
@@ -513,7 +487,29 @@ export function General({ onOpenChange }: GeneralProps) {
/>
</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>
<Switch
id='telemetry'
@@ -522,9 +518,9 @@ export function General({ onOpenChange }: GeneralProps) {
/>
</div>
<p className='text-[12px] text-[var(--text-muted)]'>
We use OpenTelemetry to collect anonymous usage data to improve Sim. All data is collected
in accordance with our privacy policy, and you can opt-out at any time.
<p className='-mt-[8px] text-[12px] text-[var(--text-muted)]'>
We use OpenTelemetry to collect anonymous usage data to improve Sim. You can opt-out at any
time.
</p>
{isTrainingEnabled && (
@@ -561,11 +557,11 @@ export function General({ onOpenChange }: GeneralProps) {
</>
)}
<Button
onClick={() => window.open('/', '_blank', 'noopener,noreferrer')}
onClick={() => window.open('/?from=settings', '_blank', 'noopener,noreferrer')}
variant='active'
className='ml-auto'
>
Landing
Home Page
</Button>
</div>

View File

@@ -13,7 +13,8 @@ import {
import { getBillingStatus, getSubscriptionStatus, getUsage } from '@/lib/billing/client/utils'
import { useSocket } from '@/app/workspace/providers/socket-provider'
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')
@@ -172,7 +173,7 @@ function shouldShowPlanText(
const countDigits = (value: number): number => value.toFixed(2).replace(/\D/g, '').length
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)
let totalCost = usageDigits
@@ -256,7 +257,7 @@ export function UsageIndicator({ onClick }: UsageIndicatorProps) {
)
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 calculatedCount = PILL_CONFIG.MIN_COUNT + additionalPills
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}
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) => (
<EmailTag
key={`invalid-${index}`}

View File

@@ -1,10 +1,6 @@
import { useCallback, useEffect, useState } from 'react'
import { MIN_SIDEBAR_WIDTH, useSidebarStore } from '@/stores/sidebar/store'
/**
* Constants for sidebar sizing
*/
const MAX_WIDTH_PERCENTAGE = 0.3 // 30% of viewport width
import { SIDEBAR_WIDTH } from '@/stores/constants'
import { useSidebarStore } from '@/stores/sidebar/store'
/**
* Custom hook to handle sidebar resize functionality.
@@ -33,9 +29,9 @@ export function useSidebarResize() {
const handleMouseMove = (e: MouseEvent) => {
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)
}
}

View File

@@ -30,10 +30,11 @@ import {
useExportWorkspace,
useImportWorkspace,
} from '@/app/workspace/[workspaceId]/w/hooks'
import { SIDEBAR_WIDTH } from '@/stores/constants'
import { useFolderStore } from '@/stores/folders/store'
import { useSearchModalStore } from '@/stores/search-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')
@@ -250,7 +251,7 @@ export function Sidebar() {
if (isCollapsed) {
setIsCollapsed(false)
}
setSidebarWidth(MIN_SIDEBAR_WIDTH)
setSidebarWidth(SIDEBAR_WIDTH.MIN)
}
}, [isOnWorkflowPage, isCollapsed, setIsCollapsed, setSidebarWidth])

View File

@@ -59,3 +59,58 @@ export const COPILOT_TOOL_ERROR_NAMES: Record<string, string> = {
} as const
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 { persist } from 'zustand/middleware'
import { EDITOR_CONNECTIONS_HEIGHT } from '@/stores/constants'
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.
* Tracks the currently selected block to edit its subblocks/values and connections panel height.
@@ -38,7 +32,7 @@ export const usePanelEditorStore = create<PanelEditorState>()(
persist(
(set, get) => ({
currentBlockId: null,
connectionsHeight: DEFAULT_CONNECTIONS_HEIGHT,
connectionsHeight: EDITOR_CONNECTIONS_HEIGHT.DEFAULT,
setCurrentBlockId: (blockId) => {
set({ currentBlockId: blockId })
@@ -53,8 +47,8 @@ export const usePanelEditorStore = create<PanelEditorState>()(
},
setConnectionsHeight: (height) => {
const clampedHeight = Math.max(
MIN_CONNECTIONS_HEIGHT,
Math.min(MAX_CONNECTIONS_HEIGHT, height)
EDITOR_CONNECTIONS_HEIGHT.MIN,
Math.min(EDITOR_CONNECTIONS_HEIGHT.MAX, height)
)
set({ connectionsHeight: clampedHeight })
// Update CSS variable for immediate visual feedback
@@ -68,7 +62,9 @@ export const usePanelEditorStore = create<PanelEditorState>()(
toggleConnectionsCollapsed: () => {
const currentState = get()
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 })
@@ -88,7 +84,7 @@ export const usePanelEditorStore = create<PanelEditorState>()(
if (state && typeof window !== 'undefined') {
document.documentElement.style.setProperty(
'--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 { persist } from 'zustand/middleware'
import { PANEL_WIDTH } from '@/stores/constants'
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
*/
@@ -16,10 +11,10 @@ const DEFAULT_TAB: PanelTab = 'copilot'
export const usePanelStore = create<PanelState>()(
persist(
(set) => ({
panelWidth: MIN_PANEL_WIDTH,
panelWidth: PANEL_WIDTH.DEFAULT,
setPanelWidth: (width) => {
// 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 })
// Update CSS variable for immediate visual feedback
if (typeof window !== 'undefined') {

View File

@@ -1,13 +1,6 @@
import { create } from 'zustand'
import { persist } from 'zustand/middleware'
/**
* 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
import { TOOLBAR_TRIGGERS_HEIGHT } from '@/stores/constants'
/**
* Toolbar state interface
@@ -22,9 +15,12 @@ interface ToolbarState {
export const useToolbarStore = create<ToolbarState>()(
persist(
(set) => ({
toolbarTriggersHeight: DEFAULT_TOOLBAR_TRIGGERS_HEIGHT,
toolbarTriggersHeight: TOOLBAR_TRIGGERS_HEIGHT.DEFAULT,
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 })
// Update CSS variable for immediate visual feedback
if (typeof window !== 'undefined') {
@@ -44,7 +40,7 @@ export const useToolbarStore = create<ToolbarState>()(
if (state && typeof window !== 'undefined') {
document.documentElement.style.setProperty(
'--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 { persist } from 'zustand/middleware'
import { SIDEBAR_WIDTH } from '@/stores/constants'
/**
* Sidebar state interface
@@ -15,24 +16,17 @@ interface SidebarState {
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>()(
persist(
(set, get) => ({
workspaceDropdownOpen: false,
sidebarWidth: DEFAULT_SIDEBAR_WIDTH,
sidebarWidth: SIDEBAR_WIDTH.DEFAULT,
isCollapsed: false,
_hasHydrated: false,
setWorkspaceDropdownOpen: (isOpen) => set({ workspaceDropdownOpen: isOpen }),
setSidebarWidth: (width) => {
// 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 })
// Update CSS variable for immediate visual feedback
if (typeof window !== 'undefined') {

View File

@@ -1,3 +1,3 @@
export type { ConsoleEntry, ConsoleStore, ConsoleUpdate } 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 { persist } from 'zustand/middleware'
import { OUTPUT_PANEL_WIDTH, TERMINAL_HEIGHT } from '@/stores/constants'
/**
* Display mode type for terminal output.
@@ -41,22 +42,6 @@ interface TerminalState {
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.
*/
@@ -65,8 +50,8 @@ const DEFAULT_OUTPUT_PANEL_WIDTH = 440
export const useTerminalStore = create<TerminalState>()(
persist(
(set) => ({
terminalHeight: DEFAULT_TERMINAL_HEIGHT,
lastExpandedHeight: DEFAULT_TERMINAL_HEIGHT,
terminalHeight: TERMINAL_HEIGHT.DEFAULT,
lastExpandedHeight: TERMINAL_HEIGHT.DEFAULT,
isResizing: false,
/**
* 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.
*/
setTerminalHeight: (height) => {
const clampedHeight = Math.max(MIN_TERMINAL_HEIGHT, height)
const clampedHeight = Math.max(TERMINAL_HEIGHT.MIN, height)
set((state) => ({
terminalHeight: clampedHeight,
lastExpandedHeight:
clampedHeight > MIN_TERMINAL_HEIGHT ? clampedHeight : state.lastExpandedHeight,
clampedHeight > TERMINAL_HEIGHT.MIN ? clampedHeight : state.lastExpandedHeight,
}))
// Update CSS variable for immediate visual feedback
@@ -100,14 +85,14 @@ export const useTerminalStore = create<TerminalState>()(
setIsResizing: (isResizing) => {
set({ isResizing })
},
outputPanelWidth: DEFAULT_OUTPUT_PANEL_WIDTH,
outputPanelWidth: OUTPUT_PANEL_WIDTH.DEFAULT,
/**
* Updates the output panel width, enforcing the minimum constraint.
*
* @param width - Desired width in pixels for the output panel.
*/
setOutputPanelWidth: (width) => {
const clampedWidth = Math.max(MIN_OUTPUT_PANEL_WIDTH, width)
const clampedWidth = Math.max(OUTPUT_PANEL_WIDTH.MIN, width)
set({ outputPanelWidth: clampedWidth })
},
openOnRun: true,