Skip to content

Commit 2c3f2f1

Browse files
committed
fix(mobile): align resource controls and compact touch surfaces
1 parent acbae50 commit 2c3f2f1

7 files changed

Lines changed: 137 additions & 79 deletions

File tree

‎apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-header/resource-header.tsx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,8 +178,8 @@ export const ResourceHeader = memo(function ResourceHeader({
178178
<div className='flex min-w-0 flex-1 flex-wrap items-center justify-between gap-2 md:flex-nowrap md:gap-3'>
179179
<div
180180
className={cn(
181-
'flex flex-1 items-center gap-2 overflow-hidden md:min-w-0',
182-
hasBreadcrumbs ? 'min-w-full' : 'min-w-[min(100%,9rem)]'
181+
'flex min-w-0 flex-1 items-center gap-2 overflow-hidden max-md:min-w-min',
182+
hasBreadcrumbs && 'max-md:min-w-full'
183183
)}
184184
>
185185
{hasBreadcrumbs ? (

‎apps/sim/app/workspace/[workspaceId]/components/resource/components/resource-options/resource-options.tsx‎

Lines changed: 40 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -148,23 +148,52 @@ export const ResourceOptions = memo(function ResourceOptions({
148148

149149
return (
150150
<div className={cn('border-[var(--border)] border-b px-4 py-2.5', search && 'md:px-6')}>
151-
<div className='flex flex-wrap items-center gap-y-2 md:flex-nowrap'>
151+
<div className='flex flex-wrap items-center gap-y-2 max-md:gap-x-1 md:flex-nowrap'>
152152
{search && <SearchSection search={search} />}
153153
{/* `ml-auto` moves to `trailing` when present so the menu cluster stays put
154154
and only the trailing action is pushed to the far edge. */}
155155
<div
156156
className={cn(
157157
'flex min-w-0 flex-wrap items-center gap-1.5 md:shrink-0 md:flex-nowrap',
158+
search && 'max-md:contents',
158159
search && !trailing && 'ml-auto'
159160
)}
160161
>
161-
{aside}
162-
<div className='flex min-w-0 flex-wrap items-center gap-1 md:flex-nowrap'>
163-
{filterTags?.map((tag) => (
164-
<Chip key={tag.label} rightIcon={X} onClick={tag.onRemove}>
165-
{tag.label}
166-
</Chip>
167-
))}
162+
{aside && (
163+
<div
164+
className={cn(
165+
'flex min-w-0 flex-wrap items-center gap-1.5 md:flex-nowrap',
166+
search && 'max-md:order-last max-md:basis-full'
167+
)}
168+
>
169+
{aside}
170+
</div>
171+
)}
172+
<div
173+
className={cn(
174+
'flex min-w-0 flex-wrap items-center gap-1 md:flex-nowrap',
175+
search && 'max-md:contents'
176+
)}
177+
>
178+
{Boolean(filterTags?.length) && (
179+
<div
180+
className={cn(
181+
'flex min-w-0 flex-wrap items-center gap-1 md:flex-nowrap',
182+
search && 'max-md:order-last max-md:basis-full'
183+
)}
184+
>
185+
{filterTags?.map((tag) => (
186+
<Chip
187+
key={tag.label}
188+
rightIcon={X}
189+
onClick={tag.onRemove}
190+
className='max-md:max-w-full'
191+
>
192+
{tag.label}
193+
</Chip>
194+
))}
195+
</div>
196+
)}
168197
{isToggleFilter && filter.mode === 'toggle' ? (
169198
<Chip
170199
active={filter.active}
@@ -184,7 +213,7 @@ export const ResourceOptions = memo(function ResourceOptions({
184213
}
185214
>
186215
<PopoverPrimitive.Anchor asChild>
187-
<div className='flex items-center gap-1'>
216+
<div className='flex shrink-0 items-center gap-1'>
188217
<PopoverPrimitive.Trigger asChild>
189218
<Chip active={popoverFilter.active} leftIcon={ListFilter} mobileIconOnly>
190219
Filter
@@ -231,7 +260,7 @@ export const ResourceOptions = memo(function ResourceOptions({
231260

232261
const SearchSection = memo(function SearchSection({ search }: { search: SearchConfig }) {
233262
return (
234-
<div className='relative flex min-w-0 basis-full items-center gap-1.5 md:flex-1 md:basis-auto'>
263+
<div className='relative flex min-w-0 flex-1 items-center gap-1.5'>
235264
{SEARCH_ICON}
236265
<div className='flex min-w-0 flex-1 items-center gap-1.5 overflow-x-auto [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'>
237266
{search.tags?.map((tag, i) => (
@@ -263,7 +292,7 @@ const SearchSection = memo(function SearchSection({ search }: { search: SearchCo
263292
<button
264293
type='button'
265294
aria-label='Clear search'
266-
className='mr-0.5 flex size-[14px] shrink-0 items-center justify-center text-[var(--text-muted)] transition-colors hover-hover:text-[var(--text-body)]'
295+
className='mr-0.5 flex size-[14px] shrink-0 items-center justify-center text-[var(--text-muted)] transition-colors hover-hover:text-[var(--text-body)] max-md:size-11'
267296
onClick={search.onClearAll ?? (() => search.onChange(''))}
268297
>
269298
<X className='size-[12px]' />

‎apps/sim/app/workspace/[workspaceId]/components/resource/resource.tsx‎

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,10 @@ function buildGridTemplateColumns(columns: ResourceColumn[], hasCheckbox: boolea
156156
const weights = columns.map(
157157
(col, colIdx) => (colIdx === 0 ? 2.5 : 1.0) * (col.widthMultiplier ?? 1)
158158
)
159-
const total = weights.reduce((s, w) => s + w, 0)
160-
const tracks = columns.map((_, colIdx) => `minmax(0, ${(weights[colIdx] / total).toFixed(6)}fr)`)
159+
const tracks = columns.map((_, colIdx) => {
160+
const track = `minmax(0, ${weights[colIdx]}fr)`
161+
return colIdx === 0 ? `var(--resource-table-name-width, ${track})` : track
162+
})
161163
return hasCheckbox ? `${CHECKBOX_COLUMN_WIDTH} ${tracks.join(' ')}` : tracks.join(' ')
162164
}
163165

@@ -381,7 +383,7 @@ const ResourceTable = memo(function ResourceTable({
381383
<div
382384
ref={scrollRef}
383385
className={cn(
384-
'min-h-0 flex-1 overflow-auto overscroll-none',
386+
'@container/resource-table min-h-0 flex-1 overflow-auto overscroll-none',
385387
showEmptyState && 'flex flex-col'
386388
)}
387389
onDragOver={bodyDrop?.onDragOver}
@@ -390,10 +392,11 @@ const ResourceTable = memo(function ResourceTable({
390392
>
391393
<div
392394
role='table'
393-
className='grid w-full min-w-[var(--resource-table-min-width)] text-small md:min-w-0'
395+
className='grid w-full min-w-[var(--resource-table-min-width)] text-small md:min-w-0 max-md:[--resource-table-name-width:calc(100cqw_-_var(--resource-table-selection-width))]'
394396
style={
395397
{
396-
'--resource-table-min-width': `${Math.max(360, columns.length * 136 + 120 + (hasCheckbox ? 52 : 0))}px`,
398+
'--resource-table-min-width': `calc(100cqw + ${Math.max(0, columns.length - 1) * 136}px)`,
399+
'--resource-table-selection-width': hasCheckbox ? CHECKBOX_COLUMN_WIDTH : '0px',
397400
} as CSSProperties
398401
}
399402
>
@@ -405,22 +408,23 @@ const ResourceTable = memo(function ResourceTable({
405408
{hasCheckbox && (
406409
<div
407410
role='columnheader'
408-
className='flex h-10 items-center py-1.5 pr-0 pl-5 text-left'
411+
className='flex h-10 items-center py-1.5 pr-0 pl-5 text-left max-md:h-11'
409412
>
410413
<Checkbox
411414
size='sm'
412415
checked={selectable.isAllSelected}
413416
onCheckedChange={handleSelectAll}
414417
disabled={selectable.disabled}
415418
aria-label='Select all'
419+
className='max-md:before:inset-[-15px]'
416420
/>
417421
</div>
418422
)}
419423
{columns.map((col) => (
420424
<div
421425
key={col.id}
422426
role='columnheader'
423-
className='flex h-10 min-w-0 items-center px-3 py-1.5 text-left font-normal text-[var(--text-muted)] text-small md:px-6'
427+
className='flex h-10 min-w-0 items-center px-3 py-1.5 text-left font-normal text-[var(--text-muted)] text-small max-md:h-11 md:px-6'
424428
>
425429
<span className='min-w-0 truncate'>{col.header}</span>
426430
</div>
@@ -775,6 +779,7 @@ const DataRow = memo(function DataRow({
775779
onCheckedChange={handleSelectRow}
776780
disabled={selectable.disabled}
777781
aria-label='Select row'
782+
className='max-md:before:inset-[-15px]'
778783
onClick={handleSelectRowClick}
779784
/>
780785
</div>
@@ -787,17 +792,6 @@ const DataRow = memo(function DataRow({
787792
role='cell'
788793
className='flex min-h-11 min-w-0 items-center gap-1 px-3 pointer-coarse:py-0 py-0 md:min-h-0 md:px-6 md:py-2.5'
789794
>
790-
{index === 0 && onRowContextMenu && (
791-
<Chip
792-
leftIcon={MoreHorizontal}
793-
aria-label={cell?.label ? `Actions for ${cell.label}` : 'Row actions'}
794-
className='pointer-coarse:flex hidden size-11 shrink-0 justify-center max-md:flex'
795-
onClick={(event) => {
796-
event.stopPropagation()
797-
onRowContextMenu(event, row.id)
798-
}}
799-
/>
800-
)}
801795
<CellContent
802796
icon={cell?.icon}
803797
label={cell?.label || EMPTY_CELL_PLACEHOLDER}
@@ -806,6 +800,20 @@ const DataRow = memo(function DataRow({
806800
pinned={cell?.pinned}
807801
highlight={cell?.highlight}
808802
/>
803+
{index === 0 && onRowContextMenu && (
804+
<div className='ml-auto pointer-coarse:flex hidden shrink-0 max-md:flex'>
805+
<Chip
806+
leftIcon={MoreHorizontal}
807+
mobileIconOnly
808+
aria-label={cell?.label ? `Actions for ${cell.label}` : 'Row actions'}
809+
className='size-11 justify-center'
810+
onClick={(event) => {
811+
event.stopPropagation()
812+
onRowContextMenu(event, row.id)
813+
}}
814+
/>
815+
</div>
816+
)}
809817
</div>
810818
)
811819
})}

‎apps/sim/app/workspace/[workspaceId]/components/workspace-chrome/workspace-chrome.tsx‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,6 @@ export function WorkspaceChrome({
328328
aria-expanded={mobileNavigationOpen}
329329
aria-controls={navigationId}
330330
onClick={handleSidebarToggle}
331-
className='min-h-11'
332331
>
333332
{mobileNavigationOpen ? 'Close navigation' : 'Navigation'}
334333
</Chip>

‎apps/sim/app/workspace/[workspaceId]/home/components/chat-panel-layout.tsx‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,11 @@ export function ChatPanelLayout({
7070
)}
7171
{panel}
7272
<div
73-
className={cn('z-30', RESOURCE_HEADER_CLASSES.overlay, RESOURCE_HEADER_CLASSES.endPosition)}
73+
className={cn(
74+
'z-30 max-md:w-[var(--resource-header-toggle-hit-size)] max-md:justify-center',
75+
RESOURCE_HEADER_CLASSES.overlay,
76+
RESOURCE_HEADER_CLASSES.endPosition
77+
)}
7478
>
7579
<Button
7680
variant='ghost'

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ export const RESOURCE_TAB_ICON_CLASS = 'size-[16px] text-[var(--text-icon)]'
33
/** Shared geometry for the resource header and controls positioned over it. */
44
export const RESOURCE_HEADER_CLASSES = {
55
layout:
6-
'[--resource-header-controls-height:40px] [--resource-header-end-inset:16px] [--resource-header-fixed-reserve:52px] [--resource-header-toggle-hit-size:40px] [--resource-header-toggle-size:30px] max-md:[--resource-header-controls-height:44px] max-md:[--resource-header-end-inset:8px] max-md:[--resource-header-toggle-hit-size:44px] max-md:[--resource-header-toggle-size:44px]',
6+
'[--resource-header-controls-height:40px] [--resource-header-end-inset:16px] [--resource-header-fixed-reserve:52px] [--resource-header-toggle-hit-size:40px] [--resource-header-toggle-size:30px] max-md:[--resource-header-controls-height:44px] max-md:[--resource-header-end-inset:8px] max-md:[--resource-header-toggle-hit-size:44px]',
77
/**
88
* Drives the tab strip from this header's own tokens rather than restating the
99
* strip's defaults, so the height the overlaid controls below are positioned

‎packages/emcn/src/components/chip/chip.tsx‎

Lines changed: 63 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,23 @@ const chipVariants = cva(
8989
type ChipIcon = ComponentType<{ className?: string }>
9090

9191
const CHIP_MOBILE_ICON_ONLY_CLASS =
92-
'max-md:size-11 max-md:shrink-0 max-md:justify-center max-md:gap-0 max-md:px-0'
92+
"max-md:relative max-md:size-[30px] max-md:shrink-0 max-md:justify-center max-md:gap-0 max-md:px-0 max-md:pointer-coarse:min-h-0 max-md:pointer-coarse:min-w-0 max-md:after:absolute max-md:after:top-1/2 max-md:after:left-1/2 max-md:after:size-11 max-md:after:-translate-x-1/2 max-md:after:-translate-y-1/2 max-md:after:content-['']"
93+
94+
interface ChipTouchTargetProps {
95+
compact: boolean
96+
children: ReactNode
97+
}
98+
99+
/** Reserves the expanded target without enlarging the chip's visible surface. */
100+
function ChipTouchTarget({ compact, children }: ChipTouchTargetProps) {
101+
return compact ? (
102+
<span className='contents max-md:inline-flex max-md:size-11 max-md:shrink-0 max-md:items-center max-md:justify-center'>
103+
{children}
104+
</span>
105+
) : (
106+
children
107+
)
108+
}
93109

94110
/**
95111
* Variants a `Chip`/`ChipLink` may render. The `default` (bare) chip is implicit
@@ -109,7 +125,7 @@ interface ChipBaseProps extends Omit<VariantProps<typeof chipVariants>, 'variant
109125
rightIcon?: ChipIcon
110126
/** Custom content rendered after the label, such as a spinning loader. Takes precedence over `rightIcon`. */
111127
rightAdornment?: ReactNode
112-
/** Below the desktop breakpoint, show the icon or adornment in a 44px square and retain the label for assistive technology. Requires a glyph and label. */
128+
/** Below the desktop breakpoint, show a 30px icon face with a reserved 44px hit target. Requires a glyph and a visible or accessible label. */
113129
mobileIconOnly?: boolean
114130
children?: ReactNode
115131
}
@@ -180,31 +196,32 @@ const Chip = forwardRef<HTMLButtonElement, ChipProps>(function Chip(
180196
const compactOnMobile = Boolean(
181197
mobileIconOnly &&
182198
(leftIcon || rightIcon || leftAdornment || rightAdornment) &&
183-
children != null &&
184-
children !== false
199+
((children != null && children !== false) || props['aria-label'] || props['aria-labelledby'])
185200
)
186201
return (
187-
<button
188-
ref={ref}
189-
type={type ?? 'button'}
190-
className={cn(
191-
chipVariants({ variant, shape, active, fullWidth }),
192-
compactOnMobile && CHIP_MOBILE_ICON_ONLY_CLASS,
193-
className
194-
)}
195-
{...props}
196-
>
197-
<ChipContent
198-
variant={variant}
199-
leftIcon={leftIcon}
200-
leftAdornment={leftAdornment}
201-
rightIcon={rightIcon}
202-
rightAdornment={rightAdornment}
203-
mobileIconOnly={compactOnMobile}
202+
<ChipTouchTarget compact={compactOnMobile}>
203+
<button
204+
ref={ref}
205+
type={type ?? 'button'}
206+
className={cn(
207+
chipVariants({ variant, shape, active, fullWidth }),
208+
compactOnMobile && CHIP_MOBILE_ICON_ONLY_CLASS,
209+
className
210+
)}
211+
{...props}
204212
>
205-
{children}
206-
</ChipContent>
207-
</button>
213+
<ChipContent
214+
variant={variant}
215+
leftIcon={leftIcon}
216+
leftAdornment={leftAdornment}
217+
rightIcon={rightIcon}
218+
rightAdornment={rightAdornment}
219+
mobileIconOnly={compactOnMobile}
220+
>
221+
{children}
222+
</ChipContent>
223+
</button>
224+
</ChipTouchTarget>
208225
)
209226
})
210227

@@ -236,30 +253,31 @@ const ChipLink = forwardRef<HTMLAnchorElement, ChipLinkProps>(function ChipLink(
236253
const compactOnMobile = Boolean(
237254
mobileIconOnly &&
238255
(leftIcon || rightIcon || leftAdornment || rightAdornment) &&
239-
children != null &&
240-
children !== false
256+
((children != null && children !== false) || props['aria-label'] || props['aria-labelledby'])
241257
)
242258
return (
243-
<Link
244-
ref={ref}
245-
className={cn(
246-
chipVariants({ variant, shape, active, fullWidth }),
247-
compactOnMobile && CHIP_MOBILE_ICON_ONLY_CLASS,
248-
className
249-
)}
250-
{...props}
251-
>
252-
<ChipContent
253-
variant={variant}
254-
leftIcon={leftIcon}
255-
leftAdornment={leftAdornment}
256-
rightIcon={rightIcon}
257-
rightAdornment={rightAdornment}
258-
mobileIconOnly={compactOnMobile}
259+
<ChipTouchTarget compact={compactOnMobile}>
260+
<Link
261+
ref={ref}
262+
className={cn(
263+
chipVariants({ variant, shape, active, fullWidth }),
264+
compactOnMobile && CHIP_MOBILE_ICON_ONLY_CLASS,
265+
className
266+
)}
267+
{...props}
259268
>
260-
{children}
261-
</ChipContent>
262-
</Link>
269+
<ChipContent
270+
variant={variant}
271+
leftIcon={leftIcon}
272+
leftAdornment={leftAdornment}
273+
rightIcon={rightIcon}
274+
rightAdornment={rightAdornment}
275+
mobileIconOnly={compactOnMobile}
276+
>
277+
{children}
278+
</ChipContent>
279+
</Link>
280+
</ChipTouchTarget>
263281
)
264282
})
265283

0 commit comments

Comments
 (0)