mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-06 21:54:01 -05:00
improvement(copilot): ui/ux; refactor: store dimensions (#2636)
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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)})`}
|
||||
>
|
||||
|
||||
@@ -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' />
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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}`}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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`
|
||||
)
|
||||
}
|
||||
},
|
||||
|
||||
@@ -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') {
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user