fix: pin table actions column so it stays visible on narrow screens (#10301)

* fix: pin table actions column so it stays visible on narrow screens

Wide DataTables (folders, variables, resources) scroll horizontally on
small screens, pushing the trailing per-row actions column (the ⋯ menu,
Edit/Delete, and folders' "Publish to Hub") off the right edge where it
was effectively unreachable.

Add an opt-in `stickyEnd` prop to Cell that pins a column to the right of
the scroll container with an opaque background and a left divider, and
apply it to the actions column (header + body) on the folders, variables,
and resources pages.

The background is opaque (bg-surface / bg-surface-secondary) rather than
the row's translucent hover tint, so cells sliding under the pinned column
are occluded instead of bleeding through.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: keep resources actions cell as table-cell so sticky pins correctly

The workspace resources actions cell used class="flex justify-end" on the
Cell, which forces the <td> to display:flex. A flex box inside a table row
is wrapped in an anonymous table-cell, so position:sticky on it is
constrained to that wrapper and no longer pins to the scrollport — the
header stayed pinned while the row actions scrolled away.

Move the flex layout to an inner <div> so the <td> keeps display:table-cell
and the stickyEnd pin works.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix: address review nits on pinned actions column

- resources Workspace table: add `last` to the body actions cell so its
  right padding (sm:pr-6) matches the header and the other tables.
- variables table: isolate the refresh-error ping indicator's stacking
  context so its z-50 can't paint over a sticky-pinned actions column
  scrolling past it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ruben Fiszel
2026-07-24 13:37:21 +02:00
committed by GitHub
parent 010059a449
commit 75acf7207b
4 changed files with 93 additions and 80 deletions
@@ -11,6 +11,7 @@
shouldStopPropagation?: boolean
selected?: boolean
sticky?: boolean
stickyEnd?: boolean
wrap?: boolean
children?: import('svelte').Snippet
[key: string]: any
@@ -24,6 +25,7 @@
shouldStopPropagation = false,
selected = false,
sticky = false,
stickyEnd = false,
wrap = false,
children,
...rest
@@ -53,6 +55,11 @@
last && size === 'xs' ? 'sm:pr-3' : '',
numeric ? 'text-right' : '',
// Pin an actions column to the right so it stays visible when a wide table
// scrolls horizontally. The background must be opaque so cells sliding under it
// are occluded — the row's hover tint is translucent and would bleed through.
stickyEnd ? 'sticky right-0 border-l' : '',
stickyEnd ? (head ? 'bg-surface-secondary' : 'bg-surface') : '',
sticky ? `!p-0 sticky ${first ? 'left-0' : 'right-0'}` : 'px-2 py-2',
size === 'sm' ? 'px-1.5 py-2.5' : '',
size === 'lg' ? 'px-3 py-4' : '',
@@ -180,7 +180,7 @@
<Cell head class="w-20">Variables</Cell>
<Cell head class="w-20">Resources</Cell>
<Cell head class="w-20">Participants</Cell>
<Cell head last />
<Cell head last stickyEnd />
</tr>
</Head>
<tbody class="divide-y">
@@ -242,7 +242,7 @@
<FolderUsageInfo {name} tabular />
<Cell><FolderInfo members={computeMembers(owners, extra_perms)} /></Cell>
<Cell shouldStopPropagation>
<Cell last stickyEnd shouldStopPropagation>
<Dropdown
items={[
{
@@ -1008,7 +1008,7 @@
<Cell head>Resource type</Cell>
<Cell head>Description</Cell>
<Cell head />
<Cell head last />
<Cell head last stickyEnd />
</Row>
</Head>
<tbody class="divide-y bg-surface">
@@ -1176,83 +1176,85 @@
{/if}
</div>
</Cell>
<Cell class="flex justify-end">
{#if path && assetCanBeExplored({ kind: 'resource', path }, { resource_type }) && !$userStore?.operator}
<ExploreAssetButton
asset={{ kind: 'resource', path }}
_resourceMetadata={{ resource_type }}
class="w-24"
/>
{/if}
<Dropdown
class="w-fit"
items={[
{
displayName: 'Permissions',
icon: Shield,
action: () => {
shareModal?.openDrawer?.(path, 'resource')
}
},
{
displayName: 'Edit',
icon: Pen,
disabled: !canWrite || !showCreateButtons,
action: () => {
resourceEditor?.initEdit?.(path)
}
},
...(!ws_specific && isDeployable('resource', path, deployUiSettings)
? [
{
displayName: 'Deploy to prod/staging',
icon: FileUp,
action: () => {
deploymentDrawer?.openDrawer(path, 'resource')
<Cell last stickyEnd>
<div class="flex justify-end">
{#if path && assetCanBeExplored({ kind: 'resource', path }, { resource_type }) && !$userStore?.operator}
<ExploreAssetButton
asset={{ kind: 'resource', path }}
_resourceMetadata={{ resource_type }}
class="w-24"
/>
{/if}
<Dropdown
class="w-fit"
items={[
{
displayName: 'Permissions',
icon: Shield,
action: () => {
shareModal?.openDrawer?.(path, 'resource')
}
},
{
displayName: 'Edit',
icon: Pen,
disabled: !canWrite || !showCreateButtons,
action: () => {
resourceEditor?.initEdit?.(path)
}
},
...(!ws_specific && isDeployable('resource', path, deployUiSettings)
? [
{
displayName: 'Deploy to prod/staging',
icon: FileUp,
action: () => {
deploymentDrawer?.openDrawer(path, 'resource')
}
}
}
]
: []),
{
displayName: 'Delete',
disabled: !canWrite || !showCreateButtons,
icon: Trash,
type: 'delete',
action: (event) => {
// TODO
// @ts-ignore
if (event?.shiftKey) {
deleteResource(path, account)
} else {
deleteIsLinked = is_linked ?? false
deleteConfirmedCallback = () => {
]
: []),
{
displayName: 'Delete',
disabled: !canWrite || !showCreateButtons,
icon: Trash,
type: 'delete',
action: (event) => {
// TODO
// @ts-ignore
if (event?.shiftKey) {
deleteResource(path, account)
} else {
deleteIsLinked = is_linked ?? false
deleteConfirmedCallback = () => {
deleteResource(path, account)
}
}
}
}
},
...(account != undefined
? [
{
displayName: 'Refresh token',
icon: RotateCw,
action: async () => {
await OauthService.refreshToken({
workspace: $workspaceStore ?? '',
id: account ?? 0,
requestBody: {
path
}
})
sendUserToast('Token refreshed')
loadResources()
},
...(account != undefined
? [
{
displayName: 'Refresh token',
icon: RotateCw,
action: async () => {
await OauthService.refreshToken({
workspace: $workspaceStore ?? '',
id: account ?? 0,
requestBody: {
path
}
})
sendUserToast('Token refreshed')
loadResources()
}
}
}
]
: [])
]}
/>
</Cell>
]
: [])
]}
/>
</div></Cell
>
</Row>
{/each}
{/if}
@@ -1280,7 +1282,7 @@
<Row>
<Cell head first>Name</Cell>
<Cell head>Description</Cell>
<Cell head last />
<Cell head last stickyEnd />
</Row>
</Head>
<tbody class="divide-y bg-surface">
@@ -1316,7 +1318,7 @@
{removeMarkdown(truncate(description ?? '', 200))}
</span>
</Cell>
<Cell last>
<Cell last stickyEnd>
{#if !canWrite}
<Badge>
Shared globally
@@ -362,7 +362,7 @@
<Cell head>Value</Cell>
<Cell head>Description</Cell>
<Cell head />
<Cell head last />
<Cell head last stickyEnd />
</tr>
</Head>
<tbody class="divide-y">
@@ -465,7 +465,11 @@
<div class="">
{#if refresh_error}
<Popover notClickable>
<div class="relative inline-flex justify-center items-center w-4 h-4">
<!-- isolate: confine the ping indicator's z-50 to a local stacking context
so it can't paint over a sticky-pinned actions column scrolling past it -->
<div
class="relative inline-flex justify-center items-center w-4 h-4 isolate"
>
<Circle
class="text-red-600 animate-ping absolute z-50 w-4 h-4 fill-current"
size={12}
@@ -514,7 +518,7 @@
{/if}
</div>
</Cell>
<Cell last shouldStopPropagation>
<Cell last stickyEnd shouldStopPropagation>
<Dropdown
items={() => {
let owner = isOwner(path, $userStore, $workspaceStore)