Skip to content

Commit c6047ef

Browse files
authored
fix(sidebar): align credits chip with panel toggle and square the collapsed rail (#6691)
* fix(sidebar): align credits chip with panel toggle and square the collapsed rail * fix(sidebar): center collapsed rail chips in the rail * fix(sidebar): shrink collapsed rail to 48px so chips center without shifting on toggle
1 parent 9aa36a4 commit c6047ef

11 files changed

Lines changed: 115 additions & 54 deletions

File tree

apps/sim/app/_styles/globals.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212
:root {
1313
--sidebar-width: 0px; /* 0 outside workspace; blocking script always sets actual value on workspace pages */
14-
--sidebar-collapsed-width: 51px; /* icon rail on web; desktop overrides to 0 before first paint */
14+
--sidebar-collapsed-width: 48px; /* icon rail on web; desktop overrides to 0 before first paint */
1515
--sidebar-expanded-width: 238px; /* SIDEBAR_WIDTH.DEFAULT; the width to restore to, held even while collapsed */
1616
--desktop-title-bar-height: 0px; /* macOS traffic-light lane; desktop overrides before first paint */
1717
--workspace-content-title-bar-inset: 0px; /* lane the content pane must leave clear; only non-zero when the pane, not the sidebar, sits under it */

apps/sim/app/layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ export default function RootLayout({ children }: { children: React.ReactNode })
7575
// The macOS desktop shell overlays native traffic lights on the
7676
// workspace. Mark it before first paint so the sidebar reserves
7777
// its inset title-bar lane without a post-hydration layout shift.
78-
var collapsedSidebarWidth = 51;
78+
var collapsedSidebarWidth = 48;
7979
try {
8080
if (window.simDesktop && /Mac/i.test(navigator.userAgent)) {
8181
document.documentElement.setAttribute('data-sim-desktop-title-bar', 'inset');

apps/sim/app/workspace/[workspaceId]/home/components/mothership-view/components/resource-tabs/resource-tab-controls.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,18 @@ export const RESOURCE_TAB_ICON_CLASS = 'size-[16px] text-[var(--text-icon)]'
77
/** Shared geometry for the resource header and controls positioned over it. */
88
export const RESOURCE_HEADER_CLASSES = {
99
layout:
10-
'[--resource-header-controls-height:43px] [--resource-header-end-inset:16px] [--resource-header-fixed-reserve:54px]',
10+
'[--resource-header-controls-height:43px] [--resource-header-end-inset:16px] [--resource-header-fixed-reserve:54px] [--resource-header-toggle-size:30px]',
1111
bar: 'h-[calc(var(--resource-header-controls-height)_+_1px)]',
12-
controls: 'h-[var(--resource-header-controls-height)]',
13-
contentTop: 'top-[8.5px]',
12+
overlay: 'absolute top-0 flex h-[var(--resource-header-controls-height)] items-center',
1413
startPadding: 'pl-[var(--resource-header-end-inset)]',
1514
endPadding: 'pr-[var(--resource-header-fixed-reserve)]',
1615
endPosition: 'right-[var(--resource-header-end-inset)]',
17-
adjacentEndPosition: 'right-[var(--resource-header-fixed-reserve)]',
16+
/**
17+
* Sits a control 1px clear of the overlaid 30px collapse toggle — the same
18+
* chip-to-chip gap the sidebar header cluster uses (`gap-[1px]`), so the
19+
* credits chip and the toggle read as one cluster across both surfaces.
20+
*/
21+
adjacentEndPosition:
22+
'right-[calc(var(--resource-header-end-inset)_+_var(--resource-header-toggle-size)_+_1px)]',
1823
emptyAddOffset: '-translate-x-1.5',
1924
} as const

apps/sim/app/workspace/[workspaceId]/home/home.tsx

Lines changed: 32 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -283,21 +283,32 @@ export function Home({ chatId, userName, userId, tableViewsEnabled }: HomeProps)
283283
setIsResourceCollapsed(true)
284284
}, [clearWidth])
285285

286+
const clearResourceActivity = useCallback((resourceId: string) => {
287+
setResourceActivityIds((current) => {
288+
if (!current.has(resourceId)) return current
289+
const next = new Set(current)
290+
next.delete(resourceId)
291+
return next
292+
})
293+
}, [])
294+
295+
const expandResource = () => {
296+
userOwnsResourceViewRef.current = true
297+
const activeResourceId = activeResourceParamRef.current
298+
if (activeResourceId) clearResourceActivity(activeResourceId)
299+
setIsResourceCollapsed(false)
300+
}
301+
286302
const selectResourceFromUser = useCallback(
287303
(resourceId: string) => {
288304
userOwnsResourceViewRef.current = true
289-
setResourceActivityIds((current) => {
290-
if (!current.has(resourceId)) return current
291-
const next = new Set(current)
292-
next.delete(resourceId)
293-
return next
294-
})
305+
clearResourceActivity(resourceId)
295306
if (effectiveActiveResourceIdRef.current === resourceId) return
296307
effectiveActiveResourceIdRef.current = resourceId
297308
activeResourceParamRef.current = resourceId
298309
setActiveResourceId(resourceId)
299310
},
300-
[setActiveResourceId]
311+
[setActiveResourceId, clearResourceActivity]
301312
)
302313

303314
const addResourceFromUser = useCallback(
@@ -581,9 +592,17 @@ export function Home({ chatId, userName, userId, tableViewsEnabled }: HomeProps)
581592
{showEmptyState && (
582593
<div
583594
className={cn(
584-
'absolute z-10',
585-
RESOURCE_HEADER_CLASSES.contentTop,
586-
RESOURCE_HEADER_CLASSES.adjacentEndPosition
595+
'z-10',
596+
RESOURCE_HEADER_CLASSES.overlay,
597+
// Collapsed, the expand toggle overlays this corner, so the chip
598+
// yields the fixed reserve; open, the toggle lives in the panel's
599+
// corner and the chip takes the standard end inset itself.
600+
isResourceCollapsed
601+
? RESOURCE_HEADER_CLASSES.adjacentEndPosition
602+
: RESOURCE_HEADER_CLASSES.endPosition,
603+
skipResourceTransition
604+
? 'transition-none'
605+
: 'transition-[right] duration-200 [transition-timing-function:cubic-bezier(0.25,0.1,0.25,1)]'
587606
)}
588607
>
589608
<CreditsChip />
@@ -690,34 +709,14 @@ export function Home({ chatId, userName, userId, tableViewsEnabled }: HomeProps)
690709
</MothershipResourcesProvider>
691710

692711
<div
693-
className={cn(
694-
'absolute top-0 z-30 flex items-center',
695-
RESOURCE_HEADER_CLASSES.controls,
696-
RESOURCE_HEADER_CLASSES.endPosition
697-
)}
712+
className={cn('z-30', RESOURCE_HEADER_CLASSES.overlay, RESOURCE_HEADER_CLASSES.endPosition)}
698713
>
699714
<Button
700715
variant='ghost'
701716
size={null}
702717
type='button'
703-
onClick={
704-
isResourceCollapsed
705-
? () => {
706-
userOwnsResourceViewRef.current = true
707-
const activeResourceId = activeResourceParamRef.current
708-
if (activeResourceId) {
709-
setResourceActivityIds((current) => {
710-
if (!current.has(activeResourceId)) return current
711-
const next = new Set(current)
712-
next.delete(activeResourceId)
713-
return next
714-
})
715-
}
716-
setIsResourceCollapsed(false)
717-
}
718-
: collapseResource
719-
}
720-
className='size-[30px] rounded-[8px] hover-hover:bg-[var(--surface-active)]'
718+
onClick={isResourceCollapsed ? expandResource : collapseResource}
719+
className='size-[var(--resource-header-toggle-size)] rounded-[8px] hover-hover:bg-[var(--surface-active)]'
721720
aria-label={isResourceCollapsed ? 'Expand resource view' : 'Collapse resource view'}
722721
>
723722
<span className='relative'>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/collapsed-sidebar-menu/collapsed-sidebar-menu.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { File, Folder, MoreHorizontal, Pencil, Plus, SquareArrowUpRight } from '
1616
import Link from 'next/link'
1717
import type { WorkspaceFileRecord } from '@/lib/uploads/contexts/workspace'
1818
import { ConversationListItem } from '@/app/workspace/[workspaceId]/components'
19+
import { SIDEBAR_RAIL_CHIP_CLASS } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
1920
import type { useHoverMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
2021
import { interleaveSiblings } from '@/app/workspace/[workspaceId]/w/components/sidebar/utils'
2122
import type { WorkspaceFileFolderApi } from '@/hooks/queries/workspace-file-folders'
@@ -209,7 +210,7 @@ export function CollapsedSidebarMenu({
209210
<button
210211
type='button'
211212
aria-label={ariaLabel}
212-
className={chipVariants({ fullWidth: true })}
213+
className={cn(chipVariants({ fullWidth: true }), SIDEBAR_RAIL_CHIP_CLASS)}
213214
>
214215
{icon}
215216
</button>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-sidebar/settings-sidebar.tsx

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
SIDEBAR_DIVIDER_PAD_ABOVE_CLASS,
3232
SIDEBAR_DIVIDER_PAD_BELOW_CLASS,
3333
SIDEBAR_ITEM_GAP_CLASS,
34+
SIDEBAR_RAIL_CHIP_CLASS,
3435
SIDEBAR_SECTION_GAP_CLASS,
3536
} from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
3637
import { SidebarTooltip } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
@@ -305,7 +306,11 @@ export function SettingsSidebar({
305306
)}
306307
>
307308
<SidebarTooltip label='Back' enabled={showCollapsedTooltips}>
308-
<button type='button' onClick={handleBack} className={chipVariants({ fullWidth: true })}>
309+
<button
310+
type='button'
311+
onClick={handleBack}
312+
className={cn(chipVariants({ fullWidth: true }), SIDEBAR_RAIL_CHIP_CLASS)}
313+
>
309314
{/* The 16px slot every settings row gives its icon, so Back's label starts on their baseline. */}
310315
<span aria-hidden className={cn(chipIconSlotClass, 'text-[var(--text-icon)]')}>
311316
<ChevronLeft className='size-[14px]' />
@@ -352,7 +357,10 @@ export function SettingsSidebar({
352357
(item.id === 'inbox'
353358
? !inboxEntitled
354359
: !subscriptionAccess.hasUsableMaxAccess)
355-
const itemClassName = chipVariants({ active, fullWidth: true })
360+
const itemClassName = cn(
361+
chipVariants({ active, fullWidth: true }),
362+
SIDEBAR_RAIL_CHIP_CLASS
363+
)
356364
const content = (
357365
<>
358366
<Icon className={chipContentIconClass} />

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-footer/sidebar-footer.tsx

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,10 @@ import { getDesktopUpdates } from '@/lib/desktop'
2424
import { getUserColor } from '@/lib/workspaces/colors'
2525
import { useWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider'
2626
import type { SettingsSection } from '@/app/workspace/[workspaceId]/settings/navigation'
27-
import { SIDEBAR_ITEM_GAP_CLASS } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
27+
import {
28+
SIDEBAR_ITEM_GAP_CLASS,
29+
SIDEBAR_RAIL_CHIP_CLASS,
30+
} from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
2831
import { SidebarTooltip } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar'
2932
import { useUserProfile } from '@/hooks/queries/user-profile'
3033
import { useWorkspaceInvitePolicy } from '@/hooks/use-workspace-invite-policy'
@@ -237,11 +240,11 @@ export function SidebarFooter({
237240
<button
238241
type='button'
239242
data-item-id='profile'
240-
className={
241-
isCollapsed
242-
? cn(chipVariants({ fullWidth: true }), 'min-w-0')
243-
: cn(chipVariants(), 'max-w-full')
244-
}
243+
className={cn(
244+
chipVariants({ fullWidth: isCollapsed }),
245+
isCollapsed ? 'min-w-0' : 'max-w-full',
246+
SIDEBAR_RAIL_CHIP_CLASS
247+
)}
245248
>
246249
{avatar}
247250
{profile ? (
@@ -293,7 +296,7 @@ export function SidebarFooter({
293296
than the rail, and a shrinking chip would be squeezed onto the avatar.
294297
Holding its size pushes it past the edge, where the aside's clip hides
295298
it until there is room. */
296-
className='flex-shrink-0'
299+
className={cn('flex-shrink-0', SIDEBAR_RAIL_CHIP_CLASS)}
297300
/>
298301
</DropdownMenuTrigger>
299302
</SidebarTooltip>

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/workspace-header.tsx

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import { DeleteModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/
3030
import { CreateWorkspaceModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/create-workspace-modal/create-workspace-modal'
3131
import { ViewInvitationsMenuItem } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-menu-item'
3232
import { ViewInvitationsModal } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/workspace-header/components/pending-invitations/view-invitations-modal'
33+
import { SIDEBAR_RAIL_CHIP_CLASS } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
3334
import { invitationKeys } from '@/hooks/queries/invitations'
3435
import {
3536
type Workspace,
@@ -442,7 +443,7 @@ function WorkspaceHeaderImpl({
442443
type='button'
443444
aria-label='Expand sidebar'
444445
onClick={onExpandSidebar}
445-
className={chipVariants({ fullWidth: true })}
446+
className={cn(chipVariants({ fullWidth: true }), SIDEBAR_RAIL_CHIP_CLASS)}
446447
>
447448
<div className='relative flex size-[16px] flex-shrink-0 items-center justify-center'>
448449
{activeWorkspaceFull?.logoUrl ? (

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/constants.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,34 @@ export const SIDEBAR_ITEM_GAP_CLASS = 'gap-[1px]'
3030
export const SIDEBAR_DIVIDER_PAD_ABOVE_CLASS = 'pb-2'
3131
export const SIDEBAR_DIVIDER_PAD_BELOW_CLASS = 'pt-2'
3232

33+
/**
34+
* Rail-chip geometry for the collapsed sidebar, gated by the `group/rail`
35+
* marker on the sidebar `aside` (the hover-peek card drops `data-collapsed`,
36+
* so peeked rows stay expanded). Collapses a full-width row to the natural
37+
* icon-only chip — 32px, the chip's own `px-2` + 16px glyph, matching the
38+
* header's Search/Collapse pills. Only the WIDTH is overridden: the rail is
39+
* 48px precisely so that the plain 8px item gutter centers the chip
40+
* ((48 − 32) / 2 = 8) and puts the glyph (24px in) on the rail's midline —
41+
* the same 24px column the expanded rows use, so NOTHING moves on toggle.
42+
* At the previous 51px rail those goals were mutually exclusive by 1.5px
43+
* (rail midline 25.5 vs glyph column 24), which produced either a
44+
* left-biased rail or a drift on toggle; keep the rail width and this chip
45+
* width commensurate (rail = chip + 2 × gutter) if either ever changes.
46+
* Collapsing, the width tweens down to 32px on the 175ms curve the rail
47+
* closes on; expanding targets `auto` (not interpolable), so the chip snaps
48+
* to the still-narrow rail's width and stretch-tracks it open. The duration
49+
* is `!important` because the aside zeroes chip transition durations
50+
* (`[&_.group.cursor-pointer]:duration-0`) for instant hover fills — colors
51+
* are excluded from the property list here, so hover fills keep snapping.
52+
*/
53+
export const SIDEBAR_RAIL_CHIP_CLASS = [
54+
'transition-[width]',
55+
'![transition-duration:175ms]',
56+
'[transition-timing-function:cubic-bezier(0.25,0.1,0.25,1)]',
57+
'motion-reduce:!transition-none',
58+
'group-data-[collapsed]/rail:w-[32px]',
59+
].join(' ')
60+
3361
/**
3462
* Nested-selector variants for cmdk-based surfaces (e.g. the search modal).
3563
* Written as complete literal strings so Tailwind's JIT can detect them.

apps/sim/app/workspace/[workspaceId]/w/components/sidebar/sidebar.tsx

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ import {
7777
SIDEBAR_DIVIDER_PAD_ABOVE_CLASS,
7878
SIDEBAR_DIVIDER_PAD_BELOW_CLASS,
7979
SIDEBAR_ITEM_GAP_CLASS,
80+
SIDEBAR_RAIL_CHIP_CLASS,
8081
SIDEBAR_SECTION_GAP_CLASS,
8182
} from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
8283
import {
@@ -355,6 +356,7 @@ const SidebarNavItem = memo(function SidebarNavItem({
355356
leftIcon={item.icon}
356357
active={active}
357358
fullWidth
359+
className={SIDEBAR_RAIL_CHIP_CLASS}
358360
onClick={
359361
item.onClick
360362
? (e) => {
@@ -374,6 +376,7 @@ const SidebarNavItem = memo(function SidebarNavItem({
374376
leftIcon={item.icon}
375377
active={active}
376378
fullWidth
379+
className={SIDEBAR_RAIL_CHIP_CLASS}
377380
onClick={item.onClick}
378381
>
379382
{item.label}
@@ -1351,7 +1354,7 @@ export const Sidebar = memo(function Sidebar({
13511354
/>
13521355
<div className='relative h-full'>
13531356
<aside
1354-
className='sidebar-container relative h-full overflow-hidden bg-[var(--surface-1)] [&_.group.cursor-pointer]:duration-0'
1357+
className='group/rail sidebar-container relative h-full overflow-hidden bg-[var(--surface-1)] [&_.group.cursor-pointer]:duration-0'
13551358
data-collapsed={isCollapsed || undefined}
13561359
aria-label='Workspace sidebar'
13571360
onClick={handleSidebarClick}
@@ -1404,11 +1407,24 @@ export const Sidebar = memo(function Sidebar({
14041407
* between them. `gap-[1px]` rather than `gap-px`: the `px` spacing key
14051408
* is remapped to `--border-width`, which thins to 0.5px on hidpi so
14061409
* hairline rules stay hairlines.
1410+
*
1411+
* The expanded width is EXPLICIT (2 icon chips × 32px + the 1px gap;
1412+
* 32px when the desktop inset title bar hides the collapse chip), never
1413+
* `auto`: `w-0 → auto` cannot interpolate, so on expand the cluster
1414+
* snapped to full width while the rail was still 51px wide — and since
1415+
* the cluster refuses to flex-shrink (min-width: auto) while the
1416+
* workspace chip's wrapper is `min-w-0 flex-1`, the workspace chip
1417+
* crushed to zero and the hover-filled Search chip landed exactly under
1418+
* the cursor on the workspace icon: a visible flash on every expand.
1419+
* With both endpoints explicit, the width tweens in step with the rail
1420+
* and the workspace chip keeps its space throughout.
14071421
*/}
14081422
<div
14091423
className={cn(
1410-
'flex h-[30px] items-center gap-[1px] overflow-hidden transition-all duration-200',
1411-
isCollapsed && 'w-0 opacity-0'
1424+
'flex h-[30px] items-center gap-[1px] overflow-hidden transition-all duration-200 [transition-timing-function:cubic-bezier(0.25,0.1,0.25,1)]',
1425+
isCollapsed
1426+
? 'w-0 opacity-0'
1427+
: 'w-[65px] [[data-sim-desktop-title-bar=inset]_&]:w-[32px]'
14121428
)}
14131429
>
14141430
<SidebarTooltip

0 commit comments

Comments
 (0)